[
  {
    "path": ".gitattributes",
    "content": "* text=auto\n*.css linguist-vendored\n*.less linguist-vendored\n"
  },
  {
    "path": ".github/CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, gender identity and expression, level of experience,\neducation, socio-economic status, nationality, personal appearance, race,\nreligion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n  advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n  address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n### Project Maintainer Standards\n\nProject maintainers should generally follow these additional standards:\n\n* Avoid using a negative or harsh tone in communication, Even if the other party\nis being negative themselves.\n* When providing criticism, try to make it constructive to lead the other person\ndown the correct path.\n* Keep the [project definition](https://github.com/BookStackApp/BookStack#project-definition)\nin mind when deciding what's in scope of the Project.\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior. In addition, Project\nmaintainers are responsible for following the standards themselves.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at the email address shown on [the profile here](https://github.com/ssddanbrown). All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: [ssddanbrown]\nko_fi: ssddanbrown"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/api_request.yml",
    "content": "name: New API Endpoint or API Ability\ndescription: Request a new endpoint or API feature be added\nlabels: [\":nut_and_bolt: API Request\"]\nbody:\n  - type: textarea\n    id: feature\n    attributes:\n      label: API Endpoint or Feature\n      description: Clearly describe what you'd like to have added to the API.\n    validations:\n      required: true\n  - type: textarea\n    id: usecase\n    attributes:\n      label: Use-Case\n      description: Explain the use-case that you're working-on that requires the above request.\n    validations:\n      required: true\n  - type: textarea\n    id: context\n    attributes:\n      label: Additional context\n      description: Add any other context about the feature request here.\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug Report\ndescription: Create a report to help us fix bugs & issues in existing supported functionality\nlabels: [\":bug: Bug\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out a bug report!\n        Please note that this form is for reporting bugs in existing supported functionality.\n        \n        If you are reporting something that's not an issue in functionality we've previously supported and/or is simply something different to your expectations, then it may be more appropriate to raise via a feature or support request instead.\n  - type: textarea\n    id: description\n    attributes:\n      label: Describe the Bug\n      description: Provide a clear and concise description of what the bug is.\n    validations:\n      required: true\n  - type: textarea\n    id: reproduction\n    attributes:\n      label: Steps to Reproduce\n      description: Detail the steps that would replicate this issue.\n      placeholder: |\n        1. Go to '...'\n        2. Click on '....'\n        3. Scroll down to '....'\n        4. See error\n    validations:\n      required: true\n  - type: textarea\n    id: expected\n    attributes:\n      label: Expected Behaviour\n      description: Provide clear and concise description of what you expected to happen.\n    validations:\n      required: true\n  - type: textarea\n    id: context\n    attributes:\n      label: Screenshots or Additional Context\n      description: Provide any additional context and screenshots here to help us solve this issue.\n    validations:\n      required: false\n  - type: input\n    id: browserdetails\n    attributes:\n      label: Browser Details\n      description: |\n        If this is an issue that occurs when using the BookStack interface, please provide details of the browser used which presents the reported issue.\n      placeholder: (eg. Firefox 97 (64-bit) on Windows 11)\n    validations:\n      required: false\n  - type: input\n    id: bsversion\n    attributes:\n      label: Exact BookStack Version\n      description: This can be found in the settings view of BookStack. Please provide an exact version(s) you've tested on.\n      placeholder: (eg. v23.06.7)\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Discord Chat Support\n    url: https://discord.gg/ztkBqR2\n    about: Realtime support & chat with the BookStack community and the team.\n\n  - name: Debugging & Common Issues\n    url: https://www.bookstackapp.com/docs/admin/debugging/\n    about: Find details on how to debug issues and view common issues with their resolutions.\n\n  - name: Official Support Plans\n    url: https://www.bookstackapp.com/support/\n    about: View our official support plans that offer assured support for business."
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: Feature Request\ndescription: Request a new feature or idea to be added to BookStack\nlabels: [\":hammer: Feature Request\"]\nbody:\n  - type: textarea\n    id: description\n    attributes:\n      label: Describe the feature you'd like\n      description: Provide a clear description of the feature you'd like implemented in BookStack\n    validations:\n      required: true\n  - type: textarea\n    id: benefits\n    attributes:\n      label: Describe the benefits this would bring to existing BookStack users\n      description: |\n        Explain the measurable benefits this feature would achieve for existing BookStack users.\n        These benefits should details outcomes in terms of what this request solves/achieves, and should not be specific to implementation.\n        This helps us understand the core desired goal so that a variety of potential implementations could be explored.\n        This field is important. Lack if input here may lead to early issue closure.\n    validations:\n      required: true\n  - type: textarea\n    id: already_achieved\n    attributes:\n      label: Can the goal of this request already be achieved via other means?\n      description: |\n        Yes/No. If yes, please describe how the requested approach fits in with the existing method.\n    validations:\n      required: true\n  - type: checkboxes\n    id: confirm-search\n    attributes:\n      label: Have you searched for an existing open/closed issue?\n      description: |\n        To help us keep these issues under control, please ensure you have first [searched our issue list](https://github.com/BookStackApp/BookStack/issues?q=is%3Aissue) for any existing issues that cover the fundamental benefit/goal of your request.\n      options:\n        - label: I have searched for existing issues and none cover my fundamental request\n          required: true\n  - type: dropdown\n    id: existing_usage\n    attributes:\n      label: How long have you been using BookStack?\n      options:\n        - Not using yet, just scoping\n        - Under 3 months\n        - 3 months to 1 year\n        - 1 to 5 years\n        - Over 5 years\n    validations:\n      required: true\n  - type: textarea\n    id: context\n    attributes:\n      label: Additional context\n      description: Add any other context or screenshots about the feature request here.\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/language_request.yml",
    "content": "name: Language Request\ndescription: Request a new language to be added to Crowdin for you to translate\nlabels: [\":earth_africa: Translations\"]\nassignees:\n  - ssddanbrown\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for offering to help start a new translation for BookStack!\n  - type: input\n    id: language\n    attributes:\n      label: Language to Add\n      description: What language (and region if applicable) are you offering to help add to BookStack?\n    validations:\n      required: true\n  - type: checkboxes\n    id: confirm\n    attributes:\n      label: Confirmation of Intent\n      description: |\n        This issue template is to request a new language be added to our [Crowdin translation management project](https://crowdin.com/project/bookstack).\n        Please don't use this template to request a new language that you are not prepared to provide translations for.\n      options:\n        - label: I confirm I'm offering to help translate for this new language via Crowdin.\n          required: true\n  - type: markdown\n    attributes:\n      value: |\n        *__Note: New languages are added at specific points of the development process so it may be a small while before the requested language is added for translation.__*\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/support_request.yml",
    "content": "name: Support Request\ndescription: Request support for a specific problem you have not been able to solve yourself\nlabels: [\":dog2: Support\"]\nbody:\n  - type: checkboxes\n    id: useddocs\n    attributes:\n      label: Attempted Debugging\n      description: |\n        I have read the [BookStack debugging](https://www.bookstackapp.com/docs/admin/debugging/) page and seeked resolution or more\n        detail for the issue.\n      options:\n        - label: I have read the debugging page\n          required: true\n  - type: checkboxes\n    id: searchissue\n    attributes:\n      label: Searched GitHub Issues\n      description: |\n        I have searched for the issue and potential resolutions within the [project's GitHub issue list](https://github.com/BookStackApp/BookStack/issues)\n      options:\n        - label: I have searched GitHub for the issue.\n          required: true\n  - type: textarea\n    id: scenario\n    attributes:\n      label: Describe the Scenario\n      description: Detail the problem that you're having or what you need support with.\n    validations:\n      required: true\n  - type: input\n    id: bsversion\n    attributes:\n      label: Exact BookStack Version\n      description: This can be found in the settings view of BookStack. Please provide an exact version.\n      placeholder: (eg. v23.06.7)\n    validations:\n      required: true\n  - type: textarea\n    id: logs\n    attributes:\n      label: Log Content\n      description: If the issue has produced an error, provide any [BookStack or server log](https://www.bookstackapp.com/docs/admin/debugging/) content below.\n      placeholder: Be sure to remove any confidential details in your logs\n      render: text\n    validations:\n      required: false\n  - type: textarea\n    id: hosting\n    attributes:\n      label: Hosting Environment\n      description: Describe your hosting environment as much as possible including any proxies used (If applicable).\n      placeholder: (eg. PHP8.1 on Ubuntu 22.04 VPS, installed using official installation script)\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/z_blank_request.yml",
    "content": "name: Blank Request (Maintainers Only)\ndescription: For maintainers only - Start a blank request\nbody:\n  - type: markdown\n    attributes:\n      value: \"**This blank request option is only for existing official maintainers of the project!** Please instead use a different request option. If you use this your issue will be closed off.\"\n  - type: textarea\n    attributes:\n      label: Description"
  },
  {
    "path": ".github/SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nOnly the [latest version](https://github.com/BookStackApp/BookStack/releases) of BookStack is supported.\nWe generally don't support older versions of BookStack due to maintenance effort and\nsince we aim to provide a fairly stable upgrade path for new versions.\n\n## Security Notifications\n\nIf you'd like to be notified of new potential security concerns you can [sign-up to the BookStack security mailing list](https://updates.bookstackapp.com/signup/bookstack-security-updates).\n\n## Reporting a Vulnerability\n\nIf you've found an issue that likely has no impact to existing users (For example, in a development-only branch)\nfeel free to raise it via a standard GitHub bug report issue.\n\nIf the issue could have a security impact to BookStack instances, \nplease directly contact the lead maintainer [@ssddanbrown](https://github.com/ssddanbrown). \nYou will need to log in to be able to see the email address on the [GitHub profile page](https://github.com/ssddanbrown).\nAlternatively you can send a DM via Mastodon to [@danb@fosstodon.org](https://fosstodon.org/@danb).\n\nPlease be patient while the vulnerability is being reviewed. Deploying the fix to address the vulnerability\ncan often take a little time due to the amount of preparation required, to ensure the vulnerability has\nbeen covered, and to create the content required to adequately notify the user-base.\n\nThank you for keeping BookStack instances safe!\n"
  },
  {
    "path": ".github/translators.txt",
    "content": "Name :: Languages\n@robertlandes :: German\n@SergioMendolia :: French\n@NakaharaL :: Portuguese, Brazilian\n@ReeseSebastian :: German\n@arietimmerman :: Dutch\n@diegoseso :: Spanish\n@S64 :: Japanese\n@JachuPL :: Polish\n@Joorem :: French\n@timoschwarzer :: German\n@sanderdw :: Dutch\n@lbguilherme :: Portuguese, Brazilian\n@marcusforsberg :: Swedish\n@artur-trzesiok :: Polish\n@Alwaysin :: French\n@msaus :: Japanese\n@moucho :: Spanish\n@vriic :: German\n@DeehSlash :: Portuguese, Brazilian\n@alex2702 :: German\n@nicobubulle :: French\n@kmoj86 :: Arabic\n@houbaron :: Chinese Traditional; Chinese Simplified\n@mullinsmikey :: Russian\n@limkukhyun :: Korean\n@CliffyPrime :: German\n@kejjang :: Chinese Traditional\n@TheLastOperator :: French\n@qianmengnet :: Simplified Chinese\n@ezzra :: German; German Informal\n@vasiliev123 :: Polish\n@Mant1kor :: Ukrainian\n@Xiphoseer :: German; German Informal\n@maantje :: Dutch\n@cima :: Czech\n@agvol :: Russian\n@Hambern :: Swedish\n@NootoNooto :: Dutch\n@kostefun :: Russian\n@lucaguindani :: French\n@miles75 :: Hungarian\n@danielroehrig-mm :: German\n@oykenfurkan :: Turkish\n@qligier :: French\n@johnroyer :: Traditional Chinese\n@artskoczylas :: Polish\n@dellamina :: Italian\n@jzoy :: Simplified Chinese\n@ististudio :: Korean\n@leomartinez :: Spanish Argentina\n@geins :: German\n@Ereza :: Catalan\n@benediktvolke :: German\n@Baptistou :: French\n@arcoai :: Spanish\n@Jokuna :: Korean\n@smartshogu :: German; German Informal\n@samadha56 :: Persian\n@mrmuminov :: Uzbek\ncipi1965 :: Italian\nMykola Ronik (Mantikor) :: Ukrainian\nfurkanoyk :: Turkish\nm0uch0 :: Spanish\nMaxim Zalata (zlatin) :: Russian; Ukrainian\nnutsflag :: French\nLeonardo Mario Martinez (leonardo.m.martinez) :: Spanish, Argentina\nRodrigo Saczuk Niz (rodrigoniz) :: Portuguese, Brazilian\n叫钦叔就好 (254351722) :: Chinese Traditional; Chinese Simplified\naekramer :: Dutch\nJachuPL :: Polish\nmilesteg :: Hungarian\nBeenbag :: German; German Informal\nLett3rs :: Danish\nJulian (julian.henneberg) :: German; German Informal\n3GNWn :: Danish\ndbguichu :: Chinese Simplified\nRandy Kim (hyunjun) :: Korean\nFrancesco M. Taurino (ftaurino) :: Italian\nDanielFrederiksen :: Danish\nFinn Wessel (19finnwessel6) :: German Informal; German\nGustav Kånåhols (Kurbitz) :: Swedish\nVuong Trung Hieu (fpooon) :: Vietnamese\nEmil Petersen (emoyly) :: Danish\nmrjaboozy :: Slovenian\nStatium :: Russian\nMikkel Struntze (MStruntze) :: Danish\nkostefun :: Russian\nTuyen.NG (tuyendev) :: Vietnamese\nGhost_chu (dbguichu) :: Chinese Simplified\nZiipen :: Danish\nSamuel Schwarz (Guiph7quan) :: Czech\nAleph (toishoki) :: Turkish\nJulio Alberto García (Yllelder) :: Spanish\nRafael (raribeir) :: Portuguese, Brazilian\nHiroyuki Odake (dakesan) :: Japanese\nAlex Lee (qianmengnet) :: Chinese Simplified\nswinn37 :: French\nHasan Özbey (the-turk) :: Turkish\nrcy :: Swedish\nAli Yasir Yılmaz (ayyilmaz) :: Turkish\nscureza :: Italian\nBiepa :: German Informal; German\nsyecu :: Chinese Simplified\nLap1t0r :: French\nThinkverse (thinkverse) :: Swedish\nalef (toishoki) :: Turkish\nRobbert Feunekes (Muukuro) :: Dutch\nseohyeon.joo :: Korean\nOrenda (OREDNA) :: Bulgarian\nMarek Pavelka (marapavelka) :: Czech\nVenkinovec :: Czech\nTommy Ku (tommyku) :: Chinese Traditional; Japanese\nMichał Bielejewski  (bielej) :: Polish\njozefrebjak :: Slovak\nIkhwan Koo (Ikhwan.Koo) :: Korean\nWhay (remkovdhoef) :: Dutch\njc7115 :: Chinese Traditional\n주서현 (seohyeon.joo) :: Korean\nReadySystems :: Arabic\nHFinch :: German; German Informal\nbrechtgijsens :: Dutch\nLowkey (v587ygq) :: Chinese Simplified\nsdl-blue :: German Informal\nsqlik :: Polish\nRoy van Schaijk (royvanschaijk) :: Dutch\nSimsimpicpic :: French\nZenahr Barzani (Zenahr) :: German; Japanese; Dutch; German Informal\ntatsuya.info :: Japanese\nfadiapp :: Arabic\nJakub Bouček (jakubboucek) :: Czech\nMarco (cdrfun) :: German; German Informal\n10935336 :: Chinese Simplified\n孟繁阳 (FanyangMeng) :: Chinese Simplified\nAndrej Močan (andrejm) :: Slovenian\ngilane9_ :: Arabic\nRaed alnahdi (raednahdi) :: Arabic\nXiphoseer :: German\nMerlinSVK (merlinsvk) :: Slovak\nKauê Sena (kaue.sena.ks) :: Portuguese, Brazilian\nMatthieuParis :: French\nDouradinho :: Portuguese, Brazilian; Portuguese\nGaku Yaguchi (tama11) :: Japanese\nZero Huang (johnroyer) :: Chinese Traditional\njackaaa :: Chinese Traditional\nIrfan Hukama Arsyad (IrfanArsyad) :: Indonesian\nJeff Huang (s8321414) :: Chinese Traditional\nLuís Tiago Favas (starkyller) :: Portuguese\nsemirte :: Bosnian\naarchijs :: Latvian\nMartins Pilsetnieks (pilsetnieks) :: Latvian\nYonatan Magier (yonatanmgr) :: Hebrew\nFastHogi :: German Informal; German\nOle Anders (Swoy) :: Norwegian Bokmal\nAtlochowski (atlochowski) :: Polish\nSimon (DefaultSimon) :: Slovenian\nReinis Mednis (Mednis) :: Latvian\ntoisho (toishoki) :: Turkish\nnikservik :: Ukrainian; Russian; Polish\nHenrijsS :: Latvian\nPascal R-B (pborgner) :: German\nBoris (Ginfred) :: Russian\nJonas Anker Rasmussen (jonasanker) :: Danish\nGerwin de Keijzer (gdekeijzer) :: Dutch; German Informal; German\nkometchtech :: Japanese\nAuri (Atalonica) :: Catalan\nFrancesco Franchina (ffranchina) :: Italian\nAimrane Kds (aimrane.kds) :: Arabic\nwhenwesober :: Indonesian\nRem (remkovdhoef) :: Dutch\nsyn7ax69 :: Bulgarian; Turkish; German\nBlaade :: French\nBehzad HosseinPoor (behzad.hp) :: Persian\nOle Aldric (Swoy) :: Norwegian Bokmal\nfharis arabia (raednahdi) :: Arabic\nAlexander Predl (Harveyhase68) :: German\nRem (Rem9000) :: Dutch\nMichał Stelmach (stelmach-web) :: Polish\narniom :: French\nREMOVED_USER :: French; German; Dutch; Portuguese, Brazilian; Portuguese; Turkish; \n林祖年 (contagion) :: Chinese Traditional\nSiamak Guodarzi (siamakgoudarzi88) :: Persian\nLis Maestrelo (lismtrl) :: Portuguese, Brazilian\nNathanaël (nathanaelhoun) :: French\nA Ibnu Hibban (abd.ibnuhibban) :: Indonesian\nFrost-ZX :: Chinese Simplified\nKuzma Simonov (ovmach) :: Russian\nVojtěch Krystek (acantophis) :: Czech\nMichał Lipok (mLipok) :: Polish\nNicolas Pawlak (Mikolajek) :: French; Polish; German\nThomas Hansen (thomasdk81) :: Danish\nHl2run :: Slovak\nNgo Tri Hoai (trihoai) :: Vietnamese\nAtalonica :: Catalan\n慕容潭谈 (591442386) :: Chinese Simplified\nRadim Pesek (ramess18) :: Czech\nanastasiia.motylko :: Ukrainian\nIndrek Haav (IndrekHaav) :: Estonian\nna3shkw :: Japanese\nGiancarlo Di Massa (digitall-it) :: Italian\nM Nafis Al Mukhdi (mnafisalmukhdi1) :: Indonesian\nsulfo :: Danish\nRaukze :: German\nzygimantus :: Lithuanian\nmarinkaberg :: Russian\nVitaliy (gviabcua) :: Ukrainian\nmannycarreiro :: Portuguese\nThiago Rafael Pereira de Carvalho (thiago.rafael) :: Portuguese, Brazilian\nKen Roger Bolgnes (kenbo124) :: Norwegian Bokmal\nNguyen Hung Phuong (hnwolf) :: Vietnamese\nUmut ERGENE (umutergene67) :: Turkish\nTomáš Batelka (Vofy) :: Czech\nMundo Racional (ismael.mesquita) :: Portuguese, Brazilian\nZarik (3apuk) :: Russian\nAli Shaatani (a.shaatani) :: Arabic\nChacMaster :: Portuguese, Brazilian\nSaeed (saeed205) :: Persian\nJulesdevops :: French\npeter cerny (posli.to.semka) :: Slovak\nPavel Karlin (pavelkarlin) :: Russian\nSmokingCrop :: Dutch\nMaciej Lebiest (Szwendacz) :: Polish\nDiscordDigital :: German; German Informal\nGábor Marton (dodver) :: Hungarian\nJakob Åsell (Jasell) :: Swedish\nGhost_chu (ghostchu) :: Chinese Simplified\nRavid Shachar (ravidshachar) :: Hebrew\nHelga Guchshenskaya (guchshenskaya) :: Russian\ndaniel chou (chou0214) :: Chinese Traditional\nManolis PATRIARCHE (m.patriarche) :: French\nMohammed Haboubi (haboubi92) :: Arabic\nroncallyt :: Portuguese, Brazilian\ngoegol :: Dutch\nmsevgen :: Turkish\nKhroners :: French\nMASOUD HOSSEINY (masoudme) :: Persian\nThomerson Roncally (roncallyt) :: Portuguese, Brazilian\nmetaarch :: Bulgarian\nXabi (xabikip) :: Basque\npedromcsousa :: Portuguese\nNir Louk (looknear) :: Hebrew\nAlex (qianmengnet) :: Chinese Simplified\nstothew :: German\nsgenc :: Turkish\nShukrullo (vodiylik) :: Uzbek\nWilliam W. (Nevnt) :: Chinese Traditional\neamaro :: Portuguese\nYpsilon-dev :: Arabic\nHieu Vuong Trung (vuongtrunghieu) :: Vietnamese\nDavid Clubb (davidoclubb) :: Welsh\nwelles freire (wellesximenes) :: Portuguese, Brazilian\nMagnus Jensen (MagnusHJensen) :: Danish\nHesley Magno (hesleymagno) :: Portuguese, Brazilian\nÉric Gaspar (erga) :: French\nFr3shlama :: German\nDSR :: Spanish, Argentina\nAndrii Bodnar (andrii-bodnar) :: Ukrainian\nYounes el Anjri (younesea28) :: Dutch\nGuclu Ozturk (gucluoz) :: Turkish\nAtmis :: French\nredjack666 :: Chinese Traditional\nAshita007 :: Russian\nlihaorr :: Chinese Simplified\nMarcus Silber (marcus.silber82) :: German\nPellNet :: Croatian\nWinetradr :: German\nSebastian Klaus (sebklaus) :: German\nFilip Antala (AntalaFilip) :: Slovak\nmcgong (GongMingCai) :: Chinese Simplified; Chinese Traditional\nNanang Setia Budi (sefidananang) :: Indonesian\nАндрей Павлов (andrei.pavlov) :: Russian\nAlex Navarro (alex.n.navarro) :: Portuguese, Brazilian\nJihyeon Gim (PotatoGim) :: Korean\nMihai Ochian (soulstorm19) :: Romanian\nHeartCore :: German Informal; German\nsimon.pct :: French\nokaeiz :: Persian\nNaoto Ishikawa (na3shkw) :: Japanese\nsdhadi :: Persian\nDerLinkman (derlinkman) :: German; German Informal\nTurnArabic :: Arabic\nMartin Sebek (sebekmartin) :: Czech\nKuchinashi Hoshikawa (kuchinashi) :: Chinese Simplified\ndigilady :: Greek\nLinus (LinusOP) :: Swedish\nFelipe Cardoso (felipecardosoruff) :: Portuguese, Brazilian\nRandomUser0815 :: German Informal; German\nIsmael Mesquita (mesquitoliveira) :: Portuguese, Brazilian\n구인회 (laskdjlaskdj12) :: Korean\nLiZerui (CNLiZerui) :: Chinese Traditional\nFabrice Boyer (FabriceBoyer) :: French\nmikael (bitcanon) :: Swedish\nMatthias Mai (schnapsidee) :: German Informal; German\nUfuk Ayyıldız (ufukayyildiz) :: Turkish\nJan Mitrof (jan.kachlik) :: Czech\nedwardsmirnov :: Russian\nMr_OSS117 :: French\nshotu :: French\nCesar_Lopez_Aguillon :: Spanish\nbdewoop :: German\ndina davoudi (dina.davoudi) :: Persian\nAngelos Chouvardas (achouvardas) :: Greek\nrndrss :: Portuguese, Brazilian\nrirac294 :: Russian\nDavid Furman (thefourCraft) :: Hebrew\nPafzedog :: French\nYllelder :: Spanish\nAdrian Ocneanu (aocneanu) :: Romanian\nEduardo Castanho (EduardoCastanho) :: Portuguese\nVIET NAM VPS (vietnamvps) :: Vietnamese\nm4tthi4s :: French\ntoras9000 :: Japanese\npathab :: German\nMichelSchoon85 :: Dutch\nJøran Haugli (haugli92) :: Norwegian Bokmal\nVasileios Kouvelis (VasilisKouvelis) :: Greek\nDremski :: Bulgarian\nFrédéric SENE (nothingfr) :: French\nbendem :: French\nkostasdizas :: Greek\nRicardo Schroeder (brownstone666) :: Portuguese, Brazilian\nEitan MG (EitanMG) :: Hebrew\nRobin Flikkema (RobinFlikkema) :: Dutch\nMichal Gurcik (mgurcik) :: Slovak\nPooyan Arab (pooyanarab) :: Persian\nOchi Darma Putra (troke12) :: Indonesian\nHsin-Hsiang Peng (Hsins) :: Chinese Traditional\nMosi  Wang (mosiwang) :: Chinese Traditional\n骆言 (LawssssCat) :: Chinese Simplified\nStickers Gaming Shøw (StickerSGSHOW) :: French\nLe Van Chinh (Chino) (lvanchinh86) :: Vietnamese\nRubens nagios (rubenix) :: Catalan\nPatrick Dantas (pa-tiq) :: Portuguese, Brazilian\nMichal (michalgurcik) :: Slovak\nNepomacs :: German\nRubens (rubenix) :: Catalan\nm4z :: German; German Informal\nTheRazvy :: Romanian\nYossi Zilber (lortens) :: Hebrew; Uzbek\ndesdinova :: French\nIngus Rūķis (ingus.rukis) :: Latvian\nEugene Pershin (SilentEugene) :: Russian\n周盛道 (zhoushengdao) :: Chinese Simplified\nhamidreza amini (hamidrezaamini2022) :: Persian\nTomislav Kraljević (tomislav.kraljevic) :: Croatian\nTaygun Yıldırım (yildirimtaygun) :: Turkish\nrobing29 :: German\nBruno Eduardo de Jesus Barroso (brunoejb) :: Portuguese, Brazilian\nIgor V Belousov (biv) :: Russian\nDavid Bauer (davbauer) :: German; German Informal\nGuttorm Hveem (guttormhveem) :: Norwegian Nynorsk; Norwegian Bokmal\nMinh Giang Truong (minhgiang1204) :: Vietnamese\nIoannis Ioannides (i.ioannides) :: Greek\nVadim (vadrozh) :: Russian\nFlip333 :: German Informal; German\nPaulo Henrique (paulohsantos114) :: Portuguese, Brazilian\nDženan (Dzenan) :: Swedish\nPéter Péli (peter.peli) :: Hungarian\nTWME :: Chinese Traditional\nSascha (Man-in-Black) :: German; German Informal\nMohammadreza Madadi (madadi.efl) :: Persian\nKonstantin (kkovacheli) :: Ukrainian; Russian\nlink1183 :: French\nRenan (rfpe) :: Portuguese, Brazilian\nLowkey (bbsweb) :: Chinese Simplified\nZZnOB (zznobzz) :: Russian\nrupus :: Swedish\ndevelopernecsys :: Norwegian Nynorsk\nxuan LI (xuanli233) :: Chinese Simplified\nLameeQS :: Latvian\nSorin T. (trimbitassorin) :: Romanian\npoesty :: Chinese Simplified\nbalmag :: Hungarian\nAntti-Jussi Nygård (ajnyga) :: Finnish\nEduard Ereza Martínez (Ereza) :: Catalan\nJabir Lang (amar.almrad) :: Arabic\nJaroslav Kobližek (foretix) :: Czech; French\nWiktor Adamczyk (adamczyk.wiktor) :: Polish\nAbdulmajeed Alshuaibi (4Majeed) :: Arabic\nNotSmartZakk :: Czech\nHyoungMin Lee (ddokkaebi) :: Korean\nDasferco :: Chinese Simplified\nMarcus Teräs (mteras) :: Finnish\nSerkan Yardim (serkanzz) :: Turkish\nY (cnsr) :: Ukrainian\nZY ZV (vy0b0x) :: Chinese Simplified\ndiegobenitez :: Spanish\nMarc Hagen (MarcHagen) :: Dutch\nKasper Alsøe (zeonos) :: Danish\nsultani :: Persian\nrenge :: Korean\nTim (thegatesdev) :: Dutch; German Informal; French; Romanian; Catalan; Czech; Danish; German; Finnish; Hungarian; Italian; Japanese; Korean; Polish; Russian; Ukrainian; Chinese Simplified; Chinese Traditional; Portuguese, Brazilian; Persian; Spanish, Argentina; Croatian; Norwegian Nynorsk; Estonian; Uzbek; Norwegian Bokmal\nIrdi (irdiOL) :: Albanian\nKateBarber :: Welsh\nTwister (theuncles75) :: Hebrew\nalgernon19 :: Hungarian\nIvan Krstic (ikrstic) :: Serbian (Cyrillic)\nShow :: Russian\nxBahamut :: Portuguese, Brazilian\nPavle Knežević (pavleknezzevic) :: Serbian (Cyrillic)\nVanja Cvelbar (b100w11) :: Slovenian\nsimonpct :: French\nHonza Nagy (honza.nagy) :: Czech\nasd20752 :: Norwegian Bokmal\nJan Picka (polipones) :: Czech\ndiogoalex991 :: Portuguese\nEhsan Sadeghi (ehsansadeghi) :: Persian\nka_picit :: Danish\ncracrayol :: French\nCapuaSC :: Dutch\nGuardian75 :: German Informal\nmr-kanister :: German\nMichele Bastianelli (makoblaster) :: Italian\njespernissen :: Danish\nAndrey (avmaksimov) :: Russian\nGonzalo Loyola (AlFcl) :: Spanish, Argentina; Spanish\ngrobert63 :: French\nwusst. (Supporti) :: German\nMaximMaximS :: Czech\ndamian-klima :: Slovak\ncrow_ :: Latvian\nJocelynDelalande :: French\nJan (JW-CH) :: German Informal\nTimo B (lommes) :: German Informal\nErik Lundstedt (Erik.Lundstedt) :: Swedish\nyngams (younessmouhid) :: Arabic\nOhadp :: Hebrew\ncbridi :: Portuguese, Brazilian\nnanangsb :: Indonesian\nMichal Melich (michalmelich) :: Czech\nDavid (david-prv) :: German; German Informal\nLarry (lahoje) :: Swedish\nMarcia dos Santos (marciab80) :: Portuguese\nRicard López Torres (richilpez.torres) :: Catalan\nsarahalves7 :: Portuguese, Brazilian\npetr.husak :: Czech\njavadataherian :: Persian\nLudo-code :: French\nhollsten :: Swedish\nNgoc Lan Phung (lanpncz) :: Vietnamese\nWorive :: Catalan; French\nИлья Скаба (skabailya) :: Russian\nIrjan Olsen (Irch) :: Norwegian Bokmal\nAleksandar Jovanovic (jovanoviczaleksandar) :: Serbian (Cyrillic)\nRed (RedVortex) :: Hebrew\nxgrug :: Chinese Simplified\nHrCalmar :: Danish\nAvishay Rapp (AvishayRapp) :: Hebrew\nmatthias4217 :: French\nBerke BOYLU2 (berkeboylu2) :: Turkish\netwas7B :: German\nMohammed srhiri (m.sghiri20) :: Arabic\nYongMin Kim (kym0118) :: Korean\nRivo Zängov (Eraser) :: Estonian\nFrancisco Rafael Fonseca (chicoraf) :: Portuguese, Brazilian\nИEØ_ΙΙØZ (NEO_IIOZ) :: Chinese Traditional\nmadnjpn (madnjpn.) :: Georgian\nÁsgeir Shiny Ásgeirsson (AsgeirShiny) :: Icelandic\nMohammad Aftab Uddin (chirohorit) :: Bengali\nYannis Karlaftis (meliseus) :: Greek\nfelixxx :: German Informal\nrandi (randi65535) :: Korean\ntest65428 :: Greek\nzeronell :: Chinese Simplified\njulien Vinber (julienVinber) :: French\nHyunwoo Park (oksure) :: Korean\naram.rafeq.7 (aramrafeq2) :: Kurdish\nRaphael Moreno (RaphaelMoreno) :: Portuguese, Brazilian\nyn (user99) :: Arabic\nPavel Zlatarov (pzlatarov) :: Bulgarian\ningelres :: French\nmabdullah :: Arabic\nSkrabák Csaba (kekcsi) :: Hungarian\nEvert Meulie (Evert) :: Norwegian Bokmal\nJasper Backer (jasperb) :: Dutch\nAlexandar Cavdarovski (ace.200112) :: Swedish\n구닥다리TV (yjj8353) :: Korean\nOnur Oskay (o.oskay) :: Turkish\nSébastien Merveille (SebastienMerv) :: French\nMaxim Kouznetsov (masya.work) :: Hebrew\nneodvisnost :: Slovenian\nSoubi Agatsuma (bisouya) :: Hebrew\nIlya Shaulov (ishaulov) :: Russian\nKonstantin Bobkov (b.konstantv) :: Russian\nRuben Sutter (rubensutter) :: German\njellium :: French\nQxlkdr :: Swedish\nHari (muhhari) :: Indonesian\n仙君御 (xjy) :: Chinese Simplified\nTapioM :: Finnish\nlingb58 :: Chinese Traditional\nAngel Pandey (angel-pandey) :: Nepali\nSupriya Shrestha (supriyashrestha) :: Nepali\ngprabhat :: Nepali\nCellCat :: Chinese Simplified\nAl Desrahim (aldesrahim) :: Indonesian\nahmad abbaspour (deshneh.dar.diss) :: Persian\nErjon K. (ekr) :: Albanian\nLiZerui (iamzrli) :: Chinese Traditional\nTicker (ticker.com) :: Hebrew\nCrazyComputer :: Chinese Simplified\nFirr (FirrV) :: Russian\nJoão Faro (FaroJoaoFaro) :: Portuguese\nDanilo dos Santos Barbosa (bozochegou) :: Portuguese, Brazilian\nChris (furesoft) :: German\nSilvia Isern (eiendragon) :: Catalan\nDennis Kron Pedersen (ahjdp) :: Danish\niamwhoiamwhoami :: Swedish\nGrogui :: French\nMrCharlesIII :: Arabic\nDavid Olsen (dawin) :: Danish\nltnzr :: French\nFrank Holler (holler.frank) :: German; German Informal\nKorab Arifi (korabidev) :: Albanian\nPetr Husák (petrhusak) :: Czech\nBernardo Maia (bernardo.bmaia2) :: Portuguese, Brazilian\nAmr (amr3k) :: Arabic\nTahsin Ahmed (tahsinahmed2012) :: Bengali\nbojan_che :: Serbian (Cyrillic)\nsetiawan setiawan (culture.setiawan) :: Indonesian\nDonald Mac Kenzie (kiuman) :: Norwegian Bokmal\nGabriel Silver (GabrielBSilver) :: Hebrew\nTomas Darius Davainis (Tomasdd) :: Lithuanian\nCriedHero :: Chinese Simplified\nHenrik (henrik2105) :: Norwegian Bokmal\nFoW (fofwisdom) :: Korean\nserinf-lauza :: French\nDiyan Nikolaev (nikolaev.diyan) :: Bulgarian\nShadluk Avan (quldosh) :: Uzbek\nMarci (MartonPoto) :: Hungarian\nMichał Sadurski (wheeskeey) :: Polish\nJanDziaslo :: Polish\nCharllys Fernandes (CharllysFernandes) :: Portuguese, Brazilian\nIlgiz Zigangirov (inov8) :: Russian\nMax Israelsson (Blezie) :: Swedish\n"
  },
  {
    "path": ".github/workflows/analyse-php.yml",
    "content": "name: analyse-php\n\non:\n  push:\n    paths:\n      - '**.php'\n  pull_request:\n    paths:\n      - '**.php'\n\njobs:\n  build:\n    if: ${{ github.ref != 'refs/heads/l10n_development' }}\n    runs-on: ubuntu-24.04\n    steps:\n    - uses: actions/checkout@v4\n\n    - name: Setup PHP\n      uses: shivammathur/setup-php@v2\n      with:\n        php-version: 8.3\n        extensions: gd, mbstring, json, curl, xml, mysql, ldap\n\n    - name: Get Composer Cache Directory\n      id: composer-cache\n      run: |\n        echo \"dir=$(composer config cache-files-dir)\" >> $GITHUB_OUTPUT\n\n    - name: Cache composer packages\n      uses: actions/cache@v4\n      with:\n        path: ${{ steps.composer-cache.outputs.dir }}\n        key: ${{ runner.os }}-composer-8.3\n        restore-keys: ${{ runner.os }}-composer-\n\n    - name: Install composer dependencies\n      run: composer install --prefer-dist --no-interaction --ansi\n\n    - name: Run static analysis check\n      run: composer check-static\n"
  },
  {
    "path": ".github/workflows/lint-js.yml",
    "content": "name: lint-js\n\non:\n  push:\n    paths:\n      - '**.js'\n      - '**.json'\n  pull_request:\n    paths:\n      - '**.js'\n      - '**.json'\n\njobs:\n  build:\n    if: ${{ github.ref != 'refs/heads/l10n_development' }}\n    runs-on: ubuntu-24.04\n    steps:\n    - uses: actions/checkout@v4\n\n    - name: Install NPM deps\n      run: npm ci\n\n    - name: Run formatting check\n      run: npm run lint\n"
  },
  {
    "path": ".github/workflows/lint-php.yml",
    "content": "name: lint-php\n\non:\n  push:\n    paths:\n      - '**.php'\n  pull_request:\n    paths:\n      - '**.php'\n\njobs:\n  build:\n    if: ${{ github.ref != 'refs/heads/l10n_development' }}\n    runs-on: ubuntu-24.04\n    steps:\n    - uses: actions/checkout@v4\n\n    - name: Setup PHP\n      uses: shivammathur/setup-php@v2\n      with:\n        php-version: 8.3\n        tools: phpcs\n\n    - name: Run formatting check\n      run: composer lint\n"
  },
  {
    "path": ".github/workflows/test-js.yml",
    "content": "name: test-js\n\non:\n  push:\n    paths:\n      - '**.js'\n      - '**.ts'\n      - '**.json'\n  pull_request:\n    paths:\n      - '**.js'\n      - '**.ts'\n      - '**.json'\n\njobs:\n  build:\n    if: ${{ github.ref != 'refs/heads/l10n_development' }}\n    runs-on: ubuntu-24.04\n    steps:\n    - uses: actions/checkout@v4\n\n    - name: Install NPM deps\n      run: npm ci\n\n    - name: Run TypeScript type checking\n      run: npm run ts:lint\n\n    - name: Run JavaScript tests\n      run: npm run test"
  },
  {
    "path": ".github/workflows/test-migrations.yml",
    "content": "name: test-migrations\n\non:\n  push:\n    paths:\n      - '**.php'\n      - 'composer.*'\n  pull_request:\n    paths:\n      - '**.php'\n      - 'composer.*'\n\njobs:\n  build:\n    if: ${{ github.ref != 'refs/heads/l10n_development' }}\n    runs-on: ubuntu-24.04\n    strategy:\n      matrix:\n        php: ['8.2', '8.3', '8.4', '8.5']\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: ${{ matrix.php }}\n          extensions: gd, mbstring, json, curl, xml, mysql, ldap\n\n      - name: Get Composer Cache Directory\n        id: composer-cache\n        run: |\n          echo \"dir=$(composer config cache-files-dir)\" >> $GITHUB_OUTPUT\n\n      - name: Cache composer packages\n        uses: actions/cache@v4\n        with:\n          path: ${{ steps.composer-cache.outputs.dir }}\n          key: ${{ runner.os }}-composer-${{ matrix.php }}\n          restore-keys: ${{ runner.os }}-composer-\n\n      - name: Start MySQL\n        run: |\n          sudo systemctl start mysql\n\n      - name: Create database & user\n        run: |\n          mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `bookstack-test`;'\n          mysql -uroot -proot -e \"CREATE USER 'bookstack-test'@'localhost' IDENTIFIED WITH mysql_native_password BY 'bookstack-test';\"\n          mysql -uroot -proot -e \"GRANT ALL ON \\`bookstack-test\\`.* TO 'bookstack-test'@'localhost';\"\n          mysql -uroot -proot -e 'FLUSH PRIVILEGES;'\n\n      - name: Install composer dependencies\n        run: composer install --prefer-dist --no-interaction --ansi\n\n      - name: Start migration test\n        run: |\n          php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing\n\n      - name: Start migration:rollback test\n        run: |\n          php${{ matrix.php }} artisan migrate:rollback --force -n --database=mysql_testing\n\n      - name: Start migration rerun test\n        run: |\n          php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing\n"
  },
  {
    "path": ".github/workflows/test-php.yml",
    "content": "name: test-php\n\non:\n  push:\n    paths:\n      - '**.php'\n      - 'composer.*'\n  pull_request:\n    paths:\n      - '**.php'\n      - 'composer.*'\n\njobs:\n  build:\n    if: ${{ github.ref != 'refs/heads/l10n_development' }}\n    runs-on: ubuntu-24.04\n    strategy:\n      matrix:\n        php: ['8.2', '8.3', '8.4', '8.5']\n    steps:\n    - uses: actions/checkout@v4\n\n    - name: Setup PHP\n      uses: shivammathur/setup-php@v2\n      with:\n        php-version: ${{ matrix.php }}\n        extensions: gd, mbstring, json, curl, xml, mysql, ldap, gmp\n\n    - name: Get Composer Cache Directory\n      id: composer-cache\n      run: |\n        echo \"dir=$(composer config cache-files-dir)\" >> $GITHUB_OUTPUT\n\n    - name: Cache composer packages\n      uses: actions/cache@v4\n      with:\n        path: ${{ steps.composer-cache.outputs.dir }}\n        key: ${{ runner.os }}-composer-${{ matrix.php }}\n        restore-keys: ${{ runner.os }}-composer-\n\n    - name: Start Database\n      run: |\n        sudo systemctl start mysql\n\n    - name: Setup Database\n      run: |\n        mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `bookstack-test`;'\n        mysql -uroot -proot -e \"CREATE USER 'bookstack-test'@'localhost' IDENTIFIED WITH mysql_native_password BY 'bookstack-test';\"\n        mysql -uroot -proot -e \"GRANT ALL ON \\`bookstack-test\\`.* TO 'bookstack-test'@'localhost';\"\n        mysql -uroot -proot -e 'FLUSH PRIVILEGES;'\n\n    - name: Install composer dependencies\n      run: composer install --prefer-dist --no-interaction --ansi\n\n    - name: Migrate and seed the database\n      run: |\n        php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing\n        php${{ matrix.php }} artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing\n\n    - name: Run PHP tests\n      run: php${{ matrix.php }} ./vendor/bin/phpunit\n"
  },
  {
    "path": ".gitignore",
    "content": "/vendor\n/node_modules\n/.vscode\n/composer\n/coverage\nHomestead.yaml\n.env\n.idea\nnpm-debug.log\nyarn-error.log\n/public/dist\n/public/plugins\n/public/css\n/public/js\n/public/bower\n/public/build/\n/public/favicon.ico\n/storage/images\n_ide_helper.php\n/storage/debugbar\n.phpstorm.meta.php\nyarn.lock\n/bin\nnbproject\n.buildpath\n.project\n.nvmrc\n.settings/\nwebpack-stats.json\n.phpunit.result.cache\n.DS_Store\nphpstan.neon\nesbuild-meta.json\n.phpactor.json\n/*.zip\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015-2026, Dan Brown and the BookStack project contributors.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "app/Access/Controllers/ConfirmEmailController.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Controllers;\n\nuse BookStack\\Access\\EmailConfirmationService;\nuse BookStack\\Access\\LoginService;\nuse BookStack\\Exceptions\\ConfirmationEmailException;\nuse BookStack\\Exceptions\\UserTokenExpiredException;\nuse BookStack\\Exceptions\\UserTokenNotFoundException;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Users\\UserRepo;\nuse Exception;\nuse Illuminate\\Http\\Request;\n\nclass ConfirmEmailController extends Controller\n{\n    public function __construct(\n        protected EmailConfirmationService $emailConfirmationService,\n        protected LoginService $loginService,\n        protected UserRepo $userRepo\n    ) {\n    }\n\n    /**\n     * Show the page to tell the user to check their email\n     * and confirm their address.\n     */\n    public function show()\n    {\n        return view('auth.register-confirm');\n    }\n\n    /**\n     * Shows a notice that a user's email address has not been confirmed,\n     * along with the option to re-send the confirmation email.\n     */\n    public function showAwaiting()\n    {\n        $user = $this->loginService->getLastLoginAttemptUser();\n        if ($user === null) {\n            $this->showErrorNotification(trans('errors.login_user_not_found'));\n            return redirect('/login');\n        }\n\n        return view('auth.register-confirm-awaiting');\n    }\n\n    /**\n     * Show the form for a user to provide their positive confirmation of their email.\n     */\n    public function showAcceptForm(string $token)\n    {\n        return view('auth.register-confirm-accept', ['token' => $token]);\n    }\n\n    /**\n     * Confirms an email via a token and logs the user into the system.\n     *\n     * @throws ConfirmationEmailException\n     * @throws Exception\n     */\n    public function confirm(Request $request)\n    {\n        $validated = $this->validate($request, [\n            'token' => ['required', 'string']\n        ]);\n\n        $token = $validated['token'];\n\n        try {\n            $userId = $this->emailConfirmationService->checkTokenAndGetUserId($token);\n        } catch (UserTokenNotFoundException $exception) {\n            $this->showErrorNotification(trans('errors.email_confirmation_invalid'));\n\n            return redirect('/register');\n        } catch (UserTokenExpiredException $exception) {\n            $user = $this->userRepo->getById($exception->userId);\n            $this->emailConfirmationService->sendConfirmation($user);\n            $this->showErrorNotification(trans('errors.email_confirmation_expired'));\n\n            return redirect('/register/confirm');\n        }\n\n        $user = $this->userRepo->getById($userId);\n        $user->email_confirmed = true;\n        $user->save();\n\n        $this->emailConfirmationService->deleteByUser($user);\n        $this->showSuccessNotification(trans('auth.email_confirm_success'));\n\n        return redirect('/login');\n    }\n\n    /**\n     * Resend the confirmation email.\n     */\n    public function resend()\n    {\n        $user = $this->loginService->getLastLoginAttemptUser();\n        if ($user === null) {\n            $this->showErrorNotification(trans('errors.login_user_not_found'));\n            return redirect('/login');\n        }\n\n        try {\n            $this->emailConfirmationService->sendConfirmation($user);\n        } catch (ConfirmationEmailException $e) {\n            $this->showErrorNotification($e->getMessage());\n\n            return redirect('/login');\n        } catch (Exception $e) {\n            $this->showErrorNotification(trans('auth.email_confirm_send_error'));\n\n            return redirect('/register/awaiting');\n        }\n\n        $this->showSuccessNotification(trans('auth.email_confirm_resent'));\n\n        return redirect('/register/confirm');\n    }\n}\n"
  },
  {
    "path": "app/Access/Controllers/ForgotPasswordController.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Controllers;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Http\\Controller;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\Password;\nuse Illuminate\\Support\\Sleep;\n\nclass ForgotPasswordController extends Controller\n{\n    public function __construct()\n    {\n        $this->middleware('guest');\n        $this->middleware('guard:standard');\n    }\n\n    /**\n     * Display the form to request a password reset link.\n     */\n    public function showLinkRequestForm()\n    {\n        return view('auth.passwords.email');\n    }\n\n    /**\n     * Send a reset link to the given user.\n     */\n    public function sendResetLinkEmail(Request $request)\n    {\n        $this->validate($request, [\n            'email' => ['required', 'email'],\n        ]);\n\n        // Add random pause to the response to help avoid time-base sniffing\n        // of valid resets via slower email send handling.\n        Sleep::for(random_int(1000, 3000))->milliseconds();\n\n        // We will send the password reset link to this user. Once we have attempted\n        // to send the link, we will examine the response then see the message we\n        // need to show to the user. Finally, we'll send out a proper response.\n        $response = Password::broker()->sendResetLink(\n            $request->only('email')\n        );\n\n        if ($response === Password::RESET_LINK_SENT) {\n            $this->logActivity(ActivityType::AUTH_PASSWORD_RESET, $request->get('email'));\n        }\n\n        if (in_array($response, [Password::RESET_LINK_SENT, Password::INVALID_USER, Password::RESET_THROTTLED])) {\n            $message = trans('auth.reset_password_sent', ['email' => $request->get('email')]);\n            $this->showSuccessNotification($message);\n\n            return redirect('/password/email')->with('status', trans($response));\n        }\n\n        // If an error was returned by the password broker, we will get this message\n        // translated so we can notify a user of the problem. We'll redirect back\n        // to where the users came from so they can attempt this process again.\n        return redirect('/password/email')->withErrors(\n            ['email' => trans($response)]\n        );\n    }\n}\n"
  },
  {
    "path": "app/Access/Controllers/HandlesPartialLogins.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Controllers;\n\nuse BookStack\\Access\\LoginService;\nuse BookStack\\Exceptions\\NotFoundException;\nuse BookStack\\Users\\Models\\User;\n\ntrait HandlesPartialLogins\n{\n    /**\n     * @throws NotFoundException\n     */\n    protected function currentOrLastAttemptedUser(): User\n    {\n        $loginService = app()->make(LoginService::class);\n        $user = auth()->user() ?? $loginService->getLastLoginAttemptUser();\n\n        if (!$user) {\n            throw new NotFoundException(trans('errors.login_user_not_found'));\n        }\n\n        return $user;\n    }\n}\n"
  },
  {
    "path": "app/Access/Controllers/LoginController.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Controllers;\n\nuse BookStack\\Access\\LoginService;\nuse BookStack\\Access\\SocialDriverManager;\nuse BookStack\\Exceptions\\LoginAttemptEmailNeededException;\nuse BookStack\\Exceptions\\LoginAttemptException;\nuse BookStack\\Facades\\Activity;\nuse BookStack\\Http\\Controller;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Validation\\ValidationException;\n\nclass LoginController extends Controller\n{\n    use ThrottlesLogins;\n\n    public function __construct(\n        protected SocialDriverManager $socialDriverManager,\n        protected LoginService $loginService,\n    ) {\n        $this->middleware('guest', ['only' => ['getLogin', 'login']]);\n        $this->middleware('guard:standard,ldap', ['only' => ['login']]);\n        $this->middleware('guard:standard,ldap,oidc', ['only' => ['logout']]);\n    }\n\n    /**\n     * Show the application login form.\n     */\n    public function getLogin(Request $request)\n    {\n        $socialDrivers = $this->socialDriverManager->getActive();\n        $authMethod = config('auth.method');\n        $preventInitiation = $request->get('prevent_auto_init') === 'true';\n\n        if ($request->has('email')) {\n            session()->flashInput([\n                'email'    => $request->get('email'),\n                'password' => (config('app.env') === 'demo') ? $request->get('password', '') : '',\n            ]);\n        }\n\n        // Store the previous location for redirect after login\n        $this->updateIntendedFromPrevious();\n\n        if (!$preventInitiation && $this->loginService->shouldAutoInitiate()) {\n            return view('auth.login-initiate', [\n                'authMethod'    => $authMethod,\n            ]);\n        }\n\n        return view('auth.login', [\n            'socialDrivers' => $socialDrivers,\n            'authMethod'    => $authMethod,\n        ]);\n    }\n\n    /**\n     * Handle a login request to the application.\n     */\n    public function login(Request $request)\n    {\n        $this->validateLogin($request);\n        $username = $request->get($this->username());\n\n        // Check login throttling attempts to see if they've gone over the limit\n        if ($this->hasTooManyLoginAttempts($request)) {\n            Activity::logFailedLogin($username);\n            return $this->sendLockoutResponse($request);\n        }\n\n        try {\n            if ($this->attemptLogin($request)) {\n                return $this->sendLoginResponse($request);\n            }\n        } catch (LoginAttemptException $exception) {\n            Activity::logFailedLogin($username);\n\n            return $this->sendLoginAttemptExceptionResponse($exception, $request);\n        }\n\n        // On unsuccessful login attempt, Increment login attempts for throttling and log failed login.\n        $this->incrementLoginAttempts($request);\n        Activity::logFailedLogin($username);\n\n        // Throw validation failure for failed login\n        throw ValidationException::withMessages([\n            $this->username() => [trans('auth.failed')],\n        ])->redirectTo('/login');\n    }\n\n    /**\n     * Logout user and perform subsequent redirect.\n     */\n    public function logout()\n    {\n        return redirect($this->loginService->logout());\n    }\n\n    /**\n     * Get the expected username input based upon the current auth method.\n     */\n    protected function username(): string\n    {\n        return config('auth.method') === 'standard' ? 'email' : 'username';\n    }\n\n    /**\n     * Get the needed authorization credentials from the request.\n     */\n    protected function credentials(Request $request): array\n    {\n        return $request->only('username', 'email', 'password');\n    }\n\n    /**\n     * Send the response after the user was authenticated.\n     * @return RedirectResponse\n     */\n    protected function sendLoginResponse(Request $request)\n    {\n        $request->session()->regenerate();\n        $this->clearLoginAttempts($request);\n\n        return redirect()->intended('/');\n    }\n\n    /**\n     * Attempt to log the user into the application.\n     */\n    protected function attemptLogin(Request $request): bool\n    {\n        return $this->loginService->attempt(\n            $this->credentials($request),\n            auth()->getDefaultDriver(),\n            $request->filled('remember')\n        );\n    }\n\n\n    /**\n     * Validate the user login request.\n     * @throws ValidationException\n     */\n    protected function validateLogin(Request $request): void\n    {\n        $rules = ['password' => ['required', 'string']];\n        $authMethod = config('auth.method');\n\n        if ($authMethod === 'standard') {\n            $rules['email'] = ['required', 'email'];\n        }\n\n        if ($authMethod === 'ldap') {\n            $rules['username'] = ['required', 'string'];\n            $rules['email'] = ['email'];\n        }\n\n        $request->validate($rules);\n    }\n\n    /**\n     * Send a response when a login attempt exception occurs.\n     */\n    protected function sendLoginAttemptExceptionResponse(LoginAttemptException $exception, Request $request)\n    {\n        if ($exception instanceof LoginAttemptEmailNeededException) {\n            $request->flash();\n            session()->flash('request-email', true);\n        }\n\n        if ($message = $exception->getMessage()) {\n            $this->showWarningNotification($message);\n        }\n\n        return redirect('/login');\n    }\n\n    /**\n     * Update the intended URL location from their previous URL.\n     * Ignores if not from the current app instance or if from certain\n     * login or authentication routes.\n     */\n    protected function updateIntendedFromPrevious(): void\n    {\n        // Store the previous location for redirect after login\n        $previous = url()->previous('');\n        $isPreviousFromInstance = str_starts_with($previous, url('/'));\n        if (!$previous || !setting('app-public') || !$isPreviousFromInstance) {\n            return;\n        }\n\n        $ignorePrefixList = [\n            '/login',\n            '/mfa',\n        ];\n\n        foreach ($ignorePrefixList as $ignorePrefix) {\n            if (str_starts_with($previous, url($ignorePrefix))) {\n                return;\n            }\n        }\n\n        redirect()->setIntendedUrl($previous);\n    }\n}\n"
  },
  {
    "path": "app/Access/Controllers/MfaBackupCodesController.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Controllers;\n\nuse BookStack\\Access\\LoginService;\nuse BookStack\\Access\\Mfa\\BackupCodeService;\nuse BookStack\\Access\\Mfa\\MfaSession;\nuse BookStack\\Access\\Mfa\\MfaValue;\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Exceptions\\NotFoundException;\nuse BookStack\\Http\\Controller;\nuse Exception;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Validation\\ValidationException;\n\nclass MfaBackupCodesController extends Controller\n{\n    use HandlesPartialLogins;\n\n    protected const SETUP_SECRET_SESSION_KEY = 'mfa-setup-backup-codes';\n\n    /**\n     * Show a view that generates and displays backup codes.\n     */\n    public function generate(BackupCodeService $codeService)\n    {\n        $codes = $codeService->generateNewSet();\n        session()->put(self::SETUP_SECRET_SESSION_KEY, encrypt($codes));\n\n        $downloadUrl = 'data:application/octet-stream;base64,' . base64_encode(implode(\"\\n\\n\", $codes));\n\n        $this->setPageTitle(trans('auth.mfa_gen_backup_codes_title'));\n\n        return view('mfa.backup-codes-generate', [\n            'codes'       => $codes,\n            'downloadUrl' => $downloadUrl,\n        ]);\n    }\n\n    /**\n     * Confirm the setup of backup codes, storing them against the user.\n     *\n     * @throws Exception\n     */\n    public function confirm()\n    {\n        if (!session()->has(self::SETUP_SECRET_SESSION_KEY)) {\n            return response('No generated codes found in the session', 500);\n        }\n\n        $codes = decrypt(session()->pull(self::SETUP_SECRET_SESSION_KEY));\n        MfaValue::upsertWithValue($this->currentOrLastAttemptedUser(), MfaValue::METHOD_BACKUP_CODES, json_encode($codes));\n\n        $this->logActivity(ActivityType::MFA_SETUP_METHOD, 'backup-codes');\n\n        if (!auth()->check()) {\n            $this->showSuccessNotification(trans('auth.mfa_setup_login_notification'));\n\n            return redirect('/login');\n        }\n\n        return redirect('/mfa/setup');\n    }\n\n    /**\n     * Verify the MFA method submission on check.\n     *\n     * @throws NotFoundException\n     * @throws ValidationException\n     */\n    public function verify(Request $request, BackupCodeService $codeService, MfaSession $mfaSession, LoginService $loginService)\n    {\n        $user = $this->currentOrLastAttemptedUser();\n        $codes = MfaValue::getValueForUser($user, MfaValue::METHOD_BACKUP_CODES) ?? '[]';\n\n        $this->validate($request, [\n            'code' => [\n                'required', 'max:12', 'min:8',\n                function ($attribute, $value, $fail) use ($codeService, $codes) {\n                    if (!$codeService->inputCodeExistsInSet($value, $codes)) {\n                        $fail(trans('validation.backup_codes'));\n                    }\n                },\n            ],\n        ]);\n\n        $updatedCodes = $codeService->removeInputCodeFromSet($request->get('code'), $codes);\n        MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, $updatedCodes);\n\n        $mfaSession->markVerifiedForUser($user);\n        $loginService->reattemptLoginFor($user);\n\n        if ($codeService->countCodesInSet($updatedCodes) < 5) {\n            $this->showWarningNotification(trans('auth.mfa_backup_codes_usage_limit_warning'));\n        }\n\n        return redirect()->intended();\n    }\n}\n"
  },
  {
    "path": "app/Access/Controllers/MfaController.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Controllers;\n\nuse BookStack\\Access\\Mfa\\MfaValue;\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Http\\Controller;\nuse Illuminate\\Http\\Request;\n\nclass MfaController extends Controller\n{\n    use HandlesPartialLogins;\n\n    /**\n     * Show the view to setup MFA for the current user.\n     */\n    public function setup()\n    {\n        $userMethods = $this->currentOrLastAttemptedUser()\n            ->mfaValues()\n            ->get(['id', 'method'])\n            ->groupBy('method');\n\n        $this->setPageTitle(trans('auth.mfa_setup'));\n\n        return view('mfa.setup', [\n            'userMethods' => $userMethods,\n        ]);\n    }\n\n    /**\n     * Remove an MFA method for the current user.\n     *\n     * @throws \\Exception\n     */\n    public function remove(string $method)\n    {\n        if (in_array($method, MfaValue::allMethods())) {\n            $value = user()->mfaValues()->where('method', '=', $method)->first();\n            if ($value) {\n                $value->delete();\n                $this->logActivity(ActivityType::MFA_REMOVE_METHOD, $method);\n            }\n        }\n\n        return redirect('/mfa/setup');\n    }\n\n    /**\n     * Show the page to start an MFA verification.\n     */\n    public function verify(Request $request)\n    {\n        $desiredMethod = $request->get('method');\n        $userMethods = $this->currentOrLastAttemptedUser()\n            ->mfaValues()\n            ->get(['id', 'method'])\n            ->groupBy('method');\n\n        // Basic search for the default option for a user.\n        // (Prioritises totp over backup codes)\n        $method = $userMethods->has($desiredMethod) ? $desiredMethod : $userMethods->keys()->sort()->reverse()->first();\n        $otherMethods = $userMethods->keys()->filter(function ($userMethod) use ($method) {\n            return $method !== $userMethod;\n        })->all();\n\n        return view('mfa.verify', [\n            'userMethods'  => $userMethods,\n            'method'       => $method,\n            'otherMethods' => $otherMethods,\n        ]);\n    }\n}\n"
  },
  {
    "path": "app/Access/Controllers/MfaTotpController.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Controllers;\n\nuse BookStack\\Access\\LoginService;\nuse BookStack\\Access\\Mfa\\MfaSession;\nuse BookStack\\Access\\Mfa\\MfaValue;\nuse BookStack\\Access\\Mfa\\TotpService;\nuse BookStack\\Access\\Mfa\\TotpValidationRule;\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Exceptions\\NotFoundException;\nuse BookStack\\Http\\Controller;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Validation\\ValidationException;\n\nclass MfaTotpController extends Controller\n{\n    use HandlesPartialLogins;\n\n    protected const SETUP_SECRET_SESSION_KEY = 'mfa-setup-totp-secret';\n\n    public function __construct(\n        protected TotpService $totp\n    ) {\n    }\n\n    /**\n     * Show a view that generates and displays a TOTP QR code.\n     */\n    public function generate()\n    {\n        if (session()->has(static::SETUP_SECRET_SESSION_KEY)) {\n            $totpSecret = decrypt(session()->get(static::SETUP_SECRET_SESSION_KEY));\n        } else {\n            $totpSecret = $this->totp->generateSecret();\n            session()->put(static::SETUP_SECRET_SESSION_KEY, encrypt($totpSecret));\n        }\n\n        $qrCodeUrl = $this->totp->generateUrl($totpSecret, $this->currentOrLastAttemptedUser());\n        $svg = $this->totp->generateQrCodeSvg($qrCodeUrl);\n\n        $this->setPageTitle(trans('auth.mfa_gen_totp_title'));\n\n        return view('mfa.totp-generate', [\n            'url' => $qrCodeUrl,\n            'svg' => $svg,\n        ]);\n    }\n\n    /**\n     * Confirm the setup of TOTP and save the auth method secret\n     * against the current user.\n     *\n     * @throws ValidationException\n     * @throws NotFoundException\n     */\n    public function confirm(Request $request)\n    {\n        $totpSecret = decrypt(session()->get(static::SETUP_SECRET_SESSION_KEY));\n        $this->validate($request, [\n            'code' => [\n                'required',\n                'max:12', 'min:4',\n                new TotpValidationRule($totpSecret, $this->totp),\n            ],\n        ]);\n\n        MfaValue::upsertWithValue($this->currentOrLastAttemptedUser(), MfaValue::METHOD_TOTP, $totpSecret);\n        session()->remove(static::SETUP_SECRET_SESSION_KEY);\n        $this->logActivity(ActivityType::MFA_SETUP_METHOD, 'totp');\n\n        if (!auth()->check()) {\n            $this->showSuccessNotification(trans('auth.mfa_setup_login_notification'));\n\n            return redirect('/login');\n        }\n\n        return redirect('/mfa/setup');\n    }\n\n    /**\n     * Verify the MFA method submission on check.\n     *\n     * @throws NotFoundException\n     */\n    public function verify(Request $request, LoginService $loginService, MfaSession $mfaSession)\n    {\n        $user = $this->currentOrLastAttemptedUser();\n        $totpSecret = MfaValue::getValueForUser($user, MfaValue::METHOD_TOTP);\n\n        $this->validate($request, [\n            'code' => [\n                'required',\n                'max:12', 'min:4',\n                new TotpValidationRule($totpSecret, $this->totp),\n            ],\n        ]);\n\n        $mfaSession->markVerifiedForUser($user);\n        $loginService->reattemptLoginFor($user);\n\n        return redirect()->intended();\n    }\n}\n"
  },
  {
    "path": "app/Access/Controllers/OidcController.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Controllers;\n\nuse BookStack\\Access\\Oidc\\OidcException;\nuse BookStack\\Access\\Oidc\\OidcService;\nuse BookStack\\Http\\Controller;\nuse Illuminate\\Http\\Request;\n\nclass OidcController extends Controller\n{\n    public function __construct(\n        protected OidcService $oidcService\n    ) {\n        $this->middleware('guard:oidc');\n    }\n\n    /**\n     * Start the authorization login flow via OIDC.\n     */\n    public function login()\n    {\n        try {\n            $loginDetails = $this->oidcService->login();\n        } catch (OidcException $exception) {\n            $this->showErrorNotification($exception->getMessage());\n\n            return redirect('/login');\n        }\n\n        session()->put('oidc_state', time() . ':' . $loginDetails['state']);\n\n        return redirect($loginDetails['url']);\n    }\n\n    /**\n     * Authorization flow redirect callback.\n     * Processes authorization response from the OIDC Authorization Server.\n     */\n    public function callback(Request $request)\n    {\n        $responseState = $request->query('state');\n        $splitState =  explode(':', session()->pull('oidc_state', ':'), 2);\n        if (count($splitState) !== 2) {\n            $splitState = [null, null];\n        }\n\n        [$storedStateTime, $storedState] = $splitState;\n        $threeMinutesAgo = time() - 3 * 60;\n\n        if (!$storedState || $storedState !== $responseState || intval($storedStateTime) < $threeMinutesAgo) {\n            $this->showErrorNotification(trans('errors.oidc_fail_authed', ['system' => config('oidc.name')]));\n\n            return redirect('/login');\n        }\n\n        try {\n            $this->oidcService->processAuthorizeResponse($request->query('code'));\n        } catch (OidcException $oidcException) {\n            $this->showErrorNotification($oidcException->getMessage());\n\n            return redirect('/login');\n        }\n\n        return redirect()->intended();\n    }\n\n    /**\n     * Log the user out, then start the OIDC RP-initiated logout process.\n     */\n    public function logout()\n    {\n        return redirect($this->oidcService->logout());\n    }\n}\n"
  },
  {
    "path": "app/Access/Controllers/RegisterController.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Controllers;\n\nuse BookStack\\Access\\LoginService;\nuse BookStack\\Access\\RegistrationService;\nuse BookStack\\Access\\SocialDriverManager;\nuse BookStack\\Exceptions\\StoppedAuthenticationException;\nuse BookStack\\Exceptions\\UserRegistrationException;\nuse BookStack\\Http\\Controller;\nuse Illuminate\\Contracts\\Validation\\Validator as ValidatorContract;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\Validator;\nuse Illuminate\\Validation\\Rules\\Password;\n\nclass RegisterController extends Controller\n{\n    public function __construct(\n        protected SocialDriverManager $socialDriverManager,\n        protected RegistrationService $registrationService,\n        protected LoginService $loginService\n    ) {\n        $this->middleware('guest');\n        $this->middleware('guard:standard');\n    }\n\n    /**\n     * Show the application registration form.\n     *\n     * @throws UserRegistrationException\n     */\n    public function getRegister()\n    {\n        $this->registrationService->ensureRegistrationAllowed();\n        $socialDrivers = $this->socialDriverManager->getActive();\n\n        return view('auth.register', [\n            'socialDrivers' => $socialDrivers,\n        ]);\n    }\n\n    /**\n     * Handle a registration request for the application.\n     *\n     * @throws UserRegistrationException\n     * @throws StoppedAuthenticationException\n     */\n    public function postRegister(Request $request)\n    {\n        $this->registrationService->ensureRegistrationAllowed();\n        $this->validator($request->all())->validate();\n        $userData = $request->all();\n\n        try {\n            $user = $this->registrationService->registerUser($userData);\n            $this->loginService->login($user, auth()->getDefaultDriver());\n        } catch (UserRegistrationException $exception) {\n            if ($exception->getMessage()) {\n                $this->showErrorNotification($exception->getMessage());\n            }\n\n            return redirect($exception->redirectLocation);\n        }\n\n        $this->showSuccessNotification(trans('auth.register_success'));\n\n        return redirect('/');\n    }\n\n    /**\n     * Get a validator for an incoming registration request.\n     */\n    protected function validator(array $data): ValidatorContract\n    {\n        return Validator::make($data, [\n            'name'     => ['required', 'min:2', 'max:100'],\n            'email'    => ['required', 'email', 'max:255', 'unique:users'],\n            'password' => ['required', Password::default()],\n            // Basic honey for bots that must not be filled in\n            'username' => ['prohibited'],\n        ]);\n    }\n}\n"
  },
  {
    "path": "app/Access/Controllers/ResetPasswordController.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Controllers;\n\nuse BookStack\\Access\\LoginService;\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\Hash;\nuse Illuminate\\Support\\Facades\\Password;\nuse Illuminate\\Support\\Str;\nuse Illuminate\\Validation\\Rules\\Password as PasswordRule;\n\nclass ResetPasswordController extends Controller\n{\n    public function __construct(\n        protected LoginService $loginService\n    ) {\n        $this->middleware('guest');\n        $this->middleware('guard:standard');\n    }\n\n    /**\n     * Display the password reset view for the given token.\n     * If no token is present, display the link request form.\n     */\n    public function showResetForm(Request $request)\n    {\n        $token = $request->route()->parameter('token');\n\n        return view('auth.passwords.reset')->with(\n            ['token' => $token, 'email' => $request->email]\n        );\n    }\n\n    /**\n     * Reset the given user's password.\n     */\n    public function reset(Request $request)\n    {\n        $request->validate([\n            'token' => 'required',\n            'email' => 'required|email',\n            'password' => ['required', 'confirmed', PasswordRule::defaults()],\n        ]);\n\n        // Here we will attempt to reset the user's password. If it is successful we\n        // will update the password on an actual user model and persist it to the\n        // database. Otherwise we will parse the error and return the response.\n        $credentials = $request->only('email', 'password', 'password_confirmation', 'token');\n        $response = Password::broker()->reset($credentials, function (User $user, string $password) {\n            $user->password = Hash::make($password);\n            $user->setRememberToken(Str::random(60));\n            $user->save();\n\n            $this->loginService->login($user, auth()->getDefaultDriver());\n        });\n\n        // If the password was successfully reset, we will redirect the user back to\n        // the application's home authenticated view. If there is an error we can\n        // redirect them back to where they came from with their error message.\n        return $response === Password::PASSWORD_RESET\n            ? $this->sendResetResponse()\n            : $this->sendResetFailedResponse($request, $response, $request->get('token'));\n    }\n\n    /**\n     * Get the response for a successful password reset.\n     */\n    protected function sendResetResponse(): RedirectResponse\n    {\n        $this->showSuccessNotification(trans('auth.reset_password_success'));\n        $this->logActivity(ActivityType::AUTH_PASSWORD_RESET_UPDATE, user());\n\n        return redirect('/');\n    }\n\n    /**\n     * Get the response for a failed password reset.\n     */\n    protected function sendResetFailedResponse(Request $request, string $response, string $token): RedirectResponse\n    {\n        // We show invalid users as invalid tokens as to not leak what\n        // users may exist in the system.\n        if ($response === Password::INVALID_USER) {\n            $response = Password::INVALID_TOKEN;\n        }\n\n        return redirect(\"/password/reset/{$token}\")\n            ->withInput($request->only('email'))\n            ->withErrors(['email' => trans($response)]);\n    }\n}\n"
  },
  {
    "path": "app/Access/Controllers/Saml2Controller.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Controllers;\n\nuse BookStack\\Access\\Saml2Service;\nuse BookStack\\Http\\Controller;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Str;\n\nclass Saml2Controller extends Controller\n{\n    public function __construct(\n        protected Saml2Service $samlService\n    ) {\n        $this->middleware('guard:saml2');\n    }\n\n    /**\n     * Start the login flow via SAML2.\n     */\n    public function login()\n    {\n        $loginDetails = $this->samlService->login();\n        session()->flash('saml2_request_id', $loginDetails['id']);\n\n        return redirect($loginDetails['url']);\n    }\n\n    /**\n     * Start the logout flow via SAML2.\n     */\n    public function logout()\n    {\n        $user = user();\n        if ($user->isGuest()) {\n            return redirect('/login');\n        }\n\n        $logoutDetails = $this->samlService->logout($user);\n\n        if ($logoutDetails['id']) {\n            session()->flash('saml2_logout_request_id', $logoutDetails['id']);\n        }\n\n        return redirect($logoutDetails['url']);\n    }\n\n    /*\n     * Get the metadata for this SAML2 service provider.\n     */\n    public function metadata()\n    {\n        $metaData = $this->samlService->metadata();\n\n        return response()->make($metaData, 200, [\n            'Content-Type' => 'text/xml',\n        ]);\n    }\n\n    /**\n     * Single logout service.\n     * Handle logout requests and responses.\n     */\n    public function sls()\n    {\n        $requestId = session()->pull('saml2_logout_request_id', null);\n        $redirect = $this->samlService->processSlsResponse($requestId);\n\n        return redirect($redirect);\n    }\n\n    /**\n     * Assertion Consumer Service start URL. Takes the SAMLResponse from the IDP.\n     * Due to being an external POST request, we likely won't have context of the\n     * current user session due to lax cookies. To work around this we store the\n     * SAMLResponse data and redirect to the processAcs endpoint for the actual\n     * processing of the request with proper context of the user session.\n     */\n    public function startAcs(Request $request)\n    {\n        $samlResponse = $request->get('SAMLResponse', null);\n\n        if (empty($samlResponse)) {\n            $this->showErrorNotification(trans('errors.saml_fail_authed', ['system' => config('saml2.name')]));\n\n            return redirect('/login');\n        }\n\n        $acsId = Str::random(16);\n        $cacheKey = 'saml2_acs:' . $acsId;\n        cache()->set($cacheKey, encrypt($samlResponse), 10);\n\n        return redirect()->guest('/saml2/acs?id=' . $acsId);\n    }\n\n    /**\n     * Assertion Consumer Service process endpoint.\n     * Processes the SAML response from the IDP with context of the current session.\n     * Takes the SAML request from the cache, added by the startAcs method above.\n     */\n    public function processAcs(Request $request)\n    {\n        $acsId = $request->get('id', null);\n        $cacheKey = 'saml2_acs:' . $acsId;\n        $samlResponse = null;\n\n        try {\n            $samlResponse = decrypt(cache()->pull($cacheKey));\n        } catch (\\Exception $exception) {\n        }\n        $requestId = session()->pull('saml2_request_id', null);\n\n        if (empty($acsId) || empty($samlResponse)) {\n            $this->showErrorNotification(trans('errors.saml_fail_authed', ['system' => config('saml2.name')]));\n\n            return redirect('/login');\n        }\n\n        $user = $this->samlService->processAcsResponse($requestId, $samlResponse);\n        if (is_null($user)) {\n            $this->showErrorNotification(trans('errors.saml_fail_authed', ['system' => config('saml2.name')]));\n\n            return redirect('/login');\n        }\n\n        return redirect()->intended();\n    }\n}\n"
  },
  {
    "path": "app/Access/Controllers/SocialController.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Controllers;\n\nuse BookStack\\Access\\LoginService;\nuse BookStack\\Access\\RegistrationService;\nuse BookStack\\Access\\SocialAuthService;\nuse BookStack\\Exceptions\\SocialDriverNotConfigured;\nuse BookStack\\Exceptions\\SocialSignInAccountNotUsed;\nuse BookStack\\Exceptions\\SocialSignInException;\nuse BookStack\\Exceptions\\UserRegistrationException;\nuse BookStack\\Http\\Controller;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Str;\nuse Laravel\\Socialite\\Contracts\\User as SocialUser;\n\nclass SocialController extends Controller\n{\n    public function __construct(\n        protected SocialAuthService $socialAuthService,\n        protected RegistrationService $registrationService,\n        protected LoginService $loginService,\n    ) {\n        $this->middleware('guest')->only(['register']);\n    }\n\n    /**\n     * Redirect to the relevant social site.\n     *\n     * @throws SocialDriverNotConfigured\n     */\n    public function login(string $socialDriver)\n    {\n        session()->put('social-callback', 'login');\n\n        return $this->socialAuthService->startLogIn($socialDriver);\n    }\n\n    /**\n     * Redirect to the social site for authentication intended to register.\n     *\n     * @throws SocialDriverNotConfigured\n     * @throws UserRegistrationException\n     */\n    public function register(string $socialDriver)\n    {\n        $this->registrationService->ensureRegistrationAllowed();\n        session()->put('social-callback', 'register');\n\n        return $this->socialAuthService->startRegister($socialDriver);\n    }\n\n    /**\n     * The callback for social login services.\n     *\n     * @throws SocialSignInException\n     * @throws SocialDriverNotConfigured\n     * @throws UserRegistrationException\n     */\n    public function callback(Request $request, string $socialDriver)\n    {\n        if (!session()->has('social-callback')) {\n            throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login');\n        }\n\n        // Check request for error information\n        if ($request->has('error') && $request->has('error_description')) {\n            throw new SocialSignInException(trans('errors.social_login_bad_response', [\n                'socialAccount' => $socialDriver,\n                'error'         => $request->get('error_description'),\n            ]), '/login');\n        }\n\n        $action = session()->pull('social-callback');\n\n        // Attempt login or fall-back to register if allowed.\n        $socialUser = $this->socialAuthService->getSocialUser($socialDriver);\n        if ($action === 'login') {\n            try {\n                return $this->socialAuthService->handleLoginCallback($socialDriver, $socialUser);\n            } catch (SocialSignInAccountNotUsed $exception) {\n                if ($this->socialAuthService->drivers()->isAutoRegisterEnabled($socialDriver)) {\n                    return $this->socialRegisterCallback($socialDriver, $socialUser);\n                }\n\n                throw $exception;\n            }\n        }\n\n        if ($action === 'register') {\n            return $this->socialRegisterCallback($socialDriver, $socialUser);\n        }\n\n        return redirect('/');\n    }\n\n    /**\n     * Detach a social account from a user.\n     */\n    public function detach(string $socialDriver)\n    {\n        $this->socialAuthService->detachSocialAccount($socialDriver);\n        session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => Str::title($socialDriver)]));\n\n        return redirect('/my-account/auth#social-accounts');\n    }\n\n    /**\n     * Register a new user after a registration callback.\n     *\n     * @throws UserRegistrationException\n     */\n    protected function socialRegisterCallback(string $socialDriver, SocialUser $socialUser)\n    {\n        $socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver, $socialUser);\n        $socialAccount = $this->socialAuthService->newSocialAccount($socialDriver, $socialUser);\n        $emailVerified = $this->socialAuthService->drivers()->isAutoConfirmEmailEnabled($socialDriver);\n\n        // Create an array of the user data to create a new user instance\n        $userData = [\n            'name'     => $socialUser->getName(),\n            'email'    => $socialUser->getEmail(),\n            'password' => Str::random(32),\n        ];\n\n        // Take name from email address if empty\n        if (!$userData['name']) {\n            $userData['name'] = explode('@', $userData['email'])[0];\n        }\n\n        $user = $this->registrationService->registerUser($userData, $socialAccount, $emailVerified);\n        $this->showSuccessNotification(trans('auth.register_success'));\n        $this->loginService->login($user, $socialDriver);\n\n        return redirect('/');\n    }\n}\n"
  },
  {
    "path": "app/Access/Controllers/ThrottlesLogins.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Controllers;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Support\\Str;\nuse Illuminate\\Validation\\ValidationException;\n\ntrait ThrottlesLogins\n{\n    /**\n     * Determine if the user has too many failed login attempts.\n     */\n    protected function hasTooManyLoginAttempts(Request $request): bool\n    {\n        return $this->limiter()->tooManyAttempts(\n            $this->throttleKey($request),\n            $this->maxAttempts()\n        );\n    }\n\n    /**\n     * Increment the login attempts for the user.\n     */\n    protected function incrementLoginAttempts(Request $request): void\n    {\n        $this->limiter()->hit(\n            $this->throttleKey($request),\n            $this->decayMinutes() * 60\n        );\n    }\n\n    /**\n     * Redirect the user after determining they are locked out.\n     * @throws ValidationException\n     */\n    protected function sendLockoutResponse(Request $request): \\Symfony\\Component\\HttpFoundation\\Response\n    {\n        $seconds = $this->limiter()->availableIn(\n            $this->throttleKey($request)\n        );\n\n        throw ValidationException::withMessages([\n            $this->username() => [trans('auth.throttle', [\n                'seconds' => $seconds,\n                'minutes' => ceil($seconds / 60),\n            ])],\n        ])->status(Response::HTTP_TOO_MANY_REQUESTS);\n    }\n\n    /**\n     * Clear the login locks for the given user credentials.\n     */\n    protected function clearLoginAttempts(Request $request): void\n    {\n        $this->limiter()->clear($this->throttleKey($request));\n    }\n\n    /**\n     * Get the throttle key for the given request.\n     */\n    protected function throttleKey(Request $request): string\n    {\n        return Str::transliterate(Str::lower($request->input($this->username())) . '|' . $request->ip());\n    }\n\n    /**\n     * Get the rate limiter instance.\n     */\n    protected function limiter(): RateLimiter\n    {\n        return app()->make(RateLimiter::class);\n    }\n\n    /**\n     * Get the maximum number of attempts to allow.\n     */\n    public function maxAttempts(): int\n    {\n        return 5;\n    }\n\n    /**\n     * Get the number of minutes to throttle for.\n     */\n    public function decayMinutes(): int\n    {\n        return 1;\n    }\n}\n"
  },
  {
    "path": "app/Access/Controllers/UserInviteController.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Controllers;\n\nuse BookStack\\Access\\UserInviteService;\nuse BookStack\\Exceptions\\UserTokenExpiredException;\nuse BookStack\\Exceptions\\UserTokenNotFoundException;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Users\\UserRepo;\nuse Exception;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Redirector;\nuse Illuminate\\Support\\Facades\\Hash;\nuse Illuminate\\Validation\\Rules\\Password;\n\nclass UserInviteController extends Controller\n{\n    protected UserInviteService $inviteService;\n    protected UserRepo $userRepo;\n\n    /**\n     * Create a new controller instance.\n     */\n    public function __construct(UserInviteService $inviteService, UserRepo $userRepo)\n    {\n        $this->middleware('guest');\n        $this->middleware('guard:standard');\n\n        $this->inviteService = $inviteService;\n        $this->userRepo = $userRepo;\n    }\n\n    /**\n     * Show the page for the user to set the password for their account.\n     *\n     * @throws Exception\n     */\n    public function showSetPassword(string $token)\n    {\n        try {\n            $this->inviteService->checkTokenAndGetUserId($token);\n        } catch (Exception $exception) {\n            return $this->handleTokenException($exception);\n        }\n\n        return view('auth.invite-set-password', [\n            'token' => $token,\n        ]);\n    }\n\n    /**\n     * Sets the password for an invited user and then grants them access.\n     *\n     * @throws Exception\n     */\n    public function setPassword(Request $request, string $token)\n    {\n        $this->validate($request, [\n            'password' => ['required', Password::default()],\n        ]);\n\n        try {\n            $userId = $this->inviteService->checkTokenAndGetUserId($token);\n        } catch (Exception $exception) {\n            return $this->handleTokenException($exception);\n        }\n\n        $user = $this->userRepo->getById($userId);\n        $user->password = Hash::make($request->get('password'));\n        $user->email_confirmed = true;\n        $user->save();\n\n        $this->inviteService->deleteByUser($user);\n        $this->showSuccessNotification(trans('auth.user_invite_success_login', ['appName' => setting('app-name')]));\n\n        return redirect('/login');\n    }\n\n    /**\n     * Check and validate the exception thrown when checking an invite token.\n     *\n     * @throws Exception\n     *\n     * @return RedirectResponse|Redirector\n     */\n    protected function handleTokenException(Exception $exception)\n    {\n        if ($exception instanceof UserTokenNotFoundException) {\n            return redirect('/');\n        }\n\n        if ($exception instanceof UserTokenExpiredException) {\n            $this->showErrorNotification(trans('errors.invite_token_expired'));\n\n            return redirect('/password/email');\n        }\n\n        throw $exception;\n    }\n}\n"
  },
  {
    "path": "app/Access/EmailConfirmationService.php",
    "content": "<?php\n\nnamespace BookStack\\Access;\n\nuse BookStack\\Access\\Notifications\\ConfirmEmailNotification;\nuse BookStack\\Exceptions\\ConfirmationEmailException;\nuse BookStack\\Users\\Models\\User;\n\nclass EmailConfirmationService extends UserTokenService\n{\n    protected string $tokenTable = 'email_confirmations';\n    protected int $expiryTime = 24;\n\n    /**\n     * Create new confirmation for a user,\n     * Also removes any existing old ones.\n     *\n     * @throws ConfirmationEmailException\n     */\n    public function sendConfirmation(User $user): void\n    {\n        if ($user->email_confirmed) {\n            throw new ConfirmationEmailException(trans('errors.email_already_confirmed'), '/login');\n        }\n\n        $this->deleteByUser($user);\n        $token = $this->createTokenForUser($user);\n\n        $user->notify(new ConfirmEmailNotification($token));\n    }\n\n    /**\n     * Check if confirmation is required in this instance.\n     */\n    public function confirmationRequired(): bool\n    {\n        return setting('registration-confirmation')\n            || setting('registration-restrict');\n    }\n}\n"
  },
  {
    "path": "app/Access/ExternalBaseUserProvider.php",
    "content": "<?php\n\nnamespace BookStack\\Access;\n\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Contracts\\Auth\\UserProvider;\n\nclass ExternalBaseUserProvider implements UserProvider\n{\n    /**\n     * Retrieve a user by their unique identifier.\n     */\n    public function retrieveById(mixed $identifier): ?Authenticatable\n    {\n        return User::query()->find($identifier);\n    }\n\n    /**\n     * Retrieve a user by their unique identifier and \"remember me\" token.\n     *\n     * @param string $token\n     */\n    public function retrieveByToken(mixed $identifier, $token): null\n    {\n        return null;\n    }\n\n    /**\n     * Update the \"remember me\" token for the given user in storage.\n     *\n     * @param Authenticatable $user\n     * @param string          $token\n     *\n     * @return void\n     */\n    public function updateRememberToken(Authenticatable $user, $token)\n    {\n        //\n    }\n\n    /**\n     * Retrieve a user by the given credentials.\n     */\n    public function retrieveByCredentials(array $credentials): ?Authenticatable\n    {\n        return User::query()\n            ->where('external_auth_id', $credentials['external_auth_id'])\n            ->first();\n    }\n\n    /**\n     * Validate a user against the given credentials.\n     */\n    public function validateCredentials(Authenticatable $user, array $credentials): bool\n    {\n        // Should be done in the guard.\n        return false;\n    }\n\n    public function rehashPasswordIfRequired(Authenticatable $user, #[\\SensitiveParameter] array $credentials, bool $force = false)\n    {\n        // No action to perform, any passwords are external in the auth system\n    }\n}\n"
  },
  {
    "path": "app/Access/GroupSyncService.php",
    "content": "<?php\n\nnamespace BookStack\\Access;\n\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Support\\Collection;\n\nclass GroupSyncService\n{\n    /**\n     * Check a role against an array of group names to see if it matches.\n     * Checked against role 'external_auth_id' if set otherwise the name of the role.\n     */\n    protected function roleMatchesGroupNames(Role $role, array $groupNames): bool\n    {\n        if ($role->external_auth_id) {\n            return $this->externalIdMatchesGroupNames($role->external_auth_id, $groupNames);\n        }\n\n        $roleName = str_replace(' ', '-', trim(strtolower($role->display_name)));\n\n        return in_array($roleName, $groupNames);\n    }\n\n    /**\n     * Check if the given external auth ID string matches one of the given group names.\n     */\n    protected function externalIdMatchesGroupNames(string $externalId, array $groupNames): bool\n    {\n        foreach ($this->parseRoleExternalAuthId($externalId) as $externalAuthId) {\n            if (in_array($externalAuthId, $groupNames)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    protected function parseRoleExternalAuthId(string $externalId): array\n    {\n        $inputIds = preg_split('/(?<!\\\\\\),/', strtolower($externalId));\n        $cleanIds = [];\n\n        foreach ($inputIds as $inputId) {\n            $cleanIds[] = str_replace('\\,', ',', trim($inputId));\n        }\n\n        return $cleanIds;\n    }\n\n    /**\n     * Match an array of group names to BookStack system roles.\n     * Formats group names to be lower-case and hyphenated.\n     */\n    protected function matchGroupsToSystemsRoles(array $groupNames): Collection\n    {\n        foreach ($groupNames as $i => $groupName) {\n            $groupNames[$i] = str_replace(' ', '-', trim(strtolower($groupName)));\n        }\n\n        $roles = Role::query()->get(['id', 'external_auth_id', 'display_name']);\n        $matchedRoles = $roles->filter(function (Role $role) use ($groupNames) {\n            return $this->roleMatchesGroupNames($role, $groupNames);\n        });\n\n        return $matchedRoles->pluck('id');\n    }\n\n    /**\n     * Sync the groups to the user roles for the current user.\n     */\n    public function syncUserWithFoundGroups(User $user, array $userGroups, bool $detachExisting): void\n    {\n        // Get the ids for the roles from the names\n        $groupsAsRoles = $this->matchGroupsToSystemsRoles($userGroups);\n\n        // Sync groups\n        if ($detachExisting) {\n            $user->roles()->sync($groupsAsRoles);\n            $user->attachDefaultRole();\n        } else {\n            $user->roles()->syncWithoutDetaching($groupsAsRoles);\n        }\n    }\n}\n"
  },
  {
    "path": "app/Access/Guards/AsyncExternalBaseSessionGuard.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Guards;\n\n/**\n * External Auth Session Guard.\n *\n * The login process for external auth (SAML2/OIDC) is async in nature, meaning it does not fit very well\n * into the default laravel 'Guard' auth flow. Instead, most of the logic is done via the relevant\n * controller and services. This class provides a safer, thin version of SessionGuard.\n */\nclass AsyncExternalBaseSessionGuard extends ExternalBaseSessionGuard\n{\n    /**\n     * Validate a user's credentials.\n     */\n    public function validate(array $credentials = []): bool\n    {\n        return false;\n    }\n\n    /**\n     * Attempt to authenticate a user using the given credentials.\n     *\n     * @param bool  $remember\n     */\n    public function attempt(array $credentials = [], $remember = false): bool\n    {\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/Access/Guards/ExternalBaseSessionGuard.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Guards;\n\nuse BookStack\\Access\\RegistrationService;\nuse Illuminate\\Auth\\GuardHelpers;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Contracts\\Auth\\StatefulGuard;\nuse Illuminate\\Contracts\\Auth\\UserProvider;\nuse Illuminate\\Contracts\\Session\\Session;\n\n/**\n * Class BaseSessionGuard\n * A base implementation of a session guard. Is a copy of the default Laravel\n * guard with 'remember' functionality removed. Basic auth and event emission\n * has also been removed to keep this simple. Designed to be extended by external\n * Auth Guards.\n */\nclass ExternalBaseSessionGuard implements StatefulGuard\n{\n    use GuardHelpers;\n\n    /**\n     * The name of the Guard. Typically \"session\".\n     *\n     * Corresponds to guard name in authentication configuration.\n     */\n    protected readonly string $name;\n\n    /**\n     * The user we last attempted to retrieve.\n     */\n    protected Authenticatable|null $lastAttempted;\n\n    /**\n     * The session used by the guard.\n     */\n    protected Session $session;\n\n    /**\n     * Indicates if the logout method has been called.\n     */\n    protected bool $loggedOut = false;\n\n    /**\n     * Service to handle common registration actions.\n     */\n    protected RegistrationService $registrationService;\n\n    /**\n     * Create a new authentication guard.\n     */\n    public function __construct(string $name, UserProvider $provider, Session $session, RegistrationService $registrationService)\n    {\n        $this->name = $name;\n        $this->session = $session;\n        $this->provider = $provider;\n        $this->registrationService = $registrationService;\n    }\n\n    /**\n     * Get the currently authenticated user.\n     */\n    public function user(): Authenticatable|null\n    {\n        if ($this->loggedOut) {\n            return null;\n        }\n\n        // If we've already retrieved the user for the current request we can just\n        // return it back immediately. We do not want to fetch the user data on\n        // every call to this method because that would be tremendously slow.\n        if (!is_null($this->user)) {\n            return $this->user;\n        }\n\n        $id = $this->session->get($this->getName());\n\n        // First we will try to load the user using the\n        // identifier in the session if one exists.\n        if (!is_null($id)) {\n            $this->user = $this->provider->retrieveById($id);\n        }\n\n        return $this->user;\n    }\n\n    /**\n     * Get the ID for the currently authenticated user.\n     */\n    public function id(): int|null\n    {\n        if ($this->loggedOut) {\n            return null;\n        }\n\n        return $this->user()\n            ? $this->user()->getAuthIdentifier()\n            : $this->session->get($this->getName());\n    }\n\n    /**\n     * Log a user into the application without sessions or cookies.\n     */\n    public function once(array $credentials = []): bool\n    {\n        if ($this->validate($credentials)) {\n            $this->setUser($this->lastAttempted);\n\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     * Log the given user ID into the application without sessions or cookies.\n     */\n    public function onceUsingId($id): Authenticatable|false\n    {\n        if (!is_null($user = $this->provider->retrieveById($id))) {\n            $this->setUser($user);\n\n            return $user;\n        }\n\n        return false;\n    }\n\n    /**\n     * Validate a user's credentials.\n     */\n    public function validate(array $credentials = []): bool\n    {\n        return false;\n    }\n\n    /**\n     * Attempt to authenticate a user using the given credentials.\n     * @param bool $remember\n     */\n    public function attempt(array $credentials = [], $remember = false): bool\n    {\n        return false;\n    }\n\n    /**\n     * Log the given user ID into the application.\n     * @param bool  $remember\n     */\n    public function loginUsingId(mixed $id, $remember = false): Authenticatable|false\n    {\n        // Always return false as to disable this method,\n        // Logins should route through LoginService.\n        return false;\n    }\n\n    /**\n     * Log a user into the application.\n     *\n     * @param bool $remember\n     */\n    public function login(Authenticatable $user, $remember = false): void\n    {\n        $this->updateSession($user->getAuthIdentifier());\n\n        $this->setUser($user);\n    }\n\n    /**\n     * Update the session with the given ID.\n     */\n    protected function updateSession(string|int $id): void\n    {\n        $this->session->put($this->getName(), $id);\n\n        $this->session->migrate(true);\n    }\n\n    /**\n     * Log the user out of the application.\n     */\n    public function logout(): void\n    {\n        $this->clearUserDataFromStorage();\n\n        // Now we will clear the users out of memory so they are no longer available\n        // as the user is no longer considered as being signed into this\n        // application and should not be available here.\n        $this->user = null;\n\n        $this->loggedOut = true;\n    }\n\n    /**\n     * Remove the user data from the session and cookies.\n     */\n    protected function clearUserDataFromStorage(): void\n    {\n        $this->session->remove($this->getName());\n    }\n\n    /**\n     * Get the last user we attempted to authenticate.\n     */\n    public function getLastAttempted(): Authenticatable\n    {\n        return $this->lastAttempted;\n    }\n\n    /**\n     * Get a unique identifier for the auth session value.\n     */\n    public function getName(): string\n    {\n        return 'login_' . $this->name . '_' . sha1(static::class);\n    }\n\n    /**\n     * Determine if the user was authenticated via \"remember me\" cookie.\n     */\n    public function viaRemember(): bool\n    {\n        return false;\n    }\n\n    /**\n     * Return the currently cached user.\n     */\n    public function getUser(): Authenticatable|null\n    {\n        return $this->user;\n    }\n\n    /**\n     * Set the current user.\n     */\n    public function setUser(Authenticatable $user): self\n    {\n        $this->user = $user;\n\n        $this->loggedOut = false;\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "app/Access/Guards/LdapSessionGuard.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Guards;\n\nuse BookStack\\Access\\LdapService;\nuse BookStack\\Access\\RegistrationService;\nuse BookStack\\Exceptions\\JsonDebugException;\nuse BookStack\\Exceptions\\LdapException;\nuse BookStack\\Exceptions\\LoginAttemptEmailNeededException;\nuse BookStack\\Exceptions\\LoginAttemptException;\nuse BookStack\\Exceptions\\UserRegistrationException;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Contracts\\Auth\\UserProvider;\nuse Illuminate\\Contracts\\Session\\Session;\nuse Illuminate\\Support\\Str;\n\nclass LdapSessionGuard extends ExternalBaseSessionGuard\n{\n    protected LdapService $ldapService;\n\n    /**\n     * LdapSessionGuard constructor.\n     */\n    public function __construct(\n        $name,\n        UserProvider $provider,\n        Session $session,\n        LdapService $ldapService,\n        RegistrationService $registrationService\n    ) {\n        $this->ldapService = $ldapService;\n        parent::__construct($name, $provider, $session, $registrationService);\n    }\n\n    /**\n     * Validate a user's credentials.\n     *\n     * @throws LdapException\n     */\n    public function validate(array $credentials = []): bool\n    {\n        $userDetails = $this->ldapService->getUserDetails($credentials['username']);\n\n        if (isset($userDetails['uid'])) {\n            $this->lastAttempted = $this->provider->retrieveByCredentials([\n                'external_auth_id' => $userDetails['uid'],\n            ]);\n        }\n\n        return $this->ldapService->validateUserCredentials($userDetails, $credentials['password']);\n    }\n\n    /**\n     * Attempt to authenticate a user using the given credentials.\n     *\n     * @param bool  $remember\n     *\n     * @throws LdapException\n     * @throws LoginAttemptException\n     * @throws JsonDebugException\n     */\n    public function attempt(array $credentials = [], $remember = false): bool\n    {\n        $username = $credentials['username'];\n        $userDetails = $this->ldapService->getUserDetails($username);\n\n        $user = null;\n        if (isset($userDetails['uid'])) {\n            $this->lastAttempted = $user = $this->provider->retrieveByCredentials([\n                'external_auth_id' => $userDetails['uid'],\n            ]);\n        }\n\n        if (!$this->ldapService->validateUserCredentials($userDetails, $credentials['password'])) {\n            return false;\n        }\n\n        if (is_null($user)) {\n            try {\n                $user = $this->createNewFromLdapAndCreds($userDetails, $credentials);\n            } catch (UserRegistrationException $exception) {\n                throw new LoginAttemptException($exception->getMessage());\n            }\n        }\n\n        // Sync LDAP groups if required\n        if ($this->ldapService->shouldSyncGroups()) {\n            $this->ldapService->syncGroups($user, $username);\n        }\n\n        // Attach avatar if non-existent\n        if (!$user->avatar()->exists()) {\n            $this->ldapService->saveAndAttachAvatar($user, $userDetails);\n        }\n\n        $this->login($user, $remember);\n\n        return true;\n    }\n\n    /**\n     * Create a new user from the given ldap credentials and login credentials.\n     *\n     * @throws LoginAttemptEmailNeededException\n     * @throws LoginAttemptException\n     * @throws UserRegistrationException\n     */\n    protected function createNewFromLdapAndCreds(array $ldapUserDetails, array $credentials): User\n    {\n        $email = trim($ldapUserDetails['email'] ?: ($credentials['email'] ?? ''));\n\n        if (empty($email)) {\n            throw new LoginAttemptEmailNeededException();\n        }\n\n        $details = [\n            'name'             => $ldapUserDetails['name'],\n            'email'            => $ldapUserDetails['email'] ?: $credentials['email'],\n            'external_auth_id' => $ldapUserDetails['uid'],\n            'password'         => Str::random(32),\n        ];\n\n        $user = $this->registrationService->registerUser($details, null, false);\n        $this->ldapService->saveAndAttachAvatar($user, $ldapUserDetails);\n\n        return $user;\n    }\n}\n"
  },
  {
    "path": "app/Access/Ldap.php",
    "content": "<?php\n\nnamespace BookStack\\Access;\n\n/**\n * Class Ldap\n * An object-orientated thin abstraction wrapper for common PHP LDAP functions.\n * Allows the standard LDAP functions to be mocked for testing.\n */\nclass Ldap\n{\n    /**\n     * Connect to an LDAP server.\n     *\n     * @return resource|\\LDAP\\Connection|false\n     */\n    public function connect(string $hostName)\n    {\n        return ldap_connect($hostName);\n    }\n\n    /**\n     * Set the value of an LDAP option for the given connection.\n     *\n     * @param resource|\\LDAP\\Connection|null $ldapConnection\n     */\n    public function setOption($ldapConnection, int $option, mixed $value): bool\n    {\n        return ldap_set_option($ldapConnection, $option, $value);\n    }\n\n    /**\n     * Start TLS on the given LDAP connection.\n     */\n    public function startTls($ldapConnection): bool\n    {\n        return ldap_start_tls($ldapConnection);\n    }\n\n    /**\n     * Set the version number for the given LDAP connection.\n     *\n     * @param resource|\\LDAP\\Connection $ldapConnection\n     */\n    public function setVersion($ldapConnection, int $version): bool\n    {\n        return $this->setOption($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, $version);\n    }\n\n    /**\n     * Search LDAP tree using the provided filter.\n     *\n     * @param resource|\\LDAP\\Connection   $ldapConnection\n     *\n     * @return \\LDAP\\Result|array|false\n     */\n    public function search($ldapConnection, string $baseDn, string $filter, array $attributes = [])\n    {\n        return ldap_search($ldapConnection, $baseDn, $filter, $attributes);\n    }\n\n    /**\n     * Read an entry from the LDAP tree.\n     *\n     * @param resource|\\Ldap\\Connection $ldapConnection\n     *\n     * @return \\LDAP\\Result|array|false\n     */\n    public function read($ldapConnection, string $baseDn, string $filter, array $attributes = [])\n    {\n        return ldap_read($ldapConnection, $baseDn, $filter, $attributes);\n    }\n\n    /**\n     * Get entries from an LDAP search result.\n     *\n     * @param resource|\\LDAP\\Connection $ldapConnection\n     * @param resource|\\LDAP\\Result $ldapSearchResult\n     */\n    public function getEntries($ldapConnection, $ldapSearchResult): array|false\n    {\n        return ldap_get_entries($ldapConnection, $ldapSearchResult);\n    }\n\n    /**\n     * Search and get entries immediately.\n     *\n     * @param resource|\\LDAP\\Connection   $ldapConnection\n     */\n    public function searchAndGetEntries($ldapConnection, string $baseDn, string $filter, array $attributes = []): array|false\n    {\n        $search = $this->search($ldapConnection, $baseDn, $filter, $attributes);\n\n        return $this->getEntries($ldapConnection, $search);\n    }\n\n    /**\n     * Bind to LDAP directory.\n     *\n     * @param resource|\\LDAP\\Connection $ldapConnection\n     */\n    public function bind($ldapConnection, ?string $bindRdn = null, ?string $bindPassword = null): bool\n    {\n        return ldap_bind($ldapConnection, $bindRdn, $bindPassword);\n    }\n\n    /**\n     * Explode an LDAP dn string into an array of components.\n     */\n    public function explodeDn(string $dn, int $withAttrib): array|false\n    {\n        return ldap_explode_dn($dn, $withAttrib);\n    }\n\n    /**\n     * Escape a string for use in an LDAP filter.\n     */\n    public function escape(string $value, string $ignore = '', int $flags = 0): string\n    {\n        return ldap_escape($value, $ignore, $flags);\n    }\n}\n"
  },
  {
    "path": "app/Access/LdapService.php",
    "content": "<?php\n\nnamespace BookStack\\Access;\n\nuse BookStack\\Exceptions\\JsonDebugException;\nuse BookStack\\Exceptions\\LdapException;\nuse BookStack\\Uploads\\UserAvatars;\nuse BookStack\\Users\\Models\\User;\nuse ErrorException;\nuse Illuminate\\Support\\Facades\\Log;\n\n/**\n * Class LdapService\n * Handles any app-specific LDAP tasks.\n */\nclass LdapService\n{\n    /**\n     * @var resource|\\LDAP\\Connection\n     */\n    protected $ldapConnection;\n\n    protected array $config;\n    protected bool $enabled;\n\n    public function __construct(\n        protected Ldap $ldap,\n        protected UserAvatars $userAvatars,\n        protected GroupSyncService $groupSyncService\n    ) {\n        $this->config = config('services.ldap');\n        $this->enabled = config('auth.method') === 'ldap';\n    }\n\n    /**\n     * Check if groups should be synced.\n     */\n    public function shouldSyncGroups(): bool\n    {\n        return $this->enabled && $this->config['user_to_groups'] !== false;\n    }\n\n    /**\n     * Search for attributes for a specific user on the ldap.\n     *\n     * @throws LdapException\n     */\n    private function getUserWithAttributes(string $userName, array $attributes): ?array\n    {\n        $ldapConnection = $this->getConnection();\n        $this->bindSystemUser($ldapConnection);\n\n        // Clean attributes\n        foreach ($attributes as $index => $attribute) {\n            if (str_starts_with($attribute, 'BIN;')) {\n                $attributes[$index] = substr($attribute, strlen('BIN;'));\n            }\n        }\n\n        // Find user\n        $userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]);\n        $baseDn = $this->config['base_dn'];\n\n        $followReferrals = $this->config['follow_referrals'] ? 1 : 0;\n        $this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);\n        $users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, $attributes);\n        if ($users['count'] === 0) {\n            return null;\n        }\n\n        return $users[0];\n    }\n\n    /**\n     * Build the user display name from the (potentially multiple) attributes defined by the configuration.\n     */\n    protected function getUserDisplayName(array $userDetails, array $displayNameAttrs, string $defaultValue): string\n    {\n        $displayNameParts = [];\n        foreach ($displayNameAttrs as $dnAttr) {\n            $dnComponent = $this->getUserResponseProperty($userDetails, $dnAttr, null);\n            if ($dnComponent) {\n                $displayNameParts[] = $dnComponent;\n            }\n        }\n\n        if (empty($displayNameParts)) {\n            return $defaultValue;\n        }\n\n        return implode(' ', $displayNameParts);\n    }\n\n    /**\n     * Get the details of a user from LDAP using the given username.\n     * User found via configurable user filter.\n     *\n     * @throws LdapException|JsonDebugException\n     */\n    public function getUserDetails(string $userName): ?array\n    {\n        $idAttr = $this->config['id_attribute'];\n        $emailAttr = $this->config['email_attribute'];\n        $displayNameAttrs = explode('|', $this->config['display_name_attribute']);\n        $thumbnailAttr = $this->config['thumbnail_attribute'];\n\n        $user = $this->getUserWithAttributes($userName, array_filter([\n            'cn', 'dn', $idAttr, $emailAttr, ...$displayNameAttrs, $thumbnailAttr,\n        ]));\n\n        if (is_null($user)) {\n            return null;\n        }\n\n        $nameDefault = $this->getUserResponseProperty($user, 'cn', null);\n        if (is_null($nameDefault)) {\n            $nameDefault = ldap_explode_dn($user['dn'], 1)[0] ?? $user['dn'];\n        }\n\n        $formatted = [\n            'uid'   => $this->getUserResponseProperty($user, $idAttr, $user['dn']),\n            'name'  => $this->getUserDisplayName($user, $displayNameAttrs, $nameDefault),\n            'dn'    => $user['dn'],\n            'email' => $this->getUserResponseProperty($user, $emailAttr, null),\n            'avatar' => $thumbnailAttr ? $this->getUserResponseProperty($user, $thumbnailAttr, null) : null,\n        ];\n\n        if ($this->config['dump_user_details']) {\n            throw new JsonDebugException([\n                'details_from_ldap'        => $user,\n                'details_bookstack_parsed' => $formatted,\n            ]);\n        }\n\n        return $formatted;\n    }\n\n    /**\n     * Get a property from an LDAP user response fetch.\n     * Handles properties potentially being part of an array.\n     * If the given key is prefixed with 'BIN;', that indicator will be stripped\n     * from the key and any fetched values will be converted from binary to hex.\n     */\n    protected function getUserResponseProperty(array $userDetails, string $propertyKey, $defaultValue)\n    {\n        $isBinary = str_starts_with($propertyKey, 'BIN;');\n        $propertyKey = strtolower($propertyKey);\n        $value = $defaultValue;\n\n        if ($isBinary) {\n            $propertyKey = substr($propertyKey, strlen('BIN;'));\n        }\n\n        if (isset($userDetails[$propertyKey])) {\n            $value = (is_array($userDetails[$propertyKey]) ? $userDetails[$propertyKey][0] : $userDetails[$propertyKey]);\n            if ($isBinary) {\n                $value = bin2hex($value);\n            }\n        }\n\n        return $value;\n    }\n\n    /**\n     * Check if the given credentials are valid for the given user.\n     *\n     * @throws LdapException\n     */\n    public function validateUserCredentials(?array $ldapUserDetails, string $password): bool\n    {\n        if (is_null($ldapUserDetails)) {\n            return false;\n        }\n\n        $ldapConnection = $this->getConnection();\n\n        try {\n            $ldapBind = $this->ldap->bind($ldapConnection, $ldapUserDetails['dn'], $password);\n        } catch (ErrorException $e) {\n            $ldapBind = false;\n        }\n\n        return $ldapBind;\n    }\n\n    /**\n     * Bind the system user to the LDAP connection using the given credentials\n     * otherwise anonymous access is attempted.\n     *\n     * @param resource|\\LDAP\\Connection $connection\n     *\n     * @throws LdapException\n     */\n    protected function bindSystemUser($connection): void\n    {\n        $ldapDn = $this->config['dn'];\n        $ldapPass = $this->config['pass'];\n\n        $isAnonymous = ($ldapDn === false || $ldapPass === false);\n        if ($isAnonymous) {\n            $ldapBind = $this->ldap->bind($connection);\n        } else {\n            $ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass);\n        }\n\n        if (!$ldapBind) {\n            throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed')));\n        }\n    }\n\n    /**\n     * Get the connection to the LDAP server.\n     * Creates a new connection if one does not exist.\n     *\n     * @throws LdapException\n     *\n     * @return resource|\\LDAP\\Connection\n     */\n    protected function getConnection()\n    {\n        if ($this->ldapConnection !== null) {\n            return $this->ldapConnection;\n        }\n\n        // Check LDAP extension in installed\n        if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {\n            throw new LdapException(trans('errors.ldap_extension_not_installed'));\n        }\n\n        // Disable certificate verification.\n        // This option works globally and must be set before a connection is created.\n        if ($this->config['tls_insecure']) {\n            $this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);\n        }\n\n        // Configure any user-provided CA cert files for LDAP.\n        // This option works globally and must be set before a connection is created.\n        if ($this->config['tls_ca_cert']) {\n            $this->configureTlsCaCerts($this->config['tls_ca_cert']);\n        }\n\n        $ldapHost = $this->parseServerString($this->config['server']);\n        $ldapConnection = $this->ldap->connect($ldapHost);\n\n        if ($ldapConnection === false) {\n            throw new LdapException(trans('errors.ldap_cannot_connect'));\n        }\n\n        // Set any required options\n        if ($this->config['version']) {\n            $this->ldap->setVersion($ldapConnection, $this->config['version']);\n        }\n\n        // Start and verify TLS if it's enabled\n        if ($this->config['start_tls']) {\n            try {\n                $started = $this->ldap->startTls($ldapConnection);\n            } catch (\\Exception $exception) {\n                $error = $exception->getMessage() . ' :: ' . ldap_error($ldapConnection);\n                ldap_get_option($ldapConnection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $detail);\n                Log::info(\"LDAP STARTTLS failure: {$error} {$detail}\");\n                throw new LdapException('Could not start TLS connection. Further details in the application log.');\n            }\n            if (!$started) {\n                throw new LdapException('Could not start TLS connection');\n            }\n        }\n\n        $this->ldapConnection = $ldapConnection;\n\n        return $this->ldapConnection;\n    }\n\n    /**\n     * Configure TLS CA certs globally for ldap use.\n     * This will detect if the given path is a directory or file, and set the relevant\n     * LDAP TLS options appropriately otherwise throw an exception if no file/folder found.\n     *\n     * Note: When using a folder, certificates are expected to be correctly named by hash\n     * which can be done via the c_rehash utility.\n     *\n     * @throws LdapException\n     */\n    protected function configureTlsCaCerts(string $caCertPath): void\n    {\n        $errMessage = \"Provided path [{$caCertPath}] for LDAP TLS CA certs could not be resolved to an existing location\";\n        $path = realpath($caCertPath);\n        if ($path === false) {\n            throw new LdapException($errMessage);\n        }\n\n        if (is_dir($path)) {\n            $this->ldap->setOption(null, LDAP_OPT_X_TLS_CACERTDIR, $path);\n        } else if (is_file($path)) {\n            $this->ldap->setOption(null, LDAP_OPT_X_TLS_CACERTFILE, $path);\n        } else {\n            throw new LdapException($errMessage);\n        }\n    }\n\n    /**\n     * Parse an LDAP server string and return the host suitable for a connection.\n     * Is flexible to formats such as 'ldap.example.com:8069' or 'ldaps://ldap.example.com'.\n     */\n    protected function parseServerString(string $serverString): string\n    {\n        if (str_starts_with($serverString, 'ldaps://') || str_starts_with($serverString, 'ldap://')) {\n            return $serverString;\n        }\n\n        return \"ldap://{$serverString}\";\n    }\n\n    /**\n     * Build a filter string by injecting common variables.\n     * Both \"${var}\" and \"{var}\" style placeholders are supported.\n     * Dollar based are old format but supported for compatibility.\n     */\n    protected function buildFilter(string $filterString, array $attrs): string\n    {\n        $newAttrs = [];\n        foreach ($attrs as $key => $attrText) {\n            $escapedText = $this->ldap->escape($attrText);\n            $oldVarKey = '${' . $key . '}';\n            $newVarKey = '{' . $key . '}';\n            $newAttrs[$oldVarKey] = $escapedText;\n            $newAttrs[$newVarKey] = $escapedText;\n        }\n\n        return strtr($filterString, $newAttrs);\n    }\n\n    /**\n     * Get the groups a user is a part of on ldap.\n     *\n     * @throws LdapException\n     * @throws JsonDebugException\n     */\n    public function getUserGroups(string $userName): array\n    {\n        $groupsAttr = $this->config['group_attribute'];\n        $user = $this->getUserWithAttributes($userName, [$groupsAttr]);\n\n        if ($user === null) {\n            return [];\n        }\n\n        $userGroups = $this->extractGroupsFromSearchResponseEntry($user);\n        $allGroups = $this->getGroupsRecursive($userGroups, []);\n        $formattedGroups = $this->extractGroupNamesFromLdapGroupDns($allGroups);\n\n        if ($this->config['dump_user_groups']) {\n            throw new JsonDebugException([\n                'details_from_ldap'            => $user,\n                'parsed_direct_user_groups'    => $userGroups,\n                'parsed_recursive_user_groups' => $allGroups,\n                'parsed_resulting_group_names' => $formattedGroups,\n            ]);\n        }\n\n        return $formattedGroups;\n    }\n\n    protected function extractGroupNamesFromLdapGroupDns(array $groupDNs): array\n    {\n        $names = [];\n\n        foreach ($groupDNs as $groupDN) {\n            $exploded = $this->ldap->explodeDn($groupDN, 1);\n            if ($exploded !== false && count($exploded) > 0) {\n                $names[] = $exploded[0];\n            }\n        }\n\n        return array_unique($names);\n    }\n\n    /**\n     * Build an array of all relevant groups DNs after recursively scanning\n     * across parents of the groups given.\n     *\n     * @throws LdapException\n     */\n    protected function getGroupsRecursive(array $groupDNs, array $checked): array\n    {\n        $groupsToAdd = [];\n        foreach ($groupDNs as $groupDN) {\n            if (in_array($groupDN, $checked)) {\n                continue;\n            }\n\n            $parentGroups = $this->getParentsOfGroup($groupDN);\n            $groupsToAdd = array_merge($groupsToAdd, $parentGroups);\n            $checked[] = $groupDN;\n        }\n\n        $uniqueDNs = array_unique(array_merge($groupDNs, $groupsToAdd), SORT_REGULAR);\n\n        if (empty($groupsToAdd)) {\n            return $uniqueDNs;\n        }\n\n        return $this->getGroupsRecursive($uniqueDNs, $checked);\n    }\n\n    /**\n     * @throws LdapException\n     */\n    protected function getParentsOfGroup(string $groupDN): array\n    {\n        $groupsAttr = strtolower($this->config['group_attribute']);\n        $ldapConnection = $this->getConnection();\n        $this->bindSystemUser($ldapConnection);\n\n        $followReferrals = $this->config['follow_referrals'] ? 1 : 0;\n        $this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);\n        $read = $this->ldap->read($ldapConnection, $groupDN, '(objectClass=*)', [$groupsAttr]);\n        $results = $this->ldap->getEntries($ldapConnection, $read);\n        if ($results['count'] === 0) {\n            return [];\n        }\n\n        return $this->extractGroupsFromSearchResponseEntry($results[0]);\n    }\n\n    /**\n     * Extract an array of group DN values from the given LDAP search response entry\n     */\n    protected function extractGroupsFromSearchResponseEntry(array $ldapEntry): array\n    {\n        $groupsAttr = strtolower($this->config['group_attribute']);\n        $groupDNs = [];\n        $count = 0;\n\n        if (isset($ldapEntry[$groupsAttr]['count'])) {\n            $count = (int) $ldapEntry[$groupsAttr]['count'];\n        }\n\n        for ($i = 0; $i < $count; $i++) {\n            $dn = $ldapEntry[$groupsAttr][$i];\n            if (!in_array($dn, $groupDNs)) {\n                $groupDNs[] = $dn;\n            }\n        }\n\n        return $groupDNs;\n    }\n\n    /**\n     * Sync the LDAP groups to the user roles for the current user.\n     *\n     * @throws LdapException\n     * @throws JsonDebugException\n     */\n    public function syncGroups(User $user, string $username): void\n    {\n        $userLdapGroups = $this->getUserGroups($username);\n        $this->groupSyncService->syncUserWithFoundGroups($user, $userLdapGroups, $this->config['remove_from_groups']);\n    }\n\n    /**\n     * Save and attach an avatar image, if found in the ldap details, and attach\n     * to the given user model.\n     */\n    public function saveAndAttachAvatar(User $user, array $ldapUserDetails): void\n    {\n        if (is_null(config('services.ldap.thumbnail_attribute')) || is_null($ldapUserDetails['avatar'])) {\n            return;\n        }\n\n        try {\n            $imageData = $ldapUserDetails['avatar'];\n            $this->userAvatars->assignToUserFromExistingData($user, $imageData, 'jpg');\n        } catch (\\Exception $exception) {\n            Log::info(\"Failed to use avatar image from LDAP data for user id {$user->id}\");\n        }\n    }\n}\n"
  },
  {
    "path": "app/Access/LoginService.php",
    "content": "<?php\n\nnamespace BookStack\\Access;\n\nuse BookStack\\Access\\Mfa\\MfaSession;\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Exceptions\\LoginAttemptException;\nuse BookStack\\Exceptions\\LoginAttemptInvalidUserException;\nuse BookStack\\Exceptions\\StoppedAuthenticationException;\nuse BookStack\\Facades\\Activity;\nuse BookStack\\Facades\\Theme;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Theming\\ThemeEvents;\nuse BookStack\\Users\\Models\\User;\nuse Exception;\n\nclass LoginService\n{\n    protected const LAST_LOGIN_ATTEMPTED_SESSION_KEY = 'auth-login-last-attempted';\n\n    public function __construct(\n        protected MfaSession $mfaSession,\n        protected EmailConfirmationService $emailConfirmationService,\n        protected SocialDriverManager $socialDriverManager,\n    ) {\n    }\n\n    /**\n     * Log the given user into the system.\n     * Will start a login of the given user but will prevent if there's\n     * a reason to (MFA or Unconfirmed Email).\n     * Returns a boolean to indicate the current login result.\n     *\n     * @throws StoppedAuthenticationException|LoginAttemptInvalidUserException\n     */\n    public function login(User $user, string $method, bool $remember = false): void\n    {\n        if ($user->isGuest()) {\n            throw new LoginAttemptInvalidUserException('Login not allowed for guest user');\n        }\n\n        if ($this->awaitingEmailConfirmation($user) || $this->needsMfaVerification($user)) {\n            $this->setLastLoginAttemptedForUser($user, $method, $remember);\n\n            throw new StoppedAuthenticationException($user, $this);\n        }\n\n        $this->clearLastLoginAttempted();\n        auth()->login($user, $remember);\n        Activity::add(ActivityType::AUTH_LOGIN, \"{$method}; {$user->logDescriptor()}\");\n        Theme::dispatch(ThemeEvents::AUTH_LOGIN, $method, $user);\n\n        // Authenticate on all session guards if a likely admin\n        if ($user->can(Permission::UsersManage) && $user->can(Permission::UserRolesManage)) {\n            $guards = ['standard', 'ldap', 'saml2', 'oidc'];\n            foreach ($guards as $guard) {\n                auth($guard)->login($user);\n            }\n        }\n    }\n\n    /**\n     * Reattempt a system login after a previous stopped attempt.\n     *\n     * @throws Exception\n     */\n    public function reattemptLoginFor(User $user): void\n    {\n        if ($user->id !== ($this->getLastLoginAttemptUser()->id ?? null)) {\n            throw new Exception('Login reattempt user does align with current session state');\n        }\n\n        $lastLoginDetails = $this->getLastLoginAttemptDetails();\n        $this->login($user, $lastLoginDetails['method'], $lastLoginDetails['remember'] ?? false);\n    }\n\n    /**\n     * Get the last user that was attempted to be logged in.\n     * Only exists if the last login attempt had correct credentials\n     * but had been prevented by a secondary factor.\n     */\n    public function getLastLoginAttemptUser(): ?User\n    {\n        $id = $this->getLastLoginAttemptDetails()['user_id'];\n\n        return User::query()->where('id', '=', $id)->first();\n    }\n\n    /**\n     * Get the details of the last login attempt.\n     * Checks upon a ttl of about 1 hour since that last attempted login.\n     *\n     * @return array{user_id: ?string, method: ?string, remember: bool}\n     */\n    protected function getLastLoginAttemptDetails(): array\n    {\n        $value = session()->get(self::LAST_LOGIN_ATTEMPTED_SESSION_KEY);\n        if (!$value) {\n            return ['user_id' => null, 'method' => null, 'remember' => false];\n        }\n\n        [$id, $method, $remember, $time] = explode(':', $value);\n        $hourAgo = time() - (60 * 60);\n        if ($time < $hourAgo) {\n            $this->clearLastLoginAttempted();\n\n            return ['user_id' => null, 'method' => null, 'remember' => false];\n        }\n\n        return ['user_id' => $id, 'method' => $method, 'remember' => boolval($remember)];\n    }\n\n    /**\n     * Set the last login-attempted user.\n     * Must be only used when credentials are correct and a login could be\n     * achieved, but a secondary factor has stopped the login.\n     */\n    protected function setLastLoginAttemptedForUser(User $user, string $method, bool $remember): void\n    {\n        session()->put(\n            self::LAST_LOGIN_ATTEMPTED_SESSION_KEY,\n            implode(':', [$user->id, $method, $remember, time()])\n        );\n    }\n\n    /**\n     * Clear the last login attempted session value.\n     */\n    protected function clearLastLoginAttempted(): void\n    {\n        session()->remove(self::LAST_LOGIN_ATTEMPTED_SESSION_KEY);\n    }\n\n    /**\n     * Check if MFA verification is needed.\n     */\n    public function needsMfaVerification(User $user): bool\n    {\n        return !$this->mfaSession->isVerifiedForUser($user) && $this->mfaSession->isRequiredForUser($user);\n    }\n\n    /**\n     * Check if the given user is awaiting email confirmation.\n     */\n    public function awaitingEmailConfirmation(User $user): bool\n    {\n        return $this->emailConfirmationService->confirmationRequired() && !$user->email_confirmed;\n    }\n\n    /**\n     * Attempt the login of a user using the given credentials.\n     * Meant to mirror Laravel's default guard 'attempt' method\n     * but in a manner that always routes through our login system.\n     * May interrupt the flow if extra authentication requirements are imposed.\n     *\n     * @throws StoppedAuthenticationException\n     * @throws LoginAttemptException\n     */\n    public function attempt(array $credentials, string $method, bool $remember = false): bool\n    {\n        if ($this->areCredentialsForGuest($credentials)) {\n            return false;\n        }\n\n        $result = auth()->attempt($credentials, $remember);\n        if ($result) {\n            $user = auth()->user();\n            auth()->logout();\n            try {\n                $this->login($user, $method, $remember);\n            } catch (LoginAttemptInvalidUserException $e) {\n                // Catch and return false for non-login accounts\n                // so it looks like a normal invalid login.\n                return false;\n            }\n        }\n\n        return $result;\n    }\n\n    /**\n     * Check if the given credentials are likely for the system guest account.\n     */\n    protected function areCredentialsForGuest(array $credentials): bool\n    {\n        if (isset($credentials['email'])) {\n            return User::query()->where('email', '=', $credentials['email'])\n                ->where('system_name', '=', 'public')\n                ->exists();\n        }\n\n        return false;\n    }\n\n    /**\n     * Logs the current user out of the application.\n     * Returns an app post-redirect path.\n     */\n    public function logout(): string\n    {\n        auth()->logout();\n        session()->invalidate();\n        session()->regenerateToken();\n\n        return $this->shouldAutoInitiate() ? '/login?prevent_auto_init=true' : '/';\n    }\n\n    /**\n     * Check if login auto-initiate should be active based upon authentication config.\n     */\n    public function shouldAutoInitiate(): bool\n    {\n        $autoRedirect = config('auth.auto_initiate');\n        if (!$autoRedirect) {\n            return false;\n        }\n\n        $socialDrivers = $this->socialDriverManager->getActive();\n        $authMethod = config('auth.method');\n\n        return count($socialDrivers) === 0 && in_array($authMethod, ['oidc', 'saml2']);\n    }\n}\n"
  },
  {
    "path": "app/Access/Mfa/BackupCodeService.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Mfa;\n\nuse Illuminate\\Support\\Str;\n\nclass BackupCodeService\n{\n    /**\n     * Generate a new set of 16 backup codes.\n     */\n    public function generateNewSet(): array\n    {\n        $codes = [];\n        while (count($codes) < 16) {\n            $code = Str::random(5) . '-' . Str::random(5);\n            if (!in_array($code, $codes)) {\n                $codes[] = strtolower($code);\n            }\n        }\n\n        return $codes;\n    }\n\n    /**\n     * Check if the given code matches one of the available options.\n     */\n    public function inputCodeExistsInSet(string $code, string $codeSet): bool\n    {\n        $cleanCode = $this->cleanInputCode($code);\n        $codes = json_decode($codeSet);\n\n        return in_array($cleanCode, $codes);\n    }\n\n    /**\n     * Remove the given input code from the given available options.\n     * Will return a JSON string containing the codes.\n     */\n    public function removeInputCodeFromSet(string $code, string $codeSet): string\n    {\n        $cleanCode = $this->cleanInputCode($code);\n        $codes = json_decode($codeSet);\n        $pos = array_search($cleanCode, $codes, true);\n        array_splice($codes, $pos, 1);\n\n        return json_encode($codes);\n    }\n\n    /**\n     * Count the number of codes in the given set.\n     */\n    public function countCodesInSet(string $codeSet): int\n    {\n        return count(json_decode($codeSet));\n    }\n\n    protected function cleanInputCode(string $code): string\n    {\n        return strtolower(str_replace(' ', '-', trim($code)));\n    }\n}\n"
  },
  {
    "path": "app/Access/Mfa/MfaSession.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Mfa;\n\nuse BookStack\\Users\\Models\\User;\n\nclass MfaSession\n{\n    /**\n     * Check if MFA is required for the given user.\n     */\n    public function isRequiredForUser(User $user): bool\n    {\n        return $user->mfaValues()->exists() || $this->userRoleEnforcesMfa($user);\n    }\n\n    /**\n     * Check if the given user is pending MFA setup.\n     * (MFA required but not yet configured).\n     */\n    public function isPendingMfaSetup(User $user): bool\n    {\n        return $this->isRequiredForUser($user) && !$user->mfaValues()->exists();\n    }\n\n    /**\n     * Check if a role of the given user enforces MFA.\n     */\n    protected function userRoleEnforcesMfa(User $user): bool\n    {\n        return $user->roles()\n            ->where('mfa_enforced', '=', true)\n            ->exists();\n    }\n\n    /**\n     * Check if the current MFA session has already been verified for the given user.\n     */\n    public function isVerifiedForUser(User $user): bool\n    {\n        return session()->get($this->getMfaVerifiedSessionKey($user)) === 'true';\n    }\n\n    /**\n     * Mark the current session as MFA-verified.\n     */\n    public function markVerifiedForUser(User $user): void\n    {\n        session()->put($this->getMfaVerifiedSessionKey($user), 'true');\n    }\n\n    /**\n     * Get the session key in which the MFA verification status is stored.\n     */\n    protected function getMfaVerifiedSessionKey(User $user): string\n    {\n        return 'mfa-verification-passed:' . $user->id;\n    }\n}\n"
  },
  {
    "path": "app/Access/Mfa/MfaValue.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Mfa;\n\nuse BookStack\\Users\\Models\\User;\nuse Carbon\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Model;\n\n/**\n * @property int    $id\n * @property int    $user_id\n * @property string $method\n * @property string $value\n * @property Carbon $created_at\n * @property Carbon $updated_at\n */\nclass MfaValue extends Model\n{\n    use HasFactory;\n\n    protected static $unguarded = true;\n\n    const METHOD_TOTP = 'totp';\n    const METHOD_BACKUP_CODES = 'backup_codes';\n\n    /**\n     * Get all the MFA methods available.\n     */\n    public static function allMethods(): array\n    {\n        return [self::METHOD_TOTP, self::METHOD_BACKUP_CODES];\n    }\n\n    /**\n     * Upsert a new MFA value for the given user and method\n     * using the provided value.\n     */\n    public static function upsertWithValue(User $user, string $method, string $value): void\n    {\n        /** @var MfaValue $mfaVal */\n        $mfaVal = static::query()->firstOrNew([\n            'user_id' => $user->id,\n            'method'  => $method,\n        ]);\n        $mfaVal->setValue($value);\n        $mfaVal->save();\n    }\n\n    /**\n     * Easily get the decrypted MFA value for the given user and method.\n     */\n    public static function getValueForUser(User $user, string $method): ?string\n    {\n        /** @var MfaValue $mfaVal */\n        $mfaVal = static::query()\n            ->where('user_id', '=', $user->id)\n            ->where('method', '=', $method)\n            ->first();\n\n        return $mfaVal ? $mfaVal->getValue() : null;\n    }\n\n    /**\n     * Decrypt the value attribute upon access.\n     */\n    protected function getValue(): string\n    {\n        return decrypt($this->value);\n    }\n\n    /**\n     * Encrypt the value attribute upon access.\n     */\n    protected function setValue($value): void\n    {\n        $this->value = encrypt($value);\n    }\n}\n"
  },
  {
    "path": "app/Access/Mfa/TotpService.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Mfa;\n\nuse BaconQrCode\\Renderer\\Color\\Rgb;\nuse BaconQrCode\\Renderer\\Image\\SvgImageBackEnd;\nuse BaconQrCode\\Renderer\\ImageRenderer;\nuse BaconQrCode\\Renderer\\RendererStyle\\Fill;\nuse BaconQrCode\\Renderer\\RendererStyle\\RendererStyle;\nuse BaconQrCode\\Writer;\nuse BookStack\\Users\\Models\\User;\nuse PragmaRX\\Google2FA\\Google2FA;\nuse PragmaRX\\Google2FA\\Support\\Constants;\n\nclass TotpService\n{\n    public function __construct(\n        protected Google2FA $google2fa\n    ) {\n        $this->google2fa = $google2fa;\n        // Use SHA1 as a default, Personal testing of other options in 2021 found\n        // many apps lack support for other algorithms yet still will scan\n        // the code causing a confusing UX.\n        $this->google2fa->setAlgorithm(Constants::SHA1);\n    }\n\n    /**\n     * Generate a new totp secret key.\n     */\n    public function generateSecret(): string\n    {\n        /** @noinspection PhpUnhandledExceptionInspection */\n        return $this->google2fa->generateSecretKey();\n    }\n\n    /**\n     * Generate a TOTP URL from a secret key.\n     */\n    public function generateUrl(string $secret, User $user): string\n    {\n        return $this->google2fa->getQRCodeUrl(\n            setting('app-name'),\n            $user->email,\n            $secret\n        );\n    }\n\n    /**\n     * Generate a QR code to display a TOTP URL.\n     */\n    public function generateQrCodeSvg(string $url): string\n    {\n        $color = Fill::uniformColor(new Rgb(255, 255, 255), new Rgb(32, 110, 167));\n\n        return (new Writer(\n            new ImageRenderer(\n                new RendererStyle(192, 4, null, null, $color),\n                new SvgImageBackEnd()\n            )\n        ))->writeString($url);\n    }\n\n    /**\n     * Verify that the user provided code is valid for the secret.\n     * The secret must be known, not user-provided.\n     */\n    public function verifyCode(string $code, string $secret): bool\n    {\n        /** @noinspection PhpUnhandledExceptionInspection */\n        return $this->google2fa->verifyKey($secret, $code);\n    }\n}\n"
  },
  {
    "path": "app/Access/Mfa/TotpValidationRule.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Mfa;\n\nuse Closure;\nuse Illuminate\\Contracts\\Validation\\ValidationRule;\n\nclass TotpValidationRule implements ValidationRule\n{\n    /**\n     * Create a new rule instance.\n     * Takes the TOTP secret that must be system provided, not user provided.\n     */\n    public function __construct(\n        protected string $secret,\n        protected TotpService $totpService,\n    ) {\n    }\n\n    public function validate(string $attribute, mixed $value, Closure $fail): void\n    {\n        $passes = $this->totpService->verifyCode($value, $this->secret);\n        if (!$passes) {\n            $fail(trans('validation.totp'));\n        }\n    }\n}\n"
  },
  {
    "path": "app/Access/Notifications/ConfirmEmailNotification.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Notifications;\n\nuse BookStack\\App\\MailNotification;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Notifications\\Messages\\MailMessage;\n\nclass ConfirmEmailNotification extends MailNotification\n{\n    public function __construct(\n        public string $token\n    ) {\n    }\n\n    public function toMail(User $notifiable): MailMessage\n    {\n        $appName = ['appName' => setting('app-name')];\n\n        return $this->newMailMessage()\n                ->subject(trans('auth.email_confirm_subject', $appName))\n                ->greeting(trans('auth.email_confirm_greeting', $appName))\n                ->line(trans('auth.email_confirm_text'))\n                ->action(trans('auth.email_confirm_action'), url('/register/confirm/' . $this->token));\n    }\n}\n"
  },
  {
    "path": "app/Access/Notifications/ResetPasswordNotification.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Notifications;\n\nuse BookStack\\App\\MailNotification;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Notifications\\Messages\\MailMessage;\n\nclass ResetPasswordNotification extends MailNotification\n{\n    public function __construct(\n        public string $token\n    ) {\n    }\n\n    public function toMail(User $notifiable): MailMessage\n    {\n        return $this->newMailMessage()\n            ->subject(trans('auth.email_reset_subject', ['appName' => setting('app-name')]))\n            ->line(trans('auth.email_reset_text'))\n            ->action(trans('auth.reset_password'), url('password/reset/' . $this->token))\n            ->line(trans('auth.email_reset_not_requested'));\n    }\n}\n"
  },
  {
    "path": "app/Access/Notifications/UserInviteNotification.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Notifications;\n\nuse BookStack\\App\\MailNotification;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Notifications\\Messages\\MailMessage;\n\nclass UserInviteNotification extends MailNotification\n{\n    public function __construct(\n        public string $token\n    ) {\n    }\n\n    public function toMail(User $notifiable): MailMessage\n    {\n        $appName = ['appName' => setting('app-name')];\n        $locale = $notifiable->getLocale();\n\n        return $this->newMailMessage($locale)\n                ->subject($locale->trans('auth.user_invite_email_subject', $appName))\n                ->greeting($locale->trans('auth.user_invite_email_greeting', $appName))\n                ->line($locale->trans('auth.user_invite_email_text'))\n                ->action($locale->trans('auth.user_invite_email_action'), url('/register/invite/' . $this->token));\n    }\n}\n"
  },
  {
    "path": "app/Access/Oidc/OidcAccessToken.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Oidc;\n\nuse InvalidArgumentException;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\n\nclass OidcAccessToken extends AccessToken\n{\n    /**\n     * Constructs an access token.\n     *\n     * @param array $options An array of options returned by the service provider\n     *                       in the access token request. The `access_token` option is required.\n     *\n     * @throws InvalidArgumentException if `access_token` is not provided in `$options`.\n     */\n    public function __construct(array $options = [])\n    {\n        parent::__construct($options);\n        $this->validate($options);\n    }\n\n    /**\n     * Validate this access token response for OIDC.\n     * As per https://openid.net/specs/openid-connect-basic-1_0.html#TokenOK.\n     */\n    private function validate(array $options): void\n    {\n        // access_token: REQUIRED. Access Token for the UserInfo Endpoint.\n        // Performed on the extended class\n\n        // token_type: REQUIRED. OAuth 2.0 Token Type value. The value MUST be Bearer, as specified in OAuth 2.0\n        // Bearer Token Usage [RFC6750], for Clients using this subset.\n        // Note that the token_type value is case-insensitive.\n        if (strtolower(($options['token_type'] ?? '')) !== 'bearer') {\n            throw new InvalidArgumentException('The response token type MUST be \"Bearer\"');\n        }\n\n        // id_token: REQUIRED. ID Token.\n        if (empty($options['id_token'])) {\n            throw new InvalidArgumentException('An \"id_token\" property must be provided');\n        }\n    }\n\n    /**\n     * Get the id token value from this access token response.\n     */\n    public function getIdToken(): string\n    {\n        return $this->getValues()['id_token'];\n    }\n}\n"
  },
  {
    "path": "app/Access/Oidc/OidcException.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Oidc;\n\nuse Exception;\n\nclass OidcException extends Exception\n{\n}\n"
  },
  {
    "path": "app/Access/Oidc/OidcIdToken.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Oidc;\n\nclass OidcIdToken extends OidcJwtWithClaims implements ProvidesClaims\n{\n    /**\n     * Validate all possible parts of the id token.\n     *\n     * @throws OidcInvalidTokenException\n     */\n    public function validate(string $clientId): bool\n    {\n        parent::validateCommonTokenDetails($clientId);\n        $this->validateTokenClaims($clientId);\n\n        return true;\n    }\n\n    /**\n     * Validate the claims of the token.\n     * As per https://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation.\n     *\n     * @throws OidcInvalidTokenException\n     */\n    protected function validateTokenClaims(string $clientId): void\n    {\n        // 1. The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery)\n        // MUST exactly match the value of the iss (issuer) Claim.\n        // Already done in parent.\n\n        // 2. The Client MUST validate that the aud (audience) Claim contains its client_id value registered\n        // at the Issuer identified by the iss (issuer) Claim as an audience. The ID Token MUST be rejected\n        // if the ID Token does not list the Client as a valid audience, or if it contains additional\n        // audiences not trusted by the Client.\n        // Partially done in parent.\n        $aud = is_string($this->payload['aud']) ? [$this->payload['aud']] : $this->payload['aud'];\n        if (count($aud) !== 1) {\n            throw new OidcInvalidTokenException('Token audience value has ' . count($aud) . ' values, Expected 1');\n        }\n\n        // 3. If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present.\n        // NOTE: Addressed by enforcing a count of 1 above.\n\n        // 4. If an azp (authorized party) Claim is present, the Client SHOULD verify that its client_id\n        // is the Claim Value.\n        if (isset($this->payload['azp']) && $this->payload['azp'] !== $clientId) {\n            throw new OidcInvalidTokenException('Token authorized party exists but does not match the expected client_id');\n        }\n\n        // 5. The current time MUST be before the time represented by the exp Claim\n        // (possibly allowing for some small leeway to account for clock skew).\n        if (empty($this->payload['exp'])) {\n            throw new OidcInvalidTokenException('Missing token expiration time value');\n        }\n\n        $skewSeconds = 120;\n        $now = time();\n        if ($now >= (intval($this->payload['exp']) + $skewSeconds)) {\n            throw new OidcInvalidTokenException('Token has expired');\n        }\n\n        // 6. The iat Claim can be used to reject tokens that were issued too far away from the current time,\n        // limiting the amount of time that nonces need to be stored to prevent attacks.\n        // The acceptable range is Client specific.\n        if (empty($this->payload['iat'])) {\n            throw new OidcInvalidTokenException('Missing token issued at time value');\n        }\n\n        $dayAgo = time() - 86400;\n        $iat = intval($this->payload['iat']);\n        if ($iat > ($now + $skewSeconds) || $iat < $dayAgo) {\n            throw new OidcInvalidTokenException('Token issue at time is not recent or is invalid');\n        }\n\n        // 7. If the acr Claim was requested, the Client SHOULD check that the asserted Claim Value is appropriate.\n        // The meaning and processing of acr Claim Values is out of scope for this document.\n        // NOTE: Not used for our case here. acr is not requested.\n\n        // 8. When a max_age request is made, the Client SHOULD check the auth_time Claim value and request\n        // re-authentication if it determines too much time has elapsed since the last End-User authentication.\n        // NOTE: Not used for our case here. A max_age request is not made.\n\n        // Custom: Ensure the \"sub\" (Subject) Claim exists and has a value.\n        if (empty($this->payload['sub'])) {\n            throw new OidcInvalidTokenException('Missing token subject value');\n        }\n    }\n}\n"
  },
  {
    "path": "app/Access/Oidc/OidcInvalidKeyException.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Oidc;\n\nclass OidcInvalidKeyException extends \\Exception\n{\n}\n"
  },
  {
    "path": "app/Access/Oidc/OidcInvalidTokenException.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Oidc;\n\nuse Exception;\n\nclass OidcInvalidTokenException extends Exception\n{\n}\n"
  },
  {
    "path": "app/Access/Oidc/OidcIssuerDiscoveryException.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Oidc;\n\nuse Exception;\n\nclass OidcIssuerDiscoveryException extends Exception\n{\n}\n"
  },
  {
    "path": "app/Access/Oidc/OidcJwtSigningKey.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Oidc;\n\nuse phpseclib3\\Crypt\\Common\\PublicKey;\nuse phpseclib3\\Crypt\\PublicKeyLoader;\nuse phpseclib3\\Crypt\\RSA;\nuse phpseclib3\\Math\\BigInteger;\n\nclass OidcJwtSigningKey\n{\n    /**\n     * @var PublicKey\n     */\n    protected $key;\n\n    /**\n     * Can be created either from a JWK parameter array or local file path to load a certificate from.\n     * Examples:\n     * 'file:///var/www/cert.pem'\n     * ['kty' => 'RSA', 'alg' => 'RS256', 'n' => 'abc123...'].\n     *\n     * @param array|string $jwkOrKeyPath\n     *\n     * @throws OidcInvalidKeyException\n     */\n    public function __construct($jwkOrKeyPath)\n    {\n        if (is_array($jwkOrKeyPath)) {\n            $this->loadFromJwkArray($jwkOrKeyPath);\n        } elseif (is_string($jwkOrKeyPath) && strpos($jwkOrKeyPath, 'file://') === 0) {\n            $this->loadFromPath($jwkOrKeyPath);\n        } else {\n            throw new OidcInvalidKeyException('Unexpected type of key value provided');\n        }\n    }\n\n    /**\n     * @throws OidcInvalidKeyException\n     */\n    protected function loadFromPath(string $path)\n    {\n        try {\n            $key = PublicKeyLoader::load(\n                file_get_contents($path)\n            );\n        } catch (\\Exception $exception) {\n            throw new OidcInvalidKeyException(\"Failed to load key from file path with error: {$exception->getMessage()}\");\n        }\n\n        if (!$key instanceof RSA) {\n            throw new OidcInvalidKeyException('Key loaded from file path is not an RSA key as expected');\n        }\n\n        $this->key = $key->withPadding(RSA::SIGNATURE_PKCS1);\n    }\n\n    /**\n     * @throws OidcInvalidKeyException\n     */\n    protected function loadFromJwkArray(array $jwk)\n    {\n        // 'alg' is optional for a JWK, but we will still attempt to validate if\n        // it exists otherwise presume it will be compatible.\n        $alg = $jwk['alg'] ?? null;\n        if ($jwk['kty'] !== 'RSA' || !(is_null($alg) || $alg === 'RS256')) {\n            throw new OidcInvalidKeyException(\"Only RS256 keys are currently supported. Found key using {$alg}\");\n        }\n\n        // 'use' is optional for a JWK but we assume 'sig' where no value exists since that's what\n        // the OIDC discovery spec infers since 'sig' MUST be set if encryption keys come into play.\n        $use = $jwk['use'] ?? 'sig';\n        if ($use !== 'sig') {\n            throw new OidcInvalidKeyException(\"Only signature keys are currently supported. Found key for use {$jwk['use']}\");\n        }\n\n        if (empty($jwk['e'])) {\n            throw new OidcInvalidKeyException('An \"e\" parameter on the provided key is expected');\n        }\n\n        if (empty($jwk['n'])) {\n            throw new OidcInvalidKeyException('A \"n\" parameter on the provided key is expected');\n        }\n\n        $n = strtr($jwk['n'] ?? '', '-_', '+/');\n\n        try {\n            $key = PublicKeyLoader::load([\n                'e' => new BigInteger(base64_decode($jwk['e']), 256),\n                'n' => new BigInteger(base64_decode($n), 256),\n            ]);\n        } catch (\\Exception $exception) {\n            throw new OidcInvalidKeyException(\"Failed to load key from JWK parameters with error: {$exception->getMessage()}\");\n        }\n\n        if (!$key instanceof RSA) {\n            throw new OidcInvalidKeyException('Key loaded from file path is not an RSA key as expected');\n        }\n\n        $this->key = $key->withPadding(RSA::SIGNATURE_PKCS1);\n    }\n\n    /**\n     * Use this key to sign the given content and return the signature.\n     */\n    public function verify(string $content, string $signature): bool\n    {\n        return $this->key->verify($content, $signature);\n    }\n\n    /**\n     * Convert the key to a PEM encoded key string.\n     */\n    public function toPem(): string\n    {\n        return $this->key->toString('PKCS8');\n    }\n}\n"
  },
  {
    "path": "app/Access/Oidc/OidcJwtWithClaims.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Oidc;\n\nclass OidcJwtWithClaims implements ProvidesClaims\n{\n    protected array $header;\n    protected array $payload;\n    protected string $signature;\n    protected string $issuer;\n    protected array $tokenParts = [];\n\n    /**\n     * @var array[]|string[]\n     */\n    protected array $keys;\n\n    public function __construct(string $token, string $issuer, array $keys)\n    {\n        $this->keys = $keys;\n        $this->issuer = $issuer;\n        $this->parse($token);\n    }\n\n    /**\n     * Parse the token content into its components.\n     */\n    protected function parse(string $token): void\n    {\n        $this->tokenParts = explode('.', $token);\n        $this->header = $this->parseEncodedTokenPart($this->tokenParts[0]);\n        $this->payload = $this->parseEncodedTokenPart($this->tokenParts[1] ?? '');\n        $this->signature = $this->base64UrlDecode($this->tokenParts[2] ?? '') ?: '';\n    }\n\n    /**\n     * Parse a Base64-JSON encoded token part.\n     * Returns the data as a key-value array or empty array upon error.\n     */\n    protected function parseEncodedTokenPart(string $part): array\n    {\n        $json = $this->base64UrlDecode($part) ?: '{}';\n        $decoded = json_decode($json, true);\n\n        return is_array($decoded) ? $decoded : [];\n    }\n\n    /**\n     * Base64URL decode. Needs some character conversions to be compatible\n     * with PHP's default base64 handling.\n     */\n    protected function base64UrlDecode(string $encoded): string\n    {\n        return base64_decode(strtr($encoded, '-_', '+/'));\n    }\n\n    /**\n     * Validate common parts of OIDC JWT tokens.\n     *\n     * @throws OidcInvalidTokenException\n     */\n    public function validateCommonTokenDetails(string $clientId): bool\n    {\n        $this->validateTokenStructure();\n        $this->validateTokenSignature();\n        $this->validateCommonClaims($clientId);\n\n        return true;\n    }\n\n    /**\n     * Fetch a specific claim from this token.\n     * Returns null if it is null or does not exist.\n     */\n    public function getClaim(string $claim): mixed\n    {\n        return $this->payload[$claim] ?? null;\n    }\n\n    /**\n     * Get all returned claims within the token.\n     */\n    public function getAllClaims(): array\n    {\n        return $this->payload;\n    }\n\n    /**\n     * Replace the existing claim data of this token with that provided.\n     */\n    public function replaceClaims(array $claims): void\n    {\n        $this->payload = $claims;\n    }\n\n    /**\n     * Validate the structure of the given token and ensure we have the required pieces.\n     * As per https://datatracker.ietf.org/doc/html/rfc7519#section-7.2.\n     *\n     * @throws OidcInvalidTokenException\n     */\n    protected function validateTokenStructure(): void\n    {\n        foreach (['header', 'payload'] as $prop) {\n            if (empty($this->$prop) || !is_array($this->$prop)) {\n                throw new OidcInvalidTokenException(\"Could not parse out a valid {$prop} within the provided token\");\n            }\n        }\n\n        if (empty($this->signature) || !is_string($this->signature)) {\n            throw new OidcInvalidTokenException('Could not parse out a valid signature within the provided token');\n        }\n    }\n\n    /**\n     * Validate the signature of the given token and ensure it validates against the provided key.\n     *\n     * @throws OidcInvalidTokenException\n     */\n    protected function validateTokenSignature(): void\n    {\n        if ($this->header['alg'] !== 'RS256') {\n            throw new OidcInvalidTokenException(\"Only RS256 signature validation is supported. Token reports using {$this->header['alg']}\");\n        }\n\n        $parsedKeys = array_map(function ($key) {\n            try {\n                return new OidcJwtSigningKey($key);\n            } catch (OidcInvalidKeyException $e) {\n                throw new OidcInvalidTokenException('Failed to read signing key with error: ' . $e->getMessage());\n            }\n        }, $this->keys);\n\n        $parsedKeys = array_filter($parsedKeys);\n\n        $contentToSign = $this->tokenParts[0] . '.' . $this->tokenParts[1];\n        /** @var OidcJwtSigningKey $parsedKey */\n        foreach ($parsedKeys as $parsedKey) {\n            if ($parsedKey->verify($contentToSign, $this->signature)) {\n                return;\n            }\n        }\n\n        throw new OidcInvalidTokenException('Token signature could not be validated using the provided keys');\n    }\n\n    /**\n     * Validate common claims for OIDC JWT tokens.\n     * As per https://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation\n     * and https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse\n     *\n     * @throws OidcInvalidTokenException\n     */\n    protected function validateCommonClaims(string $clientId): void\n    {\n        // 1. The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery)\n        // MUST exactly match the value of the iss (issuer) Claim.\n        if (empty($this->payload['iss']) || $this->issuer !== $this->payload['iss']) {\n            throw new OidcInvalidTokenException('Missing or non-matching token issuer value');\n        }\n\n        // 2. The Client MUST validate that the aud (audience) Claim contains its client_id value registered\n        // at the Issuer identified by the iss (issuer) Claim as an audience. The ID Token MUST be rejected\n        // if the ID Token does not list the Client as a valid audience.\n        if (empty($this->payload['aud'])) {\n            throw new OidcInvalidTokenException('Missing token audience value');\n        }\n\n        $aud = is_string($this->payload['aud']) ? [$this->payload['aud']] : $this->payload['aud'];\n        if (!in_array($clientId, $aud, true)) {\n            throw new OidcInvalidTokenException('Token audience value did not match the expected client_id');\n        }\n    }\n}\n"
  },
  {
    "path": "app/Access/Oidc/OidcOAuthProvider.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Oidc;\n\nuse League\\OAuth2\\Client\\Grant\\AbstractGrant;\nuse League\\OAuth2\\Client\\Provider\\AbstractProvider;\nuse League\\OAuth2\\Client\\Provider\\Exception\\IdentityProviderException;\nuse League\\OAuth2\\Client\\Provider\\GenericResourceOwner;\nuse League\\OAuth2\\Client\\Provider\\ResourceOwnerInterface;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse League\\OAuth2\\Client\\Tool\\BearerAuthorizationTrait;\nuse Psr\\Http\\Message\\ResponseInterface;\n\n/**\n * Extended OAuth2Provider for using with OIDC.\n * Credit to the https://github.com/steverhoades/oauth2-openid-connect-client\n * project for the idea of extending a League\\OAuth2 client for this use-case.\n */\nclass OidcOAuthProvider extends AbstractProvider\n{\n    use BearerAuthorizationTrait;\n\n    protected string $authorizationEndpoint;\n    protected string $tokenEndpoint;\n\n    /**\n     * Scopes to use for the OIDC authorization call.\n     */\n    protected array $scopes = ['openid', 'profile', 'email'];\n\n    /**\n     * Returns the base URL for authorizing a client.\n     */\n    public function getBaseAuthorizationUrl(): string\n    {\n        return $this->authorizationEndpoint;\n    }\n\n    /**\n     * Returns the base URL for requesting an access token.\n     */\n    public function getBaseAccessTokenUrl(array $params): string\n    {\n        return $this->tokenEndpoint;\n    }\n\n    /**\n     * Returns the URL for requesting the resource owner's details.\n     */\n    public function getResourceOwnerDetailsUrl(AccessToken $token): string\n    {\n        return '';\n    }\n\n    /**\n     * Add another scope to this provider upon the default.\n     */\n    public function addScope(string $scope): void\n    {\n        $this->scopes[] = $scope;\n        $this->scopes = array_unique($this->scopes);\n    }\n\n    /**\n     * Returns the default scopes used by this provider.\n     *\n     * This should only be the scopes that are required to request the details\n     * of the resource owner, rather than all the available scopes.\n     */\n    protected function getDefaultScopes(): array\n    {\n        return $this->scopes;\n    }\n\n    /**\n     * Returns the string that should be used to separate scopes when building\n     * the URL for requesting an access token.\n     */\n    protected function getScopeSeparator(): string\n    {\n        return ' ';\n    }\n\n    /**\n     * Checks a provider response for errors.\n     * @throws IdentityProviderException\n     */\n    protected function checkResponse(ResponseInterface $response, $data): void\n    {\n        if ($response->getStatusCode() >= 400 || isset($data['error'])) {\n            throw new IdentityProviderException(\n                $data['error'] ?? $response->getReasonPhrase(),\n                $response->getStatusCode(),\n                (string) $response->getBody()\n            );\n        }\n    }\n\n    /**\n     * Generates a resource owner object from a successful resource owner\n     * details request.\n     */\n    protected function createResourceOwner(array $response, AccessToken $token): ResourceOwnerInterface\n    {\n        return new GenericResourceOwner($response, '');\n    }\n\n    /**\n     * Creates an access token from a response.\n     *\n     * The grant that was used to fetch the response can be used to provide\n     * additional context.\n     */\n    protected function createAccessToken(array $response, AbstractGrant $grant): OidcAccessToken\n    {\n        return new OidcAccessToken($response);\n    }\n\n    /**\n     * Get the method used for PKCE code verifier hashing, which is passed\n     * in the \"code_challenge_method\" parameter in the authorization request.\n     */\n    protected function getPkceMethod(): string\n    {\n        return static::PKCE_METHOD_S256;\n    }\n}\n"
  },
  {
    "path": "app/Access/Oidc/OidcProviderSettings.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Oidc;\n\nuse GuzzleHttp\\Psr7\\Request;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse InvalidArgumentException;\nuse Psr\\Http\\Client\\ClientExceptionInterface;\nuse Psr\\Http\\Client\\ClientInterface;\n\n/**\n * OpenIdConnectProviderSettings\n * Acts as a DTO for settings used within the oidc request and token handling.\n * Performs auto-discovery upon request.\n */\nclass OidcProviderSettings\n{\n    public string $issuer;\n    public string $clientId;\n    public string $clientSecret;\n    public ?string $authorizationEndpoint;\n    public ?string $tokenEndpoint;\n    public ?string $endSessionEndpoint;\n    public ?string $userinfoEndpoint;\n\n    /**\n     * @var string[]|array[]\n     */\n    public ?array $keys = [];\n\n    public function __construct(array $settings)\n    {\n        $this->applySettingsFromArray($settings);\n        $this->validateInitial();\n    }\n\n    /**\n     * Apply an array of settings to populate setting properties within this class.\n     */\n    protected function applySettingsFromArray(array $settingsArray): void\n    {\n        foreach ($settingsArray as $key => $value) {\n            if (property_exists($this, $key)) {\n                $this->$key = $value;\n            }\n        }\n    }\n\n    /**\n     * Validate any core, required properties have been set.\n     *\n     * @throws InvalidArgumentException\n     */\n    protected function validateInitial(): void\n    {\n        $required = ['clientId', 'clientSecret', 'issuer'];\n        foreach ($required as $prop) {\n            if (empty($this->$prop)) {\n                throw new InvalidArgumentException(\"Missing required configuration \\\"{$prop}\\\" value\");\n            }\n        }\n\n        if (!str_starts_with($this->issuer, 'https://')) {\n            throw new InvalidArgumentException('Issuer value must start with https://');\n        }\n    }\n\n    /**\n     * Perform a full validation on these settings.\n     *\n     * @throws InvalidArgumentException\n     */\n    public function validate(): void\n    {\n        $this->validateInitial();\n\n        $required = ['keys', 'tokenEndpoint', 'authorizationEndpoint'];\n        foreach ($required as $prop) {\n            if (empty($this->$prop)) {\n                throw new InvalidArgumentException(\"Missing required configuration \\\"{$prop}\\\" value\");\n            }\n        }\n\n        $endpointProperties = ['tokenEndpoint', 'authorizationEndpoint', 'userinfoEndpoint'];\n        foreach ($endpointProperties as $prop) {\n            if (is_string($this->$prop) && !str_starts_with($this->$prop, 'https://')) {\n                throw new InvalidArgumentException(\"Endpoint value for \\\"{$prop}\\\" must start with https://\");\n            }\n        }\n    }\n\n    /**\n     * Discover and autoload settings from the configured issuer.\n     *\n     * @throws OidcIssuerDiscoveryException\n     */\n    public function discoverFromIssuer(ClientInterface $httpClient, Repository $cache, int $cacheMinutes): void\n    {\n        try {\n            $cacheKey = 'oidc-discovery::' . $this->issuer;\n            $discoveredSettings = $cache->remember($cacheKey, $cacheMinutes * 60, function () use ($httpClient) {\n                return $this->loadSettingsFromIssuerDiscovery($httpClient);\n            });\n            $this->applySettingsFromArray($discoveredSettings);\n        } catch (ClientExceptionInterface $exception) {\n            throw new OidcIssuerDiscoveryException(\"HTTP request failed during discovery with error: {$exception->getMessage()}\");\n        }\n    }\n\n    /**\n     * @throws OidcIssuerDiscoveryException\n     * @throws ClientExceptionInterface\n     */\n    protected function loadSettingsFromIssuerDiscovery(ClientInterface $httpClient): array\n    {\n        $issuerUrl = rtrim($this->issuer, '/') . '/.well-known/openid-configuration';\n        $request = new Request('GET', $issuerUrl);\n        $response = $httpClient->sendRequest($request);\n        $result = json_decode($response->getBody()->getContents(), true);\n\n        if (empty($result) || !is_array($result)) {\n            throw new OidcIssuerDiscoveryException(\"Error discovering provider settings from issuer at URL {$issuerUrl}\");\n        }\n\n        if ($result['issuer'] !== $this->issuer) {\n            throw new OidcIssuerDiscoveryException('Unexpected issuer value found on discovery response');\n        }\n\n        $discoveredSettings = [];\n\n        if (!empty($result['authorization_endpoint'])) {\n            $discoveredSettings['authorizationEndpoint'] = $result['authorization_endpoint'];\n        }\n\n        if (!empty($result['token_endpoint'])) {\n            $discoveredSettings['tokenEndpoint'] = $result['token_endpoint'];\n        }\n\n        if (!empty($result['userinfo_endpoint'])) {\n            $discoveredSettings['userinfoEndpoint'] = $result['userinfo_endpoint'];\n        }\n\n        if (!empty($result['jwks_uri'])) {\n            $keys = $this->loadKeysFromUri($result['jwks_uri'], $httpClient);\n            $discoveredSettings['keys'] = $this->filterKeys($keys);\n        }\n\n        if (!empty($result['end_session_endpoint'])) {\n            $discoveredSettings['endSessionEndpoint'] = $result['end_session_endpoint'];\n        }\n\n        return $discoveredSettings;\n    }\n\n    /**\n     * Filter the given JWK keys down to just those we support.\n     */\n    protected function filterKeys(array $keys): array\n    {\n        return array_filter($keys, function (array $key) {\n            $alg = $key['alg'] ?? 'RS256';\n            $use = $key['use'] ?? 'sig';\n\n            return $key['kty'] === 'RSA' && $use === 'sig' && $alg === 'RS256';\n        });\n    }\n\n    /**\n     * Return an array of jwks as PHP key=>value arrays.\n     *\n     * @throws ClientExceptionInterface\n     * @throws OidcIssuerDiscoveryException\n     */\n    protected function loadKeysFromUri(string $uri, ClientInterface $httpClient): array\n    {\n        $request = new Request('GET', $uri);\n        $response = $httpClient->sendRequest($request);\n        $result = json_decode($response->getBody()->getContents(), true);\n\n        if (empty($result) || !is_array($result) || !isset($result['keys'])) {\n            throw new OidcIssuerDiscoveryException('Error reading keys from issuer jwks_uri');\n        }\n\n        return $result['keys'];\n    }\n\n    /**\n     * Get the settings needed by an OAuth provider, as a key=>value array.\n     */\n    public function arrayForOAuthProvider(): array\n    {\n        $settingKeys = ['clientId', 'clientSecret', 'authorizationEndpoint', 'tokenEndpoint', 'userinfoEndpoint'];\n        $settings = [];\n        foreach ($settingKeys as $setting) {\n            $settings[$setting] = $this->$setting;\n        }\n\n        return $settings;\n    }\n}\n"
  },
  {
    "path": "app/Access/Oidc/OidcService.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Oidc;\n\nuse BookStack\\Access\\GroupSyncService;\nuse BookStack\\Access\\LoginService;\nuse BookStack\\Access\\RegistrationService;\nuse BookStack\\Exceptions\\JsonDebugException;\nuse BookStack\\Exceptions\\StoppedAuthenticationException;\nuse BookStack\\Exceptions\\UserRegistrationException;\nuse BookStack\\Facades\\Theme;\nuse BookStack\\Http\\HttpRequestService;\nuse BookStack\\Theming\\ThemeEvents;\nuse BookStack\\Uploads\\UserAvatars;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Support\\Facades\\Cache;\nuse League\\OAuth2\\Client\\OptionProvider\\HttpBasicAuthOptionProvider;\nuse League\\OAuth2\\Client\\Provider\\Exception\\IdentityProviderException;\n\n/**\n * Class OpenIdConnectService\n * Handles any app-specific OIDC tasks.\n */\nclass OidcService\n{\n    public function __construct(\n        protected RegistrationService $registrationService,\n        protected LoginService $loginService,\n        protected HttpRequestService $http,\n        protected GroupSyncService $groupService,\n        protected UserAvatars $userAvatars\n    ) {\n    }\n\n    /**\n     * Initiate an authorization flow.\n     * Provides back an authorize redirect URL, in addition to other\n     * details which may be required for the auth flow.\n     *\n     * @throws OidcException\n     *\n     * @return array{url: string, state: string}\n     */\n    public function login(): array\n    {\n        $settings = $this->getProviderSettings();\n        $provider = $this->getProvider($settings);\n\n        $url = $provider->getAuthorizationUrl();\n        session()->put('oidc_pkce_code', $provider->getPkceCode() ?? '');\n\n        $returnUrl = Theme::dispatch(ThemeEvents::OIDC_AUTH_PRE_REDIRECT, $url);\n        if (is_string($returnUrl)) {\n            $url = $returnUrl;\n        }\n\n        return [\n            'url'   => $url,\n            'state' => $provider->getState(),\n        ];\n    }\n\n    /**\n     * Process the Authorization response from the authorization server and\n     * return the matching, or new if registration active, user matched to the\n     * authorization server. Throws if the user cannot be auth if not authenticated.\n     *\n     * @throws JsonDebugException\n     * @throws OidcException\n     * @throws StoppedAuthenticationException\n     * @throws IdentityProviderException\n     */\n    public function processAuthorizeResponse(?string $authorizationCode): User\n    {\n        $settings = $this->getProviderSettings();\n        $provider = $this->getProvider($settings);\n\n        // Set PKCE code flashed at login\n        $pkceCode = session()->pull('oidc_pkce_code', '');\n        $provider->setPkceCode($pkceCode);\n\n        // Try to exchange authorization code for access token\n        $accessToken = $provider->getAccessToken('authorization_code', [\n            'code' => $authorizationCode,\n        ]);\n\n        return $this->processAccessTokenCallback($accessToken, $settings);\n    }\n\n    /**\n     * @throws OidcException\n     */\n    protected function getProviderSettings(): OidcProviderSettings\n    {\n        $config = $this->config();\n        $settings = new OidcProviderSettings([\n            'issuer'                => $config['issuer'],\n            'clientId'              => $config['client_id'],\n            'clientSecret'          => $config['client_secret'],\n            'authorizationEndpoint' => $config['authorization_endpoint'],\n            'tokenEndpoint'         => $config['token_endpoint'],\n            'endSessionEndpoint'    => is_string($config['end_session_endpoint']) ? $config['end_session_endpoint'] : null,\n            'userinfoEndpoint'      => $config['userinfo_endpoint'],\n        ]);\n\n        // Use keys if configured\n        if (!empty($config['jwt_public_key'])) {\n            $settings->keys = [$config['jwt_public_key']];\n        }\n\n        // Run discovery\n        if ($config['discover'] ?? false) {\n            try {\n                $settings->discoverFromIssuer($this->http->buildClient(5), Cache::store(null), 15);\n            } catch (OidcIssuerDiscoveryException $exception) {\n                throw new OidcException('OIDC Discovery Error: ' . $exception->getMessage());\n            }\n        }\n\n        // Prevent use of RP-initiated logout if specifically disabled\n        // Or force use of a URL if specifically set.\n        if ($config['end_session_endpoint'] === false) {\n            $settings->endSessionEndpoint = null;\n        } else if (is_string($config['end_session_endpoint'])) {\n            $settings->endSessionEndpoint = $config['end_session_endpoint'];\n        }\n\n        $settings->validate();\n\n        return $settings;\n    }\n\n    /**\n     * Load the underlying OpenID Connect Provider.\n     */\n    protected function getProvider(OidcProviderSettings $settings): OidcOAuthProvider\n    {\n        $provider = new OidcOAuthProvider([\n            ...$settings->arrayForOAuthProvider(),\n            'redirectUri' => url('/oidc/callback'),\n        ], [\n            'httpClient'     => $this->http->buildClient(5),\n            'optionProvider' => new HttpBasicAuthOptionProvider(),\n        ]);\n\n        foreach ($this->getAdditionalScopes() as $scope) {\n            $provider->addScope($scope);\n        }\n\n        return $provider;\n    }\n\n    /**\n     * Get any user-defined addition/custom scopes to apply to the authentication request.\n     *\n     * @return string[]\n     */\n    protected function getAdditionalScopes(): array\n    {\n        $scopeConfig = $this->config()['additional_scopes'] ?: '';\n\n        $scopeArr = explode(',', $scopeConfig);\n        $scopeArr = array_map(fn (string $scope) => trim($scope), $scopeArr);\n\n        return array_filter($scopeArr);\n    }\n\n    /**\n     * Processes a received access token for a user. Login the user when\n     * they exist, optionally registering them automatically.\n     *\n     * @throws OidcException\n     * @throws JsonDebugException\n     * @throws StoppedAuthenticationException\n     */\n    protected function processAccessTokenCallback(OidcAccessToken $accessToken, OidcProviderSettings $settings): User\n    {\n        $idTokenText = $accessToken->getIdToken();\n        $idToken = new OidcIdToken(\n            $idTokenText,\n            $settings->issuer,\n            $settings->keys,\n        );\n\n        session()->put(\"oidc_id_token\", $idTokenText);\n\n        $returnClaims = Theme::dispatch(ThemeEvents::OIDC_ID_TOKEN_PRE_VALIDATE, $idToken->getAllClaims(), [\n            'access_token' => $accessToken->getToken(),\n            'expires_in' => $accessToken->getExpires(),\n            'refresh_token' => $accessToken->getRefreshToken(),\n        ]);\n\n        if (!is_null($returnClaims)) {\n            $idToken->replaceClaims($returnClaims);\n        }\n\n        if ($this->config()['dump_user_details']) {\n            throw new JsonDebugException($idToken->getAllClaims());\n        }\n\n        try {\n            $idToken->validate($settings->clientId);\n        } catch (OidcInvalidTokenException $exception) {\n            throw new OidcException(\"ID token validation failed with error: {$exception->getMessage()}\");\n        }\n\n        $userDetails = $this->getUserDetailsFromToken($idToken, $accessToken, $settings);\n        if (empty($userDetails->email)) {\n            throw new OidcException(trans('errors.oidc_no_email_address'));\n        }\n        if (empty($userDetails->name)) {\n            $userDetails->name = $userDetails->externalId;\n        }\n\n        $isLoggedIn = auth()->check();\n        if ($isLoggedIn) {\n            throw new OidcException(trans('errors.oidc_already_logged_in'));\n        }\n\n        try {\n            $user = $this->registrationService->findOrRegister(\n                $userDetails->name,\n                $userDetails->email,\n                $userDetails->externalId\n            );\n        } catch (UserRegistrationException $exception) {\n            throw new OidcException($exception->getMessage());\n        }\n\n        if ($this->config()['fetch_avatar'] && !$user->avatar()->exists() && $userDetails->picture) {\n            $this->userAvatars->assignToUserFromUrl($user, $userDetails->picture);\n        }\n\n        if ($this->shouldSyncGroups()) {\n            $detachExisting = $this->config()['remove_from_groups'];\n            $this->groupService->syncUserWithFoundGroups($user, $userDetails->groups ?? [], $detachExisting);\n        }\n\n        $this->loginService->login($user, 'oidc');\n\n        return $user;\n    }\n\n    /**\n     * @throws OidcException\n     */\n    protected function getUserDetailsFromToken(OidcIdToken $idToken, OidcAccessToken $accessToken, OidcProviderSettings $settings): OidcUserDetails\n    {\n        $userDetails = new OidcUserDetails();\n        $userDetails->populate(\n            $idToken,\n            $this->config()['external_id_claim'],\n            $this->config()['display_name_claims'] ?? '',\n            $this->config()['groups_claim'] ?? ''\n        );\n\n        if (!$userDetails->isFullyPopulated($this->shouldSyncGroups()) && !empty($settings->userinfoEndpoint)) {\n            $provider = $this->getProvider($settings);\n            $request = $provider->getAuthenticatedRequest('GET', $settings->userinfoEndpoint, $accessToken->getToken());\n            $response = new OidcUserinfoResponse(\n                $provider->getResponse($request),\n                $settings->issuer,\n                $settings->keys,\n            );\n\n            try {\n                $response->validate($idToken->getClaim('sub'), $settings->clientId);\n            } catch (OidcInvalidTokenException $exception) {\n                throw new OidcException(\"Userinfo endpoint response validation failed with error: {$exception->getMessage()}\");\n            }\n\n            $userDetails->populate(\n                $response,\n                $this->config()['external_id_claim'],\n                $this->config()['display_name_claims'] ?? '',\n                $this->config()['groups_claim'] ?? ''\n            );\n        }\n\n        return $userDetails;\n    }\n\n    /**\n     * Get the OIDC config from the application.\n     */\n    protected function config(): array\n    {\n        return config('oidc');\n    }\n\n    /**\n     * Check if groups should be synced.\n     */\n    protected function shouldSyncGroups(): bool\n    {\n        return $this->config()['user_to_groups'] !== false;\n    }\n\n    /**\n     * Start the RP-initiated logout flow if active, otherwise start a standard logout flow.\n     * Returns a post-app-logout redirect URL.\n     * Reference: https://openid.net/specs/openid-connect-rpinitiated-1_0.html\n     * @throws OidcException\n     */\n    public function logout(): string\n    {\n        $oidcToken = session()->pull(\"oidc_id_token\");\n        $defaultLogoutUrl = url($this->loginService->logout());\n        $oidcSettings = $this->getProviderSettings();\n\n        if (!$oidcSettings->endSessionEndpoint) {\n            return $defaultLogoutUrl;\n        }\n\n        $endpointParams = [\n            'id_token_hint' => $oidcToken,\n            'post_logout_redirect_uri' => $defaultLogoutUrl,\n        ];\n\n        $joiner = str_contains($oidcSettings->endSessionEndpoint, '?') ? '&' : '?';\n\n        return $oidcSettings->endSessionEndpoint . $joiner . http_build_query($endpointParams);\n    }\n}\n"
  },
  {
    "path": "app/Access/Oidc/OidcUserDetails.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Oidc;\n\nuse Illuminate\\Support\\Arr;\n\nclass OidcUserDetails\n{\n    public function __construct(\n        public ?string $externalId = null,\n        public ?string $email = null,\n        public ?string $name = null,\n        public ?array $groups = null,\n        public ?string $picture = null,\n    ) {\n    }\n\n    /**\n     * Check if the user details are fully populated for our usage.\n     */\n    public function isFullyPopulated(bool $groupSyncActive): bool\n    {\n        $hasEmpty = empty($this->externalId)\n            || empty($this->email)\n            || empty($this->name)\n            || ($groupSyncActive && $this->groups === null);\n\n        return !$hasEmpty;\n    }\n\n    /**\n     * Populate user details from the given claim data.\n     */\n    public function populate(\n        ProvidesClaims $claims,\n        string $idClaim,\n        string $displayNameClaims,\n        string $groupsClaim,\n    ): void {\n        $this->externalId = $claims->getClaim($idClaim) ?? $this->externalId;\n        $this->email = $claims->getClaim('email') ?? $this->email;\n        $this->name = static::getUserDisplayName($displayNameClaims, $claims) ?? $this->name;\n        $this->groups = static::getUserGroups($groupsClaim, $claims) ?? $this->groups;\n        $this->picture = static::getPicture($claims) ?: $this->picture;\n    }\n\n    protected static function getUserDisplayName(string $displayNameClaims, ProvidesClaims $claims): string\n    {\n        $displayNameClaimParts = explode('|', $displayNameClaims);\n\n        $displayName = [];\n        foreach ($displayNameClaimParts as $claim) {\n            $component = $claims->getClaim(trim($claim)) ?? '';\n            if ($component !== '') {\n                $displayName[] = $component;\n            }\n        }\n\n        return implode(' ', $displayName);\n    }\n\n    protected static function getUserGroups(string $groupsClaim, ProvidesClaims $claims): ?array\n    {\n        if (empty($groupsClaim)) {\n            return null;\n        }\n\n        $groupsList = Arr::get($claims->getAllClaims(), $groupsClaim);\n        if (!is_array($groupsList)) {\n            return null;\n        }\n\n        return array_values(array_filter($groupsList, function ($val) {\n            return is_string($val);\n        }));\n    }\n\n    protected static function getPicture(ProvidesClaims $claims): ?string\n    {\n        $picture = $claims->getClaim('picture');\n        if (is_string($picture) && str_starts_with($picture, 'http')) {\n            return $picture;\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "app/Access/Oidc/OidcUserinfoResponse.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Oidc;\n\nuse Psr\\Http\\Message\\ResponseInterface;\n\nclass OidcUserinfoResponse implements ProvidesClaims\n{\n    protected array $claims = [];\n    protected ?OidcJwtWithClaims $jwt = null;\n\n    public function __construct(ResponseInterface $response, string $issuer, array $keys)\n    {\n        $contentTypeHeaderValue = $response->getHeader('Content-Type')[0] ?? '';\n        $contentType = strtolower(trim(explode(';', $contentTypeHeaderValue, 2)[0]));\n\n        if ($contentType === 'application/json') {\n            $this->claims = json_decode($response->getBody()->getContents(), true);\n        }\n\n        if ($contentType === 'application/jwt') {\n            $this->jwt = new OidcJwtWithClaims($response->getBody()->getContents(), $issuer, $keys);\n            $this->claims = $this->jwt->getAllClaims();\n        }\n    }\n\n    /**\n     * @throws OidcInvalidTokenException\n     */\n    public function validate(string $idTokenSub, string $clientId): bool\n    {\n        if (!is_null($this->jwt)) {\n            $this->jwt->validateCommonTokenDetails($clientId);\n        }\n\n        $sub = $this->getClaim('sub');\n\n        // Spec: v1.0 5.3.2: The sub (subject) Claim MUST always be returned in the UserInfo Response.\n        if (!is_string($sub) || empty($sub)) {\n            throw new OidcInvalidTokenException(\"No valid subject value found in userinfo data\");\n        }\n\n        // Spec: v1.0 5.3.2: The sub Claim in the UserInfo Response MUST be verified to exactly match the sub Claim in the ID Token;\n        // if they do not match, the UserInfo Response values MUST NOT be used.\n        if ($idTokenSub !== $sub) {\n            throw new OidcInvalidTokenException(\"Subject value provided in the userinfo endpoint does not match the provided ID token value\");\n        }\n\n        // Spec v1.0 5.3.4 Defines the following:\n        // Verify that the OP that responded was the intended OP through a TLS server certificate check, per RFC 6125 [RFC6125].\n          // This is effectively done as part of the HTTP request we're making through CURLOPT_SSL_VERIFYHOST on the request.\n        // If the Client has provided a userinfo_encrypted_response_alg parameter during Registration, decrypt the UserInfo Response using the keys specified during Registration.\n          // We don't currently support JWT encryption for OIDC\n        // If the response was signed, the Client SHOULD validate the signature according to JWS [JWS].\n          // This is done as part of the validateCommonClaims above.\n\n        return true;\n    }\n\n    public function getClaim(string $claim): mixed\n    {\n        return $this->claims[$claim] ?? null;\n    }\n\n    public function getAllClaims(): array\n    {\n        return $this->claims;\n    }\n}\n"
  },
  {
    "path": "app/Access/Oidc/ProvidesClaims.php",
    "content": "<?php\n\nnamespace BookStack\\Access\\Oidc;\n\ninterface ProvidesClaims\n{\n    /**\n     * Fetch a specific claim.\n     * Returns null if it is null or does not exist.\n     */\n    public function getClaim(string $claim): mixed;\n\n    /**\n     * Get all contained claims.\n     */\n    public function getAllClaims(): array;\n}\n"
  },
  {
    "path": "app/Access/RegistrationService.php",
    "content": "<?php\n\nnamespace BookStack\\Access;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Exceptions\\UserRegistrationException;\nuse BookStack\\Facades\\Activity;\nuse BookStack\\Facades\\Theme;\nuse BookStack\\Theming\\ThemeEvents;\nuse BookStack\\Users\\Models\\User;\nuse BookStack\\Users\\UserRepo;\nuse Exception;\nuse Illuminate\\Support\\Str;\n\nclass RegistrationService\n{\n    public function __construct(\n        protected UserRepo $userRepo,\n        protected EmailConfirmationService $emailConfirmationService,\n    ) {\n    }\n\n    /**\n     * Check if registrations are allowed in the app settings.\n     *\n     * @throws UserRegistrationException\n     */\n    public function ensureRegistrationAllowed()\n    {\n        if (!$this->registrationAllowed()) {\n            throw new UserRegistrationException(trans('auth.registrations_disabled'), '/login');\n        }\n    }\n\n    /**\n     * Check if standard BookStack User registrations are currently allowed.\n     * Does not prevent external-auth based registration.\n     */\n    protected function registrationAllowed(): bool\n    {\n        $authMethod = config('auth.method');\n        $authMethodsWithRegistration = ['standard'];\n\n        return in_array($authMethod, $authMethodsWithRegistration) && setting('registration-enabled');\n    }\n\n    /**\n     * Attempt to find a user in the system otherwise register them as a new\n     * user. For use with external auth systems since password is auto-generated.\n     *\n     * @throws UserRegistrationException\n     */\n    public function findOrRegister(string $name, string $email, string $externalId): User\n    {\n        $user = User::query()\n            ->where('external_auth_id', '=', $externalId)\n            ->first();\n\n        if (is_null($user)) {\n            $userData = [\n                'name'             => $name,\n                'email'            => $email,\n                'password'         => Str::random(32),\n                'external_auth_id' => $externalId,\n            ];\n\n            $user = $this->registerUser($userData, null, false);\n        }\n\n        return $user;\n    }\n\n    /**\n     * The registrations flow for all users.\n     *\n     * @throws UserRegistrationException\n     */\n    public function registerUser(array $userData, ?SocialAccount $socialAccount = null, bool $emailConfirmed = false): User\n    {\n        $userEmail = $userData['email'];\n        $authSystem = $socialAccount ? $socialAccount->driver : auth()->getDefaultDriver();\n\n        // Email restriction\n        $this->ensureEmailDomainAllowed($userEmail);\n\n        // Ensure user does not already exist\n        $alreadyUser = !is_null($this->userRepo->getByEmail($userEmail));\n        if ($alreadyUser) {\n            throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $userEmail]), '/login');\n        }\n\n        /** @var ?bool $shouldRegister */\n        $shouldRegister = Theme::dispatch(ThemeEvents::AUTH_PRE_REGISTER, $authSystem, $userData);\n        if ($shouldRegister === false) {\n            throw new UserRegistrationException(trans('errors.auth_pre_register_theme_prevention'), '/login');\n        }\n\n        // Create the user\n        $newUser = $this->userRepo->createWithoutActivity($userData, $emailConfirmed);\n        $newUser->attachDefaultRole();\n\n        // Assign social account if given\n        if ($socialAccount) {\n            $newUser->socialAccounts()->save($socialAccount);\n        }\n\n        Activity::add(ActivityType::AUTH_REGISTER, $socialAccount ?? $newUser);\n        Theme::dispatch(ThemeEvents::AUTH_REGISTER, $authSystem, $newUser);\n\n        // Start email confirmation flow if required\n        if ($this->emailConfirmationService->confirmationRequired() && !$emailConfirmed) {\n            $newUser->save();\n\n            try {\n                $this->emailConfirmationService->sendConfirmation($newUser);\n                session()->flash('sent-email-confirmation', true);\n            } catch (Exception $e) {\n                $message = trans('auth.email_confirm_send_error');\n\n                throw new UserRegistrationException($message, '/register/confirm');\n            }\n        }\n\n        return $newUser;\n    }\n\n    /**\n     * Ensure that the given email meets any active email domain registration restrictions.\n     * Throws if restrictions are active and the email does not match an allowed domain.\n     *\n     * @throws UserRegistrationException\n     */\n    protected function ensureEmailDomainAllowed(string $userEmail): void\n    {\n        $registrationRestrict = setting('registration-restrict');\n\n        if (!$registrationRestrict) {\n            return;\n        }\n\n        $restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict));\n        $userEmailDomain = mb_substr(mb_strrchr($userEmail, '@'), 1);\n        if (!in_array($userEmailDomain, $restrictedEmailDomains)) {\n            $redirect = $this->registrationAllowed() ? '/register' : '/login';\n\n            throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), $redirect);\n        }\n    }\n}\n"
  },
  {
    "path": "app/Access/Saml2Service.php",
    "content": "<?php\n\nnamespace BookStack\\Access;\n\nuse BookStack\\Exceptions\\JsonDebugException;\nuse BookStack\\Exceptions\\SamlException;\nuse BookStack\\Exceptions\\StoppedAuthenticationException;\nuse BookStack\\Exceptions\\UserRegistrationException;\nuse BookStack\\Users\\Models\\User;\nuse Exception;\nuse OneLogin\\Saml2\\Auth;\nuse OneLogin\\Saml2\\Constants;\nuse OneLogin\\Saml2\\Error;\nuse OneLogin\\Saml2\\IdPMetadataParser;\nuse OneLogin\\Saml2\\ValidationError;\n\n/**\n * Class Saml2Service\n * Handles any app-specific SAML tasks.\n */\nclass Saml2Service\n{\n    protected array $config;\n\n    public function __construct(\n        protected RegistrationService $registrationService,\n        protected LoginService $loginService,\n        protected GroupSyncService $groupSyncService\n    ) {\n        $this->config = config('saml2');\n    }\n\n    /**\n     * Initiate a login flow.\n     *\n     * @throws Error\n     */\n    public function login(): array\n    {\n        $toolKit = $this->getToolkit();\n        $returnRoute = url('/saml2/acs');\n\n        return [\n            'url' => $toolKit->login($returnRoute, [], false, false, true),\n            'id'  => $toolKit->getLastRequestID(),\n        ];\n    }\n\n    /**\n     * Initiate a logout flow.\n     * Returns the SAML2 request ID, and the URL to redirect the user to.\n     *\n     * @throws Error\n     * @return array{url: string, id: ?string}\n     */\n    public function logout(User $user): array\n    {\n        $toolKit = $this->getToolkit();\n        $sessionIndex = session()->get('saml2_session_index');\n        $returnUrl = url($this->loginService->logout());\n\n        try {\n            $url = $toolKit->logout(\n                $returnUrl,\n                [],\n                $user->email,\n                $sessionIndex,\n                true,\n                Constants::NAMEID_EMAIL_ADDRESS\n            );\n            $id = $toolKit->getLastRequestID();\n        } catch (Error $error) {\n            if ($error->getCode() !== Error::SAML_SINGLE_LOGOUT_NOT_SUPPORTED) {\n                throw $error;\n            }\n\n            $url = $returnUrl;\n            $id = null;\n        }\n\n        return ['url' => $url, 'id' => $id];\n    }\n\n    /**\n     * Process the ACS response from the idp and return the\n     * matching, or new if registration active, user matched to the idp.\n     * Returns null if not authenticated.\n     *\n     * @throws Error\n     * @throws SamlException\n     * @throws ValidationError\n     * @throws JsonDebugException\n     * @throws UserRegistrationException\n     */\n    public function processAcsResponse(?string $requestId, string $samlResponse): ?User\n    {\n        // The SAML2 toolkit expects the response to be within the $_POST superglobal\n        // so we need to manually put it back there at this point.\n        $_POST['SAMLResponse'] = $samlResponse;\n        $toolkit = $this->getToolkit();\n        $toolkit->processResponse($requestId);\n        $errors = $toolkit->getErrors();\n\n        if (!empty($errors)) {\n            $reason = $toolkit->getLastErrorReason();\n            $message = 'Invalid ACS Response; Errors: ' . implode(', ', $errors);\n            $message .= $reason ? \"; Reason: {$reason}\" : '';\n            throw new Error($message);\n        }\n\n        if (!$toolkit->isAuthenticated()) {\n            return null;\n        }\n\n        $attrs = $toolkit->getAttributes();\n        $id = $toolkit->getNameId();\n        session()->put('saml2_session_index', $toolkit->getSessionIndex());\n\n        return $this->processLoginCallback($id, $attrs);\n    }\n\n    /**\n     * Process a response for the single logout service.\n     *\n     * @throws Error\n     */\n    public function processSlsResponse(?string $requestId): string\n    {\n        $toolkit = $this->getToolkit();\n\n        // The $retrieveParametersFromServer in the call below will mean the library will take the query\n        // parameters, used for the response signing, from the raw $_SERVER['QUERY_STRING']\n        // value so that the exact encoding format is matched when checking the signature.\n        // This is primarily due to ADFS encoding query params with lowercase percent encoding while\n        // PHP (And most other sensible providers) standardise on uppercase.\n        /** @var ?string $samlRedirect */\n        $samlRedirect = $toolkit->processSLO(true, $requestId, true, null, true);\n        $errors = $toolkit->getErrors();\n\n        if (!empty($errors)) {\n            throw new Error(\n                'Invalid SLS Response: ' . implode(', ', $errors)\n            );\n        }\n\n        $defaultBookStackRedirect = $this->loginService->logout();\n\n        return $samlRedirect ?? $defaultBookStackRedirect;\n    }\n\n    /**\n     * Get the metadata for this service provider.\n     *\n     * @throws Error\n     */\n    public function metadata(): string\n    {\n        $toolKit = $this->getToolkit(true);\n        $settings = $toolKit->getSettings();\n        $metadata = $settings->getSPMetadata();\n        $errors = $settings->validateMetadata($metadata);\n\n        if (!empty($errors)) {\n            throw new Error(\n                'Invalid SP metadata: ' . implode(', ', $errors),\n                Error::METADATA_SP_INVALID\n            );\n        }\n\n        return $metadata;\n    }\n\n    /**\n     * Load the underlying Onelogin SAML2 toolkit.\n     *\n     * @throws Error\n     * @throws Exception\n     */\n    protected function getToolkit(bool $spOnly = false): Auth\n    {\n        $settings = $this->config['onelogin'];\n        $overrides = $this->config['onelogin_overrides'] ?? [];\n\n        if ($overrides && is_string($overrides)) {\n            $overrides = json_decode($overrides, true);\n        }\n\n        $metaDataSettings = [];\n        if (!$spOnly && $this->config['autoload_from_metadata']) {\n            $metaDataSettings = IdPMetadataParser::parseRemoteXML($settings['idp']['entityId']);\n        }\n\n        $spSettings = $this->loadOneloginServiceProviderDetails();\n        $settings = array_replace_recursive($settings, $spSettings, $metaDataSettings, $overrides);\n\n        return new Auth($settings, $spOnly);\n    }\n\n    /**\n     * Load dynamic service provider options required by the onelogin toolkit.\n     */\n    protected function loadOneloginServiceProviderDetails(): array\n    {\n        $spDetails = [\n            'entityId'                 => url('/saml2/metadata'),\n            'assertionConsumerService' => [\n                'url' => url('/saml2/acs'),\n            ],\n            'singleLogoutService' => [\n                'url' => url('/saml2/sls'),\n            ],\n        ];\n\n        return [\n            'baseurl' => url('/saml2'),\n            'sp'      => $spDetails,\n        ];\n    }\n\n    /**\n     * Check if groups should be synced.\n     */\n    protected function shouldSyncGroups(): bool\n    {\n        return $this->config['user_to_groups'] !== false;\n    }\n\n    /**\n     * Calculate the display name.\n     */\n    protected function getUserDisplayName(array $samlAttributes, string $defaultValue): string\n    {\n        $displayNameAttr = $this->config['display_name_attributes'];\n\n        $displayName = [];\n        foreach ($displayNameAttr as $dnAttr) {\n            $dnComponent = $this->getSamlResponseAttribute($samlAttributes, $dnAttr, null);\n            if ($dnComponent !== null) {\n                $displayName[] = $dnComponent;\n            }\n        }\n\n        if (count($displayName) == 0) {\n            $displayName = $defaultValue;\n        } else {\n            $displayName = implode(' ', $displayName);\n        }\n\n        return $displayName;\n    }\n\n    /**\n     * Get the value to use as the external id saved in BookStack\n     * used to link the user to an existing BookStack DB user.\n     */\n    protected function getExternalId(array $samlAttributes, string $defaultValue)\n    {\n        $userNameAttr = $this->config['external_id_attribute'];\n        if ($userNameAttr === null) {\n            return $defaultValue;\n        }\n\n        return $this->getSamlResponseAttribute($samlAttributes, $userNameAttr, $defaultValue);\n    }\n\n    /**\n     * Extract the details of a user from a SAML response.\n     *\n     * @return array{external_id: string, name: string, email: string, saml_id: string}\n     */\n    protected function getUserDetails(string $samlID, $samlAttributes): array\n    {\n        $emailAttr = $this->config['email_attribute'];\n        $externalId = $this->getExternalId($samlAttributes, $samlID);\n\n        $defaultEmail = filter_var($samlID, FILTER_VALIDATE_EMAIL) ? $samlID : null;\n        $email = $this->getSamlResponseAttribute($samlAttributes, $emailAttr, $defaultEmail);\n\n        return [\n            'external_id' => $externalId,\n            'name'        => $this->getUserDisplayName($samlAttributes, $externalId),\n            'email'       => $email,\n            'saml_id'     => $samlID,\n        ];\n    }\n\n    /**\n     * Get the groups a user is a part of from the SAML response.\n     */\n    public function getUserGroups(array $samlAttributes): array\n    {\n        $groupsAttr = $this->config['group_attribute'];\n        $userGroups = $samlAttributes[$groupsAttr] ?? null;\n\n        if (!is_array($userGroups)) {\n            $userGroups = [];\n        }\n\n        return $userGroups;\n    }\n\n    /**\n     *  For an array of strings, return a default for an empty array,\n     *  a string for an array with one element and the full array for\n     *  more than one element.\n     */\n    protected function simplifyValue(array $data, $defaultValue)\n    {\n        switch (count($data)) {\n            case 0:\n                $data = $defaultValue;\n                break;\n            case 1:\n                $data = $data[0];\n                break;\n        }\n\n        return $data;\n    }\n\n    /**\n     * Get a property from an SAML response.\n     * Handles properties potentially being an array.\n     */\n    protected function getSamlResponseAttribute(array $samlAttributes, string $propertyKey, $defaultValue)\n    {\n        if (isset($samlAttributes[$propertyKey])) {\n            return $this->simplifyValue($samlAttributes[$propertyKey], $defaultValue);\n        }\n\n        return $defaultValue;\n    }\n\n    /**\n     * Process the SAML response for a user. Login the user when\n     * they exist, optionally registering them automatically.\n     *\n     * @throws SamlException\n     * @throws JsonDebugException\n     * @throws UserRegistrationException\n     * @throws StoppedAuthenticationException\n     */\n    public function processLoginCallback(string $samlID, array $samlAttributes): User\n    {\n        $userDetails = $this->getUserDetails($samlID, $samlAttributes);\n        $isLoggedIn = auth()->check();\n\n        if ($this->shouldSyncGroups()) {\n            $userDetails['groups'] = $this->getUserGroups($samlAttributes);\n        }\n\n        if ($this->config['dump_user_details']) {\n            throw new JsonDebugException([\n                'id_from_idp'         => $samlID,\n                'attrs_from_idp'      => $samlAttributes,\n                'attrs_after_parsing' => $userDetails,\n            ]);\n        }\n\n        if ($userDetails['email'] === null) {\n            throw new SamlException(trans('errors.saml_no_email_address'));\n        }\n\n        if ($isLoggedIn) {\n            throw new SamlException(trans('errors.saml_already_logged_in'), '/login');\n        }\n\n        $user = $this->registrationService->findOrRegister(\n            $userDetails['name'],\n            $userDetails['email'],\n            $userDetails['external_id']\n        );\n\n        if ($this->shouldSyncGroups()) {\n            $this->groupSyncService->syncUserWithFoundGroups($user, $userDetails['groups'], $this->config['remove_from_groups']);\n        }\n\n        $this->loginService->login($user, 'saml2');\n\n        return $user;\n    }\n}\n"
  },
  {
    "path": "app/Access/SocialAccount.php",
    "content": "<?php\n\nnamespace BookStack\\Access;\n\nuse BookStack\\Activity\\Models\\Loggable;\nuse BookStack\\App\\Model;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\n\n/**\n * @property string $driver\n * @property User   $user\n */\nclass SocialAccount extends Model implements Loggable\n{\n    use HasFactory;\n\n    protected $fillable = ['user_id', 'driver', 'driver_id'];\n\n    /**\n     * @return BelongsTo<User, $this>\n     */\n    public function user(): BelongsTo\n    {\n        return $this->belongsTo(User::class);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function logDescriptor(): string\n    {\n        return \"{$this->driver}; {$this->user->logDescriptor()}\";\n    }\n}\n"
  },
  {
    "path": "app/Access/SocialAuthService.php",
    "content": "<?php\n\nnamespace BookStack\\Access;\n\nuse BookStack\\Exceptions\\SocialDriverNotConfigured;\nuse BookStack\\Exceptions\\SocialSignInAccountNotUsed;\nuse BookStack\\Exceptions\\UserRegistrationException;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Support\\Str;\nuse Laravel\\Socialite\\Contracts\\Factory as Socialite;\nuse Laravel\\Socialite\\Contracts\\Provider;\nuse Laravel\\Socialite\\Contracts\\User as SocialUser;\nuse Laravel\\Socialite\\Two\\GoogleProvider;\nuse Symfony\\Component\\HttpFoundation\\RedirectResponse;\n\nclass SocialAuthService\n{\n    public function __construct(\n        protected Socialite $socialite,\n        protected LoginService $loginService,\n        protected SocialDriverManager $driverManager,\n    ) {\n    }\n\n    /**\n     * Start the social login path.\n     *\n     * @throws SocialDriverNotConfigured\n     */\n    public function startLogIn(string $socialDriver): RedirectResponse\n    {\n        $socialDriver = trim(strtolower($socialDriver));\n        $this->driverManager->ensureDriverActive($socialDriver);\n\n        return $this->getDriverForRedirect($socialDriver)->redirect();\n    }\n\n    /**\n     * Start the social registration process.\n     *\n     * @throws SocialDriverNotConfigured\n     */\n    public function startRegister(string $socialDriver): RedirectResponse\n    {\n        $socialDriver = trim(strtolower($socialDriver));\n        $this->driverManager->ensureDriverActive($socialDriver);\n\n        return $this->getDriverForRedirect($socialDriver)->redirect();\n    }\n\n    /**\n     * Handle the social registration process on callback.\n     *\n     * @throws UserRegistrationException\n     */\n    public function handleRegistrationCallback(string $socialDriver, SocialUser $socialUser): SocialUser\n    {\n        // Check social account has not already been used\n        if (SocialAccount::query()->where('driver_id', '=', $socialUser->getId())->exists()) {\n            throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount' => $socialDriver]), '/login');\n        }\n\n        if (User::query()->where('email', '=', $socialUser->getEmail())->exists()) {\n            $email = $socialUser->getEmail();\n\n            throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $email]), '/login');\n        }\n\n        return $socialUser;\n    }\n\n    /**\n     * Get the social user details via the social driver.\n     *\n     * @throws SocialDriverNotConfigured\n     */\n    public function getSocialUser(string $socialDriver): SocialUser\n    {\n        $socialDriver = trim(strtolower($socialDriver));\n        $this->driverManager->ensureDriverActive($socialDriver);\n\n        return $this->socialite->driver($socialDriver)->user();\n    }\n\n    /**\n     * Handle the login process on a oAuth callback.\n     *\n     * @throws SocialSignInAccountNotUsed\n     */\n    public function handleLoginCallback(string $socialDriver, SocialUser $socialUser)\n    {\n        $socialDriver = trim(strtolower($socialDriver));\n        $socialId = $socialUser->getId();\n\n        // Get any attached social accounts or users\n        $socialAccount = SocialAccount::query()->where('driver_id', '=', $socialId)->first();\n        $isLoggedIn = auth()->check();\n        $currentUser = user();\n        $titleCaseDriver = Str::title($socialDriver);\n\n        // When a user is not logged in and a matching SocialAccount exists,\n        // Simply log the user into the application.\n        if (!$isLoggedIn && $socialAccount !== null) {\n            $this->loginService->login($socialAccount->user, $socialDriver);\n\n            return redirect()->intended('/');\n        }\n\n        // When a user is logged in but the social account does not exist,\n        // Create the social account and attach it to the user & redirect to the profile page.\n        if ($isLoggedIn && $socialAccount === null) {\n            $account = $this->newSocialAccount($socialDriver, $socialUser);\n            $currentUser->socialAccounts()->save($account);\n            session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => $titleCaseDriver]));\n\n            return redirect('/my-account/auth#social_accounts');\n        }\n\n        // When a user is logged in and the social account exists and is already linked to the current user.\n        if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) {\n            session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => $titleCaseDriver]));\n\n            return redirect('/my-account/auth#social_accounts');\n        }\n\n        // When a user is logged in, A social account exists but the users do not match.\n        if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {\n            session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => $titleCaseDriver]));\n\n            return redirect('/my-account/auth#social_accounts');\n        }\n\n        // Otherwise let the user know this social account is not used by anyone.\n        $message = trans('errors.social_account_not_used', ['socialAccount' => $titleCaseDriver]);\n        if (setting('registration-enabled') && config('auth.method') !== 'ldap' && config('auth.method') !== 'saml2') {\n            $message .= trans('errors.social_account_register_instructions', ['socialAccount' => $titleCaseDriver]);\n        }\n\n        throw new SocialSignInAccountNotUsed($message, '/login');\n    }\n\n    /**\n     * Get the social driver manager used by this service.\n     */\n    public function drivers(): SocialDriverManager\n    {\n        return $this->driverManager;\n    }\n\n    /**\n     * Fill and return a SocialAccount from the given driver name and SocialUser.\n     */\n    public function newSocialAccount(string $socialDriver, SocialUser $socialUser): SocialAccount\n    {\n        return new SocialAccount([\n            'driver'    => $socialDriver,\n            'driver_id' => $socialUser->getId(),\n            'avatar'    => $socialUser->getAvatar(),\n        ]);\n    }\n\n    /**\n     * Detach a social account from a user.\n     */\n    public function detachSocialAccount(string $socialDriver): void\n    {\n        user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();\n    }\n\n    /**\n     * Provide redirect options per service for the Laravel Socialite driver.\n     */\n    protected function getDriverForRedirect(string $driverName): Provider\n    {\n        $driver = $this->socialite->driver($driverName);\n\n        if ($driver instanceof GoogleProvider && config('services.google.select_account')) {\n            $driver->with(['prompt' => 'select_account']);\n        }\n\n        $this->driverManager->getConfigureForRedirectCallback($driverName)($driver);\n\n        return $driver;\n    }\n}\n"
  },
  {
    "path": "app/Access/SocialDriverManager.php",
    "content": "<?php\n\nnamespace BookStack\\Access;\n\nuse BookStack\\Exceptions\\SocialDriverNotConfigured;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Support\\Str;\nuse SocialiteProviders\\Manager\\SocialiteWasCalled;\n\nclass SocialDriverManager\n{\n    /**\n     * The default built-in social drivers we support.\n     *\n     * @var string[]\n     */\n    protected array $validDrivers = [\n        'google',\n        'github',\n        'facebook',\n        'slack',\n        'twitter',\n        'azure',\n        'okta',\n        'gitlab',\n        'twitch',\n        'discord',\n    ];\n\n    /**\n     * Callbacks to run when configuring a social driver\n     * for an initial redirect action.\n     * Array is keyed by social driver name.\n     * Callbacks are passed an instance of the driver.\n     *\n     * @var array<string, callable>\n     */\n    protected array $configureForRedirectCallbacks = [];\n\n    /**\n     * Check if the current config for the given driver allows auto-registration.\n     */\n    public function isAutoRegisterEnabled(string $driver): bool\n    {\n        return $this->getDriverConfigProperty($driver, 'auto_register') === true;\n    }\n\n    /**\n     * Check if the current config for the given driver allow email address auto-confirmation.\n     */\n    public function isAutoConfirmEmailEnabled(string $driver): bool\n    {\n        return $this->getDriverConfigProperty($driver, 'auto_confirm') === true;\n    }\n\n    /**\n     * Gets the names of the active social drivers, keyed by driver id.\n     * @return array<string, string>\n     */\n    public function getActive(): array\n    {\n        $activeDrivers = [];\n\n        foreach ($this->validDrivers as $driverKey) {\n            if ($this->checkDriverConfigured($driverKey)) {\n                $activeDrivers[$driverKey] = $this->getName($driverKey);\n            }\n        }\n\n        return $activeDrivers;\n    }\n\n    /**\n     * Get the configure-for-redirect callback for the given driver.\n     * This is a callable that allows modification of the driver at redirect time.\n     * Commonly used to perform custom dynamic configuration where required.\n     * The callback is passed a \\Laravel\\Socialite\\Contracts\\Provider instance.\n     */\n    public function getConfigureForRedirectCallback(string $driver): callable\n    {\n        return $this->configureForRedirectCallbacks[$driver] ?? (fn() => true);\n    }\n\n    /**\n     * Add a custom socialite driver to be used.\n     * Driver name should be lower_snake_case.\n     * Config array should mirror the structure of a service\n     * within the `Config/services.php` file.\n     * Handler should be a Class@method handler to the SocialiteWasCalled event.\n     */\n    public function addSocialDriver(\n        string $driverName,\n        array $config,\n        string $socialiteHandler,\n        ?callable $configureForRedirect = null\n    ) {\n        $this->validDrivers[] = $driverName;\n        config()->set('services.' . $driverName, $config);\n        config()->set('services.' . $driverName . '.redirect', url('/login/service/' . $driverName . '/callback'));\n        config()->set('services.' . $driverName . '.name', $config['name'] ?? $driverName);\n        Event::listen(SocialiteWasCalled::class, $socialiteHandler);\n        if (!is_null($configureForRedirect)) {\n            $this->configureForRedirectCallbacks[$driverName] = $configureForRedirect;\n        }\n    }\n\n    /**\n     * Get the presentational name for a driver.\n     */\n    protected function getName(string $driver): string\n    {\n        return $this->getDriverConfigProperty($driver, 'name') ?? '';\n    }\n\n    protected function getDriverConfigProperty(string $driver, string $property): mixed\n    {\n        return config(\"services.{$driver}.{$property}\");\n    }\n\n    /**\n     * Ensure the social driver is correct and supported.\n     *\n     * @throws SocialDriverNotConfigured\n     */\n    public function ensureDriverActive(string $driverName): void\n    {\n        if (!in_array($driverName, $this->validDrivers)) {\n            abort(404, trans('errors.social_driver_not_found'));\n        }\n\n        if (!$this->checkDriverConfigured($driverName)) {\n            throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => Str::title($driverName)]));\n        }\n    }\n\n    /**\n     * Check a social driver has been configured correctly.\n     */\n    protected function checkDriverConfigured(string $driver): bool\n    {\n        $lowerName = strtolower($driver);\n        $configPrefix = 'services.' . $lowerName . '.';\n        $config = [config($configPrefix . 'client_id'), config($configPrefix . 'client_secret'), config('services.callback_url')];\n\n        return !in_array(false, $config) && !in_array(null, $config);\n    }\n}\n"
  },
  {
    "path": "app/Access/UserInviteException.php",
    "content": "<?php\n\nnamespace BookStack\\Access;\n\nuse Exception;\n\nclass UserInviteException extends Exception\n{\n    //\n}\n"
  },
  {
    "path": "app/Access/UserInviteService.php",
    "content": "<?php\n\nnamespace BookStack\\Access;\n\nuse BookStack\\Access\\Notifications\\UserInviteNotification;\nuse BookStack\\Users\\Models\\User;\n\nclass UserInviteService extends UserTokenService\n{\n    protected string $tokenTable = 'user_invites';\n    protected int $expiryTime = 336; // Two weeks\n\n    /**\n     * Send an invitation to a user to sign into BookStack\n     * Removes existing invitation tokens.\n     * @throws UserInviteException\n     */\n    public function sendInvitation(User $user)\n    {\n        $this->deleteByUser($user);\n        $token = $this->createTokenForUser($user);\n\n        try {\n            $user->notify(new UserInviteNotification($token));\n        } catch (\\Exception $exception) {\n            throw new UserInviteException($exception->getMessage(), $exception->getCode(), $exception);\n        }\n    }\n}\n"
  },
  {
    "path": "app/Access/UserTokenService.php",
    "content": "<?php\n\nnamespace BookStack\\Access;\n\nuse BookStack\\Exceptions\\UserTokenExpiredException;\nuse BookStack\\Exceptions\\UserTokenNotFoundException;\nuse BookStack\\Users\\Models\\User;\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Str;\nuse stdClass;\n\nclass UserTokenService\n{\n    /**\n     * Name of table where user tokens are stored.\n     */\n    protected string $tokenTable = 'user_tokens';\n\n    /**\n     * Token expiry time in hours.\n     */\n    protected int $expiryTime = 24;\n\n    /**\n     * Delete all tokens that belong to a user.\n     */\n    public function deleteByUser(User $user): void\n    {\n        DB::table($this->tokenTable)\n            ->where('user_id', '=', $user->id)\n            ->delete();\n    }\n\n    /**\n     * Get the user id from a token, while checking the token exists and has not expired.\n     *\n     * @throws UserTokenNotFoundException\n     * @throws UserTokenExpiredException\n     */\n    public function checkTokenAndGetUserId(string $token): int\n    {\n        $entry = $this->getEntryByToken($token);\n\n        if (is_null($entry)) {\n            throw new UserTokenNotFoundException('Token \"' . $token . '\" not found');\n        }\n\n        if ($this->entryExpired($entry)) {\n            throw new UserTokenExpiredException(\"Token of id {$entry->id} has expired.\", $entry->user_id);\n        }\n\n        return $entry->user_id;\n    }\n\n    /**\n     * Creates a unique token within the email confirmation database.\n     */\n    protected function generateToken(): string\n    {\n        $token = Str::random(24);\n        while ($this->tokenExists($token)) {\n            $token = Str::random(25);\n        }\n\n        return $token;\n    }\n\n    /**\n     * Generate and store a token for the given user.\n     */\n    protected function createTokenForUser(User $user): string\n    {\n        $token = $this->generateToken();\n        DB::table($this->tokenTable)->insert([\n            'user_id'    => $user->id,\n            'token'      => $token,\n            'created_at' => Carbon::now(),\n            'updated_at' => Carbon::now(),\n        ]);\n\n        return $token;\n    }\n\n    /**\n     * Check if the given token exists.\n     */\n    protected function tokenExists(string $token): bool\n    {\n        return DB::table($this->tokenTable)\n            ->where('token', '=', $token)->exists();\n    }\n\n    /**\n     * Get a token entry for the given token.\n     */\n    protected function getEntryByToken(string $token): ?stdClass\n    {\n        return DB::table($this->tokenTable)\n            ->where('token', '=', $token)\n            ->first();\n    }\n\n    /**\n     * Check if the given token entry has expired.\n     */\n    protected function entryExpired(stdClass $tokenEntry): bool\n    {\n        return Carbon::now()->subHours($this->expiryTime)\n            ->gt(new Carbon($tokenEntry->created_at));\n    }\n}\n"
  },
  {
    "path": "app/Activity/ActivityQueries.php",
    "content": "<?php\n\nnamespace BookStack\\Activity;\n\nuse BookStack\\Activity\\Models\\Activity;\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Tools\\MixedEntityListLoader;\nuse BookStack\\Permissions\\PermissionApplicator;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\MorphTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\Relation;\n\nclass ActivityQueries\n{\n    public function __construct(\n        protected PermissionApplicator $permissions,\n        protected MixedEntityListLoader $listLoader,\n    ) {\n    }\n\n    /**\n     * Gets the latest activity.\n     */\n    public function latest(int $count = 20, int $page = 0): array\n    {\n        $activityList = $this->permissions\n            ->restrictEntityRelationQuery(Activity::query(), 'activities', 'loggable_id', 'loggable_type')\n            ->orderBy('created_at', 'desc')\n            ->with(['user'])\n            ->skip($count * $page)\n            ->take($count)\n            ->get();\n\n        $this->listLoader->loadIntoRelations($activityList->all(), 'loggable', false);\n\n        return $this->filterSimilar($activityList);\n    }\n\n    /**\n     * Gets the latest activity for an entity, Filtering out similar\n     * items to prevent a message activity list.\n     */\n    public function entityActivity(Entity $entity, int $count = 20, int $page = 1): array\n    {\n        /** @var array<string, int[]> $queryIds */\n        $queryIds = [$entity->getMorphClass() => [$entity->id]];\n\n        if ($entity instanceof Book) {\n            $queryIds[(new Chapter())->getMorphClass()] = $entity->chapters()->scopes('visible')->pluck('id');\n        }\n        if ($entity instanceof Book || $entity instanceof Chapter) {\n            $queryIds[(new Page())->getMorphClass()] = $entity->pages()->scopes('visible')->pluck('id');\n        }\n\n        $query = Activity::query();\n        $query->where(function (Builder $query) use ($queryIds) {\n            foreach ($queryIds as $morphClass => $idArr) {\n                $query->orWhere(function (Builder $innerQuery) use ($morphClass, $idArr) {\n                    $innerQuery->where('loggable_type', '=', $morphClass)\n                        ->whereIn('loggable_id', $idArr);\n                });\n            }\n        });\n\n        $activity = $query->orderBy('created_at', 'desc')\n            ->with(['loggable' => function (Relation $query) {\n                /** @var MorphTo<Entity, Activity> $query */\n                $query->withTrashed();\n            }, 'user.avatar'])\n            ->skip($count * ($page - 1))\n            ->take($count)\n            ->get();\n\n        return $this->filterSimilar($activity);\n    }\n\n    /**\n     * Get the latest activity for a user, Filtering out similar items.\n     */\n    public function userActivity(User $user, int $count = 20, int $page = 0): array\n    {\n        $activityList = $this->permissions\n            ->restrictEntityRelationQuery(Activity::query(), 'activities', 'loggable_id', 'loggable_type')\n            ->orderBy('created_at', 'desc')\n            ->where('user_id', '=', $user->id)\n            ->skip($count * $page)\n            ->take($count)\n            ->get();\n\n        return $this->filterSimilar($activityList);\n    }\n\n    /**\n     * Filters out similar activity.\n     *\n     * @param Activity[] $activities\n     */\n    protected function filterSimilar(iterable $activities): array\n    {\n        $newActivity = [];\n        $previousItem = null;\n\n        foreach ($activities as $activityItem) {\n            if (!$previousItem || !$activityItem->isSimilarTo($previousItem)) {\n                $newActivity[] = $activityItem;\n            }\n\n            $previousItem = $activityItem;\n        }\n\n        return $newActivity;\n    }\n}\n"
  },
  {
    "path": "app/Activity/ActivityType.php",
    "content": "<?php\n\nnamespace BookStack\\Activity;\n\nclass ActivityType\n{\n    const PAGE_CREATE = 'page_create';\n    const PAGE_UPDATE = 'page_update';\n    const PAGE_DELETE = 'page_delete';\n    const PAGE_RESTORE = 'page_restore';\n    const PAGE_MOVE = 'page_move';\n\n    const CHAPTER_CREATE = 'chapter_create';\n    const CHAPTER_UPDATE = 'chapter_update';\n    const CHAPTER_DELETE = 'chapter_delete';\n    const CHAPTER_MOVE = 'chapter_move';\n\n    const BOOK_CREATE = 'book_create';\n    const BOOK_CREATE_FROM_CHAPTER = 'book_create_from_chapter';\n    const BOOK_UPDATE = 'book_update';\n    const BOOK_DELETE = 'book_delete';\n    const BOOK_SORT = 'book_sort';\n\n    const BOOKSHELF_CREATE = 'bookshelf_create';\n    const BOOKSHELF_CREATE_FROM_BOOK = 'bookshelf_create_from_book';\n    const BOOKSHELF_UPDATE = 'bookshelf_update';\n    const BOOKSHELF_DELETE = 'bookshelf_delete';\n\n    const COMMENTED_ON = 'commented_on';\n    const COMMENT_CREATE = 'comment_create';\n    const COMMENT_UPDATE = 'comment_update';\n    const COMMENT_DELETE = 'comment_delete';\n\n    const PERMISSIONS_UPDATE = 'permissions_update';\n\n    const REVISION_RESTORE = 'revision_restore';\n    const REVISION_DELETE = 'revision_delete';\n\n    const SETTINGS_UPDATE = 'settings_update';\n    const MAINTENANCE_ACTION_RUN = 'maintenance_action_run';\n\n    const RECYCLE_BIN_EMPTY = 'recycle_bin_empty';\n    const RECYCLE_BIN_RESTORE = 'recycle_bin_restore';\n    const RECYCLE_BIN_DESTROY = 'recycle_bin_destroy';\n\n    const USER_CREATE = 'user_create';\n    const USER_UPDATE = 'user_update';\n    const USER_DELETE = 'user_delete';\n\n    const API_TOKEN_CREATE = 'api_token_create';\n    const API_TOKEN_UPDATE = 'api_token_update';\n    const API_TOKEN_DELETE = 'api_token_delete';\n\n    const ROLE_CREATE = 'role_create';\n    const ROLE_UPDATE = 'role_update';\n    const ROLE_DELETE = 'role_delete';\n\n    const AUTH_PASSWORD_RESET = 'auth_password_reset_request';\n    const AUTH_PASSWORD_RESET_UPDATE = 'auth_password_reset_update';\n    const AUTH_LOGIN = 'auth_login';\n    const AUTH_REGISTER = 'auth_register';\n\n    const MFA_SETUP_METHOD = 'mfa_setup_method';\n    const MFA_REMOVE_METHOD = 'mfa_remove_method';\n\n    const WEBHOOK_CREATE = 'webhook_create';\n    const WEBHOOK_UPDATE = 'webhook_update';\n    const WEBHOOK_DELETE = 'webhook_delete';\n\n    const IMPORT_CREATE = 'import_create';\n    const IMPORT_RUN = 'import_run';\n    const IMPORT_DELETE = 'import_delete';\n\n    const SORT_RULE_CREATE = 'sort_rule_create';\n    const SORT_RULE_UPDATE = 'sort_rule_update';\n    const SORT_RULE_DELETE = 'sort_rule_delete';\n\n    /**\n     * Get all the possible values.\n     */\n    public static function all(): array\n    {\n        return (new \\ReflectionClass(static::class))->getConstants();\n    }\n}\n"
  },
  {
    "path": "app/Activity/CommentRepo.php",
    "content": "<?php\n\nnamespace BookStack\\Activity;\n\nuse BookStack\\Activity\\Models\\Comment;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Exceptions\\NotifyException;\nuse BookStack\\Facades\\Activity as ActivityService;\nuse BookStack\\Util\\HtmlDescriptionFilter;\nuse Illuminate\\Database\\Eloquent\\Builder;\n\nclass CommentRepo\n{\n    /**\n     * Get a comment by ID.\n     */\n    public function getById(int $id): Comment\n    {\n        return Comment::query()->findOrFail($id);\n    }\n\n    /**\n     * Get a comment by ID, ensuring it is visible to the user based upon access to the page\n     * which the comment is attached to.\n     */\n    public function getVisibleById(int $id): Comment\n    {\n        return $this->getQueryForVisible()->findOrFail($id);\n    }\n\n    /**\n     * Start a query for comments visible to the user.\n     * @return Builder<Comment>\n     */\n    public function getQueryForVisible(): Builder\n    {\n        return Comment::query()->scopes('visible');\n    }\n\n    /**\n     * Create a new comment on an entity.\n     */\n    public function create(Entity $entity, string $html, ?int $parentId, string $contentRef): Comment\n    {\n        // Prevent comments being added to draft pages\n        if ($entity instanceof Page && $entity->draft) {\n            throw new \\Exception(trans('errors.cannot_add_comment_to_draft'));\n        }\n\n        // Validate parent ID\n        if ($parentId !== null) {\n            $parentCommentExists = Comment::query()\n                ->where('commentable_id', '=', $entity->id)\n                ->where('commentable_type', '=', $entity->getMorphClass())\n                ->where('local_id', '=', $parentId)\n                ->exists();\n            if (!$parentCommentExists) {\n                $parentId = null;\n            }\n        }\n\n        $userId = user()->id;\n        $comment = new Comment();\n\n        $comment->html = HtmlDescriptionFilter::filterFromString($html);\n        $comment->created_by = $userId;\n        $comment->updated_by = $userId;\n        $comment->local_id = $this->getNextLocalId($entity);\n        $comment->parent_id = $parentId;\n        $comment->content_ref = preg_match('/^bkmrk-(.*?):\\d+:(\\d*-\\d*)?$/', $contentRef) === 1 ? $contentRef : '';\n\n        $entity->comments()->save($comment);\n        ActivityService::add(ActivityType::COMMENT_CREATE, $comment);\n        ActivityService::add(ActivityType::COMMENTED_ON, $entity);\n\n        $comment->refresh()->unsetRelations();\n        return $comment;\n    }\n\n    /**\n     * Update an existing comment.\n     */\n    public function update(Comment $comment, string $html): Comment\n    {\n        $comment->updated_by = user()->id;\n        $comment->html = HtmlDescriptionFilter::filterFromString($html);\n        $comment->save();\n\n        ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);\n\n        return $comment;\n    }\n\n\n    /**\n     * Archive an existing comment.\n     */\n    public function archive(Comment $comment, bool $log = true): Comment\n    {\n        if ($comment->parent_id) {\n            throw new NotifyException('Only top-level comments can be archived.', '/', 400);\n        }\n\n        $comment->archived = true;\n        $comment->save();\n\n        if ($log) {\n            ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);\n        }\n\n        return $comment;\n    }\n\n    /**\n     * Un-archive an existing comment.\n     */\n    public function unarchive(Comment $comment, bool $log = true): Comment\n    {\n        if ($comment->parent_id) {\n            throw new NotifyException('Only top-level comments can be un-archived.', '/', 400);\n        }\n\n        $comment->archived = false;\n        $comment->save();\n\n        if ($log) {\n            ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);\n        }\n\n        return $comment;\n    }\n\n    /**\n     * Delete a comment from the system.\n     */\n    public function delete(Comment $comment): void\n    {\n        $comment->delete();\n\n        ActivityService::add(ActivityType::COMMENT_DELETE, $comment);\n    }\n\n    /**\n     * Get the next local ID relative to the linked entity.\n     */\n    protected function getNextLocalId(Entity $entity): int\n    {\n        $currentMaxId = $entity->comments()->max('local_id');\n\n        return $currentMaxId + 1;\n    }\n}\n"
  },
  {
    "path": "app/Activity/Controllers/AuditLogApiController.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Controllers;\n\nuse BookStack\\Activity\\Models\\Activity;\nuse BookStack\\Http\\ApiController;\nuse BookStack\\Permissions\\Permission;\n\nclass AuditLogApiController extends ApiController\n{\n    /**\n     * Get a listing of audit log events in the system.\n     * The loggable relation fields currently only relates to core\n     * content types (page, book, bookshelf, chapter) but this may be\n     * used more in the future across other types.\n     * Requires permission to manage both users and system settings.\n     */\n    public function list()\n    {\n        $this->checkPermission(Permission::SettingsManage);\n        $this->checkPermission(Permission::UsersManage);\n\n        $query = Activity::query()->with(['user']);\n\n        return $this->apiListingResponse($query, [\n            'id', 'type', 'detail', 'user_id', 'loggable_id', 'loggable_type', 'ip', 'created_at',\n        ]);\n    }\n}\n"
  },
  {
    "path": "app/Activity/Controllers/AuditLogController.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Controllers;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Activity\\Models\\Activity;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Sorting\\SortUrl;\nuse BookStack\\Util\\SimpleListOptions;\nuse Illuminate\\Http\\Request;\n\nclass AuditLogController extends Controller\n{\n    public function index(Request $request)\n    {\n        $this->checkPermission(Permission::SettingsManage);\n        $this->checkPermission(Permission::UsersManage);\n\n        $sort = $request->get('sort', 'activity_date');\n        $order = $request->get('order', 'desc');\n        $listOptions = (new SimpleListOptions('', $sort, $order))->withSortOptions([\n            'created_at' => trans('settings.audit_table_date'),\n            'type' => trans('settings.audit_table_event'),\n        ]);\n\n        $filters = [\n            'event'     => $request->get('event', ''),\n            'date_from' => $request->get('date_from', ''),\n            'date_to'   => $request->get('date_to', ''),\n            'user'      => $request->get('user', ''),\n            'ip'        => $request->get('ip', ''),\n        ];\n\n        $query = Activity::query()\n            ->with([\n                'loggable' => fn ($query) => $query->withTrashed(),\n                'user',\n            ])\n            ->orderBy($listOptions->getSort(), $listOptions->getOrder());\n\n        if ($filters['event']) {\n            $query->where('type', '=', $filters['event']);\n        }\n        if ($filters['user']) {\n            $query->where('user_id', '=', $filters['user']);\n        }\n\n        if ($filters['date_from']) {\n            $query->where('created_at', '>=', $filters['date_from']);\n        }\n        if ($filters['date_to']) {\n            $query->where('created_at', '<=', $filters['date_to']);\n        }\n        if ($filters['ip']) {\n            $query->where('ip', 'like', $filters['ip'] . '%');\n        }\n\n        $activities = $query->paginate(100);\n        $activities->appends($request->all());\n\n        $types = ActivityType::all();\n        $this->setPageTitle(trans('settings.audit'));\n\n        return view('settings.audit', [\n            'activities'    => $activities,\n            'filters'       => $filters,\n            'listOptions'   => $listOptions,\n            'activityTypes' => $types,\n            'filterSortUrl' => new SortUrl('settings/audit', array_filter($request->except('page')))\n        ]);\n    }\n}\n"
  },
  {
    "path": "app/Activity/Controllers/CommentApiController.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace BookStack\\Activity\\Controllers;\n\nuse BookStack\\Activity\\CommentRepo;\nuse BookStack\\Activity\\Models\\Comment;\nuse BookStack\\Entities\\Queries\\PageQueries;\nuse BookStack\\Http\\ApiController;\nuse BookStack\\Permissions\\Permission;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\n\n/**\n * The comment data model has a 'local_id' property, which is a unique integer ID\n * scoped to the page which the comment is on. The 'parent_id' is used for replies\n * and refers to the 'local_id' of the parent comment on the same page, not the main\n * globally unique 'id'.\n *\n * If you want to get all comments for a page in a tree-like structure, as reflected in\n * the UI, then that is provided on pages-read API responses.\n */\nclass CommentApiController extends ApiController\n{\n    protected array $rules = [\n        'create' => [\n            'page_id' => ['required', 'integer'],\n            'reply_to' => ['nullable', 'integer'],\n            'html' => ['required', 'string'],\n            'content_ref' => ['string'],\n        ],\n        'update' => [\n            'html' => ['string'],\n            'archived' => ['boolean'],\n        ]\n    ];\n\n    public function __construct(\n        protected CommentRepo $commentRepo,\n        protected PageQueries $pageQueries,\n    ) {\n    }\n\n    /**\n     * Get a listing of comments visible to the user.\n     */\n    public function list(): JsonResponse\n    {\n        $query = $this->commentRepo->getQueryForVisible();\n\n        return $this->apiListingResponse($query, [\n            'id', 'commentable_id', 'commentable_type', 'parent_id', 'local_id', 'content_ref', 'created_by', 'updated_by', 'created_at', 'updated_at'\n        ]);\n    }\n\n    /**\n     * Create a new comment on a page.\n     * If commenting as a reply to an existing comment, the 'reply_to' parameter\n     * should be provided, set to the 'local_id' of the comment being replied to.\n     */\n    public function create(Request $request): JsonResponse\n    {\n        $this->checkPermission(Permission::CommentCreateAll);\n\n        $input = $this->validate($request, $this->rules()['create']);\n        $page = $this->pageQueries->findVisibleByIdOrFail($input['page_id']);\n\n        $comment = $this->commentRepo->create(\n            $page,\n            $input['html'],\n            $input['reply_to'] ?? null,\n            $input['content_ref'] ?? '',\n        );\n\n        return response()->json($comment);\n    }\n\n    /**\n     * Read the details of a single comment, along with its direct replies.\n     */\n    public function read(string $id): JsonResponse\n    {\n        $comment = $this->commentRepo->getVisibleById(intval($id));\n        $comment->load('createdBy', 'updatedBy');\n\n        $replies = $this->commentRepo->getQueryForVisible()\n            ->where('parent_id', '=', $comment->local_id)\n            ->where('commentable_id', '=', $comment->commentable_id)\n            ->where('commentable_type', '=', $comment->commentable_type)\n            ->get();\n\n        /** @var Comment[] $toProcess */\n        $toProcess = [$comment, ...$replies];\n        foreach ($toProcess as $commentToProcess) {\n            $commentToProcess->setAttribute('html', $commentToProcess->safeHtml());\n            $commentToProcess->makeVisible('html');\n        }\n\n        $comment->setRelation('replies', $replies);\n\n        return response()->json($comment);\n    }\n\n\n    /**\n     * Update the content or archived status of an existing comment.\n     *\n     * Only provide a new archived status if needing to actively change the archive state.\n     * Only top-level comments (non-replies) can be archived or unarchived.\n     */\n    public function update(Request $request, string $id): JsonResponse\n    {\n        $comment = $this->commentRepo->getVisibleById(intval($id));\n        $this->checkOwnablePermission(Permission::CommentUpdate, $comment);\n\n        $input = $this->validate($request, $this->rules()['update']);\n        $hasHtml = isset($input['html']);\n\n        if (isset($input['archived'])) {\n            if ($input['archived']) {\n                $this->commentRepo->archive($comment, !$hasHtml);\n            } else {\n                $this->commentRepo->unarchive($comment, !$hasHtml);\n            }\n        }\n\n        if ($hasHtml) {\n            $comment = $this->commentRepo->update($comment, $input['html']);\n        }\n\n        return response()->json($comment);\n    }\n\n    /**\n     * Delete a single comment from the system.\n     */\n    public function delete(string $id): Response\n    {\n        $comment = $this->commentRepo->getVisibleById(intval($id));\n        $this->checkOwnablePermission(Permission::CommentDelete, $comment);\n\n        $this->commentRepo->delete($comment);\n\n        return response('', 204);\n    }\n}\n"
  },
  {
    "path": "app/Activity/Controllers/CommentController.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Controllers;\n\nuse BookStack\\Activity\\CommentRepo;\nuse BookStack\\Activity\\Tools\\CommentTree;\nuse BookStack\\Activity\\Tools\\CommentTreeNode;\nuse BookStack\\Entities\\Queries\\PageQueries;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Validation\\ValidationException;\n\nclass CommentController extends Controller\n{\n    public function __construct(\n        protected CommentRepo $commentRepo,\n        protected PageQueries $pageQueries,\n    ) {\n    }\n\n    /**\n     * Save a new comment for a Page.\n     *\n     * @throws ValidationException|\\Exception\n     */\n    public function savePageComment(Request $request, int $pageId)\n    {\n        $input = $this->validate($request, [\n            'html'      => ['required', 'string'],\n            'parent_id' => ['nullable', 'integer'],\n            'content_ref' => ['string'],\n        ]);\n\n        $page = $this->pageQueries->findVisibleById($pageId);\n        if ($page === null) {\n            return response('Not found', 404);\n        }\n\n        // Create a new comment.\n        $this->checkPermission(Permission::CommentCreateAll);\n        $contentRef = $input['content_ref'] ?? '';\n        $comment = $this->commentRepo->create($page, $input['html'], $input['parent_id'] ?? null, $contentRef);\n\n        return view('comments.comment-branch', [\n            'readOnly' => false,\n            'branch' => new CommentTreeNode($comment, 0, []),\n        ]);\n    }\n\n    /**\n     * Update an existing comment.\n     *\n     * @throws ValidationException\n     */\n    public function update(Request $request, int $commentId)\n    {\n        $input = $this->validate($request, [\n            'html' => ['required', 'string'],\n        ]);\n\n        $comment = $this->commentRepo->getById($commentId);\n        $this->checkOwnablePermission(Permission::PageView, $comment->entity);\n        $this->checkOwnablePermission(Permission::CommentUpdate, $comment);\n\n        $comment = $this->commentRepo->update($comment, $input['html']);\n\n        return view('comments.comment', [\n            'comment' => $comment,\n            'readOnly' => false,\n        ]);\n    }\n\n    /**\n     * Mark a comment as archived.\n     */\n    public function archive(int $id)\n    {\n        $comment = $this->commentRepo->getById($id);\n        $this->checkOwnablePermission(Permission::PageView, $comment->entity);\n        if (!userCan(Permission::CommentUpdate, $comment) && !userCan(Permission::CommentDelete, $comment)) {\n            $this->showPermissionError();\n        }\n\n        $this->commentRepo->archive($comment);\n\n        $tree = new CommentTree($comment->entity);\n        return view('comments.comment-branch', [\n            'readOnly' => false,\n            'branch' => $tree->getCommentNodeForId($id),\n        ]);\n    }\n\n    /**\n     * Unmark a comment as archived.\n     */\n    public function unarchive(int $id)\n    {\n        $comment = $this->commentRepo->getById($id);\n        $this->checkOwnablePermission(Permission::PageView, $comment->entity);\n        if (!userCan(Permission::CommentUpdate, $comment) && !userCan(Permission::CommentDelete, $comment)) {\n            $this->showPermissionError();\n        }\n\n        $this->commentRepo->unarchive($comment);\n\n        $tree = new CommentTree($comment->entity);\n        return view('comments.comment-branch', [\n            'readOnly' => false,\n            'branch' => $tree->getCommentNodeForId($id),\n        ]);\n    }\n\n    /**\n     * Delete a comment from the system.\n     */\n    public function destroy(int $id)\n    {\n        $comment = $this->commentRepo->getById($id);\n        $this->checkOwnablePermission(Permission::CommentDelete, $comment);\n\n        $this->commentRepo->delete($comment);\n\n        return response()->json(['message' => trans('entities.comment_deleted')]);\n    }\n}\n"
  },
  {
    "path": "app/Activity/Controllers/FavouriteController.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Controllers;\n\nuse BookStack\\Entities\\Queries\\QueryTopFavourites;\nuse BookStack\\Entities\\Tools\\MixedEntityRequestHelper;\nuse BookStack\\Http\\Controller;\nuse Illuminate\\Http\\Request;\n\nclass FavouriteController extends Controller\n{\n    public function __construct(\n        protected MixedEntityRequestHelper $entityHelper,\n    ) {\n    }\n\n    /**\n     * Show a listing of all favourite items for the current user.\n     */\n    public function index(Request $request, QueryTopFavourites $topFavourites)\n    {\n        $viewCount = 20;\n        $page = intval($request->get('page', 1));\n        $favourites = $topFavourites->run($viewCount + 1, (($page - 1) * $viewCount));\n\n        $hasMoreLink = ($favourites->count() > $viewCount) ? url('/favourites?page=' . ($page + 1)) : null;\n\n        $this->setPageTitle(trans('entities.my_favourites'));\n\n        return view('common.detailed-listing-with-more', [\n            'title'       => trans('entities.my_favourites'),\n            'entities'    => $favourites->slice(0, $viewCount),\n            'hasMoreLink' => $hasMoreLink,\n        ]);\n    }\n\n    /**\n     * Add a new item as a favourite.\n     */\n    public function add(Request $request)\n    {\n        $modelInfo = $this->validate($request, $this->entityHelper->validationRules());\n        $entity = $this->entityHelper->getVisibleEntityFromRequestData($modelInfo);\n        $entity->favourites()->firstOrCreate([\n            'user_id' => user()->id,\n        ]);\n\n        $this->showSuccessNotification(trans('activities.favourite_add_notification', [\n            'name' => $entity->name,\n        ]));\n\n        return redirect($entity->getUrl());\n    }\n\n    /**\n     * Remove an item as a favourite.\n     */\n    public function remove(Request $request)\n    {\n        $modelInfo = $this->validate($request, $this->entityHelper->validationRules());\n        $entity = $this->entityHelper->getVisibleEntityFromRequestData($modelInfo);\n        $entity->favourites()->where([\n            'user_id' => user()->id,\n        ])->delete();\n\n        $this->showSuccessNotification(trans('activities.favourite_remove_notification', [\n            'name' => $entity->name,\n        ]));\n\n        return redirect($entity->getUrl());\n    }\n}\n"
  },
  {
    "path": "app/Activity/Controllers/TagController.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Controllers;\n\nuse BookStack\\Activity\\TagRepo;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Util\\SimpleListOptions;\nuse Illuminate\\Http\\Request;\n\nclass TagController extends Controller\n{\n    public function __construct(\n        protected TagRepo $tagRepo\n    ) {\n    }\n\n    /**\n     * Show a listing of existing tags in the system.\n     */\n    public function index(Request $request)\n    {\n        $listOptions = SimpleListOptions::fromRequest($request, 'tags')->withSortOptions([\n            'name' => trans('common.sort_name'),\n            'usages' => trans('entities.tags_usages'),\n        ]);\n\n        $nameFilter = $request->get('name', '');\n        $tags = $this->tagRepo\n            ->queryWithTotals($listOptions, $nameFilter)\n            ->paginate(50)\n            ->appends(array_filter(array_merge($listOptions->getPaginationAppends(), [\n                'name'   => $nameFilter,\n            ])));\n\n        $this->setPageTitle(trans('entities.tags'));\n\n        return view('tags.index', [\n            'tags'        => $tags,\n            'nameFilter'  => $nameFilter,\n            'listOptions' => $listOptions,\n        ]);\n    }\n\n    /**\n     * Get tag name suggestions from a given search term.\n     */\n    public function getNameSuggestions(Request $request)\n    {\n        $searchTerm = $request->get('search', '');\n        $suggestions = $this->tagRepo->getNameSuggestions($searchTerm);\n\n        return response()->json($suggestions);\n    }\n\n    /**\n     * Get tag value suggestions from a given search term.\n     */\n    public function getValueSuggestions(Request $request)\n    {\n        $searchTerm = $request->get('search', '');\n        $tagName = $request->get('name', '');\n        $suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName);\n\n        return response()->json($suggestions);\n    }\n}\n"
  },
  {
    "path": "app/Activity/Controllers/WatchController.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Controllers;\n\nuse BookStack\\Activity\\Tools\\UserEntityWatchOptions;\nuse BookStack\\Entities\\Tools\\MixedEntityRequestHelper;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse Illuminate\\Http\\Request;\n\nclass WatchController extends Controller\n{\n    public function update(Request $request, MixedEntityRequestHelper $entityHelper)\n    {\n        $this->checkPermission(Permission::ReceiveNotifications);\n        $this->preventGuestAccess();\n\n        $requestData = $this->validate($request, array_merge([\n            'level' => ['required', 'string'],\n        ], $entityHelper->validationRules()));\n\n        $watchable = $entityHelper->getVisibleEntityFromRequestData($requestData);\n        $watchOptions = new UserEntityWatchOptions(user(), $watchable);\n        $watchOptions->updateLevelByName($requestData['level']);\n\n        $this->showSuccessNotification(trans('activities.watch_update_level_notification'));\n\n        return redirect($watchable->getUrl());\n    }\n}\n"
  },
  {
    "path": "app/Activity/Controllers/WebhookController.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Controllers;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Activity\\Models\\Webhook;\nuse BookStack\\Activity\\Queries\\WebhooksAllPaginatedAndSorted;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Util\\SimpleListOptions;\nuse Illuminate\\Http\\Request;\n\nclass WebhookController extends Controller\n{\n    public function __construct()\n    {\n        $this->middleware([\n            Permission::SettingsManage->middleware()\n        ]);\n    }\n\n    /**\n     * Show all webhooks configured in the system.\n     */\n    public function index(Request $request)\n    {\n        $listOptions = SimpleListOptions::fromRequest($request, 'webhooks')->withSortOptions([\n            'name' => trans('common.sort_name'),\n            'endpoint'  => trans('settings.webhooks_endpoint'),\n            'created_at' => trans('common.sort_created_at'),\n            'updated_at' => trans('common.sort_updated_at'),\n            'active'     => trans('common.status'),\n        ]);\n\n        $webhooks = (new WebhooksAllPaginatedAndSorted())->run(20, $listOptions);\n        $webhooks->appends($listOptions->getPaginationAppends());\n\n        $this->setPageTitle(trans('settings.webhooks'));\n\n        return view('settings.webhooks.index', [\n            'webhooks'    => $webhooks,\n            'listOptions' => $listOptions,\n        ]);\n    }\n\n    /**\n     * Show the view for creating a new webhook in the system.\n     */\n    public function create()\n    {\n        $this->setPageTitle(trans('settings.webhooks_create'));\n\n        return view('settings.webhooks.create');\n    }\n\n    /**\n     * Store a new webhook in the system.\n     */\n    public function store(Request $request)\n    {\n        $validated = $this->validate($request, [\n            'name'     => ['required', 'max:150'],\n            'endpoint' => ['required', 'url', 'max:500'],\n            'events'   => ['required', 'array'],\n            'active'   => ['required'],\n            'timeout'  => ['required', 'integer', 'min:1', 'max:600'],\n        ]);\n\n        $webhook = new Webhook($validated);\n        $webhook->active = $validated['active'] === 'true';\n        $webhook->save();\n        $webhook->updateTrackedEvents(array_values($validated['events']));\n\n        $this->logActivity(ActivityType::WEBHOOK_CREATE, $webhook);\n\n        return redirect('/settings/webhooks');\n    }\n\n    /**\n     * Show the view to edit an existing webhook.\n     */\n    public function edit(string $id)\n    {\n        /** @var Webhook $webhook */\n        $webhook = Webhook::query()\n            ->with('trackedEvents')\n            ->findOrFail($id);\n\n        $this->setPageTitle(trans('settings.webhooks_edit'));\n\n        return view('settings.webhooks.edit', ['webhook' => $webhook]);\n    }\n\n    /**\n     * Update an existing webhook with the provided request data.\n     */\n    public function update(Request $request, string $id)\n    {\n        $validated = $this->validate($request, [\n            'name'     => ['required', 'max:150'],\n            'endpoint' => ['required', 'url', 'max:500'],\n            'events'   => ['required', 'array'],\n            'active'   => ['required'],\n            'timeout'  => ['required', 'integer', 'min:1', 'max:600'],\n        ]);\n\n        /** @var Webhook $webhook */\n        $webhook = Webhook::query()->findOrFail($id);\n\n        $webhook->active = $validated['active'] === 'true';\n        $webhook->fill($validated)->save();\n        $webhook->updateTrackedEvents($validated['events']);\n\n        $this->logActivity(ActivityType::WEBHOOK_UPDATE, $webhook);\n\n        return redirect('/settings/webhooks');\n    }\n\n    /**\n     * Show the view to delete a webhook.\n     */\n    public function delete(string $id)\n    {\n        /** @var Webhook $webhook */\n        $webhook = Webhook::query()->findOrFail($id);\n\n        $this->setPageTitle(trans('settings.webhooks_delete'));\n\n        return view('settings.webhooks.delete', ['webhook' => $webhook]);\n    }\n\n    /**\n     * Destroy a webhook from the system.\n     */\n    public function destroy(string $id)\n    {\n        /** @var Webhook $webhook */\n        $webhook = Webhook::query()->findOrFail($id);\n\n        $webhook->trackedEvents()->delete();\n        $webhook->delete();\n\n        $this->logActivity(ActivityType::WEBHOOK_DELETE, $webhook);\n\n        return redirect('/settings/webhooks');\n    }\n}\n"
  },
  {
    "path": "app/Activity/DispatchWebhookJob.php",
    "content": "<?php\n\nnamespace BookStack\\Activity;\n\nuse BookStack\\Activity\\Models\\Loggable;\nuse BookStack\\Activity\\Models\\Webhook;\nuse BookStack\\Activity\\Tools\\WebhookFormatter;\nuse BookStack\\Facades\\Theme;\nuse BookStack\\Http\\HttpRequestService;\nuse BookStack\\Theming\\ThemeEvents;\nuse BookStack\\Users\\Models\\User;\nuse BookStack\\Util\\SsrUrlValidator;\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass DispatchWebhookJob implements ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    protected Webhook $webhook;\n    protected User $initiator;\n    protected int $initiatedTime;\n    protected array $webhookData;\n\n    /**\n     * Create a new job instance.\n     *\n     * @return void\n     */\n    public function __construct(Webhook $webhook, string $event, Loggable|string $detail)\n    {\n        $this->webhook = $webhook;\n        $this->initiator = user();\n        $this->initiatedTime = time();\n\n        $themeResponse = Theme::dispatch(ThemeEvents::WEBHOOK_CALL_BEFORE, $event, $this->webhook, $detail, $this->initiator, $this->initiatedTime);\n        $this->webhookData =  $themeResponse ?? WebhookFormatter::getDefault($event, $this->webhook, $detail, $this->initiator, $this->initiatedTime)->format();\n    }\n\n    /**\n     * Execute the job.\n     *\n     * @return void\n     */\n    public function handle(HttpRequestService $http)\n    {\n        $lastError = null;\n\n        try {\n            (new SsrUrlValidator())->ensureAllowed($this->webhook->endpoint);\n\n            $client = $http->buildClient($this->webhook->timeout, [\n                'connect_timeout' => 10,\n                'allow_redirects' => ['strict' => true],\n            ]);\n\n            $response = $client->sendRequest($http->jsonRequest('POST', $this->webhook->endpoint, $this->webhookData));\n            $statusCode = $response->getStatusCode();\n\n            if ($statusCode >= 400) {\n                $lastError = \"Response status from endpoint was {$statusCode}\";\n                Log::error(\"Webhook call to endpoint {$this->webhook->endpoint} failed with status {$statusCode}\");\n            }\n        } catch (\\Exception $error) {\n            $lastError = $error->getMessage();\n            Log::error(\"Webhook call to endpoint {$this->webhook->endpoint} failed with error \\\"{$lastError}\\\"\");\n        }\n\n        $this->webhook->last_called_at = now();\n        if ($lastError) {\n            $this->webhook->last_errored_at = now();\n            $this->webhook->last_error = $lastError;\n        }\n\n        $this->webhook->save();\n    }\n}\n"
  },
  {
    "path": "app/Activity/Models/Activity.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Models;\n\nuse BookStack\\App\\Model;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Permissions\\Models\\JointPermission;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\MorphTo;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Str;\n\n/**\n * @property string $type\n * @property User   $user\n * @property Entity $loggable\n * @property string $detail\n * @property string $loggable_type\n * @property int    $loggable_id\n * @property int    $user_id\n * @property Carbon $created_at\n */\nclass Activity extends Model\n{\n    use HasFactory;\n\n    /**\n     * Get the loggable model related to this activity.\n     * Currently only used for entities (previously entity_[id/type] columns).\n     * Could be used for others but will need an audit of uses where assumed\n     * to be entities.\n     */\n    public function loggable(): MorphTo\n    {\n        return $this->morphTo('loggable');\n    }\n\n    /**\n     * Get the user this activity relates to.\n     */\n    public function user(): BelongsTo\n    {\n        return $this->belongsTo(User::class);\n    }\n\n    public function jointPermissions(): HasMany\n    {\n        return $this->hasMany(JointPermission::class, 'entity_id', 'loggable_id')\n            ->whereColumn('activities.loggable_type', '=', 'joint_permissions.entity_type');\n    }\n\n    /**\n     * Returns text from the language files, Looks up by using the activity key.\n     */\n    public function getText(): string\n    {\n        return trans('activities.' . $this->type);\n    }\n\n    /**\n     * Check if this activity is intended to be for an entity.\n     */\n    public function isForEntity(): bool\n    {\n        return Str::startsWith($this->type, [\n            'page_', 'chapter_', 'book_', 'bookshelf_',\n        ]);\n    }\n\n    /**\n     * Checks if another Activity matches the general information of another.\n     */\n    public function isSimilarTo(self $activityB): bool\n    {\n        return [$this->type, $this->loggable_type, $this->loggable_id] === [$activityB->type, $activityB->loggable_type, $activityB->loggable_id];\n    }\n}\n"
  },
  {
    "path": "app/Activity/Models/Comment.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Models;\n\nuse BookStack\\App\\Model;\nuse BookStack\\Permissions\\Models\\JointPermission;\nuse BookStack\\Permissions\\PermissionApplicator;\nuse BookStack\\Users\\Models\\HasCreatorAndUpdater;\nuse BookStack\\Users\\Models\\OwnableInterface;\nuse BookStack\\Util\\HtmlContentFilter;\nuse BookStack\\Util\\HtmlContentFilterConfig;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\MorphTo;\n\n/**\n * @property int      $id\n * @property string   $html\n * @property int|null $parent_id  - Relates to local_id, not id\n * @property int      $local_id\n * @property string   $commentable_type\n * @property int      $commentable_id\n * @property string   $content_ref\n * @property bool     $archived\n */\nclass Comment extends Model implements Loggable, OwnableInterface\n{\n    use HasFactory;\n    use HasCreatorAndUpdater;\n\n    protected $fillable = ['parent_id'];\n    protected $hidden = ['html'];\n\n    protected $casts = [\n        'archived' => 'boolean',\n    ];\n\n    /**\n     * Get the entity that this comment belongs to.\n     */\n    public function entity(): MorphTo\n    {\n        // We specifically define null here to avoid the different name (commentable)\n        // being used by Laravel eager loading instead of the method name, which it was doing\n        // in some scenarios like when deserialized when going through the queue system.\n        // So we instead specify the type and id column names to use.\n        // Related to:\n        // https://github.com/laravel/framework/pull/24815\n        // https://github.com/laravel/framework/issues/27342\n        // https://github.com/laravel/framework/issues/47953\n        // (and probably more)\n\n        // Ultimately, we could just align the method name to 'commentable' but that would be a potential\n        // breaking change and not really worthwhile in a patch due to the risk of creating extra problems.\n        return $this->morphTo(null, 'commentable_type', 'commentable_id');\n    }\n\n    /**\n     * Get the parent comment this is in reply to (if existing).\n     * @return BelongsTo<Comment, $this>\n     */\n    public function parent(): BelongsTo\n    {\n        return $this->belongsTo(Comment::class, 'parent_id', 'local_id', 'parent')\n            ->where('commentable_type', '=', $this->commentable_type)\n            ->where('commentable_id', '=', $this->commentable_id);\n    }\n\n    /**\n     * Check if a comment has been updated since creation.\n     */\n    public function isUpdated(): bool\n    {\n        return $this->updated_at->timestamp > $this->created_at->timestamp;\n    }\n\n    public function logDescriptor(): string\n    {\n        return \"Comment #{$this->local_id} (ID: {$this->id}) for {$this->commentable_type} (ID: {$this->commentable_id})\";\n    }\n\n    public function safeHtml(): string\n    {\n        $filter = new HtmlContentFilter(new HtmlContentFilterConfig());\n        return $filter->filterString($this->html ?? '');\n    }\n\n    public function jointPermissions(): HasMany\n    {\n        return $this->hasMany(JointPermission::class, 'entity_id', 'commentable_id')\n            ->whereColumn('joint_permissions.entity_type', '=', 'comments.commentable_type');\n    }\n\n    /**\n     * Scope the query to just the comments visible to the user based upon the\n     * user visibility of what has been commented on.\n     */\n    public function scopeVisible(Builder $query): Builder\n    {\n        return app()->make(PermissionApplicator::class)\n            ->restrictEntityRelationQuery($query, 'comments', 'commentable_id', 'commentable_type');\n    }\n}\n"
  },
  {
    "path": "app/Activity/Models/Favouritable.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Relations\\MorphMany;\n\ninterface Favouritable\n{\n    /**\n     * Get the related favourite instances.\n     */\n    public function favourites(): MorphMany;\n}\n"
  },
  {
    "path": "app/Activity/Models/Favourite.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Models;\n\nuse BookStack\\App\\Model;\nuse BookStack\\Permissions\\Models\\JointPermission;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\MorphTo;\n\nclass Favourite extends Model\n{\n    use HasFactory;\n\n    protected $fillable = ['user_id'];\n\n    /**\n     * Get the related model that can be favourited.\n     */\n    public function favouritable(): MorphTo\n    {\n        return $this->morphTo();\n    }\n\n    public function jointPermissions(): HasMany\n    {\n        return $this->hasMany(JointPermission::class, 'entity_id', 'favouritable_id')\n            ->whereColumn('favourites.favouritable_type', '=', 'joint_permissions.entity_type');\n    }\n}\n"
  },
  {
    "path": "app/Activity/Models/Loggable.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Models;\n\ninterface Loggable\n{\n    /**\n     * Get the string descriptor for this item.\n     */\n    public function logDescriptor(): string;\n}\n"
  },
  {
    "path": "app/Activity/Models/MentionHistory.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Support\\Carbon;\n\n/**\n * @property int $id\n * @property string $mentionable_type\n * @property int $mentionable_id\n * @property int $from_user_id\n * @property int $to_user_id\n * @property Carbon $created_at\n * @property Carbon $updated_at\n */\nclass MentionHistory extends Model\n{\n    protected $table = 'mention_history';\n}\n"
  },
  {
    "path": "app/Activity/Models/Tag.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Models;\n\nuse BookStack\\App\\Model;\nuse BookStack\\Permissions\\Models\\JointPermission;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\MorphTo;\n\n/**\n * @property int    $id\n * @property string $name\n * @property string $value\n * @property int    $entity_id\n * @property string $entity_type\n * @property int    $order\n */\nclass Tag extends Model\n{\n    use HasFactory;\n\n    protected $fillable = ['name', 'value', 'order'];\n    protected $hidden = ['id', 'entity_id', 'entity_type', 'created_at', 'updated_at'];\n\n    /**\n     * Get the entity that this tag belongs to.\n     */\n    public function entity(): MorphTo\n    {\n        return $this->morphTo('entity');\n    }\n\n    public function jointPermissions(): HasMany\n    {\n        return $this->hasMany(JointPermission::class, 'entity_id', 'entity_id')\n            ->whereColumn('tags.entity_type', '=', 'joint_permissions.entity_type');\n    }\n\n    /**\n     * Get a full URL to start a tag name search for this tag name.\n     */\n    public function nameUrl(): string\n    {\n        return url('/search?term=%5B' . urlencode($this->name) . '%5D');\n    }\n\n    /**\n     * Get a full URL to start a tag name and value search for this tag's values.\n     */\n    public function valueUrl(): string\n    {\n        return url('/search?term=%5B' . urlencode($this->name) . '%3D' . urlencode($this->value) . '%5D');\n    }\n}\n"
  },
  {
    "path": "app/Activity/Models/View.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Models;\n\nuse BookStack\\App\\Model;\nuse BookStack\\Permissions\\Models\\JointPermission;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\MorphTo;\n\n/**\n * Class View\n * Views are stored per-item per-person within the database.\n * They can be used to find popular items or recently viewed items\n * at a per-person level. They do not record every view instance as an\n * activity. Only the latest and original view times could be recognised.\n *\n * @property int $views\n * @property int $user_id\n */\nclass View extends Model\n{\n    protected $fillable = ['user_id', 'views'];\n\n    /**\n     * Get all owning viewable models.\n     */\n    public function viewable(): MorphTo\n    {\n        return $this->morphTo();\n    }\n\n    public function jointPermissions(): HasMany\n    {\n        return $this->hasMany(JointPermission::class, 'entity_id', 'viewable_id')\n            ->whereColumn('views.viewable_type', '=', 'joint_permissions.entity_type');\n    }\n\n    /**\n     * Increment the current user's view count for the given viewable model.\n     */\n    public static function incrementFor(Viewable $viewable): int\n    {\n        $user = user();\n        if ($user->isGuest()) {\n            return 0;\n        }\n\n        /** @var View $view */\n        $view = $viewable->views()->firstOrNew([\n            'user_id' => $user->id,\n        ], ['views' => 0]);\n\n        $view->forceFill(['views' => $view->views + 1])->save();\n\n        return $view->views;\n    }\n}\n"
  },
  {
    "path": "app/Activity/Models/Viewable.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Relations\\MorphMany;\n\ninterface Viewable\n{\n    /**\n     * Get all view instances for this viewable model.\n     */\n    public function views(): MorphMany;\n}\n"
  },
  {
    "path": "app/Activity/Models/Watch.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Models;\n\nuse BookStack\\Activity\\WatchLevels;\nuse BookStack\\Permissions\\Models\\JointPermission;\nuse Carbon\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\MorphTo;\n\n/**\n * @property int $id\n * @property int $user_id\n * @property int $watchable_id\n * @property string $watchable_type\n * @property int $level\n * @property Carbon $created_at\n * @property Carbon $updated_at\n */\nclass Watch extends Model\n{\n    use HasFactory;\n\n    protected $guarded = [];\n\n    public function watchable(): MorphTo\n    {\n        return $this->morphTo();\n    }\n\n    public function jointPermissions(): HasMany\n    {\n        return $this->hasMany(JointPermission::class, 'entity_id', 'watchable_id')\n            ->whereColumn('watches.watchable_type', '=', 'joint_permissions.entity_type');\n    }\n\n    public function getLevelName(): string\n    {\n        return WatchLevels::levelValueToName($this->level);\n    }\n\n    public function ignoring(): bool\n    {\n        return $this->level === WatchLevels::IGNORE;\n    }\n}\n"
  },
  {
    "path": "app/Activity/Models/Webhook.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Models;\n\nuse BookStack\\Activity\\ActivityType;\nuse Carbon\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\n\n/**\n * @property int        $id\n * @property string     $name\n * @property string     $endpoint\n * @property Collection $trackedEvents\n * @property bool       $active\n * @property int        $timeout\n * @property string     $last_error\n * @property Carbon     $last_called_at\n * @property Carbon     $last_errored_at\n */\nclass Webhook extends Model implements Loggable\n{\n    use HasFactory;\n\n    protected $fillable = ['name', 'endpoint', 'timeout'];\n\n    protected $casts = [\n        'last_called_at'  => 'datetime',\n        'last_errored_at' => 'datetime',\n    ];\n\n    /**\n     * Define the tracked event relation a webhook.\n     */\n    public function trackedEvents(): HasMany\n    {\n        return $this->hasMany(WebhookTrackedEvent::class);\n    }\n\n    /**\n     * Update the tracked events for a webhook from the given list of event types.\n     */\n    public function updateTrackedEvents(array $events): void\n    {\n        $this->trackedEvents()->delete();\n\n        $eventsToStore = array_intersect($events, array_values(ActivityType::all()));\n        if (in_array('all', $events)) {\n            $eventsToStore = ['all'];\n        }\n\n        $trackedEvents = [];\n        foreach ($eventsToStore as $event) {\n            $trackedEvents[] = new WebhookTrackedEvent(['event' => $event]);\n        }\n\n        $this->trackedEvents()->saveMany($trackedEvents);\n    }\n\n    /**\n     * Check if this webhook tracks the given event.\n     */\n    public function tracksEvent(string $event): bool\n    {\n        return $this->trackedEvents->pluck('event')->contains($event);\n    }\n\n    /**\n     * Get a URL for this webhook within the settings interface.\n     */\n    public function getUrl(string $path = ''): string\n    {\n        return url('/settings/webhooks/' . $this->id . '/' . ltrim($path, '/'));\n    }\n\n    /**\n     * Get the string descriptor for this item.\n     */\n    public function logDescriptor(): string\n    {\n        return \"({$this->id}) {$this->name}\";\n    }\n}\n"
  },
  {
    "path": "app/Activity/Models/WebhookTrackedEvent.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Model;\n\n/**\n * @property int    $id\n * @property int    $webhook_id\n * @property string $event\n */\nclass WebhookTrackedEvent extends Model\n{\n    use HasFactory;\n\n    protected $fillable = ['event'];\n}\n"
  },
  {
    "path": "app/Activity/Notifications/Handlers/BaseNotificationHandler.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Notifications\\Handlers;\n\nuse BookStack\\Activity\\Models\\Loggable;\nuse BookStack\\Activity\\Notifications\\Messages\\BaseActivityNotification;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Permissions\\PermissionApplicator;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Support\\Facades\\Log;\n\nabstract class BaseNotificationHandler implements NotificationHandler\n{\n    /**\n     * @param class-string<BaseActivityNotification> $notification\n     * @param int[] $userIds\n     */\n    protected function sendNotificationToUserIds(string $notification, array $userIds, User $initiator, string|Loggable $detail, Entity $relatedModel): void\n    {\n        $users = User::query()->whereIn('id', array_unique($userIds))->get();\n\n        /** @var User $user */\n        foreach ($users as $user) {\n            // Prevent sending to the user that initiated the activity\n            if ($user->id === $initiator->id) {\n                continue;\n            }\n\n            // Prevent sending of the user does not have notification permissions\n            if (!$user->can(Permission::ReceiveNotifications)) {\n                continue;\n            }\n\n            // Prevent sending if the user does not have access to the related content\n            $permissions = new PermissionApplicator($user);\n            if (!$permissions->checkOwnableUserAccess($relatedModel, 'view')) {\n                continue;\n            }\n\n            // Send the notification\n            try {\n                $user->notify(new $notification($detail, $initiator));\n            } catch (\\Exception $exception) {\n                Log::error(\"Failed to send email notification to user [id:{$user->id}] with error: {$exception->getMessage()}\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/Activity/Notifications/Handlers/CommentCreationNotificationHandler.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Notifications\\Handlers;\n\nuse BookStack\\Activity\\Models\\Activity;\nuse BookStack\\Activity\\Models\\Comment;\nuse BookStack\\Activity\\Models\\Loggable;\nuse BookStack\\Activity\\Notifications\\Messages\\CommentCreationNotification;\nuse BookStack\\Activity\\Tools\\EntityWatchers;\nuse BookStack\\Activity\\WatchLevels;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Settings\\UserNotificationPreferences;\nuse BookStack\\Users\\Models\\User;\n\nclass CommentCreationNotificationHandler extends BaseNotificationHandler\n{\n    public function handle(Activity $activity, Loggable|string $detail, User $user): void\n    {\n        if (!($detail instanceof Comment)) {\n            throw new \\InvalidArgumentException(\"Detail for comment creation notifications must be a comment\");\n        }\n\n        // Main watchers\n        /** @var Page $page */\n        $page = $detail->entity;\n        $watchers = new EntityWatchers($page, WatchLevels::COMMENTS);\n        $watcherIds = $watchers->getWatcherUserIds();\n\n        // Page owner if user preferences allow\n        if ($page->owned_by && !$watchers->isUserIgnoring($page->owned_by) && $page->ownedBy) {\n            $userNotificationPrefs = new UserNotificationPreferences($page->ownedBy);\n            if ($userNotificationPrefs->notifyOnOwnPageComments()) {\n                $watcherIds[] = $page->owned_by;\n            }\n        }\n\n        // Parent comment creator if preferences allow\n        $parentComment = $detail->parent()->first();\n        if ($parentComment && $parentComment->created_by && !$watchers->isUserIgnoring($parentComment->created_by) && $parentComment->createdBy) {\n            $parentCommenterNotificationsPrefs = new UserNotificationPreferences($parentComment->createdBy);\n            if ($parentCommenterNotificationsPrefs->notifyOnCommentReplies()) {\n                $watcherIds[] = $parentComment->created_by;\n            }\n        }\n\n        $this->sendNotificationToUserIds(CommentCreationNotification::class, $watcherIds, $user, $detail, $page);\n    }\n}\n"
  },
  {
    "path": "app/Activity/Notifications/Handlers/CommentMentionNotificationHandler.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Notifications\\Handlers;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Activity\\Models\\Activity;\nuse BookStack\\Activity\\Models\\Comment;\nuse BookStack\\Activity\\Models\\Loggable;\nuse BookStack\\Activity\\Models\\MentionHistory;\nuse BookStack\\Activity\\Notifications\\Messages\\CommentMentionNotification;\nuse BookStack\\Activity\\Tools\\MentionParser;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Settings\\UserNotificationPreferences;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Carbon;\n\nclass CommentMentionNotificationHandler extends BaseNotificationHandler\n{\n    public function handle(Activity $activity, Loggable|string $detail, User $user): void\n    {\n        if (!($detail instanceof Comment) || !($detail->entity instanceof Page)) {\n            throw new \\InvalidArgumentException(\"Detail for comment mention notifications must be a comment on a page\");\n        }\n\n        /** @var Page $page */\n        $page = $detail->entity;\n\n        $parser = new MentionParser();\n        $mentionedUserIds = $parser->parseUserIdsFromHtml($detail->html);\n        $realMentionedUsers = User::whereIn('id', $mentionedUserIds)->get();\n\n        $receivingNotifications = $realMentionedUsers->filter(function (User $user) {\n            $prefs = new UserNotificationPreferences($user);\n            return $prefs->notifyOnCommentMentions();\n        });\n        $receivingNotificationsUserIds = $receivingNotifications->pluck('id')->toArray();\n\n        $userMentionsToLog = $realMentionedUsers;\n\n        // When an edit, we check our history to see if we've already notified the user about this comment before\n        // so that we can filter them out to avoid double notifications.\n        if ($activity->type === ActivityType::COMMENT_UPDATE) {\n            $previouslyNotifiedUserIds = $this->getPreviouslyNotifiedUserIds($detail);\n            $receivingNotificationsUserIds = array_values(array_diff($receivingNotificationsUserIds, $previouslyNotifiedUserIds));\n            $userMentionsToLog = $userMentionsToLog->filter(function (User $user) use ($previouslyNotifiedUserIds) {\n                return !in_array($user->id, $previouslyNotifiedUserIds);\n            });\n        }\n\n        $this->logMentions($userMentionsToLog, $detail, $user);\n        $this->sendNotificationToUserIds(CommentMentionNotification::class, $receivingNotificationsUserIds, $user, $detail, $page);\n    }\n\n    /**\n     * @param Collection<User> $mentionedUsers\n     */\n    protected function logMentions(Collection $mentionedUsers, Comment $comment, User $fromUser): void\n    {\n        $mentions = [];\n        $now = Carbon::now();\n\n        foreach ($mentionedUsers as $mentionedUser) {\n            $mentions[] = [\n                'mentionable_type' => $comment->getMorphClass(),\n                'mentionable_id' => $comment->id,\n                'from_user_id' => $fromUser->id,\n                'to_user_id' => $mentionedUser->id,\n                'created_at' => $now,\n                'updated_at' => $now,\n            ];\n        }\n\n        MentionHistory::query()->insert($mentions);\n    }\n\n    protected function getPreviouslyNotifiedUserIds(Comment $comment): array\n    {\n        return MentionHistory::query()\n            ->where('mentionable_id', $comment->id)\n            ->where('mentionable_type', $comment->getMorphClass())\n            ->pluck('to_user_id')\n            ->toArray();\n    }\n}\n"
  },
  {
    "path": "app/Activity/Notifications/Handlers/NotificationHandler.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Notifications\\Handlers;\n\nuse BookStack\\Activity\\Models\\Activity;\nuse BookStack\\Activity\\Models\\Loggable;\nuse BookStack\\Users\\Models\\User;\n\ninterface NotificationHandler\n{\n    /**\n     * Run this handler.\n     * Provides the activity, related activity detail/model\n     * along with the user that triggered the activity.\n     */\n    public function handle(Activity $activity, string|Loggable $detail, User $user): void;\n}\n"
  },
  {
    "path": "app/Activity/Notifications/Handlers/PageCreationNotificationHandler.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Notifications\\Handlers;\n\nuse BookStack\\Activity\\Models\\Activity;\nuse BookStack\\Activity\\Models\\Loggable;\nuse BookStack\\Activity\\Notifications\\Messages\\PageCreationNotification;\nuse BookStack\\Activity\\Tools\\EntityWatchers;\nuse BookStack\\Activity\\WatchLevels;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Users\\Models\\User;\n\nclass PageCreationNotificationHandler extends BaseNotificationHandler\n{\n    public function handle(Activity $activity, Loggable|string $detail, User $user): void\n    {\n        if (!($detail instanceof Page)) {\n            throw new \\InvalidArgumentException(\"Detail for page create notifications must be a page\");\n        }\n\n        $watchers = new EntityWatchers($detail, WatchLevels::NEW);\n        $this->sendNotificationToUserIds(PageCreationNotification::class, $watchers->getWatcherUserIds(), $user, $detail, $detail);\n    }\n}\n"
  },
  {
    "path": "app/Activity/Notifications/Handlers/PageUpdateNotificationHandler.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Notifications\\Handlers;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Activity\\Models\\Activity;\nuse BookStack\\Activity\\Models\\Loggable;\nuse BookStack\\Activity\\Notifications\\Messages\\PageUpdateNotification;\nuse BookStack\\Activity\\Tools\\EntityWatchers;\nuse BookStack\\Activity\\WatchLevels;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Settings\\UserNotificationPreferences;\nuse BookStack\\Users\\Models\\User;\n\nclass PageUpdateNotificationHandler extends BaseNotificationHandler\n{\n    public function handle(Activity $activity, Loggable|string $detail, User $user): void\n    {\n        if (!($detail instanceof Page)) {\n            throw new \\InvalidArgumentException(\"Detail for page update notifications must be a page\");\n        }\n\n        // Get the last update from activity\n        /** @var ?Activity $lastUpdate */\n        $lastUpdate = $detail->activity()\n            ->where('type', '=', ActivityType::PAGE_UPDATE)\n            ->where('id', '!=', $activity->id)\n            ->latest('created_at')\n            ->first();\n\n        // Return if the same user has already updated the page in the last 15 mins\n        if ($lastUpdate && $lastUpdate->user_id === $user->id) {\n            if ($lastUpdate->created_at->gt(now()->subMinutes(15))) {\n                return;\n            }\n        }\n\n        // Get active watchers\n        $watchers = new EntityWatchers($detail, WatchLevels::UPDATES);\n        $watcherIds = $watchers->getWatcherUserIds();\n\n        // Add the page owner if preferences allow\n        if ($detail->owned_by && !$watchers->isUserIgnoring($detail->owned_by) && $detail->ownedBy) {\n            $userNotificationPrefs = new UserNotificationPreferences($detail->ownedBy);\n            if ($userNotificationPrefs->notifyOnOwnPageChanges()) {\n                $watcherIds[] = $detail->owned_by;\n            }\n        }\n\n        $this->sendNotificationToUserIds(PageUpdateNotification::class, $watcherIds, $user, $detail, $detail);\n    }\n}\n"
  },
  {
    "path": "app/Activity/Notifications/MessageParts/EntityLinkMessageLine.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Notifications\\MessageParts;\n\nuse BookStack\\Entities\\Models\\Entity;\nuse Illuminate\\Contracts\\Support\\Htmlable;\nuse Stringable;\n\n/**\n * A link to a specific entity in the system, with the text showing its name.\n */\nclass EntityLinkMessageLine implements Htmlable, Stringable\n{\n    public function __construct(\n        protected Entity $entity,\n        protected int $nameLength = 120,\n    ) {\n    }\n\n    public function toHtml(): string\n    {\n        return '<a href=\"' . e($this->entity->getUrl()) . '\">' . e($this->entity->getShortName($this->nameLength)) . '</a>';\n    }\n\n    public function __toString(): string\n    {\n        return \"{$this->entity->getShortName($this->nameLength)} ({$this->entity->getUrl()})\";\n    }\n}\n"
  },
  {
    "path": "app/Activity/Notifications/MessageParts/EntityPathMessageLine.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Notifications\\MessageParts;\n\nuse BookStack\\Entities\\Models\\Entity;\nuse Illuminate\\Contracts\\Support\\Htmlable;\nuse Stringable;\n\n/**\n * A link to a specific entity in the system, with the text showing its name.\n */\nclass EntityPathMessageLine implements Htmlable, Stringable\n{\n    /**\n     * @var EntityLinkMessageLine[]\n     */\n    protected array $entityLinks;\n\n    public function __construct(\n        protected array $entities\n    ) {\n        $this->entityLinks = array_map(fn (Entity $entity) => new EntityLinkMessageLine($entity, 24), $this->entities);\n    }\n\n    public function toHtml(): string\n    {\n        $entityHtmls = array_map(fn (EntityLinkMessageLine $line) => $line->toHtml(), $this->entityLinks);\n        return implode(' &gt; ', $entityHtmls);\n    }\n\n    public function __toString(): string\n    {\n        return implode(' > ', $this->entityLinks);\n    }\n}\n"
  },
  {
    "path": "app/Activity/Notifications/MessageParts/LinkedMailMessageLine.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Notifications\\MessageParts;\n\nuse Illuminate\\Contracts\\Support\\Htmlable;\nuse Stringable;\n\n/**\n * A line of text with linked text included, intended for use\n * in MailMessages. The line should have a ':link' placeholder for\n * where the link should be inserted within the line.\n */\nclass LinkedMailMessageLine implements Htmlable, Stringable\n{\n    public function __construct(\n        protected string $url,\n        protected string $line,\n        protected string $linkText,\n    ) {\n    }\n\n    public function toHtml(): string\n    {\n        $link = '<a href=\"' . e($this->url) . '\">' . e($this->linkText) . '</a>';\n        return str_replace(':link', $link, e($this->line));\n    }\n\n    public function __toString(): string\n    {\n        $link = \"{$this->linkText} ({$this->url})\";\n        return str_replace(':link', $link, $this->line);\n    }\n}\n"
  },
  {
    "path": "app/Activity/Notifications/MessageParts/ListMessageLine.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Notifications\\MessageParts;\n\nuse Illuminate\\Contracts\\Support\\Htmlable;\nuse Stringable;\n\n/**\n * A bullet point list of content, where the keys of the given list array\n * are bolded header elements, and the values follow.\n */\nclass ListMessageLine implements Htmlable, Stringable\n{\n    public function __construct(\n        protected array $list\n    ) {\n    }\n\n    public function toHtml(): string\n    {\n        $list = [];\n        foreach ($this->list as $header => $content) {\n            $list[] = '<strong>' . e($header) . '</strong> ' . e($content);\n        }\n        return implode(\"<br>\\n\", $list);\n    }\n\n    public function __toString(): string\n    {\n        $list = [];\n        foreach ($this->list as $header => $content) {\n            $list[] = $header . ' ' . $content;\n        }\n        return implode(\"\\n\", $list);\n    }\n}\n"
  },
  {
    "path": "app/Activity/Notifications/Messages/BaseActivityNotification.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Notifications\\Messages;\n\nuse BookStack\\Activity\\Models\\Loggable;\nuse BookStack\\Activity\\Notifications\\MessageParts\\EntityPathMessageLine;\nuse BookStack\\Activity\\Notifications\\MessageParts\\LinkedMailMessageLine;\nuse BookStack\\App\\MailNotification;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Permissions\\PermissionApplicator;\nuse BookStack\\Translation\\LocaleDefinition;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Bus\\Queueable;\n\nabstract class BaseActivityNotification extends MailNotification\n{\n    use Queueable;\n\n    public function __construct(\n        protected Loggable|string $detail,\n        protected User $user,\n    ) {\n    }\n\n    /**\n     * Get the array representation of the notification.\n     *\n     * @param  mixed  $notifiable\n     * @return array\n     */\n    public function toArray($notifiable)\n    {\n        return [\n            'activity_detail' => $this->detail,\n            'activity_creator' => $this->user,\n        ];\n    }\n\n    /**\n     * Build the common reason footer line used in mail messages.\n     */\n    protected function buildReasonFooterLine(LocaleDefinition $locale): LinkedMailMessageLine\n    {\n        return new LinkedMailMessageLine(\n            url('/my-account/notifications'),\n            $locale->trans('notifications.footer_reason'),\n            $locale->trans('notifications.footer_reason_link'),\n        );\n    }\n\n    /**\n     * Build a line which provides the book > chapter path to a page.\n     * Takes into account visibility of these parent items.\n     * Returns null if no path items can be used.\n     */\n    protected function buildPagePathLine(Page $page, User $notifiable): ?EntityPathMessageLine\n    {\n        $permissions = new PermissionApplicator($notifiable);\n\n        $path = array_filter([$page->book, $page->chapter], function (?Entity $entity) use ($permissions) {\n            return !is_null($entity) && $permissions->checkOwnableUserAccess($entity, 'view');\n        });\n\n        return empty($path) ? null : new EntityPathMessageLine($path);\n    }\n}\n"
  },
  {
    "path": "app/Activity/Notifications/Messages/CommentCreationNotification.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Notifications\\Messages;\n\nuse BookStack\\Activity\\Models\\Comment;\nuse BookStack\\Activity\\Notifications\\MessageParts\\EntityLinkMessageLine;\nuse BookStack\\Activity\\Notifications\\MessageParts\\ListMessageLine;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Notifications\\Messages\\MailMessage;\n\nclass CommentCreationNotification extends BaseActivityNotification\n{\n    public function toMail(User $notifiable): MailMessage\n    {\n        /** @var Comment $comment */\n        $comment = $this->detail;\n        /** @var Page $page */\n        $page = $comment->entity;\n\n        $locale = $notifiable->getLocale();\n\n        $listLines = array_filter([\n            $locale->trans('notifications.detail_page_name') => new EntityLinkMessageLine($page),\n            $locale->trans('notifications.detail_page_path') => $this->buildPagePathLine($page, $notifiable),\n            $locale->trans('notifications.detail_commenter') => $this->user->name,\n            $locale->trans('notifications.detail_comment') => strip_tags($comment->html),\n        ]);\n\n        return $this->newMailMessage($locale)\n            ->subject($locale->trans('notifications.new_comment_subject', ['pageName' => $page->getShortName()]))\n            ->line($locale->trans('notifications.new_comment_intro', ['appName' => setting('app-name')]))\n            ->line(new ListMessageLine($listLines))\n            ->action($locale->trans('notifications.action_view_comment'), $page->getUrl('#comment' . $comment->local_id))\n            ->line($this->buildReasonFooterLine($locale));\n    }\n}\n"
  },
  {
    "path": "app/Activity/Notifications/Messages/CommentMentionNotification.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Notifications\\Messages;\n\nuse BookStack\\Activity\\Models\\Comment;\nuse BookStack\\Activity\\Notifications\\MessageParts\\EntityLinkMessageLine;\nuse BookStack\\Activity\\Notifications\\MessageParts\\ListMessageLine;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Notifications\\Messages\\MailMessage;\n\nclass CommentMentionNotification extends BaseActivityNotification\n{\n    public function toMail(User $notifiable): MailMessage\n    {\n        /** @var Comment $comment */\n        $comment = $this->detail;\n        /** @var Page $page */\n        $page = $comment->entity;\n\n        $locale = $notifiable->getLocale();\n\n        $listLines = array_filter([\n            $locale->trans('notifications.detail_page_name') => new EntityLinkMessageLine($page),\n            $locale->trans('notifications.detail_page_path') => $this->buildPagePathLine($page, $notifiable),\n            $locale->trans('notifications.detail_commenter') => $this->user->name,\n            $locale->trans('notifications.detail_comment') => strip_tags($comment->html),\n        ]);\n\n        return $this->newMailMessage($locale)\n            ->subject($locale->trans('notifications.comment_mention_subject', ['pageName' => $page->getShortName()]))\n            ->line($locale->trans('notifications.comment_mention_intro', ['appName' => setting('app-name')]))\n            ->line(new ListMessageLine($listLines))\n            ->action($locale->trans('notifications.action_view_comment'), $page->getUrl('#comment' . $comment->local_id))\n            ->line($this->buildReasonFooterLine($locale));\n    }\n}\n"
  },
  {
    "path": "app/Activity/Notifications/Messages/PageCreationNotification.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Notifications\\Messages;\n\nuse BookStack\\Activity\\Notifications\\MessageParts\\EntityLinkMessageLine;\nuse BookStack\\Activity\\Notifications\\MessageParts\\ListMessageLine;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Notifications\\Messages\\MailMessage;\n\nclass PageCreationNotification extends BaseActivityNotification\n{\n    public function toMail(User $notifiable): MailMessage\n    {\n        /** @var Page $page */\n        $page = $this->detail;\n\n        $locale = $notifiable->getLocale();\n\n        $listLines = array_filter([\n            $locale->trans('notifications.detail_page_name') => new EntityLinkMessageLine($page),\n            $locale->trans('notifications.detail_page_path') => $this->buildPagePathLine($page, $notifiable),\n            $locale->trans('notifications.detail_created_by') => $this->user->name,\n        ]);\n\n        return $this->newMailMessage($locale)\n            ->subject($locale->trans('notifications.new_page_subject', ['pageName' => $page->getShortName()]))\n            ->line($locale->trans('notifications.new_page_intro', ['appName' => setting('app-name')]))\n            ->line(new ListMessageLine($listLines))\n            ->action($locale->trans('notifications.action_view_page'), $page->getUrl())\n            ->line($this->buildReasonFooterLine($locale));\n    }\n}\n"
  },
  {
    "path": "app/Activity/Notifications/Messages/PageUpdateNotification.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Notifications\\Messages;\n\nuse BookStack\\Activity\\Notifications\\MessageParts\\EntityLinkMessageLine;\nuse BookStack\\Activity\\Notifications\\MessageParts\\ListMessageLine;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Notifications\\Messages\\MailMessage;\n\nclass PageUpdateNotification extends BaseActivityNotification\n{\n    public function toMail(User $notifiable): MailMessage\n    {\n        /** @var Page $page */\n        $page = $this->detail;\n\n        $locale = $notifiable->getLocale();\n\n        $listLines = array_filter([\n            $locale->trans('notifications.detail_page_name') => new EntityLinkMessageLine($page),\n            $locale->trans('notifications.detail_page_path') => $this->buildPagePathLine($page, $notifiable),\n            $locale->trans('notifications.detail_updated_by') => $this->user->name,\n        ]);\n\n        return $this->newMailMessage($locale)\n            ->subject($locale->trans('notifications.updated_page_subject', ['pageName' => $page->getShortName()]))\n            ->line($locale->trans('notifications.updated_page_intro', ['appName' => setting('app-name')]))\n            ->line(new ListMessageLine($listLines))\n            ->line($locale->trans('notifications.updated_page_debounce'))\n            ->action($locale->trans('notifications.action_view_page'), $page->getUrl())\n            ->line($this->buildReasonFooterLine($locale));\n    }\n}\n"
  },
  {
    "path": "app/Activity/Notifications/NotificationManager.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Notifications;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Activity\\Models\\Activity;\nuse BookStack\\Activity\\Models\\Loggable;\nuse BookStack\\Activity\\Notifications\\Handlers\\CommentCreationNotificationHandler;\nuse BookStack\\Activity\\Notifications\\Handlers\\CommentMentionNotificationHandler;\nuse BookStack\\Activity\\Notifications\\Handlers\\NotificationHandler;\nuse BookStack\\Activity\\Notifications\\Handlers\\PageCreationNotificationHandler;\nuse BookStack\\Activity\\Notifications\\Handlers\\PageUpdateNotificationHandler;\nuse BookStack\\Users\\Models\\User;\n\nclass NotificationManager\n{\n    /**\n     * @var class-string<NotificationHandler>[]\n     */\n    protected array $handlers = [];\n\n    public function handle(Activity $activity, string|Loggable $detail, User $user): void\n    {\n        $activityType = $activity->type;\n        $handlersToRun = $this->handlers[$activityType] ?? [];\n        foreach ($handlersToRun as $handlerClass) {\n            /** @var NotificationHandler $handler */\n            $handler = new $handlerClass();\n            $handler->handle($activity, $detail, $user);\n        }\n    }\n\n    /**\n     * @param class-string<NotificationHandler> $handlerClass\n     */\n    public function registerHandler(string $activityType, string $handlerClass): void\n    {\n        if (!isset($this->handlers[$activityType])) {\n            $this->handlers[$activityType] = [];\n        }\n\n        if (!in_array($handlerClass, $this->handlers[$activityType])) {\n            $this->handlers[$activityType][] = $handlerClass;\n        }\n    }\n\n    public function loadDefaultHandlers(): void\n    {\n        $this->registerHandler(ActivityType::PAGE_CREATE, PageCreationNotificationHandler::class);\n        $this->registerHandler(ActivityType::PAGE_UPDATE, PageUpdateNotificationHandler::class);\n        $this->registerHandler(ActivityType::COMMENT_CREATE, CommentCreationNotificationHandler::class);\n        $this->registerHandler(ActivityType::COMMENT_CREATE, CommentMentionNotificationHandler::class);\n        $this->registerHandler(ActivityType::COMMENT_UPDATE, CommentMentionNotificationHandler::class);\n    }\n}\n"
  },
  {
    "path": "app/Activity/Queries/WebhooksAllPaginatedAndSorted.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Queries;\n\nuse BookStack\\Activity\\Models\\Webhook;\nuse BookStack\\Util\\SimpleListOptions;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\n\n/**\n * Get all the webhooks in the system in a paginated format.\n */\nclass WebhooksAllPaginatedAndSorted\n{\n    public function run(int $count, SimpleListOptions $listOptions): LengthAwarePaginator\n    {\n        $query = Webhook::query()->select(['*'])\n            ->withCount(['trackedEvents'])\n            ->orderBy($listOptions->getSort(), $listOptions->getOrder());\n\n        if ($listOptions->getSearch()) {\n            $term = '%' . $listOptions->getSearch() . '%';\n            $query->where(function ($query) use ($term) {\n                $query->where('name', 'like', $term)\n                    ->orWhere('endpoint', 'like', $term);\n            });\n        }\n\n        return $query->paginate($count);\n    }\n}\n"
  },
  {
    "path": "app/Activity/TagRepo.php",
    "content": "<?php\n\nnamespace BookStack\\Activity;\n\nuse BookStack\\Activity\\Models\\Tag;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Permissions\\PermissionApplicator;\nuse BookStack\\Util\\SimpleListOptions;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\n\nclass TagRepo\n{\n    public function __construct(\n        protected PermissionApplicator $permissions\n    ) {\n    }\n\n    /**\n     * Start a query against all tags in the system.\n     */\n    public function queryWithTotals(SimpleListOptions $listOptions, string $nameFilter): Builder\n    {\n        $searchTerm = $listOptions->getSearch();\n        $sort = $listOptions->getSort();\n        if ($sort === 'name' && $nameFilter) {\n            $sort = 'value';\n        }\n\n        $query = Tag::query()\n            ->select([\n                'name',\n                ($searchTerm || $nameFilter) ? 'value' : DB::raw('COUNT(distinct value) as `values`'),\n                DB::raw('COUNT(id) as usages'),\n                DB::raw('SUM(IF(entity_type = \\'page\\', 1, 0)) as page_count'),\n                DB::raw('SUM(IF(entity_type = \\'chapter\\', 1, 0)) as chapter_count'),\n                DB::raw('SUM(IF(entity_type = \\'book\\', 1, 0)) as book_count'),\n                DB::raw('SUM(IF(entity_type = \\'bookshelf\\', 1, 0)) as shelf_count'),\n            ])\n            ->orderBy($sort, $listOptions->getOrder())\n            ->whereHas('entity');\n\n        if ($nameFilter) {\n            $query->where('name', '=', $nameFilter);\n            $query->groupBy('value');\n        } elseif ($searchTerm) {\n            $query->groupBy('name', 'value');\n        } else {\n            $query->groupBy('name');\n        }\n\n        if ($searchTerm) {\n            $query->where(function (Builder $query) use ($searchTerm) {\n                $query->where('name', 'like', '%' . $searchTerm . '%')\n                    ->orWhere('value', 'like', '%' . $searchTerm . '%');\n            });\n        }\n\n        return $this->permissions->restrictEntityRelationQuery($query, 'tags', 'entity_id', 'entity_type');\n    }\n\n    /**\n     * Get tag name suggestions from scanning existing tag names.\n     * If no search term is given the 50 most popular tag names are provided.\n     */\n    public function getNameSuggestions(string $searchTerm): Collection\n    {\n        $query = Tag::query()\n            ->select('*', DB::raw('count(*) as count'))\n            ->groupBy('name');\n\n        if ($searchTerm) {\n            $query = $query->where('name', 'LIKE', $searchTerm . '%')->orderBy('name', 'asc');\n        } else {\n            $query = $query->orderBy('count', 'desc')->take(50);\n        }\n\n        $query = $this->permissions->restrictEntityRelationQuery($query, 'tags', 'entity_id', 'entity_type');\n\n        return $query->pluck('name');\n    }\n\n    /**\n     * Get tag value suggestions from scanning existing tag values.\n     * If no search is given the 50 most popular values are provided.\n     * Passing a tagName will only find values for a tags with a particular name.\n     */\n    public function getValueSuggestions(string $searchTerm, string $tagName): Collection\n    {\n        $query = Tag::query()\n            ->select('*', DB::raw('count(*) as count'))\n            ->where('value', '!=', '')\n            ->groupBy('value');\n\n        if ($searchTerm) {\n            $query = $query->where('value', 'LIKE', $searchTerm . '%')->orderBy('value', 'desc');\n        } else {\n            $query = $query->orderBy('count', 'desc')->take(50);\n        }\n\n        if ($tagName) {\n            $query = $query->where('name', '=', $tagName);\n        }\n\n        $query = $this->permissions->restrictEntityRelationQuery($query, 'tags', 'entity_id', 'entity_type');\n\n        return $query->pluck('value');\n    }\n\n    /**\n     * Save an array of tags to an entity.\n     */\n    public function saveTagsToEntity(Entity $entity, array $tags = []): iterable\n    {\n        $entity->tags()->delete();\n\n        $newTags = collect($tags)->filter(function ($tag) {\n            return boolval(trim($tag['name']));\n        })->map(function ($tag) {\n            return $this->newInstanceFromInput($tag);\n        })->all();\n\n        return $entity->tags()->saveMany($newTags);\n    }\n\n    /**\n     * Create a new Tag instance from user input.\n     * Input must be an array with a 'name' and an optional 'value' key.\n     */\n    protected function newInstanceFromInput(array $input): Tag\n    {\n        return new Tag([\n            'name'  => trim($input['name']),\n            'value' => trim($input['value'] ?? ''),\n        ]);\n    }\n}\n"
  },
  {
    "path": "app/Activity/Tools/ActivityLogger.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Tools;\n\nuse BookStack\\Activity\\DispatchWebhookJob;\nuse BookStack\\Activity\\Models\\Activity;\nuse BookStack\\Activity\\Models\\Loggable;\nuse BookStack\\Activity\\Models\\Webhook;\nuse BookStack\\Activity\\Notifications\\NotificationManager;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Facades\\Theme;\nuse BookStack\\Theming\\ThemeEvents;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass ActivityLogger\n{\n    public function __construct(\n        protected NotificationManager $notifications\n    ) {\n        $this->notifications->loadDefaultHandlers();\n    }\n\n    /**\n     * Add a generic activity event to the database.\n     */\n    public function add(string $type, string|Loggable $detail = ''): void\n    {\n        $detailToStore = ($detail instanceof Loggable) ? $detail->logDescriptor() : $detail;\n\n        $activity = $this->newActivityForUser($type);\n        $activity->detail = $detailToStore;\n\n        if ($detail instanceof Entity) {\n            $activity->loggable_id = $detail->id;\n            $activity->loggable_type = $detail->getMorphClass();\n        }\n\n        $activity->save();\n\n        $this->setNotification($type);\n        $this->dispatchWebhooks($type, $detail);\n        $this->notifications->handle($activity, $detail, user());\n        Theme::dispatch(ThemeEvents::ACTIVITY_LOGGED, $type, $detail);\n    }\n\n    /**\n     * Get a new activity instance for the current user.\n     */\n    protected function newActivityForUser(string $type): Activity\n    {\n        return (new Activity())->forceFill([\n            'type'     => strtolower($type),\n            'user_id'  => user()->id,\n            'ip'       => IpFormatter::fromCurrentRequest()->format(),\n        ]);\n    }\n\n    /**\n     * Removes the entity attachment from each of its activities\n     * and instead uses the 'extra' field with the entities name.\n     * Used when an entity is deleted.\n     */\n    public function removeEntity(Entity $entity): void\n    {\n        $entity->activity()->update([\n            'detail'         => $entity->name,\n            'loggable_id'    => null,\n            'loggable_type'  => null,\n        ]);\n    }\n\n    /**\n     * Flashes a notification message to the session if an appropriate message is available.\n     */\n    protected function setNotification(string $type): void\n    {\n        $notificationTextKey = 'activities.' . $type . '_notification';\n        if (trans()->has($notificationTextKey)) {\n            $message = trans($notificationTextKey);\n            session()->flash('success', $message);\n        }\n    }\n\n    protected function dispatchWebhooks(string $type, string|Loggable $detail): void\n    {\n        $webhooks = Webhook::query()\n            ->whereHas('trackedEvents', function (Builder $query) use ($type) {\n                $query->where('event', '=', $type)\n                    ->orWhere('event', '=', 'all');\n            })\n            ->where('active', '=', true)\n            ->get();\n\n        foreach ($webhooks as $webhook) {\n            dispatch(new DispatchWebhookJob($webhook, $type, $detail));\n        }\n    }\n\n    /**\n     * Log out a failed login attempt, Providing the given username\n     * as part of the message if the '%u' string is used.\n     */\n    public function logFailedLogin(string $username): void\n    {\n        $message = config('logging.failed_login.message');\n        if (!$message) {\n            return;\n        }\n\n        $message = str_replace('%u', $username, $message);\n        $channel = config('logging.failed_login.channel');\n        Log::channel($channel)->warning($message);\n    }\n}\n"
  },
  {
    "path": "app/Activity/Tools/CommentTree.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Tools;\n\nuse BookStack\\Activity\\Models\\Comment;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Permissions\\Permission;\n\nclass CommentTree\n{\n    /**\n     * The built nested tree structure array.\n     * @var CommentTreeNode[]\n     */\n    protected array $tree;\n\n    /**\n     * A linear array of loaded comments.\n     * @var Comment[]\n     */\n    protected array $comments;\n\n    public function __construct(\n        protected Page $page\n    ) {\n        $this->comments = $this->loadComments();\n        $this->tree = $this->createTree($this->comments);\n    }\n\n    public function enabled(): bool\n    {\n        return !setting('app-disable-comments');\n    }\n\n    public function empty(): bool\n    {\n        return count($this->getActive()) === 0;\n    }\n\n    public function count(): int\n    {\n        return count($this->comments);\n    }\n\n    public function getActive(): array\n    {\n        return array_values(array_filter($this->tree, fn (CommentTreeNode $node) => !$node->comment->archived));\n    }\n\n    public function activeThreadCount(): int\n    {\n        return count($this->getActive());\n    }\n\n    public function getArchived(): array\n    {\n        return array_values(array_filter($this->tree, fn (CommentTreeNode $node) => $node->comment->archived));\n    }\n\n    public function archivedThreadCount(): int\n    {\n        return count($this->getArchived());\n    }\n\n    public function getCommentNodeForId(int $commentId): ?CommentTreeNode\n    {\n        foreach ($this->tree as $node) {\n            if ($node->comment->id === $commentId) {\n                return $node;\n            }\n        }\n\n        return null;\n    }\n\n    public function canUpdateAny(): bool\n    {\n        foreach ($this->comments as $comment) {\n            if (userCan(Permission::CommentUpdate, $comment)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    public function loadVisibleHtml(): void\n    {\n        foreach ($this->comments as $comment) {\n            $comment->setAttribute('html', $comment->safeHtml());\n            $comment->makeVisible('html');\n        }\n    }\n\n    /**\n     * @param Comment[] $comments\n     * @return CommentTreeNode[]\n     */\n    protected function createTree(array $comments): array\n    {\n        $byId = [];\n        foreach ($comments as $comment) {\n            $byId[$comment->local_id] = $comment;\n        }\n\n        $childMap = [];\n        foreach ($comments as $comment) {\n            $parent = $comment->parent_id;\n            if (is_null($parent) || !isset($byId[$parent])) {\n                $parent = 0;\n            }\n\n            if (!isset($childMap[$parent])) {\n                $childMap[$parent] = [];\n            }\n            $childMap[$parent][] = $comment->local_id;\n        }\n\n        $tree = [];\n        foreach ($childMap[0] ?? [] as $childId) {\n            $tree[] = $this->createTreeNodeForId($childId, 0, $byId, $childMap);\n        }\n\n        return $tree;\n    }\n\n    protected function createTreeNodeForId(int $id, int $depth, array &$byId, array &$childMap): CommentTreeNode\n    {\n        $childIds = $childMap[$id] ?? [];\n        $children = [];\n\n        foreach ($childIds as $childId) {\n            $children[] = $this->createTreeNodeForId($childId, $depth + 1, $byId, $childMap);\n        }\n\n        return new CommentTreeNode($byId[$id], $depth, $children);\n    }\n\n    /**\n     * @return Comment[]\n     */\n    protected function loadComments(): array\n    {\n        if (!$this->enabled()) {\n            return [];\n        }\n\n        return $this->page->comments()\n            ->with('createdBy')\n            ->get()\n            ->all();\n    }\n}\n"
  },
  {
    "path": "app/Activity/Tools/CommentTreeNode.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Tools;\n\nuse BookStack\\Activity\\Models\\Comment;\n\nclass CommentTreeNode\n{\n    public Comment $comment;\n    public int $depth;\n\n    /**\n     * @var CommentTreeNode[]\n     */\n    public array $children;\n\n    public function __construct(Comment $comment, int $depth, array $children)\n    {\n        $this->comment = $comment;\n        $this->depth = $depth;\n        $this->children = $children;\n    }\n}\n"
  },
  {
    "path": "app/Activity/Tools/EntityWatchers.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Tools;\n\nuse BookStack\\Activity\\Models\\Watch;\nuse BookStack\\Entities\\Models\\BookChild;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse Illuminate\\Database\\Eloquent\\Builder;\n\nclass EntityWatchers\n{\n    /**\n     * @var int[]\n     */\n    protected array $watchers = [];\n\n    /**\n     * @var int[]\n     */\n    protected array $ignorers = [];\n\n    public function __construct(\n        protected Entity $entity,\n        protected int $watchLevel,\n    ) {\n        $this->build();\n    }\n\n    public function getWatcherUserIds(): array\n    {\n        return $this->watchers;\n    }\n\n    public function isUserIgnoring(int $userId): bool\n    {\n        return in_array($userId, $this->ignorers);\n    }\n\n    protected function build(): void\n    {\n        $watches = $this->getRelevantWatches();\n\n        // Sort before de-duping, so that the order looped below follows book -> chapter -> page ordering\n        usort($watches, function (Watch $watchA, Watch $watchB) {\n            $entityTypeDiff = $watchA->watchable_type <=> $watchB->watchable_type;\n            return $entityTypeDiff === 0 ? ($watchA->user_id <=> $watchB->user_id) : $entityTypeDiff;\n        });\n\n        // De-dupe by user id to get their most relevant level\n        $levelByUserId = [];\n        foreach ($watches as $watch) {\n            $levelByUserId[$watch->user_id] = $watch->level;\n        }\n\n        // Populate the class arrays\n        $this->watchers = array_keys(array_filter($levelByUserId, fn(int $level) => $level >= $this->watchLevel));\n        $this->ignorers = array_keys(array_filter($levelByUserId, fn(int $level) => $level === 0));\n    }\n\n    /**\n     * @return Watch[]\n     */\n    protected function getRelevantWatches(): array\n    {\n        /** @var Entity[] $entitiesInvolved */\n        $entitiesInvolved = array_filter([\n            $this->entity,\n            $this->entity instanceof BookChild ? $this->entity->book : null,\n            $this->entity instanceof Page ? $this->entity->chapter : null,\n        ]);\n\n        $query = Watch::query()->where(function (Builder $query) use ($entitiesInvolved) {\n            foreach ($entitiesInvolved as $entity) {\n                $query->orWhere(function (Builder $query) use ($entity) {\n                    $query->where('watchable_type', '=', $entity->getMorphClass())\n                        ->where('watchable_id', '=', $entity->id);\n                });\n            }\n        });\n\n        return $query->get([\n            'level', 'watchable_id', 'watchable_type', 'user_id'\n        ])->all();\n    }\n}\n"
  },
  {
    "path": "app/Activity/Tools/IpFormatter.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Tools;\n\nclass IpFormatter\n{\n    protected string $ip;\n    protected int $precision;\n\n    public function __construct(string $ip, int $precision)\n    {\n        $this->ip = trim($ip);\n        $this->precision = max(0, min($precision, 4));\n    }\n\n    public function format(): string\n    {\n        if (empty($this->ip) || $this->precision === 4) {\n            return $this->ip;\n        }\n\n        return $this->isIpv6() ? $this->maskIpv6() : $this->maskIpv4();\n    }\n\n    protected function maskIpv4(): string\n    {\n        $exploded = $this->explodeAndExpandIp('.', 4);\n        $maskGroupCount = min(4 - $this->precision, count($exploded));\n\n        for ($i = 0; $i < $maskGroupCount; $i++) {\n            $exploded[3 - $i] = 'x';\n        }\n\n        return implode('.', $exploded);\n    }\n\n    protected function maskIpv6(): string\n    {\n        $exploded = $this->explodeAndExpandIp(':', 8);\n        $maskGroupCount = min(8 - ($this->precision * 2), count($exploded));\n\n        for ($i = 0; $i < $maskGroupCount; $i++) {\n            $exploded[7 - $i] = 'x';\n        }\n\n        return implode(':', $exploded);\n    }\n\n    protected function isIpv6(): bool\n    {\n        return strpos($this->ip, ':') !== false;\n    }\n\n    protected function explodeAndExpandIp(string $separator, int $targetLength): array\n    {\n        $exploded = explode($separator, $this->ip);\n\n        while (count($exploded) < $targetLength) {\n            $emptyIndex = array_search('', $exploded) ?: count($exploded) - 1;\n            array_splice($exploded, $emptyIndex, 0, '0');\n        }\n\n        $emptyIndex = array_search('', $exploded);\n        if ($emptyIndex !== false) {\n            $exploded[$emptyIndex] = '0';\n        }\n\n        return $exploded;\n    }\n\n    public static function fromCurrentRequest(): self\n    {\n        $ip = request()->ip() ?? '';\n\n        if (config('app.env') === 'demo') {\n            $ip = '127.0.0.1';\n        }\n\n        return new self($ip, config('app.ip_address_precision'));\n    }\n}\n"
  },
  {
    "path": "app/Activity/Tools/MentionParser.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Tools;\n\nuse BookStack\\Util\\HtmlDocument;\nuse DOMElement;\n\nclass MentionParser\n{\n    public function parseUserIdsFromHtml(string $html): array\n    {\n        $doc = new HtmlDocument($html);\n\n        $ids = [];\n        $mentionLinks = $doc->queryXPath('//a[@data-mention-user-id]');\n\n        foreach ($mentionLinks as $link) {\n            if ($link instanceof DOMElement) {\n                $id = intval($link->getAttribute('data-mention-user-id'));\n                if ($id > 0) {\n                    $ids[] = $id;\n                }\n            }\n        }\n\n        return array_values(array_unique($ids));\n    }\n}\n"
  },
  {
    "path": "app/Activity/Tools/TagClassGenerator.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Tools;\n\nuse BookStack\\Activity\\Models\\Tag;\nuse BookStack\\Entities\\Models\\BookChild;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Permissions\\Permission;\n\nclass TagClassGenerator\n{\n    public function __construct(\n        protected Entity $entity\n    ) {\n    }\n\n    /**\n     * @return string[]\n     */\n    public function generate(): array\n    {\n        $classes = [];\n        $tags = $this->entity->tags->all();\n\n        foreach ($tags as $tag) {\n             array_push($classes, ...$this->generateClassesForTag($tag));\n        }\n\n        if ($this->entity instanceof BookChild && userCan(Permission::BookView, $this->entity->book)) {\n            $bookTags = $this->entity->book->tags;\n            foreach ($bookTags as $bookTag) {\n                 array_push($classes, ...$this->generateClassesForTag($bookTag, 'book-'));\n            }\n        }\n\n        if ($this->entity instanceof Page && $this->entity->chapter && userCan(Permission::ChapterView, $this->entity->chapter)) {\n            $chapterTags = $this->entity->chapter->tags;\n            foreach ($chapterTags as $chapterTag) {\n                 array_push($classes, ...$this->generateClassesForTag($chapterTag, 'chapter-'));\n            }\n        }\n\n        return array_unique($classes);\n    }\n\n    public function generateAsString(): string\n    {\n        return implode(' ', $this->generate());\n    }\n\n    /**\n     * @return string[]\n     */\n    protected function generateClassesForTag(Tag $tag, string $prefix = ''): array\n    {\n        $classes = [];\n        $name = $this->normalizeTagClassString($tag->name);\n        $value = $this->normalizeTagClassString($tag->value);\n        $classes[] = \"{$prefix}tag-name-{$name}\";\n        if ($value) {\n            $classes[] = \"{$prefix}tag-value-{$value}\";\n            $classes[] = \"{$prefix}tag-pair-{$name}-{$value}\";\n        }\n        return $classes;\n    }\n\n    protected function normalizeTagClassString(string $value): string\n    {\n        $value = str_replace(' ', '', strtolower($value));\n        $value = str_replace('-', '', strtolower($value));\n\n        return $value;\n    }\n}\n"
  },
  {
    "path": "app/Activity/Tools/UserEntityWatchOptions.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Tools;\n\nuse BookStack\\Activity\\Models\\Watch;\nuse BookStack\\Activity\\WatchLevels;\nuse BookStack\\Entities\\Models\\BookChild;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Builder;\n\nclass UserEntityWatchOptions\n{\n    protected ?array $watchMap = null;\n\n    public function __construct(\n        protected User $user,\n        protected Entity $entity,\n    ) {\n    }\n\n    public function canWatch(): bool\n    {\n        return $this->user->can(Permission::ReceiveNotifications) && !$this->user->isGuest();\n    }\n\n    public function getWatchLevel(): string\n    {\n        return WatchLevels::levelValueToName($this->getWatchLevelValue());\n    }\n\n    public function isWatching(): bool\n    {\n        return $this->getWatchLevelValue() !== WatchLevels::DEFAULT;\n    }\n\n    public function getWatchedParent(): ?WatchedParentDetails\n    {\n        $watchMap = $this->getWatchMap();\n        unset($watchMap[$this->entity->getMorphClass()]);\n\n        if (isset($watchMap['chapter'])) {\n            return new WatchedParentDetails('chapter', $watchMap['chapter']);\n        }\n\n        if (isset($watchMap['book'])) {\n            return new WatchedParentDetails('book', $watchMap['book']);\n        }\n\n        return null;\n    }\n\n    public function updateLevelByName(string $level): void\n    {\n        $levelValue = WatchLevels::levelNameToValue($level);\n        $this->updateLevelByValue($levelValue);\n    }\n\n    public function updateLevelByValue(int $level): void\n    {\n        if ($level < 0) {\n            $this->remove();\n            return;\n        }\n\n        $this->updateLevel($level);\n    }\n\n    public function getWatchMap(): array\n    {\n        if (!is_null($this->watchMap)) {\n            return $this->watchMap;\n        }\n\n        $entities = [$this->entity];\n        if ($this->entity instanceof BookChild) {\n            $entities[] = $this->entity->book;\n        }\n        if ($this->entity instanceof Page && $this->entity->chapter) {\n            $entities[] = $this->entity->chapter;\n        }\n\n        $query = Watch::query()\n            ->where('user_id', '=', $this->user->id)\n            ->where(function (Builder $subQuery) use ($entities) {\n                foreach ($entities as $entity) {\n                    $subQuery->orWhere(function (Builder $whereQuery) use ($entity) {\n                        $whereQuery->where('watchable_type', '=', $entity->getMorphClass())\n                        ->where('watchable_id', '=', $entity->id);\n                    });\n                }\n            });\n\n        $this->watchMap = $query->get(['watchable_type', 'level'])\n            ->pluck('level', 'watchable_type')\n            ->toArray();\n\n        return $this->watchMap;\n    }\n\n    protected function getWatchLevelValue()\n    {\n        return $this->getWatchMap()[$this->entity->getMorphClass()] ?? WatchLevels::DEFAULT;\n    }\n\n    protected function updateLevel(int $levelValue): void\n    {\n        Watch::query()->updateOrCreate([\n            'watchable_id' => $this->entity->id,\n            'watchable_type' => $this->entity->getMorphClass(),\n            'user_id' => $this->user->id,\n        ], [\n            'level' => $levelValue,\n        ]);\n        $this->watchMap = null;\n    }\n\n    protected function remove(): void\n    {\n        $this->entityQuery()->delete();\n        $this->watchMap = null;\n    }\n\n    protected function entityQuery(): Builder\n    {\n        return Watch::query()->where('watchable_id', '=', $this->entity->id)\n            ->where('watchable_type', '=', $this->entity->getMorphClass())\n            ->where('user_id', '=', $this->user->id);\n    }\n}\n"
  },
  {
    "path": "app/Activity/Tools/WatchedParentDetails.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Tools;\n\nuse BookStack\\Activity\\WatchLevels;\n\nclass WatchedParentDetails\n{\n    public function __construct(\n        public string $type,\n        public int $level,\n    ) {\n    }\n\n    public function ignoring(): bool\n    {\n        return $this->level === WatchLevels::IGNORE;\n    }\n}\n"
  },
  {
    "path": "app/Activity/Tools/WebhookFormatter.php",
    "content": "<?php\n\nnamespace BookStack\\Activity\\Tools;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Activity\\Models\\Loggable;\nuse BookStack\\Activity\\Models\\Webhook;\nuse BookStack\\App\\Model;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Support\\Carbon;\n\nclass WebhookFormatter\n{\n    protected Webhook $webhook;\n    protected string $event;\n    protected User $initiator;\n    protected int $initiatedTime;\n    protected string|Loggable $detail;\n\n    /**\n     * @var array{condition: callable(string, Model):bool, format: callable(Model):void}[]\n     */\n    protected $modelFormatters = [];\n\n    public function __construct(string $event, Webhook $webhook, string|Loggable $detail, User $initiator, int $initiatedTime)\n    {\n        $this->webhook = $webhook;\n        $this->event = $event;\n        $this->initiator = $initiator;\n        $this->initiatedTime = $initiatedTime;\n        $this->detail = is_object($detail) ? clone $detail : $detail;\n    }\n\n    public function format(): array\n    {\n        $data = [\n            'event'                    => $this->event,\n            'text'                     => $this->formatText(),\n            'triggered_at'             => Carbon::createFromTimestampUTC($this->initiatedTime)->toISOString(),\n            'triggered_by'             => $this->initiator->attributesToArray(),\n            'triggered_by_profile_url' => $this->initiator->getProfileUrl(),\n            'webhook_id'               => $this->webhook->id,\n            'webhook_name'             => $this->webhook->name,\n        ];\n\n        if (method_exists($this->detail, 'getUrl')) {\n            $data['url'] = $this->detail->getUrl();\n        }\n\n        if ($this->detail instanceof Model) {\n            $data['related_item'] = $this->formatModel($this->detail);\n        }\n\n        return $data;\n    }\n\n    /**\n     * @param callable(string, Model):bool $condition\n     * @param callable(Model):void         $format\n     */\n    public function addModelFormatter(callable $condition, callable $format): void\n    {\n        $this->modelFormatters[] = [\n            'condition' => $condition,\n            'format'    => $format,\n        ];\n    }\n\n    public function addDefaultModelFormatters(): void\n    {\n        // Load entity owner, creator, updater details\n        $this->addModelFormatter(\n            fn ($event, $model) => ($model instanceof Entity),\n            fn ($model) => $model->load(['ownedBy', 'createdBy', 'updatedBy'])\n        );\n\n        // Load revision detail for page update and create events\n        $this->addModelFormatter(\n            fn ($event, $model) => ($model instanceof Page && ($event === ActivityType::PAGE_CREATE || $event === ActivityType::PAGE_UPDATE)),\n            fn ($model) => $model->load('currentRevision')\n        );\n    }\n\n    protected function formatModel(Model $model): array\n    {\n        $model->unsetRelations();\n\n        foreach ($this->modelFormatters as $formatter) {\n            if ($formatter['condition']($this->event, $model)) {\n                $formatter['format']($model);\n            }\n        }\n\n        return $model->toArray();\n    }\n\n    protected function formatText(): string\n    {\n        $textParts = [\n            $this->initiator->name,\n            trans('activities.' . $this->event),\n        ];\n\n        if ($this->detail instanceof Entity) {\n            $textParts[] = '\"' . $this->detail->name . '\"';\n        }\n\n        return implode(' ', $textParts);\n    }\n\n    public static function getDefault(string $event, Webhook $webhook, $detail, User $initiator, int $initiatedTime): self\n    {\n        $instance = new self($event, $webhook, $detail, $initiator, $initiatedTime);\n        $instance->addDefaultModelFormatters();\n\n        return $instance;\n    }\n}\n"
  },
  {
    "path": "app/Activity/WatchLevels.php",
    "content": "<?php\n\nnamespace BookStack\\Activity;\n\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\n\nclass WatchLevels\n{\n    /**\n     * Default level, No specific option set\n     * Typically not a stored status\n     */\n    const DEFAULT = -1;\n\n    /**\n     * Ignore all notifications.\n     */\n    const IGNORE = 0;\n\n    /**\n     * Watch for new content.\n     */\n    const NEW = 1;\n\n    /**\n     * Watch for updates and new content\n     */\n    const UPDATES = 2;\n\n    /**\n     * Watch for comments, updates and new content.\n     */\n    const COMMENTS = 3;\n\n    /**\n     * Get all the possible values as an option_name => value array.\n     * @return array<string, int>\n     */\n    public static function all(): array\n    {\n        $options = [];\n        foreach ((new \\ReflectionClass(static::class))->getConstants() as $name => $value) {\n            $options[strtolower($name)] = $value;\n        }\n\n        return $options;\n    }\n\n    /**\n     * Get the watch options suited for the given entity.\n     * @return array<string, int>\n     */\n    public static function allSuitedFor(Entity $entity): array\n    {\n        $options = static::all();\n\n        if ($entity instanceof Page) {\n            unset($options['new']);\n        } elseif ($entity instanceof Bookshelf) {\n            return [];\n        }\n\n        return $options;\n    }\n\n    /**\n     * Convert the given name to a level value.\n     * Defaults to default value if the level does not exist.\n     */\n    public static function levelNameToValue(string $level): int\n    {\n        return static::all()[$level] ?? static::DEFAULT;\n    }\n\n    /**\n     * Convert the given int level value to a level name.\n     * Defaults to 'default' level name if not existing.\n     */\n    public static function levelValueToName(int $level): string\n    {\n        foreach (static::all() as $name => $value) {\n            if ($level === $value) {\n                return $name;\n            }\n        }\n\n        return 'default';\n    }\n}\n"
  },
  {
    "path": "app/Api/ApiDocsController.php",
    "content": "<?php\n\nnamespace BookStack\\Api;\n\nuse BookStack\\Http\\ApiController;\n\nclass ApiDocsController extends ApiController\n{\n    /**\n     * Load the docs page for the API.\n     */\n    public function display()\n    {\n        $docs = ApiDocsGenerator::generateConsideringCache();\n        $this->setPageTitle(trans('settings.users_api_tokens_docs'));\n\n        return view('api-docs.index', [\n            'docs' => $docs,\n        ]);\n    }\n\n    /**\n     * Show a JSON view of the API docs data.\n     */\n    public function json()\n    {\n        $docs = ApiDocsGenerator::generateConsideringCache();\n\n        return response()->json($docs);\n    }\n\n    /**\n     * Redirect to the API docs page.\n     *  Required as a controller method, instead of the Route::redirect helper,\n     *  to ensure the URL is generated correctly.\n     */\n    public function redirect()\n    {\n        return redirect('/api/docs');\n    }\n}\n"
  },
  {
    "path": "app/Api/ApiDocsGenerator.php",
    "content": "<?php\n\nnamespace BookStack\\Api;\n\nuse BookStack\\App\\AppVersion;\nuse BookStack\\Http\\ApiController;\nuse Exception;\nuse Illuminate\\Contracts\\Container\\BindingResolutionException;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\Route;\nuse Illuminate\\Support\\Str;\nuse Illuminate\\Validation\\Rules\\Password;\nuse ReflectionClass;\nuse ReflectionException;\nuse ReflectionMethod;\n\nclass ApiDocsGenerator\n{\n    protected array $reflectionClasses = [];\n    protected array $controllerClasses = [];\n\n    /**\n     * Load the docs form the cache if existing\n     * otherwise generate and store in the cache.\n     */\n    public static function generateConsideringCache(): Collection\n    {\n        $appVersion = AppVersion::get();\n        $cacheKey = 'api-docs::' . $appVersion;\n        $isProduction = config('app.env') === 'production';\n        $cacheVal = $isProduction ? Cache::get($cacheKey) : null;\n\n        if (!is_null($cacheVal)) {\n            return $cacheVal;\n        }\n\n        $docs = (new ApiDocsGenerator())->generate();\n        Cache::put($cacheKey, $docs, 60 * 24);\n\n        return $docs;\n    }\n\n    /**\n     * Generate API documentation.\n     */\n    protected function generate(): Collection\n    {\n        $apiRoutes = $this->getFlatApiRoutes();\n        $apiRoutes = $this->loadDetailsFromControllers($apiRoutes);\n        $apiRoutes = $this->loadDetailsFromFiles($apiRoutes);\n        $apiRoutes = $apiRoutes->groupBy('base_model');\n\n        return $apiRoutes;\n    }\n\n    /**\n     * Load any API details stored in static files.\n     */\n    protected function loadDetailsFromFiles(Collection $routes): Collection\n    {\n        return $routes->map(function (array $route) {\n            $exampleTypes = ['request', 'response'];\n            $fileTypes = ['json', 'http'];\n            foreach ($exampleTypes as $exampleType) {\n                foreach ($fileTypes as $fileType) {\n                    $exampleFile = base_path(\"dev/api/{$exampleType}s/{$route['name']}.\" . $fileType);\n                    if (file_exists($exampleFile)) {\n                        $route[\"example_{$exampleType}\"] = file_get_contents($exampleFile);\n                        continue 2;\n                    }\n                }\n                $route[\"example_{$exampleType}\"] = null;\n            }\n\n            return $route;\n        });\n    }\n\n    /**\n     * Load any details we can fetch from the controller and its methods.\n     */\n    protected function loadDetailsFromControllers(Collection $routes): Collection\n    {\n        return $routes->map(function (array $route) {\n            $class = $this->getReflectionClass($route['controller']);\n            $method = $this->getReflectionMethod($route['controller'], $route['controller_method']);\n            $comment = $method->getDocComment();\n            $route['description'] = $comment ? $this->parseDescriptionFromDocBlockComment($comment) : null;\n            $route['body_params'] = $this->getBodyParamsFromClass($route['controller'], $route['controller_method']);\n\n            // Load class description for the model\n            // Not ideal to have it here on each route, but adding it in a more structured manner would break\n            // docs resulting JSON format and therefore be an API break.\n            // Save refactoring for a more significant set of changes.\n            $classComment = $class->getDocComment();\n            $route['model_description'] = $classComment ? $this->parseDescriptionFromDocBlockComment($classComment) : null;\n\n            return $route;\n        });\n    }\n\n    /**\n     * Load body params and their rules by inspecting the given class and method name.\n     *\n     * @throws BindingResolutionException\n     */\n    protected function getBodyParamsFromClass(string $className, string $methodName): ?array\n    {\n        /** @var ApiController $class */\n        $class = $this->controllerClasses[$className] ?? null;\n        if ($class === null) {\n            $class = app()->make($className);\n            $this->controllerClasses[$className] = $class;\n        }\n\n        $rules = collect($class->getValidationRules()[$methodName] ?? [])->map(function ($validations) {\n            return array_map(function ($validation) {\n                return $this->getValidationAsString($validation);\n            }, $validations);\n        })->toArray();\n\n        return empty($rules) ? null : $rules;\n    }\n\n    /**\n     * Convert the given validation message to a readable string.\n     */\n    protected function getValidationAsString($validation): string\n    {\n        if (is_string($validation)) {\n            return $validation;\n        }\n\n        if (is_object($validation) && method_exists($validation, '__toString')) {\n            return strval($validation);\n        }\n\n        if ($validation instanceof Password) {\n            return 'min:8';\n        }\n\n        $class = get_class($validation);\n\n        throw new Exception(\"Cannot provide string representation of rule for class: {$class}\");\n    }\n\n    /**\n     * Parse out the description text from a class method comment.\n     */\n    protected function parseDescriptionFromDocBlockComment(string $comment): string\n    {\n        $matches = [];\n        preg_match_all('/^\\s*?\\*\\s?($|((?![\\/@\\s]).*?))$/m', $comment, $matches);\n\n        $text = implode(' ', $matches[1] ?? []);\n        return str_replace('  ', \"\\n\", $text);\n    }\n\n    /**\n     * Get a reflection method from the given class name and method name.\n     *\n     * @throws ReflectionException\n     */\n    protected function getReflectionMethod(string $className, string $methodName): ReflectionMethod\n    {\n        return $this->getReflectionClass($className)->getMethod($methodName);\n    }\n\n    /**\n     * Get a reflection class from the given class name.\n     *\n     * @throws ReflectionException\n     */\n    protected function getReflectionClass(string $className): ReflectionClass\n    {\n        $class = $this->reflectionClasses[$className] ?? null;\n        if ($class === null) {\n            $class = new ReflectionClass($className);\n            $this->reflectionClasses[$className] = $class;\n        }\n\n        return $class;\n    }\n\n    /**\n     * Get the system API routes, formatted into a flat collection.\n     */\n    protected function getFlatApiRoutes(): Collection\n    {\n        return collect(Route::getRoutes()->getRoutes())->filter(function ($route) {\n            return strpos($route->uri, 'api/') === 0;\n        })->map(function ($route) {\n            [$controller, $controllerMethod] = explode('@', $route->action['uses']);\n            $baseModelName = explode('.', explode('/', $route->uri)[1])[0];\n            $shortName = $baseModelName . '-' . $controllerMethod;\n\n            return [\n                'name'                    => $shortName,\n                'uri'                     => $route->uri,\n                'method'                  => $route->methods[0],\n                'controller'              => $controller,\n                'controller_method'       => $controllerMethod,\n                'controller_method_kebab' => Str::kebab($controllerMethod),\n                'base_model'              => $baseModelName,\n            ];\n        });\n    }\n}\n"
  },
  {
    "path": "app/Api/ApiEntityListFormatter.php",
    "content": "<?php\n\nnamespace BookStack\\Api;\n\nuse BookStack\\Entities\\Models\\BookChild;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\n\nclass ApiEntityListFormatter\n{\n    /**\n     * The list to be formatted.\n     * @var Entity[]\n     */\n    protected array $list = [];\n\n    /**\n     * The fields to show in the formatted data.\n     * Can be a plain string array item for a direct model field (If existing on model).\n     * If the key is a string, with a callable value, the return value of the callable\n     * will be used for the resultant value. A null return value will omit the property.\n     * @var array<string|int, string|callable>\n     */\n    protected array $fields = [\n        'id',\n        'name',\n        'slug',\n        'book_id',\n        'chapter_id',\n        'draft',\n        'template',\n        'priority',\n        'created_at',\n        'updated_at',\n    ];\n\n    public function __construct(array $list)\n    {\n        $this->list = $list;\n\n        // Default dynamic fields\n        $this->withField('url', fn(Entity $entity) => $entity->getUrl());\n    }\n\n    /**\n     * Add a field to be used in the formatter, with the property using the given\n     * name and value being the return type of the given callback.\n     */\n    public function withField(string $property, callable $callback): self\n    {\n        $this->fields[$property] = $callback;\n        return $this;\n    }\n\n    /**\n     * Show the 'type' property in the response reflecting the entity type.\n     * EG: page, chapter, bookshelf, book\n     * To be included in results with non-pre-determined types.\n     */\n    public function withType(): self\n    {\n        $this->withField('type', fn(Entity $entity) => $entity->getType());\n        return $this;\n    }\n\n    /**\n     * Include tags in the formatted data.\n     */\n    public function withTags(): self\n    {\n        $this->withField('tags', fn(Entity $entity) => $entity->tags);\n        return $this;\n    }\n\n    /**\n     * Include parent book/chapter info in the formatted data.\n     */\n    public function withParents(): self\n    {\n        $this->withField('book', function (Entity $entity) {\n            if ($entity instanceof BookChild && $entity->book) {\n                return $entity->book->only(['id', 'name', 'slug']);\n            }\n            return null;\n        });\n\n        $this->withField('chapter', function (Entity $entity) {\n            if ($entity instanceof Page && $entity->chapter) {\n                return $entity->chapter->only(['id', 'name', 'slug']);\n            }\n            return null;\n        });\n\n        return $this;\n    }\n\n    /**\n     * Format the data and return an array of formatted content.\n     * @return array[]\n     */\n    public function format(): array\n    {\n        $results = [];\n\n        foreach ($this->list as $item) {\n            $results[] = $this->formatSingle($item);\n        }\n\n        return $results;\n    }\n\n    /**\n     * Format a single entity item to a plain array.\n     */\n    protected function formatSingle(Entity $entity): array\n    {\n        $result = [];\n        $values = (clone $entity)->toArray();\n\n        foreach ($this->fields as $field => $callback) {\n            if (is_string($callback)) {\n                $field = $callback;\n                if (!isset($values[$field])) {\n                    continue;\n                }\n                $value = $values[$field];\n            } else {\n                $value = $callback($entity);\n                if (is_null($value)) {\n                    continue;\n                }\n            }\n\n            $result[$field] = $value;\n        }\n\n        return $result;\n    }\n}\n"
  },
  {
    "path": "app/Api/ApiToken.php",
    "content": "<?php\n\nnamespace BookStack\\Api;\n\nuse BookStack\\Activity\\Models\\Loggable;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Support\\Carbon;\n\n/**\n * Class ApiToken.\n *\n * @property int    $id\n * @property string $token_id\n * @property string $secret\n * @property string $name\n * @property Carbon $expires_at\n * @property User   $user\n */\nclass ApiToken extends Model implements Loggable\n{\n    use HasFactory;\n\n    protected $fillable = ['name', 'expires_at'];\n    protected $casts = [\n        'expires_at' => 'date:Y-m-d',\n    ];\n\n    /**\n     * Get the user that this token belongs to.\n     */\n    public function user(): BelongsTo\n    {\n        return $this->belongsTo(User::class);\n    }\n\n    /**\n     * Get the default expiry value for an API token.\n     * Set to 100 years from now.\n     */\n    public static function defaultExpiry(): string\n    {\n        return Carbon::now()->addYears(100)->format('Y-m-d');\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function logDescriptor(): string\n    {\n        return \"({$this->id}) {$this->name}; User: {$this->user->logDescriptor()}\";\n    }\n\n    /**\n     * Get the URL for managing this token.\n     */\n    public function getUrl(string $path = ''): string\n    {\n        return url(\"/api-tokens/{$this->user_id}/{$this->id}/\" . trim($path, '/'));\n    }\n}\n"
  },
  {
    "path": "app/Api/ApiTokenGuard.php",
    "content": "<?php\n\nnamespace BookStack\\Api;\n\nuse BookStack\\Access\\LoginService;\nuse BookStack\\Exceptions\\ApiAuthException;\nuse BookStack\\Permissions\\Permission;\nuse Illuminate\\Auth\\GuardHelpers;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Contracts\\Auth\\Guard;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Facades\\Hash;\nuse Symfony\\Component\\HttpFoundation\\Request;\n\nclass ApiTokenGuard implements Guard\n{\n    use GuardHelpers;\n\n    /**\n     * The request instance.\n     */\n    protected $request;\n\n    /**\n     * @var LoginService\n     */\n    protected $loginService;\n\n    /**\n     * The last auth exception thrown in this request.\n     *\n     * @var ApiAuthException\n     */\n    protected $lastAuthException;\n\n    /**\n     * ApiTokenGuard constructor.\n     */\n    public function __construct(Request $request, LoginService $loginService)\n    {\n        $this->request = $request;\n        $this->loginService = $loginService;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function user()\n    {\n        // Return the user if we've already retrieved them.\n        // Effectively a request-instance cache for this method.\n        if (!is_null($this->user)) {\n            return $this->user;\n        }\n\n        $user = null;\n\n        try {\n            $user = $this->getAuthorisedUserFromRequest();\n        } catch (ApiAuthException $exception) {\n            $this->lastAuthException = $exception;\n        }\n\n        $this->user = $user;\n\n        return $user;\n    }\n\n    /**\n     * Determine if current user is authenticated. If not, throw an exception.\n     *\n     * @throws ApiAuthException\n     *\n     * @return \\Illuminate\\Contracts\\Auth\\Authenticatable\n     */\n    public function authenticate()\n    {\n        if (!is_null($user = $this->user())) {\n            return $user;\n        }\n\n        if ($this->lastAuthException) {\n            throw $this->lastAuthException;\n        }\n\n        throw new ApiAuthException('Unauthorized');\n    }\n\n    /**\n     * Check the API token in the request and fetch a valid authorised user.\n     *\n     * @throws ApiAuthException\n     */\n    protected function getAuthorisedUserFromRequest(): Authenticatable\n    {\n        $authToken = trim($this->request->headers->get('Authorization', ''));\n        $this->validateTokenHeaderValue($authToken);\n\n        [$id, $secret] = explode(':', str_replace('Token ', '', $authToken));\n        $token = ApiToken::query()\n            ->where('token_id', '=', $id)\n            ->with(['user'])->first();\n\n        $this->validateToken($token, $secret);\n\n        if ($this->loginService->awaitingEmailConfirmation($token->user)) {\n            throw new ApiAuthException(trans('errors.email_confirmation_awaiting'));\n        }\n\n        return $token->user;\n    }\n\n    /**\n     * Validate the format of the token header value string.\n     *\n     * @throws ApiAuthException\n     */\n    protected function validateTokenHeaderValue(string $authToken): void\n    {\n        if (empty($authToken)) {\n            throw new ApiAuthException(trans('errors.api_no_authorization_found'));\n        }\n\n        if (strpos($authToken, ':') === false || strpos($authToken, 'Token ') !== 0) {\n            throw new ApiAuthException(trans('errors.api_bad_authorization_format'));\n        }\n    }\n\n    /**\n     * Validate the given secret against the given token and ensure the token\n     * currently has access to the instance API.\n     *\n     * @throws ApiAuthException\n     */\n    protected function validateToken(?ApiToken $token, string $secret): void\n    {\n        if ($token === null) {\n            throw new ApiAuthException(trans('errors.api_user_token_not_found'));\n        }\n\n        if (!Hash::check($secret, $token->secret)) {\n            throw new ApiAuthException(trans('errors.api_incorrect_token_secret'));\n        }\n\n        $now = Carbon::now();\n        if ($token->expires_at <= $now) {\n            throw new ApiAuthException(trans('errors.api_user_token_expired'), 403);\n        }\n\n        if (!$token->user->can(Permission::AccessApi)) {\n            throw new ApiAuthException(trans('errors.api_user_no_api_permission'), 403);\n        }\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function validate(array $credentials = [])\n    {\n        if (empty($credentials['id']) || empty($credentials['secret'])) {\n            return false;\n        }\n\n        $token = ApiToken::query()\n            ->where('token_id', '=', $credentials['id'])\n            ->with(['user'])->first();\n\n        if ($token === null) {\n            return false;\n        }\n\n        return Hash::check($credentials['secret'], $token->secret);\n    }\n\n    /**\n     * \"Log out\" the currently authenticated user.\n     */\n    public function logout()\n    {\n        $this->user = null;\n    }\n}\n"
  },
  {
    "path": "app/Api/ListingResponseBuilder.php",
    "content": "<?php\n\nnamespace BookStack\\Api;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\n\nclass ListingResponseBuilder\n{\n    protected Builder $query;\n    protected Request $request;\n\n    /**\n     * @var string[]\n     */\n    protected array $fields;\n\n    /**\n     * @var array<callable>\n     */\n    protected array $resultModifiers = [];\n\n    /**\n     * @var array<string, string>\n     */\n    protected array $filterOperators = [\n        'eq'   => '=',\n        'ne'   => '!=',\n        'gt'   => '>',\n        'lt'   => '<',\n        'gte'  => '>=',\n        'lte'  => '<=',\n        'like' => 'like',\n    ];\n\n    /**\n     * ListingResponseBuilder constructor.\n     * The given fields will be forced visible within the model results.\n     */\n    public function __construct(Builder $query, Request $request, array $fields)\n    {\n        $this->query = $query;\n        $this->request = $request;\n        $this->fields = $fields;\n    }\n\n    /**\n     * Get the response from this builder.\n     */\n    public function toResponse(): JsonResponse\n    {\n        $filteredQuery = $this->filterQuery($this->query);\n\n        $total = $filteredQuery->count();\n        $data = $this->fetchData($filteredQuery)->each(function ($model) {\n            foreach ($this->resultModifiers as $modifier) {\n                $modifier($model);\n            }\n        });\n\n        return response()->json([\n            'data'  => $data,\n            'total' => $total,\n        ]);\n    }\n\n    /**\n     * Add a callback to modify each element of the results.\n     *\n     * @param (callable(Model): void) $modifier\n     */\n    public function modifyResults(callable $modifier): void\n    {\n        $this->resultModifiers[] = $modifier;\n    }\n\n    /**\n     * Fetch the data to return within the response.\n     */\n    protected function fetchData(Builder $query): Collection\n    {\n        $query = $this->countAndOffsetQuery($query);\n        $query = $this->sortQuery($query);\n\n        return $query->get($this->fields);\n    }\n\n    /**\n     * Apply any filtering operations found in the request.\n     */\n    protected function filterQuery(Builder $query): Builder\n    {\n        $query = clone $query;\n        $requestFilters = $this->request->get('filter', []);\n        if (!is_array($requestFilters)) {\n            return $query;\n        }\n\n        $queryFilters = collect($requestFilters)->map(function ($value, $key) {\n            return $this->requestFilterToQueryFilter($key, $value);\n        })->filter(function ($value) {\n            return !is_null($value);\n        })->values()->toArray();\n\n        return $query->where($queryFilters);\n    }\n\n    /**\n     * Convert a request filter query key/value pair into a [field, op, value] where condition.\n     */\n    protected function requestFilterToQueryFilter($fieldKey, $value): ?array\n    {\n        $splitKey = explode(':', $fieldKey);\n        $field = $splitKey[0];\n        $filterOperator = $splitKey[1] ?? 'eq';\n\n        if (!in_array($field, $this->fields)) {\n            return null;\n        }\n\n        if (!in_array($filterOperator, array_keys($this->filterOperators))) {\n            $filterOperator = 'eq';\n        }\n\n        $queryOperator = $this->filterOperators[$filterOperator];\n\n        return [$field, $queryOperator, $value];\n    }\n\n    /**\n     * Apply sorting operations to the query from given parameters\n     * otherwise falling back to the first given field, ascending.\n     */\n    protected function sortQuery(Builder $query): Builder\n    {\n        $query = clone $query;\n        $defaultSortName = $this->fields[0];\n        $direction = 'asc';\n\n        $sort = $this->request->get('sort', '');\n        if (strpos($sort, '-') === 0) {\n            $direction = 'desc';\n        }\n\n        $sortName = ltrim($sort, '+- ');\n        if (!in_array($sortName, $this->fields)) {\n            $sortName = $defaultSortName;\n        }\n\n        return $query->orderBy($sortName, $direction);\n    }\n\n    /**\n     * Apply count and offset for paging, based on params from the request while falling\n     * back to system defined default, taking the max limit into account.\n     */\n    protected function countAndOffsetQuery(Builder $query): Builder\n    {\n        $query = clone $query;\n        $offset = max(0, $this->request->get('offset', 0));\n        $maxCount = config('api.max_item_count');\n        $count = $this->request->get('count', config('api.default_item_count'));\n        $count = max(min($maxCount, $count), 1);\n\n        return $query->skip($offset)->take($count);\n    }\n}\n"
  },
  {
    "path": "app/Api/UserApiTokenController.php",
    "content": "<?php\n\nnamespace BookStack\\Api;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\Hash;\nuse Illuminate\\Support\\Str;\n\nclass UserApiTokenController extends Controller\n{\n    /**\n     * Show the form to create a new API token.\n     */\n    public function create(Request $request, int $userId)\n    {\n        $this->checkPermission(Permission::AccessApi);\n        $this->checkPermissionOrCurrentUser(Permission::UsersManage, $userId);\n        $this->updateContext($request);\n\n        $user = User::query()->findOrFail($userId);\n\n        $this->setPageTitle(trans('settings.user_api_token_create'));\n\n        return view('users.api-tokens.create', [\n            'user' => $user,\n            'back' => $this->getRedirectPath($user),\n        ]);\n    }\n\n    /**\n     * Store a new API token in the system.\n     */\n    public function store(Request $request, int $userId)\n    {\n        $this->checkPermission(Permission::AccessApi);\n        $this->checkPermissionOrCurrentUser(Permission::UsersManage, $userId);\n\n        $this->validate($request, [\n            'name'       => ['required', 'max:250'],\n            'expires_at' => ['date_format:Y-m-d'],\n        ]);\n\n        $user = User::query()->findOrFail($userId);\n        $secret = Str::random(32);\n\n        $token = (new ApiToken())->forceFill([\n            'name'       => $request->get('name'),\n            'token_id'   => Str::random(32),\n            'secret'     => Hash::make($secret),\n            'user_id'    => $user->id,\n            'expires_at' => $request->get('expires_at') ?: ApiToken::defaultExpiry(),\n        ]);\n\n        while (ApiToken::query()->where('token_id', '=', $token->token_id)->exists()) {\n            $token->token_id = Str::random(32);\n        }\n\n        $token->save();\n\n        session()->flash('api-token-secret:' . $token->id, $secret);\n        $this->logActivity(ActivityType::API_TOKEN_CREATE, $token);\n\n        return redirect($token->getUrl());\n    }\n\n    /**\n     * Show the details for a user API token, with access to edit.\n     */\n    public function edit(Request $request, int $userId, int $tokenId)\n    {\n        $this->updateContext($request);\n\n        [$user, $token] = $this->checkPermissionAndFetchUserToken($userId, $tokenId);\n        $secret = session()->pull('api-token-secret:' . $token->id, null);\n\n        $this->setPageTitle(trans('settings.user_api_token'));\n\n        return view('users.api-tokens.edit', [\n            'user'   => $user,\n            'token'  => $token,\n            'model'  => $token,\n            'secret' => $secret,\n            'back' => $this->getRedirectPath($user),\n        ]);\n    }\n\n    /**\n     * Update the API token.\n     */\n    public function update(Request $request, int $userId, int $tokenId)\n    {\n        $this->validate($request, [\n            'name'       => ['required', 'max:250'],\n            'expires_at' => ['date_format:Y-m-d'],\n        ]);\n\n        [$user, $token] = $this->checkPermissionAndFetchUserToken($userId, $tokenId);\n        $token->fill([\n            'name'       => $request->get('name'),\n            'expires_at' => $request->get('expires_at') ?: ApiToken::defaultExpiry(),\n        ])->save();\n\n        $this->logActivity(ActivityType::API_TOKEN_UPDATE, $token);\n\n        return redirect($token->getUrl());\n    }\n\n    /**\n     * Show the delete view for this token.\n     */\n    public function delete(int $userId, int $tokenId)\n    {\n        [$user, $token] = $this->checkPermissionAndFetchUserToken($userId, $tokenId);\n\n        $this->setPageTitle(trans('settings.user_api_token_delete'));\n\n        return view('users.api-tokens.delete', [\n            'user'  => $user,\n            'token' => $token,\n        ]);\n    }\n\n    /**\n     * Destroy a token from the system.\n     */\n    public function destroy(int $userId, int $tokenId)\n    {\n        [$user, $token] = $this->checkPermissionAndFetchUserToken($userId, $tokenId);\n        $token->delete();\n\n        $this->logActivity(ActivityType::API_TOKEN_DELETE, $token);\n\n        return redirect($this->getRedirectPath($user));\n    }\n\n    /**\n     * Check the permission for the current user and return an array\n     * where the first item is the user in context and the second item is their\n     * API token in context.\n     */\n    protected function checkPermissionAndFetchUserToken(int $userId, int $tokenId): array\n    {\n        $this->checkPermissionOr(Permission::UsersManage, function () use ($userId) {\n            return $userId === user()->id && userCan(Permission::AccessApi);\n        });\n\n        $user = User::query()->findOrFail($userId);\n        $token = ApiToken::query()->where('user_id', '=', $user->id)->where('id', '=', $tokenId)->firstOrFail();\n\n        return [$user, $token];\n    }\n\n    /**\n     * Update the context for where the user is coming from to manage API tokens.\n     * (Track of location for correct return redirects)\n     */\n    protected function updateContext(Request $request): void\n    {\n        $context = $request->query('context');\n        if ($context) {\n            session()->put('api-token-context', $context);\n        }\n    }\n\n    /**\n     * Get the redirect path for the current api token editing session.\n     * Attempts to recall the context of where the user is editing from.\n     */\n    protected function getRedirectPath(User $relatedUser): string\n    {\n        $context = session()->get('api-token-context');\n        if ($context === 'settings' || user()->id !== $relatedUser->id) {\n            return $relatedUser->getEditUrl('#api_tokens');\n        }\n\n        return url('/my-account/auth#api_tokens');\n    }\n}\n"
  },
  {
    "path": "app/App/AppVersion.php",
    "content": "<?php\n\nnamespace BookStack\\App;\n\nclass AppVersion\n{\n    protected static string $version = '';\n\n    /**\n     * Get the application's version number from its top-level `version` text file.\n     */\n    public static function get(): string\n    {\n        if (!empty(static::$version)) {\n            return static::$version;\n        }\n\n        $versionFile = base_path('version');\n        $version = trim(file_get_contents($versionFile));\n        static::$version = $version;\n\n        return $version;\n    }\n}\n"
  },
  {
    "path": "app/App/Application.php",
    "content": "<?php\n\nnamespace BookStack\\App;\n\nclass Application extends \\Illuminate\\Foundation\\Application\n{\n    /**\n     * Get the path to the application configuration files.\n     *\n     * @param string $path Optionally, a path to append to the config path\n     *\n     * @return string\n     */\n    public function configPath($path = '')\n    {\n        return $this->basePath\n            . DIRECTORY_SEPARATOR\n            . 'app'\n            . DIRECTORY_SEPARATOR\n            . 'Config'\n            . ($path ? DIRECTORY_SEPARATOR . $path : $path);\n    }\n}\n"
  },
  {
    "path": "app/App/HomeController.php",
    "content": "<?php\n\nnamespace BookStack\\App;\n\nuse BookStack\\Activity\\ActivityQueries;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse BookStack\\Entities\\Queries\\QueryRecentlyViewed;\nuse BookStack\\Entities\\Queries\\QueryTopFavourites;\nuse BookStack\\Entities\\Tools\\PageContent;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Util\\SimpleListOptions;\nuse Illuminate\\Http\\Request;\n\nclass HomeController extends Controller\n{\n    public function __construct(\n        protected EntityQueries $queries,\n    ) {\n    }\n\n    /**\n     * Display the homepage.\n     */\n    public function index(\n        Request $request,\n        ActivityQueries $activities,\n        QueryRecentlyViewed $recentlyViewed,\n        QueryTopFavourites $topFavourites,\n    ) {\n        $activity = $activities->latest(10);\n        $draftPages = [];\n\n        if ($this->isSignedIn()) {\n            $draftPages = $this->queries->pages->currentUserDraftsForList()\n                ->orderBy('updated_at', 'desc')\n                ->with('book')\n                ->take(6)\n                ->get();\n        }\n\n        $recentFactor = count($draftPages) > 0 ? 0.5 : 1;\n        $recents = $this->isSignedIn() ?\n            $recentlyViewed->run(12 * $recentFactor, 1)\n            : $this->queries->books->visibleForList()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();\n        $favourites = $topFavourites->run(6);\n        $recentlyUpdatedPages = $this->queries->pages->visibleForList()\n            ->where('draft', false)\n            ->orderBy('updated_at', 'desc')\n            ->take($favourites->count() > 0 ? 5 : 10)\n            ->get();\n\n        $homepageOptions = ['default', 'books', 'bookshelves', 'page'];\n        $homepageOption = setting('app-homepage-type', 'default');\n        if (!in_array($homepageOption, $homepageOptions)) {\n            $homepageOption = 'default';\n        }\n\n        $commonData = [\n            'activity'             => $activity,\n            'recents'              => $recents,\n            'recentlyUpdatedPages' => $recentlyUpdatedPages,\n            'draftPages'           => $draftPages,\n            'favourites'           => $favourites,\n        ];\n\n        // Add required list ordering & sorting for books & shelves views.\n        if ($homepageOption === 'bookshelves' || $homepageOption === 'books') {\n            $key = $homepageOption;\n            $view = setting()->getForCurrentUser($key . '_view_type');\n            $listOptions = SimpleListOptions::fromRequest($request, $key)->withSortOptions([\n                'name' => trans('common.sort_name'),\n                'created_at' => trans('common.sort_created_at'),\n                'updated_at' => trans('common.sort_updated_at'),\n            ]);\n\n            $commonData = array_merge($commonData, [\n                'view'        => $view,\n                'listOptions' => $listOptions,\n            ]);\n        }\n\n        if ($homepageOption === 'bookshelves') {\n            $shelves = $this->queries->shelves->visibleForListWithCover()\n                ->orderBy($commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder())\n                ->paginate(setting()->getInteger('lists-page-count-shelves', 18, 1, 1000));\n            $data = array_merge($commonData, ['shelves' => $shelves]);\n\n            return view('home.shelves', $data);\n        }\n\n        if ($homepageOption === 'books') {\n            $books = $this->queries->books->visibleForListWithCover()\n                ->orderBy($commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder())\n                ->paginate(setting()->getInteger('lists-page-count-books', 18, 1, 1000));\n            $data = array_merge($commonData, ['books' => $books]);\n\n            return view('home.books', $data);\n        }\n\n        if ($homepageOption === 'page') {\n            $homepageSetting = setting('app-homepage', '0:');\n            $id = intval(explode(':', $homepageSetting)[0]);\n            /** @var Page $customHomepage */\n            $customHomepage = $this->queries->pages->start()->where('draft', '=', false)->findOrFail($id);\n            $pageContent = new PageContent($customHomepage);\n            $customHomepage->html = $pageContent->render(false);\n\n            return view('home.specific-page', array_merge($commonData, ['customHomepage' => $customHomepage]));\n        }\n\n        return view('home.default', $commonData);\n    }\n}\n"
  },
  {
    "path": "app/App/MailNotification.php",
    "content": "<?php\n\nnamespace BookStack\\App;\n\nuse BookStack\\Translation\\LocaleDefinition;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Notifications\\Messages\\MailMessage;\nuse Illuminate\\Notifications\\Notification;\n\nabstract class MailNotification extends Notification implements ShouldQueue\n{\n    use Queueable;\n\n    /**\n     * Get the mail representation of the notification.\n     */\n    abstract public function toMail(User $notifiable): MailMessage;\n\n    /**\n     * Get the notification's channels.\n     *\n     * @param mixed $notifiable\n     *\n     * @return array|string\n     */\n    public function via($notifiable)\n    {\n        return ['mail'];\n    }\n\n    /**\n     * Create a new mail message.\n     */\n    protected function newMailMessage(?LocaleDefinition $locale = null): MailMessage\n    {\n        $data = ['locale' => $locale ?? user()->getLocale()];\n\n        return (new MailMessage())->view([\n            'html' => 'vendor.notifications.email',\n            'text' => 'vendor.notifications.email-plain',\n        ], $data);\n    }\n}\n"
  },
  {
    "path": "app/App/MetaController.php",
    "content": "<?php\n\nnamespace BookStack\\App;\n\nuse BookStack\\Http\\Controller;\nuse BookStack\\Uploads\\FaviconHandler;\n\nclass MetaController extends Controller\n{\n    /**\n     * Show the view for /robots.txt.\n     */\n    public function robots()\n    {\n        $sitePublic = setting('app-public', false);\n        $allowRobots = config('app.allow_robots');\n\n        if ($allowRobots === null) {\n            $allowRobots = $sitePublic;\n        }\n\n        return response()\n            ->view('misc.robots', ['allowRobots' => $allowRobots])\n            ->header('Content-Type', 'text/plain');\n    }\n\n    /**\n     * Show the route for 404 responses.\n     */\n    public function notFound()\n    {\n        return response()->view('errors.404', [], 404);\n    }\n\n    /**\n     * Serve the application favicon.\n     * Ensures a 'favicon.ico' file exists at the web root location (if writable) to be served\n     * directly by the webserver in the future.\n     */\n    public function favicon(FaviconHandler $favicons)\n    {\n        $exists = $favicons->restoreOriginalIfNotExists();\n        return response()->file($exists ? $favicons->getPath() : $favicons->getOriginalPath());\n    }\n\n    /**\n     * Serve a PWA application manifest.\n     */\n    public function pwaManifest(PwaManifestBuilder $manifestBuilder)\n    {\n        return response()->json($manifestBuilder->build());\n    }\n\n    /**\n     * Show license information for the application.\n     */\n    public function licenses()\n    {\n        $this->setPageTitle(trans('settings.licenses'));\n\n        return view('help.licenses', [\n            'license' => file_get_contents(base_path('LICENSE')),\n            'phpLibData' => file_get_contents(base_path('dev/licensing/php-library-licenses.txt')),\n            'jsLibData' => file_get_contents(base_path('dev/licensing/js-library-licenses.txt')),\n        ]);\n    }\n\n    /**\n     * Show the view for /opensearch.xml.\n     */\n    public function opensearch()\n    {\n        return response()\n            ->view('misc.opensearch')\n            ->header('Content-Type', 'application/opensearchdescription+xml');\n    }\n}\n"
  },
  {
    "path": "app/App/Model.php",
    "content": "<?php\n\nnamespace BookStack\\App;\n\nuse Illuminate\\Database\\Eloquent\\Model as EloquentModel;\n\nclass Model extends EloquentModel\n{\n    /**\n     * Provides public access to get the raw attribute value from the model.\n     * Used in areas where no mutations are required, but performance is critical.\n     *\n     * @return mixed\n     */\n    public function getRawAttribute(string $key)\n    {\n        return parent::getAttributeFromArray($key);\n    }\n}\n"
  },
  {
    "path": "app/App/Providers/AppServiceProvider.php",
    "content": "<?php\n\nnamespace BookStack\\App\\Providers;\n\nuse BookStack\\Access\\SocialDriverManager;\nuse BookStack\\Activity\\Models\\Comment;\nuse BookStack\\Activity\\Tools\\ActivityLogger;\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Exceptions\\BookStackExceptionHandlerPage;\nuse BookStack\\Http\\HttpRequestService;\nuse BookStack\\Permissions\\PermissionApplicator;\nuse BookStack\\Settings\\SettingService;\nuse BookStack\\Util\\CspService;\nuse Illuminate\\Contracts\\Foundation\\ExceptionRenderer;\nuse Illuminate\\Database\\Eloquent\\Relations\\Relation;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Illuminate\\Support\\Facades\\URL;\nuse Illuminate\\Support\\ServiceProvider;\n\nclass AppServiceProvider extends ServiceProvider\n{\n    /**\n     * Custom container bindings to register.\n     * @var string[]\n     */\n    public array $bindings = [\n        ExceptionRenderer::class => BookStackExceptionHandlerPage::class,\n    ];\n\n    /**\n     * Custom singleton bindings to register.\n     * @var string[]\n     */\n    public array $singletons = [\n        'activity' => ActivityLogger::class,\n        SettingService::class => SettingService::class,\n        SocialDriverManager::class => SocialDriverManager::class,\n        CspService::class => CspService::class,\n        HttpRequestService::class => HttpRequestService::class,\n    ];\n\n    /**\n     * Register any application services.\n     */\n    public function register(): void\n    {\n        $this->app->singleton(PermissionApplicator::class, function ($app) {\n            return new PermissionApplicator(null);\n        });\n    }\n\n    /**\n     * Bootstrap any application services.\n     */\n    public function boot(): void\n    {\n        // Set root URL\n        $appUrl = config('app.url');\n        if ($appUrl) {\n            $isHttps = str_starts_with($appUrl, 'https://');\n            URL::forceRootUrl($appUrl);\n            URL::forceScheme($isHttps ? 'https' : 'http');\n        }\n\n        // Set SMTP mail driver to use a local domain matching the app domain,\n        // which helps avoid defaulting to a 127.0.0.1 domain\n        if ($appUrl) {\n            $hostName = parse_url($appUrl, PHP_URL_HOST) ?: null;\n            config()->set('mail.mailers.smtp.local_domain', $hostName);\n        }\n\n        // Allow longer string lengths after upgrade to utf8mb4\n        Schema::defaultStringLength(191);\n\n        // Set morph-map for our relations to friendlier aliases\n        Relation::enforceMorphMap([\n            'bookshelf' => Bookshelf::class,\n            'book'      => Book::class,\n            'chapter'   => Chapter::class,\n            'page'      => Page::class,\n            'comment'   => Comment::class,\n        ]);\n    }\n}\n"
  },
  {
    "path": "app/App/Providers/AuthServiceProvider.php",
    "content": "<?php\n\nnamespace BookStack\\App\\Providers;\n\nuse BookStack\\Access\\ExternalBaseUserProvider;\nuse BookStack\\Access\\Guards\\AsyncExternalBaseSessionGuard;\nuse BookStack\\Access\\Guards\\LdapSessionGuard;\nuse BookStack\\Access\\LdapService;\nuse BookStack\\Access\\LoginService;\nuse BookStack\\Access\\RegistrationService;\nuse BookStack\\Api\\ApiTokenGuard;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Support\\Facades\\Auth;\nuse Illuminate\\Support\\ServiceProvider;\nuse Illuminate\\Validation\\Rules\\Password;\n\nclass AuthServiceProvider extends ServiceProvider\n{\n    /**\n     * Bootstrap the application services.\n     */\n    public function boot(): void\n    {\n        // Password Configuration\n        // Changes here must be reflected in ApiDocsGenerate@getValidationAsString.\n        Password::defaults(fn () => Password::min(8));\n\n        // Custom guards\n        Auth::extend('api-token', function ($app, $name, array $config) {\n            return new ApiTokenGuard($app['request'], $app->make(LoginService::class));\n        });\n\n        Auth::extend('ldap-session', function ($app, $name, array $config) {\n            $provider = Auth::createUserProvider($config['provider']);\n\n            return new LdapSessionGuard(\n                $name,\n                $provider,\n                $app['session.store'],\n                $app[LdapService::class],\n                $app[RegistrationService::class]\n            );\n        });\n\n        Auth::extend('async-external-session', function ($app, $name, array $config) {\n            $provider = Auth::createUserProvider($config['provider']);\n\n            return new AsyncExternalBaseSessionGuard(\n                $name,\n                $provider,\n                $app['session.store'],\n                $app[RegistrationService::class]\n            );\n        });\n    }\n\n    /**\n     * Register the application services.\n     */\n    public function register(): void\n    {\n        Auth::provider('external-users', function () {\n            return new ExternalBaseUserProvider();\n        });\n\n        // Bind and provide the default system user as a singleton to the app instance when needed.\n        // This effectively \"caches\" fetching the user at an app-instance level.\n        $this->app->singleton('users.default', function () {\n            return User::query()->where('system_name', '=', 'public')->first();\n        });\n    }\n}\n"
  },
  {
    "path": "app/App/Providers/EventServiceProvider.php",
    "content": "<?php\n\nnamespace BookStack\\App\\Providers;\n\nuse Illuminate\\Foundation\\Support\\Providers\\EventServiceProvider as ServiceProvider;\nuse SocialiteProviders\\Azure\\AzureExtendSocialite;\nuse SocialiteProviders\\Discord\\DiscordExtendSocialite;\nuse SocialiteProviders\\GitLab\\GitLabExtendSocialite;\nuse SocialiteProviders\\Manager\\SocialiteWasCalled;\nuse SocialiteProviders\\Okta\\OktaExtendSocialite;\nuse SocialiteProviders\\Twitch\\TwitchExtendSocialite;\n\nclass EventServiceProvider extends ServiceProvider\n{\n    /**\n     * The event listener mappings for the application.\n     *\n     * @var array<class-string, array<int, string>>\n     */\n    protected $listen = [\n        SocialiteWasCalled::class => [\n            AzureExtendSocialite::class . '@handle',\n            OktaExtendSocialite::class . '@handle',\n            GitLabExtendSocialite::class . '@handle',\n            TwitchExtendSocialite::class . '@handle',\n            DiscordExtendSocialite::class . '@handle',\n        ],\n    ];\n\n    /**\n     * Register any events for your application.\n     */\n    public function boot(): void\n    {\n        //\n    }\n\n    /**\n     * Determine if events and listeners should be automatically discovered.\n     */\n    public function shouldDiscoverEvents(): bool\n    {\n        return false;\n    }\n\n    /**\n     * Overrides the registration of Laravel's default email verification system\n     */\n    protected function configureEmailVerification(): void\n    {\n        //\n    }\n}\n"
  },
  {
    "path": "app/App/Providers/RouteServiceProvider.php",
    "content": "<?php\n\nnamespace BookStack\\App\\Providers;\n\nuse BookStack\\Facades\\Theme;\nuse BookStack\\Theming\\ThemeEvents;\nuse Illuminate\\Cache\\RateLimiting\\Limit;\nuse Illuminate\\Foundation\\Support\\Providers\\RouteServiceProvider as ServiceProvider;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Router;\nuse Illuminate\\Support\\Facades\\RateLimiter;\nuse Illuminate\\Support\\Facades\\Route;\n\nclass RouteServiceProvider extends ServiceProvider\n{\n    /**\n     * The path to the \"home\" route for your application.\n     *\n     * This is used by Laravel authentication to redirect users after login.\n     *\n     * @var string\n     */\n    public const HOME = '/';\n\n    /**\n     * Define your route model bindings, pattern filters, etc.\n     */\n    public function boot(): void\n    {\n        $this->configureRateLimiting();\n\n        $this->routes(function () {\n            $this->mapWebRoutes();\n            $this->mapApiRoutes();\n        });\n    }\n\n    /**\n     * Define the \"web\" routes for the application.\n     *\n     * These routes all receive session state, CSRF protection, etc.\n     */\n    protected function mapWebRoutes(): void\n    {\n        Route::group([\n            'middleware' => 'web',\n            'namespace'  => $this->namespace,\n        ], function (Router $router) {\n            require base_path('routes/web.php');\n            Theme::dispatch(ThemeEvents::ROUTES_REGISTER_WEB, $router);\n        });\n\n        Route::group([\n            'middleware' => ['web', 'auth'],\n        ], function (Router $router) {\n            Theme::dispatch(ThemeEvents::ROUTES_REGISTER_WEB_AUTH, $router);\n        });\n    }\n\n    /**\n     * Define the \"api\" routes for the application.\n     *\n     * These routes are typically stateless.\n     */\n    protected function mapApiRoutes(): void\n    {\n        Route::group([\n            'middleware' => 'api',\n            'namespace'  => $this->namespace . '\\Api',\n            'prefix'     => 'api',\n        ], function ($router) {\n            require base_path('routes/api.php');\n        });\n    }\n\n    /**\n     * Configure the rate limiters for the application.\n     */\n    protected function configureRateLimiting(): void\n    {\n        RateLimiter::for('api', function (Request $request) {\n            return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());\n        });\n\n        RateLimiter::for('public', function (Request $request) {\n            return Limit::perMinute(10)->by($request->ip());\n        });\n\n        RateLimiter::for('exports', function (Request $request) {\n            $user = user();\n            $attempts = $user->isGuest() ? 4 : 10;\n            $key = $user->isGuest() ? $request->ip() : $user->id;\n            return Limit::perMinute($attempts)->by($key);\n        });\n    }\n}\n"
  },
  {
    "path": "app/App/Providers/ThemeServiceProvider.php",
    "content": "<?php\n\nnamespace BookStack\\App\\Providers;\n\nuse BookStack\\Theming\\ThemeEvents;\nuse BookStack\\Theming\\ThemeService;\nuse BookStack\\Theming\\ThemeViews;\nuse Illuminate\\Support\\Facades\\Blade;\nuse Illuminate\\Support\\ServiceProvider;\n\nclass ThemeServiceProvider extends ServiceProvider\n{\n    /**\n     * Register services.\n     */\n    public function register(): void\n    {\n        // Register the ThemeService as a singleton\n        $this->app->singleton(ThemeService::class, fn ($app) => new ThemeService());\n    }\n\n    /**\n     * Bootstrap services.\n     */\n    public function boot(): void\n    {\n        // Boot up the theme system\n        $themeService = $this->app->make(ThemeService::class);\n        $viewFactory = $this->app->make('view');\n        $themeViews = new ThemeViews($viewFactory->getFinder());\n\n        // Use a custom include so that we can insert theme views before/after includes.\n        // This is done, even if no theme is active, so that view caching does not create problems\n        // when switching between themes or when switching a theme on/off.\n        $viewFactory->share('__themeViews', $themeViews);\n        Blade::directive('include', function ($expression) {\n            return \"<?php echo \\$__themeViews->handleViewInclude({$expression}, array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1])); ?>\";\n        });\n\n        if (!$themeService->getTheme()) {\n            return;\n        }\n\n        $themeService->loadModules();\n        $themeService->readThemeActions();\n        $themeService->dispatch(ThemeEvents::APP_BOOT, $this->app);\n\n        $themeViews->registerViewPathsForTheme($themeService->getModules());\n        $themeService->dispatch(ThemeEvents::THEME_REGISTER_VIEWS, $themeViews);\n    }\n}\n"
  },
  {
    "path": "app/App/Providers/TranslationServiceProvider.php",
    "content": "<?php\n\nnamespace BookStack\\App\\Providers;\n\nuse BookStack\\Translation\\FileLoader;\nuse BookStack\\Translation\\MessageSelector;\nuse Illuminate\\Translation\\TranslationServiceProvider as BaseProvider;\nuse Illuminate\\Translation\\Translator;\n\nclass TranslationServiceProvider extends BaseProvider\n{\n    /**\n     * Register the service provider.\n     */\n    public function register(): void\n    {\n        $this->registerLoader();\n\n        // This is a tweak upon Laravel's based translation service registration to allow\n        // usage of a custom MessageSelector class\n        $this->app->singleton('translator', function ($app) {\n            $loader = $app['translation.loader'];\n\n            // When registering the translator component, we'll need to set the default\n            // locale as well as the fallback locale. So, we'll grab the application\n            // configuration so we can easily get both of these values from there.\n            $locale = $app['config']['app.locale'];\n\n            $trans = new Translator($loader, $locale);\n            $trans->setFallback($app['config']['app.fallback_locale']);\n            $trans->setSelector(new MessageSelector());\n\n            return $trans;\n        });\n    }\n\n\n\n    /**\n     * Register the translation line loader.\n     * Overrides the default register action from Laravel so a custom loader can be used.\n     */\n    protected function registerLoader(): void\n    {\n        $this->app->singleton('translation.loader', function ($app) {\n            return new FileLoader($app['files'], $app['path.lang']);\n        });\n    }\n}\n"
  },
  {
    "path": "app/App/Providers/ValidationRuleServiceProvider.php",
    "content": "<?php\n\nnamespace BookStack\\App\\Providers;\n\nuse BookStack\\Uploads\\ImageService;\nuse Illuminate\\Support\\Facades\\Validator;\nuse Illuminate\\Support\\ServiceProvider;\n\nclass ValidationRuleServiceProvider extends ServiceProvider\n{\n    /**\n     * Register our custom validation rules when the application boots.\n     */\n    public function boot(): void\n    {\n        Validator::extend('image_extension', function ($attribute, $value, $parameters, $validator) {\n            $extension = strtolower($value->getClientOriginalExtension());\n\n            return ImageService::isExtensionSupported($extension);\n        });\n\n        Validator::extend('safe_url', function ($attribute, $value, $parameters, $validator) {\n            $cleanLinkName = strtolower(trim($value));\n            $isJs = str_starts_with($cleanLinkName, 'javascript:');\n            $isData = str_starts_with($cleanLinkName, 'data:');\n\n            return !$isJs && !$isData;\n        });\n    }\n}\n"
  },
  {
    "path": "app/App/Providers/ViewTweaksServiceProvider.php",
    "content": "<?php\n\nnamespace BookStack\\App\\Providers;\n\nuse BookStack\\Entities\\BreadcrumbsViewComposer;\nuse BookStack\\Util\\DateFormatter;\nuse Illuminate\\Pagination\\Paginator;\nuse Illuminate\\Support\\Facades\\Blade;\nuse Illuminate\\Support\\Facades\\View;\nuse Illuminate\\Support\\ServiceProvider;\n\nclass ViewTweaksServiceProvider extends ServiceProvider\n{\n    public function register()\n    {\n        $this->app->singleton(DateFormatter::class, function ($app) {\n            return new DateFormatter(\n                $app['config']->get('app.display_timezone'),\n            );\n        });\n    }\n\n    /**\n     * Bootstrap services.\n     */\n    public function boot(): void\n    {\n        // Set paginator to use bootstrap-style pagination\n        Paginator::useBootstrap();\n\n        // View Composers\n        View::composer('entities.breadcrumbs', BreadcrumbsViewComposer::class);\n\n        // View Globals\n        View::share('dates', $this->app->make(DateFormatter::class));\n\n        // Custom blade view directives\n        Blade::directive('icon', function ($expression) {\n            return \"<?php echo (new \\BookStack\\Util\\SvgIcon($expression))->toHtml(); ?>\";\n        });\n    }\n}\n"
  },
  {
    "path": "app/App/PwaManifestBuilder.php",
    "content": "<?php\n\nnamespace BookStack\\App;\n\nclass PwaManifestBuilder\n{\n    public function build(): array\n    {\n        // Note, while we attempt to use the user's preference here, the request to the manifest\n        // does not start a session, so we won't have current user context.\n        // This was attempted but removed since manifest calls could affect user session\n        // history tracking and back redirection.\n        // Context: https://github.com/BookStackApp/BookStack/issues/4649\n        $darkMode = (bool) setting()->getForCurrentUser('dark-mode-enabled');\n        $appName = setting('app-name');\n\n        return [\n            \"name\" => $appName,\n            \"short_name\" => $appName,\n            \"start_url\" => \"./\",\n            \"scope\" => \"/\",\n            \"display\" => \"standalone\",\n            \"background_color\" => $darkMode ? '#111111' : '#F2F2F2',\n            \"description\" => $appName,\n            \"theme_color\" => ($darkMode ? setting('app-color-dark') : setting('app-color')),\n            \"launch_handler\" => [\n                \"client_mode\" => \"focus-existing\"\n            ],\n            \"orientation\" => \"any\",\n            \"icons\" => [\n                [\n                    \"src\" => setting('app-icon-32') ?: url('/icon-32.png'),\n                    \"sizes\" => \"32x32\",\n                    \"type\" => \"image/png\"\n                ],\n                [\n                    \"src\" => setting('app-icon-64') ?: url('/icon-64.png'),\n                    \"sizes\" => \"64x64\",\n                    \"type\" => \"image/png\"\n                ],\n                [\n                    \"src\" => setting('app-icon-128') ?: url('/icon-128.png'),\n                    \"sizes\" => \"128x128\",\n                    \"type\" => \"image/png\"\n                ],\n                [\n                    \"src\" => setting('app-icon-180') ?: url('/icon-180.png'),\n                    \"sizes\" => \"180x180\",\n                    \"type\" => \"image/png\"\n                ],\n                [\n                    \"src\" => setting('app-icon') ?: url('/icon.png'),\n                    \"sizes\" => \"256x256\",\n                    \"type\" => \"image/png\"\n                ],\n                [\n                    \"src\" => url('favicon.ico'),\n                    \"sizes\" => \"48x48\",\n                    \"type\" => \"image/vnd.microsoft.icon\"\n                ],\n            ],\n        ];\n    }\n}\n"
  },
  {
    "path": "app/App/SluggableInterface.php",
    "content": "<?php\n\nnamespace BookStack\\App;\n\n/**\n * Assigned to models that can have slugs.\n * Must have the below properties.\n *\n * @property string $slug\n */\ninterface SluggableInterface\n{\n}\n"
  },
  {
    "path": "app/App/SystemApiController.php",
    "content": "<?php\n\nnamespace BookStack\\App;\n\nuse BookStack\\Http\\ApiController;\nuse Illuminate\\Http\\JsonResponse;\n\nclass SystemApiController extends ApiController\n{\n    /**\n     * Read details regarding the BookStack instance.\n     * Some details may be null where not set, like the app logo for example.\n     */\n    public function read(): JsonResponse\n    {\n        $logoSetting = setting('app-logo', '');\n        if ($logoSetting === 'none') {\n            $logo = null;\n        } else {\n            $logo = $logoSetting ? url($logoSetting) : url('/logo.png');\n        }\n\n        return response()->json([\n            'version' => AppVersion::get(),\n            'instance_id' => setting('instance-id'),\n            'app_name' => setting('app-name'),\n            'app_logo' => $logo,\n            'base_url' => url('/'),\n        ]);\n    }\n}\n"
  },
  {
    "path": "app/App/helpers.php",
    "content": "<?php\n\nuse BookStack\\App\\AppVersion;\nuse BookStack\\App\\Model;\nuse BookStack\\Facades\\Theme;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Permissions\\PermissionApplicator;\nuse BookStack\\Settings\\SettingService;\nuse BookStack\\Users\\Models\\User;\n\n/**\n * Get the path to a versioned file.\n *\n * @throws Exception\n */\nfunction versioned_asset(string $file = ''): string\n{\n    $version = AppVersion::get();\n\n    $additional = '';\n    if (config('app.env') === 'development') {\n        $additional = sha1_file(public_path($file));\n    }\n\n    $path = $file . '?version=' . urlencode($version) . $additional;\n\n    return url($path);\n}\n\n/**\n * Helper method to get the current User.\n * Defaults to public 'Guest' user if not logged in.\n */\nfunction user(): User\n{\n    return auth()->user() ?: User::getGuest();\n}\n\n/**\n * Check if the current user has a permission. If an ownable element\n * is passed in the jointPermissions are checked against that particular item.\n */\nfunction userCan(string|Permission $permission, ?Model $ownable = null): bool\n{\n    if (is_null($ownable)) {\n        return user()->can($permission);\n    }\n\n    // Check permission on ownable item\n    $permissions = app()->make(PermissionApplicator::class);\n\n    return $permissions->checkOwnableUserAccess($ownable, $permission);\n}\n\n/**\n * Check if the current user can perform the given action on any items in the system.\n * Can be provided the class name of an entity to filter ability to that specific entity type.\n */\nfunction userCanOnAny(string|Permission $action, string $entityClass = ''): bool\n{\n    $permissions = app()->make(PermissionApplicator::class);\n\n    return $permissions->checkUserHasEntityPermissionOnAny($action, $entityClass);\n}\n\n/**\n * Helper to access system settings.\n *\n * @return mixed|SettingService\n */\nfunction setting(?string $key = null, mixed $default = null): mixed\n{\n    $settingService = app()->make(SettingService::class);\n\n    if (is_null($key)) {\n        return $settingService;\n    }\n\n    return $settingService->get($key, $default);\n}\n\n/**\n * Get a path to a theme resource.\n * Returns null if a theme is not configured, and therefore a full path is not available for use.\n */\nfunction theme_path(string $path = ''): ?string\n{\n    $theme = Theme::getTheme();\n    if (!$theme) {\n        return null;\n    }\n\n    return base_path('themes/' . $theme . ($path ? DIRECTORY_SEPARATOR . $path : $path));\n}\n"
  },
  {
    "path": "app/Config/api.php",
    "content": "<?php\n\n/**\n * API configuration options.\n *\n * Changes to these config files are not supported by BookStack and may break upon updates.\n * Configuration should be altered via the `.env` file or environment variables.\n * Do not edit this file unless you're happy to maintain any changes yourself.\n */\n\nreturn [\n\n    // The default number of items that are returned in listing API requests.\n    // This count can often be overridden, up the the max option, per-request via request options.\n    'default_item_count' => env('API_DEFAULT_ITEM_COUNT', 100),\n\n    // The maximum number of items that can be returned in a listing API request.\n    'max_item_count' => env('API_MAX_ITEM_COUNT', 500),\n\n    // The number of API requests that can be made per minute by a single user.\n    'requests_per_minute' => env('API_REQUESTS_PER_MIN', 180),\n\n];\n"
  },
  {
    "path": "app/Config/app.php",
    "content": "<?php\n\n/**\n * Global app configuration options.\n *\n * Changes to these config files are not supported by BookStack and may break upon updates.\n * Configuration should be altered via the `.env` file or environment variables.\n * Do not edit this file unless you're happy to maintain any changes yourself.\n */\n\nuse Illuminate\\Support\\Facades\\Facade;\nuse Illuminate\\Support\\ServiceProvider;\n\nreturn [\n\n    // The environment to run BookStack in.\n    // Options: production, development, demo, testing\n    'env' => env('APP_ENV', 'production'),\n\n    // Enter the application in debug mode.\n    // Shows much more verbose error messages. Has potential to show\n    // private configuration variables so should remain disabled in public.\n    'debug' => env('APP_DEBUG', false),\n\n    // The number of revisions to keep in the database.\n    // Once this limit is reached older revisions will be deleted.\n    // If set to false then a limit will not be enforced.\n    'revision_limit' => env('REVISION_LIMIT', 100),\n\n    // The number of days that content will remain in the recycle bin before\n    // being considered for auto-removal. It is not a guarantee that content will\n    // be removed after this time.\n    // Set to 0 for no recycle bin functionality.\n    // Set to -1 for unlimited recycle bin lifetime.\n    'recycle_bin_lifetime' => env('RECYCLE_BIN_LIFETIME', 30),\n\n    // The limit for all uploaded files, including images and attachments in MB.\n    'upload_limit' => env('FILE_UPLOAD_SIZE_LIMIT', 50),\n\n    // Control the behaviour of content filtering, primarily used for page content.\n    // This setting is a string of characters which represent different available filters:\n    // - j - Filter out JavaScript and unknown binary data based content\n    // - h - Filter out unexpected, and potentially dangerous, HTML elements\n    // - f - Filter out unexpected form elements\n    // - a - Run content through a more complex allowlist filter\n    // This defaults to using all filters, unless ALLOW_CONTENT_SCRIPTS is set to true in which case no filters are used.\n    // Note: These filters are a best-attempt and may not be 100% effective. They are typically a layer used in addition to other security measures.\n    'content_filtering' => env('APP_CONTENT_FILTERING', env('ALLOW_CONTENT_SCRIPTS', false) === true ? '' : 'jhfa'),\n\n    // Allow server-side fetches to be performed to potentially unknown\n    // and user-provided locations. Primarily used in exports when loading\n    // in externally referenced assets.\n    'allow_untrusted_server_fetching' => env('ALLOW_UNTRUSTED_SERVER_FETCHING', false),\n\n    // Override the default behaviour for allowing crawlers to crawl the instance.\n    // May be ignored if the underlying view has been overridden or modified.\n    // Defaults to null in which case the 'app-public' status is used instead.\n    'allow_robots' => env('ALLOW_ROBOTS', null),\n\n    // Application Base URL, Used by laravel in development commands\n    // and used by BookStack in URL generation.\n    'url' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''),\n\n    // A list of hosts that BookStack can be iframed within.\n    // Space separated if multiple. BookStack host domain is auto-inferred.\n    'iframe_hosts' => env('ALLOWED_IFRAME_HOSTS', null),\n\n    // A list of sources/hostnames that can be loaded within iframes within BookStack.\n    // Space separated if multiple. BookStack host domain is auto-inferred.\n    // Can be set to a lone \"*\" to allow all sources for iframe content (Not advised).\n    // Defaults to a set of common services.\n    // Current host and source for the \"DRAWIO\" setting will be auto-appended to the sources configured.\n    'iframe_sources' => env('ALLOWED_IFRAME_SOURCES', 'https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com'),\n\n    // A list of the sources/hostnames that can be reached by application SSR calls.\n    // This is used wherever users can provide URLs/hosts in-platform, like for webhooks.\n    // Host-specific functionality (usually controlled via other options) like auth\n    // or user avatars, for example, won't use this list.\n    // Space separated if multiple. Can use '*' as a wildcard.\n    // Values will be compared prefix-matched, case-insensitive, against called SSR urls.\n    // Defaults to allow all hosts.\n    'ssr_hosts' => env('ALLOWED_SSR_HOSTS', '*'),\n\n    // Alter the precision of IP addresses stored by BookStack.\n    // Integer value between 0 (IP hidden) to 4 (Full IP usage)\n    'ip_address_precision' => env('IP_ADDRESS_PRECISION', 4),\n\n    // Application timezone for stored date/time values.\n    'timezone' => env('APP_TIMEZONE', 'UTC'),\n    // Application timezone for displayed date/time values in the UI.\n    'display_timezone' => env('APP_DISPLAY_TIMEZONE', env('APP_TIMEZONE', 'UTC')),\n\n    // Default locale to use\n    // A default variant is also stored since Laravel can overwrite\n    // app.locale when dynamically setting the locale in-app.\n    'locale' => env('APP_LANG', 'en'),\n    'default_locale' => env('APP_LANG', 'en'),\n\n    //  Application Fallback Locale\n    'fallback_locale' => 'en',\n\n    // Faker Locale\n    'faker_locale' => 'en_GB',\n\n    // Auto-detect the locale for public users\n    // For public users their locale can be guessed by headers sent by their\n    // browser. This is usually set by users in their browser settings.\n    // If not found the default app locale will be used.\n    'auto_detect_locale' => env('APP_AUTO_LANG_PUBLIC', true),\n\n    // Encryption key\n    'key' => env('APP_KEY', 'AbAZchsay4uBTU33RubBzLKw203yqSqr'),\n\n    // Encryption cipher\n    'cipher' => 'AES-256-CBC',\n\n    // Maintenance Mode Driver\n    'maintenance' => [\n        'driver' => 'file',\n        // 'store'  => 'redis',\n    ],\n\n    // Application Service Providers\n    'providers' => ServiceProvider::defaultProviders()->merge([\n        // Third party service providers\n        SocialiteProviders\\Manager\\ServiceProvider::class,\n\n        // BookStack custom service providers\n        BookStack\\App\\Providers\\ThemeServiceProvider::class,\n        BookStack\\App\\Providers\\AppServiceProvider::class,\n        BookStack\\App\\Providers\\AuthServiceProvider::class,\n        BookStack\\App\\Providers\\EventServiceProvider::class,\n        BookStack\\App\\Providers\\RouteServiceProvider::class,\n        BookStack\\App\\Providers\\TranslationServiceProvider::class,\n        BookStack\\App\\Providers\\ValidationRuleServiceProvider::class,\n        BookStack\\App\\Providers\\ViewTweaksServiceProvider::class,\n    ])->toArray(),\n\n    // Class Aliases\n    // This array of class aliases to be registered on application start.\n    'aliases' => Facade::defaultAliases()->merge([\n        // Laravel Packages\n        'Socialite'    => Laravel\\Socialite\\Facades\\Socialite::class,\n\n        // Custom BookStack\n        'Activity'    => BookStack\\Facades\\Activity::class,\n        'Theme'       => BookStack\\Facades\\Theme::class,\n    ])->toArray(),\n\n    // Proxy configuration\n    'proxies' => env('APP_PROXIES', ''),\n\n];\n"
  },
  {
    "path": "app/Config/auth.php",
    "content": "<?php\n\n/**\n * Authentication configuration options.\n *\n * Changes to these config files are not supported by BookStack and may break upon updates.\n * Configuration should be altered via the `.env` file or environment variables.\n * Do not edit this file unless you're happy to maintain any changes yourself.\n */\n\nreturn [\n\n    // Options: standard, ldap, saml2, oidc\n    'method' => env('AUTH_METHOD', 'standard'),\n\n    // Automatically initiate login via external auth system if it's the sole auth method.\n    // Works with saml2 or oidc auth methods.\n    'auto_initiate' => env('AUTH_AUTO_INITIATE', false),\n\n    // Authentication Defaults\n    // This option controls the default authentication \"guard\" and password\n    // reset options for your application.\n    'defaults' => [\n        'guard'     => env('AUTH_METHOD', 'standard'),\n        'passwords' => 'users',\n    ],\n\n    // Authentication Guards\n    // All authentication drivers have a user provider. This defines how the\n    // users are actually retrieved out of your database or other storage\n    // mechanisms used by this application to persist your user's data.\n    // Supported drivers: \"session\", \"api-token\", \"ldap-session\", \"async-external-session\"\n    'guards' => [\n        'standard' => [\n            'driver'   => 'session',\n            'provider' => 'users',\n        ],\n        'ldap' => [\n            'driver'   => 'ldap-session',\n            'provider' => 'external',\n        ],\n        'saml2' => [\n            'driver'   => 'async-external-session',\n            'provider' => 'external',\n        ],\n        'oidc' => [\n            'driver'   => 'async-external-session',\n            'provider' => 'external',\n        ],\n        'api' => [\n            'driver'   => 'api-token',\n        ],\n    ],\n\n    // User Providers\n    // All authentication drivers have a user provider. This defines how the\n    // users are actually retrieved out of your database or other storage\n    // mechanisms used by this application to persist your user's data.\n    'providers' => [\n        'users' => [\n            'driver' => 'eloquent',\n            'model'  => \\BookStack\\Users\\Models\\User::class,\n        ],\n\n        'external' => [\n            'driver' => 'external-users',\n            'model'  => \\BookStack\\Users\\Models\\User::class,\n        ],\n\n        // 'users' => [\n        //     'driver' => 'database',\n        //     'table' => 'users',\n        // ],\n    ],\n\n    // Resetting Passwords\n    // The expire time is the number of minutes that the reset token should be\n    // considered valid. This security feature keeps tokens short-lived so\n    // they have less time to be guessed. You may change this as needed.\n    'passwords' => [\n        'users' => [\n            'provider' => 'users',\n            'email'    => 'emails.password',\n            'table'    => 'password_resets',\n            'expire'   => 60,\n            'throttle' => 60,\n        ],\n    ],\n\n    // Password Confirmation Timeout\n    // Here you may define the amount of seconds before a password confirmation\n    // times out and the user is prompted to re-enter their password via the\n    // confirmation screen. By default, the timeout lasts for three hours.\n    'password_timeout' => 10800,\n\n];\n"
  },
  {
    "path": "app/Config/cache.php",
    "content": "<?php\n\nuse Illuminate\\Support\\Str;\n\n/**\n * Caching configuration options.\n *\n * Changes to these config files are not supported by BookStack and may break upon updates.\n * Configuration should be altered via the `.env` file or environment variables.\n * Do not edit this file unless you're happy to maintain any changes yourself.\n */\n\n// MEMCACHED - Split out configuration into an array\nif (env('CACHE_DRIVER') === 'memcached') {\n    $memcachedServerKeys = ['host', 'port', 'weight'];\n    $memcachedServers = explode(',', trim(env('MEMCACHED_SERVERS', '127.0.0.1:11211:100'), ','));\n    foreach ($memcachedServers as $index => $memcachedServer) {\n        $memcachedServerDetails = explode(':', $memcachedServer);\n        if (count($memcachedServerDetails) < 2) {\n            $memcachedServerDetails[] = '11211';\n        }\n        if (count($memcachedServerDetails) < 3) {\n            $memcachedServerDetails[] = '100';\n        }\n        $memcachedServers[$index] = array_combine($memcachedServerKeys, $memcachedServerDetails);\n    }\n}\n\nreturn [\n\n    // Default cache store to use\n    // Can be overridden at cache call-time\n    'default' => env('CACHE_DRIVER', 'file'),\n\n    // Available caches stores\n    'stores' => [\n\n        'array' => [\n            'driver'    => 'array',\n            'serialize' => false,\n        ],\n\n        'database' => [\n            'driver'          => 'database',\n            'table'           => 'cache',\n            'connection'      => null,\n            'lock_connection' => null,\n            'lock_table'      => null,\n        ],\n\n        'file' => [\n            'driver' => 'file',\n            'path'   => storage_path('framework/cache'),\n            'lock_path' => storage_path('framework/cache'),\n        ],\n\n        'memcached' => [\n            'driver'        => 'memcached',\n            'options'       => [\n                // Memcached::OPT_CONNECT_TIMEOUT => 2000,\n            ],\n            'servers' => $memcachedServers ?? [],\n        ],\n\n        'redis' => [\n            'driver'          => 'redis',\n            'connection'      => 'default',\n            'lock_connection' => 'default',\n        ],\n\n        'octane' => [\n            'driver' => 'octane',\n        ],\n\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Cache Key Prefix\n    |--------------------------------------------------------------------------\n    |\n    | When utilizing a RAM based store such as APC or Memcached, there might\n    | be other applications utilizing the same cache. So, we'll specify a\n    | value to get prefixed to all our keys so we can avoid collisions.\n    |\n    */\n\n    'prefix' => env('CACHE_PREFIX', 'bookstack_cache_'),\n\n];\n"
  },
  {
    "path": "app/Config/clockwork.php",
    "content": "<?php\n\nreturn [\n\n    /*\n    |------------------------------------------------------------------------------------------------------------------\n    | Enable Clockwork\n    |------------------------------------------------------------------------------------------------------------------\n    |\n    | Clockwork is enabled by default only when your application is in debug mode. Here you can explicitly enable or\n    | disable Clockwork. When disabled, no data is collected and the api and web ui are inactive.\n    |\n    */\n\n    'enable' => env('CLOCKWORK_ENABLE', false),\n\n    /*\n    |------------------------------------------------------------------------------------------------------------------\n    | Features\n    |------------------------------------------------------------------------------------------------------------------\n    |\n    | You can enable or disable various Clockwork features here. Some features have additional settings (eg. slow query\n    | threshold for database queries).\n    |\n    */\n\n    'features' => [\n\n        // Cache usage stats and cache queries including results\n        'cache' => [\n            'enabled' => true,\n\n            // Collect cache queries\n            'collect_queries' => true,\n\n            // Collect values from cache queries (high performance impact with a very high number of queries)\n            'collect_values' => false,\n        ],\n\n        // Database usage stats and queries\n        'database' => [\n            'enabled' => true,\n\n            // Collect database queries (high performance impact with a very high number of queries)\n            'collect_queries' => true,\n\n            // Collect details of models updates (high performance impact with a lot of model updates)\n            'collect_models_actions' => true,\n\n            // Collect details of retrieved models (very high performance impact with a lot of models retrieved)\n            'collect_models_retrieved' => false,\n\n            // Query execution time threshold in miliseconds after which the query will be marked as slow\n            'slow_threshold' => null,\n\n            // Collect only slow database queries\n            'slow_only' => false,\n\n            // Detect and report duplicate (N+1) queries\n            'detect_duplicate_queries' => false,\n        ],\n\n        // Dispatched events\n        'events' => [\n            'enabled' => true,\n\n            // Ignored events (framework events are ignored by default)\n            'ignored_events' => [\n                // App\\Events\\UserRegistered::class,\n                // 'user.registered'\n            ],\n        ],\n\n        // Laravel log (you can still log directly to Clockwork with laravel log disabled)\n        'log' => [\n            'enabled' => true,\n        ],\n\n        // Sent notifications\n        'notifications' => [\n            'enabled' => true,\n        ],\n\n        // Performance metrics\n        'performance' => [\n            // Allow collecting of client metrics. Requires separate clockwork-browser npm package.\n            'client_metrics' => true,\n        ],\n\n        // Dispatched queue jobs\n        'queue' => [\n            'enabled' => true,\n        ],\n\n        // Redis commands\n        'redis' => [\n            'enabled' => true,\n        ],\n\n        // Routes list\n        'routes' => [\n            'enabled' => false,\n\n            // Collect only routes from particular namespaces (only application routes by default)\n            'only_namespaces' => ['App'],\n        ],\n\n        // Rendered views\n        'views' => [\n            'enabled' => true,\n\n            // Collect views including view data (high performance impact with a high number of views)\n            'collect_data' => false,\n\n            // Use Twig profiler instead of Laravel events for apps using laravel-twigbridge (more precise, but does\n            // not support collecting view data)\n            'use_twig_profiler' => false,\n        ],\n\n    ],\n\n    /*\n    |------------------------------------------------------------------------------------------------------------------\n    | Enable web UI\n    |------------------------------------------------------------------------------------------------------------------\n    |\n    | Clockwork comes with a web UI accessibla via http://your.app/clockwork. Here you can enable or disable this\n    | feature. You can also set a custom path for the web UI.\n    |\n    */\n\n    'web' => true,\n\n    /*\n    |------------------------------------------------------------------------------------------------------------------\n    | Enable toolbar\n    |------------------------------------------------------------------------------------------------------------------\n    |\n    | Clockwork can show a toolbar with basic metrics on all responses. Here you can enable or disable this feature.\n    | Requires a separate clockwork-browser npm library.\n    | For installation instructions see https://underground.works/clockwork/#docs-viewing-data\n    |\n    */\n\n    'toolbar' => true,\n\n    /*\n    |------------------------------------------------------------------------------------------------------------------\n    | HTTP requests collection\n    |------------------------------------------------------------------------------------------------------------------\n    |\n    | Clockwork collects data about HTTP requests to your app. Here you can choose which requests should be collected.\n    |\n    */\n\n    'requests' => [\n        // With on-demand mode enabled, Clockwork will only profile requests when the browser extension is open or you\n        // manually pass a \"clockwork-profile\" cookie or get/post data key.\n        // Optionally you can specify a \"secret\" that has to be passed as the value to enable profiling.\n        'on_demand' => false,\n\n        // Collect only errors (requests with HTTP 4xx and 5xx responses)\n        'errors_only' => false,\n\n        // Response time threshold in miliseconds after which the request will be marked as slow\n        'slow_threshold' => null,\n\n        // Collect only slow requests\n        'slow_only' => false,\n\n        // Sample the collected requests (eg. set to 100 to collect only 1 in 100 requests)\n        'sample' => false,\n\n        // List of URIs that should not be collected\n        'except' => [\n            '/uploads/images/.*', // BookStack image requests\n\n            '/horizon/.*', // Laravel Horizon requests\n            '/telescope/.*', // Laravel Telescope requests\n            '/_debugbar/.*', // Laravel DebugBar requests\n        ],\n\n        // List of URIs that should be collected, any other URI will not be collected if not empty\n        'only' => [\n            // '/api/.*'\n        ],\n\n        // Don't collect OPTIONS requests, mostly used in the CSRF pre-flight requests and are rarely of interest\n        'except_preflight' => true,\n    ],\n\n    /*\n    |------------------------------------------------------------------------------------------------------------------\n    | Artisan commands collection\n    |------------------------------------------------------------------------------------------------------------------\n    |\n    | Clockwork can collect data about executed artisan commands. Here you can enable and configure which commands\n    | should be collected.\n    |\n    */\n\n    'artisan' => [\n        // Enable or disable collection of executed Artisan commands\n        'collect' => false,\n\n        // List of commands that should not be collected (built-in commands are not collected by default)\n        'except' => [\n            // 'inspire'\n        ],\n\n        // List of commands that should be collected, any other command will not be collected if not empty\n        'only' => [\n            // 'inspire'\n        ],\n\n        // Enable or disable collection of command output\n        'collect_output' => false,\n\n        // Enable or disable collection of built-in Laravel commands\n        'except_laravel_commands' => true,\n    ],\n\n    /*\n    |------------------------------------------------------------------------------------------------------------------\n    | Queue jobs collection\n    |------------------------------------------------------------------------------------------------------------------\n    |\n    | Clockwork can collect data about executed queue jobs. Here you can enable and configure which queue jobs should\n    | be collected.\n    |\n    */\n\n    'queue' => [\n        // Enable or disable collection of executed queue jobs\n        'collect' => false,\n\n        // List of queue jobs that should not be collected\n        'except' => [\n            // App\\Jobs\\ExpensiveJob::class\n        ],\n\n        // List of queue jobs that should be collected, any other queue job will not be collected if not empty\n        'only' => [\n            // App\\Jobs\\BuggyJob::class\n        ],\n    ],\n\n    /*\n    |------------------------------------------------------------------------------------------------------------------\n    | Tests collection\n    |------------------------------------------------------------------------------------------------------------------\n    |\n    | Clockwork can collect data about executed tests. Here you can enable and configure which tests should be\n    | collected.\n    |\n    */\n\n    'tests' => [\n        // Enable or disable collection of ran tests\n        'collect' => false,\n\n        // List of tests that should not be collected\n        'except' => [\n            // Tests\\Unit\\ExampleTest::class\n        ],\n    ],\n\n    /*\n    |------------------------------------------------------------------------------------------------------------------\n    | Enable data collection when Clockwork is disabled\n    |------------------------------------------------------------------------------------------------------------------\n    |\n    | You can enable this setting to collect data even when Clockwork is disabled. Eg. for future analysis.\n    |\n    */\n\n    'collect_data_always' => false,\n\n    /*\n    |------------------------------------------------------------------------------------------------------------------\n    | Metadata storage\n    |------------------------------------------------------------------------------------------------------------------\n    |\n    | Configure how is the metadata collected by Clockwork stored. Two options are available:\n    |   - files - A simple fast storage implementation storing data in one-per-request files.\n    |   - sql - Stores requests in a sql database. Supports MySQL, Postgresql, Sqlite and requires PDO.\n    |\n    */\n\n    'storage' => 'files',\n\n    // Path where the Clockwork metadata is stored\n    'storage_files_path' => storage_path('clockwork'),\n\n    // Compress the metadata files using gzip, trading a little bit of performance for lower disk usage\n    'storage_files_compress' => false,\n\n    // SQL database to use, can be a name of database configured in database.php or a path to a sqlite file\n    'storage_sql_database' => storage_path('clockwork.sqlite'),\n\n    // SQL table name to use, the table is automatically created and udpated when needed\n    'storage_sql_table' => 'clockwork',\n\n    // Maximum lifetime of collected metadata in minutes, older requests will automatically be deleted, false to disable\n    'storage_expiration' => 60 * 24 * 7,\n\n    /*\n    |------------------------------------------------------------------------------------------------------------------\n    | Authentication\n    |------------------------------------------------------------------------------------------------------------------\n    |\n    | Clockwork can be configured to require authentication before allowing access to the collected data. This might be\n    | useful when the application is publicly accessible. Setting to true will enable a simple authentication with a\n    | pre-configured password. You can also pass a class name of a custom implementation.\n    |\n    */\n\n    'authentication' => false,\n\n    // Password for the simple authentication\n    'authentication_password' => 'VerySecretPassword',\n\n    /*\n    |------------------------------------------------------------------------------------------------------------------\n    | Stack traces collection\n    |------------------------------------------------------------------------------------------------------------------\n    |\n    | Clockwork can collect stack traces for log messages and certain data like database queries. Here you can set\n    | whether to collect stack traces, limit the number of collected frames and set further configuration. Collecting\n    | long stack traces considerably increases metadata size.\n    |\n    */\n\n    'stack_traces' => [\n        // Enable or disable collecting of stack traces\n        'enabled' => true,\n\n        // Limit the number of frames to be collected\n        'limit' => 10,\n\n        // List of vendor names to skip when determining caller, common vendors are automatically added\n        'skip_vendors' => [\n            // 'phpunit'\n        ],\n\n        // List of namespaces to skip when determining caller\n        'skip_namespaces' => [\n            // 'Laravel'\n        ],\n\n        // List of class names to skip when determining caller\n        'skip_classes' => [\n            // App\\CustomLog::class\n        ],\n\n    ],\n\n    /*\n    |------------------------------------------------------------------------------------------------------------------\n    | Serialization\n    |------------------------------------------------------------------------------------------------------------------\n    |\n    | Clockwork serializes the collected data to json for storage and transfer. Here you can configure certain aspects\n    | of serialization. Serialization has a large effect on the cpu time and memory usage.\n    |\n    */\n\n    // Maximum depth of serialized multi-level arrays and objects\n    'serialization_depth' => 10,\n\n    // A list of classes that will never be serialized (eg. a common service container class)\n    'serialization_blackbox' => [\n        \\Illuminate\\Container\\Container::class,\n        \\Illuminate\\Foundation\\Application::class,\n    ],\n\n    /*\n    |------------------------------------------------------------------------------------------------------------------\n    | Register helpers\n    |------------------------------------------------------------------------------------------------------------------\n    |\n    | Clockwork comes with a \"clock\" global helper function. You can use this helper to quickly log something and to\n    | access the Clockwork instance.\n    |\n    */\n\n    'register_helpers' => true,\n\n    /*\n    |------------------------------------------------------------------------------------------------------------------\n    | Send Headers for AJAX request\n    |------------------------------------------------------------------------------------------------------------------\n    |\n    | When trying to collect data the AJAX method can sometimes fail if it is missing required headers. For example, an\n    | API might require a version number using Accept headers to route the HTTP request to the correct codebase.\n    |\n    */\n\n    'headers' => [\n        // 'Accept' => 'application/vnd.com.whatever.v1+json',\n    ],\n\n    /*\n    |------------------------------------------------------------------------------------------------------------------\n    | Server-Timing\n    |------------------------------------------------------------------------------------------------------------------\n    |\n    | Clockwork supports the W3C Server Timing specification, which allows for collecting a simple performance metrics\n    | in a cross-browser way. Eg. in Chrome, your app, database and timeline event timings will be shown in the Dev\n    | Tools network tab. This setting specifies the max number of timeline events that will be sent. Setting to false\n    | will disable the feature.\n    |\n    */\n\n    'server_timing' => 10,\n\n];\n"
  },
  {
    "path": "app/Config/database.php",
    "content": "<?php\n\n/**\n * Database configuration options.\n *\n * Changes to these config files are not supported by BookStack and may break upon updates.\n * Configuration should be altered via the `.env` file or environment variables.\n * Do not edit this file unless you're happy to maintain any changes yourself.\n */\n\n// REDIS\n// Split out configuration into an array\nif (env('REDIS_SERVERS', false)) {\n    $redisDefaults = ['host' => '127.0.0.1', 'port' => '6379', 'database' => '0', 'password' => null];\n    $redisServers = explode(',', trim(env('REDIS_SERVERS', '127.0.0.1:6379:0'), ','));\n    $redisConfig = ['client' => 'predis'];\n    $cluster = count($redisServers) > 1;\n\n    if ($cluster) {\n        $redisConfig['clusters'] = ['default' => []];\n    }\n\n    foreach ($redisServers as $index => $redisServer) {\n        $redisServerDetails = explode(':', $redisServer);\n\n        $serverConfig = [];\n        $configIndex = 0;\n        foreach ($redisDefaults as $configKey => $configDefault) {\n            $serverConfig[$configKey] = ($redisServerDetails[$configIndex] ?? $configDefault);\n            $configIndex++;\n        }\n\n        if ($cluster) {\n            $redisConfig['clusters']['default'][] = $serverConfig;\n        } else {\n            $redisConfig['default'] = $serverConfig;\n        }\n    }\n}\n\n// MYSQL\n// Split out port from host if set\n$mysqlHost = env('DB_HOST', 'localhost');\n$mysqlHostExploded = explode(':', $mysqlHost);\n$mysqlPort = env('DB_PORT', 3306);\n$mysqlHostIpv6 = str_starts_with($mysqlHost, '[');\nif ($mysqlHostIpv6 && str_contains($mysqlHost, ']:')) {\n    $mysqlHost = implode(':', array_slice($mysqlHostExploded, 0, -1));\n    $mysqlPort = intval(end($mysqlHostExploded));\n} else if (!$mysqlHostIpv6 && count($mysqlHostExploded) > 1) {\n    $mysqlHost = $mysqlHostExploded[0];\n    $mysqlPort = intval($mysqlHostExploded[1]);\n}\n\nreturn [\n\n    // Default database connection name.\n    // Options: mysql, mysql_testing\n    'default' => env('DB_CONNECTION', 'mysql'),\n\n    // Available database connections\n    // Many of those shown here are unsupported by BookStack.\n    'connections' => [\n\n        'mysql' => [\n            'driver'         => 'mysql',\n            'url'            => env('DATABASE_URL'),\n            'host'           => $mysqlHost,\n            'database'       => env('DB_DATABASE', 'forge'),\n            'username'       => env('DB_USERNAME', 'forge'),\n            'password'       => env('DB_PASSWORD', ''),\n            'unix_socket'    => env('DB_SOCKET', ''),\n            'port'           => $mysqlPort,\n            'charset'        => 'utf8mb4',\n            'collation'      => 'utf8mb4_unicode_ci',\n            // Prefixes are only semi-supported and may be unstable\n            // since they are not tested as part of our automated test suite.\n            // If used, the prefix should not be changed; otherwise you will likely receive errors.\n            'prefix'         => env('DB_TABLE_PREFIX', ''),\n            'prefix_indexes' => true,\n            'strict'         => false,\n            'engine'         => null,\n            'options'        => extension_loaded('pdo_mysql') ? array_filter([\n                // @phpstan-ignore class.notFound\n                (PHP_VERSION_ID >= 80500 ? \\Pdo\\Mysql::ATTR_SSL_CA : \\PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),\n            ]) : [],\n        ],\n\n        'mysql_testing' => [\n            'driver'         => 'mysql',\n            'url'            => env('TEST_DATABASE_URL'),\n            'host'           => '127.0.0.1',\n            'database'       => 'bookstack-test',\n            'username'       => env('MYSQL_USER', 'bookstack-test'),\n            'password'       => env('MYSQL_PASSWORD', 'bookstack-test'),\n            'port'           => $mysqlPort,\n            'charset'        => 'utf8mb4',\n            'collation'      => 'utf8mb4_unicode_ci',\n            'prefix'         => '',\n            'prefix_indexes' => true,\n            'strict'         => false,\n        ],\n\n    ],\n\n    // Migration Repository Table\n    // This table keeps track of all the migrations that have already run for the application.\n    'migrations' => 'migrations',\n\n    // Redis configuration to use if set\n    'redis' => $redisConfig ?? [],\n\n];\n"
  },
  {
    "path": "app/Config/debugbar.php",
    "content": "<?php\n\n/**\n * Debugbar Configuration Options.\n *\n * Changes to these config files are not supported by BookStack and may break upon updates.\n * Configuration should be altered via the `.env` file or environment variables.\n * Do not edit this file unless you're happy to maintain any changes yourself.\n */\n\nreturn [\n\n    // Debugbar is enabled by default, when debug is set to true in app.php.\n    // You can override the value by setting enable to true or false instead of null.\n    //\n    // You can provide an array of URI's that must be ignored (eg. 'api/*')\n    'enabled' => env('DEBUGBAR_ENABLED', false),\n    'except'  => [\n        'telescope*',\n    ],\n\n    // DebugBar stores data for session/ajax requests.\n    // You can disable this, so the debugbar stores data in headers/session,\n    // but this can cause problems with large data collectors.\n    // By default, file storage (in the storage folder) is used. Redis and PDO\n    // can also be used. For PDO, run the package migrations first.\n    'storage' => [\n        'enabled'    => true,\n        'driver'     => 'file', // redis, file, pdo, custom\n        'path'       => storage_path('debugbar'), // For file driver\n        'connection' => null,   // Leave null for default connection (Redis/PDO)\n        'provider'   => '', // Instance of StorageInterface for custom driver\n    ],\n\n    // Vendor files are included by default, but can be set to false.\n    // This can also be set to 'js' or 'css', to only include javascript or css vendor files.\n    // Vendor files are for css: font-awesome (including fonts) and highlight.js (css files)\n    // and for js: jquery and and highlight.js\n    // So if you want syntax highlighting, set it to true.\n    // jQuery is set to not conflict with existing jQuery scripts.\n    'include_vendors' => true,\n\n    // The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors),\n    // you can use this option to disable sending the data through the headers.\n    // Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools.\n\n    'capture_ajax'    => true,\n    'add_ajax_timing' => false,\n\n    // When enabled, the Debugbar shows deprecated warnings for Symfony components\n    // in the Messages tab.\n    'error_handler' => false,\n\n    // The Debugbar can emulate the Clockwork headers, so you can use the Chrome\n    // Extension, without the server-side code. It uses Debugbar collectors instead.\n    'clockwork' => false,\n\n    // Enable/disable DataCollectors\n    'collectors' => [\n        'phpinfo'         => true,  // Php version\n        'messages'        => true,  // Messages\n        'time'            => true,  // Time Datalogger\n        'memory'          => true,  // Memory usage\n        'exceptions'      => true,  // Exception displayer\n        'log'             => true,  // Logs from Monolog (merged in messages if enabled)\n        'db'              => true,  // Show database (PDO) queries and bindings\n        'views'           => true,  // Views with their data\n        'route'           => true,  // Current route information\n        'auth'            => true, // Display Laravel authentication status\n        'gate'            => true, // Display Laravel Gate checks\n        'session'         => true,  // Display session data\n        'symfony_request' => true,  // Only one can be enabled..\n        'mail'            => true,  // Catch mail messages\n        'laravel'         => false, // Laravel version and environment\n        'events'          => false, // All events fired\n        'default_request' => false, // Regular or special Symfony request logger\n        'logs'            => false, // Add the latest log messages\n        'files'           => false, // Show the included files\n        'config'          => false, // Display config settings\n        'cache'           => false, // Display cache events\n        'models'          => true, // Display models\n    ],\n\n    // Configure some DataCollectors\n    'options' => [\n        'auth' => [\n            'show_name' => true,   // Also show the users name/email in the debugbar\n        ],\n        'db' => [\n            'with_params'       => true,   // Render SQL with the parameters substituted\n            'backtrace'         => true,   // Use a backtrace to find the origin of the query in your files.\n            'timeline'          => false,  // Add the queries to the timeline\n            'explain'           => [                 // Show EXPLAIN output on queries\n                'enabled' => false,\n                'types'   => ['SELECT'],     // ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; for MySQL 5.6.3+\n            ],\n            'hints'             => true,    // Show hints for common mistakes\n        ],\n        'mail' => [\n            'full_log' => false,\n        ],\n        'views' => [\n            'data' => false,    //Note: Can slow down the application, because the data can be quite large..\n        ],\n        'route' => [\n            'label' => true,  // show complete route on bar\n        ],\n        'logs' => [\n            'file' => null,\n        ],\n        'cache' => [\n            'values' => true, // collect cache values\n        ],\n    ],\n\n    // Inject Debugbar into the response\n    // Usually, the debugbar is added just before </body>, by listening to the\n    // Response after the App is done. If you disable this, you have to add them\n    // in your template yourself. See http://phpdebugbar.com/docs/rendering.html\n    'inject' => true,\n\n    // DebugBar route prefix\n    // Sometimes you want to set route prefix to be used by DebugBar to load\n    // its resources from. Usually the need comes from misconfigured web server or\n    // from trying to overcome bugs like this: http://trac.nginx.org/nginx/ticket/97\n    'route_prefix' => '_debugbar',\n\n    // DebugBar route domain\n    // By default DebugBar route served from the same domain that request served.\n    // To override default domain, specify it as a non-empty value.\n    'route_domain' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''),\n];\n"
  },
  {
    "path": "app/Config/exports.php",
    "content": "<?php\n\n/**\n * Export configuration options.\n *\n * Changes to these config files are not supported by BookStack and may break upon updates.\n * Configuration should be altered via the `.env` file or environment variables.\n * Do not edit this file unless you're happy to maintain any changes yourself.\n */\n\n$snappyPaperSizeMap = [\n    'a4'     => 'A4',\n    'letter' => 'Letter',\n];\n\n$dompdfPaperSizeMap = [\n    'a4'     => 'a4',\n    'letter' => 'letter',\n];\n\n$exportPageSize = env('EXPORT_PAGE_SIZE', 'a4');\n\nreturn [\n\n    // Set a command which can be used to convert a HTML file into a PDF file.\n    // When false this will not be used.\n    // String values represent the command to be called for conversion.\n    // Supports '{input_html_path}' and '{output_pdf_path}' placeholder values.\n    // Example: EXPORT_PDF_COMMAND=\"/scripts/convert.sh {input_html_path} {output_pdf_path}\"\n    'pdf_command' => env('EXPORT_PDF_COMMAND', false),\n\n    // The amount of time allowed for PDF generation command to run\n    // before the process times out and is stopped.\n    'pdf_command_timeout' => env('EXPORT_PDF_COMMAND_TIMEOUT', 15),\n\n    // 2024-04: Snappy/WKHTMLtoPDF now considered deprecated in regard to BookStack support.\n    'snappy' => [\n        'pdf_binary' => env('WKHTMLTOPDF', false),\n        'options' => [\n            'print-media-type' => true,\n            'outline'   => true,\n            'page-size' => $snappyPaperSizeMap[$exportPageSize] ?? 'A4',\n        ],\n    ],\n\n    'dompdf' => [\n        /**\n         * The location of the DOMPDF font directory.\n         *\n         * The location of the directory where DOMPDF will store fonts and font metrics\n         * Note: This directory must exist and be writable by the webserver process.\n         * *Please note the trailing slash.*\n         *\n         * Notes regarding fonts:\n         * Additional .afm font metrics can be added by executing load_font.php from command line.\n         *\n         * Only the original \"Base 14 fonts\" are present on all pdf viewers. Additional fonts must\n         * be embedded in the pdf file or the PDF may not display correctly. This can significantly\n         * increase file size unless font subsetting is enabled. Before embedding a font please\n         * review your rights under the font license.\n         *\n         * Any font specification in the source HTML is translated to the closest font available\n         * in the font directory.\n         *\n         * The pdf standard \"Base 14 fonts\" are:\n         * Courier, Courier-Bold, Courier-BoldOblique, Courier-Oblique,\n         * Helvetica, Helvetica-Bold, Helvetica-BoldOblique, Helvetica-Oblique,\n         * Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic,\n         * Symbol, ZapfDingbats.\n         */\n        'font_dir' => storage_path('fonts/'),  // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)\n\n        /**\n         * The location of the DOMPDF font cache directory.\n         *\n         * This directory contains the cached font metrics for the fonts used by DOMPDF.\n         * This directory can be the same as DOMPDF_FONT_DIR\n         *\n         * Note: This directory must exist and be writable by the webserver process.\n         */\n        'font_cache' => storage_path('fonts/'),\n\n        /**\n         * The location of a temporary directory.\n         *\n         * The directory specified must be writeable by the webserver process.\n         * The temporary directory is required to download remote images and when\n         * using the PFDLib back end.\n         */\n        'temp_dir' => sys_get_temp_dir(),\n\n        /**\n         * ==== IMPORTANT ====.\n         *\n         * dompdf's \"chroot\": Prevents dompdf from accessing system files or other\n         * files on the webserver.  All local files opened by dompdf must be in a\n         * subdirectory of this directory.  DO NOT set it to '/' since this could\n         * allow an attacker to use dompdf to read any files on the server.  This\n         * should be an absolute path.\n         * This is only checked on command line call by dompdf.php, but not by\n         * direct class use like:\n         * $dompdf = new DOMPDF();  $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();\n         */\n        'chroot' => realpath(public_path()),\n\n        /**\n         * Protocol whitelist.\n         *\n         * Protocols and PHP wrappers allowed in URIs, and the validation rules\n         * that determine if a resouce may be loaded. Full support is not guaranteed\n         * for the protocols/wrappers specified\n         * by this array.\n         *\n         * @var array\n         */\n        'allowed_protocols' => [\n            \"data://\" => [\"rules\" => []],\n            'file://'  => ['rules' => []],\n            'http://'  => ['rules' => []],\n            'https://' => ['rules' => []],\n        ],\n\n        /**\n         * @var string\n         */\n        'log_output_file' => null,\n\n        /**\n         * Whether to enable font subsetting or not.\n         */\n        'enable_font_subsetting' => false,\n\n        /**\n         * The PDF rendering backend to use.\n         *\n         * Valid settings are 'PDFLib', 'CPDF' (the bundled R&OS PDF class), 'GD' and\n         * 'auto'. 'auto' will look for PDFLib and use it if found, or if not it will\n         * fall back on CPDF. 'GD' renders PDFs to graphic files. {@link * Canvas_Factory} ultimately determines which rendering class to instantiate\n         * based on this setting.\n         *\n         * Both PDFLib & CPDF rendering backends provide sufficient rendering\n         * capabilities for dompdf, however additional features (e.g. object,\n         * image and font support, etc.) differ between backends.  Please see\n         * {@link PDFLib_Adapter} for more information on the PDFLib backend\n         * and {@link CPDF_Adapter} and lib/class.pdf.php for more information\n         * on CPDF. Also see the documentation for each backend at the links\n         * below.\n         *\n         * The GD rendering backend is a little different than PDFLib and\n         * CPDF. Several features of CPDF and PDFLib are not supported or do\n         * not make any sense when creating image files.  For example,\n         * multiple pages are not supported, nor are PDF 'objects'.  Have a\n         * look at {@link GD_Adapter} for more information.  GD support is\n         * experimental, so use it at your own risk.\n         *\n         * @link http://www.pdflib.com\n         * @link http://www.ros.co.nz/pdf\n         * @link http://www.php.net/image\n         */\n        'pdf_backend' => 'CPDF',\n\n        /**\n         * PDFlib license key.\n         *\n         * If you are using a licensed, commercial version of PDFlib, specify\n         * your license key here.  If you are using PDFlib-Lite or are evaluating\n         * the commercial version of PDFlib, comment out this setting.\n         *\n         * @link http://www.pdflib.com\n         *\n         * If pdflib present in web server and auto or selected explicitely above,\n         * a real license code must exist!\n         */\n        //\"DOMPDF_PDFLIB_LICENSE\" => \"your license key here\",\n\n        /**\n         * html target media view which should be rendered into pdf.\n         * List of types and parsing rules for future extensions:\n         * http://www.w3.org/TR/REC-html40/types.html\n         *   screen, tty, tv, projection, handheld, print, braille, aural, all\n         * Note: aural is deprecated in CSS 2.1 because it is replaced by speech in CSS 3.\n         * Note, even though the generated pdf file is intended for print output,\n         * the desired content might be different (e.g. screen or projection view of html file).\n         * Therefore allow specification of content here.\n         */\n        'default_media_type' => 'print',\n\n        /**\n         * The default paper size.\n         *\n         * North America standard is \"letter\"; other countries generally \"a4\"\n         *\n         * @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.)\n         */\n        'default_paper_size' => $dompdfPaperSizeMap[$exportPageSize] ?? 'a4',\n\n        /**\n         * The default paper orientation.\n         *\n         * The orientation of the page (portrait or landscape).\n         *\n         * @var string\n         */\n        'default_paper_orientation' => 'portrait',\n\n        /**\n         * The default font family.\n         *\n         * Used if no suitable fonts can be found. This must exist in the font folder.\n         *\n         * @var string\n         */\n        'default_font' => 'dejavu sans',\n\n        /**\n         * Image DPI setting.\n         *\n         * This setting determines the default DPI setting for images and fonts.  The\n         * DPI may be overridden for inline images by explictly setting the\n         * image's width & height style attributes (i.e. if the image's native\n         * width is 600 pixels and you specify the image's width as 72 points,\n         * the image will have a DPI of 600 in the rendered PDF.  The DPI of\n         * background images can not be overridden and is controlled entirely\n         * via this parameter.\n         *\n         * For the purposes of DOMPDF, pixels per inch (PPI) = dots per inch (DPI).\n         * If a size in html is given as px (or without unit as image size),\n         * this tells the corresponding size in pt.\n         * This adjusts the relative sizes to be similar to the rendering of the\n         * html page in a reference browser.\n         *\n         * In pdf, always 1 pt = 1/72 inch\n         *\n         * Rendering resolution of various browsers in px per inch:\n         * Windows Firefox and Internet Explorer:\n         *   SystemControl->Display properties->FontResolution: Default:96, largefonts:120, custom:?\n         * Linux Firefox:\n         *   about:config *resolution: Default:96\n         *   (xorg screen dimension in mm and Desktop font dpi settings are ignored)\n         *\n         * Take care about extra font/image zoom factor of browser.\n         *\n         * In images, <img> size in pixel attribute, img css style, are overriding\n         * the real image dimension in px for rendering.\n         *\n         * @var int\n         */\n        'dpi' => 96,\n\n        /**\n         * Enable inline PHP.\n         *\n         * If this setting is set to true then DOMPDF will automatically evaluate\n         * inline PHP contained within <script type=\"text/php\"> ... </script> tags.\n         *\n         * Enabling this for documents you do not trust (e.g. arbitrary remote html\n         * pages) is a security risk.  Set this option to false if you wish to process\n         * untrusted documents.\n         *\n         * @var bool\n         */\n        'enable_php' => false,\n\n        /**\n         * Enable inline Javascript.\n         *\n         * If this setting is set to true then DOMPDF will automatically insert\n         * JavaScript code contained within <script type=\"text/javascript\"> ... </script> tags.\n         *\n         * @var bool\n         */\n        'enable_javascript' => false,\n\n        /**\n         * Enable remote file access.\n         *\n         * If this setting is set to true, DOMPDF will access remote sites for\n         * images and CSS files as required.\n         * This is required for part of test case www/test/image_variants.html through www/examples.php\n         *\n         * Attention!\n         * This can be a security risk, in particular in combination with DOMPDF_ENABLE_PHP and\n         * allowing remote access to dompdf.php or on allowing remote html code to be passed to\n         * $dompdf = new DOMPDF(, $dompdf->load_html(...,\n         * This allows anonymous users to download legally doubtful internet content which on\n         * tracing back appears to being downloaded by your server, or allows malicious php code\n         * in remote html pages to be executed by your server with your account privileges.\n         *\n         * @var bool\n         */\n        'enable_remote' => env('ALLOW_UNTRUSTED_SERVER_FETCHING', false),\n\n        /**\n         * A ratio applied to the fonts height to be more like browsers' line height.\n         */\n        'font_height_ratio' => 1.1,\n\n        /**\n         * Use the HTML5 Lib parser.\n         *\n         * @deprecated This feature is now always on in dompdf 2.x\n         *\n         * @var bool\n         */\n        'enable_html5_parser' => true,\n    ],\n];\n"
  },
  {
    "path": "app/Config/filesystems.php",
    "content": "<?php\n\n/**\n * Filesystem configuration options.\n *\n * Changes to these config files are not supported by BookStack and may break upon updates.\n * Configuration should be altered via the `.env` file or environment variables.\n * Do not edit this file unless you're happy to maintain any changes yourself.\n */\n\nreturn [\n\n    // Default Filesystem Disk\n    // Options: local, local_secure, local_secure_restricted, s3\n    'default' => env('STORAGE_TYPE', 'local'),\n\n    // Filesystem to use specifically for image uploads.\n    'images' => env('STORAGE_IMAGE_TYPE', env('STORAGE_TYPE', 'local')),\n\n    // Filesystem to use specifically for file attachments.\n    'attachments' => env('STORAGE_ATTACHMENT_TYPE', env('STORAGE_TYPE', 'local')),\n\n    // Storage URL\n    // This is the url to where the storage is located for when using an external\n    // file storage service, such as s3, to store publicly accessible assets.\n    'url' => env('STORAGE_URL', false),\n\n    // Available filesystem disks\n    // Only local, local_secure & s3 are supported by BookStack\n    'disks' => [\n\n        'local' => [\n            'driver'     => 'local',\n            'root'       => public_path(),\n            'serve'      => false,\n            'throw'      => true,\n            'directory_visibility' => 'public',\n        ],\n\n        'local_secure_attachments' => [\n            'driver' => 'local',\n            'root'   => storage_path('uploads/files/'),\n            'serve'  => false,\n            'throw'  => true,\n        ],\n\n        'local_secure_images' => [\n            'driver'     => 'local',\n            'root'       => storage_path('uploads/images/'),\n            'serve'      => false,\n            'throw'      => true,\n        ],\n\n        's3' => [\n            'driver'                  => 's3',\n            'key'                     => env('STORAGE_S3_KEY', 'your-key'),\n            'secret'                  => env('STORAGE_S3_SECRET', 'your-secret'),\n            'region'                  => env('STORAGE_S3_REGION', 'your-region'),\n            'bucket'                  => env('STORAGE_S3_BUCKET', 'your-bucket'),\n            'endpoint'                => env('STORAGE_S3_ENDPOINT', null),\n            'use_path_style_endpoint' => env('STORAGE_S3_ENDPOINT', null) !== null,\n            'throw'                   => true,\n            'stream_reads'            => false,\n        ],\n\n    ],\n\n    // Symbolic Links\n    // Here you may configure the symbolic links that will be created when the\n    // `storage:link` Artisan command is executed. The array keys should be\n    // the locations of the links and the values should be their targets.\n    'links' => [\n        public_path('storage') => storage_path('app/public'),\n    ],\n\n];\n"
  },
  {
    "path": "app/Config/hashing.php",
    "content": "<?php\n\n/**\n * Hashing configuration options.\n *\n * Changes to these config files are not supported by BookStack and may break upon updates.\n * Configuration should be altered via the `.env` file or environment variables.\n * Do not edit this file unless you're happy to maintain any changes yourself.\n */\n\nreturn [\n\n    // Default Hash Driver\n    // This option controls the default hash driver that will be used to hash\n    // passwords for your application. By default, the bcrypt algorithm is used.\n    // Supported: \"bcrypt\", \"argon\", \"argon2id\"\n    'driver' => 'bcrypt',\n\n    // Bcrypt Options\n    // Here you may specify the configuration options that should be used when\n    // passwords are hashed using the Bcrypt algorithm. This will allow you\n    // to control the amount of time it takes to hash the given password.\n    'bcrypt' => [\n        'rounds' => env('BCRYPT_ROUNDS', 12),\n        'verify' => true,\n    ],\n\n    // Argon Options\n    // Here you may specify the configuration options that should be used when\n    // passwords are hashed using the Argon algorithm. These will allow you\n    // to control the amount of time it takes to hash the given password.\n    'argon' => [\n        'memory'  => 1024,\n        'threads' => 2,\n        'time'    => 2,\n    ],\n\n];\n"
  },
  {
    "path": "app/Config/logging.php",
    "content": "<?php\n\nuse Monolog\\Formatter\\LineFormatter;\nuse Monolog\\Handler\\ErrorLogHandler;\nuse Monolog\\Handler\\NullHandler;\nuse Monolog\\Handler\\StreamHandler;\nuse Monolog\\Processor\\PsrLogMessageProcessor;\n\n/**\n * Logging configuration options.\n *\n * Changes to these config files are not supported by BookStack and may break upon updates.\n * Configuration should be altered via the `.env` file or environment variables.\n * Do not edit this file unless you're happy to maintain any changes yourself.\n */\n\nreturn [\n\n    // Default Log Channel\n    // This option defines the default log channel that gets used when writing\n    // messages to the logs. The name specified in this option should match\n    // one of the channels defined in the \"channels\" configuration array.\n    'default' => env('LOG_CHANNEL', 'single'),\n\n    // Deprecations Log Channel\n    // This option controls the log channel that should be used to log warnings\n    // regarding deprecated PHP and library features. This allows you to get\n    // your application ready for upcoming major versions of dependencies.\n    'deprecations' => [\n        'channel' => 'null',\n        'trace' => false,\n    ],\n\n    // Log Channels\n    // Here you may configure the log channels for your application. Out of\n    // the box, Laravel uses the Monolog PHP logging library. This gives\n    // you a variety of powerful log handlers / formatters to utilize.\n    // Available Drivers: \"single\", \"daily\", \"slack\", \"syslog\",\n    //                    \"errorlog\", \"monolog\",\n    //                    \"custom\", \"stack\"\n    'channels' => [\n        'stack' => [\n            'driver'            => 'stack',\n            'channels'          => ['daily'],\n            'ignore_exceptions' => false,\n        ],\n\n        'single' => [\n            'driver' => 'single',\n            'path'   => storage_path('logs/laravel.log'),\n            'level'  => 'debug',\n            'days'   => 14,\n            'replace_placeholders' => true,\n        ],\n\n        'daily' => [\n            'driver' => 'daily',\n            'path'   => storage_path('logs/laravel.log'),\n            'level'  => 'debug',\n            'days'   => 7,\n            'replace_placeholders' => true,\n        ],\n\n        'stderr' => [\n            'driver'  => 'monolog',\n            'level'   => 'debug',\n            'handler' => StreamHandler::class,\n            'with'    => [\n                'stream' => 'php://stderr',\n            ],\n            'processors' => [PsrLogMessageProcessor::class],\n        ],\n\n        'syslog' => [\n            'driver' => 'syslog',\n            'level'  => 'debug',\n            'facility' => LOG_USER,\n            'replace_placeholders' => true,\n        ],\n\n        'errorlog' => [\n            'driver' => 'errorlog',\n            'level'  => 'debug',\n            'replace_placeholders' => true,\n        ],\n\n        // Custom errorlog implementation that logs out a plain,\n        // non-formatted message intended for the webserver log.\n        'errorlog_plain_webserver' => [\n            'driver'         => 'monolog',\n            'level'          => 'debug',\n            'handler'        => ErrorLogHandler::class,\n            'handler_with'   => [4],\n            'formatter'      => LineFormatter::class,\n            'formatter_with' => [\n                'format' => '%message%',\n            ],\n            'replace_placeholders' => true,\n        ],\n\n        'null' => [\n            'driver'  => 'monolog',\n            'handler' => NullHandler::class,\n        ],\n\n        // Testing channel\n        // Uses a shared testing instance during tests\n        // so that logs can be checked against.\n        'testing' => [\n            'driver' => 'testing',\n        ],\n\n        'emergency' => [\n            'path' => storage_path('logs/laravel.log'),\n        ],\n    ],\n\n    // Failed Login Message\n    // Allows a configurable message to be logged when a login request fails.\n    'failed_login' => [\n        'message' => env('LOG_FAILED_LOGIN_MESSAGE', null),\n        'channel' => env('LOG_FAILED_LOGIN_CHANNEL', 'errorlog_plain_webserver'),\n    ],\n\n];\n"
  },
  {
    "path": "app/Config/mail.php",
    "content": "<?php\n\n/**\n * Mail configuration options.\n *\n * Changes to these config files are not supported by BookStack and may break upon updates.\n * Configuration should be altered via the `.env` file or environment variables.\n * Do not edit this file unless you're happy to maintain any changes yourself.\n */\n\n// Configured mail encryption method.\n// STARTTLS should still be attempted, but tls/ssl forces TLS usage.\n$mailEncryption = env('MAIL_ENCRYPTION', null);\n$mailPort = intval(env('MAIL_PORT', 587));\n\nreturn [\n\n    // Mail driver to use.\n    // From Laravel 7+ this is MAIL_MAILER in laravel.\n    // Kept as MAIL_DRIVER in BookStack to prevent breaking change.\n    // Options: smtp, sendmail, log, array\n    'default' => env('MAIL_DRIVER', 'smtp'),\n\n    // Global \"From\" address & name\n    'from' => [\n        'address' => env('MAIL_FROM', 'bookstack@example.com'),\n        'name'    => env('MAIL_FROM_NAME', 'BookStack'),\n    ],\n\n    // Mailer Configurations\n    // Available mailing methods and their settings.\n    'mailers' => [\n        'smtp' => [\n            'transport' => 'smtp',\n            'scheme' => null,\n            'host' => env('MAIL_HOST', 'smtp.mailgun.org'),\n            'port' => $mailPort,\n            'username' => env('MAIL_USERNAME'),\n            'password' => env('MAIL_PASSWORD'),\n            'verify_peer' => env('MAIL_VERIFY_SSL', true),\n            'timeout' => null,\n            'local_domain' => null,\n            'require_tls' => ($mailEncryption === 'tls' || $mailEncryption === 'ssl' || $mailPort === 465),\n        ],\n\n        'sendmail' => [\n            'transport' => 'sendmail',\n            'path' => env('MAIL_SENDMAIL_COMMAND', '/usr/sbin/sendmail -bs'),\n        ],\n\n        'log' => [\n            'transport' => 'log',\n            'channel' => env('MAIL_LOG_CHANNEL'),\n        ],\n\n        'array' => [\n            'transport' => 'array',\n        ],\n\n        'failover' => [\n            'transport' => 'failover',\n            'mailers' => [\n                'smtp',\n                'log',\n            ],\n        ],\n    ],\n];\n"
  },
  {
    "path": "app/Config/oidc.php",
    "content": "<?php\n\nreturn [\n\n    // Display name, shown to users, for OpenId option\n    'name' => env('OIDC_NAME', 'SSO'),\n\n    // Dump user details after a login request for debugging purposes\n    'dump_user_details' => env('OIDC_DUMP_USER_DETAILS', false),\n\n    // Claim, within an OpenId token, to find the user's display name\n    'display_name_claims' => env('OIDC_DISPLAY_NAME_CLAIMS', 'name'),\n\n    // Claim, within an OpenID token, to use to connect a BookStack user to the OIDC user.\n    'external_id_claim' => env('OIDC_EXTERNAL_ID_CLAIM', 'sub'),\n\n    // OAuth2/OpenId client id, as configured in your Authorization server.\n    'client_id' => env('OIDC_CLIENT_ID', null),\n\n    // OAuth2/OpenId client secret, as configured in your Authorization server.\n    'client_secret' => env('OIDC_CLIENT_SECRET', null),\n\n    // The issuer of the identity token (id_token) this will be compared with\n    // what is returned in the token.\n    'issuer' => env('OIDC_ISSUER', null),\n\n    // Auto-discover the relevant endpoints and keys from the issuer.\n    // Fetched details are cached for 15 minutes.\n    'discover' => env('OIDC_ISSUER_DISCOVER', false),\n\n    // Public key that's used to verify the JWT token with.\n    // Can be the key value itself or a local 'file://public.key' reference.\n    'jwt_public_key' => env('OIDC_PUBLIC_KEY', null),\n\n    // OAuth2 endpoints.\n    'authorization_endpoint' => env('OIDC_AUTH_ENDPOINT', null),\n    'token_endpoint'         => env('OIDC_TOKEN_ENDPOINT', null),\n    'userinfo_endpoint'      => env('OIDC_USERINFO_ENDPOINT', null),\n\n    // OIDC RP-Initiated Logout endpoint URL.\n    // A false value force-disables RP-Initiated Logout.\n    // A true value gets the URL from discovery, if active.\n    // A string value is used as the URL.\n    'end_session_endpoint' => env('OIDC_END_SESSION_ENDPOINT', false),\n\n    // Add extra scopes, upon those required, to the OIDC authentication request\n    // Multiple values can be provided comma seperated.\n    'additional_scopes' => env('OIDC_ADDITIONAL_SCOPES', null),\n\n    // Enable fetching of the user's avatar from the 'picture' claim on login.\n    // Will only be fetched if the user doesn't already have an avatar image assigned.\n    // This can be a security risk due to performing server-side fetching (with up to 3 redirects) of\n    // data from external URLs. Only enable if you trust the OIDC auth provider to provide safe URLs for user images.\n    'fetch_avatar' => env('OIDC_FETCH_AVATAR', false),\n\n    // Group sync options\n    // Enable syncing, upon login, of OIDC groups to BookStack roles\n    'user_to_groups' => env('OIDC_USER_TO_GROUPS', false),\n    // Attribute, within a OIDC ID token, to find group names within\n    'groups_claim' => env('OIDC_GROUPS_CLAIM', 'groups'),\n    // When syncing groups, remove any groups that no longer match. Otherwise, sync only adds new groups.\n    'remove_from_groups' => env('OIDC_REMOVE_FROM_GROUPS', false),\n];\n"
  },
  {
    "path": "app/Config/queue.php",
    "content": "<?php\n\n/**\n * Queue configuration options.\n *\n * Changes to these config files are not supported by BookStack and may break upon updates.\n * Configuration should be altered via the `.env` file or environment variables.\n * Do not edit this file unless you're happy to maintain any changes yourself.\n */\n\nreturn [\n\n    // Default driver to use for the queue\n    // Options: sync, database, redis\n    'default' => env('QUEUE_CONNECTION', 'sync'),\n\n    // Queue connection configuration\n    'connections' => [\n\n        'sync' => [\n            'driver' => 'sync',\n        ],\n\n        'database' => [\n            'driver'       => 'database',\n            'connection'   => null,\n            'table'        => 'jobs',\n            'queue'        => 'default',\n            'retry_after'  => 90,\n            'after_commit' => false,\n        ],\n\n        'redis' => [\n            'driver'       => 'redis',\n            'connection'   => 'default',\n            'queue'        => env('REDIS_QUEUE', 'default'),\n            'retry_after'  => 90,\n            'block_for'    => null,\n            'after_commit' => false,\n        ],\n\n    ],\n\n    // Job batching\n    'batching' => [\n        'database' => 'mysql',\n        'table' => 'job_batches',\n    ],\n\n    // Failed queue job logging\n    'failed' => [\n        'driver'   => 'database-uuids',\n        'database' => 'mysql',\n        'table'    => 'failed_jobs',\n    ],\n\n];\n"
  },
  {
    "path": "app/Config/saml2.php",
    "content": "<?php\n\n$SAML2_IDP_AUTHNCONTEXT = env('SAML2_IDP_AUTHNCONTEXT', true);\n$SAML2_SP_x509 = env('SAML2_SP_x509', false);\n\nreturn [\n\n    // Display name, shown to users, for SAML2 option\n    'name' => env('SAML2_NAME', 'SSO'),\n\n    // Dump user details after a login request for debugging purposes\n    'dump_user_details' => env('SAML2_DUMP_USER_DETAILS', false),\n\n    // Attribute, within a SAML response, to find the user's email address\n    'email_attribute' => env('SAML2_EMAIL_ATTRIBUTE', 'email'),\n    // Attribute, within a SAML response, to find the user's display name\n    'display_name_attributes' => explode('|', env('SAML2_DISPLAY_NAME_ATTRIBUTES', 'username')),\n    // Attribute, within a SAML response, to use to connect a BookStack user to the SAML user.\n    'external_id_attribute' => env('SAML2_EXTERNAL_ID_ATTRIBUTE', null),\n\n    // Group sync options\n    // Enable syncing, upon login, of SAML2 groups to BookStack groups\n    'user_to_groups' => env('SAML2_USER_TO_GROUPS', false),\n    // Attribute, within a SAML response, to find group names on\n    'group_attribute' => env('SAML2_GROUP_ATTRIBUTE', 'group'),\n    // When syncing groups, remove any groups that no longer match. Otherwise sync only adds new groups.\n    'remove_from_groups' => env('SAML2_REMOVE_FROM_GROUPS', false),\n\n    // Autoload IDP details from the metadata endpoint\n    'autoload_from_metadata' => env('SAML2_AUTOLOAD_METADATA', false),\n\n    // Overrides, in JSON format, to the configuration passed to underlying onelogin library.\n    'onelogin_overrides' => env('SAML2_ONELOGIN_OVERRIDES', null),\n\n    'onelogin' => [\n        // If 'strict' is True, then the PHP Toolkit will reject unsigned\n        // or unencrypted messages if it expects them signed or encrypted\n        // Also will reject the messages if not strictly follow the SAML\n        // standard: Destination, NameId, Conditions ... are validated too.\n        'strict' => true,\n\n        // Enable debug mode (to print errors)\n        'debug' => env('APP_DEBUG', false),\n\n        // Set a BaseURL to be used instead of try to guess\n        // the BaseURL of the view that process the SAML Message.\n        // Ex. http://sp.example.com/\n        //     http://example.com/sp/\n        'baseurl' => null,\n\n        // Service Provider Data that we are deploying\n        'sp' => [\n            // Identifier of the SP entity  (must be a URI)\n            'entityId' => '',\n\n            // Specifies info about where and how the <AuthnResponse> message MUST be\n            // returned to the requester, in this case our SP.\n            'assertionConsumerService' => [\n                // URL Location where the <Response> from the IdP will be returned\n                'url' => '',\n                // SAML protocol binding to be used when returning the <Response>\n                // message.  Onelogin Toolkit supports for this endpoint the\n                // HTTP-POST binding only\n                'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',\n            ],\n\n            // Specifies info about where and how the <Logout Response> message MUST be\n            // returned to the requester, in this case our SP.\n            'singleLogoutService' => [\n                // URL Location where the <Response> from the IdP will be returned\n                'url' => '',\n                // SAML protocol binding to be used when returning the <Response>\n                // message.  Onelogin Toolkit supports for this endpoint the\n                // HTTP-Redirect binding only\n                'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',\n            ],\n\n            // Specifies constraints on the name identifier to be used to\n            // represent the requested subject.\n            // Take a look on lib/Saml2/Constants.php to see the NameIdFormat supported\n            'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',\n\n            // Usually x509cert and privateKey of the SP are provided by files placed at\n            // the certs folder. But we can also provide them with the following parameters\n            'x509cert'   => $SAML2_SP_x509 ?: '',\n            'privateKey' => env('SAML2_SP_x509_KEY', ''),\n        ],\n        // Identity Provider Data that we want connect with our SP\n        'idp' => [\n            // Identifier of the IdP entity  (must be a URI)\n            'entityId' => env('SAML2_IDP_ENTITYID', null),\n            // SSO endpoint info of the IdP. (Authentication Request protocol)\n            'singleSignOnService' => [\n                // URL Target of the IdP where the SP will send the Authentication Request Message\n                'url' => env('SAML2_IDP_SSO', null),\n                // SAML protocol binding to be used when returning the <Response>\n                // message.  Onelogin Toolkit supports for this endpoint the\n                // HTTP-Redirect binding only\n                'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',\n            ],\n            // SLO endpoint info of the IdP.\n            'singleLogoutService' => [\n                // URL Location of the IdP where the SP will send the SLO Request\n                'url' => env('SAML2_IDP_SLO', null),\n                // URL location of the IdP where the SP will send the SLO Response (ResponseLocation)\n                // if not set, url for the SLO Request will be used\n                'responseUrl' => null,\n                // SAML protocol binding to be used when returning the <Response>\n                // message.  Onelogin Toolkit supports for this endpoint the\n                // HTTP-Redirect binding only\n                'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',\n            ],\n            // Public x509 certificate of the IdP\n            'x509cert' => env('SAML2_IDP_x509', null),\n            /*\n             *  Instead of use the whole x509cert you can use a fingerprint in\n             *  order to validate the SAMLResponse, but we don't recommend to use\n             *  that method on production since is exploitable by a collision\n             *  attack.\n             *  (openssl x509 -noout -fingerprint -in \"idp.crt\" to generate it,\n             *   or add for example the -sha256 , -sha384 or -sha512 parameter)\n             *\n             *  If a fingerprint is provided, then the certFingerprintAlgorithm is required in order to\n             *  let the toolkit know which Algorithm was used. Possible values: sha1, sha256, sha384 or sha512\n             *  'sha1' is the default value.\n             */\n            // 'certFingerprint' => '',\n            // 'certFingerprintAlgorithm' => 'sha1',\n            /* In some scenarios the IdP uses different certificates for\n             * signing/encryption, or is under key rollover phase and more\n             * than one certificate is published on IdP metadata.\n             * In order to handle that the toolkit offers that parameter.\n             * (when used, 'x509cert' and 'certFingerprint' values are\n             * ignored).\n             */\n            // 'x509certMulti' => array(\n            //      'signing' => array(\n            //          0 => '<cert1-string>',\n            //      ),\n            //      'encryption' => array(\n            //          0 => '<cert2-string>',\n            //      )\n            // ),\n        ],\n        'security' => [\n            // SAML2 Authn context\n            // When set to false no AuthContext will be sent in the AuthNRequest,\n            // When set to true (Default) you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'.\n            // Multiple forced values can be passed via a space separated array, For example:\n            // SAML2_IDP_AUTHNCONTEXT=\"urn:federation:authentication:windows urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport\"\n            'requestedAuthnContext' => is_string($SAML2_IDP_AUTHNCONTEXT) ? explode(' ', $SAML2_IDP_AUTHNCONTEXT) : $SAML2_IDP_AUTHNCONTEXT,\n            // Sign requests and responses if a certificate is in use\n            'logoutRequestSigned'   => (bool) $SAML2_SP_x509,\n            'logoutResponseSigned'  => (bool) $SAML2_SP_x509,\n            'authnRequestsSigned'   => (bool) $SAML2_SP_x509,\n            'lowercaseUrlencoding'  => false,\n        ],\n    ],\n\n];\n"
  },
  {
    "path": "app/Config/services.php",
    "content": "<?php\n\n/**\n * Third party service configuration options.\n *\n * Changes to these config files are not supported by BookStack and may break upon updates.\n * Configuration should be altered via the `.env` file or environment variables.\n * Do not edit this file unless you're happy to maintain any changes yourself.\n */\n\nreturn [\n\n    // Single option to disable non-auth external services such as Gravatar and Draw.io\n    'disable_services' => env('DISABLE_EXTERNAL_SERVICES', false),\n\n    // Draw.io integration active\n    'drawio' => env('DRAWIO', !env('DISABLE_EXTERNAL_SERVICES', false)),\n\n    // URL for fetching avatars\n    'avatar_url' => env('AVATAR_URL', ''),\n\n    // Callback URL for social authentication methods\n    'callback_url' => env('APP_URL', false),\n\n    'github'   => [\n        'client_id'     => env('GITHUB_APP_ID', false),\n        'client_secret' => env('GITHUB_APP_SECRET', false),\n        'redirect'      => env('APP_URL') . '/login/service/github/callback',\n        'name'          => 'GitHub',\n        'auto_register' => env('GITHUB_AUTO_REGISTER', false),\n        'auto_confirm'  => env('GITHUB_AUTO_CONFIRM_EMAIL', false),\n    ],\n\n    'google'   => [\n        'client_id'      => env('GOOGLE_APP_ID', false),\n        'client_secret'  => env('GOOGLE_APP_SECRET', false),\n        'redirect'       => env('APP_URL') . '/login/service/google/callback',\n        'name'           => 'Google',\n        'auto_register'  => env('GOOGLE_AUTO_REGISTER', false),\n        'auto_confirm'   => env('GOOGLE_AUTO_CONFIRM_EMAIL', false),\n        'select_account' => env('GOOGLE_SELECT_ACCOUNT', false),\n    ],\n\n    'slack'   => [\n        'client_id'     => env('SLACK_APP_ID', false),\n        'client_secret' => env('SLACK_APP_SECRET', false),\n        'redirect'      => env('APP_URL') . '/login/service/slack/callback',\n        'name'          => 'Slack',\n        'auto_register' => env('SLACK_AUTO_REGISTER', false),\n        'auto_confirm'  => env('SLACK_AUTO_CONFIRM_EMAIL', false),\n    ],\n\n    'facebook'   => [\n        'client_id'     => env('FACEBOOK_APP_ID', false),\n        'client_secret' => env('FACEBOOK_APP_SECRET', false),\n        'redirect'      => env('APP_URL') . '/login/service/facebook/callback',\n        'name'          => 'Facebook',\n        'auto_register' => env('FACEBOOK_AUTO_REGISTER', false),\n        'auto_confirm'  => env('FACEBOOK_AUTO_CONFIRM_EMAIL', false),\n    ],\n\n    'twitter'   => [\n        'client_id'     => env('TWITTER_APP_ID', false),\n        'client_secret' => env('TWITTER_APP_SECRET', false),\n        'redirect'      => env('APP_URL') . '/login/service/twitter/callback',\n        'name'          => 'Twitter',\n        'auto_register' => env('TWITTER_AUTO_REGISTER', false),\n        'auto_confirm'  => env('TWITTER_AUTO_CONFIRM_EMAIL', false),\n    ],\n\n    'azure'   => [\n        'client_id'     => env('AZURE_APP_ID', false),\n        'client_secret' => env('AZURE_APP_SECRET', false),\n        'tenant'        => env('AZURE_TENANT', false),\n        'redirect'      => env('APP_URL') . '/login/service/azure/callback',\n        'name'          => 'Microsoft Azure',\n        'auto_register' => env('AZURE_AUTO_REGISTER', false),\n        'auto_confirm'  => env('AZURE_AUTO_CONFIRM_EMAIL', false),\n    ],\n\n    'okta' => [\n        'client_id'     => env('OKTA_APP_ID'),\n        'client_secret' => env('OKTA_APP_SECRET'),\n        'redirect'      => env('APP_URL') . '/login/service/okta/callback',\n        'base_url'      => env('OKTA_BASE_URL'),\n        'name'          => 'Okta',\n        'auto_register' => env('OKTA_AUTO_REGISTER', false),\n        'auto_confirm'  => env('OKTA_AUTO_CONFIRM_EMAIL', false),\n    ],\n\n    'gitlab' => [\n        'client_id'     => env('GITLAB_APP_ID'),\n        'client_secret' => env('GITLAB_APP_SECRET'),\n        'redirect'      => env('APP_URL') . '/login/service/gitlab/callback',\n        'instance_uri'  => env('GITLAB_BASE_URI'), // Needed only for self hosted instances\n        'name'          => 'GitLab',\n        'auto_register' => env('GITLAB_AUTO_REGISTER', false),\n        'auto_confirm'  => env('GITLAB_AUTO_CONFIRM_EMAIL', false),\n    ],\n\n    'twitch' => [\n        'client_id'     => env('TWITCH_APP_ID'),\n        'client_secret' => env('TWITCH_APP_SECRET'),\n        'redirect'      => env('APP_URL') . '/login/service/twitch/callback',\n        'name'          => 'Twitch',\n        'auto_register' => env('TWITCH_AUTO_REGISTER', false),\n        'auto_confirm'  => env('TWITCH_AUTO_CONFIRM_EMAIL', false),\n    ],\n\n    'discord' => [\n        'client_id'     => env('DISCORD_APP_ID'),\n        'client_secret' => env('DISCORD_APP_SECRET'),\n        'redirect'      => env('APP_URL') . '/login/service/discord/callback',\n        'name'          => 'Discord',\n        'auto_register' => env('DISCORD_AUTO_REGISTER', false),\n        'auto_confirm'  => env('DISCORD_AUTO_CONFIRM_EMAIL', false),\n    ],\n\n    'ldap' => [\n        'server'                 => env('LDAP_SERVER', false),\n        'dump_user_details'      => env('LDAP_DUMP_USER_DETAILS', false),\n        'dump_user_groups'       => env('LDAP_DUMP_USER_GROUPS', false),\n        'dn'                     => env('LDAP_DN', false),\n        'pass'                   => env('LDAP_PASS', false),\n        'base_dn'                => env('LDAP_BASE_DN', false),\n        'user_filter'            => env('LDAP_USER_FILTER', '(&(uid={user}))'),\n        'version'                => env('LDAP_VERSION', false),\n        'id_attribute'           => env('LDAP_ID_ATTRIBUTE', 'uid'),\n        'email_attribute'        => env('LDAP_EMAIL_ATTRIBUTE', 'mail'),\n        'display_name_attribute' => env('LDAP_DISPLAY_NAME_ATTRIBUTE', 'cn'),\n        'follow_referrals'       => env('LDAP_FOLLOW_REFERRALS', false),\n        'user_to_groups'         => env('LDAP_USER_TO_GROUPS', false),\n        'group_attribute'        => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),\n        'remove_from_groups'     => env('LDAP_REMOVE_FROM_GROUPS', false),\n        'tls_insecure'           => env('LDAP_TLS_INSECURE', false),\n        'tls_ca_cert'            => env('LDAP_TLS_CA_CERT', false),\n        'start_tls'              => env('LDAP_START_TLS', false),\n        'thumbnail_attribute'    => env('LDAP_THUMBNAIL_ATTRIBUTE', null),\n    ],\n\n];\n"
  },
  {
    "path": "app/Config/session.php",
    "content": "<?php\n\nuse Illuminate\\Support\\Str;\n\n/**\n * Session configuration options.\n *\n * Changes to these config files are not supported by BookStack and may break upon updates.\n * Configuration should be altered via the `.env` file or environment variables.\n * Do not edit this file unless you're happy to maintain any changes yourself.\n */\n\nreturn [\n\n    // Default session driver\n    // Options: file, cookie, database, redis, memcached, array\n    'driver' => env('SESSION_DRIVER', 'file'),\n\n    // Session lifetime, in minutes\n    'lifetime' => env('SESSION_LIFETIME', 120),\n\n    // Expire session on browser close\n    'expire_on_close' => false,\n\n    // Encrypt session data\n    'encrypt' => false,\n\n    // Location to store session files\n    'files' => storage_path('framework/sessions'),\n\n    // Session Database Connection\n    // When using the \"database\" or \"redis\" session drivers, you can specify a\n    // connection that should be used to manage these sessions. This should\n    // correspond to a connection in your database configuration options.\n    'connection' => null,\n\n    // Session database table, if database driver is in use\n    'table' => 'sessions',\n\n    // Session Cache Store\n    // When using the \"apc\" or \"memcached\" session drivers, you may specify a\n    // cache store that should be used for these sessions. This value must\n    // correspond with one of the application's configured cache stores.\n    'store' => null,\n\n    // Session Sweeping Lottery\n    // Some session drivers must manually sweep their storage location to get\n    // rid of old sessions from storage. Here are the chances that it will\n    // happen on a given request. By default, the odds are 2 out of 100.\n    'lottery' => [2, 100],\n\n    // Session Cookie Name\n    // Here you may change the name of the cookie used to identify a session\n    // instance by ID. The name specified here will get used every time a\n    // new session cookie is created by the framework for every driver.\n    'cookie' => env('SESSION_COOKIE_NAME', 'bookstack_session'),\n\n    // Session Cookie Path\n    // The session cookie path determines the path for which the cookie will\n    // be regarded as available. Typically, this will be the root path of\n    // your application but you are free to change this when necessary.\n    'path' => '/' . (explode('/', env('APP_URL', ''), 4)[3] ?? ''),\n\n    // Session Cookie Domain\n    // Here you may change the domain of the cookie used to identify a session\n    // in your application. This will determine which domains the cookie is\n    // available to in your application. A sensible default has been set.\n    'domain' => env('SESSION_DOMAIN', null),\n\n    // HTTPS Only Cookies\n    // By setting this option to true, session cookies will only be sent back\n    // to the server if the browser has a HTTPS connection. This will keep\n    // the cookie from being sent to you if it can not be done securely.\n    'secure' => env('SESSION_SECURE_COOKIE', null)\n        ?? Str::startsWith(env('APP_URL', ''), 'https:'),\n\n    // HTTP Access Only\n    // Setting this value to true will prevent JavaScript from accessing the\n    // value of the cookie and the cookie will only be accessible through the HTTP protocol.\n    'http_only' => true,\n\n    // Same-Site Cookies\n    // This option determines how your cookies behave when cross-site requests\n    // take place, and can be used to mitigate CSRF attacks. By default, we\n    // do not enable this as other CSRF protection services are in place.\n    // Options: lax, strict, none\n    'same_site' => 'lax',\n\n\n    // Partitioned Cookies\n    // Setting this value to true will tie the cookie to the top-level site for\n    // a cross-site context. Partitioned cookies are accepted by the browser\n    // when flagged \"secure\" and the Same-Site attribute is set to \"none\".\n    'partitioned' => false,\n];\n"
  },
  {
    "path": "app/Config/setting-defaults.php",
    "content": "<?php\n\n/**\n * Default system settings.\n *\n * Changes to these config files are not supported by BookStack and may break upon updates.\n * Configuration should be altered via the `.env` file or environment variables.\n * Do not edit this file unless you're happy to maintain any changes yourself.\n */\n\nreturn [\n\n    'app-name'             => 'BookStack',\n    'app-logo'             => '',\n    'app-name-header'      => true,\n    'app-editor'           => 'wysiwyg',\n    'app-color'            => '#206ea7',\n    'app-color-light'      => 'rgba(32,110,167,0.15)',\n    'link-color'           => '#206ea7',\n    'bookshelf-color'      => '#a94747',\n    'book-color'           => '#077b70',\n    'chapter-color'        => '#af4d0d',\n    'page-color'           => '#206ea7',\n    'page-draft-color'     => '#7e50b1',\n    'app-color-dark'       => '#195785',\n    'app-color-light-dark' => 'rgba(32,110,167,0.15)',\n    'link-color-dark'      => '#429fe3',\n    'bookshelf-color-dark' => '#ff5454',\n    'book-color-dark'      => '#389f60',\n    'chapter-color-dark'   => '#ee7a2d',\n    'page-color-dark'      => '#429fe3',\n    'page-draft-color-dark' => '#a66ce8',\n    'app-custom-head'      => false,\n    'registration-enabled' => false,\n\n    // User-level default settings\n    'user' => [\n        'ui-shortcuts'          => '{}',\n        'ui-shortcuts-enabled'  => false,\n        'dark-mode-enabled'     => env('APP_DEFAULT_DARK_MODE', false),\n        'bookshelves_view_type' => env('APP_VIEWS_BOOKSHELVES', 'grid'),\n        'bookshelf_view_type'   => env('APP_VIEWS_BOOKSHELF', 'grid'),\n        'books_view_type'       => env('APP_VIEWS_BOOKS', 'grid'),\n        'notifications#comment-mentions' => true,\n    ],\n\n];\n"
  },
  {
    "path": "app/Config/view.php",
    "content": "<?php\n\n/**\n * View configuration options.\n *\n * Changes to these config files are not supported by BookStack and may break upon updates.\n * Configuration should be altered via the `.env` file or environment variables.\n * Do not edit this file unless you're happy to maintain any changes yourself.\n */\n\nreturn [\n\n    // App theme\n    // This option defines the theme to use for the application. When a theme\n    // is set there must be a `themes/<theme_name>` folder to hold the\n    // custom theme overrides.\n    'theme' => env('APP_THEME', false),\n\n    // View Storage Paths\n    // Most templating systems load templates from disk. Here you may specify\n    // an array of paths that should be checked for your views. Of course\n    // the usual Laravel view path has already been registered for you.\n    'paths' => [realpath(base_path('resources/views'))],\n\n    // Compiled View Path\n    // This option determines where all the compiled Blade templates will be\n    // stored for your application. Typically, this is within the storage\n    // directory. However, as usual, you are free to change this value.\n    'compiled' => realpath(storage_path('framework/views')),\n\n];\n"
  },
  {
    "path": "app/Console/Commands/AssignSortRuleCommand.php",
    "content": "<?php\n\nnamespace BookStack\\Console\\Commands;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Sorting\\BookSorter;\nuse BookStack\\Sorting\\SortRule;\nuse Illuminate\\Console\\Command;\n\nclass AssignSortRuleCommand extends Command\n{\n    /**\n     * The name and signature of the console command.\n     *\n     * @var string\n     */\n    protected $signature = 'bookstack:assign-sort-rule\n                            {sort-rule=0: ID of the sort rule to apply}\n                            {--all-books : Apply to all books in the system}\n                            {--books-without-sort : Apply to only books without a sort rule already assigned}\n                            {--books-with-sort= : Apply to only books with the sort rule of given id}';\n\n    /**\n     * The console command description.\n     *\n     * @var string\n     */\n    protected $description = 'Assign a sort rule to content in the system';\n\n    /**\n     * Execute the console command.\n     */\n    public function handle(BookSorter $sorter): int\n    {\n        $sortRuleId = intval($this->argument('sort-rule')) ?? 0;\n        if ($sortRuleId === 0) {\n            return $this->listSortRules();\n        }\n\n        $rule = SortRule::query()->find($sortRuleId);\n        if ($this->option('all-books')) {\n            $query = Book::query();\n        } else if ($this->option('books-without-sort')) {\n            $query = Book::query()->whereNull('sort_rule_id');\n        } else if ($this->option('books-with-sort')) {\n            $sortId = intval($this->option('books-with-sort')) ?: 0;\n            if (!$sortId) {\n                $this->error(\"Provided --books-with-sort option value is invalid\");\n                return 1;\n            }\n            $query = Book::query()->where('sort_rule_id', $sortId);\n        } else {\n            $this->error(\"No option provided to specify target. Run with the -h option to see all available options.\");\n            return 1;\n        }\n\n        if (!$rule) {\n            $this->error(\"Sort rule of provided id {$sortRuleId} not found!\");\n            return 1;\n        }\n\n        $count = $query->clone()->count();\n        $this->warn(\"This will apply sort rule [{$rule->id}: {$rule->name}] to {$count} book(s) and run the sort on each.\");\n        $confirmed = $this->confirm(\"Are you sure you want to continue?\");\n\n        if (!$confirmed) {\n            return 1;\n        }\n\n        $processed = 0;\n        $query->chunkById(10, function ($books) use ($rule, $sorter, $count, &$processed) {\n            $max = min($count, ($processed + 10));\n            $this->info(\"Applying to {$processed}-{$max} of {$count} books\");\n            foreach ($books as $book) {\n                $book->sort_rule_id = $rule->id;\n                $book->save();\n                $sorter->runBookAutoSort($book);\n            }\n            $processed = $max;\n        });\n\n        $this->info(\"Sort applied to {$processed} book(s)!\");\n\n        return 0;\n    }\n\n    protected function listSortRules(): int\n    {\n\n        $rules = SortRule::query()->orderBy('id', 'asc')->get();\n        $this->error(\"Sort rule ID required!\");\n        $this->warn(\"\\nAvailable sort rules:\");\n        foreach ($rules as $rule) {\n            $this->info(\"{$rule->id}: {$rule->name}\");\n        }\n\n        return 1;\n    }\n}\n"
  },
  {
    "path": "app/Console/Commands/CleanupImagesCommand.php",
    "content": "<?php\n\nnamespace BookStack\\Console\\Commands;\n\nuse BookStack\\Uploads\\ImageService;\nuse Illuminate\\Console\\Command;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\n\nclass CleanupImagesCommand extends Command\n{\n    /**\n     * The name and signature of the console command.\n     *\n     * @var string\n     */\n    protected $signature = 'bookstack:cleanup-images\n                            {--a|all : Also delete images that are only used in old revisions}\n                            {--f|force : Actually run the deletions, Defaults to a dry-run}\n                            ';\n\n    /**\n     * The console command description.\n     *\n     * @var string\n     */\n    protected $description = 'Cleanup images and drawings';\n\n    /**\n     * Execute the console command.\n     */\n    public function handle(ImageService $imageService): int\n    {\n        $checkRevisions = !$this->option('all');\n        $dryRun = !$this->option('force');\n\n        if (!$dryRun) {\n            $this->warn(\"This operation is destructive and is not guaranteed to be fully accurate.\\nEnsure you have a backup of your images.\\n\");\n            $proceed = !$this->input->isInteractive() || $this->confirm(\"Are you sure you want to proceed?\");\n            if (!$proceed) {\n                return 0;\n            }\n        }\n\n        $deleted = $imageService->deleteUnusedImages($checkRevisions, $dryRun);\n        $deleteCount = count($deleted);\n\n        if ($dryRun) {\n            $this->comment('Dry run, no images have been deleted');\n            $this->comment($deleteCount . ' image(s) found that would have been deleted');\n            $this->showDeletedImages($deleted);\n            $this->comment('Run with -f or --force to perform deletions');\n\n            return 0;\n        }\n\n        $this->showDeletedImages($deleted);\n        $this->comment(\"{$deleteCount} image(s) deleted\");\n\n        return 0;\n    }\n\n    protected function showDeletedImages($paths): void\n    {\n        if ($this->getOutput()->getVerbosity() <= OutputInterface::VERBOSITY_NORMAL) {\n            return;\n        }\n\n        if (count($paths) > 0) {\n            $this->line('Image(s) to delete:');\n        }\n\n        foreach ($paths as $path) {\n            $this->line($path);\n        }\n    }\n}\n"
  },
  {
    "path": "app/Console/Commands/ClearActivityCommand.php",
    "content": "<?php\n\nnamespace BookStack\\Console\\Commands;\n\nuse BookStack\\Activity\\Models\\Activity;\nuse Illuminate\\Console\\Command;\n\nclass ClearActivityCommand extends Command\n{\n    /**\n     * The name and signature of the console command.\n     *\n     * @var string\n     */\n    protected $signature = 'bookstack:clear-activity';\n\n    /**\n     * The console command description.\n     *\n     * @var string\n     */\n    protected $description = 'Clear user (audit-log) activity from the system';\n\n    /**\n     * Execute the console command.\n     */\n    public function handle(): int\n    {\n        Activity::query()->truncate();\n        $this->comment('System activity cleared');\n        return 0;\n    }\n}\n"
  },
  {
    "path": "app/Console/Commands/ClearRevisionsCommand.php",
    "content": "<?php\n\nnamespace BookStack\\Console\\Commands;\n\nuse BookStack\\Entities\\Models\\PageRevision;\nuse Illuminate\\Console\\Command;\n\nclass ClearRevisionsCommand extends Command\n{\n    /**\n     * The name and signature of the console command.\n     *\n     * @var string\n     */\n    protected $signature = 'bookstack:clear-revisions\n                            {--a|all : Include active update drafts in deletion}\n                            ';\n\n    /**\n     * The console command description.\n     *\n     * @var string\n     */\n    protected $description = 'Clear page revisions';\n\n    /**\n     * Execute the console command.\n     */\n    public function handle(): int\n    {\n        $deleteTypes = $this->option('all') ? ['version', 'update_draft'] : ['version'];\n        PageRevision::query()->whereIn('type', $deleteTypes)->delete();\n        $this->comment('Revisions deleted');\n        return 0;\n    }\n}\n"
  },
  {
    "path": "app/Console/Commands/ClearViewsCommand.php",
    "content": "<?php\n\nnamespace BookStack\\Console\\Commands;\n\nuse BookStack\\Activity\\Models\\View;\nuse Illuminate\\Console\\Command;\n\nclass ClearViewsCommand extends Command\n{\n    /**\n     * The name and signature of the console command.\n     *\n     * @var string\n     */\n    protected $signature = 'bookstack:clear-views';\n\n    /**\n     * The console command description.\n     *\n     * @var string\n     */\n    protected $description = 'Clear all view-counts for all entities';\n\n    /**\n     * Execute the console command.\n     */\n    public function handle(): int\n    {\n        View::query()->truncate();\n        $this->comment('Views cleared');\n        return 0;\n    }\n}\n"
  },
  {
    "path": "app/Console/Commands/CopyShelfPermissionsCommand.php",
    "content": "<?php\n\nnamespace BookStack\\Console\\Commands;\n\nuse BookStack\\Entities\\Queries\\BookshelfQueries;\nuse BookStack\\Entities\\Tools\\PermissionsUpdater;\nuse Illuminate\\Console\\Command;\n\nclass CopyShelfPermissionsCommand extends Command\n{\n    /**\n     * The name and signature of the console command.\n     *\n     * @var string\n     */\n    protected $signature = 'bookstack:copy-shelf-permissions\n                            {--a|all : Perform for all shelves in the system}\n                            {--s|slug= : The slug for a shelf to target}\n                            ';\n\n    /**\n     * The console command description.\n     *\n     * @var string\n     */\n    protected $description = 'Copy shelf permissions to all child books';\n\n    /**\n     * Execute the console command.\n     */\n    public function handle(PermissionsUpdater $permissionsUpdater, BookshelfQueries $queries): int\n    {\n        $shelfSlug = $this->option('slug');\n        $cascadeAll = $this->option('all');\n        $shelves = null;\n\n        if (!$cascadeAll && !$shelfSlug) {\n            $this->error('Either a --slug or --all option must be provided.');\n\n            return 1;\n        }\n\n        if ($cascadeAll) {\n            $continue = $this->confirm(\n                'Permission settings for all shelves will be cascaded. ' .\n                        'Books assigned to multiple shelves will receive only the permissions of it\\'s last processed shelf. ' .\n                        'Are you sure you want to proceed?'\n            );\n\n            if (!$continue && !$this->hasOption('no-interaction')) {\n                return 0;\n            }\n\n            $shelves = $queries->start()->get(['id']);\n        }\n\n        if ($shelfSlug) {\n            $shelves = $queries->start()->where('slug', '=', $shelfSlug)->get(['id']);\n            if ($shelves->count() === 0) {\n                $this->info('No shelves found with the given slug.');\n            }\n        }\n\n        foreach ($shelves as $shelf) {\n            $permissionsUpdater->updateBookPermissionsFromShelf($shelf, false);\n            $this->info('Copied permissions for shelf [' . $shelf->id . ']');\n        }\n\n        $this->info('Permissions copied for ' . $shelves->count() . ' shelves.');\n        return 0;\n    }\n}\n"
  },
  {
    "path": "app/Console/Commands/CreateAdminCommand.php",
    "content": "<?php\n\nnamespace BookStack\\Console\\Commands;\n\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\UserRepo;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Support\\Facades\\Validator;\nuse Illuminate\\Support\\Str;\nuse Illuminate\\Validation\\Rules\\Password;\n\nclass CreateAdminCommand extends Command\n{\n    /**\n     * The name and signature of the console command.\n     *\n     * @var string\n     */\n    protected $signature = 'bookstack:create-admin\n                            {--email= : The email address for the new admin user}\n                            {--name= : The name of the new admin user}\n                            {--password= : The password to assign to the new admin user}\n                            {--external-auth-id= : The external authentication system id for the new admin user (SAML2/LDAP/OIDC)}\n                            {--generate-password : Generate a random password for the new admin user}\n                            {--initial : Indicate if this should set/update the details of the initial admin user}';\n\n    /**\n     * The console command description.\n     *\n     * @var string\n     */\n    protected $description = 'Add a new admin user to the system';\n\n    /**\n     * Execute the console command.\n     */\n    public function handle(UserRepo $userRepo): int\n    {\n        $initialAdminOnly = $this->option('initial');\n        $shouldGeneratePassword = $this->option('generate-password');\n        $details = $this->gatherDetails($shouldGeneratePassword, $initialAdminOnly);\n\n        $validator = Validator::make($details, [\n            'email'            => ['required', 'email', 'min:5'],\n            'name'             => ['required', 'min:2'],\n            'password'         => ['required_without:external_auth_id', Password::default()],\n            'external_auth_id' => ['required_without:password'],\n        ]);\n\n        if ($validator->fails()) {\n            foreach ($validator->errors()->all() as $error) {\n                $this->error($error);\n            }\n\n            return 1;\n        }\n\n        $adminRole = Role::getSystemRole('admin');\n\n        if ($initialAdminOnly) {\n            $handled = $this->handleInitialAdminIfExists($userRepo, $details, $shouldGeneratePassword, $adminRole);\n            if ($handled !== null) {\n                return $handled;\n            }\n        }\n\n        $emailUsed = $userRepo->getByEmail($details['email']) !== null;\n        if ($emailUsed) {\n            $this->error(\"Could not create admin account.\");\n            $this->error(\"An account with the email address \\\"{$details['email']}\\\" already exists.\");\n            return 1;\n        }\n\n        $user = $userRepo->createWithoutActivity($validator->validated());\n        $user->attachRole($adminRole);\n        $user->email_confirmed = true;\n        $user->save();\n\n        if ($shouldGeneratePassword) {\n            $this->line($details['password']);\n        } else {\n            $this->info(\"Admin account with email \\\"{$user->email}\\\" successfully created!\");\n        }\n\n        return 0;\n    }\n\n    /**\n     * Handle updates to the original admin account if it exists.\n     * Returns an int return status if handled, otherwise returns null if not handled (new user to be created).\n     */\n    protected function handleInitialAdminIfExists(UserRepo $userRepo, array $data, bool $generatePassword, Role $adminRole): int|null\n    {\n        $defaultAdmin = $userRepo->getByEmail('admin@admin.com');\n        if ($defaultAdmin && $defaultAdmin->hasSystemRole('admin')) {\n            if ($defaultAdmin->email !== $data['email'] && $userRepo->getByEmail($data['email']) !== null) {\n                $this->error(\"Could not create admin account.\");\n                $this->error(\"An account with the email address \\\"{$data['email']}\\\" already exists.\");\n                return 1;\n            }\n\n            $userRepo->updateWithoutActivity($defaultAdmin, $data, true);\n            if ($generatePassword) {\n                $this->line($data['password']);\n            } else {\n                $this->info(\"The default admin user has been updated with the provided details!\");\n            }\n\n            return 0;\n        } else if ($adminRole->users()->count() > 0) {\n            $this->warn('Non-default admin user already exists. Skipping creation of new admin user.');\n            return 2;\n        }\n\n        return null;\n    }\n\n    protected function gatherDetails(bool $generatePassword, bool $initialAdmin): array\n    {\n        $details = $this->snakeCaseOptions();\n\n        if (empty($details['email'])) {\n            if ($initialAdmin) {\n                $details['email'] = 'admin@example.com';\n            } else {\n                $details['email'] = $this->ask('Please specify an email address for the new admin user');\n            }\n        }\n\n        if (empty($details['name'])) {\n            if ($initialAdmin) {\n                $details['name'] = 'Admin';\n            } else {\n                $details['name'] = $this->ask('Please specify a name for the new admin user');\n            }\n        }\n\n        if (empty($details['password'])) {\n            if (empty($details['external_auth_id'])) {\n                if ($generatePassword) {\n                    $details['password'] = Str::random(32);\n                } else {\n                    $details['password'] = $this->ask('Please specify a password for the new admin user (8 characters min)');\n                }\n            } else {\n                $details['password'] = Str::random(32);\n            }\n        }\n\n        return $details;\n    }\n\n    protected function snakeCaseOptions(): array\n    {\n        $returnOpts = [];\n        foreach ($this->options() as $key => $value) {\n            $returnOpts[str_replace('-', '_', $key)] = $value;\n        }\n\n        return $returnOpts;\n    }\n}\n"
  },
  {
    "path": "app/Console/Commands/DeleteUsersCommand.php",
    "content": "<?php\n\nnamespace BookStack\\Console\\Commands;\n\nuse BookStack\\Users\\Models\\User;\nuse BookStack\\Users\\UserRepo;\nuse Illuminate\\Console\\Command;\n\nclass DeleteUsersCommand extends Command\n{\n    /**\n     * The name and signature of the console command.\n     *\n     * @var string\n     */\n    protected $signature = 'bookstack:delete-users';\n\n    /**\n     * The console command description.\n     *\n     * @var string\n     */\n    protected $description = 'Delete users that are not \"admin\" or system users';\n\n    /**\n     * Execute the console command.\n     */\n    public function handle(UserRepo $userRepo): int\n    {\n        $this->warn('This will delete all users from the system that are not \"admin\" or system users.');\n        $confirm = $this->confirm('Are you sure you want to continue?');\n\n        if (!$confirm) {\n            return 0;\n        }\n\n        $totalUsers = User::query()->count();\n        $numDeleted = 0;\n        $users = User::query()->whereNull('system_name')->with('roles')->get();\n\n        foreach ($users as $user) {\n            if ($user->hasSystemRole('admin')) {\n                // don't delete users with \"admin\" role\n                continue;\n            }\n            $userRepo->destroy($user);\n            $numDeleted++;\n        }\n\n        $this->info(\"Deleted $numDeleted of $totalUsers total users.\");\n        return 0;\n    }\n}\n"
  },
  {
    "path": "app/Console/Commands/HandlesSingleUser.php",
    "content": "<?php\n\nnamespace BookStack\\Console\\Commands;\n\nuse BookStack\\Users\\Models\\User;\nuse Exception;\nuse Illuminate\\Console\\Command;\n\n/**\n * @mixin Command\n */\ntrait HandlesSingleUser\n{\n    /**\n     * Fetch a user provided to this command.\n     * Expects the command to accept 'id' and 'email' options.\n     * @throws Exception\n     */\n    private function fetchProvidedUser(): User\n    {\n        $id = $this->option('id');\n        $email = $this->option('email');\n        if (!$id && !$email) {\n            throw new Exception(\"Either a --id=<number> or --email=<email> option must be provided.\\nRun this command with `--help` to show more options.\");\n        }\n\n        $field = $id ? 'id' : 'email';\n        $value = $id ?: $email;\n\n        $user = User::query()\n            ->where($field, '=', $value)\n            ->first();\n\n        if (!$user) {\n            throw new Exception(\"A user where {$field}={$value} could not be found.\");\n        }\n\n        return $user;\n    }\n}\n"
  },
  {
    "path": "app/Console/Commands/InstallModuleCommand.php",
    "content": "<?php\n\nnamespace BookStack\\Console\\Commands;\n\nuse BookStack\\Http\\HttpRequestService;\nuse BookStack\\Theming\\ThemeModule;\nuse BookStack\\Theming\\ThemeModuleException;\nuse BookStack\\Theming\\ThemeModuleManager;\nuse BookStack\\Theming\\ThemeModuleZip;\nuse GuzzleHttp\\Psr7\\Request;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Support\\Str;\n\nclass InstallModuleCommand extends Command\n{\n    /**\n     * The name and signature of the console command.\n     *\n     * @var string\n     */\n    protected $signature = 'bookstack:install-module\n                            {location : The URL or path of the module file}';\n\n    /**\n     * The console command description.\n     *\n     * @var string\n     */\n    protected $description = 'Install a module to the currently configured theme';\n\n    protected array $cleanupActions = [];\n\n    /**\n     * Execute the console command.\n     */\n    public function handle(): int\n    {\n        $location = $this->argument('location');\n\n        // Get the ZIP file containing the module files\n        $zipPath = $this->getPathToZip($location);\n        if (!$zipPath) {\n            $this->cleanup();\n            return 1;\n        }\n\n        // Validate module zip file (metadata, size, etc...) and get module instance\n        $zip = new ThemeModuleZip($zipPath);\n        $themeModule = $this->validateAndGetModuleInfoFromZip($zip);\n        if (!$themeModule) {\n            $this->cleanup();\n            return 1;\n        }\n\n        // Get the theme folder in use, attempting to create one if no active theme in use\n        $themeFolder = $this->getThemeFolder();\n        if (!$themeFolder) {\n            $this->cleanup();\n            return 1;\n        }\n\n        // Get the modules folder of the theme, attempting to create it if not existing,\n        // and create a new module manager instance.\n        $moduleFolder = $this->getModuleFolder($themeFolder);\n        if (!$moduleFolder) {\n            $this->cleanup();\n            return 1;\n        }\n\n        $manager = new ThemeModuleManager($moduleFolder);\n\n        // Handle existing modules with the same name\n        $exitingModulesWithName = $manager->getByName($themeModule->name);\n        $shouldContinue = $this->handleExistingModulesWithSameName($exitingModulesWithName, $manager);\n        if (!$shouldContinue) {\n            $this->cleanup();\n            return 1;\n        }\n\n        // Extract module ZIP into the theme modules folder\n        try {\n            $newModule = $manager->addFromZip($themeModule->name, $zip);\n        } catch (ThemeModuleException $exception) {\n            $this->error(\"ERROR: Failed to install module with error: {$exception->getMessage()}\");\n            $this->cleanup();\n            return 1;\n        }\n\n        $this->info(\"Module \\\"{$newModule->name}\\\" ({$newModule->getVersion()}) successfully installed!\");\n        $this->info(\"Install location: {$moduleFolder}/{$newModule->folderName}\");\n        $this->cleanup();\n        return 0;\n    }\n\n    /**\n     * @param ThemeModule[] $existingModules\n     */\n    protected function handleExistingModulesWithSameName(array $existingModules, ThemeModuleManager $manager): bool\n    {\n        if (count($existingModules) === 0) {\n            return true;\n        }\n\n        $this->warn(\"The following modules already exist with the same name:\");\n        foreach ($existingModules as $folder => $module) {\n            $this->line(\"{$module->name} ({$folder}:{$module->getVersion()}) - {$module->description}\");\n        }\n        $this->line('');\n\n        $choices = ['Cancel module install', 'Add alongside existing module'];\n        if (count($existingModules) === 1) {\n            $choices[] = 'Replace existing module';\n        }\n        $choice = $this->choice(\"What would you like to do?\", $choices, 0, null, false);\n        if ($choice === 'Cancel module install') {\n            return false;\n        }\n\n        if ($choice === 'Replace existing module') {\n            $existingModuleFolder = array_key_first($existingModules);\n            $this->info(\"Replacing existing module in {$existingModuleFolder} folder\");\n            $manager->deleteModuleFolder($existingModuleFolder);\n        }\n\n        return true;\n    }\n\n    protected function getModuleFolder(string $themeFolder): string|null\n    {\n        $path = $themeFolder . DIRECTORY_SEPARATOR . 'modules';\n\n        if (file_exists($path) && !is_dir($path)) {\n            $this->error(\"ERROR: Cannot create a modules folder, file already exists at {$path}\");\n            return null;\n        }\n\n        if (!file_exists($path)) {\n            $created = mkdir($path, 0755, true);\n            if (!$created) {\n                $this->error(\"ERROR: Failed to create a modules folder at {$path}\");\n                return null;\n            }\n        }\n\n        return $path;\n    }\n\n    protected function getThemeFolder(): string|null\n    {\n        $path = theme_path('');\n        if (!$path || !is_dir($path)) {\n            $shouldCreate = $this->confirm('No active theme folder found, would you like to create one?');\n            if (!$shouldCreate) {\n                return null;\n            }\n\n            $folder = 'custom';\n            while (file_exists(base_path(\"themes\" . DIRECTORY_SEPARATOR  . $folder))) {\n                $folder = 'custom-' . Str::random(4);\n            }\n\n            $path = base_path(\"themes/{$folder}\");\n            $created = mkdir($path, 0755, true);\n            if (!$created) {\n                $this->error('Failed to create a theme folder to use. This may be a permissions issue. Try manually configuring an active theme');\n                return null;\n            }\n\n            $this->info(\"Created theme folder at {$path}\");\n            $this->warn(\"You will need to set APP_THEME={$folder} in your BookStack env configuration to enable this theme!\");\n        }\n\n        return $path;\n    }\n\n    protected function validateAndGetModuleInfoFromZip(ThemeModuleZip $zip): ThemeModule|null\n    {\n        if (!$zip->exists()) {\n            $this->error(\"ERROR: Cannot open ZIP file at {$zip->getPath()}\");\n            return null;\n        }\n\n        if ($zip->getContentsSize() > (50 * 1024 * 1024)) {\n            $this->error(\"ERROR: Module ZIP file contents are too large. Maximum size is 50MB\");\n            return null;\n        }\n\n        try {\n            $themeModule = $zip->getModuleInstance();\n        } catch (ThemeModuleException $exception) {\n            $this->error(\"ERROR: Failed to read module metadata with error: {$exception->getMessage()}\");\n            return null;\n        }\n\n        return $themeModule;\n    }\n\n    protected function downloadModuleFile(string $location): string|null\n    {\n        $httpRequests = app()->make(HttpRequestService::class);\n        $client = $httpRequests->buildClient(30, ['stream' => true]);\n        $originalUrl = parse_url($location);\n        $currentLocation = $location;\n        $maxRedirects = 3;\n        $redirectCount = 0;\n\n        // Follow redirects up to 3 times for the same hostname\n        do {\n            $resp = $client->sendRequest(new Request('GET', $currentLocation));\n            $statusCode = $resp->getStatusCode();\n\n            if ($statusCode >= 300 && $statusCode < 400 && $redirectCount < $maxRedirects) {\n                $redirectLocation = $resp->getHeaderLine('Location');\n                if ($redirectLocation) {\n                    $redirectUrl = parse_url($redirectLocation);\n                    if (\n                        ($originalUrl['host'] ?? '') === ($redirectUrl['host'] ?? '')\n                        && ($originalUrl['scheme'] ?? '') === ($redirectUrl['scheme'] ?? '')\n                        && ($originalUrl['port'] ?? '') === ($redirectUrl['port'] ?? '')\n                    ) {\n                        $currentLocation = $redirectLocation;\n                        $redirectCount++;\n                        continue;\n                    }\n                }\n            }\n\n            break;\n        } while (true);\n\n        if ($resp->getStatusCode() >= 300) {\n            $this->error(\"ERROR: Failed to download module from {$location}\");\n            $this->error(\"Download failed with status code {$resp->getStatusCode()}\");\n            return null;\n        }\n\n        $tempFile = tempnam(sys_get_temp_dir(), 'bookstack_module_');\n        $fileHandle = fopen($tempFile, 'w');\n        $respBody = $resp->getBody();\n        $size = 0;\n        $maxSize = 50 * 1024 * 1024;\n\n        while (!$respBody->eof()) {\n            fwrite($fileHandle, $respBody->read(1024));\n            $size += 1024;\n            if ($size > $maxSize) {\n                fclose($fileHandle);\n                unlink($tempFile);\n                $this->error(\"ERROR: Module ZIP file is too large. Maximum size is 50MB\");\n                return '';\n            }\n        }\n\n        fclose($fileHandle);\n\n        $this->cleanupActions[] = function () use ($tempFile) {\n            unlink($tempFile);\n        };\n\n        return $tempFile;\n    }\n\n    protected function getPathToZip(string $location): string|null\n    {\n        $lowerLocation = strtolower($location);\n        $isRemote = str_starts_with($lowerLocation, 'http://') || str_starts_with($lowerLocation, 'https://');\n\n        if ($isRemote) {\n            // Warning about fetching from source\n            $host = parse_url($location, PHP_URL_HOST);\n            $this->warn(\"\\nThis will download a module from: {$host}\\n\\nModules can contain code which would have the ability to do anything on the BookStack host server.\\nYou should only install modules from trusted sources.\");\n            $trustHost = $this->confirm('Are you sure you trust this source?');\n            if (!$trustHost) {\n                return null;\n            }\n\n            // Check if the connection is http. If so, warn the user.\n            if (str_starts_with($lowerLocation, 'http://')) {\n                $this->warn(\"You are downloading a module from an insecure HTTP source.\\nWe recommend only using HTTPS sources to avoid various security risks.\");\n                if (!$this->confirm('Are you sure you want to continue without HTTPS?')) {\n                    return null;\n                }\n            }\n\n            // Download ZIP and get its location\n            return $this->downloadModuleFile($location);\n        }\n\n        // Validate the file and get the full location\n        $zipPath = realpath($location);\n\n        if (!$zipPath || !is_file($zipPath)) {\n            $this->error(\"ERROR: Module file not found at {$location}\");\n            return null;\n        }\n\n        $this->warn(\"\\nThis will install a module from: {$zipPath}\\n\\nModules can contain code which would have the ability to do anything on the BookStack host server.\\nYou should only install modules from trusted sources.\");\n        $trustHost = $this->confirm('Are you sure you want to install this module?');\n        if (!$trustHost) {\n            return null;\n        }\n\n        return $zipPath;\n    }\n\n    protected function cleanup(): void\n    {\n        foreach ($this->cleanupActions as $action) {\n            $action();\n        }\n    }\n}\n"
  },
  {
    "path": "app/Console/Commands/RefreshAvatarCommand.php",
    "content": "<?php\n\nnamespace BookStack\\Console\\Commands;\n\nuse BookStack\\Users\\Models\\User;\nuse Exception;\nuse Illuminate\\Console\\Command;\nuse BookStack\\Uploads\\UserAvatars;\n\nclass RefreshAvatarCommand extends Command\n{\n    use HandlesSingleUser;\n\n    /**\n     * The name and signature of the console command.\n     *\n     * @var string\n     */\n    protected $signature = 'bookstack:refresh-avatar\n                            {--id= : Numeric ID of the user to refresh avatar for}\n                            {--email= : Email address of the user to refresh avatar for}\n                            {--users-without-avatars : Refresh avatars for users that currently have no avatar}\n                            {--a|all : Refresh avatars for all users}\n                            {--f|force : Actually run the update, Defaults to a dry-run}';\n\n    /**\n     * The console command description.\n     *\n     * @var string\n     */\n    protected $description = 'Refresh avatar for the given user(s)';\n\n    public function handle(UserAvatars $userAvatar): int\n    {\n        if (!$userAvatar->avatarFetchEnabled()) {\n            $this->error(\"Avatar fetching is disabled on this instance.\");\n            return self::FAILURE;\n        }\n\n        if ($this->option('users-without-avatars')) {\n            return $this->processUsers(User::query()->whereDoesntHave('avatar')->get()->all(), $userAvatar);\n        }\n\n        if ($this->option('all')) {\n            return $this->processUsers(User::query()->get()->all(), $userAvatar);\n        }\n\n        try {\n            $user = $this->fetchProvidedUser();\n            return $this->processUsers([$user], $userAvatar);\n        } catch (Exception $exception) {\n            $this->error($exception->getMessage());\n            return self::FAILURE;\n        }\n    }\n\n    /**\n     * @param User[] $users\n     */\n    private function processUsers(array $users, UserAvatars $userAvatar): int\n    {\n        $dryRun = !$this->option('force');\n        $this->info(count($users) . \" user(s) found to update avatars for.\");\n\n        if (count($users) === 0) {\n            return self::SUCCESS;\n        }\n\n        if (!$dryRun) {\n            $fetchHost = parse_url($userAvatar->getAvatarUrl(), PHP_URL_HOST);\n            $this->warn(\"This will destroy any existing avatar images these users have, and attempt to fetch new avatar images from {$fetchHost}.\");\n            $proceed = !$this->input->isInteractive() || $this->confirm('Are you sure you want to proceed?');\n            if (!$proceed) {\n                return self::SUCCESS;\n            }\n        }\n\n        $this->info(\"\");\n\n        $exitCode = self::SUCCESS;\n        foreach ($users as $user) {\n            $linePrefix = \"[ID: {$user->id}] $user->email -\";\n\n            if ($dryRun) {\n                $this->warn(\"{$linePrefix} Not updated\");\n                continue;\n            }\n\n            if ($this->fetchAvatar($userAvatar, $user)) {\n                $this->info(\"{$linePrefix} Updated\");\n            } else {\n                $this->error(\"{$linePrefix} Not updated\");\n                $exitCode = self::FAILURE;\n            }\n        }\n\n        if ($dryRun) {\n            $this->comment(\"\");\n            $this->comment(\"Dry run, no avatars were updated.\");\n            $this->comment('Run with -f or --force to perform the update.');\n        }\n\n        return $exitCode;\n    }\n\n    private function fetchAvatar(UserAvatars $userAvatar, User $user): bool\n    {\n        $oldId = $user->avatar->id ?? 0;\n\n        $userAvatar->fetchAndAssignToUser($user);\n\n        $user->refresh();\n        $newId = $user->avatar->id ?? $oldId;\n        return $oldId !== $newId;\n    }\n}\n"
  },
  {
    "path": "app/Console/Commands/RegeneratePermissionsCommand.php",
    "content": "<?php\n\nnamespace BookStack\\Console\\Commands;\n\nuse BookStack\\Permissions\\JointPermissionBuilder;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Support\\Facades\\DB;\n\nclass RegeneratePermissionsCommand extends Command\n{\n    /**\n     * The name and signature of the console command.\n     *\n     * @var string\n     */\n    protected $signature = 'bookstack:regenerate-permissions \n                            {--database= : The database connection to use}';\n\n    /**\n     * The console command description.\n     *\n     * @var string\n     */\n    protected $description = 'Regenerate all system permissions';\n\n    /**\n     * Execute the console command.\n     */\n    public function handle(JointPermissionBuilder $permissionBuilder): int\n    {\n        $connection = DB::getDefaultConnection();\n\n        if ($this->option('database')) {\n            DB::setDefaultConnection($this->option('database'));\n        }\n\n        $permissionBuilder->rebuildForAll();\n\n        DB::setDefaultConnection($connection);\n        $this->comment('Permissions regenerated');\n\n        return 0;\n    }\n}\n"
  },
  {
    "path": "app/Console/Commands/RegenerateReferencesCommand.php",
    "content": "<?php\n\nnamespace BookStack\\Console\\Commands;\n\nuse BookStack\\References\\ReferenceStore;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Support\\Facades\\DB;\n\nclass RegenerateReferencesCommand extends Command\n{\n    /**\n     * The name and signature of the console command.\n     *\n     * @var string\n     */\n    protected $signature = 'bookstack:regenerate-references\n                            {--database= : The database connection to use}';\n\n    /**\n     * The console command description.\n     *\n     * @var string\n     */\n    protected $description = 'Regenerate all the cross-item model reference index';\n\n    /**\n     * Execute the console command.\n     */\n    public function handle(ReferenceStore $references): int\n    {\n        $connection = DB::getDefaultConnection();\n\n        if ($this->option('database')) {\n            DB::setDefaultConnection($this->option('database'));\n        }\n\n        $references->updateForAll();\n\n        DB::setDefaultConnection($connection);\n\n        $this->comment('References have been regenerated');\n\n        return 0;\n    }\n}\n"
  },
  {
    "path": "app/Console/Commands/RegenerateSearchCommand.php",
    "content": "<?php\n\nnamespace BookStack\\Console\\Commands;\n\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Search\\SearchIndex;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Support\\Facades\\DB;\n\nclass RegenerateSearchCommand extends Command\n{\n    /**\n     * The name and signature of the console command.\n     *\n     * @var string\n     */\n    protected $signature = 'bookstack:regenerate-search \n                            {--database= : The database connection to use}';\n\n    /**\n     * The console command description.\n     *\n     * @var string\n     */\n    protected $description = 'Re-index all content for searching';\n\n    /**\n     * Execute the console command.\n     */\n    public function handle(SearchIndex $searchIndex): int\n    {\n        $connection = DB::getDefaultConnection();\n        if ($this->option('database') !== null) {\n            DB::setDefaultConnection($this->option('database'));\n        }\n\n        $searchIndex->indexAllEntities(function (Entity $model, int $processed, int $total): void {\n            $this->info('Indexed ' . class_basename($model) . ' entries (' . $processed . '/' . $total . ')');\n        });\n\n        DB::setDefaultConnection($connection);\n        $this->line('Search index regenerated!');\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "app/Console/Commands/ResetMfaCommand.php",
    "content": "<?php\n\nnamespace BookStack\\Console\\Commands;\n\nuse Exception;\nuse Illuminate\\Console\\Command;\n\nclass ResetMfaCommand extends Command\n{\n    use HandlesSingleUser;\n\n    /**\n     * The name and signature of the console command.\n     *\n     * @var string\n     */\n    protected $signature = 'bookstack:reset-mfa\n                            {--id= : Numeric ID of the user to reset MFA for}\n                            {--email= : Email address of the user to reset MFA for} \n                            ';\n\n    /**\n     * The console command description.\n     *\n     * @var string\n     */\n    protected $description = 'Reset & Clear any configured MFA methods for the given user';\n\n    /**\n     * Execute the console command.\n     */\n    public function handle(): int\n    {\n        try {\n            $user = $this->fetchProvidedUser();\n        } catch (Exception $exception) {\n            $this->error($exception->getMessage());\n            return 1;\n        }\n\n        $this->info(\"This will delete any configure multi-factor authentication methods for user: \\n- ID: {$user->id}\\n- Name: {$user->name}\\n- Email: {$user->email}\\n\");\n        $this->info('If multi-factor authentication is required for this user they will be asked to reconfigure their methods on next login.');\n        $confirm = $this->confirm('Are you sure you want to proceed?');\n        if (!$confirm) {\n            return 1;\n        }\n\n        $user->mfaValues()->delete();\n        $this->info('User MFA methods have been reset.');\n\n        return 0;\n    }\n}\n"
  },
  {
    "path": "app/Console/Commands/UpdateUrlCommand.php",
    "content": "<?php\n\nnamespace BookStack\\Console\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Database\\Connection;\n\nclass UpdateUrlCommand extends Command\n{\n    /**\n     * The name and signature of the console command.\n     *\n     * @var string\n     */\n    protected $signature = 'bookstack:update-url\n                            {oldUrl : URL to replace}\n                            {newUrl : URL to use as the replacement}\n                            {--force : Force the operation to run, ignoring confirmations}';\n\n    /**\n     * The console command description.\n     *\n     * @var string\n     */\n    protected $description = 'Find and replace the given URLs in your BookStack database';\n\n    /**\n     * Execute the console command.\n     */\n    public function handle(Connection $db): int\n    {\n        $oldUrl = str_replace(\"'\", '', $this->argument('oldUrl'));\n        $newUrl = str_replace(\"'\", '', $this->argument('newUrl'));\n\n        $urlPattern = '/https?:\\/\\/(.+)/';\n        if (!preg_match($urlPattern, $oldUrl) || !preg_match($urlPattern, $newUrl)) {\n            $this->error('The given urls are expected to be full urls starting with http:// or https://');\n\n            return 1;\n        }\n\n        if (!$this->checkUserOkayToProceed($oldUrl, $newUrl)) {\n            return 1;\n        }\n\n        $columnsToUpdateByTable = [\n            'attachments' => ['path'],\n            'entity_page_data' => ['html', 'text', 'markdown'],\n            'entity_container_data' => ['description_html'],\n            'page_revisions' => ['html', 'text', 'markdown'],\n            'images'      => ['url'],\n            'settings'    => ['value'],\n            'comments'    => ['html'],\n        ];\n\n        foreach ($columnsToUpdateByTable as $table => $columns) {\n            foreach ($columns as $column) {\n                $changeCount = $this->replaceValueInTable($db, $table, $column, $oldUrl, $newUrl);\n                $this->info(\"Updated {$changeCount} rows in {$table}->{$column}\");\n            }\n        }\n\n        $jsonColumnsToUpdateByTable = [\n            'settings' => ['value'],\n        ];\n\n        foreach ($jsonColumnsToUpdateByTable as $table => $columns) {\n            foreach ($columns as $column) {\n                $oldJson = trim(json_encode($oldUrl), '\"');\n                $newJson = trim(json_encode($newUrl), '\"');\n                $changeCount = $this->replaceValueInTable($db, $table, $column, $oldJson, $newJson);\n                $this->info(\"Updated {$changeCount} JSON encoded rows in {$table}->{$column}\");\n            }\n        }\n\n        $this->info('URL update procedure complete.');\n        $this->info('============================================================================');\n        $this->info('Be sure to run \"php artisan cache:clear\" to clear any old URLs in the cache.');\n\n        if (!str_starts_with($newUrl, url('/'))) {\n            $this->warn('You still need to update your APP_URL env value. This is currently set to:');\n            $this->warn(url('/'));\n        }\n\n        $this->info('============================================================================');\n\n        return 0;\n    }\n\n    /**\n     * Perform a find+replace operations in the provided table and column.\n     * Returns the count of rows changed.\n     */\n    protected function replaceValueInTable(\n        Connection $db,\n        string $table,\n        string $column,\n        string $oldUrl,\n        string $newUrl\n    ): int {\n        $oldQuoted = $db->getPdo()->quote($oldUrl);\n        $newQuoted = $db->getPdo()->quote($newUrl);\n\n        return $db->table($table)->update([\n            $column => $db->raw(\"REPLACE({$column}, {$oldQuoted}, {$newQuoted})\"),\n        ]);\n    }\n\n    /**\n     * Warn the user of the dangers of this operation.\n     * Returns a boolean indicating if they've accepted the warnings.\n     */\n    protected function checkUserOkayToProceed(string $oldUrl, string $newUrl): bool\n    {\n        if ($this->option('force')) {\n            return true;\n        }\n\n        $dangerWarning = \"This will search for \\\"{$oldUrl}\\\" in your database and replace it with  \\\"{$newUrl}\\\".\\n\";\n        $dangerWarning .= 'Are you sure you want to proceed?';\n        $backupConfirmation = 'This operation could cause issues if used incorrectly. Have you made a backup of your existing database?';\n\n        return $this->confirm($dangerWarning) && $this->confirm($backupConfirmation);\n    }\n}\n"
  },
  {
    "path": "app/Console/Commands/UpgradeDatabaseEncodingCommand.php",
    "content": "<?php\n\nnamespace BookStack\\Console\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Support\\Facades\\DB;\n\nclass UpgradeDatabaseEncodingCommand extends Command\n{\n    /**\n     * The name and signature of the console command.\n     *\n     * @var string\n     */\n    protected $signature = 'bookstack:db-utf8mb4 \n                            {--database= : The database connection to use}';\n\n    /**\n     * The console command description.\n     *\n     * @var string\n     */\n    protected $description = 'Generate SQL commands to upgrade the database to UTF8mb4';\n\n\n    /**\n     * Execute the console command.\n     */\n    public function handle(): int\n    {\n        $connection = DB::getDefaultConnection();\n        if ($this->option('database') !== null) {\n            DB::setDefaultConnection($this->option('database'));\n        }\n\n        $database = DB::getDatabaseName();\n        $tables = DB::select('SHOW TABLES');\n        $this->line('ALTER DATABASE `' . $database . '` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');\n        $this->line('USE `' . $database . '`;');\n        $key = 'Tables_in_' . $database;\n        foreach ($tables as $table) {\n            $tableName = $table->$key;\n            $this->line(\"ALTER TABLE `{$tableName}` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;\");\n        }\n\n        DB::setDefaultConnection($connection);\n\n        return 0;\n    }\n}\n"
  },
  {
    "path": "app/Console/Kernel.php",
    "content": "<?php\n\nnamespace BookStack\\Console;\n\nuse Illuminate\\Console\\Scheduling\\Schedule;\nuse Illuminate\\Foundation\\Console\\Kernel as ConsoleKernel;\n\nclass Kernel extends ConsoleKernel\n{\n    /**\n     * Define the application's command schedule.\n     *\n     * @param \\Illuminate\\Console\\Scheduling\\Schedule $schedule\n     *\n     * @return void\n     */\n    protected function schedule(Schedule $schedule)\n    {\n        //\n    }\n\n    /**\n     * Register the commands for the application.\n     *\n     * @return void\n     */\n    protected function commands()\n    {\n        $this->load(__DIR__ . '/Commands');\n    }\n}\n"
  },
  {
    "path": "app/Entities/BreadcrumbsViewComposer.php",
    "content": "<?php\n\nnamespace BookStack\\Entities;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Tools\\ShelfContext;\nuse Illuminate\\View\\View;\n\nclass BreadcrumbsViewComposer\n{\n    public function __construct(\n        protected ShelfContext $shelfContext\n    ) {\n    }\n\n    /**\n     * Modify data when the view is composed.\n     */\n    public function compose(View $view): void\n    {\n        $crumbs = $view->getData()['crumbs'];\n        $firstCrumb = $crumbs[0] ?? null;\n\n        if ($firstCrumb instanceof Book) {\n            $shelf = $this->shelfContext->getContextualShelfForBook($firstCrumb);\n            if ($shelf) {\n                array_unshift($crumbs, $shelf);\n                $view->with('crumbs', $crumbs);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/Entities/Controllers/BookApiController.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Controllers;\n\nuse BookStack\\Api\\ApiEntityListFormatter;\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Queries\\BookQueries;\nuse BookStack\\Entities\\Queries\\BookshelfQueries;\nuse BookStack\\Entities\\Queries\\PageQueries;\nuse BookStack\\Entities\\Repos\\BookRepo;\nuse BookStack\\Entities\\Tools\\BookContents;\nuse BookStack\\Http\\ApiController;\nuse BookStack\\Permissions\\Permission;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Validation\\ValidationException;\n\nclass BookApiController extends ApiController\n{\n    public function __construct(\n        protected BookRepo $bookRepo,\n        protected BookQueries $queries,\n        protected PageQueries $pageQueries,\n        protected BookshelfQueries $shelfQueries,\n    ) {\n    }\n\n    /**\n     * Get a listing of books visible to the user.\n     */\n    public function list()\n    {\n        $books = $this->queries\n            ->visibleForList()\n            ->with(['cover:id,name,url'])\n            ->addSelect(['created_by', 'updated_by']);\n\n        return $this->apiListingResponse($books, [\n            'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by',\n        ]);\n    }\n\n    /**\n     * Create a new book in the system.\n     * The cover image of a book can be set by sending a file via an 'image' property within a 'multipart/form-data' request.\n     * If the 'image' property is null then the book cover image will be removed.\n     *\n     * @throws ValidationException\n     */\n    public function create(Request $request)\n    {\n        $this->checkPermission(Permission::BookCreateAll);\n        $requestData = $this->validate($request, $this->rules()['create']);\n\n        $book = $this->bookRepo->create($requestData);\n\n        return response()->json($this->forJsonDisplay($book));\n    }\n\n    /**\n     * View the details of a single book.\n     * The response data will contain a 'content' property listing the chapter and pages directly within, in\n     * the same structure as you'd see within the BookStack interface when viewing a book. Top-level\n     * contents will have a 'type' property to distinguish between pages and chapters.\n     */\n    public function read(string $id)\n    {\n        $book = $this->queries->findVisibleByIdOrFail(intval($id));\n        $book = $this->forJsonDisplay($book);\n        $book->load([\n            'createdBy',\n            'updatedBy',\n            'ownedBy',\n            'shelves' => function (BelongsToMany $query) {\n                $query->select(['id', 'name', 'slug'])->scopes('visible');\n            }\n        ]);\n\n        $contents = (new BookContents($book))->getTree(true, false)->all();\n        $contentsApiData = (new ApiEntityListFormatter($contents))\n            ->withType()\n            ->withField('pages', function (Entity $entity) {\n                if ($entity instanceof Chapter) {\n                    $pages = $this->pageQueries->visibleForChapterList($entity->id)->get()->all();\n                    return (new ApiEntityListFormatter($pages))->format();\n                }\n                return null;\n            })->format();\n        $book->setAttribute('contents', $contentsApiData);\n\n        return response()->json($book);\n    }\n\n    /**\n     * Update the details of a single book.\n     * The cover image of a book can be set by sending a file via an 'image' property within a 'multipart/form-data' request.\n     * If the 'image' property is null then the book cover image will be removed.\n     *\n     * @throws ValidationException\n     */\n    public function update(Request $request, string $id)\n    {\n        $book = $this->queries->findVisibleByIdOrFail(intval($id));\n        $this->checkOwnablePermission(Permission::BookUpdate, $book);\n\n        $requestData = $this->validate($request, $this->rules()['update']);\n        $book = $this->bookRepo->update($book, $requestData);\n\n        return response()->json($this->forJsonDisplay($book));\n    }\n\n    /**\n     * Delete a single book.\n     * This will typically send the book to the recycle bin.\n     *\n     * @throws \\Exception\n     */\n    public function delete(string $id)\n    {\n        $book = $this->queries->findVisibleByIdOrFail(intval($id));\n        $this->checkOwnablePermission(Permission::BookDelete, $book);\n\n        $this->bookRepo->destroy($book);\n\n        return response('', 204);\n    }\n\n    protected function forJsonDisplay(Book $book): Book\n    {\n        $book = clone $book;\n        $book->unsetRelations()->refresh();\n\n        $book->load(['tags']);\n        $book->makeVisible(['cover', 'description_html'])\n            ->setAttribute('description_html', $book->descriptionInfo()->getHtml())\n            ->setAttribute('cover', $book->coverInfo()->getImage());\n\n        return $book;\n    }\n\n    protected function rules(): array\n    {\n        return [\n            'create' => [\n                'name'                => ['required', 'string', 'max:255'],\n                'description'         => ['string', 'max:1900'],\n                'description_html'    => ['string', 'max:2000'],\n                'tags'                => ['array'],\n                'image'               => array_merge(['nullable'], $this->getImageValidationRules()),\n                'default_template_id' => ['nullable', 'integer'],\n            ],\n            'update' => [\n                'name'                => ['string', 'min:1', 'max:255'],\n                'description'         => ['string', 'max:1900'],\n                'description_html'    => ['string', 'max:2000'],\n                'tags'                => ['array'],\n                'image'               => array_merge(['nullable'], $this->getImageValidationRules()),\n                'default_template_id' => ['nullable', 'integer'],\n            ],\n        ];\n    }\n}\n"
  },
  {
    "path": "app/Entities/Controllers/BookController.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Controllers;\n\nuse BookStack\\Activity\\ActivityQueries;\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Activity\\Models\\View;\nuse BookStack\\Activity\\Tools\\UserEntityWatchOptions;\nuse BookStack\\Entities\\Queries\\BookQueries;\nuse BookStack\\Entities\\Queries\\BookshelfQueries;\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse BookStack\\Entities\\Repos\\BookRepo;\nuse BookStack\\Entities\\Tools\\BookContents;\nuse BookStack\\Entities\\Tools\\Cloner;\nuse BookStack\\Entities\\Tools\\HierarchyTransformer;\nuse BookStack\\Entities\\Tools\\ShelfContext;\nuse BookStack\\Exceptions\\ImageUploadException;\nuse BookStack\\Exceptions\\NotFoundException;\nuse BookStack\\Facades\\Activity;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\References\\ReferenceFetcher;\nuse BookStack\\Util\\DatabaseTransaction;\nuse BookStack\\Util\\SimpleListOptions;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Validation\\ValidationException;\nuse Throwable;\n\nclass BookController extends Controller\n{\n    public function __construct(\n        protected ShelfContext $shelfContext,\n        protected BookRepo $bookRepo,\n        protected BookQueries $queries,\n        protected EntityQueries $entityQueries,\n        protected BookshelfQueries $shelfQueries,\n        protected ReferenceFetcher $referenceFetcher,\n    ) {\n    }\n\n    /**\n     * Display a listing of the book.\n     */\n    public function index(Request $request)\n    {\n        $view = setting()->getForCurrentUser('books_view_type');\n        $listOptions = SimpleListOptions::fromRequest($request, 'books')->withSortOptions([\n            'name' => trans('common.sort_name'),\n            'created_at' => trans('common.sort_created_at'),\n            'updated_at' => trans('common.sort_updated_at'),\n        ]);\n\n        $books = $this->queries->visibleForListWithCover()\n            ->orderBy($listOptions->getSort(), $listOptions->getOrder())\n            ->paginate(setting()->getInteger('lists-page-count-books', 18, 1, 1000));\n        $recents = $this->isSignedIn() ? $this->queries->recentlyViewedForCurrentUser()->take(4)->get() : false;\n        $popular = $this->queries->popularForList()->take(4)->get();\n        $new = $this->queries->visibleForList()->orderBy('created_at', 'desc')->take(4)->get();\n\n        $this->shelfContext->clearShelfContext();\n\n        $this->setPageTitle(trans('entities.books'));\n\n        return view('books.index', [\n            'books'   => $books,\n            'recents' => $recents,\n            'popular' => $popular,\n            'new'     => $new,\n            'view'    => $view,\n            'listOptions' => $listOptions,\n        ]);\n    }\n\n    /**\n     * Show the form for creating a new book.\n     */\n    public function create(?string $shelfSlug = null)\n    {\n        $this->checkPermission(Permission::BookCreateAll);\n\n        $bookshelf = null;\n        if ($shelfSlug !== null) {\n            $bookshelf = $this->shelfQueries->findVisibleBySlugOrFail($shelfSlug);\n            $this->checkOwnablePermission(Permission::BookshelfUpdate, $bookshelf);\n        }\n\n        $this->setPageTitle(trans('entities.books_create'));\n\n        return view('books.create', [\n            'bookshelf' => $bookshelf,\n        ]);\n    }\n\n    /**\n     * Store a newly created book in storage.\n     *\n     * @throws ImageUploadException\n     * @throws ValidationException\n     */\n    public function store(Request $request, ?string $shelfSlug = null)\n    {\n        $this->checkPermission(Permission::BookCreateAll);\n        $validated = $this->validate($request, [\n            'name'                => ['required', 'string', 'max:255'],\n            'description_html'    => ['string', 'max:2000'],\n            'image'               => array_merge(['nullable'], $this->getImageValidationRules()),\n            'tags'                => ['array'],\n            'default_template_id' => ['nullable', 'integer'],\n        ]);\n\n        $bookshelf = null;\n        if ($shelfSlug !== null) {\n            $bookshelf = $this->shelfQueries->findVisibleBySlugOrFail($shelfSlug);\n            $this->checkOwnablePermission(Permission::BookshelfUpdate, $bookshelf);\n        }\n\n        $book = $this->bookRepo->create($validated);\n\n        if ($bookshelf) {\n            $bookshelf->appendBook($book);\n            Activity::add(ActivityType::BOOKSHELF_UPDATE, $bookshelf);\n        }\n\n        return redirect($book->getUrl());\n    }\n\n    /**\n     * Display the specified book.\n     */\n    public function show(Request $request, ActivityQueries $activities, string $slug)\n    {\n        try {\n            $book = $this->queries->findVisibleBySlugOrFail($slug);\n        } catch (NotFoundException $exception) {\n            $book = $this->entityQueries->findVisibleByOldSlugs('book', $slug);\n            if (is_null($book)) {\n                throw $exception;\n            }\n            return redirect($book->getUrl());\n        }\n\n        $bookChildren = (new BookContents($book))->getTree(true);\n        $bookParentShelves = $book->shelves()->scopes('visible')->get();\n\n        View::incrementFor($book);\n        if ($request->has('shelf')) {\n            $this->shelfContext->setShelfContext(intval($request->get('shelf')));\n        }\n\n        $this->setPageTitle($book->getShortName());\n\n        return view('books.show', [\n            'book'              => $book,\n            'current'           => $book,\n            'bookChildren'      => $bookChildren,\n            'bookParentShelves' => $bookParentShelves,\n            'watchOptions'      => new UserEntityWatchOptions(user(), $book),\n            'activity'          => $activities->entityActivity($book, 20, 1),\n            'referenceCount'    => $this->referenceFetcher->getReferenceCountToEntity($book),\n        ]);\n    }\n\n    /**\n     * Show the form for editing the specified book.\n     */\n    public function edit(string $slug)\n    {\n        $book = $this->queries->findVisibleBySlugOrFail($slug);\n        $this->checkOwnablePermission(Permission::BookUpdate, $book);\n        $this->setPageTitle(trans('entities.books_edit_named', ['bookName' => $book->getShortName()]));\n\n        return view('books.edit', ['book' => $book, 'current' => $book]);\n    }\n\n    /**\n     * Update the specified book in storage.\n     *\n     * @throws ImageUploadException\n     * @throws ValidationException\n     * @throws Throwable\n     */\n    public function update(Request $request, string $slug)\n    {\n        $book = $this->queries->findVisibleBySlugOrFail($slug);\n        $this->checkOwnablePermission(Permission::BookUpdate, $book);\n\n        $validated = $this->validate($request, [\n            'name'                => ['required', 'string', 'max:255'],\n            'description_html'    => ['string', 'max:2000'],\n            'image'               => array_merge(['nullable'], $this->getImageValidationRules()),\n            'tags'                => ['array'],\n            'default_template_id' => ['nullable', 'integer'],\n        ]);\n\n        if ($request->has('image_reset')) {\n            $validated['image'] = null;\n        } elseif (array_key_exists('image', $validated) && is_null($validated['image'])) {\n            unset($validated['image']);\n        }\n\n        $book = $this->bookRepo->update($book, $validated);\n\n        return redirect($book->getUrl());\n    }\n\n    /**\n     * Shows the page to confirm deletion.\n     */\n    public function showDelete(string $bookSlug)\n    {\n        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);\n        $this->checkOwnablePermission(Permission::BookDelete, $book);\n        $this->setPageTitle(trans('entities.books_delete_named', ['bookName' => $book->getShortName()]));\n\n        return view('books.delete', ['book' => $book, 'current' => $book]);\n    }\n\n    /**\n     * Remove the specified book from the system.\n     *\n     * @throws Throwable\n     */\n    public function destroy(string $bookSlug)\n    {\n        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);\n        $this->checkOwnablePermission(Permission::BookDelete, $book);\n        $contextShelf = $this->shelfContext->getContextualShelfForBook($book);\n\n        $this->bookRepo->destroy($book);\n\n        if ($contextShelf) {\n            return redirect($contextShelf->getUrl());\n        }\n\n        return redirect('/books');\n    }\n\n    /**\n     * Show the view to copy a book.\n     *\n     * @throws NotFoundException\n     */\n    public function showCopy(string $bookSlug)\n    {\n        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);\n        $this->checkOwnablePermission(Permission::BookView, $book);\n\n        session()->flashInput(['name' => $book->name]);\n\n        return view('books.copy', [\n            'book' => $book,\n        ]);\n    }\n\n    /**\n     * Create a copy of a book within the requested target destination.\n     *\n     * @throws NotFoundException\n     */\n    public function copy(Request $request, Cloner $cloner, string $bookSlug)\n    {\n        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);\n        $this->checkOwnablePermission(Permission::BookView, $book);\n        $this->checkPermission(Permission::BookCreateAll);\n\n        $newName = $request->get('name') ?: $book->name;\n        $bookCopy = $cloner->cloneBook($book, $newName);\n        $this->showSuccessNotification(trans('entities.books_copy_success'));\n\n        return redirect($bookCopy->getUrl());\n    }\n\n    /**\n     * Convert the chapter to a book.\n     */\n    public function convertToShelf(HierarchyTransformer $transformer, string $bookSlug)\n    {\n        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);\n        $this->checkOwnablePermission(Permission::BookUpdate, $book);\n        $this->checkOwnablePermission(Permission::BookDelete, $book);\n        $this->checkPermission(Permission::BookshelfCreateAll);\n        $this->checkPermission(Permission::BookCreateAll);\n\n        $shelf = (new DatabaseTransaction(function () use ($book, $transformer) {\n            return $transformer->transformBookToShelf($book);\n        }))->run();\n\n        return redirect($shelf->getUrl());\n    }\n}\n"
  },
  {
    "path": "app/Entities/Controllers/BookshelfApiController.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Controllers;\n\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Entities\\Queries\\BookshelfQueries;\nuse BookStack\\Entities\\Repos\\BookshelfRepo;\nuse BookStack\\Http\\ApiController;\nuse BookStack\\Permissions\\Permission;\nuse Exception;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Validation\\ValidationException;\n\nclass BookshelfApiController extends ApiController\n{\n    public function __construct(\n        protected BookshelfRepo $bookshelfRepo,\n        protected BookshelfQueries $queries,\n    ) {\n    }\n\n    /**\n     * Get a listing of shelves visible to the user.\n     */\n    public function list()\n    {\n        $shelves = $this->queries\n            ->visibleForList()\n            ->with(['cover:id,name,url'])\n            ->addSelect(['created_by', 'updated_by']);\n\n        return $this->apiListingResponse($shelves, [\n            'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by',\n        ]);\n    }\n\n    /**\n     * Create a new shelf in the system.\n     * An array of books IDs can be provided in the request. These\n     * will be added to the shelf in the same order as provided.\n     * The cover image of a shelf can be set by sending a file via an 'image' property within a 'multipart/form-data' request.\n     * If the 'image' property is null then the shelf cover image will be removed.\n     *\n     * @throws ValidationException\n     */\n    public function create(Request $request)\n    {\n        $this->checkPermission(Permission::BookshelfCreateAll);\n        $requestData = $this->validate($request, $this->rules()['create']);\n\n        $bookIds = $request->get('books', []);\n        $shelf = $this->bookshelfRepo->create($requestData, $bookIds);\n\n        return response()->json($this->forJsonDisplay($shelf));\n    }\n\n    /**\n     * View the details of a single shelf.\n     */\n    public function read(string $id)\n    {\n        $shelf = $this->queries->findVisibleByIdOrFail(intval($id));\n        $shelf = $this->forJsonDisplay($shelf);\n        $shelf->load([\n            'createdBy', 'updatedBy', 'ownedBy',\n            'books' => function (BelongsToMany $query) {\n                $query->scopes('visible')->get(['id', 'name', 'slug']);\n            },\n        ]);\n\n        return response()->json($shelf);\n    }\n\n    /**\n     * Update the details of a single shelf.\n     * An array of books IDs can be provided in the request. These\n     * will be added to the shelf in the same order as provided and overwrite\n     * any existing book assignments.\n     * The cover image of a shelf can be set by sending a file via an 'image' property within a 'multipart/form-data' request.\n     * If the 'image' property is null then the shelf cover image will be removed.\n     *\n     * @throws ValidationException\n     */\n    public function update(Request $request, string $id)\n    {\n        $shelf = $this->queries->findVisibleByIdOrFail(intval($id));\n        $this->checkOwnablePermission(Permission::BookshelfUpdate, $shelf);\n\n        $requestData = $this->validate($request, $this->rules()['update']);\n        $bookIds = $request->get('books', null);\n\n        $shelf = $this->bookshelfRepo->update($shelf, $requestData, $bookIds);\n\n        return response()->json($this->forJsonDisplay($shelf));\n    }\n\n    /**\n     * Delete a single shelf.\n     * This will typically send the shelf to the recycle bin.\n     *\n     * @throws Exception\n     */\n    public function delete(string $id)\n    {\n        $shelf = $this->queries->findVisibleByIdOrFail(intval($id));\n        $this->checkOwnablePermission(Permission::BookshelfDelete, $shelf);\n\n        $this->bookshelfRepo->destroy($shelf);\n\n        return response('', 204);\n    }\n\n    protected function forJsonDisplay(Bookshelf $shelf): Bookshelf\n    {\n        $shelf = clone $shelf;\n        $shelf->unsetRelations()->refresh();\n\n        $shelf->load(['tags']);\n        $shelf->makeVisible(['cover', 'description_html'])\n            ->setAttribute('description_html', $shelf->descriptionInfo()->getHtml())\n            ->setAttribute('cover', $shelf->coverInfo()->getImage());\n\n        return $shelf;\n    }\n\n    protected function rules(): array\n    {\n        return [\n            'create' => [\n                'name'             => ['required', 'string', 'max:255'],\n                'description'      => ['string', 'max:1900'],\n                'description_html' => ['string', 'max:2000'],\n                'books'            => ['array'],\n                'tags'             => ['array'],\n                'image'            => array_merge(['nullable'], $this->getImageValidationRules()),\n            ],\n            'update' => [\n                'name'             => ['string', 'min:1', 'max:255'],\n                'description'      => ['string', 'max:1900'],\n                'description_html' => ['string', 'max:2000'],\n                'books'            => ['array'],\n                'tags'             => ['array'],\n                'image'            => array_merge(['nullable'], $this->getImageValidationRules()),\n            ],\n        ];\n    }\n}\n"
  },
  {
    "path": "app/Entities/Controllers/BookshelfController.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Controllers;\n\nuse BookStack\\Activity\\ActivityQueries;\nuse BookStack\\Activity\\Models\\View;\nuse BookStack\\Entities\\Queries\\BookQueries;\nuse BookStack\\Entities\\Queries\\BookshelfQueries;\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse BookStack\\Entities\\Repos\\BookshelfRepo;\nuse BookStack\\Entities\\Tools\\ShelfContext;\nuse BookStack\\Exceptions\\ImageUploadException;\nuse BookStack\\Exceptions\\NotFoundException;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\References\\ReferenceFetcher;\nuse BookStack\\Util\\SimpleListOptions;\nuse Exception;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Validation\\ValidationException;\n\nclass BookshelfController extends Controller\n{\n    public function __construct(\n        protected BookshelfRepo $shelfRepo,\n        protected BookshelfQueries $queries,\n        protected EntityQueries $entityQueries,\n        protected BookQueries $bookQueries,\n        protected ShelfContext $shelfContext,\n        protected ReferenceFetcher $referenceFetcher,\n    ) {\n    }\n\n    /**\n     * Display a listing of bookshelves.\n     */\n    public function index(Request $request)\n    {\n        $view = setting()->getForCurrentUser('bookshelves_view_type');\n        $listOptions = SimpleListOptions::fromRequest($request, 'bookshelves')->withSortOptions([\n            'name'       => trans('common.sort_name'),\n            'created_at' => trans('common.sort_created_at'),\n            'updated_at' => trans('common.sort_updated_at'),\n        ]);\n\n        $shelves = $this->queries->visibleForListWithCover()\n            ->orderBy($listOptions->getSort(), $listOptions->getOrder())\n            ->paginate(setting()->getInteger('lists-page-count-shelves', 18, 1, 1000));\n        $recents = $this->isSignedIn() ? $this->queries->recentlyViewedForCurrentUser()->get() : false;\n        $popular = $this->queries->popularForList()->get();\n        $new = $this->queries->visibleForList()\n            ->orderBy('created_at', 'desc')\n            ->take(4)\n            ->get();\n\n        $this->shelfContext->clearShelfContext();\n        $this->setPageTitle(trans('entities.shelves'));\n\n        return view('shelves.index', [\n            'shelves'     => $shelves,\n            'recents'     => $recents,\n            'popular'     => $popular,\n            'new'         => $new,\n            'view'        => $view,\n            'listOptions' => $listOptions,\n        ]);\n    }\n\n    /**\n     * Show the form for creating a new bookshelf.\n     */\n    public function create()\n    {\n        $this->checkPermission(Permission::BookshelfCreateAll);\n        $books = $this->bookQueries->visibleForList()->orderBy('name')->get(['name', 'id', 'slug', 'created_at', 'updated_at']);\n        $this->setPageTitle(trans('entities.shelves_create'));\n\n        return view('shelves.create', ['books' => $books]);\n    }\n\n    /**\n     * Store a newly created bookshelf in storage.\n     *\n     * @throws ValidationException\n     * @throws ImageUploadException\n     */\n    public function store(Request $request)\n    {\n        $this->checkPermission(Permission::BookshelfCreateAll);\n        $validated = $this->validate($request, [\n            'name'             => ['required', 'string', 'max:255'],\n            'description_html' => ['string', 'max:2000'],\n            'image'            => array_merge(['nullable'], $this->getImageValidationRules()),\n            'tags'             => ['array'],\n        ]);\n\n        $bookIds = explode(',', $request->get('books', ''));\n        $shelf = $this->shelfRepo->create($validated, $bookIds);\n\n        return redirect($shelf->getUrl());\n    }\n\n    /**\n     * Display the bookshelf of the given slug.\n     *\n     * @throws NotFoundException\n     */\n    public function show(Request $request, ActivityQueries $activities, string $slug)\n    {\n        try {\n            $shelf = $this->queries->findVisibleBySlugOrFail($slug);\n        } catch (NotFoundException $exception) {\n            $shelf = $this->entityQueries->findVisibleByOldSlugs('bookshelf', $slug);\n            if (is_null($shelf)) {\n                throw $exception;\n            }\n            return redirect($shelf->getUrl());\n        }\n\n        $this->checkOwnablePermission(Permission::BookshelfView, $shelf);\n\n        $listOptions = SimpleListOptions::fromRequest($request, 'shelf_books')->withSortOptions([\n            'default' => trans('common.sort_default'),\n            'name' => trans('common.sort_name'),\n            'created_at' => trans('common.sort_created_at'),\n            'updated_at' => trans('common.sort_updated_at'),\n        ]);\n\n        $sort = $listOptions->getSort();\n\n        $sortedVisibleShelfBooks = $shelf->visibleBooks()\n            ->reorder($sort === 'default' ? 'order' : $sort, $listOptions->getOrder())\n            ->get()\n            ->values()\n            ->all();\n\n        View::incrementFor($shelf);\n        $this->shelfContext->setShelfContext($shelf->id);\n        $view = setting()->getForCurrentUser('bookshelf_view_type');\n\n        $this->setPageTitle($shelf->getShortName());\n\n        return view('shelves.show', [\n            'shelf'                   => $shelf,\n            'sortedVisibleShelfBooks' => $sortedVisibleShelfBooks,\n            'view'                    => $view,\n            'activity'                => $activities->entityActivity($shelf, 20, 1),\n            'listOptions'             => $listOptions,\n            'referenceCount'          => $this->referenceFetcher->getReferenceCountToEntity($shelf),\n        ]);\n    }\n\n    /**\n     * Show the form for editing the specified bookshelf.\n     */\n    public function edit(string $slug)\n    {\n        $shelf = $this->queries->findVisibleBySlugOrFail($slug);\n        $this->checkOwnablePermission(Permission::BookshelfUpdate, $shelf);\n\n        $shelfBookIds = $shelf->books()->get(['id'])->pluck('id');\n        $books = $this->bookQueries->visibleForList()\n            ->whereNotIn('id', $shelfBookIds)\n            ->orderBy('name')\n            ->get(['name', 'id', 'slug', 'created_at', 'updated_at']);\n\n        $this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $shelf->getShortName()]));\n\n        return view('shelves.edit', [\n            'shelf' => $shelf,\n            'books' => $books,\n        ]);\n    }\n\n    /**\n     * Update the specified bookshelf in storage.\n     *\n     * @throws ValidationException\n     * @throws ImageUploadException\n     * @throws NotFoundException\n     */\n    public function update(Request $request, string $slug)\n    {\n        $shelf = $this->queries->findVisibleBySlugOrFail($slug);\n        $this->checkOwnablePermission(Permission::BookshelfUpdate, $shelf);\n        $validated = $this->validate($request, [\n            'name'             => ['required', 'string', 'max:255'],\n            'description_html' => ['string', 'max:2000'],\n            'image'            => array_merge(['nullable'], $this->getImageValidationRules()),\n            'tags'             => ['array'],\n        ]);\n\n        if ($request->has('image_reset')) {\n            $validated['image'] = null;\n        } elseif (array_key_exists('image', $validated) && is_null($validated['image'])) {\n            unset($validated['image']);\n        }\n\n        $bookIds = explode(',', $request->get('books', ''));\n        $shelf = $this->shelfRepo->update($shelf, $validated, $bookIds);\n\n        return redirect($shelf->getUrl());\n    }\n\n    /**\n     * Shows the page to confirm deletion.\n     */\n    public function showDelete(string $slug)\n    {\n        $shelf = $this->queries->findVisibleBySlugOrFail($slug);\n        $this->checkOwnablePermission(Permission::BookshelfDelete, $shelf);\n\n        $this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()]));\n\n        return view('shelves.delete', ['shelf' => $shelf]);\n    }\n\n    /**\n     * Remove the specified bookshelf from storage.\n     *\n     * @throws Exception\n     */\n    public function destroy(string $slug)\n    {\n        $shelf = $this->queries->findVisibleBySlugOrFail($slug);\n        $this->checkOwnablePermission(Permission::BookshelfDelete, $shelf);\n\n        $this->shelfRepo->destroy($shelf);\n\n        return redirect('/shelves');\n    }\n}\n"
  },
  {
    "path": "app/Entities/Controllers/ChapterApiController.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Controllers;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Queries\\ChapterQueries;\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse BookStack\\Entities\\Repos\\ChapterRepo;\nuse BookStack\\Exceptions\\PermissionsException;\nuse BookStack\\Http\\ApiController;\nuse BookStack\\Permissions\\Permission;\nuse Exception;\nuse Illuminate\\Http\\Request;\n\nclass ChapterApiController extends ApiController\n{\n    protected array $rules = [\n        'create' => [\n            'book_id'             => ['required', 'integer'],\n            'name'                => ['required', 'string', 'max:255'],\n            'description'         => ['string', 'max:1900'],\n            'description_html'    => ['string', 'max:2000'],\n            'tags'                => ['array'],\n            'priority'            => ['integer'],\n            'default_template_id' => ['nullable', 'integer'],\n        ],\n        'update' => [\n            'book_id'             => ['integer'],\n            'name'                => ['string', 'min:1', 'max:255'],\n            'description'         => ['string', 'max:1900'],\n            'description_html'    => ['string', 'max:2000'],\n            'tags'                => ['array'],\n            'priority'            => ['integer'],\n            'default_template_id' => ['nullable', 'integer'],\n        ],\n    ];\n\n    public function __construct(\n        protected ChapterRepo $chapterRepo,\n        protected ChapterQueries $queries,\n        protected EntityQueries $entityQueries,\n    ) {\n    }\n\n    /**\n     * Get a listing of chapters visible to the user.\n     */\n    public function list()\n    {\n        $chapters = $this->queries->visibleForList()\n            ->addSelect(['created_by', 'updated_by']);\n\n        return $this->apiListingResponse($chapters, [\n            'id', 'book_id', 'name', 'slug', 'description', 'priority',\n            'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by',\n        ]);\n    }\n\n    /**\n     * Create a new chapter in the system.\n     */\n    public function create(Request $request)\n    {\n        $requestData = $this->validate($request, $this->rules['create']);\n\n        $bookId = $request->get('book_id');\n        $book = $this->entityQueries->books->findVisibleByIdOrFail(intval($bookId));\n        $this->checkOwnablePermission(Permission::ChapterCreate, $book);\n\n        $chapter = $this->chapterRepo->create($requestData, $book);\n\n        return response()->json($this->forJsonDisplay($chapter));\n    }\n\n    /**\n     * View the details of a single chapter.\n     */\n    public function read(string $id)\n    {\n        $chapter = $this->queries->findVisibleByIdOrFail(intval($id));\n        $chapter = $this->forJsonDisplay($chapter);\n\n        $chapter->load(['createdBy', 'updatedBy', 'ownedBy']);\n\n        // Note: More fields than usual here, for backwards compatibility,\n        // due to previously accidentally including more fields that desired.\n        $pages = $this->entityQueries->pages->visibleForChapterList($chapter->id)\n            ->addSelect(['created_by', 'updated_by', 'revision_count', 'editor'])\n            ->get();\n        $chapter->setRelation('pages', $pages);\n\n        return response()->json($chapter);\n    }\n\n    /**\n     * Update the details of a single chapter.\n     * Providing a 'book_id' property will essentially move the chapter\n     * into that parent element if you have permissions to do so.\n     */\n    public function update(Request $request, string $id)\n    {\n        $requestData = $this->validate($request, $this->rules()['update']);\n        $chapter = $this->queries->findVisibleByIdOrFail(intval($id));\n        $this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);\n\n        if ($request->has('book_id') && $chapter->book_id !== (intval($requestData['book_id']) ?: null)) {\n            $this->checkOwnablePermission(Permission::ChapterDelete, $chapter);\n\n            try {\n                $this->chapterRepo->move($chapter, \"book:{$requestData['book_id']}\");\n            } catch (Exception $exception) {\n                if ($exception instanceof PermissionsException) {\n                    $this->showPermissionError();\n                }\n\n                return $this->jsonError(trans('errors.selected_book_not_found'));\n            }\n        }\n\n        $updatedChapter = $this->chapterRepo->update($chapter, $requestData);\n\n        return response()->json($this->forJsonDisplay($updatedChapter));\n    }\n\n    /**\n     * Delete a chapter.\n     * This will typically send the chapter to the recycle bin.\n     */\n    public function delete(string $id)\n    {\n        $chapter = $this->queries->findVisibleByIdOrFail(intval($id));\n        $this->checkOwnablePermission(Permission::ChapterDelete, $chapter);\n\n        $this->chapterRepo->destroy($chapter);\n\n        return response('', 204);\n    }\n\n    protected function forJsonDisplay(Chapter $chapter): Chapter\n    {\n        $chapter = clone $chapter;\n        $chapter->unsetRelations()->refresh();\n\n        $chapter->load(['tags']);\n        $chapter->makeVisible('description_html');\n        $chapter->setAttribute('description_html', $chapter->descriptionInfo()->getHtml());\n\n        /** @var Book $book */\n        $book = $chapter->book()->first();\n        $chapter->setAttribute('book_slug', $book->slug);\n\n        return $chapter;\n    }\n}\n"
  },
  {
    "path": "app/Entities/Controllers/ChapterController.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Controllers;\n\nuse BookStack\\Activity\\Models\\View;\nuse BookStack\\Activity\\Tools\\UserEntityWatchOptions;\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Queries\\ChapterQueries;\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse BookStack\\Entities\\Repos\\ChapterRepo;\nuse BookStack\\Entities\\Tools\\BookContents;\nuse BookStack\\Entities\\Tools\\Cloner;\nuse BookStack\\Entities\\Tools\\HierarchyTransformer;\nuse BookStack\\Entities\\Tools\\NextPreviousContentLocator;\nuse BookStack\\Exceptions\\MoveOperationException;\nuse BookStack\\Exceptions\\NotFoundException;\nuse BookStack\\Exceptions\\NotifyException;\nuse BookStack\\Exceptions\\PermissionsException;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\References\\ReferenceFetcher;\nuse BookStack\\Util\\DatabaseTransaction;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Validation\\ValidationException;\nuse Throwable;\n\nclass ChapterController extends Controller\n{\n    public function __construct(\n        protected ChapterRepo $chapterRepo,\n        protected ChapterQueries $queries,\n        protected EntityQueries $entityQueries,\n        protected ReferenceFetcher $referenceFetcher,\n    ) {\n    }\n\n    /**\n     * Show the form for creating a new chapter.\n     */\n    public function create(string $bookSlug)\n    {\n        $book = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);\n        $this->checkOwnablePermission(Permission::ChapterCreate, $book);\n\n        $this->setPageTitle(trans('entities.chapters_create'));\n\n        return view('chapters.create', [\n            'book' => $book,\n            'current' => $book,\n        ]);\n    }\n\n    /**\n     * Store a newly created chapter in storage.\n     *\n     * @throws ValidationException\n     */\n    public function store(Request $request, string $bookSlug)\n    {\n        $validated = $this->validate($request, [\n            'name'                => ['required', 'string', 'max:255'],\n            'description_html'    => ['string', 'max:2000'],\n            'tags'                => ['array'],\n            'default_template_id' => ['nullable', 'integer'],\n        ]);\n\n        $book = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);\n        $this->checkOwnablePermission(Permission::ChapterCreate, $book);\n\n        $chapter = $this->chapterRepo->create($validated, $book);\n\n        return redirect($chapter->getUrl());\n    }\n\n    /**\n     * Display the specified chapter.\n     */\n    public function show(string $bookSlug, string $chapterSlug)\n    {\n        try {\n            $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);\n        } catch (NotFoundException $exception) {\n            $chapter = $this->entityQueries->findVisibleByOldSlugs('chapter', $chapterSlug, $bookSlug);\n            if (is_null($chapter)) {\n                throw $exception;\n            }\n            return redirect($chapter->getUrl());\n        }\n\n        $sidebarTree = (new BookContents($chapter->book))->getTree();\n        $pages = $this->entityQueries->pages->visibleForChapterList($chapter->id)->get();\n\n        $nextPreviousLocator = new NextPreviousContentLocator($chapter, $sidebarTree);\n        View::incrementFor($chapter);\n\n        $this->setPageTitle($chapter->getShortName());\n\n        return view('chapters.show', [\n            'book'           => $chapter->book,\n            'chapter'        => $chapter,\n            'current'        => $chapter,\n            'sidebarTree'    => $sidebarTree,\n            'watchOptions'   => new UserEntityWatchOptions(user(), $chapter),\n            'pages'          => $pages,\n            'next'           => $nextPreviousLocator->getNext(),\n            'previous'       => $nextPreviousLocator->getPrevious(),\n            'referenceCount' => $this->referenceFetcher->getReferenceCountToEntity($chapter),\n        ]);\n    }\n\n    /**\n     * Show the form for editing the specified chapter.\n     */\n    public function edit(string $bookSlug, string $chapterSlug)\n    {\n        $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);\n        $this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);\n\n        $this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));\n\n        return view('chapters.edit', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);\n    }\n\n    /**\n     * Update the specified chapter in storage.\n     *\n     * @throws NotFoundException\n     */\n    public function update(Request $request, string $bookSlug, string $chapterSlug)\n    {\n        $validated = $this->validate($request, [\n            'name'                => ['required', 'string', 'max:255'],\n            'description_html'    => ['string', 'max:2000'],\n            'tags'                => ['array'],\n            'default_template_id' => ['nullable', 'integer'],\n        ]);\n\n        $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);\n        $this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);\n\n        $chapter = $this->chapterRepo->update($chapter, $validated);\n\n        return redirect($chapter->getUrl());\n    }\n\n    /**\n     * Shows the page to confirm deletion of this chapter.\n     *\n     * @throws NotFoundException\n     */\n    public function showDelete(string $bookSlug, string $chapterSlug)\n    {\n        $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);\n        $this->checkOwnablePermission(Permission::ChapterDelete, $chapter);\n\n        $this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));\n\n        return view('chapters.delete', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);\n    }\n\n    /**\n     * Remove the specified chapter from storage.\n     *\n     * @throws NotFoundException\n     * @throws Throwable\n     */\n    public function destroy(string $bookSlug, string $chapterSlug)\n    {\n        $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);\n        $this->checkOwnablePermission(Permission::ChapterDelete, $chapter);\n\n        $this->chapterRepo->destroy($chapter);\n\n        return redirect($chapter->book->getUrl());\n    }\n\n    /**\n     * Show the page for moving a chapter.\n     *\n     * @throws NotFoundException\n     */\n    public function showMove(string $bookSlug, string $chapterSlug)\n    {\n        $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);\n        $this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));\n        $this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);\n        $this->checkOwnablePermission(Permission::ChapterDelete, $chapter);\n\n        return view('chapters.move', [\n            'chapter' => $chapter,\n            'book'    => $chapter->book,\n        ]);\n    }\n\n    /**\n     * Perform the move action for a chapter.\n     *\n     * @throws NotFoundException|NotifyException\n     */\n    public function move(Request $request, string $bookSlug, string $chapterSlug)\n    {\n        $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);\n        $this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);\n        $this->checkOwnablePermission(Permission::ChapterDelete, $chapter);\n\n        $entitySelection = $request->get('entity_selection', null);\n        if ($entitySelection === null || $entitySelection === '') {\n            return redirect($chapter->getUrl());\n        }\n\n        try {\n            $this->chapterRepo->move($chapter, $entitySelection);\n        } catch (PermissionsException $exception) {\n            $this->showPermissionError();\n        } catch (MoveOperationException $exception) {\n            $this->showErrorNotification(trans('errors.selected_book_not_found'));\n\n            return redirect($chapter->getUrl('/move'));\n        }\n\n        return redirect($chapter->getUrl());\n    }\n\n    /**\n     * Show the view to copy a chapter.\n     *\n     * @throws NotFoundException\n     */\n    public function showCopy(string $bookSlug, string $chapterSlug)\n    {\n        $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);\n\n        session()->flashInput(['name' => $chapter->name]);\n\n        return view('chapters.copy', [\n            'book'    => $chapter->book,\n            'chapter' => $chapter,\n        ]);\n    }\n\n    /**\n     * Create a copy of a chapter within the requested target destination.\n     *\n     * @throws NotFoundException\n     * @throws Throwable\n     */\n    public function copy(Request $request, Cloner $cloner, string $bookSlug, string $chapterSlug)\n    {\n        $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);\n\n        $entitySelection = $request->get('entity_selection') ?: null;\n        $newParentBook = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $chapter->getParent();\n\n        if (!$newParentBook instanceof Book) {\n            $this->showErrorNotification(trans('errors.selected_book_not_found'));\n\n            return redirect($chapter->getUrl('/copy'));\n        }\n\n        $this->checkOwnablePermission(Permission::ChapterCreate, $newParentBook);\n\n        $newName = $request->get('name') ?: $chapter->name;\n        $chapterCopy = $cloner->cloneChapter($chapter, $newParentBook, $newName);\n        $this->showSuccessNotification(trans('entities.chapters_copy_success'));\n\n        return redirect($chapterCopy->getUrl());\n    }\n\n    /**\n     * Convert the chapter to a book.\n     */\n    public function convertToBook(HierarchyTransformer $transformer, string $bookSlug, string $chapterSlug)\n    {\n        $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);\n        $this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);\n        $this->checkOwnablePermission(Permission::ChapterDelete, $chapter);\n        $this->checkPermission(Permission::BookCreateAll);\n\n        $book = (new DatabaseTransaction(function () use ($chapter, $transformer) {\n            return $transformer->transformChapterToBook($chapter);\n        }))->run();\n\n        return redirect($book->getUrl());\n    }\n}\n"
  },
  {
    "path": "app/Entities/Controllers/PageApiController.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Controllers;\n\nuse BookStack\\Activity\\Tools\\CommentTree;\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse BookStack\\Entities\\Queries\\PageQueries;\nuse BookStack\\Entities\\Repos\\PageRepo;\nuse BookStack\\Exceptions\\PermissionsException;\nuse BookStack\\Http\\ApiController;\nuse BookStack\\Permissions\\Permission;\nuse Exception;\nuse Illuminate\\Http\\Request;\n\nclass PageApiController extends ApiController\n{\n    protected array $rules = [\n        'create' => [\n            'book_id'    => ['required_without:chapter_id', 'integer'],\n            'chapter_id' => ['required_without:book_id', 'integer'],\n            'name'       => ['required', 'string', 'max:255'],\n            'html'       => ['required_without:markdown', 'string'],\n            'markdown'   => ['required_without:html', 'string'],\n            'tags'       => ['array'],\n            'priority'   => ['integer'],\n        ],\n        'update' => [\n            'book_id'    => ['integer'],\n            'chapter_id' => ['integer'],\n            'name'       => ['string', 'min:1', 'max:255'],\n            'html'       => ['string'],\n            'markdown'   => ['string'],\n            'tags'       => ['array'],\n            'priority'   => ['integer'],\n        ],\n    ];\n\n    public function __construct(\n        protected PageRepo $pageRepo,\n        protected PageQueries $queries,\n        protected EntityQueries $entityQueries,\n    ) {\n    }\n\n    /**\n     * Get a listing of pages visible to the user.\n     */\n    public function list()\n    {\n        $pages = $this->queries->visibleForList()\n            ->addSelect(['created_by', 'updated_by', 'revision_count', 'editor']);\n\n        return $this->apiListingResponse($pages, [\n            'id', 'book_id', 'chapter_id', 'name', 'slug', 'priority',\n            'draft', 'template',\n            'created_at', 'updated_at',\n            'created_by', 'updated_by', 'owned_by',\n        ]);\n    }\n\n    /**\n     * Create a new page in the system.\n     *\n     * The ID of a parent book or chapter is required to indicate\n     * where this page should be located.\n     *\n     * Any HTML content provided should be kept to a single-block depth of plain HTML\n     * elements to remain compatible with the BookStack front-end and editors.\n     * Any images included via base64 data URIs will be extracted and saved as gallery\n     * images against the page during upload.\n     */\n    public function create(Request $request)\n    {\n        $this->validate($request, $this->rules['create']);\n\n        if ($request->has('chapter_id')) {\n            $parent = $this->entityQueries->chapters->findVisibleByIdOrFail(intval($request->get('chapter_id')));\n        } else {\n            $parent = $this->entityQueries->books->findVisibleByIdOrFail(intval($request->get('book_id')));\n        }\n        $this->checkOwnablePermission(Permission::PageCreate, $parent);\n\n        $draft = $this->pageRepo->getNewDraftPage($parent);\n        $this->pageRepo->publishDraft($draft, $request->only(array_keys($this->rules['create'])));\n\n        return response()->json($draft->forJsonDisplay());\n    }\n\n    /**\n     * View the details of a single page.\n     * Pages will always have HTML content. They may have markdown content\n     * if the Markdown editor was used to last update the page.\n     *\n     * The 'html' property is the fully rendered and escaped HTML content that BookStack\n     * would show on page view, with page includes handled.\n     * The 'raw_html' property is the direct database stored HTML content, which would be\n     * what BookStack shows on page edit.\n     *\n     * See the \"Content Security\" section of these docs for security considerations when using\n     * the page content returned from this endpoint.\n     *\n     * Comments for the page are provided in a tree-structure representing the hierarchy of top-level\n     * comments and replies, for both archived and active comments.\n     */\n    public function read(string $id)\n    {\n        $page = $this->queries->findVisibleByIdOrFail($id);\n\n        $page = $page->forJsonDisplay();\n        $commentTree = (new CommentTree($page));\n        $commentTree->loadVisibleHtml();\n        $page->setAttribute('comments', [\n            'active' => $commentTree->getActive(),\n            'archived' => $commentTree->getArchived(),\n        ]);\n\n        return response()->json($page);\n    }\n\n    /**\n     * Update the details of a single page.\n     *\n     * See the 'create' action for details on the provided HTML/Markdown.\n     * Providing a 'book_id' or 'chapter_id' property will essentially move\n     * the page into that parent element if you have permissions to do so.\n     */\n    public function update(Request $request, string $id)\n    {\n        $requestData = $this->validate($request, $this->rules['update']);\n\n        $page = $this->queries->findVisibleByIdOrFail($id);\n        $this->checkOwnablePermission(Permission::PageUpdate, $page);\n\n        $parent = null;\n        if ($request->has('chapter_id')) {\n            $parent = $this->entityQueries->chapters->findVisibleByIdOrFail(intval($request->get('chapter_id')));\n        } elseif ($request->has('book_id')) {\n            $parent = $this->entityQueries->books->findVisibleByIdOrFail(intval($request->get('book_id')));\n        }\n\n        if ($parent && !$parent->matches($page->getParent())) {\n            $this->checkOwnablePermission(Permission::PageDelete, $page);\n\n            try {\n                $this->pageRepo->move($page, $parent->getType() . ':' . $parent->id);\n            } catch (Exception $exception) {\n                if ($exception instanceof  PermissionsException) {\n                    $this->showPermissionError();\n                }\n\n                return $this->jsonError(trans('errors.selected_book_chapter_not_found'));\n            }\n        }\n\n        $updatedPage = $this->pageRepo->update($page, $requestData);\n\n        return response()->json($updatedPage->forJsonDisplay());\n    }\n\n    /**\n     * Delete a page.\n     * This will typically send the page to the recycle bin.\n     */\n    public function delete(string $id)\n    {\n        $page = $this->queries->findVisibleByIdOrFail($id);\n        $this->checkOwnablePermission(Permission::PageDelete, $page);\n\n        $this->pageRepo->destroy($page);\n\n        return response('', 204);\n    }\n}\n"
  },
  {
    "path": "app/Entities/Controllers/PageController.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Controllers;\n\nuse BookStack\\Activity\\Models\\View;\nuse BookStack\\Activity\\Tools\\CommentTree;\nuse BookStack\\Activity\\Tools\\UserEntityWatchOptions;\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse BookStack\\Entities\\Queries\\PageQueries;\nuse BookStack\\Entities\\Repos\\PageRepo;\nuse BookStack\\Entities\\Tools\\BookContents;\nuse BookStack\\Entities\\Tools\\Cloner;\nuse BookStack\\Entities\\Tools\\NextPreviousContentLocator;\nuse BookStack\\Entities\\Tools\\PageContent;\nuse BookStack\\Entities\\Tools\\PageEditActivity;\nuse BookStack\\Entities\\Tools\\PageEditorData;\nuse BookStack\\Exceptions\\NotFoundException;\nuse BookStack\\Exceptions\\PermissionsException;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\References\\ReferenceFetcher;\nuse BookStack\\Util\\HtmlContentFilter;\nuse BookStack\\Util\\HtmlContentFilterConfig;\nuse Exception;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Validation\\ValidationException;\nuse Throwable;\n\nclass PageController extends Controller\n{\n    public function __construct(\n        protected PageRepo $pageRepo,\n        protected PageQueries $queries,\n        protected EntityQueries $entityQueries,\n        protected ReferenceFetcher $referenceFetcher\n    ) {\n    }\n\n    /**\n     * Show the form for creating a new page.\n     *\n     * @throws Throwable\n     */\n    public function create(string $bookSlug, ?string $chapterSlug = null)\n    {\n        if ($chapterSlug) {\n            $parent = $this->entityQueries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);\n        } else {\n            $parent = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);\n        }\n\n        $this->checkOwnablePermission(Permission::PageCreate, $parent);\n\n        // Redirect to draft edit screen if signed in\n        if ($this->isSignedIn()) {\n            $draft = $this->pageRepo->getNewDraftPage($parent);\n\n            return redirect($draft->getUrl());\n        }\n\n        // Otherwise show the edit view if they're a guest\n        $this->setPageTitle(trans('entities.pages_new'));\n\n        return view('pages.guest-create', ['parent' => $parent]);\n    }\n\n    /**\n     * Create a new page as a guest user.\n     *\n     * @throws ValidationException\n     */\n    public function createAsGuest(Request $request, string $bookSlug, ?string $chapterSlug = null)\n    {\n        $this->validate($request, [\n            'name' => ['required', 'string', 'max:255'],\n        ]);\n\n        if ($chapterSlug) {\n            $parent = $this->entityQueries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);\n        } else {\n            $parent = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);\n        }\n\n        $this->checkOwnablePermission(Permission::PageCreate, $parent);\n\n        $page = $this->pageRepo->getNewDraftPage($parent);\n        $this->pageRepo->publishDraft($page, [\n            'name' => $request->get('name'),\n        ]);\n\n        return redirect($page->getUrl('/edit'));\n    }\n\n    /**\n     * Show form to continue editing a draft page.\n     *\n     * @throws NotFoundException\n     */\n    public function editDraft(Request $request, string $bookSlug, int $pageId)\n    {\n        $draft = $this->queries->findVisibleByIdOrFail($pageId);\n        $this->checkOwnablePermission(Permission::PageCreate, $draft->getParent());\n\n        $editorData = new PageEditorData($draft, $this->entityQueries, $request->query('editor', ''));\n        $this->setPageTitle(trans('entities.pages_edit_draft'));\n\n        return view('pages.edit', $editorData->getViewData());\n    }\n\n    /**\n     * Store a new page by changing a draft into a page.\n     *\n     * @throws NotFoundException\n     * @throws ValidationException\n     */\n    public function store(Request $request, string $bookSlug, int $pageId)\n    {\n        $this->validate($request, [\n            'name' => ['required', 'string', 'max:255'],\n        ]);\n\n        $draftPage = $this->queries->findVisibleByIdOrFail($pageId);\n        $this->checkOwnablePermission(Permission::PageCreate, $draftPage->getParent());\n\n        $page = $this->pageRepo->publishDraft($draftPage, $request->all());\n\n        return redirect($page->getUrl());\n    }\n\n    /**\n     * Display the specified page.\n     * If the page is not found via the slug the revisions are searched for a match.\n     *\n     * @throws NotFoundException\n     */\n    public function show(string $bookSlug, string $pageSlug)\n    {\n        try {\n            $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);\n        } catch (NotFoundException $e) {\n            $page = $this->entityQueries->findVisibleByOldSlugs('page', $pageSlug, $bookSlug);\n            if (is_null($page)) {\n                throw $e;\n            }\n\n            return redirect($page->getUrl());\n        }\n\n        $pageContent = (new PageContent($page));\n        $page->html = $pageContent->render();\n        $pageNav = $pageContent->getNavigation($page->html);\n\n        $sidebarTree = (new BookContents($page->book))->getTree();\n        $commentTree = (new CommentTree($page));\n        $nextPreviousLocator = new NextPreviousContentLocator($page, $sidebarTree);\n\n        View::incrementFor($page);\n        $this->setPageTitle($page->getShortName());\n\n        return view('pages.show', [\n            'page'            => $page,\n            'book'            => $page->book,\n            'current'         => $page,\n            'sidebarTree'     => $sidebarTree,\n            'commentTree'     => $commentTree,\n            'pageNav'         => $pageNav,\n            'watchOptions'    => new UserEntityWatchOptions(user(), $page),\n            'next'            => $nextPreviousLocator->getNext(),\n            'previous'        => $nextPreviousLocator->getPrevious(),\n            'referenceCount'  => $this->referenceFetcher->getReferenceCountToEntity($page),\n        ]);\n    }\n\n    /**\n     * Get a page from an ajax request.\n     *\n     * @throws NotFoundException\n     */\n    public function getPageAjax(int $pageId)\n    {\n        $page = $this->queries->findVisibleByIdOrFail($pageId);\n        $page->setHidden(array_diff($page->getHidden(), ['html', 'markdown']));\n        $page->makeHidden(['book']);\n\n        $filterConfig = HtmlContentFilterConfig::fromConfigString(config('app.content_filtering'));\n        $filter = new HtmlContentFilter($filterConfig);\n        $page->html = $filter->filterString($page->html);\n\n        return response()->json($page);\n    }\n\n    /**\n     * Show the form for editing the specified page.\n     *\n     * @throws NotFoundException\n     */\n    public function edit(Request $request, string $bookSlug, string $pageSlug)\n    {\n        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);\n        $this->checkOwnablePermission(Permission::PageUpdate, $page, $page->getUrl());\n\n        $editorData = new PageEditorData($page, $this->entityQueries, $request->query('editor', ''));\n        if ($editorData->getWarnings()) {\n            $this->showWarningNotification(implode(\"\\n\", $editorData->getWarnings()));\n        }\n\n        $this->setPageTitle(trans('entities.pages_editing_named', ['pageName' => $page->getShortName()]));\n\n        return view('pages.edit', $editorData->getViewData());\n    }\n\n    /**\n     * Update the specified page in storage.\n     *\n     * @throws ValidationException\n     * @throws NotFoundException\n     */\n    public function update(Request $request, string $bookSlug, string $pageSlug)\n    {\n        $this->validate($request, [\n            'name' => ['required', 'string', 'max:255'],\n        ]);\n        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);\n        $this->checkOwnablePermission(Permission::PageUpdate, $page);\n\n        $this->pageRepo->update($page, $request->all());\n\n        return redirect($page->getUrl());\n    }\n\n    /**\n     * Save a draft update as a revision.\n     *\n     * @throws NotFoundException\n     */\n    public function saveDraft(Request $request, int $pageId)\n    {\n        $page = $this->queries->findVisibleByIdOrFail($pageId);\n        $this->checkOwnablePermission(Permission::PageUpdate, $page);\n\n        if (!$this->isSignedIn()) {\n            return $this->jsonError(trans('errors.guests_cannot_save_drafts'), 500);\n        }\n\n        $draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));\n        $warnings = (new PageEditActivity($page))->getWarningMessagesForDraft($draft);\n\n        return response()->json([\n            'status'    => 'success',\n            'message'   => trans('entities.pages_edit_draft_save_at'),\n            'warning'   => implode(\"\\n\", $warnings),\n            'timestamp' => $draft->updated_at->timestamp,\n        ]);\n    }\n\n    /**\n     * Redirect from a special link url which uses the page id rather than the name.\n     *\n     * @throws NotFoundException\n     */\n    public function redirectFromLink(int $pageId)\n    {\n        $page = $this->queries->findVisibleByIdOrFail($pageId);\n\n        return redirect($page->getUrl());\n    }\n\n    /**\n     * Show the deletion page for the specified page.\n     *\n     * @throws NotFoundException\n     */\n    public function showDelete(string $bookSlug, string $pageSlug)\n    {\n        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);\n        $this->checkOwnablePermission(Permission::PageDelete, $page);\n        $this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()]));\n        $usedAsTemplate =\n            $this->entityQueries->books->start()->where('default_template_id', '=', $page->id)->count() > 0 ||\n            $this->entityQueries->chapters->start()->where('default_template_id', '=', $page->id)->count() > 0;\n\n        return view('pages.delete', [\n            'book'    => $page->book,\n            'page'    => $page,\n            'current' => $page,\n            'usedAsTemplate' => $usedAsTemplate,\n        ]);\n    }\n\n    /**\n     * Show the deletion page for the specified page.\n     *\n     * @throws NotFoundException\n     */\n    public function showDeleteDraft(string $bookSlug, int $pageId)\n    {\n        $page = $this->queries->findVisibleByIdOrFail($pageId);\n        $this->checkOwnablePermission(Permission::PageUpdate, $page);\n        $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()]));\n        $usedAsTemplate =\n            $this->entityQueries->books->start()->where('default_template_id', '=', $page->id)->count() > 0 ||\n            $this->entityQueries->chapters->start()->where('default_template_id', '=', $page->id)->count() > 0;\n\n        return view('pages.delete', [\n            'book'    => $page->book,\n            'page'    => $page,\n            'current' => $page,\n            'usedAsTemplate' => $usedAsTemplate,\n        ]);\n    }\n\n    /**\n     * Remove the specified page from storage.\n     *\n     * @throws NotFoundException\n     * @throws Throwable\n     */\n    public function destroy(string $bookSlug, string $pageSlug)\n    {\n        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);\n        $this->checkOwnablePermission(Permission::PageDelete, $page);\n        $parent = $page->getParent();\n\n        $this->pageRepo->destroy($page);\n\n        return redirect($parent->getUrl());\n    }\n\n    /**\n     * Remove the specified draft page from storage.\n     *\n     * @throws NotFoundException\n     * @throws Throwable\n     */\n    public function destroyDraft(string $bookSlug, int $pageId)\n    {\n        $page = $this->queries->findVisibleByIdOrFail($pageId);\n        $book = $page->book;\n        $chapter = $page->chapter;\n        $this->checkOwnablePermission(Permission::PageUpdate, $page);\n\n        $this->pageRepo->destroy($page);\n\n        $this->showSuccessNotification(trans('entities.pages_delete_draft_success'));\n\n        if ($chapter && userCan(Permission::ChapterView, $chapter)) {\n            return redirect($chapter->getUrl());\n        }\n\n        return redirect($book->getUrl());\n    }\n\n    /**\n     * Show a listing of recently created pages.\n     */\n    public function showRecentlyUpdated()\n    {\n        $visibleBelongsScope = function (BelongsTo $query) {\n            $query->scopes('visible');\n        };\n\n        $pages = $this->queries->visibleForList()\n            ->addSelect('updated_by')\n            ->with(['updatedBy', 'book' => $visibleBelongsScope, 'chapter' => $visibleBelongsScope])\n            ->orderBy('updated_at', 'desc')\n            ->paginate(20)\n            ->setPath(url('/pages/recently-updated'));\n\n        $this->setPageTitle(trans('entities.recently_updated_pages'));\n\n        return view('common.detailed-listing-paginated', [\n            'title'         => trans('entities.recently_updated_pages'),\n            'entities'      => $pages,\n            'showUpdatedBy' => true,\n            'showPath'      => true,\n        ]);\n    }\n\n    /**\n     * Show the view to choose a new parent to move a page into.\n     *\n     * @throws NotFoundException\n     */\n    public function showMove(string $bookSlug, string $pageSlug)\n    {\n        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);\n        $this->checkOwnablePermission(Permission::PageUpdate, $page);\n        $this->checkOwnablePermission(Permission::PageDelete, $page);\n\n        return view('pages.move', [\n            'book' => $page->book,\n            'page' => $page,\n        ]);\n    }\n\n    /**\n     * Does the action of moving the location of a page.\n     *\n     * @throws NotFoundException\n     * @throws Throwable\n     */\n    public function move(Request $request, string $bookSlug, string $pageSlug)\n    {\n        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);\n        $this->checkOwnablePermission(Permission::PageUpdate, $page);\n        $this->checkOwnablePermission(Permission::PageDelete, $page);\n\n        $entitySelection = $request->get('entity_selection', null);\n        if ($entitySelection === null || $entitySelection === '') {\n            return redirect($page->getUrl());\n        }\n\n        try {\n            $this->pageRepo->move($page, $entitySelection);\n        } catch (PermissionsException $exception) {\n            $this->showPermissionError();\n        } catch (Exception $exception) {\n            $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));\n\n            return redirect($page->getUrl('/move'));\n        }\n\n        return redirect($page->getUrl());\n    }\n\n    /**\n     * Show the view to copy a page.\n     *\n     * @throws NotFoundException\n     */\n    public function showCopy(string $bookSlug, string $pageSlug)\n    {\n        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);\n        session()->flashInput(['name' => $page->name]);\n\n        return view('pages.copy', [\n            'book' => $page->book,\n            'page' => $page,\n        ]);\n    }\n\n    /**\n     * Create a copy of a page within the requested target destination.\n     *\n     * @throws NotFoundException\n     * @throws Throwable\n     */\n    public function copy(Request $request, Cloner $cloner, string $bookSlug, string $pageSlug)\n    {\n        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);\n        $this->checkOwnablePermission(Permission::PageView, $page);\n\n        $entitySelection = $request->get('entity_selection') ?: null;\n        $newParent = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $page->getParent();\n\n        if (!$newParent instanceof Book && !$newParent instanceof Chapter) {\n            $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));\n\n            return redirect($page->getUrl('/copy'));\n        }\n\n        $this->checkOwnablePermission(Permission::PageCreate, $newParent);\n\n        $newName = $request->get('name') ?: $page->name;\n        $pageCopy = $cloner->clonePage($page, $newParent, $newName);\n        $this->showSuccessNotification(trans('entities.pages_copy_success'));\n\n        return redirect($pageCopy->getUrl());\n    }\n}\n"
  },
  {
    "path": "app/Entities/Controllers/PageRevisionController.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Controllers;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Entities\\Models\\PageRevision;\nuse BookStack\\Entities\\Queries\\PageQueries;\nuse BookStack\\Entities\\Repos\\PageRepo;\nuse BookStack\\Entities\\Repos\\RevisionRepo;\nuse BookStack\\Entities\\Tools\\PageContent;\nuse BookStack\\Exceptions\\NotFoundException;\nuse BookStack\\Facades\\Activity;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Util\\HtmlContentFilter;\nuse BookStack\\Util\\HtmlContentFilterConfig;\nuse BookStack\\Util\\SimpleListOptions;\nuse Illuminate\\Http\\Request;\nuse Ssddanbrown\\HtmlDiff\\Diff;\n\nclass PageRevisionController extends Controller\n{\n    public function __construct(\n        protected PageRepo $pageRepo,\n        protected PageQueries $pageQueries,\n        protected RevisionRepo $revisionRepo,\n    ) {\n    }\n\n    /**\n     * Shows the last revisions for this page.\n     *\n     * @throws NotFoundException\n     */\n    public function index(Request $request, string $bookSlug, string $pageSlug)\n    {\n        $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);\n        $listOptions = SimpleListOptions::fromRequest($request, 'page_revisions', true)->withSortOptions([\n            'id' => trans('entities.pages_revisions_sort_number')\n        ]);\n\n        $revisions = $page->revisions()->select([\n                'id', 'page_id', 'name', 'created_at', 'created_by', 'updated_at',\n                'type', 'revision_number', 'summary',\n            ])\n            ->selectRaw(\"IF(markdown = '', false, true) as is_markdown\")\n            ->with(['page.book', 'createdBy'])\n            ->reorder('id', $listOptions->getOrder())\n            ->paginate(50);\n\n        $this->setPageTitle(trans('entities.pages_revisions_named', ['pageName' => $page->getShortName()]));\n\n        return view('pages.revisions', [\n            'revisions'   => $revisions,\n            'page'        => $page,\n            'listOptions' => $listOptions,\n            'oldestRevisionId' => $page->revisions()->min('id'),\n        ]);\n    }\n\n    /**\n     * Shows a preview of a single revision.\n     *\n     * @throws NotFoundException\n     */\n    public function show(string $bookSlug, string $pageSlug, int $revisionId)\n    {\n        $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);\n        /** @var ?PageRevision $revision */\n        $revision = $page->revisions()->where('id', '=', $revisionId)->first();\n        if ($revision === null) {\n            throw new NotFoundException();\n        }\n\n        $page->fill($revision->toArray());\n        // TODO - Refactor PageContent so we don't need to juggle this\n        $page->html = $revision->html;\n        $page->html = (new PageContent($page))->render();\n\n        $this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));\n\n        return view('pages.revision', [\n            'page'     => $page,\n            'book'     => $page->book,\n            'diff'     => null,\n            'revision' => $revision,\n        ]);\n    }\n\n    /**\n     * Shows the changes of a single revision.\n     *\n     * @throws NotFoundException\n     */\n    public function changes(string $bookSlug, string $pageSlug, int $revisionId)\n    {\n        $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);\n        /** @var ?PageRevision $revision */\n        $revision = $page->revisions()->where('id', '=', $revisionId)->first();\n        if ($revision === null) {\n            throw new NotFoundException();\n        }\n\n        $prev = $revision->getPreviousRevision();\n        $prevContent = $prev->html ?? '';\n\n        // TODO - Refactor PageContent so we can de-dupe these steps\n        $rawDiff = Diff::excecute($prevContent, $revision->html);\n        $filterConfig = HtmlContentFilterConfig::fromConfigString(config('app.content_filtering'));\n        $filter = new HtmlContentFilter($filterConfig);\n        $diff = $filter->filterString($rawDiff);\n\n        $page->fill($revision->toArray());\n        $page->html = '';\n        $this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));\n\n        return view('pages.revision', [\n            'page'     => $page,\n            'book'     => $page->book,\n            'diff'     => $diff,\n            'revision' => $revision,\n        ]);\n    }\n\n    /**\n     * Restores a page using the content of the specified revision.\n     *\n     * @throws NotFoundException\n     */\n    public function restore(string $bookSlug, string $pageSlug, int $revisionId)\n    {\n        $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);\n        $this->checkOwnablePermission(Permission::PageUpdate, $page);\n\n        $page = $this->pageRepo->restoreRevision($page, $revisionId);\n\n        return redirect($page->getUrl());\n    }\n\n    /**\n     * Deletes a revision using the id of the specified revision.\n     *\n     * @throws NotFoundException\n     */\n    public function destroy(string $bookSlug, string $pageSlug, int $revId)\n    {\n        $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);\n        $this->checkOwnablePermission(Permission::PageDelete, $page);\n\n        $revision = $page->revisions()->where('id', '=', $revId)->first();\n        if ($revision === null) {\n            throw new NotFoundException(\"Revision #{$revId} not found\");\n        }\n\n        // Check if it's the latest revision, cannot delete the latest revision.\n        if (intval($page->currentRevision->id ?? null) === intval($revId)) {\n            $this->showErrorNotification(trans('entities.revision_cannot_delete_latest'));\n\n            return redirect($page->getUrl('/revisions'));\n        }\n\n        $revision->delete();\n        Activity::add(ActivityType::REVISION_DELETE, $revision);\n\n        return redirect($page->getUrl('/revisions'));\n    }\n\n    /**\n     * Destroys existing drafts, belonging to the current user, for the given page.\n     */\n    public function destroyUserDraft(string $pageId)\n    {\n        $page = $this->pageQueries->findVisibleByIdOrFail($pageId);\n        $this->revisionRepo->deleteDraftsForCurrentUser($page);\n\n        return response('', 200);\n    }\n}\n"
  },
  {
    "path": "app/Entities/Controllers/PageTemplateController.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Controllers;\n\nuse BookStack\\Entities\\Queries\\PageQueries;\nuse BookStack\\Entities\\Repos\\PageRepo;\nuse BookStack\\Exceptions\\NotFoundException;\nuse BookStack\\Http\\Controller;\nuse Illuminate\\Http\\Request;\n\nclass PageTemplateController extends Controller\n{\n    public function __construct(\n        protected PageRepo $pageRepo,\n        protected PageQueries $pageQueries,\n    ) {\n    }\n\n    /**\n     * Fetch a list of templates from the system.\n     */\n    public function list(Request $request)\n    {\n        $page = $request->get('page', 1);\n        $search = $request->get('search', '');\n        $count = 10;\n\n        $query = $this->pageQueries->visibleTemplates()\n            ->orderBy('name', 'asc')\n            ->skip(($page - 1) * $count)\n            ->take($count);\n\n        if ($search) {\n            $query->where('name', 'like', '%' . $search . '%');\n        }\n\n        $templates = $query->paginate($count, ['*'], 'page', $page);\n        $templates->withPath('/templates');\n\n        if ($search) {\n            $templates->appends(['search' => $search]);\n        }\n\n        return view('pages.parts.template-manager-list', [\n            'templates' => $templates,\n        ]);\n    }\n\n    /**\n     * Get the content of a template.\n     *\n     * @throws NotFoundException\n     */\n    public function get(int $templateId)\n    {\n        $page = $this->pageQueries->findVisibleByIdOrFail($templateId);\n\n        if (!$page->template) {\n            throw new NotFoundException();\n        }\n\n        return response()->json([\n            'html'     => $page->html,\n            'markdown' => $page->markdown,\n        ]);\n    }\n}\n"
  },
  {
    "path": "app/Entities/Controllers/RecycleBinApiController.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Controllers;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\BookChild;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Deletion;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Repos\\DeletionRepo;\nuse BookStack\\Http\\ApiController;\nuse BookStack\\Permissions\\Permission;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\n\nclass RecycleBinApiController extends ApiController\n{\n    public function __construct()\n    {\n        $this->middleware(function ($request, $next) {\n            $this->checkPermission(Permission::SettingsManage);\n            $this->checkPermission(Permission::RestrictionsManageAll);\n\n            return $next($request);\n        });\n    }\n\n    /**\n     * Get a top-level listing of the items in the recycle bin.\n     * The \"deletable\" property will reflect the main item deleted.\n     * For books and chapters, counts of child pages/chapters will\n     * be loaded within this \"deletable\" data.\n     * For chapters & pages, the parent item will be loaded within this \"deletable\" data.\n     * Requires permission to manage both system settings and permissions.\n     */\n    public function list()\n    {\n        return $this->apiListingResponse(Deletion::query()->with('deletable'), [\n            'id',\n            'deleted_by',\n            'created_at',\n            'updated_at',\n            'deletable_type',\n            'deletable_id',\n        ], [$this->listFormatter(...)]);\n    }\n\n    /**\n     * Restore a single deletion from the recycle bin.\n     * Requires permission to manage both system settings and permissions.\n     */\n    public function restore(DeletionRepo $deletionRepo, string $deletionId)\n    {\n        $restoreCount = $deletionRepo->restore(intval($deletionId));\n\n        return response()->json(['restore_count' => $restoreCount]);\n    }\n\n    /**\n     * Remove a single deletion from the recycle bin.\n     * Use this endpoint carefully as it will entirely remove the underlying deleted items from the system.\n     * Requires permission to manage both system settings and permissions.\n     */\n    public function destroy(DeletionRepo $deletionRepo, string $deletionId)\n    {\n        $deleteCount = $deletionRepo->destroy(intval($deletionId));\n\n        return response()->json(['delete_count' => $deleteCount]);\n    }\n\n    /**\n     * Load some related details for the deletion listing.\n     */\n    protected function listFormatter(Deletion $deletion): void\n    {\n        $deletable = $deletion->deletable;\n\n        if ($deletable instanceof BookChild) {\n            $parent = $deletable->getParent();\n            $parent->setAttribute('type', $parent->getType());\n            $deletable->setRelation('parent', $parent);\n        }\n\n        if ($deletable instanceof Book || $deletable instanceof Chapter) {\n            $countsToLoad = ['pages' => static::withTrashedQuery(...)];\n            if ($deletable instanceof Book) {\n                $countsToLoad['chapters'] = static::withTrashedQuery(...);\n            }\n            $deletable->loadCount($countsToLoad);\n        }\n    }\n\n    /**\n     * @param Builder<Chapter|Page> $query\n     */\n    protected static function withTrashedQuery(Builder $query): void\n    {\n        $query->withTrashed();\n    }\n}\n"
  },
  {
    "path": "app/Entities/Controllers/RecycleBinController.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Controllers;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Entities\\Models\\Deletion;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Repos\\DeletionRepo;\nuse BookStack\\Entities\\Tools\\TrashCan;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\n\nclass RecycleBinController extends Controller\n{\n    protected string $recycleBinBaseUrl = '/settings/recycle-bin';\n\n    /**\n     * On each request to a method of this controller check permissions\n     * using a middleware closure.\n     */\n    public function __construct()\n    {\n        $this->middleware(function ($request, $next) {\n            $this->checkPermission(Permission::SettingsManage);\n            $this->checkPermission(Permission::RestrictionsManageAll);\n\n            return $next($request);\n        });\n    }\n\n    /**\n     * Show the top-level listing for the recycle bin.\n     */\n    public function index()\n    {\n        $deletions = Deletion::query()->with(['deletable', 'deleter'])->paginate(10);\n\n        $this->setPageTitle(trans('settings.recycle_bin'));\n\n        return view('settings.recycle-bin.index', [\n            'deletions' => $deletions,\n        ]);\n    }\n\n    /**\n     * Show the page to confirm a restore of the deletion of the given id.\n     */\n    public function showRestore(string $id)\n    {\n        /** @var Deletion $deletion */\n        $deletion = Deletion::query()->findOrFail($id);\n\n        // Walk the parent chain to find any cascading parent deletions\n        $currentDeletable = $deletion->deletable;\n        $searching = true;\n        while ($searching && $currentDeletable instanceof Entity) {\n            $parent = $currentDeletable->getParent();\n            if ($parent && $parent->trashed()) {\n                $currentDeletable = $parent;\n            } else {\n                $searching = false;\n            }\n        }\n\n        /** @var ?Deletion $parentDeletion */\n        $parentDeletion = ($currentDeletable === $deletion->deletable) ? null : $currentDeletable->deletions()->first();\n\n        return view('settings.recycle-bin.restore', [\n            'deletion'       => $deletion,\n            'parentDeletion' => $parentDeletion,\n        ]);\n    }\n\n    /**\n     * Restore the element attached to the given deletion.\n     *\n     * @throws \\Exception\n     */\n    public function restore(DeletionRepo $deletionRepo, string $id)\n    {\n        $restoreCount = $deletionRepo->restore((int) $id);\n\n        $this->showSuccessNotification(trans('settings.recycle_bin_restore_notification', ['count' => $restoreCount]));\n\n        return redirect($this->recycleBinBaseUrl);\n    }\n\n    /**\n     * Show the page to confirm a Permanent deletion of the element attached to the deletion of the given id.\n     */\n    public function showDestroy(string $id)\n    {\n        /** @var Deletion $deletion */\n        $deletion = Deletion::query()->findOrFail($id);\n\n        return view('settings.recycle-bin.destroy', [\n            'deletion' => $deletion,\n        ]);\n    }\n\n    /**\n     * Permanently delete the content associated with the given deletion.\n     *\n     * @throws \\Exception\n     */\n    public function destroy(DeletionRepo $deletionRepo, string $id)\n    {\n        $deleteCount = $deletionRepo->destroy((int) $id);\n\n        $this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount]));\n\n        return redirect($this->recycleBinBaseUrl);\n    }\n\n    /**\n     * Empty out the recycle bin.\n     *\n     * @throws \\Exception\n     */\n    public function empty(TrashCan $trash)\n    {\n        $deleteCount = $trash->empty();\n\n        $this->logActivity(ActivityType::RECYCLE_BIN_EMPTY);\n        $this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount]));\n\n        return redirect($this->recycleBinBaseUrl);\n    }\n}\n"
  },
  {
    "path": "app/Entities/EntityExistsRule.php",
    "content": "<?php\n\nnamespace BookStack\\Entities;\n\nuse Illuminate\\Validation\\Rules\\Exists;\n\nclass EntityExistsRule implements \\Stringable\n{\n    public function __construct(\n        protected string $type,\n    ) {\n    }\n\n    public function __toString()\n    {\n        $existsRule = (new Exists('entities', 'id'))\n            ->where('type', $this->type);\n        return $existsRule->__toString();\n    }\n}\n"
  },
  {
    "path": "app/Entities/EntityProvider.php",
    "content": "<?php\n\nnamespace BookStack\\Entities;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Models\\PageRevision;\n\n/**\n * Class EntityProvider.\n *\n * Provides access to the core entity models.\n * Wrapped up in this provider since they are often used together\n * so this is a neater alternative to injecting all in individually.\n */\nclass EntityProvider\n{\n    public Bookshelf $bookshelf;\n    public Book $book;\n    public Chapter $chapter;\n    public Page $page;\n    public PageRevision $pageRevision;\n\n    public function __construct()\n    {\n        $this->bookshelf = new Bookshelf();\n        $this->book = new Book();\n        $this->chapter = new Chapter();\n        $this->page = new Page();\n        $this->pageRevision = new PageRevision();\n    }\n\n    /**\n     * Fetch all core entity types as an associated array\n     * with their basic names as the keys.\n     *\n     * @return array<string, Entity>\n     */\n    public function all(): array\n    {\n        return [\n            'bookshelf' => $this->bookshelf,\n            'book'      => $this->book,\n            'chapter'   => $this->chapter,\n            'page'      => $this->page,\n        ];\n    }\n\n    /**\n     * Get an entity instance by its basic name.\n     */\n    public function get(string $type): Entity\n    {\n        $type = strtolower($type);\n        $instance = $this->all()[$type] ?? null;\n\n        if (is_null($instance)) {\n            throw new \\InvalidArgumentException(\"Provided type \\\"{$type}\\\" is not a valid entity type\");\n        }\n\n        return $instance;\n    }\n\n    /**\n     * Get the morph classes, as an array, for a single or multiple types.\n     */\n    public function getMorphClasses(array $types): array\n    {\n        $morphClasses = [];\n        foreach ($types as $type) {\n            $model = $this->get($type);\n            $morphClasses[] = $model->getMorphClass();\n        }\n\n        return $morphClasses;\n    }\n}\n"
  },
  {
    "path": "app/Entities/Models/Book.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Models;\n\nuse BookStack\\Entities\\Tools\\EntityCover;\nuse BookStack\\Entities\\Tools\\EntityDefaultTemplate;\nuse BookStack\\Sorting\\SortRule;\nuse BookStack\\Uploads\\Image;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Collection;\n\n/**\n * Class Book.\n *\n * @property string                                   $description\n * @property string                                   $description_html\n * @property int                                      $image_id\n * @property ?int                                     $default_template_id\n * @property ?int                                     $sort_rule_id\n * @property \\Illuminate\\Database\\Eloquent\\Collection $chapters\n * @property \\Illuminate\\Database\\Eloquent\\Collection $pages\n * @property \\Illuminate\\Database\\Eloquent\\Collection $directPages\n * @property \\Illuminate\\Database\\Eloquent\\Collection $shelves\n * @property ?SortRule                                $sortRule\n */\nclass Book extends Entity implements HasDescriptionInterface, HasCoverInterface, HasDefaultTemplateInterface\n{\n    use HasFactory;\n    use ContainerTrait;\n\n    public float $searchFactor = 1.2;\n\n    protected $hidden = ['pivot', 'deleted_at', 'description_html', 'entity_id', 'entity_type', 'chapter_id', 'book_id', 'priority'];\n    protected $fillable = ['name'];\n\n    /**\n     * Get the url for this book.\n     */\n    public function getUrl(string $path = ''): string\n    {\n        return url('/books/' . implode('/', [urlencode($this->slug), trim($path, '/')]));\n    }\n\n    /**\n     * Get all pages within this book.\n     * @return HasMany<Page, $this>\n     */\n    public function pages(): HasMany\n    {\n        return $this->hasMany(Page::class);\n    }\n\n    /**\n     * Get the direct child pages of this book.\n     */\n    public function directPages(): HasMany\n    {\n        return $this->pages()->whereNull('chapter_id');\n    }\n\n    /**\n     * Get all chapters within this book.\n     * @return HasMany<Chapter, $this>\n     */\n    public function chapters(): HasMany\n    {\n        return $this->hasMany(Chapter::class);\n    }\n\n    /**\n     * Get the shelves this book is contained within.\n     */\n    public function shelves(): BelongsToMany\n    {\n        return $this->belongsToMany(Bookshelf::class, 'bookshelves_books', 'book_id', 'bookshelf_id');\n    }\n\n    /**\n     * Get the direct child items within this book.\n     */\n    public function getDirectVisibleChildren(): Collection\n    {\n        $pages = $this->directPages()->scopes('visible')->get();\n        $chapters = $this->chapters()->scopes('visible')->get();\n\n        return $pages->concat($chapters)->sortBy('priority')->sortByDesc('draft');\n    }\n\n    public function defaultTemplate(): EntityDefaultTemplate\n    {\n        return new EntityDefaultTemplate($this);\n    }\n\n    public function cover(): BelongsTo\n    {\n        return $this->belongsTo(Image::class, 'image_id');\n    }\n\n    public function coverInfo(): EntityCover\n    {\n        return new EntityCover($this);\n    }\n\n    /**\n     * Get the sort rule assigned to this container, if existing.\n     */\n    public function sortRule(): BelongsTo\n    {\n        return $this->belongsTo(SortRule::class);\n    }\n}\n"
  },
  {
    "path": "app/Entities/Models/BookChild.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\n\n/**\n * Class BookChild.\n *\n * @property int    $book_id\n * @property int    $priority\n * @property string $book_slug\n * @property Book   $book\n */\nabstract class BookChild extends Entity\n{\n    /**\n     * Get the book this page sits in.\n     * @return BelongsTo<Book, $this>\n     */\n    public function book(): BelongsTo\n    {\n        return $this->belongsTo(Book::class)->withTrashed();\n    }\n}\n"
  },
  {
    "path": "app/Entities/Models/Bookshelf.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Models;\n\nuse BookStack\\Entities\\Tools\\EntityCover;\nuse BookStack\\Uploads\\Image;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\n\n/**\n * @property string $description\n * @property string $description_html\n */\nclass Bookshelf extends Entity implements HasDescriptionInterface, HasCoverInterface\n{\n    use HasFactory;\n    use ContainerTrait;\n\n    public float $searchFactor = 1.2;\n\n    protected $hidden = ['pivot', 'image_id', 'deleted_at', 'description_html', 'priority', 'default_template_id', 'sort_rule_id', 'entity_id', 'entity_type', 'chapter_id', 'book_id'];\n    protected $fillable = ['name'];\n\n    /**\n     * Get the books in this shelf.\n     * Should not be used directly since it does not take into account permissions.\n     */\n    public function books(): BelongsToMany\n    {\n        return $this->belongsToMany(Book::class, 'bookshelves_books', 'bookshelf_id', 'book_id')\n            ->select(['entities.*', 'entity_container_data.*'])\n            ->withPivot('order')\n            ->orderBy('order', 'asc');\n    }\n\n    /**\n     * Related books that are visible to the current user.\n     */\n    public function visibleBooks(): BelongsToMany\n    {\n        return $this->books()->scopes('visible');\n    }\n\n    /**\n     * Get the url for this bookshelf.\n     */\n    public function getUrl(string $path = ''): string\n    {\n        return url('/shelves/' . implode('/', [urlencode($this->slug), trim($path, '/')]));\n    }\n\n    /**\n     * Check if this shelf contains the given book.\n     */\n    public function contains(Book $book): bool\n    {\n        return $this->books()->where('id', '=', $book->id)->count() > 0;\n    }\n\n    /**\n     * Add a book to the end of this shelf.\n     */\n    public function appendBook(Book $book): void\n    {\n        if ($this->contains($book)) {\n            return;\n        }\n\n        $maxOrder = $this->books()->max('order');\n        $this->books()->attach($book->id, ['order' => $maxOrder + 1]);\n    }\n\n    public function coverInfo(): EntityCover\n    {\n        return new EntityCover($this);\n    }\n\n    public function cover(): BelongsTo\n    {\n        return $this->belongsTo(Image::class, 'image_id');\n    }\n}\n"
  },
  {
    "path": "app/Entities/Models/Chapter.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Models;\n\nuse BookStack\\Entities\\Tools\\EntityDefaultTemplate;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Collection;\n\n/**\n * @property Collection<Page> $pages\n * @property ?int             $default_template_id\n * @property string           $description\n * @property string           $description_html\n */\nclass Chapter extends BookChild implements HasDescriptionInterface, HasDefaultTemplateInterface\n{\n    use HasFactory;\n    use ContainerTrait;\n\n    public float $searchFactor = 1.2;\n    protected $hidden = ['pivot', 'deleted_at', 'description_html', 'sort_rule_id', 'image_id', 'entity_id', 'entity_type', 'chapter_id'];\n    protected $fillable = ['name', 'priority'];\n\n    /**\n     * Get the pages that this chapter contains.\n     *\n     * @return HasMany<Page, $this>\n     */\n    public function pages(string $dir = 'ASC'): HasMany\n    {\n        return $this->hasMany(Page::class)->orderBy('priority', $dir);\n    }\n\n    /**\n     * Get the url of this chapter.\n     */\n    public function getUrl(string $path = ''): string\n    {\n        $parts = [\n            'books',\n            urlencode($this->book_slug ?? $this->book->slug),\n            'chapter',\n            urlencode($this->slug),\n            trim($path, '/'),\n        ];\n\n        return url('/' . implode('/', $parts));\n    }\n\n    /**\n     * Get the visible pages in this chapter.\n     * @return Collection<Page>\n     */\n    public function getVisiblePages(): Collection\n    {\n        return $this->pages()\n        ->scopes('visible')\n        ->orderBy('draft', 'desc')\n        ->orderBy('priority', 'asc')\n        ->get();\n    }\n\n    public function defaultTemplate(): EntityDefaultTemplate\n    {\n        return new EntityDefaultTemplate($this);\n    }\n}\n"
  },
  {
    "path": "app/Entities/Models/ContainerTrait.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Models;\n\nuse BookStack\\Entities\\Tools\\EntityHtmlDescription;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\n\n/**\n * @mixin Entity\n */\ntrait ContainerTrait\n{\n    public function descriptionInfo(): EntityHtmlDescription\n    {\n        return new EntityHtmlDescription($this);\n    }\n\n    /**\n     * @return HasOne<EntityContainerData, $this>\n     */\n    public function relatedData(): HasOne\n    {\n        return $this->hasOne(EntityContainerData::class, 'entity_id', 'id')\n            ->where('entity_type', '=', $this->getMorphClass());\n    }\n}\n"
  },
  {
    "path": "app/Entities/Models/DeletableInterface.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Relations\\MorphMany;\n\n/**\n * A model that can be deleted in a manner that deletions\n * are tracked to be part of the recycle bin system.\n */\ninterface DeletableInterface\n{\n    public function deletions(): MorphMany;\n}\n"
  },
  {
    "path": "app/Entities/Models/Deletion.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Models;\n\nuse BookStack\\Activity\\Models\\Loggable;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\MorphTo;\n\n/**\n * @property int       $id\n * @property int       $deleted_by\n * @property string    $deletable_type\n * @property int       $deletable_id\n * @property DeletableInterface $deletable\n */\nclass Deletion extends Model implements Loggable\n{\n    use HasFactory;\n\n    protected $hidden = [];\n\n    /**\n     * Get the related deletable record.\n     */\n    public function deletable(): MorphTo\n    {\n        return $this->morphTo('deletable')->withTrashed();\n    }\n\n    /**\n     * Get the user that performed the deletion.\n     */\n    public function deleter(): BelongsTo\n    {\n        return $this->belongsTo(User::class, 'deleted_by');\n    }\n\n    /**\n     * Create a new deletion record for the provided entity.\n     */\n    public static function createForEntity(Entity $entity): self\n    {\n        $record = (new self())->forceFill([\n            'deleted_by'     => user()->id,\n            'deletable_type' => $entity->getMorphClass(),\n            'deletable_id'   => $entity->id,\n        ]);\n        $record->save();\n\n        return $record;\n    }\n\n    public function logDescriptor(): string\n    {\n        $deletable = $this->deletable()->first();\n\n        if ($deletable instanceof Entity) {\n            return \"Deletion ({$this->id}) for {$deletable->getType()} ({$deletable->id}) {$deletable->name}\";\n        }\n\n        return \"Deletion ({$this->id})\";\n    }\n\n    /**\n     * Get a URL for this specific deletion.\n     */\n    public function getUrl(string $path = 'restore'): string\n    {\n        return url(\"/settings/recycle-bin/{$this->id}/\" . ltrim($path, '/'));\n    }\n}\n"
  },
  {
    "path": "app/Entities/Models/Entity.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Models;\n\nuse BookStack\\Activity\\Models\\Activity;\nuse BookStack\\Activity\\Models\\Comment;\nuse BookStack\\Activity\\Models\\Favouritable;\nuse BookStack\\Activity\\Models\\Favourite;\nuse BookStack\\Activity\\Models\\Loggable;\nuse BookStack\\Activity\\Models\\Tag;\nuse BookStack\\Activity\\Models\\View;\nuse BookStack\\Activity\\Models\\Viewable;\nuse BookStack\\Activity\\Models\\Watch;\nuse BookStack\\App\\Model;\nuse BookStack\\App\\SluggableInterface;\nuse BookStack\\Permissions\\JointPermissionBuilder;\nuse BookStack\\Permissions\\Models\\EntityPermission;\nuse BookStack\\Permissions\\Models\\JointPermission;\nuse BookStack\\Permissions\\PermissionApplicator;\nuse BookStack\\References\\Reference;\nuse BookStack\\Search\\SearchIndex;\nuse BookStack\\Search\\SearchTerm;\nuse BookStack\\Users\\Models\\HasCreatorAndUpdater;\nuse BookStack\\Users\\Models\\OwnableInterface;\nuse BookStack\\Users\\Models\\User;\nuse Carbon\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\Relations\\MorphMany;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\n\n/**\n * Class Entity\n * The base class for book-like items such as pages, chapters and books.\n * This is not a database model in itself but extended.\n *\n * @property int        $id\n * @property string     $type\n * @property string     $name\n * @property string     $slug\n * @property Carbon     $created_at\n * @property Carbon     $updated_at\n * @property Carbon     $deleted_at\n * @property int|null   $created_by\n * @property int|null   $updated_by\n * @property int|null   $owned_by\n * @property Collection $tags\n *\n * @method static Entity|Builder visible()\n * @method static Builder withLastView()\n * @method static Builder withViewCount()\n */\nabstract class Entity extends Model implements\n    SluggableInterface,\n    Favouritable,\n    Viewable,\n    DeletableInterface,\n    OwnableInterface,\n    Loggable\n{\n    use SoftDeletes;\n    use HasCreatorAndUpdater;\n\n    /**\n     * @var string - Name of property where the main text content is found\n     */\n    public string $textField = 'description';\n\n    /**\n     * @var string - Name of the property where the main HTML content is found\n     */\n    public string $htmlField = 'description_html';\n\n    /**\n     * @var float - Multiplier for search indexing.\n     */\n    public float $searchFactor = 1.0;\n\n    /**\n     * Set the table to be that used by all entities.\n     */\n    protected $table = 'entities';\n\n    /**\n     * Set a custom query builder for entities.\n     */\n    protected static string $builder = EntityQueryBuilder::class;\n\n    public static array $commonFields = [\n        'id',\n        'type',\n        'name',\n        'slug',\n        'book_id',\n        'chapter_id',\n        'priority',\n        'created_at',\n        'updated_at',\n        'deleted_at',\n        'created_by',\n        'updated_by',\n        'owned_by',\n    ];\n\n    /**\n     * Override the save method to also save the contents for convenience.\n     */\n    public function save(array $options = []): bool\n    {\n        /** @var EntityPageData|EntityContainerData $contents */\n        $contents = $this->relatedData()->firstOrNew();\n        $contentFields = $this->getContentsAttributes();\n\n        foreach ($contentFields as $key => $value) {\n            $contents->setAttribute($key, $value);\n            unset($this->attributes[$key]);\n        }\n\n        $this->setAttribute('type', $this->getMorphClass());\n        $result = parent::save($options);\n        $contentsResult = true;\n\n        if ($result && $contents->isDirty()) {\n            $contentsFillData = $contents instanceof EntityPageData ? ['page_id' => $this->id] : ['entity_id' => $this->id, 'entity_type' => $this->getMorphClass()];\n            $contents->forceFill($contentsFillData);\n            $contentsResult = $contents->save();\n            $this->touch();\n        }\n\n        $this->forceFill($contentFields);\n\n        return $result && $contentsResult;\n    }\n\n    /**\n     * Check if this item is a container item.\n     */\n    public function isContainer(): bool\n    {\n        return $this instanceof Bookshelf ||\n            $this instanceof Book ||\n            $this instanceof Chapter;\n    }\n\n    /**\n     * Get the entities that are visible to the current user.\n     */\n    public function scopeVisible(Builder $query): Builder\n    {\n        return app()->make(PermissionApplicator::class)->restrictEntityQuery($query);\n    }\n\n    /**\n     * Query scope to get the last view from the current user.\n     */\n    public function scopeWithLastView(Builder $query)\n    {\n        $viewedAtQuery = View::query()->select('updated_at')\n            ->whereColumn('viewable_id', '=', 'entities.id')\n            ->whereColumn('viewable_type', '=', 'entities.type')\n            ->where('user_id', '=', user()->id)\n            ->take(1);\n\n        return $query->addSelect(['last_viewed_at' => $viewedAtQuery]);\n    }\n\n    /**\n     * Query scope to get the total view count of the entities.\n     */\n    public function scopeWithViewCount(Builder $query): void\n    {\n        $viewCountQuery = View::query()->selectRaw('SUM(views) as view_count')\n            ->whereColumn('viewable_id', '=', 'entities.id')\n            ->whereColumn('viewable_type', '=', 'entities.type')\n            ->take(1);\n\n        $query->addSelect(['view_count' => $viewCountQuery]);\n    }\n\n    /**\n     * Compares this entity to another given entity.\n     * Matches by comparing class and id.\n     */\n    public function matches(self $entity): bool\n    {\n        return [get_class($this), $this->id] === [get_class($entity), $entity->id];\n    }\n\n    /**\n     * Checks if the current entity matches or contains the given.\n     */\n    public function matchesOrContains(self $entity): bool\n    {\n        if ($this->matches($entity)) {\n            return true;\n        }\n\n        if (($entity instanceof BookChild) && $this instanceof Book) {\n            return $entity->book_id === $this->id;\n        }\n\n        if ($entity instanceof Page && $this instanceof Chapter) {\n            return $entity->chapter_id === $this->id;\n        }\n\n        return false;\n    }\n\n    /**\n     * Gets the activity objects for this entity.\n     */\n    public function activity(): MorphMany\n    {\n        return $this->morphMany(Activity::class, 'loggable')\n            ->orderBy('created_at', 'desc');\n    }\n\n    /**\n     * Get View objects for this entity.\n     */\n    public function views(): MorphMany\n    {\n        return $this->morphMany(View::class, 'viewable');\n    }\n\n    /**\n     * Get the Tag models that have been user assigned to this entity.\n     */\n    public function tags(): MorphMany\n    {\n        return $this->morphMany(Tag::class, 'entity')\n            ->orderBy('order', 'asc');\n    }\n\n    /**\n     * Get the comments for an entity.\n     * @return MorphMany<Comment, $this>\n     */\n    public function comments(bool $orderByCreated = true): MorphMany\n    {\n        $query = $this->morphMany(Comment::class, 'commentable');\n\n        return $orderByCreated ? $query->orderBy('created_at', 'asc') : $query;\n    }\n\n    /**\n     * Get the related search terms.\n     */\n    public function searchTerms(): MorphMany\n    {\n        return $this->morphMany(SearchTerm::class, 'entity');\n    }\n\n    /**\n     * Get this entities assigned permissions.\n     */\n    public function permissions(): MorphMany\n    {\n        return $this->morphMany(EntityPermission::class, 'entity');\n    }\n\n    /**\n     * Check if this entity has a specific restriction set against it.\n     */\n    public function hasPermissions(): bool\n    {\n        return $this->permissions()->count() > 0;\n    }\n\n    /**\n     * Get the entity jointPermissions this is connected to.\n     */\n    public function jointPermissions(): MorphMany\n    {\n        return $this->morphMany(JointPermission::class, 'entity');\n    }\n\n    /**\n     * Get the user who owns this entity.\n     * @return BelongsTo<User, $this>\n     */\n    public function ownedBy(): BelongsTo\n    {\n        return $this->belongsTo(User::class, 'owned_by');\n    }\n\n    public function getOwnerFieldName(): string\n    {\n        return 'owned_by';\n    }\n\n    /**\n     * Get the related delete records for this entity.\n     */\n    public function deletions(): MorphMany\n    {\n        return $this->morphMany(Deletion::class, 'deletable');\n    }\n\n    /**\n     * Get the references pointing from this entity to other items.\n     */\n    public function referencesFrom(): MorphMany\n    {\n        return $this->morphMany(Reference::class, 'from');\n    }\n\n    /**\n     * Get the references pointing to this entity from other items.\n     */\n    public function referencesTo(): MorphMany\n    {\n        return $this->morphMany(Reference::class, 'to');\n    }\n\n    /**\n     * Check if this instance or class is a certain type of entity.\n     * Examples of $type are 'page', 'book', 'chapter'.\n     *\n     * @deprecated Use instanceof instead.\n     */\n    public static function isA(string $type): bool\n    {\n        return static::getType() === strtolower($type);\n    }\n\n    /**\n     * Get the entity type as a simple lowercase word.\n     */\n    public static function getType(): string\n    {\n        $className = array_slice(explode('\\\\', static::class), -1, 1)[0];\n\n        return strtolower($className);\n    }\n\n    /**\n     * Gets a limited-length version of the entity name.\n     */\n    public function getShortName(int $length = 25): string\n    {\n        if (mb_strlen($this->name) <= $length) {\n            return $this->name;\n        }\n\n        return mb_substr($this->name, 0, $length - 3) . '...';\n    }\n\n    /**\n     * Get an excerpt of this entity's descriptive content to the specified length.\n     */\n    public function getExcerpt(int $length = 100): string\n    {\n        $text = $this->{$this->textField} ?? '';\n\n        if (mb_strlen($text) > $length) {\n            $text = mb_substr($text, 0, $length - 3) . '...';\n        }\n\n        return trim($text);\n    }\n\n    /**\n     * Get the url of this entity.\n     */\n    abstract public function getUrl(string $path = '/'): string;\n\n    /**\n     * Get the parent entity if existing.\n     * This is the \"static\" parent and does not include dynamic\n     * relations such as shelves to books.\n     */\n    public function getParent(): ?self\n    {\n        if ($this instanceof Page) {\n            /** @var BelongsTo<Chapter|Book, Page>  $builder */\n            $builder = $this->chapter_id ? $this->chapter() : $this->book();\n            return $builder->withTrashed()->first();\n        }\n        if ($this instanceof Chapter) {\n            /** @var BelongsTo<Book, Page>  $builder */\n            $builder = $this->book();\n            return $builder->withTrashed()->first();\n        }\n\n        return null;\n    }\n\n    /**\n     * Rebuild the permissions for this entity.\n     */\n    public function rebuildPermissions(): void\n    {\n        app()->make(JointPermissionBuilder::class)->rebuildForEntity(clone $this);\n    }\n\n    /**\n     * Index the current entity for search.\n     */\n    public function indexForSearch(): void\n    {\n        app()->make(SearchIndex::class)->indexEntity(clone $this);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function favourites(): MorphMany\n    {\n        return $this->morphMany(Favourite::class, 'favouritable');\n    }\n\n    /**\n     * Check if the entity is a favourite of the current user.\n     */\n    public function isFavourite(): bool\n    {\n        return $this->favourites()\n            ->where('user_id', '=', user()->id)\n            ->exists();\n    }\n\n    /**\n     * Get the related watches for this entity.\n     */\n    public function watches(): MorphMany\n    {\n        return $this->morphMany(Watch::class, 'watchable');\n    }\n\n    /**\n     * Get the related slug history for this entity.\n     */\n    public function slugHistory(): MorphMany\n    {\n        return $this->morphMany(SlugHistory::class, 'sluggable');\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function logDescriptor(): string\n    {\n        return \"({$this->id}) {$this->name}\";\n    }\n\n    /**\n     * @return HasOne<covariant (EntityContainerData|EntityPageData), $this>\n     */\n    abstract public function relatedData(): HasOne;\n\n    /**\n     * Get the attributes that are intended for the related contents model.\n     * @return array<string, mixed>\n     */\n    protected function getContentsAttributes(): array\n    {\n        $contentFields = [];\n        $contentModel = $this instanceof Page ? EntityPageData::class : EntityContainerData::class;\n\n        foreach ($this->attributes as $key => $value) {\n            if (in_array($key, $contentModel::$fields)) {\n                $contentFields[$key] = $value;\n            }\n        }\n\n        return $contentFields;\n    }\n\n    /**\n     * Create a new instance for the given entity type.\n     */\n    public static function instanceFromType(string $type): self\n    {\n        return match ($type) {\n            'page' => new Page(),\n            'chapter' => new Chapter(),\n            'book' => new Book(),\n            'bookshelf' => new Bookshelf(),\n        };\n    }\n}\n"
  },
  {
    "path": "app/Entities/Models/EntityContainerData.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Model;\n\n/**\n * @property int     $entity_id\n * @property string  $entity_type\n * @property string  $description\n * @property string  $description_html\n * @property ?int    $default_template_id\n * @property ?int    $image_id\n * @property ?int    $sort_rule_id\n */\nclass EntityContainerData extends Model\n{\n    public $timestamps = false;\n    protected $primaryKey = 'entity_id';\n    public $incrementing = false;\n\n    public static array $fields = [\n        'description',\n        'description_html',\n        'default_template_id',\n        'image_id',\n        'sort_rule_id',\n    ];\n\n    /**\n     * Override the default set keys for save query method to make it work with composite keys.\n     */\n    public function setKeysForSaveQuery($query): Builder\n    {\n        $query->where($this->getKeyName(), '=', $this->getKeyForSaveQuery())\n            ->where('entity_type', '=', $this->entity_type);\n\n        return $query;\n    }\n\n    /**\n     * Override the default set keys for a select query method to make it work with composite keys.\n     */\n    protected function setKeysForSelectQuery($query): Builder\n    {\n        $query->where($this->getKeyName(), '=', $this->getKeyForSelectQuery())\n            ->where('entity_type', '=', $this->entity_type);\n\n        return $query;\n    }\n}\n"
  },
  {
    "path": "app/Entities/Models/EntityPageData.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Model;\n\n/**\n * @property int    $page_id\n */\nclass EntityPageData extends Model\n{\n    public $timestamps = false;\n    protected $primaryKey = 'page_id';\n    public $incrementing = false;\n\n    public static array $fields = [\n        'draft',\n        'template',\n        'revision_count',\n        'editor',\n        'html',\n        'text',\n        'markdown',\n    ];\n}\n"
  },
  {
    "path": "app/Entities/Models/EntityQueryBuilder.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Query\\Builder as QueryBuilder;\n\nclass EntityQueryBuilder extends Builder\n{\n    /**\n     * Create a new Eloquent query builder instance.\n     */\n    public function __construct(QueryBuilder $query)\n    {\n        parent::__construct($query);\n\n        $this->withGlobalScope('entity', new EntityScope());\n    }\n\n    public function withoutGlobalScope($scope): static\n    {\n        // Prevent removal of the entity scope\n        if ($scope === 'entity') {\n            return $this;\n        }\n\n        return parent::withoutGlobalScope($scope);\n    }\n\n    /**\n     * Override the default forceDelete method to add type filter onto the query\n     * since it specifically ignores scopes by default.\n     */\n    public function forceDelete()\n    {\n        return $this->query->where('type', '=', $this->model->getMorphClass())->delete();\n    }\n}\n"
  },
  {
    "path": "app/Entities/Models/EntityScope.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Scope;\nuse Illuminate\\Database\\Query\\JoinClause;\n\nclass EntityScope implements Scope\n{\n    /**\n     * Apply the scope to a given Eloquent query builder.\n     */\n    public function apply(Builder $builder, Model $model): void\n    {\n        $builder = $builder->where('type', '=', $model->getMorphClass());\n        $table = $model->getTable();\n        if ($model instanceof Page) {\n            $builder->leftJoin('entity_page_data', 'entity_page_data.page_id', '=', \"{$table}.id\");\n        } else {\n            $builder->leftJoin('entity_container_data', function (JoinClause $join) use ($model, $table) {\n                $join->on('entity_container_data.entity_id', '=', \"{$table}.id\")\n                    ->where('entity_container_data.entity_type', '=', $model->getMorphClass());\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "app/Entities/Models/EntityTable.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Models;\n\nuse BookStack\\Activity\\Models\\Tag;\nuse BookStack\\Activity\\Models\\View;\nuse BookStack\\App\\Model;\nuse BookStack\\Permissions\\Models\\EntityPermission;\nuse BookStack\\Permissions\\Models\\JointPermission;\nuse BookStack\\Permissions\\PermissionApplicator;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\MorphMany;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\n\n/**\n * This is a simplistic model interpretation of a generic Entity used to query and represent\n * that database abstractly. Generally, this should rarely be used outside queries.\n */\nclass EntityTable extends Model\n{\n    use SoftDeletes;\n\n    protected $table = 'entities';\n\n    /**\n     * Get the entities that are visible to the current user.\n     */\n    public function scopeVisible(Builder $query): Builder\n    {\n        return app()->make(PermissionApplicator::class)->restrictEntityQuery($query);\n    }\n\n    /**\n     * Get the entity jointPermissions this is connected to.\n     */\n    public function jointPermissions(): HasMany\n    {\n        return $this->hasMany(JointPermission::class, 'entity_id')\n            ->whereColumn('entity_type', '=', 'entities.type');\n    }\n\n    /**\n     * Get the Tags that have been assigned to entities.\n     */\n    public function tags(): HasMany\n    {\n        return $this->hasMany(Tag::class, 'entity_id')\n            ->whereColumn('entity_type', '=', 'entities.type');\n    }\n\n    /**\n     * Get the assigned permissions.\n     */\n    public function permissions(): HasMany\n    {\n        return $this->hasMany(EntityPermission::class, 'entity_id')\n            ->whereColumn('entity_type', '=', 'entities.type');\n    }\n\n    /**\n     * Get View objects for this entity.\n     */\n    public function views(): HasMany\n    {\n        return $this->hasMany(View::class, 'viewable_id')\n            ->whereColumn('viewable_type', '=', 'entities.type');\n    }\n}\n"
  },
  {
    "path": "app/Entities/Models/HasCoverInterface.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Models;\n\nuse BookStack\\Entities\\Tools\\EntityCover;\nuse BookStack\\Uploads\\Image;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\n\ninterface HasCoverInterface\n{\n    public function coverInfo(): EntityCover;\n\n    /**\n     * The cover image of this entity.\n     * @return BelongsTo<Image, covariant Entity>\n     */\n    public function cover(): BelongsTo;\n}\n"
  },
  {
    "path": "app/Entities/Models/HasDefaultTemplateInterface.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Models;\n\nuse BookStack\\Entities\\Tools\\EntityDefaultTemplate;\n\ninterface HasDefaultTemplateInterface\n{\n    public function defaultTemplate(): EntityDefaultTemplate;\n}\n"
  },
  {
    "path": "app/Entities/Models/HasDescriptionInterface.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Models;\n\nuse BookStack\\Entities\\Tools\\EntityHtmlDescription;\n\ninterface HasDescriptionInterface\n{\n    public function descriptionInfo(): EntityHtmlDescription;\n}\n"
  },
  {
    "path": "app/Entities/Models/Page.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Models;\n\nuse BookStack\\Entities\\Tools\\PageContent;\nuse BookStack\\Permissions\\PermissionApplicator;\nuse BookStack\\Uploads\\Attachment;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\n\n/**\n * Class Page.\n * @property EntityPageData $pageData\n * @property int          $chapter_id\n * @property string       $html\n * @property string       $markdown\n * @property string       $text\n * @property bool         $template\n * @property bool         $draft\n * @property int          $revision_count\n * @property string       $editor\n * @property Chapter      $chapter\n * @property Collection   $attachments\n * @property Collection   $revisions\n * @property PageRevision $currentRevision\n */\nclass Page extends BookChild\n{\n    use HasFactory;\n\n    public string $textField = 'text';\n    public string $htmlField = 'html';\n    protected $hidden = ['html', 'markdown', 'text', 'pivot', 'deleted_at',  'entity_id', 'entity_type'];\n    protected $fillable = ['name', 'priority'];\n\n    protected $casts = [\n        'draft'    => 'boolean',\n        'template' => 'boolean',\n    ];\n\n    /**\n     * Get the entities that are visible to the current user.\n     */\n    public function scopeVisible(Builder $query): Builder\n    {\n        $query = app()->make(PermissionApplicator::class)->restrictDraftsOnPageQuery($query);\n\n        return parent::scopeVisible($query);\n    }\n\n    /**\n     * Get the chapter that this page is in, If applicable.\n     */\n    public function chapter(): BelongsTo\n    {\n        return $this->belongsTo(Chapter::class);\n    }\n\n    /**\n     * Check if this page has a chapter.\n     */\n    public function hasChapter(): bool\n    {\n        return $this->chapter()->count() > 0;\n    }\n\n    /**\n     * Get the associated page revisions, ordered by created date.\n     * Only provides actual saved page revision instances, Not drafts.\n     */\n    public function revisions(): HasMany\n    {\n        return $this->allRevisions()\n            ->where('type', '=', 'version')\n            ->orderBy('created_at', 'desc')\n            ->orderBy('id', 'desc');\n    }\n\n    /**\n     * Get the current revision for the page if existing.\n     */\n    public function currentRevision(): HasOne\n    {\n        return $this->hasOne(PageRevision::class)\n            ->where('type', '=', 'version')\n            ->orderBy('created_at', 'desc')\n            ->orderBy('id', 'desc');\n    }\n\n    /**\n     * Get all revision instances assigned to this page.\n     * Includes all types of revisions.\n     */\n    public function allRevisions(): HasMany\n    {\n        return $this->hasMany(PageRevision::class);\n    }\n\n    /**\n     * Get the attachments assigned to this page.\n     */\n    public function attachments(): HasMany\n    {\n        return $this->hasMany(Attachment::class, 'uploaded_to')->orderBy('order', 'asc');\n    }\n\n    /**\n     * Get the url of this page.\n     */\n    public function getUrl(string $path = ''): string\n    {\n        $parts = [\n            'books',\n            urlencode($this->book_slug ?? $this->book->slug),\n            $this->draft ? 'draft' : 'page',\n            $this->draft ? $this->id : urlencode($this->slug),\n            trim($path, '/'),\n        ];\n\n        return url('/' . implode('/', $parts));\n    }\n\n    /**\n     * Get the ID-based permalink for this page.\n     */\n    public function getPermalink(): string\n    {\n        return url(\"/link/{$this->id}\");\n    }\n\n    /**\n     * Get this page for JSON display.\n     */\n    public function forJsonDisplay(): self\n    {\n        $refreshed = $this->refresh()->unsetRelations()->load(['tags', 'createdBy', 'updatedBy', 'ownedBy']);\n        $refreshed->setHidden(array_diff($refreshed->getHidden(), ['html', 'markdown']));\n        $refreshed->setAttribute('raw_html', $refreshed->html);\n        $refreshed->setAttribute('html', (new PageContent($refreshed))->render());\n\n        return $refreshed;\n    }\n\n    /**\n     * @return HasOne<EntityPageData, $this>\n     */\n    public function relatedData(): HasOne\n    {\n        return $this->hasOne(EntityPageData::class, 'page_id', 'id');\n    }\n}\n"
  },
  {
    "path": "app/Entities/Models/PageRevision.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Models;\n\nuse BookStack\\Activity\\Models\\Loggable;\nuse BookStack\\App\\Model;\nuse BookStack\\Users\\Models\\User;\nuse Carbon\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\n\n/**\n * Class PageRevision.\n *\n * @property mixed  $id\n * @property int    $page_id\n * @property string $name\n * @property string $slug\n * @property string $book_slug\n * @property int    $created_by\n * @property Carbon $created_at\n * @property Carbon $updated_at\n * @property string $type\n * @property string $summary\n * @property string $markdown\n * @property string $html\n * @property string $text\n * @property int    $revision_number\n * @property Page   $page\n * @property-read ?User $createdBy\n */\nclass PageRevision extends Model implements Loggable\n{\n    use HasFactory;\n\n    protected $fillable = ['name', 'text', 'summary'];\n    protected $hidden = ['html', 'markdown', 'text'];\n\n    /**\n     * Get the user that created the page revision.\n     */\n    public function createdBy(): BelongsTo\n    {\n        return $this->belongsTo(User::class, 'created_by');\n    }\n\n    /**\n     * Get the page this revision originates from.\n     */\n    public function page(): BelongsTo\n    {\n        return $this->belongsTo(Page::class);\n    }\n\n    /**\n     * Get the url for this revision.\n     */\n    public function getUrl(string $path = ''): string\n    {\n        return $this->page->getUrl('/revisions/' . $this->id . '/' . ltrim($path, '/'));\n    }\n\n    /**\n     * Get the previous revision for the same page if existing.\n     */\n    public function getPreviousRevision(): ?PageRevision\n    {\n        $id = static::newQuery()->where('page_id', '=', $this->page_id)\n            ->where('id', '<', $this->id)\n            ->max('id');\n\n        if ($id) {\n            return static::query()->find($id);\n        }\n\n        return null;\n    }\n\n    /**\n     * Allows checking of the exact class, Used to check entity type.\n     * Included here to align with entities in similar use cases.\n     * (Yup, Bit of an awkward hack).\n     *\n     * @deprecated Use instanceof instead.\n     */\n    public static function isA(string $type): bool\n    {\n        return $type === 'revision';\n    }\n\n    public function logDescriptor(): string\n    {\n        return \"Revision #{$this->revision_number} (ID: {$this->id}) for page ID {$this->page_id}\";\n    }\n}\n"
  },
  {
    "path": "app/Entities/Models/SlugHistory.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Models;\n\nuse BookStack\\App\\Model;\nuse BookStack\\Permissions\\Models\\JointPermission;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\n\n/**\n * @property int $id\n * @property int $sluggable_id\n * @property string $sluggable_type\n * @property string $slug\n * @property ?string $parent_slug\n */\nclass SlugHistory extends Model\n{\n    use HasFactory;\n\n    protected $table = 'slug_history';\n\n    public function jointPermissions(): HasMany\n    {\n        return $this->hasMany(JointPermission::class, 'entity_id', 'sluggable_id')\n            ->whereColumn('joint_permissions.entity_type', '=', 'slug_history.sluggable_type');\n    }\n}\n"
  },
  {
    "path": "app/Entities/Queries/BookQueries.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Queries;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Exceptions\\NotFoundException;\nuse Illuminate\\Database\\Eloquent\\Builder;\n\n/**\n * @implements ProvidesEntityQueries<Book>\n */\nclass BookQueries implements ProvidesEntityQueries\n{\n    protected static array $listAttributes = [\n        'id', 'slug', 'name', 'description',\n        'created_at', 'updated_at', 'image_id', 'owned_by',\n    ];\n\n    /**\n     * @return Builder<Book>\n     */\n    public function start(): Builder\n    {\n        return Book::query();\n    }\n\n    public function findVisibleById(int $id): ?Book\n    {\n        return $this->start()->scopes('visible')->find($id);\n    }\n\n    public function findVisibleByIdOrFail(int $id): Book\n    {\n        return $this->start()->scopes('visible')->findOrFail($id);\n    }\n\n    public function findVisibleBySlugOrFail(string $slug): Book\n    {\n        /** @var ?Book $book */\n        $book = $this->start()\n            ->scopes('visible')\n            ->where('slug', '=', $slug)\n            ->first();\n\n        if ($book === null) {\n            throw new NotFoundException(trans('errors.book_not_found'));\n        }\n\n        return $book;\n    }\n\n    public function visibleForList(): Builder\n    {\n        return $this->start()->scopes('visible')\n            ->select(static::$listAttributes);\n    }\n\n    public function visibleForContent(): Builder\n    {\n        return $this->start()->scopes('visible');\n    }\n\n    public function visibleForListWithCover(): Builder\n    {\n        return $this->visibleForList()->with('cover');\n    }\n\n    public function recentlyViewedForCurrentUser(): Builder\n    {\n        return $this->visibleForList()\n            ->scopes('withLastView')\n            ->having('last_viewed_at', '>', 0)\n            ->orderBy('last_viewed_at', 'desc');\n    }\n\n    public function popularForList(): Builder\n    {\n        return $this->visibleForList()\n            ->scopes('withViewCount')\n            ->having('view_count', '>', 0)\n            ->orderBy('view_count', 'desc');\n    }\n}\n"
  },
  {
    "path": "app/Entities/Queries/BookshelfQueries.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Queries;\n\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Exceptions\\NotFoundException;\nuse Illuminate\\Database\\Eloquent\\Builder;\n\n/**\n * @implements ProvidesEntityQueries<Bookshelf>\n */\nclass BookshelfQueries implements ProvidesEntityQueries\n{\n    protected static array $listAttributes = [\n        'id', 'slug', 'name', 'description',\n        'created_at', 'updated_at', 'image_id', 'owned_by',\n    ];\n\n    /**\n     * @return Builder<Bookshelf>\n     */\n    public function start(): Builder\n    {\n        return Bookshelf::query();\n    }\n\n    public function findVisibleById(int $id): ?Bookshelf\n    {\n        return $this->start()->scopes('visible')->find($id);\n    }\n\n    public function findVisibleByIdOrFail(int $id): Bookshelf\n    {\n        $shelf = $this->findVisibleById($id);\n\n        if (is_null($shelf)) {\n            throw new NotFoundException(trans('errors.bookshelf_not_found'));\n        }\n\n        return $shelf;\n    }\n\n    public function findVisibleBySlugOrFail(string $slug): Bookshelf\n    {\n        /** @var ?Bookshelf $shelf */\n        $shelf = $this->start()\n            ->scopes('visible')\n            ->where('slug', '=', $slug)\n            ->first();\n\n        if ($shelf === null) {\n            throw new NotFoundException(trans('errors.bookshelf_not_found'));\n        }\n\n        return $shelf;\n    }\n\n    public function visibleForList(): Builder\n    {\n        return $this->start()->scopes('visible')->select(static::$listAttributes);\n    }\n\n    public function visibleForContent(): Builder\n    {\n        return $this->start()->scopes('visible');\n    }\n\n    public function visibleForListWithCover(): Builder\n    {\n        return $this->visibleForList()->with('cover');\n    }\n\n    public function recentlyViewedForCurrentUser(): Builder\n    {\n        return $this->visibleForList()\n            ->scopes('withLastView')\n            ->having('last_viewed_at', '>', 0)\n            ->orderBy('last_viewed_at', 'desc');\n    }\n\n    public function popularForList(): Builder\n    {\n        return $this->visibleForList()\n            ->scopes('withViewCount')\n            ->having('view_count', '>', 0)\n            ->orderBy('view_count', 'desc');\n    }\n}\n"
  },
  {
    "path": "app/Entities/Queries/ChapterQueries.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Queries;\n\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Exceptions\\NotFoundException;\nuse Illuminate\\Database\\Eloquent\\Builder;\n\n/**\n * @implements ProvidesEntityQueries<Chapter>\n */\nclass ChapterQueries implements ProvidesEntityQueries\n{\n    protected static array $listAttributes = [\n        'id', 'slug', 'name', 'description', 'priority',\n        'book_id', 'created_at', 'updated_at', 'owned_by',\n    ];\n\n    public function start(): Builder\n    {\n        return Chapter::query();\n    }\n\n    public function findVisibleById(int $id): ?Chapter\n    {\n        return $this->start()->scopes('visible')->find($id);\n    }\n\n    public function findVisibleByIdOrFail(int $id): Chapter\n    {\n        return $this->start()->scopes('visible')->findOrFail($id);\n    }\n\n    public function findVisibleBySlugsOrFail(string $bookSlug, string $chapterSlug): Chapter\n    {\n        /** @var ?Chapter $chapter */\n        $chapter = $this->start()\n            ->scopes('visible')\n            ->with('book')\n            ->whereHas('book', function (Builder $query) use ($bookSlug) {\n                $query->where('slug', '=', $bookSlug);\n            })\n            ->where('slug', '=', $chapterSlug)\n            ->first();\n\n        if (is_null($chapter)) {\n            throw new NotFoundException(trans('errors.chapter_not_found'));\n        }\n\n        return $chapter;\n    }\n\n    public function usingSlugs(string $bookSlug, string $chapterSlug): Builder\n    {\n        return $this->start()\n            ->where('slug', '=', $chapterSlug)\n            ->whereHas('book', function (Builder $query) use ($bookSlug) {\n                $query->where('slug', '=', $bookSlug);\n            });\n    }\n\n    public function visibleForList(): Builder\n    {\n        return $this->start()\n            ->scopes('visible')\n            ->select(array_merge(static::$listAttributes, ['book_slug' => function ($builder) {\n                $builder->select('slug')\n                    ->from('entities as books')\n                    ->where('type', '=', 'book')\n                    ->whereColumn('books.id', '=', 'entities.book_id');\n            }]));\n    }\n\n    public function visibleForContent(): Builder\n    {\n        return $this->start()->scopes('visible');\n    }\n}\n"
  },
  {
    "path": "app/Entities/Queries/EntityQueries.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Queries;\n\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\EntityTable;\nuse BookStack\\Entities\\Tools\\SlugHistory;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Query\\Builder as QueryBuilder;\nuse Illuminate\\Database\\Query\\JoinClause;\nuse Illuminate\\Support\\Facades\\DB;\nuse InvalidArgumentException;\n\nclass EntityQueries\n{\n    public function __construct(\n        public BookshelfQueries $shelves,\n        public BookQueries $books,\n        public ChapterQueries $chapters,\n        public PageQueries $pages,\n        public PageRevisionQueries $revisions,\n        protected SlugHistory $slugHistory,\n    ) {\n    }\n\n    /**\n     * Find an entity via an identifier string in the format:\n     * {type}:{id}\n     * Example: (book:5).\n     */\n    public function findVisibleByStringIdentifier(string $identifier): ?Entity\n    {\n        $explodedId = explode(':', $identifier);\n        $entityType = $explodedId[0];\n        $entityId = intval($explodedId[1]);\n\n        return $this->findVisibleById($entityType, $entityId);\n    }\n\n    /**\n     * Find an entity by its ID.\n     */\n    public function findVisibleById(string $type, int $id): ?Entity\n    {\n        $queries = $this->getQueriesForType($type);\n        return $queries->findVisibleById($id);\n    }\n\n    /**\n     * Find an entity by looking up old slugs in the slug history.\n     */\n    public function findVisibleByOldSlugs(string $type, string $slug, string $parentSlug = ''): ?Entity\n    {\n        $id = $this->slugHistory->lookupEntityIdUsingSlugs($type, $slug, $parentSlug);\n        if ($id === null) {\n            return null;\n        }\n\n        return $this->findVisibleById($type, $id);\n    }\n\n    /**\n     * Start a query across all entity types.\n     * Combines the description/text fields into a single 'description' field.\n     * @return Builder<EntityTable>\n     */\n    public function visibleForList(): Builder\n    {\n        $rawDescriptionField = DB::raw('COALESCE(description, text) as description');\n        $bookSlugSelect = function (QueryBuilder $query) {\n            return $query->select('slug')->from('entities as books')\n                ->whereColumn('books.id', '=', 'entities.book_id')\n                ->where('type', '=', 'book');\n        };\n\n        return EntityTable::query()->scopes('visible')\n            ->select(['id', 'type', 'name', 'slug', 'book_id', 'chapter_id', 'created_at', 'updated_at', 'draft', 'book_slug' => $bookSlugSelect, $rawDescriptionField])\n            ->leftJoin('entity_container_data', function (JoinClause $join) {\n                $join->on('entity_container_data.entity_id', '=', 'entities.id')\n                    ->on('entity_container_data.entity_type', '=', 'entities.type');\n            })->leftJoin('entity_page_data', function (JoinClause $join) {\n                $join->on('entity_page_data.page_id', '=', 'entities.id')\n                    ->where('entities.type', '=', 'page');\n            });\n    }\n\n    /**\n     * Start a query of visible entities of the given type,\n     * suitable for listing display.\n     * @return Builder<Entity>\n     */\n    public function visibleForListForType(string $entityType): Builder\n    {\n        $queries = $this->getQueriesForType($entityType);\n        return $queries->visibleForList();\n    }\n\n    /**\n     * Start a query of visible entities of the given type,\n     * suitable for using the contents of the items.\n     * @return Builder<Entity>\n     */\n    public function visibleForContentForType(string $entityType): Builder\n    {\n        $queries = $this->getQueriesForType($entityType);\n        return $queries->visibleForContent();\n    }\n\n    protected function getQueriesForType(string $type): ProvidesEntityQueries\n    {\n        $queries = match ($type) {\n            'page' => $this->pages,\n            'chapter' => $this->chapters,\n            'book' => $this->books,\n            'bookshelf' => $this->shelves,\n            default => null,\n        };\n\n        if (is_null($queries)) {\n            throw new InvalidArgumentException(\"No entity query class configured for {$type}\");\n        }\n\n        return $queries;\n    }\n}\n"
  },
  {
    "path": "app/Entities/Queries/PageQueries.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Queries;\n\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Exceptions\\NotFoundException;\nuse Illuminate\\Database\\Eloquent\\Builder;\n\n/**\n * @implements ProvidesEntityQueries<Page>\n */\nclass PageQueries implements ProvidesEntityQueries\n{\n    protected static array $contentAttributes = [\n        'name', 'id', 'slug', 'book_id', 'chapter_id', 'draft',\n        'template', 'html', 'markdown', 'text', 'created_at', 'updated_at', 'priority',\n        'created_by', 'updated_by', 'owned_by',\n    ];\n    protected static array $listAttributes = [\n        'name', 'id', 'slug', 'book_id', 'chapter_id', 'draft',\n        'template', 'text', 'created_at', 'updated_at', 'priority', 'owned_by',\n    ];\n\n    /**\n     * @return Builder<Page>\n     */\n    public function start(): Builder\n    {\n        return Page::query();\n    }\n\n    public function findVisibleById(int $id): ?Page\n    {\n        return $this->start()->scopes('visible')->find($id);\n    }\n\n    public function findVisibleByIdOrFail(int $id): Page\n    {\n        $page = $this->findVisibleById($id);\n\n        if (is_null($page)) {\n            throw new NotFoundException(trans('errors.page_not_found'));\n        }\n\n        return $page;\n    }\n\n    public function findVisibleBySlugsOrFail(string $bookSlug, string $pageSlug): Page\n    {\n        /** @var ?Page $page */\n        $page = $this->start()->with('book')\n            ->scopes('visible')\n            ->whereHas('book', function (Builder $query) use ($bookSlug) {\n                $query->where('slug', '=', $bookSlug);\n            })\n            ->where('slug', '=', $pageSlug)\n            ->first();\n\n        if (is_null($page)) {\n            throw new NotFoundException(trans('errors.page_not_found'));\n        }\n\n        return $page;\n    }\n\n    public function usingSlugs(string $bookSlug, string $pageSlug): Builder\n    {\n        return $this->start()\n            ->where('slug', '=', $pageSlug)\n            ->whereHas('book', function (Builder $query) use ($bookSlug) {\n                $query->where('slug', '=', $bookSlug);\n            });\n    }\n\n    /**\n     * @return Builder<Page>\n     */\n    public function visibleForList(): Builder\n    {\n        return $this->start()\n            ->scopes('visible')\n            ->select($this->mergeBookSlugForSelect(static::$listAttributes));\n    }\n\n    /**\n     * @return Builder<Page>\n     */\n    public function visibleForContent(): Builder\n    {\n        return $this->start()->scopes('visible');\n    }\n\n    public function visibleForChapterList(int $chapterId): Builder\n    {\n        return $this->visibleForList()\n            ->where('chapter_id', '=', $chapterId)\n            ->orderBy('draft', 'desc')\n            ->orderBy('priority', 'asc');\n    }\n\n    public function visibleWithContents(): Builder\n    {\n        return $this->start()\n            ->scopes('visible')\n            ->select($this->mergeBookSlugForSelect(static::$contentAttributes));\n    }\n\n    public function currentUserDraftsForList(): Builder\n    {\n        return $this->visibleForList()\n            ->where('draft', '=', true)\n            ->where('created_by', '=', user()->id);\n    }\n\n    public function visibleTemplates(bool $includeContents = false): Builder\n    {\n        $base = $includeContents ? $this->visibleWithContents() : $this->visibleForList();\n        return $base->where('template', '=', true);\n    }\n\n    protected function mergeBookSlugForSelect(array $columns): array\n    {\n        return array_merge($columns, ['book_slug' => function ($builder) {\n            $builder->select('slug')\n                ->from('entities as books')\n                ->where('type', '=', 'book')\n                ->whereColumn('books.id', '=', 'entities.book_id');\n        }]);\n    }\n}\n"
  },
  {
    "path": "app/Entities/Queries/PageRevisionQueries.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Queries;\n\nuse BookStack\\Entities\\Models\\PageRevision;\nuse Illuminate\\Database\\Eloquent\\Builder;\n\nclass PageRevisionQueries\n{\n    public function start(): Builder\n    {\n        return PageRevision::query();\n    }\n\n    public function findLatestVersionBySlugs(string $bookSlug, string $pageSlug): ?PageRevision\n    {\n        return PageRevision::query()\n            ->whereHas('page', function (Builder $query) {\n                $query->scopes('visible');\n            })\n            ->where('slug', '=', $pageSlug)\n            ->where('type', '=', 'version')\n            ->where('book_slug', '=', $bookSlug)\n            ->orderBy('created_at', 'desc')\n            ->first();\n    }\n\n    public function findLatestCurrentUserDraftsForPageId(int $pageId): ?PageRevision\n    {\n        /** @var ?PageRevision $revision */\n        $revision = $this->latestCurrentUserDraftsForPageId($pageId)->first();\n\n        return $revision;\n    }\n\n    public function latestCurrentUserDraftsForPageId(int $pageId): Builder\n    {\n        return $this->start()\n            ->where('created_by', '=', user()->id)\n            ->where('type', 'update_draft')\n            ->where('page_id', '=', $pageId)\n            ->orderBy('created_at', 'desc');\n    }\n}\n"
  },
  {
    "path": "app/Entities/Queries/ProvidesEntityQueries.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Queries;\n\nuse BookStack\\Entities\\Models\\Entity;\nuse Illuminate\\Database\\Eloquent\\Builder;\n\n/**\n * Interface for our classes which provide common queries for our\n * entity objects. Ideally, all queries for entities should run through\n * these classes.\n * Any added methods should return a builder instances to allow extension\n * via building on the query, unless the method starts with 'find'\n * in which case an entity object should be returned.\n * (nullable unless it's a *OrFail method).\n *\n * @template TModel of Entity\n */\ninterface ProvidesEntityQueries\n{\n    /**\n     * Start a new query for this entity type.\n     * @return Builder<TModel>\n     */\n    public function start(): Builder;\n\n    /**\n     * Find the entity of the given ID or return null if not found.\n     */\n    public function findVisibleById(int $id): ?Entity;\n\n    /**\n     * Start a query for items that are visible, with selection\n     * configured for list display of this item.\n     * @return Builder<TModel>\n     */\n    public function visibleForList(): Builder;\n\n    /**\n     * Start a query for items that are visible, with selection\n     * configured for using the content of the items found.\n     * @return Builder<TModel>\n     */\n    public function visibleForContent(): Builder;\n}\n"
  },
  {
    "path": "app/Entities/Queries/QueryPopular.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Queries;\n\nuse BookStack\\Activity\\Models\\View;\nuse BookStack\\Entities\\EntityProvider;\nuse BookStack\\Entities\\Tools\\MixedEntityListLoader;\nuse BookStack\\Permissions\\PermissionApplicator;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\n\nclass QueryPopular\n{\n    public function __construct(\n        protected PermissionApplicator $permissions,\n        protected EntityProvider $entityProvider,\n        protected MixedEntityListLoader $listLoader,\n    ) {\n    }\n\n    public function run(int $count, int $page, array $filterModels): Collection\n    {\n        $query = $this->permissions\n            ->restrictEntityRelationQuery(View::query(), 'views', 'viewable_id', 'viewable_type')\n            ->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))\n            ->groupBy('viewable_id', 'viewable_type')\n            ->orderBy('view_count', 'desc');\n\n        if (!empty($filterModels)) {\n            $query->whereIn('viewable_type', $this->entityProvider->getMorphClasses($filterModels));\n        }\n\n        $views = $query\n            ->skip($count * ($page - 1))\n            ->take($count)\n            ->get();\n\n        $this->listLoader->loadIntoRelations($views->all(), 'viewable', true);\n\n        return $views->pluck('viewable')->filter();\n    }\n}\n"
  },
  {
    "path": "app/Entities/Queries/QueryRecentlyViewed.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Queries;\n\nuse BookStack\\Activity\\Models\\View;\nuse BookStack\\Entities\\Tools\\MixedEntityListLoader;\nuse BookStack\\Permissions\\PermissionApplicator;\nuse Illuminate\\Support\\Collection;\n\nclass QueryRecentlyViewed\n{\n    public function __construct(\n        protected PermissionApplicator $permissions,\n        protected MixedEntityListLoader $listLoader,\n    ) {\n    }\n\n    public function run(int $count, int $page): Collection\n    {\n        $user = user();\n        if ($user->isGuest()) {\n            return collect();\n        }\n\n        $query = $this->permissions->restrictEntityRelationQuery(\n            View::query(),\n            'views',\n            'viewable_id',\n            'viewable_type'\n        )\n            ->orderBy('views.updated_at', 'desc')\n            ->where('user_id', '=', user()->id);\n\n        $views = $query\n            ->skip(($page - 1) * $count)\n            ->take($count)\n            ->get();\n\n        $this->listLoader->loadIntoRelations($views->all(), 'viewable', false);\n\n        return $views->pluck('viewable')->filter();\n    }\n}\n"
  },
  {
    "path": "app/Entities/Queries/QueryTopFavourites.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Queries;\n\nuse BookStack\\Activity\\Models\\Favourite;\nuse BookStack\\Entities\\Tools\\MixedEntityListLoader;\nuse BookStack\\Permissions\\PermissionApplicator;\nuse Illuminate\\Database\\Query\\JoinClause;\n\nclass QueryTopFavourites\n{\n    public function __construct(\n        protected PermissionApplicator $permissions,\n        protected MixedEntityListLoader $listLoader,\n    ) {\n    }\n\n    public function run(int $count, int $skip = 0)\n    {\n        $user = user();\n        if ($user->isGuest()) {\n            return collect();\n        }\n\n        $query = $this->permissions\n            ->restrictEntityRelationQuery(Favourite::query(), 'favourites', 'favouritable_id', 'favouritable_type')\n            ->select('favourites.*')\n            ->leftJoin('views', function (JoinClause $join) {\n                $join->on('favourites.favouritable_id', '=', 'views.viewable_id');\n                $join->on('favourites.favouritable_type', '=', 'views.viewable_type');\n                $join->where('views.user_id', '=', user()->id);\n            })\n            ->orderBy('views.views', 'desc')\n            ->where('favourites.user_id', '=', user()->id);\n\n        $favourites = $query\n            ->skip($skip)\n            ->take($count)\n            ->get();\n\n        $this->listLoader->loadIntoRelations($favourites->all(), 'favouritable', false);\n\n        return $favourites->pluck('favouritable')->filter();\n    }\n}\n"
  },
  {
    "path": "app/Entities/Repos/BaseRepo.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Repos;\n\nuse BookStack\\Activity\\TagRepo;\nuse BookStack\\Entities\\Models\\BookChild;\nuse BookStack\\Entities\\Models\\HasCoverInterface;\nuse BookStack\\Entities\\Models\\HasDescriptionInterface;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Queries\\PageQueries;\nuse BookStack\\Entities\\Tools\\SlugGenerator;\nuse BookStack\\Entities\\Tools\\SlugHistory;\nuse BookStack\\Exceptions\\ImageUploadException;\nuse BookStack\\References\\ReferenceStore;\nuse BookStack\\References\\ReferenceUpdater;\nuse BookStack\\Sorting\\BookSorter;\nuse BookStack\\Uploads\\ImageRepo;\nuse BookStack\\Util\\HtmlDescriptionFilter;\nuse Illuminate\\Http\\UploadedFile;\n\nclass BaseRepo\n{\n    public function __construct(\n        protected TagRepo $tagRepo,\n        protected ImageRepo $imageRepo,\n        protected ReferenceUpdater $referenceUpdater,\n        protected ReferenceStore $referenceStore,\n        protected PageQueries $pageQueries,\n        protected BookSorter $bookSorter,\n        protected SlugGenerator $slugGenerator,\n        protected SlugHistory $slugHistory,\n    ) {\n    }\n\n    /**\n     * Create a new entity in the system.\n     * @template T of Entity\n     * @param T $entity\n     * @return T\n     */\n    public function create(Entity $entity, array $input): Entity\n    {\n        $entity = (clone $entity)->refresh();\n        $entity->fill($input);\n        $entity->forceFill([\n            'created_by' => user()->id,\n            'updated_by' => user()->id,\n            'owned_by'   => user()->id,\n        ]);\n        $this->refreshSlug($entity);\n\n        if ($entity instanceof HasDescriptionInterface) {\n            $this->updateDescription($entity, $input);\n        }\n\n        $entity->save();\n\n        if (isset($input['tags'])) {\n            $this->tagRepo->saveTagsToEntity($entity, $input['tags']);\n        }\n\n        $entity->refresh();\n        $entity->rebuildPermissions();\n        $entity->indexForSearch();\n\n        $this->referenceStore->updateForEntity($entity);\n\n        return $entity;\n    }\n\n    /**\n     * Update the given entity.\n     * @template T of Entity\n     * @param T $entity\n     * @return T\n     */\n    public function update(Entity $entity, array $input): Entity\n    {\n        $oldUrl = $entity->getUrl();\n\n        $entity->fill($input);\n        $entity->updated_by = user()->id;\n\n        if ($entity->isDirty('name') || empty($entity->slug)) {\n            $this->refreshSlug($entity);\n        }\n\n        if ($entity instanceof HasDescriptionInterface) {\n            $this->updateDescription($entity, $input);\n        }\n\n        $entity->save();\n\n        if (isset($input['tags'])) {\n            $this->tagRepo->saveTagsToEntity($entity, $input['tags']);\n            $entity->touch();\n        }\n\n        $entity->indexForSearch();\n        $this->referenceStore->updateForEntity($entity);\n\n        if ($oldUrl !== $entity->getUrl()) {\n            $this->referenceUpdater->updateEntityReferences($entity, $oldUrl);\n        }\n\n        return $entity;\n    }\n\n    /**\n     * Update the given items' cover image or clear it.\n     *\n     * @throws ImageUploadException\n     * @throws \\Exception\n     */\n    public function updateCoverImage(Entity&HasCoverInterface $entity, ?UploadedFile $coverImage, bool $removeImage = false): void\n    {\n        if ($coverImage) {\n            $imageType = 'cover_' . $entity->type;\n            $this->imageRepo->destroyImage($entity->coverInfo()->getImage());\n            $image = $this->imageRepo->saveNew($coverImage, $imageType, $entity->id, 512, 512, true);\n            $entity->coverInfo()->setImage($image);\n            $entity->save();\n        }\n\n        if ($removeImage) {\n            $this->imageRepo->destroyImage($entity->coverInfo()->getImage());\n            $entity->coverInfo()->setImage(null);\n            $entity->save();\n        }\n    }\n\n    /**\n     * Sort the parent of the given entity if any auto sort actions are set for it.\n     * Typically ran during create/update/insert events.\n     */\n    public function sortParent(Entity $entity): void\n    {\n        if ($entity instanceof BookChild) {\n            $book = $entity->book;\n            $this->bookSorter->runBookAutoSort($book);\n        }\n    }\n\n    /**\n     * Update the description of the given entity from input data.\n     */\n    protected function updateDescription(Entity $entity, array $input): void\n    {\n        if (!$entity instanceof HasDescriptionInterface) {\n            return;\n        }\n\n        if (isset($input['description_html'])) {\n            $entity->descriptionInfo()->set(\n                HtmlDescriptionFilter::filterFromString($input['description_html']),\n                html_entity_decode(strip_tags($input['description_html']))\n            );\n        } else if (isset($input['description'])) {\n            $entity->descriptionInfo()->set('', $input['description']);\n        }\n    }\n\n    /**\n     * Refresh the slug for the given entity.\n     */\n    public function refreshSlug(Entity $entity): void\n    {\n        $this->slugHistory->recordForEntity($entity);\n        $this->slugGenerator->regenerateForEntity($entity);\n    }\n}\n"
  },
  {
    "path": "app/Entities/Repos/BookRepo.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Repos;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Activity\\TagRepo;\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Tools\\TrashCan;\nuse BookStack\\Exceptions\\ImageUploadException;\nuse BookStack\\Facades\\Activity;\nuse BookStack\\Sorting\\SortRule;\nuse BookStack\\Uploads\\ImageRepo;\nuse BookStack\\Util\\DatabaseTransaction;\nuse Exception;\nuse Illuminate\\Http\\UploadedFile;\n\nclass BookRepo\n{\n    public function __construct(\n        protected BaseRepo $baseRepo,\n        protected TagRepo $tagRepo,\n        protected ImageRepo $imageRepo,\n        protected TrashCan $trashCan,\n    ) {\n    }\n\n    /**\n     * Create a new book in the system.\n     */\n    public function create(array $input): Book\n    {\n        return (new DatabaseTransaction(function () use ($input) {\n            $book = $this->baseRepo->create(new Book(), $input);\n            $this->baseRepo->updateCoverImage($book, $input['image'] ?? null);\n            $book->defaultTemplate()->setFromId(intval($input['default_template_id'] ?? null));\n            Activity::add(ActivityType::BOOK_CREATE, $book);\n\n            $defaultBookSortSetting = intval(setting('sorting-book-default', '0'));\n            if ($defaultBookSortSetting && SortRule::query()->find($defaultBookSortSetting)) {\n                $book->sort_rule_id = $defaultBookSortSetting;\n            }\n\n            $book->save();\n\n            return $book;\n        }))->run();\n    }\n\n    /**\n     * Update the given book.\n     */\n    public function update(Book $book, array $input): Book\n    {\n        $book = $this->baseRepo->update($book, $input);\n\n        if (array_key_exists('default_template_id', $input)) {\n            $book->defaultTemplate()->setFromId(intval($input['default_template_id']));\n        }\n\n        if (array_key_exists('image', $input)) {\n            $this->baseRepo->updateCoverImage($book, $input['image'], $input['image'] === null);\n        }\n\n        $book->save();\n        Activity::add(ActivityType::BOOK_UPDATE, $book);\n\n        return $book;\n    }\n\n    /**\n     * Update the given book's cover image or clear it.\n     *\n     * @throws ImageUploadException\n     * @throws Exception\n     */\n    public function updateCoverImage(Book $book, ?UploadedFile $coverImage, bool $removeImage = false): void\n    {\n        $this->baseRepo->updateCoverImage($book, $coverImage, $removeImage);\n    }\n\n    /**\n     * Remove a book from the system.\n     *\n     * @throws Exception\n     */\n    public function destroy(Book $book): void\n    {\n        $this->trashCan->softDestroyBook($book);\n        Activity::add(ActivityType::BOOK_DELETE, $book);\n\n        $this->trashCan->autoClearOld();\n    }\n}\n"
  },
  {
    "path": "app/Entities/Repos/BookshelfRepo.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Repos;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Entities\\Queries\\BookQueries;\nuse BookStack\\Entities\\Tools\\TrashCan;\nuse BookStack\\Facades\\Activity;\nuse BookStack\\Util\\DatabaseTransaction;\nuse Exception;\n\nclass BookshelfRepo\n{\n    public function __construct(\n        protected BaseRepo $baseRepo,\n        protected BookQueries $bookQueries,\n        protected TrashCan $trashCan,\n    ) {\n    }\n\n    /**\n     * Create a new shelf in the system.\n     */\n    public function create(array $input, array $bookIds): Bookshelf\n    {\n        return (new DatabaseTransaction(function () use ($input, $bookIds) {\n            $shelf = $this->baseRepo->create(new Bookshelf(), $input);\n            $this->baseRepo->updateCoverImage($shelf, $input['image'] ?? null);\n            $this->updateBooks($shelf, $bookIds);\n            Activity::add(ActivityType::BOOKSHELF_CREATE, $shelf);\n            return $shelf;\n        }))->run();\n    }\n\n    /**\n     * Update an existing shelf in the system using the given input.\n     */\n    public function update(Bookshelf $shelf, array $input, ?array $bookIds): Bookshelf\n    {\n        $shelf = $this->baseRepo->update($shelf, $input);\n\n        if (!is_null($bookIds)) {\n            $this->updateBooks($shelf, $bookIds);\n        }\n\n        if (array_key_exists('image', $input)) {\n            $this->baseRepo->updateCoverImage($shelf, $input['image'], $input['image'] === null);\n        }\n\n        Activity::add(ActivityType::BOOKSHELF_UPDATE, $shelf);\n\n        return $shelf;\n    }\n\n    /**\n     * Update which books are assigned to this shelf by syncing the given book ids.\n     * Function ensures the managed books are visible to the current user and existing,\n     * and that the user does not alter the assignment of books that are not visible to them.\n     */\n    protected function updateBooks(Bookshelf $shelf, array $bookIds): void\n    {\n        $numericIDs = collect($bookIds)->map(function ($id) {\n            return intval($id);\n        });\n\n        $existingBookIds = $shelf->books()->pluck('id')->toArray();\n        $visibleExistingBookIds = $this->bookQueries->visibleForList()\n            ->whereIn('id', $existingBookIds)\n            ->pluck('id')\n            ->toArray();\n        $nonVisibleExistingBookIds = array_values(array_diff($existingBookIds, $visibleExistingBookIds));\n\n        $newIdsToAssign = $this->bookQueries->visibleForList()\n            ->whereIn('id', $bookIds)\n            ->pluck('id')\n            ->toArray();\n\n        $maxNewIndex = max($numericIDs->keys()->toArray() ?: [0]);\n\n        $syncData = [];\n        foreach ($newIdsToAssign as $id) {\n            $syncData[$id] = ['order' => $numericIDs->search($id)];\n        }\n\n        foreach ($nonVisibleExistingBookIds as $index => $id) {\n            $syncData[$id] = ['order' => $maxNewIndex + ($index + 1)];\n        }\n\n        $shelf->books()->sync($syncData);\n    }\n\n    /**\n     * Remove a bookshelf from the system.\n     *\n     * @throws Exception\n     */\n    public function destroy(Bookshelf $shelf): void\n    {\n        $this->trashCan->softDestroyShelf($shelf);\n        Activity::add(ActivityType::BOOKSHELF_DELETE, $shelf);\n        $this->trashCan->autoClearOld();\n    }\n}\n"
  },
  {
    "path": "app/Entities/Repos/ChapterRepo.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Repos;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse BookStack\\Entities\\Tools\\BookContents;\nuse BookStack\\Entities\\Tools\\ParentChanger;\nuse BookStack\\Entities\\Tools\\TrashCan;\nuse BookStack\\Exceptions\\MoveOperationException;\nuse BookStack\\Exceptions\\PermissionsException;\nuse BookStack\\Facades\\Activity;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Util\\DatabaseTransaction;\nuse Exception;\n\nclass ChapterRepo\n{\n    public function __construct(\n        protected BaseRepo $baseRepo,\n        protected EntityQueries $entityQueries,\n        protected TrashCan $trashCan,\n        protected ParentChanger $parentChanger,\n    ) {\n    }\n\n    /**\n     * Create a new chapter in the system.\n     */\n    public function create(array $input, Book $parentBook): Chapter\n    {\n        return (new DatabaseTransaction(function () use ($input, $parentBook) {\n            $chapter = new Chapter();\n            $chapter->book_id = $parentBook->id;\n            $chapter->priority = (new BookContents($parentBook))->getLastPriority() + 1;\n\n            $chapter = $this->baseRepo->create($chapter, $input);\n            $chapter->defaultTemplate()->setFromId(intval($input['default_template_id'] ?? null));\n\n            $chapter->save();\n            Activity::add(ActivityType::CHAPTER_CREATE, $chapter);\n\n            $this->baseRepo->sortParent($chapter);\n\n            return $chapter;\n        }))->run();\n    }\n\n    /**\n     * Update the given chapter.\n     */\n    public function update(Chapter $chapter, array $input): Chapter\n    {\n        $chapter = $this->baseRepo->update($chapter, $input);\n\n        if (array_key_exists('default_template_id', $input)) {\n            $chapter->defaultTemplate()->setFromId(intval($input['default_template_id']));\n        }\n\n        $chapter->save();\n        Activity::add(ActivityType::CHAPTER_UPDATE, $chapter);\n\n        $this->baseRepo->sortParent($chapter);\n\n        return $chapter;\n    }\n\n    /**\n     * Remove a chapter from the system.\n     *\n     * @throws Exception\n     */\n    public function destroy(Chapter $chapter): void\n    {\n        $this->trashCan->softDestroyChapter($chapter);\n        Activity::add(ActivityType::CHAPTER_DELETE, $chapter);\n        $this->trashCan->autoClearOld();\n    }\n\n    /**\n     * Move the given chapter into a new parent book.\n     * The $parentIdentifier must be a string of the following format:\n     * 'book:<id>' (book:5).\n     *\n     * @throws MoveOperationException\n     * @throws PermissionsException\n     */\n    public function move(Chapter $chapter, string $parentIdentifier): Book\n    {\n        $parent = $this->entityQueries->findVisibleByStringIdentifier($parentIdentifier);\n        if (!$parent instanceof Book) {\n            throw new MoveOperationException('Book to move chapter into not found');\n        }\n\n        if (!userCan(Permission::ChapterCreate, $parent)) {\n            throw new PermissionsException('User does not have permission to create a chapter within the chosen book');\n        }\n\n        return (new DatabaseTransaction(function () use ($chapter, $parent) {\n            $this->parentChanger->changeBook($chapter, $parent->id);\n            $chapter->rebuildPermissions();\n            Activity::add(ActivityType::CHAPTER_MOVE, $chapter);\n\n            $this->baseRepo->sortParent($chapter);\n\n            return $parent;\n        }))->run();\n    }\n}\n"
  },
  {
    "path": "app/Entities/Repos/DeletionRepo.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Repos;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Entities\\Models\\Deletion;\nuse BookStack\\Entities\\Tools\\TrashCan;\nuse BookStack\\Facades\\Activity;\n\nclass DeletionRepo\n{\n    public function __construct(\n        protected TrashCan $trashCan\n    ) {\n    }\n\n    public function restore(int $id): int\n    {\n        /** @var Deletion $deletion */\n        $deletion = Deletion::query()->findOrFail($id);\n        Activity::add(ActivityType::RECYCLE_BIN_RESTORE, $deletion);\n\n        return $this->trashCan->restoreFromDeletion($deletion);\n    }\n\n    public function destroy(int $id): int\n    {\n        /** @var Deletion $deletion */\n        $deletion = Deletion::query()->findOrFail($id);\n        Activity::add(ActivityType::RECYCLE_BIN_DESTROY, $deletion);\n\n        return $this->trashCan->destroyFromDeletion($deletion);\n    }\n}\n"
  },
  {
    "path": "app/Entities/Repos/PageRepo.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Repos;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Models\\PageRevision;\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse BookStack\\Entities\\Tools\\BookContents;\nuse BookStack\\Entities\\Tools\\PageContent;\nuse BookStack\\Entities\\Tools\\PageEditorType;\nuse BookStack\\Entities\\Tools\\ParentChanger;\nuse BookStack\\Entities\\Tools\\TrashCan;\nuse BookStack\\Exceptions\\MoveOperationException;\nuse BookStack\\Exceptions\\PermissionsException;\nuse BookStack\\Facades\\Activity;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\References\\ReferenceStore;\nuse BookStack\\References\\ReferenceUpdater;\nuse BookStack\\Util\\DatabaseTransaction;\nuse Exception;\n\nclass PageRepo\n{\n    public function __construct(\n        protected BaseRepo $baseRepo,\n        protected RevisionRepo $revisionRepo,\n        protected EntityQueries $entityQueries,\n        protected ReferenceStore $referenceStore,\n        protected ReferenceUpdater $referenceUpdater,\n        protected TrashCan $trashCan,\n        protected ParentChanger $parentChanger,\n    ) {\n    }\n\n    /**\n     * Get a new draft page belonging to the given parent entity.\n     */\n    public function getNewDraftPage(Entity $parent): Page\n    {\n        $page = (new Page())->forceFill([\n            'name'       => trans('entities.pages_initial_name'),\n            'created_by' => user()->id,\n            'owned_by'   => user()->id,\n            'updated_by' => user()->id,\n            'draft'      => true,\n            'editor'     => PageEditorType::getSystemDefault()->value,\n            'html'       => '',\n            'markdown'   => '',\n            'text'       => '',\n        ]);\n\n        if ($parent instanceof Chapter) {\n            $page->chapter_id = $parent->id;\n            $page->book_id = $parent->book_id;\n        } else {\n            $page->book_id = $parent->id;\n        }\n\n        $defaultTemplate = $page->chapter?->defaultTemplate()->get() ?? $page->book?->defaultTemplate()->get();\n        if ($defaultTemplate) {\n            $page->forceFill([\n                'html'  => $defaultTemplate->html,\n                'markdown' => $defaultTemplate->markdown,\n            ]);\n            $page->text = (new PageContent($page))->toPlainText();\n        }\n\n        (new DatabaseTransaction(function () use ($page) {\n            $page->save();\n            $page->rebuildPermissions();\n        }))->run();\n\n        return $page;\n    }\n\n    /**\n     * Publish a draft page to make it a live, non-draft page.\n     */\n    public function publishDraft(Page $draft, array $input): Page\n    {\n        return (new DatabaseTransaction(function () use ($draft, $input) {\n            $draft->draft = false;\n            $draft->revision_count = 1;\n            $draft->priority = $this->getNewPriority($draft);\n            $this->updateTemplateStatusAndContentFromInput($draft, $input);\n\n            $draft = $this->baseRepo->update($draft, $input);\n            $draft->rebuildPermissions();\n\n            $summary = trim($input['summary'] ?? '') ?: trans('entities.pages_initial_revision');\n            $this->revisionRepo->storeNewForPage($draft, $summary);\n            $draft->refresh();\n\n            Activity::add(ActivityType::PAGE_CREATE, $draft);\n            $this->baseRepo->sortParent($draft);\n\n            return $draft;\n        }))->run();\n    }\n\n    /**\n     * Directly update the content for the given page from the provided input.\n     * Used for direct content access in a way that performs required changes\n     * (Search index and reference regen) without performing an official update.\n     */\n    public function setContentFromInput(Page $page, array $input): void\n    {\n        $this->updateTemplateStatusAndContentFromInput($page, $input);\n        $this->baseRepo->update($page, []);\n    }\n\n    /**\n     * Update a page in the system.\n     */\n    public function update(Page $page, array $input): Page\n    {\n        // Hold the old details to compare later\n        $oldName = $page->name;\n        $oldHtml = $page->html;\n        $oldMarkdown = $page->markdown;\n\n        $this->updateTemplateStatusAndContentFromInput($page, $input);\n        $page = $this->baseRepo->update($page, $input);\n\n        // Update with new details\n        $page->revision_count++;\n        $page->save();\n\n        // Remove all update drafts for this user and page.\n        $this->revisionRepo->deleteDraftsForCurrentUser($page);\n\n        // Save a revision after updating\n        $summary = trim($input['summary'] ?? '');\n        $htmlChanged = isset($input['html']) && $input['html'] !== $oldHtml;\n        $nameChanged = isset($input['name']) && $input['name'] !== $oldName;\n        $markdownChanged = isset($input['markdown']) && $input['markdown'] !== $oldMarkdown;\n        if ($htmlChanged || $nameChanged || $markdownChanged || $summary) {\n            $this->revisionRepo->storeNewForPage($page, $summary);\n        }\n\n        Activity::add(ActivityType::PAGE_UPDATE, $page);\n        $this->baseRepo->sortParent($page);\n\n        return $page;\n    }\n\n    protected function updateTemplateStatusAndContentFromInput(Page $page, array $input): void\n    {\n        if (isset($input['template']) && userCan(Permission::TemplatesManage)) {\n            $page->template = ($input['template'] === 'true');\n        }\n\n        $pageContent = new PageContent($page);\n        $defaultEditor = PageEditorType::getSystemDefault();\n        $currentEditor = PageEditorType::forPage($page) ?: $defaultEditor;\n        $inputEditor = PageEditorType::fromRequestValue($input['editor'] ?? '') ?? $currentEditor;\n        $newEditor = $currentEditor;\n\n        $haveInput = isset($input['markdown']) || isset($input['html']);\n        $inputEmpty = empty($input['markdown']) && empty($input['html']);\n\n        if ($haveInput && $inputEmpty) {\n            $pageContent->setNewHTML('', user());\n        } elseif (!empty($input['markdown']) && is_string($input['markdown'])) {\n            $newEditor = PageEditorType::Markdown;\n            $pageContent->setNewMarkdown($input['markdown'], user());\n        } elseif (isset($input['html'])) {\n            $newEditor = ($inputEditor->isHtmlBased() ? $inputEditor : null) ?? ($defaultEditor->isHtmlBased() ? $defaultEditor : null) ?? PageEditorType::WysiwygTinymce;\n            $pageContent->setNewHTML($input['html'], user());\n        }\n\n        if (($newEditor !== $currentEditor || empty($page->editor)) && userCan(Permission::EditorChange)) {\n            $page->editor = $newEditor->value;\n        } elseif (empty($page->editor)) {\n            $page->editor = $defaultEditor->value;\n        }\n    }\n\n    /**\n     * Save a page update draft.\n     */\n    public function updatePageDraft(Page $page, array $input): Page|PageRevision\n    {\n        // If the page itself is a draft, simply update that\n        if ($page->draft) {\n            $this->updateTemplateStatusAndContentFromInput($page, $input);\n            $page->forceFill(array_intersect_key($input, array_flip(['name'])))->save();\n            $page->save();\n\n            return $page;\n        }\n\n        // Otherwise, save the data to a revision\n        $draft = $this->revisionRepo->getNewDraftForCurrentUser($page);\n        $draft->fill($input);\n\n        if (!empty($input['markdown'])) {\n            $draft->markdown = $input['markdown'];\n            $draft->html = '';\n        } else {\n            $draft->html = $input['html'];\n            $draft->markdown = '';\n        }\n\n        $draft->save();\n\n        return $draft;\n    }\n\n    /**\n     * Destroy a page from the system.\n     *\n     * @throws Exception\n     */\n    public function destroy(Page $page): void\n    {\n        $this->trashCan->softDestroyPage($page);\n        Activity::add(ActivityType::PAGE_DELETE, $page);\n        $this->trashCan->autoClearOld();\n    }\n\n    /**\n     * Restores a revision's content back into a page.\n     */\n    public function restoreRevision(Page $page, int $revisionId): Page\n    {\n        $oldUrl = $page->getUrl();\n        $page->revision_count++;\n\n        /** @var PageRevision $revision */\n        $revision = $page->revisions()->where('id', '=', $revisionId)->first();\n\n        $page->fill($revision->toArray());\n        $content = new PageContent($page);\n\n        if (!empty($revision->markdown)) {\n            $content->setNewMarkdown($revision->markdown, user());\n        } else {\n            $content->setNewHTML($revision->html, user());\n        }\n\n        $page->updated_by = user()->id;\n        $this->baseRepo->refreshSlug($page);\n        $page->save();\n        $page->indexForSearch();\n        $this->referenceStore->updateForEntity($page);\n\n        $summary = trans('entities.pages_revision_restored_from', ['id' => strval($revisionId), 'summary' => $revision->summary]);\n        $this->revisionRepo->storeNewForPage($page, $summary);\n\n        if ($oldUrl !== $page->getUrl()) {\n            $this->referenceUpdater->updateEntityReferences($page, $oldUrl);\n        }\n\n        Activity::add(ActivityType::PAGE_RESTORE, $page);\n        Activity::add(ActivityType::REVISION_RESTORE, $revision);\n\n        $this->baseRepo->sortParent($page);\n\n        return $page;\n    }\n\n    /**\n     * Move the given page into a new parent book or chapter.\n     * The $parentIdentifier must be a string of the following format:\n     * 'book:<id>' (book:5).\n     *\n     * @throws MoveOperationException\n     * @throws PermissionsException\n     */\n    public function move(Page $page, string $parentIdentifier): Entity\n    {\n        $parent = $this->entityQueries->findVisibleByStringIdentifier($parentIdentifier);\n        if (!$parent instanceof Chapter && !$parent instanceof Book) {\n            throw new MoveOperationException('Book or chapter to move page into not found');\n        }\n\n        if (!userCan(Permission::PageCreate, $parent)) {\n            throw new PermissionsException('User does not have permission to create a page within the new parent');\n        }\n\n        return (new DatabaseTransaction(function () use ($page, $parent) {\n            $page->chapter_id = ($parent instanceof Chapter) ? $parent->id : null;\n            $newBookId = ($parent instanceof Chapter) ? $parent->book->id : $parent->id;\n            $this->parentChanger->changeBook($page, $newBookId);\n            $page->rebuildPermissions();\n\n            Activity::add(ActivityType::PAGE_MOVE, $page);\n\n            $this->baseRepo->sortParent($page);\n\n            return $parent;\n        }))->run();\n    }\n\n    /**\n     * Get a new priority for a page.\n     */\n    protected function getNewPriority(Page $page): int\n    {\n        $parent = $page->getParent();\n        if ($parent instanceof Chapter) {\n            /** @var ?Page $lastPage */\n            $lastPage = $parent->pages('desc')->first();\n\n            return $lastPage ? $lastPage->priority + 1 : 0;\n        }\n\n        return (new BookContents($page->book))->getLastPriority() + 1;\n    }\n}\n"
  },
  {
    "path": "app/Entities/Repos/RevisionRepo.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Repos;\n\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Models\\PageRevision;\nuse BookStack\\Entities\\Queries\\PageRevisionQueries;\n\nclass RevisionRepo\n{\n    public function __construct(\n        protected PageRevisionQueries $queries,\n    ) {\n    }\n\n    /**\n     * Delete all drafts revisions, for the given page, belonging to the current user.\n     */\n    public function deleteDraftsForCurrentUser(Page $page): void\n    {\n        $this->queries->latestCurrentUserDraftsForPageId($page->id)->delete();\n    }\n\n    /**\n     * Get a user update_draft page revision to update for the given page.\n     * Checks for an existing revision before providing a fresh one.\n     */\n    public function getNewDraftForCurrentUser(Page $page): PageRevision\n    {\n        $draft = $this->queries->findLatestCurrentUserDraftsForPageId($page->id);\n\n        if ($draft) {\n            return $draft;\n        }\n\n        $draft = new PageRevision();\n        $draft->page_id = $page->id;\n        $draft->slug = $page->slug;\n        $draft->book_slug = $page->book->slug;\n        $draft->created_by = user()->id;\n        $draft->type = 'update_draft';\n\n        return $draft;\n    }\n\n    /**\n     * Store a new revision in the system for the given page.\n     */\n    public function storeNewForPage(Page $page, ?string $summary = null): PageRevision\n    {\n        $revision = new PageRevision();\n\n        $revision->name = $page->name;\n        $revision->html = $page->html;\n        $revision->markdown = $page->markdown;\n        $revision->text = $page->text;\n        $revision->page_id = $page->id;\n        $revision->slug = $page->slug;\n        $revision->book_slug = $page->book->slug;\n        $revision->created_by = user()->id;\n        $revision->created_at = $page->updated_at;\n        $revision->type = 'version';\n        $revision->summary = $summary;\n        $revision->revision_number = $page->revision_count;\n        $revision->save();\n\n        $this->deleteOldRevisions($page);\n\n        return $revision;\n    }\n\n    /**\n     * Delete old revisions, for the given page, from the system.\n     */\n    protected function deleteOldRevisions(Page $page): void\n    {\n        $revisionLimit = config('app.revision_limit');\n        if ($revisionLimit === false) {\n            return;\n        }\n\n        $revisionsToDelete = PageRevision::query()\n            ->where('page_id', '=', $page->id)\n            ->orderBy('created_at', 'desc')\n            ->skip(intval($revisionLimit))\n            ->take(10)\n            ->get(['id']);\n\n        if ($revisionsToDelete->count() > 0) {\n            PageRevision::query()->whereIn('id', $revisionsToDelete->pluck('id'))->delete();\n        }\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/BookContents.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse Illuminate\\Support\\Collection;\n\nclass BookContents\n{\n    protected EntityQueries $queries;\n\n    public function __construct(\n        protected Book $book,\n    ) {\n        $this->queries = app()->make(EntityQueries::class);\n    }\n\n    /**\n     * Get the current priority of the last item at the top-level of the book.\n     */\n    public function getLastPriority(): int\n    {\n        $maxPage = $this->book->pages()\n            ->where('draft', '=', false)\n            ->whereDoesntHave('chapter')\n            ->max('priority');\n\n        $maxChapter = $this->book->chapters()\n            ->max('priority');\n\n        return max($maxChapter, $maxPage, 1);\n    }\n\n    /**\n     * Get the contents as a sorted collection tree.\n     */\n    public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection\n    {\n        $pages = $this->getPages($showDrafts, $renderPages);\n        $chapters = $this->book->chapters()->scopes('visible')->get();\n        $all = collect()->concat($pages)->concat($chapters);\n        $chapterMap = $chapters->keyBy('id');\n        $lonePages = collect();\n\n        $pages->groupBy('chapter_id')->each(function ($pages, $chapter_id) use ($chapterMap, &$lonePages) {\n            $chapter = $chapterMap->get($chapter_id);\n            if ($chapter) {\n                $chapter->setAttribute('visible_pages', collect($pages)->sortBy($this->bookChildSortFunc()));\n            } else {\n                $lonePages = $lonePages->concat($pages);\n            }\n        });\n\n        $chapters->whereNull('visible_pages')->each(function (Chapter $chapter) {\n            $chapter->setAttribute('visible_pages', collect([]));\n        });\n\n        $all->each(function (Entity $entity) use ($renderPages) {\n            $entity->setRelation('book', $this->book);\n\n            if ($renderPages && $entity instanceof Page) {\n                $entity->html = (new PageContent($entity))->render();\n            }\n        });\n\n        return collect($chapters)->concat($lonePages)->sortBy($this->bookChildSortFunc());\n    }\n\n    /**\n     * Function for providing a sorting score for an entity in relation to the\n     * other items within the book.\n     */\n    protected function bookChildSortFunc(): callable\n    {\n        return function (Entity $entity) {\n            if ($entity->getAttribute('draft') ?? false) {\n                return -100;\n            }\n\n            return $entity->getAttribute('priority') ?? 0;\n        };\n    }\n\n    /**\n     * Get the visible pages within this book.\n     */\n    protected function getPages(bool $showDrafts = false, bool $getPageContent = false): Collection\n    {\n        if ($getPageContent) {\n            $query = $this->queries->pages->visibleWithContents();\n        } else {\n            $query = $this->queries->pages->visibleForList();\n        }\n\n        if (!$showDrafts) {\n            $query->where('draft', '=', false);\n        }\n\n        return $query->where('book_id', '=', $this->book->id)->get();\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/Cloner.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools;\n\nuse BookStack\\Activity\\Models\\Tag;\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\HasCoverInterface;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Repos\\BookRepo;\nuse BookStack\\Entities\\Repos\\ChapterRepo;\nuse BookStack\\Entities\\Repos\\PageRepo;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\References\\ReferenceChangeContext;\nuse BookStack\\References\\ReferenceUpdater;\nuse BookStack\\Uploads\\Image;\nuse BookStack\\Uploads\\ImageService;\nuse Illuminate\\Http\\UploadedFile;\n\nclass Cloner\n{\n    protected ReferenceChangeContext $referenceChangeContext;\n\n    public function __construct(\n        protected PageRepo $pageRepo,\n        protected ChapterRepo $chapterRepo,\n        protected BookRepo $bookRepo,\n        protected ImageService $imageService,\n        protected ReferenceUpdater $referenceUpdater,\n    ) {\n        $this->referenceChangeContext = new ReferenceChangeContext();\n    }\n\n    /**\n     * Clone the given page into the given parent using the provided name.\n     */\n    public function clonePage(Page $original, Entity $parent, string $newName): Page\n    {\n        $context = $this->newReferenceChangeContext();\n        $page = $this->createPageClone($original, $parent, $newName);\n        $this->referenceUpdater->changeReferencesUsingContext($context);\n        return $page;\n    }\n\n    protected function createPageClone(Page $original, Entity $parent, string $newName): Page\n    {\n        $copyPage = $this->pageRepo->getNewDraftPage($parent);\n        $pageData = $this->entityToInputData($original);\n        $pageData['name'] = $newName;\n\n        $newPage = $this->pageRepo->publishDraft($copyPage, $pageData);\n        $this->referenceChangeContext->add($original, $newPage);\n\n        return $newPage;\n    }\n\n    /**\n     * Clone the given page into the given parent using the provided name.\n     * Clones all child pages.\n     */\n    public function cloneChapter(Chapter $original, Book $parent, string $newName): Chapter\n    {\n        $context = $this->newReferenceChangeContext();\n        $chapter = $this->createChapterClone($original, $parent, $newName);\n        $this->referenceUpdater->changeReferencesUsingContext($context);\n        return $chapter;\n    }\n\n    protected function createChapterClone(Chapter $original, Book $parent, string $newName): Chapter\n    {\n        $chapterDetails = $this->entityToInputData($original);\n        $chapterDetails['name'] = $newName;\n\n        $copyChapter = $this->chapterRepo->create($chapterDetails, $parent);\n\n        if (userCan(Permission::PageCreate, $copyChapter)) {\n            /** @var Page $page */\n            foreach ($original->getVisiblePages() as $page) {\n                $this->createPageClone($page, $copyChapter, $page->name);\n            }\n        }\n\n        $this->referenceChangeContext->add($original, $copyChapter);\n\n        return $copyChapter;\n    }\n\n    /**\n     * Clone the given book.\n     * Clones all child chapters and pages.\n     */\n    public function cloneBook(Book $original, string $newName): Book\n    {\n        $context = $this->newReferenceChangeContext();\n        $book = $this->createBookClone($original, $newName);\n        $this->referenceUpdater->changeReferencesUsingContext($context);\n        return $book;\n    }\n\n    protected function createBookClone(Book $original, string $newName): Book\n    {\n        $bookDetails = $this->entityToInputData($original);\n        $bookDetails['name'] = $newName;\n\n        // Clone book\n        $copyBook = $this->bookRepo->create($bookDetails);\n\n        // Clone contents\n        $directChildren = $original->getDirectVisibleChildren();\n        foreach ($directChildren as $child) {\n            if ($child instanceof Chapter && userCan(Permission::ChapterCreate, $copyBook)) {\n                $this->createChapterClone($child, $copyBook, $child->name);\n            }\n\n            if ($child instanceof Page && !$child->draft && userCan(Permission::PageCreate, $copyBook)) {\n                $this->createPageClone($child, $copyBook, $child->name);\n            }\n        }\n\n        // Clone bookshelf relationships\n        /** @var Bookshelf $shelf */\n        foreach ($original->shelves as $shelf) {\n            if (userCan(Permission::BookshelfUpdate, $shelf)) {\n                $shelf->appendBook($copyBook);\n            }\n        }\n\n        $this->referenceChangeContext->add($original, $copyBook);\n\n        return $copyBook;\n    }\n\n    /**\n     * Convert an entity to a raw data array of input data.\n     *\n     * @return array<string, mixed>\n     */\n    public function entityToInputData(Entity $entity): array\n    {\n        $inputData = $entity->getAttributes();\n        $inputData['tags'] = $this->entityTagsToInputArray($entity);\n\n        // Add a cover to the data if existing on the original entity\n        if ($entity instanceof HasCoverInterface) {\n            $cover = $entity->coverInfo()->getImage();\n            if ($cover) {\n                $inputData['image'] = $this->imageToUploadedFile($cover);\n            }\n        }\n\n        return $inputData;\n    }\n\n    /**\n     * Copy the permission settings from the source entity to the target entity.\n     */\n    public function copyEntityPermissions(Entity $sourceEntity, Entity $targetEntity): void\n    {\n        $permissions = $sourceEntity->permissions()->get(['role_id', 'view', 'create', 'update', 'delete'])->toArray();\n        $targetEntity->permissions()->delete();\n        $targetEntity->permissions()->createMany($permissions);\n        $targetEntity->rebuildPermissions();\n    }\n\n    /**\n     * Convert an image instance to an UploadedFile instance to mimic\n     * a file being uploaded.\n     */\n    protected function imageToUploadedFile(Image $image): ?UploadedFile\n    {\n        $imgData = $this->imageService->getImageData($image);\n        $tmpImgFilePath = tempnam(sys_get_temp_dir(), 'bs_cover_clone_');\n        file_put_contents($tmpImgFilePath, $imgData);\n\n        return new UploadedFile($tmpImgFilePath, basename($image->path));\n    }\n\n    /**\n     * Convert the tags on the given entity to the raw format\n     * that's used for incoming request data.\n     */\n    protected function entityTagsToInputArray(Entity $entity): array\n    {\n        $tags = [];\n\n        /** @var Tag $tag */\n        foreach ($entity->tags as $tag) {\n            $tags[] = ['name' => $tag->name, 'value' => $tag->value];\n        }\n\n        return $tags;\n    }\n\n    protected function newReferenceChangeContext(): ReferenceChangeContext\n    {\n        $this->referenceChangeContext = new ReferenceChangeContext();\n        return $this->referenceChangeContext;\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/EntityCover.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Uploads\\Image;\nuse Exception;\nuse Illuminate\\Database\\Eloquent\\Builder;\n\nclass EntityCover\n{\n    public function __construct(\n        protected Book|Bookshelf $entity,\n    ) {\n    }\n\n    protected function imageQuery(): Builder\n    {\n        return Image::query()->where('id', '=', $this->entity->image_id);\n    }\n\n    /**\n     * Check if a cover image exists for this entity.\n     */\n    public function exists(): bool\n    {\n        return $this->entity->image_id !== null && $this->imageQuery()->exists();\n    }\n\n    /**\n     * Get the assigned cover image model.\n     */\n    public function getImage(): Image|null\n    {\n        if ($this->entity->image_id === null) {\n            return null;\n        }\n\n        $cover = $this->imageQuery()->first();\n        if ($cover instanceof Image) {\n            return $cover;\n        }\n\n        return null;\n    }\n\n    /**\n     * Returns a cover image URL, or the given default if none assigned/existing.\n     */\n    public function getUrl(int $width = 440, int $height = 250, string|null $default = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='): string|null\n    {\n        if (!$this->entity->image_id) {\n            return $default;\n        }\n\n        try {\n            return $this->getImage()?->getThumb($width, $height, false) ?? $default;\n        } catch (Exception $err) {\n            return $default;\n        }\n    }\n\n    /**\n     * Set the image to use as the cover for this entity.\n     */\n    public function setImage(Image|null $image): void\n    {\n        if ($image === null) {\n            $this->entity->image_id = null;\n        } else {\n            $this->entity->image_id = $image->id;\n        }\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/EntityDefaultTemplate.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Queries\\PageQueries;\n\nclass EntityDefaultTemplate\n{\n    public function __construct(\n        protected Book|Chapter $entity,\n    ) {\n    }\n\n    /**\n     * Set the default template ID for this entity.\n     */\n    public function setFromId(int $templateId): void\n    {\n        $changing = $templateId !== intval($this->entity->default_template_id);\n        if (!$changing) {\n            return;\n        }\n\n        if ($templateId === 0) {\n            $this->entity->default_template_id = null;\n            return;\n        }\n\n        $pageQueries = app()->make(PageQueries::class);\n        $templateExists = $pageQueries->visibleTemplates()\n            ->where('id', '=', $templateId)\n            ->exists();\n\n        $this->entity->default_template_id = $templateExists ? $templateId : null;\n    }\n\n    /**\n     * Get the default template for this entity (if visible).\n     */\n    public function get(): Page|null\n    {\n        if (!$this->entity->default_template_id) {\n            return null;\n        }\n\n        $pageQueries = app()->make(PageQueries::class);\n        $page = $pageQueries->visibleTemplates(true)\n            ->where('id', '=', $this->entity->default_template_id)\n            ->first();\n\n        if ($page instanceof Page) {\n            return $page;\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/EntityHtmlDescription.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Util\\HtmlContentFilter;\nuse BookStack\\Util\\HtmlContentFilterConfig;\n\nclass EntityHtmlDescription\n{\n    protected string $html = '';\n    protected string $plain = '';\n\n    public function __construct(\n        protected Book|Chapter|Bookshelf $entity,\n    ) {\n        $this->html = $this->entity->description_html ?? '';\n        $this->plain = $this->entity->description ?? '';\n    }\n\n    /**\n     * Update the description from HTML code.\n     * Optionally takes plaintext to use for the model also.\n     */\n    public function set(string $html, string|null $plaintext = null): void\n    {\n        $this->html = $html;\n        $this->entity->description_html = $this->html;\n\n        if ($plaintext !== null) {\n            $this->plain = $plaintext;\n            $this->entity->description = $this->plain;\n        }\n\n        if (empty($html) && !empty($plaintext)) {\n            $this->html = $this->getHtml();\n            $this->entity->description_html = $this->html;\n        }\n    }\n\n    /**\n     * Get the description as HTML.\n     * Optionally returns the raw HTML if requested.\n     */\n    public function getHtml(bool $raw = false): string\n    {\n        $html = $this->html ?: '<p>' . nl2br(e($this->plain)) . '</p>';\n        if ($raw) {\n            return $html;\n        }\n\n        $isEmpty = empty(trim(strip_tags($html)));\n        if ($isEmpty) {\n            return '<p></p>';\n        }\n\n        $filter = new HtmlContentFilter(new HtmlContentFilterConfig());\n        return $filter->filterString($html);\n    }\n\n    public function getPlain(): string\n    {\n        return $this->plain;\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/EntityHydrator.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools;\n\nuse BookStack\\Activity\\Models\\Tag;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\EntityTable;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse Illuminate\\Database\\Eloquent\\Collection;\n\nclass EntityHydrator\n{\n    public function __construct(\n        protected EntityQueries $entityQueries,\n    ) {\n    }\n\n    /**\n     * Hydrate the entities of this hydrator to return a list of entities represented\n     * in their original intended models.\n     * @param EntityTable[] $entities\n     * @return Entity[]\n     */\n    public function hydrate(array $entities, bool $loadTags = false, bool $loadParents = false): array\n    {\n        $hydrated = [];\n\n        foreach ($entities as $entity) {\n            $data = $entity->getRawOriginal();\n            $instance = Entity::instanceFromType($entity->type);\n\n            if ($instance instanceof Page) {\n                $data['text'] = $data['description'];\n                unset($data['description']);\n            }\n\n            $instance = $instance->setRawAttributes($data, true);\n            $hydrated[] = $instance;\n        }\n\n        if ($loadTags) {\n            $this->loadTagsIntoModels($hydrated);\n        }\n\n        if ($loadParents) {\n            $this->loadParentsIntoModels($hydrated);\n        }\n\n        return $hydrated;\n    }\n\n    /**\n     * @param Entity[] $entities\n     */\n    protected function loadTagsIntoModels(array $entities): void\n    {\n        $idsByType = [];\n        $entityMap = [];\n        foreach ($entities as $entity) {\n            if (!isset($idsByType[$entity->type])) {\n                $idsByType[$entity->type] = [];\n            }\n            $idsByType[$entity->type][] = $entity->id;\n            $entityMap[$entity->type . ':' . $entity->id] = $entity;\n        }\n\n        $query = Tag::query();\n        foreach ($idsByType as $type => $ids) {\n            $query->orWhere(function ($query) use ($type, $ids) {\n                $query->where('entity_type', '=', $type)\n                    ->whereIn('entity_id', $ids);\n            });\n        }\n\n        $tags = empty($idsByType) ? [] : $query->get()->all();\n        $tagMap = [];\n        foreach ($tags as $tag) {\n            $key = $tag->entity_type . ':' . $tag->entity_id;\n            if (!isset($tagMap[$key])) {\n                $tagMap[$key] = [];\n            }\n            $tagMap[$key][] = $tag;\n        }\n\n        foreach ($entityMap as $key => $entity) {\n            $entityTags = new Collection($tagMap[$key] ?? []);\n            $entity->setRelation('tags', $entityTags);\n        }\n    }\n\n    /**\n     * @param Entity[] $entities\n     */\n    protected function loadParentsIntoModels(array $entities): void\n    {\n        $parentsByType = ['book' => [], 'chapter' => []];\n\n        foreach ($entities as $entity) {\n            if ($entity->getAttribute('book_id') !== null) {\n                $parentsByType['book'][] = $entity->getAttribute('book_id');\n            }\n            if ($entity->getAttribute('chapter_id') !== null) {\n                $parentsByType['chapter'][] = $entity->getAttribute('chapter_id');\n            }\n        }\n\n        $parentQuery = $this->entityQueries->visibleForList();\n        $filtered = count($parentsByType['book']) > 0 || count($parentsByType['chapter']) > 0;\n        $parentQuery = $parentQuery->where(function ($query) use ($parentsByType) {\n            foreach ($parentsByType as $type => $ids) {\n                if (count($ids) > 0) {\n                    $query = $query->orWhere(function ($query) use ($type, $ids) {\n                        $query->where('type', '=', $type)\n                            ->whereIn('id', $ids);\n                    });\n                }\n            }\n        });\n\n        $parentModels = $filtered ? $parentQuery->get()->all() : [];\n        $parents = $this->hydrate($parentModels);\n        $parentMap = [];\n        foreach ($parents as $parent) {\n            $parentMap[$parent->type . ':' . $parent->id] = $parent;\n        }\n\n        foreach ($entities as $entity) {\n            if ($entity instanceof Page || $entity instanceof Chapter) {\n                $key = 'book:' . $entity->getRawAttribute('book_id');\n                $entity->setRelation('book', $parentMap[$key] ?? null);\n            }\n            if ($entity instanceof Page) {\n                $key = 'chapter:' . $entity->getRawAttribute('chapter_id');\n                $entity->setRelation('chapter', $parentMap[$key] ?? null);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/HierarchyTransformer.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Repos\\BookRepo;\nuse BookStack\\Entities\\Repos\\BookshelfRepo;\nuse BookStack\\Facades\\Activity;\n\nclass HierarchyTransformer\n{\n    public function __construct(\n        protected BookRepo $bookRepo,\n        protected BookshelfRepo $shelfRepo,\n        protected Cloner $cloner,\n        protected TrashCan $trashCan,\n        protected ParentChanger $parentChanger,\n    ) {\n    }\n\n    /**\n     * Transform a chapter into a book.\n     * Does not check permissions, check before calling.\n     */\n    public function transformChapterToBook(Chapter $chapter): Book\n    {\n        $inputData = $this->cloner->entityToInputData($chapter);\n        $book = $this->bookRepo->create($inputData);\n        $this->cloner->copyEntityPermissions($chapter, $book);\n\n        /** @var Page $page */\n        foreach ($chapter->pages as $page) {\n            $page->chapter_id = 0;\n            $page->save();\n            $this->parentChanger->changeBook($page, $book->id);\n        }\n\n        $this->trashCan->destroyEntity($chapter);\n\n        Activity::add(ActivityType::BOOK_CREATE_FROM_CHAPTER, $book);\n\n        return $book;\n    }\n\n    /**\n     * Transform a book into a shelf.\n     * Does not check permissions, check before calling.\n     */\n    public function transformBookToShelf(Book $book): Bookshelf\n    {\n        $inputData = $this->cloner->entityToInputData($book);\n        $shelf = $this->shelfRepo->create($inputData, []);\n        $this->cloner->copyEntityPermissions($book, $shelf);\n\n        $shelfBookSyncData = [];\n\n        /** @var Chapter $chapter */\n        foreach ($book->chapters as $index => $chapter) {\n            $newBook = $this->transformChapterToBook($chapter);\n            $shelfBookSyncData[$newBook->id] = ['order' => $index];\n            if (!$newBook->hasPermissions()) {\n                $this->cloner->copyEntityPermissions($shelf, $newBook);\n            }\n        }\n\n        if ($book->directPages->count() > 0) {\n            $book->name .= ' ' . trans('entities.pages');\n            $shelfBookSyncData[$book->id] = ['order' => count($shelfBookSyncData) + 1];\n            $book->save();\n        } else {\n            $this->trashCan->destroyEntity($book);\n        }\n\n        $shelf->books()->sync($shelfBookSyncData);\n\n        Activity::add(ActivityType::BOOKSHELF_CREATE_FROM_BOOK, $shelf);\n\n        return $shelf;\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/Markdown/CheckboxConverter.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools\\Markdown;\n\nuse League\\HTMLToMarkdown\\Converter\\ConverterInterface;\nuse League\\HTMLToMarkdown\\ElementInterface;\n\nclass CheckboxConverter implements ConverterInterface\n{\n    public function convert(ElementInterface $element): string\n    {\n        if (strtolower($element->getAttribute('type')) === 'checkbox') {\n            $isChecked = $element->getAttribute('checked') === 'checked';\n\n            return $isChecked ? ' [x] ' : ' [ ] ';\n        }\n\n        return $element->getValue();\n    }\n\n    /**\n     * @return string[]\n     */\n    public function getSupportedTags(): array\n    {\n        return ['input'];\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/Markdown/CustomDivConverter.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools\\Markdown;\n\nuse League\\HTMLToMarkdown\\Converter\\DivConverter;\nuse League\\HTMLToMarkdown\\ElementInterface;\n\nclass CustomDivConverter extends DivConverter\n{\n    public function convert(ElementInterface $element): string\n    {\n        // Clean up draw.io diagrams\n        $drawIoDiagram = $element->getAttribute('drawio-diagram');\n        if ($drawIoDiagram) {\n            return \"<div drawio-diagram=\\\"{$drawIoDiagram}\\\">{$element->getValue()}</div>\\n\\n\";\n        }\n\n        return parent::convert($element);\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/Markdown/CustomImageConverter.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools\\Markdown;\n\nuse League\\HTMLToMarkdown\\Converter\\ImageConverter;\nuse League\\HTMLToMarkdown\\ElementInterface;\n\nclass CustomImageConverter extends ImageConverter\n{\n    public function convert(ElementInterface $element): string\n    {\n        $parent = $element->getParent();\n\n        // Remain as HTML if within diagram block.\n        $withinDrawing = $parent && !empty($parent->getAttribute('drawio-diagram'));\n        if ($withinDrawing) {\n            $src = e($element->getAttribute('src'));\n            $alt = e($element->getAttribute('alt'));\n\n            return \"<img src=\\\"{$src}\\\" alt=\\\"{$alt}\\\"/>\";\n        }\n\n        return parent::convert($element);\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/Markdown/CustomListItemRenderer.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools\\Markdown;\n\nuse League\\CommonMark\\Extension\\CommonMark\\Node\\Block\\ListItem;\nuse League\\CommonMark\\Extension\\CommonMark\\Renderer\\Block\\ListItemRenderer;\nuse League\\CommonMark\\Extension\\TaskList\\TaskListItemMarker;\nuse League\\CommonMark\\Node\\Block\\Paragraph;\nuse League\\CommonMark\\Node\\Node;\nuse League\\CommonMark\\Renderer\\ChildNodeRendererInterface;\nuse League\\CommonMark\\Renderer\\NodeRendererInterface;\nuse League\\CommonMark\\Util\\HtmlElement;\n\nclass CustomListItemRenderer implements NodeRendererInterface\n{\n    protected ListItemRenderer $baseRenderer;\n\n    public function __construct()\n    {\n        $this->baseRenderer = new ListItemRenderer();\n    }\n\n    /**\n     * @return HtmlElement|string|null\n     */\n    public function render(Node $node, ChildNodeRendererInterface $childRenderer)\n    {\n        $listItem = $this->baseRenderer->render($node, $childRenderer);\n\n        if ($node instanceof ListItem && $this->startsTaskListItem($node) && $listItem instanceof HtmlElement) {\n            $listItem->setAttribute('class', 'task-list-item');\n        }\n\n        return $listItem;\n    }\n\n    private function startsTaskListItem(ListItem $block): bool\n    {\n        $firstChild = $block->firstChild();\n\n        return $firstChild instanceof Paragraph && $firstChild->firstChild() instanceof TaskListItemMarker;\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/Markdown/CustomParagraphConverter.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools\\Markdown;\n\nuse League\\HTMLToMarkdown\\Converter\\ParagraphConverter;\nuse League\\HTMLToMarkdown\\ElementInterface;\n\nclass CustomParagraphConverter extends ParagraphConverter\n{\n    public function convert(ElementInterface $element): string\n    {\n        $class = e($element->getAttribute('class'));\n        if (strpos($class, 'callout') !== false) {\n            return \"<{$element->getTagName()} class=\\\"{$class}\\\">{$element->getValue()}</{$element->getTagName()}>\\n\\n\";\n        }\n\n        return parent::convert($element);\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/Markdown/CustomStrikeThroughExtension.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools\\Markdown;\n\nuse League\\CommonMark\\Environment\\EnvironmentBuilderInterface;\nuse League\\CommonMark\\Extension\\ExtensionInterface;\nuse League\\CommonMark\\Extension\\Strikethrough\\Strikethrough;\nuse League\\CommonMark\\Extension\\Strikethrough\\StrikethroughDelimiterProcessor;\n\nclass CustomStrikeThroughExtension implements ExtensionInterface\n{\n    public function register(EnvironmentBuilderInterface $environment): void\n    {\n        $environment->addDelimiterProcessor(new StrikethroughDelimiterProcessor());\n        $environment->addRenderer(Strikethrough::class, new CustomStrikethroughRenderer());\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/Markdown/CustomStrikethroughRenderer.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools\\Markdown;\n\nuse League\\CommonMark\\Extension\\Strikethrough\\Strikethrough;\nuse League\\CommonMark\\Node\\Node;\nuse League\\CommonMark\\Renderer\\ChildNodeRendererInterface;\nuse League\\CommonMark\\Renderer\\NodeRendererInterface;\nuse League\\CommonMark\\Util\\HtmlElement;\n\n/**\n * This is a somewhat clone of the League\\CommonMark\\Extension\\Strikethrough\\StrikethroughRender\n * class but modified slightly to use <s> HTML tags instead of <del> in order to\n * match front-end markdown-it rendering.\n */\nclass CustomStrikethroughRenderer implements NodeRendererInterface\n{\n    public function render(Node $node, ChildNodeRendererInterface $childRenderer)\n    {\n        Strikethrough::assertInstanceOf($node);\n\n        return new HtmlElement('s', $node->data->get('attributes'), $childRenderer->renderNodes($node->children()));\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/Markdown/HtmlToMarkdown.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools\\Markdown;\n\nuse League\\HTMLToMarkdown\\Converter\\BlockquoteConverter;\nuse League\\HTMLToMarkdown\\Converter\\CodeConverter;\nuse League\\HTMLToMarkdown\\Converter\\CommentConverter;\nuse League\\HTMLToMarkdown\\Converter\\EmphasisConverter;\nuse League\\HTMLToMarkdown\\Converter\\HardBreakConverter;\nuse League\\HTMLToMarkdown\\Converter\\HeaderConverter;\nuse League\\HTMLToMarkdown\\Converter\\HorizontalRuleConverter;\nuse League\\HTMLToMarkdown\\Converter\\LinkConverter;\nuse League\\HTMLToMarkdown\\Converter\\ListBlockConverter;\nuse League\\HTMLToMarkdown\\Converter\\ListItemConverter;\nuse League\\HTMLToMarkdown\\Converter\\PreformattedConverter;\nuse League\\HTMLToMarkdown\\Converter\\TextConverter;\nuse League\\HTMLToMarkdown\\Environment;\nuse League\\HTMLToMarkdown\\HtmlConverter;\n\nclass HtmlToMarkdown\n{\n    protected string $html;\n\n    public function __construct(string $html)\n    {\n        $this->html = $html;\n    }\n\n    /**\n     * Run the conversion.\n     */\n    public function convert(): string\n    {\n        $converter = new HtmlConverter($this->getConverterEnvironment());\n        $html = $this->prepareHtml($this->html);\n\n        return $converter->convert($html);\n    }\n\n    /**\n     * Run any pre-processing to the HTML to clean it up manually before conversion.\n     */\n    protected function prepareHtml(string $html): string\n    {\n        // Carriage returns can cause whitespace issues in output\n        $html = str_replace(\"\\r\\n\", \"\\n\", $html);\n        // Attributes on the pre tag can cause issues with conversion\n        return preg_replace('/<pre .*?>/', '<pre>', $html);\n    }\n\n    /**\n     * Get the HTML to Markdown customized environment.\n     * Extends the default provided environment with some BookStack specific tweaks.\n     */\n    protected function getConverterEnvironment(): Environment\n    {\n        $environment = new Environment([\n            'header_style'            => 'atx', // Set to 'atx' to output H1 and H2 headers as # Header1 and ## Header2\n            'suppress_errors'         => true, // Set to false to show warnings when loading malformed HTML\n            'strip_tags'              => false, // Set to true to strip tags that don't have markdown equivalents. N.B. Strips tags, not their content. Useful to clean MS Word HTML output.\n            'strip_placeholder_links' => false, // Set to true to remove <a> that doesn't have href.\n            'bold_style'              => '**', // DEPRECATED: Set to '__' if you prefer the underlined style\n            'italic_style'            => '*', // DEPRECATED: Set to '_' if you prefer the underlined style\n            'remove_nodes'            => '', // space-separated list of dom nodes that should be removed. example: 'meta style script'\n            'hard_break'              => false, // Set to true to turn <br> into `\\n` instead of `  \\n`\n            'list_item_style'         => '-', // Set the default character for each <li> in a <ul>. Can be '-', '*', or '+'\n            'preserve_comments'       => false, // Set to true to preserve comments, or set to an array of strings to preserve specific comments\n            'use_autolinks'           => false, // Set to true to use simple link syntax if possible. Will always use []() if set to false\n            'table_pipe_escape'       => '\\|', // Replacement string for pipe characters inside markdown table cells\n            'table_caption_side'      => 'top', // Set to 'top' or 'bottom' to show <caption> content before or after table, null to suppress\n        ]);\n\n        $environment->addConverter(new BlockquoteConverter());\n        $environment->addConverter(new CodeConverter());\n        $environment->addConverter(new CommentConverter());\n        $environment->addConverter(new CustomDivConverter());\n        $environment->addConverter(new EmphasisConverter());\n        $environment->addConverter(new HardBreakConverter());\n        $environment->addConverter(new HeaderConverter());\n        $environment->addConverter(new HorizontalRuleConverter());\n        $environment->addConverter(new CustomImageConverter());\n        $environment->addConverter(new LinkConverter());\n        $environment->addConverter(new ListBlockConverter());\n        $environment->addConverter(new ListItemConverter());\n        $environment->addConverter(new CustomParagraphConverter());\n        $environment->addConverter(new PreformattedConverter());\n        $environment->addConverter(new TextConverter());\n        $environment->addConverter(new CheckboxConverter());\n        $environment->addConverter(new SpacedTagFallbackConverter());\n\n        return $environment;\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/Markdown/MarkdownToHtml.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools\\Markdown;\n\nuse BookStack\\Facades\\Theme;\nuse BookStack\\Theming\\ThemeEvents;\nuse League\\CommonMark\\Environment\\Environment;\nuse League\\CommonMark\\Extension\\CommonMark\\CommonMarkCoreExtension;\nuse League\\CommonMark\\Extension\\CommonMark\\Node\\Block\\ListItem;\nuse League\\CommonMark\\Extension\\Table\\TableExtension;\nuse League\\CommonMark\\Extension\\TaskList\\TaskListExtension;\nuse League\\CommonMark\\MarkdownConverter;\n\nclass MarkdownToHtml\n{\n    protected string $markdown;\n\n    public function __construct(string $markdown)\n    {\n        $this->markdown = $markdown;\n    }\n\n    public function convert(): string\n    {\n        $environment = new Environment();\n        $environment->addExtension(new CommonMarkCoreExtension());\n        $environment->addExtension(new TableExtension());\n        $environment->addExtension(new TaskListExtension());\n        $environment->addExtension(new CustomStrikeThroughExtension());\n        $environment = Theme::dispatch(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $environment) ?? $environment;\n        $converter = new MarkdownConverter($environment);\n\n        $environment->addRenderer(ListItem::class, new CustomListItemRenderer(), 10);\n\n        return $converter->convert($this->markdown)->getContent();\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/Markdown/SpacedTagFallbackConverter.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools\\Markdown;\n\nuse League\\HTMLToMarkdown\\Converter\\ConverterInterface;\nuse League\\HTMLToMarkdown\\ElementInterface;\n\n/**\n * For certain defined tags, add additional spacing upon the retained HTML content\n * to separate it out from anything that may be markdown soon afterwards or within.\n */\nclass SpacedTagFallbackConverter implements ConverterInterface\n{\n    public function convert(ElementInterface $element): string\n    {\n        return \\html_entity_decode($element->getChildrenAsString()) . \"\\n\\n\";\n    }\n\n    public function getSupportedTags(): array\n    {\n        return ['summary', 'iframe'];\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/MixedEntityListLoader.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools;\n\nuse BookStack\\App\\Model;\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse Illuminate\\Database\\Eloquent\\Relations\\Relation;\n\nclass MixedEntityListLoader\n{\n    public function __construct(\n        protected EntityQueries $queries,\n    ) {\n    }\n\n    /**\n     * Efficiently load in entities for listing onto the given list\n     * where entities are set as a relation via the given name.\n     * This will look for a model id and type via 'name_id' and 'name_type'.\n     * @param Model[] $relations\n     */\n    public function loadIntoRelations(array $relations, string $relationName, bool $loadParents, bool $withContents = false): void\n    {\n        $idsByType = [];\n        foreach ($relations as $relation) {\n            $type = $relation->getAttribute($relationName . '_type');\n            $id = $relation->getAttribute($relationName . '_id');\n\n            if (!isset($idsByType[$type])) {\n                $idsByType[$type] = [];\n            }\n\n            $idsByType[$type][] = $id;\n        }\n\n        $modelMap = $this->idsByTypeToModelMap($idsByType, $loadParents, $withContents);\n\n        foreach ($relations as $relation) {\n            $type = $relation->getAttribute($relationName . '_type');\n            $id = $relation->getAttribute($relationName . '_id');\n            $related = $modelMap[$type][strval($id)] ?? null;\n            if ($related) {\n                $relation->setRelation($relationName, $related);\n            }\n        }\n    }\n\n    /**\n     * @param array<string, int[]> $idsByType\n     * @return array<string, array<int, Model>>\n     */\n    protected function idsByTypeToModelMap(array $idsByType, bool $eagerLoadParents, bool $withContents): array\n    {\n        $modelMap = [];\n\n        foreach ($idsByType as $type => $ids) {\n            $base = $withContents ? $this->queries->visibleForContentForType($type) : $this->queries->visibleForListForType($type);\n            $models = $base->whereIn('id', $ids)\n                ->with($eagerLoadParents ? $this->getRelationsToEagerLoad($type) : [])\n                ->get();\n\n            if (count($models) > 0) {\n                $modelMap[$type] = [];\n            }\n\n            foreach ($models as $model) {\n                $modelMap[$type][strval($model->id)] = $model;\n            }\n        }\n\n        return $modelMap;\n    }\n\n    protected function getRelationsToEagerLoad(string $type): array\n    {\n        $toLoad = [];\n        $loadVisible = fn (Relation $query) => $query->scopes('visible');\n\n        if ($type === 'chapter' || $type === 'page') {\n            $toLoad['book'] = $loadVisible;\n        }\n\n        if ($type === 'page') {\n            $toLoad['chapter'] = $loadVisible;\n        }\n\n        return $toLoad;\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/MixedEntityRequestHelper.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools;\n\nuse BookStack\\Entities\\EntityProvider;\nuse BookStack\\Entities\\Models\\Entity;\n\nclass MixedEntityRequestHelper\n{\n    public function __construct(\n        protected EntityProvider $entities,\n    ) {\n    }\n\n    /**\n     * Query out an entity, visible to the current user, for the given\n     * entity request details (this provided in a request validated by\n     * this classes' validationRules method).\n     * @param array{type: string, id: string} $requestData\n     */\n    public function getVisibleEntityFromRequestData(array $requestData): Entity\n    {\n        $entityType = $this->entities->get($requestData['type']);\n\n        return $entityType->newQuery()->scopes(['visible'])->findOrFail($requestData['id']);\n    }\n\n    /**\n     * Get the validation rules for an abstract entity request.\n     * @return array{type: string[], id: string[]}\n     */\n    public function validationRules(): array\n    {\n        return [\n                'type' => ['required', 'string'],\n                'id'   => ['required', 'integer'],\n        ];\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/NextPreviousContentLocator.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools;\n\nuse BookStack\\Entities\\Models\\BookChild;\nuse BookStack\\Entities\\Models\\Entity;\nuse Illuminate\\Support\\Collection;\n\n/**\n * Finds the next or previous content of a book element (page or chapter).\n */\nclass NextPreviousContentLocator\n{\n    protected $relativeBookItem;\n    protected $flatTree;\n    protected $currentIndex = null;\n\n    /**\n     * NextPreviousContentLocator constructor.\n     */\n    public function __construct(BookChild $relativeBookItem, Collection $bookTree)\n    {\n        $this->relativeBookItem = $relativeBookItem;\n        $this->flatTree = $this->treeToFlatOrderedCollection($bookTree);\n        $this->currentIndex = $this->getCurrentIndex();\n    }\n\n    /**\n     * Get the next logical entity within the book hierarchy.\n     */\n    public function getNext(): ?Entity\n    {\n        return $this->flatTree->get($this->currentIndex + 1);\n    }\n\n    /**\n     * Get the next logical entity within the book hierarchy.\n     */\n    public function getPrevious(): ?Entity\n    {\n        return $this->flatTree->get($this->currentIndex - 1);\n    }\n\n    /**\n     * Get the index of the current relative item.\n     */\n    protected function getCurrentIndex(): ?int\n    {\n        $index = $this->flatTree->search(function (Entity $entity) {\n            return get_class($entity) === get_class($this->relativeBookItem)\n                && $entity->id === $this->relativeBookItem->id;\n        });\n\n        return $index === false ? null : $index;\n    }\n\n    /**\n     * Convert a book tree collection to a flattened version\n     * where all items follow the expected order of user flow.\n     */\n    protected function treeToFlatOrderedCollection(Collection $bookTree): Collection\n    {\n        $flatOrdered = collect();\n        /** @var Entity $item */\n        foreach ($bookTree->all() as $item) {\n            $flatOrdered->push($item);\n            $childPages = $item->getAttribute('visible_pages') ?? [];\n            $flatOrdered = $flatOrdered->concat($childPages);\n        }\n\n        return $flatOrdered;\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/PageContent.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools;\n\nuse BookStack\\App\\AppVersion;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Queries\\PageQueries;\nuse BookStack\\Entities\\Tools\\Markdown\\MarkdownToHtml;\nuse BookStack\\Exceptions\\ImageUploadException;\nuse BookStack\\Facades\\Theme;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Theming\\ThemeEvents;\nuse BookStack\\Uploads\\ImageRepo;\nuse BookStack\\Uploads\\ImageService;\nuse BookStack\\Users\\Models\\User;\nuse BookStack\\Util\\HtmlContentFilter;\nuse BookStack\\Util\\HtmlContentFilterConfig;\nuse BookStack\\Util\\HtmlDocument;\nuse BookStack\\Util\\WebSafeMimeSniffer;\nuse Closure;\nuse DOMElement;\nuse DOMNode;\nuse DOMNodeList;\nuse Illuminate\\Support\\Str;\n\nclass PageContent\n{\n    protected PageQueries $pageQueries;\n\n    public function __construct(\n        protected Page $page\n    ) {\n        $this->pageQueries = app()->make(PageQueries::class);\n    }\n\n    /**\n     * Update the content of the page with new provided HTML.\n     */\n    public function setNewHTML(string $html, User $updater): void\n    {\n        $html = $this->extractBase64ImagesFromHtml($html, $updater);\n        $html = $this->formatHtml($html);\n\n        $themeResult = Theme::dispatch(ThemeEvents::PAGE_CONTENT_PRE_STORE, $html, $this->page);\n        if (is_string($themeResult)) {\n            $html = $themeResult;\n        }\n\n        $this->page->html = $html;\n        $this->page->text = $this->toPlainText();\n        $this->page->markdown = '';\n    }\n\n    /**\n     * Update the content of the page with new provided Markdown content.\n     */\n    public function setNewMarkdown(string $markdown, User $updater): void\n    {\n        $markdown = $this->extractBase64ImagesFromMarkdown($markdown, $updater);\n        $this->page->markdown = $markdown;\n        $html = (new MarkdownToHtml($markdown))->convert();\n        $html = $this->formatHtml($html);\n\n        $themeResult = Theme::dispatch(ThemeEvents::PAGE_CONTENT_PRE_STORE, $html, $this->page);\n        if (is_string($themeResult)) {\n            $html = $themeResult;\n        }\n\n        $this->page->html = $html;\n        $this->page->text = $this->toPlainText();\n    }\n\n    /**\n     * Convert all base64 image data to saved images.\n     */\n    protected function extractBase64ImagesFromHtml(string $htmlText, User $updater): string\n    {\n        if (empty($htmlText) || !str_contains($htmlText, 'data:image')) {\n            return $htmlText;\n        }\n\n        $doc = new HtmlDocument($htmlText);\n\n        // Get all img elements with image data blobs\n        $imageNodes = $doc->queryXPath('//img[contains(@src, \\'data:image\\')]');\n        /** @var DOMElement $imageNode */\n        foreach ($imageNodes as $imageNode) {\n            $imageSrc = $imageNode->getAttribute('src');\n            $newUrl = $this->base64ImageUriToUploadedImageUrl($imageSrc, $updater);\n            $imageNode->setAttribute('src', $newUrl);\n        }\n\n        return $doc->getBodyInnerHtml();\n    }\n\n    /**\n     * Convert all inline base64 content to uploaded image files.\n     * Regex is used to locate the start of data-uri definitions, then\n     * manual looping over content is done to parse the whole data uri.\n     * Attempting to capture the whole data uri using regex can cause PHP\n     * PCRE limits to be hit with larger, multi-MB, files.\n     */\n    protected function extractBase64ImagesFromMarkdown(string $markdown, User $updater): string\n    {\n        $matches = [];\n        $contentLength = strlen($markdown);\n        $replacements = [];\n        preg_match_all('/!\\[.*?]\\(.*?(data:image\\/.{1,6};base64,)/', $markdown, $matches, PREG_OFFSET_CAPTURE);\n\n        foreach ($matches[1] as $base64MatchPair) {\n            [$dataUri, $index] = $base64MatchPair;\n\n            for ($i = strlen($dataUri) + $index; $i < $contentLength; $i++) {\n                $char = $markdown[$i];\n                if ($char === ')' || $char === ' ' || $char === \"\\n\" || $char === '\"') {\n                    break;\n                }\n                $dataUri .= $char;\n            }\n\n            $newUrl = $this->base64ImageUriToUploadedImageUrl($dataUri, $updater);\n            $replacements[] = [$dataUri, $newUrl];\n        }\n\n        foreach ($replacements as [$dataUri, $newUrl]) {\n            $markdown = str_replace($dataUri, $newUrl, $markdown);\n        }\n\n        return $markdown;\n    }\n\n    /**\n     * Parse the given base64 image URI and return the URL to the created image instance.\n     * Returns an empty string if the parsed URI is invalid or causes an error upon upload.\n     */\n    protected function base64ImageUriToUploadedImageUrl(string $uri, User $updater): string\n    {\n        $imageRepo = app()->make(ImageRepo::class);\n        $imageInfo = $this->parseBase64ImageUri($uri);\n\n        // Validate user has permission to create images\n        if (!$updater->can(Permission::ImageCreateAll)) {\n            return '';\n        }\n\n        // Validate extension and content\n        if (empty($imageInfo['data']) || !ImageService::isExtensionSupported($imageInfo['extension'])) {\n            return '';\n        }\n\n        // Validate content looks like an image via sniffing mime type\n        $mimeSniffer = new WebSafeMimeSniffer();\n        $mime = $mimeSniffer->sniff($imageInfo['data']);\n        if (!str_starts_with($mime, 'image/')) {\n            return '';\n        }\n\n        // Validate that the content is not over our upload limit\n        $uploadLimitBytes = (config('app.upload_limit') * 1000000);\n        if (strlen($imageInfo['data']) > $uploadLimitBytes) {\n            return '';\n        }\n\n        // Save image from data with a random name\n        $imageName = 'embedded-image-' . Str::random(8) . '.' . $imageInfo['extension'];\n\n        try {\n            $image = $imageRepo->saveNewFromData($imageName, $imageInfo['data'], 'gallery', $this->page->id);\n        } catch (ImageUploadException $exception) {\n            return '';\n        }\n\n        return $image->url;\n    }\n\n    /**\n     * Parse a base64 image URI into the data and extension.\n     *\n     * @return array{extension: string, data: string}\n     */\n    protected function parseBase64ImageUri(string $uri): array\n    {\n        [$dataDefinition, $base64ImageData] = explode(',', $uri, 2);\n        $extension = strtolower(preg_split('/[\\/;]/', $dataDefinition)[1] ?? '');\n\n        return [\n            'extension' => $extension,\n            'data'      => base64_decode($base64ImageData) ?: '',\n        ];\n    }\n\n    /**\n     * Formats a page's html to be tagged correctly within the system.\n     */\n    protected function formatHtml(string $htmlText): string\n    {\n        if (empty($htmlText)) {\n            return $htmlText;\n        }\n\n        $doc = new HtmlDocument($htmlText);\n\n        // Map to hold used ID references\n        $idMap = [];\n        // Map to hold changing ID references\n        $changeMap = [];\n\n        $this->updateIdsRecursively($doc->getBody(), 0, $idMap, $changeMap);\n        $this->updateLinks($doc, $changeMap);\n\n        // Generate inner html as a string & perform required string-level tweaks\n        $html = $doc->getBodyInnerHtml();\n        $html = str_replace(' ', '&nbsp;', $html);\n\n        return $html;\n    }\n\n    /**\n     * For the given DOMNode, traverse its children recursively and update IDs\n     * where required (Top-level, headers & elements with IDs).\n     * Will update the provided $changeMap array with changes made, where keys are the old\n     * ids and the corresponding values are the new ids.\n     */\n    protected function updateIdsRecursively(DOMNode $element, int $depth, array &$idMap, array &$changeMap): void\n    {\n        /* @var DOMNode $child */\n        foreach ($element->childNodes as $child) {\n            if ($child instanceof DOMElement && ($depth === 0 || in_array($child->nodeName, ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']) || $child->getAttribute('id'))) {\n                [$oldId, $newId] = $this->setUniqueId($child, $idMap);\n                if ($newId && $newId !== $oldId && !isset($idMap[$oldId])) {\n                    $changeMap[$oldId] = $newId;\n                }\n            }\n\n            if ($child->hasChildNodes()) {\n                $this->updateIdsRecursively($child, $depth + 1, $idMap, $changeMap);\n            }\n        }\n    }\n\n    /**\n     * Update the all links in the given xpath to apply requires changes within the\n     * given $changeMap array.\n     */\n    protected function updateLinks(HtmlDocument $doc, array $changeMap): void\n    {\n        if (empty($changeMap)) {\n            return;\n        }\n\n        $links = $doc->queryXPath('//body//*//*[@href]');\n        /** @var DOMElement $domElem */\n        foreach ($links as $domElem) {\n            $href = ltrim($domElem->getAttribute('href'), '#');\n            $newHref = $changeMap[$href] ?? null;\n            if ($newHref) {\n                $domElem->setAttribute('href', '#' . $newHref);\n            }\n        }\n    }\n\n    /**\n     * Set a unique id on the given DOMElement.\n     * A map for existing ID's should be passed in to check for current existence,\n     * and this will be updated with any new IDs set upon elements.\n     * Returns a pair of strings in the format [old_id, new_id].\n     */\n    protected function setUniqueId(DOMNode $element, array &$idMap): array\n    {\n        if (!$element instanceof DOMElement) {\n            return ['', ''];\n        }\n\n        // Stop if there's an existing valid id that has not already been used.\n        $existingId = $element->getAttribute('id');\n        if (str_starts_with($existingId, 'bkmrk') && !isset($idMap[$existingId])) {\n            $idMap[$existingId] = true;\n\n            return [$existingId, $existingId];\n        }\n\n        // Create a unique id for the element\n        // Uses the content as a basis to ensure output is the same every time\n        // the same content is passed through.\n        $contentId = 'bkmrk-' . mb_substr(strtolower(preg_replace('/\\s+/', '-', trim($element->nodeValue))), 0, 20);\n        $newId = urlencode($contentId);\n        $loopIndex = 1;\n\n        while (isset($idMap[$newId])) {\n            $newId = urlencode($contentId . '-' . $loopIndex);\n            $loopIndex++;\n        }\n\n        $element->setAttribute('id', $newId);\n        $idMap[$newId] = true;\n\n        return [$existingId, $newId];\n    }\n\n    /**\n     * Get a plain-text visualisation of this page.\n     */\n    public function toPlainText(): string\n    {\n        $html = $this->render(true);\n\n        return html_entity_decode(strip_tags($html));\n    }\n\n    /**\n     * Render the page for viewing.\n     */\n    public function render(bool $blankIncludes = false): string\n    {\n        $html = $this->page->html ?? '';\n\n        if (empty($html)) {\n            return $this->handlePostRender('');\n        }\n\n        $doc = new HtmlDocument($html);\n        $contentProvider = $this->getContentProviderClosure($blankIncludes);\n        $parser = new PageIncludeParser($doc, $contentProvider);\n\n        $nodesAdded = 1;\n        for ($includeDepth = 0; $includeDepth < 3 && $nodesAdded !== 0; $includeDepth++) {\n            $nodesAdded = $parser->parse();\n        }\n\n        if ($includeDepth > 1) {\n            $idMap = [];\n            $changeMap = [];\n            $this->updateIdsRecursively($doc->getBody(), 0, $idMap, $changeMap);\n        }\n\n        $cacheKey = $this->getContentCacheKey($doc->getBodyInnerHtml());\n        $cached = cache()->get($cacheKey, null);\n        if ($cached !== null) {\n            return $this->handlePostRender($cached);\n        }\n\n        $filterConfig = HtmlContentFilterConfig::fromConfigString(config('app.content_filtering'));\n        $filter = new HtmlContentFilter($filterConfig);\n        $filtered = $filter->filterDocument($doc);\n\n        $cacheTime = 86400 * 7; // 1 week\n        cache()->put($cacheKey, $filtered, $cacheTime);\n\n        return $this->handlePostRender($filtered);\n    }\n\n    protected function handlePostRender(string $html): string\n    {\n        $themeResult = Theme::dispatch(ThemeEvents::PAGE_CONTENT_POST_RENDER, $html, $this->page);\n        return is_string($themeResult) ? $themeResult : $html;\n    }\n\n    protected function getContentCacheKey(string $html): string\n    {\n        $contentHash = md5($html);\n        $contentId = $this->page->id;\n        $contentTime = $this->page->updated_at?->timestamp ?? time();\n        $appVersion = AppVersion::get();\n        $filterConfig = config('app.content_filtering') ?? '';\n        return \"page-content-cache::{$filterConfig}::{$appVersion}::{$contentId}::{$contentTime}::{$contentHash}\";\n    }\n\n    /**\n     * Get the closure used to fetch content for page includes.\n     */\n    protected function getContentProviderClosure(bool $blankIncludes): Closure\n    {\n        $contextPage = $this->page;\n        $queries = $this->pageQueries;\n\n        return function (PageIncludeTag $tag) use ($blankIncludes, $contextPage, $queries): PageIncludeContent {\n            if ($blankIncludes) {\n                return PageIncludeContent::fromHtmlAndTag('', $tag);\n            }\n\n            $matchedPage = $queries->findVisibleById($tag->getPageId());\n            $content = PageIncludeContent::fromHtmlAndTag($matchedPage->html ?? '', $tag);\n\n            if (Theme::hasListeners(ThemeEvents::PAGE_INCLUDE_PARSE)) {\n                $themeReplacement = Theme::dispatch(\n                    ThemeEvents::PAGE_INCLUDE_PARSE,\n                    $tag->tagContent,\n                    $content->toHtml(),\n                    clone $contextPage,\n                    $matchedPage ? (clone $matchedPage) : null,\n                );\n\n                if ($themeReplacement !== null) {\n                    $content = PageIncludeContent::fromInlineHtml(strval($themeReplacement));\n                }\n            }\n\n            return $content;\n        };\n    }\n\n    /**\n     * Parse the headers on the page to get a navigation menu.\n     */\n    public function getNavigation(string $htmlContent): array\n    {\n        if (empty($htmlContent)) {\n            return [];\n        }\n\n        $doc = new HtmlDocument($htmlContent);\n        $headers = $doc->queryXPath('//h1|//h2|//h3|//h4|//h5|//h6');\n\n        return $headers->count() === 0 ? [] : $this->headerNodesToLevelList($headers);\n    }\n\n    /**\n     * Convert a DOMNodeList into an array of readable header attributes\n     * with levels normalised to the lower header level.\n     */\n    protected function headerNodesToLevelList(DOMNodeList $nodeList): array\n    {\n        $tree = collect($nodeList)->map(function (DOMElement $header) {\n            $text = trim(str_replace(\"\\xc2\\xa0\", ' ', $header->nodeValue));\n            $text = mb_substr($text, 0, 100);\n\n            return [\n                'nodeName' => strtolower($header->nodeName),\n                'level'    => intval(str_replace('h', '', $header->nodeName)),\n                'link'     => '#' . $header->getAttribute('id'),\n                'text'     => $text,\n            ];\n        })->filter(function ($header) {\n            return mb_strlen($header['text']) > 0;\n        });\n\n        // Shift headers if only smaller headers have been used\n        $levelChange = ($tree->pluck('level')->min() - 1);\n        $tree = $tree->map(function ($header) use ($levelChange) {\n            $header['level'] -= ($levelChange);\n\n            return $header;\n        });\n\n        return $tree->toArray();\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/PageEditActivity.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools;\n\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Models\\PageRevision;\nuse BookStack\\Util\\DateFormatter;\nuse Carbon\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Builder;\n\nclass PageEditActivity\n{\n    public function __construct(\n        protected Page $page\n    ) {\n    }\n\n    /**\n     * Check if there's active editing being performed on this page.\n     */\n    public function hasActiveEditing(): bool\n    {\n        return $this->activePageEditingQuery(60)->count() > 0;\n    }\n\n    /**\n     * Get a notification message concerning the editing activity on the page.\n     */\n    public function activeEditingMessage(): string\n    {\n        $pageDraftEdits = $this->activePageEditingQuery(60)->get();\n        $count = $pageDraftEdits->count();\n\n        $userMessage = trans('entities.pages_draft_edit_active.start_a', ['count' => $count]);\n        if ($count === 1) {\n            /** @var PageRevision $firstDraft */\n            $firstDraft = $pageDraftEdits->first();\n            $userMessage = trans('entities.pages_draft_edit_active.start_b', ['userName' => $firstDraft->createdBy->name ?? '']);\n        }\n\n        $timeMessage = trans('entities.pages_draft_edit_active.time_b', ['minCount' => 60]);\n\n        return trans('entities.pages_draft_edit_active.message', ['start' => $userMessage, 'time' => $timeMessage]);\n    }\n\n    /**\n     * Get any editor clash warning messages to show for the given draft revision.\n     *\n     * @return string[]\n     */\n    public function getWarningMessagesForDraft(Page|PageRevision $draft): array\n    {\n        $warnings = [];\n\n        if ($this->hasActiveEditing()) {\n            $warnings[] = $this->activeEditingMessage();\n        }\n\n        if ($draft instanceof PageRevision && $this->hasPageBeenUpdatedSinceDraftCreated($draft)) {\n            $warnings[] = trans('entities.pages_draft_page_changed_since_creation');\n        }\n\n        return $warnings;\n    }\n\n    /**\n     * Check if the page has been updated since the draft has been saved.\n     */\n    protected function hasPageBeenUpdatedSinceDraftCreated(PageRevision $draft): bool\n    {\n        return $draft->page->updated_at->timestamp > $draft->created_at->timestamp;\n    }\n\n    /**\n     * Get the message to show when the user will be editing one of their drafts.\n     */\n    public function getEditingActiveDraftMessage(PageRevision $draft): string\n    {\n        $formatter = resolve(DateFormatter::class);\n        $message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $formatter->relative($draft->updated_at)]);\n        if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) {\n            return $message;\n        }\n\n        return $message . \"\\n\" . trans('entities.pages_draft_edited_notification');\n    }\n\n    /**\n     * A query to check for active update drafts on a particular page\n     * within the last given many minutes.\n     */\n    protected function activePageEditingQuery(int $withinMinutes): Builder\n    {\n        $checkTime = Carbon::now()->subMinutes($withinMinutes);\n        $query = PageRevision::query()\n            ->where('type', '=', 'update_draft')\n            ->where('page_id', '=', $this->page->id)\n            ->where('updated_at', '>', $this->page->updated_at)\n            ->where('created_by', '!=', user()->id)\n            ->where('updated_at', '>=', $checkTime)\n            ->with('createdBy');\n\n        return $query;\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/PageEditorData.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools;\n\nuse BookStack\\Activity\\Tools\\CommentTree;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse BookStack\\Entities\\Tools\\Markdown\\HtmlToMarkdown;\nuse BookStack\\Entities\\Tools\\Markdown\\MarkdownToHtml;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Util\\HtmlContentFilter;\nuse BookStack\\Util\\HtmlContentFilterConfig;\n\nclass PageEditorData\n{\n    protected array $viewData;\n    protected array $warnings;\n\n    public function __construct(\n        protected Page $page,\n        protected EntityQueries $queries,\n        protected string $requestedEditor\n    ) {\n        $this->viewData = $this->build();\n    }\n\n    public function getViewData(): array\n    {\n        return $this->viewData;\n    }\n\n    public function getWarnings(): array\n    {\n        return $this->warnings;\n    }\n\n    protected function build(): array\n    {\n        $page = clone $this->page;\n        $isDraft = boolval($this->page->draft);\n        $templates = $this->queries->pages->visibleTemplates()\n            ->orderBy('name', 'asc')\n            ->take(10)\n            ->paginate()\n            ->withPath('/templates');\n\n        $draftsEnabled = auth()->check();\n\n        $isDraftRevision = false;\n        $this->warnings = [];\n        $editActivity = new PageEditActivity($page);\n        $lastEditorId = $page->updated_by ?? user()->id;\n\n        if ($editActivity->hasActiveEditing()) {\n            $this->warnings[] = $editActivity->activeEditingMessage();\n        }\n\n        // Check for a current draft version for this user\n        $userDraft = $this->queries->revisions->findLatestCurrentUserDraftsForPageId($page->id);\n        if (!is_null($userDraft)) {\n            $page->forceFill($userDraft->only(['name', 'html', 'markdown']));\n            $isDraftRevision = true;\n            $this->warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft);\n            $lastEditorId = $userDraft->created_by;\n        }\n\n        // Get editor type and handle changes\n        $editorType = $this->getEditorType($page);\n        $this->updateContentForEditor($page, $editorType);\n\n        // Filter HTML content if required\n        if ($editorType->isHtmlBased() && !old('html') && $lastEditorId !== user()->id) {\n            $filterConfig = HtmlContentFilterConfig::fromConfigString(config('app.content_filtering'));\n            $filter = new HtmlContentFilter($filterConfig);\n            $page->html = $filter->filterString($page->html);\n        }\n\n        return [\n            'page'            => $page,\n            'book'            => $page->book,\n            'isDraft'         => $isDraft,\n            'isDraftRevision' => $isDraftRevision,\n            'draftsEnabled'   => $draftsEnabled,\n            'templates'       => $templates,\n            'editor'          => $editorType,\n            'comments'        => new CommentTree($page),\n        ];\n    }\n\n    protected function updateContentForEditor(Page $page, PageEditorType $editorType): void\n    {\n        $isHtml = !empty($page->html) && empty($page->markdown);\n\n        // HTML to markdown-clean conversion\n        if ($editorType === PageEditorType::Markdown && $isHtml && $this->requestedEditor === 'markdown-clean') {\n            $page->markdown = (new HtmlToMarkdown($page->html))->convert();\n        }\n\n        // Markdown to HTML conversion if we don't have HTML\n        if ($editorType->isHtmlBased() && !$isHtml) {\n            $page->html = (new MarkdownToHtml($page->markdown))->convert();\n        }\n    }\n\n    /**\n     * Get the type of editor to show for editing the given page.\n     * Defaults based upon the current content of the page otherwise will fall back\n     * to system default but will take a requested type (if provided) if permissions allow.\n     */\n    protected function getEditorType(Page $page): PageEditorType\n    {\n        $editorType = PageEditorType::forPage($page) ?: PageEditorType::getSystemDefault();\n\n        // Use the requested editor if valid and if we have permission\n        $requestedType = PageEditorType::fromRequestValue($this->requestedEditor);\n        if ($requestedType && userCan(Permission::EditorChange)) {\n            $editorType = $requestedType;\n        }\n\n        return $editorType;\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/PageEditorType.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools;\n\nuse BookStack\\Entities\\Models\\Page;\n\nenum PageEditorType: string\n{\n    case WysiwygTinymce = 'wysiwyg';\n    case WysiwygLexical = 'wysiwyg2024';\n    case Markdown = 'markdown';\n\n    public function isHtmlBased(): bool\n    {\n        return match ($this) {\n            self::WysiwygTinymce, self::WysiwygLexical => true,\n            self::Markdown => false,\n        };\n    }\n\n    public static function fromRequestValue(string $value): static|null\n    {\n        $editor = explode('-', $value)[0];\n        return static::tryFrom($editor);\n    }\n\n    public static function forPage(Page $page): static|null\n    {\n        return static::tryFrom($page->editor);\n    }\n\n    public static function getSystemDefault(): static\n    {\n        $setting = setting('app-editor');\n        return static::tryFrom($setting) ?? static::WysiwygTinymce;\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/PageIncludeContent.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools;\n\nuse BookStack\\Util\\HtmlDocument;\nuse DOMNode;\n\nclass PageIncludeContent\n{\n    protected static array $topLevelTags = ['table', 'ul', 'ol', 'pre'];\n\n    /**\n     * @param DOMNode[] $contents\n     * @param bool $isInline\n     */\n    public function __construct(\n        protected array $contents,\n        protected bool $isInline,\n    ) {\n    }\n\n    public static function fromHtmlAndTag(string $html, PageIncludeTag $tag): self\n    {\n        if (empty($html)) {\n            return new self([], true);\n        }\n\n        $doc = new HtmlDocument($html);\n\n        $sectionId = $tag->getSectionId();\n        if (!$sectionId) {\n            $contents = [...$doc->getBodyChildren()];\n            return new self($contents, false);\n        }\n\n        $section = $doc->getElementById($sectionId);\n        if (!$section) {\n            return new self([], true);\n        }\n\n        $isTopLevel = in_array(strtolower($section->nodeName), static::$topLevelTags);\n        $contents = $isTopLevel ? [$section] : [...$section->childNodes];\n        return new self($contents, !$isTopLevel);\n    }\n\n    public static function fromInlineHtml(string $html): self\n    {\n        if (empty($html)) {\n            return new self([], true);\n        }\n\n        $doc = new HtmlDocument($html);\n\n        return new self([...$doc->getBodyChildren()], true);\n    }\n\n    public function isInline(): bool\n    {\n        return $this->isInline;\n    }\n\n    public function isEmpty(): bool\n    {\n        return empty($this->contents);\n    }\n\n    /**\n     * @return DOMNode[]\n     */\n    public function toDomNodes(): array\n    {\n        return $this->contents;\n    }\n\n    public function toHtml(): string\n    {\n        $html = '';\n\n        foreach ($this->contents as $content) {\n            $html .= $content->ownerDocument->saveHTML($content);\n        }\n\n        return $html;\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/PageIncludeParser.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools;\n\nuse BookStack\\Util\\HtmlDocument;\nuse Closure;\nuse DOMDocument;\nuse DOMElement;\nuse DOMNode;\n\nclass PageIncludeParser\n{\n    protected static string $includeTagRegex = \"/{{@\\s?([0-9].*?)}}/\";\n\n    /**\n     * Nodes to clean up and remove if left empty after a parsing operation.\n     * @var DOMNode[]\n     */\n    protected array $toCleanup = [];\n\n    /**\n     * @param Closure(PageIncludeTag $tag): PageContent $pageContentForId\n     */\n    public function __construct(\n        protected HtmlDocument $doc,\n        protected Closure $pageContentForId,\n    ) {\n    }\n\n    /**\n     * Parse out the include tags.\n     * Returns the count of new content DOM nodes added to the document.\n     */\n    public function parse(): int\n    {\n        $nodesAdded = 0;\n        $tags = $this->locateAndIsolateIncludeTags();\n\n        foreach ($tags as $tag) {\n            /** @var PageIncludeContent $content */\n            $content = $this->pageContentForId->call($this, $tag);\n\n            if (!$content->isInline()) {\n                $parentP = $this->getParentParagraph($tag->domNode);\n                $isWithinParentP = $parentP === $tag->domNode->parentNode;\n                if ($parentP && $isWithinParentP) {\n                    $this->splitNodeAtChildNode($tag->domNode->parentNode, $tag->domNode);\n                } else if ($parentP) {\n                    $this->moveTagNodeToBesideParent($tag, $parentP);\n                }\n            }\n\n            $replacementNodes = $content->toDomNodes();\n            $nodesAdded += count($replacementNodes);\n            $this->replaceNodeWithNodes($tag->domNode, $replacementNodes);\n        }\n\n        $this->cleanup();\n\n        return $nodesAdded;\n    }\n\n    /**\n     * Locate include tags within the given document, isolating them to their\n     * own nodes in the DOM for future targeted manipulation.\n     * @return PageIncludeTag[]\n     */\n    protected function locateAndIsolateIncludeTags(): array\n    {\n        $includeHosts = $this->doc->queryXPath(\"//*[text()[contains(., '{{@')]]\");\n        $includeTags = [];\n\n        /** @var DOMNode $node */\n        foreach ($includeHosts as $node) {\n            /** @var DOMNode $childNode */\n            foreach ($node->childNodes as $childNode) {\n                if ($childNode->nodeName === '#text') {\n                    array_push($includeTags, ...$this->splitTextNodesAtTags($childNode));\n                }\n            }\n        }\n\n        return $includeTags;\n    }\n\n    /**\n     * Takes a text DOMNode and splits its text content at include tags\n     * into multiple text nodes within the original parent.\n     * Returns found PageIncludeTag references.\n     * @return PageIncludeTag[]\n     */\n    protected function splitTextNodesAtTags(DOMNode $textNode): array\n    {\n        $includeTags = [];\n        $text = $textNode->textContent;\n        preg_match_all(static::$includeTagRegex, $text, $matches, PREG_OFFSET_CAPTURE);\n\n        $currentOffset = 0;\n        foreach ($matches[0] as $index => $fullTagMatch) {\n            $tagOuterContent = $fullTagMatch[0];\n            $tagInnerContent = $matches[1][$index][0];\n            $tagStartOffset = $fullTagMatch[1];\n\n            if ($currentOffset < $tagStartOffset) {\n                $previousText = substr($text, $currentOffset, $tagStartOffset - $currentOffset);\n                $textNode->parentNode->insertBefore($this->doc->createTextNode($previousText), $textNode);\n            }\n\n            $node = $textNode->parentNode->insertBefore($this->doc->createTextNode($tagOuterContent), $textNode);\n            $includeTags[] = new PageIncludeTag($tagInnerContent, $node);\n            $currentOffset = $tagStartOffset + strlen($tagOuterContent);\n        }\n\n        if ($currentOffset > 0) {\n            $textNode->textContent = substr($text, $currentOffset);\n        }\n\n        return $includeTags;\n    }\n\n    /**\n     * Replace the given node with all those in $replacements\n     * @param DOMNode[] $replacements\n     */\n    protected function replaceNodeWithNodes(DOMNode $toReplace, array $replacements): void\n    {\n        /** @var DOMDocument $targetDoc */\n        $targetDoc = $toReplace->ownerDocument;\n\n        foreach ($replacements as $replacement) {\n            if ($replacement->ownerDocument !== $targetDoc) {\n                $replacement = $targetDoc->importNode($replacement, true);\n            }\n\n            $toReplace->parentNode->insertBefore($replacement, $toReplace);\n        }\n\n        $toReplace->parentNode->removeChild($toReplace);\n    }\n\n    /**\n     * Move a tag node to become a sibling of the given parent.\n     * Will attempt to guess a position based upon the tag content within the parent.\n     */\n    protected function moveTagNodeToBesideParent(PageIncludeTag $tag, DOMNode $parent): void\n    {\n        $parentText = $parent->textContent;\n        $tagPos = strpos($parentText, $tag->tagContent);\n        $before = $tagPos < (strlen($parentText) / 2);\n        $this->toCleanup[] = $tag->domNode->parentNode;\n\n        if ($before) {\n            $parent->parentNode->insertBefore($tag->domNode, $parent);\n        } else {\n            $parent->parentNode->insertBefore($tag->domNode, $parent->nextSibling);\n        }\n    }\n\n    /**\n     * Splits the given $parentNode at the location of the $domNode within it.\n     * Attempts to replicate the original $parentNode, moving some of their parent\n     * children in where needed, before adding the $domNode between.\n     */\n    protected function splitNodeAtChildNode(DOMElement $parentNode, DOMNode $domNode): void\n    {\n        $children = [...$parentNode->childNodes];\n        $splitPos = array_search($domNode, $children, true);\n        if ($splitPos === false) {\n            $splitPos = count($children) - 1;\n        }\n\n        $parentClone = $parentNode->cloneNode();\n        if (!($parentClone instanceof DOMElement)) {\n            return;\n        }\n\n        $parentNode->parentNode->insertBefore($parentClone, $parentNode);\n        $parentClone->removeAttribute('id');\n\n        for ($i = 0; $i < $splitPos; $i++) {\n            /** @var DOMNode $child */\n            $child = $children[$i];\n            $parentClone->appendChild($child);\n        }\n\n        $parentNode->parentNode->insertBefore($domNode, $parentNode);\n\n        $this->toCleanup[] = $parentNode;\n        $this->toCleanup[] = $parentClone;\n    }\n\n    /**\n     * Get the parent paragraph of the given node, if existing.\n     */\n    protected function getParentParagraph(DOMNode $parent): ?DOMNode\n    {\n        do {\n            if (strtolower($parent->nodeName) === 'p') {\n                return $parent;\n            }\n\n            $parent = $parent->parentNode;\n        } while ($parent !== null);\n\n        return null;\n    }\n\n    /**\n     * Clean up after a parse operation.\n     * Removes stranded elements we may have left during the parse.\n     */\n    protected function cleanup(): void\n    {\n        foreach ($this->toCleanup as $element) {\n            $element->normalize();\n            while ($element->parentNode && !$element->hasChildNodes()) {\n                $parent = $element->parentNode;\n                $parent->removeChild($element);\n                $element = $parent;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/PageIncludeTag.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools;\n\nuse DOMNode;\n\nclass PageIncludeTag\n{\n    public function __construct(\n        public string $tagContent,\n        public DOMNode $domNode,\n    ) {\n    }\n\n    /**\n     * Get the page ID that this tag references.\n     */\n    public function getPageId(): int\n    {\n        return intval(trim(explode('#', $this->tagContent, 2)[0]));\n    }\n\n    /**\n     * Get the section ID that this tag references (if any)\n     */\n    public function getSectionId(): string\n    {\n        return trim(explode('#', $this->tagContent, 2)[1] ?? '');\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/ParentChanger.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools;\n\nuse BookStack\\Entities\\Models\\BookChild;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\References\\ReferenceUpdater;\n\nclass ParentChanger\n{\n    public function __construct(\n        protected SlugGenerator $slugGenerator,\n        protected ReferenceUpdater $referenceUpdater\n    ) {\n    }\n\n    /**\n     * Change the parent book of a chapter or page.\n     */\n    public function changeBook(BookChild $child, int $newBookId): void\n    {\n        $oldUrl = $child->getUrl();\n\n        $child->book_id = $newBookId;\n        $child->unsetRelation('book');\n        $this->slugGenerator->regenerateForEntity($child);\n        $child->save();\n\n        if ($oldUrl !== $child->getUrl()) {\n            $this->referenceUpdater->updateEntityReferences($child, $oldUrl);\n        }\n\n        // Update all child pages if a chapter\n        if ($child instanceof Chapter) {\n            foreach ($child->pages()->withTrashed()->get() as $page) {\n                $this->changeBook($page, $newBookId);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/PermissionsUpdater.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Facades\\Activity;\nuse BookStack\\Permissions\\Models\\EntityPermission;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Http\\Request;\n\nclass PermissionsUpdater\n{\n    /**\n     * Update an entities permissions from a permission form submit request.\n     */\n    public function updateFromPermissionsForm(Entity $entity, Request $request): void\n    {\n        $permissions = $request->get('permissions', null);\n        $ownerId = $request->get('owned_by', null);\n\n        $entity->permissions()->delete();\n\n        if (!is_null($permissions)) {\n            $entityPermissionData = $this->formatPermissionsFromRequestToEntityPermissions($permissions);\n            $entity->permissions()->createMany($entityPermissionData);\n        }\n\n        if (!is_null($ownerId)) {\n            $this->updateOwnerFromId($entity, intval($ownerId));\n        }\n\n        $entity->save();\n        $entity->rebuildPermissions();\n\n        Activity::add(ActivityType::PERMISSIONS_UPDATE, $entity);\n    }\n\n    /**\n     * Update permissions from API request data.\n     */\n    public function updateFromApiRequestData(Entity $entity, array $data): void\n    {\n        if (isset($data['role_permissions'])) {\n            $entity->permissions()->where('role_id', '!=', 0)->delete();\n            $rolePermissionData = $this->formatPermissionsFromApiRequestToEntityPermissions($data['role_permissions'] ?? [], false);\n            $entity->permissions()->createMany($rolePermissionData);\n        }\n\n        if (array_key_exists('fallback_permissions', $data)) {\n            $entity->permissions()->where('role_id', '=', 0)->delete();\n        }\n\n        if (isset($data['fallback_permissions']['inheriting']) && $data['fallback_permissions']['inheriting'] !== true) {\n            $fallbackData = $data['fallback_permissions'];\n            $fallbackData['role_id'] = 0;\n            $rolePermissionData = $this->formatPermissionsFromApiRequestToEntityPermissions([$fallbackData], true);\n            $entity->permissions()->createMany($rolePermissionData);\n        }\n\n        if (isset($data['owner_id'])) {\n            $this->updateOwnerFromId($entity, intval($data['owner_id']));\n        }\n\n        $entity->save();\n        $entity->rebuildPermissions();\n\n        Activity::add(ActivityType::PERMISSIONS_UPDATE, $entity);\n    }\n\n    /**\n     * Update the owner of the given entity.\n     * Checks the user exists in the system first.\n     * Does not save the model, just updates it.\n     */\n    protected function updateOwnerFromId(Entity $entity, int $newOwnerId): void\n    {\n        $newOwner = User::query()->find($newOwnerId);\n        if (!is_null($newOwner)) {\n            $entity->owned_by = $newOwner->id;\n        }\n    }\n\n    /**\n     * Format permissions provided from a permission form to be EntityPermission data.\n     */\n    protected function formatPermissionsFromRequestToEntityPermissions(array $permissions): array\n    {\n        $formatted = [];\n\n        foreach ($permissions as $roleId => $info) {\n            $entityPermissionData = ['role_id' => $roleId];\n            foreach (Permission::genericForEntity() as $permission) {\n                $permName = $permission->value;\n                $entityPermissionData[$permName] = (($info[$permName] ?? false) === \"true\");\n            }\n            $formatted[] = $entityPermissionData;\n        }\n\n        return $this->filterEntityPermissionDataUponRole($formatted, true);\n    }\n\n    protected function formatPermissionsFromApiRequestToEntityPermissions(array $permissions, bool $allowFallback): array\n    {\n        $formatted = [];\n\n        foreach ($permissions as $requestPermissionData) {\n            $entityPermissionData = ['role_id' => $requestPermissionData['role_id']];\n            foreach (Permission::genericForEntity() as $permission) {\n                $permName = $permission->value;\n                $entityPermissionData[$permName] = boolval($requestPermissionData[$permName] ?? false);\n            }\n            $formatted[] = $entityPermissionData;\n        }\n\n        return $this->filterEntityPermissionDataUponRole($formatted, $allowFallback);\n    }\n\n    protected function filterEntityPermissionDataUponRole(array $entityPermissionData, bool $allowFallback): array\n    {\n        $roleIds = [];\n        foreach ($entityPermissionData as $permissionEntry) {\n            $roleIds[] = intval($permissionEntry['role_id']);\n        }\n\n        $actualRoleIds = array_unique(array_values(array_filter($roleIds)));\n        $rolesById = Role::query()->whereIn('id', $actualRoleIds)->get('id')->keyBy('id');\n\n        return array_values(array_filter($entityPermissionData, function ($data) use ($rolesById, $allowFallback) {\n            if (intval($data['role_id']) === 0) {\n                return $allowFallback;\n            }\n\n            return $rolesById->has($data['role_id']);\n        }));\n    }\n\n    /**\n     * Copy down the permissions of the given shelf to all child books.\n     */\n    public function updateBookPermissionsFromShelf(Bookshelf $shelf, $checkUserPermissions = true): int\n    {\n        $shelfPermissions = $shelf->permissions()->get(['role_id', 'view', 'create', 'update', 'delete'])->toArray();\n        $shelfBooks = $shelf->books()->get(['id', 'owned_by']);\n        $updatedBookCount = 0;\n\n        /** @var Book $book */\n        foreach ($shelfBooks as $book) {\n            if ($checkUserPermissions && !userCan(Permission::RestrictionsManage, $book)) {\n                continue;\n            }\n            $book->permissions()->delete();\n            $book->permissions()->createMany($shelfPermissions);\n            $book->rebuildPermissions();\n            $updatedBookCount++;\n        }\n\n        return $updatedBookCount;\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/ShelfContext.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Entities\\Queries\\BookshelfQueries;\n\nclass ShelfContext\n{\n    protected string $KEY_SHELF_CONTEXT_ID = 'context_bookshelf_id';\n\n    public function __construct(\n        protected BookshelfQueries $shelfQueries,\n    ) {\n    }\n\n    /**\n     * Get the current bookshelf context for the given book.\n     */\n    public function getContextualShelfForBook(Book $book): ?Bookshelf\n    {\n        $contextBookshelfId = session()->get($this->KEY_SHELF_CONTEXT_ID, null);\n\n        if (!is_int($contextBookshelfId)) {\n            return null;\n        }\n\n        $shelf = $this->shelfQueries->findVisibleById($contextBookshelfId);\n        $shelfContainsBook = $shelf && $shelf->contains($book);\n\n        return $shelfContainsBook ? $shelf : null;\n    }\n\n    /**\n     * Store the current contextual shelf ID.\n     */\n    public function setShelfContext(int $shelfId): void\n    {\n        session()->put($this->KEY_SHELF_CONTEXT_ID, $shelfId);\n    }\n\n    /**\n     * Clear the session stored shelf context id.\n     */\n    public function clearShelfContext(): void\n    {\n        session()->forget($this->KEY_SHELF_CONTEXT_ID);\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/SiblingFetcher.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools;\n\nuse BookStack\\Entities\\EntityProvider;\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse Illuminate\\Support\\Collection;\n\nclass SiblingFetcher\n{\n    public function __construct(\n        protected EntityQueries $queries,\n        protected ShelfContext $shelfContext,\n    ) {\n    }\n\n    /**\n     * Search among the siblings of the entity of given type and id.\n     */\n    public function fetch(string $entityType, int $entityId): Collection\n    {\n        $entity = (new EntityProvider())->get($entityType)->visible()->findOrFail($entityId);\n        $entities = [];\n\n        // Page in chapter\n        if ($entity instanceof Page && $entity->chapter) {\n            $entities = $entity->chapter->getVisiblePages();\n        }\n\n        // Page in book or chapter\n        if (($entity instanceof Page && !$entity->chapter) || $entity instanceof Chapter) {\n            $entities = $entity->book->getDirectVisibleChildren();\n        }\n\n        // Book\n        // Gets just the books in a shelf if shelf is in context\n        if ($entity instanceof Book) {\n            $contextShelf = $this->shelfContext->getContextualShelfForBook($entity);\n            if ($contextShelf) {\n                $entities = $contextShelf->visibleBooks()->get();\n            } else {\n                $entities = $this->queries->books->visibleForList()->orderBy('name', 'asc')->get();\n            }\n        }\n\n        // Shelf\n        if ($entity instanceof Bookshelf) {\n            $entities = $this->queries->shelves->visibleForList()->orderBy('name', 'asc')->get();\n        }\n\n        return $entities;\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/SlugGenerator.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools;\n\nuse BookStack\\App\\Model;\nuse BookStack\\App\\SluggableInterface;\nuse BookStack\\Entities\\Models\\BookChild;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Support\\Str;\n\nclass SlugGenerator\n{\n    /**\n     * Generate a fresh slug for the given item.\n     * The slug will be generated so that it doesn't conflict within the same parent item.\n     */\n    public function generate(SluggableInterface&Model $model, string $slugSource): string\n    {\n        $slug = $this->formatNameAsSlug($slugSource);\n        while ($this->slugInUse($slug, $model)) {\n            $slug .= '-' . Str::random(3);\n        }\n\n        return $slug;\n    }\n\n    /**\n     * Regenerate the slug for the given entity.\n     */\n    public function regenerateForEntity(Entity $entity): string\n    {\n        $entity->slug = $this->generate($entity, $entity->name);\n\n        return $entity->slug;\n    }\n\n    /**\n     * Regenerate the slug for a user.\n     */\n    public function regenerateForUser(User $user): string\n    {\n        $user->slug = $this->generate($user, $user->name);\n\n        return $user->slug;\n    }\n\n    /**\n     * Format a name as a URL slug.\n     */\n    protected function formatNameAsSlug(string $name): string\n    {\n        $slug = Str::slug($name);\n        if ($slug === '') {\n            $slug = substr(md5(rand(1, 500)), 0, 5);\n        }\n\n        return $slug;\n    }\n\n    /**\n     * Check if a slug is already in-use for this\n     * type of model within the same parent.\n     */\n    protected function slugInUse(string $slug, SluggableInterface&Model $model): bool\n    {\n        $query = $model->newQuery()->where('slug', '=', $slug);\n\n        if ($model instanceof BookChild) {\n            $query->where('book_id', '=', $model->book_id);\n        }\n\n        if ($model->id) {\n            $query->where('id', '!=', $model->id);\n        }\n\n        return $query->count() > 0;\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/SlugHistory.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\BookChild;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\EntityTable;\nuse BookStack\\Entities\\Models\\SlugHistory as SlugHistoryModel;\nuse BookStack\\Permissions\\PermissionApplicator;\nuse Illuminate\\Support\\Facades\\DB;\n\nclass SlugHistory\n{\n    public function __construct(\n        protected PermissionApplicator $permissions,\n    ) {\n    }\n\n    /**\n     * Record the current slugs for the given entity.\n     */\n    public function recordForEntity(Entity $entity): void\n    {\n        if (!$entity->id || !$entity->slug) {\n            return;\n        }\n\n        $parentSlug = null;\n        if ($entity instanceof BookChild) {\n            $parentSlug = $entity->book()->first()?->slug;\n        }\n\n        $latest = $this->getLatestEntryForEntity($entity);\n        if ($latest && $latest->slug === $entity->slug && $latest->parent_slug === $parentSlug) {\n            return;\n        }\n\n        $info = [\n            'sluggable_type' => $entity->getMorphClass(),\n            'sluggable_id'   => $entity->id,\n            'slug'           => $entity->slug,\n            'parent_slug'    => $parentSlug,\n        ];\n\n        $entry = new SlugHistoryModel();\n        $entry->forceFill($info);\n        $entry->save();\n\n        if ($entity instanceof Book) {\n            $this->recordForBookChildren($entity);\n        }\n    }\n\n    protected function recordForBookChildren(Book $book): void\n    {\n        $query = EntityTable::query()\n            ->select(['type', 'id', 'slug', DB::raw(\"'{$book->slug}' as parent_slug\"), DB::raw('now() as created_at'), DB::raw('now() as updated_at')])\n            ->where('book_id', '=', $book->id)\n            ->whereNotNull('book_id');\n\n        SlugHistoryModel::query()->insertUsing(\n            ['sluggable_type', 'sluggable_id', 'slug', 'parent_slug', 'created_at', 'updated_at'],\n            $query\n        );\n    }\n\n    /**\n     * Find the latest visible entry for an entity which uses the given slug(s) in the history.\n     */\n    public function lookupEntityIdUsingSlugs(string $type, string $slug, string $parentSlug = ''): ?int\n    {\n        $query = SlugHistoryModel::query()\n            ->where('sluggable_type', '=', $type)\n            ->where('slug', '=', $slug);\n\n        if ($parentSlug) {\n            $query->where('parent_slug', '=', $parentSlug);\n        }\n\n        $query = $this->permissions->restrictEntityRelationQuery($query, 'slug_history', 'sluggable_id', 'sluggable_type');\n\n        /** @var SlugHistoryModel|null $result */\n        $result = $query->orderBy('created_at', 'desc')->first();\n\n        return $result?->sluggable_id;\n    }\n\n    protected function getLatestEntryForEntity(Entity $entity): SlugHistoryModel|null\n    {\n        return SlugHistoryModel::query()\n            ->where('sluggable_type', '=', $entity->getMorphClass())\n            ->where('sluggable_id', '=', $entity->id)\n            ->orderBy('created_at', 'desc')\n            ->first();\n    }\n}\n"
  },
  {
    "path": "app/Entities/Tools/TrashCan.php",
    "content": "<?php\n\nnamespace BookStack\\Entities\\Tools;\n\nuse BookStack\\Entities\\EntityProvider;\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\EntityContainerData;\nuse BookStack\\Entities\\Models\\HasCoverInterface;\nuse BookStack\\Entities\\Models\\Deletion;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse BookStack\\Exceptions\\NotifyException;\nuse BookStack\\Facades\\Activity;\nuse BookStack\\Uploads\\AttachmentService;\nuse BookStack\\Uploads\\Image;\nuse BookStack\\Uploads\\ImageService;\nuse BookStack\\Util\\DatabaseTransaction;\nuse Exception;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\n\nclass TrashCan\n{\n    public function __construct(\n        protected EntityQueries $queries,\n    ) {\n    }\n\n    /**\n     * Send a shelf to the recycle bin.\n     *\n     * @throws NotifyException\n     */\n    public function softDestroyShelf(Bookshelf $shelf)\n    {\n        $this->ensureDeletable($shelf);\n        Deletion::createForEntity($shelf);\n        $shelf->delete();\n    }\n\n    /**\n     * Send a book to the recycle bin.\n     *\n     * @throws Exception\n     */\n    public function softDestroyBook(Book $book)\n    {\n        $this->ensureDeletable($book);\n        Deletion::createForEntity($book);\n\n        foreach ($book->pages as $page) {\n            $this->softDestroyPage($page, false);\n        }\n\n        foreach ($book->chapters as $chapter) {\n            $this->softDestroyChapter($chapter, false);\n        }\n\n        $book->delete();\n    }\n\n    /**\n     * Send a chapter to the recycle bin.\n     *\n     * @throws Exception\n     */\n    public function softDestroyChapter(Chapter $chapter, bool $recordDelete = true)\n    {\n        if ($recordDelete) {\n            $this->ensureDeletable($chapter);\n            Deletion::createForEntity($chapter);\n        }\n\n        if (count($chapter->pages) > 0) {\n            foreach ($chapter->pages as $page) {\n                $this->softDestroyPage($page, false);\n            }\n        }\n\n        $chapter->delete();\n    }\n\n    /**\n     * Send a page to the recycle bin.\n     *\n     * @throws Exception\n     */\n    public function softDestroyPage(Page $page, bool $recordDelete = true)\n    {\n        if ($recordDelete) {\n            $this->ensureDeletable($page);\n            Deletion::createForEntity($page);\n        }\n\n        $page->delete();\n    }\n\n    /**\n     * Ensure the given entity is deletable.\n     * Is not for permissions, but logical conditions within the application.\n     * Will throw if not deletable.\n     *\n     * @throws NotifyException\n     */\n    protected function ensureDeletable(Entity $entity): void\n    {\n        $customHomeId = intval(explode(':', setting('app-homepage', '0:'))[0]);\n        $customHomeActive = setting('app-homepage-type') === 'page';\n        $removeCustomHome = false;\n\n        // Check custom homepage usage for pages\n        if ($entity instanceof Page && $entity->id === $customHomeId) {\n            if ($customHomeActive) {\n                throw new NotifyException(trans('errors.page_custom_home_deletion'), $entity->getUrl());\n            }\n            $removeCustomHome = true;\n        }\n\n        // Check custom homepage usage within chapters or books\n        if ($entity instanceof Chapter || $entity instanceof Book) {\n            if ($entity->pages()->where('id', '=', $customHomeId)->exists()) {\n                if ($customHomeActive) {\n                    throw new NotifyException(trans('errors.page_custom_home_deletion'), $entity->getUrl());\n                }\n                $removeCustomHome = true;\n            }\n        }\n\n        if ($removeCustomHome) {\n            setting()->remove('app-homepage');\n        }\n    }\n\n    /**\n     * Remove a bookshelf from the system.\n     *\n     * @throws Exception\n     */\n    protected function destroyShelf(Bookshelf $shelf): int\n    {\n        $this->destroyCommonRelations($shelf);\n        $shelf->books()->detach();\n        $shelf->forceDelete();\n\n        return 1;\n    }\n\n    /**\n     * Remove a book from the system.\n     * Destroys any child chapters and pages.\n     *\n     * @throws Exception\n     */\n    protected function destroyBook(Book $book): int\n    {\n        $count = 0;\n        $pages = $book->pages()->withTrashed()->get();\n        foreach ($pages as $page) {\n            $this->destroyPage($page);\n            $count++;\n        }\n\n        $chapters = $book->chapters()->withTrashed()->get();\n        foreach ($chapters as $chapter) {\n            $this->destroyChapter($chapter);\n            $count++;\n        }\n\n        $this->destroyCommonRelations($book);\n        $book->shelves()->detach();\n        $book->forceDelete();\n\n        return $count + 1;\n    }\n\n    /**\n     * Remove a chapter from the system.\n     * Destroys all pages within.\n     *\n     * @throws Exception\n     */\n    protected function destroyChapter(Chapter $chapter): int\n    {\n        $count = 0;\n        $pages = $chapter->pages()->withTrashed()->get();\n        foreach ($pages as $page) {\n            $this->destroyPage($page);\n            $count++;\n        }\n\n        $this->destroyCommonRelations($chapter);\n        $chapter->forceDelete();\n\n        return $count + 1;\n    }\n\n    /**\n     * Remove a page from the system.\n     *\n     * @throws Exception\n     */\n    protected function destroyPage(Page $page): int\n    {\n        $this->destroyCommonRelations($page);\n        $page->allRevisions()->delete();\n\n        // Delete Attached Files\n        $attachmentService = app()->make(AttachmentService::class);\n        foreach ($page->attachments as $attachment) {\n            $attachmentService->deleteFile($attachment);\n        }\n\n        // Remove use as a template\n        EntityContainerData::query()\n            ->where('default_template_id', '=', $page->id)\n            ->update(['default_template_id' => null]);\n\n        // Nullify uploaded image relations\n        Image::query()\n            ->whereIn('type', ['gallery', 'drawio'])\n            ->where('uploaded_to', '=', $page->id)\n            ->update(['uploaded_to' => null]);\n\n        $page->forceDelete();\n\n        return 1;\n    }\n\n    /**\n     * Get the total counts of those that have been trashed\n     * but not yet fully deleted (In recycle bin).\n     */\n    public function getTrashedCounts(): array\n    {\n        $counts = [];\n\n        foreach ((new EntityProvider())->all() as $key => $instance) {\n            /** @var Builder<Entity> $query */\n            $query = $instance->newQuery();\n            $counts[$key] = $query->onlyTrashed()->count();\n        }\n\n        return $counts;\n    }\n\n    /**\n     * Destroy all items that have pending deletions.\n     *\n     * @throws Exception\n     */\n    public function empty(): int\n    {\n        $deletions = Deletion::all();\n        $deleteCount = 0;\n        foreach ($deletions as $deletion) {\n            $deleteCount += $this->destroyFromDeletion($deletion);\n        }\n\n        return $deleteCount;\n    }\n\n    /**\n     * Destroy an element from the given deletion model.\n     *\n     * @throws Exception\n     */\n    public function destroyFromDeletion(Deletion $deletion): int\n    {\n        // We directly load the deletable element here just to ensure it still\n        // exists in the event it has already been destroyed during this request.\n        $entity = $deletion->deletable()->first();\n        $count = 0;\n        if ($entity instanceof Entity) {\n            $count = $this->destroyEntity($entity);\n        }\n        $deletion->delete();\n\n        return $count;\n    }\n\n    /**\n     * Restore the content within the given deletion.\n     *\n     * @throws Exception\n     */\n    public function restoreFromDeletion(Deletion $deletion): int\n    {\n        $shouldRestore = true;\n        $restoreCount = 0;\n\n        if ($deletion->deletable instanceof Entity) {\n            $parent = $deletion->deletable->getParent();\n            if ($parent && $parent->trashed()) {\n                $shouldRestore = false;\n            }\n        }\n\n        if ($deletion->deletable instanceof Entity && $shouldRestore) {\n            $restoreCount = $this->restoreEntity($deletion->deletable);\n        }\n\n        $deletion->delete();\n\n        return $restoreCount;\n    }\n\n    /**\n     * Automatically clear old content from the recycle bin\n     * depending on the configured lifetime.\n     * Returns the total number of deleted elements.\n     *\n     * @throws Exception\n     */\n    public function autoClearOld(): int\n    {\n        $lifetime = intval(config('app.recycle_bin_lifetime'));\n        if ($lifetime < 0) {\n            return 0;\n        }\n\n        $clearBeforeDate = Carbon::now()->addSeconds(10)->subDays($lifetime);\n        $deleteCount = 0;\n\n        $deletionsToRemove = Deletion::query()->where('created_at', '<', $clearBeforeDate)->get();\n        foreach ($deletionsToRemove as $deletion) {\n            $deleteCount += $this->destroyFromDeletion($deletion);\n        }\n\n        return $deleteCount;\n    }\n\n    /**\n     * Restore an entity so it is essentially un-deleted.\n     * Deletions on restored child elements will be removed during this restoration.\n     */\n    protected function restoreEntity(Entity $entity): int\n    {\n        $count = 1;\n        $entity->restore();\n\n        $restoreAction = function ($entity) use (&$count) {\n            if ($entity->deletions_count > 0) {\n                $entity->deletions()->delete();\n            }\n\n            $entity->restore();\n            $count++;\n        };\n\n        if ($entity instanceof Chapter || $entity instanceof Book) {\n            $entity->pages()->withTrashed()->withCount('deletions')->get()->each($restoreAction);\n        }\n\n        if ($entity instanceof Book) {\n            $entity->chapters()->withTrashed()->withCount('deletions')->get()->each($restoreAction);\n        }\n\n        return $count;\n    }\n\n    /**\n     * Destroy the given entity.\n     * Returns the number of total entities destroyed in the operation.\n     *\n     * @throws Exception\n     */\n    public function destroyEntity(Entity $entity): int\n    {\n        $result = (new DatabaseTransaction(function () use ($entity) {\n            if ($entity instanceof Page) {\n                return $this->destroyPage($entity);\n            } else if ($entity instanceof Chapter) {\n                return $this->destroyChapter($entity);\n            } else if ($entity instanceof Book) {\n                return $this->destroyBook($entity);\n            } else if ($entity instanceof Bookshelf) {\n                return $this->destroyShelf($entity);\n            }\n            return null;\n        }))->run();\n\n        return $result ?? 0;\n    }\n\n    /**\n     * Update entity relations to remove or update outstanding connections.\n     */\n    protected function destroyCommonRelations(Entity $entity): void\n    {\n        Activity::removeEntity($entity);\n        $entity->views()->delete();\n        $entity->permissions()->delete();\n        $entity->tags()->delete();\n        $entity->comments()->delete();\n        $entity->jointPermissions()->delete();\n        $entity->searchTerms()->delete();\n        $entity->deletions()->delete();\n        $entity->favourites()->delete();\n        $entity->watches()->delete();\n        $entity->referencesTo()->delete();\n        $entity->referencesFrom()->delete();\n        $entity->slugHistory()->delete();\n\n        if ($entity instanceof HasCoverInterface && $entity->coverInfo()->exists()) {\n            $imageService = app()->make(ImageService::class);\n            $imageService->destroy($entity->coverInfo()->getImage());\n        }\n\n        $entity->relatedData()->delete();\n    }\n}\n"
  },
  {
    "path": "app/Exceptions/ApiAuthException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nuse Symfony\\Component\\HttpKernel\\Exception\\HttpExceptionInterface;\n\nclass ApiAuthException extends \\Exception implements HttpExceptionInterface\n{\n    protected int $status;\n\n    public function __construct(string $message, int $statusCode = 401)\n    {\n        $this->status = $statusCode;\n        parent::__construct($message, $statusCode);\n    }\n\n    public function getStatusCode(): int\n    {\n        return $this->status;\n    }\n\n    public function getHeaders(): array\n    {\n        return [];\n    }\n}\n"
  },
  {
    "path": "app/Exceptions/BookStackExceptionHandlerPage.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nuse BookStack\\App\\AppVersion;\nuse Illuminate\\Contracts\\Foundation\\ExceptionRenderer;\n\nclass BookStackExceptionHandlerPage implements ExceptionRenderer\n{\n    public function render($throwable)\n    {\n        return view('errors.debug', [\n            'error'       => $throwable->getMessage(),\n            'errorClass'  => get_class($throwable),\n            'trace'       => $throwable->getTraceAsString(),\n            'environment' => $this->getEnvironment(),\n        ])->render();\n    }\n\n    protected function safeReturn(callable $callback, $default = null)\n    {\n        try {\n            return $callback();\n        } catch (\\Exception $e) {\n            return $default;\n        }\n    }\n\n    protected function getEnvironment(): array\n    {\n        return [\n            'PHP Version'       => phpversion(),\n            'BookStack Version' => $this->safeReturn(function () {\n                return AppVersion::get();\n            }, 'unknown'),\n            'Theme Configured' => $this->safeReturn(function () {\n                return config('view.theme');\n            }) ?? 'None',\n        ];\n    }\n}\n"
  },
  {
    "path": "app/Exceptions/ConfirmationEmailException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nclass ConfirmationEmailException extends NotifyException\n{\n}\n"
  },
  {
    "path": "app/Exceptions/FileUploadException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nclass FileUploadException extends PrettyException\n{\n}\n"
  },
  {
    "path": "app/Exceptions/Handler.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nuse Illuminate\\Auth\\AuthenticationException;\nuse Illuminate\\Database\\Eloquent\\ModelNotFoundException;\nuse Illuminate\\Foundation\\Exceptions\\Handler as ExceptionHandler;\nuse Illuminate\\Http\\Exceptions\\PostTooLargeException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Validation\\ValidationException;\nuse Symfony\\Component\\ErrorHandler\\Error\\FatalError;\nuse Symfony\\Component\\HttpFoundation\\Response as SymfonyResponse;\nuse Symfony\\Component\\HttpKernel\\Exception\\HttpExceptionInterface;\nuse Throwable;\n\nclass Handler extends ExceptionHandler\n{\n    /**\n     * A list of the exception types that are not reported.\n     *\n     * @var array<int, class-string<Throwable>>\n     */\n    protected $dontReport = [\n        NotFoundException::class,\n        StoppedAuthenticationException::class,\n    ];\n\n    /**\n     * A list of the inputs that are never flashed to the session on validation exceptions.\n     *\n     * @var array<int, string>\n     */\n    protected $dontFlash = [\n        'current_password',\n        'password',\n        'password_confirmation',\n    ];\n\n    /**\n     * A function to run upon out of memory.\n     * If it returns a response, that will be provided back to the request\n     * upon an out of memory event.\n     *\n     * @var ?callable(): ?Response\n     */\n    protected $onOutOfMemory = null;\n\n    /**\n     * Report or log an exception.\n     *\n     * @param Throwable $exception\n     *\n     * @return void\n     *@throws Throwable\n     *\n     */\n    public function report(Throwable $exception)\n    {\n        parent::report($exception);\n    }\n\n    /**\n     * Render an exception into an HTTP response.\n     *\n     * @param Request $request\n     */\n    public function render($request, Throwable $e): SymfonyResponse\n    {\n        if ($e instanceof FatalError && str_contains($e->getMessage(), 'bytes exhausted (tried to allocate') && $this->onOutOfMemory) {\n            $response = call_user_func($this->onOutOfMemory);\n            if ($response) {\n                return $response;\n            }\n        }\n\n        if ($e instanceof PostTooLargeException) {\n            $e = new NotifyException(trans('errors.server_post_limit'), '/', 413);\n        }\n\n        if ($this->isApiRequest($request)) {\n            return $this->renderApiException($e);\n        }\n\n        return parent::render($request, $e);\n    }\n\n    /**\n     * Provide a function to be called when an out of memory event occurs.\n     * If the callable returns a response, this response will be returned\n     * to the request upon error.\n     */\n    public function prepareForOutOfMemory(callable $onOutOfMemory): void\n    {\n        $this->onOutOfMemory = $onOutOfMemory;\n    }\n\n    /**\n     * Forget the current out of memory handler, if existing.\n     */\n    public function forgetOutOfMemoryHandler(): void\n    {\n        $this->onOutOfMemory = null;\n    }\n\n    /**\n     * Check if the given request is an API request.\n     */\n    protected function isApiRequest(Request $request): bool\n    {\n        return str_starts_with($request->path(), 'api/');\n    }\n\n    /**\n     * Render an exception when the API is in use.\n     */\n    protected function renderApiException(Throwable $e): JsonResponse\n    {\n        $code = 500;\n        $headers = [];\n\n        if ($e instanceof HttpExceptionInterface) {\n            $code = $e->getStatusCode();\n            $headers = $e->getHeaders();\n        }\n\n        if ($e instanceof ModelNotFoundException) {\n            $code = 404;\n        }\n\n        $responseData = [\n            'error' => [\n                'message' => $e->getMessage(),\n            ],\n        ];\n\n        if ($e instanceof ValidationException) {\n            $responseData['error']['message'] = 'The given data was invalid.';\n            $responseData['error']['validation'] = $e->errors();\n            $code = $e->status;\n        }\n\n        $responseData['error']['code'] = $code;\n\n        return new JsonResponse($responseData, $code, $headers);\n    }\n\n    /**\n     * Convert an authentication exception into an unauthenticated response.\n     *\n     * @param Request $request\n     */\n    protected function unauthenticated($request, AuthenticationException $exception): SymfonyResponse\n    {\n        if ($request->expectsJson()) {\n            return response()->json(['error' => 'Unauthenticated.'], 401);\n        }\n\n        return redirect()->guest('login');\n    }\n\n    /**\n     * Convert a validation exception into a JSON response.\n     *\n     * @param Request $request\n     */\n    protected function invalidJson($request, ValidationException $exception): JsonResponse\n    {\n        return response()->json($exception->errors(), $exception->status);\n    }\n}\n"
  },
  {
    "path": "app/Exceptions/HttpFetchException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nuse Exception;\n\nclass HttpFetchException extends Exception\n{\n}\n"
  },
  {
    "path": "app/Exceptions/ImageUploadException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nclass ImageUploadException extends PrettyException\n{\n}\n"
  },
  {
    "path": "app/Exceptions/JsonDebugException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nuse Exception;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Contracts\\Support\\Responsable;\n\nclass JsonDebugException extends Exception implements Responsable\n{\n    protected array $data;\n\n    /**\n     * JsonDebugException constructor.\n     */\n    public function __construct(array $data)\n    {\n        $this->data = $data;\n        parent::__construct();\n    }\n\n    /**\n     * Convert this exception into a response.\n     * We add a manual data conversion to UTF8 to ensure any binary data is presentable as a JSON string.\n     */\n    public function toResponse($request): JsonResponse\n    {\n        $cleaned = mb_convert_encoding($this->data, 'UTF-8');\n\n        return response()->json($cleaned);\n    }\n}\n"
  },
  {
    "path": "app/Exceptions/LdapException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nclass LdapException extends PrettyException\n{\n}\n"
  },
  {
    "path": "app/Exceptions/LoginAttemptEmailNeededException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nclass LoginAttemptEmailNeededException extends LoginAttemptException\n{\n}\n"
  },
  {
    "path": "app/Exceptions/LoginAttemptException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nclass LoginAttemptException extends \\Exception\n{\n}\n"
  },
  {
    "path": "app/Exceptions/LoginAttemptInvalidUserException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nclass LoginAttemptInvalidUserException extends LoginAttemptException\n{\n}\n"
  },
  {
    "path": "app/Exceptions/MoveOperationException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nuse Exception;\n\nclass MoveOperationException extends Exception\n{\n}\n"
  },
  {
    "path": "app/Exceptions/NotFoundException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nclass NotFoundException extends PrettyException\n{\n    /**\n     * NotFoundException constructor.\n     */\n    public function __construct($message = 'Item not found')\n    {\n        parent::__construct($message, 404);\n    }\n}\n"
  },
  {
    "path": "app/Exceptions/NotifyException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nuse Exception;\nuse Illuminate\\Contracts\\Support\\Responsable;\nuse Symfony\\Component\\HttpKernel\\Exception\\HttpExceptionInterface;\n\nclass NotifyException extends Exception implements Responsable, HttpExceptionInterface\n{\n    public $message;\n    public string $redirectLocation;\n    protected int $status;\n\n    public function __construct(string $message, string $redirectLocation = '/', int $status = 500)\n    {\n        $this->message = $message;\n        $this->redirectLocation = $redirectLocation;\n        $this->status = $status;\n\n        parent::__construct();\n    }\n\n    /**\n     * Get the desired HTTP status code for this exception.\n     */\n    public function getStatusCode(): int\n    {\n        return $this->status;\n    }\n\n    /**\n     * Get the desired HTTP headers for this exception.\n     */\n    public function getHeaders(): array\n    {\n        return [];\n    }\n\n    /**\n     * Send the response for this type of exception.\n     *\n     * {@inheritdoc}\n     */\n    public function toResponse($request)\n    {\n        $message = $this->getMessage();\n\n        // Front-end JSON handling. API-side handling managed via handler.\n        if ($request->wantsJson()) {\n            return response()->json(['error' => $message], $this->getStatusCode());\n        }\n\n        if (!empty($message)) {\n            session()->flash('error', $message);\n        }\n\n        return redirect($this->redirectLocation);\n    }\n}\n"
  },
  {
    "path": "app/Exceptions/PdfExportException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nclass PdfExportException extends \\Exception\n{\n}\n"
  },
  {
    "path": "app/Exceptions/PermissionsException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nuse Exception;\n\nclass PermissionsException extends Exception\n{\n}\n"
  },
  {
    "path": "app/Exceptions/PrettyException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nuse Exception;\nuse Illuminate\\Contracts\\Support\\Responsable;\nuse Symfony\\Component\\HttpKernel\\Exception\\HttpExceptionInterface;\n\nclass PrettyException extends Exception implements Responsable, HttpExceptionInterface\n{\n    protected ?string $subtitle = null;\n    protected ?string $details = null;\n\n    /**\n     * Render a response for when this exception occurs.\n     *\n     * {@inheritdoc}\n     */\n    public function toResponse($request)\n    {\n        $code = $this->getStatusCode();\n\n        return response()->view('errors.' . $code, [\n            'message'  => $this->getMessage(),\n            'subtitle' => $this->subtitle,\n            'details'  => $this->details,\n        ], $code);\n    }\n\n    public function setSubtitle(string $subtitle): self\n    {\n        $this->subtitle = $subtitle;\n\n        return $this;\n    }\n\n    public function setDetails(string $details): self\n    {\n        $this->details = $details;\n\n        return $this;\n    }\n\n    /**\n     * Get the desired HTTP status code for this exception.\n     */\n    public function getStatusCode(): int\n    {\n        return ($this->getCode() === 0) ? 500 : $this->getCode();\n    }\n\n    /**\n     * Get the desired HTTP headers for this exception.\n     */\n    public function getHeaders(): array\n    {\n        return [];\n    }\n}\n"
  },
  {
    "path": "app/Exceptions/SamlException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nclass SamlException extends NotifyException\n{\n}\n"
  },
  {
    "path": "app/Exceptions/SocialDriverNotConfigured.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nclass SocialDriverNotConfigured extends PrettyException\n{\n}\n"
  },
  {
    "path": "app/Exceptions/SocialSignInAccountNotUsed.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nclass SocialSignInAccountNotUsed extends SocialSignInException\n{\n}\n"
  },
  {
    "path": "app/Exceptions/SocialSignInException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nclass SocialSignInException extends NotifyException\n{\n}\n"
  },
  {
    "path": "app/Exceptions/StoppedAuthenticationException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nuse BookStack\\Access\\LoginService;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Contracts\\Support\\Responsable;\nuse Illuminate\\Http\\Request;\n\nclass StoppedAuthenticationException extends \\Exception implements Responsable\n{\n    public function __construct(\n        protected User $user,\n        protected LoginService $loginService\n    ) {\n        parent::__construct();\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function toResponse($request)\n    {\n        $redirect = '/login';\n\n        if ($this->loginService->awaitingEmailConfirmation($this->user)) {\n            return $this->awaitingEmailConfirmationResponse($request);\n        }\n\n        if ($this->loginService->needsMfaVerification($this->user)) {\n            $redirect = '/mfa/verify';\n        }\n\n        return redirect($redirect);\n    }\n\n    /**\n     * Provide an error response for when the current user's email is not confirmed\n     * in a system which requires it.\n     */\n    protected function awaitingEmailConfirmationResponse(Request $request)\n    {\n        if ($request->wantsJson()) {\n            return response()->json([\n                'error' => [\n                    'code'    => 401,\n                    'message' => trans('errors.email_confirmation_awaiting'),\n                ],\n            ], 401);\n        }\n\n        if (session()->pull('sent-email-confirmation') === true) {\n            return redirect('/register/confirm');\n        }\n\n        return redirect('/register/confirm/awaiting');\n    }\n}\n"
  },
  {
    "path": "app/Exceptions/ThemeException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nclass ThemeException extends \\Exception\n{\n}\n"
  },
  {
    "path": "app/Exceptions/UserRegistrationException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nclass UserRegistrationException extends NotifyException\n{\n}\n"
  },
  {
    "path": "app/Exceptions/UserTokenExpiredException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nclass UserTokenExpiredException extends \\Exception\n{\n    public $userId;\n\n    /**\n     * UserTokenExpiredException constructor.\n     *\n     * @param string $message\n     * @param int    $userId\n     */\n    public function __construct(string $message, int $userId)\n    {\n        $this->userId = $userId;\n        parent::__construct($message);\n    }\n}\n"
  },
  {
    "path": "app/Exceptions/UserTokenNotFoundException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nclass UserTokenNotFoundException extends \\Exception\n{\n}\n"
  },
  {
    "path": "app/Exceptions/UserUpdateException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nclass UserUpdateException extends NotifyException\n{\n}\n"
  },
  {
    "path": "app/Exceptions/ZipExportException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nclass ZipExportException extends \\Exception\n{\n}\n"
  },
  {
    "path": "app/Exceptions/ZipImportException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nclass ZipImportException extends \\Exception\n{\n    public function __construct(\n        public array $errors\n    ) {\n        $message = \"Import failed with errors:\" . implode(\"\\n\", $this->errors);\n        parent::__construct($message);\n    }\n}\n"
  },
  {
    "path": "app/Exceptions/ZipValidationException.php",
    "content": "<?php\n\nnamespace BookStack\\Exceptions;\n\nclass ZipValidationException extends \\Exception\n{\n    public function __construct(\n        public array $errors\n    ) {\n        parent::__construct();\n    }\n}\n"
  },
  {
    "path": "app/Exports/Controllers/BookExportApiController.php",
    "content": "<?php\n\nnamespace BookStack\\Exports\\Controllers;\n\nuse BookStack\\Entities\\Queries\\BookQueries;\nuse BookStack\\Exports\\ExportFormatter;\nuse BookStack\\Exports\\ZipExports\\ZipExportBuilder;\nuse BookStack\\Http\\ApiController;\nuse BookStack\\Permissions\\Permission;\nuse Throwable;\n\nclass BookExportApiController extends ApiController\n{\n    public function __construct(\n        protected ExportFormatter $exportFormatter,\n        protected BookQueries $queries,\n    ) {\n        $this->middleware(Permission::ContentExport->middleware());\n    }\n\n    /**\n     * Export a book as a PDF file.\n     *\n     * @throws Throwable\n     */\n    public function exportPdf(int $id)\n    {\n        $book = $this->queries->findVisibleByIdOrFail($id);\n        $pdfContent = $this->exportFormatter->bookToPdf($book);\n\n        return $this->download()->directly($pdfContent, $book->slug . '.pdf');\n    }\n\n    /**\n     * Export a book as a contained HTML file.\n     *\n     * @throws Throwable\n     */\n    public function exportHtml(int $id)\n    {\n        $book = $this->queries->findVisibleByIdOrFail($id);\n        $htmlContent = $this->exportFormatter->bookToContainedHtml($book);\n\n        return $this->download()->directly($htmlContent, $book->slug . '.html');\n    }\n\n    /**\n     * Export a book as a plain text file.\n     */\n    public function exportPlainText(int $id)\n    {\n        $book = $this->queries->findVisibleByIdOrFail($id);\n        $textContent = $this->exportFormatter->bookToPlainText($book);\n\n        return $this->download()->directly($textContent, $book->slug . '.txt');\n    }\n\n    /**\n     * Export a book as a markdown file.\n     */\n    public function exportMarkdown(int $id)\n    {\n        $book = $this->queries->findVisibleByIdOrFail($id);\n        $markdown = $this->exportFormatter->bookToMarkdown($book);\n\n        return $this->download()->directly($markdown, $book->slug . '.md');\n    }\n\n    /**\n     * Export a book as a contained ZIP export file.\n     */\n    public function exportZip(int $id, ZipExportBuilder $builder)\n    {\n        $book = $this->queries->findVisibleByIdOrFail($id);\n        $zip = $builder->buildForBook($book);\n\n        return $this->download()->streamedFileDirectly($zip, $book->slug . '.zip', true);\n    }\n}\n"
  },
  {
    "path": "app/Exports/Controllers/BookExportController.php",
    "content": "<?php\n\nnamespace BookStack\\Exports\\Controllers;\n\nuse BookStack\\Entities\\Queries\\BookQueries;\nuse BookStack\\Exceptions\\NotFoundException;\nuse BookStack\\Exports\\ExportFormatter;\nuse BookStack\\Exports\\ZipExports\\ZipExportBuilder;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse Throwable;\n\nclass BookExportController extends Controller\n{\n    public function __construct(\n        protected BookQueries $queries,\n        protected ExportFormatter $exportFormatter,\n    ) {\n        $this->middleware(Permission::ContentExport->middleware());\n        $this->middleware('throttle:exports');\n    }\n\n    /**\n     * Export a book as a PDF file.\n     *\n     * @throws Throwable\n     */\n    public function pdf(string $bookSlug)\n    {\n        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);\n        $pdfContent = $this->exportFormatter->bookToPdf($book);\n\n        return $this->download()->directly($pdfContent, $bookSlug . '.pdf');\n    }\n\n    /**\n     * Export a book as a contained HTML file.\n     *\n     * @throws Throwable\n     */\n    public function html(string $bookSlug)\n    {\n        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);\n        $htmlContent = $this->exportFormatter->bookToContainedHtml($book);\n\n        return $this->download()->directly($htmlContent, $bookSlug . '.html');\n    }\n\n    /**\n     * Export a book as a plain text file.\n     */\n    public function plainText(string $bookSlug)\n    {\n        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);\n        $textContent = $this->exportFormatter->bookToPlainText($book);\n\n        return $this->download()->directly($textContent, $bookSlug . '.txt');\n    }\n\n    /**\n     * Export a book as a markdown file.\n     */\n    public function markdown(string $bookSlug)\n    {\n        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);\n        $textContent = $this->exportFormatter->bookToMarkdown($book);\n\n        return $this->download()->directly($textContent, $bookSlug . '.md');\n    }\n\n    /**\n     * Export a book to a contained ZIP export file.\n     * @throws NotFoundException\n     */\n    public function zip(string $bookSlug, ZipExportBuilder $builder)\n    {\n        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);\n        $zip = $builder->buildForBook($book);\n\n        return $this->download()->streamedFileDirectly($zip, $bookSlug . '.zip', true);\n    }\n}\n"
  },
  {
    "path": "app/Exports/Controllers/ChapterExportApiController.php",
    "content": "<?php\n\nnamespace BookStack\\Exports\\Controllers;\n\nuse BookStack\\Entities\\Queries\\ChapterQueries;\nuse BookStack\\Exports\\ExportFormatter;\nuse BookStack\\Exports\\ZipExports\\ZipExportBuilder;\nuse BookStack\\Http\\ApiController;\nuse BookStack\\Permissions\\Permission;\nuse Throwable;\n\nclass ChapterExportApiController extends ApiController\n{\n    public function __construct(\n        protected ExportFormatter $exportFormatter,\n        protected ChapterQueries $queries,\n    ) {\n        $this->middleware(Permission::ContentExport->middleware());\n    }\n\n    /**\n     * Export a chapter as a PDF file.\n     *\n     * @throws Throwable\n     */\n    public function exportPdf(int $id)\n    {\n        $chapter = $this->queries->findVisibleByIdOrFail($id);\n        $pdfContent = $this->exportFormatter->chapterToPdf($chapter);\n\n        return $this->download()->directly($pdfContent, $chapter->slug . '.pdf');\n    }\n\n    /**\n     * Export a chapter as a contained HTML file.\n     *\n     * @throws Throwable\n     */\n    public function exportHtml(int $id)\n    {\n        $chapter = $this->queries->findVisibleByIdOrFail($id);\n        $htmlContent = $this->exportFormatter->chapterToContainedHtml($chapter);\n\n        return $this->download()->directly($htmlContent, $chapter->slug . '.html');\n    }\n\n    /**\n     * Export a chapter as a plain text file.\n     */\n    public function exportPlainText(int $id)\n    {\n        $chapter = $this->queries->findVisibleByIdOrFail($id);\n        $textContent = $this->exportFormatter->chapterToPlainText($chapter);\n\n        return $this->download()->directly($textContent, $chapter->slug . '.txt');\n    }\n\n    /**\n     * Export a chapter as a markdown file.\n     */\n    public function exportMarkdown(int $id)\n    {\n        $chapter = $this->queries->findVisibleByIdOrFail($id);\n        $markdown = $this->exportFormatter->chapterToMarkdown($chapter);\n\n        return $this->download()->directly($markdown, $chapter->slug . '.md');\n    }\n\n    /**\n     * Export a chapter as a contained ZIP file.\n     */\n    public function exportZip(int $id, ZipExportBuilder $builder)\n    {\n        $chapter = $this->queries->findVisibleByIdOrFail($id);\n        $zip = $builder->buildForChapter($chapter);\n\n        return $this->download()->streamedFileDirectly($zip, $chapter->slug . '.zip', true);\n    }\n}\n"
  },
  {
    "path": "app/Exports/Controllers/ChapterExportController.php",
    "content": "<?php\n\nnamespace BookStack\\Exports\\Controllers;\n\nuse BookStack\\Entities\\Queries\\ChapterQueries;\nuse BookStack\\Exceptions\\NotFoundException;\nuse BookStack\\Exports\\ExportFormatter;\nuse BookStack\\Exports\\ZipExports\\ZipExportBuilder;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse Throwable;\n\nclass ChapterExportController extends Controller\n{\n    public function __construct(\n        protected ChapterQueries $queries,\n        protected ExportFormatter $exportFormatter,\n    ) {\n        $this->middleware(Permission::ContentExport->middleware());\n        $this->middleware('throttle:exports');\n    }\n\n    /**\n     * Exports a chapter to pdf.\n     *\n     * @throws NotFoundException\n     * @throws Throwable\n     */\n    public function pdf(string $bookSlug, string $chapterSlug)\n    {\n        $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);\n        $pdfContent = $this->exportFormatter->chapterToPdf($chapter);\n\n        return $this->download()->directly($pdfContent, $chapterSlug . '.pdf');\n    }\n\n    /**\n     * Export a chapter to a self-contained HTML file.\n     *\n     * @throws NotFoundException\n     * @throws Throwable\n     */\n    public function html(string $bookSlug, string $chapterSlug)\n    {\n        $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);\n        $containedHtml = $this->exportFormatter->chapterToContainedHtml($chapter);\n\n        return $this->download()->directly($containedHtml, $chapterSlug . '.html');\n    }\n\n    /**\n     * Export a chapter to a simple plaintext .txt file.\n     *\n     * @throws NotFoundException\n     */\n    public function plainText(string $bookSlug, string $chapterSlug)\n    {\n        $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);\n        $chapterText = $this->exportFormatter->chapterToPlainText($chapter);\n\n        return $this->download()->directly($chapterText, $chapterSlug . '.txt');\n    }\n\n    /**\n     * Export a chapter to a simple markdown file.\n     *\n     * @throws NotFoundException\n     */\n    public function markdown(string $bookSlug, string $chapterSlug)\n    {\n        $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);\n        $chapterText = $this->exportFormatter->chapterToMarkdown($chapter);\n\n        return $this->download()->directly($chapterText, $chapterSlug . '.md');\n    }\n\n    /**\n     * Export a book to a contained ZIP export file.\n     * @throws NotFoundException\n     */\n    public function zip(string $bookSlug, string $chapterSlug, ZipExportBuilder $builder)\n    {\n        $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);\n        $zip = $builder->buildForChapter($chapter);\n\n        return $this->download()->streamedFileDirectly($zip, $chapterSlug . '.zip', true);\n    }\n}\n"
  },
  {
    "path": "app/Exports/Controllers/ImportApiController.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace BookStack\\Exports\\Controllers;\n\nuse BookStack\\Exceptions\\ZipImportException;\nuse BookStack\\Exceptions\\ZipValidationException;\nuse BookStack\\Exports\\ImportRepo;\nuse BookStack\\Http\\ApiController;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Uploads\\AttachmentService;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Response;\n\nclass ImportApiController extends ApiController\n{\n    public function __construct(\n        protected ImportRepo $imports,\n    ) {\n        $this->middleware(Permission::ContentImport->middleware());\n    }\n\n    /**\n     * List existing ZIP imports visible to the user.\n     * Requires permission to import content.\n     */\n    public function list(): JsonResponse\n    {\n        $query = $this->imports->queryVisible();\n\n        return $this->apiListingResponse($query, [\n            'id', 'name', 'size', 'type', 'created_by', 'created_at', 'updated_at'\n        ]);\n    }\n\n    /**\n     * Start a new import from a ZIP file.\n     * This does not actually run the import since that is performed via the \"run\" endpoint.\n     * This uploads, validates and stores the ZIP file so it's ready to be imported.\n     *\n     * This \"file\" parameter must be a BookStack-compatible ZIP file, and this must be\n     * sent via a 'multipart/form-data' type request.\n     *\n     * Requires permission to import content.\n     */\n    public function create(Request $request): JsonResponse\n    {\n        $this->validate($request, $this->rules()['create']);\n\n        $file = $request->file('file');\n\n        try {\n            $import = $this->imports->storeFromUpload($file);\n        } catch (ZipValidationException $exception) {\n            $message = \"ZIP upload failed with the following validation errors: \\n\" . $this->formatErrors($exception->errors);\n            return $this->jsonError($message, 422);\n        }\n\n        return response()->json($import);\n    }\n\n    /**\n     * Read details of a pending ZIP import.\n     * The \"details\" property contains high-level metadata regarding the ZIP import content,\n     * and the structure of this will change depending on import \"type\".\n     * Requires permission to import content.\n     */\n    public function read(int $id): JsonResponse\n    {\n        $import = $this->imports->findVisible($id);\n\n        $import->setAttribute('details', $import->decodeMetadata());\n\n        return response()->json($import);\n    }\n\n    /**\n     * Run the import process for an uploaded ZIP import.\n     * The \"parent_id\" and \"parent_type\" parameters are required when the import type is \"chapter\" or \"page\".\n     * On success, this endpoint returns the imported item.\n     * Requires permission to import content.\n     */\n    public function run(int $id, Request $request): JsonResponse\n    {\n        $import = $this->imports->findVisible($id);\n        $parent = null;\n        $rules = $this->rules()['run'];\n\n        if ($import->type === 'page' || $import->type === 'chapter') {\n            $rules['parent_type'][] = 'required';\n            $rules['parent_id'][] = 'required';\n            $data = $this->validate($request, $rules);\n            $parent = \"{$data['parent_type']}:{$data['parent_id']}\";\n        }\n\n        try {\n            $entity = $this->imports->runImport($import, $parent);\n        } catch (ZipImportException $exception) {\n            $message = \"ZIP import failed with the following errors: \\n\" . $this->formatErrors($exception->errors);\n            return $this->jsonError($message);\n        }\n\n        return response()->json($entity->withoutRelations());\n    }\n\n    /**\n     * Delete a pending ZIP import from the system.\n     * Requires permission to import content.\n     */\n    public function delete(int $id): Response\n    {\n        $import = $this->imports->findVisible($id);\n        $this->imports->deleteImport($import);\n\n        return response('', 204);\n    }\n\n    protected function rules(): array\n    {\n        return [\n            'create' => [\n                'file' => ['required', ...AttachmentService::getFileValidationRules()],\n            ],\n            'run' => [\n                'parent_type' => ['string', 'in:book,chapter'],\n                'parent_id' => ['int'],\n            ],\n        ];\n    }\n\n    protected function formatErrors(array $errors): string\n    {\n        $parts = [];\n        foreach ($errors as $key => $error) {\n            if (is_string($key)) {\n                $parts[] = \"[{$key}] {$error}\";\n            } else {\n                $parts[] = $error;\n            }\n        }\n        return implode(\"\\n\", $parts);\n    }\n}\n"
  },
  {
    "path": "app/Exports/Controllers/ImportController.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace BookStack\\Exports\\Controllers;\n\nuse BookStack\\Exceptions\\ZipImportException;\nuse BookStack\\Exceptions\\ZipValidationException;\nuse BookStack\\Exports\\ImportRepo;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Uploads\\AttachmentService;\nuse Illuminate\\Http\\Request;\n\nclass ImportController extends Controller\n{\n    public function __construct(\n        protected ImportRepo $imports,\n    ) {\n        $this->middleware(Permission::ContentImport->middleware());\n    }\n\n    /**\n     * Show the view to start a new import, and also list out the existing\n     * in progress imports that are visible to the user.\n     */\n    public function start()\n    {\n        $imports = $this->imports->getVisibleImports();\n\n        $this->setPageTitle(trans('entities.import'));\n\n        return view('exports.import', [\n            'imports' => $imports,\n            'zipErrors' => session()->pull('validation_errors') ?? [],\n        ]);\n    }\n\n    /**\n     * Upload, validate and store an import file.\n     */\n    public function upload(Request $request)\n    {\n        $this->validate($request, [\n            'file' => ['required', ...AttachmentService::getFileValidationRules()]\n        ]);\n\n        $file = $request->file('file');\n        try {\n            $import = $this->imports->storeFromUpload($file);\n        } catch (ZipValidationException $exception) {\n            return redirect('/import')->with('validation_errors', $exception->errors);\n        }\n\n        return redirect($import->getUrl());\n    }\n\n    /**\n     * Show a pending import, with a form to allow progressing\n     * with the import process.\n     */\n    public function show(int $id)\n    {\n        $import = $this->imports->findVisible($id);\n\n        $this->setPageTitle(trans('entities.import_continue'));\n\n        return view('exports.import-show', [\n            'import' => $import,\n            'data' => $import->decodeMetadata(),\n        ]);\n    }\n\n    /**\n     * Run the import process against an uploaded import ZIP.\n     */\n    public function run(int $id, Request $request)\n    {\n        $import = $this->imports->findVisible($id);\n        $parent = null;\n\n        if ($import->type === 'page' || $import->type === 'chapter') {\n            session()->setPreviousUrl($import->getUrl());\n            $data = $this->validate($request, [\n                'parent' => ['required', 'string'],\n            ]);\n            $parent = $data['parent'];\n        }\n\n        try {\n            $entity = $this->imports->runImport($import, $parent);\n        } catch (ZipImportException $exception) {\n            session()->forget(['success', 'warning']);\n            $this->showErrorNotification(trans('errors.import_zip_failed_notification'));\n            return redirect($import->getUrl())->with('import_errors', $exception->errors);\n        }\n\n        return redirect($entity->getUrl());\n    }\n\n    /**\n     * Delete an active pending import from the filesystem and database.\n     */\n    public function delete(int $id)\n    {\n        $import = $this->imports->findVisible($id);\n        $this->imports->deleteImport($import);\n\n        return redirect('/import');\n    }\n}\n"
  },
  {
    "path": "app/Exports/Controllers/PageExportApiController.php",
    "content": "<?php\n\nnamespace BookStack\\Exports\\Controllers;\n\nuse BookStack\\Entities\\Queries\\PageQueries;\nuse BookStack\\Exports\\ExportFormatter;\nuse BookStack\\Exports\\ZipExports\\ZipExportBuilder;\nuse BookStack\\Http\\ApiController;\nuse BookStack\\Permissions\\Permission;\nuse Throwable;\n\nclass PageExportApiController extends ApiController\n{\n    public function __construct(\n        protected ExportFormatter $exportFormatter,\n        protected PageQueries $queries,\n    ) {\n        $this->middleware(Permission::ContentExport->middleware());\n    }\n\n    /**\n     * Export a page as a PDF file.\n     *\n     * @throws Throwable\n     */\n    public function exportPdf(int $id)\n    {\n        $page = $this->queries->findVisibleByIdOrFail($id);\n        $pdfContent = $this->exportFormatter->pageToPdf($page);\n\n        return $this->download()->directly($pdfContent, $page->slug . '.pdf');\n    }\n\n    /**\n     * Export a page as a contained HTML file.\n     *\n     * @throws Throwable\n     */\n    public function exportHtml(int $id)\n    {\n        $page = $this->queries->findVisibleByIdOrFail($id);\n        $htmlContent = $this->exportFormatter->pageToContainedHtml($page);\n\n        return $this->download()->directly($htmlContent, $page->slug . '.html');\n    }\n\n    /**\n     * Export a page as a plain text file.\n     */\n    public function exportPlainText(int $id)\n    {\n        $page = $this->queries->findVisibleByIdOrFail($id);\n        $textContent = $this->exportFormatter->pageToPlainText($page);\n\n        return $this->download()->directly($textContent, $page->slug . '.txt');\n    }\n\n    /**\n     * Export a page as a markdown file.\n     */\n    public function exportMarkdown(int $id)\n    {\n        $page = $this->queries->findVisibleByIdOrFail($id);\n        $markdown = $this->exportFormatter->pageToMarkdown($page);\n\n        return $this->download()->directly($markdown, $page->slug . '.md');\n    }\n\n    /**\n     * Export a page as a contained ZIP file.\n     */\n    public function exportZip(int $id, ZipExportBuilder $builder)\n    {\n        $page = $this->queries->findVisibleByIdOrFail($id);\n        $zip = $builder->buildForPage($page);\n\n        return $this->download()->streamedFileDirectly($zip, $page->slug . '.zip', true);\n    }\n}\n"
  },
  {
    "path": "app/Exports/Controllers/PageExportController.php",
    "content": "<?php\n\nnamespace BookStack\\Exports\\Controllers;\n\nuse BookStack\\Entities\\Queries\\PageQueries;\nuse BookStack\\Entities\\Tools\\PageContent;\nuse BookStack\\Exceptions\\NotFoundException;\nuse BookStack\\Exports\\ExportFormatter;\nuse BookStack\\Exports\\ZipExports\\ZipExportBuilder;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse Throwable;\n\nclass PageExportController extends Controller\n{\n    public function __construct(\n        protected PageQueries $queries,\n        protected ExportFormatter $exportFormatter,\n    ) {\n        $this->middleware(Permission::ContentExport->middleware());\n        $this->middleware('throttle:exports');\n    }\n\n    /**\n     * Exports a page to a PDF.\n     * https://github.com/barryvdh/laravel-dompdf.\n     *\n     * @throws NotFoundException\n     * @throws Throwable\n     */\n    public function pdf(string $bookSlug, string $pageSlug)\n    {\n        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);\n        $page->html = (new PageContent($page))->render();\n        $pdfContent = $this->exportFormatter->pageToPdf($page);\n\n        return $this->download()->directly($pdfContent, $pageSlug . '.pdf');\n    }\n\n    /**\n     * Export a page to a self-contained HTML file.\n     *\n     * @throws NotFoundException\n     * @throws Throwable\n     */\n    public function html(string $bookSlug, string $pageSlug)\n    {\n        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);\n        $page->html = (new PageContent($page))->render();\n        $containedHtml = $this->exportFormatter->pageToContainedHtml($page);\n\n        return $this->download()->directly($containedHtml, $pageSlug . '.html');\n    }\n\n    /**\n     * Export a page to a simple plaintext .txt file.\n     *\n     * @throws NotFoundException\n     */\n    public function plainText(string $bookSlug, string $pageSlug)\n    {\n        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);\n        $pageText = $this->exportFormatter->pageToPlainText($page);\n\n        return $this->download()->directly($pageText, $pageSlug . '.txt');\n    }\n\n    /**\n     * Export a page to a simple markdown .md file.\n     *\n     * @throws NotFoundException\n     */\n    public function markdown(string $bookSlug, string $pageSlug)\n    {\n        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);\n        $pageText = $this->exportFormatter->pageToMarkdown($page);\n\n        return $this->download()->directly($pageText, $pageSlug . '.md');\n    }\n\n    /**\n     * Export a page to a contained ZIP export file.\n     * @throws NotFoundException\n     */\n    public function zip(string $bookSlug, string $pageSlug, ZipExportBuilder $builder)\n    {\n        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);\n        $zip = $builder->buildForPage($page);\n\n        return $this->download()->streamedFileDirectly($zip, $pageSlug . '.zip', true);\n    }\n}\n"
  },
  {
    "path": "app/Exports/ExportFormatter.php",
    "content": "<?php\n\nnamespace BookStack\\Exports;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Tools\\BookContents;\nuse BookStack\\Entities\\Tools\\Markdown\\HtmlToMarkdown;\nuse BookStack\\Entities\\Tools\\PageContent;\nuse BookStack\\Uploads\\ImageService;\nuse BookStack\\Util\\CspService;\nuse BookStack\\Util\\HtmlDocument;\nuse DOMElement;\nuse Exception;\nuse Throwable;\n\nclass ExportFormatter\n{\n    public function __construct(\n        protected ImageService $imageService,\n        protected PdfGenerator $pdfGenerator,\n        protected CspService $cspService\n    ) {\n    }\n\n    /**\n     * Convert a page to a self-contained HTML file.\n     * Includes required CSS & image content. Images are base64 encoded into the HTML.\n     *\n     * @throws Throwable\n     */\n    public function pageToContainedHtml(Page $page): string\n    {\n        $page->html = (new PageContent($page))->render();\n        $pageHtml = view('exports.page', [\n            'page'       => $page,\n            'format'     => 'html',\n            'cspContent' => $this->cspService->getCspMetaTagValue(),\n            'locale'     => user()->getLocale(),\n        ])->render();\n\n        return $this->containHtml($pageHtml);\n    }\n\n    /**\n     * Convert a chapter to a self-contained HTML file.\n     *\n     * @throws Throwable\n     */\n    public function chapterToContainedHtml(Chapter $chapter): string\n    {\n        $pages = $chapter->getVisiblePages();\n        $pages->each(function ($page) {\n            $page->html = (new PageContent($page))->render();\n        });\n        $html = view('exports.chapter', [\n            'chapter'    => $chapter,\n            'pages'      => $pages,\n            'format'     => 'html',\n            'cspContent' => $this->cspService->getCspMetaTagValue(),\n            'locale'     => user()->getLocale(),\n        ])->render();\n\n        return $this->containHtml($html);\n    }\n\n    /**\n     * Convert a book to a self-contained HTML file.\n     *\n     * @throws Throwable\n     */\n    public function bookToContainedHtml(Book $book): string\n    {\n        $bookTree = (new BookContents($book))->getTree(false, true);\n        $html = view('exports.book', [\n            'book'         => $book,\n            'bookChildren' => $bookTree,\n            'format'       => 'html',\n            'cspContent'   => $this->cspService->getCspMetaTagValue(),\n            'locale'       => user()->getLocale(),\n        ])->render();\n\n        return $this->containHtml($html);\n    }\n\n    /**\n     * Convert a page to a PDF file.\n     *\n     * @throws Throwable\n     */\n    public function pageToPdf(Page $page): string\n    {\n        $page->html = (new PageContent($page))->render();\n        $html = view('exports.page', [\n            'page'   => $page,\n            'format' => 'pdf',\n            'engine' => $this->pdfGenerator->getActiveEngine(),\n            'locale' => user()->getLocale(),\n        ])->render();\n\n        return $this->htmlToPdf($html);\n    }\n\n    /**\n     * Convert a chapter to a PDF file.\n     *\n     * @throws Throwable\n     */\n    public function chapterToPdf(Chapter $chapter): string\n    {\n        $pages = $chapter->getVisiblePages();\n        $pages->each(function ($page) {\n            $page->html = (new PageContent($page))->render();\n        });\n\n        $html = view('exports.chapter', [\n            'chapter' => $chapter,\n            'pages'   => $pages,\n            'format'  => 'pdf',\n            'engine'  => $this->pdfGenerator->getActiveEngine(),\n            'locale'  => user()->getLocale(),\n        ])->render();\n\n        return $this->htmlToPdf($html);\n    }\n\n    /**\n     * Convert a book to a PDF file.\n     *\n     * @throws Throwable\n     */\n    public function bookToPdf(Book $book): string\n    {\n        $bookTree = (new BookContents($book))->getTree(false, true);\n        $html = view('exports.book', [\n            'book'         => $book,\n            'bookChildren' => $bookTree,\n            'format'       => 'pdf',\n            'engine'       => $this->pdfGenerator->getActiveEngine(),\n            'locale'       => user()->getLocale(),\n        ])->render();\n\n        return $this->htmlToPdf($html);\n    }\n\n    /**\n     * Convert normal web-page HTML to a PDF.\n     *\n     * @throws Exception\n     */\n    protected function htmlToPdf(string $html): string\n    {\n        $html = $this->containHtml($html);\n        $doc = new HtmlDocument();\n        $doc->loadCompleteHtml($html);\n\n        $this->replaceIframesWithLinks($doc);\n        $this->openDetailElements($doc);\n        $cleanedHtml = $doc->getHtml();\n\n        return $this->pdfGenerator->fromHtml($cleanedHtml);\n    }\n\n    /**\n     * Within the given HTML content, Open any detail blocks.\n     */\n    protected function openDetailElements(HtmlDocument $doc): void\n    {\n        $details = $doc->queryXPath('//details');\n        /** @var DOMElement $detail */\n        foreach ($details as $detail) {\n            $detail->setAttribute('open', 'open');\n        }\n    }\n\n    /**\n     * Within the given HTML document, replace any iframe elements\n     * with anchor links within paragraph blocks.\n     */\n    protected function replaceIframesWithLinks(HtmlDocument $doc): void\n    {\n        $iframes = $doc->queryXPath('//iframe');\n\n        /** @var DOMElement $iframe */\n        foreach ($iframes as $iframe) {\n            $link = $iframe->getAttribute('src');\n            if (str_starts_with($link, '//')) {\n                $link = 'https:' . $link;\n            }\n\n            $anchor = $doc->createElement('a', $link);\n            $anchor->setAttribute('href', $link);\n            $paragraph = $doc->createElement('p');\n            $paragraph->appendChild($anchor);\n            $iframe->parentNode->replaceChild($paragraph, $iframe);\n        }\n    }\n\n    /**\n     * Bundle of the contents of a html file to be self-contained.\n     *\n     * @throws Exception\n     */\n    protected function containHtml(string $htmlContent): string\n    {\n        $imageTagsOutput = [];\n        preg_match_all(\"/\\<img.*?src\\=(\\'|\\\")(.*?)(\\'|\\\").*?\\>/i\", $htmlContent, $imageTagsOutput);\n\n        // Replace image src with base64 encoded image strings\n        if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {\n            foreach ($imageTagsOutput[0] as $index => $imgMatch) {\n                $oldImgTagString = $imgMatch;\n                $srcString = $imageTagsOutput[2][$index];\n                $imageEncoded = $this->imageService->imageUrlToBase64($srcString);\n                if ($imageEncoded === null) {\n                    $imageEncoded = $srcString;\n                }\n                $newImgTagString = str_replace($srcString, $imageEncoded, $oldImgTagString);\n                $htmlContent = str_replace($oldImgTagString, $newImgTagString, $htmlContent);\n            }\n        }\n\n        $linksOutput = [];\n        preg_match_all(\"/\\<a.*href\\=(\\'|\\\")(.*?)(\\'|\\\").*?\\>/i\", $htmlContent, $linksOutput);\n\n        // Update relative links to be absolute, with instance url\n        if (isset($linksOutput[0]) && count($linksOutput[0]) > 0) {\n            foreach ($linksOutput[0] as $index => $linkMatch) {\n                $oldLinkString = $linkMatch;\n                $srcString = $linksOutput[2][$index];\n                if (!str_starts_with(trim($srcString), 'http')) {\n                    $newSrcString = url($srcString);\n                    $newLinkString = str_replace($srcString, $newSrcString, $oldLinkString);\n                    $htmlContent = str_replace($oldLinkString, $newLinkString, $htmlContent);\n                }\n            }\n        }\n\n        return $htmlContent;\n    }\n\n    /**\n     * Converts the page contents into simple plain text.\n     * This method filters any bad looking content to provide a nice final output.\n     */\n    public function pageToPlainText(Page $page, bool $pageRendered = false, bool $fromParent = false): string\n    {\n        $html = $pageRendered ? $page->html : (new PageContent($page))->render();\n        // Add proceeding spaces before tags so spaces remain between\n        // text within elements after stripping tags.\n        $html = str_replace('<', \" <\", $html);\n        $text = trim(strip_tags($html));\n        // Replace multiple spaces with single spaces\n        $text = preg_replace('/ {2,}/', ' ', $text);\n        // Reduce multiple horrid whitespace characters.\n        $text = preg_replace('/(\\x0A|\\xA0|\\x0A|\\r|\\n){2,}/su', \"\\n\\n\", $text);\n        $text = html_entity_decode($text);\n        // Add title\n        $text = $page->name . ($fromParent ? \"\\n\" : \"\\n\\n\") . $text;\n\n        return $text;\n    }\n\n    /**\n     * Convert a chapter into a plain text string.\n     */\n    public function chapterToPlainText(Chapter $chapter): string\n    {\n        $text = $chapter->name . \"\\n\" . $chapter->description;\n        $text = trim($text) . \"\\n\\n\";\n\n        $parts = [];\n        foreach ($chapter->getVisiblePages() as $page) {\n            $parts[] = $this->pageToPlainText($page, false, true);\n        }\n\n        return $text . implode(\"\\n\\n\", $parts);\n    }\n\n    /**\n     * Convert a book into a plain text string.\n     */\n    public function bookToPlainText(Book $book): string\n    {\n        $bookTree = (new BookContents($book))->getTree(false, true);\n        $text = $book->name . \"\\n\" . $book->descriptionInfo()->getPlain();\n        $text = rtrim($text) . \"\\n\\n\";\n\n        $parts = [];\n        foreach ($bookTree as $bookChild) {\n            if ($bookChild->isA('chapter')) {\n                $parts[] = $this->chapterToPlainText($bookChild);\n            } else {\n                $parts[] = $this->pageToPlainText($bookChild, true, true);\n            }\n        }\n\n        return $text . implode(\"\\n\\n\", $parts);\n    }\n\n    /**\n     * Convert a page to a Markdown file.\n     */\n    public function pageToMarkdown(Page $page): string\n    {\n        if ($page->markdown) {\n            return '# ' . $page->name . \"\\n\\n\" . $page->markdown;\n        }\n\n        return '# ' . $page->name . \"\\n\\n\" . (new HtmlToMarkdown($page->html))->convert();\n    }\n\n    /**\n     * Convert a chapter to a Markdown file.\n     */\n    public function chapterToMarkdown(Chapter $chapter): string\n    {\n        $text = '# ' . $chapter->name . \"\\n\\n\";\n\n        $description = (new HtmlToMarkdown($chapter->descriptionInfo()->getHtml()))->convert();\n        if ($description) {\n            $text .= $description . \"\\n\\n\";\n        }\n\n        foreach ($chapter->getVisiblePages() as $page) {\n            $text .= $this->pageToMarkdown($page) . \"\\n\\n\";\n        }\n\n        return trim($text);\n    }\n\n    /**\n     * Convert a book into a plain text string.\n     */\n    public function bookToMarkdown(Book $book): string\n    {\n        $bookTree = (new BookContents($book))->getTree(false, true);\n        $text = '# ' . $book->name . \"\\n\\n\";\n\n        $description = (new HtmlToMarkdown($book->descriptionInfo()->getHtml()))->convert();\n        if ($description) {\n            $text .= $description . \"\\n\\n\";\n        }\n\n        foreach ($bookTree as $bookChild) {\n            if ($bookChild instanceof Chapter) {\n                $text .= $this->chapterToMarkdown($bookChild) . \"\\n\\n\";\n            } else {\n                $text .= $this->pageToMarkdown($bookChild) . \"\\n\\n\";\n            }\n        }\n\n        return trim($text);\n    }\n}\n"
  },
  {
    "path": "app/Exports/Import.php",
    "content": "<?php\n\nnamespace BookStack\\Exports;\n\nuse BookStack\\Activity\\Models\\Loggable;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportBook;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportChapter;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportPage;\nuse BookStack\\Users\\Models\\User;\nuse Carbon\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\n\n/**\n * @property int $id\n * @property string $path\n * @property string $name\n * @property int $size - ZIP size in bytes\n * @property string $type\n * @property string $metadata\n * @property int $created_by\n * @property Carbon $created_at\n * @property Carbon $updated_at\n * @property User $createdBy\n */\nclass Import extends Model implements Loggable\n{\n    use HasFactory;\n\n    protected $hidden = ['metadata'];\n\n    public function getSizeString(): string\n    {\n        $mb = round($this->size / 1000000, 2);\n        return \"{$mb} MB\";\n    }\n\n    /**\n     * Get the URL to view/continue this import.\n     */\n    public function getUrl(string $path = ''): string\n    {\n        $path = ltrim($path, '/');\n        return url(\"/import/{$this->id}\" . ($path ? '/' . $path : ''));\n    }\n\n    public function logDescriptor(): string\n    {\n        return \"({$this->id}) {$this->name}\";\n    }\n\n    public function createdBy(): BelongsTo\n    {\n        return $this->belongsTo(User::class, 'created_by');\n    }\n\n    public function decodeMetadata(): ZipExportBook|ZipExportChapter|ZipExportPage|null\n    {\n        $metadataArray = json_decode($this->metadata, true);\n        return match ($this->type) {\n            'book' => ZipExportBook::fromArray($metadataArray),\n            'chapter' => ZipExportChapter::fromArray($metadataArray),\n            'page' => ZipExportPage::fromArray($metadataArray),\n            default => null,\n        };\n    }\n}\n"
  },
  {
    "path": "app/Exports/ImportRepo.php",
    "content": "<?php\n\nnamespace BookStack\\Exports;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse BookStack\\Exceptions\\FileUploadException;\nuse BookStack\\Exceptions\\ZipExportException;\nuse BookStack\\Exceptions\\ZipImportException;\nuse BookStack\\Exceptions\\ZipValidationException;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportBook;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportChapter;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportPage;\nuse BookStack\\Exports\\ZipExports\\ZipExportReader;\nuse BookStack\\Exports\\ZipExports\\ZipExportValidator;\nuse BookStack\\Exports\\ZipExports\\ZipImportRunner;\nuse BookStack\\Facades\\Activity;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Uploads\\FileStorage;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Symfony\\Component\\HttpFoundation\\File\\UploadedFile;\n\nclass ImportRepo\n{\n    public function __construct(\n        protected FileStorage $storage,\n        protected ZipImportRunner $importer,\n        protected EntityQueries $entityQueries,\n    ) {\n    }\n\n    /**\n     * @return Collection<Import>\n     */\n    public function getVisibleImports(): Collection\n    {\n        return $this->queryVisible()->get();\n    }\n\n    /**\n     * @return Builder<Import>\n     */\n    public function queryVisible(): Builder\n    {\n        $query = Import::query();\n\n        if (!userCan(Permission::SettingsManage)) {\n            $query->where('created_by', user()->id);\n        }\n\n        return $query;\n    }\n\n    public function findVisible(int $id): Import\n    {\n        $query = Import::query();\n\n        if (!userCan(Permission::SettingsManage)) {\n            $query->where('created_by', user()->id);\n        }\n\n        return $query->findOrFail($id);\n    }\n\n    /**\n     * @throws FileUploadException\n     * @throws ZipValidationException\n     * @throws ZipExportException\n     */\n    public function storeFromUpload(UploadedFile $file): Import\n    {\n        $zipPath = $file->getRealPath();\n        $reader = new ZipExportReader($zipPath);\n\n        $errors = (new ZipExportValidator($reader))->validate();\n        if ($errors) {\n            throw new ZipValidationException($errors);\n        }\n\n        $exportModel = $reader->decodeDataToExportModel();\n\n        $import = new Import();\n        $import->type = match (get_class($exportModel)) {\n            ZipExportPage::class => 'page',\n            ZipExportChapter::class => 'chapter',\n            ZipExportBook::class => 'book',\n        };\n\n        $import->name = $exportModel->name;\n        $import->created_by = user()->id;\n        $import->size = filesize($zipPath);\n\n        $exportModel->metadataOnly();\n        $import->metadata = json_encode($exportModel);\n\n        $path = $this->storage->uploadFile(\n            $file,\n            'uploads/files/imports/',\n            '',\n            'zip'\n        );\n\n        $import->path = $path;\n        $import->save();\n\n        Activity::add(ActivityType::IMPORT_CREATE, $import);\n\n        return $import;\n    }\n\n    /**\n     * @throws ZipImportException\n     */\n    public function runImport(Import $import, ?string $parent = null): Entity\n    {\n        $parentModel = null;\n        if ($import->type === 'page' || $import->type === 'chapter') {\n            $parentModel = $parent ? $this->entityQueries->findVisibleByStringIdentifier($parent) : null;\n        }\n\n        DB::beginTransaction();\n        try {\n            $model = $this->importer->run($import, $parentModel);\n        } catch (ZipImportException $e) {\n            DB::rollBack();\n            $this->importer->revertStoredFiles();\n            throw $e;\n        }\n\n        DB::commit();\n        $this->deleteImport($import);\n        Activity::add(ActivityType::IMPORT_RUN, $import);\n\n        return $model;\n    }\n\n    public function deleteImport(Import $import): void\n    {\n        $this->storage->delete($import->path);\n        $import->delete();\n\n        Activity::add(ActivityType::IMPORT_DELETE, $import);\n    }\n}\n"
  },
  {
    "path": "app/Exports/PdfGenerator.php",
    "content": "<?php\n\nnamespace BookStack\\Exports;\n\nuse BookStack\\Exceptions\\PdfExportException;\nuse Dompdf\\Dompdf;\nuse Knp\\Snappy\\Pdf as SnappyPdf;\nuse Symfony\\Component\\Process\\Exception\\ProcessTimedOutException;\nuse Symfony\\Component\\Process\\Process;\n\nclass PdfGenerator\n{\n    const ENGINE_DOMPDF = 'dompdf';\n    const ENGINE_WKHTML = 'wkhtml';\n    const ENGINE_COMMAND = 'command';\n\n    /**\n     * Generate PDF content from the given HTML content.\n     * @throws PdfExportException\n     */\n    public function fromHtml(string $html): string\n    {\n        return match ($this->getActiveEngine()) {\n            self::ENGINE_COMMAND => $this->renderUsingCommand($html),\n            self::ENGINE_WKHTML => $this->renderUsingWkhtml($html),\n            default => $this->renderUsingDomPdf($html)\n        };\n    }\n\n    /**\n     * Get the currently active PDF engine.\n     * Returns the value of an `ENGINE_` const on this class.\n     */\n    public function getActiveEngine(): string\n    {\n        if (config('exports.pdf_command')) {\n            return self::ENGINE_COMMAND;\n        }\n\n        if ($this->getWkhtmlBinaryPath() && config('app.allow_untrusted_server_fetching') === true) {\n            return self::ENGINE_WKHTML;\n        }\n\n        return self::ENGINE_DOMPDF;\n    }\n\n    protected function getWkhtmlBinaryPath(): string\n    {\n        $wkhtmlBinaryPath = config('exports.snappy.pdf_binary');\n        if (file_exists(base_path('wkhtmltopdf'))) {\n            $wkhtmlBinaryPath = base_path('wkhtmltopdf');\n        }\n\n        return $wkhtmlBinaryPath ?: '';\n    }\n\n    protected function renderUsingDomPdf(string $html): string\n    {\n        $options = config('exports.dompdf');\n        $domPdf = new Dompdf($options);\n        $domPdf->setBasePath(base_path('public'));\n\n        $domPdf->loadHTML($this->convertEntities($html));\n        $domPdf->render();\n\n        return (string) $domPdf->output();\n    }\n\n    /**\n     * @throws PdfExportException\n     */\n    protected function renderUsingCommand(string $html): string\n    {\n        $command = config('exports.pdf_command');\n        $inputHtml = tempnam(sys_get_temp_dir(), 'bs-pdfgen-html-');\n        $outputPdf = tempnam(sys_get_temp_dir(), 'bs-pdfgen-output-');\n\n        $replacementsByPlaceholder = [\n            '{input_html_path}' => $inputHtml,\n            '{output_pdf_path}' => $outputPdf,\n        ];\n\n        foreach ($replacementsByPlaceholder as $placeholder => $replacement) {\n            $command = str_replace($placeholder, escapeshellarg($replacement), $command);\n        }\n\n        file_put_contents($inputHtml, $html);\n\n        $timeout = intval(config('exports.pdf_command_timeout'));\n        $process = Process::fromShellCommandline($command);\n        $process->setTimeout($timeout);\n\n        $cleanup = function () use ($inputHtml, $outputPdf) {\n            foreach ([$inputHtml, $outputPdf] as $file) {\n                if (file_exists($file)) {\n                    unlink($file);\n                }\n            }\n        };\n\n        try {\n            $process->run();\n        } catch (ProcessTimedOutException $e) {\n            $cleanup();\n            throw new PdfExportException(\"PDF Export via command failed due to timeout at {$timeout} second(s)\");\n        }\n\n        if (!$process->isSuccessful()) {\n            $cleanup();\n            throw new PdfExportException(\"PDF Export via command failed with exit code {$process->getExitCode()}, stdout: {$process->getOutput()}, stderr: {$process->getErrorOutput()}\");\n        }\n\n        $pdfContents = file_get_contents($outputPdf);\n        $cleanup();\n\n        if ($pdfContents === false) {\n            throw new PdfExportException(\"PDF Export via command failed, unable to read PDF output file\");\n        } else if (empty($pdfContents)) {\n            throw new PdfExportException(\"PDF Export via command failed, PDF output file is empty\");\n        }\n\n        return $pdfContents;\n    }\n\n    protected function renderUsingWkhtml(string $html): string\n    {\n        $snappy = new SnappyPdf($this->getWkhtmlBinaryPath());\n        $options = config('exports.snappy.options');\n        return $snappy->getOutputFromHtml($html, $options);\n    }\n\n    /**\n     * Taken from https://github.com/barryvdh/laravel-dompdf/blob/v2.1.1/src/PDF.php\n     * Copyright (c) 2021 barryvdh, MIT License\n     * https://github.com/barryvdh/laravel-dompdf/blob/v2.1.1/LICENSE\n     */\n    protected function convertEntities(string $subject): string\n    {\n        $entities = [\n            '€' => '&euro;',\n            '£' => '&pound;',\n        ];\n\n        foreach ($entities as $search => $replace) {\n            $subject = str_replace($search, $replace, $subject);\n        }\n        return $subject;\n    }\n}\n"
  },
  {
    "path": "app/Exports/ZipExports/Models/ZipExportAttachment.php",
    "content": "<?php\n\nnamespace BookStack\\Exports\\ZipExports\\Models;\n\nuse BookStack\\Exports\\ZipExports\\ZipExportFiles;\nuse BookStack\\Exports\\ZipExports\\ZipValidationHelper;\nuse BookStack\\Uploads\\Attachment;\n\nfinal class ZipExportAttachment extends ZipExportModel\n{\n    public ?int $id = null;\n    public string $name;\n    public ?string $link = null;\n    public ?string $file = null;\n\n    public function metadataOnly(): void\n    {\n        $this->link = $this->file = null;\n    }\n\n    public static function fromModel(Attachment $model, ZipExportFiles $files): self\n    {\n        $instance = new self();\n        $instance->id = $model->id;\n        $instance->name = $model->name;\n\n        if ($model->external) {\n            $instance->link = $model->path;\n        } else {\n            $instance->file = $files->referenceForAttachment($model);\n        }\n\n        return $instance;\n    }\n\n    public static function fromModelArray(array $attachmentArray, ZipExportFiles $files): array\n    {\n        return array_values(array_map(function (Attachment $attachment) use ($files) {\n            return self::fromModel($attachment, $files);\n        }, $attachmentArray));\n    }\n\n    public static function validate(ZipValidationHelper $context, array $data): array\n    {\n        $rules = [\n            'id'    => ['nullable', 'int', $context->uniqueIdRule('attachment')],\n            'name'  => ['required', 'string', 'min:1'],\n            'link'  => ['required_without:file', 'nullable', 'string'],\n            'file'  => ['required_without:link', 'nullable', 'string', $context->fileReferenceRule()],\n        ];\n\n        return $context->validateData($data, $rules);\n    }\n\n    public static function fromArray(array $data): static\n    {\n        $model = new static();\n\n        $model->id = $data['id'] ?? null;\n        $model->name = $data['name'];\n        $model->link = $data['link'] ?? null;\n        $model->file = $data['file'] ?? null;\n\n        return $model;\n    }\n}\n"
  },
  {
    "path": "app/Exports/ZipExports/Models/ZipExportBook.php",
    "content": "<?php\n\nnamespace BookStack\\Exports\\ZipExports\\Models;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Exports\\ZipExports\\ZipExportFiles;\nuse BookStack\\Exports\\ZipExports\\ZipValidationHelper;\n\nfinal class ZipExportBook extends ZipExportModel\n{\n    public ?int $id = null;\n    public string $name;\n    public ?string $description_html = null;\n    public ?string $cover = null;\n    /** @var ZipExportChapter[] */\n    public array $chapters = [];\n    /** @var ZipExportPage[] */\n    public array $pages = [];\n    /** @var ZipExportTag[] */\n    public array $tags = [];\n\n    public function metadataOnly(): void\n    {\n        $this->description_html = $this->cover = null;\n\n        foreach ($this->chapters as $chapter) {\n            $chapter->metadataOnly();\n        }\n        foreach ($this->pages as $page) {\n            $page->metadataOnly();\n        }\n        foreach ($this->tags as $tag) {\n            $tag->metadataOnly();\n        }\n    }\n\n    public function children(): array\n    {\n        $children = [\n            ...$this->pages,\n            ...$this->chapters,\n        ];\n\n        usort($children, function ($a, $b) {\n            return ($a->priority ?? 0) - ($b->priority ?? 0);\n        });\n\n        return $children;\n    }\n\n    public static function fromModel(Book $model, ZipExportFiles $files): self\n    {\n        $instance = new self();\n        $instance->id = $model->id;\n        $instance->name = $model->name;\n        $instance->description_html = $model->descriptionInfo()->getHtml();\n\n        if ($model->coverInfo()->exists()) {\n            $instance->cover = $files->referenceForImage($model->coverInfo()->getImage());\n        }\n\n        $instance->tags = ZipExportTag::fromModelArray($model->tags()->get()->all());\n\n        $chapters = [];\n        $pages = [];\n\n        $children = $model->getDirectVisibleChildren()->all();\n        foreach ($children as $child) {\n            if ($child instanceof Chapter) {\n                $chapters[] = $child;\n            } else if ($child instanceof Page && !$child->draft) {\n                $pages[] = $child;\n            }\n        }\n\n        $instance->pages = ZipExportPage::fromModelArray($pages, $files);\n        $instance->chapters = ZipExportChapter::fromModelArray($chapters, $files);\n\n        return $instance;\n    }\n\n    public static function validate(ZipValidationHelper $context, array $data): array\n    {\n        $rules = [\n            'id'    => ['nullable', 'int', $context->uniqueIdRule('book')],\n            'name'  => ['required', 'string', 'min:1'],\n            'description_html' => ['nullable', 'string'],\n            'cover' => ['nullable', 'string', $context->fileReferenceRule()],\n            'tags' => ['array'],\n            'pages' => ['array'],\n            'chapters' => ['array'],\n        ];\n\n        $errors = $context->validateData($data, $rules);\n        $errors['tags'] = $context->validateRelations($data['tags'] ?? [], ZipExportTag::class);\n        $errors['pages'] = $context->validateRelations($data['pages'] ?? [], ZipExportPage::class);\n        $errors['chapters'] = $context->validateRelations($data['chapters'] ?? [], ZipExportChapter::class);\n\n        return $errors;\n    }\n\n    public static function fromArray(array $data): static\n    {\n        $model = new static();\n\n        $model->id = $data['id'] ?? null;\n        $model->name = $data['name'];\n        $model->description_html = $data['description_html'] ?? null;\n        $model->cover = $data['cover'] ?? null;\n        $model->tags = ZipExportTag::fromManyArray($data['tags'] ?? []);\n        $model->pages = ZipExportPage::fromManyArray($data['pages'] ?? []);\n        $model->chapters = ZipExportChapter::fromManyArray($data['chapters'] ?? []);\n\n        return $model;\n    }\n}\n"
  },
  {
    "path": "app/Exports/ZipExports/Models/ZipExportChapter.php",
    "content": "<?php\n\nnamespace BookStack\\Exports\\ZipExports\\Models;\n\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Exports\\ZipExports\\ZipExportFiles;\nuse BookStack\\Exports\\ZipExports\\ZipValidationHelper;\n\nfinal class ZipExportChapter extends ZipExportModel\n{\n    public ?int $id = null;\n    public string $name;\n    public ?string $description_html = null;\n    public ?int $priority = null;\n    /** @var ZipExportPage[] */\n    public array $pages = [];\n    /** @var ZipExportTag[] */\n    public array $tags = [];\n\n    public function metadataOnly(): void\n    {\n        $this->description_html = null;\n\n        foreach ($this->pages as $page) {\n            $page->metadataOnly();\n        }\n        foreach ($this->tags as $tag) {\n            $tag->metadataOnly();\n        }\n    }\n\n    public function children(): array\n    {\n        return $this->pages;\n    }\n\n    public static function fromModel(Chapter $model, ZipExportFiles $files): self\n    {\n        $instance = new self();\n        $instance->id = $model->id;\n        $instance->name = $model->name;\n        $instance->description_html = $model->descriptionInfo()->getHtml();\n        $instance->priority = $model->priority;\n        $instance->tags = ZipExportTag::fromModelArray($model->tags()->get()->all());\n\n        $pages = $model->getVisiblePages()->filter(fn (Page $page) => !$page->draft)->all();\n        $instance->pages = ZipExportPage::fromModelArray($pages, $files);\n\n        return $instance;\n    }\n\n    /**\n     * @param Chapter[] $chapterArray\n     * @return self[]\n     */\n    public static function fromModelArray(array $chapterArray, ZipExportFiles $files): array\n    {\n        return array_values(array_map(function (Chapter $chapter) use ($files) {\n            return self::fromModel($chapter, $files);\n        }, $chapterArray));\n    }\n\n    public static function validate(ZipValidationHelper $context, array $data): array\n    {\n        $rules = [\n            'id'    => ['nullable', 'int', $context->uniqueIdRule('chapter')],\n            'name'  => ['required', 'string', 'min:1'],\n            'description_html' => ['nullable', 'string'],\n            'priority' => ['nullable', 'int'],\n            'tags' => ['array'],\n            'pages' => ['array'],\n        ];\n\n        $errors = $context->validateData($data, $rules);\n        $errors['tags'] = $context->validateRelations($data['tags'] ?? [], ZipExportTag::class);\n        $errors['pages'] = $context->validateRelations($data['pages'] ?? [], ZipExportPage::class);\n\n        return $errors;\n    }\n\n    public static function fromArray(array $data): static\n    {\n        $model = new static();\n\n        $model->id = $data['id'] ?? null;\n        $model->name = $data['name'];\n        $model->description_html = $data['description_html'] ?? null;\n        $model->priority = isset($data['priority']) ? intval($data['priority']) : null;\n        $model->tags = ZipExportTag::fromManyArray($data['tags'] ?? []);\n        $model->pages = ZipExportPage::fromManyArray($data['pages'] ?? []);\n\n        return $model;\n    }\n}\n"
  },
  {
    "path": "app/Exports/ZipExports/Models/ZipExportImage.php",
    "content": "<?php\n\nnamespace BookStack\\Exports\\ZipExports\\Models;\n\nuse BookStack\\Exports\\ZipExports\\ZipExportFiles;\nuse BookStack\\Exports\\ZipExports\\ZipValidationHelper;\nuse BookStack\\Uploads\\Image;\nuse Illuminate\\Validation\\Rule;\n\nfinal class ZipExportImage extends ZipExportModel\n{\n    public ?int $id = null;\n    public string $name;\n    public string $file;\n    public string $type;\n\n    public static function fromModel(Image $model, ZipExportFiles $files): self\n    {\n        $instance = new self();\n        $instance->id = $model->id;\n        $instance->name = $model->name;\n        $instance->type = $model->type;\n        $instance->file = $files->referenceForImage($model);\n\n        return $instance;\n    }\n\n    public function metadataOnly(): void\n    {\n        //\n    }\n\n    public static function validate(ZipValidationHelper $context, array $data): array\n    {\n        $acceptedImageTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];\n        $rules = [\n            'id'    => ['nullable', 'int', $context->uniqueIdRule('image')],\n            'name'  => ['required', 'string', 'min:1'],\n            'file'  => ['required', 'string', $context->fileReferenceRule($acceptedImageTypes)],\n            'type'  => ['required', 'string', Rule::in(['gallery', 'drawio'])],\n        ];\n\n        return $context->validateData($data, $rules);\n    }\n\n    public static function fromArray(array $data): static\n    {\n        $model = new static();\n\n        $model->id = $data['id'] ?? null;\n        $model->name = $data['name'];\n        $model->file = $data['file'];\n        $model->type = $data['type'];\n\n        return $model;\n    }\n}\n"
  },
  {
    "path": "app/Exports/ZipExports/Models/ZipExportModel.php",
    "content": "<?php\n\nnamespace BookStack\\Exports\\ZipExports\\Models;\n\nuse BookStack\\Exports\\ZipExports\\ZipValidationHelper;\nuse JsonSerializable;\n\nabstract class ZipExportModel implements JsonSerializable\n{\n    /**\n     * Handle the serialization to JSON.\n     * For these exports, we filter out optional (represented as nullable) fields\n     * just to clean things up and prevent confusion to avoid null states in the\n     * resulting export format itself.\n     */\n    public function jsonSerialize(): array\n    {\n        $publicProps = get_object_vars(...)->__invoke($this);\n        return array_filter($publicProps, fn ($value) => $value !== null);\n    }\n\n    /**\n     * Validate the given array of data intended for this model.\n     * Return an array of validation errors messages.\n     * Child items can be considered in the validation result by returning a keyed\n     * item in the array for its own validation messages.\n     */\n    abstract public static function validate(ZipValidationHelper $context, array $data): array;\n\n    /**\n     * Decode the array of data into this export model.\n     */\n    abstract public static function fromArray(array $data): static;\n\n    /**\n     * Decode an array of array data into an array of export models.\n     * @param array[] $data\n     * @return static[]\n     */\n    public static function fromManyArray(array $data): array\n    {\n        $results = [];\n        foreach ($data as $item) {\n            $results[] = static::fromArray($item);\n        }\n        return $results;\n    }\n\n    /**\n     * Remove additional content in this model to reduce it down\n     * to just essential id/name values for identification.\n     *\n     * The result of this may be something that does not pass validation, but is\n     * simple for the purpose of creating a contents.\n     */\n    abstract public function metadataOnly(): void;\n}\n"
  },
  {
    "path": "app/Exports/ZipExports/Models/ZipExportPage.php",
    "content": "<?php\n\nnamespace BookStack\\Exports\\ZipExports\\Models;\n\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Tools\\PageContent;\nuse BookStack\\Exports\\ZipExports\\ZipExportFiles;\nuse BookStack\\Exports\\ZipExports\\ZipValidationHelper;\n\nfinal class ZipExportPage extends ZipExportModel\n{\n    public ?int $id = null;\n    public string $name;\n    public ?string $html = null;\n    public ?string $markdown = null;\n    public ?int $priority = null;\n    /** @var ZipExportAttachment[] */\n    public array $attachments = [];\n    /** @var ZipExportImage[] */\n    public array $images = [];\n    /** @var ZipExportTag[] */\n    public array $tags = [];\n\n    public function metadataOnly(): void\n    {\n        $this->html = $this->markdown = null;\n\n        foreach ($this->attachments as $attachment) {\n            $attachment->metadataOnly();\n        }\n        foreach ($this->images as $image) {\n            $image->metadataOnly();\n        }\n        foreach ($this->tags as $tag) {\n            $tag->metadataOnly();\n        }\n    }\n\n    public static function fromModel(Page $model, ZipExportFiles $files): self\n    {\n        $instance = new self();\n        $instance->id = $model->id;\n        $instance->name = $model->name;\n        $instance->html = (new PageContent($model))->render();\n        $instance->priority = $model->priority;\n\n        if (!empty($model->markdown)) {\n            $instance->markdown = $model->markdown;\n        }\n\n        $instance->tags = ZipExportTag::fromModelArray($model->tags()->get()->all());\n        $instance->attachments = ZipExportAttachment::fromModelArray($model->attachments()->get()->all(), $files);\n\n        return $instance;\n    }\n\n    /**\n     * @param Page[] $pageArray\n     * @return self[]\n     */\n    public static function fromModelArray(array $pageArray, ZipExportFiles $files): array\n    {\n        return array_values(array_map(function (Page $page) use ($files) {\n            return self::fromModel($page, $files);\n        }, $pageArray));\n    }\n\n    public static function validate(ZipValidationHelper $context, array $data): array\n    {\n        $rules = [\n            'id'    => ['nullable', 'int', $context->uniqueIdRule('page')],\n            'name'  => ['required', 'string', 'min:1'],\n            'html' => ['nullable', 'string'],\n            'markdown' => ['nullable', 'string'],\n            'priority' => ['nullable', 'int'],\n            'attachments' => ['array'],\n            'images' => ['array'],\n            'tags' => ['array'],\n        ];\n\n        $errors = $context->validateData($data, $rules);\n        $errors['attachments'] = $context->validateRelations($data['attachments'] ?? [], ZipExportAttachment::class);\n        $errors['images'] = $context->validateRelations($data['images'] ?? [], ZipExportImage::class);\n        $errors['tags'] = $context->validateRelations($data['tags'] ?? [], ZipExportTag::class);\n\n        return $errors;\n    }\n\n    public static function fromArray(array $data): static\n    {\n        $model = new static();\n\n        $model->id = $data['id'] ?? null;\n        $model->name = $data['name'];\n        $model->html = $data['html'] ?? null;\n        $model->markdown = $data['markdown'] ?? null;\n        $model->priority = isset($data['priority']) ? intval($data['priority']) : null;\n        $model->attachments = ZipExportAttachment::fromManyArray($data['attachments'] ?? []);\n        $model->images = ZipExportImage::fromManyArray($data['images'] ?? []);\n        $model->tags = ZipExportTag::fromManyArray($data['tags'] ?? []);\n\n        return $model;\n    }\n}\n"
  },
  {
    "path": "app/Exports/ZipExports/Models/ZipExportTag.php",
    "content": "<?php\n\nnamespace BookStack\\Exports\\ZipExports\\Models;\n\nuse BookStack\\Activity\\Models\\Tag;\nuse BookStack\\Exports\\ZipExports\\ZipValidationHelper;\n\nfinal class ZipExportTag extends ZipExportModel\n{\n    public string $name;\n    public ?string $value = null;\n\n    public function metadataOnly(): void\n    {\n        $this->value =  null;\n    }\n\n    public static function fromModel(Tag $model): self\n    {\n        $instance = new self();\n        $instance->name = $model->name;\n        $instance->value = $model->value;\n\n        return $instance;\n    }\n\n    public static function fromModelArray(array $tagArray): array\n    {\n        return array_values(array_map(self::fromModel(...), $tagArray));\n    }\n\n    public static function validate(ZipValidationHelper $context, array $data): array\n    {\n        $rules = [\n            'name'  => ['required', 'string', 'min:1'],\n            'value' => ['nullable', 'string'],\n        ];\n\n        return $context->validateData($data, $rules);\n    }\n\n    public static function fromArray(array $data): static\n    {\n        $model = new static();\n\n        $model->name = $data['name'];\n        $model->value = $data['value'] ?? null;\n\n        return $model;\n    }\n}\n"
  },
  {
    "path": "app/Exports/ZipExports/ZipExportBuilder.php",
    "content": "<?php\n\nnamespace BookStack\\Exports\\ZipExports;\n\nuse BookStack\\App\\AppVersion;\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Exceptions\\ZipExportException;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportBook;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportChapter;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportPage;\nuse ZipArchive;\n\nclass ZipExportBuilder\n{\n    protected array $data = [];\n\n    public function __construct(\n        protected ZipExportFiles $files,\n        protected ZipExportReferences $references,\n    ) {\n    }\n\n    /**\n     * @throws ZipExportException\n     */\n    public function buildForPage(Page $page): string\n    {\n        $exportPage = ZipExportPage::fromModel($page, $this->files);\n        $this->data['page'] = $exportPage;\n\n        $this->references->addPage($exportPage);\n\n        return $this->build();\n    }\n\n    /**\n     * @throws ZipExportException\n     */\n    public function buildForChapter(Chapter $chapter): string\n    {\n        $exportChapter = ZipExportChapter::fromModel($chapter, $this->files);\n        $this->data['chapter'] = $exportChapter;\n\n        $this->references->addChapter($exportChapter);\n\n        return $this->build();\n    }\n\n    /**\n     * @throws ZipExportException\n     */\n    public function buildForBook(Book $book): string\n    {\n        $exportBook = ZipExportBook::fromModel($book, $this->files);\n        $this->data['book'] = $exportBook;\n\n        $this->references->addBook($exportBook);\n\n        return $this->build();\n    }\n\n    /**\n     * @throws ZipExportException\n     */\n    protected function build(): string\n    {\n        $this->references->buildReferences($this->files);\n\n        $this->data['exported_at'] = date(DATE_ATOM);\n        $this->data['instance'] = [\n            'id'      => setting('instance-id', ''),\n            'version' => AppVersion::get(),\n        ];\n\n        $zipFile = tempnam(sys_get_temp_dir(), 'bszip-');\n        $zip = new ZipArchive();\n        $opened = $zip->open($zipFile, ZipArchive::OVERWRITE);\n        if ($opened !== true) {\n            throw new ZipExportException('Failed to create zip file for export.');\n        }\n\n        $zip->addFromString('data.json', json_encode($this->data));\n        $zip->addEmptyDir('files');\n\n        $toRemove = [];\n        $addedNames = [];\n\n        try {\n            $this->files->extractEach(function ($filePath, $fileRef) use ($zip, &$toRemove, &$addedNames) {\n                $entryName = \"files/$fileRef\";\n                $zip->addFile($filePath, $entryName);\n                $toRemove[] = $filePath;\n                $addedNames[] = $entryName;\n            });\n        } catch (\\Exception $exception) {\n            // Cleanup the files we've processed so far and respond back with error\n            foreach ($toRemove as $file) {\n                unlink($file);\n            }\n            foreach ($addedNames as $name) {\n                $zip->deleteName($name);\n            }\n            $zip->close();\n            unlink($zipFile);\n            throw new ZipExportException(\"Failed to add files for ZIP export, received error: \" . $exception->getMessage());\n        }\n\n        $zip->close();\n\n        foreach ($toRemove as $file) {\n            unlink($file);\n        }\n\n        return $zipFile;\n    }\n}\n"
  },
  {
    "path": "app/Exports/ZipExports/ZipExportFiles.php",
    "content": "<?php\n\nnamespace BookStack\\Exports\\ZipExports;\n\nuse BookStack\\Uploads\\Attachment;\nuse BookStack\\Uploads\\AttachmentService;\nuse BookStack\\Uploads\\Image;\nuse BookStack\\Uploads\\ImageService;\nuse Illuminate\\Support\\Str;\n\nclass ZipExportFiles\n{\n    /**\n     * References for attachments by attachment ID.\n     * @var array<int, string>\n     */\n    protected array $attachmentRefsById = [];\n\n    /**\n     * References for images by image ID.\n     * @var array<int, string>\n     */\n    protected array $imageRefsById = [];\n\n    public function __construct(\n        protected AttachmentService $attachmentService,\n        protected ImageService $imageService,\n    ) {\n    }\n\n    /**\n     * Gain a reference to the given attachment instance.\n     * This is expected to be a file-based attachment that the user\n     * has visibility of, no permission/access checks are performed here.\n     */\n    public function referenceForAttachment(Attachment $attachment): string\n    {\n        if (isset($this->attachmentRefsById[$attachment->id])) {\n            return $this->attachmentRefsById[$attachment->id];\n        }\n\n        $existingFiles = $this->getAllFileNames();\n        do {\n            $fileName = Str::random(20) . '.' . $attachment->extension;\n        } while (in_array($fileName, $existingFiles));\n\n        $this->attachmentRefsById[$attachment->id] = $fileName;\n\n        return $fileName;\n    }\n\n    /**\n     * Gain a reference to the given image instance.\n     * This is expected to be an image that the user has visibility of,\n     * no permission/access checks are performed here.\n     */\n    public function referenceForImage(Image $image): string\n    {\n        if (isset($this->imageRefsById[$image->id])) {\n            return $this->imageRefsById[$image->id];\n        }\n\n        $existingFiles = $this->getAllFileNames();\n        $extension = pathinfo($image->path, PATHINFO_EXTENSION);\n        do {\n            $fileName = Str::random(20) . '.' . $extension;\n        } while (in_array($fileName, $existingFiles));\n\n        $this->imageRefsById[$image->id] = $fileName;\n\n        return $fileName;\n    }\n\n    protected function getAllFileNames(): array\n    {\n        return array_merge(\n            array_values($this->attachmentRefsById),\n            array_values($this->imageRefsById),\n        );\n    }\n\n    /**\n     * Extract each of the ZIP export tracked files.\n     * Calls the given callback for each tracked file, passing a temporary\n     * file reference of the file contents, and the zip-local tracked reference.\n     */\n    public function extractEach(callable $callback): void\n    {\n        foreach ($this->attachmentRefsById as $attachmentId => $ref) {\n            $attachment = Attachment::query()->find($attachmentId);\n            $stream = $this->attachmentService->streamAttachmentFromStorage($attachment);\n            $tmpFile = tempnam(sys_get_temp_dir(), 'bszipfile-');\n            $tmpFileStream = fopen($tmpFile, 'w');\n            stream_copy_to_stream($stream, $tmpFileStream);\n            $callback($tmpFile, $ref);\n        }\n\n        foreach ($this->imageRefsById as $imageId => $ref) {\n            $image = Image::query()->find($imageId);\n            $stream = $this->imageService->getImageStream($image);\n            $tmpFile = tempnam(sys_get_temp_dir(), 'bszipimage-');\n            $tmpFileStream = fopen($tmpFile, 'w');\n            stream_copy_to_stream($stream, $tmpFileStream);\n            $callback($tmpFile, $ref);\n        }\n    }\n}\n"
  },
  {
    "path": "app/Exports/ZipExports/ZipExportReader.php",
    "content": "<?php\n\nnamespace BookStack\\Exports\\ZipExports;\n\nuse BookStack\\Exceptions\\ZipExportException;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportBook;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportChapter;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportPage;\nuse BookStack\\Util\\WebSafeMimeSniffer;\nuse ZipArchive;\n\nclass ZipExportReader\n{\n    protected ZipArchive $zip;\n    protected bool $open = false;\n\n    public function __construct(\n        protected string $zipPath,\n    ) {\n        $this->zip = new ZipArchive();\n    }\n\n    /**\n     * @throws ZipExportException\n     */\n    protected function open(): void\n    {\n        if ($this->open) {\n            return;\n        }\n\n        // Validate file exists\n        if (!file_exists($this->zipPath) || !is_readable($this->zipPath)) {\n            throw new ZipExportException(trans('errors.import_zip_cant_read'));\n        }\n\n        // Validate file is valid zip\n        $opened = $this->zip->open($this->zipPath, ZipArchive::RDONLY);\n        if ($opened !== true) {\n            throw new ZipExportException(trans('errors.import_zip_cant_read'));\n        }\n\n        $this->open = true;\n    }\n\n    public function close(): void\n    {\n        if ($this->open) {\n            $this->zip->close();\n            $this->open = false;\n        }\n    }\n\n    /**\n     * @throws ZipExportException\n     */\n    public function readData(): array\n    {\n        $this->open();\n\n        $info = $this->zip->statName('data.json');\n        if ($info === false) {\n            throw new ZipExportException(trans('errors.import_zip_cant_decode_data'));\n        }\n\n        $maxSize = max(intval(config()->get('app.upload_limit')), 1) * 1000000;\n        if ($info['size'] > $maxSize) {\n            throw new ZipExportException(trans('errors.import_zip_data_too_large'));\n        }\n\n        // Validate json data exists, including metadata\n        $jsonData = $this->zip->getFromName('data.json') ?: '';\n        $importData = json_decode($jsonData, true);\n        if (!$importData) {\n            throw new ZipExportException(trans('errors.import_zip_cant_decode_data'));\n        }\n\n        return $importData;\n    }\n\n    public function fileExists(string $fileName): bool\n    {\n        return $this->zip->statName(\"files/{$fileName}\") !== false;\n    }\n\n    public function fileWithinSizeLimit(string $fileName): bool\n    {\n        $fileInfo = $this->zip->statName(\"files/{$fileName}\");\n        if ($fileInfo === false) {\n            return false;\n        }\n\n        $maxSize = max(intval(config()->get('app.upload_limit')), 1) * 1000000;\n        return $fileInfo['size'] <= $maxSize;\n    }\n\n    /**\n     * @return false|resource\n     */\n    public function streamFile(string $fileName)\n    {\n        return $this->zip->getStream(\"files/{$fileName}\");\n    }\n\n    /**\n     * Sniff the mime type from the file of given name.\n     */\n    public function sniffFileMime(string $fileName): string\n    {\n        $stream = $this->streamFile($fileName);\n        $sniffContent = fread($stream, 2000);\n\n        return (new WebSafeMimeSniffer())->sniff($sniffContent);\n    }\n\n    /**\n     * @throws ZipExportException\n     */\n    public function decodeDataToExportModel(): ZipExportBook|ZipExportChapter|ZipExportPage\n    {\n        $data = $this->readData();\n        if (isset($data['book'])) {\n            return ZipExportBook::fromArray($data['book']);\n        } else if (isset($data['chapter'])) {\n            return ZipExportChapter::fromArray($data['chapter']);\n        } else if (isset($data['page'])) {\n            return ZipExportPage::fromArray($data['page']);\n        }\n\n        throw new ZipExportException(\"Could not identify content in ZIP file data.\");\n    }\n}\n"
  },
  {
    "path": "app/Exports/ZipExports/ZipExportReferences.php",
    "content": "<?php\n\nnamespace BookStack\\Exports\\ZipExports;\n\nuse BookStack\\App\\Model;\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportAttachment;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportBook;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportChapter;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportImage;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportModel;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportPage;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Uploads\\Attachment;\nuse BookStack\\Uploads\\Image;\nuse BookStack\\Uploads\\ImageService;\n\nclass ZipExportReferences\n{\n    /** @var array<int, ZipExportPage> */\n    protected array $pages = [];\n    /** @var array<int, ZipExportChapter> */\n    protected array $chapters = [];\n    /** @var array<int, ZipExportBook> */\n    protected array $books = [];\n\n    /** @var array<int, ZipExportAttachment> */\n    protected array $attachments = [];\n\n    /** @var array<int, ZipExportImage> */\n    protected array $images = [];\n\n    public function __construct(\n        protected ZipReferenceParser $parser,\n        protected ImageService $imageService,\n    ) {\n    }\n\n    public function addPage(ZipExportPage $page): void\n    {\n        if ($page->id) {\n            $this->pages[$page->id] = $page;\n        }\n\n        foreach ($page->attachments as $attachment) {\n            if ($attachment->id) {\n                $this->attachments[$attachment->id] = $attachment;\n            }\n        }\n    }\n\n    public function addChapter(ZipExportChapter $chapter): void\n    {\n        if ($chapter->id) {\n            $this->chapters[$chapter->id] = $chapter;\n        }\n\n        foreach ($chapter->pages as $page) {\n            $this->addPage($page);\n        }\n    }\n\n    public function addBook(ZipExportBook $book): void\n    {\n        if ($book->id) {\n            $this->books[$book->id] = $book;\n        }\n\n        foreach ($book->pages as $page) {\n            $this->addPage($page);\n        }\n\n        foreach ($book->chapters as $chapter) {\n            $this->addChapter($chapter);\n        }\n    }\n\n    public function buildReferences(ZipExportFiles $files): void\n    {\n        $createHandler = function (ZipExportModel $zipModel) use ($files) {\n            return function (Model $model) use ($files, $zipModel) {\n                return $this->handleModelReference($model, $zipModel, $files);\n            };\n        };\n\n        // Parse page content first\n        foreach ($this->pages as $page) {\n            $handler = $createHandler($page);\n            $page->html = $this->parser->parseLinks($page->html ?? '', $handler);\n            if ($page->markdown) {\n                $page->markdown = $this->parser->parseLinks($page->markdown, $handler);\n            }\n        }\n\n        // Parse chapter description HTML\n        foreach ($this->chapters as $chapter) {\n            if ($chapter->description_html) {\n                $handler = $createHandler($chapter);\n                $chapter->description_html = $this->parser->parseLinks($chapter->description_html, $handler);\n            }\n        }\n\n        // Parse book description HTML\n        foreach ($this->books as $book) {\n            if ($book->description_html) {\n                $handler = $createHandler($book);\n                $book->description_html = $this->parser->parseLinks($book->description_html, $handler);\n            }\n        }\n    }\n\n    protected function handleModelReference(Model $model, ZipExportModel $exportModel, ZipExportFiles $files): ?string\n    {\n        // Handle attachment references\n        // No permission check needed here since they would only already exist in this\n        // reference context if already allowed via their entity access.\n        if ($model instanceof Attachment) {\n            if (isset($this->attachments[$model->id])) {\n                return \"[[bsexport:attachment:{$model->id}]]\";\n            }\n            return null;\n        }\n\n        // Handle image references\n        if ($model instanceof Image) {\n            // Only handle gallery and drawio images\n            if ($model->type !== 'gallery' && $model->type !== 'drawio') {\n                return null;\n            }\n\n            // Handle simple links outside of page content\n            if (!($exportModel instanceof ZipExportPage) && isset($this->images[$model->id])) {\n                return \"[[bsexport:image:{$model->id}]]\";\n            }\n\n            // Get the page which we'll reference this image upon\n            $page = $model->getPage();\n            $pageExportModel = null;\n            if ($page && isset($this->pages[$page->id])) {\n                $pageExportModel = $this->pages[$page->id];\n            } elseif ($exportModel instanceof ZipExportPage) {\n                $pageExportModel = $exportModel;\n            }\n\n            // Add the image to the export if it's accessible or just return the existing reference if already added\n            if (isset($this->images[$model->id]) || ($pageExportModel && $this->imageService->imageAccessible($model))) {\n                if (!isset($this->images[$model->id])) {\n                    $exportImage = ZipExportImage::fromModel($model, $files);\n                    $this->images[$model->id] = $exportImage;\n                    $pageExportModel->images[] = $exportImage;\n                }\n                return \"[[bsexport:image:{$model->id}]]\";\n            }\n\n            return null;\n        }\n\n        // Handle entity references\n        if ($model instanceof Book && isset($this->books[$model->id])) {\n            return \"[[bsexport:book:{$model->id}]]\";\n        } else if ($model instanceof Chapter && isset($this->chapters[$model->id])) {\n            return \"[[bsexport:chapter:{$model->id}]]\";\n        } else if ($model instanceof Page && isset($this->pages[$model->id])) {\n            return \"[[bsexport:page:{$model->id}]]\";\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "app/Exports/ZipExports/ZipExportValidator.php",
    "content": "<?php\n\nnamespace BookStack\\Exports\\ZipExports;\n\nuse BookStack\\Exceptions\\ZipExportException;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportBook;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportChapter;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportPage;\n\nclass ZipExportValidator\n{\n    public function __construct(\n        protected ZipExportReader $reader,\n    ) {\n    }\n\n    public function validate(): array\n    {\n        try {\n            $importData = $this->reader->readData();\n        } catch (ZipExportException $exception) {\n            return ['format' => $exception->getMessage()];\n        }\n\n        $helper = new ZipValidationHelper($this->reader);\n\n        if (isset($importData['book'])) {\n            $modelErrors = ZipExportBook::validate($helper, $importData['book']);\n            $keyPrefix = 'book';\n        } else if (isset($importData['chapter'])) {\n            $modelErrors = ZipExportChapter::validate($helper, $importData['chapter']);\n            $keyPrefix = 'chapter';\n        } else if (isset($importData['page'])) {\n            $modelErrors = ZipExportPage::validate($helper, $importData['page']);\n            $keyPrefix = 'page';\n        } else {\n            return ['format' => trans('errors.import_zip_no_data')];\n        }\n\n        return $this->flattenModelErrors($modelErrors, $keyPrefix);\n    }\n\n    protected function flattenModelErrors(array $errors, string $keyPrefix): array\n    {\n        $flattened = [];\n\n        foreach ($errors as $key => $error) {\n            if (is_array($error)) {\n                $flattened = array_merge($flattened, $this->flattenModelErrors($error, $keyPrefix . '.' . $key));\n            } else {\n                $flattened[$keyPrefix . '.' . $key] = $error;\n            }\n        }\n\n        return $flattened;\n    }\n}\n"
  },
  {
    "path": "app/Exports/ZipExports/ZipFileReferenceRule.php",
    "content": "<?php\n\nnamespace BookStack\\Exports\\ZipExports;\n\nuse Closure;\nuse Illuminate\\Contracts\\Validation\\ValidationRule;\n\nclass ZipFileReferenceRule implements ValidationRule\n{\n    public function __construct(\n        protected ZipValidationHelper $context,\n        protected array $acceptedMimes,\n    ) {\n    }\n\n    /**\n     * @inheritDoc\n     */\n    public function validate(string $attribute, mixed $value, Closure $fail): void\n    {\n        if (!$this->context->zipReader->fileExists($value)) {\n            $fail('validation.zip_file')->translate();\n        }\n\n        if (!$this->context->zipReader->fileWithinSizeLimit($value)) {\n            $fail('validation.zip_file_size')->translate([\n                'attribute' => $value,\n                'size' => config('app.upload_limit'),\n            ]);\n        }\n\n        if (!empty($this->acceptedMimes)) {\n            $fileMime = $this->context->zipReader->sniffFileMime($value);\n            if (!in_array($fileMime, $this->acceptedMimes)) {\n                $fail('validation.zip_file_mime')->translate([\n                    'attribute' => $attribute,\n                    'validTypes' => implode(',', $this->acceptedMimes),\n                    'foundType' => $fileMime\n                ]);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/Exports/ZipExports/ZipImportReferences.php",
    "content": "<?php\n\nnamespace BookStack\\Exports\\ZipExports;\n\nuse BookStack\\App\\Model;\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Repos\\BaseRepo;\nuse BookStack\\Entities\\Repos\\PageRepo;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportBook;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportChapter;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportPage;\nuse BookStack\\Uploads\\Attachment;\nuse BookStack\\Uploads\\Image;\nuse BookStack\\Uploads\\ImageResizer;\n\nclass ZipImportReferences\n{\n    /** @var Page[] */\n    protected array $pages = [];\n    /** @var Chapter[] */\n    protected array $chapters = [];\n    /** @var Book[] */\n    protected array $books = [];\n    /** @var Attachment[] */\n    protected array $attachments = [];\n    /** @var Image[] */\n    protected array $images = [];\n\n    /**\n     * Mapping keyed by \"type:old-reference-id\" with values being the new imported equivalent model.\n     * @var array<string, Model>\n     */\n    protected array $referenceMap = [];\n\n    /** @var array<int, ZipExportPage> */\n    protected array $zipExportPageMap = [];\n    /** @var array<int, ZipExportChapter> */\n    protected array $zipExportChapterMap = [];\n    /** @var array<int, ZipExportBook> */\n    protected array $zipExportBookMap = [];\n\n    public function __construct(\n        protected ZipReferenceParser $parser,\n        protected BaseRepo $baseRepo,\n        protected PageRepo $pageRepo,\n        protected ImageResizer $imageResizer,\n    ) {\n    }\n\n    protected function addReference(string $type, Model $model, ?int $importId): void\n    {\n        if ($importId) {\n            $key = $type . ':' . $importId;\n            $this->referenceMap[$key] = $model;\n        }\n    }\n\n    public function addPage(Page $page, ZipExportPage $exportPage): void\n    {\n        $this->pages[] = $page;\n        $this->zipExportPageMap[$page->id] = $exportPage;\n        $this->addReference('page', $page, $exportPage->id);\n    }\n\n    public function addChapter(Chapter $chapter, ZipExportChapter $exportChapter): void\n    {\n        $this->chapters[] = $chapter;\n        $this->zipExportChapterMap[$chapter->id] = $exportChapter;\n        $this->addReference('chapter', $chapter, $exportChapter->id);\n    }\n\n    public function addBook(Book $book, ZipExportBook $exportBook): void\n    {\n        $this->books[] = $book;\n        $this->zipExportBookMap[$book->id] = $exportBook;\n        $this->addReference('book', $book, $exportBook->id);\n    }\n\n    public function addAttachment(Attachment $attachment, ?int $importId): void\n    {\n        $this->attachments[] = $attachment;\n        $this->addReference('attachment', $attachment, $importId);\n    }\n\n    public function addImage(Image $image, ?int $importId): void\n    {\n        $this->images[] = $image;\n        $this->addReference('image', $image, $importId);\n    }\n\n    protected function handleReference(string $type, int $id): ?string\n    {\n        $key = $type . ':' . $id;\n        $model = $this->referenceMap[$key] ?? null;\n        if ($model instanceof Entity) {\n            return $model->getUrl();\n        } else if ($model instanceof Image) {\n            if ($model->type === 'gallery') {\n                $this->imageResizer->loadGalleryThumbnailsForImage($model, false);\n                return $model->thumbs['display'] ?? $model->url;\n            }\n\n            return $model->url;\n        } else if ($model instanceof Attachment) {\n            return $model->getUrl(false);\n        }\n\n        return null;\n    }\n\n    protected function replaceDrawingIdReferences(string $content): string\n    {\n        $referenceRegex = '/\\sdrawio-diagram=[\\'\"](\\d+)[\\'\"]/';\n\n        $result = preg_replace_callback($referenceRegex, function ($matches) {\n            $key = 'image:' . $matches[1];\n            $model = $this->referenceMap[$key] ?? null;\n            if ($model instanceof Image && $model->type === 'drawio') {\n                return ' drawio-diagram=\"' . $model->id . '\"';\n            }\n            return $matches[0];\n        }, $content);\n\n        return $result ?: $content;\n    }\n\n    public function replaceReferences(): void\n    {\n        foreach ($this->books as $book) {\n            $exportBook = $this->zipExportBookMap[$book->id];\n            $content = $exportBook->description_html ?? '';\n            $parsed = $this->parser->parseReferences($content, $this->handleReference(...));\n\n            $this->baseRepo->update($book, [\n                'description_html' => $parsed,\n            ]);\n        }\n\n        foreach ($this->chapters as $chapter) {\n            $exportChapter = $this->zipExportChapterMap[$chapter->id];\n            $content = $exportChapter->description_html ?? '';\n            $parsed = $this->parser->parseReferences($content, $this->handleReference(...));\n\n            $this->baseRepo->update($chapter, [\n                'description_html' => $parsed,\n            ]);\n        }\n\n        foreach ($this->pages as $page) {\n            $exportPage = $this->zipExportPageMap[$page->id];\n            $contentType = $exportPage->markdown ? 'markdown' : 'html';\n            $content = $exportPage->markdown ?: ($exportPage->html ?: '');\n\n            $parsed = $this->parser->parseReferences($content, $this->handleReference(...));\n            $parsed = $this->replaceDrawingIdReferences($parsed);\n\n            $this->pageRepo->setContentFromInput($page, [\n                $contentType => $parsed,\n            ]);\n        }\n    }\n\n\n    /**\n     * @return Image[]\n     */\n    public function images(): array\n    {\n        return $this->images;\n    }\n\n    /**\n     * @return Attachment[]\n     */\n    public function attachments(): array\n    {\n        return $this->attachments;\n    }\n}\n"
  },
  {
    "path": "app/Exports/ZipExports/ZipImportRunner.php",
    "content": "<?php\n\nnamespace BookStack\\Exports\\ZipExports;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Repos\\BookRepo;\nuse BookStack\\Entities\\Repos\\ChapterRepo;\nuse BookStack\\Entities\\Repos\\PageRepo;\nuse BookStack\\Exceptions\\ZipExportException;\nuse BookStack\\Exceptions\\ZipImportException;\nuse BookStack\\Exports\\Import;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportAttachment;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportBook;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportChapter;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportImage;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportPage;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportTag;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Uploads\\Attachment;\nuse BookStack\\Uploads\\AttachmentService;\nuse BookStack\\Uploads\\FileStorage;\nuse BookStack\\Uploads\\Image;\nuse BookStack\\Uploads\\ImageService;\nuse Illuminate\\Http\\UploadedFile;\n\nclass ZipImportRunner\n{\n    protected array $tempFilesToCleanup = [];\n\n    public function __construct(\n        protected FileStorage $storage,\n        protected PageRepo $pageRepo,\n        protected ChapterRepo $chapterRepo,\n        protected BookRepo $bookRepo,\n        protected ImageService $imageService,\n        protected AttachmentService $attachmentService,\n        protected ZipImportReferences $references,\n    ) {\n    }\n\n    /**\n     * Run the import.\n     * Performs re-validation on zip, validation on parent provided, and permissions for importing\n     * the planned content, before running the import process.\n     * Returns the top-level entity item which was imported.\n     * @throws ZipImportException\n     */\n    public function run(Import $import, ?Entity $parent = null): Entity\n    {\n        $zipPath = $this->getZipPath($import);\n        $reader = new ZipExportReader($zipPath);\n\n        $errors = (new ZipExportValidator($reader))->validate();\n        if ($errors) {\n            throw new ZipImportException([\n                trans('errors.import_validation_failed'),\n                ...$errors,\n            ]);\n        }\n\n        try {\n            $exportModel = $reader->decodeDataToExportModel();\n        } catch (ZipExportException $e) {\n            throw new ZipImportException([$e->getMessage()]);\n        }\n\n        // Validate parent type\n        if ($exportModel instanceof ZipExportBook && ($parent !== null)) {\n            throw new ZipImportException([\"Must not have a parent set for a Book import.\"]);\n        } else if ($exportModel instanceof ZipExportChapter && !($parent instanceof Book)) {\n            throw new ZipImportException([\"Parent book required for chapter import.\"]);\n        } else if ($exportModel instanceof ZipExportPage && !($parent instanceof Book || $parent instanceof Chapter)) {\n            throw new ZipImportException([\"Parent book or chapter required for page import.\"]);\n        }\n\n        $this->ensurePermissionsPermitImport($exportModel, $parent);\n\n        if ($exportModel instanceof ZipExportBook) {\n            $entity = $this->importBook($exportModel, $reader);\n        } else if ($exportModel instanceof ZipExportChapter) {\n            $entity = $this->importChapter($exportModel, $parent, $reader);\n        } else if ($exportModel instanceof ZipExportPage) {\n            $entity = $this->importPage($exportModel, $parent, $reader);\n        } else {\n            throw new ZipImportException(['No importable data found in import data.']);\n        }\n\n        $this->references->replaceReferences();\n\n        $reader->close();\n        $this->cleanup();\n\n        return $entity;\n    }\n\n    /**\n     * Revert any files which have been stored during this import process.\n     * Considers files only, and avoids the database under the\n     * assumption that the database may already have been\n     * reverted as part of a transaction rollback.\n     */\n    public function revertStoredFiles(): void\n    {\n        foreach ($this->references->images() as $image) {\n            $this->imageService->destroyFileAtPath($image->type, $image->path);\n        }\n\n        foreach ($this->references->attachments() as $attachment) {\n            if (!$attachment->external) {\n                $this->attachmentService->deleteFileInStorage($attachment);\n            }\n        }\n\n        $this->cleanup();\n    }\n\n    protected function cleanup(): void\n    {\n        foreach ($this->tempFilesToCleanup as $file) {\n            unlink($file);\n        }\n\n        $this->tempFilesToCleanup = [];\n    }\n\n    protected function importBook(ZipExportBook $exportBook, ZipExportReader $reader): Book\n    {\n        $book = $this->bookRepo->create([\n            'name' => $exportBook->name,\n            'description_html' => $exportBook->description_html ?? '',\n            'image' => $exportBook->cover ? $this->zipFileToUploadedFile($exportBook->cover, $reader) : null,\n            'tags' => $this->exportTagsToInputArray($exportBook->tags ?? []),\n        ]);\n\n        if ($book->coverInfo()->getImage()) {\n            $this->references->addImage($book->coverInfo()->getImage(), null);\n        }\n\n        $children = [\n            ...$exportBook->chapters,\n            ...$exportBook->pages,\n        ];\n\n        usort($children, function (ZipExportPage|ZipExportChapter $a, ZipExportPage|ZipExportChapter $b) {\n            return ($a->priority ?? 0) - ($b->priority ?? 0);\n        });\n\n        foreach ($children as $child) {\n            if ($child instanceof ZipExportChapter) {\n                $this->importChapter($child, $book, $reader);\n            } else if ($child instanceof ZipExportPage) {\n                $this->importPage($child, $book, $reader);\n            }\n        }\n\n        $this->references->addBook($book, $exportBook);\n\n        return $book;\n    }\n\n    protected function importChapter(ZipExportChapter $exportChapter, Book $parent, ZipExportReader $reader): Chapter\n    {\n        $chapter = $this->chapterRepo->create([\n            'name' => $exportChapter->name,\n            'description_html' => $exportChapter->description_html ?? '',\n            'tags' => $this->exportTagsToInputArray($exportChapter->tags ?? []),\n        ], $parent);\n\n        $exportPages = $exportChapter->pages;\n        usort($exportPages, function (ZipExportPage $a, ZipExportPage $b) {\n            return ($a->priority ?? 0) - ($b->priority ?? 0);\n        });\n\n        foreach ($exportPages as $exportPage) {\n            $this->importPage($exportPage, $chapter, $reader);\n        }\n\n        $this->references->addChapter($chapter, $exportChapter);\n\n        return $chapter;\n    }\n\n    protected function importPage(ZipExportPage $exportPage, Book|Chapter $parent, ZipExportReader $reader): Page\n    {\n        $page = $this->pageRepo->getNewDraftPage($parent);\n\n        foreach ($exportPage->attachments as $exportAttachment) {\n            $this->importAttachment($exportAttachment, $page, $reader);\n        }\n\n        foreach ($exportPage->images as $exportImage) {\n            $this->importImage($exportImage, $page, $reader);\n        }\n\n        $this->pageRepo->publishDraft($page, [\n            'name' => $exportPage->name,\n            'markdown' => $exportPage->markdown ?? '',\n            'html' => $exportPage->html ?? '',\n            'tags' => $this->exportTagsToInputArray($exportPage->tags ?? []),\n        ]);\n\n        $this->references->addPage($page, $exportPage);\n\n        return $page;\n    }\n\n    protected function importAttachment(ZipExportAttachment $exportAttachment, Page $page, ZipExportReader $reader): Attachment\n    {\n        if ($exportAttachment->file) {\n            $file = $this->zipFileToUploadedFile($exportAttachment->file, $reader);\n            $attachment = $this->attachmentService->saveNewUpload($file, $page->id);\n            $attachment->name = $exportAttachment->name;\n            $attachment->save();\n        } else {\n            $attachment = $this->attachmentService->saveNewFromLink(\n                $exportAttachment->name,\n                $exportAttachment->link ?? '',\n                $page->id,\n            );\n        }\n\n        $this->references->addAttachment($attachment, $exportAttachment->id);\n\n        return $attachment;\n    }\n\n    protected function importImage(ZipExportImage $exportImage, Page $page, ZipExportReader $reader): Image\n    {\n        $mime = $reader->sniffFileMime($exportImage->file);\n        $extension = explode('/', $mime)[1];\n\n        $file = $this->zipFileToUploadedFile($exportImage->file, $reader);\n        $image = $this->imageService->saveNewFromUpload(\n            $file,\n            $exportImage->type,\n            $page->id,\n            null,\n            null,\n            true,\n            $exportImage->name . '.' . $extension,\n        );\n\n        $image->name = $exportImage->name;\n        $image->save();\n\n        $this->references->addImage($image, $exportImage->id);\n\n        return $image;\n    }\n\n    protected function exportTagsToInputArray(array $exportTags): array\n    {\n        $tags = [];\n\n        /** @var ZipExportTag $tag */\n        foreach ($exportTags as $tag) {\n            $tags[] = ['name' => $tag->name, 'value' => $tag->value ?? ''];\n        }\n\n        return $tags;\n    }\n\n    protected function zipFileToUploadedFile(string $fileName, ZipExportReader $reader): UploadedFile\n    {\n        if (!$reader->fileWithinSizeLimit($fileName)) {\n            throw new ZipImportException([\n                \"File $fileName exceeds app upload limit.\"\n            ]);\n        }\n\n        $tempPath = tempnam(sys_get_temp_dir(), 'bszipextract');\n        $fileStream = $reader->streamFile($fileName);\n        $tempStream = fopen($tempPath, 'wb');\n        stream_copy_to_stream($fileStream, $tempStream);\n        fclose($tempStream);\n\n        $this->tempFilesToCleanup[] = $tempPath;\n\n        return new UploadedFile($tempPath, $fileName);\n    }\n\n    /**\n     * @throws ZipImportException\n     */\n    protected function ensurePermissionsPermitImport(ZipExportPage|ZipExportChapter|ZipExportBook $exportModel, Book|Chapter|null $parent = null): void\n    {\n        $errors = [];\n\n        $chapters = [];\n        $pages = [];\n        $images = [];\n        $attachments = [];\n\n        if ($exportModel instanceof ZipExportBook) {\n            if (!userCan(Permission::BookCreateAll)) {\n                $errors[] = trans('errors.import_perms_books');\n            }\n            array_push($pages, ...$exportModel->pages);\n            array_push($chapters, ...$exportModel->chapters);\n        } else if ($exportModel instanceof ZipExportChapter) {\n            $chapters[] = $exportModel;\n        } else if ($exportModel instanceof ZipExportPage) {\n            $pages[] = $exportModel;\n        }\n\n        foreach ($chapters as $chapter) {\n            array_push($pages, ...$chapter->pages);\n        }\n\n        if (count($chapters) > 0) {\n            $permission = 'chapter-create' . ($parent ? '' : '-all');\n            if (!userCan($permission, $parent)) {\n                $errors[] = trans('errors.import_perms_chapters');\n            }\n        }\n\n        foreach ($pages as $page) {\n            array_push($attachments, ...$page->attachments);\n            array_push($images, ...$page->images);\n        }\n\n        if (count($pages) > 0) {\n            if ($parent) {\n                if (!userCan(Permission::PageCreate, $parent)) {\n                    $errors[] = trans('errors.import_perms_pages');\n                }\n            } else {\n                $hasPermission = userCan(Permission::PageCreateAll) || userCan(Permission::PageCreateOwn);\n                if (!$hasPermission) {\n                    $errors[] = trans('errors.import_perms_pages');\n                }\n            }\n        }\n\n        if (count($images) > 0) {\n            if (!userCan(Permission::ImageCreateAll)) {\n                $errors[] = trans('errors.import_perms_images');\n            }\n        }\n\n        if (count($attachments) > 0) {\n            if (!userCan(Permission::AttachmentCreateAll)) {\n                $errors[] = trans('errors.import_perms_attachments');\n            }\n        }\n\n        if (count($errors)) {\n            throw new ZipImportException($errors);\n        }\n    }\n\n    protected function getZipPath(Import $import): string\n    {\n        if (!$this->storage->isRemote()) {\n            return $this->storage->getSystemPath($import->path);\n        }\n\n        $tempFilePath = tempnam(sys_get_temp_dir(), 'bszip-import-');\n        $tempFile = fopen($tempFilePath, 'wb');\n        $stream = $this->storage->getReadStream($import->path);\n        stream_copy_to_stream($stream, $tempFile);\n        fclose($tempFile);\n\n        $this->tempFilesToCleanup[] = $tempFilePath;\n\n        return $tempFilePath;\n    }\n}\n"
  },
  {
    "path": "app/Exports/ZipExports/ZipReferenceParser.php",
    "content": "<?php\n\nnamespace BookStack\\Exports\\ZipExports;\n\nuse BookStack\\App\\Model;\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse BookStack\\References\\ModelResolvers\\AttachmentModelResolver;\nuse BookStack\\References\\ModelResolvers\\BookLinkModelResolver;\nuse BookStack\\References\\ModelResolvers\\ChapterLinkModelResolver;\nuse BookStack\\References\\ModelResolvers\\CrossLinkModelResolver;\nuse BookStack\\References\\ModelResolvers\\ImageModelResolver;\nuse BookStack\\References\\ModelResolvers\\PageLinkModelResolver;\nuse BookStack\\References\\ModelResolvers\\PagePermalinkModelResolver;\nuse BookStack\\Uploads\\ImageStorage;\n\nclass ZipReferenceParser\n{\n    /**\n     * @var CrossLinkModelResolver[]|null\n     */\n    protected ?array $modelResolvers = null;\n\n    public function __construct(\n        protected EntityQueries $queries\n    ) {\n    }\n\n    /**\n     * Parse and replace references in the given content.\n     * Calls the handler for each model link detected and replaces the link\n     * with the handler return value if provided.\n     * Returns the resulting content with links replaced.\n     * @param callable(Model):(string|null) $handler\n     */\n    public function parseLinks(string $content, callable $handler): string\n    {\n        $linkRegex = $this->getLinkRegex();\n        $matches = [];\n        preg_match_all($linkRegex, $content, $matches);\n\n        if (count($matches) < 2) {\n            return $content;\n        }\n\n        foreach ($matches[1] as $link) {\n            $model = $this->linkToModel($link);\n            if ($model) {\n                $result = $handler($model);\n                if ($result !== null) {\n                    $content = str_replace($link, $result, $content);\n                }\n            }\n        }\n\n        return $content;\n    }\n\n    /**\n     * Parse and replace references in the given content.\n     * Calls the handler for each reference detected and replaces the link\n     * with the handler return value if provided.\n     * Returns the resulting content string with references replaced.\n     * @param callable(string $type, int $id):(string|null) $handler\n     */\n    public function parseReferences(string $content, callable $handler): string\n    {\n        $referenceRegex = '/\\[\\[bsexport:([a-z]+):(\\d+)]]/';\n        $matches = [];\n        preg_match_all($referenceRegex, $content, $matches);\n\n        if (count($matches) < 3) {\n            return $content;\n        }\n\n        for ($i = 0; $i < count($matches[0]); $i++) {\n            $referenceText = $matches[0][$i];\n            $type = strtolower($matches[1][$i]);\n            $id = intval($matches[2][$i]);\n            $result = $handler($type, $id);\n            if ($result !== null) {\n                $content = str_replace($referenceText, $result, $content);\n            }\n        }\n\n        return $content;\n    }\n\n\n    /**\n     * Attempt to resolve the given link to a model using the instance model resolvers.\n     */\n    protected function linkToModel(string $link): ?Model\n    {\n        foreach ($this->getModelResolvers() as $resolver) {\n            $model = $resolver->resolve($link);\n            if (!is_null($model)) {\n                return $model;\n            }\n        }\n\n        return null;\n    }\n\n    protected function getModelResolvers(): array\n    {\n        if (isset($this->modelResolvers)) {\n            return $this->modelResolvers;\n        }\n\n        $this->modelResolvers = [\n            new PagePermalinkModelResolver($this->queries->pages),\n            new PageLinkModelResolver($this->queries->pages),\n            new ChapterLinkModelResolver($this->queries->chapters),\n            new BookLinkModelResolver($this->queries->books),\n            new ImageModelResolver(),\n            new AttachmentModelResolver(),\n        ];\n\n        return $this->modelResolvers;\n    }\n\n    /**\n     * Build the regex to identify links we should handle in content.\n     */\n    protected function getLinkRegex(): string\n    {\n        $urls = [rtrim(url('/'), '/')];\n        $imageUrl = rtrim(ImageStorage::getPublicUrl('/'), '/');\n        if ($urls[0] !== $imageUrl) {\n            $urls[] = $imageUrl;\n        }\n\n\n        $urlBaseRegex = implode('|', array_map(function ($url) {\n            return preg_quote($url, '/');\n        }, $urls));\n\n        return \"/(({$urlBaseRegex}).*?)[\\\\t\\\\n\\\\f>\\\"'=?#()]/\";\n    }\n}\n"
  },
  {
    "path": "app/Exports/ZipExports/ZipUniqueIdRule.php",
    "content": "<?php\n\nnamespace BookStack\\Exports\\ZipExports;\n\nuse Closure;\nuse Illuminate\\Contracts\\Validation\\ValidationRule;\n\nclass ZipUniqueIdRule implements ValidationRule\n{\n    public function __construct(\n        protected ZipValidationHelper $context,\n        protected string $modelType,\n    ) {\n    }\n\n\n    /**\n     * @inheritDoc\n     */\n    public function validate(string $attribute, mixed $value, Closure $fail): void\n    {\n        if ($this->context->hasIdBeenUsed($this->modelType, $value)) {\n            $fail('validation.zip_unique')->translate(['attribute' => $attribute]);\n        }\n    }\n}\n"
  },
  {
    "path": "app/Exports/ZipExports/ZipValidationHelper.php",
    "content": "<?php\n\nnamespace BookStack\\Exports\\ZipExports;\n\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportModel;\nuse Illuminate\\Validation\\Factory;\n\nclass ZipValidationHelper\n{\n    protected Factory $validationFactory;\n\n    /**\n     * Local store of validated IDs (in format \"<type>:<id>\". Example: \"book:2\")\n     * which we can use to check uniqueness.\n     * @var array<string, bool>\n     */\n    protected array $validatedIds = [];\n\n    public function __construct(\n        public ZipExportReader $zipReader,\n    ) {\n        $this->validationFactory = app(Factory::class);\n    }\n\n    public function validateData(array $data, array $rules): array\n    {\n        $messages = $this->validationFactory->make($data, $rules)->errors()->messages();\n\n        foreach ($messages as $key => $message) {\n            $messages[$key] = implode(\"\\n\", $message);\n        }\n\n        return $messages;\n    }\n\n    public function fileReferenceRule(array $acceptedMimes = []): ZipFileReferenceRule\n    {\n        return new ZipFileReferenceRule($this, $acceptedMimes);\n    }\n\n    public function uniqueIdRule(string $type): ZipUniqueIdRule\n    {\n        return new ZipUniqueIdRule($this, $type);\n    }\n\n    public function hasIdBeenUsed(string $type, mixed $id): bool\n    {\n        $key = $type . ':' . $id;\n        if (isset($this->validatedIds[$key])) {\n            return true;\n        }\n\n        $this->validatedIds[$key] = true;\n\n        return false;\n    }\n\n    /**\n     * Validate an array of relation data arrays that are expected\n     * to be for the given ZipExportModel.\n     * @param class-string<ZipExportModel> $model\n     */\n    public function validateRelations(array $relations, string $model): array\n    {\n        $results = [];\n\n        foreach ($relations as $key => $relationData) {\n            if (is_array($relationData)) {\n                $results[$key] = $model::validate($this, $relationData);\n            } else {\n                $results[$key] = [trans('validation.zip_model_expected', ['type' => gettype($relationData)])];\n            }\n        }\n\n        return $results;\n    }\n}\n"
  },
  {
    "path": "app/Facades/Activity.php",
    "content": "<?php\n\nnamespace BookStack\\Facades;\n\nuse Illuminate\\Support\\Facades\\Facade;\n\n/**\n * @mixin \\BookStack\\Activity\\Tools\\ActivityLogger\n */\nclass Activity extends Facade\n{\n    /**\n     * Get the registered name of the component.\n     *\n     * @return string\n     */\n    protected static function getFacadeAccessor()\n    {\n        return 'activity';\n    }\n}\n"
  },
  {
    "path": "app/Facades/Theme.php",
    "content": "<?php\n\nnamespace BookStack\\Facades;\n\nuse BookStack\\Theming\\ThemeService;\nuse Illuminate\\Support\\Facades\\Facade;\n\nclass Theme extends Facade\n{\n    /**\n     * Get the registered name of the component.\n     *\n     * @return string\n     */\n    protected static function getFacadeAccessor()\n    {\n        return ThemeService::class;\n    }\n}\n"
  },
  {
    "path": "app/Http/ApiController.php",
    "content": "<?php\n\nnamespace BookStack\\Http;\n\nuse BookStack\\Api\\ListingResponseBuilder;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Http\\JsonResponse;\n\nabstract class ApiController extends Controller\n{\n    /**\n     * The validation rules for this controller.\n     * Can alternative be defined in a rules() method is they need to be dynamic.\n     *\n     * @var array<string, array<string, string[]>>\n     */\n    protected array $rules = [];\n\n    /**\n     * Provide a paginated listing JSON response in a standard format\n     * taking into account any pagination parameters passed by the user.\n     */\n    protected function apiListingResponse(Builder $query, array $fields, array $modifiers = []): JsonResponse\n    {\n        $listing = new ListingResponseBuilder($query, request(), $fields);\n\n        foreach ($modifiers as $modifier) {\n            $listing->modifyResults($modifier);\n        }\n\n        return $listing->toResponse();\n    }\n\n    /**\n     * Get the validation rules for this controller.\n     * Defaults to a $rules property but can be a rules() method.\n     */\n    public function getValidationRules(): array\n    {\n        return $this->rules();\n    }\n\n    /**\n     * Get the validation rules for the actions in this controller.\n     * Defaults to a $rules property but can be a rules() method.\n     */\n    protected function rules(): array\n    {\n        return $this->rules;\n    }\n}\n"
  },
  {
    "path": "app/Http/Controller.php",
    "content": "<?php\n\nnamespace BookStack\\Http;\n\nuse BookStack\\Activity\\Models\\Loggable;\nuse BookStack\\App\\Model;\nuse BookStack\\Exceptions\\NotifyException;\nuse BookStack\\Facades\\Activity;\nuse BookStack\\Permissions\\Permission;\nuse Illuminate\\Foundation\\Bus\\DispatchesJobs;\nuse Illuminate\\Foundation\\Validation\\ValidatesRequests;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Controller as BaseController;\n\nabstract class Controller extends BaseController\n{\n    use DispatchesJobs;\n    use ValidatesRequests;\n\n    /**\n     * Check if the current user is signed in.\n     */\n    protected function isSignedIn(): bool\n    {\n        return auth()->check();\n    }\n\n    /**\n     * Stops the application and shows a permission error if the application is in demo mode.\n     */\n    protected function preventAccessInDemoMode(): void\n    {\n        if (config('app.env') === 'demo') {\n            $this->showPermissionError();\n        }\n    }\n\n    /**\n     * Adds the page title into the view.\n     */\n    public function setPageTitle(string $title): void\n    {\n        view()->share('pageTitle', $title);\n    }\n\n    /**\n     * On a permission error redirect to home and display the error as a notification.\n     *\n     * @throws NotifyException\n     */\n    protected function showPermissionError(string $redirectLocation = '/'): never\n    {\n        $message = request()->wantsJson() ? trans('errors.permissionJson') : trans('errors.permission');\n\n        throw new NotifyException($message, $redirectLocation, 403);\n    }\n\n    /**\n     * Checks that the current user has the given permission otherwise throw an exception.\n     */\n    protected function checkPermission(string|Permission $permission): void\n    {\n        if (!user() || !user()->can($permission)) {\n            $this->showPermissionError();\n        }\n    }\n\n    /**\n     * Prevent access for guest users beyond this point.\n     */\n    protected function preventGuestAccess(): void\n    {\n        if (user()->isGuest()) {\n            $this->showPermissionError();\n        }\n    }\n\n    /**\n     * Check the current user's permissions against an ownable item otherwise throw an exception.\n     */\n    protected function checkOwnablePermission(string|Permission $permission, Model $ownable, string $redirectLocation = '/'): void\n    {\n        if (!userCan($permission, $ownable)) {\n            $this->showPermissionError($redirectLocation);\n        }\n    }\n\n    /**\n     * Check if a user has a permission or bypass the permission\n     * check if the given callback resolves true.\n     */\n    protected function checkPermissionOr(string|Permission $permission, callable $callback): void\n    {\n        if ($callback() !== true) {\n            $this->checkPermission($permission);\n        }\n    }\n\n    /**\n     * Check if the current user has a permission or bypass if the provided user\n     * id matches the current user.\n     */\n    protected function checkPermissionOrCurrentUser(string|Permission $permission, int $userId): void\n    {\n        $this->checkPermissionOr($permission, function () use ($userId) {\n            return $userId === user()->id;\n        });\n    }\n\n    /**\n     * Send back a JSON error message.\n     */\n    protected function jsonError(string $messageText = '', int $statusCode = 500): JsonResponse\n    {\n        return response()->json(['message' => $messageText, 'status' => 'error'], $statusCode);\n    }\n\n    /**\n     * Create and return a new download response factory using the current request.\n     */\n    protected function download(): DownloadResponseFactory\n    {\n        return new DownloadResponseFactory(request());\n    }\n\n    /**\n     * Show a positive, successful notification to the user on the next view load.\n     */\n    protected function showSuccessNotification(string $message): void\n    {\n        session()->flash('success', $message);\n    }\n\n    /**\n     * Show a warning notification to the user on the next view load.\n     */\n    protected function showWarningNotification(string $message): void\n    {\n        session()->flash('warning', $message);\n    }\n\n    /**\n     * Show an error notification to the user on the next view load.\n     */\n    protected function showErrorNotification(string $message): void\n    {\n        session()->flash('error', $message);\n    }\n\n    /**\n     * Log an activity in the system.\n     */\n    protected function logActivity(string $type, string|Loggable $detail = ''): void\n    {\n        Activity::add($type, $detail);\n    }\n\n    /**\n     * Get the validation rules for image files.\n     */\n    protected function getImageValidationRules(): array\n    {\n        return ['image_extension', 'mimes:jpeg,png,gif,webp,avif', 'max:' . (config('app.upload_limit') * 1000)];\n    }\n\n    /**\n     * Redirect to the URL provided in the request as a '_return' parameter.\n     * Will check that the parameter leads to a URL under the same origin as the application.\n     */\n    protected function redirectToRequest(Request $request): RedirectResponse\n    {\n        $basePath = url('/');\n        $returnUrl = $request->input('_return') ?? $basePath;\n\n        // Only allow use of _return on requests where we expect CSRF to be active\n        // to prevent it potentially being used as an open redirect\n        $allowedMethods = ['POST', 'PUT', 'PATCH', 'DELETE'];\n        if (!in_array($request->getMethod(), $allowedMethods)) {\n            return redirect($basePath);\n        }\n\n        $intendedUrl = parse_url($returnUrl);\n        $baseUrl = parse_url($basePath);\n        $isSameOrigin = ($intendedUrl['host'] ?? '') === ($baseUrl['host'] ?? '')\n            && ($intendedUrl['scheme'] ?? '') === ($baseUrl['scheme'] ?? '')\n            && ($intendedUrl['port'] ?? 0) === ($baseUrl['port'] ?? 0);\n        if (!$isSameOrigin) {\n            return redirect($basePath);\n        }\n\n        return redirect($returnUrl);\n    }\n}\n"
  },
  {
    "path": "app/Http/DownloadResponseFactory.php",
    "content": "<?php\n\nnamespace BookStack\\Http;\n\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\n\nclass DownloadResponseFactory\n{\n    public function __construct(\n        protected Request $request,\n    ) {\n    }\n\n    /**\n     * Create a response that directly forces a download in the browser.\n     */\n    public function directly(string $content, string $fileName): Response\n    {\n        return response()->make($content, 200, $this->getHeaders($fileName, strlen($content)));\n    }\n\n    /**\n     * Create a response that forces a download, from a given stream of content.\n     */\n    public function streamedDirectly($stream, string $fileName, int $fileSize): StreamedResponse\n    {\n        $rangeStream = new RangeSupportedStream($stream, $fileSize, $this->request);\n        $headers = array_merge($this->getHeaders($fileName, $fileSize), $rangeStream->getResponseHeaders());\n        return response()->stream(\n            fn() => $rangeStream->outputAndClose(),\n            $rangeStream->getResponseStatus(),\n            $headers,\n        );\n    }\n\n    /**\n     * Create a response that downloads the given file via a stream.\n     * Has the option to delete the provided file once the stream is closed.\n     */\n    public function streamedFileDirectly(string $filePath, string $fileName, bool $deleteAfter = false): StreamedResponse\n    {\n        $fileSize = filesize($filePath);\n        $stream = fopen($filePath, 'r');\n\n        if ($deleteAfter) {\n            // Delete the given file if it still exists after the app terminates\n            $callback = function () use ($filePath) {\n                if (file_exists($filePath)) {\n                    unlink($filePath);\n                }\n            };\n\n            // We watch both app terminate and php shutdown to cover both normal app termination\n            // as well as other potential scenarios (connection termination).\n            app()->terminating($callback);\n            register_shutdown_function($callback);\n        }\n\n        return $this->streamedDirectly($stream, $fileName, $fileSize);\n    }\n\n\n    /**\n     * Create a file download response that provides the file with a content-type\n     * correct for the file, in a way so the browser can show the content in browser,\n     * for a given content stream.\n     */\n    public function streamedInline($stream, string $fileName, int $fileSize): StreamedResponse\n    {\n        $rangeStream = new RangeSupportedStream($stream, $fileSize, $this->request);\n        $mime = $rangeStream->sniffMime(pathinfo($fileName, PATHINFO_EXTENSION));\n        $headers = array_merge($this->getHeaders($fileName, $fileSize, $mime), $rangeStream->getResponseHeaders());\n\n        return response()->stream(\n            fn() => $rangeStream->outputAndClose(),\n            $rangeStream->getResponseStatus(),\n            $headers,\n        );\n    }\n\n    /**\n     * Create a response that provides the given file via a stream with detected content-type.\n     * Has the option to delete the provided file once the stream is closed.\n     */\n    public function streamedFileInline(string $filePath, ?string $fileName = null): StreamedResponse\n    {\n        $fileSize = filesize($filePath);\n        $stream = fopen($filePath, 'r');\n\n        if ($fileName === null) {\n            $fileName = basename($filePath);\n        }\n\n        return $this->streamedInline($stream, $fileName, $fileSize);\n    }\n\n    /**\n     * Get the common headers to provide for a download response.\n     */\n    protected function getHeaders(string $fileName, int $fileSize, string $mime = 'application/octet-stream'): array\n    {\n        $disposition = ($mime === 'application/octet-stream') ? 'attachment' : 'inline';\n\n        $downloadName = str_replace(['\"', '/', '\\\\', '$'], '', $fileName);\n        $downloadName = preg_replace('/[\\x00-\\x1F\\x7F]/', '', $downloadName);\n        $encodedDownloadName = rawurlencode($downloadName);\n\n        return [\n            'Content-Type'           => $mime,\n            'Content-Length'         => $fileSize,\n            'Content-Disposition'    => \"{$disposition}; filename*=UTF-8''{$encodedDownloadName}\",\n            'X-Content-Type-Options' => 'nosniff',\n        ];\n    }\n}\n"
  },
  {
    "path": "app/Http/HttpClientHistory.php",
    "content": "<?php\n\nnamespace BookStack\\Http;\n\nuse GuzzleHttp\\Psr7\\Request as GuzzleRequest;\n\nclass HttpClientHistory\n{\n    public function __construct(\n        protected &$container\n    ) {\n    }\n\n    public function requestCount(): int\n    {\n        return count($this->container);\n    }\n\n    public function requestAt(int $index): ?GuzzleRequest\n    {\n        return $this->container[$index]['request'] ?? null;\n    }\n\n    public function latestRequest(): ?GuzzleRequest\n    {\n        return $this->requestAt($this->requestCount() - 1);\n    }\n\n    public function all(): array\n    {\n        return $this->container;\n    }\n}\n"
  },
  {
    "path": "app/Http/HttpRequestService.php",
    "content": "<?php\n\nnamespace BookStack\\Http;\n\nuse GuzzleHttp\\Client;\nuse GuzzleHttp\\Handler\\MockHandler;\nuse GuzzleHttp\\HandlerStack;\nuse GuzzleHttp\\Middleware;\nuse GuzzleHttp\\Psr7\\Request as GuzzleRequest;\nuse GuzzleHttp\\Psr7\\Response;\nuse Psr\\Http\\Client\\ClientInterface;\n\nclass HttpRequestService\n{\n    protected ?HandlerStack $handler = null;\n\n    /**\n     * Build a new http client for sending requests on.\n     */\n    public function buildClient(int $timeout, array $options = []): ClientInterface\n    {\n        $defaultOptions = [\n            'timeout' => $timeout,\n            'handler' => $this->handler,\n        ];\n\n        return new Client(array_merge($options, $defaultOptions));\n    }\n\n    /**\n     * Create a new JSON http request for use with a client.\n     */\n    public function jsonRequest(string $method, string $uri, array $data): GuzzleRequest\n    {\n        $headers = ['Content-Type' => 'application/json'];\n        return new GuzzleRequest($method, $uri, $headers, json_encode($data));\n    }\n\n    /**\n     * Mock any http clients built from this service, and response with the given responses.\n     * Returns history which can then be queried.\n     * @link https://docs.guzzlephp.org/en/stable/testing.html#history-middleware\n     */\n    public function mockClient(array $responses = [], bool $pad = true): HttpClientHistory\n    {\n        // By default, we pad out the responses with 10 successful values so that requests will be\n        // properly recorded for inspection. Otherwise, we can't later check if we're received\n        // too many requests.\n        if ($pad) {\n            $response = new Response(200, [], 'success');\n            $responses = array_merge($responses, array_fill(0, 10, $response));\n        }\n\n        $container = [];\n        $history = Middleware::history($container);\n        $mock = new MockHandler($responses);\n        $this->handler = HandlerStack::create($mock);\n        $this->handler->push($history, 'history');\n\n        return new HttpClientHistory($container);\n    }\n\n    /**\n     * Clear mocking that has been set up for clients.\n     */\n    public function clearMocking(): void\n    {\n        $this->handler = null;\n    }\n}\n"
  },
  {
    "path": "app/Http/Kernel.php",
    "content": "<?php\n\nnamespace BookStack\\Http;\n\nuse Illuminate\\Foundation\\Http\\Kernel as HttpKernel;\n\nclass Kernel extends HttpKernel\n{\n    /**\n     * The application's global HTTP middleware stack.\n     * These middleware are run during every request to your application.\n     *\n     * @var list<class-string>\n     */\n    protected $middleware = [\n        \\BookStack\\Http\\Middleware\\PreventRequestsDuringMaintenance::class,\n        \\Illuminate\\Foundation\\Http\\Middleware\\ValidatePostSize::class,\n        \\BookStack\\Http\\Middleware\\TrimStrings::class,\n        \\BookStack\\Http\\Middleware\\TrustProxies::class,\n        \\BookStack\\Http\\Middleware\\PreventResponseCaching::class,\n    ];\n\n    /**\n     * The application's route middleware groups.\n     *\n     * @var array<string, array<int, class-string>>\n     */\n    protected $middlewareGroups = [\n        'web' => [\n            \\BookStack\\Http\\Middleware\\ApplyCspRules::class,\n            \\BookStack\\Http\\Middleware\\EncryptCookies::class,\n            \\Illuminate\\Cookie\\Middleware\\AddQueuedCookiesToResponse::class,\n            \\BookStack\\Http\\Middleware\\StartSessionExtended::class,\n            \\Illuminate\\View\\Middleware\\ShareErrorsFromSession::class,\n            \\BookStack\\Http\\Middleware\\VerifyCsrfToken::class,\n            \\BookStack\\Http\\Middleware\\CheckEmailConfirmed::class,\n            \\BookStack\\Http\\Middleware\\RunThemeActions::class,\n            \\BookStack\\Http\\Middleware\\Localization::class,\n        ],\n        'api' => [\n            \\BookStack\\Http\\Middleware\\ThrottleApiRequests::class,\n            \\BookStack\\Http\\Middleware\\EncryptCookies::class,\n            \\BookStack\\Http\\Middleware\\StartSessionIfCookieExists::class,\n            \\BookStack\\Http\\Middleware\\ApiAuthenticate::class,\n            \\BookStack\\Http\\Middleware\\CheckEmailConfirmed::class,\n        ],\n    ];\n\n    /**\n     * The application's middleware aliases.\n     *\n     * @var array<string, class-string>\n     */\n    protected $middlewareAliases = [\n        'auth'       => \\BookStack\\Http\\Middleware\\Authenticate::class,\n        'can'        => \\BookStack\\Http\\Middleware\\CheckUserHasPermission::class,\n        'guest'      => \\BookStack\\Http\\Middleware\\RedirectIfAuthenticated::class,\n        'throttle'   => \\Illuminate\\Routing\\Middleware\\ThrottleRequests::class,\n        'guard'      => \\BookStack\\Http\\Middleware\\CheckGuard::class,\n        'mfa-setup'  => \\BookStack\\Http\\Middleware\\AuthenticatedOrPendingMfa::class,\n    ];\n}\n"
  },
  {
    "path": "app/Http/Middleware/ApiAuthenticate.php",
    "content": "<?php\n\nnamespace BookStack\\Http\\Middleware;\n\nuse BookStack\\Exceptions\\ApiAuthException;\nuse BookStack\\Permissions\\Permission;\nuse Closure;\nuse Illuminate\\Http\\Request;\n\nclass ApiAuthenticate\n{\n    /**\n     * Handle an incoming request.\n     *\n     * @throws ApiAuthException\n     */\n    public function handle(Request $request, Closure $next)\n    {\n        // Validate the token and it's users API access\n        $this->ensureAuthorizedBySessionOrToken($request);\n\n        return $next($request);\n    }\n\n    /**\n     * Ensure the current user can access authenticated API routes, either via existing session\n     * authentication or via API Token authentication.\n     *\n     * @throws ApiAuthException\n     */\n    protected function ensureAuthorizedBySessionOrToken(Request $request): void\n    {\n        // Use the active user session already exists.\n        // This is to make it easy to explore API endpoints via the UI.\n        if (session()->isStarted()) {\n            // Ensure the user has API access permission\n            if (!$this->sessionUserHasApiAccess()) {\n                throw new ApiAuthException(trans('errors.api_user_no_api_permission'), 403);\n            }\n\n            // Only allow GET requests for cookie-based API usage\n            if ($request->method() !== 'GET') {\n                throw new ApiAuthException(trans('errors.api_cookie_auth_only_get'), 403);\n            }\n\n            return;\n        }\n\n        // Set our api guard to be the default for this request lifecycle.\n        auth()->shouldUse('api');\n\n        // Validate the token and its users API access\n        auth()->authenticate();\n    }\n\n    /**\n     * Check if the active session user has API access.\n     */\n    protected function sessionUserHasApiAccess(): bool\n    {\n        $hasApiPermission = user()->can(Permission::AccessApi);\n\n        return $hasApiPermission && user()->hasAppAccess();\n    }\n}\n"
  },
  {
    "path": "app/Http/Middleware/ApplyCspRules.php",
    "content": "<?php\n\nnamespace BookStack\\Http\\Middleware;\n\nuse BookStack\\Util\\CspService;\nuse Closure;\nuse Illuminate\\Http\\Request;\n\nclass ApplyCspRules\n{\n    protected CspService $cspService;\n\n    public function __construct(CspService $cspService)\n    {\n        $this->cspService = $cspService;\n    }\n\n    /**\n     * Handle an incoming request.\n     *\n     * @param Request $request\n     * @param Closure $next\n     *\n     * @return mixed\n     */\n    public function handle($request, Closure $next)\n    {\n        view()->share('cspNonce', $this->cspService->getNonce());\n        if ($this->cspService->allowedIFrameHostsConfigured()) {\n            config()->set('session.same_site', 'none');\n        }\n\n        $response = $next($request);\n\n        $cspHeader = $this->cspService->getCspHeader();\n        $response->headers->set('Content-Security-Policy', $cspHeader, false);\n\n        return $response;\n    }\n}\n"
  },
  {
    "path": "app/Http/Middleware/Authenticate.php",
    "content": "<?php\n\nnamespace BookStack\\Http\\Middleware;\n\nuse Closure;\nuse Illuminate\\Http\\Request;\n\nclass Authenticate\n{\n    /**\n     * Handle an incoming request.\n     */\n    public function handle(Request $request, Closure $next)\n    {\n        if (!user()->hasAppAccess()) {\n            if ($request->ajax()) {\n                return response('Unauthorized.', 401);\n            }\n\n            return redirect()->guest(url('/login'));\n        }\n\n        return $next($request);\n    }\n}\n"
  },
  {
    "path": "app/Http/Middleware/AuthenticatedOrPendingMfa.php",
    "content": "<?php\n\nnamespace BookStack\\Http\\Middleware;\n\nuse BookStack\\Access\\LoginService;\nuse BookStack\\Access\\Mfa\\MfaSession;\nuse Closure;\n\nclass AuthenticatedOrPendingMfa\n{\n    protected $loginService;\n    protected $mfaSession;\n\n    public function __construct(LoginService $loginService, MfaSession $mfaSession)\n    {\n        $this->loginService = $loginService;\n        $this->mfaSession = $mfaSession;\n    }\n\n    /**\n     * Handle an incoming request.\n     *\n     * @param \\Illuminate\\Http\\Request $request\n     * @param \\Closure                 $next\n     *\n     * @return mixed\n     */\n    public function handle($request, Closure $next)\n    {\n        $user = auth()->user();\n        $loggedIn = $user !== null;\n        $lastAttemptUser = $this->loginService->getLastLoginAttemptUser();\n\n        if ($loggedIn || ($lastAttemptUser && $this->mfaSession->isPendingMfaSetup($lastAttemptUser))) {\n            return $next($request);\n        }\n\n        return redirect()->to(url('/login'));\n    }\n}\n"
  },
  {
    "path": "app/Http/Middleware/CheckEmailConfirmed.php",
    "content": "<?php\n\nnamespace BookStack\\Http\\Middleware;\n\nuse BookStack\\Access\\EmailConfirmationService;\nuse BookStack\\Users\\Models\\User;\nuse Closure;\n\n/**\n * Check that the user's email address is confirmed.\n *\n * As of v21.08 this is technically not required but kept as a prevention\n * to log out any users that may be logged in but in an \"awaiting confirmation\" state.\n * We'll keep this for a while until it'd be very unlikely for a user to be upgrading from\n * a pre-v21.08 version.\n *\n * Ideally we'd simply invalidate all existing sessions upon update but that has\n * proven to be a lot more difficult than expected.\n */\nclass CheckEmailConfirmed\n{\n    protected $confirmationService;\n\n    public function __construct(EmailConfirmationService $confirmationService)\n    {\n        $this->confirmationService = $confirmationService;\n    }\n\n    /**\n     * Handle an incoming request.\n     *\n     * @param \\Illuminate\\Http\\Request $request\n     * @param \\Closure                 $next\n     *\n     * @return mixed\n     */\n    public function handle($request, Closure $next)\n    {\n        /** @var User $user */\n        $user = auth()->user();\n        if (auth()->check() && !$user->email_confirmed && $this->confirmationService->confirmationRequired()) {\n            auth()->logout();\n\n            return redirect()->to('/');\n        }\n\n        return $next($request);\n    }\n}\n"
  },
  {
    "path": "app/Http/Middleware/CheckGuard.php",
    "content": "<?php\n\nnamespace BookStack\\Http\\Middleware;\n\nuse Closure;\n\nclass CheckGuard\n{\n    /**\n     * Handle an incoming request.\n     *\n     * @param \\Illuminate\\Http\\Request $request\n     * @param \\Closure                 $next\n     * @param string                   $allowedGuards\n     *\n     * @return mixed\n     */\n    public function handle($request, Closure $next, ...$allowedGuards)\n    {\n        $activeGuard = config('auth.method');\n        if (!in_array($activeGuard, $allowedGuards)) {\n            session()->flash('error', trans('errors.permission'));\n\n            return redirect('/');\n        }\n\n        return $next($request);\n    }\n}\n"
  },
  {
    "path": "app/Http/Middleware/CheckUserHasPermission.php",
    "content": "<?php\n\nnamespace BookStack\\Http\\Middleware;\n\nuse BookStack\\Permissions\\Permission;\nuse Closure;\nuse Illuminate\\Http\\Request;\n\nclass CheckUserHasPermission\n{\n    /**\n     * Handle an incoming request.\n     *\n     * @return mixed\n     */\n    public function handle(Request $request, Closure $next, string|Permission $permission)\n    {\n        if (!user()->can($permission)) {\n            return $this->errorResponse($request);\n        }\n\n        return $next($request);\n    }\n\n    protected function errorResponse(Request $request)\n    {\n        if ($request->wantsJson()) {\n            return response()->json(['error' => trans('errors.permissionJson')], 403);\n        }\n\n        session()->flash('error', trans('errors.permission'));\n\n        return redirect('/');\n    }\n}\n"
  },
  {
    "path": "app/Http/Middleware/EncryptCookies.php",
    "content": "<?php\n\nnamespace BookStack\\Http\\Middleware;\n\nuse Illuminate\\Cookie\\Middleware\\EncryptCookies as Middleware;\n\nclass EncryptCookies extends Middleware\n{\n    /**\n     * The names of the cookies that should not be encrypted.\n     *\n     * @var array<int, string>\n     */\n    protected $except = [\n        //\n    ];\n}\n"
  },
  {
    "path": "app/Http/Middleware/Localization.php",
    "content": "<?php\n\nnamespace BookStack\\Http\\Middleware;\n\nuse BookStack\\Translation\\LocaleManager;\nuse Closure;\n\nclass Localization\n{\n    public function __construct(\n        protected LocaleManager $localeManager\n    ) {\n    }\n\n    /**\n     * Handle an incoming request.\n     *\n     * @param \\Illuminate\\Http\\Request $request\n     * @param \\Closure                 $next\n     *\n     * @return mixed\n     */\n    public function handle($request, Closure $next)\n    {\n        // Share details of the user's locale for use in views\n        $userLocale = $this->localeManager->getForUser(user());\n        view()->share('locale', $userLocale);\n\n        // Set locale for system components\n        app()->setLocale($userLocale->appLocale());\n\n        return $next($request);\n    }\n}\n"
  },
  {
    "path": "app/Http/Middleware/PreventRequestsDuringMaintenance.php",
    "content": "<?php\n\nnamespace BookStack\\Http\\Middleware;\n\nuse Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance as Middleware;\n\nclass PreventRequestsDuringMaintenance extends Middleware\n{\n    /**\n     * The URIs that should be reachable while maintenance mode is enabled.\n     *\n     * @var array<int, string>\n     */\n    protected $except = [\n        //\n    ];\n}\n"
  },
  {
    "path": "app/Http/Middleware/PreventResponseCaching.php",
    "content": "<?php\n\nnamespace BookStack\\Http\\Middleware;\n\nuse Closure;\nuse Symfony\\Component\\HttpFoundation\\Response;\n\nclass PreventResponseCaching\n{\n    /**\n     * Paths to ignore when preventing response caching.\n     */\n    protected array $ignoredPathPrefixes = [\n        'theme/',\n    ];\n\n    /**\n     * Handle an incoming request.\n     *\n     * @param \\Illuminate\\Http\\Request $request\n     * @param \\Closure                 $next\n     *\n     * @return mixed\n     */\n    public function handle($request, Closure $next)\n    {\n        /** @var Response $response */\n        $response = $next($request);\n\n        $path = $request->path();\n        foreach ($this->ignoredPathPrefixes as $ignoredPath) {\n            if (str_starts_with($path, $ignoredPath)) {\n                return $response;\n            }\n        }\n\n        $response->headers->set('Cache-Control', 'no-cache, no-store, private');\n        $response->headers->set('Expires', 'Sun, 12 Jul 2015 19:01:00 GMT');\n\n        return $response;\n    }\n}\n"
  },
  {
    "path": "app/Http/Middleware/RedirectIfAuthenticated.php",
    "content": "<?php\n\nnamespace BookStack\\Http\\Middleware;\n\nuse BookStack\\App\\Providers\\RouteServiceProvider;\nuse Closure;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\Auth;\nuse Symfony\\Component\\HttpFoundation\\Response;\n\nclass RedirectIfAuthenticated\n{\n    /**\n     * Handle an incoming request.\n     *\n     * @param Closure(Request): (Response) $next\n     */\n    public function handle(Request $request, Closure $next, string ...$guards): Response\n    {\n        $guards = empty($guards) ? [null] : $guards;\n\n        foreach ($guards as $guard) {\n            if (Auth::guard($guard)->check()) {\n                return redirect(RouteServiceProvider::HOME);\n            }\n        }\n\n        return $next($request);\n    }\n}\n"
  },
  {
    "path": "app/Http/Middleware/RunThemeActions.php",
    "content": "<?php\n\nnamespace BookStack\\Http\\Middleware;\n\nuse BookStack\\Facades\\Theme;\nuse BookStack\\Theming\\ThemeEvents;\nuse Closure;\n\nclass RunThemeActions\n{\n    /**\n     * Handle an incoming request.\n     *\n     * @param \\Illuminate\\Http\\Request $request\n     * @param \\Closure                 $next\n     *\n     * @return mixed\n     */\n    public function handle($request, Closure $next)\n    {\n        $earlyResponse = Theme::dispatch(ThemeEvents::WEB_MIDDLEWARE_BEFORE, $request);\n        if (!is_null($earlyResponse)) {\n            return $earlyResponse;\n        }\n\n        $response = $next($request);\n        $response = Theme::dispatch(ThemeEvents::WEB_MIDDLEWARE_AFTER, $request, $response) ?? $response;\n\n        return $response;\n    }\n}\n"
  },
  {
    "path": "app/Http/Middleware/StartSessionExtended.php",
    "content": "<?php\n\nnamespace BookStack\\Http\\Middleware;\n\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Session\\Middleware\\StartSession as Middleware;\n\n/**\n * An extended version of the default Laravel \"StartSession\" middleware\n * with customizations applied as required:\n *\n * - Adds filtering for the request URLs stored in session history.\n */\nclass StartSessionExtended extends Middleware\n{\n    protected static array $pathPrefixesExcludedFromHistory = [\n        'uploads/images/',\n        'dist/',\n        'manifest.json',\n        'opensearch.xml',\n    ];\n\n    /**\n     * @inheritdoc\n     */\n    protected function storeCurrentUrl(Request $request, $session): void\n    {\n        $requestPath = strtolower($request->path());\n        foreach (static::$pathPrefixesExcludedFromHistory as $excludedPath) {\n            if (str_starts_with($requestPath, $excludedPath)) {\n                return;\n            }\n        }\n\n        parent::storeCurrentUrl($request, $session);\n    }\n}\n"
  },
  {
    "path": "app/Http/Middleware/StartSessionIfCookieExists.php",
    "content": "<?php\n\nnamespace BookStack\\Http\\Middleware;\n\nuse Closure;\nuse Illuminate\\Session\\Middleware\\StartSession as Middleware;\n\nclass StartSessionIfCookieExists extends Middleware\n{\n    /**\n     * Handle an incoming request.\n     */\n    public function handle($request, Closure $next)\n    {\n        $sessionCookieName = config('session.cookie');\n        if ($request->cookies->has($sessionCookieName)) {\n            return parent::handle($request, $next);\n        }\n\n        return $next($request);\n    }\n}\n"
  },
  {
    "path": "app/Http/Middleware/ThrottleApiRequests.php",
    "content": "<?php\n\nnamespace BookStack\\Http\\Middleware;\n\nuse Illuminate\\Routing\\Middleware\\ThrottleRequests as Middleware;\n\nclass ThrottleApiRequests extends Middleware\n{\n    /**\n     * Resolve the number of attempts if the user is authenticated or not.\n     */\n    protected function resolveMaxAttempts($request, $maxAttempts): int\n    {\n        return (int) config('api.requests_per_minute');\n    }\n}\n"
  },
  {
    "path": "app/Http/Middleware/TrimStrings.php",
    "content": "<?php\n\nnamespace BookStack\\Http\\Middleware;\n\nuse Illuminate\\Foundation\\Http\\Middleware\\TrimStrings as Middleware;\n\nclass TrimStrings extends Middleware\n{\n    /**\n     * The names of the attributes that should not be trimmed.\n     *\n     * @var array<int, string>\n     */\n    protected $except = [\n        'password',\n        'password_confirmation',\n        'password-confirm',\n    ];\n}\n"
  },
  {
    "path": "app/Http/Middleware/TrustHosts.php",
    "content": "<?php\n\nnamespace BookStack\\Http\\Middleware;\n\nuse Illuminate\\Http\\Middleware\\TrustHosts as Middleware;\n\nclass TrustHosts extends Middleware\n{\n    /**\n     * Get the host patterns that should be trusted.\n     *\n     * @return array<int, string|null>\n     */\n    public function hosts(): array\n    {\n        return [\n            $this->allSubdomainsOfApplicationUrl(),\n        ];\n    }\n}\n"
  },
  {
    "path": "app/Http/Middleware/TrustProxies.php",
    "content": "<?php\n\nnamespace BookStack\\Http\\Middleware;\n\nuse Closure;\nuse Illuminate\\Http\\Middleware\\TrustProxies as Middleware;\nuse Illuminate\\Http\\Request;\n\nclass TrustProxies extends Middleware\n{\n    /**\n     * The trusted proxies for this application.\n     *\n     * @var array<int,string>|string|null\n     */\n    protected $proxies;\n\n    /**\n     * The headers that should be used to detect proxies.\n     *\n     * @var int\n     */\n    protected $headers = Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_AWS_ELB;\n\n    /**\n     * Handle the request, Set the correct user-configured proxy information.\n     *\n     * @param Request $request\n     * @param Closure $next\n     *\n     * @return mixed\n     */\n    public function handle(Request $request, Closure $next)\n    {\n        $setProxies = config('app.proxies');\n        if ($setProxies !== '**' && $setProxies !== '*' && $setProxies !== '') {\n            $setProxies = explode(',', $setProxies);\n        }\n        $this->proxies = $setProxies;\n\n        return parent::handle($request, $next);\n    }\n}\n"
  },
  {
    "path": "app/Http/Middleware/VerifyCsrfToken.php",
    "content": "<?php\n\nnamespace BookStack\\Http\\Middleware;\n\nuse Illuminate\\Foundation\\Http\\Middleware\\VerifyCsrfToken as Middleware;\n\nclass VerifyCsrfToken extends Middleware\n{\n    /**\n     * Indicates whether the XSRF-TOKEN cookie should be set on the response.\n     *\n     * @var bool\n     */\n    protected $addHttpCookie = true;\n\n    /**\n     * The URIs that should be excluded from CSRF verification.\n     *\n     * @var array<int, string>\n     */\n    protected $except = [\n        'saml2/*',\n    ];\n}\n"
  },
  {
    "path": "app/Http/RangeSupportedStream.php",
    "content": "<?php\n\nnamespace BookStack\\Http;\n\nuse BookStack\\Util\\WebSafeMimeSniffer;\nuse Illuminate\\Http\\Request;\n\n/**\n * Helper wrapper for range-based stream response handling.\n * Much of this used symfony/http-foundation as a reference during build.\n * URL: https://github.com/symfony/http-foundation/blob/v6.0.20/BinaryFileResponse.php\n * License: MIT license, Copyright (c) Fabien Potencier.\n */\nclass RangeSupportedStream\n{\n    protected string $sniffContent = '';\n    protected array $responseHeaders = [];\n    protected int $responseStatus = 200;\n\n    protected int $responseLength = 0;\n    protected int $responseOffset = 0;\n\n    public function __construct(\n        protected $stream,\n        protected int $fileSize,\n        Request $request,\n    ) {\n        $this->responseLength = $this->fileSize;\n        $this->parseRequest($request);\n    }\n\n    /**\n     * Sniff a mime type from the stream.\n     */\n    public function sniffMime(string $extension = ''): string\n    {\n        $offset = min(2000, $this->fileSize);\n        $this->sniffContent = fread($this->stream, $offset);\n\n        return (new WebSafeMimeSniffer())->sniff($this->sniffContent, $extension);\n    }\n\n    /**\n     * Output the current stream to stdout before closing out the stream.\n     */\n    public function outputAndClose(): void\n    {\n        // End & flush the output buffer, if we're in one, otherwise we still use memory.\n        // Output buffer may or may not exist depending on PHP `output_buffering` setting.\n        // Ignore in testing since output buffers are used to gather a response.\n        if (!empty(ob_get_status()) && !app()->runningUnitTests()) {\n            ob_end_clean();\n        }\n\n        $outStream = fopen('php://output', 'w');\n        $sniffLength = strlen($this->sniffContent);\n        $bytesToWrite = $this->responseLength;\n\n        if ($sniffLength > 0 && $this->responseOffset < $sniffLength) {\n            $sniffEnd = min($sniffLength, $bytesToWrite + $this->responseOffset);\n            $sniffOutLength = $sniffEnd - $this->responseOffset;\n            $sniffOutput = substr($this->sniffContent, $this->responseOffset, $sniffOutLength);\n            fwrite($outStream, $sniffOutput);\n            $bytesToWrite -= $sniffOutLength;\n        } else if ($this->responseOffset !== 0) {\n            fseek($this->stream, $this->responseOffset);\n        }\n\n        stream_copy_to_stream($this->stream, $outStream, $bytesToWrite);\n\n        fclose($this->stream);\n        fclose($outStream);\n    }\n\n    public function getResponseHeaders(): array\n    {\n        return $this->responseHeaders;\n    }\n\n    public function getResponseStatus(): int\n    {\n        return $this->responseStatus;\n    }\n\n    protected function parseRequest(Request $request): void\n    {\n        $this->responseHeaders['Accept-Ranges'] = $request->isMethodSafe() ? 'bytes' : 'none';\n\n        $range = $this->getRangeFromRequest($request);\n        if ($range) {\n            [$start, $end] = $range;\n            if ($start < 0 || $start > $end) {\n                $this->responseStatus = 416;\n                $this->responseHeaders['Content-Range'] = sprintf('bytes */%s', $this->fileSize);\n            } else {\n                $this->responseLength = $end < $this->fileSize ? $end - $start + 1 : -1;\n                $this->responseOffset = $start;\n                $this->responseStatus = 206;\n                $this->responseHeaders['Content-Range'] = sprintf('bytes %s-%s/%s', $start, $end, $this->fileSize);\n                $this->responseHeaders['Content-Length'] = $end - $start + 1;\n            }\n        }\n\n        if ($request->isMethod('HEAD')) {\n            $this->responseLength = 0;\n        }\n    }\n\n    protected function getRangeFromRequest(Request $request): ?array\n    {\n        $range = $request->headers->get('Range');\n        if (!$range || !$request->isMethod('GET') || !str_starts_with($range, 'bytes=')) {\n            return null;\n        }\n\n        if ($request->headers->has('If-Range')) {\n            return null;\n        }\n\n        [$start, $end] = explode('-', substr($range, 6), 2) + [0];\n\n        $end = ('' === $end) ? $this->fileSize - 1 : (int) $end;\n\n        if ('' === $start) {\n            $start = $this->fileSize - $end;\n            $end = $this->fileSize - 1;\n        } else {\n            $start = (int) $start;\n        }\n\n        $end = min($end, $this->fileSize - 1);\n        return [$start, $end];\n    }\n}\n"
  },
  {
    "path": "app/Http/Request.php",
    "content": "<?php\n\nnamespace BookStack\\Http;\n\nuse Illuminate\\Http\\Request as LaravelRequest;\n\nclass Request extends LaravelRequest\n{\n    /**\n     * Override the default request methods to get the scheme and host\n     * to directly use the custom APP_URL, if set.\n     */\n    public function getSchemeAndHttpHost(): string\n    {\n        $appUrl = config('app.url', null);\n\n        if ($appUrl) {\n            return implode('/', array_slice(explode('/', $appUrl), 0, 3));\n        }\n\n        return parent::getSchemeAndHttpHost();\n    }\n\n    /**\n     * Override the default request methods to get the base URL\n     * to directly use the custom APP_URL, if set.\n     * The base URL never ends with a / but should start with one if not empty.\n     */\n    public function getBaseUrl(): string\n    {\n        $appUrl = config('app.url', null);\n\n        if ($appUrl) {\n            $parsedBaseUrl = rtrim(implode('/', array_slice(explode('/', $appUrl), 3)), '/');\n\n            return empty($parsedBaseUrl) ? '' : ('/' . $parsedBaseUrl);\n        }\n\n        return parent::getBaseUrl();\n    }\n}\n"
  },
  {
    "path": "app/Permissions/ContentPermissionApiController.php",
    "content": "<?php\n\nnamespace BookStack\\Permissions;\n\nuse BookStack\\Entities\\EntityProvider;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Tools\\PermissionsUpdater;\nuse BookStack\\Http\\ApiController;\nuse Illuminate\\Http\\Request;\n\nclass ContentPermissionApiController extends ApiController\n{\n    public function __construct(\n        protected PermissionsUpdater $permissionsUpdater,\n        protected EntityProvider $entities\n    ) {\n    }\n\n    protected array $rules = [\n        'update' => [\n            'owner_id'  => ['int'],\n\n            'role_permissions' => ['array'],\n            'role_permissions.*.role_id' => ['required', 'int', 'exists:roles,id'],\n            'role_permissions.*.view' => ['required', 'boolean'],\n            'role_permissions.*.create' => ['required', 'boolean'],\n            'role_permissions.*.update' => ['required', 'boolean'],\n            'role_permissions.*.delete' => ['required', 'boolean'],\n\n            'fallback_permissions' => ['nullable'],\n            'fallback_permissions.inheriting' => ['required_with:fallback_permissions', 'boolean'],\n            'fallback_permissions.view' => ['required_if:fallback_permissions.inheriting,false', 'boolean'],\n            'fallback_permissions.create' => ['required_if:fallback_permissions.inheriting,false', 'boolean'],\n            'fallback_permissions.update' => ['required_if:fallback_permissions.inheriting,false', 'boolean'],\n            'fallback_permissions.delete' => ['required_if:fallback_permissions.inheriting,false', 'boolean'],\n        ]\n    ];\n\n    /**\n     * Read the configured content-level permissions for the item of the given type and ID.\n     *\n     * 'contentType' should be one of: page, book, chapter, bookshelf.\n     * 'contentId' should be the relevant ID of that item type you'd like to handle permissions for.\n     *\n     * The permissions shown are those that override the default for just the specified item, they do not show the\n     * full evaluated permission for a role, nor do they reflect permissions inherited from other items in the hierarchy.\n     * Fallback permission values may be `null` when inheriting is active.\n     */\n    public function read(string $contentType, string $contentId)\n    {\n        $entity = $this->entities->get($contentType)\n            ->newQuery()->scopes(['visible'])->findOrFail($contentId);\n\n        $this->checkOwnablePermission(Permission::RestrictionsManage, $entity);\n\n        return response()->json($this->formattedPermissionDataForEntity($entity));\n    }\n\n    /**\n     * Update the configured content-level permission overrides for the item of the given type and ID.\n     * 'contentType' should be one of: page, book, chapter, bookshelf.\n     *\n     * 'contentId' should be the relevant ID of that item type you'd like to handle permissions for.\n     * Providing an empty `role_permissions` array will remove any existing configured role permissions,\n     * so you may want to fetch existing permissions beforehand if just adding/removing a single item.\n     * You should completely omit the `owner_id`, `role_permissions` and/or the `fallback_permissions` properties\n     * from your request data if you don't wish to update details within those categories.\n     */\n    public function update(Request $request, string $contentType, string $contentId)\n    {\n        $entity = $this->entities->get($contentType)\n            ->newQuery()->scopes(['visible'])->findOrFail($contentId);\n\n        $this->checkOwnablePermission(Permission::RestrictionsManage, $entity);\n\n        $data = $this->validate($request, $this->rules()['update']);\n        $this->permissionsUpdater->updateFromApiRequestData($entity, $data);\n\n        return response()->json($this->formattedPermissionDataForEntity($entity));\n    }\n\n    protected function formattedPermissionDataForEntity(Entity $entity): array\n    {\n        $rolePermissions = $entity->permissions()\n            ->where('role_id', '!=', 0)\n            ->with(['role:id,display_name'])\n            ->get();\n\n        $fallback = $entity->permissions()->where('role_id', '=', 0)->first();\n        $fallbackData = [\n            'inheriting' => is_null($fallback),\n            'view' => $fallback->view ?? null,\n            'create' => $fallback->create ?? null,\n            'update' => $fallback->update ?? null,\n            'delete' => $fallback->delete ?? null,\n        ];\n\n        return [\n            'owner' => $entity->ownedBy()->first(),\n            'role_permissions' => $rolePermissions,\n            'fallback_permissions' => $fallbackData,\n        ];\n    }\n}\n"
  },
  {
    "path": "app/Permissions/EntityPermissionEvaluator.php",
    "content": "<?php\n\nnamespace BookStack\\Permissions;\n\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Permissions\\Models\\EntityPermission;\nuse BookStack\\Users\\Models\\Role;\nuse Illuminate\\Database\\Eloquent\\Builder;\n\nclass EntityPermissionEvaluator\n{\n    public function __construct(\n        protected string $action\n    ) {\n    }\n\n    public function evaluateEntityForUser(Entity $entity, array $userRoleIds): ?bool\n    {\n        if ($this->isUserSystemAdmin($userRoleIds)) {\n            return true;\n        }\n\n        $typeIdChain = $this->gatherEntityChainTypeIds(SimpleEntityData::fromEntity($entity));\n        $relevantPermissions = $this->getPermissionsMapByTypeId($typeIdChain, [...$userRoleIds, 0]);\n        $permitsByType = $this->collapseAndCategorisePermissions($typeIdChain, $relevantPermissions);\n\n        $status = $this->evaluatePermitsByType($permitsByType);\n\n        return is_null($status) ? null : $status === PermissionStatus::IMPLICIT_ALLOW || $status === PermissionStatus::EXPLICIT_ALLOW;\n    }\n\n    /**\n     * @param array<string, array<int, string>> $permitsByType\n     */\n    protected function evaluatePermitsByType(array $permitsByType): ?int\n    {\n        // Return grant or reject from role-level if exists\n        if (count($permitsByType['role']) > 0) {\n            return max($permitsByType['role']) ? PermissionStatus::EXPLICIT_ALLOW : PermissionStatus::EXPLICIT_DENY;\n        }\n\n        // Return fallback permission if exists\n        if (count($permitsByType['fallback']) > 0) {\n            return $permitsByType['fallback'][0] ? PermissionStatus::IMPLICIT_ALLOW : PermissionStatus::IMPLICIT_DENY;\n        }\n\n        return null;\n    }\n\n    /**\n     * @param string[] $typeIdChain\n     * @param array<string, EntityPermission[]> $permissionMapByTypeId\n     * @return array<string, array<int, string>>\n     */\n    protected function collapseAndCategorisePermissions(array $typeIdChain, array $permissionMapByTypeId): array\n    {\n        $permitsByType = ['fallback' => [], 'role' => []];\n\n        foreach ($typeIdChain as $typeId) {\n            $permissions = $permissionMapByTypeId[$typeId] ?? [];\n            foreach ($permissions as $permission) {\n                $roleId = $permission->role_id;\n                $type = $roleId === 0 ? 'fallback' : 'role';\n                if (!isset($permitsByType[$type][$roleId])) {\n                    $permitsByType[$type][$roleId] = $permission->{$this->action};\n                }\n            }\n\n            if (isset($permitsByType['fallback'][0])) {\n                break;\n            }\n        }\n\n        return $permitsByType;\n    }\n\n    /**\n     * @param string[] $typeIdChain\n     * @return array<string, EntityPermission[]>\n     */\n    protected function getPermissionsMapByTypeId(array $typeIdChain, array $filterRoleIds): array\n    {\n        $idsByType = [];\n        foreach ($typeIdChain as $typeId) {\n            [$type, $id] = explode(':', $typeId);\n            if (!isset($idsByType[$type])) {\n                $idsByType[$type] = [];\n            }\n\n            $idsByType[$type][] = $id;\n        }\n\n        $relevantPermissions = [];\n\n        foreach ($idsByType as $type => $ids) {\n            $idsChunked = array_chunk($ids, 10000);\n            foreach ($idsChunked as $idChunk) {\n                $permissions = $this->getPermissionsForEntityIdsOfType($type, $idChunk, $filterRoleIds);\n                array_push($relevantPermissions, ...$permissions);\n            }\n        }\n\n        $map = [];\n        foreach ($relevantPermissions as $permission) {\n            $key = $permission->entity_type . ':' . $permission->entity_id;\n            if (!isset($map[$key])) {\n                $map[$key] = [];\n            }\n\n            $map[$key][] = $permission;\n        }\n\n        return $map;\n    }\n\n    /**\n     * @param string[] $ids\n     * @param int[] $filterRoleIds\n     * @return EntityPermission[]\n     */\n    protected function getPermissionsForEntityIdsOfType(string $type, array $ids, array $filterRoleIds): array\n    {\n        $query = EntityPermission::query()\n            ->where('entity_type', '=', $type)\n            ->whereIn('entity_id', $ids);\n\n        if (!empty($filterRoleIds)) {\n            $query->where(function (Builder $query) use ($filterRoleIds) {\n                $query->whereIn('role_id', [...$filterRoleIds, 0]);\n            });\n        }\n\n        return $query->get(['entity_id', 'entity_type', 'role_id', $this->action])->all();\n    }\n\n    /**\n     * @return string[]\n     */\n    protected function gatherEntityChainTypeIds(SimpleEntityData $entity): array\n    {\n        // The array order here is very important due to the fact we walk up the chain\n        // elsewhere in the class. Earlier items in the chain have higher priority.\n\n        $chain = [$entity->type . ':' . $entity->id];\n\n        if ($entity->type === 'page' && $entity->chapter_id) {\n            $chain[] = 'chapter:' . $entity->chapter_id;\n        }\n\n        if ($entity->type === 'page' || $entity->type === 'chapter') {\n            $chain[] = 'book:' . $entity->book_id;\n        }\n\n        return $chain;\n    }\n\n    protected function isUserSystemAdmin($userRoleIds): bool\n    {\n        $adminRoleId = Role::getSystemRole('admin')->id;\n        return in_array($adminRoleId, $userRoleIds);\n    }\n}\n"
  },
  {
    "path": "app/Permissions/JointPermissionBuilder.php",
    "content": "<?php\n\nnamespace BookStack\\Permissions;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\BookChild;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse BookStack\\Permissions\\Models\\JointPermission;\nuse BookStack\\Users\\Models\\Role;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Collection as EloquentCollection;\nuse Illuminate\\Support\\Facades\\DB;\n\n/**\n * Joint permissions provide a pre-query \"cached\" table of view permissions for all core entity\n * types for all roles in the system. This class generates out that table for different scenarios.\n */\nclass JointPermissionBuilder\n{\n    public function __construct(\n        protected EntityQueries $queries,\n    ) {\n    }\n\n\n    /**\n     * Re-generate all entity permission from scratch.\n     */\n    public function rebuildForAll(): void\n    {\n        JointPermission::query()->truncate();\n\n        // Get all roles (Should be the most limited dimension)\n        $roles = Role::query()->with('permissions')->get()->all();\n\n        // Chunk through all books\n        $this->bookFetchQuery()->chunk(5, function (EloquentCollection $books) use ($roles) {\n            $this->buildJointPermissionsForBooks($books, $roles);\n        });\n\n        // Chunk through all bookshelves\n        $this->queries->shelves->start()->withTrashed()->select(['id', 'owned_by'])\n            ->chunk(50, function (EloquentCollection $shelves) use ($roles) {\n                $this->createManyJointPermissions($shelves->all(), $roles);\n            });\n    }\n\n    /**\n     * Rebuild the entity jointPermissions for a particular entity.\n     */\n    public function rebuildForEntity(Entity $entity): void\n    {\n        $entities = [$entity];\n        if ($entity instanceof Book) {\n            $books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();\n            $this->buildJointPermissionsForBooks($books, Role::query()->with('permissions')->get()->all(), true);\n\n            return;\n        }\n\n        /** @var BookChild $entity */\n        if ($entity->book) {\n            $entities[] = $entity->book;\n        }\n\n        if ($entity instanceof Page && $entity->chapter_id) {\n            $entities[] = $entity->chapter;\n        }\n\n        if ($entity instanceof Chapter) {\n            foreach ($entity->pages as $page) {\n                $entities[] = $page;\n            }\n        }\n\n        $this->buildJointPermissionsForEntities($entities);\n    }\n\n    /**\n     * Build the entity jointPermissions for a particular role.\n     */\n    public function rebuildForRole(Role $role)\n    {\n        $roles = [$role];\n        $role->jointPermissions()->delete();\n        $role->load('permissions');\n\n        // Chunk through all books\n        $this->bookFetchQuery()->chunk(10, function ($books) use ($roles) {\n            $this->buildJointPermissionsForBooks($books, $roles);\n        });\n\n        // Chunk through all bookshelves\n        $this->queries->shelves->start()->select(['id', 'owned_by'])\n            ->chunk(100, function ($shelves) use ($roles) {\n                $this->createManyJointPermissions($shelves->all(), $roles);\n            });\n    }\n\n    /**\n     * Get a query for fetching a book with its children.\n     */\n    protected function bookFetchQuery(): Builder\n    {\n        return $this->queries->books->start()->withTrashed()\n            ->select(['id', 'owned_by'])->with([\n                'chapters' => function ($query) {\n                    $query->withTrashed()->select(['id', 'owned_by', 'book_id']);\n                },\n                'pages' => function ($query) {\n                    $query->withTrashed()->select(['id', 'owned_by', 'book_id', 'chapter_id']);\n                },\n            ]);\n    }\n\n    /**\n     * Build joint permissions for the given book and role combinations.\n     */\n    protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false): void\n    {\n        $entities = clone $books;\n\n        /** @var Book $book */\n        foreach ($books->all() as $book) {\n            foreach ($book->getRelation('chapters') as $chapter) {\n                $entities->push($chapter);\n            }\n            foreach ($book->getRelation('pages') as $page) {\n                $entities->push($page);\n            }\n        }\n\n        if ($deleteOld) {\n            $this->deleteManyJointPermissionsForEntities($entities->all());\n        }\n\n        $this->createManyJointPermissions($entities->all(), $roles);\n    }\n\n    /**\n     * Rebuild the entity jointPermissions for a collection of entities.\n     */\n    protected function buildJointPermissionsForEntities(array $entities): void\n    {\n        $roles = Role::query()->get()->values()->all();\n        $this->deleteManyJointPermissionsForEntities($entities);\n        $this->createManyJointPermissions($entities, $roles);\n    }\n\n    /**\n     * Delete all the entity jointPermissions for a list of entities.\n     *\n     * @param Entity[] $entities\n     */\n    protected function deleteManyJointPermissionsForEntities(array $entities): void\n    {\n        $simpleEntities = $this->entitiesToSimpleEntities($entities);\n        $idsByType = $this->entitiesToTypeIdMap($simpleEntities);\n\n        foreach ($idsByType as $type => $ids) {\n            foreach (array_chunk($ids, 1000) as $idChunk) {\n                DB::table('joint_permissions')\n                    ->where('entity_type', '=', $type)\n                    ->whereIn('entity_id', $idChunk)\n                    ->delete();\n            }\n        }\n    }\n\n    /**\n     * @param Entity[] $entities\n     *\n     * @return SimpleEntityData[]\n     */\n    protected function entitiesToSimpleEntities(array $entities): array\n    {\n        $simpleEntities = [];\n\n        foreach ($entities as $entity) {\n            $simple = SimpleEntityData::fromEntity($entity);\n            $simpleEntities[] = $simple;\n        }\n\n        return $simpleEntities;\n    }\n\n    /**\n     * Create & Save entity jointPermissions for many entities and roles.\n     *\n     * @param Entity[] $originalEntities\n     * @param Role[]   $roles\n     */\n    protected function createManyJointPermissions(array $originalEntities, array $roles): void\n    {\n        $entities = $this->entitiesToSimpleEntities($originalEntities);\n        $jointPermissions = [];\n\n        // Fetch related entity permissions\n        $permissions = new MassEntityPermissionEvaluator($entities, 'view');\n\n        // Create a mapping of role permissions\n        $rolePermissionMap = [];\n        foreach ($roles as $role) {\n            foreach ($role->permissions as $permission) {\n                $rolePermissionMap[$role->getRawAttribute('id') . ':' . $permission->getRawAttribute('name')] = true;\n            }\n        }\n\n        // Create Joint Permission Data\n        foreach ($entities as $entity) {\n            foreach ($roles as $role) {\n                $jp = $this->createJointPermissionData(\n                    $entity,\n                    $role->getRawAttribute('id'),\n                    $permissions,\n                    $rolePermissionMap,\n                    $role->system_name === 'admin'\n                );\n                $jointPermissions[] = $jp;\n            }\n        }\n\n        foreach (array_chunk($jointPermissions, 1000) as $jointPermissionChunk) {\n            DB::table('joint_permissions')->insert($jointPermissionChunk);\n        }\n    }\n\n    /**\n     * From the given entity list, provide back a mapping of entity types to\n     * the ids of that given type. The type used is the DB morph class.\n     *\n     * @param SimpleEntityData[] $entities\n     *\n     * @return array<string, int[]>\n     */\n    protected function entitiesToTypeIdMap(array $entities): array\n    {\n        $idsByType = [];\n\n        foreach ($entities as $entity) {\n            if (!isset($idsByType[$entity->type])) {\n                $idsByType[$entity->type] = [];\n            }\n\n            $idsByType[$entity->type][] = $entity->id;\n        }\n\n        return $idsByType;\n    }\n\n    /**\n     * Create entity permission data for an entity and role\n     * for a particular action.\n     */\n    protected function createJointPermissionData(SimpleEntityData $entity, int $roleId, MassEntityPermissionEvaluator $permissionMap, array $rolePermissionMap, bool $isAdminRole): array\n    {\n        // Ensure system admin role retains permissions\n        if ($isAdminRole) {\n            return $this->createJointPermissionDataArray($entity, $roleId, PermissionStatus::EXPLICIT_ALLOW, true);\n        }\n\n        // Return evaluated entity permission status if it has an affect.\n        $entityPermissionStatus = $permissionMap->evaluateEntityForRole($entity, $roleId);\n        if ($entityPermissionStatus !== null) {\n            return $this->createJointPermissionDataArray($entity, $roleId, $entityPermissionStatus, false);\n        }\n\n        // Otherwise default to the role-level permissions\n        $permissionPrefix = $entity->type . '-view';\n        $roleHasPermission = isset($rolePermissionMap[$roleId . ':' . $permissionPrefix . '-all']);\n        $roleHasPermissionOwn = isset($rolePermissionMap[$roleId . ':' . $permissionPrefix . '-own']);\n        $status = $roleHasPermission ? PermissionStatus::IMPLICIT_ALLOW : PermissionStatus::IMPLICIT_DENY;\n        return $this->createJointPermissionDataArray($entity, $roleId, $status, $roleHasPermissionOwn);\n    }\n\n    /**\n     * Create an array of data with the information of an entity jointPermissions.\n     * Used to build data for bulk insertion.\n     */\n    protected function createJointPermissionDataArray(SimpleEntityData $entity, int $roleId, int $permissionStatus, bool $hasPermissionOwn): array\n    {\n        $ownPermissionActive = ($hasPermissionOwn && $permissionStatus !== PermissionStatus::EXPLICIT_DENY && $entity->owned_by);\n\n        return [\n            'entity_id'   => $entity->id,\n            'entity_type' => $entity->type,\n            'role_id'     => $roleId,\n            'status'      => $permissionStatus,\n            'owner_id'    => $ownPermissionActive ? $entity->owned_by : null,\n        ];\n    }\n}\n"
  },
  {
    "path": "app/Permissions/MassEntityPermissionEvaluator.php",
    "content": "<?php\n\nnamespace BookStack\\Permissions;\n\nuse BookStack\\Permissions\\Models\\EntityPermission;\n\nclass MassEntityPermissionEvaluator extends EntityPermissionEvaluator\n{\n    /**\n     * @var SimpleEntityData[]\n     */\n    protected array $entitiesInvolved;\n    protected array $permissionMapCache;\n\n    public function __construct(array $entitiesInvolved, string $action)\n    {\n        $this->entitiesInvolved = $entitiesInvolved;\n        parent::__construct($action);\n    }\n\n    public function evaluateEntityForRole(SimpleEntityData $entity, int $roleId): ?int\n    {\n        $typeIdChain = $this->gatherEntityChainTypeIds($entity);\n        $relevantPermissions = $this->getPermissionMapByTypeIdForChainAndRole($typeIdChain, $roleId);\n        $permitsByType = $this->collapseAndCategorisePermissions($typeIdChain, $relevantPermissions);\n\n        return $this->evaluatePermitsByType($permitsByType);\n    }\n\n    /**\n     * @param string[] $typeIdChain\n     * @return array<string, EntityPermission[]>\n     */\n    protected function getPermissionMapByTypeIdForChainAndRole(array $typeIdChain, int $roleId): array\n    {\n        $allPermissions = $this->getPermissionMapByTypeIdAndRoleForAllInvolved();\n        $relevantPermissions = [];\n\n        // Filter down permissions to just those for current typeId\n        // and current roleID or fallback permissions.\n        foreach ($typeIdChain as $typeId) {\n            $relevantPermissions[$typeId] = [\n                ...($allPermissions[$typeId][$roleId] ?? []),\n                ...($allPermissions[$typeId][0] ?? [])\n            ];\n        }\n\n        return $relevantPermissions;\n    }\n\n    /**\n     * @return array<string, array<int, EntityPermission[]>>\n     */\n    protected function getPermissionMapByTypeIdAndRoleForAllInvolved(): array\n    {\n        if (isset($this->permissionMapCache)) {\n            return $this->permissionMapCache;\n        }\n\n        $entityTypeIdChain = [];\n        foreach ($this->entitiesInvolved as $entity) {\n            $entityTypeIdChain[] = $entity->type . ':' . $entity->id;\n        }\n\n        $permissionMap = $this->getPermissionsMapByTypeId($entityTypeIdChain, []);\n\n       // Manipulate permission map to also be keyed by roleId.\n        foreach ($permissionMap as $typeId => $permissions) {\n            $permissionMap[$typeId] = [];\n            foreach ($permissions as $permission) {\n                $roleId = $permission->getRawAttribute('role_id');\n                if (!isset($permissionMap[$typeId][$roleId])) {\n                    $permissionMap[$typeId][$roleId] = [];\n                }\n                $permissionMap[$typeId][$roleId][] = $permission;\n            }\n        }\n\n        $this->permissionMapCache = $permissionMap;\n\n        return $this->permissionMapCache;\n    }\n}\n"
  },
  {
    "path": "app/Permissions/Models/EntityPermission.php",
    "content": "<?php\n\nnamespace BookStack\\Permissions\\Models;\n\nuse BookStack\\App\\Model;\nuse BookStack\\Users\\Models\\Role;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\n\n/**\n * @property int $id\n * @property int $role_id\n * @property int $entity_id\n * @property string $entity_type\n * @property boolean $view\n * @property boolean $create\n * @property boolean $update\n * @property boolean $delete\n */\nclass EntityPermission extends Model\n{\n    protected $fillable = ['role_id', 'view', 'create', 'update', 'delete'];\n    public $timestamps = false;\n    protected $hidden = ['entity_id', 'entity_type', 'id'];\n    protected $casts = [\n        'view' => 'boolean',\n        'create' => 'boolean',\n        'read' => 'boolean',\n        'update' => 'boolean',\n        'delete' => 'boolean',\n    ];\n\n    /**\n     * Get the role assigned to this entity permission.\n     */\n    public function role(): BelongsTo\n    {\n        return $this->belongsTo(Role::class);\n    }\n}\n"
  },
  {
    "path": "app/Permissions/Models/JointPermission.php",
    "content": "<?php\n\nnamespace BookStack\\Permissions\\Models;\n\nuse BookStack\\App\\Model;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Users\\Models\\Role;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\MorphOne;\n\nclass JointPermission extends Model\n{\n    protected $primaryKey = null;\n    public $timestamps = false;\n\n    /**\n     * Get the role that this points to.\n     */\n    public function role(): BelongsTo\n    {\n        return $this->belongsTo(Role::class);\n    }\n\n    /**\n     * Get the entity this points to.\n     */\n    public function entity(): MorphOne\n    {\n        return $this->morphOne(Entity::class, 'entity');\n    }\n}\n"
  },
  {
    "path": "app/Permissions/Models/RolePermission.php",
    "content": "<?php\n\nnamespace BookStack\\Permissions\\Models;\n\nuse BookStack\\App\\Model;\nuse BookStack\\Users\\Models\\Role;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\n\n/**\n * @property int $id\n * @property string $name\n */\nclass RolePermission extends Model\n{\n    /**\n     * The roles that belong to the permission.\n     */\n    public function roles(): BelongsToMany\n    {\n        return $this->belongsToMany(Role::class, 'permission_role', 'permission_id', 'role_id');\n    }\n\n    /**\n     * Get the permission object by name.\n     */\n    public static function getByName(string $name): ?RolePermission\n    {\n        return static::where('name', '=', $name)->first();\n    }\n}\n"
  },
  {
    "path": "app/Permissions/Permission.php",
    "content": "<?php\n\nnamespace BookStack\\Permissions;\n\n/**\n * Enum to represent the permissions which may be used in checks.\n * These generally align with RolePermission names, although some are abstract or truncated as some checks\n * are performed across a range of different items which may be subject to inheritance and other complications.\n *\n * We use and still allow the string values in usage to allow for compatibility with scenarios where\n * users have customised their instance with additional permissions via the theme system.\n * This enum primarily exists for alignment within the codebase.\n *\n * Permissions with all/own suffixes may also be represented as a higher-level alias without the own/all\n * suffix, which are used and assessed in the permission system logic.\n */\nenum Permission: string\n{\n    // Generic Actions\n    // Used for more abstract entity permission checks\n    case View = 'view';\n    case Create = 'create';\n    case Update = 'update';\n    case Delete = 'delete';\n\n    // System Permissions\n    case AccessApi = 'access-api';\n    case ContentExport = 'content-export';\n    case ContentImport = 'content-import';\n    case EditorChange = 'editor-change';\n    case ReceiveNotifications = 'receive-notifications';\n    case RestrictionsManage = 'restrictions-manage';\n    case RestrictionsManageAll = 'restrictions-manage-all';\n    case RestrictionsManageOwn = 'restrictions-manage-own';\n    case SettingsManage = 'settings-manage';\n    case TemplatesManage = 'templates-manage';\n    case UserRolesManage = 'user-roles-manage';\n    case UsersManage = 'users-manage';\n\n    // Non-entity content permissions\n    case AttachmentCreate = 'attachment-create';\n    case AttachmentCreateAll = 'attachment-create-all';\n    case AttachmentCreateOwn = 'attachment-create-own';\n    case AttachmentDelete = 'attachment-delete';\n    case AttachmentDeleteAll = 'attachment-delete-all';\n    case AttachmentDeleteOwn = 'attachment-delete-own';\n    case AttachmentUpdate = 'attachment-update';\n    case AttachmentUpdateAll = 'attachment-update-all';\n    case AttachmentUpdateOwn = 'attachment-update-own';\n\n    case CommentCreateAll = 'comment-create-all';\n    case CommentDelete = 'comment-delete';\n    case CommentDeleteAll = 'comment-delete-all';\n    case CommentDeleteOwn = 'comment-delete-own';\n    case CommentUpdate = 'comment-update';\n    case CommentUpdateAll = 'comment-update-all';\n    case CommentUpdateOwn = 'comment-update-own';\n\n    case ImageCreateAll = 'image-create-all';\n    case ImageCreateOwn = 'image-create-own';\n    case ImageDelete = 'image-delete';\n    case ImageDeleteAll = 'image-delete-all';\n    case ImageDeleteOwn = 'image-delete-own';\n    case ImageUpdate = 'image-update';\n    case ImageUpdateAll = 'image-update-all';\n    case ImageUpdateOwn = 'image-update-own';\n\n    // Entity content permissions\n    case BookCreate = 'book-create';\n    case BookCreateAll = 'book-create-all';\n    case BookCreateOwn = 'book-create-own';\n    case BookDelete = 'book-delete';\n    case BookDeleteAll = 'book-delete-all';\n    case BookDeleteOwn = 'book-delete-own';\n    case BookUpdate = 'book-update';\n    case BookUpdateAll = 'book-update-all';\n    case BookUpdateOwn = 'book-update-own';\n    case BookView = 'book-view';\n    case BookViewAll = 'book-view-all';\n    case BookViewOwn = 'book-view-own';\n\n    case BookshelfCreate = 'bookshelf-create';\n    case BookshelfCreateAll = 'bookshelf-create-all';\n    case BookshelfCreateOwn = 'bookshelf-create-own';\n    case BookshelfDelete = 'bookshelf-delete';\n    case BookshelfDeleteAll = 'bookshelf-delete-all';\n    case BookshelfDeleteOwn = 'bookshelf-delete-own';\n    case BookshelfUpdate = 'bookshelf-update';\n    case BookshelfUpdateAll = 'bookshelf-update-all';\n    case BookshelfUpdateOwn = 'bookshelf-update-own';\n    case BookshelfView = 'bookshelf-view';\n    case BookshelfViewAll = 'bookshelf-view-all';\n    case BookshelfViewOwn = 'bookshelf-view-own';\n\n    case ChapterCreate = 'chapter-create';\n    case ChapterCreateAll = 'chapter-create-all';\n    case ChapterCreateOwn = 'chapter-create-own';\n    case ChapterDelete = 'chapter-delete';\n    case ChapterDeleteAll = 'chapter-delete-all';\n    case ChapterDeleteOwn = 'chapter-delete-own';\n    case ChapterUpdate = 'chapter-update';\n    case ChapterUpdateAll = 'chapter-update-all';\n    case ChapterUpdateOwn = 'chapter-update-own';\n    case ChapterView = 'chapter-view';\n    case ChapterViewAll = 'chapter-view-all';\n    case ChapterViewOwn = 'chapter-view-own';\n\n    case PageCreate = 'page-create';\n    case PageCreateAll = 'page-create-all';\n    case PageCreateOwn = 'page-create-own';\n    case PageDelete = 'page-delete';\n    case PageDeleteAll = 'page-delete-all';\n    case PageDeleteOwn = 'page-delete-own';\n    case PageUpdate = 'page-update';\n    case PageUpdateAll = 'page-update-all';\n    case PageUpdateOwn = 'page-update-own';\n    case PageView = 'page-view';\n    case PageViewAll = 'page-view-all';\n    case PageViewOwn = 'page-view-own';\n\n    /**\n     * Get the generic permissions which may be queried for entities.\n     */\n    public static function genericForEntity(): array\n    {\n        return [\n            self::View,\n            self::Create,\n            self::Update,\n            self::Delete,\n        ];\n    }\n\n    /**\n     * Return the application permission-check middleware-string for this permission.\n     * Uses registered CheckUserHasPermission middleware.\n     */\n    public function middleware(): string\n    {\n        return 'can:' . $this->value;\n    }\n}\n"
  },
  {
    "path": "app/Permissions/PermissionApplicator.php",
    "content": "<?php\n\nnamespace BookStack\\Permissions;\n\nuse BookStack\\App\\Model;\nuse BookStack\\Entities\\EntityProvider;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Permissions\\Models\\EntityPermission;\nuse BookStack\\Users\\Models\\OwnableInterface;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Query\\Builder as QueryBuilder;\nuse Illuminate\\Database\\Query\\JoinClause;\nuse InvalidArgumentException;\n\nclass PermissionApplicator\n{\n    public function __construct(\n        protected ?User $user = null\n    ) {\n    }\n\n    /**\n     * Checks if an entity has a restriction set upon it.\n     */\n    public function checkOwnableUserAccess(Model&OwnableInterface $ownable, string|Permission $permission): bool\n    {\n        $permissionName = is_string($permission) ? $permission : $permission->value;\n        $explodedPermission = explode('-', $permissionName);\n        $action = $explodedPermission[1] ?? $explodedPermission[0];\n        $fullPermission = count($explodedPermission) > 1 ? $permissionName : $ownable->getMorphClass() . '-' . $permissionName;\n\n        $user = $this->currentUser();\n        $userRoleIds = $this->getCurrentUserRoleIds();\n\n        $allRolePermission = $user->can($fullPermission . '-all');\n        $ownRolePermission = $user->can($fullPermission . '-own');\n        $nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment'];\n        $ownerField = $ownable->getOwnerFieldName();\n        $ownableFieldVal = $ownable->getAttribute($ownerField);\n\n        $isOwner = $user->id === $ownableFieldVal;\n        $hasRolePermission = $allRolePermission || ($isOwner && $ownRolePermission);\n\n        // Handle non-entity-specific jointPermissions\n        if (in_array($explodedPermission[0], $nonJointPermissions)) {\n            return $hasRolePermission;\n        }\n\n        if (!($ownable instanceof Entity)) {\n            return false;\n        }\n\n        $hasApplicableEntityPermissions = $this->hasEntityPermission($ownable, $userRoleIds, $action);\n\n        return is_null($hasApplicableEntityPermissions) ? $hasRolePermission : $hasApplicableEntityPermissions;\n    }\n\n    /**\n     * Check if there are permissions that are applicable for the given entity item, action and roles.\n     * Returns null when no entity permissions are in force.\n     */\n    protected function hasEntityPermission(Entity $entity, array $userRoleIds, string $action): ?bool\n    {\n        $this->ensureValidEntityAction($action);\n\n        return (new EntityPermissionEvaluator($action))->evaluateEntityForUser($entity, $userRoleIds);\n    }\n\n    /**\n     * Checks if a user has the given permission for any items in the system.\n     * Can be passed an entity instance to filter on a specific type.\n     */\n    public function checkUserHasEntityPermissionOnAny(string|Permission $action, string $entityClass = ''): bool\n    {\n        $permissionName = is_string($action) ? $action : $action->value;\n        $this->ensureValidEntityAction($permissionName);\n\n        $permissionQuery = EntityPermission::query()\n            ->where($permissionName, '=', true)\n            ->whereIn('role_id', $this->getCurrentUserRoleIds());\n\n        if (!empty($entityClass)) {\n            /** @var Entity $entityInstance */\n            $entityInstance = app()->make($entityClass);\n            $permissionQuery = $permissionQuery->where('entity_type', '=', $entityInstance->getMorphClass());\n        }\n\n        $hasPermission = $permissionQuery->count() > 0;\n\n        return $hasPermission;\n    }\n\n    /**\n     * Limit the given entity query so that the query will only\n     * return items that the user has view permission for.\n     */\n    public function restrictEntityQuery(Builder $query): Builder\n    {\n        return $query->where(function (Builder $parentQuery) {\n            $parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) {\n                $permissionQuery->select(['entity_id', 'entity_type'])\n                    ->selectRaw('max(owner_id) as owner_id')\n                    ->selectRaw('max(status) as status')\n                    ->whereIn('role_id', $this->getCurrentUserRoleIds())\n                    ->groupBy(['entity_type', 'entity_id'])\n                    ->havingRaw('(status IN (1, 3) or (owner_id = ? and status != 2))', [$this->currentUser()->id]);\n            });\n        });\n    }\n\n    /**\n     * Extend the given page query to ensure draft items are not visible\n     * unless created by the given user.\n     */\n    public function restrictDraftsOnPageQuery(Builder $query): Builder\n    {\n        return $query->where(function (Builder $query) {\n            $query->where('draft', '=', false)\n                ->orWhere(function (Builder $query) {\n                    $query->where('draft', '=', true)\n                        ->where('owned_by', '=', $this->currentUser()->id);\n                });\n        });\n    }\n\n    /**\n     * Filter items that have entities set as a polymorphic relation.\n     * For simplicity, this will not return results attached to draft pages.\n     * Draft pages should never really have related items though.\n     */\n    public function restrictEntityRelationQuery(Builder $query, string $tableName, string $entityIdColumn, string $entityTypeColumn): Builder\n    {\n        $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];\n        $pageMorphClass = (new Page())->getMorphClass();\n\n        return $this->restrictEntityQuery($query)\n            ->where(function ($query) use ($tableDetails, $pageMorphClass) {\n                /** @var Builder $query */\n                $query->where($tableDetails['entityTypeColumn'], '!=', $pageMorphClass)\n                ->orWhereExists(function (QueryBuilder $query) use ($tableDetails, $pageMorphClass) {\n                    $query->select('page_id')->from('entity_page_data')\n                        ->whereColumn('entity_page_data.page_id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])\n                        ->where($tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'], '=', $pageMorphClass)\n                        ->where('entity_page_data.draft', '=', false);\n                });\n            });\n    }\n\n    /**\n     * Filter out items that have related entity relations where\n     * the entity is marked as deleted.\n     */\n    public function filterDeletedFromEntityRelationQuery(Builder $query, string $tableName, string $entityIdColumn, string $entityTypeColumn): Builder\n    {\n        $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];\n        $entityProvider = new EntityProvider();\n\n        $joinQuery = function ($query) use ($entityProvider) {\n            $first = true;\n            foreach ($entityProvider->all() as $entity) {\n                /** @var Builder $query */\n                $entityQuery = function ($query) use ($entity) {\n                    $query->select(['id', 'deleted_at'])\n                        ->selectRaw(\"'{$entity->getMorphClass()}' as type\")\n                        ->from($entity->getTable())\n                        ->whereNotNull('deleted_at');\n                };\n\n                if ($first) {\n                    $entityQuery($query);\n                    $first = false;\n                } else {\n                    $query->union($entityQuery);\n                }\n            }\n        };\n\n        return $query->leftJoinSub($joinQuery, 'deletions', function (JoinClause $join) use ($tableDetails) {\n            $join->on($tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'], '=', 'deletions.id')\n                ->on($tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'], '=', 'deletions.type');\n        })->whereNull('deletions.deleted_at');\n    }\n\n    /**\n     * Add conditions to a query for a model that's a relation of a page, so only the model results\n     * on visible pages are returned by the query.\n     * Is effectively the same as \"restrictEntityRelationQuery\" but takes into account page drafts\n     * while not expecting a polymorphic relation, Just a simpler one-page-to-many-relations set-up.\n     */\n    public function restrictPageRelationQuery(Builder $query, string $tableName, string $pageIdColumn): Builder\n    {\n        $fullPageIdColumn = $tableName . '.' . $pageIdColumn;\n        return $this->restrictEntityQuery($query)\n            ->whereExists(function (QueryBuilder $query) use ($fullPageIdColumn) {\n                $query->select('id')->from('entities')\n                    ->leftJoin('entity_page_data', 'entities.id', '=', 'entity_page_data.page_id')\n                    ->whereColumn('entities.id', '=', $fullPageIdColumn)\n                    ->where('entities.type', '=', 'page')\n                    ->where(function (QueryBuilder $query) {\n                        $query->where('entity_page_data.draft', '=', false)\n                            ->orWhere(function (QueryBuilder $query) {\n                                $query->where('entity_page_data.draft', '=', true)\n                                    ->where('entities.created_by', '=', $this->currentUser()->id);\n                            });\n                    });\n            });\n    }\n\n    /**\n     * Get the current user.\n     */\n    protected function currentUser(): User\n    {\n        return $this->user ?? user();\n    }\n\n    /**\n     * Get the roles for the current logged-in user.\n     *\n     * @return int[]\n     */\n    protected function getCurrentUserRoleIds(): array\n    {\n        return $this->currentUser()->roles->pluck('id')->values()->all();\n    }\n\n    /**\n     * Ensure the given action is a valid and expected entity action.\n     * Throws an exception if invalid otherwise does nothing.\n     * @throws InvalidArgumentException\n     */\n    protected function ensureValidEntityAction(string $action): void\n    {\n        $allowed = Permission::genericForEntity();\n        foreach ($allowed as $permission) {\n            if ($permission->value === $action) {\n                return;\n            }\n        }\n\n        throw new InvalidArgumentException('Action should be a simple entity permission action, not a role permission');\n    }\n}\n"
  },
  {
    "path": "app/Permissions/PermissionFormData.php",
    "content": "<?php\n\nnamespace BookStack\\Permissions;\n\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Permissions\\Models\\EntityPermission;\nuse BookStack\\Users\\Models\\Role;\n\nclass PermissionFormData\n{\n    protected Entity $entity;\n\n    public function __construct(Entity $entity)\n    {\n        $this->entity = $entity;\n    }\n\n    /**\n     * Get the permissions with assigned roles.\n     */\n    public function permissionsWithRoles(): array\n    {\n        return $this->entity->permissions()\n            ->with('role')\n            ->where('role_id', '!=', 0)\n            ->get()\n            ->sortBy('role.display_name')\n            ->all();\n    }\n\n    /**\n     * Get the roles that don't yet have specific permissions for the\n     * entity we're managing permissions for.\n     */\n    public function rolesNotAssigned(): array\n    {\n        $assigned = $this->entity->permissions()->pluck('role_id');\n        return Role::query()\n            ->where('system_name', '!=', 'admin')\n            ->whereNotIn('id', $assigned)\n            ->orderBy('display_name', 'asc')\n            ->get()\n            ->all();\n    }\n\n    /**\n     * Get the entity permission for the \"Everyone Else\" option.\n     */\n    public function everyoneElseEntityPermission(): EntityPermission\n    {\n        /** @var ?EntityPermission $permission */\n        $permission = $this->entity->permissions()\n            ->where('role_id', '=', 0)\n            ->first();\n        return $permission ?? (new EntityPermission());\n    }\n\n    /**\n     * Get the \"Everyone Else\" role entry.\n     */\n    public function everyoneElseRole(): Role\n    {\n        return (new Role())->forceFill([\n            'id' => 0,\n            'display_name' => trans('entities.permissions_role_everyone_else'),\n            'description' => trans('entities.permissions_role_everyone_else_desc'),\n        ]);\n    }\n}\n"
  },
  {
    "path": "app/Permissions/PermissionStatus.php",
    "content": "<?php\n\nnamespace BookStack\\Permissions;\n\nclass PermissionStatus\n{\n    const IMPLICIT_DENY = 0;\n    const IMPLICIT_ALLOW = 1;\n    const EXPLICIT_DENY = 2;\n    const EXPLICIT_ALLOW = 3;\n}\n"
  },
  {
    "path": "app/Permissions/PermissionsController.php",
    "content": "<?php\n\nnamespace BookStack\\Permissions;\n\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse BookStack\\Entities\\Tools\\PermissionsUpdater;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Models\\EntityPermission;\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Util\\DatabaseTransaction;\nuse Illuminate\\Http\\Request;\n\nclass PermissionsController extends Controller\n{\n    public function __construct(\n        protected PermissionsUpdater $permissionsUpdater,\n        protected EntityQueries $queries,\n    ) {\n    }\n\n    /**\n     * Show the permissions view for a page.\n     */\n    public function showForPage(string $bookSlug, string $pageSlug)\n    {\n        $page = $this->queries->pages->findVisibleBySlugsOrFail($bookSlug, $pageSlug);\n        $this->checkOwnablePermission(Permission::RestrictionsManage, $page);\n\n        $this->setPageTitle(trans('entities.pages_permissions'));\n        return view('pages.permissions', [\n            'page' => $page,\n            'data' => new PermissionFormData($page),\n        ]);\n    }\n\n    /**\n     * Set the permissions for a page.\n     */\n    public function updateForPage(Request $request, string $bookSlug, string $pageSlug)\n    {\n        $page = $this->queries->pages->findVisibleBySlugsOrFail($bookSlug, $pageSlug);\n        $this->checkOwnablePermission(Permission::RestrictionsManage, $page);\n\n        (new DatabaseTransaction(function () use ($page, $request) {\n            $this->permissionsUpdater->updateFromPermissionsForm($page, $request);\n        }))->run();\n\n        $this->showSuccessNotification(trans('entities.pages_permissions_success'));\n\n        return redirect($page->getUrl());\n    }\n\n    /**\n     * Show the permissions view for a chapter.\n     */\n    public function showForChapter(string $bookSlug, string $chapterSlug)\n    {\n        $chapter = $this->queries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);\n        $this->checkOwnablePermission(Permission::RestrictionsManage, $chapter);\n\n        $this->setPageTitle(trans('entities.chapters_permissions'));\n        return view('chapters.permissions', [\n            'chapter' => $chapter,\n            'data' => new PermissionFormData($chapter),\n        ]);\n    }\n\n    /**\n     * Set the permissions for a chapter.\n     */\n    public function updateForChapter(Request $request, string $bookSlug, string $chapterSlug)\n    {\n        $chapter = $this->queries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);\n        $this->checkOwnablePermission(Permission::RestrictionsManage, $chapter);\n\n        (new DatabaseTransaction(function () use ($chapter, $request) {\n            $this->permissionsUpdater->updateFromPermissionsForm($chapter, $request);\n        }))->run();\n\n        $this->showSuccessNotification(trans('entities.chapters_permissions_success'));\n\n        return redirect($chapter->getUrl());\n    }\n\n    /**\n     * Show the permissions view for a book.\n     */\n    public function showForBook(string $slug)\n    {\n        $book = $this->queries->books->findVisibleBySlugOrFail($slug);\n        $this->checkOwnablePermission(Permission::RestrictionsManage, $book);\n\n        $this->setPageTitle(trans('entities.books_permissions'));\n        return view('books.permissions', [\n            'book' => $book,\n            'data' => new PermissionFormData($book),\n        ]);\n    }\n\n    /**\n     * Set the permissions for a book.\n     */\n    public function updateForBook(Request $request, string $slug)\n    {\n        $book = $this->queries->books->findVisibleBySlugOrFail($slug);\n        $this->checkOwnablePermission(Permission::RestrictionsManage, $book);\n\n        (new DatabaseTransaction(function () use ($book, $request) {\n            $this->permissionsUpdater->updateFromPermissionsForm($book, $request);\n        }))->run();\n\n        $this->showSuccessNotification(trans('entities.books_permissions_updated'));\n\n        return redirect($book->getUrl());\n    }\n\n    /**\n     * Show the permissions view for a shelf.\n     */\n    public function showForShelf(string $slug)\n    {\n        $shelf = $this->queries->shelves->findVisibleBySlugOrFail($slug);\n        $this->checkOwnablePermission(Permission::RestrictionsManage, $shelf);\n\n        $this->setPageTitle(trans('entities.shelves_permissions'));\n        return view('shelves.permissions', [\n            'shelf' => $shelf,\n            'data' => new PermissionFormData($shelf),\n        ]);\n    }\n\n    /**\n     * Set the permissions for a shelf.\n     */\n    public function updateForShelf(Request $request, string $slug)\n    {\n        $shelf = $this->queries->shelves->findVisibleBySlugOrFail($slug);\n        $this->checkOwnablePermission(Permission::RestrictionsManage, $shelf);\n\n        (new DatabaseTransaction(function () use ($shelf, $request) {\n            $this->permissionsUpdater->updateFromPermissionsForm($shelf, $request);\n        }))->run();\n\n        $this->showSuccessNotification(trans('entities.shelves_permissions_updated'));\n\n        return redirect($shelf->getUrl());\n    }\n\n    /**\n     * Copy the permissions of a bookshelf to the child books.\n     */\n    public function copyShelfPermissionsToBooks(string $slug)\n    {\n        $shelf = $this->queries->shelves->findVisibleBySlugOrFail($slug);\n        $this->checkOwnablePermission(Permission::RestrictionsManage, $shelf);\n\n        $updateCount = (new DatabaseTransaction(function () use ($shelf) {\n            return $this->permissionsUpdater->updateBookPermissionsFromShelf($shelf);\n        }))->run();\n\n        $this->showSuccessNotification(trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));\n\n        return redirect($shelf->getUrl());\n    }\n\n    /**\n     * Get an empty entity permissions form row for the given role.\n     */\n    public function formRowForRole(string $entityType, string $roleId)\n    {\n        $this->checkPermissionOr(Permission::RestrictionsManageAll, fn() => userCan(Permission::RestrictionsManageOwn));\n\n        $role = Role::query()->findOrFail($roleId);\n\n        return view('form.entity-permissions-row', [\n            'role' => $role,\n            'permission' => new EntityPermission(),\n            'entityType' => $entityType,\n            'inheriting' => false,\n        ]);\n    }\n}\n"
  },
  {
    "path": "app/Permissions/PermissionsRepo.php",
    "content": "<?php\n\nnamespace BookStack\\Permissions;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Exceptions\\PermissionsException;\nuse BookStack\\Facades\\Activity;\nuse BookStack\\Permissions\\Models\\RolePermission;\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Util\\DatabaseTransaction;\nuse Exception;\nuse Illuminate\\Database\\Eloquent\\Collection;\n\nclass PermissionsRepo\n{\n    protected array $systemRoles = ['admin', 'public'];\n\n    public function __construct(\n        protected JointPermissionBuilder $permissionBuilder\n    ) {\n    }\n\n    /**\n     * Get all the user roles from the system.\n     */\n    public function getAllRoles(): Collection\n    {\n        return Role::query()->get();\n    }\n\n    /**\n     * Get all the roles except for the provided one.\n     */\n    public function getAllRolesExcept(Role $role): Collection\n    {\n        return Role::query()->where('id', '!=', $role->id)->get();\n    }\n\n    /**\n     * Get a role via its ID.\n     */\n    public function getRoleById(int $id): Role\n    {\n        return Role::query()->findOrFail($id);\n    }\n\n    /**\n     * Save a new role into the system.\n     */\n    public function saveNewRole(array $roleData): Role\n    {\n        return (new DatabaseTransaction(function () use ($roleData) {\n            $role = new Role($roleData);\n            $role->mfa_enforced = boolval($roleData['mfa_enforced'] ?? false);\n            $role->save();\n\n            $permissions = $roleData['permissions'] ?? [];\n            $this->assignRolePermissions($role, $permissions);\n            $this->permissionBuilder->rebuildForRole($role);\n\n            Activity::add(ActivityType::ROLE_CREATE, $role);\n\n            return $role;\n        }))->run();\n    }\n\n    /**\n     * Updates an existing role.\n     * Ensures the Admin system role always has core permissions.\n     */\n    public function updateRole($roleId, array $roleData): Role\n    {\n        $role = $this->getRoleById($roleId);\n\n        return (new DatabaseTransaction(function () use ($role, $roleData) {\n            if (isset($roleData['permissions'])) {\n                $this->assignRolePermissions($role, $roleData['permissions']);\n            }\n\n            $role->fill($roleData);\n            $role->save();\n            $this->permissionBuilder->rebuildForRole($role);\n\n            Activity::add(ActivityType::ROLE_UPDATE, $role);\n\n            return $role;\n        }))->run();\n    }\n\n    /**\n     * Assign a list of permission names to the given role.\n     */\n    protected function assignRolePermissions(Role $role, array $permissionNameArray = []): void\n    {\n        $permissions = [];\n        $permissionNameArray = array_values($permissionNameArray);\n\n        // Ensure the admin system role retains vital system permissions\n        if ($role->system_name === 'admin') {\n            $permissionNameArray = array_unique(array_merge($permissionNameArray, [\n                'users-manage',\n                'user-roles-manage',\n                'restrictions-manage-all',\n                'restrictions-manage-own',\n                'settings-manage',\n            ]));\n        }\n\n        if (!empty($permissionNameArray)) {\n            $permissions = RolePermission::query()\n                ->whereIn('name', $permissionNameArray)\n                ->pluck('id')\n                ->toArray();\n        }\n\n        $role->permissions()->sync($permissions);\n    }\n\n    /**\n     * Delete a role from the system.\n     * Check it's not an admin role or set as default before deleting.\n     * If a migration Role ID is specified, the users assigned to the current role\n     * will be added to the role of the specified id.\n     *\n     * @throws PermissionsException\n     * @throws Exception\n     */\n    public function deleteRole(int $roleId, int $migrateRoleId = 0): void\n    {\n        $role = $this->getRoleById($roleId);\n\n        // Prevent deleting admin role or default registration role.\n        if ($role->system_name && in_array($role->system_name, $this->systemRoles)) {\n            throw new PermissionsException(trans('errors.role_system_cannot_be_deleted'));\n        } elseif ($role->id === intval(setting('registration-role'))) {\n            throw new PermissionsException(trans('errors.role_registration_default_cannot_delete'));\n        }\n\n        (new DatabaseTransaction(function () use ($migrateRoleId, $role) {\n            if ($migrateRoleId !== 0) {\n                $newRole = Role::query()->find($migrateRoleId);\n                if ($newRole) {\n                    $users = $role->users()->pluck('id')->toArray();\n                    $newRole->users()->sync($users);\n                }\n            }\n\n            $role->entityPermissions()->delete();\n            $role->jointPermissions()->delete();\n            Activity::add(ActivityType::ROLE_DELETE, $role);\n            $role->delete();\n        }))->run();\n    }\n}\n"
  },
  {
    "path": "app/Permissions/SimpleEntityData.php",
    "content": "<?php\n\nnamespace BookStack\\Permissions;\n\nuse BookStack\\Entities\\Models\\Entity;\n\nclass SimpleEntityData\n{\n    public int $id;\n    public string $type;\n    public int $owned_by;\n    public ?int $book_id;\n    public ?int $chapter_id;\n\n    public static function fromEntity(Entity $entity): self\n    {\n        $attrs = $entity->getAttributes();\n        $simple = new self();\n\n        $simple->id = $attrs['id'];\n        $simple->type = $entity->getMorphClass();\n        $simple->owned_by = $attrs['owned_by'] ?? 0;\n        $simple->book_id = $attrs['book_id'] ?? null;\n        $simple->chapter_id = $attrs['chapter_id'] ?? null;\n\n        return $simple;\n    }\n}\n"
  },
  {
    "path": "app/References/CrossLinkParser.php",
    "content": "<?php\n\nnamespace BookStack\\References;\n\nuse BookStack\\App\\Model;\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse BookStack\\References\\ModelResolvers\\BookLinkModelResolver;\nuse BookStack\\References\\ModelResolvers\\BookshelfLinkModelResolver;\nuse BookStack\\References\\ModelResolvers\\ChapterLinkModelResolver;\nuse BookStack\\References\\ModelResolvers\\CrossLinkModelResolver;\nuse BookStack\\References\\ModelResolvers\\PageLinkModelResolver;\nuse BookStack\\References\\ModelResolvers\\PagePermalinkModelResolver;\nuse BookStack\\Util\\HtmlDocument;\n\nclass CrossLinkParser\n{\n    /**\n     * @var CrossLinkModelResolver[]\n     */\n    protected array $modelResolvers;\n\n    public function __construct(array $modelResolvers)\n    {\n        $this->modelResolvers = $modelResolvers;\n    }\n\n    /**\n     * Extract any found models within the given HTML content.\n     *\n     * @return Model[]\n     */\n    public function extractLinkedModels(string $html): array\n    {\n        $models = [];\n\n        $links = $this->getLinksFromContent($html);\n\n        foreach ($links as $link) {\n            $model = $this->linkToModel($link);\n            if (!is_null($model)) {\n                $models[get_class($model) . ':' . $model->id] = $model;\n            }\n        }\n\n        return array_values($models);\n    }\n\n    /**\n     * Get a list of href values from the given document.\n     *\n     * @return string[]\n     */\n    protected function getLinksFromContent(string $html): array\n    {\n        $links = [];\n\n        $doc = new HtmlDocument($html);\n        $anchors = $doc->queryXPath('//a[@href]');\n\n        /** @var \\DOMElement $anchor */\n        foreach ($anchors as $anchor) {\n            $links[] = $anchor->getAttribute('href');\n        }\n\n        return $links;\n    }\n\n    /**\n     * Attempt to resolve the given link to a model using the instance model resolvers.\n     */\n    protected function linkToModel(string $link): ?Model\n    {\n        foreach ($this->modelResolvers as $resolver) {\n            $model = $resolver->resolve($link);\n            if (!is_null($model)) {\n                return $model;\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * Create a new instance with a pre-defined set of model resolvers, specifically for the\n     * default set of entities within BookStack.\n     */\n    public static function createWithEntityResolvers(): self\n    {\n        $queries = app()->make(EntityQueries::class);\n\n        return new self([\n            new PagePermalinkModelResolver($queries->pages),\n            new PageLinkModelResolver($queries->pages),\n            new ChapterLinkModelResolver($queries->chapters),\n            new BookLinkModelResolver($queries->books),\n            new BookshelfLinkModelResolver($queries->shelves),\n        ]);\n    }\n}\n"
  },
  {
    "path": "app/References/ModelResolvers/AttachmentModelResolver.php",
    "content": "<?php\n\nnamespace BookStack\\References\\ModelResolvers;\n\nuse BookStack\\Uploads\\Attachment;\n\nclass AttachmentModelResolver implements CrossLinkModelResolver\n{\n    public function resolve(string $link): ?Attachment\n    {\n        $pattern = '/^' . preg_quote(url('/attachments'), '/') . '\\/(\\d+)/';\n        $matches = [];\n        $match = preg_match($pattern, $link, $matches);\n        if (!$match) {\n            return null;\n        }\n\n        $id = intval($matches[1]);\n\n        return Attachment::query()->find($id);\n    }\n}\n"
  },
  {
    "path": "app/References/ModelResolvers/BookLinkModelResolver.php",
    "content": "<?php\n\nnamespace BookStack\\References\\ModelResolvers;\n\nuse BookStack\\App\\Model;\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Queries\\BookQueries;\n\nclass BookLinkModelResolver implements CrossLinkModelResolver\n{\n    public function __construct(\n        protected BookQueries $queries\n    ) {\n    }\n\n    public function resolve(string $link): ?Model\n    {\n        $pattern = '/^' . preg_quote(url('/books'), '/') . '\\/([\\w-]+)' . '([#?\\/]|$)/';\n        $matches = [];\n        $match = preg_match($pattern, $link, $matches);\n        if (!$match) {\n            return null;\n        }\n\n        $bookSlug = $matches[1];\n\n        /** @var ?Book $model */\n        $model = $this->queries->start()->where('slug', '=', $bookSlug)->first(['id']);\n\n        return $model;\n    }\n}\n"
  },
  {
    "path": "app/References/ModelResolvers/BookshelfLinkModelResolver.php",
    "content": "<?php\n\nnamespace BookStack\\References\\ModelResolvers;\n\nuse BookStack\\App\\Model;\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Entities\\Queries\\BookshelfQueries;\n\nclass BookshelfLinkModelResolver implements CrossLinkModelResolver\n{\n    public function __construct(\n        protected BookshelfQueries $queries\n    ) {\n    }\n    public function resolve(string $link): ?Model\n    {\n        $pattern = '/^' . preg_quote(url('/shelves'), '/') . '\\/([\\w-]+)' . '([#?\\/]|$)/';\n        $matches = [];\n        $match = preg_match($pattern, $link, $matches);\n        if (!$match) {\n            return null;\n        }\n\n        $shelfSlug = $matches[1];\n\n        /** @var ?Bookshelf $model */\n        $model = $this->queries->start()->where('slug', '=', $shelfSlug)->first(['id']);\n\n        return $model;\n    }\n}\n"
  },
  {
    "path": "app/References/ModelResolvers/ChapterLinkModelResolver.php",
    "content": "<?php\n\nnamespace BookStack\\References\\ModelResolvers;\n\nuse BookStack\\App\\Model;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Queries\\ChapterQueries;\n\nclass ChapterLinkModelResolver implements CrossLinkModelResolver\n{\n    public function __construct(\n        protected ChapterQueries $queries\n    ) {\n    }\n\n    public function resolve(string $link): ?Model\n    {\n        $pattern = '/^' . preg_quote(url('/books'), '/') . '\\/([\\w-]+)' . '\\/chapter\\/' . '([\\w-]+)' . '([#?\\/]|$)/';\n        $matches = [];\n        $match = preg_match($pattern, $link, $matches);\n        if (!$match) {\n            return null;\n        }\n\n        $bookSlug = $matches[1];\n        $chapterSlug = $matches[2];\n\n        /** @var ?Chapter $model */\n        $model = $this->queries->usingSlugs($bookSlug, $chapterSlug)->first(['id']);\n\n        return $model;\n    }\n}\n"
  },
  {
    "path": "app/References/ModelResolvers/CrossLinkModelResolver.php",
    "content": "<?php\n\nnamespace BookStack\\References\\ModelResolvers;\n\nuse BookStack\\App\\Model;\n\ninterface CrossLinkModelResolver\n{\n    /**\n     * Resolve the given href link value to a model.\n     */\n    public function resolve(string $link): ?Model;\n}\n"
  },
  {
    "path": "app/References/ModelResolvers/ImageModelResolver.php",
    "content": "<?php\n\nnamespace BookStack\\References\\ModelResolvers;\n\nuse BookStack\\Uploads\\Image;\nuse BookStack\\Uploads\\ImageStorage;\n\nclass ImageModelResolver implements CrossLinkModelResolver\n{\n    protected ?string $pattern = null;\n\n    public function resolve(string $link): ?Image\n    {\n        $pattern = $this->getUrlPattern();\n        $matches = [];\n        $match = preg_match($pattern, $link, $matches);\n        if (!$match) {\n            return null;\n        }\n\n        $path = $matches[2];\n\n        // Strip thumbnail element from path if existing\n        $originalPathSplit = array_filter(explode('/', $path), function (string $part) {\n            $resizedDir = (str_starts_with($part, 'thumbs-') || str_starts_with($part, 'scaled-'));\n            $missingExtension = !str_contains($part, '.');\n\n            return !($resizedDir && $missingExtension);\n        });\n\n        // Build a database-format image path and search for the image entry\n        $fullPath = '/uploads/images/' . ltrim(implode('/', $originalPathSplit), '/');\n\n        return Image::query()->where('path', '=', $fullPath)->first();\n    }\n\n    /**\n     * Get the regex pattern to identify image URLs.\n     * Caches the pattern since it requires looking up to settings/config.\n     */\n    protected function getUrlPattern(): string\n    {\n        if ($this->pattern) {\n            return $this->pattern;\n        }\n\n        $urls = [url('/uploads/images')];\n        $baseImageUrl = ImageStorage::getPublicUrl('/uploads/images');\n        if ($baseImageUrl !== $urls[0]) {\n            $urls[] = $baseImageUrl;\n        }\n\n        $imageUrlRegex = implode('|', array_map(fn ($url) => preg_quote($url, '/'), $urls));\n        $this->pattern = '/^(' . $imageUrlRegex . ')\\/(.+)/';\n\n        return $this->pattern;\n    }\n}\n"
  },
  {
    "path": "app/References/ModelResolvers/PageLinkModelResolver.php",
    "content": "<?php\n\nnamespace BookStack\\References\\ModelResolvers;\n\nuse BookStack\\App\\Model;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Queries\\PageQueries;\n\nclass PageLinkModelResolver implements CrossLinkModelResolver\n{\n    public function __construct(\n        protected PageQueries $queries\n    ) {\n    }\n\n    public function resolve(string $link): ?Model\n    {\n        $pattern = '/^' . preg_quote(url('/books'), '/') . '\\/([\\w-]+)' . '\\/page\\/' . '([\\w-]+)' . '([#?\\/]|$)/';\n        $matches = [];\n        $match = preg_match($pattern, $link, $matches);\n        if (!$match) {\n            return null;\n        }\n\n        $bookSlug = $matches[1];\n        $pageSlug = $matches[2];\n\n        /** @var ?Page $model */\n        $model = $this->queries->usingSlugs($bookSlug, $pageSlug)->first(['id']);\n\n        return $model;\n    }\n}\n"
  },
  {
    "path": "app/References/ModelResolvers/PagePermalinkModelResolver.php",
    "content": "<?php\n\nnamespace BookStack\\References\\ModelResolvers;\n\nuse BookStack\\App\\Model;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Queries\\PageQueries;\n\nclass PagePermalinkModelResolver implements CrossLinkModelResolver\n{\n    public function __construct(\n        protected PageQueries $queries\n    ) {\n    }\n\n    public function resolve(string $link): ?Model\n    {\n        $pattern = '/^' . preg_quote(url('/link'), '/') . '\\/(\\d+)/';\n        $matches = [];\n        $match = preg_match($pattern, $link, $matches);\n        if (!$match) {\n            return null;\n        }\n\n        $id = intval($matches[1]);\n        /** @var ?Page $model */\n        $model = $this->queries->start()->find($id, ['id']);\n\n        return $model;\n    }\n}\n"
  },
  {
    "path": "app/References/Reference.php",
    "content": "<?php\n\nnamespace BookStack\\References;\n\nuse BookStack\\Permissions\\Models\\JointPermission;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\MorphTo;\n\n/**\n * @property int    $from_id\n * @property string $from_type\n * @property int    $to_id\n * @property string $to_type\n */\nclass Reference extends Model\n{\n    public $timestamps = false;\n\n    public function from(): MorphTo\n    {\n        return $this->morphTo('from');\n    }\n\n    public function to(): MorphTo\n    {\n        return $this->morphTo('to');\n    }\n\n    public function jointPermissions(): HasMany\n    {\n        return $this->hasMany(JointPermission::class, 'entity_id', 'from_id')\n            ->whereColumn('references.from_type', '=', 'joint_permissions.entity_type');\n    }\n}\n"
  },
  {
    "path": "app/References/ReferenceChangeContext.php",
    "content": "<?php\n\nnamespace BookStack\\References;\n\nuse BookStack\\Entities\\Models\\Entity;\n\nclass ReferenceChangeContext\n{\n    /**\n     * Entity pairs where the first is the old entity and the second is the new entity.\n     * @var array<array{0: Entity, 1: Entity}>\n     */\n    protected array $changes = [];\n\n    public function add(Entity $oldEntity, Entity $newEntity): void\n    {\n        $this->changes[] = [$oldEntity, $newEntity];\n    }\n\n    /**\n     * Get all the new entities from the changes.\n     */\n    public function getNewEntities(): array\n    {\n        return array_column($this->changes, 1);\n    }\n\n    /**\n     * Get all the old entities from the changes.\n     */\n    public function getOldEntities(): array\n    {\n        return array_column($this->changes, 0);\n    }\n\n    public function getNewForOld(Entity $oldEntity): ?Entity\n    {\n        foreach ($this->changes as [$old, $new]) {\n            if ($old->id === $oldEntity->id && $old->type === $oldEntity->type) {\n                return $new;\n            }\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "app/References/ReferenceController.php",
    "content": "<?php\n\nnamespace BookStack\\References;\n\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse BookStack\\Http\\Controller;\n\nclass ReferenceController extends Controller\n{\n    public function __construct(\n        protected ReferenceFetcher $referenceFetcher,\n        protected EntityQueries $queries,\n    ) {\n    }\n\n    /**\n     * Display the references to a given page.\n     */\n    public function page(string $bookSlug, string $pageSlug)\n    {\n        $page = $this->queries->pages->findVisibleBySlugsOrFail($bookSlug, $pageSlug);\n        $references = $this->referenceFetcher->getReferencesToEntity($page);\n\n        return view('pages.references', [\n            'page'       => $page,\n            'references' => $references,\n        ]);\n    }\n\n    /**\n     * Display the references to a given chapter.\n     */\n    public function chapter(string $bookSlug, string $chapterSlug)\n    {\n        $chapter = $this->queries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);\n        $references = $this->referenceFetcher->getReferencesToEntity($chapter);\n\n        return view('chapters.references', [\n            'chapter'    => $chapter,\n            'references' => $references,\n        ]);\n    }\n\n    /**\n     * Display the references to a given book.\n     */\n    public function book(string $slug)\n    {\n        $book = $this->queries->books->findVisibleBySlugOrFail($slug);\n        $references = $this->referenceFetcher->getReferencesToEntity($book);\n\n        return view('books.references', [\n            'book'       => $book,\n            'references' => $references,\n        ]);\n    }\n\n    /**\n     * Display the references to a given shelf.\n     */\n    public function shelf(string $slug)\n    {\n        $shelf = $this->queries->shelves->findVisibleBySlugOrFail($slug);\n        $references = $this->referenceFetcher->getReferencesToEntity($shelf);\n\n        return view('shelves.references', [\n            'shelf'      => $shelf,\n            'references' => $references,\n        ]);\n    }\n}\n"
  },
  {
    "path": "app/References/ReferenceFetcher.php",
    "content": "<?php\n\nnamespace BookStack\\References;\n\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Tools\\MixedEntityListLoader;\nuse BookStack\\Permissions\\PermissionApplicator;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Collection;\n\nclass ReferenceFetcher\n{\n    public function __construct(\n        protected PermissionApplicator $permissions,\n        protected MixedEntityListLoader $mixedEntityListLoader,\n    ) {\n    }\n\n    /**\n     * Query and return the references pointing to the given entity.\n     * Loads the commonly required relations while taking permissions into account.\n     */\n    public function getReferencesToEntity(Entity $entity, bool $withContents = false): Collection\n    {\n        $references = $this->queryReferencesToEntity($entity)->get();\n        $this->mixedEntityListLoader->loadIntoRelations($references->all(), 'from', false, $withContents);\n\n        return $references;\n    }\n\n    /**\n     * Returns the count of references pointing to the given entity.\n     * Takes permissions into account.\n     */\n    public function getReferenceCountToEntity(Entity $entity): int\n    {\n        return $this->queryReferencesToEntity($entity)->count();\n    }\n\n    protected function queryReferencesToEntity(Entity $entity): Builder\n    {\n        $baseQuery = Reference::query()\n            ->where('to_type', '=', $entity->getMorphClass())\n            ->where('to_id', '=', $entity->id)\n            ->whereHas('from');\n\n        return $this->permissions->restrictEntityRelationQuery(\n            $baseQuery,\n            'references',\n            'from_id',\n            'from_type'\n        );\n    }\n}\n"
  },
  {
    "path": "app/References/ReferenceStore.php",
    "content": "<?php\n\nnamespace BookStack\\References;\n\nuse BookStack\\Entities\\EntityProvider;\nuse BookStack\\Entities\\Models\\Entity;\nuse Illuminate\\Database\\Eloquent\\Collection;\n\nclass ReferenceStore\n{\n    public function __construct(\n        protected EntityProvider $entityProvider\n    ) {\n    }\n\n    /**\n     * Update the outgoing references for the given entity.\n     */\n    public function updateForEntity(Entity $entity): void\n    {\n        $this->updateForEntities([$entity]);\n    }\n\n    /**\n     * Update the outgoing references for all entities in the system.\n     */\n    public function updateForAll(): void\n    {\n        Reference::query()->delete();\n\n        foreach ($this->entityProvider->all() as $entity) {\n            $entity->newQuery()->select(['id', $entity->htmlField])->chunk(100, function (Collection $entities) {\n                $this->updateForEntities($entities->all());\n            });\n        }\n    }\n\n    /**\n     * Update the outgoing references for the entities in the given array.\n     *\n     * @param Entity[] $entities\n     */\n    protected function updateForEntities(array $entities): void\n    {\n        if (count($entities) === 0) {\n            return;\n        }\n\n        $parser = CrossLinkParser::createWithEntityResolvers();\n        $references = [];\n\n        $this->dropReferencesFromEntities($entities);\n\n        foreach ($entities as $entity) {\n            $models = $parser->extractLinkedModels($entity->getAttribute($entity->htmlField));\n\n            foreach ($models as $model) {\n                $references[] = [\n                    'from_id'   => $entity->id,\n                    'from_type' => $entity->getMorphClass(),\n                    'to_id'     => $model->id,\n                    'to_type'   => $model->getMorphClass(),\n                ];\n            }\n        }\n\n        foreach (array_chunk($references, 1000) as $referenceDataChunk) {\n            Reference::query()->insert($referenceDataChunk);\n        }\n    }\n\n    /**\n     * Delete all the existing references originating from the given entities.\n     * @param Entity[] $entities\n     */\n    protected function dropReferencesFromEntities(array $entities): void\n    {\n        $IdsByType = [];\n\n        foreach ($entities as $entity) {\n            $type = $entity->getMorphClass();\n            if (!isset($IdsByType[$type])) {\n                $IdsByType[$type] = [];\n            }\n\n            $IdsByType[$type][] = $entity->id;\n        }\n\n        foreach ($IdsByType as $type => $entityIds) {\n            Reference::query()\n                ->where('from_type', '=', $type)\n                ->whereIn('from_id', $entityIds)\n                ->delete();\n        }\n    }\n}\n"
  },
  {
    "path": "app/References/ReferenceUpdater.php",
    "content": "<?php\n\nnamespace BookStack\\References;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\HasDescriptionInterface;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Repos\\RevisionRepo;\nuse BookStack\\Util\\HtmlDocument;\n\nclass ReferenceUpdater\n{\n    public function __construct(\n        protected ReferenceFetcher $referenceFetcher,\n        protected RevisionRepo $revisionRepo,\n    ) {\n    }\n\n    public function updateEntityReferences(Entity $entity, string $oldLink): void\n    {\n        $references = $this->getReferencesToUpdate($entity);\n        $newLink = $entity->getUrl();\n\n        foreach ($references as $reference) {\n            /** @var Entity $entity */\n            $entity = $reference->from;\n            $this->updateReferencesWithinEntity($entity, $oldLink, $newLink);\n        }\n    }\n\n    /**\n     * Change existing references for a range of entities using the given context.\n     */\n    public function changeReferencesUsingContext(ReferenceChangeContext $context): void\n    {\n        $bindings = [];\n        foreach ($context->getOldEntities() as $old) {\n            $bindings[] = $old->getMorphClass();\n            $bindings[] = $old->id;\n        }\n\n        // No targets to update within the context, so no need to continue.\n        if (count($bindings) < 2) {\n            return;\n        }\n\n        $toReferenceQuery = '(to_type, to_id) IN (' . rtrim(str_repeat('(?,?),', count($bindings) / 2), ',') . ')';\n\n        // Cycle each new entity in the context\n        foreach ($context->getNewEntities() as $new) {\n            // For each, get all references from it which lead to other items within the context of the change\n            $newReferencesInContext = $new->referencesFrom()->whereRaw($toReferenceQuery, $bindings)->get();\n            // For each reference, update the URL and the reference entry\n            foreach ($newReferencesInContext as $reference) {\n                $oldToEntity = $reference->to;\n                $newToEntity = $context->getNewForOld($oldToEntity);\n                if ($newToEntity === null) {\n                    continue;\n                }\n\n                $this->updateReferencesWithinEntity($new, $oldToEntity->getUrl(), $newToEntity->getUrl());\n                if ($newToEntity instanceof Page && $oldToEntity instanceof Page) {\n                    $this->updateReferencesWithinEntity($new, $oldToEntity->getPermalink(), $newToEntity->getPermalink());\n                }\n                $reference->to_id = $newToEntity->id;\n                $reference->to_type = $newToEntity->getMorphClass();\n                $reference->save();\n            }\n        }\n    }\n\n    /**\n     * @return Reference[]\n     */\n    protected function getReferencesToUpdate(Entity $entity): array\n    {\n        /** @var Reference[] $references */\n        $references = $this->referenceFetcher->getReferencesToEntity($entity, true)->values()->all();\n\n        if ($entity instanceof Book) {\n            $pages = $entity->pages()->get(['id']);\n            $chapters = $entity->chapters()->get(['id']);\n            $children = $pages->concat($chapters);\n            foreach ($children as $bookChild) {\n                /** @var Reference[] $childRefs */\n                $childRefs = $this->referenceFetcher->getReferencesToEntity($bookChild, true)->values()->all();\n                array_push($references, ...$childRefs);\n            }\n        }\n\n        $deduped = [];\n        foreach ($references as $reference) {\n            $key = $reference->from_id . ':' . $reference->from_type;\n            $deduped[$key] = $reference;\n        }\n\n        return array_values($deduped);\n    }\n\n    protected function updateReferencesWithinEntity(Entity $entity, string $oldLink, string $newLink): void\n    {\n        if ($entity instanceof Page) {\n            $this->updateReferencesWithinPage($entity, $oldLink, $newLink);\n        }\n\n        if ($entity instanceof HasDescriptionInterface) {\n            $this->updateReferencesWithinDescription($entity, $oldLink, $newLink);\n        }\n    }\n\n    protected function updateReferencesWithinDescription(Entity&HasDescriptionInterface $entity, string $oldLink, string $newLink): void\n    {\n        $description = $entity->descriptionInfo();\n        $html = $this->updateLinksInHtml($description->getHtml(true) ?: '', $oldLink, $newLink);\n        $description->set($html);\n        $entity->save();\n    }\n\n    protected function updateReferencesWithinPage(Page $page, string $oldLink, string $newLink): void\n    {\n        $page = (clone $page)->refresh();\n        $html = $this->updateLinksInHtml($page->html, $oldLink, $newLink);\n        $markdown = $this->updateLinksInMarkdown($page->markdown, $oldLink, $newLink);\n\n        $page->html = $html;\n        $page->markdown = $markdown;\n        $page->revision_count++;\n        $page->save();\n\n        $summary = trans('entities.pages_references_update_revision');\n        $this->revisionRepo->storeNewForPage($page, $summary);\n    }\n\n    protected function updateLinksInMarkdown(string $markdown, string $oldLink, string $newLink): string\n    {\n        if (empty($markdown)) {\n            return $markdown;\n        }\n\n        $commonLinkRegex = '/(\\[.*?\\]\\()' . preg_quote($oldLink, '/') . '(.*?\\))/i';\n        $markdown = preg_replace($commonLinkRegex, '$1' . $newLink . '$2', $markdown);\n\n        $referenceLinkRegex = '/(\\[.*?\\]:\\s?)' . preg_quote($oldLink, '/') . '(.*?)($|\\s)/i';\n        $markdown = preg_replace($referenceLinkRegex, '$1' . $newLink . '$2$3', $markdown);\n\n        return $markdown;\n    }\n\n    protected function updateLinksInHtml(string $html, string $oldLink, string $newLink): string\n    {\n        if (empty($html)) {\n            return $html;\n        }\n\n        $doc = new HtmlDocument($html);\n        $anchors = $doc->queryXPath('//a[@href]');\n\n        /** @var \\DOMElement $anchor */\n        foreach ($anchors as $anchor) {\n            $link = $anchor->getAttribute('href');\n            $updated = str_ireplace($oldLink, $newLink, $link);\n            $anchor->setAttribute('href', $updated);\n        }\n\n        return $doc->getBodyInnerHtml();\n    }\n}\n"
  },
  {
    "path": "app/Search/Options/ExactSearchOption.php",
    "content": "<?php\n\nnamespace BookStack\\Search\\Options;\n\nclass ExactSearchOption extends SearchOption\n{\n    public function toString(): string\n    {\n        $escaped = str_replace('\\\\', '\\\\\\\\', $this->value);\n        $escaped = str_replace('\"', '\\\"', $escaped);\n        return ($this->negated ? '-' : '') . '\"' . $escaped . '\"';\n    }\n}\n"
  },
  {
    "path": "app/Search/Options/FilterSearchOption.php",
    "content": "<?php\n\nnamespace BookStack\\Search\\Options;\n\nclass FilterSearchOption extends SearchOption\n{\n    protected string $name;\n\n    public function __construct(\n        string $value,\n        string $name,\n        bool $negated = false,\n    ) {\n        parent::__construct($value, $negated);\n        $this->name = $name;\n    }\n\n    public function toString(): string\n    {\n        $valueText = ($this->value ? ':' . $this->value : '');\n        $filterBrace = '{' . $this->name .  $valueText . '}';\n        return ($this->negated ? '-' : '') . $filterBrace;\n    }\n\n    public function getKey(): string\n    {\n        return $this->name;\n    }\n\n    public static function fromContentString(string $value, bool $negated = false): self\n    {\n        $explodedFilter = explode(':', $value, 2);\n        $filterValue = (count($explodedFilter) > 1) ? $explodedFilter[1] : '';\n        $filterName = $explodedFilter[0];\n        return new self($filterValue, $filterName, $negated);\n    }\n}\n"
  },
  {
    "path": "app/Search/Options/SearchOption.php",
    "content": "<?php\n\nnamespace BookStack\\Search\\Options;\n\nabstract class SearchOption\n{\n    public function __construct(\n        public string $value,\n        public bool $negated = false,\n    ) {\n    }\n\n    /**\n     * Get the key used for this option when used in a map.\n     * Null indicates to use the index of the containing array.\n     */\n    public function getKey(): string|null\n    {\n        return null;\n    }\n\n    /**\n     * Get the search string representation for this search option.\n     */\n    abstract public function toString(): string;\n}\n"
  },
  {
    "path": "app/Search/Options/TagSearchOption.php",
    "content": "<?php\n\nnamespace BookStack\\Search\\Options;\n\nclass TagSearchOption extends SearchOption\n{\n    /**\n     * Acceptable operators to be used within a tag search option.\n     *\n     * @var string[]\n     */\n    protected array $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];\n\n    public function toString(): string\n    {\n        return ($this->negated ? '-' : '') . \"[{$this->value}]\";\n    }\n\n    /**\n     * @return array{name: string, operator: string, value: string}\n     */\n    public function getParts(): array\n    {\n        $operatorRegex = implode('|', array_map(fn($op) => preg_quote($op), $this->queryOperators));\n        preg_match('/^(.*?)((' . $operatorRegex . ')(.*?))?$/', $this->value, $tagSplit);\n\n        $extractedOperator = count($tagSplit) > 2 ? $tagSplit[3] : '';\n        $tagOperator = in_array($extractedOperator, $this->queryOperators) ? $extractedOperator : '=';\n        $tagValue = count($tagSplit) > 3 ? $tagSplit[4] : '';\n\n        return [\n            'name' => $tagSplit[1],\n            'operator' => $tagOperator,\n            'value' => $tagValue,\n        ];\n    }\n}\n"
  },
  {
    "path": "app/Search/Options/TermSearchOption.php",
    "content": "<?php\n\nnamespace BookStack\\Search\\Options;\n\nclass TermSearchOption extends SearchOption\n{\n    public function toString(): string\n    {\n        return $this->value;\n    }\n}\n"
  },
  {
    "path": "app/Search/SearchApiController.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace BookStack\\Search;\n\nuse BookStack\\Api\\ApiEntityListFormatter;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Http\\ApiController;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\n\nclass SearchApiController extends ApiController\n{\n    protected array $rules = [\n        'all' => [\n            'query' => ['required'],\n            'page'  => ['integer', 'min:1'],\n            'count' => ['integer', 'min:1', 'max:100'],\n        ],\n    ];\n\n    public function __construct(\n        protected SearchRunner $searchRunner,\n        protected SearchResultsFormatter $resultsFormatter\n    ) {\n    }\n\n    /**\n     * Run a search query against all main content types (shelves, books, chapters & pages)\n     * in the system. Takes the same input as the main search bar within the BookStack\n     * interface as a 'query' parameter. See https://www.bookstackapp.com/docs/user/searching/\n     * for a full list of search term options. Results contain a 'type' property to distinguish\n     * between: bookshelf, book, chapter & page.\n     *\n     * The paging parameters and response format emulates a standard listing endpoint\n     * but standard sorting and filtering cannot be done on this endpoint.\n     */\n    public function all(Request $request): JsonResponse\n    {\n        $this->validate($request, $this->rules['all']);\n\n        $options = SearchOptions::fromString($request->get('query') ?? '');\n        $page = intval($request->get('page', '0')) ?: 1;\n        $count = min(intval($request->get('count', '0')) ?: 20, 100);\n\n        $results = $this->searchRunner->searchEntities($options, 'all', $page, $count);\n        $this->resultsFormatter->format($results['results']->all(), $options);\n\n        $data = (new ApiEntityListFormatter($results['results']->all()))\n            ->withType()->withTags()->withParents()\n            ->withField('preview_html', function (Entity $entity) {\n                return [\n                    'name' => (string) $entity->getAttribute('preview_name'),\n                    'content' => (string) $entity->getAttribute('preview_content'),\n                ];\n            })->format();\n\n        return response()->json([\n            'data' => $data,\n            'total' => $results['total'],\n        ]);\n    }\n}\n"
  },
  {
    "path": "app/Search/SearchController.php",
    "content": "<?php\n\nnamespace BookStack\\Search;\n\nuse BookStack\\Entities\\Queries\\PageQueries;\nuse BookStack\\Entities\\Queries\\QueryPopular;\nuse BookStack\\Entities\\Tools\\SiblingFetcher;\nuse BookStack\\Http\\Controller;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\n\nclass SearchController extends Controller\n{\n    public function __construct(\n        protected SearchRunner $searchRunner,\n        protected PageQueries $pageQueries,\n    ) {\n    }\n\n    /**\n     * Searches all entities.\n     */\n    public function search(Request $request, SearchResultsFormatter $formatter)\n    {\n        $searchOpts = SearchOptions::fromRequest($request);\n        $fullSearchString = $searchOpts->toString();\n        $page = intval($request->get('page', '0')) ?: 1;\n        $count = setting()->getInteger('lists-page-count-search', 18, 1, 1000);\n\n        $results = $this->searchRunner->searchEntities($searchOpts, 'all', $page, $count);\n        $formatter->format($results['results']->all(), $searchOpts);\n        $paginator = new LengthAwarePaginator($results['results'], $results['total'], $count, $page);\n        $paginator->setPath(url('/search'));\n        $paginator->appends($request->except('page'));\n\n        $this->setPageTitle(trans('entities.search_for_term', ['term' => $fullSearchString]));\n\n        return view('search.all', [\n            'entities'     => $results['results'],\n            'totalResults' => $results['total'],\n            'paginator'    => $paginator,\n            'searchTerm'   => $fullSearchString,\n            'options'      => $searchOpts,\n        ]);\n    }\n\n    /**\n     * Searches all entities within a book.\n     */\n    public function searchBook(Request $request, int $bookId)\n    {\n        $term = $request->get('term', '');\n        $results = $this->searchRunner->searchBook($bookId, $term);\n\n        return view('entities.list', ['entities' => $results]);\n    }\n\n    /**\n     * Searches all entities within a chapter.\n     */\n    public function searchChapter(Request $request, int $chapterId)\n    {\n        $term = $request->get('term', '');\n        $results = $this->searchRunner->searchChapter($chapterId, $term);\n\n        return view('entities.list', ['entities' => $results]);\n    }\n\n    /**\n     * Search for a list of entities and return a partial HTML response of matching entities.\n     * Returns the most popular entities if no search is provided.\n     */\n    public function searchForSelector(Request $request, QueryPopular $queryPopular)\n    {\n        $entityTypes = $request->filled('types') ? explode(',', $request->get('types')) : ['page', 'chapter', 'book'];\n        $searchTerm = $request->get('term', false);\n        $permission = $request->get('permission', 'view');\n\n        // Search for entities otherwise show most popular\n        if ($searchTerm !== false) {\n            $options = SearchOptions::fromString($searchTerm);\n            $options->setFilter('type', implode('|', $entityTypes));\n            $entities = $this->searchRunner->searchEntities($options, 'all', 1, 20)['results'];\n        } else {\n            $entities = $queryPopular->run(20, 0, $entityTypes);\n        }\n\n        return view('search.parts.entity-selector-list', ['entities' => $entities, 'permission' => $permission]);\n    }\n\n    /**\n     * Search for a list of templates to choose from.\n     */\n    public function templatesForSelector(Request $request)\n    {\n        $searchTerm = $request->get('term', false);\n\n        if ($searchTerm !== false) {\n            $searchOptions = SearchOptions::fromString($searchTerm);\n            $searchOptions->setFilter('is_template');\n            $entities = $this->searchRunner->searchEntities($searchOptions, 'page', 1, 20)['results'];\n        } else {\n            $entities = $this->pageQueries->visibleTemplates()\n                ->where('draft', '=', false)\n                ->orderBy('updated_at', 'desc')\n                ->take(20)\n                ->get();\n        }\n\n        return view('search.parts.entity-selector-list', [\n            'entities' => $entities,\n            'permission' => 'view'\n        ]);\n    }\n\n    /**\n     * Search for a list of entities and return a partial HTML response of matching entities\n     * to be used as a result preview suggestion list for global system searches.\n     */\n    public function searchSuggestions(Request $request)\n    {\n        $searchTerm = $request->get('term', '');\n        $entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 5)['results'];\n\n        foreach ($entities as $entity) {\n            $entity->setAttribute('preview_content', '');\n        }\n\n        return view('search.parts.entity-suggestion-list', [\n            'entities' => $entities->slice(0, 5)\n        ]);\n    }\n\n    /**\n     * Search sibling items in the system.\n     */\n    public function searchSiblings(Request $request, SiblingFetcher $siblingFetcher)\n    {\n        $type = $request->get('entity_type', null);\n        $id = $request->get('entity_id', null);\n\n        $entities = $siblingFetcher->fetch($type, $id);\n\n        return view('entities.list-basic', ['entities' => $entities, 'style' => 'compact']);\n    }\n}\n"
  },
  {
    "path": "app/Search/SearchIndex.php",
    "content": "<?php\n\nnamespace BookStack\\Search;\n\nuse BookStack\\Activity\\Models\\Tag;\nuse BookStack\\Entities\\EntityProvider;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Util\\HtmlDocument;\nuse DOMNode;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Collection;\n\nclass SearchIndex\n{\n    /**\n     * A list of delimiter characters used to break-up parsed content into terms for indexing.\n     */\n    public static string $delimiters = \" \\n\\t.-,!?:;()[]{}<>`'\\\"«»\";\n\n    /**\n     * A list of delimiter which could be commonly used within a single term and also indicate a break between terms.\n     * The indexer will index the full term with these delimiters, plus the terms split via these delimiters.\n     */\n    public static string $softDelimiters = \".-\";\n\n    public function __construct(\n        protected EntityProvider $entityProvider\n    ) {\n    }\n\n    /**\n     * Index the given entity.\n     */\n    public function indexEntity(Entity $entity): void\n    {\n        $this->deleteEntityTerms($entity);\n        $terms = $this->entityToTermDataArray($entity);\n        $this->insertTerms($terms);\n    }\n\n    /**\n     * Index multiple Entities at once.\n     *\n     * @param Entity[] $entities\n     */\n    public function indexEntities(array $entities): void\n    {\n        $terms = [];\n        foreach ($entities as $entity) {\n            $entityTerms = $this->entityToTermDataArray($entity);\n            array_push($terms, ...$entityTerms);\n        }\n\n        $this->insertTerms($terms);\n    }\n\n    /**\n     * Delete and re-index the terms for all entities in the system.\n     * Can take a callback which is used for reporting progress.\n     * Callback receives three arguments:\n     * - An instance of the model being processed\n     * - The number that have been processed so far.\n     * - The total number of that model to be processed.\n     *\n     * @param callable(Entity, int, int):void|null $progressCallback\n     */\n    public function indexAllEntities(?callable $progressCallback = null): void\n    {\n        SearchTerm::query()->truncate();\n\n        foreach ($this->entityProvider->all() as $entityModel) {\n            $indexContentField = $entityModel instanceof Page ? 'html' : 'description';\n            $selectFields = ['id', 'name', $indexContentField];\n            /** @var Builder<Entity> $query */\n            $query = $entityModel->newQuery();\n            $total = $query->withTrashed()->count();\n            $chunkSize = 250;\n            $processed = 0;\n\n            $chunkCallback = function (Collection $entities) use ($progressCallback, &$processed, $total, $chunkSize, $entityModel) {\n                $this->indexEntities($entities->all());\n                $processed = min($processed + $chunkSize, $total);\n\n                if (is_callable($progressCallback)) {\n                    $progressCallback($entityModel, $processed, $total);\n                }\n            };\n\n            $entityModel->newQuery()\n                ->select($selectFields)\n                ->with(['tags:id,name,value,entity_id,entity_type'])\n                ->chunk($chunkSize, $chunkCallback);\n        }\n    }\n\n    /**\n     * Delete related Entity search terms.\n     */\n    public function deleteEntityTerms(Entity $entity): void\n    {\n        $entity->searchTerms()->delete();\n    }\n\n    /**\n     * Insert the given terms into the database.\n     * Chunks through the given terms to remain within database limits.\n     * @param array[] $terms\n     */\n    protected function insertTerms(array $terms): void\n    {\n        $chunkedTerms = array_chunk($terms, 500);\n        foreach ($chunkedTerms as $termChunk) {\n            SearchTerm::query()->insert($termChunk);\n        }\n    }\n\n    /**\n     * Create a scored term array from the given text, where the keys are the terms\n     * and the values are their scores.\n     *\n     * @return array<string, int>\n     */\n    protected function generateTermScoreMapFromText(string $text, float $scoreAdjustment = 1): array\n    {\n        $termMap = $this->textToTermCountMap($text);\n\n        foreach ($termMap as $term => $count) {\n            $termMap[$term] = intval($count * $scoreAdjustment);\n        }\n\n        return $termMap;\n    }\n\n    /**\n     * Create a scored term array from the given HTML, where the keys are the terms\n     * and the values are their scores.\n     *\n     * @return array<string, int>\n     */\n    protected function generateTermScoreMapFromHtml(string $html): array\n    {\n        if (empty($html)) {\n            return [];\n        }\n\n        $scoresByTerm = [];\n        $elementScoreAdjustmentMap = [\n            'h1' => 10,\n            'h2' => 5,\n            'h3' => 4,\n            'h4' => 3,\n            'h5' => 2,\n            'h6' => 1.5,\n        ];\n\n        $html = str_ireplace(['<br>', '<br />', '<br/>'], \"\\n\", $html);\n        $doc = new HtmlDocument($html);\n\n        /** @var DOMNode $child */\n        foreach ($doc->getBodyChildren() as $child) {\n            $nodeName = $child->nodeName;\n            $text = trim($child->textContent);\n            $text = str_replace(\"\\u{00A0}\", ' ', $text);\n            $termCounts = $this->textToTermCountMap($text);\n            foreach ($termCounts as $term => $count) {\n                $scoreChange = $count * ($elementScoreAdjustmentMap[$nodeName] ?? 1);\n                $scoresByTerm[$term] = ($scoresByTerm[$term] ?? 0) + $scoreChange;\n            }\n        }\n\n        return $scoresByTerm;\n    }\n\n    /**\n     * Create a scored term map from the given set of entity tags.\n     *\n     * @param Tag[] $tags\n     *\n     * @return array<string, int>\n     */\n    protected function generateTermScoreMapFromTags(array $tags): array\n    {\n        $names = [];\n        $values = [];\n\n        foreach ($tags as $tag) {\n            $names[] = $tag->name;\n            $values[] = $tag->value;\n        }\n\n        $nameMap = $this->generateTermScoreMapFromText(implode(' ', $names), 3);\n        $valueMap = $this->generateTermScoreMapFromText(implode(' ', $values), 5);\n\n        return $this->mergeTermScoreMaps($nameMap, $valueMap);\n    }\n\n    /**\n     * For the given text, return an array where the keys are the unique term words\n     * and the values are the frequency of that term.\n     *\n     * @return array<string, int>\n     */\n    protected function textToTermCountMap(string $text): array\n    {\n        $tokenMap = []; // {TextToken => OccurrenceCount}\n        $softDelims = static::$softDelimiters;\n        $tokenizer = new SearchTextTokenizer($text, static::$delimiters);\n        $extendedToken = '';\n        $extendedLen = 0;\n\n        $token = $tokenizer->next();\n\n        while ($token !== false) {\n            $delim = $tokenizer->previousDelimiter();\n\n            if ($delim && str_contains($softDelims, $delim) && $token !== '') {\n                $extendedToken .= $delim . $token;\n                $extendedLen++;\n            } else {\n                if ($extendedLen > 1) {\n                    $tokenMap[$extendedToken] = ($tokenMap[$extendedToken] ?? 0) + 1;\n                }\n                $extendedToken = $token;\n                $extendedLen = 1;\n            }\n\n            if ($token) {\n                $tokenMap[$token] = ($tokenMap[$token] ?? 0) + 1;\n            }\n\n            $token = $tokenizer->next();\n        }\n\n        if ($extendedLen > 1) {\n            $tokenMap[$extendedToken] = ($tokenMap[$extendedToken] ?? 0) + 1;\n        }\n\n        return $tokenMap;\n    }\n\n    /**\n     * For the given entity, Generate an array of term data details.\n     * Is the raw term data, not instances of SearchTerm models.\n     *\n     * @return array{term: string, score: float, entity_id: int, entity_type: string}[]\n     */\n    protected function entityToTermDataArray(Entity $entity): array\n    {\n        $nameTermsMap = $this->generateTermScoreMapFromText($entity->name, 40 * $entity->searchFactor);\n        $tagTermsMap = $this->generateTermScoreMapFromTags($entity->tags->all());\n\n        if ($entity instanceof Page) {\n            $bodyTermsMap = $this->generateTermScoreMapFromHtml($entity->html);\n        } else {\n            $bodyTermsMap = $this->generateTermScoreMapFromText($entity->getAttribute('description') ?? '', $entity->searchFactor);\n        }\n\n        $mergedScoreMap = $this->mergeTermScoreMaps($nameTermsMap, $bodyTermsMap, $tagTermsMap);\n\n        $dataArray = [];\n        $entityId = $entity->id;\n        $entityType = $entity->getMorphClass();\n        foreach ($mergedScoreMap as $term => $score) {\n            $dataArray[] = [\n                'term'        => $term,\n                'score'       => $score,\n                'entity_type' => $entityType,\n                'entity_id'   => $entityId,\n            ];\n        }\n\n        return $dataArray;\n    }\n\n    /**\n     * For the given term data arrays, Merge their contents by term\n     * while combining any scores.\n     *\n     * @param array<string, int>[] ...$scoreMaps\n     *\n     * @return array<string, int>\n     */\n    protected function mergeTermScoreMaps(...$scoreMaps): array\n    {\n        $mergedMap = [];\n\n        foreach ($scoreMaps as $scoreMap) {\n            foreach ($scoreMap as $term => $score) {\n                $mergedMap[$term] = ($mergedMap[$term] ?? 0) + $score;\n            }\n        }\n\n        return $mergedMap;\n    }\n}\n"
  },
  {
    "path": "app/Search/SearchOptionSet.php",
    "content": "<?php\n\nnamespace BookStack\\Search;\n\nuse BookStack\\Search\\Options\\SearchOption;\n\n/**\n * @template T of SearchOption\n */\nclass SearchOptionSet\n{\n    /**\n     * @var T[]\n     */\n    protected array $options = [];\n\n    /**\n     * @param T[] $options\n     */\n    public function __construct(array $options = [])\n    {\n        $this->options = $options;\n    }\n\n    public function toValueArray(): array\n    {\n        return array_map(fn(SearchOption $option) => $option->value, $this->options);\n    }\n\n    public function toValueMap(): array\n    {\n        $map = [];\n        foreach ($this->options as $index => $option) {\n            $key = $option->getKey() ?? $index;\n            $map[$key] = $option->value;\n        }\n        return $map;\n    }\n\n    public function merge(SearchOptionSet $set): self\n    {\n        return new self(array_merge($this->options, $set->options));\n    }\n\n    public function filterEmpty(): self\n    {\n        $filteredOptions = array_values(array_filter($this->options, fn (SearchOption $option) => !empty($option->value)));\n        return new self($filteredOptions);\n    }\n\n    /**\n     * @param class-string<SearchOption> $class\n     */\n    public static function fromValueArray(array $values, string $class): self\n    {\n        $options = array_map(fn($val) => new $class($val), $values);\n        return new self($options);\n    }\n\n    /**\n     * @return T[]\n     */\n    public function all(): array\n    {\n        return $this->options;\n    }\n\n    /**\n     * @return self<T>\n     */\n    public function negated(): self\n    {\n        $values = array_values(array_filter($this->options, fn (SearchOption $option) => $option->negated));\n        return new self($values);\n    }\n\n    /**\n     * @return self<T>\n     */\n    public function nonNegated(): self\n    {\n        $values = array_values(array_filter($this->options, fn (SearchOption $option) => !$option->negated));\n        return new self($values);\n    }\n\n    /**\n     * @return self<T>\n     */\n    public function limit(int $limit): self\n    {\n        return new self(array_slice(array_values($this->options), 0, $limit));\n    }\n}\n"
  },
  {
    "path": "app/Search/SearchOptions.php",
    "content": "<?php\n\nnamespace BookStack\\Search;\n\nuse BookStack\\Search\\Options\\ExactSearchOption;\nuse BookStack\\Search\\Options\\FilterSearchOption;\nuse BookStack\\Search\\Options\\SearchOption;\nuse BookStack\\Search\\Options\\TagSearchOption;\nuse BookStack\\Search\\Options\\TermSearchOption;\nuse Illuminate\\Http\\Request;\n\nclass SearchOptions\n{\n    /** @var SearchOptionSet<TermSearchOption> */\n    public SearchOptionSet $searches;\n    /** @var SearchOptionSet<ExactSearchOption> */\n    public SearchOptionSet $exacts;\n    /** @var SearchOptionSet<TagSearchOption> */\n    public SearchOptionSet $tags;\n    /** @var SearchOptionSet<FilterSearchOption> */\n    public SearchOptionSet $filters;\n\n    public function __construct()\n    {\n        $this->searches = new SearchOptionSet();\n        $this->exacts = new SearchOptionSet();\n        $this->tags = new SearchOptionSet();\n        $this->filters = new SearchOptionSet();\n    }\n\n    /**\n     * Create a new instance from a search string.\n     */\n    public static function fromString(string $search): self\n    {\n        $instance = new self();\n        $instance->addOptionsFromString($search);\n        $instance->limitOptions();\n        return $instance;\n    }\n\n    /**\n     * Create a new instance from a request.\n     * Will look for a classic string term and use that\n     * Otherwise we'll use the details from an advanced search form.\n     */\n    public static function fromRequest(Request $request): self\n    {\n        if (!$request->has('search') && !$request->has('term')) {\n            return static::fromString('');\n        }\n\n        if ($request->has('term')) {\n            return static::fromString($request->get('term'));\n        }\n\n        $instance = new SearchOptions();\n        $inputs = $request->only(['search', 'types', 'filters', 'exact', 'tags', 'extras']);\n\n        $parsedStandardTerms = static::parseStandardTermString($inputs['search'] ?? '');\n        $inputExacts = array_filter($inputs['exact'] ?? []);\n        $instance->searches = SearchOptionSet::fromValueArray(array_filter($parsedStandardTerms['terms']), TermSearchOption::class);\n        $instance->exacts = SearchOptionSet::fromValueArray(array_filter($parsedStandardTerms['exacts']), ExactSearchOption::class);\n        $instance->exacts = $instance->exacts->merge(SearchOptionSet::fromValueArray($inputExacts, ExactSearchOption::class));\n        $instance->tags = SearchOptionSet::fromValueArray(array_filter($inputs['tags'] ?? []), TagSearchOption::class);\n\n        $cleanedFilters = [];\n        foreach (($inputs['filters'] ?? []) as $filterKey => $filterVal) {\n            if (empty($filterVal)) {\n                continue;\n            }\n            $cleanedFilterVal = $filterVal === 'true' ? '' : $filterVal;\n            $cleanedFilters[] = new FilterSearchOption($cleanedFilterVal, $filterKey);\n        }\n\n        if (isset($inputs['types']) && count($inputs['types']) < 4) {\n            $cleanedFilters[] = new FilterSearchOption(implode('|', $inputs['types']), 'type');\n        }\n\n        $instance->filters = new SearchOptionSet($cleanedFilters);\n\n        // Parse and merge in extras if provided\n        if (!empty($inputs['extras'])) {\n            $extras = static::fromString($inputs['extras']);\n            $instance->searches = $instance->searches->merge($extras->searches);\n            $instance->exacts = $instance->exacts->merge($extras->exacts);\n            $instance->tags = $instance->tags->merge($extras->tags);\n            $instance->filters = $instance->filters->merge($extras->filters);\n        }\n\n        $instance->limitOptions();\n\n        return $instance;\n    }\n\n    /**\n     * Decode a search string and add its contents to this instance.\n     */\n    protected function addOptionsFromString(string $searchString): void\n    {\n        /** @var array<string, SearchOption[]> $terms */\n        $terms = [\n            'exacts'   => [],\n            'tags'     => [],\n            'filters'  => [],\n        ];\n\n        $patterns = [\n            'exacts'  => '/-?\"((?:\\\\\\\\.|[^\"\\\\\\\\])*)\"/',\n            'tags'    => '/-?\\[(.*?)\\]/',\n            'filters' => '/-?\\{(.*?)\\}/',\n        ];\n\n        $constructors = [\n            'exacts'   => fn(string $value, bool $negated) => new ExactSearchOption($value, $negated),\n            'tags'     => fn(string $value, bool $negated) => new TagSearchOption($value, $negated),\n            'filters'  => fn(string $value, bool $negated) => FilterSearchOption::fromContentString($value, $negated),\n        ];\n\n        // Parse special terms\n        foreach ($patterns as $termType => $pattern) {\n            $matches = [];\n            preg_match_all($pattern, $searchString, $matches);\n            if (count($matches) > 0) {\n                foreach ($matches[1] as $index => $value) {\n                    $negated = str_starts_with($matches[0][$index], '-');\n                    $terms[$termType][] = $constructors[$termType]($value, $negated);\n                }\n                $searchString = preg_replace($pattern, '', $searchString);\n            }\n        }\n\n        // Unescape exacts and backslash escapes\n        foreach ($terms['exacts'] as $exact) {\n            $exact->value = static::decodeEscapes($exact->value);\n        }\n\n        // Parse standard terms\n        $parsedStandardTerms = static::parseStandardTermString($searchString);\n        $this->searches = $this->searches\n            ->merge(SearchOptionSet::fromValueArray($parsedStandardTerms['terms'], TermSearchOption::class))\n            ->filterEmpty();\n        $this->exacts = $this->exacts\n            ->merge(new SearchOptionSet($terms['exacts']))\n            ->merge(SearchOptionSet::fromValueArray($parsedStandardTerms['exacts'], ExactSearchOption::class))\n            ->filterEmpty();\n\n        // Add tags & filters\n        $this->tags = $this->tags->merge(new SearchOptionSet($terms['tags']));\n        $this->filters = $this->filters->merge(new SearchOptionSet($terms['filters']));\n    }\n\n    /**\n     * Limit the amount of search options to reasonable levels.\n     * Provides higher limits to logged-in users since that signals a slightly\n     * higher level of trust.\n     */\n    protected function limitOptions(): void\n    {\n        $userLoggedIn = !user()->isGuest();\n        $searchLimit = $userLoggedIn ? 10 : 5;\n        $exactLimit = $userLoggedIn ? 4 : 2;\n        $tagLimit = $userLoggedIn ? 8 : 4;\n        $filterLimit = $userLoggedIn ? 10 : 5;\n\n        $this->searches = $this->searches->limit($searchLimit);\n        $this->exacts = $this->exacts->limit($exactLimit);\n        $this->tags = $this->tags->limit($tagLimit);\n        $this->filters = $this->filters->limit($filterLimit);\n    }\n\n    /**\n     * Decode backslash escaping within the input string.\n     */\n    protected static function decodeEscapes(string $input): string\n    {\n        $decoded = \"\";\n        $escaping = false;\n\n        foreach (str_split($input) as $char) {\n            if ($escaping) {\n                $decoded .= $char;\n                $escaping = false;\n            } else if ($char === '\\\\') {\n                $escaping = true;\n            } else {\n                $decoded .= $char;\n            }\n        }\n\n        return $decoded;\n    }\n\n    /**\n     * Parse a standard search term string into individual search terms and\n     * convert any required terms to exact matches. This is done since some\n     * characters will never be in the standard index, since we use them as\n     * delimiters, and therefore we convert a term to be exact if it\n     * contains one of those delimiter characters.\n     *\n     * @return array{terms: array<string>, exacts: array<string>}\n     */\n    protected static function parseStandardTermString(string $termString): array\n    {\n        $terms = explode(' ', $termString);\n        $indexDelimiters = implode('', array_diff(str_split(SearchIndex::$delimiters), str_split(SearchIndex::$softDelimiters)));\n        $parsed = [\n            'terms'  => [],\n            'exacts' => [],\n        ];\n\n        foreach ($terms as $searchTerm) {\n            if ($searchTerm === '') {\n                continue;\n            }\n\n            $becomeExact = (strpbrk($searchTerm, $indexDelimiters) !== false);\n            $parsed[$becomeExact ? 'exacts' : 'terms'][] = $searchTerm;\n        }\n\n        return $parsed;\n    }\n\n    /**\n     * Set the value of a specific filter in the search options.\n     */\n    public function setFilter(string $filterName, string $filterValue = ''): void\n    {\n        $this->filters = $this->filters->merge(\n            new SearchOptionSet([new FilterSearchOption($filterValue, $filterName)])\n        );\n    }\n\n    /**\n     * Encode this instance to a search string.\n     */\n    public function toString(): string\n    {\n        $options = [\n            ...$this->searches->all(),\n            ...$this->exacts->all(),\n            ...$this->tags->all(),\n            ...$this->filters->all(),\n        ];\n\n        $parts = array_map(fn(SearchOption $o) => $o->toString(), $options);\n\n        return implode(' ', $parts);\n    }\n\n    /**\n     * Get the search options that don't have UI controls provided for.\n     * Provided back as a key => value array with the keys being expected\n     * input names for a search form, and values being the option value.\n     */\n    public function getAdditionalOptionsString(): string\n    {\n        $options = [];\n\n        // Handle filters without UI support\n        $userFilters = ['updated_by', 'created_by', 'owned_by'];\n        $unsupportedFilters = ['is_template', 'sort_by'];\n        foreach ($this->filters->all() as $filter) {\n            if (in_array($filter->getKey(), $userFilters, true) && $filter->value !== null && $filter->value !== 'me') {\n                $options[] = $filter;\n            } else if (in_array($filter->getKey(), $unsupportedFilters, true)) {\n                $options[] = $filter;\n            }\n        }\n\n        // Negated items\n        array_push($options, ...$this->exacts->negated()->all());\n        array_push($options, ...$this->tags->negated()->all());\n        array_push($options, ...$this->filters->negated()->all());\n\n        return implode(' ', array_map(fn(SearchOption $o) => $o->toString(), $options));\n    }\n}\n"
  },
  {
    "path": "app/Search/SearchResultsFormatter.php",
    "content": "<?php\n\nnamespace BookStack\\Search;\n\nuse BookStack\\Activity\\Models\\Tag;\nuse BookStack\\Entities\\Models\\Entity;\nuse Illuminate\\Support\\HtmlString;\n\nclass SearchResultsFormatter\n{\n    /**\n     * For the given array of entities, Prepare the models to be shown in search result\n     * output. This sets a series of additional attributes.\n     *\n     * @param Entity[] $results\n     */\n    public function format(array $results, SearchOptions $options): void\n    {\n        foreach ($results as $result) {\n            $this->setSearchPreview($result, $options);\n        }\n    }\n\n    /**\n     * Update the given entity model to set attributes used for previews of the item\n     * primarily within search result lists.\n     */\n    protected function setSearchPreview(Entity $entity, SearchOptions $options): void\n    {\n        $textProperty = $entity->textField;\n        $textContent = $entity->$textProperty;\n        $relevantSearchOptions = $options->exacts->merge($options->searches);\n        $terms = $relevantSearchOptions->toValueArray();\n\n        $originalContentByNewAttribute = [\n            'preview_name'    => $entity->name,\n            'preview_content' => $textContent,\n        ];\n\n        foreach ($originalContentByNewAttribute as $attributeName => $content) {\n            $targetLength = ($attributeName === 'preview_name') ? 0 : 260;\n            $matchRefs = $this->getMatchPositions($content, $terms);\n            $mergedRefs = $this->sortAndMergeMatchPositions($matchRefs);\n            $formatted = $this->formatTextUsingMatchPositions($mergedRefs, $content, $targetLength);\n            $entity->setAttribute($attributeName, new HtmlString($formatted));\n        }\n\n        $tags = $entity->relationLoaded('tags') ? $entity->tags->all() : [];\n        $this->highlightTagsContainingTerms($tags, $terms);\n    }\n\n    /**\n     * Highlight tags which match the given terms.\n     *\n     * @param Tag[]    $tags\n     * @param string[] $terms\n     */\n    protected function highlightTagsContainingTerms(array $tags, array $terms): void\n    {\n        foreach ($tags as $tag) {\n            $tagName = mb_strtolower($tag->name);\n            $tagValue = mb_strtolower($tag->value);\n\n            foreach ($terms as $term) {\n                $termLower = mb_strtolower($term);\n\n                if (mb_strpos($tagName, $termLower) !== false) {\n                    $tag->setAttribute('highlight_name', true);\n                }\n\n                if (mb_strpos($tagValue, $termLower) !== false) {\n                    $tag->setAttribute('highlight_value', true);\n                }\n            }\n        }\n    }\n\n    /**\n     * Get positions of the given terms within the given text.\n     * Is in the array format of [int $startIndex => int $endIndex] where the indexes\n     * are positions within the provided text.\n     *\n     * @return array<int, int>\n     */\n    protected function getMatchPositions(string $text, array $terms): array\n    {\n        $matchRefs = [];\n        $text = mb_strtolower($text);\n\n        foreach ($terms as $term) {\n            $offset = 0;\n            $term = mb_strtolower($term);\n            $pos = mb_strpos($text, $term, $offset);\n            while ($pos !== false && count($matchRefs) < 25) {\n                $end = $pos + mb_strlen($term);\n                $matchRefs[$pos] = $end;\n                $offset = $end;\n                $pos = mb_strpos($text, $term, $offset);\n            }\n        }\n\n        return $matchRefs;\n    }\n\n    /**\n     * Sort the given match positions before merging them where they're\n     * adjacent or where they overlap.\n     *\n     * @param array<int, int> $matchPositions\n     *\n     * @return array<int, int>\n     */\n    protected function sortAndMergeMatchPositions(array $matchPositions): array\n    {\n        ksort($matchPositions);\n        $mergedRefs = [];\n        $lastStart = 0;\n        $lastEnd = 0;\n\n        foreach ($matchPositions as $start => $end) {\n            if ($start > $lastEnd) {\n                $mergedRefs[$start] = $end;\n                $lastStart = $start;\n                $lastEnd = $end;\n            } elseif ($end > $lastEnd) {\n                $mergedRefs[$lastStart] = $end;\n                $lastEnd = $end;\n            }\n        }\n\n        return $mergedRefs;\n    }\n\n    /**\n     * Format the given original text, returning a version where terms are highlighted within.\n     * Returned content is in HTML text format.\n     * A given $targetLength of 0 asserts no target length limit.\n     *\n     * This is a complex function but written to be relatively efficient, going through the term matches in order\n     * so that we're only doing a one-time loop through of the matches. There is no further searching\n     * done within here.\n     */\n    protected function formatTextUsingMatchPositions(array $matchPositions, string $originalText, int $targetLength): string\n    {\n        $maxEnd = mb_strlen($originalText);\n        $fetchAll = ($targetLength === 0);\n        $contextLength = ($fetchAll ? 0 : 32);\n\n        $firstStart = null;\n        $lastEnd = 0;\n        $content = '';\n        $contentTextLength = 0;\n\n        if ($fetchAll) {\n            $targetLength = $maxEnd * 2;\n        }\n\n        foreach ($matchPositions as $start => $end) {\n            // Get our outer text ranges for the added context we want to show upon the result.\n            $contextStart = max($start - $contextLength, 0, $lastEnd);\n            $contextEnd = min($end + $contextLength, $maxEnd);\n\n            // Adjust the start if we're going to be touching the previous match.\n            $startDiff = $start - $lastEnd;\n            if ($startDiff < 0) {\n                $contextStart = $start;\n                // Trims off '$startDiff' number of characters to bring it back to the start\n                // if this current match zone.\n                $content = mb_substr($content, 0, mb_strlen($content) + $startDiff);\n                $contentTextLength += $startDiff;\n            }\n\n            // Add ellipsis between results\n            if (!$fetchAll && $contextStart !== 0 && $contextStart !== $start) {\n                $content .= ' ...';\n                $contentTextLength += 4;\n            } elseif ($fetchAll) {\n                // Or fill in gap since the previous match\n                $fillLength = $contextStart - $lastEnd;\n                $content .= e(mb_substr($originalText, $lastEnd, $fillLength));\n                $contentTextLength += $fillLength;\n            }\n\n            // Add our content including the bolded matching text\n            $content .= e(mb_substr($originalText, $contextStart, $start - $contextStart));\n            $contentTextLength += $start - $contextStart;\n            $content .= '<strong>' . e(mb_substr($originalText, $start, $end - $start)) . '</strong>';\n            $contentTextLength += $end - $start;\n            $content .= e(mb_substr($originalText, $end, $contextEnd - $end));\n            $contentTextLength += $contextEnd - $end;\n\n            // Update our last end position\n            $lastEnd = $contextEnd;\n\n            // Update the first start position if it's not already been set\n            if (is_null($firstStart)) {\n                $firstStart = $contextStart;\n            }\n\n            // Stop if we're near our target\n            if ($contentTextLength >= $targetLength - 10) {\n                break;\n            }\n        }\n\n        // Just copy out the content if we haven't moved along anywhere.\n        if ($lastEnd === 0) {\n            $content = e(mb_substr($originalText, 0, $targetLength));\n            $contentTextLength = $targetLength;\n            $lastEnd = $targetLength;\n        }\n\n        // Pad out the end if we're low\n        $remainder = $targetLength - $contentTextLength;\n        if ($remainder > 10) {\n            $padEndLength = min($maxEnd - $lastEnd, $remainder);\n            $content .= e(mb_substr($originalText, $lastEnd, $padEndLength));\n            $lastEnd += $padEndLength;\n            $contentTextLength += $padEndLength;\n        }\n\n        // Pad out the start if we're still low\n        $remainder = $targetLength - $contentTextLength;\n        $firstStart = $firstStart ?: 0;\n        if (!$fetchAll && $remainder > 10 && $firstStart !== 0) {\n            $padStart = max(0, $firstStart - $remainder);\n            $content = ($padStart === 0 ? '' : '...') . e(mb_substr($originalText, $padStart, $firstStart - $padStart)) . mb_substr($content, 4);\n        }\n\n        // Add ellipsis if we're not at the end\n        if ($lastEnd < $maxEnd) {\n            $content .= '...';\n        }\n\n        return $content;\n    }\n}\n"
  },
  {
    "path": "app/Search/SearchRunner.php",
    "content": "<?php\n\nnamespace BookStack\\Search;\n\nuse BookStack\\Entities\\EntityProvider;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse BookStack\\Entities\\Tools\\EntityHydrator;\nuse BookStack\\Permissions\\PermissionApplicator;\nuse BookStack\\Search\\Options\\TagSearchOption;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Connection;\nuse Illuminate\\Database\\Eloquent\\Builder as EloquentBuilder;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Query\\JoinClause;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Str;\nuse WeakMap;\n\nclass SearchRunner\n{\n    /**\n     * Retain a cache of score-adjusted terms for specific search options.\n     */\n    protected WeakMap $termAdjustmentCache;\n\n    public function __construct(\n        protected EntityProvider $entityProvider,\n        protected PermissionApplicator $permissions,\n        protected EntityQueries $entityQueries,\n        protected EntityHydrator $entityHydrator,\n    ) {\n        $this->termAdjustmentCache = new WeakMap();\n    }\n\n    /**\n     * Search all entities in the system.\n     *\n     * @return array{total: int, results: Collection<Entity>}\n     */\n    public function searchEntities(SearchOptions $searchOpts, string $entityType = 'all', int $page = 1, int $count = 20): array\n    {\n        $entityTypes = array_keys($this->entityProvider->all());\n        $entityTypesToSearch = $entityTypes;\n\n        $filterMap = $searchOpts->filters->toValueMap();\n        if ($entityType !== 'all') {\n            $entityTypesToSearch = [$entityType];\n        } elseif (isset($filterMap['type'])) {\n            $entityTypesToSearch = explode('|', $filterMap['type']);\n        }\n\n        $searchQuery = $this->buildQuery($searchOpts, $entityTypesToSearch);\n        $total = $searchQuery->count();\n        $results = $this->getPageOfDataFromQuery($searchQuery, $page, $count);\n\n        return [\n            'total'    => $total,\n            'results'  => $results->values(),\n        ];\n    }\n\n    /**\n     * Search a book for entities.\n     */\n    public function searchBook(int $bookId, string $searchString): Collection\n    {\n        $opts = SearchOptions::fromString($searchString);\n        $entityTypes = ['page', 'chapter'];\n        $filterMap = $opts->filters->toValueMap();\n        $entityTypesToSearch = isset($filterMap['type']) ? explode('|', $filterMap['type']) : $entityTypes;\n\n        $filteredTypes = array_intersect($entityTypesToSearch, $entityTypes);\n        $query = $this->buildQuery($opts, $filteredTypes)->where('book_id', '=', $bookId);\n\n        return $this->getPageOfDataFromQuery($query, 1, 20)->sortByDesc('score');\n    }\n\n    /**\n     * Search a chapter for entities.\n     */\n    public function searchChapter(int $chapterId, string $searchString): Collection\n    {\n        $opts = SearchOptions::fromString($searchString);\n        $query = $this->buildQuery($opts, ['page'])->where('chapter_id', '=', $chapterId);\n\n        return $this->getPageOfDataFromQuery($query, 1, 20)->sortByDesc('score');\n    }\n\n    /**\n     * Get a page of result data from the given query based on the provided page parameters.\n     */\n    protected function getPageOfDataFromQuery(EloquentBuilder $query, int $page, int $count): Collection\n    {\n        $entities = $query->clone()\n            ->skip(($page - 1) * $count)\n            ->take($count)\n            ->get();\n\n        $hydrated = $this->entityHydrator->hydrate($entities->all(), true, true);\n\n        return collect($hydrated);\n    }\n\n    /**\n     * Create a search query for an entity.\n     * @param string[] $entityTypes\n     */\n    protected function buildQuery(SearchOptions $searchOpts, array $entityTypes): EloquentBuilder\n    {\n        $entityQuery = $this->entityQueries->visibleForList()\n            ->whereIn('type', $entityTypes);\n\n        // Handle normal search terms\n        $this->applyTermSearch($entityQuery, $searchOpts, $entityTypes);\n\n        // Handle exact term matching\n        foreach ($searchOpts->exacts->all() as $exact) {\n            $filter = function (EloquentBuilder $query) use ($exact) {\n                $inputTerm = str_replace('\\\\', '\\\\\\\\', $exact->value);\n                $query->where('name', 'like', '%' . $inputTerm . '%')\n                    ->orWhere('description', 'like', '%' . $inputTerm . '%')\n                    ->orWhere('text', 'like', '%' . $inputTerm . '%');\n            };\n\n            $exact->negated ? $entityQuery->whereNot($filter) : $entityQuery->where($filter);\n        }\n\n        // Handle tag searches\n        foreach ($searchOpts->tags->all() as $tagOption) {\n            $this->applyTagSearch($entityQuery, $tagOption);\n        }\n\n        // Handle filters\n        foreach ($searchOpts->filters->all() as $filterOption) {\n            $functionName = Str::camel('filter_' . $filterOption->getKey());\n            if (method_exists($this, $functionName)) {\n                $this->$functionName($entityQuery, $filterOption->value, $filterOption->negated);\n            }\n        }\n\n        return $entityQuery;\n    }\n\n    /**\n     * For the given search query, apply the queries for handling the regular search terms.\n     */\n    protected function applyTermSearch(EloquentBuilder $entityQuery, SearchOptions $options, array $entityTypes): void\n    {\n        $terms = $options->searches->toValueArray();\n        if (count($terms) === 0) {\n            return;\n        }\n\n        $scoredTerms = $this->getTermAdjustments($options);\n        $scoreSelect = $this->selectForScoredTerms($scoredTerms);\n\n        $subQuery = DB::table('search_terms')->select([\n            'entity_id',\n            'entity_type',\n            DB::raw($scoreSelect['statement']),\n        ]);\n\n        $subQuery->addBinding($scoreSelect['bindings'], 'select');\n        $subQuery->where(function (Builder $query) use ($terms) {\n            foreach ($terms as $inputTerm) {\n                $escapedTerm = str_replace('\\\\', '\\\\\\\\', $inputTerm);\n                $query->orWhere('term', 'like', $escapedTerm . '%');\n            }\n        });\n        $subQuery->groupBy('entity_type', 'entity_id');\n\n        $entityQuery->joinSub($subQuery, 's', function (JoinClause $join) {\n            $join->on('s.entity_id', '=', 'entities.id')\n                ->on('s.entity_type', '=', 'entities.type');\n        });\n        $entityQuery->addSelect('s.score');\n        $entityQuery->orderBy('score', 'desc');\n    }\n\n    /**\n     * Create a select statement, with prepared bindings, for the given\n     * set of scored search terms.\n     *\n     * @param array<string, float> $scoredTerms\n     *\n     * @return array{statement: string, bindings: string[]}\n     */\n    protected function selectForScoredTerms(array $scoredTerms): array\n    {\n        // Within this we walk backwards to create the chain of 'if' statements\n        // so that each previous statement is used in the 'else' condition of\n        // the next (earlier) to be built. We start at '0' to have no score\n        // on no match (Should never actually get to this case).\n        $ifChain = '0';\n        $bindings = [];\n        foreach ($scoredTerms as $term => $score) {\n            $ifChain = 'IF(term like ?, score * ' . (float) $score . ', ' . $ifChain . ')';\n            $bindings[] = $term . '%';\n        }\n\n        return [\n            'statement' => 'SUM(' . $ifChain . ') as score',\n            'bindings'  => array_reverse($bindings),\n        ];\n    }\n\n    /**\n     * For the terms in the given search options, query their popularity across all\n     * search terms then provide that back as score adjustment multiplier applicable\n     * for their rarity. Returns an array of float multipliers, keyed by term.\n     *\n     * @return array<string, float>\n     */\n    protected function getTermAdjustments(SearchOptions $options): array\n    {\n        if (isset($this->termAdjustmentCache[$options])) {\n            return $this->termAdjustmentCache[$options];\n        }\n\n        $termQuery = SearchTerm::query()->toBase();\n        $whenStatements = [];\n        $whenBindings = [];\n\n        foreach ($options->searches->toValueArray() as $term) {\n            $whenStatements[] = 'WHEN term LIKE ? THEN ?';\n            $whenBindings[] = $term . '%';\n            $whenBindings[] = $term;\n\n            $termQuery->orWhere('term', 'like', $term . '%');\n        }\n\n        $case = 'CASE ' . implode(' ', $whenStatements) . ' END';\n        $termQuery->selectRaw($case . ' as term', $whenBindings);\n        $termQuery->selectRaw('COUNT(*) as count');\n        $termQuery->groupByRaw($case, $whenBindings);\n\n        $termCounts = $termQuery->pluck('count', 'term')->toArray();\n        $adjusted = $this->rawTermCountsToAdjustments($termCounts);\n\n        $this->termAdjustmentCache[$options] = $adjusted;\n\n        return $this->termAdjustmentCache[$options];\n    }\n\n    /**\n     * Convert counts of terms into a relative-count normalised multiplier.\n     *\n     * @param array<string, int> $termCounts\n     *\n     * @return array<string, float>\n     */\n    protected function rawTermCountsToAdjustments(array $termCounts): array\n    {\n        if (empty($termCounts)) {\n            return [];\n        }\n\n        $multipliers = [];\n        $max = max(array_values($termCounts));\n\n        foreach ($termCounts as $term => $count) {\n            $percent = round($count / $max, 5);\n            $multipliers[$term] = 1.3 - $percent;\n        }\n\n        return $multipliers;\n    }\n\n    /**\n     * Apply a tag search term onto an entity query.\n     */\n    protected function applyTagSearch(EloquentBuilder $query, TagSearchOption $option): void\n    {\n        $filter = function (EloquentBuilder $query) use ($option): void {\n            $tagParts = $option->getParts();\n            if (empty($tagParts['operator']) || empty($tagParts['value'])) {\n                $query->where('name', '=', $tagParts['name']);\n                return;\n            }\n\n            if (!empty($tagParts['name'])) {\n                $query->where('name', '=', $tagParts['name']);\n            }\n\n            if (is_numeric($tagParts['value']) && $tagParts['operator'] !== 'like') {\n                // We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will\n                // search the value as a string which prevents being able to do number-based operations\n                // on the tag values. We ensure it has a numeric value and then cast it just to be sure.\n                /** @var Connection $connection */\n                $connection = $query->getConnection();\n                $quotedValue = (float) trim($connection->getPdo()->quote($tagParts['value']), \"'\");\n                $query->whereRaw(\"value {$tagParts['operator']} {$quotedValue}\");\n            } else if ($tagParts['operator'] === 'like') {\n                $query->where('value', $tagParts['operator'], str_replace('\\\\', '\\\\\\\\', $tagParts['value']));\n            } else {\n                $query->where('value', $tagParts['operator'], $tagParts['value']);\n            }\n        };\n\n        $option->negated ? $query->whereDoesntHave('tags', $filter) : $query->whereHas('tags', $filter);\n    }\n\n    protected function applyNegatableWhere(EloquentBuilder $query, bool $negated, string|callable $column, string|null $operator, mixed $value): void\n    {\n        if ($negated) {\n            $query->whereNot($column, $operator, $value);\n        } else {\n            $query->where($column, $operator, $value);\n        }\n    }\n\n    /**\n     * Custom entity search filters.\n     */\n    protected function filterUpdatedAfter(EloquentBuilder $query, string $input, bool $negated): void\n    {\n        $date = date_create($input);\n        $this->applyNegatableWhere($query, $negated, 'updated_at', '>=', $date);\n    }\n\n    protected function filterUpdatedBefore(EloquentBuilder $query, string $input, bool $negated): void\n    {\n        $date = date_create($input);\n        $this->applyNegatableWhere($query, $negated, 'updated_at', '<', $date);\n    }\n\n    protected function filterCreatedAfter(EloquentBuilder $query, string $input, bool $negated): void\n    {\n        $date = date_create($input);\n        $this->applyNegatableWhere($query, $negated, 'created_at', '>=', $date);\n    }\n\n    protected function filterCreatedBefore(EloquentBuilder $query, string $input, bool $negated)\n    {\n        $date = date_create($input);\n        $this->applyNegatableWhere($query, $negated, 'created_at', '<', $date);\n    }\n\n    protected function filterCreatedBy(EloquentBuilder $query, string $input, bool $negated)\n    {\n        $userSlug = $input === 'me' ? user()->slug : trim($input);\n        $user = User::query()->where('slug', '=', $userSlug)->first(['id']);\n        if ($user) {\n            $this->applyNegatableWhere($query, $negated, 'created_by', '=', $user->id);\n        }\n    }\n\n    protected function filterUpdatedBy(EloquentBuilder $query, string $input, bool $negated)\n    {\n        $userSlug = $input === 'me' ? user()->slug : trim($input);\n        $user = User::query()->where('slug', '=', $userSlug)->first(['id']);\n        if ($user) {\n            $this->applyNegatableWhere($query, $negated, 'updated_by', '=', $user->id);\n        }\n    }\n\n    protected function filterOwnedBy(EloquentBuilder $query, string $input, bool $negated)\n    {\n        $userSlug = $input === 'me' ? user()->slug : trim($input);\n        $user = User::query()->where('slug', '=', $userSlug)->first(['id']);\n        if ($user) {\n            $this->applyNegatableWhere($query, $negated, 'owned_by', '=', $user->id);\n        }\n    }\n\n    protected function filterInName(EloquentBuilder $query, string $input, bool $negated)\n    {\n        $this->applyNegatableWhere($query, $negated, 'name', 'like', '%' . $input . '%');\n    }\n\n    protected function filterInTitle(EloquentBuilder $query, string $input, bool $negated)\n    {\n        $this->filterInName($query, $input, $negated);\n    }\n\n    protected function filterInBody(EloquentBuilder $query, string $input, bool $negated)\n    {\n        $this->applyNegatableWhere($query, $negated, function (EloquentBuilder $query) use ($input) {\n            $query->where('description', 'like', '%' . $input . '%')\n                ->orWhere('text', 'like', '%' . $input . '%');\n        }, null, null);\n    }\n\n    protected function filterIsRestricted(EloquentBuilder $query, string $input, bool $negated)\n    {\n        $negated ? $query->whereDoesntHave('permissions') : $query->whereHas('permissions');\n    }\n\n    protected function filterViewedByMe(EloquentBuilder $query, string $input, bool $negated)\n    {\n        $filter = function ($query) {\n            $query->where('user_id', '=', user()->id);\n        };\n\n        $negated ? $query->whereDoesntHave('views', $filter) : $query->whereHas('views', $filter);\n    }\n\n    protected function filterNotViewedByMe(EloquentBuilder $query, string $input, bool $negated)\n    {\n        $filter = function ($query) {\n            $query->where('user_id', '=', user()->id);\n        };\n\n        $negated ? $query->whereHas('views', $filter) : $query->whereDoesntHave('views', $filter);\n    }\n\n    protected function filterIsTemplate(EloquentBuilder $query, string $input, bool $negated)\n    {\n        $this->applyNegatableWhere($query, $negated, 'template', '=', true);\n    }\n\n    protected function filterSortBy(EloquentBuilder $query, string $input, bool $negated)\n    {\n        $functionName = Str::camel('sort_by_' . $input);\n        if (method_exists($this, $functionName)) {\n            $this->$functionName($query, $negated);\n        }\n    }\n\n    /**\n     * Sorting filter options.\n     */\n    protected function sortByLastCommented(EloquentBuilder $query, bool $negated)\n    {\n        $commentsTable = DB::getTablePrefix() . 'comments';\n        $commentQuery = DB::raw('(SELECT c1.commentable_id, c1.commentable_type, c1.created_at as last_commented FROM ' . $commentsTable . ' c1 LEFT JOIN ' . $commentsTable . ' c2 ON (c1.commentable_id = c2.commentable_id AND c1.commentable_type = c2.commentable_type AND c1.created_at < c2.created_at) WHERE c2.created_at IS NULL) as comments');\n\n        $query->join($commentQuery, function (JoinClause $join) {\n            $join->on('entities.id', '=', 'comments.commentable_id')\n                ->on('entities.type', '=', 'comments.commentable_type');\n        })->orderBy('last_commented', $negated ? 'asc' : 'desc');\n    }\n}\n"
  },
  {
    "path": "app/Search/SearchTerm.php",
    "content": "<?php\n\nnamespace BookStack\\Search;\n\nuse BookStack\\App\\Model;\n\nclass SearchTerm extends Model\n{\n    protected $fillable = ['term', 'entity_id', 'entity_type', 'score'];\n    public $timestamps = false;\n\n    /**\n     * Get the entity that this term belongs to.\n     *\n     * @return \\Illuminate\\Database\\Eloquent\\Relations\\MorphTo\n     */\n    public function entity()\n    {\n        return $this->morphTo('entity');\n    }\n}\n"
  },
  {
    "path": "app/Search/SearchTextTokenizer.php",
    "content": "<?php\n\nnamespace BookStack\\Search;\n\n/**\n * A custom text tokenizer which records & provides insight needed for our search indexing.\n * We used to use basic strtok() but this class does the following which that lacked:\n * - Tracks and provides the current/previous delimiter that we've stopped at.\n * - Returns empty tokens upon parsing a delimiter.\n */\nclass SearchTextTokenizer\n{\n    protected int $currentIndex = 0;\n    protected int $length;\n    protected string $currentDelimiter = '';\n    protected string $previousDelimiter = '';\n\n    public function __construct(\n        protected string $text,\n        protected string $delimiters = ' '\n    ) {\n        $this->length = strlen($this->text);\n    }\n\n    /**\n     * Get the current delimiter to be found.\n     */\n    public function currentDelimiter(): string\n    {\n        return $this->currentDelimiter;\n    }\n\n    /**\n     * Get the previous delimiter found.\n     */\n    public function previousDelimiter(): string\n    {\n        return $this->previousDelimiter;\n    }\n\n    /**\n     * Get the next token between delimiters.\n     * Returns false if there's no further tokens.\n     */\n    public function next(): string|false\n    {\n        $token = '';\n\n        for ($i = $this->currentIndex; $i < $this->length; $i++) {\n            $char = $this->text[$i];\n            if (str_contains($this->delimiters, $char)) {\n                $this->previousDelimiter = $this->currentDelimiter;\n                $this->currentDelimiter = $char;\n                $this->currentIndex = $i + 1;\n                return $token;\n            }\n\n            $token .= $char;\n        }\n\n        if ($token) {\n            $this->currentIndex = $this->length;\n            $this->previousDelimiter = $this->currentDelimiter;\n            $this->currentDelimiter = '';\n            return $token;\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/Settings/AppSettingsStore.php",
    "content": "<?php\n\nnamespace BookStack\\Settings;\n\nuse BookStack\\Uploads\\FaviconHandler;\nuse BookStack\\Uploads\\ImageRepo;\nuse Illuminate\\Http\\Request;\n\nclass AppSettingsStore\n{\n    public function __construct(\n        protected ImageRepo $imageRepo,\n        protected FaviconHandler $faviconHandler,\n    ) {\n    }\n\n    public function storeFromUpdateRequest(Request $request, string $category): void\n    {\n        $this->storeSimpleSettings($request);\n        if ($category === 'customization') {\n            $this->updateAppLogo($request);\n            $this->updateAppIcon($request);\n        }\n    }\n\n    protected function updateAppIcon(Request $request): void\n    {\n        $sizes = [180, 128, 64, 32];\n\n        // Update icon image if set\n        if ($request->hasFile('app_icon')) {\n            $iconFile = $request->file('app_icon');\n            $this->destroyExistingSettingImage('app-icon');\n            $image = $this->imageRepo->saveNew($iconFile, 'system', 0, 256, 256);\n            setting()->put('app-icon', $image->url);\n\n            foreach ($sizes as $size) {\n                $this->destroyExistingSettingImage('app-icon-' . $size);\n                $icon = $this->imageRepo->saveNew($iconFile, 'system', 0, $size, $size);\n                setting()->put('app-icon-' . $size, $icon->url);\n            }\n\n            $this->faviconHandler->saveForUploadedImage($iconFile);\n        }\n\n        // Clear icon image if requested\n        if ($request->get('app_icon_reset')) {\n            $this->destroyExistingSettingImage('app-icon');\n            setting()->remove('app-icon');\n            foreach ($sizes as $size) {\n                $this->destroyExistingSettingImage('app-icon-' . $size);\n                setting()->remove('app-icon-' . $size);\n            }\n\n            $this->faviconHandler->restoreOriginal();\n        }\n    }\n\n    protected function updateAppLogo(Request $request): void\n    {\n        // Update logo image if set\n        if ($request->hasFile('app_logo')) {\n            $logoFile = $request->file('app_logo');\n            $this->destroyExistingSettingImage('app-logo');\n            $image = $this->imageRepo->saveNew($logoFile, 'system', 0, null, 86);\n            setting()->put('app-logo', $image->url);\n        }\n\n        // Clear logo image if requested\n        if ($request->get('app_logo_reset')) {\n            $this->destroyExistingSettingImage('app-logo');\n            setting()->remove('app-logo');\n        }\n    }\n\n    protected function storeSimpleSettings(Request $request): void\n    {\n        foreach ($request->all() as $name => $value) {\n            if (!str_starts_with($name, 'setting-')) {\n                continue;\n            }\n\n            $key = str_replace('setting-', '', trim($name));\n            setting()->put($key, $value);\n        }\n    }\n\n    protected function destroyExistingSettingImage(string $settingKey): void\n    {\n        $existingVal = setting()->get($settingKey);\n        if ($existingVal) {\n            $this->imageRepo->destroyByUrlAndType($existingVal, 'system');\n        }\n    }\n}\n"
  },
  {
    "path": "app/Settings/MaintenanceController.php",
    "content": "<?php\n\nnamespace BookStack\\Settings;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\App\\AppVersion;\nuse BookStack\\Entities\\Tools\\TrashCan;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\References\\ReferenceStore;\nuse BookStack\\Uploads\\ImageService;\nuse Illuminate\\Http\\Request;\n\nclass MaintenanceController extends Controller\n{\n    /**\n     * Show the page for application maintenance.\n     */\n    public function index(TrashCan $trashCan)\n    {\n        $this->checkPermission(Permission::SettingsManage);\n        $this->setPageTitle(trans('settings.maint'));\n\n        // Recycle bin details\n        $recycleStats = $trashCan->getTrashedCounts();\n\n        return view('settings.maintenance', [\n            'version'      => AppVersion::get(),\n            'recycleStats' => $recycleStats,\n        ]);\n    }\n\n    /**\n     * Action to clean-up images in the system.\n     */\n    public function cleanupImages(Request $request, ImageService $imageService)\n    {\n        $this->checkPermission(Permission::SettingsManage);\n        $this->logActivity(ActivityType::MAINTENANCE_ACTION_RUN, 'cleanup-images');\n\n        $checkRevisions = !($request->get('ignore_revisions', 'false') === 'true');\n        $dryRun = !($request->has('confirm'));\n\n        $imagesToDelete = $imageService->deleteUnusedImages($checkRevisions, $dryRun);\n        $deleteCount = count($imagesToDelete);\n        if ($deleteCount === 0) {\n            $this->showWarningNotification(trans('settings.maint_image_cleanup_nothing_found'));\n\n            return redirect('/settings/maintenance')->withInput();\n        }\n\n        if ($dryRun) {\n            session()->flash('cleanup-images-warning', trans('settings.maint_image_cleanup_warning', ['count' => $deleteCount]));\n        } else {\n            $this->showSuccessNotification(trans('settings.maint_image_cleanup_success', ['count' => $deleteCount]));\n        }\n\n        return redirect('/settings/maintenance#image-cleanup')->withInput();\n    }\n\n    /**\n     * Action to send a test e-mail to the current user.\n     */\n    public function sendTestEmail()\n    {\n        $this->checkPermission(Permission::SettingsManage);\n        $this->logActivity(ActivityType::MAINTENANCE_ACTION_RUN, 'send-test-email');\n\n        try {\n            user()->notifyNow(new TestEmailNotification());\n            $this->showSuccessNotification(trans('settings.maint_send_test_email_success', ['address' => user()->email]));\n        } catch (\\Exception $exception) {\n            $errorMessage = trans('errors.maintenance_test_email_failure') . \"\\n\" . $exception->getMessage();\n            $this->showErrorNotification($errorMessage);\n        }\n\n        return redirect('/settings/maintenance#image-cleanup');\n    }\n\n    /**\n     * Action to regenerate the reference index in the system.\n     */\n    public function regenerateReferences(ReferenceStore $referenceStore)\n    {\n        $this->checkPermission(Permission::SettingsManage);\n        $this->logActivity(ActivityType::MAINTENANCE_ACTION_RUN, 'regenerate-references');\n\n        try {\n            $referenceStore->updateForAll();\n            $this->showSuccessNotification(trans('settings.maint_regen_references_success'));\n        } catch (\\Exception $exception) {\n            $this->showErrorNotification($exception->getMessage());\n        }\n\n        return redirect('/settings/maintenance#regenerate-references');\n    }\n}\n"
  },
  {
    "path": "app/Settings/Setting.php",
    "content": "<?php\n\nnamespace BookStack\\Settings;\n\nuse BookStack\\App\\Model;\n\nclass Setting extends Model\n{\n    protected $fillable = ['setting_key', 'value'];\n\n    protected $primaryKey = 'setting_key';\n}\n"
  },
  {
    "path": "app/Settings/SettingController.php",
    "content": "<?php\n\nnamespace BookStack\\Settings;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\App\\AppVersion;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Http\\Request;\n\nclass SettingController extends Controller\n{\n    /**\n     * Handle requests to the settings index path.\n     */\n    public function index()\n    {\n        return redirect('/settings/features');\n    }\n\n    /**\n     * Display the settings for the given category.\n     */\n    public function category(string $category)\n    {\n        $this->ensureCategoryExists($category);\n        $this->checkPermission(Permission::SettingsManage);\n        $this->setPageTitle(trans('settings.settings'));\n\n        return view('settings.categories.' . $category, [\n            'category'  => $category,\n            'version'   => AppVersion::get(),\n            'guestUser' => User::getGuest(),\n        ]);\n    }\n\n    /**\n     * Update the specified settings in storage.\n     */\n    public function update(Request $request, AppSettingsStore $store, string $category)\n    {\n        $this->ensureCategoryExists($category);\n        $this->preventAccessInDemoMode();\n        $this->checkPermission(Permission::SettingsManage);\n        $this->validate($request, [\n            'app_logo' => ['nullable', ...$this->getImageValidationRules()],\n            'app_icon' => ['nullable', ...$this->getImageValidationRules()],\n        ]);\n\n        $store->storeFromUpdateRequest($request, $category);\n        $this->logActivity(ActivityType::SETTINGS_UPDATE, $category);\n\n        return redirect(\"/settings/{$category}\");\n    }\n\n    protected function ensureCategoryExists(string $category): void\n    {\n        if (!view()->exists('settings.categories.' . $category)) {\n            abort(404);\n        }\n    }\n}\n"
  },
  {
    "path": "app/Settings/SettingService.php",
    "content": "<?php\n\nnamespace BookStack\\Settings;\n\nuse BookStack\\Users\\Models\\User;\n\n/**\n * Class SettingService\n * The settings are a simple key-value database store.\n * For non-authenticated users, user settings are stored via the session instead.\n * A local array-based cache is used to for setting accesses across a request.\n */\nclass SettingService\n{\n    protected array $localCache = [];\n\n    /**\n     * Gets a setting from the database,\n     * If not found, Returns default, Which is false by default.\n     */\n    public function get(string $key, $default = null): mixed\n    {\n        if (is_null($default)) {\n            $default = config('setting-defaults.' . $key, false);\n        }\n\n        $value = $this->getValueFromStore($key) ?? $default;\n        return $this->formatValue($value, $default);\n    }\n\n    /**\n     * Get a setting from the database as an integer.\n     * Returns the default value if not found or not an integer, and clamps the value to the given min/max range.\n     */\n    public function getInteger(string $key, int $default, int $min = 0, int $max = PHP_INT_MAX): int\n    {\n        $value = $this->get($key, $default);\n        if (!is_numeric($value)) {\n            return $default;\n        }\n\n        $int = intval($value);\n        return max($min, min($max, $int));\n    }\n\n    /**\n     * Get a value from the session instead of the main store option.\n     */\n    protected function getFromSession(string $key, $default = false)\n    {\n        $value = session()->get($key, $default);\n\n        return $this->formatValue($value, $default);\n    }\n\n    /**\n     * Get a user-specific setting from the database or cache.\n     */\n    public function getUser(User $user, string $key, $default = null)\n    {\n        if (is_null($default)) {\n            $default = config('setting-defaults.user.' . $key, false);\n        }\n\n        if ($user->isGuest()) {\n            return $this->getFromSession($key, $default);\n        }\n\n        return $this->get($this->userKey($user->id, $key), $default);\n    }\n\n    /**\n     * Get a value for the current logged-in user.\n     */\n    public function getForCurrentUser(string $key, $default = null)\n    {\n        return $this->getUser(user(), $key, $default);\n    }\n\n    /**\n     * Gets a setting value from the local cache.\n     * Will load the local cache if not previously loaded.\n     */\n    protected function getValueFromStore(string $key): mixed\n    {\n        $cacheCategory = $this->localCacheCategory($key);\n        if (!isset($this->localCache[$cacheCategory])) {\n            $this->loadToLocalCache($cacheCategory);\n        }\n\n        return $this->localCache[$cacheCategory][$key] ?? null;\n    }\n\n    /**\n     * Put the given value into the local cached under the given key.\n     */\n    protected function putValueIntoLocalCache(string $key, mixed $value): void\n    {\n        $cacheCategory = $this->localCacheCategory($key);\n        if (!isset($this->localCache[$cacheCategory])) {\n            $this->loadToLocalCache($cacheCategory);\n        }\n\n        $this->localCache[$cacheCategory][$key] = $value;\n    }\n\n    /**\n     * Get the category for the given setting key.\n     * Will return 'app' for a general app setting otherwise 'user:<user_id>' for a user setting.\n     */\n    protected function localCacheCategory(string $key): string\n    {\n        if (str_starts_with($key, 'user:')) {\n            return implode(':', array_slice(explode(':', $key), 0, 2));\n        }\n\n        return 'app';\n    }\n\n    /**\n     * For the given category, load the relevant settings from the database into the local cache.\n     */\n    protected function loadToLocalCache(string $cacheCategory): void\n    {\n        $query = Setting::query();\n\n        if ($cacheCategory === 'app') {\n            $query->where('setting_key', 'not like', 'user:%');\n        } else {\n            $query->where('setting_key', 'like', $cacheCategory . ':%');\n        }\n        $settings = $query->toBase()->get();\n\n        if (!isset($this->localCache[$cacheCategory])) {\n            $this->localCache[$cacheCategory] = [];\n        }\n\n        foreach ($settings as $setting) {\n            $value = $setting->value;\n\n            if ($setting->type === 'array') {\n                $value = json_decode($value, true) ?? [];\n            }\n\n            $this->localCache[$cacheCategory][$setting->setting_key] = $value;\n        }\n    }\n\n    /**\n     * Format a settings value.\n     */\n    protected function formatValue(mixed $value, mixed $default): mixed\n    {\n        // Change string booleans to actual booleans\n        if ($value === 'true') {\n            $value = true;\n        } elseif ($value === 'false') {\n            $value = false;\n        }\n\n        // Set to default if empty\n        if ($value === '') {\n            $value = $default;\n        }\n\n        return $value;\n    }\n\n    /**\n     * Checks if a setting exists.\n     */\n    public function has(string $key): bool\n    {\n        $setting = $this->getSettingObjectByKey($key);\n\n        return $setting !== null;\n    }\n\n    /**\n     * Add a setting to the database.\n     * Values can be an array or a string.\n     */\n    public function put(string $key, mixed $value): bool\n    {\n        $setting = Setting::query()->firstOrNew([\n            'setting_key' => $key,\n        ]);\n\n        $setting->type = 'string';\n        $setting->value = $value;\n\n        if (is_array($value)) {\n            $setting->type = 'array';\n            $setting->value = $this->formatArrayValue($value);\n        }\n\n        $setting->save();\n        $this->putValueIntoLocalCache($key, $value);\n\n        return true;\n    }\n\n    /**\n     * Format an array to be stored as a setting.\n     * Array setting types are expected to be a flat array of child key=>value array items.\n     * This filters out any child items that are empty.\n     */\n    protected function formatArrayValue(array $value): string\n    {\n        $values = collect($value)->values()->filter(function (array $item) {\n            return count(array_filter($item)) > 0;\n        });\n\n        return json_encode($values);\n    }\n\n    /**\n     * Put a user-specific setting into the database.\n     * Can only take string value types since this may use\n     * the session which is less flexible to data types.\n     */\n    public function putUser(User $user, string $key, string $value): bool\n    {\n        if ($user->isGuest()) {\n            session()->put($key, $value);\n\n            return true;\n        }\n\n        return $this->put($this->userKey($user->id, $key), $value);\n    }\n\n    /**\n     * Put a user-specific setting into the database for the current access user.\n     * Can only take string value types since this may use\n     * the session which is less flexible to data types.\n     */\n    public function putForCurrentUser(string $key, string $value): bool\n    {\n        return $this->putUser(user(), $key, $value);\n    }\n\n    /**\n     * Convert a setting key into a user-specific key.\n     */\n    protected function userKey(string $userId, string $key = ''): string\n    {\n        return 'user:' . $userId . ':' . $key;\n    }\n\n    /**\n     * Removes a setting from the database.\n     */\n    public function remove(string $key): void\n    {\n        $setting = $this->getSettingObjectByKey($key);\n        if ($setting) {\n            $setting->delete();\n        }\n\n        $cacheCategory = $this->localCacheCategory($key);\n        if (isset($this->localCache[$cacheCategory])) {\n            unset($this->localCache[$cacheCategory][$key]);\n        }\n    }\n\n    /**\n     * Delete settings for a given user id.\n     */\n    public function deleteUserSettings(string $userId): void\n    {\n        Setting::query()\n            ->where('setting_key', 'like', $this->userKey($userId) . '%')\n            ->delete();\n    }\n\n    /**\n     * Gets a setting model from the database for the given key.\n     */\n    protected function getSettingObjectByKey(string $key): ?Setting\n    {\n        return Setting::query()\n            ->where('setting_key', '=', $key)\n            ->first();\n    }\n\n    /**\n     * Empty the local setting value cache used by this service.\n     */\n    public function flushCache(): void\n    {\n        $this->localCache = [];\n    }\n}\n"
  },
  {
    "path": "app/Settings/StatusController.php",
    "content": "<?php\n\nnamespace BookStack\\Settings;\n\nuse BookStack\\Http\\Controller;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Session;\nuse Illuminate\\Support\\Str;\n\nclass StatusController extends Controller\n{\n    /**\n     * Show the system status as a simple json page.\n     */\n    public function show()\n    {\n        $statuses = [\n            'database' => $this->trueWithoutError(function () {\n                return DB::table('migrations')->count() > 0;\n            }),\n            'cache' => $this->trueWithoutError(function () {\n                $rand = Str::random(12);\n                $key = \"status_test_{$rand}\";\n                Cache::add($key, $rand);\n\n                return Cache::pull($key) === $rand;\n            }),\n            'session' => $this->trueWithoutError(function () {\n                $rand = Str::random();\n                Session::put('status_test', $rand);\n\n                return Session::get('status_test') === $rand;\n            }),\n        ];\n\n        $hasError = in_array(false, $statuses);\n\n        return response()->json($statuses, $hasError ? 500 : 200);\n    }\n\n    /**\n     * Check the callable passed returns true and does not throw an exception.\n     */\n    protected function trueWithoutError(callable $test): bool\n    {\n        try {\n            return $test() === true;\n        } catch (\\Exception $e) {\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "app/Settings/TestEmailNotification.php",
    "content": "<?php\n\nnamespace BookStack\\Settings;\n\nuse BookStack\\App\\MailNotification;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Notifications\\Messages\\MailMessage;\n\nclass TestEmailNotification extends MailNotification\n{\n    public function toMail(User $notifiable): MailMessage\n    {\n        return $this->newMailMessage()\n                ->subject(trans('settings.maint_send_test_email_mail_subject'))\n                ->greeting(trans('settings.maint_send_test_email_mail_greeting'))\n                ->line(trans('settings.maint_send_test_email_mail_text'));\n    }\n}\n"
  },
  {
    "path": "app/Settings/UserNotificationPreferences.php",
    "content": "<?php\n\nnamespace BookStack\\Settings;\n\nuse BookStack\\Users\\Models\\User;\n\nclass UserNotificationPreferences\n{\n    public function __construct(\n        protected User $user\n    ) {\n    }\n\n    public function notifyOnOwnPageChanges(): bool\n    {\n        return $this->getNotificationSetting('own-page-changes');\n    }\n\n    public function notifyOnOwnPageComments(): bool\n    {\n        return $this->getNotificationSetting('own-page-comments');\n    }\n\n    public function notifyOnCommentReplies(): bool\n    {\n        return $this->getNotificationSetting('comment-replies');\n    }\n\n    public function notifyOnCommentMentions(): bool\n    {\n        return $this->getNotificationSetting('comment-mentions');\n    }\n\n    public function updateFromSettingsArray(array $settings)\n    {\n        $allowList = ['own-page-changes', 'own-page-comments', 'comment-replies', 'comment-mentions'];\n        foreach ($settings as $setting => $status) {\n            if (!in_array($setting, $allowList)) {\n                continue;\n            }\n\n            $value = $status === 'true' ? 'true' : 'false';\n            setting()->putUser($this->user, 'notifications#' . $setting, $value);\n        }\n    }\n\n    protected function getNotificationSetting(string $key): bool\n    {\n        return setting()->getUser($this->user, 'notifications#' . $key);\n    }\n}\n"
  },
  {
    "path": "app/Settings/UserShortcutMap.php",
    "content": "<?php\n\nnamespace BookStack\\Settings;\n\nclass UserShortcutMap\n{\n    protected const DEFAULTS = [\n        // Header actions\n        \"home_view\" => \"1\",\n        \"shelves_view\" => \"2\",\n        \"books_view\" => \"3\",\n        \"settings_view\" => \"4\",\n        \"favourites_view\" => \"5\",\n        \"profile_view\" => \"6\",\n        \"global_search\" => \"/\",\n        \"logout\" => \"0\",\n\n        // Common actions\n        \"edit\" => \"e\",\n        \"new\" => \"n\",\n        \"copy\" => \"c\",\n        \"delete\" => \"d\",\n        \"favourite\" => \"f\",\n        \"export\" => \"x\",\n        \"sort\" => \"s\",\n        \"permissions\" => \"p\",\n        \"move\" => \"m\",\n        \"revisions\" => \"r\",\n\n        // Navigation\n        \"next\" => \"ArrowRight\",\n        \"previous\" => \"ArrowLeft\",\n    ];\n\n    /**\n     * @var array<string, string>\n     */\n    protected array $mapping;\n\n    public function __construct(array $map)\n    {\n        $this->mapping = static::DEFAULTS;\n        $this->merge($map);\n    }\n\n    /**\n     * Merge the given map into the current shortcut mapping.\n     */\n    protected function merge(array $map): void\n    {\n        foreach ($map as $key => $value) {\n            if (is_string($value) && isset($this->mapping[$key])) {\n                $this->mapping[$key] = $value;\n            }\n        }\n    }\n\n    /**\n     * Get the shortcut defined for the given ID.\n     */\n    public function getShortcut(string $id): string\n    {\n        return $this->mapping[$id] ?? '';\n    }\n\n    /**\n     * Convert this mapping to JSON.\n     */\n    public function toJson(): string\n    {\n        return json_encode($this->mapping);\n    }\n\n    /**\n     * Create a new instance from the current user's preferences.\n     */\n    public static function fromUserPreferences(): self\n    {\n        $userKeyMap = setting()->getForCurrentUser('ui-shortcuts');\n        return new self(json_decode($userKeyMap, true) ?: []);\n    }\n}\n"
  },
  {
    "path": "app/Sorting/BookSortController.php",
    "content": "<?php\n\nnamespace BookStack\\Sorting;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Entities\\Queries\\BookQueries;\nuse BookStack\\Entities\\Tools\\BookContents;\nuse BookStack\\Facades\\Activity;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Util\\DatabaseTransaction;\nuse Illuminate\\Http\\Request;\n\nclass BookSortController extends Controller\n{\n    public function __construct(\n        protected BookQueries $queries,\n    ) {\n    }\n\n    /**\n     * Shows the view which allows pages to be re-ordered and sorted.\n     */\n    public function show(string $bookSlug)\n    {\n        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);\n        $this->checkOwnablePermission(Permission::BookUpdate, $book);\n\n        $bookChildren = (new BookContents($book))->getTree(false);\n\n        $this->setPageTitle(trans('entities.books_sort_named', ['bookName' => $book->getShortName()]));\n\n        return view('books.sort', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);\n    }\n\n    /**\n     * Shows the sort box for a single book.\n     * Used via AJAX when loading in extra books to a sort.\n     */\n    public function showItem(string $bookSlug)\n    {\n        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);\n        $bookChildren = (new BookContents($book))->getTree();\n\n        return view('books.parts.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);\n    }\n\n    /**\n     * Update the sort options of a book, setting the auto-sort and/or updating\n     * child order via mapping.\n     */\n    public function update(Request $request, BookSorter $sorter, string $bookSlug)\n    {\n        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);\n        $this->checkOwnablePermission(Permission::BookUpdate, $book);\n        $loggedActivityForBook = false;\n\n        // Sort via map\n        if ($request->filled('sort-tree')) {\n            (new DatabaseTransaction(function () use ($book, $request, $sorter, &$loggedActivityForBook) {\n                $sortMap = BookSortMap::fromJson($request->get('sort-tree'));\n                $booksInvolved = $sorter->sortUsingMap($sortMap);\n\n                // Add activity for involved books.\n                foreach ($booksInvolved as $bookInvolved) {\n                    Activity::add(ActivityType::BOOK_SORT, $bookInvolved);\n                    if ($bookInvolved->id === $book->id) {\n                        $loggedActivityForBook = true;\n                    }\n                }\n            }))->run();\n        }\n\n        if ($request->filled('auto-sort')) {\n            $sortSetId = intval($request->get('auto-sort')) ?: null;\n            if ($sortSetId && SortRule::query()->find($sortSetId) === null) {\n                $sortSetId = null;\n            }\n            $book->sort_rule_id = $sortSetId;\n            $book->save();\n            $sorter->runBookAutoSort($book);\n            if (!$loggedActivityForBook) {\n                Activity::add(ActivityType::BOOK_SORT, $book);\n            }\n        }\n\n        return redirect($book->getUrl());\n    }\n}\n"
  },
  {
    "path": "app/Sorting/BookSortMap.php",
    "content": "<?php\n\nnamespace BookStack\\Sorting;\n\nclass BookSortMap\n{\n    /**\n     * @var BookSortMapItem[]\n     */\n    protected $mapData = [];\n\n    public function addItem(BookSortMapItem $mapItem): void\n    {\n        $this->mapData[] = $mapItem;\n    }\n\n    /**\n     * @return BookSortMapItem[]\n     */\n    public function all(): array\n    {\n        return $this->mapData;\n    }\n\n    public static function fromJson(string $json): self\n    {\n        $map = new BookSortMap();\n        $mapData = json_decode($json);\n\n        foreach ($mapData as $mapDataItem) {\n            $item = new BookSortMapItem(\n                intval($mapDataItem->id),\n                intval($mapDataItem->sort),\n                $mapDataItem->parentChapter ? intval($mapDataItem->parentChapter) : null,\n                $mapDataItem->type,\n                intval($mapDataItem->book)\n            );\n\n            $map->addItem($item);\n        }\n\n        return $map;\n    }\n}\n"
  },
  {
    "path": "app/Sorting/BookSortMapItem.php",
    "content": "<?php\n\nnamespace BookStack\\Sorting;\n\nclass BookSortMapItem\n{\n    /**\n     * @var int\n     */\n    public $id;\n\n    /**\n     * @var int\n     */\n    public $sort;\n\n    /**\n     * @var ?int\n     */\n    public $parentChapterId;\n\n    /**\n     * @var string\n     */\n    public $type;\n\n    /**\n     * @var int\n     */\n    public $parentBookId;\n\n    public function __construct(int $id, int $sort, ?int $parentChapterId, string $type, int $parentBookId)\n    {\n        $this->id = $id;\n        $this->sort = $sort;\n        $this->parentChapterId = $parentChapterId;\n        $this->type = $type;\n        $this->parentBookId = $parentBookId;\n    }\n}\n"
  },
  {
    "path": "app/Sorting/BookSorter.php",
    "content": "<?php\n\nnamespace BookStack\\Sorting;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\BookChild;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse BookStack\\Entities\\Tools\\ParentChanger;\nuse BookStack\\Permissions\\Permission;\n\nclass BookSorter\n{\n    public function __construct(\n        protected EntityQueries $queries,\n        protected ParentChanger $parentChanger,\n    ) {\n    }\n\n    public function runBookAutoSortForAllWithSet(SortRule $set): void\n    {\n        $set->books()->chunk(50, function ($books) {\n            foreach ($books as $book) {\n                $this->runBookAutoSort($book);\n            }\n        });\n    }\n\n    /**\n     * Runs the auto-sort for a book if the book has a sort set applied to it.\n     * This does not consider permissions since the sort operations are centrally\n     * managed by admins so considered permitted if existing and assigned.\n     */\n    public function runBookAutoSort(Book $book): void\n    {\n        $rule = $book->sortRule()->first();\n        if (!($rule instanceof SortRule)) {\n            return;\n        }\n\n        $sortFunctions = array_map(function (SortRuleOperation $op) {\n            return $op->getSortFunction();\n        }, $rule->getOperations());\n\n        $chapters = $book->chapters()\n            ->with('pages:id,name,book_id,chapter_id,priority,created_at,updated_at')\n            ->get(['id', 'name', 'priority', 'created_at', 'updated_at']);\n\n        /** @var (Chapter|Book)[] $topItems */\n        $topItems = [\n            ...$book->directPages()->get(['id', 'book_id', 'name', 'priority', 'created_at', 'updated_at']),\n            ...$chapters,\n        ];\n\n        foreach ($sortFunctions as $sortFunction) {\n            usort($topItems, $sortFunction);\n        }\n\n        foreach ($topItems as $index => $topItem) {\n            $topItem->priority = $index + 1;\n            $topItem::withoutTimestamps(fn () => $topItem->save());\n        }\n\n        foreach ($chapters as $chapter) {\n            $pages = $chapter->pages->all();\n            foreach ($sortFunctions as $sortFunction) {\n                usort($pages, $sortFunction);\n            }\n\n            foreach ($pages as $index => $page) {\n                $page->priority = $index + 1;\n                $page::withoutTimestamps(fn () => $page->save());\n            }\n        }\n    }\n\n\n    /**\n     * Sort the books content using the given sort map.\n     * Returns a list of books that were involved in the operation.\n     *\n     * @return Book[]\n     */\n    public function sortUsingMap(BookSortMap $sortMap): array\n    {\n        // Load models into map\n        $modelMap = $this->loadModelsFromSortMap($sortMap);\n\n        // Sort our changes from our map to be chapters first\n        // Since they need to be process to ensure book alignment for child page changes.\n        $sortMapItems = $sortMap->all();\n        usort($sortMapItems, function (BookSortMapItem $itemA, BookSortMapItem $itemB) {\n            $aScore = $itemA->type === 'page' ? 2 : 1;\n            $bScore = $itemB->type === 'page' ? 2 : 1;\n\n            return $aScore - $bScore;\n        });\n\n        // Perform the sort\n        foreach ($sortMapItems as $item) {\n            $this->applySortUpdates($item, $modelMap);\n        }\n\n        /** @var Book[] $booksInvolved */\n        $booksInvolved = array_values(array_filter($modelMap, function (string $key) {\n            return str_starts_with($key, 'book:');\n        }, ARRAY_FILTER_USE_KEY));\n\n        // Update permissions of books involved\n        foreach ($booksInvolved as $book) {\n            $book->rebuildPermissions();\n        }\n\n        return $booksInvolved;\n    }\n\n    /**\n     * Using the given sort map item, detect changes for the related model\n     * and update it if required. Changes where permissions are lacking will\n     * be skipped and not throw an error.\n     *\n     * @param array<string, Entity> $modelMap\n     */\n    protected function applySortUpdates(BookSortMapItem $sortMapItem, array $modelMap): void\n    {\n        /** @var BookChild $model */\n        $model = $modelMap[$sortMapItem->type . ':' . $sortMapItem->id] ?? null;\n        if (!$model) {\n            return;\n        }\n\n        $priorityChanged = $model->priority !== $sortMapItem->sort;\n        $bookChanged = $model->book_id !== $sortMapItem->parentBookId;\n        $chapterChanged = ($model instanceof Page) && $model->chapter_id !== $sortMapItem->parentChapterId;\n\n        // Stop if there's no change\n        if (!$priorityChanged && !$bookChanged && !$chapterChanged) {\n            return;\n        }\n\n        $currentParentKey = 'book:' . $model->book_id;\n        if ($model instanceof Page && $model->chapter_id) {\n            $currentParentKey = 'chapter:' . $model->chapter_id;\n        }\n\n        $currentParent = $modelMap[$currentParentKey] ?? null;\n        /** @var Book $newBook */\n        $newBook = $modelMap['book:' . $sortMapItem->parentBookId] ?? null;\n        /** @var ?Chapter $newChapter */\n        $newChapter = $sortMapItem->parentChapterId ? ($modelMap['chapter:' . $sortMapItem->parentChapterId] ?? null) : null;\n\n        if (!$this->isSortChangePermissible($sortMapItem, $model, $currentParent, $newBook, $newChapter)) {\n            return;\n        }\n\n        // Action the required changes\n        if ($bookChanged) {\n            $this->parentChanger->changeBook($model, $newBook->id);\n        }\n\n        if ($model instanceof Page && $chapterChanged) {\n            $model->chapter_id = $newChapter->id ?? 0;\n            $model->unsetRelation('chapter');\n        }\n\n        if ($priorityChanged) {\n            $model->priority = $sortMapItem->sort;\n        }\n\n        if ($chapterChanged || $priorityChanged) {\n            $model::withoutTimestamps(fn () => $model->save());\n        }\n    }\n\n    /**\n     * Check if the current user has permissions to apply the given sorting change.\n     * Is quite complex since items can gain a different parent change. Acts as a:\n     * - Update of old parent element (Change of content/order).\n     * - Update of sorted/moved element.\n     * - Deletion of element (Relative to parent upon move).\n     * - Creation of element within parent (Upon move to new parent).\n     */\n    protected function isSortChangePermissible(BookSortMapItem $sortMapItem, BookChild $model, ?Entity $currentParent, ?Entity $newBook, ?Entity $newChapter): bool\n    {\n        // Stop if we can't see the current parent or new book.\n        if (!$currentParent || !$newBook) {\n            return false;\n        }\n\n        $hasNewParent = $newBook->id !== $model->book_id || ($model instanceof Page && $model->chapter_id !== ($sortMapItem->parentChapterId ?? 0));\n        if ($model instanceof Chapter) {\n            $hasPermission = userCan(Permission::BookUpdate, $currentParent)\n                && userCan(Permission::BookUpdate, $newBook)\n                && userCan(Permission::ChapterUpdate, $model)\n                && (!$hasNewParent || userCan(Permission::ChapterCreate, $newBook))\n                && (!$hasNewParent || userCan(Permission::ChapterDelete, $model));\n\n            if (!$hasPermission) {\n                return false;\n            }\n        }\n\n        if ($model instanceof Page) {\n            $parentPermission = ($currentParent instanceof Chapter) ? 'chapter-update' : 'book-update';\n            $hasCurrentParentPermission = userCan($parentPermission, $currentParent);\n\n            // This needs to check if there was an intended chapter location in the original sort map\n            // rather than inferring from the $newChapter since that variable may be null\n            // due to other reasons (Visibility).\n            $newParent = $sortMapItem->parentChapterId ? $newChapter : $newBook;\n            if (!$newParent) {\n                return false;\n            }\n\n            $hasPageEditPermission = userCan(Permission::PageUpdate, $model);\n            $newParentInRightLocation = ($newParent instanceof Book || ($newParent instanceof Chapter && $newParent->book_id === $newBook->id));\n            $newParentPermission = ($newParent instanceof Chapter) ? 'chapter-update' : 'book-update';\n            $hasNewParentPermission = userCan($newParentPermission, $newParent);\n\n            $hasDeletePermissionIfMoving = (!$hasNewParent || userCan(Permission::PageDelete, $model));\n            $hasCreatePermissionIfMoving = (!$hasNewParent || userCan(Permission::PageCreate, $newParent));\n\n            $hasPermission = $hasCurrentParentPermission\n                && $newParentInRightLocation\n                && $hasNewParentPermission\n                && $hasPageEditPermission\n                && $hasDeletePermissionIfMoving\n                && $hasCreatePermissionIfMoving;\n\n            if (!$hasPermission) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    /**\n     * Load models from the database into the given sort map.\n     *\n     * @return array<string, Entity>\n     */\n    protected function loadModelsFromSortMap(BookSortMap $sortMap): array\n    {\n        $modelMap = [];\n        $ids = [\n            'chapter' => [],\n            'page'    => [],\n            'book'    => [],\n        ];\n\n        foreach ($sortMap->all() as $sortMapItem) {\n            $ids[$sortMapItem->type][] = $sortMapItem->id;\n            $ids['book'][] = $sortMapItem->parentBookId;\n            if ($sortMapItem->parentChapterId) {\n                $ids['chapter'][] = $sortMapItem->parentChapterId;\n            }\n        }\n\n        $pages = $this->queries->pages->visibleForList()->whereIn('id', array_unique($ids['page']))->get();\n        /** @var Page $page */\n        foreach ($pages as $page) {\n            $modelMap['page:' . $page->id] = $page;\n            $ids['book'][] = $page->book_id;\n            if ($page->chapter_id) {\n                $ids['chapter'][] = $page->chapter_id;\n            }\n        }\n\n        $chapters = $this->queries->chapters->visibleForList()->whereIn('id', array_unique($ids['chapter']))->get();\n        /** @var Chapter $chapter */\n        foreach ($chapters as $chapter) {\n            $modelMap['chapter:' . $chapter->id] = $chapter;\n            $ids['book'][] = $chapter->book_id;\n        }\n\n        $books = $this->queries->books->visibleForList()->whereIn('id', array_unique($ids['book']))->get();\n        /** @var Book $book */\n        foreach ($books as $book) {\n            $modelMap['book:' . $book->id] = $book;\n        }\n\n        return $modelMap;\n    }\n}\n"
  },
  {
    "path": "app/Sorting/SortRule.php",
    "content": "<?php\n\nnamespace BookStack\\Sorting;\n\nuse BookStack\\Activity\\Models\\Loggable;\nuse BookStack\\Entities\\Models\\Book;\nuse Carbon\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\n\n/**\n * @property int $id\n * @property string $name\n * @property string $sequence\n * @property Carbon $created_at\n * @property Carbon $updated_at\n */\nclass SortRule extends Model implements Loggable\n{\n    use HasFactory;\n\n    /**\n     * @return SortRuleOperation[]\n     */\n    public function getOperations(): array\n    {\n        return SortRuleOperation::fromSequence($this->sequence);\n    }\n\n    /**\n     * @param SortRuleOperation[] $options\n     */\n    public function setOperations(array $options): void\n    {\n        $values = array_map(fn (SortRuleOperation $opt) => $opt->value, $options);\n        $this->sequence = implode(',', $values);\n    }\n\n    public function logDescriptor(): string\n    {\n        return \"({$this->id}) {$this->name}\";\n    }\n\n    public function getUrl(): string\n    {\n        return url(\"/settings/sorting/rules/{$this->id}\");\n    }\n\n    public function books(): HasMany\n    {\n        return $this->hasMany(Book::class, 'entity_container_data.sort_rule_id', 'id');\n    }\n\n    public static function allByName(): Collection\n    {\n        return static::query()\n            ->withCount('books')\n            ->orderBy('name', 'asc')\n            ->get();\n    }\n}\n"
  },
  {
    "path": "app/Sorting/SortRuleController.php",
    "content": "<?php\n\nnamespace BookStack\\Sorting;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Entities\\Models\\EntityContainerData;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse Illuminate\\Http\\Request;\n\nclass SortRuleController extends Controller\n{\n    public function __construct()\n    {\n        $this->middleware(Permission::SettingsManage->middleware());\n    }\n\n    public function create()\n    {\n        $this->setPageTitle(trans('settings.sort_rule_create'));\n\n        return view('settings.sort-rules.create');\n    }\n\n    public function store(Request $request)\n    {\n        $this->validate($request, [\n            'name' => ['required', 'string', 'min:1', 'max:200'],\n            'sequence' => ['required', 'string', 'min:1'],\n        ]);\n\n        $operations = SortRuleOperation::fromSequence($request->input('sequence'));\n        if (count($operations) === 0) {\n            return redirect('/settings/sorting/rules/new')->withInput()->withErrors(['sequence' => 'No operations set.']);\n        }\n\n        $rule = new SortRule();\n        $rule->name = $request->input('name');\n        $rule->setOperations($operations);\n        $rule->save();\n\n        $this->logActivity(ActivityType::SORT_RULE_CREATE, $rule);\n\n        return redirect('/settings/sorting');\n    }\n\n    public function edit(string $id)\n    {\n        $rule = SortRule::query()->findOrFail($id);\n\n        $this->setPageTitle(trans('settings.sort_rule_edit'));\n\n        return view('settings.sort-rules.edit', ['rule' => $rule]);\n    }\n\n    public function update(string $id, Request $request, BookSorter $bookSorter)\n    {\n        $this->validate($request, [\n            'name' => ['required', 'string', 'min:1', 'max:200'],\n            'sequence' => ['required', 'string', 'min:1'],\n        ]);\n\n        $rule = SortRule::query()->findOrFail($id);\n        $operations = SortRuleOperation::fromSequence($request->input('sequence'));\n        if (count($operations) === 0) {\n            return redirect($rule->getUrl())->withInput()->withErrors(['sequence' => 'No operations set.']);\n        }\n\n        $rule->name = $request->input('name');\n        $rule->setOperations($operations);\n        $changedSequence = $rule->isDirty('sequence');\n        $rule->save();\n\n        $this->logActivity(ActivityType::SORT_RULE_UPDATE, $rule);\n\n        if ($changedSequence) {\n            $bookSorter->runBookAutoSortForAllWithSet($rule);\n        }\n\n        return redirect('/settings/sorting');\n    }\n\n    public function destroy(string $id, Request $request)\n    {\n        $rule = SortRule::query()->findOrFail($id);\n        $confirmed = $request->input('confirm') === 'true';\n        $booksAssigned = $rule->books()->count();\n        $warnings = [];\n\n        if ($booksAssigned > 0) {\n            if ($confirmed) {\n                EntityContainerData::query()\n                    ->where('sort_rule_id', $rule->id)\n                    ->update(['sort_rule_id' => null]);\n            } else {\n                $warnings[] = trans('settings.sort_rule_delete_warn_books', ['count' => $booksAssigned]);\n            }\n        }\n\n        $defaultBookSortSetting = intval(setting('sorting-book-default', '0'));\n        if ($defaultBookSortSetting === intval($id)) {\n            if ($confirmed) {\n                setting()->remove('sorting-book-default');\n            } else {\n                $warnings[] = trans('settings.sort_rule_delete_warn_default');\n            }\n        }\n\n        if (count($warnings) > 0) {\n            return redirect($rule->getUrl() . '#delete')->withErrors(['delete' => $warnings]);\n        }\n\n        $rule->delete();\n        $this->logActivity(ActivityType::SORT_RULE_DELETE, $rule);\n\n        return redirect('/settings/sorting');\n    }\n}\n"
  },
  {
    "path": "app/Sorting/SortRuleOperation.php",
    "content": "<?php\n\nnamespace BookStack\\Sorting;\n\nuse Closure;\nuse Illuminate\\Support\\Str;\n\nenum SortRuleOperation: string\n{\n    case NameAsc = 'name_asc';\n    case NameDesc = 'name_desc';\n    case NameNumericAsc = 'name_numeric_asc';\n    case NameNumericDesc = 'name_numeric_desc';\n    case CreatedDateAsc = 'created_date_asc';\n    case CreatedDateDesc = 'created_date_desc';\n    case UpdateDateAsc = 'updated_date_asc';\n    case UpdateDateDesc = 'updated_date_desc';\n    case ChaptersFirst = 'chapters_first';\n    case ChaptersLast = 'chapters_last';\n\n    /**\n     * Provide a translated label string for this option.\n     */\n    public function getLabel(): string\n    {\n        $key = $this->value;\n        $label = '';\n        if (str_ends_with($key, '_asc')) {\n            $key = substr($key, 0, -4);\n            $label = trans('settings.sort_rule_op_asc');\n        } elseif (str_ends_with($key, '_desc')) {\n            $key = substr($key, 0, -5);\n            $label = trans('settings.sort_rule_op_desc');\n        }\n\n        $label = trans('settings.sort_rule_op_' . $key) . ' ' . $label;\n        return trim($label);\n    }\n\n    public function getSortFunction(): callable\n    {\n        $camelValue = Str::camel($this->value);\n        return SortSetOperationComparisons::$camelValue(...);\n    }\n\n    /**\n     * @return SortRuleOperation[]\n     */\n    public static function allExcluding(array $operations): array\n    {\n        $all = SortRuleOperation::cases();\n        $filtered = array_filter($all, function (SortRuleOperation $operation) use ($operations) {\n            return !in_array($operation, $operations);\n        });\n        return array_values($filtered);\n    }\n\n    /**\n     * Create a set of operations from a string sequence representation.\n     * (values seperated by commas).\n     * @return SortRuleOperation[]\n     */\n    public static function fromSequence(string $sequence): array\n    {\n        $strOptions = explode(',', $sequence);\n        $options = array_map(fn ($val) => SortRuleOperation::tryFrom($val), $strOptions);\n        return array_filter($options);\n    }\n}\n"
  },
  {
    "path": "app/Sorting/SortSetOperationComparisons.php",
    "content": "<?php\n\nnamespace BookStack\\Sorting;\n\nuse voku\\helper\\ASCII;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Entity;\n\n/**\n * Sort comparison function for each of the possible SortSetOperation values.\n * Method names should be camelCase names for the SortSetOperation enum value.\n */\nclass SortSetOperationComparisons\n{\n    public static function nameAsc(Entity $a, Entity $b): int\n    {\n        return strtolower(ASCII::to_transliterate($a->name, null)) <=> strtolower(ASCII::to_transliterate($b->name, null));\n    }\n\n    public static function nameDesc(Entity $a, Entity $b): int\n    {\n        return strtolower(ASCII::to_transliterate($b->name, null)) <=> strtolower(ASCII::to_transliterate($a->name, null));\n    }\n\n    public static function nameNumericAsc(Entity $a, Entity $b): int\n    {\n        $numRegex = '/^\\d+(\\.\\d+)?/';\n        $aMatches = [];\n        $bMatches = [];\n        preg_match($numRegex, $a->name, $aMatches);\n        preg_match($numRegex, $b->name, $bMatches);\n        $aVal = floatval(($aMatches[0] ?? 0));\n        $bVal = floatval(($bMatches[0] ?? 0));\n\n        return $aVal <=> $bVal;\n    }\n\n    public static function nameNumericDesc(Entity $a, Entity $b): int\n    {\n        return -(static::nameNumericAsc($a, $b));\n    }\n\n    public static function createdDateAsc(Entity $a, Entity $b): int\n    {\n        return $a->created_at->unix() <=> $b->created_at->unix();\n    }\n\n    public static function createdDateDesc(Entity $a, Entity $b): int\n    {\n        return $b->created_at->unix() <=> $a->created_at->unix();\n    }\n\n    public static function updatedDateAsc(Entity $a, Entity $b): int\n    {\n        return $a->updated_at->unix() <=> $b->updated_at->unix();\n    }\n\n    public static function updatedDateDesc(Entity $a, Entity $b): int\n    {\n        return $b->updated_at->unix() <=> $a->updated_at->unix();\n    }\n\n    public static function chaptersFirst(Entity $a, Entity $b): int\n    {\n        return ($b instanceof Chapter ? 1 : 0) - (($a instanceof Chapter) ? 1 : 0);\n    }\n\n    public static function chaptersLast(Entity $a, Entity $b): int\n    {\n        return ($a instanceof Chapter ? 1 : 0) - (($b instanceof Chapter) ? 1 : 0);\n    }\n}\n"
  },
  {
    "path": "app/Sorting/SortUrl.php",
    "content": "<?php\n\nnamespace BookStack\\Sorting;\n\n/**\n * Generate a URL with multiple parameters for sorting purposes.\n * Works out the logic to set the correct sorting direction\n * Discards empty parameters and allows overriding.\n */\nclass SortUrl\n{\n    public function __construct(\n        protected string $path,\n        protected array $data,\n        protected array $overrideData = []\n    ) {\n    }\n\n    public function withOverrideData(array $overrideData = []): self\n    {\n        return new self($this->path, $this->data, $overrideData);\n    }\n\n    public function build(): string\n    {\n        $queryStringSections = [];\n        $queryData = array_merge($this->data, $this->overrideData);\n\n        // Change sorting direction if already sorted on current attribute\n        if (isset($this->overrideData['sort']) && $this->overrideData['sort'] === $this->data['sort']) {\n            $queryData['order'] = ($this->data['order'] === 'asc') ? 'desc' : 'asc';\n        } elseif (isset($this->overrideData['sort'])) {\n            $queryData['order'] = 'asc';\n        }\n\n        foreach ($queryData as $name => $value) {\n            $trimmedVal = trim($value);\n            if ($trimmedVal !== '') {\n                $queryStringSections[] = urlencode($name) . '=' . urlencode($trimmedVal);\n            }\n        }\n\n        if (count($queryStringSections) === 0) {\n            return url($this->path);\n        }\n\n        return url($this->path . '?' . implode('&', $queryStringSections));\n    }\n}\n"
  },
  {
    "path": "app/Theming/CustomHtmlHeadContentProvider.php",
    "content": "<?php\n\nnamespace BookStack\\Theming;\n\nuse BookStack\\Util\\CspService;\nuse BookStack\\Util\\HtmlContentFilter;\nuse BookStack\\Util\\HtmlContentFilterConfig;\nuse BookStack\\Util\\HtmlNonceApplicator;\nuse Illuminate\\Contracts\\Cache\\Repository as Cache;\n\nclass CustomHtmlHeadContentProvider\n{\n    public function __construct(\n        protected CspService $cspService,\n        protected Cache $cache,\n        protected ThemeService $themeService,\n    ) {\n    }\n\n    /**\n     * Fetch our custom HTML head content prepared for use on web pages.\n     * Content has a nonce applied for CSP.\n     */\n    public function forWeb(): string\n    {\n        $content = $this->getSourceContent();\n        $hash = md5($content) . ':' . $this->themeService->getModulesHash();\n        $html = $this->cache->remember('custom-head-web:' . $hash, 86400, function () use ($content) {\n            $content .= \"\\n\" . $this->getModuleHeadContent();\n            return HtmlNonceApplicator::prepare($content);\n        });\n\n        return HtmlNonceApplicator::apply($html, $this->cspService->getNonce());\n    }\n\n    /**\n     * Fetch our custom HTML head content prepared for use in export formats.\n     * Scripts are stripped to avoid potential issues.\n     */\n    public function forExport(): string\n    {\n        $content = $this->getSourceContent();\n        $hash = md5($content);\n\n        return $this->cache->remember('custom-head-export:' . $hash, 86400, function () use ($content) {\n            $config = new HtmlContentFilterConfig(filterOutNonContentElements: false, useAllowListFilter: false);\n            return (new HtmlContentFilter($config))->filterString($content);\n        });\n    }\n\n    /**\n     * Get the original custom head content to use.\n     */\n    protected function getSourceContent(): string\n    {\n        return setting('app-custom-head', '');\n    }\n\n    /**\n     * Get any custom head content from installed modules.\n     */\n    protected function getModuleHeadContent(): string\n    {\n        $content = '';\n        foreach ($this->themeService->getModules() as $module) {\n            $headContentPath = $module->path('head');\n            if (file_exists($headContentPath) && is_dir($headContentPath)) {\n                $htmlFiles = glob($headContentPath . '/*.html');\n                foreach ($htmlFiles as $file) {\n                    $content .= file_get_contents($file);\n                }\n            }\n        }\n\n        return $content;\n    }\n}\n"
  },
  {
    "path": "app/Theming/ThemeController.php",
    "content": "<?php\n\nnamespace BookStack\\Theming;\n\nuse BookStack\\Facades\\Theme;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Util\\FilePathNormalizer;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\n\nclass ThemeController extends Controller\n{\n    /**\n     * Serve a public file from the configured theme.\n     */\n    public function publicFile(string $theme, string $path): StreamedResponse\n    {\n        $cleanPath = FilePathNormalizer::normalize($path);\n        if ($theme !== Theme::getTheme() || !$cleanPath) {\n            abort(404);\n        }\n\n        $filePath = Theme::findFirstFile(\"public/{$cleanPath}\");\n        if (!$filePath) {\n            abort(404);\n        }\n\n        $response = $this->download()->streamedFileInline($filePath);\n        $response->setMaxAge(86400);\n\n        return $response;\n    }\n}\n"
  },
  {
    "path": "app/Theming/ThemeEvents.php",
    "content": "<?php\n\nnamespace BookStack\\Theming;\n\n/**\n * The ThemeEvents used within BookStack.\n *\n * This file details the events that BookStack may fire via the custom\n * theme system, including event names, parameters and expected return types.\n *\n * This system is regarded as semi-stable.\n * We'll look to fix issues with it or migrate old event types but\n * events and their signatures may change in new versions of BookStack.\n * We'd advise testing any usage of these events upon upgrade.\n */\nclass ThemeEvents\n{\n    /**\n     * Activity logged event.\n     * Runs right after an activity is logged by bookstack.\n     * These are the activities that can be seen in the audit log area of BookStack.\n     * Activity types can be seen listed in the \\BookStack\\Actions\\ActivityType class.\n     * The provided $detail can be a string or a loggable type of model. You should check\n     * the type before making use of this parameter.\n     *\n     * @param string $type\n     * @param string|\\BookStack\\Activity\\Models\\Loggable $detail\n     */\n    const ACTIVITY_LOGGED = 'activity_logged';\n\n    /**\n     * Application boot-up.\n     * After main services are registered.\n     *\n     * @param \\BookStack\\App\\Application $app\n     */\n    const APP_BOOT = 'app_boot';\n\n    /**\n     * Auth login event.\n     * Runs right after a user is logged-in to the application by any authentication\n     * system as a standard app user. This includes a user becoming logged in\n     * after registration. This is not emitted upon API usage.\n     *\n     * @param string $authSystem\n     * @param \\BookStack\\Users\\Models\\User $user\n     */\n    const AUTH_LOGIN = 'auth_login';\n\n    /**\n     * Auth pre-register event.\n     * Runs right before a new user account is registered in the system by any authentication\n     * system as a standard app user including auto-registration systems used by LDAP,\n     * SAML, OIDC and social systems. It only includes self-registrations,\n     * not accounts created by others in the UI or via the REST API.\n     * It runs after any other normal validation steps.\n     * Any account/email confirmation occurs post-registration.\n     * The provided $userData contains the main details that would be used to create\n     * the account, and may depend on authentication method.\n     * If false is returned from the event, registration will be prevented and the user\n     * will be returned to the login page.\n     *\n     * @param string $authSystem\n     * @param array $userData\n     * @return bool|null\n     */\n    const AUTH_PRE_REGISTER = 'auth_pre_register';\n\n    /**\n     * Auth register event.\n     * Runs right after a user is newly registered to the application by any authentication\n     * system as a standard app user. This includes auto-registration systems used\n     * by LDAP, SAML, OIDC and social systems. It only includes self-registrations.\n     *\n     * @param string $authSystem\n     * @param \\BookStack\\Users\\Models\\User $user\n     */\n    const AUTH_REGISTER = 'auth_register';\n\n    /**\n     * Commonmark environment configure.\n     * Provides the commonmark library environment for customization before it's used to render markdown content.\n     * If the listener returns a non-null value, that will be used as an environment instead.\n     *\n     * @param \\League\\CommonMark\\Environment\\Environment $environment\n     * @return \\League\\CommonMark\\Environment\\Environment|null\n     */\n    const COMMONMARK_ENVIRONMENT_CONFIGURE = 'commonmark_environment_configure';\n\n    /**\n     * OIDC auth pre-redirect event.\n     * Runs just before BookStack redirects the user to the identity provider for authentication.\n     * Provides the redirect URL that will be used.\n     * If the listener returns a string value, that will be used as the redirect URL instead.\n     *\n     * @param string $redirectUrl\n     * @return string|null\n     */\n    const OIDC_AUTH_PRE_REDIRECT = 'oidc_auth_pre_redirect';\n\n    /**\n     * OIDC ID token pre-validate event.\n     * Runs just before BookStack validates the user ID token data upon login.\n     * Provides the existing found set of claims for the user as a key-value array,\n     * along with an array of the proceeding access token data provided by the identity platform.\n     * If the listener returns a non-null value, that will replace the existing ID token claim data.\n     *\n     * @param array $idTokenData\n     * @param array $accessTokenData\n     * @return array|null\n     */\n    const OIDC_ID_TOKEN_PRE_VALIDATE = 'oidc_id_token_pre_validate';\n\n    /**\n     * Page content post-render event.\n     * Runs after any display rendering of page content, typically when page content is being processed for viewing.\n     * Rendering typically includes parsing of page includes, and content filtering.\n     * Provides the HTML content about to be shown, along with the related page instance.\n     * If the listener returns a string value, that will be used as the HTML content instead.\n     *\n     * @param string $html\n     * @param \\BookStack\\Entities\\Models\\Page $page\n     * @return string|null\n     */\n    const PAGE_CONTENT_POST_RENDER = 'page_content_post_render';\n\n    /**\n     * Page content pre-store event.\n     * Runs just before page HTML is stored in the database, after BookStack's own processing.\n     * Provides the HTML content about to be stored, along with the related page instance.\n     * If the listener returns a string value, that will be used as the HTML content instead.\n     *\n     * @param string $html\n     * @param \\BookStack\\Entities\\Models\\Page $page\n     * @return string|null\n     */\n    const PAGE_CONTENT_PRE_STORE = 'page_content_pre_store';\n\n    /**\n     * Page include parse event.\n     * Runs when a page include tag is being parsed, typically when page content is being processed for viewing.\n     * Provides the \"include tag\" reference string, the default BookStack replacement content for the tag,\n     * the current page being processed, and the page that's being referenced by the include tag.\n     * The referenced page may be null where the page does not exist or where permissions prevent visibility.\n     * If the listener returns a non-null value, that will be used as the replacement HTML content instead.\n     *\n     * @param string $tagReference\n     * @param string $replacementHTML\n     * @param \\BookStack\\Entities\\Models\\Page $currentPage\n     * @param ?\\BookStack\\Entities\\Models\\Page $referencedPage\n     */\n    const PAGE_INCLUDE_PARSE = 'page_include_parse';\n\n    /**\n     * Routes register web event.\n     * Called when standard web (browser/non-api) app routes are registered.\n     * Provides an app router, so you can register your own web routes.\n     *\n     * @param \\Illuminate\\Routing\\Router $router\n     */\n    const ROUTES_REGISTER_WEB = 'routes_register_web';\n\n    /**\n     * Routes register web auth event.\n     * Called when auth-required web (browser/non-api) app routes can be registered.\n     * These are routes that typically require login to access (unless the instance is made public).\n     * Provides an app router, so you can register your own web routes.\n     *\n     * @param \\Illuminate\\Routing\\Router $router\n     */\n    const ROUTES_REGISTER_WEB_AUTH = 'routes_register_web_auth';\n\n\n    /**\n     * Theme register views event.\n     * Called by the theme system when a theme is active, so that custom view templates can be registered\n     * to be rendered in addition to existing app views.\n     *\n     * @param \\BookStack\\Theming\\ThemeViews $themeViews\n     */\n    const THEME_REGISTER_VIEWS = 'theme_register_views';\n\n    /**\n     * Web before middleware action.\n     * Runs before the request is handled but after all other middleware apart from those\n     * that depend on the current session user (Localization for example).\n     * Provides the original request to use.\n     * Return values, if provided, will be used as a new response to use.\n     *\n     * @param \\Illuminate\\Http\\Request $request\n     * @return \\Illuminate\\Http\\Response|null\n     */\n    const WEB_MIDDLEWARE_BEFORE = 'web_middleware_before';\n\n    /**\n     * Web after middleware action.\n     * Runs after the request is handled but before the response is sent.\n     * Provides both the original request and the currently resolved response.\n     * Return values, if provided, will be used as a new response to use.\n     *\n     * @param \\Illuminate\\Http\\Request $request\n     * @param \\Illuminate\\Http\\Response|\\Symfony\\Component\\HttpFoundation\\BinaryFileResponse $response\n     * @return \\Illuminate\\Http\\Response|null\n     */\n    const WEB_MIDDLEWARE_AFTER = 'web_middleware_after';\n\n    /**\n     * Webhook call before event.\n     * Runs before a webhook endpoint is called. Allows for customization\n     * of the data format & content within the webhook POST request.\n     * Provides the original event name as a string (see \\BookStack\\Actions\\ActivityType)\n     * along with the webhook instance along with the event detail which may be a\n     * \"Loggable\" model type or a string.\n     * If the listener returns a non-null value, that will be used as the POST data instead\n     * of the system default.\n     *\n     * @param string $event\n     * @param \\BookStack\\Activity\\Models\\Webhook $webhook\n     * @param string|\\BookStack\\Activity\\Models\\Loggable $detail\n     * @param \\BookStack\\Users\\Models\\User $initiator\n     * @param int $initiatedTime\n     * @return array|null\n     */\n    const WEBHOOK_CALL_BEFORE = 'webhook_call_before';\n}\n"
  },
  {
    "path": "app/Theming/ThemeModule.php",
    "content": "<?php\n\nnamespace BookStack\\Theming;\n\nreadonly class ThemeModule\n{\n    public function __construct(\n        public string $name,\n        public string $description,\n        public string $version,\n        public string $folderName,\n    ) {\n    }\n\n    /**\n     * Create a ThemeModule instance from JSON data.\n     *\n     * @throws ThemeModuleException\n     */\n    public static function fromJson(array $data, string $folderName): self\n    {\n        if (empty($data['name']) || !is_string($data['name'])) {\n            throw new ThemeModuleException(\"Module in folder \\\"{$folderName}\\\" is missing a valid 'name' property\");\n        }\n\n        if (!isset($data['description']) || !is_string($data['description'])) {\n            throw new ThemeModuleException(\"Module in folder \\\"{$folderName}\\\" is missing a valid 'description' property\");\n        }\n\n        if (!isset($data['version']) || !is_string($data['version'])) {\n            throw new ThemeModuleException(\"Module in folder \\\"{$folderName}\\\" is missing a valid 'version' property\");\n        }\n\n        if (!preg_match('/^v?\\d+\\.\\d+\\.\\d+(-.*)?$/', $data['version'])) {\n            throw new ThemeModuleException(\"Module in folder \\\"{$folderName}\\\" has an invalid 'version' format. Expected semantic version format like '1.0.0' or 'v1.0.0'\");\n        }\n\n        return new self(\n            name: $data['name'],\n            description: $data['description'],\n            version: $data['version'],\n            folderName: $folderName,\n        );\n    }\n\n    /**\n     * Get a path for a file within this module.\n     */\n    public function path($path = ''): string\n    {\n        $component = trim($path, '/');\n        return theme_path(\"modules/{$this->folderName}/{$component}\");\n    }\n\n    public function getVersion(): string\n    {\n        return str_starts_with($this->version, 'v') ? $this->version : 'v' . $this->version;\n    }\n}\n"
  },
  {
    "path": "app/Theming/ThemeModuleException.php",
    "content": "<?php\n\nnamespace BookStack\\Theming;\n\nclass ThemeModuleException extends \\Exception\n{\n}\n"
  },
  {
    "path": "app/Theming/ThemeModuleManager.php",
    "content": "<?php\n\nnamespace BookStack\\Theming;\n\nuse Illuminate\\Support\\Str;\n\nclass ThemeModuleManager\n{\n    /** @var array<string, ThemeModule>|null */\n    protected array|null $loadedModules = null;\n\n    public function __construct(\n        protected string $modulesFolderPath\n    ) {\n    }\n\n    /**\n     * @return array<string, ThemeModule>\n     */\n    public function getByName(string $name): array\n    {\n        return array_filter($this->load(), fn(ThemeModule $module) => $module->name === $name);\n    }\n\n    public function deleteModuleFolder(string $moduleFolderName): void\n    {\n        $modules = $this->load();\n        $module = $modules[$moduleFolderName] ?? null;\n        if (!$module) {\n            return;\n        }\n\n        $moduleFolderPath = $module->path('');\n        if (!file_exists($moduleFolderPath)) {\n            return;\n        }\n\n        $this->deleteDirectoryRecursively($moduleFolderPath);\n        unset($this->loadedModules[$moduleFolderName]);\n    }\n\n    /**\n     * @throws ThemeModuleException\n     */\n    public function addFromZip(string $name, ThemeModuleZip $zip): ThemeModule\n    {\n        $baseFolderName = Str::limit(Str::slug($name), 40, '');\n        $folderName = $baseFolderName;\n        while (!$baseFolderName || file_exists($this->modulesFolderPath . DIRECTORY_SEPARATOR . $folderName)) {\n            $folderName = ($baseFolderName ?: 'mod') . '-' . Str::random(4);\n        }\n\n        $folderPath = $this->modulesFolderPath . DIRECTORY_SEPARATOR . $folderName;\n        $zip->extractTo($folderPath);\n\n        $module = $this->loadFromFolder($folderName);\n        if (!$module) {\n            throw new ThemeModuleException(\"Failed to load module from zip file after extraction\");\n        }\n\n        return $module;\n    }\n\n    protected function deleteDirectoryRecursively(string $path): void\n    {\n        $items = array_diff(scandir($path), ['.', '..']);\n        foreach ($items as $item) {\n            $itemPath = $path . DIRECTORY_SEPARATOR . $item;\n            if (is_dir($itemPath)) {\n                $this->deleteDirectoryRecursively($itemPath);\n            } else {\n                $deleted = unlink($itemPath);\n                if (!$deleted) {\n                    throw new ThemeModuleException(\"Failed to delete file at \\\"{$itemPath}\\\"\");\n                }\n            }\n        }\n        rmdir($path);\n    }\n\n    public function load(): array\n    {\n        if ($this->loadedModules !== null) {\n            return $this->loadedModules;\n        }\n\n        if (!is_dir($this->modulesFolderPath)) {\n            return [];\n        }\n\n        $subFolders = array_filter(scandir($this->modulesFolderPath), function ($item) {\n            return $item !== '.' && $item !== '..' && is_dir($this->modulesFolderPath . DIRECTORY_SEPARATOR . $item);\n        });\n\n        $modules = [];\n\n        foreach ($subFolders as $folderName) {\n            $module = $this->loadFromFolder($folderName);\n            if ($module) {\n                $modules[$folderName] = $module;\n            }\n        }\n\n        $this->loadedModules = $modules;\n\n        return $modules;\n    }\n\n    protected function loadFromFolder(string $folderName): ThemeModule|null\n    {\n        $moduleJsonFile = $this->modulesFolderPath . DIRECTORY_SEPARATOR . $folderName . DIRECTORY_SEPARATOR . 'bookstack-module.json';\n        if (!file_exists($moduleJsonFile)) {\n            return null;\n        }\n\n        try {\n            $jsonContent = file_get_contents($moduleJsonFile);\n            $jsonData = json_decode($jsonContent, true);\n\n            if (json_last_error() !== JSON_ERROR_NONE) {\n                throw new ThemeModuleException(\"Invalid JSON in module file at \\\"{$moduleJsonFile}\\\": \" . json_last_error_msg());\n            }\n\n            $module = ThemeModule::fromJson($jsonData, $folderName);\n        } catch (ThemeModuleException $exception) {\n            throw $exception;\n        } catch (\\Exception $exception) {\n            throw new ThemeModuleException(\"Failed loading module from \\\"{$moduleJsonFile}\\\" with error: {$exception->getMessage()}\");\n        }\n\n        return $module;\n    }\n}\n"
  },
  {
    "path": "app/Theming/ThemeModuleZip.php",
    "content": "<?php\n\nnamespace BookStack\\Theming;\n\nuse ZipArchive;\n\nreadonly class ThemeModuleZip\n{\n    public function __construct(\n        protected string $path\n    ) {\n    }\n\n    public function extractTo(string $destinationPath): void\n    {\n        $zip = new ZipArchive();\n        $zip->open($this->path);\n        $zip->extractTo($destinationPath);\n        $zip->close();\n    }\n\n    /**\n     * Read the module's JSON metadata to read it into a ThemeModule instance.\n     * @throws ThemeModuleException\n     */\n    public function getModuleInstance(): ThemeModule\n    {\n        $zip = new ZipArchive();\n        $open = $zip->open($this->path);\n        if ($open !== true) {\n            throw new ThemeModuleException(\"Unable to open zip file at {$this->path}\");\n        }\n\n        $moduleJsonText = $zip->getFromName('bookstack-module.json');\n        $zip->close();\n\n        if ($moduleJsonText === false) {\n            throw new ThemeModuleException(\"bookstack-module.json not found within module ZIP at {$this->path}\");\n        }\n\n        $moduleJson = json_decode($moduleJsonText, true);\n        if ($moduleJson === null) {\n            throw new ThemeModuleException(\"Could not read JSON from bookstack-module.json within module ZIP at {$this->path}\");\n        }\n\n        return ThemeModule::fromJson($moduleJson, '_temp');\n    }\n\n    /**\n     * Get the path to the zip file.\n     */\n    public function getPath(): string\n    {\n        return $this->path;\n    }\n\n    /**\n     * Check if the zip file exists and that it appears to be a valid zip file.\n     */\n    public function exists(): bool\n    {\n        if (!file_exists($this->path)) {\n            return false;\n        }\n\n        $zip = new ZipArchive();\n        $open = $zip->open($this->path, ZipArchive::RDONLY);\n        if ($open === true) {\n            $zip->close();\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * Get the total size of the zip file contents when uncompressed.\n     */\n    public function getContentsSize(): int\n    {\n        $zip = new ZipArchive();\n\n        if ($zip->open($this->path) !== true) {\n            return 0;\n        }\n\n        $totalSize = 0;\n        for ($i = 0; $i < $zip->numFiles; $i++) {\n            $stat = $zip->statIndex($i);\n            if ($stat !== false) {\n                $totalSize += $stat['size'];\n            }\n        }\n\n        $zip->close();\n\n        return $totalSize;\n    }\n}\n"
  },
  {
    "path": "app/Theming/ThemeService.php",
    "content": "<?php\n\nnamespace BookStack\\Theming;\n\nuse BookStack\\Access\\SocialDriverManager;\nuse BookStack\\Exceptions\\ThemeException;\nuse Illuminate\\Console\\Application;\nuse Illuminate\\Console\\Application as Artisan;\nuse Illuminate\\View\\FileViewFinder;\nuse Symfony\\Component\\Console\\Command\\Command;\n\nclass ThemeService\n{\n    /**\n     * @var array<string, callable[]>\n     */\n    protected array $listeners = [];\n\n    /**\n     * @var array<string, ThemeModule>\n     */\n    protected array $modules = [];\n\n    /**\n     * Get the currently configured theme.\n     * Returns an empty string if not configured.\n     */\n    public function getTheme(): string\n    {\n        return config('view.theme') ?? '';\n    }\n\n    /**\n     * Listen to a given custom theme event,\n     * setting up the action to be ran when the event occurs.\n     */\n    public function listen(string $event, callable $action): void\n    {\n        if (!isset($this->listeners[$event])) {\n            $this->listeners[$event] = [];\n        }\n\n        $this->listeners[$event][] = $action;\n    }\n\n    /**\n     * Dispatch the given event name.\n     * Runs any registered listeners for that event name,\n     * passing all additional variables to the listener action.\n     *\n     * If a callback returns a non-null value, this method will\n     * stop and return that value itself.\n     */\n    public function dispatch(string $event, ...$args): mixed\n    {\n        foreach ($this->listeners[$event] ?? [] as $action) {\n            $result = call_user_func_array($action, $args);\n            if (!is_null($result)) {\n                return $result;\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * Check if there are listeners registered for the given event name.\n     */\n    public function hasListeners(string $event): bool\n    {\n        return count($this->listeners[$event] ?? []) > 0;\n    }\n\n    /**\n     * Register a new custom artisan command to be available.\n     */\n    public function registerCommand(Command $command): void\n    {\n        Artisan::starting(function (Application $application) use ($command) {\n            $application->addCommands([$command]);\n        });\n    }\n\n    /**\n     * Read any actions from the 'functions.php' file of the active theme or its modules.\n     */\n    public function readThemeActions(): void\n    {\n        $moduleFunctionFiles = array_map(function (ThemeModule $module): string {\n            return $module->path('functions.php');\n        }, $this->modules);\n        $allFunctionFiles = array_merge(array_values($moduleFunctionFiles), [theme_path('functions.php')]);\n        $filteredFunctionFiles = array_filter($allFunctionFiles, function (string $file): bool {\n            return $file && file_exists($file);\n        });\n\n        foreach ($filteredFunctionFiles as $functionFile) {\n            try {\n                require $functionFile;\n            } catch (\\Error $exception) {\n                throw new ThemeException(\"Failed loading theme functions file at \\\"{$functionFile}\\\" with error: {$exception->getMessage()}\");\n            }\n        }\n    }\n\n    /**\n     * Read the modules folder and load in any valid theme modules.\n     * @throws ThemeModuleException\n     */\n    public function loadModules(): void\n    {\n        $modulesFolder = theme_path('modules');\n        if (!$modulesFolder) {\n            return;\n        }\n\n        $this->modules = (new ThemeModuleManager($modulesFolder))->load();\n    }\n\n    /**\n     * Get all loaded theme modules.\n     * @return array<string, ThemeModule>\n     */\n    public function getModules(): array\n    {\n        return $this->modules;\n    }\n\n    /**\n     * Get a hash to represent the currently loaded modules.\n     */\n    public function getModulesHash(): string\n    {\n        $key = \"\";\n\n        foreach ($this->modules as $module) {\n            $key .= $module->name . ':' . $module->version . ';';\n        }\n\n        return md5($key);\n    }\n\n    /**\n     * Look for a specific file within the theme or its modules.\n     * Returns the first file found or null if not found.\n     */\n    public function findFirstFile(string $path): ?string\n    {\n        $themePath = theme_path($path);\n        if (file_exists($themePath)) {\n            return $themePath;\n        }\n\n        foreach ($this->modules as $module) {\n            $customizedFile = $module->path($path);\n            if (file_exists($customizedFile)) {\n                return $customizedFile;\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * @see SocialDriverManager::addSocialDriver\n     */\n    public function addSocialDriver(string $driverName, array $config, string $socialiteHandler, ?callable $configureForRedirect = null): void\n    {\n        $driverManager = app()->make(SocialDriverManager::class);\n        $driverManager->addSocialDriver($driverName, $config, $socialiteHandler, $configureForRedirect);\n    }\n}\n"
  },
  {
    "path": "app/Theming/ThemeViews.php",
    "content": "<?php\n\nnamespace BookStack\\Theming;\n\nuse BookStack\\Exceptions\\ThemeException;\nuse Illuminate\\View\\FileViewFinder;\n\nclass ThemeViews\n{\n    /**\n     * @var array<string, array<string, int>>\n     */\n    protected array $beforeViews = [];\n\n    /**\n     * @var array<string, array<string, int>>\n     */\n    protected array $afterViews = [];\n\n    public function __construct(\n        protected FileViewFinder $finder\n    ) {\n    }\n\n    /**\n     * Register any extra paths for where we may expect views to be located\n     * with the FileViewFinder, to make custom views available for use.\n     * @param ThemeModule[] $modules\n     */\n    public function registerViewPathsForTheme(array $modules): void\n    {\n        foreach ($modules as $module) {\n            $moduleViewsPath = $module->path('views');\n            if (file_exists($moduleViewsPath) && is_dir($moduleViewsPath)) {\n                $this->finder->prependLocation($moduleViewsPath);\n            }\n        }\n\n        $this->finder->prependLocation(theme_path());\n    }\n\n    /**\n     * Provide the response for a blade template view include.\n     */\n    public function handleViewInclude(string $viewPath, array $data = [], array $mergeData = []): string\n    {\n        if (!$this->hasRegisteredViews()) {\n            return view()->make($viewPath, $data, $mergeData)->render();\n        }\n\n        if (str_contains('book-tree', $viewPath)) {\n            dd($viewPath, $data);\n        }\n\n        $viewsContent = [\n            ...$this->renderViewSets($this->beforeViews[$viewPath] ?? [], $data, $mergeData),\n            view()->make($viewPath, $data, $mergeData)->render(),\n            ...$this->renderViewSets($this->afterViews[$viewPath] ?? [], $data, $mergeData),\n        ];\n\n        return implode(\"\\n\", $viewsContent);\n    }\n\n    /**\n     * Register a custom view to be rendered before the given target view is included in the template system.\n     */\n    public function renderBefore(string $targetView, string $localView, int $priority = 50): void\n    {\n        $this->registerAdjacentView($this->beforeViews, $targetView, $localView, $priority);\n    }\n\n    /**\n     * Register a custom view to be rendered after the given target view is included in the template system.\n     */\n    public function renderAfter(string $targetView, string $localView, int $priority = 50): void\n    {\n        $this->registerAdjacentView($this->afterViews, $targetView, $localView, $priority);\n    }\n\n    public function hasRegisteredViews(): bool\n    {\n        return !empty($this->beforeViews) || !empty($this->afterViews);\n    }\n\n    protected function registerAdjacentView(array &$location, string $targetView, string $localView, int $priority = 50): void\n    {\n        try {\n            $viewPath = $this->finder->find($localView);\n        } catch (\\InvalidArgumentException $exception) {\n            throw new ThemeException(\"Expected registered view file with name \\\"{$localView}\\\" could not be found.\");\n        }\n\n        if (!isset($location[$targetView])) {\n            $location[$targetView] = [];\n        }\n\n        $location[$targetView][$viewPath] = $priority;\n    }\n\n    /**\n     * @param array<string, int> $viewSet\n     * @return string[]\n     */\n    protected function renderViewSets(array $viewSet, array $data, array $mergeData): array\n    {\n        $paths = array_keys($viewSet);\n        usort($paths, function (string $a, string $b) use ($viewSet) {\n            return $viewSet[$a] <=> $viewSet[$b];\n        });\n\n        return array_map(function (string $viewPath) use ($data, $mergeData) {\n            return view()->file($viewPath, $data, $mergeData)->render();\n        }, $paths);\n    }\n}\n"
  },
  {
    "path": "app/Translation/FileLoader.php",
    "content": "<?php\n\nnamespace BookStack\\Translation;\n\nuse BookStack\\Facades\\Theme;\nuse Illuminate\\Translation\\FileLoader as BaseLoader;\n\nclass FileLoader extends BaseLoader\n{\n    /**\n     * Load the messages for the given locale.\n     *\n     * Extends Laravel's translation FileLoader to look in multiple directories\n     * so that we can load in translation overrides from the theme file if wanted.\n     *\n     * @param string      $locale\n     * @param string      $group\n     * @param string|null $namespace\n     *\n     * @return array\n     */\n    public function load($locale, $group, $namespace = null): array\n    {\n        if ($group === '*' && $namespace === '*') {\n            return $this->loadJsonPaths($locale);\n        }\n\n        if (is_null($namespace) || $namespace === '*') {\n            $themePath = theme_path('lang');\n            $themeTranslations = $themePath ? $this->loadPaths([$themePath], $locale, $group) : [];\n\n            $modules = Theme::getModules();\n            $moduleTranslations = [];\n            foreach ($modules as $module) {\n                $modulePath = $module->path('lang');\n                if (file_exists($modulePath)) {\n                    $moduleTranslations = array_merge($moduleTranslations, $this->loadPaths([$modulePath], $locale, $group));\n                }\n            }\n\n            $originalTranslations = $this->loadPaths($this->paths, $locale, $group);\n            return array_merge($originalTranslations, $moduleTranslations, $themeTranslations);\n        }\n\n        return $this->loadNamespaced($locale, $group, $namespace);\n    }\n}\n"
  },
  {
    "path": "app/Translation/LocaleDefinition.php",
    "content": "<?php\n\nnamespace BookStack\\Translation;\n\nclass LocaleDefinition\n{\n    public function __construct(\n        protected string $appName,\n        protected string $isoName,\n        protected bool $isRtl\n    ) {\n    }\n\n    /**\n     * Provide the BookStack-specific locale name.\n     */\n    public function appLocale(): string\n    {\n        return $this->appName;\n    }\n\n    /**\n     * Provide the ISO-aligned locale name.\n     */\n    public function isoLocale(): string\n    {\n        return $this->isoName;\n    }\n\n    /**\n     * Returns a string suitable for the HTML \"lang\" attribute.\n     */\n    public function htmlLang(): string\n    {\n        return str_replace('_', '-', $this->isoName);\n    }\n\n    /**\n     * Returns a string suitable for the HTML \"dir\" attribute.\n     */\n    public function htmlDirection(): string\n    {\n        return $this->isRtl ? 'rtl' : 'ltr';\n    }\n\n    /**\n     * Translate using this locate.\n     */\n    public function trans(string $key, array $replace = []): string\n    {\n        return trans($key, $replace, $this->appLocale());\n    }\n}\n"
  },
  {
    "path": "app/Translation/LocaleManager.php",
    "content": "<?php\n\nnamespace BookStack\\Translation;\n\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Http\\Request;\n\nclass LocaleManager\n{\n    /**\n     * Array of right-to-left locale options.\n     */\n    protected array $rtlLocales = ['ar', 'fa', 'he'];\n\n    /**\n     * Map of BookStack locale names to best-estimate ISO locale names.\n     * Locales can often be found by running `locale -a` on a linux system.\n     *\n     * @var array<string, string>\n     */\n    protected array $localeMap = [\n        'ar'          => 'ar',\n        'bg'          => 'bg_BG',\n        'bn'          => 'bn_BD',\n        'bs'          => 'bs_BA',\n        'ca'          => 'ca',\n        'cs'          => 'cs_CZ',\n        'cy'          => 'cy_GB',\n        'da'          => 'da_DK',\n        'de'          => 'de_DE',\n        'de_informal' => 'de_DE',\n        'el'          => 'el_GR',\n        'en'          => 'en_GB',\n        'es'          => 'es_ES',\n        'es_AR'       => 'es_AR',\n        'et'          => 'et_EE',\n        'eu'          => 'eu_ES',\n        'fa'          => 'fa_IR',\n        'fi'          => 'fi_FI',\n        'fr'          => 'fr_FR',\n        'he'          => 'he_IL',\n        'hr'          => 'hr_HR',\n        'hu'          => 'hu_HU',\n        'id'          => 'id_ID',\n        'is'          => 'is_IS',\n        'it'          => 'it_IT',\n        'ja'          => 'ja',\n        'ka'          => 'ka_GE',\n        'ko'          => 'ko_KR',\n        'ku'          => 'ku_TR',\n        'lt'          => 'lt_LT',\n        'lv'          => 'lv_LV',\n        'ne'          => 'ne_NP',\n        'nb'          => 'nb_NO',\n        'nl'          => 'nl_NL',\n        'nn'          => 'nn_NO',\n        'pl'          => 'pl_PL',\n        'pt'          => 'pt_PT',\n        'pt_BR'       => 'pt_BR',\n        'ro'          => 'ro_RO',\n        'ru'          => 'ru',\n        'sk'          => 'sk_SK',\n        'sl'          => 'sl_SI',\n        'sq'          => 'sq_AL',\n        'sr'          => 'sr_RS',\n        'sv'          => 'sv_SE',\n        'tk'          => 'tk_TM',\n        'tr'          => 'tr_TR',\n        'uk'          => 'uk_UA',\n        'uz'          => 'uz_UZ',\n        'vi'          => 'vi_VN',\n        'zh_CN'       => 'zh_CN',\n        'zh_TW'       => 'zh_TW',\n    ];\n\n    /**\n     * Get the BookStack locale string for the given user.\n     */\n    protected function getLocaleForUser(User $user): string\n    {\n        $default = config('app.default_locale');\n\n        if ($user->isGuest() && config('app.auto_detect_locale')) {\n            return $this->autoDetectLocale(request(), $default);\n        }\n\n        return setting()->getUser($user, 'language', $default);\n    }\n\n    /**\n     * Get a locale definition for the current user.\n     */\n    public function getForUser(User $user): LocaleDefinition\n    {\n        $localeString = $this->getLocaleForUser($user);\n\n        return new LocaleDefinition(\n            $localeString,\n            $this->localeMap[$localeString] ?? $localeString,\n            in_array($localeString, $this->rtlLocales),\n        );\n    }\n\n    /**\n     * Autodetect the visitors locale by matching locales in their headers\n     * against the locales supported by BookStack.\n     */\n    protected function autoDetectLocale(Request $request, string $default): string\n    {\n        $availableLocales = $this->getAllAppLocales();\n\n        foreach ($request->getLanguages() as $lang) {\n            if (in_array($lang, $availableLocales)) {\n                return $lang;\n            }\n        }\n\n        return $default;\n    }\n\n    /**\n     * Get all the available app-specific level locale strings.\n     */\n    public function getAllAppLocales(): array\n    {\n        return array_keys($this->localeMap);\n    }\n}\n"
  },
  {
    "path": "app/Translation/MessageSelector.php",
    "content": "<?php\n\nnamespace BookStack\\Translation;\n\nuse Illuminate\\Translation\\MessageSelector as BaseClass;\n\n/**\n * This is a customization of the default Laravel MessageSelector class to tweak pluralization,\n * so that is uses just the first part of the locale string to provide support with\n * non-standard locales such as \"de_informal\".\n */\nclass MessageSelector extends BaseClass\n{\n    public function getPluralIndex($locale, $number)\n    {\n        $locale = explode('_', $locale)[0];\n        return parent::getPluralIndex($locale, $number);\n    }\n}\n"
  },
  {
    "path": "app/Uploads/Attachment.php",
    "content": "<?php\n\nnamespace BookStack\\Uploads;\n\nuse BookStack\\App\\Model;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Permissions\\Models\\JointPermission;\nuse BookStack\\Permissions\\PermissionApplicator;\nuse BookStack\\Users\\Models\\HasCreatorAndUpdater;\nuse BookStack\\Users\\Models\\OwnableInterface;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\n\n/**\n * @property int    $id\n * @property string $name\n * @property string $path\n * @property string $extension\n * @property ?Page  $page\n * @property bool   $external\n * @property int    $uploaded_to\n * @property User   $updatedBy\n * @property User   $createdBy\n *\n * @method static Entity|Builder visible()\n */\nclass Attachment extends Model implements OwnableInterface\n{\n    use HasCreatorAndUpdater;\n    use HasFactory;\n\n    protected $fillable = ['name', 'order'];\n    protected $hidden = ['path', 'page'];\n    protected $casts = [\n        'external' => 'bool',\n    ];\n\n    /**\n     * Get the downloadable file name for this upload.\n     */\n    public function getFileName(): string\n    {\n        if (str_contains($this->name, '.')) {\n            return $this->name;\n        }\n\n        return $this->name . '.' . $this->extension;\n    }\n\n    /**\n     * Get the page this file was uploaded to.\n     */\n    public function page(): BelongsTo\n    {\n        return $this->belongsTo(Page::class, 'uploaded_to');\n    }\n\n    public function jointPermissions(): HasMany\n    {\n        return $this->hasMany(JointPermission::class, 'entity_id', 'uploaded_to')\n            ->where('joint_permissions.entity_type', '=', 'page');\n    }\n\n    /**\n     * Get the url of this file.\n     */\n    public function getUrl($openInline = false): string\n    {\n        if ($this->external && !str_starts_with($this->path, 'http')) {\n            return $this->path;\n        }\n\n        return url('/attachments/' . $this->id . ($openInline ? '?open=true' : ''));\n    }\n\n    /**\n     * Get the representation of this attachment in a format suitable for the page editors.\n     * Detects and adapts video content to use an inline video embed.\n     */\n    public function editorContent(): array\n    {\n        $videoExtensions = ['mp4', 'webm', 'mkv', 'ogg', 'avi'];\n        if (in_array(strtolower($this->extension), $videoExtensions)) {\n            $html = '<video src=\"' . e($this->getUrl(true)) . '\" controls width=\"480\" height=\"270\"></video>';\n            return ['text/html' => $html, 'text/plain' => $html];\n        }\n\n        return ['text/html' => $this->htmlLink(), 'text/plain' => $this->markdownLink()];\n    }\n\n    /**\n     * Generate the HTML link to this attachment.\n     */\n    public function htmlLink(): string\n    {\n        return '<a target=\"_blank\" href=\"' . e($this->getUrl()) . '\">' . e($this->name) . '</a>';\n    }\n\n    /**\n     * Generate a MarkDown link to this attachment.\n     */\n    public function markdownLink(): string\n    {\n        return '[' . $this->name . '](' . $this->getUrl() . ')';\n    }\n\n    /**\n     * Scope the query to those attachments that are visible based upon related page permissions.\n     */\n    public function scopeVisible(): Builder\n    {\n        $permissions = app()->make(PermissionApplicator::class);\n\n        return $permissions->restrictPageRelationQuery(\n            self::query(),\n            'attachments',\n            'uploaded_to'\n        );\n    }\n}\n"
  },
  {
    "path": "app/Uploads/AttachmentService.php",
    "content": "<?php\n\nnamespace BookStack\\Uploads;\n\nuse BookStack\\Exceptions\\FileUploadException;\nuse Exception;\nuse Symfony\\Component\\HttpFoundation\\File\\UploadedFile;\n\nclass AttachmentService\n{\n    public function __construct(\n        protected FileStorage $storage,\n    ) {\n    }\n\n    /**\n     * Stream an attachment from storage.\n     *\n     * @return resource|null\n     */\n    public function streamAttachmentFromStorage(Attachment $attachment)\n    {\n        return $this->storage->getReadStream($attachment->path);\n    }\n\n    /**\n     * Read the file size of an attachment from storage, in bytes.\n     */\n    public function getAttachmentFileSize(Attachment $attachment): int\n    {\n        return $this->storage->getSize($attachment->path);\n    }\n\n    /**\n     * Store a new attachment upon user upload.\n     *\n     * @throws FileUploadException\n     */\n    public function saveNewUpload(UploadedFile $uploadedFile, int $pageId): Attachment\n    {\n        $attachmentName = $uploadedFile->getClientOriginalName();\n        $attachmentPath = $this->putFileInStorage($uploadedFile);\n        $largestExistingOrder = Attachment::query()->where('uploaded_to', '=', $pageId)->max('order');\n\n        /** @var Attachment $attachment */\n        $attachment = Attachment::query()->forceCreate([\n            'name'        => $attachmentName,\n            'path'        => $attachmentPath,\n            'extension'   => $uploadedFile->getClientOriginalExtension(),\n            'uploaded_to' => $pageId,\n            'created_by'  => user()->id,\n            'updated_by'  => user()->id,\n            'order'       => $largestExistingOrder + 1,\n        ]);\n\n        return $attachment;\n    }\n\n    /**\n     * Store an upload, saving to a file and deleting any existing uploads\n     * attached to that file.\n     *\n     * @throws FileUploadException\n     */\n    public function saveUpdatedUpload(UploadedFile $uploadedFile, Attachment $attachment): Attachment\n    {\n        if (!$attachment->external) {\n            $this->deleteFileInStorage($attachment);\n        }\n\n        $attachmentName = $uploadedFile->getClientOriginalName();\n        $attachmentPath = $this->putFileInStorage($uploadedFile);\n\n        $attachment->name = $attachmentName;\n        $attachment->path = $attachmentPath;\n        $attachment->external = false;\n        $attachment->extension = $uploadedFile->getClientOriginalExtension();\n        $attachment->save();\n\n        return $attachment;\n    }\n\n    /**\n     * Save a new File attachment from a given link and name.\n     */\n    public function saveNewFromLink(string $name, string $link, int $page_id): Attachment\n    {\n        $largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');\n\n        return Attachment::forceCreate([\n            'name'        => $name,\n            'path'        => $link,\n            'external'    => true,\n            'extension'   => '',\n            'uploaded_to' => $page_id,\n            'created_by'  => user()->id,\n            'updated_by'  => user()->id,\n            'order'       => $largestExistingOrder + 1,\n        ]);\n    }\n\n    /**\n     * Updates the ordering for a listing of attached files.\n     */\n    public function updateFileOrderWithinPage(array $attachmentOrder, string $pageId)\n    {\n        foreach ($attachmentOrder as $index => $attachmentId) {\n            Attachment::query()->where('uploaded_to', '=', $pageId)\n                ->where('id', '=', $attachmentId)\n                ->update(['order' => $index]);\n        }\n    }\n\n    /**\n     * Update the details of a file.\n     */\n    public function updateFile(Attachment $attachment, array $requestData): Attachment\n    {\n        if (isset($requestData['name'])) {\n            $attachment->name = $requestData['name'];\n        }\n\n        $link = trim($requestData['link'] ?? '');\n        if (!empty($link)) {\n            if (!$attachment->external) {\n                $this->deleteFileInStorage($attachment);\n                $attachment->external = true;\n                $attachment->extension = '';\n            }\n            $attachment->path = $link;\n        }\n\n        $attachment->save();\n\n        return $attachment->refresh();\n    }\n\n    /**\n     * Delete a File from the database and storage.\n     *\n     * @throws Exception\n     */\n    public function deleteFile(Attachment $attachment)\n    {\n        if (!$attachment->external) {\n            $this->deleteFileInStorage($attachment);\n        }\n\n        $attachment->delete();\n    }\n\n    /**\n     * Delete a file from the filesystem it sits on.\n     * Cleans any empty leftover folders.\n     */\n    public function deleteFileInStorage(Attachment $attachment): void\n    {\n        $this->storage->delete($attachment->path);\n    }\n\n    /**\n     * Store a file in storage with the given filename.\n     *\n     * @throws FileUploadException\n     */\n    protected function putFileInStorage(UploadedFile $uploadedFile): string\n    {\n        $basePath = 'uploads/files/' . date('Y-m-M') . '/';\n\n        return $this->storage->uploadFile(\n            $uploadedFile,\n            $basePath,\n            $uploadedFile->getClientOriginalExtension(),\n            ''\n        );\n    }\n\n    /**\n     * Get the file validation rules for attachments.\n     */\n    public static function getFileValidationRules(): array\n    {\n        return ['file', 'max:' . (config('app.upload_limit') * 1000)];\n    }\n}\n"
  },
  {
    "path": "app/Uploads/Controllers/AttachmentApiController.php",
    "content": "<?php\n\nnamespace BookStack\\Uploads\\Controllers;\n\nuse BookStack\\Entities\\EntityExistsRule;\nuse BookStack\\Entities\\Queries\\PageQueries;\nuse BookStack\\Exceptions\\FileUploadException;\nuse BookStack\\Http\\ApiController;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Uploads\\Attachment;\nuse BookStack\\Uploads\\AttachmentService;\nuse Exception;\nuse Illuminate\\Contracts\\Filesystem\\FileNotFoundException;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Validation\\ValidationException;\n\nclass AttachmentApiController extends ApiController\n{\n    public function __construct(\n        protected AttachmentService $attachmentService,\n        protected PageQueries $pageQueries,\n    ) {\n    }\n\n    /**\n     * Get a listing of attachments visible to the user.\n     * The external property indicates whether the attachment is simple a link.\n     * A false value for the external property would indicate a file upload.\n     */\n    public function list()\n    {\n        return $this->apiListingResponse(Attachment::visible(), [\n            'id', 'name', 'extension', 'uploaded_to', 'external', 'order', 'created_at', 'updated_at', 'created_by', 'updated_by',\n        ]);\n    }\n\n    /**\n     * Create a new attachment in the system.\n     * An uploaded_to value must be provided containing an ID of the page\n     * that this upload will be related to.\n     *\n     * If you're uploading a file the POST data should be provided via\n     * a multipart/form-data type request instead of JSON.\n     *\n     * @throws ValidationException\n     * @throws FileUploadException\n     */\n    public function create(Request $request)\n    {\n        $this->checkPermission(Permission::AttachmentCreateAll);\n        $requestData = $this->validate($request, $this->rules()['create']);\n\n        $pageId = $request->get('uploaded_to');\n        $page = $this->pageQueries->findVisibleByIdOrFail($pageId);\n        $this->checkOwnablePermission(Permission::PageUpdate, $page);\n\n        if ($request->hasFile('file')) {\n            $uploadedFile = $request->file('file');\n            $attachment = $this->attachmentService->saveNewUpload($uploadedFile, $page->id);\n        } else {\n            $attachment = $this->attachmentService->saveNewFromLink(\n                $requestData['name'],\n                $requestData['link'],\n                $page->id\n            );\n        }\n\n        $this->attachmentService->updateFile($attachment, $requestData);\n\n        return response()->json($attachment);\n    }\n\n    /**\n     * Get the details & content of a single attachment of the given ID.\n     * The attachment link or file content is provided via a 'content' property.\n     * For files the content will be base64 encoded.\n     *\n     * @throws FileNotFoundException\n     */\n    public function read(string $id)\n    {\n        /** @var Attachment $attachment */\n        $attachment = Attachment::visible()\n            ->with(['createdBy', 'updatedBy'])\n            ->findOrFail($id);\n\n        $attachment->setAttribute('links', [\n            'html'     => $attachment->htmlLink(),\n            'markdown' => $attachment->markdownLink(),\n        ]);\n\n        // Simply return a JSON response of the attachment for link-based attachments\n        if ($attachment->external) {\n            $attachment->setAttribute('content', $attachment->path);\n\n            return response()->json($attachment);\n        }\n\n        // Build and split our core JSON, at point of content.\n        $splitter = 'CONTENT_SPLIT_LOCATION_' . time() . '_' . rand(1, 40000);\n        $attachment->setAttribute('content', $splitter);\n        $json = $attachment->toJson();\n        $jsonParts = explode($splitter, $json);\n        // Get a stream for the file data from storage\n        $stream = $this->attachmentService->streamAttachmentFromStorage($attachment);\n\n        return response()->stream(function () use ($jsonParts, $stream) {\n            // Output the pre-content JSON data\n            echo $jsonParts[0];\n\n            // Stream out our attachment data as base64 content\n            stream_filter_append($stream, 'convert.base64-encode', STREAM_FILTER_READ);\n            fpassthru($stream);\n            fclose($stream);\n\n            // Output our post-content JSON data\n            echo $jsonParts[1];\n        }, 200, ['Content-Type' => 'application/json']);\n    }\n\n    /**\n     * Update the details of a single attachment.\n     * As per the create endpoint, if a file is being provided as the attachment content\n     * the request should be formatted as a multipart/form-data request instead of JSON.\n     *\n     * @throws ValidationException\n     * @throws FileUploadException\n     */\n    public function update(Request $request, string $id)\n    {\n        $requestData = $this->validate($request, $this->rules()['update']);\n        /** @var Attachment $attachment */\n        $attachment = Attachment::visible()->findOrFail($id);\n\n        $page = $attachment->page;\n        if ($requestData['uploaded_to'] ?? false) {\n            $pageId = $request->get('uploaded_to');\n            $page = $this->pageQueries->findVisibleByIdOrFail($pageId);\n            $attachment->uploaded_to = $requestData['uploaded_to'];\n        }\n\n        $this->checkOwnablePermission(Permission::PageView, $page);\n        $this->checkOwnablePermission(Permission::PageUpdate, $page);\n        $this->checkOwnablePermission(Permission::AttachmentUpdate, $attachment);\n\n        if ($request->hasFile('file')) {\n            $uploadedFile = $request->file('file');\n            $attachment = $this->attachmentService->saveUpdatedUpload($uploadedFile, $attachment);\n        }\n\n        $this->attachmentService->updateFile($attachment, $requestData);\n\n        return response()->json($attachment);\n    }\n\n    /**\n     * Delete an attachment of the given ID.\n     *\n     * @throws Exception\n     */\n    public function delete(string $id)\n    {\n        /** @var Attachment $attachment */\n        $attachment = Attachment::visible()->findOrFail($id);\n        $this->checkOwnablePermission(Permission::AttachmentDelete, $attachment);\n\n        $this->attachmentService->deleteFile($attachment);\n\n        return response('', 204);\n    }\n\n    protected function rules(): array\n    {\n        return [\n            'create' => [\n                'name'        => ['required', 'string', 'min:1', 'max:255'],\n                'uploaded_to' => ['required', 'integer', new EntityExistsRule('page')],\n                'file'        => array_merge(['required_without:link'], $this->attachmentService->getFileValidationRules()),\n                'link'        => ['required_without:file', 'string', 'min:1', 'max:2000', 'safe_url'],\n            ],\n            'update' => [\n                'name'        => ['string', 'min:1', 'max:255'],\n                'uploaded_to' => ['integer', new EntityExistsRule('page')],\n                'file'        => $this->attachmentService->getFileValidationRules(),\n                'link'        => ['string', 'min:1', 'max:2000', 'safe_url'],\n            ],\n        ];\n    }\n}\n"
  },
  {
    "path": "app/Uploads/Controllers/AttachmentController.php",
    "content": "<?php\n\nnamespace BookStack\\Uploads\\Controllers;\n\nuse BookStack\\Entities\\EntityExistsRule;\nuse BookStack\\Entities\\Queries\\PageQueries;\nuse BookStack\\Entities\\Repos\\PageRepo;\nuse BookStack\\Exceptions\\FileUploadException;\nuse BookStack\\Exceptions\\NotFoundException;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Uploads\\Attachment;\nuse BookStack\\Uploads\\AttachmentService;\nuse Exception;\nuse Illuminate\\Contracts\\Filesystem\\FileNotFoundException;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\MessageBag;\nuse Illuminate\\Validation\\ValidationException;\n\nclass AttachmentController extends Controller\n{\n    public function __construct(\n        protected AttachmentService $attachmentService,\n        protected PageQueries $pageQueries,\n        protected PageRepo $pageRepo\n    ) {\n    }\n\n    /**\n     * Endpoint at which attachments are uploaded to.\n     *\n     * @throws ValidationException\n     * @throws NotFoundException\n     */\n    public function upload(Request $request)\n    {\n        $this->validate($request, [\n            'uploaded_to' => ['required', 'integer',  new EntityExistsRule('page')],\n            'file'        => array_merge(['required'], $this->attachmentService->getFileValidationRules()),\n        ]);\n\n        $pageId = $request->get('uploaded_to');\n        $page = $this->pageQueries->findVisibleByIdOrFail($pageId);\n\n        $this->checkPermission(Permission::AttachmentCreateAll);\n        $this->checkOwnablePermission(Permission::PageUpdate, $page);\n\n        $uploadedFile = $request->file('file');\n\n        try {\n            $attachment = $this->attachmentService->saveNewUpload($uploadedFile, $pageId);\n        } catch (FileUploadException $e) {\n            return response($e->getMessage(), 500);\n        }\n\n        return response()->json($attachment);\n    }\n\n    /**\n     * Update an uploaded attachment.\n     *\n     * @throws ValidationException\n     */\n    public function uploadUpdate(Request $request, $attachmentId)\n    {\n        $this->validate($request, [\n            'file' => array_merge(['required'], $this->attachmentService->getFileValidationRules()),\n        ]);\n\n        /** @var Attachment $attachment */\n        $attachment = Attachment::query()->findOrFail($attachmentId);\n        $this->checkOwnablePermission(Permission::PageView, $attachment->page);\n        $this->checkOwnablePermission(Permission::PageUpdate, $attachment->page);\n        $this->checkOwnablePermission(Permission::AttachmentUpdate, $attachment);\n\n        $uploadedFile = $request->file('file');\n\n        try {\n            $attachment = $this->attachmentService->saveUpdatedUpload($uploadedFile, $attachment);\n        } catch (FileUploadException $e) {\n            return response($e->getMessage(), 500);\n        }\n\n        return response()->json($attachment);\n    }\n\n    /**\n     * Get the update form for an attachment.\n     */\n    public function getUpdateForm(string $attachmentId)\n    {\n        /** @var Attachment $attachment */\n        $attachment = Attachment::query()->findOrFail($attachmentId);\n\n        $this->checkOwnablePermission(Permission::PageUpdate, $attachment->page);\n        $this->checkOwnablePermission(Permission::AttachmentCreate, $attachment);\n\n        return view('attachments.manager-edit-form', [\n            'attachment' => $attachment,\n        ]);\n    }\n\n    /**\n     * Update the details of an existing file.\n     */\n    public function update(Request $request, string $attachmentId)\n    {\n        /** @var Attachment $attachment */\n        $attachment = Attachment::query()->findOrFail($attachmentId);\n\n        try {\n            $this->validate($request, [\n                'attachment_edit_name' => ['required', 'string', 'min:1', 'max:255'],\n                'attachment_edit_url'  => ['string', 'min:1', 'max:2000', 'safe_url'],\n            ]);\n        } catch (ValidationException $exception) {\n            return response()->view('attachments.manager-edit-form', array_merge($request->only(['attachment_edit_name', 'attachment_edit_url']), [\n                'attachment' => $attachment,\n                'errors'     => new MessageBag($exception->errors()),\n            ]), 422);\n        }\n\n        $this->checkOwnablePermission(Permission::PageView, $attachment->page);\n        $this->checkOwnablePermission(Permission::PageUpdate, $attachment->page);\n        $this->checkOwnablePermission(Permission::AttachmentUpdate, $attachment);\n\n        $attachment = $this->attachmentService->updateFile($attachment, [\n            'name' => $request->get('attachment_edit_name'),\n            'link' => $request->get('attachment_edit_url'),\n        ]);\n\n        return view('attachments.manager-edit-form', [\n            'attachment' => $attachment,\n        ]);\n    }\n\n    /**\n     * Attach a link to a page.\n     *\n     * @throws NotFoundException\n     */\n    public function attachLink(Request $request)\n    {\n        $pageId = $request->get('attachment_link_uploaded_to');\n\n        try {\n            $this->validate($request, [\n                'attachment_link_uploaded_to' => ['required', 'integer',  new EntityExistsRule('page')],\n                'attachment_link_name'        => ['required', 'string', 'min:1', 'max:255'],\n                'attachment_link_url'         => ['required', 'string', 'min:1', 'max:2000', 'safe_url'],\n            ]);\n        } catch (ValidationException $exception) {\n            return response()->view('attachments.manager-link-form', array_merge($request->only(['attachment_link_name', 'attachment_link_url']), [\n                'pageId' => $pageId,\n                'errors' => new MessageBag($exception->errors()),\n            ]), 422);\n        }\n\n        $page = $this->pageQueries->findVisibleByIdOrFail($pageId);\n\n        $this->checkPermission(Permission::AttachmentCreateAll);\n        $this->checkOwnablePermission(Permission::PageUpdate, $page);\n\n        $attachmentName = $request->get('attachment_link_name');\n        $link = $request->get('attachment_link_url');\n        $this->attachmentService->saveNewFromLink($attachmentName, $link, intval($pageId));\n\n        return view('attachments.manager-link-form', [\n            'pageId' => $pageId,\n        ]);\n    }\n\n    /**\n     * Get the attachments for a specific page.\n     *\n     * @throws NotFoundException\n     */\n    public function listForPage(int $pageId)\n    {\n        $page = $this->pageQueries->findVisibleByIdOrFail($pageId);\n\n        return view('attachments.manager-list', [\n            'attachments' => $page->attachments->all(),\n        ]);\n    }\n\n    /**\n     * Update the attachment sorting.\n     *\n     * @throws ValidationException\n     * @throws NotFoundException\n     */\n    public function sortForPage(Request $request, int $pageId)\n    {\n        $this->validate($request, [\n            'order' => ['required', 'array'],\n        ]);\n        $page = $this->pageQueries->findVisibleByIdOrFail($pageId);\n        $this->checkOwnablePermission(Permission::PageUpdate, $page);\n\n        $attachmentOrder = $request->get('order');\n        $this->attachmentService->updateFileOrderWithinPage($attachmentOrder, $pageId);\n\n        return response()->json(['message' => trans('entities.attachments_order_updated')]);\n    }\n\n    /**\n     * Get an attachment from storage.\n     *\n     * @throws FileNotFoundException\n     * @throws NotFoundException\n     */\n    public function get(Request $request, string $attachmentId)\n    {\n        /** @var Attachment $attachment */\n        $attachment = Attachment::query()->findOrFail($attachmentId);\n\n        try {\n            $page = $this->pageQueries->findVisibleByIdOrFail($attachment->uploaded_to);\n        } catch (NotFoundException $exception) {\n            throw new NotFoundException(trans('errors.attachment_not_found'));\n        }\n\n        $this->checkOwnablePermission(Permission::PageView, $page);\n\n        if ($attachment->external) {\n            return redirect($attachment->path);\n        }\n\n        $fileName = $attachment->getFileName();\n        $attachmentStream = $this->attachmentService->streamAttachmentFromStorage($attachment);\n        $attachmentSize = $this->attachmentService->getAttachmentFileSize($attachment);\n\n        if ($request->get('open') === 'true') {\n            return $this->download()->streamedInline($attachmentStream, $fileName, $attachmentSize);\n        }\n\n        return $this->download()->streamedDirectly($attachmentStream, $fileName, $attachmentSize);\n    }\n\n    /**\n     * Delete a specific attachment in the system.\n     *\n     * @throws Exception\n     */\n    public function delete(string $attachmentId)\n    {\n        /** @var Attachment $attachment */\n        $attachment = Attachment::query()->findOrFail($attachmentId);\n        $this->checkOwnablePermission(Permission::AttachmentDelete, $attachment);\n        $this->attachmentService->deleteFile($attachment);\n\n        return response()->json(['message' => trans('entities.attachments_deleted')]);\n    }\n}\n"
  },
  {
    "path": "app/Uploads/Controllers/DrawioImageController.php",
    "content": "<?php\n\nnamespace BookStack\\Uploads\\Controllers;\n\nuse BookStack\\Exceptions\\ImageUploadException;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Uploads\\ImageRepo;\nuse BookStack\\Uploads\\ImageResizer;\nuse BookStack\\Util\\OutOfMemoryHandler;\nuse Exception;\nuse Illuminate\\Http\\Request;\n\nclass DrawioImageController extends Controller\n{\n    public function __construct(\n        protected ImageRepo $imageRepo\n    ) {\n    }\n\n    /**\n     * Get a list of gallery images, in a list.\n     * Can be paged and filtered by entity.\n     */\n    public function list(Request $request, ImageResizer $resizer)\n    {\n        $page = $request->get('page', 1);\n        $searchTerm = $request->get('search', null);\n        $uploadedToFilter = $request->get('uploaded_to', null);\n        $parentTypeFilter = $request->get('filter_type', null);\n\n        $imgData = $this->imageRepo->getEntityFiltered('drawio', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm);\n        $viewData = [\n            'warning' => '',\n            'images'  => $imgData['images'],\n            'hasMore' => $imgData['has_more'],\n        ];\n\n        new OutOfMemoryHandler(function () use ($viewData) {\n            $viewData['warning'] = trans('errors.image_gallery_thumbnail_memory_limit');\n            return response()->view('pages.parts.image-manager-list', $viewData, 200);\n        });\n\n        $resizer->loadGalleryThumbnailsForMany($imgData['images']);\n\n        return view('pages.parts.image-manager-list', $viewData);\n    }\n\n    /**\n     * Store a new gallery image in the system.\n     *\n     * @throws Exception\n     */\n    public function create(Request $request)\n    {\n        $this->validate($request, [\n            'image'       => ['required', 'string'],\n            'uploaded_to' => ['required', 'integer'],\n        ]);\n\n        $this->checkPermission(Permission::ImageCreateAll);\n        $imageBase64Data = $request->get('image');\n\n        try {\n            $uploadedTo = $request->get('uploaded_to', 0);\n            $image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo);\n        } catch (ImageUploadException $e) {\n            return response($e->getMessage(), 500);\n        }\n\n        return response()->json($image);\n    }\n\n    /**\n     * Get the content of an image based64 encoded.\n     */\n    public function getAsBase64($id)\n    {\n        try {\n            $image = $this->imageRepo->getById($id);\n        } catch (Exception $exception) {\n            return $this->jsonError(trans('errors.drawing_data_not_found'), 404);\n        }\n\n        if ($image->type !== 'drawio' || !userCan(Permission::PageView, $image->getPage())) {\n            return $this->jsonError(trans('errors.drawing_data_not_found'), 404);\n        }\n\n        $imageData = $this->imageRepo->getImageData($image);\n        if (is_null($imageData)) {\n            return $this->jsonError(trans('errors.drawing_data_not_found'), 404);\n        }\n\n        return response()->json([\n            'content' => base64_encode($imageData),\n        ]);\n    }\n}\n"
  },
  {
    "path": "app/Uploads/Controllers/GalleryImageController.php",
    "content": "<?php\n\nnamespace BookStack\\Uploads\\Controllers;\n\nuse BookStack\\Exceptions\\ImageUploadException;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Uploads\\ImageRepo;\nuse BookStack\\Uploads\\ImageResizer;\nuse BookStack\\Util\\OutOfMemoryHandler;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Validation\\ValidationException;\n\nclass GalleryImageController extends Controller\n{\n    public function __construct(\n        protected ImageRepo $imageRepo\n    ) {\n    }\n\n    /**\n     * Get a list of gallery images, in a list.\n     * Can be paged and filtered by entity.\n     */\n    public function list(Request $request, ImageResizer $resizer)\n    {\n        $page = $request->get('page', 1);\n        $searchTerm = $request->get('search', null);\n        $uploadedToFilter = $request->get('uploaded_to', null);\n        $parentTypeFilter = $request->get('filter_type', null);\n\n        $imgData = $this->imageRepo->getEntityFiltered('gallery', $parentTypeFilter, $page, 30, $uploadedToFilter, $searchTerm);\n        $viewData = [\n            'warning' => '',\n            'images'  => $imgData['images'],\n            'hasMore' => $imgData['has_more'],\n        ];\n\n        new OutOfMemoryHandler(function () use ($viewData) {\n            $viewData['warning'] = trans('errors.image_gallery_thumbnail_memory_limit');\n            return response()->view('pages.parts.image-manager-list', $viewData, 200);\n        });\n\n        $resizer->loadGalleryThumbnailsForMany($imgData['images']);\n\n        return view('pages.parts.image-manager-list', $viewData);\n    }\n\n    /**\n     * Store a new gallery image in the system.\n     *\n     * @throws ValidationException\n     */\n    public function create(Request $request)\n    {\n        $this->checkPermission(Permission::ImageCreateAll);\n\n        try {\n            $this->validate($request, [\n                'file' => $this->getImageValidationRules(),\n            ]);\n        } catch (ValidationException $exception) {\n            return $this->jsonError(implode(\"\\n\", $exception->errors()['file']));\n        }\n\n        new OutOfMemoryHandler(function () {\n            return $this->jsonError(trans('errors.image_upload_memory_limit'));\n        });\n\n        try {\n            $imageUpload = $request->file('file');\n            $uploadedTo = $request->get('uploaded_to', 0);\n            $image = $this->imageRepo->saveNew($imageUpload, 'gallery', $uploadedTo);\n        } catch (ImageUploadException $e) {\n            return response($e->getMessage(), 500);\n        }\n\n        return response()->json($image);\n    }\n}\n"
  },
  {
    "path": "app/Uploads/Controllers/ImageController.php",
    "content": "<?php\n\nnamespace BookStack\\Uploads\\Controllers;\n\nuse BookStack\\Exceptions\\ImageUploadException;\nuse BookStack\\Exceptions\\NotFoundException;\nuse BookStack\\Exceptions\\NotifyException;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Uploads\\Image;\nuse BookStack\\Uploads\\ImageRepo;\nuse BookStack\\Uploads\\ImageResizer;\nuse BookStack\\Uploads\\ImageService;\nuse BookStack\\Util\\OutOfMemoryHandler;\nuse Exception;\nuse Illuminate\\Http\\Request;\n\nclass ImageController extends Controller\n{\n    public function __construct(\n        protected ImageRepo $imageRepo,\n        protected ImageService $imageService,\n        protected ImageResizer $imageResizer,\n    ) {\n    }\n\n    /**\n     * Provide an image file from storage.\n     *\n     * @throws NotFoundException\n     */\n    public function showImage(string $path)\n    {\n        if (!$this->imageService->pathAccessibleInLocalSecure($path)) {\n            throw (new NotFoundException(trans('errors.image_not_found')))\n                ->setSubtitle(trans('errors.image_not_found_subtitle'))\n                ->setDetails(trans('errors.image_not_found_details'));\n        }\n\n        return $this->imageService->streamImageFromStorageResponse('gallery', $path);\n    }\n\n    /**\n     * Update image details.\n     */\n    public function update(Request $request, string $id)\n    {\n        $data = $this->validate($request, [\n            'name' => ['required', 'min:2', 'string'],\n        ]);\n\n        $image = $this->imageRepo->getById($id);\n        $this->checkImagePermission($image);\n        $this->checkOwnablePermission(Permission::ImageUpdate, $image);\n\n        $image = $this->imageRepo->updateImageDetails($image, $data);\n\n        return view('pages.parts.image-manager-form', [\n            'image'          => $image,\n            'dependantPages' => null,\n        ]);\n    }\n\n    /**\n     * Update the file for an existing image.\n     */\n    public function updateFile(Request $request, string $id)\n    {\n        $this->validate($request, [\n            'file' => ['required', 'file', ...$this->getImageValidationRules()],\n        ]);\n\n        $image = $this->imageRepo->getById($id);\n        $this->checkImagePermission($image);\n        $this->checkOwnablePermission(Permission::ImageUpdate, $image);\n        $file = $request->file('file');\n\n        new OutOfMemoryHandler(function () {\n            return $this->jsonError(trans('errors.image_upload_memory_limit'));\n        });\n\n        try {\n            $this->imageRepo->updateImageFile($image, $file);\n        } catch (ImageUploadException $exception) {\n            return $this->jsonError($exception->getMessage());\n        }\n\n        return response('');\n    }\n\n    /**\n     * Get the form for editing the given image.\n     *\n     * @throws Exception\n     */\n    public function edit(Request $request, string $id)\n    {\n        $image = $this->imageRepo->getById($id);\n        $this->checkImagePermission($image);\n\n        if ($request->has('delete')) {\n            $dependantPages = $this->imageRepo->getPagesUsingImage($image);\n        }\n\n        $viewData = [\n            'image'          => $image,\n            'dependantPages' => $dependantPages ?? null,\n            'warning'        => '',\n        ];\n\n        new OutOfMemoryHandler(function () use ($viewData) {\n            $viewData['warning'] = trans('errors.image_thumbnail_memory_limit');\n            return response()->view('pages.parts.image-manager-form', $viewData);\n        });\n\n        $this->imageResizer->loadGalleryThumbnailsForImage($image, false);\n\n        return view('pages.parts.image-manager-form', $viewData);\n    }\n\n    /**\n     * Deletes an image and all thumbnail/image files.\n     *\n     * @throws Exception\n     */\n    public function destroy(string $id)\n    {\n        $image = $this->imageRepo->getById($id);\n        $this->checkOwnablePermission(Permission::ImageDelete, $image);\n        $this->checkImagePermission($image);\n\n        $this->imageRepo->destroyImage($image);\n\n        return response('');\n    }\n\n    /**\n     * Rebuild the thumbnails for the given image.\n     */\n    public function rebuildThumbnails(string $id)\n    {\n        $image = $this->imageRepo->getById($id);\n        $this->checkImagePermission($image);\n        $this->checkOwnablePermission(Permission::ImageUpdate, $image);\n\n        new OutOfMemoryHandler(function () {\n            return $this->jsonError(trans('errors.image_thumbnail_memory_limit'));\n        });\n\n        $this->imageResizer->loadGalleryThumbnailsForImage($image, true);\n\n        return response(trans('components.image_rebuild_thumbs_success'));\n    }\n\n    /**\n     * Check related page permission and ensure type is drawio or gallery.\n     * @throws NotifyException\n     */\n    protected function checkImagePermission(Image $image): void\n    {\n        if ($image->type !== 'drawio' && $image->type !== 'gallery') {\n            $this->showPermissionError();\n        }\n\n        $relatedPage = $image->getPage();\n        if ($relatedPage) {\n            $this->checkOwnablePermission(Permission::PageView, $relatedPage);\n        }\n    }\n}\n"
  },
  {
    "path": "app/Uploads/Controllers/ImageGalleryApiController.php",
    "content": "<?php\n\nnamespace BookStack\\Uploads\\Controllers;\n\nuse BookStack\\Entities\\Queries\\PageQueries;\nuse BookStack\\Exceptions\\NotFoundException;\nuse BookStack\\Http\\ApiController;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Uploads\\Image;\nuse BookStack\\Uploads\\ImageRepo;\nuse BookStack\\Uploads\\ImageResizer;\nuse BookStack\\Uploads\\ImageService;\nuse Illuminate\\Http\\Request;\n\nclass ImageGalleryApiController extends ApiController\n{\n    protected array $fieldsToExpose = [\n        'id', 'name', 'url', 'path', 'type', 'uploaded_to', 'created_by', 'updated_by',  'created_at', 'updated_at',\n    ];\n\n    public function __construct(\n        protected ImageRepo $imageRepo,\n        protected ImageResizer $imageResizer,\n        protected PageQueries $pageQueries,\n        protected ImageService $imageService,\n    ) {\n    }\n\n    protected function rules(): array\n    {\n        return [\n            'create' => [\n                'type'  => ['required', 'string', 'in:gallery,drawio'],\n                'uploaded_to' => ['required', 'integer'],\n                'image' => ['required', 'file', ...$this->getImageValidationRules()],\n                'name'  => ['string', 'max:180'],\n            ],\n            'readDataForUrl' => [\n                'url' => ['required', 'string', 'url'],\n            ],\n            'update' => [\n                'name'  => ['string', 'max:180'],\n                'image' => ['file', ...$this->getImageValidationRules()],\n            ]\n        ];\n    }\n\n    /**\n     * Get a listing of images in the system. Includes gallery (page content) images and drawings.\n     * Requires visibility of the page they're originally uploaded to.\n     */\n    public function list()\n    {\n        $images = Image::query()->scopes(['visible'])\n            ->select($this->fieldsToExpose)\n            ->whereIn('type', ['gallery', 'drawio']);\n\n        return $this->apiListingResponse($images, [\n            ...$this->fieldsToExpose\n        ]);\n    }\n\n    /**\n     * Create a new image in the system.\n     *\n     * Since \"image\" is expected to be a file, this needs to be a 'multipart/form-data' type request.\n     * The provided \"uploaded_to\" should be an existing page ID in the system.\n     *\n     * If the \"name\" parameter is omitted, the filename of the provided image file will be used instead.\n     * The \"type\" parameter should be 'gallery' for page content images, and 'drawio' should only be used\n     * when the file is a PNG file with diagrams.net image data embedded within.\n     */\n    public function create(Request $request)\n    {\n        $this->checkPermission(Permission::ImageCreateAll);\n        $data = $this->validate($request, $this->rules()['create']);\n        $page = $this->pageQueries->findVisibleByIdOrFail($data['uploaded_to']);\n\n        $image = $this->imageRepo->saveNew($data['image'], $data['type'], $page->id);\n\n        if (isset($data['name'])) {\n            $image->refresh();\n            $image->update(['name' => $data['name']]);\n        }\n\n        return response()->json($this->formatForSingleResponse($image));\n    }\n\n    /**\n     * View the details of a single image.\n     * The \"thumbs\" response property contains links to scaled variants that BookStack may use in its UI.\n     * The \"content\" response property provides HTML and Markdown content, in the format that BookStack\n     * would typically use by default to add the image in page content, as a convenience.\n     * Actual image file data is not provided but can be fetched via the \"url\" response property or by\n     * using the \"read-data\" endpoint.\n     */\n    public function read(string $id)\n    {\n        $image = Image::query()->scopes(['visible'])->findOrFail($id);\n\n        return response()->json($this->formatForSingleResponse($image));\n    }\n\n    /**\n     * Read the image file data for a single image in the system.\n     * The returned response will be a stream of image data instead of a JSON response.\n     */\n    public function readData(string $id)\n    {\n        $image = Image::query()->scopes(['visible'])->findOrFail($id);\n\n        return $this->imageService->streamImageFromStorageResponse('gallery', $image->path);\n    }\n\n    /**\n     * Read the image file data for a single image in the system, using the provided URL\n     * to identify the image instead of its ID, which is provided as a \"URL\" query parameter.\n     * The returned response will be a stream of image data instead of a JSON response.\n     */\n    public function readDataForUrl(Request $request)\n    {\n        $data = $this->validate($request, $this->rules()['readDataForUrl']);\n        $basePath = url('/uploads/images/');\n        $imagePath = str_replace($basePath, '', $data['url']);\n\n        if (!$this->imageService->pathAccessible($imagePath)) {\n            throw (new NotFoundException(trans('errors.image_not_found')))\n                ->setSubtitle(trans('errors.image_not_found_subtitle'))\n                ->setDetails(trans('errors.image_not_found_details'));\n        }\n\n        return $this->imageService->streamImageFromStorageResponse('gallery', $imagePath);\n    }\n\n    /**\n     * Update the details of an existing image in the system.\n     * Since \"image\" is expected to be a file, this needs to be a 'multipart/form-data' type request if providing a\n     * new image file. Updated image files should be of the same file type as the original image.\n     */\n    public function update(Request $request, string $id)\n    {\n        $data = $this->validate($request, $this->rules()['update']);\n        $image = $this->imageRepo->getById($id);\n        $this->checkOwnablePermission(Permission::PageView, $image->getPage());\n        $this->checkOwnablePermission(Permission::ImageUpdate, $image);\n\n        $this->imageRepo->updateImageDetails($image, $data);\n        if (isset($data['image'])) {\n            $this->imageRepo->updateImageFile($image, $data['image']);\n        }\n\n        return response()->json($this->formatForSingleResponse($image));\n    }\n\n    /**\n     * Delete an image from the system.\n     * Will also delete thumbnails for the image.\n     * Does not check or handle image usage so this could leave pages with broken image references.\n     */\n    public function delete(string $id)\n    {\n        $image = $this->imageRepo->getById($id);\n        $this->checkOwnablePermission(Permission::PageView, $image->getPage());\n        $this->checkOwnablePermission(Permission::ImageDelete, $image);\n        $this->imageRepo->destroyImage($image);\n\n        return response('', 204);\n    }\n\n    /**\n     * Format the given image model for single-result display.\n     */\n    protected function formatForSingleResponse(Image $image): array\n    {\n        $this->imageResizer->loadGalleryThumbnailsForImage($image, false);\n        $data = $image->toArray();\n        $data['created_by'] = $image->createdBy;\n        $data['updated_by'] = $image->updatedBy;\n        $data['content'] = [];\n\n        $escapedUrl = htmlentities($image->url);\n        $escapedName = htmlentities($image->name);\n\n        if ($image->type === 'drawio') {\n            $data['content']['html'] = \"<div drawio-diagram=\\\"{$image->id}\\\"><img src=\\\"{$escapedUrl}\\\"></div>\";\n            $data['content']['markdown'] = $data['content']['html'];\n        } else {\n            $escapedDisplayThumb = htmlentities($image->getAttribute('thumbs')['display']);\n            $data['content']['html'] = \"<a href=\\\"{$escapedUrl}\\\" target=\\\"_blank\\\"><img src=\\\"{$escapedDisplayThumb}\\\" alt=\\\"{$escapedName}\\\"></a>\";\n            $mdEscapedName = str_replace(']', '', str_replace('[', '', $image->name));\n            $mdEscapedThumb = str_replace(']', '', str_replace('[', '', $image->getAttribute('thumbs')['display']));\n            $data['content']['markdown'] = \"![{$mdEscapedName}]({$mdEscapedThumb})\";\n        }\n\n        return $data;\n    }\n}\n"
  },
  {
    "path": "app/Uploads/FaviconHandler.php",
    "content": "<?php\n\nnamespace BookStack\\Uploads;\n\nuse Illuminate\\Http\\UploadedFile;\n\nclass FaviconHandler\n{\n    protected string $path;\n\n    public function __construct(\n        protected ImageResizer $imageResizer,\n    ) {\n        $this->path = public_path('favicon.ico');\n    }\n\n    /**\n     * Save the given UploadedFile instance as the application favicon.\n     */\n    public function saveForUploadedImage(UploadedFile $file): void\n    {\n        if (!is_writeable($this->path)) {\n            return;\n        }\n\n        $imageData = file_get_contents($file->getRealPath());\n        $pngData = $this->imageResizer->resizeImageData($imageData, 32, 32, false, 'png');\n        $icoData = $this->pngToIco($pngData, 32, 32);\n\n        file_put_contents($this->path, $icoData);\n    }\n\n    /**\n     * Restore the original favicon image.\n     * Returned boolean indicates if the copy occurred.\n     */\n    public function restoreOriginal(): bool\n    {\n        $permissionItem = file_exists($this->path) ? $this->path : dirname($this->path);\n        if (!is_writeable($permissionItem)) {\n            return false;\n        }\n\n        return copy($this->getOriginalPath(), $this->path);\n    }\n\n    /**\n     * Restore the original favicon image if no favicon image is already in use.\n     * Returns a boolean to indicate if the file exists.\n     */\n    public function restoreOriginalIfNotExists(): bool\n    {\n        if (file_exists($this->path)) {\n            return true;\n        }\n\n        return $this->restoreOriginal();\n    }\n\n    /**\n     * Get the path to the favicon file.\n     */\n    public function getPath(): string\n    {\n        return $this->path;\n    }\n\n    /**\n     * Get the path of the original favicon copy.\n     */\n    public function getOriginalPath(): string\n    {\n        return public_path('icon.ico');\n    }\n\n    /**\n     * Convert PNG image data to ICO file format.\n     * Built following the file format info from Wikipedia:\n     * https://en.wikipedia.org/wiki/ICO_(file_format)\n     */\n    protected function pngToIco(string $pngData, int $width, int $height): string\n    {\n        // ICO header\n        $header = pack('v', 0x00); // Reserved. Must always be 0\n        $header .= pack('v', 0x01); // Specifies ico image\n        $header .= pack('v', 0x01); // Specifies number of images\n\n        // ICO Image Directory\n        $entry = hex2bin(dechex($width)); // Image width\n        $entry .= hex2bin(dechex($height)); // Image height\n        $entry .= \"\\0\"; // Color palette, typically 0\n        $entry .= \"\\0\"; // Reserved\n\n        // Color planes, Appears to remain 1 for bmp image data\n        $entry .= pack('v', 0x01);\n        // Bits per pixel, can range from 1 to 32. From testing conversion\n        // via intervention from png typically provides this as 24.\n        $entry .= pack('v', 0x00);\n        // Size of the image data in bytes\n        $entry .= pack('V', strlen($pngData));\n        // Offset of the bmp data from file start\n        $entry .= pack('V', strlen($header) + strlen($entry) + 4);\n\n        // Join & return the combined parts of the ICO image data\n        return $header . $entry . $pngData;\n    }\n}\n"
  },
  {
    "path": "app/Uploads/FileStorage.php",
    "content": "<?php\n\nnamespace BookStack\\Uploads;\n\nuse BookStack\\Exceptions\\FileUploadException;\nuse BookStack\\Util\\FilePathNormalizer;\nuse Exception;\nuse Illuminate\\Contracts\\Filesystem\\Filesystem as Storage;\nuse Illuminate\\Filesystem\\FilesystemManager;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Str;\nuse Symfony\\Component\\HttpFoundation\\File\\UploadedFile;\n\nclass FileStorage\n{\n    public function __construct(\n        protected FilesystemManager $fileSystem,\n    ) {\n    }\n\n    /**\n     * @return resource|null\n     */\n    public function getReadStream(string $path)\n    {\n        return $this->getStorageDisk()->readStream($this->adjustPathForStorageDisk($path));\n    }\n\n    public function getSize(string $path): int\n    {\n        return $this->getStorageDisk()->size($this->adjustPathForStorageDisk($path));\n    }\n\n    public function delete(string $path, bool $removeEmptyDir = false): void\n    {\n        $storage = $this->getStorageDisk();\n        $adjustedPath = $this->adjustPathForStorageDisk($path);\n        $dir = dirname($adjustedPath);\n\n        $storage->delete($adjustedPath);\n        if ($removeEmptyDir && count($storage->allFiles($dir)) === 0) {\n            $storage->deleteDirectory($dir);\n        }\n    }\n\n    /**\n     * @throws FileUploadException\n     */\n    public function uploadFile(UploadedFile $file, string $subDirectory, string $suffix, string $extension): string\n    {\n        $storage = $this->getStorageDisk();\n        $basePath = trim($subDirectory, '/') . '/';\n\n        $uploadFileName = Str::random(16) . ($suffix ? \"-{$suffix}\" : '') . ($extension ? \".{$extension}\" : '');\n        while ($storage->exists($this->adjustPathForStorageDisk($basePath . $uploadFileName))) {\n            $uploadFileName = Str::random(3) . $uploadFileName;\n        }\n\n        $fileStream = fopen($file->getRealPath(), 'r');\n        $filePath = $basePath . $uploadFileName;\n\n        try {\n            $storage->writeStream($this->adjustPathForStorageDisk($filePath), $fileStream);\n        } catch (Exception $e) {\n            Log::error('Error when attempting file upload:' . $e->getMessage());\n\n            throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $filePath]));\n        }\n\n        return $filePath;\n    }\n\n    /**\n     * Check whether the configured storage is remote from the host of this app.\n     */\n    public function isRemote(): bool\n    {\n        return $this->getStorageDiskName() === 's3';\n    }\n\n    /**\n     * Get the actual path on system for the given relative file path.\n     */\n    public function getSystemPath(string $filePath): string\n    {\n        if ($this->isRemote()) {\n            return '';\n        }\n\n        return storage_path('uploads/files/' . ltrim($this->adjustPathForStorageDisk($filePath), '/'));\n    }\n\n    /**\n     * Get the storage that will be used for storing files.\n     */\n    protected function getStorageDisk(): Storage\n    {\n        return $this->fileSystem->disk($this->getStorageDiskName());\n    }\n\n    /**\n     * Get the name of the storage disk to use.\n     */\n    protected function getStorageDiskName(): string\n    {\n        $storageType = trim(strtolower(config('filesystems.attachments')));\n\n        // Change to our secure-attachment disk if any of the local options\n        // are used to prevent escaping that location.\n        if ($storageType === 'local' || $storageType === 'local_secure' || $storageType === 'local_secure_restricted') {\n            $storageType = 'local_secure_attachments';\n        }\n\n        return $storageType;\n    }\n\n    /**\n     * Change the originally provided path to fit any disk-specific requirements.\n     * This also ensures the path is kept to the expected root folders.\n     */\n    protected function adjustPathForStorageDisk(string $path): string\n    {\n        $trimmed = str_replace('uploads/files/', '', $path);\n        $normalized = FilePathNormalizer::normalize($trimmed);\n\n        if ($this->getStorageDiskName() === 'local_secure_attachments') {\n            return $normalized;\n        }\n\n        return 'uploads/files/' . $normalized;\n    }\n}\n"
  },
  {
    "path": "app/Uploads/Image.php",
    "content": "<?php\n\nnamespace BookStack\\Uploads;\n\nuse BookStack\\App\\Model;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Permissions\\Models\\JointPermission;\nuse BookStack\\Permissions\\PermissionApplicator;\nuse BookStack\\Users\\Models\\HasCreatorAndUpdater;\nuse BookStack\\Users\\Models\\OwnableInterface;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\n\n/**\n * @property int      $id\n * @property string   $name\n * @property string   $url\n * @property string   $path\n * @property string   $type\n * @property int|null $uploaded_to\n * @property int      $created_by\n * @property int      $updated_by\n */\nclass Image extends Model implements OwnableInterface\n{\n    use HasFactory;\n    use HasCreatorAndUpdater;\n\n    protected $fillable = ['name'];\n    protected $hidden = [];\n\n    public function jointPermissions(): HasMany\n    {\n        return $this->hasMany(JointPermission::class, 'entity_id', 'uploaded_to')\n            ->where('joint_permissions.entity_type', '=', 'page');\n    }\n\n    /**\n     * Scope the query to just the images visible to the user based upon the\n     * user visibility of the uploaded_to page.\n     */\n    public function scopeVisible(Builder $query): Builder\n    {\n        return app()->make(PermissionApplicator::class)\n            ->restrictPageRelationQuery($query, 'images', 'uploaded_to')\n            ->whereIn('type', ['gallery', 'drawio']);\n    }\n\n    /**\n     * Get a thumbnail URL for this image.\n     * Attempts to generate the thumbnail if not already existing.\n     *\n     * @throws \\Exception\n     */\n    public function getThumb(?int $width, ?int $height, bool $keepRatio = false): ?string\n    {\n        return app()->make(ImageResizer::class)->resizeToThumbnailUrl($this, $width, $height, $keepRatio, false);\n    }\n\n    /**\n     * Get the page this image has been uploaded to.\n     * Only applicable to gallery or drawio image types.\n     */\n    public function getPage(): ?Page\n    {\n        return $this->belongsTo(Page::class, 'uploaded_to')->first();\n    }\n}\n"
  },
  {
    "path": "app/Uploads/ImageRepo.php",
    "content": "<?php\n\nnamespace BookStack\\Uploads;\n\nuse BookStack\\Entities\\Queries\\PageQueries;\nuse BookStack\\Exceptions\\ImageUploadException;\nuse BookStack\\Permissions\\PermissionApplicator;\nuse Exception;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Symfony\\Component\\HttpFoundation\\File\\UploadedFile;\n\nclass ImageRepo\n{\n    public function __construct(\n        protected ImageService $imageService,\n        protected PermissionApplicator $permissions,\n        protected ImageResizer $imageResizer,\n        protected PageQueries $pageQueries,\n    ) {\n    }\n\n    /**\n     * Get an image with the given id.\n     */\n    public function getById($id): Image\n    {\n        return Image::query()->findOrFail($id);\n    }\n\n    /**\n     * Execute a paginated query, returning in a standard format.\n     * Also runs the query through the restriction system.\n     */\n    protected function returnPaginated(Builder $query, int $page = 1, int $pageSize = 24): array\n    {\n        $images = $query->orderBy('created_at', 'desc')->skip($pageSize * ($page - 1))->take($pageSize + 1)->get();\n\n        return [\n            'images'   => $images->take($pageSize),\n            'has_more' => count($images) > $pageSize,\n        ];\n    }\n\n    /**\n     * Fetch a list of images in a paginated format, filtered by image type.\n     * Can be filtered by uploaded to and also by name.\n     */\n    public function getPaginatedByType(\n        string $type,\n        int $page = 0,\n        int $pageSize = 24,\n        ?int $uploadedTo = null,\n        ?string $search = null,\n        ?callable $whereClause = null\n    ): array {\n        $imageQuery = Image::query()->where('type', '=', strtolower($type));\n\n        if ($uploadedTo !== null) {\n            $imageQuery = $imageQuery->where('uploaded_to', '=', $uploadedTo);\n        }\n\n        if ($search !== null) {\n            $imageQuery = $imageQuery->where('name', 'LIKE', '%' . $search . '%');\n        }\n\n        // Filter by page access\n        $imageQuery = $this->permissions->restrictPageRelationQuery($imageQuery, 'images', 'uploaded_to');\n\n        if ($whereClause !== null) {\n            $imageQuery = $imageQuery->where($whereClause);\n        }\n\n        return $this->returnPaginated($imageQuery, $page, $pageSize);\n    }\n\n    /**\n     * Get paginated gallery images within a specific page or book.\n     */\n    public function getEntityFiltered(\n        string $type,\n        ?string $filterType,\n        int $page,\n        int $pageSize,\n        int $uploadedTo,\n        ?string $search\n    ): array {\n        $contextPage = $this->pageQueries->findVisibleByIdOrFail($uploadedTo);\n        $parentFilter = null;\n\n        if ($filterType === 'book' || $filterType === 'page') {\n            $parentFilter = function (Builder $query) use ($filterType, $contextPage) {\n                if ($filterType === 'page') {\n                    $query->where('uploaded_to', '=', $contextPage->id);\n                } else if ($filterType === 'book') {\n                    $validPageIds = $contextPage->book->pages()\n                        ->scopes('visible')\n                        ->pluck('id')\n                        ->toArray();\n                    $query->whereIn('uploaded_to', $validPageIds);\n                }\n            };\n        }\n\n        return $this->getPaginatedByType($type, $page, $pageSize, null, $search, $parentFilter);\n    }\n\n    /**\n     * Save a new image into storage and return the new image.\n     *\n     * @throws ImageUploadException\n     */\n    public function saveNew(\n        UploadedFile $uploadFile,\n        string $type,\n        int $uploadedTo = 0,\n        ?int $resizeWidth = null,\n        ?int $resizeHeight = null,\n        bool $keepRatio = true\n    ): Image {\n        $image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo, $resizeWidth, $resizeHeight, $keepRatio);\n\n        if ($type !== 'system') {\n            $this->imageResizer->loadGalleryThumbnailsForImage($image, true);\n        }\n\n        return $image;\n    }\n\n    /**\n     * Save a new image from an existing image data string.\n     *\n     * @throws ImageUploadException\n     */\n    public function saveNewFromData(string $imageName, string $imageData, string $type, int $uploadedTo = 0): Image\n    {\n        $image = $this->imageService->saveNew($imageName, $imageData, $type, $uploadedTo);\n        $this->imageResizer->loadGalleryThumbnailsForImage($image, true);\n\n        return $image;\n    }\n\n    /**\n     * Save a drawing in the database.\n     *\n     * @throws ImageUploadException\n     */\n    public function saveDrawing(string $base64Uri, int $uploadedTo): Image\n    {\n        $name = 'Drawing-' . user()->id . '-' . time() . '.png';\n\n        return $this->imageService->saveNewFromBase64Uri($base64Uri, $name, 'drawio', $uploadedTo);\n    }\n\n    /**\n     * Update the details of an image via an array of properties.\n     *\n     * @throws Exception\n     */\n    public function updateImageDetails(Image $image, $updateDetails): Image\n    {\n        $image->fill($updateDetails);\n        $image->updated_by = user()->id;\n        $image->save();\n        $this->imageResizer->loadGalleryThumbnailsForImage($image, false);\n\n        return $image;\n    }\n\n    /**\n     * Update the image file of an existing image in the system.\n     * @throws ImageUploadException\n     */\n    public function updateImageFile(Image $image, UploadedFile $file): void\n    {\n        if (strtolower($file->getClientOriginalExtension()) !== strtolower(pathinfo($image->path, PATHINFO_EXTENSION))) {\n            throw new ImageUploadException(trans('errors.image_upload_replace_type'));\n        }\n\n        $image->refresh();\n        $image->updated_by = user()->id;\n        $image->touch();\n        $image->save();\n\n        $this->imageService->replaceExistingFromUpload($image->path, $image->type, $file);\n        $this->imageResizer->loadGalleryThumbnailsForImage($image, true);\n    }\n\n    /**\n     * Destroys an Image object along with its revisions, files and thumbnails.\n     *\n     * @throws Exception\n     */\n    public function destroyImage(?Image $image = null): void\n    {\n        if ($image) {\n            $this->imageService->destroy($image);\n        }\n    }\n\n    /**\n     * Destroy images that have a specific URL and type combination.\n     *\n     * @throws Exception\n     */\n    public function destroyByUrlAndType(string $url, string $imageType): void\n    {\n        $images = Image::query()\n            ->where('url', '=', $url)\n            ->where('type', '=', $imageType)\n            ->get();\n\n        foreach ($images as $image) {\n            $this->destroyImage($image);\n        }\n    }\n\n    /**\n     * Get the raw image data from an Image.\n     */\n    public function getImageData(Image $image): ?string\n    {\n        try {\n            return $this->imageService->getImageData($image);\n        } catch (Exception $exception) {\n            return null;\n        }\n    }\n\n    /**\n     * Get the user visible pages using the given image.\n     */\n    public function getPagesUsingImage(Image $image): array\n    {\n        $pages = $this->pageQueries->visibleForList()\n            ->where('html', 'like', '%' . $image->url . '%')\n            ->get();\n\n        foreach ($pages as $page) {\n            $page->setAttribute('url', $page->getUrl());\n        }\n\n        return $pages->all();\n    }\n}\n"
  },
  {
    "path": "app/Uploads/ImageResizer.php",
    "content": "<?php\n\nnamespace BookStack\\Uploads;\n\nuse BookStack\\Exceptions\\ImageUploadException;\nuse Exception;\nuse GuzzleHttp\\Psr7\\Utils;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\Log;\nuse Intervention\\Image\\Decoders\\BinaryImageDecoder;\nuse Intervention\\Image\\Drivers\\Gd\\Decoders\\NativeObjectDecoder;\nuse Intervention\\Image\\Drivers\\Gd\\Driver;\nuse Intervention\\Image\\Encoders\\AutoEncoder;\nuse Intervention\\Image\\Encoders\\PngEncoder;\nuse Intervention\\Image\\Interfaces\\ImageInterface as InterventionImage;\nuse Intervention\\Image\\ImageManager;\nuse Intervention\\Image\\Origin;\n\nclass ImageResizer\n{\n    protected const THUMBNAIL_CACHE_TIME = 604_800; // 1 week\n\n    public function __construct(\n        protected ImageStorage $storage,\n    ) {\n    }\n\n    /**\n     * Load gallery thumbnails for a set of images.\n     * @param iterable<Image> $images\n     */\n    public function loadGalleryThumbnailsForMany(iterable $images, bool $shouldCreate = false): void\n    {\n        foreach ($images as $image) {\n            $this->loadGalleryThumbnailsForImage($image, $shouldCreate);\n        }\n    }\n\n    /**\n     * Load gallery thumbnails into the given image instance.\n     */\n    public function loadGalleryThumbnailsForImage(Image $image, bool $shouldCreate): void\n    {\n        $thumbs = ['gallery' => null, 'display' => null];\n\n        try {\n            $thumbs['gallery'] = $this->resizeToThumbnailUrl($image, 150, 150, false, $shouldCreate);\n            $thumbs['display'] = $this->resizeToThumbnailUrl($image, 1680, null, true, $shouldCreate);\n        } catch (Exception $exception) {\n            // Prevent thumbnail errors from stopping execution\n        }\n\n        $image->setAttribute('thumbs', $thumbs);\n    }\n\n    /**\n     * Get the thumbnail for an image.\n     * If $keepRatio is true, only the width will be used.\n     * Checks the cache then storage to avoid creating / accessing the filesystem on every check.\n     *\n     * @throws Exception\n     */\n    public function resizeToThumbnailUrl(\n        Image $image,\n        ?int $width,\n        ?int $height,\n        bool $keepRatio = false,\n        bool $shouldCreate = false\n    ): ?string {\n        // Do not resize GIF images where we're not cropping\n        if ($keepRatio && $this->isGif($image)) {\n            return $this->storage->getPublicUrl($image->path);\n        }\n\n        $thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/';\n        $imagePath = $image->path;\n        $thumbFilePath = dirname($imagePath) . $thumbDirName . basename($imagePath);\n\n        $thumbCacheKey = 'images::' . $image->id . '::' . $thumbFilePath;\n\n        // Return path if in cache\n        $cachedThumbPath = Cache::get($thumbCacheKey);\n        if ($cachedThumbPath && !$shouldCreate) {\n            return $this->storage->getPublicUrl($cachedThumbPath);\n        }\n\n        // If a thumbnail has already been generated, serve that and cache path\n        $disk = $this->storage->getDisk($image->type);\n        if (!$shouldCreate && $disk->exists($thumbFilePath)) {\n            Cache::put($thumbCacheKey, $thumbFilePath, static::THUMBNAIL_CACHE_TIME);\n\n            return $this->storage->getPublicUrl($thumbFilePath);\n        }\n\n        $imageData = $disk->get($imagePath);\n\n        // Do not resize animated images where we're not cropping\n        if ($keepRatio && $this->isAnimated($image, $imageData)) {\n            Cache::put($thumbCacheKey, $image->path, static::THUMBNAIL_CACHE_TIME);\n\n            return $this->storage->getPublicUrl($image->path);\n        }\n\n        // If not in cache and thumbnail does not exist, generate thumb and cache path\n        $thumbData = $this->resizeImageData($imageData, $width, $height, $keepRatio, $this->getExtension($image));\n        $disk->put($thumbFilePath, $thumbData, true);\n        Cache::put($thumbCacheKey, $thumbFilePath, static::THUMBNAIL_CACHE_TIME);\n\n        return $this->storage->getPublicUrl($thumbFilePath);\n    }\n\n    /**\n     * Resize the image of given data to the specified size and return the new image data.\n     * Format will remain the same as the input format, unless specified.\n     *\n     * @throws ImageUploadException\n     */\n    public function resizeImageData(\n        string $imageData,\n        ?int $width,\n        ?int $height,\n        bool $keepRatio,\n        ?string $format = null,\n    ): string {\n        try {\n            $thumb = $this->interventionFromImageData($imageData, $format);\n        } catch (Exception $e) {\n            Log::error('Failed to resize image with error:' . $e->getMessage());\n            throw new ImageUploadException(trans('errors.cannot_create_thumbs'));\n        }\n\n        $this->orientImageToOriginalExif($thumb, $imageData);\n\n        if ($keepRatio) {\n            $thumb->scaleDown($width, $height);\n        } else {\n            $thumb->cover($width, $height);\n        }\n\n        $encoder = match ($format) {\n            'png' => new PngEncoder(),\n            default => new AutoEncoder(),\n        };\n\n        $thumbData = (string) $thumb->encode($encoder);\n\n        // Use original image data if we're keeping the ratio\n        // and the resizing does not save any space.\n        if ($keepRatio && strlen($thumbData) > strlen($imageData)) {\n            return $imageData;\n        }\n\n        return $thumbData;\n    }\n\n    /**\n     * Create an intervention image instance from the given image data.\n     * Performs some manual library usage to ensure the image is specifically loaded\n     * from given binary data instead of data being misinterpreted.\n     */\n    protected function interventionFromImageData(string $imageData, ?string $fileType): InterventionImage\n    {\n        if (!extension_loaded('gd')) {\n            throw new ImageUploadException('The PHP \"gd\" extension is required to resize images, but is missing.');\n        }\n\n        $manager = new ImageManager(\n            new Driver(),\n            autoOrientation: false,\n        );\n\n        // Ensure GIF images are decoded natively instead of deferring to intervention GIF\n        // handling since we don't need the added animation support.\n        $isGif = $fileType === 'gif';\n        $decoder = $isGif ? NativeObjectDecoder::class : BinaryImageDecoder::class;\n        $input = $isGif ? @imagecreatefromstring($imageData) : $imageData;\n\n        $image = $manager->read($input, $decoder);\n\n        if ($isGif) {\n            $image->setOrigin(new Origin('image/gif'));\n        }\n\n        return $image;\n    }\n\n    /**\n     * Orientate the given intervention image based upon the given original image data.\n     * Intervention does have an `orientate` method but the exif data it needs is lost before it\n     * can be used (At least when created using binary string data) so we need to do some\n     * implementation on our side to use the original image data.\n     * Bulk of logic taken from: https://github.com/Intervention/image/blob/b734a4988b2148e7d10364b0609978a88d277536/src/Intervention/Image/Commands/OrientateCommand.php\n     * Copyright (c) Oliver Vogel, MIT License.\n     */\n    protected function orientImageToOriginalExif(InterventionImage $image, string $originalData): void\n    {\n        if (!extension_loaded('exif')) {\n            return;\n        }\n\n        $stream = Utils::streamFor($originalData)->detach();\n        $exif = @exif_read_data($stream);\n        $orientation = $exif ? ($exif['Orientation'] ?? null) : null;\n\n        switch ($orientation) {\n            case 2:\n                $image->flip();\n                break;\n            case 3:\n                $image->rotate(180);\n                break;\n            case 4:\n                $image->rotate(180)->flip();\n                break;\n            case 5:\n                $image->rotate(270)->flip();\n                break;\n            case 6:\n                $image->rotate(270);\n                break;\n            case 7:\n                $image->rotate(90)->flip();\n                break;\n            case 8:\n                $image->rotate(90);\n                break;\n        }\n    }\n\n    /**\n     * Checks if the image is a GIF. Returns true if it is, else false.\n     */\n    protected function isGif(Image $image): bool\n    {\n        return $this->getExtension($image) === 'gif';\n    }\n\n    /**\n     * Get the extension for the given image, normalised to lower-case.\n     */\n    protected function getExtension(Image $image): string\n    {\n        return strtolower(pathinfo($image->path, PATHINFO_EXTENSION));\n    }\n\n    /**\n     * Check if the given image and image data is apng.\n     */\n    protected function isApngData(string &$imageData): bool\n    {\n        $initialHeader = substr($imageData, 0, strpos($imageData, 'IDAT'));\n\n        return str_contains($initialHeader, 'acTL');\n    }\n\n    /**\n     * Check if the given avif image data represents an animated image.\n     * This is based upon the answer here: https://stackoverflow.com/a/79457313\n     */\n    protected function isAnimatedAvifData(string &$imageData): bool\n    {\n        $stszPos = strpos($imageData, 'stsz');\n        if ($stszPos === false) {\n            return false;\n        }\n\n        // Look 12 bytes after the start of 'stsz'\n        $start = $stszPos + 12;\n        $end = $start + 4;\n        if ($end > strlen($imageData) - 1) {\n            return false;\n        }\n\n        $data = substr($imageData, $start, 4);\n        $count = unpack('Nvalue', $data)['value'];\n        return $count > 1;\n    }\n\n    /**\n     * Check if the given image is animated.\n     */\n    protected function isAnimated(Image $image, string &$imageData): bool\n    {\n        $extension = strtolower(pathinfo($image->path, PATHINFO_EXTENSION));\n        if ($extension === 'png') {\n            return $this->isApngData($imageData);\n        }\n\n        if ($extension === 'avif') {\n            return $this->isAnimatedAvifData($imageData);\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/Uploads/ImageService.php",
    "content": "<?php\n\nnamespace BookStack\\Uploads;\n\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse BookStack\\Exceptions\\ImageUploadException;\nuse Exception;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Str;\nuse Symfony\\Component\\HttpFoundation\\File\\UploadedFile;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\n\nclass ImageService\n{\n    protected static array $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif'];\n\n    public function __construct(\n        protected ImageStorage $storage,\n        protected ImageResizer $resizer,\n        protected EntityQueries $queries,\n    ) {\n    }\n\n    /**\n     * Saves a new image from an upload.\n     *\n     * @throws ImageUploadException\n     */\n    public function saveNewFromUpload(\n        UploadedFile $uploadedFile,\n        string $type,\n        int $uploadedTo = 0,\n        ?int $resizeWidth = null,\n        ?int $resizeHeight = null,\n        bool $keepRatio = true,\n        string $imageName = '',\n    ): Image {\n        $imageName = $imageName ?: $uploadedFile->getClientOriginalName();\n        $imageData = file_get_contents($uploadedFile->getRealPath());\n\n        if ($resizeWidth !== null || $resizeHeight !== null) {\n            $imageData = $this->resizer->resizeImageData($imageData, $resizeWidth, $resizeHeight, $keepRatio);\n        }\n\n        return $this->saveNew($imageName, $imageData, $type, $uploadedTo);\n    }\n\n    /**\n     * Save a new image from a uri-encoded base64 string of data.\n     *\n     * @throws ImageUploadException\n     */\n    public function saveNewFromBase64Uri(string $base64Uri, string $name, string $type, int $uploadedTo = 0): Image\n    {\n        $splitData = explode(';base64,', $base64Uri);\n        if (count($splitData) < 2) {\n            throw new ImageUploadException('Invalid base64 image data provided');\n        }\n        $data = base64_decode($splitData[1]);\n\n        return $this->saveNew($name, $data, $type, $uploadedTo);\n    }\n\n    /**\n     * Save a new image into storage.\n     *\n     * @throws ImageUploadException\n     */\n    public function saveNew(string $imageName, string $imageData, string $type, int $uploadedTo = 0): Image\n    {\n        $disk = $this->storage->getDisk($type);\n        $secureUploads = setting('app-secure-images');\n        $fileName = $this->storage->cleanImageFileName($imageName);\n\n        $imagePath = '/uploads/images/' . $type . '/' . date('Y-m') . '/';\n\n        while ($disk->exists($imagePath . $fileName)) {\n            $fileName = Str::random(3) . $fileName;\n        }\n\n        $fullPath = $imagePath . $fileName;\n        if ($secureUploads) {\n            $fullPath = $imagePath . Str::random(16) . '-' . $fileName;\n        }\n\n        try {\n            $disk->put($fullPath, $imageData, true);\n        } catch (Exception $e) {\n            Log::error('Error when attempting image upload:' . $e->getMessage());\n\n            throw new ImageUploadException(trans('errors.path_not_writable', ['filePath' => $fullPath]));\n        }\n\n        $imageDetails = [\n            'name'        => $imageName,\n            'path'        => $fullPath,\n            'url'         => $this->storage->getPublicUrl($fullPath),\n            'type'        => $type,\n            'uploaded_to' => $uploadedTo,\n        ];\n\n        if (user()->id !== 0) {\n            $userId = user()->id;\n            $imageDetails['created_by'] = $userId;\n            $imageDetails['updated_by'] = $userId;\n        }\n\n        $image = (new Image())->forceFill($imageDetails);\n        $image->save();\n\n        return $image;\n    }\n\n    /**\n     * Replace an existing image file in the system using the given file.\n     */\n    public function replaceExistingFromUpload(string $path, string $type, UploadedFile $file): void\n    {\n        $imageData = file_get_contents($file->getRealPath());\n        $disk = $this->storage->getDisk($type);\n        $disk->put($path, $imageData);\n    }\n\n    /**\n     * Get the raw data content from an image.\n     *\n     * @throws Exception\n     */\n    public function getImageData(Image $image): string\n    {\n        $disk = $this->storage->getDisk();\n\n        return $disk->get($image->path);\n    }\n\n    /**\n     * Get the raw data content from an image.\n     *\n     * @throws Exception\n     * @return ?resource\n     */\n    public function getImageStream(Image $image): mixed\n    {\n        $disk = $this->storage->getDisk();\n\n        return $disk->stream($image->path);\n    }\n\n    /**\n     * Destroy an image along with its revisions, thumbnails, and remaining folders.\n     *\n     * @throws Exception\n     */\n    public function destroy(Image $image): void\n    {\n        $this->destroyFileAtPath($image->type, $image->path);\n        $image->delete();\n    }\n\n    /**\n     * Destroy the underlying image file at the given path.\n     */\n    public function destroyFileAtPath(string $type, string $path): void\n    {\n        $disk = $this->storage->getDisk($type);\n        $disk->destroyAllMatchingNameFromPath($path);\n    }\n\n    /**\n     * Delete gallery and drawings that are not within HTML content of pages or page revisions.\n     * Checks based off of only the image name.\n     * Could be much improved to be more specific but kept it generic for now to be safe.\n     *\n     * Returns the path of the images that would be/have been deleted.\n     */\n    public function deleteUnusedImages(bool $checkRevisions = true, bool $dryRun = true): array\n    {\n        $types = ['gallery', 'drawio'];\n        $deletedPaths = [];\n\n        Image::query()->whereIn('type', $types)\n            ->chunk(1000, function ($images) use ($checkRevisions, &$deletedPaths, $dryRun) {\n                /** @var Image $image */\n                foreach ($images as $image) {\n                    $searchQuery = '%' . basename($image->path) . '%';\n                    $inPage = DB::table('entity_page_data')\n                            ->where('html', 'like', $searchQuery)->count() > 0;\n\n                    $inRevision = false;\n                    if ($checkRevisions) {\n                        $inRevision = DB::table('page_revisions')\n                                ->where('html', 'like', $searchQuery)->count() > 0;\n                    }\n\n                    if (!$inPage && !$inRevision) {\n                        $deletedPaths[] = $image->path;\n                        if (!$dryRun) {\n                            $this->destroy($image);\n                        }\n                    }\n                }\n            });\n\n        return $deletedPaths;\n    }\n\n    /**\n     * Convert an image URI to a Base64 encoded string.\n     * Attempts to convert the URL to a system storage url then\n     * fetch the data from the disk or storage location.\n     * Returns null if the image data cannot be fetched from storage.\n     */\n    public function imageUrlToBase64(string $url): ?string\n    {\n        $storagePath = $this->storage->urlToPath($url);\n        if (empty($url) || is_null($storagePath)) {\n            return null;\n        }\n\n        // Apply access control when local_secure_restricted images are active\n        if ($this->storage->usingSecureRestrictedImages()) {\n            if (!$this->checkUserHasAccessToRelationOfImageAtPath($storagePath)) {\n                return null;\n            }\n        }\n\n        $disk = $this->storage->getDisk();\n        $imageData = null;\n        if ($disk->exists($storagePath)) {\n            $imageData = $disk->get($storagePath);\n        }\n\n        if (is_null($imageData)) {\n            return null;\n        }\n\n        $extension = pathinfo($url, PATHINFO_EXTENSION);\n        if ($extension === 'svg') {\n            $extension = 'svg+xml';\n        }\n\n        return 'data:image/' . $extension . ';base64,' . base64_encode($imageData);\n    }\n\n    /**\n     * Check if the given path exists and is accessible in the local secure image system.\n     * Returns false if local_secure is not in use, if the file does not exist, if the\n     * file is likely not a valid image, or if permission does not allow access.\n     */\n    public function pathAccessibleInLocalSecure(string $imagePath): bool\n    {\n        $disk = $this->storage->getDisk('gallery');\n\n        return $disk->usingSecureImages() && $this->pathAccessible($imagePath);\n    }\n\n    /**\n     * Check if the given path exists and is accessible depending on the current settings.\n     */\n    public function pathAccessible(string $imagePath): bool\n    {\n        if ($this->storage->usingSecureRestrictedImages() && !$this->checkUserHasAccessToRelationOfImageAtPath($imagePath)) {\n            return false;\n        }\n\n        if ($this->blockedBySecureImages()) {\n            return false;\n        }\n\n        return $this->imageFileExists($imagePath, 'gallery');\n    }\n\n    /**\n     * Check if the given image should be accessible to the current user.\n     */\n    public function imageAccessible(Image $image): bool\n    {\n        if ($this->storage->usingSecureRestrictedImages() && !$this->checkUserHasAccessToRelationOfImage($image)) {\n            return false;\n        }\n\n        if ($this->blockedBySecureImages()) {\n            return false;\n        }\n\n        return $this->imageFileExists($image->path, $image->type);\n    }\n\n    /**\n     * Check if the current user should be blocked from accessing images based on if secure images are enabled\n     * and if public access is enabled for the application.\n     */\n    protected function blockedBySecureImages(): bool\n    {\n        $enforced = $this->storage->usingSecureImages() && !setting('app-public');\n\n        return $enforced && user()->isGuest();\n    }\n\n    /**\n     * Check if the given image path exists for the given image type and that it is likely an image file.\n     */\n    protected function imageFileExists(string $imagePath, string $imageType): bool\n    {\n        $disk = $this->storage->getDisk($imageType);\n        return $disk->exists($imagePath) && str_starts_with($disk->mimeType($imagePath), 'image/');\n    }\n\n    /**\n     * Check that the current user has access to the relation\n     * of the image at the given path.\n     */\n    protected function checkUserHasAccessToRelationOfImageAtPath(string $path): bool\n    {\n        if (str_starts_with($path, 'uploads/images/')) {\n            $path = substr($path, 15);\n        }\n\n        // Strip thumbnail element from path if existing\n        $originalPathSplit = array_filter(explode('/', $path), function (string $part) {\n            $resizedDir = (str_starts_with($part, 'thumbs-') || str_starts_with($part, 'scaled-'));\n            $missingExtension = !str_contains($part, '.');\n\n            return !($resizedDir && $missingExtension);\n        });\n\n        // Build a database-format image path and search for the image entry\n        $fullPath = '/uploads/images/' . ltrim(implode('/', $originalPathSplit), '/');\n        $image = Image::query()->where('path', '=', $fullPath)->first();\n\n        if (is_null($image)) {\n            return false;\n        }\n\n        return $this->checkUserHasAccessToRelationOfImage($image);\n    }\n\n    protected function checkUserHasAccessToRelationOfImage(Image $image): bool\n    {\n        $imageType = $image->type;\n\n        // Allow user or system (logo) images\n        // (No specific relation control but may still have access controlled by auth)\n        if ($imageType === 'user' || $imageType === 'system') {\n            return true;\n        }\n\n        if ($imageType === 'gallery' || $imageType === 'drawio') {\n            return $this->queries->pages->visibleForList()->where('id', '=', $image->uploaded_to)->exists();\n        }\n\n        if ($imageType === 'cover_book') {\n            return $this->queries->books->visibleForList()->where('id', '=', $image->uploaded_to)->exists();\n        }\n\n        if ($imageType === 'cover_bookshelf') {\n            return $this->queries->shelves->visibleForList()->where('id', '=', $image->uploaded_to)->exists();\n        }\n\n        return false;\n    }\n\n    /**\n     * For the given path, if existing, provide a response that will stream the image contents.\n     */\n    public function streamImageFromStorageResponse(string $imageType, string $path): StreamedResponse\n    {\n        $disk = $this->storage->getDisk($imageType);\n\n        return $disk->response($path);\n    }\n\n    /**\n     * Check if the given image extension is supported by BookStack.\n     * The extension must not be altered in this function. This check should provide a guarantee\n     * that the provided extension is safe to use for the image to be saved.\n     */\n    public static function isExtensionSupported(string $extension): bool\n    {\n        return in_array($extension, static::$supportedExtensions);\n    }\n}\n"
  },
  {
    "path": "app/Uploads/ImageStorage.php",
    "content": "<?php\n\nnamespace BookStack\\Uploads;\n\nuse Illuminate\\Filesystem\\FilesystemManager;\nuse Illuminate\\Support\\Str;\n\nclass ImageStorage\n{\n    public function __construct(\n        protected FilesystemManager $fileSystem,\n    ) {\n    }\n\n    /**\n     * Get the storage disk for the given image type.\n     */\n    public function getDisk(string $imageType = ''): ImageStorageDisk\n    {\n        $diskName = $this->getDiskName($imageType);\n\n        return new ImageStorageDisk(\n            $diskName,\n            $this->fileSystem->disk($diskName),\n        );\n    }\n\n    /**\n     * Check if \"local secure restricted\" (Fetched behind auth, with permissions enforced)\n     * is currently active in the instance.\n     */\n    public function usingSecureRestrictedImages(): bool\n    {\n        return config('filesystems.images') === 'local_secure_restricted';\n    }\n\n    /**\n     * Check if \"local secure\" (Fetched behind auth, either with or without permissions enforced)\n     * is currently active in the instance.\n     */\n    public function usingSecureImages(): bool\n    {\n        return config('filesystems.images') === 'local_secure' || $this->usingSecureRestrictedImages();\n    }\n\n    /**\n     * Clean up an image file name to be both URL and storage safe.\n     */\n    public function cleanImageFileName(string $name): string\n    {\n        $name = str_replace(' ', '-', $name);\n        $nameParts = explode('.', $name);\n        $extension = array_pop($nameParts);\n        $name = implode('-', $nameParts);\n        $name = Str::slug($name);\n\n        if (strlen($name) === 0) {\n            $name = Str::random(10);\n        }\n\n        return $name . '.' . $extension;\n    }\n\n    /**\n     * Get the name of the storage disk to use.\n     */\n    protected function getDiskName(string $imageType): string\n    {\n        $storageType = strtolower(config('filesystems.images'));\n        $localSecureInUse = ($storageType === 'local_secure' || $storageType === 'local_secure_restricted');\n\n        // Ensure system images (App logo) are uploaded to a public space\n        if ($imageType === 'system' && $localSecureInUse) {\n            return 'local';\n        }\n\n        // Rename local_secure options to get our image-specific storage driver, which\n        // is scoped to the relevant image directories.\n        if ($localSecureInUse) {\n            return 'local_secure_images';\n        }\n\n        return $storageType;\n    }\n\n    /**\n     * Get a storage path for the given image URL.\n     * Ensures the path will start with \"uploads/images\".\n     * Returns null if the url cannot be resolved to a local URL.\n     */\n    public function urlToPath(string $url): ?string\n    {\n        $url = ltrim(trim($url), '/');\n\n        // Handle potential relative paths\n        $isRelative = !str_starts_with($url, 'http');\n        if ($isRelative) {\n            if (str_starts_with(strtolower($url), 'uploads/images')) {\n                return trim($url, '/');\n            }\n\n            return null;\n        }\n\n        // Handle local images based on paths on the same domain\n        $potentialHostPaths = [\n            url('uploads/images/'),\n            $this->getPublicUrl('/uploads/images/'),\n        ];\n\n        foreach ($potentialHostPaths as $potentialBasePath) {\n            $potentialBasePath = strtolower($potentialBasePath);\n            if (str_starts_with(strtolower($url), $potentialBasePath)) {\n                return 'uploads/images/' . trim(substr($url, strlen($potentialBasePath)), '/');\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * Gets a public facing url for an image or location at the given path.\n     */\n    public static function getPublicUrl(string $filePath): string\n    {\n        return static::getPublicBaseUrl() . '/' . ltrim($filePath, '/');\n    }\n\n    /**\n     * Get the public base URL used for images.\n     * Will not include any path element of the image file, just the base part\n     * from where the path is then expected to start from.\n     * If s3-style store is in use it will default to guessing a public bucket URL.\n     */\n    protected static function getPublicBaseUrl(): string\n    {\n        $storageUrl = config('filesystems.url');\n\n        // Get the standard public s3 url if s3 is set as storage type\n        // Uses the nice, short URL if bucket name has no periods in otherwise the longer\n        // region-based url will be used to prevent http issues.\n        if (!$storageUrl && config('filesystems.images') === 's3') {\n            $storageDetails = config('filesystems.disks.s3');\n            if (!str_contains($storageDetails['bucket'], '.')) {\n                $storageUrl = 'https://' . $storageDetails['bucket'] . '.s3.amazonaws.com';\n            } else {\n                $storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];\n            }\n        }\n\n        $basePath = $storageUrl ?: url('/');\n\n        return rtrim($basePath, '/');\n    }\n}\n"
  },
  {
    "path": "app/Uploads/ImageStorageDisk.php",
    "content": "<?php\n\nnamespace BookStack\\Uploads;\n\nuse BookStack\\Util\\FilePathNormalizer;\nuse Illuminate\\Contracts\\Filesystem\\Filesystem;\nuse Illuminate\\Filesystem\\FilesystemAdapter;\nuse Illuminate\\Support\\Facades\\Log;\nuse League\\Flysystem\\UnableToSetVisibility;\nuse League\\Flysystem\\Visibility;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\n\nclass ImageStorageDisk\n{\n    public function __construct(\n        protected string $diskName,\n        protected Filesystem $filesystem,\n    ) {\n    }\n\n    /**\n     * Check if local secure image storage (Fetched behind authentication)\n     * is currently active in the instance.\n     */\n    public function usingSecureImages(): bool\n    {\n        return $this->diskName === 'local_secure_images';\n    }\n\n    /**\n     * Change the originally provided path to fit any disk-specific requirements.\n     * This also ensures the path is kept to the expected root folders.\n     */\n    protected function adjustPathForDisk(string $path): string\n    {\n        $trimmed = str_replace('uploads/images/', '', $path);\n        $normalized = FilePathNormalizer::normalize($trimmed);\n\n        if ($this->usingSecureImages()) {\n            return $normalized;\n        }\n\n        return 'uploads/images/' . $normalized;\n    }\n\n    /**\n     * Check if a file at the given path exists.\n     */\n    public function exists(string $path): bool\n    {\n        return $this->filesystem->exists($this->adjustPathForDisk($path));\n    }\n\n    /**\n     * Get the file at the given path.\n     */\n    public function get(string $path): ?string\n    {\n        return $this->filesystem->get($this->adjustPathForDisk($path));\n    }\n\n    /**\n     * Get a stream to the file at the given path.\n     * @return ?resource\n     */\n    public function stream(string $path): mixed\n    {\n        return $this->filesystem->readStream($this->adjustPathForDisk($path));\n    }\n\n    /**\n     * Save the given image data at the given path. Can choose to set\n     * the image as public which will update its visibility after saving.\n     */\n    public function put(string $path, string $data, bool $makePublic = false): void\n    {\n        $path = $this->adjustPathForDisk($path);\n        $this->filesystem->put($path, $data);\n\n        // Set public visibility to ensure public access on S3, or that the file is accessible\n        // to other processes (like web-servers) for local file storage options.\n        // We avoid attempting this for (non-AWS) s3-like systems (even in a try-catch) as\n        // we've always avoided setting permissions for s3-like due to potential issues,\n        // with docs advising setting pre-configured permissions instead.\n        // We also don't do this as the default filesystem/driver level as that can technically\n        // require different ACLs for S3, and this provides us more logical control.\n        if ($makePublic && !$this->isS3Like()) {\n            try {\n                $this->filesystem->setVisibility($path, Visibility::PUBLIC);\n            } catch (UnableToSetVisibility $e) {\n                Log::warning(\"Unable to set visibility for image upload with relative path: {$path}\");\n            }\n        }\n    }\n\n    /**\n     * Destroys an image at the given path.\n     * Searches for image thumbnails in addition to main provided path.\n     */\n    public function destroyAllMatchingNameFromPath(string $path): void\n    {\n        $path = $this->adjustPathForDisk($path);\n\n        $imageFolder = dirname($path);\n        $imageFileName = basename($path);\n        $allImages = collect($this->filesystem->allFiles($imageFolder));\n\n        // Delete image files\n        $imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) {\n            return basename($imagePath) === $imageFileName;\n        });\n        $this->filesystem->delete($imagesToDelete->all());\n\n        // Cleanup of empty folders\n        $foldersInvolved = array_merge([$imageFolder], $this->filesystem->directories($imageFolder));\n        foreach ($foldersInvolved as $directory) {\n            if ($this->isFolderEmpty($directory)) {\n                $this->filesystem->deleteDirectory($directory);\n            }\n        }\n    }\n\n    /**\n     * Get the mime type of the file at the given path.\n     * Only works for local filesystem adapters.\n     */\n    public function mimeType(string $path): string\n    {\n        $path = $this->adjustPathForDisk($path);\n        return $this->filesystem instanceof FilesystemAdapter ? $this->filesystem->mimeType($path) : '';\n    }\n\n    /**\n     * Get a stream response for the image at the given path.\n     */\n    public function response(string $path): StreamedResponse\n    {\n        return $this->filesystem->response($this->adjustPathForDisk($path));\n    }\n\n    /**\n     * Check if the image storage in use is an S3-like (but not likely S3) external system.\n     */\n    protected function isS3Like(): bool\n    {\n        $usingS3 = $this->diskName === 's3';\n        return $usingS3 && !is_null(config('filesystems.disks.s3.endpoint'));\n    }\n\n    /**\n     * Check whether a folder is empty.\n     */\n    protected function isFolderEmpty(string $path): bool\n    {\n        $files = $this->filesystem->files($path);\n        $folders = $this->filesystem->directories($path);\n\n        return count($files) === 0 && count($folders) === 0;\n    }\n}\n"
  },
  {
    "path": "app/Uploads/UserAvatars.php",
    "content": "<?php\n\nnamespace BookStack\\Uploads;\n\nuse BookStack\\Exceptions\\HttpFetchException;\nuse BookStack\\Http\\HttpRequestService;\nuse BookStack\\Users\\Models\\User;\nuse BookStack\\Util\\WebSafeMimeSniffer;\nuse Exception;\nuse GuzzleHttp\\Psr7\\Request;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Str;\nuse Psr\\Http\\Client\\ClientExceptionInterface;\n\nclass UserAvatars\n{\n    public function __construct(\n        protected ImageService $imageService,\n        protected HttpRequestService $http\n    ) {\n    }\n\n    /**\n     * Fetch and assign an avatar image to the given user.\n     */\n    public function fetchAndAssignToUser(User $user): void\n    {\n        if (!$this->avatarFetchEnabled()) {\n            return;\n        }\n\n        try {\n            $this->destroyAllForUser($user);\n            $avatar = $this->saveAvatarImage($user);\n            $user->avatar()->associate($avatar);\n            $user->save();\n        } catch (Exception $e) {\n            Log::error('Failed to save user avatar image', ['exception' => $e]);\n        }\n    }\n\n    /**\n     * Assign a new avatar image to the given user using the given image data.\n     */\n    public function assignToUserFromExistingData(User $user, string $imageData, string $extension): void\n    {\n        try {\n            $this->destroyAllForUser($user);\n            $avatar = $this->createAvatarImageFromData($user, $imageData, $extension);\n            $user->avatar()->associate($avatar);\n            $user->save();\n        } catch (Exception $e) {\n            Log::error('Failed to save user avatar image', ['exception' => $e]);\n        }\n    }\n\n    /**\n     * Assign a new avatar image to the given user by fetching from a remote URL.\n     */\n    public function assignToUserFromUrl(User $user, string $avatarUrl): void\n    {\n        try {\n            $this->destroyAllForUser($user);\n            $imageData = $this->getAvatarImageData($avatarUrl);\n\n            $mime = (new WebSafeMimeSniffer())->sniff($imageData);\n            [$format, $type] = explode('/', $mime, 2);\n            if ($format !== 'image' || !ImageService::isExtensionSupported($type)) {\n                return;\n            }\n\n            $avatar = $this->createAvatarImageFromData($user, $imageData, $type);\n            $user->avatar()->associate($avatar);\n            $user->save();\n        } catch (Exception $e) {\n            Log::error('Failed to save user avatar image from URL', [\n                'exception' => $e->getMessage(),\n                'url'       => $avatarUrl,\n                'user_id'   => $user->id,\n            ]);\n        }\n    }\n\n    /**\n     * Destroy all user avatars uploaded to the given user.\n     */\n    public function destroyAllForUser(User $user): void\n    {\n        $profileImages = Image::query()->where('type', '=', 'user')\n            ->where('uploaded_to', '=', $user->id)\n            ->get();\n\n        foreach ($profileImages as $image) {\n            $this->imageService->destroy($image);\n        }\n    }\n\n    /**\n     * Save an avatar image from an external service.\n     *\n     * @throws HttpFetchException\n     */\n    protected function saveAvatarImage(User $user, int $size = 500): Image\n    {\n        $avatarUrl = $this->getAvatarUrl();\n        $email = strtolower(trim($user->email));\n\n        $replacements = [\n            '${hash}'  => md5($email),\n            '${size}'  => $size,\n            '${email}' => urlencode($email),\n        ];\n\n        $userAvatarUrl = strtr($avatarUrl, $replacements);\n        $imageData = $this->getAvatarImageData($userAvatarUrl);\n\n        return $this->createAvatarImageFromData($user, $imageData, 'png');\n    }\n\n    /**\n     * Creates a new image instance and saves it in the system as a new user avatar image.\n     */\n    protected function createAvatarImageFromData(User $user, string $imageData, string $extension): Image\n    {\n        $imageName = Str::random(10) . '-avatar.' . $extension;\n\n        $image = $this->imageService->saveNew($imageName, $imageData, 'user', $user->id);\n        $image->created_by = $user->id;\n        $image->updated_by = $user->id;\n        $image->save();\n\n        return $image;\n    }\n\n    /**\n     * Get an image from a URL and return it as a string of image data.\n     *\n     * @throws HttpFetchException\n     */\n    protected function getAvatarImageData(string $url): string\n    {\n        try {\n            $client = $this->http->buildClient(5);\n            $responseCount = 0;\n\n            do {\n                $response = $client->sendRequest(new Request('GET', $url));\n                $responseCount++;\n                $isRedirect = ($response->getStatusCode() === 301 || $response->getStatusCode() === 302);\n                $url = $response->getHeader('Location')[0] ?? '';\n            } while ($responseCount < 3 && $isRedirect && is_string($url) && str_starts_with($url, 'http'));\n\n            if ($responseCount === 3) {\n                throw new HttpFetchException(\"Failed to fetch image, max redirect limit of 3 tries reached. Last fetched URL: {$url}\");\n            }\n\n            if ($response->getStatusCode() !== 200) {\n                throw new HttpFetchException(trans('errors.cannot_get_image_from_url', ['url' => $url]));\n            }\n\n            return (string) $response->getBody();\n        } catch (ClientExceptionInterface $exception) {\n            throw new HttpFetchException(trans('errors.cannot_get_image_from_url', ['url' => $url]), $exception->getCode(), $exception);\n        }\n    }\n\n    /**\n     * Check if fetching external avatars is enabled.\n     */\n    public function avatarFetchEnabled(): bool\n    {\n        $fetchUrl = $this->getAvatarUrl();\n\n        return str_starts_with($fetchUrl, 'http');\n    }\n\n    /**\n     * Get the URL to fetch avatars from.\n     */\n    public function getAvatarUrl(): string\n    {\n        $configOption = config('services.avatar_url');\n        if ($configOption === false) {\n            return '';\n        }\n\n        $url = trim($configOption);\n\n        if (empty($url) && !config('services.disable_services')) {\n            $url = 'https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon';\n        }\n\n        return $url;\n    }\n}\n"
  },
  {
    "path": "app/Users/Controllers/RoleApiController.php",
    "content": "<?php\n\nnamespace BookStack\\Users\\Controllers;\n\nuse BookStack\\Http\\ApiController;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Permissions\\PermissionsRepo;\nuse BookStack\\Users\\Models\\Role;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\DB;\n\nclass RoleApiController extends ApiController\n{\n    protected array $fieldsToExpose = [\n        'display_name', 'description', 'mfa_enforced', 'external_auth_id', 'created_at', 'updated_at',\n    ];\n\n    protected array $rules = [\n        'create' => [\n            'display_name'  => ['required', 'string', 'min:3', 'max:180'],\n            'description'   => ['string', 'max:180'],\n            'mfa_enforced'  => ['boolean'],\n            'external_auth_id' => ['string', 'max:180'],\n            'permissions'   => ['array'],\n            'permissions.*' => ['string'],\n        ],\n        'update' => [\n            'display_name'  => ['string', 'min:3', 'max:180'],\n            'description'   => ['string', 'max:180'],\n            'mfa_enforced'  => ['boolean'],\n            'external_auth_id' => ['string', 'max:180'],\n            'permissions'   => ['array'],\n            'permissions.*' => ['string'],\n        ]\n    ];\n\n    public function __construct(\n        protected PermissionsRepo $permissionsRepo\n    ) {\n        // Checks for all endpoints in this controller\n        $this->middleware(function ($request, $next) {\n            $this->checkPermission(Permission::UserRolesManage);\n\n            return $next($request);\n        });\n    }\n\n    /**\n     * Get a listing of roles in the system.\n     * Requires permission to manage roles.\n     */\n    public function list()\n    {\n        $roles = Role::query()->select(['*'])\n            ->withCount(['users', 'permissions']);\n\n        return $this->apiListingResponse($roles, [\n            ...$this->fieldsToExpose,\n            'permissions_count',\n            'users_count',\n        ]);\n    }\n\n    /**\n     * Create a new role in the system.\n     * Permissions should be provided as an array of permission name strings.\n     * Requires permission to manage roles.\n     */\n    public function create(Request $request)\n    {\n        $data = $this->validate($request, $this->rules()['create']);\n\n        $role = null;\n        DB::transaction(function () use ($data, &$role) {\n            $role = $this->permissionsRepo->saveNewRole($data);\n        });\n\n        $this->singleFormatter($role);\n\n        return response()->json($role);\n    }\n\n    /**\n     * View the details of a single role.\n     * Provides the permissions and a high-level list of the users assigned.\n     * Requires permission to manage roles.\n     */\n    public function read(string $id)\n    {\n        $role = $this->permissionsRepo->getRoleById($id);\n        $this->singleFormatter($role);\n\n        return response()->json($role);\n    }\n\n    /**\n     * Update an existing role in the system.\n     * Permissions should be provided as an array of permission name strings.\n     * An empty \"permissions\" array would clear granted permissions.\n     * In many cases, where permissions are changed, you'll want to fetch the existing\n     * permissions and then modify before providing in your update request.\n     * Requires permission to manage roles.\n     */\n    public function update(Request $request, string $id)\n    {\n        $data = $this->validate($request, $this->rules()['update']);\n        $role = $this->permissionsRepo->updateRole($id, $data);\n\n        $this->singleFormatter($role);\n\n        return response()->json($role);\n    }\n\n    /**\n     * Delete a role from the system.\n     * Requires permission to manage roles.\n     */\n    public function delete(string $id)\n    {\n        $this->permissionsRepo->deleteRole(intval($id));\n\n        return response('', 204);\n    }\n\n    /**\n     * Format the given role model for a single-result display.\n     */\n    protected function singleFormatter(Role $role): void\n    {\n        $role->load('users:id,name,slug');\n        $role->unsetRelation('permissions');\n        $role->setAttribute('permissions', $role->permissions()->orderBy('name', 'asc')->pluck('name'));\n        $role->makeVisible(['users', 'permissions']);\n    }\n}\n"
  },
  {
    "path": "app/Users/Controllers/RoleController.php",
    "content": "<?php\n\nnamespace BookStack\\Users\\Controllers;\n\nuse BookStack\\Exceptions\\PermissionsException;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Permissions\\PermissionsRepo;\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Queries\\RolesAllPaginatedAndSorted;\nuse BookStack\\Util\\SimpleListOptions;\nuse Exception;\nuse Illuminate\\Http\\Request;\n\nclass RoleController extends Controller\n{\n    public function __construct(\n        protected PermissionsRepo $permissionsRepo\n    ) {\n    }\n\n    /**\n     * Show a listing of the roles in the system.\n     */\n    public function index(Request $request)\n    {\n        $this->checkPermission(Permission::UserRolesManage);\n\n        $listOptions = SimpleListOptions::fromRequest($request, 'roles')->withSortOptions([\n            'display_name' => trans('common.sort_name'),\n            'users_count' => trans('settings.roles_assigned_users'),\n            'permissions_count' => trans('settings.roles_permissions_provided'),\n            'created_at' => trans('common.sort_created_at'),\n            'updated_at' => trans('common.sort_updated_at'),\n        ]);\n\n        $roles = (new RolesAllPaginatedAndSorted())->run(20, $listOptions);\n        $roles->appends($listOptions->getPaginationAppends());\n\n        $this->setPageTitle(trans('settings.roles'));\n\n        return view('settings.roles.index', [\n            'roles'       => $roles,\n            'listOptions' => $listOptions,\n        ]);\n    }\n\n    /**\n     * Show the form to create a new role.\n     */\n    public function create(Request $request)\n    {\n        $this->checkPermission(Permission::UserRolesManage);\n\n        /** @var ?Role $role */\n        $role = null;\n        if ($request->has('copy_from')) {\n            $role = Role::query()->find($request->get('copy_from'));\n        }\n\n        if ($role) {\n            $role->display_name .= ' (' . trans('common.copy') . ')';\n        }\n\n        $this->setPageTitle(trans('settings.role_create'));\n\n        return view('settings.roles.create', ['role' => $role]);\n    }\n\n    /**\n     * Store a new role in the system.\n     */\n    public function store(Request $request)\n    {\n        $this->checkPermission(Permission::UserRolesManage);\n        $data = $this->validate($request, [\n            'display_name' => ['required', 'min:3', 'max:180'],\n            'description'  => ['max:180'],\n            'external_auth_id' => ['string', 'max:180'],\n            'permissions'  => ['array'],\n            'mfa_enforced' => ['string'],\n        ]);\n\n        $data['permissions'] = array_keys($data['permissions'] ?? []);\n        $data['mfa_enforced'] = ($data['mfa_enforced'] ?? 'false') === 'true';\n        $this->permissionsRepo->saveNewRole($data);\n\n        return redirect('/settings/roles');\n    }\n\n    /**\n     * Show the form for editing a user role.\n     */\n    public function edit(string $id)\n    {\n        $this->checkPermission(Permission::UserRolesManage);\n        $role = $this->permissionsRepo->getRoleById($id);\n\n        $this->setPageTitle(trans('settings.role_edit'));\n\n        return view('settings.roles.edit', ['role' => $role]);\n    }\n\n    /**\n     * Updates a user role.\n     */\n    public function update(Request $request, string $id)\n    {\n        $this->checkPermission(Permission::UserRolesManage);\n        $data = $this->validate($request, [\n            'display_name' => ['required', 'min:3', 'max:180'],\n            'description'  => ['max:180'],\n            'external_auth_id' => ['string', 'max:180'],\n            'permissions'  => ['array'],\n            'mfa_enforced' => ['string'],\n        ]);\n\n        $data['permissions'] = array_keys($data['permissions'] ?? []);\n        $data['mfa_enforced'] = ($data['mfa_enforced'] ?? 'false') === 'true';\n        $this->permissionsRepo->updateRole($id, $data);\n\n        return redirect('/settings/roles');\n    }\n\n    /**\n     * Show the view to delete a role.\n     * Offers the chance to migrate users.\n     */\n    public function showDelete(string $id)\n    {\n        $this->checkPermission(Permission::UserRolesManage);\n        $role = $this->permissionsRepo->getRoleById($id);\n        $roles = $this->permissionsRepo->getAllRolesExcept($role);\n        $blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]);\n        $roles->prepend($blankRole);\n\n        $this->setPageTitle(trans('settings.role_delete'));\n\n        return view('settings.roles.delete', ['role' => $role, 'roles' => $roles]);\n    }\n\n    /**\n     * Delete a role from the system,\n     * Migrate from a previous role if set.\n     *\n     * @throws Exception\n     */\n    public function delete(Request $request, string $id)\n    {\n        $this->checkPermission(Permission::UserRolesManage);\n\n        try {\n            $migrateRoleId = intval($request->get('migrate_role_id') ?: \"0\");\n            $this->permissionsRepo->deleteRole($id, $migrateRoleId);\n        } catch (PermissionsException $e) {\n            $this->showErrorNotification($e->getMessage());\n\n            return redirect(\"/settings/roles/delete/{$id}\");\n        }\n\n        return redirect('/settings/roles');\n    }\n}\n"
  },
  {
    "path": "app/Users/Controllers/UserAccountController.php",
    "content": "<?php\n\nnamespace BookStack\\Users\\Controllers;\n\nuse BookStack\\Access\\SocialDriverManager;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Permissions\\PermissionApplicator;\nuse BookStack\\Settings\\UserNotificationPreferences;\nuse BookStack\\Settings\\UserShortcutMap;\nuse BookStack\\Uploads\\ImageRepo;\nuse BookStack\\Users\\UserRepo;\nuse Closure;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Validation\\Rules\\Password;\n\nclass UserAccountController extends Controller\n{\n    public function __construct(\n        protected UserRepo $userRepo,\n    ) {\n        $this->middleware(function (Request $request, Closure $next) {\n            $this->preventGuestAccess();\n            return $next($request);\n        });\n    }\n\n    /**\n     * Redirect the root my-account path to the main/first category.\n     * Required as a controller method, instead of the Route::redirect helper,\n     * to ensure the URL is generated correctly.\n     */\n    public function redirect()\n    {\n        return redirect('/my-account/profile');\n    }\n\n    /**\n     * Show the profile form interface.\n     */\n    public function showProfile()\n    {\n        $this->setPageTitle(trans('preferences.profile'));\n\n        return view('users.account.profile', [\n            'model' => user(),\n            'category' => 'profile',\n        ]);\n    }\n\n    /**\n     * Handle the submission of the user profile form.\n     */\n    public function updateProfile(Request $request, ImageRepo $imageRepo)\n    {\n        $this->preventAccessInDemoMode();\n\n        $user = user();\n        $validated = $this->validate($request, [\n            'name'             => ['min:2', 'max:100'],\n            'email'            => ['min:2', 'email', 'unique:users,email,' . $user->id],\n            'language'         => ['string', 'max:15', 'alpha_dash'],\n            'profile_image'    => array_merge(['nullable'], $this->getImageValidationRules()),\n        ]);\n\n        $this->userRepo->update($user, $validated, userCan(Permission::UsersManage));\n\n        // Save the profile image if in request\n        if ($request->hasFile('profile_image')) {\n            $imageUpload = $request->file('profile_image');\n            $imageRepo->destroyImage($user->avatar);\n            $image = $imageRepo->saveNew($imageUpload, 'user', $user->id);\n            $user->image_id = $image->id;\n            $user->save();\n        }\n\n        // Delete the profile image if the reset option is in request\n        if ($request->has('profile_image_reset')) {\n            $imageRepo->destroyImage($user->avatar);\n            $user->image_id = 0;\n            $user->save();\n        }\n\n        return redirect('/my-account/profile');\n    }\n\n    /**\n     * Show the user-specific interface shortcuts.\n     */\n    public function showShortcuts()\n    {\n        $shortcuts = UserShortcutMap::fromUserPreferences();\n        $enabled = setting()->getForCurrentUser('ui-shortcuts-enabled', false);\n\n        $this->setPageTitle(trans('preferences.shortcuts_interface'));\n\n        return view('users.account.shortcuts', [\n            'category' => 'shortcuts',\n            'shortcuts' => $shortcuts,\n            'enabled' => $enabled,\n        ]);\n    }\n\n    /**\n     * Update the user-specific interface shortcuts.\n     */\n    public function updateShortcuts(Request $request)\n    {\n        $enabled = $request->get('enabled') === 'true';\n        $providedShortcuts = $request->get('shortcut', []);\n        $shortcuts = new UserShortcutMap($providedShortcuts);\n\n        setting()->putForCurrentUser('ui-shortcuts', $shortcuts->toJson());\n        setting()->putForCurrentUser('ui-shortcuts-enabled', $enabled);\n\n        $this->showSuccessNotification(trans('preferences.shortcuts_update_success'));\n\n        return redirect('/my-account/shortcuts');\n    }\n\n    /**\n     * Show the notification preferences for the current user.\n     */\n    public function showNotifications(PermissionApplicator $permissions)\n    {\n        $this->checkPermission(Permission::ReceiveNotifications);\n\n        $preferences = (new UserNotificationPreferences(user()));\n\n        $query = user()->watches()->getQuery();\n        $query = $permissions->restrictEntityRelationQuery($query, 'watches', 'watchable_id', 'watchable_type');\n        $query = $permissions->filterDeletedFromEntityRelationQuery($query, 'watches', 'watchable_id', 'watchable_type');\n        $watches = $query->with('watchable')->paginate(20);\n\n        $this->setPageTitle(trans('preferences.notifications'));\n        return view('users.account.notifications', [\n            'category' => 'notifications',\n            'preferences' => $preferences,\n            'watches' => $watches,\n        ]);\n    }\n\n    /**\n     * Update the notification preferences for the current user.\n     */\n    public function updateNotifications(Request $request)\n    {\n        $this->preventAccessInDemoMode();\n        $this->checkPermission(Permission::ReceiveNotifications);\n        $data = $this->validate($request, [\n           'preferences' => ['required', 'array'],\n           'preferences.*' => ['required', 'string'],\n        ]);\n\n        $preferences = (new UserNotificationPreferences(user()));\n        $preferences->updateFromSettingsArray($data['preferences']);\n        $this->showSuccessNotification(trans('preferences.notifications_update_success'));\n\n        return redirect('/my-account/notifications');\n    }\n\n    /**\n     * Show the view for the \"Access & Security\" account options.\n     */\n    public function showAuth(SocialDriverManager $socialDriverManager)\n    {\n        $mfaMethods = user()->mfaValues()->get()->groupBy('method');\n\n        $this->setPageTitle(trans('preferences.auth'));\n\n        return view('users.account.auth', [\n            'category' => 'auth',\n            'mfaMethods' => $mfaMethods,\n            'authMethod' => config('auth.method'),\n            'activeSocialDrivers' => $socialDriverManager->getActive(),\n        ]);\n    }\n\n    /**\n     * Handle the submission for the auth change password form.\n     */\n    public function updatePassword(Request $request)\n    {\n        $this->preventAccessInDemoMode();\n\n        if (config('auth.method') !== 'standard') {\n            $this->showPermissionError();\n        }\n\n        $validated = $this->validate($request, [\n            'password'         => ['required_with:password_confirm', Password::default()],\n            'password-confirm' => ['same:password', 'required_with:password'],\n        ]);\n\n        $this->userRepo->update(user(), $validated, false);\n\n        $this->showSuccessNotification(trans('preferences.auth_change_password_success'));\n\n        return redirect('/my-account/auth');\n    }\n\n    /**\n     * Show the user self-delete page.\n     */\n    public function delete()\n    {\n        $this->setPageTitle(trans('preferences.delete_my_account'));\n\n        return view('users.account.delete', [\n            'category' => 'profile',\n        ]);\n    }\n\n    /**\n     * Remove the current user from the system.\n     */\n    public function destroy(Request $request)\n    {\n        $this->preventAccessInDemoMode();\n\n        $requestNewOwnerId = intval($request->get('new_owner_id')) ?: null;\n        $newOwnerId = userCan(Permission::UsersManage) ? $requestNewOwnerId : null;\n\n        $this->userRepo->destroy(user(), $newOwnerId);\n\n        return redirect('/');\n    }\n}\n"
  },
  {
    "path": "app/Users/Controllers/UserApiController.php",
    "content": "<?php\n\nnamespace BookStack\\Users\\Controllers;\n\nuse BookStack\\Entities\\EntityExistsRule;\nuse BookStack\\Exceptions\\UserUpdateException;\nuse BookStack\\Http\\ApiController;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Users\\Models\\User;\nuse BookStack\\Users\\UserRepo;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Validation\\Rules\\Password;\nuse Illuminate\\Validation\\Rules\\Unique;\n\nclass UserApiController extends ApiController\n{\n    protected UserRepo $userRepo;\n\n    protected array $fieldsToExpose = [\n        'email', 'created_at', 'updated_at', 'last_activity_at', 'external_auth_id',\n    ];\n\n    public function __construct(UserRepo $userRepo)\n    {\n        $this->userRepo = $userRepo;\n\n        // Checks for all endpoints in this controller\n        $this->middleware(function ($request, $next) {\n            $this->checkPermission(Permission::UsersManage);\n            $this->preventAccessInDemoMode();\n\n            return $next($request);\n        });\n    }\n\n    protected function rules(?int $userId = null): array\n    {\n        return [\n            'create' => [\n                'name'  => ['required', 'string', 'min:1', 'max:100'],\n                'email' => [\n                    'required', 'string', 'email', 'min:2', new Unique('users', 'email'),\n                ],\n                'external_auth_id' => ['string'],\n                'language'         => ['string', 'max:15', 'alpha_dash'],\n                'password'         => ['string', Password::default()],\n                'roles'            => ['array'],\n                'roles.*'          => ['integer'],\n                'send_invite'      => ['boolean'],\n            ],\n            'update' => [\n                'name'  => ['string', 'min:1', 'max:100'],\n                'email' => [\n                    'string',\n                    'email',\n                    'min:2',\n                    (new Unique('users', 'email'))->ignore($userId),\n                ],\n                'external_auth_id' => ['string'],\n                'language'         => ['string', 'max:15', 'alpha_dash'],\n                'password'         => ['string', Password::default()],\n                'roles'            => ['array'],\n                'roles.*'          => ['integer'],\n            ],\n            'delete' => [\n                'migrate_ownership_id' => ['integer', 'exists:users,id'],\n            ],\n        ];\n    }\n\n    /**\n     * Get a listing of users in the system.\n     * Requires permission to manage users.\n     */\n    public function list()\n    {\n        $users = User::query()->select(['users.*'])\n            ->scopes('withLastActivityAt')\n            ->with(['avatar']);\n\n        return $this->apiListingResponse($users, [\n            'id', 'name', 'slug', 'email', 'external_auth_id',\n            'created_at', 'updated_at', 'last_activity_at',\n        ], [$this->listFormatter(...)]);\n    }\n\n    /**\n     * Create a new user in the system.\n     * Requires permission to manage users.\n     */\n    public function create(Request $request)\n    {\n        $data = $this->validate($request, $this->rules()['create']);\n        $sendInvite = boolval($data['send_invite'] ?? false) === true;\n\n        $user = null;\n        DB::transaction(function () use ($data, $sendInvite, &$user) {\n            $user = $this->userRepo->create($data, $sendInvite);\n        });\n\n        $this->singleFormatter($user);\n\n        return response()->json($user);\n    }\n\n    /**\n     * View the details of a single user.\n     * Requires permission to manage users.\n     */\n    public function read(string $id)\n    {\n        $user = $this->userRepo->getById($id);\n        $this->singleFormatter($user);\n\n        return response()->json($user);\n    }\n\n    /**\n     * Update an existing user in the system.\n     * Requires permission to manage users.\n     *\n     * @throws UserUpdateException\n     */\n    public function update(Request $request, string $id)\n    {\n        $data = $this->validate($request, $this->rules($id)['update']);\n        $user = $this->userRepo->getById($id);\n        $this->userRepo->update($user, $data, userCan(Permission::UsersManage));\n        $this->singleFormatter($user);\n\n        return response()->json($user);\n    }\n\n    /**\n     * Delete a user from the system.\n     * Can optionally accept a user id via `migrate_ownership_id` to indicate\n     * who should be the new owner of their related content.\n     * Requires permission to manage users.\n     */\n    public function delete(Request $request, string $id)\n    {\n        $user = $this->userRepo->getById($id);\n        $newOwnerId = $request->get('migrate_ownership_id', null);\n\n        $this->userRepo->destroy($user, $newOwnerId);\n\n        return response('', 204);\n    }\n\n    /**\n     * Format the given user model for single-result display.\n     */\n    protected function singleFormatter(User $user)\n    {\n        $this->listFormatter($user);\n        $user->load('roles:id,display_name');\n        $user->makeVisible(['roles']);\n    }\n\n    /**\n     * Format the given user model for a listing multi-result display.\n     */\n    protected function listFormatter(User $user)\n    {\n        $user->makeVisible($this->fieldsToExpose);\n        $user->setAttribute('profile_url', $user->getProfileUrl());\n        $user->setAttribute('edit_url', $user->getEditUrl());\n        $user->setAttribute('avatar_url', $user->getAvatar());\n    }\n}\n"
  },
  {
    "path": "app/Users/Controllers/UserController.php",
    "content": "<?php\n\nnamespace BookStack\\Users\\Controllers;\n\nuse BookStack\\Access\\SocialDriverManager;\nuse BookStack\\Access\\UserInviteException;\nuse BookStack\\Exceptions\\ImageUploadException;\nuse BookStack\\Exceptions\\UserUpdateException;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Uploads\\ImageRepo;\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Queries\\UsersAllPaginatedAndSorted;\nuse BookStack\\Users\\UserRepo;\nuse BookStack\\Util\\SimpleListOptions;\nuse Exception;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Validation\\Rules\\Password;\nuse Illuminate\\Validation\\ValidationException;\n\nclass UserController extends Controller\n{\n    public function __construct(\n        protected UserRepo $userRepo,\n        protected ImageRepo $imageRepo\n    ) {\n    }\n\n    /**\n     * Display a listing of the users.\n     */\n    public function index(Request $request)\n    {\n        $this->checkPermission(Permission::UsersManage);\n\n        $listOptions = SimpleListOptions::fromRequest($request, 'users')->withSortOptions([\n            'name' => trans('common.sort_name'),\n            'email' => trans('auth.email'),\n            'created_at' => trans('common.sort_created_at'),\n            'updated_at' => trans('common.sort_updated_at'),\n            'last_activity_at' => trans('settings.users_latest_activity'),\n        ]);\n\n        $users = (new UsersAllPaginatedAndSorted())->run(20, $listOptions);\n\n        $this->setPageTitle(trans('settings.users'));\n        $users->appends($listOptions->getPaginationAppends());\n\n        return view('users.index', [\n            'users'       => $users,\n            'listOptions' => $listOptions,\n        ]);\n    }\n\n    /**\n     * Show the form for creating a new user.\n     */\n    public function create()\n    {\n        $this->checkPermission(Permission::UsersManage);\n        $authMethod = config('auth.method');\n        $roles = Role::query()->orderBy('display_name', 'asc')->get();\n        $this->setPageTitle(trans('settings.users_add_new'));\n\n        return view('users.create', ['authMethod' => $authMethod, 'roles' => $roles]);\n    }\n\n    /**\n     * Store a new user in storage.\n     *\n     * @throws ValidationException\n     */\n    public function store(Request $request)\n    {\n        $this->checkPermission(Permission::UsersManage);\n\n        $authMethod = config('auth.method');\n        $sendInvite = ($request->get('send_invite', 'false') === 'true');\n        $externalAuth = $authMethod === 'ldap' || $authMethod === 'saml2' || $authMethod === 'oidc';\n        $passwordRequired = ($authMethod === 'standard' && !$sendInvite);\n\n        $validationRules = [\n            'name'             => ['required', 'max:100'],\n            'email'            => ['required', 'email', 'unique:users,email'],\n            'language'         => ['string', 'max:15', 'alpha_dash'],\n            'roles'            => ['array'],\n            'roles.*'          => ['integer'],\n            'password'         => $passwordRequired ? ['required', Password::default()] : null,\n            'password-confirm' => $passwordRequired ? ['required', 'same:password'] : null,\n            'external_auth_id' => $externalAuth ? ['required'] : null,\n        ];\n\n        $validated = $this->validate($request, array_filter($validationRules));\n\n        try {\n            DB::transaction(function () use ($validated, $sendInvite) {\n                $this->userRepo->create($validated, $sendInvite);\n            });\n        } catch (UserInviteException $e) {\n            Log::error(\"Failed to send user invite with error: {$e->getMessage()}\");\n            $this->showErrorNotification(trans('errors.users_could_not_send_invite'));\n            return redirect('/settings/users/create')->withInput();\n        }\n\n        return redirect('/settings/users');\n    }\n\n    /**\n     * Show the form for editing the specified user.\n     */\n    public function edit(int $id, SocialDriverManager $socialDriverManager)\n    {\n        $this->checkPermission(Permission::UsersManage);\n\n        $user = $this->userRepo->getById($id);\n        $user->load(['apiTokens', 'mfaValues']);\n        $authMethod = ($user->system_name) ? 'system' : config('auth.method');\n\n        $activeSocialDrivers = $socialDriverManager->getActive();\n        $mfaMethods = $user->mfaValues->groupBy('method');\n        $this->setPageTitle(trans('settings.user_profile'));\n        $roles = Role::query()->orderBy('display_name', 'asc')->get();\n\n        return view('users.edit', [\n            'user'                => $user,\n            'activeSocialDrivers' => $activeSocialDrivers,\n            'mfaMethods'          => $mfaMethods,\n            'authMethod'          => $authMethod,\n            'roles'               => $roles,\n        ]);\n    }\n\n    /**\n     * Update the specified user in storage.\n     *\n     * @throws UserUpdateException\n     * @throws ImageUploadException\n     * @throws ValidationException\n     */\n    public function update(Request $request, int $id)\n    {\n        $this->preventAccessInDemoMode();\n        $this->checkPermission(Permission::UsersManage);\n\n        $validated = $this->validate($request, [\n            'name'             => ['min:1', 'max:100'],\n            'email'            => ['min:2', 'email', 'unique:users,email,' . $id],\n            'password'         => ['required_with:password_confirm', Password::default()],\n            'password-confirm' => ['same:password', 'required_with:password'],\n            'language'         => ['string', 'max:15', 'alpha_dash'],\n            'roles'            => ['array'],\n            'roles.*'          => ['integer'],\n            'external_auth_id' => ['string'],\n            'profile_image'    => array_merge(['nullable'], $this->getImageValidationRules()),\n        ]);\n\n        $user = $this->userRepo->getById($id);\n        $this->userRepo->update($user, $validated, true);\n\n        // Save profile image if in request\n        if ($request->hasFile('profile_image')) {\n            $imageUpload = $request->file('profile_image');\n            $this->imageRepo->destroyImage($user->avatar);\n            $image = $this->imageRepo->saveNew($imageUpload, 'user', $user->id);\n            $user->image_id = $image->id;\n            $user->save();\n        }\n\n        // Delete the profile image if reset option is in request\n        if ($request->has('profile_image_reset')) {\n            $this->imageRepo->destroyImage($user->avatar);\n            $user->image_id = 0;\n            $user->save();\n        }\n\n        return redirect('/settings/users');\n    }\n\n    /**\n     * Show the user delete page.\n     */\n    public function delete(int $id)\n    {\n        $this->checkPermission(Permission::UsersManage);\n\n        $user = $this->userRepo->getById($id);\n        $this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name]));\n\n        return view('users.delete', ['user' => $user]);\n    }\n\n    /**\n     * Remove the specified user from storage.\n     *\n     * @throws Exception\n     */\n    public function destroy(Request $request, int $id)\n    {\n        $this->preventAccessInDemoMode();\n        $this->checkPermission(Permission::UsersManage);\n\n        $user = $this->userRepo->getById($id);\n        $newOwnerId = intval($request->get('new_owner_id')) ?: null;\n\n        $this->userRepo->destroy($user, $newOwnerId);\n\n        return redirect('/settings/users');\n    }\n}\n"
  },
  {
    "path": "app/Users/Controllers/UserPreferencesController.php",
    "content": "<?php\n\nnamespace BookStack\\Users\\Controllers;\n\nuse BookStack\\Http\\Controller;\nuse BookStack\\Users\\UserRepo;\nuse Illuminate\\Http\\Request;\n\nclass UserPreferencesController extends Controller\n{\n    public function __construct(\n        protected UserRepo $userRepo\n    ) {\n    }\n\n    /**\n     * Update the preferred view format for a list view of the given type.\n     */\n    public function changeView(Request $request, string $type)\n    {\n        $valueViewTypes = ['books', 'bookshelves', 'bookshelf'];\n        if (!in_array($type, $valueViewTypes)) {\n            return $this->redirectToRequest($request);\n        }\n\n        $view = $request->get('view');\n        if (!in_array($view, ['grid', 'list'])) {\n            $view = 'list';\n        }\n\n        $key = $type . '_view_type';\n        setting()->putForCurrentUser($key, $view);\n\n        return $this->redirectToRequest($request);\n    }\n\n    /**\n     * Change the stored sort type for a particular view.\n     */\n    public function changeSort(Request $request, string $type)\n    {\n        $validSortTypes = ['books', 'bookshelves', 'shelf_books', 'users', 'roles', 'webhooks', 'tags', 'page_revisions'];\n        if (!in_array($type, $validSortTypes)) {\n            return $this->redirectToRequest($request);\n        }\n\n        $sort = substr($request->get('sort') ?: 'name', 0, 50);\n        $order = $request->get('order') === 'desc' ? 'desc' : 'asc';\n\n        $sortKey = $type . '_sort';\n        $orderKey = $type . '_sort_order';\n        setting()->putForCurrentUser($sortKey, $sort);\n        setting()->putForCurrentUser($orderKey, $order);\n\n        return $this->redirectToRequest($request);\n    }\n\n    /**\n     * Toggle dark mode for the current user.\n     */\n    public function toggleDarkMode(Request $request)\n    {\n        $enabled = setting()->getForCurrentUser('dark-mode-enabled');\n        setting()->putForCurrentUser('dark-mode-enabled', $enabled ? 'false' : 'true');\n\n        return $this->redirectToRequest($request);\n    }\n\n    /**\n     * Update the stored section expansion preference for the given user.\n     */\n    public function changeExpansion(Request $request, string $type)\n    {\n        $typeWhitelist = ['home-details'];\n        if (!in_array($type, $typeWhitelist)) {\n            return response('Invalid key', 500);\n        }\n\n        $newState = $request->get('expand', 'false');\n        setting()->putForCurrentUser('section_expansion#' . $type, $newState);\n\n        return response('', 204);\n    }\n\n    /**\n     * Update the favorite status for a code language.\n     */\n    public function updateCodeLanguageFavourite(Request $request)\n    {\n        $validated = $this->validate($request, [\n            'language' => ['required', 'string', 'max:20'],\n            'active' => ['required', 'bool'],\n        ]);\n\n        $currentFavoritesStr = setting()->getForCurrentUser('code-language-favourites', '');\n        $currentFavorites = array_filter(explode(',', $currentFavoritesStr));\n\n        $isFav = in_array($validated['language'], $currentFavorites);\n        if (!$isFav && $validated['active']) {\n            $currentFavorites[] = $validated['language'];\n        } elseif ($isFav && !$validated['active']) {\n            $index = array_search($validated['language'], $currentFavorites);\n            array_splice($currentFavorites, $index, 1);\n        }\n\n        setting()->putForCurrentUser('code-language-favourites', implode(',', $currentFavorites));\n        return response('', 204);\n    }\n}\n"
  },
  {
    "path": "app/Users/Controllers/UserProfileController.php",
    "content": "<?php\n\nnamespace BookStack\\Users\\Controllers;\n\nuse BookStack\\Activity\\ActivityQueries;\nuse BookStack\\Http\\Controller;\nuse BookStack\\Users\\Queries\\UserContentCounts;\nuse BookStack\\Users\\Queries\\UserRecentlyCreatedContent;\nuse BookStack\\Users\\UserRepo;\n\nclass UserProfileController extends Controller\n{\n    public function __construct(\n        protected UserRepo $userRepo,\n        protected ActivityQueries $activityQueries,\n        protected UserContentCounts $contentCounts,\n        protected UserRecentlyCreatedContent $recentlyCreatedContent\n    ) {\n    }\n\n\n    /**\n     * Show the user profile page.\n     */\n    public function show(string $slug)\n    {\n        $user = $this->userRepo->getBySlug($slug);\n\n        $userActivity = $this->activityQueries->userActivity($user);\n        $recentlyCreated = $this->recentlyCreatedContent->run($user, 5);\n        $assetCounts = $this->contentCounts->run($user);\n\n        $this->setPageTitle($user->name);\n\n        return view('users.profile', [\n            'user'            => $user,\n            'activity'        => $userActivity,\n            'recentlyCreated' => $recentlyCreated,\n            'assetCounts'     => $assetCounts,\n        ]);\n    }\n}\n"
  },
  {
    "path": "app/Users/Controllers/UserSearchController.php",
    "content": "<?php\n\nnamespace BookStack\\Users\\Controllers;\n\nuse BookStack\\Http\\Controller;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Http\\Request;\n\nclass UserSearchController extends Controller\n{\n    /**\n     * Search users in the system, with the response formatted\n     * for use in a select-style list.\n     */\n    public function forSelect(Request $request)\n    {\n        $hasPermission = !user()->isGuest() && (\n            userCan(Permission::UsersManage)\n                || userCan(Permission::RestrictionsManageOwn)\n                || userCan(Permission::RestrictionsManageAll)\n        );\n\n        if (!$hasPermission) {\n            $this->showPermissionError();\n        }\n\n        $search = $request->get('search', '');\n        $query = User::query()\n            ->orderBy('name', 'asc')\n            ->take(20);\n\n        if (!empty($search)) {\n            $query->where('name', 'like', '%' . $search . '%');\n        }\n\n        /** @var Collection<User> $users */\n        $users = $query->get();\n\n        return view('form.user-select-list', [\n            'users' => $users,\n        ]);\n    }\n\n    /**\n     * Search users in the system, with the response formatted\n     * for use in a list of mentions.\n     */\n    public function forMentions(Request $request)\n    {\n        $hasPermission = !user()->isGuest() && (\n                userCan(Permission::CommentCreateAll)\n                || userCan(Permission::CommentUpdate)\n            );\n\n        if (!$hasPermission) {\n            $this->showPermissionError();\n        }\n\n        $search = $request->get('search', '');\n        $query = User::query()\n            ->orderBy('name', 'asc')\n            ->take(20);\n\n        if (!empty($search)) {\n            $query->where('name', 'like', '%' . $search . '%');\n        }\n\n        /** @var Collection<User> $users */\n        $users = $query->get();\n\n        return view('form.user-mention-list', [\n            'users' => $users,\n        ]);\n    }\n}\n"
  },
  {
    "path": "app/Users/Models/HasCreatorAndUpdater.php",
    "content": "<?php\n\nnamespace BookStack\\Users\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\n\n/**\n * @property int $created_by\n * @property int $updated_by\n * @property ?User $createdBy\n * @property ?User $updatedBy\n */\ntrait HasCreatorAndUpdater\n{\n    /**\n     * Relation for the user that created this entity.\n     */\n    public function createdBy(): BelongsTo\n    {\n        return $this->belongsTo(User::class, 'created_by');\n    }\n\n    /**\n     * Relation for the user that updated this entity.\n     */\n    public function updatedBy(): BelongsTo\n    {\n        return $this->belongsTo(User::class, 'updated_by');\n    }\n\n    public function getOwnerFieldName(): string\n    {\n        return 'created_by';\n    }\n}\n"
  },
  {
    "path": "app/Users/Models/OwnableInterface.php",
    "content": "<?php\n\nnamespace BookStack\\Users\\Models;\n\ninterface OwnableInterface\n{\n    public function getOwnerFieldName(): string;\n}\n"
  },
  {
    "path": "app/Users/Models/Role.php",
    "content": "<?php\n\nnamespace BookStack\\Users\\Models;\n\nuse BookStack\\Activity\\Models\\Loggable;\nuse BookStack\\App\\Model;\nuse BookStack\\Permissions\\Models\\EntityPermission;\nuse BookStack\\Permissions\\Models\\JointPermission;\nuse BookStack\\Permissions\\Models\\RolePermission;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\n\n/**\n * Class Role.\n *\n * @property int        $id\n * @property string     $display_name\n * @property string     $description\n * @property string     $external_auth_id\n * @property string     $system_name\n * @property bool       $mfa_enforced\n * @property Collection $users\n */\nclass Role extends Model implements Loggable\n{\n    use HasFactory;\n\n    protected $fillable = ['display_name', 'description', 'external_auth_id', 'mfa_enforced'];\n\n    protected $hidden = ['pivot'];\n\n    protected $casts = [\n        'mfa_enforced' => 'boolean',\n    ];\n\n    /**\n     * The roles that belong to the role.\n     */\n    public function users(): BelongsToMany\n    {\n        return $this->belongsToMany(User::class)->orderBy('name', 'asc');\n    }\n\n    /**\n     * Get all related JointPermissions.\n     */\n    public function jointPermissions(): HasMany\n    {\n        return $this->hasMany(JointPermission::class);\n    }\n\n    /**\n     * The RolePermissions that belong to the role.\n     * @return BelongsToMany<RolePermission, $this>\n     */\n    public function permissions(): BelongsToMany\n    {\n        return $this->belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id')\n            ->select(['id', 'name']);\n    }\n\n    /**\n     * Get the entity permissions assigned to this role.\n     */\n    public function entityPermissions(): HasMany\n    {\n        return $this->hasMany(EntityPermission::class);\n    }\n\n    /**\n     * Check if this role has a permission.\n     */\n    public function hasPermission(string $permissionName): bool\n    {\n        $permissions = $this->getRelationValue('permissions');\n        foreach ($permissions as $permission) {\n            if ($permission->getRawAttribute('name') === $permissionName) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * Add a permission to this role.\n     */\n    public function attachPermission(RolePermission $permission)\n    {\n        $this->permissions()->attach($permission->id);\n    }\n\n    /**\n     * Detach a single permission from this role.\n     */\n    public function detachPermission(RolePermission $permission)\n    {\n        $this->permissions()->detach([$permission->id]);\n    }\n\n    /**\n     * Get the role of the specified display name.\n     */\n    public static function getRole(string $displayName): ?self\n    {\n        return static::query()->where('display_name', '=', $displayName)->first();\n    }\n\n    /**\n     * Get the role object for the specified system role.\n     */\n    public static function getSystemRole(string $systemName): ?self\n    {\n        static $cache = [];\n\n        if (!isset($cache[$systemName])) {\n            $cache[$systemName] = static::query()->where('system_name', '=', $systemName)->first();\n        }\n\n        return $cache[$systemName];\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function logDescriptor(): string\n    {\n        return \"({$this->id}) {$this->display_name}\";\n    }\n}\n"
  },
  {
    "path": "app/Users/Models/User.php",
    "content": "<?php\n\nnamespace BookStack\\Users\\Models;\n\nuse BookStack\\Access\\Mfa\\MfaValue;\nuse BookStack\\Access\\Notifications\\ResetPasswordNotification;\nuse BookStack\\Access\\SocialAccount;\nuse BookStack\\Activity\\Models\\Favourite;\nuse BookStack\\Activity\\Models\\Loggable;\nuse BookStack\\Activity\\Models\\Watch;\nuse BookStack\\Api\\ApiToken;\nuse BookStack\\App\\Model;\nuse BookStack\\App\\SluggableInterface;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Translation\\LocaleDefinition;\nuse BookStack\\Translation\\LocaleManager;\nuse BookStack\\Uploads\\Image;\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Auth\\Authenticatable;\nuse Illuminate\\Auth\\Passwords\\CanResetPassword;\nuse Illuminate\\Contracts\\Auth\\Authenticatable as AuthenticatableContract;\nuse Illuminate\\Contracts\\Auth\\CanResetPassword as CanResetPasswordContract;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Notifications\\Notifiable;\nuse Illuminate\\Support\\Collection;\n\n/**\n * @property int        $id\n * @property string     $name\n * @property string     $slug\n * @property string     $email\n * @property string     $password\n * @property Carbon     $created_at\n * @property Carbon     $updated_at\n * @property bool       $email_confirmed\n * @property int        $image_id\n * @property string     $external_auth_id\n * @property string     $system_name\n * @property Collection $roles\n * @property Collection $mfaValues\n * @property ?Image     $avatar\n */\nclass User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable, SluggableInterface\n{\n    use HasFactory;\n    use Authenticatable;\n    use CanResetPassword;\n    use Notifiable;\n\n    /**\n     * The database table used by the model.\n     *\n     * @var string\n     */\n    protected $table = 'users';\n\n    /**\n     * The attributes that are mass assignable.\n     *\n     * @var list<string>\n     */\n    protected $fillable = ['name', 'email'];\n\n    protected $casts = ['last_activity_at' => 'datetime'];\n\n    /**\n     * The attributes excluded from the model's JSON form.\n     *\n     * @var list<string>\n     */\n    protected $hidden = [\n        'password', 'remember_token', 'system_name', 'email_confirmed', 'external_auth_id', 'email',\n        'created_at', 'updated_at', 'image_id', 'roles', 'avatar', 'user_id', 'pivot',\n    ];\n\n    /**\n     * This holds the user's permissions when loaded.\n     */\n    protected ?Collection $permissions;\n\n    /**\n     * This holds the user's avatar URL when loaded to prevent re-calculating within the same request.\n     */\n    protected string $avatarUrl = '';\n\n    /**\n     * Returns the default public user.\n     * Fetches from the container as a singleton to effectively cache at an app level.\n     */\n    public static function getGuest(): self\n    {\n        return app()->make('users.default');\n    }\n\n    /**\n     * Check if the user is the default public user.\n     */\n    public function isGuest(): bool\n    {\n        return $this->system_name === 'public';\n    }\n\n    /**\n     * Check if the user has general access to the application.\n     */\n    public function hasAppAccess(): bool\n    {\n        return !$this->isGuest() || setting('app-public');\n    }\n\n    /**\n     * The roles that belong to the user.\n     *\n     * @return BelongsToMany<Role, $this>\n     */\n    public function roles(): BelongsToMany\n    {\n        return $this->belongsToMany(Role::class);\n    }\n\n    /**\n     * Check if the user has a role.\n     */\n    public function hasRole($roleId): bool\n    {\n        return $this->roles->pluck('id')->contains($roleId);\n    }\n\n    /**\n     * Check if the user has a role.\n     */\n    public function hasSystemRole(string $roleSystemName): bool\n    {\n        return $this->roles->pluck('system_name')->contains($roleSystemName);\n    }\n\n    /**\n     * Attach the default system role to this user.\n     */\n    public function attachDefaultRole(): void\n    {\n        $roleId = intval(setting('registration-role'));\n        if ($roleId && $this->roles()->where('id', '=', $roleId)->count() === 0) {\n            $this->roles()->attach($roleId);\n        }\n    }\n\n    /**\n     * Check if the user has a particular permission.\n     */\n    public function can(string|Permission $permission): bool\n    {\n        $permissionName = is_string($permission) ? $permission : $permission->value;\n        return $this->permissions()->contains($permissionName);\n    }\n\n    /**\n     * Get all permissions belonging to the current user.\n     */\n    protected function permissions(): Collection\n    {\n        if (isset($this->permissions)) {\n            return $this->permissions;\n        }\n\n        $this->permissions = $this->newQuery()->getConnection()->table('role_user', 'ru')\n            ->select('role_permissions.name as name')->distinct()\n            ->leftJoin('permission_role', 'ru.role_id', '=', 'permission_role.role_id')\n            ->leftJoin('role_permissions', 'permission_role.permission_id', '=', 'role_permissions.id')\n            ->where('ru.user_id', '=', $this->id)\n            ->pluck('name');\n\n        return $this->permissions;\n    }\n\n    /**\n     * Clear any cached permissions in this instance.\n     */\n    public function clearPermissionCache(): void\n    {\n        $this->permissions = null;\n    }\n\n    /**\n     * Attach a role to this user.\n     */\n    public function attachRole(Role $role): void\n    {\n        $this->roles()->attach($role->id);\n        $this->unsetRelation('roles');\n    }\n\n    /**\n     * Get the social account associated with this user.\n     */\n    public function socialAccounts(): HasMany\n    {\n        return $this->hasMany(SocialAccount::class);\n    }\n\n    /**\n     * Check if the user has a social account,\n     * If a driver is passed, it checks for that single account type.\n     */\n    public function hasSocialAccount(string $socialDriver = ''): bool\n    {\n        if (empty($socialDriver)) {\n            return $this->socialAccounts()->count() > 0;\n        }\n\n        return $this->socialAccounts()->where('driver', '=', $socialDriver)->exists();\n    }\n\n    /**\n     * Returns a URL to the user's avatar.\n     */\n    public function getAvatar(int $size = 50): string\n    {\n        $default = url('/user_avatar.png');\n        $imageId = $this->image_id;\n        if ($imageId === 0 || $imageId === '0' || $imageId === null) {\n            return $default;\n        }\n\n        if (!empty($this->avatarUrl)) {\n            return $this->avatarUrl;\n        }\n\n        try {\n            $avatar = $this->avatar?->getThumb($size, $size, false) ?? $default;\n        } catch (Exception $err) {\n            $avatar = $default;\n        }\n\n        $this->avatarUrl = $avatar;\n\n        return $avatar;\n    }\n\n    /**\n     * Get the avatar for the user.\n     */\n    public function avatar(): BelongsTo\n    {\n        return $this->belongsTo(Image::class, 'image_id');\n    }\n\n    /**\n     * Get the API tokens assigned to this user.\n     */\n    public function apiTokens(): HasMany\n    {\n        return $this->hasMany(ApiToken::class);\n    }\n\n    /**\n     * Get the favourite instances for this user.\n     */\n    public function favourites(): HasMany\n    {\n        return $this->hasMany(Favourite::class);\n    }\n\n    /**\n     * Get the MFA values belonging to this use.\n     */\n    public function mfaValues(): HasMany\n    {\n        return $this->hasMany(MfaValue::class);\n    }\n\n    /**\n     * Get the tracked entity watches for this user.\n     */\n    public function watches(): HasMany\n    {\n        return $this->hasMany(Watch::class);\n    }\n\n    /**\n     * Get the last activity time for this user.\n     */\n    public function scopeWithLastActivityAt(Builder $query)\n    {\n        $query->addSelect(['activities.created_at as last_activity_at'])\n            ->leftJoinSub(function (\\Illuminate\\Database\\Query\\Builder $query) {\n                $query->from('activities')->select('user_id')\n                    ->selectRaw('max(created_at) as created_at')\n                    ->groupBy('user_id');\n            }, 'activities', 'users.id', '=', 'activities.user_id');\n    }\n\n    /**\n     * Get the url for editing this user.\n     */\n    public function getEditUrl(string $path = ''): string\n    {\n        $uri = '/settings/users/' . $this->id . '/' . trim($path, '/');\n\n        return url(rtrim($uri, '/'));\n    }\n\n    /**\n     * Get the url that links to this user's profile.\n     */\n    public function getProfileUrl(): string\n    {\n        return url('/user/' . $this->slug);\n    }\n\n    /**\n     * Get a shortened version of the user's name.\n     */\n    public function getShortName(int $chars = 8): string\n    {\n        if (mb_strlen($this->name) <= $chars) {\n            return $this->name;\n        }\n\n        $splitName = explode(' ', $this->name);\n        if (mb_strlen($splitName[0]) <= $chars) {\n            return $splitName[0];\n        }\n\n        return mb_substr($this->name, 0, max($chars - 2, 0)) . '…';\n    }\n\n    /**\n     * Get the locale for this user.\n     */\n    public function getLocale(): LocaleDefinition\n    {\n        return app()->make(LocaleManager::class)->getForUser($this);\n    }\n\n    /**\n     * Send the password reset notification.\n     *\n     * @param string $token\n     *\n     * @return void\n     */\n    public function sendPasswordResetNotification($token)\n    {\n        $this->notify(new ResetPasswordNotification($token));\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function logDescriptor(): string\n    {\n        return \"({$this->id}) {$this->name}\";\n    }\n}\n"
  },
  {
    "path": "app/Users/Queries/RolesAllPaginatedAndSorted.php",
    "content": "<?php\n\nnamespace BookStack\\Users\\Queries;\n\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Util\\SimpleListOptions;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\n\n/**\n * Get all the roles in the system in a paginated format.\n */\nclass RolesAllPaginatedAndSorted\n{\n    public function run(int $count, SimpleListOptions $listOptions): LengthAwarePaginator\n    {\n        $sort = $listOptions->getSort();\n        if ($sort === 'created_at') {\n            $sort = 'roles.created_at';\n        }\n\n        $query = Role::query()->select(['*'])\n            ->withCount(['users', 'permissions'])\n            ->orderBy($sort, $listOptions->getOrder());\n\n        if ($listOptions->getSearch()) {\n            $term = '%' . $listOptions->getSearch() . '%';\n            $query->where(function ($query) use ($term) {\n                $query->where('display_name', 'like', $term)\n                    ->orWhere('description', 'like', $term);\n            });\n        }\n\n        return $query->paginate($count);\n    }\n}\n"
  },
  {
    "path": "app/Users/Queries/UserContentCounts.php",
    "content": "<?php\n\nnamespace BookStack\\Users\\Queries;\n\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse BookStack\\Users\\Models\\User;\n\n/**\n * Get asset created counts for the given user.\n */\nclass UserContentCounts\n{\n    public function __construct(\n        protected EntityQueries $queries,\n    ) {\n    }\n\n\n    /**\n     * @return array{pages: int, chapters: int, books: int, shelves: int}\n     */\n    public function run(User $user): array\n    {\n        $createdBy = ['created_by' => $user->id];\n\n        return [\n            'pages'    => $this->queries->pages->visibleForList()->where($createdBy)->count(),\n            'chapters' => $this->queries->chapters->visibleForList()->where($createdBy)->count(),\n            'books'    => $this->queries->books->visibleForList()->where($createdBy)->count(),\n            'shelves'  => $this->queries->shelves->visibleForList()->where($createdBy)->count(),\n        ];\n    }\n}\n"
  },
  {
    "path": "app/Users/Queries/UserRecentlyCreatedContent.php",
    "content": "<?php\n\nnamespace BookStack\\Users\\Queries;\n\nuse BookStack\\Entities\\Queries\\EntityQueries;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Collection;\n\n/**\n * Get the recently created content for the provided user.\n */\nclass UserRecentlyCreatedContent\n{\n    public function __construct(\n        protected EntityQueries $queries,\n    ) {\n    }\n\n    /**\n     * @return array{pages: Collection, chapters: Collection, books: Collection, shelves: Collection}\n     */\n    public function run(User $user, int $count): array\n    {\n        $query = function (Builder $query) use ($user, $count) {\n            return $query->orderBy('created_at', 'desc')\n                ->where('created_by', '=', $user->id)\n                ->take($count)\n                ->get();\n        };\n\n        return [\n            'pages'    => $query($this->queries->pages->visibleForList()->where('draft', '=', false)),\n            'chapters' => $query($this->queries->chapters->visibleForList()),\n            'books'    => $query($this->queries->books->visibleForList()),\n            'shelves'  => $query($this->queries->shelves->visibleForList()),\n        ];\n    }\n}\n"
  },
  {
    "path": "app/Users/Queries/UsersAllPaginatedAndSorted.php",
    "content": "<?php\n\nnamespace BookStack\\Users\\Queries;\n\nuse BookStack\\Users\\Models\\User;\nuse BookStack\\Util\\SimpleListOptions;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\n\n/**\n * Get all the users with their permissions in a paginated format.\n * Note: Due to the use of email search this should only be used when\n * user is assumed to be trusted. (Admin users).\n * Email search can be abused to extract email addresses.\n */\nclass UsersAllPaginatedAndSorted\n{\n    public function run(int $count, SimpleListOptions $listOptions): LengthAwarePaginator\n    {\n        $sort = $listOptions->getSort();\n        if ($sort === 'created_at') {\n            $sort = 'users.created_at';\n        }\n\n        $query = User::query()->select(['*'])\n            ->scopes(['withLastActivityAt'])\n            ->with(['roles', 'avatar'])\n            ->withCount('mfaValues')\n            ->orderBy($sort, $listOptions->getOrder());\n\n        if ($listOptions->getSearch()) {\n            $term = '%' . $listOptions->getSearch() . '%';\n            $query->where(function ($query) use ($term) {\n                $query->where('name', 'like', $term)\n                    ->orWhere('email', 'like', $term);\n            });\n        }\n\n        return $query->paginate($count);\n    }\n}\n"
  },
  {
    "path": "app/Users/UserRepo.php",
    "content": "<?php\n\nnamespace BookStack\\Users;\n\nuse BookStack\\Access\\UserInviteException;\nuse BookStack\\Access\\UserInviteService;\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Entities\\Tools\\SlugGenerator;\nuse BookStack\\Exceptions\\NotifyException;\nuse BookStack\\Exceptions\\UserUpdateException;\nuse BookStack\\Facades\\Activity;\nuse BookStack\\Uploads\\UserAvatars;\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Models\\User;\nuse DB;\nuse Exception;\nuse Illuminate\\Support\\Facades\\Hash;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Str;\n\nclass UserRepo\n{\n    public function __construct(\n        protected UserAvatars $userAvatar,\n        protected UserInviteService $inviteService,\n        protected SlugGenerator $slugGenerator,\n    ) {\n    }\n\n    /**\n     * Get a user by their email address.\n     */\n    public function getByEmail(string $email): ?User\n    {\n        return User::query()->where('email', '=', $email)->first();\n    }\n\n    /**\n     * Get a user by their ID.\n     */\n    public function getById(int $id): User\n    {\n        return User::query()->findOrFail($id);\n    }\n\n    /**\n     * Get a user by their slug.\n     */\n    public function getBySlug(string $slug): User\n    {\n        return User::query()->where('slug', '=', $slug)->firstOrFail();\n    }\n\n    /**\n     * Create a new basic instance of user with the given pre-validated data.\n     *\n     * @param array{name: string, email: string, password: ?string, external_auth_id: ?string, language: ?string, roles: ?array} $data\n     */\n    public function createWithoutActivity(array $data, bool $emailConfirmed = false): User\n    {\n        $user = new User();\n        $user->name = $data['name'];\n        $user->email = $data['email'];\n        $user->password = Hash::make(empty($data['password']) ? Str::random(32) : $data['password']);\n        $user->email_confirmed = $emailConfirmed;\n        $user->external_auth_id = $data['external_auth_id'] ?? '';\n\n        $this->slugGenerator->regenerateForUser($user);\n        $user->save();\n\n        if (!empty($data['language'])) {\n            setting()->putUser($user, 'language', $data['language']);\n        }\n\n        if (isset($data['roles'])) {\n            $this->setUserRoles($user, $data['roles']);\n        }\n\n        $this->downloadAndAssignUserAvatar($user);\n\n        return $user;\n    }\n\n    /**\n     * As per \"createWithoutActivity\" but records a \"create\" activity.\n     *\n     * @param array{name: string, email: string, password: ?string, external_auth_id: ?string, language: ?string, roles: ?array} $data\n     * @throws UserInviteException\n     */\n    public function create(array $data, bool $sendInvite = false): User\n    {\n        $user = $this->createWithoutActivity($data, true);\n\n        if ($sendInvite) {\n            $this->inviteService->sendInvitation($user);\n        }\n\n        Activity::add(ActivityType::USER_CREATE, $user);\n\n        return $user;\n    }\n\n    /**\n     * Update the given user with the given data, but do not create an activity.\n     *\n     * @param array{name: ?string, email: ?string, external_auth_id: ?string, password: ?string, roles: ?array<int>, language: ?string} $data\n     *\n     * @throws UserUpdateException\n     */\n    public function updateWithoutActivity(User $user, array $data, bool $manageUsersAllowed): User\n    {\n        if (!empty($data['name'])) {\n            $user->name = $data['name'];\n            $this->slugGenerator->regenerateForUser($user);\n        }\n\n        if (!empty($data['email']) && $manageUsersAllowed) {\n            $user->email = $data['email'];\n        }\n\n        if (!empty($data['external_auth_id']) && $manageUsersAllowed) {\n            $user->external_auth_id = $data['external_auth_id'];\n        }\n\n        if (isset($data['roles']) && $manageUsersAllowed) {\n            $this->setUserRoles($user, $data['roles']);\n        }\n\n        if (!empty($data['password'])) {\n            $user->password = Hash::make($data['password']);\n        }\n\n        if (!empty($data['language'])) {\n            setting()->putUser($user, 'language', $data['language']);\n        }\n\n        $user->save();\n\n        return $user;\n    }\n\n    /**\n     * Update the given user with the given data.\n     *\n     * @param array{name: ?string, email: ?string, external_auth_id: ?string, password: ?string, roles: ?array<int>, language: ?string} $data\n     *\n     * @throws UserUpdateException\n     */\n    public function update(User $user, array $data, bool $manageUsersAllowed): User\n    {\n        $user = $this->updateWithoutActivity($user, $data, $manageUsersAllowed);\n\n        Activity::add(ActivityType::USER_UPDATE, $user);\n\n        return $user;\n    }\n\n    /**\n     * Remove the given user from storage, Delete all related content.\n     *\n     * @throws Exception\n     */\n    public function destroy(User $user, ?int $newOwnerId = null): void\n    {\n        $this->ensureDeletable($user);\n\n        $this->removeUserDependantRelations($user);\n        $this->nullifyUserNonDependantRelations($user);\n        $user->delete();\n\n        // Delete user profile images\n        $this->userAvatar->destroyAllForUser($user);\n\n        // Delete related activities\n        setting()->deleteUserSettings($user->id);\n\n        // Migrate or nullify ownership\n        $newOwner = null;\n        if (!empty($newOwnerId)) {\n            $newOwner = User::query()->find($newOwnerId);\n        }\n        $this->migrateOwnership($user, $newOwner);\n\n        Activity::add(ActivityType::USER_DELETE, $user);\n    }\n\n    protected function removeUserDependantRelations(User $user): void\n    {\n        $user->apiTokens()->delete();\n        $user->socialAccounts()->delete();\n        $user->favourites()->delete();\n        $user->mfaValues()->delete();\n        $user->watches()->delete();\n\n        $tables = ['email_confirmations', 'user_invites', 'views'];\n        foreach ($tables as $table) {\n            DB::table($table)->where('user_id', '=', $user->id)->delete();\n        }\n    }\n    protected function nullifyUserNonDependantRelations(User $user): void\n    {\n        $toNullify = [\n            'attachments' => ['created_by', 'updated_by'],\n            'comments' => ['created_by', 'updated_by'],\n            'deletions' => ['deleted_by'],\n            'entities' => ['created_by', 'updated_by'],\n            'images' => ['created_by', 'updated_by'],\n            'imports' => ['created_by'],\n            'joint_permissions' => ['owner_id'],\n            'page_revisions' => ['created_by'],\n            'sessions' => ['user_id'],\n        ];\n\n        foreach ($toNullify as $table => $columns) {\n            foreach ($columns as $column) {\n                DB::table($table)\n                    ->where($column, '=', $user->id)\n                    ->update([$column => null]);\n            }\n        }\n    }\n\n    /**\n     * @throws NotifyException\n     */\n    protected function ensureDeletable(User $user): void\n    {\n        if ($this->isOnlyAdmin($user)) {\n            throw new NotifyException(trans('errors.users_cannot_delete_only_admin'), $user->getEditUrl());\n        }\n\n        if ($user->system_name === 'public') {\n            throw new NotifyException(trans('errors.users_cannot_delete_guest'), $user->getEditUrl());\n        }\n    }\n\n    /**\n     * Migrate ownership of items in the system from one user to another.\n     */\n    protected function migrateOwnership(User $fromUser, User|null $toUser): void\n    {\n        $newOwnerValue = $toUser ? $toUser->id : null;\n        DB::table('entities')\n            ->where('owned_by', '=', $fromUser->id)\n            ->update(['owned_by' => $newOwnerValue]);\n    }\n\n    /**\n     * Get an avatar image for a user and set it as their avatar.\n     * Returns early if avatars disabled or not set in config.\n     */\n    protected function downloadAndAssignUserAvatar(User $user): void\n    {\n        try {\n            $this->userAvatar->fetchAndAssignToUser($user);\n        } catch (Exception $e) {\n            Log::error('Failed to save user avatar image');\n        }\n    }\n\n    /**\n     * Checks if the give user is the only admin.\n     */\n    protected function isOnlyAdmin(User $user): bool\n    {\n        if (!$user->hasSystemRole('admin')) {\n            return false;\n        }\n\n        $adminRole = Role::getSystemRole('admin');\n        if ($adminRole->users()->count() > 1) {\n            return false;\n        }\n\n        return true;\n    }\n\n    /**\n     * Set the assigned user roles via an array of role IDs.\n     *\n     * @throws UserUpdateException\n     */\n    protected function setUserRoles(User $user, array $roles): void\n    {\n        $roles = array_filter(array_values($roles));\n\n        if ($this->demotingLastAdmin($user, $roles)) {\n            throw new UserUpdateException(trans('errors.role_cannot_remove_only_admin'), $user->getEditUrl());\n        }\n\n        $user->roles()->sync($roles);\n    }\n\n    /**\n     * Check if the given user is the last admin and their new roles no longer\n     * contain the admin role.\n     */\n    protected function demotingLastAdmin(User $user, array $newRoles): bool\n    {\n        if ($this->isOnlyAdmin($user)) {\n            $adminRole = Role::getSystemRole('admin');\n            if (!in_array(strval($adminRole->id), $newRoles)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/Util/ConfiguredHtmlPurifier.php",
    "content": "<?php\n\nnamespace BookStack\\Util;\n\nuse BookStack\\App\\AppVersion;\nuse HTMLPurifier;\nuse HTMLPurifier_Config;\nuse HTMLPurifier_DefinitionCache_Serializer;\nuse HTMLPurifier_HTML5Config;\nuse HTMLPurifier_HTMLDefinition;\n\n/**\n * Provides a configured HTML Purifier instance.\n * https://github.com/ezyang/htmlpurifier\n * Also uses this to extend support to HTML5 elements:\n * https://github.com/xemlock/htmlpurifier-html5\n */\nclass ConfiguredHtmlPurifier\n{\n    protected HTMLPurifier $purifier;\n    protected static bool $cachedChecked = false;\n\n    public function __construct()\n    {\n        // This is done by the web-server at run-time, with the existing\n        // storage/framework/cache folder to ensure we're using a server-writable folder.\n        $cachePath = storage_path('framework/cache/purifier');\n        $this->createCacheFolderIfNeeded($cachePath);\n\n        $config = HTMLPurifier_HTML5Config::createDefault();\n        $this->setConfig($config, $cachePath);\n        $this->resetCacheIfNeeded($config);\n\n        $htmlDef = $config->getDefinition('HTML', true, true);\n        if ($htmlDef instanceof HTMLPurifier_HTMLDefinition) {\n            $this->configureDefinition($htmlDef);\n        }\n\n        $this->purifier = new HTMLPurifier($config);\n    }\n\n    protected function createCacheFolderIfNeeded(string $cachePath): void\n    {\n        if (!file_exists($cachePath)) {\n            mkdir($cachePath, 0777, true);\n        }\n    }\n\n    protected function resetCacheIfNeeded(HTMLPurifier_Config $config): void\n    {\n        if (self::$cachedChecked) {\n            return;\n        }\n\n        $cachedForVersion = cache('htmlpurifier::cache-version');\n        $appVersion = AppVersion::get();\n        if ($cachedForVersion !== $appVersion) {\n            foreach (['HTML', 'CSS', 'URI'] as $name) {\n                $cache = new HTMLPurifier_DefinitionCache_Serializer($name);\n                $cache->flush($config);\n            }\n            cache()->set('htmlpurifier::cache-version', $appVersion);\n        }\n\n        self::$cachedChecked = true;\n    }\n\n    protected function setConfig(HTMLPurifier_Config $config, string $cachePath): void\n    {\n        $config->set('Cache.SerializerPath', $cachePath);\n        $config->set('Core.AllowHostnameUnderscore', true);\n        $config->set('CSS.AllowTricky', true);\n        $config->set('HTML.SafeIframe', true);\n        $config->set('HTML.TargetNoopener', false);\n        $config->set('HTML.TargetNoreferrer', false);\n        $config->set('Attr.EnableID', true);\n        $config->set('Attr.ID.HTML5', true);\n        $config->set('Output.FixInnerHTML', false);\n        $config->set('URI.SafeIframeRegexp', '%^(http://|https://|//)%');\n        $config->set('URI.AllowedSchemes', [\n            'http' => true,\n            'https' => true,\n            'mailto' => true,\n            'ftp' => true,\n            'nntp' => true,\n            'news' => true,\n            'tel' => true,\n            'file' => true,\n        ]);\n\n         // $config->set('Cache.DefinitionImpl', null); // Disable cache during testing\n    }\n\n    public function configureDefinition(HTMLPurifier_HTMLDefinition $definition): void\n    {\n        // Allow the object element\n        $definition->addElement(\n            'object',\n            'Inline',\n            'Flow',\n            'Common',\n            [\n                'data'   => 'URI',\n                'type'   => 'Text',\n                'width'  => 'Length',\n                'height' => 'Length',\n            ]\n        );\n\n        // Allow the embed element\n        $definition->addElement(\n            'embed',\n            'Inline',\n            'Empty',\n            'Common',\n            [\n                'src'   => 'URI',\n                'type'   => 'Text',\n                'width'  => 'Length',\n                'height' => 'Length',\n            ]\n        );\n\n        // Allow checkbox inputs\n        $definition->addElement(\n            'input',\n            'Formctrl',\n            'Empty',\n            'Common',\n            [\n                'checked' => 'Bool#checked',\n                'disabled' => 'Bool#disabled',\n                'name' => 'Text',\n                'readonly' => 'Bool#readonly',\n                'type' => 'Enum#checkbox',\n                'value' => 'Text',\n            ]\n        );\n\n        // Allow the drawio-diagram attribute on div elements\n        $definition->addAttribute(\n            'div',\n            'drawio-diagram',\n            'Number',\n        );\n\n        // Allow target=\"_blank\" on links\n        $definition->addAttribute('a', 'target', 'Enum#_blank');\n\n        // Allow mention-ids on links\n        $definition->addAttribute('a', 'data-mention-user-id', 'Number');\n    }\n\n    public function purify(string $html): string\n    {\n        return $this->purifier->purify($html);\n    }\n}\n"
  },
  {
    "path": "app/Util/CspService.php",
    "content": "<?php\n\nnamespace BookStack\\Util;\n\nuse Illuminate\\Support\\Str;\n\nclass CspService\n{\n    protected string $nonce;\n\n    public function __construct(string $nonce = '')\n    {\n        $this->nonce = $nonce ?: Str::random(24);\n    }\n\n    /**\n     * Get the nonce value for CSP.\n     */\n    public function getNonce(): string\n    {\n        return $this->nonce;\n    }\n\n    /**\n     * Get the CSP headers for the application.\n     */\n    public function getCspHeader(): string\n    {\n        $headers = [\n            $this->getFrameAncestors(),\n            $this->getFrameSrc(),\n            $this->getScriptSrc(),\n            $this->getObjectSrc(),\n            $this->getBaseUri(),\n        ];\n\n        return implode('; ', array_filter($headers));\n    }\n\n    /**\n     * Get the CSP rules for the application for a HTML meta tag.\n     */\n    public function getCspMetaTagValue(): string\n    {\n        $headers = [\n            $this->getFrameSrc(),\n            $this->getScriptSrc(),\n            $this->getObjectSrc(),\n            $this->getBaseUri(),\n        ];\n\n        return implode('; ', array_filter($headers));\n    }\n\n    /**\n     * Check if the user has configured some allowed iframe hosts.\n     */\n    public function allowedIFrameHostsConfigured(): bool\n    {\n        return count($this->getAllowedIframeHosts()) > 0;\n    }\n\n    /**\n     * Create CSP 'script-src' rule to restrict the forms of script that can run on the page.\n     */\n    protected function getScriptSrc(): string\n    {\n        if ($this->scriptFilteringDisabled()) {\n            return '';\n        }\n\n        $parts = [\n            'http:',\n            'https:',\n            '\\'nonce-' . $this->nonce . '\\'',\n            '\\'strict-dynamic\\'',\n        ];\n\n        return 'script-src ' . implode(' ', $parts);\n    }\n\n    /**\n     * Create CSP \"frame-ancestors\" rule to restrict the hosts that BookStack can be iframed within.\n     */\n    protected function getFrameAncestors(): string\n    {\n        $iframeHosts = $this->getAllowedIframeHosts();\n        array_unshift($iframeHosts, \"'self'\");\n\n        return 'frame-ancestors ' . implode(' ', $iframeHosts);\n    }\n\n    /**\n     * Creates CSP \"frame-src\" rule to restrict what hosts/sources can be loaded\n     * within iframes to provide an allow-list-style approach to iframe content.\n     */\n    protected function getFrameSrc(): string\n    {\n        $iframeHosts = $this->getAllowedIframeSources();\n        array_unshift($iframeHosts, \"'self'\");\n\n        return 'frame-src ' . implode(' ', $iframeHosts);\n    }\n\n    /**\n     * Creates CSP 'object-src' rule to restrict the types of dynamic content\n     * that can be embedded on the page.\n     */\n    protected function getObjectSrc(): string\n    {\n        if ($this->scriptFilteringDisabled()) {\n            return '';\n        }\n\n        return \"object-src 'self'\";\n    }\n\n    /**\n     * Creates CSP 'base-uri' rule to restrict what base tags can be set on\n     * the page to prevent manipulation of relative links.\n     */\n    protected function getBaseUri(): string\n    {\n        return \"base-uri 'self'\";\n    }\n\n    protected function scriptFilteringDisabled(): bool\n    {\n        return !HtmlContentFilterConfig::fromConfigString(config('app.content_filtering'))->filterOutJavaScript;\n    }\n\n    protected function getAllowedIframeHosts(): array\n    {\n        $hosts = config('app.iframe_hosts') ?? '';\n\n        return array_filter(explode(' ', $hosts));\n    }\n\n    protected function getAllowedIframeSources(): array\n    {\n        $sources = explode(' ', config('app.iframe_sources', ''));\n        $sources[] = $this->getDrawioHost();\n\n        return array_filter($sources);\n    }\n\n    /**\n     * Extract the host name of the configured drawio URL for use in CSP.\n     * Returns empty string if not in use.\n     */\n    protected function getDrawioHost(): string\n    {\n        $drawioConfigValue = config('services.drawio');\n        if (!$drawioConfigValue) {\n            return '';\n        }\n\n        $drawioSource = is_string($drawioConfigValue) ? $drawioConfigValue : 'https://embed.diagrams.net/';\n        $drawioSourceParsed = parse_url($drawioSource);\n        $drawioHost = $drawioSourceParsed['scheme'] . '://' . $drawioSourceParsed['host'];\n        if (isset($drawioSourceParsed['port'])) {\n            $drawioHost .= ':' . $drawioSourceParsed['port'];\n        }\n\n        return $drawioHost;\n    }\n}\n"
  },
  {
    "path": "app/Util/DatabaseTransaction.php",
    "content": "<?php\n\nnamespace BookStack\\Util;\n\nuse Closure;\nuse Illuminate\\Support\\Facades\\DB;\nuse Throwable;\n\n/**\n * Run the given code within a database transactions.\n * Wraps Laravel's own transaction method, but sets a specific runtime isolation method.\n * This sets a session level since this won't cause issues if already within a transaction,\n * and this should apply to the next transactions anyway.\n *\n * \"READ COMMITTED\" ensures that changes from other transactions can be read within\n * a transaction, even if started afterward (and for example, it was blocked by the initial\n * transaction). This is quite important for things like permission generation, where we would\n * want to consider the changes made by other committed transactions by the time we come to\n * regenerate permission access.\n *\n * @throws Throwable\n * @template TReturn of mixed\n */\nclass DatabaseTransaction\n{\n    /**\n     * @param  (Closure(static): TReturn)  $callback\n     */\n    public function __construct(\n        protected Closure $callback\n    ) {\n    }\n\n    /**\n     * @return TReturn\n     */\n    public function run(): mixed\n    {\n        DB::statement('SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED');\n        return DB::transaction($this->callback);\n    }\n}\n"
  },
  {
    "path": "app/Util/DateFormatter.php",
    "content": "<?php\n\nnamespace BookStack\\Util;\n\nuse Carbon\\Carbon;\nuse Carbon\\CarbonInterface;\n\nclass DateFormatter\n{\n    public function __construct(\n        protected string $displayTimezone,\n    ) {\n    }\n\n    public function absolute(Carbon $date): string\n    {\n        $withDisplayTimezone = $date->clone()->setTimezone($this->displayTimezone);\n\n        return $withDisplayTimezone->format('Y-m-d H:i:s T');\n    }\n\n    public function relative(Carbon $date, bool $includeSuffix = true): string\n    {\n        return $date->diffForHumans(null, $includeSuffix ? null : CarbonInterface::DIFF_ABSOLUTE);\n    }\n}\n"
  },
  {
    "path": "app/Util/FilePathNormalizer.php",
    "content": "<?php\n\nnamespace BookStack\\Util;\n\nuse League\\Flysystem\\WhitespacePathNormalizer;\n\n/**\n * Utility to normalize (potentially) user provided file paths\n * to avoid things like directory traversal.\n */\nclass FilePathNormalizer\n{\n    public static function normalize(string $path): string\n    {\n        return (new WhitespacePathNormalizer())->normalizePath($path);\n    }\n}\n"
  },
  {
    "path": "app/Util/HtmlContentFilter.php",
    "content": "<?php\n\nnamespace BookStack\\Util;\n\nuse DOMAttr;\nuse DOMElement;\nuse DOMNodeList;\n\nclass HtmlContentFilter\n{\n    public function __construct(\n        protected HtmlContentFilterConfig $config\n    ) {\n    }\n\n    public function filterDocument(HtmlDocument $doc): string\n    {\n        if ($this->config->filterOutJavaScript) {\n            $this->filterOutScriptsFromDocument($doc);\n        }\n        if ($this->config->filterOutFormElements) {\n            $this->filterOutFormElementsFromDocument($doc);\n        }\n        if ($this->config->filterOutBadHtmlElements) {\n            $this->filterOutBadHtmlElementsFromDocument($doc);\n        }\n        if ($this->config->filterOutNonContentElements) {\n            $this->filterOutNonContentElementsFromDocument($doc);\n        }\n\n        $filtered = $doc->getBodyInnerHtml();\n        if ($this->config->useAllowListFilter) {\n            $filtered = $this->applyAllowListFiltering($filtered);\n        }\n\n        return $filtered;\n    }\n\n    public function filterString(string $html): string\n    {\n        return $this->filterDocument(new HtmlDocument($html));\n    }\n\n    protected function applyAllowListFiltering(string $html): string\n    {\n        $purifier = new ConfiguredHtmlPurifier();\n        return $purifier->purify($html);\n    }\n\n    protected function filterOutScriptsFromDocument(HtmlDocument $doc): void\n    {\n        // Remove standard script tags\n        $scriptElems = $doc->queryXPath('//script');\n        static::removeNodes($scriptElems);\n\n        // Remove clickable links to JavaScript URI\n        $badLinks = $doc->queryXPath('//*[' . static::xpathContains('@href', 'javascript:') . ']');\n        static::removeNodes($badLinks);\n\n        // Remove elements with form-like attributes with calls to JavaScript URI\n        $badForms = $doc->queryXPath('//*[' . static::xpathContains('@action', 'javascript:') . '] | //*[' . static::xpathContains('@formaction', 'javascript:') . ']');\n        static::removeNodes($badForms);\n\n        // Remove data or JavaScript iFrames & embeds\n        $badIframes = $doc->queryXPath('//*[' . static::xpathContains('@src', 'data:') . '] | //*[' . static::xpathContains('@src', 'javascript:') . '] | //*[@srcdoc]');\n        static::removeNodes($badIframes);\n\n        // Remove data or JavaScript objects\n        $badObjects = $doc->queryXPath('//*[' . static::xpathContains('@data', 'data:') . '] | //*[' . static::xpathContains('@data', 'javascript:') . ']');\n        static::removeNodes($badObjects);\n\n        // Remove attributes, within svg children, hiding JavaScript or data uris.\n        // A bunch of svg element and attribute combinations expose xss possibilities.\n        // For example, SVG animate tag can exploit JavaScript in values.\n        $badValuesAttrs = $doc->queryXPath('//svg//@*[' . static::xpathContains('.', 'data:') . '] | //svg//@*[' . static::xpathContains('.', 'javascript:') . ']');\n        static::removeAttributes($badValuesAttrs);\n\n        // Remove elements with a xlink:href attribute\n        // Used in SVG but deprecated anyway, so we'll be a bit more heavy-handed here.\n        $xlinkHrefAttributes = $doc->queryXPath('//@*[contains(name(), \\'xlink:href\\')]');\n        static::removeAttributes($xlinkHrefAttributes);\n\n        // Remove 'on*' attributes\n        $onAttributes = $doc->queryXPath('//@*[starts-with(name(), \\'on\\')]');\n        static::removeAttributes($onAttributes);\n    }\n\n    protected function filterOutFormElementsFromDocument(HtmlDocument $doc): void\n    {\n        // Remove form elements\n        $formElements = ['form', 'fieldset', 'button', 'textarea', 'select'];\n        foreach ($formElements as $formElement) {\n            $matchingFormElements = $doc->queryXPath('//' . $formElement);\n            static::removeNodes($matchingFormElements);\n        }\n\n        // Remove non-checkbox inputs\n        $inputsToRemove = $doc->queryXPath('//input');\n        /** @var DOMElement $input */\n        foreach ($inputsToRemove as $input) {\n            $type = strtolower($input->getAttribute('type'));\n            if ($type !== 'checkbox') {\n                $input->parentNode->removeChild($input);\n            }\n        }\n\n        // Remove form attributes\n        $formAttrs = ['form', 'formaction', 'formmethod', 'formtarget'];\n        foreach ($formAttrs as $formAttr) {\n            $matchingFormAttrs = $doc->queryXPath('//@' . $formAttr);\n            static::removeAttributes($matchingFormAttrs);\n        }\n    }\n\n    protected function filterOutBadHtmlElementsFromDocument(HtmlDocument $doc): void\n    {\n        // Remove meta tag to prevent external redirects\n        $metaTags = $doc->queryXPath('//meta[' . static::xpathContains('@content', 'url') . ']');\n        static::removeNodes($metaTags);\n    }\n\n    protected function filterOutNonContentElementsFromDocument(HtmlDocument $doc): void\n    {\n        // Remove non-content elements\n        $formElements = ['link', 'style', 'meta', 'title', 'template'];\n        foreach ($formElements as $formElement) {\n            $matchingFormElements = $doc->queryXPath('//' . $formElement);\n            static::removeNodes($matchingFormElements);\n        }\n    }\n\n    /**\n     * Create an x-path 'contains' statement with a translation automatically built within\n     * to affectively search in a cases-insensitive manner.\n     */\n    protected static function xpathContains(string $property, string $value): string\n    {\n        $value = strtolower($value);\n        $upperVal = strtoupper($value);\n\n        return 'contains(translate(' . $property . ', \\'' . $upperVal . '\\', \\'' . $value . '\\'), \\'' . $value . '\\')';\n    }\n\n    /**\n     * Remove all the given DOMNodes.\n     */\n    protected static function removeNodes(DOMNodeList $nodes): void\n    {\n        foreach ($nodes as $node) {\n            $node->parentNode->removeChild($node);\n        }\n    }\n\n    /**\n     * Remove all the given attribute nodes.\n     */\n    protected static function removeAttributes(DOMNodeList $attrs): void\n    {\n        /** @var DOMAttr $attr */\n        foreach ($attrs as $attr) {\n            $attrName = $attr->nodeName;\n            /** @var DOMElement $parentNode */\n            $parentNode = $attr->parentNode;\n            $parentNode->removeAttribute($attrName);\n        }\n    }\n\n    /**\n     * Alias using the old method name to avoid potential compatibility breaks during patch release.\n     * To remove in future feature release.\n     * @deprecated Use filterDocument instead.\n     */\n    public static function removeScriptsFromDocument(HtmlDocument $doc): void\n    {\n        $config = new HtmlContentFilterConfig(\n            filterOutNonContentElements: false,\n            useAllowListFilter: false,\n        );\n        $filter = new self($config);\n        $filter->filterDocument($doc);\n    }\n\n    /**\n     * Alias using the old method name to avoid potential compatibility breaks during patch release.\n     * To remove in future feature release.\n     * @deprecated Use filterString instead.\n     */\n    public static function removeScriptsFromHtmlString(string $html): string\n    {\n        $config = new HtmlContentFilterConfig(\n            filterOutNonContentElements: false,\n            useAllowListFilter: false,\n        );\n        $filter = new self($config);\n        return $filter->filterString($html);\n    }\n}\n"
  },
  {
    "path": "app/Util/HtmlContentFilterConfig.php",
    "content": "<?php\n\nnamespace BookStack\\Util;\n\nreadonly class HtmlContentFilterConfig\n{\n    public function __construct(\n        public bool $filterOutJavaScript = true,\n        public bool $filterOutBadHtmlElements = true,\n        public bool $filterOutFormElements = true,\n        public bool $filterOutNonContentElements = true,\n        public bool $useAllowListFilter = true,\n    ) {\n    }\n\n    /**\n     * Create an instance from a config string, where the string\n     * is a combination of characters to enable filters.\n     */\n    public static function fromConfigString(string $config): self\n    {\n        $config = strtolower($config);\n        return new self(\n            filterOutJavaScript: str_contains($config, 'j'),\n            filterOutBadHtmlElements: str_contains($config, 'h'),\n            filterOutFormElements: str_contains($config, 'f'),\n            filterOutNonContentElements: str_contains($config, 'h'),\n            useAllowListFilter: str_contains($config, 'a'),\n        );\n    }\n}\n"
  },
  {
    "path": "app/Util/HtmlDescriptionFilter.php",
    "content": "<?php\n\nnamespace BookStack\\Util;\n\nuse DOMAttr;\nuse DOMElement;\nuse DOMNode;\n\n/**\n * Filter to ensure HTML input for description content remains simple and\n * to a limited allow-list of elements and attributes.\n * More for consistency and to prevent nuisance rather than for security\n * (which would be done via a separate content filter and CSP).\n */\nclass HtmlDescriptionFilter\n{\n    /**\n     * @var array<string, string[]>\n     */\n    protected static array $allowedAttrsByElements = [\n        'p' => [],\n        'a' => ['href', 'title', 'target', 'data-mention-user-id'],\n        'ol' => [],\n        'ul' => [],\n        'li' => [],\n        'strong' => [],\n        'span' => [],\n        'em' => [],\n        'br' => [],\n    ];\n\n    public static function filterFromString(string $html): string\n    {\n        if (empty(trim($html))) {\n            return '';\n        }\n\n        $doc = new HtmlDocument($html);\n\n        $topLevel = [...$doc->getBodyChildren()];\n        foreach ($topLevel as $child) {\n            /** @var DOMNode $child */\n            if ($child instanceof DOMElement) {\n                static::filterElement($child);\n            } else {\n                $child->parentNode->removeChild($child);\n            }\n        }\n\n        return $doc->getBodyInnerHtml();\n    }\n\n    protected static function filterElement(DOMElement $element): void\n    {\n        $elType = strtolower($element->tagName);\n        $allowedAttrs = static::$allowedAttrsByElements[$elType] ?? null;\n        if (is_null($allowedAttrs)) {\n            $element->remove();\n            return;\n        }\n\n        $attrs = $element->attributes;\n        for ($i = $attrs->length - 1; $i >= 0; $i--) {\n            /** @var DOMAttr $attr */\n            $attr = $attrs->item($i);\n            $name = strtolower($attr->name);\n            if (!in_array($name, $allowedAttrs)) {\n                $element->removeAttribute($attr->name);\n            }\n        }\n\n        $childNodes = [...$element->childNodes];\n        foreach ($childNodes as $child) {\n            if ($child instanceof DOMElement) {\n                static::filterElement($child);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/Util/HtmlDocument.php",
    "content": "<?php\n\nnamespace BookStack\\Util;\n\nuse DOMDocument;\nuse DOMElement;\nuse DOMNode;\nuse DOMNodeList;\nuse DOMText;\nuse DOMXPath;\n\n/**\n * HtmlDocument is a thin wrapper around DOMDocument built\n * specifically for loading, querying and generating HTML content.\n */\nclass HtmlDocument\n{\n    protected DOMDocument $document;\n    protected ?DOMXPath $xpath = null;\n    protected int $loadOptions;\n\n    public function __construct(string $partialHtml = '', int $loadOptions = 0)\n    {\n        libxml_use_internal_errors(true);\n        $this->document = new DOMDocument();\n        $this->loadOptions = $loadOptions;\n\n        if ($partialHtml) {\n            $this->loadPartialHtml($partialHtml);\n        }\n    }\n\n    /**\n     * Load some HTML content that's part of a document (e.g. body content)\n     * into the current document.\n     */\n    public function loadPartialHtml(string $html): void\n    {\n        $html = '<?xml encoding=\"utf-8\" ?><body>' . $html . '</body>';\n        $this->document->loadHTML($html, $this->loadOptions);\n        $this->xpath = null;\n    }\n\n    /**\n     * Load a complete page of HTML content into the document.\n     */\n    public function loadCompleteHtml(string $html): void\n    {\n        $html = '<?xml encoding=\"utf-8\" ?>' . $html;\n        $this->document->loadHTML($html, $this->loadOptions);\n        $this->xpath = null;\n    }\n\n    /**\n     * Start an XPath query on the current document.\n     */\n    public function queryXPath(string $expression): DOMNodeList\n    {\n        if (is_null($this->xpath)) {\n            $this->xpath = new DOMXPath($this->document);\n        }\n\n        $result = $this->xpath->query($expression);\n        if ($result === false) {\n            throw new \\InvalidArgumentException(\"XPath query for expression [$expression] failed to execute\");\n        }\n\n        return $result;\n    }\n\n    /**\n     * Create a new DOMElement instance within the document.\n     */\n    public function createElement(string $localName, string $value = ''): DOMElement\n    {\n        $element = $this->document->createElement($localName, $value);\n\n        if ($element === false) {\n            throw new \\InvalidArgumentException(\"Failed to create element of name [$localName] and value [$value]\");\n        }\n\n        return $element;\n    }\n\n    /**\n     * Create a new text node within this document.\n     */\n    public function createTextNode(string $text): DOMText\n    {\n        return $this->document->createTextNode($text);\n    }\n\n    /**\n     * Get an element within the document of the given ID.\n     */\n    public function getElementById(string $elementId): ?DOMElement\n    {\n        return $this->document->getElementById($elementId);\n    }\n\n    /**\n     * Get the DOMNode that represents the HTML body.\n     */\n    public function getBody(): DOMNode\n    {\n        $bodies = $this->document->getElementsByTagName('body');\n\n        if ($bodies->length === 0) {\n            return new DOMElement('body', '');\n        }\n\n        return $bodies[0];\n    }\n\n    /**\n     * Get the nodes that are a direct child of the body.\n     * This is usually all the content nodes if loaded partially.\n     */\n    public function getBodyChildren(): DOMNodeList\n    {\n        return $this->getBody()->childNodes;\n    }\n\n    /**\n     * Get the inner HTML content of the body.\n     * This is usually all the content if loaded partially.\n     */\n    public function getBodyInnerHtml(): string\n    {\n        $html = '';\n        foreach ($this->getBodyChildren() as $child) {\n            $html .= $this->document->saveHTML($child);\n        }\n\n        return $html;\n    }\n\n    /**\n     * Get the HTML content of the whole document.\n     */\n    public function getHtml(): string\n    {\n        return $this->document->saveHTML($this->document->documentElement);\n    }\n\n    /**\n     * Get the inner HTML for the given node.\n     */\n    public function getNodeInnerHtml(DOMNode $node): string\n    {\n        $html = '';\n\n        foreach ($node->childNodes as $childNode) {\n            $html .= $this->document->saveHTML($childNode);\n        }\n\n        return $html;\n    }\n\n    /**\n     * Get the outer HTML for the given node.\n     */\n    public function getNodeOuterHtml(DOMNode $node): string\n    {\n        return $this->document->saveHTML($node);\n    }\n}\n"
  },
  {
    "path": "app/Util/HtmlNonceApplicator.php",
    "content": "<?php\n\nnamespace BookStack\\Util;\n\nuse DOMElement;\nuse DOMNodeList;\n\nclass HtmlNonceApplicator\n{\n    protected static string $placeholder = '[CSP_NONCE_VALUE]';\n\n    /**\n     * Prepare the given HTML content with nonce attributes including a placeholder\n     * value which we can target later.\n     */\n    public static function prepare(string $html): string\n    {\n        if (empty($html)) {\n            return $html;\n        }\n\n        // LIBXML_SCHEMA_CREATE was found to be required here otherwise\n        // the PHP DOMDocument handling will attempt to format/close\n        // HTML tags within scripts and therefore change JS content.\n        $doc = new HtmlDocument($html, LIBXML_SCHEMA_CREATE);\n\n        // Apply to scripts\n        $scriptElems = $doc->queryXPath('//script');\n        static::addNonceAttributes($scriptElems, static::$placeholder);\n\n        // Apply to styles\n        $styleElems = $doc->queryXPath('//style');\n        static::addNonceAttributes($styleElems, static::$placeholder);\n\n        return $doc->getBodyInnerHtml();\n    }\n\n    /**\n     * Apply the give nonce value to the given prepared HTML.\n     */\n    public static function apply(string $html, string $nonce): string\n    {\n        return str_replace(static::$placeholder, $nonce, $html);\n    }\n\n    protected static function addNonceAttributes(DOMNodeList $nodes, string $attrValue): void\n    {\n        /** @var DOMElement $node */\n        foreach ($nodes as $node) {\n            $node->setAttribute('nonce', $attrValue);\n        }\n    }\n}\n"
  },
  {
    "path": "app/Util/OutOfMemoryHandler.php",
    "content": "<?php\n\nnamespace BookStack\\Util;\n\nuse BookStack\\Exceptions\\Handler;\nuse Illuminate\\Contracts\\Debug\\ExceptionHandler;\n\n/**\n * Create a handler which runs the provided actions upon an\n * out-of-memory event. This allows reserving of memory to allow\n * the desired action to run as needed.\n *\n * Essentially provides a wrapper and memory reserving around the\n * memory handling added to the default app error handler.\n */\nclass OutOfMemoryHandler\n{\n    protected $onOutOfMemory;\n    protected string $memoryReserve = '';\n\n    public function __construct(callable $onOutOfMemory, int $memoryReserveMB = 4)\n    {\n        $this->onOutOfMemory = $onOutOfMemory;\n\n        $this->memoryReserve = str_repeat('x', $memoryReserveMB * 1_000_000);\n        $this->getHandler()->prepareForOutOfMemory(function () {\n            return $this->handle();\n        });\n    }\n\n    protected function handle(): mixed\n    {\n        $result = null;\n        $this->memoryReserve = '';\n\n        if ($this->onOutOfMemory) {\n            $result = call_user_func($this->onOutOfMemory);\n            $this->forget();\n        }\n\n        return $result;\n    }\n\n    /**\n     * Forget the handler, so no action is taken place on out of memory.\n     */\n    public function forget(): void\n    {\n        $this->memoryReserve = '';\n        $this->onOutOfMemory = null;\n        $this->getHandler()->forgetOutOfMemoryHandler();\n    }\n\n    protected function getHandler(): Handler\n    {\n        /**\n         * We want to resolve our specific BookStack handling via the set app handler\n         * singleton, but phpstan will only infer based on the interface.\n         * @phpstan-ignore return.type\n         */\n        return app()->make(ExceptionHandler::class);\n    }\n}\n"
  },
  {
    "path": "app/Util/SimpleListOptions.php",
    "content": "<?php\n\nnamespace BookStack\\Util;\n\nuse Illuminate\\Http\\Request;\n\n/**\n * Handled options commonly used for item lists within the system, providing a standard\n * model for handling and validating sort, order and search options.\n */\nclass SimpleListOptions\n{\n    protected string $typeKey;\n    protected string $sort;\n    protected string $order;\n    protected string $search;\n    protected array $sortOptions = [];\n\n    public function __construct(string $typeKey, string $sort, string $order, string $search = '')\n    {\n        $this->typeKey = $typeKey;\n        $this->sort = $sort;\n        $this->order = $order;\n        $this->search = $search;\n    }\n\n    /**\n     * Create a new instance from the given request.\n     * Takes the item type (plural) that's used as a key for storing sort preferences.\n     */\n    public static function fromRequest(Request $request, string $typeKey, bool $sortDescDefault = false): self\n    {\n        $search = $request->get('search', '');\n        $sort = setting()->getForCurrentUser($typeKey . '_sort', '');\n        $order = setting()->getForCurrentUser($typeKey . '_sort_order', $sortDescDefault ? 'desc' : 'asc');\n\n        return new self($typeKey, $sort, $order, $search);\n    }\n\n    /**\n     * Configure the valid sort options for this set of list options.\n     * Provided sort options must be an array, keyed by search properties\n     * with values being user-visible option labels.\n     * Returns current options for easy fluent usage during creation.\n     */\n    public function withSortOptions(array $sortOptions): self\n    {\n        $this->sortOptions = array_merge($this->sortOptions, $sortOptions);\n\n        return $this;\n    }\n\n    /**\n     * Get the current order option.\n     */\n    public function getOrder(): string\n    {\n        return strtolower($this->order) === 'desc' ? 'desc' : 'asc';\n    }\n\n    /**\n     * Get the current sort option.\n     */\n    public function getSort(): string\n    {\n        $default = array_key_first($this->sortOptions) ?? 'name';\n        $sort = $this->sort ?: $default;\n\n        if (empty($this->sortOptions) || array_key_exists($sort, $this->sortOptions)) {\n            return $sort;\n        }\n\n        return $default;\n    }\n\n    /**\n     * Get the set search term.\n     */\n    public function getSearch(): string\n    {\n        return $this->search;\n    }\n\n    /**\n     * Get the data to append for pagination.\n     */\n    public function getPaginationAppends(): array\n    {\n        return ['search' => $this->search];\n    }\n\n    /**\n     * Get the data required by the sort control view.\n     */\n    public function getSortControlData(): array\n    {\n        return [\n            'options' => $this->sortOptions,\n            'order' => $this->getOrder(),\n            'sort' => $this->getSort(),\n            'type' => $this->typeKey,\n        ];\n    }\n}\n"
  },
  {
    "path": "app/Util/SsrUrlValidator.php",
    "content": "<?php\n\nnamespace BookStack\\Util;\n\nuse BookStack\\Exceptions\\HttpFetchException;\n\n/**\n * Validate the host we're connecting to when making a server-side-request.\n * Will use the given hosts config if given during construction otherwise\n * will look to the app configured config.\n */\nclass SsrUrlValidator\n{\n    protected string $config;\n\n    public function __construct(?string $config = null)\n    {\n        $this->config = $config ?? config('app.ssr_hosts') ?? '';\n    }\n\n    /**\n     * @throws HttpFetchException\n     */\n    public function ensureAllowed(string $url): void\n    {\n        if (!$this->allowed($url)) {\n            throw new HttpFetchException(trans('errors.http_ssr_url_no_match'));\n        }\n    }\n\n    /**\n     * Check if the given URL is allowed by the configured SSR host values.\n     */\n    public function allowed(string $url): bool\n    {\n        $allowed = $this->getHostPatterns();\n\n        foreach ($allowed as $pattern) {\n            if ($this->urlMatchesPattern($url, $pattern)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    protected function urlMatchesPattern($url, $pattern): bool\n    {\n        $pattern = rtrim(trim($pattern), '/');\n        $url = trim($url);\n\n        if (empty($pattern) || empty($url)) {\n            return false;\n        }\n\n        $quoted = preg_quote($pattern, '/');\n        $regexPattern = str_replace('\\*', '.*', $quoted);\n\n        return preg_match('/^' . $regexPattern . '($|\\/.*$|#.*$)/i', $url);\n    }\n\n    /**\n     * @return string[]\n     */\n    protected function getHostPatterns(): array\n    {\n        return explode(' ', strtolower($this->config));\n    }\n}\n"
  },
  {
    "path": "app/Util/SvgIcon.php",
    "content": "<?php\n\nnamespace BookStack\\Util;\n\nuse BookStack\\Facades\\Theme;\n\nclass SvgIcon\n{\n    public function __construct(\n        protected string $name,\n        protected array $attrs = []\n    ) {\n    }\n\n    public function toHtml(): string\n    {\n        $attrs = array_merge([\n            'class'     => 'svg-icon',\n            'data-icon' => $this->name,\n            'role'      => 'presentation',\n        ], $this->attrs);\n\n        $attrString = ' ';\n        foreach ($attrs as $attrName => $attr) {\n            $attrString .= $attrName . '=\"' . $attr . '\" ';\n        }\n\n        $defaultIconPath = resource_path('icons/' . $this->name . '.svg');\n        $iconPath = Theme::findFirstFile(\"icons/{$this->name}.svg\") ?? $defaultIconPath;\n        if (!file_exists($iconPath)) {\n            return '';\n        }\n\n        $fileContents = file_get_contents($iconPath);\n\n        return str_replace('<svg', '<svg' . $attrString, $fileContents);\n    }\n}\n"
  },
  {
    "path": "app/Util/WebSafeMimeSniffer.php",
    "content": "<?php\n\nnamespace BookStack\\Util;\n\nuse finfo;\n\n/**\n * Helper class to sniff out the mime-type of content resulting in\n * a mime-type that's relatively safe to serve to a browser.\n */\nclass WebSafeMimeSniffer\n{\n    /**\n     * @var string[]\n     */\n    protected array $safeMimes = [\n        'application/json',\n        'application/octet-stream',\n        'application/pdf',\n        'audio/aac',\n        'audio/midi',\n        'audio/mpeg',\n        'audio/ogg',\n        'audio/opus',\n        'audio/wav',\n        'audio/webm',\n        'audio/x-m4a',\n        'image/apng',\n        'image/bmp',\n        'image/jpeg',\n        'image/png',\n        'image/gif',\n        'image/webp',\n        'image/avif',\n        'image/heic',\n        'text/css',\n        'text/csv',\n        'text/javascript',\n        'text/json',\n        'text/plain',\n        'video/x-msvideo',\n        'video/mp4',\n        'video/mpeg',\n        'video/ogg',\n        'video/webm',\n        'video/vp9',\n        'video/h264',\n        'video/av1',\n    ];\n\n    protected array $textTypesByExtension = [\n        'css' => 'text/css',\n        'js' => 'text/javascript',\n        'json' => 'application/json',\n        'csv' => 'text/csv',\n    ];\n\n    /**\n     * Sniff the mime-type from the given file content while running the result\n     * through an allow-list to ensure a web-safe result.\n     * Takes the content as a reference since the value may be quite large.\n     * Accepts an optional $extension which can be used for further guessing.\n     */\n    public function sniff(string &$content, string $extension = ''): string\n    {\n        $fInfo = new finfo(FILEINFO_MIME_TYPE);\n        $mime = $fInfo->buffer($content) ?: 'application/octet-stream';\n\n        if ($mime === 'text/plain' && $extension) {\n            $mime = $this->textTypesByExtension[$extension] ?? 'text/plain';\n        }\n\n        if (in_array($mime, $this->safeMimes)) {\n            return $mime;\n        }\n\n        [$category] = explode('/', $mime, 2);\n        if ($category === 'text') {\n            return 'text/plain';\n        }\n\n        return 'application/octet-stream';\n    }\n}\n"
  },
  {
    "path": "artisan",
    "content": "#!/usr/bin/env php\n<?php\n\ndefine('LARAVEL_START', microtime(true));\n\n/*\n|--------------------------------------------------------------------------\n| Register The Auto Loader\n|--------------------------------------------------------------------------\n|\n| Composer provides a convenient, automatically generated class loader\n| for our application. We just need to utilize it! We'll require it\n| into the script here so that we do not have to worry about the\n| loading of any our classes \"manually\". Feels great to relax.\n|\n*/\n\nrequire __DIR__.'/vendor/autoload.php';\n\n$app = require_once __DIR__.'/bootstrap/app.php';\n\n/*\n|--------------------------------------------------------------------------\n| Run The Artisan Application\n|--------------------------------------------------------------------------\n|\n| When we run the console application, the current CLI command will be\n| executed in this console and the response sent back to a terminal\n| or another output device for the developers. Here goes nothing!\n|\n*/\n\n$kernel = $app->make(Illuminate\\Contracts\\Console\\Kernel::class);\n\n$status = $kernel->handle(\n    $input = new Symfony\\Component\\Console\\Input\\ArgvInput,\n    new Symfony\\Component\\Console\\Output\\ConsoleOutput\n);\n\n/*\n|--------------------------------------------------------------------------\n| Shutdown The Application\n|--------------------------------------------------------------------------\n|\n| Once Artisan has finished running, we will fire off the shutdown events\n| so that any final work may be done by the application before we shut\n| down the process. This is the last thing to happen to the request.\n|\n*/\n\n$kernel->terminate($input, $status);\n\nexit($status);"
  },
  {
    "path": "bootstrap/app.php",
    "content": "<?php\n\n/*\n|--------------------------------------------------------------------------\n| Create The Application\n|--------------------------------------------------------------------------\n|\n| The first thing we will do is create a new Laravel application instance\n| which serves as the \"glue\" for all the components of Laravel, and is\n| the IoC container for the system binding all of the various parts.\n|\n*/\n\n$app = new \\BookStack\\App\\Application(\n    dirname(__DIR__)\n);\n\n/*\n|--------------------------------------------------------------------------\n| Bind Important Interfaces\n|--------------------------------------------------------------------------\n|\n| Next, we need to bind some important interfaces into the container so\n| we will be able to resolve them when needed. The kernels serve the\n| incoming requests to this application from both the web and CLI.\n|\n*/\n\n$app->singleton(\n    Illuminate\\Contracts\\Http\\Kernel::class,\n    BookStack\\Http\\Kernel::class\n);\n\n$app->singleton(\n    Illuminate\\Contracts\\Console\\Kernel::class,\n    BookStack\\Console\\Kernel::class\n);\n\n$app->singleton(\n    Illuminate\\Contracts\\Debug\\ExceptionHandler::class,\n    BookStack\\Exceptions\\Handler::class\n);\n\n/*\n|--------------------------------------------------------------------------\n| Return The Application\n|--------------------------------------------------------------------------\n|\n| This script returns the application instance. The instance is given to\n| the calling script so we can separate the building of the instances\n| from the actual running of the application and sending responses.\n|\n*/\n\nreturn $app;\n"
  },
  {
    "path": "bootstrap/cache/.gitignore",
    "content": "*\n!.gitignore\n"
  },
  {
    "path": "bootstrap/phpstan.php",
    "content": "<?php\n\n// Overwrite configuration that can interfere with the phpstan/larastan scanning.\nconfig()->set([\n    'filesystems.default' => 'local',\n]);\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"bookstackapp/bookstack\",\n    \"description\": \"BookStack documentation platform\",\n    \"keywords\": [\n        \"BookStack\",\n        \"Documentation\"\n    ],\n    \"license\": \"MIT\",\n    \"type\": \"project\",\n    \"require\": {\n        \"php\": \"^8.2.0\",\n        \"ext-curl\": \"*\",\n        \"ext-dom\": \"*\",\n        \"ext-fileinfo\": \"*\",\n        \"ext-gd\": \"*\",\n        \"ext-json\": \"*\",\n        \"ext-mbstring\": \"*\",\n        \"ext-xml\": \"*\",\n        \"ext-zip\": \"*\",\n        \"bacon/bacon-qr-code\": \"^3.0\",\n        \"dompdf/dompdf\": \"^3.1\",\n        \"ezyang/htmlpurifier\": \"^4.19\",\n        \"guzzlehttp/guzzle\": \"^7.4\",\n        \"intervention/image\": \"^3.5\",\n        \"knplabs/knp-snappy\": \"^1.5\",\n        \"laravel/framework\": \"^v12.26.4\",\n        \"laravel/socialite\": \"^5.10\",\n        \"laravel/tinker\": \"^2.8\",\n        \"league/commonmark\": \"^2.3\",\n        \"league/flysystem-aws-s3-v3\": \"^3.0\",\n        \"league/html-to-markdown\": \"^5.0.0\",\n        \"league/oauth2-client\": \"^2.6\",\n        \"onelogin/php-saml\": \"^4.3.1\",\n        \"phpseclib/phpseclib\": \"^3.0\",\n        \"pragmarx/google2fa\": \"^9.0\",\n        \"predis/predis\": \"^3.2\",\n        \"socialiteproviders/discord\": \"^4.1\",\n        \"socialiteproviders/gitlab\": \"^4.1\",\n        \"socialiteproviders/microsoft-azure\": \"^5.1\",\n        \"socialiteproviders/okta\": \"^4.2\",\n        \"socialiteproviders/twitch\": \"^5.3\",\n        \"ssddanbrown/htmldiff\": \"^2.0.0\",\n        \"xemlock/htmlpurifier-html5\": \"^0.1.12\"\n    },\n    \"require-dev\": {\n        \"fakerphp/faker\": \"^1.21\",\n        \"itsgoingd/clockwork\": \"^5.1\",\n        \"mockery/mockery\": \"^1.5\",\n        \"nunomaduro/collision\": \"^8.6\",\n        \"larastan/larastan\": \"^v3.0\",\n        \"phpunit/phpunit\": \"^11.5\",\n        \"squizlabs/php_codesniffer\": \"^4.0.1\",\n        \"ssddanbrown/asserthtml\": \"^3.1\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"BookStack\\\\\": \"app/\",\n            \"Database\\\\Factories\\\\\": \"database/factories/\",\n            \"Database\\\\Seeders\\\\\": \"database/seeders/\"\n        },\n        \"files\": [\n            \"app/App/helpers.php\"\n        ]\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Tests\\\\\": \"tests/\"\n        }\n    },\n    \"scripts\": {\n        \"check-static\": \"phpstan --memory-limit=2g\",\n        \"format\": \"phpcbf\",\n        \"lint\": \"phpcs\",\n        \"test\": \"phpunit\",\n        \"t-reset\": \"@php artisan test --recreate-databases\",\n        \"build-licenses\": [\n            \"@php ./dev/licensing/gen-js-licenses\",\n            \"@php ./dev/licensing/gen-php-licenses\"\n        ],\n        \"post-autoload-dump\": [\n            \"Illuminate\\\\Foundation\\\\ComposerScripts::postAutoloadDump\",\n            \"@php artisan package:discover --ansi\"\n        ],\n        \"post-root-package-install\": [\n            \"@php -r \\\"file_exists('.env') || copy('.env.example', '.env');\\\"\"\n        ],\n        \"post-create-project-cmd\": [\n            \"@php artisan key:generate --ansi\"\n        ],\n        \"pre-install-cmd\": [\n            \"@php -r \\\"!file_exists('bootstrap/cache/services.php') || @unlink('bootstrap/cache/services.php');\\\"\"\n        ],\n        \"post-install-cmd\": [\n            \"@php artisan cache:clear\",\n            \"@php artisan view:clear\"\n        ],\n        \"refresh-test-database\": [\n            \"@putenv APP_TIMEZONE=UTC\",\n            \"@php artisan migrate:refresh --database=mysql_testing\",\n            \"@php artisan db:seed --class=DummyContentSeeder --database=mysql_testing\"\n        ]\n    },\n    \"config\": {\n        \"optimize-autoloader\": true,\n        \"preferred-install\": \"dist\",\n        \"sort-packages\": true,\n        \"platform\": {\n            \"php\": \"8.2.0\"\n        }\n    },\n    \"extra\": {\n        \"laravel\": {\n            \"dont-discover\": []\n        }\n    },\n    \"minimum-stability\": \"stable\",\n    \"prefer-stable\": true\n}\n"
  },
  {
    "path": "crowdin.yml",
    "content": "project_id: \"377219\"\nproject_identifier: bookstack\nbase_path: .\npreserve_hierarchy: false\npull_request_title: Updated translations with latest Crowdin changes\npull_request_labels:\n  - \":earth_africa: Translations\"\nfiles:\n  - source: /lang/en/*.php\n    translation: /lang/%two_letters_code%/%original_file_name%\n    type: php\n"
  },
  {
    "path": "database/.gitignore",
    "content": "*.sqlite\n"
  },
  {
    "path": "database/factories/Access/Mfa/MfaValueFactory.php",
    "content": "<?php\n\nnamespace Database\\Factories\\Access\\Mfa;\n\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\n\n/**\n * @extends \\Illuminate\\Database\\Eloquent\\Factories\\Factory<\\BookStack\\Access\\Mfa\\MfaValue>\n */\nclass MfaValueFactory extends Factory\n{\n    protected $model = \\BookStack\\Access\\Mfa\\MfaValue::class;\n\n    /**\n     * Define the model's default state.\n     *\n     * @return array<string, mixed>\n     */\n    public function definition(): array\n    {\n        return [\n            'user_id' => User::factory(),\n            'method' => 'totp',\n            'value' => '123456',\n        ];\n    }\n}\n"
  },
  {
    "path": "database/factories/Access/SocialAccountFactory.php",
    "content": "<?php\n\nnamespace Database\\Factories\\Access;\n\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\n\n/**\n * @extends \\Illuminate\\Database\\Eloquent\\Factories\\Factory<\\BookStack\\Access\\SocialAccount>\n */\nclass SocialAccountFactory extends Factory\n{\n    protected $model = \\BookStack\\Access\\SocialAccount::class;\n\n    /**\n     * Define the model's default state.\n     *\n     * @return array<string, mixed>\n     */\n    public function definition(): array\n    {\n        return [\n            'user_id' => User::factory(),\n            'driver' => 'github',\n            'driver_id' => '123456',\n            'avatar' => '',\n        ];\n    }\n}\n"
  },
  {
    "path": "database/factories/Activity/Models/ActivityFactory.php",
    "content": "<?php\n\nnamespace Database\\Factories\\Activity\\Models;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\n\n/**\n * @extends \\Illuminate\\Database\\Eloquent\\Factories\\Factory<\\BookStack\\Activity\\Models\\Activity>\n */\nclass ActivityFactory extends Factory\n{\n    protected $model = \\BookStack\\Activity\\Models\\Activity::class;\n\n    /**\n     * Define the model's default state.\n     *\n     * @return array<string, mixed>\n     */\n    public function definition(): array\n    {\n        $activities = ActivityType::all();\n        $activity = $activities[array_rand($activities)];\n        return [\n            'type' => $activity,\n            'detail' => 'Activity detail for ' . $activity,\n            'user_id' => User::factory(),\n            'ip' => $this->faker->ipv4(),\n            'loggable_id' => null,\n            'loggable_type' => null,\n        ];\n    }\n}\n"
  },
  {
    "path": "database/factories/Activity/Models/CommentFactory.php",
    "content": "<?php\n\nnamespace Database\\Factories\\Activity\\Models;\n\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\n\nclass CommentFactory extends Factory\n{\n    /**\n     * The name of the factory's corresponding model.\n     *\n     * @var string\n     */\n    protected $model = \\BookStack\\Activity\\Models\\Comment::class;\n\n    /**\n     * A static counter to provide a unique local_id for each comment.\n     */\n    protected static int $nextLocalId = 1000;\n\n    /**\n     * Define the model's default state.\n     *\n     * @return array\n     */\n    public function definition()\n    {\n        $text = $this->faker->paragraph(1);\n        $html = '<p>' . $text . '</p>';\n        $nextLocalId = static::$nextLocalId++;\n\n        $user = User::query()->first();\n\n        return [\n            'html'      => $html,\n            'parent_id' => null,\n            'local_id'  => $nextLocalId,\n            'content_ref' => '',\n            'archived' => false,\n            'created_by' => $user ?? User::factory(),\n            'updated_by' => $user ?? User::factory(),\n        ];\n    }\n}\n"
  },
  {
    "path": "database/factories/Activity/Models/FavouriteFactory.php",
    "content": "<?php\n\nnamespace Database\\Factories\\Activity\\Models;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\n\n/**\n * @extends \\Illuminate\\Database\\Eloquent\\Factories\\Factory<\\BookStack\\Activity\\Models\\Favourite>\n */\nclass FavouriteFactory extends Factory\n{\n    protected $model = \\BookStack\\Activity\\Models\\Favourite::class;\n\n    /**\n     * Define the model's default state.\n     *\n     * @return array<string, mixed>\n     */\n    public function definition(): array\n    {\n        $book = Book::query()->first();\n\n        return [\n            'user_id' => User::factory(),\n            'favouritable_id' => $book->id,\n            'favouritable_type' => 'book',\n        ];\n    }\n}\n"
  },
  {
    "path": "database/factories/Activity/Models/TagFactory.php",
    "content": "<?php\n\nnamespace Database\\Factories\\Activity\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\n\nclass TagFactory extends Factory\n{\n    /**\n     * The name of the factory's corresponding model.\n     *\n     * @var string\n     */\n    protected $model = \\BookStack\\Activity\\Models\\Tag::class;\n\n    /**\n     * Define the model's default state.\n     *\n     * @return array\n     */\n    public function definition()\n    {\n        return [\n            'name'  => $this->faker->city(),\n            'value' => $this->faker->sentence(3),\n        ];\n    }\n}\n"
  },
  {
    "path": "database/factories/Activity/Models/WatchFactory.php",
    "content": "<?php\n\nnamespace Database\\Factories\\Activity\\Models;\n\nuse BookStack\\Activity\\WatchLevels;\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\n\n/**\n * @extends \\Illuminate\\Database\\Eloquent\\Factories\\Factory<\\BookStack\\Activity\\Models\\Watch>\n */\nclass WatchFactory extends Factory\n{\n    protected $model = \\BookStack\\Activity\\Models\\Watch::class;\n\n    /**\n     * Define the model's default state.\n     *\n     * @return array<string, mixed>\n     */\n    public function definition(): array\n    {\n        $book = Book::factory()->create();\n\n        return [\n            'user_id' => User::factory(),\n            'watchable_id' => $book->id,\n            'watchable_type' => 'book',\n            'level' => WatchLevels::NEW,\n        ];\n    }\n}\n"
  },
  {
    "path": "database/factories/Activity/Models/WebhookFactory.php",
    "content": "<?php\n\nnamespace Database\\Factories\\Activity\\Models;\n\nuse BookStack\\Activity\\Models\\Webhook;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\n\nclass WebhookFactory extends Factory\n{\n    protected $model = Webhook::class;\n\n    /**\n     * Define the model's default state.\n     *\n     * @return array\n     */\n    public function definition()\n    {\n        return [\n            'name'     => 'My webhook for ' . $this->faker->country(),\n            'endpoint' => $this->faker->url(),\n            'active'   => true,\n            'timeout'  => 3,\n        ];\n    }\n}\n"
  },
  {
    "path": "database/factories/Activity/Models/WebhookTrackedEventFactory.php",
    "content": "<?php\n\nnamespace Database\\Factories\\Activity\\Models;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Activity\\Models\\Webhook;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\n\nclass WebhookTrackedEventFactory extends Factory\n{\n    /**\n     * Define the model's default state.\n     *\n     * @return array\n     */\n    public function definition()\n    {\n        return [\n            'webhook_id' => Webhook::factory(),\n            'event'      => ActivityType::all()[array_rand(ActivityType::all())],\n        ];\n    }\n}\n"
  },
  {
    "path": "database/factories/Api/ApiTokenFactory.php",
    "content": "<?php\n\nnamespace Database\\Factories\\Api;\n\nuse BookStack\\Api\\ApiToken;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Str;\n\nclass ApiTokenFactory extends Factory\n{\n    protected $model = ApiToken::class;\n\n    public function definition(): array\n    {\n        return [\n            'token_id' => Str::random(10),\n            'secret' => Str::random(12),\n            'name' => $this->faker->name(),\n            'expires_at' => Carbon::now()->addYear(),\n            'created_at' => Carbon::now(),\n            'updated_at' => Carbon::now(),\n            'user_id' => User::factory(),\n        ];\n    }\n}\n"
  },
  {
    "path": "database/factories/Entities/Models/BookFactory.php",
    "content": "<?php\n\nnamespace Database\\Factories\\Entities\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Support\\Str;\n\nclass BookFactory extends Factory\n{\n    /**\n     * The name of the factory's corresponding model.\n     *\n     * @var string\n     */\n    protected $model = \\BookStack\\Entities\\Models\\Book::class;\n\n    /**\n     * Define the model's default state.\n     *\n     * @return array\n     */\n    public function definition()\n    {\n        $description = $this->faker->paragraph();\n        return [\n            'name'        => $this->faker->sentence(),\n            'slug'        => Str::random(10),\n            'description' => $description,\n            'description_html' => '<p>' . e($description) . '</p>',\n            'sort_rule_id' => null,\n            'default_template_id' => null,\n        ];\n    }\n}\n"
  },
  {
    "path": "database/factories/Entities/Models/BookshelfFactory.php",
    "content": "<?php\n\nnamespace Database\\Factories\\Entities\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Support\\Str;\n\nclass BookshelfFactory extends Factory\n{\n    /**\n     * The name of the factory's corresponding model.\n     *\n     * @var string\n     */\n    protected $model = \\BookStack\\Entities\\Models\\Bookshelf::class;\n\n    /**\n     * Define the model's default state.\n     *\n     * @return array\n     */\n    public function definition()\n    {\n        $description = $this->faker->paragraph();\n        return [\n            'name'        => $this->faker->sentence,\n            'slug'        => Str::random(10),\n            'description' => $description,\n            'description_html' => '<p>' . e($description) . '</p>'\n        ];\n    }\n}\n"
  },
  {
    "path": "database/factories/Entities/Models/ChapterFactory.php",
    "content": "<?php\n\nnamespace Database\\Factories\\Entities\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Support\\Str;\n\nclass ChapterFactory extends Factory\n{\n    /**\n     * The name of the factory's corresponding model.\n     *\n     * @var string\n     */\n    protected $model = \\BookStack\\Entities\\Models\\Chapter::class;\n\n    /**\n     * Define the model's default state.\n     *\n     * @return array\n     */\n    public function definition()\n    {\n        $description = $this->faker->paragraph();\n        return [\n            'name'        => $this->faker->sentence(),\n            'slug'        => Str::random(10),\n            'description' => $description,\n            'description_html' => '<p>' . e($description) . '</p>',\n            'priority' => 5,\n        ];\n    }\n}\n"
  },
  {
    "path": "database/factories/Entities/Models/DeletionFactory.php",
    "content": "<?php\n\nnamespace Database\\Factories\\Entities\\Models;\n\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\n\n/**\n * @extends \\Illuminate\\Database\\Eloquent\\Factories\\Factory<\\BookStack\\Entities\\Models\\Deletion>\n */\nclass DeletionFactory extends Factory\n{\n    protected $model = \\BookStack\\Entities\\Models\\Deletion::class;\n\n    /**\n     * Define the model's default state.\n     *\n     * @return array<string, mixed>\n     */\n    public function definition(): array\n    {\n        return [\n            'deleted_by' => User::factory(),\n            'deletable_id' => Page::factory(),\n            'deletable_type' => 'page',\n        ];\n    }\n}\n"
  },
  {
    "path": "database/factories/Entities/Models/PageFactory.php",
    "content": "<?php\n\nnamespace Database\\Factories\\Entities\\Models;\n\nuse BookStack\\Entities\\Tools\\PageEditorType;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Support\\Str;\n\nclass PageFactory extends Factory\n{\n    /**\n     * The name of the factory's corresponding model.\n     *\n     * @var string\n     */\n    protected $model = \\BookStack\\Entities\\Models\\Page::class;\n\n    /**\n     * Define the model's default state.\n     */\n    public function definition(): array\n    {\n        $html = '<p>' . implode('</p>', $this->faker->paragraphs(5)) . '</p>';\n\n        return [\n            'name'           => $this->faker->sentence(),\n            'slug'           => Str::random(10),\n            'html'           => $html,\n            'text'           => strip_tags($html),\n            'revision_count' => 1,\n            'editor'         => 'wysiwyg',\n            'priority'       => 1,\n        ];\n    }\n}\n"
  },
  {
    "path": "database/factories/Entities/Models/PageRevisionFactory.php",
    "content": "<?php\n\nnamespace Database\\Factories\\Entities\\Models;\n\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\n\nclass PageRevisionFactory extends Factory\n{\n    /**\n     * The name of the factory's corresponding model.\n     *\n     * @var string\n     */\n    protected $model = \\BookStack\\Entities\\Models\\PageRevision::class;\n\n    /**\n     * Define the model's default state.\n     */\n    public function definition(): array\n    {\n        $html = '<p>' . implode('</p>', $this->faker->paragraphs(5)) . '</p>';\n        $page = Page::query()->first();\n\n        return [\n            'page_id'        => $page->id,\n            'name'           => $this->faker->sentence(),\n            'html'           => $html,\n            'text'           => strip_tags($html),\n            'created_by'     => User::factory(),\n            'slug'           => $page->slug,\n            'book_slug'      => $page->book->slug,\n            'type'           => 'version',\n            'markdown'       => strip_tags($html),\n            'summary'        => $this->faker->sentence(),\n            'revision_number' => rand(1, 4000),\n        ];\n    }\n}\n"
  },
  {
    "path": "database/factories/Entities/Models/SlugHistoryFactory.php",
    "content": "<?php\n\nnamespace Database\\Factories\\Entities\\Models;\n\nuse BookStack\\Entities\\Models\\Book;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\n\n/**\n * @extends \\Illuminate\\Database\\Eloquent\\Factories\\Factory<\\BookStack\\Entities\\Models\\SlugHistory>\n */\nclass SlugHistoryFactory extends Factory\n{\n    protected $model = \\BookStack\\Entities\\Models\\SlugHistory::class;\n\n    /**\n     * Define the model's default state.\n     *\n     * @return array<string, mixed>\n     */\n    public function definition(): array\n    {\n        return [\n            'sluggable_id' => Book::factory(),\n            'sluggable_type' => 'book',\n            'slug' => $this->faker->slug(),\n            'parent_slug' => null,\n        ];\n    }\n}\n"
  },
  {
    "path": "database/factories/Exports/ImportFactory.php",
    "content": "<?php\n\nnamespace Database\\Factories\\Exports;\n\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Support\\Str;\n\nclass ImportFactory extends Factory\n{\n    /**\n     * The name of the factory's corresponding model.\n     *\n     * @var string\n     */\n    protected $model = \\BookStack\\Exports\\Import::class;\n\n    /**\n     * Define the model's default state.\n     */\n    public function definition(): array\n    {\n        return [\n            'path' => 'uploads/files/imports/' . Str::random(10) . '.zip',\n            'name' => $this->faker->words(3, true),\n            'type' => 'book',\n            'size' => rand(1, 1001),\n            'metadata' => '{\"name\": \"My book\"}',\n            'created_at' => User::factory(),\n        ];\n    }\n}\n"
  },
  {
    "path": "database/factories/Sorting/SortRuleFactory.php",
    "content": "<?php\n\nnamespace Database\\Factories\\Sorting;\n\nuse BookStack\\Sorting\\SortRule;\nuse BookStack\\Sorting\\SortRuleOperation;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\n\nclass SortRuleFactory extends Factory\n{\n    /**\n     * The name of the factory's corresponding model.\n     *\n     * @var string\n     */\n    protected $model = SortRule::class;\n\n    /**\n     * Define the model's default state.\n     */\n    public function definition(): array\n    {\n        $cases = SortRuleOperation::cases();\n        $op = $cases[array_rand($cases)];\n        return [\n            'name' => $op->name . ' Sort',\n            'sequence' => $op->value,\n        ];\n    }\n}\n"
  },
  {
    "path": "database/factories/Uploads/AttachmentFactory.php",
    "content": "<?php\n\nnamespace Database\\Factories\\Uploads;\n\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\n\n/**\n * @extends \\Illuminate\\Database\\Eloquent\\Factories\\Factory<\\BookStack\\Uploads\\Attachment>\n */\nclass AttachmentFactory extends Factory\n{\n    /**\n     * The name of the factory's corresponding model.\n     *\n     * @var string\n     */\n    protected $model = \\BookStack\\Uploads\\Attachment::class;\n\n    /**\n     * Define the model's default state.\n     *\n     * @return array<string, mixed>\n     */\n    public function definition()\n    {\n        return [\n            'name' => $this->faker->words(2, true),\n            'path' => $this->faker->url(),\n            'extension' => '',\n            'external' => true,\n            'uploaded_to' => Page::factory(),\n            'created_by' => User::factory(),\n            'updated_by' => User::factory(),\n            'order' => 0,\n        ];\n    }\n}\n"
  },
  {
    "path": "database/factories/Uploads/ImageFactory.php",
    "content": "<?php\n\nnamespace Database\\Factories\\Uploads;\n\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\n\nclass ImageFactory extends Factory\n{\n    /**\n     * The name of the factory's corresponding model.\n     *\n     * @var string\n     */\n    protected $model = \\BookStack\\Uploads\\Image::class;\n\n    /**\n     * Define the model's default state.\n     *\n     * @return array\n     */\n    public function definition()\n    {\n        return [\n            'name'        => $this->faker->slug() . '.jpg',\n            'url'         => $this->faker->url(),\n            'path'        => $this->faker->url(),\n            'type'        => 'gallery',\n            'uploaded_to' => 0,\n        ];\n    }\n}\n"
  },
  {
    "path": "database/factories/Users/Models/RoleFactory.php",
    "content": "<?php\n\nnamespace Database\\Factories\\Users\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\n\nclass RoleFactory extends Factory\n{\n    /**\n     * The name of the factory's corresponding model.\n     *\n     * @var string\n     */\n    protected $model = \\BookStack\\Users\\Models\\Role::class;\n\n    /**\n     * Define the model's default state.\n     *\n     * @return array\n     */\n    public function definition()\n    {\n        return [\n            'display_name'     => $this->faker->sentence(3),\n            'description'      => $this->faker->sentence(10),\n            'external_auth_id' => '',\n        ];\n    }\n}\n"
  },
  {
    "path": "database/factories/Users/Models/UserFactory.php",
    "content": "<?php\n\nnamespace Database\\Factories\\Users\\Models;\n\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Support\\Str;\n\nclass UserFactory extends Factory\n{\n    /**\n     * The name of the factory's corresponding model.\n     *\n     * @var string\n     */\n    protected $model = User::class;\n\n    /**\n     * Define the model's default state.\n     *\n     * @return array\n     */\n    public function definition()\n    {\n        $name = $this->faker->name();\n\n        return [\n            'name'            => $name,\n            'email'           => $this->faker->email(),\n            'slug'            => Str::slug($name . '-' . Str::random(5)),\n            'password'        => Str::random(10),\n            'remember_token'  => Str::random(10),\n            'email_confirmed' => 1,\n        ];\n    }\n}\n"
  },
  {
    "path": "database/migrations/.gitkeep",
    "content": ""
  },
  {
    "path": "database/migrations/2014_10_12_000000_create_users_table.php",
    "content": "<?php\n\nuse Carbon\\Carbon;\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('users', function (Blueprint $table) {\n            $table->increments('id');\n            $table->string('name');\n            $table->string('email')->unique();\n            $table->string('password', 60);\n            $table->rememberToken();\n            $table->nullableTimestamps();\n        });\n\n        // Create the initial admin user\n        DB::table('users')->insert([\n            'name'       => 'Admin',\n            'email'      => 'admin@admin.com',\n            'password'   => bcrypt('password'),\n            'created_at' => Carbon::now()->toDateTimeString(),\n            'updated_at' => Carbon::now()->toDateTimeString(),\n        ]);\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::drop('users');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2014_10_12_100000_create_password_resets_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('password_resets', function (Blueprint $table) {\n            $table->string('email')->index();\n            $table->string('token')->index();\n            $table->timestamp('created_at');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::drop('password_resets');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2015_07_12_114933_create_books_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('books', function (Blueprint $table) {\n            $table->increments('id');\n            $table->string('name');\n            $table->string('slug')->indexed();\n            $table->text('description');\n            $table->nullableTimestamps();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::drop('books');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2015_07_12_190027_create_pages_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('pages', function (Blueprint $table) {\n            $table->increments('id');\n            $table->integer('book_id');\n            $table->integer('chapter_id');\n            $table->string('name');\n            $table->string('slug')->indexed();\n            $table->longText('html');\n            $table->longText('text');\n            $table->integer('priority');\n            $table->nullableTimestamps();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::drop('pages');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2015_07_13_172121_create_images_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('images', function (Blueprint $table) {\n            $table->increments('id');\n            $table->string('name');\n            $table->string('url');\n            $table->nullableTimestamps();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::drop('images');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2015_07_27_172342_create_chapters_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('chapters', function (Blueprint $table) {\n            $table->increments('id');\n            $table->integer('book_id');\n            $table->string('slug')->indexed();\n            $table->text('name');\n            $table->text('description');\n            $table->integer('priority');\n            $table->nullableTimestamps();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::drop('chapters');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2015_08_08_200447_add_users_to_entities.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('pages', function (Blueprint $table) {\n            $table->integer('created_by');\n            $table->integer('updated_by');\n        });\n        Schema::table('chapters', function (Blueprint $table) {\n            $table->integer('created_by');\n            $table->integer('updated_by');\n        });\n        Schema::table('images', function (Blueprint $table) {\n            $table->integer('created_by');\n            $table->integer('updated_by');\n        });\n        Schema::table('books', function (Blueprint $table) {\n            $table->integer('created_by');\n            $table->integer('updated_by');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('pages', function (Blueprint $table) {\n            $table->dropColumn('created_by');\n            $table->dropColumn('updated_by');\n        });\n        Schema::table('chapters', function (Blueprint $table) {\n            $table->dropColumn('created_by');\n            $table->dropColumn('updated_by');\n        });\n        Schema::table('images', function (Blueprint $table) {\n            $table->dropColumn('created_by');\n            $table->dropColumn('updated_by');\n        });\n        Schema::table('books', function (Blueprint $table) {\n            $table->dropColumn('created_by');\n            $table->dropColumn('updated_by');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2015_08_09_093534_create_page_revisions_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('page_revisions', function (Blueprint $table) {\n            $table->increments('id');\n            $table->integer('page_id')->indexed();\n            $table->string('name');\n            $table->longText('html');\n            $table->longText('text');\n            $table->integer('created_by');\n            $table->nullableTimestamps();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::drop('page_revisions');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2015_08_16_142133_create_activities_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('activities', function (Blueprint $table) {\n            $table->increments('id');\n            $table->string('key');\n            $table->text('extra');\n            $table->integer('book_id')->indexed();\n            $table->integer('user_id');\n            $table->integer('entity_id');\n            $table->string('entity_type');\n            $table->nullableTimestamps();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::drop('activities');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2015_08_29_105422_add_roles_and_permissions.php",
    "content": "<?php\n\n/**\n * Much of this code has been taken from entrust,\n * a role & permission management solution for Laravel.\n *\n * Full attribution of the database Schema shown below goes to the entrust project.\n *\n * @license MIT\n * @url https://github.com/Zizaco/entrust\n */\n\nuse Carbon\\Carbon;\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        // Create table for storing roles\n        Schema::create('roles', function (Blueprint $table) {\n            $table->increments('id');\n            $table->string('name')->unique();\n            $table->string('display_name')->nullable();\n            $table->string('description')->nullable();\n            $table->nullableTimestamps();\n        });\n\n        // Create table for associating roles to users (Many-to-Many)\n        Schema::create('role_user', function (Blueprint $table) {\n            $table->integer('user_id')->unsigned();\n            $table->integer('role_id')->unsigned();\n\n            $table->foreign('user_id')->references('id')->on('users')\n                ->onUpdate('cascade')->onDelete('cascade');\n            $table->foreign('role_id')->references('id')->on('roles')\n                ->onUpdate('cascade')->onDelete('cascade');\n\n            $table->primary(['user_id', 'role_id']);\n        });\n\n        // Create table for storing permissions\n        Schema::create('permissions', function (Blueprint $table) {\n            $table->increments('id');\n            $table->string('name')->unique();\n            $table->string('display_name')->nullable();\n            $table->string('description')->nullable();\n            $table->nullableTimestamps();\n        });\n\n        // Create table for associating permissions to roles (Many-to-Many)\n        Schema::create('permission_role', function (Blueprint $table) {\n            $table->integer('permission_id')->unsigned();\n            $table->integer('role_id')->unsigned();\n\n            $table->foreign('permission_id')->references('id')->on('permissions')\n                ->onUpdate('cascade')->onDelete('cascade');\n            $table->foreign('role_id')->references('id')->on('roles')\n                ->onUpdate('cascade')->onDelete('cascade');\n\n            $table->primary(['permission_id', 'role_id']);\n        });\n\n        // Create default roles\n        $adminId = DB::table('roles')->insertGetId([\n            'name'         => 'admin',\n            'display_name' => 'Admin',\n            'description'  => 'Administrator of the whole application',\n            'created_at'   => Carbon::now()->toDateTimeString(),\n            'updated_at'   => Carbon::now()->toDateTimeString(),\n        ]);\n        $editorId = DB::table('roles')->insertGetId([\n            'name'         => 'editor',\n            'display_name' => 'Editor',\n            'description'  => 'User can edit Books, Chapters & Pages',\n            'created_at'   => Carbon::now()->toDateTimeString(),\n            'updated_at'   => Carbon::now()->toDateTimeString(),\n        ]);\n        $viewerId = DB::table('roles')->insertGetId([\n            'name'         => 'viewer',\n            'display_name' => 'Viewer',\n            'description'  => 'User can view books & their content behind authentication',\n            'created_at'   => Carbon::now()->toDateTimeString(),\n            'updated_at'   => Carbon::now()->toDateTimeString(),\n        ]);\n\n        // Create default CRUD permissions and allocate to admins and editors\n        $entities = ['Book', 'Page', 'Chapter', 'Image'];\n        $ops = ['Create', 'Update', 'Delete'];\n        foreach ($entities as $entity) {\n            foreach ($ops as $op) {\n                $newPermId = DB::table('permissions')->insertGetId([\n                    'name'         => strtolower($entity) . '-' . strtolower($op),\n                    'display_name' => $op . ' ' . $entity . 's',\n                    'created_at'   => Carbon::now()->toDateTimeString(),\n                    'updated_at'   => Carbon::now()->toDateTimeString(),\n                ]);\n                DB::table('permission_role')->insert([\n                    ['permission_id' => $newPermId, 'role_id' => $adminId],\n                    ['permission_id' => $newPermId, 'role_id' => $editorId],\n                ]);\n            }\n        }\n\n        // Create admin permissions\n        $entities = ['Settings', 'User'];\n        $ops = ['Create', 'Update', 'Delete'];\n        foreach ($entities as $entity) {\n            foreach ($ops as $op) {\n                $newPermId = DB::table('permissions')->insertGetId([\n                    'name'         => strtolower($entity) . '-' . strtolower($op),\n                    'display_name' => $op . ' ' . $entity,\n                    'created_at'   => Carbon::now()->toDateTimeString(),\n                    'updated_at'   => Carbon::now()->toDateTimeString(),\n                ]);\n                DB::table('permission_role')->insert([\n                    'permission_id' => $newPermId,\n                    'role_id'       => $adminId,\n                ]);\n            }\n        }\n\n        // Set all current users as admins\n        // (At this point only the initially create user should be an admin)\n        $users = DB::table('users')->get()->all();\n        foreach ($users as $user) {\n            DB::table('role_user')->insert([\n                'role_id' => $adminId,\n                'user_id' => $user->id,\n            ]);\n        }\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::drop('permission_role');\n        Schema::drop('permissions');\n        Schema::drop('role_user');\n        Schema::drop('roles');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2015_08_30_125859_create_settings_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('settings', function (Blueprint $table) {\n            $table->string('setting_key')->primary()->indexed();\n            $table->text('value');\n            $table->nullableTimestamps();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::drop('settings');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2015_08_31_175240_add_search_indexes.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up()\n    {\n        // This was removed for v0.24 since these indexes are removed anyway\n        // and will cause issues for db engines that don't support such indexes.\n\n//        $prefix = DB::getTablePrefix();\n//        DB::statement(\"ALTER TABLE {$prefix}pages ADD FULLTEXT search(name, text)\");\n//        DB::statement(\"ALTER TABLE {$prefix}books ADD FULLTEXT search(name, description)\");\n//        DB::statement(\"ALTER TABLE {$prefix}chapters ADD FULLTEXT search(name, description)\");\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        if (Schema::hasIndex('pages', 'search')) {\n            Schema::table('pages', function (Blueprint $table) {\n                $table->dropIndex('search');\n            });\n        }\n\n        if (Schema::hasIndex('books', 'search')) {\n            Schema::table('books', function (Blueprint $table) {\n                $table->dropIndex('search');\n            });\n        }\n\n        if (Schema::hasIndex('chapters', 'search')) {\n            Schema::table('chapters', function (Blueprint $table) {\n                $table->dropIndex('search');\n            });\n        }\n    }\n};\n"
  },
  {
    "path": "database/migrations/2015_09_04_165821_create_social_accounts_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('social_accounts', function (Blueprint $table) {\n            $table->increments('id');\n            $table->integer('user_id')->index();\n            $table->string('driver')->index();\n            $table->string('driver_id');\n            $table->string('avatar');\n            $table->nullableTimestamps();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::drop('social_accounts');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2015_09_05_164707_add_email_confirmation_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('users', function (Blueprint $table) {\n            $table->boolean('email_confirmed')->default(true);\n        });\n\n        Schema::create('email_confirmations', function (Blueprint $table) {\n            $table->increments('id');\n            $table->integer('user_id')->index();\n            $table->string('token')->index();\n            $table->nullableTimestamps();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('users', function (Blueprint $table) {\n            $table->dropColumn('email_confirmed');\n        });\n        Schema::drop('email_confirmations');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2015_11_21_145609_create_views_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('views', function (Blueprint $table) {\n            $table->increments('id');\n            $table->integer('user_id');\n            $table->integer('viewable_id');\n            $table->string('viewable_type');\n            $table->integer('views');\n            $table->nullableTimestamps();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::drop('views');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2015_11_26_221857_add_entity_indexes.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('books', function (Blueprint $table) {\n            $table->index('slug');\n            $table->index('created_by');\n            $table->index('updated_by');\n        });\n        Schema::table('pages', function (Blueprint $table) {\n            $table->index('slug');\n            $table->index('book_id');\n            $table->index('chapter_id');\n            $table->index('priority');\n            $table->index('created_by');\n            $table->index('updated_by');\n        });\n        Schema::table('page_revisions', function (Blueprint $table) {\n            $table->index('page_id');\n        });\n        Schema::table('chapters', function (Blueprint $table) {\n            $table->index('slug');\n            $table->index('book_id');\n            $table->index('priority');\n            $table->index('created_by');\n            $table->index('updated_by');\n        });\n        Schema::table('activities', function (Blueprint $table) {\n            $table->index('book_id');\n            $table->index('user_id');\n            $table->index('entity_id');\n        });\n        Schema::table('views', function (Blueprint $table) {\n            $table->index('user_id');\n            $table->index('viewable_id');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('books', function (Blueprint $table) {\n            $table->dropIndex('books_slug_index');\n            $table->dropIndex('books_created_by_index');\n            $table->dropIndex('books_updated_by_index');\n        });\n        Schema::table('pages', function (Blueprint $table) {\n            $table->dropIndex('pages_slug_index');\n            $table->dropIndex('pages_book_id_index');\n            $table->dropIndex('pages_chapter_id_index');\n            $table->dropIndex('pages_priority_index');\n            $table->dropIndex('pages_created_by_index');\n            $table->dropIndex('pages_updated_by_index');\n        });\n        Schema::table('page_revisions', function (Blueprint $table) {\n            $table->dropIndex('page_revisions_page_id_index');\n        });\n        Schema::table('chapters', function (Blueprint $table) {\n            $table->dropIndex('chapters_slug_index');\n            $table->dropIndex('chapters_book_id_index');\n            $table->dropIndex('chapters_priority_index');\n            $table->dropIndex('chapters_created_by_index');\n            $table->dropIndex('chapters_updated_by_index');\n        });\n        Schema::table('activities', function (Blueprint $table) {\n            $table->dropIndex('activities_book_id_index');\n            $table->dropIndex('activities_user_id_index');\n            $table->dropIndex('activities_entity_id_index');\n        });\n        Schema::table('views', function (Blueprint $table) {\n            $table->dropIndex('views_user_id_index');\n            $table->dropIndex('views_viewable_id_index');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2015_12_05_145049_fulltext_weighting.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up()\n    {\n        // This was removed for v0.24 since these indexes are removed anyway\n        // and will cause issues for db engines that don't support such indexes.\n\n//        $prefix = DB::getTablePrefix();\n//        DB::statement(\"ALTER TABLE {$prefix}pages ADD FULLTEXT name_search(name)\");\n//        DB::statement(\"ALTER TABLE {$prefix}books ADD FULLTEXT name_search(name)\");\n//        DB::statement(\"ALTER TABLE {$prefix}chapters ADD FULLTEXT name_search(name)\");\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        if (Schema::hasIndex('pages', 'name_search')) {\n            Schema::table('pages', function (Blueprint $table) {\n                $table->dropIndex('name_search');\n            });\n        }\n\n        if (Schema::hasIndex('books', 'name_search')) {\n            Schema::table('books', function (Blueprint $table) {\n                $table->dropIndex('name_search');\n            });\n        }\n\n        if (Schema::hasIndex('chapters', 'name_search')) {\n            Schema::table('chapters', function (Blueprint $table) {\n                $table->dropIndex('name_search');\n            });\n        }\n    }\n};\n"
  },
  {
    "path": "database/migrations/2015_12_07_195238_add_image_upload_types.php",
    "content": "<?php\n\nuse BookStack\\Uploads\\Image;\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('images', function (Blueprint $table) {\n            $table->string('path', 400);\n            $table->string('type')->index();\n        });\n\n        Image::all()->each(function ($image) {\n            $image->path = $image->url;\n            $image->type = 'gallery';\n            $image->save();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('images', function (Blueprint $table) {\n            $table->dropColumn('type');\n            $table->dropColumn('path');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2015_12_09_195748_add_user_avatars.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('users', function (Blueprint $table) {\n            $table->integer('image_id')->default(0);\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('users', function (Blueprint $table) {\n            $table->dropColumn('image_id');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2016_01_11_210908_add_external_auth_to_users.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('users', function (Blueprint $table) {\n            $table->string('external_auth_id')->index();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('users', function (Blueprint $table) {\n            $table->dropColumn('external_auth_id');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2016_02_25_184030_add_slug_to_revisions.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('page_revisions', function (Blueprint $table) {\n            $table->string('slug');\n            $table->index('slug');\n            $table->string('book_slug');\n            $table->index('book_slug');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('page_revisions', function (Blueprint $table) {\n            $table->dropColumn('slug');\n            $table->dropColumn('book_slug');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2016_02_27_120329_update_permissions_and_roles.php",
    "content": "<?php\n\nuse Carbon\\Carbon;\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Support\\Facades\\DB;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        // Get roles with permissions we need to change\n        $adminRoleId = DB::table('roles')->where('name', '=', 'admin')->first()->id;\n        $editorRole = DB::table('roles')->where('name', '=', 'editor')->first();\n\n        // Delete old permissions\n        $permissions = DB::table('permissions')->delete();\n\n        // Create & attach new admin permissions\n        $permissionsToCreate = [\n            'settings-manage'         => 'Manage Settings',\n            'users-manage'            => 'Manage Users',\n            'user-roles-manage'       => 'Manage Roles & Permissions',\n            'restrictions-manage-all' => 'Manage All Entity Permissions',\n            'restrictions-manage-own' => 'Manage Entity Permissions On Own Content',\n        ];\n        foreach ($permissionsToCreate as $name => $displayName) {\n            $permissionId = DB::table('permissions')->insertGetId([\n                'name'         => $name,\n                'display_name' => $displayName,\n                'created_at'   => Carbon::now()->toDateTimeString(),\n                'updated_at'   => Carbon::now()->toDateTimeString(),\n            ]);\n            DB::table('permission_role')->insert([\n                'role_id'       => $adminRoleId,\n                'permission_id' => $permissionId,\n            ]);\n        }\n\n        // Create & attach new entity permissions\n        $entities = ['Book', 'Page', 'Chapter', 'Image'];\n        $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];\n        foreach ($entities as $entity) {\n            foreach ($ops as $op) {\n                $permissionId = DB::table('permissions')->insertGetId([\n                    'name'         => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),\n                    'display_name' => $op . ' ' . $entity . 's',\n                    'created_at'   => Carbon::now()->toDateTimeString(),\n                    'updated_at'   => Carbon::now()->toDateTimeString(),\n                ]);\n                DB::table('permission_role')->insert([\n                    'role_id'       => $adminRoleId,\n                    'permission_id' => $permissionId,\n                ]);\n                if ($editorRole !== null) {\n                    DB::table('permission_role')->insert([\n                        'role_id'       => $editorRole->id,\n                        'permission_id' => $permissionId,\n                    ]);\n                }\n            }\n        }\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        // Get roles with permissions we need to change\n        $adminRoleId = DB::table('roles')->where('name', '=', 'admin')->first()->id;\n\n        // Delete old permissions\n        $permissions = DB::table('permissions')->delete();\n\n        // Create default CRUD permissions and allocate to admins and editors\n        $entities = ['Book', 'Page', 'Chapter', 'Image'];\n        $ops = ['Create', 'Update', 'Delete'];\n        foreach ($entities as $entity) {\n            foreach ($ops as $op) {\n                $permissionId = DB::table('permissions')->insertGetId([\n                    'name'         => strtolower($entity) . '-' . strtolower($op),\n                    'display_name' => $op . ' ' . $entity . 's',\n                    'created_at'   => Carbon::now()->toDateTimeString(),\n                    'updated_at'   => Carbon::now()->toDateTimeString(),\n                ]);\n                DB::table('permission_role')->insert([\n                    'role_id'       => $adminRoleId,\n                    'permission_id' => $permissionId,\n                ]);\n            }\n        }\n\n        // Create admin permissions\n        $entities = ['Settings', 'User'];\n        $ops = ['Create', 'Update', 'Delete'];\n        foreach ($entities as $entity) {\n            foreach ($ops as $op) {\n                $permissionId = DB::table('permissions')->insertGetId([\n                    'name'         => strtolower($entity) . '-' . strtolower($op),\n                    'display_name' => $op . ' ' . $entity,\n                    'created_at'   => Carbon::now()->toDateTimeString(),\n                    'updated_at'   => Carbon::now()->toDateTimeString(),\n                ]);\n                DB::table('permission_role')->insert([\n                    'role_id'       => $adminRoleId,\n                    'permission_id' => $permissionId,\n                ]);\n            }\n        }\n    }\n};\n"
  },
  {
    "path": "database/migrations/2016_02_28_084200_add_entity_access_controls.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('images', function (Blueprint $table) {\n            $table->integer('uploaded_to')->default(0);\n            $table->index('uploaded_to');\n        });\n\n        Schema::table('books', function (Blueprint $table) {\n            $table->boolean('restricted')->default(false);\n            $table->index('restricted');\n        });\n\n        Schema::table('chapters', function (Blueprint $table) {\n            $table->boolean('restricted')->default(false);\n            $table->index('restricted');\n        });\n\n        Schema::table('pages', function (Blueprint $table) {\n            $table->boolean('restricted')->default(false);\n            $table->index('restricted');\n        });\n\n        Schema::create('restrictions', function (Blueprint $table) {\n            $table->increments('id');\n            $table->integer('restrictable_id');\n            $table->string('restrictable_type');\n            $table->integer('role_id');\n            $table->string('action');\n            $table->index('role_id');\n            $table->index('action');\n            $table->index(['restrictable_id', 'restrictable_type']);\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('images', function (Blueprint $table) {\n            $table->dropColumn('uploaded_to');\n        });\n\n        Schema::table('books', function (Blueprint $table) {\n            $table->dropColumn('restricted');\n        });\n\n        Schema::table('chapters', function (Blueprint $table) {\n            $table->dropColumn('restricted');\n        });\n\n        Schema::table('pages', function (Blueprint $table) {\n            $table->dropColumn('restricted');\n        });\n\n        Schema::drop('restrictions');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2016_03_09_203143_add_page_revision_types.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('page_revisions', function (Blueprint $table) {\n            $table->string('type')->default('version');\n            $table->index('type');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('page_revisions', function (Blueprint $table) {\n            $table->dropColumn('type');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2016_03_13_082138_add_page_drafts.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('pages', function (Blueprint $table) {\n            $table->boolean('draft')->default(false);\n            $table->index('draft');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('pages', function (Blueprint $table) {\n            $table->dropColumn('draft');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2016_03_25_123157_add_markdown_support.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('pages', function (Blueprint $table) {\n            $table->longText('markdown')->default('');\n        });\n\n        Schema::table('page_revisions', function (Blueprint $table) {\n            $table->longText('markdown')->default('');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('pages', function (Blueprint $table) {\n            $table->dropColumn('markdown');\n        });\n\n        Schema::table('page_revisions', function (Blueprint $table) {\n            $table->dropColumn('markdown');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2016_04_09_100730_add_view_permissions_to_roles.php",
    "content": "<?php\n\nuse Carbon\\Carbon;\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Support\\Facades\\DB;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        $currentRoles = DB::table('roles')->get();\n\n        // Create new view permission\n        $entities = ['Book', 'Page', 'Chapter'];\n        $ops = ['View All', 'View Own'];\n        foreach ($entities as $entity) {\n            foreach ($ops as $op) {\n                $permId = DB::table('permissions')->insertGetId([\n                    'name'         => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),\n                    'display_name' => $op . ' ' . $entity . 's',\n                    'created_at'   => Carbon::now()->toDateTimeString(),\n                    'updated_at'   => Carbon::now()->toDateTimeString(),\n                ]);\n                // Assign view permission to all current roles\n                foreach ($currentRoles as $role) {\n                    DB::table('permission_role')->insert([\n                        'role_id'       => $role->id,\n                        'permission_id' => $permId,\n                    ]);\n                }\n            }\n        }\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        // Delete the new view permission\n        $entities = ['Book', 'Page', 'Chapter'];\n        $ops = ['View All', 'View Own'];\n        foreach ($entities as $entity) {\n            foreach ($ops as $op) {\n                $permissionName = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));\n                $permission = DB::table('permissions')->where('name', '=', $permissionName)->first();\n                DB::table('permission_role')->where('permission_id', '=', $permission->id)->delete();\n                DB::table('permissions')->where('name', '=', $permissionName)->delete();\n            }\n        }\n    }\n};\n"
  },
  {
    "path": "database/migrations/2016_04_20_192649_create_joint_permissions_table.php",
    "content": "<?php\n\nuse Carbon\\Carbon;\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Illuminate\\Support\\Str;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('joint_permissions', function (Blueprint $table) {\n            $table->increments('id');\n            $table->integer('role_id');\n            $table->string('entity_type');\n            $table->integer('entity_id');\n            $table->string('action');\n            $table->boolean('has_permission')->default(false);\n            $table->boolean('has_permission_own')->default(false);\n            $table->integer('created_by');\n            // Create indexes\n            $table->index(['entity_id', 'entity_type']);\n            $table->index('has_permission');\n            $table->index('has_permission_own');\n            $table->index('role_id');\n            $table->index('action');\n            $table->index('created_by');\n        });\n\n        Schema::table('roles', function (Blueprint $table) {\n            $table->string('system_name');\n            $table->boolean('hidden')->default(false);\n            $table->index('hidden');\n            $table->index('system_name');\n        });\n\n        Schema::rename('permissions', 'role_permissions');\n        Schema::rename('restrictions', 'entity_permissions');\n\n        // Create the new public role\n        $publicRoleData = [\n            'name'         => 'public',\n            'display_name' => 'Public',\n            'description'  => 'The role given to public visitors if allowed',\n            'system_name'  => 'public',\n            'hidden'       => true,\n            'created_at'   => Carbon::now()->toDateTimeString(),\n            'updated_at'   => Carbon::now()->toDateTimeString(),\n        ];\n\n        // Ensure unique name\n        while (DB::table('roles')->where('name', '=', $publicRoleData['display_name'])->count() > 0) {\n            $publicRoleData['display_name'] = $publicRoleData['display_name'] . Str::random(2);\n        }\n        $publicRoleId = DB::table('roles')->insertGetId($publicRoleData);\n\n        // Add new view permissions to public role\n        $entities = ['Book', 'Page', 'Chapter'];\n        $ops = ['View All', 'View Own'];\n        foreach ($entities as $entity) {\n            foreach ($ops as $op) {\n                $name = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));\n                $permission = DB::table('role_permissions')->where('name', '=', $name)->first();\n                // Assign view permission to public\n                DB::table('permission_role')->insert([\n                    'permission_id' => $permission->id,\n                    'role_id'       => $publicRoleId,\n                ]);\n            }\n        }\n\n        // Update admin role with system name\n        DB::table('roles')->where('name', '=', 'admin')->update(['system_name' => 'admin']);\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::drop('joint_permissions');\n\n        Schema::rename('role_permissions', 'permissions');\n        Schema::rename('entity_permissions', 'restrictions');\n\n        // Delete the public role\n        DB::table('roles')->where('system_name', '=', 'public')->delete();\n\n        Schema::table('roles', function (Blueprint $table) {\n            $table->dropColumn('system_name');\n            $table->dropColumn('hidden');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2016_05_06_185215_create_tags_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('tags', function (Blueprint $table) {\n            $table->increments('id');\n            $table->integer('entity_id');\n            $table->string('entity_type', 100);\n            $table->string('name');\n            $table->string('value');\n            $table->integer('order');\n            $table->timestamps();\n\n            $table->index('name');\n            $table->index('value');\n            $table->index('order');\n            $table->index(['entity_id', 'entity_type']);\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::drop('tags');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2016_07_07_181521_add_summary_to_page_revisions.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('page_revisions', function ($table) {\n            $table->string('summary')->nullable();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('page_revisions', function ($table) {\n            $table->dropColumn('summary');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2016_09_29_101449_remove_hidden_roles.php",
    "content": "<?php\n\nuse Carbon\\Carbon;\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        // Remove the hidden property from roles\n        Schema::table('roles', function (Blueprint $table) {\n            $table->dropColumn('hidden');\n        });\n\n        // Add column to mark system users\n        Schema::table('users', function (Blueprint $table) {\n            $table->string('system_name')->nullable()->index();\n        });\n\n        // Insert our new public system user.\n        $publicUserId = DB::table('users')->insertGetId([\n            'email'           => 'guest@example.com',\n            'name'            => 'Guest',\n            'system_name'     => 'public',\n            'email_confirmed' => true,\n            'created_at'      => Carbon::now(),\n            'updated_at'      => Carbon::now(),\n        ]);\n\n        // Get the public role\n        $publicRole = DB::table('roles')->where('system_name', '=', 'public')->first();\n\n        // Connect the new public user to the public role\n        DB::table('role_user')->insert([\n            'user_id' => $publicUserId,\n            'role_id' => $publicRole->id,\n        ]);\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('roles', function (Blueprint $table) {\n            $table->boolean('hidden')->default(false);\n            $table->index('hidden');\n        });\n\n        DB::table('users')->where('system_name', '=', 'public')->delete();\n\n        Schema::table('users', function (Blueprint $table) {\n            $table->dropColumn('system_name');\n        });\n\n        DB::table('roles')->where('system_name', '=', 'public')->update(['hidden' => true]);\n    }\n};\n"
  },
  {
    "path": "database/migrations/2016_10_09_142037_create_attachments_table.php",
    "content": "<?php\n\nuse Carbon\\Carbon;\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('attachments', function (Blueprint $table) {\n            $table->increments('id');\n            $table->string('name');\n            $table->string('path');\n            $table->string('extension', 20);\n            $table->integer('uploaded_to');\n\n            $table->boolean('external');\n            $table->integer('order');\n\n            $table->integer('created_by');\n            $table->integer('updated_by');\n\n            $table->index('uploaded_to');\n            $table->timestamps();\n        });\n\n        // Get roles with permissions we need to change\n        $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;\n\n        // Create & attach new entity permissions\n        $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];\n        $entity = 'Attachment';\n        foreach ($ops as $op) {\n            $permissionId = DB::table('role_permissions')->insertGetId([\n                'name'         => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),\n                'display_name' => $op . ' ' . $entity . 's',\n                'created_at'   => Carbon::now()->toDateTimeString(),\n                'updated_at'   => Carbon::now()->toDateTimeString(),\n            ]);\n            DB::table('permission_role')->insert([\n                'role_id'       => $adminRoleId,\n                'permission_id' => $permissionId,\n            ]);\n        }\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('attachments');\n\n        // Create & attach new entity permissions\n        $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];\n        $entity = 'Attachment';\n        foreach ($ops as $op) {\n            $permName = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));\n            DB::table('role_permissions')->where('name', '=', $permName)->delete();\n        }\n    }\n};\n"
  },
  {
    "path": "database/migrations/2017_01_21_163556_create_cache_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('cache', function (Blueprint $table) {\n            $table->string('key')->unique();\n            $table->text('value');\n            $table->integer('expiration');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('cache');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2017_01_21_163602_create_sessions_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('sessions', function (Blueprint $table) {\n            $table->string('id')->unique();\n            $table->integer('user_id')->nullable();\n            $table->string('ip_address', 45)->nullable();\n            $table->text('user_agent')->nullable();\n            $table->text('payload');\n            $table->integer('last_activity');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('sessions');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2017_03_19_091553_create_search_index_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('search_terms', function (Blueprint $table) {\n            $table->increments('id');\n            $table->string('term', 180);\n            $table->string('entity_type', 100);\n            $table->integer('entity_id');\n            $table->integer('score');\n\n            $table->index('term');\n            $table->index('entity_type');\n            $table->index(['entity_type', 'entity_id']);\n            $table->index('score');\n        });\n\n        if (Schema::hasIndex('pages', 'search')) {\n            Schema::table('pages', function (Blueprint $table) {\n                $table->dropIndex('search');\n                $table->dropIndex('name_search');\n            });\n        }\n\n        if (Schema::hasIndex('books', 'search')) {\n            Schema::table('books', function (Blueprint $table) {\n                $table->dropIndex('search');\n                $table->dropIndex('name_search');\n            });\n        }\n\n        if (Schema::hasIndex('chapters', 'search')) {\n            Schema::table('chapters', function (Blueprint $table) {\n                $table->dropIndex('search');\n                $table->dropIndex('name_search');\n            });\n        }\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        // This was removed for v0.24 since these indexes are removed anyway\n        // and will cause issues for db engines that don't support such indexes.\n\n//        $prefix = DB::getTablePrefix();\n//        DB::statement(\"ALTER TABLE {$prefix}pages ADD FULLTEXT search(name, text)\");\n//        DB::statement(\"ALTER TABLE {$prefix}books ADD FULLTEXT search(name, description)\");\n//        DB::statement(\"ALTER TABLE {$prefix}chapters ADD FULLTEXT search(name, description)\");\n//        DB::statement(\"ALTER TABLE {$prefix}pages ADD FULLTEXT name_search(name)\");\n//        DB::statement(\"ALTER TABLE {$prefix}books ADD FULLTEXT name_search(name)\");\n//        DB::statement(\"ALTER TABLE {$prefix}chapters ADD FULLTEXT name_search(name)\");\n\n        Schema::dropIfExists('search_terms');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2017_04_20_185112_add_revision_counts.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('pages', function (Blueprint $table) {\n            $table->integer('revision_count');\n        });\n        Schema::table('page_revisions', function (Blueprint $table) {\n            $table->integer('revision_number');\n            $table->index('revision_number');\n        });\n\n        // Update revision count\n        $pTable = DB::getTablePrefix() . 'pages';\n        $rTable = DB::getTablePrefix() . 'page_revisions';\n        DB::statement(\"UPDATE {$pTable} SET {$pTable}.revision_count=(SELECT count(*) FROM {$rTable} WHERE {$rTable}.page_id={$pTable}.id)\");\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('pages', function (Blueprint $table) {\n            $table->dropColumn('revision_count');\n        });\n        Schema::table('page_revisions', function (Blueprint $table) {\n            $table->dropColumn('revision_number');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2017_07_02_152834_update_db_encoding_to_ut8mb4.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up()\n    {\n        // Migration removed due to issues during live migration.\n        // Instead you can run the command `artisan bookstack:db-utf8mb4`\n        // which will generate out the SQL request to upgrade your DB to utf8mb4.\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down()\n    {\n        //\n    }\n};\n"
  },
  {
    "path": "database/migrations/2017_08_01_130541_create_comments_table.php",
    "content": "<?php\n\nuse Carbon\\Carbon;\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('comments', function (Blueprint $table) {\n            $table->increments('id')->unsigned();\n            $table->integer('entity_id')->unsigned();\n            $table->string('entity_type');\n            $table->longText('text')->nullable();\n            $table->longText('html')->nullable();\n            $table->integer('parent_id')->unsigned()->nullable();\n            $table->integer('local_id')->unsigned()->nullable();\n            $table->integer('created_by')->unsigned();\n            $table->integer('updated_by')->unsigned()->nullable();\n            $table->timestamps();\n\n            $table->index(['entity_id', 'entity_type']);\n            $table->index(['local_id']);\n\n            // Assign new comment permissions to admin role\n            $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;\n            // Create & attach new entity permissions\n            $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];\n            $entity = 'Comment';\n            foreach ($ops as $op) {\n                $permissionId = DB::table('role_permissions')->insertGetId([\n                    'name'         => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),\n                    'display_name' => $op . ' ' . $entity . 's',\n                    'created_at'   => Carbon::now()->toDateTimeString(),\n                    'updated_at'   => Carbon::now()->toDateTimeString(),\n                ]);\n                DB::table('permission_role')->insert([\n                    'role_id'       => $adminRoleId,\n                    'permission_id' => $permissionId,\n                ]);\n            }\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('comments');\n        // Delete comment role permissions\n        $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];\n        $entity = 'Comment';\n        foreach ($ops as $op) {\n            $permName = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));\n            DB::table('role_permissions')->where('name', '=', $permName)->delete();\n        }\n    }\n};\n"
  },
  {
    "path": "database/migrations/2017_08_29_102650_add_cover_image_display.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('books', function (Blueprint $table) {\n            $table->integer('image_id')->nullable()->default(null);\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('books', function (Blueprint $table) {\n            $table->dropColumn('image_id');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2018_07_15_173514_add_role_external_auth_id.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('roles', function (Blueprint $table) {\n            $table->string('external_auth_id', 180)->default('');\n            $table->index('external_auth_id');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('roles', function (Blueprint $table) {\n            $table->dropColumn('external_auth_id');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2018_08_04_115700_create_bookshelves_table.php",
    "content": "<?php\n\nuse Carbon\\Carbon;\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n\n        // Convert the existing entity tables to InnoDB.\n        // Wrapped in try-catch just in the event a different database system is used\n        // which does not support InnoDB but does support all required features\n        // like foreign key references.\n        try {\n            $prefix = DB::getTablePrefix();\n            DB::statement(\"ALTER TABLE {$prefix}pages ENGINE = InnoDB;\");\n            DB::statement(\"ALTER TABLE {$prefix}chapters ENGINE = InnoDB;\");\n            DB::statement(\"ALTER TABLE {$prefix}books ENGINE = InnoDB;\");\n        } catch (Exception $exception) {\n        }\n\n        // Here we have table drops before the creations due to upgrade issues\n        // people were having due to the bookshelves_books table creation failing.\n        if (Schema::hasTable('bookshelves_books')) {\n            Schema::drop('bookshelves_books');\n        }\n\n        if (Schema::hasTable('bookshelves')) {\n            Schema::drop('bookshelves');\n        }\n\n        Schema::create('bookshelves', function (Blueprint $table) {\n            $table->increments('id');\n            $table->string('name', 180);\n            $table->string('slug', 180);\n            $table->text('description');\n            $table->integer('created_by')->nullable()->default(null);\n            $table->integer('updated_by')->nullable()->default(null);\n            $table->boolean('restricted')->default(false);\n            $table->integer('image_id')->nullable()->default(null);\n            $table->timestamps();\n\n            $table->index('slug');\n            $table->index('created_by');\n            $table->index('updated_by');\n            $table->index('restricted');\n        });\n\n        Schema::create('bookshelves_books', function (Blueprint $table) {\n            $table->integer('bookshelf_id')->unsigned();\n            $table->integer('book_id')->unsigned();\n            $table->integer('order')->unsigned();\n\n            $table->primary(['bookshelf_id', 'book_id']);\n\n            $table->foreign('bookshelf_id')->references('id')->on('bookshelves')\n                ->onUpdate('cascade')->onDelete('cascade');\n            $table->foreign('book_id')->references('id')->on('books')\n                ->onUpdate('cascade')->onDelete('cascade');\n        });\n\n        // Delete old bookshelf permissions\n        // Needed to to issues upon upgrade.\n        DB::table('role_permissions')->where('name', 'like', 'bookshelf-%')->delete();\n\n        // Copy existing role permissions from Books\n        $ops = ['View All', 'View Own', 'Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];\n        foreach ($ops as $op) {\n            $dbOpName = strtolower(str_replace(' ', '-', $op));\n            $roleIdsWithBookPermission = DB::table('role_permissions')\n                ->leftJoin('permission_role', 'role_permissions.id', '=', 'permission_role.permission_id')\n                ->leftJoin('roles', 'roles.id', '=', 'permission_role.role_id')\n                ->where('role_permissions.name', '=', 'book-' . $dbOpName)->get(['roles.id'])->pluck('id');\n\n            $permId = DB::table('role_permissions')->insertGetId([\n                'name'         => 'bookshelf-' . $dbOpName,\n                'display_name' => $op . ' ' . 'BookShelves',\n                'created_at'   => Carbon::now()->toDateTimeString(),\n                'updated_at'   => Carbon::now()->toDateTimeString(),\n            ]);\n\n            $rowsToInsert = $roleIdsWithBookPermission->filter(function ($roleId) {\n                return !is_null($roleId);\n            })->map(function ($roleId) use ($permId) {\n                return [\n                    'role_id'       => $roleId,\n                    'permission_id' => $permId,\n                ];\n            })->toArray();\n\n            // Assign view permission to all current roles\n            DB::table('permission_role')->insert($rowsToInsert);\n        }\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        // Drop created permissions\n        $ops = ['bookshelf-create-all', 'bookshelf-create-own', 'bookshelf-delete-all', 'bookshelf-delete-own', 'bookshelf-update-all', 'bookshelf-update-own', 'bookshelf-view-all', 'bookshelf-view-own'];\n\n        $permissionIds = DB::table('role_permissions')->whereIn('name', $ops)\n            ->get(['id'])->pluck('id')->toArray();\n        DB::table('permission_role')->whereIn('permission_id', $permissionIds)->delete();\n        DB::table('role_permissions')->whereIn('id', $permissionIds)->delete();\n\n        // Drop shelves table\n        Schema::dropIfExists('bookshelves_books');\n        Schema::dropIfExists('bookshelves');\n\n        // Drop related polymorphic items\n        DB::table('activities')->where('entity_type', '=', 'BookStack\\Entities\\Models\\Bookshelf')->delete();\n        DB::table('views')->where('viewable_type', '=', 'BookStack\\Entities\\Models\\Bookshelf')->delete();\n        DB::table('entity_permissions')->where('restrictable_type', '=', 'BookStack\\Entities\\Models\\Bookshelf')->delete();\n        DB::table('tags')->where('entity_type', '=', 'BookStack\\Entities\\Models\\Bookshelf')->delete();\n        DB::table('search_terms')->where('entity_type', '=', 'BookStack\\Entities\\Models\\Bookshelf')->delete();\n        DB::table('comments')->where('entity_type', '=', 'BookStack\\Entities\\Models\\Bookshelf')->delete();\n    }\n};\n"
  },
  {
    "path": "database/migrations/2019_07_07_112515_add_template_support.php",
    "content": "<?php\n\nuse Carbon\\Carbon;\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('pages', function (Blueprint $table) {\n            $table->boolean('template')->default(false);\n            $table->index('template');\n        });\n\n        // Create new templates-manage permission and assign to admin role\n        $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;\n        $permissionId = DB::table('role_permissions')->insertGetId([\n            'name'         => 'templates-manage',\n            'display_name' => 'Manage Page Templates',\n            'created_at'   => Carbon::now()->toDateTimeString(),\n            'updated_at'   => Carbon::now()->toDateTimeString(),\n        ]);\n        DB::table('permission_role')->insert([\n            'role_id'       => $adminRoleId,\n            'permission_id' => $permissionId,\n        ]);\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('pages', function (Blueprint $table) {\n            $table->dropColumn('template');\n        });\n\n        // Remove templates-manage permission\n        $templatesManagePermission = DB::table('role_permissions')\n            ->where('name', '=', 'templates-manage')->first();\n\n        DB::table('permission_role')->where('permission_id', '=', $templatesManagePermission->id)->delete();\n        DB::table('role_permissions')->where('name', '=', 'templates-manage')->delete();\n    }\n};\n"
  },
  {
    "path": "database/migrations/2019_08_17_140214_add_user_invites_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('user_invites', function (Blueprint $table) {\n            $table->increments('id');\n            $table->integer('user_id')->index();\n            $table->string('token')->index();\n            $table->nullableTimestamps();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('user_invites');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2019_12_29_120917_add_api_auth.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n\n        // Add API tokens table\n        Schema::create('api_tokens', function (Blueprint $table) {\n            $table->increments('id');\n            $table->string('name');\n            $table->string('token_id')->unique();\n            $table->string('secret');\n            $table->integer('user_id')->unsigned()->index();\n            $table->date('expires_at')->index();\n            $table->nullableTimestamps();\n        });\n\n        // Add access-api permission\n        $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;\n        $permissionId = DB::table('role_permissions')->insertGetId([\n            'name'         => 'access-api',\n            'display_name' => 'Access system API',\n            'created_at'   => Carbon::now()->toDateTimeString(),\n            'updated_at'   => Carbon::now()->toDateTimeString(),\n        ]);\n        DB::table('permission_role')->insert([\n            'role_id'       => $adminRoleId,\n            'permission_id' => $permissionId,\n        ]);\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        // Remove API tokens table\n        Schema::dropIfExists('api_tokens');\n\n        // Remove access-api permission\n        $apiAccessPermission = DB::table('role_permissions')\n            ->where('name', '=', 'access-api')->first();\n\n        DB::table('permission_role')->where('permission_id', '=', $apiAccessPermission->id)->delete();\n        DB::table('role_permissions')->where('name', '=', 'access-api')->delete();\n    }\n};\n"
  },
  {
    "path": "database/migrations/2020_08_04_111754_drop_joint_permissions_id.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('joint_permissions', function (Blueprint $table) {\n            $table->dropColumn('id');\n            $table->primary(['role_id', 'entity_type', 'entity_id', 'action'], 'joint_primary');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('joint_permissions', function (Blueprint $table) {\n            $table->dropPrimary(['role_id', 'entity_type', 'entity_id', 'action']);\n        });\n\n        Schema::table('joint_permissions', function (Blueprint $table) {\n            $table->increments('id')->unsigned();\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2020_08_04_131052_remove_role_name_field.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('roles', function (Blueprint $table) {\n            $table->dropColumn('name');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('roles', function (Blueprint $table) {\n            $table->string('name')->index();\n        });\n\n        DB::table('roles')->update([\n            'name' => DB::raw(\"lower(replace(`display_name`, ' ', '-'))\"),\n        ]);\n    }\n};\n"
  },
  {
    "path": "database/migrations/2020_09_19_094251_add_activity_indexes.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('activities', function (Blueprint $table) {\n            $table->index('key');\n            $table->index('created_at');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('activities', function (Blueprint $table) {\n            $table->dropIndex('activities_key_index');\n            $table->dropIndex('activities_created_at_index');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2020_09_27_210059_add_entity_soft_deletes.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('bookshelves', function (Blueprint $table) {\n            $table->softDeletes();\n        });\n        Schema::table('books', function (Blueprint $table) {\n            $table->softDeletes();\n        });\n        Schema::table('chapters', function (Blueprint $table) {\n            $table->softDeletes();\n        });\n        Schema::table('pages', function (Blueprint $table) {\n            $table->softDeletes();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('bookshelves', function (Blueprint $table) {\n            $table->dropSoftDeletes();\n        });\n        Schema::table('books', function (Blueprint $table) {\n            $table->dropSoftDeletes();\n        });\n        Schema::table('chapters', function (Blueprint $table) {\n            $table->dropSoftDeletes();\n        });\n        Schema::table('pages', function (Blueprint $table) {\n            $table->dropSoftDeletes();\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2020_09_27_210528_create_deletions_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('deletions', function (Blueprint $table) {\n            $table->increments('id');\n            $table->integer('deleted_by');\n            $table->string('deletable_type', 100);\n            $table->integer('deletable_id');\n            $table->timestamps();\n\n            $table->index('deleted_by');\n            $table->index('deletable_type');\n            $table->index('deletable_id');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('deletions');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2020_11_07_232321_simplify_activities_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('activities', function (Blueprint $table) {\n            $table->renameColumn('key', 'type');\n            $table->renameColumn('extra', 'detail');\n            $table->dropColumn('book_id');\n            $table->integer('entity_id')->nullable()->change();\n            $table->string('entity_type', 191)->nullable()->change();\n        });\n\n        DB::table('activities')\n            ->where('entity_id', '=', 0)\n            ->update([\n                'entity_id'   => null,\n                'entity_type' => null,\n            ]);\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        DB::table('activities')\n            ->whereNull('entity_id')\n            ->update([\n                'entity_id'   => 0,\n                'entity_type' => '',\n            ]);\n\n        Schema::table('activities', function (Blueprint $table) {\n            $table->renameColumn('type', 'key');\n            $table->renameColumn('detail', 'extra');\n            $table->integer('book_id');\n\n            $table->integer('entity_id')->change();\n            $table->string('entity_type', 191)->change();\n\n            $table->index('book_id');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2020_12_30_173528_add_owned_by_field_to_entities.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        $tables = ['pages', 'books', 'chapters', 'bookshelves'];\n        foreach ($tables as $table) {\n            Schema::table($table, function (Blueprint $table) {\n                $table->integer('owned_by')->unsigned()->index();\n            });\n\n            DB::table($table)->update(['owned_by' => DB::raw('`created_by`')]);\n        }\n\n        Schema::table('joint_permissions', function (Blueprint $table) {\n            $table->renameColumn('created_by', 'owned_by');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        $tables = ['pages', 'books', 'chapters', 'bookshelves'];\n        foreach ($tables as $table) {\n            Schema::table($table, function (Blueprint $table) {\n                $table->dropColumn('owned_by');\n            });\n        }\n\n        Schema::table('joint_permissions', function (Blueprint $table) {\n            $table->renameColumn('owned_by', 'created_by');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2021_01_30_225441_add_settings_type_column.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('settings', function (Blueprint $table) {\n            $table->string('type', 50)->default('string');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('settings', function (Blueprint $table) {\n            $table->dropColumn('type');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2021_03_08_215138_add_user_slug.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Illuminate\\Support\\Str;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('users', function (Blueprint $table) {\n            $table->string('slug', 180);\n        });\n\n        $slugMap = [];\n        DB::table('users')->cursor()->each(function ($user) use (&$slugMap) {\n            $userSlug = Str::slug($user->name);\n            while (isset($slugMap[$userSlug])) {\n                $userSlug = Str::slug($user->name . Str::random(4));\n            }\n            $slugMap[$userSlug] = true;\n\n            DB::table('users')\n                ->where('id', $user->id)\n                ->update(['slug' => $userSlug]);\n        });\n\n        Schema::table('users', function (Blueprint $table) {\n            $table->unique('slug');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('users', function (Blueprint $table) {\n            $table->dropColumn('slug');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2021_05_15_173110_create_favourites_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('favourites', function (Blueprint $table) {\n            $table->increments('id');\n            $table->integer('user_id')->index();\n            $table->integer('favouritable_id');\n            $table->string('favouritable_type', 100);\n            $table->timestamps();\n\n            $table->index(['favouritable_id', 'favouritable_type'], 'favouritable_index');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('favourites');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2021_06_30_173111_create_mfa_values_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('mfa_values', function (Blueprint $table) {\n            $table->increments('id');\n            $table->integer('user_id')->index();\n            $table->string('method', 20)->index();\n            $table->text('value');\n            $table->timestamps();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('mfa_values');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2021_07_03_085038_add_mfa_enforced_to_roles_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('roles', function (Blueprint $table) {\n            $table->boolean('mfa_enforced');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('roles', function (Blueprint $table) {\n            $table->dropColumn('mfa_enforced');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2021_08_28_161743_add_export_role_permission.php",
    "content": "<?php\n\nuse Carbon\\Carbon;\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Support\\Facades\\DB;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        // Create new content-export permission\n        $permissionId = DB::table('role_permissions')->insertGetId([\n            'name'         => 'content-export',\n            'display_name' => 'Export Content',\n            'created_at'   => Carbon::now()->toDateTimeString(),\n            'updated_at'   => Carbon::now()->toDateTimeString(),\n        ]);\n\n        $roles = DB::table('roles')->get('id');\n        $permissionRoles = $roles->map(function ($role) use ($permissionId) {\n            return [\n                'role_id'       => $role->id,\n                'permission_id' => $permissionId,\n            ];\n        })->values()->toArray();\n\n        // Assign to all existing roles in the system\n        DB::table('permission_role')->insert($permissionRoles);\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        // Remove content-export permission\n        $contentExportPermission = DB::table('role_permissions')\n            ->where('name', '=', 'content-export')->first();\n\n        DB::table('permission_role')->where('permission_id', '=', $contentExportPermission->id)->delete();\n        DB::table('role_permissions')->where('id', '=', $contentExportPermission->id)->delete();\n    }\n};\n"
  },
  {
    "path": "database/migrations/2021_09_26_044614_add_activities_ip_column.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('activities', function (Blueprint $table) {\n            $table->string('ip', 45)->after('user_id');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('activities', function (Blueprint $table) {\n            $table->dropColumn('ip');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2021_11_26_070438_add_index_for_user_ip.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('activities', function (Blueprint $table) {\n            $table->index('ip', 'activities_ip_index');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('activities', function (Blueprint $table) {\n            $table->dropIndex('activities_ip_index');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2021_12_07_111343_create_webhooks_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('webhooks', function (Blueprint $table) {\n            $table->increments('id');\n            $table->string('name', 150);\n            $table->boolean('active');\n            $table->string('endpoint', 500);\n            $table->timestamps();\n\n            $table->index('name');\n            $table->index('active');\n        });\n\n        Schema::create('webhook_tracked_events', function (Blueprint $table) {\n            $table->increments('id');\n            $table->integer('webhook_id');\n            $table->string('event', 50);\n            $table->timestamps();\n\n            $table->index('event');\n            $table->index('webhook_id');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('webhooks');\n        Schema::dropIfExists('webhook_tracked_events');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2021_12_13_152024_create_jobs_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('jobs', function (Blueprint $table) {\n            $table->bigIncrements('id');\n            $table->string('queue')->index();\n            $table->longText('payload');\n            $table->unsignedTinyInteger('attempts');\n            $table->unsignedInteger('reserved_at')->nullable();\n            $table->unsignedInteger('available_at');\n            $table->unsignedInteger('created_at');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('jobs');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2021_12_13_152120_create_failed_jobs_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('failed_jobs', function (Blueprint $table) {\n            $table->id();\n            $table->string('uuid')->unique();\n            $table->text('connection');\n            $table->text('queue');\n            $table->longText('payload');\n            $table->longText('exception');\n            $table->timestamp('failed_at')->useCurrent();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('failed_jobs');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2022_01_03_154041_add_webhooks_timeout_error_columns.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('webhooks', function (Blueprint $table) {\n            $table->unsignedInteger('timeout')->default(3);\n            $table->text('last_error')->default('');\n            $table->timestamp('last_called_at')->nullable();\n            $table->timestamp('last_errored_at')->nullable();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('webhooks', function (Blueprint $table) {\n            $table->dropColumn('timeout');\n            $table->dropColumn('last_error');\n            $table->dropColumn('last_called_at');\n            $table->dropColumn('last_errored_at');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2022_04_17_101741_add_editor_change_field_and_permission.php",
    "content": "<?php\n\nuse Carbon\\Carbon;\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        // Add the new 'editor' column to the pages table\n        Schema::table('pages', function (Blueprint $table) {\n            $table->string('editor', 50)->default('');\n        });\n\n        // Populate the new 'editor' column\n        // We set it to 'markdown' for pages currently with markdown content\n        DB::table('pages')->where('markdown', '!=', '')->update(['editor' => 'markdown']);\n        // We set it to 'wysiwyg' where we have HTML but no markdown\n        DB::table('pages')->where('markdown', '=', '')\n            ->where('html', '!=', '')\n            ->update(['editor' => 'wysiwyg']);\n\n        // Give the admin user permission to change the editor\n        $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;\n\n        $permissionId = DB::table('role_permissions')->insertGetId([\n            'name'         => 'editor-change',\n            'display_name' => 'Change page editor',\n            'created_at'   => Carbon::now()->toDateTimeString(),\n            'updated_at'   => Carbon::now()->toDateTimeString(),\n        ]);\n\n        DB::table('permission_role')->insert([\n            'role_id'       => $adminRoleId,\n            'permission_id' => $permissionId,\n        ]);\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        // Drop the new column from the pages table\n        Schema::table('pages', function (Blueprint $table) {\n            $table->dropColumn('editor');\n        });\n\n        // Remove traces of the role permission\n        DB::table('role_permissions')->where('name', '=', 'editor-change')->delete();\n    }\n};\n"
  },
  {
    "path": "database/migrations/2022_04_25_140741_update_polymorphic_types.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Support\\Facades\\DB;\n\nreturn new class extends Migration\n{\n    /**\n     * Mapping of old polymorphic types to new simpler values.\n     */\n    protected array $changeMap = [\n        'BookStack\\\\Bookshelf' => 'bookshelf',\n        'BookStack\\\\Book'      => 'book',\n        'BookStack\\\\Chapter'   => 'chapter',\n        'BookStack\\\\Page'      => 'page',\n    ];\n\n    /**\n     * Mapping of tables and columns that contain polymorphic types.\n     */\n    protected array $columnsByTable = [\n        'activities'         => 'entity_type',\n        'comments'           => 'entity_type',\n        'deletions'          => 'deletable_type',\n        'entity_permissions' => 'restrictable_type',\n        'favourites'         => 'favouritable_type',\n        'joint_permissions'  => 'entity_type',\n        'search_terms'       => 'entity_type',\n        'tags'               => 'entity_type',\n        'views'              => 'viewable_type',\n    ];\n\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        foreach ($this->columnsByTable as $table => $column) {\n            foreach ($this->changeMap as $oldVal => $newVal) {\n                DB::table($table)\n                    ->where([$column => $oldVal])\n                    ->update([$column => $newVal]);\n            }\n        }\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        foreach ($this->columnsByTable as $table => $column) {\n            foreach ($this->changeMap as $oldVal => $newVal) {\n                DB::table($table)\n                    ->where([$column => $newVal])\n                    ->update([$column => $oldVal]);\n            }\n        }\n    }\n};\n"
  },
  {
    "path": "database/migrations/2022_07_16_170051_drop_joint_permission_type.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        DB::table('joint_permissions')\n            ->where('action', '!=', 'view')\n            ->delete();\n\n        Schema::table('joint_permissions', function (Blueprint $table) {\n            $table->dropPrimary(['role_id', 'entity_type', 'entity_id', 'action']);\n            $table->dropColumn('action');\n            $table->primary(['role_id', 'entity_type', 'entity_id'], 'joint_primary');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('joint_permissions', function (Blueprint $table) {\n            $table->string('action');\n            $table->dropPrimary(['role_id', 'entity_type', 'entity_id']);\n            $table->primary(['role_id', 'entity_type', 'entity_id', 'action']);\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2022_08_17_092941_create_references_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('references', function (Blueprint $table) {\n            $table->id();\n            $table->unsignedInteger('from_id')->index();\n            $table->string('from_type', 25)->index();\n            $table->unsignedInteger('to_id')->index();\n            $table->string('to_type', 25)->index();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('references');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2022_09_02_082910_fix_shelf_cover_image_types.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Support\\Facades\\DB;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        // This updates the 'type' field for images, uploaded as shelf cover images,\n        // to be cover_bookshelf instead of cover_book.\n        // This does not fix their paths, since fixing that requires a more complicated operation,\n        // but their path does not affect functionality at time of this fix.\n\n        $shelfImageIds = DB::table('bookshelves')\n            ->whereNotNull('image_id')\n            ->pluck('image_id')\n            ->values()->all();\n\n        if (count($shelfImageIds) > 0) {\n            DB::table('images')\n                ->where('type', '=', 'cover_book')\n                ->whereIn('id', $shelfImageIds)\n                ->update(['type' => 'cover_bookshelf']);\n        }\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        DB::table('images')\n            ->where('type', '=', 'cover_bookshelf')\n            ->update(['type' => 'cover_book']);\n    }\n};\n"
  },
  {
    "path": "database/migrations/2022_10_07_091406_flatten_entity_permissions_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        // Remove entries for non-existing roles (Caused by previous lack of deletion handling)\n        $roleIds = DB::table('roles')->pluck('id');\n        DB::table('entity_permissions')->whereNotIn('role_id', $roleIds)->delete();\n\n        // Create new table structure for entity_permissions\n        Schema::create('new_entity_permissions', function (Blueprint $table) {\n            $table->id();\n            $table->unsignedInteger('entity_id');\n            $table->string('entity_type', 25);\n            $table->unsignedInteger('role_id')->index();\n            $table->boolean('view')->default(0);\n            $table->boolean('create')->default(0);\n            $table->boolean('update')->default(0);\n            $table->boolean('delete')->default(0);\n\n            $table->index(['entity_id', 'entity_type']);\n        });\n\n        // Migrate existing entity_permission data into new table structure\n\n        $subSelect = function (Builder $query, string $action, string $subAlias) {\n            $sub = $query->newQuery()->select('action')->from('entity_permissions', $subAlias)\n                ->whereColumn('a.restrictable_id', '=', $subAlias . '.restrictable_id')\n                ->whereColumn('a.restrictable_type', '=', $subAlias . '.restrictable_type')\n                ->whereColumn('a.role_id', '=', $subAlias . '.role_id')\n                ->where($subAlias . '.action', '=', $action);\n            return $query->selectRaw(\"EXISTS({$sub->toSql()})\", $sub->getBindings());\n        };\n\n        $query = DB::table('entity_permissions', 'a')->select([\n            'restrictable_id as entity_id',\n            'restrictable_type as entity_type',\n            'role_id',\n            'view'   => fn(Builder $query) => $subSelect($query, 'view', 'b'),\n            'create' => fn(Builder $query) => $subSelect($query, 'create', 'c'),\n            'update' => fn(Builder $query) => $subSelect($query, 'update', 'd'),\n            'delete' => fn(Builder $query) => $subSelect($query, 'delete', 'e'),\n        ])->groupBy('restrictable_id', 'restrictable_type', 'role_id');\n\n        DB::table('new_entity_permissions')->insertUsing(['entity_id', 'entity_type', 'role_id', 'view', 'create', 'update', 'delete'], $query);\n\n        // Drop old entity_permissions table and replace with new structure\n        Schema::dropIfExists('entity_permissions');\n        Schema::rename('new_entity_permissions', 'entity_permissions');\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        // Create old table structure for entity_permissions\n        Schema::create('old_entity_permissions', function (Blueprint $table) {\n            $table->increments('id');\n            $table->integer('restrictable_id');\n            $table->string('restrictable_type', 191);\n            $table->integer('role_id')->index();\n            $table->string('action', 191)->index();\n\n            $table->index(['restrictable_id', 'restrictable_type']);\n        });\n\n        // Convert newer data format to old data format, and insert into old database\n\n        $actionQuery = function (Builder $query, string $action) {\n            return $query->select([\n                'entity_id as restrictable_id',\n                'entity_type as restrictable_type',\n                'role_id',\n            ])->selectRaw(\"? as action\", [$action])\n            ->from('entity_permissions')\n            ->where($action, '=', true);\n        };\n\n        $query = $actionQuery(DB::query(), 'view')\n            ->union(fn(Builder $query) => $actionQuery($query, 'create'))\n            ->union(fn(Builder $query) => $actionQuery($query, 'update'))\n            ->union(fn(Builder $query) => $actionQuery($query, 'delete'));\n\n        DB::table('old_entity_permissions')->insertUsing(['restrictable_id', 'restrictable_type', 'role_id', 'action'], $query);\n\n        // Drop new entity_permissions table and replace with old structure\n        Schema::dropIfExists('entity_permissions');\n        Schema::rename('old_entity_permissions', 'entity_permissions');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2022_10_08_104202_drop_entity_restricted_field.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Query\\JoinClause;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        // Remove entity-permissions on non-restricted entities\n        $deleteInactiveEntityPermissions = function (string $table, string $morphClass) {\n            $permissionIds = DB::table('entity_permissions')->select('entity_permissions.id as id')\n                ->join($table, function (JoinClause $join) use ($table, $morphClass) {\n                    return $join->where($table . '.restricted', '=', 0)\n                        ->on($table . '.id', '=', 'entity_permissions.entity_id');\n                })->where('entity_type', '=', $morphClass)\n                ->pluck('id');\n            DB::table('entity_permissions')->whereIn('id', $permissionIds)->delete();\n        };\n        $deleteInactiveEntityPermissions('pages', 'page');\n        $deleteInactiveEntityPermissions('chapters', 'chapter');\n        $deleteInactiveEntityPermissions('books', 'book');\n        $deleteInactiveEntityPermissions('bookshelves', 'bookshelf');\n\n        // Migrate restricted=1 entries to new entity_permissions (role_id=0) entries\n        $defaultEntityPermissionGenQuery = function (Builder $query, string $table, string $morphClass) {\n            return $query->select(['id as entity_id'])\n                ->selectRaw('? as entity_type', [$morphClass])\n                ->selectRaw('? as `role_id`', [0])\n                ->selectRaw('? as `view`', [0])\n                ->selectRaw('? as `create`', [0])\n                ->selectRaw('? as `update`', [0])\n                ->selectRaw('? as `delete`', [0])\n                ->from($table)\n                ->where('restricted', '=', 1);\n        };\n\n        $query = $defaultEntityPermissionGenQuery(DB::query(), 'pages', 'page')\n            ->union(fn(Builder $query) => $defaultEntityPermissionGenQuery($query, 'books', 'book'))\n            ->union(fn(Builder $query) => $defaultEntityPermissionGenQuery($query, 'chapters', 'chapter'))\n            ->union(fn(Builder $query) => $defaultEntityPermissionGenQuery($query, 'bookshelves', 'bookshelf'));\n\n        DB::table('entity_permissions')->insertUsing(['entity_id', 'entity_type', 'role_id', 'view', 'create', 'update', 'delete'], $query);\n\n        // Drop restricted columns\n        $dropRestrictedColumn = fn(Blueprint $table) => $table->dropColumn('restricted');\n        Schema::table('pages', $dropRestrictedColumn);\n        Schema::table('chapters', $dropRestrictedColumn);\n        Schema::table('books', $dropRestrictedColumn);\n        Schema::table('bookshelves', $dropRestrictedColumn);\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        // Create restricted columns\n        $createRestrictedColumn = fn(Blueprint $table) => $table->boolean('restricted')->index()->default(0);\n        Schema::table('pages', $createRestrictedColumn);\n        Schema::table('chapters', $createRestrictedColumn);\n        Schema::table('books', $createRestrictedColumn);\n        Schema::table('bookshelves', $createRestrictedColumn);\n\n        // Set restrictions for entities that have a default entity permission assigned\n        // Note: Possible loss of data where default entity permissions have been configured\n        $restrictEntities = function (string $table, string $morphClass) {\n            $toRestrictIds = DB::table('entity_permissions')\n                ->where('role_id', '=', 0)\n                ->where('entity_type', '=', $morphClass)\n                ->pluck('entity_id');\n            DB::table($table)->whereIn('id', $toRestrictIds)->update(['restricted' => true]);\n        };\n        $restrictEntities('pages', 'page');\n        $restrictEntities('chapters', 'chapter');\n        $restrictEntities('books', 'book');\n        $restrictEntities('bookshelves', 'bookshelf');\n\n        // Delete default entity permissions\n        DB::table('entity_permissions')->where('role_id', '=', 0)->delete();\n    }\n};\n"
  },
  {
    "path": "database/migrations/2023_01_24_104625_refactor_joint_permissions_storage.php",
    "content": "<?php\n\nuse BookStack\\Permissions\\JointPermissionBuilder;\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        // Truncate before schema changes to avoid performance issues\n        // since we'll need to rebuild anyway.\n        DB::table('joint_permissions')->truncate();\n\n        if (Schema::hasColumn('joint_permissions', 'owned_by')) {\n            Schema::table('joint_permissions', function (Blueprint $table) {\n                $table->dropColumn(['has_permission', 'has_permission_own', 'owned_by']);\n\n                $table->unsignedTinyInteger('status')->index();\n                $table->unsignedInteger('owner_id')->nullable()->index();\n            });\n        }\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        DB::table('joint_permissions')->truncate();\n\n        Schema::table('joint_permissions', function (Blueprint $table) {\n            $table->dropColumn(['status', 'owner_id']);\n\n            $table->boolean('has_permission')->index();\n            $table->boolean('has_permission_own')->index();\n            $table->unsignedInteger('owned_by')->index();\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2023_01_28_141230_copy_color_settings_for_dark_mode.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Support\\Facades\\DB;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        $colorSettings = [\n            'app-color',\n            'app-color-light',\n            'bookshelf-color',\n            'book-color',\n            'chapter-color',\n            'page-color',\n            'page-draft-color',\n        ];\n\n        $existing = DB::table('settings')\n            ->whereIn('setting_key', $colorSettings)\n            ->get()->toArray();\n\n        $newData = [];\n        foreach ($existing as $setting) {\n            $newSetting = (array) $setting;\n            $newSetting['setting_key'] .= '-dark';\n            $newData[] = $newSetting;\n\n            if ($newSetting['setting_key'] === 'app-color-dark') {\n                $newSetting['setting_key'] = 'link-color';\n                $newData[] = $newSetting;\n                $newSetting['setting_key'] = 'link-color-dark';\n                $newData[] = $newSetting;\n            }\n        }\n\n        DB::table('settings')->insert($newData);\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        $colorSettings = [\n            'app-color-dark',\n            'link-color',\n            'link-color-dark',\n            'app-color-light-dark',\n            'bookshelf-color-dark',\n            'book-color-dark',\n            'chapter-color-dark',\n            'page-color-dark',\n            'page-draft-color-dark',\n        ];\n\n        DB::table('settings')\n            ->whereIn('setting_key', $colorSettings)\n            ->delete();\n    }\n};\n"
  },
  {
    "path": "database/migrations/2023_02_20_093655_increase_attachments_path_length.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('attachments', function (Blueprint $table) {\n            $table->text('path')->change();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('attachments', function (Blueprint $table) {\n            $table->string('path')->change();\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2023_02_23_200227_add_updated_at_index_to_pages.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('pages', function (Blueprint $table) {\n            $table->index('updated_at', 'pages_updated_at_index');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('pages', function (Blueprint $table) {\n            $table->dropIndex('pages_updated_at_index');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2023_06_10_071823_remove_guest_user_secondary_roles.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Support\\Facades\\DB;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        $guestUserId = DB::table('users')\n            ->where('system_name', '=', 'public')\n            ->first(['id'])->id;\n        $publicRoleId = DB::table('roles')\n            ->where('system_name', '=', 'public')\n            ->first(['id'])->id;\n\n        // This migration deletes secondary \"Guest\" user role assignments\n        // as a safety precaution upon upgrade since the logic is changing\n        // within the release this is introduced in, which could result in wider\n        // permissions being provided upon upgrade without this intervention.\n\n        // Previously, added roles would only partially apply their permissions\n        // since some permission checks would only consider the originally assigned\n        // public role, and not added roles. Within this release, additional roles\n        // will fully apply.\n        DB::table('role_user')\n            ->where('user_id', '=', $guestUserId)\n            ->where('role_id', '!=', $publicRoleId)\n            ->delete();\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down()\n    {\n        // No structural changes to make, and we cannot know the role ids to re-assign.\n    }\n};\n"
  },
  {
    "path": "database/migrations/2023_06_25_181952_remove_bookshelf_create_entity_permissions.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Support\\Facades\\DB;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up()\n    {\n        // Note: v23.06.2\n        // Migration removed since change to remove bookshelf create permissions was reverted.\n        // Create permissions were removed as incorrectly thought to be unused, but they did\n        // have a use via shelf permission copy-down to books.\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down()\n    {\n        // No structural changes to make, and we cannot know the permissions to re-assign.\n    }\n};\n"
  },
  {
    "path": "database/migrations/2023_07_25_124945_add_receive_notifications_role_permissions.php",
    "content": "<?php\n\nuse Carbon\\Carbon;\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Support\\Facades\\DB;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        // Create new receive-notifications permission and assign to admin role\n        $permissionId = DB::table('role_permissions')->insertGetId([\n            'name'         => 'receive-notifications',\n            'display_name' => 'Receive & Manage Notifications',\n            'created_at'   => Carbon::now()->toDateTimeString(),\n            'updated_at'   => Carbon::now()->toDateTimeString(),\n        ]);\n\n        $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;\n        DB::table('permission_role')->insert([\n            'role_id' => $adminRoleId,\n            'permission_id' => $permissionId,\n        ]);\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        $permission = DB::table('role_permissions')\n            ->where('name', '=', 'receive-notifications')\n            ->first();\n\n        if ($permission) {\n            DB::table('permission_role')->where([\n                'permission_id' => $permission->id,\n            ])->delete();\n        }\n\n        DB::table('role_permissions')\n            ->where('name', '=', 'receive-notifications')\n            ->delete();\n    }\n};\n"
  },
  {
    "path": "database/migrations/2023_07_31_104430_create_watches_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('watches', function (Blueprint $table) {\n            $table->increments('id');\n            $table->integer('user_id')->index();\n            $table->integer('watchable_id');\n            $table->string('watchable_type', 100);\n            $table->tinyInteger('level', false, true)->index();\n            $table->timestamps();\n\n            $table->index(['watchable_id', 'watchable_type'], 'watchable_index');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('watches');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2023_08_21_174248_increase_cache_size.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('cache', function (Blueprint $table) {\n            $table->mediumText('value')->change();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('cache', function (Blueprint $table) {\n            $table->text('value')->change();\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2023_12_02_104541_add_default_template_to_books.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nclass AddDefaultTemplateToBooks extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('books', function (Blueprint $table) {\n            $table->integer('default_template_id')->nullable()->default(null);\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('books', function (Blueprint $table) {\n            $table->dropColumn('default_template_id');\n        });\n    }\n}\n"
  },
  {
    "path": "database/migrations/2023_12_17_140913_add_description_html_to_entities.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        $addColumn = fn(Blueprint $table) => $table->text('description_html');\n\n        Schema::table('books', $addColumn);\n        Schema::table('chapters', $addColumn);\n        Schema::table('bookshelves', $addColumn);\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        $removeColumn = fn(Blueprint $table) => $table->removeColumn('description_html');\n\n        Schema::table('books', $removeColumn);\n        Schema::table('chapters', $removeColumn);\n        Schema::table('bookshelves', $removeColumn);\n    }\n};\n"
  },
  {
    "path": "database/migrations/2024_01_01_104542_add_default_template_to_chapters.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nclass AddDefaultTemplateToChapters extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('chapters', function (Blueprint $table) {\n            $table->integer('default_template_id')->nullable()->default(null);\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('chapters', function (Blueprint $table) {\n            $table->dropColumn('default_template_id');\n        });\n    }\n}\n"
  },
  {
    "path": "database/migrations/2024_02_04_141358_add_views_updated_index.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('views', function (Blueprint $table) {\n            $table->index(['updated_at'], 'views_updated_at_index');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('views', function (Blueprint $table) {\n            $table->dropIndex('views_updated_at_index');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2024_05_04_154409_rename_activity_relation_columns.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('activities', function (Blueprint $table) {\n            $table->renameColumn('entity_id', 'loggable_id');\n            $table->renameColumn('entity_type', 'loggable_type');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('activities', function (Blueprint $table) {\n            $table->renameColumn('loggable_id', 'entity_id');\n            $table->renameColumn('loggable_type', 'entity_type');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2024_09_29_140340_ensure_editor_value_set.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Support\\Facades\\DB;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        // Ensure we have an \"editor\" value set for pages\n\n        // Get default\n        $default = DB::table('settings')\n            ->where('setting_key', '=', 'app-editor')\n            ->first()\n            ->value ?? 'wysiwyg';\n        $default = ($default === 'markdown') ? 'markdown' : 'wysiwyg';\n\n        // We set it to 'markdown' for pages currently with markdown content\n        DB::table('pages')\n            ->where('editor', '=', '')\n            ->where('markdown', '!=', '')\n            ->update(['editor' => 'markdown']);\n\n        // We set it to 'wysiwyg' where we have HTML but no markdown\n        DB::table('pages')\n            ->where('editor', '=', '')\n            ->where('markdown', '=', '')\n            ->where('html', '!=', '')\n            ->update(['editor' => 'wysiwyg']);\n\n        // Otherwise, where still empty, set to the current default\n        DB::table('pages')\n            ->where('editor', '=', '')\n            ->update(['editor' => $default]);\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        // Can't reverse due to not knowing what would have been empty before\n    }\n};\n"
  },
  {
    "path": "database/migrations/2024_10_29_114420_add_import_role_permission.php",
    "content": "<?php\n\nuse Carbon\\Carbon;\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Support\\Facades\\DB;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        // Create new content-import permission\n        $permissionId = DB::table('role_permissions')->insertGetId([\n            'name'         => 'content-import',\n            'display_name' => 'Import Content',\n            'created_at'   => Carbon::now()->toDateTimeString(),\n            'updated_at'   => Carbon::now()->toDateTimeString(),\n        ]);\n\n        // Get existing admin-level role ids\n        $settingManagePermission = DB::table('role_permissions')\n            ->where('name', '=', 'settings-manage')->first();\n\n        if (!$settingManagePermission) {\n            return;\n        }\n\n        $adminRoleIds = DB::table('permission_role')\n            ->where('permission_id', '=', $settingManagePermission->id)\n            ->pluck('role_id')->all();\n\n        // Assign the new permission to all existing admins\n        $newPermissionRoles = array_values(array_map(function ($roleId) use ($permissionId) {\n            return [\n                'role_id'       => $roleId,\n                'permission_id' => $permissionId,\n            ];\n        }, $adminRoleIds));\n\n        DB::table('permission_role')->insert($newPermissionRoles);\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        // Remove content-import permission\n        $importPermission = DB::table('role_permissions')\n            ->where('name', '=', 'content-import')->first();\n\n        if (!$importPermission) {\n            return;\n        }\n\n        DB::table('permission_role')->where('permission_id', '=', $importPermission->id)->delete();\n        DB::table('role_permissions')->where('id', '=', $importPermission->id)->delete();\n    }\n};\n"
  },
  {
    "path": "database/migrations/2024_11_02_160700_create_imports_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('imports', function (Blueprint $table) {\n            $table->increments('id');\n            $table->string('name');\n            $table->string('path');\n            $table->integer('size');\n            $table->string('type');\n            $table->longText('metadata');\n            $table->integer('created_by')->index();\n            $table->timestamps();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('imports');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2024_11_27_171039_add_instance_id_setting.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Facades\\DB;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        DB::table('settings')->insert([\n            'setting_key' => 'instance-id',\n            'value' => Str::uuid(),\n            'created_at' => Carbon::now(),\n            'updated_at' => Carbon::now(),\n            'type' => 'string',\n        ]);\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        DB::table('settings')->where('setting_key', '=', 'instance-id')->delete();\n    }\n};\n"
  },
  {
    "path": "database/migrations/2025_01_29_180933_create_sort_rules_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('sort_rules', function (Blueprint $table) {\n            $table->increments('id');\n            $table->string('name');\n            $table->text('sequence');\n            $table->timestamps();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('sort_rules');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2025_02_05_150842_add_sort_rule_id_to_books.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('books', function (Blueprint $table) {\n            $table->unsignedInteger('sort_rule_id')->nullable()->default(null);\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('books', function (Blueprint $table) {\n            $table->dropColumn('sort_rule_id');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2025_04_18_215145_add_content_refs_and_archived_to_comments.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('comments', function (Blueprint $table) {\n            $table->string('content_ref');\n            $table->boolean('archived')->index();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('comments', function (Blueprint $table) {\n            $table->dropColumn('content_ref');\n            $table->dropColumn('archived');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2025_09_02_111542_remove_unused_columns.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('comments', function (Blueprint $table) {\n            $table->dropColumn('text');\n        });\n\n        Schema::table('role_permissions', function (Blueprint $table) {\n            $table->dropColumn('display_name');\n            $table->dropColumn('description');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('comments', function (Blueprint $table) {\n            $table->longText('text')->nullable();\n        });\n\n        Schema::table('role_permissions', function (Blueprint $table) {\n            $table->string('display_name')->nullable();\n            $table->string('description')->nullable();\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2025_09_15_132850_create_entities_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('entities', function (Blueprint $table) {\n            $table->bigIncrements('id');\n            $table->string('type', 10)->index();\n            $table->string('name');\n            $table->string('slug')->index();\n\n            $table->unsignedBigInteger('book_id')->nullable()->index();\n            $table->unsignedBigInteger('chapter_id')->nullable()->index();\n            $table->unsignedInteger('priority')->nullable();\n\n            $table->timestamp('created_at')->nullable();\n            $table->timestamp('updated_at')->nullable()->index();\n            $table->timestamp('deleted_at')->nullable()->index();\n\n            $table->unsignedInteger('created_by')->nullable();\n            $table->unsignedInteger('updated_by')->nullable();\n            $table->unsignedInteger('owned_by')->nullable()->index();\n\n            $table->primary(['id', 'type'], 'entities_pk');\n        });\n\n        Schema::create('entity_container_data', function (Blueprint $table) {\n            $table->unsignedBigInteger('entity_id');\n            $table->string('entity_type', 10);\n            $table->text('description');\n            $table->text('description_html');\n\n            $table->unsignedBigInteger('default_template_id')->nullable();\n            $table->unsignedInteger('image_id')->nullable();\n            $table->unsignedInteger('sort_rule_id')->nullable();\n\n            $table->primary(['entity_id', 'entity_type'], 'entity_container_data_pk');\n        });\n\n        Schema::create('entity_page_data', function (Blueprint $table) {\n            $table->unsignedBigInteger('page_id')->primary();\n\n            $table->boolean('draft')->index();\n            $table->boolean('template')->index();\n            $table->unsignedInteger('revision_count');\n            $table->string('editor', 50);\n\n            $table->longText('html');\n            $table->longText('text');\n            $table->longText('markdown');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('entities');\n        Schema::dropIfExists('entity_container_data');\n        Schema::dropIfExists('entity_page_data');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2025_09_15_134701_migrate_entity_data.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        // Start a transaction to avoid leaving a message DB on error\n        DB::beginTransaction();\n\n        // Migrate book/shelf data to entities\n        foreach (['books' => 'book', 'bookshelves' => 'bookshelf'] as $table => $type) {\n            DB::table('entities')->insertUsing([\n                'id', 'type', 'name', 'slug', 'created_at', 'updated_at', 'deleted_at', 'created_by', 'updated_by', 'owned_by',\n            ], DB::table($table)->select([\n                'id', DB::raw(\"'{$type}'\"), 'name', 'slug', 'created_at', 'updated_at', 'deleted_at', 'created_by', 'updated_by', 'owned_by',\n            ]));\n        }\n\n        // Migrate chapter data to entities\n        DB::table('entities')->insertUsing([\n            'id', 'type', 'name', 'slug', 'book_id', 'priority', 'created_at', 'updated_at', 'deleted_at', 'created_by', 'updated_by', 'owned_by',\n        ], DB::table('chapters')->select([\n            'id', DB::raw(\"'chapter'\"), 'name', 'slug', 'book_id', 'priority', 'created_at', 'updated_at', 'deleted_at', 'created_by', 'updated_by', 'owned_by',\n        ]));\n\n        DB::table('entities')->insertUsing([\n            'id', 'type', 'name', 'slug', 'book_id', 'chapter_id', 'priority', 'created_at', 'updated_at', 'deleted_at', 'created_by', 'updated_by', 'owned_by',\n        ], DB::table('pages')->select([\n            'id', DB::raw(\"'page'\"), 'name', 'slug', 'book_id', 'chapter_id', 'priority', 'created_at', 'updated_at', 'deleted_at', 'created_by', 'updated_by', 'owned_by',\n        ]));\n\n        // Migrate shelf data to entity_container_data\n        DB::table('entity_container_data')->insertUsing([\n            'entity_id', 'entity_type', 'description', 'description_html', 'image_id',\n        ], DB::table('bookshelves')->select([\n            'id', DB::raw(\"'bookshelf'\"), 'description', 'description_html', 'image_id',\n        ]));\n\n        // Migrate book data to entity_container_data\n        DB::table('entity_container_data')->insertUsing([\n            'entity_id', 'entity_type', 'description', 'description_html', 'default_template_id', 'image_id', 'sort_rule_id'\n        ], DB::table('books')->select([\n            'id', DB::raw(\"'book'\"), 'description', 'description_html', 'default_template_id', 'image_id', 'sort_rule_id'\n        ]));\n\n        // Migrate chapter data to entity_container_data\n        DB::table('entity_container_data')->insertUsing([\n            'entity_id', 'entity_type', 'description', 'description_html', 'default_template_id',\n        ], DB::table('chapters')->select([\n            'id', DB::raw(\"'chapter'\"), 'description', 'description_html', 'default_template_id',\n        ]));\n\n        // Migrate page data to entity_page_data\n        DB::table('entity_page_data')->insertUsing([\n            'page_id', 'draft', 'template', 'revision_count', 'editor', 'html', 'text', 'markdown',\n        ], DB::table('pages')->select([\n            'id', 'draft', 'template', 'revision_count', 'editor', 'html', 'text', 'markdown',\n        ]));\n\n        // Fix up data - Convert 0 id references to null\n        DB::table('entities')->where('created_by', '=', 0)->update(['created_by' => null]);\n        DB::table('entities')->where('updated_by', '=', 0)->update(['updated_by' => null]);\n        DB::table('entities')->where('owned_by', '=', 0)->update(['owned_by' => null]);\n        DB::table('entities')->where('chapter_id', '=', 0)->update(['chapter_id' => null]);\n\n        // Fix up data - Convert any missing id-based references to null\n        $userIdQuery = DB::table('users')->select('id');\n        DB::table('entities')->whereNotIn('created_by', $userIdQuery)->update(['created_by' => null]);\n        DB::table('entities')->whereNotIn('updated_by', $userIdQuery)->update(['updated_by' => null]);\n        DB::table('entities')->whereNotIn('owned_by', $userIdQuery)->update(['owned_by' => null]);\n        DB::table('entities')->whereNotIn('chapter_id', DB::table('chapters')->select('id'))->update(['chapter_id' => null]);\n\n        // Commit our changes within our transaction\n        DB::commit();\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        // No action here since the actual data remains in the database for the old tables,\n        // so data reversion actions are done in a later migration when the old tables are dropped.\n    }\n};\n"
  },
  {
    "path": "database/migrations/2025_09_15_134751_update_entity_relation_columns.php",
    "content": "<?php\n\nuse BookStack\\Permissions\\JointPermissionBuilder;\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * @var array<string, string|array<string>> $columnByTable\n     */\n    protected static array $columnByTable = [\n        'activities' => 'loggable_id',\n        'attachments' => 'uploaded_to',\n        'bookshelves_books' => ['bookshelf_id', 'book_id'],\n        'comments' => 'entity_id',\n        'deletions' => 'deletable_id',\n        'entity_permissions' => 'entity_id',\n        'favourites' => 'favouritable_id',\n        'images' => 'uploaded_to',\n        'joint_permissions' => 'entity_id',\n        'page_revisions' => 'page_id',\n        'references' => ['from_id', 'to_id'],\n        'search_terms' => 'entity_id',\n        'tags' => 'entity_id',\n        'views' => 'viewable_id',\n        'watches' => 'watchable_id',\n    ];\n\n    protected static array $nullable = [\n        'activities.loggable_id',\n        'images.uploaded_to',\n    ];\n\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        // Drop foreign key constraints\n        Schema::table('bookshelves_books', function (Blueprint $table) {\n            $table->dropForeign(['book_id']);\n            $table->dropForeign(['bookshelf_id']);\n        });\n\n        // Update column types to unsigned big integers\n        foreach (static::$columnByTable as $table => $column) {\n            $tableName = $table;\n            Schema::table($table, function (Blueprint $table) use ($tableName, $column) {\n                if (is_string($column)) {\n                    $column = [$column];\n                }\n\n                foreach ($column as $col) {\n                    if (in_array($tableName . '.' . $col, static::$nullable)) {\n                        $table->unsignedBigInteger($col)->nullable()->change();\n                    } else {\n                        $table->unsignedBigInteger($col)->change();\n                    }\n                }\n            });\n        }\n\n        // Convert image and activity zero values to null\n        DB::table('images')->where('uploaded_to', '=', 0)->update(['uploaded_to' => null]);\n        DB::table('activities')->where('loggable_id', '=', 0)->update(['loggable_id' => null]);\n\n        // Clean up any orphaned gallery/drawio images to nullify their page relation\n        DB::table('images')\n            ->whereIn('type', ['gallery', 'drawio'])\n            ->whereNotIn('uploaded_to', function (Builder $query) {\n                $query->select('id')\n                    ->from('entities')\n                    ->where('type', '=', 'page');\n            })->update(['uploaded_to' => null]);\n\n        // Rebuild joint permissions if needed\n        // This was moved here from 2023_01_24_104625_refactor_joint_permissions_storage since the changes\n        // made for this release would mean our current logic would not be compatible with\n        // the database changes being made. This is based on a count since any joint permissions\n        // would have been truncated in the previous migration.\n        if (\\Illuminate\\Support\\Facades\\DB::table('joint_permissions')->count() === 0) {\n            app(JointPermissionBuilder::class)->rebuildForAll();\n        }\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        // Convert image null values back to zeros\n        DB::table('images')->whereNull('uploaded_to')->update(['uploaded_to' => '0']);\n\n        // Revert columns to standard integers\n        foreach (static::$columnByTable as $table => $column) {\n            $tableName = $table;\n            Schema::table($table, function (Blueprint $table) use ($tableName, $column) {\n                if (is_string($column)) {\n                    $column = [$column];\n                }\n\n                foreach ($column as $col) {\n                    if ($tableName . '.' . $col === 'activities.loggable_id') {\n                        $table->unsignedInteger($col)->nullable()->change();\n                    } else if ($tableName . '.' . $col === 'images.uploaded_to') {\n                        $table->unsignedInteger($col)->default(0)->change();\n                    } else {\n                        $table->unsignedInteger($col)->change();\n                    }\n                }\n            });\n        }\n\n        // Re-add foreign key constraints\n        Schema::table('bookshelves_books', function (Blueprint $table) {\n            $table->foreign('bookshelf_id')->references('id')->on('bookshelves')\n                ->onUpdate('cascade')->onDelete('cascade');\n            $table->foreign('book_id')->references('id')->on('books')\n                ->onUpdate('cascade')->onDelete('cascade');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2025_09_15_134813_drop_old_entity_tables.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Query\\JoinClause;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::dropIfExists('pages');\n        Schema::dropIfExists('chapters');\n        Schema::dropIfExists('books');\n        Schema::dropIfExists('bookshelves');\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::create('pages', function (Blueprint $table) {\n            $table->unsignedInteger('id', true)->primary();\n            $table->integer('book_id')->index();\n            $table->integer('chapter_id')->index();\n            $table->string('name');\n            $table->string('slug')->index();\n            $table->longText('html');\n            $table->longText('text');\n            $table->integer('priority')->index();\n\n            $table->timestamp('created_at')->nullable();\n            $table->timestamp('updated_at')->nullable()->index();\n            $table->integer('created_by')->index();\n            $table->integer('updated_by')->index();\n\n            $table->boolean('draft')->default(0)->index();\n            $table->longText('markdown');\n            $table->integer('revision_count');\n            $table->boolean('template')->default(0)->index();\n            $table->timestamp('deleted_at')->nullable();\n\n            $table->unsignedInteger('owned_by')->index();\n            $table->string('editor', 50)->default('');\n        });\n\n        Schema::create('chapters', function (Blueprint $table) {\n            $table->unsignedInteger('id', true)->primary();\n            $table->integer('book_id')->index();\n            $table->string('slug')->index();\n            $table->text('name');\n            $table->text('description');\n            $table->integer('priority')->index();\n\n            $table->timestamp('created_at')->nullable();\n            $table->timestamp('updated_at')->nullable();\n            $table->integer('created_by')->index();\n            $table->integer('updated_by')->index();\n\n            $table->timestamp('deleted_at')->nullable();\n            $table->unsignedInteger('owned_by')->index();\n            $table->text('description_html');\n            $table->integer('default_template_id')->nullable();\n        });\n\n        Schema::create('books', function (Blueprint $table) {\n            $table->unsignedInteger('id', true)->primary();\n            $table->string('name');\n            $table->string('slug')->index();\n            $table->text('description');\n            $table->timestamp('created_at')->nullable();\n            $table->timestamp('updated_at')->nullable();\n\n            $table->integer('created_by')->index();\n            $table->integer('updated_by')->index();\n\n            $table->integer('image_id')->nullable();\n            $table->timestamp('deleted_at')->nullable();\n            $table->unsignedInteger('owned_by')->index();\n\n            $table->integer('default_template_id')->nullable();\n            $table->text('description_html');\n            $table->unsignedInteger('sort_rule_id')->nullable();\n        });\n\n        Schema::create('bookshelves', function (Blueprint $table) {\n            $table->unsignedInteger('id', true)->primary();\n            $table->string('name', 180);\n            $table->string('slug', 180)->index();\n            $table->text('description');\n\n            $table->integer('created_by')->index();\n            $table->integer('updated_by')->index();\n            $table->integer('image_id')->nullable();\n\n            $table->timestamp('created_at')->nullable();\n            $table->timestamp('updated_at')->nullable();\n            $table->timestamp('deleted_at')->nullable();\n\n            $table->unsignedInteger('owned_by')->index();\n            $table->text('description_html');\n        });\n\n        DB::beginTransaction();\n\n        // Revert nulls back to zeros\n        DB::table('entities')->whereNull('created_by')->update(['created_by' => 0]);\n        DB::table('entities')->whereNull('updated_by')->update(['updated_by' => 0]);\n        DB::table('entities')->whereNull('owned_by')->update(['owned_by' => 0]);\n        DB::table('entities')->whereNull('chapter_id')->update(['chapter_id' => 0]);\n\n        // Restore data back into pages table\n        $pageFields = [\n            'id', 'book_id', 'chapter_id', 'name', 'slug', 'html', 'text', 'priority', 'created_at', 'updated_at',\n            'created_by', 'updated_by', 'draft', 'markdown', 'revision_count', 'template', 'deleted_at', 'owned_by', 'editor'\n        ];\n        $pageQuery = DB::table('entities')->select($pageFields)\n            ->leftJoin('entity_page_data', 'entities.id', '=', 'entity_page_data.page_id')\n            ->where('type', '=', 'page');\n        DB::table('pages')->insertUsing($pageFields, $pageQuery);\n\n        // Restore data back into chapters table\n        $containerJoinClause = function (JoinClause $join) {\n            return $join->on('entities.id', '=', 'entity_container_data.entity_id')\n                ->on('entities.type', '=', 'entity_container_data.entity_type');\n        };\n        $chapterFields = [\n            'id', 'book_id', 'slug', 'name', 'description', 'priority', 'created_at', 'updated_at', 'created_by', 'updated_by',\n            'deleted_at', 'owned_by', 'description_html', 'default_template_id'\n        ];\n        $chapterQuery = DB::table('entities')->select($chapterFields)\n            ->leftJoin('entity_container_data', $containerJoinClause)\n            ->where('type', '=', 'chapter');\n        DB::table('chapters')->insertUsing($chapterFields, $chapterQuery);\n\n        // Restore data back into books table\n        $bookFields = [\n            'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'image_id',\n            'deleted_at', 'owned_by', 'default_template_id', 'description_html', 'sort_rule_id'\n        ];\n        $bookQuery = DB::table('entities')->select($bookFields)\n            ->leftJoin('entity_container_data', $containerJoinClause)\n            ->where('type', '=', 'book');\n        DB::table('books')->insertUsing($bookFields, $bookQuery);\n\n        // Restore data back into bookshelves table\n        $shelfFields = [\n            'id', 'name', 'slug', 'description',  'created_by', 'updated_by', 'image_id', 'created_at', 'updated_at',\n            'deleted_at', 'owned_by', 'description_html',\n        ];\n        $shelfQuery = DB::table('entities')->select($shelfFields)\n            ->leftJoin('entity_container_data', $containerJoinClause)\n            ->where('type', '=', 'bookshelf');\n        DB::table('bookshelves')->insertUsing($shelfFields, $shelfQuery);\n\n        DB::commit();\n    }\n};\n"
  },
  {
    "path": "database/migrations/2025_10_18_163331_clean_user_id_references.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration {\n    protected static array $toNullify = [\n        'attachments' => ['created_by', 'updated_by'],\n        'comments' => ['created_by', 'updated_by'],\n        'deletions' => ['deleted_by'],\n        'entities' => ['created_by', 'updated_by', 'owned_by'],\n        'images' => ['created_by', 'updated_by'],\n        'imports' => ['created_by'],\n        'joint_permissions' => ['owner_id'],\n        'page_revisions' => ['created_by'],\n    ];\n\n    protected static array $toClean = [\n        'api_tokens' => ['user_id'],\n        'email_confirmations' => ['user_id'],\n        'favourites' => ['user_id'],\n        'mfa_values' => ['user_id'],\n        'role_user' => ['user_id'],\n        'sessions' => ['user_id'],\n        'social_accounts' => ['user_id'],\n        'user_invites' => ['user_id'],\n        'views' => ['user_id'],\n        'watches' => ['user_id'],\n    ];\n\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        $idSelectQuery = DB::table('users')->select('id');\n\n        foreach (self::$toNullify as $tableName => $columns) {\n            Schema::table($tableName, function (Blueprint $table) use ($columns) {\n                foreach ($columns as $columnName) {\n                    $table->unsignedInteger($columnName)->nullable()->change();\n                }\n            });\n\n            foreach ($columns as $columnName) {\n                DB::table($tableName)->where($columnName, '=', 0)->update([$columnName => null]);\n                DB::table($tableName)->whereNotIn($columnName, $idSelectQuery)->update([$columnName => null]);\n            }\n        }\n\n        foreach (self::$toClean as $tableName => $columns) {\n            foreach ($columns as $columnName) {\n                DB::table($tableName)->whereNotIn($columnName, $idSelectQuery)->delete();\n            }\n        }\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        foreach (self::$toNullify as $tableName => $columns) {\n            foreach ($columns as $columnName) {\n                DB::table($tableName)->whereNull($columnName)->update([$columnName => 0]);\n            }\n\n            Schema::table($tableName, function (Blueprint $table) use ($columns) {\n                foreach ($columns as $columnName) {\n                    $table->unsignedInteger($columnName)->nullable(false)->change();\n                }\n            });\n        }\n    }\n};\n"
  },
  {
    "path": "database/migrations/2025_10_22_134507_update_comments_relation_field_names.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('comments', function (Blueprint $table) {\n            $table->renameColumn('entity_id', 'commentable_id');\n            $table->renameColumn('entity_type', 'commentable_type');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('comments', function (Blueprint $table) {\n            $table->renameColumn('commentable_id', 'entity_id');\n            $table->renameColumn('commentable_type', 'entity_type');\n        });\n    }\n};\n"
  },
  {
    "path": "database/migrations/2025_11_23_161812_create_slug_history_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        // Create the table for storing slug history\n        Schema::create('slug_history', function (Blueprint $table) {\n            $table->increments('id');\n            $table->string('sluggable_type', 10)->index();\n            $table->unsignedBigInteger('sluggable_id')->index();\n            $table->string('slug')->index();\n            $table->string('parent_slug')->nullable()->index();\n            $table->timestamps();\n        });\n\n        // Migrate in slugs from page revisions\n        $revisionSlugQuery = DB::table('page_revisions')\n            ->select([\n                DB::raw('\\'page\\' as sluggable_type'),\n                'page_id as sluggable_id',\n                'slug',\n                'book_slug as parent_slug',\n                DB::raw('min(created_at) as created_at'),\n                DB::raw('min(updated_at) as updated_at'),\n            ])\n            ->where('type', '=', 'version')\n            ->groupBy(['sluggable_id', 'slug', 'parent_slug']);\n\n        DB::table('slug_history')->insertUsing(\n            ['sluggable_type', 'sluggable_id', 'slug', 'parent_slug', 'created_at', 'updated_at'],\n            $revisionSlugQuery,\n        );\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('slug_history');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2025_12_15_140219_create_mention_history_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('mention_history', function (Blueprint $table) {\n            $table->increments('id');\n            $table->string('mentionable_type', 50)->index();\n            $table->unsignedBigInteger('mentionable_id')->index();\n            $table->unsignedInteger('from_user_id');\n            $table->unsignedInteger('to_user_id');\n            $table->timestamps();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('mention_history');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2025_12_19_103417_add_views_viewable_type_index.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('views', function (Blueprint $table) {\n            $table->index('viewable_type', 'views_viewable_type_index');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('views', function (Blueprint $table) {\n            $table->dropIndex('views_viewable_type_index');\n        });\n    }\n};\n"
  },
  {
    "path": "database/seeders/.gitkeep",
    "content": ""
  },
  {
    "path": "database/seeders/DatabaseSeeder.php",
    "content": "<?php\n\nnamespace Database\\Seeders;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Seeder;\n\nclass DatabaseSeeder extends Seeder\n{\n    /**\n     * Seed the application's database.\n     *\n     * @return void\n     */\n    public function run()\n    {\n        Model::unguard();\n\n        // $this->call(UserTableSeeder::class);\n\n        Model::reguard();\n    }\n}\n"
  },
  {
    "path": "database/seeders/DummyContentSeeder.php",
    "content": "<?php\n\nnamespace Database\\Seeders;\n\nuse BookStack\\Api\\ApiToken;\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Permissions\\JointPermissionBuilder;\nuse BookStack\\Permissions\\Models\\RolePermission;\nuse BookStack\\Search\\SearchIndex;\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Seeder;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Hash;\nuse Illuminate\\Support\\Str;\n\nclass DummyContentSeeder extends Seeder\n{\n    /**\n     * Run the database seeds.\n     */\n    public function run(): void\n    {\n        // Create an editor user\n        $editorUser = User::factory()->create();\n        $editorRole = Role::getRole('editor');\n        $additionalEditorPerms = ['receive-notifications', 'comment-create-all'];\n        $editorRole->permissions()->syncWithoutDetaching(RolePermission::whereIn('name', $additionalEditorPerms)->pluck('id'));\n        $editorUser->attachRole($editorRole);\n\n        // Create a viewer user\n        $viewerUser = User::factory()->create();\n        $role = Role::getRole('viewer');\n        $viewerUser->attachRole($role);\n\n        $byData = ['created_by' => $editorUser->id, 'updated_by' => $editorUser->id, 'owned_by' => $editorUser->id];\n\n        Book::factory()->count(5)->make($byData)\n            ->each(function ($book) use ($byData) {\n                $book->save();\n                $chapters = Chapter::factory()->count(3)->create($byData)\n                    ->each(function ($chapter) use ($book, $byData) {\n                        $pages = Page::factory()->count(3)->make(array_merge($byData, ['book_id' => $book->id]));\n                        $this->saveManyOnRelation($pages, $chapter->pages());\n                    });\n                $pages = Page::factory()->count(3)->make($byData);\n                $this->saveManyOnRelation($chapters, $book->chapters());\n                $this->saveManyOnRelation($pages, $book->pages());\n            });\n\n        $largeBook = Book::factory()->make(array_merge($byData, ['name' => 'Large book' . Str::random(10)]));\n        $largeBook->save();\n\n        $pages = Page::factory()->count(200)->make($byData);\n        $chapters = Chapter::factory()->count(50)->make($byData);\n        $this->saveManyOnRelation($pages, $largeBook->pages());\n        $this->saveManyOnRelation($chapters, $largeBook->chapters());\n\n        $shelves = Bookshelf::factory()->count(10)->make($byData);\n        foreach ($shelves as $shelf) {\n            $shelf->save();\n        }\n\n        $largeBook->shelves()->attach($shelves->pluck('id'));\n\n        // Assign API permission to editor role and create an API key\n        $apiPermission = RolePermission::getByName('access-api');\n        $editorRole->attachPermission($apiPermission);\n        $token = (new ApiToken())->forceFill([\n            'user_id' => $editorUser->id,\n            'name' => 'Testing API key',\n            'expires_at' => ApiToken::defaultExpiry(),\n            'secret' => Hash::make('password'),\n            'token_id' => 'apitoken',\n        ]);\n        $token->save();\n\n        app(JointPermissionBuilder::class)->rebuildForAll();\n        app(SearchIndex::class)->indexAllEntities();\n    }\n\n    /**\n     * Inefficient workaround for saving many on a relation since we can't directly insert\n     * entities since we split them across tables.\n     */\n    protected function saveManyOnRelation(Collection $entities, HasMany $relation): void\n    {\n        foreach ($entities as $entity) {\n            $relation->save($entity);\n        }\n    }\n}\n"
  },
  {
    "path": "database/seeders/LargeContentSeeder.php",
    "content": "<?php\n\nnamespace Database\\Seeders;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Permissions\\JointPermissionBuilder;\nuse BookStack\\Search\\SearchIndex;\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Seeder;\nuse Illuminate\\Support\\Str;\n\nclass LargeContentSeeder extends Seeder\n{\n    /**\n     * Run the database seeds.\n     *\n     * @return void\n     */\n    public function run()\n    {\n        // Create an editor user\n        $editorUser = User::factory()->create();\n        $editorRole = Role::getRole('editor');\n        $editorUser->attachRole($editorRole);\n\n        /** @var Book $largeBook */\n        $largeBook = Book::factory()->create(['name' => 'Large book' . Str::random(10), 'created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);\n        $chapters = Chapter::factory()->count(50)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);\n        $largeBook->chapters()->saveMany($chapters);\n\n        $allPages = [];\n\n        foreach ($chapters as $chapter) {\n            $pages = Page::factory()->count(100)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id, 'chapter_id' => $chapter->id]);\n            $largeBook->pages()->saveMany($pages);\n            array_push($allPages, ...$pages->all());\n        }\n\n        $all = array_merge([$largeBook], $allPages, array_values($chapters->all()));\n\n        app()->make(JointPermissionBuilder::class)->rebuildForEntity($largeBook);\n        app()->make(SearchIndex::class)->indexEntities($all);\n    }\n}\n"
  },
  {
    "path": "dev/api/requests/attachments-create.json",
    "content": "{\n  \"name\": \"My uploaded attachment\",\n  \"uploaded_to\": 8,\n  \"link\": \"https://link.example.com\"\n}"
  },
  {
    "path": "dev/api/requests/attachments-update.json",
    "content": "{\n  \"name\": \"My updated attachment\",\n  \"uploaded_to\": 4,\n  \"link\": \"https://link.example.com/updated\"\n}"
  },
  {
    "path": "dev/api/requests/books-create.json",
    "content": "{\n  \"name\": \"My own book\",\n  \"description_html\": \"<p>This is <strong>my</strong> own little book created via the API</p>\",\n  \"default_template_id\": 2427,\n  \"tags\": [\n    {\"name\": \"Category\", \"value\": \"Top Content\"},\n    {\"name\": \"Rating\", \"value\": \"Highest\"}\n  ]\n}"
  },
  {
    "path": "dev/api/requests/books-update.json",
    "content": "{\n  \"name\": \"My updated book\",\n  \"description_html\": \"<p>This is my book with <em>updated</em> details</p>\",\n  \"default_template_id\": 2427,\n  \"tags\": [\n    {\"name\": \"Subject\", \"value\": \"Updates\"}\n  ]\n}"
  },
  {
    "path": "dev/api/requests/chapters-create.json",
    "content": "{\n  \"book_id\": 1,\n  \"name\": \"My fantastic new chapter\",\n  \"description_html\": \"<p>This is a <strong>great new chapter</strong> that I've created via the API</p>\",\n  \"priority\": 15,\n  \"default_template_id\": 25,\n  \"tags\": [\n    {\"name\": \"Category\", \"value\": \"Top Content\"},\n    {\"name\": \"Rating\", \"value\": \"Highest\"}\n  ]\n}\n"
  },
  {
    "path": "dev/api/requests/chapters-update.json",
    "content": "{\n  \"book_id\": 1,\n  \"name\": \"My fantastic updated chapter\",\n  \"description_html\": \"<p>This is an <strong>updated chapter</strong> that I've altered via the API</p>\",\n  \"priority\": 16,\n  \"default_template_id\": 2428,\n  \"tags\": [\n    {\"name\": \"Category\", \"value\": \"Kinda Good Content\"},\n    {\"name\": \"Rating\", \"value\": \"Medium\"}\n  ]\n}\n"
  },
  {
    "path": "dev/api/requests/comments-create.json",
    "content": "{\n\t\"page_id\": 2646,\n\t\"html\": \"<p>Can the title be updated?</p>\",\n\t\"content_ref\": \"bkmrk-page-title:7341676876991010:3-14\"\n}"
  },
  {
    "path": "dev/api/requests/comments-update.json",
    "content": "{\n\t\"html\": \"<p>Can this comment be updated??????</p>\",\n\t\"archived\": true\n}"
  },
  {
    "path": "dev/api/requests/content-permissions-update.json",
    "content": "{\n  \"owner_id\": 1,\n  \"role_permissions\": [\n    {\n      \"role_id\": 2,\n      \"view\": true,\n      \"create\": true,\n      \"update\": true,\n      \"delete\": false\n    },\n    {\n      \"role_id\": 3,\n      \"view\": false,\n      \"create\": false,\n      \"update\": false,\n      \"delete\": false\n    }\n  ],\n  \"fallback_permissions\": {\n    \"inheriting\": false,\n    \"view\": true,\n    \"create\": true,\n    \"update\": false,\n    \"delete\": false\n  }\n}"
  },
  {
    "path": "dev/api/requests/image-gallery-readDataForUrl.http",
    "content": "GET /api/image-gallery/url/data?url=https%3A%2F%2Fbookstack.example.com%2Fuploads%2Fimages%2Fgallery%2F2025-10%2Fmy-image.png\n"
  },
  {
    "path": "dev/api/requests/image-gallery-update.json",
    "content": "{\n  \"name\": \"My updated image name\"\n}"
  },
  {
    "path": "dev/api/requests/imports-run.json",
    "content": "{\n  \"parent_type\": \"book\",\n  \"parent_id\": 28\n}"
  },
  {
    "path": "dev/api/requests/pages-create.json",
    "content": "{\n\t\"book_id\": 1,\n\t\"name\": \"My API Page\",\n\t\"html\": \"<p>my new API page</p>\",\n\t\"priority\": 15,\n\t\"tags\": [\n\t\t{\"name\": \"Category\", \"value\": \"Not Bad Content\"},\n\t\t{\"name\": \"Rating\", \"value\": \"Average\"}\n\t]\n}\n"
  },
  {
    "path": "dev/api/requests/pages-update.json",
    "content": "{\n\t\"chapter_id\": 1,\n\t\"name\": \"My updated API Page\",\n\t\"html\": \"<p>my new API page - Updated</p>\",\n\t\"priority\": 16,\n\t\"tags\": [\n\t\t{\"name\": \"Category\", \"value\": \"API Examples\"},\n\t\t{\"name\": \"Rating\", \"value\": \"Alright\"}\n\t]\n}\n"
  },
  {
    "path": "dev/api/requests/roles-create.json",
    "content": "{\n  \"display_name\": \"Book Maintainer\",\n  \"description\": \"People who maintain books\",\n  \"mfa_enforced\": true,\n  \"permissions\": [\n    \"book-view-all\",\n    \"book-update-all\",\n    \"book-delete-all\",\n    \"restrictions-manage-all\"\n  ]\n}"
  },
  {
    "path": "dev/api/requests/roles-update.json",
    "content": "{\n  \"display_name\": \"Book & Shelf Maintainers\",\n  \"description\": \"All those who maintain books & shelves\",\n  \"mfa_enforced\": false,\n  \"permissions\": [\n    \"book-view-all\",\n    \"book-update-all\",\n    \"book-delete-all\",\n    \"bookshelf-view-all\",\n    \"bookshelf-update-all\",\n    \"bookshelf-delete-all\",\n    \"restrictions-manage-all\"\n  ]\n}"
  },
  {
    "path": "dev/api/requests/search-all.http",
    "content": "GET /api/search?query=cats+{created_by:me}&page=1&count=2\n"
  },
  {
    "path": "dev/api/requests/shelves-create.json",
    "content": "{\n  \"name\": \"My shelf\",\n  \"description_html\": \"<p>This is <strong>my shelf</strong> with some books</p>\",\n  \"books\": [5,1,3],\n  \"tags\": [\n    {\"name\": \"Category\", \"value\": \"Learning\"}\n  ]\n}"
  },
  {
    "path": "dev/api/requests/shelves-update.json",
    "content": "{\n  \"name\": \"My updated shelf\",\n  \"description_html\": \"<p>This is my <em>updated shelf</em> with some books</p>\",\n  \"books\": [5,1,3]\n}"
  },
  {
    "path": "dev/api/requests/users-create.json",
    "content": "{\n  \"name\": \"Dan Brown\",\n  \"email\": \"dannyb@example.com\",\n  \"roles\": [1],\n  \"language\": \"fr\",\n  \"send_invite\": true\n}"
  },
  {
    "path": "dev/api/requests/users-delete.json",
    "content": "{\n  \"migrate_ownership_id\": 5\n}"
  },
  {
    "path": "dev/api/requests/users-update.json",
    "content": "{\n  \"name\": \"Dan Spaggleforth\",\n  \"email\": \"dspaggles@example.com\",\n  \"roles\": [2],\n  \"language\": \"de\",\n  \"password\": \"hunter2000\"\n}"
  },
  {
    "path": "dev/api/responses/attachments-create.json",
    "content": "{\n  \"id\": 5,\n  \"name\": \"My uploaded attachment\",\n  \"extension\": \"\",\n  \"uploaded_to\": 8,\n  \"external\": true,\n  \"order\": 2,\n  \"created_by\": 1,\n  \"updated_by\": 1,\n  \"created_at\": \"2021-10-20T06:35:46.000000Z\",\n  \"updated_at\": \"2021-10-20T06:35:46.000000Z\"\n}"
  },
  {
    "path": "dev/api/responses/attachments-list.json",
    "content": "{\n  \"data\": [\n    {\n      \"id\": 3,\n      \"name\": \"datasheet.pdf\",\n      \"extension\": \"pdf\",\n      \"uploaded_to\": 8,\n      \"external\": false,\n      \"order\": 1,\n      \"created_at\": \"2021-10-11T06:18:49.000000Z\",\n      \"updated_at\": \"2021-10-20T06:31:10.000000Z\",\n      \"created_by\": 1,\n      \"updated_by\": 1\n    },\n    {\n      \"id\": 4,\n      \"name\": \"Cat reference\",\n      \"extension\": \"\",\n      \"uploaded_to\": 9,\n      \"external\": true,\n      \"order\": 1,\n      \"created_at\": \"2021-10-20T06:30:11.000000Z\",\n      \"updated_at\": \"2021-10-20T06:30:11.000000Z\",\n      \"created_by\": 1,\n      \"updated_by\": 1\n    }\n  ],\n  \"total\": 2\n}"
  },
  {
    "path": "dev/api/responses/attachments-read.json",
    "content": "{\n  \"id\": 5,\n  \"name\": \"My link attachment\",\n  \"extension\": \"\",\n  \"uploaded_to\": 4,\n  \"external\": true,\n  \"order\": 2,\n  \"created_by\": {\n    \"id\": 1,\n    \"name\": \"Admin\",\n    \"slug\": \"admin\"\n  },\n  \"updated_by\": {\n    \"id\": 1,\n    \"name\": \"Admin\",\n    \"slug\": \"admin\"\n  },\n  \"created_at\": \"2021-10-20T06:35:46.000000Z\",\n  \"updated_at\": \"2021-10-20T06:37:11.000000Z\",\n  \"links\": {\n    \"html\": \"<a target=\\\"_blank\\\" href=\\\"https://bookstack.local/attachments/5\\\">My updated attachment</a>\",\n    \"markdown\": \"[My updated attachment](https://bookstack.local/attachments/5)\"\n  },\n  \"content\": \"https://link.example.com/updated\"\n}"
  },
  {
    "path": "dev/api/responses/attachments-update.json",
    "content": "{\n  \"id\": 5,\n  \"name\": \"My updated attachment\",\n  \"extension\": \"\",\n  \"uploaded_to\": 4,\n  \"external\": true,\n  \"order\": 2,\n  \"created_by\": 1,\n  \"updated_by\": 1,\n  \"created_at\": \"2021-10-20T06:35:46.000000Z\",\n  \"updated_at\": \"2021-10-20T06:37:11.000000Z\"\n}"
  },
  {
    "path": "dev/api/responses/audit-log-list.json",
    "content": "{\n  \"data\": [\n    {\n      \"id\": 1,\n      \"type\": \"bookshelf_create\",\n      \"detail\": \"\",\n      \"user_id\": 1,\n      \"loggable_id\": 1,\n      \"loggable_type\": \"bookshelf\",\n      \"ip\": \"124.4.x.x\",\n      \"created_at\": \"2021-09-29T12:32:02.000000Z\",\n      \"user\": {\n        \"id\": 1,\n        \"name\": \"Admins\",\n        \"slug\": \"admins\"\n      }\n    },\n    {\n      \"id\": 2,\n      \"type\": \"auth_login\",\n      \"detail\": \"standard; (1) Admin\",\n      \"user_id\": 1,\n      \"loggable_id\": null,\n      \"loggable_type\": null,\n      \"ip\": \"127.0.x.x\",\n      \"created_at\": \"2021-09-29T12:32:04.000000Z\",\n      \"user\": {\n        \"id\": 1,\n        \"name\": \"Admins\",\n        \"slug\": \"admins\"\n      }\n    },\n    {\n      \"id\": 3,\n      \"type\": \"bookshelf_update\",\n      \"detail\": \"\",\n      \"user_id\": 1,\n      \"loggable_id\": 1,\n      \"loggable_type\": \"bookshelf\",\n      \"ip\": \"127.0.x.x\",\n      \"created_at\": \"2021-09-29T12:32:07.000000Z\",\n      \"user\": {\n        \"id\": 1,\n        \"name\": \"Admins\",\n        \"slug\": \"admins\"\n      }\n    },\n    {\n      \"id\": 4,\n      \"type\": \"page_create\",\n      \"detail\": \"\",\n      \"user_id\": 1,\n      \"loggable_id\": 1,\n      \"loggable_type\": \"page\",\n      \"ip\": \"127.0.x.x\",\n      \"created_at\": \"2021-09-29T12:32:13.000000Z\",\n      \"user\": {\n        \"id\": 1,\n        \"name\": \"Admins\",\n        \"slug\": \"admins\"\n      }\n    },\n    {\n      \"id\": 5,\n      \"type\": \"page_update\",\n      \"detail\": \"\",\n      \"user_id\": 1,\n      \"loggable_id\": 1,\n      \"loggable_type\": \"page\",\n      \"ip\": \"127.0.x.x\",\n      \"created_at\": \"2021-09-29T12:37:27.000000Z\",\n      \"user\": {\n        \"id\": 1,\n        \"name\": \"Admins\",\n        \"slug\": \"admins\"\n      }\n    }\n  ],\n  \"total\": 6088\n}"
  },
  {
    "path": "dev/api/responses/books-create.json",
    "content": "{\n  \"id\": 226,\n  \"name\": \"My own book\",\n  \"slug\": \"my-own-book\",\n  \"description\": \"This is my own little book created via the API\",\n  \"created_at\": \"2023-12-22T14:22:28.000000Z\",\n  \"updated_at\": \"2023-12-22T14:22:28.000000Z\",\n  \"created_by\": 1,\n  \"updated_by\": 1,\n  \"owned_by\": 1,\n  \"default_template_id\": 2427,\n  \"description_html\": \"<p>This is <strong>my<\\/strong> own little book created via the API<\\/p>\",\n  \"tags\": [\n    {\n      \"name\": \"Category\",\n      \"value\": \"Top Content\",\n      \"order\": 0\n    },\n    {\n      \"name\": \"Rating\",\n      \"value\": \"Highest\",\n      \"order\": 0\n    }\n  ],\n  \"cover\": null\n}"
  },
  {
    "path": "dev/api/responses/books-list.json",
    "content": "{\n  \"data\": [\n    {\n      \"id\": 1,\n      \"name\": \"BookStack User Guide\",\n      \"slug\": \"bookstack-user-guide\",\n      \"description\": \"This is a general guide on using BookStack on a day-to-day basis.\",\n      \"created_at\": \"2019-05-05T21:48:46.000000Z\",\n      \"updated_at\": \"2019-12-11T20:57:31.000000Z\",\n      \"created_by\": 1,\n      \"updated_by\": 1,\n      \"owned_by\": 1,\n      \"cover\": null\n    },\n    {\n      \"id\": 2,\n      \"name\": \"Inventore inventore quia voluptatem.\",\n      \"slug\": \"inventore-inventore-quia-voluptatem\",\n      \"description\": \"Veniam nihil voluptas enim laborum corporis quos sint. Ab rerum voluptas ut iste voluptas magni quibusdam ut. Amet omnis enim voluptate neque facilis.\",\n      \"created_at\": \"2019-05-05T22:10:14.000000Z\",\n      \"updated_at\": \"2019-12-11T20:57:23.000000Z\",\n      \"created_by\": 4,\n      \"updated_by\": 3,\n      \"owned_by\": 3,\n      \"cover\": {\n        \"id\": 11,\n        \"name\": \"cat_banner.jpg\",\n        \"url\": \"https://example.com/uploads/images/cover_book/2021-10/cat-banner.jpg\"\n      }\n    }\n  ],\n  \"total\": 14\n}"
  },
  {
    "path": "dev/api/responses/books-read.json",
    "content": "{\n  \"id\": 16,\n  \"name\": \"My own book\",\n  \"slug\": \"my-own-book\",\n  \"description\": \"This is my own little book\",\n  \"description_html\": \"<p>This is my own <em>little</em> book</p>\",\n  \"created_at\": \"2020-01-12T14:09:59.000000Z\",\n  \"updated_at\": \"2020-01-12T14:11:51.000000Z\",\n  \"created_by\": {\n    \"id\": 1,\n    \"name\": \"Admin\",\n    \"slug\": \"admin\"\n  },\n  \"updated_by\": {\n    \"id\": 1,\n    \"name\": \"Admin\",\n    \"slug\": \"admin\"\n  },\n  \"owned_by\": {\n    \"id\": 1,\n    \"name\": \"Admin\",\n    \"slug\": \"admin\"\n  },\n  \"default_template_id\": null,\n  \"contents\": [\n    {\n      \"id\": 50,\n      \"name\": \"Bridge Structures\",\n      \"slug\": \"bridge-structures\",\n      \"book_id\": 16,\n      \"created_at\": \"2021-12-19T15:22:11.000000Z\",\n      \"updated_at\": \"2021-12-21T19:42:29.000000Z\",\n      \"url\": \"https://example.com/books/my-own-book/chapter/bridge-structures\",\n      \"type\": \"chapter\",\n      \"pages\": [\n        {\n          \"id\": 42,\n          \"name\": \"Building Bridges\",\n          \"slug\": \"building-bridges\",\n          \"book_id\": 16,\n          \"chapter_id\": 50,\n          \"draft\": false,\n          \"template\": false,\n          \"created_at\": \"2021-12-19T15:22:11.000000Z\",\n          \"updated_at\": \"2022-09-29T13:44:15.000000Z\",\n          \"url\": \"https://example.com/books/my-own-book/page/building-bridges\"\n        }\n      ]\n    },\n    {\n      \"id\": 43,\n      \"name\": \"Cool Animals\",\n      \"slug\": \"cool-animals\",\n      \"book_id\": 16,\n      \"chapter_id\": null,\n      \"draft\": false,\n      \"template\": false,\n      \"created_at\": \"2021-12-19T18:22:11.000000Z\",\n      \"updated_at\": \"2022-07-29T13:44:15.000000Z\",\n      \"url\": \"https://example.com/books/my-own-book/page/cool-animals\",\n      \"type\": \"page\"\n    }\n  ],\n  \"tags\": [\n    {\n      \"name\": \"Category\",\n      \"value\": \"Guide\",\n      \"order\": 0\n    }\n  ],\n  \"cover\": {\n    \"id\": 452,\n    \"name\": \"sjovall_m117hUWMu40.jpg\",\n    \"url\": \"https://example.com/uploads/images/cover_book/2020-01/sjovall_m117hUWMu40.jpg\",\n    \"created_at\": \"2020-01-12T14:11:51.000000Z\",\n    \"updated_at\": \"2020-01-12T14:11:51.000000Z\",\n    \"created_by\": 1,\n    \"updated_by\": 1,\n    \"path\": \"/uploads/images/cover_book/2020-01/sjovall_m117hUWMu40.jpg\",\n    \"type\": \"cover_book\",\n    \"uploaded_to\": 16\n  },\n  \"shelves\": [\n    {\n      \"id\": 1,\n      \"name\": \"Great reads\",\n      \"slug\": \"great-reads\"\n    },\n    {\n      \"id\": 5,\n      \"name\": \"Personal Books\",\n      \"slug\": \"personal-books\"\n    }\n  ]\n}"
  },
  {
    "path": "dev/api/responses/books-update.json",
    "content": "{\n  \"id\": 226,\n  \"name\": \"My updated book\",\n  \"slug\": \"my-updated-book\",\n  \"description\": \"This is my book with updated details\",\n  \"created_at\": \"2023-12-22T14:22:28.000000Z\",\n  \"updated_at\": \"2023-12-22T14:24:07.000000Z\",\n  \"created_by\": 1,\n  \"updated_by\": 1,\n  \"owned_by\": 1,\n  \"default_template_id\": 2427,\n  \"description_html\": \"<p>This is my book with <em>updated<\\/em> details<\\/p>\",\n  \"tags\": [\n    {\n      \"name\": \"Subject\",\n      \"value\": \"Updates\",\n      \"order\": 0\n    }\n  ],\n  \"cover\": null\n}"
  },
  {
    "path": "dev/api/responses/chapters-create.json",
    "content": "{\n  \"id\": 668,\n  \"book_id\": 1,\n  \"slug\": \"my-fantastic-new-chapter\",\n  \"name\": \"My fantastic new chapter\",\n  \"description\": \"This is a great new chapter that I've created via the API\",\n  \"priority\": 15,\n  \"created_at\": \"2023-12-22T14:26:28.000000Z\",\n  \"updated_at\": \"2023-12-22T14:26:28.000000Z\",\n  \"created_by\": 1,\n  \"updated_by\": 1,\n  \"owned_by\": 1,\n  \"description_html\": \"<p>This is a <strong>great new chapter<\\/strong> that I've created via the API<\\/p>\",\n  \"default_template_id\": 25,\n  \"book_slug\": \"example-book\",\n  \"tags\": [\n    {\n      \"name\": \"Category\",\n      \"value\": \"Top Content\",\n      \"order\": 0\n    },\n    {\n      \"name\": \"Rating\",\n      \"value\": \"Highest\",\n      \"order\": 0\n    }\n  ]\n}\n"
  },
  {
    "path": "dev/api/responses/chapters-list.json",
    "content": "{\n  \"data\": [\n    {\n      \"id\": 1,\n      \"book_id\": 1,\n      \"name\": \"Content Creation\",\n      \"slug\": \"content-creation\",\n      \"description\": \"How to create documentation on whatever subject you need to write about.\",\n      \"priority\": 3,\n      \"created_at\": \"2019-05-05T21:49:56.000000Z\",\n      \"updated_at\": \"2019-09-28T11:24:23.000000Z\",\n      \"created_by\": 1,\n      \"updated_by\": 1,\n      \"owned_by\": 1,\n      \"book_slug\": \"example-book\"\n    },\n    {\n      \"id\": 2,\n      \"book_id\": 1,\n      \"name\": \"Managing Content\",\n      \"slug\": \"managing-content\",\n      \"description\": \"How to keep things organised and orderly in the system for easier navigation and better user experience.\",\n      \"priority\": 5,\n      \"created_at\": \"2019-05-05T21:58:07.000000Z\",\n      \"updated_at\": \"2019-10-17T15:05:34.000000Z\",\n      \"created_by\": 3,\n      \"updated_by\": 3,\n      \"owned_by\": 3,\n      \"book_slug\": \"example-book\"\n    }\n  ],\n  \"total\": 40\n}"
  },
  {
    "path": "dev/api/responses/chapters-read.json",
    "content": "{\n  \"id\": 1,\n  \"book_id\": 1,\n  \"slug\": \"content-creation\",\n  \"name\": \"Content Creation\",\n  \"description\": \"How to create documentation on whatever subject you need to write about.\",\n  \"description_html\": \"<p>How to create <strong>documentation</strong> on whatever subject you need to write about.</p>\",\n  \"default_template_id\": 25,\n  \"priority\": 3,\n  \"created_at\": \"2019-05-05T21:49:56.000000Z\",\n  \"updated_at\": \"2019-09-28T11:24:23.000000Z\",\n  \"created_by\": {\n    \"id\": 1,\n    \"name\": \"Admin\",\n    \"slug\": \"admin\"\n  },\n  \"updated_by\": {\n    \"id\": 1,\n    \"name\": \"Admin\",\n    \"slug\": \"admin\"\n  },\n  \"owned_by\": {\n    \"id\": 1,\n    \"name\": \"Admin\",\n    \"slug\": \"admin\"\n  },\n  \"book_slug\": \"example-book\",\n  \"tags\": [\n    {\n      \"name\": \"Category\",\n      \"value\": \"Guide\",\n      \"order\": 0\n    }\n  ],\n  \"pages\": [\n    {\n      \"id\": 1,\n      \"book_id\": 1,\n      \"chapter_id\": 1,\n      \"name\": \"How to create page content\",\n      \"slug\": \"how-to-create-page-content\",\n      \"priority\": 0,\n      \"created_at\": \"2019-05-05T21:49:58.000000Z\",\n      \"updated_at\": \"2019-08-26T14:32:59.000000Z\",\n      \"created_by\": 1,\n      \"updated_by\": 1,\n      \"owned_by\": 1,\n      \"draft\": false,\n      \"revision_count\": 2,\n      \"template\": false,\n      \"editor\": \"wysiwyg\",\n      \"book_slug\": \"example-book\"\n    },\n    {\n      \"id\": 7,\n      \"book_id\": 1,\n      \"chapter_id\": 1,\n      \"name\": \"Good book structure\",\n      \"slug\": \"good-book-structure\",\n      \"priority\": 1,\n      \"created_at\": \"2019-05-05T22:01:55.000000Z\",\n      \"updated_at\": \"2019-06-06T12:03:04.000000Z\",\n      \"created_by\": 3,\n      \"updated_by\": 3,\n      \"owned_by\": 1,\n      \"draft\": false,\n      \"revision_count\": 1,\n      \"template\": false,\n      \"editor\": \"wysiwyg\",\n      \"book_slug\": \"example-book\"\n    }\n  ]\n}"
  },
  {
    "path": "dev/api/responses/chapters-update.json",
    "content": "{\n  \"id\": 668,\n  \"book_id\": 1,\n  \"slug\": \"my-fantastic-updated-chapter\",\n  \"name\": \"My fantastic updated chapter\",\n  \"description\": \"This is an updated chapter that I've altered via the API\",\n  \"priority\": 16,\n  \"created_at\": \"2023-12-22T14:26:28.000000Z\",\n  \"updated_at\": \"2023-12-22T14:27:59.000000Z\",\n  \"created_by\": 1,\n  \"updated_by\": 1,\n  \"owned_by\": 1,\n  \"description_html\": \"<p>This is an <strong>updated chapter<\\/strong> that I've altered via the API<\\/p>\",\n  \"default_template_id\": 2428,\n  \"book_slug\": \"example-book\",\n  \"tags\": [\n    {\n      \"name\": \"Category\",\n      \"value\": \"Kinda Good Content\",\n      \"order\": 0\n    },\n    {\n      \"name\": \"Rating\",\n      \"value\": \"Medium\",\n      \"order\": 0\n    }\n  ]\n}\n"
  },
  {
    "path": "dev/api/responses/comments-create.json",
    "content": "{\n\t\"id\": 167,\n\t\"commentable_id\": 2646,\n\t\"commentable_type\": \"page\",\n\t\"parent_id\": null,\n\t\"local_id\": 29,\n\t\"created_by\": 1,\n\t\"updated_by\": 1,\n\t\"created_at\": \"2025-10-24T14:05:41.000000Z\",\n\t\"updated_at\": \"2025-10-24T14:05:41.000000Z\",\n\t\"content_ref\": \"bkmrk-page-title:7341676876991010:3-14\",\n\t\"archived\": false\n}"
  },
  {
    "path": "dev/api/responses/comments-list.json",
    "content": "{\n\t\"data\": [\n\t\t{\n\t\t\t\"id\": 1,\n\t\t\t\"commentable_id\": 2607,\n\t\t\t\"commentable_type\": \"page\",\n\t\t\t\"parent_id\": null,\n\t\t\t\"local_id\": 1,\n\t\t\t\"content_ref\": \"\",\n\t\t\t\"created_by\": 1,\n\t\t\t\"updated_by\": 1,\n\t\t\t\"created_at\": \"2022-04-20T08:43:27.000000Z\",\n\t\t\t\"updated_at\": \"2022-04-20T08:43:27.000000Z\"\n\t\t},\n\t\t{\n\t\t\t\"id\": 18,\n\t\t\t\"commentable_id\": 2607,\n\t\t\t\"commentable_type\": \"page\",\n\t\t\t\"parent_id\": 1,\n\t\t\t\"local_id\": 2,\n\t\t\t\"content_ref\": \"\",\n\t\t\t\"created_by\": 3,\n\t\t\t\"updated_by\": 3,\n\t\t\t\"created_at\": \"2022-11-15T08:12:35.000000Z\",\n\t\t\t\"updated_at\": \"2022-11-15T08:12:35.000000Z\"\n\t\t}\n\t],\n\t\"total\": 88\n}"
  },
  {
    "path": "dev/api/responses/comments-read.json",
    "content": "{\n\t\"id\": 22,\n\t\"commentable_id\": 2646,\n\t\"commentable_type\": \"page\",\n\t\"html\": \"<p>This page looks great!<\\/p>\\n\",\n\t\"parent_id\": null,\n\t\"local_id\": 2,\n\t\"created_by\": {\n\t\t\"id\": 1,\n\t\t\"name\": \"Admin\",\n\t\t\"slug\": \"admin\"\n\t},\n\t\"updated_by\": {\n\t\t\"id\": 1,\n\t\t\"name\": \"Admin\",\n\t\t\"slug\": \"admin\"\n\t},\n\t\"created_at\": \"2023-06-07T07:50:56.000000Z\",\n\t\"updated_at\": \"2023-06-07T07:50:56.000000Z\",\n\t\"content_ref\": \"\",\n\t\"archived\": false,\n\t\"replies\": [\n\t\t{\n\t\t\t\"id\": 34,\n\t\t\t\"commentable_id\": 2646,\n\t\t\t\"commentable_type\": \"page\",\n\t\t\t\"html\": \"<p>Thanks for the comment!<\\/p>\\n\",\n\t\t\t\"parent_id\": 2,\n\t\t\t\"local_id\": 10,\n\t\t\t\"created_by\": 2,\n\t\t\t\"updated_by\": 2,\n\t\t\t\"created_at\": \"2023-06-07T13:46:25.000000Z\",\n\t\t\t\"updated_at\": \"2023-06-07T13:46:25.000000Z\",\n\t\t\t\"content_ref\": \"\",\n\t\t\t\"archived\": false\n\t\t}\n\t]\n}"
  },
  {
    "path": "dev/api/responses/comments-update.json",
    "content": "{\n\t\"id\": 167,\n\t\"commentable_id\": 2646,\n\t\"commentable_type\": \"page\",\n\t\"parent_id\": null,\n\t\"local_id\": 29,\n\t\"created_by\": 1,\n\t\"updated_by\": 1,\n\t\"created_at\": \"2025-10-24T14:05:41.000000Z\",\n\t\"updated_at\": \"2025-10-24T14:09:56.000000Z\",\n\t\"content_ref\": \"bkmrk-page-title:7341676876991010:3-14\",\n\t\"archived\": true\n}"
  },
  {
    "path": "dev/api/responses/content-permissions-read.json",
    "content": "{\n  \"owner\": {\n    \"id\": 1,\n    \"name\": \"Admin\",\n    \"slug\": \"admin\"\n  },\n  \"role_permissions\": [\n    {\n      \"role_id\": 2,\n      \"view\": true,\n      \"create\": false,\n      \"update\": true,\n      \"delete\": false,\n      \"role\": {\n        \"id\": 2,\n        \"display_name\": \"Editor\"\n      }\n    },\n    {\n      \"role_id\": 10,\n      \"view\": true,\n      \"create\": true,\n      \"update\": false,\n      \"delete\": false,\n      \"role\": {\n        \"id\": 10,\n        \"display_name\": \"Wizards of the west\"\n      }\n    }\n  ],\n  \"fallback_permissions\": {\n    \"inheriting\": false,\n    \"view\": true,\n    \"create\": false,\n    \"update\": false,\n    \"delete\": false\n  }\n}"
  },
  {
    "path": "dev/api/responses/content-permissions-update.json",
    "content": "{\n  \"owner\": {\n    \"id\": 1,\n    \"name\": \"Admin\",\n    \"slug\": \"admin\"\n  },\n  \"role_permissions\": [\n    {\n      \"role_id\": 2,\n      \"view\": true,\n      \"create\": true,\n      \"update\": true,\n      \"delete\": false,\n      \"role\": {\n        \"id\": 2,\n        \"display_name\": \"Editor\"\n      }\n    },\n    {\n      \"role_id\": 3,\n      \"view\": false,\n      \"create\": false,\n      \"update\": false,\n      \"delete\": false,\n      \"role\": {\n        \"id\": 3,\n        \"display_name\": \"Viewer\"\n      }\n    }\n  ],\n  \"fallback_permissions\": {\n    \"inheriting\": false,\n    \"view\": true,\n    \"create\": true,\n    \"update\": false,\n    \"delete\": false\n  }\n}"
  },
  {
    "path": "dev/api/responses/image-gallery-create.json",
    "content": "{\n  \"name\": \"cute-cat-image.png\",\n  \"path\": \"\\/uploads\\/images\\/gallery\\/2023-03\\/cute-cat-image.png\",\n  \"url\": \"https:\\/\\/bookstack.example.com\\/uploads\\/images\\/gallery\\/2023-03\\/cute-cat-image.png\",\n  \"type\": \"gallery\",\n  \"uploaded_to\": 1,\n  \"created_by\": {\n    \"id\": 1,\n    \"name\": \"Admin\",\n    \"slug\": \"admin\"\n  },\n  \"updated_by\": {\n    \"id\": 1,\n    \"name\": \"Admin\",\n    \"slug\": \"admin\"\n  },\n  \"updated_at\": \"2023-03-15T16:32:09.000000Z\",\n  \"created_at\": \"2023-03-15T16:32:09.000000Z\",\n  \"id\": 618,\n  \"thumbs\": {\n    \"gallery\": \"https:\\/\\/bookstack.example.com\\/uploads\\/images\\/gallery\\/2023-03\\/thumbs-150-150\\/cute-cat-image.png\",\n    \"display\": \"https:\\/\\/bookstack.example.com\\/uploads\\/images\\/gallery\\/2023-03\\/scaled-1680-\\/cute-cat-image.png\"\n  },\n  \"content\": {\n    \"html\": \"<a href=\\\"https:\\/\\/bookstack.example.com\\/uploads\\/images\\/gallery\\/2023-03\\/cute-cat-image.png\\\" target=\\\"_blank\\\"><img src=\\\"https:\\/\\/bookstack.example.com\\/uploads\\/images\\/gallery\\/2023-03\\/scaled-1680-\\/cute-cat-image.png\\\" alt=\\\"cute-cat-image.png\\\"><\\/a>\",\n    \"markdown\": \"![cute-cat-image.png](https:\\/\\/bookstack.example.com\\/uploads\\/images\\/gallery\\/2023-03\\/scaled-1680-\\/cute-cat-image.png)\"\n  }\n}"
  },
  {
    "path": "dev/api/responses/image-gallery-list.json",
    "content": "{\n  \"data\": [\n    {\n      \"id\": 1,\n      \"name\": \"My cat scribbles\",\n      \"url\": \"https:\\/\\/bookstack.example.com\\/uploads\\/images\\/gallery\\/2023-02\\/scribbles.jpg\",\n      \"path\": \"\\/uploads\\/images\\/gallery\\/2023-02\\/scribbles.jpg\",\n      \"type\": \"gallery\",\n      \"uploaded_to\": 1,\n      \"created_by\": 1,\n      \"updated_by\": 1,\n      \"created_at\": \"2023-02-12T16:34:57.000000Z\",\n      \"updated_at\": \"2023-02-12T16:34:57.000000Z\"\n    },\n    {\n      \"id\": 2,\n      \"name\": \"Drawing-1.png\",\n      \"url\": \"https:\\/\\/bookstack.example.com\\/uploads\\/images\\/drawio\\/2023-02\\/drawing-1.png\",\n      \"path\": \"\\/uploads\\/images\\/drawio\\/2023-02\\/drawing-1.png\",\n      \"type\": \"drawio\",\n      \"uploaded_to\": 2,\n      \"created_by\": 2,\n      \"updated_by\": 2,\n      \"created_at\": \"2023-02-12T16:39:19.000000Z\",\n      \"updated_at\": \"2023-02-12T16:39:19.000000Z\"\n    },\n    {\n      \"id\": 8,\n      \"name\": \"beans.jpg\",\n      \"url\": \"https:\\/\\/bookstack.example.com\\/uploads\\/images\\/gallery\\/2023-02\\/beans.jpg\",\n      \"path\": \"\\/uploads\\/images\\/gallery\\/2023-02\\/beans.jpg\",\n      \"type\": \"gallery\",\n      \"uploaded_to\": 6,\n      \"created_by\": 1,\n      \"updated_by\": 1,\n      \"created_at\": \"2023-02-15T19:37:44.000000Z\",\n      \"updated_at\": \"2023-02-15T19:37:44.000000Z\"\n    }\n  ],\n  \"total\": 3\n}"
  },
  {
    "path": "dev/api/responses/image-gallery-read.json",
    "content": "{\n  \"id\": 618,\n  \"name\": \"cute-cat-image.png\",\n  \"url\": \"https:\\/\\/bookstack.example.com\\/uploads\\/images\\/gallery\\/2023-03\\/cute-cat-image.png\",\n  \"created_at\": \"2023-03-15T16:32:09.000000Z\",\n  \"updated_at\": \"2023-03-15T16:32:09.000000Z\",\n  \"created_by\": {\n    \"id\": 1,\n    \"name\": \"Admin\",\n    \"slug\": \"admin\"\n  },\n  \"updated_by\": {\n    \"id\": 1,\n    \"name\": \"Admin\",\n    \"slug\": \"admin\"\n  },\n  \"path\": \"\\/uploads\\/images\\/gallery\\/2023-03\\/cute-cat-image.png\",\n  \"type\": \"gallery\",\n  \"uploaded_to\": 1,\n  \"thumbs\": {\n    \"gallery\": \"https:\\/\\/bookstack.example.com\\/uploads\\/images\\/gallery\\/2023-03\\/thumbs-150-150\\/cute-cat-image.png\",\n    \"display\": \"https:\\/\\/bookstack.example.com\\/uploads\\/images\\/gallery\\/2023-03\\/scaled-1680-\\/cute-cat-image.png\"\n  },\n  \"content\": {\n    \"html\": \"<a href=\\\"https:\\/\\/bookstack.example.com\\/uploads\\/images\\/gallery\\/2023-03\\/cute-cat-image.png\\\" target=\\\"_blank\\\"><img src=\\\"https:\\/\\/bookstack.example.com\\/uploads\\/images\\/gallery\\/2023-03\\/scaled-1680-\\/cute-cat-image.png\\\" alt=\\\"cute-cat-image.png\\\"><\\/a>\",\n    \"markdown\": \"![cute-cat-image.png](https:\\/\\/bookstack.example.com\\/uploads\\/images\\/gallery\\/2023-03\\/scaled-1680-\\/cute-cat-image.png)\"\n  }\n}"
  },
  {
    "path": "dev/api/responses/image-gallery-update.json",
    "content": "{\n  \"id\": 618,\n  \"name\": \"My updated image name\",\n  \"url\": \"https:\\/\\/bookstack.example.com\\/uploads\\/images\\/gallery\\/2023-03\\/cute-cat-image.png\",\n  \"created_at\": \"2023-03-15T16:32:09.000000Z\",\n  \"updated_at\": \"2023-03-15T18:31:14.000000Z\",\n  \"created_by\": {\n    \"id\": 1,\n    \"name\": \"Admin\",\n    \"slug\": \"admin\"\n  },\n  \"updated_by\": {\n    \"id\": 1,\n    \"name\": \"Admin\",\n    \"slug\": \"admin\"\n  },\n  \"path\": \"\\/uploads\\/images\\/gallery\\/2023-03\\/cute-cat-image.png\",\n  \"type\": \"gallery\",\n  \"uploaded_to\": 1,\n  \"thumbs\": {\n    \"gallery\": \"https:\\/\\/bookstack.example.com\\/uploads\\/images\\/gallery\\/2023-03\\/thumbs-150-150\\/cute-cat-image.png\",\n    \"display\": \"https:\\/\\/bookstack.example.com\\/uploads\\/images\\/gallery\\/2023-03\\/scaled-1680-\\/cute-cat-image.png\"\n  },\n  \"content\": {\n    \"html\": \"<a href=\\\"https:\\/\\/bookstack.example.com\\/uploads\\/images\\/gallery\\/2023-03\\/cute-cat-image.png\\\" target=\\\"_blank\\\"><img src=\\\"https:\\/\\/bookstack.example.com\\/uploads\\/images\\/gallery\\/2023-03\\/scaled-1680-\\/cute-cat-image.png\\\" alt=\\\"My updated image name\\\"><\\/a>\",\n    \"markdown\": \"![My updated image name](https:\\/\\/bookstack.example.com\\/uploads\\/images\\/gallery\\/2023-03\\/scaled-1680-\\/cute-cat-image.png)\"\n  }\n}"
  },
  {
    "path": "dev/api/responses/imports-create.json",
    "content": "{\n  \"type\": \"chapter\",\n  \"name\": \"Pension Providers\",\n  \"created_by\": 1,\n  \"size\": 2757,\n  \"path\": \"uploads\\/files\\/imports\\/ghnxmS3u9QxLWu82.zip\",\n  \"updated_at\": \"2025-07-18T14:50:27.000000Z\",\n  \"created_at\": \"2025-07-18T14:50:27.000000Z\",\n  \"id\": 31\n}"
  },
  {
    "path": "dev/api/responses/imports-list.json",
    "content": "{\n  \"data\": [\n    {\n      \"id\": 25,\n      \"name\": \"IT Department\",\n      \"size\": 618462,\n      \"type\": \"book\",\n      \"created_by\": 1,\n      \"created_at\": \"2024-12-20T18:40:38.000000Z\",\n      \"updated_at\": \"2024-12-20T18:40:38.000000Z\"\n    },\n    {\n      \"id\": 27,\n      \"name\": \"Clients\",\n      \"size\": 15364,\n      \"type\": \"chapter\",\n      \"created_by\": 1,\n      \"created_at\": \"2025-03-20T12:41:44.000000Z\",\n      \"updated_at\": \"2025-03-20T12:41:44.000000Z\"\n    }\n  ],\n  \"total\": 2\n}"
  },
  {
    "path": "dev/api/responses/imports-read.json",
    "content": "{\n  \"id\": 25,\n  \"name\": \"IT Department\",\n  \"path\": \"uploads\\/files\\/imports\\/7YOpZ6sGIEbYdRFL.zip\",\n  \"size\": 618462,\n  \"type\": \"book\",\n  \"created_by\": 1,\n  \"created_at\": \"2024-12-20T18:40:38.000000Z\",\n  \"updated_at\": \"2024-12-20T18:40:38.000000Z\",\n  \"details\": {\n    \"id\": 4,\n    \"name\": \"IT Department\",\n    \"chapters\": [\n      {\n        \"id\": 3,\n        \"name\": \"Server Systems\",\n        \"priority\": 1,\n        \"pages\": [\n          {\n            \"id\": 22,\n            \"name\": \"prod-aws-stonehawk\",\n            \"priority\": 0,\n            \"attachments\": [],\n            \"images\": [],\n            \"tags\": []\n          }\n        ],\n        \"tags\": []\n      }\n    ],\n    \"pages\": [\n      {\n        \"id\": 23,\n        \"name\": \"Member Onboarding Guide\",\n        \"priority\": 0,\n        \"attachments\": [],\n        \"images\": [],\n        \"tags\": []\n      },\n      {\n        \"id\": 25,\n        \"name\": \"IT Holiday Party Event\",\n        \"priority\": 2,\n        \"attachments\": [],\n        \"images\": [],\n        \"tags\": []\n      }\n    ],\n    \"tags\": []\n  }\n}"
  },
  {
    "path": "dev/api/responses/imports-run.json",
    "content": "{\n  \"id\": 1067,\n  \"book_id\": 28,\n  \"slug\": \"pension-providers\",\n  \"name\": \"Pension Providers\",\n  \"description\": \"Details on the various pension providers that are available\",\n  \"priority\": 7,\n  \"created_at\": \"2025-07-18T14:53:35.000000Z\",\n  \"updated_at\": \"2025-07-18T14:53:36.000000Z\",\n  \"created_by\": 1,\n  \"updated_by\": 1,\n  \"owned_by\": 1,\n  \"default_template_id\": null\n}"
  },
  {
    "path": "dev/api/responses/pages-create.json",
    "content": "{\n\t\"id\": 358,\n\t\"book_id\": 1,\n\t\"chapter_id\": null,\n\t\"name\": \"My API Page\",\n\t\"slug\": \"my-api-page\",\n\t\"html\": \"<p id=\\\"bkmrk-my-new-api-page\\\">my new API page</p>\",\n\t\"raw_html\": \"<p id=\\\"bkmrk-my-new-api-page\\\">my new API page</p>\",\n\t\"priority\": 15,\n\t\"created_at\": \"2020-11-28T15:01:39.000000Z\",\n\t\"updated_at\": \"2020-11-28T15:01:39.000000Z\",\n\t\"created_by\": {\n\t\t\"id\": 1,\n\t\t\"name\": \"Admin\",\n\t\t\"slug\": \"admin\"\n\t},\n\t\"updated_by\": {\n\t\t\"id\": 1,\n\t\t\"name\": \"Admin\",\n\t\t\"slug\": \"admin\"\n\t},\n\t\"owned_by\": {\n\t\t\"id\": 1,\n\t\t\"name\": \"Admin\",\n\t\t\"slug\": \"admin\"\n\t},\n\t\"draft\": false,\n\t\"markdown\": \"\",\n\t\"revision_count\": 1,\n\t\"template\": false,\n\t\"editor\": \"wysiwyg\",\n\t\"tags\": [\n\t\t{\n\t\t\t\"name\": \"Category\",\n\t\t\t\"value\": \"Not Bad Content\",\n\t\t\t\"order\": 0\n\t\t},\n\t\t{\n\t\t\t\"name\": \"Rating\",\n\t\t\t\"value\": \"Average\",\n\t\t\t\"order\": 1\n\t\t}\n\t]\n}"
  },
  {
    "path": "dev/api/responses/pages-list.json",
    "content": "{\n\t\"data\": [\n\t\t{\n\t\t\t\"id\": 1,\n\t\t\t\"book_id\": 1,\n\t\t\t\"chapter_id\": 1,\n\t\t\t\"name\": \"How to create page content\",\n\t\t\t\"slug\": \"how-to-create-page-content\",\n\t\t\t\"priority\": 0,\n\t\t\t\"draft\": false,\n\t\t\t\"revision_count\": 3,\n\t\t\t\"template\": false,\n\t\t\t\"created_at\": \"2019-05-05T21:49:58.000000Z\",\n\t\t\t\"updated_at\": \"2020-07-04T15:50:58.000000Z\",\n\t\t\t\"created_by\": 1,\n\t\t\t\"updated_by\": 1,\n\t\t\t\"owned_by\": 1,\n\t\t\t\"editor\": \"wysiwyg\",\n\t\t\t\"book_slug\": \"example-book\"\n\t\t},\n\t\t{\n\t\t\t\"id\": 2,\n\t\t\t\"book_id\": 1,\n\t\t\t\"chapter_id\": 1,\n\t\t\t\"name\": \"How to use images\",\n\t\t\t\"slug\": \"how-to-use-images\",\n\t\t\t\"priority\": 2,\n\t\t\t\"draft\": false,\n\t\t\t\"revision_count\": 3,\n\t\t\t\"template\": false,\n\t\t\t\"created_at\": \"2019-05-05T21:53:30.000000Z\",\n\t\t\t\"updated_at\": \"2019-06-06T12:03:04.000000Z\",\n\t\t\t\"created_by\": 1,\n\t\t\t\"updated_by\": 1,\n\t\t\t\"owned_by\": 1,\n\t\t\t\"editor\": \"wysiwyg\",\n\t\t\t\"book_slug\": \"example-book\"\n\t\t},\n\t\t{\n\t\t\t\"id\": 3,\n\t\t\t\"book_id\": 1,\n\t\t\t\"chapter_id\": 1,\n\t\t\t\"name\": \"Drawings via draw.io\",\n\t\t\t\"slug\": \"drawings-via-drawio\",\n\t\t\t\"priority\": 3,\n\t\t\t\"draft\": false,\n\t\t\t\"revision_count\": 3,\n\t\t\t\"template\": false,\n\t\t\t\"created_at\": \"2019-05-05T21:53:49.000000Z\",\n\t\t\t\"updated_at\": \"2019-12-18T21:56:52.000000Z\",\n\t\t\t\"created_by\": 1,\n\t\t\t\"updated_by\": 1,\n\t\t\t\"owned_by\": 1,\n\t\t\t\"editor\": \"wysiwyg\",\n\t\t\t\"book_slug\": \"example-book\"\n\t\t}\n\t],\n\t\"total\": 322\n}"
  },
  {
    "path": "dev/api/responses/pages-read.json",
    "content": "{\n\t\"id\": 306,\n\t\"book_id\": 1,\n\t\"chapter_id\": null,\n\t\"name\": \"A page written in markdown\",\n\t\"slug\": \"a-page-written-in-markdown\",\n\t\"html\": \"<h1 id=\\\"bkmrk-this-is-my-cool-page\\\">This is my cool page! With some included text</h1>\",\n\t\"raw_html\": \"<h1 id=\\\"bkmrk-this-is-my-cool-page\\\">This is my cool page! {{@1#bkmrk-a}}</h1>\",\n\t\"priority\": 13,\n\t\"created_at\": \"2020-02-02T21:40:38.000000Z\",\n\t\"updated_at\": \"2020-11-28T14:43:20.000000Z\",\n\t\"created_by\": {\n\t\t\"id\": 1,\n\t\t\"name\": \"Admin\",\n\t\t\"slug\": \"admin\"\n\t},\n\t\"updated_by\": {\n\t\t\"id\": 1,\n\t\t\"name\": \"Admin\",\n\t\t\"slug\": \"admin\"\n\t},\n\t\"owned_by\": {\n\t\t\"id\": 1,\n\t\t\"name\": \"Admin\",\n\t\t\"slug\": \"admin\"\n\t},\n\t\"draft\": false,\n\t\"markdown\": \"# How this is built\\r\\n\\r\\nThis page is written in markdown. BookStack stores the page data in HTML.\\r\\n\\r\\nHere's a cute picture of my cat:\\r\\n\\r\\n[![yXSrubes.jpg](http://example.com/uploads/images/gallery/2020-04/scaled-1680-/yXSrubes.jpg)](http://example.com/uploads/images/gallery/2020-04/yXSrubes.jpg)\",\n\t\"revision_count\": 5,\n\t\"template\": false,\n\t\"editor\": \"wysiwyg\",\n\t\"comments\": {\n\t\t\"active\": [\n\t\t\t{\n\t\t\t\t\"comment\": {\n\t\t\t\t\t\"id\": 22,\n\t\t\t\t\t\"commentable_id\": 306,\n\t\t\t\t\t\"commentable_type\": \"page\",\n\t\t\t\t\t\"html\": \"<p>Does this need revising?<\\/p>\\n\",\n\t\t\t\t\t\"parent_id\": null,\n\t\t\t\t\t\"local_id\": 1,\n\t\t\t\t\t\"created_by\": {\n\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\"name\": \"Admin\",\n\t\t\t\t\t\t\"slug\": \"admin\"\n\t\t\t\t\t},\n\t\t\t\t\t\"updated_by\": 1,\n\t\t\t\t\t\"created_at\": \"2023-06-07T07:50:56.000000Z\",\n\t\t\t\t\t\"updated_at\": \"2023-06-07T07:50:56.000000Z\",\n\t\t\t\t\t\"content_ref\": \"\",\n\t\t\t\t\t\"archived\": false\n\t\t\t\t},\n\t\t\t\t\"depth\": 0,\n\t\t\t\t\"children\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"comment\": {\n\t\t\t\t\t\t\t\"id\": 34,\n\t\t\t\t\t\t\t\"commentable_id\": 2646,\n\t\t\t\t\t\t\t\"commentable_type\": \"page\",\n\t\t\t\t\t\t\t\"html\": \"<p>I think it's okay!<\\/p>\\n\",\n\t\t\t\t\t\t\t\"parent_id\": 1,\n\t\t\t\t\t\t\t\"local_id\": 2,\n\t\t\t\t\t\t\t\"created_by\": {\n\t\t\t\t\t\t\t\t\"id\": 2,\n\t\t\t\t\t\t\t\t\"name\": \"Editor\",\n\t\t\t\t\t\t\t\t\"slug\": \"editor\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"updated_by\": 1,\n\t\t\t\t\t\t\t\"created_at\": \"2023-06-07T13:46:25.000000Z\",\n\t\t\t\t\t\t\t\"updated_at\": \"2023-06-07T13:46:25.000000Z\",\n\t\t\t\t\t\t\t\"content_ref\": \"\",\n\t\t\t\t\t\t\t\"archived\": false\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"depth\": 1,\n\t\t\t\t\t\t\"children\": []\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t}\n\t\t],\n\t\t\"archived\": [\n\t\t\t{\n\t\t\t\t\"comment\": {\n\t\t\t\t\t\"id\": 21,\n\t\t\t\t\t\"commentable_id\": 2646,\n\t\t\t\t\t\"commentable_type\": \"page\",\n\t\t\t\t\t\"html\": \"<p>The title needs to be fixed<\\/p>\\n\",\n\t\t\t\t\t\"parent_id\": null,\n\t\t\t\t\t\"local_id\": 3,\n\t\t\t\t\t\"created_by\": {\n\t\t\t\t\t\t\"id\": 2,\n\t\t\t\t\t\t\"name\": \"Editor\",\n\t\t\t\t\t\t\"slug\": \"editor\"\n\t\t\t\t\t},\n\t\t\t\t\t\"updated_by\": 1,\n\t\t\t\t\t\"created_at\": \"2023-06-07T07:50:49.000000Z\",\n\t\t\t\t\t\"updated_at\": \"2025-10-24T08:37:22.000000Z\",\n\t\t\t\t\t\"content_ref\": \"\",\n\t\t\t\t\t\"archived\": true\n\t\t\t\t},\n\t\t\t\t\"depth\": 0,\n\t\t\t\t\"children\": []\n\t\t\t}\n\t\t]\n\t},\n\t\"tags\": [\n\t\t{\n\t\t\t\"name\": \"Category\",\n\t\t\t\"value\": \"Top Content\",\n\t\t\t\"order\": 0\n\t\t},\n\t\t{\n\t\t\t\"name\": \"Animal\",\n\t\t\t\"value\": \"Cat\",\n\t\t\t\"order\": 1\n\t\t}\n\t]\n}"
  },
  {
    "path": "dev/api/responses/pages-update.json",
    "content": "{\n\t\"id\": 361,\n\t\"book_id\": 1,\n\t\"chapter_id\": 1,\n\t\"name\": \"My updated API Page\",\n\t\"slug\": \"my-updated-api-page\",\n\t\"html\": \"<p id=\\\"bkmrk-my-new-api-page---up\\\">my new API page - Updated</p>\",\n\t\"raw_html\": \"<p id=\\\"bkmrk-my-new-api-page---up\\\">my new API page - Updated</p>\",\n\t\"priority\": 16,\n\t\"created_at\": \"2020-11-28T15:10:54.000000Z\",\n\t\"updated_at\": \"2020-11-28T15:13:03.000000Z\",\n\t\"created_by\": {\n\t\t\"id\": 1,\n\t\t\"name\": \"Admin\",\n\t\t\"slug\": \"admin\"\n\t},\n\t\"updated_by\": {\n\t\t\"id\": 1,\n\t\t\"name\": \"Admin\",\n\t\t\"slug\": \"admin\"\n\t},\n\t\"owned_by\": {\n\t\t\"id\": 1,\n\t\t\"name\": \"Admin\",\n\t\t\"slug\": \"admin\"\n\t},\n\t\"draft\": false,\n\t\"markdown\": \"\",\n\t\"revision_count\": 5,\n\t\"template\": false,\n\t\"editor\": \"wysiwyg\",\n\t\"tags\": [\n\t\t{\n\t\t\t\"name\": \"Category\",\n\t\t\t\"value\": \"API Examples\",\n\t\t\t\"order\": 0\n\t\t},\n\t\t{\n\t\t\t\"name\": \"Rating\",\n\t\t\t\"value\": \"Alright\",\n\t\t\t\"order\": 0\n\t\t}\n\t]\n}"
  },
  {
    "path": "dev/api/responses/recycle-bin-destroy.json",
    "content": "{\n  \"delete_count\": 2\n}"
  },
  {
    "path": "dev/api/responses/recycle-bin-list.json",
    "content": "{\n  \"data\": [\n    {\n      \"id\": 18,\n      \"deleted_by\": 1,\n      \"created_at\": \"2022-04-20T12:57:46.000000Z\",\n      \"updated_at\": \"2022-04-20T12:57:46.000000Z\",\n      \"deletable_type\": \"page\",\n      \"deletable_id\": 2582,\n      \"deletable\": {\n        \"id\": 2582,\n        \"book_id\": 25,\n        \"chapter_id\": null,\n        \"name\": \"A Wonderful Page\",\n        \"slug\": \"a-wonderful-page\",\n        \"priority\": 9,\n        \"created_at\": \"2022-02-08T00:44:45.000000Z\",\n        \"updated_at\": \"2022-04-20T12:57:46.000000Z\",\n        \"created_by\": 1,\n        \"updated_by\": 1,\n        \"draft\": false,\n        \"revision_count\": 1,\n        \"template\": false,\n        \"owned_by\": 1,\n        \"editor\": \"wysiwyg\",\n        \"book_slug\": \"a-great-book\",\n        \"parent\": {\n          \"id\": 25,\n          \"name\": \"A Great Book\",\n          \"slug\": \"a-great-book\",\n          \"description\": \"\",\n          \"created_at\": \"2022-01-24T16:14:28.000000Z\",\n          \"updated_at\": \"2022-03-06T15:14:50.000000Z\",\n          \"created_by\": 1,\n          \"updated_by\": 1,\n          \"owned_by\": 1,\n          \"type\": \"book\"\n        }\n      }\n    },\n    {\n      \"id\": 19,\n      \"deleted_by\": 1,\n      \"created_at\": \"2022-04-25T16:07:46.000000Z\",\n      \"updated_at\": \"2022-04-25T16:07:46.000000Z\",\n      \"deletable_type\": \"book\",\n      \"deletable_id\": 13,\n      \"deletable\": {\n        \"id\": 13,\n        \"name\": \"A Big Book!\",\n        \"slug\": \"a-big-book\",\n        \"description\": \"This is a very large book with loads of cool stuff in it!\",\n        \"created_at\": \"2021-11-08T11:26:43.000000Z\",\n        \"updated_at\": \"2022-04-25T16:07:47.000000Z\",\n        \"created_by\": 27,\n        \"updated_by\": 1,\n        \"owned_by\": 1,\n        \"pages_count\": 208,\n        \"chapters_count\": 50\n      }\n    }\n  ],\n  \"total\": 2\n}"
  },
  {
    "path": "dev/api/responses/recycle-bin-restore.json",
    "content": "{\n  \"restore_count\": 2\n}"
  },
  {
    "path": "dev/api/responses/roles-create.json",
    "content": "{\n  \"display_name\": \"Book Maintainer\",\n  \"description\": \"People who maintain books\",\n  \"mfa_enforced\": true,\n  \"updated_at\": \"2023-02-19T15:38:40.000000Z\",\n  \"created_at\": \"2023-02-19T15:38:40.000000Z\",\n  \"id\": 26,\n  \"permissions\": [\n    \"book-delete-all\",\n    \"book-update-all\",\n    \"book-view-all\",\n    \"restrictions-manage-all\"\n  ],\n  \"users\": []\n}"
  },
  {
    "path": "dev/api/responses/roles-list.json",
    "content": "{\n  \"data\": [\n    {\n      \"id\": 1,\n      \"display_name\": \"Admin\",\n      \"description\": \"Administrator of the whole application\",\n      \"created_at\": \"2021-09-29T16:29:19.000000Z\",\n      \"updated_at\": \"2022-11-03T13:26:18.000000Z\",\n      \"system_name\": \"admin\",\n      \"external_auth_id\": \"wizards\",\n      \"mfa_enforced\": true,\n      \"users_count\": 11,\n      \"permissions_count\": 54\n    },\n    {\n      \"id\": 2,\n      \"display_name\": \"Editor\",\n      \"description\": \"User can edit Books, Chapters & Pages\",\n      \"created_at\": \"2021-09-29T16:29:19.000000Z\",\n      \"updated_at\": \"2022-12-01T02:32:57.000000Z\",\n      \"system_name\": \"\",\n      \"external_auth_id\": \"\",\n      \"mfa_enforced\": false,\n      \"users_count\": 17,\n      \"permissions_count\": 49\n    },\n    {\n      \"id\": 3,\n      \"display_name\": \"Public\",\n      \"description\": \"The role given to public visitors if allowed\",\n      \"created_at\": \"2021-09-29T16:29:19.000000Z\",\n      \"updated_at\": \"2022-09-02T12:32:12.000000Z\",\n      \"system_name\": \"public\",\n      \"external_auth_id\": \"\",\n      \"mfa_enforced\": false,\n      \"users_count\": 1,\n      \"permissions_count\": 2\n    }\n  ],\n  \"total\": 3\n}"
  },
  {
    "path": "dev/api/responses/roles-read.json",
    "content": "{\n  \"id\": 26,\n  \"display_name\": \"Book Maintainer\",\n  \"description\": \"People who maintain books\",\n  \"created_at\": \"2023-02-19T15:38:40.000000Z\",\n  \"updated_at\": \"2023-02-19T15:38:40.000000Z\",\n  \"system_name\": \"\",\n  \"external_auth_id\": \"\",\n  \"mfa_enforced\": true,\n  \"permissions\": [\n    \"book-delete-all\",\n    \"book-update-all\",\n    \"book-view-all\",\n    \"restrictions-manage-all\"\n  ],\n  \"users\": [\n    {\n      \"id\": 11,\n      \"name\": \"Barry Scott\",\n      \"slug\": \"barry-scott\"\n    }\n  ]\n}"
  },
  {
    "path": "dev/api/responses/roles-update.json",
    "content": "{\n  \"id\": 26,\n  \"display_name\": \"Book & Shelf Maintainers\",\n  \"description\": \"All those who maintain books & shelves\",\n  \"created_at\": \"2023-02-19T15:38:40.000000Z\",\n  \"updated_at\": \"2023-02-19T15:49:13.000000Z\",\n  \"system_name\": \"\",\n  \"external_auth_id\": \"\",\n  \"mfa_enforced\": false,\n  \"permissions\": [\n    \"book-delete-all\",\n    \"book-update-all\",\n    \"book-view-all\",\n    \"bookshelf-delete-all\",\n    \"bookshelf-update-all\",\n    \"bookshelf-view-all\",\n    \"restrictions-manage-all\"\n  ],\n  \"users\": [\n    {\n      \"id\": 11,\n      \"name\": \"Barry Scott\",\n      \"slug\": \"barry-scott\"\n    }\n  ]\n}"
  },
  {
    "path": "dev/api/responses/search-all.json",
    "content": "{\n  \"data\": [\n    {\n      \"id\": 84,\n      \"book_id\": 1,\n      \"slug\": \"a-chapter-for-cats\",\n      \"name\": \"A chapter for cats\",\n      \"created_at\": \"2021-11-14T15:57:35.000000Z\",\n      \"updated_at\": \"2021-11-14T15:57:35.000000Z\",\n      \"type\": \"chapter\",\n      \"url\": \"https://example.com/books/cats/chapter/a-chapter-for-cats\",\n      \"book\": {\n        \"id\": 1,\n        \"name\": \"Cats\",\n        \"slug\": \"cats\"\n      },\n      \"preview_html\": {\n        \"name\": \"A chapter for <strong>cats</strong>\",\n        \"content\": \"...once a bunch of <strong>cats</strong> named tony...behaviour of <strong>cats</strong> is unsuitable\"\n      },\n      \"tags\": []\n    },\n    {\n      \"name\": \"The hows and whys of cats\",\n      \"id\": 396,\n      \"slug\": \"the-hows-and-whys-of-cats\",\n      \"book_id\": 1,\n      \"chapter_id\": 75,\n      \"draft\": false,\n      \"template\": false,\n      \"created_at\": \"2021-05-15T16:28:10.000000Z\",\n      \"updated_at\": \"2021-11-14T15:56:49.000000Z\",\n      \"type\": \"page\",\n      \"url\": \"https://example.com/books/cats/page/the-hows-and-whys-of-cats\",\n      \"book\": {\n        \"id\": 1,\n        \"name\": \"Cats\",\n        \"slug\": \"cats\"\n      },\n      \"chapter\": {\n        \"id\": 75,\n        \"name\": \"A chapter for cats\",\n        \"slug\": \"a-chapter-for-cats\"\n      },\n      \"preview_html\": {\n        \"name\": \"The hows and whys of <strong>cats</strong>\",\n        \"content\": \"...people ask why <strong>cats</strong>? but there are...the reason that <strong>cats</strong> are fast are due to...\"\n      },\n      \"tags\": [\n        {\n          \"name\": \"Animal\",\n          \"value\": \"Cat\",\n          \"order\": 0\n        },\n        {\n          \"name\": \"Category\",\n          \"value\": \"Top Content\",\n          \"order\": 0\n        }\n      ]\n    },\n    {\n      \"name\": \"How advanced are cats?\",\n      \"id\": 362,\n      \"slug\": \"how-advanced-are-cats\",\n      \"book_id\": 13,\n      \"chapter_id\": 73,\n      \"draft\": false,\n      \"template\": false,\n      \"created_at\": \"2020-11-29T21:55:07.000000Z\",\n      \"updated_at\": \"2021-11-14T16:02:39.000000Z\",\n      \"type\": \"page\",\n      \"url\": \"https://example.com/books/big-cats/page/how-advanced-are-cats\",\n      \"book\": {\n        \"id\": 13,\n        \"name\": \"Big Cats\",\n        \"slug\": \"big-cats\"\n      },\n      \"chapter\": {\n        \"id\": 73,\n        \"name\": \"A chapter for bigger cats\",\n        \"slug\": \"a-chapter-for-bigger-cats\"\n      },\n      \"preview_html\": {\n        \"name\": \"How advanced are <strong>cats</strong>?\",\n        \"content\": \"<strong>cats</strong> are some of the most advanced animals in the world.\"\n      },\n      \"tags\": []\n    }\n  ],\n  \"total\": 3\n}\n"
  },
  {
    "path": "dev/api/responses/shelves-create.json",
    "content": "{\n  \"id\": 20,\n  \"name\": \"My shelf\",\n  \"slug\": \"my-shelf\",\n  \"description\": \"This is my shelf with some books\",\n  \"created_by\": 1,\n  \"updated_by\": 1,\n  \"created_at\": \"2023-12-22T14:33:52.000000Z\",\n  \"updated_at\": \"2023-12-22T14:33:52.000000Z\",\n  \"owned_by\": 1,\n  \"description_html\": \"<p>This is <strong>my shelf<\\/strong> with some books<\\/p>\",\n  \"tags\": [\n    {\n      \"name\": \"Category\",\n      \"value\": \"Learning\",\n      \"order\": 0\n    }\n  ],\n  \"cover\": null\n}"
  },
  {
    "path": "dev/api/responses/shelves-list.json",
    "content": "{\n  \"data\": [\n    {\n      \"id\": 8,\n      \"name\": \"Qui qui aspernatur autem molestiae libero necessitatibus molestias.\",\n      \"slug\": \"qui-qui-aspernatur-autem-molestiae-libero-necessitatibus-molestias\",\n      \"description\": \"Enim dolor ut quia error dolores est. Aut distinctio consequuntur non nisi nostrum. Labore cupiditate error labore aliquid provident impedit voluptatibus. Quaerat impedit excepturi eius qui eius voluptatem reiciendis.\",\n      \"created_at\": \"2019-05-05T22:10:16.000000Z\",\n      \"updated_at\": \"2020-04-10T13:00:45.000000Z\",\n      \"created_by\": 4,\n      \"updated_by\": 1,\n      \"owned_by\": 1,\n      \"cover\": {\n        \"id\": 4,\n        \"name\": \"shelf.jpg\",\n        \"url\": \"https://example.com/uploads/images/cover_bookshelf/2024-12/shelf.jpg\"\n      }\n    },\n    {\n      \"id\": 9,\n      \"name\": \"Ipsum aut inventore fuga libero non facilis.\",\n      \"slug\": \"ipsum-aut-inventore-fuga-libero-non-facilis\",\n      \"description\": \"Labore culpa modi perspiciatis harum sit. Maxime non et nam est. Quae ut laboriosam repellendus sunt quisquam. Velit at est perspiciatis nesciunt adipisci nobis illo. Sed possimus odit optio officiis nisi voluptates officiis dolor.\",\n      \"created_at\": \"2019-05-05T22:10:16.000000Z\",\n      \"updated_at\": \"2020-04-10T13:00:58.000000Z\",\n      \"created_by\": 4,\n      \"updated_by\": 1,\n      \"owned_by\": 1,\n      \"cover\": null\n    },\n    {\n      \"id\": 10,\n      \"name\": \"Omnis reiciendis aut molestias sint accusantium.\",\n      \"slug\": \"omnis-reiciendis-aut-molestias-sint-accusantium\",\n      \"description\": \"Qui ea occaecati alias est dolores voluptatem doloribus. Ad reiciendis corporis vero nostrum omnis et. Non doloribus ut eaque ut quos dolores.\",\n      \"created_at\": \"2019-05-05T22:10:16.000000Z\",\n      \"updated_at\": \"2020-04-10T13:00:53.000000Z\",\n      \"created_by\": 4,\n      \"updated_by\": 1,\n      \"owned_by\": 4,\n      \"cover\": null\n    }\n  ],\n  \"total\": 3\n}"
  },
  {
    "path": "dev/api/responses/shelves-read.json",
    "content": "{\n  \"id\": 14,\n  \"name\": \"My shelf\",\n  \"slug\": \"my-shelf\",\n  \"description\": \"This is my shelf with some books\",\n  \"description_html\": \"<p>This is my shelf with some books</p>\",\n  \"created_by\": {\n    \"id\": 1,\n    \"name\": \"Admin\",\n    \"slug\": \"admin\"\n  },\n  \"updated_by\": {\n    \"id\": 1,\n    \"name\": \"Admin\",\n    \"slug\": \"admin\"\n  },\n  \"owned_by\": {\n    \"id\": 1,\n    \"name\": \"Admin\",\n    \"slug\": \"admin\"\n  },\n  \"created_at\": \"2020-04-10T13:24:09.000000Z\",\n  \"updated_at\": \"2020-04-10T13:31:04.000000Z\",\n  \"tags\": [\n    {\n      \"name\": \"Category\",\n      \"value\": \"Guide\",\n      \"order\": 0\n    }\n  ],\n  \"cover\": {\n    \"id\": 501,\n    \"name\": \"anafrancisconi_Sp04AfFCPNM.jpg\",\n    \"url\": \"http://bookstack.local/uploads/images/cover_book/2020-04/anafrancisconi_Sp04AfFCPNM.jpg\",\n    \"created_at\": \"2020-04-10T13:31:04.000000Z\",\n    \"updated_at\": \"2020-04-10T13:31:04.000000Z\",\n    \"created_by\": 1,\n    \"updated_by\": 1,\n    \"path\": \"/uploads/images/cover_book/2020-04/anafrancisconi_Sp04AfFCPNM.jpg\",\n    \"type\": \"cover_book\",\n    \"uploaded_to\": 14\n  },\n  \"books\": [\n    {\n      \"id\": 5,\n      \"name\": \"Sint explicabo alias sunt.\",\n      \"slug\": \"jbsQrzuaXe\",\n      \"description\": \"Hic forum est.\",\n      \"created_at\": \"2020-04-10T13:31:04.000000Z\",\n      \"updated_at\": \"2020-04-10T13:31:04.000000Z\",\n      \"created_by\": 1,\n      \"updated_by\": 1,\n      \"owned_by\": 1\n    },\n    {\n      \"id\": 1,\n      \"name\": \"BookStack User Guide\",\n      \"slug\": \"bookstack-user-guide\",\n      \"description\": \"The Bookstack User Guide Book.\",\n      \"created_at\": \"2020-04-10T15:30:32.000000Z\",\n      \"updated_at\": \"2020-04-13T09:01:04.000000Z\",\n      \"created_by\": 1,\n      \"updated_by\": 2,\n      \"owned_by\": 1\n    },\n    {\n      \"id\": 3,\n      \"name\": \"Molestiae doloribus sint velit suscipit dolorem.\",\n      \"slug\": \"H99QxALaoG\",\n      \"description\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit.\",\n      \"created_at\": \"2020-04-10T13:31:04.000000Z\",\n      \"updated_at\": \"2020-04-10T13:31:04.000000Z\",\n      \"created_by\": 1,\n      \"updated_by\": 1,\n      \"owned_by\": 1\n    }\n  ]\n}"
  },
  {
    "path": "dev/api/responses/shelves-update.json",
    "content": "{\n  \"id\": 20,\n  \"name\": \"My updated shelf\",\n  \"slug\": \"my-updated-shelf\",\n  \"description\": \"This is my updated shelf with some books\",\n  \"created_by\": 1,\n  \"updated_by\": 1,\n  \"created_at\": \"2023-12-22T14:33:52.000000Z\",\n  \"updated_at\": \"2023-12-22T14:35:00.000000Z\",\n  \"owned_by\": 1,\n  \"description_html\": \"<p>This is my <em>updated shelf<\\/em> with some books<\\/p>\",\n  \"tags\": [\n    {\n      \"name\": \"Category\",\n      \"value\": \"Learning\",\n      \"order\": 0\n    }\n  ],\n  \"cover\": null\n}"
  },
  {
    "path": "dev/api/responses/system-read.json",
    "content": "{\n  \"version\": \"v25.02.4\",\n  \"instance_id\": \"1234abcd-cc12-7808-af0a-264cb0cbd611\",\n  \"app_name\": \"My BookStack Instance\",\n  \"app_logo\": \"https://docs.example.com/uploads/images/system/2025-05/cat-icon.png\",\n  \"base_url\": \"https://docs.example.com\"\n}"
  },
  {
    "path": "dev/api/responses/users-create.json",
    "content": "{\n  \"id\": 1,\n  \"name\": \"Dan Brown\",\n  \"email\": \"dannyb@example.com\",\n  \"created_at\": \"2022-02-03T16:27:55.000000Z\",\n  \"updated_at\": \"2022-02-03T16:27:55.000000Z\",\n  \"external_auth_id\": \"abc123456\",\n  \"slug\": \"dan-brown\",\n  \"last_activity_at\": \"2022-02-03T16:27:55.000000Z\",\n  \"profile_url\": \"https://docs.example.com/user/dan-brown\",\n  \"edit_url\": \"https://docs.example.com/settings/users/1\",\n  \"avatar_url\": \"https://docs.example.com/uploads/images/user/2021-10/thumbs-50-50/profile-2021.jpg\",\n  \"roles\": [\n    {\n      \"id\": 1,\n      \"display_name\": \"Admin\"\n    }\n  ]\n}"
  },
  {
    "path": "dev/api/responses/users-list.json",
    "content": "{\n  \"data\": [\n    {\n      \"id\": 1,\n      \"name\": \"Dan Brown\",\n      \"email\": \"dannyb@example.com\",\n      \"created_at\": \"2022-02-03T16:27:55.000000Z\",\n      \"updated_at\": \"2022-02-03T16:27:55.000000Z\",\n      \"external_auth_id\": \"abc123456\",\n      \"slug\": \"dan-brown\",\n      \"last_activity_at\": \"2022-02-03T16:27:55.000000Z\",\n      \"profile_url\": \"https://docs.example.com/user/dan-brown\",\n      \"edit_url\": \"https://docs.example.com/settings/users/1\",\n      \"avatar_url\": \"https://docs.example.com/uploads/images/user/2021-10/thumbs-50-50/profile-2021.jpg\"\n    },\n    {\n      \"id\": 2,\n      \"name\": \"Benny\",\n      \"email\": \"benny@example.com\",\n      \"created_at\": \"2020-01-15T04:43:11.000000Z\",\n      \"updated_at\": \"2021-11-18T17:10:58.000000Z\",\n      \"external_auth_id\": \"\",\n      \"slug\": \"benny\",\n      \"last_activity_at\": \"2022-01-31T20:39:24.000000Z\",\n      \"profile_url\": \"https://docs.example.com/user/benny\",\n      \"edit_url\": \"https://docs.example.com/settings/users/2\",\n      \"avatar_url\": \"https://docs.example.com/uploads/images/user/2021-11/thumbs-50-50/guest.jpg\"\n    }\n  ],\n  \"total\": 28\n}"
  },
  {
    "path": "dev/api/responses/users-read.json",
    "content": "{\n  \"id\": 1,\n  \"name\": \"Dan Brown\",\n  \"email\": \"dannyb@example.com\",\n  \"created_at\": \"2022-02-03T16:27:55.000000Z\",\n  \"updated_at\": \"2022-02-03T16:27:55.000000Z\",\n  \"external_auth_id\": \"abc123456\",\n  \"slug\": \"dan-brown\",\n  \"last_activity_at\": \"2022-02-03T16:27:55.000000Z\",\n  \"profile_url\": \"https://docs.example.com/user/dan-brown\",\n  \"edit_url\": \"https://docs.example.com/settings/users/1\",\n  \"avatar_url\": \"https://docs.example.com/uploads/images/user/2021-10/thumbs-50-50/profile-2021.jpg\",\n  \"roles\": [\n    {\n      \"id\": 1,\n      \"display_name\": \"Admin\"\n    }\n  ]\n}"
  },
  {
    "path": "dev/api/responses/users-update.json",
    "content": "{\n  \"id\": 1,\n  \"name\": \"Dan Spaggleforth\",\n  \"email\": \"dspaggles@example.com\",\n  \"created_at\": \"2022-02-03T16:27:55.000000Z\",\n  \"updated_at\": \"2022-02-03T16:27:55.000000Z\",\n  \"external_auth_id\": \"abc123456\",\n  \"slug\": \"dan-spaggleforth\",\n  \"last_activity_at\": \"2022-02-03T16:27:55.000000Z\",\n  \"profile_url\": \"https://docs.example.com/user/dan-spaggleforth\",\n  \"edit_url\": \"https://docs.example.com/settings/users/1\",\n  \"avatar_url\": \"https://docs.example.com/uploads/images/user/2021-10/thumbs-50-50/profile-2021.jpg\",\n  \"roles\": [\n    {\n      \"id\": 2,\n      \"display_name\": \"Editors\"\n    }\n  ]\n}"
  },
  {
    "path": "dev/build/esbuild.mjs",
    "content": "#!/usr/bin/env node\n\nimport * as esbuild from 'esbuild';\nimport * as path from 'node:path';\nimport * as fs from 'node:fs';\nimport * as process from \"node:process\";\n\n// Check if we're building for production\n// (Set via passing `production` as first argument)\nconst mode = process.argv[2];\nconst isProd = mode === 'production';\nconst __dirname = import.meta.dirname;\n\n// Gather our input files\nconst entryPoints = {\n    app: path.join(__dirname, '../../resources/js/app.ts'),\n    code: path.join(__dirname, '../../resources/js/code/index.mjs'),\n    'legacy-modes': path.join(__dirname, '../../resources/js/code/legacy-modes.mjs'),\n    markdown: path.join(__dirname, '../../resources/js/markdown/index.mts'),\n    wysiwyg: path.join(__dirname, '../../resources/js/wysiwyg/index.ts'),\n};\n\n// Watch styles so we can reload on change\nif (mode === 'watch') {\n    entryPoints['styles-dummy'] = path.join(__dirname, '../../public/dist/styles.css');\n}\n\n// Locate our output directory\nconst outdir = path.join(__dirname, '../../public/dist');\n\n// Define the options for esbuild\nconst options = {\n    bundle: true,\n    metafile: true,\n    entryPoints,\n    outdir,\n    sourcemap: true,\n    target: 'es2021',\n    mainFields: ['module', 'main'],\n    format: 'esm',\n    minify: isProd,\n    logLevel: 'info',\n    loader: {\n        '.html': 'copy',\n        '.svg': 'text',\n    },\n    absWorkingDir: path.join(__dirname, '../..'),\n    alias: {\n        '@icons': './resources/icons',\n        lexical: './resources/js/wysiwyg/lexical/core',\n        '@lexical': './resources/js/wysiwyg/lexical',\n    },\n    banner: {\n        js: '// See the \"/licenses\" URI for full package license details',\n        css: '/* See the \"/licenses\" URI for full package license details */',\n    },\n};\n\nif (mode === 'watch') {\n    options.inject = [\n        path.join(__dirname, './livereload.js'),\n    ];\n}\n\nconst ctx = await esbuild.context(options);\n\nif (mode === 'watch') {\n    // Watch for changes and rebuild on change\n    ctx.watch({});\n    let {hosts, port} = await ctx.serve({\n        servedir: path.join(__dirname, '../../public'),\n        cors: {\n            origin: '*',\n        }\n    });\n} else {\n    // Build with meta output for analysis\n    const result = await ctx.rebuild();\n    const outputs = result.metafile.outputs;\n    const files = Object.keys(outputs);\n    for (const file of files) {\n        const output = outputs[file];\n        console.log(`Written: ${file} @ ${Math.round(output.bytes / 1000)}kB`);\n    }\n    fs.writeFileSync('esbuild-meta.json', JSON.stringify(result.metafile));\n    process.exit(0);\n}\n"
  },
  {
    "path": "dev/build/livereload.js",
    "content": "if (!window.__dev_reload_listening) {\n    listen();\n    window.__dev_reload_listening = true;\n}\n\n\nfunction listen() {\n    console.log('Listening for livereload events...');\n    new EventSource(\"http://127.0.0.1:8000/esbuild\").addEventListener('change', e => {\n        const { added, removed, updated } = JSON.parse(e.data);\n\n        if (!added.length && !removed.length && updated.length > 0) {\n            const updatedPath = updated.filter(path => path.endsWith('.css'))[0]\n            if (!updatedPath) return;\n\n            const links = [...document.querySelectorAll(\"link[rel='stylesheet']\")];\n            for (const link of links) {\n                const url = new URL(link.href);\n                const name = updatedPath.replace('-dummy', '');\n\n                if (url.pathname.endsWith(name)) {\n                    const next = link.cloneNode();\n                    next.href = name + '?version=' + Math.random().toString(36).slice(2);\n                    next.onload = function() {\n                        link.remove();\n                    };\n                    link.after(next);\n                    return\n                }\n            }\n        }\n\n        location.reload()\n    });\n}"
  },
  {
    "path": "dev/build/svg-blank-transform.js",
    "content": "// This is a basic transformer stub to help jest handle SVG files.\n// Essentially blanks them since we don't really need to involve them\n// in our tests (yet).\nmodule.exports = {\n    process() {\n        return {\n            code: 'module.exports = \\'\\';',\n        };\n    },\n    getCacheKey() {\n        // The output is always the same.\n        return 'svgTransform';\n    },\n};\n"
  },
  {
    "path": "dev/checksums/.gitignore",
    "content": "!.gitignore"
  },
  {
    "path": "dev/checksums/vendor",
    "content": "22e02ee72d21ff719c1073abbec8302f8e2096ba6d072e133051064ed24b45b1\n"
  },
  {
    "path": "dev/docker/Dockerfile",
    "content": "FROM php:8.3-apache\n\n# Install additional dependencies\nRUN apt-get update && \\\n    apt-get install -y \\\n        git \\\n        zip \\\n        unzip \\\n        libfreetype-dev \\\n        libjpeg62-turbo-dev \\\n        libldap2-dev \\\n        libpng-dev \\\n        libzip-dev \\\n        wait-for-it && \\\n    rm -rf /var/lib/apt/lists/*\n\n# Mark /app as safe for Git >= 2.35.2\nRUN git config --system --add safe.directory /app\n\n# Install PHP extensions\nRUN docker-php-ext-configure ldap --with-libdir=\"lib/$(gcc -dumpmachine)\" && \\\n    docker-php-ext-configure gd --with-freetype --with-jpeg && \\\n    docker-php-ext-install -j$(nproc) pdo_mysql gd ldap zip && \\\n    pecl install xdebug && \\\n    docker-php-ext-enable xdebug\n\n# Install composer\nRUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer\n\n# Configure apache\nRUN a2enmod rewrite && \\\n    sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf && \\\n    sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf\n\n# Use the default production configuration and update it as required\nRUN mv \"$PHP_INI_DIR/php.ini-production\" \"$PHP_INI_DIR/php.ini\" && \\\n    sed -i 's/memory_limit = 128M/memory_limit = 512M/g' \"$PHP_INI_DIR/php.ini\"\n\nENV APACHE_DOCUMENT_ROOT=\"/app/public\"\n\nWORKDIR /app\n"
  },
  {
    "path": "dev/docker/db-testing/Dockerfile",
    "content": "FROM ubuntu:24.04\n\n# Install additional dependencies\nRUN apt-get update && \\\n    apt-get install -y \\\n        git \\\n\t\twget \\\n        zip \\\n        unzip \\\n\t\tphp \\\n\t\tphp-bcmath php-curl php-mbstring php-gd php-xml php-zip php-mysql php-ldap \\\n       && \\\n    rm -rf /var/lib/apt/lists/*\n\n# Take branch as an argument so we can choose which BookStack\n# branch to test against\nARG BRANCH=development\n\n# Download BookStack & install PHP deps\nRUN mkdir -p /var/www && \\\n    git clone https://github.com/bookstackapp/bookstack.git --branch \"$BRANCH\" --single-branch /var/www/bookstack && \\\n    cd /var/www/bookstack && \\\n    wget https://raw.githubusercontent.com/composer/getcomposer.org/f3108f64b4e1c1ce6eb462b159956461592b3e3e/web/installer -O - -q | php -- --quiet --filename=composer && \\\n    php composer install\n\n# Set the BookStack dir as the default working dir\nWORKDIR /var/www/bookstack\n\n# Set the default action as running php\nENTRYPOINT [\"/bin/php\"]\n"
  },
  {
    "path": "dev/docker/db-testing/readme.md",
    "content": "# Database Testing Suite\n\nThis docker setup is designed to run BookStack's test suite against each major database version we support\nacross MySQL and MariaDB to ensure compatibility and highlight any potential issues before a release.\nThis is a fairly slow and heavy process, so is designed to just be run manually before a release which\nmakes changes to the database schema, or a release which makes significant changes to database queries.\n\n### Running\n\nEverything is ran via the `run.sh` script. This will:\n\n- Optionally, accept a branch of BookStack to use for testing.\n- Build the docker image from the `Dockerfile`.\n  - This will include a built-in copy of the chosen BookStack branch. \n- Cycle through each major supported database version:\n  - Migrate and seed the database.\n  - Run the full PHP test suite.\n\nIf there's a failure for a database version, the script will prompt if you'd like to continue or stop testing.\n\nThis script should be ran from this `db-testing` directory:\n\n```bash\n# Enter this directory\ncd dev/docker/db-testing\n\n# Runs for the 'development' branch by default\n./run.sh\n\n# Run for a specific branch\n./run.sh v25-11\n```\n"
  },
  {
    "path": "dev/docker/db-testing/run.sh",
    "content": "#!/bin/bash\n\nBRANCH=${1:-development}\n\n# Build the container with a known name\ndocker build --no-cache --build-arg BRANCH=\"$BRANCH\" -t bookstack:db-testing .\nif [ $? -eq 1 ]; then\n  echo \"Failed to build app container for testing\"\n  exit 1\nfi\n\n# List of database containers to test against\ncontainers=(\n  \"mysql:8.0\"\n  \"mysql:8.4\"\n  \"mysql:9.5\"\n  \"mariadb:10.6\"\n  \"mariadb:10.11\"\n  \"mariadb:11.4\"\n  \"mariadb:11.8\"\n  \"mariadb:12.0\"\n)\n\n# Pre-clean-up from prior runs\ndocker stop bs-dbtest-db\ndocker network rm bs-dbtest-net\n\n# Cycle over each database image\nfor img in \"${containers[@]}\"; do\n  echo \"Starting tests with $img...\"\n  docker network create bs-dbtest-net\n  docker run -d --rm --name \"bs-dbtest-db\" \\\n    -e MYSQL_DATABASE=bookstack-test \\\n    -e MYSQL_USER=bookstack \\\n    -e MYSQL_PASSWORD=bookstack \\\n    -e MYSQL_ROOT_PASSWORD=password \\\n\t  --network=bs-dbtest-net \\\n    \"$img\"\n  sleep 20\n  APP_RUN='docker run -it --rm --network=bs-dbtest-net -e TEST_DATABASE_URL=\"mysql://bookstack:bookstack@bs-dbtest-db:3306\" bookstack:db-testing'\n  $APP_RUN artisan migrate --force --database=mysql_testing\n  $APP_RUN artisan db:seed --force --class=DummyContentSeeder --database=mysql_testing\n  $APP_RUN vendor/bin/phpunit\n  if [ $? -eq 0 ]; then\n    echo \"$img - Success\"\n  else\n    echo \"$img - Error\"\n    read -p \"Stop script? [y/N] \" ans\n    [[ $ans == [yY] ]] && exit 0\n  fi\n\n  docker stop \"bs-dbtest-db\"\n  docker network rm bs-dbtest-net\ndone\n\n"
  },
  {
    "path": "dev/docker/entrypoint.app.sh",
    "content": "#!/bin/bash\n\nset -e\n\nenv\n\nif [[ -n \"$1\" ]]; then\n    exec \"$@\"\nelse\n    composer install\n    wait-for-it db:3306 -t 45\n    php artisan migrate --database=mysql --force\n    chown -R www-data storage public/uploads bootstrap/cache\n    exec apache2-foreground\nfi\n"
  },
  {
    "path": "dev/docker/entrypoint.node.sh",
    "content": "#!/bin/sh\n\nset -e\n\nnpm install\nnpm rebuild node-sass\n\nSHELL=/bin/sh exec npm run watch\n"
  },
  {
    "path": "dev/docker/php/conf.d/xdebug.ini",
    "content": "zend_extension=xdebug\n\n[xdebug]\nxdebug.mode=debug\nxdebug.client_host=host.docker.internal\nxdebug.start_with_request=yes\nxdebug.client_port=9090"
  },
  {
    "path": "dev/docs/development.md",
    "content": "# Development & Testing\n\nAll development on BookStack is currently done on the `development` branch. \nWhen it's time for a release the `development` branch is merged into release with built & minified CSS & JS then tagged at its version. Here are the current development requirements:\n\n* [Node.js](https://nodejs.org/en/) v22.0+\n\n## Building CSS & JavaScript Assets\n\nThis project uses SASS for CSS development which is built, along with the JavaScript, using a range of npm scripts. The below npm commands can be used to install the dependencies & run the build tasks:\n\n``` bash\n# Install NPM Dependencies\nnpm install\n\n# Build assets for development\nnpm run build\n\n# Build and minify assets for production\nnpm run production\n\n# Build for dev (With sourcemaps) and watch for changes\nnpm run dev\n```\n\nFurther details about the BookStack JavaScript codebase can be found in the [javascript-code.md document](javascript-code.md).\n\n## Automated App Testing \n\nBookStack has a large suite of PHP tests to cover application functionality. We try to ensure that all additions and changes to the platform are covered with testing.\n\nFor details about setting-up, running and writing tests please see the [php-testing.md document](php-testing.md).\n\n## Code Standards\n\nWe use tools to manage code standards and formatting within the project. If submitting a PR, formatting as per our project standards would help for clarity but don't worry too much about using/understanding these tools as we can always address issues at a later stage when they're picked up by our automated tools.\n\n### PHP\n\nPHP code standards are managed by [using PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer).\nStatic analysis is in place using [PHPStan](https://phpstan.org/) & [Larastan](https://github.com/nunomaduro/larastan).\nThe below commands can be used to utilise these tools:\n\n```bash\n# Run code linting using PHP_CodeSniffer\ncomposer lint\n\n# As above, but show rule names in output\ncomposer lint -- -s\n\n# Auto-fix formatting & lint issues via PHP_CodeSniffer phpcbf\ncomposer format\n\n# Run static analysis via larastan/phpstan\ncomposer check-static\n```\n\n### JavaScript\n\nJavaScript code standards use managed using [ESLint](https://eslint.org/).\nThe ESLint rule configuration is managed within the `package.json` file.\nThe below commands can be used to lint and format:\n\n```bash\n# Run code linting using ESLint\nnpm run lint\n\n# Fix code where possible using ESLint\nnpm run fix\n```\n\n## Development using Docker\n\nThis repository ships with a Docker Compose configuration intended for development purposes. It'll build a PHP image with all needed extensions installed and start up a MySQL server and a Node image watching the UI assets.\n\nTo get started, make sure you meet the following requirements:\n\n- Docker and Docker Compose are installed\n- Your user is part of the `docker` group\n\nIf all the conditions are met, you can proceed with the following steps:\n\n1. **Copy `.env.example` to `.env`**, change `APP_KEY` to a random 32 char string and set `APP_ENV` to `local`.\n2. Make sure **port 8080 is unused** *or else* change `DEV_PORT` to a free port on your host.\n3. **Run `chgrp -R docker storage`**. The development container will chown the `storage`, `public/uploads` and `bootstrap/cache` directories to the `www-data` user inside the container so BookStack can write to it. You need to change the group to your host's `docker` group here to not lose access to the `storage` directory.\n4. **Run `docker-compose up`** and wait until the image is built and all database migrations have been done.\n5. You can now login with `admin@admin.com` and `password` as password on `localhost:8080` (or another port if specified).\n\nIf needed, You'll be able to run any artisan commands via docker-compose like so:\n\n```bash\ndocker-compose run app php artisan list\n```\n\nThe docker-compose setup runs an instance of [MailHog](https://github.com/mailhog/MailHog) and sets environment variables to redirect any BookStack-sent emails to MailHog. You can view this mail via the MailHog web interface on `localhost:8025`. You can change the port MailHog is accessible on by setting a `DEV_MAIL_PORT` environment variable.\n\n### Running tests\n\nAfter starting the general development Docker, migrate & seed the testing database:\n\n ```bash\n# This only needs to be done once\ndocker-compose run app php artisan migrate --database=mysql_testing\ndocker-compose run app php artisan db:seed --class=DummyContentSeeder --database=mysql_testing\n```\n\nOnce the database has been migrated & seeded, you can run the tests like so:\n\n ```bash\ndocker-compose run app php vendor/bin/phpunit\n```\n\n### Debugging\n\nThe docker-compose setup ships with Xdebug, which you can listen to on port 9090.\nNB: For some editors like Visual Studio Code, you might need to map your workspace folder to the /app folder within the docker container for this to work.\n"
  },
  {
    "path": "dev/docs/javascript-code.md",
    "content": "# BookStack JavaScript Code\n\nBookStack is primarily server-side-rendered, but it uses JavaScript sparingly to drive any required dynamic elements. Most JavaScript is applied via a custom, and very thin, component interface to keep code organised and somewhat reusable.\n\nJavaScript source code can be found in the `resources/js` directory. This gets bundled and transformed by `esbuild`, ending up in the `public/dist` folder for browser use. Read the [Development > \"Building CSS & JavaScript Assets\"](development.md#building-css-&-javascript-assets) documentation for details on this process.\n\n## Components\n\nThis section details the format for JavaScript components in BookStack. This is a really simple class-based setup with a few helpers provided.\n\n### Defining a Component in JS\n\n```js\nclass Dropdown {\n    setup() {\n        this.container = this.$el;\n        this.menu = this.$refs.menu;\n        this.toggles = this.$manyRefs.toggle;\n    \n        this.speed = parseInt(this.$opts.speed);\n    }\n}\n```\n\nAll usage of $refs, $manyRefs and $opts should be done at the top of the `setup` function so any requirements can be easily seen.\n\nOnce defined, the component has to be registered for use. This is done in the `resources/js/components/index.js` file by defining an additional export, following the pattern of other components. \n\n### Using a Component in HTML\n\nA component is used like so:\n\n```html\n<div component=\"dropdown\"></div>\n\n<!-- or, for multiple -->\n\n<div components=\"dropdown image-picker\"></div>\n```\n\nThe names will be parsed and new component instance will be created if a matching name is found in the `components/index.js` componentMapping. \n\n### Element References\n\nWithin a component you'll often need to refer to other element instances. This can be done like so:\n\n```html\n<div component=\"dropdown\">\n    <span refs=\"dropdown@toggle othercomponent@handle\">View more</span>\n</div>\n```\n\nYou can then access the span element as `this.$refs.toggle` in your component.\n\nMultiple elements of the same reference name can be accessed via a `this.$manyRefs` property within your component. For example, all the buttons in the below example could be accessed via `this.$manyRefs.buttons`.\n\n```html\n<div component=\"list\">\n    <button refs=\"list@button\">Click here</button>\n    <button refs=\"list@button\">No, Click here</button>\n    <button refs=\"list@button\">This button is better</button>\n</div>\n```\n\n### Component Options\n\n```html\n<div component=\"dropdown\"\n    option:dropdown:delay=\"500\"\n    option:dropdown:show>\n</div>\n```\n\nWill result with `this.$opts` being:\n\n```json\n{\n    \"delay\": \"500\",\n    \"show\": \"\"  \n}\n```\n\n#### Component Properties & Methods\n\nA component has the below shown properties & methods available for use. As mentioned above, most of these should be used within the `setup()` function to make the requirements/dependencies of the component clear.\n\n```javascript\n// The root element that the component has been applied to.\nthis.$el\n\n// A map of defined element references within the component.\n// See \"Element References\" above.\nthis.$refs\n\n// A map of defined multi-element references within the component.\n// See \"Element References\" above.\nthis.$manyRefs\n\n// Options defined for the component.\nthis.$opts\n\n// The registered name of the component, usually kebab-case.\nthis.$name\n\n// Emit a custom event from this component.\n// Will be bubbled up from the dom element this is registered on, \n// as a custom event with the name `<elementName>-<eventName>`,\n// with the provided data in the event detail.\nthis.$emit(eventName, data = {})\n```\n\n## Global JavaScript Helpers\n\nThere are various global helper libraries in BookStack which can be accessed via the `window`. The below provides an overview of what's available. \n\n```js\n// HTTP service\n// Relative URLs will be resolved against the instance BASE_URL\nwindow.$http.get(url, params);\nwindow.$http.post(url, data);\nwindow.$http.put(url, data);\nwindow.$http.delete(url, data);\nwindow.$http.patch(url, data);\n\n// Global event system\n// Emit a global event\nwindow.$events.emit(eventName, eventData);\n// Listen to a global event\nwindow.$events.listen(eventName, callback);\n// Show a success message\nwindow.$events.success(message);\n// Show an error message\nwindow.$events.error(message);\n// Show validation errors, if existing, as an error notification\nwindow.$events.showValidationErrors(error);\n\n// Translator\n// Take the given plural text and count to decide on what plural option\n// to use, Similar to laravel's trans_choice function but instead\n// takes the translation text directly instead of a translation key.\nwindow.$trans.choice(translationString, count, replacements);\n\n// Component System\n// Parse and initialise any components from the given root el down.\nwindow.$components.init(rootEl);\n// Register component models to be used by the component system.\n// Takes a mapping of classes/constructors keyed by component names.\n// Names will be converted to kebab-case.\nwindow.$components.register(mapping);\n// Get the first active component of the given name.\nwindow.$components.first(name);\n// Get all the active components of the given name. \nwindow.$components.get(name);\n// Get the first active component of the given name that's been\n// created on the given element.\nwindow.$components.firstOnElement(element, name);\n```\n\n## Public Events\n\nThere are a range of available events that are emitted as part of a public & supported API for accessing or extending JavaScript libraries & components used in the system.\n\nDetails on these events can be found in the [JavaScript Public Events file](javascript-public-events.md).\n\n## WYSIWYG Editor API\n\nDetails on the API for our custom-built WYSIWYG editor can be found in the [WYSIWYG JavaScript API file](./wysiwyg-js-api.md)."
  },
  {
    "path": "dev/docs/javascript-public-events.md",
    "content": "# JavaScript Public Events\n\nThere are a range of available events emitted as part of a public & [supported](#support) API for accessing or extending JavaScript libraries and components used in the system.\nThese are emitted via standard DOM events so can be consumed using standard DOM APIs like so:\n\n```javascript\nwindow.addEventListener('event-name', event => {\n   const eventData = event.detail; \n});\n```\n\nSuch events are typically emitted from a DOM element relevant to event, which then bubbles up.\nFor most use-cases you can probably just listen on the `window` as shown above.\n\n## Support\n\nThis event system, and the events emitted, are considered semi-supported.\nBreaking changes of the event API, event names, or event properties, are possible but will be documented in update notes.\nThe detail provided within the events, and the libraries made accessible, are not considered supported nor stable, and changes to these won't be clearly documented changelogs.\n\n## Event Naming Scheme\n\nEvents are typically named in the following format:\n\n```text\n<context>::<action/lifecycle>\n\n# Examples:\neditor-tinymce::setup\nlibrary-cm6::configure-theme\n```\n\nIf the event is generic in use but specific to a library, the `<context>` will start with `library-` followed by the library name. Otherwise `<context>` may reflect the UI context/component.\n\nThe `<action/lifecycle>` reflects the lifecycle stage of the context, or a specific action to perform if the event is specific to a certain use-case.\n\n## Event Listing\n\n### `editor-markdown-cm6::pre-init`\n\nThis event is called before the markdown input editor CodeMirror instance is created or loaded.\n\n#### Event Data\n\n- `editorViewConfig` - An [EditorViewConfig](https://codemirror.net/docs/ref/#view.EditorViewConfig) object that will eventually be passed when creating the CodeMirror EditorView instance.\n\n##### Example\n\n```javascript\n// Always load the editor with specific pre-defined content if empty\nwindow.addEventListener('editor-markdown-cm6::pre-init', event => {\n    const config = event.detail.editorViewConfig;\n    config.doc = config.doc || \"Start this page with a nice story\";\n});\n```\n\n### `editor-markdown::setup`\n\nThis event is called when the markdown editor loads, post configuration but before the editor is ready to use.\n\n#### Event Data\n\n- `markdownIt` - A reference to the [MarkdownIt](https://markdown-it.github.io/markdown-it/#MarkdownIt) instance used to render markdown to HTML (Just for the preview).\n- `displayEl` - The IFrame Element that wraps the HTML preview display.\n- `cmEditorView` - The CodeMirror [EditorView](https://codemirror.net/docs/ref/#view.EditorView) instance used for the markdown input editor.\n\n##### Example\n\n```javascript\n// Set all text in the display to be red by default.\nwindow.addEventListener('editor-markdown::setup', event => {\n    const display = event.detail.displayEl;\n    display.contentDocument.body.style.color = 'red';\n});\n```\n\n### `editor-drawio::configure`\n\nThis event is called as the embedded diagrams.net drawing editor loads, to allow configuration of the diagrams.net interface.\nSee [this diagrams.net page](https://www.diagrams.net/doc/faq/configure-diagram-editor) for details on the available options for the configure event.\n\nIf using a custom diagrams.net instance, via the `DRAWIO` option, you will need to ensure your DRAWIO option URL has the `configure=1` query parameter.\n\n#### Event Data\n\n- `config` - The configuration object that will be passed to diagrams.net.\n  - This will likely be empty by default, but modify this object in-place as needed with your desired options.\n\n##### Example\n\n```javascript\n// Set only the \"general\" and \"android\" libraries to show by default\nwindow.addEventListener('editor-drawio::configure', event => {\n    const config = event.detail.config;\n    config.enabledLibraries = [\"general\", \"android\"];\n});\n```\n\n### `editor-tinymce::pre-init`\n\nThis event is called before the TinyMCE editor, used as the BookStack WYSIWYG page editor, is initialised.\n\n#### Event Data\n\n- `config` - Object containing the configuration that's going to be passed to [tinymce.init](https://www.tiny.cloud/docs/api/tinymce/root_tinymce/#init).\n\n##### Example\n\n```javascript\n// Removed \"bold\" from the editor toolbar\nwindow.addEventListener('editor-tinymce::pre-init', event => {\n    const tinyConfig = event.detail.config;\n    tinyConfig.toolbar = tinyConfig.toolbar.replace('bold ', '');\n});\n```\n\n### `editor-tinymce::setup`\n\nThis event is called during the `setup` lifecycle stage of the TinyMCE editor used as the BookStack WYSIWYG editor. This is after configuration, but before the editor is fully loaded and ready to use. \n\n##### Event Data\n\n- `editor` - The [tinymce.Editor](https://www.tiny.cloud/docs/api/tinymce/tinymce.editor/) instance used for the WYSIWYG editor.\n\n##### Example\n\n```javascript\n// Replaces the editor content with redacted message 3 seconds after load.\nwindow.addEventListener('editor-tinymce::setup', event => {\n    const editor = event.detail.editor;\n    setTimeout(() => {\n        editor.setContent('REDACTED!');\n    }, 3000);\n});\n```\n\n### `editor-wysiwyg::post-init`\n\nThis is called after the (new custom-built Lexical-based) WYSIWYG editor has been initialised.\n\n#### Event Data\n\n- `usage` - A string label to identify the usage type of the WYSIWYG editor in BookStack.\n- `api` - An instance to the WYSIWYG editor API, as documented in the [WYSIWYG JavaScript API file](./wysiwyg-js-api.md).\n\n##### Example\n\nThe below example shows how you'd use this API to create a button, with that button added to the main toolbar of the page editor, which inserts bold \"Hello!\" text on press:\n\n<details>\n<summary>Show Example</summary>\n\n```javascript\nwindow.addEventListener('editor-wysiwyg::post-init', event => {\n    const {usage, api} = event.detail;\n    // Check that it's the page editor which is being loaded\n    if (usage !== 'page-editor') {\n        return;\n    }\n    \n    // Create a custom button which inserts bold hello text on press\n    const button = api.ui.createButton({\n        label: 'Greet',\n        action: () => {\n            api.content.insertHtml(`<strong>Hello!</strong>`);\n        }\n    });\n    \n    // Add the button to the start of the first section within the main toolbar\n    const toolbar = api.ui.getMainToolbar();\n    if (toolbar) {\n      toolbar.getSections()[0]?.addButton(button, 0);   \n    }\n});\n```\n</details>\n\n### `library-cm6::configure-theme`\n\nThis event is called whenever a CodeMirror instance is loaded, as a method to configure the theme used by CodeMirror. This applies to all CodeMirror instances including in-page code blocks, editors using in BookStack settings, and the Page markdown editor.\n\n#### Event Data\n\n- `darkModeActive` - A boolean to indicate if the current view/page is being loaded with dark mode active.\n- `registerViewTheme(builder)` - A method that can be called to register a new view (CodeMirror UI) theme.\n  - `builder` - A function that will return an object that will be passed into the CodeMirror [EditorView.theme()](https://codemirror.net/docs/ref/#view.EditorView^theme) function as a StyleSpec.\n- `registerHighlightStyle(builder)` - A method that can be called to register a new HighlightStyle (code highlighting) theme.\n  - `builder` - A function, that receives a reference to [Tag.tags](https://lezer.codemirror.net/docs/ref/#highlight.tags) and returns an array of [TagStyle](https://codemirror.net/docs/ref/#language.TagStyle) objects.\n\n##### Example\n\nThe below shows registering a custom \"Solarized dark\" editor and syntax theme:\n\n<details>\n<summary>Show Example</summary>\n\n```javascript\n// Theme data taken from:\n// https://github.com/craftzdog/cm6-themes/blob/main/packages/solarized-dark/src/index.ts\n// (MIT License) - Copyright (C) 2022 by Takuya Matsuyama and others\nconst base00 = '#002b36',\n    base01 = '#073642',\n    base02 = '#586e75',\n    base03 = '#657b83',\n    base04 = '#839496',\n    base05 = '#93a1a1',\n    base06 = '#eee8d5',\n    base07 = '#fdf6e3',\n    base_red = '#dc322f',\n    base_orange = '#cb4b16',\n    base_yellow = '#b58900',\n    base_green = '#859900',\n    base_cyan = '#2aa198',\n    base_blue = '#268bd2',\n    base_violet = '#6c71c4',\n    base_magenta = '#d33682'\n\nconst invalid = '#d30102',\n    stone = base04,\n    darkBackground = '#00252f',\n    highlightBackground = '#173541',\n    background = base00,\n    tooltipBackground = base01,\n    selection = '#173541',\n    cursor = base04\n\nfunction viewThemeBuilder() {\n    return {\n      '&':{color:base05,backgroundColor:background},\n      '.cm-content':{caretColor:cursor},\n      '.cm-cursor, .cm-dropCursor':{borderLeftColor:cursor},\n      '&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':{backgroundColor:selection},\n      '.cm-panels':{backgroundColor:darkBackground,color:base03},\n      '.cm-panels.cm-panels-top':{borderBottom:'2px solid black'},\n      '.cm-panels.cm-panels-bottom':{borderTop:'2px solid black'},\n      '.cm-searchMatch':{backgroundColor:'#72a1ff59',outline:'1px solid #457dff'},\n      '.cm-searchMatch.cm-searchMatch-selected':{backgroundColor:'#6199ff2f'},\n      '.cm-activeLine':{backgroundColor:highlightBackground},\n      '.cm-selectionMatch':{backgroundColor:'#aafe661a'},\n      '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket':{outline:`1px solid ${base06}`},\n      '.cm-gutters':{backgroundColor:darkBackground,color:stone,border:'none'},\n      '.cm-activeLineGutter':{backgroundColor:highlightBackground},\n      '.cm-foldPlaceholder':{backgroundColor:'transparent',border:'none',color:'#ddd'},\n      '.cm-tooltip':{border:'none',backgroundColor:tooltipBackground},\n      '.cm-tooltip .cm-tooltip-arrow:before':{borderTopColor:'transparent',borderBottomColor:'transparent'},\n      '.cm-tooltip .cm-tooltip-arrow:after':{borderTopColor:tooltipBackground,borderBottomColor:tooltipBackground},\n      '.cm-tooltip-autocomplete':{\n        '& > ul > li[aria-selected]':{backgroundColor:highlightBackground,color:base03}\n      }\n    };\n}\n\nfunction highlightStyleBuilder(t) {\n    return [{tag:t.keyword,color:base_green},\n      {tag:[t.name,t.deleted,t.character,t.propertyName,t.macroName],color:base_cyan},\n      {tag:[t.variableName],color:base05},\n      {tag:[t.function(t.variableName)],color:base_blue},\n      {tag:[t.labelName],color:base_magenta},\n      {tag:[t.color,t.constant(t.name),t.standard(t.name)],color:base_yellow},\n      {tag:[t.definition(t.name),t.separator],color:base_cyan},\n      {tag:[t.brace],color:base_magenta},\n      {tag:[t.annotation],color:invalid},\n      {tag:[t.number,t.changed,t.annotation,t.modifier,t.self,t.namespace],color:base_magenta},\n      {tag:[t.typeName,t.className],color:base_orange},\n      {tag:[t.operator,t.operatorKeyword],color:base_violet},\n      {tag:[t.tagName],color:base_blue},\n      {tag:[t.squareBracket],color:base_red},\n      {tag:[t.angleBracket],color:base02},\n      {tag:[t.attributeName],color:base05},\n      {tag:[t.regexp],color:invalid},\n      {tag:[t.quote],color:base_green},\n      {tag:[t.string],color:base_yellow},\n      {tag:t.link,color:base_cyan,textDecoration:'underline',textUnderlinePosition:'under'},\n      {tag:[t.url,t.escape,t.special(t.string)],color:base_yellow},\n      {tag:[t.meta],color:base_red},\n      {tag:[t.comment],color:base02,fontStyle:'italic'},\n      {tag:t.strong,fontWeight:'bold',color:base06},\n      {tag:t.emphasis,fontStyle:'italic',color:base_green},\n      {tag:t.strikethrough,textDecoration:'line-through'},\n      {tag:t.heading,fontWeight:'bold',color:base_yellow},\n      {tag:t.heading1,fontWeight:'bold',color:base07},\n      {tag:[t.heading2,t.heading3,t.heading4],fontWeight:'bold',color:base06},\n      {tag:[t.heading5,t.heading6],color:base06},\n      {tag:[t.atom,t.bool,t.special(t.variableName)],color:base_magenta},\n      {tag:[t.processingInstruction,t.inserted,t.contentSeparator],color:base_red},\n      {tag:[t.contentSeparator],color:base_yellow},\n      {tag:t.invalid,color:base02,borderBottom:`1px dotted ${base_red}`}];\n}\n\nwindow.addEventListener('library-cm6::configure-theme', event => {\n    const detail = event.detail;\n    detail.registerViewTheme(viewThemeBuilder);\n    detail.registerHighlightStyle(highlightStyleBuilder);\n});\n```\n</details>\n\n### `library-cm6::pre-init`\n\nThis event is called just before any CodeMirror instances are initialised so that the instance configuration can be viewed and altered before the instance is created.\n\n#### Event Data\n\n- `usage` - A string label to identify the usage type of the CodeMirror instance in BookStack.\n- `editorViewConfig` - A reference to the [EditorViewConfig](https://codemirror.net/docs/ref/#view.EditorViewConfig) that will be used to create the instance.\n- `libEditorView` - The CodeMirror [EditorView](https://codemirror.net/docs/ref/#view.EditorView) class object, provided for convenience.\n- `libEditorState` - The CodeMirror [EditorState](https://codemirror.net/docs/ref/#state.EditorState) class object, provided for convenience.\n- `libCompartment` - The CodeMirror [Compartment](https://codemirror.net/docs/ref/#state.Compartment) class object, provided for convenience.\n\n##### Example\n\nThe below shows how you'd enable the built-in line wrapping extension for page content code blocks: \n\n<details>\n<summary>Show Example</summary>\n\n```javascript\nwindow.addEventListener('library-cm6::pre-init', event => {\n    const detail = event.detail;\n    const config = detail.editorViewConfig;\n    const EditorView = detail.libEditorView;\n    \n    if (detail.usage === 'content-code-block') {\n        config.extensions.push(EditorView.lineWrapping);\n    }\n});\n```\n</details>\n\n### `library-cm6::post-init`\n\nThis event is called just after any CodeMirror instances are initialised so that you're able to gain a reference to the CodeMirror EditorView instance.\n\n#### Event Data\n\n- `usage` - A string label to identify the usage type of the CodeMirror instance in BookStack.\n- `editorView` - A reference to the [EditorView](https://codemirror.net/docs/ref/#view.EditorView) instance that has been created.\n- `editorViewConfig` - A reference to the [EditorViewConfig](https://codemirror.net/docs/ref/#view.EditorViewConfig) that was used to create the instance.\n- `libEditorView` - The CodeMirror [EditorView](https://codemirror.net/docs/ref/#view.EditorView) class object, provided for convenience.\n- `libEditorState` - The CodeMirror [EditorState](https://codemirror.net/docs/ref/#state.EditorState) class object, provided for convenience.\n- `libCompartment` - The CodeMirror [Compartment](https://codemirror.net/docs/ref/#state.Compartment) class object, provided for convenience.\n\n##### Example\n\nThe below example shows how you'd prepend some default text to all content (page) code blocks.\n\n<details>\n<summary>Show Example</summary>\n\n```javascript\nwindow.addEventListener('library-cm6::post-init', event => {\n    const detail = event.detail;\n    const editorView = detail.editorView;\n    \n    if (detail.usage === 'content-code-block') {\n        editorView.dispatch({\n          changes: {from: 0, to: 0, insert: 'Copyright 2023\\n\\n'}\n        });\n    }\n});\n```\n</details>\n"
  },
  {
    "path": "dev/docs/logical-theme-system.md",
    "content": "# Logical Theme System\n\nBookStack allows logical customization via the theme system which enables you to add, or extend, functionality within the PHP side of the system without needing to alter the core application files.\n\nThis is part of the theme system alongside the [visual theme system](./visual-theme-system.md).\n\n**Note:** This system is considered semi-stable. The `Theme::` system is kept maintained but specific customizations or deeper app/framework usage using this system will not be supported nor considered in any way stable. Customizations using this system should be checked after updates.\n\n## Getting Started\n\n*[Video Guide](https://www.youtube.com/watch?v=YVbpm_35crQ)*\n\nThis makes use of the theme system. Create a folder for your theme within your BookStack `themes` directory. As an example we'll use `my_theme`, so we'd create a `themes/my_theme` folder.\nYou'll need to tell BookStack to use your theme via the `APP_THEME` option in your `.env` file. For example: `APP_THEME=my_theme`.\n\nWithin your theme folder create a `functions.php` file. BookStack will look for this and run it during app boot-up. Within this file you can use the `Theme` facade API, described below, to hook into certain app events.\n\n## `Theme` Facade API\n\nBelow details the public methods of the `Theme` facade that are considered stable:\n\n### `Theme::listen`\n\nThis method listens to a system event and runs the given action when that event occurs. The arguments passed to the action depend on the event. Event names are exposed as static properties on the `\\BookStack\\Theming\\ThemeEvents` class. \n\nIt is possible to listen to a single event using multiple actions. When dispatched, BookStack will loop over and run each action for that event.\nIf an action returns a non-null value then BookStack will stop cycling through actions at that point and make use of the non-null return value if possible (Depending on the event).\n\n**Arguments**\n- string $event\n- callable $action\n\n**Example**\n\n```php\nTheme::listen(\n    \\BookStack\\Theming\\ThemeEvents::AUTH_LOGIN,\n    function($service, $user) {\n        \\Log::info(\"Login by {$user->name} via {$service}\");\n    }\n);\n```\n\n### `Theme::addSocialDriver`\n\nThis method allows you to register a custom social authentication driver within the system. This is primarily intended to use with [Socialite Providers](https://socialiteproviders.com/).\n\n**Arguments**\n- string $driverName\n- array $config\n- string $socialiteHandler\n- callable|null $configureForRedirect = null\n\n**Example**\n\n*See \"Custom Socialite Service Example\" below.*\n\n### `Theme::registerCommand`\n\nThis method allows you to register a custom command which can then be used via the artisan console.\n\n**Arguments**\n- string $driverName\n- array $config\n- string $socialiteHandler\n\n**Example**\n\n*See \"Custom Command Registration Example\" below for a more detailed example.*\n\n```php\nTheme::registerCommand(new SayHelloCommand());\n```\n\n## Available Events\n\nAll available events dispatched by BookStack are exposed as static properties on the `\\BookStack\\Theming\\ThemeEvents` class, which can be found within the file `app/Theming/ThemeEvents.php` relative to your root BookStack folder. Alternatively, the events for the latest release can be [seen on GitHub here](https://github.com/BookStackApp/BookStack/blob/release/app/Theming/ThemeEvents.php).\n\nThe comments above each constant with the `ThemeEvents.php` file describe the dispatch conditions of the event, in addition to the arguments the action will receive. The comments may also describe any ways the return value of the action may be used. \n\n## Example `functions.php` file\n\n```php\n<?php\n\nuse BookStack\\Facades\\Theme;\nuse BookStack\\Theming\\ThemeEvents;\n\n// Logs custom message on user login\nTheme::listen(ThemeEvents::AUTH_LOGIN, function($method, $user) {\n    Log::info(\"Login via {$method} for {$user->name}\");\n});\n\n// Adds a `/info` public URL endpoint that emits php debug details\nTheme::listen(ThemeEvents::APP_BOOT, function($app) {\n    \\Route::get('info', function() {\n        phpinfo(); // Don't do this on a production instance!\n    });\n});\n```\n\n## Custom View Registration Example\n\nUsing the logical theme system, you can register custom views to be rendered before/after other existing views, providing a flexible way to add content without needing to override and/or replicate existing content. This is done by listening to the `THEME_REGISTER_VIEWS`.\n\n**Note:** You don't need to use this to override existing views, or register whole new main views to use, since that's done automatically based on their existence. This is just for advanced capabilities like inserting before/after existing views.\n\nThis event provides a `ThemeViews` instance which has the following methods made available:\n\n- `renderBefore(string $targetView, string $localView, int $priority)`\n- `renderAfter(string $targetView, string $localView, int $priority)`\n\nThe target view is the name of that which we want to insert our custom view relative to.\nThe local view is the name of the view we want to add and render.\nThe priority provides a suggestion to the ordering of view display, with lower numbers being shown first. This defaults to 50 if not provided.\n\nHere's an example of this in use:\n\n```php\n<?php\n\nuse BookStack\\Facades\\Theme;\nuse BookStack\\Theming\\ThemeEvents;\nuse BookStack\\Theming\\ThemeViews;\n\nTheme::listen(ThemeEvents::THEME_REGISTER_VIEWS, function (ThemeViews $themeViews) {\n    $themeViews->renderBefore('layouts.parts.header', 'welcome-banner', 4);\n    $themeViews->renderAfter('layouts.parts.header', 'information-alert');\n    $themeViews->renderAfter('layouts.parts.header', 'additions.password-notice', 20);\n});\n```\n\nIn this example, we're inserting custom views before and after the main header bar.\nBookStack will look for a `welcome-banner.blade.php` file within our theme folder (or a theme module view folder) to render before the header. It'll look for the `information-alert.blade.php` and `additions/password-notice.blade.php` views to render afterwards.\nThe password notice will be shown above the information alert view, since it has a specified priority of 20, whereas the information alert view would default to a priority of 50.\n\n## Custom Command Registration Example\n\nThe logical theme system supports adding custom [artisan commands](https://laravel.com/docs/8.x/artisan) to BookStack.\nThese can be registered in your `functions.php` file by calling `Theme::registerCommand($command)`, where `$command` is an instance of `\\Symfony\\Component\\Console\\Command\\Command`. \n\nBelow is an example of registering a command that could then be ran using `php artisan bookstack:meow` on the command line.\n\n```php\n<?php\n\nuse BookStack\\Facades\\Theme;\nuse Illuminate\\Console\\Command;\n\nclass MeowCommand extends Command\n{\n    protected $signature = 'bookstack:meow';\n    protected $description = 'Say meow on the command line';\n\n    public function handle()\n    {\n        $this->line('Meow there!');\n    }\n}\n\nTheme::registerCommand(new MeowCommand);\n```\n\n## Custom Socialite Service Example\n\nThe below shows an example of adding a custom reddit socialite service to BookStack. \nBookStack exposes a helper function for this via `Theme::addSocialDriver` which sets the required config and event listeners in the platform.\n\nThe require statements reference composer installed dependencies within the theme folder. They are required manually since they are not auto-loaded like other app files due to being outside the main BookStack dependency list. \n\n```php\nrequire \"vendor/socialiteproviders/reddit/Provider.php\";\nrequire \"vendor/socialiteproviders/reddit/RedditExtendSocialite.php\";\n\nTheme::listen(ThemeEvents::APP_BOOT, function($app) {\n    Theme::addSocialDriver('reddit', [\n        'client_id' => 'abc123',\n        'client_secret' => 'def456789',\n        'name' => 'Reddit',\n    ], '\\SocialiteProviders\\Reddit\\RedditExtendSocialite@handle');\n});\n```\n\nIn some cases you may need to customize the driver before it performs a redirect. \nThis can be done by providing a callback as a fourth parameter like so:\n\n```php\nTheme::addSocialDriver('reddit', [\n    'client_id' => 'abc123',\n    'client_secret' => 'def456789',\n    'name' => 'Reddit',\n], '\\SocialiteProviders\\Reddit\\RedditExtendSocialite@handle', function($driver) {\n    $driver->with(['prompt' => 'select_account']);\n    $driver->scopes(['open_id']);\n});\n```"
  },
  {
    "path": "dev/docs/permission-scenario-testing.md",
    "content": "# Permission Scenario Testing\n\nDue to complexity that can arise in the various combinations of permissions, this document details scenarios and their expected results.\n\nTest cases are written ability abstract, since all abilities should act the same in theory. Functional test cases may test abilities separate due to implementation differences.\n\nTests are categorised by the most specific element involved in the scenario, where the below list is most specific to least:\n\n- Role entity permissions.\n- Fallback entity permissions.\n- Role permissions.\n\n## General Permission Logical Rules\n\nThe below are some general rules we follow to standardise the behaviour of permissions in the platform:\n\n- Most specific permission application (as above) take priority and can deny less specific permissions.\n- Parent role entity permissions that may be inherited, are considered to essentially be applied on the item they are inherited to unless a lower level has its own permission rule for an already specific role.\n- Where both grant and deny exist at the same specificity, we side towards grant.\n\n## Cases\n\n### Content Role Permissions\n\nThese are tests related to item/entity permissions that are set only at a role level.\n\n#### test_01_allow\n\n- Role A has role all-page permission.\n- User has Role A.\n\nUser granted page permission.\n\n#### test_02_deny\n\n- Role A has no page permission.\n- User has Role A.\n\nUser denied page permission.\n\n#### test_10_allow_on_own_with_own\n\n- Role A has role own-page permission.\n- User has Role A.\n- User is owner of page.\n\nUser granted page permission.\n\n#### test_11_deny_on_other_with_own\n\n- Role A has role own-page permission.\n- User has Role A.\n- User is not owner of page.\n\nUser denied page permission.\n\n#### test_20_multiple_role_conflicting_all\n\n- Role A has role all-page permission.\n- Role B has no page permission.\n- User has Role A & B.\n\nUser granted page permission.\n\n#### test_21_multiple_role_conflicting_own\n\n- Role A has role own-page permission.\n- Role B has no page permission.\n- User has Role A & B.\n- User is owner of page.\n\nUser granted page permission.\n\n---\n\n### Entity Role Permissions\n\nThese are tests related to entity-level role-specific permission overrides.\n\n#### test_01_explicit_allow\n\n- Page permissions have inherit disabled.\n- Role A has entity allow page permission.\n- User has Role A.\n\nUser granted page permission.\n\n#### test_02_explicit_deny\n\n- Page permissions have inherit disabled.\n- Role A has entity deny page permission.\n- User has Role A.\n\nUser denied page permission.\n\n#### test_03_same_level_conflicting\n\n- Page permissions have inherit disabled.\n- Role A has entity allow page permission.\n- Role B has entity deny page permission.\n- User has both Role A & B.\n\nUser granted page permission. \nExplicit grant overrides entity deny at same level.\n\n#### test_20_inherit_allow\n\n- Page permissions have inherit enabled.\n- Chapter permissions has inherit disabled.\n- Role A has entity allow chapter permission.\n- User has Role A.\n\nUser granted page permission.\n\n#### test_21_inherit_deny\n\n- Page permissions have inherit enabled.\n- Chapter permissions has inherit disabled.\n- Role A has entity deny chapter permission.\n- User has Role A.\n\nUser denied page permission.\n\n#### test_22_same_level_conflict_inherit \n\n- Page permissions have inherit enabled.\n- Chapter permissions has inherit disabled.\n- Role A has entity deny chapter permission.\n- Role B has entity allow chapter permission.\n- User has both Role A & B.\n\nUser granted page permission.\n\n#### test_30_child_inherit_override_allow\n\n- Page permissions have inherit enabled.\n- Chapter permissions has inherit disabled.\n- Role A has entity deny chapter permission.\n- Role A has entity allow page permission.\n- User has Role A.\n\nUser granted page permission.\n\n#### test_31_child_inherit_override_deny\n\n- Page permissions have inherit enabled.\n- Chapter permissions has inherit disabled.\n- Role A has entity allow chapter permission.\n- Role A has entity deny page permission.\n- User has Role A.\n\nUser denied page permission.\n\n#### test_40_multi_role_inherit_conflict_override_deny\n\n- Page permissions have inherit enabled.\n- Chapter permissions has inherit disabled.\n- Role A has entity deny page permission.\n- Role B has entity allow chapter permission.\n- User has Role A & B.\n\nUser granted page permission.\n\n#### test_41_multi_role_inherit_conflict_retain_allow\n\n- Page permissions have inherit enabled.\n- Chapter permissions has inherit disabled.\n- Role A has entity allow page permission.\n- Role B has entity deny chapter permission.\n- User has Role A & B.\n\nUser granted page permission.\n\n#### test_50_role_override_allow\n\n- Page permissions have inherit enabled.\n- Role A has no page role permission.\n- Role A has entity allow page permission.\n- User has Role A.\n\nUser granted page permission.\n\n#### test_51_role_override_deny\n\n- Page permissions have inherit enabled.\n- Role A has no page-view-all role permission.\n- Role A has entity deny page permission.\n- User has Role A.\n\nUser denied page permission.\n\n#### test_60_inherited_role_override_allow\n\n- Page permissions have inherit enabled.\n- Chapter permissions have inherit enabled.\n- Role A has no page role permission.\n- Role A has entity allow chapter permission.\n- User has Role A.\n\nUser granted page permission.\n\n#### test_61_inherited_role_override_deny\n\n- Page permissions have inherit enabled.\n- Chapter permissions have inherit enabled.\n- Role A has page role permission.\n- Role A has entity denied chapter permission.\n- User has Role A.\n\nUser denied page permission.\n\n#### test_62_inherited_role_override_deny_on_own\n\n- Page permissions have inherit enabled.\n- Chapter permissions have inherit enabled.\n- Role A has own-page role permission.\n- Role A has entity denied chapter permission.\n- User has Role A.\n- User owns Page.\n\nUser denied page permission.\n\n#### test_70_multi_role_inheriting_deny\n\n- Page permissions have inherit enabled.\n- Role A has all page role permission.\n- Role B has entity denied page permission.\n- User has Role A and B.\n\nUser denied page permission.\n\n#### test_71_multi_role_inheriting_deny_on_own\n\n- Page permissions have inherit enabled.\n- Role A has own page role permission.\n- Role B has entity denied page permission.\n- User has Role A and B.\n- Use owns Page.\n\nUser denied page permission.\n\n#### test_75_multi_role_inherited_deny_via_parent\n\n- Page permissions have inherit enabled.\n- Chapter permissions have inherit enabled.\n- Role A has all-pages role permission.\n- Role B has entity denied chapter permission.\n- User has Role A & B.\n\nUser denied page permission.\n\n#### test_76_multi_role_inherited_deny_via_parent_on_own\n\n- Page permissions have inherit enabled.\n- Chapter permissions have inherit enabled.\n- Role A has own page role permission.\n- Role B has entity denied chapter permission.\n- User has Role A & B.\n\nUser denied page permission.\n\n#### test_80_fallback_override_allow\n\n- Page permissions have inherit disabled.\n- Page fallback has entity deny permission.\n- Role A has entity allow page permission.\n- User has Role A.\n\nUser granted page permission.\n\n#### test_81_fallback_override_deny\n\n- Page permissions have inherit disabled.\n- Page fallback has entity allow permission.\n- Role A has entity deny page permission.\n- User has Role A.\n\nUser denied page permission.\n\n#### test_84_fallback_override_allow_multi_role\n\n- Page permissions have inherit disabled.\n- Page fallback has entity deny permission.\n- Role A has entity allow page permission.\n- Role B has no entity page permissions.\n- User has Role A & B.\n\nUser granted page permission.\n\n#### test_85_fallback_override_deny_multi_role\n\n- Page permissions have inherit disabled.\n- Page fallback has entity allow permission.\n- Role A has entity deny page permission.\n- Role B has no entity page permissions.\n- User has Role A & B.\n\nUser denied page permission.\n\n#### test_86_fallback_override_allow_inherit\n\n- Chapter permissions have inherit disabled.\n- Page permissions have inherit enabled.\n- Chapter fallback has entity deny permission.\n- Role A has entity allow chapter permission.\n- User has Role A.\n\nUser granted page permission.\n\n#### test_87_fallback_override_deny_inherit\n\n- Chapter permissions have inherit disabled.\n- Page permissions have inherit enabled.\n- Chapter fallback has entity allow permission.\n- Role A has entity deny chapter permission.\n- User has Role A.\n\nUser denied page permission.\n\n#### test_88_fallback_override_allow_multi_role_inherit\n\n- Chapter permissions have inherit disabled.\n- Page permissions have inherit enabled.\n- Chapter fallback has entity deny permission.\n- Role A has entity allow chapter permission.\n- Role B has no entity chapter permissions.\n- User has Role A & B.\n\nUser granted page permission.\n\n#### test_89_fallback_override_deny_multi_role_inherit\n\n- Chapter permissions have inherit disabled.\n- Page permissions have inherit enabled.\n- Chapter fallback has entity allow permission.\n- Role A has entity deny chapter permission.\n- Role B has no entity chapter permissions.\n- User has Role A & B.\n\nUser denied page permission.\n\n#### test_90_fallback_overrides_parent_entity_role_deny\n\n- Chapter permissions have inherit disabled.\n- Page permissions have inherit disabled.\n- Chapter fallback has entity deny permission.\n- Page fallback has entity deny permission.\n- Role A has entity allow chapter permission.\n- User has Role A.\n\nUser denied page permission.\n\n#### test_91_fallback_overrides_parent_entity_role_inherit\n\n- Book permissions have inherit disabled.\n- Chapter permissions have inherit disabled. \n- Page permissions have inherit enabled.\n- Book fallback has entity deny permission.\n- Chapter fallback has entity deny permission.\n- Role A has entity allow book permission.\n- User has Role A.\n\nUser denied page permission."
  },
  {
    "path": "dev/docs/php-testing.md",
    "content": "# BookStack PHP Testing\n\nBookStack has many test cases defined within the `tests/` directory of the app. These are built upon [PHPUnit](https://phpunit.de/) along with Laravel's own test framework additions, and a bunch of custom helper classes.\n\n## Setup\n\nThe application tests are mostly functional, rather than unit tests, meaning they simulate user actions and system components, and therefore these require use of the database. To avoid potential conflicts within your development environment, the tests use a separate database. This is defined via a specific `mysql_testing` database connection in our configuration, and expects to use the following database access details:\n\n- Host: `127.0.0.1`\n- Username: `bookstack-test`\n- Password: `bookstack-test`\n- Database: `bookstack-test`\n\nYou will need to create a database, with access for these credentials, to allow the system to connect when running tests. Alternatively, if those don't suit, you can define a `TEST_DATABASE_URL` option in your `.env` file, or environment, with connection details like so:\n\n```bash\nTEST_DATABASE_URL=\"mysql://username:password@host-name:port/database-name\"\n```\n\nThe testing database will need migrating and seeding with test data beforehand. This can be done by running `composer refresh-test-database`.\n\n## Running Tests\n\nYou can run all tests via composer with `composer test` in the application root directory.\nAlternatively, you can run PHPUnit directly with `php vendor/bin/phpunit`.\n\nSome editors, like PHPStorm, have in-built support for running tests on a per file, directory or class basis.\nOtherwise, you can run PHPUnit with specified tests and/or filter to limit the tests ran:\n\n```bash\n# Run all test in the \"./tests/HomepageTest.php\" file\nphp vendor/bin/phpunit ./tests/HomepageTest.php\n\n# Run all test in the \"./tests/User\" directory\nphp vendor/bin/phpunit ./tests/User\n\n# Filter to a particular test method name\nphp vendor/bin/phpunit --filter test_default_homepage_visible\n\n# Filter to a particular test class name\nphp vendor/bin/phpunit --filter HomepageTest\n```\n\nIf the codebase needs to be tested with deprecations, this can be done via uncommenting the relevant line within the `TestCase@setUp` function.  This is not expected for most PRs to the project, but instead used for maintenance tasks like dependency & PHP upgrades.\n\n## Writing Tests\n\nTo understand how tests are written & used, it's advised you read through existing test cases similar to what you need to write. Tests are written in a rather scrappy manner, compared to the core app codebase, which is fine and expected since there's often hoops to jump through for various functionality. Scrappy tests are better than no tests.\n\nTest classes have to be within the `tests/` folder, and be named ending in `Test`. These should always extend the `Tests\\TestCase` class.\nTest methods should be written in snake_case, start with `test_`, and be public methods.\n\nHere are some general rules & patterns we follow in the tests:\n\n- All external remote system resources, like HTTP calls and LDAP connections, are mocked.\n- We prefer to hard-code expected text & URLs to better detect potential changes in the system rather than use dynamic references. This provides higher sensitivity to changes, and has never been much of a maintenance issue.\n- Only test with an admin user if needed, otherwise keep to less privileged users to ensure permission systems are active and exercised within tests.\n- If testing for the lack of something (e.g. `$this->assertDontSee('TextAfterChange')`) then this should be accompanied by some form of positive confirmation (e.g. `$this->assertSee('TextBeforeChange')`).\n\n### Test Helpers\n\nOur default `TestCase` is bloated with helpers to assist in testing scenarios. Some of these shown below, but you should jump through and explore these in your IDE/editor to explore their full capabilities and options:\n\n```php\n// Run the test as a logged-in-user at a certain privilege level\n$this->asAdmin();\n$this->asEditor();\n$this->asViewer();\n\n// Provides a bunch of entity (shelf/book/chapter/page) content and actions \n$this->entities;\n\n// Provides various user & role abilities\n$this->users;\n\n// Provides many helpful actions relate to system & content permissions\n$this->permissions;\n\n// Provides a range of methods for dealing with files & uploads in tests\n$this->files;\n\n// Parse HTML of a response to assert HTML-based conditions\n// Uses https://github.com/ssddanbrown/asserthtml library.\n$this->withHtml($resp);\n// Example:\n$this->withHtml($this->get('/'))->assertElementContains('p[id=\"top\"]', 'Hello!');\n```"
  },
  {
    "path": "dev/docs/portable-zip-file-format.md",
    "content": "# Portable ZIP File Format\n\nBookStack provides exports in a \"Portable ZIP\" which allows the portable transfer, storage, import & export of BookStack content.\nThis document details the format used, and is intended for our own internal development use in addition to detailing the format for potential external use-cases (readers, apps, import for other platforms etc...).\n\n**Note:** This is not a BookStack backup format! This format misses much of the data that would be needed to re-create/restore a BookStack instance. There are existing better alternative options for this use-case.\n\n## Stability\n\nFollowing the goals & ideals of BookStack, stability is very important. We aim for this defined format to be stable and forwards compatible, to prevent breakages in use-case due to changes. Here are the general rules we follow in regard to stability & changes:\n\n- New features & properties may be added with any release.\n- Where reasonably possible, we will attempt to avoid modifications/removals of existing features/properties.\n- Where potentially breaking changes do have to be made, these will be noted in BookStack release/update notes.\n\nThe addition of new features/properties alone are not considered as a breaking change to the format. Breaking changes are considered as such where they could impact common/expected use of the existing properties and features we document, they are not considered based upon user assumptions or any possible breakage.\nFor example if your application, using the format, breaks because we added a new property while you hard-coded your application to use the third property (instead of a property name), then that's on you.\n\n## Format Outline\n\nThe format is intended to be very simple, readable and based on open standards that could be easily read/handled in most common programming languages.\nThe below outlines the structure of the format:\n\n- **ZIP archive container**\n   - **data.json** - Export data.\n   - **files/** - Directory containing referenced files.\n     - *file-a*\n     - *file-b*\n     - *...*\n\n## References\n\nSome properties in the export data JSON are indicated as `String reference`, and these are direct references to a file name within the `files/` directory of the ZIP. For example, the below book cover is directly referencing a `files/4a5m4a.jpg` within the ZIP which would be expected to exist.\n\n```json\n{\n  \"book\": {\n    \"cover\": \"4a5m4a.jpg\"\n  }\n}\n```\n\nWithin HTML and markdown content, you may require references across to other items within the export content.\nThis can be done using the following format:\n\n```\n[[bsexport:<object>:<reference>]]\n```\n\nReferences are to the `id` for data objects.\nHere's an example of each type of such reference that could be used:\n\n```\n[[bsexport:image:22]]\n[[bsexport:attachment:55]]\n[[bsexport:page:40]]\n[[bsexport:chapter:2]]\n[[bsexport:book:8]]\n```\n\n## HTML & Markdown Content\n\nBookStack commonly stores & utilises content in the HTML format.\nProperties that expect or provided HTML will either be named `html` or contain `html` in the property name.\nWhile BookStack supports a range of HTML, not all HTML content will be supported by BookStack and be assured to work as desired across all BookStack features.\nThe HTML supported by BookStack is not yet formally documented, but you can inspect to what the WYSIWYG editor produces as a basis.\nGenerally, top-level elements should keep to common block formats (p, blockquote, h1, h2 etc...) with no nesting or custom structure apart from common inline elements.\nSome areas of BookStack where HTML is used, like book & chapter descriptions, will strictly limit/filter HTML tag & attributes to an allow-list.\n\nFor markdown content, in BookStack we target [the commonmark spec](https://commonmark.org/) with the addition of tables & task-lists.\nHTML within markdown is supported but not all HTML is assured to work as advised above.\n\n### Content Security\n\nIf you're consuming HTML or markdown within an export please consider that the content is not assured to be safe, even if provided directly by a BookStack instance. It's best to treat such content as potentially unsafe.\nBy default, BookStack performs some basic filtering to remove scripts among other potentially dangerous elements but this is not foolproof. BookStack itself relies on additional security mechanisms such as [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) to help prevent a range of exploits.\n\n## Export Data - `data.json`\n\nThe `data.json` file is a JSON format file which contains all structured data for the export. The properties are as follows:\n\n- `instance` - [Instance](#instance) Object, optional, details of the export source instance.\n- `exported_at` - String, optional, full ISO 8601 datetime of when the export was created.\n- `book` - [Book](#book) Object, optional, book export data.\n- `chapter` - [Chapter](#chapter) Object, optional, chapter export data.\n- `page` - [Page](#page) Object, optional, page export data.\n\nEither `book`, `chapter` or `page` will exist depending on export type. You'd want to check for each to check what kind of export this is, and if it's an export you can handle. It's possible that other options are added in the future (`books` for a range of books for example) so it'd be wise to specifically check for properties that can be handled, otherwise error to indicate lack of support.\n\n## Data Objects\n\nThe below details the objects & their properties used in Application Data.\n\n#### Instance\n\nThese details are informational regarding the exporting BookStack instance from where an export was created from.\n\n- `id` - String, required, unique identifier for the BookStack instance.\n- `version` - String, required, BookStack version of the export source instance.\n\n#### Book\n\n- `id` - Number, optional, original ID for the book from exported system.\n- `name` - String, required, name/title of the book.\n- `description_html` - String, optional, HTML description content.\n- `cover` - String reference, optional, reference to book cover image.\n- `chapters` - [Chapter](#chapter) array, optional, chapters within this book.\n- `pages` - [Page](#page) array, optional, direct child pages for this book.\n- `tags` - [Tag](#tag) array, optional, tags assigned to this book.\n\nThe `pages` are not all pages within the book, just those that are direct children (not in a chapter). To build an ordered mixed chapter/page list for the book, as what you'd see in BookStack, you'd need to combine `chapters` and `pages` together and sort by their `priority` value (low to high).\n\n#### Chapter\n\n- `id` - Number, optional, original ID for the chapter from exported system.\n- `name` - String, required, name/title of the chapter.\n- `description_html` - String, optional, HTML description content.\n- `priority` - Number, optional, integer order for when shown within a book (shown low to high).\n- `pages` - [Page](#page) array, optional, pages within this chapter.\n- `tags` - [Tag](#tag) array, optional, tags assigned to this chapter.\n\n#### Page\n\n- `id` - Number, optional, original ID for the page from exported system.\n- `name` - String, required, name/title of the page.\n- `html` - String, optional, page HTML content.\n- `markdown` - String, optional, user markdown content for this page.\n- `priority` - Number, optional, integer order for when shown within a book (shown low to high).\n- `attachments` - [Attachment](#attachment) array, optional, attachments uploaded to this page.\n- `images` - [Image](#image) array, optional, images used in this page.\n- `tags` - [Tag](#tag) array, optional, tags assigned to this page.\n\nTo define the page content, either `markdown` or `html` should be provided. Ideally these should be limited to the range of markdown and HTML which BookStack supports. See the [\"HTML & Markdown Content\"](#html--markdown-content) section.\n\nThe page editor type, and edit content will be determined by what content is provided. If non-empty `markdown` is provided, the page will be assumed as a markdown editor page (where permissions allow) and the HTML will be rendered from the markdown content. Otherwise, the provided `html` will be used as editor & display content.\n\n#### Image\n\n- `id` - Number, optional, original ID for the page from exported system.\n- `name` - String, required, name of image.\n- `file` - String reference, required, reference to image file.\n- `type` - String, required, must be 'gallery' or 'drawio'\n\nFile must be an image type accepted by BookStack (png, jpg, gif, webp).\nImages of type 'drawio' are expected to be png with draw.io drawing data\nembedded within it.\n\n#### Attachment\n\n- `id` - Number, optional, original ID for the attachment from exported system.\n- `name` - String, required, name of attachment.\n- `link` - String, semi-optional, URL of attachment.\n- `file` - String reference, semi-optional, reference to attachment file.\n\nEither `link` or `file` must be present, as that will determine the type of attachment. \n\n#### Tag\n\n- `name` - String, required, name of the tag.\n- `value` - String, optional, value of the tag (can be empty)."
  },
  {
    "path": "dev/docs/release-process.md",
    "content": "# Release Versioning & Process\n\n### BookStack Version Number Scheme\n\nBookStack releases are each assigned a date-based version number in the format `v<year>.<month>[.<optional_patch_number>]`. For example:\n\n- `v20.12` - New feature released launched during December 2020.\n- `v21.06.2` - Second patch release upon the June 2021 feature release.\n\nPatch releases are generally fairly minor, primarily intended for fixes and therefore are fairly unlikely to cause breakages upon update.\nFeature releases are generally larger, bringing new features in addition to fixes and enhancements. These releases have a greater chance of introducing breaking changes upon update, so it's worth checking for any notes in the [update guide](https://www.bookstackapp.com/docs/admin/updates/).\n\n### Release Planning Process\n\nEach BookStack release will have a [milestone](https://github.com/BookStackApp/BookStack/milestones) created with issues & pull requests assigned to it to define what will be in that release. Milestones are built up then worked through until complete at which point, after some testing and documentation updates, the release will be deployed.\n\n### Release Announcements\n\nFeature releases, and some patch releases, will be accompanied by a post on the [BookStack blog](https://www.bookstackapp.com/blog/) which will provide additional detail on features, changes & updates otherwise the [GitHub release page](https://github.com/BookStackApp/BookStack/releases) will show a list of changes. You can sign up to be alerted to new BookStack blog posts (once per week maximum) [at this link](https://updates.bookstackapp.com/signup/bookstack-news-and-updates).\n\n### Release Technical Process\n\nDeploying a release, at a high level, simply involves merging the development branch into the release branch before then building & committing any release-only assets.\nA helper script [can be found in our](https://github.com/BookStackApp/devops/blob/main/meta-scripts/bookstack-release-steps) devops repo which provides the steps and commands for deploying a new release. "
  },
  {
    "path": "dev/docs/theme-system-modules.md",
    "content": "# Theme System Modules\n\nA theme system module is a collection of customizations using the [visual](visual-theme-system.md) and [logical](logical-theme-system.md) theme systems, provided along with some metadata, that can be installed alongside other modules within a theme. They can effectively be thought of as \"plugins\" or \"extensions\" that can be applied in addition to any customizations in the active theme.\n\n### Module Location\n\nModules are contained within a folder themselves, which should be located inside a `modules` folder within a [BookStack theme folder](visual-theme-system.md#getting-started). \nAs an example, starting from the `themes/` top-level folder of a BookStack instance:\n\n```txt\nthemes\n└── my-theme\n    └── modules\n        ├── module-a\n        │   └── bookstack-module.json\n        └── module-b\n            └── bookstack-module.json\n```\n\n### Module Format\n\nA module exists as a folder in the location [as detailed above](#module-location).\nThe content within the module folder should then follow this format:\n\n- `bookstack-module.json` - REQUIRED - A JSON file containing [the metadata](#module-json-metadata) for the module.\n- `functions.php` - OPTIONAL - A PHP file containing code for the [logical theme system](logical-theme-system.md).\n- `head/` - OPTIONAL - A folder containing HTML files which will be included into the HTML head of app-views.\n- `icons/` - OPTIONAL - A folder containing any icons to use as per [the visual theme system](visual-theme-system.md#customizing-icons).\n- `lang/` - OPTIONAL - A folder containing any language files to use as per [the visual theme system](visual-theme-system.md#customizing-text-content).\n- `public/` - OPTIONAL - A folder containing any files to expose into public web-space as per [the visual theme system](visual-theme-system.md#publicly-accessible-files).\n- `views/` - OPTIONAL - A folder containing any view additions or overrides as per [the visual theme system](visual-theme-system.md#customizing-view-files).\n\nYou can create additional directories/files for your own needs within the module, but ideally name them something unique to prevent conflicts with the above structure.\n\n### Module JSON Metadata\n\nModules are required to have a `bookstack-module.json` file in the top level directory of the module.\nThis must be a JSON file with the following properties:\n\n- `name` - string - An (ideally unique) name for the module.\n- `description` - string - A short description of the module.\n- `version` - string - A string version number generally following [semantic versioning](https://semver.org/).\n  - Examples: `v0.4.0`, `4.3.12`,  `v0.1.0-beta4`.\n\n### Customization Order/Precedence\n\nIt's possible that multiple modules may override/customize the same content.\nRight now, there's no assurance in regard to the order in which modules may be loaded.\nGenerally they will be used/searched in order of their module folder name, but this is not assured and should not be relied upon.\n\nIt's also possible that modules customize the same content as the configured theme.\nIn this scenario, the theme takes precedence. Modules are designed to be more portable and instance abstract, whereas the theme folder would typically be specific to the instance. \nThis allows the theme to be used to customize or override module content for the BookStack instance, without altering the module code itself.\n\n### Module Best Practices\n\nHere are some general best practices when it comes to creating modules:\n\n- Use a unique name and clear description so the user can understand the purpose of the module.\n- Increment the metadata version on change, keeping to [semver](https://semver.org/) to indicate compatibility of new versions.\n- Where possible, prefer to [insert views before/after](logical-theme-system.md#custom-view-registration-example) instead of overriding existing views, to reduce likelihood of conflicts or update troubles.\n- When using/registering custom views, use some level of unique namespacing within the view path to prevent potential conflicts with other customizations.\n  - For example, I may store a view within my module as `views/my-module-name-welcome.blade.php`, to be registered as 'my-module-name-welcome'.\n  - This is important since views may be resolved from other modules or the active theme, which may/will override your module level view.\n\n### Distribution Format\n\nModules are expected to be distributed as a compressed ZIP file, where the ZIP contents follow that of a module folder.\nBookStack provides a `php artisan bookstack:install-module` command which allows modules to be installed from these ZIP files, either from a local path or from a web URL.\nCurrently, there's a hardcoded total filesize limit of 50MB for module contents installed via this method.\n\nThere is not yet any direct update mechanism for modules, although this is something we may introduce in the future."
  },
  {
    "path": "dev/docs/visual-theme-system.md",
    "content": "# Visual Theme System\n\nBookStack allows visual customization via the theme system which enables you to extensively customize views, translation text & icons.\n\nThis is part of the theme system alongside the [logical theme system](./logical-theme-system.md).\n\n**Note:** This theme system itself is maintained and supported, but usages of this system, including the files you are able to override, are not considered stable and may change upon any update. You should test any customizations made after updates.\n\n## Getting Started\n\n*[Video Guide](https://foss.video/w/ibNY6bGmKFV1tva3Jz4KfA)*\n\nThis makes use of the theme system. Create a folder for your theme within your BookStack `themes` directory. As an example we'll use `my_theme`, so we'd create a `themes/my_theme` folder.\nYou'll need to tell BookStack to use your theme via the `APP_THEME` option in your `.env` file. For example: `APP_THEME=my_theme`.\n\n## Customizing View Files\n\nContent placed in your `themes/<theme_name>/` folder will override the original view files found in the `resources/views` folder. These files are typically [Laravel Blade](https://laravel.com/docs/10.x/blade) files.\nAs an example, I could override the `resources/views/books/parts/list-item.blade.php` file with my own template at the path `themes/<theme_name>/books/parts/list-item.blade.php`. \n\nIn addition to overriding original views, this could be used to add new views for use via the [logical theme system](logical-theme-system.md).\nBy using the `THEME_REGISTER_VIEWS` logical event, you can also register your views to be rendered before/after existing views. An example of this can be found in our [logical theme guidance](logical-theme-system.md#custom-view-registration-example).\n\n## Customizing Icons\n\nSVG files placed in a `themes/<theme_name>/icons` folder will override any icons of the same name within `resources/icons`. You'd typically want to follow the format convention of the existing icons, where no XML deceleration is included and no width & height attributes are set, to ensure optimal compatibility. \n\n## Customizing Text Content\n\nFolders with PHP translation files placed in a `themes/<theme_name>/lang` folder will override translations defined within the `lang` folder. Custom translations are merged with the original files, so you only need to override the select translations you want to override, you don't need to copy the whole original file. Note that you'll need to include the language folder.\n\nAs an example, Say I wanted to change 'Search' to 'Find'; Within a `themes/<theme_name>/lang/en/common.php` file I'd set the following:\n\n```php\n<?php\nreturn [\n    'search' => 'find',\n];\n```\n\n## Publicly Accessible Files\n\nAs part of deeper customizations you may want to expose additional files \n(images, scripts, styles, etc...) as part of your theme, in a way so they're\naccessible in public web-space to browsers.\n\nTo achieve this, you can put files within a `themes/<theme_name>/public` folder.\nBookStack will serve any files within this folder from a `/theme/<theme_name>` base path.\n\nAs an example, if I had an image located at `themes/custom/public/cat.jpg`, I could access\nthat image via the URL path `/theme/custom/cat.jpg`. That's assuming that `custom` is the currently\nconfigured application theme.\n\nThere are some considerations to these publicly served files:\n\n- Only a predetermined range of \"web safe\" content-types are currently served.\n  - This limits running into potential insecure scenarios in serving problematic file types.\n- A static 1-day cache time it set on files served from this folder.\n  - You can use alternative cache-breaking techniques (change of query string) upon changes if needed. \n  - If required, you could likely override caching at the webserver level.  \n"
  },
  {
    "path": "dev/docs/wysiwyg-js-api.md",
    "content": "# WYSIWYG JavaScript API\n\n**Warning: This API is currently in development and may change without notice.**\n\nFeedback is very much welcomed via this issue: https://github.com/BookStackApp/BookStack/issues/5937\n\nThis document covers the JavaScript API for the (newer Lexical-based) WYSIWYG editor.\nThis API is built and designed to abstract the internals of the editor away\nto provide a stable interface for performing common customizations.\n\nOnly the methods and properties documented here are guaranteed to be stable **once this API\nis out of initial development**.\nOther elements may be accessible but are not designed to be used directly, and therefore may change\nwithout notice.\nStable parts of the API may still change where needed, but such changes would be noted as part of BookStack update advisories.\n\nThe methods shown here are documented using standard TypeScript notation.\n\n## Overview\n\nThe API is provided as an object, which itself provides a number of modules\nvia its properties:\n\n- `ui` - Provides methods related to the UI of the editor, like buttons and toolbars.\n- `content` - Provides methods related to the live user content being edited upon.\n\nEach of these modules, and the relevant types used within, are documented in detail below.\n\nThe API object itself is provided via the [editor-wysiwyg::post-init](./javascript-public-events.md#editor-wysiwygpost-init)\nJavaScript public event, so you can access it like so:\n\n```javascript\nwindow.addEventListener('editor-wysiwyg::post-init', event => {\n    const {api} = event.detail;\n});\n```\n\n---\n\n## UI Module\n\nThis module provides methods related to the UI of the editor, like buttons and toolbars.\n\n### Methods\n\n#### createButton(options: object): EditorApiButton\n\nCreates a new button which can be used by other methods.\nThis takes an option object with the following properties:\n\n- `label` - string, optional - Used for the button text if no icon provided, or the button tooltip if an icon is provided.\n- `icon` - string, optional - The icon to use for the button. Expected to be an SVG string.\n- `action` - callback, required - The action to perform when the button is clicked.\n\nThe function returns an [EditorApiButton](#editorapibutton) object.\n\n**Example**\n\n```javascript\nconst button = api.ui.createButton({\n    label: 'Warn',\n    icon: '<svg>...</svg>',\n    action: () => {\n        window.alert('You clicked the button!');\n    }\n});\n```\n\n### getMainToolbar(): EditorApiToolbar\n\nGet the main editor toolbar. This is typically the toolbar at the top of the editor.\nThe function returns an [EditorApiToolbar](#editorapitoolbar) object, or null if no toolbar is found.\n\n**Example**\n\n```javascript\nconst toolbar = api.ui.getMainToolbar();\nconst sections = toolbar?.getSections() || [];\nif (sections.length > 0) {\n    sections[0].addButton(button);\n}\n```\n\n### Types\n\nThese are types which may be provided from UI module methods.\n\n#### EditorApiButton\n\nRepresents a button created via the `createButton` method.\nThis has the following methods:\n\n- `setActive(isActive: boolean): void` - Sets whether the button should be in an active state or not (typically active buttons appear as pressed).\n\n#### EditorApiToolbar\n\nRepresents a toolbar within the editor. This is a bar typically containing sets of buttons.\nThis has the following methods:\n\n- `getSections(): EditorApiToolbarSection[]` - Provides the main [EditorApiToolbarSections](#editorapitoolbarsection) contained within this toolbar.\n\n#### EditorApiToolbarSection\n\nRepresents a section of the main editor toolbar, which contains a set of buttons.\nThis has the following methods:\n\n- `getLabel(): string` - Provides the string label of the section.\n- `addButton(button: EditorApiButton, targetIndex: number = -1): void` - Adds a button to the section.\n  - By default, this will append the button, although a target index can be provided to insert at a specific position.\n\n---\n\n## Content Module\n\nThis module provides methods related to the live user content being edited within the editor.\n\n### Methods\n\n#### insertHtml(html: string, position: string = 'selection'): void\n\nInserts the given HTML string at the given position string.\nThe position, if not provided, will default to `'selection'`, replacing any existing selected content (or inserting at the selection if there's no active selection range).\nValid position string values are: `selection`, `start` and `end`. `start` & `end` are relative to the whole editor document.\nThe HTML is not assured to be added to the editor exactly as provided, since it will be parsed and serialised to fit the editor's internal known model format. Different parts of the HTML content may be handled differently depending on if it's block or inline content.\n\nThe function does not return anything.\n\n**Example**\n\n```javascript\n// Basic insert at selection\napi.content.insertHtml('<p>Hello <strong>world</strong>!</p>');\n\n// Insert at the start of the editor content\napi.content.insertHtml('<p>I\\'m at the start!</p>', 'start');\n```"
  },
  {
    "path": "dev/licensing/gen-js-licenses",
    "content": "#!/usr/bin/env php\n<?php\n\n// This script reads the project composer.lock file to generate\n// clear license details for our PHP dependencies.\n\ndeclare(strict_types=1);\nrequire \"gen-licenses-shared.php\";\n\n$rootPath = dirname(__DIR__, 2);\n$outputPath = \"{$rootPath}/dev/licensing/js-library-licenses.txt\";\n$outputSeparator = \"\\n-----------\\n\";\n\n$packages = [\n    ...glob(\"{$rootPath}/node_modules/*/package.json\"),\n    ...glob(\"{$rootPath}/node_modules/@*/*/package.json\"),\n];\n\n$packageOutput = array_map(packageToOutput(...), $packages);\n\n$licenseInfo = implode($outputSeparator, $packageOutput) . \"\\n\";\nfile_put_contents($outputPath, $licenseInfo);\n\necho \"License information written to {$outputPath}\\n\";\necho implode(\"\\n\", getWarnings()) . \"\\n\";\n\nfunction packageToOutput(string $packagePath): string\n{\n    global $rootPath;\n    $package = json_decode(file_get_contents($packagePath));\n    $output = [\"{$package->name}\"];\n\n    $license = $package->license ?? '';\n    if ($license) {\n        $output[] = \"License: {$license}\";\n    } else {\n        warn(\"Package {$package->name}: No license found\");\n    }\n\n    $licenseFile = findLicenseFile($package->name, $packagePath);\n    if ($licenseFile) {\n        $relLicenseFile = str_replace(\"{$rootPath}/\", '', $licenseFile);\n        $output[] = \"License File: {$relLicenseFile}\";\n        $copyright = findCopyright($licenseFile);\n        if ($copyright) {\n            $output[] = \"Copyright: {$copyright}\";\n        } else {\n            warn(\"Package {$package->name}: no copyright found in its license\");\n        }\n    }\n\n    $source = $package->repository->url ?? $package->repository ?? '';\n    if ($source) {\n        $output[] = \"Source: {$source}\";\n    }\n\n    $link = $package->homepage ?? $source;\n    if ($link) {\n        $output[] = \"Link: {$link}\";\n    }\n\n    return implode(\"\\n\", $output);\n}\n"
  },
  {
    "path": "dev/licensing/gen-licenses-shared.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n$warnings = [];\n\nfunction findLicenseFile(string $packageName, string $packagePath): string\n{\n    $licenseNameOptions = [\n        'license', 'LICENSE', 'License',\n        'license.*', 'LICENSE.*', 'License.*',\n        'license-*.*', 'LICENSE-*.*', 'License-*.*',\n    ];\n    $packageDir = dirname($packagePath);\n\n    $foundLicenses = [];\n    foreach ($licenseNameOptions as $option) {\n        $search = glob(\"{$packageDir}/$option\");\n        array_push($foundLicenses, ...$search);\n    }\n\n    if (count($foundLicenses) > 1) {\n        warn(\"Package {$packageName}: more than one license file found\");\n    }\n\n    if (count($foundLicenses) > 0) {\n        $fileName = basename($foundLicenses[0]);\n        return \"{$packageDir}/{$fileName}\";\n    }\n\n    warn(\"Package {$packageName}: no license files found\");\n    return '';\n}\n\nfunction findCopyright(string $licenseFile): string\n{\n    $fileContents = file_get_contents($licenseFile);\n    $pattern = '/^.*?copyright (\\(c\\)|\\d{4})[\\s\\S]*?(\\n\\n|\\.\\n)/mi';\n    $matches = [];\n    preg_match($pattern, $fileContents, $matches);\n    $copyright = trim($matches[0] ?? '');\n\n    if (str_contains($copyright, 'i.e.')) {\n        return '';\n    }\n\n    $emailPattern = '/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})/i';\n    return preg_replace_callback($emailPattern, obfuscateEmail(...), $copyright);\n}\n\nfunction obfuscateEmail(array $matches): string\n{\n    return preg_replace('/[^@.]/', '*', $matches[1]);\n}\n\nfunction warn(string $text): void\n{\n    global $warnings;\n    $warnings[] = \"WARN:\" . $text;\n}\n\nfunction getWarnings(): array\n{\n    global $warnings;\n    return $warnings;\n}\n"
  },
  {
    "path": "dev/licensing/gen-php-licenses",
    "content": "#!/usr/bin/env php\n<?php\n\n// This script reads the project composer.lock file to generate\n// clear license details for our PHP dependencies.\n\ndeclare(strict_types=1);\nrequire \"gen-licenses-shared.php\";\n\n$rootPath = dirname(__DIR__, 2);\n$outputPath = \"{$rootPath}/dev/licensing/php-library-licenses.txt\";\n$composerLock = json_decode(file_get_contents($rootPath . \"/composer.lock\"));\n$outputSeparator = \"\\n-----------\\n\";\n\n$packages = $composerLock->packages;\n$packageOutput = array_map(packageToOutput(...), $packages);\n\n$licenseInfo =  implode($outputSeparator, $packageOutput) . \"\\n\";\nfile_put_contents($outputPath, $licenseInfo);\n\necho \"License information written to {$outputPath}\\n\";\necho implode(\"\\n\", getWarnings()) . \"\\n\";\n\nfunction packageToOutput(stdClass $package) : string {\n    global $rootPath;\n    $output = [\"{$package->name}\"];\n\n    $licenses = is_array($package->license) ? $package->license : [$package->license];\n    $output[] = \"License: \" . implode(' ', $licenses);\n\n    $packagePath = \"{$rootPath}/vendor/{$package->name}/package.json\";\n    $licenseFile = findLicenseFile($package->name, $packagePath);\n    if ($licenseFile) {\n        $relLicenseFile = str_replace(\"{$rootPath}/\", '', $licenseFile);\n        $output[] = \"License File: {$relLicenseFile}\";\n        $copyright = findCopyright($licenseFile);\n        if ($copyright) {\n            $output[] = \"Copyright: {$copyright}\";\n        } else {\n            warn(\"Package {$package->name}: no copyright found in its license\");\n        }\n    }\n\n    $source = $package->source->url;\n    if ($source) {\n        $output[] = \"Source: {$source}\";\n    }\n\n    $link = $package->homepage ?? $package->source->url ?? '';\n    if ($link) {\n        $output[] = \"Link: {$link}\";\n    }\n\n    return implode(\"\\n\", $output);\n}"
  },
  {
    "path": "dev/licensing/js-library-licenses.txt",
    "content": "acorn-jsx\nLicense: MIT\nLicense File: node_modules/acorn-jsx/LICENSE\nCopyright: Copyright (C) 2012-2017 by Ingvar Stepanyan\nSource: https://github.com/acornjs/acorn-jsx\nLink: https://github.com/acornjs/acorn-jsx\n-----------\nacorn-walk\nLicense: MIT\nLicense File: node_modules/acorn-walk/LICENSE\nCopyright: Copyright (C) 2012-2020 by various contributors (see AUTHORS)\nSource: https://github.com/acornjs/acorn.git\nLink: https://github.com/acornjs/acorn\n-----------\nacorn\nLicense: MIT\nLicense File: node_modules/acorn/LICENSE\nCopyright: Copyright (C) 2012-2022 by various contributors (see AUTHORS)\nSource: git+https://github.com/acornjs/acorn.git\nLink: https://github.com/acornjs/acorn\n-----------\nagent-base\nLicense: MIT\nLicense File: node_modules/agent-base/LICENSE\nCopyright: Copyright (c) 2013 Nathan Rajlich <******@***********.***>\nSource: https://github.com/TooTallNate/proxy-agents.git\nLink: https://github.com/TooTallNate/proxy-agents.git\n-----------\najv\nLicense: MIT\nLicense File: node_modules/ajv/LICENSE\nCopyright: Copyright (c) 2015-2017 Evgeny Poberezkin\nSource: https://github.com/ajv-validator/ajv.git\nLink: https://github.com/ajv-validator/ajv\n-----------\nansi-escapes\nLicense: MIT\nLicense File: node_modules/ansi-escapes/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com)\nSource: sindresorhus/ansi-escapes\nLink: sindresorhus/ansi-escapes\n-----------\nansi-regex\nLicense: MIT\nLicense File: node_modules/ansi-regex/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com)\nSource: chalk/ansi-regex\nLink: chalk/ansi-regex\n-----------\nansi-styles\nLicense: MIT\nLicense File: node_modules/ansi-styles/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: chalk/ansi-styles\nLink: chalk/ansi-styles\n-----------\nanymatch\nLicense: ISC\nLicense File: node_modules/anymatch/LICENSE\nCopyright: Copyright (c) 2019 Elan Shanker, Paul Miller (https://paulmillr.com)\nSource: https://github.com/micromatch/anymatch\nLink: https://github.com/micromatch/anymatch\n-----------\narg\nLicense: MIT\nLicense File: node_modules/arg/LICENSE.md\nCopyright: Copyright (c) 2017-2019 Zeit, Inc.\nSource: zeit/arg\nLink: zeit/arg\n-----------\nargparse\nLicense: Python-2.0\nLicense File: node_modules/argparse/LICENSE\nSource: nodeca/argparse\nLink: nodeca/argparse\n-----------\narray-buffer-byte-length\nLicense: MIT\nLicense File: node_modules/array-buffer-byte-length/LICENSE\nCopyright: Copyright (c) 2023 Inspect JS\nSource: git+https://github.com/inspect-js/array-buffer-byte-length.git\nLink: https://github.com/inspect-js/array-buffer-byte-length#readme\n-----------\narray-includes\nLicense: MIT\nLicense File: node_modules/array-includes/LICENSE\nCopyright: Copyright (C) 2015 Jordan Harband\nSource: git://github.com/es-shims/array-includes.git\nLink: git://github.com/es-shims/array-includes.git\n-----------\narray.prototype.findlastindex\nLicense: MIT\nLicense File: node_modules/array.prototype.findlastindex/LICENSE\nCopyright: Copyright (c) 2021 ECMAScript Shims\nSource: git+https://github.com/es-shims/Array.prototype.findLastIndex.git\nLink: https://github.com/es-shims/Array.prototype.findLastIndex#readme\n-----------\narray.prototype.flat\nLicense: MIT\nLicense File: node_modules/array.prototype.flat/LICENSE\nCopyright: Copyright (c) 2017 ECMAScript Shims\nSource: git://github.com/es-shims/Array.prototype.flat.git\nLink: git://github.com/es-shims/Array.prototype.flat.git\n-----------\narray.prototype.flatmap\nLicense: MIT\nLicense File: node_modules/array.prototype.flatmap/LICENSE\nCopyright: Copyright (c) 2017 ECMAScript Shims\nSource: git://github.com/es-shims/Array.prototype.flatMap.git\nLink: git://github.com/es-shims/Array.prototype.flatMap.git\n-----------\narraybuffer.prototype.slice\nLicense: MIT\nLicense File: node_modules/arraybuffer.prototype.slice/LICENSE\nCopyright: Copyright (c) 2023 ECMAScript Shims\nSource: git+https://github.com/es-shims/ArrayBuffer.prototype.slice.git\nLink: https://github.com/es-shims/ArrayBuffer.prototype.slice#readme\n-----------\nasync-function\nLicense: MIT\nLicense File: node_modules/async-function/LICENSE\nCopyright: Copyright (c) 2016 EduardoRFS\nSource: git+https://github.com/ljharb/async-function.git\nLink: https://github.com/ljharb/async-function#readme\n-----------\navailable-typed-arrays\nLicense: MIT\nLicense File: node_modules/available-typed-arrays/LICENSE\nCopyright: Copyright (c) 2020 Inspect JS\nSource: git+https://github.com/inspect-js/available-typed-arrays.git\nLink: https://github.com/inspect-js/available-typed-arrays#readme\n-----------\nbabel-jest\nLicense: MIT\nLicense File: node_modules/babel-jest/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\nbabel-plugin-istanbul\nLicense: BSD-3-Clause\nLicense File: node_modules/babel-plugin-istanbul/LICENSE\nCopyright: Copyright (c) 2016, Istanbul Code Coverage\nAll rights reserved.\nSource: git+https://github.com/istanbuljs/babel-plugin-istanbul.git\nLink: https://github.com/istanbuljs/babel-plugin-istanbul#readme\n-----------\nbabel-plugin-jest-hoist\nLicense: MIT\nLicense File: node_modules/babel-plugin-jest-hoist/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\nbabel-preset-current-node-syntax\nLicense: MIT\nLicense File: node_modules/babel-preset-current-node-syntax/LICENSE\nCopyright: Copyright (c) 2020 Nicolò Ribaudo and other contributors\nSource: https://github.com/nicolo-ribaudo/babel-preset-current-node-syntax.git\nLink: https://github.com/nicolo-ribaudo/babel-preset-current-node-syntax.git\n-----------\nbabel-preset-jest\nLicense: MIT\nLicense File: node_modules/babel-preset-jest/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\nbalanced-match\nLicense: MIT\nLicense File: node_modules/balanced-match/LICENSE.md\nCopyright: Copyright (c) 2013 Julian Gruber &lt;******@************.***&gt;\nSource: git://github.com/juliangruber/balanced-match.git\nLink: https://github.com/juliangruber/balanced-match\n-----------\nbinary-extensions\nLicense: MIT\nLicense File: node_modules/binary-extensions/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com)\nCopyright (c) Paul Miller (https://paulmillr.com)\nSource: sindresorhus/binary-extensions\nLink: sindresorhus/binary-extensions\n-----------\nbrace-expansion\nLicense: MIT\nLicense File: node_modules/brace-expansion/LICENSE\nCopyright: Copyright (c) 2013 Julian Gruber <******@************.***>\nSource: git://github.com/juliangruber/brace-expansion.git\nLink: https://github.com/juliangruber/brace-expansion\n-----------\nbraces\nLicense: MIT\nLicense File: node_modules/braces/LICENSE\nCopyright: Copyright (c) 2014-present, Jon Schlinkert.\nSource: micromatch/braces\nLink: https://github.com/micromatch/braces\n-----------\nbrowserslist\nLicense: MIT\nLicense File: node_modules/browserslist/LICENSE\nCopyright: Copyright 2014 Andrey Sitnik <******@******.**> and other contributors\nSource: browserslist/browserslist\nLink: browserslist/browserslist\n-----------\nbs-logger\nLicense: MIT\nLicense File: node_modules/bs-logger/LICENSE\nCopyright: Copyright (c) 2018 Huafu Gandon\nSource: git+https://github.com/huafu/bs-logger.git\nLink: git+https://github.com/huafu/bs-logger.git\n-----------\nbser\nLicense: Apache-2.0\nSource: https://github.com/facebook/watchman\nLink: https://facebook.github.io/watchman/docs/bser.html\n-----------\nbuffer-from\nLicense: MIT\nLicense File: node_modules/buffer-from/LICENSE\nCopyright: Copyright (c) 2016, 2018 Linus Unnebäck\nSource: LinusU/buffer-from\nLink: LinusU/buffer-from\n-----------\ncall-bind-apply-helpers\nLicense: MIT\nLicense File: node_modules/call-bind-apply-helpers/LICENSE\nCopyright: Copyright (c) 2024 Jordan Harband\nSource: git+https://github.com/ljharb/call-bind-apply-helpers.git\nLink: https://github.com/ljharb/call-bind-apply-helpers#readme\n-----------\ncall-bind\nLicense: MIT\nLicense File: node_modules/call-bind/LICENSE\nCopyright: Copyright (c) 2020 Jordan Harband\nSource: git+https://github.com/ljharb/call-bind.git\nLink: https://github.com/ljharb/call-bind#readme\n-----------\ncall-bound\nLicense: MIT\nLicense File: node_modules/call-bound/LICENSE\nCopyright: Copyright (c) 2024 Jordan Harband\nSource: git+https://github.com/ljharb/call-bound.git\nLink: https://github.com/ljharb/call-bound#readme\n-----------\ncallsites\nLicense: MIT\nLicense File: node_modules/callsites/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/callsites\nLink: sindresorhus/callsites\n-----------\ncamelcase\nLicense: MIT\nLicense File: node_modules/camelcase/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/camelcase\nLink: sindresorhus/camelcase\n-----------\ncaniuse-lite\nLicense: CC-BY-4.0\nLicense File: node_modules/caniuse-lite/LICENSE\nSource: browserslist/caniuse-lite\nLink: browserslist/caniuse-lite\n-----------\nchalk\nLicense: MIT\nLicense File: node_modules/chalk/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: chalk/chalk\nLink: chalk/chalk\n-----------\nchar-regex\nLicense: MIT\nLicense File: node_modules/char-regex/LICENSE\nSource: https://github.com/Richienb/char-regex.git\nLink: https://github.com/Richienb/char-regex.git\n-----------\nchokidar-cli\nLicense: MIT\nLicense File: node_modules/chokidar-cli/LICENSE\nCopyright: Copyright (c) 2015 Kimmo Brunfeldt\nSource: https://github.com/open-npm-tools/chokidar-cli.git\nLink: https://github.com/open-npm-tools/chokidar-cli\n-----------\nchokidar\nLicense: MIT\nLicense File: node_modules/chokidar/LICENSE\nCopyright: Copyright (c) 2012-2019 Paul Miller (https://paulmillr.com), Elan Shanker\nSource: git+https://github.com/paulmillr/chokidar.git\nLink: https://github.com/paulmillr/chokidar\n-----------\nci-info\nLicense: MIT\nLicense File: node_modules/ci-info/LICENSE\nCopyright: Copyright (c) 2016 Thomas Watson Steen\nSource: github:watson/ci-info\nLink: https://github.com/watson/ci-info\n-----------\ncjs-module-lexer\nLicense: MIT\nLicense File: node_modules/cjs-module-lexer/LICENSE\nCopyright: Copyright (C) 2018-2020 Guy Bedford\nSource: git+https://github.com/nodejs/cjs-module-lexer.git\nLink: https://github.com/nodejs/cjs-module-lexer#readme\n-----------\ncliui\nLicense: ISC\nLicense File: node_modules/cliui/LICENSE.txt\nCopyright: Copyright (c) 2015, Contributors\nSource: http://github.com/yargs/cliui.git\nLink: http://github.com/yargs/cliui.git\n-----------\nco\nLicense: MIT\nLicense File: node_modules/co/LICENSE\nCopyright: Copyright (c) 2014 TJ Holowaychuk &lt;**@************.**&gt;\nSource: tj/co\nLink: tj/co\n-----------\ncodemirror\nLicense: MIT\nLicense File: node_modules/codemirror/LICENSE\nCopyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/codemirror/basic-setup.git\nLink: https://github.com/codemirror/basic-setup.git\n-----------\ncollect-v8-coverage\nLicense: MIT\nLicense File: node_modules/collect-v8-coverage/LICENSE\nCopyright: Copyright (c) 2019 Simen Bekkhus\nSource: SimenB/collect-v8-coverage\nLink: SimenB/collect-v8-coverage\n-----------\ncolor-convert\nLicense: MIT\nLicense File: node_modules/color-convert/LICENSE\nCopyright: Copyright (c) 2011-2016 Heather Arthur <**********@*****.***>\nSource: Qix-/color-convert\nLink: Qix-/color-convert\n-----------\ncolor-name\nLicense: MIT\nLicense File: node_modules/color-name/LICENSE\nSource: git@github.com:colorjs/color-name.git\nLink: https://github.com/colorjs/color-name\n-----------\nconcat-map\nLicense: MIT\nLicense File: node_modules/concat-map/LICENSE\nSource: git://github.com/substack/node-concat-map.git\nLink: git://github.com/substack/node-concat-map.git\n-----------\nconvert-source-map\nLicense: MIT\nLicense File: node_modules/convert-source-map/LICENSE\nCopyright: Copyright 2013 Thorsten Lorenz. \nAll rights reserved.\nSource: git://github.com/thlorenz/convert-source-map.git\nLink: https://github.com/thlorenz/convert-source-map\n-----------\ncreate-require\nLicense: MIT\nLicense File: node_modules/create-require/LICENSE\nCopyright: Copyright (c) 2020\nSource: nuxt-contrib/create-require\nLink: nuxt-contrib/create-require\n-----------\ncrelt\nLicense: MIT\nLicense File: node_modules/crelt/LICENSE\nCopyright: Copyright (C) 2020 by Marijn Haverbeke <******@*********.******>\nSource: git+https://github.com/marijnh/crelt.git\nLink: https://github.com/marijnh/crelt#readme\n-----------\ncross-spawn\nLicense: MIT\nLicense File: node_modules/cross-spawn/LICENSE\nCopyright: Copyright (c) 2018 Made With MOXY Lda <*****@****.******>\nSource: git@github.com:moxystudio/node-cross-spawn.git\nLink: https://github.com/moxystudio/node-cross-spawn\n-----------\ncssstyle\nLicense: MIT\nLicense File: node_modules/cssstyle/LICENSE\nCopyright: Copyright (c) Chad Walker\nSource: jsdom/cssstyle\nLink: https://github.com/jsdom/cssstyle\n-----------\ndata-urls\nLicense: MIT\nLicense File: node_modules/data-urls/LICENSE.txt\nSource: jsdom/data-urls\nLink: jsdom/data-urls\n-----------\ndata-view-buffer\nLicense: MIT\nLicense File: node_modules/data-view-buffer/LICENSE\nCopyright: Copyright (c) 2023 Jordan Harband\nSource: git+https://github.com/inspect-js/data-view-buffer.git\nLink: https://github.com/inspect-js/data-view-buffer#readme\n-----------\ndata-view-byte-length\nLicense: MIT\nLicense File: node_modules/data-view-byte-length/LICENSE\nCopyright: Copyright (c) 2024 Jordan Harband\nSource: git+https://github.com/inspect-js/data-view-byte-length.git\nLink: https://github.com/inspect-js/data-view-byte-length#readme\n-----------\ndata-view-byte-offset\nLicense: MIT\nLicense File: node_modules/data-view-byte-offset/LICENSE\nCopyright: Copyright (c) 2024 Jordan Harband\nSource: git+https://github.com/inspect-js/data-view-byte-offset.git\nLink: https://github.com/inspect-js/data-view-byte-offset#readme\n-----------\ndebug\nLicense: MIT\nLicense File: node_modules/debug/LICENSE\nCopyright: Copyright (c) 2014-2017 TJ Holowaychuk <**@************.**>\nCopyright (c) 2018-2021 Josh Junon\nSource: git://github.com/debug-js/debug.git\nLink: git://github.com/debug-js/debug.git\n-----------\ndecamelize\nLicense: MIT\nLicense File: node_modules/decamelize/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/decamelize\nLink: sindresorhus/decamelize\n-----------\ndecimal.js\nLicense: MIT\nSource: https://github.com/MikeMcl/decimal.js.git\nLink: https://github.com/MikeMcl/decimal.js.git\n-----------\ndedent\nLicense: MIT\nLicense File: node_modules/dedent/LICENSE.md\nSource: https://github.com/dmnd/dedent\nLink: https://github.com/dmnd/dedent\n-----------\ndeep-is\nLicense: MIT\nLicense File: node_modules/deep-is/LICENSE\nCopyright: Copyright (c) 2012, 2013 Thorsten Lorenz <********@***.**>\nCopyright (c) 2012 James Halliday <****@********.***>\nCopyright (c) 2009 Thomas Robinson <280north.com>\nSource: http://github.com/thlorenz/deep-is.git\nLink: http://github.com/thlorenz/deep-is.git\n-----------\ndeepmerge\nLicense: MIT\nLicense File: node_modules/deepmerge/license.txt\nCopyright: Copyright (c) 2012 James Halliday, Josh Duff, and other contributors\nSource: git://github.com/TehShrike/deepmerge.git\nLink: https://github.com/TehShrike/deepmerge\n-----------\ndefine-data-property\nLicense: MIT\nLicense File: node_modules/define-data-property/LICENSE\nCopyright: Copyright (c) 2023 Jordan Harband\nSource: git+https://github.com/ljharb/define-data-property.git\nLink: https://github.com/ljharb/define-data-property#readme\n-----------\ndefine-properties\nLicense: MIT\nLicense File: node_modules/define-properties/LICENSE\nCopyright: Copyright (C) 2015 Jordan Harband\nSource: git://github.com/ljharb/define-properties.git\nLink: git://github.com/ljharb/define-properties.git\n-----------\ndetect-libc\nLicense: Apache-2.0\nLicense File: node_modules/detect-libc/LICENSE\nSource: git://github.com/lovell/detect-libc\nLink: git://github.com/lovell/detect-libc\n-----------\ndetect-newline\nLicense: MIT\nLicense File: node_modules/detect-newline/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/detect-newline\nLink: sindresorhus/detect-newline\n-----------\ndiff\nLicense: BSD-3-Clause\nLicense File: node_modules/diff/LICENSE\nCopyright: Copyright (c) 2009-2015, Kevin Decker <********@*****.***>\nSource: git://github.com/kpdecker/jsdiff.git\nLink: git://github.com/kpdecker/jsdiff.git\n-----------\ndoctrine\nLicense: Apache-2.0\nLicense File: node_modules/doctrine/LICENSE\nSource: eslint/doctrine\nLink: https://github.com/eslint/doctrine\n-----------\ndunder-proto\nLicense: MIT\nLicense File: node_modules/dunder-proto/LICENSE\nCopyright: Copyright (c) 2024 ECMAScript Shims\nSource: git+https://github.com/es-shims/dunder-proto.git\nLink: https://github.com/es-shims/dunder-proto#readme\n-----------\neastasianwidth\nLicense: MIT\nSource: git://github.com/komagata/eastasianwidth.git\nLink: git://github.com/komagata/eastasianwidth.git\n-----------\nelectron-to-chromium\nLicense: ISC\nLicense File: node_modules/electron-to-chromium/LICENSE\nCopyright: Copyright 2018 Kilian Valkhof\nSource: https://github.com/kilian/electron-to-chromium/\nLink: https://github.com/kilian/electron-to-chromium/\n-----------\nemittery\nLicense: MIT\nLicense File: node_modules/emittery/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com)\nSource: sindresorhus/emittery\nLink: sindresorhus/emittery\n-----------\nemoji-regex\nLicense: MIT\nLicense File: node_modules/emoji-regex/LICENSE-MIT.txt\nSource: https://github.com/mathiasbynens/emoji-regex.git\nLink: https://mths.be/emoji-regex\n-----------\nentities\nLicense: BSD-2-Clause\nLicense File: node_modules/entities/LICENSE\nCopyright: Copyright (c) Felix Böhm\nAll rights reserved.\nSource: git://github.com/fb55/entities.git\nLink: git://github.com/fb55/entities.git\n-----------\nerror-ex\nLicense: MIT\nLicense File: node_modules/error-ex/LICENSE\nCopyright: Copyright (c) 2015 JD Ballard\nSource: qix-/node-error-ex\nLink: qix-/node-error-ex\n-----------\nes-abstract\nLicense: MIT\nLicense File: node_modules/es-abstract/LICENSE\nCopyright: Copyright (C) 2015 Jordan Harband\nSource: git://github.com/ljharb/es-abstract.git\nLink: git://github.com/ljharb/es-abstract.git\n-----------\nes-define-property\nLicense: MIT\nLicense File: node_modules/es-define-property/LICENSE\nCopyright: Copyright (c) 2024 Jordan Harband\nSource: git+https://github.com/ljharb/es-define-property.git\nLink: https://github.com/ljharb/es-define-property#readme\n-----------\nes-errors\nLicense: MIT\nLicense File: node_modules/es-errors/LICENSE\nCopyright: Copyright (c) 2024 Jordan Harband\nSource: git+https://github.com/ljharb/es-errors.git\nLink: https://github.com/ljharb/es-errors#readme\n-----------\nes-object-atoms\nLicense: MIT\nLicense File: node_modules/es-object-atoms/LICENSE\nCopyright: Copyright (c) 2024 Jordan Harband\nSource: git+https://github.com/ljharb/es-object-atoms.git\nLink: https://github.com/ljharb/es-object-atoms#readme\n-----------\nes-set-tostringtag\nLicense: MIT\nLicense File: node_modules/es-set-tostringtag/LICENSE\nCopyright: Copyright (c) 2022 ECMAScript Shims\nSource: git+https://github.com/es-shims/es-set-tostringtag.git\nLink: https://github.com/es-shims/es-set-tostringtag#readme\n-----------\nes-shim-unscopables\nLicense: MIT\nLicense File: node_modules/es-shim-unscopables/LICENSE\nCopyright: Copyright (c) 2022 Jordan Harband\nSource: git+https://github.com/ljharb/es-shim-unscopables.git\nLink: https://github.com/ljharb/es-shim-unscopables#readme\n-----------\nes-to-primitive\nLicense: MIT\nLicense File: node_modules/es-to-primitive/LICENSE\nCopyright: Copyright (c) 2015 Jordan Harband\nSource: git://github.com/ljharb/es-to-primitive.git\nLink: git://github.com/ljharb/es-to-primitive.git\n-----------\nesbuild\nLicense: MIT\nLicense File: node_modules/esbuild/LICENSE.md\nCopyright: Copyright (c) 2020 Evan Wallace\nSource: git+https://github.com/evanw/esbuild.git\nLink: git+https://github.com/evanw/esbuild.git\n-----------\nescalade\nLicense: MIT\nLicense File: node_modules/escalade/license\nCopyright: Copyright (c) Luke Edwards <****.*********@*****.***> (lukeed.com)\nSource: lukeed/escalade\nLink: lukeed/escalade\n-----------\nescape-string-regexp\nLicense: MIT\nLicense File: node_modules/escape-string-regexp/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com)\nSource: sindresorhus/escape-string-regexp\nLink: sindresorhus/escape-string-regexp\n-----------\neslint-import-resolver-node\nLicense: MIT\nLicense File: node_modules/eslint-import-resolver-node/LICENSE\nCopyright: Copyright (c) 2015 Ben Mosher\nSource: https://github.com/import-js/eslint-plugin-import\nLink: https://github.com/import-js/eslint-plugin-import\n-----------\neslint-module-utils\nLicense: MIT\nLicense File: node_modules/eslint-module-utils/LICENSE\nCopyright: Copyright (c) 2015 Ben Mosher\nSource: git+https://github.com/import-js/eslint-plugin-import.git\nLink: https://github.com/import-js/eslint-plugin-import#readme\n-----------\neslint-plugin-import\nLicense: MIT\nLicense File: node_modules/eslint-plugin-import/LICENSE\nCopyright: Copyright (c) 2015 Ben Mosher\nSource: https://github.com/import-js/eslint-plugin-import\nLink: https://github.com/import-js/eslint-plugin-import\n-----------\neslint-scope\nLicense: BSD-2-Clause\nLicense File: node_modules/eslint-scope/LICENSE\nCopyright: Copyright (C) 2012-2013 Yusuke Suzuki (twitter: @Constellation) and other contributors.\nSource: https://github.com/eslint/js.git\nLink: https://github.com/eslint/js/blob/main/packages/eslint-scope/README.md\n-----------\neslint-visitor-keys\nLicense: Apache-2.0\nLicense File: node_modules/eslint-visitor-keys/LICENSE\nSource: https://github.com/eslint/js.git\nLink: https://github.com/eslint/js/blob/main/packages/eslint-visitor-keys/README.md\n-----------\neslint\nLicense: MIT\nLicense File: node_modules/eslint/LICENSE\nSource: eslint/eslint\nLink: https://eslint.org\n-----------\nespree\nLicense: BSD-2-Clause\nLicense File: node_modules/espree/LICENSE\nCopyright: Copyright (c) Open JS Foundation\nAll rights reserved.\nSource: https://github.com/eslint/js.git\nLink: https://github.com/eslint/js/blob/main/packages/espree/README.md\n-----------\nesprima\nLicense: BSD-2-Clause\nLicense File: node_modules/esprima/LICENSE.BSD\nSource: https://github.com/jquery/esprima.git\nLink: http://esprima.org\n-----------\nesquery\nLicense: BSD-3-Clause\nLicense File: node_modules/esquery/license.txt\nCopyright: Copyright (c) 2013, Joel Feenstra\nAll rights reserved.\nSource: https://github.com/estools/esquery.git\nLink: https://github.com/estools/esquery/\n-----------\nesrecurse\nLicense: BSD-2-Clause\nSource: https://github.com/estools/esrecurse.git\nLink: https://github.com/estools/esrecurse\n-----------\nestraverse\nLicense: BSD-2-Clause\nLicense File: node_modules/estraverse/LICENSE.BSD\nSource: http://github.com/estools/estraverse.git\nLink: https://github.com/estools/estraverse\n-----------\nesutils\nLicense: BSD-2-Clause\nLicense File: node_modules/esutils/LICENSE.BSD\nSource: http://github.com/estools/esutils.git\nLink: https://github.com/estools/esutils\n-----------\nexeca\nLicense: MIT\nLicense File: node_modules/execa/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com)\nSource: sindresorhus/execa\nLink: sindresorhus/execa\n-----------\nexit-x\nLicense: MIT\nSource: git://github.com/gruntjs/node-exit-x.git\nLink: https://github.com/gruntjs/node-exit-x\n-----------\nexpect\nLicense: MIT\nLicense File: node_modules/expect/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\nfast-deep-equal\nLicense: MIT\nLicense File: node_modules/fast-deep-equal/LICENSE\nCopyright: Copyright (c) 2017 Evgeny Poberezkin\nSource: git+https://github.com/epoberezkin/fast-deep-equal.git\nLink: https://github.com/epoberezkin/fast-deep-equal#readme\n-----------\nfast-json-stable-stringify\nLicense: MIT\nLicense File: node_modules/fast-json-stable-stringify/LICENSE\nCopyright: Copyright (c) 2017 Evgeny Poberezkin\nCopyright (c) 2013 James Halliday\nSource: git://github.com/epoberezkin/fast-json-stable-stringify.git\nLink: https://github.com/epoberezkin/fast-json-stable-stringify\n-----------\nfast-levenshtein\nLicense: MIT\nLicense File: node_modules/fast-levenshtein/LICENSE.md\nCopyright: Copyright (c) 2013 [Ramesh Nair](http://www.hiddentao.com/)\nSource: https://github.com/hiddentao/fast-levenshtein.git\nLink: https://github.com/hiddentao/fast-levenshtein.git\n-----------\nfb-watchman\nLicense: Apache-2.0\nSource: git@github.com:facebook/watchman.git\nLink: https://facebook.github.io/watchman/\n-----------\nfile-entry-cache\nLicense: MIT\nLicense File: node_modules/file-entry-cache/LICENSE\nCopyright: Copyright (c) Roy Riojas & Jared Wray\nSource: jaredwray/file-entry-cache\nLink: jaredwray/file-entry-cache\n-----------\nfill-range\nLicense: MIT\nLicense File: node_modules/fill-range/LICENSE\nCopyright: Copyright (c) 2014-present, Jon Schlinkert.\nSource: jonschlinkert/fill-range\nLink: https://github.com/jonschlinkert/fill-range\n-----------\nfind-up\nLicense: MIT\nLicense File: node_modules/find-up/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com)\nSource: sindresorhus/find-up\nLink: sindresorhus/find-up\n-----------\nflat-cache\nLicense: MIT\nLicense File: node_modules/flat-cache/LICENSE\nCopyright: Copyright (c) Roy Riojas and Jared Wray\nSource: jaredwray/flat-cache\nLink: jaredwray/flat-cache\n-----------\nflatted\nLicense: ISC\nLicense File: node_modules/flatted/LICENSE\nCopyright: Copyright (c) 2018-2020, Andrea Giammarchi, @WebReflection\nSource: git+https://github.com/WebReflection/flatted.git\nLink: https://github.com/WebReflection/flatted#readme\n-----------\nfor-each\nLicense: MIT\nLicense File: node_modules/for-each/LICENSE\nCopyright: Copyright (c) 2012 Raynos.\nSource: https://github.com/Raynos/for-each.git\nLink: https://github.com/Raynos/for-each\n-----------\nforeground-child\nLicense: ISC\nLicense File: node_modules/foreground-child/LICENSE\nCopyright: Copyright (c) 2015-2023 Isaac Z. Schlueter and Contributors\nSource: git+https://github.com/tapjs/foreground-child.git\nLink: git+https://github.com/tapjs/foreground-child.git\n-----------\nfs.realpath\nLicense: ISC\nLicense File: node_modules/fs.realpath/LICENSE\nCopyright: Copyright (c) Isaac Z. Schlueter and Contributors\nSource: git+https://github.com/isaacs/fs.realpath.git\nLink: git+https://github.com/isaacs/fs.realpath.git\n-----------\nfunction-bind\nLicense: MIT\nLicense File: node_modules/function-bind/LICENSE\nCopyright: Copyright (c) 2013 Raynos.\nSource: https://github.com/Raynos/function-bind.git\nLink: https://github.com/Raynos/function-bind\n-----------\nfunction.prototype.name\nLicense: MIT\nLicense File: node_modules/function.prototype.name/LICENSE\nCopyright: Copyright (c) 2016 Jordan Harband\nSource: git://github.com/es-shims/Function.prototype.name.git\nLink: git://github.com/es-shims/Function.prototype.name.git\n-----------\nfunctions-have-names\nLicense: MIT\nLicense File: node_modules/functions-have-names/LICENSE\nCopyright: Copyright (c) 2019 Jordan Harband\nSource: git+https://github.com/inspect-js/functions-have-names.git\nLink: https://github.com/inspect-js/functions-have-names#readme\n-----------\ngensync\nLicense: MIT\nLicense File: node_modules/gensync/LICENSE\nCopyright: Copyright 2018 Logan Smyth <***********@*****.***>\nSource: https://github.com/loganfsmyth/gensync.git\nLink: https://github.com/loganfsmyth/gensync\n-----------\nget-caller-file\nLicense: ISC\nLicense File: node_modules/get-caller-file/LICENSE.md\nCopyright: Copyright 2018 Stefan Penner\nSource: git+https://github.com/stefanpenner/get-caller-file.git\nLink: https://github.com/stefanpenner/get-caller-file#readme\n-----------\nget-intrinsic\nLicense: MIT\nLicense File: node_modules/get-intrinsic/LICENSE\nCopyright: Copyright (c) 2020 Jordan Harband\nSource: git+https://github.com/ljharb/get-intrinsic.git\nLink: https://github.com/ljharb/get-intrinsic#readme\n-----------\nget-package-type\nLicense: MIT\nLicense File: node_modules/get-package-type/LICENSE\nCopyright: Copyright (c) 2020 CFWare, LLC\nSource: git+https://github.com/cfware/get-package-type.git\nLink: https://github.com/cfware/get-package-type#readme\n-----------\nget-proto\nLicense: MIT\nLicense File: node_modules/get-proto/LICENSE\nCopyright: Copyright (c) 2025 Jordan Harband\nSource: git+https://github.com/ljharb/get-proto.git\nLink: https://github.com/ljharb/get-proto#readme\n-----------\nget-stream\nLicense: MIT\nLicense File: node_modules/get-stream/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com)\nSource: sindresorhus/get-stream\nLink: sindresorhus/get-stream\n-----------\nget-symbol-description\nLicense: MIT\nLicense File: node_modules/get-symbol-description/LICENSE\nCopyright: Copyright (c) 2021 Inspect JS\nSource: git+https://github.com/inspect-js/get-symbol-description.git\nLink: https://github.com/inspect-js/get-symbol-description#readme\n-----------\nglob-parent\nLicense: ISC\nLicense File: node_modules/glob-parent/LICENSE\nCopyright: Copyright (c) 2015, 2019 Elan Shanker\nSource: gulpjs/glob-parent\nLink: gulpjs/glob-parent\n-----------\nglob\nLicense: ISC\nLicense File: node_modules/glob/LICENSE\nCopyright: Copyright (c) 2009-2023 Isaac Z. Schlueter and Contributors\nSource: git://github.com/isaacs/node-glob.git\nLink: git://github.com/isaacs/node-glob.git\n-----------\nglobals\nLicense: MIT\nLicense File: node_modules/globals/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com)\nSource: sindresorhus/globals\nLink: sindresorhus/globals\n-----------\nglobalthis\nLicense: MIT\nLicense File: node_modules/globalthis/LICENSE\nCopyright: Copyright (c) 2016 Jordan Harband\nSource: git://github.com/ljharb/System.global.git\nLink: git://github.com/ljharb/System.global.git\n-----------\ngopd\nLicense: MIT\nLicense File: node_modules/gopd/LICENSE\nCopyright: Copyright (c) 2022 Jordan Harband\nSource: git+https://github.com/ljharb/gopd.git\nLink: https://github.com/ljharb/gopd#readme\n-----------\ngraceful-fs\nLicense: ISC\nLicense File: node_modules/graceful-fs/LICENSE\nCopyright: Copyright (c) 2011-2022 Isaac Z. Schlueter, Ben Noordhuis, and Contributors\nSource: https://github.com/isaacs/node-graceful-fs\nLink: https://github.com/isaacs/node-graceful-fs\n-----------\nhandlebars\nLicense: MIT\nLicense File: node_modules/handlebars/LICENSE\nCopyright: Copyright (C) 2011-2019 by Yehuda Katz\nSource: https://github.com/handlebars-lang/handlebars.js.git\nLink: https://www.handlebarsjs.com/\n-----------\nhas-bigints\nLicense: MIT\nLicense File: node_modules/has-bigints/LICENSE\nCopyright: Copyright (c) 2019 Jordan Harband\nSource: git+https://github.com/ljharb/has-bigints.git\nLink: https://github.com/ljharb/has-bigints#readme\n-----------\nhas-flag\nLicense: MIT\nLicense File: node_modules/has-flag/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/has-flag\nLink: sindresorhus/has-flag\n-----------\nhas-property-descriptors\nLicense: MIT\nLicense File: node_modules/has-property-descriptors/LICENSE\nCopyright: Copyright (c) 2022 Inspect JS\nSource: git+https://github.com/inspect-js/has-property-descriptors.git\nLink: https://github.com/inspect-js/has-property-descriptors#readme\n-----------\nhas-proto\nLicense: MIT\nLicense File: node_modules/has-proto/LICENSE\nCopyright: Copyright (c) 2022 Inspect JS\nSource: git+https://github.com/inspect-js/has-proto.git\nLink: https://github.com/inspect-js/has-proto#readme\n-----------\nhas-symbols\nLicense: MIT\nLicense File: node_modules/has-symbols/LICENSE\nCopyright: Copyright (c) 2016 Jordan Harband\nSource: git://github.com/inspect-js/has-symbols.git\nLink: https://github.com/ljharb/has-symbols#readme\n-----------\nhas-tostringtag\nLicense: MIT\nLicense File: node_modules/has-tostringtag/LICENSE\nCopyright: Copyright (c) 2021 Inspect JS\nSource: git+https://github.com/inspect-js/has-tostringtag.git\nLink: https://github.com/inspect-js/has-tostringtag#readme\n-----------\nhasown\nLicense: MIT\nLicense File: node_modules/hasown/LICENSE\nCopyright: Copyright (c) Jordan Harband and contributors\nSource: git+https://github.com/inspect-js/hasOwn.git\nLink: https://github.com/inspect-js/hasOwn#readme\n-----------\nhosted-git-info\nLicense: ISC\nLicense File: node_modules/hosted-git-info/LICENSE\nCopyright: Copyright (c) 2015, Rebecca Turner\nSource: git+https://github.com/npm/hosted-git-info.git\nLink: https://github.com/npm/hosted-git-info\n-----------\nhtml-encoding-sniffer\nLicense: MIT\nLicense File: node_modules/html-encoding-sniffer/LICENSE.txt\nSource: jsdom/html-encoding-sniffer\nLink: jsdom/html-encoding-sniffer\n-----------\nhtml-escaper\nLicense: MIT\nLicense File: node_modules/html-escaper/LICENSE.txt\nCopyright: Copyright (C) 2017-present by Andrea Giammarchi - @WebReflection\nSource: https://github.com/WebReflection/html-escaper.git\nLink: https://github.com/WebReflection/html-escaper\n-----------\nhttp-proxy-agent\nLicense: MIT\nLicense File: node_modules/http-proxy-agent/LICENSE\nCopyright: Copyright (c) 2013 Nathan Rajlich <******@***********.***>\nSource: https://github.com/TooTallNate/proxy-agents.git\nLink: https://github.com/TooTallNate/proxy-agents.git\n-----------\nhttps-proxy-agent\nLicense: MIT\nLicense File: node_modules/https-proxy-agent/LICENSE\nCopyright: Copyright (c) 2013 Nathan Rajlich <******@***********.***>\nSource: https://github.com/TooTallNate/proxy-agents.git\nLink: https://github.com/TooTallNate/proxy-agents.git\n-----------\nhuman-signals\nLicense: Apache-2.0\nLicense File: node_modules/human-signals/LICENSE\nCopyright: Copyright 2019 ehmicky <*******@*****.***>\nSource: ehmicky/human-signals\nLink: https://git.io/JeluP\n-----------\niconv-lite\nLicense: MIT\nLicense File: node_modules/iconv-lite/LICENSE\nCopyright: Copyright (c) 2011 Alexander Shtuchkin\nSource: git://github.com/ashtuchkin/iconv-lite.git\nLink: https://github.com/ashtuchkin/iconv-lite\n-----------\nidb-keyval\nLicense: Apache-2.0\nSource: git+https://github.com/jakearchibald/idb-keyval.git\nLink: https://github.com/jakearchibald/idb-keyval#readme\n-----------\nignore\nLicense: MIT\nSource: git@github.com:kaelzhang/node-ignore.git\nLink: git@github.com:kaelzhang/node-ignore.git\n-----------\nimmutable\nLicense: MIT\nLicense File: node_modules/immutable/LICENSE\nCopyright: Copyright (c) 2014-present, Lee Byron and other contributors.\nSource: git://github.com/immutable-js/immutable-js.git\nLink: https://immutable-js.com\n-----------\nimport-fresh\nLicense: MIT\nLicense File: node_modules/import-fresh/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com)\nSource: sindresorhus/import-fresh\nLink: sindresorhus/import-fresh\n-----------\nimport-local\nLicense: MIT\nLicense File: node_modules/import-local/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com)\nSource: sindresorhus/import-local\nLink: sindresorhus/import-local\n-----------\nimurmurhash\nLicense: MIT\nSource: https://github.com/jensyt/imurmurhash-js\nLink: https://github.com/jensyt/imurmurhash-js\n-----------\ninflight\nLicense: ISC\nLicense File: node_modules/inflight/LICENSE\nCopyright: Copyright (c) Isaac Z. Schlueter\nSource: https://github.com/npm/inflight.git\nLink: https://github.com/isaacs/inflight\n-----------\ninherits\nLicense: ISC\nLicense File: node_modules/inherits/LICENSE\nCopyright: Copyright (c) Isaac Z. Schlueter\nSource: git://github.com/isaacs/inherits\nLink: git://github.com/isaacs/inherits\n-----------\ninternal-slot\nLicense: MIT\nLicense File: node_modules/internal-slot/LICENSE\nCopyright: Copyright (c) 2019 Jordan Harband\nSource: git+https://github.com/ljharb/internal-slot.git\nLink: https://github.com/ljharb/internal-slot#readme\n-----------\nis-array-buffer\nLicense: MIT\nLicense File: node_modules/is-array-buffer/LICENSE\nCopyright: Copyright (c) 2015 Chen Gengyuan, Inspect JS\nSource: git+https://github.com/inspect-js/is-array-buffer.git\nLink: https://github.com/inspect-js/is-array-buffer#readme\n-----------\nis-arrayish\nLicense: MIT\nLicense File: node_modules/is-arrayish/LICENSE\nCopyright: Copyright (c) 2015 JD Ballard\nSource: https://github.com/qix-/node-is-arrayish.git\nLink: https://github.com/qix-/node-is-arrayish.git\n-----------\nis-async-function\nLicense: MIT\nLicense File: node_modules/is-async-function/LICENSE\nCopyright: Copyright (c) 2021 Jordan Harband\nSource: git://github.com/inspect-js/is-async-function.git\nLink: git://github.com/inspect-js/is-async-function.git\n-----------\nis-bigint\nLicense: MIT\nLicense File: node_modules/is-bigint/LICENSE\nCopyright: Copyright (c) 2018 Jordan Harband\nSource: git+https://github.com/inspect-js/is-bigint.git\nLink: https://github.com/inspect-js/is-bigint#readme\n-----------\nis-binary-path\nLicense: MIT\nLicense File: node_modules/is-binary-path/license\nCopyright: Copyright (c) 2019 Sindre Sorhus <************@*****.***> (https://sindresorhus.com), Paul Miller (https://paulmillr.com)\nSource: sindresorhus/is-binary-path\nLink: sindresorhus/is-binary-path\n-----------\nis-boolean-object\nLicense: MIT\nLicense File: node_modules/is-boolean-object/LICENSE\nCopyright: Copyright (c) 2015 Jordan Harband\nSource: git://github.com/inspect-js/is-boolean-object.git\nLink: git://github.com/inspect-js/is-boolean-object.git\n-----------\nis-callable\nLicense: MIT\nLicense File: node_modules/is-callable/LICENSE\nCopyright: Copyright (c) 2015 Jordan Harband\nSource: git://github.com/inspect-js/is-callable.git\nLink: git://github.com/inspect-js/is-callable.git\n-----------\nis-core-module\nLicense: MIT\nLicense File: node_modules/is-core-module/LICENSE\nCopyright: Copyright (c) 2014 Dave Justice\nSource: git+https://github.com/inspect-js/is-core-module.git\nLink: https://github.com/inspect-js/is-core-module\n-----------\nis-data-view\nLicense: MIT\nLicense File: node_modules/is-data-view/LICENSE\nCopyright: Copyright (c) 2024 Inspect JS\nSource: git+https://github.com/inspect-js/is-data-view.git\nLink: https://github.com/inspect-js/is-data-view#readme\n-----------\nis-date-object\nLicense: MIT\nLicense File: node_modules/is-date-object/LICENSE\nCopyright: Copyright (c) 2015 Jordan Harband\nSource: git://github.com/inspect-js/is-date-object.git\nLink: git://github.com/inspect-js/is-date-object.git\n-----------\nis-extglob\nLicense: MIT\nLicense File: node_modules/is-extglob/LICENSE\nCopyright: Copyright (c) 2014-2016, Jon Schlinkert\nSource: jonschlinkert/is-extglob\nLink: https://github.com/jonschlinkert/is-extglob\n-----------\nis-finalizationregistry\nLicense: MIT\nLicense File: node_modules/is-finalizationregistry/LICENSE\nCopyright: Copyright (c) 2020 Inspect JS\nSource: git+https://github.com/inspect-js/is-finalizationregistry.git\nLink: https://github.com/inspect-js/is-finalizationregistry#readme\n-----------\nis-fullwidth-code-point\nLicense: MIT\nLicense File: node_modules/is-fullwidth-code-point/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/is-fullwidth-code-point\nLink: sindresorhus/is-fullwidth-code-point\n-----------\nis-generator-fn\nLicense: MIT\nLicense File: node_modules/is-generator-fn/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/is-generator-fn\nLink: sindresorhus/is-generator-fn\n-----------\nis-generator-function\nLicense: MIT\nLicense File: node_modules/is-generator-function/LICENSE\nCopyright: Copyright (c) 2014 Jordan Harband\nSource: git://github.com/inspect-js/is-generator-function.git\nLink: git://github.com/inspect-js/is-generator-function.git\n-----------\nis-glob\nLicense: MIT\nLicense File: node_modules/is-glob/LICENSE\nCopyright: Copyright (c) 2014-2017, Jon Schlinkert.\nSource: micromatch/is-glob\nLink: https://github.com/micromatch/is-glob\n-----------\nis-map\nLicense: MIT\nLicense File: node_modules/is-map/LICENSE\nCopyright: Copyright (c) 2019 Inspect JS\nSource: git+https://github.com/inspect-js/is-map.git\nLink: https://github.com/inspect-js/is-map#readme\n-----------\nis-negative-zero\nLicense: MIT\nLicense File: node_modules/is-negative-zero/LICENSE\nCopyright: Copyright (c) 2014 Jordan Harband\nSource: git://github.com/inspect-js/is-negative-zero.git\nLink: https://github.com/inspect-js/is-negative-zero\n-----------\nis-number-object\nLicense: MIT\nLicense File: node_modules/is-number-object/LICENSE\nCopyright: Copyright (c) 2015 Jordan Harband\nSource: git://github.com/inspect-js/is-number-object.git\nLink: https://github.com/inspect-js/is-number-object#readme\n-----------\nis-number\nLicense: MIT\nLicense File: node_modules/is-number/LICENSE\nCopyright: Copyright (c) 2014-present, Jon Schlinkert.\nSource: jonschlinkert/is-number\nLink: https://github.com/jonschlinkert/is-number\n-----------\nis-potential-custom-element-name\nLicense: MIT\nLicense File: node_modules/is-potential-custom-element-name/LICENSE-MIT.txt\nSource: https://github.com/mathiasbynens/is-potential-custom-element-name.git\nLink: https://github.com/mathiasbynens/is-potential-custom-element-name\n-----------\nis-regex\nLicense: MIT\nLicense File: node_modules/is-regex/LICENSE\nCopyright: Copyright (c) 2014 Jordan Harband\nSource: git://github.com/inspect-js/is-regex.git\nLink: https://github.com/inspect-js/is-regex\n-----------\nis-set\nLicense: MIT\nLicense File: node_modules/is-set/LICENSE\nCopyright: Copyright (c) 2019 Inspect JS\nSource: git+https://github.com/inspect-js/is-set.git\nLink: https://github.com/inspect-js/is-set#readme\n-----------\nis-shared-array-buffer\nLicense: MIT\nLicense File: node_modules/is-shared-array-buffer/LICENSE\nCopyright: Copyright (c) 2021 Inspect JS\nSource: git+https://github.com/inspect-js/is-shared-array-buffer.git\nLink: https://github.com/inspect-js/is-shared-array-buffer#readme\n-----------\nis-stream\nLicense: MIT\nLicense File: node_modules/is-stream/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com)\nSource: sindresorhus/is-stream\nLink: sindresorhus/is-stream\n-----------\nis-string\nLicense: MIT\nLicense File: node_modules/is-string/LICENSE\nCopyright: Copyright (c) 2015 Jordan Harband\nSource: git://github.com/inspect-js/is-string.git\nLink: git://github.com/inspect-js/is-string.git\n-----------\nis-symbol\nLicense: MIT\nLicense File: node_modules/is-symbol/LICENSE\nCopyright: Copyright (c) 2015 Jordan Harband\nSource: git://github.com/inspect-js/is-symbol.git\nLink: git://github.com/inspect-js/is-symbol.git\n-----------\nis-typed-array\nLicense: MIT\nLicense File: node_modules/is-typed-array/LICENSE\nCopyright: Copyright (c) 2015 Jordan Harband\nSource: git://github.com/inspect-js/is-typed-array.git\nLink: git://github.com/inspect-js/is-typed-array.git\n-----------\nis-weakmap\nLicense: MIT\nLicense File: node_modules/is-weakmap/LICENSE\nCopyright: Copyright (c) 2019 Inspect JS\nSource: git+https://github.com/inspect-js/is-weakmap.git\nLink: https://github.com/inspect-js/is-weakmap#readme\n-----------\nis-weakref\nLicense: MIT\nLicense File: node_modules/is-weakref/LICENSE\nCopyright: Copyright (c) 2020 Inspect JS\nSource: git+https://github.com/inspect-js/is-weakref.git\nLink: https://github.com/inspect-js/is-weakref#readme\n-----------\nis-weakset\nLicense: MIT\nLicense File: node_modules/is-weakset/LICENSE\nCopyright: Copyright (c) 2019 Inspect JS\nSource: git+https://github.com/inspect-js/is-weakset.git\nLink: https://github.com/inspect-js/is-weakset#readme\n-----------\nisarray\nLicense: MIT\nLicense File: node_modules/isarray/LICENSE\nCopyright: Copyright (c) 2013 Julian Gruber <******@************.***>\nSource: git://github.com/juliangruber/isarray.git\nLink: https://github.com/juliangruber/isarray\n-----------\nisexe\nLicense: ISC\nLicense File: node_modules/isexe/LICENSE\nCopyright: Copyright (c) Isaac Z. Schlueter and Contributors\nSource: git+https://github.com/isaacs/isexe.git\nLink: https://github.com/isaacs/isexe#readme\n-----------\nistanbul-lib-coverage\nLicense: BSD-3-Clause\nLicense File: node_modules/istanbul-lib-coverage/LICENSE\nCopyright: Copyright 2012-2015 Yahoo! Inc.\nSource: git+ssh://git@github.com/istanbuljs/istanbuljs.git\nLink: https://istanbul.js.org/\n-----------\nistanbul-lib-instrument\nLicense: BSD-3-Clause\nLicense File: node_modules/istanbul-lib-instrument/LICENSE\nCopyright: Copyright 2012-2015 Yahoo! Inc.\nSource: git+ssh://git@github.com/istanbuljs/istanbuljs.git\nLink: https://istanbul.js.org/\n-----------\nistanbul-lib-report\nLicense: BSD-3-Clause\nLicense File: node_modules/istanbul-lib-report/LICENSE\nCopyright: Copyright 2012-2015 Yahoo! Inc.\nSource: git+ssh://git@github.com/istanbuljs/istanbuljs.git\nLink: https://istanbul.js.org/\n-----------\nistanbul-lib-source-maps\nLicense: BSD-3-Clause\nLicense File: node_modules/istanbul-lib-source-maps/LICENSE\nCopyright: Copyright 2015 Yahoo! Inc.\nSource: git+ssh://git@github.com/istanbuljs/istanbuljs.git\nLink: https://istanbul.js.org/\n-----------\nistanbul-reports\nLicense: BSD-3-Clause\nLicense File: node_modules/istanbul-reports/LICENSE\nCopyright: Copyright 2012-2015 Yahoo! Inc.\nSource: git+ssh://git@github.com/istanbuljs/istanbuljs.git\nLink: https://istanbul.js.org/\n-----------\njackspeak\nLicense: BlueOak-1.0.0\nLicense File: node_modules/jackspeak/LICENSE.md\nSource: git+https://github.com/isaacs/jackspeak.git\nLink: git+https://github.com/isaacs/jackspeak.git\n-----------\njest-changed-files\nLicense: MIT\nLicense File: node_modules/jest-changed-files/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\njest-circus\nLicense: MIT\nLicense File: node_modules/jest-circus/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\njest-cli\nLicense: MIT\nLicense File: node_modules/jest-cli/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://jestjs.io/\n-----------\njest-config\nLicense: MIT\nLicense File: node_modules/jest-config/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\njest-diff\nLicense: MIT\nLicense File: node_modules/jest-diff/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\njest-docblock\nLicense: MIT\nLicense File: node_modules/jest-docblock/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\njest-each\nLicense: MIT\nLicense File: node_modules/jest-each/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\njest-environment-jsdom\nLicense: MIT\nLicense File: node_modules/jest-environment-jsdom/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\njest-environment-node\nLicense: MIT\nLicense File: node_modules/jest-environment-node/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\njest-haste-map\nLicense: MIT\nLicense File: node_modules/jest-haste-map/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\njest-leak-detector\nLicense: MIT\nLicense File: node_modules/jest-leak-detector/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\njest-matcher-utils\nLicense: MIT\nLicense File: node_modules/jest-matcher-utils/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\njest-message-util\nLicense: MIT\nLicense File: node_modules/jest-message-util/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\njest-mock\nLicense: MIT\nLicense File: node_modules/jest-mock/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\njest-pnp-resolver\nLicense: MIT\nSource: https://github.com/arcanis/jest-pnp-resolver.git\nLink: https://github.com/arcanis/jest-pnp-resolver\n-----------\njest-regex-util\nLicense: MIT\nLicense File: node_modules/jest-regex-util/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\njest-resolve-dependencies\nLicense: MIT\nLicense File: node_modules/jest-resolve-dependencies/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\njest-resolve\nLicense: MIT\nLicense File: node_modules/jest-resolve/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\njest-runner\nLicense: MIT\nLicense File: node_modules/jest-runner/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\njest-runtime\nLicense: MIT\nLicense File: node_modules/jest-runtime/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\njest-snapshot\nLicense: MIT\nLicense File: node_modules/jest-snapshot/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\njest-util\nLicense: MIT\nLicense File: node_modules/jest-util/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\njest-validate\nLicense: MIT\nLicense File: node_modules/jest-validate/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\njest-watcher\nLicense: MIT\nLicense File: node_modules/jest-watcher/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://jestjs.io/\n-----------\njest-worker\nLicense: MIT\nLicense File: node_modules/jest-worker/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\njest\nLicense: MIT\nLicense File: node_modules/jest/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://jestjs.io/\n-----------\njs-tokens\nLicense: MIT\nLicense File: node_modules/js-tokens/LICENSE\nCopyright: Copyright (c) 2014, 2015, 2016, 2017, 2018 Simon Lydell\nSource: lydell/js-tokens\nLink: lydell/js-tokens\n-----------\njs-yaml\nLicense: MIT\nLicense File: node_modules/js-yaml/LICENSE\nCopyright: Copyright (C) 2011-2015 by Vitaly Puzrin\nSource: nodeca/js-yaml\nLink: nodeca/js-yaml\n-----------\njsdom\nLicense: MIT\nLicense File: node_modules/jsdom/LICENSE.txt\nCopyright: Copyright (c) 2010 Elijah Insua\nSource: git+https://github.com/jsdom/jsdom.git\nLink: git+https://github.com/jsdom/jsdom.git\n-----------\njsesc\nLicense: MIT\nLicense File: node_modules/jsesc/LICENSE-MIT.txt\nSource: https://github.com/mathiasbynens/jsesc.git\nLink: https://mths.be/jsesc\n-----------\njson-buffer\nLicense: MIT\nLicense File: node_modules/json-buffer/LICENSE\nCopyright: Copyright (c) 2013 Dominic Tarr\nSource: git://github.com/dominictarr/json-buffer.git\nLink: https://github.com/dominictarr/json-buffer\n-----------\njson-parse-better-errors\nLicense: MIT\nLicense File: node_modules/json-parse-better-errors/LICENSE.md\nCopyright: Copyright 2017 Kat Marchán\nSource: https://github.com/zkat/json-parse-better-errors\nLink: https://github.com/zkat/json-parse-better-errors\n-----------\njson-parse-even-better-errors\nLicense: MIT\nLicense File: node_modules/json-parse-even-better-errors/LICENSE.md\nCopyright: Copyright 2017 Kat Marchán\nCopyright npm, Inc.\nSource: https://github.com/npm/json-parse-even-better-errors\nLink: https://github.com/npm/json-parse-even-better-errors\n-----------\njson-schema-traverse\nLicense: MIT\nLicense File: node_modules/json-schema-traverse/LICENSE\nCopyright: Copyright (c) 2017 Evgeny Poberezkin\nSource: git+https://github.com/epoberezkin/json-schema-traverse.git\nLink: https://github.com/epoberezkin/json-schema-traverse#readme\n-----------\njson-stable-stringify-without-jsonify\nLicense: MIT\nLicense File: node_modules/json-stable-stringify-without-jsonify/LICENSE\nSource: git://github.com/samn/json-stable-stringify.git\nLink: https://github.com/samn/json-stable-stringify\n-----------\njson5\nLicense: MIT\nLicense File: node_modules/json5/LICENSE.md\nCopyright: Copyright (c) 2012-2018 Aseem Kishore, and [others].\nSource: git+https://github.com/json5/json5.git\nLink: http://json5.org/\n-----------\nkeyv\nLicense: MIT\nSource: git+https://github.com/jaredwray/keyv.git\nLink: https://github.com/jaredwray/keyv\n-----------\nleven\nLicense: MIT\nLicense File: node_modules/leven/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/leven\nLink: sindresorhus/leven\n-----------\nlevn\nLicense: MIT\nLicense File: node_modules/levn/LICENSE\nCopyright: Copyright (c) George Zahariev\nSource: git://github.com/gkz/levn.git\nLink: https://github.com/gkz/levn\n-----------\nlines-and-columns\nLicense: MIT\nLicense File: node_modules/lines-and-columns/LICENSE\nCopyright: Copyright (c) 2015 Brian Donovan\nSource: https://github.com/eventualbuddha/lines-and-columns.git\nLink: https://github.com/eventualbuddha/lines-and-columns#readme\n-----------\nlinkify-it\nLicense: MIT\nLicense File: node_modules/linkify-it/LICENSE\nCopyright: Copyright (c) 2015 Vitaly Puzrin.\nSource: markdown-it/linkify-it\nLink: markdown-it/linkify-it\n-----------\nload-json-file\nLicense: MIT\nLicense File: node_modules/load-json-file/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/load-json-file\nLink: sindresorhus/load-json-file\n-----------\nlocate-path\nLicense: MIT\nLicense File: node_modules/locate-path/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com)\nSource: sindresorhus/locate-path\nLink: sindresorhus/locate-path\n-----------\nlodash.debounce\nLicense: MIT\nLicense File: node_modules/lodash.debounce/LICENSE\nSource: lodash/lodash\nLink: https://lodash.com/\n-----------\nlodash.memoize\nLicense: MIT\nLicense File: node_modules/lodash.memoize/LICENSE\nSource: lodash/lodash\nLink: https://lodash.com/\n-----------\nlodash.merge\nLicense: MIT\nLicense File: node_modules/lodash.merge/LICENSE\nSource: lodash/lodash\nLink: https://lodash.com/\n-----------\nlodash.throttle\nLicense: MIT\nLicense File: node_modules/lodash.throttle/LICENSE\nSource: lodash/lodash\nLink: https://lodash.com/\n-----------\nlru-cache\nLicense: ISC\nLicense File: node_modules/lru-cache/LICENSE\nCopyright: Copyright (c) Isaac Z. Schlueter and Contributors\nSource: git://github.com/isaacs/node-lru-cache.git\nLink: git://github.com/isaacs/node-lru-cache.git\n-----------\nmake-dir\nLicense: MIT\nLicense File: node_modules/make-dir/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com)\nSource: sindresorhus/make-dir\nLink: sindresorhus/make-dir\n-----------\nmake-error\nLicense: ISC\nLicense File: node_modules/make-error/LICENSE\nCopyright: Copyright 2014 Julien Fontanet\nSource: git://github.com/JsCommunity/make-error.git\nLink: https://github.com/JsCommunity/make-error\n-----------\nmakeerror\nLicense: BSD-3-Clause\nLicense File: node_modules/makeerror/license\nCopyright: Copyright (c) 2014, Naitik Shah. All rights reserved.\nSource: https://github.com/daaku/nodejs-makeerror\nLink: https://github.com/daaku/nodejs-makeerror\n-----------\nmarkdown-it-task-lists\nLicense: ISC\nLicense File: node_modules/markdown-it-task-lists/LICENSE\nCopyright: Copyright (c) 2016, Revin Guillen\nSource: git@github.com:revin/markdown-it-task-lists.git\nLink: https://github.com/revin/markdown-it-task-lists#readme\n-----------\nmarkdown-it\nLicense: MIT\nLicense File: node_modules/markdown-it/LICENSE\nCopyright: Copyright (c) 2014 Vitaly Puzrin, Alex Kocharin.\nSource: markdown-it/markdown-it\nLink: markdown-it/markdown-it\n-----------\nmath-intrinsics\nLicense: MIT\nLicense File: node_modules/math-intrinsics/LICENSE\nCopyright: Copyright (c) 2024 ECMAScript Shims\nSource: git+https://github.com/es-shims/math-intrinsics.git\nLink: https://github.com/es-shims/math-intrinsics#readme\n-----------\nmdurl\nLicense: MIT\nLicense File: node_modules/mdurl/LICENSE\nCopyright: Copyright (c) 2015 Vitaly Puzrin, Alex Kocharin.\nSource: markdown-it/mdurl\nLink: markdown-it/mdurl\n-----------\nmemorystream\nLicense File: node_modules/memorystream/LICENSE\nCopyright: Copyright (C) 2011 Dmitry Nizovtsev\nSource: https://github.com/JSBizon/node-memorystream.git\nLink: https://github.com/JSBizon/node-memorystream\n-----------\nmerge-stream\nLicense: MIT\nLicense File: node_modules/merge-stream/LICENSE\nCopyright: Copyright (c) Stephen Sugden <**@*************.***> (stephensugden.com)\nSource: grncdr/merge-stream\nLink: grncdr/merge-stream\n-----------\nmicromatch\nLicense: MIT\nLicense File: node_modules/micromatch/LICENSE\nCopyright: Copyright (c) 2014-present, Jon Schlinkert.\nSource: micromatch/micromatch\nLink: https://github.com/micromatch/micromatch\n-----------\nmimic-fn\nLicense: MIT\nLicense File: node_modules/mimic-fn/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/mimic-fn\nLink: sindresorhus/mimic-fn\n-----------\nminimatch\nLicense: ISC\nLicense File: node_modules/minimatch/LICENSE\nCopyright: Copyright (c) Isaac Z. Schlueter and Contributors\nSource: git://github.com/isaacs/minimatch.git\nLink: git://github.com/isaacs/minimatch.git\n-----------\nminimist\nLicense: MIT\nLicense File: node_modules/minimist/LICENSE\nSource: git://github.com/minimistjs/minimist.git\nLink: https://github.com/minimistjs/minimist\n-----------\nminipass\nLicense: ISC\nLicense File: node_modules/minipass/LICENSE\nCopyright: Copyright (c) 2017-2023 npm, Inc., Isaac Z. Schlueter, and Contributors\nSource: https://github.com/isaacs/minipass\nLink: https://github.com/isaacs/minipass\n-----------\nms\nLicense: MIT\nLicense File: node_modules/ms/license.md\nCopyright: Copyright (c) 2020 Vercel, Inc.\nSource: vercel/ms\nLink: vercel/ms\n-----------\nnapi-postinstall\nLicense: MIT\nLicense File: node_modules/napi-postinstall/LICENSE\nCopyright: Copyright (c) 2021-present UnTS\nSource: git+https://github.com/un-ts/napi-postinstall.git\nLink: git+https://github.com/un-ts/napi-postinstall.git\n-----------\nnatural-compare\nLicense: MIT\nSource: git://github.com/litejs/natural-compare-lite.git\nLink: git://github.com/litejs/natural-compare-lite.git\n-----------\nneo-async\nLicense: MIT\nLicense File: node_modules/neo-async/LICENSE\nCopyright: Copyright (c) 2014-2018 Suguru Motegi\nBased on Async.js, Copyright Caolan McMahon\nSource: git@github.com:suguru03/neo-async.git\nLink: https://github.com/suguru03/neo-async\n-----------\nnice-try\nLicense: MIT\nLicense File: node_modules/nice-try/LICENSE\nCopyright: Copyright (c) 2018 Tobias Reich\nSource: https://github.com/electerious/nice-try.git\nLink: https://github.com/electerious/nice-try\n-----------\nnode-addon-api\nLicense: MIT\nLicense File: node_modules/node-addon-api/LICENSE.md\nCopyright: Copyright (c) 2017 [Node.js API collaborators](https://github.com/nodejs/node-addon-api#collaborators)\nSource: git://github.com/nodejs/node-addon-api.git\nLink: https://github.com/nodejs/node-addon-api\n-----------\nnode-int64\nLicense: MIT\nLicense File: node_modules/node-int64/LICENSE\nCopyright: Copyright (c) 2014 Robert Kieffer\nSource: https://github.com/broofa/node-int64\nLink: https://github.com/broofa/node-int64\n-----------\nnode-releases\nLicense: MIT\nLicense File: node_modules/node-releases/LICENSE\nCopyright: Copyright (c) 2017 Sergey Rubanov (https://github.com/chicoxyzzy)\nSource: git+https://github.com/chicoxyzzy/node-releases.git\nLink: git+https://github.com/chicoxyzzy/node-releases.git\n-----------\nnormalize-package-data\nLicense: BSD-2-Clause\nLicense File: node_modules/normalize-package-data/LICENSE\nCopyright: Copyright (c) Meryn Stol (\"Author\")\nAll rights reserved.\nSource: git://github.com/npm/normalize-package-data.git\nLink: git://github.com/npm/normalize-package-data.git\n-----------\nnormalize-path\nLicense: MIT\nLicense File: node_modules/normalize-path/LICENSE\nCopyright: Copyright (c) 2014-2018, Jon Schlinkert.\nSource: jonschlinkert/normalize-path\nLink: https://github.com/jonschlinkert/normalize-path\n-----------\nnpm-run-all\nLicense: MIT\nLicense File: node_modules/npm-run-all/LICENSE\nCopyright: Copyright (c) 2015 Toru Nagashima\nSource: mysticatea/npm-run-all\nLink: https://github.com/mysticatea/npm-run-all\n-----------\nnpm-run-path\nLicense: MIT\nLicense File: node_modules/npm-run-path/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/npm-run-path\nLink: sindresorhus/npm-run-path\n-----------\nnwsapi\nLicense: MIT\nLicense File: node_modules/nwsapi/LICENSE\nCopyright: Copyright (c) 2007-2025 Diego Perini (http://www.iport.it/)\nSource: git://github.com/dperini/nwsapi.git\nLink: https://javascript.nwbox.com/nwsapi/\n-----------\nobject-inspect\nLicense: MIT\nLicense File: node_modules/object-inspect/LICENSE\nCopyright: Copyright (c) 2013 James Halliday\nSource: git://github.com/inspect-js/object-inspect.git\nLink: https://github.com/inspect-js/object-inspect\n-----------\nobject-keys\nLicense: MIT\nLicense File: node_modules/object-keys/LICENSE\nCopyright: Copyright (C) 2013 Jordan Harband\nSource: git://github.com/ljharb/object-keys.git\nLink: git://github.com/ljharb/object-keys.git\n-----------\nobject.assign\nLicense: MIT\nLicense File: node_modules/object.assign/LICENSE\nCopyright: Copyright (c) 2014 Jordan Harband\nSource: git://github.com/ljharb/object.assign.git\nLink: git://github.com/ljharb/object.assign.git\n-----------\nobject.fromentries\nLicense: MIT\nLicense File: node_modules/object.fromentries/LICENSE\nCopyright: Copyright (c) 2018 Jordan Harband\nSource: git://github.com/es-shims/Object.fromEntries.git\nLink: git://github.com/es-shims/Object.fromEntries.git\n-----------\nobject.groupby\nLicense: MIT\nLicense File: node_modules/object.groupby/LICENSE\nCopyright: Copyright (c) 2023 ECMAScript Shims\nSource: git+https://github.com/es-shims/Object.groupBy.git\nLink: https://github.com/es-shims/Object.groupBy#readme\n-----------\nobject.values\nLicense: MIT\nLicense File: node_modules/object.values/LICENSE\nCopyright: Copyright (c) 2015 Jordan Harband\nSource: git://github.com/es-shims/Object.values.git\nLink: git://github.com/es-shims/Object.values.git\n-----------\nonce\nLicense: ISC\nLicense File: node_modules/once/LICENSE\nCopyright: Copyright (c) Isaac Z. Schlueter and Contributors\nSource: git://github.com/isaacs/once\nLink: git://github.com/isaacs/once\n-----------\nonetime\nLicense: MIT\nLicense File: node_modules/onetime/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com)\nSource: sindresorhus/onetime\nLink: sindresorhus/onetime\n-----------\noptionator\nLicense: MIT\nLicense File: node_modules/optionator/LICENSE\nCopyright: Copyright (c) George Zahariev\nSource: git://github.com/gkz/optionator.git\nLink: https://github.com/gkz/optionator\n-----------\nown-keys\nLicense: MIT\nLicense File: node_modules/own-keys/LICENSE\nCopyright: Copyright (c) 2024 Jordan Harband\nSource: git+https://github.com/ljharb/own-keys.git\nLink: https://github.com/ljharb/own-keys#readme\n-----------\np-limit\nLicense: MIT\nLicense File: node_modules/p-limit/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com)\nSource: sindresorhus/p-limit\nLink: sindresorhus/p-limit\n-----------\np-locate\nLicense: MIT\nLicense File: node_modules/p-locate/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com)\nSource: sindresorhus/p-locate\nLink: sindresorhus/p-locate\n-----------\np-try\nLicense: MIT\nLicense File: node_modules/p-try/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/p-try\nLink: sindresorhus/p-try\n-----------\npackage-json-from-dist\nLicense: BlueOak-1.0.0\nLicense File: node_modules/package-json-from-dist/LICENSE.md\nSource: git+https://github.com/isaacs/package-json-from-dist.git\nLink: git+https://github.com/isaacs/package-json-from-dist.git\n-----------\nparent-module\nLicense: MIT\nLicense File: node_modules/parent-module/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/parent-module\nLink: sindresorhus/parent-module\n-----------\nparse-json\nLicense: MIT\nLicense File: node_modules/parse-json/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com)\nSource: sindresorhus/parse-json\nLink: sindresorhus/parse-json\n-----------\nparse5\nLicense: MIT\nLicense File: node_modules/parse5/LICENSE\nCopyright: Copyright (c) 2013-2019 Ivan Nikulin (******@*****.***, https://github.com/inikulin)\nSource: git://github.com/inikulin/parse5.git\nLink: https://parse5.js.org\n-----------\npath-exists\nLicense: MIT\nLicense File: node_modules/path-exists/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/path-exists\nLink: sindresorhus/path-exists\n-----------\npath-is-absolute\nLicense: MIT\nLicense File: node_modules/path-is-absolute/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/path-is-absolute\nLink: sindresorhus/path-is-absolute\n-----------\npath-key\nLicense: MIT\nLicense File: node_modules/path-key/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/path-key\nLink: sindresorhus/path-key\n-----------\npath-parse\nLicense: MIT\nLicense File: node_modules/path-parse/LICENSE\nCopyright: Copyright (c) 2015 Javier Blanco\nSource: https://github.com/jbgutierrez/path-parse.git\nLink: https://github.com/jbgutierrez/path-parse#readme\n-----------\npath-scurry\nLicense: BlueOak-1.0.0\nLicense File: node_modules/path-scurry/LICENSE.md\nSource: git+https://github.com/isaacs/path-scurry\nLink: git+https://github.com/isaacs/path-scurry\n-----------\npath-type\nLicense: MIT\nLicense File: node_modules/path-type/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/path-type\nLink: sindresorhus/path-type\n-----------\npicocolors\nLicense: ISC\nLicense File: node_modules/picocolors/LICENSE\nCopyright: Copyright (c) 2021-2024 Oleksii Raspopov, Kostiantyn Denysov, Anton Verinov\nSource: alexeyraspopov/picocolors\nLink: alexeyraspopov/picocolors\n-----------\npicomatch\nLicense: MIT\nLicense File: node_modules/picomatch/LICENSE\nCopyright: Copyright (c) 2017-present, Jon Schlinkert.\nSource: micromatch/picomatch\nLink: https://github.com/micromatch/picomatch\n-----------\npidtree\nLicense: MIT\nLicense File: node_modules/pidtree/license\nCopyright: Copyright (c) 2018 Simone Primarosa\nSource: github:simonepri/pidtree\nLink: http://github.com/simonepri/pidtree#readme\n-----------\npify\nLicense: MIT\nLicense File: node_modules/pify/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/pify\nLink: sindresorhus/pify\n-----------\npirates\nLicense: MIT\nLicense File: node_modules/pirates/LICENSE\nCopyright: Copyright (c) 2016-2018 Ari Porad\nSource: https://github.com/danez/pirates.git\nLink: https://github.com/danez/pirates#readme\n-----------\npkg-dir\nLicense: MIT\nLicense File: node_modules/pkg-dir/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/pkg-dir\nLink: sindresorhus/pkg-dir\n-----------\npossible-typed-array-names\nLicense: MIT\nLicense File: node_modules/possible-typed-array-names/LICENSE\nCopyright: Copyright (c) 2024 Jordan Harband\nSource: git+https://github.com/ljharb/possible-typed-array-names.git\nLink: https://github.com/ljharb/possible-typed-array-names#readme\n-----------\nprelude-ls\nLicense: MIT\nLicense File: node_modules/prelude-ls/LICENSE\nCopyright: Copyright (c) George Zahariev\nSource: git://github.com/gkz/prelude-ls.git\nLink: http://preludels.com\n-----------\npretty-format\nLicense: MIT\nLicense File: node_modules/pretty-format/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\npunycode.js\nLicense: MIT\nLicense File: node_modules/punycode.js/LICENSE-MIT.txt\nSource: https://github.com/mathiasbynens/punycode.js.git\nLink: https://mths.be/punycode\n-----------\npunycode\nLicense: MIT\nLicense File: node_modules/punycode/LICENSE-MIT.txt\nSource: https://github.com/mathiasbynens/punycode.js.git\nLink: https://mths.be/punycode\n-----------\npure-rand\nLicense: MIT\nLicense File: node_modules/pure-rand/LICENSE\nCopyright: Copyright (c) 2018 Nicolas DUBIEN\nSource: git+https://github.com/dubzzz/pure-rand.git\nLink: https://github.com/dubzzz/pure-rand#readme\n-----------\nreact-is\nLicense: MIT\nLicense File: node_modules/react-is/LICENSE\nCopyright: Copyright (c) Facebook, Inc. and its affiliates.\nSource: https://github.com/facebook/react.git\nLink: https://reactjs.org/\n-----------\nread-pkg\nLicense: MIT\nLicense File: node_modules/read-pkg/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/read-pkg\nLink: sindresorhus/read-pkg\n-----------\nreaddirp\nLicense: MIT\nLicense File: node_modules/readdirp/LICENSE\nCopyright: Copyright (c) 2012-2019 Thorsten Lorenz, Paul Miller (https://paulmillr.com)\nSource: git://github.com/paulmillr/readdirp.git\nLink: https://github.com/paulmillr/readdirp\n-----------\nreflect.getprototypeof\nLicense: MIT\nLicense File: node_modules/reflect.getprototypeof/LICENSE\nCopyright: Copyright (c) 2021 ECMAScript Shims\nSource: git+https://github.com/es-shims/Reflect.getPrototypeOf.git\nLink: https://github.com/es-shims/Reflect.getPrototypeOf\n-----------\nregexp.prototype.flags\nLicense: MIT\nLicense File: node_modules/regexp.prototype.flags/LICENSE\nCopyright: Copyright (C) 2014 Jordan Harband\nSource: git://github.com/es-shims/RegExp.prototype.flags.git\nLink: git://github.com/es-shims/RegExp.prototype.flags.git\n-----------\nrequire-directory\nLicense: MIT\nLicense File: node_modules/require-directory/LICENSE\nCopyright: Copyright (c) 2011 Troy Goode <*********@*****.***>\nSource: git://github.com/troygoode/node-require-directory.git\nLink: https://github.com/troygoode/node-require-directory/\n-----------\nrequire-main-filename\nLicense: ISC\nLicense File: node_modules/require-main-filename/LICENSE.txt\nCopyright: Copyright (c) 2016, Contributors\nSource: git+ssh://git@github.com/yargs/require-main-filename.git\nLink: https://github.com/yargs/require-main-filename#readme\n-----------\nresolve-cwd\nLicense: MIT\nLicense File: node_modules/resolve-cwd/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/resolve-cwd\nLink: sindresorhus/resolve-cwd\n-----------\nresolve-from\nLicense: MIT\nLicense File: node_modules/resolve-from/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/resolve-from\nLink: sindresorhus/resolve-from\n-----------\nresolve\nLicense: MIT\nLicense File: node_modules/resolve/LICENSE\nCopyright: Copyright (c) 2012 James Halliday\nSource: git://github.com/browserify/resolve.git\nLink: git://github.com/browserify/resolve.git\n-----------\nrrweb-cssom\nLicense: MIT\nLicense File: node_modules/rrweb-cssom/LICENSE.txt\nCopyright: Copyright (c) Nikita Vasilyev\nSource: rrweb-io/CSSOM\nLink: rrweb-io/CSSOM\n-----------\nsafe-array-concat\nLicense: MIT\nLicense File: node_modules/safe-array-concat/LICENSE\nCopyright: Copyright (c) 2023 Jordan Harband\nSource: git+https://github.com/ljharb/safe-array-concat.git\nLink: https://github.com/ljharb/safe-array-concat#readme\n-----------\nsafe-push-apply\nLicense: MIT\nLicense File: node_modules/safe-push-apply/LICENSE\nCopyright: Copyright (c) 2024 Jordan Harband\nSource: git+https://github.com/ljharb/safe-push-apply.git\nLink: https://github.com/ljharb/safe-push-apply#readme\n-----------\nsafe-regex-test\nLicense: MIT\nLicense File: node_modules/safe-regex-test/LICENSE\nCopyright: Copyright (c) 2022 Jordan Harband\nSource: git+https://github.com/ljharb/safe-regex-test.git\nLink: https://github.com/ljharb/safe-regex-test#readme\n-----------\nsafer-buffer\nLicense: MIT\nLicense File: node_modules/safer-buffer/LICENSE\nCopyright: Copyright (c) 2018 Nikita Skovoroda <********@*****.***>\nSource: git+https://github.com/ChALkeR/safer-buffer.git\nLink: git+https://github.com/ChALkeR/safer-buffer.git\n-----------\nsass\nLicense: MIT\nLicense File: node_modules/sass/LICENSE\nCopyright: Copyright (c) 2016, Google Inc.\nSource: https://github.com/sass/dart-sass\nLink: https://github.com/sass/dart-sass\n-----------\nsaxes\nLicense: ISC\nSource: https://github.com/lddubeau/saxes.git\nLink: https://github.com/lddubeau/saxes.git\n-----------\nsemver\nLicense: ISC\nLicense File: node_modules/semver/LICENSE\nCopyright: Copyright (c) Isaac Z. Schlueter and Contributors\nSource: https://github.com/npm/node-semver.git\nLink: https://github.com/npm/node-semver.git\n-----------\nset-blocking\nLicense: ISC\nLicense File: node_modules/set-blocking/LICENSE.txt\nCopyright: Copyright (c) 2016, Contributors\nSource: git+https://github.com/yargs/set-blocking.git\nLink: https://github.com/yargs/set-blocking#readme\n-----------\nset-function-length\nLicense: MIT\nLicense File: node_modules/set-function-length/LICENSE\nCopyright: Copyright (c) Jordan Harband and contributors\nSource: git+https://github.com/ljharb/set-function-length.git\nLink: https://github.com/ljharb/set-function-length#readme\n-----------\nset-function-name\nLicense: MIT\nLicense File: node_modules/set-function-name/LICENSE\nCopyright: Copyright (c) Jordan Harband and contributors\nSource: git+https://github.com/ljharb/set-function-name.git\nLink: https://github.com/ljharb/set-function-name#readme\n-----------\nset-proto\nLicense: MIT\nLicense File: node_modules/set-proto/LICENSE\nCopyright: Copyright (c) 2024 Jordan Harband\nSource: git+https://github.com/ljharb/set-proto.git\nLink: https://github.com/ljharb/set-proto#readme\n-----------\nshebang-command\nLicense: MIT\nLicense File: node_modules/shebang-command/license\nCopyright: Copyright (c) Kevin Mårtensson <***************@*****.***> (github.com/kevva)\nSource: kevva/shebang-command\nLink: kevva/shebang-command\n-----------\nshebang-regex\nLicense: MIT\nLicense File: node_modules/shebang-regex/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/shebang-regex\nLink: sindresorhus/shebang-regex\n-----------\nshell-quote\nLicense: MIT\nLicense File: node_modules/shell-quote/LICENSE\nCopyright: Copyright (c) 2013 James Halliday (****@********.***)\nSource: http://github.com/ljharb/shell-quote.git\nLink: https://github.com/ljharb/shell-quote\n-----------\nside-channel-list\nLicense: MIT\nLicense File: node_modules/side-channel-list/LICENSE\nCopyright: Copyright (c) 2024 Jordan Harband\nSource: git+https://github.com/ljharb/side-channel-list.git\nLink: https://github.com/ljharb/side-channel-list#readme\n-----------\nside-channel-map\nLicense: MIT\nLicense File: node_modules/side-channel-map/LICENSE\nCopyright: Copyright (c) 2024 Jordan Harband\nSource: git+https://github.com/ljharb/side-channel-map.git\nLink: https://github.com/ljharb/side-channel-map#readme\n-----------\nside-channel-weakmap\nLicense: MIT\nLicense File: node_modules/side-channel-weakmap/LICENSE\nCopyright: Copyright (c) 2019 Jordan Harband\nSource: git+https://github.com/ljharb/side-channel-weakmap.git\nLink: https://github.com/ljharb/side-channel-weakmap#readme\n-----------\nside-channel\nLicense: MIT\nLicense File: node_modules/side-channel/LICENSE\nCopyright: Copyright (c) 2019 Jordan Harband\nSource: git+https://github.com/ljharb/side-channel.git\nLink: https://github.com/ljharb/side-channel#readme\n-----------\nsignal-exit\nLicense: ISC\nLicense File: node_modules/signal-exit/LICENSE.txt\nCopyright: Copyright (c) 2015-2023 Benjamin Coe, Isaac Z. Schlueter, and Contributors\nSource: https://github.com/tapjs/signal-exit.git\nLink: https://github.com/tapjs/signal-exit.git\n-----------\nslash\nLicense: MIT\nLicense File: node_modules/slash/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/slash\nLink: sindresorhus/slash\n-----------\nsnabbdom\nLicense: MIT\nLicense File: node_modules/snabbdom/LICENSE\nCopyright: Copyright (c) 2015 Simon Friis Vindum\nSource: git+https://github.com/snabbdom/snabbdom.git\nLink: https://github.com/snabbdom/snabbdom#readme\n-----------\nsortablejs\nLicense: MIT\nLicense File: node_modules/sortablejs/LICENSE\nSource: git://github.com/SortableJS/Sortable.git\nLink: git://github.com/SortableJS/Sortable.git\n-----------\nsource-map-js\nLicense: BSD-3-Clause\nLicense File: node_modules/source-map-js/LICENSE\nCopyright: Copyright (c) 2009-2011, Mozilla Foundation and contributors\nAll rights reserved.\nSource: 7rulnik/source-map-js\nLink: https://github.com/7rulnik/source-map-js\n-----------\nsource-map-support\nLicense: MIT\nLicense File: node_modules/source-map-support/LICENSE.md\nCopyright: Copyright (c) 2014 Evan Wallace\nSource: https://github.com/evanw/node-source-map-support\nLink: https://github.com/evanw/node-source-map-support\n-----------\nsource-map\nLicense: BSD-3-Clause\nLicense File: node_modules/source-map/LICENSE\nCopyright: Copyright (c) 2009-2011, Mozilla Foundation and contributors\nAll rights reserved.\nSource: http://github.com/mozilla/source-map.git\nLink: https://github.com/mozilla/source-map\n-----------\nspdx-correct\nLicense: Apache-2.0\nLicense File: node_modules/spdx-correct/LICENSE\nSource: jslicense/spdx-correct.js\nLink: jslicense/spdx-correct.js\n-----------\nspdx-exceptions\nLicense: CC-BY-3.0\nSource: kemitchell/spdx-exceptions.json\nLink: kemitchell/spdx-exceptions.json\n-----------\nspdx-expression-parse\nLicense: MIT\nLicense File: node_modules/spdx-expression-parse/LICENSE\nCopyright: Copyright (c) 2015 Kyle E. Mitchell & other authors listed in AUTHORS\nSource: jslicense/spdx-expression-parse.js\nLink: jslicense/spdx-expression-parse.js\n-----------\nspdx-license-ids\nLicense: CC0-1.0\nSource: jslicense/spdx-license-ids\nLink: jslicense/spdx-license-ids\n-----------\nsprintf-js\nLicense: BSD-3-Clause\nLicense File: node_modules/sprintf-js/LICENSE\nCopyright: Copyright (c) 2007-2014, Alexandru Marasteanu <hello [at) alexei (dot] ro>\nAll rights reserved.\nSource: https://github.com/alexei/sprintf.js.git\nLink: https://github.com/alexei/sprintf.js.git\n-----------\nstack-utils\nLicense: MIT\nLicense File: node_modules/stack-utils/LICENSE.md\nCopyright: Copyright (c) 2016-2022 Isaac Z. Schlueter <*@***.**>, James Talmage <*****@*******.**> (github.com/jamestalmage), and Contributors\nSource: tapjs/stack-utils\nLink: tapjs/stack-utils\n-----------\nstop-iteration-iterator\nLicense: MIT\nLicense File: node_modules/stop-iteration-iterator/LICENSE\nCopyright: Copyright (c) 2023 Jordan Harband\nSource: git+https://github.com/ljharb/stop-iteration-iterator.git\nLink: https://github.com/ljharb/stop-iteration-iterator#readme\n-----------\nstring-length\nLicense: MIT\nLicense File: node_modules/string-length/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com)\nSource: sindresorhus/string-length\nLink: sindresorhus/string-length\n-----------\nstring-width\nLicense: MIT\nLicense File: node_modules/string-width-cjs/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/string-width\nLink: sindresorhus/string-width\n-----------\nstring-width\nLicense: MIT\nLicense File: node_modules/string-width/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/string-width\nLink: sindresorhus/string-width\n-----------\nstring.prototype.padend\nLicense: MIT\nLicense File: node_modules/string.prototype.padend/LICENSE\nCopyright: Copyright (c) 2015 EcmaScript Shims\nSource: git://github.com/es-shims/String.prototype.padEnd.git\nLink: git://github.com/es-shims/String.prototype.padEnd.git\n-----------\nstring.prototype.trim\nLicense: MIT\nLicense File: node_modules/string.prototype.trim/LICENSE\nCopyright: Copyright (c) 2015 Jordan Harband\nSource: git://github.com/es-shims/String.prototype.trim.git\nLink: git://github.com/es-shims/String.prototype.trim.git\n-----------\nstring.prototype.trimend\nLicense: MIT\nLicense File: node_modules/string.prototype.trimend/LICENSE\nCopyright: Copyright (c) 2017 Khaled Al-Ansari\nSource: git://github.com/es-shims/String.prototype.trimEnd.git\nLink: git://github.com/es-shims/String.prototype.trimEnd.git\n-----------\nstring.prototype.trimstart\nLicense: MIT\nLicense File: node_modules/string.prototype.trimstart/LICENSE\nCopyright: Copyright (c) 2017 Khaled Al-Ansari\nSource: git://github.com/es-shims/String.prototype.trimStart.git\nLink: git://github.com/es-shims/String.prototype.trimStart.git\n-----------\nstrip-ansi\nLicense: MIT\nLicense File: node_modules/strip-ansi-cjs/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: chalk/strip-ansi\nLink: chalk/strip-ansi\n-----------\nstrip-ansi\nLicense: MIT\nLicense File: node_modules/strip-ansi/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com)\nSource: chalk/strip-ansi\nLink: chalk/strip-ansi\n-----------\nstrip-bom\nLicense: MIT\nLicense File: node_modules/strip-bom/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/strip-bom\nLink: sindresorhus/strip-bom\n-----------\nstrip-final-newline\nLicense: MIT\nLicense File: node_modules/strip-final-newline/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/strip-final-newline\nLink: sindresorhus/strip-final-newline\n-----------\nstrip-json-comments\nLicense: MIT\nLicense File: node_modules/strip-json-comments/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com)\nSource: sindresorhus/strip-json-comments\nLink: sindresorhus/strip-json-comments\n-----------\nstyle-mod\nLicense: MIT\nLicense File: node_modules/style-mod/LICENSE\nCopyright: Copyright (C) 2018 by Marijn Haverbeke <******@*********.******> and others\nSource: git+https://github.com/marijnh/style-mod.git\nLink: git+https://github.com/marijnh/style-mod.git\n-----------\nsupports-color\nLicense: MIT\nLicense File: node_modules/supports-color/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: chalk/supports-color\nLink: chalk/supports-color\n-----------\nsupports-preserve-symlinks-flag\nLicense: MIT\nLicense File: node_modules/supports-preserve-symlinks-flag/LICENSE\nCopyright: Copyright (c) 2022 Inspect JS\nSource: git+https://github.com/inspect-js/node-supports-preserve-symlinks-flag.git\nLink: https://github.com/inspect-js/node-supports-preserve-symlinks-flag#readme\n-----------\nsymbol-tree\nLicense: MIT\nLicense File: node_modules/symbol-tree/LICENSE\nCopyright: Copyright (c) 2015 Joris van der Wel\nSource: https://github.com/jsdom/js-symbol-tree.git\nLink: https://github.com/jsdom/js-symbol-tree#symbol-tree\n-----------\nsynckit\nLicense: MIT\nLicense File: node_modules/synckit/LICENSE\nCopyright: Copyright (c) 2021 UnTS\nSource: https://github.com/un-ts/synckit.git\nLink: https://github.com/un-ts/synckit.git\n-----------\ntest-exclude\nLicense: ISC\nLicense File: node_modules/test-exclude/LICENSE.txt\nCopyright: Copyright (c) 2016, Contributors\nSource: git+https://github.com/istanbuljs/test-exclude.git\nLink: https://istanbul.js.org/\n-----------\ntldts-core\nLicense: MIT\nLicense File: node_modules/tldts-core/LICENSE\nCopyright: Copyright (c) 2017 Thomas Parisot, 2018 Rémi Berson\nSource: git+ssh://git@github.com/remusao/tldts.git\nLink: https://github.com/remusao/tldts#readme\n-----------\ntldts\nLicense: MIT\nLicense File: node_modules/tldts/LICENSE\nCopyright: Copyright (c) 2017 Thomas Parisot, 2018 Rémi Berson\nSource: git+ssh://git@github.com/remusao/tldts.git\nLink: https://github.com/remusao/tldts#readme\n-----------\ntmpl\nLicense: BSD-3-Clause\nLicense File: node_modules/tmpl/license\nCopyright: Copyright (c) 2014, Naitik Shah. All rights reserved.\nSource: https://github.com/daaku/nodejs-tmpl\nLink: https://github.com/daaku/nodejs-tmpl\n-----------\nto-regex-range\nLicense: MIT\nLicense File: node_modules/to-regex-range/LICENSE\nCopyright: Copyright (c) 2015-present, Jon Schlinkert.\nSource: micromatch/to-regex-range\nLink: https://github.com/micromatch/to-regex-range\n-----------\ntough-cookie\nLicense: BSD-3-Clause\nLicense File: node_modules/tough-cookie/LICENSE\nCopyright: Copyright (c) 2015, Salesforce.com, Inc.\nSource: git://github.com/salesforce/tough-cookie.git\nLink: https://github.com/salesforce/tough-cookie\n-----------\ntr46\nLicense: MIT\nLicense File: node_modules/tr46/LICENSE.md\nCopyright: Copyright (c) Sebastian Mayr\nSource: https://github.com/jsdom/tr46\nLink: https://github.com/jsdom/tr46\n-----------\nts-jest\nLicense: MIT\nLicense File: node_modules/ts-jest/LICENSE.md\nCopyright: Copyright (c) 2016-2025\nSource: git+https://github.com/kulshekhar/ts-jest.git\nLink: https://kulshekhar.github.io/ts-jest\n-----------\nts-node\nLicense: MIT\nLicense File: node_modules/ts-node/LICENSE\nCopyright: Copyright (c) 2014 Blake Embrey (*****@***********.***)\nSource: git://github.com/TypeStrong/ts-node.git\nLink: https://typestrong.org/ts-node\n-----------\ntsconfig-paths\nLicense: MIT\nLicense File: node_modules/tsconfig-paths/LICENSE\nCopyright: Copyright (c) 2016 Jonas Kello\nSource: https://github.com/dividab/tsconfig-paths\nLink: https://github.com/dividab/tsconfig-paths\n-----------\ntype-check\nLicense: MIT\nLicense File: node_modules/type-check/LICENSE\nCopyright: Copyright (c) George Zahariev\nSource: git://github.com/gkz/type-check.git\nLink: https://github.com/gkz/type-check\n-----------\ntype-detect\nLicense: MIT\nLicense File: node_modules/type-detect/LICENSE\nCopyright: Copyright (c) 2013 Jake Luer <****@***************.***> (http://alogicalparadox.com)\nSource: git+ssh://git@github.com/chaijs/type-detect.git\nLink: git+ssh://git@github.com/chaijs/type-detect.git\n-----------\ntype-fest\nLicense: (MIT OR CC0-1.0)\nLicense File: node_modules/type-fest/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (https:/sindresorhus.com)\nSource: sindresorhus/type-fest\nLink: sindresorhus/type-fest\n-----------\ntyped-array-buffer\nLicense: MIT\nLicense File: node_modules/typed-array-buffer/LICENSE\nCopyright: Copyright (c) 2023 Jordan Harband\nSource: git+https://github.com/inspect-js/typed-array-buffer.git\nLink: https://github.com/inspect-js/typed-array-buffer#readme\n-----------\ntyped-array-byte-length\nLicense: MIT\nLicense File: node_modules/typed-array-byte-length/LICENSE\nCopyright: Copyright (c) 2020 Inspect JS\nSource: git+https://github.com/inspect-js/typed-array-byte-length.git\nLink: https://github.com/inspect-js/typed-array-byte-length#readme\n-----------\ntyped-array-byte-offset\nLicense: MIT\nLicense File: node_modules/typed-array-byte-offset/LICENSE\nCopyright: Copyright (c) 2020 Inspect JS\nSource: git+https://github.com/inspect-js/typed-array-byte-offset.git\nLink: https://github.com/inspect-js/typed-array-byte-offset#readme\n-----------\ntyped-array-length\nLicense: MIT\nLicense File: node_modules/typed-array-length/LICENSE\nCopyright: Copyright (c) 2020 Inspect JS\nSource: git+https://github.com/inspect-js/typed-array-length.git\nLink: https://github.com/inspect-js/typed-array-length#readme\n-----------\ntypescript\nLicense: Apache-2.0\nLicense File: node_modules/typescript/LICENSE.txt\nSource: https://github.com/microsoft/TypeScript.git\nLink: https://www.typescriptlang.org/\n-----------\nuc.micro\nLicense: MIT\nLicense File: node_modules/uc.micro/LICENSE.txt\nSource: markdown-it/uc.micro\nLink: markdown-it/uc.micro\n-----------\nuglify-js\nLicense: BSD-2-Clause\nLicense File: node_modules/uglify-js/LICENSE\nCopyright: Copyright 2012-2024 (c) Mihai Bazon <*****.*****@*****.***>\nSource: mishoo/UglifyJS\nLink: mishoo/UglifyJS\n-----------\nunbox-primitive\nLicense: MIT\nLicense File: node_modules/unbox-primitive/LICENSE\nCopyright: Copyright (c) 2019 Jordan Harband\nSource: git+https://github.com/ljharb/unbox-primitive.git\nLink: https://github.com/ljharb/unbox-primitive#readme\n-----------\nundici-types\nLicense: MIT\nLicense File: node_modules/undici-types/LICENSE\nCopyright: Copyright (c) Matteo Collina and Undici contributors\nSource: git+https://github.com/nodejs/undici.git\nLink: https://undici.nodejs.org\n-----------\nunrs-resolver\nLicense: MIT\nSource: git+https://github.com/unrs/unrs-resolver.git\nLink: https://github.com/unrs/unrs-resolver#readme\n-----------\nupdate-browserslist-db\nLicense: MIT\nLicense File: node_modules/update-browserslist-db/LICENSE\nCopyright: Copyright 2022 Andrey Sitnik <******@******.**> and other contributors\nSource: browserslist/update-db\nLink: browserslist/update-db\n-----------\nuri-js\nLicense: BSD-2-Clause\nLicense File: node_modules/uri-js/LICENSE\nCopyright: Copyright 2011 Gary Court. All rights reserved.\nSource: http://github.com/garycourt/uri-js\nLink: https://github.com/garycourt/uri-js\n-----------\nv8-compile-cache-lib\nLicense: MIT\nLicense File: node_modules/v8-compile-cache-lib/LICENSE\nCopyright: Copyright (c) 2019 Andres Suarez\nSource: https://github.com/cspotcode/v8-compile-cache-lib.git\nLink: https://github.com/cspotcode/v8-compile-cache-lib.git\n-----------\nv8-to-istanbul\nLicense: ISC\nLicense File: node_modules/v8-to-istanbul/LICENSE.txt\nCopyright: Copyright (c) 2017, Contributors\nSource: istanbuljs/v8-to-istanbul\nLink: istanbuljs/v8-to-istanbul\n-----------\nvalidate-npm-package-license\nLicense: Apache-2.0\nLicense File: node_modules/validate-npm-package-license/LICENSE\nSource: kemitchell/validate-npm-package-license.js\nLink: kemitchell/validate-npm-package-license.js\n-----------\nw3c-keyname\nLicense: MIT\nLicense File: node_modules/w3c-keyname/LICENSE\nCopyright: Copyright (C) 2016 by Marijn Haverbeke <******@*********.******> and others\nSource: git+https://github.com/marijnh/w3c-keyname.git\nLink: https://github.com/marijnh/w3c-keyname#readme\n-----------\nw3c-xmlserializer\nLicense: MIT\nLicense File: node_modules/w3c-xmlserializer/LICENSE.md\nSource: jsdom/w3c-xmlserializer\nLink: jsdom/w3c-xmlserializer\n-----------\nwalker\nLicense: Apache-2.0\nLicense File: node_modules/walker/LICENSE\nCopyright: Copyright 2013 Naitik Shah\nSource: https://github.com/daaku/nodejs-walker\nLink: https://github.com/daaku/nodejs-walker\n-----------\nwebidl-conversions\nLicense: BSD-2-Clause\nLicense File: node_modules/webidl-conversions/LICENSE.md\nCopyright: Copyright (c) 2014, Domenic Denicola\nAll rights reserved.\nSource: jsdom/webidl-conversions\nLink: jsdom/webidl-conversions\n-----------\nwhatwg-encoding\nLicense: MIT\nLicense File: node_modules/whatwg-encoding/LICENSE.txt\nSource: jsdom/whatwg-encoding\nLink: jsdom/whatwg-encoding\n-----------\nwhatwg-mimetype\nLicense: MIT\nLicense File: node_modules/whatwg-mimetype/LICENSE.txt\nSource: jsdom/whatwg-mimetype\nLink: jsdom/whatwg-mimetype\n-----------\nwhatwg-url\nLicense: MIT\nLicense File: node_modules/whatwg-url/LICENSE.txt\nCopyright: Copyright (c) Sebastian Mayr\nSource: jsdom/whatwg-url\nLink: jsdom/whatwg-url\n-----------\nwhich-boxed-primitive\nLicense: MIT\nLicense File: node_modules/which-boxed-primitive/LICENSE\nCopyright: Copyright (c) 2019 Jordan Harband\nSource: git+https://github.com/inspect-js/which-boxed-primitive.git\nLink: https://github.com/inspect-js/which-boxed-primitive#readme\n-----------\nwhich-builtin-type\nLicense: MIT\nLicense File: node_modules/which-builtin-type/LICENSE\nCopyright: Copyright (c) 2020 ECMAScript Shims\nSource: git+https://github.com/inspect-js/which-builtin-type.git\nLink: https://github.com/inspect-js/which-builtin-type#readme\n-----------\nwhich-collection\nLicense: MIT\nLicense File: node_modules/which-collection/LICENSE\nCopyright: Copyright (c) 2019 Inspect JS\nSource: git+https://github.com/inspect-js/which-collection.git\nLink: https://github.com/inspect-js/which-collection#readme\n-----------\nwhich-module\nLicense: ISC\nLicense File: node_modules/which-module/LICENSE\nCopyright: Copyright (c) 2016, Contributors\nSource: git+https://github.com/nexdrew/which-module.git\nLink: https://github.com/nexdrew/which-module#readme\n-----------\nwhich-typed-array\nLicense: MIT\nLicense File: node_modules/which-typed-array/LICENSE\nCopyright: Copyright (c) 2015 Jordan Harband\nSource: git://github.com/inspect-js/which-typed-array.git\nLink: git://github.com/inspect-js/which-typed-array.git\n-----------\nwhich\nLicense: ISC\nLicense File: node_modules/which/LICENSE\nCopyright: Copyright (c) Isaac Z. Schlueter and Contributors\nSource: git://github.com/isaacs/node-which.git\nLink: git://github.com/isaacs/node-which.git\n-----------\nword-wrap\nLicense: MIT\nLicense File: node_modules/word-wrap/LICENSE\nCopyright: Copyright (c) 2014-2016, Jon Schlinkert\nSource: jonschlinkert/word-wrap\nLink: https://github.com/jonschlinkert/word-wrap\n-----------\nwordwrap\nLicense: MIT\nLicense File: node_modules/wordwrap/LICENSE\nSource: git://github.com/substack/node-wordwrap.git\nLink: git://github.com/substack/node-wordwrap.git\n-----------\nwrap-ansi\nLicense: MIT\nLicense File: node_modules/wrap-ansi-cjs/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com)\nSource: chalk/wrap-ansi\nLink: chalk/wrap-ansi\n-----------\nwrap-ansi\nLicense: MIT\nLicense File: node_modules/wrap-ansi/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: chalk/wrap-ansi\nLink: chalk/wrap-ansi\n-----------\nwrappy\nLicense: ISC\nLicense File: node_modules/wrappy/LICENSE\nCopyright: Copyright (c) Isaac Z. Schlueter and Contributors\nSource: https://github.com/npm/wrappy\nLink: https://github.com/npm/wrappy\n-----------\nwrite-file-atomic\nLicense: ISC\nLicense File: node_modules/write-file-atomic/LICENSE.md\nCopyright: Copyright (c) 2015, Rebecca Turner\nSource: https://github.com/npm/write-file-atomic.git\nLink: https://github.com/npm/write-file-atomic\n-----------\nws\nLicense: MIT\nLicense File: node_modules/ws/LICENSE\nCopyright: Copyright (c) 2011 Einar Otto Stangvik <*******@*****.***>\nCopyright (c) 2013 Arnout Kazemier and contributors\nCopyright (c) 2016 Luigi Pinca and contributors\nSource: git+https://github.com/websockets/ws.git\nLink: https://github.com/websockets/ws\n-----------\nxml-name-validator\nLicense: Apache-2.0\nLicense File: node_modules/xml-name-validator/LICENSE.txt\nSource: jsdom/xml-name-validator\nLink: jsdom/xml-name-validator\n-----------\nxmlchars\nLicense: MIT\nLicense File: node_modules/xmlchars/LICENSE\nSource: https://github.com/lddubeau/xmlchars.git\nLink: https://github.com/lddubeau/xmlchars.git\n-----------\ny18n\nLicense: ISC\nLicense File: node_modules/y18n/LICENSE\nCopyright: Copyright (c) 2015, Contributors\nSource: git@github.com:yargs/y18n.git\nLink: https://github.com/yargs/y18n\n-----------\nyallist\nLicense: ISC\nLicense File: node_modules/yallist/LICENSE\nCopyright: Copyright (c) Isaac Z. Schlueter and Contributors\nSource: git+https://github.com/isaacs/yallist.git\nLink: git+https://github.com/isaacs/yallist.git\n-----------\nyargs-parser\nLicense: ISC\nLicense File: node_modules/yargs-parser/LICENSE.txt\nCopyright: Copyright (c) 2016, Contributors\nSource: https://github.com/yargs/yargs-parser.git\nLink: https://github.com/yargs/yargs-parser.git\n-----------\nyargs\nLicense: MIT\nLicense File: node_modules/yargs/LICENSE\nCopyright: Copyright 2010 James Halliday (****@********.***)\nModified work Copyright 2014 Contributors (***@*****.***)\nSource: https://github.com/yargs/yargs.git\nLink: https://yargs.js.org/\n-----------\nyn\nLicense: MIT\nLicense File: node_modules/yn/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)\nSource: sindresorhus/yn\nLink: sindresorhus/yn\n-----------\nyocto-queue\nLicense: MIT\nLicense File: node_modules/yocto-queue/license\nCopyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com)\nSource: sindresorhus/yocto-queue\nLink: sindresorhus/yocto-queue\n-----------\n@ampproject/remapping\nLicense: Apache-2.0\nLicense File: node_modules/@ampproject/remapping/LICENSE\nSource: git+https://github.com/ampproject/remapping.git\nLink: git+https://github.com/ampproject/remapping.git\n-----------\n@asamuzakjp/css-color\nLicense: MIT\nLicense File: node_modules/@asamuzakjp/css-color/LICENSE\nCopyright: Copyright (c) 2024 asamuzaK (Kazz)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\nSource: git+https://github.com/asamuzaK/cssColor.git\nLink: https://github.com/asamuzaK/cssColor#readme\n-----------\n@babel/code-frame\nLicense: MIT\nLicense File: node_modules/@babel/code-frame/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://babel.dev/docs/en/next/babel-code-frame\n-----------\n@babel/compat-data\nLicense: MIT\nLicense File: node_modules/@babel/compat-data/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://github.com/babel/babel.git\n-----------\n@babel/core\nLicense: MIT\nLicense File: node_modules/@babel/core/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://babel.dev/docs/en/next/babel-core\n-----------\n@babel/generator\nLicense: MIT\nLicense File: node_modules/@babel/generator/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://babel.dev/docs/en/next/babel-generator\n-----------\n@babel/helper-compilation-targets\nLicense: MIT\nLicense File: node_modules/@babel/helper-compilation-targets/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://github.com/babel/babel.git\n-----------\n@babel/helper-globals\nLicense: MIT\nLicense File: node_modules/@babel/helper-globals/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://github.com/babel/babel.git\n-----------\n@babel/helper-module-imports\nLicense: MIT\nLicense File: node_modules/@babel/helper-module-imports/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://babel.dev/docs/en/next/babel-helper-module-imports\n-----------\n@babel/helper-module-transforms\nLicense: MIT\nLicense File: node_modules/@babel/helper-module-transforms/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://babel.dev/docs/en/next/babel-helper-module-transforms\n-----------\n@babel/helper-plugin-utils\nLicense: MIT\nLicense File: node_modules/@babel/helper-plugin-utils/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://babel.dev/docs/en/next/babel-helper-plugin-utils\n-----------\n@babel/helper-string-parser\nLicense: MIT\nLicense File: node_modules/@babel/helper-string-parser/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://babel.dev/docs/en/next/babel-helper-string-parser\n-----------\n@babel/helper-validator-identifier\nLicense: MIT\nLicense File: node_modules/@babel/helper-validator-identifier/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://github.com/babel/babel.git\n-----------\n@babel/helper-validator-option\nLicense: MIT\nLicense File: node_modules/@babel/helper-validator-option/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://github.com/babel/babel.git\n-----------\n@babel/helpers\nLicense: MIT\nLicense File: node_modules/@babel/helpers/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nCopyright (c) 2014-present, Facebook, Inc. (ONLY ./src/helpers/regenerator* files)\nSource: https://github.com/babel/babel.git\nLink: https://babel.dev/docs/en/next/babel-helpers\n-----------\n@babel/parser\nLicense: MIT\nLicense File: node_modules/@babel/parser/LICENSE\nCopyright: Copyright (C) 2012-2014 by various contributors (see AUTHORS)\nSource: https://github.com/babel/babel.git\nLink: https://babel.dev/docs/en/next/babel-parser\n-----------\n@babel/plugin-syntax-async-generators\nLicense: MIT\nLicense File: node_modules/@babel/plugin-syntax-async-generators/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel/tree/master/packages/babel-plugin-syntax-async-generators\nLink: https://github.com/babel/babel/tree/master/packages/babel-plugin-syntax-async-generators\n-----------\n@babel/plugin-syntax-bigint\nLicense: MIT\nLicense File: node_modules/@babel/plugin-syntax-bigint/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel/tree/master/packages/babel-plugin-syntax-bigint\nLink: https://github.com/babel/babel/tree/master/packages/babel-plugin-syntax-bigint\n-----------\n@babel/plugin-syntax-class-properties\nLicense: MIT\nLicense File: node_modules/@babel/plugin-syntax-class-properties/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://babel.dev/docs/en/next/babel-plugin-syntax-class-properties\n-----------\n@babel/plugin-syntax-class-static-block\nLicense: MIT\nLicense File: node_modules/@babel/plugin-syntax-class-static-block/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://babel.dev/docs/en/next/babel-plugin-syntax-class-static-block\n-----------\n@babel/plugin-syntax-import-attributes\nLicense: MIT\nLicense File: node_modules/@babel/plugin-syntax-import-attributes/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://github.com/babel/babel.git\n-----------\n@babel/plugin-syntax-import-meta\nLicense: MIT\nLicense File: node_modules/@babel/plugin-syntax-import-meta/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://github.com/babel/babel.git\n-----------\n@babel/plugin-syntax-json-strings\nLicense: MIT\nLicense File: node_modules/@babel/plugin-syntax-json-strings/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel/tree/master/packages/babel-plugin-syntax-json-strings\nLink: https://github.com/babel/babel/tree/master/packages/babel-plugin-syntax-json-strings\n-----------\n@babel/plugin-syntax-jsx\nLicense: MIT\nLicense File: node_modules/@babel/plugin-syntax-jsx/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://babel.dev/docs/en/next/babel-plugin-syntax-jsx\n-----------\n@babel/plugin-syntax-logical-assignment-operators\nLicense: MIT\nLicense File: node_modules/@babel/plugin-syntax-logical-assignment-operators/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://github.com/babel/babel.git\n-----------\n@babel/plugin-syntax-nullish-coalescing-operator\nLicense: MIT\nLicense File: node_modules/@babel/plugin-syntax-nullish-coalescing-operator/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel/tree/master/packages/babel-plugin-syntax-nullish-coalescing-operator\nLink: https://github.com/babel/babel/tree/master/packages/babel-plugin-syntax-nullish-coalescing-operator\n-----------\n@babel/plugin-syntax-numeric-separator\nLicense: MIT\nLicense File: node_modules/@babel/plugin-syntax-numeric-separator/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://github.com/babel/babel.git\n-----------\n@babel/plugin-syntax-object-rest-spread\nLicense: MIT\nLicense File: node_modules/@babel/plugin-syntax-object-rest-spread/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel/tree/master/packages/babel-plugin-syntax-object-rest-spread\nLink: https://github.com/babel/babel/tree/master/packages/babel-plugin-syntax-object-rest-spread\n-----------\n@babel/plugin-syntax-optional-catch-binding\nLicense: MIT\nLicense File: node_modules/@babel/plugin-syntax-optional-catch-binding/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel/tree/master/packages/babel-plugin-syntax-optional-catch-binding\nLink: https://github.com/babel/babel/tree/master/packages/babel-plugin-syntax-optional-catch-binding\n-----------\n@babel/plugin-syntax-optional-chaining\nLicense: MIT\nLicense File: node_modules/@babel/plugin-syntax-optional-chaining/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel/tree/master/packages/babel-plugin-syntax-optional-chaining\nLink: https://github.com/babel/babel/tree/master/packages/babel-plugin-syntax-optional-chaining\n-----------\n@babel/plugin-syntax-private-property-in-object\nLicense: MIT\nLicense File: node_modules/@babel/plugin-syntax-private-property-in-object/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://babel.dev/docs/en/next/babel-plugin-syntax-private-property-in-object\n-----------\n@babel/plugin-syntax-top-level-await\nLicense: MIT\nLicense File: node_modules/@babel/plugin-syntax-top-level-await/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://babel.dev/docs/en/next/babel-plugin-syntax-top-level-await\n-----------\n@babel/plugin-syntax-typescript\nLicense: MIT\nLicense File: node_modules/@babel/plugin-syntax-typescript/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://babel.dev/docs/en/next/babel-plugin-syntax-typescript\n-----------\n@babel/template\nLicense: MIT\nLicense File: node_modules/@babel/template/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://babel.dev/docs/en/next/babel-template\n-----------\n@babel/traverse\nLicense: MIT\nLicense File: node_modules/@babel/traverse/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://babel.dev/docs/en/next/babel-traverse\n-----------\n@babel/types\nLicense: MIT\nLicense File: node_modules/@babel/types/LICENSE\nCopyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors\nSource: https://github.com/babel/babel.git\nLink: https://babel.dev/docs/en/next/babel-types\n-----------\n@bcoe/v8-coverage\nLicense: MIT\nLicense File: node_modules/@bcoe/v8-coverage/LICENSE.md\nSource: git://github.com/demurgos/v8-coverage.git\nLink: https://demurgos.github.io/v8-coverage\n-----------\n@codemirror/autocomplete\nLicense: MIT\nLicense File: node_modules/@codemirror/autocomplete/LICENSE\nCopyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/codemirror/autocomplete.git\nLink: https://github.com/codemirror/autocomplete.git\n-----------\n@codemirror/commands\nLicense: MIT\nLicense File: node_modules/@codemirror/commands/LICENSE\nCopyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/codemirror/commands.git\nLink: https://github.com/codemirror/commands.git\n-----------\n@codemirror/lang-css\nLicense: MIT\nLicense File: node_modules/@codemirror/lang-css/LICENSE\nCopyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/codemirror/lang-css.git\nLink: https://github.com/codemirror/lang-css.git\n-----------\n@codemirror/lang-html\nLicense: MIT\nLicense File: node_modules/@codemirror/lang-html/LICENSE\nCopyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/codemirror/lang-html.git\nLink: https://github.com/codemirror/lang-html.git\n-----------\n@codemirror/lang-javascript\nLicense: MIT\nLicense File: node_modules/@codemirror/lang-javascript/LICENSE\nCopyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/codemirror/lang-javascript.git\nLink: https://github.com/codemirror/lang-javascript.git\n-----------\n@codemirror/lang-json\nLicense: MIT\nLicense File: node_modules/@codemirror/lang-json/LICENSE\nCopyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/codemirror/lang-json.git\nLink: https://github.com/codemirror/lang-json.git\n-----------\n@codemirror/lang-markdown\nLicense: MIT\nLicense File: node_modules/@codemirror/lang-markdown/LICENSE\nCopyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/codemirror/lang-markdown.git\nLink: https://github.com/codemirror/lang-markdown.git\n-----------\n@codemirror/lang-php\nLicense: MIT\nLicense File: node_modules/@codemirror/lang-php/LICENSE\nCopyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/codemirror/lang-php.git\nLink: https://github.com/codemirror/lang-php.git\n-----------\n@codemirror/lang-xml\nLicense: MIT\nLicense File: node_modules/@codemirror/lang-xml/LICENSE\nCopyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/codemirror/lang-xml.git\nLink: https://github.com/codemirror/lang-xml.git\n-----------\n@codemirror/language\nLicense: MIT\nLicense File: node_modules/@codemirror/language/LICENSE\nCopyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/codemirror/language.git\nLink: https://github.com/codemirror/language.git\n-----------\n@codemirror/legacy-modes\nLicense: MIT\nLicense File: node_modules/@codemirror/legacy-modes/LICENSE\nCopyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/codemirror/legacy-modes.git\nLink: https://github.com/codemirror/legacy-modes.git\n-----------\n@codemirror/lint\nLicense: MIT\nLicense File: node_modules/@codemirror/lint/LICENSE\nCopyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/codemirror/lint.git\nLink: https://github.com/codemirror/lint.git\n-----------\n@codemirror/search\nLicense: MIT\nLicense File: node_modules/@codemirror/search/LICENSE\nCopyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/codemirror/search.git\nLink: https://github.com/codemirror/search.git\n-----------\n@codemirror/state\nLicense: MIT\nLicense File: node_modules/@codemirror/state/LICENSE\nCopyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/codemirror/state.git\nLink: https://github.com/codemirror/state.git\n-----------\n@codemirror/theme-one-dark\nLicense: MIT\nLicense File: node_modules/@codemirror/theme-one-dark/LICENSE\nCopyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/codemirror/theme-one-dark.git\nLink: https://github.com/codemirror/theme-one-dark.git\n-----------\n@codemirror/view\nLicense: MIT\nLicense File: node_modules/@codemirror/view/LICENSE\nCopyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/codemirror/view.git\nLink: https://github.com/codemirror/view.git\n-----------\n@cspotcode/source-map-support\nLicense: MIT\nLicense File: node_modules/@cspotcode/source-map-support/LICENSE.md\nCopyright: Copyright (c) 2014 Evan Wallace\nSource: https://github.com/cspotcode/node-source-map-support\nLink: https://github.com/cspotcode/node-source-map-support\n-----------\n@csstools/color-helpers\nLicense: MIT-0\nLicense File: node_modules/@csstools/color-helpers/LICENSE.md\nSource: git+https://github.com/csstools/postcss-plugins.git\nLink: https://github.com/csstools/postcss-plugins/tree/main/packages/color-helpers#readme\n-----------\n@csstools/css-calc\nLicense: MIT\nLicense File: node_modules/@csstools/css-calc/LICENSE.md\nCopyright: Copyright 2022 Romain Menke, Antonio Laguna <*******@******.**>\nSource: git+https://github.com/csstools/postcss-plugins.git\nLink: https://github.com/csstools/postcss-plugins/tree/main/packages/css-calc#readme\n-----------\n@csstools/css-color-parser\nLicense: MIT\nLicense File: node_modules/@csstools/css-color-parser/LICENSE.md\nCopyright: Copyright 2022 Romain Menke, Antonio Laguna <*******@******.**>\nSource: git+https://github.com/csstools/postcss-plugins.git\nLink: https://github.com/csstools/postcss-plugins/tree/main/packages/css-color-parser#readme\n-----------\n@csstools/css-parser-algorithms\nLicense: MIT\nLicense File: node_modules/@csstools/css-parser-algorithms/LICENSE.md\nCopyright: Copyright 2022 Romain Menke, Antonio Laguna <*******@******.**>\nSource: git+https://github.com/csstools/postcss-plugins.git\nLink: https://github.com/csstools/postcss-plugins/tree/main/packages/css-parser-algorithms#readme\n-----------\n@csstools/css-tokenizer\nLicense: MIT\nLicense File: node_modules/@csstools/css-tokenizer/LICENSE.md\nCopyright: Copyright 2022 Romain Menke, Antonio Laguna <*******@******.**>\nSource: git+https://github.com/csstools/postcss-plugins.git\nLink: https://github.com/csstools/postcss-plugins/tree/main/packages/css-tokenizer#readme\n-----------\n@esbuild/linux-x64\nLicense: MIT\nSource: git+https://github.com/evanw/esbuild.git\nLink: git+https://github.com/evanw/esbuild.git\n-----------\n@eslint-community/eslint-utils\nLicense: MIT\nLicense File: node_modules/@eslint-community/eslint-utils/LICENSE\nCopyright: Copyright (c) 2018 Toru Nagashima\nSource: https://github.com/eslint-community/eslint-utils\nLink: https://github.com/eslint-community/eslint-utils#readme\n-----------\n@eslint-community/regexpp\nLicense: MIT\nLicense File: node_modules/@eslint-community/regexpp/LICENSE\nCopyright: Copyright (c) 2018 Toru Nagashima\nSource: https://github.com/eslint-community/regexpp\nLink: https://github.com/eslint-community/regexpp#readme\n-----------\n@eslint/config-array\nLicense: Apache-2.0\nLicense File: node_modules/@eslint/config-array/LICENSE\nSource: git+https://github.com/eslint/rewrite.git\nLink: https://github.com/eslint/rewrite/tree/main/packages/config-array#readme\n-----------\n@eslint/config-helpers\nLicense: Apache-2.0\nLicense File: node_modules/@eslint/config-helpers/LICENSE\nSource: git+https://github.com/eslint/rewrite.git\nLink: https://github.com/eslint/rewrite/tree/main/packages/config-helpers#readme\n-----------\n@eslint/core\nLicense: Apache-2.0\nLicense File: node_modules/@eslint/core/LICENSE\nSource: git+https://github.com/eslint/rewrite.git\nLink: https://github.com/eslint/rewrite/tree/main/packages/core#readme\n-----------\n@eslint/eslintrc\nLicense: MIT\nLicense File: node_modules/@eslint/eslintrc/LICENSE\nSource: eslint/eslintrc\nLink: https://github.com/eslint/eslintrc#readme\n-----------\n@eslint/js\nLicense: MIT\nLicense File: node_modules/@eslint/js/LICENSE\nSource: https://github.com/eslint/eslint.git\nLink: https://eslint.org\n-----------\n@eslint/object-schema\nLicense: Apache-2.0\nLicense File: node_modules/@eslint/object-schema/LICENSE\nSource: git+https://github.com/eslint/rewrite.git\nLink: https://github.com/eslint/rewrite/tree/main/packages/object-schema#readme\n-----------\n@eslint/plugin-kit\nLicense: Apache-2.0\nLicense File: node_modules/@eslint/plugin-kit/LICENSE\nSource: git+https://github.com/eslint/rewrite.git\nLink: https://github.com/eslint/rewrite/tree/main/packages/plugin-kit#readme\n-----------\n@humanfs/core\nLicense: Apache-2.0\nLicense File: node_modules/@humanfs/core/LICENSE\nSource: git+https://github.com/humanwhocodes/humanfs.git\nLink: https://github.com/humanwhocodes/humanfs#readme\n-----------\n@humanfs/node\nLicense: Apache-2.0\nLicense File: node_modules/@humanfs/node/LICENSE\nSource: git+https://github.com/humanwhocodes/humanfs.git\nLink: https://github.com/humanwhocodes/humanfs#readme\n-----------\n@humanwhocodes/module-importer\nLicense: Apache-2.0\nLicense File: node_modules/@humanwhocodes/module-importer/LICENSE\nSource: git+https://github.com/humanwhocodes/module-importer.git\nLink: git+https://github.com/humanwhocodes/module-importer.git\n-----------\n@humanwhocodes/retry\nLicense: Apache-2.0\nLicense File: node_modules/@humanwhocodes/retry/LICENSE\nSource: git+https://github.com/humanwhocodes/retry.git\nLink: git+https://github.com/humanwhocodes/retry.git\n-----------\n@isaacs/cliui\nLicense: ISC\nLicense File: node_modules/@isaacs/cliui/LICENSE.txt\nCopyright: Copyright (c) 2015, Contributors\nSource: yargs/cliui\nLink: yargs/cliui\n-----------\n@istanbuljs/load-nyc-config\nLicense: ISC\nLicense File: node_modules/@istanbuljs/load-nyc-config/LICENSE\nCopyright: Copyright (c) 2019, Contributors\nSource: git+https://github.com/istanbuljs/load-nyc-config.git\nLink: https://github.com/istanbuljs/load-nyc-config#readme\n-----------\n@istanbuljs/schema\nLicense: MIT\nLicense File: node_modules/@istanbuljs/schema/LICENSE\nCopyright: Copyright (c) 2019 CFWare, LLC\nSource: git+https://github.com/istanbuljs/schema.git\nLink: https://github.com/istanbuljs/schema#readme\n-----------\n@jest/console\nLicense: MIT\nLicense File: node_modules/@jest/console/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\n@jest/core\nLicense: MIT\nLicense File: node_modules/@jest/core/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://jestjs.io/\n-----------\n@jest/diff-sequences\nLicense: MIT\nLicense File: node_modules/@jest/diff-sequences/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\n@jest/environment-jsdom-abstract\nLicense: MIT\nLicense File: node_modules/@jest/environment-jsdom-abstract/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\n@jest/environment\nLicense: MIT\nLicense File: node_modules/@jest/environment/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\n@jest/expect-utils\nLicense: MIT\nLicense File: node_modules/@jest/expect-utils/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\n@jest/expect\nLicense: MIT\nLicense File: node_modules/@jest/expect/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\n@jest/fake-timers\nLicense: MIT\nLicense File: node_modules/@jest/fake-timers/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\n@jest/get-type\nLicense: MIT\nLicense File: node_modules/@jest/get-type/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\n@jest/globals\nLicense: MIT\nLicense File: node_modules/@jest/globals/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\n@jest/pattern\nLicense: MIT\nLicense File: node_modules/@jest/pattern/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\n@jest/reporters\nLicense: MIT\nLicense File: node_modules/@jest/reporters/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://jestjs.io/\n-----------\n@jest/schemas\nLicense: MIT\nLicense File: node_modules/@jest/schemas/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\n@jest/snapshot-utils\nLicense: MIT\nLicense File: node_modules/@jest/snapshot-utils/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\n@jest/source-map\nLicense: MIT\nLicense File: node_modules/@jest/source-map/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\n@jest/test-result\nLicense: MIT\nLicense File: node_modules/@jest/test-result/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\n@jest/test-sequencer\nLicense: MIT\nLicense File: node_modules/@jest/test-sequencer/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\n@jest/transform\nLicense: MIT\nLicense File: node_modules/@jest/transform/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\n@jest/types\nLicense: MIT\nLicense File: node_modules/@jest/types/LICENSE\nCopyright: Copyright (c) Meta Platforms, Inc. and affiliates.\nSource: https://github.com/jestjs/jest.git\nLink: https://github.com/jestjs/jest.git\n-----------\n@jridgewell/gen-mapping\nLicense: MIT\nLicense File: node_modules/@jridgewell/gen-mapping/LICENSE\nCopyright: Copyright 2024 Justin Ridgewell <******@*********.****>\nSource: git+https://github.com/jridgewell/sourcemaps.git\nLink: https://github.com/jridgewell/sourcemaps/tree/main/packages/gen-mapping\n-----------\n@jridgewell/resolve-uri\nLicense: MIT\nLicense File: node_modules/@jridgewell/resolve-uri/LICENSE\nCopyright: Copyright 2019 Justin Ridgewell <**********@******.***>\nSource: https://github.com/jridgewell/resolve-uri\nLink: https://github.com/jridgewell/resolve-uri\n-----------\n@jridgewell/sourcemap-codec\nLicense: MIT\nLicense File: node_modules/@jridgewell/sourcemap-codec/LICENSE\nCopyright: Copyright 2024 Justin Ridgewell <******@*********.****>\nSource: git+https://github.com/jridgewell/sourcemaps.git\nLink: https://github.com/jridgewell/sourcemaps/tree/main/packages/sourcemap-codec\n-----------\n@jridgewell/trace-mapping\nLicense: MIT\nLicense File: node_modules/@jridgewell/trace-mapping/LICENSE\nCopyright: Copyright 2024 Justin Ridgewell <******@*********.****>\nSource: git+https://github.com/jridgewell/sourcemaps.git\nLink: https://github.com/jridgewell/sourcemaps/tree/main/packages/trace-mapping\n-----------\n@lezer/common\nLicense: MIT\nLicense File: node_modules/@lezer/common/LICENSE\nCopyright: Copyright (C) 2018 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/lezer-parser/common.git\nLink: https://github.com/lezer-parser/common.git\n-----------\n@lezer/css\nLicense: MIT\nLicense File: node_modules/@lezer/css/LICENSE\nCopyright: Copyright (C) 2018 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/lezer-parser/css.git\nLink: https://github.com/lezer-parser/css.git\n-----------\n@lezer/generator\nLicense: MIT\nLicense File: node_modules/@lezer/generator/LICENSE\nCopyright: Copyright (C) 2018 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/lezer-parser/generator.git\nLink: https://github.com/lezer-parser/generator.git\n-----------\n@lezer/highlight\nLicense: MIT\nLicense File: node_modules/@lezer/highlight/LICENSE\nCopyright: Copyright (C) 2018 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/lezer-parser/highlight.git\nLink: https://github.com/lezer-parser/highlight.git\n-----------\n@lezer/html\nLicense: MIT\nLicense File: node_modules/@lezer/html/LICENSE\nCopyright: Copyright (C) 2018 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/lezer-parser/html.git\nLink: https://github.com/lezer-parser/html.git\n-----------\n@lezer/javascript\nLicense: MIT\nLicense File: node_modules/@lezer/javascript/LICENSE\nCopyright: Copyright (C) 2018 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/lezer-parser/javascript.git\nLink: https://github.com/lezer-parser/javascript.git\n-----------\n@lezer/json\nLicense: MIT\nLicense File: node_modules/@lezer/json/LICENSE\nCopyright: Copyright (C) 2020 by Marijn Haverbeke <******@*********.******>, Arun Srinivasan <*******@*****.***>, and others\nSource: https://github.com/lezer-parser/json.git\nLink: https://github.com/lezer-parser/json.git\n-----------\n@lezer/lr\nLicense: MIT\nLicense File: node_modules/@lezer/lr/LICENSE\nCopyright: Copyright (C) 2018 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/lezer-parser/lr.git\nLink: https://github.com/lezer-parser/lr.git\n-----------\n@lezer/markdown\nLicense: MIT\nLicense File: node_modules/@lezer/markdown/LICENSE\nCopyright: Copyright (C) 2020 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/lezer-parser/markdown.git\nLink: https://github.com/lezer-parser/markdown.git\n-----------\n@lezer/php\nLicense: MIT\nLicense File: node_modules/@lezer/php/LICENSE\nCopyright: Copyright (C) 2018 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/lezer-parser/php.git\nLink: https://github.com/lezer-parser/php.git\n-----------\n@lezer/xml\nLicense: MIT\nLicense File: node_modules/@lezer/xml/LICENSE\nCopyright: Copyright (C) 2018 by Marijn Haverbeke <******@*********.******> and others\nSource: https://github.com/lezer-parser/xml.git\nLink: https://github.com/lezer-parser/xml.git\n-----------\n@marijn/find-cluster-break\nLicense: MIT\nLicense File: node_modules/@marijn/find-cluster-break/LICENSE\nCopyright: Copyright (C) 2024 by Marijn Haverbeke <******@*********.******>\nSource: git+https://github.com/marijnh/find-cluster-break.git\nLink: https://github.com/marijnh/find-cluster-break#readme\n-----------\n@parcel/watcher-linux-x64-glibc\nLicense: MIT\nLicense File: node_modules/@parcel/watcher-linux-x64-glibc/LICENSE\nCopyright: Copyright (c) 2017-present Devon Govett\nSource: https://github.com/parcel-bundler/watcher.git\nLink: https://github.com/parcel-bundler/watcher.git\n-----------\n@parcel/watcher-linux-x64-musl\nLicense: MIT\nLicense File: node_modules/@parcel/watcher-linux-x64-musl/LICENSE\nCopyright: Copyright (c) 2017-present Devon Govett\nSource: https://github.com/parcel-bundler/watcher.git\nLink: https://github.com/parcel-bundler/watcher.git\n-----------\n@parcel/watcher\nLicense: MIT\nLicense File: node_modules/@parcel/watcher/LICENSE\nCopyright: Copyright (c) 2017-present Devon Govett\nSource: https://github.com/parcel-bundler/watcher.git\nLink: https://github.com/parcel-bundler/watcher.git\n-----------\n@pkgjs/parseargs\nLicense: MIT\nLicense File: node_modules/@pkgjs/parseargs/LICENSE\nSource: git@github.com:pkgjs/parseargs.git\nLink: https://github.com/pkgjs/parseargs#readme\n-----------\n@pkgr/core\nLicense: MIT\nSource: git+https://github.com/un-ts/pkgr.git\nLink: https://github.com/un-ts/pkgr/blob/master/packages/core\n-----------\n@rtsao/scc\nLicense: MIT\nLicense File: node_modules/@rtsao/scc/LICENSE\nCopyright: Copyright (c) 2019 Ryan Tsao\nSource: rtsao/scc\nLink: rtsao/scc\n-----------\n@sinclair/typebox\nLicense: MIT\nLicense File: node_modules/@sinclair/typebox/license\nSource: https://github.com/sinclairzx81/typebox\nLink: https://github.com/sinclairzx81/typebox\n-----------\n@sinonjs/commons\nLicense: BSD-3-Clause\nLicense File: node_modules/@sinonjs/commons/LICENSE\nCopyright: Copyright (c) 2018, Sinon.JS\nAll rights reserved.\nSource: git+https://github.com/sinonjs/commons.git\nLink: https://github.com/sinonjs/commons#readme\n-----------\n@sinonjs/fake-timers\nLicense: BSD-3-Clause\nLicense File: node_modules/@sinonjs/fake-timers/LICENSE\nCopyright: Copyright (c) 2010-2014, Christian Johansen, *********@*********.**. All rights reserved.\nSource: git+https://github.com/sinonjs/fake-timers.git\nLink: https://github.com/sinonjs/fake-timers\n-----------\n@ssddanbrown/codemirror-lang-smarty\nLicense: MIT\nLicense File: node_modules/@ssddanbrown/codemirror-lang-smarty/LICENSE\nCopyright: Copyright (C) 2023 by Dan Brown, Marijn Haverbeke and others\n-----------\n@ssddanbrown/codemirror-lang-twig\nLicense: MIT\nLicense File: node_modules/@ssddanbrown/codemirror-lang-twig/LICENSE\nCopyright: Copyright (C) 2023 by Dan Brown, Marijn Haverbeke and others\n-----------\n@tsconfig/node10\nLicense: MIT\nLicense File: node_modules/@tsconfig/node10/LICENSE\nCopyright: Copyright (c) Microsoft Corporation.\nSource: https://github.com/tsconfig/bases.git\nLink: https://github.com/tsconfig/bases.git\n-----------\n@tsconfig/node12\nLicense: MIT\nLicense File: node_modules/@tsconfig/node12/LICENSE\nCopyright: Copyright (c) Microsoft Corporation.\nSource: https://github.com/tsconfig/bases.git\nLink: https://github.com/tsconfig/bases.git\n-----------\n@tsconfig/node14\nLicense: MIT\nLicense File: node_modules/@tsconfig/node14/LICENSE\nCopyright: Copyright (c) Microsoft Corporation.\nSource: https://github.com/tsconfig/bases.git\nLink: https://github.com/tsconfig/bases.git\n-----------\n@tsconfig/node16\nLicense: MIT\nLicense File: node_modules/@tsconfig/node16/LICENSE\nCopyright: Copyright (c) Microsoft Corporation.\nSource: https://github.com/tsconfig/bases.git\nLink: https://github.com/tsconfig/bases.git\n-----------\n@types/babel__core\nLicense: MIT\nLicense File: node_modules/@types/babel__core/LICENSE\nCopyright: Copyright (c) Microsoft Corporation.\nSource: https://github.com/DefinitelyTyped/DefinitelyTyped.git\nLink: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/babel__core\n-----------\n@types/babel__generator\nLicense: MIT\nLicense File: node_modules/@types/babel__generator/LICENSE\nCopyright: Copyright (c) Microsoft Corporation.\nSource: https://github.com/DefinitelyTyped/DefinitelyTyped.git\nLink: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/babel__generator\n-----------\n@types/babel__template\nLicense: MIT\nLicense File: node_modules/@types/babel__template/LICENSE\nCopyright: Copyright (c) Microsoft Corporation.\nSource: https://github.com/DefinitelyTyped/DefinitelyTyped.git\nLink: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/babel__template\n-----------\n@types/babel__traverse\nLicense: MIT\nLicense File: node_modules/@types/babel__traverse/LICENSE\nCopyright: Copyright (c) Microsoft Corporation.\nSource: https://github.com/DefinitelyTyped/DefinitelyTyped.git\nLink: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/babel__traverse\n-----------\n@types/estree\nLicense: MIT\nLicense File: node_modules/@types/estree/LICENSE\nCopyright: Copyright (c) Microsoft Corporation.\nSource: https://github.com/DefinitelyTyped/DefinitelyTyped.git\nLink: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/estree\n-----------\n@types/istanbul-lib-coverage\nLicense: MIT\nLicense File: node_modules/@types/istanbul-lib-coverage/LICENSE\nCopyright: Copyright (c) Microsoft Corporation.\nSource: https://github.com/DefinitelyTyped/DefinitelyTyped.git\nLink: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/istanbul-lib-coverage\n-----------\n@types/istanbul-lib-report\nLicense: MIT\nLicense File: node_modules/@types/istanbul-lib-report/LICENSE\nCopyright: Copyright (c) Microsoft Corporation.\nSource: https://github.com/DefinitelyTyped/DefinitelyTyped.git\nLink: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/istanbul-lib-report\n-----------\n@types/istanbul-reports\nLicense: MIT\nLicense File: node_modules/@types/istanbul-reports/LICENSE\nCopyright: Copyright (c) Microsoft Corporation.\nSource: https://github.com/DefinitelyTyped/DefinitelyTyped.git\nLink: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/istanbul-reports\n-----------\n@types/jest\nLicense: MIT\nLicense File: node_modules/@types/jest/LICENSE\nCopyright: Copyright (c) Microsoft Corporation.\nSource: https://github.com/DefinitelyTyped/DefinitelyTyped.git\nLink: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/jest\n-----------\n@types/jsdom\nLicense: MIT\nLicense File: node_modules/@types/jsdom/LICENSE\nCopyright: Copyright (c) Microsoft Corporation.\nSource: https://github.com/DefinitelyTyped/DefinitelyTyped.git\nLink: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/jsdom\n-----------\n@types/json-schema\nLicense: MIT\nLicense File: node_modules/@types/json-schema/LICENSE\nCopyright: Copyright (c) Microsoft Corporation.\nSource: https://github.com/DefinitelyTyped/DefinitelyTyped.git\nLink: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/json-schema\n-----------\n@types/json5\nLicense: MIT\nSource: https://www.github.com/DefinitelyTyped/DefinitelyTyped.git\nLink: https://www.github.com/DefinitelyTyped/DefinitelyTyped.git\n-----------\n@types/linkify-it\nLicense: MIT\nLicense File: node_modules/@types/linkify-it/LICENSE\nCopyright: Copyright (c) Microsoft Corporation.\nSource: https://github.com/DefinitelyTyped/DefinitelyTyped.git\nLink: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/linkify-it\n-----------\n@types/markdown-it\nLicense: MIT\nLicense File: node_modules/@types/markdown-it/LICENSE\nCopyright: Copyright (c) Microsoft Corporation.\nSource: https://github.com/DefinitelyTyped/DefinitelyTyped.git\nLink: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/markdown-it\n-----------\n@types/mdurl\nLicense: MIT\nLicense File: node_modules/@types/mdurl/LICENSE\nCopyright: Copyright (c) Microsoft Corporation.\nSource: https://github.com/DefinitelyTyped/DefinitelyTyped.git\nLink: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/mdurl\n-----------\n@types/node\nLicense: MIT\nLicense File: node_modules/@types/node/LICENSE\nCopyright: Copyright (c) Microsoft Corporation.\nSource: https://github.com/DefinitelyTyped/DefinitelyTyped.git\nLink: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node\n-----------\n@types/sortablejs\nLicense: MIT\nLicense File: node_modules/@types/sortablejs/LICENSE\nCopyright: Copyright (c) Microsoft Corporation.\nSource: https://github.com/DefinitelyTyped/DefinitelyTyped.git\nLink: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/sortablejs\n-----------\n@types/stack-utils\nLicense: MIT\nLicense File: node_modules/@types/stack-utils/LICENSE\nCopyright: Copyright (c) Microsoft Corporation.\nSource: https://github.com/DefinitelyTyped/DefinitelyTyped.git\nLink: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/stack-utils\n-----------\n@types/tough-cookie\nLicense: MIT\nLicense File: node_modules/@types/tough-cookie/LICENSE\nCopyright: Copyright (c) Microsoft Corporation.\nSource: https://github.com/DefinitelyTyped/DefinitelyTyped.git\nLink: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/tough-cookie\n-----------\n@types/yargs-parser\nLicense: MIT\nLicense File: node_modules/@types/yargs-parser/LICENSE\nCopyright: Copyright (c) Microsoft Corporation.\nSource: https://github.com/DefinitelyTyped/DefinitelyTyped.git\nLink: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/yargs-parser\n-----------\n@types/yargs\nLicense: MIT\nLicense File: node_modules/@types/yargs/LICENSE\nCopyright: Copyright (c) Microsoft Corporation.\nSource: https://github.com/DefinitelyTyped/DefinitelyTyped.git\nLink: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/yargs\n-----------\n@ungap/structured-clone\nLicense: ISC\nLicense File: node_modules/@ungap/structured-clone/LICENSE\nCopyright: Copyright (c) 2021, Andrea Giammarchi, @WebReflection\nSource: git+https://github.com/ungap/structured-clone.git\nLink: https://github.com/ungap/structured-clone#readme\n-----------\n@unrs/resolver-binding-linux-x64-gnu\nLicense: MIT\nSource: git+https://github.com/unrs/unrs-resolver.git\nLink: https://github.com/unrs/unrs-resolver#readme\n-----------\n@unrs/resolver-binding-linux-x64-musl\nLicense: MIT\nSource: git+https://github.com/unrs/unrs-resolver.git\nLink: https://github.com/unrs/unrs-resolver#readme\n"
  },
  {
    "path": "dev/licensing/php-library-licenses.txt",
    "content": "aws/aws-crt-php\nLicense: Apache-2.0\nLicense File: vendor/aws/aws-crt-php/LICENSE\nSource: https://github.com/awslabs/aws-crt-php.git\nLink: https://github.com/awslabs/aws-crt-php\n-----------\naws/aws-sdk-php\nLicense: Apache-2.0\nLicense File: vendor/aws/aws-sdk-php/LICENSE\nSource: https://github.com/aws/aws-sdk-php.git\nLink: https://aws.amazon.com/sdk-for-php\n-----------\nbacon/bacon-qr-code\nLicense: BSD-2-Clause\nLicense File: vendor/bacon/bacon-qr-code/LICENSE\nCopyright: Copyright (c) 2017-present, Ben Scholzen 'DASPRiD'\nAll rights reserved.\nSource: https://github.com/Bacon/BaconQrCode.git\nLink: https://github.com/Bacon/BaconQrCode\n-----------\nbrick/math\nLicense: MIT\nLicense File: vendor/brick/math/LICENSE\nCopyright: Copyright (c) 2013-present Benjamin Morel\nSource: https://github.com/brick/math.git\nLink: https://github.com/brick/math.git\n-----------\ncarbonphp/carbon-doctrine-types\nLicense: MIT\nLicense File: vendor/carbonphp/carbon-doctrine-types/LICENSE\nCopyright: Copyright (c) 2023 Carbon\nSource: https://github.com/CarbonPHP/carbon-doctrine-types.git\nLink: https://github.com/CarbonPHP/carbon-doctrine-types.git\n-----------\ndasprid/enum\nLicense: BSD-2-Clause\nLicense File: vendor/dasprid/enum/LICENSE\nCopyright: Copyright (c) 2017, Ben Scholzen 'DASPRiD'\nAll rights reserved.\nSource: https://github.com/DASPRiD/Enum.git\nLink: https://github.com/DASPRiD/Enum.git\n-----------\ndflydev/dot-access-data\nLicense: MIT\nLicense File: vendor/dflydev/dot-access-data/LICENSE\nCopyright: Copyright (c) 2012 Dragonfly Development Inc.\nSource: https://github.com/dflydev/dflydev-dot-access-data.git\nLink: https://github.com/dflydev/dflydev-dot-access-data\n-----------\ndoctrine/inflector\nLicense: MIT\nLicense File: vendor/doctrine/inflector/LICENSE\nCopyright: Copyright (c) 2006-2015 Doctrine Project\nSource: https://github.com/doctrine/inflector.git\nLink: https://www.doctrine-project.org/projects/inflector.html\n-----------\ndoctrine/lexer\nLicense: MIT\nLicense File: vendor/doctrine/lexer/LICENSE\nCopyright: Copyright (c) 2006-2018 Doctrine Project\nSource: https://github.com/doctrine/lexer.git\nLink: https://www.doctrine-project.org/projects/lexer.html\n-----------\ndompdf/dompdf\nLicense: LGPL-2.1\nLicense File: vendor/dompdf/dompdf/LICENSE.LGPL\nCopyright: Copyright (C) 1991, 1999 Free Software Foundation, Inc.\nSource: https://github.com/dompdf/dompdf.git\nLink: https://github.com/dompdf/dompdf\n-----------\ndompdf/php-font-lib\nLicense: LGPL-2.1-or-later\nLicense File: vendor/dompdf/php-font-lib/LICENSE\nCopyright: Copyright (C) 1991, 1999 Free Software Foundation, Inc.\nSource: https://github.com/dompdf/php-font-lib.git\nLink: https://github.com/dompdf/php-font-lib\n-----------\ndompdf/php-svg-lib\nLicense: LGPL-3.0-or-later\nLicense File: vendor/dompdf/php-svg-lib/LICENSE\nCopyright: Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\nSource: https://github.com/dompdf/php-svg-lib.git\nLink: https://github.com/dompdf/php-svg-lib\n-----------\ndragonmantank/cron-expression\nLicense: MIT\nLicense File: vendor/dragonmantank/cron-expression/LICENSE\nCopyright: Copyright (c) 2011 Michael Dowling <*********@*****.***>, 2016 Chris Tankersley <*****@***********.***>, and contributors\nSource: https://github.com/dragonmantank/cron-expression.git\nLink: https://github.com/dragonmantank/cron-expression.git\n-----------\negulias/email-validator\nLicense: MIT\nLicense File: vendor/egulias/email-validator/LICENSE\nCopyright: Copyright (c) 2013-2023 Eduardo Gulias Davis\nSource: https://github.com/egulias/EmailValidator.git\nLink: https://github.com/egulias/EmailValidator\n-----------\nezyang/htmlpurifier\nLicense: LGPL-2.1-or-later\nLicense File: vendor/ezyang/htmlpurifier/LICENSE\nCopyright: Copyright (C) 1991, 1999 Free Software Foundation, Inc.\nSource: https://github.com/ezyang/htmlpurifier.git\nLink: http://htmlpurifier.org/\n-----------\nfirebase/php-jwt\nLicense: BSD-3-Clause\nLicense File: vendor/firebase/php-jwt/LICENSE\nCopyright: Copyright (c) 2011, Neuman Vong\nSource: https://github.com/firebase/php-jwt.git\nLink: https://github.com/firebase/php-jwt\n-----------\nfruitcake/php-cors\nLicense: MIT\nLicense File: vendor/fruitcake/php-cors/LICENSE\nCopyright: Copyright (c) 2013-2017 Alexander <***.*****@*****.***>\nCopyright (c) 2017-2022 Barryvdh <********@*****.***>\nSource: https://github.com/fruitcake/php-cors.git\nLink: https://github.com/fruitcake/php-cors\n-----------\ngraham-campbell/result-type\nLicense: MIT\nLicense File: vendor/graham-campbell/result-type/LICENSE\nCopyright: Copyright (c) 2020-2024 Graham Campbell <*****@**********.**.**>\nSource: https://github.com/GrahamCampbell/Result-Type.git\nLink: https://github.com/GrahamCampbell/Result-Type.git\n-----------\nguzzlehttp/guzzle\nLicense: MIT\nLicense File: vendor/guzzlehttp/guzzle/LICENSE\nCopyright: Copyright (c) 2011 Michael Dowling <*********@*****.***>\nCopyright (c) 2012 Jeremy Lindblom <**********@*****.***>\nCopyright (c) 2014 Graham Campbell <*****@**********.**.**>\nCopyright (c) 2015 Márk Sági-Kazár <****.*********@*****.***>\nCopyright (c) 2015 Tobias Schultze <*********@**********.**>\nCopyright (c) 2016 Tobias Nyholm <******.******@*****.***>\nCopyright (c) 2016 George Mponos <*******@*****.***>\nSource: https://github.com/guzzle/guzzle.git\nLink: https://github.com/guzzle/guzzle.git\n-----------\nguzzlehttp/promises\nLicense: MIT\nLicense File: vendor/guzzlehttp/promises/LICENSE\nCopyright: Copyright (c) 2015 Michael Dowling <*********@*****.***>\nCopyright (c) 2015 Graham Campbell <*****@**********.**.**>\nCopyright (c) 2017 Tobias Schultze <*********@**********.**>\nCopyright (c) 2020 Tobias Nyholm <******.******@*****.***>\nSource: https://github.com/guzzle/promises.git\nLink: https://github.com/guzzle/promises.git\n-----------\nguzzlehttp/psr7\nLicense: MIT\nLicense File: vendor/guzzlehttp/psr7/LICENSE\nCopyright: Copyright (c) 2015 Michael Dowling <*********@*****.***>\nCopyright (c) 2015 Márk Sági-Kazár <****.*********@*****.***>\nCopyright (c) 2015 Graham Campbell <*****@**********.**.**>\nCopyright (c) 2016 Tobias Schultze <*********@**********.**>\nCopyright (c) 2016 George Mponos <*******@*****.***>\nCopyright (c) 2018 Tobias Nyholm <******.******@*****.***>\nSource: https://github.com/guzzle/psr7.git\nLink: https://github.com/guzzle/psr7.git\n-----------\nguzzlehttp/uri-template\nLicense: MIT\nLicense File: vendor/guzzlehttp/uri-template/LICENSE\nCopyright: Copyright (c) 2014 Michael Dowling <*********@*****.***>\nCopyright (c) 2020 George Mponos <*******@*****.***>\nCopyright (c) 2020 Graham Campbell <*****@**********.**.**>\nSource: https://github.com/guzzle/uri-template.git\nLink: https://github.com/guzzle/uri-template.git\n-----------\nintervention/gif\nLicense: MIT\nLicense File: vendor/intervention/gif/LICENSE\nCopyright: Copyright (c) 2020-present Oliver Vogel\nSource: https://github.com/Intervention/gif.git\nLink: https://github.com/intervention/gif\n-----------\nintervention/image\nLicense: MIT\nLicense File: vendor/intervention/image/LICENSE\nCopyright: Copyright (c) 2013-present Oliver Vogel\nSource: https://github.com/Intervention/image.git\nLink: https://image.intervention.io\n-----------\nknplabs/knp-snappy\nLicense: MIT\nLicense File: vendor/knplabs/knp-snappy/LICENSE\nCopyright: Copyright (c) 2010 Matthieu Bontemps\nSource: https://github.com/KnpLabs/snappy.git\nLink: http://github.com/KnpLabs/snappy\n-----------\nlaravel/framework\nLicense: MIT\nLicense File: vendor/laravel/framework/LICENSE.md\nCopyright: Copyright (c) Taylor Otwell\nSource: https://github.com/laravel/framework.git\nLink: https://laravel.com\n-----------\nlaravel/prompts\nLicense: MIT\nLicense File: vendor/laravel/prompts/LICENSE.md\nCopyright: Copyright (c) Taylor Otwell\nSource: https://github.com/laravel/prompts.git\nLink: https://github.com/laravel/prompts.git\n-----------\nlaravel/serializable-closure\nLicense: MIT\nLicense File: vendor/laravel/serializable-closure/LICENSE.md\nCopyright: Copyright (c) Taylor Otwell\nSource: https://github.com/laravel/serializable-closure.git\nLink: https://github.com/laravel/serializable-closure.git\n-----------\nlaravel/socialite\nLicense: MIT\nLicense File: vendor/laravel/socialite/LICENSE.md\nCopyright: Copyright (c) Taylor Otwell\nSource: https://github.com/laravel/socialite.git\nLink: https://laravel.com\n-----------\nlaravel/tinker\nLicense: MIT\nLicense File: vendor/laravel/tinker/LICENSE.md\nCopyright: Copyright (c) Taylor Otwell\nSource: https://github.com/laravel/tinker.git\nLink: https://github.com/laravel/tinker.git\n-----------\nleague/commonmark\nLicense: BSD-3-Clause\nLicense File: vendor/league/commonmark/LICENSE\nCopyright: Copyright (c) 2014-2022, Colin O'Dell. All rights reserved. Some code based on commonmark.js (copyright 2014-2018, John MacFarlane) and commonmark-java (copyright 2015-2016, Atlassian Pty Ltd)\nSource: https://github.com/thephpleague/commonmark.git\nLink: https://commonmark.thephpleague.com\n-----------\nleague/config\nLicense: BSD-3-Clause\nLicense File: vendor/league/config/LICENSE.md\nCopyright: Copyright (c) 2022, Colin O'Dell. All rights reserved.\nSource: https://github.com/thephpleague/config.git\nLink: https://config.thephpleague.com\n-----------\nleague/flysystem\nLicense: MIT\nLicense File: vendor/league/flysystem/LICENSE\nCopyright: Copyright (c) 2013-2026 Frank de Jonge\nSource: https://github.com/thephpleague/flysystem.git\nLink: https://github.com/thephpleague/flysystem.git\n-----------\nleague/flysystem-aws-s3-v3\nLicense: MIT\nLicense File: vendor/league/flysystem-aws-s3-v3/LICENSE\nCopyright: Copyright (c) 2013-2026 Frank de Jonge\nSource: https://github.com/thephpleague/flysystem-aws-s3-v3.git\nLink: https://github.com/thephpleague/flysystem-aws-s3-v3.git\n-----------\nleague/flysystem-local\nLicense: MIT\nLicense File: vendor/league/flysystem-local/LICENSE\nCopyright: Copyright (c) 2013-2026 Frank de Jonge\nSource: https://github.com/thephpleague/flysystem-local.git\nLink: https://github.com/thephpleague/flysystem-local.git\n-----------\nleague/html-to-markdown\nLicense: MIT\nLicense File: vendor/league/html-to-markdown/LICENSE\nCopyright: Copyright (c) 2015 Colin O'Dell; Originally created by Nick Cernis\nSource: https://github.com/thephpleague/html-to-markdown.git\nLink: https://github.com/thephpleague/html-to-markdown\n-----------\nleague/mime-type-detection\nLicense: MIT\nLicense File: vendor/league/mime-type-detection/LICENSE\nCopyright: Copyright (c) 2013-2023 Frank de Jonge\nSource: https://github.com/thephpleague/mime-type-detection.git\nLink: https://github.com/thephpleague/mime-type-detection.git\n-----------\nleague/oauth1-client\nLicense: MIT\nLicense File: vendor/league/oauth1-client/LICENSE\nCopyright: Copyright (c) 2013 Ben Corlett <**********@**.***>\nSource: https://github.com/thephpleague/oauth1-client.git\nLink: https://github.com/thephpleague/oauth1-client.git\n-----------\nleague/oauth2-client\nLicense: MIT\nLicense File: vendor/league/oauth2-client/LICENSE\nCopyright: Copyright (c) 2013-2023 Alex Bilbie <*****@**********.***>\nSource: https://github.com/thephpleague/oauth2-client.git\nLink: https://github.com/thephpleague/oauth2-client.git\n-----------\nleague/uri\nLicense: MIT\nLicense File: vendor/league/uri/LICENSE\nCopyright: Copyright (c) 2015 ignace nyamagana butera\nSource: https://github.com/thephpleague/uri.git\nLink: https://uri.thephpleague.com\n-----------\nleague/uri-interfaces\nLicense: MIT\nLicense File: vendor/league/uri-interfaces/LICENSE\nCopyright: Copyright (c) 2015 ignace nyamagana butera\nSource: https://github.com/thephpleague/uri-interfaces.git\nLink: https://uri.thephpleague.com\n-----------\nmasterminds/html5\nLicense: MIT\nLicense File: vendor/masterminds/html5/LICENSE.txt\nCopyright: Copyright (c) 2013 The Authors of HTML5-PHP\nSource: https://github.com/Masterminds/html5-php.git\nLink: http://masterminds.github.io/html5-php\n-----------\nmonolog/monolog\nLicense: MIT\nLicense File: vendor/monolog/monolog/LICENSE\nCopyright: Copyright (c) 2011-2020 Jordi Boggiano\nSource: https://github.com/Seldaek/monolog.git\nLink: https://github.com/Seldaek/monolog\n-----------\nmtdowling/jmespath.php\nLicense: MIT\nLicense File: vendor/mtdowling/jmespath.php/LICENSE\nCopyright: Copyright (c) 2014 Michael Dowling, https://github.com/mtdowling\nSource: https://github.com/jmespath/jmespath.php.git\nLink: https://github.com/jmespath/jmespath.php.git\n-----------\nnesbot/carbon\nLicense: MIT\nLicense File: vendor/nesbot/carbon/LICENSE\nCopyright: Copyright (C) Brian Nesbitt\nSource: https://github.com/CarbonPHP/carbon.git\nLink: https://carbonphp.github.io/carbon/\n-----------\nnette/schema\nLicense: BSD-3-Clause GPL-2.0-only GPL-3.0-only\nLicense File: vendor/nette/schema/license.md\nCopyright: Copyright (c) 2004, 2014 David Grudl (https://davidgrudl.com)\nAll rights reserved.\nSource: https://github.com/nette/schema.git\nLink: https://nette.org\n-----------\nnette/utils\nLicense: BSD-3-Clause GPL-2.0-only GPL-3.0-only\nLicense File: vendor/nette/utils/license.md\nCopyright: Copyright (c) 2004, 2014 David Grudl (https://davidgrudl.com)\nAll rights reserved.\nSource: https://github.com/nette/utils.git\nLink: https://nette.org\n-----------\nnikic/php-parser\nLicense: BSD-3-Clause\nLicense File: vendor/nikic/php-parser/LICENSE\nCopyright: Copyright (c) 2011, Nikita Popov\nAll rights reserved.\nSource: https://github.com/nikic/PHP-Parser.git\nLink: https://github.com/nikic/PHP-Parser.git\n-----------\nnunomaduro/termwind\nLicense: MIT\nLicense File: vendor/nunomaduro/termwind/LICENSE.md\nCopyright: Copyright (c) Nuno Maduro <***********@*****.***>\nSource: https://github.com/nunomaduro/termwind.git\nLink: https://github.com/nunomaduro/termwind.git\n-----------\nonelogin/php-saml\nLicense: MIT\nLicense File: vendor/onelogin/php-saml/LICENSE\nCopyright: Copyright (c) 2010-2022 OneLogin, Inc.\nSource: https://github.com/SAML-Toolkits/php-saml.git\nLink: https://github.com/SAML-Toolkits/php-saml\n-----------\nparagonie/constant_time_encoding\nLicense: MIT\nLicense File: vendor/paragonie/constant_time_encoding/LICENSE.txt\nCopyright: Copyright (c) 2016 - 2022 Paragon Initiative Enterprises\nSource: https://github.com/paragonie/constant_time_encoding.git\nLink: https://github.com/paragonie/constant_time_encoding.git\n-----------\nparagonie/random_compat\nLicense: MIT\nLicense File: vendor/paragonie/random_compat/LICENSE\nCopyright: Copyright (c) 2015 Paragon Initiative Enterprises\nSource: https://github.com/paragonie/random_compat.git\nLink: https://github.com/paragonie/random_compat.git\n-----------\nphpoption/phpoption\nLicense: Apache-2.0\nLicense File: vendor/phpoption/phpoption/LICENSE\nSource: https://github.com/schmittjoh/php-option.git\nLink: https://github.com/schmittjoh/php-option.git\n-----------\nphpseclib/phpseclib\nLicense: MIT\nLicense File: vendor/phpseclib/phpseclib/LICENSE\nCopyright: Copyright (c) 2011-2019 TerraFrost and other contributors\nSource: https://github.com/phpseclib/phpseclib.git\nLink: http://phpseclib.sourceforge.net\n-----------\npragmarx/google2fa\nLicense: MIT\nLicense File: vendor/pragmarx/google2fa/LICENSE.md\nCopyright: Copyright 2014-2018 Phil, Antonio Carlos Ribeiro and All Contributors\nSource: https://github.com/antonioribeiro/google2fa.git\nLink: https://github.com/antonioribeiro/google2fa.git\n-----------\npredis/predis\nLicense: MIT\nLicense File: vendor/predis/predis/LICENSE\nCopyright: Copyright (c) 2009-2020 Daniele Alessandri (original work)\nCopyright (c) 2021-2024 Till Krüss (modified work)\nSource: https://github.com/predis/predis.git\nLink: http://github.com/predis/predis\n-----------\npsr/clock\nLicense: MIT\nLicense File: vendor/psr/clock/LICENSE\nCopyright: Copyright (c) 2017 PHP Framework Interoperability Group\nSource: https://github.com/php-fig/clock.git\nLink: https://github.com/php-fig/clock\n-----------\npsr/container\nLicense: MIT\nLicense File: vendor/psr/container/LICENSE\nCopyright: Copyright (c) 2013-2016 container-interop\nCopyright (c) 2016 PHP Framework Interoperability Group\nSource: https://github.com/php-fig/container.git\nLink: https://github.com/php-fig/container\n-----------\npsr/event-dispatcher\nLicense: MIT\nLicense File: vendor/psr/event-dispatcher/LICENSE\nCopyright: Copyright (c) 2018 PHP-FIG\nSource: https://github.com/php-fig/event-dispatcher.git\nLink: https://github.com/php-fig/event-dispatcher.git\n-----------\npsr/http-client\nLicense: MIT\nLicense File: vendor/psr/http-client/LICENSE\nCopyright: Copyright (c) 2017 PHP Framework Interoperability Group\nSource: https://github.com/php-fig/http-client.git\nLink: https://github.com/php-fig/http-client\n-----------\npsr/http-factory\nLicense: MIT\nLicense File: vendor/psr/http-factory/LICENSE\nCopyright: Copyright (c) 2018 PHP-FIG\nSource: https://github.com/php-fig/http-factory.git\nLink: https://github.com/php-fig/http-factory.git\n-----------\npsr/http-message\nLicense: MIT\nLicense File: vendor/psr/http-message/LICENSE\nCopyright: Copyright (c) 2014 PHP Framework Interoperability Group\nSource: https://github.com/php-fig/http-message.git\nLink: https://github.com/php-fig/http-message\n-----------\npsr/log\nLicense: MIT\nLicense File: vendor/psr/log/LICENSE\nCopyright: Copyright (c) 2012 PHP Framework Interoperability Group\nSource: https://github.com/php-fig/log.git\nLink: https://github.com/php-fig/log\n-----------\npsr/simple-cache\nLicense: MIT\nLicense File: vendor/psr/simple-cache/LICENSE.md\nCopyright: Copyright (c) 2016 PHP Framework Interoperability Group\nSource: https://github.com/php-fig/simple-cache.git\nLink: https://github.com/php-fig/simple-cache.git\n-----------\npsy/psysh\nLicense: MIT\nLicense File: vendor/psy/psysh/LICENSE\nCopyright: Copyright (c) 2012-2026 Justin Hileman\nSource: https://github.com/bobthecow/psysh.git\nLink: https://psysh.org\n-----------\nralouphie/getallheaders\nLicense: MIT\nLicense File: vendor/ralouphie/getallheaders/LICENSE\nCopyright: Copyright (c) 2014 Ralph Khattar\nSource: https://github.com/ralouphie/getallheaders.git\nLink: https://github.com/ralouphie/getallheaders.git\n-----------\nramsey/collection\nLicense: MIT\nLicense File: vendor/ramsey/collection/LICENSE\nCopyright: Copyright (c) 2015-2022 Ben Ramsey <***@*********.***>\nSource: https://github.com/ramsey/collection.git\nLink: https://github.com/ramsey/collection.git\n-----------\nramsey/uuid\nLicense: MIT\nLicense File: vendor/ramsey/uuid/LICENSE\nCopyright: Copyright (c) 2012-2025 Ben Ramsey <***@*********.***>\nSource: https://github.com/ramsey/uuid.git\nLink: https://github.com/ramsey/uuid.git\n-----------\nrobrichards/xmlseclibs\nLicense: BSD-3-Clause\nLicense File: vendor/robrichards/xmlseclibs/LICENSE\nCopyright: Copyright (c) 2007-2024, Robert Richards <*********@*********.***>.\nSource: https://github.com/robrichards/xmlseclibs.git\nLink: https://github.com/robrichards/xmlseclibs\n-----------\nsabberworm/php-css-parser\nLicense: MIT\nLicense File: vendor/sabberworm/php-css-parser/LICENSE\nCopyright: Copyright (c) 2011 Raphael Schweikert, https://www.sabberworm.com/\nSource: https://github.com/MyIntervals/PHP-CSS-Parser.git\nLink: https://www.sabberworm.com/blog/2010/6/10/php-css-parser\n-----------\nsocialiteproviders/discord\nLicense: MIT\nSource: https://github.com/SocialiteProviders/Discord.git\nLink: https://github.com/SocialiteProviders/Discord.git\n-----------\nsocialiteproviders/gitlab\nLicense: MIT\nSource: https://github.com/SocialiteProviders/GitLab.git\nLink: https://github.com/SocialiteProviders/GitLab.git\n-----------\nsocialiteproviders/manager\nLicense: MIT\nLicense File: vendor/socialiteproviders/manager/LICENSE\nCopyright: Copyright (c) 2015 Andy Wendt\nSource: https://github.com/SocialiteProviders/Manager.git\nLink: https://socialiteproviders.com\n-----------\nsocialiteproviders/microsoft-azure\nLicense: MIT\nSource: https://github.com/SocialiteProviders/Microsoft-Azure.git\nLink: https://github.com/SocialiteProviders/Microsoft-Azure.git\n-----------\nsocialiteproviders/okta\nLicense: MIT\nSource: https://github.com/SocialiteProviders/Okta.git\nLink: https://github.com/SocialiteProviders/Okta.git\n-----------\nsocialiteproviders/twitch\nLicense: MIT\nSource: https://github.com/SocialiteProviders/Twitch.git\nLink: https://github.com/SocialiteProviders/Twitch.git\n-----------\nssddanbrown/htmldiff\nLicense: MIT\nLicense File: vendor/ssddanbrown/htmldiff/license.md\nCopyright: Copyright (c) 2024 Nathan Herald, Rohland de Charmoy, Dan Brown\nSource: https://codeberg.org/danb/HtmlDiff\nLink: https://codeberg.org/danb/HtmlDiff\n-----------\nsymfony/clock\nLicense: MIT\nLicense File: vendor/symfony/clock/LICENSE\nCopyright: Copyright (c) 2022-present Fabien Potencier\nSource: https://github.com/symfony/clock.git\nLink: https://symfony.com\n-----------\nsymfony/console\nLicense: MIT\nLicense File: vendor/symfony/console/LICENSE\nCopyright: Copyright (c) 2004-present Fabien Potencier\nSource: https://github.com/symfony/console.git\nLink: https://symfony.com\n-----------\nsymfony/css-selector\nLicense: MIT\nLicense File: vendor/symfony/css-selector/LICENSE\nCopyright: Copyright (c) 2004-present Fabien Potencier\nSource: https://github.com/symfony/css-selector.git\nLink: https://symfony.com\n-----------\nsymfony/deprecation-contracts\nLicense: MIT\nLicense File: vendor/symfony/deprecation-contracts/LICENSE\nCopyright: Copyright (c) 2020-present Fabien Potencier\nSource: https://github.com/symfony/deprecation-contracts.git\nLink: https://symfony.com\n-----------\nsymfony/error-handler\nLicense: MIT\nLicense File: vendor/symfony/error-handler/LICENSE\nCopyright: Copyright (c) 2019-present Fabien Potencier\nSource: https://github.com/symfony/error-handler.git\nLink: https://symfony.com\n-----------\nsymfony/event-dispatcher\nLicense: MIT\nLicense File: vendor/symfony/event-dispatcher/LICENSE\nCopyright: Copyright (c) 2004-present Fabien Potencier\nSource: https://github.com/symfony/event-dispatcher.git\nLink: https://symfony.com\n-----------\nsymfony/event-dispatcher-contracts\nLicense: MIT\nLicense File: vendor/symfony/event-dispatcher-contracts/LICENSE\nCopyright: Copyright (c) 2018-present Fabien Potencier\nSource: https://github.com/symfony/event-dispatcher-contracts.git\nLink: https://symfony.com\n-----------\nsymfony/filesystem\nLicense: MIT\nLicense File: vendor/symfony/filesystem/LICENSE\nCopyright: Copyright (c) 2004-present Fabien Potencier\nSource: https://github.com/symfony/filesystem.git\nLink: https://symfony.com\n-----------\nsymfony/finder\nLicense: MIT\nLicense File: vendor/symfony/finder/LICENSE\nCopyright: Copyright (c) 2004-present Fabien Potencier\nSource: https://github.com/symfony/finder.git\nLink: https://symfony.com\n-----------\nsymfony/http-foundation\nLicense: MIT\nLicense File: vendor/symfony/http-foundation/LICENSE\nCopyright: Copyright (c) 2004-present Fabien Potencier\nSource: https://github.com/symfony/http-foundation.git\nLink: https://symfony.com\n-----------\nsymfony/http-kernel\nLicense: MIT\nLicense File: vendor/symfony/http-kernel/LICENSE\nCopyright: Copyright (c) 2004-present Fabien Potencier\nSource: https://github.com/symfony/http-kernel.git\nLink: https://symfony.com\n-----------\nsymfony/mailer\nLicense: MIT\nLicense File: vendor/symfony/mailer/LICENSE\nCopyright: Copyright (c) 2019-present Fabien Potencier\nSource: https://github.com/symfony/mailer.git\nLink: https://symfony.com\n-----------\nsymfony/mime\nLicense: MIT\nLicense File: vendor/symfony/mime/LICENSE\nCopyright: Copyright (c) 2010-present Fabien Potencier\nSource: https://github.com/symfony/mime.git\nLink: https://symfony.com\n-----------\nsymfony/polyfill-ctype\nLicense: MIT\nLicense File: vendor/symfony/polyfill-ctype/LICENSE\nCopyright: Copyright (c) 2018-present Fabien Potencier\nSource: https://github.com/symfony/polyfill-ctype.git\nLink: https://symfony.com\n-----------\nsymfony/polyfill-intl-grapheme\nLicense: MIT\nLicense File: vendor/symfony/polyfill-intl-grapheme/LICENSE\nCopyright: Copyright (c) 2015-present Fabien Potencier\nSource: https://github.com/symfony/polyfill-intl-grapheme.git\nLink: https://symfony.com\n-----------\nsymfony/polyfill-intl-idn\nLicense: MIT\nLicense File: vendor/symfony/polyfill-intl-idn/LICENSE\nCopyright: Copyright (c) 2018-present Fabien Potencier and Trevor Rowbotham <******.*********@**.**>\nSource: https://github.com/symfony/polyfill-intl-idn.git\nLink: https://symfony.com\n-----------\nsymfony/polyfill-intl-normalizer\nLicense: MIT\nLicense File: vendor/symfony/polyfill-intl-normalizer/LICENSE\nCopyright: Copyright (c) 2015-present Fabien Potencier\nSource: https://github.com/symfony/polyfill-intl-normalizer.git\nLink: https://symfony.com\n-----------\nsymfony/polyfill-mbstring\nLicense: MIT\nLicense File: vendor/symfony/polyfill-mbstring/LICENSE\nCopyright: Copyright (c) 2015-present Fabien Potencier\nSource: https://github.com/symfony/polyfill-mbstring.git\nLink: https://symfony.com\n-----------\nsymfony/polyfill-php80\nLicense: MIT\nLicense File: vendor/symfony/polyfill-php80/LICENSE\nCopyright: Copyright (c) 2020-present Fabien Potencier\nSource: https://github.com/symfony/polyfill-php80.git\nLink: https://symfony.com\n-----------\nsymfony/polyfill-php83\nLicense: MIT\nLicense File: vendor/symfony/polyfill-php83/LICENSE\nCopyright: Copyright (c) 2022-present Fabien Potencier\nSource: https://github.com/symfony/polyfill-php83.git\nLink: https://symfony.com\n-----------\nsymfony/polyfill-php84\nLicense: MIT\nLicense File: vendor/symfony/polyfill-php84/LICENSE\nCopyright: Copyright (c) 2024-present Fabien Potencier\nSource: https://github.com/symfony/polyfill-php84.git\nLink: https://symfony.com\n-----------\nsymfony/polyfill-php85\nLicense: MIT\nLicense File: vendor/symfony/polyfill-php85/LICENSE\nCopyright: Copyright (c) 2025-present Fabien Potencier\nSource: https://github.com/symfony/polyfill-php85.git\nLink: https://symfony.com\n-----------\nsymfony/polyfill-uuid\nLicense: MIT\nLicense File: vendor/symfony/polyfill-uuid/LICENSE\nCopyright: Copyright (c) 2018-present Fabien Potencier\nSource: https://github.com/symfony/polyfill-uuid.git\nLink: https://symfony.com\n-----------\nsymfony/process\nLicense: MIT\nLicense File: vendor/symfony/process/LICENSE\nCopyright: Copyright (c) 2004-present Fabien Potencier\nSource: https://github.com/symfony/process.git\nLink: https://symfony.com\n-----------\nsymfony/routing\nLicense: MIT\nLicense File: vendor/symfony/routing/LICENSE\nCopyright: Copyright (c) 2004-present Fabien Potencier\nSource: https://github.com/symfony/routing.git\nLink: https://symfony.com\n-----------\nsymfony/service-contracts\nLicense: MIT\nLicense File: vendor/symfony/service-contracts/LICENSE\nCopyright: Copyright (c) 2018-present Fabien Potencier\nSource: https://github.com/symfony/service-contracts.git\nLink: https://symfony.com\n-----------\nsymfony/string\nLicense: MIT\nLicense File: vendor/symfony/string/LICENSE\nCopyright: Copyright (c) 2019-present Fabien Potencier\nSource: https://github.com/symfony/string.git\nLink: https://symfony.com\n-----------\nsymfony/translation\nLicense: MIT\nLicense File: vendor/symfony/translation/LICENSE\nCopyright: Copyright (c) 2004-present Fabien Potencier\nSource: https://github.com/symfony/translation.git\nLink: https://symfony.com\n-----------\nsymfony/translation-contracts\nLicense: MIT\nLicense File: vendor/symfony/translation-contracts/LICENSE\nCopyright: Copyright (c) 2018-present Fabien Potencier\nSource: https://github.com/symfony/translation-contracts.git\nLink: https://symfony.com\n-----------\nsymfony/uid\nLicense: MIT\nLicense File: vendor/symfony/uid/LICENSE\nCopyright: Copyright (c) 2020-present Fabien Potencier\nSource: https://github.com/symfony/uid.git\nLink: https://symfony.com\n-----------\nsymfony/var-dumper\nLicense: MIT\nLicense File: vendor/symfony/var-dumper/LICENSE\nCopyright: Copyright (c) 2014-present Fabien Potencier\nSource: https://github.com/symfony/var-dumper.git\nLink: https://symfony.com\n-----------\nthecodingmachine/safe\nLicense: MIT\nLicense File: vendor/thecodingmachine/safe/LICENSE\nCopyright: Copyright (c) 2018 TheCodingMachine\nSource: https://github.com/thecodingmachine/safe.git\nLink: https://github.com/thecodingmachine/safe.git\n-----------\ntijsverkoyen/css-to-inline-styles\nLicense: BSD-3-Clause\nLicense File: vendor/tijsverkoyen/css-to-inline-styles/LICENSE.md\nCopyright: Copyright (c) Tijs Verkoyen. All rights reserved.\nSource: https://github.com/tijsverkoyen/CssToInlineStyles.git\nLink: https://github.com/tijsverkoyen/CssToInlineStyles\n-----------\nvlucas/phpdotenv\nLicense: BSD-3-Clause\nLicense File: vendor/vlucas/phpdotenv/LICENSE\nCopyright: Copyright (c) 2014, Graham Campbell.\nSource: https://github.com/vlucas/phpdotenv.git\nLink: https://github.com/vlucas/phpdotenv.git\n-----------\nvoku/portable-ascii\nLicense: MIT\nLicense File: vendor/voku/portable-ascii/LICENSE.txt\nCopyright: Copyright (C) 2019 Lars Moelleken\nSource: https://github.com/voku/portable-ascii.git\nLink: https://github.com/voku/portable-ascii\n-----------\nxemlock/htmlpurifier-html5\nLicense: MIT\nLicense File: vendor/xemlock/htmlpurifier-html5/LICENSE\nCopyright: Copyright (c) 2015 Xemlock\nSource: https://github.com/xemlock/htmlpurifier-html5.git\nLink: https://github.com/xemlock/htmlpurifier-html5\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "# This is a Docker Compose configuration\n# intended for development purposes only\n\nvolumes:\n  db: {}\n\nservices:\n  db:\n    image: mysql:8.4\n    environment:\n      MYSQL_DATABASE: bookstack-dev\n      MYSQL_USER: bookstack-test\n      MYSQL_PASSWORD: bookstack-test\n      MYSQL_RANDOM_ROOT_PASSWORD: 'true'\n    volumes:\n      - ./dev/docker/init.db:/docker-entrypoint-initdb.d\n      - db:/var/lib/mysql\n  app:\n    build:\n      context: .\n      dockerfile: ./dev/docker/Dockerfile\n    environment:\n      APP_URL: http://localhost:${DEV_PORT:-8080}\n      DB_CONNECTION: mysql\n      DB_HOST: db\n      DB_PORT: 3306\n      DB_DATABASE: bookstack-dev\n      DB_USERNAME: bookstack-test\n      DB_PASSWORD: bookstack-test\n      TEST_DATABASE_URL: mysql://bookstack-test:bookstack-test@db/bookstack-test\n      MAIL_DRIVER: smtp\n      MAIL_HOST: mailhog\n      MAIL_PORT: 1025\n    ports:\n      - ${DEV_PORT:-8080}:80\n    volumes:\n      - ./:/app\n      - ./dev/docker/php/conf.d/xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini\n    entrypoint: /app/dev/docker/entrypoint.app.sh\n    extra_hosts:\n    - \"host.docker.internal:host-gateway\"\n  node:\n    image: node:22-alpine\n    working_dir: /app\n    user: node\n    volumes:\n      - ./:/app\n    entrypoint: /app/dev/docker/entrypoint.node.sh\n  mailhog:\n    image: mailhog/mailhog\n    ports:\n      - ${DEV_MAIL_PORT:-8025}:8025\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import globals from 'globals';\nimport js from '@eslint/js';\n\nexport default [\n    js.configs.recommended,\n    {\n        ignores: ['resources/**/*-stub.js', 'resources/**/*.ts'],\n    }, {\n        languageOptions: {\n            globals: {\n                ...globals.browser,\n            },\n\n            ecmaVersion: 'latest',\n            sourceType: 'module',\n        },\n\n        rules: {\n            indent: ['error', 4],\n            'arrow-parens': ['error', 'as-needed'],\n\n            'padded-blocks': ['error', {\n                blocks: 'never',\n                classes: 'always',\n            }],\n\n            'object-curly-spacing': ['error', 'never'],\n\n            'space-before-function-paren': ['error', {\n                anonymous: 'never',\n                named: 'never',\n                asyncArrow: 'always',\n            }],\n\n            'import/prefer-default-export': 'off',\n\n            'no-plusplus': ['error', {\n                allowForLoopAfterthoughts: true,\n            }],\n\n            'arrow-body-style': 'off',\n            'no-restricted-syntax': 'off',\n            'no-continue': 'off',\n            'prefer-destructuring': 'off',\n            'class-methods-use-this': 'off',\n            'no-param-reassign': 'off',\n\n            'no-console': ['warn', {\n                allow: ['error', 'warn'],\n            }],\n\n            'no-new': 'off',\n\n            'max-len': ['error', {\n                code: 110,\n                tabWidth: 4,\n                ignoreUrls: true,\n                ignoreComments: false,\n                ignoreRegExpLiterals: true,\n                ignoreStrings: true,\n                ignoreTemplateLiterals: true,\n            }],\n        },\n    }];\n"
  },
  {
    "path": "jest.config.ts",
    "content": "/**\n * For a detailed explanation regarding each configuration property, visit:\n * https://jestjs.io/docs/configuration\n */\n\nimport type {Config} from 'jest';\nimport {pathsToModuleNameMapper} from \"ts-jest\";\nimport { compilerOptions }  from './tsconfig.json';\n\nconst config: Config = {\n  // All imported modules in your tests should be mocked automatically\n  // automock: false,\n\n  // Stop running tests after `n` failures\n  // bail: 0,\n\n  // The directory where Jest should store its cached dependency information\n  // cacheDirectory: \"/tmp/jest_rs\",\n\n  // Automatically clear mock calls, instances, contexts and results before every test\n  clearMocks: true,\n\n  // Indicates whether the coverage information should be collected while executing the test\n  collectCoverage: false,\n\n  // An array of glob patterns indicating a set of files for which coverage information should be collected\n  // collectCoverageFrom: undefined,\n\n  // The directory where Jest should output its coverage files\n  coverageDirectory: \"coverage\",\n\n  // An array of regexp pattern strings used to skip coverage collection\n  // coveragePathIgnorePatterns: [\n  //   \"/node_modules/\"\n  // ],\n\n  // Indicates which provider should be used to instrument code for coverage\n  coverageProvider: \"v8\",\n\n  // A list of reporter names that Jest uses when writing coverage reports\n  // coverageReporters: [\n  //   \"json\",\n  //   \"text\",\n  //   \"lcov\",\n  //   \"clover\"\n  // ],\n\n  // An object that configures minimum threshold enforcement for coverage results\n  // coverageThreshold: undefined,\n\n  // A path to a custom dependency extractor\n  // dependencyExtractor: undefined,\n\n  // Make calling deprecated APIs throw helpful error messages\n  // errorOnDeprecated: false,\n\n  // The default configuration for fake timers\n  // fakeTimers: {\n  //   \"enableGlobally\": false\n  // },\n\n  // Force coverage collection from ignored files using an array of glob patterns\n  // forceCoverageMatch: [],\n\n  // A path to a module which exports an async function that is triggered once before all test suites\n  // globalSetup: undefined,\n\n  // A path to a module which exports an async function that is triggered once after all test suites\n  // globalTeardown: undefined,\n\n  // A set of global variables that need to be available in all test environments\n  globals: {\n    __DEV__: true,\n  },\n\n  // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.\n  // maxWorkers: \"50%\",\n\n  // An array of directory names to be searched recursively up from the requiring module's location\n  // moduleDirectories: [\n  //   \"node_modules\"\n  // ],\n\n  // An array of file extensions your modules use\n  // moduleFileExtensions: [\n  //   \"js\",\n  //   \"mjs\",\n  //   \"cjs\",\n  //   \"jsx\",\n  //   \"ts\",\n  //   \"tsx\",\n  //   \"json\",\n  //   \"node\"\n  // ],\n\n  modulePaths: ['./'],\n\n  // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module\n  moduleNameMapper: {\n    'lexical/shared/invariant': 'resources/js/wysiwyg/lexical/core/shared/__mocks__/invariant',\n    ...pathsToModuleNameMapper(compilerOptions.paths),\n  },\n\n  // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader\n  // modulePathIgnorePatterns: [],\n\n  // Activates notifications for test results\n  // notify: false,\n\n  // An enum that specifies notification mode. Requires { notify: true }\n  // notifyMode: \"failure-change\",\n\n  // A preset that is used as a base for Jest's configuration\n  // preset: undefined,\n\n  // Run tests from one or more projects\n  // projects: undefined,\n\n  // Use this configuration option to add custom reporters to Jest\n  // reporters: undefined,\n\n  // Automatically reset mock state before every test\n  // resetMocks: false,\n\n  // Reset the module registry before running each individual test\n  // resetModules: false,\n\n  // A path to a custom resolver\n  // resolver: undefined,\n\n  // Automatically restore mock state and implementation before every test\n  // restoreMocks: false,\n\n  // The root directory that Jest should scan for tests and modules within\n  // rootDir: undefined,\n\n  // A list of paths to directories that Jest should use to search for files in\n  roots: [\n    \"./resources/js\"\n  ],\n\n  // Allows you to use a custom runner instead of Jest's default test runner\n  // runner: \"jest-runner\",\n\n  // The paths to modules that run some code to configure or set up the testing environment before each test\n  // setupFiles: [],\n\n  // A list of paths to modules that run some code to configure or set up the testing framework before each test\n  // setupFilesAfterEnv: [],\n\n  // The number of seconds after which a test is considered as slow and reported as such in the results.\n  // slowTestThreshold: 5,\n\n  // A list of paths to snapshot serializer modules Jest should use for snapshot testing\n  // snapshotSerializers: [],\n\n  // The test environment that will be used for testing\n  testEnvironment: \"jsdom\",\n\n  // Options that will be passed to the testEnvironment\n  // testEnvironmentOptions: {},\n\n  // Adds a location field to test results\n  // testLocationInResults: false,\n\n  // The glob patterns Jest uses to detect test files\n  testMatch: [\n    \"**/__tests__/**/*.test.[jt]s\",\n  ],\n\n  // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped\n  // testPathIgnorePatterns: [\n  //   \"/node_modules/\"\n  // ],\n\n  // The regexp pattern or array of patterns that Jest uses to detect test files\n  // testRegex: [],\n\n  // This option allows the use of a custom results processor\n  // testResultsProcessor: undefined,\n\n  // This option allows use of a custom test runner\n  // testRunner: \"jest-circus/runner\",\n\n  // A map from regular expressions to paths to transformers\n  transform: {\n    \"^.+.tsx?$\": [\"ts-jest\",{}],\n    \"^.+.svg$\": [\"<rootDir>/dev/build/svg-blank-transform.js\",{}],\n  },\n\n  // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation\n  // transformIgnorePatterns: [\n  //   \"/node_modules/\",\n  //   \"\\\\.pnp\\\\.[^\\\\/]+$\"\n  // ],\n\n  // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them\n  // unmockedModulePathPatterns: undefined,\n\n  // Indicates whether each individual test should be reported during the run\n  // verbose: undefined,\n\n  // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode\n  // watchPathIgnorePatterns: [],\n\n  // Whether to use watchman for file crawling\n  // watchman: true,\n};\n\nexport default config;\n"
  },
  {
    "path": "lang/ar/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'تم إنشاء صفحة',\n    'page_create_notification'    => 'تم إنشاء الصفحة بنجاح',\n    'page_update'                 => 'تم تحديث الصفحة',\n    'page_update_notification'    => 'تم تحديث الصفحة بنجاح',\n    'page_delete'                 => 'تم حذف الصفحة',\n    'page_delete_notification'    => 'تم حذف الصفحة بنجاح',\n    'page_restore'                => 'تمت استعادة الصفحة',\n    'page_restore_notification'   => 'تمت استعادة الصفحة بنجاح',\n    'page_move'                   => 'تم نقل الصفحة',\n    'page_move_notification'      => 'تم نقل الصفحة بنجاح',\n\n    // Chapters\n    'chapter_create'              => 'تم إنشاء فصل',\n    'chapter_create_notification' => 'تم إنشاء الفصل بنجاح',\n    'chapter_update'              => 'تم تحديث الفصل',\n    'chapter_update_notification' => 'تم تحديث الفصل بنجاح',\n    'chapter_delete'              => 'تم حذف الفصل',\n    'chapter_delete_notification' => 'تم حذف الفصل بنجاح',\n    'chapter_move'                => 'تم نقل الفصل',\n    'chapter_move_notification' => 'تم نقل الفصل بنجاح',\n\n    // Books\n    'book_create'                 => 'تم إنشاء كتاب',\n    'book_create_notification'    => 'تم إنشاء الكتاب بنجاح',\n    'book_create_from_chapter'              => 'تم تحويل الفصل إلى كتاب',\n    'book_create_from_chapter_notification' => 'تم تحويل الفصل إلى كتاب بنجاح',\n    'book_update'                 => 'تم تحديث الكتاب',\n    'book_update_notification'    => 'تم تحديث الكتاب بنجاح',\n    'book_delete'                 => 'تم حذف الكتاب',\n    'book_delete_notification'    => 'تم حذف الكتاب بنجاح',\n    'book_sort'                   => 'تم سرد الكتاب',\n    'book_sort_notification'      => 'تم إعادة فرز الكتاب بنجاح',\n\n    // Bookshelves\n    'bookshelf_create'            => 'تم إنشاء رف',\n    'bookshelf_create_notification'    => 'تم إنشاء رف بنجاح',\n    'bookshelf_create_from_book'    => 'تم تحويل الكتاب إلى رف',\n    'bookshelf_create_from_book_notification'    => 'تم تحويل الكتاب إلى رف بنجاح',\n    'bookshelf_update'                 => 'تم تحديث الرف',\n    'bookshelf_update_notification'    => 'تم تحديث الرف بنجاح',\n    'bookshelf_delete'                 => 'تم حذف الرف',\n    'bookshelf_delete_notification'    => 'تم حذف الرف بنجاح',\n\n    // Revisions\n    'revision_restore' => 'استعادة مراجعة',\n    'revision_delete' => 'مراجعة محذوفة',\n    'revision_delete_notification' => 'تم حذف المراجعة بنجاح',\n\n    // Favourites\n    'favourite_add_notification' => 'تم إضافة \":name\" إلى المفضلة لديك',\n    'favourite_remove_notification' => 'تم إزالة \":name\" من المفضلة لديك',\n\n    // Watching\n    'watch_update_level_notification' => 'تم تحديث الإعدادات المشاهدة بنجاح',\n\n    // Auth\n    'auth_login' => 'تم تسجيل الدخول',\n    'auth_register' => 'سجل كمستخدم جديد',\n    'auth_password_reset_request' => 'طلب رابط جديد لإعادة تعيين كلمة السر',\n    'auth_password_reset_update' => 'إعادة تعيين كلمة مرور المستخدم',\n    'mfa_setup_method' => 'طريقة المصادقة متعددة العوامل المُهيأة',\n    'mfa_setup_method_notification' => 'تم إعداد المصادقة متعددة العوامل بنجاح',\n    'mfa_remove_method' => 'إزالة طريقة المصادقة متعددة العوامل',\n    'mfa_remove_method_notification' => 'تمت إزالة المصادقة متعددة العوامل بنجاح',\n\n    // Settings\n    'settings_update' => 'تحديث الإعدادات',\n    'settings_update_notification' => 'تم تحديث الإعدادات',\n    'maintenance_action_run' => 'إجراء الصيانة',\n\n    // Webhooks\n    'webhook_create' => 'تم إنشاء خطاف ويب',\n    'webhook_create_notification' => 'تم إنشاء خطاف ويب بنجاح',\n    'webhook_update' => 'تم تحديث خطاف الويب',\n    'webhook_update_notification' => 'تم تحديث خطاف الويب بنجاح',\n    'webhook_delete' => 'حذف خطاف ويب',\n    'webhook_delete_notification' => 'تم حذف خطاف الويب بنجاح',\n\n    // Imports\n    'import_create' => 'تم إنشاء الاستيراد',\n    'import_create_notification' => 'تم رفع الاستيراد بنجاح',\n    'import_run' => 'تم تحديث الاستيراد',\n    'import_run_notification' => 'تم استيراد المحتوى بنجاح',\n    'import_delete' => 'تم حذف الاستيراد',\n    'import_delete_notification' => 'تم الاستيراد بنجاح',\n\n    // Users\n    'user_create' => 'إنشاء مستخدم',\n    'user_create_notification' => 'تم إنشاء الحساب',\n    'user_update' => 'المستخدم المحدث',\n    'user_update_notification' => 'تم تحديث المستخدم بنجاح',\n    'user_delete' => 'المستخدم المحذوف',\n    'user_delete_notification' => 'تم إزالة المستخدم بنجاح',\n\n    // API Tokens\n    'api_token_create' => 'تم إنشاء رمز واجهة برمجة التطبيقات -API-',\n    'api_token_create_notification' => 'تم إنشاء  واجهة برمجة التطبيقات -API- بنجاح',\n    'api_token_update' => 'رمز واجهة برمجة التطبيقات المحدث',\n    'api_token_update_notification' => 'تم تحديث رمز واجهة برمجة التطبيقات -API- بنجاح',\n    'api_token_delete' => 'رمز واجهة برمجة التطبيقات المحذوف',\n    'api_token_delete_notification' => 'تم حذف رمز  واجهة برمجة التطبيقات -API- بنجاح',\n\n    // Roles\n    'role_create' => 'إنشاء صَلاحِيَة',\n    'role_create_notification' => 'تم إنشاء الدور بنجاح',\n    'role_update' => 'حدّث الدور',\n    'role_update_notification' => 'تم تحديث الدور بنجاح',\n    'role_delete' => 'حذف الدور',\n    'role_delete_notification' => 'تم حذف الدور بنجاح',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'سلة إعادة التدوير المفرغة',\n    'recycle_bin_restore' => 'استعادة من سلة المحذوفات',\n    'recycle_bin_destroy' => 'إزالة من سلة المحذوفات',\n\n    // Comments\n    'commented_on'                => 'تم التعليق',\n    'comment_create'              => 'تعليق مضاف',\n    'comment_update'              => 'تعليق محدث',\n    'comment_delete'              => 'تعليق محذوف',\n\n    // Sort Rules\n    'sort_rule_create' => 'تم إنشاء قاعدة الفرز',\n    'sort_rule_create_notification' => 'تم إنشاء قاعدة الفرز بنجاح',\n    'sort_rule_update' => 'تم تحديث قاعدة الفرز',\n    'sort_rule_update_notification' => 'تم تحديث قاعدة الفرز بنجاح',\n    'sort_rule_delete' => 'تم حذف قاعدة الفرز',\n    'sort_rule_delete_notification' => 'تم حذف قاعدة الفرز بنجاح',\n\n    // Other\n    'permissions_update'          => 'تحديث الصلاحيات',\n];\n"
  },
  {
    "path": "lang/ar/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'البيانات المعطاة لا توافق سجلاتنا.',\n    'throttle' => 'تجاوزت الحد الأقصى من المحاولات. الرجاء المحاولة مرة أخرى بعد :seconds ثانية/ثواني.',\n\n    // Login & Register\n    'sign_up' => 'إنشاء حساب',\n    'log_in' => 'تسجيل الدخول',\n    'log_in_with' => 'تسجيل الدخول باستخدام :socialDriver',\n    'sign_up_with' => 'إنشاء حساب باستخدام :socialDriver',\n    'logout' => 'الخروج',\n\n    'name' => 'الاسم',\n    'username' => 'اسم المستخدم',\n    'email' => 'البريد الإلكتروني',\n    'password' => 'كلمة السر',\n    'password_confirm' => 'تأكيد كلمة السر',\n    'password_hint' => 'يجب أن تحتوي كلمة السر على 8 خانات على الأقل',\n    'forgot_password' => 'نسيت كلمة السر؟',\n    'remember_me' => 'تذكرني',\n    'ldap_email_hint' => 'الرجاء إدخال عنوان بريد إلكتروني لاستخدامه مع الحساب.',\n    'create_account' => 'إنشاء حساب',\n    'already_have_account' => 'لديك حساب مسبقاً؟',\n    'dont_have_account' => 'ليس لديك حساب؟',\n    'social_login' => 'تسجيل الدخول باستخدام حسابات التواصل الاجتماعي',\n    'social_registration' => 'إنشاء حساب باستخدام حسابات التواصل الاجتماعي',\n    'social_registration_text' => 'إنشاء حساب والدخول باستخدام خدمة أخرى.',\n\n    'register_thanks' => 'شكراً لتسجيل حسابك!',\n    'register_confirm' => 'الرجاء مراجعة البريد الإلكتروني والضغط على زر التأكيد لاستخدام :appName.',\n    'registrations_disabled' => 'التسجيل مغلق حالياً',\n    'registration_email_domain_invalid' => 'المجال الخاص بالبريد الإلكتروني لا يملك حق الوصول لهذا التطبيق',\n    'register_success' => 'شكراً لإنشاء حسابكم! تم تسجيلكم ودخولكم للحساب الخاص بكم.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'محاولة تسجيل الدخول',\n    'auto_init_starting_desc' => 'نحن نتصل بنظام المصادقة الخاص بك لبدء عملية تسجيل الدخول. إذا لم يحدث أي تقدم بعد 5 ثوان يمكنك محاولة النقر على الرابط أدناه.',\n    'auto_init_start_link' => 'المتابعة مع المصادقة',\n\n    // Password Reset\n    'reset_password' => 'استعادة كلمة السر',\n    'reset_password_send_instructions' => 'أدخل بريدك الإلكتروني بالأسفل وسيتم إرسال رسالة برابط لاستعادة كلمة السر.',\n    'reset_password_send_button' => 'أرسل رابط الاستعادة',\n    'reset_password_sent' => 'سيتم إرسال رابط إعادة تعيين كلمة السر إلى عنوان البريد الإلكتروني هذا إذا كان موجودًا في النظام.',\n    'reset_password_success' => 'تمت استعادة كلمة السر بنجاح.',\n    'email_reset_subject' => 'استعد كلمة السر الخاصة بتطبيق :appName',\n    'email_reset_text' => 'تم إرسال هذه الرسالة بسبب تلقينا لطلب استعادة كلمة السر الخاصة بحسابكم.',\n    'email_reset_not_requested' => 'إذا لم يتم طلب استعادة كلمة السر من قبلكم، فلا حاجة لاتخاذ أية خطوات.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'تأكيد بريدكم الإلكتروني لتطبيق :appName',\n    'email_confirm_greeting' => 'شكرا لانضمامكم إلى :appName!',\n    'email_confirm_text' => 'الرجاء تأكيد بريدكم الإلكتروني بالضغط على الزر أدناه:',\n    'email_confirm_action' => 'تأكيد البريد الإلكتروني',\n    'email_confirm_send_error' => 'تأكيد البريد الإلكتروني مطلوب ولكن النظام لم يستطع إرسال الرسالة. تواصل مع مشرف النظام للتأكد من إعدادات البريد.',\n    'email_confirm_success' => 'تم تأكيد بريدك الإلكتروني! يمكنك الآن تسجيل الدخول باستخدام عنوان البريد الإلكتروني هذا.',\n    'email_confirm_resent' => 'تمت إعادة إرسال رسالة التأكيد، الرجاء مراجعة صندوق الوارد.',\n    'email_confirm_thanks' => 'شكرا للتأكيد!',\n    'email_confirm_thanks_desc' => 'الرجاء الانتظار لحظة بينما يتم التعامل مع التأكيد الخاص بك. إذا لم يتم إعادة توجيهك بعد 3 ثوان اضغط على الرابط \"المتابعة\" أدناه للمتابعة.',\n\n    'email_not_confirmed' => 'لم يتم تأكيد البريد الإلكتروني',\n    'email_not_confirmed_text' => 'لم يتم بعد تأكيد عنوان البريد الإلكتروني.',\n    'email_not_confirmed_click_link' => 'الرجاء الضغط على الرابط المرسل إلى بريدكم الإلكتروني بعد تسجيلكم.',\n    'email_not_confirmed_resend' => 'إذا لم يتم إيجاد الرسالة، بإمكانكم إعادة إرسال رسالة التأكيد عن طريق تعبئة النموذج أدناه.',\n    'email_not_confirmed_resend_button' => 'إعادة إرسال رسالة التأكيد',\n\n    // User Invite\n    'user_invite_email_subject' => 'تمت دعوتك للانضمام إلى صفحة الحالة الخاصة بـ :app_name!',\n    'user_invite_email_greeting' => 'تم إنشاء حساب مستخدم لك على :appName.',\n    'user_invite_email_text' => 'انقر على الزر أدناه لتعيين كلمة سر الحساب والحصول على الوصول:',\n    'user_invite_email_action' => 'كلمة سر المستخدم',\n    'user_invite_page_welcome' => 'مرحبا بكم في :appName!',\n    'user_invite_page_text' => 'لإكمال حسابك والحصول على حق الوصول تحتاج إلى تعيين كلمة السر سيتم استخدامها لتسجيل الدخول إلى :appName في الزيارات المستقبلية.',\n    'user_invite_page_confirm_button' => 'تأكيد كلمة السر',\n    'user_invite_success_login' => 'تم تأكيد كلمة السر. يمكنك الآن تسجيل الدخول باستخدام كلمة السر المحددة للوصول إلى :appName !',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'إعداد المصادقة متعددة العوامل',\n    'mfa_setup_desc' => 'إعداد المصادقة متعددة العوامل كطبقة إضافية من الأمان لحساب المستخدم الخاص بك.',\n    'mfa_setup_configured' => 'تم إعداده مسبقاً',\n    'mfa_setup_reconfigure' => 'إعادة التكوين',\n    'mfa_setup_remove_confirmation' => 'متأكد من أنك تريد إزالة طريقة المصادقة متعددة العوامل هذه؟',\n    'mfa_setup_action' => 'إعداد',\n    'mfa_backup_codes_usage_limit_warning' => 'لديك أقل من 5 رموز احتياطية متبقية، الرجاء إنشاء وتخزين مجموعة جديدة قبل نفاد الرموز لتجنب إغلاق حسابك.',\n    'mfa_option_totp_title' => 'تطبيق الجوال',\n    'mfa_option_totp_desc' => 'لاستخدام المصادقة المتعددة العوامل، ستحتاج إلى تطبيق جوال يدعم كلمة السر المؤقته -TOTP- مثل جوجل أوثنتيكاتور -Google Authenticator- أو أوثي -Authy- أو مايكروسوفت أوثنتيكاتور -Microsoft Authenticator-.',\n    'mfa_option_backup_codes_title' => 'رموز النسخ الاحتياطي',\n    'mfa_option_backup_codes_desc' => 'إنشاء مجموعة من رموز النسخ الاحتياطية للاستخدام مرة واحدة و التي سَتُدِخلها عند تسجيل الدخول للتحقق من هويتك. احرص أن تخزينها في مكان آمن.',\n    'mfa_gen_confirm_and_enable' => 'تأكيد وتمكين',\n    'mfa_gen_backup_codes_title' => 'إعداد رموز النسخ الاحتياطي',\n    'mfa_gen_backup_codes_desc' => 'خَزِن قائمة الرموز أدناه في مكان آمن. عند الوصول إلى النظام، ستتمكن من استخدام أحد الرموز كآلية مصادقة ثانية.',\n    'mfa_gen_backup_codes_download' => 'تنزيل الرموز',\n    'mfa_gen_backup_codes_usage_warning' => 'يمكن استخدام كل رمز مرة واحدة فقط',\n    'mfa_gen_totp_title' => 'إعداد تطبيق الجوال',\n    'mfa_gen_totp_desc' => 'لاستخدام المصادقة المتعددة ، ستحتاج إلى تطبيق جوال كلمة السر المؤقته -TOTP- مثل جوجل أوثنتيكاتور -Google Authenticator- أو أوثي -Authy- أو مايكروسوفت أوثنتيكاتور -Microsoft Authenticator-.',\n    'mfa_gen_totp_scan' => 'امسح رمز الاستجابة السريعة -QR- أدناه باستخدام تطبيق المصادقة المفضل لديك للبدء.',\n    'mfa_gen_totp_verify_setup' => 'التحقق من الإعداد',\n    'mfa_gen_totp_verify_setup_desc' => 'تحقق أن كل شيء يعمل عن طريق إدخال رمز تم إنشاؤه داخل تطبيق المصادقة الخاص بك في مربع الإدخال أدناه:',\n    'mfa_gen_totp_provide_code_here' => 'أدخل الرمز الذي تم إنشاؤه للتطبيق الخاص بك هنا',\n    'mfa_verify_access' => 'التحقق من الوصول',\n    'mfa_verify_access_desc' => 'يتطلب حساب المستخدم الخاص بك تأكيد هويتك عن طريق مستوى إضافي من التحقق قبل منحك حق الوصول. تحقق استخدام إحدى الطرق التي إعدادها للمتابعة.',\n    'mfa_verify_no_methods' => 'لا توجد طرق معدة',\n    'mfa_verify_no_methods_desc' => 'لم يتم العثور على طرق مصادقة متعددة العوامل لحسابك. ستحتاج إلى إعداد طريقة واحدة على الأقل قبل أن تتمكن من الوصول.',\n    'mfa_verify_use_totp' => 'التحقق باستخدام تطبيق الجوال',\n    'mfa_verify_use_backup_codes' => 'التحقق باستخدام رمز النسخ الاحتياطي',\n    'mfa_verify_backup_code' => 'الرموز الاحتياطية',\n    'mfa_verify_backup_code_desc' => 'أدخل أحد الرموز الاحتياطية المتبقية أدناه:',\n    'mfa_verify_backup_code_enter_here' => 'أدخل الرمز الاحتياطي هنا',\n    'mfa_verify_totp_desc' => 'أدخل الرمز الذي تم إنشاؤه باستخدام تطبيق الجوال الخاص بك، أدناه:',\n    'mfa_setup_login_notification' => 'تم إعداد طريقة الدخول متعددة العوامل، يرجى الآن تسجيل الدخول مرة أخرى باستخدام الطريقة التي تم إعدادها.',\n];\n"
  },
  {
    "path": "lang/ar/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'إلغاء',\n    'close' => 'إغلاق',\n    'confirm' => 'تأكيد',\n    'back' => 'رجوع',\n    'save' => 'حفظ',\n    'continue' => 'استمرار',\n    'select' => 'تحديد',\n    'toggle_all' => 'تبديل الكل',\n    'more' => 'المزيد',\n\n    // Form Labels\n    'name' => 'الاسم',\n    'description' => 'الوصف',\n    'role' => 'الدور',\n    'cover_image' => 'صورة الغلاف',\n    'cover_image_description' => 'يجب أن يكون حجم هذه الصورة تقريبًا 440 في 250 بكسل، مع أنّه سيتم تحجيمها وقصها بشكل مرن لتناسب واجهة المستخدم في سيناريوهات مختلفة حسب الحاجة، لذا فإن الأبعاد الفعلية للعرض ستختلف.',\n\n    // Actions\n    'actions' => 'إجراءات',\n    'view' => 'عرض',\n    'view_all' => 'عرض الكل',\n    'new' => 'جديد',\n    'create' => 'إنشاء',\n    'update' => 'تحديث',\n    'edit' => 'تعديل',\n    'archive' => 'أرشف',\n    'unarchive' => 'إلغاء الأرشفة',\n    'sort' => 'سرد',\n    'move' => 'نقل',\n    'copy' => 'نسخ',\n    'reply' => 'رد',\n    'delete' => 'حذف',\n    'delete_confirm' => 'تأكيد الحذف',\n    'search' => 'بحث',\n    'search_clear' => 'مسح البحث',\n    'reset' => 'إعادة تعيين',\n    'remove' => 'إزالة',\n    'add' => 'إضافة',\n    'configure' => 'ضبط',\n    'manage' => 'إدارة',\n    'fullscreen' => 'شاشة كاملة',\n    'favourite' => 'أضف إلى المفضلة',\n    'unfavourite' => 'إزالة من المفضلة',\n    'next' => 'التالي',\n    'previous' => 'السابق',\n    'filter_active' => 'التصفية المفعلة:',\n    'filter_clear' => 'مسح التصفية',\n    'download' => 'تنزيل',\n    'open_in_tab' => 'فتح في علامة تبويب',\n    'open' => 'فتح',\n\n    // Sort Options\n    'sort_options' => 'خيارات الفرز',\n    'sort_direction_toggle' => 'الفرز وفق الاتجاه',\n    'sort_ascending' => 'فرز تصاعدي',\n    'sort_descending' => 'فرز تنازلي',\n    'sort_name' => 'الاسم',\n    'sort_default' => 'افتراضي',\n    'sort_created_at' => 'تاريخ الإنشاء',\n    'sort_updated_at' => 'تاريخ التحديث',\n\n    // Misc\n    'deleted_user' => 'المستخدم المحذوف',\n    'no_activity' => 'لا يوجد نشاط لعرضه',\n    'no_items' => 'لا توجد عناصر متوفرة',\n    'back_to_top' => 'العودة إلى الأعلى',\n    'skip_to_main_content' => 'تخطى إلى المحتوى الرئيسي',\n    'toggle_details' => 'عرض / إخفاء التفاصيل',\n    'toggle_thumbnails' => 'عرض / إخفاء الصور المصغرة',\n    'details' => 'التفاصيل',\n    'grid_view' => 'عرض شبكي',\n    'list_view' => 'عرض منسدل',\n    'default' => 'افتراضي',\n    'breadcrumb' => 'شريط التنقل',\n    'status' => 'الحالة',\n    'status_active' => 'نشط',\n    'status_inactive' => 'غير نشط',\n    'never' => 'مطلقاً',\n    'none' => 'لا شَيْء',\n\n    // Header\n    'homepage' => 'الصفحة الرئيسية',\n    'header_menu_expand' => 'عرض القائمة',\n    'profile_menu' => 'قائمة ملف التعريف',\n    'view_profile' => 'عرض الملف الشخصي',\n    'edit_profile' => 'تعديل الملف الشخصي',\n    'dark_mode' => 'الوضع المظلم',\n    'light_mode' => 'الوضع المضيء',\n    'global_search' => 'البحث العام',\n\n    // Layout tabs\n    'tab_info' => 'معلومات',\n    'tab_info_label' => 'تبويب: إظهار المعلومات الثانوية',\n    'tab_content' => 'المحتوى',\n    'tab_content_label' => 'تبويب: إظهار المحتوى الأساسي',\n\n    // Email Content\n    'email_action_help' => 'إذا واجهتكم مشكلة عند ضغط زر \":actionText\" فبإمكانكم نسخ الرابط أدناه ولصقه بالمتصفح:',\n    'email_rights' => 'جميع الحقوق محفوظة',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'سياسة الخصوصية',\n    'terms_of_service' => 'اتفاقية شروط الخدمة',\n\n    // OpenSearch\n    'opensearch_description' => 'البحث عن :appName',\n];\n"
  },
  {
    "path": "lang/ar/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'تحديد صورة',\n    'image_list' => 'قائمة الصور',\n    'image_details' => 'تفاصيل الصورة',\n    'image_upload' => 'تحميل صورة',\n    'image_intro' => 'هنا يمكنك تحديد وإدارة الصور التي تم تحميلها مسبقًا إلى النظام.',\n    'image_intro_upload' => 'تحميل صورة جديدة عن طريق سحب الصورة إلى هذه النافذة، أو باستخدام زر \"تحميل صورة\" أعلاه.',\n    'image_all' => 'الكل',\n    'image_all_title' => 'عرض جميع الصور',\n    'image_book_title' => 'عرض الصور المرفوعة لهذا الكتاب',\n    'image_page_title' => 'عرض الصور المرفوعة لهذه الصفحة',\n    'image_search_hint' => 'البحث باستخدام اسم الصورة',\n    'image_uploaded' => 'وقت الرفع :uploadedDate',\n    'image_uploaded_by' => 'تم تحميلها من قبل :userName',\n    'image_uploaded_to' => 'تم رفعها إلى :pageLink',\n    'image_updated' => 'تم تحديثها :updatedate',\n    'image_load_more' => 'المزيد',\n    'image_image_name' => 'اسم الصورة',\n    'image_delete_used' => 'هذه الصورة مستخدمة بالصفحات أدناه.',\n    'image_delete_confirm_text' => 'هل أنت متأكد من أنك تريد حذف هذه الصورة؟',\n    'image_select_image' => 'تحديد الصورة',\n    'image_dropzone' => 'قم بإسقاط الصورة أو اضغط هنا للرفع',\n    'image_dropzone_drop' => 'إسقاط صورة أو اضغط هنا للرفع',\n    'images_deleted' => 'تم حذف الصور',\n    'image_preview' => 'معاينة الصور',\n    'image_upload_success' => 'تم رفع الصورة بنجاح',\n    'image_update_success' => 'تم تحديث تفاصيل الصورة بنجاح',\n    'image_delete_success' => 'تم حذف الصورة بنجاح',\n    'image_replace' => 'استبدال صورة',\n    'image_replace_success' => 'تم تحديث الصورة بنجاح',\n    'image_rebuild_thumbs' => 'تجديد تغيرات الحجم',\n    'image_rebuild_thumbs_success' => 'تم إعادة بناء تغيرات حجم الصورة بنجاح!',\n\n    // Code Editor\n    'code_editor' => 'تعديل الشفرة',\n    'code_language' => 'لغة الشفرة',\n    'code_content' => 'محتويات الشفرة',\n    'code_session_history' => 'سجل الدورة',\n    'code_save' => 'حفظ الشفرة',\n];\n"
  },
  {
    "path": "lang/ar/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'عام',\n    'advanced' => 'متقدم',\n    'none' => 'بدون',\n    'cancel' => 'إلغاء',\n    'save' => 'حفظ',\n    'close' => 'إغلاق',\n    'apply' => 'تطبيق',\n    'undo' => 'تراجع',\n    'redo' => 'إعادة التنفيذ',\n    'left' => 'يسار',\n    'center' => 'منتصف',\n    'right' => 'يمين',\n    'top' => 'الأعلى',\n    'middle' => 'وسط',\n    'bottom' => 'أسفل',\n    'width' => 'العرض',\n    'height' => 'الارتفاع',\n    'More' => 'المزيد',\n    'select' => 'إختار...',\n\n    // Toolbar\n    'formats' => 'التنسيقات',\n    'header_large' => 'رأس صفحة كبير',\n    'header_medium' => 'رأس صفحة متوسط',\n    'header_small' => 'رأس صفحة صغير',\n    'header_tiny' => 'رأس صفحة صغير جدا',\n    'paragraph' => 'فقرة',\n    'blockquote' => 'صندوق اقتباس',\n    'inline_code' => 'كود مدمج',\n    'callouts' => 'تعليق تفسيري',\n    'callout_information' => 'معلومات',\n    'callout_success' => 'نجاح',\n    'callout_warning' => 'تحذير',\n    'callout_danger' => 'خطر',\n    'bold' => 'غامق',\n    'italic' => 'مائل',\n    'underline' => 'تحته خط',\n    'strikethrough' => 'مشطوب',\n    'superscript' => 'نص مرتفع',\n    'subscript' => 'نص منخفض',\n    'text_color' => 'لون النص',\n    'highlight_color' => 'لون التمييز',\n    'custom_color' => 'لون مخصص',\n    'remove_color' => 'إزالة اللون',\n    'background_color' => 'لون الخلفية',\n    'align_left' => 'محاذاة لليسار',\n    'align_center' => 'محاذاة بالمنتصف',\n    'align_right' => 'مُحاذاة لليمين',\n    'align_justify' => 'المحاذاة',\n    'list_bullet' => 'قائمة نقاط',\n    'list_numbered' => 'قائمة مرقمة',\n    'list_task' => 'قائمة المهام',\n    'indent_increase' => 'زيادة البادئة',\n    'indent_decrease' => 'إنقاص البادئة',\n    'table' => 'جدول',\n    'insert_image' => 'ادراج صورة',\n    'insert_image_title' => 'إضافة/تحرير الصورة',\n    'insert_link' => 'إضافة/تعديل الرابط',\n    'insert_link_title' => 'إضافة/تحرير الرابط',\n    'insert_horizontal_line' => 'إضافة خط أفقي',\n    'insert_code_block' => 'إضافة مربع رموز برمجية',\n    'edit_code_block' => 'تعديل مربع الرموز البرمجية',\n    'insert_drawing' => 'إضافة/تعديل الرسم',\n    'drawing_manager' => 'إدارة الرسم',\n    'insert_media' => 'إضافة/تحرير الوسائط',\n    'insert_media_title' => 'إضافة/تحرير الوسائط',\n    'clear_formatting' => 'مسح التنسيق',\n    'source_code' => 'الرمز البرمجي',\n    'source_code_title' => 'الرمز البرمجي',\n    'fullscreen' => 'شاشة كاملة',\n    'image_options' => 'خيارات الصورة',\n\n    // Tables\n    'table_properties' => 'خصائص الجدول',\n    'table_properties_title' => 'خصائص الجدول',\n    'delete_table' => 'حذف الجدول',\n    'table_clear_formatting' => 'مسح تنسيق الجدول',\n    'resize_to_contents' => 'تغيير الحجم إلى المحتوى',\n    'row_header' => 'رأس الصف',\n    'insert_row_before' => 'إضافة صف قبل',\n    'insert_row_after' => 'إضافة صف بعد',\n    'delete_row' => 'حذف الصف',\n    'insert_column_before' => 'إدراج عمود قبل',\n    'insert_column_after' => 'إدراج عمود بعد',\n    'delete_column' => 'حذف عمود',\n    'table_cell' => 'خلية',\n    'table_row' => 'صف',\n    'table_column' => 'عمود',\n    'cell_properties' => 'خصائص الخلية',\n    'cell_properties_title' => 'خصائص الخلية',\n    'cell_type' => 'نوع الخلية',\n    'cell_type_cell' => 'الخلية',\n    'cell_scope' => 'النِطَاق',\n    'cell_type_header' => 'عنوان الخلية',\n    'merge_cells' => 'دمج الخلايا',\n    'split_cell' => 'خلية منقسمة',\n    'table_row_group' => 'مجموعة الصفوف',\n    'table_column_group' => 'مجموعة الأعمدة',\n    'horizontal_align' => 'محاذاة أفقية',\n    'vertical_align' => 'محاذاة عمودية',\n    'border_width' => 'عرض الحدود',\n    'border_style' => 'نمط الحدود',\n    'border_color' => 'لون الحدود',\n    'row_properties' => 'خصائص الصف',\n    'row_properties_title' => 'خصائص الصف',\n    'cut_row' => 'فص الصف',\n    'copy_row' => 'نسخ الصف',\n    'paste_row_before' => 'لصق الصف قبل',\n    'paste_row_after' => 'لصق الصف بعد',\n    'row_type' => 'نوع الصف',\n    'row_type_header' => 'العنوان',\n    'row_type_body' => 'المحتوى ',\n    'row_type_footer' => 'تذييل',\n    'alignment' => 'المحاذاة',\n    'cut_column' => 'قص العمود',\n    'copy_column' => 'نسخ العمود',\n    'paste_column_before' => 'لصق عمود قبل',\n    'paste_column_after' => 'لصق عمود بعد',\n    'cell_padding' => 'هوامش الخلايا',\n    'cell_spacing' => 'تباعد الخلايا',\n    'caption' => 'الوصف',\n    'show_caption' => 'إظهار الوصف',\n    'constrain' => 'تقييد النسب',\n    'cell_border_solid' => 'لون كامل',\n    'cell_border_dotted' => 'مُنَقط',\n    'cell_border_dashed' => 'متقطع',\n    'cell_border_double' => 'مزدوج',\n    'cell_border_groove' => 'أخدود',\n    'cell_border_ridge' => 'الحافَة',\n    'cell_border_inset' => 'الداخلية',\n    'cell_border_outset' => 'الخارجية',\n    'cell_border_none' => 'لا شَيْء',\n    'cell_border_hidden' => 'مخفي',\n\n    // Images, links, details/summary & embed\n    'source' => 'المصدر',\n    'alt_desc' => 'وصف بديل',\n    'embed' => 'تضمين',\n    'paste_embed' => 'قم بلصق الرموز المصدرية المضمنة الخاص بك أدناه:',\n    'url' => 'الرابط',\n    'text_to_display' => 'النص المراد عرضه',\n    'title' => 'العنوان',\n    'browse_links' => 'تصفح الروابط',\n    'open_link' => 'افتح الرابط',\n    'open_link_in' => 'افتح الرابط في...',\n    'open_link_current' => 'النافذة الحالية',\n    'open_link_new' => 'نافذة جديدة',\n    'remove_link' => 'إزالة الرابط',\n    'insert_collapsible' => 'أدخل كتلة قابلة للطي',\n    'collapsible_unwrap' => 'بسط',\n    'edit_label' => 'عدل الوصف',\n    'toggle_open_closed' => 'التبديل بين الفتح والإغلاق',\n    'collapsible_edit' => 'تحرير الكتلة القابلة للطي',\n    'toggle_label' => 'تبديل التسمية',\n\n    // About view\n    'about' => 'عن المحرر',\n    'about_title' => 'حول محرر ما تراه هو ما تحصل عليه -WYSIWYG-',\n    'editor_license' => 'رخصة المحرر وحقوق  التأليف والنشر',\n    'editor_lexical_license' => 'تم إنشاء هذا المحرر باعتباره فرعًا لـ :lexicalLink الذي يتم توزيعه بموجب ترخيص معهد ماساتشوستس للتقانة -MIT-.',\n    'editor_lexical_license_link' => 'يمكنك العثور على تفاصيل الترخيص الكاملة هنا.',\n    'editor_tiny_license' => 'تم إنشاء هذا المحرر باستخدام :tinyLink والذي يتم توفيره بموجب ترخيص معهد ماساتشوستس للتقانة -MIT-.',\n    'editor_tiny_license_link' => 'يمكن الاطلاع هنا على تفاصيل حقوق التأليف والنشر والترخيص الخاصة بتاینی‌ام‌سی‌ای -TinyMCE-.',\n    'save_continue' => 'حفظ الصفحة ومتابعة',\n    'callouts_cycle' => '(استمر في الضغط للتبديل بين الأنواع)',\n    'link_selector' => 'رابط للمحتوى',\n    'shortcuts' => 'الاختصارات',\n    'shortcut' => 'الاختصار',\n    'shortcuts_intro' => 'الاختصارات التالية متاحة في المحرر:',\n    'windows_linux' => '(ويندوز/لينكس)',\n    'mac' => '(ماك)',\n    'description' => 'الوصف',\n];\n"
  },
  {
    "path": "lang/ar/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'أنشئت مؤخراً',\n    'recently_created_pages' => 'صفحات أنشئت مؤخراً',\n    'recently_updated_pages' => 'صفحات حُدثت مؤخراً',\n    'recently_created_chapters' => 'فصول أنشئت مؤخراً',\n    'recently_created_books' => 'كتب أنشئت مؤخراً',\n    'recently_created_shelves' => 'أرفف أنشئت مؤخراً',\n    'recently_update' => 'حُدثت مؤخراً',\n    'recently_viewed' => 'عُرضت مؤخراً',\n    'recent_activity' => 'نشاطات حديثة',\n    'create_now' => 'أنشئ الآن',\n    'revisions' => 'مراجعات',\n    'meta_revision' => 'مراجعة #:revisionCount',\n    'meta_created' => 'أنشئ :timeLength',\n    'meta_created_name' => 'أنشئ :timeLength بواسطة :user',\n    'meta_updated' => 'مُحدث :timeLength',\n    'meta_updated_name' => 'مُحدث :timeLength بواسطة :user',\n    'meta_owned_name' => 'مملوكة لـ:user',\n    'meta_reference_count' => 'مشار إليه :count مرة|مشار إليه :count مرة',\n    'entity_select' => 'اختيار الكيان',\n    'entity_select_lack_permission' => 'ليس لديك الصلاحيات المطلوبة لتحديد هذا العنصر',\n    'images' => 'صور',\n    'my_recent_drafts' => 'مسوداتي الحديثة',\n    'my_recently_viewed' => 'ما عرضته مؤخراً',\n    'my_most_viewed_favourites' => 'مفضلاتي الأكثر مشاهدة',\n    'my_favourites' => 'مفضلاتي',\n    'no_pages_viewed' => 'لم تستعرض أي صفحات',\n    'no_pages_recently_created' => 'لم تنشأ أي صفحات مؤخراً',\n    'no_pages_recently_updated' => 'لم تُحدّث أي صفحات مؤخراً',\n    'export' => 'تصدير',\n    'export_html' => 'صفحة ويب',\n    'export_pdf' => 'ملف PDF',\n    'export_text' => 'ملف نص عادي',\n    'export_md' => 'ملف ماركداون -Markdown-',\n    'export_zip' => 'ملف مضغوط -ZIP-',\n    'default_template' => 'قالب الصفحة الافتراضية',\n    'default_template_explain' => 'قم بتعيين قالب صفحة سيتم استخدامه كمحتوى افتراضي لجميع الصفحات التي تم إنشاؤها ضمن هذا العنصر. ضع في اعتبارك أن هذا لن يتم استخدامه إلا إذا كان لدى منشئ الصفحة حق الوصول إلى صفحة القالب المختارة.',\n    'default_template_select' => 'حدد صفحة القالب',\n    'import' => 'استيراد',\n    'import_validate' => 'التحقق من صحة الاستيراد',\n    'import_desc' => 'استيراد الكتب والفصول والصفحات باستخدام تصدير مِلَفّ مضغوط ZIP محمول من نفس النظام أو نظام مختلف. حدد مِلَفّ ZIP للمتابعة. بعد تحميل المِلَفّ والتحقق من صحته، ستتمكن من إعداد وتأكيد الاستيراد في العرض التالي.',\n    'import_zip_select' => 'حدد مِلَفّ مضغوط بصيغة ZIP للتحميل',\n    'import_zip_validation_errors' => 'تم اكتشاف أخطاء في أثناء التحقق من صحة المِلَفّ المضغوط ZIP المقدم:',\n    'import_pending' => 'الاستيرادات المعلقة',\n    'import_pending_none' => 'لم يتم البَدْء في أي عملية استيراد.',\n    'import_continue' => 'متابعة الاستيراد',\n    'import_continue_desc' => 'راجع المحتوى الذي يجب استيراده من المِلَفّ المضغوط ZIP الذي تم تحميله. عندما يكون جاهزًا، تشتغل عملية الاستيراد لإضافة محتوياته إلى هذا النظام. سيتم إزالة مِلَفّ الاستيراد الذي تم تحميله تلقائيًا عند الاستيراد الناجح.',\n    'import_details' => 'تفاصيل الاستيراد',\n    'import_run' => 'تشغيل الاستيراد',\n    'import_size' => 'حجم الاستيراد :size ',\n    'import_uploaded_at' => 'تم تحميلة في  :relativeTime',\n    'import_uploaded_by' => 'رُفِع بواسطة',\n    'import_location' => 'موقع الاستيراد',\n    'import_location_desc' => 'حدد موقعًا مستهدفًا للمحتوى المستورد. ستحتاج إلى الصلاحيات ذات الصلة لإنشاء المحتوى داخل الموقع الذي تختاره.',\n    'import_delete_confirm' => 'متيقِّن من أنك تريد حذف الاستيراد؟',\n    'import_delete_desc' => 'سيؤدي هذا إلى حذف مِلَفّ الاستيراد المضغوط ZIP، ولا يمكن التراجع عنه.',\n    'import_errors' => 'أخطاء الاستيراد',\n    'import_errors_desc' => 'حدثت الأخطاء التالية خلال محاولة الاستيراد:',\n    'breadcrumb_siblings_for_page' => 'تصفح أشقاء هذه الصفحة',\n    'breadcrumb_siblings_for_chapter' => 'تصفح أشقاء هذا الفصل',\n    'breadcrumb_siblings_for_book' => 'تصفح أشقاء هذا الكتاب',\n    'breadcrumb_siblings_for_bookshelf' => 'تصفح أشقاء هذا الرف',\n\n    // Permissions and restrictions\n    'permissions' => 'الأذونات',\n    'permissions_desc' => 'تعيين الصلاحيات هنا لتجاوز الصلاحيات الافتراضية التي توفرها أدوار المستخدم.',\n    'permissions_book_cascade' => 'سيتم نقل الصلاحيات التي تم تعيينها للكتب تلقائيًا إلى الفصول والصفحات الفرعية، ما لم تكن لديها صلاحيات خاصة بها محددة.',\n    'permissions_chapter_cascade' => 'سيتم نقل الصلاحيات التي تم تعيينها على الفصول تلقائيًا إلى الصفحات الفرعية، ما لم تكن لديها صلاحيات خاصة بها محددة.',\n    'permissions_save' => 'حفظ الأذونات',\n    'permissions_owner' => 'المالك',\n    'permissions_role_everyone_else' => 'الآخرين',\n    'permissions_role_everyone_else_desc' => 'تعيين الصلاحيات لجميع الأدوار التي لم يتم تجاوزها على وجه التحديد.',\n    'permissions_role_override' => 'تجاوز الصلاحيات للدور',\n    'permissions_inherit_defaults' => 'وراثة الإعدادات الافتراضية',\n\n    // Search\n    'search_results' => 'نتائج البحث',\n    'search_total_results_found' => 'عدد النتائج :count|مجموع النتائج :count',\n    'search_clear' => 'مسح البحث',\n    'search_no_pages' => 'لم يطابق بحثكم أي صفحة',\n    'search_for_term' => 'ابحث عن :term',\n    'search_more' => 'المزيد من النتائج',\n    'search_advanced' => 'بحث مفصل',\n    'search_terms' => 'البحث باستخدام المصطلحات',\n    'search_content_type' => 'نوع المحتوى',\n    'search_exact_matches' => 'نتائج مطابقة تماماً',\n    'search_tags' => 'بحث الوسوم',\n    'search_options' => 'الخيارات',\n    'search_viewed_by_me' => 'استعرضت من قبلي',\n    'search_not_viewed_by_me' => 'لم تستعرض من قبلي',\n    'search_permissions_set' => 'حزمة الأذونات',\n    'search_created_by_me' => 'أنشئت بواسطتي',\n    'search_updated_by_me' => 'حُدثت بواسطتي',\n    'search_owned_by_me' => 'مملوكة لي',\n    'search_date_options' => 'خيارات التاريخ',\n    'search_updated_before' => 'حدثت قبل',\n    'search_updated_after' => 'حدثت بعد',\n    'search_created_before' => 'أنشئت قبل',\n    'search_created_after' => 'أنشئت بعد',\n    'search_set_date' => 'تحديد التاريخ',\n    'search_update' => 'تحديث البحث',\n\n    // Shelves\n    'shelf' => 'رف',\n    'shelves' => 'الأرفف',\n    'x_shelves' => ':count رف|:count أرفف',\n    'shelves_empty' => 'لم ينشأ أي رف',\n    'shelves_create' => 'إنشاء رف جديد',\n    'shelves_popular' => 'أرفف رائجة',\n    'shelves_new' => 'أرفف جديدة',\n    'shelves_new_action' => 'رف جديد',\n    'shelves_popular_empty' => 'ستظهر هنا الأرفف الأكثر رواجًا.',\n    'shelves_new_empty' => 'ستظهر هنا الأرفف التي أنشئت مؤخرًا.',\n    'shelves_save' => 'حفظ الرف',\n    'shelves_books' => 'كتب على هذا الرف',\n    'shelves_add_books' => 'إضافة كتب لهذا الرف',\n    'shelves_drag_books' => 'اسحب الكتب الموجودة بالأسفل لإضافتها إلى هذا الرف',\n    'shelves_empty_contents' => 'لا توجد كتب مخصصة لهذا الرف',\n    'shelves_edit_and_assign' => 'تحرير الرف لإدراج كتب',\n    'shelves_edit_named' => 'تعديل الرف :name',\n    'shelves_edit' => 'تعديل الرف',\n    'shelves_delete' => 'حذف الرف',\n    'shelves_delete_named' => 'حذف الرف :name',\n    'shelves_delete_explain' => \"سيؤدي هذا إلى حذف الرف الذي يحمل الاسم ':name'. لن يتم حذف الكتب المضمنة بداخله.\",\n    'shelves_delete_confirmation' => 'هل أنت متأكد أنك تريد حذف هذا الرف؟',\n    'shelves_permissions' => 'صلاحيات الرف',\n    'shelves_permissions_updated' => 'تم تحديث صلاحيات الرف',\n    'shelves_permissions_active' => 'صلاحيات الرف نشطة',\n    'shelves_permissions_cascade_warning' => 'لا يتم نقل الصلاحيات الموجودة على الأرفف تلقائيًا إلى الكتب الموجودة في كل رف. وذلك لأن الكتاب يمكن أن يوجد على أرفف متعددة. ومع ذلك، يمكن نسخ الصلاحيات إلى الكتب الفرعية باستخدام الخِيار الموجود أدناه.',\n    'shelves_permissions_create' => 'تُستخدم صلاحيات إنشاء الرفوف فقط لنسخ الصلاحيات إلى الكتب الفرعية باستخدام الإجراء أدناه. ولا تتحكم في القدرة على إنشاء الكتب.',\n    'shelves_copy_permissions_to_books' => 'نسخ أذونات الوصول إلى الكتب',\n    'shelves_copy_permissions' => 'نسخ الأذونات',\n    'shelves_copy_permissions_explain' => 'سيؤدي هذا إلى تطبيق إعدادات الصلاحيات الحالية لهذا الرف على جميع الكتب الموجودة بداخله. قبل التنشيط، تأكد من حفظ أي تغييرات على صلاحيات هذا الرف.',\n    'shelves_copy_permission_success' => 'تم نسخ صلاحيات الرف إلى :count كتاب/كتب',\n\n    // Books\n    'book' => 'كتاب',\n    'books' => 'الكتب',\n    'x_books' => ':count كتاب|:count كتب',\n    'books_empty' => 'لم يتم إنشاء أي كتب',\n    'books_popular' => 'كتب رائجة',\n    'books_recent' => 'كتب حديثة',\n    'books_new' => 'كتب جديدة',\n    'books_new_action' => 'كتاب جديد',\n    'books_popular_empty' => 'الكتب الأكثر رواجاً ستظهر هنا.',\n    'books_new_empty' => 'الكتب المنشأة مؤخراً ستظهر هنا.',\n    'books_create' => 'إنشاء كتاب جديد',\n    'books_delete' => 'حذف الكتاب',\n    'books_delete_named' => 'حذف كتاب :bookName',\n    'books_delete_explain' => 'سيتم حذف كتاب \\':bookName\\'، وأيضا حذف جميع الفصول والصفحات.',\n    'books_delete_confirmation' => 'تأكيد حذف الكتاب؟',\n    'books_edit' => 'تعديل الكتاب',\n    'books_edit_named' => 'تعديل كتاب :bookName',\n    'books_form_book_name' => 'اسم الكتاب',\n    'books_save' => 'حفظ الكتاب',\n    'books_permissions' => 'أذونات الكتاب',\n    'books_permissions_updated' => 'تم تحديث أذونات الكتاب',\n    'books_empty_contents' => 'لم يتم إنشاء أي صفحات أو فصول لهذا الكتاب.',\n    'books_empty_create_page' => 'إنشاء صفحة جديدة',\n    'books_empty_sort_current_book' => 'فرز الكتاب الحالي',\n    'books_empty_add_chapter' => 'إضافة فصل',\n    'books_permissions_active' => 'أذونات الكتاب مفعلة',\n    'books_search_this' => 'البحث في هذا الكتاب',\n    'books_navigation' => 'تصفح الكتاب',\n    'books_sort' => 'فرز محتويات الكتاب',\n    'books_sort_desc' => 'نقل الفصول والصفحات داخل الكتاب لإعادة تنظيم محتوياته. يمكن إضافة كتب أخرى مما يسمح بنقل الفصول والصفحات بسهولة بين الكتب. اختياريًا، يمكن تعيين قاعدة فرز تلقائي لفرز محتويات هذا الكتاب تلقائيًا عند حدوث تغييرات.',\n    'books_sort_auto_sort' => 'خِيار الفرز التلقائي',\n    'books_sort_auto_sort_active' => 'الفرز التلقائي الشَغَّال: :sortName',\n    'books_sort_named' => 'فرز كتاب :bookName',\n    'books_sort_name' => 'ترتيب حسب الإسم',\n    'books_sort_created' => 'ترتيب حسب تاريخ الإنشاء',\n    'books_sort_updated' => 'فرز حسب تاريخ التحديث',\n    'books_sort_chapters_first' => 'الفصول الأولى',\n    'books_sort_chapters_last' => 'الفصول الأخيرة',\n    'books_sort_show_other' => 'عرض كتب أخرى',\n    'books_sort_save' => 'حفظ الترتيب الجديد',\n    'books_sort_show_other_desc' => 'أضف كتبًا أخرى هنا لتضمينها في عملية الفرز، والسماح بإعادة تنظيم الكتب بسهولة.',\n    'books_sort_move_up' => 'حرك للأعلى',\n    'books_sort_move_down' => 'حرك للأسفل',\n    'books_sort_move_prev_book' => 'نقل للكتاب السابق',\n    'books_sort_move_next_book' => 'نقل للكتاب التالي',\n    'books_sort_move_prev_chapter' => 'نقل إلى الفصل السابق',\n    'books_sort_move_next_chapter' => 'نقل إلى الفصل التالي',\n    'books_sort_move_book_start' => 'نقل إلى بداية الكتاب',\n    'books_sort_move_book_end' => 'نقل إلى نهاية الكتاب',\n    'books_sort_move_before_chapter' => 'نقل إلى الفصل السابق',\n    'books_sort_move_after_chapter' => 'نقل إلى الفصل التالي',\n    'books_copy' => 'نسخة الكتاب',\n    'books_copy_success' => 'تم نسخ الكتاب بنجاح',\n\n    // Chapters\n    'chapter' => 'فصل',\n    'chapters' => 'فصول',\n    'x_chapters' => ':count فصل|:count فصول',\n    'chapters_popular' => 'فصول رائجة',\n    'chapters_new' => 'فصل جديد',\n    'chapters_create' => 'إنشاء فصل جديد',\n    'chapters_delete' => 'حذف الفصل',\n    'chapters_delete_named' => 'حذف فصل :chapterName',\n    'chapters_delete_explain' => 'سيؤدي هذا إلى حذف الفصل الذي يحمل الاسم \\':chapterName\\'. كما سيتم حذف جميع الصفحات الموجودة داخل هذا الفصل.',\n    'chapters_delete_confirm' => 'تأكيد حذف الفصل؟',\n    'chapters_edit' => 'تعديل الفصل',\n    'chapters_edit_named' => 'تعديل فصل :chapterName',\n    'chapters_save' => 'حفظ الفصل',\n    'chapters_move' => 'نقل الفصل',\n    'chapters_move_named' => 'نقل فصل :chapterName',\n    'chapters_copy' => 'نسخ الفصل',\n    'chapters_copy_success' => 'تم نسخ الفصل بنجاح',\n    'chapters_permissions' => 'أذونات الفصل',\n    'chapters_empty' => 'لا توجد أي صفحات في هذا الفصل حالياً',\n    'chapters_permissions_active' => 'أذونات الفصل مفعلة',\n    'chapters_permissions_success' => 'تم تحديث أذونات الفصل',\n    'chapters_search_this' => 'البحث في هذا الفصل',\n    'chapter_sort_book' => 'فرز الكتاب',\n\n    // Pages\n    'page' => 'صفحة',\n    'pages' => 'صفحات',\n    'x_pages' => ':count صفحة|:count صفحات',\n    'pages_popular' => 'صفحات رائجة',\n    'pages_new' => 'صفحة جديدة',\n    'pages_attachments' => 'مرفقات',\n    'pages_navigation' => 'تصفح الصفحة',\n    'pages_delete' => 'حذف الصفحة',\n    'pages_delete_named' => 'حذف صفحة :pageName',\n    'pages_delete_draft_named' => 'حذف مسودة :pageName',\n    'pages_delete_draft' => 'حذف المسودة',\n    'pages_delete_success' => 'تم حذف الصفحة',\n    'pages_delete_draft_success' => 'تم حذف المسودة',\n    'pages_delete_warning_template' => 'هذه الصفحة قيد الاستخدام كقالب افتراضي لصفحات الكتب أو الفصول. لن يكون لهذه الكتب أو الفصول قالب افتراضي بعد حذفها.',\n    'pages_delete_confirm' => 'تأكيد حذف الصفحة؟',\n    'pages_delete_draft_confirm' => 'تأكيد حذف المسودة؟',\n    'pages_editing_named' => ':pageName قيد التعديل',\n    'pages_edit_draft_options' => 'خيارات المسودة',\n    'pages_edit_save_draft' => 'حفظ المسودة',\n    'pages_edit_draft' => 'تعديل مسودة الصفحة',\n    'pages_editing_draft' => 'المسودة قيد التعديل',\n    'pages_editing_page' => 'الصفحة قيد التعديل',\n    'pages_edit_draft_save_at' => 'تم خفظ المسودة في ',\n    'pages_edit_delete_draft' => 'حذف المسودة',\n    'pages_edit_delete_draft_confirm' => 'متيقِّن من رغبتك في حذف تغييرات صفحة المُسَوَّدَة؟ ستُفقد جميع تغييراتك، منذ آخر حفظ كامل، وسيتم تحديث المحرر بأحدث حالة حفظ للصفحة (غير مسودة).',\n    'pages_edit_discard_draft' => 'التخلص من المسودة',\n    'pages_edit_switch_to_markdown' => 'التبديل إلى محرر ماركداون -Markdown-',\n    'pages_edit_switch_to_markdown_clean' => '(محتوى نظيف)',\n    'pages_edit_switch_to_markdown_stable' => '(محتوى مستقر)',\n    'pages_edit_switch_to_wysiwyg' => 'التبديل إلى محرر ما تراه هو ما تحصل عليه -WYSIWYG-',\n    'pages_edit_switch_to_new_wysiwyg' => 'التبديل إلى محرر ما تراه هو ما تحصل عليه الجديد -new WYSIWYG-',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(في الاختبار التجريبي)',\n    'pages_edit_set_changelog' => 'تثبيت سجل التعديل',\n    'pages_edit_enter_changelog_desc' => 'ضع وصف مختصر للتعديلات التي تمت',\n    'pages_edit_enter_changelog' => 'أدخل سجل التعديل',\n    'pages_editor_switch_title' => 'تبديل المحرر',\n    'pages_editor_switch_are_you_sure' => 'متيقِّن أنك تريد تغيير المحرر لهذه الصفحة؟',\n    'pages_editor_switch_consider_following' => 'عند تغيير المحررين، ضع في اعتبارك ما يلي:',\n    'pages_editor_switch_consideration_a' => 'بمجرد الحفظ، سيتم استخدام خِيار المحرر الجديد بواسطة أي محررين مستقبليين، بما في ذلك أولئك الذين قد لا يتمكنون من تغيير نوع المحرر بأنفسهم.',\n    'pages_editor_switch_consideration_b' => 'من الممكن أن يؤدي هذا إلى فقدان التفاصيل والنحو في ظروف معينة.',\n    'pages_editor_switch_consideration_c' => 'لن تستمر تغييرات العلامة أو سجل التغييرات، التي تم إجراؤها منذ الحفظ الأخير، عبر هذا التغيير.',\n    'pages_save' => 'حفظ الصفحة',\n    'pages_title' => 'عنوان الصفحة',\n    'pages_name' => 'اسم الصفحة',\n    'pages_md_editor' => 'المحرر',\n    'pages_md_preview' => 'معاينة',\n    'pages_md_insert_image' => 'إدخال صورة',\n    'pages_md_insert_link' => 'إدراج ارتباط الكيان',\n    'pages_md_insert_drawing' => 'إدخال رسمة',\n    'pages_md_show_preview' => 'عرض المعاينة',\n    'pages_md_sync_scroll' => 'مزامنة معاينة التمرير',\n    'pages_md_plain_editor' => 'محرر النصوص العادي',\n    'pages_drawing_unsaved' => 'تم العثور على رسم غير محفوظ',\n    'pages_drawing_unsaved_confirm' => 'تم العثور على بيانات رسم غير محفوظة من محاولة حفظ رسم سابقة فاشلة. هل ترغب في استعادة هذا الرسم غير المحفوظ ومواصلة تحريره؟',\n    'pages_not_in_chapter' => 'صفحة ليست في فصل',\n    'pages_move' => 'نقل الصفحة',\n    'pages_copy' => 'نسخ الصفحة',\n    'pages_copy_desination' => 'نسخ مكان الوصول',\n    'pages_copy_success' => 'تم نسخ الصفحة بنجاح',\n    'pages_permissions' => 'أذونات الصفحة',\n    'pages_permissions_success' => 'تم تحديث أذونات الصفحة',\n    'pages_revision' => 'مراجعة',\n    'pages_revisions' => 'مراجعات الصفحة',\n    'pages_revisions_desc' => 'تجد أدناه جميع الإصدارات السابقة لهذه الصفحة. يمكنك الاطلاع عليها ومقارنتها واستعادة الإصدارات القديمة إذا سمحت الصلاحيات بذلك. قد لا يظهر تاريخ الصفحة بالكامل هنا، إذ قد تُحذف الإصدارات القديمة تلقائيًا، وذلك حسب إعدادات النظام.',\n    'pages_revisions_named' => 'مراجعات صفحة :pageName',\n    'pages_revision_named' => 'مراجعة صفحة :pageName',\n    'pages_revision_restored_from' => 'تم الاستعادة من #:id; :summary',\n    'pages_revisions_created_by' => 'أنشئ بواسطة',\n    'pages_revisions_date' => 'تاريخ المراجعة',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'رَقْم المراجعة',\n    'pages_revisions_numbered' => 'مراجعة #:id',\n    'pages_revisions_numbered_changes' => 'مراجعة #: رقم تعريفي التغييرات',\n    'pages_revisions_editor' => 'نوع المحرر',\n    'pages_revisions_changelog' => 'سجل التعديل',\n    'pages_revisions_changes' => 'التعديلات',\n    'pages_revisions_current' => 'النسخة الحالية',\n    'pages_revisions_preview' => 'معاينة',\n    'pages_revisions_restore' => 'استرجاع',\n    'pages_revisions_none' => 'لا توجد مراجعات لهذه الصفحة',\n    'pages_copy_link' => 'نسخ الرابط',\n    'pages_edit_content_link' => 'انتقل إلى القسم في المحرر',\n    'pages_pointer_enter_mode' => 'أدخل وضع اختيار القسم',\n    'pages_pointer_label' => 'خيارات قسم الصفحة',\n    'pages_pointer_permalink' => 'رابط دائم لقسم الصفحة',\n    'pages_pointer_include_tag' => 'قسم الصفحة يتضمن العلامة',\n    'pages_pointer_toggle_link' => 'وضع الرابط الدائم، اضغط لإظهار علامة التضمين',\n    'pages_pointer_toggle_include' => 'تضمين وضع العلامة، اضغط لإظهار الرابط الدائم',\n    'pages_permissions_active' => 'أذونات الصفحة مفعلة',\n    'pages_initial_revision' => 'نشر مبدئي',\n    'pages_references_update_revision' => 'التحديث التلقائي للنظام للروابط الداخلية',\n    'pages_initial_name' => 'صفحة جديدة',\n    'pages_editing_draft_notification' => 'جارٍ تعديل مسودة لم يتم حفظها من :timeDiff.',\n    'pages_draft_edited_notification' => 'تم تحديث هذه الصفحة منذ ذلك الوقت. من الأفضل التخلص من هذه المسودة.',\n    'pages_draft_page_changed_since_creation' => 'تم تحديث هذه الصفحة منذ إنشاء هذه المُسَوَّدَة. يُنصح بتجاهل هذه المُسَوَّدَة أو الحرص على عدم استبدال أي تغييرات في الصفحة.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count من المستخدمين بدأوا بتعديل هذه الصفحة',\n        'start_b' => ':userName بدأ بتعديل هذه الصفحة',\n        'time_a' => 'منذ أن تم تحديث هذه الصفحة',\n        'time_b' => 'في آخر :minCount دقيقة/دقائق',\n        'message' => 'وقت البدء: احرص على عدم الكتابة فوق تحديثات بعضنا البعض!',\n    ],\n    'pages_draft_discarded' => 'تم رفض المُسَوَّدَة! تم تحديث المحرر بمحتوى الصفحة الحالي.',\n    'pages_draft_deleted' => 'تم حذف المُسَوَّدَة! تم تحديث المحرر بمحتوى الصفحة الحالي.',\n    'pages_specific' => 'صفحة محددة',\n    'pages_is_template' => 'قالب الصفحة',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'تبديل الشريط الجانبي',\n    'page_tags' => 'وسوم الصفحة',\n    'chapter_tags' => 'وسوم الفصل',\n    'book_tags' => 'وسوم الكتاب',\n    'shelf_tags' => 'علامات الرف',\n    'tag' => 'وسم',\n    'tags' =>  'وسوم',\n    'tags_index_desc' => 'يمكن تطبيق الوسوم على المحتوى داخل النظام لتطبيق تصنيف مرن. يمكن أن تحتوي الوسوم على مفتاح وقيمة، مع العلم أن القيمة اختيارية. بعد تطبيقها، يمكن الاستعلام عن المحتوى باستخدام اسم الوسم وقيمته.',\n    'tag_name' =>  'اسم العلامة',\n    'tag_value' => 'قيمة الوسم (اختياري)',\n    'tags_explain' => \"إضافة الوسوم تساعد بترتيب وتقسيم المحتوى. \\n من الممكن وضع قيمة لكل وسم لترتيب أفضل وأدق.\",\n    'tags_add' => 'إضافة وسم آخر',\n    'tags_remove' => 'إزالة هذه العلامة',\n    'tags_usages' => 'إجمالي استخدامات العلامة',\n    'tags_assigned_pages' => 'مُخصصة للصفحات',\n    'tags_assigned_chapters' => 'مُخصصة للفصول',\n    'tags_assigned_books' => 'مُخصص للكتب',\n    'tags_assigned_shelves' => 'مُخصصة للأرفف',\n    'tags_x_unique_values' => 'قيم الفريدة :count',\n    'tags_all_values' => 'جميع القيم',\n    'tags_view_tags' => 'عرض العلامات',\n    'tags_view_existing_tags' => 'عرض العلامات الموجودة',\n    'tags_list_empty_hint' => 'يمكن تعيين العلامات بواسطة الشريط الجانبي لمحرر الصفحة أو خلال تحرير تفاصيل الكتاب أو الفصل أو الرف.',\n    'attachments' => 'المرفقات',\n    'attachments_explain' => 'ارفع بعض الملفات أو أرفق بعض الروابط لعرضها بصفحتك. ستكون الملفات والروابط معروضة في الشريط الجانبي للصفحة.',\n    'attachments_explain_instant_save' => 'سيتم حفظ التغييرات هنا آنيا.',\n    'attachments_upload' => 'رفع ملف',\n    'attachments_link' => 'إرفاق رابط',\n    'attachments_upload_drop' => 'وبدلاً من ذلك، يمكنك سحب المِلَفّ وإفلاته هنا لتحميله كمرفق.',\n    'attachments_set_link' => 'تحديد الرابط',\n    'attachments_delete' => 'هل أنت متأكد من أنك تريد حذف هذا المرفق؟',\n    'attachments_dropzone' => 'قم بإسقاط الملفات هنا للتحميل',\n    'attachments_no_files' => 'لم تُرفع أي ملفات',\n    'attachments_explain_link' => 'بالإمكان إرفاق رابط في حال عدم تفضيل رفع ملف. قد يكون الرابط لصفحة أخرى أو لملف في أحد خدمات التخزين السحابي.',\n    'attachments_link_name' => 'اسم الرابط',\n    'attachment_link' => 'رابط المرفق',\n    'attachments_link_url' => 'رابط الملف',\n    'attachments_link_url_hint' => 'رابط الموقع أو الملف',\n    'attach' => 'إرفاق',\n    'attachments_insert_link' => 'إضافة رابط مرفق إلى الصفحة',\n    'attachments_edit_file' => 'تعديل الملف',\n    'attachments_edit_file_name' => 'اسم الملف',\n    'attachments_edit_drop_upload' => 'أسقط الملفات أو اضغط هنا للرفع والاستبدال',\n    'attachments_order_updated' => 'تم تحديث ترتيب المرفقات',\n    'attachments_updated_success' => 'تم تحديث تفاصيل المرفق',\n    'attachments_deleted' => 'تم حذف المرفق',\n    'attachments_file_uploaded' => 'تم رفع الملف بنجاح',\n    'attachments_file_updated' => 'تم تحديث الملف بنجاح',\n    'attachments_link_attached' => 'تم إرفاق الرابط بالصفحة بنجاح',\n    'templates' => 'القوالب',\n    'templates_set_as_template' => 'هذه الصفحة عبارة عن قالب',\n    'templates_explain_set_as_template' => 'يمكنك تعيين هذه الصفحة كقالب بحيث تستخدم محتوياتها عند إنشاء صفحات أخرى. سيتمكن المستخدمون الآخرون من استخدام هذا القالب إذا كان لديهم أذونات عرض لهذه الصفحة.',\n    'templates_replace_content' => 'استبدال محتوى الصفحة',\n    'templates_append_content' => 'تذييل محتوى الصفحة',\n    'templates_prepend_content' => 'بادئة محتوى الصفحة',\n\n    // Profile View\n    'profile_user_for_x' => 'المستخدم لـ :time',\n    'profile_created_content' => 'المحتوى المنشأ',\n    'profile_not_created_pages' => 'لم يتم إنشاء أي صفحات بواسطة :userName',\n    'profile_not_created_chapters' => 'لم يتم إنشاء أي فصول بواسطة :userName',\n    'profile_not_created_books' => 'لم يتم إنشاء أي كتب بواسطة :userName',\n    'profile_not_created_shelves' => 'لم يقم \"اسم المستخدم\"بإنشاء أي أرفف',\n\n    // Comments\n    'comment' => 'تعليق',\n    'comments' => 'تعليقات',\n    'comment_add' => 'إضافة تعليق',\n    'comment_none' => 'لا توجد تعليقات لعرضها',\n    'comment_placeholder' => 'ضع تعليقاً هنا',\n    'comment_thread_count' => ':count تعليقات| :count تعليقات',\n    'comment_archived_count' => ':count مؤرشف',\n    'comment_archived_threads' => 'المواضيع المؤرشفة',\n    'comment_save' => 'حفظ التعليق',\n    'comment_new' => 'تعليق جديد',\n    'comment_created' => 'تم التعليق :createDiff',\n    'comment_updated' => 'تم التحديث :updateDiff بواسطة :username',\n    'comment_updated_indicator' => 'تم التحديث',\n    'comment_deleted_success' => 'تم حذف التعليق',\n    'comment_created_success' => 'تمت إضافة التعليق',\n    'comment_updated_success' => 'تم تحديث التعليق',\n    'comment_archive_success' => 'تم أرشفة التعليق',\n    'comment_unarchive_success' => 'تعليق غير مؤرشف',\n    'comment_view' => 'عرض التعليق',\n    'comment_jump_to_thread' => 'انتقل إلى الموضوع',\n    'comment_delete_confirm' => 'تأكيد حذف التعليق؟',\n    'comment_in_reply_to' => 'رداً على :commentId',\n    'comment_reference' => 'المرجع',\n    'comment_reference_outdated' => '(قديمة)',\n    'comment_editor_explain' => 'هذه هي التعليقات المُضافة على هذه الصفحة. يُمكنك إضافة التعليقات وإدارتها عند عرض الصفحة المحفوظة.',\n\n    // Revision\n    'revision_delete_confirm' => 'هل أنت متأكد من أنك تريد حذف هذه المراجعة؟',\n    'revision_restore_confirm' => 'هل أنت متأكد من أنك تريد استعادة هذه المراجعة؟ سيتم استبدال محتوى الصفحة الحالية.',\n    'revision_cannot_delete_latest' => 'لايمكن حذف آخر مراجعة.',\n\n    // Copy view\n    'copy_consider' => 'يرجى مراعاة ما يلي عند نسخ المحتوى.',\n    'copy_consider_permissions' => 'لن يتم نسخ إعدادات الصلاحيات المخصصة.',\n    'copy_consider_owner' => 'سوف تصبح مالكًا لجميع المحتوى المنسوخ.',\n    'copy_consider_images' => 'لن يتم تكرار ملفات صور الصفحة وستحتفظ الصور الأصلية بعلاقتها بالصفحة التي تم تحميلها إليها في الأصل.',\n    'copy_consider_attachments' => 'لن يتم نسخ مرفقات الصفحة.',\n    'copy_consider_access' => 'قد يؤدي تغيير الموقع أو المالك أو الصلاحيات إلى إمكانية وصول الأشخاص الذين لم يتمكنوا من الوصول إلى هذا المحتوى سابقًا.',\n\n    // Conversions\n    'convert_to_shelf' => 'تحويل إلى رف',\n    'convert_to_shelf_contents_desc' => 'يمكنك تحويل هذا الكتاب إلى رف جديد بنفس المحتويات. سيتم تحويل الفصول الموجودة فيه إلى كتب جديدة. إذا احتوى هذا الكتاب على أي صفحات غير موجودة في أي فصل، فسيتم إعادة تسمية الكتاب وإضافة هذه الصفحات إليه، وسيصبح جزءًا من الرف الجديد.',\n    'convert_to_shelf_permissions_desc' => 'سيتم نسخ أي صلاحيات مُحددة لهذا الكتاب إلى الرف الجديد وإلى جميع الكتب الفرعية الجديدة التي لم تُطبّق عليها صلاحيات خاصة بها. يُرجى العلم بأن الصلاحيات على الرفوف لا تنتقل تلقائيًا إلى المحتوى داخلها، كما هو الحال مع الكتب.',\n    'convert_book' => 'تحويل الكتاب',\n    'convert_book_confirm' => 'هل أنت متيقِّن أنك تريد تحويل هذا الكتاب؟',\n    'convert_undo_warning' => 'لا يمكن التراجع عن هذا الأمر بسهولة.',\n    'convert_to_book' => 'تحويله إلى كتاب',\n    'convert_to_book_desc' => 'يمكنك تحويل هذا الفصل إلى كتاب جديد بنفس المحتوى. سيتم نسخ أي صلاحيات مُعيّنة لهذا الفصل إلى الكتاب الجديد، ولكن لن يتم نسخ أي صلاحيات موروثة من الكتاب الأصلي، مما قد يؤدي إلى تغيير في التحكم في الوصول.',\n    'convert_chapter' => 'تحويل الفصل',\n    'convert_chapter_confirm' => 'هل أنت متيقِّن أنك تريد تحويل هذا الفصل؟',\n\n    // References\n    'references' => 'مراجع',\n    'references_none' => 'لا توجد مراجع متعقبة لهذا العنصر.',\n    'references_to_desc' => 'تجد أدناه كل المحتوى المعروف في النظام المرتبط بهذا العنصر.',\n\n    // Watch Options\n    'watch' => 'شاهد',\n    'watch_title_default' => 'التفضيلات الافتراضية',\n    'watch_desc_default' => 'استعادة المشاهدة إلى تفضيلات الإشعارات الافتراضية فقط.',\n    'watch_title_ignore' => 'تجاهل',\n    'watch_desc_ignore' => 'تجاهل كافة الإشعارات، بما في ذلك تلك الواردة من تفضيلات مستوى المستخدم.',\n    'watch_title_new' => 'صفحات جديدة',\n    'watch_desc_new' => 'إعلام عند إنشاء أي صفحة جديدة ضمن هذا العنصر.',\n    'watch_title_updates' => 'جميع تحديثات الصفحة',\n    'watch_desc_updates' => 'إشعار بجميع الصفحات الجديدة والتغييرات في الصفحات.',\n    'watch_desc_updates_page' => 'إشعار عند حدوث أي تغييرات في الصفحة.',\n    'watch_title_comments' => 'جميع تحديثات الصفحة والتعليقات',\n    'watch_desc_comments' => 'إشعار بجميع الصفحات الجديدة، وتغييرات الصفحات والتعليقات الجديدة.',\n    'watch_desc_comments_page' => 'إشعار عند حدوث تغييرات في الصفحة أو تعليقات جديدة.',\n    'watch_change_default' => 'تغيير تفضيلات الإشعارات الافتراضية',\n    'watch_detail_ignore' => 'تجاهل الإشعارات',\n    'watch_detail_new' => 'ترقب الصفحات الجديدة',\n    'watch_detail_updates' => 'مشاهدة الصفحات الجديدة والتحديثات',\n    'watch_detail_comments' => 'مشاهدة الصفحات الجديدة والتحديثات والتعليقات',\n    'watch_detail_parent_book' => 'المشاهدة عبر الكتاب الرئيس',\n    'watch_detail_parent_book_ignore' => 'التجاهل عبر الكتاب الرئيس',\n    'watch_detail_parent_chapter' => 'المشاهدة عبر الفصل الرئيس',\n    'watch_detail_parent_chapter_ignore' => 'التجاهل عبر الفصل الرئيس',\n];\n"
  },
  {
    "path": "lang/ar/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'لم يؤذن لك بالدخول للصفحة المطلوبة.',\n    'permissionJson' => 'لم يؤذن لك بعمل الإجراء المطلوب.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'يوجد مستخدم ببيانات مختلفة مسجل بالنظام للبريد الإلكتروني :email.',\n    'auth_pre_register_theme_prevention' => 'لم يتمكن حساب المستخدم من التسجيل للحصول على التفاصيل المقدمة',\n    'email_already_confirmed' => 'تم تأكيد البريد الإلكتروني من قبل, الرجاء محاولة تسجيل الدخول.',\n    'email_confirmation_invalid' => 'رابط التأكيد غير صحيح أو قد تم استخدامه من قبل, الرجاء محاولة التسجيل من جديد.',\n    'email_confirmation_expired' => 'صلاحية رابط التأكيد انتهت, تم إرسال رسالة تأكيد جديدة لعنوان البريد الإلكتروني.',\n    'email_confirmation_awaiting' => 'عنوان البريد الإلكتروني للحساب قيد الاستخدام يحتاج إلى تأكيد',\n    'ldap_fail_anonymous' => 'فشل الوصول إلى LDAP باستخدام الربط المجهول',\n    'ldap_fail_authed' => 'فشل الوصول إلى LDAP باستخدام dn و كلمة السر المعطاة',\n    'ldap_extension_not_installed' => 'لم يتم تثبيت إضافة LDAP PHP',\n    'ldap_cannot_connect' => 'لا يمكن الاتصال بخادم ldap, فشل الاتصال المبدئي',\n    'saml_already_logged_in' => 'تم تسجيل الدخول بالفعل',\n    'saml_no_email_address' => 'تعذر العثور على عنوان بريد إلكتروني، لهذا المستخدم، في البيانات المقدمة من نظام المصادقة الخارجي',\n    'saml_invalid_response_id' => 'لم يتم التعرف على الطلب من نظام التوثيق الخارجي من خلال عملية تبدأ بهذا التطبيق. العودة بعد تسجيل الدخول يمكن أن يسبب هذه المشكلة.',\n    'saml_fail_authed' => 'تسجيل الدخول باستخدام :system فشل، النظام لم يوفر التفويض الناجح',\n    'oidc_already_logged_in' => 'تم تسجيل الدخول مسبقاً',\n    'oidc_no_email_address' => 'تعذر العثور على عنوان بريد إلكتروني، لهذا المستخدم، في البيانات المقدمة من نظام المصادقة الخارجي',\n    'oidc_fail_authed' => 'تسجيل الدخول باستخدام :system فشل، النظام لم يوفر التفويض الناجح',\n    'social_no_action_defined' => 'لم يتم تعريف أي إجراء',\n    'social_login_bad_response' => \"حصل خطأ خلال تسجيل الدخول باستخدام :socialAccount \\n:error\",\n    'social_account_in_use' => 'حساب :socialAccount قيد الاستخدام حالياً, الرجاء محاولة الدخول باستخدام خيار :socialAccount.',\n    'social_account_email_in_use' => 'البريد الإلكتروني :email مستخدم. إذا كان لديكم حساب فبإمكانكم ربط حساب :socialAccount من إعدادات ملفكم.',\n    'social_account_existing' => 'تم ربط حساب :socialAccount بملفكم من قبل.',\n    'social_account_already_used_existing' => 'حساب :socialAccount مستخدَم من قبل مستخدم آخر.',\n    'social_account_not_used' => 'حساب :socialAccount غير مرتبط بأي مستخدم. الرجاء ربطه من خلال إعدادات ملفكم. ',\n    'social_account_register_instructions' => 'إذا لم يكن لديكم حساب فيمكنكم التجسيل باستخدام خيار :socialAccount.',\n    'social_driver_not_found' => 'لم يتم العثور على السوشيال درايفر \"Social driver\"',\n    'social_driver_not_configured' => 'لم يتم تهيئة إعدادات حسابك الاجتماعي بشكل صحيح.',\n    'invite_token_expired' => 'انتهت صلاحية رابط هذه الدعوة. يمكنك بدلاً من ذلك محاولة إعادة تعيين كلمة مرور حسابك.',\n    'login_user_not_found' => 'لم يتم العثور على مستخدم لهذا الإجراء.',\n\n    // System\n    'path_not_writable' => 'لا يمكن الرفع إلى مسار :filePath. الرجاء التأكد من قابلية الكتابة إلى الخادم.',\n    'cannot_get_image_from_url' => 'لا يمكن الحصول على الصورة من :url',\n    'cannot_create_thumbs' => 'لا يمكن للخادم إنشاء صور مصغرة. الرجاء التأكد من تثبيت إضافة GD PHP.',\n    'server_upload_limit' => 'الخادم لا يسمح برفع ملفات بهذا الحجم. الرجاء محاولة الرفع بحجم أصغر.',\n    'server_post_limit' => 'لا يمكن للخادم تلقي كمية البيانات المتاحة. حاول مرة أخرى باستخدام بيانات أقل أو ملف أصغر.',\n    'uploaded'  => 'الخادم لا يسمح برفع ملفات بهذا الحجم. الرجاء محاولة الرفع بحجم أصغر.',\n\n    // Drawing & Images\n    'image_upload_error' => 'حدث خطأ خلال رفع الصورة',\n    'image_upload_type_error' => 'صيغة الصورة المرفوعة غير صالحة',\n    'image_upload_replace_type' => 'يجب أن يكون استبدال ملف الصورة من نفس النوع',\n    'image_upload_memory_limit' => 'فشل في التعامل مع تحميل الصورة و/أو إنشاء الصور المصغرة بسبب حدود موارد النظام.',\n    'image_thumbnail_memory_limit' => 'فشل في إنشاء تغيرات حجم الصورة بسبب حدود موارد النظام.',\n    'image_gallery_thumbnail_memory_limit' => 'فشل في إنشاء الصور المصغرة للمعرض بسبب حدود موارد النظام.',\n    'drawing_data_not_found' => 'تعذر تحميل بيانات الرسم. قد لا يكون ملف الرسم موجودا أو قد لا يكون لديك إذن للوصول إليه.',\n\n    // Attachments\n    'attachment_not_found' => 'لم يتم العثور على المرفق',\n    'attachment_upload_error' => 'حدث خطأ أثناء تحميل الملف المرفق',\n\n    // Pages\n    'page_draft_autosave_fail' => 'فشل حفظ المسودة. الرجاء التأكد من وجود اتصال بالإنترنت قبل حفظ الصفحة',\n    'page_draft_delete_fail' => 'فشل في حذف مسودة الصفحة وجلب محتوى الصفحة الحالية المحفوظة',\n    'page_custom_home_deletion' => 'لا يمكن حذف الصفحة إذا كانت محددة كصفحة رئيسية',\n\n    // Entities\n    'entity_not_found' => 'الكيان غير موجود',\n    'bookshelf_not_found' => 'رف الكتب غير موجود',\n    'book_not_found' => 'لم يتم العثور على الكتاب',\n    'page_not_found' => 'لم يتم العثور على الصفحة',\n    'chapter_not_found' => 'لم يتم العثور على الفصل',\n    'selected_book_not_found' => 'لم يتم العثور على الكتاب المحدد',\n    'selected_book_chapter_not_found' => 'لم يتم العثور على الكتاب أو الفصل المحدد',\n    'guests_cannot_save_drafts' => 'لا يمكن حفظ المسودات من قبل الضيوف',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'لا يمكن حذف المشرف الوحيد',\n    'users_cannot_delete_guest' => 'لا يمكن حذف المستخدم الضيف',\n    'users_could_not_send_invite' => 'لم يتم إنشاء المستخدم بسبب فشل إرسال بريد الدعوة',\n\n    // Roles\n    'role_cannot_be_edited' => 'لا يمكن تعديل هذا الدور',\n    'role_system_cannot_be_deleted' => 'هذا الدور خاص بالنظام ولا يمكن حذفه',\n    'role_registration_default_cannot_delete' => 'لا يمكن حذف الدور إذا كان مسجل كالدور الأساسي بعد تسجيل الحساب',\n    'role_cannot_remove_only_admin' => 'هذا المستخدم هو المستخدم الوحيد المعين لدور المسؤول. قم بتعيين دور المسؤول لمستخدم آخر قبل محاولة إزالته هنا.',\n\n    // Comments\n    'comment_list' => 'حصل خطأ خلال جلب التعليقات.',\n    'cannot_add_comment_to_draft' => 'لا يمكن إضافة تعليقات على مسودة.',\n    'comment_add' => 'حصل خطاً خلال إضافة / تحديث التعليق.',\n    'comment_delete' => 'حصل خطأ خلال حذف التعليق.',\n    'empty_comment' => 'لايمكن إضافة تعليق فارغ.',\n\n    // Error pages\n    '404_page_not_found' => 'لم يتم العثور على الصفحة',\n    'sorry_page_not_found' => 'عفواً, لا يمكن العثور على الصفحة التي تبحث عنها.',\n    'sorry_page_not_found_permission_warning' => 'إذا كنت تتوقع أن تكون هذه الصفحة موجودة، قد لا يكون لديك تصريح بمشاهدتها.',\n    'image_not_found' => 'لم يتم العثور على الصورة',\n    'image_not_found_subtitle' => 'عذراً، لم يتم العثور على ملف الصورة الذي كنت تبحث عنه.',\n    'image_not_found_details' => 'إذا كنت تتوقع وجود هذه الصورة ربما تم حذفها.',\n    'return_home' => 'العودة للصفحة الرئيسية',\n    'error_occurred' => 'حدث خطأ',\n    'app_down' => ':appName لا يعمل حالياً',\n    'back_soon' => 'سيعود للعمل قريباً.',\n\n    // Import\n    'import_zip_cant_read' => 'لم أتمكن من قراءة المِلَفّ المضغوط -ZIP-.',\n    'import_zip_cant_decode_data' => 'لم نتمكن من العثور على محتوى المِلَفّ المضغوط data.json وفك تشفيره.',\n    'import_zip_no_data' => 'لا تتضمن بيانات المِلَفّ المضغوط أي محتوى متوقع للكتاب أو الفصل أو الصفحة.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'فشل التحقق من صحة استيراد المِلَفّ المضغوط بسبب الأخطاء التالية:',\n    'import_zip_failed_notification' => 'فشل استيراد المِلَفّ المضغوط.',\n    'import_perms_books' => 'أنت تفتقر إلى الصلاحيات المطلوبة لإنشاء الكتب.',\n    'import_perms_chapters' => 'أنت تفتقر إلى الصلاحيات المطلوبة لإنشاء الفصول.',\n    'import_perms_pages' => 'أنت تفتقر إلى الصلاحيات المطلوبة لإنشاء الصفحات.',\n    'import_perms_images' => 'أنت تفتقر إلى الصلاحيات المطلوبة لإنشاء الصور.',\n    'import_perms_attachments' => 'أنت تفتقر إلى الصَّلاحِيَة المطلوب لإنشاء المرفقات.',\n\n    // API errors\n    'api_no_authorization_found' => 'لم يتم العثور على رمز ترخيص مميز في الطلب',\n    'api_bad_authorization_format' => 'تم العثور على رمز ترخيص مميز في الطلب ولكن يبدو أن التنسيق غير صحيح',\n    'api_user_token_not_found' => 'لم يتم العثور على رمز API مطابق لرمز الترخيص المُقدم',\n    'api_incorrect_token_secret' => 'الشفرة المُقدمة لرمز API المستخدم المحدد غير صحيحة',\n    'api_user_no_api_permission' => 'مالك رمز API المستخدم ليس لديه الصلاحية لإجراء مكالمات API',\n    'api_user_token_expired' => 'انتهت صلاحية رمز الترخيص المستخدم',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'حدث خطأ عند إرسال بريد إلكتروني تجريبي:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'الرابط لا يتطابق مع الاعدادات المسموح بها لاستضافة SSR',\n];\n"
  },
  {
    "path": "lang/ar/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'تعليق جديد على الصفحة: :pageName',\n    'new_comment_intro' => 'قام أحد المستخدمين بالتعليق على صفحة في :appName:',\n    'new_page_subject' => 'صفحة جديدة: :pageName',\n    'new_page_intro' => 'تم إنشاء صفحة جديدة في :appName:',\n    'updated_page_subject' => 'تم تحديث الصفحة: :pageName',\n    'updated_page_intro' => 'تم تحديث الصفحة في :appName:',\n    'updated_page_debounce' => 'لمنع تلقي عدد كبير من الإشعارات، لن يتم إرسال إشعارات إليك لفترة من الوقت لإجراء المزيد من التعديلات على هذه الصفحة بواسطة نفس المحرر.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'اسم الصفحة:',\n    'detail_page_path' => 'مسار الصفحة:',\n    'detail_commenter' => 'المُعَلِق:',\n    'detail_comment' => 'التعليق:',\n    'detail_created_by' => 'أنشئ من طرف:',\n    'detail_updated_by' => 'تم التحديث بواسطة:',\n\n    'action_view_comment' => 'عرض التعليق',\n    'action_view_page' => 'عرض الصفحة',\n\n    'footer_reason' => 'لقد تم إرسال هذا الإشعار إليك لأن :link يغطي هذا النوع من النشاط لهذا العنصر.',\n    'footer_reason_link' => 'إعدادات الإشعارات الخاصة بك',\n];\n"
  },
  {
    "path": "lang/ar/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; السابق',\n    'next'     => 'التالي &raquo;',\n\n];\n"
  },
  {
    "path": "lang/ar/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'يجب أن تتكون كلمة السر من ستة أحرف على الأقل وأن تطابق التأكيد.',\n    'user' => \"لم يتم العثور على مستخدم بعنوان البريد الإلكتروني المعطى.\",\n    'token' => 'رمز إعادة تعيين كلمة السر غير صالح لعنوان هذا البريد الإلكتروني.',\n    'sent' => 'تم إرسال رابط تجديد كلمة السر إلى بريدكم الإلكتروني!',\n    'reset' => 'تم تجديد كلمة السر الخاصة بكم!',\n\n];\n"
  },
  {
    "path": "lang/ar/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'حسابي',\n\n    'shortcuts' => 'الاختصارات',\n    'shortcuts_interface' => 'خيارات اختصار واجهة المستخدم',\n    'shortcuts_toggle_desc' => 'هنا يمكنك تمكين أو تعطيل اختصارات واجهة نظام لوحة المفاتيح، المستخدمة للتنقل والإجراءات.',\n    'shortcuts_customize_desc' => 'يمكنك تخصيص كل اختصار من الاختصارات أدناه. ما عليك سوى الضغط على تركيبة المفاتيح المطلوبة بعد تحديد مدخل الاختصار.',\n    'shortcuts_toggle_label' => 'تم تمكين اختصارات لوحة المفاتيح',\n    'shortcuts_section_navigation' => 'التنقل',\n    'shortcuts_section_actions' => 'الإجراءات المشتركة',\n    'shortcuts_save' => 'حفظ الاختصارات',\n    'shortcuts_overlay_desc' => 'ملاحظة: عندما يتم تمكين الاختصارات، تتوفر تراكب المساعد عن طريق الضغط على \"؟\" الذي سيسلط الضوء على الاختصارات المتاحة للإجراءات المرئية حاليا على الشاشة.',\n    'shortcuts_update_success' => 'تم تحديث خيارات الاختصار!',\n    'shortcuts_overview_desc' => 'إدارة اختصارات لوحة المفاتيح التي يمكنك استخدامها للتنقل في واجهة مستخدم النظام.',\n\n    'notifications' => 'إعدادات الإشعارات',\n    'notifications_desc' => 'التحكم في إشعارات البريد الإلكتروني الذي تتلقاها عند إجراء نشاط معين داخل النظام.',\n    'notifications_opt_own_page_changes' => 'إشعاري عند حدوث تغييرات في الصفحات التي أملكها',\n    'notifications_opt_own_page_comments' => 'إشعاري بشأن التعليقات على الصفحات التي أملكها',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'إشعاري عند الردود على تعليقاتي',\n    'notifications_save' => 'حفظ اﻹعدادات',\n    'notifications_update_success' => 'تم تحديث إعدادات الإشعارات!',\n    'notifications_watched' => 'العناصر التي تمت مشاهدتها وتجاهلها',\n    'notifications_watched_desc' => 'فيما يلي العناصر التي طُبِّقت عليها إعدادات ساعة مخصصة. لتحديث إعداداتك، استعرض العنصر ثم ابحث عن خيارات الساعة في الشريط الجانبي.',\n\n    'auth' => 'الوصول و الأمان',\n    'auth_change_password' => 'تغيير كلمة السر',\n    'auth_change_password_desc' => 'غيّر كلمة السر التي تستخدمها لتسجيل الدخول إلى التطبيق. يجب ألا تقل عن 8 أحرف.',\n    'auth_change_password_success' => 'تم تحديث كلمة السر!',\n\n    'profile' => 'تفاصيل المِلَفّ الشخصي',\n    'profile_desc' => 'إدارة تفاصيل حسابك الذي يمثلك أمام المستخدمين الآخرين، بالإضافة إلى التفاصيل المستخدمة للتواصل وتخصيص النظام.',\n    'profile_view_public' => 'عرض المِلَفّ الشخصي العام',\n    'profile_name_desc' => 'إعداد اسم العرض الخاص بك الذي سيكون مرئيًا للمستخدمين الآخرين في النظام من خلال النشاط الذي تقوم به والمحتوى الذي تملكه.',\n    'profile_email_desc' => 'سيتم استخدام هذا البريد الإلكتروني للإشعارات، وبناءً على مصادقة النظام النشط، سيتم استخدام الوصول إلى النظام.',\n    'profile_email_no_permission' => 'للأسف، ليس لديك إذن لتغيير عنوان بريدك الإلكتروني. إذا كنت ترغب في تغييره، فعليك طلب ذلك من أحد المسؤولين.',\n    'profile_avatar_desc' => 'اختر صورةً تُمثّلك أمام الآخرين في النظام. يُفضّل أن تكون الصورة مربعةً، وعرضها وارتفاعها حوالي ٢٥٦ بكسل.',\n    'profile_admin_options' => 'خيارات المسؤول',\n    'profile_admin_options_desc' => 'يمكنك العثور على خيارات إضافية على مستوى المسؤول، مثل تلك الخاصة بإدارة تعيينات الأدوار، لحساب المستخدم الخاص بك في منطقة \"الإعدادات > المستخدمون\" في التطبيق.',\n\n    'delete_account' => 'حذف الحساب',\n    'delete_my_account' => 'حذف حسابي',\n    'delete_my_account_desc' => 'سيؤدي هذا إلى حذف حساب المستخدم الخاص بك بالكامل من النظام. لن تتمكن من استعادة هذا الحساب أو التراجع عن هذا الإجراء. سيبقى المحتوى الذي أنشأته، مثل الصفحات التي أنشأتها والصور التي رفعتها، كما هي.',\n    'delete_my_account_warning' => 'هل أنت متأكد أنك تريد حذف حسابك؟',\n];\n"
  },
  {
    "path": "lang/ar/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'الإعدادات',\n    'settings_save' => 'حفظ الإعدادات',\n    'system_version' => 'إصدار النظام',\n    'categories' => 'التصنيفات',\n\n    // App Settings\n    'app_customization' => 'تخصيص',\n    'app_features_security' => 'الميزات و الأمان',\n    'app_name' => 'اسم التطبيق',\n    'app_name_desc' => 'سيتم عرض هذا الاسم في الترويسة وفي أي رسالة بريد إلكتروني.',\n    'app_name_header' => 'عرض اسم التطبيق في الترويسة؟',\n    'app_public_access' => 'الوصول العام',\n    'app_public_access_desc' => 'تمكين هذا الخيار سيسمح للزوار، الذين لم يتم تسجيل دخولهم، بالوصول إلى المحتوى في مثيل مكدس الكتب الخاص بك.',\n    'app_public_access_desc_guest' => 'يمكن التحكم في وصول الزوار العموميين من خلال المستخدم \"الضيف\".',\n    'app_public_access_toggle' => 'السماح بالوصول العام',\n    'app_public_viewing' => 'السماح بالعرض على العامة؟',\n    'app_secure_images' => 'تفعيل حماية أكبر لرفع الصور؟',\n    'app_secure_images_toggle' => 'لمزيد من الحماية',\n    'app_secure_images_desc' => 'لتحسين أداء النظام, ستكون جميع الصور متاحة للعامة. هذا الخيار يضيف سلسلة من الحروف والأرقام العشوائية صعبة التخمين إلى رابط الصورة. الرجاء التأكد من تعطيل فهرسة المسارات لمنع الوصول السهل.',\n    'app_default_editor' => 'محرر الصفحة الافتراضي',\n    'app_default_editor_desc' => 'حدد أي محرر سيتم استخدامه بشكل افتراضي عند تحرير صفحات جديدة. يمكن تجاوز هذا على مستوى الصفحة حيث تسمح الأذونات.',\n    'app_custom_html' => 'Custom HTML head content',\n    'app_custom_html_desc' => 'سيتم إدراج أي محتوى مضاف هنا في الجزء السفلي من قسم <head> من كل صفحة. هذا أمر مفيد لتجاوز الأنماط أو إضافة رمز التحليل.',\n    'app_custom_html_disabled_notice' => 'تم تعطيل محتوى HTML الرئيسي المخصص في صفحة الإعدادات هذه لضمان عكس أي تغييرات متتالية.',\n    'app_logo' => 'شعار التطبيق',\n    'app_logo_desc' => 'يستخدم هذا في شريط رأس التطبيق، ضمن مجالات أخرى. يجب أن تكون هذه الصورة 86 بكسل في الطول. سيتم تقليص الصور الكبيرة.',\n    'app_icon' => 'أيقونة التطبيق',\n    'app_icon_desc' => 'يستخدم هذا الرمز لعلامات تبويب المتصفح والرموز المختصرة. يجب أن تكون هذه صورة PNG مربعة 256px.',\n    'app_homepage' => 'الصفحة الرئيسية للتطبيق',\n    'app_homepage_desc' => 'الرجاء اختيار صفحة لتصبح الصفحة الرئيسية بدل من الافتراضية. سيتم تجاهل جميع الأذونات الخاصة بالصفحة المختارة.',\n    'app_homepage_select' => 'اختر صفحة',\n    'app_footer_links' => 'روابط تذييل الصفحة',\n    'app_footer_links_desc' => 'إضافة روابط لعرضها داخل تذييل الموقع. سيتم عرضها في أسفل معظم الصفحات، بما في ذلك تلك التي لا تتطلب تسجيل الدخول. يمكنك استخدام تسمية \"trans::<key>لاستخدام الترجمات المحددة في النظام. على سبيل المثال: باستخدام \"trans::common.privacy_policy\" سيوفر النص المترجم \"Privacy Policy\" و \"trans::common.terms_of_service\" سيوفر النص المترجم \"شروط الخدمة\".',\n    'app_footer_links_label' => 'تسمية الرابط',\n    'app_footer_links_url' => 'عنوان الرابط',\n    'app_footer_links_add' => 'إضافة رابط تذييل الصفحة',\n    'app_disable_comments' => 'تعطيل التعليقات',\n    'app_disable_comments_toggle' => 'تعطيل التعليقات',\n    'app_disable_comments_desc' => 'تعطيل التعليقات على جميع الصفحات داخل التطبيق. التعليقات الموجودة من الأصل لن تكون ظاهرة.',\n\n    // Color settings\n    'color_scheme' => 'مخطط ألوان التطبيق',\n    'color_scheme_desc' => 'حدّد الألوان المستخدمة في واجهة مستخدم التطبيق. يمكن ضبط الألوان بشكل منفصل للوضعين الداكن والفاتح لتناسب المظهر بشكل أفضل ولضمان وضوح النص.',\n    'ui_colors_desc' => 'عيّن اللون الأساسي للتطبيق ولون الرابط الافتراضي. يُستخدم اللون الأساسي بشكل رئيس في شعار الصفحة الرئيسة والأزرار وزخارف الواجهة. أما اللون الافتراضي للرابط، فيُستخدم للروابط والإجراءات النصية، سواءً داخل المحتوى المكتوب أو في واجهة التطبيق.',\n    'app_color' => 'اللون الأساسي',\n    'link_color' => 'لون الرابط الافتراضي',\n    'content_colors_desc' => 'حدّد ألوان جميع عناصر هيكل تنظيم الصفحة. يُنصح باختيار ألوان بنفس سطوع الألوان الافتراضية لسهولة القراءة.',\n    'bookshelf_color' => 'لون الرف',\n    'book_color' => 'لون الكتاب',\n    'chapter_color' => 'لون الفصل',\n    'page_color' => 'لون الصفحة',\n    'page_draft_color' => 'لون مسودة الصفحة',\n\n    // Registration Settings\n    'reg_settings' => 'إعدادات التسجيل',\n    'reg_enable' => 'تمكين التسجيل',\n    'reg_enable_toggle' => 'تمكين التسجيل',\n    'reg_enable_desc' => 'عند تمكين التسجيل سيكون المستخدم قادرا على تسجيل نفسه كمستخدم تطبيق. عند التسجيل يعطى لهم دور مستخدم افتراضي وحيد.',\n    'reg_default_role' => 'دور المستخدم الأساسي بعد التسجيل',\n    'reg_enable_external_warning' => 'يتم تجاهل الخيار أعلاه بينما يتم تفعيل مصادقة LDAP الخارجية أو SAML. حسابات المستخدم للأعضاء غير الحاليين سيتم إنشاؤها تلقائياً إذا كانت المصادقة، مقابل النظام الخارجي المستخدم، ناجحة.',\n    'reg_email_confirmation' => 'تأكيد البريد الإلكتروني',\n    'reg_email_confirmation_toggle' => 'يتطلب تأكيد البريد الإلكتروني',\n    'reg_confirm_email_desc' => 'إذا تم استخدام قيود للمجال سيصبح التأكيد عن طريق البريد الإلكتروني إلزامي وسيتم تجاهل القيمة أسفله.',\n    'reg_confirm_restrict_domain' => 'تقييد التسجيل على مجال محدد',\n    'reg_confirm_restrict_domain_desc' => 'أدخل قائمة مفصولة بفواصل لنطاقات البريد الإلكتروني التي ترغب في تقييد التسجيل إليها. سيتم إرسال بريد إلكتروني للمستخدمين لتأكيد عنوانهم قبل السماح لهم بالتفاعل مع التطبيق. <br> لاحظ أن المستخدمين سيكونون قادرين على تغيير عناوين البريد الإلكتروني الخاصة بهم بعد التسجيل بنجاح.',\n    'reg_confirm_restrict_domain_placeholder' => 'لم يتم اختيار أي قيود',\n\n    // Sorting Settings\n    'sorting' => 'القوائم و الفرز',\n    'sorting_book_default' => 'ترتيب الكتاب الافتراضي',\n    'sorting_book_default_desc' => 'حدد قاعدة الترتيب الافتراضية لتطبيقها على الكتب الجديدة. لن يؤثر هذا على الكتب الحالية، ويمكن تجاوزه لكل كتاب على حدة.',\n    'sorting_rules' => 'قواعد الترتيب',\n    'sorting_rules_desc' => 'هذه هي عمليات الترتيب المحددة مسبقًا الذي يمكن تطبيقها على المحتوى الموجود في النظام.',\n    'sort_rule_assigned_to_x_books' => 'مُعيَّن إلى :count كتاب|مُعيَّن إلى :count كتاب',\n    'sort_rule_create' => 'إنشاء قاعدة الترتيب',\n    'sort_rule_edit' => 'تعديل قاعدة الترتيب',\n    'sort_rule_delete' => 'حذف قاعدة الترتيب',\n    'sort_rule_delete_desc' => 'أزل قاعدة الترتيب هذه من النظام. الكتب التي تستخدم هذا الفرز ستعود إلى الفرز اليدوي.',\n    'sort_rule_delete_warn_books' => 'تُستخدم قاعدة الترتيب هذه حاليًا على :count كتاب/كتب. متيقن من رغبتك في حذف هذا؟',\n    'sort_rule_delete_warn_default' => 'تُستخدم قاعدة الترتيب هذه حاليًا كإعداد افتراضي للكتب. متيقن من رغبتك في حذفها؟',\n    'sort_rule_details' => 'تفاصيل قاعدة الترتيب',\n    'sort_rule_details_desc' => 'تعيين اسم لقاعدة الترتيب هذه، التي ستظهر في القوائم عندما يقوم المستخدمون باختيار نوع ما.',\n    'sort_rule_operations' => 'عمليات الترتيب',\n    'sort_rule_operations_desc' => 'جهّز إجراءات الترتيب المطلوب تنفيذها بنقلها من قائمة العمليات المتاحة. عند الاستخدام، سيتم تطبيق العمليات بالترتيب من الأعلى إلى الأسفل. أي تغييرات تُجرى هنا ستُطبّق على جميع الكتب المُخصّصة عند الحفظ.',\n    'sort_rule_available_operations' => 'العمليات المتاحة',\n    'sort_rule_available_operations_empty' => 'لا توجد عمليات متبقية',\n    'sort_rule_configured_operations' => 'العمليات المُهيأة',\n    'sort_rule_configured_operations_empty' => 'اسحب/أضف العمليات من قائمة \"العمليات المتاحة\"',\n    'sort_rule_op_asc' => '(تصاعدي)',\n    'sort_rule_op_desc' => '(تنازلي)',\n    'sort_rule_op_name' => 'الاسم - أبجديًا',\n    'sort_rule_op_name_numeric' => 'الاسم - رقمي',\n    'sort_rule_op_created_date' => 'تاريخ الإنشاء',\n    'sort_rule_op_updated_date' => 'تاريخ التحديث',\n    'sort_rule_op_chapters_first' => 'الفصول الأولى',\n    'sort_rule_op_chapters_last' => 'الفصول الأخيرة',\n    'sorting_page_limits' => 'حدود العرض لكل صفحة',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'الصيانة',\n    'maint_image_cleanup' => 'تنظيف الصور',\n    'maint_image_cleanup_desc' => 'مسح الصفحة ومراجعة المحتوى للتحقق من أي الصور والرسوم المستخدمة حاليًا وأي الصور زائدة عن الحاجة. تأكد من إنشاء قاعدة بيانات كاملة و نسخة احتياطية للصور قبل تشغيل هذا.',\n    'maint_delete_images_only_in_revisions' => 'قم أيضًا بحذف الصور الموجودة فقط في مراجعات الصفحة القديمة',\n    'maint_image_cleanup_run' => 'بدء التنظيف',\n    'maint_image_cleanup_warning' => 'يوجد عدد :count من الصور المحتمل عدم استخدامها. تأكيد حذف الصور؟',\n    'maint_image_cleanup_success' => 'تم إيجاد وحذف عدد :count من الصور المحتمل عدم استخدامها!',\n    'maint_image_cleanup_nothing_found' => 'لم يتم حذف أي شيء لعدم وجود أي صور غير مسمتخدمة',\n    'maint_send_test_email' => 'إرسال بريد إلكتروني تجريبي',\n    'maint_send_test_email_desc' => 'يرسل هذا بريدًا إلكترونيًا تجريبيًا إلى عنوان بريدك الإلكتروني المحدد في ملفك الشخصي.',\n    'maint_send_test_email_run' => 'إرسال بريد إليكتروني تجريبي',\n    'maint_send_test_email_success' => 'تم إرسال البريد الإلكتروني إلى:العنوان',\n    'maint_send_test_email_mail_subject' => 'اختبار البريد الإلكتروني',\n    'maint_send_test_email_mail_greeting' => 'يبدو أن تسليم البريد الإلكتروني يعمل!',\n    'maint_send_test_email_mail_text' => 'تهانينا! كما تلقيت إشعار هذا البريد الإلكتروني، يبدو أن إعدادات البريد الإلكتروني الخاص بك قد تم تكوينها بشكل صحيح.',\n    'maint_recycle_bin_desc' => 'تُرسل الأرفف والكتب والفصول والصفحات المحذوفة إلى سلة المحذوفات حتى يمكن استعادتها أو حذفها نهائيًا. قد يتم إزالة العناصر الأقدم في سلة المحذوفات تلقائيًا بعد فترة اعتمادًا على تكوين النظام.',\n    'maint_recycle_bin_open' => 'افتح سلة المحذوفات',\n    'maint_regen_references' => 'إعادة إنشاء المراجع',\n    'maint_regen_references_desc' => 'سيعيد هذا الإجراء بناء فِهْرِس المراجع بين العناصر داخل قاعدة البيانات. عادةً ما يتم ذلك تلقائيًا، ولكنه قد يكون مفيدًا لفهرسة المحتوى القديم أو المحتوى المُضاف بطرق غير رسمية.',\n    'maint_regen_references_success' => 'لقد تم تجديد فِهْرِس المرجع!',\n    'maint_timeout_command_note' => 'ملاحظة: قد يستغرق تنفيذ هذا الإجراء بعض الوقت، مما قد يؤدي إلى مشاكل في مهلة التنفيذ في بعض بيئات الويب. كبديل، يمكن تنفيذ هذا الإجراء باستخدام سطر الأوامر.',\n\n    // Recycle Bin\n    'recycle_bin' => 'سلة المحذوفات',\n    'recycle_bin_desc' => 'هنا يمكنك استعادة العناصر التي تم حذفها أو اختيار إزالتها نهائيا من النظام. هذه القائمة غير مصفاة خلافاً لقوائم الأنشطة المماثلة في النظام حيث يتم تطبيق عوامل تصفية الأذونات.',\n    'recycle_bin_deleted_item' => 'عنصر محذوف',\n    'recycle_bin_deleted_parent' => 'اﻷب',\n    'recycle_bin_deleted_by' => 'حُذف بواسطة',\n    'recycle_bin_deleted_at' => 'وقت الحذف',\n    'recycle_bin_permanently_delete' => 'حُذف نهائيًا',\n    'recycle_bin_restore' => 'استرجاع',\n    'recycle_bin_contents_empty' => 'سلة المحذوفات فارغة حاليًا',\n    'recycle_bin_empty' => 'إفراغ سلة المحذوفات',\n    'recycle_bin_empty_confirm' => 'سيؤدي هذا إلى إتلاف جميع العناصر الموجودة في سلة المحذوفات بشكل دائم بما في ذلك المحتوى الموجود داخل كل عنصر. هل أنت متأكد من أنك تريد إفراغ سلة المحذوفات؟',\n    'recycle_bin_destroy_confirm' => 'سيؤدي هذا الإجراء إلى حذف هذا العنصر نهائيًا من النظام، بالإضافة إلى أي عناصر فرعية مدرجة أدناه، ولن تتمكن من استعادة هذا المحتوى. هل أنت متيقِّن من رغبتك في حذف هذا العنصر نهائيًا؟',\n    'recycle_bin_destroy_list' => 'العناصر المراد تدميرها',\n    'recycle_bin_restore_list' => 'العناصر المراد استرجاعها',\n    'recycle_bin_restore_confirm' => 'سيعيد هذا الإجراء العنصر المحذوف ، بما في ذلك أي عناصر فرعية ، إلى موقعه الأصلي. إذا تم حذف الموقع الأصلي منذ ذلك الحين ، وهو الآن في سلة المحذوفات ، فسيلزم أيضًا استعادة العنصر الأصلي.',\n    'recycle_bin_restore_deleted_parent' => 'تم حذف أصل هذا العنصر أيضًا. سيبقى حذفه حتى يتم استعادة ذلك الأصل أيضًا.',\n    'recycle_bin_restore_parent' => 'استعادة اﻷب',\n    'recycle_bin_destroy_notification' => 'المحذوف: قُم بعد إجمالي العناصر من سلة المحذوفات.',\n    'recycle_bin_restore_notification' => 'المرتجع: قُم بعد إجمالي العناصر من سلة المحذوفات.',\n\n    // Audit Log\n    'audit' => 'سجل المراجعة',\n    'audit_desc' => 'يعرض هذا السجل قائمة بالأنشطة المتعقبة في النظام. هذه القائمة غير مصفاة خلافاً لقوائم الأنشطة المماثلة في النظام حيث يتم تطبيق عوامل تصفية الأذونات.',\n    'audit_event_filter' => 'تصفية الحدث',\n    'audit_event_filter_no_filter' => 'لا يوجد فلتر',\n    'audit_deleted_item' => 'عنصر محذوف',\n    'audit_deleted_item_name' => 'الاسم: كتابة الاسم',\n    'audit_table_user' => 'المستخدم',\n    'audit_table_event' => 'الحدث',\n    'audit_table_related' => 'العنصر أو التفاصيل ذات الصلة',\n    'audit_table_ip' => 'عنوان عُرف اﻹنترنت -IP-',\n    'audit_table_date' => 'تاريخ النشاط',\n    'audit_date_from' => 'نطاق التاريخ من',\n    'audit_date_to' => 'نطاق التاريخ إلى',\n\n    // Role Settings\n    'roles' => 'الأدوار',\n    'role_user_roles' => 'أدوار المستخدمين',\n    'roles_index_desc' => 'تُستخدم الأدوار لتجميع المستخدمين ومنح أذونات النظام لأعضائها. عندما يكون المستخدم عضوًا في أدوار متعددة، تتراكم الصلاحيات الممنوحة، ويرث المستخدم جميع القدرات.',\n    'roles_x_users_assigned' => ':count مستخدم معين|:count مستخدمين معينين',\n    'roles_x_permissions_provided' => ':count إذن |:count إذونات',\n    'roles_assigned_users' => 'المستخدمون المعينون',\n    'roles_permissions_provided' => 'الصلاحيات المقدمة',\n    'role_create' => 'إنشاء دور جديد',\n    'role_delete' => 'حذف الدور',\n    'role_delete_confirm' => 'سيتم حذف الدور المسمى \\':roleName\\'.',\n    'role_delete_users_assigned' => 'هذا الدور له: عدد المستخدمين المعينين له. إذا كنت ترغب في ترحيل المستخدمين من هذا الدور ، فحدد دورًا جديدًا أدناه.',\n    'role_delete_no_migration' => \"لا تقم بترجيل المستخدمين\",\n    'role_delete_sure' => 'تأكيد حذف الدور؟',\n    'role_edit' => 'تعديل الدور',\n    'role_details' => 'تفاصيل الدور',\n    'role_name' => 'اسم الدور',\n    'role_desc' => 'وصف مختصر للدور',\n    'role_mfa_enforced' => 'يتطلب مصادقة متعددة العوامل',\n    'role_external_auth_id' => 'ربط الحساب بمواقع التواصل',\n    'role_system' => 'أذونات النظام',\n    'role_manage_users' => 'إدارة المستخدمين',\n    'role_manage_roles' => 'إدارة الأدوار وأذوناتها',\n    'role_manage_entity_permissions' => 'إدارة جميع أذونات الكتب والفصول والصفحات',\n    'role_manage_own_entity_permissions' => 'إدارة الأذونات الخاصة بكتابك أو فصلك أو صفحاتك',\n    'role_manage_page_templates' => 'إدارة قوالب الصفحة',\n    'role_access_api' => 'الوصول إلى واجهة برمجة تطبيقات النظام API',\n    'role_manage_settings' => 'إدارة إعدادات التطبيق',\n    'role_export_content' => 'تصدير المحتوى',\n    'role_import_content' => 'استيراد المحتوى',\n    'role_editor_change' => 'تغيير محرر الصفحة',\n    'role_notifications' => 'تلقي الإشعارات وإدارتها',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'أذونات الأصول',\n    'roles_system_warning' => 'اعلم أن الوصول إلى أي من الأذونات الثلاثة المذكورة أعلاه يمكن أن يسمح للمستخدم بتغيير امتيازاته الخاصة أو امتيازات الآخرين في النظام. قم بتعيين الأدوار مع هذه الأذونات فقط للمستخدمين الموثوق بهم.',\n    'role_asset_desc' => 'تتحكم هذه الأذونات في الوصول الافتراضي إلى الأصول داخل النظام. ستتجاوز الأذونات الخاصة بالكتب والفصول والصفحات هذه الأذونات.',\n    'role_asset_admins' => 'يُمنح المسؤولين حق الوصول تلقائيًا إلى جميع المحتويات ولكن هذه الخيارات قد تعرض خيارات واجهة المستخدم أو تخفيها.',\n    'role_asset_image_view_note' => 'يتعلق هذا بالرؤية داخل مدير الصور. يعتمد الوصول الفعلي لملفات الصور المُحمّلة على خِيار تخزين الصور في النظام.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'الكل',\n    'role_own' => 'ما يخص',\n    'role_controlled_by_asset' => 'يتحكم فيها الأصول التي يتم رفعها إلى',\n    'role_save' => 'حفظ الدور',\n    'role_users' => 'مستخدمون داخل هذا الدور',\n    'role_users_none' => 'لم يتم تعيين أي مستخدمين لهذا الدور',\n\n    // Users\n    'users' => 'المستخدمون',\n    'users_index_desc' => 'إنشاء وإدارة حسابات المستخدمين الفردية داخل النظام. يتم استخدام حسابات المستخدم لتسجيل الدخول وإسناد المحتوى والنشاط. صلاحيات الوصول هي أساسا قائمة على الأدوار ولكن ملكية محتوى المستخدم، من بين عوامل أخرى، قد تؤثر أيضا على صلاحيات والوصول إليها.',\n    'user_profile' => 'ملف المستخدم',\n    'users_add_new' => 'إضافة مستخدم جديد',\n    'users_search' => 'بحث عن مستخدم',\n    'users_latest_activity' => 'أحدث نشاط',\n    'users_details' => 'بيانات المستخدم',\n    'users_details_desc' => 'قم بتعيين اسم عرض وعنوان بريد إلكتروني لهذا المستخدم. سيتم استخدام عنوان البريد الإلكتروني لتسجيل الدخول إلى التطبيق.',\n    'users_details_desc_no_email' => 'قم بتعيين اسم عرض لهذا المستخدم حتى يتمكن الآخرون من التعرف عليه.',\n    'users_role' => 'أدوار المستخدمين',\n    'users_role_desc' => 'حدد الأدوار التي سيتم تعيين هذا المستخدم لها. إذا تم تعيين مستخدم لأدوار متعددة ، فسيتم تكديس الأذونات من هذه الأدوار وسيتلقى كل قدرات الأدوار المعينة.',\n    'users_password' => 'كلمة مرور المستخدم',\n    'users_password_desc' => 'عيّن كلمة سر لتسجيل الدخول إلى التطبيق. يجب ألا تقل عن 8 أحرف.',\n    'users_send_invite_text' => 'يمكنك اختيار إرسال دعوة بالبريد الإلكتروني إلى هذا المستخدم مما يسمح له بتعيين كلمة السر الخاصة به أو يمكنك تعيين كلمة المرور الخاصة به بنفسك.',\n    'users_send_invite_option' => 'أرسل بريدًا إلكترونيًا لدعوة المستخدم',\n    'users_external_auth_id' => 'ربط الحساب بمواقع التواصل',\n    'users_external_auth_id_desc' => 'عند استخدام نظام مصادقة خارجي (مثل SAML2 أو OIDC أو LDAP)، يكون هذا هو المعرف الذي يربط مستخدم بوكستاك -BookStack- بحساب نظام المصادقة. يمكنك تجاهل هذا الحقل عند استخدام المصادقة الافتراضية عبر البريد الإلكتروني.',\n    'users_password_warning' => 'قم بملء الحقل أدناه فقط إذا كنت ترغب في تغيير كلمة السر لهذا المستخدم.',\n    'users_system_public' => 'هذا المستخدم يمثل أي ضيف يقوم بزيارة شيء يخصك. لا يمكن استخدامه لتسجيل الدخول ولكن يتم تعيينه تلقائياً.',\n    'users_delete' => 'حذف المستخدم',\n    'users_delete_named' => 'حذف المستخدم :userName',\n    'users_delete_warning' => 'سيتم حذف المستخدم \\':userName\\' بشكل تام من النظام.',\n    'users_delete_confirm' => 'تأكيد حذف المستخدم؟',\n    'users_migrate_ownership' => 'نقل الملكية',\n    'users_migrate_ownership_desc' => 'حدد مستخدم هنا إذا كنت تريد أن يصبح مستخدم آخر مالك جميع العناصر التي يمتلكها هذا المستخدم حاليا.',\n    'users_none_selected' => 'لم يتم تحديد مستخدم',\n    'users_edit' => 'تعديل المستخدم',\n    'users_edit_profile' => 'تعديل الملف',\n    'users_avatar' => 'صورة المستخدم',\n    'users_avatar_desc' => 'يجب أن تكون الصورة مربعة ومقاربة لحجم 256 بكسل',\n    'users_preferred_language' => 'اللغة المفضلة',\n    'users_preferred_language_desc' => 'سيؤدي هذا الخيار إلى تغيير اللغة المستخدمة لواجهة المستخدم الخاصة بالتطبيق. لن يؤثر هذا على أي محتوى قد أنشائه المستخدم.',\n    'users_social_accounts' => 'الحسابات الاجتماعية',\n    'users_social_accounts_desc' => 'عرض حالة الحسابات الاجتماعية المرتبطة لهذا المستخدم. ويمكن استخدام الحسابات الاجتماعية بالإضافة إلى نظام التوثيق الرئيس للوصول إلى النظام.',\n    'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not previously authorized access. Revoke access from your profile settings on the connected social account.',\n    'users_social_connect' => 'ربط الحساب',\n    'users_social_disconnect' => 'فصل الحساب',\n    'users_social_status_connected' => 'متصل',\n    'users_social_status_disconnected' => 'غير متصل',\n    'users_social_connected' => 'تم ربط حساب :socialAccount بملفك بنجاح.',\n    'users_social_disconnected' => 'تم فصل حساب :socialAccount من ملفك بنجاح.',\n    'users_api_tokens' => 'رموز الـ API',\n    'users_api_tokens_desc' => 'أنشئ وأدر رموز الوصول المستخدمة للمصادقة باستخدام واجهة برمجة تطبيقات بوكستاك رِست -BookStack REST API-. تتم إدارة صلاحيات واجهة برمجة التطبيقات بواسطة المستخدم الذي ينتمي إليه الرمز.',\n    'users_api_tokens_none' => 'لم يتم إنشاء رموز API لهذا المستخدم',\n    'users_api_tokens_create' => 'قم بإنشاء رمز مميز',\n    'users_api_tokens_expires' => 'انتهاء مدة الصلاحية',\n    'users_api_tokens_docs' => 'وثائق API',\n    'users_mfa' => 'المصادقة متعددة العوامل',\n    'users_mfa_desc' => 'إعداد المصادقة متعددة العوامل كطبقة إضافية من الأمان لحساب المستخدم الخاص بك.',\n    'users_mfa_x_methods' => ':count طريقة مُهيأة | :count طرق مُهيأة',\n    'users_mfa_configure' => 'إعداد الطرق',\n\n    // API Tokens\n    'user_api_token_create' => 'قم بإنشاء رمز API',\n    'user_api_token_name' => 'الاسم',\n    'user_api_token_name_desc' => 'اعطي الرمز الخاص بك اسمًا يمكن قراءته للتذكير مستقبلًا بالغرض المقصود منه.',\n    'user_api_token_expiry' => 'تاريخ انتهاء الصلاحية',\n    'user_api_token_expiry_desc' => 'حدد التاريخ الذي تنتهي فيه صلاحية هذا الرمز. بعد هذا التاريخ ، لن تعمل الطلبات المقدمة باستخدام هذا الرمز. سيؤدي ترك هذا الحقل فارغًا إلى تعيين انتهاء صلاحية لمدة 100 عام في المستقبل.',\n    'user_api_token_create_secret_message' => 'عقب إنشاء هذا الرمز مباشرة، سيتم إنشاء \"مُعرّف الرمز\" و \"رمز سري\" وعرضهما. وسيتم عرض الرمز السري لمرة واحدة فقط ، لذا تأكد من نسخ قيمة هذا الرمز إلى مكان آمن ومضمون قبل المتابعة.',\n    'user_api_token' => 'رمز الـ API',\n    'user_api_token_id' => 'مُعرّف الرمز',\n    'user_api_token_id_desc' => 'هذا مُعرّف تم إنشاؤه بواسطة النظام غير قابل للتحرير لهذا الرمز والذي يجب توفيره في طلبات API.',\n    'user_api_token_secret' => 'الرمز السري',\n    'user_api_token_secret_desc' => 'هذا الرمز السري تم إنشاؤه بواسطة النظام والذي يجب توفيره ضمن طلبات API. سيتم عرضه لمرة واحدة فقط ، لذا انسخ قيمة هذا الرمز إلى مكان آمن ومضمون.',\n    'user_api_token_created' => 'تم إنشاء رمز :الوقت الزمني',\n    'user_api_token_updated' => 'تم تحديث الرمز :الوقت الزمني',\n    'user_api_token_delete' => 'حذف الرمز',\n    'user_api_token_delete_warning' => 'سيؤدي هذا إلى حذف رمز API المُشار إليه بالكامل باسم \\'اسم الرمز\\' من النظام.',\n    'user_api_token_delete_confirm' => 'هل أنت متأكد من أنك تريد حذف رمز API؟',\n\n    // Webhooks\n    'webhooks' => 'خطافات الويب -Webhooks-',\n    'webhooks_index_desc' => 'خطافات الويب هي طريقة لإرسال البيانات إلى الروابط الخارجية عندما تحدث بعض الإجراءات والأحداث داخل النظام الذي يسمح بالتكامل القائم على الأحداث مع المنصات الخارجية مثل نظم المراسلة أو الإشعار.',\n    'webhooks_x_trigger_events' => ':count حدث تشغيل |:count أحداث تشغيل',\n    'webhooks_create' => 'إنشاء خطاف ويب جديد',\n    'webhooks_none_created' => 'لم يتم إنشاء أي خطافات ويب حتى الآن.',\n    'webhooks_edit' => 'تحرير خطاف ويب',\n    'webhooks_save' => 'حفظ خطاف ويب',\n    'webhooks_details' => 'تفاصيل خطاف الويب',\n    'webhooks_details_desc' => 'قم بتوفير اسم سهل الاستخدام ونقطة نهاية POST كموقع لإرسال بيانات خطافات الويب إليه.',\n    'webhooks_events' => 'أحداث خطفات الويب',\n    'webhooks_events_desc' => 'حدد جميع الأحداث التي يجب أن تشغل هذا الرابط ليتم استدعاؤها.',\n    'webhooks_events_warning' => 'ضع في اعتبارك أن هذه الأحداث سيتم تشغيلها لجميع الأحداث المحددة، حتى إذا تم تطبيق صلاحيات مخصصة. تحقق أن استخدام خطاف الويب هذا لن يكشف عن محتوى سري.',\n    'webhooks_events_all' => 'جميع أحداث النظام',\n    'webhooks_name' => 'اسم خطاف الويب',\n    'webhooks_timeout' => 'مهلة طلب خطاف الويب (بالثواني)',\n    'webhooks_endpoint' => 'نقطة نهاية خطاف الويب',\n    'webhooks_active' => 'خطاف الويب فعال',\n    'webhook_events_table_header' => 'الأحداث',\n    'webhooks_delete' => 'حذف خطاف الويب',\n    'webhooks_delete_warning' => 'سيؤدي هذا إلى حذف خطاف الويب بالكامل، الذي يحمل اسم \\':webhookName\\'، من النظام.',\n    'webhooks_delete_confirm' => 'هل أنت متيقِّن أنك تريد حذف هذا الخطاف؟',\n    'webhooks_format_example' => 'مثال على تنسيق خطاف الويب',\n    'webhooks_format_example_desc' => 'يتم إرسال بيانات خطاف الويب كطلب بوست -POST- إلى نقطة النهاية المكونة كجيسون -JSON- باتباع التنسيق أدناه. خصائص \"ذات صلة\" و \"روابط\" اختيارية و ستعتمد على نوع الحدث الذي تم تشغيله.',\n    'webhooks_status' => 'حالة خطاف الويب',\n    'webhooks_last_called' => 'آخر اتصال:',\n    'webhooks_last_errored' => 'أخر خطأ:',\n    'webhooks_last_error_message' => 'رسالة الخطأ الأخيرة:',\n\n    // Licensing\n    'licenses' => 'الرخص',\n    'licenses_desc' => 'هذه الصفحة تفصل معلومات الرخص لبوكستاك -BookStack- بالإضافة إلى المشاريع والمكتبات المستخدمة في بوكستاك. ولا يمكن استخدام العديد من المشاريع المدرجة إلا في سياق إنمائي.',\n    'licenses_bookstack' => 'رخص بوكستاك',\n    'licenses_php' => 'رخص مكتبات بي إتش بي -PHP-',\n    'licenses_js' => 'رخص مكتبة جافا سكريبت -JavaScript-',\n    'licenses_other' => 'رخص أخرى',\n    'license_details' => 'تفاصيل الرخصة',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/ar/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => 'يجب الموافقة على :attribute.',\n    'active_url'           => ':attribute ليس رابط صالح.',\n    'after'                => 'يجب أن يكون التاريخ :attribute بعد :date.',\n    'alpha'                => 'يجب أن يقتصر :attribute على الحروف فقط.',\n    'alpha_dash'           => 'يجب أن يقتصر :attribute على حروف أو أرقام أو شرطات فقط.',\n    'alpha_num'            => 'يجب أن يقتصر :attribute على الحروف والأرقام فقط.',\n    'array'                => 'يجب أن تكون السمة مصفوفة.',\n    'backup_codes'         => 'الرمز المقدم غير صالح أو تم استخدامه بالفعل.',\n    'before'               => 'يجب أن يكون التاريخ :attribute قبل :date.',\n    'between'              => [\n        'numeric' => 'يجب أن يكون :attribute بين :min و :max.',\n        'file'    => 'يجب أن يكون :attribute بين :min و :max كيلو بايت.',\n        'string'  => 'يجب أن يكون :attribute بين :min و :max حرف / حروف.',\n        'array'   => 'يجب أن يكون :attribute بين :min و :max عنصر / عناصر.',\n    ],\n    'boolean'              => 'يجب أن يحتمل حقل السمة الصحة أو الخطأ.',\n    'confirmed'            => ':attribute غير مطابق.',\n    'date'                 => ':attribute ليس تاريخ صالح.',\n    'date_format'          => ':attribute لا يطابق الصيغة :format.',\n    'different'            => 'يجب أن يكون :attribute مختلف عن :other.',\n    'digits'               => 'يجب أن يكون :attribute بعدد :digits خانات.',\n    'digits_between'       => 'يجب أن يكون :attribute بعدد خانات بين :min و :max.',\n    'email'                => 'يجب أن يكون :attribute عنوان بريد إلكتروني صالح.',\n    'ends_with' => 'يجب أن تنتهي السمة بأحد القيم التالية',\n    'file'                 => 'يجب توفير :attribute كملف صالح.',\n    'filled'               => 'حقل :attribute مطلوب.',\n    'gt'                   => [\n        'numeric' => 'يجب أن تكون السمة أكبر من: القيمة.',\n        'file'    => 'يجب أن تكون السمة أكبر من: القيمة كيلوبايت.',\n        'string'  => 'يجب أن تكون السمة أكبر من: أحرف القيمة.',\n        'array'   => 'يجب أن تحتوي السمة على أكثر من: عناصر القيمة.',\n    ],\n    'gte'                  => [\n        'numeric' => 'يجب أن تكون السمة أكبر من أو تساوي: القيمة.',\n        'file'    => 'يجب أن تكون السمة أكبر من أو تساوي: القيمة كيلوبايت.',\n        'string'  => 'يجب أن تكون السمة أكبر من أو تساوي: أحرف القيمة.',\n        'array'   => 'يجب أن تحتوي السمة على: عناصر القيمة أو أكثر.',\n    ],\n    'exists'               => ':attribute المحدد غير صالح.',\n    'image'                => 'يجب أن يكون :attribute صورة.',\n    'image_extension'      => 'يجب أن تحتوي السمة على امتداد صورة صالح ومدعوم.',\n    'in'                   => ':attribute المحدد غير صالح.',\n    'integer'              => 'يجب أن يكون :attribute عدد صحيح.',\n    'ip'                   => 'يجب أن يكون :attribute عنوان IP صالح.',\n    'ipv4'                 => 'يجب أن تكون السمة: عنوان IPv4 صالحًا.',\n    'ipv6'                 => 'يجب أن تكون السمة: عنوان IPv6 صالحًا.',\n    'json'                 => 'يجب أن تكون السمة: سلسلة من نوع جسون JSON صالح.',\n    'lt'                   => [\n        'numeric' => 'يجب أن تكون السمة أقل من: القيمة.',\n        'file'    => 'يجب أن تكون السمة أقل من: القيمة كيلوبايت.',\n        'string'  => 'يجب أن تكون السمة أقل من: أحرف القيمة.',\n        'array'   => 'يجب أن تحتوي السمة على أقل من: عناصر القيمة.',\n    ],\n    'lte'                  => [\n        'numeric' => 'يجب أن تكون السمة أقل من أو تساوي: القيمة.',\n        'file'    => 'يجب أن تكون السمة أقل من أو تساوي: القيمة كيلوبايت.',\n        'string'  => 'يجب أن تكون السمة أقل من أو تساوي: أحرف القيمة.',\n        'array'   => 'يجب ألا تحتوي السمة على أكثر من: عناصر القيمة.',\n    ],\n    'max'                  => [\n        'numeric' => 'يجب ألا يكون :attribute أكبر من :max.',\n        'file'    => 'يجب ألا يكون :attribute أكبر من :max كيلو بايت.',\n        'string'  => 'يجب ألا يكون :attribute أكثر من :max حرف / حروف.',\n        'array'   => 'يجب ألا يحتوي :attribute على أكثر من :max عنصر / عناصر.',\n    ],\n    'mimes'                => 'يجب أن يكون :attribute ملف من نوع: :values.',\n    'min'                  => [\n        'numeric' => 'يجب أن يكون :attribute على الأقل :min.',\n        'file'    => 'يجب أن يكون :attribute على الأقل :min كيلو بايت.',\n        'string'  => 'يجب أن يكون :attribute على الأقل :min حرف / حروف.',\n        'array'   => 'يجب أن يحتوي :attribute على :min عنصر / عناصر كحد أدنى.',\n    ],\n    'not_in'               => ':attribute المحدد غير صالح.',\n    'not_regex'            => 'صيغة السمة: غير صالحة.',\n    'numeric'              => 'يجب أن يكون :attribute رقم.',\n    'regex'                => 'صيغة :attribute غير صالحة.',\n    'required'             => 'حقل :attribute مطلوب.',\n    'required_if'          => 'حقل :attribute مطلوب عندما يكون :other :value.',\n    'required_with'        => 'حقل :attribute مطلوب عندما تكون :values موجودة.',\n    'required_with_all'    => 'حقل :attribute مطلوب عندما تكون :values موجودة.',\n    'required_without'     => 'حقل :attribute مطلوب عندما تكون :values غير موجودة.',\n    'required_without_all' => 'حقل :attribute مطلوب عندما لا يكون أي من :values موجودة.',\n    'same'                 => 'يجب تطابق :attribute مع :other.',\n    'safe_url'             => 'قد لايكون الرابط المتوفر آمنا.',\n    'size'                 => [\n        'numeric' => 'يجب أن يكون :attribute بحجم :size.',\n        'file'    => 'يجب أن يكون :attribute بحجم :size كيلو بايت.',\n        'string'  => 'يجب أن يكون :attribute بعدد :size حرف / حروف.',\n        'array'   => 'يجب أن يحتوي :attribute على :size عنصر / عناصر.',\n    ],\n    'string'               => 'يجب أن تكون السمة: سلسلة.',\n    'timezone'             => 'يجب أن تكون :attribute منطقة صالحة.',\n    'totp'                 => 'الرمز المقدم غير صالح أو انتهت صلاحيته.',\n    'unique'               => 'تم حجز :attribute من قبل.',\n    'url'                  => 'صيغة :attribute غير صالحة.',\n    'uploaded'             => 'تعذر تحميل الملف. قد لا يقبل الخادم ملفات بهذا الحجم.',\n\n    'zip_file' => ':attribute بحاجة إلى الرجوع إلى مِلَفّ داخل المِلَفّ المضغوط.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => ':attribute بحاجة إلى الإشارة إلى مِلَفّ من نوع :validTypes، وجدت :foundType.',\n    'zip_model_expected' => 'عنصر البيانات المتوقع ولكن \":type\" تم العثور عليه.',\n    'zip_unique' => 'يجب أن يكون :attribute فريداً لنوع الكائن داخل المِلَفّ المضغوط.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'يجب تأكيد كلمة السر',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/bg/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'създадена страница',\n    'page_create_notification'    => 'Страницата е създадена успешно',\n    'page_update'                 => 'обновена страница',\n    'page_update_notification'    => 'Страницата е обновена успешно',\n    'page_delete'                 => 'изтрита страница',\n    'page_delete_notification'    => 'Страницата е изтрита успешно',\n    'page_restore'                => 'възстановена страница',\n    'page_restore_notification'   => 'Страницата е възстановена успешно',\n    'page_move'                   => 'преместена страница',\n    'page_move_notification'      => 'Страницата беше успешно преместена',\n\n    // Chapters\n    'chapter_create'              => 'създадена глава',\n    'chapter_create_notification' => 'Успешно създадена глава',\n    'chapter_update'              => 'обновена глава',\n    'chapter_update_notification' => 'Успешно обновена глава',\n    'chapter_delete'              => 'изтрита глава',\n    'chapter_delete_notification' => 'Успешно изтрита глава',\n    'chapter_move'                => 'преместена глава',\n    'chapter_move_notification' => 'Главата е успешно преместена',\n\n    // Books\n    'book_create'                 => 'създадена книга',\n    'book_create_notification'    => 'Книгата е създадена успешно',\n    'book_create_from_chapter'              => 'превърната глава в книга',\n    'book_create_from_chapter_notification' => 'Главата е успешно преобразувана в книга',\n    'book_update'                 => 'обновена книга',\n    'book_update_notification'    => 'Книгата е обновена успешно',\n    'book_delete'                 => 'изтрита книга',\n    'book_delete_notification'    => 'Книгата е изтрита успешно',\n    'book_sort'                   => 'сортирана книга',\n    'book_sort_notification'      => 'Книгата е преподредена успешно',\n\n    // Bookshelves\n    'bookshelf_create'            => 'created shelf',\n    'bookshelf_create_notification'    => 'Shelf successfully created',\n    'bookshelf_create_from_book'    => 'converted book to shelf',\n    'bookshelf_create_from_book_notification'    => 'Book successfully converted to a shelf',\n    'bookshelf_update'                 => 'updated shelf',\n    'bookshelf_update_notification'    => 'Shelf successfully updated',\n    'bookshelf_delete'                 => 'deleted shelf',\n    'bookshelf_delete_notification'    => 'Shelf successfully deleted',\n\n    // Revisions\n    'revision_restore' => 'restored revision',\n    'revision_delete' => 'deleted revision',\n    'revision_delete_notification' => 'Revision successfully deleted',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" е добавен към любими успешно',\n    'favourite_remove_notification' => '\":name\" е премахнат от любими успешно',\n\n    // Watching\n    'watch_update_level_notification' => 'Watch preferences successfully updated',\n\n    // Auth\n    'auth_login' => 'logged in',\n    'auth_register' => 'registered as new user',\n    'auth_password_reset_request' => 'requested user password reset',\n    'auth_password_reset_update' => 'reset user password',\n    'mfa_setup_method' => 'configured MFA method',\n    'mfa_setup_method_notification' => 'Многофакторният метод е конфигуриран успешно',\n    'mfa_remove_method' => 'removed MFA method',\n    'mfa_remove_method_notification' => 'Многофакторният метод е премахнат успешно',\n\n    // Settings\n    'settings_update' => 'updated settings',\n    'settings_update_notification' => 'Settings successfully updated',\n    'maintenance_action_run' => 'ran maintenance action',\n\n    // Webhooks\n    'webhook_create' => 'създадена уебкука',\n    'webhook_create_notification' => 'Уебкуката е създадена успешно',\n    'webhook_update' => 'обновена уебкука',\n    'webhook_update_notification' => 'Уебкуката е обновена успешно',\n    'webhook_delete' => 'изтрита уебкука',\n    'webhook_delete_notification' => 'Уебкуката е изтрита успешно',\n\n    // Imports\n    'import_create' => 'created import',\n    'import_create_notification' => 'Import successfully uploaded',\n    'import_run' => 'updated import',\n    'import_run_notification' => 'Content successfully imported',\n    'import_delete' => 'deleted import',\n    'import_delete_notification' => 'Import successfully deleted',\n\n    // Users\n    'user_create' => 'created user',\n    'user_create_notification' => 'User successfully created',\n    'user_update' => 'updated user',\n    'user_update_notification' => 'Потребителят е обновен успешно',\n    'user_delete' => 'deleted user',\n    'user_delete_notification' => 'Потребителят е премахнат успешно',\n\n    // API Tokens\n    'api_token_create' => 'created API token',\n    'api_token_create_notification' => 'API token successfully created',\n    'api_token_update' => 'updated API token',\n    'api_token_update_notification' => 'API token successfully updated',\n    'api_token_delete' => 'deleted API token',\n    'api_token_delete_notification' => 'API token successfully deleted',\n\n    // Roles\n    'role_create' => 'created role',\n    'role_create_notification' => 'Успешна създадена роля',\n    'role_update' => 'updated role',\n    'role_update_notification' => 'Успешно обновена роля',\n    'role_delete' => 'deleted role',\n    'role_delete_notification' => 'Успешно изтрита роля',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'emptied recycle bin',\n    'recycle_bin_restore' => 'restored from recycle bin',\n    'recycle_bin_destroy' => 'removed from recycle bin',\n\n    // Comments\n    'commented_on'                => 'коментирано на',\n    'comment_create'              => 'added comment',\n    'comment_update'              => 'updated comment',\n    'comment_delete'              => 'deleted comment',\n\n    // Sort Rules\n    'sort_rule_create' => 'created sort rule',\n    'sort_rule_create_notification' => 'Sort rule successfully created',\n    'sort_rule_update' => 'updated sort rule',\n    'sort_rule_update_notification' => 'Sort rule successfully updated',\n    'sort_rule_delete' => 'deleted sort rule',\n    'sort_rule_delete_notification' => 'Sort rule successfully deleted',\n\n    // Other\n    'permissions_update'          => 'обновени права',\n];\n"
  },
  {
    "path": "lang/bg/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Въведените данни не съвпадат с информацията в системата.',\n    'throttle' => 'Твърде много опити за влизане. Опитайте пак след :seconds секунди.',\n\n    // Login & Register\n    'sign_up' => 'Регистриране',\n    'log_in' => 'Влизане',\n    'log_in_with' => 'Влизане с :socialDriver',\n    'sign_up_with' => 'Регистриране с :socialDriver',\n    'logout' => 'Изход',\n\n    'name' => 'Име',\n    'username' => 'Потребителско име',\n    'email' => 'Имейл',\n    'password' => 'Парола',\n    'password_confirm' => 'Потвърди паролата',\n    'password_hint' => 'Трябва да бъде поне 8 символа',\n    'forgot_password' => 'Забравена парола?',\n    'remember_me' => 'Запомни ме',\n    'ldap_email_hint' => 'Моля въведете емейл, който да използвате за дадения профил.',\n    'create_account' => 'Създаване на акаунт',\n    'already_have_account' => 'Вече имате профил?',\n    'dont_have_account' => 'Нямате ли акаунт?',\n    'social_login' => 'Влизане по друг начин',\n    'social_registration' => 'Регистрация по друг начин',\n    'social_registration_text' => 'Регистриране и влизане посредством друга услуга.',\n\n    'register_thanks' => 'Благодарности за регистрирането!',\n    'register_confirm' => 'Моля, провери своя имейл адрес и натисни бутона за потвърждение, за да достъпиш :appName.',\n    'registrations_disabled' => 'Регистрациите към момента са забранени',\n    'registration_email_domain_invalid' => 'Този емейл домейн към момента няма достъп до приложението',\n    'register_success' => 'Благодарим Ви за регистрацията! В момента сте регистриран и сте вписани в приложението.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Опит за вход в системата',\n    'auto_init_starting_desc' => 'Свързахме системата ви за удостоверяване към началото на процеса при влизане. Ако няма напредък след 5 секунди, то може да опитате да щракнете върху долната връзка.',\n    'auto_init_start_link' => 'Продължаване с удостоверяването',\n\n    // Password Reset\n    'reset_password' => 'Нулиране на паролата',\n    'reset_password_send_instructions' => 'Въведете емейла си и ще ви бъде изпратен емейл с линк за нулиране на паролата.',\n    'reset_password_send_button' => 'Изпращане на линк за възстановяване',\n    'reset_password_sent' => 'Линк за нулиране на паролата ще Ви бъде изпратен на :email, ако емейлът Ви бъде открит в системата.',\n    'reset_password_success' => 'Паролата Ви е променена успешно.',\n    'email_reset_subject' => 'Възстанови паролата си за :appName',\n    'email_reset_text' => 'Вие получихте този имейл, защото поискахте Вашата парола да бъде възстановена.',\n    'email_reset_not_requested' => 'Ако Вие не сте поискали зануляването на паролата, няма нужда от други действия.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Потвърди емейла си за :appName',\n    'email_confirm_greeting' => 'Благодарим Ви, че се присъединихте към :appName!',\n    'email_confirm_text' => 'Потвърдете адреса на имейла си, щраквайки върху връзката по-долу:',\n    'email_confirm_action' => 'Потвърдете имейл',\n    'email_confirm_send_error' => 'Нужно ви е потвърждение чрез емейл, но системата не успя да го изпрати. Моля свържете се с администратора, за да проверите дали вашият емейл адрес е конфигуриран правилно.',\n    'email_confirm_success' => 'Имейлът ти е потвърден! Вече би трябвало да можеш да се впишеш с този имейл адрес.',\n    'email_confirm_resent' => 'Е-писмо за потвърждение е изпратено пак, проверете кутията си.',\n    'email_confirm_thanks' => 'Благодарности за потвърждаването!',\n    'email_confirm_thanks_desc' => 'Почакайте малко, обработвайки потвърждението ви. Ако не сте пренасочени след 3 секунди, то натиснете долу връзката \"Продължаване\", за да продължите.',\n\n    'email_not_confirmed' => 'Имейл адресът не е потвърден',\n    'email_not_confirmed_text' => 'Вашият имейл адрес все още не е потвърден.',\n    'email_not_confirmed_click_link' => 'Моля да последвате линка, който ви беше изпратен непосредствено след регистрацията.',\n    'email_not_confirmed_resend' => 'Ако не откривате писмото, може да го изпратите отново като попълните формуляра по-долу.',\n    'email_not_confirmed_resend_button' => 'Изпрати отново емейла за потвърждение',\n\n    // User Invite\n    'user_invite_email_subject' => 'Вие бяхте поканен да се присъедините към :appName!',\n    'user_invite_email_greeting' => 'Беше създаден акаунт за Вас във :appName.',\n    'user_invite_email_text' => 'Натисните бутона по-долу за да определите парола и да получите достъп:',\n    'user_invite_email_action' => 'Задаване на парола на акаунта',\n    'user_invite_page_welcome' => 'Добре дошли в :appName!',\n    'user_invite_page_text' => 'За да довършвам окончателно акаунта ви и да получите достъп трябва да зададете парола, която ще се използва за влизане в :appName при бъдещи посещения.',\n    'user_invite_page_confirm_button' => 'Потвърди паролата',\n    'user_invite_success_login' => 'Паролата е настроена, вече можеш да се впишеш с новата парола, за да достъпиш :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Настрой многофакторно удостоверяване',\n    'mfa_setup_desc' => 'Настрой многофакторно удостверяване като втори слой сигурност на твоя профил.',\n    'mfa_setup_configured' => 'Вече е конфигурирано',\n    'mfa_setup_reconfigure' => 'Преконфигурирай',\n    'mfa_setup_remove_confirmation' => 'Сигурен ли си, че желаеш да премахнеш този метод за многофакторно удостоверяване?',\n    'mfa_setup_action' => 'Настройка',\n    'mfa_backup_codes_usage_limit_warning' => 'Имаш по-малко от 5 останали резервни кода. Генерирай и съхрани нов набор, преди тези да са свършили, за да избегнеш да останеш без достъп до профила си.',\n    'mfa_option_totp_title' => 'Мобилно приложение',\n    'mfa_option_totp_desc' => 'За да използваш многофакторно удостоверяване, ще ти трябва мобилно приложение, което поддържа временни еднократни пароли (TOTP), като например Google Authenticator, Authy или Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Резервни кодове',\n    'mfa_option_backup_codes_desc' => 'Генерира набор от еднократни резервни кодове, които ще въвеждате при влизане, за да потвърдите самоличността си. Уверете се, че ги съхранявате на безопасно и сигурно място.',\n    'mfa_gen_confirm_and_enable' => 'Потвърди и включи',\n    'mfa_gen_backup_codes_title' => 'Настройка на резервни кодове',\n    'mfa_gen_backup_codes_desc' => 'Запази този лист с кодове на сигурно място. Когато достъпваш системата, ще можеш да използваш един от тези кодове като вторичен механизъм за удостоверяване.',\n    'mfa_gen_backup_codes_download' => 'Изтегли кодовете',\n    'mfa_gen_backup_codes_usage_warning' => 'Всеки код може да бъде използван само веднъж',\n    'mfa_gen_totp_title' => 'Настройка на мобилно приложение',\n    'mfa_gen_totp_desc' => 'За да използваш многофакторно удостоверяване, ще ти трябва мобилно приложение, което поддържа временни еднократни пароли (TOTP), като например Google Authenticator, Authy или Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'За да започнеш, сканирай QR кода отдолу с предпочитано от теб приложение.',\n    'mfa_gen_totp_verify_setup' => 'Потвърди настройката',\n    'mfa_gen_totp_verify_setup_desc' => 'Потвърди, че всичко работи, като в кутията отдолу въведеш код, генериран от твоето приложение за удостоверяване:',\n    'mfa_gen_totp_provide_code_here' => 'Въведи тук кода, генериран от мобилното ти приложение',\n    'mfa_verify_access' => 'Потвърди достъпа',\n    'mfa_verify_access_desc' => 'Твоят потребителски профил изисква да потвърдиш идентичността си чрез допълнително ниво проверка преди да получиш достъп. Потвърди чрез един от конфигурираните методи, за да продължиш.',\n    'mfa_verify_no_methods' => 'Няма конфигурирани методи',\n    'mfa_verify_no_methods_desc' => 'Няма намерени методи за многофакторно удостоверяване за твоя профил. Ще трябва да настроиш поне един метод, преди да получиш достъп.',\n    'mfa_verify_use_totp' => 'Потвърди чрез мобилно приложение',\n    'mfa_verify_use_backup_codes' => 'Потвърди чрез резервен код',\n    'mfa_verify_backup_code' => 'Резервен код',\n    'mfa_verify_backup_code_desc' => 'Въведи един от останалите ти резервни кодове отдолу:',\n    'mfa_verify_backup_code_enter_here' => 'Въведи резервен код тук',\n    'mfa_verify_totp_desc' => 'Въведи кода, генериран от мобилното ти приложение, отдолу:',\n    'mfa_setup_login_notification' => 'Многофакторният метод е конфигуриран, моля да се впишете отново чрез конфигурирания метод.',\n];\n"
  },
  {
    "path": "lang/bg/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Отказ',\n    'close' => 'Затвори',\n    'confirm' => 'Потвърждаване',\n    'back' => 'Назад',\n    'save' => 'Запис',\n    'continue' => 'Продължаване',\n    'select' => 'Изберете',\n    'toggle_all' => 'Избери всички',\n    'more' => 'Повече',\n\n    // Form Labels\n    'name' => 'Име',\n    'description' => 'Описание',\n    'role' => 'Роля',\n    'cover_image' => 'Образ на корицата',\n    'cover_image_description' => 'Изображението трябва да е около 440x250 px. Тъй като ще се мащабира и изрязва автоматично спрямо нуждите на интерфейса, крайните размери при показване може да се различават.',\n\n    // Actions\n    'actions' => 'Действия',\n    'view' => 'Преглед',\n    'view_all' => 'Преглед на всички',\n    'new' => 'Ново',\n    'create' => 'Създаване',\n    'update' => 'Обновяване',\n    'edit' => 'Редактиране',\n    'archive' => 'Архивирай',\n    'unarchive' => 'Разархивирай',\n    'sort' => 'Сортиране',\n    'move' => 'Преместване',\n    'copy' => 'Копиране',\n    'reply' => 'Отговор',\n    'delete' => 'Изтриване',\n    'delete_confirm' => 'Потвърждаване на изтриването',\n    'search' => 'Търсене',\n    'search_clear' => 'Изчистване на търсенето',\n    'reset' => 'Нулиране',\n    'remove' => 'Премахване',\n    'add' => 'Добавяне',\n    'configure' => 'Конфигуриране',\n    'manage' => 'Управлявай',\n    'fullscreen' => 'Цял екран',\n    'favourite' => 'Любимо',\n    'unfavourite' => 'Не е любимо',\n    'next' => 'Напред',\n    'previous' => 'Предишен',\n    'filter_active' => 'Активен филтър:',\n    'filter_clear' => 'Изчистване на филтрите',\n    'download' => 'Изтегляне',\n    'open_in_tab' => 'Отваряне в раздел',\n    'open' => 'Отвори',\n\n    // Sort Options\n    'sort_options' => 'Опции за сортиране',\n    'sort_direction_toggle' => 'Активирай сортиране',\n    'sort_ascending' => 'Сортирай възходящо',\n    'sort_descending' => 'Низходящо сортиране',\n    'sort_name' => 'Име',\n    'sort_default' => 'По подразбиране',\n    'sort_created_at' => 'Дата на създаване',\n    'sort_updated_at' => 'Дата на обновяване',\n\n    // Misc\n    'deleted_user' => 'Изтриване на потребител',\n    'no_activity' => 'Няма активност за показване',\n    'no_items' => 'Няма налични артикули',\n    'back_to_top' => 'Върнете се в началото',\n    'skip_to_main_content' => 'Прескочи към основното съдържание',\n    'toggle_details' => 'Активирай детайли',\n    'toggle_thumbnails' => 'Активирай миниатюри',\n    'details' => 'Подробности',\n    'grid_view' => 'Табличен изглед',\n    'list_view' => 'Изглед списък',\n    'default' => 'Основен',\n    'breadcrumb' => 'Трасиране',\n    'status' => 'Състояние',\n    'status_active' => 'Активен',\n    'status_inactive' => 'Неактивен',\n    'never' => 'Никога',\n    'none' => 'Нищо',\n\n    // Header\n    'homepage' => 'Начална страница',\n    'header_menu_expand' => 'Разшири заглавното меню',\n    'profile_menu' => 'Меню на профила',\n    'view_profile' => 'Преглед на профила',\n    'edit_profile' => 'Редактиране на профила',\n    'dark_mode' => 'Тъмен режим',\n    'light_mode' => 'Светъл режим',\n    'global_search' => 'Глобално търсене',\n\n    // Layout tabs\n    'tab_info' => 'Инфо.',\n    'tab_info_label' => 'Раздел: показва вторична информация',\n    'tab_content' => 'Съдържание',\n    'tab_content_label' => 'Раздел: Показва първично съдържание',\n\n    // Email Content\n    'email_action_help' => 'Ако имате проблеми с бутона \":actionText\" по-горе, копирайте и поставете URL адреса по-долу в уеб браузъра си:',\n    'email_rights' => 'Всички права запазени',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Политика за поверителност',\n    'terms_of_service' => 'Условия на услугата',\n\n    // OpenSearch\n    'opensearch_description' => 'Търси :appName',\n];\n"
  },
  {
    "path": "lang/bg/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Избор на изображение',\n    'image_list' => 'Image List',\n    'image_details' => 'Image Details',\n    'image_upload' => 'Upload Image',\n    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',\n    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the \"Upload Image\" button above.',\n    'image_all' => 'Всички',\n    'image_all_title' => 'Преглед на всички изображения',\n    'image_book_title' => 'Виж изображенията прикачени към тази книга',\n    'image_page_title' => 'Виж изображенията прикачени към страницата',\n    'image_search_hint' => 'Търси по име на картина',\n    'image_uploaded' => 'Качено :uploadedDate',\n    'image_uploaded_by' => 'Uploaded by :userName',\n    'image_uploaded_to' => 'Uploaded to :pageLink',\n    'image_updated' => 'Updated :updateDate',\n    'image_load_more' => 'Зареди повече',\n    'image_image_name' => 'Име на изображението',\n    'image_delete_used' => 'Това изображение е използвано в страницата по-долу.',\n    'image_delete_confirm_text' => 'Сигурни ли сте, че искате да изтриете това изображение?',\n    'image_select_image' => 'Изберете изображение',\n    'image_dropzone' => 'Поставете тук изображение или кликнете тук за да качите',\n    'image_dropzone_drop' => 'Drop images here to upload',\n    'images_deleted' => 'Изображението е изтрито',\n    'image_preview' => 'Преглед на изображенията',\n    'image_upload_success' => 'Изображението бе качено успешно',\n    'image_update_success' => 'Данните за изобтажението са обновенни успешно',\n    'image_delete_success' => 'Изображението е успешно изтрито',\n    'image_replace' => 'Replace Image',\n    'image_replace_success' => 'Image file successfully updated',\n    'image_rebuild_thumbs' => 'Regenerate Size Variations',\n    'image_rebuild_thumbs_success' => 'Image size variations successfully rebuilt!',\n\n    // Code Editor\n    'code_editor' => 'Редактиране на кода',\n    'code_language' => 'Език на кода',\n    'code_content' => 'Съдържание на кода',\n    'code_session_history' => 'История на сесиите',\n    'code_save' => 'Запази кода',\n];\n"
  },
  {
    "path": "lang/bg/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Общи',\n    'advanced' => 'Разширени',\n    'none' => 'Нищо',\n    'cancel' => 'Отказ',\n    'save' => 'Запис',\n    'close' => 'Затваряне',\n    'apply' => 'Приложи',\n    'undo' => 'Отмяна',\n    'redo' => 'Повтаряне',\n    'left' => 'Вляво',\n    'center' => 'По средата',\n    'right' => 'Вдясно',\n    'top' => 'Отгоре',\n    'middle' => 'Среда',\n    'bottom' => 'Отдолу',\n    'width' => 'Ширина',\n    'height' => 'Височина',\n    'More' => 'Още',\n    'select' => 'Изберете...',\n\n    // Toolbar\n    'formats' => 'Формати',\n    'header_large' => 'Голяма заглавка',\n    'header_medium' => 'Средна заглавка',\n    'header_small' => 'Малка заглавка',\n    'header_tiny' => 'Миниатюрна заглавка',\n    'paragraph' => 'Параграф',\n    'blockquote' => 'Цитат',\n    'inline_code' => 'Вложен код',\n    'callouts' => 'Призиви',\n    'callout_information' => 'Информация',\n    'callout_success' => 'Успех',\n    'callout_warning' => 'Предупреждение',\n    'callout_danger' => 'Опасност',\n    'bold' => 'Удебелено',\n    'italic' => 'Наклонен',\n    'underline' => 'Подчертан',\n    'strikethrough' => 'Зачертан',\n    'superscript' => 'Горен индекс',\n    'subscript' => 'Долен индекс',\n    'text_color' => 'Цвят на текста',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Цвят по избор',\n    'remove_color' => 'Премахване на цвят',\n    'background_color' => 'Фонов цвят',\n    'align_left' => 'Подравняване отляво',\n    'align_center' => 'Подравняване в средата',\n    'align_right' => 'Подравняване отдясно',\n    'align_justify' => 'Justify',\n    'list_bullet' => 'Списък',\n    'list_numbered' => 'Номериран списък',\n    'list_task' => 'Task list',\n    'indent_increase' => 'Увеличаване на отстъпа',\n    'indent_decrease' => 'Намаляване на отстъпа',\n    'table' => 'Таблица',\n    'insert_image' => 'Вмъкване на образ',\n    'insert_image_title' => 'Вмъкване/редактиране на образ',\n    'insert_link' => 'Вмъкване/редактиране на връзка',\n    'insert_link_title' => 'Вмъкни/редактирай връзка',\n    'insert_horizontal_line' => 'Вмъкване на хоризонтална линия',\n    'insert_code_block' => 'Вмъкване на блок код',\n    'edit_code_block' => 'Edit code block',\n    'insert_drawing' => 'Вмъкване/редактиране на рисунка',\n    'drawing_manager' => 'Управление на рисунките',\n    'insert_media' => 'Вмъкване/редактиране на мултимедията',\n    'insert_media_title' => 'Вмъкване/редактиране на мултимедията',\n    'clear_formatting' => 'Изчистване на форматирането',\n    'source_code' => 'Изходен код',\n    'source_code_title' => 'Изходен код',\n    'fullscreen' => 'Цял екран',\n    'image_options' => 'Възможности на образа',\n\n    // Tables\n    'table_properties' => 'Свойства на таблицата',\n    'table_properties_title' => 'Свойства на таблица',\n    'delete_table' => 'Изтриване на таблица',\n    'table_clear_formatting' => 'Clear table formatting',\n    'resize_to_contents' => 'Resize to contents',\n    'row_header' => 'Row header',\n    'insert_row_before' => 'Вмъкни реда преди',\n    'insert_row_after' => 'Вмъкни реда след',\n    'delete_row' => 'Изтриване на ред',\n    'insert_column_before' => 'Вмъкни колоната преди',\n    'insert_column_after' => 'Вмъкни колоната след',\n    'delete_column' => 'Изтрий колоната',\n    'table_cell' => 'Клетка',\n    'table_row' => 'Ред',\n    'table_column' => 'Колона',\n    'cell_properties' => 'Свойства на клетката',\n    'cell_properties_title' => 'Свойства на клетката',\n    'cell_type' => 'Тип на клетката',\n    'cell_type_cell' => 'Клетка',\n    'cell_scope' => 'Scope',\n    'cell_type_header' => 'Заглавна клетка',\n    'merge_cells' => 'Merge cells',\n    'split_cell' => 'Split cell',\n    'table_row_group' => 'Група от редове',\n    'table_column_group' => 'Група от колони',\n    'horizontal_align' => 'Хоризонтално разположение',\n    'vertical_align' => 'Вертикално разположение',\n    'border_width' => 'Дължината на рамката',\n    'border_style' => 'Стил на рамката',\n    'border_color' => 'Цвят на рамката',\n    'row_properties' => 'Свойства на реда',\n    'row_properties_title' => 'Свойства на реда',\n    'cut_row' => 'Изрежи реда',\n    'copy_row' => 'Копирай реда',\n    'paste_row_before' => 'Постави реда преди',\n    'paste_row_after' => 'Постави реда след',\n    'row_type' => 'Тип на реда',\n    'row_type_header' => 'Заглавка',\n    'row_type_body' => 'Тяло',\n    'row_type_footer' => 'Долна част',\n    'alignment' => 'Подравняване',\n    'cut_column' => 'Изрежи колоната',\n    'copy_column' => 'Копирай колоната',\n    'paste_column_before' => 'Постави колоната преди',\n    'paste_column_after' => 'Постави колоната след',\n    'cell_padding' => 'Отстояние на клетката',\n    'cell_spacing' => 'Отстояние на клетката',\n    'caption' => 'Надпис',\n    'show_caption' => 'Покажи надпис',\n    'constrain' => 'Ограничи пропорциите',\n    'cell_border_solid' => 'Solid',\n    'cell_border_dotted' => 'Dotted',\n    'cell_border_dashed' => 'Dashed',\n    'cell_border_double' => 'Double',\n    'cell_border_groove' => 'Groove',\n    'cell_border_ridge' => 'Ridge',\n    'cell_border_inset' => 'Inset',\n    'cell_border_outset' => 'Outset',\n    'cell_border_none' => 'None',\n    'cell_border_hidden' => 'Hidden',\n\n    // Images, links, details/summary & embed\n    'source' => 'Източник',\n    'alt_desc' => 'Алтернативно описание',\n    'embed' => 'Вгради',\n    'paste_embed' => 'Постави кода за вмъкване отдолу:',\n    'url' => 'URL',\n    'text_to_display' => 'Текст за показване',\n    'title' => 'Заглавие',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Open link',\n    'open_link_in' => 'Open link in...',\n    'open_link_current' => 'Текущ прозорец',\n    'open_link_new' => 'Нов прозорец',\n    'remove_link' => 'Премахване на връзка',\n    'insert_collapsible' => 'Вмъкни сгъваем блок',\n    'collapsible_unwrap' => 'Разгъни',\n    'edit_label' => 'Редактирай етикета',\n    'toggle_open_closed' => 'Превключи отворено/затворено',\n    'collapsible_edit' => 'Редактирай сгъваем блок',\n    'toggle_label' => 'Превключи надписа',\n\n    // About view\n    'about' => 'За редактора',\n    'about_title' => 'Относно визуалния редактор',\n    'editor_license' => 'Лиценз, авторски и сходни права на редактора',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'Този редактор е изграден посредством :tinyLink, което е предоставен под лиценз MIT.',\n    'editor_tiny_license_link' => 'Авторското и сходните му права, както и лицензът на TinyMCE, могат да бъдат намерени тук.',\n    'save_continue' => 'Запази страницата и продължи',\n    'callouts_cycle' => '(Продължавай да натискаш, за да превключваш типовете)',\n    'link_selector' => 'Свържи със съдържанието',\n    'shortcuts' => 'Преки пътища',\n    'shortcut' => 'Пряк път',\n    'shortcuts_intro' => 'Следните клавишни комбинации са налични за редактора:',\n    'windows_linux' => '(Уиндоус/Линукс)',\n    'mac' => '(Мак.)',\n    'description' => 'Описание',\n];\n"
  },
  {
    "path": "lang/bg/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Наскоро създадени',\n    'recently_created_pages' => 'Наскоро създадени страници',\n    'recently_updated_pages' => 'Наскоро актуализирани страници',\n    'recently_created_chapters' => 'Наскоро създадени глави',\n    'recently_created_books' => 'Наскоро създадени книги',\n    'recently_created_shelves' => 'Наскоро създадени рафтове',\n    'recently_update' => 'Наскоро актуализирани',\n    'recently_viewed' => 'Скорошно разгледани',\n    'recent_activity' => 'Последна активност',\n    'create_now' => 'Създай една сега',\n    'revisions' => 'Ревизии',\n    'meta_revision' => 'Ревизия #:revisionCount',\n    'meta_created' => 'Създадено преди :timeLength',\n    'meta_created_name' => 'Създадено преди :timeLength от :user',\n    'meta_updated' => 'Актуализирано :timeLength',\n    'meta_updated_name' => 'Актуализирано преди :timeLength от :user',\n    'meta_owned_name' => 'Притежавано от :user',\n    'meta_reference_count' => 'Referenced by :count item|Referenced by :count items',\n    'entity_select' => 'Избор на обект',\n    'entity_select_lack_permission' => 'You don\\'t have the required permissions to select this item',\n    'images' => 'Изображения',\n    'my_recent_drafts' => 'Моите скорошни драфтове',\n    'my_recently_viewed' => 'Моите скорошни преглеждания',\n    'my_most_viewed_favourites' => 'Моите най-преглеждани любими',\n    'my_favourites' => 'Моите фаворити',\n    'no_pages_viewed' => 'Не сте прегледали никакви страници',\n    'no_pages_recently_created' => 'Не са били създавани страници скоро',\n    'no_pages_recently_updated' => 'Не са били актуализирани страници скоро',\n    'export' => 'Експортиране',\n    'export_html' => 'Прикачени уеб файлове',\n    'export_pdf' => 'PDF файл',\n    'export_text' => 'Обикновен текстов файл',\n    'export_md' => 'Markdown файл',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Default Page Template',\n    'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',\n    'default_template_select' => 'Select a template page',\n    'import' => 'Import',\n    'import_validate' => 'Validate Import',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'Select ZIP file to upload',\n    'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',\n    'import_pending' => 'Pending Imports',\n    'import_pending_none' => 'No imports have been started.',\n    'import_continue' => 'Continue Import',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Run Import',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Uploaded by',\n    'import_location' => 'Import Location',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'Import Errors',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Права',\n    'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.',\n    'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.',\n    'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.',\n    'permissions_save' => 'Запази права',\n    'permissions_owner' => 'Собственик',\n    'permissions_role_everyone_else' => 'Everyone Else',\n    'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',\n    'permissions_role_override' => 'Override permissions for role',\n    'permissions_inherit_defaults' => 'Inherit defaults',\n\n    // Search\n    'search_results' => 'Резултати от търсенето',\n    'search_total_results_found' => ':count резултати намерени|:count общо намерени резултати',\n    'search_clear' => 'Изчисти търсенето',\n    'search_no_pages' => 'Няма страници отговарящи на търсенето',\n    'search_for_term' => 'Търси :term',\n    'search_more' => 'Още резултати',\n    'search_advanced' => 'Подробно търсене',\n    'search_terms' => 'Термини за търсене',\n    'search_content_type' => 'Тип на съдържание',\n    'search_exact_matches' => 'Точни съвпадения',\n    'search_tags' => 'Търсене на тагове',\n    'search_options' => 'Настройки',\n    'search_viewed_by_me' => 'Прегледано от мен',\n    'search_not_viewed_by_me' => 'Непрегледано от мен',\n    'search_permissions_set' => 'Задаване на права',\n    'search_created_by_me' => 'Създадено от мен',\n    'search_updated_by_me' => 'Обновено от мен',\n    'search_owned_by_me' => 'Притежаван от мен',\n    'search_date_options' => 'Настройки на дати',\n    'search_updated_before' => 'Обновено преди',\n    'search_updated_after' => 'Обновено след',\n    'search_created_before' => 'Създадено преди',\n    'search_created_after' => 'Създадено след',\n    'search_set_date' => 'Задаване на дата',\n    'search_update' => 'Обнови търсенето',\n\n    // Shelves\n    'shelf' => 'Рафт',\n    'shelves' => 'Рафтове',\n    'x_shelves' => ':count Рафт|:count Рафтове',\n    'shelves_empty' => 'Няма създадени рафтове',\n    'shelves_create' => 'Създай нов рафт',\n    'shelves_popular' => 'Популярни рафтове',\n    'shelves_new' => 'Нови рафтове',\n    'shelves_new_action' => 'Нов рафт',\n    'shelves_popular_empty' => 'Най-популярните рафтове ще излязат тук.',\n    'shelves_new_empty' => 'Най-новите рафтове ще излязат тук.',\n    'shelves_save' => 'Запази рафт',\n    'shelves_books' => 'Книги на този рафт',\n    'shelves_add_books' => 'Добави книги към този рафт',\n    'shelves_drag_books' => 'Drag books below to add them to this shelf',\n    'shelves_empty_contents' => 'Този рафт няма добавени книги',\n    'shelves_edit_and_assign' => 'Редактирай рафта за да добавиш книги',\n    'shelves_edit_named' => 'Edit Shelf :name',\n    'shelves_edit' => 'Edit Shelf',\n    'shelves_delete' => 'Delete Shelf',\n    'shelves_delete_named' => 'Delete Shelf :name',\n    'shelves_delete_explain' => \"This will delete the shelf with the name ':name'. Contained books will not be deleted.\",\n    'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',\n    'shelves_permissions' => 'Shelf Permissions',\n    'shelves_permissions_updated' => 'Shelf Permissions Updated',\n    'shelves_permissions_active' => 'Shelf Permissions Active',\n    'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',\n    'shelves_permissions_create' => 'Shelf create permissions are only used for copying permissions to child books using the action below. They do not control the ability to create books.',\n    'shelves_copy_permissions_to_books' => 'Копирай настойките за достъп към книгите',\n    'shelves_copy_permissions' => 'Копирай настройките за достъп',\n    'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',\n    'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',\n\n    // Books\n    'book' => 'Книга',\n    'books' => 'Книги',\n    'x_books' => ':count Книга|:count Книги',\n    'books_empty' => 'Няма създадени книги',\n    'books_popular' => 'Популярни книги',\n    'books_recent' => 'Скоро разглеждани книги',\n    'books_new' => 'Нови книги',\n    'books_new_action' => 'Нова книга',\n    'books_popular_empty' => 'Най-популярните книги ще излязат тук.',\n    'books_new_empty' => 'Най-новите книги ще излязат тук.',\n    'books_create' => 'Създай нова книга',\n    'books_delete' => 'Изтрита книга',\n    'books_delete_named' => 'Изтрий книга :bookName',\n    'books_delete_explain' => 'Това действие ще изтрие книга с името \\':bookName\\'. Всички страници и глави ще бъдат изтрити.',\n    'books_delete_confirmation' => 'Сигурен ли сте, че искате да изтриете книгата?',\n    'books_edit' => 'Редактиране на книга',\n    'books_edit_named' => 'Редактирай книга :bookName',\n    'books_form_book_name' => 'Име на книга',\n    'books_save' => 'Запази книга',\n    'books_permissions' => 'Настройки за достъп до книгата',\n    'books_permissions_updated' => 'Настройките за достъп до книгата бяха обновени',\n    'books_empty_contents' => 'Няма създадени страници или глави към тази книга.',\n    'books_empty_create_page' => 'Създаване на нова страница',\n    'books_empty_sort_current_book' => 'Сортирай настоящата книга',\n    'books_empty_add_chapter' => 'Добавяне на раздел',\n    'books_permissions_active' => 'Настройките за достъп до книгата са активни',\n    'books_search_this' => 'Търси в книгата',\n    'books_navigation' => 'Навигация на книгата',\n    'books_sort' => 'Сортирай съдържанието на книгата',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Сортирай книга :bookName',\n    'books_sort_name' => 'Сортиране по име',\n    'books_sort_created' => 'Сортирай по дата на създаване',\n    'books_sort_updated' => 'Сортирай по дата на обновяване',\n    'books_sort_chapters_first' => 'Първа глава',\n    'books_sort_chapters_last' => 'Последна глава',\n    'books_sort_show_other' => 'Покажи други книги',\n    'books_sort_save' => 'Запази новата подредба',\n    'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.',\n    'books_sort_move_up' => 'Move Up',\n    'books_sort_move_down' => 'Move Down',\n    'books_sort_move_prev_book' => 'Move to Previous Book',\n    'books_sort_move_next_book' => 'Move to Next Book',\n    'books_sort_move_prev_chapter' => 'Move Into Previous Chapter',\n    'books_sort_move_next_chapter' => 'Move Into Next Chapter',\n    'books_sort_move_book_start' => 'Move to Start of Book',\n    'books_sort_move_book_end' => 'Move to End of Book',\n    'books_sort_move_before_chapter' => 'Move to Before Chapter',\n    'books_sort_move_after_chapter' => 'Move to After Chapter',\n    'books_copy' => 'Копирай книгата',\n    'books_copy_success' => 'Книгата е копирана успешно',\n\n    // Chapters\n    'chapter' => 'Глава',\n    'chapters' => 'Глави',\n    'x_chapters' => ':count Глава|:count Глави',\n    'chapters_popular' => 'Популярни глави',\n    'chapters_new' => 'Нова глава',\n    'chapters_create' => 'Създай нова глава',\n    'chapters_delete' => 'Изтрий глава',\n    'chapters_delete_named' => 'Изтрий глава :chapterName',\n    'chapters_delete_explain' => 'Това ще изтрие главата \\':chapterName\\'. Всички страници в главата също ще бъдат изтрити.',\n    'chapters_delete_confirm' => 'Сигурни ли сте, че искате да изтриете тази глава?',\n    'chapters_edit' => 'Редактирай глава',\n    'chapters_edit_named' => 'Актуализирай глава :chapterName',\n    'chapters_save' => 'Запази глава',\n    'chapters_move' => 'Премести глава',\n    'chapters_move_named' => 'Премести глава :chapterName',\n    'chapters_copy' => 'Копирай главата',\n    'chapters_copy_success' => 'Главата е копирана успешно',\n    'chapters_permissions' => 'Настойки за достъп на главата',\n    'chapters_empty' => 'Няма създадени страници в тази глава.',\n    'chapters_permissions_active' => 'Настройките за достъп до глава са активни',\n    'chapters_permissions_success' => 'Настройките за достъп до главата бяха обновени',\n    'chapters_search_this' => 'Търси в тази глава',\n    'chapter_sort_book' => 'Sort Book',\n\n    // Pages\n    'page' => 'Страница',\n    'pages' => 'Страници',\n    'x_pages' => ':count Страница|:count Страници',\n    'pages_popular' => 'Популярни страници',\n    'pages_new' => 'Нова страница',\n    'pages_attachments' => 'Прикачени файлове',\n    'pages_navigation' => 'Навигация на страница',\n    'pages_delete' => 'Изтрий страница',\n    'pages_delete_named' => 'Изтрий страница :pageName',\n    'pages_delete_draft_named' => 'Изтрий чернова :pageName',\n    'pages_delete_draft' => 'Изтрий чернова',\n    'pages_delete_success' => 'Страницата е изтрита',\n    'pages_delete_draft_success' => 'Черновата на страницата бе изтрита',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'Сигурни ли сте, че искате да изтриете тази страница?',\n    'pages_delete_draft_confirm' => 'Сигурни ли сте, че искате да изтриете тази чернова?',\n    'pages_editing_named' => 'Редактиране на страница :pageName',\n    'pages_edit_draft_options' => 'Настройки на черновата',\n    'pages_edit_save_draft' => 'Запазване на чернова',\n    'pages_edit_draft' => 'Редактирай на черновата',\n    'pages_editing_draft' => 'Редактиране на чернова',\n    'pages_editing_page' => 'Редактиране на страница',\n    'pages_edit_draft_save_at' => 'Черновата е запазена в ',\n    'pages_edit_delete_draft' => 'Изтрий чернова',\n    'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.',\n    'pages_edit_discard_draft' => 'Отхвърляне на черновата',\n    'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',\n    'pages_edit_switch_to_markdown_clean' => '(Clean Content)',\n    'pages_edit_switch_to_markdown_stable' => '(Stable Content)',\n    'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Задайте регистър на промените',\n    'pages_edit_enter_changelog_desc' => 'Въведете кратко резюме на промените, които сте създали',\n    'pages_edit_enter_changelog' => 'Въведи регистър на промените',\n    'pages_editor_switch_title' => 'Switch Editor',\n    'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',\n    'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',\n    'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',\n    'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',\n    'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\\'t persist across this change.',\n    'pages_save' => 'Запазване на страницата',\n    'pages_title' => 'Заглавие на страницата',\n    'pages_name' => 'Име на страницата',\n    'pages_md_editor' => 'Редактор',\n    'pages_md_preview' => 'Предварителен преглед',\n    'pages_md_insert_image' => 'Добавяна на изображение',\n    'pages_md_insert_link' => 'Добави линк към обекта',\n    'pages_md_insert_drawing' => 'Вмъкни рисунка',\n    'pages_md_show_preview' => 'Show preview',\n    'pages_md_sync_scroll' => 'Sync preview scroll',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Unsaved Drawing Found',\n    'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',\n    'pages_not_in_chapter' => 'Страницата не принадлежи в никоя глава',\n    'pages_move' => 'Премести страницата',\n    'pages_copy' => 'Копиране на страницата',\n    'pages_copy_desination' => 'Копиране на дестинацията',\n    'pages_copy_success' => 'Страницата беше успешно копирана',\n    'pages_permissions' => 'Настройки за достъп на страницата',\n    'pages_permissions_success' => 'Настройките за достъп до страницата бяха обновени',\n    'pages_revision' => 'Ревизия',\n    'pages_revisions' => 'Ревизии на страницата',\n    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',\n    'pages_revisions_named' => 'Ревизии на страницата :pageName',\n    'pages_revision_named' => 'Ревизия на страницата :pageName',\n    'pages_revision_restored_from' => 'Възстановено от #:id; :summary',\n    'pages_revisions_created_by' => 'Създадено от',\n    'pages_revisions_date' => 'Дата на ревизията',\n    'pages_revisions_number' => '№',\n    'pages_revisions_sort_number' => 'Revision Number',\n    'pages_revisions_numbered' => 'Ревизия №:id',\n    'pages_revisions_numbered_changes' => 'Ревизия №:id Промени',\n    'pages_revisions_editor' => 'Editor Type',\n    'pages_revisions_changelog' => 'История на промените',\n    'pages_revisions_changes' => 'Промени',\n    'pages_revisions_current' => 'Текуща версия',\n    'pages_revisions_preview' => 'Предварителен преглед',\n    'pages_revisions_restore' => 'Възстановяване',\n    'pages_revisions_none' => 'Тази страница няма ревизии',\n    'pages_copy_link' => 'Копирай връзката',\n    'pages_edit_content_link' => 'Jump to section in editor',\n    'pages_pointer_enter_mode' => 'Enter section select mode',\n    'pages_pointer_label' => 'Page Section Options',\n    'pages_pointer_permalink' => 'Page Section Permalink',\n    'pages_pointer_include_tag' => 'Page Section Include Tag',\n    'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag',\n    'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink',\n    'pages_permissions_active' => 'Настройките за достъп до страницата са активни',\n    'pages_initial_revision' => 'Първо публикуване',\n    'pages_references_update_revision' => 'System auto-update of internal links',\n    'pages_initial_name' => 'Нова страница',\n    'pages_editing_draft_notification' => 'В момента редактирате чернова, която беше последно обновена :timeDiff.',\n    'pages_draft_edited_notification' => 'Тази страница беше актуализирана от тогава. Препоръчително е да изтриете настоящата чернова.',\n    'pages_draft_page_changed_since_creation' => 'Страницата е била обновена от създаването на черновата. Препоръчително е да изтриеш черновата или да се погрижиш да не презапишеш промени по страницата.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count потребителя започнаха да редактират настоящата страница',\n        'start_b' => ':userName в момента редактира тази страница',\n        'time_a' => 'от както страницата беше актуализирана',\n        'time_b' => 'в последните :minCount минути',\n        'message' => ':start :time. Внимавайте да не попречите на актуализацията на другия!',\n    ],\n    'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',\n    'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',\n    'pages_specific' => 'Определена страница',\n    'pages_is_template' => 'Шаблон на страницата',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Toggle Sidebar',\n    'page_tags' => 'Тагове на страницата',\n    'chapter_tags' => 'Тагове на главата',\n    'book_tags' => 'Тагове на книгата',\n    'shelf_tags' => 'Тагове на рафта',\n    'tag' => 'Таг',\n    'tags' =>  'Тагове',\n    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',\n    'tag_name' =>  'Име на таг',\n    'tag_value' => 'Съдържание на тага (Опционално)',\n    'tags_explain' => \"Добавете няколко тага за да категоризирате по добре вашето съдържание. \\n Може да добавите съдържание на таговете за по-подробна организация.\",\n    'tags_add' => 'Добави друг таг',\n    'tags_remove' => 'Премахни този таг',\n    'tags_usages' => 'Общо ползвания на таг',\n    'tags_assigned_pages' => 'Присвоен на страници',\n    'tags_assigned_chapters' => 'Присвоен на глави',\n    'tags_assigned_books' => 'Присвоен на книги',\n    'tags_assigned_shelves' => 'Присвоен на рафтове',\n    'tags_x_unique_values' => ':count уникални стойности',\n    'tags_all_values' => 'Всички стойности',\n    'tags_view_tags' => 'Виж тагове',\n    'tags_view_existing_tags' => 'Виж съществуващи тагове',\n    'tags_list_empty_hint' => 'Таговете могат да бъдат прилагани чрез страничната лента в редактора на страници или по време на редактирането на детайлите за книги, глави или рафтове.',\n    'attachments' => 'Прикачени файлове',\n    'attachments_explain' => 'Прикачете файлове или линкове, които да са видими на вашата страница. Същите ще бъдат видими във вашето странично поле.',\n    'attachments_explain_instant_save' => 'Промените тук се запазват веднага.',\n    'attachments_upload' => 'Прикачен файл',\n    'attachments_link' => 'Прикачване на линк',\n    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',\n    'attachments_set_link' => 'Поставяне на линк',\n    'attachments_delete' => 'Сигурни ли сте, че искате да изтриете прикачения файл?',\n    'attachments_dropzone' => 'Drop files here to upload',\n    'attachments_no_files' => 'Няма прикачени фалове',\n    'attachments_explain_link' => 'Може да прикачите линк, ако не искате да качвате файл. Този линк може да бъде към друга страница или към файл в облакова пространство.',\n    'attachments_link_name' => 'Има на линка',\n    'attachment_link' => 'Линк към прикачения файл',\n    'attachments_link_url' => 'Линк към файла',\n    'attachments_link_url_hint' => 'Url на сайт или файл',\n    'attach' => 'Прикачване',\n    'attachments_insert_link' => 'Добави линк на прикачен файл към страница',\n    'attachments_edit_file' => 'Редактирай файл',\n    'attachments_edit_file_name' => 'Име на файл',\n    'attachments_edit_drop_upload' => 'Поставете файл или цъкнете тук за да прикачите и обновите',\n    'attachments_order_updated' => 'Прикачения файл беше обновен',\n    'attachments_updated_success' => 'Данните на прикачения файл бяха обновени',\n    'attachments_deleted' => 'Прикачения файл беше изтрит',\n    'attachments_file_uploaded' => 'Файлът беше качен успешно',\n    'attachments_file_updated' => 'Файлът беше обновен успешно',\n    'attachments_link_attached' => 'Линкът беше успешно прикачен към страницата',\n    'templates' => 'Шаблони',\n    'templates_set_as_template' => 'Страницата е шаблон',\n    'templates_explain_set_as_template' => 'Можете да зададете тази страница като шаблон, така че нейното съдържание да бъде използвано при създаването на други страници. Други потребители ще могат да използват този шаблон, ако имат разрешения за преглед на тази страница.',\n    'templates_replace_content' => 'Замени съдържанието на страницата',\n    'templates_append_content' => 'Добави в края на съдържанието на страницата',\n    'templates_prepend_content' => 'Добави в началото на съдържанието на страницата',\n\n    // Profile View\n    'profile_user_for_x' => 'Потребител от :time',\n    'profile_created_content' => 'Създадено съдържание',\n    'profile_not_created_pages' => ':userName не е създал страници',\n    'profile_not_created_chapters' => ':userName не е създавал глави',\n    'profile_not_created_books' => ':userName не е създавал книги',\n    'profile_not_created_shelves' => ':userName не е създавал рафтове',\n\n    // Comments\n    'comment' => 'Коментирай',\n    'comments' => 'Коментари',\n    'comment_add' => 'Добавяне на коментар',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Напишете коментар',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Запази коментар',\n    'comment_new' => 'Нов коментар',\n    'comment_created' => 'коментирано :createDiff',\n    'comment_updated' => 'Актуализирано :updateDiff от :username',\n    'comment_updated_indicator' => 'Updated',\n    'comment_deleted_success' => 'Коментарът е изтрит',\n    'comment_created_success' => 'Коментарът е добавен',\n    'comment_updated_success' => 'Коментарът е обновен',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Наистина ли искате да изтриете този коментар?',\n    'comment_in_reply_to' => 'В отговор на :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',\n\n    // Revision\n    'revision_delete_confirm' => 'Наистина ли искате да изтриете тази версия?',\n    'revision_restore_confirm' => 'Сигурни ли сте, че искате да изтриете тази версия? Настоящата страница ще бъде заместена.',\n    'revision_cannot_delete_latest' => 'Не може да изтриете последната версия.',\n\n    // Copy view\n    'copy_consider' => 'Моля, имай предвид долното при копиране на съдържание.',\n    'copy_consider_permissions' => 'Специфичните настройки на привилегиите няма да бъдат копирани.',\n    'copy_consider_owner' => 'Ти ще станеш собственикът на цялото копирано съдържание.',\n    'copy_consider_images' => 'Файловете на изображенията в страницата няма да бъдат дубликирани и оригиналните изображения ще запазят връзката си със страницата, на която са били качени първоначално.',\n    'copy_consider_attachments' => 'Прикачените към страницата обекти няма да бъдат копирани.',\n    'copy_consider_access' => 'Смяна на местоположението, собственика или привилегиите може да направи това съдържание достъпно за тези, които не са го виждали преди.',\n\n    // Conversions\n    'convert_to_shelf' => 'Convert to Shelf',\n    'convert_to_shelf_contents_desc' => 'You can convert this book to a new shelf with the same contents. Chapters contained within this book will be converted to new books. If this book contains any pages, that are not in a chapter, this book will be renamed and contain such pages, and this book will become part of the new shelf.',\n    'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.',\n    'convert_book' => 'Convert Book',\n    'convert_book_confirm' => 'Are you sure you want to convert this book?',\n    'convert_undo_warning' => 'This cannot be as easily undone.',\n    'convert_to_book' => 'Convert to Book',\n    'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',\n    'convert_chapter' => 'Convert Chapter',\n    'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',\n\n    // References\n    'references' => 'References',\n    'references_none' => 'There are no tracked references to this item.',\n    'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',\n\n    // Watch Options\n    'watch' => 'Watch',\n    'watch_title_default' => 'Default Preferences',\n    'watch_desc_default' => 'Revert watching to just your default notification preferences.',\n    'watch_title_ignore' => 'Ignore',\n    'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.',\n    'watch_title_new' => 'New Pages',\n    'watch_desc_new' => 'Notify when any new page is created within this item.',\n    'watch_title_updates' => 'All Page Updates',\n    'watch_desc_updates' => 'Notify upon all new pages and page changes.',\n    'watch_desc_updates_page' => 'Notify upon all page changes.',\n    'watch_title_comments' => 'All Page Updates & Comments',\n    'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.',\n    'watch_desc_comments_page' => 'Notify upon page changes and new comments.',\n    'watch_change_default' => 'Change default notification preferences',\n    'watch_detail_ignore' => 'Ignoring notifications',\n    'watch_detail_new' => 'Watching for new pages',\n    'watch_detail_updates' => 'Watching new pages and updates',\n    'watch_detail_comments' => 'Watching new pages, updates & comments',\n    'watch_detail_parent_book' => 'Watching via parent book',\n    'watch_detail_parent_book_ignore' => 'Ignoring via parent book',\n    'watch_detail_parent_chapter' => 'Watching via parent chapter',\n    'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter',\n];\n"
  },
  {
    "path": "lang/bg/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Нямате права за достъп до избраната страница.',\n    'permissionJson' => 'Нямате права да извършите тази операция.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Потребител с емайл :email вече съществува но с други данни.',\n    'auth_pre_register_theme_prevention' => 'Потребителски профил не може да бъде създаден с посочената информация',\n    'email_already_confirmed' => 'Емейлът вече беше потвърден. Моля опитрайте да влезете.',\n    'email_confirmation_invalid' => 'Този код за достъп не е валиден или вече е бил използван, Моля опитай да се регистрираш отново.',\n    'email_confirmation_expired' => 'Кодът за потвърждение изтече, нов емейл за потвърждение беше изпратен.',\n    'email_confirmation_awaiting' => 'Емайл адреса, който използвате трябва да се потвърди',\n    'ldap_fail_anonymous' => 'LDAP достъпът е неуспешен с анонимни настройки',\n    'ldap_fail_authed' => 'Опита за достъп чрез LDAP с използваната парола не беше успешен',\n    'ldap_extension_not_installed' => 'LDAP PHP не беше инсталирана',\n    'ldap_cannot_connect' => 'Не може да се свържете с Ldap сървъра, първоначалната връзка се разпадна',\n    'saml_already_logged_in' => 'Вече сте влезли',\n    'saml_no_email_address' => 'Не успяхме да намерим емейл адрес, за този потребител, от информацията предоставена от външната система',\n    'saml_invalid_response_id' => 'Заявката от външната система не е разпознат от процеса започнат от това приложение. Връщането назад след влизане може да породи този проблем.',\n    'saml_fail_authed' => 'Влизането чрез :system не беше успешно, системата не успя да удостовери потребителя',\n    'oidc_already_logged_in' => 'Вече си вписан',\n    'oidc_no_email_address' => 'Не можах да намеря имейл адрес за този потребител в данните, предоставени от външната удостоверителна система',\n    'oidc_fail_authed' => 'Вписването чрез :system не беше успешно, тъй като системата не предостави успешна оторизация',\n    'social_no_action_defined' => 'Действието не беше дефинирано',\n    'social_login_bad_response' => \"Възникна грешка по време на :socialAccount login: \\n:error\",\n    'social_account_in_use' => 'Този :socialAccount вече е използван. Опитайте се да влезете чрез опцията за :socialAccount.',\n    'social_account_email_in_use' => 'Този емейл адрес вече е бил използван. Ако вече имате профил, може да го свържете чрез :socialAccount от вашия профил.',\n    'social_account_existing' => 'Този :socialAccount вече в свързан с вашия профил.',\n    'social_account_already_used_existing' => 'Този :socialAccount вече се използва от друг потребител.',\n    'social_account_not_used' => 'Социалният профил :socialAccount не е свързан с потребител. Моля, свържи го в настройките на профила си. ',\n    'social_account_register_instructions' => 'Ако все още нямаш профил, може да се регистрираш чрез опцията :socialAccount.',\n    'social_driver_not_found' => 'Кодът за връзка със социалната мрежа не съществува',\n    'social_driver_not_configured' => 'Социалните настройки на твоя :socialAccount не са конфигурирани правилно.',\n    'invite_token_expired' => 'Твоята покана е изтекла. Вместо това може да пробваш да възстановиш паролата на профила си.',\n    'login_user_not_found' => 'Потребител за това действие не може да бъде намерено.',\n\n    // System\n    'path_not_writable' => 'Не може да се качи файл в :filePath. Увери се на сървъра, че в пътя може да се записва.',\n    'cannot_get_image_from_url' => 'Не мога да взема съобщението от :url',\n    'cannot_create_thumbs' => 'Сървърът не може да създаде малки изображения. Моля, увери се, че разширението GD PHP е инсталирано.',\n    'server_upload_limit' => 'Сървърът не позволява качвания с такъв размер. Моля, пробвайте файл с по-малък размер.',\n    'server_post_limit' => 'The server cannot receive the provided amount of data. Try again with less data or a smaller file.',\n    'uploaded'  => 'Сървърът не позволява качвания с такъв размер. Моля, пробвайте файл с по-малък размер.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Възникна грешка при качването на изображението',\n    'image_upload_type_error' => 'Типът на качваното изображение е невалиден',\n    'image_upload_replace_type' => 'Image file replacements must be of the same type',\n    'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.',\n    'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.',\n    'image_gallery_thumbnail_memory_limit' => 'Failed to create gallery thumbnails due to system resource limits.',\n    'drawing_data_not_found' => 'Drawing data could not be loaded. The drawing file might no longer exist or you may not have permission to access it.',\n\n    // Attachments\n    'attachment_not_found' => 'Прикачения файл не е намерен',\n    'attachment_upload_error' => 'An error occurred uploading the attachment file',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Неуспешно запазване на черновата. Увери се, че имаш свързаност с интернет преди да запазиш страницата',\n    'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content',\n    'page_custom_home_deletion' => 'Не мога да изтрия страницата, докато е настроена като начална',\n\n    // Entities\n    'entity_not_found' => 'Обектът не е намерен',\n    'bookshelf_not_found' => 'Няма намерен рафт',\n    'book_not_found' => 'Книгата не е намерена',\n    'page_not_found' => 'Страницата не е намерена',\n    'chapter_not_found' => 'Главата не е намерена',\n    'selected_book_not_found' => 'Избраната книга не е намерена',\n    'selected_book_chapter_not_found' => 'Избраната книга или глава не е намерена',\n    'guests_cannot_save_drafts' => 'Гостите не могат да запазват чернови',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Не можеш да изтриеш единствения администратор',\n    'users_cannot_delete_guest' => 'Не можеш да изтриеш потребителя на госта',\n    'users_could_not_send_invite' => 'Could not create user since invite email failed to send',\n\n    // Roles\n    'role_cannot_be_edited' => 'Ролята не може да бъде редактирана',\n    'role_system_cannot_be_deleted' => 'Тази роля е системна и не може да бъде изтрита',\n    'role_registration_default_cannot_delete' => 'Тази роля не може да бъде изтрита, докато е настроена по подразбиране за нови регистрации',\n    'role_cannot_remove_only_admin' => 'Този потребител е единственият с присвоена администраторска роля. Приложи администраторската роля на друг потребител, преди да я премахнеш от тук.',\n\n    // Comments\n    'comment_list' => 'Настъпи грешка при зареждането на коментарите.',\n    'cannot_add_comment_to_draft' => 'Не може да добавяте коментари към чернова.',\n    'comment_add' => 'Възникна грешка при актуализиране/добавяне на коментар.',\n    'comment_delete' => 'Възникна грешка при изтриването на коментара.',\n    'empty_comment' => 'Не може да добавите празен коментар.',\n\n    // Error pages\n    '404_page_not_found' => 'Страницата не е намерена',\n    'sorry_page_not_found' => 'Страницата, която търсите не може да бъде намерена.',\n    'sorry_page_not_found_permission_warning' => 'Ако смятате, че тази страница съществува, най-вероятно нямате право да я преглеждате.',\n    'image_not_found' => 'Изображението не е намерено',\n    'image_not_found_subtitle' => 'Съжалявам, файлът на изображението, което търсиш, не може да бъде намерен.',\n    'image_not_found_details' => 'Ако си очаквал/а това изображение да същестува, може да е било изтрито.',\n    'return_home' => 'Назад към Начало',\n    'error_occurred' => 'Възникна грешка',\n    'app_down' => ':appName не е достъпно в момента',\n    'back_soon' => 'Ще се върне обратно онлайн скоро.',\n\n    // Import\n    'import_zip_cant_read' => 'Could not read ZIP file.',\n    'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',\n    'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Import ZIP failed to validate with errors:',\n    'import_zip_failed_notification' => 'Failed to import ZIP file.',\n    'import_perms_books' => 'You are lacking the required permissions to create books.',\n    'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',\n    'import_perms_pages' => 'You are lacking the required permissions to create pages.',\n    'import_perms_images' => 'You are lacking the required permissions to create images.',\n    'import_perms_attachments' => 'You are lacking the required permission to create attachments.',\n\n    // API errors\n    'api_no_authorization_found' => 'Но беше намерен код за достъп в заявката',\n    'api_bad_authorization_format' => 'В заявката имаше код за достъп, но формата изглежда е неправилен',\n    'api_user_token_not_found' => 'Няма открит API код, който да отговоря на предоставения такъв',\n    'api_incorrect_token_secret' => 'Секретния код, който беше предоставен за достъп до API-а е неправилен',\n    'api_user_no_api_permission' => 'Собственика на АPI кода няма право да прави API заявки',\n    'api_user_token_expired' => 'Кода за достъп, който беше използван, вече не е валиден',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Беше върната грешка, когато се изпрати тестовият емейл:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'The URL does not match the configured allowed SSR hosts',\n];\n"
  },
  {
    "path": "lang/bg/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'New comment on page: :pageName',\n    'new_comment_intro' => 'A user has commented on a page in :appName:',\n    'new_page_subject' => 'New page: :pageName',\n    'new_page_intro' => 'A new page has been created in :appName:',\n    'updated_page_subject' => 'Updated page: :pageName',\n    'updated_page_intro' => 'A page has been updated in :appName:',\n    'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\\'t be sent notifications for further edits to this page by the same editor.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Page Name:',\n    'detail_page_path' => 'Page Path:',\n    'detail_commenter' => 'Commenter:',\n    'detail_comment' => 'Comment:',\n    'detail_created_by' => 'Created By:',\n    'detail_updated_by' => 'Updated By:',\n\n    'action_view_comment' => 'View Comment',\n    'action_view_page' => 'View Page',\n\n    'footer_reason' => 'This notification was sent to you because :link cover this type of activity for this item.',\n    'footer_reason_link' => 'your notification preferences',\n];\n"
  },
  {
    "path": "lang/bg/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Предишна',\n    'next'     => 'Следваща &raquo;',\n\n];\n"
  },
  {
    "path": "lang/bg/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Паролите трябва да имат поне 8 символа и да съвпадат с потвърждението.',\n    'user' => \"Не може да се намери потребител с този имейл адрес.\",\n    'token' => 'Кодът за възстановяване на паролата е невалиден за този емейл адрес.',\n    'sent' => 'На имейла ти е изпратена връзка за възстановяване на паролата ти!',\n    'reset' => 'Парола ти е възстановена!',\n\n];\n"
  },
  {
    "path": "lang/bg/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'My Account',\n\n    'shortcuts' => 'Преки пътища',\n    'shortcuts_interface' => 'UI Shortcut Preferences',\n    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',\n    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',\n    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',\n    'shortcuts_section_navigation' => 'Навигация',\n    'shortcuts_section_actions' => 'Common Actions',\n    'shortcuts_save' => 'Запазване на преките пътища',\n    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing \"?\" which will highlight the available shortcuts for actions currently visible on the screen.',\n    'shortcuts_update_success' => 'Обновени предпочитания за преки пътища!',\n    'shortcuts_overview_desc' => 'Manage keyboard shortcuts you can use to navigate the system user interface.',\n\n    'notifications' => 'Notification Preferences',\n    'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.',\n    'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own',\n    'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Notify upon replies to my comments',\n    'notifications_save' => 'Save Preferences',\n    'notifications_update_success' => 'Notification preferences have been updated!',\n    'notifications_watched' => 'Watched & Ignored Items',\n    'notifications_watched_desc' => 'Below are the items that have custom watch preferences applied. To update your preferences for these, view the item then find the watch options in the sidebar.',\n\n    'auth' => 'Access & Security',\n    'auth_change_password' => 'Change Password',\n    'auth_change_password_desc' => 'Change the password you use to log-in to the application. This must be at least 8 characters long.',\n    'auth_change_password_success' => 'Password has been updated!',\n\n    'profile' => 'Profile Details',\n    'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.',\n    'profile_view_public' => 'View Public Profile',\n    'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.',\n    'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.',\n    'profile_email_no_permission' => 'Unfortunately you don\\'t have permission to change your email address. If you want to change this, you\\'d need to ask an administrator to change this for you.',\n    'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.',\n    'profile_admin_options' => 'Administrator Options',\n    'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the \"Settings > Users\" area of the application.',\n\n    'delete_account' => 'Delete Account',\n    'delete_my_account' => 'Delete My Account',\n    'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\\'ve created, such as created pages and uploaded images, will remain.',\n    'delete_my_account_warning' => 'Are you sure you want to delete your account?',\n];\n"
  },
  {
    "path": "lang/bg/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Настройки',\n    'settings_save' => 'Запази настройките',\n    'system_version' => 'System Version',\n    'categories' => 'Categories',\n\n    // App Settings\n    'app_customization' => 'Персонализиране',\n    'app_features_security' => 'Екстри и Сигурност',\n    'app_name' => 'Име на приложението',\n    'app_name_desc' => 'Това име е включено във всяка шапка и във всеки имейл изпратен от системата.',\n    'app_name_header' => 'Покажи името в шапката',\n    'app_public_access' => 'Публичен достъп',\n    'app_public_access_desc' => 'Активирането на тази настройка, ще позволи на гости, които не са влезли в системта, да имат достъп до съдържанието на вашето приложение.',\n    'app_public_access_desc_guest' => 'Достъпа на гостите може да бъде контролиран от \"Guest\" потребителя.',\n    'app_public_access_toggle' => 'Позволяване на публичен достъп',\n    'app_public_viewing' => 'Позволване на публичен достъп?',\n    'app_secure_images' => 'По-висока сигурност при качване на изображения',\n    'app_secure_images_toggle' => 'Активиране на по-висока сигурност при качване на изображения',\n    'app_secure_images_desc' => 'С цел производителност, всички изображения са публични. Тази настройка добавя случаен, труден за отгатване низ от символи пред линка на изображението. Подсигурете, че индексите на директорията не са включени за да предотвратите лесен достъп.',\n    'app_default_editor' => 'Default Page Editor',\n    'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.',\n    'app_custom_html' => 'Персонализирано съдържание на HTML шапката',\n    'app_custom_html_desc' => 'Всяко съдържание, добавено тук, ще бъде поставено в долната част на секцията <head> на всяка страница. Това е удобно за преобладаващи стилове или добавяне на код за анализ.',\n    'app_custom_html_disabled_notice' => 'Съдържанието на персонализираната HTML шапка е деактивирано на страницата с настройки, за да се гарантира, че евентуални лоши промени могат да бъдат върнати.',\n    'app_logo' => 'Лого на приложението',\n    'app_logo_desc' => 'This is used in the application header bar, among other areas. This image should be 86px in height. Large images will be scaled down.',\n    'app_icon' => 'Application Icon',\n    'app_icon_desc' => 'This icon is used for browser tabs and shortcut icons. This should be a 256px square PNG image.',\n    'app_homepage' => 'Начлна страница на приложението',\n    'app_homepage_desc' => 'Изберете начална страница, която ще замени изгледа по подразбиране. Дефинираните права на страницата, която е избрана ще бъдат игнорирани.',\n    'app_homepage_select' => 'Избери страница',\n    'app_footer_links' => 'Футър линкове',\n    'app_footer_links_desc' => 'Добави линк в съдържанието на футъра. Добавените линкове ще се показват долу в повечето страници, включително и в страниците, в които логването не е задължително. Можете да използвате заместител \"trans::<key>\", за да използвате дума дефинирана от системата. Пример: Използването на \"trans::common.privacy_policy\" ще покаже \"Лични данни\" или на \"trans::common.terms_of_service\" ще покаже \"Общи условия\".',\n    'app_footer_links_label' => 'Надпис на връзката',\n    'app_footer_links_url' => 'Линк URL',\n    'app_footer_links_add' => 'Добави футър линк',\n    'app_disable_comments' => 'Изключи коментарите',\n    'app_disable_comments_toggle' => 'Изключи коментарите',\n    'app_disable_comments_desc' => 'Изключва коментарите във всички на страници на приложението. <br> Съществуващите коментари няма да се показват.',\n\n    // Color settings\n    'color_scheme' => 'Application Color Scheme',\n    'color_scheme_desc' => 'Set the colors to use in the application user interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.',\n    'ui_colors_desc' => 'Set the application primary color and default link color. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the application interface.',\n    'app_color' => 'Primary Color',\n    'link_color' => 'Default Link Color',\n    'content_colors_desc' => 'Set colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.',\n    'bookshelf_color' => 'Цвят на рафта',\n    'book_color' => 'Цвят на книгата',\n    'chapter_color' => 'Цвят на главата',\n    'page_color' => 'Цвят на страницата',\n    'page_draft_color' => 'Цвят на черновата за страница',\n\n    // Registration Settings\n    'reg_settings' => 'Регистрация',\n    'reg_enable' => 'Включи регистрацията',\n    'reg_enable_toggle' => 'Включи регистрацията',\n    'reg_enable_desc' => 'Когато регистрацията е включена, потребителите ще могат да се регистрират като потребители на приложението. След регистрация на тях им се дава роля по подразбиране.',\n    'reg_default_role' => 'Роля по подразбиране след регистрация',\n    'reg_enable_external_warning' => 'Опцията отгоре се игнорира при активно външно LDAP или SAML удостоверяване. Ако удостоверяването от външната система е успешно, автоматично ще се създават потребителски профили за несъществуващи членове.',\n    'reg_email_confirmation' => 'Имейл потвърждение',\n    'reg_email_confirmation_toggle' => 'Изисквай имейл потвърждение',\n    'reg_confirm_email_desc' => 'Ако се използват ограничения за домейна, ще се изисква имейл потвърждение и тази настройка ще бъде игнорирана.',\n    'reg_confirm_restrict_domain' => 'Ограничения за домейна',\n    'reg_confirm_restrict_domain_desc' => 'Въведи разделен със запетаи списък от имейл домейни, до които да бъде ограничена регистрацията. На потребителите ще им бъде изпратен имейл, за да потвърдят адреса, преди да могат да използват приложението. <br> Имай предвид, че потребителите ще могат да сменят имейл адресите си след успешна регистрация.',\n    'reg_confirm_restrict_domain_placeholder' => 'Няма наложени ограничения',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Поддръжка',\n    'maint_image_cleanup' => 'Разчисти изображения',\n    'maint_image_cleanup_desc' => 'Сканира съдържанието на страници и ревизиите, за да провери кои изображения и рисунки се използват и кои се повтарят. Увери се, че имаш пълни резервни копия на базата данни и на изображенията, преди да пуснеш това.',\n    'maint_delete_images_only_in_revisions' => 'Също изтрий изображенията, които съществуват само в стари ревизии на страниците',\n    'maint_image_cleanup_run' => 'Пусни разчистване',\n    'maint_image_cleanup_warning' => 'Намерени са :count потенциално неизползвани изображения. Сигурен/на ли си, че искаш да изтриеш тези изображения?',\n    'maint_image_cleanup_success' => 'Намерени и изтрити са :count потенциално неизползвани изображения!',\n    'maint_image_cleanup_nothing_found' => 'Не са намерени неизползвани изображения и нищо не е изтрито!',\n    'maint_send_test_email' => 'Изпрати тестови имейл',\n    'maint_send_test_email_desc' => 'Това изпраща тестови имейл на имейл адреса, посочен в профила ти.',\n    'maint_send_test_email_run' => 'Изпрати тестов имейл',\n    'maint_send_test_email_success' => 'Имейл изпратен на :address',\n    'maint_send_test_email_mail_subject' => 'Тестов Имейл',\n    'maint_send_test_email_mail_greeting' => 'Изпращането на Имейл работи!',\n    'maint_send_test_email_mail_text' => 'Поздравления! След като получихте този имейл, Вашите имейл настройки са конфигурирани правилно.',\n    'maint_recycle_bin_desc' => 'Изтрити рафти, книги, глави и страници се преместват в кошчето, откъдето можете да ги възстановите или изтриете завинаги. Стари съдържания в кошчето ще бъдат изтрити автоматично след време, в зависимост от настройките на системата.',\n    'maint_recycle_bin_open' => 'Отвори Кошчето',\n    'maint_regen_references' => 'Regenerate References',\n    'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',\n    'maint_regen_references_success' => 'Reference index has been regenerated!',\n    'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Кошче',\n    'recycle_bin_desc' => 'Тук може да възстановиш изтрити обекти или да ги премахнеш завинаги от системата. Този списък не е филтриран, за разлика от подобни списъци с активност в системата, където са приложени списъци за привилегии.',\n    'recycle_bin_deleted_item' => 'Изтрит предмет',\n    'recycle_bin_deleted_parent' => 'Родител',\n    'recycle_bin_deleted_by' => 'Изтрит от',\n    'recycle_bin_deleted_at' => 'Час на изтриване',\n    'recycle_bin_permanently_delete' => 'Изтрий завинаги',\n    'recycle_bin_restore' => 'Възстанови',\n    'recycle_bin_contents_empty' => 'Кошчето е празно',\n    'recycle_bin_empty' => 'Изпразни кочшето',\n    'recycle_bin_empty_confirm' => 'Това ще унищожи завинаги всички обекти в кошчето, включително съдържанието във всеки обект. Сигурен/на ли си, че искаш да изпразниш кошчето?',\n    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?',\n    'recycle_bin_destroy_list' => 'Обекти за унищожение',\n    'recycle_bin_restore_list' => 'Обекти за възстановяване',\n    'recycle_bin_restore_confirm' => 'Това действие ще възстанови изтрития обект, както и всички негови поделементи, в оригиналното им местоположение. Ако оригиналното им местоположение също е изтрито и сега се намира в кошчето, то също ще трябва да бъде възстановено.',\n    'recycle_bin_restore_deleted_parent' => 'Родителският елемент на този обект също е бил изтрит. Тези ще останат изтрити, докато родителят също бъде възстановен.',\n    'recycle_bin_restore_parent' => 'Възстанови родителския елемент',\n    'recycle_bin_destroy_notification' => 'Изтрити общо :count обекта от кошчето.',\n    'recycle_bin_restore_notification' => 'Възстановени общо :count обекта от кошчето.',\n\n    // Audit Log\n    'audit' => 'Ревизорен журнал',\n    'audit_desc' => 'Ревизорният журнал показва списък с всички дейности, следенив системата. Това е нефилтриран списък, за разлика от подобни списъци с дейности в системата, където са приложени филтри за привилегии.',\n    'audit_event_filter' => 'Филтър на събитията',\n    'audit_event_filter_no_filter' => 'Без филтър',\n    'audit_deleted_item' => 'Изтрит предмет',\n    'audit_deleted_item_name' => 'Име: :name',\n    'audit_table_user' => 'Потребител',\n    'audit_table_event' => 'Събитие',\n    'audit_table_related' => 'Свързан обект или детайл',\n    'audit_table_ip' => 'IP адрес',\n    'audit_table_date' => 'Дата на активност',\n    'audit_date_from' => 'Време от',\n    'audit_date_to' => 'Време до',\n\n    // Role Settings\n    'roles' => 'Роли',\n    'role_user_roles' => 'Потребителски роли',\n    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',\n    'roles_x_users_assigned' => ':count user assigned|:count users assigned',\n    'roles_x_permissions_provided' => ':count permission|:count permissions',\n    'roles_assigned_users' => 'Assigned Users',\n    'roles_permissions_provided' => 'Provided Permissions',\n    'role_create' => 'Създай нова роля',\n    'role_delete' => 'Изтрий роля',\n    'role_delete_confirm' => 'Това ще изтрие ролята \\':roleName\\'.',\n    'role_delete_users_assigned' => 'В тази роля се намират :userCount потребители. Ако искате да преместите тези потребители в друга роля, моля изберете нова роля отдолу.',\n    'role_delete_no_migration' => \"Не премествай потребителите в нова роля\",\n    'role_delete_sure' => 'Сигурни ли сте, че искате да изтриете тази роля?',\n    'role_edit' => 'Редактиране на роля',\n    'role_details' => 'Детайли на роля',\n    'role_name' => 'Име на ролята',\n    'role_desc' => 'Кратко описание на ролята',\n    'role_mfa_enforced' => 'Изисква многофакторно удостоверяване',\n    'role_external_auth_id' => 'Външни ауторизиращи ID-a',\n    'role_system' => 'Настойки за достъп на системата',\n    'role_manage_users' => 'Управление на потребители',\n    'role_manage_roles' => 'Управление роли и права',\n    'role_manage_entity_permissions' => 'Управление на правата за достъп всички книги, глави и страници',\n    'role_manage_own_entity_permissions' => 'Управление на правата за достъп на собствени книги, глави и страници',\n    'role_manage_page_templates' => 'Управление на шаблони на страници',\n    'role_access_api' => 'Достъп до API на системата',\n    'role_manage_settings' => 'Управление на настройките на приложението',\n    'role_export_content' => 'Експортирай съдържанието',\n    'role_import_content' => 'Import content',\n    'role_editor_change' => 'Change page editor',\n    'role_notifications' => 'Receive & manage notifications',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Настройки за достъп до активи',\n    'roles_system_warning' => 'Важно: Добавянето на потребител в някое от горните три роли може да му позволи да промени собствените си права или правата на другите в системата. Възлагайте тези роли само на доверени потребители.',\n    'role_asset_desc' => 'Тези настройки за достъп контролират достъпа по подразбиране до активите в системата. Настройките за достъп до книги, глави и страници ще отменят тези настройки.',\n    'role_asset_admins' => 'Администраторите автоматично получават достъп до цялото съдържание, но тези опции могат да показват или скриват опциите за потребителския интерфейс.',\n    'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'Всички',\n    'role_own' => 'Собствени',\n    'role_controlled_by_asset' => 'Контролирани от актива, към който са качени',\n    'role_save' => 'Запази ролята',\n    'role_users' => 'Потребители в тази роля',\n    'role_users_none' => 'В момента няма потребители, назначени за тази роля',\n\n    // Users\n    'users' => 'Потребители',\n    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',\n    'user_profile' => 'Потребителски профил',\n    'users_add_new' => 'Добави нов потребител',\n    'users_search' => 'Търси Потребители',\n    'users_latest_activity' => 'Последна активност',\n    'users_details' => 'Потребителски детайли',\n    'users_details_desc' => 'Настрой име и имейл адрес за този потребител. Имейл адресът ще се използва за вписване в приложението.',\n    'users_details_desc_no_email' => 'Настрой име за този потребител, за да могат другите да го разпознават.',\n    'users_role' => 'Потребителски роли',\n    'users_role_desc' => 'Настрой ролите, които ще бъдат присвоени на този потребител. Ако му бъдат присвоени няколко роли, привилегиите от тях ще се насложат и потребителят ще получи всички привилегии на зададените роли.',\n    'users_password' => 'Потребителска парола',\n    'users_password_desc' => 'Настрой парола за вписване в приложението. Тя трябва да бъде дълга поне 8 знака.',\n    'users_send_invite_text' => 'Можеш да изпратиш на потребителя покана по имейл, след което той ще може да настрои своя собствена парола. В противен случай, ти също можеш да настроиш паролата му.',\n    'users_send_invite_option' => 'Изпрати на потребителя имейл покана',\n    'users_external_auth_id' => 'Външен номер за удостоверяване',\n    'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.',\n    'users_password_warning' => 'Only fill the below if you would like to change the password for this user.',\n    'users_system_public' => 'Този потребител представлява всеки гост, който посещава това приложение. Потребителят не може да се използва за вписване, а вместо това се присвоява автоматично.',\n    'users_delete' => 'Изтрий потребител',\n    'users_delete_named' => 'Изтрий потребителя :userName',\n    'users_delete_warning' => 'Това изцяло ще изтрие този потребител с името \\':userName\\' от системата.',\n    'users_delete_confirm' => 'Сигурни ли сте, че искате да изтриете този потребител?',\n    'users_migrate_ownership' => 'Мигрирайте собствеността на сайта',\n    'users_migrate_ownership_desc' => 'Тук избери потребител, ако желаеш друг да стане собственик на всички обекти, които към момента са притежавани от този потребител.',\n    'users_none_selected' => 'Няма избрани потребители',\n    'users_edit' => 'Редактирай потребител',\n    'users_edit_profile' => 'Редактирай профил',\n    'users_avatar' => 'Потребителски аватар',\n    'users_avatar_desc' => 'Избери изображение, което да представлява този потребител. То трябва да бъде квадрат с размер приблизително 256 пиксела.',\n    'users_preferred_language' => 'Предпочитан език',\n    'users_preferred_language_desc' => 'Тази настройка ще промени езика за потребителския интерфейс на приложението. Това няма да се отрази на създаденото от потребителите съдържание.',\n    'users_social_accounts' => 'Социални профили',\n    'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.',\n    'users_social_accounts_info' => 'Тук можеш да свържеш другите си профили за по-бързо и лесно вписване. Отвързването на профил тук няма да анулира предишно удостоверен достъп. Вместо това, спри достъпа от настройките на профила си в свързаната социална мрежа.',\n    'users_social_connect' => 'Свържи профил',\n    'users_social_disconnect' => 'Отвържи профил',\n    'users_social_status_connected' => 'Connected',\n    'users_social_status_disconnected' => 'Disconnected',\n    'users_social_connected' => 'Профилът :socialAccount беше успешно свързан с профила ти.',\n    'users_social_disconnected' => 'Профилът :socialAccount беше успешно отвързан от профила ти.',\n    'users_api_tokens' => 'API маркери',\n    'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.',\n    'users_api_tokens_none' => 'Няма създадени API маркери за този потребител',\n    'users_api_tokens_create' => 'Създай маркер',\n    'users_api_tokens_expires' => 'Изтича на',\n    'users_api_tokens_docs' => 'Документация на API',\n    'users_mfa' => 'Многофакторно удостоверяване',\n    'users_mfa_desc' => 'Настрой многофакторно удостверяване като втори слой сигурност на твоя профил.',\n    'users_mfa_x_methods' => ':count метод е настроен|:count методи са настроени',\n    'users_mfa_configure' => 'Конфигурирай методи',\n\n    // API Tokens\n    'user_api_token_create' => 'Създай API маркер',\n    'user_api_token_name' => 'Име',\n    'user_api_token_name_desc' => 'Дай на маркера си четимо име като бъдещо напомняне за предназначението му.',\n    'user_api_token_expiry' => 'Дата на изтичане',\n    'user_api_token_expiry_desc' => 'Настрой дата на изтичане на този маркер. След тази дата, заявки направени с този маркер вече няма да работят. Ако оставиш това поле празно, маркерът ще изтече след 100 години.',\n    'user_api_token_create_secret_message' => 'Веднага след създаването на този маркер ще се генерират и покажат \"Номер на маркер\" и \"Тайна на маркер\". Тайната ще бъде показана само веднъж, така че се увери, че си я копирал на сигурно място, преди да продължиш.',\n    'user_api_token' => 'API маркер',\n    'user_api_token_id' => 'Номер на маркер',\n    'user_api_token_id_desc' => 'Това е нередактируем, системно генериран идентификатор за този маркер, който ще бъде необходимо да бъде предоставян в API заявките.',\n    'user_api_token_secret' => 'Тайна на маркер',\n    'user_api_token_secret_desc' => 'Това е системно генерирана тайна за този маркер, която ще бъде необходимо да бъде предоставяна в API заявки. Тайната ще бъде показана само веднъж, така че се увери, че си я копирал на сигурно място.',\n    'user_api_token_created' => 'Маркерът е създаден :timeAgo',\n    'user_api_token_updated' => 'Маркерът е редактиран :timeAgo',\n    'user_api_token_delete' => 'Изтрий маркер',\n    'user_api_token_delete_warning' => 'Това ще изтрие напълно API маркерът с име \\':tokenName\\' от системата.',\n    'user_api_token_delete_confirm' => 'Сигурен/на ли си, че искаш да изтриеш този API маркер?',\n\n    // Webhooks\n    'webhooks' => 'Уебкука',\n    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',\n    'webhooks_x_trigger_events' => ':count trigger event|:count trigger events',\n    'webhooks_create' => 'Създай нова уебкука',\n    'webhooks_none_created' => 'Няма създадени уебкуки.',\n    'webhooks_edit' => 'Редактирай уебкука',\n    'webhooks_save' => 'Запази уебкука',\n    'webhooks_details' => 'Подробности за уебкука',\n    'webhooks_details_desc' => 'Въведи име и POST крайна точка като местоположение, на което уебкуката да изпраща данни.',\n    'webhooks_events' => 'Събития на уебкуката',\n    'webhooks_events_desc' => 'Избери всички събития, които ще задействат съответната уебкука.',\n    'webhooks_events_warning' => 'Имай предвид, че тези събития ще се задействат за всички избрани събития, дори при приложени специфични привилегии. Увери се, че употребата на тази уебкука няма да разкрие чувствително съдържание.',\n    'webhooks_events_all' => 'Всички системни събития',\n    'webhooks_name' => 'Име на уебкука',\n    'webhooks_timeout' => 'Време за изтичане на заявката не уебкуката (в секунди)',\n    'webhooks_endpoint' => 'Крайна точка на уебкуката',\n    'webhooks_active' => 'Уебкуката е активна',\n    'webhook_events_table_header' => 'Събития',\n    'webhooks_delete' => 'Изтрий уебкуката',\n    'webhooks_delete_warning' => 'Това ще изтрие изцяло уебкуката с име \\':webhookName\\' от системата.',\n    'webhooks_delete_confirm' => 'Сигурен/на ли си, че искаш да изтриеш тази уебкука?',\n    'webhooks_format_example' => 'Примерен формат на уебкука',\n    'webhooks_format_example_desc' => 'Данните на уебкуката се изпращат като POST заявки към конфигурираната крайна точка като JSON, следвайки формата отдолу. Свойствата \"related_item\" и \"url\" са по желание и зависят от типа на задействаното събитие.',\n    'webhooks_status' => 'Статус на уебкука',\n    'webhooks_last_called' => 'Последно извикан на:',\n    'webhooks_last_errored' => 'Последна грешка на:',\n    'webhooks_last_error_message' => 'Последно съобщение за грешка:',\n\n    // Licensing\n    'licenses' => 'Licenses',\n    'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.',\n    'licenses_bookstack' => 'BookStack License',\n    'licenses_php' => 'PHP Library Licenses',\n    'licenses_js' => 'JavaScript Library Licenses',\n    'licenses_other' => 'Other Licenses',\n    'license_details' => 'License Details',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/bg/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute трябва да бъде одобрен.',\n    'active_url'           => ':attribute не е валиден URL адрес.',\n    'after'                => ':attribute трябва да е дата след :date.',\n    'alpha'                => ':attribute може да съдържа само букви.',\n    'alpha_dash'           => ':attribute може да съдържа само букви, числа, тире и долна черта.',\n    'alpha_num'            => ':attribute може да съдържа само букви и числа.',\n    'array'                => ':attribute трябва да е масив (array).',\n    'backup_codes'         => 'Предоставеният код не е валиден или вече е бил използван.',\n    'before'               => ':attribute трябва да е дата след :date.',\n    'between'              => [\n        'numeric' => ':attribute трябва да е между :min и :max.',\n        'file'    => ':attribute трябва да е между :min и :max килобайта.',\n        'string'  => 'Дължината на :attribute трябва да бъде между :min и :max символа.',\n        'array'   => 'Атрибутът :attribute трябва да има между :min и :max елемента.',\n    ],\n    'boolean'              => 'Полето :attribute трябва да съдържа булева стойност (true или false).',\n    'confirmed'            => 'Потвърждението на :attribute не съвпада.',\n    'date'                 => ':attribute не е валидна дата.',\n    'date_format'          => ':attribute не е в посоченият формат - :format.',\n    'different'            => ':attribute и :other трябва да са различни.',\n    'digits'               => ':attribute трябва да съдържа :digits цифри.',\n    'digits_between'       => ':attribute трябва да бъде с дължина между :min и :max цифри.',\n    'email'                => ':attribute трябва да бъде валиден имейл адрес.',\n    'ends_with' => ':attribute трябва да свършва с един от следните символи: :values',\n    'file'                 => 'Атрибутът :attribute трябва да бъде предоставен като валиден файл.',\n    'filled'               => 'Полето :attribute е задължителен.',\n    'gt'                   => [\n        'numeric' => ':attribute трябва да бъде по-голям от :value.',\n        'file'    => 'Големината на :attribute трябва да бъде по-голямо от :value килобайта.',\n        'string'  => 'Дължината на :attribute трябва да бъде по-голямо от :value символа.',\n        'array'   => 'Атрибутът :attribute трябва да има повече от :value елемента.',\n    ],\n    'gte'                  => [\n        'numeric' => 'Атрибутът :attribute трябва бъде равен на или по-голям от :value.',\n        'file'    => 'Големината на :attribute трябва да бъде по-голямо или равно на :value килобайта.',\n        'string'  => 'Дължината на :attribute трябва да бъде по-голямо или равно на :value символа.',\n        'array'   => 'Атрибутът :attribute трябва да има поне :value елемента или повече.',\n    ],\n    'exists'               => 'Избраният :attribute е невалиден.',\n    'image'                => ':attribute трябва да e изображение.',\n    'image_extension'      => ':attribute трябва да е валиден и/или допустим графичен файлов формат.',\n    'in'                   => 'Избраният :attribute е невалиден.',\n    'integer'              => ':attribute трябва да бъде цяло число.',\n    'ip'                   => ':attribute трябва да бъде валиден IP адрес.',\n    'ipv4'                 => ':attribute трябва да бъде валиден IPv4 адрес.',\n    'ipv6'                 => ':attribute трябва да бъде валиден IPv6 адрес.',\n    'json'                 => ':attribute трябва да съдържа валиден JSON.',\n    'lt'                   => [\n        'numeric' => ':attribute трябва да бъде по-малко от :value.',\n        'file'    => 'Големината на :attribute трябва да бъде по-малко от :value килобайта.',\n        'string'  => 'Дължината на :attribute трябва да бъде по-малко от :value символа.',\n        'array'   => 'Атрибутът :attribute трябва да има по-малко от :value елемента.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute трябва да бъде по-малко или равно на :value.',\n        'file'    => 'Големината на :attribute трябва да бъде по-малко или равно на :value килобайта.',\n        'string'  => 'Дължината на :attribute трябва да бъде по-малко или равно на :value символа.',\n        'array'   => 'Атрибутът :attribute не трябва да има повече от :value елемента.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute не трябва да бъде по-голям от :max.',\n        'file'    => 'Големината на :attribute не може да бъде по-голямо от :value килобайта.',\n        'string'  => 'Дължината на :attribute не може да бъде по-голямо от :value символа.',\n        'array'   => 'Атрибутът :attribute не може да има повече от :max елемента.',\n    ],\n    'mimes'                => 'Атрибутът :attribute трябва да бъде файл от тип: :values.',\n    'min'                  => [\n        'numeric' => 'Атрибутът :attribute трябва да бъде поне :min.',\n        'file'    => 'Атрибутът :attribute трябва да бъде поне :min килобайта.',\n        'string'  => 'Атрибутът :attribute трябва да бъде съдържа поне :min символа.',\n        'array'   => 'Атрибутът :attribute трябва да има поне :min елемента.',\n    ],\n    'not_in'               => 'Избраният :attribute не е валиден.',\n    'not_regex'            => 'Форматът на :attribute не е валиден.',\n    'numeric'              => 'Атрибутът :attribute трябва да бъде число.',\n    'regex'                => 'Форматът на :attribute не е валиден.',\n    'required'             => 'Полето :attribute е задължително.',\n    'required_if'          => 'Полето :attribute е задължително, когато :other е :value.',\n    'required_with'        => 'Полето :attribute е задължително, когато :values е налично.',\n    'required_with_all'    => 'Полето :attribute е задължително, когато :values са налични.',\n    'required_without'     => 'Полето :attribute е задължително, когато :values не е налично.',\n    'required_without_all' => 'Полето :attribute е задължително, когато никоя стойност от :values не е налична.',\n    'same'                 => 'Атрибутът :attribute и :other трябва да си съвпадат.',\n    'safe_url'             => 'Предоставеният линк може да не е сигурен.',\n    'size'                 => [\n        'numeric' => 'Атрибутът :attribute трябва да бъде :size.',\n        'file'    => 'Атрибутът :attribute трябва да бъде :size килобайта.',\n        'string'  => 'Атрибутът :attribute трябва да бъде с дължина :size знака.',\n        'array'   => 'Атрибутът :attribute трябва да съдържа :size елемента.',\n    ],\n    'string'               => 'Атрибутът :attribute трябва да бъде текст.',\n    'timezone'             => 'Атрибутът :attribute трябва да бъде валидна зона.',\n    'totp'                 => 'Предоставеният код не е валиден или е изтекъл.',\n    'unique'               => 'Атрибутът :attribute вече е зает.',\n    'url'                  => 'Форматът на :attribute не е валиден.',\n    'uploaded'             => 'Файлът не можа да бъде качен. Сървърът може да не приема файлове с такъв размер.',\n\n    'zip_file' => 'The :attribute needs to reference a file within the ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',\n    'zip_model_expected' => 'Data object expected but \":type\" found.',\n    'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Изисква се потвърждение на паролата',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/bn/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'নতুন পৃষ্ঠা সৃষ্টি করেছেন',\n    'page_create_notification'    => 'পৃষ্ঠাটি সার্থকভাবে তৈরী করা হয়েছে',\n    'page_update'                 => 'পৃষ্ঠা হালনাগাদ করেছেন',\n    'page_update_notification'    => 'পৃষ্ঠাটি সার্থকভাবে হালনাগাদ করা হয়েছে',\n    'page_delete'                 => 'পৃষ্ঠা মুছে ফেলেছেন',\n    'page_delete_notification'    => 'পৃষ্ঠাটি সার্থকভাবে মুছে ফেলা হয়েছে',\n    'page_restore'                => 'মুছে ফেলা পৃষ্ঠা পুনরুদ্ধার করেছেন',\n    'page_restore_notification'   => 'পৃষ্ঠাটি সার্থকভাবে পুনরুদ্ধার করা হয়েছে',\n    'page_move'                   => 'পৃষ্ঠা স্থানান্তর করেছেন',\n    'page_move_notification'      => 'পৃষ্ঠাটি সার্থকভাবে স্থানান্তর করা হয়েছে',\n\n    // Chapters\n    'chapter_create'              => 'নতুন অধ্যায় সৃষ্টি করেছেন',\n    'chapter_create_notification' => 'অধ্যায়টি সার্থকভাবে তৈরী করা হয়েছে',\n    'chapter_update'              => 'অধ্যায় হালনাগাদ করেছেন',\n    'chapter_update_notification' => 'অধ্যায়টি সার্থকভাবে হালনাগাদ করা হয়েছে',\n    'chapter_delete'              => 'অধ্যায় মুছে ফেলেছেন',\n    'chapter_delete_notification' => 'অধ্যায়টি সার্থকভাবে মুছে ফেলা হয়েছে',\n    'chapter_move'                => 'অধ্যায় স্থানান্তর করেছেন',\n    'chapter_move_notification' => 'অধ্যায়টি সার্থকভাবে স্থানান্তর করা হয়েছে',\n\n    // Books\n    'book_create'                 => 'নতুন বই সৃষ্টি করেছেন',\n    'book_create_notification'    => 'বইটি সার্থকভাবে তৈরী করা হয়েছে',\n    'book_create_from_chapter'              => 'অধ্যায়কে বইতে রূপান্তরিত করেছেন',\n    'book_create_from_chapter_notification' => 'অধ্যায়কে বইতে রূপান্তর করার প্রক্রিয়া সফলভাবে সম্পন্ন হয়েছে',\n    'book_update'                 => 'বই হালনাগাদ করেছেন',\n    'book_update_notification'    => 'বইটি সার্থকভাবে হালনাগাদ করা হয়েছে',\n    'book_delete'                 => 'বই মুছে ফেলেছেন',\n    'book_delete_notification'    => 'বইটি সার্থকভাবে মুছে ফেলা হয়েছে',\n    'book_sort'                   => 'বইটি ক্রমানুযায়ী সাজিয়েছেন',\n    'book_sort_notification'      => 'বইটি সার্থকভাবে ক্রমানুযায়ী সাজানো হয়েছে',\n\n    // Bookshelves\n    'bookshelf_create'            => 'নতুন বুকশেলফ তৈরী করেছেন',\n    'bookshelf_create_notification'    => 'বুকশেলফটি সার্থকভাবে তৈরী করা হয়েছে',\n    'bookshelf_create_from_book'    => 'বইটিকে বুকশেলফে রূপান্তরিত করার প্রক্রিয়া সফলভাবে সম্পন্ন হয়েছে',\n    'bookshelf_create_from_book_notification'    => 'বইকে বুকশেলফে রূপান্তর করার প্রক্রিয়া সফলভাবে সম্পন্ন হয়েছে',\n    'bookshelf_update'                 => 'বুকশেলফটি হালনাগাদ করেছেন',\n    'bookshelf_update_notification'    => 'বুকশেলফটি সার্থকভাবে হালনাগাদ করা হয়েছে',\n    'bookshelf_delete'                 => 'বুকশেলফটি মুছে ফেলেছেন',\n    'bookshelf_delete_notification'    => 'বুকশেলফটি সার্থকভাবে মুছে ফেলা হয়েছে',\n\n    // Revisions\n    'revision_restore' => 'সংশোধনী পুনঃস্থাপন করেছেন',\n    'revision_delete' => 'সংশোধনী মুছে ফেলেছেন',\n    'revision_delete_notification' => 'সংশোধনী সার্থকভাবে মুছে ফেলা হয়েছে',\n\n    // Favourites\n    'favourite_add_notification' => 'আপনার প্রিয় তালিকায় \":name\" যোগ করা হয়েছে',\n    'favourite_remove_notification' => 'আপনার প্রিয় তালিকা হতে \":name\"-কে মুছে ফেলা হয়েছে',\n\n    // Watching\n    'watch_update_level_notification' => 'পর্যবেক্ষণনীতি সার্থকভাবে হালনাগাদ করা হয়েছে',\n\n    // Auth\n    'auth_login' => 'লগড ইন অবস্থায় আছেন',\n    'auth_register' => 'নতুন ব্যবহারকারী হিসাবে নিবন্ধিত',\n    'auth_password_reset_request' => 'ব্যবহারকারীর পাসওয়ার্ড রিসেটের আবেদন করেছেন',\n    'auth_password_reset_update' => 'ব্যবহারকারী পাসওয়ার্ড রিসেট করুন',\n    'mfa_setup_method' => 'মাল্টি ফ্যাক্টর অথেনটিকেশন সক্রিয় করেছেন',\n    'mfa_setup_method_notification' => 'মাল্টি ফ্যাক্টর অথেনটিকেশন সার্থকভাবে সক্রিয় করা হয়েছে',\n    'mfa_remove_method' => 'মাল্টি ফ্যাক্টর অথেনটিকেশন নিষ্ক্রিয় করেছেন',\n    'mfa_remove_method_notification' => 'মাল্টি ফ্যাক্টর অথেনটিকেশন সার্থকভাবে নিষ্ক্রিয় করা হয়েছে',\n\n    // Settings\n    'settings_update' => 'সেটিংস হালনাগাদ করেছেন',\n    'settings_update_notification' => 'সেটিংস সার্থকভাবে হালনাগাদ করা হয়েছে',\n    'maintenance_action_run' => 'রক্ষণাবেক্ষণ কার্যক্রম চালু করেছেন',\n\n    // Webhooks\n    'webhook_create' => 'নতুন ওয়েবহুক তৈরী করেছেন',\n    'webhook_create_notification' => 'নতুন ওয়েবহুক সার্থকভাবে তৈরী করা হয়েছে',\n    'webhook_update' => 'ওয়েবহুকটি হালনাগাদ করেছেন',\n    'webhook_update_notification' => 'ওয়েবহুকটি সার্থকভাবে হালনাগাদ করা হয়েছে',\n    'webhook_delete' => 'ওয়েবহুকটি মুছে ফেলেছেন',\n    'webhook_delete_notification' => 'ওয়েবহুকটি সার্থকভাবে মুছে ফেলা হয়েছে',\n\n    // Imports\n    'import_create' => 'ইমপোর্টটি তৈরী করেছেন',\n    'import_create_notification' => 'ইমপোর্টটি সার্থকভাবে আপলোড করা হয়েছে',\n    'import_run' => 'ইমপোর্টটি হালনাগাদ করেছেন',\n    'import_run_notification' => 'কনটেন্ট সার্থকভাবে ইমপোর্ট করা হয়েছে',\n    'import_delete' => 'ইমপোর্টটি মুছে ফেলেছেন',\n    'import_delete_notification' => 'ইমপোর্টটি সার্থকভাবে মুছে ফেলা হয়েছে',\n\n    // Users\n    'user_create' => 'নতুন ব্যবহারকারী তৈরী করেছেন',\n    'user_create_notification' => 'নতুন ব্যবহারকারী সার্থকভাবে তৈরী করা হয়েছে',\n    'user_update' => 'ব্যবহারকারীটি হালনাগাদ করেছেন',\n    'user_update_notification' => 'ব্যবহারকারীটি সার্থকভাবে হালনাগাদ করা হয়েছে',\n    'user_delete' => 'ব্যবহারকারীটি মুছে ফেলেছেন',\n    'user_delete_notification' => 'ব্যবহারকারীটি সার্থকভাবে মুছে ফেলা হয়েছে',\n\n    // API Tokens\n    'api_token_create' => 'এপিআই টোকেনটি তৈরী করেছেন',\n    'api_token_create_notification' => 'এপিআই টোকেনটি সার্থকভাবে তৈরী করা হয়েছে',\n    'api_token_update' => 'এপিআই টোকেনটি হালনাগাদ করেছেন',\n    'api_token_update_notification' => 'এপিআই টোকেনটি হালনাগাদ করা হয়েছে',\n    'api_token_delete' => 'এপিআই টোকেনটি মুছে ফেলেছেন',\n    'api_token_delete_notification' => 'এপিআই টোকেনটি সার্থকভাবে মুছে ফেলা হয়েছে',\n\n    // Roles\n    'role_create' => 'রোলটি তৈরী করেছেন',\n    'role_create_notification' => 'রোলটি সার্থকভাবে তৈরী করা হয়েছে',\n    'role_update' => 'রোলটি হালনাগাদ করেছেন',\n    'role_update_notification' => 'রোলটি সার্থকভাবে হালনাগাদ করা হয়েছে',\n    'role_delete' => 'রোলটি মুছে ফেলেছেন',\n    'role_delete_notification' => 'রোলটি সার্থকভাবে মুছে ফেলা হয়েছে',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'রিসাইকেল বিন খালি করে ফেলেছেন',\n    'recycle_bin_restore' => 'রিসাইকেল বিন হতে প্রত্যাবর্তন করা হয়েছে',\n    'recycle_bin_destroy' => 'রিসাইকেল বিন হতে অপসারণ করা হয়েছে',\n\n    // Comments\n    'commented_on'                => 'মন্তব্য প্রদান করেছেন',\n    'comment_create'              => 'মন্তব্য যোগ করেছেন',\n    'comment_update'              => 'মন্তব্য হালনাগাদ করেছেন',\n    'comment_delete'              => 'মন্তব্য মুছে ফেলেছেন',\n\n    // Sort Rules\n    'sort_rule_create' => 'created sort rule',\n    'sort_rule_create_notification' => 'Sort rule successfully created',\n    'sort_rule_update' => 'updated sort rule',\n    'sort_rule_update_notification' => 'রোলটি সার্থকভাবে হালনাগাদ করা হয়েছে',\n    'sort_rule_delete' => 'deleted sort rule',\n    'sort_rule_delete_notification' => 'Sort rule successfully deleted',\n\n    // Other\n    'permissions_update'          => 'অনুমতিক্রম হালনাগাদ করেছেন',\n];\n"
  },
  {
    "path": "lang/bn/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'প্রদত্ত তথ্যনিরূপিত কোন রেকর্ড পাওয়া যায়নি।',\n    'throttle' => 'লগইন প্রচেষ্টার সীমা অতিক্রান্ত। দয়া করে :seconds সেকেন্ড পর আবার চেষ্টা করুন।',\n\n    // Login & Register\n    'sign_up' => 'নিবন্ধিত হোন',\n    'log_in' => 'লগ ইন করুন',\n    'log_in_with' => ':socialDriver দ্বারা লগইন করুন',\n    'sign_up_with' => ':socialDriver দ্বারা নিবন্ধিত হোন',\n    'logout' => 'লগআউট',\n\n    'name' => 'নাম',\n    'username' => 'ব্যবহারকারী',\n    'email' => 'ই-মেইল',\n    'password' => 'পাসওয়ার্ড',\n    'password_confirm' => 'পাসওয়ার্ডের পুনরাবৃত্তি',\n    'password_hint' => 'ন্যূনতম ৮ অক্ষরের হতে হবে',\n    'forgot_password' => 'পাসওয়ার্ড ভুলে গেছেন?',\n    'remember_me' => 'লগইন স্থায়িত্ব ধরে রাখুন',\n    'ldap_email_hint' => 'অনুগ্রহ করে এই অ্যাকাউন্টের জন্য ব্যবহার করার জন্য একটি ইমেইল ঠিকানা লিখুন।',\n    'create_account' => 'অ্যাকাউন্ট তৈরি করুন',\n    'already_have_account' => 'Already have an account?',\n    'dont_have_account' => 'Don\\'t have an account?',\n    'social_login' => 'Social Login',\n    'social_registration' => 'Social Registration',\n    'social_registration_text' => 'Register and sign in using another service.',\n\n    'register_thanks' => 'Thanks for registering!',\n    'register_confirm' => 'Please check your email and click the confirmation button to access :appName.',\n    'registrations_disabled' => 'Registrations are currently disabled',\n    'registration_email_domain_invalid' => 'That email domain does not have access to this application',\n    'register_success' => 'Thanks for signing up! You are now registered and signed in.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'লগইন করার চেষ্টা করা হচ্ছে',\n    'auto_init_starting_desc' => 'We\\'re contacting your authentication system to start the login process. If there\\'s no progress after 5 seconds you can try clicking the link below.',\n    'auto_init_start_link' => 'Proceed with authentication',\n\n    // Password Reset\n    'reset_password' => 'পাসওয়ার্ড রিসেট করুন',\n    'reset_password_send_instructions' => 'Enter your email below and you will be sent an email with a password reset link.',\n    'reset_password_send_button' => 'Send Reset Link',\n    'reset_password_sent' => 'A password reset link will be sent to :email if that email address is found in the system.',\n    'reset_password_success' => 'আপনার পাসওয়ার্ড সফলভাবে রিসেট করা হয়েছে.',\n    'email_reset_subject' => 'Reset your :appName password',\n    'email_reset_text' => 'You are receiving this email because we received a password reset request for your account.',\n    'email_reset_not_requested' => 'If you did not request a password reset, no further action is required.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Confirm your email on :appName',\n    'email_confirm_greeting' => 'Thanks for joining :appName!',\n    'email_confirm_text' => 'Please confirm your email address by clicking the button below:',\n    'email_confirm_action' => 'Confirm Email',\n    'email_confirm_send_error' => 'Email confirmation required but the system could not send the email. Contact the admin to ensure email is set up correctly.',\n    'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',\n    'email_confirm_resent' => 'Confirmation email resent, Please check your inbox.',\n    'email_confirm_thanks' => 'Thanks for confirming!',\n    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the \"Continue\" link below to proceed.',\n\n    'email_not_confirmed' => 'Email Address Not Confirmed',\n    'email_not_confirmed_text' => 'Your email address has not yet been confirmed.',\n    'email_not_confirmed_click_link' => 'Please click the link in the email that was sent shortly after you registered.',\n    'email_not_confirmed_resend' => 'If you cannot find the email you can re-send the confirmation email by submitting the form below.',\n    'email_not_confirmed_resend_button' => 'Resend Confirmation Email',\n\n    // User Invite\n    'user_invite_email_subject' => 'You have been invited to join :appName!',\n    'user_invite_email_greeting' => 'An account has been created for you on :appName.',\n    'user_invite_email_text' => 'Click the button below to set an account password and gain access:',\n    'user_invite_email_action' => 'Set Account Password',\n    'user_invite_page_welcome' => 'Welcome to :appName!',\n    'user_invite_page_text' => 'To finalise your account and gain access you need to set a password which will be used to log-in to :appName on future visits.',\n    'user_invite_page_confirm_button' => 'Confirm Password',\n    'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Setup Multi-Factor Authentication',\n    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'mfa_setup_configured' => 'Already configured',\n    'mfa_setup_reconfigure' => 'Reconfigure',\n    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',\n    'mfa_setup_action' => 'Setup',\n    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',\n    'mfa_option_totp_title' => 'Mobile App',\n    'mfa_option_totp_desc' => 'To use multi-factor authentication you\\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Backup Codes',\n    'mfa_option_backup_codes_desc' => 'Generates a set of one-time-use backup codes which you\\'ll enter on login to verify your identity. Make sure to store these in a safe & secure place.',\n    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',\n    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',\n    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\\'ll be able to use one of the codes as a second authentication mechanism.',\n    'mfa_gen_backup_codes_download' => 'Download Codes',\n    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',\n    'mfa_gen_totp_title' => 'Mobile App Setup',\n    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',\n    'mfa_gen_totp_verify_setup' => 'Verify Setup',\n    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',\n    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',\n    'mfa_verify_access' => 'Verify Access',\n    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\\'re granted access. Verify using one of your configured methods to continue.',\n    'mfa_verify_no_methods' => 'No Methods Configured',\n    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\\'ll need to set up at least one method before you gain access.',\n    'mfa_verify_use_totp' => 'Verify using a mobile app',\n    'mfa_verify_use_backup_codes' => 'Verify using a backup code',\n    'mfa_verify_backup_code' => 'Backup Code',\n    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',\n    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',\n    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',\n    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',\n];\n"
  },
  {
    "path": "lang/bn/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'প্রত্যাহার করুন',\n    'close' => 'বন্ধ করুন',\n    'confirm' => 'নিশ্চিত করুন',\n    'back' => 'প্রত্যাবর্তন করুন',\n    'save' => 'সংরক্ষণ করুন',\n    'continue' => 'অগ্রসর হউন',\n    'select' => 'নির্বাচন করুন',\n    'toggle_all' => 'সবগুলোকে টগল করুন',\n    'more' => 'বিস্তারিত',\n\n    // Form Labels\n    'name' => 'নাম',\n    'description' => 'বিবরণ',\n    'role' => 'রোল',\n    'cover_image' => 'প্রচ্ছদ ছবি',\n    'cover_image_description' => 'এই চিত্রটি আনুমানিক 440x250px হওয়া বাঞ্চনীয়। ক্ষেত্রবিশেষে ও ব্যবহারকারীর ইন্টারফেসের সাথে মানানসই করে উপস্থাপন করার জন্যে প্রয়োজনে এর আকার পরিবর্তন করে প্রদর্শন করা হবে, যা প্রকৃত মাত্রা হতে ভিন্ন হবে৷',\n\n    // Actions\n    'actions' => 'কার্যক্রম',\n    'view' => 'দেখুন',\n    'view_all' => 'সব দেখুন',\n    'new' => 'নতুন',\n    'create' => 'তৈরী করুন',\n    'update' => 'হালনাগাদ করুন',\n    'edit' => 'সম্পাদন করুন',\n    'archive' => 'Archive',\n    'unarchive' => 'Un-Archive',\n    'sort' => 'ক্রমান্বয় করুন',\n    'move' => 'স্থানান্তর করুন',\n    'copy' => 'অনুলিপি করুন',\n    'reply' => 'প্রত্যুত্তর করুন',\n    'delete' => 'মুছে ফেলুন',\n    'delete_confirm' => 'মুছে ফেলা নিশ্চিত করুন',\n    'search' => 'অনুসন্ধান করুন',\n    'search_clear' => 'অনুসন্ধান পুনঃসূচনা করুন',\n    'reset' => 'পুনঃসূচনা করুন',\n    'remove' => 'অপসারণ করুন',\n    'add' => 'যোগ করুন',\n    'configure' => 'সংস্থাপন করুন',\n    'manage' => 'ব্যবস্থাপনা করুন',\n    'fullscreen' => 'ফুলস্ক্রিন',\n    'favourite' => 'প্রিয় তালিকায় যুক্ত করুন',\n    'unfavourite' => 'প্রিয় তালিকা হতে অপসারণ করুন',\n    'next' => 'পরবর্তী',\n    'previous' => 'পূর্ববর্তী',\n    'filter_active' => 'Active Filter:',\n    'filter_clear' => 'Clear Filter',\n    'download' => 'Download',\n    'open_in_tab' => 'Open in Tab',\n    'open' => 'Open',\n\n    // Sort Options\n    'sort_options' => 'Sort Options',\n    'sort_direction_toggle' => 'Sort Direction Toggle',\n    'sort_ascending' => 'Sort Ascending',\n    'sort_descending' => 'Sort Descending',\n    'sort_name' => 'Name',\n    'sort_default' => 'Default',\n    'sort_created_at' => 'Created Date',\n    'sort_updated_at' => 'Updated Date',\n\n    // Misc\n    'deleted_user' => 'Deleted User',\n    'no_activity' => 'No activity to show',\n    'no_items' => 'No items available',\n    'back_to_top' => 'Back to top',\n    'skip_to_main_content' => 'Skip to main content',\n    'toggle_details' => 'Toggle Details',\n    'toggle_thumbnails' => 'Toggle Thumbnails',\n    'details' => 'Details',\n    'grid_view' => 'Grid View',\n    'list_view' => 'List View',\n    'default' => 'Default',\n    'breadcrumb' => 'Breadcrumb',\n    'status' => 'অবস্থা',\n    'status_active' => 'Active',\n    'status_inactive' => 'নিষ্ক্রিয়',\n    'never' => 'অভূতপূর্ব',\n    'none' => 'None',\n\n    // Header\n    'homepage' => 'নীড়পাতা',\n    'header_menu_expand' => 'হেডার মেন্যু প্রসারিত করুন',\n    'profile_menu' => 'প্রোফাইল মেন্যু',\n    'view_profile' => 'প্রোফাইল দেখুন',\n    'edit_profile' => 'প্রোফাইল সম্পাদনা করুন',\n    'dark_mode' => 'নৈশরূপ',\n    'light_mode' => 'দিবারূপ',\n    'global_search' => 'সকল স্থানে অনুসন্ধান',\n\n    // Layout tabs\n    'tab_info' => 'তথ্য',\n    'tab_info_label' => 'ট্যাব: গৌণ তথ্য',\n    'tab_content' => 'কনটেন্ট',\n    'tab_content_label' => 'ট্যাব: মূখ্য তথ্য',\n\n    // Email Content\n    'email_action_help' => 'আপনার যদি \":actionText\"-এ ক্লিক করতে সমস্যা হয়, তবে নিচের লিংকটি কপি করে আপনার ওয়েব ব্রাউজারে পেস্ট করুন:',\n    'email_rights' => 'সর্বস্বত্ব সংরক্ষিত',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'গোপনীয়তা নীতি',\n    'terms_of_service' => 'পরিষেবার শর্তাবলী',\n\n    // OpenSearch\n    'opensearch_description' => 'অনুসন্ধান :appName',\n];\n"
  },
  {
    "path": "lang/bn/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Image Select',\n    'image_list' => 'Image List',\n    'image_details' => 'Image Details',\n    'image_upload' => 'Upload Image',\n    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',\n    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the \"Upload Image\" button above.',\n    'image_all' => 'All',\n    'image_all_title' => 'View all images',\n    'image_book_title' => 'View images uploaded to this book',\n    'image_page_title' => 'View images uploaded to this page',\n    'image_search_hint' => 'Search by image name',\n    'image_uploaded' => 'Uploaded :uploadedDate',\n    'image_uploaded_by' => 'Uploaded by :userName',\n    'image_uploaded_to' => 'Uploaded to :pageLink',\n    'image_updated' => 'Updated :updateDate',\n    'image_load_more' => 'Load More',\n    'image_image_name' => 'Image Name',\n    'image_delete_used' => 'This image is used in the pages below.',\n    'image_delete_confirm_text' => 'Are you sure you want to delete this image?',\n    'image_select_image' => 'Select Image',\n    'image_dropzone' => 'Drop images or click here to upload',\n    'image_dropzone_drop' => 'Drop images here to upload',\n    'images_deleted' => 'Images Deleted',\n    'image_preview' => 'Image Preview',\n    'image_upload_success' => 'Image uploaded successfully',\n    'image_update_success' => 'Image details successfully updated',\n    'image_delete_success' => 'Image successfully deleted',\n    'image_replace' => 'Replace Image',\n    'image_replace_success' => 'Image file successfully updated',\n    'image_rebuild_thumbs' => 'Regenerate Size Variations',\n    'image_rebuild_thumbs_success' => 'Image size variations successfully rebuilt!',\n\n    // Code Editor\n    'code_editor' => 'Edit Code',\n    'code_language' => 'Code Language',\n    'code_content' => 'Code Content',\n    'code_session_history' => 'Session History',\n    'code_save' => 'Save Code',\n];\n"
  },
  {
    "path": "lang/bn/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'সাধারণ',\n    'advanced' => 'উন্নত',\n    'none' => 'অপ্রযোজ্য',\n    'cancel' => 'প্রত্যাহার করুন',\n    'save' => 'সংরক্ষণ করুন',\n    'close' => 'বন্ধ করুন',\n    'apply' => 'Apply',\n    'undo' => 'প্রত্যাহার করুন',\n    'redo' => 'পুনর্বহাল রাখুন',\n    'left' => 'বাম',\n    'center' => 'মধ্য',\n    'right' => 'ডান',\n    'top' => 'উপর',\n    'middle' => 'মধ্য',\n    'bottom' => 'নিচে',\n    'width' => 'প্রস্থ',\n    'height' => 'উচ্চতা',\n    'More' => 'বিস্তারিত',\n    'select' => 'নির্বাচন করুন...',\n\n    // Toolbar\n    'formats' => 'প্রকরণ',\n    'header_large' => 'বড় হেডার',\n    'header_medium' => 'মাঝারি হেডার',\n    'header_small' => 'ছোট হেডার',\n    'header_tiny' => 'ক্ষুদ্র হেডার',\n    'paragraph' => 'প্যারাগ্রাফ',\n    'blockquote' => 'ব্লককোট',\n    'inline_code' => 'ইনলাইন কোড',\n    'callouts' => 'কলআউট',\n    'callout_information' => 'তথ্যমূলক',\n    'callout_success' => 'সফলজনক',\n    'callout_warning' => 'সতর্কতামূলক',\n    'callout_danger' => 'বিপদজনক',\n    'bold' => 'বোল্ড',\n    'italic' => 'ইটালিক',\n    'underline' => 'আন্ডারলাইন',\n    'strikethrough' => 'স্ট্রাইকথ্রু',\n    'superscript' => 'Superscript',\n    'subscript' => 'Subscript',\n    'text_color' => 'Text color',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Custom color',\n    'remove_color' => 'Remove color',\n    'background_color' => 'Background color',\n    'align_left' => 'Align left',\n    'align_center' => 'Align center',\n    'align_right' => 'Align right',\n    'align_justify' => 'Justify',\n    'list_bullet' => 'Bullet list',\n    'list_numbered' => 'Numbered list',\n    'list_task' => 'Task list',\n    'indent_increase' => 'Increase indent',\n    'indent_decrease' => 'Decrease indent',\n    'table' => 'Table',\n    'insert_image' => 'Insert image',\n    'insert_image_title' => 'Insert/Edit Image',\n    'insert_link' => 'Insert/edit link',\n    'insert_link_title' => 'Insert/Edit Link',\n    'insert_horizontal_line' => 'Insert horizontal line',\n    'insert_code_block' => 'Insert code block',\n    'edit_code_block' => 'Edit code block',\n    'insert_drawing' => 'Insert/edit drawing',\n    'drawing_manager' => 'Drawing manager',\n    'insert_media' => 'Insert/edit media',\n    'insert_media_title' => 'Insert/Edit Media',\n    'clear_formatting' => 'Clear formatting',\n    'source_code' => 'Source code',\n    'source_code_title' => 'Source Code',\n    'fullscreen' => 'Fullscreen',\n    'image_options' => 'Image options',\n\n    // Tables\n    'table_properties' => 'Table properties',\n    'table_properties_title' => 'Table Properties',\n    'delete_table' => 'Delete table',\n    'table_clear_formatting' => 'Clear table formatting',\n    'resize_to_contents' => 'Resize to contents',\n    'row_header' => 'Row header',\n    'insert_row_before' => 'Insert row before',\n    'insert_row_after' => 'Insert row after',\n    'delete_row' => 'Delete row',\n    'insert_column_before' => 'Insert column before',\n    'insert_column_after' => 'Insert column after',\n    'delete_column' => 'Delete column',\n    'table_cell' => 'Cell',\n    'table_row' => 'Row',\n    'table_column' => 'Column',\n    'cell_properties' => 'Cell properties',\n    'cell_properties_title' => 'Cell Properties',\n    'cell_type' => 'Cell type',\n    'cell_type_cell' => 'Cell',\n    'cell_scope' => 'Scope',\n    'cell_type_header' => 'Header cell',\n    'merge_cells' => 'Merge cells',\n    'split_cell' => 'Split cell',\n    'table_row_group' => 'Row Group',\n    'table_column_group' => 'Column Group',\n    'horizontal_align' => 'Horizontal align',\n    'vertical_align' => 'Vertical align',\n    'border_width' => 'Border width',\n    'border_style' => 'Border style',\n    'border_color' => 'Border color',\n    'row_properties' => 'Row properties',\n    'row_properties_title' => 'Row Properties',\n    'cut_row' => 'Cut row',\n    'copy_row' => 'Copy row',\n    'paste_row_before' => 'Paste row before',\n    'paste_row_after' => 'Paste row after',\n    'row_type' => 'Row type',\n    'row_type_header' => 'Header',\n    'row_type_body' => 'Body',\n    'row_type_footer' => 'Footer',\n    'alignment' => 'Alignment',\n    'cut_column' => 'Cut column',\n    'copy_column' => 'Copy column',\n    'paste_column_before' => 'Paste column before',\n    'paste_column_after' => 'Paste column after',\n    'cell_padding' => 'Cell padding',\n    'cell_spacing' => 'Cell spacing',\n    'caption' => 'Caption',\n    'show_caption' => 'Show caption',\n    'constrain' => 'Constrain proportions',\n    'cell_border_solid' => 'Solid',\n    'cell_border_dotted' => 'Dotted',\n    'cell_border_dashed' => 'Dashed',\n    'cell_border_double' => 'Double',\n    'cell_border_groove' => 'Groove',\n    'cell_border_ridge' => 'Ridge',\n    'cell_border_inset' => 'Inset',\n    'cell_border_outset' => 'Outset',\n    'cell_border_none' => 'None',\n    'cell_border_hidden' => 'Hidden',\n\n    // Images, links, details/summary & embed\n    'source' => 'Source',\n    'alt_desc' => 'Alternative description',\n    'embed' => 'Embed',\n    'paste_embed' => 'Paste your embed code below:',\n    'url' => 'URL',\n    'text_to_display' => 'Text to display',\n    'title' => 'Title',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Open link',\n    'open_link_in' => 'Open link in...',\n    'open_link_current' => 'Current window',\n    'open_link_new' => 'New window',\n    'remove_link' => 'Remove link',\n    'insert_collapsible' => 'Insert collapsible block',\n    'collapsible_unwrap' => 'Unwrap',\n    'edit_label' => 'Edit label',\n    'toggle_open_closed' => 'Toggle open/closed',\n    'collapsible_edit' => 'Edit collapsible block',\n    'toggle_label' => 'Toggle label',\n\n    // About view\n    'about' => 'About the editor',\n    'about_title' => 'About the WYSIWYG Editor',\n    'editor_license' => 'Editor License & Copyright',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under the MIT license.',\n    'editor_tiny_license_link' => 'The copyright and license details of TinyMCE can be found here.',\n    'save_continue' => 'Save Page & Continue',\n    'callouts_cycle' => '(Keep pressing to toggle through types)',\n    'link_selector' => 'Link to content',\n    'shortcuts' => 'Shortcuts',\n    'shortcut' => 'Shortcut',\n    'shortcuts_intro' => 'The following shortcuts are available in the editor:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Description',\n];\n"
  },
  {
    "path": "lang/bn/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Recently Created',\n    'recently_created_pages' => 'Recently Created Pages',\n    'recently_updated_pages' => 'Recently Updated Pages',\n    'recently_created_chapters' => 'Recently Created Chapters',\n    'recently_created_books' => 'Recently Created Books',\n    'recently_created_shelves' => 'Recently Created Shelves',\n    'recently_update' => 'Recently Updated',\n    'recently_viewed' => 'Recently Viewed',\n    'recent_activity' => 'Recent Activity',\n    'create_now' => 'Create one now',\n    'revisions' => 'Revisions',\n    'meta_revision' => 'Revision #:revisionCount',\n    'meta_created' => 'Created :timeLength',\n    'meta_created_name' => 'Created :timeLength by :user',\n    'meta_updated' => 'Updated :timeLength',\n    'meta_updated_name' => 'Updated :timeLength by :user',\n    'meta_owned_name' => 'Owned by :user',\n    'meta_reference_count' => 'Referenced by :count item|Referenced by :count items',\n    'entity_select' => 'Entity Select',\n    'entity_select_lack_permission' => 'You don\\'t have the required permissions to select this item',\n    'images' => 'Images',\n    'my_recent_drafts' => 'My Recent Drafts',\n    'my_recently_viewed' => 'My Recently Viewed',\n    'my_most_viewed_favourites' => 'My Most Viewed Favourites',\n    'my_favourites' => 'My Favourites',\n    'no_pages_viewed' => 'You have not viewed any pages',\n    'no_pages_recently_created' => 'No pages have been recently created',\n    'no_pages_recently_updated' => 'No pages have been recently updated',\n    'export' => 'Export',\n    'export_html' => 'Contained Web File',\n    'export_pdf' => 'PDF File',\n    'export_text' => 'Plain Text File',\n    'export_md' => 'Markdown File',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Default Page Template',\n    'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',\n    'default_template_select' => 'Select a template page',\n    'import' => 'Import',\n    'import_validate' => 'Validate Import',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'Select ZIP file to upload',\n    'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',\n    'import_pending' => 'Pending Imports',\n    'import_pending_none' => 'No imports have been started.',\n    'import_continue' => 'Continue Import',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Run Import',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Uploaded by',\n    'import_location' => 'Import Location',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'Import Errors',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Permissions',\n    'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.',\n    'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.',\n    'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.',\n    'permissions_save' => 'Save Permissions',\n    'permissions_owner' => 'Owner',\n    'permissions_role_everyone_else' => 'Everyone Else',\n    'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',\n    'permissions_role_override' => 'Override permissions for role',\n    'permissions_inherit_defaults' => 'Inherit defaults',\n\n    // Search\n    'search_results' => 'Search Results',\n    'search_total_results_found' => ':count result found|:count total results found',\n    'search_clear' => 'Clear Search',\n    'search_no_pages' => 'No pages matched this search',\n    'search_for_term' => 'Search for :term',\n    'search_more' => 'More Results',\n    'search_advanced' => 'Advanced Search',\n    'search_terms' => 'Search Terms',\n    'search_content_type' => 'Content Type',\n    'search_exact_matches' => 'Exact Matches',\n    'search_tags' => 'Tag Searches',\n    'search_options' => 'Options',\n    'search_viewed_by_me' => 'Viewed by me',\n    'search_not_viewed_by_me' => 'Not viewed by me',\n    'search_permissions_set' => 'Permissions set',\n    'search_created_by_me' => 'Created by me',\n    'search_updated_by_me' => 'Updated by me',\n    'search_owned_by_me' => 'Owned by me',\n    'search_date_options' => 'Date Options',\n    'search_updated_before' => 'Updated before',\n    'search_updated_after' => 'Updated after',\n    'search_created_before' => 'Created before',\n    'search_created_after' => 'Created after',\n    'search_set_date' => 'Set Date',\n    'search_update' => 'Update Search',\n\n    // Shelves\n    'shelf' => 'Shelf',\n    'shelves' => 'Shelves',\n    'x_shelves' => ':count Shelf|:count Shelves',\n    'shelves_empty' => 'No shelves have been created',\n    'shelves_create' => 'Create New Shelf',\n    'shelves_popular' => 'Popular Shelves',\n    'shelves_new' => 'New Shelves',\n    'shelves_new_action' => 'New Shelf',\n    'shelves_popular_empty' => 'The most popular shelves will appear here.',\n    'shelves_new_empty' => 'The most recently created shelves will appear here.',\n    'shelves_save' => 'Save Shelf',\n    'shelves_books' => 'Books on this shelf',\n    'shelves_add_books' => 'Add books to this shelf',\n    'shelves_drag_books' => 'Drag books below to add them to this shelf',\n    'shelves_empty_contents' => 'This shelf has no books assigned to it',\n    'shelves_edit_and_assign' => 'Edit shelf to assign books',\n    'shelves_edit_named' => 'Edit Shelf :name',\n    'shelves_edit' => 'Edit Shelf',\n    'shelves_delete' => 'Delete Shelf',\n    'shelves_delete_named' => 'Delete Shelf :name',\n    'shelves_delete_explain' => \"This will delete the shelf with the name ':name'. Contained books will not be deleted.\",\n    'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',\n    'shelves_permissions' => 'Shelf Permissions',\n    'shelves_permissions_updated' => 'Shelf Permissions Updated',\n    'shelves_permissions_active' => 'Shelf Permissions Active',\n    'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',\n    'shelves_permissions_create' => 'Shelf create permissions are only used for copying permissions to child books using the action below. They do not control the ability to create books.',\n    'shelves_copy_permissions_to_books' => 'Copy Permissions to Books',\n    'shelves_copy_permissions' => 'Copy Permissions',\n    'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',\n    'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',\n\n    // Books\n    'book' => 'Book',\n    'books' => 'Books',\n    'x_books' => ':count Book|:count Books',\n    'books_empty' => 'No books have been created',\n    'books_popular' => 'Popular Books',\n    'books_recent' => 'Recent Books',\n    'books_new' => 'New Books',\n    'books_new_action' => 'New Book',\n    'books_popular_empty' => 'The most popular books will appear here.',\n    'books_new_empty' => 'The most recently created books will appear here.',\n    'books_create' => 'Create New Book',\n    'books_delete' => 'Delete Book',\n    'books_delete_named' => 'Delete Book :bookName',\n    'books_delete_explain' => 'This will delete the book with the name \\':bookName\\'. All pages and chapters will be removed.',\n    'books_delete_confirmation' => 'Are you sure you want to delete this book?',\n    'books_edit' => 'Edit Book',\n    'books_edit_named' => 'Edit Book :bookName',\n    'books_form_book_name' => 'Book Name',\n    'books_save' => 'Save Book',\n    'books_permissions' => 'Book Permissions',\n    'books_permissions_updated' => 'Book Permissions Updated',\n    'books_empty_contents' => 'No pages or chapters have been created for this book.',\n    'books_empty_create_page' => 'Create a new page',\n    'books_empty_sort_current_book' => 'Sort the current book',\n    'books_empty_add_chapter' => 'Add a chapter',\n    'books_permissions_active' => 'Book Permissions Active',\n    'books_search_this' => 'Search this book',\n    'books_navigation' => 'Book Navigation',\n    'books_sort' => 'Sort Book Contents',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Sort Book :bookName',\n    'books_sort_name' => 'Sort by Name',\n    'books_sort_created' => 'Sort by Created Date',\n    'books_sort_updated' => 'Sort by Updated Date',\n    'books_sort_chapters_first' => 'Chapters First',\n    'books_sort_chapters_last' => 'Chapters Last',\n    'books_sort_show_other' => 'Show Other Books',\n    'books_sort_save' => 'Save New Order',\n    'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.',\n    'books_sort_move_up' => 'Move Up',\n    'books_sort_move_down' => 'Move Down',\n    'books_sort_move_prev_book' => 'Move to Previous Book',\n    'books_sort_move_next_book' => 'Move to Next Book',\n    'books_sort_move_prev_chapter' => 'Move Into Previous Chapter',\n    'books_sort_move_next_chapter' => 'Move Into Next Chapter',\n    'books_sort_move_book_start' => 'Move to Start of Book',\n    'books_sort_move_book_end' => 'Move to End of Book',\n    'books_sort_move_before_chapter' => 'Move to Before Chapter',\n    'books_sort_move_after_chapter' => 'Move to After Chapter',\n    'books_copy' => 'Copy Book',\n    'books_copy_success' => 'Book successfully copied',\n\n    // Chapters\n    'chapter' => 'Chapter',\n    'chapters' => 'Chapters',\n    'x_chapters' => ':count Chapter|:count Chapters',\n    'chapters_popular' => 'Popular Chapters',\n    'chapters_new' => 'New Chapter',\n    'chapters_create' => 'Create New Chapter',\n    'chapters_delete' => 'Delete Chapter',\n    'chapters_delete_named' => 'Delete Chapter :chapterName',\n    'chapters_delete_explain' => 'This will delete the chapter with the name \\':chapterName\\'. All pages that exist within this chapter will also be deleted.',\n    'chapters_delete_confirm' => 'Are you sure you want to delete this chapter?',\n    'chapters_edit' => 'Edit Chapter',\n    'chapters_edit_named' => 'Edit Chapter :chapterName',\n    'chapters_save' => 'Save Chapter',\n    'chapters_move' => 'Move Chapter',\n    'chapters_move_named' => 'Move Chapter :chapterName',\n    'chapters_copy' => 'Copy Chapter',\n    'chapters_copy_success' => 'Chapter successfully copied',\n    'chapters_permissions' => 'Chapter Permissions',\n    'chapters_empty' => 'No pages are currently in this chapter.',\n    'chapters_permissions_active' => 'Chapter Permissions Active',\n    'chapters_permissions_success' => 'Chapter Permissions Updated',\n    'chapters_search_this' => 'Search this chapter',\n    'chapter_sort_book' => 'Sort Book',\n\n    // Pages\n    'page' => 'Page',\n    'pages' => 'Pages',\n    'x_pages' => ':count Page|:count Pages',\n    'pages_popular' => 'Popular Pages',\n    'pages_new' => 'New Page',\n    'pages_attachments' => 'Attachments',\n    'pages_navigation' => 'Page Navigation',\n    'pages_delete' => 'Delete Page',\n    'pages_delete_named' => 'Delete Page :pageName',\n    'pages_delete_draft_named' => 'Delete Draft Page :pageName',\n    'pages_delete_draft' => 'Delete Draft Page',\n    'pages_delete_success' => 'Page deleted',\n    'pages_delete_draft_success' => 'Draft page deleted',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'Are you sure you want to delete this page?',\n    'pages_delete_draft_confirm' => 'Are you sure you want to delete this draft page?',\n    'pages_editing_named' => 'Editing Page :pageName',\n    'pages_edit_draft_options' => 'Draft Options',\n    'pages_edit_save_draft' => 'Save Draft',\n    'pages_edit_draft' => 'Edit Page Draft',\n    'pages_editing_draft' => 'Editing Draft',\n    'pages_editing_page' => 'Editing Page',\n    'pages_edit_draft_save_at' => 'Draft saved at ',\n    'pages_edit_delete_draft' => 'Delete Draft',\n    'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.',\n    'pages_edit_discard_draft' => 'Discard Draft',\n    'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',\n    'pages_edit_switch_to_markdown_clean' => '(Clean Content)',\n    'pages_edit_switch_to_markdown_stable' => '(Stable Content)',\n    'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Set Changelog',\n    'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\\'ve made',\n    'pages_edit_enter_changelog' => 'Enter Changelog',\n    'pages_editor_switch_title' => 'Switch Editor',\n    'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',\n    'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',\n    'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',\n    'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',\n    'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\\'t persist across this change.',\n    'pages_save' => 'Save Page',\n    'pages_title' => 'Page Title',\n    'pages_name' => 'Page Name',\n    'pages_md_editor' => 'Editor',\n    'pages_md_preview' => 'Preview',\n    'pages_md_insert_image' => 'Insert Image',\n    'pages_md_insert_link' => 'Insert Entity Link',\n    'pages_md_insert_drawing' => 'Insert Drawing',\n    'pages_md_show_preview' => 'Show preview',\n    'pages_md_sync_scroll' => 'Sync preview scroll',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Unsaved Drawing Found',\n    'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',\n    'pages_not_in_chapter' => 'Page is not in a chapter',\n    'pages_move' => 'Move Page',\n    'pages_copy' => 'Copy Page',\n    'pages_copy_desination' => 'Copy Destination',\n    'pages_copy_success' => 'Page successfully copied',\n    'pages_permissions' => 'Page Permissions',\n    'pages_permissions_success' => 'Page permissions updated',\n    'pages_revision' => 'Revision',\n    'pages_revisions' => 'Page Revisions',\n    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',\n    'pages_revisions_named' => 'Page Revisions for :pageName',\n    'pages_revision_named' => 'Page Revision for :pageName',\n    'pages_revision_restored_from' => 'Restored from #:id; :summary',\n    'pages_revisions_created_by' => 'Created By',\n    'pages_revisions_date' => 'Revision Date',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Revision Number',\n    'pages_revisions_numbered' => 'Revision #:id',\n    'pages_revisions_numbered_changes' => 'Revision #:id Changes',\n    'pages_revisions_editor' => 'Editor Type',\n    'pages_revisions_changelog' => 'Changelog',\n    'pages_revisions_changes' => 'Changes',\n    'pages_revisions_current' => 'Current Version',\n    'pages_revisions_preview' => 'Preview',\n    'pages_revisions_restore' => 'Restore',\n    'pages_revisions_none' => 'This page has no revisions',\n    'pages_copy_link' => 'Copy Link',\n    'pages_edit_content_link' => 'Jump to section in editor',\n    'pages_pointer_enter_mode' => 'Enter section select mode',\n    'pages_pointer_label' => 'Page Section Options',\n    'pages_pointer_permalink' => 'Page Section Permalink',\n    'pages_pointer_include_tag' => 'Page Section Include Tag',\n    'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag',\n    'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink',\n    'pages_permissions_active' => 'Page Permissions Active',\n    'pages_initial_revision' => 'Initial publish',\n    'pages_references_update_revision' => 'System auto-update of internal links',\n    'pages_initial_name' => 'New Page',\n    'pages_editing_draft_notification' => 'You are currently editing a draft that was last saved :timeDiff.',\n    'pages_draft_edited_notification' => 'This page has been updated by since that time. It is recommended that you discard this draft.',\n    'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count users have started editing this page',\n        'start_b' => ':userName has started editing this page',\n        'time_a' => 'since the page was last updated',\n        'time_b' => 'in the last :minCount minutes',\n        'message' => ':start :time. Take care not to overwrite each other\\'s updates!',\n    ],\n    'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',\n    'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',\n    'pages_specific' => 'Specific Page',\n    'pages_is_template' => 'Page Template',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Toggle Sidebar',\n    'page_tags' => 'Page Tags',\n    'chapter_tags' => 'Chapter Tags',\n    'book_tags' => 'Book Tags',\n    'shelf_tags' => 'Shelf Tags',\n    'tag' => 'Tag',\n    'tags' =>  'Tags',\n    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',\n    'tag_name' =>  'Tag Name',\n    'tag_value' => 'Tag Value (Optional)',\n    'tags_explain' => \"Add some tags to better categorise your content. \\n You can assign a value to a tag for more in-depth organisation.\",\n    'tags_add' => 'Add another tag',\n    'tags_remove' => 'Remove this tag',\n    'tags_usages' => 'Total tag usages',\n    'tags_assigned_pages' => 'Assigned to Pages',\n    'tags_assigned_chapters' => 'Assigned to Chapters',\n    'tags_assigned_books' => 'Assigned to Books',\n    'tags_assigned_shelves' => 'Assigned to Shelves',\n    'tags_x_unique_values' => ':count unique values',\n    'tags_all_values' => 'All values',\n    'tags_view_tags' => 'View Tags',\n    'tags_view_existing_tags' => 'View existing tags',\n    'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',\n    'attachments' => 'Attachments',\n    'attachments_explain' => 'Upload some files or attach some links to display on your page. These are visible in the page sidebar.',\n    'attachments_explain_instant_save' => 'Changes here are saved instantly.',\n    'attachments_upload' => 'Upload File',\n    'attachments_link' => 'Attach Link',\n    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',\n    'attachments_set_link' => 'Set Link',\n    'attachments_delete' => 'Are you sure you want to delete this attachment?',\n    'attachments_dropzone' => 'Drop files here to upload',\n    'attachments_no_files' => 'No files have been uploaded',\n    'attachments_explain_link' => 'You can attach a link if you\\'d prefer not to upload a file. This can be a link to another page or a link to a file in the cloud.',\n    'attachments_link_name' => 'Link Name',\n    'attachment_link' => 'Attachment link',\n    'attachments_link_url' => 'Link to file',\n    'attachments_link_url_hint' => 'Url of site or file',\n    'attach' => 'Attach',\n    'attachments_insert_link' => 'Add Attachment Link to Page',\n    'attachments_edit_file' => 'Edit File',\n    'attachments_edit_file_name' => 'File Name',\n    'attachments_edit_drop_upload' => 'Drop files or click here to upload and overwrite',\n    'attachments_order_updated' => 'Attachment order updated',\n    'attachments_updated_success' => 'Attachment details updated',\n    'attachments_deleted' => 'Attachment deleted',\n    'attachments_file_uploaded' => 'File successfully uploaded',\n    'attachments_file_updated' => 'File successfully updated',\n    'attachments_link_attached' => 'Link successfully attached to page',\n    'templates' => 'Templates',\n    'templates_set_as_template' => 'Page is a template',\n    'templates_explain_set_as_template' => 'You can set this page as a template so its contents be utilized when creating other pages. Other users will be able to use this template if they have view permissions for this page.',\n    'templates_replace_content' => 'Replace page content',\n    'templates_append_content' => 'Append to page content',\n    'templates_prepend_content' => 'Prepend to page content',\n\n    // Profile View\n    'profile_user_for_x' => 'User for :time',\n    'profile_created_content' => 'Created Content',\n    'profile_not_created_pages' => ':userName has not created any pages',\n    'profile_not_created_chapters' => ':userName has not created any chapters',\n    'profile_not_created_books' => ':userName has not created any books',\n    'profile_not_created_shelves' => ':userName has not created any shelves',\n\n    // Comments\n    'comment' => 'Comment',\n    'comments' => 'Comments',\n    'comment_add' => 'Add Comment',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Leave a comment here',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Save Comment',\n    'comment_new' => 'New Comment',\n    'comment_created' => 'commented :createDiff',\n    'comment_updated' => 'Updated :updateDiff by :username',\n    'comment_updated_indicator' => 'Updated',\n    'comment_deleted_success' => 'Comment deleted',\n    'comment_created_success' => 'Comment added',\n    'comment_updated_success' => 'Comment updated',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Are you sure you want to delete this comment?',\n    'comment_in_reply_to' => 'In reply to :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',\n\n    // Revision\n    'revision_delete_confirm' => 'Are you sure you want to delete this revision?',\n    'revision_restore_confirm' => 'Are you sure you want to restore this revision? The current page contents will be replaced.',\n    'revision_cannot_delete_latest' => 'Cannot delete the latest revision.',\n\n    // Copy view\n    'copy_consider' => 'Please consider the below when copying content.',\n    'copy_consider_permissions' => 'Custom permission settings will not be copied.',\n    'copy_consider_owner' => 'You will become the owner of all copied content.',\n    'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',\n    'copy_consider_attachments' => 'Page attachments will not be copied.',\n    'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',\n\n    // Conversions\n    'convert_to_shelf' => 'Convert to Shelf',\n    'convert_to_shelf_contents_desc' => 'You can convert this book to a new shelf with the same contents. Chapters contained within this book will be converted to new books. If this book contains any pages, that are not in a chapter, this book will be renamed and contain such pages, and this book will become part of the new shelf.',\n    'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.',\n    'convert_book' => 'Convert Book',\n    'convert_book_confirm' => 'Are you sure you want to convert this book?',\n    'convert_undo_warning' => 'This cannot be as easily undone.',\n    'convert_to_book' => 'Convert to Book',\n    'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',\n    'convert_chapter' => 'Convert Chapter',\n    'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',\n\n    // References\n    'references' => 'References',\n    'references_none' => 'There are no tracked references to this item.',\n    'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',\n\n    // Watch Options\n    'watch' => 'Watch',\n    'watch_title_default' => 'Default Preferences',\n    'watch_desc_default' => 'Revert watching to just your default notification preferences.',\n    'watch_title_ignore' => 'Ignore',\n    'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.',\n    'watch_title_new' => 'New Pages',\n    'watch_desc_new' => 'Notify when any new page is created within this item.',\n    'watch_title_updates' => 'All Page Updates',\n    'watch_desc_updates' => 'Notify upon all new pages and page changes.',\n    'watch_desc_updates_page' => 'Notify upon all page changes.',\n    'watch_title_comments' => 'All Page Updates & Comments',\n    'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.',\n    'watch_desc_comments_page' => 'Notify upon page changes and new comments.',\n    'watch_change_default' => 'Change default notification preferences',\n    'watch_detail_ignore' => 'Ignoring notifications',\n    'watch_detail_new' => 'Watching for new pages',\n    'watch_detail_updates' => 'Watching new pages and updates',\n    'watch_detail_comments' => 'Watching new pages, updates & comments',\n    'watch_detail_parent_book' => 'Watching via parent book',\n    'watch_detail_parent_book_ignore' => 'Ignoring via parent book',\n    'watch_detail_parent_chapter' => 'Watching via parent chapter',\n    'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter',\n];\n"
  },
  {
    "path": "lang/bn/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'অনুরোধকৃত পৃষ্ঠাটিতে আপনার ব্যবহারাধিকারের অনুমতি নেই।',\n    'permissionJson' => 'You do not have permission to perform the requested action.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'A user with the email :email already exists but with different credentials.',\n    'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',\n    'email_already_confirmed' => 'Email has already been confirmed, Try logging in.',\n    'email_confirmation_invalid' => 'This confirmation token is not valid or has already been used, Please try registering again.',\n    'email_confirmation_expired' => 'The confirmation token has expired, A new confirmation email has been sent.',\n    'email_confirmation_awaiting' => 'The email address for the account in use needs to be confirmed',\n    'ldap_fail_anonymous' => 'LDAP access failed using anonymous bind',\n    'ldap_fail_authed' => 'LDAP access failed using given dn & password details',\n    'ldap_extension_not_installed' => 'LDAP PHP extension not installed',\n    'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed',\n    'saml_already_logged_in' => 'Already logged in',\n    'saml_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',\n    'saml_invalid_response_id' => 'The request from the external authentication system is not recognised by a process started by this application. Navigating back after a login could cause this issue.',\n    'saml_fail_authed' => 'Login using :system failed, system did not provide successful authorization',\n    'oidc_already_logged_in' => 'Already logged in',\n    'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',\n    'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',\n    'social_no_action_defined' => 'No action defined',\n    'social_login_bad_response' => \"Error received during :socialAccount login: \\n:error\",\n    'social_account_in_use' => 'This :socialAccount account is already in use, Try logging in via the :socialAccount option.',\n    'social_account_email_in_use' => 'The email :email is already in use. If you already have an account you can connect your :socialAccount account from your profile settings.',\n    'social_account_existing' => 'This :socialAccount is already attached to your profile.',\n    'social_account_already_used_existing' => 'This :socialAccount account is already used by another user.',\n    'social_account_not_used' => 'This :socialAccount account is not linked to any users. Please attach it in your profile settings. ',\n    'social_account_register_instructions' => 'If you do not yet have an account, You can register an account using the :socialAccount option.',\n    'social_driver_not_found' => 'Social driver not found',\n    'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.',\n    'invite_token_expired' => 'This invitation link has expired. You can instead try to reset your account password.',\n    'login_user_not_found' => 'A user for this action could not be found.',\n\n    // System\n    'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.',\n    'cannot_get_image_from_url' => 'Cannot get image from :url',\n    'cannot_create_thumbs' => 'The server cannot create thumbnails. Please check you have the GD PHP extension installed.',\n    'server_upload_limit' => 'The server does not allow uploads of this size. Please try a smaller file size.',\n    'server_post_limit' => 'The server cannot receive the provided amount of data. Try again with less data or a smaller file.',\n    'uploaded'  => 'The server does not allow uploads of this size. Please try a smaller file size.',\n\n    // Drawing & Images\n    'image_upload_error' => 'An error occurred uploading the image',\n    'image_upload_type_error' => 'The image type being uploaded is invalid',\n    'image_upload_replace_type' => 'Image file replacements must be of the same type',\n    'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.',\n    'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.',\n    'image_gallery_thumbnail_memory_limit' => 'Failed to create gallery thumbnails due to system resource limits.',\n    'drawing_data_not_found' => 'Drawing data could not be loaded. The drawing file might no longer exist or you may not have permission to access it.',\n\n    // Attachments\n    'attachment_not_found' => 'Attachment not found',\n    'attachment_upload_error' => 'An error occurred uploading the attachment file',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page',\n    'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content',\n    'page_custom_home_deletion' => 'Cannot delete a page while it is set as a homepage',\n\n    // Entities\n    'entity_not_found' => 'Entity not found',\n    'bookshelf_not_found' => 'Shelf not found',\n    'book_not_found' => 'Book not found',\n    'page_not_found' => 'Page not found',\n    'chapter_not_found' => 'Chapter not found',\n    'selected_book_not_found' => 'The selected book was not found',\n    'selected_book_chapter_not_found' => 'The selected Book or Chapter was not found',\n    'guests_cannot_save_drafts' => 'Guests cannot save drafts',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'You cannot delete the only admin',\n    'users_cannot_delete_guest' => 'You cannot delete the guest user',\n    'users_could_not_send_invite' => 'Could not create user since invite email failed to send',\n\n    // Roles\n    'role_cannot_be_edited' => 'This role cannot be edited',\n    'role_system_cannot_be_deleted' => 'This role is a system role and cannot be deleted',\n    'role_registration_default_cannot_delete' => 'This role cannot be deleted while set as the default registration role',\n    'role_cannot_remove_only_admin' => 'This user is the only user assigned to the administrator role. Assign the administrator role to another user before attempting to remove it here.',\n\n    // Comments\n    'comment_list' => 'An error occurred while fetching the comments.',\n    'cannot_add_comment_to_draft' => 'You cannot add comments to a draft.',\n    'comment_add' => 'An error occurred while adding / updating the comment.',\n    'comment_delete' => 'An error occurred while deleting the comment.',\n    'empty_comment' => 'Cannot add an empty comment.',\n\n    // Error pages\n    '404_page_not_found' => 'Page Not Found',\n    'sorry_page_not_found' => 'Sorry, The page you were looking for could not be found.',\n    'sorry_page_not_found_permission_warning' => 'If you expected this page to exist, you might not have permission to view it.',\n    'image_not_found' => 'Image Not Found',\n    'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',\n    'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',\n    'return_home' => 'Return to home',\n    'error_occurred' => 'An Error Occurred',\n    'app_down' => ':appName is down right now',\n    'back_soon' => 'It will be back up soon.',\n\n    // Import\n    'import_zip_cant_read' => 'Could not read ZIP file.',\n    'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',\n    'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Import ZIP failed to validate with errors:',\n    'import_zip_failed_notification' => 'Failed to import ZIP file.',\n    'import_perms_books' => 'You are lacking the required permissions to create books.',\n    'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',\n    'import_perms_pages' => 'You are lacking the required permissions to create pages.',\n    'import_perms_images' => 'You are lacking the required permissions to create images.',\n    'import_perms_attachments' => 'You are lacking the required permission to create attachments.',\n\n    // API errors\n    'api_no_authorization_found' => 'No authorization token found on the request',\n    'api_bad_authorization_format' => 'An authorization token was found on the request but the format appeared incorrect',\n    'api_user_token_not_found' => 'No matching API token was found for the provided authorization token',\n    'api_incorrect_token_secret' => 'The secret provided for the given used API token is incorrect',\n    'api_user_no_api_permission' => 'The owner of the used API token does not have permission to make API calls',\n    'api_user_token_expired' => 'The authorization token used has expired',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Error thrown when sending a test email:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'The URL does not match the configured allowed SSR hosts',\n];\n"
  },
  {
    "path": "lang/bn/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'New comment on page: :pageName',\n    'new_comment_intro' => 'A user has commented on a page in :appName:',\n    'new_page_subject' => 'New page: :pageName',\n    'new_page_intro' => 'A new page has been created in :appName:',\n    'updated_page_subject' => 'Updated page: :pageName',\n    'updated_page_intro' => 'A page has been updated in :appName:',\n    'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\\'t be sent notifications for further edits to this page by the same editor.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Page Name:',\n    'detail_page_path' => 'Page Path:',\n    'detail_commenter' => 'Commenter:',\n    'detail_comment' => 'Comment:',\n    'detail_created_by' => 'Created By:',\n    'detail_updated_by' => 'Updated By:',\n\n    'action_view_comment' => 'View Comment',\n    'action_view_page' => 'View Page',\n\n    'footer_reason' => 'This notification was sent to you because :link cover this type of activity for this item.',\n    'footer_reason_link' => 'your notification preferences',\n];\n"
  },
  {
    "path": "lang/bn/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; পূর্ববর্তী',\n    'next'     => 'পরবর্তী &raquo;',\n\n];\n"
  },
  {
    "path": "lang/bn/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'পাসওয়ার্ড কমপক্ষে আট অক্ষরের হতে হবে এবং পাসওয়ার্ড নিশ্চিতকরণের ঘরে প্রদত্ত পাসওয়ার্ডের সাথে মিলতে হবে।',\n    'user' => \"প্রদত্ত ইমেইল ঠিকানার স্বাপেক্ষে কোন ব্যবহারকারী খুঁজে পাওয়া যায়নি।\",\n    'token' => 'প্রদত্ত পাসওয়ার্ড রিসেট টোকেন অত্র ইমেল ঠিকানার জন্য বৈধ নয়৷',\n    'sent' => 'আপনার পাসওয়ার্ড রিসেট লিঙ্কটি ই-মেইলের মাধ্যমে প্রেরণ করা হয়েছে!',\n    'reset' => 'আপনার পাসওয়ার্ডটি রিসেট করা হয়েছে!',\n\n];\n"
  },
  {
    "path": "lang/bn/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'আমার অ্যাকাউন্ট',\n\n    'shortcuts' => 'Shortcuts',\n    'shortcuts_interface' => 'UI Shortcut Preferences',\n    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',\n    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',\n    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',\n    'shortcuts_section_navigation' => 'Navigation',\n    'shortcuts_section_actions' => 'Common Actions',\n    'shortcuts_save' => 'Save Shortcuts',\n    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing \"?\" which will highlight the available shortcuts for actions currently visible on the screen.',\n    'shortcuts_update_success' => 'Shortcut preferences have been updated!',\n    'shortcuts_overview_desc' => 'Manage keyboard shortcuts you can use to navigate the system user interface.',\n\n    'notifications' => 'Notification Preferences',\n    'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.',\n    'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own',\n    'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Notify upon replies to my comments',\n    'notifications_save' => 'Save Preferences',\n    'notifications_update_success' => 'Notification preferences have been updated!',\n    'notifications_watched' => 'Watched & Ignored Items',\n    'notifications_watched_desc' => 'Below are the items that have custom watch preferences applied. To update your preferences for these, view the item then find the watch options in the sidebar.',\n\n    'auth' => 'Access & Security',\n    'auth_change_password' => 'Change Password',\n    'auth_change_password_desc' => 'Change the password you use to log-in to the application. This must be at least 8 characters long.',\n    'auth_change_password_success' => 'Password has been updated!',\n\n    'profile' => 'Profile Details',\n    'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.',\n    'profile_view_public' => 'View Public Profile',\n    'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.',\n    'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.',\n    'profile_email_no_permission' => 'Unfortunately you don\\'t have permission to change your email address. If you want to change this, you\\'d need to ask an administrator to change this for you.',\n    'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.',\n    'profile_admin_options' => 'Administrator Options',\n    'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the \"Settings > Users\" area of the application.',\n\n    'delete_account' => 'Delete Account',\n    'delete_my_account' => 'Delete My Account',\n    'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\\'ve created, such as created pages and uploaded images, will remain.',\n    'delete_my_account_warning' => 'Are you sure you want to delete your account?',\n];\n"
  },
  {
    "path": "lang/bn/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'সেটিংস',\n    'settings_save' => 'সেটিংস সংরক্ষণ করুন',\n    'system_version' => 'সিস্টেম ভার্সন',\n    'categories' => 'শ্রেণীবিভাগ সমূহ',\n\n    // App Settings\n    'app_customization' => 'নিজস্বীকরণ',\n    'app_features_security' => 'ফিচারসমূহ ও নিরাপত্তা',\n    'app_name' => 'এপ্লিকেশনের নাম',\n    'app_name_desc' => 'এই নামটি হেডারে এবং যেকোন সিস্টেম-প্রেরিত ইমেলে দেখানো হয়।',\n    'app_name_header' => 'হেডারে নাম দেখান',\n    'app_public_access' => 'পাবলিক এক্সেস',\n    'app_public_access_desc' => 'উক্ত অপশনটি সক্রিয় করলে আপনার বুকস্ট্যাক ওয়েবসাইটের সকল তথ্য, যে কেউ লগ ইন করা ছাড়াই, দেখতে বা পড়তে অথবা এক্সেস করতে পারবেন।',\n    'app_public_access_desc_guest' => '\"Guest\" ব্যবহারকারীর মাধ্যমে ওয়েবসাইট ভিসিটরদের পঠনধিকার নিয়ন্ত্রণ করা যেতে পারে।',\n    'app_public_access_toggle' => 'পাবলিক অ্যাক্সেসের অনুমতি দিন',\n    'app_public_viewing' => 'সকলের জন্যে উন্মুক্ত করতে চান?',\n    'app_secure_images' => 'Higher Security Image Uploads',\n    'app_secure_images_toggle' => 'Enable higher security image uploads',\n    'app_secure_images_desc' => 'For performance reasons, all images are public. This option adds a random, hard-to-guess string in front of image urls. Ensure directory indexes are not enabled to prevent easy access.',\n    'app_default_editor' => 'Default Page Editor',\n    'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.',\n    'app_custom_html' => 'Custom HTML Head Content',\n    'app_custom_html_desc' => 'Any content added here will be inserted into the bottom of the <head> section of every page. This is handy for overriding styles or adding analytics code.',\n    'app_custom_html_disabled_notice' => 'Custom HTML head content is disabled on this settings page to ensure any breaking changes can be reverted.',\n    'app_logo' => 'অ্যাপ্লিকেশনের লোগো',\n    'app_logo_desc' => 'এটি অ্যাপ্লিকেশনের হেডার বার-এ দেখানো হবে।  উক্ত ছবিটির উচ্চতা সর্বোচ্চ ৮৬ পিক্সেলের হতে হবে। অধিকতর উচ্চতার ছবিকে স্কেল ডাউন করা হবে। ',\n    'app_icon' => 'অ্যাপ্লিকেশনের আইকন',\n    'app_icon_desc' => 'This icon is used for browser tabs and shortcut icons. This should be a 256px square PNG image.',\n    'app_homepage' => 'অ্যাপ্লিকেশনের নীড়পাতা',\n    'app_homepage_desc' => 'Select a view to show on the homepage instead of the default view. Page permissions are ignored for selected pages.',\n    'app_homepage_select' => 'একটি পৃষ্ঠা নির্বাচন করুন',\n    'app_footer_links' => 'ফুটার লিঙ্কসমূহ',\n    'app_footer_links_desc' => 'Add links to show within the site footer. These will be displayed at the bottom of most pages, including those that do not require login. You can use a label of \"trans::<key>\" to use system-defined translations. For example: Using \"trans::common.privacy_policy\" will provide the translated text \"Privacy Policy\" and \"trans::common.terms_of_service\" will provide the translated text \"Terms of Service\".',\n    'app_footer_links_label' => 'Link Label',\n    'app_footer_links_url' => 'Link URL',\n    'app_footer_links_add' => 'Add Footer Link',\n    'app_disable_comments' => 'Disable Comments',\n    'app_disable_comments_toggle' => 'Disable comments',\n    'app_disable_comments_desc' => 'Disables comments across all pages in the application. <br> Existing comments are not shown.',\n\n    // Color settings\n    'color_scheme' => 'Application Color Scheme',\n    'color_scheme_desc' => 'Set the colors to use in the application user interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.',\n    'ui_colors_desc' => 'Set the application primary color and default link color. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the application interface.',\n    'app_color' => 'Primary Color',\n    'link_color' => 'Default Link Color',\n    'content_colors_desc' => 'Set colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.',\n    'bookshelf_color' => 'Shelf Color',\n    'book_color' => 'Book Color',\n    'chapter_color' => 'Chapter Color',\n    'page_color' => 'Page Color',\n    'page_draft_color' => 'Page Draft Color',\n\n    // Registration Settings\n    'reg_settings' => 'Registration',\n    'reg_enable' => 'Enable Registration',\n    'reg_enable_toggle' => 'Enable registration',\n    'reg_enable_desc' => 'When registration is enabled user will be able to sign themselves up as an application user. Upon registration they are given a single, default user role.',\n    'reg_default_role' => 'Default user role after registration',\n    'reg_enable_external_warning' => 'The option above is ignored while external LDAP or SAML authentication is active. User accounts for non-existing members will be auto-created if authentication, against the external system in use, is successful.',\n    'reg_email_confirmation' => 'Email Confirmation',\n    'reg_email_confirmation_toggle' => 'Require email confirmation',\n    'reg_confirm_email_desc' => 'If domain restriction is used then email confirmation will be required and this option will be ignored.',\n    'reg_confirm_restrict_domain' => 'Domain Restriction',\n    'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.',\n    'reg_confirm_restrict_domain_placeholder' => 'No restriction set',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Maintenance',\n    'maint_image_cleanup' => 'Cleanup Images',\n    'maint_image_cleanup_desc' => 'Scans page & revision content to check which images and drawings are currently in use and which images are redundant. Ensure you create a full database and image backup before running this.',\n    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',\n    'maint_image_cleanup_run' => 'Run Cleanup',\n    'maint_image_cleanup_warning' => ':count potentially unused images were found. Are you sure you want to delete these images?',\n    'maint_image_cleanup_success' => ':count potentially unused images found and deleted!',\n    'maint_image_cleanup_nothing_found' => 'No unused images found, Nothing deleted!',\n    'maint_send_test_email' => 'Send a Test Email',\n    'maint_send_test_email_desc' => 'This sends a test email to your email address specified in your profile.',\n    'maint_send_test_email_run' => 'Send test email',\n    'maint_send_test_email_success' => 'Email sent to :address',\n    'maint_send_test_email_mail_subject' => 'Test Email',\n    'maint_send_test_email_mail_greeting' => 'Email delivery seems to work!',\n    'maint_send_test_email_mail_text' => 'Congratulations! As you received this email notification, your email settings seem to be configured properly.',\n    'maint_recycle_bin_desc' => 'Deleted shelves, books, chapters & pages are sent to the recycle bin so they can be restored or permanently deleted. Older items in the recycle bin may be automatically removed after a while depending on system configuration.',\n    'maint_recycle_bin_open' => 'Open Recycle Bin',\n    'maint_regen_references' => 'Regenerate References',\n    'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',\n    'maint_regen_references_success' => 'Reference index has been regenerated!',\n    'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Recycle Bin',\n    'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',\n    'recycle_bin_deleted_item' => 'Deleted Item',\n    'recycle_bin_deleted_parent' => 'Parent',\n    'recycle_bin_deleted_by' => 'Deleted By',\n    'recycle_bin_deleted_at' => 'Deletion Time',\n    'recycle_bin_permanently_delete' => 'Permanently Delete',\n    'recycle_bin_restore' => 'Restore',\n    'recycle_bin_contents_empty' => 'The recycle bin is currently empty',\n    'recycle_bin_empty' => 'Empty Recycle Bin',\n    'recycle_bin_empty_confirm' => 'This will permanently destroy all items in the recycle bin including content contained within each item. Are you sure you want to empty the recycle bin?',\n    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?',\n    'recycle_bin_destroy_list' => 'Items to be Destroyed',\n    'recycle_bin_restore_list' => 'Items to be Restored',\n    'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.',\n    'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.',\n    'recycle_bin_restore_parent' => 'Restore Parent',\n    'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',\n    'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',\n\n    // Audit Log\n    'audit' => 'Audit Log',\n    'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',\n    'audit_event_filter' => 'Event Filter',\n    'audit_event_filter_no_filter' => 'No Filter',\n    'audit_deleted_item' => 'Deleted Item',\n    'audit_deleted_item_name' => 'Name: :name',\n    'audit_table_user' => 'User',\n    'audit_table_event' => 'Event',\n    'audit_table_related' => 'Related Item or Detail',\n    'audit_table_ip' => 'IP Address',\n    'audit_table_date' => 'Activity Date',\n    'audit_date_from' => 'Date Range From',\n    'audit_date_to' => 'Date Range To',\n\n    // Role Settings\n    'roles' => 'Roles',\n    'role_user_roles' => 'User Roles',\n    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',\n    'roles_x_users_assigned' => ':count user assigned|:count users assigned',\n    'roles_x_permissions_provided' => ':count permission|:count permissions',\n    'roles_assigned_users' => 'Assigned Users',\n    'roles_permissions_provided' => 'Provided Permissions',\n    'role_create' => 'Create New Role',\n    'role_delete' => 'Delete Role',\n    'role_delete_confirm' => 'This will delete the role with the name \\':roleName\\'.',\n    'role_delete_users_assigned' => 'This role has :userCount users assigned to it. If you would like to migrate the users from this role select a new role below.',\n    'role_delete_no_migration' => \"Don't migrate users\",\n    'role_delete_sure' => 'Are you sure you want to delete this role?',\n    'role_edit' => 'Edit Role',\n    'role_details' => 'Role Details',\n    'role_name' => 'Role Name',\n    'role_desc' => 'Short Description of Role',\n    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',\n    'role_external_auth_id' => 'External Authentication IDs',\n    'role_system' => 'System Permissions',\n    'role_manage_users' => 'Manage users',\n    'role_manage_roles' => 'Manage roles & role permissions',\n    'role_manage_entity_permissions' => 'Manage all book, chapter & page permissions',\n    'role_manage_own_entity_permissions' => 'Manage permissions on own book, chapter & pages',\n    'role_manage_page_templates' => 'Manage page templates',\n    'role_access_api' => 'Access system API',\n    'role_manage_settings' => 'Manage app settings',\n    'role_export_content' => 'Export content',\n    'role_import_content' => 'Import content',\n    'role_editor_change' => 'Change page editor',\n    'role_notifications' => 'Receive & manage notifications',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Asset Permissions',\n    'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',\n    'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',\n    'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.',\n    'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'All',\n    'role_own' => 'Own',\n    'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',\n    'role_save' => 'Save Role',\n    'role_users' => 'Users in this role',\n    'role_users_none' => 'No users are currently assigned to this role',\n\n    // Users\n    'users' => 'Users',\n    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',\n    'user_profile' => 'User Profile',\n    'users_add_new' => 'Add New User',\n    'users_search' => 'Search Users',\n    'users_latest_activity' => 'Latest Activity',\n    'users_details' => 'User Details',\n    'users_details_desc' => 'Set a display name and an email address for this user. The email address will be used for logging into the application.',\n    'users_details_desc_no_email' => 'Set a display name for this user so others can recognise them.',\n    'users_role' => 'User Roles',\n    'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.',\n    'users_password' => 'User Password',\n    'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',\n    'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.',\n    'users_send_invite_option' => 'Send user invite email',\n    'users_external_auth_id' => 'External Authentication ID',\n    'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.',\n    'users_password_warning' => 'Only fill the below if you would like to change the password for this user.',\n    'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.',\n    'users_delete' => 'Delete User',\n    'users_delete_named' => 'Delete user :userName',\n    'users_delete_warning' => 'This will fully delete this user with the name \\':userName\\' from the system.',\n    'users_delete_confirm' => 'Are you sure you want to delete this user?',\n    'users_migrate_ownership' => 'Migrate Ownership',\n    'users_migrate_ownership_desc' => 'Select a user here if you want another user to become the owner of all items currently owned by this user.',\n    'users_none_selected' => 'No user selected',\n    'users_edit' => 'Edit User',\n    'users_edit_profile' => 'Edit Profile',\n    'users_avatar' => 'User Avatar',\n    'users_avatar_desc' => 'Select an image to represent this user. This should be approx 256px square.',\n    'users_preferred_language' => 'Preferred Language',\n    'users_preferred_language_desc' => 'This option will change the language used for the user-interface of the application. This will not affect any user-created content.',\n    'users_social_accounts' => 'Social Accounts',\n    'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.',\n    'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not revoke previously authorized access. Revoke access from your profile settings on the connected social account.',\n    'users_social_connect' => 'Connect Account',\n    'users_social_disconnect' => 'Disconnect Account',\n    'users_social_status_connected' => 'Connected',\n    'users_social_status_disconnected' => 'Disconnected',\n    'users_social_connected' => ':socialAccount account was successfully attached to your profile.',\n    'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.',\n    'users_api_tokens' => 'API Tokens',\n    'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.',\n    'users_api_tokens_none' => 'No API tokens have been created for this user',\n    'users_api_tokens_create' => 'Create Token',\n    'users_api_tokens_expires' => 'Expires',\n    'users_api_tokens_docs' => 'API Documentation',\n    'users_mfa' => 'Multi-Factor Authentication',\n    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'users_mfa_x_methods' => ':count method configured|:count methods configured',\n    'users_mfa_configure' => 'Configure Methods',\n\n    // API Tokens\n    'user_api_token_create' => 'Create API Token',\n    'user_api_token_name' => 'Name',\n    'user_api_token_name_desc' => 'Give your token a readable name as a future reminder of its intended purpose.',\n    'user_api_token_expiry' => 'Expiry Date',\n    'user_api_token_expiry_desc' => 'Set a date at which this token expires. After this date, requests made using this token will no longer work. Leaving this field blank will set an expiry 100 years into the future.',\n    'user_api_token_create_secret_message' => 'Immediately after creating this token a \"Token ID\" & \"Token Secret\" will be generated and displayed. The secret will only be shown a single time so be sure to copy the value to somewhere safe and secure before proceeding.',\n    'user_api_token' => 'API Token',\n    'user_api_token_id' => 'Token ID',\n    'user_api_token_id_desc' => 'This is a non-editable system generated identifier for this token which will need to be provided in API requests.',\n    'user_api_token_secret' => 'Token Secret',\n    'user_api_token_secret_desc' => 'This is a system generated secret for this token which will need to be provided in API requests. This will only be displayed this one time so copy this value to somewhere safe and secure.',\n    'user_api_token_created' => 'Token created :timeAgo',\n    'user_api_token_updated' => 'Token updated :timeAgo',\n    'user_api_token_delete' => 'Delete Token',\n    'user_api_token_delete_warning' => 'This will fully delete this API token with the name \\':tokenName\\' from the system.',\n    'user_api_token_delete_confirm' => 'Are you sure you want to delete this API token?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',\n    'webhooks_x_trigger_events' => ':count trigger event|:count trigger events',\n    'webhooks_create' => 'Create New Webhook',\n    'webhooks_none_created' => 'No webhooks have yet been created.',\n    'webhooks_edit' => 'Edit Webhook',\n    'webhooks_save' => 'Save Webhook',\n    'webhooks_details' => 'Webhook Details',\n    'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',\n    'webhooks_events' => 'Webhook Events',\n    'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',\n    'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\\'t expose confidential content.',\n    'webhooks_events_all' => 'All system events',\n    'webhooks_name' => 'Webhook Name',\n    'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',\n    'webhooks_endpoint' => 'Webhook Endpoint',\n    'webhooks_active' => 'Webhook Active',\n    'webhook_events_table_header' => 'Events',\n    'webhooks_delete' => 'Delete Webhook',\n    'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \\':webhookName\\', from the system.',\n    'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?',\n    'webhooks_format_example' => 'Webhook Format Example',\n    'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The \"related_item\" and \"url\" properties are optional and will depend on the type of event triggered.',\n    'webhooks_status' => 'Webhook Status',\n    'webhooks_last_called' => 'Last Called:',\n    'webhooks_last_errored' => 'Last Errored:',\n    'webhooks_last_error_message' => 'Last Error Message:',\n\n    // Licensing\n    'licenses' => 'Licenses',\n    'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.',\n    'licenses_bookstack' => 'BookStack License',\n    'licenses_php' => 'PHP Library Licenses',\n    'licenses_js' => 'JavaScript Library Licenses',\n    'licenses_other' => 'Other Licenses',\n    'license_details' => 'License Details',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/bn/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => 'The :attribute must be accepted.',\n    'active_url'           => 'The :attribute is not a valid URL.',\n    'after'                => 'The :attribute must be a date after :date.',\n    'alpha'                => 'The :attribute may only contain letters.',\n    'alpha_dash'           => 'The :attribute may only contain letters, numbers, dashes and underscores.',\n    'alpha_num'            => 'The :attribute may only contain letters and numbers.',\n    'array'                => 'The :attribute must be an array.',\n    'backup_codes'         => 'The provided code is not valid or has already been used.',\n    'before'               => 'The :attribute must be a date before :date.',\n    'between'              => [\n        'numeric' => 'The :attribute must be between :min and :max.',\n        'file'    => 'The :attribute must be between :min and :max kilobytes.',\n        'string'  => 'The :attribute must be between :min and :max characters.',\n        'array'   => 'The :attribute must have between :min and :max items.',\n    ],\n    'boolean'              => 'The :attribute field must be true or false.',\n    'confirmed'            => 'The :attribute confirmation does not match.',\n    'date'                 => 'The :attribute is not a valid date.',\n    'date_format'          => 'The :attribute does not match the format :format.',\n    'different'            => 'The :attribute and :other must be different.',\n    'digits'               => 'The :attribute must be :digits digits.',\n    'digits_between'       => 'The :attribute must be between :min and :max digits.',\n    'email'                => 'The :attribute must be a valid email address.',\n    'ends_with' => 'The :attribute must end with one of the following: :values',\n    'file'                 => 'The :attribute must be provided as a valid file.',\n    'filled'               => 'The :attribute field is required.',\n    'gt'                   => [\n        'numeric' => 'The :attribute must be greater than :value.',\n        'file'    => 'The :attribute must be greater than :value kilobytes.',\n        'string'  => 'The :attribute must be greater than :value characters.',\n        'array'   => 'The :attribute must have more than :value items.',\n    ],\n    'gte'                  => [\n        'numeric' => 'The :attribute must be greater than or equal :value.',\n        'file'    => 'The :attribute must be greater than or equal :value kilobytes.',\n        'string'  => 'The :attribute must be greater than or equal :value characters.',\n        'array'   => 'The :attribute must have :value items or more.',\n    ],\n    'exists'               => 'The selected :attribute is invalid.',\n    'image'                => 'The :attribute must be an image.',\n    'image_extension'      => 'The :attribute must have a valid & supported image extension.',\n    'in'                   => 'The selected :attribute is invalid.',\n    'integer'              => 'The :attribute must be an integer.',\n    'ip'                   => 'The :attribute must be a valid IP address.',\n    'ipv4'                 => 'The :attribute must be a valid IPv4 address.',\n    'ipv6'                 => 'The :attribute must be a valid IPv6 address.',\n    'json'                 => 'The :attribute must be a valid JSON string.',\n    'lt'                   => [\n        'numeric' => 'The :attribute must be less than :value.',\n        'file'    => 'The :attribute must be less than :value kilobytes.',\n        'string'  => 'The :attribute must be less than :value characters.',\n        'array'   => 'The :attribute must have less than :value items.',\n    ],\n    'lte'                  => [\n        'numeric' => 'The :attribute must be less than or equal :value.',\n        'file'    => 'The :attribute must be less than or equal :value kilobytes.',\n        'string'  => 'The :attribute must be less than or equal :value characters.',\n        'array'   => 'The :attribute must not have more than :value items.',\n    ],\n    'max'                  => [\n        'numeric' => 'The :attribute may not be greater than :max.',\n        'file'    => 'The :attribute may not be greater than :max kilobytes.',\n        'string'  => 'The :attribute may not be greater than :max characters.',\n        'array'   => 'The :attribute may not have more than :max items.',\n    ],\n    'mimes'                => 'The :attribute must be a file of type: :values.',\n    'min'                  => [\n        'numeric' => 'The :attribute must be at least :min.',\n        'file'    => 'The :attribute must be at least :min kilobytes.',\n        'string'  => 'The :attribute must be at least :min characters.',\n        'array'   => 'The :attribute must have at least :min items.',\n    ],\n    'not_in'               => 'The selected :attribute is invalid.',\n    'not_regex'            => 'The :attribute format is invalid.',\n    'numeric'              => 'The :attribute must be a number.',\n    'regex'                => 'The :attribute format is invalid.',\n    'required'             => 'The :attribute field is required.',\n    'required_if'          => 'The :attribute field is required when :other is :value.',\n    'required_with'        => 'The :attribute field is required when :values is present.',\n    'required_with_all'    => 'The :attribute field is required when :values is present.',\n    'required_without'     => 'The :attribute field is required when :values is not present.',\n    'required_without_all' => 'The :attribute field is required when none of :values are present.',\n    'same'                 => 'The :attribute and :other must match.',\n    'safe_url'             => 'The provided link may not be safe.',\n    'size'                 => [\n        'numeric' => 'The :attribute must be :size.',\n        'file'    => 'The :attribute must be :size kilobytes.',\n        'string'  => 'The :attribute must be :size characters.',\n        'array'   => 'The :attribute must contain :size items.',\n    ],\n    'string'               => 'The :attribute must be a string.',\n    'timezone'             => 'The :attribute must be a valid zone.',\n    'totp'                 => 'The provided code is not valid or has expired.',\n    'unique'               => 'The :attribute has already been taken.',\n    'url'                  => 'The :attribute format is invalid.',\n    'uploaded'             => 'The file could not be uploaded. The server may not accept files of this size.',\n\n    'zip_file' => 'The :attribute needs to reference a file within the ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',\n    'zip_model_expected' => 'Data object expected but \":type\" found.',\n    'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Password confirmation required',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/bs/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'je kreirao/la stranicu',\n    'page_create_notification'    => 'Page successfully created',\n    'page_update'                 => 'je ažurirao/la stranicu',\n    'page_update_notification'    => 'Page successfully updated',\n    'page_delete'                 => 'je izbrisao/la stranicu',\n    'page_delete_notification'    => 'Page successfully deleted',\n    'page_restore'                => 'je vratio/la stranicu',\n    'page_restore_notification'   => 'Page successfully restored',\n    'page_move'                   => 'je premjestio/la stranicu',\n    'page_move_notification'      => 'Page successfully moved',\n\n    // Chapters\n    'chapter_create'              => 'je kreirao/la poglavlje',\n    'chapter_create_notification' => 'Chapter successfully created',\n    'chapter_update'              => 'je ažurirao/la poglavlje',\n    'chapter_update_notification' => 'Chapter successfully updated',\n    'chapter_delete'              => 'je izbrisao/la poglavlje',\n    'chapter_delete_notification' => 'Chapter successfully deleted',\n    'chapter_move'                => 'je premjestio/la poglavlje',\n    'chapter_move_notification' => 'Chapter successfully moved',\n\n    // Books\n    'book_create'                 => 'je kreirao/la knjigu',\n    'book_create_notification'    => 'Book successfully created',\n    'book_create_from_chapter'              => 'converted chapter to book',\n    'book_create_from_chapter_notification' => 'Chapter successfully converted to a book',\n    'book_update'                 => 'je ažurirao/la knjigu',\n    'book_update_notification'    => 'Book successfully updated',\n    'book_delete'                 => 'je izbrisao/la knjigu',\n    'book_delete_notification'    => 'Book successfully deleted',\n    'book_sort'                   => 'je sortirao/la knjigu',\n    'book_sort_notification'      => 'Book successfully re-sorted',\n\n    // Bookshelves\n    'bookshelf_create'            => 'created shelf',\n    'bookshelf_create_notification'    => 'Shelf successfully created',\n    'bookshelf_create_from_book'    => 'converted book to shelf',\n    'bookshelf_create_from_book_notification'    => 'Book successfully converted to a shelf',\n    'bookshelf_update'                 => 'updated shelf',\n    'bookshelf_update_notification'    => 'Shelf successfully updated',\n    'bookshelf_delete'                 => 'deleted shelf',\n    'bookshelf_delete_notification'    => 'Shelf successfully deleted',\n\n    // Revisions\n    'revision_restore' => 'restored revision',\n    'revision_delete' => 'deleted revision',\n    'revision_delete_notification' => 'Revision successfully deleted',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" je dodan u tvoje favorite',\n    'favourite_remove_notification' => '\":name\" je uklonjen iz tvojih favorita',\n\n    // Watching\n    'watch_update_level_notification' => 'Watch preferences successfully updated',\n\n    // Auth\n    'auth_login' => 'logged in',\n    'auth_register' => 'registered as new user',\n    'auth_password_reset_request' => 'requested user password reset',\n    'auth_password_reset_update' => 'reset user password',\n    'mfa_setup_method' => 'configured MFA method',\n    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',\n    'mfa_remove_method' => 'removed MFA method',\n    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',\n\n    // Settings\n    'settings_update' => 'updated settings',\n    'settings_update_notification' => 'Settings successfully updated',\n    'maintenance_action_run' => 'ran maintenance action',\n\n    // Webhooks\n    'webhook_create' => 'created webhook',\n    'webhook_create_notification' => 'Webhook successfully created',\n    'webhook_update' => 'updated webhook',\n    'webhook_update_notification' => 'Webhook successfully updated',\n    'webhook_delete' => 'deleted webhook',\n    'webhook_delete_notification' => 'Webhook successfully deleted',\n\n    // Imports\n    'import_create' => 'created import',\n    'import_create_notification' => 'Import successfully uploaded',\n    'import_run' => 'updated import',\n    'import_run_notification' => 'Content successfully imported',\n    'import_delete' => 'deleted import',\n    'import_delete_notification' => 'Import successfully deleted',\n\n    // Users\n    'user_create' => 'created user',\n    'user_create_notification' => 'User successfully created',\n    'user_update' => 'updated user',\n    'user_update_notification' => 'User successfully updated',\n    'user_delete' => 'deleted user',\n    'user_delete_notification' => 'User successfully removed',\n\n    // API Tokens\n    'api_token_create' => 'created API token',\n    'api_token_create_notification' => 'API token successfully created',\n    'api_token_update' => 'updated API token',\n    'api_token_update_notification' => 'API token successfully updated',\n    'api_token_delete' => 'deleted API token',\n    'api_token_delete_notification' => 'API token successfully deleted',\n\n    // Roles\n    'role_create' => 'created role',\n    'role_create_notification' => 'Role successfully created',\n    'role_update' => 'updated role',\n    'role_update_notification' => 'Role successfully updated',\n    'role_delete' => 'deleted role',\n    'role_delete_notification' => 'Role successfully deleted',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'emptied recycle bin',\n    'recycle_bin_restore' => 'restored from recycle bin',\n    'recycle_bin_destroy' => 'removed from recycle bin',\n\n    // Comments\n    'commented_on'                => 'je komentarisao/la na',\n    'comment_create'              => 'added comment',\n    'comment_update'              => 'updated comment',\n    'comment_delete'              => 'deleted comment',\n\n    // Sort Rules\n    'sort_rule_create' => 'created sort rule',\n    'sort_rule_create_notification' => 'Sort rule successfully created',\n    'sort_rule_update' => 'updated sort rule',\n    'sort_rule_update_notification' => 'Sort rule successfully updated',\n    'sort_rule_delete' => 'deleted sort rule',\n    'sort_rule_delete_notification' => 'Sort rule successfully deleted',\n\n    // Other\n    'permissions_update'          => 'je ažurirao/la dozvole',\n];\n"
  },
  {
    "path": "lang/bs/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Ovi pristupni podaci se ne slažu sa našom evidencijom.',\n    'throttle' => 'Preveliki broj pokušaja prijave. Molimo vas da pokušate ponovo za :seconds sekundi.',\n\n    // Login & Register\n    'sign_up' => 'Registruj se',\n    'log_in' => 'Prijavi se',\n    'log_in_with' => 'Prijavi se sa :socialDriver',\n    'sign_up_with' => 'Registruj se sa :socialDriver',\n    'logout' => 'Odjavi se',\n\n    'name' => 'Ime',\n    'username' => 'Korisničko ime',\n    'email' => 'E-mail',\n    'password' => 'Lozinka',\n    'password_confirm' => 'Potvrdi lozinku',\n    'password_hint' => 'Must be at least 8 characters',\n    'forgot_password' => 'Zaboravljena lozinka?',\n    'remember_me' => 'Zapamti me',\n    'ldap_email_hint' => 'Unesite e-mail koji će se koristiti za ovaj račun.',\n    'create_account' => 'Napravi račun',\n    'already_have_account' => 'Već imate račun?',\n    'dont_have_account' => 'Nemate korisnički račun?',\n    'social_login' => 'Prijava preko društvene mreže',\n    'social_registration' => 'Registracija pomoću društvene mreže',\n    'social_registration_text' => 'Registruj i prijavi se koristeći drugi servis.',\n\n    'register_thanks' => 'Hvala na registraciji!',\n    'register_confirm' => 'Provjerite vašu e-mail adresu i pritisnite dugme za potvrdu da bi dobili pristup :appName.',\n    'registrations_disabled' => 'Registracije su trenutno onemogućene',\n    'registration_email_domain_invalid' => 'Ta e-mail domena nema pristup ovoj aplikaciji',\n    'register_success' => 'Hvala na registraciji! Sada ste registrovani i prijavljeni.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Attempting Login',\n    'auto_init_starting_desc' => 'We\\'re contacting your authentication system to start the login process. If there\\'s no progress after 5 seconds you can try clicking the link below.',\n    'auto_init_start_link' => 'Proceed with authentication',\n\n    // Password Reset\n    'reset_password' => 'Resetuj Lozinku',\n    'reset_password_send_instructions' => 'Unesite vašu e-mail adresu ispod i na nju ćemo vam poslati e-mail sa linkom za promjenu lozinke.',\n    'reset_password_send_button' => 'Pošalji link za promjenu',\n    'reset_password_sent' => 'Link za promjenu lozinke će biti poslan na :email ako ta adresa postoji u sistemu.',\n    'reset_password_success' => 'Vaša lozinka je uspješno promijenjena.',\n    'email_reset_subject' => 'Resetujte vašu lozinku od :appName',\n    'email_reset_text' => 'Primate ovaj e-mail jer smo dobili zahtjev za promjenu lozinke za vaš račun.',\n    'email_reset_not_requested' => 'Ako niste zahtijevali promjenu lozinke ne trebate ništa više uraditi.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Potvrdite vaš e-mail na :appName',\n    'email_confirm_greeting' => 'Hvala na pristupanju :appName!',\n    'email_confirm_text' => 'Potvrdite vašu e-mail adresu pritiskom na dugme ispod:',\n    'email_confirm_action' => 'Potvrdi e-mail',\n    'email_confirm_send_error' => 'Potvrda e-maila je obavezna ali sistem nije mogao poslati e-mail. Kontaktirajte administratora da biste bili sigurni da je e-mail postavljen ispravno.',\n    'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',\n    'email_confirm_resent' => 'E-mail za potvrdu je ponovno poslan. Provjerite vaš e-mail.',\n    'email_confirm_thanks' => 'Thanks for confirming!',\n    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the \"Continue\" link below to proceed.',\n\n    'email_not_confirmed' => 'E-mail adresa nije potvrđena',\n    'email_not_confirmed_text' => 'Vaša e-mail adresa nije još potvrđena.',\n    'email_not_confirmed_click_link' => 'Kliknite na link u e-mailu koji vam je poslan nakon što ste se registrovali.',\n    'email_not_confirmed_resend' => 'Ako ne možete naći e-mail možete ponovno poslati e-mail za potvrdu tako što ćete ispuniti formu ispod.',\n    'email_not_confirmed_resend_button' => 'Ponovno pošalji e-mail za potvrdu',\n\n    // User Invite\n    'user_invite_email_subject' => 'Pozvani ste da se pridružite :appName!',\n    'user_invite_email_greeting' => 'Račun je napravljen za vas na :appName.',\n    'user_invite_email_text' => 'Pritisnite dugme ispod da niste postavili lozinku vašeg računa i tako dobili pristup:',\n    'user_invite_email_action' => 'Postavi lozinku računa',\n    'user_invite_page_welcome' => 'Dobrodošli na :appName!',\n    'user_invite_page_text' => 'Da biste završili vaš račun i dobili pristup morate postaviti lozinku koju ćete koristiti da se prijavite na :appName tokom budućih posjeta.',\n    'user_invite_page_confirm_button' => 'Potvrdi lozinku',\n    'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Setup Multi-Factor Authentication',\n    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'mfa_setup_configured' => 'Already configured',\n    'mfa_setup_reconfigure' => 'Reconfigure',\n    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',\n    'mfa_setup_action' => 'Setup',\n    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',\n    'mfa_option_totp_title' => 'Mobile App',\n    'mfa_option_totp_desc' => 'To use multi-factor authentication you\\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Backup Codes',\n    'mfa_option_backup_codes_desc' => 'Generates a set of one-time-use backup codes which you\\'ll enter on login to verify your identity. Make sure to store these in a safe & secure place.',\n    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',\n    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',\n    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\\'ll be able to use one of the codes as a second authentication mechanism.',\n    'mfa_gen_backup_codes_download' => 'Download Codes',\n    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',\n    'mfa_gen_totp_title' => 'Mobile App Setup',\n    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',\n    'mfa_gen_totp_verify_setup' => 'Verify Setup',\n    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',\n    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',\n    'mfa_verify_access' => 'Verify Access',\n    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\\'re granted access. Verify using one of your configured methods to continue.',\n    'mfa_verify_no_methods' => 'No Methods Configured',\n    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\\'ll need to set up at least one method before you gain access.',\n    'mfa_verify_use_totp' => 'Verify using a mobile app',\n    'mfa_verify_use_backup_codes' => 'Verify using a backup code',\n    'mfa_verify_backup_code' => 'Backup Code',\n    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',\n    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',\n    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',\n    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',\n];\n"
  },
  {
    "path": "lang/bs/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Otkaži',\n    'close' => 'Close',\n    'confirm' => 'Potvrdi',\n    'back' => 'Nazad',\n    'save' => 'Spremi',\n    'continue' => 'Nastavi',\n    'select' => 'Odaberi',\n    'toggle_all' => 'Prebaci sve',\n    'more' => 'Više',\n\n    // Form Labels\n    'name' => 'Ime',\n    'description' => 'Opis',\n    'role' => 'Uloga',\n    'cover_image' => 'Naslovna slika',\n    'cover_image_description' => 'This image should be approximately 440x250px although it will be flexibly scaled & cropped to fit the user interface in different scenarios as required, so actual dimensions for display will differ.',\n\n    // Actions\n    'actions' => 'Akcije',\n    'view' => 'Prikaz',\n    'view_all' => 'Prikaži sve',\n    'new' => 'New',\n    'create' => 'Kreiraj',\n    'update' => 'Ažuriraj',\n    'edit' => 'Uredi',\n    'archive' => 'Archive',\n    'unarchive' => 'Un-Archive',\n    'sort' => 'Sortiraj',\n    'move' => 'Pomjeri',\n    'copy' => 'Kopiraj',\n    'reply' => 'Odgovori',\n    'delete' => 'Izbriši',\n    'delete_confirm' => 'Potvrdi brisanje',\n    'search' => 'Traži',\n    'search_clear' => 'Očisti pretragu',\n    'reset' => 'Resetuj',\n    'remove' => 'Ukloni',\n    'add' => 'Dodaj',\n    'configure' => 'Configure',\n    'manage' => 'Manage',\n    'fullscreen' => 'Prikaz preko čitavog ekrana',\n    'favourite' => 'Favorit',\n    'unfavourite' => 'Ukloni favorit',\n    'next' => 'Sljedeće',\n    'previous' => 'Prethodno',\n    'filter_active' => 'Active Filter:',\n    'filter_clear' => 'Clear Filter',\n    'download' => 'Download',\n    'open_in_tab' => 'Open in Tab',\n    'open' => 'Open',\n\n    // Sort Options\n    'sort_options' => 'Opcije sortiranja',\n    'sort_direction_toggle' => 'Prebacivanje smjera sortiranja',\n    'sort_ascending' => 'Sortiraj uzlazno',\n    'sort_descending' => 'Sortiraj silazno',\n    'sort_name' => 'Ime',\n    'sort_default' => 'Početne postavke',\n    'sort_created_at' => 'Datum kreiranja',\n    'sort_updated_at' => 'Datum ažuriranja',\n\n    // Misc\n    'deleted_user' => 'Obrisani korisnik',\n    'no_activity' => 'Nema aktivnosti za prikazivanje',\n    'no_items' => 'Nema dostupnih stavki',\n    'back_to_top' => 'Povratak na vrh',\n    'skip_to_main_content' => 'Idi odmah na glavni sadržaj',\n    'toggle_details' => 'Vidi detalje',\n    'toggle_thumbnails' => 'Vidi prikaze slika',\n    'details' => 'Detalji',\n    'grid_view' => 'Prikaz rešetke',\n    'list_view' => 'Prikaz liste',\n    'default' => 'Početne postavke',\n    'breadcrumb' => 'Navigacijske stavke',\n    'status' => 'Status',\n    'status_active' => 'Active',\n    'status_inactive' => 'Inactive',\n    'never' => 'Never',\n    'none' => 'None',\n\n    // Header\n    'homepage' => 'Homepage',\n    'header_menu_expand' => 'Otvori meni u zaglavlju',\n    'profile_menu' => 'Meni profila',\n    'view_profile' => 'Pogledaj profil',\n    'edit_profile' => 'Izmjeni profil',\n    'dark_mode' => 'Tamni način rada',\n    'light_mode' => 'Svijetli način rada',\n    'global_search' => 'Global Search',\n\n    // Layout tabs\n    'tab_info' => 'Informacije',\n    'tab_info_label' => 'Kartica: Prikaži dodatnu informaciju',\n    'tab_content' => 'Sadržaj',\n    'tab_content_label' => 'Kartica: Prikaži glavni sadržaj',\n\n    // Email Content\n    'email_action_help' => 'Ukoliko imate poteškoća sa pritiskom na \":actionText\" dugme, kopirajte i zaljepite URL koji se nalazi ispod u vaš web pretraživač:',\n    'email_rights' => 'Sva prava pridržana',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Pravila o privatnosti',\n    'terms_of_service' => 'Uslovi korištenja',\n\n    // OpenSearch\n    'opensearch_description' => 'Search :appName',\n];\n"
  },
  {
    "path": "lang/bs/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Biraj sliku',\n    'image_list' => 'Image List',\n    'image_details' => 'Image Details',\n    'image_upload' => 'Upload Image',\n    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',\n    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the \"Upload Image\" button above.',\n    'image_all' => 'Sve',\n    'image_all_title' => 'Pogledaj sve slike',\n    'image_book_title' => 'Pogledaj slike prenesene u ovu knjigu',\n    'image_page_title' => 'Pogledaj slike prenesene na ovu stranicu',\n    'image_search_hint' => 'Traži po nazivu slike',\n    'image_uploaded' => 'Preneseno :uploadedDate',\n    'image_uploaded_by' => 'Uploaded by :userName',\n    'image_uploaded_to' => 'Uploaded to :pageLink',\n    'image_updated' => 'Updated :updateDate',\n    'image_load_more' => 'Učitaj još',\n    'image_image_name' => 'Naziv slike',\n    'image_delete_used' => 'Ova slika se koristi na stranicama prikazanim ispod.',\n    'image_delete_confirm_text' => 'Jeste li sigurni da želite obrisati ovu sliku?',\n    'image_select_image' => 'Odaberi sliku',\n    'image_dropzone' => 'Ostavi slike ili pritisnite ovdje da ih prenesete',\n    'image_dropzone_drop' => 'Drop images here to upload',\n    'images_deleted' => 'Slike su izbrisane',\n    'image_preview' => 'Pregled Slike',\n    'image_upload_success' => 'Slika uspješno učitana',\n    'image_update_success' => 'Detalji slike uspješno ažurirani',\n    'image_delete_success' => 'Slika uspješno izbrisana',\n    'image_replace' => 'Replace Image',\n    'image_replace_success' => 'Image file successfully updated',\n    'image_rebuild_thumbs' => 'Regenerate Size Variations',\n    'image_rebuild_thumbs_success' => 'Image size variations successfully rebuilt!',\n\n    // Code Editor\n    'code_editor' => 'Uredi Kod',\n    'code_language' => 'Jezik koda',\n    'code_content' => 'Sadržaj Koda',\n    'code_session_history' => 'Historija Sesije',\n    'code_save' => 'Snimi Kod',\n];\n"
  },
  {
    "path": "lang/bs/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'General',\n    'advanced' => 'Advanced',\n    'none' => 'None',\n    'cancel' => 'Cancel',\n    'save' => 'Save',\n    'close' => 'Close',\n    'apply' => 'Apply',\n    'undo' => 'Undo',\n    'redo' => 'Redo',\n    'left' => 'Left',\n    'center' => 'Center',\n    'right' => 'Right',\n    'top' => 'Top',\n    'middle' => 'Middle',\n    'bottom' => 'Bottom',\n    'width' => 'Width',\n    'height' => 'Height',\n    'More' => 'More',\n    'select' => 'Select...',\n\n    // Toolbar\n    'formats' => 'Formats',\n    'header_large' => 'Large Header',\n    'header_medium' => 'Medium Header',\n    'header_small' => 'Small Header',\n    'header_tiny' => 'Tiny Header',\n    'paragraph' => 'Paragraph',\n    'blockquote' => 'Blockquote',\n    'inline_code' => 'Inline code',\n    'callouts' => 'Callouts',\n    'callout_information' => 'Information',\n    'callout_success' => 'Success',\n    'callout_warning' => 'Warning',\n    'callout_danger' => 'Danger',\n    'bold' => 'Bold',\n    'italic' => 'Italic',\n    'underline' => 'Underline',\n    'strikethrough' => 'Strikethrough',\n    'superscript' => 'Superscript',\n    'subscript' => 'Subscript',\n    'text_color' => 'Text color',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Custom color',\n    'remove_color' => 'Remove color',\n    'background_color' => 'Background color',\n    'align_left' => 'Align left',\n    'align_center' => 'Align center',\n    'align_right' => 'Align right',\n    'align_justify' => 'Justify',\n    'list_bullet' => 'Bullet list',\n    'list_numbered' => 'Numbered list',\n    'list_task' => 'Task list',\n    'indent_increase' => 'Increase indent',\n    'indent_decrease' => 'Decrease indent',\n    'table' => 'Table',\n    'insert_image' => 'Insert image',\n    'insert_image_title' => 'Insert/Edit Image',\n    'insert_link' => 'Insert/edit link',\n    'insert_link_title' => 'Insert/Edit Link',\n    'insert_horizontal_line' => 'Insert horizontal line',\n    'insert_code_block' => 'Insert code block',\n    'edit_code_block' => 'Edit code block',\n    'insert_drawing' => 'Insert/edit drawing',\n    'drawing_manager' => 'Drawing manager',\n    'insert_media' => 'Insert/edit media',\n    'insert_media_title' => 'Insert/Edit Media',\n    'clear_formatting' => 'Clear formatting',\n    'source_code' => 'Source code',\n    'source_code_title' => 'Source Code',\n    'fullscreen' => 'Fullscreen',\n    'image_options' => 'Image options',\n\n    // Tables\n    'table_properties' => 'Table properties',\n    'table_properties_title' => 'Table Properties',\n    'delete_table' => 'Delete table',\n    'table_clear_formatting' => 'Clear table formatting',\n    'resize_to_contents' => 'Resize to contents',\n    'row_header' => 'Row header',\n    'insert_row_before' => 'Insert row before',\n    'insert_row_after' => 'Insert row after',\n    'delete_row' => 'Delete row',\n    'insert_column_before' => 'Insert column before',\n    'insert_column_after' => 'Insert column after',\n    'delete_column' => 'Delete column',\n    'table_cell' => 'Cell',\n    'table_row' => 'Row',\n    'table_column' => 'Column',\n    'cell_properties' => 'Cell properties',\n    'cell_properties_title' => 'Cell Properties',\n    'cell_type' => 'Cell type',\n    'cell_type_cell' => 'Cell',\n    'cell_scope' => 'Scope',\n    'cell_type_header' => 'Header cell',\n    'merge_cells' => 'Merge cells',\n    'split_cell' => 'Split cell',\n    'table_row_group' => 'Row Group',\n    'table_column_group' => 'Column Group',\n    'horizontal_align' => 'Horizontal align',\n    'vertical_align' => 'Vertical align',\n    'border_width' => 'Border width',\n    'border_style' => 'Border style',\n    'border_color' => 'Border color',\n    'row_properties' => 'Row properties',\n    'row_properties_title' => 'Row Properties',\n    'cut_row' => 'Cut row',\n    'copy_row' => 'Copy row',\n    'paste_row_before' => 'Paste row before',\n    'paste_row_after' => 'Paste row after',\n    'row_type' => 'Row type',\n    'row_type_header' => 'Header',\n    'row_type_body' => 'Body',\n    'row_type_footer' => 'Footer',\n    'alignment' => 'Alignment',\n    'cut_column' => 'Cut column',\n    'copy_column' => 'Copy column',\n    'paste_column_before' => 'Paste column before',\n    'paste_column_after' => 'Paste column after',\n    'cell_padding' => 'Cell padding',\n    'cell_spacing' => 'Cell spacing',\n    'caption' => 'Caption',\n    'show_caption' => 'Show caption',\n    'constrain' => 'Constrain proportions',\n    'cell_border_solid' => 'Solid',\n    'cell_border_dotted' => 'Dotted',\n    'cell_border_dashed' => 'Dashed',\n    'cell_border_double' => 'Double',\n    'cell_border_groove' => 'Groove',\n    'cell_border_ridge' => 'Ridge',\n    'cell_border_inset' => 'Inset',\n    'cell_border_outset' => 'Outset',\n    'cell_border_none' => 'None',\n    'cell_border_hidden' => 'Hidden',\n\n    // Images, links, details/summary & embed\n    'source' => 'Source',\n    'alt_desc' => 'Alternative description',\n    'embed' => 'Embed',\n    'paste_embed' => 'Paste your embed code below:',\n    'url' => 'URL',\n    'text_to_display' => 'Text to display',\n    'title' => 'Title',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Open link',\n    'open_link_in' => 'Open link in...',\n    'open_link_current' => 'Current window',\n    'open_link_new' => 'New window',\n    'remove_link' => 'Remove link',\n    'insert_collapsible' => 'Insert collapsible block',\n    'collapsible_unwrap' => 'Unwrap',\n    'edit_label' => 'Edit label',\n    'toggle_open_closed' => 'Toggle open/closed',\n    'collapsible_edit' => 'Edit collapsible block',\n    'toggle_label' => 'Toggle label',\n\n    // About view\n    'about' => 'About the editor',\n    'about_title' => 'About the WYSIWYG Editor',\n    'editor_license' => 'Editor License & Copyright',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under the MIT license.',\n    'editor_tiny_license_link' => 'The copyright and license details of TinyMCE can be found here.',\n    'save_continue' => 'Save Page & Continue',\n    'callouts_cycle' => '(Keep pressing to toggle through types)',\n    'link_selector' => 'Link to content',\n    'shortcuts' => 'Shortcuts',\n    'shortcut' => 'Shortcut',\n    'shortcuts_intro' => 'The following shortcuts are available in the editor:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Description',\n];\n"
  },
  {
    "path": "lang/bs/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Nedavno napravljen',\n    'recently_created_pages' => 'Nedavno napravljene stranice',\n    'recently_updated_pages' => 'Nedavno ažurirane stranice',\n    'recently_created_chapters' => 'Nedavno napravljena poglavlja',\n    'recently_created_books' => 'Nedavno napravljene knjige',\n    'recently_created_shelves' => 'Nedavno napravljene police',\n    'recently_update' => 'Nedavno ažurirana',\n    'recently_viewed' => 'Nedavno pogledana',\n    'recent_activity' => 'Nedavna aktivnost',\n    'create_now' => 'Napravi jednu sada',\n    'revisions' => 'Promjene',\n    'meta_revision' => 'Promjena #:revisionCount',\n    'meta_created' => 'Napravljena :timeLength',\n    'meta_created_name' => 'Napravljena :timeLength od :user',\n    'meta_updated' => 'Ažurirana :timeLength',\n    'meta_updated_name' => 'Ažurirana :timeLength od :user',\n    'meta_owned_name' => 'Vlasnik je :user',\n    'meta_reference_count' => 'Referenced by :count item|Referenced by :count items',\n    'entity_select' => 'Odaberi entitet',\n    'entity_select_lack_permission' => 'You don\\'t have the required permissions to select this item',\n    'images' => 'Slike',\n    'my_recent_drafts' => 'Moje nedavne skice',\n    'my_recently_viewed' => 'Moji nedavni pregledi',\n    'my_most_viewed_favourites' => 'My Most Viewed Favourites',\n    'my_favourites' => 'My Favourites',\n    'no_pages_viewed' => 'Niste pogledali nijednu stranicu',\n    'no_pages_recently_created' => 'Nijedna stranica nije napravljena nedavno',\n    'no_pages_recently_updated' => 'Niijedna stranica nije ažurirana nedavno',\n    'export' => 'Izvezi',\n    'export_html' => 'Sadržani web fajl',\n    'export_pdf' => 'PDF fajl',\n    'export_text' => 'Plain Text fajl',\n    'export_md' => 'Markdown File',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Default Page Template',\n    'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',\n    'default_template_select' => 'Select a template page',\n    'import' => 'Import',\n    'import_validate' => 'Validate Import',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'Select ZIP file to upload',\n    'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',\n    'import_pending' => 'Pending Imports',\n    'import_pending_none' => 'No imports have been started.',\n    'import_continue' => 'Continue Import',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Run Import',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Uploaded by',\n    'import_location' => 'Import Location',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'Import Errors',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Dozvole',\n    'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.',\n    'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.',\n    'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.',\n    'permissions_save' => 'Snimi dozvole',\n    'permissions_owner' => 'Vlasnik',\n    'permissions_role_everyone_else' => 'Everyone Else',\n    'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',\n    'permissions_role_override' => 'Override permissions for role',\n    'permissions_inherit_defaults' => 'Inherit defaults',\n\n    // Search\n    'search_results' => 'Rezultati pretrage',\n    'search_total_results_found' => ':count rezultata je nađeno|:count ukupno rezultata je nađeno',\n    'search_clear' => 'Očisti pretragu',\n    'search_no_pages' => 'Nijedna stranica nije nađena',\n    'search_for_term' => 'Traži :term',\n    'search_more' => 'Više rezultata',\n    'search_advanced' => 'Napredna pretraga',\n    'search_terms' => 'Pojmovi za pretragu',\n    'search_content_type' => 'Vrsta sadržaja',\n    'search_exact_matches' => 'Tačna podudaranja',\n    'search_tags' => 'Pretraga oznaka',\n    'search_options' => 'Opcije',\n    'search_viewed_by_me' => 'Ja sam pogledao/la',\n    'search_not_viewed_by_me' => 'Nisam pogledao/la',\n    'search_permissions_set' => 'Dozvole',\n    'search_created_by_me' => 'Ja sam napravio/la',\n    'search_updated_by_me' => 'Ja sam ažurirao/la',\n    'search_owned_by_me' => 'Owned by me',\n    'search_date_options' => 'Opcije datuma',\n    'search_updated_before' => 'Ažurirano prije',\n    'search_updated_after' => 'Ažurirano nakon',\n    'search_created_before' => 'Kreirano prije',\n    'search_created_after' => 'Kreirano nakon',\n    'search_set_date' => 'Postavi datum',\n    'search_update' => 'Ažuriraj pretragu',\n\n    // Shelves\n    'shelf' => 'Polica',\n    'shelves' => 'Police',\n    'x_shelves' => ':count Polica|:count Police',\n    'shelves_empty' => 'Niti jedna polica nije kreirana',\n    'shelves_create' => 'Kreiraj novu policu',\n    'shelves_popular' => 'Popularne police',\n    'shelves_new' => 'Nove police',\n    'shelves_new_action' => 'Nova polica',\n    'shelves_popular_empty' => 'Najpopularnije police će se pojaviti ovdje.',\n    'shelves_new_empty' => 'Najnovije police će se pojaviti ovdje.',\n    'shelves_save' => 'Spremi policu',\n    'shelves_books' => 'Knjige na ovoj polici',\n    'shelves_add_books' => 'Dodaj knjige na ovu policu',\n    'shelves_drag_books' => 'Drag books below to add them to this shelf',\n    'shelves_empty_contents' => 'Ova polica nema knjiga koje su postavljene na nju',\n    'shelves_edit_and_assign' => 'Uredi policu da bi dodao/la knjige',\n    'shelves_edit_named' => 'Edit Shelf :name',\n    'shelves_edit' => 'Edit Shelf',\n    'shelves_delete' => 'Delete Shelf',\n    'shelves_delete_named' => 'Delete Shelf :name',\n    'shelves_delete_explain' => \"This will delete the shelf with the name ':name'. Contained books will not be deleted.\",\n    'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',\n    'shelves_permissions' => 'Shelf Permissions',\n    'shelves_permissions_updated' => 'Shelf Permissions Updated',\n    'shelves_permissions_active' => 'Shelf Permissions Active',\n    'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',\n    'shelves_permissions_create' => 'Shelf create permissions are only used for copying permissions to child books using the action below. They do not control the ability to create books.',\n    'shelves_copy_permissions_to_books' => 'Copy Permissions to Books',\n    'shelves_copy_permissions' => 'Copy Permissions',\n    'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',\n    'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',\n\n    // Books\n    'book' => 'Book',\n    'books' => 'Books',\n    'x_books' => ':count Book|:count Books',\n    'books_empty' => 'No books have been created',\n    'books_popular' => 'Popular Books',\n    'books_recent' => 'Recent Books',\n    'books_new' => 'New Books',\n    'books_new_action' => 'New Book',\n    'books_popular_empty' => 'The most popular books will appear here.',\n    'books_new_empty' => 'The most recently created books will appear here.',\n    'books_create' => 'Create New Book',\n    'books_delete' => 'Delete Book',\n    'books_delete_named' => 'Delete Book :bookName',\n    'books_delete_explain' => 'Ovo će izbrisati knjigu naziva \\':bookName\\'. Sve stranice i poglavlja će biti uklonjene.',\n    'books_delete_confirmation' => 'Jeste li sigurni da želite izbrisati ovu knjigu?',\n    'books_edit' => 'Uredi knjigu',\n    'books_edit_named' => 'Uredi knjigu :bookName',\n    'books_form_book_name' => 'Naziv knjige',\n    'books_save' => 'Spremi knjigu',\n    'books_permissions' => 'Dozvole knjige',\n    'books_permissions_updated' => 'Dozvole knjige su ažurirane',\n    'books_empty_contents' => 'Za ovu knjigu nisu napravljene ni stranice ni poglavlja.',\n    'books_empty_create_page' => 'Napravi novu stranicu',\n    'books_empty_sort_current_book' => 'Sortiraj trenutnu knjigu',\n    'books_empty_add_chapter' => 'Dodaj poglavlje',\n    'books_permissions_active' => 'Dozvole za knjigu su aktivne',\n    'books_search_this' => 'Pretraži ovu knjigu',\n    'books_navigation' => 'Navigacija knjige',\n    'books_sort' => 'Sortiraj sadržaj knjige',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Sortiraj knjigu :bookName',\n    'books_sort_name' => 'Sortiraj po imenu',\n    'books_sort_created' => 'Sortiraj po datumu kreiranja',\n    'books_sort_updated' => 'Sortiraj po datumu ažuriranja',\n    'books_sort_chapters_first' => 'Poglavlja prva',\n    'books_sort_chapters_last' => 'Poglavlja zadnja',\n    'books_sort_show_other' => 'Prikaži druge knjige',\n    'books_sort_save' => 'Spremi trenutni poredak',\n    'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.',\n    'books_sort_move_up' => 'Move Up',\n    'books_sort_move_down' => 'Move Down',\n    'books_sort_move_prev_book' => 'Move to Previous Book',\n    'books_sort_move_next_book' => 'Move to Next Book',\n    'books_sort_move_prev_chapter' => 'Move Into Previous Chapter',\n    'books_sort_move_next_chapter' => 'Move Into Next Chapter',\n    'books_sort_move_book_start' => 'Move to Start of Book',\n    'books_sort_move_book_end' => 'Move to End of Book',\n    'books_sort_move_before_chapter' => 'Move to Before Chapter',\n    'books_sort_move_after_chapter' => 'Move to After Chapter',\n    'books_copy' => 'Copy Book',\n    'books_copy_success' => 'Book successfully copied',\n\n    // Chapters\n    'chapter' => 'Poglavlje',\n    'chapters' => 'Poglavlja',\n    'x_chapters' => ':count Poglavlje|:count Poglavlja',\n    'chapters_popular' => 'Popularna poglavlja',\n    'chapters_new' => 'Novo poglavlje',\n    'chapters_create' => 'Napravi novo poglavlje',\n    'chapters_delete' => 'Izbriši poglavlje',\n    'chapters_delete_named' => 'Izbriši poglavlje :chapterName',\n    'chapters_delete_explain' => 'Ovo će izbrisati poglavlje naziva \\':chapterName\\'. Sve stranice koje postoje u ovom poglavlju će također biti izbrisane.',\n    'chapters_delete_confirm' => 'Jeste li sigurni da želite izbrisati ovo poglavlje?',\n    'chapters_edit' => 'Uredi poglavlje',\n    'chapters_edit_named' => 'Uredi poglavlje :chapterName',\n    'chapters_save' => 'Spremi poglavlje',\n    'chapters_move' => 'Premjesti poglavlje',\n    'chapters_move_named' => 'Premjesti poglavlje :chapterName',\n    'chapters_copy' => 'Copy Chapter',\n    'chapters_copy_success' => 'Chapter successfully copied',\n    'chapters_permissions' => 'Dozvole poglavlja',\n    'chapters_empty' => 'U ovom poglavlju trenutno nema stranica.',\n    'chapters_permissions_active' => 'Dozvole za poglavlje su aktivne',\n    'chapters_permissions_success' => 'Dozvole za poglavlje su ažurirane',\n    'chapters_search_this' => 'Pretražuj ovo poglavlje',\n    'chapter_sort_book' => 'Sort Book',\n\n    // Pages\n    'page' => 'Stranica',\n    'pages' => 'Stranice',\n    'x_pages' => ':count Stranica|:count Stranice',\n    'pages_popular' => 'Popularne stranice',\n    'pages_new' => 'Nova stranica',\n    'pages_attachments' => 'Attachments',\n    'pages_navigation' => 'Page Navigation',\n    'pages_delete' => 'Delete Page',\n    'pages_delete_named' => 'Delete Page :pageName',\n    'pages_delete_draft_named' => 'Delete Draft Page :pageName',\n    'pages_delete_draft' => 'Delete Draft Page',\n    'pages_delete_success' => 'Page deleted',\n    'pages_delete_draft_success' => 'Draft page deleted',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'Are you sure you want to delete this page?',\n    'pages_delete_draft_confirm' => 'Are you sure you want to delete this draft page?',\n    'pages_editing_named' => 'Editing Page :pageName',\n    'pages_edit_draft_options' => 'Draft Options',\n    'pages_edit_save_draft' => 'Save Draft',\n    'pages_edit_draft' => 'Edit Page Draft',\n    'pages_editing_draft' => 'Editing Draft',\n    'pages_editing_page' => 'Editing Page',\n    'pages_edit_draft_save_at' => 'Draft saved at ',\n    'pages_edit_delete_draft' => 'Delete Draft',\n    'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.',\n    'pages_edit_discard_draft' => 'Discard Draft',\n    'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',\n    'pages_edit_switch_to_markdown_clean' => '(Clean Content)',\n    'pages_edit_switch_to_markdown_stable' => '(Stable Content)',\n    'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Set Changelog',\n    'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\\'ve made',\n    'pages_edit_enter_changelog' => 'Enter Changelog',\n    'pages_editor_switch_title' => 'Switch Editor',\n    'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',\n    'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',\n    'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',\n    'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',\n    'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\\'t persist across this change.',\n    'pages_save' => 'Save Page',\n    'pages_title' => 'Page Title',\n    'pages_name' => 'Page Name',\n    'pages_md_editor' => 'Editor',\n    'pages_md_preview' => 'Preview',\n    'pages_md_insert_image' => 'Insert Image',\n    'pages_md_insert_link' => 'Insert Entity Link',\n    'pages_md_insert_drawing' => 'Insert Drawing',\n    'pages_md_show_preview' => 'Show preview',\n    'pages_md_sync_scroll' => 'Sync preview scroll',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Unsaved Drawing Found',\n    'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',\n    'pages_not_in_chapter' => 'Page is not in a chapter',\n    'pages_move' => 'Move Page',\n    'pages_copy' => 'Copy Page',\n    'pages_copy_desination' => 'Copy Destination',\n    'pages_copy_success' => 'Page successfully copied',\n    'pages_permissions' => 'Page Permissions',\n    'pages_permissions_success' => 'Page permissions updated',\n    'pages_revision' => 'Revision',\n    'pages_revisions' => 'Page Revisions',\n    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',\n    'pages_revisions_named' => 'Page Revisions for :pageName',\n    'pages_revision_named' => 'Page Revision for :pageName',\n    'pages_revision_restored_from' => 'Restored from #:id; :summary',\n    'pages_revisions_created_by' => 'Created By',\n    'pages_revisions_date' => 'Revision Date',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Revision Number',\n    'pages_revisions_numbered' => 'Revision #:id',\n    'pages_revisions_numbered_changes' => 'Revision #:id Changes',\n    'pages_revisions_editor' => 'Editor Type',\n    'pages_revisions_changelog' => 'Changelog',\n    'pages_revisions_changes' => 'Changes',\n    'pages_revisions_current' => 'Trenutna verzija',\n    'pages_revisions_preview' => 'Pregled',\n    'pages_revisions_restore' => 'Vrati',\n    'pages_revisions_none' => 'Ova stranica nema promjena',\n    'pages_copy_link' => 'Iskopiraj link',\n    'pages_edit_content_link' => 'Jump to section in editor',\n    'pages_pointer_enter_mode' => 'Enter section select mode',\n    'pages_pointer_label' => 'Page Section Options',\n    'pages_pointer_permalink' => 'Page Section Permalink',\n    'pages_pointer_include_tag' => 'Page Section Include Tag',\n    'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag',\n    'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink',\n    'pages_permissions_active' => 'Dozvole za stranicu su aktivne',\n    'pages_initial_revision' => 'Prvo izdavanje',\n    'pages_references_update_revision' => 'System auto-update of internal links',\n    'pages_initial_name' => 'Nova stranica',\n    'pages_editing_draft_notification' => 'Trenutno uređujete skicu koja je posljednji put snimljena :timeDiff.',\n    'pages_draft_edited_notification' => 'Ova stranica je ažurirana nakon tog vremena. Preporučujemo da odbacite ovu skicu.',\n    'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count korisnika je počelo sa uređivanjem ove stranice',\n        'start_b' => ':userName je počeo/la sa uređivanjem ove stranice',\n        'time_a' => 'od kada je stranica posljednji put ažurirana',\n        'time_b' => 'u posljednjih :minCount minuta',\n        'message' => ':start :time. Pazite da jedni drugima ne prepišete promjene!',\n    ],\n    'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',\n    'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',\n    'pages_specific' => 'Specifična stranica',\n    'pages_is_template' => 'Predložak stranice',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Toggle Sidebar',\n    'page_tags' => 'Oznake stranice',\n    'chapter_tags' => 'Oznake poglavlja',\n    'book_tags' => 'Oznake knjige',\n    'shelf_tags' => 'Oznake police',\n    'tag' => 'Oznaka',\n    'tags' =>  'Oznake',\n    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',\n    'tag_name' =>  'Naziv oznake',\n    'tag_value' => 'Vrijednost oznake (nije obavezno)',\n    'tags_explain' => \"Dodaj nekoliko oznaka da bi sadržaj bio bolje kategorisan. \\n Možeš dodati vrijednost oznaci za dublju organizaciju.\",\n    'tags_add' => 'Dodaj još jednu oznaku',\n    'tags_remove' => 'Ukloni ovu oznaku',\n    'tags_usages' => 'Total tag usages',\n    'tags_assigned_pages' => 'Assigned to Pages',\n    'tags_assigned_chapters' => 'Assigned to Chapters',\n    'tags_assigned_books' => 'Assigned to Books',\n    'tags_assigned_shelves' => 'Assigned to Shelves',\n    'tags_x_unique_values' => ':count unique values',\n    'tags_all_values' => 'All values',\n    'tags_view_tags' => 'View Tags',\n    'tags_view_existing_tags' => 'View existing tags',\n    'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',\n    'attachments' => 'Prilozi',\n    'attachments_explain' => 'Učitajte fajlove ili priložite poveznice da bi ih prikazali na stranici. Oni su onda vidljivi u navigaciji sa strane.',\n    'attachments_explain_instant_save' => 'Sve promjene se snimaju odmah.',\n    'attachments_upload' => 'Učitaj fajl',\n    'attachments_link' => 'Zakači link',\n    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',\n    'attachments_set_link' => 'Postavi link',\n    'attachments_delete' => 'Jeste li sigurni da želite obrisati ovaj prilog?',\n    'attachments_dropzone' => 'Drop files here to upload',\n    'attachments_no_files' => 'Niti jedan fajl nije prenesen',\n    'attachments_explain_link' => 'Možete zakačiti link ako ne želite učitati fajl. To može biti link druge stranice ili link za fajl u oblaku.',\n    'attachments_link_name' => 'Naziv linka',\n    'attachment_link' => 'Link poveznice',\n    'attachments_link_url' => 'Link do fajla',\n    'attachments_link_url_hint' => 'Url stranice ili fajla',\n    'attach' => 'Zakači',\n    'attachments_insert_link' => 'Dodaj priloženi link na stranicu',\n    'attachments_edit_file' => 'Uredi fajl',\n    'attachments_edit_file_name' => 'Naziv fajla',\n    'attachments_edit_drop_upload' => 'Spusti fajlove ili pritisni ovdje da učitaš i prepišeš',\n    'attachments_order_updated' => 'Attachment order updated',\n    'attachments_updated_success' => 'Attachment details updated',\n    'attachments_deleted' => 'Attachment deleted',\n    'attachments_file_uploaded' => 'File successfully uploaded',\n    'attachments_file_updated' => 'File successfully updated',\n    'attachments_link_attached' => 'Link successfully attached to page',\n    'templates' => 'Templates',\n    'templates_set_as_template' => 'Page is a template',\n    'templates_explain_set_as_template' => 'You can set this page as a template so its contents be utilized when creating other pages. Other users will be able to use this template if they have view permissions for this page.',\n    'templates_replace_content' => 'Replace page content',\n    'templates_append_content' => 'Append to page content',\n    'templates_prepend_content' => 'Prepend to page content',\n\n    // Profile View\n    'profile_user_for_x' => 'User for :time',\n    'profile_created_content' => 'Created Content',\n    'profile_not_created_pages' => ':userName has not created any pages',\n    'profile_not_created_chapters' => ':userName has not created any chapters',\n    'profile_not_created_books' => ':userName has not created any books',\n    'profile_not_created_shelves' => ':userName has not created any shelves',\n\n    // Comments\n    'comment' => 'Comment',\n    'comments' => 'Comments',\n    'comment_add' => 'Add Comment',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Leave a comment here',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Save Comment',\n    'comment_new' => 'New Comment',\n    'comment_created' => 'commented :createDiff',\n    'comment_updated' => 'Updated :updateDiff by :username',\n    'comment_updated_indicator' => 'Updated',\n    'comment_deleted_success' => 'Comment deleted',\n    'comment_created_success' => 'Comment added',\n    'comment_updated_success' => 'Comment updated',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Are you sure you want to delete this comment?',\n    'comment_in_reply_to' => 'In reply to :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',\n\n    // Revision\n    'revision_delete_confirm' => 'Are you sure you want to delete this revision?',\n    'revision_restore_confirm' => 'Are you sure you want to restore this revision? The current page contents will be replaced.',\n    'revision_cannot_delete_latest' => 'Cannot delete the latest revision.',\n\n    // Copy view\n    'copy_consider' => 'Please consider the below when copying content.',\n    'copy_consider_permissions' => 'Custom permission settings will not be copied.',\n    'copy_consider_owner' => 'You will become the owner of all copied content.',\n    'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',\n    'copy_consider_attachments' => 'Page attachments will not be copied.',\n    'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',\n\n    // Conversions\n    'convert_to_shelf' => 'Convert to Shelf',\n    'convert_to_shelf_contents_desc' => 'You can convert this book to a new shelf with the same contents. Chapters contained within this book will be converted to new books. If this book contains any pages, that are not in a chapter, this book will be renamed and contain such pages, and this book will become part of the new shelf.',\n    'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.',\n    'convert_book' => 'Convert Book',\n    'convert_book_confirm' => 'Are you sure you want to convert this book?',\n    'convert_undo_warning' => 'This cannot be as easily undone.',\n    'convert_to_book' => 'Convert to Book',\n    'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',\n    'convert_chapter' => 'Convert Chapter',\n    'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',\n\n    // References\n    'references' => 'References',\n    'references_none' => 'There are no tracked references to this item.',\n    'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',\n\n    // Watch Options\n    'watch' => 'Watch',\n    'watch_title_default' => 'Default Preferences',\n    'watch_desc_default' => 'Revert watching to just your default notification preferences.',\n    'watch_title_ignore' => 'Ignore',\n    'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.',\n    'watch_title_new' => 'New Pages',\n    'watch_desc_new' => 'Notify when any new page is created within this item.',\n    'watch_title_updates' => 'All Page Updates',\n    'watch_desc_updates' => 'Notify upon all new pages and page changes.',\n    'watch_desc_updates_page' => 'Notify upon all page changes.',\n    'watch_title_comments' => 'All Page Updates & Comments',\n    'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.',\n    'watch_desc_comments_page' => 'Notify upon page changes and new comments.',\n    'watch_change_default' => 'Change default notification preferences',\n    'watch_detail_ignore' => 'Ignoring notifications',\n    'watch_detail_new' => 'Watching for new pages',\n    'watch_detail_updates' => 'Watching new pages and updates',\n    'watch_detail_comments' => 'Watching new pages, updates & comments',\n    'watch_detail_parent_book' => 'Watching via parent book',\n    'watch_detail_parent_book_ignore' => 'Ignoring via parent book',\n    'watch_detail_parent_chapter' => 'Watching via parent chapter',\n    'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter',\n];\n"
  },
  {
    "path": "lang/bs/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Nemate ovlaštenje da pristupite ovoj stranici.',\n    'permissionJson' => 'Nemate ovlaštenje da izvršite tu akciju.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Korisnik sa e-mailom :email već postoji ali sa različitim podacima.',\n    'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',\n    'email_already_confirmed' => 'E-mail je već potvrđen, pokušajte se prijaviti.',\n    'email_confirmation_invalid' => 'Ovaj token za potvrdu nije ispravan ili je već iskorišten, molimo vas pokušajte se registrovati ponovno.',\n    'email_confirmation_expired' => 'Ovaj token za potvrdu je istekao, novi e-mail za potvrdu je poslan.',\n    'email_confirmation_awaiting' => 'E-mail adresa za račun koji se koristi mora biti potvrđena',\n    'ldap_fail_anonymous' => 'LDAP pristup nije uspio koristeći anonimno povezivanje',\n    'ldap_fail_authed' => 'LDAP pristup nije uspio koristeći date detalje lozinke i dn',\n    'ldap_extension_not_installed' => 'LDAP PHP ekstenzija nije instalirana',\n    'ldap_cannot_connect' => 'Nije se moguće povezati sa ldap serverom, incijalna konekcija nije uspjela',\n    'saml_already_logged_in' => 'Već prijavljeni',\n    'saml_no_email_address' => 'E-mail adresa za ovog korisnika nije nađena u podacima dobijenim od eksternog autentifikacijskog sistema',\n    'saml_invalid_response_id' => 'Proces, koji je pokrenula ova aplikacija, nije prepoznao zahtjev od eksternog sistema za autentifikaciju. Navigacija nazad nakon prijave može uzrokovati ovaj problem.',\n    'saml_fail_authed' => 'Prijava koristeći :system nije uspjela, sistem nije obezbijedio uspješnu autorizaciju',\n    'oidc_already_logged_in' => 'Already logged in',\n    'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',\n    'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',\n    'social_no_action_defined' => 'Nema definisane akcije',\n    'social_login_bad_response' => \"Došlo je do greške prilikom prijave preko :socialAccount :\\n:error\",\n    'social_account_in_use' => 'Ovaj :socialAccount račun se već koristi, pokušajte se prijaviti putem :socialAccount opcije.',\n    'social_account_email_in_use' => 'E-mail :email se već koristi. Ako već imate račun možete povezati vaš :socialAccount račun u postavkama profila.',\n    'social_account_existing' => 'Ovaj :socialAccount je već povezan sa vašim profilom.',\n    'social_account_already_used_existing' => 'Drugi korisnik već koristi ovaj :socialAccount.',\n    'social_account_not_used' => 'Ovaj :socialAccount nije povezan ni sa jednim korisnikom. Povežite ga u postavkama profila. ',\n    'social_account_register_instructions' => 'Ako još uvijek nemate račun, možete se registrovati koristeći :socialAccount opciju.',\n    'social_driver_not_found' => 'Driver društvene mreže nije pronađen',\n    'social_driver_not_configured' => 'Vaše :socialAccount postavke nisu konfigurisane ispravno.',\n    'invite_token_expired' => 'Pozivni link je istekao. Možete umjesto toga pokušati da resetujete lozinku.',\n    'login_user_not_found' => 'A user for this action could not be found.',\n\n    // System\n    'path_not_writable' => 'Na putanju fajla :filePath se ne može učitati. Potvrdite da je omogućeno pisanje na server.',\n    'cannot_get_image_from_url' => 'Nije moguće dobiti sliku sa :url',\n    'cannot_create_thumbs' => 'Server ne može kreirati sličice. Provjerite da imate instaliranu GD PHP ekstenziju.',\n    'server_upload_limit' => 'Server ne dopušta učitavanja ove veličine. Pokušajte sa manjom veličinom fajla.',\n    'server_post_limit' => 'The server cannot receive the provided amount of data. Try again with less data or a smaller file.',\n    'uploaded'  => 'Server ne dopušta učitavanja ove veličine. Pokušajte sa manjom veličinom fajla.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Desila se greška prilikom učitavanja slike',\n    'image_upload_type_error' => 'Vrsta slike koja se učitava je neispravna',\n    'image_upload_replace_type' => 'Image file replacements must be of the same type',\n    'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.',\n    'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.',\n    'image_gallery_thumbnail_memory_limit' => 'Failed to create gallery thumbnails due to system resource limits.',\n    'drawing_data_not_found' => 'Drawing data could not be loaded. The drawing file might no longer exist or you may not have permission to access it.',\n\n    // Attachments\n    'attachment_not_found' => 'Prilog nije pronađen',\n    'attachment_upload_error' => 'An error occurred uploading the attachment file',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Snimanje skice nije uspjelo. Provjerite da ste povezani na internet prije snimanja ove stranice',\n    'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content',\n    'page_custom_home_deletion' => 'Stranicu nije moguće izbrisati dok se koristi kao početna stranica',\n\n    // Entities\n    'entity_not_found' => 'Entitet nije pronađen',\n    'bookshelf_not_found' => 'Shelf not found',\n    'book_not_found' => 'Knjiga nije pronađena',\n    'page_not_found' => 'Stranica nije pronađena',\n    'chapter_not_found' => 'Poglavlje nije pronađeno',\n    'selected_book_not_found' => 'Odabrana knjiga nije pronađena',\n    'selected_book_chapter_not_found' => 'Odabrana knjiga ili poglavlje nije pronađeno',\n    'guests_cannot_save_drafts' => 'Gosti ne mogu snimati skice',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Ne možete izbrisati jedinog administratora',\n    'users_cannot_delete_guest' => 'Ne možete izbrisati gost korisnika',\n    'users_could_not_send_invite' => 'Could not create user since invite email failed to send',\n\n    // Roles\n    'role_cannot_be_edited' => 'Ova uloga ne može biti mijenjana',\n    'role_system_cannot_be_deleted' => 'Ova uloga je sistemska uloga i ne može biti izbrisana',\n    'role_registration_default_cannot_delete' => 'Ova uloga ne može biti izbrisana dok je postavljena kao osnovna registracijska uloga',\n    'role_cannot_remove_only_admin' => 'Ovaj korisnik je jedini korisnik sa ulogom administratora. Postavite ulogu administratora drugom korisniku prije nego je uklonite ovdje.',\n\n    // Comments\n    'comment_list' => 'Desila se greška prilikom dobavljanja komentara.',\n    'cannot_add_comment_to_draft' => 'Ne možete dodati komentare na skicu.',\n    'comment_add' => 'Desila se greška prilikom dodavanja / ažuriranja komentara.',\n    'comment_delete' => 'Desila se greška prilikom brisanja komentara.',\n    'empty_comment' => 'Nemoguće dodati prazan komentar.',\n\n    // Error pages\n    '404_page_not_found' => 'Stranica nije pronađena',\n    'sorry_page_not_found' => 'Stranica koju ste tražili nije pronađena.',\n    'sorry_page_not_found_permission_warning' => 'Ako ste očekivali da ova stranica postoji, možda nemate privilegije da joj pristupite.',\n    'image_not_found' => 'Image Not Found',\n    'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',\n    'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',\n    'return_home' => 'Nazad na početnu stranu',\n    'error_occurred' => 'Desila se greška',\n    'app_down' => ':appName trenutno nije u funkciji',\n    'back_soon' => 'Biti će uskoro u funkciji.',\n\n    // Import\n    'import_zip_cant_read' => 'Could not read ZIP file.',\n    'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',\n    'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Import ZIP failed to validate with errors:',\n    'import_zip_failed_notification' => 'Failed to import ZIP file.',\n    'import_perms_books' => 'You are lacking the required permissions to create books.',\n    'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',\n    'import_perms_pages' => 'You are lacking the required permissions to create pages.',\n    'import_perms_images' => 'You are lacking the required permissions to create images.',\n    'import_perms_attachments' => 'You are lacking the required permission to create attachments.',\n\n    // API errors\n    'api_no_authorization_found' => 'Na zahtjevu nije pronađen token za autorizaciju',\n    'api_bad_authorization_format' => 'Token za autorizaciju je pronađen u zahtjevu ali je format neispravan',\n    'api_user_token_not_found' => 'Nije pronađen odgovarajući API token za pruženi token autorizacije',\n    'api_incorrect_token_secret' => 'Tajni ključ naveden za dati korišteni API token nije tačan',\n    'api_user_no_api_permission' => 'Vlasnik korištenog API tokena nema dozvolu za upućivanje API poziva',\n    'api_user_token_expired' => 'Autorizacijski token je istekao',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Došlo je do greške prilikom slanja testnog e-maila:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'The URL does not match the configured allowed SSR hosts',\n];\n"
  },
  {
    "path": "lang/bs/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'New comment on page: :pageName',\n    'new_comment_intro' => 'A user has commented on a page in :appName:',\n    'new_page_subject' => 'New page: :pageName',\n    'new_page_intro' => 'A new page has been created in :appName:',\n    'updated_page_subject' => 'Updated page: :pageName',\n    'updated_page_intro' => 'A page has been updated in :appName:',\n    'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\\'t be sent notifications for further edits to this page by the same editor.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Page Name:',\n    'detail_page_path' => 'Page Path:',\n    'detail_commenter' => 'Commenter:',\n    'detail_comment' => 'Comment:',\n    'detail_created_by' => 'Created By:',\n    'detail_updated_by' => 'Updated By:',\n\n    'action_view_comment' => 'View Comment',\n    'action_view_page' => 'View Page',\n\n    'footer_reason' => 'This notification was sent to you because :link cover this type of activity for this item.',\n    'footer_reason_link' => 'your notification preferences',\n];\n"
  },
  {
    "path": "lang/bs/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Prethodna',\n    'next'     => 'Sljedeća &raquo;',\n\n];\n"
  },
  {
    "path": "lang/bs/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Lozinke moraju sadržavati najmanje osam karaktera i podudarati se sa potvrdom lozinke.',\n    'user' => \"Ne možemo naći korisnika sa tom e-mail adresom.\",\n    'token' => 'Token za poništavanje lozinke nije validan za ovu e-mail adresu.',\n    'sent' => 'Poslali smo link za poništavanje vaše lozinke na e-mail!',\n    'reset' => 'Vaša lozinka je resetovana!',\n\n];\n"
  },
  {
    "path": "lang/bs/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'My Account',\n\n    'shortcuts' => 'Shortcuts',\n    'shortcuts_interface' => 'UI Shortcut Preferences',\n    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',\n    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',\n    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',\n    'shortcuts_section_navigation' => 'Navigation',\n    'shortcuts_section_actions' => 'Common Actions',\n    'shortcuts_save' => 'Save Shortcuts',\n    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing \"?\" which will highlight the available shortcuts for actions currently visible on the screen.',\n    'shortcuts_update_success' => 'Shortcut preferences have been updated!',\n    'shortcuts_overview_desc' => 'Manage keyboard shortcuts you can use to navigate the system user interface.',\n\n    'notifications' => 'Notification Preferences',\n    'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.',\n    'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own',\n    'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Notify upon replies to my comments',\n    'notifications_save' => 'Save Preferences',\n    'notifications_update_success' => 'Notification preferences have been updated!',\n    'notifications_watched' => 'Watched & Ignored Items',\n    'notifications_watched_desc' => 'Below are the items that have custom watch preferences applied. To update your preferences for these, view the item then find the watch options in the sidebar.',\n\n    'auth' => 'Access & Security',\n    'auth_change_password' => 'Change Password',\n    'auth_change_password_desc' => 'Change the password you use to log-in to the application. This must be at least 8 characters long.',\n    'auth_change_password_success' => 'Password has been updated!',\n\n    'profile' => 'Profile Details',\n    'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.',\n    'profile_view_public' => 'View Public Profile',\n    'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.',\n    'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.',\n    'profile_email_no_permission' => 'Unfortunately you don\\'t have permission to change your email address. If you want to change this, you\\'d need to ask an administrator to change this for you.',\n    'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.',\n    'profile_admin_options' => 'Administrator Options',\n    'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the \"Settings > Users\" area of the application.',\n\n    'delete_account' => 'Delete Account',\n    'delete_my_account' => 'Delete My Account',\n    'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\\'ve created, such as created pages and uploaded images, will remain.',\n    'delete_my_account_warning' => 'Are you sure you want to delete your account?',\n];\n"
  },
  {
    "path": "lang/bs/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Settings',\n    'settings_save' => 'Save Settings',\n    'system_version' => 'System Version',\n    'categories' => 'Categories',\n\n    // App Settings\n    'app_customization' => 'Customization',\n    'app_features_security' => 'Features & Security',\n    'app_name' => 'Application Name',\n    'app_name_desc' => 'This name is shown in the header and in any system-sent emails.',\n    'app_name_header' => 'Show name in header',\n    'app_public_access' => 'Public Access',\n    'app_public_access_desc' => 'Enabling this option will allow visitors, that are not logged-in, to access content in your BookStack instance.',\n    'app_public_access_desc_guest' => 'Access for public visitors can be controlled through the \"Guest\" user.',\n    'app_public_access_toggle' => 'Allow public access',\n    'app_public_viewing' => 'Allow public viewing?',\n    'app_secure_images' => 'Higher Security Image Uploads',\n    'app_secure_images_toggle' => 'Enable higher security image uploads',\n    'app_secure_images_desc' => 'For performance reasons, all images are public. This option adds a random, hard-to-guess string in front of image urls. Ensure directory indexes are not enabled to prevent easy access.',\n    'app_default_editor' => 'Default Page Editor',\n    'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.',\n    'app_custom_html' => 'Custom HTML Head Content',\n    'app_custom_html_desc' => 'Any content added here will be inserted into the bottom of the <head> section of every page. This is handy for overriding styles or adding analytics code.',\n    'app_custom_html_disabled_notice' => 'Custom HTML head content is disabled on this settings page to ensure any breaking changes can be reverted.',\n    'app_logo' => 'Application Logo',\n    'app_logo_desc' => 'This is used in the application header bar, among other areas. This image should be 86px in height. Large images will be scaled down.',\n    'app_icon' => 'Application Icon',\n    'app_icon_desc' => 'This icon is used for browser tabs and shortcut icons. This should be a 256px square PNG image.',\n    'app_homepage' => 'Application Homepage',\n    'app_homepage_desc' => 'Select a view to show on the homepage instead of the default view. Page permissions are ignored for selected pages.',\n    'app_homepage_select' => 'Select a page',\n    'app_footer_links' => 'Footer Links',\n    'app_footer_links_desc' => 'Add links to show within the site footer. These will be displayed at the bottom of most pages, including those that do not require login. You can use a label of \"trans::<key>\" to use system-defined translations. For example: Using \"trans::common.privacy_policy\" will provide the translated text \"Privacy Policy\" and \"trans::common.terms_of_service\" will provide the translated text \"Terms of Service\".',\n    'app_footer_links_label' => 'Link Label',\n    'app_footer_links_url' => 'Link URL',\n    'app_footer_links_add' => 'Add Footer Link',\n    'app_disable_comments' => 'Disable Comments',\n    'app_disable_comments_toggle' => 'Disable comments',\n    'app_disable_comments_desc' => 'Disables comments across all pages in the application. <br> Existing comments are not shown.',\n\n    // Color settings\n    'color_scheme' => 'Application Color Scheme',\n    'color_scheme_desc' => 'Set the colors to use in the application user interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.',\n    'ui_colors_desc' => 'Set the application primary color and default link color. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the application interface.',\n    'app_color' => 'Primary Color',\n    'link_color' => 'Default Link Color',\n    'content_colors_desc' => 'Set colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.',\n    'bookshelf_color' => 'Shelf Color',\n    'book_color' => 'Book Color',\n    'chapter_color' => 'Chapter Color',\n    'page_color' => 'Page Color',\n    'page_draft_color' => 'Page Draft Color',\n\n    // Registration Settings\n    'reg_settings' => 'Registration',\n    'reg_enable' => 'Enable Registration',\n    'reg_enable_toggle' => 'Enable registration',\n    'reg_enable_desc' => 'When registration is enabled user will be able to sign themselves up as an application user. Upon registration they are given a single, default user role.',\n    'reg_default_role' => 'Default user role after registration',\n    'reg_enable_external_warning' => 'The option above is ignored while external LDAP or SAML authentication is active. User accounts for non-existing members will be auto-created if authentication, against the external system in use, is successful.',\n    'reg_email_confirmation' => 'Email Confirmation',\n    'reg_email_confirmation_toggle' => 'Require email confirmation',\n    'reg_confirm_email_desc' => 'If domain restriction is used then email confirmation will be required and this option will be ignored.',\n    'reg_confirm_restrict_domain' => 'Domain Restriction',\n    'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.',\n    'reg_confirm_restrict_domain_placeholder' => 'No restriction set',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Maintenance',\n    'maint_image_cleanup' => 'Cleanup Images',\n    'maint_image_cleanup_desc' => 'Scans page & revision content to check which images and drawings are currently in use and which images are redundant. Ensure you create a full database and image backup before running this.',\n    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',\n    'maint_image_cleanup_run' => 'Run Cleanup',\n    'maint_image_cleanup_warning' => ':count potentially unused images were found. Are you sure you want to delete these images?',\n    'maint_image_cleanup_success' => ':count potentially unused images found and deleted!',\n    'maint_image_cleanup_nothing_found' => 'No unused images found, Nothing deleted!',\n    'maint_send_test_email' => 'Send a Test Email',\n    'maint_send_test_email_desc' => 'This sends a test email to your email address specified in your profile.',\n    'maint_send_test_email_run' => 'Send test email',\n    'maint_send_test_email_success' => 'Email sent to :address',\n    'maint_send_test_email_mail_subject' => 'Test Email',\n    'maint_send_test_email_mail_greeting' => 'Email delivery seems to work!',\n    'maint_send_test_email_mail_text' => 'Congratulations! As you received this email notification, your email settings seem to be configured properly.',\n    'maint_recycle_bin_desc' => 'Deleted shelves, books, chapters & pages are sent to the recycle bin so they can be restored or permanently deleted. Older items in the recycle bin may be automatically removed after a while depending on system configuration.',\n    'maint_recycle_bin_open' => 'Open Recycle Bin',\n    'maint_regen_references' => 'Regenerate References',\n    'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',\n    'maint_regen_references_success' => 'Reference index has been regenerated!',\n    'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Recycle Bin',\n    'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',\n    'recycle_bin_deleted_item' => 'Deleted Item',\n    'recycle_bin_deleted_parent' => 'Parent',\n    'recycle_bin_deleted_by' => 'Deleted By',\n    'recycle_bin_deleted_at' => 'Deletion Time',\n    'recycle_bin_permanently_delete' => 'Permanently Delete',\n    'recycle_bin_restore' => 'Restore',\n    'recycle_bin_contents_empty' => 'The recycle bin is currently empty',\n    'recycle_bin_empty' => 'Empty Recycle Bin',\n    'recycle_bin_empty_confirm' => 'This will permanently destroy all items in the recycle bin including content contained within each item. Are you sure you want to empty the recycle bin?',\n    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?',\n    'recycle_bin_destroy_list' => 'Items to be Destroyed',\n    'recycle_bin_restore_list' => 'Items to be Restored',\n    'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.',\n    'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.',\n    'recycle_bin_restore_parent' => 'Restore Parent',\n    'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',\n    'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',\n\n    // Audit Log\n    'audit' => 'Audit Log',\n    'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',\n    'audit_event_filter' => 'Event Filter',\n    'audit_event_filter_no_filter' => 'No Filter',\n    'audit_deleted_item' => 'Deleted Item',\n    'audit_deleted_item_name' => 'Name: :name',\n    'audit_table_user' => 'User',\n    'audit_table_event' => 'Event',\n    'audit_table_related' => 'Related Item or Detail',\n    'audit_table_ip' => 'IP Address',\n    'audit_table_date' => 'Activity Date',\n    'audit_date_from' => 'Date Range From',\n    'audit_date_to' => 'Date Range To',\n\n    // Role Settings\n    'roles' => 'Roles',\n    'role_user_roles' => 'User Roles',\n    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',\n    'roles_x_users_assigned' => ':count user assigned|:count users assigned',\n    'roles_x_permissions_provided' => ':count permission|:count permissions',\n    'roles_assigned_users' => 'Assigned Users',\n    'roles_permissions_provided' => 'Provided Permissions',\n    'role_create' => 'Create New Role',\n    'role_delete' => 'Delete Role',\n    'role_delete_confirm' => 'This will delete the role with the name \\':roleName\\'.',\n    'role_delete_users_assigned' => 'This role has :userCount users assigned to it. If you would like to migrate the users from this role select a new role below.',\n    'role_delete_no_migration' => \"Don't migrate users\",\n    'role_delete_sure' => 'Are you sure you want to delete this role?',\n    'role_edit' => 'Edit Role',\n    'role_details' => 'Role Details',\n    'role_name' => 'Role Name',\n    'role_desc' => 'Short Description of Role',\n    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',\n    'role_external_auth_id' => 'External Authentication IDs',\n    'role_system' => 'System Permissions',\n    'role_manage_users' => 'Manage users',\n    'role_manage_roles' => 'Manage roles & role permissions',\n    'role_manage_entity_permissions' => 'Manage all book, chapter & page permissions',\n    'role_manage_own_entity_permissions' => 'Manage permissions on own book, chapter & pages',\n    'role_manage_page_templates' => 'Manage page templates',\n    'role_access_api' => 'Access system API',\n    'role_manage_settings' => 'Manage app settings',\n    'role_export_content' => 'Export content',\n    'role_import_content' => 'Import content',\n    'role_editor_change' => 'Change page editor',\n    'role_notifications' => 'Receive & manage notifications',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Asset Permissions',\n    'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',\n    'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',\n    'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.',\n    'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'All',\n    'role_own' => 'Own',\n    'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',\n    'role_save' => 'Save Role',\n    'role_users' => 'Users in this role',\n    'role_users_none' => 'No users are currently assigned to this role',\n\n    // Users\n    'users' => 'Users',\n    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',\n    'user_profile' => 'User Profile',\n    'users_add_new' => 'Add New User',\n    'users_search' => 'Search Users',\n    'users_latest_activity' => 'Latest Activity',\n    'users_details' => 'User Details',\n    'users_details_desc' => 'Set a display name and an email address for this user. The email address will be used for logging into the application.',\n    'users_details_desc_no_email' => 'Set a display name for this user so others can recognise them.',\n    'users_role' => 'User Roles',\n    'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.',\n    'users_password' => 'User Password',\n    'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',\n    'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.',\n    'users_send_invite_option' => 'Send user invite email',\n    'users_external_auth_id' => 'External Authentication ID',\n    'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.',\n    'users_password_warning' => 'Only fill the below if you would like to change the password for this user.',\n    'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.',\n    'users_delete' => 'Delete User',\n    'users_delete_named' => 'Delete user :userName',\n    'users_delete_warning' => 'This will fully delete this user with the name \\':userName\\' from the system.',\n    'users_delete_confirm' => 'Are you sure you want to delete this user?',\n    'users_migrate_ownership' => 'Migrate Ownership',\n    'users_migrate_ownership_desc' => 'Select a user here if you want another user to become the owner of all items currently owned by this user.',\n    'users_none_selected' => 'No user selected',\n    'users_edit' => 'Edit User',\n    'users_edit_profile' => 'Edit Profile',\n    'users_avatar' => 'User Avatar',\n    'users_avatar_desc' => 'Select an image to represent this user. This should be approx 256px square.',\n    'users_preferred_language' => 'Preferred Language',\n    'users_preferred_language_desc' => 'This option will change the language used for the user-interface of the application. This will not affect any user-created content.',\n    'users_social_accounts' => 'Social Accounts',\n    'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.',\n    'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not revoke previously authorized access. Revoke access from your profile settings on the connected social account.',\n    'users_social_connect' => 'Connect Account',\n    'users_social_disconnect' => 'Disconnect Account',\n    'users_social_status_connected' => 'Connected',\n    'users_social_status_disconnected' => 'Disconnected',\n    'users_social_connected' => ':socialAccount account was successfully attached to your profile.',\n    'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.',\n    'users_api_tokens' => 'API Tokens',\n    'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.',\n    'users_api_tokens_none' => 'No API tokens have been created for this user',\n    'users_api_tokens_create' => 'Create Token',\n    'users_api_tokens_expires' => 'Expires',\n    'users_api_tokens_docs' => 'API Documentation',\n    'users_mfa' => 'Multi-Factor Authentication',\n    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'users_mfa_x_methods' => ':count method configured|:count methods configured',\n    'users_mfa_configure' => 'Configure Methods',\n\n    // API Tokens\n    'user_api_token_create' => 'Create API Token',\n    'user_api_token_name' => 'Name',\n    'user_api_token_name_desc' => 'Give your token a readable name as a future reminder of its intended purpose.',\n    'user_api_token_expiry' => 'Expiry Date',\n    'user_api_token_expiry_desc' => 'Set a date at which this token expires. After this date, requests made using this token will no longer work. Leaving this field blank will set an expiry 100 years into the future.',\n    'user_api_token_create_secret_message' => 'Immediately after creating this token a \"Token ID\" & \"Token Secret\" will be generated and displayed. The secret will only be shown a single time so be sure to copy the value to somewhere safe and secure before proceeding.',\n    'user_api_token' => 'API Token',\n    'user_api_token_id' => 'Token ID',\n    'user_api_token_id_desc' => 'This is a non-editable system generated identifier for this token which will need to be provided in API requests.',\n    'user_api_token_secret' => 'Token Secret',\n    'user_api_token_secret_desc' => 'This is a system generated secret for this token which will need to be provided in API requests. This will only be displayed this one time so copy this value to somewhere safe and secure.',\n    'user_api_token_created' => 'Token created :timeAgo',\n    'user_api_token_updated' => 'Token updated :timeAgo',\n    'user_api_token_delete' => 'Delete Token',\n    'user_api_token_delete_warning' => 'This will fully delete this API token with the name \\':tokenName\\' from the system.',\n    'user_api_token_delete_confirm' => 'Are you sure you want to delete this API token?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',\n    'webhooks_x_trigger_events' => ':count trigger event|:count trigger events',\n    'webhooks_create' => 'Create New Webhook',\n    'webhooks_none_created' => 'No webhooks have yet been created.',\n    'webhooks_edit' => 'Edit Webhook',\n    'webhooks_save' => 'Save Webhook',\n    'webhooks_details' => 'Webhook Details',\n    'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',\n    'webhooks_events' => 'Webhook Events',\n    'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',\n    'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\\'t expose confidential content.',\n    'webhooks_events_all' => 'All system events',\n    'webhooks_name' => 'Webhook Name',\n    'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',\n    'webhooks_endpoint' => 'Webhook Endpoint',\n    'webhooks_active' => 'Webhook Active',\n    'webhook_events_table_header' => 'Events',\n    'webhooks_delete' => 'Delete Webhook',\n    'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \\':webhookName\\', from the system.',\n    'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?',\n    'webhooks_format_example' => 'Webhook Format Example',\n    'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The \"related_item\" and \"url\" properties are optional and will depend on the type of event triggered.',\n    'webhooks_status' => 'Webhook Status',\n    'webhooks_last_called' => 'Last Called:',\n    'webhooks_last_errored' => 'Last Errored:',\n    'webhooks_last_error_message' => 'Last Error Message:',\n\n    // Licensing\n    'licenses' => 'Licenses',\n    'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.',\n    'licenses_bookstack' => 'BookStack License',\n    'licenses_php' => 'PHP Library Licenses',\n    'licenses_js' => 'JavaScript Library Licenses',\n    'licenses_other' => 'Other Licenses',\n    'license_details' => 'License Details',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/bs/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute mora biti prihvaćen.',\n    'active_url'           => ':attribute nije ispravan URL.',\n    'after'                => ':attribute mora biti datum nakon :date.',\n    'alpha'                => ':attribute može sadržavati samo slova.',\n    'alpha_dash'           => ':attribute može sadržavati samo slova, brojeve, crtice i donje crtice.',\n    'alpha_num'            => ':attribute može sadržavati samo slova i brojeve.',\n    'array'                => ':attribute mora biti niz.',\n    'backup_codes'         => 'The provided code is not valid or has already been used.',\n    'before'               => ':attribute mora biti datum prije :date.',\n    'between'              => [\n        'numeric' => ':attribute mora biti između :min i :max.',\n        'file'    => ':attribute mora biti između :min i :max kilobajta.',\n        'string'  => ':attribute mora biti između :min i :max karaktera.',\n        'array'   => ':attribute mora imati između :min i :max stavki.',\n    ],\n    'boolean'              => ':attribute polje mora biti tačno ili netačno.',\n    'confirmed'            => ':attribute potvrda se ne slaže.',\n    'date'                 => ':attribute nije ispravan datum.',\n    'date_format'          => ':attribute ne odgovara formatu :format.',\n    'different'            => ':attribute i :other moraju biti različiti.',\n    'digits'               => ':attribute mora imati :digits brojeve.',\n    'digits_between'       => ':attribute mora imati između :min i :max brojeva.',\n    'email'                => ':attribute mora biti ispravna e-mail adresa.',\n    'ends_with' => ':attribute mora završavati sa jednom od sljedećih: :values',\n    'file'                 => 'The :attribute must be provided as a valid file.',\n    'filled'               => 'Polje :attribute je obavezno.',\n    'gt'                   => [\n        'numeric' => ':attribute mora biti veći od :value.',\n        'file'    => ':attribute mota biti veći od :value kilobajta.',\n        'string'  => ':attribute mora imati više od :value karaktera.',\n        'array'   => ':attribute mora imati više od :value stavki.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute mora biti veći od ili jednak :value.',\n        'file'    => ':attribute mora imati više od ili jednako :value kilobajta.',\n        'string'  => ':attribute mora imati više od ili jednako :value karaktera.',\n        'array'   => ':attribute mora imati :value stavki ili više.',\n    ],\n    'exists'               => 'Odabrani :attribute je neispravan.',\n    'image'                => ':attribute mora biti slika.',\n    'image_extension'      => ':attribute mora imati ispravnu i podržanu ekstenziju slike.',\n    'in'                   => 'Odabrani :attribute je neispravan.',\n    'integer'              => ':attribute mora biti integer.',\n    'ip'                   => ':attribute mora biti ispravna IP adresa.',\n    'ipv4'                 => ':attribute mora biti ispravna IPv4 adresa.',\n    'ipv6'                 => ':attribute mora biti ispravna IPv6 adresa.',\n    'json'                 => ':attribute mora biti ispravan JSON string.',\n    'lt'                   => [\n        'numeric' => ':attribute mora biti manji od :value.',\n        'file'    => ':attribute mora imati manje od :value kilobajta.',\n        'string'  => ':attribute mora imati manje od :value karaktera.',\n        'array'   => ':attribute mora imati manje od :value stavki.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute mora imati vrijednost manju od ili jednaku :value.',\n        'file'    => ':attribute mora imati manje od ili jednako :value kilobajta.',\n        'string'  => ':attribute mora imati manje od ili jednako :value karaktera.',\n        'array'   => ':attribute ne smije imati više od :value stavki.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute ne može biti veći od :max.',\n        'file'    => ':attribute ne može imati više od :max kilobajta.',\n        'string'  => ':attribute ne može imati više od :max karaktera.',\n        'array'   => ':attribute ne može imati više od :max stavki.',\n    ],\n    'mimes'                => ':attribute mora biti fajl vrste: values.',\n    'min'                  => [\n        'numeric' => ':attribute mora biti najmanje :min.',\n        'file'    => ':attribute mora imati najmanje :min kilobajta.',\n        'string'  => ':attribute mora imati najmanje :min karaktera.',\n        'array'   => ':attribute mora imati najmanje :min stavki.',\n    ],\n    'not_in'               => 'Odabrani :attribute je neispravan.',\n    'not_regex'            => 'Format :attribute je neispravan.',\n    'numeric'              => ':attribute mora biti broj.',\n    'regex'                => 'Format :attribute je neispravan.',\n    'required'             => 'Polje :attribute je obavezno.',\n    'required_if'          => 'Polje :attribute je obavezno kada :other ima vrijednost :value.',\n    'required_with'        => 'Polje :attribute je obavezno kada su prisutne :values.',\n    'required_with_all'    => 'Polje :attribute je obavezno kada su prisutne :values.',\n    'required_without'     => 'Polje :attribute je obavezno kada :values nisu prisutne.',\n    'required_without_all' => 'Polje :attribute je obavezno kada nijedno od :values nije prisutno.',\n    'same'                 => ':attribute i :other se moraju poklapati.',\n    'safe_url'             => 'Navedeni link možda nije siguran.',\n    'size'                 => [\n        'numeric' => ':attribute mora biti :size.',\n        'file'    => ':attribute mora imati :size kilobajta.',\n        'string'  => ':attribute mora imati :size karaktera.',\n        'array'   => ':attribute mora sadržavati :size stavki.',\n    ],\n    'string'               => ':attribute mora biti string.',\n    'timezone'             => ':attribute mora biti ispravna zona.',\n    'totp'                 => 'The provided code is not valid or has expired.',\n    'unique'               => ':attribute je zauzet.',\n    'url'                  => 'Format :attribute je neispravan.',\n    'uploaded'             => 'Fajl nije učitan. Server ne prihvata fajlove ove veličine.',\n\n    'zip_file' => 'The :attribute needs to reference a file within the ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',\n    'zip_model_expected' => 'Data object expected but \":type\" found.',\n    'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Zahtijeva se potvrda lozinke',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/ca/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'ha creat la pàgina',\n    'page_create_notification'    => 'S’ha creat la pàgina',\n    'page_update'                 => 'ha actualitzat la pàgina',\n    'page_update_notification'    => 'S’ha actualitzat la pàgina',\n    'page_delete'                 => 'ha suprimit la pàgina',\n    'page_delete_notification'    => 'S’ha suprimit la pàgina',\n    'page_restore'                => 'ha restaurat la pàgina',\n    'page_restore_notification'   => 'S’ha restaurat la pàgina',\n    'page_move'                   => 'ha mogut la pàgina',\n    'page_move_notification'      => 'S’ha mogut la pàgina',\n\n    // Chapters\n    'chapter_create'              => 'S\\'ha creat el capítol',\n    'chapter_create_notification' => 'S’ha creat el capítol',\n    'chapter_update'              => 'ha actualitzat el capítol',\n    'chapter_update_notification' => 'S’ha actualitzat el capítol',\n    'chapter_delete'              => 'ha suprimit el capítol',\n    'chapter_delete_notification' => 'S’ha suprimit el capítol',\n    'chapter_move'                => 's\\'ha mogut el capítol',\n    'chapter_move_notification' => 'S’ha mogut el capítol',\n\n    // Books\n    'book_create'                 => 'llibre creat',\n    'book_create_notification'    => 'S’ha creat el llibre',\n    'book_create_from_chapter'              => 'ha convertit el capítol a llibre',\n    'book_create_from_chapter_notification' => 'S’ha convertit el capítol a llibre',\n    'book_update'                 => 'llibre actualitzat',\n    'book_update_notification'    => 'S’ha actualitzat el llibre',\n    'book_delete'                 => 'llibre suprimit',\n    'book_delete_notification'    => 'S’ha suprimit el llibre',\n    'book_sort'                   => 'llibre ordenat',\n    'book_sort_notification'      => 'S’ha tornat a ordenar el llibre',\n\n    // Bookshelves\n    'bookshelf_create'            => 'ha creat el prestatge',\n    'bookshelf_create_notification'    => 'S’ha creat el prestatge',\n    'bookshelf_create_from_book'    => 'llibre convertit a prestatge',\n    'bookshelf_create_from_book_notification'    => 'S’ha convertit el llibre a prestatge',\n    'bookshelf_update'                 => 'prestatge actualitzat',\n    'bookshelf_update_notification'    => 'S’ha actualitzat el prestatge',\n    'bookshelf_delete'                 => 'prestatge suprimit',\n    'bookshelf_delete_notification'    => 'S’ha suprimit el prestatge',\n\n    // Revisions\n    'revision_restore' => 'ha restaurat la revisió',\n    'revision_delete' => 'ha suprimit la revisió',\n    'revision_delete_notification' => 'S’ha suprimit la revisió',\n\n    // Favourites\n    'favourite_add_notification' => 'S’ha afegit &laquo;:name&raquo; als favorits.',\n    'favourite_remove_notification' => 'S’ha eliminat &laquo;:name&raquo; dels favorits.',\n\n    // Watching\n    'watch_update_level_notification' => 'S’han actualitzat les preferències de seguiment',\n\n    // Auth\n    'auth_login' => 'ha iniciat sessió',\n    'auth_register' => 's’ha registrat com a usuari nou',\n    'auth_password_reset_request' => 'ha sol·licitat la reinicialització de la contrasenya',\n    'auth_password_reset_update' => 'ha reinicialitzat la contrasenya',\n    'mfa_setup_method' => 'ha configurat un mètode d’autenticació multifactorial',\n    'mfa_setup_method_notification' => 'S’ha configurat un mètode d’autenticació multifactorial',\n    'mfa_remove_method' => 'ha eliminat un mètode d’autenticació multifactorial',\n    'mfa_remove_method_notification' => 'S’ha eliminat un mètode d’autenticació multifactorial',\n\n    // Settings\n    'settings_update' => 'ha actualitzat la configuració',\n    'settings_update_notification' => 'S’ha actualitzat la configuració',\n    'maintenance_action_run' => 'ha executat una acció de manteniment',\n\n    // Webhooks\n    'webhook_create' => 'ha creat un webhook',\n    'webhook_create_notification' => 'S’ha creat el webhook',\n    'webhook_update' => 'ha actualitzat el webhook',\n    'webhook_update_notification' => 'S’ha actualitzat el webhook',\n    'webhook_delete' => 'ha suprimit el webhook',\n    'webhook_delete_notification' => 'S’ha suprimit el webhook',\n\n    // Imports\n    'import_create' => 'importació creada',\n    'import_create_notification' => 'L\\'importació s\\'ha carregat correctament',\n    'import_run' => 'importació actualitzada',\n    'import_run_notification' => 'Contingut importat correctament',\n    'import_delete' => 'importació eliminada',\n    'import_delete_notification' => 'Importació eliminada correctament',\n\n    // Users\n    'user_create' => 'ha creat l’usuari',\n    'user_create_notification' => 'S’ha creat l’usuari',\n    'user_update' => 'ha actualitzat l’usuari',\n    'user_update_notification' => 'S’ha actualitzat l’usuari',\n    'user_delete' => 'ha suprimit l’usuari',\n    'user_delete_notification' => 'S’ha suprimit l’usuari',\n\n    // API Tokens\n    'api_token_create' => 'ha creat el testimoni API',\n    'api_token_create_notification' => 'S’ha creat el testimoni API',\n    'api_token_update' => 'ha actualitzat el testimoni API',\n    'api_token_update_notification' => 'S’ha actualitzat el testimoni API',\n    'api_token_delete' => 'ha suprimit el testimoni API',\n    'api_token_delete_notification' => 'S’ha suprimit el testimoni API',\n\n    // Roles\n    'role_create' => 'ha creat el rol',\n    'role_create_notification' => 'S’ha creat el rol',\n    'role_update' => 'ha actualitzat el rol',\n    'role_update_notification' => 'S’ha actualitzat el rol',\n    'role_delete' => 'ha suprimit el rol',\n    'role_delete_notification' => 'S’ha suprimit el rol',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'ha buidat la paperera',\n    'recycle_bin_restore' => 'ha restaurat de la paperera',\n    'recycle_bin_destroy' => 'ha eliminat de la paperera',\n\n    // Comments\n    'commented_on'                => 'ha comentat a',\n    'comment_create'              => 'ha afegit un comentari',\n    'comment_update'              => 'ha actualitzat un comentari',\n    'comment_delete'              => 'ha suprimit un comentari',\n\n    // Sort Rules\n    'sort_rule_create' => 'crear regla d\\'ordenació',\n    'sort_rule_create_notification' => 'Regla d\\'ordenació creada correctament',\n    'sort_rule_update' => 'regla d\\'ordenació actualitzada',\n    'sort_rule_update_notification' => 'Regla d\\'ordenació actualitzada correctament',\n    'sort_rule_delete' => 'regla d\\'ordenació eliminada',\n    'sort_rule_delete_notification' => 'Regla d\\'ordenació eliminada correctament',\n\n    // Other\n    'permissions_update'          => 'ha actualitzat els permisos',\n];\n"
  },
  {
    "path": "lang/ca/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Aquestes credencials no existeixen al nostre registre.',\n    'throttle' => 'Massa intents d’inici de sessió. Torneu-ho a provar d’aquí :seconds segons.',\n\n    // Login & Register\n    'sign_up' => 'Registreu-vos',\n    'log_in' => 'Inicia sessió',\n    'log_in_with' => 'Inicia sessió amb :socialDriver',\n    'sign_up_with' => 'Registreu-vos amb :socialDriver',\n    'logout' => 'Tanca la sessió',\n\n    'name' => 'Nom',\n    'username' => 'Nom d’usuari',\n    'email' => 'Correu electrònic',\n    'password' => 'Contrasenya',\n    'password_confirm' => 'Confirmeu la contrasenya',\n    'password_hint' => 'Ha de tenir almenys 8 caràcters',\n    'forgot_password' => 'Heu oblidat la contrasenya?',\n    'remember_me' => 'Recorda’m',\n    'ldap_email_hint' => 'Introduïu un correu electrònic per al compte.',\n    'create_account' => 'Creeu un compte',\n    'already_have_account' => 'Ja teniu un compte?',\n    'dont_have_account' => 'No teniu cap compte?',\n    'social_login' => 'Inici de sessió amb una xarxa social',\n    'social_registration' => 'Registre amb una xarxa social',\n    'social_registration_text' => 'Registreu-vos i inicieu sessió amb un altre servei.',\n\n    'register_thanks' => 'Gràcies per registrar-vos.',\n    'register_confirm' => 'Comproveu el correu electrònic i cliqueu el botó de confirmació per a accedir a :appName.',\n    'registrations_disabled' => 'Els registres estan desactivats',\n    'registration_email_domain_invalid' => 'Aquest domini de correu electrònic no té accés a l’aplicació.',\n    'register_success' => 'Gràcies per registrar-vos Us heu registrat i heu iniciat sessió.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'S’està provant d’iniciar sessió',\n    'auto_init_starting_desc' => 'Estem contactant amb el vostre sistema d’autenticació per a començar el procés d’inici sessió. Si d’aquí 5 segons no hi ha hagut cap progrés proveu de clicar l’enllaç.',\n    'auto_init_start_link' => 'Continua amb la l’autenticació',\n\n    // Password Reset\n    'reset_password' => 'Reinicialitza la contrasenya',\n    'reset_password_send_instructions' => 'Introduïu la vostra adreça electrònica i us enviarem un correu electrònic amb un enllaç de reinicialització de la contrasenya.',\n    'reset_password_send_button' => 'Envia’m un enllaç de reinicialització',\n    'reset_password_sent' => 'Si l’adreça electrònica :email existeix al sistema, us hi enviarem un enllaç de reinicialització de contrasenya.',\n    'reset_password_success' => 'S’ha reinicialitzat la contrasenya.',\n    'email_reset_subject' => 'Reinicialització de la contrasenya de :appName',\n    'email_reset_text' => 'Heu rebut aquest correu electrònic perquè hem rebut una sol·licitud de reinicialització de contrasenya per al vostre compte.',\n    'email_reset_not_requested' => 'Si no heu sol·licitat la reinicialització de la contrasenya, no cal que feu res més.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Confirmeu l’adreça electrònica de :appName',\n    'email_confirm_greeting' => 'Gràcies per registrar-vos a :appName!',\n    'email_confirm_text' => 'Cliqueu el botó per a confirmar l’adreça electrònica:',\n    'email_confirm_action' => 'Confirmeu d’adreça electrònica',\n    'email_confirm_send_error' => 'S’ha de confirmar de l’adreça electrònica però no s’ha pogut enviar el correu de confirmació. Contacteu l’administrador per a assegurar-vos que el correu està ben configurat.',\n    'email_confirm_success' => 'S’ha confirmat el correu electrònic. Ara hauríeu de poder iniciar sessió amb aquesta adreça electrònica.',\n    'email_confirm_resent' => 'S’ha enviat el correu de confirmació. Comproveu la safata d’entrada.',\n    'email_confirm_thanks' => 'Gràcies per la confirmació.',\n    'email_confirm_thanks_desc' => 'Espereu-vos un moment mentre es gestiona la confirmació. Si d’aquí 3 segons no se us ha redirigit, cliqueu l’enllaç &laquo;Continua&raquo; per a continuar.',\n\n    'email_not_confirmed' => 'No s’ha confirmat l’adreça de correu electrònic',\n    'email_not_confirmed_text' => 'Encara no heu confirmat l’adreça electrònica.',\n    'email_not_confirmed_click_link' => 'Cliqueu l’enllaç que hi ha al correu electrònic que se us va enviar en registrar-vos.',\n    'email_not_confirmed_resend' => 'Si no trobeu el correu electrònic ompliu el formulari a continuació i us n’enviarem un altre.',\n    'email_not_confirmed_resend_button' => 'Torna a enviar el correu de confirmació',\n\n    // User Invite\n    'user_invite_email_subject' => 'Us han convidat a utilitzar :appName!',\n    'user_invite_email_greeting' => 'Us han creat un compte a :appName.',\n    'user_invite_email_text' => 'Cliqueu el botó per a configurar una contrasenya pel compte i poder-hi accedir:',\n    'user_invite_email_action' => 'Configura la contrasenya del compte',\n    'user_invite_page_welcome' => 'Us donem la benvinguda a :appName!',\n    'user_invite_page_text' => 'Per a poder accedir al compte heu de configurar una contrasenya que s’utilitzarà per a iniciar sessió a :appName d’ara endavant.',\n    'user_invite_page_confirm_button' => 'Confirma la contrasenya',\n    'user_invite_success_login' => 'S’ha configurat la contrasenya. Ara hauríeu de poder iniciar sessió a :appName amb la contrasenya configurada.',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Configuració de l’autenticació multifactorial',\n    'mfa_setup_desc' => 'Configureu l’autenticació multifactorial per a afegir una capa de seguretat extra al vostre compte d’usuari.',\n    'mfa_setup_configured' => 'Ja està configurat',\n    'mfa_setup_reconfigure' => 'Torna-la a configurar',\n    'mfa_setup_remove_confirmation' => 'Esteu segur que voleu eliminar aquest mètode d’autenticació multifactorial?',\n    'mfa_setup_action' => 'Configura',\n    'mfa_backup_codes_usage_limit_warning' => 'Us queden menys de 5 codis de suport. Genereu-ne de nous i deseu-los abans de quedar-vos-en sense per a evitar perdre l’accés al  compte.',\n    'mfa_option_totp_title' => 'Aplicació mòbil',\n    'mfa_option_totp_desc' => 'Per a utilitzar l’autenticació multifactorial heu de tenir una aplicació que sigui compatible amb TOPT (contrasenyes d’un sol ús basades en el temps) com ara Google Authenticator, Authy, o Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Codis de suport',\n    'mfa_option_backup_codes_desc' => 'Genereu un joc de codis de suport d’un sol ús que haureu d’introduir per a verificar la vostra identitat. Assegureu-vos de desar-los en un lloc segur.',\n    'mfa_gen_confirm_and_enable' => 'Confirma i activa',\n    'mfa_gen_backup_codes_title' => 'Configuració dels codis de suport',\n    'mfa_gen_backup_codes_desc' => 'Deseu els codis en un lloc segur. Podreu utilitzar un dels codis com a un altre mètode d’autenticació per a iniciar sessió.',\n    'mfa_gen_backup_codes_download' => 'Baixa els codis',\n    'mfa_gen_backup_codes_usage_warning' => 'Només podeu utilitzar cada codi un sol cop.',\n    'mfa_gen_totp_title' => 'Configuració de l’aplicació mòbil',\n    'mfa_gen_totp_desc' => 'Per a utilitzar l’autenticació multifactorial heu de tenir una aplicació que sigui compatible amb TOPT (contrasenyes d’un sol ús basades en el temps) com ara Google Authenticator, Authy, o Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Escanegeu el codi QR amb l’aplicació d’autenticació que preferiu per a començar.',\n    'mfa_gen_totp_verify_setup' => 'Verificació de la configuració',\n    'mfa_gen_totp_verify_setup_desc' => 'Introduïu un dels codis generats per l’aplicació d’autenticació per a verificar que tot funciona:',\n    'mfa_gen_totp_provide_code_here' => 'Codi',\n    'mfa_verify_access' => 'Verifica l’accés',\n    'mfa_verify_access_desc' => 'Heu de verificar la vostra identitat amb un nivell de verificació addicional per a iniciar sessió. Verifiqueu-la amb un dels mètodes que heu configurat per a continuar.',\n    'mfa_verify_no_methods' => 'No hi ha cap mètode configurat',\n    'mfa_verify_no_methods_desc' => 'No hi ha cap mètode d’autenticació multifactorial configurat al vostre compte. Heu de configurar com a mínim un mètode per a iniciar sessió.',\n    'mfa_verify_use_totp' => 'Verificació amb una aplicació mòbil',\n    'mfa_verify_use_backup_codes' => 'Verificació amb un codi de suport',\n    'mfa_verify_backup_code' => 'Codi de suport',\n    'mfa_verify_backup_code_desc' => 'Introduïu un dels codis de suport que us quedin:',\n    'mfa_verify_backup_code_enter_here' => 'Codi:',\n    'mfa_verify_totp_desc' => 'Introduïu el codi generat per l’aplicació mòbil:',\n    'mfa_setup_login_notification' => 'S’ha configurat el mètode multifactorial. Torneu a iniciar sessió amb el mètode configurat.',\n];\n"
  },
  {
    "path": "lang/ca/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Cancel·la',\n    'close' => 'Tanca',\n    'confirm' => 'Confirma',\n    'back' => 'Torna',\n    'save' => 'Desa',\n    'continue' => 'Continua',\n    'select' => 'Selecciona',\n    'toggle_all' => 'Commuta-ho tot',\n    'more' => 'Més',\n\n    // Form Labels\n    'name' => 'Nom',\n    'description' => 'Descripció',\n    'role' => 'Funció',\n    'cover_image' => 'Imatge de coberta',\n    'cover_image_description' => 'La imatge ha de ser d’uns 440×250px, però quan calgui se n’ajustarà la mida o s’escapçarà per a adaptar-la a la interfície d’usuari i les mides amb què es mostrarà canviaran.',\n\n    // Actions\n    'actions' => 'Accions',\n    'view' => 'Mostra',\n    'view_all' => 'Mostra-ho tot',\n    'new' => 'Nou',\n    'create' => 'Crea',\n    'update' => 'Actualitza',\n    'edit' => 'Edita',\n    'archive' => 'Arxivar',\n    'unarchive' => 'Desarxivar',\n    'sort' => 'Ordena',\n    'move' => 'Mou',\n    'copy' => 'Copia',\n    'reply' => 'Respon',\n    'delete' => 'Suprimeix',\n    'delete_confirm' => 'Confirma la supressió',\n    'search' => 'Cerca',\n    'search_clear' => 'Esborra la cerca',\n    'reset' => 'Reinicialitza',\n    'remove' => 'Elimina',\n    'add' => 'Afegeix',\n    'configure' => 'Configura',\n    'manage' => 'Gestiona',\n    'fullscreen' => 'Pantalla completa',\n    'favourite' => 'Afegeix als favorits',\n    'unfavourite' => 'Suprimeix dels favorits',\n    'next' => 'Endavant',\n    'previous' => 'Endarrere',\n    'filter_active' => 'Filtre actiu:',\n    'filter_clear' => 'Esborra el filtre',\n    'download' => 'Baixa',\n    'open_in_tab' => 'Obre en una pestanya',\n    'open' => 'Obre',\n\n    // Sort Options\n    'sort_options' => 'Opcions d’ordenació',\n    'sort_direction_toggle' => 'Inverteix l’ordre',\n    'sort_ascending' => 'Ordenació ascendent',\n    'sort_descending' => 'Ordenació descendent',\n    'sort_name' => 'Nom',\n    'sort_default' => 'Predeterminat',\n    'sort_created_at' => 'Data de creació',\n    'sort_updated_at' => 'Data d’actualització',\n\n    // Misc\n    'deleted_user' => 'Usuari suprimit',\n    'no_activity' => 'No hi ha cap activitat per mostrar',\n    'no_items' => 'No hi ha cap element disponible',\n    'back_to_top' => 'Amunt',\n    'skip_to_main_content' => 'Ves al contingut principal',\n    'toggle_details' => 'Commuta els detalls',\n    'toggle_thumbnails' => 'Commuta les miniatures',\n    'details' => 'Detalls',\n    'grid_view' => 'Visualització en graella',\n    'list_view' => 'Visualització en llista',\n    'default' => 'Predeterminat',\n    'breadcrumb' => 'Ruta de navegació',\n    'status' => 'Estat',\n    'status_active' => 'Actiu',\n    'status_inactive' => 'Inactiu',\n    'never' => 'Mai',\n    'none' => 'Cap',\n\n    // Header\n    'homepage' => 'Pàgina d’inici',\n    'header_menu_expand' => 'Desplega el menú de la capçalera',\n    'profile_menu' => 'Menú del perfil',\n    'view_profile' => 'Mostra el perfil',\n    'edit_profile' => 'Edita el perfil',\n    'dark_mode' => 'Mode fosc',\n    'light_mode' => 'Mode clar',\n    'global_search' => 'Cerca global',\n\n    // Layout tabs\n    'tab_info' => 'Informació',\n    'tab_info_label' => 'Pestanya: Mostra la informació secundària',\n    'tab_content' => 'Contingut',\n    'tab_content_label' => 'Pestanya: Mostra el contingut principal',\n\n    // Email Content\n    'email_action_help' => 'Si el botó &laquo;:actionText&raquo; no funciona, copieu i enganxeu l’URL següent al vostre navegador:',\n    'email_rights' => 'Tots els drets reservats',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Política de privadesa',\n    'terms_of_service' => 'Condicions del servei',\n\n    // OpenSearch\n    'opensearch_description' => 'Buscar :appName',\n];\n"
  },
  {
    "path": "lang/ca/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Selecciona una imatge',\n    'image_list' => 'Llista d’imatges',\n    'image_details' => 'Detalls de la imatge',\n    'image_upload' => 'Puja una imatge',\n    'image_intro' => 'Seleccioneu i gestioneu les imatges que s’han pujat al sistema amb anterioritat.',\n    'image_intro_upload' => 'Pugeu una imatge arrossegant-la i deixant-la anar en aquesta finestra o amb el botó &laquo;Puja una imatge&raquo;.',\n    'image_all' => 'Totes',\n    'image_all_title' => 'Mostra totes les imatges',\n    'image_book_title' => 'Mostra les imatges pujades en aquest llibre',\n    'image_page_title' => 'Mostra les imatges pujades en aquesta pàgina',\n    'image_search_hint' => 'Cerca pel nom de la imatge',\n    'image_uploaded' => 'Pujada el :uploadedDate',\n    'image_uploaded_by' => 'Pujada per :userName',\n    'image_uploaded_to' => 'Pujada a :pageLink',\n    'image_updated' => 'Actualitzada el :updateDate',\n    'image_load_more' => 'Carrega’n més',\n    'image_image_name' => 'Nom de la imatge',\n    'image_delete_used' => 'Aquesta imatge s’utilitza en les pàgines següents.',\n    'image_delete_confirm_text' => 'Esteu segur que voleu suprimir aquesta imatge?',\n    'image_select_image' => 'Selecció d’imatges',\n    'image_dropzone' => 'Deixeu-hi anar les imatges o cliqueu aquí per a pujar-ne',\n    'image_dropzone_drop' => 'Deixeu-hi anar les imatges per a pujar-les',\n    'images_deleted' => 'Imatges suprimides',\n    'image_preview' => 'Previsualització de la imatge',\n    'image_upload_success' => 'S’ha pujat la imatge',\n    'image_update_success' => 'S’han actualitzat els detalls de la imatge',\n    'image_delete_success' => 'S’ha suprimit la imatge',\n    'image_replace' => 'Substitueix la imatge',\n    'image_replace_success' => 'S’ha substituït la imatge',\n    'image_rebuild_thumbs' => 'Regenera les variacions de mida',\n    'image_rebuild_thumbs_success' => 'S’han regenerat les variacions de mida',\n\n    // Code Editor\n    'code_editor' => 'Edita el codi',\n    'code_language' => 'Llenguatge de codificació',\n    'code_content' => 'Contingut del codi',\n    'code_session_history' => 'Historial de la sessió',\n    'code_save' => 'Desa el codi',\n];\n"
  },
  {
    "path": "lang/ca/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'General',\n    'advanced' => 'Avançat',\n    'none' => 'Cap',\n    'cancel' => 'Cancel·la',\n    'save' => 'Desa',\n    'close' => 'Tanca',\n    'apply' => 'Aplicar',\n    'undo' => 'Desfés',\n    'redo' => 'Refés',\n    'left' => 'Esquerra',\n    'center' => 'Centre',\n    'right' => 'Dreta',\n    'top' => 'Dalt',\n    'middle' => 'Mig',\n    'bottom' => 'Baix',\n    'width' => 'Amplada',\n    'height' => 'Altura',\n    'More' => 'Més',\n    'select' => 'Selecciona …',\n\n    // Toolbar\n    'formats' => 'Formats',\n    'header_large' => 'Títol gros',\n    'header_medium' => 'Títol mitjà',\n    'header_small' => 'Títol petit',\n    'header_tiny' => 'Títol minúscul',\n    'paragraph' => 'Paràgraf',\n    'blockquote' => 'Bloc de cita',\n    'inline_code' => 'Línia de codi',\n    'callouts' => 'Crides',\n    'callout_information' => 'Informació',\n    'callout_success' => 'Èxit',\n    'callout_warning' => 'Advertiment',\n    'callout_danger' => 'Perill',\n    'bold' => 'Negreta',\n    'italic' => 'Cursiva',\n    'underline' => 'Subratllat',\n    'strikethrough' => 'Ratllat',\n    'superscript' => 'Superíndex',\n    'subscript' => 'Subíndex',\n    'text_color' => 'Color del text',\n    'highlight_color' => 'Color ressaltat',\n    'custom_color' => 'Color personalitzat',\n    'remove_color' => 'Elimina el color',\n    'background_color' => 'Color de fons',\n    'align_left' => 'Alinea a l’esquerra',\n    'align_center' => 'Alinea al centre',\n    'align_right' => 'Alinea a la dreta',\n    'align_justify' => 'Justifica',\n    'list_bullet' => 'Llista de pics',\n    'list_numbered' => 'Llista numerada',\n    'list_task' => 'Llista de caselles',\n    'indent_increase' => 'Augmenta la sagnia',\n    'indent_decrease' => 'Redueix la sagnia',\n    'table' => 'Taula',\n    'insert_image' => 'Insereix una imatge',\n    'insert_image_title' => 'Insereix o edita una imatge',\n    'insert_link' => 'Insereix un enllaç',\n    'insert_link_title' => 'Insereix o edita un enllaç',\n    'insert_horizontal_line' => 'Insereix una línia horitzontal',\n    'insert_code_block' => 'Insereix un bloc de codi',\n    'edit_code_block' => 'Edita el bloc de codi',\n    'insert_drawing' => 'Insereix o edita un diagrama',\n    'drawing_manager' => 'Gestor de diagrames',\n    'insert_media' => 'Insereix o edita un contingut multimèdia',\n    'insert_media_title' => 'Insereix o edita un contingut multimèdia',\n    'clear_formatting' => 'Lleva el format',\n    'source_code' => 'Codi font',\n    'source_code_title' => 'Codi font',\n    'fullscreen' => 'Pantalla completa',\n    'image_options' => 'Opcions d’imatge',\n\n    // Tables\n    'table_properties' => 'Propietats de la taula',\n    'table_properties_title' => 'Propietats de la taula',\n    'delete_table' => 'Suprimeix la taula',\n    'table_clear_formatting' => 'Lleva el format de la taula',\n    'resize_to_contents' => 'Ajusta al contingut',\n    'row_header' => 'Capçalera de la fila',\n    'insert_row_before' => 'Insereix una fila abans',\n    'insert_row_after' => 'Insereix una fila després',\n    'delete_row' => 'Suprimeix la fila',\n    'insert_column_before' => 'Insereix una columna abans',\n    'insert_column_after' => 'Insereix una columna després',\n    'delete_column' => 'Suprimeix la columna',\n    'table_cell' => 'Cel·la',\n    'table_row' => 'Fila',\n    'table_column' => 'Columna',\n    'cell_properties' => 'Propietats de la cel·la',\n    'cell_properties_title' => 'Propietats de la cel·la',\n    'cell_type' => 'Tipus de cel·la',\n    'cell_type_cell' => 'Cel·la',\n    'cell_scope' => 'Àmbit de la cel·la',\n    'cell_type_header' => 'Cel·la de capçalera',\n    'merge_cells' => 'Combina les cel·les',\n    'split_cell' => 'Divideix la cel·la',\n    'table_row_group' => 'Grup de files',\n    'table_column_group' => 'Grup de columnes',\n    'horizontal_align' => 'Alineació horitzontal',\n    'vertical_align' => 'Alineació vertical',\n    'border_width' => 'Amplada de la vora',\n    'border_style' => 'Estil de la vora',\n    'border_color' => 'Color de la vora',\n    'row_properties' => 'Propietats de la fila',\n    'row_properties_title' => 'Propietats de la fila',\n    'cut_row' => 'Retalla la fila',\n    'copy_row' => 'Copia la fila',\n    'paste_row_before' => 'Enganxa la fila abans',\n    'paste_row_after' => 'Enganxa la fila després',\n    'row_type' => 'Tipus de fila',\n    'row_type_header' => 'Capçalera',\n    'row_type_body' => 'Cos',\n    'row_type_footer' => 'Peu',\n    'alignment' => 'Alineació',\n    'cut_column' => 'Retalla la columna',\n    'copy_column' => 'Copia la columna',\n    'paste_column_before' => 'Enganxa la columna abans',\n    'paste_column_after' => 'Enganxa la columna després',\n    'cell_padding' => 'Separació de cel·la',\n    'cell_spacing' => 'Espaiat de cel·la',\n    'caption' => 'Llegenda',\n    'show_caption' => 'Mostra la llegenda',\n    'constrain' => 'Mantén les proporcions',\n    'cell_border_solid' => 'Sòlida',\n    'cell_border_dotted' => 'De punts',\n    'cell_border_dashed' => 'De guions',\n    'cell_border_double' => 'Doble',\n    'cell_border_groove' => 'Vora enfonsada',\n    'cell_border_ridge' => 'Vora ressaltada',\n    'cell_border_inset' => 'Fons enfonsat',\n    'cell_border_outset' => 'Fons ressaltat',\n    'cell_border_none' => 'Sense vora',\n    'cell_border_hidden' => 'Amagada',\n\n    // Images, links, details/summary & embed\n    'source' => 'Font',\n    'alt_desc' => 'Descripció alternativa',\n    'embed' => 'Incrusta',\n    'paste_embed' => 'Enganxa el codi per a incrustar a sota:',\n    'url' => 'URL',\n    'text_to_display' => 'Text per a mostrar',\n    'title' => 'Títol',\n    'browse_links' => 'Explorar enllaços',\n    'open_link' => 'Obre l’enllaç',\n    'open_link_in' => 'Obre l’enllaç…',\n    'open_link_current' => 'A la finestra actual',\n    'open_link_new' => 'En una finestra nova',\n    'remove_link' => 'Lleva l’enllaç',\n    'insert_collapsible' => 'Insereix un bloc desplegable',\n    'collapsible_unwrap' => 'Lleva el bloc desplegable',\n    'edit_label' => 'Edita l’etiqueta',\n    'toggle_open_closed' => 'Obre o tanca',\n    'collapsible_edit' => 'Edita el bloc desplegable',\n    'toggle_label' => 'Commuta l’etiqueta',\n\n    // About view\n    'about' => 'Quant a l’Editor',\n    'about_title' => 'Quant a l’Editor WYSIWYG',\n    'editor_license' => 'Llicència i copyright de l’Editor',\n    'editor_lexical_license' => 'Aquest editor està construït com una bifurcació de :lexicalLink i es distribueix sota la llicència MIT.',\n    'editor_lexical_license_link' => 'Tots els detalls complets de la llicència es poden trobar aquí.',\n    'editor_tiny_license' => 'Aquest editor s’ha creat amb :tinyLink que es proporciona amb la llicència MIT.',\n    'editor_tiny_license_link' => 'Detalls de la llicència i el copyright de TinyMCE.',\n    'save_continue' => 'Desa la pàgina i continua',\n    'callouts_cycle' => '(Continueu prement per a canviar entre tipus)',\n    'link_selector' => 'Enllaç a contingut',\n    'shortcuts' => 'Dreceres de teclat',\n    'shortcut' => 'Drecera',\n    'shortcuts_intro' => 'A l’editor, hi ha disponibles les dreceres de teclat següents:',\n    'windows_linux' => '(Windows o Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Descripció',\n];\n"
  },
  {
    "path": "lang/ca/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Elements creats recentment',\n    'recently_created_pages' => 'Pàgines creades recentment',\n    'recently_updated_pages' => 'Pàgines actualitzades recentment',\n    'recently_created_chapters' => 'Capítols creats recentment',\n    'recently_created_books' => 'Llibres creats recentment',\n    'recently_created_shelves' => 'Prestatges creats recentment',\n    'recently_update' => 'Elements actualitzats recentment',\n    'recently_viewed' => 'Elements vistos recentment',\n    'recent_activity' => 'Activitat recent',\n    'create_now' => 'Crea un element',\n    'revisions' => 'Revisions',\n    'meta_revision' => 'Revisió núm. :revisionCount',\n    'meta_created' => 'S’ha creat :timeLength',\n    'meta_created_name' => ':user l’ha creat :timeLength',\n    'meta_updated' => 'S’ha actualitzat :timeLength',\n    'meta_updated_name' => ':user l’ha actualitzat :timeLength',\n    'meta_owned_name' => 'Propietat de :user',\n    'meta_reference_count' => 'S’hi fa referència en :count element|S’hi fa referència en :count elements',\n    'entity_select' => 'Selecció de l’entitat',\n    'entity_select_lack_permission' => 'No teniu els permisos necessaris per a seleccionar aquest element',\n    'images' => 'Imatges',\n    'my_recent_drafts' => 'Els meus esborranys recents',\n    'my_recently_viewed' => 'Els meus vistos recentment',\n    'my_most_viewed_favourites' => 'Els meus favorits més vistos',\n    'my_favourites' => 'Els meus favorits',\n    'no_pages_viewed' => 'No heu vist cap pàgina',\n    'no_pages_recently_created' => 'No s’ha creat cap pàgina recentment',\n    'no_pages_recently_updated' => 'No s’ha actualitzat cap pàgina recentment',\n    'export' => 'Exporta',\n    'export_html' => 'Fitxer web independent',\n    'export_pdf' => 'Fitxer PDF',\n    'export_text' => 'Fitxer de text sense format',\n    'export_md' => 'Fitxer Markdown',\n    'export_zip' => 'ZIP portable',\n    'default_template' => 'Plantilla de pàgina per defecte',\n    'default_template_explain' => 'Assigna una plantilla de pàgina que s\\'utilitzarà com a contingut predeterminat per a totes les pàgines creades en aquest element. Tingues en compte que això només s\\'utilitzarà si el creador de pàgines té accés a la plantilla de pàgina triada.',\n    'default_template_select' => 'Seleccioneu una plantilla de pàgina',\n    'import' => 'Importar',\n    'import_validate' => 'Validar importació',\n    'import_desc' => 'Importar llibres, capítols i pàgines utilitzant una exportació ZIP portable de la mateixa o una altra instància. Selecciona un arxiu ZIP per continuar. Després que l\\'arxiu s\\'hagi penjat i validat, podrà configurar i confirmar l\\'importació en la següent vista.',\n    'import_zip_select' => 'Seleccioneu un fitxer ZIP per pujar',\n    'import_zip_validation_errors' => 'S\\'han detectat errors al validar l\\'arxiu ZIP proporcionat:',\n    'import_pending' => 'Importació pendent',\n    'import_pending_none' => 'No s\\'han iniciat les importacions.',\n    'import_continue' => 'Continuar importació',\n    'import_continue_desc' => 'Revisa el contingut que s\\'ha d\\'importar de l\\'arxiu ZIP penjat. Quan estigui llest, executa l\\'importació per afegir el seu contingut al sistema. L\\'arxiu d\\'importació ZIP penjat s\\'eliminarà automàticament al finalitzar l\\'importació correctament.',\n    'import_details' => 'Detalls d\\'importació',\n    'import_run' => 'Executar importació',\n    'import_size' => ':size Mida de l\\'arxiu ZIP',\n    'import_uploaded_at' => 'Penjat :relativeTime',\n    'import_uploaded_by' => 'Actualitzat per',\n    'import_location' => 'Importar ubicació',\n    'import_location_desc' => 'Selecciona una ubicació de destí pel contingut importat. Necessitarà els permisos pertinents per crear-lo dins de la ubicació triada.',\n    'import_delete_confirm' => 'Esteu segur que voleu suplir aquesta importació?',\n    'import_delete_desc' => 'Això eliminarà l\\'arxiu ZIP d\\'importació penjat i no es pot desfer.',\n    'import_errors' => 'Importar errors',\n    'import_errors_desc' => 'S\\'han produït els següents errors durant l\\'intent d\\'importació:',\n    'breadcrumb_siblings_for_page' => 'Navegar entre pàgines del mateix nivell',\n    'breadcrumb_siblings_for_chapter' => 'Navegar entre capítols del mateix nivell',\n    'breadcrumb_siblings_for_book' => 'Navegar entre llibres del mateix nivell',\n    'breadcrumb_siblings_for_bookshelf' => 'Navegar entre llibreries del mateix nivell',\n\n    // Permissions and restrictions\n    'permissions' => 'Permisos',\n    'permissions_desc' => 'Configureu aquí els permisos per a sobreescriure els permisos per defecte proporcionats pels rols d’usuari.',\n    'permissions_book_cascade' => 'Els permisos dels llibres s’aplicaran també als capítols i les pàgines que continguin llevat que tinguin els seus propis permisos.',\n    'permissions_chapter_cascade' => 'Els permisos dels capítols s’aplicaran també a les pàgines que continguin llevat que tinguin els seus propis permisos.',\n    'permissions_save' => 'Desa els permisos',\n    'permissions_owner' => 'Propietari',\n    'permissions_role_everyone_else' => 'La resta dels usuaris',\n    'permissions_role_everyone_else_desc' => 'Configureu permisos per a tots els rols que no s’hagin sobreescrit específicament.',\n    'permissions_role_override' => 'Sobreescriu els permisos per al rol',\n    'permissions_inherit_defaults' => 'Hereta la configuració per defecte',\n\n    // Search\n    'search_results' => 'Resultats de la cerca',\n    'search_total_results_found' => 'S’ha trobat :count coincidència|S’han trobat :count coincidències',\n    'search_clear' => 'Esborra la cerca',\n    'search_no_pages' => 'No s’ha trobat cap pàgina que coincideixi amb la cerca',\n    'search_for_term' => 'Cerca :term',\n    'search_more' => 'Més resultats',\n    'search_advanced' => 'Cerca avançada',\n    'search_terms' => 'Termes de cerca',\n    'search_content_type' => 'Tipus de contingut',\n    'search_exact_matches' => 'Coincidències exactes',\n    'search_tags' => 'Cerca d’etiquetes',\n    'search_options' => 'Opcions',\n    'search_viewed_by_me' => 'Vistos per mi',\n    'search_not_viewed_by_me' => 'No vistos per mi',\n    'search_permissions_set' => 'Amb permisos',\n    'search_created_by_me' => 'Creats per mi',\n    'search_updated_by_me' => 'Actualitzats per mi',\n    'search_owned_by_me' => 'En soc propietari',\n    'search_date_options' => 'Opcions de data',\n    'search_updated_before' => 'Actualitzats abans de',\n    'search_updated_after' => 'Actualitzats després de',\n    'search_created_before' => 'Creats abans de',\n    'search_created_after' => 'Creats després de',\n    'search_set_date' => 'Defineix una data',\n    'search_update' => 'Actualitza la cerca',\n\n    // Shelves\n    'shelf' => 'Prestatge',\n    'shelves' => 'Prestatges',\n    'x_shelves' => ':count prestatge|:count prestatges',\n    'shelves_empty' => 'No hi ha cap prestatge',\n    'shelves_create' => 'Crea un prestatge',\n    'shelves_popular' => 'Prestatges populars',\n    'shelves_new' => 'Prestatges nous',\n    'shelves_new_action' => 'Prestatge nou',\n    'shelves_popular_empty' => 'Aquí apareixeran els prestatges més populars.',\n    'shelves_new_empty' => 'Aquí apareixeran els prestatges més recents.',\n    'shelves_save' => 'Desa el prestatge',\n    'shelves_books' => 'Llibres en aquest prestatge',\n    'shelves_add_books' => 'Afegiu llibres en aquest prestatge',\n    'shelves_drag_books' => 'Arrossegueu i deixeu anar llibres a sota per a afegir-los en aquest prestatge',\n    'shelves_empty_contents' => 'Aquest prestatge no té cap llibre',\n    'shelves_edit_and_assign' => 'Editeu el prestatge per a afegir-hi llibres',\n    'shelves_edit_named' => 'Edita el prestatge &laquo;:name&raquo;',\n    'shelves_edit' => 'Edita el prestatge',\n    'shelves_delete' => 'Suprimeix el prestatge',\n    'shelves_delete_named' => 'Suprimeix del prestatge &laquo;:name&raquo;',\n    'shelves_delete_explain' => \"Se suprimirà el prestatge &laquo;:name&raquo;. No se suprimiran els llibres que contingui.\",\n    'shelves_delete_confirmation' => 'Esteu segur que voleu suprimir aquest prestatge?',\n    'shelves_permissions' => 'Permisos del prestatge',\n    'shelves_permissions_updated' => 'S’han actualitzat els permisos del prestatge',\n    'shelves_permissions_active' => 'El prestatge té permisos',\n    'shelves_permissions_cascade_warning' => 'Els permisos dels prestatges no s’aplicaran als llibres que contingui. Això és perquè els llibres poden estar en més d’un prestatge a la vegada. Això no obstant, els permisos es poden copiar als llibres que contingui amb l’opció de sota.',\n    'shelves_permissions_create' => 'Els permisos per a crear prestatges només s’utilitzen per a copiar permisos als llibres que contenen amb l’acció de sota. No controlen els permisos per a crear llibres.',\n    'shelves_copy_permissions_to_books' => 'Copia els permisos als llibres',\n    'shelves_copy_permissions' => 'Copia els permisos',\n    'shelves_copy_permissions_explain' => 'S’aplicaran els permisos del prestatge a tots els llibres que contingui. Abans de continuar assegureu-vos que heu desat els canvis que hàgiu fet als permisos del prestatge.',\n    'shelves_copy_permission_success' => 'S’han copiat els permisos del prestatge a :count llibres',\n\n    // Books\n    'book' => 'Llibre',\n    'books' => 'Llibres',\n    'x_books' => ':count llibre|:count llibres',\n    'books_empty' => 'No hi ha cap llibre',\n    'books_popular' => 'Llibres populars',\n    'books_recent' => 'Llibres recents',\n    'books_new' => 'Llibres nous',\n    'books_new_action' => 'Llibre nou',\n    'books_popular_empty' => 'Aquí apareixeran els llibres més populars.',\n    'books_new_empty' => 'Aquí apareixeran els llibres més recents.',\n    'books_create' => 'Crea un llibre',\n    'books_delete' => 'Suprimeix el llibre',\n    'books_delete_named' => 'Suprimeix el llibre &laquo;:bookName&raquo;',\n    'books_delete_explain' => 'Se suprimirà el llibre &laquo;:bookName&raquo;. Se suprimiran totes les pàgines i capítols.',\n    'books_delete_confirmation' => 'Esteu segur que voleu suprimir aquest llibre?',\n    'books_edit' => 'Edita el llibre',\n    'books_edit_named' => 'Edita el llibre &laquo;:bookName&raquo;',\n    'books_form_book_name' => 'Títol del llibre',\n    'books_save' => 'Desa el llibre',\n    'books_permissions' => 'Permisos del llibre',\n    'books_permissions_updated' => 'S’han actualitzat els permisos',\n    'books_empty_contents' => 'No hi ha cap capítol ni cap pàgina en aquest llibre.',\n    'books_empty_create_page' => 'Crea una pàgina',\n    'books_empty_sort_current_book' => 'Ordena el llibre',\n    'books_empty_add_chapter' => 'Afegeix un capítol',\n    'books_permissions_active' => 'El llibre té permisos',\n    'books_search_this' => 'Cerca en aquest llibre',\n    'books_navigation' => 'Navegació del llibre',\n    'books_sort' => 'Ordena el contingut d’un llibre',\n    'books_sort_desc' => 'Mou capítols i pàgines dins d\\'un llibre per reorganitzar el seu contingut. Es poden afegir altres llibres que permetin moure fàcilment capítols i pàgines entre llibres. De manera opcional, es poden establir regles d\\'ordenació automàtica per ordenar automàticament el contingut d\\'aquest llibre quan hi hagi canvis.',\n    'books_sort_auto_sort' => 'Opció d\\'ordenació automàtica',\n    'books_sort_auto_sort_active' => 'Opció d\\'ordenació activa :sortName',\n    'books_sort_named' => 'Ordena el llibre &laquo;:bookName&raquo;',\n    'books_sort_name' => 'Ordena pel nom',\n    'books_sort_created' => 'Ordena per la data de creació',\n    'books_sort_updated' => 'Ordena per la data d’actualització',\n    'books_sort_chapters_first' => 'Capítols al principi',\n    'books_sort_chapters_last' => 'Capítols al final',\n    'books_sort_show_other' => 'Mostra altres llibres',\n    'books_sort_save' => 'Desa l’ordre nou',\n    'books_sort_show_other_desc' => 'Afegiu llibres per a incloure’ls en l’ordenació i permetre una reorganització entre llibres més senzilla.',\n    'books_sort_move_up' => 'Mou-ho cap amunt',\n    'books_sort_move_down' => 'Mou-ho cap avall',\n    'books_sort_move_prev_book' => 'Mou-ho al llibre anterior',\n    'books_sort_move_next_book' => 'Mou-ho al llibre següent',\n    'books_sort_move_prev_chapter' => 'Mou-ho al capítol anterior',\n    'books_sort_move_next_chapter' => 'Mou-ho al capítol següent',\n    'books_sort_move_book_start' => 'Mou-ho al principi del llibre',\n    'books_sort_move_book_end' => 'Mou-ho al final del llibre',\n    'books_sort_move_before_chapter' => 'Mou-ho a abans del capítol',\n    'books_sort_move_after_chapter' => 'Mou-ho a després del capítol',\n    'books_copy' => 'Copia el llibre',\n    'books_copy_success' => 'S’ha copiat el llibre',\n\n    // Chapters\n    'chapter' => 'Capítol',\n    'chapters' => 'Capítols',\n    'x_chapters' => ':count capítol|:count capítols',\n    'chapters_popular' => 'Capítols populars',\n    'chapters_new' => 'Capítol nou',\n    'chapters_create' => 'Crea un capítol',\n    'chapters_delete' => 'Suprimeix un capítol',\n    'chapters_delete_named' => 'Suprimeix el capítol &laquo;:chapterName&raquo;',\n    'chapters_delete_explain' => 'Se suprimirà el capítol &laquo;:chapterName&raquo;. Se suprimiran totes les pàgines que hi ha al capítol.',\n    'chapters_delete_confirm' => 'Esteu segur que voleu suprimir aquest capítol?',\n    'chapters_edit' => 'Edita el capítol',\n    'chapters_edit_named' => 'Edita el capítol &laquo;:chapterName&raquo;',\n    'chapters_save' => 'Desa el capítol',\n    'chapters_move' => 'Mou el capítol',\n    'chapters_move_named' => 'Mou el capítol &laquo;:chapterName&raquo;',\n    'chapters_copy' => 'Copia el capítol',\n    'chapters_copy_success' => 'S’ha copiat el capítol',\n    'chapters_permissions' => 'Permisos del capítol',\n    'chapters_empty' => 'No hi ha cap pàgina en aquest capítol.',\n    'chapters_permissions_active' => 'El capítol té permisos',\n    'chapters_permissions_success' => 'S’han actualitzat els permisos del capítol',\n    'chapters_search_this' => 'Cerca en aquest capítol',\n    'chapter_sort_book' => 'Ordena el llibre',\n\n    // Pages\n    'page' => 'Pàgina',\n    'pages' => 'Pàgines',\n    'x_pages' => ':count pàgina|:count pàgines',\n    'pages_popular' => 'Pàgines populars',\n    'pages_new' => 'Crea una pàgina',\n    'pages_attachments' => 'Fitxers adjunts',\n    'pages_navigation' => 'Navegació de la pàgina',\n    'pages_delete' => 'Suprimeix la pàgina',\n    'pages_delete_named' => 'Suprimeix la pàgina &laquo;:pageName&raquo;',\n    'pages_delete_draft_named' => 'Suprimeix l’esborrany de pàgina &laquo;:pageName&raquo;',\n    'pages_delete_draft' => 'Suprimeix l’esborrany de pàgina',\n    'pages_delete_success' => 'S’ha suprimit la pàgina',\n    'pages_delete_draft_success' => 'S’ha suprimit l’esborrany de pàgina',\n    'pages_delete_warning_template' => 'Aquesta pàgina està en ús com a plantilla de pàgina predeterminada de llibre o capítol. Aquests llibres o capítols ja no tindran una plantilla de pàgina predeterminada assignada després d\\'eliminar aquesta pàgina.',\n    'pages_delete_confirm' => 'Esteu segur que voleu suprimir aquesta pàgina?',\n    'pages_delete_draft_confirm' => 'Esteu segur que voleu suprimir aquest esborrany de pàgina?',\n    'pages_editing_named' => 'Edició de la pàgina &laquo;:pageName&raquo;',\n    'pages_edit_draft_options' => 'Opcions d’esborrany',\n    'pages_edit_save_draft' => 'Desa l’esborrany',\n    'pages_edit_draft' => 'Edita l’esborrany de pàgina',\n    'pages_editing_draft' => 'Estàs editant l’esborrany',\n    'pages_editing_page' => 'Edita la pàgina',\n    'pages_edit_draft_save_at' => 'S’ha desat l’esborrany a les ',\n    'pages_edit_delete_draft' => 'Suprimeix l’esborrany',\n    'pages_edit_delete_draft_confirm' => 'Esteu segur que voleu suprimir els canvis de l’esborrany de pàgina? Es perdran tots els canvis que hàgiu fet després de la darrera vegada que vau desar la pàgina i l’editor s’actualitzarà amb l’estat de la darrera vegada que es va desar la pàgina que no era esborrany.',\n    'pages_edit_discard_draft' => 'Descarta l’esborrany',\n    'pages_edit_switch_to_markdown' => 'Canvia a l’editor de Markdown',\n    'pages_edit_switch_to_markdown_clean' => '(Contingut net)',\n    'pages_edit_switch_to_markdown_stable' => '(Contingut estable)',\n    'pages_edit_switch_to_wysiwyg' => 'Canvia a l’editor WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg' => 'Canviar al nou editor WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(En Beta Test)',\n    'pages_edit_set_changelog' => 'Registre de canvis',\n    'pages_edit_enter_changelog_desc' => 'Introduïu una descripció breu dels canvis que heu fet',\n    'pages_edit_enter_changelog' => 'Registra un canvi',\n    'pages_editor_switch_title' => 'Canvia l’editor',\n    'pages_editor_switch_are_you_sure' => 'Esteu segur que voleu canviar l’editor d’aquesta pàgina?',\n    'pages_editor_switch_consider_following' => 'Quan canvieu l’editor tingueu en compte que:',\n    'pages_editor_switch_consideration_a' => 'Un cop s’hagi desat, l’editor nou serà el que hagin d’utilitzar tots els editor futurs, incloent-hi aquells que no puguin canviar el tipus d’editor.',\n    'pages_editor_switch_consideration_b' => 'És possible que hi hagi pèrdues en el detall i la sintaxi.',\n    'pages_editor_switch_consideration_c' => 'Els canvis en les etiquetes i el registre de canvis que s’hagin fet després de la darrera vegada que es va desar no es conservaran.',\n    'pages_save' => 'Desa la pàgina',\n    'pages_title' => 'Títol de la pàgina',\n    'pages_name' => 'Nom de la pàgina',\n    'pages_md_editor' => 'Editor',\n    'pages_md_preview' => 'Visualització prèvia',\n    'pages_md_insert_image' => 'Insereix una imatge',\n    'pages_md_insert_link' => 'Insereix un enllaç d’entitat',\n    'pages_md_insert_drawing' => 'Insereix un dibuix',\n    'pages_md_show_preview' => 'Mostra la visualització prèvia',\n    'pages_md_sync_scroll' => 'Sincronitza el desplaçament de la visualització prèvia',\n    'pages_md_plain_editor' => 'Editor de text pla',\n    'pages_drawing_unsaved' => 'S’ha trobat un dibuix sense desar',\n    'pages_drawing_unsaved_confirm' => 'S’han trobat dades d’un dibuix d’un intent anterior de desar un dibuix. Voleu restaurar aquest dibuix no desat per a reprendre’n l’edició?',\n    'pages_not_in_chapter' => 'La pàgina no és un capítol',\n    'pages_move' => 'Mou la pàgina',\n    'pages_copy' => 'Copia la pàgina',\n    'pages_copy_desination' => 'Copia la destinació',\n    'pages_copy_success' => 'S’ha copiat la pàgina',\n    'pages_permissions' => 'Permisos de la pàgina',\n    'pages_permissions_success' => 'S’han actualitzat els permisos de la pàgina',\n    'pages_revision' => 'Revisió',\n    'pages_revisions' => 'Revisions de la pàgina',\n    'pages_revisions_desc' => 'A continuació hi ha la llista de totes les revisions d’aquesta pàgina. Si hi teniu permís, podeu mirar, comparar i restaurar versions antigues de la pàgina. És possible que no hi aparegui tot l’historial de la pàgina ja que, segons la configuració del sistema, és possible que s’hagin suprimit les revisions antigues automàticament.',\n    'pages_revisions_named' => 'Revisions de la pàgina &laquo;:pageName&raquo;',\n    'pages_revision_named' => 'Revisió de la pàgina &laquo;:pageName&raquo;',\n    'pages_revision_restored_from' => 'S’ha restaurat de núm. :id: :summary',\n    'pages_revisions_created_by' => 'Revisor:',\n    'pages_revisions_date' => 'Data de la revisió',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Número de la revisió',\n    'pages_revisions_numbered' => 'Revisió núm. :id',\n    'pages_revisions_numbered_changes' => 'Canvis de la revisió núm. :id',\n    'pages_revisions_editor' => 'Tipus d’editor',\n    'pages_revisions_changelog' => 'Registre de canvis',\n    'pages_revisions_changes' => 'Canvis',\n    'pages_revisions_current' => 'Versió actual',\n    'pages_revisions_preview' => 'Visualització prèvia',\n    'pages_revisions_restore' => 'Restaura',\n    'pages_revisions_none' => 'Aquesta pàgina no té cap revisió',\n    'pages_copy_link' => 'Copia l’enllaç',\n    'pages_edit_content_link' => 'Ves a la secció a l’editor',\n    'pages_pointer_enter_mode' => 'Entra al mode de selecció de secció',\n    'pages_pointer_label' => 'Opcions de la secció de pàgina',\n    'pages_pointer_permalink' => 'Enllaç permanent de la secció de pàgina',\n    'pages_pointer_include_tag' => 'Etiqueta inclou de la secció de pàgina',\n    'pages_pointer_toggle_link' => 'Enllaç permanent. Cliqueu per a mostrar l’etiqueta d’inclusió',\n    'pages_pointer_toggle_include' => 'Etiqueta d’inclusió. Cliqueu per a mostrar l’enllaç permanent',\n    'pages_permissions_active' => 'La pàgina té permisos',\n    'pages_initial_revision' => 'Publicació inicial',\n    'pages_references_update_revision' => 'Actualització automàtica dels enllaços interns',\n    'pages_initial_name' => 'Pàgina nova',\n    'pages_editing_draft_notification' => 'Esteu editant un esborrany que es va desar :timeDiff.',\n    'pages_draft_edited_notification' => 'Des de llavors, s’ha actualitzat la pàgina. Us recomanem que descarteu aquest esborrany.',\n    'pages_draft_page_changed_since_creation' => 'Des que es va crear l’esborrany, la pàgina s’ha actualitzat. Us recomanem que descarteu aquest esborrany o que aneu amb compte de no sobreescriure cap canvi.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count usuaris han començat a editar aquesta pàgina',\n        'start_b' => ':userName ha començat a editar aquesta pàgina',\n        'time_a' => 'des que es va actualitzar aquesta pàgina',\n        'time_b' => 'en els darrers :minCount minuts',\n        'message' => ':start :time. Aneu amb compte de no sobreescriure-us els canvis.',\n    ],\n    'pages_draft_discarded' => 'S’ha descartat l’esborrany. L’editor s’ha actualitzat amb el contingut actual de la pàgina',\n    'pages_draft_deleted' => 'S’ha suprimit l’esborrany! L’editor s’ha actualitzat amb el contingut actual de la pàgina',\n    'pages_specific' => 'Pàgina específica',\n    'pages_is_template' => 'Plantilla de la pàgina',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Commuta la barra lateral',\n    'page_tags' => 'Etiquetes de la pàgina',\n    'chapter_tags' => 'Etiquetes del capítol',\n    'book_tags' => 'Etiquetes del llibre',\n    'shelf_tags' => 'Etiquetes del prestatge',\n    'tag' => 'Etiqueta',\n    'tags' =>  'Etiquetes',\n    'tags_index_desc' => 'Es poden posar etiquetes al contingut per a aconseguir una categorització flexible. Les etiquetes han de tenir una clau i poden o no tenir un valor. Un cop s’hi han posat, es pot cercar el contingut utilitzant el nom o el valor de les etiquetes.',\n    'tag_name' =>  'Nom de l’etiqueta',\n    'tag_value' => 'Valor de l’etiqueta (opcional)',\n    'tags_explain' => \"Poseu alguna etiqueta per a categoritzar millor el contingut. \\n Podeu assignar un valor a l’etiqueta per a aconseguir una organització més detallada.\",\n    'tags_add' => 'Posa-hi una altra etiqueta',\n    'tags_remove' => 'Lleva aquesta etiqueta',\n    'tags_usages' => 'Ús total de les etiquetes',\n    'tags_assigned_pages' => 'Assignades a pàgines',\n    'tags_assigned_chapters' => 'Assignades a capítols',\n    'tags_assigned_books' => 'Assignades a llibres',\n    'tags_assigned_shelves' => 'Assignades a prestatges',\n    'tags_x_unique_values' => ':count valors únics',\n    'tags_all_values' => 'Tots els valors',\n    'tags_view_tags' => 'Mostra les etiquetes',\n    'tags_view_existing_tags' => 'Mostra les etiquetes existents',\n    'tags_list_empty_hint' => 'Podeu posar etiquetes a la barra lateral de l’editor o quan canvieu la informació d’un capítol o d’un prestatge.',\n    'attachments' => 'Fitxers adjunts',\n    'attachments_explain' => 'Pugeu fitxers o afegiu enllaços per a mostrar a la pàgina. Els podreu veure a la barra lateral de la pàgina.',\n    'attachments_explain_instant_save' => 'Els canvis dels fitxers adjunts es desen a l’instant.',\n    'attachments_upload' => 'Puja un fitxer',\n    'attachments_link' => 'Afegeix un enllaç',\n    'attachments_upload_drop' => 'També podeu arrossegar i deixar anar un fitxer aquí per a adjuntar-lo.',\n    'attachments_set_link' => 'Configura l’enllaç',\n    'attachments_delete' => 'Esteu segur que voleu suprimir aquest fitxer adjunt?',\n    'attachments_dropzone' => 'Deixeu-hi anar fitxers per a pujar-los',\n    'attachments_no_files' => 'No s’ha pujat cap fitxer',\n    'attachments_explain_link' => 'Si us estimeu més no pujar un fitxer, podeu afegir un enllaç. Pot ser un enllaç en una altra pàgina o a un fitxer al núvol.',\n    'attachments_link_name' => 'Nom de l’enllaç',\n    'attachment_link' => 'Enllaç del fitxer adjunt',\n    'attachments_link_url' => 'Enllaç al fitxer',\n    'attachments_link_url_hint' => 'URL del lloc o del fitxer.',\n    'attach' => 'Adjunta',\n    'attachments_insert_link' => 'Afegeix un enllaç de fitxer adjunt a la pàgina',\n    'attachments_edit_file' => 'Edita el fitxer',\n    'attachments_edit_file_name' => 'Nom del fitxer',\n    'attachments_edit_drop_upload' => 'Deixeu-hi anar fitxers o cliqueu aquí per a pujar-ne i sobreescriure’ls.',\n    'attachments_order_updated' => 'S’ha actualitzat l’ordre del fitxer adjunt',\n    'attachments_updated_success' => 'S’han actualitzat els detalls dels fitxer adjunt',\n    'attachments_deleted' => 'S’ha suprimit el fitxer adjunt',\n    'attachments_file_uploaded' => 'S’ha pujat el fitxer',\n    'attachments_file_updated' => 'S’ha actualitzat el fitxer',\n    'attachments_link_attached' => 'S’ha adjuntat l’enllaç a la pàgina',\n    'templates' => 'Plantilles',\n    'templates_set_as_template' => 'La pàgina és una plantilla',\n    'templates_explain_set_as_template' => 'Podeu configurar aquesta pàgina com a plantilla perquè s’utilitzi en la creació d’altres pàgines. Els altres usuaris podran utilitzar aquesta plantilla si tenen permisos de visualització d’aquesta pàgina.',\n    'templates_replace_content' => 'Substitueix el contingut de la pàgina',\n    'templates_append_content' => 'Afegeix-ho després del contingut la pàgina',\n    'templates_prepend_content' => 'Afegeix-ho abans del contingut la pàgina',\n\n    // Profile View\n    'profile_user_for_x' => 'Usuari des de fa :time',\n    'profile_created_content' => 'Contingut creat',\n    'profile_not_created_pages' => ':userName no ha creat cap pàgina',\n    'profile_not_created_chapters' => ':userName no ha creat cap capítol',\n    'profile_not_created_books' => ':userName no ha creat cap llibre',\n    'profile_not_created_shelves' => ':userName no ha creat cap prestatge',\n\n    // Comments\n    'comment' => 'Comentari',\n    'comments' => 'Comentaris',\n    'comment_add' => 'Afegeix un comentari',\n    'comment_none' => 'No hi ha comentaris per mostrar',\n    'comment_placeholder' => 'Deixa un comentari aquí',\n    'comment_thread_count' => ':count fil de comentaris|:count fils de comentaris',\n    'comment_archived_count' => ':count Arxivats',\n    'comment_archived_threads' => 'Fils arxivats',\n    'comment_save' => 'Desa el comentari',\n    'comment_new' => 'Crea un comentari',\n    'comment_created' => 'ha comentat :createDiff',\n    'comment_updated' => ':username l’ha actualitzat :updateDiff',\n    'comment_updated_indicator' => 'Actualitzat',\n    'comment_deleted_success' => 'S’ha suprimit el comentari',\n    'comment_created_success' => 'S’ha afegit un comentari',\n    'comment_updated_success' => 'S’ha actualitzat un comentari',\n    'comment_archive_success' => 'Comentari arxivat',\n    'comment_unarchive_success' => 'Comentari desarxivat',\n    'comment_view' => 'Mostra el comentari',\n    'comment_jump_to_thread' => 'Anar al fil',\n    'comment_delete_confirm' => 'Esteu segur que voleu suprimir aquest comentari?',\n    'comment_in_reply_to' => 'En resposta a :commentId',\n    'comment_reference' => 'Referència',\n    'comment_reference_outdated' => '(Obsolet)',\n    'comment_editor_explain' => 'Vet aquí els comentaris que s’han fet en aquesta pàgina. Els comentaris es poden fer i gestionar quan es visualitza la pàgina desada.',\n\n    // Revision\n    'revision_delete_confirm' => 'Esteu segur que voleu suprimir aquesta revisió?',\n    'revision_restore_confirm' => 'Esteu segur que voleu restaurar aquesta revisió? Se substituirà el contingut actual de la pàgina.',\n    'revision_cannot_delete_latest' => 'No es pot suprimir la darrera revisió.',\n\n    // Copy view\n    'copy_consider' => 'Quan copieu contingut, tingueu en compte el següent:',\n    'copy_consider_permissions' => 'No es copiarà la configuració personalitzada de permisos.',\n    'copy_consider_owner' => 'Sereu el propietari de tot el contingut copiat.',\n    'copy_consider_images' => 'Els fitxers d’imatge de la pàgina no es duplicaran i les imatges originals conservaran la relació amb la pàgina a què es van pujar originalment.',\n    'copy_consider_attachments' => 'No es copiaran els fitxers adjunts de la pàgina.',\n    'copy_consider_access' => 'És possible que, en canviar la ubicació, el propietari o els permisos, el contingut esdevingui accessible a usuaris que no hi tenien accés.',\n\n    // Conversions\n    'convert_to_shelf' => 'Converteix en prestatge',\n    'convert_to_shelf_contents_desc' => 'Pots convertir aquest llibre en un prestatge nou amb els mateixos continguts. Els capítols d’aquest llibre es convertiran en llibres. Si aquest llibre conté cap pàgina que no sigui en un capítol, es canviarà el nom del llibre que contindrà aquestes pàgines i el llibre formarà part del prestatge nou.',\n    'convert_to_shelf_permissions_desc' => 'Es copiaran els permisos d’aquest llibre al prestatge nou i a tots els llibres que contingui que no tinguin els seus propis permisos. Tingueu en compte que els permisos dels prestatges no es propaguen als seus continguts com sí que ho fan els dels llibres.',\n    'convert_book' => 'Converteix el llibre',\n    'convert_book_confirm' => 'Esteu segur que voleu convertir aquest llibre?',\n    'convert_undo_warning' => 'Això no es pot desfer fàcilment.',\n    'convert_to_book' => 'Converteix en llibre',\n    'convert_to_book_desc' => 'Podeu convertir aquest capítol en un llibre nou amb els mateixos continguts. Es copiaran els permisos d’aquest capítol al llibre nou, però no es copiaran els permisos heretats del llibre a què pertany cosa que podria canviar-ne el control d’accés.',\n    'convert_chapter' => 'Converteix el capítol',\n    'convert_chapter_confirm' => 'Esteu segur que voleu convertir aquest capítol?',\n\n    // References\n    'references' => 'Referències',\n    'references_none' => 'No hi ha cap referència cap a aquest element.',\n    'references_to_desc' => 'A la llista que hi ha a continuació, hi trobareu tot el contingut que enllaça cap aquest element.',\n\n    // Watch Options\n    'watch' => 'Segueix',\n    'watch_title_default' => 'Preferències predeterminades',\n    'watch_desc_default' => 'Reverteix el seguiment a les preferències de notificació per defecte.',\n    'watch_title_ignore' => 'Ignora',\n    'watch_desc_ignore' => 'Ignora totes les notificacions, incloent-hi les de les preferències de nivell usuari.',\n    'watch_title_new' => 'Pàgines noves',\n    'watch_desc_new' => 'Notifica’m la creació d’una pàgina nova dins d’aquest element.',\n    'watch_title_updates' => 'Actualitzacions de la pàgina',\n    'watch_desc_updates' => 'Notifica’m totes les pàgines noves i totes les actualitzacions de les pàgines.',\n    'watch_desc_updates_page' => 'Notifica’m totes les actualitzacions de les pàgines.',\n    'watch_title_comments' => 'Actualitzacions i comentaris de la pàgina',\n    'watch_desc_comments' => 'Notifica’m totes les pàgines noves, totes les actualitzacions de les pàgines i tots els comentaris nous.',\n    'watch_desc_comments_page' => 'Notifica’m tots les actualitzacions de pàgines i tots els comentaris nous.',\n    'watch_change_default' => 'Canvia les preferències de notificació per defecte',\n    'watch_detail_ignore' => 'S’estan ignorant les notificacions',\n    'watch_detail_new' => 'S’està fent el seguiment de pàgines noves',\n    'watch_detail_updates' => 'S’està fent el seguiment de pàgines noves i les actualitzacions de les pàgines',\n    'watch_detail_comments' => 'S’està fent el seguiment de pàgines noves i les actualitzacions de les pàgines i els comentaris',\n    'watch_detail_parent_book' => 'S’està fent el seguiment a través del llibre de què és part',\n    'watch_detail_parent_book_ignore' => 'S’està ignorant a través del llibre de què és part',\n    'watch_detail_parent_chapter' => 'S’està fent el seguiment a través del capítol de què és part',\n    'watch_detail_parent_chapter_ignore' => 'S’està ignorant a través del capítol de què és part',\n];\n"
  },
  {
    "path": "lang/ca/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'No teniu permís per a accedir a la pàgina sol·licitada.',\n    'permissionJson' => 'No teniu permís per a executar l’acció sol·licitada.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Ja existeix un usuari amb el correu electrònic :email però amb unes credencials diferents.',\n    'auth_pre_register_theme_prevention' => 'El compte d\\'usuari no s\\'ha pogut registrar amb els detalls proporcionats',\n    'email_already_confirmed' => 'L’adreça electrònica ja està confirmada. Proveu d’iniciar la sessió.',\n    'email_confirmation_invalid' => 'Aquest testimoni de confirmació no és vàlid o ja s’ha utilitzat. Proveu de tornar-vos a registrar.',\n    'email_confirmation_expired' => 'Aquest testimoni de confirmació ha caducat. S’ha enviat un altre correu electrònic de confirmació.',\n    'email_confirmation_awaiting' => 'Cal confirmar l’adreça electrònica del compte que utilitzeu',\n    'ldap_fail_anonymous' => 'L’accés LDAP anònim ha fallat',\n    'ldap_fail_authed' => 'L’accés LDAP amb el nom distintiu i la contrasenya proporcionats ha fallat',\n    'ldap_extension_not_installed' => 'L’extensió PHP de l’LDAP no està instal·lada',\n    'ldap_cannot_connect' => 'No s’ha pogut connectar amb el servidor LDAP perquè la connexió inicial ha fallat',\n    'saml_already_logged_in' => 'Ja heu iniciat sessió',\n    'saml_no_email_address' => 'No s’ha pogut trobar una adreça electrònica per a aquest usuari a les dades proporcionades pel sistema d’autenticació extern',\n    'saml_invalid_response_id' => 'Un procés iniciat per aquesta aplicació no reconeix la sol·licitud del sistema d’autenticació extern. Haver navegat enrere després d’iniciar sessió en podria ser la causa.',\n    'saml_fail_authed' => 'No s’ha pogut iniciar sessió amb :system perquè el sistema no ha proporcionat una autorització satisfactòria',\n    'oidc_already_logged_in' => 'Ja heu iniciat sessió',\n    'oidc_no_email_address' => 'No s’ha pogut trobar una adreça electrònica per a aquest usuari a les dades proporcionades pel sistema d’autenticació extern',\n    'oidc_fail_authed' => 'No s’ha pogut iniciar sessió amb :system perquè el sistema no ha proporcionat una autorització satisfactòria',\n    'social_no_action_defined' => 'No s’ha definit cap acció',\n    'social_login_bad_response' => \"S’ha produït un error en l’inici de sessió amb :socialAccount: \\n:error\",\n    'social_account_in_use' => 'Aquest compte de :socialAccount ja s’està utilitzant. Proveu d’iniciar sessió amb :socialAccount.',\n    'social_account_email_in_use' => 'L’adreça electrònica :email ja està en ús. Si ja teniu un compte, podeu connectar-hi el vostre compte de :socialAccount a la configuració del vostre perfil.',\n    'social_account_existing' => 'Aquest compte de :socialAccount ja està associat al vostre perfil.',\n    'social_account_already_used_existing' => 'Aquest compte de :socialAccount ja està associat a un altre usuari.',\n    'social_account_not_used' => 'Aquest compte de :socialAccount no està associat a cap usuari. Associeu-lo a la configuració del vostre perfil. ',\n    'social_account_register_instructions' => 'Si encara no teniu un compte, podeu registrar-vos amb l’opció :socialAccount.',\n    'social_driver_not_found' => 'No s’ha trobat el controlador social',\n    'social_driver_not_configured' => 'La configuració de :socialAccount no és correcta.',\n    'invite_token_expired' => 'Aquest enllaç d’invitació ha caducat. Podeu provar de restablir la contrasenya del vostre compte.',\n    'login_user_not_found' => 'No s\\'ha pogut trobar un usuari per aquesta acció.',\n\n    // System\n    'path_not_writable' => 'No s’ha pogut pujar a :filePath. Assegureu-vos que teniu permisos d’escriptura al servidor.',\n    'cannot_get_image_from_url' => 'No s’ha pogut obtenir la imatge des de :url',\n    'cannot_create_thumbs' => 'El servidor no pot crear miniatures. Assegureu-vos que s’ha instal·lat l’extensió de GD PHP.',\n    'server_upload_limit' => 'El servidor no permet pujades d’aquesta mida. Proveu-ho amb una mida més petita.',\n    'server_post_limit' => 'El servidor no pot rebre la quantitat de dades proporcionades. Proveu-ho amb menys dades o amb un fitxer més petit.',\n    'uploaded'  => 'El servidor no permet pujades d’aquesta mida. Proveu-ho amb un fitxer més petit.',\n\n    // Drawing & Images\n    'image_upload_error' => 'S’ha produït un error en pujar la imatge.',\n    'image_upload_type_error' => 'El tipus d’imatge no és vàlid.',\n    'image_upload_replace_type' => 'Els fitxers d’imatge s’han de substituir per un del mateix tipus',\n    'image_upload_memory_limit' => 'No s’ha pogut pujar la imatge o crear-ne les miniatures a causa dels límits dels recursos del sistema.',\n    'image_thumbnail_memory_limit' => 'No s’han pogut crear mides d’imatge diferents a causa dels límits dels recursos del sistema.',\n    'image_gallery_thumbnail_memory_limit' => 'No s’han pogut crear les miniatures de la galeria a causa dels límits dels recursos del sistema.',\n    'drawing_data_not_found' => 'No s’han pogut pujar les dades del dibuix. És possible que el fitxer del dibuix ja no existeixi o que no tingueu permís per a accedir-hi.',\n\n    // Attachments\n    'attachment_not_found' => 'No s’ha trobat el fitxer adjunt',\n    'attachment_upload_error' => 'S’ha produït un error en pujar el fitxer adjunt',\n\n    // Pages\n    'page_draft_autosave_fail' => 'No s’ha pogut desar l’esborrany. Assegureu-vos que esteu connectat a internet per a desar la pàgina.',\n    'page_draft_delete_fail' => 'No s’ha pogut suprimir l’esborrany de la pàgina i obtenir el contingut de la pàgina desada actual.',\n    'page_custom_home_deletion' => 'No es pot suprimir una pàgina que està definida com a pàgina d’inici.',\n\n    // Entities\n    'entity_not_found' => 'No s’ha trobat l’entitat.',\n    'bookshelf_not_found' => 'No s’ha trobat el prestatge.',\n    'book_not_found' => 'No s’ha trobat el llibre.',\n    'page_not_found' => 'No s’ha trobat la pàgina.',\n    'chapter_not_found' => 'No s’ha trobat el capítol.',\n    'selected_book_not_found' => 'No s’ha trobat el llibre seleccionat.',\n    'selected_book_chapter_not_found' => 'No s’ha trobat el llibre o el capítol seleccionat.',\n    'guests_cannot_save_drafts' => 'Els convidats no poden desar esborranys.',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'No podeu suprimir l’administrador únic.',\n    'users_cannot_delete_guest' => 'No podeu suprimir l’usuari convidat.',\n    'users_could_not_send_invite' => 'No s\\'ha pogut crear l\\'usuari, ja que no s\\'ha pogut enviar el correu d\\'invitació',\n\n    // Roles\n    'role_cannot_be_edited' => 'No es pot editar aquest rol.',\n    'role_system_cannot_be_deleted' => 'No es pot suprimir aquest rol perquè és un rol del sistema.',\n    'role_registration_default_cannot_delete' => 'No es pot suprimir aquest rol mentre estigui configurat com a rol de registre per defecte.',\n    'role_cannot_remove_only_admin' => 'Aquest és l’únic usuari assignat al rol d’administrador. Assigneu el rol d’administrador a un altre usuari abans de provar de suprimir-lo.',\n\n    // Comments\n    'comment_list' => 'S’ha produït un error en obtenir els comentaris.',\n    'cannot_add_comment_to_draft' => 'No es poden afegir comentaris en un esborrany.',\n    'comment_add' => 'S’ha produït un error en afegir o actualitzar el comentari.',\n    'comment_delete' => 'S’ha produït un error en suprimir el comentari.',\n    'empty_comment' => 'No es pot afegir un comentari buit.',\n\n    // Error pages\n    '404_page_not_found' => 'No s’ha trobat la pàgina',\n    'sorry_page_not_found' => 'No s’ha trobat la pàgina que heu cercat.',\n    'sorry_page_not_found_permission_warning' => 'Si la pàgina existeix, és possible que no tingueu permís per a accedir-hi.',\n    'image_not_found' => 'No s’ha trobat la imatge',\n    'image_not_found_subtitle' => 'No s’ha trobat la imatge que heu cercat.',\n    'image_not_found_details' => 'És possible que s’hagi suprimit.',\n    'return_home' => 'Torna a la pàgina d’inici',\n    'error_occurred' => 'S’ha produït un error.',\n    'app_down' => ':appName està fora de servei.',\n    'back_soon' => 'Aviat ho arreglarem.',\n\n    // Import\n    'import_zip_cant_read' => 'No es pot llegir el fitxer ZIP.',\n    'import_zip_cant_decode_data' => 'No s\\'ha pogut trobar i descodificar el fitxer data.json en el fitxer ZIP.',\n    'import_zip_no_data' => 'Les dades del fitxer ZIP no contenen cap llibre, capítol o contingut de pàgina.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Error en validar la importació del ZIP amb els errors:',\n    'import_zip_failed_notification' => 'Error en importar l\\'arxiu ZIP.',\n    'import_perms_books' => 'Li falten els permisos necessaris per crear llibres.',\n    'import_perms_chapters' => 'Li falten els permisos necessaris per crear capítols.',\n    'import_perms_pages' => 'Li falten els permisos necessaris per crear pàgines.',\n    'import_perms_images' => 'Li falten els permisos necessaris per crear imatges.',\n    'import_perms_attachments' => 'Li falten els permisos necessaris per crear adjunts.',\n\n    // API errors\n    'api_no_authorization_found' => 'No s’ha trobat cap testimoni d’autorització en aquesta sol·licitud.',\n    'api_bad_authorization_format' => 'S’ha trobat un testimoni d’autorització en aquesta sol·licitud però no tenia el format correcte.',\n    'api_user_token_not_found' => 'No s’ha trobat cap testimoni d’API per al testimoni d’autorització proporcionat.',\n    'api_incorrect_token_secret' => 'El secret proporcionat per al testimoni d’API utilitzat no és correcte.',\n    'api_user_no_api_permission' => 'El propietari del testimoni API utilitzat no té permís per a fer crides a l’API.',\n    'api_user_token_expired' => 'El testimoni d’autorització utilitzat ha caducat.',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'S’ha produït un error en enviar el correu electrònic de prova:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'L’URL no coincideix amb els amfitrions SSR configurats permesos.',\n];\n"
  },
  {
    "path": "lang/ca/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'S’ha fet un comentari a la pàgina :pageName.',\n    'new_comment_intro' => 'S’ha fet un comentari nou en una pàgina a :appName.',\n    'new_page_subject' => 'S’ha creat la pàgina :pageName',\n    'new_page_intro' => 'S’ha creat una pàgina nova a :appName.',\n    'updated_page_subject' => 'S’ha actualitzat la pàgina :pageName',\n    'updated_page_intro' => 'S’ha actualitzat una pàgina a :appName.',\n    'updated_page_debounce' => 'Per a evitar que s’acumulin les notificacions, durant un temps no se us notificarà cap canvi fet en aquesta pàgina pel mateix usuari.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Nom de la pàgina:',\n    'detail_page_path' => 'Ruta de la pàgina:',\n    'detail_commenter' => 'Autor del comentari:',\n    'detail_comment' => 'Comentari:',\n    'detail_created_by' => 'Creada per:',\n    'detail_updated_by' => 'Actualitzada per:',\n\n    'action_view_comment' => 'Mostra el comentari',\n    'action_view_page' => 'Mostra la pàgina',\n\n    'footer_reason' => 'Heu rebut aquesta notificació perquè :link inclouen aquest tipus d’activitat per a aquest element.',\n    'footer_reason_link' => 'les vostres preferències de notificació',\n];\n"
  },
  {
    "path": "lang/ca/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Anterior',\n    'next'     => 'Següent &raquo;',\n\n];\n"
  },
  {
    "path": "lang/ca/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Les contrasenyes han de tenir almenys 8 caràcters i han de coincidir amb la confirmació de contrasenya.',\n    'user' => \"No es pot trobar un usuari amb aquesta adreça electrònica.\",\n    'token' => 'El testimoni de restabliment de la contrasenya no és vàlid per a aquesta adreça electrònica.',\n    'sent' => 'Us hem enviat un enllaç per a restablir la contrasenya!',\n    'reset' => 'S’ha reinicialitzat la contrasenya.',\n\n];\n"
  },
  {
    "path": "lang/ca/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'El meu compte',\n\n    'shortcuts' => 'Dreceres de teclat',\n    'shortcuts_interface' => 'Preferències de dreceres de teclat de la interfície d’usuari',\n    'shortcuts_toggle_desc' => 'Configureu les dreceres de teclat de la interfície d’usuari del sistema utilitzades per a la navegació i les accions.',\n    'shortcuts_customize_desc' => 'Podeu personalitzar cadascuna de les dreceres a continuació. seleccionant el camp d’entrada de cada drecera i prement la combinació de tecles que vulgueu.',\n    'shortcuts_toggle_label' => 'Activa les dreceres de teclat',\n    'shortcuts_section_navigation' => 'Navegació',\n    'shortcuts_section_actions' => 'Accions habituals',\n    'shortcuts_save' => 'Desa les dreceres',\n    'shortcuts_overlay_desc' => 'Nota: Quan les dreces estan activades hi haurà disponible una ajuda que es pot obtenir prement &laquo;?&raquo; que ressaltarà les dreceres disponibles a la pantalla que s’estigui visualitzant.',\n    'shortcuts_update_success' => 'S’han actualitzat les preferències de drecera.',\n    'shortcuts_overview_desc' => 'Gestioneu les dreceres que s’utilitzen per a navegar per la interfície d’usuari.',\n\n    'notifications' => 'Preferències de les notificacions',\n    'notifications_desc' => 'Gestioneu les notificacions de correu electrònic que rebreu quan es facin certes activitats.',\n    'notifications_opt_own_page_changes' => 'Notifica’m els canvis a les meves pàgines.',\n    'notifications_opt_own_page_comments' => 'Notifica’m la creació de comentaris a les meves pàgines.',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Notifica’m les respostes als meus comentaris.',\n    'notifications_save' => 'Desa les preferències',\n    'notifications_update_success' => 'S’han actualitzat les preferències de notificació',\n    'notifications_watched' => 'Elements seguits i ignorats',\n    'notifications_watched_desc' => 'A continuació hi ha els elements que tenen preferències de seguiment personalitzades. Per a actualitzar-les, accediu a l’element i configureu-les a la barra lateral.',\n\n    'auth' => 'Accés i seguretat',\n    'auth_change_password' => 'Canvia la contrasenya',\n    'auth_change_password_desc' => 'Canvieu la contrasenya que s’utilitzarà per a iniciar sessió a l’aplicació. Ha de tenir com a mínim 8 caràcters.',\n    'auth_change_password_success' => 'S’ha actualitzat la contrasenya.',\n\n    'profile' => 'Informació del perfil',\n    'profile_desc' => 'Gestioneu la informació del vostre compte que representa com us veuran els altres usuaris a més de la informació que s’utilitza per a la comunicació i la personalització del sistema.',\n    'profile_view_public' => 'Mostra’m el perfil públic',\n    'profile_name_desc' => 'Configureu el nom públic que veuran els altres usuaris a les activitats que feu i al vostre contingut.',\n    'profile_email_desc' => 'Aquesta serà l’adreça electrònica on s’enviaran les notificacions i que, segons l’autenticació del sistema activada, s’utilitzarà per a iniciar sessió.',\n    'profile_email_no_permission' => 'No teniu permís per a canviar la vostra adreça electrònica. Si voleu canviar-la, demaneu-ho a un administrador.',\n    'profile_avatar_desc' => 'Seleccioneu una imatge que us representi davant dels altres usuaris. És millor que sigui una imatge quadrada de 256px de costat.',\n    'profile_admin_options' => 'Preferències d’administrador',\n    'profile_admin_options_desc' => 'Podeu trobar preferències addicionals d’administrador pel vostre compte, com ara les que gestionen els rols d’usuari, a &laquo;Preferències &rsaquo; Usuaris&raquo;.',\n\n    'delete_account' => 'Suprimeix el compte',\n    'delete_my_account' => 'Suprimeix el meu compte',\n    'delete_my_account_desc' => 'Se suprimirà completament del sistema el vostre compte d’usuari. No podreu recuperar el compte ni revertir-ne la supressió. Es conservarà el contingut que hàgiu creat, com ara les pàgines o les imatges que hàgiu pujat.',\n    'delete_my_account_warning' => 'Esteu segur que voleu suprimir el vostre compte?',\n];\n"
  },
  {
    "path": "lang/ca/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Configuració',\n    'settings_save' => 'Guardar configuració',\n    'system_version' => 'Versió de sistema',\n    'categories' => 'Categories',\n\n    // App Settings\n    'app_customization' => 'Personalització',\n    'app_features_security' => 'Funcions i seguretat',\n    'app_name' => 'Nom de l’aplicació',\n    'app_name_desc' => 'El nom es mostra a la capçalera i als correus electrònics enviats pel sistema.',\n    'app_name_header' => 'Mostra el nom a la capçalera',\n    'app_public_access' => 'Accés públic',\n    'app_public_access_desc' => 'Si activeu aquesta opció permetrà als visitants, accedir a la vostra instància del BookStack sense iniciar sessió.',\n    'app_public_access_desc_guest' => 'L’accés per als visitants públics es pot gestionar amb l’usuari Convidat.',\n    'app_public_access_toggle' => 'Permet l’accés públic',\n    'app_public_viewing' => 'Esteu segur que voleu permetre l’accés públic?',\n    'app_secure_images' => 'Pujada d’imatges amb més seguretat',\n    'app_secure_images_toggle' => 'Habilita la pujada d’imatges amb més seguretat',\n    'app_secure_images_desc' => 'Per raons de rendiment, totes les imatges són públiques. Aquesta opció afegeix una cadena aleatòria que és difícil d’endevinar al davant de l’URL de les imatges. Assegureu-vos que els índex de carpetes estan desactivats perquè no s’hi pugui accedir fàcilment.',\n    'app_default_editor' => 'Editor de pàgines per defecte',\n    'app_default_editor_desc' => 'Seleccioneu quin editor s’utilitzarà per defecte quan s’editin pàgines noves. Això es pot sobreescriure a nivell de pàgina si els permisos ho permeten.',\n    'app_custom_html' => 'Contingut personalitzat a la capçalera HTML',\n    'app_custom_html_desc' => 'El contingut que s’afegeixi aquí s’inserirà al final de la secció <head> de cada pàgina. Això permet sobreescriure els estils o afegir codi d’analítiques web.',\n    'app_custom_html_disabled_notice' => 'El contingut personalitzat a la capçalera HTML està desactivat en aquesta pàgina perquè es puguin revertir els canvis que trenquin el lloc web.',\n    'app_logo' => 'Logotip de l’aplicació',\n    'app_logo_desc' => 'El logotip s’utilitzarà a la barra de la capçalera de l’aplicació, entre d’altres llocs. La imatge ha de tenir 86px d’alçada. Les imatges més grans es reduiran.',\n    'app_icon' => 'Icona de l’aplicació',\n    'app_icon_desc' => 'La icona s’utilitzarà a les pestanyes del navegador i a les icones de les adreces. La imatge ha de ser un PNG quadrat de 256px de costat.',\n    'app_homepage' => 'Pàgina d’inici de l’aplicació',\n    'app_homepage_desc' => 'Seleccioneu una vista per a mostrar a la pàgina d’inici en comptes de la vista per defecte. Els permisos de pàgina s’ignoren a les pàgines seleccionades.',\n    'app_homepage_select' => 'Seleccioneu una pàgina',\n    'app_footer_links' => 'Enllaços del peu de pàgina',\n    'app_footer_links_desc' => 'Afegiu enllaços per a mostrar al peu de pàgina. Els enllaços es mostraran al final de la majoria de les pàgines, incloent-hi les pàgines per a les que no es requereix iniciar sessió. Podeu utilitzar una etiqueta &laquo;trans::<key>&raquo; per a utilitzar traduccions definides pel sistema. Per exemple: Si utilitzeu &laquo;trans::common.privacy_policy&raquo; es mostrarà la traducció de &laquo;Privacy Policy&raquo; i si utilitzeu &laquo;trans::common.terms_of_service&raquo; es mostrarà la traducció de &laquo;Terms of Service&raquo;.',\n    'app_footer_links_label' => 'Text de l’enllaç',\n    'app_footer_links_url' => 'URL de l’enllaç',\n    'app_footer_links_add' => 'Afegeix l’enllaç en el peu de pàgina',\n    'app_disable_comments' => 'Desactiva els comentaris',\n    'app_disable_comments_toggle' => 'Desactiva els comentaris',\n    'app_disable_comments_desc' => 'Desactivarà els comentaris a totes les pàgines de l’aplicació. <br> Els comentaris existents no es mostraran.',\n\n    // Color settings\n    'color_scheme' => 'Esquema de colors de l’aplicació',\n    'color_scheme_desc' => 'Configureu els colors que s’utilitzaran a la interfície d’usuari de l’aplicació. Podeu configurar els colors pel mode clar o el mode fosc per separat perquè escaigui millor al tema i assegurar la llegibilitat.',\n    'ui_colors_desc' => 'Configureu el color principal de l’aplicació i el color per defecte dels enllaços. El color principal s’utilitza al bàner de la capçalera, els botons i les decoracions de la interfície. El color per defecte dels enllaços s’utilitza a les accions i els enllaços de text, tant al contingut escrit com a la interfície de l’aplicació.',\n    'app_color' => 'Color principal',\n    'link_color' => 'Color per defecte dels enllaços',\n    'content_colors_desc' => 'Configureu els colors per a tots els elements en la jerarquia de l’organització de la pàgina. És recomanable que trieu uns colors amb una brillantor similar a la dels colors per defecte per assegurar la llegibilitat.',\n    'bookshelf_color' => 'Color dels prestatges',\n    'book_color' => 'Color dels llibres',\n    'chapter_color' => 'Color dels capítols',\n    'page_color' => 'Color de les pàgines',\n    'page_draft_color' => 'Color de les pàgines d’esborrany',\n\n    // Registration Settings\n    'reg_settings' => 'Registre',\n    'reg_enable' => 'Activa el registre d’usuaris',\n    'reg_enable_toggle' => 'Activa el registre d’usuaris',\n    'reg_enable_desc' => 'Quan el registre està activat, els usuaris es podran registrar com a usuari de l’aplicació. Un cop registrat, se’ls assignarà un rol d’usuari per defecte únic.',\n    'reg_default_role' => 'Rol d’usuari per defecte després del registre.',\n    'reg_enable_external_warning' => 'S’ignorarà l’opció de sobre quan l’autenticació LDAP or SAML estigui activada. Es crearan automàticament comptes d’usuari per als membres que encara no ho siguin si no és possible l’autenticació amb els sistema d’autenticació extern.',\n    'reg_email_confirmation' => 'Correu electrònic de confirmació',\n    'reg_email_confirmation_toggle' => 'Requereix un correu electrònic de confirmació',\n    'reg_confirm_email_desc' => 'Si s’utilitza la restricció de dominis es requerirà un correu electrònic de confirmació i s’ignorarà aquesta opció.',\n    'reg_confirm_restrict_domain' => 'Restricció de dominis',\n    'reg_confirm_restrict_domain_desc' => 'Introduïu una llista separada per comes dels dominis a què voleu restringir el registre. S’enviarà un correu electrònic als usuaris perquè confirmin la seva adreça electrònica abans que puguin interactuar amb l’aplicació. <br> Tingueu en compte que els usuaris podran canviar l’adreça electrònica un cop s’hagin registrat.',\n    'reg_confirm_restrict_domain_placeholder' => 'No hi ha cap restricció',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Selecciona la regla d\\'ordenació predeterminada per aplicar a nous llibres. Això no afectarà els llibres existents, i pot ser anul·lat per llibre.',\n    'sorting_rules' => 'Regles d\\'ordenació',\n    'sorting_rules_desc' => 'Són operacions d\\'ordenació predefinides que es poden aplicar al contingut en el sistema.',\n    'sort_rule_assigned_to_x_books' => 'Assignat a :count llibre | Assignat a :count llibres',\n    'sort_rule_create' => 'Crear regla d\\'ordenació',\n    'sort_rule_edit' => 'Editar regla d\\'ordenació',\n    'sort_rule_delete' => 'Eliminar regla d\\'ordenació',\n    'sort_rule_delete_desc' => 'Eliminar aquesta regla d\\'ordenació del sistema. Els llibres que utilitzin aquest tipus, es revertiran a l\\'ordenació manual.',\n    'sort_rule_delete_warn_books' => 'Aquesta regla d\\'ordenació s\\'utilitza actualment en :count llibre(s). Està segur que vol eliminar-la?',\n    'sort_rule_delete_warn_default' => 'Aquesta regla d\\'ordenació s\\'utilitza actualment com a predeterminada per als llibres. Està segur que vol eliminar-la?',\n    'sort_rule_details' => 'Detalls de la regla d\\'ordenació',\n    'sort_rule_details_desc' => 'Defineix un nom per aquesta regla d\\'ordenació, que apareixerà a les llistes quan els usuaris estiguin seleccionant un ordre.',\n    'sort_rule_operations' => 'Operacions d\\'ordenació',\n    'sort_rule_operations_desc' => 'Configura les accions d\\'ordenació a realitzar movent-les de la llista d\\'operacions disponibles. En utilitzar-se, les operacions s\\'aplicaran en ordre, de dalt cap a baix. Qualsevol canvi realitzat aquí, s\\'aplicarà a tots els llibres assignats en guardar.',\n    'sort_rule_available_operations' => 'Operacions disponibles',\n    'sort_rule_available_operations_empty' => 'No hi ha operacions pendents',\n    'sort_rule_configured_operations' => 'Operacions configurades',\n    'sort_rule_configured_operations_empty' => 'Arrossegar/afegir operacions des de la llista d\\'Operacions Disponibles',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Nom - Alfabètic',\n    'sort_rule_op_name_numeric' => 'Nom - Numèric',\n    'sort_rule_op_created_date' => 'Data de creació',\n    'sort_rule_op_updated_date' => 'Data d\\'actualització',\n    'sort_rule_op_chapters_first' => 'Capítols a l\\'inici',\n    'sort_rule_op_chapters_last' => 'Capítols al final',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Manteniment',\n    'maint_image_cleanup' => 'Neteja d’imatges',\n    'maint_image_cleanup_desc' => 'Escanegeu les pàgines i les revisions per a comprovar quines imatges o dibuixos s’utilitzen i quins no. Assegureu-vos de crear una còpia de seguretat completa de la base de dades i de les imatges abans d’executar-la.',\n    'maint_delete_images_only_in_revisions' => 'Suprimiu també les imatges que només existeixen en revisions de pàgina antigues.',\n    'maint_image_cleanup_run' => 'Executa la neteja',\n    'maint_image_cleanup_warning' => 'Imatges que potencialment no s’utilitzen: :count. Esteu segur que voleu suprimir aquestes imatges?',\n    'maint_image_cleanup_success' => 'Imatges que potencialment no s’utilitzen que s’han suprimit: :count.',\n    'maint_image_cleanup_nothing_found' => 'No s’ha trobat cap imatge que no s’utilitzi. No s’ha suprimit res.',\n    'maint_send_test_email' => 'Envia un correu electrònic de prova.',\n    'maint_send_test_email_desc' => 'S’enviarà un correu electrònic de prova a l’adreça electrònica que figura al vostre perfil.',\n    'maint_send_test_email_run' => 'Envia el correu electrònic de prova',\n    'maint_send_test_email_success' => 'S’ha enviat un correu electrònic a :address',\n    'maint_send_test_email_mail_subject' => 'Correu electrònic de prova',\n    'maint_send_test_email_mail_greeting' => 'Sembla que l’enviament de correus electrònics funciona.',\n    'maint_send_test_email_mail_text' => 'Enhorabona! Que hagis rebut aquesta notificació de correu electrònic vol dir que la vostra configuració de correu electrònic és correcta.',\n    'maint_recycle_bin_desc' => 'Els prestatges, els llibres, els capítols i les pàgines suprimides s’envien a la paperera perquè es puguin restaurar o suprimir permanentment. És possible que els elements que fa més temps que són a la paperera s’eliminin automàticament al cap d’un temps segons la configuració del sistema.',\n    'maint_recycle_bin_open' => 'Obre la paperera',\n    'maint_regen_references' => 'Regenera les referències',\n    'maint_regen_references_desc' => 'Aquesta acció reconstruirà l’índex de referències creuades entre elements a la base de dades. Normalment es fa automàticament però aquesta acció és útil per a indexar contingut antic o contingut afegit a través de mètodes no oficials.',\n    'maint_regen_references_success' => 'S’ha regenerat l’índex de referències.',\n    'maint_timeout_command_note' => 'Nota: És possible que aquesta acció trigui a executar-se cosa que pot provocar que s’excedeixi el temps d’espera en alguns entorns web. Com a alternativa, podeu executar aquesta acció amb una ordre del terminal.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Paperera',\n    'recycle_bin_desc' => 'Aquí podeu restaurar els elements que s’han suprimit o suprimir-los permanentment del sistema. Aquesta llista no està filtrada, a diferència de llistes d’activitat similars on s’apliquen filtres de permisos.',\n    'recycle_bin_deleted_item' => 'Element suprimit',\n    'recycle_bin_deleted_parent' => 'Pare',\n    'recycle_bin_deleted_by' => 'Suprimit per',\n    'recycle_bin_deleted_at' => 'Hora de supressió',\n    'recycle_bin_permanently_delete' => 'Suprimit permanentment',\n    'recycle_bin_restore' => 'Restaura',\n    'recycle_bin_contents_empty' => 'La paperera és buida',\n    'recycle_bin_empty' => 'Buida la paperera',\n    'recycle_bin_empty_confirm' => 'Se suprimiran permanentment tots els elements que hi ha a la paperera incloent-hi el contingut que hi hagi a cada element. Esteu segur que voleu buidar la paperera?',\n    'recycle_bin_destroy_confirm' => 'Aquesta acció suprimirà del sistema de manera permanent aquest element, juntament amb tots els fills que es llisten a sota, i no podreu restaurar aquest contingut. Segur que voleu suprimir de manera permanent aquest element?',\n    'recycle_bin_destroy_list' => 'Elements per destruir',\n    'recycle_bin_restore_list' => 'Elements per restaurar',\n    'recycle_bin_restore_confirm' => 'Aquesta acció restaurarà l’element suprimit, incloent-hi els elements fills, a la seva ubicació original. Si la ubicació original s’ha suprimit i és a la paperera, l’element pare també s’haurà de restaurar.',\n    'recycle_bin_restore_deleted_parent' => 'El pare d’aquest element també s’ha suprimit. L’element continuarà suprimit fins que no se’n restauri també el pare.',\n    'recycle_bin_restore_parent' => 'Restaura el pare',\n    'recycle_bin_destroy_notification' => 'Elements suprimits de la paperera: :count.',\n    'recycle_bin_restore_notification' => 'Elements restaurats de la paperera: :count.',\n\n    // Audit Log\n    'audit' => 'Registre d’auditoria',\n    'audit_desc' => 'El registre d\\'auditoria mostra una llista de les activitats de què es fa un seguiment. Aquesta llista no està filtrada, a diferència de llistes d’activitat similars on s’apliquen filtres de permisos.',\n    'audit_event_filter' => 'Filtre d’esdeveniments',\n    'audit_event_filter_no_filter' => 'Sense filtre',\n    'audit_deleted_item' => 'Element suprimit',\n    'audit_deleted_item_name' => 'Nom: :name',\n    'audit_table_user' => 'Usuari',\n    'audit_table_event' => 'Esdeveniment',\n    'audit_table_related' => 'Element relacionat o detall',\n    'audit_table_ip' => 'Adreça IP',\n    'audit_table_date' => 'Data de l’activitat',\n    'audit_date_from' => 'Des de',\n    'audit_date_to' => 'Fins a',\n\n    // Role Settings\n    'roles' => 'Rols',\n    'role_user_roles' => 'Rols d’usuari',\n    'roles_index_desc' => 'Els rols d’usuari s’utilitzen per a agrupar usuaris i donar-los permisos conjuntament. Un usuari que tingui més d’un rol acumularà els privilegis que s’atorguin a tots els rols i n’heretarà els permisos.',\n    'roles_x_users_assigned' => ':count usuari assignat|:count usuaris assignats',\n    'roles_x_permissions_provided' => ':count permís|:count permisos',\n    'roles_assigned_users' => 'Usuaris assignats',\n    'roles_permissions_provided' => 'Permisos atorgats',\n    'role_create' => 'Crea un rol',\n    'role_delete' => 'Suprimeix un rol',\n    'role_delete_confirm' => 'Se suprimirà el rol &laquo;:roleName&raquo;.',\n    'role_delete_users_assigned' => 'Usuaris assignats en aquest rol: :userCount. Si voleu migrar aquests usuaris a un altre rol, seleccioneu-ne un dels de sota.',\n    'role_delete_no_migration' => \"No migris els usuaris\",\n    'role_delete_sure' => 'Esteu segur que voleu suprimir aquest rol?',\n    'role_edit' => 'Edita el rol',\n    'role_details' => 'Detalls del rol',\n    'role_name' => 'Nom del rol',\n    'role_desc' => 'Descripció del rol',\n    'role_mfa_enforced' => 'Autenticació multifactorial requerida',\n    'role_external_auth_id' => 'Identificadors d’autenticació externa',\n    'role_system' => 'Permisos del sistema',\n    'role_manage_users' => 'Gestió dels usuaris',\n    'role_manage_roles' => 'Gestió dels rols i els seus permisos',\n    'role_manage_entity_permissions' => 'Gestió de tots els permisos de llibres, capítols i pàgines',\n    'role_manage_own_entity_permissions' => 'Gestió dels permisos als seus llibres, capítols i pàgines',\n    'role_manage_page_templates' => 'Gestió de les plantilles de pàgina',\n    'role_access_api' => 'Accés a l’API del sistema',\n    'role_manage_settings' => 'Gestió de la configuració de l’aplicació',\n    'role_export_content' => 'Exportació de contingut',\n    'role_import_content' => 'Importar contingut',\n    'role_editor_change' => 'Canvi de l’editor de pàgina',\n    'role_notifications' => 'Recepció i gestió de notificacions',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Permisos de recursos',\n    'roles_system_warning' => 'Tingueu en compte que l’accés a qualsevol dels tres permisos de dalt permeten que l’usuari canviï els seus privilegis i els privilegis d’altres usuaris. Assigneu rols d’usuari amb aquests permisos només a usuaris de confiança.',\n    'role_asset_desc' => 'Aquests permisos controlen l’accés per defecte als recursos del sistema. El permisos dels llibres, capítols i pàgines sobreescriuran aquests permisos.',\n    'role_asset_admins' => 'Als administradors se’ls dona accés automàticament a tot el contingut però aquestes opcions mostren o amaguen opcions de la interfície d’usuari.',\n    'role_asset_image_view_note' => 'Això té relació amb la visibilitat al gestor d’imatges. L’accés a les imatges pujades dependrà de l’opció d’emmagatzematge d’imatges dels sistema.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'Tot',\n    'role_own' => 'Propi',\n    'role_controlled_by_asset' => 'Controlat pel recurs a què estan pujats',\n    'role_save' => 'Desa el rol',\n    'role_users' => 'Usuaris assignats en aquest rol',\n    'role_users_none' => 'No hi ha cap usuari assignat en aquest rol',\n\n    // Users\n    'users' => 'Usuaris',\n    'users_index_desc' => 'Creeu i gestioneu comptes d’usuari individuals. Els comptes d’usuari s’utilitzen per als inicis de sessió i les atribucions de contingut i activitat. Els permisos d’accés es basen fonamentalment en el rol d’usuari però la propietat del contingut, entre d’altres, també afecta els permisos i l’accés.',\n    'user_profile' => 'Perfil d’usuari',\n    'users_add_new' => 'Afegeix un usuari nou',\n    'users_search' => 'Cerca usuaris',\n    'users_latest_activity' => 'Activitat més recent',\n    'users_details' => 'Detalls de l’usuari',\n    'users_details_desc' => 'Configureu un nom i una adreça electrònica per a aquest usuari. L’adreça electrònica s’usarà per a iniciar sessió a l’aplicació.',\n    'users_details_desc_no_email' => 'Configureu un nom per a aquest usuari perquè se’l pugui reconèixer.',\n    'users_role' => 'Rols d’usuari',\n    'users_role_desc' => 'Seleccioneu quins rols s’assignaran en aquest usuari. Si a un usuari se li assignen més d’un rol acumularà els privilegis que s’atorguin a tots els rols i n’heretarà les els permisos.',\n    'users_password' => 'Contrasenya d’usuari',\n    'users_password_desc' => 'Configureu la contrasenya que s’utilitzarà per a iniciar sessió a l’aplicació. Ha de tenir com a mínim 8 caràcters.',\n    'users_send_invite_text' => 'Podeu configurar la contrasenya o enviar un correu electrònic que convidi a l’usuari a configurar-la.',\n    'users_send_invite_option' => 'Envia el correu electrònic',\n    'users_external_auth_id' => 'Identificador d’autenticació extern',\n    'users_external_auth_id_desc' => 'Quan s’utilitza un sistema d’autenticació extern (com ara SAML2, OIDC or LDAP) l’identificador enllaça l’usuari amb el compte dels sistema d’autenticació. Podeu ignorar aquest camp si utilitzeu l’autenticació amb correu electrònic per defecte.',\n    'users_password_warning' => 'Ompliu els camps de sota només si voleu canviar la contrasenya d’aquest usuari.',\n    'users_system_public' => 'Aquest usuari representa qualsevol usuari convidat que visiti la instància. No es pot utilitzar per a iniciar sessió sinó que s’assigna automàticament.',\n    'users_delete' => 'Suprimeix l’usuari',\n    'users_delete_named' => 'Suprimeix l’usuari :userName',\n    'users_delete_warning' => 'Se suprimirà l’usuari &laquo;:userName&raquo; del sistema.',\n    'users_delete_confirm' => 'Esteu segur que voleu suprimir aquest usuari?',\n    'users_migrate_ownership' => 'Migració de la propietat',\n    'users_migrate_ownership_desc' => 'Si voleu que un altre usuari esdevingui el propietari de tots els elements d’aquest usuari, seleccioneu-ne un.',\n    'users_none_selected' => 'No s’ha seleccionat cap usuari',\n    'users_edit' => 'Edita l’usuari',\n    'users_edit_profile' => 'Edita el perfil',\n    'users_avatar' => 'Avatar',\n    'users_avatar_desc' => 'Seleccioneu una imatge per a representar aquest usuari. Ha de ser una imatge quadrada de 256px de costat, aproximadament.',\n    'users_preferred_language' => 'Llengua',\n    'users_preferred_language_desc' => 'Canvia la llengua en què es mostra la interfície d’usuari de l’aplicació. No afectarà el contingut creat pels usuaris.',\n    'users_social_accounts' => 'Comptes a les xarxes socials',\n    'users_social_accounts_desc' => 'Mireu l’estat dels comptes socials connectats d’aquest usuari. També es poden utilitzar els comptes socials per a iniciar sessió a banda del sistema d’autenticació principal.',\n    'users_social_accounts_info' => 'Connecteu els vostres comptes socials per a iniciar sessió més ràpidament. La desconnexió d’un compte no revoca l’autorització d’accés atorgada prèviament. Revoqueu l’accés des de la configuració de perfil del compte social connectat.',\n    'users_social_connect' => 'Connecta el compte',\n    'users_social_disconnect' => 'Desconnecta el compte',\n    'users_social_status_connected' => 'Connectat',\n    'users_social_status_disconnected' => 'Desconnectat',\n    'users_social_connected' => 'S’ha connectat el compte :socialAccount al vostre perfil.',\n    'users_social_disconnected' => 'S’ha desconnectat el compte :socialAccount del vostre perfil.',\n    'users_api_tokens' => 'Testimonis API',\n    'users_api_tokens_desc' => 'Creeu i gestioneu els testimonis d’accés que s’utilitzen per autenticar els usuaris amb la l’API REST del BookStack. Els permisos de l’API es gestionen a través de l’usuari a qui pertany el testimoni.',\n    'users_api_tokens_none' => 'No s’ha creat cap testimoni API per a aquest usuari',\n    'users_api_tokens_create' => 'Crea un testimoni',\n    'users_api_tokens_expires' => 'Caducitat',\n    'users_api_tokens_docs' => 'Documentació de l’API',\n    'users_mfa' => 'Autenticació multifactorial',\n    'users_mfa_desc' => 'Configureu l’autenticació multifactorial per a afegir una capa de seguretat extra al vostre compte d’usuari.',\n    'users_mfa_x_methods' => 'Hi ha :count mètode configurat|Hi ha :count mètodes configurats',\n    'users_mfa_configure' => 'Configura un mètode',\n\n    // API Tokens\n    'user_api_token_create' => 'Crea un testimoni API',\n    'user_api_token_name' => 'Nom',\n    'user_api_token_name_desc' => 'Anomeneu el testimoni amb un nom entenedor que permeti saber-ne el propòsit.',\n    'user_api_token_expiry' => 'Data de caducitat',\n    'user_api_token_expiry_desc' => 'Configureu la data de caducitat del testimoni. Un cop passada aquesta data, les sol·licituds fetes amb aquest testimoni no funcionaran. Si deixeu aquest camp en blanc, el testimoni caducarà d’aquí 100 anys.',\n    'user_api_token_create_secret_message' => 'Es crearà i es mostrarà un &laquo;identificador de testimoni&raquo; i un &laquo;secret de testimoni&raquo; immediatament després de crear aquest testimoni. El secret es mostrarà només un sol cop. Assegureu-vos d’anotar-lo i de desar-lo en un lloc segur abans de continuar.',\n    'user_api_token' => 'Testimoni API',\n    'user_api_token_id' => 'Identificador de testimoni',\n    'user_api_token_id_desc' => 'És un identificador no editable generat pel sistema per a aquest testimoni i que s’haurà de proporcionar en les sol·licituds API.',\n    'user_api_token_secret' => 'Secret de testimoni',\n    'user_api_token_secret_desc' => 'És un secret generat pel sistema per a aquest testimoni i que s’haurà de proporcionar en les sol·licituds API. Es mostrarà només un sol cop. Assegureu-vos d’anotar-lo i de desar-lo en un lloc segur abans de continuar.',\n    'user_api_token_created' => 'Testimoni creat :timeAgo',\n    'user_api_token_updated' => 'Testimoni actualitzat :timeAgo',\n    'user_api_token_delete' => 'Suprimeix el testimoni',\n    'user_api_token_delete_warning' => 'Se suprimirà el testimoni API &laquo;:tokenName&raquo; del sistema.',\n    'user_api_token_delete_confirm' => 'Esteu segur que voleu suprimir aquest testimoni API?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Els webhooks permeten enviar dades a URLs externs quan ocorren unes accions o esdeveniments determinats. Això permet la integració basada en esdeveniments amb plataformes externes com ara sistemes de missatgeria o notificació.',\n    'webhooks_x_trigger_events' => 'Hi ha :count esdeveniment disparador|Hi ha :count esdeveniments disparadors',\n    'webhooks_create' => 'Crea un webhook',\n    'webhooks_none_created' => 'No hi ha cap webhook',\n    'webhooks_edit' => 'Edita el webhook',\n    'webhooks_save' => 'Desa el webhook',\n    'webhooks_details' => 'Detalls del webhook',\n    'webhooks_details_desc' => 'Anomeneu el webhook amb un nom entenedor i proporcioneu un extrem POST com a ubicació on enviar les dades per al webhook.',\n    'webhooks_events' => 'Esdeveniments webhook',\n    'webhooks_events_desc' => 'Seleccioneu els esdeveniment que voleu que disparin la crida d’aquest webhook.',\n    'webhooks_events_warning' => 'Tingueu en compte que aquestes crides es dispararan a tots els esdeveniments seleccionats, inclús si s’apliquen permisos personalitzats. Assegureu-vos que l’ús d’aquest webhook no exposarà contingut confidencial.',\n    'webhooks_events_all' => 'Tots els esdeveniments del sistema',\n    'webhooks_name' => 'Nom del webhook',\n    'webhooks_timeout' => 'Temps d’espera del webhook (en segons)',\n    'webhooks_endpoint' => 'Extrem del webhook',\n    'webhooks_active' => 'Webhook actiu',\n    'webhook_events_table_header' => 'Esdeveniments',\n    'webhooks_delete' => 'Suprimeix el webhook',\n    'webhooks_delete_warning' => 'Se suprimirà el webhook &laquo;:webhookName&raquo; del sistema.',\n    'webhooks_delete_confirm' => 'Esteu segur que voleu suprimir aquest webhook?',\n    'webhooks_format_example' => 'Exemple de format de webhook',\n    'webhooks_format_example_desc' => 'Les dades d’un webhook s’envien com una sol·licitud POST a l’extrem configurat com a JSON amb el format següent. Les propietats &laquo;related_item&raquo; i &laquo;url&raquo; són opcionals i dependran del tipus d’esdeveniment que es dispari.',\n    'webhooks_status' => 'Estat del webhook',\n    'webhooks_last_called' => 'Darrera crida:',\n    'webhooks_last_errored' => 'Darrer error:',\n    'webhooks_last_error_message' => 'Darrer missatge d’error:',\n\n    // Licensing\n    'licenses' => 'Llicències',\n    'licenses_desc' => 'Aquesta pàgina detalla informació sobre la llicència de BookStack a més dels projectes i biblioteques que s\\'utilitzen en BookStack. Molts projectes enumerats aquí poden ser utilitzats només en un context de desenvolupament.',\n    'licenses_bookstack' => 'Llicència de BookStack',\n    'licenses_php' => 'Llicències de Biblioteques de PHP',\n    'licenses_js' => 'Llicències de Biblioteques de JavaScript',\n    'licenses_other' => 'Altres llicències',\n    'license_details' => 'Detalls de la llicència',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/ca/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => 'Cal que accepteu el camp :attribute.',\n    'active_url'           => 'El camp :attribute no és un URL vàlid.',\n    'after'                => 'El camp :attribute ha de ser una data posterior a :date.',\n    'alpha'                => 'El camp :attribute només pot contenir lletres.',\n    'alpha_dash'           => 'El camp :attribute només pot contenir lletres, xifres, guionets i guions baixos.',\n    'alpha_num'            => 'El camp :attribute només pot contenir lletres, xifres.',\n    'array'                => 'El camp :attribute ha de ser una matriu.',\n    'backup_codes'         => 'El codi que heu proporcionat no és vàlid o ja s’ha utilitzat.',\n    'before'               => 'El camp :attribute ha de ser una data posterior a :date.',\n    'between'              => [\n        'numeric' => 'El camp :attribute ha de ser un nombre entre :min i :max.',\n        'file'    => 'El camp :attribute ha de tenir entre :min i :max kilobytes.',\n        'string'  => 'El camp :attribute ha de tenir entre :min i :max caràcters.',\n        'array'   => 'El camp :attribute ha de tenir entre :min i :max elements.',\n    ],\n    'boolean'              => 'El camp :attribute ha de ser cert o fals.',\n    'confirmed'            => 'La confirmació del camp :attribute no coincideix.',\n    'date'                 => 'El camp :attribute no és una data vàlida.',\n    'date_format'          => 'El camp :attribute no coincideix amb el format :format.',\n    'different'            => 'El camp :attribute i :other han de ser diferents.',\n    'digits'               => 'El camp :attribute ha de tenir :digits xifres.',\n    'digits_between'       => 'El camp :attribute ha de tenir entre :min i :max xifres.',\n    'email'                => 'El camp :attribute ha de ser un adreça electrònica vàlida.',\n    'ends_with' => 'El camp :attribute ha d’acabar amb un dels signes següents: :values',\n    'file'                 => 'El camp :attribute ha de ser un fitxer vàlid.',\n    'filled'               => 'El camp :attribute és obligatori.',\n    'gt'                   => [\n        'numeric' => 'El camp :attribute ha de ser més gran que :value.',\n        'file'    => 'El camp :attribute ha de tenir més de :value kilobytes.',\n        'string'  => 'El camp :attribute ha de tenir més de :value caràcters.',\n        'array'   => 'El camp :attribute ha de tenir més de :value elements.',\n    ],\n    'gte'                  => [\n        'numeric' => 'El camp :attribute ha de ser com a mínim :value.',\n        'file'    => 'El camp :attribute ha de tenir com a mínim :value kilobytes.',\n        'string'  => 'El camp :attribute ha de tenir com a mínim :value caràcters.',\n        'array'   => 'El camp :attribute ha de tenir com a mínim :value elements.',\n    ],\n    'exists'               => 'El camp :attribute seleccionat no és vàlid.',\n    'image'                => 'El camp :attribute ha de ser una imatge.',\n    'image_extension'      => 'El camp :attribute ha de tenir una extensió d’imatge compatible.',\n    'in'                   => 'El camp :attribute no és vàlid.',\n    'integer'              => 'El camp :attribute ha de ser un nombre enter.',\n    'ip'                   => 'El camp :attribute ha de ser un adreça IP vàlida.',\n    'ipv4'                 => 'El camp :attribute ha de ser un adreça IPv4 vàlida.',\n    'ipv6'                 => 'El camp :attribute ha de ser un adreça IPv6 vàlida.',\n    'json'                 => 'El camp :attribute ha de ser una cadena JSON vàlida.',\n    'lt'                   => [\n        'numeric' => 'El camp :attribute ha de ser més petit que :value.',\n        'file'    => 'El camp :attribute ha de tenir menys de :value kilobytes.',\n        'string'  => 'El camp :attribute ha de tenir menys de :value caràcters.',\n        'array'   => 'El camp :attribute ha de tenir menys de :value elements.',\n    ],\n    'lte'                  => [\n        'numeric' => 'El camp :attribute ha de ser com a màxim :value.',\n        'file'    => 'El camp :attribute ha de tenir com a màxim :value kilobytes.',\n        'string'  => 'El camp :attribute ha de tenir com a màxim :value caràcters.',\n        'array'   => 'El camp :attribute ha de tenir com a màxim :value elements.',\n    ],\n    'max'                  => [\n        'numeric' => 'El camp :attribute ha de ser com a màxim :max.',\n        'file'    => 'El camp :attribute ha de tenir com a màxim :max kilobytes.',\n        'string'  => 'El camp :attribute ha de tenir com a màxim :max caràcters.',\n        'array'   => 'El camp :attribute ha de tenir com a màxim :max elements.',\n    ],\n    'mimes'                => 'El camp :attribute ha de ser un fitxer del tipus: :values.',\n    'min'                  => [\n        'numeric' => 'El camp :attribute ha de ser com a mínim :min.',\n        'file'    => 'El camp :attribute ha de tenir com a mínim :min kilobytes.',\n        'string'  => 'El camp :attribute ha de tenir com a mínim :min caràcters.',\n        'array'   => 'El camp :attribute ha de tenir com a mínim :min elements.',\n    ],\n    'not_in'               => 'El camp :attribute no és vàlid.',\n    'not_regex'            => 'El format :attribute no és vàlid.',\n    'numeric'              => 'El camp :attribute ha de ser un nombre.',\n    'regex'                => 'El format :attribute no és vàlid.',\n    'required'             => 'El camp :attribute és obligatori.',\n    'required_if'          => 'El camp :attribute és obligatori quan :other és :value.',\n    'required_with'        => 'El camp :attribute és obligatori quan hi ha :values.',\n    'required_with_all'    => 'El camp :attribute és obligatori quan hi ha tots aquests valors: :values.',\n    'required_without'     => 'El camp :attribute és obligatori quan no hi ha :values.',\n    'required_without_all' => 'El camp :attribute és obligatori quan no hi ha cap d’aquests valors: :values.',\n    'same'                 => 'El camp :attribute i :other han de coincidir.',\n    'safe_url'             => 'És possible que l’enllaç proporcionat no sigui segur.',\n    'size'                 => [\n        'numeric' => 'El camp :attribute ha de ser :size.',\n        'file'    => 'El camp :attribute ha de tenir :size kilobytes.',\n        'string'  => 'El camp :attribute ha de tenir :size caràcters',\n        'array'   => 'El camp :attribute ha de tenir :size elements.',\n    ],\n    'string'               => 'El camp :attribute ha de ser una cadena de text.',\n    'timezone'             => 'El camp :attribute ha de ser un fus horari vàlid.',\n    'totp'                 => 'El codi proporcionat no és vàlid o ha caducat.',\n    'unique'               => 'El camp :attribute ja s’ha utilitzat.',\n    'url'                  => 'El format :attribute no és vàlid.',\n    'uploaded'             => 'No s’ha pogut pujar el fitxer. És possible que el servidor no admeti fitxers d’aquesta mida.',\n\n    'zip_file' => 'El :attribute necessita fer referència a un arxiu dins del ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'El :attribute necessita fer referència a un arxiu de tipus :validTyes, trobat :foundType.',\n    'zip_model_expected' => 'S\\'esperava un objecte de dades, però s\\'ha trobat \":type\".',\n    'zip_unique' => 'El :attribute ha de ser únic pel tipus d\\'objecte dins del ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Heu de confirmar la contrasenya.',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/cs/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'vytvořil/a stránku',\n    'page_create_notification'    => 'Stránka byla úspěšně vytvořena',\n    'page_update'                 => 'aktualizoval/a stránku',\n    'page_update_notification'    => 'Stránka byla úspěšně aktualizována',\n    'page_delete'                 => 'odstranil/a stránku',\n    'page_delete_notification'    => 'Stránka byla úspěšně smazána',\n    'page_restore'                => 'obnovil/a stránku',\n    'page_restore_notification'   => 'Stránka byla úspěšně obnovena',\n    'page_move'                   => 'přesunul/a stránku',\n    'page_move_notification'      => 'Strana byla úspěšně přesunuta',\n\n    // Chapters\n    'chapter_create'              => 'vytvořil/a kapitolu',\n    'chapter_create_notification' => 'Kapitola byla úspěšně vytvořena',\n    'chapter_update'              => 'aktualizoval/a kapitolu',\n    'chapter_update_notification' => 'Kapitola byla úspěšně aktualizována',\n    'chapter_delete'              => 'odstranila/a kapitolu',\n    'chapter_delete_notification' => 'Kapitola byla úspěšně odstraněna',\n    'chapter_move'                => 'přesunul/a kapitolu',\n    'chapter_move_notification' => 'Kapitola byla úspěšně přesunuta',\n\n    // Books\n    'book_create'                 => 'vytvořil/a knihu',\n    'book_create_notification'    => 'Kniha byla úspěšně vytvořena',\n    'book_create_from_chapter'              => 'převést kapitolu na knihu',\n    'book_create_from_chapter_notification' => 'Kapitola byla úspěšně převedena na knihu',\n    'book_update'                 => 'aktualizoval/a knihu',\n    'book_update_notification'    => 'Kniha byla úspěšně aktualizována',\n    'book_delete'                 => 'odstranil/a knihu',\n    'book_delete_notification'    => 'Kniha byla úspěšně odstraněna',\n    'book_sort'                   => 'seřadil/a knihu',\n    'book_sort_notification'      => 'Kniha byla úspěšně seřazena',\n\n    // Bookshelves\n    'bookshelf_create'            => 'vytvořil polici',\n    'bookshelf_create_notification'    => 'Police byla úspěšně vytvořena',\n    'bookshelf_create_from_book'    => 'převést knihu na polici',\n    'bookshelf_create_from_book_notification'    => 'Kniha byla úspěšně převedena na polici',\n    'bookshelf_update'                 => 'aktualizovat polici',\n    'bookshelf_update_notification'    => 'Police byla úspěšně aktualizována',\n    'bookshelf_delete'                 => 'odstranil polici',\n    'bookshelf_delete_notification'    => 'Police byla úspěšně odstraněna',\n\n    // Revisions\n    'revision_restore' => 'obnovil revizi',\n    'revision_delete' => 'odstranil revizi',\n    'revision_delete_notification' => 'Revize byla úspěšně odstraněna',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" byla přidána do Vašich oblíbených',\n    'favourite_remove_notification' => '\":name\" byla odstraněna z Vašich oblíbených',\n\n    // Watching\n    'watch_update_level_notification' => 'Předvolby sledování úspěšně aktualizovány',\n\n    // Auth\n    'auth_login' => 'se přihlásil',\n    'auth_register' => 'se zaregistroval jako nový uživatel',\n    'auth_password_reset_request' => 'zažádal o resetování hesla',\n    'auth_password_reset_update' => 'zresetoval uživatelské heslo',\n    'mfa_setup_method' => 'nastavil MFA metodu',\n    'mfa_setup_method_notification' => 'Vícefaktorová metoda byla úspěšně nakonfigurována',\n    'mfa_remove_method' => 'odstranil MFA metodu',\n    'mfa_remove_method_notification' => 'Vícefaktorová metoda byla úspěšně odstraněna',\n\n    // Settings\n    'settings_update' => 'aktualizoval nastavení',\n    'settings_update_notification' => 'Nastavení bylo úspěšně aktualizováno',\n    'maintenance_action_run' => 'spustil údržbu',\n\n    // Webhooks\n    'webhook_create' => 'vytvořil/a webhook',\n    'webhook_create_notification' => 'Webhook byl úspěšně vytvořen',\n    'webhook_update' => 'aktualizoval/a webhook',\n    'webhook_update_notification' => 'Webhook byl úspěšně aktualizován',\n    'webhook_delete' => 'odstranil/a webhook',\n    'webhook_delete_notification' => 'Webhook byl úspěšně odstraněn',\n\n    // Imports\n    'import_create' => 'vytvořil/a import',\n    'import_create_notification' => 'Import byl úspěšně nahrán',\n    'import_run' => 'aktualizoval/a import',\n    'import_run_notification' => 'Obsah byl úspěšně importován',\n    'import_delete' => 'odstranil/a import',\n    'import_delete_notification' => 'Import byl úspěšně odstraněn',\n\n    // Users\n    'user_create' => 'vytvořil uživatele',\n    'user_create_notification' => 'Uživatel byl úspěšně vytvořen',\n    'user_update' => 'aktualizoval uživatele',\n    'user_update_notification' => 'Uživatel byl úspěšně aktualizován',\n    'user_delete' => 'odstranil uživatele',\n    'user_delete_notification' => 'Uživatel byl úspěšně odstraněn',\n\n    // API Tokens\n    'api_token_create' => 'API token byl vytvořen',\n    'api_token_create_notification' => 'API token úspěšně vytvořen',\n    'api_token_update' => 'API token byl aktualizován',\n    'api_token_update_notification' => 'API token úspěšně aktualizován',\n    'api_token_delete' => 'API token byl odstraněn',\n    'api_token_delete_notification' => 'API token úspěšně odstraněn',\n\n    // Roles\n    'role_create' => 'vytvořil roli',\n    'role_create_notification' => 'Role byla úspěšně vytvořena',\n    'role_update' => 'aktualizoval roli',\n    'role_update_notification' => 'Role byla úspěšně aktualizována',\n    'role_delete' => 'odstranil roli',\n    'role_delete_notification' => 'Role byla odstraněna',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'vyprázdnil koš',\n    'recycle_bin_restore' => 'obnovil z koše',\n    'recycle_bin_destroy' => 'odstranil z koše',\n\n    // Comments\n    'commented_on'                => 'okomentoval/a',\n    'comment_create'              => 'přidal komentář',\n    'comment_update'              => 'aktualizoval komentář',\n    'comment_delete'              => 'odstranil komentář',\n\n    // Sort Rules\n    'sort_rule_create' => 'vytvořil/a pravidlo řazení',\n    'sort_rule_create_notification' => 'Pravidlo řazení bylo úspěšně vytvořeno',\n    'sort_rule_update' => 'aktualizoval/a pravidlo řazení',\n    'sort_rule_update_notification' => 'Pravidlo řazení bylo úspěšně aktualizováno',\n    'sort_rule_delete' => 'odstranil/a pravidlo řazení',\n    'sort_rule_delete_notification' => 'Pravidlo řazení bylo úspěšně odstraněno',\n\n    // Other\n    'permissions_update'          => 'oprávnění upravena',\n];\n"
  },
  {
    "path": "lang/cs/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Neplatné přihlašovací údaje.',\n    'throttle' => 'Příliš mnoho pokusů o přihlášení. Zkuste to prosím znovu za :seconds sekund.',\n\n    // Login & Register\n    'sign_up' => 'Registrace',\n    'log_in' => 'Přihlášení',\n    'log_in_with' => 'Přihlásit se přes :socialDriver',\n    'sign_up_with' => 'Registrovat se přes :socialDriver',\n    'logout' => 'Odhlásit',\n\n    'name' => 'Jméno',\n    'username' => 'Uživatelské jméno',\n    'email' => 'E-mail',\n    'password' => 'Heslo',\n    'password_confirm' => 'Potvrzení hesla',\n    'password_hint' => 'Musí mít alespoň 8 znaků',\n    'forgot_password' => 'Zapomenuté heslo?',\n    'remember_me' => 'Zapamatovat si mě',\n    'ldap_email_hint' => 'Zadejte email, který chcete přiřadit k tomuto účtu.',\n    'create_account' => 'Vytvořit účet',\n    'already_have_account' => 'Již máte účet?',\n    'dont_have_account' => 'Nemáte učet?',\n    'social_login' => 'Přihlášení přes sociální sítě',\n    'social_registration' => 'Registrace přes sociální sítě',\n    'social_registration_text' => 'Registrovat a přihlásit se přes jinou službu',\n\n    'register_thanks' => 'Děkujeme za registraci!',\n    'register_confirm' => 'Zkontrolujte prosím svůj e-mail a klikněte na potvrzovací tlačítko pro přístup do :appName.',\n    'registrations_disabled' => 'Registrace jsou momentálně pozastaveny',\n    'registration_email_domain_invalid' => 'Registrace z této e-mailové domény nejsou povoleny',\n    'register_success' => 'Děkujeme za registraci! Nyní jste zaregistrováni a přihlášeni.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Pokus o přihlášení',\n    'auto_init_starting_desc' => 'Kontaktujeme váš ověřovací systém pro zahájení procesu přihlášení. Pokud po 5 sekundách nedojde k žádnému pokroku, můžete zkusit kliknout na odkaz níže.',\n    'auto_init_start_link' => 'Pokračovat s ověřováním',\n\n    // Password Reset\n    'reset_password' => 'Obnovit heslo',\n    'reset_password_send_instructions' => 'Níže zadejte svou e-mailovou adresu a bude vám zaslán e-mail s odkazem na obnovení hesla.',\n    'reset_password_send_button' => 'Zaslat odkaz na obnovení hesla',\n    'reset_password_sent' => 'Odkaz pro obnovení hesla bude odeslán na :email, pokud bude tato e-mailová adresa nalezena v systému.',\n    'reset_password_success' => 'Vaše heslo bylo obnoveno.',\n    'email_reset_subject' => 'Obnovit heslo do :appName',\n    'email_reset_text' => 'Tento e-mail jste obdrželi, protože jsme obdrželi žádost o obnovení hesla k vašemu účtu.',\n    'email_reset_not_requested' => 'Pokud jste o obnovení hesla nežádali, není vyžadována žádná další akce.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Potvrďte svůj e-mail pro :appName',\n    'email_confirm_greeting' => 'Díky že jste se přidali do :appName!',\n    'email_confirm_text' => 'Prosíme potvrďte svou e-mailovou adresu kliknutím na níže uvedené tlačítko:',\n    'email_confirm_action' => 'Potvrdit e-mail',\n    'email_confirm_send_error' => 'Potvrzení e-mailu je vyžadováno, ale systém nemohl odeslat e-mail. Obraťte se na správce, abyste se ujistili, že je e-mail správně nastaven.',\n    'email_confirm_success' => 'Váš email byl ověřen! Nyní byste měli být schopni se touto emailovou adresou přihlásit.',\n    'email_confirm_resent' => 'E-mail s potvrzením byl znovu odeslán. Zkontrolujte svou příchozí poštu.',\n    'email_confirm_thanks' => 'Děkujeme za potvrzení!',\n    'email_confirm_thanks_desc' => 'Počkejte prosím chvíli, než se vaše potvrzení vyřizuje. Pokud nebudete po 3 sekundách přesměrováni, klikněte na odkaz \"Pokračovat\" níže pro pokračování.',\n\n    'email_not_confirmed' => 'E-mailová adresa nebyla potvrzena',\n    'email_not_confirmed_text' => 'Vaše e-mailová adresa nebyla dosud potvrzena.',\n    'email_not_confirmed_click_link' => 'Klikněte prosím na odkaz v e-mailu, který byl odeslán krátce po registraci.',\n    'email_not_confirmed_resend' => 'Pokud nemůžete e-mail nalézt, můžete znovu odeslat potvrzovací e-mail odesláním níže uvedeného formuláře.',\n    'email_not_confirmed_resend_button' => 'Znovu odeslat potvrzovací e-mail',\n\n    // User Invite\n    'user_invite_email_subject' => 'Byli jste pozváni do :appName!',\n    'user_invite_email_greeting' => 'Byl pro vás vytvořen účet na :appName.',\n    'user_invite_email_text' => 'Klikněte na níže uvedené tlačítko pro nastavení hesla k účtu a získání přístupu:',\n    'user_invite_email_action' => 'Nastavit heslo k účtu',\n    'user_invite_page_welcome' => 'Vítejte v :appName!',\n    'user_invite_page_text' => 'Pro dokončení vašeho účtu a získání přístupu musíte nastavit heslo, které bude použito k přihlášení do :appName při dalších návštěvách.',\n    'user_invite_page_confirm_button' => 'Potvrdit heslo',\n    'user_invite_success_login' => 'Heslo bylo nasteaveno, nyní byste měli být schopni přihlásit se nastaveným heslem do aplikace :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Nastavit vícefaktorové ověření',\n    'mfa_setup_desc' => 'Nastavit vícefaktorové ověřování jako další vrstvu zabezpečení vašeho uživatelského účtu.',\n    'mfa_setup_configured' => 'Již nastaveno',\n    'mfa_setup_reconfigure' => 'Přenastavit',\n    'mfa_setup_remove_confirmation' => 'Opravdu chcete odstranit tuto metodu vícefaktorového ověřování?',\n    'mfa_setup_action' => 'Nastavit',\n    'mfa_backup_codes_usage_limit_warning' => 'Zbývá vám méně než 5 záložních kódů. Před vypršením kódu si prosím vygenerujte a uložte novou sadu, abyste se vyhnuli zablokování vašeho účtu.',\n    'mfa_option_totp_title' => 'Mobilní aplikace',\n    'mfa_option_totp_desc' => 'Pro použití vícefaktorového ověření budete potřebovat mobilní aplikaci, která podporuje TOTP jako např. Google Authenticator, Authy nebo Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Záložní kódy',\n    'mfa_option_backup_codes_desc' => 'Vygeneruje sadu jednorázových záložních kódů, které zadáte při přihlášení k ověření své identity. Ujistěte se, že jsou uloženy na bezpečném místě.',\n    'mfa_gen_confirm_and_enable' => 'Potvrdit a povolit',\n    'mfa_gen_backup_codes_title' => 'Nastavení záložních kódů',\n    'mfa_gen_backup_codes_desc' => 'Uložte níže uvedený seznam kódů na bezpečné místo. Při přístupu k systému budete moci použít jeden z kódů jako druhou metodu ověření.',\n    'mfa_gen_backup_codes_download' => 'Stáhnout kódy',\n    'mfa_gen_backup_codes_usage_warning' => 'Každý kód může být použit pouze jednou',\n    'mfa_gen_totp_title' => 'Nastavení mobilní aplikace',\n    'mfa_gen_totp_desc' => 'Pro použití vícefaktorového ověření budete potřebovat mobilní aplikaci, která podporuje TOTP jako např. Google Authenticator, Authy nebo Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Naskenujte QR kód níže pomocí vaší preferované ověřovací aplikace.',\n    'mfa_gen_totp_verify_setup' => 'Ověřit nastavení',\n    'mfa_gen_totp_verify_setup_desc' => 'Ověřte, že vše funguje zadáním kódu, generovaného v ověřovací aplikaci, do níže uvedeného vstupního pole:',\n    'mfa_gen_totp_provide_code_here' => 'Zde zadejte kód vygenerovaný vaší aplikací',\n    'mfa_verify_access' => 'Ověřit přístup',\n    'mfa_verify_access_desc' => 'Váš uživatelský účet vyžaduje, abyste před udělením přístupu potvrdili svou totožnost prostřednictvím další úrovně ověření. Ověřte pomocí jedné z vašich nakonfigurovaných metod, abyste mohli pokračovat.',\n    'mfa_verify_no_methods' => 'Nejsou nastaveny žádné metody',\n    'mfa_verify_no_methods_desc' => 'Pro váš účet nebyly nalezeny žádné vícefázové metody ověřování. Před získáním přístupu budete muset nastavit alespoň jednu metodu.',\n    'mfa_verify_use_totp' => 'Ověřit pomocí mobilní aplikace',\n    'mfa_verify_use_backup_codes' => 'Ověřit pomocí záložního kódu',\n    'mfa_verify_backup_code' => 'Záložní kód',\n    'mfa_verify_backup_code_desc' => 'Níže zadejte jeden z vašich zbývajících záložních kódů:',\n    'mfa_verify_backup_code_enter_here' => 'Zde zadejte záložní kód',\n    'mfa_verify_totp_desc' => 'Níže zadejte kód, který jste si vygenerovali pomocí mobilní aplikace:',\n    'mfa_setup_login_notification' => 'Vícefázová metoda nastavena, nyní se prosím znovu přihlaste pomocí konfigurované metody.',\n];\n"
  },
  {
    "path": "lang/cs/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Zrušit',\n    'close' => 'Zavřít‏',\n    'confirm' => 'Potvrdit',\n    'back' => 'Zpět',\n    'save' => 'Uložit',\n    'continue' => 'Pokračovat',\n    'select' => 'Vybrat',\n    'toggle_all' => 'Přepnout vše',\n    'more' => 'Více',\n\n    // Form Labels\n    'name' => 'Název',\n    'description' => 'Popis',\n    'role' => 'Role',\n    'cover_image' => 'Obrázek obálky',\n    'cover_image_description' => 'Tento obrázek by měl mít rozměry přibližně 440x250px, ačkoli bude podle potřeby zmenšen a oříznut, aby se vešel do uživatelského rozhraní, takže se skutečné rozměry budou lišit.',\n\n    // Actions\n    'actions' => 'Akce',\n    'view' => 'Zobrazit',\n    'view_all' => 'Zobrazit vše',\n    'new' => 'Nový',\n    'create' => 'Vytvořit',\n    'update' => 'Aktualizovat',\n    'edit' => 'Upravit',\n    'archive' => 'Archivovat',\n    'unarchive' => 'Od-Archivovat',\n    'sort' => 'Seřadit',\n    'move' => 'Přesunout',\n    'copy' => 'Kopírovat',\n    'reply' => 'Odpovědět',\n    'delete' => 'Odstranit',\n    'delete_confirm' => 'Potvrdit odstranění',\n    'search' => 'Hledat',\n    'search_clear' => 'Vymazat hledání',\n    'reset' => 'Obnovit',\n    'remove' => 'Odebrat',\n    'add' => 'Přidat',\n    'configure' => 'Nastavit',\n    'manage' => 'Spravovat',\n    'fullscreen' => 'Celá obrazovka',\n    'favourite' => 'Přidat do oblíbených',\n    'unfavourite' => 'Odebrat z oblíbených',\n    'next' => 'Další',\n    'previous' => 'Předchozí',\n    'filter_active' => 'Aktivní filtr:',\n    'filter_clear' => 'Zrušit filtr',\n    'download' => 'Stáhnout',\n    'open_in_tab' => 'Otevřít v nové záložce',\n    'open' => 'Otevřít',\n\n    // Sort Options\n    'sort_options' => 'Možnosti řazení',\n    'sort_direction_toggle' => 'Přepínač směru řazení',\n    'sort_ascending' => 'Řadit vzestupně',\n    'sort_descending' => 'Řadit sestupně',\n    'sort_name' => 'Název',\n    'sort_default' => 'Výchozí',\n    'sort_created_at' => 'Datum vytvoření',\n    'sort_updated_at' => 'Datum aktualizace',\n\n    // Misc\n    'deleted_user' => 'Odstraněný uživatel',\n    'no_activity' => 'Žádná aktivita k zobrazení',\n    'no_items' => 'Žádné položky k dispozici',\n    'back_to_top' => 'Zpět nahoru',\n    'skip_to_main_content' => 'Přeskočit na hlavní obsah',\n    'toggle_details' => 'Přepnout podrobnosti',\n    'toggle_thumbnails' => 'Přepnout náhledy',\n    'details' => 'Podrobnosti',\n    'grid_view' => 'Zobrazit mřížku',\n    'list_view' => 'Zobrazit seznam',\n    'default' => 'Výchozí',\n    'breadcrumb' => 'Drobečková navigace',\n    'status' => 'Stav',\n    'status_active' => 'Aktivní',\n    'status_inactive' => 'Neaktivní',\n    'never' => 'Nikdy',\n    'none' => 'Žádná',\n\n    // Header\n    'homepage' => 'Domovská stránka',\n    'header_menu_expand' => 'Rozbalit menu v záhlaví',\n    'profile_menu' => 'Nabídka profilu',\n    'view_profile' => 'Zobrazit profil',\n    'edit_profile' => 'Upravit profil',\n    'dark_mode' => 'Tmavý režim',\n    'light_mode' => 'Světlý režim',\n    'global_search' => 'Globální vyhledávání',\n\n    // Layout tabs\n    'tab_info' => 'Informace',\n    'tab_info_label' => 'Tab: Zobrazit podružné informace',\n    'tab_content' => 'Obsah',\n    'tab_content_label' => 'Tab: Zobrazit hlavní obsah',\n\n    // Email Content\n    'email_action_help' => 'Pokud se vám nedaří kliknout na tlačítko „:actionText“, zkopírujte a vložte níže uvedenou URL do vašeho webového prohlížeče:',\n    'email_rights' => 'Všechna práva vyhrazena',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Zásady ochrany osobních údajů',\n    'terms_of_service' => 'Podmínky služby',\n\n    // OpenSearch\n    'opensearch_description' => 'Vyhledat :appName',\n];\n"
  },
  {
    "path": "lang/cs/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Výběr obrázku',\n    'image_list' => 'Seznam obrázků',\n    'image_details' => 'Detail obrázku',\n    'image_upload' => 'Nahrát obrázek',\n    'image_intro' => 'Zde můžete vybrat a spravovat obrázky, které byly dříve nahrány do systému.',\n    'image_intro_upload' => 'Nahrajte nový obrázek přetažením obrázku do tohoto okna, nebo pomocí tlačítka \"Nahrát obrázek\" výše.',\n    'image_all' => 'Vše',\n    'image_all_title' => 'Zobrazit všechny obrázky',\n    'image_book_title' => 'Zobrazit obrázky nahrané do této knihy',\n    'image_page_title' => 'Zobrazit obrázky nahrané na tuto stránku',\n    'image_search_hint' => 'Hledat podle názvu obrázku',\n    'image_uploaded' => 'Nahráno :uploadedDate',\n    'image_uploaded_by' => 'Nahráno uživatelem :userName',\n    'image_uploaded_to' => 'Nahráno na :pageLink',\n    'image_updated' => 'Aktualizováno :updateDate',\n    'image_load_more' => 'Načíst další',\n    'image_image_name' => 'Název obrázku',\n    'image_delete_used' => 'Tento obrázek je použit na níže uvedených stránkách.',\n    'image_delete_confirm_text' => 'Opravdu chcete odstranit tento obrázek?',\n    'image_select_image' => 'Zvolte obrázek',\n    'image_dropzone' => 'Přetáhněte obrázky nebo klikněte sem pro nahrání',\n    'image_dropzone_drop' => 'Obrázky nahrajete přetažením sem',\n    'images_deleted' => 'Obrázky odstraněny',\n    'image_preview' => 'Náhled obrázku',\n    'image_upload_success' => 'Obrázek byl nahrán',\n    'image_update_success' => 'Podrobnosti o obrázku byly aktualizovány',\n    'image_delete_success' => 'Obrázek byl odstraněn',\n    'image_replace' => 'Nahradit obrázek',\n    'image_replace_success' => 'Obrázek úspěšně vytvořen',\n    'image_rebuild_thumbs' => 'Přegenerovat všechny velikosti',\n    'image_rebuild_thumbs_success' => 'Všechny velikostní varianty obrázku byly úspěšně znovu vytvořeny!',\n\n    // Code Editor\n    'code_editor' => 'Upravit kód',\n    'code_language' => 'Jazyk kódu',\n    'code_content' => 'Obsah kódu',\n    'code_session_history' => 'Historie relace',\n    'code_save' => 'Uložit kód',\n];\n"
  },
  {
    "path": "lang/cs/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Základní nastavení',\n    'advanced' => 'Pokročilé',\n    'none' => 'Nic',\n    'cancel' => 'Zrušit',\n    'save' => 'Uložit',\n    'close' => 'Zavřít‏',\n    'apply' => 'Použít',\n    'undo' => 'Zpět',\n    'redo' => 'Znovu',\n    'left' => 'Vlevo',\n    'center' => 'Na střed',\n    'right' => 'Vpravo',\n    'top' => 'Nahoru',\n    'middle' => 'Uprostřed',\n    'bottom' => 'Odspodu',\n    'width' => 'Šířka',\n    'height' => 'výška',\n    'More' => 'Více',\n    'select' => 'Vybrat...',\n\n    // Toolbar\n    'formats' => 'Formáty',\n    'header_large' => 'Velké záhlaví',\n    'header_medium' => 'Střední záhlaví',\n    'header_small' => 'Malé záhlaví',\n    'header_tiny' => 'Malá hlavička',\n    'paragraph' => 'Odstavec',\n    'blockquote' => 'Citát do bloku',\n    'inline_code' => 'Vložený kód',\n    'callouts' => 'Poznámka',\n    'callout_information' => 'Informace',\n    'callout_success' => 'Úspěšně dokončeno',\n    'callout_warning' => 'Upozornění',\n    'callout_danger' => 'Nebezpečí',\n    'bold' => 'Tučně',\n    'italic' => 'Kurzíva',\n    'underline' => 'Podtržené',\n    'strikethrough' => 'Proškrtnuté',\n    'superscript' => 'horní index',\n    'subscript' => 'Dolní index',\n    'text_color' => 'Barva textu',\n    'highlight_color' => 'Barva zvýraznění',\n    'custom_color' => 'Vlastní barva',\n    'remove_color' => 'Odstranit barvu',\n    'background_color' => 'Barva pozadí',\n    'align_left' => 'Zarovnat vlevo',\n    'align_center' => 'Zarovnat na střed',\n    'align_right' => 'Zarovnat doprava',\n    'align_justify' => 'Zarovnat do bloku',\n    'list_bullet' => 'Bodový seznam',\n    'list_numbered' => 'Číslovaný seznam',\n    'list_task' => 'Seznam úkolů',\n    'indent_increase' => 'Zvýšit odsazení',\n    'indent_decrease' => 'Zmenšit odsazení',\n    'table' => 'Tabulka',\n    'insert_image' => 'Vložit obrázek',\n    'insert_image_title' => 'Vložit/upravit obrázek',\n    'insert_link' => 'Vložit/upravit odkaz',\n    'insert_link_title' => 'Vložit/upravit odkaz',\n    'insert_horizontal_line' => 'Vložit vodorovnou čáru',\n    'insert_code_block' => 'Vložit blok s kódem',\n    'edit_code_block' => 'Upravit blok kódu',\n    'insert_drawing' => 'Vložit/upravit kreslení',\n    'drawing_manager' => 'Správce kreslení',\n    'insert_media' => 'Vložit/upravit média',\n    'insert_media_title' => 'Vložit/upravit média',\n    'clear_formatting' => 'Vymazat formátování',\n    'source_code' => 'Zdrojový kód',\n    'source_code_title' => 'Zdrojový kód',\n    'fullscreen' => 'Celá obrazovka',\n    'image_options' => 'Možnosti obrázku',\n\n    // Tables\n    'table_properties' => 'Vlastnosti tabulky',\n    'table_properties_title' => 'Vlastnosti tabulky',\n    'delete_table' => 'Smazat tabulku',\n    'table_clear_formatting' => 'Vymazat formátování tabulky',\n    'resize_to_contents' => 'Změnit velikost podle obsahu',\n    'row_header' => 'Záhlaví řádku',\n    'insert_row_before' => 'Vložit řádek před',\n    'insert_row_after' => 'Vložit řádek za',\n    'delete_row' => 'Smazat řádek',\n    'insert_column_before' => 'Vložit sloupec před',\n    'insert_column_after' => 'Vložit sloupec za',\n    'delete_column' => 'Odstranit sloupec',\n    'table_cell' => 'Buňka',\n    'table_row' => 'Řádek',\n    'table_column' => 'Sloupec',\n    'cell_properties' => 'Vlastnosti buňky',\n    'cell_properties_title' => 'Vlastnosti buňky',\n    'cell_type' => 'Typ buňky',\n    'cell_type_cell' => 'Buňka',\n    'cell_scope' => 'Rozsah',\n    'cell_type_header' => 'buňka záhlaví',\n    'merge_cells' => 'Sloučit buňky',\n    'split_cell' => 'Rozdělit buňku',\n    'table_row_group' => 'Skupina řádků',\n    'table_column_group' => 'Skupina sloupců',\n    'horizontal_align' => 'Vodorovné zarovnání',\n    'vertical_align' => 'Svislé vyrovnání',\n    'border_width' => 'Šířka okraje',\n    'border_style' => 'Styl okraje',\n    'border_color' => 'Barva okraje',\n    'row_properties' => 'Vlastnosti řádku',\n    'row_properties_title' => 'Vlastnosti řádku',\n    'cut_row' => 'Vyjmout řádek',\n    'copy_row' => 'Kopírovat řádek',\n    'paste_row_before' => 'Vložit řádek před',\n    'paste_row_after' => 'Vložit za',\n    'row_type' => 'Typ řádku',\n    'row_type_header' => 'Hlavička',\n    'row_type_body' => 'Tělo',\n    'row_type_footer' => 'Zápatí',\n    'alignment' => 'zarovnání',\n    'cut_column' => 'Vyjmout sloupec',\n    'copy_column' => 'Kopírovat sloupec',\n    'paste_column_before' => 'Přidat sloupec před',\n    'paste_column_after' => 'Přidat sloupec za',\n    'cell_padding' => 'Odsazení obsahu buněk',\n    'cell_spacing' => 'Mezery mezi buňkami',\n    'caption' => 'Titulek',\n    'show_caption' => 'Zobrazit titulek',\n    'constrain' => 'Vazba poměrů',\n    'cell_border_solid' => 'Nepřerušovaná čára',\n    'cell_border_dotted' => 'Tečkovaná čára',\n    'cell_border_dashed' => 'Přerušovaná čára',\n    'cell_border_double' => 'Dvojitá',\n    'cell_border_groove' => 'Drážek',\n    'cell_border_ridge' => 'hřeben',\n    'cell_border_inset' => 'Vsazený',\n    'cell_border_outset' => 'Počátek',\n    'cell_border_none' => 'Žádné',\n    'cell_border_hidden' => 'Skrytý',\n\n    // Images, links, details/summary & embed\n    'source' => 'Zdroj',\n    'alt_desc' => 'Alternativní popis',\n    'embed' => 'Vložení',\n    'paste_embed' => 'Vložte svůj vložený kód níže:',\n    'url' => 'Adresa URL',\n    'text_to_display' => 'Text k zobrazení',\n    'title' => 'Titulek',\n    'browse_links' => 'Procházet odkazy',\n    'open_link' => 'Otevřít odkaz',\n    'open_link_in' => 'Otevřít odkaz v...',\n    'open_link_current' => 'Aktuální okno',\n    'open_link_new' => 'Nové okno',\n    'remove_link' => 'Odstranit odkaz',\n    'insert_collapsible' => 'Vložit sbalitelný blok',\n    'collapsible_unwrap' => 'Rozbalit',\n    'edit_label' => 'Upravit štítek',\n    'toggle_open_closed' => 'Přepnout otevření/zavření',\n    'collapsible_edit' => 'Upravit sbalitelný blok',\n    'toggle_label' => 'Přepnout popisek',\n\n    // About view\n    'about' => 'O editoru',\n    'about_title' => 'O WYSIWYG editoru',\n    'editor_license' => 'Editor licence a autorská práva',\n    'editor_lexical_license' => 'Tento editor je vytvořen jako fork :lexicalLink, který je distribuován pod licencí MIT.',\n    'editor_lexical_license_link' => 'Kompletní údaje o licenci naleznete zde.',\n    'editor_tiny_license' => 'Tento editor je vytvořen pomocí :tinyLink, který je poskytován pod licencí MIT.',\n    'editor_tiny_license_link' => 'Podrobnosti o autorských právech a licenci TinyMCE naleznete zde.',\n    'save_continue' => 'Uložit stránku a pokračovat',\n    'callouts_cycle' => '(Stiskněte pro přepnutí typů)',\n    'link_selector' => 'Odkaz na obsah',\n    'shortcuts' => 'Zkratky',\n    'shortcut' => 'Zástupce',\n    'shortcuts_intro' => 'Následující zkratky jsou k dispozici v editoru:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Popis',\n];\n"
  },
  {
    "path": "lang/cs/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Nedávno vytvořené',\n    'recently_created_pages' => 'Nedávno vytvořené stránky',\n    'recently_updated_pages' => 'Nedávno aktualizované stránky',\n    'recently_created_chapters' => 'Nedávno vytvořené kapitoly',\n    'recently_created_books' => 'Nedávno vytvořené knihy',\n    'recently_created_shelves' => 'Nedávno vytvořené knihovny',\n    'recently_update' => 'Nedávno aktualizované',\n    'recently_viewed' => 'Nedávno zobrazené',\n    'recent_activity' => 'Nedávné aktivity',\n    'create_now' => 'Vytvořit nyní',\n    'revisions' => 'Revize',\n    'meta_revision' => 'Revize č. :revisionCount',\n    'meta_created' => 'Vytvořeno :timeLength',\n    'meta_created_name' => 'Vytvořeno :timeLength uživatelem :user',\n    'meta_updated' => 'Aktualizováno :timeLength',\n    'meta_updated_name' => 'Aktualizováno :timeLength uživatelem :user',\n    'meta_owned_name' => 'Vlastník :user',\n    'meta_reference_count' => 'Odkazováno :count položkou|Odkazováno :count položkami',\n    'entity_select' => 'Výběr entity',\n    'entity_select_lack_permission' => 'Nemáte dostatečná oprávnění k výběru této položky',\n    'images' => 'Obrázky',\n    'my_recent_drafts' => 'Mé nedávné koncepty',\n    'my_recently_viewed' => 'Mé nedávno zobrazené',\n    'my_most_viewed_favourites' => 'Mé nejčastěji zobrazené oblíbené',\n    'my_favourites' => 'Mé oblíbené',\n    'no_pages_viewed' => 'Nezobrazili jste žádné stránky',\n    'no_pages_recently_created' => 'Žádné nedávno vytvořené stránky',\n    'no_pages_recently_updated' => 'Žádné nedávno aktualizované stránky',\n    'export' => 'Exportovat',\n    'export_html' => 'HTML stránka s celým obsahem',\n    'export_pdf' => 'PDF dokument',\n    'export_text' => 'Textový soubor',\n    'export_md' => 'Markdown',\n    'export_zip' => 'Přenosný archiv ZIP',\n    'default_template' => 'Výchozí šablona stránky',\n    'default_template_explain' => 'Přiřadit šablonu stránky, která bude použita jako výchozí obsah pro všechny nové stránky v této knize. Mějte na paměti, že šablona bude použita pouze v případě, že tvůrce stránek bude mít přístup k těmto vybraným stránkám šablony.',\n    'default_template_select' => 'Vyberte šablonu stránky',\n    'import' => 'Importovat',\n    'import_validate' => 'Ověřit import',\n    'import_desc' => 'Import knih, kapitol a stránek pomocí přenosného archivu ZIP z tohoto nebo jiného webu. Pro pokračování vyberte ZIP soubor. Po nahrání a ověření souboru budete moci v dalším kroku nastavit a potvrdit import.',\n    'import_zip_select' => 'Vybrat ZIP soubor k nahrání',\n    'import_zip_validation_errors' => 'Při ověřování vybraného souboru ZIP byly zjištěny chyby:',\n    'import_pending' => 'Čekající importy',\n    'import_pending_none' => 'Žádné importy nebyly spuštěny.',\n    'import_continue' => 'Pokračovat v importu',\n    'import_continue_desc' => 'Zkontrolujte obsah, který má být importován z nahraného souboru. Pokud je vše v pořádku, spusťte import pro přidání obsahu ZIP archivu na tento web. Nahraný soubor ZIP bude automaticky odstraněn po úspěšném importu.',\n    'import_details' => 'Obsah importu',\n    'import_run' => 'Spustit import',\n    'import_size' => 'Velikost archivu ZIP: :size',\n    'import_uploaded_at' => 'Nahráno :relativeTime',\n    'import_uploaded_by' => 'Nahrál/a',\n    'import_location' => 'Cílové umístění',\n    'import_location_desc' => 'Vyberte cílové umístění pro importovaný obsah. K vytvoření potřebujete příslušná oprávnění.',\n    'import_delete_confirm' => 'Opravdu chcete tento import odstranit?',\n    'import_delete_desc' => 'Potvrzením odstraníte nahraný ZIP soubor. Tento krok nelze vrátit zpět.',\n    'import_errors' => 'Chyby importu',\n    'import_errors_desc' => 'Při pokusu o import došlo k následujícím chybám:',\n    'breadcrumb_siblings_for_page' => 'Přejít na jinou stránku',\n    'breadcrumb_siblings_for_chapter' => 'Přejít na jinou kapitolu',\n    'breadcrumb_siblings_for_book' => 'Přejít na jinou knihu',\n    'breadcrumb_siblings_for_bookshelf' => 'Přejít na jinou polici',\n\n    // Permissions and restrictions\n    'permissions' => 'Oprávnění',\n    'permissions_desc' => 'Nastavte oprávnění, které změní výchozích oprávnění pochazejících z uživatelské role.',\n    'permissions_book_cascade' => 'Oprávnění nastavená v knihách budou automaticky kaskádována do podřízených kapitol a stránek, pokud nemají svá vlastní oprávnění.',\n    'permissions_chapter_cascade' => 'Oprávnění nastavená v knihách budou automaticky kaskádována do podřízených kapitol a stránek, pokud nemají svá vlastní oprávnění.',\n    'permissions_save' => 'Uložit oprávnění',\n    'permissions_owner' => 'Vlastník',\n    'permissions_role_everyone_else' => 'Všichni ostatní',\n    'permissions_role_everyone_else_desc' => 'Nastavte oprávnění pro všechny role, které nejsou výslovně přepsány.',\n    'permissions_role_override' => 'Přepsat oprávnění pro roli',\n    'permissions_inherit_defaults' => 'Zdědit výchozí oprávnění',\n\n    // Search\n    'search_results' => 'Výsledky hledání',\n    'search_total_results_found' => '{1}Nalezen :count výsledek|[2,4]Nalezeny :count výsledky|[5,*]Nalezeno :count výsledků',\n    'search_clear' => 'Vymazat hledání',\n    'search_no_pages' => 'Tomuto hledání neodpovídají žádné stránky',\n    'search_for_term' => 'Hledat :term',\n    'search_more' => 'Další výsledky',\n    'search_advanced' => 'Rozšířené hledání',\n    'search_terms' => 'Hledané výrazy',\n    'search_content_type' => 'Typ obsahu',\n    'search_exact_matches' => 'Přesné shody',\n    'search_tags' => 'Hledat štítky',\n    'search_options' => 'Možnosti',\n    'search_viewed_by_me' => 'Zobrazeno mnou',\n    'search_not_viewed_by_me' => 'Nezobrazeno mnou',\n    'search_permissions_set' => 'Sada oprávnění',\n    'search_created_by_me' => 'Vytvořeno mnou',\n    'search_updated_by_me' => 'Aktualizováno mnou',\n    'search_owned_by_me' => 'Patřící mně',\n    'search_date_options' => 'Možnosti data',\n    'search_updated_before' => 'Aktualizováno před',\n    'search_updated_after' => 'Aktualizováno po',\n    'search_created_before' => 'Vytvořeno před',\n    'search_created_after' => 'Vytvořeno po',\n    'search_set_date' => 'Nastavit datum',\n    'search_update' => 'Aktualizovat hledání',\n\n    // Shelves\n    'shelf' => 'Police',\n    'shelves' => 'Police',\n    'x_shelves' => '{0}:count polic|{1}:count police|[2,4]:count police|[5,*]:count polic',\n    'shelves_empty' => 'Nebyly vytvořeny žádné police',\n    'shelves_create' => 'Vytvořit novou polici',\n    'shelves_popular' => 'Populární police',\n    'shelves_new' => 'Nové police',\n    'shelves_new_action' => 'Nová Police',\n    'shelves_popular_empty' => 'Nejpopulárnější police se objeví zde.',\n    'shelves_new_empty' => 'Zde se zobrazí nejnovější police.',\n    'shelves_save' => 'Uložit polici',\n    'shelves_books' => 'Knihy na této polici',\n    'shelves_add_books' => 'Přidat knihy do této police',\n    'shelves_drag_books' => 'Přetáhněte knihy níže a přidejte je do této police',\n    'shelves_empty_contents' => 'Tato police neobsahuje žádné knihy',\n    'shelves_edit_and_assign' => 'Upravit polici a přiřadit knihy',\n    'shelves_edit_named' => 'Upravit polici :name',\n    'shelves_edit' => 'Upravit polici',\n    'shelves_delete' => 'Odstranit polici',\n    'shelves_delete_named' => 'Odstranit polici :name',\n    'shelves_delete_explain' => \"Chystáte se smazat polici ':name'. Knihy v ní obsažené zůstanou zachovány.\",\n    'shelves_delete_confirmation' => 'Opravdu chcete odstranit tuto polici?',\n    'shelves_permissions' => 'Oprávnění police',\n    'shelves_permissions_updated' => 'Oprávnění police byla aktualizována',\n    'shelves_permissions_active' => 'Oprávnění police byla aktivována',\n    'shelves_permissions_cascade_warning' => 'Oprávnění v policiích nejsou automaticky kaskádována do obsažených knih. To proto, že kniha může existovat ve více policích. Oprávnění však lze zkopírovat do podřízených knih pomocí níže uvedené možnosti.',\n    'shelves_permissions_create' => 'Oprávnění k vytváření Shelf se používají pouze ke kopírování oprávnění do dětských knih pomocí níže uvedené akce. Nekontrolují schopnost vytvářet knihy.',\n    'shelves_copy_permissions_to_books' => 'Kopírovat oprávnění na knihy',\n    'shelves_copy_permissions' => 'Kopírovat oprávnění',\n    'shelves_copy_permissions_explain' => 'Tímto se použije aktuální nastavení oprávnění police na všechny knihy v ní obsažené. Před aktivací se ujistěte, že byly uloženy všechny změny oprávnění této police.',\n    'shelves_copy_permission_success' => '{1}Oprávnění police byla zkopírována na :count knihu|[2,4]Oprávnění police byla zkopírována na :count knihy|[5,*]Oprávnění police byla zkopírována na :count knih',\n\n    // Books\n    'book' => 'Kniha',\n    'books' => 'Knihy',\n    'x_books' => '{0}:count knih|{1}:count kniha|[2,4]:count knihy|[5,*]:count knih',\n    'books_empty' => 'Nebyly vytvořeny žádné knihy',\n    'books_popular' => 'Oblíbené knihy',\n    'books_recent' => 'Nedávné knihy',\n    'books_new' => 'Nové knihy',\n    'books_new_action' => 'Nová kniha',\n    'books_popular_empty' => 'Zde se zobrazí nejoblíbenější knihy.',\n    'books_new_empty' => 'Zde se zobrazí nejnověji vytvořené knihy.',\n    'books_create' => 'Vytvořit novou knihu',\n    'books_delete' => 'Odstranit knihu',\n    'books_delete_named' => 'Odstranit knihu :bookName',\n    'books_delete_explain' => 'Toto odstraní knihu ‚:bookName‘. Všechny stránky a kapitoly v této knize budou také odstraněny.',\n    'books_delete_confirmation' => 'Opravdu chcete odstranit tuto knihu?',\n    'books_edit' => 'Upravit knihu',\n    'books_edit_named' => 'Upravit knihu :bookName',\n    'books_form_book_name' => 'Název knihy',\n    'books_save' => 'Uložit knihu',\n    'books_permissions' => 'Oprávnění knihy',\n    'books_permissions_updated' => 'Oprávnění knihy byla aktualizována',\n    'books_empty_contents' => 'Pro tuto knihu nebyly vytvořeny žádné stránky ani kapitoly.',\n    'books_empty_create_page' => 'Vytvořit novou stránku',\n    'books_empty_sort_current_book' => 'Seřadit aktuální knihu',\n    'books_empty_add_chapter' => 'Přidat kapitolu',\n    'books_permissions_active' => 'Oprávnění knihy byla aktivována',\n    'books_search_this' => 'Prohledat tuto knihu',\n    'books_navigation' => 'Navigace knihy',\n    'books_sort' => 'Seřadit obsah knihy',\n    'books_sort_desc' => 'Pro přeuspořádání obsahu přesuňte kapitoly a stránky v knize. Mohou být přidány další knihy, které umožní snadný přesun kapitol a stránek mezi knihami. Volitelně lze nastavit pravidlo automatického řazení, aby se při změnách automaticky seřadil obsah této knihy.',\n    'books_sort_auto_sort' => 'Možnost automatického řazení',\n    'books_sort_auto_sort_active' => 'Aktivní automatické řazení: :sortName',\n    'books_sort_named' => 'Seřadit knihu :bookName',\n    'books_sort_name' => 'Seřadit podle názvu',\n    'books_sort_created' => 'Seřadit podle data vytvoření',\n    'books_sort_updated' => 'Seřadit podle data aktualizace',\n    'books_sort_chapters_first' => 'Kapitoly jako první',\n    'books_sort_chapters_last' => 'Kapitoly jako poslední',\n    'books_sort_show_other' => 'Zobrazit ostatní knihy',\n    'books_sort_save' => 'Uložit nové pořadí',\n    'books_sort_show_other_desc' => 'Přidejte sem další knihy, abyste je zahrnuli do operace třídění, a umožněte snadnou křížovou reorganizaci.',\n    'books_sort_move_up' => 'Posunout Nahoru',\n    'books_sort_move_down' => 'Posunout dolů',\n    'books_sort_move_prev_book' => 'Přesunout se na předchozí knihu',\n    'books_sort_move_next_book' => 'Přesunout se na další knihu',\n    'books_sort_move_prev_chapter' => 'Přesunout se do předchozí kapitoly',\n    'books_sort_move_next_chapter' => 'Přesunout se do další kapitoly',\n    'books_sort_move_book_start' => 'Přesunout se na začátek knihy',\n    'books_sort_move_book_end' => 'Přesunout se na konec knihy',\n    'books_sort_move_before_chapter' => 'Přesunout se před kapitolu',\n    'books_sort_move_after_chapter' => 'Přesunout se za kapitolu',\n    'books_copy' => 'Kopírovat knihu',\n    'books_copy_success' => 'Kniha byla úspěšně zkopírována',\n\n    // Chapters\n    'chapter' => 'Kapitola',\n    'chapters' => 'Kapitoly',\n    'x_chapters' => '{0}:count kapitol|{1}:count kapitola|[2,4]:count kapitoly|[5,*]:count kapitol',\n    'chapters_popular' => 'Populární kapitoly',\n    'chapters_new' => 'Nová kapitola',\n    'chapters_create' => 'Vytvořit novou kapitolu',\n    'chapters_delete' => 'Odstranit kapitolu',\n    'chapters_delete_named' => 'Odstranit kapitolu :chapterName',\n    'chapters_delete_explain' => 'Toto odstraní kapitolu ‚:chapterName‘. Všechny stránky v této kapitole budou také odstraněny.',\n    'chapters_delete_confirm' => 'Opravdu chcete odstranit tuto kapitolu?',\n    'chapters_edit' => 'Upravit kapitolu',\n    'chapters_edit_named' => 'Upravit kapitolu :chapterName',\n    'chapters_save' => 'Uložit kapitolu',\n    'chapters_move' => 'Přesunout kapitolu',\n    'chapters_move_named' => 'Přesunout kapitolu :chapterName',\n    'chapters_copy' => 'Kopírovat kapitolu',\n    'chapters_copy_success' => 'Kapitola byla úspěšně zkopírována',\n    'chapters_permissions' => 'Oprávnění kapitoly',\n    'chapters_empty' => 'Tato kapitola neobsahuje žádné stránky',\n    'chapters_permissions_active' => 'Oprávnění kapitoly byla aktivována',\n    'chapters_permissions_success' => 'Oprávnění kapitoly byla aktualizována',\n    'chapters_search_this' => 'Prohledat tuto kapitolu',\n    'chapter_sort_book' => 'Seřadit knihy',\n\n    // Pages\n    'page' => 'Stránka',\n    'pages' => 'Stránky',\n    'x_pages' => '{0}:count stran|{1}:count strana|[2,4]:count strany|[5,*]:count stran',\n    'pages_popular' => 'Populární stránky',\n    'pages_new' => 'Nová stránka',\n    'pages_attachments' => 'Přílohy',\n    'pages_navigation' => 'Obsah stránky',\n    'pages_delete' => 'Odstranit stránku',\n    'pages_delete_named' => 'Odstranit stránku :pageName',\n    'pages_delete_draft_named' => 'Odstranit koncept stránky :pageName',\n    'pages_delete_draft' => 'Odstranit koncept stránky',\n    'pages_delete_success' => 'Stránka odstraněna',\n    'pages_delete_draft_success' => 'Koncept stránky odstraněn',\n    'pages_delete_warning_template' => 'Tato stránka je aktivní výchozí šablona pro nějakou knihu či kapitolu. Tyto knihy nebo kapitoly již nebudou mít výchozí šablonu stránky přiřazenou po odstranění této stránky.',\n    'pages_delete_confirm' => 'Opravdu chcete odstranit tuto stránku?',\n    'pages_delete_draft_confirm' => 'Opravdu chcete odstranit tento koncept stránky?',\n    'pages_editing_named' => 'Úpravy stránky :pageName',\n    'pages_edit_draft_options' => 'Možnosti konceptu',\n    'pages_edit_save_draft' => 'Uložit koncept',\n    'pages_edit_draft' => 'Upravit koncept stránky',\n    'pages_editing_draft' => 'Úprava konceptu',\n    'pages_editing_page' => 'Úpravy stránky',\n    'pages_edit_draft_save_at' => 'Koncept uložen v ',\n    'pages_edit_delete_draft' => 'Odstranit koncept',\n    'pages_edit_delete_draft_confirm' => 'Jste si jisti, že chcete odstranit změny vašich konceptů? Všechny vaše změny, od posledního úplného uložení, budou ztraceny a editor bude aktualizován s nejnovějším stavem nekonceptu stránky.',\n    'pages_edit_discard_draft' => 'Zahodit koncept',\n    'pages_edit_switch_to_markdown' => 'Přepnout na Markdown Editor',\n    'pages_edit_switch_to_markdown_clean' => '(Prostý zápis)',\n    'pages_edit_switch_to_markdown_stable' => '(Formátovaný zápis)',\n    'pages_edit_switch_to_wysiwyg' => 'Přepnout na WYSIWYG Editor',\n    'pages_edit_switch_to_new_wysiwyg' => 'Přepnout na nový WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(V beta testování)',\n    'pages_edit_set_changelog' => 'Nastavit protokol změn',\n    'pages_edit_enter_changelog_desc' => 'Zadejte stručný popis změn, které jste provedli',\n    'pages_edit_enter_changelog' => 'Zadejte protokol změn',\n    'pages_editor_switch_title' => 'Přepnout editor',\n    'pages_editor_switch_are_you_sure' => 'Jste si jisti, že chcete změnit editor této stránky?',\n    'pages_editor_switch_consider_following' => 'Při změně editorů zvažte následující:',\n    'pages_editor_switch_consideration_a' => 'Po uložení bude nová možnost editoru použita všemi budoucími editory, včetně těch, které nemusí být schopny změnit typ editoru.',\n    'pages_editor_switch_consideration_b' => 'To může za určitých okolností vést ke ztrátě podrobností a syntaxe.',\n    'pages_editor_switch_consideration_c' => 'Změny tagu nebo seznamu změn, provedené od posledního uložení, nebudou přetrvávat po celé této změně.',\n    'pages_save' => 'Uložit stránku',\n    'pages_title' => 'Nadpis stránky',\n    'pages_name' => 'Název stránky',\n    'pages_md_editor' => 'Editor',\n    'pages_md_preview' => 'Náhled',\n    'pages_md_insert_image' => 'Vložit obrázek',\n    'pages_md_insert_link' => 'Vložit odkaz na entitu',\n    'pages_md_insert_drawing' => 'Vložit kresbu',\n    'pages_md_show_preview' => 'Zobrazit náhled',\n    'pages_md_sync_scroll' => 'Synchronizovat náhled',\n    'pages_md_plain_editor' => 'Upravovat prostý text',\n    'pages_drawing_unsaved' => 'Nalezen neuložený výkres',\n    'pages_drawing_unsaved_confirm' => 'Byly nalezeny neuložené kresby z předchozí neúspěšné pokusu o uložení kresby. Chcete je obnovit a pokračovat v úpravě této neuložené kresby?',\n    'pages_not_in_chapter' => 'Stránka není v kapitole',\n    'pages_move' => 'Přesunout stránku',\n    'pages_copy' => 'Kopírovat stránku',\n    'pages_copy_desination' => 'Cíl kopírování',\n    'pages_copy_success' => 'Stránka byla zkopírována',\n    'pages_permissions' => 'Oprávnění stránky',\n    'pages_permissions_success' => 'Oprávnění stránky byla aktualizována',\n    'pages_revision' => 'Revize',\n    'pages_revisions' => 'Revize stránky',\n    'pages_revisions_desc' => 'Níže uvedené jsou všechny minulé revize této stránky. Můžete se podívat zpět, porovnat a obnovit staré verze stránek, pokud to dovolí oprávnění. Úplná historie stránky nemusí být plně zohledněna, protože v závislosti na konfiguraci systému mohou být staré revize automaticky smazány.',\n    'pages_revisions_named' => 'Revize stránky pro :pageName',\n    'pages_revision_named' => 'Revize stránky pro :pageName',\n    'pages_revision_restored_from' => 'Obnoveno z #:id; :summary',\n    'pages_revisions_created_by' => 'Vytvořeno uživatelem',\n    'pages_revisions_date' => 'Datum revize',\n    'pages_revisions_number' => 'Č. ',\n    'pages_revisions_sort_number' => 'Číslo revize',\n    'pages_revisions_numbered' => 'Revize č. :id',\n    'pages_revisions_numbered_changes' => 'Změny revize č. :id',\n    'pages_revisions_editor' => 'Typ editoru',\n    'pages_revisions_changelog' => 'Protokol změn',\n    'pages_revisions_changes' => 'Změny',\n    'pages_revisions_current' => 'Aktuální verze',\n    'pages_revisions_preview' => 'Náhled',\n    'pages_revisions_restore' => 'Obnovit',\n    'pages_revisions_none' => 'Tato stránka nemá žádné revize',\n    'pages_copy_link' => 'Kopírovat odkaz',\n    'pages_edit_content_link' => 'Přejít na sekci v editoru',\n    'pages_pointer_enter_mode' => 'Zadejte režim výběru sekce',\n    'pages_pointer_label' => 'Možnosti sekce stránky',\n    'pages_pointer_permalink' => 'Trvalý odkaz sekce stránky',\n    'pages_pointer_include_tag' => 'Sekce stránky obsahuje štítek',\n    'pages_pointer_toggle_link' => 'Režim trvalého odkazu, stiskem zobrazíte značku',\n    'pages_pointer_toggle_include' => 'Zahrnout režim značek, stiskněte pro zobrazení trvalého odkazu',\n    'pages_permissions_active' => 'Oprávnění stránky byla aktivována',\n    'pages_initial_revision' => 'První vydání',\n    'pages_references_update_revision' => 'Automatická aktualizace interních odkazů',\n    'pages_initial_name' => 'Nová stránka',\n    'pages_editing_draft_notification' => 'Právě upravujete koncept, který byl uložen :timeDiff.',\n    'pages_draft_edited_notification' => 'Tato stránka se od té doby změnila. Je doporučeno aktuální koncept zahodit.',\n    'pages_draft_page_changed_since_creation' => 'Tato stránka byla aktualizována od vytvoření tohoto konceptu. Doporučuje se zrušit tento koncept nebo se postarat o to, abyste si nepřepsali žádné již zadané změny.',\n    'pages_draft_edit_active' => [\n        'start_a' => '{1}:count uživatel začal upravovat tuto stránku|[2,4]:count uživatelé začali upravovat tuto stránku|[5,*]:count uživatelů začalo upravovat tuto stránku',\n        'start_b' => ':userName začal/a upravovat tuto stránku',\n        'time_a' => 'od doby, kdy byla tato stránky naposledy aktualizována',\n        'time_b' => 'v posledních minutách (:minCount min.)',\n        'message' => ':start :time. Dávejte pozor abyste nepřepsali změny ostatním!',\n    ],\n    'pages_draft_discarded' => 'Koncept byl zahozen. Editor nyní obsahuje aktuální verzi stránky',\n    'pages_draft_deleted' => 'Koncept byl zahozen. Editor nyní obsahuje aktuální verzi stránky',\n    'pages_specific' => 'Konkrétní stránka',\n    'pages_is_template' => 'Šablona stránky',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Skrýt/Zobrazit postranní panel',\n    'page_tags' => 'Štítky stránky',\n    'chapter_tags' => 'Štítky kapitoly',\n    'book_tags' => 'Štítky knihy',\n    'shelf_tags' => 'Štítky police',\n    'tag' => 'Štítek',\n    'tags' =>  'Štítky',\n    'tags_index_desc' => 'Tagy mohou být použity pro obsah v rámci systému pro pružnou formu kategorizace. Tagy mohou mít klíč i hodnotu, přičemž hodnota je nepovinná. Po aplikaci může být obsah dotazován pomocí názvu a hodnoty štítku.',\n    'tag_name' =>  'Název štítku',\n    'tag_value' => 'Hodnota štítku (volitelné)',\n    'tags_explain' => \"Přidejte si štítky pro lepší kategorizaci knih. \\n Štítky mohou nést i hodnotu pro detailnější klasifikaci.\",\n    'tags_add' => 'Přidat další štítek',\n    'tags_remove' => 'Odstranit tento štítek',\n    'tags_usages' => 'Počet použití štítku',\n    'tags_assigned_pages' => 'Přiřazeno ke stránkám',\n    'tags_assigned_chapters' => 'Přiřazeno ke kapitolám',\n    'tags_assigned_books' => 'Přiřazeno ke knihám',\n    'tags_assigned_shelves' => 'Přiřazeno k policím',\n    'tags_x_unique_values' => '{1}:count jedinečná hodnota|[2,4]:count jedinečné hodnoty|[5,*]:count jedinečných hodnot',\n    'tags_all_values' => 'Všechny hodnoty',\n    'tags_view_tags' => 'Zobrazit štítky',\n    'tags_view_existing_tags' => 'Zobrazit existující štítky',\n    'tags_list_empty_hint' => 'Štítky mohou být přiřazeny pomocí postranního panelu editoru stránky nebo při úpravách podrobností knihy, kapitoly nebo police.',\n    'attachments' => 'Přílohy',\n    'attachments_explain' => 'Nahrajte soubory nebo připojte odkazy, které se zobrazí na stránce. Budou k nalezení v postranní liště.',\n    'attachments_explain_instant_save' => 'Změny zde provedené se okamžitě ukládají.',\n    'attachments_upload' => 'Nahrát soubor',\n    'attachments_link' => 'Připojit odkaz',\n    'attachments_upload_drop' => 'Případně můžete přetáhnout soubor zde, abyste jej mohli nahrát jako přílohu.',\n    'attachments_set_link' => 'Nastavit odkaz',\n    'attachments_delete' => 'Jste si jisti, že chcete odstranit tuto přílohu?',\n    'attachments_dropzone' => 'Soubory nahrajete přetažením sem',\n    'attachments_no_files' => 'Žádné soubory nebyly nahrány',\n    'attachments_explain_link' => 'Můžete pouze připojit odkaz pokud nechcete nahrávat soubor přímo. Může to být odkaz na jinou stránku nebo na soubor v cloudu.',\n    'attachments_link_name' => 'Název odkazu',\n    'attachment_link' => 'Odkaz na přílohu',\n    'attachments_link_url' => 'Odkaz na soubor',\n    'attachments_link_url_hint' => 'URL stránky nebo souboru',\n    'attach' => 'Připojit',\n    'attachments_insert_link' => 'Přidat odkaz na přílohu do stránky',\n    'attachments_edit_file' => 'Upravit soubor',\n    'attachments_edit_file_name' => 'Název souboru',\n    'attachments_edit_drop_upload' => 'Přetáhněte sem soubor myší nebo klikněte pro nahrání nového souboru a následné přepsání starého',\n    'attachments_order_updated' => 'Pořadí příloh aktualizováno',\n    'attachments_updated_success' => 'Podrobnosti příloh aktualizovány',\n    'attachments_deleted' => 'Příloha byla odstraněna',\n    'attachments_file_uploaded' => 'Soubor byl nahrán',\n    'attachments_file_updated' => 'Soubor byl aktualizován',\n    'attachments_link_attached' => 'Odkaz byl přiložen ke stránce',\n    'templates' => 'Šablony',\n    'templates_set_as_template' => 'Tato stránka je šablona',\n    'templates_explain_set_as_template' => 'Tuto stránku můžete nastavit jako šablonu, aby byl její obsah využit při vytváření dalších stránek. Ostatní uživatelé budou moci použít tuto šablonu, pokud mají oprávnění k zobrazení této stránky.',\n    'templates_replace_content' => 'Nahradit obsah stránky',\n    'templates_append_content' => 'Připojit za obsah stránky',\n    'templates_prepend_content' => 'Připojit před obsah stránky',\n\n    // Profile View\n    'profile_user_for_x' => 'Uživatelem již :time',\n    'profile_created_content' => 'Vytvořený obsah',\n    'profile_not_created_pages' => ':userName nevytvořil/a žádné stránky',\n    'profile_not_created_chapters' => ':userName nevytvořil/a žádné kapitoly',\n    'profile_not_created_books' => ':userName nevytvořil/a žádné knihy',\n    'profile_not_created_shelves' => ':userName nevytvořil/a žádné knihovny',\n\n    // Comments\n    'comment' => 'Komentář',\n    'comments' => 'Komentáře',\n    'comment_add' => 'Přidat komentář',\n    'comment_none' => 'Žádné komentáře k zobrazení',\n    'comment_placeholder' => 'Zde zadejte komentář',\n    'comment_thread_count' => '{0}:count vláken komentářů|{1}:count vlákno komentářů|[2,4]:count vlákna komentářů|[5,*]:count vláken komentářů',\n    'comment_archived_count' => '[0,1]:count archivováno|[2,4]:count archivována|[5,*]:count archivováno',\n    'comment_archived_threads' => 'Archivovaná vlákna',\n    'comment_save' => 'Uložit komentář',\n    'comment_new' => 'Nový komentář',\n    'comment_created' => 'komentováno :createDiff',\n    'comment_updated' => 'Aktualizováno :updateDiff uživatelem :username',\n    'comment_updated_indicator' => 'Aktualizováno',\n    'comment_deleted_success' => 'Komentář odstraněn',\n    'comment_created_success' => 'Komentář přidán',\n    'comment_updated_success' => 'Komentář aktualizován',\n    'comment_archive_success' => 'Komentář archivován',\n    'comment_unarchive_success' => 'Komentář od-archivován',\n    'comment_view' => 'Zobrazit komentář',\n    'comment_jump_to_thread' => 'Přejít na vlákno',\n    'comment_delete_confirm' => 'Opravdu chcete odstranit tento komentář?',\n    'comment_in_reply_to' => 'Odpověď na :commentId',\n    'comment_reference' => 'Odkaz',\n    'comment_reference_outdated' => '(Zastaralý)',\n    'comment_editor_explain' => 'Zde jsou komentáře, které zůstaly na této stránce. Komentáře lze přidat a spravovat při prohlížení uložené stránky.',\n\n    // Revision\n    'revision_delete_confirm' => 'Opravdu chcete odstranit tuto revizi?',\n    'revision_restore_confirm' => 'Jste si jisti, že chcete obnovit tuto revizi? Aktuální obsah stránky bude nahrazen.',\n    'revision_cannot_delete_latest' => 'Nelze odstranit poslední revizi.',\n\n    // Copy view\n    'copy_consider' => 'Při kopírování obsahu zvažte prosím níže.',\n    'copy_consider_permissions' => 'Vlastní nastavení oprávnění nebudou zkopírovány.',\n    'copy_consider_owner' => 'Stanete se vlastníkem veškerého kopírovaného obsahu.',\n    'copy_consider_images' => 'Soubory obrázků stránky nebudou duplikovány a původní obrázky si zachovají jejich vztah ke stránce, na kterou byly původně nahrány.',\n    'copy_consider_attachments' => 'Přílohy stránky nebudou zkopírovány.',\n    'copy_consider_access' => 'Po změně umístění, vlastníka nebo oprávnění může dojít k tomu, že obsah může být přístupný těm, kteří přístup dříve něměli.',\n\n    // Conversions\n    'convert_to_shelf' => 'Převést na polici',\n    'convert_to_shelf_contents_desc' => 'Tuto knihu můžete převést na novou polici se stejným obsahem. Kapitoly obsažené v této knize budou převedeny na nové knihy. Pokud tato kniha obsahuje jakékoli stránky, které nejsou uvedeny v kapitole, bude tato kniha přejmenována a bude obsahovat tyto stránky a stane se součástí nové police.',\n    'convert_to_shelf_permissions_desc' => 'Veškerá oprávnění nastavená v této knize budou zkopírována do nové police a do všech nových podřazených knih, které nemají vlastní oprávnění. Všimněte si, že oprávnění na policích neobsahují automatickou kaskádu na obsah, jako je tomu u knih.',\n    'convert_book' => 'Převést knihu',\n    'convert_book_confirm' => 'Opravdu chcete převést tuto knihu?',\n    'convert_undo_warning' => 'To nelze tak snadno vrátit zpět.',\n    'convert_to_book' => 'Převést knihu',\n    'convert_to_book_desc' => 'Tuto kapitolu můžete převést na novou knihu se stejným obsahem. Veškerá oprávnění nastavená v této kapitole budou zkopírována do nové knihy, ale všechna zděděná oprávnění, z nadřazené knihy nebudou kopírovány, což by mohlo vést ke změně kontroly přístupu.',\n    'convert_chapter' => 'Převést kapitolu',\n    'convert_chapter_confirm' => 'Jste si jisti, že chcete převést tuto kapitolu?',\n\n    // References\n    'references' => 'Odkazy',\n    'references_none' => 'Nebyly nalezeny žádné odkazy na tuto položku.',\n    'references_to_desc' => 'Níže je uveden veškerý obsah o kterém systém ví, že odkazuje na tuto položku.',\n\n    // Watch Options\n    'watch' => 'Sledovat',\n    'watch_title_default' => 'Výchozí vlastnosti',\n    'watch_desc_default' => 'Vrátit sledování pouze do výchozích nastavení oznámení.',\n    'watch_title_ignore' => 'Ignorovat',\n    'watch_desc_ignore' => 'Ignorovat všechna oznámení, včetně oznámení z nastavení uživatelské úrovně.',\n    'watch_title_new' => 'Nová stránka',\n    'watch_desc_new' => 'Upozornit, když je v této položce vytvořena nová stránka.',\n    'watch_title_updates' => 'Všechny aktualizace stránky',\n    'watch_desc_updates' => 'Upozornit na všechny nové stránky a změny stránek.',\n    'watch_desc_updates_page' => 'Upozornit na všechny změny stránky.',\n    'watch_title_comments' => 'Všechny aktualizace a komentáře stránky',\n    'watch_desc_comments' => 'Upozornit na všechny nové stránky, změny stránek a nové komentáře.',\n    'watch_desc_comments_page' => 'Upozornit na změny stránky a nové komentáře.',\n    'watch_change_default' => 'Změnit výchozí předvolby oznámení',\n    'watch_detail_ignore' => 'Ignorování oznámení',\n    'watch_detail_new' => 'Sledování nových stránek',\n    'watch_detail_updates' => 'Sledování nových stránek a aktualizací',\n    'watch_detail_comments' => 'Sledování nových stránek, aktualizací a komentářů',\n    'watch_detail_parent_book' => 'Sledování přes nadřazenou knihu',\n    'watch_detail_parent_book_ignore' => 'Ignorování přes nadřazenou knihu',\n    'watch_detail_parent_chapter' => 'Sledování přes nadřazenou knihu',\n    'watch_detail_parent_chapter_ignore' => 'Ignorování přes nadřazenou knihu',\n];\n"
  },
  {
    "path": "lang/cs/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Nemáte povolení přistupovat na požadovanou stránku.',\n    'permissionJson' => 'Nemáte povolení k provedení požadované akce.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Uživatel s emailem :email již existuje, ale s jinými přihlašovacími údaji.',\n    'auth_pre_register_theme_prevention' => 'Zadané údaje nedovolují zaregistrovat uživatelský účet',\n    'email_already_confirmed' => 'Emailová adresa již byla potvrzena. Zkuste se přihlásit.',\n    'email_confirmation_invalid' => 'Tento potvrzovací odkaz již neplatí nebo už byl použit. Zkuste prosím registraci znovu.',\n    'email_confirmation_expired' => 'Tento potvrzovací odkaz již neplatí, byl Vám odeslán nový potvrzovací e-mail.',\n    'email_confirmation_awaiting' => 'E-mailová adresa pro používaný účet musí být potvrzena',\n    'ldap_fail_anonymous' => 'Přístup k adresáři LDAP jako anonymní uživatel (anonymous bind) selhal',\n    'ldap_fail_authed' => 'Přístup k adresáři LDAP pomocí zadaného jména (dn) a hesla selhal',\n    'ldap_extension_not_installed' => 'Není nainstalováno rozšíření LDAP pro PHP',\n    'ldap_cannot_connect' => 'Nelze se připojit k adresáři LDAP. Prvotní připojení selhalo.',\n    'saml_already_logged_in' => 'Již jste přihlášeni',\n    'saml_no_email_address' => 'Nelze najít e-mailovou adresu pro tohoto uživatele v datech poskytnutých externím přihlašovacím systémem',\n    'saml_invalid_response_id' => 'Požadavek z externího ověřovacího systému nebyl rozpoznám procesem, který tato aplikace spustila. Tento problém může způsobit stisknutí tlačítka Zpět po přihlášení.',\n    'saml_fail_authed' => 'Přihlášení pomocí :system selhalo, systém neposkytl úspěšnou autorizaci',\n    'oidc_already_logged_in' => 'Již jste přihlášeni',\n    'oidc_no_email_address' => 'Nelze najít e-mailovou adresu pro tohoto uživatele v datech poskytnutých externím přihlašovacím systémem',\n    'oidc_fail_authed' => 'Přihlášení pomocí :system selhalo, systém neposkytl úspěšnou autorizaci',\n    'social_no_action_defined' => 'Nebyla zvolena žádá akce',\n    'social_login_bad_response' => \"Nastala chyba během přihlašování přes :socialAccount \\n:error\",\n    'social_account_in_use' => 'Tento účet na :socialAccount se již používá. Pokuste se s ním přihlásit volbou Přihlásit přes :socialAccount.',\n    'social_account_email_in_use' => 'Emailová adresa :email se již používá. Pokud máte již máte náš účet, můžete si jej propojit se svým účtem na :socialAccount v nastavení vašeho profilu.',\n    'social_account_existing' => 'Tento účet na :socialAccount je již propojen s vaším profilem zde.',\n    'social_account_already_used_existing' => 'Tento účet na :socialAccount je již používán jiným uživatelem.',\n    'social_account_not_used' => 'Tento účet na :socialAccount není spřažen s žádným uživatelem. Prosím přiřaďtě si jej v nastavení svého profilu.',\n    'social_account_register_instructions' => 'Pokud ještě nemáte náš účet, můžete se zaregistrovat pomocí vašeho účtu na :socialAccount.',\n    'social_driver_not_found' => 'Doplněk pro tohoto správce identity nebyl nalezen.',\n    'social_driver_not_configured' => 'Nastavení vašeho účtu na :socialAccount není správné. :socialAccount musí mít vaše svolení pro naší aplikaci vás přihlásit.',\n    'invite_token_expired' => 'Odkaz v pozvánce již bohužel vypršel. Namísto toho ale můžete zkusit resetovat heslo do Vašeho účtu.',\n    'login_user_not_found' => 'Uživatele pro tuto akci se nepodařilo najít.',\n\n    // System\n    'path_not_writable' => 'Nelze zapisovat na cestu k souboru :filePath. Zajistěte aby se dalo nahrávat na server.',\n    'cannot_get_image_from_url' => 'Nelze získat obrázek z adresy :url',\n    'cannot_create_thumbs' => 'Server nedokáže udělat náhledy. Zkontrolujte, že rozšíření GD pro PHP je nainstalováno.',\n    'server_upload_limit' => 'Server nepovoluje nahrávat tak veliké soubory. Zkuste prosím menší soubor.',\n    'server_post_limit' => 'Server nemůže přijmout takové množství dat. Zkuste to znovu s méně daty nebo menším souborem.',\n    'uploaded'  => 'Server nepovoluje nahrávat tak veliké soubory. Zkuste prosím menší soubor.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Nastala chyba během nahrávání souboru',\n    'image_upload_type_error' => 'Typ nahrávaného obrázku je neplatný.',\n    'image_upload_replace_type' => 'Náhrady souboru obrázku musí být stejného typu',\n    'image_upload_memory_limit' => 'Nepodařilo se zpracovat nahrávaný obrázek anebo vytvořit náhledy z důvodu omezených systémových prostředků.',\n    'image_thumbnail_memory_limit' => 'Nepodařilo se vytvořit všechny velikostní varianty obrázku z důvodu omezených systémových prostředků.',\n    'image_gallery_thumbnail_memory_limit' => 'Nepodařilo se vytvořit náhledy alba z důvodu omezených systémových prostředků.',\n    'drawing_data_not_found' => 'Data výkresu nelze načíst. Výkresový soubor již nemusí existovat nebo nemusí mít oprávnění k němu přistupovat.',\n\n    // Attachments\n    'attachment_not_found' => 'Příloha nenalezena',\n    'attachment_upload_error' => 'Nastala chyba během nahrávání přiloženého souboru',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Nepovedlo se uložit koncept. Než stránku uložíte, ujistěte se, že jste připojeni k internetu.',\n    'page_draft_delete_fail' => 'Nepodařilo se odstranit koncept stránky a načíst její aktuální obsah',\n    'page_custom_home_deletion' => 'Nelze odstranit tuto stránku, protože je nastavena jako uvítací stránka',\n\n    // Entities\n    'entity_not_found' => 'Prvek nenalezen',\n    'bookshelf_not_found' => 'Police nenalezena',\n    'book_not_found' => 'Kniha nenalezena',\n    'page_not_found' => 'Stránka nenalezena',\n    'chapter_not_found' => 'Kapitola nenalezena',\n    'selected_book_not_found' => 'Vybraná kniha nebyla nalezena',\n    'selected_book_chapter_not_found' => 'Zvolená kniha nebo kapitola nebyla nalezena',\n    'guests_cannot_save_drafts' => 'Nepřihlášení návštěvníci nemohou ukládat koncepty',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Nemůžete odstranit posledního administrátora',\n    'users_cannot_delete_guest' => 'Uživatele Host není možno odstranit',\n    'users_could_not_send_invite' => 'Nebylo možné vytvořit uživatele, protože se nepodařilo odeslat email s pozvánkou',\n\n    // Roles\n    'role_cannot_be_edited' => 'Tuto roli nelze editovat',\n    'role_system_cannot_be_deleted' => 'Toto je systémová role a nelze jí odstranit',\n    'role_registration_default_cannot_delete' => 'Tuto roli nelze odstranit dokud je nastavená jako výchozí role pro registraci nových uživatelů',\n    'role_cannot_remove_only_admin' => 'Tento uživatel má roli administrátora. Přiřaďte roli administrátora někomu jinému než jí odeberete zde.',\n\n    // Comments\n    'comment_list' => 'Při načítání komentářů nastala chyba.',\n    'cannot_add_comment_to_draft' => 'Nemůžete přidávat komentáře ke konceptu.',\n    'comment_add' => 'Při přidávání / aktualizaci komentáře nastala chyba.',\n    'comment_delete' => 'Při odstraňování komentáře nastala chyba.',\n    'empty_comment' => 'Nemůžete přidat prázdný komentář.',\n\n    // Error pages\n    '404_page_not_found' => 'Stránka nenalezena',\n    'sorry_page_not_found' => 'Omlouváme se, ale stránka, kterou hledáte, nebyla nalezena.',\n    'sorry_page_not_found_permission_warning' => 'Pokud očekáváte, že by stránka měla existovat, možná jen nemáte oprávnění pro její zobrazení.',\n    'image_not_found' => 'Obrázek nenalezen',\n    'image_not_found_subtitle' => 'Omlouváme se, ale obrázek, který hledáte, nebyl nalezen.',\n    'image_not_found_details' => 'Pokud očekáváte, že by obrázel měl existovat, tak byl zřejmě již odstraněn.',\n    'return_home' => 'Návrat domů',\n    'error_occurred' => 'Nastala chyba',\n    'app_down' => ':appName je momentálně vypnutá',\n    'back_soon' => 'Brzy bude opět v provozu.',\n\n    // Import\n    'import_zip_cant_read' => 'Nelze načíst ZIP soubor.',\n    'import_zip_cant_decode_data' => 'Nelze najít a dekódovat data.json v archivu ZIP.',\n    'import_zip_no_data' => 'ZIP archiv neobsahuje knihy, kapitoly nebo stránky.',\n    'import_zip_data_too_large' => 'Obsah souboru data.json v archivu ZIP překračuje maximální povolenou velikost.',\n    'import_validation_failed' => 'Importování ZIP selhalo s chybami:',\n    'import_zip_failed_notification' => 'Nepodařilo se naimportovat ZIP soubor.',\n    'import_perms_books' => 'Chybí vám požadovaná oprávnění k vytvoření knih.',\n    'import_perms_chapters' => 'Chybí vám požadovaná oprávnění k vytvoření kapitol.',\n    'import_perms_pages' => 'Chybí vám požadovaná oprávnění k vytvoření stránek.',\n    'import_perms_images' => 'Chybí vám požadovaná oprávnění k vytvoření obrázků.',\n    'import_perms_attachments' => 'Chybí vám požadovaná oprávnění k vytvoření příloh.',\n\n    // API errors\n    'api_no_authorization_found' => 'V požadavku nebyl nalezen žádný autorizační token',\n    'api_bad_authorization_format' => 'V požadavku byl nalezen autorizační token, ale jeho formát se zdá být chybný',\n    'api_user_token_not_found' => 'Pro zadaný autorizační token nebyl nalezen žádný odpovídající API token',\n    'api_incorrect_token_secret' => 'Poskytnutý Token Secret neodpovídá použitému API tokenu',\n    'api_user_no_api_permission' => 'Vlastník použitého API tokenu nemá oprávnění provádět API volání',\n    'api_user_token_expired' => 'Platnost autorizačního tokenu vypršela',\n    'api_cookie_auth_only_get' => 'Při používání API s ověřováním pomocí souborů cookie jsou povoleny pouze požadavky GET',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Při posílání testovacího e-mailu nastala chyba:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'URL adresa neodpovídá povoleným SSR poskytovatelům',\n];\n"
  },
  {
    "path": "lang/cs/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Nový komentář na stránce: :pageName',\n    'new_comment_intro' => 'Uživatel/ka okomentoval/a stránku v :appName:',\n    'new_page_subject' => 'Nová stránka: :pageName',\n    'new_page_intro' => 'V :appName byla vytvořena nová stránka:',\n    'updated_page_subject' => 'Aktualizovaná stránka: :pageName',\n    'updated_page_intro' => 'V :appName byla aktualizována stránka:',\n    'updated_page_debounce' => 'Po nějakou dobu neobdržíte další oznámení o aktualizaci této stránky stejným editorem, aby se omezil počet stejných zpráv.',\n    'comment_mention_subject' => 'Byli jste zmíněni v komentáři na stránce: :pageName',\n    'comment_mention_intro' => 'Byli jste zmíněni v komentáři na webu :appName:',\n\n    'detail_page_name' => 'Název stránky:',\n    'detail_page_path' => 'Umístění:',\n    'detail_commenter' => 'Komentoval/a:',\n    'detail_comment' => 'Komentář:',\n    'detail_created_by' => 'Vytvořil/a:',\n    'detail_updated_by' => 'Aktualizoval/a:',\n\n    'action_view_comment' => 'Zobrazit komentář',\n    'action_view_page' => 'Zobrazit stránku',\n\n    'footer_reason' => 'Tato zpráva Vám byla doručena na základě Vašeho :link.',\n    'footer_reason_link' => 'nastavení upozornění',\n];\n"
  },
  {
    "path": "lang/cs/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Předchozí',\n    'next'     => 'Další &raquo;',\n\n];\n"
  },
  {
    "path": "lang/cs/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Heslo musí mít alespoň osm znaků a shodovat se v obou polích.',\n    'user' => \"Uživatel s touto e-mailovou adresou nebyl nalezen.\",\n    'token' => 'Token pro obnovení hesla není platný pro tuto e-mailovou adresu.',\n    'sent' => 'Poslali jsme Vám e-mail s odkazem pro obnovení hesla!',\n    'reset' => 'Vaše heslo bylo obnoveno!',\n\n];\n"
  },
  {
    "path": "lang/cs/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Můj účet',\n\n    'shortcuts' => 'Zkratky',\n    'shortcuts_interface' => 'Nastavení klávesových zkratek',\n    'shortcuts_toggle_desc' => 'Zde můžete povolit nebo zakázat klávesové zkratky systémového rozhraní používané pro navigaci a akce.',\n    'shortcuts_customize_desc' => 'Po výběru vstupu pro zástupce si můžete přizpůsobit všechny klávesové zkratky.',\n    'shortcuts_toggle_label' => 'Klávesové zkratky povoleny',\n    'shortcuts_section_navigation' => 'Navigace',\n    'shortcuts_section_actions' => 'Společné akce',\n    'shortcuts_save' => 'Uložit zkratky',\n    'shortcuts_overlay_desc' => 'Poznámka: Když jsou povoleny zkratky, je k dispozici pomocný překryv stisknutím \"? která zvýrazní dostupné zkratky pro akce viditelné na obrazovce.',\n    'shortcuts_update_success' => 'Nastavení pro zkratky bylo aktualizováno!',\n    'shortcuts_overview_desc' => 'Správa klávesových zkratek, které můžete použít k navigaci systémového uživatelského rozhraní.',\n\n    'notifications' => 'Nastavení upozornění',\n    'notifications_desc' => 'Nastavte si e-mailová oznámení, která dostanete při provedení určitých akcí v systému.',\n    'notifications_opt_own_page_changes' => 'Upozornit na změny stránek u kterých jsem vlastníkem',\n    'notifications_opt_own_page_comments' => 'Upozornit na komentáře na stránkách, které vlastním',\n    'notifications_opt_comment_mentions' => 'Upozornit, když mě někdo zmíní v komentáři',\n    'notifications_opt_comment_replies' => 'Upozornit na odpovědi na mé komentáře',\n    'notifications_save' => 'Uložit nastavení',\n    'notifications_update_success' => 'Nastavení oznámení byla aktualizována!',\n    'notifications_watched' => 'Sledované a ignorované položky',\n    'notifications_watched_desc' => 'Níže jsou položky, které mají vlastní nastavení sledování. Chcete-li aktualizovat vaše předvolby, zobrazte položku a pak upravte možnosti sledování v postranním panelu.',\n\n    'auth' => 'Přístup a zabezpečení',\n    'auth_change_password' => 'Změnit heslo',\n    'auth_change_password_desc' => 'Změňte heslo, které používáte pro přihlášení do aplikace. Musí být minimálně 8 znaků dlouhé.',\n    'auth_change_password_success' => 'Heslo bylo aktualizováno!',\n\n    'profile' => 'Podrobnosti profilu',\n    'profile_desc' => 'Spravujte údaje k vašemu účtu, které vás reprezentují před ostatními uživateli, kromě údajů, které se používají pro komunikaci a přizpůsobení systému.',\n    'profile_view_public' => 'Zobrazit veřejný profil',\n    'profile_name_desc' => 'Nastavte vaše zobrazované jméno, které bude viditelné ostatním uživatelům v systému prostřednictvím provedených aktivit a vlastního obsahu.',\n    'profile_email_desc' => 'Tento e-mail bude použit pro oznámení a přístup do systému v závislosti na systémové autentizaci.',\n    'profile_email_no_permission' => 'Bohužel nemáte oprávnění ke změně své e-mailové adresy. Pokud ji chcete změnit, musíte požádat správce, aby provedl tuto změnu za vás.',\n    'profile_avatar_desc' => 'Vyberte obrázek, který bude použit k vaší prezentaci pro ostatní v systému. Ideálně by tento obrázek měl být čtverec o velikosti 256px na šířku a výšku.',\n    'profile_admin_options' => 'Možnosti správce',\n    'profile_admin_options_desc' => 'Další možnosti na úrovni administrátora. Například ty, které spravují přiřazení rolí lze najít pro váš účet v sekci \"Nastavení > Uživatelé\".',\n\n    'delete_account' => 'Smazat účet',\n    'delete_my_account' => 'Smazat můj účet',\n    'delete_my_account_desc' => 'Tímto zcela smažete váš účet ze systému. Nebudete moci tento účet obnovit nebo tuto akci vrátit. Obsah, který jste vytvořili, jako jsou vytvořené stránky a nahrané obrázky, zůstanou zachovány.',\n    'delete_my_account_warning' => 'Opravdu si přejete smazat váš účet?',\n];\n"
  },
  {
    "path": "lang/cs/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Nastavení',\n    'settings_save' => 'Uložit nastavení',\n    'system_version' => 'Verze systému',\n    'categories' => 'Kategorie',\n\n    // App Settings\n    'app_customization' => 'Přizpůsobení',\n    'app_features_security' => 'Funkce a zabezpečení',\n    'app_name' => 'Název aplikace',\n    'app_name_desc' => 'Název se bude zobrazovat v záhlaví této aplikace a v e-mailech odesílaných systémem.',\n    'app_name_header' => 'Zobrazovat název aplikace v záhlaví',\n    'app_public_access' => 'Veřejný přístup',\n    'app_public_access_desc' => 'Zapnutím této volby umožníte nepřihlášeným návštěvníkům přístup k Vašemu obsahu v BookStack aplikaci.',\n    'app_public_access_desc_guest' => 'Přístup pro nepřihlášené návštěvníky je možné nastavit přes uživatele \"Guest\".',\n    'app_public_access_toggle' => 'Povolit veřejný přístup',\n    'app_public_viewing' => 'Povolit prohlížení veřejností?',\n    'app_secure_images' => 'Nahrávat obrázky neveřejně a zabezpečeně',\n    'app_secure_images_toggle' => 'Zapnout bezpečnější nahrávání obrázků',\n    'app_secure_images_desc' => 'Z výkonnostních důvodů jsou všechny obrázky veřejně dostupné. Tato volba přidá do adresy obrázku náhodný řetězec, aby nikdo neodhadnul adresu obrázku. Ujistěte se, že server nezobrazuje v adresáři seznam souborů, což by přístup k obrázkům opět otevřelo.',\n    'app_default_editor' => 'Výchozí editor',\n    'app_default_editor_desc' => 'Vyberte, který editor bude použit ve výchozím nastavení při úpravách nových stránek. To může být přepsáno na úrovni stránky, kde to dovolují oprávnění.',\n    'app_custom_html' => 'Vlastní obsah hlavičky HTML',\n    'app_custom_html_desc' => 'Cokoliv sem napíšete bude přidáno na konec sekce <head> v každém místě této aplikace. To se hodí pro přidávání nebo změnu CSS stylů nebo přidání kódu pro analýzu používání (např.: google analytics.).',\n    'app_custom_html_disabled_notice' => 'Na této stránce nastavení je zakázán vlastní obsah HTML hlavičky, aby bylo zajištěno, že bude možné vrátit případnou problematickou úpravu.',\n    'app_logo' => 'Logo aplikace',\n    'app_logo_desc' => 'Používá se v záhlaví aplikace, a v jiných oblastech. Tento obrázek by měl být velký 86px. Větší obrázky budou zmenšeny.',\n    'app_icon' => 'Ikona aplikace',\n    'app_icon_desc' => 'Tato ikona se používá pro záložky prohlížeče a ikony zástupců. Obrazek by měl být čtverec o velikosti 256px a formátu PNG.',\n    'app_homepage' => 'Úvodní stránka aplikace',\n    'app_homepage_desc' => 'Zvolte si zobrazení, které se použije jako úvodní stránka. U zvolených stránek bude ignorováno jejich oprávnění.',\n    'app_homepage_select' => 'Zvolte stránku',\n    'app_footer_links' => 'Odkazy v zápatí',\n    'app_footer_links_desc' => 'Přidejte odkazy, které se zobrazí v zápatí webu. Ty se zobrazí ve spodní části většiny stránek, včetně těch, které nevyžadují přihlášení. K použití překladů definovaných systémem můžete použít štítek „trans::<key>“. Například: Použití „trans::common.privacy_policy“ přeloží text na „Zásady ochrany osobních údajů“ a „trans::common.terms_of_service“ poskytne přeložený text „Podmínky služby“.',\n    'app_footer_links_label' => 'Text odkazu',\n    'app_footer_links_url' => 'URL odkazu',\n    'app_footer_links_add' => 'Přidat odkaz do zápatí',\n    'app_disable_comments' => 'Vypnutí komentářů',\n    'app_disable_comments_toggle' => 'Vypnout komentáře',\n    'app_disable_comments_desc' => 'Vypne komentáře napříč všemi stránkami. <br> Existující komentáře se přestanou zobrazovat.',\n\n    // Color settings\n    'color_scheme' => 'Barevné schéma aplikace',\n    'color_scheme_desc' => 'Nastavte barvy pro uživatelské rozhraní. Barvy mohou být konfigurovány samostatně pro tmavý a světlý režim, aby co nejlépe odpovídaly tématu a zajistily čitelnost.',\n    'ui_colors_desc' => 'Nastavte primární barvu aplikace a výchozí barvu odkazu. Primární barva je použitá hlavně na banneru hlavičky, tlačítkách a ozdobách rozhraní. Výchozí barva odkazu se používá pro odkazy a akce napříč psaným textem a rozhraním aplikace.',\n    'app_color' => 'Hlavní barva',\n    'link_color' => 'Výchozí barva odkazu',\n    'content_colors_desc' => 'Nastaví barvy pro všechny prvky v organizační struktuře stránky. Pro lepší čitelnost doporučujeme zvolit barvy s podobným jasem, jakou mají výchozí barvy.',\n    'bookshelf_color' => 'Barva police',\n    'book_color' => 'Barva knihy',\n    'chapter_color' => 'Barva kapitoly',\n    'page_color' => 'Barva stránky',\n    'page_draft_color' => 'Barva návrhu stránky',\n\n    // Registration Settings\n    'reg_settings' => 'Nastavení registrace',\n    'reg_enable' => 'Povolení registrace',\n    'reg_enable_toggle' => 'Povolit registrace',\n    'reg_enable_desc' => 'Pokud jsou povoleny registrace, bude se uživatel moci sám registrovat jako uživatel aplikace. Po registraci dostane jednu výchozí uživatelskou roli.',\n    'reg_default_role' => 'Role přiřazená po registraci',\n    'reg_enable_external_warning' => 'Pokud je povolené externí ověřování přes LDAP nebo SAML, je výše uvedená možnost ignorována. Uživatelský účet budou automaticky vytvořen i neexistujícímu uživateli, jakmile se úspěšně přihlásí přes použitý externí přihlašovací systém.',\n    'reg_email_confirmation' => 'Ověření e-mailu',\n    'reg_email_confirmation_toggle' => 'Vyžadovat ověření e-mailu',\n    'reg_confirm_email_desc' => 'Pokud je zapnuté Omezení registrace podle domény, bude e-mail ověřován vždy a tato volba bude ignorována.',\n    'reg_confirm_restrict_domain' => 'Omezit registraci podle domény',\n    'reg_confirm_restrict_domain_desc' => 'Zadejte seznam e-mailových domén oddělených čárkami, na které chcete registraci omezit. Registrujícímu se uživateli bude zaslán e-mail, aby ověřil svoji e-mailovou adresu před tím, než mu bude přístup do aplikace povolen. <br> Upozorňujeme, že po úspěšné registraci může uživatel svoji e-mailovou adresu změnit.',\n    'reg_confirm_restrict_domain_placeholder' => 'Žádná omezení nebyla nastavena',\n\n    // Sorting Settings\n    'sorting' => 'Seznamy a řazení',\n    'sorting_book_default' => 'Výchozí řazení knih',\n    'sorting_book_default_desc' => 'Vybere výchozí pravidlo řazení pro nové knihy. Řazení neovlivní existující knihy a může být upraveno u konkrétní knihy.',\n    'sorting_rules' => 'Pravidla řazení',\n    'sorting_rules_desc' => 'Toto jsou předem definovaná pravidla řazení, která mohou být použita na webu.',\n    'sort_rule_assigned_to_x_books' => 'Přiřazeno k :count knize|Přiřazeno :count knihám',\n    'sort_rule_create' => 'Vytvořit pravidlo řazení',\n    'sort_rule_edit' => 'Upravit pravidlo řazení',\n    'sort_rule_delete' => 'Odstranit pravidlo řazení',\n    'sort_rule_delete_desc' => 'Odstraní toto pravidlo řazení z webu. Knihy, které jej používají, se vrátí k ručnímu řazení.',\n    'sort_rule_delete_warn_books' => 'Toto pravidlo řazení se v současné době používá na :count knihách. Opravdu ho chcete smazat?',\n    'sort_rule_delete_warn_default' => 'Toto pravidlo řazení je v současné době používáno jako výchozí. Opravdu ho chcete odstranit?',\n    'sort_rule_details' => 'Podrobnosti pravidla pro řazení',\n    'sort_rule_details_desc' => 'Nastavte název pro toto pravidlo, který se zobrazí v seznamu při výběru řazení.',\n    'sort_rule_operations' => 'Možnosti řazení',\n    'sort_rule_operations_desc' => 'Přesunem ze seznamu dostupných operací nastavte akce řazení, které mají být provedeny. Operace se použijí v pořadí od shora dolů. Veškeré změny zde provedené budou při uložení aplikovány na všechny přiřazené knihy.',\n    'sort_rule_available_operations' => 'Dostupné operace',\n    'sort_rule_available_operations_empty' => 'Žádné zbývající operace',\n    'sort_rule_configured_operations' => 'Konfigurované operace',\n    'sort_rule_configured_operations_empty' => 'Přetáhněte/přidejte operace ze seznamu \"Dostupné operace\"',\n    'sort_rule_op_asc' => '(Vzest)',\n    'sort_rule_op_desc' => '(Sest)',\n    'sort_rule_op_name' => 'Název - abecední',\n    'sort_rule_op_name_numeric' => 'Název - číselné',\n    'sort_rule_op_created_date' => 'Datum vytvoření',\n    'sort_rule_op_updated_date' => 'Datum aktualizace',\n    'sort_rule_op_chapters_first' => 'Kapitoly jako první',\n    'sort_rule_op_chapters_last' => 'Kapitoly jako poslední',\n    'sorting_page_limits' => 'Počet zobrazených položek na stránce',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Údržba',\n    'maint_image_cleanup' => 'Pročistění obrázků',\n    'maint_image_cleanup_desc' => 'Prohledá stránky a jejich revize, aby zjistil, které obrázky a kresby jsou momentálně používány a které jsou zbytečné. Zajistěte plnou zálohu databáze a obrázků než se do toho pustíte.',\n    'maint_delete_images_only_in_revisions' => 'Odstranit i obrázky, které se vyskytují pouze ve starých revizích stránky',\n    'maint_image_cleanup_run' => 'Spustit pročištění',\n    'maint_image_cleanup_warning' => 'Nalezeno :count potenciálně nepoužitých obrázků. Jste si jisti, že je chcete odstranit?',\n    'maint_image_cleanup_success' => 'Nalezeno :count potenciálně nepoužitých obrázků a všechny byly odstraněny!',\n    'maint_image_cleanup_nothing_found' => 'Žádné potenciálně nepoužité obrázky nebyly nalezeny. Nic nebylo smazáno.',\n    'maint_send_test_email' => 'Odeslat zkušební e-mail',\n    'maint_send_test_email_desc' => 'Toto pošle zkušební e-mail na vaši e-mailovou adresu uvedenou ve vašem profilu.',\n    'maint_send_test_email_run' => 'Odeslat zkušební e-mail',\n    'maint_send_test_email_success' => 'E-mail odeslán na :address',\n    'maint_send_test_email_mail_subject' => 'Testovací e-mail',\n    'maint_send_test_email_mail_greeting' => 'Zdá se, že posílání e-mailů funguje!',\n    'maint_send_test_email_mail_text' => 'Gratulujeme! Protože jste dostali tento e-mail, zdá se, že nastavení e-mailů je v pořádku.',\n    'maint_recycle_bin_desc' => 'Odstraněné police, knihy, kapitoly a stránky se přesouvají do Koše, aby je bylo možné obnovit nebo trvale smazat. Starší položky v koši mohou být po čase automaticky odstraněny v závislosti na konfiguraci systému.',\n    'maint_recycle_bin_open' => 'Otevřít Koš',\n    'maint_regen_references' => 'Přegenerovat odkazy',\n    'maint_regen_references_desc' => 'Tato akce obnoví referenční index křížových položek v rámci databáze. Toto je obvykle zpracováno automaticky, ale tato akce může být užitečná pro indexování starého obsahu nebo obsahu přidaného neoficiálními metodami.',\n    'maint_regen_references_success' => 'Referenční index byl obnoven!',\n    'maint_timeout_command_note' => 'Poznámka: Tato akce může trvat nějakou dobu, což může vést k vypršení časového limitu v některých webových prostředích. Alternativně se tato akce provádí pomocí příkazu terminálu.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Koš',\n    'recycle_bin_desc' => 'Zde můžete obnovit položky, které byly odstraněny, nebo zvolit jejich trvalé odstranění ze systému. Tento seznam je nefiltrovaný, na rozdíl od podobných seznamů aktivit v systému, kde jsou použity filtry podle oprávnění.',\n    'recycle_bin_deleted_item' => 'Odstraněná položka',\n    'recycle_bin_deleted_parent' => 'Nadřazená položka',\n    'recycle_bin_deleted_by' => 'Odstranil/a',\n    'recycle_bin_deleted_at' => 'Čas odstranění',\n    'recycle_bin_permanently_delete' => 'Trvale odstranit',\n    'recycle_bin_restore' => 'Obnovit',\n    'recycle_bin_contents_empty' => 'Koš je nyní prázdný',\n    'recycle_bin_empty' => 'Vysypat Koš',\n    'recycle_bin_empty_confirm' => 'Toto trvale odstraní všechny položky v Koši včetně obsahu vloženého v každé položce. Opravdu chcete vysypat Koš?',\n    'recycle_bin_destroy_confirm' => 'Tato akce trvale odstraní tuto položku ze systému spolu se všemi položkami uvedenými níže a nebudete je moci obnovit. Opravdu chcete tuto položku trvale odstranit?',\n    'recycle_bin_destroy_list' => 'Položky k trvalému odstranění',\n    'recycle_bin_restore_list' => 'Položky k obnovení',\n    'recycle_bin_restore_confirm' => 'Tato akce obnoví odstraněnou položku včetně veškerého vloženého obsahu do původního umístění. Pokud bylo původní umístění od té doby odstraněno a nyní je v Koši, bude také nutné obnovit nadřazenou položku.',\n    'recycle_bin_restore_deleted_parent' => 'Nadřazená položka této položky byla také odstraněna. Ty zůstanou odstraněny, dokud nebude obnoven i nadřazený objekt.',\n    'recycle_bin_restore_parent' => 'Obnovit nadřazenu položku',\n    'recycle_bin_destroy_notification' => 'Celkem odstraněno :count položek z Koše.',\n    'recycle_bin_restore_notification' => 'Celkem obnoveno :count položek z Koše.',\n\n    // Audit Log\n    'audit' => 'Protokol auditu',\n    'audit_desc' => 'Tento protokol auditu zobrazuje seznam činností zaznamenaných v systému. Tento seznam není filtrován na rozdíl od podobných seznamů aktivit v systému, kde jsou použity filtry podle oprávnění.',\n    'audit_event_filter' => 'Filtr událostí',\n    'audit_event_filter_no_filter' => 'Bez filtru',\n    'audit_deleted_item' => 'Odstraněná položka',\n    'audit_deleted_item_name' => 'Jméno: :name',\n    'audit_table_user' => 'Uživatel',\n    'audit_table_event' => 'Událost',\n    'audit_table_related' => 'Související položka nebo detail',\n    'audit_table_ip' => 'IP adresa',\n    'audit_table_date' => 'Datum aktivity',\n    'audit_date_from' => 'Časový rozsah od',\n    'audit_date_to' => 'Časový rozsah do',\n\n    // Role Settings\n    'roles' => 'Role',\n    'role_user_roles' => 'Uživatelské role',\n    'roles_index_desc' => 'Role se používají ke sdružování uživatelů a k poskytování systémových oprávnění jejich členům. Pokud je uživatel členem více rolí, udělená oprávnění budou uložena a uživatel zdědí všechny schopnosti.',\n    'roles_x_users_assigned' => '1 přiřazený uživatel|:count přiřazených uživatelů',\n    'roles_x_permissions_provided' => '1 oprávnění|:count oprávnění',\n    'roles_assigned_users' => 'Přiřazení uživatelé',\n    'roles_permissions_provided' => 'Poskytnutá oprávnění',\n    'role_create' => 'Vytvořit novou roli',\n    'role_delete' => 'Odstranit roli',\n    'role_delete_confirm' => 'Role \\':roleName\\' bude odstraněna.',\n    'role_delete_users_assigned' => 'Role je přiřazena :userCount uživatelům. Pokud jim chcete náhradou přidělit jinou roli, zvolte jednu z následujících.',\n    'role_delete_no_migration' => \"Nepřiřazovat uživatelům náhradní roli\",\n    'role_delete_sure' => 'Opravdu chcete tuto roli odstranit?',\n    'role_edit' => 'Upravit roli',\n    'role_details' => 'Detaily role',\n    'role_name' => 'Název role',\n    'role_desc' => 'Stručný popis role',\n    'role_mfa_enforced' => 'Vyžaduje Vícefaktorové ověření',\n    'role_external_auth_id' => 'Přihlašovací identifikátory třetích stran',\n    'role_system' => 'Systémová oprávnění',\n    'role_manage_users' => 'Správa uživatelů',\n    'role_manage_roles' => 'Správa rolí a jejich práv',\n    'role_manage_entity_permissions' => 'Správa práv všech knih, kapitol a stránek',\n    'role_manage_own_entity_permissions' => 'Správa práv vlastních knih, kapitol a stránek',\n    'role_manage_page_templates' => 'Správa šablon stránek',\n    'role_access_api' => 'Přístup k systémovému API',\n    'role_manage_settings' => 'Správa nastavení aplikace',\n    'role_export_content' => 'Exportovat obsah',\n    'role_import_content' => 'Importovat obsah',\n    'role_editor_change' => 'Změnit editor stránek',\n    'role_notifications' => 'Přijímat a spravovat oznámení',\n    'role_permission_note_users_and_roles' => 'Tato oprávnění zároveň umožní zobrazit a vyhledat uživatele a role na webu.',\n    'role_asset' => 'Obsahová oprávnění',\n    'roles_system_warning' => 'Berte na vědomí, že přístup k některému ze tří výše uvedených oprávnění může uživateli umožnit změnit svá vlastní oprávnění nebo oprávnění ostatních uživatelů v systému. Přiřazujte role s těmito oprávněními pouze důvěryhodným uživatelům.',\n    'role_asset_desc' => 'Tato oprávnění řídí přístup k obsahu napříč systémem. Specifická oprávnění na knihách, kapitolách a stránkách převáží tato nastavení.',\n    'role_asset_admins' => 'Administrátoři automaticky dostávají přístup k veškerému obsahu, ale tyto volby mohou ukázat nebo skrýt volby v uživatelském rozhraní.',\n    'role_asset_image_view_note' => 'To se týká viditelnosti ve správci obrázků. Skutečný přístup k nahraným souborům obrázků bude záviset na možnosti uložení systémových obrázků.',\n    'role_asset_users_note' => 'Tato oprávnění zároveň umožní zobrazit a vyhledat uživatele v systému.',\n    'role_all' => 'Vše',\n    'role_own' => 'Vlastní',\n    'role_controlled_by_asset' => 'Řídí se obsahem, do kterého jsou nahrávány',\n    'role_save' => 'Uložit roli',\n    'role_users' => 'Uživatelé mající tuto roli',\n    'role_users_none' => 'Žádný uživatel nemá tuto roli',\n\n    // Users\n    'users' => 'Uživatelé',\n    'users_index_desc' => 'Vytváření a správa jednotlivých uživatelských účtů v rámci systému. Uživatelské účty jsou používány pro přihlášení a přiřazování obsahu a aktivity. Přístupová práva jsou primárně založena na roli, ale vlastnictví obsahu uživatele může kromě jiných faktorů také ovlivnit oprávnění a přístup.',\n    'user_profile' => 'Profil uživatele',\n    'users_add_new' => 'Přidat nového uživatele',\n    'users_search' => 'Vyhledávání uživatelů',\n    'users_latest_activity' => 'Nedávná aktivita',\n    'users_details' => 'Údaje o uživateli',\n    'users_details_desc' => 'Nastavte zobrazované jméno a e-mailovou adresu pro tohoto uživatele. E-mailová adresa bude použita pro přihlášení do aplikace.',\n    'users_details_desc_no_email' => 'Nastavte zobrazované jméno pro tohoto uživatele, aby jej ostatní uživatele poznali.',\n    'users_role' => 'Uživatelské role',\n    'users_role_desc' => 'Zvolte role, do kterých chcete uživatele zařadit. Pokud bude uživatel zařazen do více rolí, oprávnění z těchto rolí se sloučí a uživateli bude dovoleno vše, k čemu mají jednotlivé role oprávnění.',\n    'users_password' => 'Heslo uživatele',\n    'users_password_desc' => 'Zadejte heslo pro přihlášení do aplikace. Heslo musí být nejméně 8 znaků dlouhé.',\n    'users_send_invite_text' => 'Uživateli můžete poslat pozvánku e-mailem, která umožní uživateli, aby si zvolil sám svoje heslo do aplikace a nebo můžete zadat heslo sami.',\n    'users_send_invite_option' => 'Poslat uživateli pozvánku e-mailem',\n    'users_external_auth_id' => 'Přihlašovací identifikátor třetích stran',\n    'users_external_auth_id_desc' => 'Při použití externího autentizačního systému (např. SAML2, OIDC nebo LDAP) toto je ID, které spojuje tohoto BookStack uživatele s ověřovacím systémem. Toto pole můžete ignorovat, pokud používáte ověření pomocí e-mailu.',\n    'users_password_warning' => 'Vyplňte pouze v případě, že chcete změnit heslo pro tohoto uživatele.',\n    'users_system_public' => 'Symbolizuje každého nepřihlášeného návštěvníka, který navštívil aplikaci. Nelze ho použít k přihlášení ale je přiřazen automaticky nepřihlášeným.',\n    'users_delete' => 'Odstranit uživatele',\n    'users_delete_named' => 'Odstranit uživatele :userName',\n    'users_delete_warning' => 'Uživatel \\':userName\\' bude zcela odstraněn ze systému.',\n    'users_delete_confirm' => 'Opravdu chcete tohoto uživatele smazat?',\n    'users_migrate_ownership' => 'Převést vlastnictví',\n    'users_migrate_ownership_desc' => 'Zde zvolte jiného uživatele, pokud chcete, aby se stal vlastníkem všech položek aktuálně vlastněných tímto uživatelem.',\n    'users_none_selected' => 'Nebyl zvolen žádný uživatel',\n    'users_edit' => 'Upravit uživatele',\n    'users_edit_profile' => 'Upravit profil',\n    'users_avatar' => 'Obrázek uživatele',\n    'users_avatar_desc' => 'Zvolte obrázek, který bude reprezentovat tohoto uživatele. Měl by být přibližně 256px velký ve tvaru čtverce.',\n    'users_preferred_language' => 'Preferovaný jazyk',\n    'users_preferred_language_desc' => 'Tato volba ovlivní pouze jazyk používaný v uživatelském rozhraní aplikace. Volba nemá vliv na žádný uživateli vytvářený obsah.',\n    'users_social_accounts' => 'Sociální účty',\n    'users_social_accounts_desc' => 'Zobrazit stav připojených sociálních účtů tohoto uživatele. Sociální účty mohou být použity pro přístup do systému vedle hlavního systému ověřování.',\n    'users_social_accounts_info' => 'Zde můžete přidat vaše účty ze sociálních sítí pro pohodlnější přihlašování. Odpojení účtů neznamená, že tato aplikace ztratí práva číst detaily z vašeho účtu. Zakázat této aplikaci přístup k detailům vašeho účtu musíte přímo ve svém profilu na dané sociální síti.',\n    'users_social_connect' => 'Připojit účet',\n    'users_social_disconnect' => 'Odpojit účet',\n    'users_social_status_connected' => 'Připojeno',\n    'users_social_status_disconnected' => 'Odpojeno',\n    'users_social_connected' => 'Účet :socialAccount byl připojen k vašemu profilu.',\n    'users_social_disconnected' => 'Účet :socialAccount byl odpojen od vašeho profilu.',\n    'users_api_tokens' => 'API Tokeny',\n    'users_api_tokens_desc' => 'Vytvořte a spravujte přístupové tokeny používané pro ověření k REST API aplikace BookStack. Oprávnění pro API jsou spravována prostřednictvím uživatele, kterému token patří.',\n    'users_api_tokens_none' => 'Tento uživatel nemá vytvořené žádné API Tokeny',\n    'users_api_tokens_create' => 'Vytvořit Token',\n    'users_api_tokens_expires' => 'Vyprší',\n    'users_api_tokens_docs' => 'Dokumentace API',\n    'users_mfa' => 'Vícefázové ověření',\n    'users_mfa_desc' => 'Nastavit vícefaktorové ověřování jako další vrstvu zabezpečení vašeho uživatelského účtu.',\n    'users_mfa_x_methods' => ':count nastavená metoda|:count nastavených metod',\n    'users_mfa_configure' => 'Konfigurovat metody',\n\n    // API Tokens\n    'user_api_token_create' => 'Vytvořit API Token',\n    'user_api_token_name' => 'Název',\n    'user_api_token_name_desc' => 'Zadejte srozumitelný název tokenu, který vám později může pomoci připomenout účel, za jakým jste token vytvářeli.',\n    'user_api_token_expiry' => 'Platný do',\n    'user_api_token_expiry_desc' => 'Zadejte datum, kdy platnost tokenu vyprší. Po tomto datu nebudou požadavky, které používají tento token, fungovat. Pokud ponecháte pole prázdné, bude tokenu nastavena platnost na dalších 100 let.',\n    'user_api_token_create_secret_message' => 'Ihned po vytvoření tokenu Vám bude vygenerován a zobrazen \"Token ID\" a \"Token Secret\". Upozorňujeme, že \"Token Secret\" bude možné zobrazit pouze jednou, ujistěte se, že si jej poznamenáte a uložíte na bezpečné místo před tím, než budete pokračovat dále.',\n    'user_api_token' => 'API Token',\n    'user_api_token_id' => 'Token ID',\n    'user_api_token_id_desc' => 'Toto je neupravitelný systémový identifikátor generovaný pro tento Token, který musí být uveden v API requestu.',\n    'user_api_token_secret' => 'Token Secret',\n    'user_api_token_secret_desc' => 'Toto je systémem generovaný \"Secret\" pro tento Token, který musí být v API requestech. Toto bude zobrazeno pouze jednou, takže si uložte tuto hodnotu na bezpečné místo.',\n    'user_api_token_created' => 'Token vytvořen :timeAgo',\n    'user_api_token_updated' => 'Token aktualizován :timeAgo',\n    'user_api_token_delete' => 'Odstranit Token',\n    'user_api_token_delete_warning' => 'Tímto plně odstraníte tento API Token s názvem \\':tokenName\\' ze systému.',\n    'user_api_token_delete_confirm' => 'Opravdu chcete odstranit tento API Token?',\n\n    // Webhooks\n    'webhooks' => 'Webhooky',\n    'webhooks_index_desc' => 'Webhooks jsou způsob, jak odeslat data na externí URL, pokud se vyskytnou určité akce a události v systému, které umožňují integraci událostí s externími platformami, jako jsou systémy zasílání zpráv nebo oznámení.',\n    'webhooks_x_trigger_events' => '1 spouštěcí událost|:count spouštěcích událostí',\n    'webhooks_create' => 'Vytvořit nový webhook',\n    'webhooks_none_created' => 'Žádné webhooky nebyly doposud vytvořeny.',\n    'webhooks_edit' => 'Upravit webhook',\n    'webhooks_save' => 'Uložit webhook',\n    'webhooks_details' => 'Podrobnosti webhooku',\n    'webhooks_details_desc' => 'Zadejte uživatelsky přívětivé jméno a koncový bod POST jako umístění pro zasílání dat webhooku.',\n    'webhooks_events' => 'Události webhooku',\n    'webhooks_events_desc' => 'Vyberte všechny události, které by měly spustit tento webhook pro volání.',\n    'webhooks_events_warning' => 'Mějte na paměti, že tyto události budou spouštěny pro všechny vybrané události, i když budou použita vlastní oprávnění. Zajistěte, aby používání tohoto webového háčku nezobrazovalo důvěrné obsahy.',\n    'webhooks_events_all' => 'Všechny události systému',\n    'webhooks_name' => 'Název webhooku',\n    'webhooks_timeout' => 'Časový limit požadavku Webhook (sekundy)',\n    'webhooks_endpoint' => 'Koncový bod webhooku',\n    'webhooks_active' => 'Webhook aktivní',\n    'webhook_events_table_header' => 'Události',\n    'webhooks_delete' => 'Odstranit webhook',\n    'webhooks_delete_warning' => 'Webhook s názvem \\':webhookName\\' bude úplně odstraněn ze systému.',\n    'webhooks_delete_confirm' => 'Opravdu chcete odstranit tento webhook?',\n    'webhooks_format_example' => 'Příklad formátu webhooku',\n    'webhooks_format_example_desc' => 'Webový háček je odesílán jako POST požadavek na konfigurovaný koncový bod ve formátu JSON ve formátu níže. Vlastnosti \"related_item\" a \"url\" jsou volitelné a budou záviset na typu události spuštěné.',\n    'webhooks_status' => 'Stav webhooku',\n    'webhooks_last_called' => 'Poslední volání:',\n    'webhooks_last_errored' => 'Poslední chyba:',\n    'webhooks_last_error_message' => 'Poslední chybová zpráva',\n\n    // Licensing\n    'licenses' => 'Licence',\n    'licenses_desc' => 'Na této stránce naleznete kromě informací o projektech a knihovnách, které se v rámci BookStacku používají, také informace o licencích pro BookStack. Mnoho uvedených projektů lze používat pouze ve vývojovém kontextu.',\n    'licenses_bookstack' => 'BookStack licence',\n    'licenses_php' => 'Licence PHP knihoven',\n    'licenses_js' => 'Licence JavaScript knihoven',\n    'licenses_other' => 'Ostatní licence',\n    'license_details' => 'Podrobnosti o licenci',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/cs/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute musí být přijat.',\n    'active_url'           => ':attribute není platnou URL adresou.',\n    'after'                => ':attribute musí být datum po :date.',\n    'alpha'                => ':attribute může obsahovat pouze písmena.',\n    'alpha_dash'           => ':attribute může obsahovat pouze písmena, číslice, pomlčky a podtržítka. České znaky (á, é, í, ó, ú, ů, ž, š, č, ř, ď, ť, ň) nejsou podporovány.',\n    'alpha_num'            => ':attribute může obsahovat pouze písmena a číslice.',\n    'array'                => ':attribute musí být pole.',\n    'backup_codes'         => 'Zadaný kód není platný nebo již byl použit.',\n    'before'               => ':attribute musí být datum před :date.',\n    'between'              => [\n        'numeric' => ':attribute musí být hodnota mezi :min a :max.',\n        'file'    => ':attribute musí být větší než :min a menší než :max Kilobytů.',\n        'string'  => ':attribute musí být delší než :min a kratší než :max znaků.',\n        'array'   => ':attribute musí obsahovat nejméně :min a nesmí obsahovat více než :max prvků.',\n    ],\n    'boolean'              => ':attribute musí být true nebo false',\n    'confirmed'            => ':attribute nesouhlasí.',\n    'date'                 => ':attribute musí být platné datum.',\n    'date_format'          => ':attribute není platný formát data podle :format.',\n    'different'            => ':attribute a :other se musí lišit.',\n    'digits'               => ':attribute musí být :digits pozic dlouhé.',\n    'digits_between'       => ':attribute musí být dlouhé nejméně :min a nejvíce :max pozic.',\n    'email'                => ':attribute není platný formát.',\n    'ends_with' => ':attribute musí končit jednou z následujících hodnot: :values',\n    'file'                 => ':attribute musí být zadán jako platný soubor.',\n    'filled'               => ':attribute musí být vyplněno.',\n    'gt'                   => [\n        'numeric' => ':attribute musí být větší než :value.',\n        'file'    => 'Velikost souboru :attribute musí být větší než :value kB.',\n        'string'  => 'Počet znaků :attribute musí být větší :value.',\n        'array'   => 'Pole :attribute musí mít více prvků než :value.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute musí být větší nebo rovno :value.',\n        'file'    => 'Velikost souboru :attribute musí být větší nebo rovno :value kB.',\n        'string'  => 'Počet znaků :attribute musí být větší nebo rovno :value.',\n        'array'   => 'Pole :attribute musí mít :value prvků nebo více.',\n    ],\n    'exists'               => 'Zvolená hodnota pro :attribute není platná.',\n    'image'                => ':attribute musí být obrázek.',\n    'image_extension'      => ':attribute musí mít platné a podporované rozšíření obrázku.',\n    'in'                   => 'Zvolená hodnota pro :attribute je neplatná.',\n    'integer'              => ':attribute musí být celé číslo.',\n    'ip'                   => ':attribute musí být platnou IP adresou.',\n    'ipv4'                 => ':attribute musí být platná IPv4 adresa.',\n    'ipv6'                 => ':attribute musí být platná IPv6 adresa.',\n    'json'                 => ':attribute musí být platný JSON řetězec.',\n    'lt'                   => [\n        'numeric' => ':attribute musí být menší než :value.',\n        'file'    => 'Velikost souboru :attribute musí být menší než :value kB.',\n        'string'  => ':attribute musí obsahovat méně než :value znaků.',\n        'array'   => ':attribute by měl obsahovat méně než :value položek.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute musí být menší nebo rovno :value.',\n        'file'    => 'Velikost souboru :attribute musí být menší než :value kB.',\n        'string'  => ':attribute nesmí být delší než :value znaků.',\n        'array'   => ':attribute by měl obsahovat maximálně :value položek.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute nemůže být větší než :max.',\n        'file'    => 'Velikost souboru :attribute musí být menší než :value kB.',\n        'string'  => ':attribute nemůže být delší než :max znaků.',\n        'array'   => ':attribute nemůže obsahovat více než :max prvků.',\n    ],\n    'mimes'                => ':attribute musí být jeden z následujících datových typů :values.',\n    'min'                  => [\n        'numeric' => ':attribute musí být větší než :min.',\n        'file'    => ':attribute musí být větší než :min kB.',\n        'string'  => ':attribute musí být delší než :min znaků.',\n        'array'   => ':attribute musí obsahovat více než :min prvků.',\n    ],\n    'not_in'               => 'Zvolená hodnota pro :attribute je neplatná.',\n    'not_regex'            => ':attribute musí být regulární výraz.',\n    'numeric'              => ':attribute musí být číslo.',\n    'regex'                => ':attribute nemá správný formát.',\n    'required'             => ':attribute musí být vyplněno.',\n    'required_if'          => ':attribute musí být vyplněno pokud :other je :value.',\n    'required_with'        => ':attribute musí být vyplněno pokud :values je vyplněno.',\n    'required_with_all'    => ':attribute musí být vyplněno pokud :values je zvoleno.',\n    'required_without'     => ':attribute musí být vyplněno pokud :values není vyplněno.',\n    'required_without_all' => ':attribute musí být vyplněno pokud není žádné z :values zvoleno.',\n    'same'                 => ':attribute a :other se musí shodovat.',\n    'safe_url'             => 'Zadaný odkaz může být nebezpečný.',\n    'size'                 => [\n        'numeric' => ':attribute musí být přesně :size.',\n        'file'    => ':attribute musí mít přesně :size Kilobytů.',\n        'string'  => ':attribute musí být přesně :size znaků dlouhý.',\n        'array'   => ':attribute musí obsahovat právě :size prvků.',\n    ],\n    'string'               => ':attribute musí být řetězec znaků.',\n    'timezone'             => ':attribute musí být platná časová zóna.',\n    'totp'                 => 'Zadaný kód je neplatný nebo vypršel.',\n    'unique'               => ':attribute musí být unikátní.',\n    'url'                  => 'Formát :attribute je neplatný.',\n    'uploaded'             => 'Nahrávání :attribute se nezdařilo.',\n\n    'zip_file' => ':attribute musí odkazovat na soubor v archivu ZIP.',\n    'zip_file_size' => 'Soubor :attribute nesmí překročit :size MB.',\n    'zip_file_mime' => ':attribute musí odkazovat na soubor typu :validTypes, nalezen :foundType.',\n    'zip_model_expected' => 'Očekáván datový objekt, ale nalezen „:type“.',\n    'zip_unique' => ':attribute musí být jedinečný pro typ objektu v archivu ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Je nutné potvrdit heslo',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/cy/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'creodd dudalen',\n    'page_create_notification'    => 'Tudalen wedi\\'i chreu\\'n llwyddiannus',\n    'page_update'                 => 'diweddarodd dudalen',\n    'page_update_notification'    => 'Tudalen wedi\\'i diweddaru\\'n llwyddiannus',\n    'page_delete'                 => 'dileodd dudalen',\n    'page_delete_notification'    => 'Tudalen wedi\\'i dileu\\'n llwyddiannus',\n    'page_restore'                => 'adferodd dudalen',\n    'page_restore_notification'   => 'Tudalen wedi\\'i hadfer yn llwyddiannus',\n    'page_move'                   => 'symydodd dudalen',\n    'page_move_notification'      => 'Tudalen wedi\\'i symud yn llwyddianus',\n\n    // Chapters\n    'chapter_create'              => 'creodd bennod',\n    'chapter_create_notification' => 'Pennod wedi\\'i chreu\\'n llwyddiannus',\n    'chapter_update'              => 'pennod wedi diweddaru',\n    'chapter_update_notification' => 'Pennod wedi\\'i diweddaru\\'n llwyddiannus',\n    'chapter_delete'              => 'pennod wedi dileu',\n    'chapter_delete_notification' => 'Pennod wedi\\'i dileu\\'n llwyddiannus',\n    'chapter_move'                => 'pennod wedi symud',\n    'chapter_move_notification' => 'Pennod wedi\\'i symud yn llwyddianus',\n\n    // Books\n    'book_create'                 => 'llyfr wedi creu',\n    'book_create_notification'    => 'Llyfr wedi\\'i creu\\'n llwyddiannus',\n    'book_create_from_chapter'              => 'trosodd bennod i lyfr',\n    'book_create_from_chapter_notification' => 'Pennod wedi\\'i trosi\\'n llwyddiannus i lyfr',\n    'book_update'                 => 'llyfr wedi diweddaru',\n    'book_update_notification'    => 'Llyfr wedi\\'i diweddaru\\'n llwyddiannus',\n    'book_delete'                 => 'llyfr wedi\\'i dileu',\n    'book_delete_notification'    => 'Cafodd y llyfr ei dileu yn llwyddiannus',\n    'book_sort'                   => 'llyfr wedi\\'i ddidoli',\n    'book_sort_notification'      => 'Ail-archebwyd y llyfr yn llwyddiannus',\n\n    // Bookshelves\n    'bookshelf_create'            => 'creodd silff',\n    'bookshelf_create_notification'    => 'Silff wedi\\'i chreu\\'n llwyddiannus',\n    'bookshelf_create_from_book'    => 'trosodd lyfr i silff',\n    'bookshelf_create_from_book_notification'    => 'Llyfr wedi\\'i trosi\\'n llwyddiannus i silff',\n    'bookshelf_update'                 => 'diweddarodd silff',\n    'bookshelf_update_notification'    => 'Silff wedi\\'i diweddaru\\'n llwyddiannus',\n    'bookshelf_delete'                 => 'dileodd silff',\n    'bookshelf_delete_notification'    => 'Silff wedi\\'i dileu\\'n llwyddiannus',\n\n    // Revisions\n    'revision_restore' => 'adferodd y diwygiad',\n    'revision_delete' => 'dileuodd y diwygiad',\n    'revision_delete_notification' => 'Diwygiad wedi\\'i dileu\\'n llwyddiannus',\n\n    // Favourites\n    'favourite_add_notification' => 'Mae \":name\" wedi\\'i ychwanegu at eich ffefrynnau',\n    'favourite_remove_notification' => 'Mae \":name\" wedi\\'i tynnu o\\'ch ffefrynnau',\n\n    // Watching\n    'watch_update_level_notification' => 'Dewisiadau gwylio wedi’u diweddaru\\'n llwyddiannus',\n\n    // Auth\n    'auth_login' => 'wedi\\'u mewngofnodi',\n    'auth_register' => 'wedi\\'i cofrestru\\'n ddefnyddiwr newydd',\n    'auth_password_reset_request' => 'wedi ceisio ailosod gair pass defnyddiwr',\n    'auth_password_reset_update' => 'ailosododd air pass defnyddiwr',\n    'mfa_setup_method' => 'Dull Dilysu Aml-ffactor wedi’i ffurfweddu',\n    'mfa_setup_method_notification' => 'Dull aml-ffactor wedi\\'i ffurfweddu\\'n llwyddiannus',\n    'mfa_remove_method' => 'Dull Dilysu Aml-ffactor wedi\\'i ddileu',\n    'mfa_remove_method_notification' => 'Llwyddwyd i ddileu dull aml-ffactor',\n\n    // Settings\n    'settings_update' => 'diweddarodd osodiadau',\n    'settings_update_notification' => 'Gosodiadau wedi\\'i diweddaru\\'n llwyddiannus',\n    'maintenance_action_run' => 'rhedeg gweithred cynnal a chadw',\n\n    // Webhooks\n    'webhook_create' => 'webhook wedi creu',\n    'webhook_create_notification' => 'Webhook wedi\\'i creu\\'n llwyddiannus',\n    'webhook_update' => 'webhook wedi\\'i diweddaru',\n    'webhook_update_notification' => 'Webhook wedi\\'i diweddaru\\'n llwyddiannus',\n    'webhook_delete' => 'webhook wedi\\'i dileu',\n    'webhook_delete_notification' => 'Webhook wedi\\'i dileu\\'n llwyddiannus',\n\n    // Imports\n    'import_create' => 'creodd fewnforyn',\n    'import_create_notification' => 'Mewnforyn wedi\\'i lwytho i fyny\\'n llwyddiannus',\n    'import_run' => 'diweddarodd fewnforyn',\n    'import_run_notification' => 'Cynnwys wedi\\'i fewnforio\\'n llwyddiannus',\n    'import_delete' => 'dileodd fewnforyn',\n    'import_delete_notification' => 'Mewnforyn wedi\\'i ddileu\\'n llwyddiannus',\n\n    // Users\n    'user_create' => 'creodd ddefnyddiwr',\n    'user_create_notification' => 'Defnyddiwr wedi\\'i greu\\'n llwyddiannus',\n    'user_update' => 'diweddarodd ddefnyddiwr',\n    'user_update_notification' => 'Diweddarwyd y defnyddiwr yn llwyddiannus',\n    'user_delete' => 'dileodd ddefnyddiwr',\n    'user_delete_notification' => 'Tynnwyd y defnyddiwr yn llwyddiannus',\n\n    // API Tokens\n    'api_token_create' => 'creodd docyn API',\n    'api_token_create_notification' => 'Tocyn API wedi\\'i greu\\'n llwyddiannus',\n    'api_token_update' => 'diweddarodd docyn API',\n    'api_token_update_notification' => 'Tocyn API wedi\\'i ddiweddaru\\'n llwyddiannus',\n    'api_token_delete' => 'dileodd docyn API',\n    'api_token_delete_notification' => 'Tocyn API wedi\\'i ddileu\\'n llwyddiannus',\n\n    // Roles\n    'role_create' => 'creodd rôl',\n    'role_create_notification' => 'Rôl wedi\\'i creu\\'n llwyddiannus',\n    'role_update' => 'diweddarodd rôl',\n    'role_update_notification' => 'Rôl wedi\\'i diweddaru\\'n llwyddiannus',\n    'role_delete' => 'dileodd rôl',\n    'role_delete_notification' => 'Rôl wedi\\'i dileu\\'n llwyddiannus',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'gwagodd fin ailgylchu',\n    'recycle_bin_restore' => 'wedi\\'i adfer o\\'r bin ailgylchu',\n    'recycle_bin_destroy' => 'symudwyd o’r bin ailgylchu',\n\n    // Comments\n    'commented_on'                => 'gwnaeth sylwadau ar',\n    'comment_create'              => 'ychwanegodd sylw',\n    'comment_update'              => 'diweddarodd sylw',\n    'comment_delete'              => 'dileodd sylw',\n\n    // Sort Rules\n    'sort_rule_create' => 'created sort rule',\n    'sort_rule_create_notification' => 'Sort rule successfully created',\n    'sort_rule_update' => 'updated sort rule',\n    'sort_rule_update_notification' => 'Sort rule successfully updated',\n    'sort_rule_delete' => 'deleted sort rule',\n    'sort_rule_delete_notification' => 'Sort rule successfully deleted',\n\n    // Other\n    'permissions_update'          => 'caniatadau wedi\\'u diweddaru',\n];\n"
  },
  {
    "path": "lang/cy/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Nid yw\\'r manylion hyn yn cyfateb i\\'n cofnodion.',\n    'throttle' => 'Gormod o ymdrechion mewngofnodi. Rhowch gynnig arall arni o gwmpas :seconds eiliadau.',\n\n    // Login & Register\n    'sign_up' => 'Cofrestru',\n    'log_in' => 'Mewngofnodi',\n    'log_in_with' => 'Mewngofnodi efo :socialDriver',\n    'sign_up_with' => 'Cofrestru efo :socialDriver',\n    'logout' => 'Allgofnodi',\n\n    'name' => 'Enw',\n    'username' => 'Enw defnyddiwr',\n    'email' => 'Ebost',\n    'password' => 'Cyfrinair',\n    'password_confirm' => 'Cadarnhau cyfrinair',\n    'password_hint' => 'Rhaid bod o leiaf 8 nod',\n    'forgot_password' => 'Wedi anghofio cyfrinair?',\n    'remember_me' => 'Cofiwch fi',\n    'ldap_email_hint' => 'Rhowch e-bost i\\'w ddefnyddio ar gyfer y cyfrif hwn.',\n    'create_account' => 'Creu cyfrif',\n    'already_have_account' => 'Oes gennych chi gyfrif yn barod?',\n    'dont_have_account' => 'Dim cyfrif?',\n    'social_login' => 'Mewngofnodi cymdeithasol',\n    'social_registration' => 'Cofrestru cymdeithasol',\n    'social_registration_text' => 'Cofrestru a mewngofnodi gan ddefnyddio dyfais arall.',\n\n    'register_thanks' => 'Diolch am cofrestru!',\n    'register_confirm' => 'Gwiriwch eich e-bost a chliciwch ar y botwm cadarnhau i gael mynediad i: appName.',\n    'registrations_disabled' => 'Mae cofrestriadau wedi\\'u hanalluogi ar hyn o bryd',\n    'registration_email_domain_invalid' => 'Nid oes gan y parth e-bost hwnnw fynediad i\\'r rhaglen hon',\n    'register_success' => 'Diolch am arwyddo! Rydych bellach wedi cofrestru ac wedi mewngofnodi.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Wrthi\\'n ceisio mewngofnodi',\n    'auto_init_starting_desc' => 'Rydym yn cysylltu â\\'ch system ddilysu i ddechrau\\'r broses fewngofnodi. Os nad oes cynnydd ar ôl 5 eiliad, gallwch geisio clicio ar y ddolen isod.',\n    'auto_init_start_link' => 'Parhau gyda dilysu',\n\n    // Password Reset\n    'reset_password' => 'Ailosod cyfrinair',\n    'reset_password_send_instructions' => 'Rhowch eich e-bost isod ac anfonir e-bost atoch gyda dolen ailosod cyfrinair.',\n    'reset_password_send_button' => 'Anfon Dolen Ailosod',\n    'reset_password_sent' => 'Bydd dolen ailosod cyfrinair yn cael ei hanfon at :email os ceir hyd i’r cyfeiriad e-bost hwn yn y system.',\n    'reset_password_success' => 'Mae eich cyfrinair wedi\\'i ailosod yn llwyddiannus.',\n    'email_reset_subject' => 'Ailosod eich gair pass :appName',\n    'email_reset_text' => 'Anfonwyd yr e-bost hwn atoch oherwydd ein bod wedi cael cais ailosod cyfrinair ar gyfer eich cyfrif.',\n    'email_reset_not_requested' => 'Os nad oeddwch chi\\'n ceisio ailosod eich gair pass, does dim byd arall i wneud.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Cadarnhewch eich e-bost chi a :appName',\n    'email_confirm_greeting' => 'Diolch am ymuno â :appName!',\n    'email_confirm_text' => 'Os gwelwch yn dda cadarnhewch eich e-bost chi gan clicio ar y botwm isod:',\n    'email_confirm_action' => 'Cadarnhau E-bost',\n    'email_confirm_send_error' => 'Mae angen cadarnhad e-bost ond ni allai\\'r system anfon yr e-bost. Cysylltwch â\\'r gweinyddwr i sicrhau bod yr e-bost wedi\\'i osod yn gywir.',\n    'email_confirm_success' => 'Mae eich e-bost wedi\\'i gadarnhau! Dylech nawr allu mewngofnodi gan ddefnyddio\\'r cyfeiriad e-bost hwn.',\n    'email_confirm_resent' => 'Ail-anfonwyd cadarnhad e-bost, gwiriwch eich mewnflwch.',\n    'email_confirm_thanks' => 'Diolch am gadarnhau!',\n    'email_confirm_thanks_desc' => 'Arhoswch eiliad wrth i’ch cadarnhad gael ei drin. Os na chewch eich ailgyfeirio ar ôl 3 eiliad, pwyswch y ddolen \"Parhau\" isod i symud ymlaen.',\n\n    'email_not_confirmed' => 'Cyfeiriad E-bost heb ei Gadarnhau',\n    'email_not_confirmed_text' => 'Dyw eich cyfeiriad e-bost chi ddim wedi cael ei gadarnhau eto.',\n    'email_not_confirmed_click_link' => 'Cliciwch ar y ddolen yn yr e-bost a anfonwyd yn fuan ar ôl i chi gofrestru.',\n    'email_not_confirmed_resend' => 'Os na allwch ddod o hyd i\\'r e-bost, gallwch ail-anfon yr e-bost cadarnhad trwy gyflwyno\\'r ffurflen isod.',\n    'email_not_confirmed_resend_button' => 'Ail-anfon E-bost Cadarnhad',\n\n    // User Invite\n    'user_invite_email_subject' => 'Rydych chi wedi cael gwahoddiad i ymuno :appName!',\n    'user_invite_email_greeting' => 'Mae cyfrif wedi cae ei greu i chi ar :appName.',\n    'user_invite_email_text' => 'Cliciwch ar y botwm isod i osod cyfrinair cyfrif a chael mynediad:',\n    'user_invite_email_action' => 'Gosod Cyfrinair Cyfrif',\n    'user_invite_page_welcome' => 'Croeso i :appName!',\n    'user_invite_page_text' => 'I gwblhau eich cyfrif a chael mynediad mae angen i chi osod cyfrinair a fydd yn cael ei ddefnyddio i fewngofnodi i :appName ar ymweliadau yn y dyfodol.',\n    'user_invite_page_confirm_button' => 'Cadarnhau cyfrinair',\n    'user_invite_success_login' => 'Cyfrinair wedi’i osod, dylech nawr allu mewngofnodi gan ddefnyddio\\'r cyfrinair a osodwyd i gael mynediad i :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Gosod Dilysu Aml-Ffactor',\n    'mfa_setup_desc' => 'Gosod dilysu aml-ffactor fel haen ychwanegol o ddiogelwch ar gyfer eich cyfrif defnyddiwr.',\n    'mfa_setup_configured' => 'Wedi\\'i ail-ffurfweddu\\'n barod',\n    'mfa_setup_reconfigure' => 'Ail-ffurfweddu',\n    'mfa_setup_remove_confirmation' => 'Ydych chi\\'n siŵr eich bod am gael gwared ar y dull dilysu aml-ffactor hwn?',\n    'mfa_setup_action' => 'Gosodiad',\n    'mfa_backup_codes_usage_limit_warning' => 'Mae gennych lai na 5 cod wrth gefn yn weddill, crëwch a storio cyfres newydd cyn i chi redeg allan o godau i osgoi cael eich cloi allan o\\'ch cyfrif.',\n    'mfa_option_totp_title' => 'Ap Ffôn Symudol',\n    'mfa_option_totp_desc' => 'I ddefnyddio dilysu aml-ffactor bydd angen dyfais symudol arnoch sy\\'n cefnogi TOTP megis Google Authenticator, Authy neu Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Codau wrth Gefn',\n    'mfa_option_backup_codes_desc' => 'Mae’n cynhyrchu cyfres o godau wrth gefn un-amser y byddwch chi\\'n eu defnyddio i fewngofnodi i wirio pwy ydych chi. Gwnewch yn siŵr eich bod yn storio\\'r rhain mewn lle saff a diogel.',\n    'mfa_gen_confirm_and_enable' => 'Cadarnhau a Galluogi',\n    'mfa_gen_backup_codes_title' => 'Gosodiad Codau wrth Gefn',\n    'mfa_gen_backup_codes_desc' => 'Storiwch y rhestr isod o godau mewn lle diogel. Wrth ddefnyddio’r system bydd modd i chi ddefnyddio un o\\'r codau fel ail fecanwaith dilysu.',\n    'mfa_gen_backup_codes_download' => 'Llwytho Codau i Lawr',\n    'mfa_gen_backup_codes_usage_warning' => 'Gellir defnyddio pob cod unwaith yn unig',\n    'mfa_gen_totp_title' => 'Gosod Ap Symudol',\n    'mfa_gen_totp_desc' => 'I ddefnyddio dilysu aml-ffactor bydd angen dyfais symudol arnoch sy\\'n cefnogi TOTP megis Google Authenticator, Authy neu Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Sganiwch y cod QR isod gan ddefnyddio\\'ch ap dilysu dewisol i ddechrau.',\n    'mfa_gen_totp_verify_setup' => 'Gwirio Gosodiad',\n    'mfa_gen_totp_verify_setup_desc' => 'Gwiriwch fod popeth yn gweithio trwy roi cod, a gynhyrchwyd gan eich ap dilysu, yn y blwch mewnbwn isod:',\n    'mfa_gen_totp_provide_code_here' => 'Rhowch y cod a gynhyrchwyd gan eich ap yma',\n    'mfa_verify_access' => 'Gwirio Mynediad',\n    'mfa_verify_access_desc' => 'Mae eich cyfrif defnyddiwr yn gofyn i chi gadarnhau pwy ydych chi trwy lefel ychwanegol o ddilysu cyn i chi gael mynediad. Gwiriwch gan ddefnyddio un o\\'ch dulliau ffurfweddu i barhau.',\n    'mfa_verify_no_methods' => 'Dim Dulliau wedi\\'u Ffurfweddu',\n    'mfa_verify_no_methods_desc' => 'Ni ellid dod o hyd i unrhyw ddulliau dilysu aml-ffactor ar gyfer eich cyfrif. Bydd angen i chi osod o leiaf un dull cyn i chi gael mynediad.',\n    'mfa_verify_use_totp' => 'Gwirio gan ap ffôn',\n    'mfa_verify_use_backup_codes' => 'Gwirio gan god wrth gefn',\n    'mfa_verify_backup_code' => 'Cod wrth Gefn',\n    'mfa_verify_backup_code_desc' => 'Rhowch un o\\'ch codau wrth gefn sy\\'n weddill isod:',\n    'mfa_verify_backup_code_enter_here' => 'Cofnodi cod wrth gefn yma',\n    'mfa_verify_totp_desc' => 'Rhowch y cod, a gynhyrchwyd gan ddefnyddio\\'ch ap symudol, isod:',\n    'mfa_setup_login_notification' => 'Dull aml-ffactor wedi\\'i ffurfweddu, nawr mewngofnodwch eto gan ddefnyddio\\'r dull wedi\\'i ffurfweddu.',\n];\n"
  },
  {
    "path": "lang/cy/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Canslo',\n    'close' => 'Cau',\n    'confirm' => 'Cadarnhau',\n    'back' => 'Yn ôl',\n    'save' => 'Cadw',\n    'continue' => 'Parhau',\n    'select' => 'Dewis',\n    'toggle_all' => 'Toglo Popeth',\n    'more' => 'Mwy',\n\n    // Form Labels\n    'name' => 'Enw',\n    'description' => 'Disgrifiad',\n    'role' => 'Rôl',\n    'cover_image' => 'Delwedd y clawr',\n    'cover_image_description' => 'Dylai\\'r ddelwedd hon fod oddeutu 440x250px er y bydd wedi\\'i graddio a’i thocio i ffitio rhyngwyneb y defnyddiwr mewn gwahanol senarios yn ôl yr angen, felly bydd y dimensiynau arddangos gwirioneddol yn amrywio.',\n\n    // Actions\n    'actions' => 'Gweithredoedd',\n    'view' => 'Gweld',\n    'view_all' => 'Gweld popeth',\n    'new' => 'Newydd',\n    'create' => 'Creu',\n    'update' => 'Diweddaru',\n    'edit' => 'Golygu',\n    'archive' => 'Archive',\n    'unarchive' => 'Un-Archive',\n    'sort' => 'Trefnu',\n    'move' => 'Symud',\n    'copy' => 'Copïo',\n    'reply' => 'Ateb',\n    'delete' => 'Dileu',\n    'delete_confirm' => 'Cadarnhau\\'r dilead',\n    'search' => 'Chwilio',\n    'search_clear' => 'Clirio\\'r chwiliad',\n    'reset' => 'Ailosod',\n    'remove' => 'Diddymu',\n    'add' => 'Ychwanegu',\n    'configure' => 'Ffurfweddu',\n    'manage' => 'Rheoli',\n    'fullscreen' => 'Sgrin Llawn',\n    'favourite' => 'Gwneud Ffefryn',\n    'unfavourite' => 'Dadwneud Ffefryn',\n    'next' => 'Nesa',\n    'previous' => 'Cynt',\n    'filter_active' => 'Hidl weithredol:',\n    'filter_clear' => 'Clirio\\'r hidl',\n    'download' => 'Llwytho i lawr',\n    'open_in_tab' => 'Agor mewn Tab',\n    'open' => 'Agor',\n\n    // Sort Options\n    'sort_options' => 'Trefnu\\'r opsiynau',\n    'sort_direction_toggle' => 'Trefnu Cyfeiriad Togl',\n    'sort_ascending' => 'Trefnu\\'n esgynnol',\n    'sort_descending' => 'Trefnu\\'n ddisgynnol',\n    'sort_name' => 'Enw',\n    'sort_default' => 'Diofyn',\n    'sort_created_at' => 'Dyddiad Creu',\n    'sort_updated_at' => 'Dyddiad Diweddaru',\n\n    // Misc\n    'deleted_user' => 'Defnyddiwr wedi\\'i Dileu',\n    'no_activity' => 'Dim actifedd i arddangos',\n    'no_items' => 'Dim eitemau ar gael',\n    'back_to_top' => 'Yn ôl i\\'r brig',\n    'skip_to_main_content' => 'Neidio i\\'r prif gynnwys',\n    'toggle_details' => 'Toglo Manylion',\n    'toggle_thumbnails' => 'Toglo Mân-Luniau',\n    'details' => 'Manylion',\n    'grid_view' => 'Golwg Grid',\n    'list_view' => 'Golwg Rhestr',\n    'default' => 'Diofyn',\n    'breadcrumb' => 'Briwsion bara',\n    'status' => 'Statws',\n    'status_active' => 'Gweithredol',\n    'status_inactive' => 'Anweithredol',\n    'never' => 'Byth',\n    'none' => 'Dim un',\n\n    // Header\n    'homepage' => 'Tudalen cartref',\n    'header_menu_expand' => 'Ehangu Dewislen Pennawd',\n    'profile_menu' => 'Dewislen Proffil',\n    'view_profile' => 'Gweld proffil',\n    'edit_profile' => 'Golygu Proffil',\n    'dark_mode' => 'Modd Tywyll',\n    'light_mode' => 'Modd Golau',\n    'global_search' => 'Chwilio Byd-eang',\n\n    // Layout tabs\n    'tab_info' => 'Gwybodaeth',\n    'tab_info_label' => 'Tab: Dangos Gwybodaeth Eilaidd',\n    'tab_content' => 'Cynnwys',\n    'tab_content_label' => 'Tab: Dangos Prif Gynnwys',\n\n    // Email Content\n    'email_action_help' => 'Os ydych chi\\'n cael trafferth clicio ar y botwm \":actionText\", copïwch a gludwch yr URL isod i\\'ch porwr gwe:',\n    'email_rights' => 'Cedwir pob hawl',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Polisi Preifatrwydd',\n    'terms_of_service' => 'Telerau Gwasanaeth',\n\n    // OpenSearch\n    'opensearch_description' => 'Chwilio :appName',\n];\n"
  },
  {
    "path": "lang/cy/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Dewis Llun',\n    'image_list' => 'Rhestr o Ddelweddau',\n    'image_details' => 'Manylion Delwedd',\n    'image_upload' => 'Uwchlwytho Llun',\n    'image_intro' => 'Yma gallwch ddewis a rheoli lluniau sydd wedi\\'u huwchlwytho i’r system o’r blaen.',\n    'image_intro_upload' => 'Uwchlwythwch lun newydd drwy lusgo ffeil llun i\\'r ffenestr hon, neu drwy ddefnyddio\\'r botwm \"Uwchlwytho Llun\" uchod.',\n    'image_all' => 'Popeth',\n    'image_all_title' => 'Gweld holl ddelweddau',\n    'image_book_title' => 'Gweld lluniau a uwchlwythwyd i’r llyfr hwn',\n    'image_page_title' => 'Gweld lluniau a uwchlwythwyd i’r dudalen hon',\n    'image_search_hint' => 'Cwilio gan enw delwedd',\n    'image_uploaded' => 'Uwchlwythwyd am :uploadedDate',\n    'image_uploaded_by' => 'Uwchlwythwyd gan :userName',\n    'image_uploaded_to' => 'Uwchlwythwyd i :pageLink',\n    'image_updated' => 'Diweddarwyd am :updateDate',\n    'image_load_more' => 'Llwytho Mwy',\n    'image_image_name' => 'Enw Delwedd',\n    'image_delete_used' => 'Mae\\'r llun hwn yn cael ei ddefnyddio ar y tudalennau isod.',\n    'image_delete_confirm_text' => 'Wyt ti\\'n bendant eisiau dileu\\'r ddelwedd hwn?',\n    'image_select_image' => 'Dewis Llun',\n    'image_dropzone' => 'Gollyngwch luniau neu cliciwch yma i uwchlwytho',\n    'image_dropzone_drop' => 'Gollyngwch luniau yma i uwchlwytho',\n    'images_deleted' => 'Delweddau wedi\\'u Dileu',\n    'image_preview' => 'Rhagolwg o’r Llun',\n    'image_upload_success' => 'Uwchlwythwyd y llun yn llwyddiannus',\n    'image_update_success' => 'Diweddarwyd manylion y llun yn llwyddiannus',\n    'image_delete_success' => 'Dilëwyd y llun yn llwyddiannus',\n    'image_replace' => 'Newid y llun',\n    'image_replace_success' => 'Ffeil llun wedi\\'i diweddaru’n llwyddiannus',\n    'image_rebuild_thumbs' => 'Atgynhyrchu Amrywiadau Maint',\n    'image_rebuild_thumbs_success' => 'Ailadeiladwyd amrywiadau maint y llun yn llwyddiannus!',\n\n    // Code Editor\n    'code_editor' => 'Golygu Cod',\n    'code_language' => 'Iaith y Cod',\n    'code_content' => 'Cynnwys y Cod',\n    'code_session_history' => 'Hanes y Sesiwn',\n    'code_save' => 'Cadw Cod',\n];\n"
  },
  {
    "path": "lang/cy/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Cyffredin',\n    'advanced' => 'Datblygedig',\n    'none' => 'Dim un',\n    'cancel' => 'Canslo',\n    'save' => 'Cadw',\n    'close' => 'Cau',\n    'apply' => 'Apply',\n    'undo' => 'Dadwneud',\n    'redo' => 'Ail-wneud',\n    'left' => 'Chwith',\n    'center' => 'Canol',\n    'right' => 'Dde',\n    'top' => 'Pen',\n    'middle' => 'Canol',\n    'bottom' => 'Gwaelod',\n    'width' => 'Lled',\n    'height' => 'Uchder',\n    'More' => 'Mwy',\n    'select' => 'Dewis...',\n\n    // Toolbar\n    'formats' => 'Fformatau',\n    'header_large' => 'Pennyn Mawr',\n    'header_medium' => 'Pennyn Canolig',\n    'header_small' => 'Pennyn Bach',\n    'header_tiny' => 'Pennyn Mân',\n    'paragraph' => 'Paragraff',\n    'blockquote' => 'Dyfyniad bloc',\n    'inline_code' => 'Cod mewnol',\n    'callouts' => 'Galwadau',\n    'callout_information' => 'Gwybodaeth',\n    'callout_success' => 'Llwyddiant',\n    'callout_warning' => 'Rhybudd',\n    'callout_danger' => 'Perygl',\n    'bold' => 'Trwm',\n    'italic' => 'Italig',\n    'underline' => 'Tanlinellu',\n    'strikethrough' => 'Llinell Drwodd',\n    'superscript' => 'Uwchysgrif',\n    'subscript' => 'Isysgrif',\n    'text_color' => 'Lliw testun',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Lliw addasu',\n    'remove_color' => 'Dileu lliw',\n    'background_color' => 'Lliw cefnder',\n    'align_left' => 'Alinio i\\'r chwith',\n    'align_center' => 'Alinio i’r canol',\n    'align_right' => 'Alinio i\\'r dde',\n    'align_justify' => 'Unioni',\n    'list_bullet' => 'Rhestr o Bwyntiau Bwled',\n    'list_numbered' => 'Rhestr wedi\\'i rhifo',\n    'list_task' => 'Rhestr dasgau',\n    'indent_increase' => 'Cynyddu mewnoliad',\n    'indent_decrease' => 'Lleihau mewnoliad',\n    'table' => 'Bwrdd',\n    'insert_image' => 'Mewnosod llun',\n    'insert_image_title' => 'Mewnosod/Golygu Llun',\n    'insert_link' => 'Mewnosod/golygu dolen',\n    'insert_link_title' => 'Mewnosod/Golygu Dolen',\n    'insert_horizontal_line' => 'Mewnosod llinell lorweddol',\n    'insert_code_block' => 'Mewnosod bloc cod',\n    'edit_code_block' => 'Golygu bloc cod',\n    'insert_drawing' => 'Mewnosod/golygu dyluniad',\n    'drawing_manager' => 'Rheolwr dylunio',\n    'insert_media' => 'Mewnosod/golygu cyfryngau',\n    'insert_media_title' => 'Mewnosod/Golygu Cyfryngau',\n    'clear_formatting' => 'Clirio fformatio',\n    'source_code' => 'Cod ffynhonnell',\n    'source_code_title' => 'Cod Ffynhonnell',\n    'fullscreen' => 'Sgrin lawn',\n    'image_options' => 'Opsiynau llun',\n\n    // Tables\n    'table_properties' => 'Priodweddau tabl',\n    'table_properties_title' => 'Priodweddau Tabl',\n    'delete_table' => 'Dileu tabl',\n    'table_clear_formatting' => 'Clirio fformatio tabl',\n    'resize_to_contents' => 'Newid maint i\\'r cynnwys',\n    'row_header' => 'Pennyn rhes',\n    'insert_row_before' => 'Mewnosod rhes cyn',\n    'insert_row_after' => 'Mewnosod rhes ar ôl',\n    'delete_row' => 'Dileu rhes',\n    'insert_column_before' => 'Mewnosod colofn cyn',\n    'insert_column_after' => 'Mewnosod colofn ar ôl',\n    'delete_column' => 'Dileu colofn',\n    'table_cell' => 'Cell',\n    'table_row' => 'Rhes',\n    'table_column' => 'Colofn',\n    'cell_properties' => 'Priodweddau cell',\n    'cell_properties_title' => 'Priodweddau Cell',\n    'cell_type' => 'Math o gell',\n    'cell_type_cell' => 'Cell',\n    'cell_scope' => 'Cwmpas',\n    'cell_type_header' => 'Pennyn cell',\n    'merge_cells' => 'Uno celloedd',\n    'split_cell' => 'Hollti cell',\n    'table_row_group' => 'Grŵp Rhes',\n    'table_column_group' => 'Grŵp Colofn',\n    'horizontal_align' => 'Alinio llorweddol',\n    'vertical_align' => 'Alinio fertigol',\n    'border_width' => 'Lled y border',\n    'border_style' => 'Arddull y border',\n    'border_color' => 'Lliw y border',\n    'row_properties' => 'Priodweddau’r rhes',\n    'row_properties_title' => 'Priodweddau’r Rhes',\n    'cut_row' => 'Torri rhes',\n    'copy_row' => 'Copïo rhes',\n    'paste_row_before' => 'Gludo rhes cyn',\n    'paste_row_after' => 'Gludo rhes ar ôl',\n    'row_type' => 'Math o res',\n    'row_type_header' => 'Pennyn',\n    'row_type_body' => 'Corff',\n    'row_type_footer' => 'Troedyn',\n    'alignment' => 'Aliniad',\n    'cut_column' => 'Torri colofn',\n    'copy_column' => 'Copïo colofn',\n    'paste_column_before' => 'Gludo colofn cyn',\n    'paste_column_after' => 'Gludo colofn ar ôl',\n    'cell_padding' => 'Padio cell',\n    'cell_spacing' => 'Bylchau rhwng celloedd',\n    'caption' => 'Pennawd',\n    'show_caption' => 'Dangos pennawd',\n    'constrain' => 'Cyfyngu cyfrannau',\n    'cell_border_solid' => 'Solid',\n    'cell_border_dotted' => 'Dotiog',\n    'cell_border_dashed' => 'Llinell Doredig',\n    'cell_border_double' => 'Dwbl',\n    'cell_border_groove' => 'Rhigol',\n    'cell_border_ridge' => 'Crib',\n    'cell_border_inset' => 'Mewnosodiad',\n    'cell_border_outset' => 'Cychwyniad',\n    'cell_border_none' => 'Dim un',\n    'cell_border_hidden' => 'Cuddiedig',\n\n    // Images, links, details/summary & embed\n    'source' => 'Ffynhonnell',\n    'alt_desc' => 'Disgrifiad amgen',\n    'embed' => 'Ymgorffori',\n    'paste_embed' => 'Gludwch eich cod ymgorffori isod:',\n    'url' => 'URL',\n    'text_to_display' => 'Testun i\\'w arddangos',\n    'title' => 'Teitl',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Agor dolen',\n    'open_link_in' => 'Agor dolen yn...',\n    'open_link_current' => 'Ffenestr bresennol',\n    'open_link_new' => 'Ffenestr newydd',\n    'remove_link' => 'Dileu dolen',\n    'insert_collapsible' => 'Mewnosod bloc cwympadwy',\n    'collapsible_unwrap' => 'Datod',\n    'edit_label' => 'Golygu label',\n    'toggle_open_closed' => 'Agor/cau Togl',\n    'collapsible_edit' => 'Golygu bloc cwympadwy',\n    'toggle_label' => 'Toglo label',\n\n    // About view\n    'about' => 'Ynglŷn â\\'r golygydd',\n    'about_title' => 'Ynglŷn â\\'r Golygydd WYSIWYG',\n    'editor_license' => 'Trwydded a Hawlfraint Golygydd',\n    'editor_lexical_license' => 'Mae\\'r golygydd hwn wedi\\'i adeiladu fel fforc o :lexicalLink sy\\'n cael ei ddosbarthu o dan y drwydded MIT.',\n    'editor_lexical_license_link' => 'Gellir gweld manylion llawn y drwydded yma.',\n    'editor_tiny_license' => 'Mae\\'r golygydd hwn wedi\\'i adeiladu gan ddefnyddio :tinyLink sy\\'n cael ei ddarparu o dan y drwydded MIT.',\n    'editor_tiny_license_link' => 'Gellir dod o hyd i fanylion hawlfraint a thrwydded TinyMCE yma.',\n    'save_continue' => 'Cadw Tudalen a Pharhau',\n    'callouts_cycle' => '(Pwyswch i doglo drwy’r mathau)',\n    'link_selector' => 'Dolen i\\'r cynnwys',\n    'shortcuts' => 'Llwybrau Byr',\n    'shortcut' => 'Llwybr Byr',\n    'shortcuts_intro' => 'Mae\\'r llwybrau byr canlynol ar gael yn y golygydd:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Disgrifiad',\n];\n"
  },
  {
    "path": "lang/cy/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Crëwyd yn Ddiweddar',\n    'recently_created_pages' => 'Tudalennau a Grëwyd yn Ddiweddar',\n    'recently_updated_pages' => 'Tudalennau a Ddiweddarwyd yn Ddiweddar',\n    'recently_created_chapters' => 'Penodau a Grëwyd yn Ddiweddar',\n    'recently_created_books' => 'Llyfrau a Grëwyd yn Ddiweddar',\n    'recently_created_shelves' => 'Silffoedd a Grëwyd yn Ddiweddar',\n    'recently_update' => 'Diweddarwyd yn Ddiweddar',\n    'recently_viewed' => 'Gwelwyd yn Ddiweddar',\n    'recent_activity' => 'Gweithgaredd Diweddar',\n    'create_now' => 'Creu un nawr',\n    'revisions' => 'Diwygiadau',\n    'meta_revision' => 'Diwygiad #:revisionCount',\n    'meta_created' => 'Crëwyd',\n    'meta_created_name' => 'Crëwyd :timeLength gan :user',\n    'meta_updated' => 'Diweddarwyd :timeLength',\n    'meta_updated_name' => 'Diweddarwyd :timeLength gan :user',\n    'meta_owned_name' => 'Mae\\'n eiddo i :user',\n    'meta_reference_count' => 'Cyfeirir ato gan :count eitem|Cyfeirir ato gan :count o eitemau',\n    'entity_select' => 'Dewis Endid',\n    'entity_select_lack_permission' => 'Nid oes gennych y caniatâd angenrheidiol i ddewis yr eitem hon',\n    'images' => 'Delweddau',\n    'my_recent_drafts' => 'Fy Nrafftiau Diweddar',\n    'my_recently_viewed' => 'Edrych yn Ddiweddar',\n    'my_most_viewed_favourites' => 'Fy Ffefrynnau Mwyaf Poblogaidd',\n    'my_favourites' => 'Fy Ffefrynnau',\n    'no_pages_viewed' => 'Nid ydych wedi edrych ar unrhyw dudalennau',\n    'no_pages_recently_created' => 'Nid oes unrhyw dudalennau wedi\\'u creu\\'n ddiweddar',\n    'no_pages_recently_updated' => 'Nid oes unrhyw dudalennau wedi\\'u diweddaru\\'n ddiweddar',\n    'export' => 'Allforio',\n    'export_html' => 'Ffeil Gwe wedi\\'i Chynnwys',\n    'export_pdf' => 'Ffeil PDF',\n    'export_text' => 'Ffeil Testun Plaen',\n    'export_md' => 'Ffeil Markdown',\n    'export_zip' => 'ZIP cludadwy',\n    'default_template' => 'Templed Tudalen Diofyn',\n    'default_template_explain' => 'Clustnodwch dempled tudalen a fydd yn cael ei ddefnyddio fel y cynnwys diofyn ar gyfer pob tudalen a grëwyd yn yr eitem hon. Cofiwch y bydd hwn ond yn cael ei ddefnyddio os yw’r sawl a grëodd y dudalen â mynediad gweld i’r dudalen dempled a ddewiswyd.',\n    'default_template_select' => 'Dewiswch dudalen templed',\n    'import' => 'Mewnforio',\n    'import_validate' => 'Dilysu\\'r Mewnforyn',\n    'import_desc' => 'Mewnforio llyfrau, pennodau & tudalennau gan allforyn zip cludadwy o\\'r un enghraifft, neu enghraifft wahanol. Dewis ffeil ZIP i barhau. Ar ôl i\\'r ffeil wedi cael ei llwytho i fyny a dilysu, gallwch chi ffurfweddu & cadarnhau\\'r mewnforyn yn yr olygfa nesa.',\n    'import_zip_select' => 'Dewis ffeil ZIP i lwytho i fyny',\n    'import_zip_validation_errors' => 'Wedi canfod gwallau pan ddilysu\\'r ffeil ZIP wedi\\'i roi:',\n    'import_pending' => 'Mewnforion i Ddod',\n    'import_pending_none' => 'Nid wedi dechrau unrhyw fewnforion.',\n    'import_continue' => 'Parhau Mewnforyn',\n    'import_continue_desc' => 'Adolygu\\'r cynnwys sy\\'n ddod i gael ei fewnforio o\\'r ffeil ZIP wedi\\'i lwytho i fyny. Pan barod, Rhedeg y mewnforyn i ychwanegu ei gynnwys â\\'r system yma. Bydd y ffeil ZIP yn cael ei symud yn awtomatig ar ôl mewnforyn llwyddiannus.',\n    'import_details' => 'Manylion Mewnforyn',\n    'import_run' => 'Rhedeg y Mewnforyn',\n    'import_size' => ':size Mewnforio Maint ZIP',\n    'import_uploaded_at' => 'Wedi\\'i lwytho i fyny :relativeTime',\n    'import_uploaded_by' => 'Llwythwyd gan',\n    'import_location' => 'Lleoliad Mewnforyn',\n    'import_location_desc' => 'Dewis lleoliad targedi\\'r cynnwys wedi\\'i mewnforio. Bydd angen yr hawliau perthnasol i greu yn y lleoliad yno.',\n    'import_delete_confirm' => 'Ydych chi\\'n siŵr eich bod eisiau dileu\\'r mewnforyn yma?',\n    'import_delete_desc' => 'Bydd hwn yn dileu\\'r mewnforyn ffeil ZIP sy wedi\\'i lwytho i fyny, a fydd e ddim gallu cael ei ddadwneud.',\n    'import_errors' => 'Gwallau Mewnforyn',\n    'import_errors_desc' => 'Digwyddodd y gwallau canlynol yn ystod cynnig y mewnforyn:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Caniatâd',\n    'permissions_desc' => 'Gosodwch ganiatâd yma i ddiystyru\\'r caniatâd diofyn a ddarperir gan rolau defnyddwyr.',\n    'permissions_book_cascade' => 'Bydd caniatâd a osodir ar lyfrau yn rhaeadru’n awtomatig i benodau a thudalennau plant, oni bai bod ganddynt eu caniatâd diffiniedig eu hunain.',\n    'permissions_chapter_cascade' => 'Bydd caniatâd a osodir ar benodau yn rhaeadru’n awtomatig i dudalennau plant, oni bai bod ganddynt eu caniatâd diffiniedig eu hunain.',\n    'permissions_save' => 'Cadw Caniatâd',\n    'permissions_owner' => 'Perchennog',\n    'permissions_role_everyone_else' => 'Pawb arall',\n    'permissions_role_everyone_else_desc' => 'Gosod caniatâd ar gyfer pob rôl nad ydynt yn cael eu diystyru\\'n benodol.',\n    'permissions_role_override' => 'Diystyru caniatâd ar gyfer rôl',\n    'permissions_inherit_defaults' => 'Etifeddu rhagosodiadau',\n\n    // Search\n    'search_results' => 'Canlyniadau Chwilio',\n    'search_total_results_found' => 'Cafwyd :count canlyniad|Cafwyd cyfanswm o :count canlyniad',\n    'search_clear' => 'Clirio\\'r Chwiliad',\n    'search_no_pages' => 'Nid oedd unrhyw dudalennau yn cyfateb â\\'r chwiliad hwn',\n    'search_for_term' => 'Chwilio am :term',\n    'search_more' => 'Mwy o Ganlyniadau',\n    'search_advanced' => 'Math o Gynnwys',\n    'search_terms' => 'Termau Chwilio',\n    'search_content_type' => 'Math o Gynnwys',\n    'search_exact_matches' => 'Union Gyfatebiaethau',\n    'search_tags' => 'Tagio Chwiliadau',\n    'search_options' => 'Opsiynau',\n    'search_viewed_by_me' => 'Gwelwyd gennyf fi',\n    'search_not_viewed_by_me' => 'Nas gwelwyd gennyf fi',\n    'search_permissions_set' => 'Gosod Caniatâd',\n    'search_created_by_me' => 'Crëwyd gennyf fi',\n    'search_updated_by_me' => 'Diweddarwyd gennyf fi',\n    'search_owned_by_me' => 'Yn eiddo i mi',\n    'search_date_options' => 'Opsiynau Dyddiad',\n    'search_updated_before' => 'Diweddarwyd cyn',\n    'search_updated_after' => 'Diweddarwyd ar ôl',\n    'search_created_before' => 'Crëwyd cyn',\n    'search_created_after' => 'Crëwyd ar ôl',\n    'search_set_date' => 'Gosod Dyddiad',\n    'search_update' => 'Diweddaru Chwiliad',\n\n    // Shelves\n    'shelf' => 'Silff',\n    'shelves' => 'Silffau',\n    'x_shelves' => ':count Silff|:count Shelves',\n    'shelves_empty' => 'Ni chrëwyd unrhyw silffoedd',\n    'shelves_create' => 'Creu Silff Newydd',\n    'shelves_popular' => 'Silffoedd Poblogaidd',\n    'shelves_new' => 'Silffau Newydd',\n    'shelves_new_action' => 'Silff Newydd',\n    'shelves_popular_empty' => 'Bydd y silffoedd mwyaf poblogaidd yn ymddangos yma.',\n    'shelves_new_empty' => 'Bydd y silffoedd a grëwyd fwyaf diweddar yn ymddangos yma.',\n    'shelves_save' => 'Cadw Silff',\n    'shelves_books' => 'Llyfrau ar y silff hon',\n    'shelves_add_books' => 'Ychwanegu llyfrau i\\'r silff hon',\n    'shelves_drag_books' => 'Llusgwch lyfrau isod i\\'w hychwanegu at y silff hon',\n    'shelves_empty_contents' => 'Nid oes gan y silff hon unrhyw lyfrau wedi’u clustnodi iddi',\n    'shelves_edit_and_assign' => 'Golygu silff i glustnodi llyfrau',\n    'shelves_edit_named' => 'Golygu Silff :name',\n    'shelves_edit' => 'Golygu Silff',\n    'shelves_delete' => 'Dileu Silff',\n    'shelves_delete_named' => 'Dileu Silff :name',\n    'shelves_delete_explain' => \"Bydd hyn yn dileu'r silff gyda'r enw ':name'. Ni fydd llyfrau wedi'u cynnwys yn cael eu dileu.\",\n    'shelves_delete_confirmation' => 'Ydych chi\\'n siŵr eich bod chi eisiau dileu\\'r silff hon?',\n    'shelves_permissions' => 'Caniatâd Silffoedd',\n    'shelves_permissions_updated' => 'Diweddarwyd Caniatâd Silffoedd',\n    'shelves_permissions_active' => 'Caniatâd Silffoedd yn Weithredol',\n    'shelves_permissions_cascade_warning' => 'Nid yw caniatâd ar silffoedd yn rhaeadru’n awtomatig i lyfrau sydd wedi\\'u cynnwys. Mae hyn oherwydd y gall llyfr fodoli ar silffoedd lluosog. Fodd bynnag, gellir copïo caniatâd i lawr i lyfrau plant gan ddefnyddio\\'r opsiwn a geir isod.',\n    'shelves_permissions_create' => 'Dim ond ar gyfer copïo caniatâd i lyfrau plant y defnyddir caniatâd creu silff gan ddefnyddio\\'r camau isod. Nid ydynt yn rheoli\\'r gallu i greu llyfrau.',\n    'shelves_copy_permissions_to_books' => 'Copïo Caniatâd i Lyfrau',\n    'shelves_copy_permissions' => 'Copïo Caniatâd',\n    'shelves_copy_permissions_explain' => 'Bydd hyn yn cymhwyso gosodiadau caniatâd presennol y silff hon i bob llyfr sydd wedi\\'u cynnwys ynddi. Cyn ysgogi, gwnewch yn siŵr bod unrhyw newidiadau i ganiatâd y silff hon wedi\\'u cadw.',\n    'shelves_copy_permission_success' => 'Caniatâd silff wedi\\'i gopïo i :count o lyfrau',\n\n    // Books\n    'book' => 'Llyfr',\n    'books' => 'Llyfrau',\n    'x_books' => ':count Llyfr|:count o Lyfrau',\n    'books_empty' => 'Ni chrëwyd unrhyw llyfrau',\n    'books_popular' => 'Llyfrau Poblogaidd',\n    'books_recent' => 'Llyfrau Diweddar',\n    'books_new' => 'Llyfrau Newydd',\n    'books_new_action' => 'Llyfr Newydd',\n    'books_popular_empty' => 'Bydd y llyfrau mwyaf poblogaidd yn ymddangos yma.',\n    'books_new_empty' => 'Bydd y llyfrau a grëwyd fwyaf diweddar yn ymddangos yma.',\n    'books_create' => 'Creu Llyfr Newydd',\n    'books_delete' => 'Dileu Llyfr',\n    'books_delete_named' => 'Dileu :bookName Llyfr',\n    'books_delete_explain' => 'Bydd hyn yn dileu\\'r llyfr gyda\\'r enw ‘:bookName’. Bydd yr holl dudalennau a phenodau yn cael eu dileu.',\n    'books_delete_confirmation' => 'Ydych chi\\'n siŵr eich bod eisiau dileu\\'r llyfr hwn?',\n    'books_edit' => 'Golygu\\'r Llyfr',\n    'books_edit_named' => 'Golygu :bookName Llyfr',\n    'books_form_book_name' => 'Enw\\'r Llyfr',\n    'books_save' => 'Cadw Llyfr',\n    'books_permissions' => 'Caniatâd Llyfr',\n    'books_permissions_updated' => 'Diweddarwyd Caniatâd Llyfr',\n    'books_empty_contents' => 'Ni chrëwyd unrhyw dudalennau neu benodau ar gyfer y llyfr hwn.',\n    'books_empty_create_page' => 'Creu tudalen newydd',\n    'books_empty_sort_current_book' => 'Trefnu’r llyfr presennol',\n    'books_empty_add_chapter' => 'Ychwanegu pennod',\n    'books_permissions_active' => 'Caniatâd Llyfr yn Weithredol',\n    'books_search_this' => 'Chwilio\\'r llyfr hwn',\n    'books_navigation' => 'Llywio Llyfr',\n    'books_sort' => 'Trefnu Cynnwys Llyfr',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Trefnu Llyfr :bookName',\n    'books_sort_name' => 'Trefnu yn ôl Enw',\n    'books_sort_created' => 'Trefnu yn ôl Dyddiad Creu',\n    'books_sort_updated' => 'Trefnu yn ôl Dyddiad Diweddaru',\n    'books_sort_chapters_first' => 'Penodau yn Gyntaf',\n    'books_sort_chapters_last' => 'Penodau yn Olaf',\n    'books_sort_show_other' => 'Dangos Llyfrau Eraill',\n    'books_sort_save' => 'Cadw’r Drefn Newydd',\n    'books_sort_show_other_desc' => 'Ychwanegwch lyfrau eraill yma i\\'w cynnwys yn y gwaith didoli, a chaniatáu ad-drefnu hawdd rhwng llyfrau.',\n    'books_sort_move_up' => 'Symud i Fyny',\n    'books_sort_move_down' => 'Symud i Lawr',\n    'books_sort_move_prev_book' => 'Symud i\\'r Llyfr Blaenorol',\n    'books_sort_move_next_book' => 'Symud i\\'r Llyfr Nesaf',\n    'books_sort_move_prev_chapter' => 'Symud i\\'r Bennod Flaenorol',\n    'books_sort_move_next_chapter' => 'Symud i\\'r Bennod Nesaf',\n    'books_sort_move_book_start' => 'Symud i Ddechrau\\'r Llyfr',\n    'books_sort_move_book_end' => 'Symud i Ddiwedd y Llyfr',\n    'books_sort_move_before_chapter' => 'Symud i’r Bennod Cynt',\n    'books_sort_move_after_chapter' => 'Symud i’r Bennod Ddilynol',\n    'books_copy' => 'Copio Llyfr',\n    'books_copy_success' => 'Llyfr wedi\\'i copio\\'n llwyddiannus',\n\n    // Chapters\n    'chapter' => 'Pennod',\n    'chapters' => 'Penodau',\n    'x_chapters' => ':count Pennod|:count Penodau',\n    'chapters_popular' => 'Penodau Poblogaidd',\n    'chapters_new' => 'Pennod Newydd',\n    'chapters_create' => 'Creu Pennod Newydd',\n    'chapters_delete' => 'Dileu Pennod',\n    'chapters_delete_named' => 'Dileu :chapterName Pennod',\n    'chapters_delete_explain' => 'Bydd hyn yn dileu\\'r bennod gyda\\'r enw \\':chapterName\\'. Bydd yr holl dudalennau sy\\'n bodoli yn y bennod hon hefyd yn cael eu dileu.',\n    'chapters_delete_confirm' => 'Ydych chi\\'n siŵr eich bod eisiau dileu\\'r bennod hon?',\n    'chapters_edit' => 'Ychwanegu Pennod',\n    'chapters_edit_named' => 'Ychwanegu Pennod :chapterName',\n    'chapters_save' => 'Cadw Pennod',\n    'chapters_move' => 'Symud Pennod',\n    'chapters_move_named' => 'Symud Pennod :chapterName',\n    'chapters_copy' => 'Copïo Pennod',\n    'chapters_copy_success' => 'Pennod wedi\\'i chopïo\\'n llwyddiannus',\n    'chapters_permissions' => 'Pennau Taith Pennod',\n    'chapters_empty' => 'Does dim tudalennau yn y bennod hon ar hyn o bryd.',\n    'chapters_permissions_active' => 'Caniatâd Pennod yn Weithredol',\n    'chapters_permissions_success' => 'Diweddarwyd Caniatâd Pennod',\n    'chapters_search_this' => 'Chwilio yn y bennod hon',\n    'chapter_sort_book' => 'Trefnu Llyfr',\n\n    // Pages\n    'page' => 'Tudalen',\n    'pages' => 'Tudalennau',\n    'x_pages' => ':count Tudalen|:count Tudalennau',\n    'pages_popular' => 'Tudalennau Poblogaidd',\n    'pages_new' => 'Tudalen Newydd',\n    'pages_attachments' => 'Atodiadau',\n    'pages_navigation' => 'Llywio Tudalen',\n    'pages_delete' => 'Dileu Tudalen',\n    'pages_delete_named' => 'Dileu :pageName Tudalen',\n    'pages_delete_draft_named' => 'Dileu Tudalen Ddrafft :pageName',\n    'pages_delete_draft' => 'Dileu Tudalen Ddrafft',\n    'pages_delete_success' => 'Tudalen wedi\\'i dileu',\n    'pages_delete_draft_success' => 'Tudalen ddrafft wedi’i dileu',\n    'pages_delete_warning_template' => 'Mae\\'r dudalen hon yn cael ei defnyddio\\'n weithredol fel templed tudalen diofyn llyfr neu bennod. Ni fydd gan y llyfrau neu\\'r penodau hyn dempled tudalen diofyn wedi\\'i glustnodi ar ôl dileu\\'r dudalen hon.',\n    'pages_delete_confirm' => 'Ydych chi\\'n siŵr eich bod eisiau dileu\\'r dudalen hon?',\n    'pages_delete_draft_confirm' => 'Ydych chi\\'n siŵr eich bod eisiau dileu\\'r dudalen ddrafft hon?',\n    'pages_editing_named' => 'Golygu Tudalen :pageName',\n    'pages_edit_draft_options' => 'Opsiynau Drafft',\n    'pages_edit_save_draft' => 'Cadw Drafft',\n    'pages_edit_draft' => 'Golygu Tudalen Ddrafft',\n    'pages_editing_draft' => 'Golygu Drafft',\n    'pages_editing_page' => 'Golygu Tudalen',\n    'pages_edit_draft_save_at' => 'Cadwyd drafft ar ',\n    'pages_edit_delete_draft' => 'Dileu Drafft',\n    'pages_edit_delete_draft_confirm' => 'Ydych chi\\'n siŵr eich bod am ddileu eich newidiadau i’r dudalen ddrafft? Bydd eich holl newidiadau, ers eu cadw ddiwethaf, yn cael eu colli a bydd y golygydd yn cael ei ddiweddaru gyda\\'r dudalen ddiweddaraf nad yw\\'n ddrafft.',\n    'pages_edit_discard_draft' => 'Gwaredu Drafft',\n    'pages_edit_switch_to_markdown' => 'Newid i’r Golygydd Markdown',\n    'pages_edit_switch_to_markdown_clean' => '(Cynnwys Glân)',\n    'pages_edit_switch_to_markdown_stable' => '(Cynnwys Glân)',\n    'pages_edit_switch_to_wysiwyg' => 'Newid i Olygydd WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg' => 'Newid i WYSIWYG newydd',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Gosod Changelog',\n    'pages_edit_enter_changelog_desc' => 'Rhowch ddisgrifiad byr o\\'r newidiadau rydych wedi\\'u gwneud',\n    'pages_edit_enter_changelog' => 'Cofnodwch Changelog',\n    'pages_editor_switch_title' => 'Newid Golygydd',\n    'pages_editor_switch_are_you_sure' => 'Ydych chi\\'n siŵr eich bod eisiau newid y golygydd ar gyfer y dudalen hon?',\n    'pages_editor_switch_consider_following' => 'Ystyriwch y canlynol wrth newid golygyddion:',\n    'pages_editor_switch_consideration_a' => 'Ar ôl ei gadw, bydd yr opsiwn golygydd newydd yn cael ei ddefnyddio gan unrhyw olygydd yn y dyfodol, gan gynnwys y rhai na fyddant efallai\\'n gallu newid y math o olygydd eu hunain.',\n    'pages_editor_switch_consideration_b' => 'Gall hyn arwain at golli manylion a Syntax mewn rhai amgylchiadau.',\n    'pages_editor_switch_consideration_c' => 'Ni fydd newidiadau tag neu changelog, a wnaed ers eu cadw ddiwethaf, yn parhau ar draws y newid hwn.',\n    'pages_save' => 'Cadw Tudalen',\n    'pages_title' => 'Teitl y Dudalen',\n    'pages_name' => 'Enw\\'r Dudalen',\n    'pages_md_editor' => 'Golygydd',\n    'pages_md_preview' => 'Rhagolwg',\n    'pages_md_insert_image' => 'Mewnosod Delwedd',\n    'pages_md_insert_link' => 'Mewnosod Dolen Endid',\n    'pages_md_insert_drawing' => 'Mewnosod Llun',\n    'pages_md_show_preview' => 'Dangos rhagolwg',\n    'pages_md_sync_scroll' => 'Cydamseru sgrôl ragolwg',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Canfuwyd Llun heb ei Gadw',\n    'pages_drawing_unsaved_confirm' => 'Canfuwyd data llun heb ei gadw o ymgais aflwyddiannus blaenorol i gadw llun. Hoffech chi adfer a pharhau i olygu\\'r llun heb ei gadw?',\n    'pages_not_in_chapter' => 'Nid yw\\'r dudalen mewn pennod',\n    'pages_move' => 'Symud Tudalen',\n    'pages_copy' => 'Copïo Tudalen',\n    'pages_copy_desination' => 'Copïo Cyrchfan',\n    'pages_copy_success' => 'Tudalen wedi\\'i chreu\\'n llwyddiannus',\n    'pages_permissions' => 'Pennau Taith Tudalen',\n    'pages_permissions_success' => 'Pennau taith tudalen wedi\\'u diweddaru',\n    'pages_revision' => 'Diwygiad',\n    'pages_revisions' => 'Diwygiadau\\'r Dudalen',\n    'pages_revisions_desc' => 'Isod ceir holl ddiwygiadau blaenorol y dudalen hon. Gallwch edrych yn ôl ar, cymharu, ac adfer hen fersiynau o’r dudalen os oes gennych y caniatâd priodol. Efallai na fydd hanes llawn y dudalen yn cael ei adlewyrchu\\'n llawn yma oherwydd, gan ddibynnu ar ffurfweddiad y system, gallai hen fersiynau fod wedi’u dileu’n awtomatig.',\n    'pages_revisions_named' => 'Diwygiadau Tudalen ar gyfer :pageName',\n    'pages_revision_named' => 'Diwygiad Tudalen ar gyfer :pageName',\n    'pages_revision_restored_from' => 'Adferwyd o #:id; :summary',\n    'pages_revisions_created_by' => 'Crëwyd gan',\n    'pages_revisions_date' => 'Dyddiad Adolygu',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Rhif Diwygiad',\n    'pages_revisions_numbered' => 'Diwygiad #:id',\n    'pages_revisions_numbered_changes' => 'Diwygiad #:id Newidiadau',\n    'pages_revisions_editor' => 'Math o Olygydd',\n    'pages_revisions_changelog' => 'Changelog',\n    'pages_revisions_changes' => 'Newidiadau',\n    'pages_revisions_current' => 'Fersiwn Bresennol',\n    'pages_revisions_preview' => 'Rhagolwg',\n    'pages_revisions_restore' => 'Adfer',\n    'pages_revisions_none' => 'Nid oes gan y dudalen hon unrhyw ddiwygiadau',\n    'pages_copy_link' => 'Copïo Dolen',\n    'pages_edit_content_link' => 'Neidio i\\'r adran yn y golygydd',\n    'pages_pointer_enter_mode' => 'Rhowch y modd dethol adran',\n    'pages_pointer_label' => 'Dewisiadau Adran Tudalen',\n    'pages_pointer_permalink' => 'Dolen Barhaol Adran Tudalen',\n    'pages_pointer_include_tag' => 'Adran Tudalen Cynnwys Tag',\n    'pages_pointer_toggle_link' => 'Modd dolen barhaol, Pwyswch i ddangos cynnwys tag',\n    'pages_pointer_toggle_include' => 'Modd cynnwys tag, Pwyswch i ddangos dolen barhaol',\n    'pages_permissions_active' => 'Caniatâd Tudalen yn Weithredol',\n    'pages_initial_revision' => 'Cyhoeddi cychwynnol',\n    'pages_references_update_revision' => 'Diweddariad awtomatig y system o ddolenni mewnol',\n    'pages_initial_name' => 'Tudalen Newydd',\n    'pages_editing_draft_notification' => 'Rydych chi wrthi’n golygu drafft a gafodd ei gadw ddiwethaf ar :timeDiff.',\n    'pages_draft_edited_notification' => 'Mae\\'r dudalen hon wedi\\'i diweddaru ers hynny. Argymhellir eich bod yn dileu\\'r drafft hwn.',\n    'pages_draft_page_changed_since_creation' => 'Mae\\'r dudalen hon wedi\\'i diweddaru ers i\\'r drafft hwn gael ei greu. Argymhellir eich bod yn dileu\\'r drafft hwn neu\\'n sicrhau nad ydych yn ysgrifennu unrhyw newidiadau i’r dudalen.',\n    'pages_draft_edit_active' => [\n        'start_a' => 'Mae :count defnyddiwr wedi dechrau golygu\\'r dudalen hon',\n        'start_b' => 'Mae :userName wedi dechrau golygu\\'r dudalen hon',\n        'time_a' => 'ers i\\'r dudalen gael ei diweddaru ddiwethaf',\n        'time_b' => 'yn y :minCount munud diwethaf',\n        'message' => ':start :time. Gofalwch beidio ag ysgrifennu dros ddiweddariadau eich gilydd!',\n    ],\n    'pages_draft_discarded' => 'Drafft wedi\\'i waredu! Mae\\'r golygydd wedi\\'i ddiweddaru gyda chynnwys presennol y dudalen',\n    'pages_draft_deleted' => 'Drafft wedi\\'i ddileu! Mae\\'r golygydd wedi\\'i ddiweddaru gyda chynnwys presennol y dudalen',\n    'pages_specific' => 'Tudalen Benodol',\n    'pages_is_template' => 'Templed Tudalen',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Toglo Bar ochr',\n    'page_tags' => 'Tagiau Tudalennau',\n    'chapter_tags' => 'Tagiau Penodau',\n    'book_tags' => 'Tagiau Llyfrau',\n    'shelf_tags' => 'Tagiau Silffoedd',\n    'tag' => 'Tag',\n    'tags' =>  'Tagiau',\n    'tags_index_desc' => 'Gellir cymhwyso tagiau i gynnwys o fewn y system i sicrhau categoreiddio hyblyg. Gall tagiau fod ag allwedd a gwerth, gyda\\'r gwerth yn ddewisol. Ar ôl ei gymhwyso, gellir cwestiynu’r cynnwys gan ddefnyddio enw a gwerth y tag.',\n    'tag_name' =>  'Enw’r Tag',\n    'tag_value' => 'Gwerth y Tag (Dewisol)',\n    'tags_explain' => \"Ychwanegwch rai tagiau i gategoreiddio'ch cynnwys yn well. Gallwch glustnodi gwerth i dag i gael trefn fanylach.\",\n    'tags_add' => 'Ychwanegu tag arall',\n    'tags_remove' => 'Tynnu’r tag hwn',\n    'tags_usages' => 'Cyfanswm y defnydd o’r tag',\n    'tags_assigned_pages' => 'Clustnodwyd i Dudalennau',\n    'tags_assigned_chapters' => 'Clustnodwyd i Benodau',\n    'tags_assigned_books' => 'Clustnodwyd i Lyfrau',\n    'tags_assigned_shelves' => 'Clustnodwyd i Silffoedd',\n    'tags_x_unique_values' => ':count gwerthoedd unigryw',\n    'tags_all_values' => 'Pob gwerth',\n    'tags_view_tags' => 'Gweld Tagiau',\n    'tags_view_existing_tags' => 'Gweld tagiau presennol',\n    'tags_list_empty_hint' => 'Gellir clustnodi tagiau trwy far ochr golygydd y dudalen neu wrth olygu manylion llyfr, pennod neu silff.',\n    'attachments' => 'Atodiadau',\n    'attachments_explain' => 'Uwchlwytho rhai ffeiliau neu atodi rhai dolenni i\\'w harddangos ar eich tudalen. Mae\\'r rhain i\\'w gweld ym mar ochr y dudalen.',\n    'attachments_explain_instant_save' => 'Caiff y newidiadau yma eu cadw ar unwaith.',\n    'attachments_upload' => 'Uwchlwytho Ffeil',\n    'attachments_link' => 'Atodi Dolen',\n    'attachments_upload_drop' => 'Neu gallwch lusgo a gollwng ffeil yma i\\'w huwchlwytho fel atodiad.',\n    'attachments_set_link' => 'Gosod Dolen',\n    'attachments_delete' => 'Ydych chi\\'n siŵr eich bod eisiau dileu\\'r atodiad hwn?',\n    'attachments_dropzone' => 'Gollyngwch ffeiliau yma i\\'w huwchlwytho',\n    'attachments_no_files' => 'Nid oes unrhyw ffeiliau wedi\\'u huwchlwytho',\n    'attachments_explain_link' => 'Gallwch atodi dolen pe bai’n well gennych beidio ag uwchlwytho ffeil. Gall hyn fod yn ddolen i dudalen arall neu\\'n ddolen i ffeil yn y cwmwl.',\n    'attachments_link_name' => 'Enw’r Ddolen',\n    'attachment_link' => 'Dolen atodiad',\n    'attachments_link_url' => 'Dolen i ffeil',\n    'attachments_link_url_hint' => 'Url y safle neu ffeil',\n    'attach' => 'Atodi',\n    'attachments_insert_link' => 'Ychwanegu Dolen Atodiad i Dudalen',\n    'attachments_edit_file' => 'Golygu Ffeil',\n    'attachments_edit_file_name' => 'Enw\\'r Ffeil',\n    'attachments_edit_drop_upload' => 'Gollwng ffeiliau neu glicio yma i uwchlwytho ac arysgrifennu',\n    'attachments_order_updated' => 'Trefn atodiad wedi’i diweddaru',\n    'attachments_updated_success' => 'Manylion yr atodiad wedi\\'u diweddaru',\n    'attachments_deleted' => 'Atodiad wedi’i ddileu',\n    'attachments_file_uploaded' => 'Ffeil wedi\\'i huwchwytho\\'n llwyddiannus',\n    'attachments_file_updated' => 'Ffeil wedi\\'i diweddaru’n llwyddiannus',\n    'attachments_link_attached' => 'Dolen wedi\\'i atodi’n llwyddiannus i\\'r dudalen',\n    'templates' => 'Templedi',\n    'templates_set_as_template' => 'Mae\\'r dudalen yn dempled',\n    'templates_explain_set_as_template' => 'Gallwch osod y dudalen hon fel templed er mwyn gallu defnyddio ei chynnwys wrth greu tudalennau eraill. Bydd modd i ddefnyddwyr eraill ddefnyddio\\'r templed hwn os oes ganddynt ganiatâd gweld ar gyfer y dudalen hon.',\n    'templates_replace_content' => 'Disodli cynnwys tudalen',\n    'templates_append_content' => 'Atodi i gynnwys tudalen',\n    'templates_prepend_content' => 'Rhagarweiniad i gynnwys tudalen',\n\n    // Profile View\n    'profile_user_for_x' => 'Defnyddiwr am :time',\n    'profile_created_content' => 'Cynnwys a Grëwyd',\n    'profile_not_created_pages' => 'Nid yw :userName wedi creu unrhyw dudalennau',\n    'profile_not_created_chapters' => 'Nid yw :userName wedi creu unrhyw benodau',\n    'profile_not_created_books' => 'Nid yw :userName wedi creu unrhyw lyfrau',\n    'profile_not_created_shelves' => 'Nid yw :userName wedi creu unrhyw silffoedd',\n\n    // Comments\n    'comment' => 'Sylw',\n    'comments' => 'Sylwadau',\n    'comment_add' => 'Ychwanegu Sylw',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Gadewch sylw yma',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Cadw Sylw',\n    'comment_new' => 'Sylw Newydd',\n    'comment_created' => 'sylwodd :createDiff',\n    'comment_updated' => 'Diweddarwyd :update gan :username',\n    'comment_updated_indicator' => 'Diweddarwyd',\n    'comment_deleted_success' => 'Dilëwyd sylw',\n    'comment_created_success' => 'Ychwanegwyd sylw',\n    'comment_updated_success' => 'Diweddarwyd sylw',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Ydych chi\\'n siwr eich bod eisiau dileu\\'r sylw hwn?',\n    'comment_in_reply_to' => 'Mewn ymateb i :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Dyma\\'r sylwadau sydd wedi eu gadael ar y dudalen hon. Gellir ychwanegu a rheoli sylwadau wrth edrych ar y dudalen a gadwyd.',\n\n    // Revision\n    'revision_delete_confirm' => 'Ydych chi\\'n siŵr eich bod eisiau dileu\\'r adolygiad hwn?',\n    'revision_restore_confirm' => 'Ydych chi\\'n siŵr eich bod eisiau adfer yr adolygiad hwn? Bydd cynnwys presennol y dudalen yn cael ei newid.',\n    'revision_cannot_delete_latest' => 'Ni ellir dileu\\'r adolygiad diweddaraf.',\n\n    // Copy view\n    'copy_consider' => 'Ystyriwch yr isod wrth gopïo cynnwys.',\n    'copy_consider_permissions' => 'Ni fydd gosodiadau caniatâd personol yn cael eu copïo.',\n    'copy_consider_owner' => 'Byddwch yn dod yn berchennog yr holl gynnwys sydd wedi’i gopïo.',\n    'copy_consider_images' => 'Ni fydd ffeiliau delwedd tudalen yn cael eu dyblygu a bydd y delweddau gwreiddiol yn cadw eu perthynas â\\'r dudalen y cawsant eu huwchlwytho yn wreiddiol iddi.',\n    'copy_consider_attachments' => 'Ni fydd atodiadau tudalen yn cael eu copïo.',\n    'copy_consider_access' => 'Gall newid lleoliad, perchennog neu ganiatâd olygu bod y cynnwys hwn yn hygyrch i\\'r rhai nad oedd ganddynt fynediad o\\'r blaen.',\n\n    // Conversions\n    'convert_to_shelf' => 'Trosi i Silff',\n    'convert_to_shelf_contents_desc' => 'Gallwch drosi\\'r llyfr hwn i silff newydd gyda\\'r un cynnwys. Bydd penodau yn y llyfr hwn yn cael eu trosi i lyfrau newydd. Os yw\\'r llyfr hwn yn cynnwys unrhyw dudalennau, nad ydynt mewn pennod, bydd y llyfr hwn yn cael ei ailenwi ac yn cynnwys tudalennau o\\'r fath, a bydd y llyfr hwn yn dod yn rhan o\\'r silff newydd.',\n    'convert_to_shelf_permissions_desc' => 'Bydd unrhyw ganiatâd a osodir ar y llyfr hwn yn cael ei gopïo i\\'r silff newydd ac i bob llyfr plentyn newydd nad oes ganddynt eu caniatâd eu hunain. Noder nad yw caniatâd ar silffoedd yn rhaeadru’n awtomatig i’r cynnwys oddi mewn, fel y maent ar gyfer llyfrau.',\n    'convert_book' => 'Trosi Llyfr',\n    'convert_book_confirm' => 'Ydych chi\\'n siwr eich bod eisiau trosi’r llyfr hwn?',\n    'convert_undo_warning' => 'Ni ellir dad-wneud hyn mor hawdd.',\n    'convert_to_book' => 'Trosi i Lyfr',\n    'convert_to_book_desc' => 'Gallwch drosi\\'r bennod hon i lyfr newydd gyda\\'r un cynnwys. Bydd unrhyw ganiatâd a osodir ar y bennod hon yn cael ei gopïo i\\'r llyfr newydd ond ni fydd unrhyw ganiatâd a etifeddir o\\'r llyfr rhiant yn cael ei gopïo, a allai arwain at newid rheolaeth mynediad.',\n    'convert_chapter' => 'Trosi Pennod',\n    'convert_chapter_confirm' => 'Ydych chi\\'n siŵr eich bod eisiau trosi’r bennod hon?',\n\n    // References\n    'references' => 'Cyfeirnodau',\n    'references_none' => 'Nid oes unrhyw gyfeirnodau wedi\\'u holrhain ar gyfer yr eitem hon.',\n    'references_to_desc' => 'Isod ceir yr holl gynnwys hysbys yn y system sy\\'n cysylltu â\\'r eitem hon.',\n\n    // Watch Options\n    'watch' => 'Gwylio',\n    'watch_title_default' => 'Dewisiadau Diofyn',\n    'watch_desc_default' => 'Newid i weld eich dewisiadau hysbysu diofyn yn unig.',\n    'watch_title_ignore' => 'Anwybyddu',\n    'watch_desc_ignore' => 'Anwybyddu pob hysbysiad, gan gynnwys y rhai o ddewisiadau lefel defnyddiwr.',\n    'watch_title_new' => 'Tudalennau Newydd',\n    'watch_desc_new' => 'Rhoi gwybod pan fydd unrhyw dudalen newydd yn cael ei chreu yn yr eitem hon.',\n    'watch_title_updates' => 'Diweddariadau Pob Tudalen',\n    'watch_desc_updates' => 'Hysbysu am bob tudalen newydd a newid i dudalennau.',\n    'watch_desc_updates_page' => 'Hysbysu am bob newid i dudalennau.',\n    'watch_title_comments' => 'Pob Diweddariad i Dualennau a Sylwadau',\n    'watch_desc_comments' => 'Hysbysu am bob tudalen newydd, newidiadau i dudalennau a sylwadau newydd.',\n    'watch_desc_comments_page' => 'Hysbysu am newidiadau i dudalennau a sylwadau newydd.',\n    'watch_change_default' => 'Newid dewisiadau hysbysu diofyn',\n    'watch_detail_ignore' => 'Anwybyddu hysbysiadau',\n    'watch_detail_new' => 'Gwylio am dudalennau newydd',\n    'watch_detail_updates' => 'Gwylio tudalennau a diweddariadau newydd',\n    'watch_detail_comments' => 'Gwylio tudalennau newydd, diweddariadau a sylwadau',\n    'watch_detail_parent_book' => 'Gwylio trwy lyfr rhiant',\n    'watch_detail_parent_book_ignore' => 'Anwybyddu trwy lyfr rhiant',\n    'watch_detail_parent_chapter' => 'Gwylio trwy bennod rhiant',\n    'watch_detail_parent_chapter_ignore' => 'Anwybyddu trwy bennod rhiant',\n];\n"
  },
  {
    "path": "lang/cy/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Nid oes gennych ganiatâd i gael mynediad i\\'r dudalen y gofynnwyd amdani.',\n    'permissionJson' => 'Nid oes gennych ganiatâd i gyflawni\\'r weithred y gofynnwyd amdani.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Mae defnyddiwr gyda\\'r e-bost :email eisoes yn bodoli ond gyda nodweddion gwahanol.',\n    'auth_pre_register_theme_prevention' => 'Nid oedd modd cofrestru cyfrif defnyddiwr ar gyfer y manylion a ddarparwyd',\n    'email_already_confirmed' => 'E-bost eisoes wedi\\'i gadarnhau, Ceisiwch fewngofnodi.',\n    'email_confirmation_invalid' => 'Nid yw\\'r tocyn cadarnhau hwn yn ddilys neu mae eisoes wedi\\'i ddefnyddio. Ceisiwch gofrestru eto.',\n    'email_confirmation_expired' => 'Mae\\'r tocyn cadarnhad wedi dod i ben, Mae e-bost cadarnhau newydd wedi\\'i anfon.',\n    'email_confirmation_awaiting' => 'Mae angen cadarnhau cyfeiriad e-bost y cyfrif a ddefnyddir',\n    'ldap_fail_anonymous' => 'Methodd mynediad LDAP gan ddefnyddio rhwymiad dienw',\n    'ldap_fail_authed' => 'Methodd mynediad LDAP gan ddefnyddio\\'r manylion dn a chyfrinair a roddwyd',\n    'ldap_extension_not_installed' => 'Estyniad PHP LDAP heb ei osod',\n    'ldap_cannot_connect' => 'Methu cysylltu i weinydd ldap, cysylltiad cychwynnol wedi methu',\n    'saml_already_logged_in' => 'Wedi mewngofnodi yn barod',\n    'saml_no_email_address' => 'Methu dod o hyd i gyfeiriad e-bost, ar gyfer y defnyddiwr hwn, yn y data a ddarparwyd gan y system ddilysu allanol',\n    'saml_invalid_response_id' => 'Nid yw\\'r cais o\\'r system ddilysu allanol yn cael ei gydnabod gan broses a ddechreuwyd gan y cais hwn. Gallai llywio yn ôl ar ôl mewngofnodi achosi\\'r broblem hon.',\n    'saml_fail_authed' => 'Wedi methu mewngofnodi gan ddefnyddio :system, ni roddodd y system awdurdodiad llwyddiannus',\n    'oidc_already_logged_in' => 'Wedi mewngofnodi yn barod',\n    'oidc_no_email_address' => 'Methu dod o hyd i gyfeiriad e-bost, ar gyfer y defnyddiwr hwn, yn y data a ddarparwyd gan y system ddilysu allanol',\n    'oidc_fail_authed' => 'Wedi methu mewngofnodi gan ddefnyddio :system, ni roddodd y system awdurdodiad llwyddiannus',\n    'social_no_action_defined' => 'Dim gweithred wedi\\'i diffinio',\n    'social_login_bad_response' => \"Gwall a dderbyniwyd yn ystod mewngofnodi :socialAccount:\\n:error\",\n    'social_account_in_use' => 'Mae\\'r cyfrif :socialAccount hwn eisoes yn cael ei ddefnyddio, Ceisiwch fewngofnodi trwy\\'r opsiwn :socialAccount.',\n    'social_account_email_in_use' => 'Mae\\'r e-bost :email eisoes yn cael ei ddefnyddio. Os oes gennych gyfrif yn barod gallwch gysylltu eich cyfrif :socialAccount o osodiadau eich proffil.',\n    'social_account_existing' => 'Mae\\'r :socialAccount hwn eisoes ynghlwm wrth eich proffil.',\n    'social_account_already_used_existing' => 'Mae\\'r cyfrif :socialAccount hwn eisoes yn cael ei ddefnyddio gan ddefnyddiwr arall.',\n    'social_account_not_used' => 'Nid yw\\'r cyfrif :socialAccount hwn yn gysylltiedig ag unrhyw ddefnyddwyr. Atodwch ef yn eich gosodiadau proffil. ',\n    'social_account_register_instructions' => 'Os nad oes gennych gyfrif eto, gallwch gofrestru cyfrif gan ddefnyddio\\'r opsiwn :socialAccount.',\n    'social_driver_not_found' => 'Gyrrwr cymdeithasol heb ei ganfod',\n    'social_driver_not_configured' => 'Nid yw eich gosodiadau cymdeithasol :socialAccount wedi\\'u ffurfweddu\\'n gywir.',\n    'invite_token_expired' => 'Mae\\'r ddolen wahoddiad hon wedi dod i ben. Yn lle hynny, gallwch chi geisio ailosod cyfrinair eich cyfrif.',\n    'login_user_not_found' => 'Nid oedd modd dod o hyd i ddefnyddiwr ar gyfer y weithred hon.',\n\n    // System\n    'path_not_writable' => 'Nid oedd modd uwchlwytho llwybr ffeil :filePath. Sicrhewch ei fod yn ysgrifenadwy i\\'r gweinydd.',\n    'cannot_get_image_from_url' => 'Methu cael delwedd o :url',\n    'cannot_create_thumbs' => 'Ni all y gweinydd greu mân-luniau. Gwiriwch fod gennych yr estyniad GD PHP wedi\\'i osod.',\n    'server_upload_limit' => 'Nid yw\\'r gweinydd yn caniatáu uwchlwythiadau o\\'r maint hwn. Rhowch gynnig ar faint ffeil llai.',\n    'server_post_limit' => 'Ni all y gweinydd dderbyn y swm o ddata a ddarparwyd. Rhowch gynnig arall arni eto gyda llai o ddata neu ffeil lai.',\n    'uploaded'  => 'Nid yw\\'r gweinydd yn caniatáu uwchlwythiadau o\\'r maint hwn. Rhowch gynnig ar faint ffeil llai.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Bu gwall wrth uwchlwytho\\'r ddelwedd',\n    'image_upload_type_error' => 'Mae\\'r math o ddelwedd sy\\'n cael ei huwchlwytho yn annilys',\n    'image_upload_replace_type' => 'Rhaid i ffeiliau delwedd a newidir fod o\\'r un math',\n    'image_upload_memory_limit' => 'Methwyd â thrin y llun a uwchlwythwyd a/neu greu mân-luniau oherwydd cyfyngiadau i adnoddau’r system.',\n    'image_thumbnail_memory_limit' => 'Methwyd â chreu amrywiadau i faint y llun oherwydd cyfyngiadau i adnoddau’r system.',\n    'image_gallery_thumbnail_memory_limit' => 'Methwyd â chreu oriel o fân-luniau oherwydd cyfyngiadau i adnoddau’r system.',\n    'drawing_data_not_found' => 'Nid oedd modd llwytho\\'r data dylunio. Efallai nad yw’r ffeil ddylunio yn bodoli mwyach neu efallai nad oes gennych ganiatâd i\\'w defnyddio.',\n\n    // Attachments\n    'attachment_not_found' => 'Ni chanfuwyd yr atodiad',\n    'attachment_upload_error' => 'Digwyddodd gwall wrth uwchlwytho’r ffeil atodiad',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Wedi methu cadw\\'r drafft. Sicrhewch fod gennych gysylltiad rhyngrwyd cyn cadw\\'r dudalen hon',\n    'page_draft_delete_fail' => 'Methwyd â dileu’r dudalen ddrafft a chyrchu cynnwys y dudalen gyfredol',\n    'page_custom_home_deletion' => 'Methu dileu tudalen tra ei bod wedi\\'i gosod fel hafan',\n\n    // Entities\n    'entity_not_found' => 'Endid heb ei ganfod',\n    'bookshelf_not_found' => 'Ni chanfuwyd y silff',\n    'book_not_found' => 'Ni chanfuwyd y llyfr',\n    'page_not_found' => 'Heb ganfod y dudalen',\n    'chapter_not_found' => 'Pennod heb ei chanfod',\n    'selected_book_not_found' => 'Ni ddaethpwyd o hyd i\\'r llyfr a ddewiswyd',\n    'selected_book_chapter_not_found' => 'Ni ddaethpwyd o hyd i\\'r Llyfr neu\\'r Bennod a ddewiswyd',\n    'guests_cannot_save_drafts' => 'Ni all gwesteion arbed drafftiau',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Ni allwch ddileu\\'r unig weinyddwr',\n    'users_cannot_delete_guest' => 'Ni allwch ddileu\\'r defnyddiwr gwadd',\n    'users_could_not_send_invite' => 'Methu creu defnyddiwr oherwydd ni fu modd anfon e-bost gwahodd',\n\n    // Roles\n    'role_cannot_be_edited' => 'Nid oes modd golygu\\'r rôl hon',\n    'role_system_cannot_be_deleted' => 'Rôl system yw\\'r rôl hon ac ni ellir ei dileu',\n    'role_registration_default_cannot_delete' => 'Ni ellir dileu\\'r rôl hon tra ei bod wedi\\'i gosod fel y rôl gofrestru ddiofyn',\n    'role_cannot_remove_only_admin' => 'Y defnyddiwr hwn yw\\'r unig ddefnyddiwr sydd wedi\\'i neilltuo i rôl y gweinyddwr. Neilltuo rôl y gweinyddwr i ddefnyddiwr arall cyn ceisio ei dynnu yma.',\n\n    // Comments\n    'comment_list' => 'Digwyddodd gwall wrth nôl y sylwadau.',\n    'cannot_add_comment_to_draft' => 'Ni allwch ychwanegu sylwadau at ddrafft.',\n    'comment_add' => 'Digwyddodd gwall wrth ychwanegu / diweddaru\\'r sylw.',\n    'comment_delete' => 'Digwyddodd gwall wrth dileu\\'r sylwad.',\n    'empty_comment' => 'Methu ychwanegu sylw gwag.',\n\n    // Error pages\n    '404_page_not_found' => 'Heb ganfod y dudalen',\n    'sorry_page_not_found' => 'Mae\\'n ddrwg gennym, nid oedd modd dod o hyd i\\'r dudalen roeddech yn chwilio amdani.',\n    'sorry_page_not_found_permission_warning' => 'Os oeddech yn disgwyl i\\'r dudalen hon fodoli, efallai na fyddai gennych ganiatâd i\\'w gweld.',\n    'image_not_found' => 'Heb ganfod y delwedd',\n    'image_not_found_subtitle' => 'Mae\\'n ddrwg gennym, ni fu modd dod o hyd i\\'r ffeil delwedd roeddech yn chwilio amdani.',\n    'image_not_found_details' => 'Os oeddech chi\\'n disgwyl i\\'r ddelwedd hon fodoli efallai ei bod wedi\\'i dileu.',\n    'return_home' => 'Dychwelyd i gartref',\n    'error_occurred' => 'Digwyddodd Gwall',\n    'app_down' => 'Mae :appName i lawr ar hyn o bryd',\n    'back_soon' => 'Bydd yn ôl i fyny yn fuan.',\n\n    // Import\n    'import_zip_cant_read' => 'Wedi methu darllen ffeil ZIP.',\n    'import_zip_cant_decode_data' => 'Wedi methu ffeindio a dadgodio cynnwys ZIP data.json.',\n    'import_zip_no_data' => 'Nid oes cynnwys llyfr, pennod neu dudalen disgwyliedig yn nata ffeil ZIP.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'ZIP mewnforyn wedi\\'i methu dilysu gyda gwallau:',\n    'import_zip_failed_notification' => 'Wedi methu mewnforio ffeil ZIP.',\n    'import_perms_books' => 'Dych chi\\'n methu\\'r caniatâd gofynnol i greu llyfrau.',\n    'import_perms_chapters' => 'Dych chi\\'n methu\\'r caniatâd gofynnol i greu pennodau.',\n    'import_perms_pages' => 'Dych chi\\'n methu\\'r caniatâd gofynnol i greu tudalennau.',\n    'import_perms_images' => 'Dych chi\\'n methu\\'r caniatâd gofynnol i greu delwau.',\n    'import_perms_attachments' => 'Dych chi\\'n methu\\'r caniatâd gofynnol i greu atodiadau.',\n\n    // API errors\n    'api_no_authorization_found' => 'Ni chanfuwyd tocyn awdurdodi ar y cais',\n    'api_bad_authorization_format' => 'Canfuwyd tocyn awdurdodi ar y cais ond roedd yn ymddangos bod y fformat yn anghywir',\n    'api_user_token_not_found' => 'Ni chanfuwyd tocyn API cyfatebol ar gyfer y tocyn awdurdodi a ddarparwyd',\n    'api_incorrect_token_secret' => 'Mae\\'r gyfrinach a ddarparwyd ar gyfer y tocyn API defnyddiedig a roddwyd yn anghywir',\n    'api_user_no_api_permission' => 'Nid oes gan berchennog y tocyn API a ddefnyddiwyd ganiatâd i wneud galwadau API',\n    'api_user_token_expired' => 'Mae\\'r tocyn awdurdodi a ddefnyddiwyd wedi dod i ben',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Gwall a daflwyd wrth anfon e-bost prawf:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'Nid yw\\'r URL yn cyd-fynd â\\'r gwesteion SSR ffurfweddu a ganiateir',\n];\n"
  },
  {
    "path": "lang/cy/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Sylw newydd ar dudalen :pageName',\n    'new_comment_intro' => 'Mae defnyddiwr wedi sylw ar dudalen yn :appName:',\n    'new_page_subject' => 'Tudalen newydd :pageName',\n    'new_page_intro' => 'Mae tudalen newydd wedi cael ei chreu yn :appName:',\n    'updated_page_subject' => 'Tudalen wedi\\'i diweddaru :pageName',\n    'updated_page_intro' => 'Mae tudalen newydd wedi cael ei diweddaru yn :appName:',\n    'updated_page_debounce' => 'Er mwyn atal llu o hysbysiadau, am gyfnod ni fyddwch yn cael hysbysiadau am ragor o olygiadau i\\'r dudalen hon gan yr un golygydd.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Enw\\'r dudalen:',\n    'detail_page_path' => 'Llwybr Tudalen:',\n    'detail_commenter' => 'Sylwebydd:',\n    'detail_comment' => 'Sylw:',\n    'detail_created_by' => 'Crëwyd gan:',\n    'detail_updated_by' => 'Diweddarwyd Gan:',\n\n    'action_view_comment' => 'Gweld y sylw',\n    'action_view_page' => 'Gweld y dudalen',\n\n    'footer_reason' => 'Anfonwyd yr hysbysiad hwn atoch oherwydd bod y :ddolen yn ymdrin â’r math hwn o weithgaredd ar gyfer yr eitem hon.',\n    'footer_reason_link' => 'eich dewisiadau hysbysu',\n];\n"
  },
  {
    "path": "lang/cy/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Cynt',\n    'next'     => 'Nesa &raquo;',\n\n];\n"
  },
  {
    "path": "lang/cy/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Rhaid i gyfrineiriau fod yn 8 nod o leiaf ac yn cyd-fynd â\\'r cadarnhad.',\n    'user' => \"Ni allwn ddod o hyd i ddefnyddiwr gyda'r cyfeiriad e-bost hwn.\",\n    'token' => 'Mae\\'r tocyn ailosod cyfrinair yn annilys ar gyfer y cyfeiriad e-bost hwn.',\n    'sent' => 'Rydym wedi e-bostio eich dolen ailosod cyfrinair!',\n    'reset' => 'Mae eich cyfrinair wedi\\'i ailosod!',\n\n];\n"
  },
  {
    "path": "lang/cy/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Fy Nghyfrif',\n\n    'shortcuts' => 'Llwybrau Byr',\n    'shortcuts_interface' => 'Dewisiadau Llwybr Byr UI',\n    'shortcuts_toggle_desc' => 'Yma gallwch alluogi neu analluogi llwybrau byr rhyngwyneb system ar y bysellfwrdd, a ddefnyddir ar gyfer llywio a gweithredoedd.',\n    'shortcuts_customize_desc' => 'Gallwch addasu pob un o\\'r llwybrau byr isod. Pwyswch eich cyfuniad o fysellau ar ôl dewis y mewnbwn ar gyfer llwybr byr.',\n    'shortcuts_toggle_label' => 'Llwybrau byr bysellfwrdd wedi\\'u galluogi',\n    'shortcuts_section_navigation' => 'Llywio',\n    'shortcuts_section_actions' => 'Gweithredoedd Cyffredin',\n    'shortcuts_save' => 'Cadw Llwybrau Byr',\n    'shortcuts_overlay_desc' => 'Noder: Pan fydd llwybrau byr yn cael eu galluogi, mae troshaen helpwr ar gael trwy bwyso \"?\" a fydd yn tynnu sylw at y llwybrau byr sydd ar gael ar gyfer camau gweithredu sydd i\\'w gweld ar y sgrin ar hyn o bryd.',\n    'shortcuts_update_success' => 'Mae’r dewisiadau llwybr byr wedi\\'u diweddaru!',\n    'shortcuts_overview_desc' => 'Rheoli llwybrau byr bysellfwrdd a gellir eu defnyddio i lywio trwy ryngwyneb defnyddiwr y system.',\n\n    'notifications' => 'Dewisiadau Hysbysu',\n    'notifications_desc' => 'Rheoli’r hysbysiadau e-bost a gewch pan fydd gweithgaredd penodol yn cael ei gyflawni o fewn y system.',\n    'notifications_opt_own_page_changes' => 'Hysbysu am newidiadau i dudalennau yr wyf yn berchen arnynt',\n    'notifications_opt_own_page_comments' => 'Hysbysu am sylwadau ar dudalennau yr wyf yn berchen arnynt',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Hysbysu am atebion i\\'m sylwadau',\n    'notifications_save' => 'Dewisiadau Cadw',\n    'notifications_update_success' => 'Mae’r dewisiadau hysbysu wedi\\'u diweddaru!',\n    'notifications_watched' => 'Eitemau Gwylio ac Anwybyddu',\n    'notifications_watched_desc' => 'Isod ceir yr eitemau sydd â dewisiadau gwylio penodol wedi\\'u cymhwyso. I ddiweddaru eich dewisiadau ar gyfer y rhain, edrychwch ar yr eitem yna dewch o hyd i\\'r opsiynau gwylio yn y bar ochr.',\n\n    'auth' => 'Mynediad a Diogelwch',\n    'auth_change_password' => 'Newid Cyfrinair',\n    'auth_change_password_desc' => 'Newidiwch y cyfrinair rydych chi’n ei ddefnyddio i fewngofnodi i’r rhaglen. Rhaid i gyfrineiriau fod yn 8 nod o leiaf.',\n    'auth_change_password_success' => 'Mae\\'r cyfrinair wedi\\'i ddiweddaru!',\n\n    'profile' => 'Manylion Proffil',\n    'profile_desc' => 'Rheoli manylion eich cyfrif sy\\'n eich cynrychioli chi i ddefnyddwyr eraill, yn ogystal â manylion a ddefnyddir ar gyfer cyfathrebu a phersonoli systemau.',\n    'profile_view_public' => 'Gweld Proffil Cyhoeddus',\n    'profile_name_desc' => 'Ffurfweddwch eich enw arddangos a fydd yn weladwy i ddefnyddwyr eraill yn y system trwy\\'r hyn yr ydych yn ei wneud, a\\'r cynnwys rydych chi\\'n berchen arno.',\n    'profile_email_desc' => 'Bydd yr e-bost hwn yn cael ei ddefnyddio ar gyfer hysbysiadau a, gan ddibynnu ar ddilysiad system gweithredol, mynediad i’r system.',\n    'profile_email_no_permission' => 'Yn anffodus, nid oes gennych ganiatâd i newid eich cyfeiriad e-bost. Os hoffech newid hwn, byddai angen i chi ofyn i weinyddwr newid hyn ar eich rhan.',\n    'profile_avatar_desc' => 'Dewiswch lun a fydd yn cael ei ddefnyddio i’ch cynrychioli chi i eraill yn y system. Yn ddelfrydol, dylai\\'r llun hwn fod yn sgwâr a thua 256px o led ac uchder.',\n    'profile_admin_options' => 'Dewisiadau Gweinyddwr',\n    'profile_admin_options_desc' => 'Gellir dod o hyd i ddewisiadau lefel gweinyddwyr ychwanegol, fel y rhai i reoli aseiniadau rôl, ar gyfer eich cyfrif defnyddiwr yn yr ardal \"Gosodiadau > Defnyddiwr” o’r rhaglen.',\n\n    'delete_account' => 'Dileu Cyfrif',\n    'delete_my_account' => 'Dileu fy Nghyfrif',\n    'delete_my_account_desc' => 'Bydd hyn yn dileu eich cyfrif defnyddiwr o\\'r system yn llwyr. Ni fydd modd i chi adfer y cyfrif hwn na gwrthdroi\\'r weithred hon. Bydd cynnwys rydych chi wedi\\'i greu, megis tudalennau wedi\\'u creu a delweddau wedi\\'u huwchlwytho, yn parhau.',\n    'delete_my_account_warning' => 'Ydych chi\\'n siŵr eich bod eisiau dileu eich cyfrif?',\n];\n"
  },
  {
    "path": "lang/cy/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Gosodiadau',\n    'settings_save' => 'Cadw Gosodiadau',\n    'system_version' => 'Fersiwn System',\n    'categories' => 'Categorïau',\n\n    // App Settings\n    'app_customization' => 'Addasiad',\n    'app_features_security' => 'Nodweddion & Diogelwch',\n    'app_name' => 'Enw\\'r Rhaglen',\n    'app_name_desc' => 'Dangosir yr enw hwn yn y pennawd ac mewn unrhyw negeseuon e-bost a anfonir gan y system.',\n    'app_name_header' => 'Dangos enw yn y pennyn',\n    'app_public_access' => 'Mynediad Cyhoeddus',\n    'app_public_access_desc' => 'Bydd galluogi\\'r opsiwn hwn yn caniatáu i ymwelwyr, nad ydynt wedi mewngofnodi, gael mynediad at gynnwys yn achos BookStack.',\n    'app_public_access_desc_guest' => 'Gellir rheoli mynediad i ymwelwyr cyhoeddus trwy\\'r defnyddiwr \"Gwestai\".',\n    'app_public_access_toggle' => 'Caniatáu mynediad i\\'r cyhoedd',\n    'app_public_viewing' => 'Caniatáu i\\'r cyhoedd weld?',\n    'app_secure_images' => 'Uwchlwytho Delwedd Diogelwch Uwch',\n    'app_secure_images_toggle' => 'Galluogi uwchlwytho delwedd diogelwch uwch',\n    'app_secure_images_desc' => 'Am resymau perfformiad, mae\\'r holl ddelweddau yn gyhoeddus. Mae\\'r opsiwn hwn yn ychwanegu llinyn ar hap, anodd ei ddyfalu o flaen urls delwedd. Sicrhau nad yw mynegeion cyfeiriadur yn cael eu galluogi i atal mynediad hawdd.',\n    'app_default_editor' => 'Golygydd Tudalen Diofyn',\n    'app_default_editor_desc' => 'Dewiswch pa olygydd fydd yn cael ei ddefnyddio yn ddiofyn wrth olygu tudalennau newydd. Gellir diystyru hyn ar lefel tudalen os yw’r caniatâd yn galluogi hyn.',\n    'app_custom_html' => 'Cynnwys Pen HTML wedi’i deilwra',\n    'app_custom_html_desc' => 'Bydd unrhyw gynnwys a ychwanegir yma yn cael ei fewnosod i waelod yr adran <head> ar bob tudalen. Mae hyn yn ddefnyddiol ar gyfer diystyru arddulliau neu ychwanegu cod dadansoddeg.',\n    'app_custom_html_disabled_notice' => 'Mae cynnwys pen HTML wedi\\'i deilwra wedi’i analluogi ar y dudalen gosodiadau hon i sicrhau y gellir troi unrhyw newidiadau toriadol yn ôl.',\n    'app_logo' => 'Logo’r Rhaglen',\n    'app_logo_desc' => 'Defnyddir hwn ym mar pennawd y rhaglen, ymhlith ardaloedd eraill. Dylai\\'r ddelwedd hon fod yn 86px o uchder. Bydd delweddau mawr yn cael eu graddio i lawr.',\n    'app_icon' => 'Eicon yr Ap',\n    'app_icon_desc' => 'Defnyddir yr eicon hwn ar gyfer tabiau porwr ac eiconau llwybr byr. Dylai hwn fod yn ddelwedd PNG sgwâr 256px.',\n    'app_homepage' => 'Hafan y Rhaglen',\n    'app_homepage_desc' => 'Dewiswch wedd i\\'w dangos ar yr hafan yn hytrach na\\'r wedd ddiofyn. Anwybyddir caniatâd tudalen ar gyfer tudalennau dethol.',\n    'app_homepage_select' => 'Dewiswch dudalen',\n    'app_footer_links' => 'Dolenni Troedynnau',\n    'app_footer_links_desc' => 'Ychwanegwch ddolenni i\\'w dangos o fewn troedyn y safle. Bydd y rhain yn cael eu harddangos ar waelod y rhan fwyaf o dudalennau, gan gynnwys y rhai nad oes angen mewngofnodi. Gallwch ddefnyddio label \"trans::<key> i ddefnyddio cyfieithiadau wedi\\'u diffinio gan y system. Er enghraifft: Bydd defnyddio \"trans::common.privacy_policy\" yn darparu\\'r testun wedi’i gyfieithu \"Polisi Preifatrwydd\" a \"trans::common.terms_of_service\" yn darparu\\'r testun wedi’i gyfieithu \"Telerau Gwasanaeth\".',\n    'app_footer_links_label' => 'Label Dolen',\n    'app_footer_links_url' => 'URL Dolen',\n    'app_footer_links_add' => 'Ychwanegu Dolen Troedyn',\n    'app_disable_comments' => 'Analluogi Sylwadau',\n    'app_disable_comments_toggle' => 'Analluogi sylwadau',\n    'app_disable_comments_desc' => 'Analluogi sylwadau ar draws pob tudalen yn y rhaglen. <br> Nid yw\\'r sylwadau presennol yn cael eu dangos.',\n\n    // Color settings\n    'color_scheme' => 'Cynllun Lliw’r Rhaglen',\n    'color_scheme_desc' => 'Gosodwch y lliwiau i\\'w defnyddio yn rhyngwyneb defnyddiwr y rhaglen. Gellir ffurfweddu lliwiau ar wahân ar gyfer moddau tywyll a golau i gyd-fynd â\\'r thema orau a sicrhau ei fod yn ddarllenadwy.',\n    'ui_colors_desc' => 'Gosodwch liw sylfaenol y rhaglen a’r lliw diofyn y ddolen. Defnyddir y lliw sylfaenol yn bennaf ar gyfer baner y pennyn, y botymau ac addurniadau rhyngwyneb. Defnyddir lliw diofyn y ddolen ar gyfer dolenni a chamau gweithredu testun, o fewn cynnwys ysgrifenedig ac yn rhyngwyneb y rhaglen.',\n    'app_color' => 'Prif Liw',\n    'link_color' => 'Lliw Diofyn y Ddolen',\n    'content_colors_desc' => 'Gosodwch liwiau ar gyfer pob elfen yn hierarchaeth trefn y dudalen. Argymhellir dewis lliwiau â disgleirdeb tebyg i\\'r lliwiau diofyn ar gyfer darllenadwyedd.',\n    'bookshelf_color' => 'Lliw Silff',\n    'book_color' => 'Lliw Llyfr',\n    'chapter_color' => 'Lliw Pennod',\n    'page_color' => 'Lliw Tudalen',\n    'page_draft_color' => 'Lliw Tudalen Ddrafft',\n\n    // Registration Settings\n    'reg_settings' => 'Cofrestriad',\n    'reg_enable' => 'Galluogi Cofrestru',\n    'reg_enable_toggle' => 'Galluogi cofrestru',\n    'reg_enable_desc' => 'Pan fydd cofrestru wedi\\'i alluogi bydd modd i ddefnyddwyr gofrestru fel defnyddwyr y rhaglen. Wrth gofrestru maent yn cael un rôl defnyddiwr diofyn.',\n    'reg_default_role' => 'Rôl defnyddiwr diofyn ar ôl cofrestru',\n    'reg_enable_external_warning' => 'Anwybyddir y dewis uchod tra bod dilysu LDAP neu SAML allanol yn weithredol. Bydd cyfrifon defnyddwyr ar gyfer aelodau nad ydynt yn bodoli yn cael eu creu\\'n awtomatig os bydd dilysiad, yn erbyn y system allanol sy\\'n cael ei defnyddio, yn llwyddiannus.',\n    'reg_email_confirmation' => 'Cadarnhad E-bost',\n    'reg_email_confirmation_toggle' => 'Angen cadarnhad e-bost',\n    'reg_confirm_email_desc' => 'Os defnyddir cyfyngiad parth, bydd angen cadarnhad e-bost a bydd yr opsiwn hwn yn cael ei anwybyddu.',\n    'reg_confirm_restrict_domain' => 'Cyfyngiad Parth',\n    'reg_confirm_restrict_domain_desc' => 'Rhowch restr wedi\\'i gwahanu gan goma o barthau e-bost yr hoffech gyfyngu cofrestriad iddynt. Bydd defnyddwyr yn cael e-bost i gadarnhau eu cyfeiriad cyn cael caniatâd i ryngweithio â\\'r rhaglen. <br> Noder y bydd modd i ddefnyddwyr newid eu cyfeiriadau e-bost ar ôl cofrestru\\'n llwyddiannus.',\n    'reg_confirm_restrict_domain_placeholder' => 'Ni osodwyd cyfyngiad',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Cynnal',\n    'maint_image_cleanup' => 'Glanhau Delweddau',\n    'maint_image_cleanup_desc' => 'Sganio tudalennau a chynnwys adolygu i wirio pa ddelweddau a darluniau sy\\'n cael eu defnyddio ar hyn o bryd a pha ddelweddau sy\\'n cael eu diddymu. Gwnewch yn siŵr eich bod yn creu cronfa ddata lawn a chopi wrth gefn o’r ddelwedd cyn rhedeg hwn.',\n    'maint_delete_images_only_in_revisions' => 'Hefyd dilëwch ddelweddau sydd ond yn bodoli mewn dhen dudalennau',\n    'maint_image_cleanup_run' => 'Rhedeg Glanhau',\n    'maint_image_cleanup_warning' => 'Daethpwyd o hyd i :count o ddelweddau nas defnyddiwyd o bosibl. Ydych chi\\'n siŵr eich bod eisiau dileu’r delweddau hyn?',\n    'maint_image_cleanup_success' => 'daethpwyd o hyd i :count o ddelweddau nas defnyddiwyd o bosibl a chawsant eu dileu!',\n    'maint_image_cleanup_nothing_found' => 'Daethpwyd o hyd i ddim ddelweddau, Dim byd wedi\\'i dileu!',\n    'maint_send_test_email' => 'Anfon E-bost Prawf',\n    'maint_send_test_email_desc' => 'Mae hyn yn anfon e-bost prawf i\\'ch cyfeiriad e-bost a nodir yn eich proffil.',\n    'maint_send_test_email_run' => 'Anfon e-bost prawf',\n    'maint_send_test_email_success' => 'Anfonwyd e-bost at :address',\n    'maint_send_test_email_mail_subject' => 'E-bost Prawf',\n    'maint_send_test_email_mail_greeting' => 'Mae\\'n ymddangos bod anfon e-bost yn gweithio!',\n    'maint_send_test_email_mail_text' => 'Llongyfarchiadau! Gan eich bod wedi derbyn yr hysbysiad e-bost hwn, mae\\'n ymddangos bod eich gosodiadau e-bost wedi\\'u ffurfweddu\\'n iawn.',\n    'maint_recycle_bin_desc' => 'Caiff silffoedd, llyfrau, penodau a thudalennau wedi\\'u dileu eu hanfon i’r bin ailgylchu fel y gellir eu hadfer neu eu dileu\\'n barhaol. Bydd eitemau hŷn yn y bin ailgylchu yn cael eu dileu’n awtomatig ar ôl ychydig o bosibl, gan ddibynnu ar ffurfweddiad y system.',\n    'maint_recycle_bin_open' => 'Agor y Bin Ailgylchu',\n    'maint_regen_references' => 'Atgynhyrchu Cyfeiriadau',\n    'maint_regen_references_desc' => 'Bydd y weithred hon yn ailadeiladu\\'r mynegai cyfeirio traws-eitem yn y gronfa ddata. Fel arfer, caiff hyn ei drin yn awtomatig ond gall y weithred hon fod yn ddefnyddiol i fynegeio hen gynnwys neu gynnwys a ychwanegwyd trwy ddulliau answyddogol.',\n    'maint_regen_references_success' => 'Mae’r mynegai cyfeirio wedi cael ei atgynhyrchu!',\n    'maint_timeout_command_note' => 'Noder: Gall y weithred hon gymryd amser i redeg, a all arwain at faterion amseru mewn rhai amgylcheddau gwe. Fel dewis arall, cyflawnir y weithred hon gan ddefnyddio gorchymyn terfynell.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Bin Ailgylchu',\n    'recycle_bin_desc' => 'Yma gallwch adfer eitemau sydd wedi\\'u dileu neu ddewis eu dileu yn barhaol oddi ar y system. Nid yw’r rhestr hon wedi’i hidlo yn wahanol i restrau gweithgaredd tebyg yn y system lle mae hidlwyr caniatâd yn cael eu cymhwyso.',\n    'recycle_bin_deleted_item' => 'Eitem wedi\\'i Dileu',\n    'recycle_bin_deleted_parent' => 'Rhiant',\n    'recycle_bin_deleted_by' => 'Dilëwyd gan',\n    'recycle_bin_deleted_at' => 'Amser Dileu',\n    'recycle_bin_permanently_delete' => 'Dileu yn Barhaol',\n    'recycle_bin_restore' => 'Adfer',\n    'recycle_bin_contents_empty' => 'Mae\\'r bin ailgylchu yn wag ar hyn o bryd',\n    'recycle_bin_empty' => 'Gwagio’r Bin Ailgylchu',\n    'recycle_bin_empty_confirm' => 'Bydd hyn yn dileu pob eitem yn y bin ailgylchu yn barhaol gan gynnwys popeth sydd wedi\\'i gynnwys ym mhob eitem. Ydych chi\\'n siŵr eich bod am wagio’r bin ailgylchu?',\n    'recycle_bin_destroy_confirm' => 'Bydd y weithred hon yn dileu\\'r eitem hon o\\'r system yn barhaol, ynghyd ag unrhyw elfennau plentyn a restrir isod, ac ni fyddwch yn gallu adfer y cynnwys hwn. Ydych chi\\'n siŵr eich bod eisiau dileu\\'r eitem hon yn barhaol?',\n    'recycle_bin_destroy_list' => 'Eitemau i\\'w Dinistrio',\n    'recycle_bin_restore_list' => 'Eitemau i\\'w Hadfer',\n    'recycle_bin_restore_confirm' => 'Bydd y weithred hon yn adfer yr eitem a ddilëwyd, gan gynnwys unrhyw elfennau plentyn, i\\'w lleoliad gwreiddiol. Os yw\\'r lleoliad gwreiddiol wedi\\'i ddileu ers hynny, a’i fod bellach yn y bin ailgylchu, bydd angen adfer yr eitem riant hefyd.',\n    'recycle_bin_restore_deleted_parent' => 'Mae rhiant yr eitem hon hefyd wedi\\'i dileu. Bydd y rhain yn parhau i fod wedi’u dileu nes bod y rhiant hwnnw hefyd yn cael ei adfer.',\n    'recycle_bin_restore_parent' => 'Adfer Rhiant',\n    'recycle_bin_destroy_notification' => 'Dilëwyd cyfanswm o :count o eitemau o\\'r bin ailgylchu.',\n    'recycle_bin_restore_notification' => 'Adferwyd cyfanswm o :count o eitemau o\\'r bin ailgylchu.',\n\n    // Audit Log\n    'audit' => 'Log Awdit',\n    'audit_desc' => 'Mae\\'r cofnod archwilio hwn yn dangos rhestr o weithgareddau sydd wedi\\'u holrhain yn y system. Nid yw’r rhestr hon wedi’i hidlo yn wahanol i restrau gweithgaredd tebyg yn y system lle mae hidlwyr caniatâd yn cael eu cymhwyso.',\n    'audit_event_filter' => 'Hidlydd Digwyddiad',\n    'audit_event_filter_no_filter' => 'Dim Fidlydd',\n    'audit_deleted_item' => 'Eitem wedi\\'i Dileu',\n    'audit_deleted_item_name' => 'Enw: :name',\n    'audit_table_user' => 'Defnyddiwr',\n    'audit_table_event' => 'Digwyddiad',\n    'audit_table_related' => 'Eitem neu Fanylion Cysylltiedig',\n    'audit_table_ip' => 'Cyfeiriad IP',\n    'audit_table_date' => 'Dyddiad Gweithgaredd',\n    'audit_date_from' => 'Amrediad Dyddiad O',\n    'audit_date_to' => 'Amrediad Dyddiad Tan',\n\n    // Role Settings\n    'roles' => 'Rolau',\n    'role_user_roles' => 'Rolau Defnyddiwr',\n    'roles_index_desc' => 'Defnyddir rolau i grwpio defnyddwyr a rhoi caniatâd system i\\'w haelodau. Pan fydd defnyddiwr yn aelod o sawl rôl, bydd y breintiau a roddir yn pentyrru a bydd y defnyddiwr yn etifeddu pob gallu.',\n    'roles_x_users_assigned' => ':count defnyddiwr wedi\\'u clustnodi|:count o ddefnyddwyr wedi\\'u clustnodi',\n    'roles_x_permissions_provided' => ':count caniatâd|:count caniatâd',\n    'roles_assigned_users' => 'Clustnodi Defnyddwyr',\n    'roles_permissions_provided' => 'Darparu Caniatâd',\n    'role_create' => 'Creu Rôl Newydd',\n    'role_delete' => 'Dileu Rôl',\n    'role_delete_confirm' => 'Bydd hyn yn dileu\\'r rôl gyda\\'r enw \\':roleName\\'.',\n    'role_delete_users_assigned' => 'Mae gan y rôl hon :userCount o ddefnyddwyr wedi’u clustnodi iddi. Os hoffech chi fudo\\'r defnyddwyr o\\'r rôl hon dewiswch rôl newydd isod.',\n    'role_delete_no_migration' => \"Peidiwch â mudo defnyddwyr\",\n    'role_delete_sure' => 'Wyt ti\\'n bendant eisiau dileu\\'r rôl hwn?',\n    'role_edit' => 'Golygu Rôl',\n    'role_details' => 'Manylion Rôl',\n    'role_name' => 'Enw Rôl',\n    'role_desc' => 'Disgrifiad Byr o’r Rôl',\n    'role_mfa_enforced' => 'Angen Dilysu Aml-Ffactor',\n    'role_external_auth_id' => 'ID Dilysu Allanol',\n    'role_system' => 'Caniatâd System',\n    'role_manage_users' => 'Rheoli defnyddwyr',\n    'role_manage_roles' => 'Rheoli rolau & chaniatâd rolau',\n    'role_manage_entity_permissions' => 'Rheoli caniatâd pob llyfr, pennod a thudalen',\n    'role_manage_own_entity_permissions' => 'Rheoli caniatâd eich llyfr, pennod a thudalennau eich hun',\n    'role_manage_page_templates' => 'Rheoli templedi tudalen',\n    'role_access_api' => 'Mynediad i Ryngwyneb Rhaglennu Cymwysiadau (API) system',\n    'role_manage_settings' => 'Rheoli gosodiadau apiau',\n    'role_export_content' => 'Cynnwys allforio',\n    'role_import_content' => 'Mewnforio Cynnwys',\n    'role_editor_change' => 'Newid golygydd tudalen',\n    'role_notifications' => 'Derbyn a rheoli hysbysiadau',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Caniatâd Asedau',\n    'roles_system_warning' => 'Byddwch yn ymwybodol y gall mynediad i unrhyw un o\\'r tri chaniatâd uchod ganiatáu i ddefnyddiwr newid eu breintiau eu hunain neu freintiau eraill yn y system. Neilltuo rolau gyda\\'r caniatâd hyn i ddefnyddwyr dibynadwy yn unig.',\n    'role_asset_desc' => 'Mae\\'r caniatâd hwn yn rheoli mynediad diofyn i\\'r asedau o fewn y system. Bydd caniatâd ar Lyfrau, Penodau a Thudalennau yn diystyru\\'r caniatâd hwn.',\n    'role_asset_admins' => 'Mae gweinyddwyr yn cael mynediad awtomatig i\\'r holl gynnwys ond gall yr opsiynau hyn ddangos neu guddio opsiynau UI.',\n    'role_asset_image_view_note' => 'Mae hyn yn ymwneud â gwelededd o fewn y rheolwr delweddau. Bydd mynediad gwirioneddol i ffeiliau delwedd wedi\\'u huwchlwytho yn dibynnu ar opsiwn storio delwedd y system.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'Popeth',\n    'role_own' => 'Meddu',\n    'role_controlled_by_asset' => 'Wedi\\'u rheoli gan yr ased y maent yn cael eu huwchlwytho iddo',\n    'role_save' => 'Cadw Rôl',\n    'role_users' => 'Defnyddwyr yn y rôl hon',\n    'role_users_none' => 'Nid oes unrhyw ddefnyddwyr wedi’u neilltuo i\\'r rôl hon ar hyn o bryd',\n\n    // Users\n    'users' => 'Defnyddwyr',\n    'users_index_desc' => 'Creu a rheoli cyfrifon defnyddwyr unigol o fewn y system. Defnyddir cyfrifon defnyddwyr ar gyfer mewngofnodi a phriodoli cynnwys a gweithgaredd. Mae caniatâd mynediad yn seiliedig ar rôl yn bennaf ond gall perchenogaeth cynnwys defnyddwyr, ymhlith ffactorau eraill, hefyd effeithio ar ganiatâd a mynediad.',\n    'user_profile' => 'Proffil Defnyddiwr',\n    'users_add_new' => 'Ychwanegu Defnyddiwr Newydd',\n    'users_search' => 'Chwilio Defnyddwyr',\n    'users_latest_activity' => 'Gweithgaredd Diweddaraf',\n    'users_details' => 'Manylion Defnyddiwr',\n    'users_details_desc' => 'Gosodwch enw arddangos a chyfeiriad e-bost ar gyfer y defnyddiwr hwn. Bydd y cyfeiriad e-bost yn cael ei ddefnyddio ar gyfer mewngofnodi i’r cais.',\n    'users_details_desc_no_email' => 'Gosodwch enw arddangos ar gyfer y defnyddiwr hwn fel y gall eraill eu hadnabod.',\n    'users_role' => 'Rolau Defnyddiwr',\n    'users_role_desc' => 'Dewiswch ba rolau a neilltuir i’r defnyddiwr hwn. Os yw defnyddiwr yn cael ei neilltuo i rolau lluosog, bydd y caniatâd o\\'r rolau hynny yn pentyrru a byddant yn derbyn pob gallu o\\'r rolau a neilltuwyd.',\n    'users_password' => 'Cyfrinair Defnyddiwr',\n    'users_password_desc' => 'Gosodwch gyfrinair a ddefnyddir i fewngofnodi i\\'r rhaglen. Rhaid i gyfrineiriau fod yn 8 nod o leiaf.',\n    'users_send_invite_text' => 'Gallwch ddewis anfon e-bost gwahoddiad i\\'r defnyddiwr hwn sy\\'n caniatáu iddynt osod eu cyfrinair eu hunain neu gallwch osod eu cyfrinair eich hun.',\n    'users_send_invite_option' => 'Anfon e-bost gwahodd i’r defnyddiwr',\n    'users_external_auth_id' => 'External Authentication ID',\n    'users_external_auth_id_desc' => 'Pan fydd system ddilysu allanol yn cael ei defnyddio (megis SAML2, OIDC neu LDAP) dyma\\'r ID sy\\'n cysylltu\\'r defnyddiwr BookStack hwn â\\'r cyfrif system ddilysu. Gallwch anwybyddu\\'r maes hwn os ydych chi\\'n defnyddio\\'r dilysiad diofyn e-bost.',\n    'users_password_warning' => 'Llenwch y canlynol os hoffech newid y cyfrinair ar gyfer y defnyddiwr hwn yn unig.',\n    'users_system_public' => 'Mae\\'r defnyddiwr hwn yn cynrychioli unrhyw westeion sy\\'n ymweld â\\'ch enghraifft. Ni ellir ei ddefnyddio i fewngofnodi ond mae\\'n cael ei aseinio\\'n awtomatig.',\n    'users_delete' => 'Dileu Defnyddiwr',\n    'users_delete_named' => 'Dileu defnyddiwr :userName',\n    'users_delete_warning' => 'Bydd hyn yn dileu\\'r defnyddiwr hwn yn llawn gyda\\'r enw \\':userName\\' o\\'r system.',\n    'users_delete_confirm' => 'Ydych chi\\'n siŵr eich bod eisiau dileu’r defnyddiwr hwn?',\n    'users_migrate_ownership' => 'Mudo Perchnogaeth',\n    'users_migrate_ownership_desc' => 'Dewiswch ddefnyddiwr yma os ydych chi am i ddefnyddiwr arall ddod yn berchennog ar yr holl eitemau sy\\'n eiddo i\\'r defnyddiwr hwn ar hyn o bryd.',\n    'users_none_selected' => 'Ni ddewiswyd defnyddiwr',\n    'users_edit' => 'Golygu Defnyddiwr',\n    'users_edit_profile' => 'Golygu Proffil',\n    'users_avatar' => 'Afatar Defnyddiwr',\n    'users_avatar_desc' => 'Dewiswch ddelwedd i gynrychioli\\'r defnyddiwr hwn. Dylai hwn fod yn ddelwedd sgwâr tua 256px.',\n    'users_preferred_language' => 'Dewis Iaith',\n    'users_preferred_language_desc' => 'Bydd yr opsiwn hwn yn newid yr iaith a ddefnyddir ar gyfer rhyngwyneb defnyddiwr y rhaglen. Ni fydd hyn yn effeithio ar unrhyw gynnwys a grëwyd gan y defnyddiwr.',\n    'users_social_accounts' => 'Cyfrifon Cymdeithasol',\n    'users_social_accounts_desc' => 'Gweld statws y cyfrifon cymdeithasol cysylltiedig ar gyfer y defnyddiwr hwn. Gellir defnyddio cyfrifon cymdeithasol yn ychwanegol at y system ddilysu sylfaenol ar gyfer mynediad system.',\n    'users_social_accounts_info' => 'Yma gallwch gysylltu eich cyfrifon eraill ar gyfer mewngofnodi cyflymach a haws. Nid yw datgysylltu cyfrif yma yn diddymu mynediad awdurdodedig blaenorol. Diddymu mynediad o\\'ch gosodiadau proffil ar y cyfrif cymdeithasol cysylltiedig.',\n    'users_social_connect' => 'Cysylltu Cyfrif',\n    'users_social_disconnect' => 'Datgysylltu Cyfrif',\n    'users_social_status_connected' => 'Wedi Cysylltu',\n    'users_social_status_disconnected' => 'Wedi Datgysylltu',\n    'users_social_connected' => 'Mae eich cyfrif :socialAccount wedi\\'i gysylltu\\'n llwyddiannus â\\'ch proffil.',\n    'users_social_disconnected' => 'Mae eich cyfrif :socialAccount wedi\\'i ddatgysylltu\\'n llwyddiannus o\\'ch proffil.',\n    'users_api_tokens' => 'Tocynnau API',\n    'users_api_tokens_desc' => 'Creu a rheoli\\'r tocynnau mynediad a ddefnyddir i ddilysu gyda\\'r API BookStack REST. Rheolir caniatâd ar gyfer yr API trwy\\'r defnyddiwr y mae\\'r tocyn yn perthyn iddo.',\n    'users_api_tokens_none' => 'Nid oes tocynnau API wedi\\'u creu ar gyfer y defnyddiwr hwn',\n    'users_api_tokens_create' => 'Creu Tocyn',\n    'users_api_tokens_expires' => 'Yn dod i ben',\n    'users_api_tokens_docs' => 'Dogfennaeth API',\n    'users_mfa' => 'Dilysu Aml-Ffactor',\n    'users_mfa_desc' => 'Gosod dilysu aml-ffactor fel haen ychwanegol o ddiogelwch ar gyfer eich cyfrif defnyddiwr.',\n    'users_mfa_x_methods' => ':count dull wedi\\'i ffurfweddu|:count dull wedi\\'u ffurfweddu',\n    'users_mfa_configure' => 'Ffurfweddu Dulliau',\n\n    // API Tokens\n    'user_api_token_create' => 'Creu Tocyn API',\n    'user_api_token_name' => 'Enw',\n    'user_api_token_name_desc' => 'Rhowch enw darllenadwy i\\'ch tocyn i\\'ch atgoffa o\\'i bwrpas arfaethedig yn y dyfodol.',\n    'user_api_token_expiry' => 'Dyddiad Dod i Ben',\n    'user_api_token_expiry_desc' => 'Gosodwch ddyddiad pan fydd y tocyn hwn yn dod i ben. Ar ôl y dyddiad hwn, ni fydd ceisiadau a wneir gan ddefnyddio\\'r tocyn hwn yn gweithio mwyach. Bydd gadael y maes hwn yn wag yn gosod dyddiad dod i ben 100 mlynedd i\\'r dyfodol.',\n    'user_api_token_create_secret_message' => 'Yn syth ar ôl creu\\'r tocyn hwn bydd \"ID tocyn\" a \"Chyfrinach Tocyn\" yn cael eu cynhyrchu a\\'u harddangos. Dim ond unwaith y bydd y gyfrinach yn cael ei dangos, felly gwnewch yn siŵr eich bod yn copïo\\'r gwerth i rywle diogel cyn bwrw ymlaen.',\n    'user_api_token' => 'Tocyn API',\n    'user_api_token_id' => 'ID Tocyn',\n    'user_api_token_id_desc' => 'Mae hwn yn ddynodwr a gynhyrchir gan y system na ellir ei olygu ar gyfer y tocyn hwn y bydd angen ei ddarparu mewn ceisiadau API.',\n    'user_api_token_secret' => 'Cyfrinach Tocyn',\n    'user_api_token_secret_desc' => 'Mae hwn yn gyfrinach a gynhyrchir gan y system ar gyfer y tocyn hwn y bydd angen ei ddarparu mewn ceisiadau API. Dim ond unwaith y bydd hyn yn cael ei arddangos, felly copïwch y gwerth hwn i rywle diogel.',\n    'user_api_token_created' => 'Crëwyd tocyn :timeAgo',\n    'user_api_token_updated' => 'Diweddarwyd tocyn :timeAgo',\n    'user_api_token_delete' => 'Dileu Tocyn',\n    'user_api_token_delete_warning' => 'Bydd hyn yn dileu\\'r tocyn API hwn yn llawn gyda\\'r enw \\':tokenName\\' o\\'r system.',\n    'user_api_token_delete_confirm' => 'Ydych chi\\'n siŵr eich bod eisiau dileu’r tocyn API hwn?',\n\n    // Webhooks\n    'webhooks' => 'Gwe-fachau',\n    'webhooks_index_desc' => 'Mae gwe-fachau’n ffordd o anfon data at URL allanol pan fydd rhai gweithredoedd a digwyddiadau yn digwydd o fewn y system sy\\'n caniatáu integreiddio ar sail digwyddiadau gyda llwyfannau allanol megis systemau negeseuon neu hysbysu.',\n    'webhooks_x_trigger_events' => ':count sbarduno digwyddiad|:count sbarduno digwyddiadau',\n    'webhooks_create' => 'Creu Gwe-fachyn Newydd',\n    'webhooks_none_created' => 'Nid oes unrhyw we-fachau wedi\\'u creu eto.',\n    'webhooks_edit' => 'Golygu Gwe-fachyn',\n    'webhooks_save' => 'Cadw Gwe-fachyn',\n    'webhooks_details' => 'Manylion Gwe-fachyn',\n    'webhooks_details_desc' => 'Darparu enw defnyddiwr cyfeillgar a chyfeiriad POST fel lleoliad ar gyfer anfon y data gwe-fachyn iddo.',\n    'webhooks_events' => 'Digwyddiadau Gwe-fachyn',\n    'webhooks_events_desc' => 'Dewiswch yr holl ddigwyddiadau a ddylai sbarduno\\'r gwe-fachyn hwn i gael ei alw.',\n    'webhooks_events_warning' => 'Cadwch mewn cof y bydd y digwyddiadau hyn yn cael eu sbarduno ar gyfer yr holl ddigwyddiadau a ddewiswyd, hyd yn oed os rhoddir caniatâd arferol ar waith. Gwnewch yn siŵr na fydd defnyddio\\'r gwe-fachyn hwn yn datgelu cynnwys cyfrinachol.',\n    'webhooks_events_all' => 'Holl ddigwyddiadau\\'r system',\n    'webhooks_name' => 'Enw Gwe-fachyn',\n    'webhooks_timeout' => 'Terfyn Amser Gwneud Cais Gwe-fachyn (Eiliadau)',\n    'webhooks_endpoint' => 'Pwynt Terfyn Gwe-fachyn',\n    'webhooks_active' => 'Gwe-fachyn Byw',\n    'webhook_events_table_header' => 'Digwyddiadau',\n    'webhooks_delete' => 'Dileu Gwe-fachyn',\n    'webhooks_delete_warning' => 'Bydd hyn yn dileu\\'r gwe-fachyn hwn yn llawn gyda\\'r enw \\':webhookName\\', o\\'r system.',\n    'webhooks_delete_confirm' => 'Ydych chi\\'n siŵr eich bod eisiau dileu’r gwe-fachyn hwn?',\n    'webhooks_format_example' => 'Enghraifft Fformat Gwe-fachyn',\n    'webhooks_format_example_desc' => 'Anfonir data Gwe-fachyn fel cais POST i\\'r pwynt terfyn wedi\\'i ffurfweddu fel JSON yn dilyn y fformat isod. Mae\\'r nodweddion \"related_item\" ac \"url\" yn ddewisol a byddant yn dibynnu ar y math o ddigwyddiad a sbardunir.',\n    'webhooks_status' => 'Statws Gwe-fachyn',\n    'webhooks_last_called' => 'Galwyd Ddiwethaf:',\n    'webhooks_last_errored' => 'Cafwyd Gwall Ddiwethaf:',\n    'webhooks_last_error_message' => 'Neges Wall Ddiwethaf:',\n\n    // Licensing\n    'licenses' => 'Trwyddedau',\n    'licenses_desc' => 'Mae\\'r dudalen hon yn manylu ar wybodaeth am drwydded ar gyfer BookStack yn ychwanegol at y prosiectau a’r llyfrgelloedd a ddefnyddir o fewn BookStack. Dim ond mewn cyd-destun datblygu y gellir defnyddio llawer o’r prosiectau a restrir.',\n    'licenses_bookstack' => 'Trwydded BookStack',\n    'licenses_php' => 'Trwyddedau Llyfrgell PHP',\n    'licenses_js' => 'Trwyddedau Llyfrgell JavaScript',\n    'licenses_other' => 'Trwyddedau Eraill',\n    'license_details' => 'Manylion y Drwydded',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/cy/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => 'Rhaid derbyn y :attribute.',\n    'active_url'           => 'Nid ywr :attribute yn URL dilys.',\n    'after'                => 'Rhaid i\\'r :attribute bod yn dyddiad ar ol :date.',\n    'alpha'                => 'Rhaid ir :attribute cynnwys llythrennau yn unig.',\n    'alpha_dash'           => 'Dim ond llythrennau, rhifau, llinellau toriad a thanlinellau y gall y :attribute gynnwys.',\n    'alpha_num'            => 'Rhaid ir :attribute cynnwys llythrennau a rhifau yn unig.',\n    'array'                => 'Rhaid i :attribute fod yn array.',\n    'backup_codes'         => 'Nid yw\\'r cod a ddarparwyd yn ddilys neu mae eisoes wedi\\'i ddefnyddio.',\n    'before'               => 'Rhaid i\\'r :attribute bod yn dyddiad cyn :date.',\n    'between'              => [\n        'numeric' => 'Rhaid i\\'r :attribute bod rhwng :min a :max.',\n        'file'    => 'Rhaid i\\'r :attribute bod rhwng :min a :max kilobytes.',\n        'string'  => 'Rhaid i\\'r :attribute bod rhwng :min a :max cymeriadau.',\n        'array'   => 'Rhaid i\\'r :attribute cael rhwng :min a :max o eitemau.',\n    ],\n    'boolean'              => 'Rhaid i :attribute fod yn wir neu ddim.',\n    'confirmed'            => 'Dydi\\'r cadarnhad :attribute ddim yn cydfynd.',\n    'date'                 => 'Nid yw\\'r :attribute yn dyddiad dilys.',\n    'date_format'          => 'Nid yw\\'r :attribute yn cydfynd ar format :format.',\n    'different'            => 'Rhaid i :attribute a :other bod yn wahanol.',\n    'digits'               => 'Rhai i\\'r :attribute bod yn :digits o ddigidau.',\n    'digits_between'       => 'Rhaid i\\'r :attribute bod rhwng :min a :max o digidau.',\n    'email'                => 'Rhaid i\\'r :attribute bod yn cyfeiriad e-bost dilys.',\n    'ends_with' => 'Rhaid i\\'r :attribute orffen gydag un o\\'r canlynol: :values',\n    'file'                 => 'Rhaid darparu\\'r :attribute fel ffeil ddilys.',\n    'filled'               => 'Mae angen llenwi\\'r maes :attribute.',\n    'gt'                   => [\n        'numeric' => 'Rhaid i\\'r :attribute fod yn fwy na :value.',\n        'file'    => 'Rhaid i\\'r :attribute fod yn fwy na :value kilobytes.',\n        'string'  => 'Rhaid i\\'r :attribute fod yn fwy na :value cymeriadau.',\n        'array'   => 'Rhaid i\\'r :attribute fod yn fwy na :value eitemau.',\n    ],\n    'gte'                  => [\n        'numeric' => 'Rhaid i’r :attribute fod yn fwy na, neu’n gyfartal â :value.',\n        'file'    => 'Rhaid i’r :attribute fod yn fwy na, neu’n gyfartal â :value cilobeit.',\n        'string'  => 'Rhaid i’r :attribute fod yn fwy na, neu’n gyfartal â :value nod.',\n        'array'   => 'Rhaid i’r :attribute fod â :value o eitemau neu fwy.',\n    ],\n    'exists'               => 'Mae\\'r dewis :attribute yn annilys.',\n    'image'                => 'Rhaid i’r :attribute fod yn ddelwedd.',\n    'image_extension'      => 'Rhaid i’r :attribute fod ag estyniad delwedd dilys & gefnogir.',\n    'in'                   => 'Mae\\'r dewis :attribute yn annilys.',\n    'integer'              => 'Rhaid i’r :attribute fod yn gyfanrif.',\n    'ip'                   => 'Rhaid i’r :attribute fod yn gyfeiriad IP dilys.',\n    'ipv4'                 => 'Rhaid i’r :attribute fod yn gyfeiriad IPv4 dilys.',\n    'ipv6'                 => 'Rhaid i’r :attribute fod yn gyfeiriad IPv6 dilys.',\n    'json'                 => 'Rhaid i’r :attribute fod yn llinyn JSON dilys.',\n    'lt'                   => [\n        'numeric' => 'Rhaid i’r :attribute fod yn llai na :value.',\n        'file'    => 'Rhaid i’r :attribute fod yn llai na :value cilobeit.',\n        'string'  => 'Rhaid i’r :attribute fod yn llai na :value nod.',\n        'array'   => 'Rhaid i’r :attribute fod â llai na :value o eitemau.',\n    ],\n    'lte'                  => [\n        'numeric' => 'Rhaid i’r :attribute fod yn llai na, neu’n gyfartal â :value.',\n        'file'    => 'Rhaid i’r :attribute fod yn llai na, neu’n gyfartal â :value cilobeit.',\n        'string'  => 'Rhaid i’r :attribute fod yn llai na, neu’n gyfartal â :value nod.',\n        'array'   => 'Ni ddylai’r :attribute fod â mwy na :value o eitemau.',\n    ],\n    'max'                  => [\n        'numeric' => 'Ni ddylai’r :attribute fod yn fwy na :max.',\n        'file'    => 'Ni ddylai’r :attribute fod yn fwy na :max cilobeit.',\n        'string'  => 'Ni ddylai’r :attribute fod yn fwy na :max nod.',\n        'array'   => 'Ni ddylai’r :attribute fod â mwy na :max o eitemau.',\n    ],\n    'mimes'                => 'Rhaid i’r :attribute fod yn ffeil o fath: :values.',\n    'min'                  => [\n        'numeric' => 'Rhaid i’r :attribute fod yn o leiaf :min.',\n        'file'    => 'Rhaid i’r :attribute fod yn o leiaf :min cilobeit.',\n        'string'  => 'Rhaid i’r :attribute fod yn o leiaf :min nod.',\n        'array'   => 'Rhaid i’r :attribute fod â llai na :min o eitemau.',\n    ],\n    'not_in'               => 'Mae\\'r dewis :attribute yn annilys.',\n    'not_regex'            => 'Mae’r fformat :attribute yn annilys.',\n    'numeric'              => 'Rhaid i’r :attribute fod yn rhif.',\n    'regex'                => 'Mae’r fformat :attribute yn annilys.',\n    'required'             => 'Mae :attribute yn faes gofynnol.',\n    'required_if'          => 'Mae :attribute yn faes gofynnol pan fo :other yn :value.',\n    'required_with'        => 'Mae :attribute yn faes gofynnol pan fo :values yn bresennol.',\n    'required_with_all'    => 'Mae :attribute yn faes gofynnol pan fo :values yn bresennol.',\n    'required_without'     => 'Mae :attitude yn faes gofynnol pan nad yw :values yn bresennol.',\n    'required_without_all' => 'Mae angen y maes :attribute os dydi\\'r un o :values yn bresennol.',\n    'same'                 => 'Mae’n rhaid i’r :attribute a :other gyd-fynd.',\n    'safe_url'             => 'Efallai na fydd y ddolen a ddarperir yn ddiogel.',\n    'size'                 => [\n        'numeric' => 'Rhaid i’r :attribute fod yn :size.',\n        'file'    => 'Rhaid i’r :attribute fod yn :size cilobeit.',\n        'string'  => 'Rhaid i’r :attribute fod yn :size nod.',\n        'array'   => 'Rhaid i’r :attribute gynnwys eitemau :size.',\n    ],\n    'string'               => 'Rhaid i’r :attribute fod yn llinyn.',\n    'timezone'             => 'Rhaid i’r :attribute fod yn barth dilys.',\n    'totp'                 => 'Nid yw\\'r cod a ddarperir yn ddilys neu mae wedi dod i ben.',\n    'unique'               => 'Mae’r :attribute eisoes wedi ei gymryd.',\n    'url'                  => 'Mae’r fformat :attribute yn annilys.',\n    'uploaded'             => 'Nid oedd modd uwchlwytho’r ffeil. Efallai na fydd y gweinydd yn derbyn ffeiliau o\\'r maint hwn.',\n\n    'zip_file' => 'Mae\\'r :attribute angen cyfeirio at ffeil yn y ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'Mae\\'r :attribute angen cyfeirio at ffeil o fath :valid Types, sydd wedi\\'i ffeindio :foundType.',\n    'zip_model_expected' => 'Dyswgyl am wrthrych data ond wedi ffeindio \":type\".',\n    'zip_unique' => 'Mae rhaid y :attribute fod yn unigol i\\'r fath o wrthrych yn y ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Rhaid cadarnhau cyfrinair',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/da/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'oprettede side',\n    'page_create_notification'    => 'Siden blev oprettet',\n    'page_update'                 => 'opdaterede side',\n    'page_update_notification'    => 'Siden blev opdateret',\n    'page_delete'                 => 'slettede side',\n    'page_delete_notification'    => 'Siden blev slettet',\n    'page_restore'                => 'gendannede side',\n    'page_restore_notification'   => 'Siden blev gendannet',\n    'page_move'                   => 'flyttede side',\n    'page_move_notification'      => 'Siden blev flyttet',\n\n    // Chapters\n    'chapter_create'              => 'oprettede kapitel',\n    'chapter_create_notification' => 'Kapitel blev oprettet',\n    'chapter_update'              => 'opdaterede kapitel',\n    'chapter_update_notification' => 'Kapitel blev opdateret',\n    'chapter_delete'              => 'slettede kapitel',\n    'chapter_delete_notification' => 'Kapitel blev slettet',\n    'chapter_move'                => 'flyttede kapitel',\n    'chapter_move_notification' => 'Kapitlet blev flyttet',\n\n    // Books\n    'book_create'                 => 'oprettede bog',\n    'book_create_notification'    => 'Bogen blev oprettet',\n    'book_create_from_chapter'              => 'omdannede kapitel til bog',\n    'book_create_from_chapter_notification' => 'Kapitel blev omdannet til en bog',\n    'book_update'                 => 'opdaterede bog',\n    'book_update_notification'    => 'Bogen blev opdateret',\n    'book_delete'                 => 'slettede bog',\n    'book_delete_notification'    => 'Bogen blev slettet',\n    'book_sort'                   => 'sorterede bogen',\n    'book_sort_notification'      => 'Bogen blev re-sorteret',\n\n    // Bookshelves\n    'bookshelf_create'            => 'oprettede reol',\n    'bookshelf_create_notification'    => 'Reolen blev oprettet',\n    'bookshelf_create_from_book'    => 'omdannede bog til reol',\n    'bookshelf_create_from_book_notification'    => 'Bogen blev omdannet til en bogreal',\n    'bookshelf_update'                 => 'opdaterede reolen',\n    'bookshelf_update_notification'    => 'Reolen blev opdateret',\n    'bookshelf_delete'                 => 'slettede reol',\n    'bookshelf_delete_notification'    => 'Reolen blev slettet',\n\n    // Revisions\n    'revision_restore' => 'gendannede version',\n    'revision_delete' => 'slettede version',\n    'revision_delete_notification' => 'Versionen blev slettet',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" er blevet tilføjet til dine favoritter',\n    'favourite_remove_notification' => '\":name\" er blevet fjernet fra dine favoritter',\n\n    // Watching\n    'watch_update_level_notification' => 'Opdatering af urets præferencer lykkedes',\n\n    // Auth\n    'auth_login' => 'loggede ind',\n    'auth_register' => 'registreret som ny bruger',\n    'auth_password_reset_request' => 'anmodet om nulstilling af brugeradgangskode',\n    'auth_password_reset_update' => 'nulstil adgangskode',\n    'mfa_setup_method' => 'konfigureret MFA metode',\n    'mfa_setup_method_notification' => 'Multi-faktor metode konfigureret',\n    'mfa_remove_method' => 'fjernet MFA metode',\n    'mfa_remove_method_notification' => 'Multi-faktor metode fjernet',\n\n    // Settings\n    'settings_update' => 'opdaterede indstillinger',\n    'settings_update_notification' => 'Indstillinger opdateret',\n    'maintenance_action_run' => 'kørte vedligeholdelsesaktion',\n\n    // Webhooks\n    'webhook_create' => 'oprettede webhook',\n    'webhook_create_notification' => 'Webhooken blev oprettet',\n    'webhook_update' => 'opdaterede webhooken',\n    'webhook_update_notification' => 'Webhooken blev opdateret',\n    'webhook_delete' => 'slettede webhooken',\n    'webhook_delete_notification' => 'Webhooken blev slettet',\n\n    // Imports\n    'import_create' => 'oprettet import',\n    'import_create_notification' => 'Importen er uploadet med succes',\n    'import_run' => 'opdateret import',\n    'import_run_notification' => 'Indhold importeret med succes',\n    'import_delete' => 'slettet import',\n    'import_delete_notification' => 'Import slettet med succes',\n\n    // Users\n    'user_create' => 'opret bruger',\n    'user_create_notification' => 'Bruger oprettet korrekt',\n    'user_update' => 'opdateret bruger',\n    'user_update_notification' => 'Brugeren blev opdateret',\n    'user_delete' => 'slettet bruger',\n    'user_delete_notification' => 'Brugeren blev fjernet',\n\n    // API Tokens\n    'api_token_create' => 'oprettet API token',\n    'api_token_create_notification' => 'API-token oprettet med succes',\n    'api_token_update' => 'opdateret API-token',\n    'api_token_update_notification' => 'API-token opdateret med succes',\n    'api_token_delete' => 'slettet API-token',\n    'api_token_delete_notification' => 'API-token slettet med succes',\n\n    // Roles\n    'role_create' => 'oprettet rolle',\n    'role_create_notification' => 'Rolle oprettet',\n    'role_update' => 'opdateret rolle',\n    'role_update_notification' => 'Rolle opdateret',\n    'role_delete' => 'slettet rolle',\n    'role_delete_notification' => 'Rollen blev slettet',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'tømt papirkurven',\n    'recycle_bin_restore' => 'gendannet fra papirkurven',\n    'recycle_bin_destroy' => 'fjernet fra papirkurven',\n\n    // Comments\n    'commented_on'                => 'kommenterede til',\n    'comment_create'              => 'tilføjet kommentar',\n    'comment_update'              => 'opdateret kommentar',\n    'comment_delete'              => 'slettet kommentar',\n\n    // Sort Rules\n    'sort_rule_create' => 'oprettet sorteringsregel',\n    'sort_rule_create_notification' => 'Sorteringsregel oprettet med succes',\n    'sort_rule_update' => 'opdateret sorteringsregel',\n    'sort_rule_update_notification' => 'Sorteringsregel opdateret med succes',\n    'sort_rule_delete' => 'slettet sorteringsregel',\n    'sort_rule_delete_notification' => 'Sorteringsregel slettet med succes',\n\n    // Other\n    'permissions_update'          => 'opdaterede tilladelser',\n];\n"
  },
  {
    "path": "lang/da/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'De indtastede brugeroplysninger stemmer ikke overens med vores registreringer.',\n    'throttle' => 'For mange mislykkede loginforsøg. Prøv igen om :seconds sekunder.',\n\n    // Login & Register\n    'sign_up' => 'Registrer',\n    'log_in' => 'Log ind',\n    'log_in_with' => 'Log ind med :socialDriver',\n    'sign_up_with' => 'Registrér med :socialDriver',\n    'logout' => 'Log ud',\n\n    'name' => 'Navn',\n    'username' => 'Brugernavn',\n    'email' => 'E-mail',\n    'password' => 'Adgangskode',\n    'password_confirm' => 'Bekræft adgangskode',\n    'password_hint' => 'Skal være på mindst 8 karakterer',\n    'forgot_password' => 'Glemt Adgangskode?',\n    'remember_me' => 'Husk mig',\n    'ldap_email_hint' => 'Angiv venligst din kontos e-mail.',\n    'create_account' => 'Opret konto',\n    'already_have_account' => 'Har du allerede en konto?',\n    'dont_have_account' => 'Har du ikke en konto?',\n    'social_login' => 'Social Log ind',\n    'social_registration' => 'Social Registrering',\n    'social_registration_text' => 'Registrér og log ind med anden service.',\n\n    'register_thanks' => 'Tak for registreringen!',\n    'register_confirm' => 'Check venligst din e-mail og klik deri på bekræftelses knappen for at tilgå :appName.',\n    'registrations_disabled' => 'Registrering er i øjeblikket deaktiveret',\n    'registration_email_domain_invalid' => 'E-Mail domænet har ikke adgang til denne applikation',\n    'register_success' => 'Tak for din registrering. Du er nu registeret og logget ind.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Forsøger Login',\n    'auto_init_starting_desc' => 'Vi kontakter dit godkendelsessystem for at starte loginprocessen. Hvis der ikke er nogen fremskridt efter 5 sekunder, kan du prøve at klikke på linket nedenfor.',\n    'auto_init_start_link' => 'Fortsæt med godkendelse',\n\n    // Password Reset\n    'reset_password' => 'Nulstil adgangskode',\n    'reset_password_send_instructions' => 'Indtast din e-mail herunder og du vil blive sendt en e-mail med et link til at nulstille din adgangskode.',\n    'reset_password_send_button' => 'Send link til nulstilling',\n    'reset_password_sent' => 'Et link til nulstilling af adgangskode sendes til :email, hvis den e-mail-adresse findes i systemet.',\n    'reset_password_success' => 'Din adgangskode er blevet nulstillet.',\n    'email_reset_subject' => 'Nulstil din :appName adgangskode',\n    'email_reset_text' => 'Du modtager denne e-mail fordi vi har modtaget en anmodning om at nulstille din adgangskode.',\n    'email_reset_not_requested' => 'Hvis du ikke har anmodet om at få din adgangskode nulstillet, behøver du ikke at foretage dig noget.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Bekræft din E-Mail på :appName',\n    'email_confirm_greeting' => 'Tak for at oprette dig på :appName!',\n    'email_confirm_text' => 'Bekræft venligst din e-mail adresse ved at klikke på linket nedenfor:',\n    'email_confirm_action' => 'Bekræft e-mail',\n    'email_confirm_send_error' => 'E-mailbekræftelse kræves, men systemet kunne ikke sende e-mailen. Kontakt administratoren for at sikre, at e-mail er konfigureret korrekt.',\n    'email_confirm_success' => 'Din e-mail er blevet bekræftet! Du bør nu kunne logge ind med denne e-mailadresse.',\n    'email_confirm_resent' => 'Bekræftelsesmail sendt, tjek venligst din indboks.',\n    'email_confirm_thanks' => 'Tak for bekræftelsen!',\n    'email_confirm_thanks_desc' => 'Vent venligst et øjeblik, mens din bekræftelse behandles. Hvis du ikke bliver omdirigeret efter 3 sekunder, skal du trykke på linket \"Fortsæt\" nedenfor for at fortsætte.',\n\n    'email_not_confirmed' => 'E-mailadresse ikke bekræftet',\n    'email_not_confirmed_text' => 'Din e-mailadresse er endnu ikke blevet bekræftet.',\n    'email_not_confirmed_click_link' => 'Klik venligst på linket i e-mailen der blev sendt kort efter du registrerede dig.',\n    'email_not_confirmed_resend' => 'Hvis du ikke kan finde E-Mailen, kan du du få gensendt bekræftelsesemailen ved at trykke herunder.',\n    'email_not_confirmed_resend_button' => 'Gensend bekræftelsesemail',\n\n    // User Invite\n    'user_invite_email_subject' => 'Du er blevet inviteret til :appName!',\n    'user_invite_email_greeting' => 'En konto er blevet oprettet til dig på :appName.',\n    'user_invite_email_text' => 'Klik på knappen nedenunderm for at sætte en adgangskode og opnå adgang:',\n    'user_invite_email_action' => 'Set adgangskode',\n    'user_invite_page_welcome' => 'Velkommen til :appName!',\n    'user_invite_page_text' => 'For at færdiggøre din konto og få adgang skal du indstille en adgangskode, der bruges til at logge ind på :appName ved fremtidige besøg.',\n    'user_invite_page_confirm_button' => 'Bekræft adgangskode',\n    'user_invite_success_login' => 'Adgangskoden er sat. Du burde nu kunne logge ind med din angivede adgangskode for at tilgå :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Opsætning af Multi-faktor godkendelse',\n    'mfa_setup_desc' => 'Opsæt multi-faktor godkendelse som et ekstra lag af sikkerhed for din brugerkonto.',\n    'mfa_setup_configured' => 'Allerede konfigureret',\n    'mfa_setup_reconfigure' => 'Genkonfigurer',\n    'mfa_setup_remove_confirmation' => 'Er du sikker på, at du vil fjerne denne multi-faktor godkendelsesmetode?',\n    'mfa_setup_action' => 'Opsætning',\n    'mfa_backup_codes_usage_limit_warning' => 'Du har mindre end 5 backup koder tilbage, generere og gem et nyt sæt før du løber tør for koder, for at forhindre at blive lukket ude af din konto.',\n    'mfa_option_totp_title' => 'Mobil app',\n    'mfa_option_totp_desc' => 'For at bruge multi-faktor godkendelse, skal du bruge en mobil app, der understøtter TOTP såsom Google Authenticator, Authy eller Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Backup koder',\n    'mfa_option_backup_codes_desc' => 'Genererer et sæt backup-koder til engangsbrug, som du skal indtaste ved login for at bekræfte din identitet. Sørg for at opbevare dem et sikkert sted.',\n    'mfa_gen_confirm_and_enable' => 'Bekræft og aktivér',\n    'mfa_gen_backup_codes_title' => 'Backup koder opsætning',\n    'mfa_gen_backup_codes_desc' => 'Opbevar nedenstående liste med koder et sikkert sted. Når du får adgang til systemet, kan du bruge en af koderne som en ekstra godkendelsesmekanisme.',\n    'mfa_gen_backup_codes_download' => 'Download koder',\n    'mfa_gen_backup_codes_usage_warning' => 'Hver kode kan kun bruges en gang',\n    'mfa_gen_totp_title' => 'Mobil App Setup',\n    'mfa_gen_totp_desc' => 'For at bruge multifaktorgodkendelse skal du bruge en mobilapplikation, der understøtter TOTP, f.eks. Google Authenticator, Authy eller Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Scan QR-koden nedenfor med din foretrukne godkendelsesapp for at komme i gang.',\n    'mfa_gen_totp_verify_setup' => 'Verificer Opsætning',\n    'mfa_gen_totp_verify_setup_desc' => 'Kontrollér, at alt fungerer, ved at indtaste en kode, der er genereret i din godkendelsesapp, i indtastningsfeltet nedenfor:',\n    'mfa_gen_totp_provide_code_here' => 'Angiv din app-genererede kode her',\n    'mfa_verify_access' => 'Bekræft Adgang',\n    'mfa_verify_access_desc' => 'Din brugerkonto kræver, at du bekræfter din identitet via et ekstra verifikationsniveau, før du får adgang. Bekræft ved hjælp af en af dine konfigurerede metoder for at fortsætte.',\n    'mfa_verify_no_methods' => 'Ingen Metoder Konfigureret',\n    'mfa_verify_no_methods_desc' => 'Der blev ikke fundet nogen metoder til multifaktorgodkendelse til din konto. Du skal konfigurere mindst én metode, før du får adgang.',\n    'mfa_verify_use_totp' => 'Bekræft ved brug af en mobil app',\n    'mfa_verify_use_backup_codes' => 'Bekræft ved hjælp af en backup kode',\n    'mfa_verify_backup_code' => 'Backup Kode',\n    'mfa_verify_backup_code_desc' => 'Indtast en af dine resterende backup koder nedenfor:',\n    'mfa_verify_backup_code_enter_here' => 'Indtast backup kode her',\n    'mfa_verify_totp_desc' => 'Indtast koden, der er genereret med din mobilapp, nedenfor:',\n    'mfa_setup_login_notification' => 'Multifaktormetode konfigureret, log venligst ind igen med den konfigurerede metode.',\n];\n"
  },
  {
    "path": "lang/da/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Annuller',\n    'close' => 'Luk',\n    'confirm' => 'Bekræft',\n    'back' => 'Tilbage',\n    'save' => 'Gem',\n    'continue' => 'Fortsæt',\n    'select' => 'Vælg',\n    'toggle_all' => 'Vælg/Fravælg alle',\n    'more' => 'Mere',\n\n    // Form Labels\n    'name' => 'Navn',\n    'description' => 'Beskrivelse',\n    'role' => 'Rolle',\n    'cover_image' => 'Coverbillede',\n    'cover_image_description' => 'Dette billede skal være omkring 440x250px selvom det vil være fleksibelt skaleret & beskåret for at passe til brugergrænsefladen i forskellige scenarier efter behov. Så de faktiske dimensioner for visning vil være forskellige.',\n\n    // Actions\n    'actions' => 'Handlinger',\n    'view' => 'Vis',\n    'view_all' => 'Vis alle',\n    'new' => 'Ny',\n    'create' => 'Opret',\n    'update' => 'Opdater',\n    'edit' => 'Rediger',\n    'archive' => 'Arkiver',\n    'unarchive' => 'Tilbagekald',\n    'sort' => 'Sorter',\n    'move' => 'Flyt',\n    'copy' => 'Kopier',\n    'reply' => 'Besvar',\n    'delete' => 'Slet',\n    'delete_confirm' => 'Bekræft sletning',\n    'search' => 'Søg',\n    'search_clear' => 'Ryd søgning',\n    'reset' => 'Nulstil',\n    'remove' => 'Fjern',\n    'add' => 'Tilføj',\n    'configure' => 'Konfigurer',\n    'manage' => 'Administrer',\n    'fullscreen' => 'Fuld skærm',\n    'favourite' => 'Foretrukken',\n    'unfavourite' => 'Fjern som foretrukken',\n    'next' => 'Næste',\n    'previous' => 'Forrige',\n    'filter_active' => 'Aktivt filter:',\n    'filter_clear' => 'Nulstil filter',\n    'download' => 'Hent',\n    'open_in_tab' => 'Åben i ny fane',\n    'open' => 'Åbn',\n\n    // Sort Options\n    'sort_options' => 'Sorteringsindstillinger',\n    'sort_direction_toggle' => 'Sorteringsretning',\n    'sort_ascending' => 'Sorter stigende',\n    'sort_descending' => 'Sorter faldende',\n    'sort_name' => 'Navn',\n    'sort_default' => 'Standard',\n    'sort_created_at' => 'Oprettelsesdato',\n    'sort_updated_at' => 'Opdateringsdato',\n\n    // Misc\n    'deleted_user' => 'Slettet bruger',\n    'no_activity' => 'Ingen aktivitet at vise',\n    'no_items' => 'Intet indhold tilgængeligt',\n    'back_to_top' => 'Tilbage til toppen',\n    'skip_to_main_content' => 'Spring til indhold',\n    'toggle_details' => 'Vis/skjul detaljer',\n    'toggle_thumbnails' => 'Vis/skjul miniaturer',\n    'details' => 'Detaljer',\n    'grid_view' => 'Gittervisning',\n    'list_view' => 'Listevisning',\n    'default' => 'Standard',\n    'breadcrumb' => 'Brødkrumme',\n    'status' => 'Status',\n    'status_active' => 'Aktiv',\n    'status_inactive' => 'Inaktiv',\n    'never' => 'Aldrig',\n    'none' => 'Ingen',\n\n    // Header\n    'homepage' => 'Forside',\n    'header_menu_expand' => 'Udvid header menu',\n    'profile_menu' => 'Profilmenu',\n    'view_profile' => 'Vis profil',\n    'edit_profile' => 'Redigér Profil',\n    'dark_mode' => 'Mørk tilstand',\n    'light_mode' => 'Lys tilstand',\n    'global_search' => 'Global søgning',\n\n    // Layout tabs\n    'tab_info' => 'Info',\n    'tab_info_label' => 'Faneblad: Vis sekundær information',\n    'tab_content' => 'Indhold',\n    'tab_content_label' => 'Faneblad: Vis primær indhold',\n\n    // Email Content\n    'email_action_help' => 'Hvis du har problemer med at trykke på \":actionText\" knappen, prøv at kopiere og indsætte linket herunder ind i din webbrowser:',\n    'email_rights' => 'Alle rettigheder forbeholdes',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Privatlivspolitik',\n    'terms_of_service' => 'Tjenestevilkår',\n\n    // OpenSearch\n    'opensearch_description' => 'Søg :appName',\n];\n"
  },
  {
    "path": "lang/da/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Billedselektion',\n    'image_list' => 'Billedliste',\n    'image_details' => 'Billeddetaljer',\n    'image_upload' => 'Upload billede',\n    'image_intro' => 'Her kan du vælge og administrere billeder, der tidligere er blevet uploadet til systemet.',\n    'image_intro_upload' => 'Upload et nyt billede ved at trække en billedfil ind i dette vindue, eller ved at bruge knappen \"Upload billede\" ovenfor.',\n    'image_all' => 'Alt',\n    'image_all_title' => 'Se alle billeder',\n    'image_book_title' => 'Vis billeder uploadet til denne bog',\n    'image_page_title' => 'Vis billeder uploadet til denne side',\n    'image_search_hint' => 'Søg efter billednavn',\n    'image_uploaded' => 'Uploadet :uploadedDate',\n    'image_uploaded_by' => 'Uploadet af :userName',\n    'image_uploaded_to' => 'Uploadet til :pageLink',\n    'image_updated' => 'Opdateret :updateDate',\n    'image_load_more' => 'Indlæse mere',\n    'image_image_name' => 'Billednavn',\n    'image_delete_used' => 'Dette billede er brugt på siderne nedenfor.',\n    'image_delete_confirm_text' => 'Er du sikker på at du vil slette dette billede?',\n    'image_select_image' => 'Vælg billede',\n    'image_dropzone' => 'Træk-og-slip billede eller klik her for at uploade',\n    'image_dropzone_drop' => 'Slip billeder her for at uploade',\n    'images_deleted' => 'Billede slettet',\n    'image_preview' => 'Billedeksempel',\n    'image_upload_success' => 'Foto uploadet',\n    'image_update_success' => 'Billeddetaljer succesfuldt opdateret',\n    'image_delete_success' => 'Billede slettet',\n    'image_replace' => 'Erstat billede',\n    'image_replace_success' => 'Billedfil blev opdateret',\n    'image_rebuild_thumbs' => 'Regenerer størrelsesvariationer',\n    'image_rebuild_thumbs_success' => 'Variationer i billedstørrelse blev genopbygget!',\n\n    // Code Editor\n    'code_editor' => 'Rediger kode',\n    'code_language' => 'Kodesprog',\n    'code_content' => 'Kodeindhold',\n    'code_session_history' => 'Sessionshistorik',\n    'code_save' => 'Gem kode',\n];\n"
  },
  {
    "path": "lang/da/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Generel',\n    'advanced' => 'Avanceret',\n    'none' => 'Ingen',\n    'cancel' => 'Annuller',\n    'save' => 'Gem',\n    'close' => 'Luk',\n    'apply' => 'Anvend',\n    'undo' => 'Fortryd',\n    'redo' => 'Gendan',\n    'left' => 'Venstre',\n    'center' => 'Midten',\n    'right' => 'Højre',\n    'top' => 'Top',\n    'middle' => 'Midt',\n    'bottom' => 'Bund',\n    'width' => 'Bredde',\n    'height' => 'Højde',\n    'More' => 'Mere',\n    'select' => 'Vælg...',\n\n    // Toolbar\n    'formats' => 'Formater',\n    'header_large' => 'Stor Overskrift',\n    'header_medium' => 'Mellemstor Overskrift',\n    'header_small' => 'Lille Overskrift',\n    'header_tiny' => 'Lille Header',\n    'paragraph' => 'Paragraf',\n    'blockquote' => 'Citat',\n    'inline_code' => 'Inline kode',\n    'callouts' => 'Callouts',\n    'callout_information' => 'Information',\n    'callout_success' => 'Succes',\n    'callout_warning' => 'Advarsel',\n    'callout_danger' => 'Fare',\n    'bold' => 'Fed',\n    'italic' => 'Kursiv',\n    'underline' => 'Understreget',\n    'strikethrough' => 'Gennemstreget',\n    'superscript' => 'Hævet',\n    'subscript' => 'Sænket',\n    'text_color' => 'Tekstfarve',\n    'highlight_color' => 'Fremhævelsesfarve',\n    'custom_color' => 'Tilpasset farve',\n    'remove_color' => 'Fjern farve',\n    'background_color' => 'Baggrundsfarve',\n    'align_left' => 'Venstrejusteret',\n    'align_center' => 'Centrér',\n    'align_right' => 'Højrejusteret',\n    'align_justify' => 'Juster',\n    'list_bullet' => 'Punktliste',\n    'list_numbered' => 'Nummereret liste',\n    'list_task' => 'Opgaveliste',\n    'indent_increase' => 'Forøg indrykning',\n    'indent_decrease' => 'Formindsk indrykning',\n    'table' => 'Tabel',\n    'insert_image' => 'Indsæt billede',\n    'insert_image_title' => 'Indsæt/rediger billede',\n    'insert_link' => 'Indsæt/rediger link',\n    'insert_link_title' => 'Indsæt/Rediger Link',\n    'insert_horizontal_line' => 'Indsæt vandret linje',\n    'insert_code_block' => 'Indsæt kodeblok',\n    'edit_code_block' => 'Rediger kodeblok',\n    'insert_drawing' => 'Indsæt/rediger tegning',\n    'drawing_manager' => 'Tegningsansvarlig',\n    'insert_media' => 'Indsæt/rediger medie',\n    'insert_media_title' => 'Indsæt/Rediger Medie',\n    'clear_formatting' => 'Ryd formatering',\n    'source_code' => 'Kildekode',\n    'source_code_title' => 'Kildekode',\n    'fullscreen' => 'Fuld skærm',\n    'image_options' => 'Billedindstillinger',\n\n    // Tables\n    'table_properties' => 'Tabelegenskaber',\n    'table_properties_title' => 'Tabelegenskaber',\n    'delete_table' => 'Slet tabel',\n    'table_clear_formatting' => 'Ryd tabelformatering',\n    'resize_to_contents' => 'Ændre størrelse til indhold',\n    'row_header' => 'Rækkeoverskrift',\n    'insert_row_before' => 'Indsæt række før',\n    'insert_row_after' => 'Indsæt række efter',\n    'delete_row' => 'Slet række',\n    'insert_column_before' => 'Indsæt kolonne før',\n    'insert_column_after' => 'Indsæt kolonne efter',\n    'delete_column' => 'Slet kolonne',\n    'table_cell' => 'Celle',\n    'table_row' => 'Række',\n    'table_column' => 'Kolonne',\n    'cell_properties' => 'Celle egenskaber',\n    'cell_properties_title' => 'Celle Egenskaber',\n    'cell_type' => 'Celle type',\n    'cell_type_cell' => 'Celle',\n    'cell_scope' => 'Omfang',\n    'cell_type_header' => 'Overskriftscelle',\n    'merge_cells' => 'Flet celler',\n    'split_cell' => 'Opdel celle',\n    'table_row_group' => 'Række Gruppe',\n    'table_column_group' => 'Kolonne Gruppe',\n    'horizontal_align' => 'Juster vandret',\n    'vertical_align' => 'Juster lodret',\n    'border_width' => 'Kantbredde',\n    'border_style' => 'Kantstil',\n    'border_color' => 'Kantfarve',\n    'row_properties' => 'Række egenskaber',\n    'row_properties_title' => 'Række Egenskaber',\n    'cut_row' => 'Klip række',\n    'copy_row' => 'Kopier række',\n    'paste_row_before' => 'Indsæt række før',\n    'paste_row_after' => 'Indsæt række efter',\n    'row_type' => 'Række type',\n    'row_type_header' => 'Overskrift',\n    'row_type_body' => 'Krop',\n    'row_type_footer' => 'Sidefod',\n    'alignment' => 'Justering',\n    'cut_column' => 'Klip kolonne',\n    'copy_column' => 'Kopier kolonne',\n    'paste_column_before' => 'Indsæt kolonne før',\n    'paste_column_after' => 'Indsæt kolonne efter',\n    'cell_padding' => 'Celle margen',\n    'cell_spacing' => 'Celle afstand',\n    'caption' => 'Citat',\n    'show_caption' => 'Vis citat',\n    'constrain' => 'Begræns proportioner',\n    'cell_border_solid' => 'Solid',\n    'cell_border_dotted' => 'Prikket',\n    'cell_border_dashed' => 'Stiplet',\n    'cell_border_double' => 'Dobbelt',\n    'cell_border_groove' => 'Udfræsning',\n    'cell_border_ridge' => 'Kant',\n    'cell_border_inset' => 'Indsat',\n    'cell_border_outset' => 'Outset',\n    'cell_border_none' => 'Ingen',\n    'cell_border_hidden' => 'Gemt',\n\n    // Images, links, details/summary & embed\n    'source' => 'Kilde',\n    'alt_desc' => 'Alternativ beskrivelse',\n    'embed' => 'Indlejre',\n    'paste_embed' => 'Indsæt din indlejringskode nedenfor:',\n    'url' => 'URL',\n    'text_to_display' => 'Tekst til visning',\n    'title' => 'Titel',\n    'browse_links' => 'Gennemse links',\n    'open_link' => 'Åbn link',\n    'open_link_in' => 'Åbn link i...',\n    'open_link_current' => 'Nuværende vindue',\n    'open_link_new' => 'Nyt vindue',\n    'remove_link' => 'Fjern link',\n    'insert_collapsible' => 'Indsæt sammenklappelig blok',\n    'collapsible_unwrap' => 'Pak ud',\n    'edit_label' => 'Rediger mærkat',\n    'toggle_open_closed' => 'Skift mellem åben og lukket',\n    'collapsible_edit' => 'Rediger sammenklappelig blok',\n    'toggle_label' => 'Skift etiket',\n\n    // About view\n    'about' => 'Om redaktøren',\n    'about_title' => 'Om WYSIWYG-editoren',\n    'editor_license' => 'Editor-licens og copyright',\n    'editor_lexical_license' => 'Denne editor er bygget som en forgrening af :lexicalLink, som distribueres under MIT-licensen.',\n    'editor_lexical_license_link' => 'Du kan finde alle licensoplysninger her.',\n    'editor_tiny_license' => 'Denne editor er bygget ved hjælp af :tinyLink, som leveres under MIT-licensen.',\n    'editor_tiny_license_link' => 'Oplysninger om copyright og licens for TinyMCE kan findes her.',\n    'save_continue' => 'Gem og fortsæt',\n    'callouts_cycle' => '(Bliv ved med at trykke for at skifte mellem typerne)',\n    'link_selector' => 'Link til indhold',\n    'shortcuts' => 'Genveje',\n    'shortcut' => 'Genvej',\n    'shortcuts_intro' => 'Følgende genveje er tilgængelige i editoren:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Beskrivelse',\n];\n"
  },
  {
    "path": "lang/da/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Nyligt oprettet',\n    'recently_created_pages' => 'Nyligt oprettede sider',\n    'recently_updated_pages' => 'Nyligt opdaterede sider',\n    'recently_created_chapters' => 'Nyligt oprettede kapitler',\n    'recently_created_books' => 'Nyligt oprettede bøger',\n    'recently_created_shelves' => 'Nyligt oprettede reoler',\n    'recently_update' => 'Opdateret for nyligt',\n    'recently_viewed' => 'Senest viste',\n    'recent_activity' => 'Seneste aktivitet',\n    'create_now' => 'Opret en nu',\n    'revisions' => 'Revisioner',\n    'meta_revision' => 'Revision #:revisionCount',\n    'meta_created' => 'Oprettet :timeLength',\n    'meta_created_name' => 'Oprettet :timeLength af :user',\n    'meta_updated' => 'Opdateret :timeLength',\n    'meta_updated_name' => 'Opdateret :timeLength af :user',\n    'meta_owned_name' => 'Ejet af :user',\n    'meta_reference_count' => 'Refereret af :count item|Refereret af :count items',\n    'entity_select' => 'Vælg emne',\n    'entity_select_lack_permission' => 'Du har ikke de nødvendige tilladelser til at vælge dette element',\n    'images' => 'Billeder',\n    'my_recent_drafts' => 'Mine seneste kladder',\n    'my_recently_viewed' => 'Mine senest viste',\n    'my_most_viewed_favourites' => 'Mine mest viste favoritter',\n    'my_favourites' => 'Mine favoritter',\n    'no_pages_viewed' => 'Du har ikke besøgt nogle sider',\n    'no_pages_recently_created' => 'Ingen sider er blevet oprettet for nyligt',\n    'no_pages_recently_updated' => 'Ingen sider er blevet opdateret for nyligt',\n    'export' => 'Eksporter',\n    'export_html' => 'Indeholdt webfil',\n    'export_pdf' => 'PDF-fil',\n    'export_text' => 'Almindelig tekstfil',\n    'export_md' => 'Markdown Fil',\n    'export_zip' => 'Bærbar ZIP',\n    'default_template' => 'Standard sideskabelon',\n    'default_template_explain' => 'Tildel en sideskabelon, der vil blive brugt som standardindhold for alle sider, der oprettes i dette element. Husk, at dette kun vil blive brugt, hvis sideskaberen har adgang til den valgte skabelonside.',\n    'default_template_select' => 'Vælg en skabelonside',\n    'import' => 'Importer',\n    'import_validate' => 'Valider import',\n    'import_desc' => 'Importer bøger, kapitler og sider ved hjælp af en bærbar zip-eksport fra den samme eller en anden instans. Vælg en ZIP-fil for at fortsætte. Når filen er blevet uploadet og valideret, kan du konfigurere og bekræfte importen i den næste visning.',\n    'import_zip_select' => 'Vælg den ZIP-fil, der skal uploades',\n    'import_zip_validation_errors' => 'Der blev opdaget fejl under validering af den medfølgende ZIP-fil:',\n    'import_pending' => 'Afventende import',\n    'import_pending_none' => 'Der er ikke startet nogen import.',\n    'import_continue' => 'Fortsæt med at importere',\n    'import_continue_desc' => 'Gennemgå det indhold, der skal importeres fra den uploadede ZIP-fil. Når du er klar, skal du køre importen for at tilføje dens indhold til dette system. Den uploadede ZIP-importfil fjernes automatisk, når importen er vellykket.',\n    'import_details' => 'Import detaljer',\n    'import_run' => 'Kør import',\n    'import_size' => ':size Importer ZIP-størrelse',\n    'import_uploaded_at' => 'Uploadet :relativeTime',\n    'import_uploaded_by' => 'Uploadet af',\n    'import_location' => 'Importplacering',\n    'import_location_desc' => 'Vælg en målplacering for dit importerede indhold. Du skal have de relevante tilladelser til at oprette på den valgte placering.',\n    'import_delete_confirm' => 'Er du sikker på, at du vil slette denne import?',\n    'import_delete_desc' => 'Dette vil slette den uploadede ZIP-fil og kan ikke fortrydes.',\n    'import_errors' => 'Importfejl',\n    'import_errors_desc' => 'Følgende fejl opstod under importforsøget:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Rettigheder',\n    'permissions_desc' => 'Angiv tilladelser her for at tilsidesætte de standardtilladelser, der gives af brugerroller.',\n    'permissions_book_cascade' => 'Tilladelser, der er indstillet på bøger, vil automatisk blive overført til underordnede kapitler og sider, medmindre de har deres egne tilladelser defineret.',\n    'permissions_chapter_cascade' => 'Tilladelser, der er indstillet på kapitler, overføres automatisk til underordnede sider, medmindre de har deres egne tilladelser defineret.',\n    'permissions_save' => 'Gem tilladelser',\n    'permissions_owner' => 'Ejer',\n    'permissions_role_everyone_else' => 'Alle andre',\n    'permissions_role_everyone_else_desc' => 'Indstil tilladelser for alle roller, der ikke specifikt er tilsidesat.',\n    'permissions_role_override' => 'Tilsidesæt tilladelser for rolle',\n    'permissions_inherit_defaults' => 'Arv standardindstillinger',\n\n    // Search\n    'search_results' => 'Søgeresultater',\n    'search_total_results_found' => ':count resultat fundet|:count resultater fundet',\n    'search_clear' => 'Ryd søgning',\n    'search_no_pages' => 'Ingen sider matchede søgning',\n    'search_for_term' => 'Søgning for :term',\n    'search_more' => 'Flere resultater',\n    'search_advanced' => 'Avanceret søgning',\n    'search_terms' => 'Søgeord',\n    'search_content_type' => 'Indholdstype',\n    'search_exact_matches' => 'Nøjagtige matches',\n    'search_tags' => 'Tagsøgninger',\n    'search_options' => 'Indstillinger',\n    'search_viewed_by_me' => 'Set af mig',\n    'search_not_viewed_by_me' => 'Ikke set af mig',\n    'search_permissions_set' => 'Rettigheders sæt',\n    'search_created_by_me' => 'Oprettet af mig',\n    'search_updated_by_me' => 'Opdateret af mig',\n    'search_owned_by_me' => 'Ejet af mig',\n    'search_date_options' => 'Datoindstillinger',\n    'search_updated_before' => 'Opdateret før',\n    'search_updated_after' => 'Opdateret efter',\n    'search_created_before' => 'Oprettet før',\n    'search_created_after' => 'Oprettet efter',\n    'search_set_date' => 'Sæt dato',\n    'search_update' => 'Opdatér søgning',\n\n    // Shelves\n    'shelf' => 'Reol',\n    'shelves' => 'Reoler',\n    'x_shelves' => ':count reol|:count reoler',\n    'shelves_empty' => 'Ingen reoler er blevet oprettet',\n    'shelves_create' => 'Opret ny reol',\n    'shelves_popular' => 'Populære reoler',\n    'shelves_new' => 'Nye reoler',\n    'shelves_new_action' => 'Ny reol',\n    'shelves_popular_empty' => 'De mest populære reoler vil blive vist her.',\n    'shelves_new_empty' => 'De nyeste reoler vil blive vist her.',\n    'shelves_save' => 'Gem reol',\n    'shelves_books' => 'Bøger på denne reol',\n    'shelves_add_books' => 'Tilføj bøger til denne reol',\n    'shelves_drag_books' => 'Træk bøger nedenfor for at tilføje dem til denne hylde',\n    'shelves_empty_contents' => 'Denne reol har ingen bøger tilknyttet til den',\n    'shelves_edit_and_assign' => 'Rediger reol for at tilføje bøger',\n    'shelves_edit_named' => 'Rediger hylde :navn',\n    'shelves_edit' => 'Rediger Reol',\n    'shelves_delete' => 'Slettede reoler',\n    'shelves_delete_named' => 'Slet hylde :navn',\n    'shelves_delete_explain' => \"Dette vil slette hylden med navnet ':navn'. Indeholdte bøger vil ikke blive slettet.\",\n    'shelves_delete_confirmation' => 'Er du sikker på, at du vil slette denne hylde?',\n    'shelves_permissions' => 'Tilladelser til hylder',\n    'shelves_permissions_updated' => 'Tilladelser til hylder opdateret',\n    'shelves_permissions_active' => 'Hyldetilladelser aktive',\n    'shelves_permissions_cascade_warning' => 'Tilladelser på hylder overføres ikke automatisk til indeholdte bøger. Det skyldes, at en bog kan findes på flere hylder. Tilladelser kan dog kopieres ned til underordnede bøger ved hjælp af nedenstående mulighed.',\n    'shelves_permissions_create' => 'Hyldeoprettelsestilladelser bruges kun til at kopiere tilladelser til underordnede bøger ved hjælp af nedenstående handling. De kontrollerer ikke muligheden for at oprette bøger.',\n    'shelves_copy_permissions_to_books' => 'Kopier tilladelser til bøger',\n    'shelves_copy_permissions' => 'Kopier tilladelser',\n    'shelves_copy_permissions_explain' => 'Dette vil anvende de aktuelle tilladelsesindstillinger for denne hylde på alle bøger, der er indeholdt i den. Før du aktiverer, skal du sikre dig, at eventuelle ændringer af tilladelserne for denne hylde er blevet gemt.',\n    'shelves_copy_permission_success' => 'Hyldetilladelser kopieret til :count books',\n\n    // Books\n    'book' => 'Bog',\n    'books' => 'Bøger',\n    'x_books' => ':count bog|:count bøger',\n    'books_empty' => 'Ingen bøger er blevet oprettet',\n    'books_popular' => 'Populære bøger',\n    'books_recent' => 'Nylige bøger',\n    'books_new' => 'Nye bøger',\n    'books_new_action' => 'Ny bog',\n    'books_popular_empty' => 'De mest populære bøger vil blive vist her.',\n    'books_new_empty' => 'De nyeste boger vil blive vist her.',\n    'books_create' => 'Lav en ny bog',\n    'books_delete' => 'Slet bog',\n    'books_delete_named' => 'Slet bog :bookName',\n    'books_delete_explain' => 'Dette vil slette bogen ved navn \\':bookName\\'. Alle sider og kapitler vil blive slettet.',\n    'books_delete_confirmation' => 'Er du sikker på at du vil slette denne bog?',\n    'books_edit' => 'Rediger bog',\n    'books_edit_named' => 'Rediger bog :bookName',\n    'books_form_book_name' => 'Bognavn',\n    'books_save' => 'Gem bog',\n    'books_permissions' => 'Bogtilladelser',\n    'books_permissions_updated' => 'Bogtilladelser opdateret',\n    'books_empty_contents' => 'Ingen sider eller kapitler er blevet oprettet i denne bog.',\n    'books_empty_create_page' => 'Opret en ny side',\n    'books_empty_sort_current_book' => 'Sortér denne bog',\n    'books_empty_add_chapter' => 'Tilføj et kapitel',\n    'books_permissions_active' => 'Aktive bogtilladelser',\n    'books_search_this' => 'Søg i denne bog',\n    'books_navigation' => 'Bognavigation',\n    'books_sort' => 'Sorter bogindhold',\n    'books_sort_desc' => 'Flyt kapitler og sider i en bog for at omorganisere dens indhold. Der kan tilføjes andre bøger, som gør det nemt at flytte kapitler og sider mellem bøgerne. Man kan indstille en automatisk sorteringsregel, så bogens indhold automatisk sorteres efter ændringer.',\n    'books_sort_auto_sort' => 'Mulighed for automatisk sortering',\n    'books_sort_auto_sort_active' => 'Automatisk sortering Aktiv: :sortName',\n    'books_sort_named' => 'Sorter bog :bookName',\n    'books_sort_name' => 'Sortér efter navn',\n    'books_sort_created' => 'Sortér efter oprettelsesdato',\n    'books_sort_updated' => 'Sortér efter opdateringsdato',\n    'books_sort_chapters_first' => 'Kapitler først',\n    'books_sort_chapters_last' => 'Kapitler sidst',\n    'books_sort_show_other' => 'Vis andre bøger',\n    'books_sort_save' => 'Gem ny ordre',\n    'books_sort_show_other_desc' => 'Tilføj andre bøger her for at inkludere dem i sorteringen og gøre det nemt at omorganisere på tværs af bøger.',\n    'books_sort_move_up' => 'Flyt op',\n    'books_sort_move_down' => 'Flyt ned',\n    'books_sort_move_prev_book' => 'Flyt til forrige bog',\n    'books_sort_move_next_book' => 'Flyt til næste bog',\n    'books_sort_move_prev_chapter' => 'Flyt Til Foregående Kapitel',\n    'books_sort_move_next_chapter' => 'Flyt Til Næste Kapitel',\n    'books_sort_move_book_start' => 'Flyt til Start af bog',\n    'books_sort_move_book_end' => 'Flyt til slutningen af bogen',\n    'books_sort_move_before_chapter' => 'Gå til kapitlet Før',\n    'books_sort_move_after_chapter' => 'Flyt til Efter kapitel',\n    'books_copy' => 'Kopier Bog',\n    'books_copy_success' => 'Bogen blev kopieret',\n\n    // Chapters\n    'chapter' => 'Kapitel',\n    'chapters' => 'Kapitler',\n    'x_chapters' => ':count kapitel|:count kapitler',\n    'chapters_popular' => 'Populære kapitler',\n    'chapters_new' => 'Nyt kapitel',\n    'chapters_create' => 'Opret nyt kapitel',\n    'chapters_delete' => 'Slet kapitel',\n    'chapters_delete_named' => 'Slet kapitel :chapterName',\n    'chapters_delete_explain' => 'Dette vil slette kapitlet med navnet \\':chapterName\\'. Alle sider i dette kapitel vil også blive slettet.',\n    'chapters_delete_confirm' => 'Er du sikker på du vil slette dette kapitel?',\n    'chapters_edit' => 'Rediger kapitel',\n    'chapters_edit_named' => 'Rediger kapitel :chapterName',\n    'chapters_save' => 'Gem kapitel',\n    'chapters_move' => 'Flyt kapitel',\n    'chapters_move_named' => 'Flyt kapitel :chapterName',\n    'chapters_copy' => 'Kopier Kapitel',\n    'chapters_copy_success' => 'Kapitlet blev kopieret',\n    'chapters_permissions' => 'Kapiteltilladelser',\n    'chapters_empty' => 'Der er lige nu ingen sider i dette kapitel.',\n    'chapters_permissions_active' => 'Aktive kapiteltilladelser',\n    'chapters_permissions_success' => 'Kapiteltilladelser opdateret',\n    'chapters_search_this' => 'Søg i dette kapitel',\n    'chapter_sort_book' => 'Sorter bog',\n\n    // Pages\n    'page' => 'Side',\n    'pages' => 'Sider',\n    'x_pages' => ':count Side|:count Sider',\n    'pages_popular' => 'Populære sider',\n    'pages_new' => 'Ny side',\n    'pages_attachments' => 'Vedhæftninger',\n    'pages_navigation' => 'Sidenavigation',\n    'pages_delete' => 'Slet side',\n    'pages_delete_named' => 'Slet side :pageName',\n    'pages_delete_draft_named' => 'Slet kladdesidde :pageName',\n    'pages_delete_draft' => 'Slet kladdeside',\n    'pages_delete_success' => 'Side slettet',\n    'pages_delete_draft_success' => 'Kladdeside slettet',\n    'pages_delete_warning_template' => 'Denne side er i aktiv brug som standardsideskabelon for en bog eller et kapitel. Disse bøger eller kapitler vil ikke længere have en standardsideskabelon tildelt, når denne side er slettet.',\n    'pages_delete_confirm' => 'Er du sikker på, du vil slette denne side?',\n    'pages_delete_draft_confirm' => 'Er du sikker på at du vil slette denne kladdesidde?',\n    'pages_editing_named' => 'Redigerer :pageName',\n    'pages_edit_draft_options' => 'Kladdeindstillinger',\n    'pages_edit_save_draft' => 'Gem kladde',\n    'pages_edit_draft' => 'Rediger sidekladde',\n    'pages_editing_draft' => 'Redigerer kladde',\n    'pages_editing_page' => 'Redigerer side',\n    'pages_edit_draft_save_at' => 'Kladde gemt ved ',\n    'pages_edit_delete_draft' => 'Slet kladde',\n    'pages_edit_delete_draft_confirm' => 'Er du sikker på, at du vil slette dine ændringer på kladdesiden? Alle dine ændringer siden den sidste fulde lagring vil gå tabt, og editoren vil blive opdateret med den seneste lagring af siden uden udkast.',\n    'pages_edit_discard_draft' => 'Kassér kladde',\n    'pages_edit_switch_to_markdown' => 'Skift til Markdown redigering',\n    'pages_edit_switch_to_markdown_clean' => '(Rent indhold)',\n    'pages_edit_switch_to_markdown_stable' => '(Stabilt indhold)',\n    'pages_edit_switch_to_wysiwyg' => 'Skift til WYSIWYG redigering',\n    'pages_edit_switch_to_new_wysiwyg' => 'Skift til ny WYSIWYG (Hvad man ser, er hvad man får)',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(I Beta Test)',\n    'pages_edit_set_changelog' => 'Sæt ændringsoversigt',\n    'pages_edit_enter_changelog_desc' => 'Indtast en kort beskrivelse af ændringer du har lavet',\n    'pages_edit_enter_changelog' => 'Indtast ændringsoversigt',\n    'pages_editor_switch_title' => 'Skift Editor',\n    'pages_editor_switch_are_you_sure' => 'Er du sikker på, at du vil ændre editoren for denne side?',\n    'pages_editor_switch_consider_following' => 'Overvej følgende ved skift af redaktører:',\n    'pages_editor_switch_consideration_a' => 'Når den nye redaktørindstilling er gemt, vil den blive brugt af alle fremtidige redaktører, også dem, der måske ikke selv kan ændre redaktørtype.',\n    'pages_editor_switch_consideration_b' => 'Dette kan potentielt føre til tab af detaljer og syntaks under visse omstændigheder.',\n    'pages_editor_switch_consideration_c' => 'Ændringer i tag eller changelog, der er foretaget siden sidste lagring, fortsætter ikke på tværs af denne ændring.',\n    'pages_save' => 'Gem siden',\n    'pages_title' => 'Overskrift',\n    'pages_name' => 'Sidenavn',\n    'pages_md_editor' => 'Editor',\n    'pages_md_preview' => 'Forhåndsvisning',\n    'pages_md_insert_image' => 'Indsæt billede',\n    'pages_md_insert_link' => 'Indsæt emnelink',\n    'pages_md_insert_drawing' => 'Indsæt tegning',\n    'pages_md_show_preview' => 'Vis forhåndsvisning',\n    'pages_md_sync_scroll' => 'Rulning af forhåndsvisning af synkronisering',\n    'pages_md_plain_editor' => 'Klartekst editor',\n    'pages_drawing_unsaved' => 'Ikke gemt tegning fundet',\n    'pages_drawing_unsaved_confirm' => 'Der blev fundet ikke-gemte tegningsdata fra et tidligere mislykket forsøg på at gemme en tegning. Vil du gendanne og fortsætte med at redigere denne ikke-gemte tegning?',\n    'pages_not_in_chapter' => 'Side er ikke i et kapitel',\n    'pages_move' => 'Flyt side',\n    'pages_copy' => 'Kopier side',\n    'pages_copy_desination' => 'Kopier destination',\n    'pages_copy_success' => 'Side kopieret succesfuldt',\n    'pages_permissions' => 'Sidetilladelser',\n    'pages_permissions_success' => 'Sidetilladelser opdateret',\n    'pages_revision' => 'Revision',\n    'pages_revisions' => 'Sidserevisioner',\n    'pages_revisions_desc' => 'Nedenfor findes alle tidligere revisioner af denne side. Du kan se tilbage på, sammenligne og gendanne gamle sideversioner, hvis tilladelserne tillader det. Sidens fulde historik afspejles muligvis ikke fuldt ud her, da gamle revisioner kan blive slettet automatisk, afhængigt af systemkonfigurationen.',\n    'pages_revisions_named' => 'Siderevisioner for :pageName',\n    'pages_revision_named' => 'Siderevision for :pageName',\n    'pages_revision_restored_from' => 'Genoprettet fra #:id; :summary',\n    'pages_revisions_created_by' => 'Oprettet af',\n    'pages_revisions_date' => 'Revisionsdato',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Revisionsnummer',\n    'pages_revisions_numbered' => 'Revision #:id',\n    'pages_revisions_numbered_changes' => 'Revision #:id ændringer',\n    'pages_revisions_editor' => 'Editor Type',\n    'pages_revisions_changelog' => 'Ændringsoversigt',\n    'pages_revisions_changes' => 'Ændringer',\n    'pages_revisions_current' => 'Nuværende version',\n    'pages_revisions_preview' => 'Forhåndsvisning',\n    'pages_revisions_restore' => 'Gendan',\n    'pages_revisions_none' => 'Denne side har ingen revisioner',\n    'pages_copy_link' => 'Kopier link',\n    'pages_edit_content_link' => 'Spring til sektionen i editoren',\n    'pages_pointer_enter_mode' => 'Gå ind i sektionsvalgtilstand',\n    'pages_pointer_label' => 'Indstillinger for sideafsnit',\n    'pages_pointer_permalink' => 'Side Afsnit Permalink',\n    'pages_pointer_include_tag' => 'Sideafsnit inkluderer tag',\n    'pages_pointer_toggle_link' => 'Permalink-tilstand, tryk for at vise include-tag',\n    'pages_pointer_toggle_include' => 'Inkluder tag-tilstand, tryk for at vise permalink',\n    'pages_permissions_active' => 'Aktive sidetilladelser',\n    'pages_initial_revision' => 'Første udgivelse',\n    'pages_references_update_revision' => 'Automatisk systemopdatering af interne links',\n    'pages_initial_name' => 'Ny side',\n    'pages_editing_draft_notification' => 'Du redigerer en kladde der sidst var gemt :timeDiff.',\n    'pages_draft_edited_notification' => 'Siden har været opdateret siden da. Det er anbefalet at du kasserer denne kladde.',\n    'pages_draft_page_changed_since_creation' => 'Denne side er blevet opdateret siden dette udkast blev oprettet. Det anbefales at du kasserer dette udkast eller passer på ikke at overskrive nogen sideændringer.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count brugerer har begyndt at redigere denne side',\n        'start_b' => ':userName er begyndt at redigere denne side',\n        'time_a' => 'siden siden sidst blev opdateret',\n        'time_b' => 'i de sidste :minCount minutter',\n        'message' => ':start : time. Pas på ikke at overskrive hinandens opdateringer!',\n    ],\n    'pages_draft_discarded' => 'Udkast kasseret! Editoren er blevet opdateret med det aktuelle sideindhold',\n    'pages_draft_deleted' => 'Udkast slettet! Editoren er blevet opdateret med det aktuelle sideindhold',\n    'pages_specific' => 'Specifik side',\n    'pages_is_template' => 'Sideskabelon',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Sidebjælke til/fra',\n    'page_tags' => 'Sidetags',\n    'chapter_tags' => 'Kapiteltags',\n    'book_tags' => 'Bogtags',\n    'shelf_tags' => 'Reoltags',\n    'tag' => 'Tag',\n    'tags' =>  'Tags',\n    'tags_index_desc' => 'Tags kan anvendes på indhold i systemet for at anvende en fleksibel form for kategorisering. Tags kan have både en nøgle og en værdi, hvor værdien er valgfri. Når de er anvendt, kan der søges efter indhold ved hjælp af tagnavn og -værdi.',\n    'tag_name' =>  'Tagnavn',\n    'tag_value' => 'Tagværdi (valgfri)',\n    'tags_explain' => \"Tilføj nogle tags for bedre at kategorisere dit indhold. \\n Du kan tildele en værdi til et tag for mere dybdegående organisering.\",\n    'tags_add' => 'Tilføj endnu et tag',\n    'tags_remove' => 'Fjern dette tag',\n    'tags_usages' => 'Samlet brug af tags',\n    'tags_assigned_pages' => 'Tildelt til sider',\n    'tags_assigned_chapters' => 'Tildelt til Kapitler',\n    'tags_assigned_books' => 'Tildelt til Bøger',\n    'tags_assigned_shelves' => 'Tildelt til bogreoler',\n    'tags_x_unique_values' => ':count unikke værdier',\n    'tags_all_values' => 'Alle værdier',\n    'tags_view_tags' => 'Vis Tags',\n    'tags_view_existing_tags' => 'Vis eksisterende tags',\n    'tags_list_empty_hint' => 'Tags kan tildeles via sideeditorens sidepanel eller under redigering af detaljerne i en bog, et kapitel eller en hylde.',\n    'attachments' => 'Vedhæftninger',\n    'attachments_explain' => 'Upload nogle filer, eller vedhæft nogle links, der skal vises på siden. Disse er synlige i sidepanelet.',\n    'attachments_explain_instant_save' => 'Ændringer her gemmes med det samme.',\n    'attachments_upload' => 'Upload fil',\n    'attachments_link' => 'Vedhæft link',\n    'attachments_upload_drop' => 'Alternativt kan du trække og slippe en fil her for at uploade den som en vedhæftet fil.',\n    'attachments_set_link' => 'Sæt link',\n    'attachments_delete' => 'Er du sikker på at du vil slette denne vedhæftning?',\n    'attachments_dropzone' => 'Slip filer her for at uploade',\n    'attachments_no_files' => 'Ingen filer er blevet overført',\n    'attachments_explain_link' => 'Du kan vedhæfte et link, hvis du foretrækker ikke at uploade en fil. Dette kan være et link til en anden side eller et link til en fil i skyen.',\n    'attachments_link_name' => 'Linknavn',\n    'attachment_link' => 'Vedhæftningslink',\n    'attachments_link_url' => 'Link til filen',\n    'attachments_link_url_hint' => 'Adresse (URL) på side eller fil',\n    'attach' => 'Vedhæft',\n    'attachments_insert_link' => 'Tilføj vedhæftningslink til side',\n    'attachments_edit_file' => 'Rediger fil',\n    'attachments_edit_file_name' => 'Filnavn',\n    'attachments_edit_drop_upload' => 'Slip filer eller klik her for at uploade og overskrive',\n    'attachments_order_updated' => 'Vedhæftningsordre opdateret',\n    'attachments_updated_success' => 'Vedhæftningsdetaljer opdateret',\n    'attachments_deleted' => 'Vedhæftning slettet',\n    'attachments_file_uploaded' => 'Filen blev uploadet korrekt',\n    'attachments_file_updated' => 'Filen blev opdateret korrekt',\n    'attachments_link_attached' => 'Link succesfuldt vedhæftet til side',\n    'templates' => 'Skabeloner',\n    'templates_set_as_template' => 'Side er en skabelon',\n    'templates_explain_set_as_template' => 'Du kan indstille denne side som en skabelon, så dens indhold bruges, når du opretter andre sider. Andre brugere vil kunne bruge denne skabelon, hvis de har visningstilladelser til denne side.',\n    'templates_replace_content' => 'Udskift sideindhold',\n    'templates_append_content' => 'Tilføj efter sideindhold',\n    'templates_prepend_content' => 'Tilføj før sideindhold',\n\n    // Profile View\n    'profile_user_for_x' => 'Bruger i :time',\n    'profile_created_content' => 'Oprettet indhold',\n    'profile_not_created_pages' => ':userName har ikke oprettet nogle sider',\n    'profile_not_created_chapters' => ':userName har ikke oprettet nogle kapitler',\n    'profile_not_created_books' => ':userName har ikke oprettet nogle bøger',\n    'profile_not_created_shelves' => ':userName har ikke oprettet nogle reoler',\n\n    // Comments\n    'comment' => 'Kommentar',\n    'comments' => 'Kommentarer',\n    'comment_add' => 'Tilføj kommentar',\n    'comment_none' => 'Ingen kommentarer at vise',\n    'comment_placeholder' => 'Skriv en kommentar her',\n    'comment_thread_count' => ':count Kommentar Tråde:count Kommentar Tråde',\n    'comment_archived_count' => ':count Arkiveret',\n    'comment_archived_threads' => 'Arkiverede Tråde',\n    'comment_save' => 'Gem kommentar',\n    'comment_new' => 'Ny kommentar',\n    'comment_created' => 'kommenteret :createDiff',\n    'comment_updated' => 'Opdateret :updateDiff af :username',\n    'comment_updated_indicator' => 'Opdateret',\n    'comment_deleted_success' => 'Kommentar slettet',\n    'comment_created_success' => 'Kommentaren er tilføjet',\n    'comment_updated_success' => 'Kommentaren er opdateret',\n    'comment_archive_success' => 'Kommentar arkiveret',\n    'comment_unarchive_success' => 'Kommentaren er ikke længere arkiveret',\n    'comment_view' => 'Se kommentar',\n    'comment_jump_to_thread' => 'Hop til tråd',\n    'comment_delete_confirm' => 'Er du sikker på, at du vil slette denne kommentar?',\n    'comment_in_reply_to' => 'Som svar til :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Forældet)',\n    'comment_editor_explain' => 'Her er de kommentarer, der er blevet efterladt på denne side. Kommentarer kan tilføjes og administreres, når du ser den gemte side.',\n\n    // Revision\n    'revision_delete_confirm' => 'Er du sikker på at du vil slette denne revision?',\n    'revision_restore_confirm' => 'Er du sikker på at du ønsker at gendanne denne revision? Nuværende sideindhold vil blive erstattet.',\n    'revision_cannot_delete_latest' => 'Kan ikke slette seneste revision.',\n\n    // Copy view\n    'copy_consider' => 'Vær opmærksom på nedenstående, når du kopierer indhold.',\n    'copy_consider_permissions' => 'Brugerdefinerede tilladelsesindstillinger kopieres ikke.',\n    'copy_consider_owner' => 'Du bliver ejer af alt kopieret indhold.',\n    'copy_consider_images' => 'Sidens billedfiler vil ikke blive duplikeret, og de originale billeder vil bevare deres relation til den side, de oprindeligt blev uploadet til.',\n    'copy_consider_attachments' => 'Vedhæftede sider bliver ikke kopieret.',\n    'copy_consider_access' => 'En ændring af placering, ejer eller tilladelser kan resultere i, at dette indhold er tilgængeligt for dem, der tidligere ikke havde adgang.',\n\n    // Conversions\n    'convert_to_shelf' => 'Konverter til hylde',\n    'convert_to_shelf_contents_desc' => 'Du kan konvertere denne bog til en ny hylde med samme indhold. Kapitler i denne bog vil blive konverteret til nye bøger. Hvis denne bog indeholder sider, som ikke er i et kapitel, vil bogen blive omdøbt og indeholde sådanne sider, og bogen vil blive en del af den nye hylde.',\n    'convert_to_shelf_permissions_desc' => 'Alle tilladelser, der er indstillet på denne bog, vil blive kopieret til den nye hylde og til alle nye underordnede bøger, der ikke har deres egne tilladelser håndhævet. Bemærk, at tilladelser på hylder ikke automatisk overføres til indholdet på hylden, som de gør for bøger.',\n    'convert_book' => 'Omdan Bog',\n    'convert_book_confirm' => 'Er du sikker på, at du vil konvertere denne bog?',\n    'convert_undo_warning' => 'Det kan ikke gøres om så let.',\n    'convert_to_book' => 'Omdan til Bog',\n    'convert_to_book_desc' => 'Du kan konvertere dette kapitel til en ny bog med samme indhold. Alle tilladelser, der er indstillet på dette kapitel, vil blive kopieret til den nye bog, men eventuelle nedarvede tilladelser fra den overordnede bog vil ikke blive kopieret, hvilket kan føre til en ændring af adgangskontrollen.',\n    'convert_chapter' => 'Omdan Kapitel',\n    'convert_chapter_confirm' => 'Er du sikker på at du vil omdanne dette kapitel?',\n\n    // References\n    'references' => 'Kilder',\n    'references_none' => 'Der er ingen sporede referencer til dette emne.',\n    'references_to_desc' => 'Nedenfor vises alt det kendte indhold i systemet, som linker til dette element.',\n\n    // Watch Options\n    'watch' => 'Overvåg',\n    'watch_title_default' => 'Standardindstillinger',\n    'watch_desc_default' => 'Gå tilbage til kun at se dine standardindstillinger for notifikationer.',\n    'watch_title_ignore' => 'Ignorér',\n    'watch_desc_ignore' => 'Ignorer alle meddelelser, også dem fra indstillinger på brugerniveau.',\n    'watch_title_new' => 'Nye sider',\n    'watch_desc_new' => 'Giv besked, når der oprettes en ny side i dette element.',\n    'watch_title_updates' => 'Alle sideopdateringer',\n    'watch_desc_updates' => 'Giv besked om alle nye sider og sideændringer.',\n    'watch_desc_updates_page' => 'Giv besked om alle sideændringer.',\n    'watch_title_comments' => 'Alle sideopdateringer og kommentarer',\n    'watch_desc_comments' => 'Giv besked om alle nye sider, sideændringer og nye kommentarer.',\n    'watch_desc_comments_page' => 'Giv besked ved sideændringer og nye kommentarer.',\n    'watch_change_default' => 'Skift indstillinger for standardmeddelelser',\n    'watch_detail_ignore' => 'Ignorerer notifikationer',\n    'watch_detail_new' => 'Holder øje med nye sider',\n    'watch_detail_updates' => 'Holder øje med nye sider og opdateringer',\n    'watch_detail_comments' => 'Holder øje med nye sider, opdateringer og kommentarer',\n    'watch_detail_parent_book' => 'Se med via forældrebogen',\n    'watch_detail_parent_book_ignore' => 'Ignorering via forældrebog',\n    'watch_detail_parent_chapter' => 'Se med via forældrekapitel',\n    'watch_detail_parent_chapter_ignore' => 'Ignorering via forældrekapitel',\n];\n"
  },
  {
    "path": "lang/da/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Du har ikke tilladelse til at tilgå den efterspurgte side.',\n    'permissionJson' => 'Du har ikke tilladelse til at udføre den valgte handling.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'En bruger med email :email eksistere allerede, men med andre legitimationsoplysninger.',\n    'auth_pre_register_theme_prevention' => 'Brugerkonto kunne ikke registreres for de angivne detaljer',\n    'email_already_confirmed' => 'Email er allerede bekræftet. Prøv at logge ind.',\n    'email_confirmation_invalid' => 'Denne bekræftelsestoken er ikke gyldig eller er allerede blevet brugt. Prøv at registrere dig igen.',\n    'email_confirmation_expired' => 'Bekræftelsestoken er udløbet. En ny bekræftelsesmail er blevet sendt.',\n    'email_confirmation_awaiting' => 'Mail-adressen for din konto i brug er nød til at blive bekræftet',\n    'ldap_fail_anonymous' => 'LDAP-adgang fejlede ved brugen af annonym bind',\n    'ldap_fail_authed' => 'LDAP adgang fejlede med de givne DN & kodeord oplysninger',\n    'ldap_extension_not_installed' => 'LDAP PHP udvidelse er ikke installeret',\n    'ldap_cannot_connect' => 'Kan ikke forbinde til ldap server. Indledende forbindelse mislykkedes',\n    'saml_already_logged_in' => 'Allerede logget ind',\n    'saml_no_email_address' => 'Kunne ikke finde en e-mail-adresse for denne bruger i de data, der leveres af det eksterne godkendelsessystem',\n    'saml_invalid_response_id' => 'Anmodningen fra det eksterne godkendelsessystem genkendes ikke af en proces, der er startet af denne applikation. Navigering tilbage efter et login kan forårsage dette problem.',\n    'saml_fail_authed' => 'Login ved hjælp af :system failed, systemet har ikke givet tilladelse',\n    'oidc_already_logged_in' => 'Allerede logget ind',\n    'oidc_no_email_address' => 'Kunne ikke finde en e-mailadresse for denne bruger i de data, der leveres af det eksterne godkendelsessystem',\n    'oidc_fail_authed' => 'Login ved hjælp af :system fejlede, systemet har ikke givet tilladelse',\n    'social_no_action_defined' => 'Ingen handling er defineret',\n    'social_login_bad_response' => \"Der opstod en fejl under :socialAccount login:\\n:error\",\n    'social_account_in_use' => 'Denne :socialAccount konto er allerede i brug, prøv at logge ind med :socialAccount loginmetoden.',\n    'social_account_email_in_use' => 'Emailen :email er allerede i brug. Hvis du allerede har en konto, kan du forbinde din :socialAccount fra dine profilindstillinger.',\n    'social_account_existing' => ':socialAccount er allerede tilknyttet din profil.',\n    'social_account_already_used_existing' => 'Denne :socialAccount konto er allerede i brug af en anden bruger.',\n    'social_account_not_used' => 'Denne :socialAccount konto er ikke tilknyttet nogle brugere. Tilknyt den i dine profilindstillinger. ',\n    'social_account_register_instructions' => 'Hvis du ikke har en konto, kan du registrere en konto gennem :socialAccount loginmetoden.',\n    'social_driver_not_found' => 'Socialdriver ikke fundet',\n    'social_driver_not_configured' => 'Dine :socialAccount indstillinger er ikke konfigureret korret.',\n    'invite_token_expired' => 'Dette invitationslink er udløbet. I stedet kan du prøve at nulstille din kontos kodeord.',\n    'login_user_not_found' => 'Der kunne ikke findes en bruger til denne handling.',\n\n    // System\n    'path_not_writable' => 'Filsti :filePath kunne ikke uploades til. Sørg for at den kan skrives til af webserveren.',\n    'cannot_get_image_from_url' => 'Kan ikke finde billede på :url',\n    'cannot_create_thumbs' => 'Serveren kan ikke oprette miniaturer. Kontroller, at GD PHP-udvidelsen er installeret.',\n    'server_upload_limit' => 'Serveren tillader ikke uploads af denne størrelse. Prøv en mindre filstørrelse.',\n    'server_post_limit' => 'Serveren kan ikke modtage den angivne mængde data. Prøv igen med færre data eller en mindre fil.',\n    'uploaded'  => 'Serveren tillader ikke uploads af denne størrelse. Prøv en mindre filstørrelse.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Der opstod en fejl ved upload af billedet',\n    'image_upload_type_error' => 'Billedtypen, der uploades, er ugyldig',\n    'image_upload_replace_type' => 'Udskiftninger af billedfiler skal være af samme type',\n    'image_upload_memory_limit' => 'Kunne ikke håndtere billedupload og/eller oprette thumbnails på grund af begrænsninger i systemets ressourcer.',\n    'image_thumbnail_memory_limit' => 'Det lykkedes ikke at skabe variationer i billedstørrelsen på grund af begrænsninger i systemets ressourcer.',\n    'image_gallery_thumbnail_memory_limit' => 'Det lykkedes ikke at oprette galleriminiaturer på grund af begrænsede systemressourcer.',\n    'drawing_data_not_found' => 'Tegningsdata kunne ikke indlæses. Tegningsfilen findes måske ikke længere, eller du har måske ikke tilladelse til at få adgang til den.',\n\n    // Attachments\n    'attachment_not_found' => 'Vedhæftning ikke fundet',\n    'attachment_upload_error' => 'Der opstod en fejl ved upload af den vedhæftede fil',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Kunne ikke gemme kladde. Tjek at du har internetforbindelse før du gemmer siden',\n    'page_draft_delete_fail' => 'Kunne ikke slette sideudkast og hente den aktuelle sides gemte indhold',\n    'page_custom_home_deletion' => 'Kan ikke slette en side der er sat som forside',\n\n    // Entities\n    'entity_not_found' => 'Emne ikke fundet',\n    'bookshelf_not_found' => 'Hylde ikke fundet',\n    'book_not_found' => 'Bog ikke fundet',\n    'page_not_found' => 'Side ikke fundet',\n    'chapter_not_found' => 'Kapitel ikke fundet',\n    'selected_book_not_found' => 'Den valgte bog kunne ikke findes',\n    'selected_book_chapter_not_found' => 'Den valgte bog eller kapitel kunne ikke findes',\n    'guests_cannot_save_drafts' => 'Gæster kan ikke gemme kladder',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Du kan ikke slette den eneste admin',\n    'users_cannot_delete_guest' => 'Du kan ikke slette gæstebrugeren',\n    'users_could_not_send_invite' => 'Kunne ikke oprette en bruger, da invitationsmailen ikke blev sendt',\n\n    // Roles\n    'role_cannot_be_edited' => 'Denne rolle kan ikke redigeres',\n    'role_system_cannot_be_deleted' => 'Denne rolle er en systemrolle og kan ikke slettes',\n    'role_registration_default_cannot_delete' => 'Kan ikke slette rollen mens den er sat som standardrolle for registrerede brugere',\n    'role_cannot_remove_only_admin' => 'Denne bruger er den eneste bruger der har administratorrollen. Tilføj en anden bruger til administratorrollen før du forsøger at slette den her.',\n\n    // Comments\n    'comment_list' => 'Der opstod en fejl under hentning af kommentarerne.',\n    'cannot_add_comment_to_draft' => 'Du kan ikke kommentere på en kladde.',\n    'comment_add' => 'Der opstod en fejl under tilføjelse/opdatering af kommentaren.',\n    'comment_delete' => 'Der opstod en fejl under sletning af kommentaren.',\n    'empty_comment' => 'Kan ikke tilføje en tom kommentar.',\n\n    // Error pages\n    '404_page_not_found' => 'Siden blev ikke fundet',\n    'sorry_page_not_found' => 'Beklager, siden du leder efter blev ikke fundet.',\n    'sorry_page_not_found_permission_warning' => 'Hvis du forventede, at denne side skulle eksistere, har du muligvis ikke tilladelse til at se den.',\n    'image_not_found' => 'Billede ikke fundet',\n    'image_not_found_subtitle' => 'Beklager, billedet du ledte efter kunne ikke findes.',\n    'image_not_found_details' => 'Hvis du forventede, at dette billede skulle eksistere, kan det være blevet slettet.',\n    'return_home' => 'Gå tilbage til hjem',\n    'error_occurred' => 'Der opstod en fejl',\n    'app_down' => ':appName er nede lige nu',\n    'back_soon' => 'Den er oppe igen snart.',\n\n    // Import\n    'import_zip_cant_read' => 'Kunne ikke læse ZIP-filen.',\n    'import_zip_cant_decode_data' => 'Kunne ikke finde og afkode ZIP data.json-indhold.',\n    'import_zip_no_data' => 'ZIP-filens data har ikke noget forventet bog-, kapitel- eller sideindhold.',\n    'import_zip_data_too_large' => 'Indholdet af ZIP data.json overstiger den konfigurerede maksimale uploadstørrelse for applikationen.',\n    'import_validation_failed' => 'Import ZIP kunne ikke valideres med fejl:',\n    'import_zip_failed_notification' => 'Kunne ikke importere ZIP-fil.',\n    'import_perms_books' => 'Du mangler de nødvendige tilladelser til at oprette bøger.',\n    'import_perms_chapters' => 'Du mangler de nødvendige tilladelser til at oprette kapitler.',\n    'import_perms_pages' => 'Du mangler de nødvendige tilladelser til at oprette sider.',\n    'import_perms_images' => 'Du mangler de nødvendige tilladelser til at oprette billeder.',\n    'import_perms_attachments' => 'Du mangler den nødvendige tilladelse til at oprette vedhæftede filer.',\n\n    // API errors\n    'api_no_authorization_found' => 'Der blev ikke fundet nogen autorisationstoken på anmodningen',\n    'api_bad_authorization_format' => 'En autorisationstoken blev fundet på anmodningen, men formatet var forkert',\n    'api_user_token_not_found' => 'Der blev ikke fundet nogen matchende API-token for det angivne autorisationstoken',\n    'api_incorrect_token_secret' => 'Hemmeligheden leveret til det givne anvendte API-token er forkert',\n    'api_user_no_api_permission' => 'Ejeren af den brugte API token har ikke adgang til at foretage API-kald',\n    'api_user_token_expired' => 'Den brugte godkendelsestoken er udløbet',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Følgende fejl opstod under afsendelse af testemail:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'URL\\'en stemmer ikke overens med de konfigurerede tilladte SSR-værter',\n];\n"
  },
  {
    "path": "lang/da/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Ny kommentar på siden: :pageName',\n    'new_comment_intro' => 'En bruger har kommenteret på en side i :appName:',\n    'new_page_subject' => 'Ny side: :pageName',\n    'new_page_intro' => 'En ny side er blevet oprettet i :appName:',\n    'updated_page_subject' => 'Opdateret side: :pageName',\n    'updated_page_intro' => 'En side er blevet opdateret i :appName:',\n    'updated_page_debounce' => 'For at forhindre en masse af notifikationer, i et stykke tid vil du ikke blive sendt notifikationer for yderligere redigeringer til denne side af den samme editor.',\n    'comment_mention_subject' => 'Du er blevet nævnt i en kommentar på siden: :pageName',\n    'comment_mention_intro' => 'Du blev nævnt i en kommentar på :appName:',\n\n    'detail_page_name' => 'Sidens navn:',\n    'detail_page_path' => 'Sidesti:',\n    'detail_commenter' => 'Kommentator:',\n    'detail_comment' => 'Bemærkninger:',\n    'detail_created_by' => 'Oprettet af:',\n    'detail_updated_by' => 'Opdateret af:',\n\n    'action_view_comment' => 'Se kommentar',\n    'action_view_page' => 'Vis Side',\n\n    'footer_reason' => 'Denne meddelelse blev sendt til dig, fordi :link dækker denne type aktivitet for denne vare.',\n    'footer_reason_link' => 'dine indstillinger for notifikationer',\n];\n"
  },
  {
    "path": "lang/da/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Forrige',\n    'next'     => 'Næste &raquo;',\n\n];\n"
  },
  {
    "path": "lang/da/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Adgangskoder skal være mindst otte tegn og svare til bekræftelsen.',\n    'user' => \"Vi kan ikke finde en bruger med den e-mailadresse.\",\n    'token' => 'Linket til nulstilling af adgangskode er ugyldigt for denne e-mail-adresse.',\n    'sent' => 'Vi har sendt dig en e-mail med et link til at nulstille adgangskoden!',\n    'reset' => 'Dit kodeord er blevet nulstillet!',\n\n];\n"
  },
  {
    "path": "lang/da/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Min konto',\n\n    'shortcuts' => 'Genveje',\n    'shortcuts_interface' => 'Genveje',\n    'shortcuts_toggle_desc' => 'Her kan du aktivere eller deaktivere genveje, der bruges til navigation og handlinger.',\n    'shortcuts_customize_desc' => 'Du kan tilpasse hver af genvejene nedenfor. Tryk på din ønskede tastekombination efter at have valgt feltet for genvejen.',\n    'shortcuts_toggle_label' => 'Tastaturgenveje aktiveret',\n    'shortcuts_section_navigation' => 'Navigation',\n    'shortcuts_section_actions' => 'Almindelige handlinger',\n    'shortcuts_save' => 'Gem Genveje',\n    'shortcuts_overlay_desc' => 'Bemærk: Når genveje er aktiveret kan du altid se de tilgængelige genveje ved at trykke på \"?\" på dit tastatur.',\n    'shortcuts_update_success' => 'Genvejspræferencer er blevet opdateret!',\n    'shortcuts_overview_desc' => 'Håndtér tastaturgenveje, du kan bruge til at navigere i systemets brugergrænseflade.',\n\n    'notifications' => 'Notifikationer',\n    'notifications_desc' => 'Administrer de e-mail-notifikationer, du modtager, når visse aktiviteter udføres i systemet.',\n    'notifications_opt_own_page_changes' => 'Adviser ved ændringer af sider, jeg ejer',\n    'notifications_opt_own_page_comments' => 'Adviser ved kommentarer på sider, jeg ejer',\n    'notifications_opt_comment_mentions' => 'Giv besked, når jeg er nævnt i en kommentar',\n    'notifications_opt_comment_replies' => 'Adviser ved svar på mine kommentarer',\n    'notifications_save' => 'Gem indstillinger',\n    'notifications_update_success' => 'Indstillinger for notifikationer er blevet opdateret!',\n    'notifications_watched' => 'Overvågede og ignorerede',\n    'notifications_watched_desc' => 'Nedenfor er de elementer, der har brugerdefinerede overvågning aktivt. For at opdatere dine præferencer for disse, gå til elementet og find derefter overvågning i sidepanelet.',\n\n    'auth' => 'Adgang og sikkerhed',\n    'auth_change_password' => 'Skift adgangskode',\n    'auth_change_password_desc' => 'Skift den adgangskode, du bruger til at logge ind med. Den skal være mindst 8 tegn lang.',\n    'auth_change_password_success' => 'Adgangskoden er blevet opdateret!',\n\n    'profile' => 'Profil',\n    'profile_desc' => 'Administrer detaljerne på din konto, som repræsenterer dig over for andre brugere.',\n    'profile_view_public' => 'Vis offentlig profil',\n    'profile_name_desc' => 'Konfigurer dit visningsnavn, som vil være synligt for andre brugere i systemet gennem den aktivitet, du udfører, og indhold du ejer.',\n    'profile_email_desc' => 'Denne e-mail vil blive brugt til notifikationer og, afhængigt af aktiv systemgodkendelse, systemadgang.',\n    'profile_email_no_permission' => 'Desværre har du ikke tilladelse til at ændre din e-mailadresse. Hvis du ønsker at ændre dette, skal du bede en administrator om at ændre dette for dig.',\n    'profile_avatar_desc' => 'Vælg et billede som vil blive brugt til at repræsentere dig selv over for andre i systemet. Ideelt set bør dette billede være kvadrat og omkring 256px i bredde og højde.',\n    'profile_admin_options' => 'Administratorindstillinger',\n    'profile_admin_options_desc' => 'Yderligere indstillinger på administratorniveau, såsom dem der håndterer rolleopgaver, kan findes for din brugerkonto i området \"Indstillinger > Brugere\".',\n\n    'delete_account' => 'Slet konto',\n    'delete_my_account' => 'Slet min konto',\n    'delete_my_account_desc' => 'Dette vil fuldt ud slette din brugerkonto fra systemet. Du vil ikke være i stand til at gendanne denne konto eller fortryde denne handling. Indhold, du har oprettet, såsom oprettede sider og uploadede billeder, vil ikke blive slettet.',\n    'delete_my_account_warning' => 'Er du sikker at du vil slette din konto?',\n];\n"
  },
  {
    "path": "lang/da/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Indstillinger',\n    'settings_save' => 'Gem indstillinger',\n    'system_version' => 'Systemversion',\n    'categories' => 'Kategorier',\n\n    // App Settings\n    'app_customization' => 'Tilpasning',\n    'app_features_security' => 'Funktionalitet og sikkerhed',\n    'app_name' => 'Applikationsnavn',\n    'app_name_desc' => 'Dette navn vises i headeren og i alle e-mails sendt fra systemet.',\n    'app_name_header' => 'Vis navn i header',\n    'app_public_access' => 'Offentlig adgang',\n    'app_public_access_desc' => 'Aktivering af denne funktion giver besøgende, der ikke er logget ind, adgang til indhold i din BookStack-instans.',\n    'app_public_access_desc_guest' => 'Adgang for ikke-registrerede besøgende kan kontrolleres via \"Gæst\" -brugeren.',\n    'app_public_access_toggle' => 'Tillad offentlig adgang',\n    'app_public_viewing' => 'Tillad offentlig visning?',\n    'app_secure_images' => 'Højere sikkerhed for billeduploads',\n    'app_secure_images_toggle' => 'Aktiver højere sikkerhed for billeduploads',\n    'app_secure_images_desc' => 'Af performanceårsager er alle billeder offentlige. Denne funktion tilføjer en tilfældig, vanskelig at gætte streng foran billed-url\\'er. Sørg for, at mappeindeksering ikke er aktiveret for at forhindre nem adgang.',\n    'app_default_editor' => 'Standard sideeditor',\n    'app_default_editor_desc' => 'Vælg hvilken editor der som standard skal bruges ved redigering af nye sider. Dette kan tilsidesættes på side niveau, hvor tilladelser tillader det.',\n    'app_custom_html' => 'Tilpasset HTML head indhold',\n    'app_custom_html_desc' => 'Alt indhold tilføjet her, vil blive indsat i bunden af <head> sektionen på alle sider. Dette er brugbart til overskrivning af styles og tilføjelse af analytics kode.',\n    'app_custom_html_disabled_notice' => 'Brugerdefineret HTML head indhold er deaktiveret på denne indstillingsside for at, at ændringer kan rulles tilbage.',\n    'app_logo' => 'Applikationslogo',\n    'app_logo_desc' => 'Det bruges blandt andet i applikationens headerbar. Dette billede skal være 86px i højden. Store billeder vil blive skaleret ned.',\n    'app_icon' => 'Programikon',\n    'app_icon_desc' => 'Dette ikon bruges til browserfaner og genvejsikoner. Det skal være et 256px kvadratisk PNG-billede.',\n    'app_homepage' => 'Applikationsforside',\n    'app_homepage_desc' => 'Vælg en visning, der skal vises på forsiden i stedet for standardvisningen. Sidetilladelser ignoreres for de valgte sider.',\n    'app_homepage_select' => 'Vælg en side',\n    'app_footer_links' => 'Footer links',\n    'app_footer_links_desc' => 'Tilføj links til footeren. Linksene vil blive vist nederst på de fleste sider, inkluderet sider, som ikke kræver login. Brug en label med \"trans::<key>\" for at bruge systemdefinerede oversættelser. For eksempel: \"trans::common.privacy_policy\" giver den oversatte tekst \"Privacy Policy\" og \"trans::common.terms_of_service\" vil give den oversatte tekst \"Terms of Service\".',\n    'app_footer_links_label' => 'Link label',\n    'app_footer_links_url' => 'Link URL',\n    'app_footer_links_add' => 'Tilføj footer link',\n    'app_disable_comments' => 'Deaktiver kommentarer',\n    'app_disable_comments_toggle' => 'Deaktiver kommentar',\n    'app_disable_comments_desc' => 'Deaktiverer kommentarer på tværs af alle sider i applikationen. <br> Eksisterende kommentarer vises ikke.',\n\n    // Color settings\n    'color_scheme' => 'Applikationens farveskema',\n    'color_scheme_desc' => 'Indstil de farver, der skal bruges i applikationens brugergrænseflade. Farver kan konfigureres separat for mørke og lyse tilstande for at passe bedst til temaet og sikre læsbarhed.',\n    'ui_colors_desc' => 'Indstil applikationens primære farve og standardlinkfarve. Den primære farve bruges hovedsageligt til headerbanneret, knapper og interfacedekorationer. Standardlinkfarven bruges til tekstbaserede links og handlinger, både i det skrevne indhold og i programmets brugerflade.',\n    'app_color' => 'Primær farve',\n    'link_color' => 'Standard Link Farve',\n    'content_colors_desc' => 'Indstil farver for alle elementer i sideorganisationshierarkiet. Det anbefales at vælge farver med samme lysstyrke som standardfarverne af hensyn til læsbarheden.',\n    'bookshelf_color' => 'Bogreolfarve',\n    'book_color' => 'Bogfarve',\n    'chapter_color' => 'Kapitelfarve',\n    'page_color' => 'Sidefarve',\n    'page_draft_color' => 'Sidekladdefarve',\n\n    // Registration Settings\n    'reg_settings' => 'Registrering',\n    'reg_enable' => 'Aktivér tilmelding',\n    'reg_enable_toggle' => 'Aktivér tilmelding',\n    'reg_enable_desc' => 'Når registrering er aktiveret, vil alle kunne registrere sig som en applikationsbruger. Ved registrering får de en standardbrugerrolle.',\n    'reg_default_role' => 'Standardrolle efter registrering',\n    'reg_enable_external_warning' => 'Indstillingen ovenfor ignoreres, mens ekstern LDAP- eller SAML-godkendelse er aktiv. Brugerkonti for ikke-eksisterende medlemmer oprettes automatisk, hvis godkendelse mod det eksterne system, der er i brug, er vellykket.',\n    'reg_email_confirmation' => 'Email bekræftelse',\n    'reg_email_confirmation_toggle' => 'Kræv E-Mail bekræftelse',\n    'reg_confirm_email_desc' => 'Hvis domænebegrænsning bruges, kræves e-mail-bekræftelse, og denne indstilling ignoreres.',\n    'reg_confirm_restrict_domain' => 'Domæneregistrering',\n    'reg_confirm_restrict_domain_desc' => 'Indtast en kommasepareret liste over e-mail-domæner, som du vil begrænse registreringen til. Brugere får en E-Mail for at bekræfte deres adresse, før de får tilladelse til at interagere med applikationen. <br> Bemærk, at brugere vil kunne ændre deres e-mail-adresser efter vellykket registrering.',\n    'reg_confirm_restrict_domain_placeholder' => 'Ingen restriktion opsat',\n\n    // Sorting Settings\n    'sorting' => 'Lister & Sortering',\n    'sorting_book_default' => 'Standardregel for sortering af bog',\n    'sorting_book_default_desc' => 'Vælg den standardsorteringsregel, der skal gælde for nye bøger. Dette påvirker ikke eksisterende bøger og kan tilsidesættes for hver enkelt bog.',\n    'sorting_rules' => 'Regler for sortering',\n    'sorting_rules_desc' => 'Det er foruddefinerede sorteringsoperationer, som kan anvendes på indhold i systemet.',\n    'sort_rule_assigned_to_x_books' => 'Tildelt til :count Book|Tildelt til :count Books',\n    'sort_rule_create' => 'Opret sorteringsregel',\n    'sort_rule_edit' => 'Rediger sorteringsregel',\n    'sort_rule_delete' => 'Slet sorteringsregel',\n    'sort_rule_delete_desc' => 'Fjern denne sorteringsregel fra systemet. Bøger, der bruger denne sortering, vil vende tilbage til manuel sortering.',\n    'sort_rule_delete_warn_books' => 'Denne sorteringsregel bruges i øjeblikket på :count book(s). Er du sikker på, at du vil slette den?',\n    'sort_rule_delete_warn_default' => 'Denne sorteringsregel bruges i øjeblikket som standard for bøger. Er du sikker på, at du vil slette den?',\n    'sort_rule_details' => 'Detaljer om sorteringsregler',\n    'sort_rule_details_desc' => 'Angiv et navn for denne sorteringsregel, som vises i lister, når brugerne vælger en sortering.',\n    'sort_rule_operations' => 'Sorteringsoperationer',\n    'sort_rule_operations_desc' => 'Konfigurer de sorteringshandlinger, der skal udføres, ved at flytte dem fra listen over tilgængelige handlinger. Ved brug vil handlingerne blive anvendt i rækkefølge, fra top til bund. Alle ændringer, der foretages her, vil blive anvendt på alle tildelte bøger, når de gemmes.',\n    'sort_rule_available_operations' => 'Tilgængelige operationer',\n    'sort_rule_available_operations_empty' => 'Ingen operationer tilbage',\n    'sort_rule_configured_operations' => 'Konfigurerede operationer',\n    'sort_rule_configured_operations_empty' => 'Træk/tilføj operationer fra listen \"Tilgængelige operationer\"',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Navn - alfabetisk',\n    'sort_rule_op_name_numeric' => 'Navn - numerisk',\n    'sort_rule_op_created_date' => 'Oprettet den',\n    'sort_rule_op_updated_date' => 'Opdateret dato',\n    'sort_rule_op_chapters_first' => 'Kapitler først',\n    'sort_rule_op_chapters_last' => 'De sidste kapitler',\n    'sorting_page_limits' => 'Visningsgrænser pr. side',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Vedligeholdelse',\n    'maint_image_cleanup' => 'Ryd op i billeder',\n    'maint_image_cleanup_desc' => 'Scanner side & revisionsindhold for at kontrollere, hvilke billeder og tegninger, der i øjeblikket er i brug, og hvilke billeder, der er overflødige. Sørg for, at du opretter en komplet database og billedbackup, før du kører dette.',\n    'maint_delete_images_only_in_revisions' => 'Slet også billeder, der kun findes i gamle siderevisioner',\n    'maint_image_cleanup_run' => 'Kør Oprydning',\n    'maint_image_cleanup_warning' => 'der blev fundet :count potentielt ubrugte billeder. Er du sikker på, at du vil slette disse billeder?',\n    'maint_image_cleanup_success' => ':count: potentielt ubrugte billeder fundet og slettet!',\n    'maint_image_cleanup_nothing_found' => 'Ingen ubrugte billeder fundet, intet slettet!',\n    'maint_send_test_email' => 'Send en Testemail',\n    'maint_send_test_email_desc' => 'Dette sender en testmail til din mailadresse specificeret på din profil.',\n    'maint_send_test_email_run' => 'Afsend test E-Mail',\n    'maint_send_test_email_success' => 'E-Mail sendt til :address',\n    'maint_send_test_email_mail_subject' => 'Test E-Mail',\n    'maint_send_test_email_mail_greeting' => 'E-Mail levering ser ud til at virke!',\n    'maint_send_test_email_mail_text' => 'Tillykke! Da du har modtaget denne mailnotifikation, ser det ud som om, at dine mailindstillinger er opsat korrekt.',\n    'maint_recycle_bin_desc' => 'Slettede hylder, bøger, kapitler og sider overføres til papirkurven, så de kan gendannes eller slettes permanent. Ældre elementer i papirkurven fjernes automatisk efter et stykke tid afhængigt af systemets konfiguration.',\n    'maint_recycle_bin_open' => 'Åbn papirkurven',\n    'maint_regen_references' => 'Regenerer Referencer',\n    'maint_regen_references_desc' => 'Denne handling vil genopbygge referenceindekset på tværs af elementer i databasen. Dette håndteres normalt automatisk, men denne handling kan være nyttig til at indeksere gammelt indhold eller indhold, der er tilføjet via uofficielle metoder.',\n    'maint_regen_references_success' => 'Referenceindekset er blevet genskabt!',\n    'maint_timeout_command_note' => 'Bemærk: Denne handling kan tage tid at udføre, hvilket kan føre til timeout-problemer i nogle webmiljøer. Som et alternativ kan denne handling udføres med en terminalkommando.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Papirkurv',\n    'recycle_bin_desc' => 'Her kan du gendanne elementer, der er blevet slettet eller vælge at permanent fjerne dem fra systemet. Denne liste er ufiltreret, i modsætning til lignende aktivitetslister i systemet, hvor tilladelsesfiltre anvendes.',\n    'recycle_bin_deleted_item' => 'Slettet element',\n    'recycle_bin_deleted_parent' => 'Overordnet',\n    'recycle_bin_deleted_by' => 'Slettet af',\n    'recycle_bin_deleted_at' => 'Sletningstidspunkt',\n    'recycle_bin_permanently_delete' => 'Slet permanent',\n    'recycle_bin_restore' => 'Gendan',\n    'recycle_bin_contents_empty' => 'Papirkurven er tom',\n    'recycle_bin_empty' => 'Tøm papirkurv',\n    'recycle_bin_empty_confirm' => 'Dette vil permanent slette alle elementer i papirkurven, inkluderet hvert elements indhold. Er du sikker på, at du vil tømme papirkurven?',\n    'recycle_bin_destroy_confirm' => 'Denne handling vil permanent slette dette element fra systemet sammen med alle underordnede elementer, der er anført nedenfor, og du vil ikke kunne gendanne dette indhold. Er du sikker på, at du vil slette dette element permanent?',\n    'recycle_bin_destroy_list' => 'Elementer der skal slettes',\n    'recycle_bin_restore_list' => 'Elementer der skal gendannes',\n    'recycle_bin_restore_confirm' => 'Denne handling vil gendanne det slettede element, herunder alle underelementer, til deres oprindelige placering. Hvis den oprindelige placering siden er blevet slettet, og nu er i papirkurven, vil det overordnede element også skulle gendannes.',\n    'recycle_bin_restore_deleted_parent' => 'Det overordnede element til dette element er også blevet slettet. Disse vil forblive slettet indtil det overordnede også er gendannet.',\n    'recycle_bin_restore_parent' => 'Gendan Overordnet',\n    'recycle_bin_destroy_notification' => 'Slettede :count elementer fra papirkurven.',\n    'recycle_bin_restore_notification' => 'Gendannede :count elementer fra papirkurven.',\n\n    // Audit Log\n    'audit' => 'Revisionslog',\n    'audit_desc' => 'Denne revisionslog viser en liste over aktiviteter sporet i systemet. Denne liste er ufiltreret i modsætning til lignende aktivitetslister i systemet, hvor tilladelsesfiltre anvendes.',\n    'audit_event_filter' => 'Event filter',\n    'audit_event_filter_no_filter' => 'Intet filter',\n    'audit_deleted_item' => 'Element slettet',\n    'audit_deleted_item_name' => 'Navn: :name',\n    'audit_table_user' => 'Bruger',\n    'audit_table_event' => 'Hændelse',\n    'audit_table_related' => 'Relateret element eller detalje',\n    'audit_table_ip' => 'IP-adresse',\n    'audit_table_date' => 'Aktivitetsdato',\n    'audit_date_from' => 'Datointerval fra',\n    'audit_date_to' => 'Datointerval til',\n\n    // Role Settings\n    'roles' => 'Roller',\n    'role_user_roles' => 'Brugerroller',\n    'roles_index_desc' => 'Roller bruges til at gruppere brugere og give systemtilladelser til deres medlemmer. Når en bruger er medlem af flere roller, stables de tildelte rettigheder, og brugeren arver alle evner.',\n    'roles_x_users_assigned' => ':count bruger tildelt:count brugere tildelt',\n    'roles_x_permissions_provided' => ':count tilladelser: count tilladelser',\n    'roles_assigned_users' => 'Tildelte brugere',\n    'roles_permissions_provided' => 'Givne tilladelser',\n    'role_create' => 'Opret en ny rolle',\n    'role_delete' => 'Slet rolle',\n    'role_delete_confirm' => 'Dette vil slette rollen med navnet \\':roleName\\'.',\n    'role_delete_users_assigned' => 'Denne rolle er tildelt :userCount brugere. Hvis du vil rykke disse brugere fra denne rolle, kan du vælge en ny nedenunder.',\n    'role_delete_no_migration' => \"Ryk ikke brugere\",\n    'role_delete_sure' => 'Er du sikker på, at du vil slette denne rolle?',\n    'role_edit' => 'Rediger rolle',\n    'role_details' => 'Rolledetaljer',\n    'role_name' => 'Rollenavn',\n    'role_desc' => 'Kort beskrivelse af rolle',\n    'role_mfa_enforced' => 'Kræver multifaktor godkendelse',\n    'role_external_auth_id' => 'Eksterne godkendelses-IDer',\n    'role_system' => 'Systemtilladelser',\n    'role_manage_users' => 'Administrere brugere',\n    'role_manage_roles' => 'Administrer roller & rollerettigheder',\n    'role_manage_entity_permissions' => 'Administrer alle bog-, kapitel- & side-rettigheder',\n    'role_manage_own_entity_permissions' => 'Administrer tilladelser på egne bøger, kapitler og sider',\n    'role_manage_page_templates' => 'Administrer side-skabeloner',\n    'role_access_api' => 'Tilgå system-API',\n    'role_manage_settings' => 'Administrer app-indstillinger',\n    'role_export_content' => 'Eksporter indhold',\n    'role_import_content' => 'Importer indhold',\n    'role_editor_change' => 'Skift side editor',\n    'role_notifications' => 'Modtag og administrer notifikationer',\n    'role_permission_note_users_and_roles' => 'Disse tilladelser vil teknisk set også give synlighed og søgning efter brugere og roller i systemet.',\n    'role_asset' => 'Tilladelser for medier og \"assets\"',\n    'roles_system_warning' => 'Vær opmærksom på, at adgang til alle af de ovennævnte tre tilladelser, kan give en bruger mulighed for at ændre deres egne brugerrettigheder eller brugerrettigheder for andre i systemet. Tildel kun roller med disse tilladelser til betroede brugere.',\n    'role_asset_desc' => 'Disse tilladelser kontrollerer standardadgang til medier og \"assets\" i systemet. Tilladelser til bøger, kapitler og sider tilsidesætter disse tilladelser.',\n    'role_asset_admins' => 'Administratorer får automatisk adgang til alt indhold, men disse indstillinger kan vise eller skjule UI-indstillinger.',\n    'role_asset_image_view_note' => 'Dette vedrører synlighed i billedhåndteringen. Den faktiske adgang til uploadede billedfiler vil afhænge af systemets billedlagringsindstilling.',\n    'role_asset_users_note' => 'Disse tilladelser vil teknisk set også give synlighed og søgning efter brugere i systemet.',\n    'role_all' => 'Alle',\n    'role_own' => 'Eget',\n    'role_controlled_by_asset' => 'Styres af det medie/\"asset\", de uploades til',\n    'role_save' => 'Gem rolle',\n    'role_users' => 'Brugere med denne rolle',\n    'role_users_none' => 'Ingen brugere er i øjeblikket tildelt denne rolle',\n\n    // Users\n    'users' => 'Brugere',\n    'users_index_desc' => 'Opret og administrer individuelle brugerkonti i systemet. Brugerkonti bruges til login og tilskrivning af indhold og aktivitet. Adgangstilladelser er primært rollebaserede, men ejerskab af brugerindhold, blandt andre faktorer, kan også påvirke tilladelser og adgang.',\n    'user_profile' => 'Brugerprofil',\n    'users_add_new' => 'Tilføj ny bruger',\n    'users_search' => 'Søg efter brugere',\n    'users_latest_activity' => 'Seneste aktivitet',\n    'users_details' => 'Brugeroplysninger',\n    'users_details_desc' => 'Angiv et visningsnavn og en E-Mail-adresse for denne bruger. E-Mail-adressen bruges til at logge ind på applikationen.',\n    'users_details_desc_no_email' => 'Sætter et visningsnavn for denne bruger, så andre kan genkende dem.',\n    'users_role' => 'Brugerroller',\n    'users_role_desc' => 'Vælg hvilke roller denne bruger skal tildeles. Hvis en bruger er tildelt flere roller, sammenføres tilladelserne fra disse roller, og de får alle evnerne fra de tildelte roller.',\n    'users_password' => 'Brugeradgangskode',\n    'users_password_desc' => 'Sæt et kodeord, der bruges til at logge på applikationen. Dette skal være mindst 8 tegn langt.',\n    'users_send_invite_text' => 'Du kan vælge at sende denne bruger en invitation på E-Mail, som giver dem mulighed for at indstille deres egen adgangskode, ellers kan du indstille deres adgangskode selv.',\n    'users_send_invite_option' => 'Send bruger en invitationsmail',\n    'users_external_auth_id' => 'Ekstern godkendelses ID',\n    'users_external_auth_id_desc' => 'Når et eksternt godkendelsessystem er i brug (f.eks. SAML2, OIDC eller LDAP), er dette det ID, som forbinder denne BookStack-bruger med godkendelsessystemets konto. Du kan ignorere dette felt, hvis du bruger den e-mailbaserede standardgodkendelse.',\n    'users_password_warning' => 'Udfyld kun nedenstående, hvis du ønsker at ændre adgangskoden for denne bruger.',\n    'users_system_public' => 'Denne bruger repræsenterer alle gæstebrugere, der besøger din instans. Den kan ikke bruges til at logge på, men tildeles automatisk.',\n    'users_delete' => 'Slet bruger',\n    'users_delete_named' => 'Slet bruger :userName',\n    'users_delete_warning' => 'Dette vil helt slette denne bruger med navnet \\':userName\\' fra systemet.',\n    'users_delete_confirm' => 'Er du sikker på, at du vil slette denne bruger?',\n    'users_migrate_ownership' => 'Overfør ejerskab',\n    'users_migrate_ownership_desc' => 'Vælg en bruger her, hvis du vil have en anden bruger til at blive ejer af alle elementer, der i øjeblikket ejes af denne bruger.',\n    'users_none_selected' => 'Ingen bruger valgt',\n    'users_edit' => 'Rediger bruger',\n    'users_edit_profile' => 'Rediger profil',\n    'users_avatar' => 'Brugeravatar',\n    'users_avatar_desc' => 'Vælg et billede for at repræsentere denne bruger. Dette skal være ca. 256px kvadratisk.',\n    'users_preferred_language' => 'Foretrukket sprog',\n    'users_preferred_language_desc' => 'Denne indstilling ændrer det sprog, der bruges til applikationens brugergrænseflade. Dette påvirker ikke noget brugeroprettet indhold.',\n    'users_social_accounts' => 'Sociale konti',\n    'users_social_accounts_desc' => 'Se status for de tilsluttede sociale konti for denne bruger. Sociale konti kan bruges som supplement til det primære godkendelsessystem til systemadgang.',\n    'users_social_accounts_info' => 'Her kan du forbinde dine andre konti for hurtigere og lettere login. Afbrydelse af en konto her tilbagekalder ikke tidligere autoriseret adgang. Tilbagekald adgang fra dine profilindstillinger på den tilsluttede sociale konto.',\n    'users_social_connect' => 'Forbind konto',\n    'users_social_disconnect' => 'Frakobl konto',\n    'users_social_status_connected' => 'Tilsluttet',\n    'users_social_status_disconnected' => 'Afbrudt',\n    'users_social_connected' => ':socialAccount kontoen blev knyttet til din profil.',\n    'users_social_disconnected' => ':socialAccount kontoen blev afbrudt fra din profil.',\n    'users_api_tokens' => 'API Tokens',\n    'users_api_tokens_desc' => 'Opret og administrer de adgangstokens, der bruges til at godkende med BookStack REST API. Tilladelser til API\\'en administreres via den bruger, som tokenet tilhører.',\n    'users_api_tokens_none' => 'Ingen API tokens er blevet oprettet for denne bruger',\n    'users_api_tokens_create' => 'Opret Token',\n    'users_api_tokens_expires' => 'Udløber',\n    'users_api_tokens_docs' => 'API-dokumentation',\n    'users_mfa' => 'Multi-faktor godkendelse',\n    'users_mfa_desc' => 'Opsæt multi-faktor godkendelse som et ekstra lag af sikkerhed for din brugerkonto.',\n    'users_mfa_x_methods' => ':count metode konfigureret|:count metoder konfigureret',\n    'users_mfa_configure' => 'Konfigurer metoder',\n\n    // API Tokens\n    'user_api_token_create' => 'Opret API-token',\n    'user_api_token_name' => 'Navn',\n    'user_api_token_name_desc' => 'Giv din token et læsbart navn som en fremtidig påmindelse om dets tilsigtede formål.',\n    'user_api_token_expiry' => 'Udløbsdato',\n    'user_api_token_expiry_desc' => 'Indstil en dato, hvorpå denne token udløber. Efter denne dato fungerer anmodninger, der er lavet med denne token, ikke længere. Hvis du lader dette felt være tomt, udløber den 100 år ud i fremtiden.',\n    'user_api_token_create_secret_message' => 'Umiddelbart efter oprettelse af denne token genereres og vises et \"Token-ID\" og Token hemmelighed\". Hemmeligheden vises kun en gang, så husk at kopiere værdien til et sikkert sted inden du fortsætter.',\n    'user_api_token' => 'API Token',\n    'user_api_token_id' => 'Token-ID',\n    'user_api_token_id_desc' => 'Dette er en ikke-redigerbar systemgenereret identifikator for denne token, som skal sendes i API-anmodninger.',\n    'user_api_token_secret' => 'Token hemmelighed',\n    'user_api_token_secret_desc' => 'Dette er et system genereret hemmelighed for denne token, som skal sendes i API-anmodninger. Dette vises kun denne ene gang, så kopier denne værdi til et sikkert sted.',\n    'user_api_token_created' => 'Token oprettet :timeAgo',\n    'user_api_token_updated' => 'Token opdateret :timeAgo',\n    'user_api_token_delete' => 'Slet Token',\n    'user_api_token_delete_warning' => 'Dette vil helt slette API-token\\'en med navnet \\':tokenName\\' fra systemet.',\n    'user_api_token_delete_confirm' => 'Er du sikker på, at du vil slette denne API-token?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhooks er en måde at sende data til eksterne URL\\'er på, når bestemte handlinger og hændelser sker i systemet, hvilket giver mulighed for hændelsesbaseret integration med eksterne platforme som f.eks. besked- eller notifikationssystemer.',\n    'webhooks_x_trigger_events' => ':count trigger begivenhed:count trigger events',\n    'webhooks_create' => 'Opret ny Webhook',\n    'webhooks_none_created' => 'Ingen webhooks er blevet oprettet endnu.',\n    'webhooks_edit' => 'Rediger Webhook',\n    'webhooks_save' => 'Gem Webhook',\n    'webhooks_details' => 'Webhook detaljer',\n    'webhooks_details_desc' => 'Angiv et brugervenligt navn og et POST endpoint som en lokation for webhook data at blive sendt til.',\n    'webhooks_events' => 'Webhook Begivenheder',\n    'webhooks_events_desc' => 'Vælg alle begivenheder der skal udløse denne webhook til at blive kaldt.',\n    'webhooks_events_warning' => 'Husk, at disse begivenheder vil blive udløst for alle valgte begivenheder, selv om brugerdefinerede tilladelser bliver anvendt. Sørg for, at brugen af denne webhook ikke vil afsløre fortroligt indhold.',\n    'webhooks_events_all' => 'Alle systemhændelser',\n    'webhooks_name' => 'Webhook Navn',\n    'webhooks_timeout' => 'Webhook forespørgsel timeout (Sekunder)',\n    'webhooks_endpoint' => 'Webhook Endpoint',\n    'webhooks_active' => 'Webhook Aktiv',\n    'webhook_events_table_header' => 'Begivenheder',\n    'webhooks_delete' => 'Slet Webhook',\n    'webhooks_delete_warning' => 'Dette vil helt slette denne webhook med navnet \\':webhookName\\' fra systemet.',\n    'webhooks_delete_confirm' => 'Er du sikker på at du vil slette denne webhook?',\n    'webhooks_format_example' => 'Webhook format eksempel',\n    'webhooks_format_example_desc' => 'Webhook data bliver sendt som en POST anmodning til det konfigurerede endpoint som JSON efter formatet nedenfor. Egenskaberne \"related_item\" og \"url\" er valgri og vil afhænge af den type begivenhed udløst.',\n    'webhooks_status' => 'Webhook Status',\n    'webhooks_last_called' => 'Sidst Kaldt:',\n    'webhooks_last_errored' => 'Sidst Fejlet:',\n    'webhooks_last_error_message' => 'Sidste fejlmeddelelse:',\n\n    // Licensing\n    'licenses' => 'Licenser',\n    'licenses_desc' => 'Denne side indeholder licensoplysninger for BookStack ud over de projekter og biblioteker, der bruges i BookStack. Mange af de nævnte projekter må kun bruges i udviklingssammenhæng.',\n    'licenses_bookstack' => 'BookStack-licens',\n    'licenses_php' => 'Licenser til PHP-biblioteker',\n    'licenses_js' => 'Licenser til JavaScript-biblioteker',\n    'licenses_other' => 'Andre licenser',\n    'license_details' => 'Licensoplysninger',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Catalansk',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'Hebraisk',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/da/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute skal være accepteret.',\n    'active_url'           => ':attribute er ikke en gyldig URL.',\n    'after'                => ':attribute skal være en dato efter :date.',\n    'alpha'                => ':attribute må kun indeholde bogstaver.',\n    'alpha_dash'           => ':attribute må kun bestå af bogstaver, tal, binde- og under-streger.',\n    'alpha_num'            => ':attribute må kun indeholde bogstaver og tal.',\n    'array'                => ':attribute skal være et array.',\n    'backup_codes'         => 'Den angivne kode er ikke gyldig eller er allerede brugt.',\n    'before'               => ':attribute skal være en dato før :date.',\n    'between'              => [\n        'numeric' => ':attribute skal være mellem :min og :max.',\n        'file'    => ':attribute skal være mellem :min og :max kilobytes.',\n        'string'  => ':attribute skal være mellem :min og :max tegn.',\n        'array'   => ':attribute skal have mellem :min og :max elementer.',\n    ],\n    'boolean'              => ':attribute-feltet skal være enten sandt eller falsk.',\n    'confirmed'            => ':attribute-bekræftelsen matcher ikke.',\n    'date'                 => ':attribute er ikke en gyldig dato.',\n    'date_format'          => ':attribute matcher ikke formatet :format.',\n    'different'            => ':attribute og :other skal være forskellige.',\n    'digits'               => ':attribute skal være :digits cifre.',\n    'digits_between'       => ':attribute skal være mellem :min og :max cifre.',\n    'email'                => ':attribute skal være en gyldig mail-adresse.',\n    'ends_with' => ':attribute skal slutte på en af følgende værdier: :values',\n    'file'                 => ':attribute skal leveres som en gyldig fil.',\n    'filled'               => ':attribute er obligatorisk.',\n    'gt'                   => [\n        'numeric' => ':attribute skal være større end :value.',\n        'file'    => ':attribute skal være større end :value kilobytes.',\n        'string'  => ':attribute skal have mere end :value tegn.',\n        'array'   => ':attribute skal indeholde mere end :value elementer.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute skal mindst være :value.',\n        'file'    => ':attribute skal være mindst :value kilobytes.',\n        'string'  => ':attribute skal indeholde mindst :value tegn.',\n        'array'   => ':attribute skal have :value elementer eller flere.',\n    ],\n    'exists'               => 'Den valgte :attribute er ikke gyldig.',\n    'image'                => ':attribute skal være et billede.',\n    'image_extension'      => ':attribute skal være et gyldigt og understøttet billedformat.',\n    'in'                   => 'Den valgte :attribute er ikke gyldig.',\n    'integer'              => ':attribute skal være et heltal.',\n    'ip'                   => ':attribute skal være en gyldig IP-adresse.',\n    'ipv4'                 => ':attribute skal være en gyldig IPv4-adresse.',\n    'ipv6'                 => ':attribute skal være en gyldig IPv6-adresse.',\n    'json'                 => ':attribute skal være en gyldig JSON-streng.',\n    'lt'                   => [\n        'numeric' => ':attribute skal være mindre end :value.',\n        'file'    => ':attribute skal være mindre end :value kilobytes.',\n        'string'  => ':attribute skal have mindre end :value tegn.',\n        'array'   => ':attribute skal indeholde mindre end :value elementer.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute skal være mindre end eller lig med :value.',\n        'file'    => 'The :attribute skal være mindre eller lig med :value kilobytes.',\n        'string'  => ':attribute skal maks være :value tegn.',\n        'array'   => ':attribute må ikke indeholde mere end :value elementer.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute må ikke overstige :max.',\n        'file'    => ':attribute må ikke overstige :max kilobytes.',\n        'string'  => ':attribute må ikke overstige :max. tegn.',\n        'array'   => ':attribute må ikke have mere end :max elementer.',\n    ],\n    'mimes'                => ':attribute skal være en fil af typen: :values.',\n    'min'                  => [\n        'numeric' => ':attribute skal mindst være :min.',\n        'file'    => ':attribute skal være mindst :min kilobytes.',\n        'string'  => ':attribute skal mindst være :min tegn.',\n        'array'   => ':attribute skal have mindst :min elementer.',\n    ],\n    'not_in'               => 'Den valgte :attribute er ikke gyldig.',\n    'not_regex'            => ':attribute-formatet er ugyldigt.',\n    'numeric'              => ':attribute skal være et tal.',\n    'regex'                => ':attribute-formatet er ugyldigt.',\n    'required'             => ':attribute er obligatorisk.',\n    'required_if'          => ':attribute skal udfyldes når :other er :value.',\n    'required_with'        => ':attribute skal udfyldes når :values er udfyldt.',\n    'required_with_all'    => ':attribute skal udfyldes når :values er udfyldt.',\n    'required_without'     => ':attribute skal udfyldes når :values ikke er udfyldt.',\n    'required_without_all' => ':attribute skal udfyldes når ingen af :values er udfyldt.',\n    'same'                 => ':attribute og :other skal være ens.',\n    'safe_url'             => 'Det angivne link kan være usikkert.',\n    'size'                 => [\n        'numeric' => ':attribute skal være :size.',\n        'file'    => ':attribute skal være :size kilobytes.',\n        'string'  => ':attribute skal være :size tegn.',\n        'array'   => ':attribute skal indeholde :size elementer.',\n    ],\n    'string'               => ':attribute skal være tekst.',\n    'timezone'             => ':attribute skal være en gyldig zone.',\n    'totp'                 => 'Den angivne kode er ikke gyldig eller er udløbet.',\n    'unique'               => ':attribute er allerede i brug.',\n    'url'                  => ':attribute-formatet er ugyldigt.',\n    'uploaded'             => 'Filen kunne ikke oploades. Serveren accepterer muligvis ikke filer af denne størrelse.',\n\n    'zip_file' => 'Attributten skal henvise til en fil i ZIP.',\n    'zip_file_size' => 'Filen :attribute må ikke overstige: størrelse MB.',\n    'zip_file_mime' => 'Attributten skal henvise til en fil af typen: validTypes, fundet:foundType.',\n    'zip_model_expected' => 'Data objekt forventet men \":type\" fundet.',\n    'zip_unique' => 'Attributten skal være unik for objekttypen i ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Adgangskodebekræftelse påkrævet',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/de/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'erstellte Seite',\n    'page_create_notification'    => 'Seite erfolgreich erstellt',\n    'page_update'                 => 'aktualisierte Seite',\n    'page_update_notification'    => 'Seite erfolgreich aktualisiert',\n    'page_delete'                 => 'löschte Seite',\n    'page_delete_notification'    => 'Seite erfolgreich gelöscht',\n    'page_restore'                => 'wiederherstellte Seite',\n    'page_restore_notification'   => 'Seite erfolgreich wiederhergestellt',\n    'page_move'                   => 'verschob Seite',\n    'page_move_notification'      => 'Seite erfolgreich verschoben',\n\n    // Chapters\n    'chapter_create'              => 'erstellte Kapitel',\n    'chapter_create_notification' => 'Kapitel erfolgreich erstellt',\n    'chapter_update'              => 'aktualisierte Kapitel',\n    'chapter_update_notification' => 'Kapitel erfolgreich aktualisiert',\n    'chapter_delete'              => 'löschte Kapitel',\n    'chapter_delete_notification' => 'Kapitel erfolgreich gelöscht',\n    'chapter_move'                => 'verschob Kapitel',\n    'chapter_move_notification' => 'Kapitel erfolgreich verschoben',\n\n    // Books\n    'book_create'                 => 'erstellte Buch',\n    'book_create_notification'    => 'Buch erfolgreich erstellt',\n    'book_create_from_chapter'              => 'konvertierte Kapitel zu Buch',\n    'book_create_from_chapter_notification' => 'Kapitel erfolgreich in ein Buch konvertiert',\n    'book_update'                 => 'aktualisierte Buch',\n    'book_update_notification'    => 'Buch erfolgreich aktualisiert',\n    'book_delete'                 => 'löschte Buch',\n    'book_delete_notification'    => 'Buch erfolgreich gelöscht',\n    'book_sort'                   => 'sortierte Buch',\n    'book_sort_notification'      => 'Buch wurde erfolgreich umsortiert',\n\n    // Bookshelves\n    'bookshelf_create'            => 'erstellte Regal',\n    'bookshelf_create_notification'    => 'Regal erfolgreich erstellt',\n    'bookshelf_create_from_book'    => 'konvertierte Buch zu Regal',\n    'bookshelf_create_from_book_notification'    => 'Buch erfolgreich in Regal konvertiert',\n    'bookshelf_update'                 => 'aktualisierte Regal',\n    'bookshelf_update_notification'    => 'Regal erfolgreich aktualisiert',\n    'bookshelf_delete'                 => 'löschte Regal',\n    'bookshelf_delete_notification'    => 'Regal erfolgreich gelöscht',\n\n    // Revisions\n    'revision_restore' => 'stellte Revision wieder her:',\n    'revision_delete' => 'löschte Revision',\n    'revision_delete_notification' => 'Revision erfolgreich gelöscht',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" wurde zu Ihren Favoriten hinzugefügt',\n    'favourite_remove_notification' => '\":name\" wurde aus Ihren Favoriten entfernt',\n\n    // Watching\n    'watch_update_level_notification' => 'Beobachtungseinstellungen erfolgreich aktualisiert',\n\n    // Auth\n    'auth_login' => 'loggte sich ein',\n    'auth_register' => 'registrierte sich als neuer User',\n    'auth_password_reset_request' => 'forderte Rücksetzung des Benutzerpassworts an',\n    'auth_password_reset_update' => 'setzte Benutzerpasswort zurück',\n    'mfa_setup_method' => 'konfigurierte MFA-Methode',\n    'mfa_setup_method_notification' => 'Multi-Faktor-Methode erfolgreich konfiguriert',\n    'mfa_remove_method' => 'entfernte MFA-Methode',\n    'mfa_remove_method_notification' => 'Multi-Faktor-Methode erfolgreich entfernt',\n\n    // Settings\n    'settings_update' => 'aktualisierte Einstellungen',\n    'settings_update_notification' => 'Einstellungen erfolgreich aktualisiert',\n    'maintenance_action_run' => 'führte Wartungsaktion aus',\n\n    // Webhooks\n    'webhook_create' => 'erstellte Webhook',\n    'webhook_create_notification' => 'Webhook erfolgreich erstellt',\n    'webhook_update' => 'aktualisierte Webhook',\n    'webhook_update_notification' => 'Webhook erfolgreich aktualisiert',\n    'webhook_delete' => 'löschte Webhook',\n    'webhook_delete_notification' => 'Webhook erfolgreich gelöscht',\n\n    // Imports\n    'import_create' => 'Import erstellt',\n    'import_create_notification' => 'Import erfolgreich hochgeladen',\n    'import_run' => 'Import aktualisiert',\n    'import_run_notification' => 'Inhalt erfolgreich importiert',\n    'import_delete' => 'Import gelöscht',\n    'import_delete_notification' => 'Import erfolgreich gelöscht',\n\n    // Users\n    'user_create' => 'hat Benutzer erzeugt:',\n    'user_create_notification' => 'Benutzer erfolgreich erstellt',\n    'user_update' => 'hat Benutzer aktualisiert:',\n    'user_update_notification' => 'Benutzer erfolgreich aktualisiert',\n    'user_delete' => 'hat Benutzer gelöscht: ',\n    'user_delete_notification' => 'Benutzer erfolgreich entfernt',\n\n    // API Tokens\n    'api_token_create' => 'API-Token erstellt',\n    'api_token_create_notification' => 'API-Token erfolgreich erstellt',\n    'api_token_update' => 'API-Token aktualisiert',\n    'api_token_update_notification' => 'API-Token erfolgreich aktualisiert',\n    'api_token_delete' => 'API-Token gelöscht',\n    'api_token_delete_notification' => 'API-Token erfolgreich gelöscht',\n\n    // Roles\n    'role_create' => 'hat Rolle erzeugt',\n    'role_create_notification' => 'Rolle erfolgreich angelegt',\n    'role_update' => 'hat Rolle aktualisiert',\n    'role_update_notification' => 'Rolle erfolgreich aktualisiert',\n    'role_delete' => 'hat Rolle gelöscht',\n    'role_delete_notification' => 'Rolle erfolgreich gelöscht',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'hat den Papierkorb geleert',\n    'recycle_bin_restore' => 'aus dem Papierkorb wiederhergestellt',\n    'recycle_bin_destroy' => 'aus dem Papierkorb gelöscht',\n\n    // Comments\n    'commented_on'                => 'hat einen Kommentar hinzugefügt',\n    'comment_create'              => 'Kommentar hinzugefügt',\n    'comment_update'              => 'Kommentar aktualisiert',\n    'comment_delete'              => 'Kommentar gelöscht',\n\n    // Sort Rules\n    'sort_rule_create' => 'hat eine Sortierregel erstellt',\n    'sort_rule_create_notification' => 'Sortierregel erfolgreich angelegt',\n    'sort_rule_update' => 'hat eine Sortierregel aktualisiert',\n    'sort_rule_update_notification' => 'Sortierregel erfolgreich aktualisiert',\n    'sort_rule_delete' => 'hat eine Sortierregel gelöscht',\n    'sort_rule_delete_notification' => 'Sortierregel erfolgreich gelöscht',\n\n    // Other\n    'permissions_update'          => 'hat die Berechtigungen aktualisiert',\n];\n"
  },
  {
    "path": "lang/de/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Diese Anmeldedaten stimmen nicht mit unseren Aufzeichnungen überein.',\n    'throttle' => 'Zu viele Anmeldeversuche. Bitte versuchen Sie es in :seconds Sekunden erneut.',\n\n    // Login & Register\n    'sign_up' => 'Registrieren',\n    'log_in' => 'Anmelden',\n    'log_in_with' => 'Anmelden mit :socialDriver',\n    'sign_up_with' => 'Registrieren mit :socialDriver',\n    'logout' => 'Abmelden',\n\n    'name' => 'Name',\n    'username' => 'Benutzername',\n    'email' => 'E-Mail',\n    'password' => 'Passwort',\n    'password_confirm' => 'Passwort bestätigen',\n    'password_hint' => 'Muss mindestens 8 Zeichen lang sein',\n    'forgot_password' => 'Passwort vergessen?',\n    'remember_me' => 'Angemeldet bleiben',\n    'ldap_email_hint' => 'Bitte geben Sie eine E-Mail-Adresse ein, um diese mit dem Account zu nutzen.',\n    'create_account' => 'Account erstellen',\n    'already_have_account' => 'Sie haben bereits einen Account?',\n    'dont_have_account' => 'Sie haben noch keinen Account?',\n    'social_login' => 'Mit sozialem Netzwerk anmelden',\n    'social_registration' => 'Mit sozialem Netzwerk registrieren',\n    'social_registration_text' => 'Mit einem anderen Dienst registrieren oder anmelden.',\n\n    'register_thanks' => 'Vielen Dank für Ihre Registrierung!',\n    'register_confirm' => 'Bitte prüfen Sie Ihren Posteingang und bestätigen Sie die Registrierung, um :appName nutzen zu können.',\n    'registrations_disabled' => 'Eine Registrierung ist momentan nicht möglich',\n    'registration_email_domain_invalid' => 'Sie können sich mit dieser E-Mail-Adresse nicht für diese Anwendung registrieren',\n    'register_success' => 'Vielen Dank für Ihre Registrierung! Die Daten sind gespeichert und Sie sind angemeldet.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Anmeldeversuche',\n    'auto_init_starting_desc' => 'Wir verbinden uns mit Ihrem Authentifizierungssystem, um den Anmeldeprozess zu starten. Sollte es nach 5 Sekunden nicht weitergehen, klicken Sie bitte auf den unten stehenden Link.',\n    'auto_init_start_link' => 'Mit Authentifizierung fortfahren',\n\n    // Password Reset\n    'reset_password' => 'Passwort zurücksetzen',\n    'reset_password_send_instructions' => 'Bitte geben Sie Ihre E-Mail-Adresse ein. Danach erhalten Sie eine E-Mail mit einem Link zum Zurücksetzen Ihres Passwortes.',\n    'reset_password_send_button' => 'Link zum Zurücksetzen senden',\n    'reset_password_sent' => 'Ein Link zum Zurücksetzen des Passworts wird an :email gesendet, wenn diese E-Mail-Adresse im System gefunden wird.',\n    'reset_password_success' => 'Ihr Passwort wurde erfolgreich zurückgesetzt.',\n    'email_reset_subject' => 'Passwort zurücksetzen für :appName',\n    'email_reset_text' => 'Sie erhalten diese E-Mail, weil jemand versucht hat, Ihr Passwort zurückzusetzen.',\n    'email_reset_not_requested' => 'Wenn Sie das Zurücksetzen des Passworts nicht angefordert haben, ist keine weitere Aktion erforderlich.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Bestätigen Sie Ihre E-Mail-Adresse für :appName',\n    'email_confirm_greeting' => 'Danke, dass Sie sich für :appName registriert haben!',\n    'email_confirm_text' => 'Bitte bestätigen Sie Ihre E-Mail-Adresse, indem Sie auf die Schaltfläche klicken:',\n    'email_confirm_action' => 'E-Mail-Adresse bestätigen',\n    'email_confirm_send_error' => 'Leider konnte die für die Registrierung notwendige E-Mail zur Bestätigung Ihrer E-Mail-Adresse nicht versandt werden. Bitte kontaktieren Sie den Systemadministrator.',\n    'email_confirm_success' => 'Ihre E-Mail wurde bestätigt! Sie sollten nun in der Lage sein, sich mit dieser E-Mail-Adresse anzumelden.',\n    'email_confirm_resent' => 'Bestätigungs-E-Mail wurde erneut versendet, bitte überprüfen Sie Ihren Posteingang.',\n    'email_confirm_thanks' => 'Vielen Dank für das Bestätigen!',\n    'email_confirm_thanks_desc' => 'Bitte warten Sie einen Augenblick, während Ihre Bestätigung bearbeitet wird. Wenn Sie nach 3 Sekunden nicht weitergeleitet werden, drücken Sie unten den „Weiter“-Link, um fortzufahren.',\n\n    'email_not_confirmed' => 'E-Mail-Adresse ist nicht bestätigt',\n    'email_not_confirmed_text' => 'Ihre E-Mail-Adresse ist bisher nicht bestätigt.',\n    'email_not_confirmed_click_link' => 'Bitte klicken Sie auf den Link in der E-Mail, die Sie nach der Registrierung erhalten haben.',\n    'email_not_confirmed_resend' => 'Wenn Sie die E-Mail nicht erhalten haben, können Sie die Nachricht erneut anfordern. Füllen Sie hierzu bitte das folgende Formular aus.',\n    'email_not_confirmed_resend_button' => 'Bestätigungs-E-Mail erneut senden',\n\n    // User Invite\n    'user_invite_email_subject' => 'Sie wurden eingeladen, :appName beizutreten!',\n    'user_invite_email_greeting' => 'Ein Konto wurde für Sie auf :appName erstellt.',\n    'user_invite_email_text' => 'Klicken Sie auf die Schaltfläche unten, um ein Passwort festzulegen und Zugriff zu erhalten:',\n    'user_invite_email_action' => 'Account-Passwort festlegen',\n    'user_invite_page_welcome' => 'Willkommen bei :appName!',\n    'user_invite_page_text' => 'Um die Anmeldung abzuschließen und Zugriff auf :appName zu bekommen, muss noch ein Passwort festgelegt werden. Dieses wird in Zukunft für die Anmeldung benötigt.',\n    'user_invite_page_confirm_button' => 'Passwort bestätigen',\n    'user_invite_success_login' => 'Passwort gesetzt, Sie sollten nun in der Lage sein, sich mit Ihrem Passwort an :appName anzumelden!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Multi-Faktor-Authentifizierung einrichten',\n    'mfa_setup_desc' => 'Richten Sie Multi-Faktor-Authentifizierung als zusätzliche Sicherheitsstufe für Ihr Benutzerkonto ein.',\n    'mfa_setup_configured' => 'Bereits konfiguriert',\n    'mfa_setup_reconfigure' => 'Umkonfigurieren',\n    'mfa_setup_remove_confirmation' => 'Sind Sie sicher, dass Sie diese Multi-Faktor-Authentifizierungsmethode entfernen möchten?',\n    'mfa_setup_action' => 'Einrichtung',\n    'mfa_backup_codes_usage_limit_warning' => 'Sie haben weniger als 5 Backup-Codes übrig, bitte erstellen und speichern Sie ein neues Set, bevor Ihnen die Codes ausgehen, um zu verhindern, dass Sie aus Ihrem Konto ausgesperrt werden.',\n    'mfa_option_totp_title' => 'Handy-App',\n    'mfa_option_totp_desc' => 'Um Mehrfach-Faktor-Authentifizierung nutzen zu können, benötigen Sie eine Handy-Anwendung, die TOTP unterstützt, wie Google Authenticator, Authy oder Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Backup-Codes',\n    'mfa_option_backup_codes_desc' => 'Erzeugt eine Reihe von einmalig nutzbaren Backup-Codes, welche Sie bei der Anmeldung eingeben, um Ihre Identität zu bestätigen. Achten Sie darauf diese an einem sicheren Ort aufzubewahren.',\n    'mfa_gen_confirm_and_enable' => 'Bestätigen und aktivieren',\n    'mfa_gen_backup_codes_title' => 'Backup-Codes einrichten',\n    'mfa_gen_backup_codes_desc' => 'Speichern Sie die folgende Liste von Codes an einem sicheren Ort. Wenn Sie auf das System zugreifen, können Sie einen der Codes als zweiten Authentifizierungsmechanismus verwenden.',\n    'mfa_gen_backup_codes_download' => 'Codes herunterladen',\n    'mfa_gen_backup_codes_usage_warning' => 'Jeder Code kann nur einmal verwendet werden',\n    'mfa_gen_totp_title' => 'Handy-App einrichten',\n    'mfa_gen_totp_desc' => 'Um Multi-Faktor-Authentifizierung nutzen zu können, benötigen Sie eine Handy-Anwendung, die TOTP unterstützt, wie Google Authenticator, Authy oder Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Scannen Sie den QR-Code unten mit Ihrer bevorzugten Authentifizierungs-App, um loszulegen.',\n    'mfa_gen_totp_verify_setup' => 'Setup überprüfen',\n    'mfa_gen_totp_verify_setup_desc' => 'Überprüfen Sie, dass alles funktioniert, indem Sie einen von Ihrer Authentifizierungs-App generierten Code in das Eingabefeld unten eingeben:',\n    'mfa_gen_totp_provide_code_here' => 'Geben Sie hier Ihren App-generierten Code ein',\n    'mfa_verify_access' => 'Zugriff überprüfen',\n    'mfa_verify_access_desc' => 'Ihr Benutzerkonto erfordert, dass Sie Ihre Identität über eine zusätzliche Verifikationsebene bestätigen, bevor Sie Zugriff erhalten. Nutzen Sie dazu eine Ihrer konfigurierten Methoden, um fortzufahren.',\n    'mfa_verify_no_methods' => 'Keine Methoden konfiguriert',\n    'mfa_verify_no_methods_desc' => 'Es konnten keine Multi-Faktor-Authentifizierungsmethoden für Ihr Konto gefunden werden. Sie müssen mindestens eine Methode einrichten, bevor Sie Zugriff erhalten.',\n    'mfa_verify_use_totp' => 'Mit einer Handy-App verifizieren',\n    'mfa_verify_use_backup_codes' => 'Mit einem Backup-Code überprüfen',\n    'mfa_verify_backup_code' => 'Backup-Code',\n    'mfa_verify_backup_code_desc' => 'Geben Sie unten einen Ihrer verbleibenden Backup-Codes ein:',\n    'mfa_verify_backup_code_enter_here' => 'Backup-Code hier eingeben',\n    'mfa_verify_totp_desc' => 'Geben Sie den Code ein, der mit Ihrer Handy-App generiert wurde:',\n    'mfa_setup_login_notification' => 'Multi-Faktor-Methode konfiguriert. Bitte melden Sie sich jetzt erneut mit der konfigurierten Methode an.',\n];\n"
  },
  {
    "path": "lang/de/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Abbrechen',\n    'close' => 'Schließen',\n    'confirm' => 'Bestätigen',\n    'back' => 'Zurück',\n    'save' => 'Speichern',\n    'continue' => 'Fortfahren',\n    'select' => 'Auswählen',\n    'toggle_all' => 'Alle umschalten',\n    'more' => 'Mehr',\n\n    // Form Labels\n    'name' => 'Name',\n    'description' => 'Beschreibung',\n    'role' => 'Rolle',\n    'cover_image' => 'Titelbild',\n    'cover_image_description' => 'Dieses Bild sollte ungefähr 440x250px groß sein, obwohl es flexibel skaliert und beschnitten wird, um in verschiedenen Szenarien in die Benutzeroberfläche zu passen, sodass die tatsächlichen Abmessungen für die Anzeige abweichen können.',\n\n    // Actions\n    'actions' => 'Aktionen',\n    'view' => 'Anzeigen',\n    'view_all' => 'Alle anzeigen',\n    'new' => 'Neu',\n    'create' => 'Erstellen',\n    'update' => 'Aktualisieren',\n    'edit' => 'Bearbeiten',\n    'archive' => 'Archivieren',\n    'unarchive' => 'Nicht mehr archivieren',\n    'sort' => 'Sortieren',\n    'move' => 'Verschieben',\n    'copy' => 'Kopieren',\n    'reply' => 'Antworten',\n    'delete' => 'Löschen',\n    'delete_confirm' => 'Löschen bestätigen',\n    'search' => 'Suchen',\n    'search_clear' => 'Suche löschen',\n    'reset' => 'Zurücksetzen',\n    'remove' => 'Entfernen',\n    'add' => 'Hinzufügen',\n    'configure' => 'Konfigurieren',\n    'manage' => 'Verwalten',\n    'fullscreen' => 'Vollbild',\n    'favourite' => 'Favoriten',\n    'unfavourite' => 'Kein Favorit',\n    'next' => 'Nächste',\n    'previous' => 'Vorheriges',\n    'filter_active' => 'Gesetzte Filter:',\n    'filter_clear' => 'Filter löschen',\n    'download' => 'Herunterladen',\n    'open_in_tab' => 'In neuem Tab öffnen',\n    'open' => 'Öffnen',\n\n    // Sort Options\n    'sort_options' => 'Sortieroptionen',\n    'sort_direction_toggle' => 'Sortierreihenfolge umkehren',\n    'sort_ascending' => 'Aufsteigend sortieren',\n    'sort_descending' => 'Absteigend sortieren',\n    'sort_name' => 'Name',\n    'sort_default' => 'Standard',\n    'sort_created_at' => 'Erstellungsdatum',\n    'sort_updated_at' => 'Aktualisierungsdatum',\n\n    // Misc\n    'deleted_user' => 'Gelöschter Benutzer',\n    'no_activity' => 'Keine Aktivitäten zum Anzeigen',\n    'no_items' => 'Keine Einträge gefunden',\n    'back_to_top' => 'nach oben',\n    'skip_to_main_content' => 'Direkt zum Hauptinhalt',\n    'toggle_details' => 'Details zeigen/verstecken',\n    'toggle_thumbnails' => 'Thumbnails zeigen/verstecken',\n    'details' => 'Details',\n    'grid_view' => 'Gitteransicht',\n    'list_view' => 'Listenansicht',\n    'default' => 'Voreinstellung',\n    'breadcrumb' => 'Brotkrumen',\n    'status' => 'Status',\n    'status_active' => 'Aktiv',\n    'status_inactive' => 'Inaktiv',\n    'never' => 'Niemals',\n    'none' => 'Nichts',\n\n    // Header\n    'homepage' => 'Startseite',\n    'header_menu_expand' => 'Header-Menü erweitern',\n    'profile_menu' => 'Profilmenü',\n    'view_profile' => 'Profil ansehen',\n    'edit_profile' => 'Profil bearbeiten',\n    'dark_mode' => 'Dunkler Modus',\n    'light_mode' => 'Heller Modus',\n    'global_search' => 'Globale Suche',\n\n    // Layout tabs\n    'tab_info' => 'Info',\n    'tab_info_label' => 'Tab: Sekundäre Informationen anzeigen',\n    'tab_content' => 'Inhalt',\n    'tab_content_label' => 'Tab: Hauptinhalt anzeigen',\n\n    // Email Content\n    'email_action_help' => 'Sollte es beim Anklicken der Schaltfläche \":actionText\" Probleme geben, öffnen Sie folgende URL in Ihrem Browser:',\n    'email_rights' => 'Alle Rechte vorbehalten',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Datenschutzbestimmungen',\n    'terms_of_service' => 'Allgemeine Geschäftsbedingungen',\n\n    // OpenSearch\n    'opensearch_description' => 'Search :appName',\n];\n"
  },
  {
    "path": "lang/de/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Bild auswählen',\n    'image_list' => 'Bilderliste',\n    'image_details' => 'Bilddetails',\n    'image_upload' => 'Bild hochladen',\n    'image_intro' => 'Hier können Sie die zuvor hochgeladenen Bilder auswählen und verwalten.',\n    'image_intro_upload' => 'Laden Sie ein neues Bild hoch, indem Sie eine Bilddatei in dieses Fenster ziehen oder auf die Schaltfläche \"Bild hochladen\" oben klicken.',\n    'image_all' => 'Alle',\n    'image_all_title' => 'Alle Bilder anzeigen',\n    'image_book_title' => 'Zeige alle Bilder, die in dieses Buch hochgeladen wurden',\n    'image_page_title' => 'Zeige alle Bilder, die auf diese Seite hochgeladen wurden',\n    'image_search_hint' => 'Nach Bildnamen suchen',\n    'image_uploaded' => 'Hochgeladen am :uploadedDate',\n    'image_uploaded_by' => 'Hochgeladen von :userName',\n    'image_uploaded_to' => 'Hochgeladen auf :pageLink',\n    'image_updated' => 'Aktualisiert am :updateDate',\n    'image_load_more' => 'Mehr',\n    'image_image_name' => 'Bildname',\n    'image_delete_used' => 'Dieses Bild wird auf den folgenden Seiten benutzt. ',\n    'image_delete_confirm_text' => 'Möchten Sie dieses Bild wirklich löschen?',\n    'image_select_image' => 'Bild auswählen',\n    'image_dropzone' => 'Ziehen Sie Bilder hierher oder klicken Sie hier, um ein Bild auszuwählen',\n    'image_dropzone_drop' => 'Ziehen Sie Dateien hierher, um sie hochzuladen',\n    'images_deleted' => 'Bilder gelöscht',\n    'image_preview' => 'Bildvorschau',\n    'image_upload_success' => 'Bild erfolgreich hochgeladen',\n    'image_update_success' => 'Bilddetails erfolgreich aktualisiert',\n    'image_delete_success' => 'Bild erfolgreich gelöscht',\n    'image_replace' => 'Bild ersetzen',\n    'image_replace_success' => 'Bild erfolgreich aktualisiert',\n    'image_rebuild_thumbs' => 'Größenvariationen neu generieren',\n    'image_rebuild_thumbs_success' => 'Bildgrößenvariationen erfolgreich neu erstellt!',\n\n    // Code Editor\n    'code_editor' => 'Code editieren',\n    'code_language' => 'Code Sprache',\n    'code_content' => 'Code Inhalt',\n    'code_session_history' => 'Sitzungsverlauf',\n    'code_save' => 'Code speichern',\n];\n"
  },
  {
    "path": "lang/de/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Allgemein',\n    'advanced' => 'Erweitert',\n    'none' => 'Keine Auswahl',\n    'cancel' => 'Abbrechen',\n    'save' => 'Speichern',\n    'close' => 'Schließen',\n    'apply' => 'Übernehmen',\n    'undo' => 'Rückgängig',\n    'redo' => 'Wiederholen',\n    'left' => 'Links',\n    'center' => 'Zentriert',\n    'right' => 'Rechts',\n    'top' => 'Nach oben',\n    'middle' => 'Mittig',\n    'bottom' => 'Nach unten',\n    'width' => 'Breite',\n    'height' => 'Höhe',\n    'More' => 'Mehr',\n    'select' => 'Auswählen...',\n\n    // Toolbar\n    'formats' => 'Formate',\n    'header_large' => 'Große Überschrift',\n    'header_medium' => 'Mittlere Überschrift',\n    'header_small' => 'Kleine Überschrift',\n    'header_tiny' => 'Sehr kleine Überschrift',\n    'paragraph' => 'Absatz',\n    'blockquote' => 'Blockzitat',\n    'inline_code' => 'Inline-Code',\n    'callouts' => 'Anmerkungen',\n    'callout_information' => 'Info',\n    'callout_success' => 'Erfolgreich',\n    'callout_warning' => 'Warnung',\n    'callout_danger' => 'Achtung',\n    'bold' => 'Fett',\n    'italic' => 'Kursiv',\n    'underline' => 'Unterstrichen',\n    'strikethrough' => 'Durchgestrichen',\n    'superscript' => 'Hochgestellt',\n    'subscript' => 'Tiefgestellt',\n    'text_color' => 'Schriftfarbe',\n    'highlight_color' => 'Markierungsfarbe',\n    'custom_color' => 'Benutzerdefinierte Farbe',\n    'remove_color' => 'Farbe entfernen',\n    'background_color' => 'Hintergrundfarbe',\n    'align_left' => 'Linksbündig',\n    'align_center' => 'Zentriert',\n    'align_right' => 'Rechtsbündig',\n    'align_justify' => 'Blocksatz',\n    'list_bullet' => 'Liste',\n    'list_numbered' => 'Nummerierte Liste',\n    'list_task' => 'Aufgabenliste',\n    'indent_increase' => 'Einzug vergrößern',\n    'indent_decrease' => 'Einzug verkleinern',\n    'table' => 'Tabelle',\n    'insert_image' => 'Bild einfügen',\n    'insert_image_title' => 'Bild einfügen/ändern',\n    'insert_link' => 'Link einfügen/ändern',\n    'insert_link_title' => 'Link einfügen/ändern',\n    'insert_horizontal_line' => 'Horizontale Linie einfügen',\n    'insert_code_block' => 'Code-Block einfügen',\n    'edit_code_block' => 'Code-Block bearbeiten',\n    'insert_drawing' => 'Zeichnung einfügen/ändern',\n    'drawing_manager' => 'Zeichnungsmanager',\n    'insert_media' => 'Medien einfügen/ändern',\n    'insert_media_title' => 'Medien einfügen/ändern',\n    'clear_formatting' => 'Formatierung zurücksetzen',\n    'source_code' => 'Quellcode',\n    'source_code_title' => 'Quellcode',\n    'fullscreen' => 'Vollbild',\n    'image_options' => 'Bild-Optionen',\n\n    // Tables\n    'table_properties' => 'Tabelleneigenschaften',\n    'table_properties_title' => 'Tabelleneigenschaften',\n    'delete_table' => 'Tabelle löschen',\n    'table_clear_formatting' => 'Tabellenformatierung löschen',\n    'resize_to_contents' => 'Größe an Inhalt anpassen',\n    'row_header' => 'Zeilenkopf',\n    'insert_row_before' => 'Zeile davor einfügen',\n    'insert_row_after' => 'Zeile danach einfügen',\n    'delete_row' => 'Zeile löschen',\n    'insert_column_before' => 'Spalte davor einfügen',\n    'insert_column_after' => 'Spalte danach einfügen',\n    'delete_column' => 'Spalte löschen',\n    'table_cell' => 'Zelle',\n    'table_row' => 'Reihe',\n    'table_column' => 'Spalte',\n    'cell_properties' => 'Zelleneigenschaften',\n    'cell_properties_title' => 'Zelleneigenschaften',\n    'cell_type' => 'Zellentyp',\n    'cell_type_cell' => 'Zelle',\n    'cell_scope' => 'Zellbereich',\n    'cell_type_header' => 'Tabellen-Kopfzelle',\n    'merge_cells' => 'Zellen verbinden',\n    'split_cell' => 'Zellen teilen',\n    'table_row_group' => 'Zeilengruppe',\n    'table_column_group' => 'Spaltengruppe',\n    'horizontal_align' => 'Horizontal ausrichten',\n    'vertical_align' => 'Vertikal ausrichten',\n    'border_width' => 'Randbreite',\n    'border_style' => 'Randstil',\n    'border_color' => 'Randfarbe',\n    'row_properties' => 'Zeileneigenschaften',\n    'row_properties_title' => 'Zeileneigenschaften',\n    'cut_row' => 'Zeile ausschneiden',\n    'copy_row' => 'Zeile kopieren',\n    'paste_row_before' => 'Vor der Zeile einfügen',\n    'paste_row_after' => 'Nach der Zeile einfügen',\n    'row_type' => 'Zeilentyp',\n    'row_type_header' => 'Kopfzeile',\n    'row_type_body' => 'Hauptteil',\n    'row_type_footer' => 'Fußzeile',\n    'alignment' => 'Ausrichtung',\n    'cut_column' => 'Spalte ausschneiden',\n    'copy_column' => 'Spalte kopieren',\n    'paste_column_before' => 'Vor der Spalte einfügen',\n    'paste_column_after' => 'Nach der Spalte einfügen',\n    'cell_padding' => 'Zellenabstand',\n    'cell_spacing' => 'Zellen-Außenabstand',\n    'caption' => 'Beschriftung',\n    'show_caption' => 'Beschriftungen anzeigen',\n    'constrain' => 'Proportionen beschränken',\n    'cell_border_solid' => 'Voll',\n    'cell_border_dotted' => 'Gepunktet',\n    'cell_border_dashed' => 'Gestrichelt',\n    'cell_border_double' => 'Doppelt',\n    'cell_border_groove' => 'Rille',\n    'cell_border_ridge' => 'Erhaben',\n    'cell_border_inset' => 'vertiefte Fläche',\n    'cell_border_outset' => 'erhabene Fläche',\n    'cell_border_none' => 'Keine',\n    'cell_border_hidden' => 'Versteckt',\n\n    // Images, links, details/summary & embed\n    'source' => 'Quelle',\n    'alt_desc' => 'Alternative Beschreibung',\n    'embed' => 'Einbetten',\n    'paste_embed' => 'Fügen Sie Ihren Einbettungscode unten ein:',\n    'url' => 'URL',\n    'text_to_display' => 'Anzuzeigender Text',\n    'title' => 'Titel',\n    'browse_links' => 'Links durchsuchen',\n    'open_link' => 'Link öffnen',\n    'open_link_in' => 'Link öffnen in...',\n    'open_link_current' => 'Aktuelles Fenster',\n    'open_link_new' => 'Neues Fenster',\n    'remove_link' => 'Link entfernen',\n    'insert_collapsible' => 'Einklappbarer Block einfügen',\n    'collapsible_unwrap' => 'Auspacken',\n    'edit_label' => 'Label bearbeiten',\n    'toggle_open_closed' => 'Öffnen/Schließen',\n    'collapsible_edit' => 'Einklappbarer Block bearbeiten',\n    'toggle_label' => 'Label umschalten',\n\n    // About view\n    'about' => 'Über den Editor',\n    'about_title' => 'Über den WYSIWYG-Editor',\n    'editor_license' => 'Editorlizenz & Copyright',\n    'editor_lexical_license' => 'Dieser Editor wurde mithilfe von :lexicalLink erstellt, der unter der MIT-Lizenz bereitgestellt wird.',\n    'editor_lexical_license_link' => 'Vollständige Lizenzdetails finden Sie hier.',\n    'editor_tiny_license' => 'Dieser Editor wurde mithilfe von :tinyLink erstellt, der unter der MIT-Lizenz bereitgestellt wird.',\n    'editor_tiny_license_link' => 'Die Copyright- und Lizenzdetails von TinyMCE finden Sie hier.',\n    'save_continue' => 'Speichern & Fortfahren',\n    'callouts_cycle' => '(Drücken Sie weiter, um durch Typen umzuschalten)',\n    'link_selector' => 'Inhalt verlinken',\n    'shortcuts' => 'Verknüpfungen',\n    'shortcut' => 'Verknüpfung',\n    'shortcuts_intro' => 'Die folgenden Verknüpfungen sind im Editor verfügbar:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Beschreibung',\n];\n"
  },
  {
    "path": "lang/de/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Kürzlich angelegt',\n    'recently_created_pages' => 'Kürzlich angelegte Seiten',\n    'recently_updated_pages' => 'Kürzlich aktualisierte Seiten',\n    'recently_created_chapters' => 'Kürzlich angelegte Kapitel',\n    'recently_created_books' => 'Kürzlich angelegte Bücher',\n    'recently_created_shelves' => 'Kürzlich angelegte Regale',\n    'recently_update' => 'Kürzlich aktualisiert',\n    'recently_viewed' => 'Kürzlich angesehen',\n    'recent_activity' => 'Kürzliche Aktivität',\n    'create_now' => 'Jetzt anlegen',\n    'revisions' => 'Versionen',\n    'meta_revision' => 'Version #:revisionCount',\n    'meta_created' => 'Erstellt: :timeLength',\n    'meta_created_name' => 'Erstellt: :timeLength von :user',\n    'meta_updated' => 'Zuletzt aktualisiert: :timeLength',\n    'meta_updated_name' => 'Zuletzt aktualisiert: :timeLength von :user',\n    'meta_owned_name' => 'Im Besitz von :user',\n    'meta_reference_count' => 'Referenziert von :count Element|Referenziert von :count Elementen',\n    'entity_select' => 'Eintrag auswählen',\n    'entity_select_lack_permission' => 'Sie haben nicht die benötigte Berechtigung, um dieses Element auszuwählen',\n    'images' => 'Bilder',\n    'my_recent_drafts' => 'Meine kürzlichen Entwürfe',\n    'my_recently_viewed' => 'Kürzlich von mir angesehen',\n    'my_most_viewed_favourites' => 'Meine meistgesehenen Favoriten',\n    'my_favourites' => 'Meine Favoriten',\n    'no_pages_viewed' => 'Sie haben bisher keine Seiten angesehen',\n    'no_pages_recently_created' => 'Sie haben bisher keine Seiten angelegt',\n    'no_pages_recently_updated' => 'Sie haben bisher keine Seiten aktualisiert',\n    'export' => 'Exportieren',\n    'export_html' => 'HTML-Datei',\n    'export_pdf' => 'PDF-Datei',\n    'export_text' => 'Textdatei',\n    'export_md' => 'Markdown-Datei',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Standard-Seitenvorlage',\n    'default_template_explain' => 'Bestimmen Sie eine Seitenvorlage, die als Standardinhalt für alle Seiten verwendet wird, die innerhalb dieses Elements erstellt werden. Beachten Sie, dass dies nur dann verwendet wird, wenn der Ersteller der Seite Lesezugriff auf die ausgewählte Vorlagen-Seite hat.',\n    'default_template_select' => 'Wählen Sie eine Seitenvorlage',\n    'import' => 'Import',\n    'import_validate' => 'Import validieren',\n    'import_desc' => 'Importieren Sie Bücher, Kapitel & Seiten mit einem \"Portable Zip-Export\" von der gleichen oder einer anderen Instanz. Wählen Sie eine ZIP-Datei, um fortzufahren. Nachdem die Datei hochgeladen und bestätigt wurde, können Sie den Import in der nächsten Ansicht konfigurieren und bestätigen.',\n    'import_zip_select' => 'ZIP-Datei zum Hochladen auswählen',\n    'import_zip_validation_errors' => 'Fehler bei der Validierung der angegebenen ZIP-Datei:',\n    'import_pending' => 'Ausstehende Importe',\n    'import_pending_none' => 'Es wurden keine Importe gestartet.',\n    'import_continue' => 'Import fortsetzen',\n    'import_continue_desc' => 'Überprüfen Sie den Inhalt, der aus der hochgeladenen ZIP-Datei importiert werden soll. Führen Sie den Import aus, um dessen Inhalt zu diesem System hinzuzufügen. Die hochgeladene ZIP-Importdatei wird bei erfolgreichem Import automatisch entfernt.',\n    'import_details' => 'Einzelheiten zum Import',\n    'import_run' => 'Import starten',\n    'import_size' => ':size Import ZIP Größe',\n    'import_uploaded_at' => 'Hochgeladen :relativeTime',\n    'import_uploaded_by' => 'Hochgeladen von',\n    'import_location' => 'Import Ort',\n    'import_location_desc' => 'Wählen Sie einen Zielort für Ihren importierten Inhalt. Sie benötigen die entsprechenden Berechtigungen, um innerhalb des gewünschten Standortes zu erstellen.',\n    'import_delete_confirm' => 'Sind Sie sicher, dass Sie diesen Import löschen möchten?',\n    'import_delete_desc' => 'Dies löscht die hochgeladene ZIP-Datei und kann nicht rückgängig gemacht werden.',\n    'import_errors' => 'Importfehler',\n    'import_errors_desc' => 'Die folgenden Fehler sind während des Importversuchs aufgetreten:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigiere in Büchern',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Berechtigungen',\n    'permissions_desc' => 'Legen Sie hier Berechtigungen fest, um die Standardberechtigungen von Benutzerrollen zu überschreiben.',\n    'permissions_book_cascade' => 'In Büchern festgelegte Berechtigungen werden automatisch in untergeordnete Kapitel und Seiten kaskadiert, es sei denn, sie haben eigene Berechtigungen definiert.',\n    'permissions_chapter_cascade' => 'In Kapiteln festgelegte Berechtigungen werden automatisch in untergeordnete Seiten kaskadiert, es sei denn, sie haben eigene Berechtigungen definiert.',\n    'permissions_save' => 'Berechtigungen speichern',\n    'permissions_owner' => 'Besitzer',\n    'permissions_role_everyone_else' => 'Alle anderen',\n    'permissions_role_everyone_else_desc' => 'Berechtigungen für alle Rollen setzen, die nicht explizit überschrieben wurden.',\n    'permissions_role_override' => 'Berechtigungen für Rolle überschreiben',\n    'permissions_inherit_defaults' => 'Standardeinstellungen vererben',\n\n    // Search\n    'search_results' => 'Suchergebnisse',\n    'search_total_results_found' => ':count Ergebnis gefunden|:count Ergebnisse gesamt',\n    'search_clear' => 'Filter löschen',\n    'search_no_pages' => 'Keine Seiten gefunden',\n    'search_for_term' => 'Nach :term suchen',\n    'search_more' => 'Mehr Ergebnisse',\n    'search_advanced' => 'Erweiterte Suche',\n    'search_terms' => 'Suchbegriffe',\n    'search_content_type' => 'Inhaltstyp',\n    'search_exact_matches' => 'Exakte Treffer',\n    'search_tags' => 'Schlagwort-Suchen',\n    'search_options' => 'Optionen',\n    'search_viewed_by_me' => 'Schon von mir angesehen',\n    'search_not_viewed_by_me' => 'Noch nicht von mir angesehen',\n    'search_permissions_set' => 'Berechtigungen gesetzt',\n    'search_created_by_me' => 'Von mir erstellt',\n    'search_updated_by_me' => 'Von mir aktualisiert',\n    'search_owned_by_me' => 'In meinem Besitz',\n    'search_date_options' => 'Datums Optionen',\n    'search_updated_before' => 'Aktualisiert vor',\n    'search_updated_after' => 'Aktualisiert nach',\n    'search_created_before' => 'Erstellt vor',\n    'search_created_after' => 'Erstellt nach',\n    'search_set_date' => 'Datum auswählen',\n    'search_update' => 'Suche aktualisieren',\n\n    // Shelves\n    'shelf' => 'Regal',\n    'shelves' => 'Regale',\n    'x_shelves' => ':count Regal|:count Regale',\n    'shelves_empty' => 'Es wurden noch keine Regale angelegt',\n    'shelves_create' => 'Erzeuge ein Regal',\n    'shelves_popular' => 'Beliebte Regale',\n    'shelves_new' => 'Kürzlich erstellte Regale',\n    'shelves_new_action' => 'Neues Regal',\n    'shelves_popular_empty' => 'Die beliebtesten Regale werden hier angezeigt.',\n    'shelves_new_empty' => 'Die neusten Regale werden hier angezeigt.',\n    'shelves_save' => 'Regal speichern',\n    'shelves_books' => 'Bücher in diesem Regal',\n    'shelves_add_books' => 'Buch zu diesem Regal hinzufügen',\n    'shelves_drag_books' => 'Ziehen Sie Bücher nach unten, um sie diesem Regal hinzuzufügen',\n    'shelves_empty_contents' => 'Diesem Regal sind keine Bücher zugewiesen',\n    'shelves_edit_and_assign' => 'Regal bearbeiten um Bücher hinzuzufügen',\n    'shelves_edit_named' => 'Regal :name bearbeiten',\n    'shelves_edit' => 'Regal bearbeiten',\n    'shelves_delete' => 'Regal löschen',\n    'shelves_delete_named' => 'Regal :name löschen',\n    'shelves_delete_explain' => \"Dadurch wird das Regal mit dem Namen ':name' gelöscht. Die darin enthaltenen Bücher werden nicht gelöscht.\",\n    'shelves_delete_confirmation' => 'Sind Sie sicher, dass Sie dieses Regal löschen möchten?',\n    'shelves_permissions' => 'Regalberechtigungen',\n    'shelves_permissions_updated' => 'Regalberechtigungen aktualisiert',\n    'shelves_permissions_active' => 'Regalberechtigungen aktiv',\n    'shelves_permissions_cascade_warning' => 'Berechtigungen für Regale werden nicht automatisch auf die enthaltenen Bücher übertragen. Das liegt daran, dass ein Buch in mehreren Regalen vorhanden sein kann. Berechtigungen können jedoch auf untergeordnete Bücher kopiert werden, indem Sie die unten stehende Option verwenden.',\n    'shelves_permissions_create' => 'Regalerstellungsberechtigungen werden nur zum Kopieren von Berechtigungen für untergeordnete Bücher mit der folgenden Aktion verwendet. Sie kontrollieren nicht die Fähigkeit, Bücher zu erstellen.',\n    'shelves_copy_permissions_to_books' => 'Kopiere die Berechtigungen zum Buch',\n    'shelves_copy_permissions' => 'Berechtigungen kopieren',\n    'shelves_copy_permissions_explain' => 'Dadurch werden die aktuellen Berechtigungseinstellungen dieses Regals auf alle darin enthaltenen Bücher angewendet. Vergewissern Sie sich vor der Aktivierung, dass alle Änderungen an den Berechtigungen für dieses Regal gespeichert wurden.',\n    'shelves_copy_permission_success' => 'Regalberechtigungen auf :count Bücher kopiert',\n\n    // Books\n    'book' => 'Buch',\n    'books' => 'Bücher',\n    'x_books' => ':count Buch|:count Bücher',\n    'books_empty' => 'Keine Bücher vorhanden',\n    'books_popular' => 'Beliebte Bücher',\n    'books_recent' => 'Kürzlich angesehene Bücher',\n    'books_new' => 'Neue Bücher',\n    'books_new_action' => 'Neues Buch',\n    'books_popular_empty' => 'Die beliebtesten Bücher werden hier angezeigt.',\n    'books_new_empty' => 'Die neusten Bücher werden hier angezeigt.',\n    'books_create' => 'Neues Buch erstellen',\n    'books_delete' => 'Buch löschen',\n    'books_delete_named' => 'Buch \":bookName\" löschen',\n    'books_delete_explain' => 'Das Buch \":bookName\" wird gelöscht und alle zugehörigen Kapitel und Seiten entfernt.',\n    'books_delete_confirmation' => 'Sind Sie sicher, dass Sie dieses Buch löschen möchten?',\n    'books_edit' => 'Buch bearbeiten',\n    'books_edit_named' => 'Buch \":bookName\" bearbeiten',\n    'books_form_book_name' => 'Name des Buches',\n    'books_save' => 'Buch speichern',\n    'books_permissions' => 'Buch-Berechtigungen',\n    'books_permissions_updated' => 'Buch-Berechtigungen aktualisiert',\n    'books_empty_contents' => 'Es sind noch keine Seiten oder Kapitel zu diesem Buch hinzugefügt worden.',\n    'books_empty_create_page' => 'Neue Seite anlegen',\n    'books_empty_sort_current_book' => 'Aktuelles Buch sortieren',\n    'books_empty_add_chapter' => 'Neues Kapitel hinzufügen',\n    'books_permissions_active' => 'Buch-Berechtigungen aktiv',\n    'books_search_this' => 'Dieses Buch durchsuchen',\n    'books_navigation' => 'Buchnavigation',\n    'books_sort' => 'Buchinhalte sortieren',\n    'books_sort_desc' => 'Kapitel und Seiten innerhalb eines Buches verschieben, um dessen Inhalt zu reorganisieren. Andere Bücher können hinzugefügt werden, was das Verschieben von Kapiteln und Seiten zwischen Büchern erleichtert. Optional kann eine automatische Sortierregel erstellt werden, um den Inhalt dieses Buches nach Änderungen automatisch zu sortieren.',\n    'books_sort_auto_sort' => 'Auto-Sortieroption',\n    'books_sort_auto_sort_active' => 'Automatische Sortierung aktiv: :sortName',\n    'books_sort_named' => 'Buch \":bookName\" sortieren',\n    'books_sort_name' => 'Sortieren nach Namen',\n    'books_sort_created' => 'Sortieren nach Erstellungsdatum',\n    'books_sort_updated' => 'Sortieren nach Aktualisierungsdatum',\n    'books_sort_chapters_first' => 'Kapitel zuerst',\n    'books_sort_chapters_last' => 'Kapitel zuletzt',\n    'books_sort_show_other' => 'Andere Bücher anzeigen',\n    'books_sort_save' => 'Neue Reihenfolge speichern',\n    'books_sort_show_other_desc' => 'Füge hier weitere Bücher hinzu, um sie in die Sortierung einzubinden und ermögliche so eine einfache und übergreifende Reorganisation.',\n    'books_sort_move_up' => 'Nach oben bewegen',\n    'books_sort_move_down' => 'Nach unten bewegen',\n    'books_sort_move_prev_book' => 'Zum vorherigen Buch verschieben',\n    'books_sort_move_next_book' => 'Zum nächsten Buch verschieben',\n    'books_sort_move_prev_chapter' => 'In das vorherige Kapitel verschieben',\n    'books_sort_move_next_chapter' => 'In nächstes Kapitel verschieben',\n    'books_sort_move_book_start' => 'Zum Buchbeginn verschieben',\n    'books_sort_move_book_end' => 'Zum Ende des Buches verschieben',\n    'books_sort_move_before_chapter' => 'Vor Kapitel verschieben',\n    'books_sort_move_after_chapter' => 'Nach Kapitel verschieben',\n    'books_copy' => 'Buch kopieren',\n    'books_copy_success' => 'Das Buch wurde erfolgreich kopiert',\n\n    // Chapters\n    'chapter' => 'Kapitel',\n    'chapters' => 'Kapitel',\n    'x_chapters' => ':count Kapitel',\n    'chapters_popular' => 'Beliebte Kapitel',\n    'chapters_new' => 'Neues Kapitel',\n    'chapters_create' => 'Neues Kapitel anlegen',\n    'chapters_delete' => 'Kapitel entfernen',\n    'chapters_delete_named' => 'Kapitel \":chapterName\" entfernen',\n    'chapters_delete_explain' => 'Dies löscht das Kapitel mit dem Namen \\':chapterName\\'. Alle Seiten, die innerhalb dieses Kapitels existieren, werden ebenfalls gelöscht.',\n    'chapters_delete_confirm' => 'Sind Sie sicher, dass Sie dieses Kapitel löschen möchten?',\n    'chapters_edit' => 'Kapitel bearbeiten',\n    'chapters_edit_named' => 'Kapitel \":chapterName\" bearbeiten',\n    'chapters_save' => 'Kapitel speichern',\n    'chapters_move' => 'Kapitel verschieben',\n    'chapters_move_named' => 'Kapitel \":chapterName\" verschieben',\n    'chapters_copy' => 'Kapitel kopieren',\n    'chapters_copy_success' => 'Kapitel erfolgreich kopiert',\n    'chapters_permissions' => 'Kapitel-Berechtigungen',\n    'chapters_empty' => 'Aktuell sind keine Kapitel diesem Buch hinzugefügt worden.',\n    'chapters_permissions_active' => 'Kapitel-Berechtigungen aktiv',\n    'chapters_permissions_success' => 'Kapitel-Berechtigungenen aktualisisert',\n    'chapters_search_this' => 'Dieses Kapitel durchsuchen',\n    'chapter_sort_book' => 'Buch sortieren',\n\n    // Pages\n    'page' => 'Seite',\n    'pages' => 'Seiten',\n    'x_pages' => ':count Seite|:count Seiten',\n    'pages_popular' => 'Beliebte Seiten',\n    'pages_new' => 'Neue Seite',\n    'pages_attachments' => 'Anhänge',\n    'pages_navigation' => 'Seitennavigation',\n    'pages_delete' => 'Seite löschen',\n    'pages_delete_named' => 'Seite \":pageName\" löschen',\n    'pages_delete_draft_named' => 'Seitenentwurf von \":pageName\" löschen',\n    'pages_delete_draft' => 'Seitenentwurf löschen',\n    'pages_delete_success' => 'Seite gelöscht',\n    'pages_delete_draft_success' => 'Seitenentwurf gelöscht',\n    'pages_delete_warning_template' => 'Diese Seite wird aktiv als Standardvorlage für Bücher oder Kapitel verwendet. In diesen Büchern oder Kapiteln wird nach dem Löschen dieser Seite keine Standardvorlage mehr zugewiesen sein.',\n    'pages_delete_confirm' => 'Sind Sie sicher, dass Sie diese Seite löschen möchen?',\n    'pages_delete_draft_confirm' => 'Sind Sie sicher, dass Sie diesen Seitenentwurf löschen möchten?',\n    'pages_editing_named' => 'Seite \":pageName\" bearbeiten',\n    'pages_edit_draft_options' => 'Entwurfsoptionen',\n    'pages_edit_save_draft' => 'Entwurf speichern',\n    'pages_edit_draft' => 'Seitenentwurf bearbeiten',\n    'pages_editing_draft' => 'Seitenentwurf bearbeiten',\n    'pages_editing_page' => 'Seite bearbeiten',\n    'pages_edit_draft_save_at' => 'Entwurf gesichert um ',\n    'pages_edit_delete_draft' => 'Entwurf löschen',\n    'pages_edit_delete_draft_confirm' => 'Sind Sie sicher, dass Sie Ihren Entwurf löschen möchten? Alle Ihre Änderungen seit dem letzten vollständigen Speichern gehen verloren und der Editor wird mit dem letzten Speicherzustand aktualisiert, der kein Entwurf ist.',\n    'pages_edit_discard_draft' => 'Entwurf verwerfen',\n    'pages_edit_switch_to_markdown' => 'Zum Markdown-Editor wechseln',\n    'pages_edit_switch_to_markdown_clean' => '(Gesäuberter Inhalt)',\n    'pages_edit_switch_to_markdown_stable' => '(Stabiler Inhalt)',\n    'pages_edit_switch_to_wysiwyg' => 'Zum WYSIWYG-Editor wechseln',\n    'pages_edit_switch_to_new_wysiwyg' => 'Zum neuen WYSIWYG wechseln',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(Im Beta-Test)',\n    'pages_edit_set_changelog' => 'Änderungsprotokoll hinzufügen',\n    'pages_edit_enter_changelog_desc' => 'Bitte geben Sie eine kurze Zusammenfassung Ihrer Änderungen ein',\n    'pages_edit_enter_changelog' => 'Änderungsprotokoll eingeben',\n    'pages_editor_switch_title' => 'Editor wechseln',\n    'pages_editor_switch_are_you_sure' => 'Sind Sie sicher, dass Sie den Editor für diese Seite ändern möchten?',\n    'pages_editor_switch_consider_following' => 'Betrachten Sie folgendes beim Ändern von Editoren:',\n    'pages_editor_switch_consideration_a' => 'Einmal gespeichert, wird die neue Editoroption von zukünftigen Editoren verwendet, einschließlich derjenigen, die nicht in der Lage sind, den Editortyp selbst zu ändern.',\n    'pages_editor_switch_consideration_b' => 'Dies kann unter bestimmten Umständen zu einem Verlust von Details und Quellcode führen.',\n    'pages_editor_switch_consideration_c' => 'Änderungen des Tags oder Änderungsprotokolls, die seit dem letzten Speichern vorgenommen wurden, werden bei dieser Änderung nicht fortgesetzt.',\n    'pages_save' => 'Seite speichern',\n    'pages_title' => 'Seitentitel',\n    'pages_name' => 'Seitenname',\n    'pages_md_editor' => 'Redakteur',\n    'pages_md_preview' => 'Vorschau',\n    'pages_md_insert_image' => 'Bild einfügen',\n    'pages_md_insert_link' => 'Link zu einem Objekt einfügen',\n    'pages_md_insert_drawing' => 'Zeichnung einfügen',\n    'pages_md_show_preview' => 'Vorschau anzeigen',\n    'pages_md_sync_scroll' => 'Vorschau synchronisieren',\n    'pages_md_plain_editor' => 'Einfacher Editor',\n    'pages_drawing_unsaved' => 'Ungespeicherte Zeichnung gefunden',\n    'pages_drawing_unsaved_confirm' => 'Es wurden ungespeicherte Zeichnungsdaten von einem früheren, fehlgeschlagenen Versuch, die Zeichnung zu speichern, gefunden. Möchten Sie diese ungespeicherte Zeichnung wiederherstellen und weiter bearbeiten?',\n    'pages_not_in_chapter' => 'Seite ist in keinem Kapitel',\n    'pages_move' => 'Seite verschieben',\n    'pages_copy' => 'Seite kopieren',\n    'pages_copy_desination' => 'Ziel',\n    'pages_copy_success' => 'Seite erfolgreich kopiert',\n    'pages_permissions' => 'Seiten Berechtigungen',\n    'pages_permissions_success' => 'Seiten Berechtigungen aktualisiert',\n    'pages_revision' => 'Version',\n    'pages_revisions' => 'Seitenversionen',\n    'pages_revisions_desc' => 'Alle vorherhigen Revisionen dieser Seite sind unten aufgelistet. Sie können zurückschauen, vergleichen und alte Seitenversionen wiederherstellen, wenn die Berechtigungen dies erlauben. Der vollständige Verlauf der Seite kann hier möglicherweise nicht vollständig wiedergegeben werden, da je nach Systemkonfiguration alte Revisionen automatisch hätten gelöscht werden können.',\n    'pages_revisions_named' => 'Seitenversionen von \":pageName\"',\n    'pages_revision_named' => 'Seitenversion von \":pageName\"',\n    'pages_revision_restored_from' => 'Wiederhergestellt von #:id; :summary',\n    'pages_revisions_created_by' => 'Erstellt von',\n    'pages_revisions_date' => 'Versionsdatum',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Revisionsnummer',\n    'pages_revisions_numbered' => 'Revision #:id',\n    'pages_revisions_numbered_changes' => 'Revision #:id Änderungen',\n    'pages_revisions_editor' => 'Editor-Typ',\n    'pages_revisions_changelog' => 'Änderungsprotokoll',\n    'pages_revisions_changes' => 'Änderungen',\n    'pages_revisions_current' => 'Aktuelle Version',\n    'pages_revisions_preview' => 'Vorschau',\n    'pages_revisions_restore' => 'Wiederherstellen',\n    'pages_revisions_none' => 'Diese Seite hat keine älteren Versionen.',\n    'pages_copy_link' => 'Link kopieren',\n    'pages_edit_content_link' => 'Im Editor zum Abschnitt springen',\n    'pages_pointer_enter_mode' => 'Abschnittsauswahlmodus aktivieren',\n    'pages_pointer_label' => 'Abschnittsoptionen der Seite',\n    'pages_pointer_permalink' => 'Seitenabschnitt-Permalink',\n    'pages_pointer_include_tag' => 'Seitenabschnitts-Include-Tag',\n    'pages_pointer_toggle_link' => 'Permalink-Modus, Drücken, um Include-Tag anzuzeigen',\n    'pages_pointer_toggle_include' => 'Include-Tag-Modus, Drücken, um Permalink anzuzeigen',\n    'pages_permissions_active' => 'Seiten-Berechtigungen aktiv',\n    'pages_initial_revision' => 'Erste Veröffentlichung',\n    'pages_references_update_revision' => 'Automatische Systemaktualisierung interner Links',\n    'pages_initial_name' => 'Neue Seite',\n    'pages_editing_draft_notification' => 'Sie bearbeiten momenten einen Entwurf, der zuletzt :timeDiff gespeichert wurde.',\n    'pages_draft_edited_notification' => 'Diese Seite wurde seit diesem Zeitpunkt verändert. Wir empfehlen Ihnen, diesen Entwurf zu verwerfen.',\n    'pages_draft_page_changed_since_creation' => 'Diese Seite wurde seit der Erstellung dieses Entwurfs aktualisiert. Es wird empfohlen, diesen Entwurf zu verwerfen oder darauf zu achten, dass keine Seitenänderungen überschrieben werden.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count Benutzer bearbeiten derzeit diese Seite.',\n        'start_b' => ':userName bearbeitet jetzt diese Seite.',\n        'time_a' => 'seit die Seiten zuletzt aktualisiert wurden.',\n        'time_b' => 'in den letzten :minCount Minuten',\n        'message' => ':start :time. Achten Sie darauf, keine Änderungen von anderen Benutzern zu überschreiben!',\n    ],\n    'pages_draft_discarded' => 'Entwurf verworfen! Der aktuelle Seiteninhalt wurde geladen',\n    'pages_draft_deleted' => 'Entwurf gelöscht. Der aktuelle Seiteninhalt wurde geladen',\n    'pages_specific' => 'Spezifische Seite',\n    'pages_is_template' => 'Seitenvorlage',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Seitenleiste umschalten',\n    'page_tags' => 'Seiten-Schlagwörter',\n    'chapter_tags' => 'Kapitel-Schlagwörter',\n    'book_tags' => 'Buch-Schlagwörter',\n    'shelf_tags' => 'Regal-Schlagwörter',\n    'tag' => 'Schlagwort',\n    'tags' =>  'Schlagwörter',\n    'tags_index_desc' => 'Tags können auf Inhalte im System angewendet werden, um eine flexible Form der Kategorisierung anzuwenden. Tags können sowohl einen Schlüssel als auch einen Wert haben, wobei der Wert optional ist. Einmal angewendet, können Inhalte unter Verwendung des Tag-Namens und Wertes abgefragt werden.',\n    'tag_name' =>  'Schlagwortname',\n    'tag_value' => 'Inhalt (Optional)',\n    'tags_explain' => \"Fügen Sie Schlagwörter hinzu, um Ihren Inhalt zu kategorisieren.\\nSie können einen erklärenden Inhalt hinzufügen, um eine genauere Unterteilung vorzunehmen.\",\n    'tags_add' => 'Weiteres Schlagwort hinzufügen',\n    'tags_remove' => 'Diesen Tag entfernen',\n    'tags_usages' => 'Gesamte Tagnutzung',\n    'tags_assigned_pages' => 'Zugewiesen zu Seiten',\n    'tags_assigned_chapters' => 'Zugewiesen zu Kapiteln',\n    'tags_assigned_books' => 'Zugewiesen zu Büchern',\n    'tags_assigned_shelves' => 'Zugewiesen zu Regalen',\n    'tags_x_unique_values' => ':count eindeutige Werte',\n    'tags_all_values' => 'Alle Werte',\n    'tags_view_tags' => 'Tags anzeigen',\n    'tags_view_existing_tags' => 'Vorhandene Tags anzeigen',\n    'tags_list_empty_hint' => 'Tags können über die Seitenleiste des Seiteneditors oder beim Bearbeiten der Details eines Buches, Kapitels oder Regals zugewiesen werden.',\n    'attachments' => 'Anhänge',\n    'attachments_explain' => 'Sie können auf Ihrer Seite Dateien hochladen oder Links hinzufügen. Diese werden in der Seitenleiste angezeigt.',\n    'attachments_explain_instant_save' => 'Änderungen werden direkt gespeichert.',\n    'attachments_upload' => 'Datei hochladen',\n    'attachments_link' => 'Link hinzufügen',\n    'attachments_upload_drop' => 'Alternativ können Sie eine Datei per Drag & Drop hier als Anhang hochladen.',\n    'attachments_set_link' => 'Link setzen',\n    'attachments_delete' => 'Sind Sie sicher, dass Sie diesen Anhang löschen möchten?',\n    'attachments_dropzone' => 'Ziehe Dateien hierher, um sie hochzuladen',\n    'attachments_no_files' => 'Es wurden bisher keine Dateien hochgeladen.',\n    'attachments_explain_link' => 'Wenn Sie keine Datei hochladen möchten, können Sie stattdessen einen Link hinzufügen. Dieser Link kann auf eine andere Seite oder eine Datei im Internet weisen.',\n    'attachments_link_name' => 'Link-Name',\n    'attachment_link' => 'Link zum Anhang',\n    'attachments_link_url' => 'Link zu einer Datei',\n    'attachments_link_url_hint' => 'URL einer Seite oder Datei',\n    'attach' => 'Hinzufügen',\n    'attachments_insert_link' => 'Link zum Anhang auf Seite einfügen',\n    'attachments_edit_file' => 'Datei bearbeiten',\n    'attachments_edit_file_name' => 'Dateiname',\n    'attachments_edit_drop_upload' => 'Ziehen Sie Dateien hierher, um diese hochzuladen und zu überschreiben',\n    'attachments_order_updated' => 'Reihenfolge der Anhänge aktualisiert',\n    'attachments_updated_success' => 'Anhangdetails aktualisiert',\n    'attachments_deleted' => 'Anhang gelöscht',\n    'attachments_file_uploaded' => 'Datei erfolgreich hochgeladen',\n    'attachments_file_updated' => 'Datei erfolgreich aktualisiert',\n    'attachments_link_attached' => 'Link erfolgreich der Seite hinzugefügt',\n    'templates' => 'Vorlagen',\n    'templates_set_as_template' => 'Seite ist eine Vorlage',\n    'templates_explain_set_as_template' => 'Sie können diese Seite als Vorlage festlegen, damit deren Inhalt beim Erstellen anderer Seiten verwendet werden kann. Andere Benutzer können diese Vorlage verwenden, wenn sie die Zugriffsrechte für diese Seite haben.',\n    'templates_replace_content' => 'Seiteninhalt ersetzen',\n    'templates_append_content' => 'An Seiteninhalt anhängen',\n    'templates_prepend_content' => 'Seiteninhalt voranstellen',\n\n    // Profile View\n    'profile_user_for_x' => 'Benutzer seit :time',\n    'profile_created_content' => 'Erstellte Inhalte',\n    'profile_not_created_pages' => ':userName hat noch keine Seiten erstellt.',\n    'profile_not_created_chapters' => ':userName hat noch keine Kapitel erstellt.',\n    'profile_not_created_books' => ':userName hat noch keine Bücher erstellt.',\n    'profile_not_created_shelves' => ':userName hat noch keine Regale erstellt.',\n\n    // Comments\n    'comment' => 'Kommentar',\n    'comments' => 'Kommentare',\n    'comment_add' => 'Kommentieren',\n    'comment_none' => 'Keine Kommentare vorhanden',\n    'comment_placeholder' => 'Geben Sie hier Ihre Kommentare ein',\n    'comment_thread_count' => ':count Thema|:count Themen',\n    'comment_archived_count' => ':count archiviert',\n    'comment_archived_threads' => 'Archivierte Themen',\n    'comment_save' => 'Kommentar speichern',\n    'comment_new' => 'Neuer Kommentar',\n    'comment_created' => ':createDiff kommentiert',\n    'comment_updated' => ':updateDiff aktualisiert von :username',\n    'comment_updated_indicator' => 'Aktualisiert',\n    'comment_deleted_success' => 'Kommentar gelöscht',\n    'comment_created_success' => 'Kommentar hinzugefügt',\n    'comment_updated_success' => 'Kommentar aktualisiert',\n    'comment_archive_success' => 'Kommentar archiviert',\n    'comment_unarchive_success' => 'Kommentar nicht mehr archiviert',\n    'comment_view' => 'Kommentar ansehen',\n    'comment_jump_to_thread' => 'Zum Thema springen',\n    'comment_delete_confirm' => 'Möchten Sie diesen Kommentar wirklich löschen?',\n    'comment_in_reply_to' => 'Antwort auf :commentId',\n    'comment_reference' => 'Referenz',\n    'comment_reference_outdated' => '(Veraltet)',\n    'comment_editor_explain' => 'Hier sind die Kommentare, die auf dieser Seite hinterlassen wurden. Kommentare können hinzugefügt und verwaltet werden, wenn die gespeicherte Seite angezeigt wird.',\n\n    // Revision\n    'revision_delete_confirm' => 'Sind Sie sicher, dass Sie diese Revision löschen wollen?',\n    'revision_restore_confirm' => 'Sind Sie sicher, dass Sie diese Revision wiederherstellen wollen? Der aktuelle Seiteninhalt wird ersetzt.',\n    'revision_cannot_delete_latest' => 'Die letzte Version kann nicht gelöscht werden.',\n\n    // Copy view\n    'copy_consider' => 'Bitte beachten Sie das Untenstehende, wenn Sie Inhalte kopieren.',\n    'copy_consider_permissions' => 'Benutzerdefinierte Berechtigungseinstellungen werden nicht kopiert.',\n    'copy_consider_owner' => 'Sie werden Eigentümer aller kopierten Inhalte.',\n    'copy_consider_images' => 'Bilder auf der Seite werden nicht dupliziert und die originalen Bilder werden die Beziehung zur ursprünglichen Seite, auf der sie hochgeladen wurden, behalten.',\n    'copy_consider_attachments' => 'Seitenanhänge werden nicht kopiert.',\n    'copy_consider_access' => 'Eine Veränderung der Position, Besitzers oder Berechtigungen könnte dafür sorgen, dass Unberechtigte, Zugriff bekommen.',\n\n    // Conversions\n    'convert_to_shelf' => 'In Regal umwandeln',\n    'convert_to_shelf_contents_desc' => 'Sie können dieses Buch in ein neues Regal mit dem gleichen Inhalt umwandeln. Kapitel in diesem Buch werden in neue Bücher umgewandelt. Wenn dieses Buch Seiten enthält, die nicht in einem Kapitel sind, wird dieses Buch entsprechend umbenannt und wird Teil des neuen Regals.',\n    'convert_to_shelf_permissions_desc' => 'Alle Berechtigungen dieses Buches werden in das neue Regal kopiert und in alle darin enthaltenen neuen Bücher, die keine eigenen Berechtigungen haben. Beachten Sie, dass die Berechtigungen für Regale nicht automatisch auf die enthaltenen Inhalte angewendet werden, wie es bei Büchern der Fall ist.',\n    'convert_book' => 'Buch umwandeln',\n    'convert_book_confirm' => 'Sind Sie sicher, dass Sie dieses Buch umwandeln möchten?',\n    'convert_undo_warning' => 'Dies kann nicht so einfach rückgängig gemacht werden.',\n    'convert_to_book' => 'In Buch umwandeln',\n    'convert_to_book_desc' => 'Sie können dieses Kapitel zu einem neuen Buch mit dem gleichen Inhalt umwandeln. Alle Berechtigungen für dieses Kapitel werden in das neue Buch übernommen, aber alle vom ursprünglichen Buch vererbten Berechtigungen werden nicht übernommen, daher kann es zu Änderungen im Zugriff kommen.',\n    'convert_chapter' => 'Kapitel umwandeln',\n    'convert_chapter_confirm' => 'Sind Sie sicher, dass Sie dieses Kapitel umwandeln möchten?',\n\n    // References\n    'references' => 'Verweise',\n    'references_none' => 'Es gibt keine nachverfolgten Referenzen zu diesem Element.',\n    'references_to_desc' => 'Unten sind alle bekannten Inhalte im System aufgelistet, die auf diesen Eintrag verweisen.',\n\n    // Watch Options\n    'watch' => 'Beobachten',\n    'watch_title_default' => 'Standardeinstellungen',\n    'watch_desc_default' => 'Rückgängig machen auf Standard-Benachrichtigungseinstellungen.',\n    'watch_title_ignore' => 'Ignorieren',\n    'watch_desc_ignore' => 'Ignorieren aller Benachrichtigungen, auch die von den Einstellungen auf Benutzerebene.',\n    'watch_title_new' => 'Neue Seiten',\n    'watch_desc_new' => 'Benachrichtigung, wenn eine neue Seite in diesem Element erstellt wird.',\n    'watch_title_updates' => 'Alle Seitenupdates',\n    'watch_desc_updates' => 'Bei allen neuen Seiten und Seitenänderungen benachrichtigen.',\n    'watch_desc_updates_page' => 'Bei allen Seitenänderungen benachrichtigen.',\n    'watch_title_comments' => 'Alle Seitenupdates & Kommentare',\n    'watch_desc_comments' => 'Benachrichtigung bei allen neuen Seiten, Seitenänderungen und neuen Kommentaren.',\n    'watch_desc_comments_page' => 'Benachrichtigung bei Seitenänderungen und neuen Kommentaren.',\n    'watch_change_default' => 'Standard-Benachrichtigungseinstellungen ändern',\n    'watch_detail_ignore' => 'Benachrichtigungen ignorieren',\n    'watch_detail_new' => 'Beobachten von neuen Seiten',\n    'watch_detail_updates' => 'Beobachtung neuer Seiten und Aktualisierungen',\n    'watch_detail_comments' => 'Beobachtung neuer Seiten, Aktualisierungen und Kommentare',\n    'watch_detail_parent_book' => 'Beobachten über übergeordnetes Buch',\n    'watch_detail_parent_book_ignore' => 'Ignorieren über übergeordnetes Buch',\n    'watch_detail_parent_chapter' => 'Beobachten über übergeordnetes Kapitel',\n    'watch_detail_parent_chapter_ignore' => 'Ignorieren über übergeordnetes Kapitel',\n];\n"
  },
  {
    "path": "lang/de/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Sie haben keine Zugriffsberechtigung auf die angeforderte Seite.',\n    'permissionJson' => 'Sie haben keine Berechtigung die angeforderte Aktion auszuführen.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Ein Benutzer mit der E-Mail-Adresse :email ist bereits mit anderen Anmeldedaten registriert.',\n    'auth_pre_register_theme_prevention' => 'Benutzerkonto konnte für die angegebenen Details nicht registriert werden',\n    'email_already_confirmed' => 'Die E-Mail-Adresse ist bereits bestätigt. Bitte melden Sie sich an.',\n    'email_confirmation_invalid' => 'Der Bestätigungslink ist nicht gültig oder wurde bereits verwendet. Bitte registrieren Sie sich erneut.',\n    'email_confirmation_expired' => 'Der Bestätigungslink ist abgelaufen. Es wurde eine neue Bestätigungs-E-Mail gesendet.',\n    'email_confirmation_awaiting' => 'Die E-Mail-Adresse für das verwendete Konto muss bestätigt werden',\n    'ldap_fail_anonymous' => 'Anonymer LDAP-Zugriff ist fehlgeschlagen',\n    'ldap_fail_authed' => 'LDAP-Zugriff mit DN und Passwort ist fehlgeschlagen',\n    'ldap_extension_not_installed' => 'LDAP-PHP-Erweiterung ist nicht installiert.',\n    'ldap_cannot_connect' => 'Die Verbindung zum LDAP-Server ist fehlgeschlagen. Beim initialen Verbindungsaufbau trat ein Fehler auf.',\n    'saml_already_logged_in' => 'Sie sind bereits angemeldet',\n    'saml_no_email_address' => 'Es konnte keine E-Mail-Adresse für diesen Benutzer in den vom externen Authentifizierungssystem zur Verfügung gestellten Daten gefunden werden',\n    'saml_invalid_response_id' => 'Die Anfrage vom externen Authentifizierungssystem wird von einem von dieser Anwendung gestarteten Prozess nicht erkannt. Das Zurückgehen nach einer Anmeldung könnte dieses Problem verursachen.',\n    'saml_fail_authed' => 'Anmeldung mit :system fehlgeschlagen, System konnte keine erfolgreiche Autorisierung bereitstellen',\n    'oidc_already_logged_in' => 'Bereits angemeldet',\n    'oidc_no_email_address' => 'Es konnte keine E-Mail-Adresse für diesen Benutzer in den vom externen Authentifizierungssystem zur Verfügung gestellten Daten gefunden werden',\n    'oidc_fail_authed' => 'Anmeldung mit :system fehlgeschlagen, System konnte keine erfolgreiche Autorisierung bereitstellen',\n    'social_no_action_defined' => 'Es ist keine Aktion definiert',\n    'social_login_bad_response' => \"Fehler bei der :socialAccount-Anmeldung: \\n:error\",\n    'social_account_in_use' => 'Dieses :socialAccount-Konto wird bereits verwendet. Bitte melden Sie sich mit dem :socialAccount-Konto an.',\n    'social_account_email_in_use' => 'Die E-Mail-Adresse \":email\" ist bereits registriert. Wenn Sie bereits registriert sind, können Sie Ihr :socialAccount-Konto in Ihren Profil-Einstellungen verknüpfen.',\n    'social_account_existing' => 'Dieses :socialAccount-Konto ist bereits mit Ihrem Profil verknüpft.',\n    'social_account_already_used_existing' => 'Dieses :socialAccount-Konto wird bereits von einem anderen Benutzer verwendet.',\n    'social_account_not_used' => 'Dieses :socialAccount-Konto ist bisher keinem Benutzer zugeordnet. Sie können es in Ihren Profil-Einstellungen zuordnen. ',\n    'social_account_register_instructions' => 'Wenn Sie bisher keinen Social-Media Konto besitzen, können Sie ein solches Konto mit der :socialAccount Option anlegen.',\n    'social_driver_not_found' => 'Treiber für Social-Media-Konten nicht gefunden',\n    'social_driver_not_configured' => 'Ihr :socialAccount-Konto ist nicht korrekt konfiguriert.',\n    'invite_token_expired' => 'Dieser Einladungslink ist abgelaufen. Sie können stattdessen versuchen Ihr Passwort zurückzusetzen.',\n    'login_user_not_found' => 'Ein Benutzer für diese Aktion konnte nicht gefunden werden.',\n\n    // System\n    'path_not_writable' => 'Die Datei kann nicht in den angegebenen Pfad :filePath hochgeladen werden. Stellen Sie sicher, dass dieser Ordner auf dem Server beschreibbar ist.',\n    'cannot_get_image_from_url' => 'Bild konnte nicht von der URL :url geladen werden.',\n    'cannot_create_thumbs' => 'Der Server kann keine Vorschau-Bilder erzeugen. Bitte prüfen Sie, ob die GD PHP-Erweiterung installiert ist.',\n    'server_upload_limit' => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigröße. Bitte versuchen Sie es mit einer kleineren Datei.',\n    'server_post_limit' => 'Der Server kann die angegebene Datenmenge nicht empfangen. Versuchen Sie es erneut mit weniger Daten oder einer kleineren Datei.',\n    'uploaded'  => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigröße. Bitte versuchen Sie es mit einer kleineren Datei.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Beim Hochladen des Bildes trat ein Fehler auf.',\n    'image_upload_type_error' => 'Der Bildtyp der hochgeladenen Datei ist ungültig.',\n    'image_upload_replace_type' => 'Bild-Ersetzungen müssen vom gleichen Typ sein',\n    'image_upload_memory_limit' => 'Bildupload und/oder Thumbnailerstellung konnten aufgrund von Systemressourcenbeschränkungen nicht verarbeitet werden.',\n    'image_thumbnail_memory_limit' => 'Fehler beim Erstellen der Thumbnails aufgrund von Systemressourcenbeschränkungen.',\n    'image_gallery_thumbnail_memory_limit' => 'Fehler beim Erstellen der Galerie Thumbnails aufgrund von Systemressourcenbeschränkungen.',\n    'drawing_data_not_found' => 'Zeichnungsdaten konnten nicht geladen werden. Die Zeichnungsdatei existiert möglicherweise nicht mehr oder Sie haben nicht die Berechtigung, darauf zuzugreifen.',\n\n    // Attachments\n    'attachment_not_found' => 'Anhang konnte nicht gefunden werden.',\n    'attachment_upload_error' => 'Beim Hochladen des Anhangs trat ein Fehler auf',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Fehler beim Speichern des Entwurfs. Stellen Sie sicher, dass Sie mit dem Internet verbunden sind, bevor Sie den Entwurf dieser Seite speichern.',\n    'page_draft_delete_fail' => 'Fehler beim Löschen des Seitenentwurfs und beim Abrufen des gespeicherten Inhalts der aktuellen Seite',\n    'page_custom_home_deletion' => 'Eine als Startseite gesetzte Seite kann nicht gelöscht werden',\n\n    // Entities\n    'entity_not_found' => 'Eintrag nicht gefunden',\n    'bookshelf_not_found' => 'Regal nicht gefunden',\n    'book_not_found' => 'Buch nicht gefunden',\n    'page_not_found' => 'Seite nicht gefunden',\n    'chapter_not_found' => 'Kapitel nicht gefunden',\n    'selected_book_not_found' => 'Das gewählte Buch wurde nicht gefunden',\n    'selected_book_chapter_not_found' => 'Das gewählte Buch oder Kapitel wurde nicht gefunden.',\n    'guests_cannot_save_drafts' => 'Gäste können keine Entwürfe speichern',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Sie können den einzigen Administrator nicht löschen',\n    'users_cannot_delete_guest' => 'Sie können den Gast-Benutzer nicht löschen',\n    'users_could_not_send_invite' => 'Benutzer konnte nicht erstellt werden, da die Einladungs-E-Mail nicht gesendet wurde',\n\n    // Roles\n    'role_cannot_be_edited' => 'Diese Rolle kann nicht bearbeitet werden',\n    'role_system_cannot_be_deleted' => 'Dies ist eine Systemrolle und kann nicht gelöscht werden',\n    'role_registration_default_cannot_delete' => 'Diese Rolle kann nicht gelöscht werden, solange sie als Standardrolle für neue Registrierungen gesetzt ist',\n    'role_cannot_remove_only_admin' => 'Dieser Benutzer ist der einzige Benutzer, welchem die Administratorrolle zugeordnet ist. Ordnen Sie die Administratorrolle einem anderen Benutzer zu bevor Sie versuchen sie hier zu entfernen.',\n\n    // Comments\n    'comment_list' => 'Beim Abrufen der Kommentare ist ein Fehler aufgetreten.',\n    'cannot_add_comment_to_draft' => 'Sie können keine Kommentare zu einem Entwurf hinzufügen.',\n    'comment_add' => 'Beim Hinzufügen des Kommentars ist ein Fehler aufgetreten.',\n    'comment_delete' => 'Beim Löschen des Kommentars ist ein Fehler aufgetreten.',\n    'empty_comment' => 'Kann keinen leeren Kommentar hinzufügen.',\n\n    // Error pages\n    '404_page_not_found' => 'Seite nicht gefunden',\n    'sorry_page_not_found' => 'Entschuldigung. Die angeforderte Seite wurde nicht gefunden.',\n    'sorry_page_not_found_permission_warning' => 'Wenn Sie erwartet haben, dass diese Seite existiert, haben Sie möglicherweise nicht die Berechtigung, sie anzuzeigen.',\n    'image_not_found' => 'Bild nicht gefunden',\n    'image_not_found_subtitle' => 'Entschuldigung. Das angeforderte Bild wurde nicht gefunden.',\n    'image_not_found_details' => 'Wenn Sie erwartet haben, dass dieses Bild existiert, könnte es gelöscht worden sein.',\n    'return_home' => 'Zurück zur Startseite',\n    'error_occurred' => 'Es ist ein Fehler aufgetreten',\n    'app_down' => ':appName befindet sich aktuell im Wartungsmodus',\n    'back_soon' => 'Wir werden so schnell wie möglich wieder online sein.',\n\n    // Import\n    'import_zip_cant_read' => 'ZIP-Datei konnte nicht gelesen werden.',\n    'import_zip_cant_decode_data' => 'ZIP data.json konnte nicht gefunden und dekodiert werden.',\n    'import_zip_no_data' => 'ZIP-Datei Daten haben kein erwartetes Buch, Kapitel oder Seiteninhalt.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'ZIP Import konnte mit Fehlern nicht validiert werden:',\n    'import_zip_failed_notification' => 'Importieren der ZIP-Datei fehlgeschlagen.',\n    'import_perms_books' => 'Ihnen fehlt die erforderliche Berechtigung, um Bücher zu erstellen.',\n    'import_perms_chapters' => 'Ihnen fehlt die erforderliche Berechtigung, um Bücher zu erstellen.',\n    'import_perms_pages' => 'Ihnen fehlt die erforderliche Berechtigung, um Seiten zu erstellen.',\n    'import_perms_images' => 'Ihnen fehlt die erforderliche Berechtigung, um Bilder zu erstellen.',\n    'import_perms_attachments' => 'Ihnen fehlt die erforderliche Berechtigung, um Anhänge zu erstellen.',\n\n    // API errors\n    'api_no_authorization_found' => 'Kein Autorisierungstoken für die Anfrage gefunden',\n    'api_bad_authorization_format' => 'Ein Autorisierungstoken wurde auf die Anfrage gefunden, aber das Format schien falsch zu sein',\n    'api_user_token_not_found' => 'Es wurde kein passender API-Token für den angegebenen Autorisierungstoken gefunden',\n    'api_incorrect_token_secret' => 'Das Kennwort für das angegebene API-Token ist falsch',\n    'api_user_no_api_permission' => 'Der Besitzer des verwendeten API-Tokens hat keine Berechtigung für API-Aufrufe',\n    'api_user_token_expired' => 'Das verwendete Autorisierungstoken ist abgelaufen',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Fehler beim Versenden einer Test E-Mail:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'Die URL stimmt nicht mit den konfigurierten erlaubten SSR-Hosts überein',\n];\n"
  },
  {
    "path": "lang/de/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Neuer Kommentar auf Seite: :pageName',\n    'new_comment_intro' => 'Ein Benutzer hat eine Seite in :appName kommentiert:',\n    'new_page_subject' => 'Neue Seite: :pageName',\n    'new_page_intro' => 'Es wurde eine neue Seite in :appName erstellt:',\n    'updated_page_subject' => 'Aktualisierte Seite: :pageName',\n    'updated_page_intro' => 'Eine Seite wurde in :appName aktualisiert:',\n    'updated_page_debounce' => 'Um eine Flut von Benachrichtigungen zu vermeiden, werden Sie für eine gewisse Zeit keine Benachrichtigungen für weitere Bearbeitungen dieser Seite durch denselben Bearbeiter erhalten.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Name der Seite:',\n    'detail_page_path' => 'Seitenpfad:',\n    'detail_commenter' => 'Kommentator:',\n    'detail_comment' => 'Kommentar:',\n    'detail_created_by' => 'Erstellt von:',\n    'detail_updated_by' => 'Aktualisiert von:',\n\n    'action_view_comment' => 'Kommentar anzeigen',\n    'action_view_page' => 'Seite anzeigen',\n\n    'footer_reason' => 'Diese Benachrichtigung wurde an Sie gesendet, weil :link diese Art von Aktivität für dieses Element abdeckt.',\n    'footer_reason_link' => 'ihre Benachrichtigungseinstellungen',\n];\n"
  },
  {
    "path": "lang/de/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Vorherige',\n    'next'     => 'Nächste &raquo;',\n\n];\n"
  },
  {
    "path": "lang/de/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Passwörter müssen aus mindestens acht Zeichen bestehen und mit der eingegebenen Wiederholung übereinstimmen.',\n    'user' => \"Es wurde kein Benutzer mit dieser E-Mail-Adresse gefunden.\",\n    'token' => 'Der Token zum Zurücksetzen des Passworts ist für diese E-Mail-Adresse ungültig.',\n    'sent' => 'Der Link zum Zurücksetzen Ihres Passwortes wurde Ihnen per E-Mail zugesendet.',\n    'reset' => 'Ihr Passwort wurde zurückgesetzt!',\n\n];\n"
  },
  {
    "path": "lang/de/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Mein Account',\n\n    'shortcuts' => 'Tastenkürzel',\n    'shortcuts_interface' => 'Einstellungen zu UI Abkürzungen',\n    'shortcuts_toggle_desc' => 'Hier können Sie Tastaturkürzel für die Systemoberfläche für Navigation und Aktionen aktivieren oder deaktivieren.',\n    'shortcuts_customize_desc' => 'Unten können Sie alle Tastenkürzel anpassen. Drücken Sie einfach die gewünschte Tastenkombination, nachdem Sie die Eingabe für eine Tastenkombination ausgewählt haben.',\n    'shortcuts_toggle_label' => 'Tastaturkürzel aktiviert',\n    'shortcuts_section_navigation' => 'Navigation',\n    'shortcuts_section_actions' => 'Häufige Aktionen',\n    'shortcuts_save' => 'Tastenkürzel speichern',\n    'shortcuts_overlay_desc' => 'Hinweis: Wenn Tastenkürzel aktiviert sind, ist ein Hilfsoverlay durch Drücken von \"?\" verfügbar, welches die verfügbaren Tastenkürzel für Aktionen hervorhebt, die aktuell auf dem Bildschirm sichtbar sind.',\n    'shortcuts_update_success' => 'Tastenkürzel Einstellungen wurden aktualisiert!',\n    'shortcuts_overview_desc' => 'Verwalten von Tastenkombinationen, die zur Navigation der Benutzeroberfläche verwendet werden können.',\n\n    'notifications' => 'Benachrichtigungseinstellungen',\n    'notifications_desc' => 'Legen Sie fest, welche E-Mail-Benachrichtigungen Sie erhalten, wenn bestimmte Aktivitäten im System durchgeführt werden.',\n    'notifications_opt_own_page_changes' => 'Benachrichtigung bei Änderungen an eigenen Seiten',\n    'notifications_opt_own_page_comments' => 'Benachrichtigung bei Kommentaren an eigenen Seiten',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Bei Antworten auf meine Kommentare benachrichtigen',\n    'notifications_save' => 'Einstellungen speichern',\n    'notifications_update_success' => 'Benachrichtigungseinstellungen wurden aktualisiert!',\n    'notifications_watched' => 'Beobachtete und ignorierte Elemente',\n    'notifications_watched_desc' => 'Nachfolgend finden Sie die Elemente, für die benutzerdefinierten Überwachungspräferenzen gelten. Um Ihre Einstellungen für diese Elemente zu aktualisieren, sehen Sie sich das Element an und suchen dann die Überwachungsoptionen in der Seitenleiste.',\n\n    'auth' => 'Zugang & Sicherheit',\n    'auth_change_password' => 'Passwort ändern',\n    'auth_change_password_desc' => 'Legen Sie ein Passwort für die Anmeldung in der Anwendung fest. Dieses muss mindestens 8 Zeichen lang sein.',\n    'auth_change_password_success' => 'Das Passwort wurde aktualisiert!',\n\n    'profile' => 'Profildetails',\n    'profile_desc' => 'Verwalten Sie die Details Ihres Kontos, welches Sie gegenüber anderen Benutzern repräsentiert, zusätzlich zu den Details, die für die Kommunikation und Personalisierung des Systems genutzt werden.',\n    'profile_view_public' => 'Öffentliches Profil anzeigen',\n    'profile_name_desc' => 'Konfigurieren Sie Ihren Anzeigenamen, der durch die Aktivität, die Sie ausführen, und die Ihnen gehörenden Inhalte für andere Benutzer sichtbar ist.',\n    'profile_email_desc' => 'Diese E-Mail wird für Benachrichtigungen und, je nach aktiver Systemauthentifizierung, den Systemzugriff verwendet.',\n    'profile_email_no_permission' => 'Leider haben Sie nicht die Berechtigung, Ihre E-Mail-Adresse zu ändern. Wenn Sie diese ändern möchten, wenden Sie sich bitte an Ihren Administrator.',\n    'profile_avatar_desc' => 'Wählen Sie ein Bild aus, das anderen im System angezeigt wird, um Sie zu repräsentieren. Idealerweise sollte dieses Bild quadratisch und etwa 256px breit und hoch sein.',\n    'profile_admin_options' => 'Administratoroptionen',\n    'profile_admin_options_desc' => 'Weitere Administrator-Optionen wie zum Beispiel die Verwaltung von Rollenzuweisungen für Ihr Benutzerkonto finden Sie im Bereich \"Einstellungen > Benutzer\" der Anwendung.',\n\n    'delete_account' => 'Konto löschen',\n    'delete_my_account' => 'Meine Konto löschen',\n    'delete_my_account_desc' => 'Dadurch wird Ihr Benutzerkonto vollständig vom System gelöscht. Sie können dieses Konto nicht wiederherstellen oder diese Aktion rückgängig machen. Inhalte, die Sie erstellt haben, wie erstellte Seiten und hochgeladene Bilder, bleiben erhalten.',\n    'delete_my_account_warning' => 'Sind Sie sicher, dass Sie Ihr Benutzerkonto löschen möchten?',\n];\n"
  },
  {
    "path": "lang/de/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Einstellungen',\n    'settings_save' => 'Einstellungen speichern',\n    'system_version' => 'Systemversion',\n    'categories' => 'Kategorien',\n\n    // App Settings\n    'app_customization' => 'Personalisierung',\n    'app_features_security' => 'Funktionen & Sicherheit',\n    'app_name' => 'Anwendungsname',\n    'app_name_desc' => 'Dieser Name wird im Header und in E-Mails angezeigt.',\n    'app_name_header' => 'Anwendungsname im Header anzeigen?',\n    'app_public_access' => 'Öffentlicher Zugriff',\n    'app_public_access_desc' => 'Wenn Sie diese Option aktivieren, können Besucher, die nicht angemeldet sind, auf Inhalte in Ihrer BookStack-Instanz zugreifen.',\n    'app_public_access_desc_guest' => 'Der Zugang für öffentliche Besucher kann über den Benutzer \"Guest\" gesteuert werden.',\n    'app_public_access_toggle' => 'Öffentlichen Zugriff erlauben',\n    'app_public_viewing' => 'Öffentliche Ansicht erlauben?',\n    'app_secure_images' => 'Erhöhte Sicherheit für hochgeladene Bilder aktivieren?',\n    'app_secure_images_toggle' => 'Höhere Sicherheit für Bild-Uploads aktivieren',\n    'app_secure_images_desc' => 'Aus Leistungsgründen sind alle Bilder öffentlich sichtbar. Diese Option fügt zufällige, schwer zu erratende, Zeichenketten zu Bild-URLs hinzu. Stellen Sie sicher, dass Verzeichnisindizes deaktiviert sind, um einen einfachen Zugriff zu verhindern.',\n    'app_default_editor' => 'Standard-Seiten-Editor',\n    'app_default_editor_desc' => 'Wählen Sie aus, welcher Editor standardmäßig beim Bearbeiten neuer Seiten verwendet wird. Dies kann auf einer Seitenebene überschrieben werden, wenn es die Berechtigungen erlauben.',\n    'app_custom_html' => 'Benutzerdefinierter HTML-Head-Inhalt',\n    'app_custom_html_desc' => 'Jeder Inhalt, der hier hinzugefügt wird, wird am Ende der <head>-Sektion jeder Seite eingefügt. Diese kann praktisch sein, um CSS-Styles anzupassen oder Analytics-Code hinzuzufügen.',\n    'app_custom_html_disabled_notice' => 'Benutzerdefinierte HTML-Kopfzeileninhalte sind auf dieser Einstellungsseite deaktiviert, um sicherzustellen, dass alle Änderungen rückgängig gemacht werden können.',\n    'app_logo' => 'Anwendungslogo',\n    'app_logo_desc' => 'Dies wird unter anderem in der Kopfzeile der Anwendung verwendet. Dieses Bild sollte 86px hoch sein. Große Bilder werden herunterskaliert.',\n    'app_icon' => 'Anwendungssymbol',\n    'app_icon_desc' => 'Dieses Symbol wird für Browser-Registerkarten und Verknüpfungssymbole verwendet. Dies sollte ein 256px quadratisches PNG-Bild sein.',\n    'app_homepage' => 'Startseite der Anwendung',\n    'app_homepage_desc' => 'Wählen Sie eine Seite als Startseite aus, die statt der Standardansicht angezeigt werden soll. Seitenberechtigungen werden für die ausgewählten Seiten ignoriert.',\n    'app_homepage_select' => 'Wählen Sie eine Seite aus',\n    'app_footer_links' => 'Fußzeilen-Links',\n    'app_footer_links_desc' => 'Fügen Sie Links hinzu, die innerhalb der Seitenfußzeile angezeigt werden. Diese werden am unteren Ende der meisten Seiten angezeigt, einschließlich derjenigen, die keine Anmeldung benötigen. Sie können die Bezeichnung \"trans::<key>\" verwenden, um systemdefinierte Übersetzungen zu verwenden. Beispiel: Mit \"trans::common.privacy_policy\" wird der übersetzte Text \"Privacy Policy\" bereitgestellt und \"trans::common.terms_of_service\" liefert den übersetzten Text \"Terms of Service\".',\n    'app_footer_links_label' => 'Link-Label',\n    'app_footer_links_url' => 'Link-URL',\n    'app_footer_links_add' => 'Fußzeilen-Link hinzufügen',\n    'app_disable_comments' => 'Kommentare deaktivieren',\n    'app_disable_comments_toggle' => 'Kommentare deaktivieren',\n    'app_disable_comments_desc' => 'Deaktiviert Kommentare über alle Seiten in der Anwendung. Vorhandene Kommentare werden nicht angezeigt.',\n\n    // Color settings\n    'color_scheme' => 'Farbschema der Anwendung',\n    'color_scheme_desc' => 'Lege die Farben, die in der Benutzeroberfläche verwendet werden, fest. Farben können separat für dunkle und helle Modi konfiguriert werden, um am besten zum Farbschema zu passen und die Lesbarkeit zu gewährleisten.',\n    'ui_colors_desc' => 'Lege die primäre Farbe und die Standard-Linkfarbe der Anwendung fest. Die primäre Farbe wird hauptsächlich für Kopfzeilen, Buttons und Interface-Dekorationen verwendet. Die Standard-Linkfarbe wird für textbasierte Links und Aktionen sowohl innerhalb des geschriebenen Inhalts als auch in der Benutzeroberfläche verwendet.',\n    'app_color' => 'Primäre Farbe',\n    'link_color' => 'Standard-Linkfarbe',\n    'content_colors_desc' => 'Legt Farben für alle Elemente in der Seitenorganisationshierarchie fest. Die Auswahl von Farben mit einer ähnlichen Helligkeit wie die Standardfarben wird zur Lesbarkeit empfohlen.',\n    'bookshelf_color' => 'Regalfarbe',\n    'book_color' => 'Buchfarbe',\n    'chapter_color' => 'Kapitelfarbe',\n    'page_color' => 'Seitenfarbe',\n    'page_draft_color' => 'Seitenentwurfsfarbe',\n\n    // Registration Settings\n    'reg_settings' => 'Registrierungseinstellungen',\n    'reg_enable' => 'Registrierung erlauben',\n    'reg_enable_toggle' => 'Registrierung erlauben',\n    'reg_enable_desc' => 'Wenn die Registrierung erlaubt ist, kann sich der Benutzer als Anwendungsbenutzer anmelden. Bei der Registrierung erhält er eine einzige, voreingestellte Benutzerrolle.',\n    'reg_default_role' => 'Standard-Benutzerrolle nach Registrierung',\n    'reg_enable_external_warning' => 'Die obige Option wird ignoriert, während eine externe LDAP oder SAML Authentifizierung aktiv ist. Benutzerkonten für nicht existierende Mitglieder werden automatisch erzeugt, wenn die Authentifizierung gegen das verwendete externe System erfolgreich ist.',\n    'reg_email_confirmation' => 'Bestätigung per E-Mail',\n    'reg_email_confirmation_toggle' => 'Bestätigung per E-Mail erforderlich',\n    'reg_confirm_email_desc' => 'Falls die Einschränkung für Domains genutzt wird, ist die Bestätigung per E-Mail zwingend erforderlich und der untenstehende Wert wird ignoriert.',\n    'reg_confirm_restrict_domain' => 'Registrierung auf bestimmte Domains einschränken',\n    'reg_confirm_restrict_domain_desc' => 'Fügen Sie eine durch Komma getrennte Liste von Domains hinzu, auf die die Registrierung eingeschränkt werden soll. Benutzern wird eine E-Mail gesendet, um ihre E-Mail-Adresse zu bestätigen, bevor diese die Anwendung nutzen können.\nHinweis: Benutzer können ihre E-Mail-Adresse nach erfolgreicher Registrierung ändern.',\n    'reg_confirm_restrict_domain_placeholder' => 'Keine Einschränkung gesetzt',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Wählen Sie die Standard-Sortierregel aus, die auf neue Bücher angewendet werden soll. Dies wirkt sich nicht auf bestehende Bücher aus und kann pro Buch überschrieben werden.',\n    'sorting_rules' => 'Sortierregeln',\n    'sorting_rules_desc' => 'Dies sind vordefinierte Sortieraktionen, die auf Inhalte im System angewendet werden können.',\n    'sort_rule_assigned_to_x_books' => ':count Buch zugewiesen|:count Büchern zugewiesen',\n    'sort_rule_create' => 'Sortierregel erstellen',\n    'sort_rule_edit' => 'Sortierregel bearbeiten',\n    'sort_rule_delete' => 'Sortierregel löschen',\n    'sort_rule_delete_desc' => 'Diese Sortierregel aus dem System entfernen. Bücher mit dieser Sortierung werden auf manuelle Sortierung zurückgesetzt.',\n    'sort_rule_delete_warn_books' => 'Diese Sortierregel wird derzeit in :count Bücher(n) verwendet. Sind Sie sicher, dass Sie dies löschen möchten?',\n    'sort_rule_delete_warn_default' => 'Diese Sortierregel wird derzeit als Standard für Bücher verwendet. Sind Sie sicher, dass Sie dies löschen möchten?',\n    'sort_rule_details' => 'Sortierregel-Details',\n    'sort_rule_details_desc' => 'Legen Sie einen Namen für diese Sortierregel fest, der in Listen erscheint, wenn Benutzer eine Sortierung auswählen.',\n    'sort_rule_operations' => 'Sortierungs-Aktionen',\n    'sort_rule_operations_desc' => 'Konfigurieren Sie die durchzuführenden Sortieraktionen durch Verschieben von der Liste der verfügbaren Aktionen. Bei der Verwendung werden die Aktionen von oben nach unten angewendet. Alle hier vorgenommenen Änderungen werden beim Speichern auf alle zugewiesenen Bücher angewendet.',\n    'sort_rule_available_operations' => 'Verfügbare Aktionen',\n    'sort_rule_available_operations_empty' => 'Keine verbleibenden Aktionen',\n    'sort_rule_configured_operations' => 'Konfigurierte Aktionen',\n    'sort_rule_configured_operations_empty' => 'Aktionen aus der Liste \"Verfügbare Operationen\" ziehen/hinzufügen',\n    'sort_rule_op_asc' => '(Aufst.)',\n    'sort_rule_op_desc' => '(Abst.)',\n    'sort_rule_op_name' => 'Name - Alphabetisch',\n    'sort_rule_op_name_numeric' => 'Name - Numerisch',\n    'sort_rule_op_created_date' => 'Erstellungsdatum',\n    'sort_rule_op_updated_date' => 'Aktualisierungsdatum',\n    'sort_rule_op_chapters_first' => 'Kapitel zuerst',\n    'sort_rule_op_chapters_last' => 'Kapitel zuletzt',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Wartung',\n    'maint_image_cleanup' => 'Bilder bereinigen',\n    'maint_image_cleanup_desc' => 'Überprüft Seiten- und Versionsinhalte auf ungenutzte und mehrfach vorhandene Bilder. Erstellen Sie vor dem Start ein Backup Ihrer Datenbank und Bilder.',\n    'maint_delete_images_only_in_revisions' => 'Lösche auch Bilder, die nur in alten Seitenüberarbeitungen vorhanden sind',\n    'maint_image_cleanup_run' => 'Reinigung starten',\n    'maint_image_cleanup_warning' => ':count eventuell unbenutze Bilder wurden gefunden. Möchten Sie diese Bilder löschen?',\n    'maint_image_cleanup_success' => ':count eventuell unbenutze Bilder wurden gefunden und gelöscht.',\n    'maint_image_cleanup_nothing_found' => 'Keine unbenutzen Bilder gefunden. Nichts zu löschen!',\n    'maint_send_test_email' => 'Eine Test-E-Mail versenden',\n    'maint_send_test_email_desc' => 'Dies sendet eine Test-E-Mail an Ihre in Ihrem Profil angegebene E-Mail-Adresse.',\n    'maint_send_test_email_run' => 'Test-E-Mail senden',\n    'maint_send_test_email_success' => 'E-Mail wurde an :address gesendet',\n    'maint_send_test_email_mail_subject' => 'Test-E-Mail',\n    'maint_send_test_email_mail_greeting' => 'E-Mail-Versand scheint zu funktionieren!',\n    'maint_send_test_email_mail_text' => 'Glückwunsch! Da Sie diese E-Mail Benachrichtigung erhalten haben, scheinen Ihre E-Mail-Einstellungen korrekt konfiguriert zu sein.',\n    'maint_recycle_bin_desc' => 'Gelöschte Regale, Bücher, Kapitel & Seiten werden in den Papierkorb verschoben, so dass sie wiederhergestellt oder dauerhaft gelöscht werden können. Ältere Gegenstände im Papierkorb können, in Abhängigkeit von der Systemkonfiguration, nach einer Weile automatisch entfernt werden.',\n    'maint_recycle_bin_open' => 'Papierkorb öffnen',\n    'maint_regen_references' => 'Referenzen neu generieren',\n    'maint_regen_references_desc' => 'Diese Aktion wird den Referenzindex innerhalb der Datenbank neu erstellen. Dies wird normalerweise automatisch ausgeführt, aber diese Aktion kann nützlich sein, um alte Inhalte oder Inhalte zu indizieren, die mittels inoffizieller Methoden hinzugefügt wurden.',\n    'maint_regen_references_success' => 'Referenz-Index wurde neu generiert!',\n    'maint_timeout_command_note' => 'Hinweis: Die Ausführung dieser Aktion kann einige Zeit in Anspruch nehmen, was in einigen Webumgebungen zu Timeout-Problemen führen kann. Alternativ kann diese Aktion auch mit einem Terminalbefehl ausgeführt werden.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Papierkorb',\n    'recycle_bin_desc' => 'Hier können Sie gelöschte Elemente wiederherstellen oder sie dauerhaft aus dem System entfernen. Diese Liste ist nicht gefiltert, im Gegensatz zu ähnlichen Aktivitätslisten im System, wo Berechtigungsfilter angewendet werden.',\n    'recycle_bin_deleted_item' => 'Gelöschtes Element',\n    'recycle_bin_deleted_parent' => 'Übergeordnet',\n    'recycle_bin_deleted_by' => 'Gelöscht von',\n    'recycle_bin_deleted_at' => 'Löschzeitpunkt',\n    'recycle_bin_permanently_delete' => 'Dauerhaft löschen',\n    'recycle_bin_restore' => 'Wiederherstellen',\n    'recycle_bin_contents_empty' => 'Der Papierkorb ist derzeit leer',\n    'recycle_bin_empty' => 'Papierkorb leeren',\n    'recycle_bin_empty_confirm' => 'Dies wird alle Gegenstände im Papierkorb dauerhaft entfernen, einschließlich der Inhalte, die darin enthalten sind. Sind Sie sicher, dass Sie den Papierkorb leeren möchten?',\n    'recycle_bin_destroy_confirm' => 'Diese Aktion löscht dieses Element dauerhaft aus dem System, zusammen mit allen unten aufgeführten untergeordneten Elementen, und es ist nicht möglich, diesen Inhalt wiederherzustellen. Sind Sie sicher, dass Sie dieses Element dauerhaft löschen möchten?',\n    'recycle_bin_destroy_list' => 'Zu löschende Elemente',\n    'recycle_bin_restore_list' => 'Zu wiederherzustellende Elemente',\n    'recycle_bin_restore_confirm' => 'Mit dieser Aktion wird das gelöschte Element einschließlich aller untergeordneten Elemente an seinen ursprünglichen Ort wiederherstellen. Wenn der ursprüngliche Ort gelöscht wurde und sich nun im Papierkorb befindet, muss auch das übergeordnete Element wiederhergestellt werden.',\n    'recycle_bin_restore_deleted_parent' => 'Das übergeordnete Elements wurde ebenfalls gelöscht. Dieses Element wird weiterhin als gelöscht zählen, bis auch das übergeordnete Element wiederhergestellt wurde.',\n    'recycle_bin_restore_parent' => 'Übergeordneter Eintrag wiederherstellen',\n    'recycle_bin_destroy_notification' => ':count Elemente wurden aus dem Papierkorb gelöscht.',\n    'recycle_bin_restore_notification' => ':count Elemente wurden aus dem Papierkorb wiederhergestellt.',\n\n    // Audit Log\n    'audit' => 'Audit-Protokoll',\n    'audit_desc' => 'Dieses Audit-Protokoll zeigt eine Liste der Aktivitäten an, welche vom System protokolliert werden. Im Gegensatz zu den anderen Aktivitätslisten im System, bei denen Berechtigungen angewendet werden, ist diese Liste ungefiltert.',\n    'audit_event_filter' => 'Ereignisfilter',\n    'audit_event_filter_no_filter' => 'Kein Filter',\n    'audit_deleted_item' => 'Gelöschtes Objekt',\n    'audit_deleted_item_name' => 'Name: :name',\n    'audit_table_user' => 'Benutzer',\n    'audit_table_event' => 'Ereignis',\n    'audit_table_related' => 'Verknüpftes Element oder Detail',\n    'audit_table_ip' => 'IP-Adresse',\n    'audit_table_date' => 'Aktivitätsdatum',\n    'audit_date_from' => 'Zeitraum von',\n    'audit_date_to' => 'Zeitraum bis',\n\n    // Role Settings\n    'roles' => 'Rollen',\n    'role_user_roles' => 'Benutzer-Rollen',\n    'roles_index_desc' => 'Rollen werden verwendet, um Benutzer zu gruppieren System-Berechtigung für ihre Mitglieder zuzuweisen. Wenn ein Benutzer Mitglied mehrerer Rollen ist, stapeln die gewährten Berechtigungen und der Benutzer wird alle Fähigkeiten erben.',\n    'roles_x_users_assigned' => ':count Benutzer zugewiesen|:count Benutzer zugewiesen',\n    'roles_x_permissions_provided' => ':count Berechtigung|:count Berechtigungen',\n    'roles_assigned_users' => 'Zugewiesene Benutzer',\n    'roles_permissions_provided' => 'Genutzte Berechtigungen',\n    'role_create' => 'Neue Rolle anlegen',\n    'role_delete' => 'Rolle löschen',\n    'role_delete_confirm' => 'Sie möchten die Rolle \":roleName\" löschen.',\n    'role_delete_users_assigned' => 'Diese Rolle ist :userCount Benutzern zugeordnet. Sie können unten eine neue Rolle auswählen, die Sie diesen Benutzern zuordnen möchten.',\n    'role_delete_no_migration' => \"Den Benutzern keine andere Rolle zuordnen\",\n    'role_delete_sure' => 'Sind Sie sicher, dass Sie diese Rolle löschen möchten?',\n    'role_edit' => 'Rolle bearbeiten',\n    'role_details' => 'Rollendetails',\n    'role_name' => 'Rollenname',\n    'role_desc' => 'Kurzbeschreibung der Rolle',\n    'role_mfa_enforced' => 'Benötigt Mehrfach-Faktor-Authentifizierung',\n    'role_external_auth_id' => 'Externe Authentifizierungs-IDs',\n    'role_system' => 'System-Berechtigungen',\n    'role_manage_users' => 'Benutzer verwalten',\n    'role_manage_roles' => 'Rollen und Rollen-Berechtigungen verwalten',\n    'role_manage_entity_permissions' => 'Alle Buch-, Kapitel- und Seiten-Berechtigungen verwalten',\n    'role_manage_own_entity_permissions' => 'Nur Berechtigungen eigener Bücher, Kapitel und Seiten verwalten',\n    'role_manage_page_templates' => 'Seitenvorlagen verwalten',\n    'role_access_api' => 'Systemzugriffs-API',\n    'role_manage_settings' => 'Globaleinstellungen verwalten',\n    'role_export_content' => 'Inhalt exportieren',\n    'role_import_content' => 'Inhalt importieren',\n    'role_editor_change' => 'Seiten-Editor ändern',\n    'role_notifications' => 'Empfangen und Verwalten von Benachrichtigungen',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Berechtigungen',\n    'roles_system_warning' => 'Beachten Sie, dass der Zugriff auf eine der oben genannten drei Berechtigungen einem Benutzer erlauben kann, seine eigenen Berechtigungen oder die Rechte anderer im System zu ändern. Weisen Sie nur Rollen, mit diesen Berechtigungen, vertrauenswürdigen Benutzern zu.',\n    'role_asset_desc' => 'Diese Berechtigungen gelten für den Standard-Zugriff innerhalb des Systems. Berechtigungen für Bücher, Kapitel und Seiten überschreiben diese Berechtigungenen.',\n    'role_asset_admins' => 'Administratoren erhalten automatisch Zugriff auf alle Inhalte, aber diese Optionen können Oberflächenoptionen ein- oder ausblenden.',\n    'role_asset_image_view_note' => 'Das bezieht sich auf die Sichtbarkeit innerhalb des Bildmanagers. Der tatsächliche Zugriff auf hochgeladene Bilddateien hängt von der Speicheroption des Systems für Bilder ab.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'Alle',\n    'role_own' => 'Eigene',\n    'role_controlled_by_asset' => 'Berechtigungen werden vom Uploadziel bestimmt',\n    'role_save' => 'Rolle speichern',\n    'role_users' => 'Dieser Rolle zugeordnete Benutzer',\n    'role_users_none' => 'Bisher sind dieser Rolle keine Benutzer zugeordnet',\n\n    // Users\n    'users' => 'Benutzer',\n    'users_index_desc' => 'Erstellen und Verwalten Sie individuelle Benutzerkonten innerhalb des Systems. Benutzerkonten werden zur Anmeldung und Besitz von Inhalten und Aktivitäten verwendet. Zugriffsberechtigungen sind in erster Linie rollenbasiert, aber Besitz von Benutzerinhalten kann unter anderem auch Berechtigungen beeinflussen.',\n    'user_profile' => 'Benutzerprofil',\n    'users_add_new' => 'Benutzer hinzufügen',\n    'users_search' => 'Benutzer suchen',\n    'users_latest_activity' => 'Neueste Aktivitäten',\n    'users_details' => 'Benutzerdetails',\n    'users_details_desc' => 'Legen Sie für diesen Benutzer einen Anzeigenamen und eine E-Mail-Adresse fest. Die E-Mail-Adresse wird bei der Anmeldung verwendet.',\n    'users_details_desc_no_email' => 'Legen Sie für diesen Benutzer einen Anzeigenamen fest, damit andere ihn erkennen können.',\n    'users_role' => 'Benutzerrollen',\n    'users_role_desc' => 'Wählen Sie aus, welchen Rollen dieser Benutzer zugeordnet werden soll. Wenn ein Benutzer mehreren Rollen zugeordnet ist, werden die Berechtigungen dieser Rollen gestapelt und er erhält alle Fähigkeiten der zugewiesenen Rollen.',\n    'users_password' => 'Benutzerpasswort',\n    'users_password_desc' => 'Legen Sie ein Passwort fest, mit dem Sie sich anmelden möchten. Diese muss mindestens 8 Zeichen lang sein.',\n    'users_send_invite_text' => 'Sie können diesem Benutzer eine Einladungs-E-Mail senden, die es ihm erlaubt, sein eigenes Passwort zu setzen, andernfalls können Sie sein Passwort selbst setzen.',\n    'users_send_invite_option' => 'Benutzer-Einladungs-E-Mail senden',\n    'users_external_auth_id' => 'Externe Authentifizierungs-ID',\n    'users_external_auth_id_desc' => 'Wenn ein externes Authentifizierungssystem verwendet wird (z. B. SAML2, OIDC oder LDAP) ist dies die ID, die diesen BookStack-Benutzer mit dem Authentifizierungs-Systemkonto verknüpft. Sie können dieses Feld ignorieren, wenn Sie die Standard-E-Mail-basierte Authentifizierung verwenden.',\n    'users_password_warning' => 'Füllen Sie die untenstehenden Felder nur aus, wenn Sie das Passwort für diesen Benutzer ändern möchten.',\n    'users_system_public' => 'Dieser Benutzer repräsentiert alle unangemeldeten Benutzer, die diese Seite betrachten. Er kann nicht zum Anmelden benutzt werden, sondern wird automatisch zugeordnet.',\n    'users_delete' => 'Benutzer löschen',\n    'users_delete_named' => 'Benutzer \":userName\" löschen',\n    'users_delete_warning' => 'Der Benutzer \":userName\" wird aus dem System gelöscht.',\n    'users_delete_confirm' => 'Sind Sie sicher, dass Sie diesen Benutzer löschen möchten?',\n    'users_migrate_ownership' => 'Besitz migrieren',\n    'users_migrate_ownership_desc' => 'Wählen Sie hier einen Benutzer, wenn Sie möchten, dass ein anderer Benutzer der Besitzer aller Einträge wird, die diesem Benutzer derzeit gehören.',\n    'users_none_selected' => 'Kein Benutzer ausgewählt',\n    'users_edit' => 'Benutzer bearbeiten',\n    'users_edit_profile' => 'Profil bearbeiten',\n    'users_avatar' => 'Benutzer-Bild',\n    'users_avatar_desc' => 'Das Bild sollte eine Auflösung von 256x256px haben.',\n    'users_preferred_language' => 'Bevorzugte Sprache',\n    'users_preferred_language_desc' => 'Diese Option ändert die Sprache, die für die Benutzeroberfläche der Anwendung verwendet wird. Dies hat keinen Einfluss auf von Benutzern erstellte Inhalte.',\n    'users_social_accounts' => 'Social-Media Konten',\n    'users_social_accounts_desc' => 'Zeigt den Status der verbundenen sozialen Konten für diesen Benutzer an. Social Accounts können zusätzlich zum primären Authentifizierungssystem für den Systemzugriff verwendet werden.',\n    'users_social_accounts_info' => 'Hier können Sie andere Social-Media-Konten für eine schnellere und einfachere Anmeldung verknüpfen. Wenn Sie ein Social-Media Konto lösen, bleibt der Zugriff erhalten. Entfernen Sie in diesem Falle die Berechtigung in Ihren Profil-Einstellungen des verknüpften Social-Media-Kontos.',\n    'users_social_connect' => 'Social-Media-Konto verknüpfen',\n    'users_social_disconnect' => 'Social-Media-Konto löschen',\n    'users_social_status_connected' => 'Verbunden',\n    'users_social_status_disconnected' => 'Getrennt',\n    'users_social_connected' => ':socialAccount-Konto wurde erfolgreich mit dem Profil verknüpft.',\n    'users_social_disconnected' => ':socialAccount-Konto wurde erfolgreich vom Profil gelöst.',\n    'users_api_tokens' => 'API-Token',\n    'users_api_tokens_desc' => 'Erstellen und verwalten Sie die Zugangs-Tokens zur Authentifizierung mit der BookStack REST API. Berechtigungen für die API werden über den Benutzer verwaltet, dem das Token gehört.',\n    'users_api_tokens_none' => 'Für diesen Benutzer wurden keine API-Token erstellt',\n    'users_api_tokens_create' => 'Token erstellen',\n    'users_api_tokens_expires' => 'Endet',\n    'users_api_tokens_docs' => 'API Dokumentation',\n    'users_mfa' => 'Multi-Faktor-Authentifizierung',\n    'users_mfa_desc' => 'Richten Sie Multi-Faktor-Authentifizierung als zusätzliche Sicherheitsstufe für Ihr Benutzerkonto ein.',\n    'users_mfa_x_methods' => ':count Methode konfiguriert|:count Methoden konfiguriert',\n    'users_mfa_configure' => 'Methoden konfigurieren',\n\n    // API Tokens\n    'user_api_token_create' => 'Neuen API-Token erstellen',\n    'user_api_token_name' => 'Name',\n    'user_api_token_name_desc' => 'Geben Sie Ihrem Token einen aussagekräftigen Namen als spätere Erinnerung an seinen Verwendungszweck.',\n    'user_api_token_expiry' => 'Ablaufdatum',\n    'user_api_token_expiry_desc' => 'Legen Sie ein Datum fest, an dem dieser Token abläuft. Nach diesem Datum funktionieren Anfragen, die mit diesem Token gestellt werden, nicht mehr. Wenn Sie dieses Feld leer lassen, wird ein Ablaufdatum von 100 Jahren in der Zukunft festgelegt.',\n    'user_api_token_create_secret_message' => 'Unmittelbar nach der Erstellung dieses Tokens wird eine \"Token ID\" & ein \"Token Kennwort\" generiert und angezeigt. Das Kennwort wird nur ein einziges Mal angezeigt. Stellen Sie also sicher, dass Sie den Inhalt an einen sicheren Ort kopieren, bevor Sie fortfahren.',\n    'user_api_token' => 'API-Token',\n    'user_api_token_id' => 'Token ID',\n    'user_api_token_id_desc' => 'Dies ist ein nicht editierbarer, vom System generierter Identifikator für diesen Token, welcher bei API-Anfragen angegeben werden muss.',\n    'user_api_token_secret' => 'Token Kennwort',\n    'user_api_token_secret_desc' => 'Dies ist ein systemgeneriertes Kennwort für diesen Token, das bei API-Anfragen zur Verfügung gestellt werden muss. Es wird nur dieses eine Mal angezeigt, deshalb kopieren Sie diesen Wert an einen sicheren und geschützten Ort.',\n    'user_api_token_created' => 'Token erstellt :timeAgo',\n    'user_api_token_updated' => 'Token aktualisiert :timeAgo',\n    'user_api_token_delete' => 'Lösche Token',\n    'user_api_token_delete_warning' => 'Dies löscht den API-Token mit dem Namen \\':tokenName\\' vollständig aus dem System.',\n    'user_api_token_delete_confirm' => 'Sind Sie sicher, dass Sie diesen API-Token löschen möchten?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhooks sind eine Möglichkeit, Daten an externe URLs zu senden, wenn bestimmte Aktionen und Ereignisse im System auftreten, was eine ereignisbasierte Integration mit externen Plattformen wie Messaging- oder Benachrichtigungssystemen ermöglicht.',\n    'webhooks_x_trigger_events' => ':count Auslöserereignis|:count Auslöserereignisse',\n    'webhooks_create' => 'Neuen Webhook erstellen',\n    'webhooks_none_created' => 'Es wurden noch keine Webhooks erstellt.',\n    'webhooks_edit' => 'Webhook bearbeiten',\n    'webhooks_save' => 'Webhook speichern',\n    'webhooks_details' => 'Webhook-Details',\n    'webhooks_details_desc' => 'Geben Sie einen benutzerfreundlichen Namen und einen POST-Endpunkt als Ort an, an den die Webhook-Daten gesendet werden sollen.',\n    'webhooks_events' => 'Webhook Ereignisse',\n    'webhooks_events_desc' => 'Wählen Sie alle Ereignisse, die diesen Webhook auslösen sollen.',\n    'webhooks_events_warning' => 'Beachten Sie, dass diese Ereignisse für alle ausgewählten Ereignisse ausgelöst werden, auch wenn benutzerdefinierte Berechtigungen angewendet werden. Stellen Sie sicher, dass die Verwendung dieses Webhook keine vertraulichen Inhalte enthüllt.',\n    'webhooks_events_all' => 'Alle System-Ereignisse',\n    'webhooks_name' => 'Webhook-Name',\n    'webhooks_timeout' => 'Webhook Request Timeout (Sekunden)',\n    'webhooks_endpoint' => 'Webhook Endpunkt',\n    'webhooks_active' => 'Webhook aktiv',\n    'webhook_events_table_header' => 'Ereignisse',\n    'webhooks_delete' => 'Webhook löschen',\n    'webhooks_delete_warning' => 'Dies wird diesen Webhook mit dem Namen \\':webhookName\\' vollständig aus dem System löschen.',\n    'webhooks_delete_confirm' => 'Sind Sie sicher, dass Sie diesen Webhook löschen möchten?',\n    'webhooks_format_example' => 'Webhook Format Beispiel',\n    'webhooks_format_example_desc' => 'Webhook Daten werden als POST-Anfrage an den konfigurierten Endpunkt als JSON im folgenden Format gesendet. Die Eigenschaften \"related_item\" und \"url\" sind optional und hängen vom Typ des ausgelösten Ereignisses ab.',\n    'webhooks_status' => 'Webhook-Status',\n    'webhooks_last_called' => 'Zuletzt aufgerufen:',\n    'webhooks_last_errored' => 'Letzter Fehler:',\n    'webhooks_last_error_message' => 'Letzte Fehlermeldung:',\n\n    // Licensing\n    'licenses' => 'Lizenzen',\n    'licenses_desc' => 'Diese Seite beschreibt Lizenzinformationen für BookStack zusätzlich zu den Projekten und Bibliotheken, die in BookStack verwendet werden. Viele aufgelistete Projekte dürfen nur in einem Entwicklungskontext verwendet werden.',\n    'licenses_bookstack' => 'BookStack-Lizenz',\n    'licenses_php' => 'PHP-Bibliothekslizenzen',\n    'licenses_js' => 'JavaScript-Bibliothekslizenzen',\n    'licenses_other' => 'Andere Lizenzen',\n    'license_details' => 'Lizenzdetails',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'Englisch',\n        'ar' => 'Arabisch',\n        'bg' => 'Bulgarisch',\n        'bs' => 'Bosnisch',\n        'ca' => 'Katalanisch',\n        'cs' => 'Tschechisch',\n        'cy' => 'Cymraeg',\n        'da' => 'Dänisch',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Spanisch',\n        'es_AR' => 'Spanisch Argentinisch',\n        'et' => 'Estnisch',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Französisch',\n        'he' => 'Hebräisch',\n        'hr' => 'Kroatisch',\n        'hu' => 'Ungarisch',\n        'id' => 'Bahasa-Indonesisch',\n        'it' => 'Italienisch',\n        'ja' => 'Japanisch',\n        'ko' => 'Koreanisch',\n        'lt' => 'Litauisch',\n        'lv' => 'Lettisch',\n        'nb' => 'Norwegisch (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Niederländisch',\n        'pl' => 'Polnisch',\n        'pt' => 'Portugiesisch',\n        'pt_BR' => 'Portugiesisch (Brasilien)',\n        'ro' => 'Română',\n        'ru' => 'Russisch',\n        'sk' => 'Slowenisch',\n        'sl' => 'Slowenisch',\n        'sv' => 'Schwedisch',\n        'tr' => 'Türkisch',\n        'uk' => 'Ukrainisch',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Vietnamesisch',\n        'zh_CN' => 'Vereinfachtes Chinesisch',\n        'zh_TW' => 'Traditionelles Chinesisch',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/de/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute muss akzeptiert werden.',\n    'active_url'           => ':attribute ist keine gültige URL.',\n    'after'                => ':attribute muss ein Datum nach :date sein.',\n    'alpha'                => ':attribute kann nur Buchstaben enthalten.',\n    'alpha_dash'           => ':attribute kann nur Buchstaben, Zahlen und Bindestriche enthalten.',\n    'alpha_num'            => ':attribute kann nur Buchstaben und Zahlen enthalten.',\n    'array'                => ':attribute muss ein Array sein.',\n    'backup_codes'         => 'Der angegebene Code ist ungültig oder wurde bereits verwendet.',\n    'before'               => ':attribute muss ein Datum vor :date sein.',\n    'between'              => [\n        'numeric' => ':attribute muss zwischen :min und :max liegen.',\n        'file'    => ':attribute muss zwischen :min und :max Kilobytes groß sein.',\n        'string'  => ':attribute muss zwischen :min und :max Zeichen lang sein.',\n        'array'   => ':attribute muss zwischen :min und :max Elemente enthalten.',\n    ],\n    'boolean'              => ':attribute Feld muss wahr oder falsch sein.',\n    'confirmed'            => ':attribute stimmt nicht überein.',\n    'date'                 => ':attribute ist kein gültiges Datum.',\n    'date_format'          => ':attribute entspricht nicht dem Format :format.',\n    'different'            => ':attribute und :other müssen unterschiedlich sein.',\n    'digits'               => ':attribute muss :digits Stellen haben.',\n    'digits_between'       => ':attribute muss zwischen :min und :max Stellen haben.',\n    'email'                => ':attribute muss eine gültige E-Mail-Adresse sein.',\n    'ends_with' => ':attribute muss mit einem der folgenden Werte: :values enden',\n    'file'                 => ':attribute muss als gültige Datei angegeben werden.',\n    'filled'               => ':attribute ist erforderlich.',\n    'gt'                   => [\n        'numeric' => ':attribute muss größer als :value sein.',\n        'file'    => ':attribute muss mindestens :value Kilobytes groß sein.',\n        'string'  => ':attribute muss mehr als :value Zeichen haben.',\n        'array'   => ':attribute muss mindestens :value Elemente haben.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute muss größer-gleich :value sein.',\n        'file'    => ':attribute muss mindestens :value Kilobytes groß sein.',\n        'string'  => ':attribute muss mindestens :value Zeichen enthalten.',\n        'array'   => ':attribute muss :value oder mehr Elemente haben.',\n    ],\n    'exists'               => ':attribute ist ungültig.',\n    'image'                => ':attribute muss ein Bild sein.',\n    'image_extension'      => ':attribute muss eine gültige und unterstützte Bild-Dateiendung haben.',\n    'in'                   => ':attribute ist ungültig.',\n    'integer'              => ':attribute muss eine Zahl sein.',\n    'ip'                   => ':attribute muss eine gültige IP-Adresse sein.',\n    'ipv4'                 => ':attribute muss eine gültige IPv4-Adresse sein.',\n    'ipv6'                 => ':attribute muss eine gültige IPv6-Adresse sein.',\n    'json'                 => 'Das Attribut muss eine gültige JSON-Zeichenfolge sein.',\n    'lt'                   => [\n        'numeric' => ':attribute muss kleiner als :value sein.',\n        'file'    => ':attribute muss kleiner als :value Kilobytes sein.',\n        'string'  => ':attribute muss weniger als :value Zeichen haben.',\n        'array'   => ':attribute muss weniger als :value Elemente haben.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute muss kleiner oder gleich :value sein.',\n        'file'    => ':attribute muss kleiner oder gleich :value Kilobytes sein.',\n        'string'  => ':attribute darf höchstens :value Zeichen besitzen.',\n        'array'   => ':attribute darf höchstens :value Elemente haben.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute darf nicht größer als :max sein.',\n        'file'    => ':attribute darf nicht größer als :max Kilobyte sein.',\n        'string'  => ':attribute darf nicht länger als :max Zeichen sein.',\n        'array'   => ':attribute darf nicht mehr als :max Elemente enthalten.',\n    ],\n    'mimes'                => ':attribute muss eine Datei vom Typ: :values sein.',\n    'min'                  => [\n        'numeric' => ':attribute muss mindestens :min sein',\n        'file'    => ':attribute muss mindestens :min Kilobyte groß sein.',\n        'string'  => ':attribute muss mindestens :min Zeichen lang sein.',\n        'array'   => ':attribute muss mindesten :min Elemente enthalten.',\n    ],\n    'not_in'               => 'Das ausgewählte :attribute ist ungültig.',\n    'not_regex'            => ':attribute ist kein gültiges Format.',\n    'numeric'              => ':attribute muss eine Zahl sein.',\n    'regex'                => ':attribute ist in einem ungültigen Format.',\n    'required'             => ':attribute ist erforderlich.',\n    'required_if'          => ':attribute ist erforderlich, wenn :other :value ist.',\n    'required_with'        => ':attribute ist erforderlich, wenn :values vorhanden ist.',\n    'required_with_all'    => ':attribute ist erforderlich, wenn :values vorhanden sind.',\n    'required_without'     => ':attribute ist erforderlich, wenn :values nicht vorhanden ist.',\n    'required_without_all' => ':attribute ist erforderlich, wenn :values nicht vorhanden sind.',\n    'same'                 => ':attribute und :other müssen übereinstimmen.',\n    'safe_url'             => 'Der angegebene Link ist möglicherweise nicht sicher.',\n    'size'                 => [\n        'numeric' => ':attribute muss :size sein.',\n        'file'    => ':attribute muss :size Kilobytes groß sein.',\n        'string'  => ':attribute muss :size Zeichen lang sein.',\n        'array'   => ':attribute muss :size Elemente enthalten.',\n    ],\n    'string'               => ':attribute muss eine Zeichenkette sein.',\n    'timezone'             => ':attribute muss eine gültige Zeitzone sein.',\n    'totp'                 => 'Der angegebene Code ist ungültig oder abgelaufen.',\n    'unique'               => ':attribute wird bereits verwendet.',\n    'url'                  => ':attribute ist kein gültiges Format.',\n    'uploaded'             => 'Die Datei konnte nicht hochgeladen werden. Der Server akzeptiert möglicherweise keine Dateien dieser Größe.',\n\n    'zip_file' => ':attribute muss eine Datei innerhalb des ZIP referenzieren.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => ':attribute muss eine Datei des Typs :validType referenzieren, gefunden :foundType.',\n    'zip_model_expected' => 'Datenobjekt erwartet, aber \":type\" gefunden.',\n    'zip_unique' => ':attribute muss für den Objekttyp innerhalb des ZIP eindeutig sein.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Passwortbestätigung erforderlich',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/de_informal/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'erstellte Seite',\n    'page_create_notification'    => 'Seite erfolgreich erstellt',\n    'page_update'                 => 'aktualisierte Seite',\n    'page_update_notification'    => 'Seite erfolgreich aktualisiert',\n    'page_delete'                 => 'löschte Seite',\n    'page_delete_notification'    => 'Seite erfolgreich gelöscht',\n    'page_restore'                => 'stellte Seite wieder her',\n    'page_restore_notification'   => 'Seite erfolgreich wiederhergestellt',\n    'page_move'                   => 'verschob Seite',\n    'page_move_notification'      => 'Seite erfolgreich verschoben',\n\n    // Chapters\n    'chapter_create'              => 'erstellte Kapitel',\n    'chapter_create_notification' => 'Kapitel erfolgreich erstellt',\n    'chapter_update'              => 'aktualisierte Kapitel',\n    'chapter_update_notification' => 'Kapitel erfolgreich aktualisiert',\n    'chapter_delete'              => 'löschte Kapitel',\n    'chapter_delete_notification' => 'Kapitel erfolgreich gelöscht',\n    'chapter_move'                => 'verschob Kapitel',\n    'chapter_move_notification' => 'Kapitel erfolgreich verschoben',\n\n    // Books\n    'book_create'                 => 'erstellte Buch',\n    'book_create_notification'    => 'Buch erfolgreich erstellt',\n    'book_create_from_chapter'              => 'wandelte Kapitel zu Buch um',\n    'book_create_from_chapter_notification' => 'Kapitel erfolgreich in ein Buch umgewandelt',\n    'book_update'                 => 'aktualisierte Buch',\n    'book_update_notification'    => 'Buch erfolgreich aktualisiert',\n    'book_delete'                 => 'löschte Buch',\n    'book_delete_notification'    => 'Buch erfolgreich gelöscht',\n    'book_sort'                   => 'sortierte Buch',\n    'book_sort_notification'      => 'Buch erfolgreich umsortiert',\n\n    // Bookshelves\n    'bookshelf_create'            => 'erstellte Regal',\n    'bookshelf_create_notification'    => 'Regal erfolgreich erstellt',\n    'bookshelf_create_from_book'    => 'wandelte Buch zu Regal um',\n    'bookshelf_create_from_book_notification'    => 'Buch erfolgreich zu einem Regal umgewandelt',\n    'bookshelf_update'                 => 'aktualisierte Regal',\n    'bookshelf_update_notification'    => 'Regal erfolgreich aktualisiert',\n    'bookshelf_delete'                 => 'löschte Regal',\n    'bookshelf_delete_notification'    => 'Regal erfolgreich gelöscht',\n\n    // Revisions\n    'revision_restore' => 'stellte Revision wieder her',\n    'revision_delete' => 'Revision gelöscht',\n    'revision_delete_notification' => 'Revision erfolgreich gelöscht',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" wurde zu deinen Favoriten hinzugefügt',\n    'favourite_remove_notification' => '\":name\" wurde aus deinen Favoriten entfernt',\n\n    // Watching\n    'watch_update_level_notification' => 'Beobachtungseinstellungen erfolgreich aktualisiert',\n\n    // Auth\n    'auth_login' => 'hat sich eingeloggt',\n    'auth_register' => 'hat sich als neuer Benutzer registriert',\n    'auth_password_reset_request' => 'hat eine Rücksetzung des Benutzerpassworts beantragt',\n    'auth_password_reset_update' => 'Benutzerpasswort zurückgesetzt',\n    'mfa_setup_method' => 'hat MFA-Methode konfiguriert',\n    'mfa_setup_method_notification' => 'Multi-Faktor-Methode erfolgreich konfiguriert',\n    'mfa_remove_method' => 'hat MFA-Methode entfernt',\n    'mfa_remove_method_notification' => 'Multi-Faktor-Methode erfolgreich entfernt',\n\n    // Settings\n    'settings_update' => 'hat Einstellungen aktualisiert',\n    'settings_update_notification' => 'Einstellungen erfolgreich aktualisiert',\n    'maintenance_action_run' => 'hat Wartungsarbeiten ausgeführt',\n\n    // Webhooks\n    'webhook_create' => 'erstellter Webhook',\n    'webhook_create_notification' => 'Webhook erfolgreich eingerichtet',\n    'webhook_update' => 'aktualisierter Webhook',\n    'webhook_update_notification' => 'Webhook erfolgreich aktualisiert',\n    'webhook_delete' => 'gelöschter Webhook',\n    'webhook_delete_notification' => 'Webhook erfolgreich gelöscht',\n\n    // Imports\n    'import_create' => 'erstellter Import',\n    'import_create_notification' => 'Import erfolgreich hochgeladen',\n    'import_run' => 'aktualisierter Import',\n    'import_run_notification' => 'Inhalt erfolgreich importiert',\n    'import_delete' => 'gelöschter Import',\n    'import_delete_notification' => 'Import erfolgreich gelöscht',\n\n    // Users\n    'user_create' => 'hat Benutzer erzeugt:',\n    'user_create_notification' => 'Benutzer erfolgreich erstellt',\n    'user_update' => 'hat Benutzer aktualisiert:',\n    'user_update_notification' => 'Benutzer erfolgreich aktualisiert',\n    'user_delete' => 'hat Benutzer gelöscht: ',\n    'user_delete_notification' => 'Benutzer erfolgreich entfernt',\n\n    // API Tokens\n    'api_token_create' => 'API Token wurde erstellt',\n    'api_token_create_notification' => 'API-Token erfolgreich erstellt',\n    'api_token_update' => 'API Token wurde aktualisiert',\n    'api_token_update_notification' => 'API-Token erfolgreich aktualisiert',\n    'api_token_delete' => 'API Token gelöscht',\n    'api_token_delete_notification' => 'API-Token erfolgreich gelöscht',\n\n    // Roles\n    'role_create' => 'hat Rolle erzeugt:',\n    'role_create_notification' => 'Rolle erfolgreich erstellt',\n    'role_update' => 'hat Rolle aktualisiert:',\n    'role_update_notification' => 'Rolle erfolgreich aktualisiert',\n    'role_delete' => 'hat Rolle gelöscht:',\n    'role_delete_notification' => 'Rolle erfolgreich gelöscht',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'hat den Papierkorb geleert',\n    'recycle_bin_restore' => 'aus dem Papierkorb wiederhergestellt',\n    'recycle_bin_destroy' => 'aus dem Papierkorb gelöscht',\n\n    // Comments\n    'commented_on'                => 'kommentiert',\n    'comment_create'              => 'Kommentar hinzugefügt',\n    'comment_update'              => 'Kommentar aktualisiert',\n    'comment_delete'              => 'Kommentar gelöscht',\n\n    // Sort Rules\n    'sort_rule_create' => 'hat eine Sortierregel erstellt',\n    'sort_rule_create_notification' => 'Sortierregel erfolgreich angelegt',\n    'sort_rule_update' => 'hat eine Sortierregel aktualisiert',\n    'sort_rule_update_notification' => 'Sortierregel erfolgreich aktualisiert',\n    'sort_rule_delete' => 'hat eine Sortierregel gelöscht',\n    'sort_rule_delete_notification' => 'Sortierregel erfolgreich gelöscht',\n\n    // Other\n    'permissions_update'          => 'aktualisierte Berechtigungen',\n];\n"
  },
  {
    "path": "lang/de_informal/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Die eingegebenen Anmeldedaten sind ungültig.',\n    'throttle' => 'Zu viele Anmeldeversuche. Bitte versuche es in :seconds Sekunden erneut.',\n\n    // Login & Register\n    'sign_up' => 'Registrieren',\n    'log_in' => 'Anmelden',\n    'log_in_with' => 'Anmelden mit :socialDriver',\n    'sign_up_with' => 'Registrieren mit :socialDriver',\n    'logout' => 'Abmelden',\n\n    'name' => 'Name',\n    'username' => 'Benutzername',\n    'email' => 'E-Mail',\n    'password' => 'Passwort',\n    'password_confirm' => 'Passwort bestätigen',\n    'password_hint' => 'Muss mindestens 8 Zeichen lang sein',\n    'forgot_password' => 'Passwort vergessen?',\n    'remember_me' => 'Angemeldet bleiben',\n    'ldap_email_hint' => 'Bitte gib eine E-Mail-Adresse ein, um diese mit dem Account zu nutzen.',\n    'create_account' => 'Account registrieren',\n    'already_have_account' => 'Bereits ein Konto erstellt?',\n    'dont_have_account' => 'Noch kein Konto erstellt?',\n    'social_login' => 'Mit Sozialem Netzwerk anmelden',\n    'social_registration' => 'Mit Sozialem Netzwerk registrieren',\n    'social_registration_text' => 'Mit einem dieser Dienste registrieren oder anmelden',\n\n    'register_thanks' => 'Vielen Dank für deine Registrierung!',\n    'register_confirm' => 'Bitte prüfe deinen Posteingang und bestätige die Registrierung, um :appName nutzen zu können.',\n    'registrations_disabled' => 'Eine Registrierung ist momentan nicht möglich',\n    'registration_email_domain_invalid' => 'Du kannst dich mit dieser E-Mail nicht registrieren.',\n    'register_success' => 'Vielen Dank für deine Registrierung! Du bist jetzt registriert und eingeloggt.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Versuche Anmeldung',\n    'auto_init_starting_desc' => 'Wir kontaktieren dein Authentifizierungssystem, um den Anmeldevorgang zu starten. Wenn nach 5 Sekunden kein Fortschritt zu sehen ist, kannst du versuchen, auf den unten stehenden Link zu klicken.',\n    'auto_init_start_link' => 'Mit der Authentifizierung fortfahren',\n\n    // Password Reset\n    'reset_password' => 'Passwort vergessen',\n    'reset_password_send_instructions' => 'Bitte gib Deine E-Mail-Adresse ein. Danach erhältst Du eine E-Mail mit einem Link zum Zurücksetzen deines Passwortes.',\n    'reset_password_send_button' => 'Passwort zurücksetzen',\n    'reset_password_sent' => 'Ein Link zum Zurücksetzen des Passworts wird an :email gesendet, wenn diese E-Mail-Adresse im System gefunden wird.',\n    'reset_password_success' => 'Dein Passwort wurde erfolgreich zurückgesetzt.',\n    'email_reset_subject' => 'Passwort zurücksetzen für :appName',\n    'email_reset_text' => 'Du erhältst diese E-Mail, weil jemand versucht hat, dein Passwort zurückzusetzen.',\n    'email_reset_not_requested' => 'Wenn du das Zurücksetzen des Passworts nicht angefordert hast, ist keine weitere Aktion erforderlich.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Bestätige Deine E-Mail-Adresse für :appName',\n    'email_confirm_greeting' => 'Danke, dass Du dich für :appName registrierst hast!',\n    'email_confirm_text' => 'Bitte bestätige Deine E-Mail-Adresse, indem Du auf die Schaltfläche klickst:',\n    'email_confirm_action' => 'E-Mail-Adresse bestätigen',\n    'email_confirm_send_error' => 'Leider konnte die für die Registrierung notwendige E-Mail zur Bestätigung deiner E-Mail-Adresse nicht versandt werden. Bitte kontaktiere deinen Systemadministrator!',\n    'email_confirm_success' => 'Deine E-Mail Adresse wurde bestätigt! Du solltest nun in der Lage sein, dich mit deiner E-Mail-Adresse anzumelden.',\n    'email_confirm_resent' => 'Bestätigungs-E-Mail wurde erneut versendet, bitte überprüfe deinen Posteingang.',\n    'email_confirm_thanks' => 'Vielen Dank für das Bestätigen!',\n    'email_confirm_thanks_desc' => 'Bitte warte einen Augenblick, während deine Bestätigung bearbeitet wird. Wenn Du nach 3 Sekunden nicht weitergeleitet wirst, drücke unten den \"Weiter\" Link, um fortzufahren.',\n\n    'email_not_confirmed' => 'E-Mail-Adresse ist nicht bestätigt',\n    'email_not_confirmed_text' => 'Deine E-Mail-Adresse ist bisher nicht bestätigt.',\n    'email_not_confirmed_click_link' => 'Bitte klicke auf den Link in der E-Mail, die du nach der Registrierung erhalten hast.',\n    'email_not_confirmed_resend' => 'Wenn Du die E-Mail nicht erhalten hast, kannst Du die Nachricht erneut anfordern. Fülle hierzu bitte das folgende Formular aus:',\n    'email_not_confirmed_resend_button' => 'Bestätigungs-E-Mail erneut senden',\n\n    // User Invite\n    'user_invite_email_subject' => 'Du wurdest eingeladen :appName beizutreten!',\n    'user_invite_email_greeting' => 'Ein Konto wurde für dich auf :appName erstellt.',\n    'user_invite_email_text' => 'Klicke auf die Schaltfläche unten, um ein Passwort festzulegen und Zugriff zu erhalten:',\n    'user_invite_email_action' => 'Konto-Passwort festlegen',\n    'user_invite_page_welcome' => 'Willkommen bei :appName!',\n    'user_invite_page_text' => 'Um die Anmeldung abzuschließen und Zugriff auf :appName zu bekommen, muss noch ein Passwort festgelegt werden. Dieses wird in Zukunft für die Anmeldung benötigt.',\n    'user_invite_page_confirm_button' => 'Passwort bestätigen',\n    'user_invite_success_login' => 'Passwort gesetzt, du solltest nun in der Lage sein, dich mit deinem Passwort an :appName anzumelden!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Multi-Faktor-Authentifizierung einrichten',\n    'mfa_setup_desc' => 'Richte Multi-Faktor-Authentifizierung als zusätzliche Sicherheitsstufe für dein Benutzerkonto ein.',\n    'mfa_setup_configured' => 'Bereits konfiguriert',\n    'mfa_setup_reconfigure' => 'Umkonfigurieren',\n    'mfa_setup_remove_confirmation' => 'Bist du sicher, dass du diese Multi-Faktor-Authentifizierungsmethode entfernen möchtest?',\n    'mfa_setup_action' => 'Einrichtung',\n    'mfa_backup_codes_usage_limit_warning' => 'Du hast weniger als 5 Backup-Codes übrig. Bitte erstelle und speichere einen neuen Satz,  bevor Du keine Codes mehr hast, um zu verhindern, dass du von deinem Konto ausgesperrt wirst.',\n    'mfa_option_totp_title' => 'Mobile App',\n    'mfa_option_totp_desc' => 'Um Mehrfach-Faktor-Authentifizierung nutzen zu können, benötigst du eine mobile Anwendung, die TOTP unterstützt, wie Google Authenticator, Authy oder Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Backup Code',\n    'mfa_option_backup_codes_desc' => 'Erzeugt eine Reihe von einmalig nutzbaren Backup-Codes, welche Du bei der Anmeldung eingibst, um deine Identität zu bestätigen. Achte darauf diese an einem sicheren Ort aufzubewahren.',\n    'mfa_gen_confirm_and_enable' => 'Bestätigen und aktivieren',\n    'mfa_gen_backup_codes_title' => 'Backup-Codes einrichten',\n    'mfa_gen_backup_codes_desc' => 'Speichere die folgende Liste der Codes an einem sicheren Ort. Wenn du auf das System zugreifst, kannst du einen der Codes als zweiten Authentifizierungsmechanismus verwenden.',\n    'mfa_gen_backup_codes_download' => 'Codes herunterladen',\n    'mfa_gen_backup_codes_usage_warning' => 'Jeder Code kann nur einmal verwendet werden',\n    'mfa_gen_totp_title' => 'Mobile App einrichten',\n    'mfa_gen_totp_desc' => 'Um Mehrfach-Faktor-Authentifizierung nutzen zu können, benötigst du eine mobile Anwendung, die TOTP unterstützt, wie Google Authenticator, Authy oder Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Scanne den QR-Code unten mit deiner bevorzugten Authentifizierungs-App, um zu beginnen.',\n    'mfa_gen_totp_verify_setup' => 'Setup überprüfen',\n    'mfa_gen_totp_verify_setup_desc' => 'Überprüfe, dass alles funktioniert, indem du einen Code aus deiner Authentifizierungs-App in das Eingabefeld unten eingibst:',\n    'mfa_gen_totp_provide_code_here' => 'Gib hier den von der App generierten Code ein',\n    'mfa_verify_access' => 'Zugriff überprüfen',\n    'mfa_verify_access_desc' => 'Dein Benutzerkonto erfordert, dass du deine Identität über eine zusätzliche Verifikationsebene bestätigst, bevor du Zugriff erhältst. Verifiziere diese mit einer deiner konfigurierten Methoden, um fortzufahren.',\n    'mfa_verify_no_methods' => 'Keine Methoden konfiguriert',\n    'mfa_verify_no_methods_desc' => 'Es konnten keine Multi-Faktor-Authentifizierungsmethoden für dein Konto gefunden werden. Du musst mindestens eine Methode einrichten, bevor du Zugriff erhältst.',\n    'mfa_verify_use_totp' => 'Mit einer mobilen App verifizieren',\n    'mfa_verify_use_backup_codes' => 'Mit einem Backup-Code verifizieren',\n    'mfa_verify_backup_code' => 'Backup-Code',\n    'mfa_verify_backup_code_desc' => 'Gib einen deiner verbleibenden Backup-Codes unten ein:',\n    'mfa_verify_backup_code_enter_here' => 'Backup-Code hier eingeben',\n    'mfa_verify_totp_desc' => 'Gib den Code ein, der mit deiner mobilen App generiert wurde:',\n    'mfa_setup_login_notification' => 'Multi-Faktor-Methode konfiguriert. Bitte melde dich jetzt erneut mit der konfigurierten Methode an.',\n];\n"
  },
  {
    "path": "lang/de_informal/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Abbrechen',\n    'close' => 'Schließen',\n    'confirm' => 'Bestätigen',\n    'back' => 'Zurück',\n    'save' => 'Speichern',\n    'continue' => 'Weiter',\n    'select' => 'Auswählen',\n    'toggle_all' => 'Alle umschalten',\n    'more' => 'Mehr',\n\n    // Form Labels\n    'name' => 'Name',\n    'description' => 'Beschreibung',\n    'role' => 'Rolle',\n    'cover_image' => 'Titelbild',\n    'cover_image_description' => 'Dieses Bild sollte ungefähr 440 x 250 Pixel groß sein, kann jedoch je nach Bedarf flexibel skaliert und zugeschnitten werden, um es an die Benutzeroberfläche in verschiedenen Szenarien anzupassen, sodass die tatsächlichen Abmessungen für die Anzeige abweichen können.',\n\n    // Actions\n    'actions' => 'Aktionen',\n    'view' => 'Anzeigen',\n    'view_all' => 'Alle anzeigen',\n    'new' => 'Neu',\n    'create' => 'Anlegen',\n    'update' => 'Aktualisieren',\n    'edit' => 'Bearbeiten',\n    'archive' => 'Archivieren',\n    'unarchive' => 'Nicht mehr archivieren',\n    'sort' => 'Sortieren',\n    'move' => 'Verschieben',\n    'copy' => 'Kopieren',\n    'reply' => 'Antworten',\n    'delete' => 'Löschen',\n    'delete_confirm' => 'Löschen bestätigen',\n    'search' => 'Suchen',\n    'search_clear' => 'Suche löschen',\n    'reset' => 'Zurücksetzen',\n    'remove' => 'Entfernen',\n    'add' => 'Hinzufügen',\n    'configure' => 'Konfigurieren',\n    'manage' => 'Verwalten',\n    'fullscreen' => 'Vollbild',\n    'favourite' => 'Favoriten',\n    'unfavourite' => 'Kein Favorit',\n    'next' => 'Nächste',\n    'previous' => 'Vorheriges',\n    'filter_active' => 'Gesetzte Filter:',\n    'filter_clear' => 'Filter löschen',\n    'download' => 'Herunterladen',\n    'open_in_tab' => 'In Tab öffnen',\n    'open' => 'Öffnen',\n\n    // Sort Options\n    'sort_options' => 'Sortieroptionen',\n    'sort_direction_toggle' => 'Sortierreihenfolge umkehren',\n    'sort_ascending' => 'Aufsteigend sortieren',\n    'sort_descending' => 'Absteigend sortieren',\n    'sort_name' => 'Name',\n    'sort_default' => 'Standard',\n    'sort_created_at' => 'Erstellungsdatum',\n    'sort_updated_at' => 'Aktualisierungsdatum',\n\n    // Misc\n    'deleted_user' => 'Gelöschter Benutzer',\n    'no_activity' => 'Keine Aktivitäten zum Anzeigen',\n    'no_items' => 'Keine Einträge gefunden.',\n    'back_to_top' => 'nach oben',\n    'skip_to_main_content' => 'Direkt zum Hauptinhalt',\n    'toggle_details' => 'Details zeigen/verstecken',\n    'toggle_thumbnails' => 'Thumbnails zeigen/verstecken',\n    'details' => 'Details',\n    'grid_view' => 'Gitteransicht',\n    'list_view' => 'Listenansicht',\n    'default' => 'Voreinstellung',\n    'breadcrumb' => 'Brotkrumen',\n    'status' => 'Status',\n    'status_active' => 'Aktiv',\n    'status_inactive' => 'Inaktiv',\n    'never' => 'Niemals',\n    'none' => 'Keine',\n\n    // Header\n    'homepage' => 'Startseite',\n    'header_menu_expand' => 'Header-Menü erweitern',\n    'profile_menu' => 'Profilmenü',\n    'view_profile' => 'Profil ansehen',\n    'edit_profile' => 'Profil bearbeiten',\n    'dark_mode' => 'Dunkler Modus',\n    'light_mode' => 'Heller Modus',\n    'global_search' => 'Globale Suche',\n\n    // Layout tabs\n    'tab_info' => 'Info',\n    'tab_info_label' => 'Tab: Sekundäre Informationen anzeigen',\n    'tab_content' => 'Inhalt',\n    'tab_content_label' => 'Tab: Hauptinhalt anzeigen',\n\n    // Email Content\n    'email_action_help' => 'Sollte es beim Anklicken der Schaltfläche \":actionText\" Probleme geben, öffne die folgende URL in Deinem Browser:',\n    'email_rights' => 'Alle Rechte vorbehalten',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Datenschutzerklärung',\n    'terms_of_service' => 'Allgemeine Geschäftsbedingungen',\n\n    // OpenSearch\n    'opensearch_description' => 'Suche :appName',\n];\n"
  },
  {
    "path": "lang/de_informal/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Bild auswählen',\n    'image_list' => 'Bilderliste',\n    'image_details' => 'Bilddetails',\n    'image_upload' => 'Bild hochladen',\n    'image_intro' => 'Hier kannst du die zuvor hochgeladenen Bilder auswählen und verwalten.',\n    'image_intro_upload' => 'Lade ein neues Bild hoch, indem du eine Bilddatei in dieses Fenster ziehst oder auf die Schaltfläche \"Bild hochladen\" oben klickst.',\n    'image_all' => 'Alle',\n    'image_all_title' => 'Alle Bilder anzeigen',\n    'image_book_title' => 'Zeige alle Bilder, die in dieses Buch hochgeladen wurden',\n    'image_page_title' => 'Zeige alle Bilder, die auf diese Seite hochgeladen wurden',\n    'image_search_hint' => 'Nach Bildnamen suchen',\n    'image_uploaded' => 'Hochgeladen am :uploadedDate',\n    'image_uploaded_by' => 'Hochgeladen von :userName',\n    'image_uploaded_to' => 'Hochgeladen auf :pageLink',\n    'image_updated' => 'Aktualisiert am :updateDate',\n    'image_load_more' => 'Mehr',\n    'image_image_name' => 'Bildname',\n    'image_delete_used' => 'Dieses Bild wird auf den folgenden Seiten benutzt.',\n    'image_delete_confirm_text' => 'Bist Du sicher, dass Du diese Seite löschen möchtest?',\n    'image_select_image' => 'Bild auswählen',\n    'image_dropzone' => 'Ziehe Bilder hierher oder klicke hier, um ein Bild auszuwählen',\n    'image_dropzone_drop' => 'Ziehe Dateien hierher, um sie hochzuladen',\n    'images_deleted' => 'Bilder gelöscht',\n    'image_preview' => 'Bildvorschau',\n    'image_upload_success' => 'Bild erfolgreich hochgeladen',\n    'image_update_success' => 'Bilddetails erfolgreich aktualisiert',\n    'image_delete_success' => 'Bild erfolgreich gelöscht',\n    'image_replace' => 'Bild ersetzen',\n    'image_replace_success' => 'Bild erfolgreich aktualisiert',\n    'image_rebuild_thumbs' => 'Größenvariationen neu generieren',\n    'image_rebuild_thumbs_success' => 'Bildgrößenvariationen erfolgreich neu erstellt!',\n\n    // Code Editor\n    'code_editor' => 'Code editieren',\n    'code_language' => 'Code Sprache',\n    'code_content' => 'Code Inhalt',\n    'code_session_history' => 'Sitzungsverlauf',\n    'code_save' => 'Code speichern',\n];\n"
  },
  {
    "path": "lang/de_informal/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Allgemein',\n    'advanced' => 'Erweitert',\n    'none' => 'Keine',\n    'cancel' => 'Abbrechen',\n    'save' => 'Speichern',\n    'close' => 'Schließen',\n    'apply' => 'Übernehmen',\n    'undo' => 'Rückgängig machen',\n    'redo' => 'Wiederholen',\n    'left' => 'Links',\n    'center' => 'Zentriert',\n    'right' => 'Rechts',\n    'top' => 'Oben',\n    'middle' => 'Mittig',\n    'bottom' => 'Unten',\n    'width' => 'Breite',\n    'height' => 'Höhe',\n    'More' => 'Mehr',\n    'select' => 'Auswählen...',\n\n    // Toolbar\n    'formats' => 'Formate',\n    'header_large' => 'Große Überschrift',\n    'header_medium' => 'Mittlere Überschrift',\n    'header_small' => 'Kleine Überschrift',\n    'header_tiny' => 'Sehr kleine Überschrift',\n    'paragraph' => 'Absatz',\n    'blockquote' => 'Blockzitat',\n    'inline_code' => 'Inline-Code',\n    'callouts' => 'Anmerkungen',\n    'callout_information' => 'Information',\n    'callout_success' => 'Erfolg',\n    'callout_warning' => 'Warnung',\n    'callout_danger' => 'Gefahr',\n    'bold' => 'Fett',\n    'italic' => 'Kursiv',\n    'underline' => 'Unterstrichen',\n    'strikethrough' => 'Durchgestrichen',\n    'superscript' => 'Hochgestellt',\n    'subscript' => 'Tiefgestellt',\n    'text_color' => 'Textfarbe',\n    'highlight_color' => 'Markierungsfarbe',\n    'custom_color' => 'Benutzerdefinierte Farbe',\n    'remove_color' => 'Farbe entfernen',\n    'background_color' => 'Hintergrundfarbe',\n    'align_left' => 'Linksbündig',\n    'align_center' => 'Zentriert',\n    'align_right' => 'Rechtsbündig',\n    'align_justify' => 'Blocksatz',\n    'list_bullet' => 'Aufzählung',\n    'list_numbered' => 'Nummerierte Aufzählung',\n    'list_task' => 'Aufgabenliste',\n    'indent_increase' => 'Einzug vergrößern',\n    'indent_decrease' => 'Einzug verkleinern',\n    'table' => 'Tabelle',\n    'insert_image' => 'Bild einfügen',\n    'insert_image_title' => 'Bild einfügen/bearbeiten',\n    'insert_link' => 'Link einfügen/bearbeiten',\n    'insert_link_title' => 'Link einfügen/bearbeiten',\n    'insert_horizontal_line' => 'Horizontale Linie einfügen',\n    'insert_code_block' => 'Codeblock einfügen',\n    'edit_code_block' => 'Codeblock bearbeiten',\n    'insert_drawing' => 'Zeichnung einfügen/bearbeiten',\n    'drawing_manager' => 'Zeichnungsmanager',\n    'insert_media' => 'Medien einfügen/bearbeiten',\n    'insert_media_title' => 'Medien einfügen/bearbeiten',\n    'clear_formatting' => 'Formatierung löschen',\n    'source_code' => 'Quellcode',\n    'source_code_title' => 'Quellcode',\n    'fullscreen' => 'Vollbild',\n    'image_options' => 'Bildoptionen',\n\n    // Tables\n    'table_properties' => 'Tabelleneigenschaften',\n    'table_properties_title' => 'Tabelleneigenschaften',\n    'delete_table' => 'Tabelle löschen',\n    'table_clear_formatting' => 'Tabellenformatierung entfernen',\n    'resize_to_contents' => 'Größe an Inhalt anpassen',\n    'row_header' => 'Zeilenüberschrift',\n    'insert_row_before' => 'Zeile oberhalb einfügen',\n    'insert_row_after' => 'Zeile unterhalb einfügen',\n    'delete_row' => 'Zeile löschen',\n    'insert_column_before' => 'Spalte davor einfügen',\n    'insert_column_after' => 'Spalte danach einfügen',\n    'delete_column' => 'Spalte löschen',\n    'table_cell' => 'Zelle',\n    'table_row' => 'Zeile',\n    'table_column' => 'Spalte',\n    'cell_properties' => 'Zelleneigenschaften',\n    'cell_properties_title' => 'Zelleneigenschaften',\n    'cell_type' => 'Zellentyp',\n    'cell_type_cell' => 'Zelle',\n    'cell_scope' => 'Bereich',\n    'cell_type_header' => 'Überschriftszelle',\n    'merge_cells' => 'Zellen verbinden',\n    'split_cell' => 'Zellen teilen',\n    'table_row_group' => 'Zeilengruppe',\n    'table_column_group' => 'Spaltengruppe',\n    'horizontal_align' => 'Horizontal ausrichten',\n    'vertical_align' => 'Vertikal ausrichten',\n    'border_width' => 'Rahmenbreite',\n    'border_style' => 'Rahmenstil',\n    'border_color' => 'Rahmenfarbe',\n    'row_properties' => 'Zeileneigenschaften',\n    'row_properties_title' => 'Zeileneigenschaften',\n    'cut_row' => 'Zeile ausschneiden',\n    'copy_row' => 'Zeile kopieren',\n    'paste_row_before' => 'Zeile davor einfügen',\n    'paste_row_after' => 'Zeile danach einfügen',\n    'row_type' => 'Zeilentyp',\n    'row_type_header' => 'Kopfzeile',\n    'row_type_body' => 'Hauptteil',\n    'row_type_footer' => 'Fußzeile',\n    'alignment' => 'Ausrichtung',\n    'cut_column' => 'Spalte ausschneiden',\n    'copy_column' => 'Spalte kopieren',\n    'paste_column_before' => 'Spalte davor einfügen',\n    'paste_column_after' => 'Spalte danach einfügen',\n    'cell_padding' => 'Zellenabstand',\n    'cell_spacing' => 'Zellenaußenabstand',\n    'caption' => 'Überschrift',\n    'show_caption' => 'Überschrift anzeigen',\n    'constrain' => 'Proportionen festsetzen',\n    'cell_border_solid' => 'Voll',\n    'cell_border_dotted' => 'Gepunktet',\n    'cell_border_dashed' => 'Gestrichelt',\n    'cell_border_double' => 'Doppelt',\n    'cell_border_groove' => 'Rille',\n    'cell_border_ridge' => 'Erhaben',\n    'cell_border_inset' => 'Vertiefte Fläche',\n    'cell_border_outset' => 'Erhabene Fläche',\n    'cell_border_none' => 'Keiner',\n    'cell_border_hidden' => 'Versteckt',\n\n    // Images, links, details/summary & embed\n    'source' => 'Quelle',\n    'alt_desc' => 'Alternative Beschreibung',\n    'embed' => 'Einbetten',\n    'paste_embed' => 'Füge deinen Einbettungscode unten ein:',\n    'url' => 'URL',\n    'text_to_display' => 'Anzuzeigender Text',\n    'title' => 'Titel',\n    'browse_links' => 'Links durchsuchen',\n    'open_link' => 'Link öffnen',\n    'open_link_in' => 'Link öffnen in...',\n    'open_link_current' => 'Aktuellem Fenster',\n    'open_link_new' => 'Neuem Fenster',\n    'remove_link' => 'Link entfernen',\n    'insert_collapsible' => 'Einklappbaren Block einfügen',\n    'collapsible_unwrap' => 'Entfernen',\n    'edit_label' => 'Beschriftung bearbeiten',\n    'toggle_open_closed' => 'Öffnen/Schließen',\n    'collapsible_edit' => 'Einklappbaren Block bearbeiten',\n    'toggle_label' => 'Beschriftung',\n\n    // About view\n    'about' => 'Über den Editor',\n    'about_title' => 'Über den WYSIWYG-Editor',\n    'editor_license' => 'Editorlizenz & Copyright',\n    'editor_lexical_license' => 'Dieser Editor wurde mithilfe von :lexicalLink erstellt, der unter der MIT-Lizenz bereitgestellt wird.',\n    'editor_lexical_license_link' => 'Vollständige Lizenzdetails findest du hier.',\n    'editor_tiny_license' => 'Dieser Editor wurde mit :tinyLink erstellt, das unter der MIT-Lizenz zur Verfügung gestellt wird.',\n    'editor_tiny_license_link' => 'Die Copyright- und Lizenzdetails von TinyMCE findest du hier.',\n    'save_continue' => 'Seite speichern & fortfahren',\n    'callouts_cycle' => '(Drücke weiter, um durch die Typen zu schalten)',\n    'link_selector' => 'Inhalt verlinken',\n    'shortcuts' => 'Kürzel',\n    'shortcut' => 'Kürzel',\n    'shortcuts_intro' => 'Die folgenden Kürzel sind im Editor verfügbar:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Beschreibung',\n];\n"
  },
  {
    "path": "lang/de_informal/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Kürzlich angelegt',\n    'recently_created_pages' => 'Kürzlich angelegte Seiten',\n    'recently_updated_pages' => 'Kürzlich aktualisierte Seiten',\n    'recently_created_chapters' => 'Kürzlich angelegte Kapitel',\n    'recently_created_books' => 'Kürzlich angelegte Bücher',\n    'recently_created_shelves' => 'Kürzlich angelegte Regale',\n    'recently_update' => 'Kürzlich aktualisiert',\n    'recently_viewed' => 'Kürzlich angesehen',\n    'recent_activity' => 'Kürzliche Aktivität',\n    'create_now' => 'Jetzt anlegen',\n    'revisions' => 'Versionen',\n    'meta_revision' => 'Version #:revisionCount',\n    'meta_created' => 'Erstellt: :timeLength',\n    'meta_created_name' => 'Erstellt: :timeLength von :user',\n    'meta_updated' => 'Zuletzt aktualisiert: :timeLength',\n    'meta_updated_name' => 'Zuletzt aktualisiert: :timeLength von :user',\n    'meta_owned_name' => 'Im Besitz von :user',\n    'meta_reference_count' => 'Referenziert von :count Element|Referenziert von :count Elementen',\n    'entity_select' => 'Eintrag auswählen',\n    'entity_select_lack_permission' => 'Du hast nicht die benötigte Berechtigung, um dieses Element auszuwählen',\n    'images' => 'Bilder',\n    'my_recent_drafts' => 'Meine kürzlichen Entwürfe',\n    'my_recently_viewed' => 'Kürzlich von mir angesehen',\n    'my_most_viewed_favourites' => 'Meine meistgesehenen Favoriten',\n    'my_favourites' => 'Meine Favoriten',\n    'no_pages_viewed' => 'Du hast bisher keine Seiten angesehen.',\n    'no_pages_recently_created' => 'Du hast bisher keine Seiten angelegt.',\n    'no_pages_recently_updated' => 'Du hast bisher keine Seiten aktualisiert.',\n    'export' => 'Exportieren',\n    'export_html' => 'HTML-Datei',\n    'export_pdf' => 'PDF-Datei',\n    'export_text' => 'Textdatei',\n    'export_md' => 'Markdown-Datei',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Standard-Seitenvorlage',\n    'default_template_explain' => 'Bestimme eine Seitenvorlage, die als Standardinhalt für alle Seiten verwendet wird, die innerhalb dieses Elements erstellt werden. Beachte, dass dies nur dann verwendet wird, wenn der Ersteller der Seite Lesezugriff auf die ausgewählte Vorlagen-Seite hat.',\n    'default_template_select' => 'Wähle eine Seitenvorlage',\n    'import' => 'Importieren',\n    'import_validate' => 'Import validieren',\n    'import_desc' => 'Importiere Bücher, Kapitel & Seiten mit einem ZIP-Export von der gleichen oder einer anderen Instanz. Wähle eine ZIP-Datei, um fortzufahren. Nachdem die Datei hochgeladen und bestätigt wurde, kannst Du den Import in der nächsten Ansicht konfigurieren und bestätigen.',\n    'import_zip_select' => 'ZIP-Datei zum Hochladen auswählen',\n    'import_zip_validation_errors' => 'Fehler bei der Validierung der angegebenen ZIP-Datei:',\n    'import_pending' => 'Ausstehende Importe',\n    'import_pending_none' => 'Es wurden keine Importe gestartet.',\n    'import_continue' => 'Import fortsetzen',\n    'import_continue_desc' => 'Überprüfe den Inhalt, der aus der hochgeladenen ZIP-Datei importiert werden soll. Führe den Import aus, um dessen Inhalt zu diesem System hinzuzufügen. Die hochgeladene ZIP-Importdatei wird bei erfolgreichem Import automatisch gelöscht.',\n    'import_details' => 'Importdetails',\n    'import_run' => 'Import ausführen',\n    'import_size' => 'Größe des importierten ZIP: :size',\n    'import_uploaded_at' => 'Hochgeladen :relativeTime',\n    'import_uploaded_by' => 'Hochgeladen von',\n    'import_location' => 'Import Zielort',\n    'import_location_desc' => 'Wähle einen Zielort für deinen importierten Inhalt. Du benötigst die entsprechenden Berechtigungen, um innerhalb des gewünschten Zielortes Inhalte zu erstellen.',\n    'import_delete_confirm' => 'Bist Du sicher, dass Du diesen Import löschen möchtest?',\n    'import_delete_desc' => 'Dies löscht die hochgeladene ZIP-Datei und kann nicht rückgängig gemacht werden.',\n    'import_errors' => 'Importfehler',\n    'import_errors_desc' => 'Die folgenden Fehler sind während des Importversuchs aufgetreten:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigiere in Büchern',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Berechtigungen',\n    'permissions_desc' => 'Lege hier Berechtigungen fest, um die Standardberechtigungen von Benutzerrollen zu überschreiben.',\n    'permissions_book_cascade' => 'In Büchern festgelegte Berechtigungen werden automatisch in untergeordnete Kapitel und Seiten kaskadiert, es sei denn, sie haben eigene Berechtigungen definiert.',\n    'permissions_chapter_cascade' => 'In Kapiteln festgelegte Berechtigungen werden automatisch in untergeordnete Seiten kaskadiert, es sei denn, sie haben eigene Berechtigungen definiert.',\n    'permissions_save' => 'Berechtigungen speichern',\n    'permissions_owner' => 'Besitzer',\n    'permissions_role_everyone_else' => 'Alle anderen',\n    'permissions_role_everyone_else_desc' => 'Berechtigungen für alle Rollen setzen, die nicht explizit überschrieben wurden.',\n    'permissions_role_override' => 'Berechtigungen für Rolle überschreiben',\n    'permissions_inherit_defaults' => 'Standardeinstellungen vererben',\n\n    // Search\n    'search_results' => 'Suchergebnisse',\n    'search_total_results_found' => ':count Ergebnis gefunden|:count Ergebnisse gesamt',\n    'search_clear' => 'Filter löschen',\n    'search_no_pages' => 'Keine Seiten gefunden',\n    'search_for_term' => 'Nach :term suchen',\n    'search_more' => 'Mehr Ergebnisse',\n    'search_advanced' => 'Erweiterte Suche',\n    'search_terms' => 'Suchbegriffe',\n    'search_content_type' => 'Inhaltstyp',\n    'search_exact_matches' => 'Exakte Treffer',\n    'search_tags' => 'Schlagwort-Suchen',\n    'search_options' => 'Optionen',\n    'search_viewed_by_me' => 'Schon von mir angesehen',\n    'search_not_viewed_by_me' => 'Noch nicht von mir angesehen',\n    'search_permissions_set' => 'Berechtigungen gesetzt',\n    'search_created_by_me' => 'Von mir erstellt',\n    'search_updated_by_me' => 'Von mir aktualisiert',\n    'search_owned_by_me' => 'Besitzt von mir',\n    'search_date_options' => 'Datumsoptionen',\n    'search_updated_before' => 'Aktualisiert vor',\n    'search_updated_after' => 'Aktualisiert nach',\n    'search_created_before' => 'Erstellt vor',\n    'search_created_after' => 'Erstellt nach',\n    'search_set_date' => 'Datum auswählen',\n    'search_update' => 'Suche aktualisieren',\n\n    // Shelves\n    'shelf' => 'Regal',\n    'shelves' => 'Regale',\n    'x_shelves' => ':count Regal|:count Regale',\n    'shelves_empty' => 'Es wurden noch keine Regale angelegt',\n    'shelves_create' => 'Erzeuge ein Regal',\n    'shelves_popular' => 'Beliebte Regale',\n    'shelves_new' => 'Kürzlich erstellte Regale',\n    'shelves_new_action' => 'Neues Regal',\n    'shelves_popular_empty' => 'Die beliebtesten Regale werden hier angezeigt.',\n    'shelves_new_empty' => 'Die neusten Regale werden hier angezeigt.',\n    'shelves_save' => 'Regal speichern',\n    'shelves_books' => 'Bücher in diesem Regal',\n    'shelves_add_books' => 'Buch zu diesem Regal hinzufügen',\n    'shelves_drag_books' => 'Ziehe die Bücher nach unten, um sie zu diesem Regal hinzuzufügen',\n    'shelves_empty_contents' => 'Diesem Regal sind keine Bücher zugewiesen',\n    'shelves_edit_and_assign' => 'Regal bearbeiten um Bücher hinzuzufügen',\n    'shelves_edit_named' => 'Regal :name bearbeiten',\n    'shelves_edit' => 'Regal bearbeiten',\n    'shelves_delete' => 'Regal löschen',\n    'shelves_delete_named' => 'Regal :name löschen',\n    'shelves_delete_explain' => \"Dadurch wird das Regal mit dem Namen ':name' gelöscht. Die darin enthaltenen Bücher werden nicht gelöscht.\",\n    'shelves_delete_confirmation' => 'Bist du dir sicher, dass du dieses Regal löschen möchtest?',\n    'shelves_permissions' => 'Regalberechtigungen',\n    'shelves_permissions_updated' => 'Regalberechtigungen aktualisiert',\n    'shelves_permissions_active' => 'Regalberechtigungen aktiv',\n    'shelves_permissions_cascade_warning' => 'Berechtigungen für Regale werden nicht automatisch auf die enthaltenen Bücher übertragen. Das liegt daran, dass ein Buch in mehreren Regalen vorhanden sein kann. Berechtigungen können jedoch auf untergeordnete Bücher kopiert werden, indem du die unten stehende Option verwendest.',\n    'shelves_permissions_create' => '\"Regal erstellen\"-Berechtigungen werden nur zum Kopieren von Berechtigungen für untergeordnete Bücher mit der folgenden Aktion verwendet. Sie kontrollieren nicht die Fähigkeit, Bücher zu erstellen.',\n    'shelves_copy_permissions_to_books' => 'Kopiere die Berechtigungen zum Buch',\n    'shelves_copy_permissions' => 'Berechtigungen kopieren',\n    'shelves_copy_permissions_explain' => 'Dadurch werden die aktuellen Berechtigungseinstellungen dieses Regals auf alle darin enthaltenen Bücher angewendet. Vergewissere dich vor der Aktivierung, dass alle Änderungen an den Berechtigungen für dieses Regal gespeichert wurden.',\n    'shelves_copy_permission_success' => 'Regalberechtigungen auf :count Bücher kopiert',\n\n    // Books\n    'book' => 'Buch',\n    'books' => 'Bücher',\n    'x_books' => ':count Buch|:count Bücher',\n    'books_empty' => 'Es wurden noch keine Bücher angelegt',\n    'books_popular' => 'Beliebte Bücher',\n    'books_recent' => 'Kürzlich angesehene Bücher',\n    'books_new' => 'Neue Bücher',\n    'books_new_action' => 'Neues Buch',\n    'books_popular_empty' => 'Die beliebtesten Bücher werden hier angezeigt.',\n    'books_new_empty' => 'Die neusten Bücher werden hier angezeigt.',\n    'books_create' => 'Neues Buch erstellen',\n    'books_delete' => 'Buch löschen',\n    'books_delete_named' => 'Buch \":bookName\" löschen',\n    'books_delete_explain' => 'Das Buch \":bookName\" wird gelöscht und alle zugehörigen Kapitel und Seiten entfernt.',\n    'books_delete_confirmation' => 'Bist Du sicher, dass du dieses Buch löschen möchtest?',\n    'books_edit' => 'Buch bearbeiten',\n    'books_edit_named' => 'Buch \":bookName\" bearbeiten',\n    'books_form_book_name' => 'Name des Buches',\n    'books_save' => 'Buch speichern',\n    'books_permissions' => 'Buch-Berechtigungen',\n    'books_permissions_updated' => 'Buch-Berechtigungen aktualisiert',\n    'books_empty_contents' => 'Es sind noch keine Seiten oder Kapitel zu diesem Buch hinzugefügt worden.',\n    'books_empty_create_page' => 'Neue Seite anlegen',\n    'books_empty_sort_current_book' => 'Aktuelles Buch sortieren',\n    'books_empty_add_chapter' => 'Neues Kapitel hinzufügen',\n    'books_permissions_active' => 'Buch-Berechtigungen aktiv',\n    'books_search_this' => 'Dieses Buch durchsuchen',\n    'books_navigation' => 'Buchnavigation',\n    'books_sort' => 'Buchinhalte sortieren',\n    'books_sort_desc' => 'Kapitel und Seiten innerhalb eines Buches verschieben, um dessen Inhalt zu reorganisieren. Andere Bücher können hinzugefügt werden, was das Verschieben von Kapiteln und Seiten zwischen Büchern erleichtert. Optional kann eine automatische Sortierregel erstellt werden, um den Inhalt dieses Buches nach Änderungen automatisch zu sortieren.',\n    'books_sort_auto_sort' => 'Auto-Sortieroption',\n    'books_sort_auto_sort_active' => 'Automatische Sortierung aktiv: :sortName',\n    'books_sort_named' => 'Buch \":bookName\" sortieren',\n    'books_sort_name' => 'Sortieren nach Namen',\n    'books_sort_created' => 'Sortieren nach Erstellungsdatum',\n    'books_sort_updated' => 'Sortieren nach Aktualisierungsdatum',\n    'books_sort_chapters_first' => 'Kapitel zuerst',\n    'books_sort_chapters_last' => 'Kapitel zuletzt',\n    'books_sort_show_other' => 'Andere Bücher anzeigen',\n    'books_sort_save' => 'Neue Reihenfolge speichern',\n    'books_sort_show_other_desc' => 'Füge hier weitere Bücher hinzu, um sie in die Sortierung einzubinden und ermögliche so eine einfache und übergreifende Neuordnung.',\n    'books_sort_move_up' => 'Nach oben bewegen',\n    'books_sort_move_down' => 'Nach unten bewegen',\n    'books_sort_move_prev_book' => 'Zum vorherigen Buch verschieben',\n    'books_sort_move_next_book' => 'Zum nächsten Buch verschieben',\n    'books_sort_move_prev_chapter' => 'Ins vorherige Kapitel verschieben',\n    'books_sort_move_next_chapter' => 'Ins nächste Kapitel verschieben',\n    'books_sort_move_book_start' => 'Zum Buchbeginn verschieben',\n    'books_sort_move_book_end' => 'Zum Buchende verschieben',\n    'books_sort_move_before_chapter' => 'Vor Kapitel verschieben',\n    'books_sort_move_after_chapter' => 'Nach Kapitel verschieben',\n    'books_copy' => 'Buch kopieren',\n    'books_copy_success' => 'Buch erfolgreich kopiert',\n\n    // Chapters\n    'chapter' => 'Kapitel',\n    'chapters' => 'Kapitel',\n    'x_chapters' => ':count Kapitel',\n    'chapters_popular' => 'Beliebte Kapitel',\n    'chapters_new' => 'Neues Kapitel',\n    'chapters_create' => 'Neues Kapitel anlegen',\n    'chapters_delete' => 'Kapitel entfernen',\n    'chapters_delete_named' => 'Kapitel \":chapterName\" entfernen',\n    'chapters_delete_explain' => 'Hiermit löscht du das Kapitel mit dem Namen \\':chapterName\\'. Alle Seiten, die innerhalb dieses Kapitels existieren, werden ebenfalls gelöscht.',\n    'chapters_delete_confirm' => 'Bist du sicher, dass du dieses Kapitel löschen möchtest?',\n    'chapters_edit' => 'Kapitel bearbeiten',\n    'chapters_edit_named' => 'Kapitel \":chapterName\" bearbeiten',\n    'chapters_save' => 'Kapitel speichern',\n    'chapters_move' => 'Kapitel verschieben',\n    'chapters_move_named' => 'Kapitel \":chapterName\" verschieben',\n    'chapters_copy' => 'Kapitel kopieren',\n    'chapters_copy_success' => 'Kapitel erfolgreich kopiert',\n    'chapters_permissions' => 'Kapitel-Berechtigungen',\n    'chapters_empty' => 'Aktuell sind keine Kapitel diesem Buch hinzugefügt worden.',\n    'chapters_permissions_active' => 'Kapitel-Berechtigungen aktiv',\n    'chapters_permissions_success' => 'Kapitel-Berechtigungenen aktualisisert',\n    'chapters_search_this' => 'Dieses Kapitel durchsuchen',\n    'chapter_sort_book' => 'Buch sortieren',\n\n    // Pages\n    'page' => 'Seite',\n    'pages' => 'Seiten',\n    'x_pages' => ':count Seite|:count Seiten',\n    'pages_popular' => 'Beliebte Seiten',\n    'pages_new' => 'Neue Seite',\n    'pages_attachments' => 'Anhänge',\n    'pages_navigation' => 'Seitennavigation',\n    'pages_delete' => 'Seite löschen',\n    'pages_delete_named' => 'Seite \":pageName\" löschen',\n    'pages_delete_draft_named' => 'Seitenentwurf von \":pageName\" löschen',\n    'pages_delete_draft' => 'Seitenentwurf löschen',\n    'pages_delete_success' => 'Seite gelöscht',\n    'pages_delete_draft_success' => 'Seitenentwurf gelöscht',\n    'pages_delete_warning_template' => 'Diese Seite wird aktiv als Standardvorlage für Bücher oder Kapitel verwendet. In diesen Büchern oder Kapiteln wird nach dem Löschen dieser Seite keine Standardvorlage mehr zugewiesen sein.',\n    'pages_delete_confirm' => 'Bist du sicher, dass du diese Seite löschen möchtest?',\n    'pages_delete_draft_confirm' => 'Bist du sicher, dass du diesen Seitenentwurf löschen möchtest?',\n    'pages_editing_named' => 'Seite \":pageName\" bearbeiten',\n    'pages_edit_draft_options' => 'Entwurfsoptionen',\n    'pages_edit_save_draft' => 'Entwurf speichern',\n    'pages_edit_draft' => 'Seitenentwurf bearbeiten',\n    'pages_editing_draft' => 'Seitenentwurf bearbeiten',\n    'pages_editing_page' => 'Seite bearbeiten',\n    'pages_edit_draft_save_at' => 'Entwurf gesichert um ',\n    'pages_edit_delete_draft' => 'Entwurf löschen',\n    'pages_edit_delete_draft_confirm' => 'Bist du sicher, dass du deinen Entwurf löschen möchtest? Alle deine Änderungen seit dem letzten vollständigen Speichern gehen verloren und der Editor wird mit dem letzten Speicherzustand aktualisiert, der kein Entwurf ist.',\n    'pages_edit_discard_draft' => 'Entwurf verwerfen',\n    'pages_edit_switch_to_markdown' => 'Zum Markdown-Editor wechseln',\n    'pages_edit_switch_to_markdown_clean' => '(Gesäuberter Inhalt)',\n    'pages_edit_switch_to_markdown_stable' => '(Stabiler Inhalt)',\n    'pages_edit_switch_to_wysiwyg' => 'Zum WYSIWYG-Editor wechseln',\n    'pages_edit_switch_to_new_wysiwyg' => 'Zum neuen WYSIWYG wechseln',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(Im Beta-Test)',\n    'pages_edit_set_changelog' => 'Änderungsprotokoll hinzufügen',\n    'pages_edit_enter_changelog_desc' => 'Bitte gib eine kurze Zusammenfassung deiner Änderungen ein',\n    'pages_edit_enter_changelog' => 'Änderungsprotokoll eingeben',\n    'pages_editor_switch_title' => 'Editor wechseln',\n    'pages_editor_switch_are_you_sure' => 'Bist du dir sicher, dass du den Editor für diese Seite ändern willst?',\n    'pages_editor_switch_consider_following' => 'Beachte beim Wechsel des Editors folgendes:',\n    'pages_editor_switch_consideration_a' => 'Nach dem Speichern wird der neue Editor von allen zukünftigen Bearbeitern verwendet, auch von denen, die den Editortyp nicht selbst ändern können.',\n    'pages_editor_switch_consideration_b' => 'Dies kann unter Umständen zu einem Verlust von Details und Syntax führen.',\n    'pages_editor_switch_consideration_c' => 'Tag- oder Changelog-Änderungen, die seit dem letzten Speichern vorgenommen wurden, bleiben bei dieser Änderung nicht erhalten.',\n    'pages_save' => 'Seite speichern',\n    'pages_title' => 'Seitentitel',\n    'pages_name' => 'Seitenname',\n    'pages_md_editor' => 'Redakteur',\n    'pages_md_preview' => 'Vorschau',\n    'pages_md_insert_image' => 'Bild einfügen',\n    'pages_md_insert_link' => 'Link zu einem Objekt einfügen',\n    'pages_md_insert_drawing' => 'Zeichnung einfügen',\n    'pages_md_show_preview' => 'Vorschau anzeigen',\n    'pages_md_sync_scroll' => 'Vorschau synchronisieren',\n    'pages_md_plain_editor' => 'Einfacher Editor',\n    'pages_drawing_unsaved' => 'Ungespeicherte Zeichnung gefunden',\n    'pages_drawing_unsaved_confirm' => 'Es wurden ungespeicherte Zeichnungsdaten von einem früheren, fehlgeschlagenen Versuch, die Zeichnung zu speichern, gefunden. Möchtest du diese ungespeicherte Zeichnung wiederherstellen und weiter bearbeiten?',\n    'pages_not_in_chapter' => 'Seite ist in keinem Kapitel',\n    'pages_move' => 'Seite verschieben',\n    'pages_copy' => 'Seite kopieren',\n    'pages_copy_desination' => 'Ziel',\n    'pages_copy_success' => 'Seite erfolgreich kopiert',\n    'pages_permissions' => 'Seiten Berechtigungen',\n    'pages_permissions_success' => 'Seiten Berechtigungen aktualisiert',\n    'pages_revision' => 'Version',\n    'pages_revisions' => 'Seitenversionen',\n    'pages_revisions_desc' => 'Alle vorherhigen Revisionen dieser Seite sind unten aufgelistet. Du kannst zurückschauen, vergleichen und alte Seitenversionen wiederherstellen, wenn die Berechtigungen dies erlauben. Der vollständige Verlauf der Seite kann hier möglicherweise nicht vollständig wiedergegeben werden, da je nach Systemkonfiguration alte Revisionen automatisch gelöscht werden könnten.',\n    'pages_revisions_named' => 'Seitenversionen von \":pageName\"',\n    'pages_revision_named' => 'Seitenversion von \":pageName\"',\n    'pages_revision_restored_from' => 'Wiederhergestellt von #:id; :summary',\n    'pages_revisions_created_by' => 'Erstellt von',\n    'pages_revisions_date' => 'Versionsdatum',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Revisionsnummer',\n    'pages_revisions_numbered' => 'Revision #:id',\n    'pages_revisions_numbered_changes' => 'Revision #:id Änderungen',\n    'pages_revisions_editor' => 'Editortyp',\n    'pages_revisions_changelog' => 'Änderungsprotokoll',\n    'pages_revisions_changes' => 'Änderungen',\n    'pages_revisions_current' => 'Aktuelle Version',\n    'pages_revisions_preview' => 'Vorschau',\n    'pages_revisions_restore' => 'Wiederherstellen',\n    'pages_revisions_none' => 'Diese Seite hat keine älteren Versionen.',\n    'pages_copy_link' => 'Link kopieren',\n    'pages_edit_content_link' => 'Im Editor zum Abschnitt springen',\n    'pages_pointer_enter_mode' => 'Abschnittsauswahlmodus aktivieren',\n    'pages_pointer_label' => 'Abschnittsoptionen der Seite',\n    'pages_pointer_permalink' => 'Seitenabschnitt-Permalink',\n    'pages_pointer_include_tag' => 'Seitenabschnitts-Include-Tag',\n    'pages_pointer_toggle_link' => 'Permalink-Modus, Drücken, um Include-Tag anzuzeigen',\n    'pages_pointer_toggle_include' => 'Include-Tag-Modus, Drücken, um Permalink anzuzeigen',\n    'pages_permissions_active' => 'Seiten-Berechtigungen aktiv',\n    'pages_initial_revision' => 'Erste Veröffentlichung',\n    'pages_references_update_revision' => 'Automatische Systemaktualisierung interner Links',\n    'pages_initial_name' => 'Neue Seite',\n    'pages_editing_draft_notification' => 'Du bearbeitest momenten einen Entwurf, der zuletzt :timeDiff gespeichert wurde.',\n    'pages_draft_edited_notification' => 'Diese Seite wurde seit diesem Zeitpunkt verändert. Wir empfehlen diesen Entwurf zu verwerfen.',\n    'pages_draft_page_changed_since_creation' => 'Diese Seite wurde seit der Erstellung dieses Entwurfs aktualisiert. Es wird empfohlen, diesen Entwurf zu verwerfen oder darauf zu achten, dass keine Seitenänderungen überschrieben werden.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count Benutzer bearbeiten derzeit diese Seite.',\n        'start_b' => ':userName bearbeitet jetzt diese Seite.',\n        'time_a' => 'seit die Seiten zuletzt aktualisiert wurden.',\n        'time_b' => 'in den letzten :minCount Minuten',\n        'message' => ':start :time. Achte darauf, keine Änderungen von anderen Benutzern zu überschreiben!',\n    ],\n    'pages_draft_discarded' => 'Entwurf verworfen! Der aktuelle Seiteninhalt wurde geladen',\n    'pages_draft_deleted' => 'Entwurf gelöscht. Der aktuelle Seiteninhalt wurde geladen',\n    'pages_specific' => 'Spezifische Seite',\n    'pages_is_template' => 'Seitenvorlage',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Seitenleiste umschalten',\n    'page_tags' => 'Seiten-Schlagwörter',\n    'chapter_tags' => 'Kapitel-Schlagwörter',\n    'book_tags' => 'Buch-Schlagwörter',\n    'shelf_tags' => 'Regal-Schlagwörter',\n    'tag' => 'Schlagwort',\n    'tags' =>  'Schlagwörter',\n    'tags_index_desc' => 'Tags können auf Inhalte im System angewendet werden, um eine flexible Form der Kategorisierung anzuwenden. Tags können sowohl einen Schlüssel als auch einen Wert haben, wobei der Wert optional ist. Einmal angewendet, können Inhalte unter Verwendung des Tag-Namens und Wertes abgefragt werden.',\n    'tag_name' =>  'Schlagwortname',\n    'tag_value' => 'Inhalt (Optional)',\n    'tags_explain' => \"Füge Schlagwörter hinzu, um ihren Inhalt zu kategorisieren.\\nDu kannst einen erklärenden Inhalt hinzufügen, um eine genauere Unterteilung vorzunehmen.\",\n    'tags_add' => 'Weiteres Schlagwort hinzufügen',\n    'tags_remove' => 'Diesen Tag entfernen',\n    'tags_usages' => 'Gesamte Tagnutzung',\n    'tags_assigned_pages' => 'Seiten zugewiesen',\n    'tags_assigned_chapters' => 'Kapiteln zugewiesen',\n    'tags_assigned_books' => 'Büchern zugewiesen',\n    'tags_assigned_shelves' => 'Regalen zugewiesen',\n    'tags_x_unique_values' => ':count eindeutige Werte',\n    'tags_all_values' => 'Alle Werte',\n    'tags_view_tags' => 'Tags anzeigen',\n    'tags_view_existing_tags' => 'Vorhandene Tags anzeigen',\n    'tags_list_empty_hint' => 'Tags können über die Seitenleiste des Seiteneditors oder bei der Bearbeitung der Details eines Buches, Kapitels oder Regals vergeben werden.',\n    'attachments' => 'Anhänge',\n    'attachments_explain' => 'Du kannst auf deiner Seite Dateien hochladen oder Links hinzufügen. Diese werden in der Seitenleiste angezeigt.',\n    'attachments_explain_instant_save' => 'Änderungen werden direkt gespeichert.',\n    'attachments_upload' => 'Datei hochladen',\n    'attachments_link' => 'Link hinzufügen',\n    'attachments_upload_drop' => 'Alternativ kannst du eine Datei per Drag & Drop hier als Anhang hochladen.',\n    'attachments_set_link' => 'Link setzen',\n    'attachments_delete' => 'Bist du sicher, dass du diesen Anhang löschen möchtest?',\n    'attachments_dropzone' => 'Ziehe Dateien hierher, um sie hochzuladen',\n    'attachments_no_files' => 'Es wurden bisher keine Dateien hochgeladen.',\n    'attachments_explain_link' => 'Wenn du keine Datei hochladen möchtest, kannst du stattdessen einen Link hinzufügen. Dieser Link kann auf eine andere Seite oder eine Datei im Internet verweisen.',\n    'attachments_link_name' => 'Link-Name',\n    'attachment_link' => 'Link zum Anhang',\n    'attachments_link_url' => 'Link zu einer Datei',\n    'attachments_link_url_hint' => 'URL einer Seite oder Datei',\n    'attach' => 'Hinzufügen',\n    'attachments_insert_link' => 'Anhangslink zur Seite hinzufügen',\n    'attachments_edit_file' => 'Datei bearbeiten',\n    'attachments_edit_file_name' => 'Dateiname',\n    'attachments_edit_drop_upload' => 'Ziehe Dateien hierher, um diese hochzuladen und zu überschreiben',\n    'attachments_order_updated' => 'Reihenfolge der Anhänge aktualisiert',\n    'attachments_updated_success' => 'Anhangdetails aktualisiert',\n    'attachments_deleted' => 'Anhang gelöscht',\n    'attachments_file_uploaded' => 'Datei erfolgreich hochgeladen',\n    'attachments_file_updated' => 'Datei erfolgreich aktualisiert',\n    'attachments_link_attached' => 'Link erfolgreich der Seite hinzugefügt',\n    'templates' => 'Vorlagen',\n    'templates_set_as_template' => 'Seite ist eine Vorlage',\n    'templates_explain_set_as_template' => 'Du kannst diese Seite als Vorlage festlegen, damit deren Inhalt beim Erstellen anderer Seiten verwendet werden kann. Andere Benutzer können diese Vorlage verwenden, wenn diese die Zugriffsrechte für diese Seite haben.',\n    'templates_replace_content' => 'Seiteninhalt ersetzen',\n    'templates_append_content' => 'An Seiteninhalt anhängen',\n    'templates_prepend_content' => 'Seiteninhalt voranstellen',\n\n    // Profile View\n    'profile_user_for_x' => 'Benutzer seit :time',\n    'profile_created_content' => 'Erstellte Inhalte',\n    'profile_not_created_pages' => ':userName hat noch keine Seiten erstellt.',\n    'profile_not_created_chapters' => ':userName hat noch keine Kapitel erstellt.',\n    'profile_not_created_books' => ':userName hat noch keine Bücher erstellt.',\n    'profile_not_created_shelves' => ':userName hat noch keine Regale erstellt.',\n\n    // Comments\n    'comment' => 'Kommentar',\n    'comments' => 'Kommentare',\n    'comment_add' => 'Kommentieren',\n    'comment_none' => 'Keine Kommentare vorhanden',\n    'comment_placeholder' => 'Gib hier deine Kommentare ein',\n    'comment_thread_count' => ':count Thema|:count Themen',\n    'comment_archived_count' => ':count archiviert',\n    'comment_archived_threads' => 'Archivierte Themen',\n    'comment_save' => 'Kommentar speichern',\n    'comment_new' => 'Neuer Kommentar',\n    'comment_created' => ':createDiff kommentiert',\n    'comment_updated' => ':updateDiff aktualisiert von :username',\n    'comment_updated_indicator' => 'Aktualisiert',\n    'comment_deleted_success' => 'Kommentar gelöscht',\n    'comment_created_success' => 'Kommentar hinzugefügt',\n    'comment_updated_success' => 'Kommentar aktualisiert',\n    'comment_archive_success' => 'Kommentar archiviert',\n    'comment_unarchive_success' => 'Kommentar nicht mehr archiviert',\n    'comment_view' => 'Kommentar ansehen',\n    'comment_jump_to_thread' => 'Zum Thema springen',\n    'comment_delete_confirm' => 'Möchtst du diesen Kommentar wirklich löschen?',\n    'comment_in_reply_to' => 'Antwort auf :commentId',\n    'comment_reference' => 'Referenz',\n    'comment_reference_outdated' => '(Veraltet)',\n    'comment_editor_explain' => 'Hier sind die Kommentare, die auf dieser Seite hinterlassen wurden. Kommentare können hinzugefügt und verwaltet werden, wenn die gespeicherte Seite angezeigt wird.',\n\n    // Revision\n    'revision_delete_confirm' => 'Bist du sicher, dass du diese Revision löschen möchtest?',\n    'revision_restore_confirm' => 'Bist du sicher, dass du diese Revision wiederherstellen willst? Der aktuelle Seiteninhalt wird ersetzt.',\n    'revision_cannot_delete_latest' => 'Die letzte Version kann nicht gelöscht werden.',\n\n    // Copy view\n    'copy_consider' => 'Bitte beachte beim Kopieren von Inhalten die folgenden Punkte.',\n    'copy_consider_permissions' => 'Benutzerdefinierte Berechtigungseinstellungen werden nicht kopiert.',\n    'copy_consider_owner' => 'Du wirst Besitzer der gesamten kopierten Inhalte.',\n    'copy_consider_images' => 'Bilder auf der Seite werden nicht dupliziert und die originalen Bilder werden die Beziehung zur ursprünglichen Seite, auf der sie hochgeladen wurden, behalten.',\n    'copy_consider_attachments' => 'Seitenanhänge werden nicht kopiert.',\n    'copy_consider_access' => 'Ein Wechsel des Standorts, des Eigentümers oder der Berechtigungen kann dazu führen, dass diese Inhalte für diejenigen zugänglich sind, die zuvor keinen Zugang hatten.',\n\n    // Conversions\n    'convert_to_shelf' => 'In Regal umwandeln',\n    'convert_to_shelf_contents_desc' => 'Du kannst dieses Buch in ein neues Regal mit demselben Inhalt umwandeln. Die in diesem Buch enthaltenen Kapitel werden in neue Bücher umgewandelt. Wenn dieses Buch Seiten enthält, die nicht in einem Kapitel enthalten sind, wird das Buch umbenannt und enthält diese Seiten, und das Buch wird Teil des neuen Regals.',\n    'convert_to_shelf_permissions_desc' => 'Alle Berechtigungen, die für dieses Buch festgelegt wurden, werden in das neue Regal und in alle neuen untergeordneten Bücher kopiert, für die keine eigenen Berechtigungen festgelegt wurden. Beachte, dass Berechtigungen für Regale nicht automatisch auf den Inhalt übertragen werden, wie es bei Büchern der Fall ist.',\n    'convert_book' => 'Buch umwandeln',\n    'convert_book_confirm' => 'Bist du dir sicher, dass du dieses Buch umwandelt möchtest?',\n    'convert_undo_warning' => 'Das kann nicht so einfach rückgängig gemacht werden.',\n    'convert_to_book' => 'In Buch umwandeln',\n    'convert_to_book_desc' => 'Du kannst dieses Kapitel in ein neues Buch mit demselben Inhalt umwandeln. Alle Berechtigungen, die für dieses Kapitel festgelegt wurden, werden in das neue Buch kopiert, aber alle geerbten Berechtigungen aus dem übergeordneten Buch werden nicht kopiert, was zu einer Änderung der Zugriffskontrolle führen könnte.',\n    'convert_chapter' => 'Kapitel konvertieren',\n    'convert_chapter_confirm' => 'Bist du dir sicher, dass du dieses Kapitel konvertieren möchtest?',\n\n    // References\n    'references' => 'Verweise',\n    'references_none' => 'Es gibt keine nachverfolgten Referenzen zu diesem Element.',\n    'references_to_desc' => 'Unten sind alle bekannten Inhalte im System aufgelistet, die auf diesen Eintrag verweisen.',\n\n    // Watch Options\n    'watch' => 'Beobachten',\n    'watch_title_default' => 'Standardeinstellungen',\n    'watch_desc_default' => 'Rückgängig machen auf Standard-Benachrichtigungseinstellungen.',\n    'watch_title_ignore' => 'Ignorieren',\n    'watch_desc_ignore' => 'Ignorieren aller Benachrichtigungen, auch die von den Einstellungen auf Benutzerebene.',\n    'watch_title_new' => 'Neue Seiten',\n    'watch_desc_new' => 'Benachrichtigen, wenn eine neue Seite in diesem Element erstellt wird.',\n    'watch_title_updates' => 'Alle Seitenupdates',\n    'watch_desc_updates' => 'Bei allen neuen Seiten und Seitenänderungen benachrichtigen.',\n    'watch_desc_updates_page' => 'Bei allen Seitenänderungen benachrichtigen.',\n    'watch_title_comments' => 'Alle Seitenupdates & Kommentare',\n    'watch_desc_comments' => 'Benachrichtigung bei allen neuen Seiten, Seitenänderungen und neuen Kommentaren.',\n    'watch_desc_comments_page' => 'Benachrichtigung bei Seitenänderungen und neuen Kommentaren.',\n    'watch_change_default' => 'Standard-Benachrichtigungseinstellungen ändern',\n    'watch_detail_ignore' => 'Benachrichtigungen ignorieren',\n    'watch_detail_new' => 'Beobachten von neuen Seiten',\n    'watch_detail_updates' => 'Beobachtung neuer Seiten und Aktualisierungen',\n    'watch_detail_comments' => 'Beobachtung neuer Seiten, Aktualisierungen und Kommentare',\n    'watch_detail_parent_book' => 'Beobachten über übergeordnetes Buch',\n    'watch_detail_parent_book_ignore' => 'Ignorieren über übergeordnetes Buch',\n    'watch_detail_parent_chapter' => 'Beobachten über übergeordnetes Kapitel',\n    'watch_detail_parent_chapter_ignore' => 'Ignorieren über übergeordnetes Kapitel',\n];\n"
  },
  {
    "path": "lang/de_informal/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Du hast keine Berechtigung, auf diese Seite zuzugreifen.',\n    'permissionJson' => 'Du hast keine Berechtigung, die angeforderte Aktion auszuführen.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Ein Benutzer mit der E-Mail-Adresse :email ist bereits mit anderen Anmeldedaten registriert.',\n    'auth_pre_register_theme_prevention' => 'Das Benutzerkonto kann mit den bereitgestellten Informationen nicht erstellen werden',\n    'email_already_confirmed' => 'Die E-Mail-Adresse ist bereits bestätigt. Bitte melde dich an.',\n    'email_confirmation_invalid' => 'Der Bestätigungslink ist nicht gültig oder wurde bereits verwendet. Bitte registriere dich erneut.',\n    'email_confirmation_expired' => 'Der Bestätigungslink ist abgelaufen. Es wurde eine neue Bestätigungs-E-Mail gesendet.',\n    'email_confirmation_awaiting' => 'Die E-Mail-Adresse für das verwendete Konto muss bestätigt werden',\n    'ldap_fail_anonymous' => 'Anonymer LDAP-Zugriff ist fehlgeschlagen',\n    'ldap_fail_authed' => 'LDAP-Zugriff mit DN und Passwort ist fehlgeschlagen',\n    'ldap_extension_not_installed' => 'LDAP-PHP-Erweiterung ist nicht installiert',\n    'ldap_cannot_connect' => 'Die Verbindung zum LDAP-Server ist fehlgeschlagen. Beim initialen Verbindungsaufbau trat ein Fehler auf',\n    'saml_already_logged_in' => 'Du bist bereits angemeldet',\n    'saml_no_email_address' => 'Es konnte keine E-Mail-Adresse für diesen Benutzer in den vom externen Authentifizierungssystem zur Verfügung gestellten Daten gefunden werden',\n    'saml_invalid_response_id' => 'Die Anfrage vom externen Authentifizierungssystem wird von einem von dieser Anwendung gestarteten Prozess nicht erkannt. Das Zurückblättern nach einem Login könnte dieses Problem verursachen.',\n    'saml_fail_authed' => 'Anmeldung mit :system fehlgeschlagen, System konnte keine erfolgreiche Autorisierung bereitstellen',\n    'oidc_already_logged_in' => 'Bereits angemeldet',\n    'oidc_no_email_address' => 'Es konnte keine E-Mail-Adresse für diesen Benutzer in den vom externen Authentifizierungssystem bereitgestellten Daten gefunden werden',\n    'oidc_fail_authed' => 'Anmeldung mit :system fehlgeschlagen, System hat keine erfolgreiche Autorisierung geliefert',\n    'social_no_action_defined' => 'Es ist keine Aktion definiert',\n    'social_login_bad_response' => \"Fehler bei :socialAccount Login: \\n:error\",\n    'social_account_in_use' => 'Dieses :socialAccount-Konto wird bereits verwendet. Bitte melde dich mit dem :socialAccount-Konto an.',\n    'social_account_email_in_use' => 'Die E-Mail-Adresse \":email\" ist bereits registriert. Wenn du bereits registriert bist, kannst du Dein :socialAccount-Konto in Deinen Profil-Einstellungen verknüpfen.',\n    'social_account_existing' => 'Dieses :socialAccount-Konto ist bereits mit deinem Profil verknüpft.',\n    'social_account_already_used_existing' => 'Dieses :socialAccount-Konto wird bereits von einem anderen Benutzer verwendet.',\n    'social_account_not_used' => 'Dieses :socialAccount-Konto ist bisher keinem Benutzer zugeordnet. Du kannst das in deinen Profil-Einstellungen tun.',\n    'social_account_register_instructions' => 'Wenn du bisher kein Social-Media Konto besitzt, kannst du ein solches Konto mit der :socialAccount Option anlegen.',\n    'social_driver_not_found' => 'Treiber für Social-Media-Konten nicht gefunden',\n    'social_driver_not_configured' => 'Dein :socialAccount-Konto ist nicht korrekt konfiguriert.',\n    'invite_token_expired' => 'Dieser Einladungslink ist abgelaufen. Du kannst stattdessen versuchen, dein Passwort zurückzusetzen.',\n    'login_user_not_found' => 'Ein Benutzer für diese Aktion konnte nicht gefunden werden.',\n\n    // System\n    'path_not_writable' => 'Die Datei kann nicht in den angegebenen Pfad :filePath hochgeladen werden. Stelle sicher, dass dieser Ordner auf dem Server beschreibbar ist.',\n    'cannot_get_image_from_url' => 'Bild konnte nicht von der URL :url geladen werden.',\n    'cannot_create_thumbs' => 'Der Server kann keine Vorschau-Bilder erzeugen. Bitte prüfe, ob die GD PHP-Erweiterung installiert ist.',\n    'server_upload_limit' => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigröße. Bitte versuche es mit einer kleineren Datei.',\n    'server_post_limit' => 'Der Server kann die angegebene Datenmenge nicht empfangen. Versuche es erneut mit weniger Daten oder einer kleineren Datei.',\n    'uploaded'  => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigröße. Bitte versuche es mit einer kleineren Datei.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Beim Hochladen des Bildes trat ein Fehler auf.',\n    'image_upload_type_error' => 'Der Bildtyp der hochgeladenen Datei ist ungültig.',\n    'image_upload_replace_type' => 'Bild-Ersetzungen müssen vom gleichen Typ sein',\n    'image_upload_memory_limit' => 'Bildupload und/oder Thumbnailerstellung konnten aufgrund von Systemressourcenbeschränkungen nicht verarbeitet werden.',\n    'image_thumbnail_memory_limit' => 'Fehler beim Erstellen der Thumbnails aufgrund von Systemressourcenbeschränkungen.',\n    'image_gallery_thumbnail_memory_limit' => 'Fehler beim Erstellen der Galerie Thumbnails aufgrund von Systemressourcenbeschränkungen.',\n    'drawing_data_not_found' => 'Zeichnungsdaten konnten nicht geladen werden. Die Zeichnungsdatei existiert möglicherweise nicht mehr oder du hast nicht die Berechtigung, darauf zuzugreifen.',\n\n    // Attachments\n    'attachment_not_found' => 'Anhang konnte nicht gefunden werden.',\n    'attachment_upload_error' => 'Beim Hochladen des Anhangs trat ein Fehler auf',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Fehler beim Speichern des Entwurfs. Stelle sicher, dass du mit dem Internet verbunden bist, bevor du den Entwurf dieser Seite speicherst.',\n    'page_draft_delete_fail' => 'Fehler beim Löschen des Seitenentwurfs und beim Abrufen des gespeicherten Inhalts der aktuellen Seite',\n    'page_custom_home_deletion' => 'Eine als Startseite gesetzte Seite kann nicht gelöscht werden.',\n\n    // Entities\n    'entity_not_found' => 'Eintrag nicht gefunden',\n    'bookshelf_not_found' => 'Regal nicht gefunden',\n    'book_not_found' => 'Buch nicht gefunden',\n    'page_not_found' => 'Seite nicht gefunden',\n    'chapter_not_found' => 'Kapitel nicht gefunden',\n    'selected_book_not_found' => 'Das gewählte Buch wurde nicht gefunden.',\n    'selected_book_chapter_not_found' => 'Das gewählte Buch oder Kapitel wurde nicht gefunden.',\n    'guests_cannot_save_drafts' => 'Gäste können keine Entwürfe speichern',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Du kannst den einzigen Administrator nicht löschen.',\n    'users_cannot_delete_guest' => 'Du kannst den Gast-Benutzer nicht löschen',\n    'users_could_not_send_invite' => 'Benutzer konnte nicht erstellt werden, da die Einladungs-E-Mail nicht gesendet werden konnte',\n\n    // Roles\n    'role_cannot_be_edited' => 'Diese Rolle kann nicht bearbeitet werden.',\n    'role_system_cannot_be_deleted' => 'Dies ist eine Systemrolle und kann nicht gelöscht werden',\n    'role_registration_default_cannot_delete' => 'Diese Rolle kann nicht gelöscht werden, solange sie als Standardrolle für neue Registrierungen gesetzt ist',\n    'role_cannot_remove_only_admin' => 'Dieser Benutzer ist der einzige Benutzer, welchem die Administratorrolle zugeordnet ist. Ordne die Administratorrolle einem anderen Benutzer zu, bevor du versuchst, sie hier zu entfernen.',\n\n    // Comments\n    'comment_list' => 'Beim Abrufen der Kommentare ist ein Fehler aufgetreten.',\n    'cannot_add_comment_to_draft' => 'Du kannst keine Kommentare zu einem Entwurf hinzufügen.',\n    'comment_add' => 'Beim Hinzufügen des Kommentars ist ein Fehler aufgetreten.',\n    'comment_delete' => 'Beim Löschen des Kommentars ist ein Fehler aufgetreten.',\n    'empty_comment' => 'Kann keinen leeren Kommentar hinzufügen',\n\n    // Error pages\n    '404_page_not_found' => 'Seite nicht gefunden',\n    'sorry_page_not_found' => 'Entschuldigung. Die Seite, die du angefordert hast, wurde nicht gefunden.',\n    'sorry_page_not_found_permission_warning' => 'Wenn du erwartet hast, dass diese Seite existiert, hast du möglicherweise nicht die Berechtigung, sie anzuzeigen.',\n    'image_not_found' => 'Bild nicht gefunden',\n    'image_not_found_subtitle' => 'Sorry. Die Bilddatei, nach der du suchst, konnte nicht gefunden werden.',\n    'image_not_found_details' => 'Wenn du erwartet hast, dass dieses Bild existiert, wurde es möglicherweise gelöscht.',\n    'return_home' => 'Zurück zur Startseite',\n    'error_occurred' => 'Es ist ein Fehler aufgetreten',\n    'app_down' => ':appName befindet sich aktuell im Wartungsmodus.',\n    'back_soon' => 'Wir werden so schnell wie möglich wieder online sein.',\n\n    // Import\n    'import_zip_cant_read' => 'ZIP-Datei konnte nicht gelesen werden.',\n    'import_zip_cant_decode_data' => 'Konnte Inhalt der data.json im ZIP nicht finden und dekodieren.',\n    'import_zip_no_data' => 'ZIP-Datei hat kein erwartetes Buch, Kapitel oder Seiteninhalt.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'ZIP Import konnte aufgrund folgender Fehler nicht validiert werden:',\n    'import_zip_failed_notification' => 'Importieren der ZIP-Datei fehlgeschlagen.',\n    'import_perms_books' => 'Dir fehlt die erforderliche Berechtigung, um Bücher zu erstellen.',\n    'import_perms_chapters' => 'Dir fehlt die erforderliche Berechtigung, um Kapitel zu erstellen.',\n    'import_perms_pages' => 'Dir fehlt die erforderliche Berechtigung, um Seiten zu erstellen.',\n    'import_perms_images' => 'Dir fehlt die erforderliche Berechtigung, um Bilder zu erstellen.',\n    'import_perms_attachments' => 'Dir fehlt die erforderliche Berechtigung, um Anhänge zu erstellen.',\n\n    // API errors\n    'api_no_authorization_found' => 'Kein Autorisierungs-Token für die Anfrage gefunden',\n    'api_bad_authorization_format' => 'Ein Autorisierungs-Token wurde auf die Anfrage gefunden, aber das Format schien falsch zu sein',\n    'api_user_token_not_found' => 'Es wurde kein passender API-Token für den angegebenen Autorisierungs-Token gefunden',\n    'api_incorrect_token_secret' => 'Das für den API-Token angegebene geheime Token ist falsch',\n    'api_user_no_api_permission' => 'Der Besitzer des verwendeten API-Token hat keine Berechtigung für API-Aufrufe',\n    'api_user_token_expired' => 'Das verwendete Autorisierungs-Token ist abgelaufen',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Fehler beim Senden einer Test E-Mail:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'Die URL stimmt nicht mit den konfigurierten erlaubten SSR-Hosts überein',\n];\n"
  },
  {
    "path": "lang/de_informal/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Neuer Kommentar auf Seite: :pageName',\n    'new_comment_intro' => 'Ein Benutzer hat eine Seite in :appName kommentiert:',\n    'new_page_subject' => 'Neue Seite: :pageName',\n    'new_page_intro' => 'Es wurde eine neue Seite in :appName erstellt:',\n    'updated_page_subject' => 'Aktualisierte Seite: :pageName',\n    'updated_page_intro' => 'Eine Seite wurde in :appName aktualisiert:',\n    'updated_page_debounce' => 'Um eine Flut von Benachrichtigungen zu vermeiden, wirst du für eine gewisse Zeit keine Benachrichtigungen für weitere Bearbeitungen dieser Seite durch denselben Bearbeiter erhalten.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Seitenname:',\n    'detail_page_path' => 'Seitenpfad:',\n    'detail_commenter' => 'Kommentator:',\n    'detail_comment' => 'Kommentar:',\n    'detail_created_by' => 'Erstellt von:',\n    'detail_updated_by' => 'Aktualisiert von:',\n\n    'action_view_comment' => 'Kommentar anzeigen',\n    'action_view_page' => 'Seite anzeigen',\n\n    'footer_reason' => 'Diese Benachrichtigung wurde an dich gesendet, weil :link diese Art von Aktivität für dieses Element abdeckt.',\n    'footer_reason_link' => 'deine Benachrichtigungseinstellungen',\n];\n"
  },
  {
    "path": "lang/de_informal/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Vorherige',\n    'next'     => 'Nächste &raquo;',\n\n];\n"
  },
  {
    "path": "lang/de_informal/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Passwörter müssen aus mindestens acht Zeichen bestehen und mit der eingegebenen Wiederholung übereinstimmen.',\n    'user' => \"Es wurde kein Benutzer mit dieser E-Mail-Adresse gefunden.\",\n    'token' => 'Der Token zum Zurücksetzen des Passworts ist für diese E-Mail-Adresse ungültig.',\n    'sent' => 'Wir haben dir einen Link zum Zurücksetzen des Passwortes per E-Mail geschickt!',\n    'reset' => 'Dein Passwort wurde zurückgesetzt!',\n\n];\n"
  },
  {
    "path": "lang/de_informal/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Mein Account',\n\n    'shortcuts' => 'Kürzel',\n    'shortcuts_interface' => 'UI Shortcut Einstellungen',\n    'shortcuts_toggle_desc' => 'Hier kannst du Tastaturkürzel für die Systemoberfläche für Navigation und Aktionen aktivieren oder deaktivieren.',\n    'shortcuts_customize_desc' => 'Unten kannst du alle Tastenkürzel anpassen. Drücke einfach die gewünschte Tastenkombination, nachdem du die Eingabe für eine Tastenkombination ausgewählt hast.',\n    'shortcuts_toggle_label' => 'Tastaturkürzel aktiviert',\n    'shortcuts_section_navigation' => 'Navigation',\n    'shortcuts_section_actions' => 'Häufige Aktionen',\n    'shortcuts_save' => 'Tastenkürzel speichern',\n    'shortcuts_overlay_desc' => 'Hinweis: Wenn Tastenkürzel aktiviert sind, ist ein Hilfefähnchen durch Drücken von \"?\" verfügbar, welches die verfügbaren Tastenkürzel für Aktionen hervorhebt, die aktuell auf dem Bildschirm sichtbar sind.',\n    'shortcuts_update_success' => 'Tastenkürzel Einstellungen wurden aktualisiert!',\n    'shortcuts_overview_desc' => 'Verwalten von Tastenkombinationen, die zur Navigation der Benutzeroberfläche verwendet werden können.',\n\n    'notifications' => 'Benachrichtigungseinstellungen',\n    'notifications_desc' => 'Lege fest, welche E-Mail-Benachrichtigungen du erhältst, wenn bestimmte Aktivitäten im System durchgeführt werden.',\n    'notifications_opt_own_page_changes' => 'Benachrichtigung bei Änderungen an eigenen Seiten',\n    'notifications_opt_own_page_comments' => 'Benachrichtigung bei Kommentaren an eigenen Seiten',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Bei Antworten auf meine Kommentare benachrichtigen',\n    'notifications_save' => 'Einstellungen speichern',\n    'notifications_update_success' => 'Benachrichtigungseinstellungen wurden aktualisiert!',\n    'notifications_watched' => 'Beobachtete und ignorierte Elemente',\n    'notifications_watched_desc' => 'Nachfolgend finden Sie die Elemente, für die benutzerdefinierten Überwachungspräferenzen gelten. Um deine Einstellungen für diese Elemente zu aktualisieren, sieh dir das Element an und suche dann die Überwachungsoptionen in der Seitenleiste.',\n\n    'auth' => 'Zugang & Sicherheit',\n    'auth_change_password' => 'Passwort ändern',\n    'auth_change_password_desc' => 'Ändere das Passwort, mit dem du dich bei der Anwendung anmeldest. Dieses muss mindestens 8 Zeichen lang sein.',\n    'auth_change_password_success' => 'Das Passwort wurde aktualisiert!',\n\n    'profile' => 'Profildetails',\n    'profile_desc' => 'Verwalte die Details deines Kontos, welches dich gegenüber anderen Benutzern repräsentiert, zusätzlich zu den Details, die für die Kommunikation und die Personalisierung des Systems genutzt werden.',\n    'profile_view_public' => 'Öffentliches Profil anzeigen',\n    'profile_name_desc' => 'Konfiguriere deinen Anzeigenamen, der durch die Aktivität, die du ausführst, und die dir gehörenden Inhalte für andere Benutzer sichtbar ist.',\n    'profile_email_desc' => 'Diese E-Mail wird für Benachrichtigungen und, je nach aktiver Systemauthentifizierung, den Systemzugriff verwendet.',\n    'profile_email_no_permission' => 'Leider hast du nicht die Berechtigung, deine E-Mail-Adresse zu ändern. Wenn du diese ändern möchtest, wende dich bitte an deinen Administrator.',\n    'profile_avatar_desc' => 'Wähle ein Bild aus, das anderen im System angezeigt wird, um dich zu repräsentieren. Idealerweise sollte dieses Bild quadratisch und etwa 256px breit und hoch sein.',\n    'profile_admin_options' => 'Administratoroptionen',\n    'profile_admin_options_desc' => 'Weitere Administrator-Optionen, wie zum Beispiel die Verwaltung von Rollenzuweisungen, findest du in deinem Benutzerkonto im Bereich \"Einstellungen > Benutzer\" der Anwendung.',\n\n    'delete_account' => 'Konto löschen',\n    'delete_my_account' => 'Mein Konto löschen',\n    'delete_my_account_desc' => 'Dadurch wird dein Benutzerkonto vollständig vom System gelöscht. Du kannst dieses Konto nicht wiederherstellen oder diese Aktion rückgängig machen. Inhalte, die du erstellt hast, wie erstellte Seiten und hochgeladene Bilder, bleiben erhalten.',\n    'delete_my_account_warning' => 'Bist du sicher, dass du dein Benutzerkonto löschen möchten?',\n];\n"
  },
  {
    "path": "lang/de_informal/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Einstellungen',\n    'settings_save' => 'Einstellungen speichern',\n    'system_version' => 'Systemversion',\n    'categories' => 'Kategorien',\n\n    // App Settings\n    'app_customization' => 'Personalisierung',\n    'app_features_security' => 'Funktionen & Sicherheit',\n    'app_name' => 'Anwendungsname',\n    'app_name_desc' => 'Dieser Name wird im Header und in E-Mails angezeigt.',\n    'app_name_header' => 'Anwendungsname im Header anzeigen?',\n    'app_public_access' => 'Öffentlicher Zugriff',\n    'app_public_access_desc' => 'Wenn du diese Option aktivierst, können Besucher, die nicht angemeldet sind, auf Inhalte in deiner BookStack-Instanz zugreifen.',\n    'app_public_access_desc_guest' => 'Der Zugang für öffentliche Besucher kann über den Benutzer \"Guest\" gesteuert werden.',\n    'app_public_access_toggle' => 'Öffentlichen Zugriff erlauben',\n    'app_public_viewing' => 'Öffentliche Ansicht erlauben?',\n    'app_secure_images' => 'Erhöhte Sicherheit für hochgeladene Bilder aktivieren?',\n    'app_secure_images_toggle' => 'Höhere Sicherheit für Bild-Uploads aktivieren',\n    'app_secure_images_desc' => 'Aus Leistungsgründen sind alle Bilder öffentlich sichtbar. Diese Option fügt zufällige, schwer zu erratende, Zeichenketten zu Bild-URLs hinzu. Stelle sicher, dass Verzeichnisindizes deaktiviert sind, um einen einfachen Zugriff zu verhindern.',\n    'app_default_editor' => 'Standard Seiteneditor',\n    'app_default_editor_desc' => 'Wähle aus, welcher Editor bei der Bearbeitung neuer Seiten standardmäßig verwendet werden soll. Dies kann auf Seitenebene außer Kraft gesetzt werden, sofern die Berechtigungen dies zulassen.',\n    'app_custom_html' => 'Benutzerdefinierter HTML-Head-Inhalt',\n    'app_custom_html_desc' => 'Jeder Inhalt, der hier hinzugefügt wird, wird am Ende der <head>-Sektion jeder Seite eingefügt. Diese kann praktisch sein, um CSS-Styles anzupassen oder Analytics-Code hinzuzufügen.',\n    'app_custom_html_disabled_notice' => 'Benutzerdefinierte HTML-Kopfzeileninhalte sind auf dieser Einstellungsseite deaktiviert, um sicherzustellen, dass alle Änderungen rückgängig gemacht werden können.',\n    'app_logo' => 'Anwendungslogo',\n    'app_logo_desc' => 'Dies wird unter anderem in der Kopfzeile der Anwendung verwendet. Dieses Bild sollte 86px hoch sein. Große Bilder werden herunterskaliert.',\n    'app_icon' => 'Anwendungssymbol',\n    'app_icon_desc' => 'Dieses Symbol wird für Browser-Registerkarten und Verknüpfungssymbole verwendet. Dies sollte ein 256px quadratisches PNG-Bild sein.',\n    'app_homepage' => 'Startseite der Anwendung',\n    'app_homepage_desc' => 'Wähle eine Seite als Startseite aus, die statt der Standardansicht angezeigt werden soll. Seitenberechtigungen werden für die ausgewählten Seiten ignoriert.',\n    'app_homepage_select' => 'Wähle eine Seite aus',\n    'app_footer_links' => 'Fußzeilen-Links',\n    'app_footer_links_desc' => 'Füge Links hinzu, die innerhalb der Seitenfußzeile angezeigt werden. Diese werden am unteren Ende der meisten Seiten angezeigt, einschließlich derjenigen, die keinen Login benötigen. Du kannst die Bezeichnung \"trans::<key>\" verwenden, um systemdefinierte Übersetzungen zu verwenden. Beispiel: Mit \"trans::common.privacy_policy\" wird der übersetzte Text \"Privacy Policy\" bereitgestellt, und \"trans::common.terms_of_service\" liefert den übersetzten Text \"Terms of Service\".',\n    'app_footer_links_label' => 'Link-Label',\n    'app_footer_links_url' => 'Link-URL',\n    'app_footer_links_add' => 'Fußzeilenlink hinzufügen',\n    'app_disable_comments' => 'Kommentare deaktivieren',\n    'app_disable_comments_toggle' => 'Kommentare deaktivieren',\n    'app_disable_comments_desc' => 'Deaktiviert Kommentare über alle Seiten in der Anwendung. Vorhandene Kommentare werden nicht angezeigt.',\n\n    // Color settings\n    'color_scheme' => 'Farbschema der Anwendung',\n    'color_scheme_desc' => 'Lege die Farben, die in der Benutzeroberfläche verwendet werden, fest. Farben können separat für dunkle und helle Modi konfiguriert werden, um am besten zum Farbschema zu passen und die Lesbarkeit zu gewährleisten.',\n    'ui_colors_desc' => 'Lege die primäre Farbe und die Standard-Linkfarbe der Anwendung fest. Die primäre Farbe wird hauptsächlich für Kopfzeilen, Buttons und Interface-Dekorationen verwendet. Die Standard-Linkfarbe wird für textbasierte Links und Aktionen sowohl innerhalb des geschriebenen Inhalts als auch in der Benutzeroberfläche verwendet.',\n    'app_color' => 'Primäre Farbe',\n    'link_color' => 'Standard-Linkfarbe',\n    'content_colors_desc' => 'Lege Farben für alle Elemente in der Seitenorganisationshierarchie fest. Die Auswahl von Farben mit einer ähnlichen Helligkeit wie die Standardfarben wird zur Lesbarkeit empfohlen.',\n    'bookshelf_color' => 'Regalfarbe',\n    'book_color' => 'Buchfarbe',\n    'chapter_color' => 'Kapitelfarbe',\n    'page_color' => 'Seitenfarbe',\n    'page_draft_color' => 'Seitenentwurfsfarbe',\n\n    // Registration Settings\n    'reg_settings' => 'Registrierungseinstellungen',\n    'reg_enable' => 'Registrierung erlauben?',\n    'reg_enable_toggle' => 'Registrierung erlauben',\n    'reg_enable_desc' => 'Wenn die Registrierung erlaubt ist, kann sich der Benutzer als Anwendungsbenutzer anmelden. Bei der Registrierung erhält er eine einzige, voreingestellte Benutzerrolle.',\n    'reg_default_role' => 'Standard-Benutzerrolle nach Registrierung',\n    'reg_enable_external_warning' => 'Die obige Option wird ignoriert, während eine externe LDAP oder SAML Authentifizierung aktiv ist. Benutzerkonten für nicht existierende Mitglieder werden automatisch erzeugt, wenn die Authentifizierung gegen das verwendete externe System erfolgreich ist.',\n    'reg_email_confirmation' => 'Bestätigung per E-Mail',\n    'reg_email_confirmation_toggle' => 'Bestätigung per E-Mail erforderlich',\n    'reg_confirm_email_desc' => 'Falls die Einschränkung für Domains genutzt wird, ist die Bestätigung per E-Mail zwingend erforderlich und der untenstehende Wert wird ignoriert.',\n    'reg_confirm_restrict_domain' => 'Registrierung auf bestimmte Domains einschränken',\n    'reg_confirm_restrict_domain_desc' => 'Füge eine durch Komma getrennte Liste von Domains hinzu, auf die die Registrierung eingeschränkt werden soll. Benutzern wird eine E-Mail gesendet, um ihre E-Mail Adresse zu bestätigen, bevor sie diese Anwendung nutzen können.\nHinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung ändern.',\n    'reg_confirm_restrict_domain_placeholder' => 'Keine Einschränkung gesetzt',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Wähle die Standard-Sortierregel aus, die auf neue Bücher angewendet werden soll. Dies wirkt sich nicht auf bestehende Bücher aus und kann pro Buch überschrieben werden.',\n    'sorting_rules' => 'Sortierregeln',\n    'sorting_rules_desc' => 'Dies sind vordefinierte Sortieraktionen, die auf Inhalte im System angewendet werden können.',\n    'sort_rule_assigned_to_x_books' => ':count Buch zugewiesen|:count Büchern zugewiesen',\n    'sort_rule_create' => 'Sortierregel erstellen',\n    'sort_rule_edit' => 'Sortierregel bearbeiten',\n    'sort_rule_delete' => 'Sortierregel löschen',\n    'sort_rule_delete_desc' => 'Diese Sortierregel aus dem System entfernen. Bücher mit dieser Sortierung werden auf manuelle Sortierung zurückgesetzt.',\n    'sort_rule_delete_warn_books' => 'Diese Sortierregel wird derzeit in :count Bücher(n) verwendet. Bist du sicher, dass du dies löschen möchtest?',\n    'sort_rule_delete_warn_default' => 'Diese Sortierregel wird derzeit als Standard für Bücher verwendet. Bist du sicher, dass du dies löschen möchtest?',\n    'sort_rule_details' => 'Sortierregel-Details',\n    'sort_rule_details_desc' => 'Lege einen Namen für diese Sortierregel fest, der in Listen erscheint, wenn Benutzer eine Sortierung auswählen.',\n    'sort_rule_operations' => 'Sortierungs-Aktionen',\n    'sort_rule_operations_desc' => 'Konfiguriere die durchzuführenden Sortieraktionen durch Verschieben von der Liste der verfügbaren Aktionen. Bei der Verwendung werden die Aktionen von oben nach unten angewendet. Alle hier vorgenommenen Änderungen werden beim Speichern auf alle zugewiesenen Bücher angewendet.',\n    'sort_rule_available_operations' => 'Verfügbare Aktionen',\n    'sort_rule_available_operations_empty' => 'Keine verbleibenden Aktionen',\n    'sort_rule_configured_operations' => 'Konfigurierte Aktionen',\n    'sort_rule_configured_operations_empty' => 'Aktionen aus der Liste \"Verfügbare Operationen\" ziehen/hinzufügen',\n    'sort_rule_op_asc' => '(Aufst.)',\n    'sort_rule_op_desc' => '(Abst.)',\n    'sort_rule_op_name' => 'Name - Alphabetisch',\n    'sort_rule_op_name_numeric' => 'Name - Numerisch',\n    'sort_rule_op_created_date' => 'Erstellungsdatum',\n    'sort_rule_op_updated_date' => 'Aktualisierungsdatum',\n    'sort_rule_op_chapters_first' => 'Kapitel zuerst',\n    'sort_rule_op_chapters_last' => 'Kapitel zuletzt',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Wartung',\n    'maint_image_cleanup' => 'Bilder bereinigen',\n    'maint_image_cleanup_desc' => 'Überprüft Seiten- und Versionsinhalte auf ungenutzte und mehrfach vorhandene Bilder. Erstelle vor dem Start ein Backup Deiner Datenbank und Bilder.',\n    'maint_delete_images_only_in_revisions' => 'Lösche auch Bilder, die nur in alten Seitenüberarbeitungen vorhanden sind',\n    'maint_image_cleanup_run' => 'Reinigung starten',\n    'maint_image_cleanup_warning' => ':count eventuell unbenutze Bilder wurden gefunden. Möchtest du diese Bilder löschen?',\n    'maint_image_cleanup_success' => ':count eventuell unbenutze Bilder wurden gefunden und gelöscht.',\n    'maint_image_cleanup_nothing_found' => 'Keine unbenutzen Bilder gefunden. Nichts zu löschen!',\n    'maint_send_test_email' => 'Eine Test-E-Mail versenden',\n    'maint_send_test_email_desc' => 'Dies sendet eine Test-E-Mail an die in deinem Profil angegebene E-Mail-Adresse.',\n    'maint_send_test_email_run' => 'Test-E-Mail senden',\n    'maint_send_test_email_success' => 'E-Mail wurde an :address gesendet',\n    'maint_send_test_email_mail_subject' => 'Test-E-Mail',\n    'maint_send_test_email_mail_greeting' => 'E-Mail-Versand scheint zu funktionieren!',\n    'maint_send_test_email_mail_text' => 'Glückwunsch! Da du diese E-Mail Benachrichtigung erhalten hast, scheinen deine E-Mail-Einstellungen korrekt konfiguriert zu sein.',\n    'maint_recycle_bin_desc' => 'Gelöschte Regale, Bücher, Kapitel & Seiten werden in den Papierkorb verschoben, so dass sie wiederhergestellt oder dauerhaft gelöscht werden können. Ältere Einträge im Papierkorb können, in Abhängigkeit von der Systemkonfiguration, nach einer Weile automatisch entfernt werden.',\n    'maint_recycle_bin_open' => 'Papierkorb öffnen',\n    'maint_regen_references' => 'Verweise neu generieren',\n    'maint_regen_references_desc' => 'Diese Aktion wird den Verweisindex innerhalb der Datenbank neu erstellen. Dies wird normalerweise automatisch ausgeführt, aber diese Aktion kann nützlich sein, um alte Inhalte oder Inhalte zu indizieren, die mittels inoffizieller Methoden hinzugefügt wurden.',\n    'maint_regen_references_success' => 'Verweisindex wurde neu generiert!',\n    'maint_timeout_command_note' => 'Hinweis: Die Ausführung dieser Aktion kann einige Zeit in Anspruch nehmen, was in einigen Webumgebungen zu Timeout-Problemen führen kann. Alternativ kann diese Aktion auch mit einem Terminalbefehl ausgeführt werden.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Papierkorb',\n    'recycle_bin_desc' => 'Hier kannst du gelöschte Einträge wiederherstellen oder sie dauerhaft aus dem System entfernen. Diese Liste ist nicht gefiltert, im Gegensatz zu ähnlichen Aktivitätslisten im System, wo Berechtigungsfilter angewendet werden.',\n    'recycle_bin_deleted_item' => 'Gelöschter Eintrag',\n    'recycle_bin_deleted_parent' => 'Übergeordnet',\n    'recycle_bin_deleted_by' => 'Gelöscht von',\n    'recycle_bin_deleted_at' => 'Löschzeitpunkt',\n    'recycle_bin_permanently_delete' => 'Dauerhaft löschen',\n    'recycle_bin_restore' => 'Wiederherstellen',\n    'recycle_bin_contents_empty' => 'Der Papierkorb ist derzeit leer',\n    'recycle_bin_empty' => 'Papierkorb leeren',\n    'recycle_bin_empty_confirm' => 'Dies wird alle Einträge im Papierkorb dauerhaft entfernen, einschließlich der Inhalte, die darin enthalten sind. Bist du sicher, dass du den Papierkorb leeren möchtest?',\n    'recycle_bin_destroy_confirm' => 'Dieser Schritt löscht permanent das Element, gemeinsam mit allen untergeordneten Elementen, aus dem System. Dieser Schritt kann nicht rückgängig gemacht werden. Sind Sie sicher, dass Sie eine dauerhafte Löschung durchführen wollen?',\n    'recycle_bin_destroy_list' => 'Zu löschende Einträge',\n    'recycle_bin_restore_list' => 'Wiederherzustellende Einträge',\n    'recycle_bin_restore_confirm' => 'Mit dieser Aktion wird der gelöschte Eintrag einschließlich aller untergeordneten Einträge an seinem ursprünglichen Ort wiederhergestellt. Wenn der ursprüngliche Ort gelöscht wurde und sich nun im Papierkorb befindet, muss auch der übergeordnete Eintrag wiederhergestellt werden.',\n    'recycle_bin_restore_deleted_parent' => 'Der übergeordnete Eintrag wurde ebenfalls gelöscht. Dieser Eintrag wird weiterhin als gelöscht zählen, bis auch der übergeordnete Eintrag wiederhergestellt wurde.',\n    'recycle_bin_restore_parent' => 'Übergeordneter Eintrag wiederherstellen',\n    'recycle_bin_destroy_notification' => ':count Einträge wurden aus dem Papierkorb gelöscht.',\n    'recycle_bin_restore_notification' => ':count Einträge wurden aus dem Papierkorb wiederhergestellt.',\n\n    // Audit Log\n    'audit' => 'Änderungsprotokoll',\n    'audit_desc' => 'Dieses Audit-Protokoll zeigt eine Liste der Aktivitäten an, welche vom System protokolliert werden. Im Gegensatz zu den anderen Aktivitätslisten im System, bei denen Berechtigungen angewendet werden, ist diese Liste ungefiltert.',\n    'audit_event_filter' => 'Ereignisfilter',\n    'audit_event_filter_no_filter' => 'Kein Filter',\n    'audit_deleted_item' => 'Gelöschtes Element',\n    'audit_deleted_item_name' => 'Name: :name',\n    'audit_table_user' => 'Benutzer',\n    'audit_table_event' => 'Ereignis',\n    'audit_table_related' => 'Verknüpfter Eintrag oder Detail',\n    'audit_table_ip' => 'IP-Adresse',\n    'audit_table_date' => 'Aktivitätsdatum',\n    'audit_date_from' => 'Zeitraum von',\n    'audit_date_to' => 'Zeitraum bis',\n\n    // Role Settings\n    'roles' => 'Rollen',\n    'role_user_roles' => 'Benutzer-Rollen',\n    'roles_index_desc' => 'Rollen werden verwendet, um Benutzer zu gruppieren und System-Berechtigungen für ihre Mitglieder zuzuweisen. Wenn ein Benutzer Mitglied mehrerer Rollen ist, stapeln die gewährten Berechtigungen und der Benutzer wird alle Fähigkeiten erben.',\n    'roles_x_users_assigned' => ':count Benutzer zugewiesen|:count Benutzer zugewiesen',\n    'roles_x_permissions_provided' => ':count Berechtigung|:count Berechtigungen',\n    'roles_assigned_users' => 'Zugewiesene Benutzer',\n    'roles_permissions_provided' => 'Genutzte Berechtigungen',\n    'role_create' => 'Neue Rolle anlegen',\n    'role_delete' => 'Rolle löschen',\n    'role_delete_confirm' => 'Dies wird die Rolle \":roleName\" löschen.',\n    'role_delete_users_assigned' => 'Diese Rolle ist :userCount Benutzern zugeordnet. Du kannst unten eine neue Rolle auswählen, die du diesen Benutzern zuordnen möchtest.',\n    'role_delete_no_migration' => \"Den Benutzern keine andere Rolle zuordnen\",\n    'role_delete_sure' => 'Bist du sicher, dass du diese Rolle löschen möchtest?',\n    'role_edit' => 'Rolle bearbeiten',\n    'role_details' => 'Rollendetails',\n    'role_name' => 'Rollenname',\n    'role_desc' => 'Kurzbeschreibung der Rolle',\n    'role_mfa_enforced' => 'Benötigt Mehrfach-Faktor-Authentifizierung',\n    'role_external_auth_id' => 'Externe Authentifizierungs-IDs',\n    'role_system' => 'System-Berechtigungen',\n    'role_manage_users' => 'Benutzer verwalten',\n    'role_manage_roles' => 'Rollen und Rollen-Berechtigungen verwalten',\n    'role_manage_entity_permissions' => 'Alle Buch-, Kapitel- und Seiten-Berechtigungen verwalten',\n    'role_manage_own_entity_permissions' => 'Nur Berechtigungen eigener Bücher, Kapitel und Seiten verwalten',\n    'role_manage_page_templates' => 'Seitenvorlagen verwalten',\n    'role_access_api' => 'Systemzugriffs-API',\n    'role_manage_settings' => 'Globaleinstellungen verwalten',\n    'role_export_content' => 'Inhalt exportieren',\n    'role_import_content' => 'Inhalt importieren',\n    'role_editor_change' => 'Seiteneditor ändern',\n    'role_notifications' => 'Empfangen und Verwalten von Benachrichtigungen',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Berechtigungen',\n    'roles_system_warning' => 'Beachte, dass der Zugriff auf eine der oben genannten drei Berechtigungen einem Benutzer erlauben kann, seine eigenen Berechtigungen oder die Rechte anderer im System zu ändern. Weise nur Rollen mit diesen Berechtigungen vertrauenswürdigen Benutzern zu.',\n    'role_asset_desc' => 'Diese Berechtigungen gelten für den Standard-Zugriff innerhalb des Systems. Berechtigungen für Bücher, Kapitel und Seiten überschreiben diese Berechtigungen.',\n    'role_asset_admins' => 'Administratoren erhalten automatisch Zugriff auf alle Inhalte, aber diese Optionen können Oberflächenoptionen ein- oder ausblenden.',\n    'role_asset_image_view_note' => 'Das bezieht sich auf die Sichtbarkeit innerhalb des Bildmanagers. Der tatsächliche Zugriff auf hochgeladene Bilddateien hängt von der Speicheroption des Systems für Bilder ab.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'Alle',\n    'role_own' => 'Eigene',\n    'role_controlled_by_asset' => 'Berechtigungen werden vom Uploadziel bestimmt',\n    'role_save' => 'Rolle speichern',\n    'role_users' => 'Dieser Rolle zugeordnete Benutzer',\n    'role_users_none' => 'Bisher sind dieser Rolle keine Benutzer zugeordnet',\n\n    // Users\n    'users' => 'Benutzer',\n    'users_index_desc' => 'Erstelle und Verwalte individuelle Benutzerkonten innerhalb des Systems. Benutzerkonten werden zur Anmeldung und der Zuordnung von Inhalten und Aktivitäten verwendet. Zugriffsberechtigungen sind in erster Linie rollenbasiert, aber der Besitz von Benutzerinhalten kann unter anderem auch Berechtigungen beeinflussen.',\n    'user_profile' => 'Benutzerprofil',\n    'users_add_new' => 'Benutzer hinzufügen',\n    'users_search' => 'Benutzer suchen',\n    'users_latest_activity' => 'Neueste Aktivitäten',\n    'users_details' => 'Benutzerdetails',\n    'users_details_desc' => 'Lege für diesen Benutzer einen Anzeigenamen und eine E-Mail-Adresse fest. Die E-Mail-Adresse wird bei der Anmeldung verwendet.',\n    'users_details_desc_no_email' => 'Lege für diesen Benutzer einen Anzeigenamen fest, damit andere ihn erkennen können.',\n    'users_role' => 'Benutzerrollen',\n    'users_role_desc' => 'Wählen Sie aus, welchen Rollen dieser Benutzer zugeordnet werden soll. Wenn ein Benutzer mehreren Rollen zugeordnet ist, werden die Berechtigungen dieser Rollen gestapelt und er erhält alle Fähigkeiten der zugewiesenen Rollen.',\n    'users_password' => 'Benutzerpasswort',\n    'users_password_desc' => 'Lege ein Passwort fest, mit dem du dich anmelden möchtest. Diese muss mindestens 8 Zeichen lang sein.',\n    'users_send_invite_text' => 'Du kannst diesem Benutzer eine Einladungs-E-Mail senden, die es ihm erlaubt, sein eigenes Passwort zu setzen, andernfalls kannst du sein Passwort selbst setzen.',\n    'users_send_invite_option' => 'Benutzer-Einladungs-E-Mail senden',\n    'users_external_auth_id' => 'Externe Authentifizierungs-ID',\n    'users_external_auth_id_desc' => 'Wenn ein externes Authentifizierungssystem verwendet wird (z. B. SAML2, OIDC oder LDAP) ist dies die ID, die diesen BookStack-Benutzer mit dem Authentifizierungs-Systemkonto verknüpft. Du kannst dieses Feld ignorieren, wenn du die Standard-E-Mail-basierte Authentifizierung verwenden.',\n    'users_password_warning' => 'Fülle die untenstehenden Felder nur aus, wenn du das Passwort für diesen Benutzer ändern möchten.',\n    'users_system_public' => 'Dieser Benutzer repräsentiert alle unangemeldeten Benutzer, die diese Seite betrachten. Er kann nicht zum Anmelden benutzt werden, sondern wird automatisch zugeordnet.',\n    'users_delete' => 'Benutzer löschen',\n    'users_delete_named' => 'Benutzer \":userName\" löschen',\n    'users_delete_warning' => 'Der Benutzer \":userName\" wird aus dem System gelöscht.',\n    'users_delete_confirm' => 'Bist du sicher, dass du diesen Benutzer löschen möchtest?',\n    'users_migrate_ownership' => 'Besitz migrieren',\n    'users_migrate_ownership_desc' => 'Wähle hier einen Benutzer, wenn du möchtest, dass ein anderer Benutzer der Besitzer aller Einträge wird, die diesem Benutzer derzeit gehören.',\n    'users_none_selected' => 'Kein Benutzer ausgewählt',\n    'users_edit' => 'Benutzer bearbeiten',\n    'users_edit_profile' => 'Profil bearbeiten',\n    'users_avatar' => 'Benutzer-Bild',\n    'users_avatar_desc' => 'Das Bild sollte eine Auflösung von 256x256px haben.',\n    'users_preferred_language' => 'Bevorzugte Sprache',\n    'users_preferred_language_desc' => 'Diese Option ändert die Sprache, die für die Benutzeroberfläche der Anwendung verwendet wird. Dies hat keinen Einfluss auf von Benutzern erstellte Inhalte.',\n    'users_social_accounts' => 'Social-Media Konten',\n    'users_social_accounts_desc' => 'Zeigt den Status der verbundenen sozialen Konten für diesen Benutzer an. Social Accounts können zusätzlich zum primären Authentifizierungssystem für den Systemzugriff verwendet werden.',\n    'users_social_accounts_info' => 'Hier kannst Du andere Social-Media-Konten für eine schnellere und einfachere Anmeldung verknüpfen. Wenn Du ein Social-Media Konto löschst, bleibt der Zugriff erhalten. Entferne in diesem Falle die Berechtigung in Deinen Profil-Einstellungen des verknüpften Social-Media-Kontos.',\n    'users_social_connect' => 'Social-Media-Konto verknüpfen',\n    'users_social_disconnect' => 'Social-Media-Konto lösen',\n    'users_social_status_connected' => 'Verbunden',\n    'users_social_status_disconnected' => 'Getrennt',\n    'users_social_connected' => ':socialAccount-Konto wurde erfolgreich mit dem Profil verknüpft.',\n    'users_social_disconnected' => ':socialAccount-Konto wurde erfolgreich vom Profil gelöst.',\n    'users_api_tokens' => 'API-Token',\n    'users_api_tokens_desc' => 'Erstelle und verwalte die Zugangs-Tokens zur Authentifizierung mit der BookStack REST API. Berechtigungen für die API werden über den Benutzer verwaltet, dem das Token gehört.',\n    'users_api_tokens_none' => 'Für diesen Benutzer wurden kein API-Token erstellt',\n    'users_api_tokens_create' => 'Token erstellen',\n    'users_api_tokens_expires' => 'Endet',\n    'users_api_tokens_docs' => 'API Dokumentation',\n    'users_mfa' => 'Multi-Faktor-Authentifizierung',\n    'users_mfa_desc' => 'Richte Multi-Faktor-Authentifizierung als zusätzliche Sicherheitsstufe für dein Benutzerkonto ein.',\n    'users_mfa_x_methods' => ':count Methode konfiguriert|:count Methoden konfiguriert',\n    'users_mfa_configure' => 'Methoden konfigurieren',\n\n    // API Tokens\n    'user_api_token_create' => 'Neuen API-Token erstellen',\n    'user_api_token_name' => 'Name',\n    'user_api_token_name_desc' => 'Gebe deinem Token einen aussagekräftigen Namen als spätere Erinnerung an seinen Verwendungszweck.',\n    'user_api_token_expiry' => 'Ablaufdatum',\n    'user_api_token_expiry_desc' => 'Lege ein Datum fest, zu dem dieser Token abläuft. Nach diesem Datum funktionieren Anfragen, die mit diesem Token gestellt werden, nicht mehr. Wenn du dieses Feld leer lässt, wird ein Ablaufdatum von 100 Jahren in der Zukunft festgelegt.',\n    'user_api_token_create_secret_message' => 'Unmittelbar nach der Erstellung dieses Tokens wird eine \"Token ID\" & ein \"Token Kennwort\" generiert und angezeigt. Das Kennwort wird nur ein einziges Mal angezeigt. Stelle also sicher, dass du den Inhalt an einen sicheren Ort kopierst, bevor du fortfährst.',\n    'user_api_token' => 'API-Token',\n    'user_api_token_id' => 'Token ID',\n    'user_api_token_id_desc' => 'Dies ist ein nicht editierbarer, vom System generierter Identifikator für diesen Token, welcher bei API-Anfragen angegeben werden muss.',\n    'user_api_token_secret' => 'Token Kennwort',\n    'user_api_token_secret_desc' => 'Dies ist ein systemgeneriertes Kennwort für diesen Token, das bei API-Anfragen zur Verfügung gestellt werden muss. Es wird nur dieses eine Mal angezeigt, deshalb kopiere diesen an einen sicheren und geschützten Ort.',\n    'user_api_token_created' => 'Token erstellt :timeAgo',\n    'user_api_token_updated' => 'Token aktualisiert :timeAgo',\n    'user_api_token_delete' => 'Lösche Token',\n    'user_api_token_delete_warning' => 'Dies löscht den API-Token mit dem Namen \\':tokenName\\' vollständig aus dem System.',\n    'user_api_token_delete_confirm' => 'Bist du sicher, dass du diesen API-Token löschen möchtest?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhooks sind eine Möglichkeit, Daten an externe URLs zu senden, wenn bestimmte Aktionen und Ereignisse im System auftreten, was eine ereignisbasierte Integration mit externen Plattformen wie Messaging- oder Benachrichtigungssystemen ermöglicht.',\n    'webhooks_x_trigger_events' => ':count Auslöserereignis|:count Auslöserereignisse',\n    'webhooks_create' => 'Neuen Webhook erstellen',\n    'webhooks_none_created' => 'Es wurden noch keine Webhooks erstellt.',\n    'webhooks_edit' => 'Webhook bearbeiten',\n    'webhooks_save' => 'Webhook speichern',\n    'webhooks_details' => 'Webhook-Details',\n    'webhooks_details_desc' => 'Gebe einen benutzerfreundlichen Namen und einen POST-Endpunkt als Ziel an, an den die Webhook-Daten gesendet werden sollen.',\n    'webhooks_events' => 'Webhook Ereignisse',\n    'webhooks_events_desc' => 'Wähle alle Ereignisse, die diesen Webhook auslösen sollen.',\n    'webhooks_events_warning' => 'Beachte, dass diese Ereignisse für alle ausgewählten Ereignisse ausgelöst werden, auch wenn benutzerdefinierte Berechtigungen angewendet werden. Stelle sicher, dass die Verwendung dieses Webhooks keine vertraulichen Inhalte enthüllt.',\n    'webhooks_events_all' => 'Alle System-Ereignisse',\n    'webhooks_name' => 'Webhook-Name',\n    'webhooks_timeout' => 'Webhook Request Timeout (Sekunden)',\n    'webhooks_endpoint' => 'Webhook Endpunkt',\n    'webhooks_active' => 'Webhook aktiv',\n    'webhook_events_table_header' => 'Ereignisse',\n    'webhooks_delete' => 'Webhook löschen',\n    'webhooks_delete_warning' => 'Dies wird diesen Webhook mit dem Namen \\':webhookName\\' vollständig aus dem System löschen.',\n    'webhooks_delete_confirm' => 'Bist du sicher, dass du diesen Webhook löschen möchtest?',\n    'webhooks_format_example' => 'Webhook Format Beispiel',\n    'webhooks_format_example_desc' => 'Webhook Daten werden als POST-Anfrage an den konfigurierten Endpunkt als JSON im folgenden Format gesendet. Die Eigenschaften \"related_item\" und \"url\" sind optional und hängen vom Typ des ausgelösten Ereignisses ab.',\n    'webhooks_status' => 'Webhook-Status',\n    'webhooks_last_called' => 'Zuletzt aufgerufen:',\n    'webhooks_last_errored' => 'Letzter Fehler:',\n    'webhooks_last_error_message' => 'Letzte Fehlermeldung:',\n\n    // Licensing\n    'licenses' => 'Lizenzen',\n    'licenses_desc' => 'Diese Seite beschreibt Lizenzinformationen für BookStack zusätzlich zu den Projekten und Bibliotheken, die in BookStack verwendet werden. Viele aufgelistete Projekte werden nur in einem Entwicklungskontext verwendet.',\n    'licenses_bookstack' => 'BookStack-Lizenz',\n    'licenses_php' => 'PHP-Bibliothekslizenzen',\n    'licenses_js' => 'JavaScript-Bibliothekslizenzen',\n    'licenses_other' => 'Andere Lizenzen',\n    'license_details' => 'Lizenzdetails',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'Englisch',\n        'ar' => 'Arabisch',\n        'bg' => 'Bulgarisch',\n        'bs' => 'Bosnisch',\n        'ca' => 'Katalanisch',\n        'cs' => 'Tschechisch',\n        'cy' => 'Cymraeg',\n        'da' => 'Dänisch',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Spanisch',\n        'es_AR' => 'Spanisch Argentinisch',\n        'et' => 'Estnisch',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Französisch',\n        'he' => 'עברית',\n        'hr' => 'Kroatisch',\n        'hu' => 'Ungarisch',\n        'id' => 'Bahasa-Indonesisch',\n        'it' => 'Italienisch',\n        'ja' => 'Japanisch',\n        'ko' => 'Koreanisch',\n        'lt' => 'Litauisch',\n        'lv' => 'Lettisch',\n        'nb' => 'Norwegisch (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Niederländisch',\n        'pl' => 'Polnisch',\n        'pt' => 'Portugiesisch',\n        'pt_BR' => 'Portugiesisch (Brasilien)',\n        'ro' => 'Română',\n        'ru' => 'Russisch',\n        'sk' => 'Slowenisch',\n        'sl' => 'Slowenisch',\n        'sv' => 'Schwedisch',\n        'tr' => 'Türkisch',\n        'uk' => 'Ukrainisch',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Vietnamesisch',\n        'zh_CN' => 'Vereinfachtes Chinesisch',\n        'zh_TW' => 'Traditionelles Chinesisch',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/de_informal/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute muss akzeptiert werden.',\n    'active_url'           => ':attribute ist keine gültige URL.',\n    'after'                => ':attribute muss ein Datum nach :date sein.',\n    'alpha'                => ':attribute kann nur Buchstaben enthalten.',\n    'alpha_dash'           => ':attribute kann nur Buchstaben, Zahlen und Bindestriche enthalten.',\n    'alpha_num'            => ':attribute kann nur Buchstaben und Zahlen enthalten.',\n    'array'                => ':attribute muss ein Array sein.',\n    'backup_codes'         => 'Der angegebene Code ist ungültig oder wurde bereits verwendet.',\n    'before'               => ':attribute muss ein Datum vor :date sein.',\n    'between'              => [\n        'numeric' => ':attribute muss zwischen :min und :max liegen.',\n        'file'    => ':attribute muss zwischen :min und :max Kilobytes groß sein.',\n        'string'  => ':attribute muss zwischen :min und :max Zeichen lang sein.',\n        'array'   => ':attribute muss zwischen :min und :max Elemente enthalten.',\n    ],\n    'boolean'              => ':attribute Feld muss wahr oder falsch sein.',\n    'confirmed'            => ':attribute stimmt nicht überein.',\n    'date'                 => ':attribute ist kein gültiges Datum.',\n    'date_format'          => ':attribute entspricht nicht dem Format :format.',\n    'different'            => ':attribute und :other müssen unterschiedlich sein.',\n    'digits'               => ':attribute muss :digits Stellen haben.',\n    'digits_between'       => ':attribute muss zwischen :min und :max Stellen haben.',\n    'email'                => ':attribute muss eine gültige E-Mail-Adresse sein.',\n    'ends_with' => ':attribute muss mit einem der folgenden Werte: :values enden',\n    'file'                 => ':attribute muss als gültige Datei angegeben werden.',\n    'filled'               => ':attribute ist erforderlich.',\n    'gt'                   => [\n        'numeric' => ':attribute muss größer als :value sein.',\n        'file'    => ':attribute muss mindestens größer als :value Kilobytes sein.',\n        'string'  => ':attribute muss mehr als :value Zeichen haben.',\n        'array'   => ':attribute muss mehr als :value Elemente haben.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute muss größer-gleich :value sein.',\n        'file'    => ':attribute muss größer-gleich :value Kilobytes sein.',\n        'string'  => ':attribute muss mindestens :value Zeichen haben.',\n        'array'   => ':attribute muss :value Elemente oder mehr haben.',\n    ],\n    'exists'               => ':attribute ist ungültig.',\n    'image'                => ':attribute muss ein Bild sein.',\n    'image_extension'      => ':attribute muss eine gültige und unterstützte Bild-Dateiendung haben.',\n    'in'                   => ':attribute ist ungültig.',\n    'integer'              => ':attribute muss eine Zahl sein.',\n    'ip'                   => ':attribute muss eine gültige IP-Adresse sein.',\n    'ipv4'                 => ':attribute muss eine gültige IPv4-Adresse sein.',\n    'ipv6'                 => ':attribute muss eine gültige IPv6-Adresse sein.',\n    'json'                 => ':attribute muss ein gültiger JSON-String sein.',\n    'lt'                   => [\n        'numeric' => ':attribute muss kleiner als :value sein.',\n        'file'    => ':attribute muss kleiner als :value Kilobytes sein.',\n        'string'  => ':attribute muss weniger als :value Zeichen haben.',\n        'array'   => ':attribute muss weniger als :value Elemente haben.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute muss kleiner oder gleich :value sein.',\n        'file'    => ':attribute muss kleiner oder gleich :value Kilobytes sein.',\n        'string'  => ':attribute muss :value oder weniger Zeichen haben.',\n        'array'   => ':attribute darf höchstens :value Elemente haben.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute darf nicht größer als :max sein.',\n        'file'    => ':attribute darf nicht größer als :max Kilobyte sein.',\n        'string'  => ':attribute darf nicht länger als :max Zeichen sein.',\n        'array'   => ':attribute darf nicht mehr als :max Elemente enthalten.',\n    ],\n    'mimes'                => ':attribute muss eine Datei vom Typ: :values sein.',\n    'min'                  => [\n        'numeric' => ':attribute muss mindestens :min sein.',\n        'file'    => ':attribute muss mindestens :min Kilobyte groß sein.',\n        'string'  => ':attribute muss mindestens :min Zeichen lang sein.',\n        'array'   => ':attribute muss mindesten :min Elemente enthalten.',\n    ],\n    'not_in'               => 'Das ausgewählte :attribute ist ungültig.',\n    'not_regex'            => ':attribute ist kein gültiges Format.',\n    'numeric'              => ':attribute muss eine Zahl sein.',\n    'regex'                => ':attribute ist in einem ungültigen Format.',\n    'required'             => ':attribute ist erforderlich.',\n    'required_if'          => ':attribute ist erforderlich, wenn :other :value ist.',\n    'required_with'        => ':attribute ist erforderlich, wenn :values vorhanden ist.',\n    'required_with_all'    => ':attribute ist erforderlich, wenn :values vorhanden sind.',\n    'required_without'     => ':attribute ist erforderlich, wenn :values nicht vorhanden ist.',\n    'required_without_all' => ':attribute ist erforderlich, wenn :values nicht vorhanden sind.',\n    'same'                 => ':attribute und :other müssen übereinstimmen.',\n    'safe_url'             => 'Der angegebene Link ist möglicherweise nicht sicher.',\n    'size'                 => [\n        'numeric' => ':attribute muss :size sein.',\n        'file'    => ':attribute muss :size Kilobytes groß sein.',\n        'string'  => ':attribute muss :size Zeichen lang sein.',\n        'array'   => ':attribute muss :size Elemente enthalten.',\n    ],\n    'string'               => ':attribute muss eine Zeichenkette sein.',\n    'timezone'             => ':attribute muss eine gültige Zeitzone sein.',\n    'totp'                 => 'Der angegebene Code ist ungültig oder abgelaufen.',\n    'unique'               => ':attribute wird bereits verwendet.',\n    'url'                  => ':attribute ist kein valides Format.',\n    'uploaded'             => 'Die Datei konnte nicht hochgeladen werden. Der Server akzeptiert möglicherweise keine Dateien dieser Größe.',\n\n    'zip_file' => ':attribute muss auf eine Datei innerhalb des ZIP verweisen.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => ':attribute muss eine Datei des Typs :validType referenzieren, gefunden :foundType.',\n    'zip_model_expected' => 'Datenobjekt erwartet, aber \":type\" gefunden.',\n    'zip_unique' => ':attribute muss für den Objekttyp innerhalb des ZIP eindeutig sein.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Passwortbestätigung erforderlich',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/el/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'σελίδα που δημιουργήθηκε',\n    'page_create_notification'    => 'Η σελίδα δημιουργήθηκε με επιτυχία',\n    'page_update'                 => 'ενημερωμένη σελίδα',\n    'page_update_notification'    => 'Η σελίδα ενημερώθηκε με επιτυχία',\n    'page_delete'                 => 'διαγραμμένη σελίδα',\n    'page_delete_notification'    => 'Η σελίδα διαγράφηκε επιτυχώς',\n    'page_restore'                => 'αποκατεστημένη σελίδα',\n    'page_restore_notification'   => 'Η σελίδα αποκαταστάθηκε με επιτυχία',\n    'page_move'                   => 'σελίδα που μετακινήθηκε',\n    'page_move_notification'      => 'Η σελίδα μετακινήθηκε με επιτυχία',\n\n    // Chapters\n    'chapter_create'              => 'δημιουργήθηκε κεφάλαιο',\n    'chapter_create_notification' => 'Το κεφάλαιο δημιουργήθηκε με επιτυχία',\n    'chapter_update'              => 'ενημερωμένο κεφάλαιο',\n    'chapter_update_notification' => 'Το κεφάλαιο ενημερώθηκε με επιτυχία',\n    'chapter_delete'              => 'διαγραμμένο κεφάλαιο',\n    'chapter_delete_notification' => 'Το κεφάλαιο διαγράφηκε επιτυχώς',\n    'chapter_move'                => 'το κεφάλαιο μετακινήθηκε',\n    'chapter_move_notification' => 'Το κεφάλαιο μετακινήθηκε με επιτυχία',\n\n    // Books\n    'book_create'                 => 'το βιβλίο δημιουργήθηκε',\n    'book_create_notification'    => 'Το βιβλίο δημιουργήθηκε με επιτυχία',\n    'book_create_from_chapter'              => 'Το κεφάλαιο μετατράπηκε επιτυχώς σε βιβλίο',\n    'book_create_from_chapter_notification' => 'Το κεφάλαιο μετατράπηκε επιτυχώς σε βιβλίο',\n    'book_update'                 => 'ενημερωμένο βιβλίο',\n    'book_update_notification'    => 'Το βιβλίο ενημερώθηκε με επιτυχία',\n    'book_delete'                 => 'διαγραμμένο βιβλίο',\n    'book_delete_notification'    => 'Το βιβλίο διαγράφηκε επιτυχώς',\n    'book_sort'                   => 'ταξινομημένο βιβλίο',\n    'book_sort_notification'      => 'Το βιβλίο επαναταξινομήθηκε επιτυχώς',\n\n    // Bookshelves\n    'bookshelf_create'            => 'δημιουργήθηκε ράφι',\n    'bookshelf_create_notification'    => 'Το ράφι δημιουργήθηκε με επιτυχία',\n    'bookshelf_create_from_book'    => 'το βιβλίο μετατράπηκε σε ράφι',\n    'bookshelf_create_from_book_notification'    => 'Το βιβλίο μετατράπηκε σε ράφι επιτυχώς',\n    'bookshelf_update'                 => 'ενημερωμένο ράφι',\n    'bookshelf_update_notification'    => 'Το ράφι ενημερώθηκε επιτυχώς',\n    'bookshelf_delete'                 => 'διαγραμμένο ράφι',\n    'bookshelf_delete_notification'    => 'Το ράφι ενημερώθηκε επιτυχώς',\n\n    // Revisions\n    'revision_restore' => 'αποκατεστημένη αναθεώρηση',\n    'revision_delete' => 'διαγραμμένη αναθεώρηση',\n    'revision_delete_notification' => 'Η αναθεώρηση διαγράφηκε με επιτυχία',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" προστέθηκε στα αγαπημένα σας',\n    'favourite_remove_notification' => '\":name\" προστέθηκε στα αγαπημένα σας',\n\n    // Watching\n    'watch_update_level_notification' => 'Οι προτιμήσεις παρακολούθησης ενημερώθηκαν επιτυχώς',\n\n    // Auth\n    'auth_login' => 'συνδεδεμένος',\n    'auth_register' => 'εγγεγραμμένος ως νέος χρήστης',\n    'auth_password_reset_request' => 'ζητήθηκε επαναφορά κωδικού πρόσβασης χρήστη',\n    'auth_password_reset_update' => 'επαναφορά κωδικού πρόσβασης χρήστη',\n    'mfa_setup_method' => 'διαμορφωμένη μέθοδος MFA',\n    'mfa_setup_method_notification' => 'Η μέθοδος πολλαπλών παραγόντων διαμορφώθηκε επιτυχώς',\n    'mfa_remove_method' => 'καταργήθηκε η μέθοδος MFA',\n    'mfa_remove_method_notification' => 'Η μέθοδος πολλαπλών παραγόντων καταργήθηκε με επιτυχία',\n\n    // Settings\n    'settings_update' => 'ενημερωμένες ρυθμίσεις',\n    'settings_update_notification' => 'Οι ρυθμίσεις ενημερώθηκαν με επιτυχία',\n    'maintenance_action_run' => 'έτρεξε δράση συντήρησης',\n\n    // Webhooks\n    'webhook_create' => 'Το webhook δημιουργήθηκε',\n    'webhook_create_notification' => 'Το Webhook δημιουργήθηκε με επιτυχία',\n    'webhook_update' => 'ενημερωμένο webhook',\n    'webhook_update_notification' => 'Το Webhook ενημερώθηκε με επιτυχία',\n    'webhook_delete' => 'διαγραμμένο webhook',\n    'webhook_delete_notification' => 'Το Webhook διαγράφηκε επιτυχώς',\n\n    // Imports\n    'import_create' => 'δημιουργημένη εισαγωγή',\n    'import_create_notification' => 'Το εισαγόμενο αρχείο μεταφορτώθηκε επιτυχώς',\n    'import_run' => 'ενημερωμένη εισαγωγή',\n    'import_run_notification' => 'Το περιεχόμενο εισήχθη επιτυχώς',\n    'import_delete' => 'διαγραμμένη εισαγωγή',\n    'import_delete_notification' => 'Η εισαγωγή διαγράφηκε επιτυχώς',\n\n    // Users\n    'user_create' => 'δημιουργημένος χρήστης',\n    'user_create_notification' => 'Ο χρήστης δημιουργήθηκε με επιτυχία',\n    'user_update' => 'ενημερωμένος χρήστης',\n    'user_update_notification' => 'Ο Χρήστης ενημερώθηκε με επιτυχία',\n    'user_delete' => 'διαγραμμένος χρήστης',\n    'user_delete_notification' => 'Ο Χρήστης αφαιρέθηκε επιτυχώς',\n\n    // API Tokens\n    'api_token_create' => 'created API token',\n    'api_token_create_notification' => 'O κωδικός API δημιουργήθηκε με επιτυχία',\n    'api_token_update' => 'updated API token',\n    'api_token_update_notification' => 'κωδικός API ενημερώθηκε με επιτυχία',\n    'api_token_delete' => 'deleted API token',\n    'api_token_delete_notification' => 'Το διακριτικό API διαγράφηκε με επιτυχία',\n\n    // Roles\n    'role_create' => 'δημιουργημένος ρόλος',\n    'role_create_notification' => 'Ο Ρόλος δημιουργήθηκε με επιτυχία',\n    'role_update' => 'Ενημέρωση ρόλου',\n    'role_update_notification' => 'Ο Ρόλος ενημερώθηκε με επιτυχία',\n    'role_delete' => 'διαγραμμένος ρόλος',\n    'role_delete_notification' => 'Ο Ρόλος διαγράφηκε επιτυχώς',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'αδειασμένος κάδος ανακύκλωσης',\n    'recycle_bin_restore' => 'αποκαταστάθηκε από τον κάδο ανακύκλωσης',\n    'recycle_bin_destroy' => 'αφαιρέθηκε από τον κάδο ανακύκλωσης',\n\n    // Comments\n    'commented_on'                => 'σχολίασε',\n    'comment_create'              => 'προστέθηκε σχόλιο',\n    'comment_update'              => 'ενημερώθηκε σχόλιο',\n    'comment_delete'              => 'διαγράφηκε σχόλιο',\n\n    // Sort Rules\n    'sort_rule_create' => 'created sort rule',\n    'sort_rule_create_notification' => 'Sort rule successfully created',\n    'sort_rule_update' => 'updated sort rule',\n    'sort_rule_update_notification' => 'Sort rule successfully updated',\n    'sort_rule_delete' => 'deleted sort rule',\n    'sort_rule_delete_notification' => 'Sort rule successfully deleted',\n\n    // Other\n    'permissions_update'          => 'ενημερωμένα δικαιώματα',\n];\n"
  },
  {
    "path": "lang/el/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Αυτά τα διαπιστευτήρια δεν ταιριάζουν με τα αρχεία μας.',\n    'throttle' => 'Πάρα πολλές προσπάθειες σύνδεσης. Δοκιμάστε ξανά σε :δευτερόλεπτα.',\n\n    // Login & Register\n    'sign_up' => 'Εγγραφείτε',\n    'log_in' => 'Σύνδεση',\n    'log_in_with' => 'Συνδεθείτε με το :socialDriver',\n    'sign_up_with' => 'Εγγραφείτε με το :socialDriver',\n    'logout' => 'Αποσύνδεση',\n\n    'name' => 'Όνομα',\n    'username' => 'Όνομα χρήστη',\n    'email' => 'Email',\n    'password' => 'Ο κωδικός σας',\n    'password_confirm' => 'Επιβεβαιώστε τον κωδικό σας',\n    'password_hint' => 'Πρέπει να αποτελείται από τουλάχιστον 8 χαρακτήρες',\n    'forgot_password' => 'Ξεχάσατε τον κωδικό σας;',\n    'remember_me' => 'Θυμήσου με',\n    'ldap_email_hint' => 'Εισαγάγετε ένα email για χρήση για αυτόν τον λογαριασμό.',\n    'create_account' => 'Δημιουργήστε λογαριασμό',\n    'already_have_account' => 'Έχετε ήδη λογαριασμό;',\n    'dont_have_account' => 'Δεν έχετε λογαριασμό;',\n    'social_login' => 'Είσοδος με Social MEdia',\n    'social_registration' => 'Εγγραφήμε Social MEdia ',\n    'social_registration_text' => 'Εγγραφείτε και συνδεθείτε χρησιμοποιώντας άλλη υπηρεσία.',\n\n    'register_thanks' => 'Ευχαριστούμε για την εγγραφή!',\n    'register_confirm' => 'Ελέγξτε το email σας και κάντε κλικ στο κουμπί επιβεβαίωσης για πρόσβαση στο :appName.',\n    'registrations_disabled' => 'Οι εγγραφές είναι προς το παρόν απενεργοποιημένες',\n    'registration_email_domain_invalid' => 'Αυτός ο τομέας ηλεκτρονικού ταχυδρομείου δεν έχει πρόσβαση σε αυτήν την εφαρμογή',\n    'register_success' => 'Ευχαριστούμε για την εγγραφή! Είστε πλέον εγγεγραμμένοι και συνδεδεμένοι.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Προσπάθεια Σύνδεσης',\n    'auto_init_starting_desc' => 'We\\'re contacting your authentication system to start the login process. If there\\'s no progress after 5 seconds you can try clicking the link below.',\n    'auto_init_start_link' => 'Proceed with authentication',\n\n    // Password Reset\n    'reset_password' => 'Επαναφορά κωδικού πρόσβασης',\n    'reset_password_send_instructions' => 'Εισαγάγετε τη διεύθυνση του email σας παρακάτω και θα σας σταλεί ένα μήνυμα με έναν σύνδεσμο επαναφοράς του κωδικού πρόσβασης.',\n    'reset_password_send_button' => 'Αποστολή Συνδέσμου Επαναφοράς',\n    'reset_password_sent' => 'Ένας σύνδεσμος επαναφοράς κωδικού πρόσβασης θα αποσταλεί στο :email εάν αυτή η διεύθυνση email βρεθεί στο σύστημα.',\n    'reset_password_success' => 'Ο κωδικός πρόσβασής σας επαναφέρθηκε με επιτυχία.',\n    'email_reset_subject' => 'Επαναφέρετε τον κωδικό πρόσβασης :appName',\n    'email_reset_text' => 'Λαμβάνετε αυτό το μήνυμα ηλεκτρονικού ταχυδρομείου επειδή λάβαμε ένα αίτημα επαναφοράς κωδικού πρόσβασης για τον λογαριασμό σας.',\n    'email_reset_not_requested' => 'Εάν δεν ζητήσατε επαναφορά του κωδικού πρόσβασης, δεν απαιτείται καμία περαιτέρω ενέργεια.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Επιβεβαιώστε το email σας στο :appName',\n    'email_confirm_greeting' => 'Ευχαριστούμε για τη συμμετοχή σας στο :appName!',\n    'email_confirm_text' => 'Επιβεβαιώστε τη διεύθυνση email σας κάνοντας κλικ στο παρακάτω κουμπί:',\n    'email_confirm_action' => 'Επιβεβαίωση διεύθυνσης ηλεκτρονικού ταχυδρομείου',\n    'email_confirm_send_error' => 'Απαιτείται επιβεβαίωση μέσω email, αλλά το σύστημα δεν μπόρεσε να στείλει το email. Επικοινωνήστε με τον διαχειριστή για να βεβαιωθείτε ότι το email έχει ρυθμιστεί σωστά.',\n    'email_confirm_success' => 'Το email σας επιβεβαιώθηκε! Θα πρέπει τώρα να μπορείτε να συνδεθείτε χρησιμοποιώντας αυτήν τη διεύθυνση email.',\n    'email_confirm_resent' => 'Το email επιβεβαίωσης στάλθηκε εκ νέου. Ελέγξτε τα εισερχόμενά σας.',\n    'email_confirm_thanks' => 'Ευχαριστούμε για την επιβεβαίωση!',\n    'email_confirm_thanks_desc' => 'Παρακαλώ περιμένετε λίγο όσο διεκπεραιώνεται η επιβεβαίωσή σας. Εάν δεν ανακατευθυνθείτε μετά από 3 δευτερόλεπτα, πατήστε τον παρακάτω σύνδεσμο \"Συνέχεια\" για να συνεχίσετε.',\n\n    'email_not_confirmed' => 'Η διεύθυνση email δεν επιβεβαιώθηκε',\n    'email_not_confirmed_text' => 'Η διεύθυνση email σας δεν έχει ακόμη επιβεβαιωθεί.',\n    'email_not_confirmed_click_link' => 'Κάντε κλικ στο σύνδεσμο στο email που στάλθηκε λίγο μετά την εγγραφή σας.',\n    'email_not_confirmed_resend' => 'Εάν δεν μπορείτε να βρείτε το email, μπορείτε να στείλετε ξανά το email επιβεβαίωσης υποβάλλοντας την παρακάτω φόρμα.',\n    'email_not_confirmed_resend_button' => 'Ξαναστείλτε μήνυμα επιβεβαίωσης',\n\n    // User Invite\n    'user_invite_email_subject' => 'Έχετε προσκληθεί να συμμετάσχετε :appName!',\n    'user_invite_email_greeting' => 'Έχει δημιουργηθεί ένας λογαριασμός για εσάς στο :appName.',\n    'user_invite_email_text' => 'Κάντε κλικ στο κουμπί παρακάτω για να ορίσετε έναν κωδικό πρόσβασης λογαριασμού και να αποκτήσετε πρόσβαση:',\n    'user_invite_email_action' => 'Ορισμός κωδικού πρόσβασης λογαριασμού',\n    'user_invite_page_welcome' => 'Καλωσόρισες στο :appName!',\n    'user_invite_page_text' => 'Για να οριστικοποιήσετε τον λογαριασμό σας και να αποκτήσετε πρόσβαση, πρέπει να ορίσετε έναν κωδικό πρόσβασης που θα χρησιμοποιείται για να συνδεθείτε στο :appName σε μελλοντικές επισκέψεις.',\n    'user_invite_page_confirm_button' => 'Επιβεβαίωση Κωδικού',\n    'user_invite_success_login' => 'Ορίστηκε κωδικός πρόσβασης, θα πρέπει τώρα να μπορείτε να συνδεθείτε χρησιμοποιώντας τον καθορισμένο κωδικό πρόσβασης για να αποκτήσετε πρόσβαση στο :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Ρύθμιση ελέγχου ταυτότητας πολλαπλών παραγόντων',\n    'mfa_setup_desc' => 'Ρυθμίστε τον έλεγχο ταυτότητας πολλαπλών παραγόντων ως ένα επιπλέον επίπεδο ασφάλειας για τον λογαριασμό χρήστη σας.',\n    'mfa_setup_configured' => 'Έχει ήδη διαμορφωθεί',\n    'mfa_setup_reconfigure' => 'Επαναδιαμόρφωση',\n    'mfa_setup_remove_confirmation' => 'Είστε βέβαιοι ότι θέλετε να καταργήσετε αυτήν τη μέθοδο ελέγχου ταυτότητας πολλαπλών παραγόντων;',\n    'mfa_setup_action' => 'Ρύθμιση',\n    'mfa_backup_codes_usage_limit_warning' => 'Έχετε λιγότερους από 5 εφεδρικούς κωδικούς που απομένουν. Δημιουργήστε και αποθηκεύστε ένα νέο σύνολο προτού εξαντληθούν οι κωδικοί για να αποφύγετε τον αποκλεισμό του λογαριασμού σας.',\n    'mfa_option_totp_title' => 'Εφαρμογή για κινητό',\n    'mfa_option_totp_desc' => 'Για να χρησιμοποιήσετε τον έλεγχο ταυτότητας πολλαπλών παραγόντων, θα χρειαστείτε μια εφαρμογή για κινητά που υποστηρίζει TOTP, όπως το Google Authenticator, το Authy ή το Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Εφεδρικοί κωδικοί',\n    'mfa_option_backup_codes_desc' => 'Δημιουργεί ένα σύνολο εφεδρικών κωδικών μίας χρήσης τους οποίους θα εισάγετε κατά τη σύνδεση σας, για να πιστοποιήσετε την ταυτότητά σας. Φροντίστε να τους αποθηκεύσετε σε ασφαλές σημείο.',\n    'mfa_gen_confirm_and_enable' => 'Επιβεβαίωση και ενεργοποίηση',\n    'mfa_gen_backup_codes_title' => 'Ρύθμιση εφεδρικών κωδικών',\n    'mfa_gen_backup_codes_desc' => 'Αποθηκεύστε την παρακάτω λίστα κωδικών σε ασφαλές μέρος. Κατά την πρόσβαση στο σύστημα, θα μπορείτε να χρησιμοποιήσετε έναν από τους κωδικούς ως δεύτερο μηχανισμό ελέγχου ταυτότητας.',\n    'mfa_gen_backup_codes_download' => 'Λήψη κωδικών',\n    'mfa_gen_backup_codes_usage_warning' => 'Κάθε κωδικός μπορεί να χρησιμοποιηθεί μόνο μία φορά',\n    'mfa_gen_totp_title' => 'Ρύθμιση εφαρμογής για κινητά',\n    'mfa_gen_totp_desc' => 'Για να χρησιμοποιήσετε τον έλεγχο ταυτότητας πολλαπλών παραγόντων, θα χρειαστείτε μια εφαρμογή για κινητά που υποστηρίζει TOTP, όπως το Google Authenticator, το Authy ή το Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Σαρώστε τον παρακάτω κωδικό QR χρησιμοποιώντας την προτιμώμενη εφαρμογή ελέγχου ταυτότητας για να ξεκινήσετε.',\n    'mfa_gen_totp_verify_setup' => 'Επαληθεύστε τη ρύθμιση',\n    'mfa_gen_totp_verify_setup_desc' => 'Επαληθεύστε ότι όλα λειτουργούν εισάγοντας έναν κωδικό, που δημιουργήθηκε στην εφαρμογή ελέγχου ταυτότητας, στο παρακάτω πλαίσιο εισαγωγής:',\n    'mfa_gen_totp_provide_code_here' => 'Εισάγετε τον κώδικα που δημιουργήθηκε από την εφαρμογή σας εδώ',\n    'mfa_verify_access' => 'Επαλήθευση πρόσβασης',\n    'mfa_verify_access_desc' => 'Ο λογαριασμός σας απαιτεί να επιβεβαιώσετε την ταυτότητά σας μέσω ενός πρόσθετου επιπέδου επαλήθευσης προτού σας παραχωρηθεί πρόσβαση. Επαληθεύστε χρησιμοποιώντας μία από τις διαμορφωμένες μεθόδους σας για να συνεχίσετε.',\n    'mfa_verify_no_methods' => 'Δεν έχουν διαμορφωθεί μέθοδοι.',\n    'mfa_verify_no_methods_desc' => 'Δεν βρέθηκαν μέθοδοι ελέγχου ταυτότητας πολλαπλών παραγόντων για τον λογαριασμό σας. Θα χρειαστεί να ρυθμίσετε τουλάχιστον μία μέθοδο προτού αποκτήσετε πρόσβαση.',\n    'mfa_verify_use_totp' => 'Επαληθεύστε χρησιμοποιώντας μια εφαρμογή για κινητά',\n    'mfa_verify_use_backup_codes' => 'Επαληθεύστε χρησιμοποιώντας έναν εφεδρικό κωδικό',\n    'mfa_verify_backup_code' => 'Εφεδρικός κωδικός',\n    'mfa_verify_backup_code_desc' => 'Εισαγάγετε έναν από τους υπόλοιπους εφεδρικούς κωδικούς σας παρακάτω:',\n    'mfa_verify_backup_code_enter_here' => 'Εισαγάγετε τον εφεδρικό κωδικό εδώ:',\n    'mfa_verify_totp_desc' => 'Εισαγάγετε τον κωδικό, που δημιουργήθηκε χρησιμοποιώντας την εφαρμογή σας για κινητά, παρακάτω:',\n    'mfa_setup_login_notification' => 'Η μέθοδος πολλαπλών παραγόντων έχει διαμορφωθεί. Συνδεθείτε ξανά χρησιμοποιώντας τη ρυθμισμένη μέθοδο.',\n];\n"
  },
  {
    "path": "lang/el/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Ακύρωση',\n    'close' => 'Κλείσιμο',\n    'confirm' => 'Οκ',\n    'back' => 'Πίσω',\n    'save' => 'Αποθήκευση',\n    'continue' => 'Συνέχεια',\n    'select' => 'Επιλογή',\n    'toggle_all' => 'Εναλλαγή όλων',\n    'more' => 'Περισσότερα..',\n\n    // Form Labels\n    'name' => 'Όνομα',\n    'description' => 'Περιγραφή',\n    'role' => 'Ρόλος',\n    'cover_image' => 'Εικόνα εξώφυλλου',\n    'cover_image_description' => 'Αυτή η εικόνα θα πρέπει να είναι περίπου 440x250px, αν και θα κλιμακωθεί και θα περικοπεί με ευελιξία ώστε να ταιριάζει στη διεπαφή χρήστη σε διαφορετικά σενάρια όπως απαιτείται, επομένως οι πραγματικές διαστάσεις για την εμφάνιση θα διαφέρουν.',\n\n    // Actions\n    'actions' => 'Ενέργειες',\n    'view' => 'Προβολή',\n    'view_all' => 'Προβολή όλων',\n    'new' => 'Νέο',\n    'create' => 'Δημιουργία',\n    'update' => 'Ενημέρωση',\n    'edit' => 'Επεξεργασία',\n    'archive' => 'Archive',\n    'unarchive' => 'Un-Archive',\n    'sort' => 'Ταξινόμηση',\n    'move' => 'Μετακίνηση',\n    'copy' => 'Αντιγραφή',\n    'reply' => 'Απάντηση',\n    'delete' => 'Διαγραφή',\n    'delete_confirm' => 'Επιβεβαίωση Διαγραφής',\n    'search' => 'Αναζήτηση',\n    'search_clear' => 'Εκκαθάριση Αναζήτησης',\n    'reset' => 'Επαναφορά',\n    'remove' => 'Αφαίρεση',\n    'add' => 'Προσθήκη',\n    'configure' => 'Διαμόρφωση',\n    'manage' => 'Διαχείριση',\n    'fullscreen' => 'Πλήρης οθόνη',\n    'favourite' => 'Αγαπημένα',\n    'unfavourite' => 'Αφαίρεση από Αγαπημένα',\n    'next' => 'Επόμενη',\n    'previous' => 'Προηγούμενη',\n    'filter_active' => 'Ενεργό φίλτρο:',\n    'filter_clear' => 'Διαγραφή φίλτρου',\n    'download' => 'Λήψη',\n    'open_in_tab' => 'Άνοιγμα σε Καρτέλα',\n    'open' => 'Άνοιγμα',\n\n    // Sort Options\n    'sort_options' => 'Επιλογές ταξινόμησης',\n    'sort_direction_toggle' => 'Εναλλαγή κατεύθυνσης ταξινόμησης',\n    'sort_ascending' => 'Αύξουσα ταξινόμηση',\n    'sort_descending' => 'Ταξινόμηση Φθίνουσα',\n    'sort_name' => 'Ονομα',\n    'sort_default' => 'Προεπιλογή',\n    'sort_created_at' => 'Δημιουργήθηκε',\n    'sort_updated_at' => 'Ενημερώθηκε',\n\n    // Misc\n    'deleted_user' => 'Διαγραμμένος χρήστης',\n    'no_activity' => 'Δεν υπάρχει δραστηριότητα προς εμφάνιση',\n    'no_items' => 'Δεν υπάρχουν διαθέσιμα στοιχεία',\n    'back_to_top' => 'Επιστροφή στην κορυφή',\n    'skip_to_main_content' => 'Μετάβαση στο κύριο περιεχόμενο',\n    'toggle_details' => 'Εναλλαγή λεπτομερειών',\n    'toggle_thumbnails' => 'Εναλλαγή μικρογραφιών',\n    'details' => 'Λεπτομέριες',\n    'grid_view' => 'Προβολή σε πλέγμα',\n    'list_view' => 'Προβολή σε λίστα',\n    'default' => 'Προκαθορισμένο',\n    'breadcrumb' => 'Μπάρα πλοήγησης',\n    'status' => 'Κατάσταση',\n    'status_active' => 'Ενεργός',\n    'status_inactive' => 'Αδρανής',\n    'never' => 'Ποτέ',\n    'none' => 'Κανένας',\n\n    // Header\n    'homepage' => 'Αρχική σελίδα',\n    'header_menu_expand' => 'Αναπτύξτε το Head Menu',\n    'profile_menu' => 'Μενού Προφίλ',\n    'view_profile' => 'Προβολή προφίλ',\n    'edit_profile' => 'Επεξεργασία προφίλ',\n    'dark_mode' => 'Σκουρόχρωμη εμφάνιση',\n    'light_mode' => 'Ανοιχτόχρωμη εμφάνιση',\n    'global_search' => 'Καθολική αναζήτηση',\n\n    // Layout tabs\n    'tab_info' => 'Πληροφορίες',\n    'tab_info_label' => 'Καρτέλα: Εμφάνιση δευτερευουσών πληροφοριών',\n    'tab_content' => 'Περιεχόμενο',\n    'tab_content_label' => 'Καρτέλα: Εμφάνιση κύριου περιεχομένου',\n\n    // Email Content\n    'email_action_help' => 'Εάν αντιμετωπίζετε πρόβλημα κάνοντας κλικ στο κουμπί \":actionText\", αντιγράψτε και επικολλήστε την παρακάτω διεύθυνση URL στο πρόγραμμα περιήγησής σας στον ιστό:',\n    'email_rights' => 'Ολα τα πνευματικά δικαιώματα διατηρούνται',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Πολιτική Απορρήτου',\n    'terms_of_service' => 'Όροι χρήσης',\n\n    // OpenSearch\n    'opensearch_description' => 'Αναζήτηση :appName',\n];\n"
  },
  {
    "path": "lang/el/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Επιλογή εικόνας',\n    'image_list' => 'Κατάλογος εικόνων',\n    'image_details' => 'Λεπτομέρειες εικόνας',\n    'image_upload' => 'Μεταφόρτωση εικόνας',\n    'image_intro' => 'Εδώ μπορείτε να επιλέξετε και να διαχειριστείτε εικόνες που έχουν προηγουμένως μεταφορτωθεί στο σύστημα.',\n    'image_intro_upload' => 'Μεταφορτώστε μια νέα εικόνα σύροντας ένα αρχείο εικόνας σε αυτό το παράθυρο ή χρησιμοποιώντας το κουμπί \"Μεταφόρτωση εικόνας\" παραπάνω.',\n    'image_all' => 'Όλες',\n    'image_all_title' => 'Δείτε όλες τις εικόνες που υπάρχουν στο Server',\n    'image_book_title' => 'Προβολή εικόνων που έχουν μεταφορτωθεί σε αυτό το βιβλίο',\n    'image_page_title' => 'Προβολή εικόνων που έχουν δημοσιευτεί σε αυτήν τη σελίδα',\n    'image_search_hint' => 'Αναζήτηση με όνομα εικόνας',\n    'image_uploaded' => 'Μεταφορτώθηκε :uploadedDate',\n    'image_uploaded_by' => 'Μεταφορτώθηκε από :userName',\n    'image_uploaded_to' => 'Μεταφορτώθηκε στο :pageLink',\n    'image_updated' => 'Ενημερώθηκε :updateDate',\n    'image_load_more' => 'Φόρτωσε περισσότερα',\n    'image_image_name' => 'Όνομα εικόνας',\n    'image_delete_used' => 'Αυτή η εικόνα χρησιμοποιείται στις παρακάτω σελίδες.',\n    'image_delete_confirm_text' => 'Είστε σίγουροι ότι θέλετε να διαγράψετε αυτήν την εικόνα;',\n    'image_select_image' => 'Επιλέξτε Εικόνα',\n    'image_dropzone' => 'Σύρτε ή κάντε κλικ εδώ για μεταφόρτωση εικόνων',\n    'image_dropzone_drop' => 'Απόθεση εικόνων εδώ για μεταφόρτωση',\n    'images_deleted' => 'Οι εικόνες διαγράφηκαν',\n    'image_preview' => 'Προεπισκόπηση εικόνας',\n    'image_upload_success' => 'Η εικόνα μεταφορτώθηκε με επιτυχία',\n    'image_update_success' => 'Τα στοιχεία της εικόνας ενημερώθηκαν με επιτυχία',\n    'image_delete_success' => 'Η εικόνα διαγράφηκε επιτυχώς',\n    'image_replace' => 'Αντικατάσταση εικόνας',\n    'image_replace_success' => 'Το αρχείο εικόνας ενημερώθηκε επιτυχώς',\n    'image_rebuild_thumbs' => 'Επαναδημιουργία παραλλαγών μεγέθους',\n    'image_rebuild_thumbs_success' => 'Οι παραλλαγές μεγέθους εικόνας ανακατασκευάστηκαν επιτυχώς!',\n\n    // Code Editor\n    'code_editor' => 'Επεξεργασία κώδικα',\n    'code_language' => 'Γλώσσα κώδικα',\n    'code_content' => 'Περιεχόμενο κώδικα',\n    'code_session_history' => 'Ιστορικό συνεδρίας',\n    'code_save' => 'Αποθήκευση Κώδικα',\n];\n"
  },
  {
    "path": "lang/el/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Γενικά',\n    'advanced' => 'Για Προχωρημένους',\n    'none' => 'None',\n    'cancel' => 'Ακύρωση',\n    'save' => 'Αποθήκευση',\n    'close' => 'Κλείσιμο',\n    'apply' => 'Apply',\n    'undo' => 'Αναίρεση',\n    'redo' => 'Επανάληψη',\n    'left' => 'Αριστερά',\n    'center' => 'Κέντρο',\n    'right' => 'Δεξιά',\n    'top' => 'Πάνω',\n    'middle' => 'Κέντρο',\n    'bottom' => 'Κάτω',\n    'width' => 'Πλάτος',\n    'height' => 'Ύψος',\n    'More' => 'Περισσότερα',\n    'select' => 'Επιλέξτε...',\n\n    // Toolbar\n    'formats' => 'Μορφοποίηση',\n    'header_large' => 'Μεγάλη κεφαλίδα',\n    'header_medium' => 'Μεσαία κεφαλίδα',\n    'header_small' => 'Μικρή κεφαλίδα',\n    'header_tiny' => 'Μικροσκοπική κεφαλίδα',\n    'paragraph' => 'Παράγραφος',\n    'blockquote' => 'Μπλοκ κειμένου παράθεσης',\n    'inline_code' => 'Ενσωματωμένος κωδικός',\n    'callouts' => 'Επεξηγήσεις',\n    'callout_information' => 'Πληροφορίες',\n    'callout_success' => 'Επιτυχία',\n    'callout_warning' => 'Προειδοποίηση',\n    'callout_danger' => 'Κίνδυνος',\n    'bold' => 'Έντονη γραφή',\n    'italic' => 'Πλάγια γραφή',\n    'underline' => 'Υπογράμμιση',\n    'strikethrough' => 'Διακριτή διαγραφή',\n    'superscript' => 'Εκθέτης',\n    'subscript' => 'Δείκτης',\n    'text_color' => 'Χρώμα κειμένου',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Προσαρμογή χρώματος',\n    'remove_color' => 'Αφαίρεση χρώματος',\n    'background_color' => 'Χρώμα φόντου',\n    'align_left' => 'Στοίχιση αριστερά',\n    'align_center' => 'Στοίχιση κέντρο',\n    'align_right' => 'Στοίχιση δεξιά',\n    'align_justify' => 'Πλήρης στοίχιση',\n    'list_bullet' => 'Λίστα με κουκκίδες',\n    'list_numbered' => 'Λίστα με αρίθμηση',\n    'list_task' => 'Λίστα εργασιών',\n    'indent_increase' => 'Αύξηση Εσοχής',\n    'indent_decrease' => 'Μείωση εσοχής',\n    'table' => 'Πίνακας',\n    'insert_image' => 'Εισαγωγή εικόνας',\n    'insert_image_title' => 'Εισαγωγή/Επεξεργασία εικόνας',\n    'insert_link' => 'Εισαγωγή/επεξεργασία συνδέσμου',\n    'insert_link_title' => 'Εισαγωγή/Επεξεργασία συνδέσμου',\n    'insert_horizontal_line' => 'Εισαγωγή οριζόντιας γραμμής',\n    'insert_code_block' => 'Εισαγωγή μπλοκ κώδικα',\n    'edit_code_block' => 'Επεξεργασία μπλοκ κώδικα',\n    'insert_drawing' => 'Εισαγωγή/Επεξεργασία σχεδίου',\n    'drawing_manager' => 'Διαχειριστής σχεδίασης',\n    'insert_media' => 'Εισαγωγή/Επεξεργασία πολυμέσων',\n    'insert_media_title' => 'Εισαγωγή/Επεξεργασία πολυμέσων',\n    'clear_formatting' => 'Διαγραφή μορφοποίησης',\n    'source_code' => 'Πηγαίος κώδικας',\n    'source_code_title' => 'Πηγαίος κώδικας',\n    'fullscreen' => 'Πλήρης οθόνη',\n    'image_options' => 'Επιλογές εικόνας',\n\n    // Tables\n    'table_properties' => 'Ιδιότητες πίνακα',\n    'table_properties_title' => 'Ιδιότητες πίνακα',\n    'delete_table' => 'Διαγραφή πίνακα',\n    'table_clear_formatting' => 'Εκκαθάριση μορφοποίησης πίνακα',\n    'resize_to_contents' => 'Αλλαγή μεγέθους σε περιεχόμενο',\n    'row_header' => 'Κεφαλίδα γραμμής',\n    'insert_row_before' => 'Εισαγωγή γραμμής πάνω',\n    'insert_row_after' => 'Εισαγωγή γραμμής κάτω',\n    'delete_row' => 'Διαγραφή γραμμής',\n    'insert_column_before' => 'Εισαγωγή στήλης αριστερά',\n    'insert_column_after' => 'Εισαγωγή στήλης δεξιά',\n    'delete_column' => 'Διαγραφή στήλης',\n    'table_cell' => 'Κελί',\n    'table_row' => 'Γραμμή',\n    'table_column' => 'Στήλη',\n    'cell_properties' => 'Ιδιότητες κελιού',\n    'cell_properties_title' => 'Ιδιότητες κελιού',\n    'cell_type' => 'Τύπος κελιού',\n    'cell_type_cell' => 'Κελί',\n    'cell_scope' => 'Scope',\n    'cell_type_header' => 'Κεφαλίδα κελιού',\n    'merge_cells' => 'Συγχώνευση κελιών',\n    'split_cell' => 'Διαίρεση κελιού',\n    'table_row_group' => 'Ομάδα γραμμών',\n    'table_column_group' => 'Ομάδα στηλών',\n    'horizontal_align' => 'Οριζόντια στοίχιση',\n    'vertical_align' => 'Κάθετη στοίχιση',\n    'border_width' => 'Πάχος περιγράμματος',\n    'border_style' => 'Στυλ περιγράμματος',\n    'border_color' => 'Χρώμα περιγράμματος',\n    'row_properties' => 'Ιδιότητες γραμμής',\n    'row_properties_title' => 'Ιδιότητες γραμμής',\n    'cut_row' => 'Αποκοπή γραμμής',\n    'copy_row' => 'Αντιγραφή γραμμής',\n    'paste_row_before' => 'Επικόλληση γραμμής πάνω',\n    'paste_row_after' => 'Επικόλληση γραμμής κάτω',\n    'row_type' => 'Τύπος γραμμής',\n    'row_type_header' => 'Κεφαλίδα',\n    'row_type_body' => 'Σώμα',\n    'row_type_footer' => 'Υποσέλιδο',\n    'alignment' => 'Ευθυγράμμιση',\n    'cut_column' => 'Αποκοπή στήλης',\n    'copy_column' => 'Αντιγραφή στήλης',\n    'paste_column_before' => 'Επικόλληση στήλης αριστερά',\n    'paste_column_after' => 'Επικόλληση στήλης δεξιά',\n    'cell_padding' => 'Περιθώριο κελιών',\n    'cell_spacing' => 'Απόσταση κελιών',\n    'caption' => 'Τίτλος',\n    'show_caption' => 'Εμφάνιση Τίτλου',\n    'constrain' => 'Περιορισμός αναλογιών',\n    'cell_border_solid' => 'Συμπαγής γραμμή',\n    'cell_border_dotted' => 'Γραμμή με κουκκίδες',\n    'cell_border_dashed' => 'Διακεκομμένη γραμμή',\n    'cell_border_double' => 'Διπλή γραμμή',\n    'cell_border_groove' => 'Groove',\n    'cell_border_ridge' => 'Κορυφογραμμή',\n    'cell_border_inset' => 'Inset',\n    'cell_border_outset' => 'Outset',\n    'cell_border_none' => 'Χωρίς',\n    'cell_border_hidden' => '΄Διαφανές',\n\n    // Images, links, details/summary & embed\n    'source' => 'Source',\n    'alt_desc' => 'Εναλλακτική περιγραφή',\n    'embed' => 'Ενσωματωμένο',\n    'paste_embed' => 'Επικολλήστε τον κώδικα ενσωμάτωσης παρακάτω:',\n    'url' => 'URL',\n    'text_to_display' => 'Κείμενο εμφάνισης',\n    'title' => 'Τίτλος',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Άνοιγμα συνδέσμου',\n    'open_link_in' => 'Άνοιγμα συνδέσμου σε...',\n    'open_link_current' => 'Τρέχον παράθυρο',\n    'open_link_new' => 'Νέο παράθυρο',\n    'remove_link' => 'Αφαίρεση συνδέσμου',\n    'insert_collapsible' => 'Εισαγωγή πτυσσόμενου μπλοκ',\n    'collapsible_unwrap' => 'Μετατροπή πτυσσόμενου μπλοκ σε παράγραφο',\n    'edit_label' => 'Επεξεργασία ετικέτας',\n    'toggle_open_closed' => 'Εναλλαγή ανοίγματος/κλεισίματος',\n    'collapsible_edit' => 'Επεξεργασία πτυσσόμενου μπλοκ',\n    'toggle_label' => 'Εναλλαγή ετικέτας',\n\n    // About view\n    'about' => 'Σχετικά',\n    'about_title' => 'Σχετικά με τον επεξεργαστή WYSIWYG',\n    'editor_license' => 'Άδεια εκδότη και πνευματικά δικαιώματα',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'Αυτός ο επεξεργαστής έχει δημιουργηθεί χρησιμοποιώντας :tinyLink που παρέχεται με την άδεια MIT.',\n    'editor_tiny_license_link' => 'Τα πνευματικά δικαιώματα και τα στοιχεία άδειας χρήσης του TinyMCE μπορείτε να τα βρείτε εδώ.',\n    'save_continue' => 'Αποθήκευση σελίδας & Συνέχεια',\n    'callouts_cycle' => '(Συνεχίστε να πατάτε για εναλλαγή μεταξύ τύπων)',\n    'link_selector' => 'Σύνδεσμος προς το περιεχόμενο',\n    'shortcuts' => 'Συντομεύσεις',\n    'shortcut' => 'Συντόμευση',\n    'shortcuts_intro' => 'Οι ακόλουθες συντομεύσεις είναι διαθέσιμες στο πρόγραμμα επεξεργασίας:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Περιγραφή',\n];\n"
  },
  {
    "path": "lang/el/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Δημιουργήθηκε Πρόσφατα',\n    'recently_created_pages' => 'Πρόσφατα Δημιουργημένες Σελίδες',\n    'recently_updated_pages' => 'Πρόσφατες Ενημερώσεις',\n    'recently_created_chapters' => 'Πρόσφατα Δημιουργημένα Κεφάλαια',\n    'recently_created_books' => 'Πρόσφατα Δημιουργημένα Βιβλία',\n    'recently_created_shelves' => 'Πρόσφατα Δημιουργημένα Ράφια',\n    'recently_update' => 'Ενημερώθηκε πρόσφατα',\n    'recently_viewed' => 'Πρόσφατα προβεβλημένα',\n    'recent_activity' => 'Πρόσφατη Δραστηριότητα',\n    'create_now' => 'Δημιουργία ενός τώρα',\n    'revisions' => 'Αναθεωρήσεις',\n    'meta_revision' => 'Αναθεώρηση #:revisionCount',\n    'meta_created' => 'Δημιουργήθηκε :timeLength',\n    'meta_created_name' => 'Δημιουργήθηκε :timeLength από :user',\n    'meta_updated' => 'Ενημερώθηκε :timeLength',\n    'meta_updated_name' => 'Ενημερώθηκε :timeLength από :user',\n    'meta_owned_name' => 'Ανήκει στον :user',\n    'meta_reference_count' => 'Referenced by :count item|Referenced by :count items',\n    'entity_select' => 'Επιλογή Οντότητας',\n    'entity_select_lack_permission' => 'Δεν έχετε τα απαιτούμενα δικαιώματα για να επιλέξετε αυτό το στοιχείο',\n    'images' => 'Εικόνες',\n    'my_recent_drafts' => 'Τα πρόσφατα προσχέδιά μου',\n    'my_recently_viewed' => 'Είδα πρόσφατα',\n    'my_most_viewed_favourites' => 'Συχνά Αγαπημένα',\n    'my_favourites' => 'Τα αγαπημένα μου',\n    'no_pages_viewed' => 'Δεν έχετε δει καμία σελίδα',\n    'no_pages_recently_created' => 'Δεν έχουν δημιουργηθεί πρόσφατα σελίδες',\n    'no_pages_recently_updated' => 'Δεν υπάρχουν πρόσφατα ενημερώσεις σελίδων',\n    'export' => 'Εξαγωγή',\n    'export_html' => 'Αρχείο Web',\n    'export_pdf' => 'Αρχείο PDF',\n    'export_text' => 'Αρχείο Απλού κειμένου',\n    'export_md' => 'Αρχείο Markdown',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Default Page Template',\n    'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',\n    'default_template_select' => 'Select a template page',\n    'import' => 'Import',\n    'import_validate' => 'Validate Import',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'Select ZIP file to upload',\n    'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',\n    'import_pending' => 'Pending Imports',\n    'import_pending_none' => 'No imports have been started.',\n    'import_continue' => 'Continue Import',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Run Import',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Uploaded by',\n    'import_location' => 'Import Location',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'Import Errors',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Δικαιώματα',\n    'permissions_desc' => 'Ορίστε εδώ δικαιώματα για να παρακάμψετε τα προκαθορισμένα δικαιώματα που παρέχονται από τους ρόλους των χρηστών.',\n    'permissions_book_cascade' => 'Τα δικαιώματα που έχουν οριστεί στα Βιβλία θα κλιμακώνονται αυτόματα στα θυγατρικά Κεφάλαια και Σελίδες, εκτός εάν έχουν καθοριστεί διαφορετικά σε αυτά.',\n    'permissions_chapter_cascade' => 'Τα δικαιώματα που έχουν οριστεί στα Κεφάλαια θα κλιμακώνονται αυτόματα στις θυγατρικές Σελίδες, εκτός εάν έχουν καθοριστεί διαφορετικά σε αυτές.',\n    'permissions_save' => 'Αποθήκευση Δικαιωμάτων',\n    'permissions_owner' => 'Ιδιοκτήτης / Κάτοχος',\n    'permissions_role_everyone_else' => 'Όλοι Οι Άλλοι',\n    'permissions_role_everyone_else_desc' => 'Ορίστε δικαιώματα για όλους τους ρόλους που δεν παραβλέπονται συγκεκριμένα.',\n    'permissions_role_override' => 'Παράκαμψη δικαιωμάτων για ρόλο',\n    'permissions_inherit_defaults' => 'Κληρονόμηση προεπιλογών',\n\n    // Search\n    'search_results' => 'Αποτελέσματα αναζήτησης',\n    'search_total_results_found' => 'Βρέθηκε :count αποτέλεσμα|Βρέθηκαν συνολικά :count αποτελέσματα',\n    'search_clear' => 'Καθαρισμός αναζήτησης',\n    'search_no_pages' => 'Καμία σελίδα δεν ταιριάζει με αυτήν την αναζήτηση',\n    'search_for_term' => 'Αναζήτηση για :term',\n    'search_more' => 'Περισσότερα αποτελέσματα',\n    'search_advanced' => 'Προχωρημένη Αναζήτηση',\n    'search_terms' => 'Αναζήτηση Όρων',\n    'search_content_type' => 'Τύπος περιεχομένου',\n    'search_exact_matches' => 'Ακριβείς αντιστοιχίες',\n    'search_tags' => 'Αναζητήσεις Ετικετών',\n    'search_options' => 'Επιλογές',\n    'search_viewed_by_me' => 'Προβλήθηκε από εμένα',\n    'search_not_viewed_by_me' => 'Δεν προβλήθηκε από εμένα',\n    'search_permissions_set' => 'Τα δικαιώματα ορίστηκαν',\n    'search_created_by_me' => 'Δημιουργήθηκε από εμένα',\n    'search_updated_by_me' => 'Ενημερώθηκε από εμένα',\n    'search_owned_by_me' => 'Ανήκει σε μένα',\n    'search_date_options' => 'Επιλογές Ημερομηνίας',\n    'search_updated_before' => 'Ενημερώθηκε πριν',\n    'search_updated_after' => 'Ενημερώθηκε μετά',\n    'search_created_before' => 'Δημιουργήθηκε πριν',\n    'search_created_after' => 'Δημιουργήθηκε μετά',\n    'search_set_date' => 'Ορισμός Ημερομηνίας',\n    'search_update' => 'Ενημέρωση Αναζήτησης',\n\n    // Shelves\n    'shelf' => 'Ράφι',\n    'shelves' => 'Ράφια',\n    'x_shelves' => ':count Ράφι|:count Ράφια',\n    'shelves_empty' => 'Δεν έχουν δημιουργηθεί ράφια',\n    'shelves_create' => 'Δημιουργία νέου ραφιού',\n    'shelves_popular' => 'Δημοφιλή Ράφια',\n    'shelves_new' => 'Νέα Ράφια',\n    'shelves_new_action' => 'Νέο Ράφι',\n    'shelves_popular_empty' => 'Τα πιο δημοφιλή ράφια θα εμφανιστούν εδώ.',\n    'shelves_new_empty' => 'Τα πιο πρόσφατα ράφια που δημιουργήθηκαν θα εμφανιστούν εδώ.',\n    'shelves_save' => 'Αποθήκευση Ραφιού',\n    'shelves_books' => 'Βιβλία σε αυτό το Ράφι',\n    'shelves_add_books' => 'Διαθέσιμα Βιβλία, για προσθήκη στο Ράφι',\n    'shelves_drag_books' => 'Σύρετε εδώ βιβλία της διπλανή λίστας, για να τα προσθέσετε στο Ράφι',\n    'shelves_empty_contents' => 'Σε αυτό το Ράφι δεν έχουν εκχωρηθεί βιβλία',\n    'shelves_edit_and_assign' => 'Επεξεργαστείτε το Ράφι για να εκχωρήσετε βιβλία',\n    'shelves_edit_named' => 'Επεξεργασία Ραφιού :name',\n    'shelves_edit' => 'Επεξεργασία Ραφιού',\n    'shelves_delete' => 'Διαγραφή Ραφιού',\n    'shelves_delete_named' => 'Delete Bookshelf :name',\n    'shelves_delete_explain' => \"This will delete the bookshelf with the name ':name'. Contained books will not be deleted.\",\n    'shelves_delete_confirmation' => 'Are you sure you want to delete this bookshelf?',\n    'shelves_permissions' => 'Bookshelf Permissions',\n    'shelves_permissions_updated' => 'Bookshelf Permissions Updated',\n    'shelves_permissions_active' => 'Bookshelf Permissions Active',\n    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',\n    'shelves_permissions_create' => 'Shelf create permissions are only used for copying permissions to child books using the action below. They do not control the ability to create books.',\n    'shelves_copy_permissions_to_books' => 'Αντιγραφή δικαιωμάτων στα βιβλία',\n    'shelves_copy_permissions' => 'Αντιγραφή Δικαιωμάτων',\n    'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this bookshelf to all books contained within. Before activating, ensure any changes to the permissions of this bookshelf have been saved.',\n    'shelves_copy_permission_success' => 'Bookshelf permissions copied to :count books',\n\n    // Books\n    'book' => 'Βιβλίο',\n    'books' => 'Βιβλία',\n    'x_books' => ':count Βιβλίο|:count Βιβλία',\n    'books_empty' => 'Δεν έχουν δημιουργηθεί βιβλία ακόμα',\n    'books_popular' => 'Δημοφιλή Βιβλία',\n    'books_recent' => 'Πρόσφατα Βιβλία',\n    'books_new' => 'Νέα Βιβλία',\n    'books_new_action' => 'Νέο βιβλίο',\n    'books_popular_empty' => 'Τα πιο δημοφιλή εμφανίζονται εδώ.',\n    'books_new_empty' => 'Θα εμφανιστούν εδώ, αυτά που δημιουργήθηκαν πιο πρόσφατα.',\n    'books_create' => 'Δημιουργία νέου βιβλίου',\n    'books_delete' => 'Διαγραφή Βιβλίου',\n    'books_delete_named' => 'Διαγραφή Βιβλίου :bookname',\n    'books_delete_explain' => 'Αυτό θα διαγράψει το βιβλίο με το όνομα \\':bookName\\'. Όλες οι σελίδες και τα κεφάλαια θα αφαιρεθούν.',\n    'books_delete_confirmation' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το βιβλίο;',\n    'books_edit' => 'Επεξεργασία Βιβλίου',\n    'books_edit_named' => 'Επεξεργασία Βιβλίου :bookname',\n    'books_form_book_name' => 'Όνομα Βιβλίου',\n    'books_save' => 'Αποθήκευση Βιβλίου',\n    'books_permissions' => 'Άδειες Βιβλίου',\n    'books_permissions_updated' => 'Τα Δικαιώματα Βιβλίου Ενημερώθηκαν',\n    'books_empty_contents' => 'Δεν έχουν δημιουργηθεί σελίδες ή κεφάλαια για αυτό το βιβλίο.',\n    'books_empty_create_page' => 'Δημιουργία νέας σελίδας',\n    'books_empty_sort_current_book' => 'Ταξινόμηση του τρέχοντος βιβλίου',\n    'books_empty_add_chapter' => 'Προσθήκη κεφαλαίου',\n    'books_permissions_active' => 'Ενεργά Δικαιώματα ´Βιβλίου',\n    'books_search_this' => 'Αναζήτηση σε αυτό το βιβλίο',\n    'books_navigation' => 'Πλοήγηση Βιβλίου',\n    'books_sort' => 'Ταξινόμηση Περιεχομένων Βιβλίου',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Ταξινόμηση Βιβλίου :bookname',\n    'books_sort_name' => 'Ταξινόμηση κατά όνομα',\n    'books_sort_created' => 'Ταξινόμηση κατά ημερομηνία δημιουργίας',\n    'books_sort_updated' => 'Ταξινόμηση κατά ημερομηνία ενημέρωσης',\n    'books_sort_chapters_first' => 'Τα Κεφάλαια Πρώτα',\n    'books_sort_chapters_last' => 'Τελευταία Κεφάλαια',\n    'books_sort_show_other' => 'Εμφάνιση Άλλων Βιβλίων',\n    'books_sort_save' => 'Αποθήκευση Νέας Ταξινόμησης',\n    'books_sort_show_other_desc' => 'Προσθέστε άλλα βιβλία εδώ για να τα συμπεριλάβετε στην ταξινόμηση και να επιτρέψετε την εύκολη αναδιοργάνωση μεταξύ των βιβλίων.',\n    'books_sort_move_up' => 'Μετακίνηση προς τα Επάνω',\n    'books_sort_move_down' => 'Μετακίνηση προς τα Κάτω',\n    'books_sort_move_prev_book' => 'Μετακίνηση στο προηγούμενο Βιβλίο',\n    'books_sort_move_next_book' => 'Μετακίνηση στο επόμενο Βιβλίο',\n    'books_sort_move_prev_chapter' => 'Μετακίνηση στο προηγούμενο Κεφάλαιο',\n    'books_sort_move_next_chapter' => 'Μετακίνηση στο επόμενο Κεφάλαιο',\n    'books_sort_move_book_start' => 'Μετακίνηση στην Αρχή του Βιβλίου',\n    'books_sort_move_book_end' => 'Μετακίνηση στο Τέλος του Βιβλίου',\n    'books_sort_move_before_chapter' => 'Μετακίνηση στο προηγούμενο Κεφάλαιο',\n    'books_sort_move_after_chapter' => 'Μετακίνηση στο επόμενο Κεφάλαιο',\n    'books_copy' => 'Αντιγραφή Βιβλίου',\n    'books_copy_success' => 'Το βιβλίο αντιγράφηκε επιτυχώς',\n\n    // Chapters\n    'chapter' => 'Κεφάλαιο',\n    'chapters' => 'Κεφάλαια',\n    'x_chapters' => ':count Κεφάλαιο|:count Κεφάλαια',\n    'chapters_popular' => 'Δημοφιλή Κεφάλαια',\n    'chapters_new' => 'Νέο Κεφάλαιο',\n    'chapters_create' => 'Δημιουργία Νέου Κεφαλαίου',\n    'chapters_delete' => 'Διαγραφή Κεφαλαίου',\n    'chapters_delete_named' => 'Διαγραφή Κεφαλαίου :chapterName',\n    'chapters_delete_explain' => 'Αυτό θα διαγράψει το κεφάλαιο με το όνομα \\':chapterName\\'. Όλες οι σελίδες που υπάρχουν μέσα σε αυτό το κεφάλαιο θα διαγραφούν επίσης.',\n    'chapters_delete_confirm' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το κεφάλαιο;',\n    'chapters_edit' => 'Επεξεργασία Κεφαλαίου',\n    'chapters_edit_named' => 'Επεξεργασία Κεφαλαίου :chapterName',\n    'chapters_save' => 'Αποθήκευση Κεφαλαίου',\n    'chapters_move' => 'Μετακίνηση Κεφαλαίου',\n    'chapters_move_named' => 'Μετακίνηση Κεφαλαίου :chapterName',\n    'chapters_copy' => 'Αντιγραφή Κεφαλαίου',\n    'chapters_copy_success' => 'Το κεφάλαιο αντιγράφηκε επιτυχώς',\n    'chapters_permissions' => 'Δικαιώματα Κεφαλαίου',\n    'chapters_empty' => 'Καμία σελίδα δεν βρίσκεται σε αυτό το κεφάλαιο.',\n    'chapters_permissions_active' => 'Ενεργά Δικαιώματα Κεφαλαίου',\n    'chapters_permissions_success' => 'Τα Δικαιώματα Κεφαλαίου Ενημερώθηκαν',\n    'chapters_search_this' => 'Αναζήτηση σε αυτό το κεφάλαιο',\n    'chapter_sort_book' => 'Ταξινόμηση Βιβλίου',\n\n    // Pages\n    'page' => 'Σελίδα',\n    'pages' => 'Σελίδες',\n    'x_pages' => ':count Σελίδα|:count Σελίδες',\n    'pages_popular' => 'Δημοφιλείς Σελίδες',\n    'pages_new' => 'Νέα Σελίδα',\n    'pages_attachments' => 'Συνημμένα',\n    'pages_navigation' => 'Πλοήγηση στη σελίδα',\n    'pages_delete' => 'Διαγραφή Σελίδας',\n    'pages_delete_named' => 'Διαγραφή Σελίδας :pageName',\n    'pages_delete_draft_named' => 'Διαγραφή Προσχέδιας Σελίδας :pageName',\n    'pages_delete_draft' => 'Διαγραφή Προσχέδιας Σελίδας',\n    'pages_delete_success' => 'Η σελίδα διαγράφηκε',\n    'pages_delete_draft_success' => 'Η προσχέδια (πρόχειρη) σελίδα διαγράφηκε',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτή τη σελίδα;',\n    'pages_delete_draft_confirm' => 'Θέλετε σίγουρα να διαγράψετε την προσχέδια σελίδα;',\n    'pages_editing_named' => 'Επεξεργασία Σελίδας :pageName',\n    'pages_edit_draft_options' => 'Επιλογές Προσχεδίου',\n    'pages_edit_save_draft' => 'Αποθήκευση Προχείρου (Προσχεδίου)',\n    'pages_edit_draft' => 'Επεξεργασία Προσχεδίου Σελίδας',\n    'pages_editing_draft' => 'Επεξεργασία Προσχεδίου',\n    'pages_editing_page' => 'Επεξεργασία Σελίδας',\n    'pages_edit_draft_save_at' => 'Το προσχέδιο αποθηκεύτηκε στις ',\n    'pages_edit_delete_draft' => 'Διαγραφή Προσχεδίου',\n    'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.',\n    'pages_edit_discard_draft' => 'Απόρριψη Προσχεδίου',\n    'pages_edit_switch_to_markdown' => 'Μετάβαση στον Επεξεργαστή Markdown',\n    'pages_edit_switch_to_markdown_clean' => '(Καθαρισμός Περιεχομένου)',\n    'pages_edit_switch_to_markdown_stable' => '(Σταθερό Περιεχόμενο)',\n    'pages_edit_switch_to_wysiwyg' => 'Εναλλαγή στον επεξεργαστή WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Ορισμός καταγραφής αλλαγών',\n    'pages_edit_enter_changelog_desc' => 'Εισάγετε μια σύντομη περιγραφή των αλλαγών που κάνατε',\n    'pages_edit_enter_changelog' => 'Εισαγωγή Αρχείου Καταγραφής Αλλαγών',\n    'pages_editor_switch_title' => 'Εναλλαγή Επεξεργαστή',\n    'pages_editor_switch_are_you_sure' => 'Είστε βέβαιοι ότι θέλετε να αλλάξετε τον επεξεργαστή κειμένου για αυτή τη σελίδα;',\n    'pages_editor_switch_consider_following' => 'Λάβετε υπόψη τα ακόλουθα όταν αλλάζετε συντάκτες:',\n    'pages_editor_switch_consideration_a' => 'Μόλις αποθηκευτεί, η επιλογή του νέου επεξεργαστή κειμένου θα χρησιμοποιηθεί από τυχόν μελλοντικούς επεξεργαστές, συμπεριλαμβανομένων εκείνων που μπορεί να μην είναι σε θέση να αλλάξουν τον τύπο του επεξεργαστή κειμένου.',\n    'pages_editor_switch_consideration_b' => 'Αυτό μπορεί να οδηγήσει σε απώλεια λεπτομερειών και κώδικα σε ορισμένες περιπτώσεις.',\n    'pages_editor_switch_consideration_c' => 'Οι αλλαγές ετικετών ή αρχείων καταγραφής, που έγιναν από την τελευταία αποθήκευση, δεν θα συνεχιστούν σε αυτήν την αλλαγή.',\n    'pages_save' => 'Αποθήκευση Σελίδας',\n    'pages_title' => 'Τίτλος Σελίδας',\n    'pages_name' => 'Όνομα Σελίδας',\n    'pages_md_editor' => 'Επεξεργαστής',\n    'pages_md_preview' => 'Προεπισκόπηση',\n    'pages_md_insert_image' => 'Εισαγωγή Εικόνας',\n    'pages_md_insert_link' => 'Εισαγωγή/Επεξεργασία συνδέσμου',\n    'pages_md_insert_drawing' => 'Εισαγωγή Σχεδίου',\n    'pages_md_show_preview' => 'Εμφάνιση προεπισκόπησης',\n    'pages_md_sync_scroll' => 'Συγχρονισμός προεπισκόπησης',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Unsaved Drawing Found',\n    'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',\n    'pages_not_in_chapter' => 'Η σελίδα δεν είναι σε κεφάλαιο',\n    'pages_move' => 'Μετακίνηση Σελίδας',\n    'pages_copy' => 'Αντιγραφή Σελίδας',\n    'pages_copy_desination' => 'Αντιγραφή Προορισμού',\n    'pages_copy_success' => 'Η σελίδα αντιγράφηκε επιτυχώς',\n    'pages_permissions' => 'Δικαιώματα Σελίδας',\n    'pages_permissions_success' => 'Τα δικαιώματα σελίδας ενημερώθηκαν',\n    'pages_revision' => 'Αναθεώρηση',\n    'pages_revisions' => 'Αναθεωρήσεις Σελίδας',\n    'pages_revisions_desc' => 'Παρακάτω αναφέρονται όλες οι προηγούμενες αναθεωρήσεις αυτής της Σελίδας. Μπορείτε να αναζητήσετε αντίγραφα ασφαλείας, να συγκρίνετε και να επαναφέρετε παλιές εκδόσεις Σελίδας, εάν τα δικαιώματα το επιτρέπουν. Το πλήρες ιστορικό της Σελίδας μπορεί να μην αντανακλάται πλήρως εδώ επειδή, ανάλογα με τη διαμόρφωση του συστήματος, οι παλιές αναθεωρήσεις θα μπορούσαν να διαγραφούν αυτόματα.',\n    'pages_revisions_named' => 'Αναθεωρήσεις σελίδας για :pageName',\n    'pages_revision_named' => 'Αναθεώρηση σελίδας για :pageName',\n    'pages_revision_restored_from' => 'Επαναφορά από #:id; :summary',\n    'pages_revisions_created_by' => 'Δημιουργήθηκε από',\n    'pages_revisions_date' => 'Ημερομηνία Αναθεώρησης',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Αριθμός αναθεώρησης',\n    'pages_revisions_numbered' => 'Αναθεώρηση #',\n    'pages_revisions_numbered_changes' => 'Αναθεώρηση #:id Αλλαγές',\n    'pages_revisions_editor' => 'Τύπος Επεξεργαστή',\n    'pages_revisions_changelog' => 'Αρχείο καταγραφής αλλαγών',\n    'pages_revisions_changes' => 'Αλλαγές',\n    'pages_revisions_current' => 'Τρέχουσα Έκδοση',\n    'pages_revisions_preview' => 'Προεπισκόπηση',\n    'pages_revisions_restore' => 'Επαναφορά',\n    'pages_revisions_none' => 'Αυτή η σελίδα δεν έχει αναθεωρήσεις',\n    'pages_copy_link' => 'Αντιγραφή Συνδέσμου',\n    'pages_edit_content_link' => 'Jump to section in editor',\n    'pages_pointer_enter_mode' => 'Enter section select mode',\n    'pages_pointer_label' => 'Page Section Options',\n    'pages_pointer_permalink' => 'Page Section Permalink',\n    'pages_pointer_include_tag' => 'Page Section Include Tag',\n    'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag',\n    'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink',\n    'pages_permissions_active' => 'Ενεργά Δικαιώματα Σελίδας',\n    'pages_initial_revision' => 'Αρχική δημοσίευση',\n    'pages_references_update_revision' => 'Αυτόματη ενημέρωση του συστήματος των εσωτερικών συνδέσμων',\n    'pages_initial_name' => 'Νέα Σελίδα',\n    'pages_editing_draft_notification' => 'Αυτή τη στιγμή επεξεργάζεστε ένα προσχέδιο που αποθηκεύτηκε για τελευταία φορά :timeDiff.',\n    'pages_draft_edited_notification' => 'Αυτή η σελίδα έχει ενημερωθεί από εκείνη τη στιγμή. Συνιστάται να απορρίψετε αυτό το προσχέδιο.',\n    'pages_draft_page_changed_since_creation' => 'Αυτή η σελίδα έχει ενημερωθεί από τότε που δημιουργήθηκε αυτό το προσχέδιο. Συνιστάται να απορρίψετε αυτό το σχέδιο ή να φροντίσετε να μην αντικαταστήσετε τυχόν αλλαγές σελίδας.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count χρήστες έχουν αρχίσει να επεξεργάζονται αυτή τη σελίδα',\n        'start_b' => ':userName έχει ξεκινήσει την επεξεργασία αυτής της σελίδας',\n        'time_a' => 'από τότε που η σελίδα ενημερώθηκε τελευταία φορά',\n        'time_b' => 'τα τελευταία :mint λεπτά',\n        'message' => ':start :time. Προσέξτε να μην αντικαταστήσετε ο ένας τις ενημερώσεις του άλλου!',\n    ],\n    'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',\n    'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',\n    'pages_specific' => 'Συγκεκριμένη Σελίδα',\n    'pages_is_template' => 'Πρότυπο σελίδας',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Toggle Sidebar',\n    'page_tags' => 'Ετικέτες Σελίδας',\n    'chapter_tags' => 'Ετικέτες Κεφαλαίου',\n    'book_tags' => 'Ετικέτες Βιβλίου',\n    'shelf_tags' => 'Ετικέτες Ραφιών',\n    'tag' => 'Ετικέτα',\n    'tags' =>  'Ετικέτες',\n    'tags_index_desc' => 'Οι Ετικέτες μπορούν να εφαρμοστούν στο περιεχόμενο μέσα στο σύστημα για να εφαρμοστεί μια ευέλικτη μορφή κατηγοριοποίησης. Οι Ετικέτες μπορούν να έχουν τόσο κλειδί όσο και αξία, με την τιμή να είναι προαιρετική. Μόλις εφαρμοστεί, μπορεί να παρθεί περιεχόμενο χρησιμοποιώντας το όνομα της Ετικέτας και την τιμή.',\n    'tag_name' =>  'Όνομα Ετικέτας',\n    'tag_value' => 'Τιμή Ετικέτας (Προαιρετικό)',\n    'tags_explain' => \"Προσθέστε μερικές ετικέτες για να κατηγοριοποιήσετε καλύτερα το περιεχόμενό σας. \\n Μπορείτε να αντιστοιχίσετε μια τιμή σε μια ετικέτα για πιο αναλυτική οργάνωση.\",\n    'tags_add' => 'Προσθήκη άλλης ετικέτας',\n    'tags_remove' => 'Αφαίρεση ετικέτας',\n    'tags_usages' => 'Συνολικές χρήσεις ετικετών',\n    'tags_assigned_pages' => 'Ανατέθηκε σε σελίδες',\n    'tags_assigned_chapters' => 'Ανατέθηκε στα κεφάλαια',\n    'tags_assigned_books' => 'Ανατέθηκε σε Βιβλία',\n    'tags_assigned_shelves' => 'Ανατέθηκε σε Ράφια',\n    'tags_x_unique_values' => ':count μοναδικές τιμές',\n    'tags_all_values' => 'Όλες οι τιμές',\n    'tags_view_tags' => 'Προβολή Ετικετών',\n    'tags_view_existing_tags' => 'Δείτε τις υπάρχουσες ετικέτες',\n    'tags_list_empty_hint' => 'Οι ετικέτες μπορούν να εκχωρηθούν μέσω της πλαϊνής μπάρας συντάκτη σελίδας ή κατά την επεξεργασία των λεπτομερειών ενός βιβλίου, κεφαλαίου ή ράφι.',\n    'attachments' => 'Συνημμένα',\n    'attachments_explain' => 'Ανεβάστε μερικά αρχεία ή επισυνάψτε μερικούς συνδέσμους για να εμφανίσετε στη σελίδα σας. Αυτά είναι ορατά στην πλαϊνή μπάρα σελίδας.',\n    'attachments_explain_instant_save' => 'Οι αλλαγές εδώ αποθηκεύονται αμέσως.',\n    'attachments_upload' => 'Μεταφόρτωση Αρχείου',\n    'attachments_link' => 'Επισύναψη Δεσμού',\n    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',\n    'attachments_set_link' => 'Ορισμός Συνδέσμου',\n    'attachments_delete' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το συνημμένο;',\n    'attachments_dropzone' => 'Drop files here to upload',\n    'attachments_no_files' => 'Δεν έχουν μεταφορτωθεί αρχεία',\n    'attachments_explain_link' => 'Μπορείτε να επισυνάψετε έναν σύνδεσμο αν προτιμάτε να μην ανεβάσετε ένα αρχείο. Αυτό μπορεί να είναι ένας σύνδεσμος σε άλλη σελίδα ή ένας σύνδεσμος σε ένα αρχείο στο σύννεφο.',\n    'attachments_link_name' => 'Όνομα Συνδέσμου',\n    'attachment_link' => 'Σύνδεσμος συνημμένου',\n    'attachments_link_url' => 'Σύνδεση σε αρχείο',\n    'attachments_link_url_hint' => 'Url του ιστότοπου ή του αρχείου',\n    'attach' => 'Επισύναψη',\n    'attachments_insert_link' => 'Προσθήκη συνημμένου συνδέσμου στη σελίδα',\n    'attachments_edit_file' => 'Επεξεργασία Αρχείου',\n    'attachments_edit_file_name' => 'Όνομα Αρχείου',\n    'attachments_edit_drop_upload' => 'Ρίξτε αρχεία ή κάντε κλικ εδώ για να ανεβάσετε και να αντικαταστήσετε',\n    'attachments_order_updated' => 'Η παραγγελία συνημμένων ενημερώθηκε',\n    'attachments_updated_success' => 'Οι λεπτομέρειες συνημμένου ενημερώθηκαν',\n    'attachments_deleted' => 'Το συνημμένο διαγράφηκε',\n    'attachments_file_uploaded' => 'Το αρχείο μεταφορτώθηκε επιτυχώς',\n    'attachments_file_updated' => 'Το αρχείο ενημερώθηκε επιτυχώς',\n    'attachments_link_attached' => 'Ο σύνδεσμος συνδέθηκε επιτυχώς στη σελίδα',\n    'templates' => 'Πρότυπα',\n    'templates_set_as_template' => 'Η σελίδα είναι πρότυπο',\n    'templates_explain_set_as_template' => 'Μπορείτε να ορίσετε αυτή τη σελίδα ως πρότυπο, έτσι ώστε τα περιεχόμενά της να χρησιμοποιούνται κατά τη δημιουργία άλλων σελίδων. Άλλοι χρήστες θα μπορούν να χρησιμοποιήσουν αυτό το πρότυπο αν έχουν δικαιώματα προβολής για αυτή τη σελίδα.',\n    'templates_replace_content' => 'Αντικατάσταση περιεχομένου σελίδας',\n    'templates_append_content' => 'Προσθήκη στο περιεχόμενο της σελίδας',\n    'templates_prepend_content' => 'Προεπιλογή στο περιεχόμενο της σελίδας',\n\n    // Profile View\n    'profile_user_for_x' => 'Χρήστης για :time',\n    'profile_created_content' => 'Δημιουργία Περιεχομένου',\n    'profile_not_created_pages' => ':userName δεν έχει δημιουργήσει καμία σελίδα',\n    'profile_not_created_chapters' => ':userName δεν έχει δημιουργήσει κεφάλαια',\n    'profile_not_created_books' => ':userName δεν έχει δημιουργήσει βιβλία',\n    'profile_not_created_shelves' => ':userName δεν έχει δημιουργήσει ράφια',\n\n    // Comments\n    'comment' => 'Σχόλιο',\n    'comments' => 'Σχόλια',\n    'comment_add' => 'Προσθήκη Σχολίου',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Αφήστε ένα σχόλιο εδώ',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Αποθήκευση Σχολίου',\n    'comment_new' => 'Νέο Σχόλιο',\n    'comment_created' => 'σχολίασε :createDiff',\n    'comment_updated' => 'Ενημερώθηκε :updateDiff από :username',\n    'comment_updated_indicator' => 'Updated',\n    'comment_deleted_success' => 'Σχόλιο διαγράφηκε',\n    'comment_created_success' => 'Το σχόλιο προστέθηκε',\n    'comment_updated_success' => 'Το σχόλιο ενημερώθηκε',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το σχόλιο;',\n    'comment_in_reply_to' => 'Σε απάντηση στο :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',\n\n    // Revision\n    'revision_delete_confirm' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν την αναθεώρηση;',\n    'revision_restore_confirm' => 'Είστε βέβαιοι ότι θέλετε να επαναφέρετε αυτή την αναθεώρηση; Τα τρέχοντα περιεχόμενα της σελίδας θα αντικατασταθούν.',\n    'revision_cannot_delete_latest' => 'Δεν είναι δυνατή η διαγραφή της τελευταίας αναθεώρησης.',\n\n    // Copy view\n    'copy_consider' => 'Παρακαλώ σκεφτείτε τα παρακάτω κατά την αντιγραφή περιεχομένου.',\n    'copy_consider_permissions' => 'Οι ρυθμίσεις προσαρμοσμένων δικαιωμάτων δεν θα αντιγραφούν.',\n    'copy_consider_owner' => 'Θα γίνετε ο ιδιοκτήτης όλου του αντιγραμμένου περιεχομένου.',\n    'copy_consider_images' => 'Τα αρχεία εικόνας σελίδας δεν θα αναπαραχθούν και οι αρχικές εικόνες θα διατηρήσουν τη σχέση τους με τη σελίδα στην οποία είχαν αρχικά μεταφορτωθεί.',\n    'copy_consider_attachments' => 'Τα συνημμένα σελίδας δεν θα αντιγραφούν.',\n    'copy_consider_access' => 'Μια αλλαγή της θέσης, του ιδιοκτήτη ή των δικαιωμάτων μπορεί να έχει ως αποτέλεσμα το περιεχόμενο αυτό να είναι προσβάσιμο σε χρήστες που προηγουμένως δεν είχαν πρόσβαση.',\n\n    // Conversions\n    'convert_to_shelf' => 'Μετατροπή σε ράφι',\n    'convert_to_shelf_contents_desc' => 'Μπορείτε να μετατρέψετε αυτό το βιβλίο σε ένα νέο ράφι με το ίδιο περιεχόμενο. Κεφάλαια που περιέχονται σε αυτό το βιβλίο θα μετατραπούν σε νέα βιβλία. Αν αυτό το βιβλίο περιέχει σελίδες που δεν βρίσκονται σε κεφάλαιο, αυτό το βιβλίο θα μετονομαστεί και περιέχει τέτοιες σελίδες, και αυτό το βιβλίο θα γίνει μέρος του νέου ράφι.',\n    'convert_to_shelf_permissions_desc' => 'Τυχόν δικαιώματα που ορίζονται σε αυτό το βιβλίο θα αντιγραφούν στο νέο ράφι και σε όλα τα νέα θυγατρικά βιβλία στα οποία δεν επιβάλλονται τα δικά τους δικαιώματα. Λάβετε υπόψη ότι τα δικαιώματα στα ράφια δεν μεταφέρονται αυτόματα σε περιεχόμενο εντός, όπως συμβαίνει για τα βιβλία.',\n    'convert_book' => 'Μετατροπή Βιβλίου',\n    'convert_book_confirm' => 'Είστε σίγουροι ότι θέλετε να μετατρέψετε αυτό το βιβλίο;',\n    'convert_undo_warning' => 'Αυτό δεν μπορεί να αναιρεθεί τόσο εύκολα.',\n    'convert_to_book' => 'Μετατροπή σε βιβλίο',\n    'convert_to_book_desc' => 'Μπορείτε να μετατρέψετε αυτό το κεφάλαιο σε ένα νέο βιβλίο με το ίδιο περιεχόμενο. Τυχόν δικαιώματα που έχουν οριστεί σε αυτό το κεφάλαιο θα αντιγραφούν στο νέο βιβλίο αλλά τυχόν κληρονομημένα δικαιώματα, από το γονικό βιβλίο, δεν θα αντιγραφούν το οποίο θα μπορούσε να οδηγήσει σε αλλαγή του ελέγχου πρόσβασης.',\n    'convert_chapter' => 'Μετατροπή Κεφαλαίου',\n    'convert_chapter_confirm' => 'Είστε βέβαιοι ότι θέλετε να μετατρέψετε αυτό το κεφάλαιο;',\n\n    // References\n    'references' => 'Αναφορές',\n    'references_none' => 'Δεν υπάρχουν αναφορές παρακολούθησης σε αυτό το στοιχείο.',\n    'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',\n\n    // Watch Options\n    'watch' => 'Watch',\n    'watch_title_default' => 'Default Preferences',\n    'watch_desc_default' => 'Revert watching to just your default notification preferences.',\n    'watch_title_ignore' => 'Ignore',\n    'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.',\n    'watch_title_new' => 'New Pages',\n    'watch_desc_new' => 'Notify when any new page is created within this item.',\n    'watch_title_updates' => 'All Page Updates',\n    'watch_desc_updates' => 'Notify upon all new pages and page changes.',\n    'watch_desc_updates_page' => 'Notify upon all page changes.',\n    'watch_title_comments' => 'All Page Updates & Comments',\n    'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.',\n    'watch_desc_comments_page' => 'Notify upon page changes and new comments.',\n    'watch_change_default' => 'Change default notification preferences',\n    'watch_detail_ignore' => 'Ignoring notifications',\n    'watch_detail_new' => 'Watching for new pages',\n    'watch_detail_updates' => 'Watching new pages and updates',\n    'watch_detail_comments' => 'Watching new pages, updates & comments',\n    'watch_detail_parent_book' => 'Watching via parent book',\n    'watch_detail_parent_book_ignore' => 'Ignoring via parent book',\n    'watch_detail_parent_chapter' => 'Watching via parent chapter',\n    'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter',\n];\n"
  },
  {
    "path": "lang/el/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Δεν έχετε δικαίωμα πρόσβασης στη ζητούμενη σελίδα.',\n    'permissionJson' => 'Δεν έχετε άδεια να εκτελέσετε την αιτούμενη ενέργεια.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Ένας χρήστης με email :email υπάρχει ήδη αλλά με διαφορετικά διαπιστευτήρια.',\n    'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',\n    'email_already_confirmed' => 'Το email έχει ήδη επιβεβαιωθεί, Δοκιμάστε να συνδεθείτε.',\n    'email_confirmation_invalid' => 'Αυτό το διακριτικό επιβεβαίωσης δεν είναι έγκυρο ή έχει ήδη χρησιμοποιηθεί, Παρακαλώ δοκιμάστε να εγγραφείτε ξανά.',\n    'email_confirmation_expired' => 'Το διακριτικό επιβεβαίωσης έχει λήξει, έχει σταλεί ένα νέο email επιβεβαίωσης.',\n    'email_confirmation_awaiting' => 'Η διεύθυνση ηλεκτρονικού ταχυδρομείου για το λογαριασμό που χρησιμοποιείται πρέπει να επιβεβαιωθεί',\n    'ldap_fail_anonymous' => 'Η πρόσβαση LDAP απέτυχε με ανώνυμη σύνδεση',\n    'ldap_fail_authed' => 'Η πρόσβαση LDAP απέτυχε με τη χρήση δοσμένων λεπτομερειών dn & κωδικού πρόσβασης',\n    'ldap_extension_not_installed' => 'Η επέκταση LDAP PHP δεν εγκαταστάθηκε',\n    'ldap_cannot_connect' => 'Αδυναμία σύνδεσης στο διακομιστή ldap, η αρχική σύνδεση απέτυχε',\n    'saml_already_logged_in' => 'Ήδη συνδεδεμένος',\n    'saml_no_email_address' => 'Δεν ήταν δυνατή η εύρεση μιας διεύθυνσης ηλεκτρονικού ταχυδρομείου, για αυτόν τον χρήστη, στα δεδομένα που παρέχονται από το εξωτερικό σύστημα ελέγχου ταυτότητας',\n    'saml_invalid_response_id' => 'Το αίτημα από το εξωτερικό σύστημα ελέγχου ταυτότητας δεν αναγνωρίζεται από μια διαδικασία που ξεκίνησε από αυτή την εφαρμογή. Η πλοήγηση πίσω μετά από μια σύνδεση θα μπορούσε να προκαλέσει αυτό το ζήτημα.',\n    'saml_fail_authed' => 'Η σύνδεση με τη χρήση :system απέτυχε, το σύστημα δεν παρείχε επιτυχή εξουσιοδότηση',\n    'oidc_already_logged_in' => 'Ήδη συνδεδεμένος',\n    'oidc_no_email_address' => 'Δεν ήταν δυνατή η εύρεση μιας διεύθυνσης ηλεκτρονικού ταχυδρομείου, για αυτόν τον χρήστη, στα δεδομένα που παρέχονται από το εξωτερικό σύστημα ελέγχου ταυτότητας',\n    'oidc_fail_authed' => 'Η σύνδεση με τη χρήση :system απέτυχε, το σύστημα δεν παρείχε επιτυχή εξουσιοδότηση',\n    'social_no_action_defined' => 'Καμία ενέργεια δεν ορίστηκε',\n    'social_login_bad_response' => \"Παρουσιάστηκε σφάλμα κατά τη διάρκεια :socialAccount login: \\n:error\",\n    'social_account_in_use' => 'Αυτός ο λογαριασμός :socialAccount είναι ήδη σε χρήση, Δοκιμάστε να συνδεθείτε μέσω της επιλογής :socialAccount .',\n    'social_account_email_in_use' => 'Το email :email είναι ήδη σε χρήση. Αν έχετε ήδη ένα λογαριασμό, μπορείτε να συνδέσετε τον :socialAccount λογαριασμό σας από τις ρυθμίσεις του προφίλ σας.',\n    'social_account_existing' => 'Αυτός ο :socialAccount είναι ήδη συνδεδεμένος στο προφίλ σας.',\n    'social_account_already_used_existing' => 'Αυτός ο :socialAccount λογαριασμός χρησιμοποιείται ήδη από άλλο χρήστη.',\n    'social_account_not_used' => 'Αυτός ο :socialAccount λογαριασμός δεν είναι συνδεδεμένος με κανέναν χρήστη. Παρακαλώ επισυνάψτε τον στις ρυθμίσεις του προφίλ σας. ',\n    'social_account_register_instructions' => 'Εάν δεν έχετε ακόμα λογαριασμό, μπορείτε να καταχωρήσετε ένα λογαριασμό χρησιμοποιώντας την επιλογή :socialAccount .',\n    'social_driver_not_found' => 'Δεν βρέθηκε κοινωνικός οδηγός',\n    'social_driver_not_configured' => 'Οι κοινωνικές ρυθμίσεις του :socialAccount δεν έχουν ρυθμιστεί σωστά.',\n    'invite_token_expired' => 'Αυτός ο σύνδεσμος πρόσκλησης έχει λήξει. Αντ\\' αυτού μπορείτε να προσπαθήσετε να επαναφέρετε τον κωδικό πρόσβασής σας.',\n    'login_user_not_found' => 'A user for this action could not be found.',\n\n    // System\n    'path_not_writable' => 'Η διαδρομή αρχείου :filePath δεν μπόρεσε να μεταφορτωθεί. Βεβαιωθείτε ότι είναι εγγράψιμη στο διακομιστή.',\n    'cannot_get_image_from_url' => 'Αδυναμία λήψης εικόνας από :url',\n    'cannot_create_thumbs' => 'Ο διακομιστής δεν μπορεί να δημιουργήσει μικρογραφίες. Παρακαλώ ελέγξτε ότι έχετε την επέκταση GD PHP εγκατεστημένη.',\n    'server_upload_limit' => 'Ο διακομιστής δεν επιτρέπει τη μεταφόρτωση αυτού του μεγέθους. Παρακαλώ δοκιμάστε ένα μικρότερο μέγεθος αρχείου.',\n    'server_post_limit' => 'The server cannot receive the provided amount of data. Try again with less data or a smaller file.',\n    'uploaded'  => 'Ο διακομιστής δεν επιτρέπει τη μεταφόρτωση αυτού του μεγέθους. Παρακαλώ δοκιμάστε ένα μικρότερο μέγεθος αρχείου.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Παρουσιάστηκε σφάλμα κατά το ανέβασμα της εικόνας.',\n    'image_upload_type_error' => 'Ο τύπος εικόνας που μεταφορτώθηκε δεν είναι έγκυρος',\n    'image_upload_replace_type' => 'Image file replacements must be of the same type',\n    'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.',\n    'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.',\n    'image_gallery_thumbnail_memory_limit' => 'Failed to create gallery thumbnails due to system resource limits.',\n    'drawing_data_not_found' => 'Δεν ήταν δυνατή η φόρτωση δεδομένων σχεδίασης. Το αρχείο σχεδίασης ενδέχεται να μην υπάρχει πλέον ή ενδέχεται να μην έχετε άδεια πρόσβασης σε αυτά.',\n\n    // Attachments\n    'attachment_not_found' => 'Το συνημμένο δεν βρέθηκε',\n    'attachment_upload_error' => 'An error occurred uploading the attachment file',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Αποτυχία αποθήκευσης προσχέδιου. Βεβαιωθείτε ότι έχετε σύνδεση στο διαδίκτυο πριν την αποθήκευση αυτής της σελίδας',\n    'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content',\n    'page_custom_home_deletion' => 'Δεν μπορεί να διαγραφεί μια σελίδα ενώ έχει οριστεί ως αρχική σελίδα',\n\n    // Entities\n    'entity_not_found' => 'Η οντότητα δεν βρέθηκε',\n    'bookshelf_not_found' => 'Το ράφι δεν βρέθηκε',\n    'book_not_found' => 'Το βιβλίο δεν βρέθηκε',\n    'page_not_found' => 'Η σελίδα δεν βρέθηκε',\n    'chapter_not_found' => 'Το κεφάλαιο δεν βρέθηκε',\n    'selected_book_not_found' => 'Το επιλεγμένο βιβλίο δεν βρέθηκε',\n    'selected_book_chapter_not_found' => 'Το επιλεγμένο βιβλίο ή κεφάλαιο δεν βρέθηκε',\n    'guests_cannot_save_drafts' => 'Οι επισκέπτες δεν μπορούν να αποθηκεύσουν πρόχειρα',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Δεν μπορείτε να διαγράψετε τον μοναδικό διαχειριστή',\n    'users_cannot_delete_guest' => 'Δεν μπορείτε να διαγράψετε τον επισκέπτη',\n    'users_could_not_send_invite' => 'Could not create user since invite email failed to send',\n\n    // Roles\n    'role_cannot_be_edited' => 'Αυτός ο ρόλος δεν μπορεί να επεξεργαστεί',\n    'role_system_cannot_be_deleted' => 'Αυτός ο ρόλος είναι ρόλος συστήματος και δεν μπορεί να διαγραφεί',\n    'role_registration_default_cannot_delete' => 'Αυτός ο ρόλος δεν μπορεί να διαγραφεί ενώ έχει οριστεί ως προεπιλεγμένος ρόλος εγγραφής',\n    'role_cannot_remove_only_admin' => 'Αυτός ο χρήστης είναι ο μόνος χρήστης που έχει ανατεθεί στον ρόλο διαχειριστή. Εκχωρήστε τον ρόλο διαχειριστή σε άλλο χρήστη πριν επιχειρήσετε να τον καταργήσετε εδώ.',\n\n    // Comments\n    'comment_list' => 'Παρουσιάστηκε σφάλμα κατά την λήψη σχολίων.',\n    'cannot_add_comment_to_draft' => 'Δεν μπορείτε να προσθέσετε σχόλια σε ένα προσχέδιο.',\n    'comment_add' => 'Παρουσιάστηκε σφάλμα κατά την προσθήκη / ενημέρωση του σχολίου.',\n    'comment_delete' => 'Παρουσιάστηκε σφάλμα κατά τη διαγραφή του σχολίου.',\n    'empty_comment' => 'Αδυναμία προσθήκης ενός κενού σχολίου.',\n\n    // Error pages\n    '404_page_not_found' => 'Η Σελίδα δε βρέθηκε',\n    'sorry_page_not_found' => 'Λυπούμαστε, Η σελίδα που αναζητάτε δεν βρέθηκε.',\n    'sorry_page_not_found_permission_warning' => 'Αν περιμένατε να υπάρχει αυτή η σελίδα, ίσως να μην έχετε δικαίωμα να την δείτε.',\n    'image_not_found' => 'Η Εικόνα δεν βρέθηκε',\n    'image_not_found_subtitle' => 'Λυπούμαστε, το αρχείο εικόνας που αναζητάτε δεν μπορεί να βρεθεί.',\n    'image_not_found_details' => 'Αν περιμένατε να υπάρχει αυτή η εικόνα, ίσως να έχει διαγραφεί.',\n    'return_home' => 'Επιστροφή στην αρχική σελίδα',\n    'error_occurred' => 'Προέκυψε Ένα Σφάλμα',\n    'app_down' => ':appName είναι προσωρινά μη διαθέσιμη',\n    'back_soon' => 'Θα υπάρξει σύντομα υποστήριξη.',\n\n    // Import\n    'import_zip_cant_read' => 'Could not read ZIP file.',\n    'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',\n    'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Import ZIP failed to validate with errors:',\n    'import_zip_failed_notification' => 'Failed to import ZIP file.',\n    'import_perms_books' => 'You are lacking the required permissions to create books.',\n    'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',\n    'import_perms_pages' => 'You are lacking the required permissions to create pages.',\n    'import_perms_images' => 'You are lacking the required permissions to create images.',\n    'import_perms_attachments' => 'You are lacking the required permission to create attachments.',\n\n    // API errors\n    'api_no_authorization_found' => 'Δεν βρέθηκε διακριτικό εξουσιοδότησης κατόπιν αιτήματος',\n    'api_bad_authorization_format' => 'Ένα διακριτικό εξουσιοδότησης βρέθηκε κατόπιν αιτήματος, αλλά η μορφή εμφανίστηκε εσφαλμένη',\n    'api_user_token_not_found' => 'Δεν βρέθηκε αντίστοιχο διακριτικό API για το παρεχόμενο διακριτικό εξουσιοδότησης',\n    'api_incorrect_token_secret' => 'Το μυστικό που παρέχεται για το δεδομένο χρησιμοποιημένο διακριτικό API είναι εσφαλμένο',\n    'api_user_no_api_permission' => 'Ο ιδιοκτήτης του χρησιμοποιημένου διακριτικού API δεν έχει άδεια για να κάνει κλήσεις API',\n    'api_user_token_expired' => 'Το διακριτικό εξουσιοδότησης που χρησιμοποιείται έχει λήξει',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Σφάλμα κατά την αποστολή δοκιμαστικού email:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'The URL does not match the configured allowed SSR hosts',\n];\n"
  },
  {
    "path": "lang/el/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Νέο σχόλιο στη σελίδα: :pageName',\n    'new_comment_intro' => 'Ένας χρήστης έχει σχολιάσει σε μια σελίδα στο :appName:',\n    'new_page_subject' => 'Νέα σελίδα: :pageName',\n    'new_page_intro' => 'Μια νέα σελίδα έχει δημιουργηθεί στο :appName:',\n    'updated_page_subject' => 'Ενημερωμένη σελίδα: :pageName',\n    'updated_page_intro' => 'Μια σελίδα έχει ενημερωθεί στο :appName:',\n    'updated_page_debounce' => 'Για να αποτρέψετε μαζικές ειδοποιήσεις, για κάποιο διάστημα δε θα σας αποστέλλονται ειδοποιήσεις για περαιτέρω αλλαγές σε αυτήν τη σελίδα από τον ίδιο συντάκτη.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Όνομα σελίδας:',\n    'detail_page_path' => 'Διαδρομή σελίδας:',\n    'detail_commenter' => 'Σχολιαστής:',\n    'detail_comment' => 'Σχόλιο:',\n    'detail_created_by' => 'Δημιουργήθηκε από:',\n    'detail_updated_by' => 'Ενημερώθηκε από:',\n\n    'action_view_comment' => 'Προβολή σχολίου',\n    'action_view_page' => 'Προβολή σελίδας',\n\n    'footer_reason' => 'Αυτή η ειδοποίηση εστάλη σε εσάς επειδή το :link καλύπτει αυτόν τον τύπο δραστηριότητας για αυτό το αντικείμενο.',\n    'footer_reason_link' => 'προτιμήσεις σας για ειδοποιήσεις',\n];\n"
  },
  {
    "path": "lang/el/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Προηγούμενο',\n    'next'     => 'Επόμενο &raquo;',\n\n];\n"
  },
  {
    "path": "lang/el/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Ο κωδικός πρόσβασης πρέπει να αποτελείται από τουλάχιστον έξι χαρακτήρες και να ταιριάζει με τον κωδικό επιβεβαίωσης.',\n    'user' => \"Δεν μπορούμε να βρόυμε κάποιον χρήστη με αυτή τη διεύθυνση e-mail.\",\n    'token' => 'Το διακριτικό επαναφοράς κωδικού πρόσβασης δεν είναι έγκυρο για αυτή τη διεύθυνση ηλεκτρονικού ταχυδρομείου.',\n    'sent' => 'Σας έχουμε στείλει e-mail με τον σύνδεσμο επαναφοράς του κωδικού πρόσβασης!',\n    'reset' => 'Ο κωδικός σας έχει επαναφερθεί!',\n\n];\n"
  },
  {
    "path": "lang/el/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'My Account',\n\n    'shortcuts' => 'Συντομεύσεις',\n    'shortcuts_interface' => 'UI Shortcut Preferences',\n    'shortcuts_toggle_desc' => 'Εδώ μπορείτε να ενεργοποιήσετε ή να απενεργοποιήσετε τις συντομεύσεις του συστήματος πληκτρολογίου, που χρησιμοποιούνται για την πλοήγηση και τις ενέργειες.',\n    'shortcuts_customize_desc' => 'Μπορείτε να προσαρμόσετε κάθε μία από τις παρακάτω συντομεύσεις. Απλά πατήστε το επιθυμητό συνδυασμό πλήκτρων μετά την επιλογή της εισόδου για μια συντόμευση.',\n    'shortcuts_toggle_label' => 'Ενεργοποίηση συντομεύσεων πληκτρολογίου',\n    'shortcuts_section_navigation' => 'Πλοήγηση',\n    'shortcuts_section_actions' => 'Κοινές ενέργειες',\n    'shortcuts_save' => 'Αποθήκευση Συντομεύσεων',\n    'shortcuts_overlay_desc' => 'Σημείωση: Όταν οι συντομεύσεις είναι ενεργοποιημένες μια βοηθητική επικάλυψη είναι διαθέσιμη πατώντας \"?\" που θα τονίσει τις διαθέσιμες συντομεύσεις για ενέργειες που είναι ορατές στην οθόνη.',\n    'shortcuts_update_success' => 'Οι προτιμήσεις σας αποθηκεύτηκαν!',\n    'shortcuts_overview_desc' => 'Manage keyboard shortcuts you can use to navigate the system user interface.',\n\n    'notifications' => 'Notification Preferences',\n    'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.',\n    'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own',\n    'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Notify upon replies to my comments',\n    'notifications_save' => 'Save Preferences',\n    'notifications_update_success' => 'Notification preferences have been updated!',\n    'notifications_watched' => 'Watched & Ignored Items',\n    'notifications_watched_desc' => 'Below are the items that have custom watch preferences applied. To update your preferences for these, view the item then find the watch options in the sidebar.',\n\n    'auth' => 'Access & Security',\n    'auth_change_password' => 'Change Password',\n    'auth_change_password_desc' => 'Change the password you use to log-in to the application. This must be at least 8 characters long.',\n    'auth_change_password_success' => 'Password has been updated!',\n\n    'profile' => 'Profile Details',\n    'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.',\n    'profile_view_public' => 'View Public Profile',\n    'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.',\n    'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.',\n    'profile_email_no_permission' => 'Unfortunately you don\\'t have permission to change your email address. If you want to change this, you\\'d need to ask an administrator to change this for you.',\n    'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.',\n    'profile_admin_options' => 'Administrator Options',\n    'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the \"Settings > Users\" area of the application.',\n\n    'delete_account' => 'Delete Account',\n    'delete_my_account' => 'Delete My Account',\n    'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\\'ve created, such as created pages and uploaded images, will remain.',\n    'delete_my_account_warning' => 'Are you sure you want to delete your account?',\n];\n"
  },
  {
    "path": "lang/el/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Ρυθμίσεις',\n    'settings_save' => 'Αποθήκευση ρυθμίσεων',\n    'system_version' => 'Έκδοση εφαρμογής',\n    'categories' => 'Κατηγορίες',\n\n    // App Settings\n    'app_customization' => 'Προσαρμογή',\n    'app_features_security' => 'Χαρακτηριστικά & Ασφάλεια',\n    'app_name' => 'Όνομα Εφαρμογής',\n    'app_name_desc' => 'Αυτό το όνομα εμφανίζεται στην κεφαλίδα της ιστοσελίδας και σε τυχόν μηνύματα ηλεκτρονικού ταχυδρομείου που αποστέλλονται από το σύστημα.',\n    'app_name_header' => 'Εμφάνιση Ονόματος στην κεφαλίδα',\n    'app_public_access' => 'Δημόσια Πρόσβαση',\n    'app_public_access_desc' => 'Η ενεργοποίηση αυτής της επιλογής θα επιτρέψει στους επισκέπτες, που δεν είναι συνδεδεμένοι, να έχουν πρόσβαση στο περιεχόμενο της εφαρμογής BookStack.',\n    'app_public_access_desc_guest' => 'Η πρόσβαση για δημόσιους επισκέπτες μπορεί να ελεγχθεί μέσω του χρήστη \"Guest\".',\n    'app_public_access_toggle' => 'Να επιτρέπεται η δημόσια πρόσβαση',\n    'app_public_viewing' => 'Να επιτρέπεται η δημόσια προβολή;',\n    'app_secure_images' => 'Μεταφορτώσεις Εικόνων υψηλότερης Ασφάλειας',\n    'app_secure_images_toggle' => 'Ενεργοποιήστε τις μεταφορτώσεις Εικόνων υψηλότερης Ασφάλειας',\n    'app_secure_images_desc' => 'Για λόγους απόδοσης, όλες οι εικόνες είναι δημόσιες. Αυτή η επιλογή προσθέτει μια τυχαία συμβολοσειρά μπροστά από τις διευθύνσεις URL εικόνων, δύσκολο να τη μαντέψει κάποιος. Βεβαιωθείτε ότι τα ευρετήρια καταλόγου δεν είναι ενεργοποιημένα για να αποτρέψετε την εύκολη πρόσβαση.',\n    'app_default_editor' => 'Προεπιλεγμένος Επεξεργαστής σελίδων',\n    'app_default_editor_desc' => 'Επιλέξτε ποιο πρόγραμμα επεξεργασίας θα χρησιμοποιείται από προεπιλογή κατά την επεξεργασία νέων σελίδων. Αυτό μπορεί να παρακαμφθεί σε επίπεδο σελίδας όπου το επιτρέπουν τα δικαιώματα.',\n    'app_custom_html' => 'Προσαρμοσμένο περιεχόμενο κεφαλίδας HTML',\n    'app_custom_html_desc' => 'Οποιοδήποτε περιεχόμενο προστίθεται εδώ θα εισαχθεί στο κάτω μέρος της ενότητας <head> κάθε σελίδας. Αυτό είναι βολικό για την παράκαμψη ή προσθήκη στυλ καθώς και την προσθήκη κώδικα αναλυτικών στοιχείων.',\n    'app_custom_html_disabled_notice' => 'Το προσαρμοσμένο περιεχόμενο κεφαλίδας HTML είναι απενεργοποιημένο σε αυτήν τη σελίδα ρυθμίσεων, για να διασφαλιστεί ότι τυχόν αλλαγές που θα πραγματοποιηθούν και θα προκαλέσουν δυσλειτουργία στην ιστοσελίδα σας, μπορούν να επαναφερθούν.',\n    'app_logo' => 'Λογότυπο εφαρμογής',\n    'app_logo_desc' => 'Αυτό χρησιμοποιείται στη γραμμή κεφαλίδας εφαρμογής, μεταξύ άλλων περιοχών. Αυτή η εικόνα θα πρέπει να είναι 86px σε ύψος. Οι μεγάλες εικόνες θα κλιμακωθούν.',\n    'app_icon' => 'Εικονίδιο Εφαρμογής',\n    'app_icon_desc' => 'Αυτό το εικονίδιο χρησιμοποιείται για τις καρτέλες περιηγητή και τα εικονίδια συντομεύσεων. Αυτό πρέπει να είναι μια τετράγωνη εικόνα 256px σε μορφή PNG.',\n    'app_homepage' => 'Αρχική σελίδα εφαρμογής',\n    'app_homepage_desc' => 'Επιλέξτε μια προβολή για εμφάνιση στην αρχική σελίδα αντί για την προεπιλεγμένη προβολή. Τα δικαιώματα σελίδων αγνοούνται για επιλεγμένες σελίδες.',\n    'app_homepage_select' => 'Επιλέξτε μια σελίδα',\n    'app_footer_links' => 'Σύνδεσμοι υποσέλιδου',\n    'app_footer_links_desc' => 'Προσθέστε συνδέσμους για εμφάνιση στο υποσέλιδο του ιστότοπου. Αυτά θα εμφανίζονται στο κάτω μέρος των περισσότερων σελίδων, συμπεριλαμβανομένων εκείνων που δεν απαιτούν σύνδεση. Μπορείτε να χρησιμοποιήσετε μια ετικέτα \"trans::<key>\" για να χρησιμοποιήσετε μεταφράσεις που καθορίζονται από το σύστημα. Για παράδειγμα: Η χρήση του \"trans::common.privacy_policy\" θα παρέχει το μεταφρασμένο κείμενο \"Πολιτική Απορρήτου\" και το \"trans::common.terms_of_service\" θα παρέχει το μεταφρασμένο κείμενο \"Όροι Παροχής Υπηρεσιών\".',\n    'app_footer_links_label' => 'Ετικέτα Συνδέσμου',\n    'app_footer_links_url' => 'URL Σύνδεσης',\n    'app_footer_links_add' => 'Προσθήκη Συνδέσμου υποσέλιδου',\n    'app_disable_comments' => 'Απενεργοποίηση Σχολίων',\n    'app_disable_comments_toggle' => 'Απενεργοποίηση Σχολίων',\n    'app_disable_comments_desc' => 'Απενεργοποιεί τα σχόλια σε όλες τις σελίδες της εφαρμογής. <br> Τα υπάρχοντα σχόλια δεν εμφανίζονται.',\n\n    // Color settings\n    'color_scheme' => 'Θέμα Χρωμάτων Εφαρμογής',\n    'color_scheme_desc' => 'Ορίστε τα χρώματα που θα χρησιμοποιηθούν στο περιβάλλον χρήστη της εφαρμογής. Τα χρώματα μπορούν να ρυθμιστούν ξεχωριστά για τις λειτουργίες Σκούρο ή Λευκό, για να ταιριάζει καλύτερα στο θέμα και να εξασφαλίσει αναγνωσιμότητα.',\n    'ui_colors_desc' => 'Ορίστε το πρωτεύον χρώμα της εφαρμογής και το προεπιλεγμένο χρώμα συνδέσμου. Το πρωτεύον χρώμα χρησιμοποιείται κυρίως για την κεφαλίδα, τα κουμπιά και τις διακοσμήσεις διεπαφής. Το προεπιλεγμένο χρώμα συνδέσμου χρησιμοποιείται για συνδέσμους και ενέργειες που βασίζονται στο κείμενο, τόσο μέσα στο γραπτό περιεχόμενο όσο και στη διεπαφή της εφαρμογής.',\n    'app_color' => 'Κυρίως χρώμα',\n    'link_color' => 'Κυρίως χρώμα Συνδέσμου',\n    'content_colors_desc' => 'Ορίζει τα χρώματα για όλα τα στοιχεία στην ιεραρχία οργάνωσης της ιστοσελίδας. Συνιστάται η επιλογή χρωμάτων με παρόμοια φωτεινότητα με τα προεπιλεγμένα, για μέγιστη αναγνωσιμότητα.',\n    'bookshelf_color' => 'Χρώμα Ραφιού',\n    'book_color' => 'Χρώμα Βιβλίων',\n    'chapter_color' => 'Χρώμα Κεφαλαίων Βιβλίων',\n    'page_color' => 'Χρώμα Σελίδων',\n    'page_draft_color' => 'Χρώμα Πρoσχέδιων Σελίδων (Draft page)',\n\n    // Registration Settings\n    'reg_settings' => 'Εγγραφή',\n    'reg_enable' => 'Ενεργοποίηση Εγγραφής',\n    'reg_enable_toggle' => 'Ενεργοποίηση εγγραφής',\n    'reg_enable_desc' => 'Όταν ενεργοποιηθεί η εγγραφή, ο χρήστης θα μπορεί να εγγραφεί ως χρήστης της εφαρμογής. Κατά την εγγραφή τους δίνεται ένας μοναδικός, προεπιλεγμένος ρόλος χρήστη.',\n    'reg_default_role' => 'Προεπιλεγμένος ρόλος χρήστη μετά την εγγραφή',\n    'reg_enable_external_warning' => 'Η παραπάνω επιλογή αγνοείται όταν ο εξωτερικός έλεγχος ταυτότητας LDAP ή SAML είναι ενεργός. Οι λογαριασμοί χρηστών για μη υπάρχοντα μέλη θα δημιουργηθούν αυτόματα εάν ο έλεγχος ταυτότητας, έναντι του εξωτερικού συστήματος που χρησιμοποιείται, είναι επιτυχής.',\n    'reg_email_confirmation' => 'Επιβεβαίωση ηλεκτρονικού ταχυδρομείου',\n    'reg_email_confirmation_toggle' => 'Απαιτείται η επιβεβαίωση μέσω email',\n    'reg_confirm_email_desc' => 'Εάν χρησιμοποιείται περιορισμός τομέα, τότε θα απαιτείται επιβεβαίωση μέσω email και αυτή η επιλογή θα αγνοηθεί.',\n    'reg_confirm_restrict_domain' => 'Περιορισμός Τομέα',\n    'reg_confirm_restrict_domain_desc' => 'Εισαγάγετε μια λίστα διαχωρισμένων με κόμματα τομέων email στους οποίους θέλετε να περιορίσετε την εγγραφή. Θα σταλεί στους χρήστες ένα email για να επιβεβαιώσουν τη διεύθυνσή τους πριν τους επιτραπεί να αλληλεπιδράσουν με την εφαρμογή. <br> <strong>Σημειώστε ότι οι χρήστες θα μπορούν να αλλάξουν τις διευθύνσεις email τους μετά την επιτυχή εγγραφή</strong>.',\n    'reg_confirm_restrict_domain_placeholder' => 'Δε έχουν ρυθμιστεί περιορισμοί ακόμα',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Συντήρηση',\n    'maint_image_cleanup' => 'Εκκαθάριση Εικόνων',\n    'maint_image_cleanup_desc' => 'Σαρώνει το περιεχόμενο σελίδων και τις αναθεωρήσεις αυτών για να ελέγξει ποιες εικόνες και σχέδια χρησιμοποιούνται αυτήν τη στιγμή και ποιες είναι περιττές. Βεβαιωθείτε ότι έχετε δημιουργήσει ένα πλήρες αντίγραφο της βάση δεδομένων και των εικόνων προτού το εκτελέσετε.',\n    'maint_delete_images_only_in_revisions' => 'Διαγράψτε επίσης εικόνες που υπάρχουν μόνο σε παλιές αναθεωρήσεις σελίδων',\n    'maint_image_cleanup_run' => 'Εκτέλεση Εκκαθάρισης',\n    'maint_image_cleanup_warning' => 'Βρέθηκαν :count δυνητικά αχρησιμοποίητες εικόνες. Είστε βέβαιοι ότι θέλετε να τις διαγράψετε αυτές;',\n    'maint_image_cleanup_success' => ':count δυνητικά αχρησιμοποίητες εικόνες βρέθηκαν και διαγράφηκαν!',\n    'maint_image_cleanup_nothing_found' => 'Δεν βρέθηκαν αχρησιμοποίητες εικόνες, τίποτα δεν διαγράφηκε!',\n    'maint_send_test_email' => 'Στείλτε ένα δοκιμαστικό email',\n    'maint_send_test_email_desc' => 'Αυτό στέλνει ένα δοκιμαστικό μήνυμα ηλεκτρονικού ταχυδρομείου στη διεύθυνση email σας που προσδιορίζεται στο προφίλ σας.',\n    'maint_send_test_email_run' => 'Αποστολή δοκιμαστικού email',\n    'maint_send_test_email_success' => 'Το email στάλθηκε στη διεύθυνση :address',\n    'maint_send_test_email_mail_subject' => 'Δοκιμαστικό Email',\n    'maint_send_test_email_mail_greeting' => 'Η παράδοση email φαίνεται να λειτουργεί!',\n    'maint_send_test_email_mail_text' => 'Συγχαρητήρια! Καθώς λάβατε αυτήν την ειδοποίηση μέσω email, οι ρυθμίσεις email σας φαίνεται να έχουν διαμορφωθεί σωστά.',\n    'maint_recycle_bin_desc' => 'Τα διαγραμμένα Ράφια και βιβλία, τα διαγραμμένα κεφάλαια και σελίδες αποστέλλονται στον κάδο ανακύκλωσης, έτσι ώστε να μπορούν να αποκατασταθούν ή να διαγραφούν οριστικά. Τα παλαιότερα αντικείμενα στον κάδο ανακύκλωσης ενδέχεται να αφαιρεθούν αυτόματα μετά από λίγο, ανάλογα με τη διαμόρφωση του συστήματος.',\n    'maint_recycle_bin_open' => 'Άνοιγμα Κάδου Ανακύκλωσης',\n    'maint_regen_references' => 'Αναδημιουργία Αναφορών',\n    'maint_regen_references_desc' => 'Αυτή η ενέργεια θα ξαναχτίσει το ευρετήριο αναφοράς διαστοιχείου μέσα στη βάση δεδομένων. Αυτό συνήθως γίνεται αυτόματα αλλά αυτή η ενέργεια μπορεί να είναι χρήσιμη για το παλιό περιεχόμενο ή περιεχόμενο που προστίθεται μέσω ανεπίσημων μεθόδων.',\n    'maint_regen_references_success' => 'Το ευρετήριο αναφοράς αναδημιουργήθηκε!',\n    'maint_timeout_command_note' => 'Σημείωση: Αυτή η ενέργεια μπορεί να πάρει χρόνο για να εκτελεστεί, η οποία μπορεί να οδηγήσει σε προβλήματα χρονικού ορίου σε ορισμένα περιβάλλοντα ιστού. Ως εναλλακτική λύση, αυτή η ενέργεια πρέπει να εκτελείται χρησιμοποιώντας μια εντολή τερματικού.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Κάδος Ανακύκλωσης',\n    'recycle_bin_desc' => 'Εδώ μπορείτε να επαναφέρετε στοιχεία που έχουν διαγραφεί ή να επιλέξετε να τα αφαιρέσετε οριστικά από το σύστημα. Αυτή η λίστα δεν είναι φιλτραρισμένη όπως γίνεται σε παρόμοιες λίστες δραστηριοτήτων στο σύστημα στις οποίες εφαρμόζονται φίλτρα αδειών.',\n    'recycle_bin_deleted_item' => 'Διαγραμμένο στοιχείο',\n    'recycle_bin_deleted_parent' => 'Γονικό Στοιχείο',\n    'recycle_bin_deleted_by' => 'Διαγράφηκε από',\n    'recycle_bin_deleted_at' => 'Ημ/νια - Ώρα Διαγραφής',\n    'recycle_bin_permanently_delete' => 'Οριστική Διαγραφή',\n    'recycle_bin_restore' => 'Επαναφορά',\n    'recycle_bin_contents_empty' => 'Ο κάδος ανακύκλωσης είναι επί του παρόντος άδειος',\n    'recycle_bin_empty' => 'Αδειάστε τον Κάδο Ανακύκλωσης',\n    'recycle_bin_empty_confirm' => 'Αυτό θα καταστρέψει οριστικά όλα τα αντικείμενα στον κάδο ανακύκλωσης, συμπεριλαμβανομένου του περιεχομένου που περιέχεται σε κάθε αντικείμενο. Είστε βέβαιοι ότι θέλετε να αδειάσετε τον κάδο ανακύκλωσης;',\n    'recycle_bin_destroy_confirm' => 'Αυτή η ενέργεια θα διαγράψει οριστικά από το σύστημα αυτό το στοιχείο μαζί με τυχόν θυγατρικά, που αναφέρονται παρακάτω. Μετά την επιβεβαίωση της διαγραφής δε θα μπορείτε να επαναφέρετε αυτό το περιεχόμενο. Είστε βέβαιοι ότι θέλετε να διαγράψετε οριστικά αυτό το στοιχείο;',\n    'recycle_bin_destroy_list' => 'Αντικείμενα για καταστροφή',\n    'recycle_bin_restore_list' => 'Αντικείμενα για επαναφορά',\n    'recycle_bin_restore_confirm' => 'Αυτή η ενέργεια θα επαναφέρει το διαγραμμένο στοιχείο, συμπεριλαμβανομένων τυχόν θυγατρικών στοιχείων, στην αρχική τους θέση. Εάν η αρχική τοποθεσία έχει από τότε διαγραφεί και βρίσκεται τώρα στον κάδο ανακύκλωσης, θα πρέπει επίσης να αποκατασταθεί και το γονικό στοιχείο.',\n    'recycle_bin_restore_deleted_parent' => 'Το γονικό στοιχείο αυτού του στοιχείου έχει επίσης διαγραφεί. Αυτά θα παραμείνουν διαγραμμένα μέχρι να αποκατασταθεί και αυτός ο γονέας.',\n    'recycle_bin_restore_parent' => 'Επαναφορά Γονέα',\n    'recycle_bin_destroy_notification' => 'Διαγράφηκαν :count συνολικά αντικείμενα από τον κάδο ανακύκλωσης.',\n    'recycle_bin_restore_notification' => 'Επαναφέρθηκαν :count συνολικά αντικείμενα από τον κάδο ανακύκλωσης.',\n\n    // Audit Log\n    'audit' => 'Αρχείο Καταγραφής',\n    'audit_desc' => 'Αυτό το αρχείο καταγραφής ελέγχου ενεργειών, εμφανίζει μια λίστα δραστηριοτήτων που παρακολουθούνται στο σύστημα. Αυτή η λίστα δεν είναι φιλτραρισμένη σε αντίθεση με παρόμοιες λίστες δραστηριοτήτων στο σύστημα όπου εφαρμόζονται φίλτρα αδειών.',\n    'audit_event_filter' => 'Φίλτρο Συμβάντων',\n    'audit_event_filter_no_filter' => 'Χωρίς Φίλτρο',\n    'audit_deleted_item' => 'Διαγραμμένο στοιχείο',\n    'audit_deleted_item_name' => 'Ονομα: :name',\n    'audit_table_user' => 'Χρήστης',\n    'audit_table_event' => 'Συμβάν',\n    'audit_table_related' => 'Σχετικό Αντικείμενο ή Λεπτομέρεια',\n    'audit_table_ip' => 'Διεύθυνση IP',\n    'audit_table_date' => 'Ημερομηνία Δραστηριότητας',\n    'audit_date_from' => 'Εύρος Ημερομηνίας Από',\n    'audit_date_to' => 'Εύρος Ημερομηνίας Έως',\n\n    // Role Settings\n    'roles' => 'Ρόλοι',\n    'role_user_roles' => 'Ρόλοι Χρηστών',\n    'roles_index_desc' => 'Οι Ρόλοι χρησιμοποιούνται για την ομαδοποίηση των χρηστών και παρέχουν δικαιώματα για το σύστημα στα μέλη τους. Όταν ένας χρήστης είναι μέλος πολλαπλών Ρόλων, ο χρήστης θα κληρονομεί όλες τις ιδιότητες από όλους τους Ρόλους που ανήκει.',\n    'roles_x_users_assigned' => ':count εκχωρημένος χρήστης|:count εκχωρημένοι χρήστες',\n    'roles_x_permissions_provided' => ':count άδεια|:count άδειες',\n    'roles_assigned_users' => 'Εκχωρημένοι χρήστες',\n    'roles_permissions_provided' => 'Παρεχόμενα Δικαιώματα',\n    'role_create' => 'Δημιουργία νέου ρόλου',\n    'role_delete' => 'Διαγραφή Ρόλου',\n    'role_delete_confirm' => 'Αυτό θα διαγράψει τον ρόλο με το όνομα \\':roleName\\'.',\n    'role_delete_users_assigned' => 'Σε αυτόν τον ρόλο έχουν εκχωρηθεί :userCount χρήστες. Εάν θέλετε να μετεγκαταστήσετε τους χρήστες από αυτόν τον ρόλο, επιλέξτε έναν νέο ρόλο παρακάτω.',\n    'role_delete_no_migration' => \"Μην μεταφέρετε χρήστες\",\n    'role_delete_sure' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτόν τον ρόλο;',\n    'role_edit' => 'Επεξεργασία Ρόλου',\n    'role_details' => 'Λεπτομέρειες Ρόλου',\n    'role_name' => 'Όνομα Ρόλου',\n    'role_desc' => 'Σύντομη περιγραφή του Ρόλου',\n    'role_mfa_enforced' => 'Απαιτεί έλεγχο ταυτότητας πολλαπλών παραγόντων',\n    'role_external_auth_id' => 'Εξωτερικά αναγνωριστικά (IDs) ελέγχου ταυτότητας',\n    'role_system' => 'Δικαιώματα Συστήματος',\n    'role_manage_users' => 'Διαχείριση Χρηστών',\n    'role_manage_roles' => 'Διαχείριση Ρόλων και Δικαιωμάτων ρόλων',\n    'role_manage_entity_permissions' => 'Διαχειριστείτε όλα τα δικαιώματα βιβλίου, κεφαλαίων και σελίδων',\n    'role_manage_own_entity_permissions' => 'Διαχειριστείτε τα δικαιώματα στο δικό σας βιβλίο, κεφάλαιο και σελίδες',\n    'role_manage_page_templates' => 'Διαχείριση προτύπων σελίδων',\n    'role_access_api' => 'Πρόσβαση στο API του συστήματος',\n    'role_manage_settings' => 'Διαχειριστείτε τις ρυθμίσεις του ΑΡΙ',\n    'role_export_content' => 'Εξαγωγή περιεχομένου',\n    'role_import_content' => 'Εισαγωγή περιεχομένου',\n    'role_editor_change' => 'Αλλαγή προγράμματος επεξεργασίας σελίδας',\n    'role_notifications' => 'Λήψη & διαχείριση ειδοποιήσεων',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Δικαιώματα Συστήματος',\n    'roles_system_warning' => 'Λάβετε υπόψη ότι η πρόσβαση σε οποιοδήποτε από τις τρεις παραπάνω άδειες (δικαιώματα) μπορεί να επιτρέψει σε έναν χρήστη να αλλάξει τα δικά του προνόμια ή τα προνόμια άλλων στο σύστημα. Εκχωρήστε ρόλους με αυτά τα δικαιώματα μόνο σε αξιόπιστους χρήστες.',\n    'role_asset_desc' => 'Αυτά τα δικαιώματα ελέγχουν την προεπιλεγμένη πρόσβαση στα στοιχεία (άδειες) εντός του συστήματος. Τα δικαιώματα σε Βιβλία, Κεφάλαια και Σελίδες θα παρακάμψουν αυτές τις άδειες.',\n    'role_asset_admins' => 'Οι διαχειριστές έχουν αυτόματα πρόσβαση σε όλο το περιεχόμενο, αλλά αυτές οι επιλογές ενδέχεται να εμφανίζουν ή να αποκρύπτουν τις επιλογές διεπαφής χρήστη.',\n    'role_asset_image_view_note' => 'Αυτό σχετίζεται με την ορατότητα εντός του διαχειριστή εικόνων. Η πραγματική πρόσβαση των μεταφορτωμένων αρχείων εικόνας θα εξαρτηθεί από την επιλογή αποθήκευσης εικόνας συστήματος.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'Ολα',\n    'role_own' => 'Τα δικά του',\n    'role_controlled_by_asset' => 'Ελέγχονται από το στοιχείο στο οποίο ανεβαίνουν (Ράφια, Βιβλία)',\n    'role_save' => 'Αποθήκευση Ρόλου',\n    'role_users' => 'Χρήστες σε αυτόν τον Ρόλο',\n    'role_users_none' => 'Σε κανένα χρήστη δεν έχει ανατεθεί αυτήν τη στιγμή αυτός ο ρόλος.',\n\n    // Users\n    'users' => 'Χρήστες',\n    'users_index_desc' => 'Δημιουργία & διαχείριση μεμονωμένων λογαριασμών χρήστη μέσα στο σύστημα. Οι λογαριασμοί χρήστη χρησιμοποιούνται για τη σύνδεση και την απόδοση του περιεχομένου & δραστηριότητα. Τα δικαιώματα πρόσβασης βασίζονται κυρίως σε Ρόλους, αλλά η κυριότητα του περιεχομένου του χρήστη, μεταξύ άλλων παραγόντων, μπορεί επίσης να επηρεάσει τα δικαιώματα & την πρόσβαση.',\n    'user_profile' => 'Προφίλ Χρήστη',\n    'users_add_new' => 'Προσθήκη νέου Χρήστη',\n    'users_search' => 'Αναζήτηση Χρηστών',\n    'users_latest_activity' => 'Τελευταία Δραστηριότητα',\n    'users_details' => 'Στοιχεία χρήστη',\n    'users_details_desc' => 'Ορίστε ένα εμφανιζόμενο όνομα και μια διεύθυνση email για αυτόν τον χρήστη. Η διεύθυνση email θα χρησιμοποιηθεί για τη σύνδεση στην εφαρμογή.',\n    'users_details_desc_no_email' => 'Ορίστε το όνομα που θα εμφανίζεται για το χρήστη αυτόν, έτσι ώστε να είναι αναγνωρίσιμος από τους υπόλοιπους.',\n    'users_role' => 'Ρόλοι χρήστη',\n    'users_role_desc' => 'Επιλέξτε σε ποιους ρόλους θα εκχωρηθεί αυτός ο χρήστης. Εάν ένας χρήστης έχει εκχωρηθεί σε πολλούς ρόλους, τα δικαιώματα από αυτούς τους ρόλους θα στοιβάζονται και θα λαμβάνουν όλες τις ικανότητες των ρόλων που έχουν εκχωρηθεί.',\n    'users_password' => 'Κωδικός Χρήστη',\n    'users_password_desc' => 'Ορίστε έναν κωδικό πρόσβασης που θα χρησιμοποιείται για τη σύνδεση στην εφαρμογή. Αυτός πρέπει να είναι τουλάχιστον 8 χαρακτήρες.',\n    'users_send_invite_text' => 'Μπορείτε να επιλέξετε να στείλετε σε αυτόν τον χρήστη ένα email πρόσκλησης που του επιτρέπει να ορίσει τον δικό του κωδικό πρόσβασης. Σε διαφορετική περίπτωση μπορείτε να ορίσετε τον κωδικό πρόσβασής του εσείς.',\n    'users_send_invite_option' => 'Αποστολή email πρόσκλησης σε χρήστη',\n    'users_external_auth_id' => 'Εξωτερικός έλεγχος ταυτότητας',\n    'users_external_auth_id_desc' => 'Όταν χρησιμοποιείται ένα εξωτερικό σύστημα ελέγχου ταυτότητας (όπως SAML2, OIDC ή LDAP) αυτό είναι το αναγνωριστικό που συνδέει αυτόν τον χρήστη BookStack με τον λογαριασμό συστήματος ελέγχου ταυτότητας. Μπορείτε να αγνοήσετε αυτό το πεδίο αν χρησιμοποιείτε τον προεπιλεγμένο έλεγχο ταυτότητας μέσω email.',\n    'users_password_warning' => 'Συμπληρώστε τα παρακάτω μόνο αν θέλετε να αλλάξετε τον κωδικό πρόσβασης για αυτόν το χρήστη.',\n    'users_system_public' => 'Αυτός ο χρήστης αντιπροσωπεύει οποιονδήποτε επισκέπτη που επισκέπτεται τη Βιβλιοθήκη σας. Δεν μπορεί να χρησιμοποιηθεί για τη σύνδεση αλλά εκχωρείται αυτόματα.',\n    'users_delete' => 'Διαγραφή Χρήστη',\n    'users_delete_named' => 'Διαγραφή χρήστη :userName',\n    'users_delete_warning' => 'Αυτό θα διαγράψει πλήρως αυτόν τον χρήστη με το όνομα \\':userName\\' από το σύστημα.',\n    'users_delete_confirm' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτόν τον χρήστη;',\n    'users_migrate_ownership' => 'Μεταφορά ιδιοκτησίας',\n    'users_migrate_ownership_desc' => 'Επιλέξτε έναν χρήστη εδώ, εάν θέλετε ένας άλλος χρήστης να γίνει ο κάτοχος όλων των στοιχείων που ανήκουν επί του παρόντος σε αυτόν τον χρήστη.',\n    'users_none_selected' => 'Δεν έχει επιλεγεί χρήστης',\n    'users_edit' => 'Επεξεργασία Χρήστη',\n    'users_edit_profile' => 'Ρυθμίσεις προφίλ',\n    'users_avatar' => 'Avatar Χρήστη',\n    'users_avatar_desc' => 'Επιλέξτε μια εικόνα που θα αντιπροσωπεύει αυτόν τον χρήστη. Αυτό θα πρέπει να είναι περίπου 256px τετράγωνο.',\n    'users_preferred_language' => 'Προτιμώμενη γλώσσα',\n    'users_preferred_language_desc' => 'Αυτή η επιλογή θα αλλάξει τη γλώσσα που χρησιμοποιείται για τη διεπαφή χρήστη της εφαρμογής. Αυτό δεν θα επηρεάσει οποιοδήποτε περιεχόμενο που δημιουργήθηκε από χρήστες.',\n    'users_social_accounts' => 'Λογαριασμοί Κοινωνικής δικτύωσης ',\n    'users_social_accounts_desc' => 'Δείτε την κατάσταση των συνδεδεμένων λογαριασμών κοινωνικών δικτύων για αυτόν το χρήστη. Οι λογαριασμοί κοινωνικών δικτύων, μπορούν να χρησιμοποιηθούν επιπλέον του κύριου συστήματος ελέγχου ταυτότητας, για πρόσβαση στο σύστημα.',\n    'users_social_accounts_info' => 'Εδώ μπορείτε να συνδέσετε τους άλλους λογαριασμούς σας για ταχύτερη και ευκολότερη σύνδεση. Η αποσύνδεση ενός λογαριασμού εδώ δεν ανακαλεί προηγουμένως εξουσιοδοτημένη πρόσβαση. Ανάκληση πρόσβασης από τις ρυθμίσεις προφίλ σας στον συνδεδεμένο κοινωνικό λογαριασμό.',\n    'users_social_connect' => 'Σύνδεση λογαριασμού',\n    'users_social_disconnect' => 'Αποσύνδεση λογαριασμού',\n    'users_social_status_connected' => 'Συνδεδεμένο',\n    'users_social_status_disconnected' => 'Αποσυνδεδεμένο',\n    'users_social_connected' => ':socialΛογαριασμός λογαριασμού συνδέθηκε με επιτυχία στο προφίλ σας.',\n    'users_social_disconnected' => ':socialΛογαριασμός αποσυνδέθηκε επιτυχώς από το προφίλ σας.',\n    'users_api_tokens' => 'API Tokens',\n    'users_api_tokens_desc' => 'Δημιουργία και διαχείριση των διακριτικών πρόσβασης (token) που χρησιμοποιούνται για τον έλεγχο ταυτότητας με το REST API του BookStack. Τα δικαιώματα για το API τα διαχειρίζεται ο χρήστης στον οποίο ανήκει το διακριτικό (token).',\n    'users_api_tokens_none' => 'Δεν έχουν δημιουργηθεί διακριτικά API για αυτόν το χρήστη',\n    'users_api_tokens_create' => 'Δημιουργία διακριτικού  Api Token',\n    'users_api_tokens_expires' => 'Λήγει',\n    'users_api_tokens_docs' => 'Τεκμηρίωση API',\n    'users_mfa' => 'Έλεγχος Ταυτοτητας Πολλαπλων Παραγοντων',\n    'users_mfa_desc' => 'Ρυθμίστε τον έλεγχο ταυτότητας πολλαπλών παραγόντων ως ένα επιπλέον επίπεδο ασφάλειας για τον λογαριασμό χρήστη σας.',\n    'users_mfa_x_methods' => 'Έχει ρυθμιστεί :count μέθοδος|Έχουν ρυθμιστεί :count μέθοδοι',\n    'users_mfa_configure' => 'Ρύθμιση Μεθόδων',\n\n    // API Tokens\n    'user_api_token_create' => 'Δημιουργία διακριτικού (API Token)',\n    'user_api_token_name' => 'Όνομα',\n    'user_api_token_name_desc' => 'Δώστε στο διακριτικό σας ένα ευανάγνωστο όνομα ως μελλοντική υπενθύμιση του σκοπού του.',\n    'user_api_token_expiry' => 'Ημερομηνία λήξης',\n    'user_api_token_expiry_desc' => 'Ορίστε μια ημερομηνία κατά την οποία λήγει αυτό το διακριτικό. Μετά από αυτήν την ημερομηνία, τα αιτήματα που γίνονται με αυτό το διακριτικό δεν θα λειτουργούν πλέον. Αν αφήσετε αυτό το πεδίο κενό, θα οριστεί η λήξη 100 χρόνια στο μέλλον.',\n    'user_api_token_create_secret_message' => 'Αμέσως μετά τη δημιουργία αυτού του διακριτικού θα δημιουργηθεί και θα εμφανιστεί ένα \"Token ID\" & \"Token Secret\". Το μυστικό(Token Secret) θα εμφανιστεί μόνο μία φορά, επομένως φροντίστε να αντιγράψετε την τιμή σε κάποιο ασφαλές μέρος πριν συνεχίσετε.',\n    'user_api_token' => 'API Token',\n    'user_api_token_id' => 'Token ID',\n    'user_api_token_id_desc' => 'Αυτό είναι ένα μη επεξεργάσιμο αναγνωριστικό που δημιουργείται από το σύστημα για αυτό το διακριτικό, το οποίο θα πρέπει να παρέχεται σε αιτήματα API.',\n    'user_api_token_secret' => 'Μυστικό Token',\n    'user_api_token_secret_desc' => 'Αυτό είναι ένα μυστικό που δημιουργείται από το σύστημα για αυτό το διακριτικό και θα πρέπει να παρέχεται σε αιτήματα API. Αυτό θα εμφανιστεί μόνο μία φορά, επομένως αντιγράψτε αυτήν την τιμή σε κάποιο ασφαλές μέρος.',\n    'user_api_token_created' => 'Το Διακριτικό δημιουργήθηκε :timeAgo',\n    'user_api_token_updated' => 'Το Διακριτικό ενημερώθηκε :timeAgo',\n    'user_api_token_delete' => 'Διαγραφή Διακριτικού',\n    'user_api_token_delete_warning' => 'Αυτό θα διαγράψει πλήρως αυτό το διακριτικό API με το όνομα \\':tokenName\\' από το σύστημα.',\n    'user_api_token_delete_confirm' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το διακριτικό API;',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Τα Webhooks είναι ένας τρόπος αποστολής δεδομένων σε εξωτερικές διευθύνσεις URL όταν ορισμένες ενέργειες και συμβάντα συμβαίνουν στο σύστημα που επιτρέπει την ενσωμάτωση με εξωτερικές πλατφόρμες όπως συστήματα μηνυμάτων ή ειδοποιήσεων.',\n    'webhooks_x_trigger_events' => ':count συμβάν ενεργοποίησης|:count συμβάντα ενεργοποίησης',\n    'webhooks_create' => 'Δημιουργία νέου Webhook',\n    'webhooks_none_created' => 'Δεν έχουν δημιουργηθεί ακόμη webhook.',\n    'webhooks_edit' => 'Επεξεργασία Webhook',\n    'webhooks_save' => 'Αποθήκευση Webhook',\n    'webhooks_details' => 'Λεπτομέρειες Webhook',\n    'webhooks_details_desc' => 'Παρέχετε ένα φιλικό προς τον χρήστη όνομα και ένα τελικό σημείο POST ως τοποθεσία για την αποστολή των δεδομένων webhook.',\n    'webhooks_events' => 'Συμβάντα Webhook',\n    'webhooks_events_desc' => 'Επιλέξτε όλα τα συμβάντα που θα πρέπει να ενεργοποιήσουν αυτό το webhook για κλήση.',\n    'webhooks_events_warning' => 'Λάβετε υπόψη ότι αυτά τα συμβάντα θα ενεργοποιηθούν για όλα τα επιλεγμένα συμβάντα, ακόμη και αν εφαρμοστούν προσαρμοσμένα δικαιώματα. Βεβαιωθείτε ότι η χρήση αυτού του webhook δεν θα αποκαλύψει εμπιστευτικό περιεχόμενο.',\n    'webhooks_events_all' => 'Όλα τα συμβάντα του συστήματος',\n    'webhooks_name' => 'Όνομα Webhook',\n    'webhooks_timeout' => 'Χρονικό όριο λήξης αιτήματος Webhook (δευτερόλεπτα)',\n    'webhooks_endpoint' => 'Τελικό Σημείο Webhook',\n    'webhooks_active' => 'Webhook Ενεργό',\n    'webhook_events_table_header' => 'Συμβάντα',\n    'webhooks_delete' => 'Διαγραφή Webhook',\n    'webhooks_delete_warning' => 'Αυτό θα διαγράψει πλήρως αυτό το webhook, με το όνομα \\':webhookName\\', από το σύστημα.',\n    'webhooks_delete_confirm' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το webhook;',\n    'webhooks_format_example' => 'Παράδειγμα μορφής Webhook',\n    'webhooks_format_example_desc' => 'Τα δεδομένα Webhook αποστέλλονται ως αίτημα POST στο διαμορφωμένο τελικό σημείο ως JSON ακολουθώντας την παρακάτω μορφή. Οι ιδιότητες \"related_item\" και \"url\" είναι προαιρετικές και εξαρτώνται από τον τύπο του συμβάντος που ενεργοποιείται.',\n    'webhooks_status' => 'Κατάσταση Webhook',\n    'webhooks_last_called' => 'Τελευταία κλήση:',\n    'webhooks_last_errored' => 'Τελευταίο σφάλμα:',\n    'webhooks_last_error_message' => 'Τελευταίο μήνυμα λάθους:',\n\n    // Licensing\n    'licenses' => 'Άδειες',\n    'licenses_desc' => 'Αυτή η σελίδα αναφέρει λεπτομερώς τις πληροφορίες άδειας χρήσης για το BookStack επιπρόσθετα στα έργα και τις βιβλιοθήκες που χρησιμοποιούνται εντός του BookStack. Πολλά έργα που αναφέρονται μπορούν να χρησιμοποιηθούν μόνο σε ένα πλαίσιο ανάπτυξης.',\n    'licenses_bookstack' => 'Άδεια BookStack',\n    'licenses_php' => 'Άδειες Βιβλιοθήκης PHP',\n    'licenses_js' => 'Άδειες Βιβλιοθήκης JavaScript',\n    'licenses_other' => 'Άλλες άδειες',\n    'license_details' => 'Λεπτομέρειες άδειας',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/el/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => 'Το :attribute πρέπει να γίνει δεκτό.',\n    'active_url'           => 'Το :attribute δεν είναι ένα έγκυρο URL.',\n    'after'                => 'Το :attribute πρέπει να είναι μια ημερομηνία μετά τις :date.',\n    'alpha'                => 'Το :attribute μπορεί να περιέχει μόνο γράμματα.',\n    'alpha_dash'           => 'Tο :attribute μπορεί να περιλαμβάνει μόνο γράμματα, αριθμούς, παύλες και κάτω παύλες.',\n    'alpha_num'            => 'Tο :attribute μπορεί να περιλαμβάνει μόνο γράμματα και αριθμούς.',\n    'array'                => 'Το :attribute πρέπει να είναι πίνακας.',\n    'backup_codes'         => 'Ο παρεχόμενος κωδικός δεν είναι έγκυρος ή έχει ήδη χρησιμοποιηθεί.',\n    'before'               => 'Tο :attribute πρέπει να είναι μια ημερομηνία πριν από :date.',\n    'between'              => [\n        'numeric' => 'Το :attribute πρέπει να είναι μεταξύ :min και :max.',\n        'file'    => 'Το :attribute πρέπει να είναι μεταξύ :min και :max kilobytes.',\n        'string'  => 'Το πεδίο :attribute πρέπει να είναι μεταξύ από :min και :max characters.',\n        'array'   => 'Το πεδίο :attribute πρέπει να είναι μεταξύ :min και :max αντικείμενα.',\n    ],\n    'boolean'              => 'Το πεδίο :attribute πρέπει να είναι σωστό ή λάθος.',\n    'confirmed'            => 'Η επιβεβαίωση του :attribute δεν ταιριάζει.',\n    'date'                 => 'Το :attribute δεν έχει έγκυρη ημερομηνία.',\n    'date_format'          => 'Το :attribute δεν ταιριάζει με τη μορφή :format.',\n    'different'            => 'Τα πεδία :attribute και :other πρέπει να είναι διαφορετικά.',\n    'digits'               => 'Το πεδίο :attribute πρέπει να είναι :digits ψηφία.',\n    'digits_between'       => 'To :attribute πρέπει να είναι μεταξύ :min και :max ψηφία.',\n    'email'                => 'Το πεδίο :attribute πρέπει να είναι μία έγκυρη διεύθυνση E-mail.',\n    'ends_with' => 'Το :attribute πρέπει να τελειώνει με μια απο τις ακόλουθες: :values',\n    'file'                 => 'Το :attribute πρέπει να παρέχεται ως έγκυρο αρχείο.',\n    'filled'               => 'Το πεδίο :attribute είναι υποχρεωτικό.',\n    'gt'                   => [\n        'numeric' => 'Το :attribute πρέπει να είναι μεγαλύτερο από :value.',\n        'file'    => 'To :attribute πρέπει να είναι μεγαλύτερο από :value kilobytes.',\n        'string'  => 'Tο :attribute πρέπει να έχει περισσότερους από :value χαρακτήρες.',\n        'array'   => 'Το :attribute πρέπει να περιέχει περισσότερα από :value αντικείμενα.',\n    ],\n    'gte'                  => [\n        'numeric' => 'Το :attribute πρέπει να είναι μεγαλύτερο ή ίσο από :value.',\n        'file'    => 'Το :attribute πρέπει να είναι μεγαλύτερο ή ίσο με :value kilobytes.',\n        'string'  => 'To :attribute πρέπει να είναι μεγαλύτερο ή ίσο από :value χαρακτήρες.',\n        'array'   => 'Tο :attribute πρέπει να έχει :value αντικείμενα ή περισσότερα.',\n    ],\n    'exists'               => 'Το επιλεγμένο :attribute δεν είναι έγκυρο.',\n    'image'                => 'Tο :attribute πρέπει να είναι εικόνα.',\n    'image_extension'      => 'Το πεδίο :attribute πρέπει να έχει μια έγκυρη & υποστηριζόμενη επέκταση εικόνας.',\n    'in'                   => 'Το επιλεγμένο :attribute δεν είναι έγκυρο.',\n    'integer'              => 'Tο :attribute πρέπει να είναι ακέραιος αριθμός.',\n    'ip'                   => 'Το πεδίο :attribute πρέπει να είναι μία έγκυρη διεύθυνση IP.',\n    'ipv4'                 => 'Tο :attribute πρέπει να είναι μια έγκυρη διεύθυνση IPv4.',\n    'ipv6'                 => 'Tο :attribute πρέπει να είναι μια έγκυρη διεύθυνση IPv6.',\n    'json'                 => 'H :attribute πρεπει να είναι μια έγκυρη συμβολοσειρά JSON.',\n    'lt'                   => [\n        'numeric' => 'Tο :attribute πρέπει να είναι λιγότερο από :value.',\n        'file'    => 'To :attribute πρέπει να είναι μικρότερο από :value kilobytes.',\n        'string'  => 'To :attribute πρέπει να είναι μικρότερο από :value kilobytes.',\n        'array'   => 'Tο :attribute πρέπει να έχει λιγότερα από :value αντικείμενα.',\n    ],\n    'lte'                  => [\n        'numeric' => 'Το :attribute πρέπει να είναι μικρότερο ή ίσο του :value.',\n        'file'    => 'Το :attribute πρέπει να είναι μικρότερο ή ίσο του :value kilobytes.',\n        'string'  => 'Tο :attribute πρέπει να έχει λιγότερους από ή ίδιους :value χαρακτήρες.',\n        'array'   => 'Tο :attribute δεν πρέπει να έχει περισσότερα από :value αντικείμενα.',\n    ],\n    'max'                  => [\n        'numeric' => 'Tο :attribute δεν μπορεί να είναι μεγαλύτερο από :max.',\n        'file'    => 'To :attribute δεν μπορεί να είναι μεγαλύτερο από :max kilobytes.',\n        'string'  => 'Το :attribute δεν μπορεί να είναι μεγαλύτερο από :max χαρακτήρες.',\n        'array'   => 'Tο :attribute δεν μπορεί να έχει περισσότερα από :max αντικείμενα.',\n    ],\n    'mimes'                => 'Το πεδίο :attribute πρέπει να είναι ένα αρχείου τύπου: :values.',\n    'min'                  => [\n        'numeric' => 'To :attribute πρέπει να είναι τουλάχιστον :min.',\n        'file'    => 'Το :attribute πρέπει είναι τουλάχιστον :min kilobytes.',\n        'string'  => 'Το :attribute πρέπει να είναι τουλάχιστον :min χαρακτήρες.',\n        'array'   => 'To :attribute πρέπει να έχει τουλάχιστον :min αντικείμενα.',\n    ],\n    'not_in'               => 'Το επιλεγμένο :attribute δεν είναι έγκυρο.',\n    'not_regex'            => 'Η μορφή του :attribute δεν είναι έγκυρη.',\n    'numeric'              => 'To :attribute πρέπει να είναι αριθμός.',\n    'regex'                => 'Το :attribute έχει μη έγκυρη μορφή.',\n    'required'             => 'Το πεδίο :attribute είναι υποχρεωτικό.',\n    'required_if'          => 'To πεδίο :attribute είναι απαραίτητο εκτός αν :other είναι σε :values.',\n    'required_with'        => 'To πεδίο :attribute είναι απαραίτητο όταν υπάρχουν οι :values.',\n    'required_with_all'    => 'To πεδίο :attribute είναι απαραίτητο όταν υπάρχουν οι :values.',\n    'required_without'     => 'To πεδίο :attribute είναι απαραίτητο όταν δεν υπάρχουν οι :values.',\n    'required_without_all' => 'To πεδίο :attribute είναι απαραίτητο όταν δεν υπάρχουν καμία από :values.',\n    'same'                 => 'Το πεδίο :attribute και :other πρέπει να είναι ίδια.',\n    'safe_url'             => 'Ο παρεχόμενος σύνδεσμος μπορεί να μην είναι ασφαλής.',\n    'size'                 => [\n        'numeric' => 'Το :attribute πρέπει να είναι :size.',\n        'file'    => 'Το :attribute πρέπει να έχει μέγεθος :size kilobytes.',\n        'string'  => 'Το πεδίο :attribute πρέπει να είναι :size χαρακτήρες.',\n        'array'   => 'Το πεδίο :attribute πρέπει να περιέχει :size αντικείμενα.',\n    ],\n    'string'               => 'Το :attribute πρέπει να είναι συμβολοσειρά.',\n    'timezone'             => 'Το πεδίο :attribute πρέπει να είναι μία έγκυρη ζώνη ώρας.',\n    'totp'                 => 'Ο παρεχόμενος κωδικός δεν είναι έγκυρος ή έχει λήξει.',\n    'unique'               => 'Το πεδίο :attribute έχει ήδη χρησιμοποιηθεί.',\n    'url'                  => 'Η μορφή του :attribute δεν είναι έγκυρη.',\n    'uploaded'             => 'Δεν ήταν δυνατή η αποστολή του αρχείου. Ο διακομιστής ενδέχεται να μην δέχεται αρχεία αυτού του μεγέθους.',\n\n    'zip_file' => 'Το :attribute πρέπει να παραπέμπει σε ένα αρχείο εντός του ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'Το :attribute πρέπει να αναφέρεται σε αρχείο τύπου :validTypes, βρέθηκε :foundType.',\n    'zip_model_expected' => 'Αναμενόταν αντικείμενο δεδομένων, αλλά \":type\" βρέθηκε.',\n    'zip_unique' => 'Το :attribute πρέπει να είναι μοναδικό για τον τύπο αντικειμένου εντός του ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Απαιτείται επιβεβαίωση κωδικού πρόσβασης',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/en/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'created page',\n    'page_create_notification'    => 'Page successfully created',\n    'page_update'                 => 'updated page',\n    'page_update_notification'    => 'Page successfully updated',\n    'page_delete'                 => 'deleted page',\n    'page_delete_notification'    => 'Page successfully deleted',\n    'page_restore'                => 'restored page',\n    'page_restore_notification'   => 'Page successfully restored',\n    'page_move'                   => 'moved page',\n    'page_move_notification'      => 'Page successfully moved',\n\n    // Chapters\n    'chapter_create'              => 'created chapter',\n    'chapter_create_notification' => 'Chapter successfully created',\n    'chapter_update'              => 'updated chapter',\n    'chapter_update_notification' => 'Chapter successfully updated',\n    'chapter_delete'              => 'deleted chapter',\n    'chapter_delete_notification' => 'Chapter successfully deleted',\n    'chapter_move'                => 'moved chapter',\n    'chapter_move_notification' => 'Chapter successfully moved',\n\n    // Books\n    'book_create'                 => 'created book',\n    'book_create_notification'    => 'Book successfully created',\n    'book_create_from_chapter'              => 'converted chapter to book',\n    'book_create_from_chapter_notification' => 'Chapter successfully converted to a book',\n    'book_update'                 => 'updated book',\n    'book_update_notification'    => 'Book successfully updated',\n    'book_delete'                 => 'deleted book',\n    'book_delete_notification'    => 'Book successfully deleted',\n    'book_sort'                   => 'sorted book',\n    'book_sort_notification'      => 'Book successfully re-sorted',\n\n    // Bookshelves\n    'bookshelf_create'            => 'created shelf',\n    'bookshelf_create_notification'    => 'Shelf successfully created',\n    'bookshelf_create_from_book'    => 'converted book to shelf',\n    'bookshelf_create_from_book_notification'    => 'Book successfully converted to a shelf',\n    'bookshelf_update'                 => 'updated shelf',\n    'bookshelf_update_notification'    => 'Shelf successfully updated',\n    'bookshelf_delete'                 => 'deleted shelf',\n    'bookshelf_delete_notification'    => 'Shelf successfully deleted',\n\n    // Revisions\n    'revision_restore' => 'restored revision',\n    'revision_delete' => 'deleted revision',\n    'revision_delete_notification' => 'Revision successfully deleted',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" has been added to your favourites',\n    'favourite_remove_notification' => '\":name\" has been removed from your favourites',\n\n    // Watching\n    'watch_update_level_notification' => 'Watch preferences successfully updated',\n\n    // Auth\n    'auth_login' => 'logged in',\n    'auth_register' => 'registered as new user',\n    'auth_password_reset_request' => 'requested user password reset',\n    'auth_password_reset_update' => 'reset user password',\n    'mfa_setup_method' => 'configured MFA method',\n    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',\n    'mfa_remove_method' => 'removed MFA method',\n    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',\n\n    // Settings\n    'settings_update' => 'updated settings',\n    'settings_update_notification' => 'Settings successfully updated',\n    'maintenance_action_run' => 'ran maintenance action',\n\n    // Webhooks\n    'webhook_create' => 'created webhook',\n    'webhook_create_notification' => 'Webhook successfully created',\n    'webhook_update' => 'updated webhook',\n    'webhook_update_notification' => 'Webhook successfully updated',\n    'webhook_delete' => 'deleted webhook',\n    'webhook_delete_notification' => 'Webhook successfully deleted',\n\n    // Imports\n    'import_create' => 'created import',\n    'import_create_notification' => 'Import successfully uploaded',\n    'import_run' => 'updated import',\n    'import_run_notification' => 'Content successfully imported',\n    'import_delete' => 'deleted import',\n    'import_delete_notification' => 'Import successfully deleted',\n\n    // Users\n    'user_create' => 'created user',\n    'user_create_notification' => 'User successfully created',\n    'user_update' => 'updated user',\n    'user_update_notification' => 'User successfully updated',\n    'user_delete' => 'deleted user',\n    'user_delete_notification' => 'User successfully removed',\n\n    // API Tokens\n    'api_token_create' => 'created API token',\n    'api_token_create_notification' => 'API token successfully created',\n    'api_token_update' => 'updated API token',\n    'api_token_update_notification' => 'API token successfully updated',\n    'api_token_delete' => 'deleted API token',\n    'api_token_delete_notification' => 'API token successfully deleted',\n\n    // Roles\n    'role_create' => 'created role',\n    'role_create_notification' => 'Role successfully created',\n    'role_update' => 'updated role',\n    'role_update_notification' => 'Role successfully updated',\n    'role_delete' => 'deleted role',\n    'role_delete_notification' => 'Role successfully deleted',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'emptied recycle bin',\n    'recycle_bin_restore' => 'restored from recycle bin',\n    'recycle_bin_destroy' => 'removed from recycle bin',\n\n    // Comments\n    'commented_on'                => 'commented on',\n    'comment_create'              => 'added comment',\n    'comment_update'              => 'updated comment',\n    'comment_delete'              => 'deleted comment',\n\n    // Sort Rules\n    'sort_rule_create' => 'created sort rule',\n    'sort_rule_create_notification' => 'Sort rule successfully created',\n    'sort_rule_update' => 'updated sort rule',\n    'sort_rule_update_notification' => 'Sort rule successfully updated',\n    'sort_rule_delete' => 'deleted sort rule',\n    'sort_rule_delete_notification' => 'Sort rule successfully deleted',\n\n    // Other\n    'permissions_update'          => 'updated permissions',\n];\n"
  },
  {
    "path": "lang/en/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'These credentials do not match our records.',\n    'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',\n\n    // Login & Register\n    'sign_up' => 'Sign up',\n    'log_in' => 'Log in',\n    'log_in_with' => 'Login with :socialDriver',\n    'sign_up_with' => 'Sign up with :socialDriver',\n    'logout' => 'Logout',\n\n    'name' => 'Name',\n    'username' => 'Username',\n    'email' => 'Email',\n    'password' => 'Password',\n    'password_confirm' => 'Confirm Password',\n    'password_hint' => 'Must be at least 8 characters',\n    'forgot_password' => 'Forgot Password?',\n    'remember_me' => 'Remember Me',\n    'ldap_email_hint' => 'Please enter an email to use for this account.',\n    'create_account' => 'Create Account',\n    'already_have_account' => 'Already have an account?',\n    'dont_have_account' => 'Don\\'t have an account?',\n    'social_login' => 'Social Login',\n    'social_registration' => 'Social Registration',\n    'social_registration_text' => 'Register and sign in using another service.',\n\n    'register_thanks' => 'Thanks for registering!',\n    'register_confirm' => 'Please check your email and click the confirmation button to access :appName.',\n    'registrations_disabled' => 'Registrations are currently disabled',\n    'registration_email_domain_invalid' => 'That email domain does not have access to this application',\n    'register_success' => 'Thanks for signing up! You are now registered and signed in.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Attempting Login',\n    'auto_init_starting_desc' => 'We\\'re contacting your authentication system to start the login process. If there\\'s no progress after 5 seconds you can try clicking the link below.',\n    'auto_init_start_link' => 'Proceed with authentication',\n\n    // Password Reset\n    'reset_password' => 'Reset Password',\n    'reset_password_send_instructions' => 'Enter your email below and you will be sent an email with a password reset link.',\n    'reset_password_send_button' => 'Send Reset Link',\n    'reset_password_sent' => 'A password reset link will be sent to :email if that email address is found in the system.',\n    'reset_password_success' => 'Your password has been successfully reset.',\n    'email_reset_subject' => 'Reset your :appName password',\n    'email_reset_text' => 'You are receiving this email because we received a password reset request for your account.',\n    'email_reset_not_requested' => 'If you did not request a password reset, no further action is required.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Confirm your email on :appName',\n    'email_confirm_greeting' => 'Thanks for joining :appName!',\n    'email_confirm_text' => 'Please confirm your email address by clicking the button below:',\n    'email_confirm_action' => 'Confirm Email',\n    'email_confirm_send_error' => 'Email confirmation required but the system could not send the email. Contact the admin to ensure email is set up correctly.',\n    'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',\n    'email_confirm_resent' => 'Confirmation email resent, Please check your inbox.',\n    'email_confirm_thanks' => 'Thanks for confirming!',\n    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the \"Continue\" link below to proceed.',\n\n    'email_not_confirmed' => 'Email Address Not Confirmed',\n    'email_not_confirmed_text' => 'Your email address has not yet been confirmed.',\n    'email_not_confirmed_click_link' => 'Please click the link in the email that was sent shortly after you registered.',\n    'email_not_confirmed_resend' => 'If you cannot find the email you can re-send the confirmation email by submitting the form below.',\n    'email_not_confirmed_resend_button' => 'Resend Confirmation Email',\n\n    // User Invite\n    'user_invite_email_subject' => 'You have been invited to join :appName!',\n    'user_invite_email_greeting' => 'An account has been created for you on :appName.',\n    'user_invite_email_text' => 'Click the button below to set an account password and gain access:',\n    'user_invite_email_action' => 'Set Account Password',\n    'user_invite_page_welcome' => 'Welcome to :appName!',\n    'user_invite_page_text' => 'To finalise your account and gain access you need to set a password which will be used to log-in to :appName on future visits.',\n    'user_invite_page_confirm_button' => 'Confirm Password',\n    'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Setup Multi-Factor Authentication',\n    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'mfa_setup_configured' => 'Already configured',\n    'mfa_setup_reconfigure' => 'Reconfigure',\n    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',\n    'mfa_setup_action' => 'Setup',\n    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',\n    'mfa_option_totp_title' => 'Mobile App',\n    'mfa_option_totp_desc' => 'To use multi-factor authentication you\\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Backup Codes',\n    'mfa_option_backup_codes_desc' => 'Generates a set of one-time-use backup codes which you\\'ll enter on login to verify your identity. Make sure to store these in a safe & secure place.',\n    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',\n    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',\n    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\\'ll be able to use one of the codes as a second authentication mechanism.',\n    'mfa_gen_backup_codes_download' => 'Download Codes',\n    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',\n    'mfa_gen_totp_title' => 'Mobile App Setup',\n    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',\n    'mfa_gen_totp_verify_setup' => 'Verify Setup',\n    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',\n    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',\n    'mfa_verify_access' => 'Verify Access',\n    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\\'re granted access. Verify using one of your configured methods to continue.',\n    'mfa_verify_no_methods' => 'No Methods Configured',\n    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\\'ll need to set up at least one method before you gain access.',\n    'mfa_verify_use_totp' => 'Verify using a mobile app',\n    'mfa_verify_use_backup_codes' => 'Verify using a backup code',\n    'mfa_verify_backup_code' => 'Backup Code',\n    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',\n    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',\n    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',\n    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',\n];\n"
  },
  {
    "path": "lang/en/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Cancel',\n    'close' => 'Close',\n    'confirm' => 'Confirm',\n    'back' => 'Back',\n    'save' => 'Save',\n    'continue' => 'Continue',\n    'select' => 'Select',\n    'toggle_all' => 'Toggle All',\n    'more' => 'More',\n\n    // Form Labels\n    'name' => 'Name',\n    'description' => 'Description',\n    'role' => 'Role',\n    'cover_image' => 'Cover image',\n    'cover_image_description' => 'This image should be approximately 440x250px although it will be flexibly scaled & cropped to fit the user interface in different scenarios as required, so actual dimensions for display will differ.',\n\n    // Actions\n    'actions' => 'Actions',\n    'view' => 'View',\n    'view_all' => 'View All',\n    'new' => 'New',\n    'create' => 'Create',\n    'update' => 'Update',\n    'edit' => 'Edit',\n    'archive' => 'Archive',\n    'unarchive' => 'Un-Archive',\n    'sort' => 'Sort',\n    'move' => 'Move',\n    'copy' => 'Copy',\n    'reply' => 'Reply',\n    'delete' => 'Delete',\n    'delete_confirm' => 'Confirm Deletion',\n    'search' => 'Search',\n    'search_clear' => 'Clear Search',\n    'reset' => 'Reset',\n    'remove' => 'Remove',\n    'add' => 'Add',\n    'configure' => 'Configure',\n    'manage' => 'Manage',\n    'fullscreen' => 'Fullscreen',\n    'favourite' => 'Favourite',\n    'unfavourite' => 'Unfavourite',\n    'next' => 'Next',\n    'previous' => 'Previous',\n    'filter_active' => 'Active Filter:',\n    'filter_clear' => 'Clear Filter',\n    'download' => 'Download',\n    'open_in_tab' => 'Open in Tab',\n    'open' => 'Open',\n\n    // Sort Options\n    'sort_options' => 'Sort Options',\n    'sort_direction_toggle' => 'Sort Direction Toggle',\n    'sort_ascending' => 'Sort Ascending',\n    'sort_descending' => 'Sort Descending',\n    'sort_name' => 'Name',\n    'sort_default' => 'Default',\n    'sort_created_at' => 'Created Date',\n    'sort_updated_at' => 'Updated Date',\n\n    // Misc\n    'deleted_user' => 'Deleted User',\n    'no_activity' => 'No activity to show',\n    'no_items' => 'No items available',\n    'back_to_top' => 'Back to top',\n    'skip_to_main_content' => 'Skip to main content',\n    'toggle_details' => 'Toggle Details',\n    'toggle_thumbnails' => 'Toggle Thumbnails',\n    'details' => 'Details',\n    'grid_view' => 'Grid View',\n    'list_view' => 'List View',\n    'default' => 'Default',\n    'breadcrumb' => 'Breadcrumb',\n    'status' => 'Status',\n    'status_active' => 'Active',\n    'status_inactive' => 'Inactive',\n    'never' => 'Never',\n    'none' => 'None',\n\n    // Header\n    'homepage' => 'Homepage',\n    'header_menu_expand' => 'Expand Header Menu',\n    'profile_menu' => 'Profile Menu',\n    'view_profile' => 'View Profile',\n    'edit_profile' => 'Edit Profile',\n    'dark_mode' => 'Dark Mode',\n    'light_mode' => 'Light Mode',\n    'global_search' => 'Global Search',\n\n    // Layout tabs\n    'tab_info' => 'Info',\n    'tab_info_label' => 'Tab: Show Secondary Information',\n    'tab_content' => 'Content',\n    'tab_content_label' => 'Tab: Show Primary Content',\n\n    // Email Content\n    'email_action_help' => 'If you’re having trouble clicking the \":actionText\" button, copy and paste the URL below into your web browser:',\n    'email_rights' => 'All rights reserved',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Privacy Policy',\n    'terms_of_service' => 'Terms of Service',\n\n    // OpenSearch\n    'opensearch_description' => 'Search :appName',\n];\n"
  },
  {
    "path": "lang/en/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Image Select',\n    'image_list' => 'Image List',\n    'image_details' => 'Image Details',\n    'image_upload' => 'Upload Image',\n    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',\n    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the \"Upload Image\" button above.',\n    'image_all' => 'All',\n    'image_all_title' => 'View all images',\n    'image_book_title' => 'View images uploaded to this book',\n    'image_page_title' => 'View images uploaded to this page',\n    'image_search_hint' => 'Search by image name',\n    'image_uploaded' => 'Uploaded :uploadedDate',\n    'image_uploaded_by' => 'Uploaded by :userName',\n    'image_uploaded_to' => 'Uploaded to :pageLink',\n    'image_updated' => 'Updated :updateDate',\n    'image_load_more' => 'Load More',\n    'image_image_name' => 'Image Name',\n    'image_delete_used' => 'This image is used in the pages below.',\n    'image_delete_confirm_text' => 'Are you sure you want to delete this image?',\n    'image_select_image' => 'Select Image',\n    'image_dropzone' => 'Drop images or click here to upload',\n    'image_dropzone_drop' => 'Drop images here to upload',\n    'images_deleted' => 'Images Deleted',\n    'image_preview' => 'Image Preview',\n    'image_upload_success' => 'Image uploaded successfully',\n    'image_update_success' => 'Image details successfully updated',\n    'image_delete_success' => 'Image successfully deleted',\n    'image_replace' => 'Replace Image',\n    'image_replace_success' => 'Image file successfully updated',\n    'image_rebuild_thumbs' => 'Regenerate Size Variations',\n    'image_rebuild_thumbs_success' => 'Image size variations successfully rebuilt!',\n\n    // Code Editor\n    'code_editor' => 'Edit Code',\n    'code_language' => 'Code Language',\n    'code_content' => 'Code Content',\n    'code_session_history' => 'Session History',\n    'code_save' => 'Save Code',\n];\n"
  },
  {
    "path": "lang/en/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'General',\n    'advanced' => 'Advanced',\n    'none' => 'None',\n    'cancel' => 'Cancel',\n    'save' => 'Save',\n    'close' => 'Close',\n    'apply' => 'Apply',\n    'undo' => 'Undo',\n    'redo' => 'Redo',\n    'left' => 'Left',\n    'center' => 'Center',\n    'right' => 'Right',\n    'top' => 'Top',\n    'middle' => 'Middle',\n    'bottom' => 'Bottom',\n    'width' => 'Width',\n    'height' => 'Height',\n    'More' => 'More',\n    'select' => 'Select...',\n\n    // Toolbar\n    'formats' => 'Formats',\n    'header_large' => 'Large Header',\n    'header_medium' => 'Medium Header',\n    'header_small' => 'Small Header',\n    'header_tiny' => 'Tiny Header',\n    'paragraph' => 'Paragraph',\n    'blockquote' => 'Blockquote',\n    'inline_code' => 'Inline code',\n    'callouts' => 'Callouts',\n    'callout_information' => 'Information',\n    'callout_success' => 'Success',\n    'callout_warning' => 'Warning',\n    'callout_danger' => 'Danger',\n    'bold' => 'Bold',\n    'italic' => 'Italic',\n    'underline' => 'Underline',\n    'strikethrough' => 'Strikethrough',\n    'superscript' => 'Superscript',\n    'subscript' => 'Subscript',\n    'text_color' => 'Text color',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Custom color',\n    'remove_color' => 'Remove color',\n    'background_color' => 'Background color',\n    'align_left' => 'Align left',\n    'align_center' => 'Align center',\n    'align_right' => 'Align right',\n    'align_justify' => 'Justify',\n    'list_bullet' => 'Bullet list',\n    'list_numbered' => 'Numbered list',\n    'list_task' => 'Task list',\n    'indent_increase' => 'Increase indent',\n    'indent_decrease' => 'Decrease indent',\n    'table' => 'Table',\n    'insert_image' => 'Insert image',\n    'insert_image_title' => 'Insert/Edit Image',\n    'insert_link' => 'Insert/edit link',\n    'insert_link_title' => 'Insert/Edit Link',\n    'insert_horizontal_line' => 'Insert horizontal line',\n    'insert_code_block' => 'Insert code block',\n    'edit_code_block' => 'Edit code block',\n    'insert_drawing' => 'Insert/edit drawing',\n    'drawing_manager' => 'Drawing manager',\n    'insert_media' => 'Insert/edit media',\n    'insert_media_title' => 'Insert/Edit Media',\n    'clear_formatting' => 'Clear formatting',\n    'source_code' => 'Source code',\n    'source_code_title' => 'Source Code',\n    'fullscreen' => 'Fullscreen',\n    'image_options' => 'Image options',\n\n    // Tables\n    'table_properties' => 'Table properties',\n    'table_properties_title' => 'Table Properties',\n    'delete_table' => 'Delete table',\n    'table_clear_formatting' => 'Clear table formatting',\n    'resize_to_contents' => 'Resize to contents',\n    'row_header' => 'Row header',\n    'insert_row_before' => 'Insert row before',\n    'insert_row_after' => 'Insert row after',\n    'delete_row' => 'Delete row',\n    'insert_column_before' => 'Insert column before',\n    'insert_column_after' => 'Insert column after',\n    'delete_column' => 'Delete column',\n    'table_cell' => 'Cell',\n    'table_row' => 'Row',\n    'table_column' => 'Column',\n    'cell_properties' => 'Cell properties',\n    'cell_properties_title' => 'Cell Properties',\n    'cell_type' => 'Cell type',\n    'cell_type_cell' => 'Cell',\n    'cell_scope' => 'Scope',\n    'cell_type_header' => 'Header cell',\n    'merge_cells' => 'Merge cells',\n    'split_cell' => 'Split cell',\n    'table_row_group' => 'Row Group',\n    'table_column_group' => 'Column Group',\n    'horizontal_align' => 'Horizontal align',\n    'vertical_align' => 'Vertical align',\n    'border_width' => 'Border width',\n    'border_style' => 'Border style',\n    'border_color' => 'Border color',\n    'row_properties' => 'Row properties',\n    'row_properties_title' => 'Row Properties',\n    'cut_row' => 'Cut row',\n    'copy_row' => 'Copy row',\n    'paste_row_before' => 'Paste row before',\n    'paste_row_after' => 'Paste row after',\n    'row_type' => 'Row type',\n    'row_type_header' => 'Header',\n    'row_type_body' => 'Body',\n    'row_type_footer' => 'Footer',\n    'alignment' => 'Alignment',\n    'cut_column' => 'Cut column',\n    'copy_column' => 'Copy column',\n    'paste_column_before' => 'Paste column before',\n    'paste_column_after' => 'Paste column after',\n    'cell_padding' => 'Cell padding',\n    'cell_spacing' => 'Cell spacing',\n    'caption' => 'Caption',\n    'show_caption' => 'Show caption',\n    'constrain' => 'Constrain proportions',\n    'cell_border_solid' => 'Solid',\n    'cell_border_dotted' => 'Dotted',\n    'cell_border_dashed' => 'Dashed',\n    'cell_border_double' => 'Double',\n    'cell_border_groove' => 'Groove',\n    'cell_border_ridge' => 'Ridge',\n    'cell_border_inset' => 'Inset',\n    'cell_border_outset' => 'Outset',\n    'cell_border_none' => 'None',\n    'cell_border_hidden' => 'Hidden',\n\n    // Images, links, details/summary & embed\n    'source' => 'Source',\n    'alt_desc' => 'Alternative description',\n    'embed' => 'Embed',\n    'paste_embed' => 'Paste your embed code below:',\n    'url' => 'URL',\n    'text_to_display' => 'Text to display',\n    'title' => 'Title',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Open link',\n    'open_link_in' => 'Open link in...',\n    'open_link_current' => 'Current window',\n    'open_link_new' => 'New window',\n    'remove_link' => 'Remove link',\n    'insert_collapsible' => 'Insert collapsible block',\n    'collapsible_unwrap' => 'Unwrap',\n    'edit_label' => 'Edit label',\n    'toggle_open_closed' => 'Toggle open/closed',\n    'collapsible_edit' => 'Edit collapsible block',\n    'toggle_label' => 'Toggle label',\n\n    // About view\n    'about' => 'About the editor',\n    'about_title' => 'About the WYSIWYG Editor',\n    'editor_license' => 'Editor License & Copyright',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under the MIT license.',\n    'editor_tiny_license_link' => 'The copyright and license details of TinyMCE can be found here.',\n    'save_continue' => 'Save Page & Continue',\n    'callouts_cycle' => '(Keep pressing to toggle through types)',\n    'link_selector' => 'Link to content',\n    'shortcuts' => 'Shortcuts',\n    'shortcut' => 'Shortcut',\n    'shortcuts_intro' => 'The following shortcuts are available in the editor:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Description',\n];\n"
  },
  {
    "path": "lang/en/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Recently Created',\n    'recently_created_pages' => 'Recently Created Pages',\n    'recently_updated_pages' => 'Recently Updated Pages',\n    'recently_created_chapters' => 'Recently Created Chapters',\n    'recently_created_books' => 'Recently Created Books',\n    'recently_created_shelves' => 'Recently Created Shelves',\n    'recently_update' => 'Recently Updated',\n    'recently_viewed' => 'Recently Viewed',\n    'recent_activity' => 'Recent Activity',\n    'create_now' => 'Create one now',\n    'revisions' => 'Revisions',\n    'meta_revision' => 'Revision #:revisionCount',\n    'meta_created' => 'Created :timeLength',\n    'meta_created_name' => 'Created :timeLength by :user',\n    'meta_updated' => 'Updated :timeLength',\n    'meta_updated_name' => 'Updated :timeLength by :user',\n    'meta_owned_name' => 'Owned by :user',\n    'meta_reference_count' => 'Referenced by :count item|Referenced by :count items',\n    'entity_select' => 'Entity Select',\n    'entity_select_lack_permission' => 'You don\\'t have the required permissions to select this item',\n    'images' => 'Images',\n    'my_recent_drafts' => 'My Recent Drafts',\n    'my_recently_viewed' => 'My Recently Viewed',\n    'my_most_viewed_favourites' => 'My Most Viewed Favourites',\n    'my_favourites' => 'My Favourites',\n    'no_pages_viewed' => 'You have not viewed any pages',\n    'no_pages_recently_created' => 'No pages have been recently created',\n    'no_pages_recently_updated' => 'No pages have been recently updated',\n    'export' => 'Export',\n    'export_html' => 'Contained Web File',\n    'export_pdf' => 'PDF File',\n    'export_text' => 'Plain Text File',\n    'export_md' => 'Markdown File',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Default Page Template',\n    'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',\n    'default_template_select' => 'Select a template page',\n    'import' => 'Import',\n    'import_validate' => 'Validate Import',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'Select ZIP file to upload',\n    'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',\n    'import_pending' => 'Pending Imports',\n    'import_pending_none' => 'No imports have been started.',\n    'import_continue' => 'Continue Import',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Run Import',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Uploaded by',\n    'import_location' => 'Import Location',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'Import Errors',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Permissions',\n    'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.',\n    'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.',\n    'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.',\n    'permissions_save' => 'Save Permissions',\n    'permissions_owner' => 'Owner',\n    'permissions_role_everyone_else' => 'Everyone Else',\n    'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',\n    'permissions_role_override' => 'Override permissions for role',\n    'permissions_inherit_defaults' => 'Inherit defaults',\n\n    // Search\n    'search_results' => 'Search Results',\n    'search_total_results_found' => ':count result found|:count total results found',\n    'search_clear' => 'Clear Search',\n    'search_no_pages' => 'No pages matched this search',\n    'search_for_term' => 'Search for :term',\n    'search_more' => 'More Results',\n    'search_advanced' => 'Advanced Search',\n    'search_terms' => 'Search Terms',\n    'search_content_type' => 'Content Type',\n    'search_exact_matches' => 'Exact Matches',\n    'search_tags' => 'Tag Searches',\n    'search_options' => 'Options',\n    'search_viewed_by_me' => 'Viewed by me',\n    'search_not_viewed_by_me' => 'Not viewed by me',\n    'search_permissions_set' => 'Permissions set',\n    'search_created_by_me' => 'Created by me',\n    'search_updated_by_me' => 'Updated by me',\n    'search_owned_by_me' => 'Owned by me',\n    'search_date_options' => 'Date Options',\n    'search_updated_before' => 'Updated before',\n    'search_updated_after' => 'Updated after',\n    'search_created_before' => 'Created before',\n    'search_created_after' => 'Created after',\n    'search_set_date' => 'Set Date',\n    'search_update' => 'Update Search',\n\n    // Shelves\n    'shelf' => 'Shelf',\n    'shelves' => 'Shelves',\n    'x_shelves' => ':count Shelf|:count Shelves',\n    'shelves_empty' => 'No shelves have been created',\n    'shelves_create' => 'Create New Shelf',\n    'shelves_popular' => 'Popular Shelves',\n    'shelves_new' => 'New Shelves',\n    'shelves_new_action' => 'New Shelf',\n    'shelves_popular_empty' => 'The most popular shelves will appear here.',\n    'shelves_new_empty' => 'The most recently created shelves will appear here.',\n    'shelves_save' => 'Save Shelf',\n    'shelves_books' => 'Books on this shelf',\n    'shelves_add_books' => 'Add books to this shelf',\n    'shelves_drag_books' => 'Drag books below to add them to this shelf',\n    'shelves_empty_contents' => 'This shelf has no books assigned to it',\n    'shelves_edit_and_assign' => 'Edit shelf to assign books',\n    'shelves_edit_named' => 'Edit Shelf :name',\n    'shelves_edit' => 'Edit Shelf',\n    'shelves_delete' => 'Delete Shelf',\n    'shelves_delete_named' => 'Delete Shelf :name',\n    'shelves_delete_explain' => \"This will delete the shelf with the name ':name'. Contained books will not be deleted.\",\n    'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',\n    'shelves_permissions' => 'Shelf Permissions',\n    'shelves_permissions_updated' => 'Shelf Permissions Updated',\n    'shelves_permissions_active' => 'Shelf Permissions Active',\n    'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',\n    'shelves_permissions_create' => 'Shelf create permissions are only used for copying permissions to child books using the action below. They do not control the ability to create books.',\n    'shelves_copy_permissions_to_books' => 'Copy Permissions to Books',\n    'shelves_copy_permissions' => 'Copy Permissions',\n    'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',\n    'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',\n\n    // Books\n    'book' => 'Book',\n    'books' => 'Books',\n    'x_books' => ':count Book|:count Books',\n    'books_empty' => 'No books have been created',\n    'books_popular' => 'Popular Books',\n    'books_recent' => 'Recent Books',\n    'books_new' => 'New Books',\n    'books_new_action' => 'New Book',\n    'books_popular_empty' => 'The most popular books will appear here.',\n    'books_new_empty' => 'The most recently created books will appear here.',\n    'books_create' => 'Create New Book',\n    'books_delete' => 'Delete Book',\n    'books_delete_named' => 'Delete Book :bookName',\n    'books_delete_explain' => 'This will delete the book with the name \\':bookName\\'. All pages and chapters will be removed.',\n    'books_delete_confirmation' => 'Are you sure you want to delete this book?',\n    'books_edit' => 'Edit Book',\n    'books_edit_named' => 'Edit Book :bookName',\n    'books_form_book_name' => 'Book Name',\n    'books_save' => 'Save Book',\n    'books_permissions' => 'Book Permissions',\n    'books_permissions_updated' => 'Book Permissions Updated',\n    'books_empty_contents' => 'No pages or chapters have been created for this book.',\n    'books_empty_create_page' => 'Create a new page',\n    'books_empty_sort_current_book' => 'Sort the current book',\n    'books_empty_add_chapter' => 'Add a chapter',\n    'books_permissions_active' => 'Book Permissions Active',\n    'books_search_this' => 'Search this book',\n    'books_navigation' => 'Book Navigation',\n    'books_sort' => 'Sort Book Contents',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Sort Book :bookName',\n    'books_sort_name' => 'Sort by Name',\n    'books_sort_created' => 'Sort by Created Date',\n    'books_sort_updated' => 'Sort by Updated Date',\n    'books_sort_chapters_first' => 'Chapters First',\n    'books_sort_chapters_last' => 'Chapters Last',\n    'books_sort_show_other' => 'Show Other Books',\n    'books_sort_save' => 'Save New Order',\n    'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.',\n    'books_sort_move_up' => 'Move Up',\n    'books_sort_move_down' => 'Move Down',\n    'books_sort_move_prev_book' => 'Move to Previous Book',\n    'books_sort_move_next_book' => 'Move to Next Book',\n    'books_sort_move_prev_chapter' => 'Move Into Previous Chapter',\n    'books_sort_move_next_chapter' => 'Move Into Next Chapter',\n    'books_sort_move_book_start' => 'Move to Start of Book',\n    'books_sort_move_book_end' => 'Move to End of Book',\n    'books_sort_move_before_chapter' => 'Move to Before Chapter',\n    'books_sort_move_after_chapter' => 'Move to After Chapter',\n    'books_copy' => 'Copy Book',\n    'books_copy_success' => 'Book successfully copied',\n\n    // Chapters\n    'chapter' => 'Chapter',\n    'chapters' => 'Chapters',\n    'x_chapters' => ':count Chapter|:count Chapters',\n    'chapters_popular' => 'Popular Chapters',\n    'chapters_new' => 'New Chapter',\n    'chapters_create' => 'Create New Chapter',\n    'chapters_delete' => 'Delete Chapter',\n    'chapters_delete_named' => 'Delete Chapter :chapterName',\n    'chapters_delete_explain' => 'This will delete the chapter with the name \\':chapterName\\'. All pages that exist within this chapter will also be deleted.',\n    'chapters_delete_confirm' => 'Are you sure you want to delete this chapter?',\n    'chapters_edit' => 'Edit Chapter',\n    'chapters_edit_named' => 'Edit Chapter :chapterName',\n    'chapters_save' => 'Save Chapter',\n    'chapters_move' => 'Move Chapter',\n    'chapters_move_named' => 'Move Chapter :chapterName',\n    'chapters_copy' => 'Copy Chapter',\n    'chapters_copy_success' => 'Chapter successfully copied',\n    'chapters_permissions' => 'Chapter Permissions',\n    'chapters_empty' => 'No pages are currently in this chapter.',\n    'chapters_permissions_active' => 'Chapter Permissions Active',\n    'chapters_permissions_success' => 'Chapter Permissions Updated',\n    'chapters_search_this' => 'Search this chapter',\n    'chapter_sort_book' => 'Sort Book',\n\n    // Pages\n    'page' => 'Page',\n    'pages' => 'Pages',\n    'x_pages' => ':count Page|:count Pages',\n    'pages_popular' => 'Popular Pages',\n    'pages_new' => 'New Page',\n    'pages_attachments' => 'Attachments',\n    'pages_navigation' => 'Page Navigation',\n    'pages_delete' => 'Delete Page',\n    'pages_delete_named' => 'Delete Page :pageName',\n    'pages_delete_draft_named' => 'Delete Draft Page :pageName',\n    'pages_delete_draft' => 'Delete Draft Page',\n    'pages_delete_success' => 'Page deleted',\n    'pages_delete_draft_success' => 'Draft page deleted',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'Are you sure you want to delete this page?',\n    'pages_delete_draft_confirm' => 'Are you sure you want to delete this draft page?',\n    'pages_editing_named' => 'Editing Page :pageName',\n    'pages_edit_draft_options' => 'Draft Options',\n    'pages_edit_save_draft' => 'Save Draft',\n    'pages_edit_draft' => 'Edit Page Draft',\n    'pages_editing_draft' => 'Editing Draft',\n    'pages_editing_page' => 'Editing Page',\n    'pages_edit_draft_save_at' => 'Draft saved at ',\n    'pages_edit_delete_draft' => 'Delete Draft',\n    'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.',\n    'pages_edit_discard_draft' => 'Discard Draft',\n    'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',\n    'pages_edit_switch_to_markdown_clean' => '(Clean Content)',\n    'pages_edit_switch_to_markdown_stable' => '(Stable Content)',\n    'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Set Changelog',\n    'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\\'ve made',\n    'pages_edit_enter_changelog' => 'Enter Changelog',\n    'pages_editor_switch_title' => 'Switch Editor',\n    'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',\n    'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',\n    'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',\n    'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',\n    'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\\'t persist across this change.',\n    'pages_save' => 'Save Page',\n    'pages_title' => 'Page Title',\n    'pages_name' => 'Page Name',\n    'pages_md_editor' => 'Editor',\n    'pages_md_preview' => 'Preview',\n    'pages_md_insert_image' => 'Insert Image',\n    'pages_md_insert_link' => 'Insert Entity Link',\n    'pages_md_insert_drawing' => 'Insert Drawing',\n    'pages_md_show_preview' => 'Show preview',\n    'pages_md_sync_scroll' => 'Sync preview scroll',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Unsaved Drawing Found',\n    'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',\n    'pages_not_in_chapter' => 'Page is not in a chapter',\n    'pages_move' => 'Move Page',\n    'pages_copy' => 'Copy Page',\n    'pages_copy_desination' => 'Copy Destination',\n    'pages_copy_success' => 'Page successfully copied',\n    'pages_permissions' => 'Page Permissions',\n    'pages_permissions_success' => 'Page permissions updated',\n    'pages_revision' => 'Revision',\n    'pages_revisions' => 'Page Revisions',\n    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',\n    'pages_revisions_named' => 'Page Revisions for :pageName',\n    'pages_revision_named' => 'Page Revision for :pageName',\n    'pages_revision_restored_from' => 'Restored from #:id; :summary',\n    'pages_revisions_created_by' => 'Created By',\n    'pages_revisions_date' => 'Revision Date',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Revision Number',\n    'pages_revisions_numbered' => 'Revision #:id',\n    'pages_revisions_numbered_changes' => 'Revision #:id Changes',\n    'pages_revisions_editor' => 'Editor Type',\n    'pages_revisions_changelog' => 'Changelog',\n    'pages_revisions_changes' => 'Changes',\n    'pages_revisions_current' => 'Current Version',\n    'pages_revisions_preview' => 'Preview',\n    'pages_revisions_restore' => 'Restore',\n    'pages_revisions_none' => 'This page has no revisions',\n    'pages_copy_link' => 'Copy Link',\n    'pages_edit_content_link' => 'Jump to section in editor',\n    'pages_pointer_enter_mode' => 'Enter section select mode',\n    'pages_pointer_label' => 'Page Section Options',\n    'pages_pointer_permalink' => 'Page Section Permalink',\n    'pages_pointer_include_tag' => 'Page Section Include Tag',\n    'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag',\n    'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink',\n    'pages_permissions_active' => 'Page Permissions Active',\n    'pages_initial_revision' => 'Initial publish',\n    'pages_references_update_revision' => 'System auto-update of internal links',\n    'pages_initial_name' => 'New Page',\n    'pages_editing_draft_notification' => 'You are currently editing a draft that was last saved :timeDiff.',\n    'pages_draft_edited_notification' => 'This page has been updated by since that time. It is recommended that you discard this draft.',\n    'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count users have started editing this page',\n        'start_b' => ':userName has started editing this page',\n        'time_a' => 'since the page was last updated',\n        'time_b' => 'in the last :minCount minutes',\n        'message' => ':start :time. Take care not to overwrite each other\\'s updates!',\n    ],\n    'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',\n    'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',\n    'pages_specific' => 'Specific Page',\n    'pages_is_template' => 'Page Template',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Toggle Sidebar',\n    'page_tags' => 'Page Tags',\n    'chapter_tags' => 'Chapter Tags',\n    'book_tags' => 'Book Tags',\n    'shelf_tags' => 'Shelf Tags',\n    'tag' => 'Tag',\n    'tags' =>  'Tags',\n    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',\n    'tag_name' =>  'Tag Name',\n    'tag_value' => 'Tag Value (Optional)',\n    'tags_explain' => \"Add some tags to better categorise your content. \\n You can assign a value to a tag for more in-depth organisation.\",\n    'tags_add' => 'Add another tag',\n    'tags_remove' => 'Remove this tag',\n    'tags_usages' => 'Total tag usages',\n    'tags_assigned_pages' => 'Assigned to Pages',\n    'tags_assigned_chapters' => 'Assigned to Chapters',\n    'tags_assigned_books' => 'Assigned to Books',\n    'tags_assigned_shelves' => 'Assigned to Shelves',\n    'tags_x_unique_values' => ':count unique values',\n    'tags_all_values' => 'All values',\n    'tags_view_tags' => 'View Tags',\n    'tags_view_existing_tags' => 'View existing tags',\n    'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',\n    'attachments' => 'Attachments',\n    'attachments_explain' => 'Upload some files or attach some links to display on your page. These are visible in the page sidebar.',\n    'attachments_explain_instant_save' => 'Changes here are saved instantly.',\n    'attachments_upload' => 'Upload File',\n    'attachments_link' => 'Attach Link',\n    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',\n    'attachments_set_link' => 'Set Link',\n    'attachments_delete' => 'Are you sure you want to delete this attachment?',\n    'attachments_dropzone' => 'Drop files here to upload',\n    'attachments_no_files' => 'No files have been uploaded',\n    'attachments_explain_link' => 'You can attach a link if you\\'d prefer not to upload a file. This can be a link to another page or a link to a file in the cloud.',\n    'attachments_link_name' => 'Link Name',\n    'attachment_link' => 'Attachment link',\n    'attachments_link_url' => 'Link to file',\n    'attachments_link_url_hint' => 'Url of site or file',\n    'attach' => 'Attach',\n    'attachments_insert_link' => 'Add Attachment Link to Page',\n    'attachments_edit_file' => 'Edit File',\n    'attachments_edit_file_name' => 'File Name',\n    'attachments_edit_drop_upload' => 'Drop files or click here to upload and overwrite',\n    'attachments_order_updated' => 'Attachment order updated',\n    'attachments_updated_success' => 'Attachment details updated',\n    'attachments_deleted' => 'Attachment deleted',\n    'attachments_file_uploaded' => 'File successfully uploaded',\n    'attachments_file_updated' => 'File successfully updated',\n    'attachments_link_attached' => 'Link successfully attached to page',\n    'templates' => 'Templates',\n    'templates_set_as_template' => 'Page is a template',\n    'templates_explain_set_as_template' => 'You can set this page as a template so its contents be utilized when creating other pages. Other users will be able to use this template if they have view permissions for this page.',\n    'templates_replace_content' => 'Replace page content',\n    'templates_append_content' => 'Append to page content',\n    'templates_prepend_content' => 'Prepend to page content',\n\n    // Profile View\n    'profile_user_for_x' => 'User for :time',\n    'profile_created_content' => 'Created Content',\n    'profile_not_created_pages' => ':userName has not created any pages',\n    'profile_not_created_chapters' => ':userName has not created any chapters',\n    'profile_not_created_books' => ':userName has not created any books',\n    'profile_not_created_shelves' => ':userName has not created any shelves',\n\n    // Comments\n    'comment' => 'Comment',\n    'comments' => 'Comments',\n    'comment_add' => 'Add Comment',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Leave a comment here',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Save Comment',\n    'comment_new' => 'New Comment',\n    'comment_created' => 'commented :createDiff',\n    'comment_updated' => 'Updated :updateDiff by :username',\n    'comment_updated_indicator' => 'Updated',\n    'comment_deleted_success' => 'Comment deleted',\n    'comment_created_success' => 'Comment added',\n    'comment_updated_success' => 'Comment updated',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Are you sure you want to delete this comment?',\n    'comment_in_reply_to' => 'In reply to :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',\n\n    // Revision\n    'revision_delete_confirm' => 'Are you sure you want to delete this revision?',\n    'revision_restore_confirm' => 'Are you sure you want to restore this revision? The current page contents will be replaced.',\n    'revision_cannot_delete_latest' => 'Cannot delete the latest revision.',\n\n    // Copy view\n    'copy_consider' => 'Please consider the below when copying content.',\n    'copy_consider_permissions' => 'Custom permission settings will not be copied.',\n    'copy_consider_owner' => 'You will become the owner of all copied content.',\n    'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',\n    'copy_consider_attachments' => 'Page attachments will not be copied.',\n    'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',\n\n    // Conversions\n    'convert_to_shelf' => 'Convert to Shelf',\n    'convert_to_shelf_contents_desc' => 'You can convert this book to a new shelf with the same contents. Chapters contained within this book will be converted to new books. If this book contains any pages, that are not in a chapter, this book will be renamed and contain such pages, and this book will become part of the new shelf.',\n    'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.',\n    'convert_book' => 'Convert Book',\n    'convert_book_confirm' => 'Are you sure you want to convert this book?',\n    'convert_undo_warning' => 'This cannot be as easily undone.',\n    'convert_to_book' => 'Convert to Book',\n    'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',\n    'convert_chapter' => 'Convert Chapter',\n    'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',\n\n    // References\n    'references' => 'References',\n    'references_none' => 'There are no tracked references to this item.',\n    'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',\n\n    // Watch Options\n    'watch' => 'Watch',\n    'watch_title_default' => 'Default Preferences',\n    'watch_desc_default' => 'Revert watching to just your default notification preferences.',\n    'watch_title_ignore' => 'Ignore',\n    'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.',\n    'watch_title_new' => 'New Pages',\n    'watch_desc_new' => 'Notify when any new page is created within this item.',\n    'watch_title_updates' => 'All Page Updates',\n    'watch_desc_updates' => 'Notify upon all new pages and page changes.',\n    'watch_desc_updates_page' => 'Notify upon all page changes.',\n    'watch_title_comments' => 'All Page Updates & Comments',\n    'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.',\n    'watch_desc_comments_page' => 'Notify upon page changes and new comments.',\n    'watch_change_default' => 'Change default notification preferences',\n    'watch_detail_ignore' => 'Ignoring notifications',\n    'watch_detail_new' => 'Watching for new pages',\n    'watch_detail_updates' => 'Watching new pages and updates',\n    'watch_detail_comments' => 'Watching new pages, updates & comments',\n    'watch_detail_parent_book' => 'Watching via parent book',\n    'watch_detail_parent_book_ignore' => 'Ignoring via parent book',\n    'watch_detail_parent_chapter' => 'Watching via parent chapter',\n    'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter',\n];\n"
  },
  {
    "path": "lang/en/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'You do not have permission to access the requested page.',\n    'permissionJson' => 'You do not have permission to perform the requested action.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'A user with the email :email already exists but with different credentials.',\n    'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',\n    'email_already_confirmed' => 'Email has already been confirmed, Try logging in.',\n    'email_confirmation_invalid' => 'This confirmation token is not valid or has already been used, Please try registering again.',\n    'email_confirmation_expired' => 'The confirmation token has expired, A new confirmation email has been sent.',\n    'email_confirmation_awaiting' => 'The email address for the account in use needs to be confirmed',\n    'ldap_fail_anonymous' => 'LDAP access failed using anonymous bind',\n    'ldap_fail_authed' => 'LDAP access failed using given dn & password details',\n    'ldap_extension_not_installed' => 'LDAP PHP extension not installed',\n    'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed',\n    'saml_already_logged_in' => 'Already logged in',\n    'saml_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',\n    'saml_invalid_response_id' => 'The request from the external authentication system is not recognised by a process started by this application. Navigating back after a login could cause this issue.',\n    'saml_fail_authed' => 'Login using :system failed, system did not provide successful authorization',\n    'oidc_already_logged_in' => 'Already logged in',\n    'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',\n    'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',\n    'social_no_action_defined' => 'No action defined',\n    'social_login_bad_response' => \"Error received during :socialAccount login: \\n:error\",\n    'social_account_in_use' => 'This :socialAccount account is already in use, Try logging in via the :socialAccount option.',\n    'social_account_email_in_use' => 'The email :email is already in use. If you already have an account you can connect your :socialAccount account from your profile settings.',\n    'social_account_existing' => 'This :socialAccount is already attached to your profile.',\n    'social_account_already_used_existing' => 'This :socialAccount account is already used by another user.',\n    'social_account_not_used' => 'This :socialAccount account is not linked to any users. Please attach it in your profile settings. ',\n    'social_account_register_instructions' => 'If you do not yet have an account, You can register an account using the :socialAccount option.',\n    'social_driver_not_found' => 'Social driver not found',\n    'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.',\n    'invite_token_expired' => 'This invitation link has expired. You can instead try to reset your account password.',\n    'login_user_not_found' => 'A user for this action could not be found.',\n\n    // System\n    'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.',\n    'cannot_get_image_from_url' => 'Cannot get image from :url',\n    'cannot_create_thumbs' => 'The server cannot create thumbnails. Please check you have the GD PHP extension installed.',\n    'server_upload_limit' => 'The server does not allow uploads of this size. Please try a smaller file size.',\n    'server_post_limit' => 'The server cannot receive the provided amount of data. Try again with less data or a smaller file.',\n    'uploaded'  => 'The server does not allow uploads of this size. Please try a smaller file size.',\n\n    // Drawing & Images\n    'image_upload_error' => 'An error occurred uploading the image',\n    'image_upload_type_error' => 'The image type being uploaded is invalid',\n    'image_upload_replace_type' => 'Image file replacements must be of the same type',\n    'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.',\n    'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.',\n    'image_gallery_thumbnail_memory_limit' => 'Failed to create gallery thumbnails due to system resource limits.',\n    'drawing_data_not_found' => 'Drawing data could not be loaded. The drawing file might no longer exist or you may not have permission to access it.',\n\n    // Attachments\n    'attachment_not_found' => 'Attachment not found',\n    'attachment_upload_error' => 'An error occurred uploading the attachment file',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page',\n    'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content',\n    'page_custom_home_deletion' => 'Cannot delete a page while it is set as a homepage',\n\n    // Entities\n    'entity_not_found' => 'Entity not found',\n    'bookshelf_not_found' => 'Shelf not found',\n    'book_not_found' => 'Book not found',\n    'page_not_found' => 'Page not found',\n    'chapter_not_found' => 'Chapter not found',\n    'selected_book_not_found' => 'The selected book was not found',\n    'selected_book_chapter_not_found' => 'The selected Book or Chapter was not found',\n    'guests_cannot_save_drafts' => 'Guests cannot save drafts',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'You cannot delete the only admin',\n    'users_cannot_delete_guest' => 'You cannot delete the guest user',\n    'users_could_not_send_invite' => 'Could not create user since invite email failed to send',\n\n    // Roles\n    'role_cannot_be_edited' => 'This role cannot be edited',\n    'role_system_cannot_be_deleted' => 'This role is a system role and cannot be deleted',\n    'role_registration_default_cannot_delete' => 'This role cannot be deleted while set as the default registration role',\n    'role_cannot_remove_only_admin' => 'This user is the only user assigned to the administrator role. Assign the administrator role to another user before attempting to remove it here.',\n\n    // Comments\n    'comment_list' => 'An error occurred while fetching the comments.',\n    'cannot_add_comment_to_draft' => 'You cannot add comments to a draft.',\n    'comment_add' => 'An error occurred while adding / updating the comment.',\n    'comment_delete' => 'An error occurred while deleting the comment.',\n    'empty_comment' => 'Cannot add an empty comment.',\n\n    // Error pages\n    '404_page_not_found' => 'Page Not Found',\n    'sorry_page_not_found' => 'Sorry, The page you were looking for could not be found.',\n    'sorry_page_not_found_permission_warning' => 'If you expected this page to exist, you might not have permission to view it.',\n    'image_not_found' => 'Image Not Found',\n    'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',\n    'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',\n    'return_home' => 'Return to home',\n    'error_occurred' => 'An Error Occurred',\n    'app_down' => ':appName is down right now',\n    'back_soon' => 'It will be back up soon.',\n\n    // Import\n    'import_zip_cant_read' => 'Could not read ZIP file.',\n    'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',\n    'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Import ZIP failed to validate with errors:',\n    'import_zip_failed_notification' => 'Failed to import ZIP file.',\n    'import_perms_books' => 'You are lacking the required permissions to create books.',\n    'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',\n    'import_perms_pages' => 'You are lacking the required permissions to create pages.',\n    'import_perms_images' => 'You are lacking the required permissions to create images.',\n    'import_perms_attachments' => 'You are lacking the required permission to create attachments.',\n\n    // API errors\n    'api_no_authorization_found' => 'No authorization token found on the request',\n    'api_bad_authorization_format' => 'An authorization token was found on the request but the format appeared incorrect',\n    'api_user_token_not_found' => 'No matching API token was found for the provided authorization token',\n    'api_incorrect_token_secret' => 'The secret provided for the given used API token is incorrect',\n    'api_user_no_api_permission' => 'The owner of the used API token does not have permission to make API calls',\n    'api_user_token_expired' => 'The authorization token used has expired',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Error thrown when sending a test email:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'The URL does not match the configured allowed SSR hosts',\n];\n"
  },
  {
    "path": "lang/en/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'New comment on page: :pageName',\n    'new_comment_intro' => 'A user has commented on a page in :appName:',\n    'new_page_subject' => 'New page: :pageName',\n    'new_page_intro' => 'A new page has been created in :appName:',\n    'updated_page_subject' => 'Updated page: :pageName',\n    'updated_page_intro' => 'A page has been updated in :appName:',\n    'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\\'t be sent notifications for further edits to this page by the same editor.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Page Name:',\n    'detail_page_path' => 'Page Path:',\n    'detail_commenter' => 'Commenter:',\n    'detail_comment' => 'Comment:',\n    'detail_created_by' => 'Created By:',\n    'detail_updated_by' => 'Updated By:',\n\n    'action_view_comment' => 'View Comment',\n    'action_view_page' => 'View Page',\n\n    'footer_reason' => 'This notification was sent to you because :link cover this type of activity for this item.',\n    'footer_reason_link' => 'your notification preferences',\n];\n"
  },
  {
    "path": "lang/en/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Previous',\n    'next'     => 'Next &raquo;',\n\n];\n"
  },
  {
    "path": "lang/en/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Passwords must be at least eight characters and match the confirmation.',\n    'user' => \"We can't find a user with that e-mail address.\",\n    'token' => 'The password reset token is invalid for this email address.',\n    'sent' => 'We have e-mailed your password reset link!',\n    'reset' => 'Your password has been reset!',\n\n];\n"
  },
  {
    "path": "lang/en/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'My Account',\n\n    'shortcuts' => 'Shortcuts',\n    'shortcuts_interface' => 'UI Shortcut Preferences',\n    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',\n    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',\n    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',\n    'shortcuts_section_navigation' => 'Navigation',\n    'shortcuts_section_actions' => 'Common Actions',\n    'shortcuts_save' => 'Save Shortcuts',\n    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing \"?\" which will highlight the available shortcuts for actions currently visible on the screen.',\n    'shortcuts_update_success' => 'Shortcut preferences have been updated!',\n    'shortcuts_overview_desc' => 'Manage keyboard shortcuts you can use to navigate the system user interface.',\n\n    'notifications' => 'Notification Preferences',\n    'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.',\n    'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own',\n    'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Notify upon replies to my comments',\n    'notifications_save' => 'Save Preferences',\n    'notifications_update_success' => 'Notification preferences have been updated!',\n    'notifications_watched' => 'Watched & Ignored Items',\n    'notifications_watched_desc' => 'Below are the items that have custom watch preferences applied. To update your preferences for these, view the item then find the watch options in the sidebar.',\n\n    'auth' => 'Access & Security',\n    'auth_change_password' => 'Change Password',\n    'auth_change_password_desc' => 'Change the password you use to log-in to the application. This must be at least 8 characters long.',\n    'auth_change_password_success' => 'Password has been updated!',\n\n    'profile' => 'Profile Details',\n    'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.',\n    'profile_view_public' => 'View Public Profile',\n    'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.',\n    'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.',\n    'profile_email_no_permission' => 'Unfortunately you don\\'t have permission to change your email address. If you want to change this, you\\'d need to ask an administrator to change this for you.',\n    'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.',\n    'profile_admin_options' => 'Administrator Options',\n    'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the \"Settings > Users\" area of the application.',\n\n    'delete_account' => 'Delete Account',\n    'delete_my_account' => 'Delete My Account',\n    'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\\'ve created, such as created pages and uploaded images, will remain.',\n    'delete_my_account_warning' => 'Are you sure you want to delete your account?',\n];\n"
  },
  {
    "path": "lang/en/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Settings',\n    'settings_save' => 'Save Settings',\n    'system_version' => 'System Version',\n    'categories' => 'Categories',\n\n    // App Settings\n    'app_customization' => 'Customization',\n    'app_features_security' => 'Features & Security',\n    'app_name' => 'Application Name',\n    'app_name_desc' => 'This name is shown in the header and in any system-sent emails.',\n    'app_name_header' => 'Show name in header',\n    'app_public_access' => 'Public Access',\n    'app_public_access_desc' => 'Enabling this option will allow visitors, that are not logged-in, to access content in your BookStack instance.',\n    'app_public_access_desc_guest' => 'Access for public visitors can be controlled through the \"Guest\" user.',\n    'app_public_access_toggle' => 'Allow public access',\n    'app_public_viewing' => 'Allow public viewing?',\n    'app_secure_images' => 'Higher Security Image Uploads',\n    'app_secure_images_toggle' => 'Enable higher security image uploads',\n    'app_secure_images_desc' => 'For performance reasons, all images are public. This option adds a random, hard-to-guess string in front of image urls. Ensure directory indexes are not enabled to prevent easy access.',\n    'app_default_editor' => 'Default Page Editor',\n    'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.',\n    'app_custom_html' => 'Custom HTML Head Content',\n    'app_custom_html_desc' => 'Any content added here will be inserted into the bottom of the <head> section of every page. This is handy for overriding styles or adding analytics code.',\n    'app_custom_html_disabled_notice' => 'Custom HTML head content is disabled on this settings page to ensure any breaking changes can be reverted.',\n    'app_logo' => 'Application Logo',\n    'app_logo_desc' => 'This is used in the application header bar, among other areas. This image should be 86px in height. Large images will be scaled down.',\n    'app_icon' => 'Application Icon',\n    'app_icon_desc' => 'This icon is used for browser tabs and shortcut icons. This should be a 256px square PNG image.',\n    'app_homepage' => 'Application Homepage',\n    'app_homepage_desc' => 'Select a view to show on the homepage instead of the default view. Page permissions are ignored for selected pages.',\n    'app_homepage_select' => 'Select a page',\n    'app_footer_links' => 'Footer Links',\n    'app_footer_links_desc' => 'Add links to show within the site footer. These will be displayed at the bottom of most pages, including those that do not require login. You can use a label of \"trans::<key>\" to use system-defined translations. For example: Using \"trans::common.privacy_policy\" will provide the translated text \"Privacy Policy\" and \"trans::common.terms_of_service\" will provide the translated text \"Terms of Service\".',\n    'app_footer_links_label' => 'Link Label',\n    'app_footer_links_url' => 'Link URL',\n    'app_footer_links_add' => 'Add Footer Link',\n    'app_disable_comments' => 'Disable Comments',\n    'app_disable_comments_toggle' => 'Disable comments',\n    'app_disable_comments_desc' => 'Disables comments across all pages in the application. <br> Existing comments are not shown.',\n\n    // Color settings\n    'color_scheme' => 'Application Color Scheme',\n    'color_scheme_desc' => 'Set the colors to use in the application user interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.',\n    'ui_colors_desc' => 'Set the application primary color and default link color. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the application interface.',\n    'app_color' => 'Primary Color',\n    'link_color' => 'Default Link Color',\n    'content_colors_desc' => 'Set colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.',\n    'bookshelf_color' => 'Shelf Color',\n    'book_color' => 'Book Color',\n    'chapter_color' => 'Chapter Color',\n    'page_color' => 'Page Color',\n    'page_draft_color' => 'Page Draft Color',\n\n    // Registration Settings\n    'reg_settings' => 'Registration',\n    'reg_enable' => 'Enable Registration',\n    'reg_enable_toggle' => 'Enable registration',\n    'reg_enable_desc' => 'When registration is enabled user will be able to sign themselves up as an application user. Upon registration they are given a single, default user role.',\n    'reg_default_role' => 'Default user role after registration',\n    'reg_enable_external_warning' => 'The option above is ignored while external LDAP or SAML authentication is active. User accounts for non-existing members will be auto-created if authentication, against the external system in use, is successful.',\n    'reg_email_confirmation' => 'Email Confirmation',\n    'reg_email_confirmation_toggle' => 'Require email confirmation',\n    'reg_confirm_email_desc' => 'If domain restriction is used then email confirmation will be required and this option will be ignored.',\n    'reg_confirm_restrict_domain' => 'Domain Restriction',\n    'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.',\n    'reg_confirm_restrict_domain_placeholder' => 'No restriction set',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Maintenance',\n    'maint_image_cleanup' => 'Cleanup Images',\n    'maint_image_cleanup_desc' => 'Scans page & revision content to check which images and drawings are currently in use and which images are redundant. Ensure you create a full database and image backup before running this.',\n    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',\n    'maint_image_cleanup_run' => 'Run Cleanup',\n    'maint_image_cleanup_warning' => ':count potentially unused images were found. Are you sure you want to delete these images?',\n    'maint_image_cleanup_success' => ':count potentially unused images found and deleted!',\n    'maint_image_cleanup_nothing_found' => 'No unused images found, Nothing deleted!',\n    'maint_send_test_email' => 'Send a Test Email',\n    'maint_send_test_email_desc' => 'This sends a test email to your email address specified in your profile.',\n    'maint_send_test_email_run' => 'Send test email',\n    'maint_send_test_email_success' => 'Email sent to :address',\n    'maint_send_test_email_mail_subject' => 'Test Email',\n    'maint_send_test_email_mail_greeting' => 'Email delivery seems to work!',\n    'maint_send_test_email_mail_text' => 'Congratulations! As you received this email notification, your email settings seem to be configured properly.',\n    'maint_recycle_bin_desc' => 'Deleted shelves, books, chapters & pages are sent to the recycle bin so they can be restored or permanently deleted. Older items in the recycle bin may be automatically removed after a while depending on system configuration.',\n    'maint_recycle_bin_open' => 'Open Recycle Bin',\n    'maint_regen_references' => 'Regenerate References',\n    'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',\n    'maint_regen_references_success' => 'Reference index has been regenerated!',\n    'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Recycle Bin',\n    'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',\n    'recycle_bin_deleted_item' => 'Deleted Item',\n    'recycle_bin_deleted_parent' => 'Parent',\n    'recycle_bin_deleted_by' => 'Deleted By',\n    'recycle_bin_deleted_at' => 'Deletion Time',\n    'recycle_bin_permanently_delete' => 'Permanently Delete',\n    'recycle_bin_restore' => 'Restore',\n    'recycle_bin_contents_empty' => 'The recycle bin is currently empty',\n    'recycle_bin_empty' => 'Empty Recycle Bin',\n    'recycle_bin_empty_confirm' => 'This will permanently destroy all items in the recycle bin including content contained within each item. Are you sure you want to empty the recycle bin?',\n    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?',\n    'recycle_bin_destroy_list' => 'Items to be Destroyed',\n    'recycle_bin_restore_list' => 'Items to be Restored',\n    'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.',\n    'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.',\n    'recycle_bin_restore_parent' => 'Restore Parent',\n    'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',\n    'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',\n\n    // Audit Log\n    'audit' => 'Audit Log',\n    'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',\n    'audit_event_filter' => 'Event Filter',\n    'audit_event_filter_no_filter' => 'No Filter',\n    'audit_deleted_item' => 'Deleted Item',\n    'audit_deleted_item_name' => 'Name: :name',\n    'audit_table_user' => 'User',\n    'audit_table_event' => 'Event',\n    'audit_table_related' => 'Related Item or Detail',\n    'audit_table_ip' => 'IP Address',\n    'audit_table_date' => 'Activity Date',\n    'audit_date_from' => 'Date Range From',\n    'audit_date_to' => 'Date Range To',\n\n    // Role Settings\n    'roles' => 'Roles',\n    'role_user_roles' => 'User Roles',\n    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',\n    'roles_x_users_assigned' => ':count user assigned|:count users assigned',\n    'roles_x_permissions_provided' => ':count permission|:count permissions',\n    'roles_assigned_users' => 'Assigned Users',\n    'roles_permissions_provided' => 'Provided Permissions',\n    'role_create' => 'Create New Role',\n    'role_delete' => 'Delete Role',\n    'role_delete_confirm' => 'This will delete the role with the name \\':roleName\\'.',\n    'role_delete_users_assigned' => 'This role has :userCount users assigned to it. If you would like to migrate the users from this role select a new role below.',\n    'role_delete_no_migration' => \"Don't migrate users\",\n    'role_delete_sure' => 'Are you sure you want to delete this role?',\n    'role_edit' => 'Edit Role',\n    'role_details' => 'Role Details',\n    'role_name' => 'Role Name',\n    'role_desc' => 'Short Description of Role',\n    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',\n    'role_external_auth_id' => 'External Authentication IDs',\n    'role_system' => 'System Permissions',\n    'role_manage_users' => 'Manage users',\n    'role_manage_roles' => 'Manage roles & role permissions',\n    'role_manage_entity_permissions' => 'Manage all book, chapter & page permissions',\n    'role_manage_own_entity_permissions' => 'Manage permissions on own book, chapter & pages',\n    'role_manage_page_templates' => 'Manage page templates',\n    'role_access_api' => 'Access system API',\n    'role_manage_settings' => 'Manage app settings',\n    'role_export_content' => 'Export content',\n    'role_import_content' => 'Import content',\n    'role_editor_change' => 'Change page editor',\n    'role_notifications' => 'Receive & manage notifications',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Asset Permissions',\n    'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',\n    'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',\n    'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.',\n    'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'All',\n    'role_own' => 'Own',\n    'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',\n    'role_save' => 'Save Role',\n    'role_users' => 'Users in this role',\n    'role_users_none' => 'No users are currently assigned to this role',\n\n    // Users\n    'users' => 'Users',\n    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',\n    'user_profile' => 'User Profile',\n    'users_add_new' => 'Add New User',\n    'users_search' => 'Search Users',\n    'users_latest_activity' => 'Latest Activity',\n    'users_details' => 'User Details',\n    'users_details_desc' => 'Set a display name and an email address for this user. The email address will be used for logging into the application.',\n    'users_details_desc_no_email' => 'Set a display name for this user so others can recognise them.',\n    'users_role' => 'User Roles',\n    'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.',\n    'users_password' => 'User Password',\n    'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',\n    'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.',\n    'users_send_invite_option' => 'Send user invite email',\n    'users_external_auth_id' => 'External Authentication ID',\n    'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.',\n    'users_password_warning' => 'Only fill the below if you would like to change the password for this user.',\n    'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.',\n    'users_delete' => 'Delete User',\n    'users_delete_named' => 'Delete user :userName',\n    'users_delete_warning' => 'This will fully delete this user with the name \\':userName\\' from the system.',\n    'users_delete_confirm' => 'Are you sure you want to delete this user?',\n    'users_migrate_ownership' => 'Migrate Ownership',\n    'users_migrate_ownership_desc' => 'Select a user here if you want another user to become the owner of all items currently owned by this user.',\n    'users_none_selected' => 'No user selected',\n    'users_edit' => 'Edit User',\n    'users_edit_profile' => 'Edit Profile',\n    'users_avatar' => 'User Avatar',\n    'users_avatar_desc' => 'Select an image to represent this user. This should be approx 256px square.',\n    'users_preferred_language' => 'Preferred Language',\n    'users_preferred_language_desc' => 'This option will change the language used for the user-interface of the application. This will not affect any user-created content.',\n    'users_social_accounts' => 'Social Accounts',\n    'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.',\n    'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not revoke previously authorized access. Revoke access from your profile settings on the connected social account.',\n    'users_social_connect' => 'Connect Account',\n    'users_social_disconnect' => 'Disconnect Account',\n    'users_social_status_connected' => 'Connected',\n    'users_social_status_disconnected' => 'Disconnected',\n    'users_social_connected' => ':socialAccount account was successfully attached to your profile.',\n    'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.',\n    'users_api_tokens' => 'API Tokens',\n    'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.',\n    'users_api_tokens_none' => 'No API tokens have been created for this user',\n    'users_api_tokens_create' => 'Create Token',\n    'users_api_tokens_expires' => 'Expires',\n    'users_api_tokens_docs' => 'API Documentation',\n    'users_mfa' => 'Multi-Factor Authentication',\n    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'users_mfa_x_methods' => ':count method configured|:count methods configured',\n    'users_mfa_configure' => 'Configure Methods',\n\n    // API Tokens\n    'user_api_token_create' => 'Create API Token',\n    'user_api_token_name' => 'Name',\n    'user_api_token_name_desc' => 'Give your token a readable name as a future reminder of its intended purpose.',\n    'user_api_token_expiry' => 'Expiry Date',\n    'user_api_token_expiry_desc' => 'Set a date at which this token expires. After this date, requests made using this token will no longer work. Leaving this field blank will set an expiry 100 years into the future.',\n    'user_api_token_create_secret_message' => 'Immediately after creating this token a \"Token ID\" & \"Token Secret\" will be generated and displayed. The secret will only be shown a single time so be sure to copy the value to somewhere safe and secure before proceeding.',\n    'user_api_token' => 'API Token',\n    'user_api_token_id' => 'Token ID',\n    'user_api_token_id_desc' => 'This is a non-editable system generated identifier for this token which will need to be provided in API requests.',\n    'user_api_token_secret' => 'Token Secret',\n    'user_api_token_secret_desc' => 'This is a system generated secret for this token which will need to be provided in API requests. This will only be displayed this one time so copy this value to somewhere safe and secure.',\n    'user_api_token_created' => 'Token created :timeAgo',\n    'user_api_token_updated' => 'Token updated :timeAgo',\n    'user_api_token_delete' => 'Delete Token',\n    'user_api_token_delete_warning' => 'This will fully delete this API token with the name \\':tokenName\\' from the system.',\n    'user_api_token_delete_confirm' => 'Are you sure you want to delete this API token?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',\n    'webhooks_x_trigger_events' => ':count trigger event|:count trigger events',\n    'webhooks_create' => 'Create New Webhook',\n    'webhooks_none_created' => 'No webhooks have yet been created.',\n    'webhooks_edit' => 'Edit Webhook',\n    'webhooks_save' => 'Save Webhook',\n    'webhooks_details' => 'Webhook Details',\n    'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',\n    'webhooks_events' => 'Webhook Events',\n    'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',\n    'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\\'t expose confidential content.',\n    'webhooks_events_all' => 'All system events',\n    'webhooks_name' => 'Webhook Name',\n    'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',\n    'webhooks_endpoint' => 'Webhook Endpoint',\n    'webhooks_active' => 'Webhook Active',\n    'webhook_events_table_header' => 'Events',\n    'webhooks_delete' => 'Delete Webhook',\n    'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \\':webhookName\\', from the system.',\n    'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?',\n    'webhooks_format_example' => 'Webhook Format Example',\n    'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The \"related_item\" and \"url\" properties are optional and will depend on the type of event triggered.',\n    'webhooks_status' => 'Webhook Status',\n    'webhooks_last_called' => 'Last Called:',\n    'webhooks_last_errored' => 'Last Errored:',\n    'webhooks_last_error_message' => 'Last Error Message:',\n\n    // Licensing\n    'licenses' => 'Licenses',\n    'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.',\n    'licenses_bookstack' => 'BookStack License',\n    'licenses_php' => 'PHP Library Licenses',\n    'licenses_js' => 'JavaScript Library Licenses',\n    'licenses_other' => 'Other Licenses',\n    'license_details' => 'License Details',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/en/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => 'The :attribute must be accepted.',\n    'active_url'           => 'The :attribute is not a valid URL.',\n    'after'                => 'The :attribute must be a date after :date.',\n    'alpha'                => 'The :attribute may only contain letters.',\n    'alpha_dash'           => 'The :attribute may only contain letters, numbers, dashes and underscores.',\n    'alpha_num'            => 'The :attribute may only contain letters and numbers.',\n    'array'                => 'The :attribute must be an array.',\n    'backup_codes'         => 'The provided code is not valid or has already been used.',\n    'before'               => 'The :attribute must be a date before :date.',\n    'between'              => [\n        'numeric' => 'The :attribute must be between :min and :max.',\n        'file'    => 'The :attribute must be between :min and :max kilobytes.',\n        'string'  => 'The :attribute must be between :min and :max characters.',\n        'array'   => 'The :attribute must have between :min and :max items.',\n    ],\n    'boolean'              => 'The :attribute field must be true or false.',\n    'confirmed'            => 'The :attribute confirmation does not match.',\n    'date'                 => 'The :attribute is not a valid date.',\n    'date_format'          => 'The :attribute does not match the format :format.',\n    'different'            => 'The :attribute and :other must be different.',\n    'digits'               => 'The :attribute must be :digits digits.',\n    'digits_between'       => 'The :attribute must be between :min and :max digits.',\n    'email'                => 'The :attribute must be a valid email address.',\n    'ends_with' => 'The :attribute must end with one of the following: :values',\n    'file'                 => 'The :attribute must be provided as a valid file.',\n    'filled'               => 'The :attribute field is required.',\n    'gt'                   => [\n        'numeric' => 'The :attribute must be greater than :value.',\n        'file'    => 'The :attribute must be greater than :value kilobytes.',\n        'string'  => 'The :attribute must be greater than :value characters.',\n        'array'   => 'The :attribute must have more than :value items.',\n    ],\n    'gte'                  => [\n        'numeric' => 'The :attribute must be greater than or equal :value.',\n        'file'    => 'The :attribute must be greater than or equal :value kilobytes.',\n        'string'  => 'The :attribute must be greater than or equal :value characters.',\n        'array'   => 'The :attribute must have :value items or more.',\n    ],\n    'exists'               => 'The selected :attribute is invalid.',\n    'image'                => 'The :attribute must be an image.',\n    'image_extension'      => 'The :attribute must have a valid & supported image extension.',\n    'in'                   => 'The selected :attribute is invalid.',\n    'integer'              => 'The :attribute must be an integer.',\n    'ip'                   => 'The :attribute must be a valid IP address.',\n    'ipv4'                 => 'The :attribute must be a valid IPv4 address.',\n    'ipv6'                 => 'The :attribute must be a valid IPv6 address.',\n    'json'                 => 'The :attribute must be a valid JSON string.',\n    'lt'                   => [\n        'numeric' => 'The :attribute must be less than :value.',\n        'file'    => 'The :attribute must be less than :value kilobytes.',\n        'string'  => 'The :attribute must be less than :value characters.',\n        'array'   => 'The :attribute must have less than :value items.',\n    ],\n    'lte'                  => [\n        'numeric' => 'The :attribute must be less than or equal :value.',\n        'file'    => 'The :attribute must be less than or equal :value kilobytes.',\n        'string'  => 'The :attribute must be less than or equal :value characters.',\n        'array'   => 'The :attribute must not have more than :value items.',\n    ],\n    'max'                  => [\n        'numeric' => 'The :attribute may not be greater than :max.',\n        'file'    => 'The :attribute may not be greater than :max kilobytes.',\n        'string'  => 'The :attribute may not be greater than :max characters.',\n        'array'   => 'The :attribute may not have more than :max items.',\n    ],\n    'mimes'                => 'The :attribute must be a file of type: :values.',\n    'min'                  => [\n        'numeric' => 'The :attribute must be at least :min.',\n        'file'    => 'The :attribute must be at least :min kilobytes.',\n        'string'  => 'The :attribute must be at least :min characters.',\n        'array'   => 'The :attribute must have at least :min items.',\n    ],\n    'not_in'               => 'The selected :attribute is invalid.',\n    'not_regex'            => 'The :attribute format is invalid.',\n    'numeric'              => 'The :attribute must be a number.',\n    'regex'                => 'The :attribute format is invalid.',\n    'required'             => 'The :attribute field is required.',\n    'required_if'          => 'The :attribute field is required when :other is :value.',\n    'required_with'        => 'The :attribute field is required when :values is present.',\n    'required_with_all'    => 'The :attribute field is required when :values is present.',\n    'required_without'     => 'The :attribute field is required when :values is not present.',\n    'required_without_all' => 'The :attribute field is required when none of :values are present.',\n    'same'                 => 'The :attribute and :other must match.',\n    'safe_url'             => 'The provided link may not be safe.',\n    'size'                 => [\n        'numeric' => 'The :attribute must be :size.',\n        'file'    => 'The :attribute must be :size kilobytes.',\n        'string'  => 'The :attribute must be :size characters.',\n        'array'   => 'The :attribute must contain :size items.',\n    ],\n    'string'               => 'The :attribute must be a string.',\n    'timezone'             => 'The :attribute must be a valid zone.',\n    'totp'                 => 'The provided code is not valid or has expired.',\n    'unique'               => 'The :attribute has already been taken.',\n    'url'                  => 'The :attribute format is invalid.',\n    'uploaded'             => 'The file could not be uploaded. The server may not accept files of this size.',\n\n    'zip_file' => 'The :attribute needs to reference a file within the ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',\n    'zip_model_expected' => 'Data object expected but \":type\" found.',\n    'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Password confirmation required',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/es/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'Página creada',\n    'page_create_notification'    => 'Página creada correctamente',\n    'page_update'                 => 'página actualizada',\n    'page_update_notification'    => 'Página actualizada correctamente',\n    'page_delete'                 => 'página eliminada',\n    'page_delete_notification'    => 'Página eliminada correctamente',\n    'page_restore'                => 'página restaurada',\n    'page_restore_notification'   => 'Página restaurada correctamente',\n    'page_move'                   => 'página movida',\n    'page_move_notification'      => 'Página movida correctamente',\n\n    // Chapters\n    'chapter_create'              => 'capítulo creado',\n    'chapter_create_notification' => 'Capítulo creado correctamente',\n    'chapter_update'              => 'capítulo actualizado',\n    'chapter_update_notification' => 'Capítulo actualizado correctamente',\n    'chapter_delete'              => 'capítulo eliminado',\n    'chapter_delete_notification' => 'Capítulo eliminado correctamente',\n    'chapter_move'                => 'capítulo movido',\n    'chapter_move_notification' => 'Capítulo movido correctamente',\n\n    // Books\n    'book_create'                 => 'libro creado',\n    'book_create_notification'    => 'Libro creado correctamente',\n    'book_create_from_chapter'              => 'convertido capítulo a libro',\n    'book_create_from_chapter_notification' => 'Capítulo convertido a libro con éxito',\n    'book_update'                 => 'libro actualizado',\n    'book_update_notification'    => 'Libro actualizado correctamente',\n    'book_delete'                 => 'libro eliminado',\n    'book_delete_notification'    => 'Libro eliminado correctamente',\n    'book_sort'                   => 'libro ordenado',\n    'book_sort_notification'      => 'Libro reordenado correctamente',\n\n    // Bookshelves\n    'bookshelf_create'            => 'estante creado',\n    'bookshelf_create_notification'    => 'Estante creado correctamente',\n    'bookshelf_create_from_book'    => 'libro convertido a estante',\n    'bookshelf_create_from_book_notification'    => 'Libro convertido a estante con éxito',\n    'bookshelf_update'                 => 'estante actualizado',\n    'bookshelf_update_notification'    => 'Estante actualizado correctamente',\n    'bookshelf_delete'                 => 'estante eliminado',\n    'bookshelf_delete_notification'    => 'Estante eliminado correctamente',\n\n    // Revisions\n    'revision_restore' => 'revisión restaurada',\n    'revision_delete' => 'revisión eliminada',\n    'revision_delete_notification' => 'Revisión eliminada correctamente',\n\n    // Favourites\n    'favourite_add_notification' => '\".name\" ha sido añadido a sus favoritos',\n    'favourite_remove_notification' => '\".name\" ha sido eliminado de sus favoritos',\n\n    // Watching\n    'watch_update_level_notification' => 'Preferencias de suscripciones actualizadas correctamente',\n\n    // Auth\n    'auth_login' => 'conectado',\n    'auth_register' => 'registrado como nuevo usuario',\n    'auth_password_reset_request' => 'solicitado cambio de contraseña de usuario',\n    'auth_password_reset_update' => 'restablecer contraseña de usuario',\n    'mfa_setup_method' => 'método MFA configurado',\n    'mfa_setup_method_notification' => 'Método de Autenticación en Dos Pasos configurado correctamente',\n    'mfa_remove_method' => 'método MFA eliminado',\n    'mfa_remove_method_notification' => 'Método de Autenticación en Dos Pasos eliminado correctamente',\n\n    // Settings\n    'settings_update' => 'configuración actualizada',\n    'settings_update_notification' => 'Configuración actualizada correctamente',\n    'maintenance_action_run' => 'ejecutada acción de mantenimiento',\n\n    // Webhooks\n    'webhook_create' => 'webhook creado',\n    'webhook_create_notification' => 'Webhook creado correctamente',\n    'webhook_update' => 'webhook actualizado',\n    'webhook_update_notification' => 'Webhook actualizado correctamente',\n    'webhook_delete' => 'webhook eliminado',\n    'webhook_delete_notification' => 'Webhook eliminado correctamente',\n\n    // Imports\n    'import_create' => 'importación creada',\n    'import_create_notification' => 'Importación cargada correctamente',\n    'import_run' => 'importación actualizada',\n    'import_run_notification' => 'Contenido importado correctamente',\n    'import_delete' => 'importación borrada',\n    'import_delete_notification' => 'Importación borrada correctamente',\n\n    // Users\n    'user_create' => 'usuario creado',\n    'user_create_notification' => 'Usuario creado correctamente',\n    'user_update' => 'usuario actualizado',\n    'user_update_notification' => 'Usuario actualizado correctamente',\n    'user_delete' => 'usuario eliminado',\n    'user_delete_notification' => 'Usuario eliminado correctamente',\n\n    // API Tokens\n    'api_token_create' => 'token de API creado',\n    'api_token_create_notification' => 'Token API creado correctamente',\n    'api_token_update' => 'token de API actualizado',\n    'api_token_update_notification' => 'Token API actualizado correctamente',\n    'api_token_delete' => 'token de API borrado',\n    'api_token_delete_notification' => 'Token API borrado correctamente',\n\n    // Roles\n    'role_create' => 'rol creado',\n    'role_create_notification' => 'Rol creado correctamente',\n    'role_update' => 'rol actualizado',\n    'role_update_notification' => 'Rol actualizado correctamente',\n    'role_delete' => 'rol borrado',\n    'role_delete_notification' => 'Rol eliminado correctamente',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'papelera de reciclaje vaciada',\n    'recycle_bin_restore' => 'restaurado de la papelera de reciclaje',\n    'recycle_bin_destroy' => 'eliminado de la papelera de reciclaje',\n\n    // Comments\n    'commented_on'                => 'comentada el',\n    'comment_create'              => 'comentario añadido',\n    'comment_update'              => 'comentario actualizado',\n    'comment_delete'              => 'comentario borrado',\n\n    // Sort Rules\n    'sort_rule_create' => 'regla de ordenación creada',\n    'sort_rule_create_notification' => 'Rol de ordenación creada con éxito',\n    'sort_rule_update' => 'regla de ordenación actualizada',\n    'sort_rule_update_notification' => 'Regla de ordenación actualizada correctamente',\n    'sort_rule_delete' => 'regla de ordenación eliminada',\n    'sort_rule_delete_notification' => 'Rol de ordenación borrada con éxito',\n\n    // Other\n    'permissions_update'          => 'permisos actualizados',\n];\n"
  },
  {
    "path": "lang/es/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Estas credenciales no coinciden con nuestros registros.',\n    'throttle' => 'Demasiados intentos de inicio de sesión. Por favor, inténtalo de nuevo en :seconds segundos.',\n\n    // Login & Register\n    'sign_up' => 'Registrarse',\n    'log_in' => 'Acceder',\n    'log_in_with' => 'Acceder con :socialDriver',\n    'sign_up_with' => 'Registrarse con :socialDriver',\n    'logout' => 'Cerrar sesión',\n\n    'name' => 'Nombre',\n    'username' => 'Usuario',\n    'email' => 'Correo electrónico',\n    'password' => 'Contraseña',\n    'password_confirm' => 'Confirmar contraseña',\n    'password_hint' => 'Debe contener al menos 8 caracteres',\n    'forgot_password' => '¿Contraseña olvidada?',\n    'remember_me' => 'Recordarme',\n    'ldap_email_hint' => 'Por favor introduzca un mail para utilizar con esta cuenta.',\n    'create_account' => 'Crear una cuenta',\n    'already_have_account' => '¿Ya tienes una cuenta?',\n    'dont_have_account' => '¿No tienes una cuenta?',\n    'social_login' => 'Login social',\n    'social_registration' => 'Registro social',\n    'social_registration_text' => 'Registrar y entrar utilizando otro servicio.',\n\n    'register_thanks' => '¡Gracias por registrarse!',\n    'register_confirm' => 'Por favor compruebe su correo electrónico y haga clic en el botón de confirmación enviado para acceder a :appName.',\n    'registrations_disabled' => 'Los registros están deshabilitados actualmente',\n    'registration_email_domain_invalid' => 'Este dominio de correo electrónico no tiene acceso a esta aplicación',\n    'register_success' => '¡Gracias por registrarse! Ahora se encuentra registrado y logueado.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Intentando iniciar sesión',\n    'auto_init_starting_desc' => 'Estamos contactando con su sistema de autenticación para comenzar el proceso de inicio de sesión. Si no hay progreso después de 5 segundos puede intentar hacer clic en el enlace de abajo.',\n    'auto_init_start_link' => 'Continuar con la autenticación',\n\n    // Password Reset\n    'reset_password' => 'Resetear contraseña',\n    'reset_password_send_instructions' => 'Introduzca su correo electrónico a continuación y le será enviado un correo con un link para la restauración',\n    'reset_password_send_button' => 'Enviar enlace de reseteo',\n    'reset_password_sent' => 'Un enlace para cambiar la contraseña será enviado a su dirección de correo electrónico si existe en nuestro sistema.',\n    'reset_password_success' => 'Su password ha sido reseteado de manera éxitosa.',\n    'email_reset_subject' => 'Resetee la contraseña de :appName',\n    'email_reset_text' => 'Está recibiendo este correo electrónico debido a que recibimos una solicitud de reseteo de contraseña de su cuenta.',\n    'email_reset_not_requested' => 'Si no ha solicitado un reseteo de la contraseña, no es requerida ninguna acción por su parte.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Confirme su correo electrónico en :appName',\n    'email_confirm_greeting' => '¡Gracias por unirse a :appName!',\n    'email_confirm_text' => 'Por favor confirme su dirección de correo electrónico haciendo click en el siguiente botón:',\n    'email_confirm_action' => 'Confirmar correo electrónico',\n    'email_confirm_send_error' => 'Confirmation de correo electrónico requerida pero el sistema no pudo enviar el correo. Contacte con el administrador para asegurarse de que el correo electrónico está configurado correctamente.',\n    'email_confirm_success' => '¡Tu correo electrónico ha sido confirmado! Ahora deberías poder iniciar sesión usando esta dirección de correo electrónico.',\n    'email_confirm_resent' => 'correo electrónico de confirmación reenviado, compruebe su bandeja de entrada.',\n    'email_confirm_thanks' => '¡Gracias por confirmar!',\n    'email_confirm_thanks_desc' => 'Por favor, espere un momento mientras se gestiona su confirmación. Si no es redirigido después de 3 segundos, pulse el enlace \"Continuar\" para continuar.',\n\n    'email_not_confirmed' => 'Dirección de Correo Electrónico no confirmada',\n    'email_not_confirmed_text' => 'Su Cuenta de Correo electrónico todavía no ha sido confirmada.',\n    'email_not_confirmed_click_link' => 'Por favor siga el enlace en el correo electrónico que ha sido enviado durante el proceso de registro.',\n    'email_not_confirmed_resend' => 'Si no puede encontrar el correo electrónico, puede solicitar el renvío del correo electrónico de confirmación rellenando el formulario que se muestra a continuación.',\n    'email_not_confirmed_resend_button' => 'Reenviar Correo Electrónico de confirmación',\n\n    // User Invite\n    'user_invite_email_subject' => '¡Has sido invitado a unirte a :appName!',\n    'user_invite_email_greeting' => 'Se ha creado una cuenta para usted en :appName.',\n    'user_invite_email_text' => 'Haga clic en el botón de abajo para establecer una contraseña de cuenta y obtener acceso:',\n    'user_invite_email_action' => 'Establecer contraseña de la cuenta',\n    'user_invite_page_welcome' => '¡Bienvenido a :appName!',\n    'user_invite_page_text' => 'Para finalizar tu cuenta y obtener acceso necesitas establecer una contraseña que se utilizará para iniciar sesión en :appName en futuras visitas.',\n    'user_invite_page_confirm_button' => 'Confirmar Contraseña',\n    'user_invite_success_login' => 'Contraseña guardada, ¡ahora debería ser capaz de iniciar sesión usando su contraseña establecida para acceder a :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Configurar Autenticación en Dos Pasos',\n    'mfa_setup_desc' => 'La autenticación en dos pasos añade una capa de seguridad adicional a tu cuenta de usuario.',\n    'mfa_setup_configured' => 'Ya está configurado',\n    'mfa_setup_reconfigure' => 'Reconfigurar',\n    'mfa_setup_remove_confirmation' => '¿Está seguro de que desea eliminar este método de autenticación en dos pasos?',\n    'mfa_setup_action' => 'Configuración',\n    'mfa_backup_codes_usage_limit_warning' => 'Quedan menos de 5 códigos de respaldo, Por favor, genera y almacena un nuevo conjunto antes de que te quedes sin códigos para evitar que te bloquees fuera de tu cuenta.',\n    'mfa_option_totp_title' => 'Aplicación para móviles',\n    'mfa_option_totp_desc' => 'Para utilizar la autenticación en dos pasos necesitarás una aplicación móvil que soporte TOTP como Google Authenticator, Authy o Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Códigos de Respaldo',\n    'mfa_option_backup_codes_desc' => 'Genera un conjunto de códigos de seguridad de un solo uso que ingresará al iniciar sesión para verificar su identidad. Asegúrese de guardarlos en un lugar seguro.',\n    'mfa_gen_confirm_and_enable' => 'Confirmar y Activar',\n    'mfa_gen_backup_codes_title' => 'Configuración de Códigos de Respaldo',\n    'mfa_gen_backup_codes_desc' => 'Guarda la siguiente lista de códigos en un lugar seguro. Al acceder al sistema podrás usar uno de los códigos como un segundo mecanismo de autenticación.',\n    'mfa_gen_backup_codes_download' => 'Descargar Códigos',\n    'mfa_gen_backup_codes_usage_warning' => 'Cada código sólo puede utilizarse una vez',\n    'mfa_gen_totp_title' => 'Configuración de Aplicación móvil',\n    'mfa_gen_totp_desc' => 'Para utilizar la autenticación en dos pasos necesitarás una aplicación móvil que soporte TOTP como Google Authenticator, Authy o Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Escanea el código QR mostrado a continuación usando tu aplicación de autenticación preferida para empezar.',\n    'mfa_gen_totp_verify_setup' => 'Verificar Configuración',\n    'mfa_gen_totp_verify_setup_desc' => 'Verifica que todo está funcionando introduciendo un código, generado en tu aplicación de autenticación, en el campo de texto a continuación:',\n    'mfa_gen_totp_provide_code_here' => 'Introduce aquí tu código generado por la aplicación',\n    'mfa_verify_access' => 'Verificar Acceso',\n    'mfa_verify_access_desc' => 'Tu cuenta de usuario requiere que confirmes tu identidad a través de un nivel adicional de verificación antes de que te conceda el acceso. Verifica tu identidad usando uno de los métodos configurados para continuar.',\n    'mfa_verify_no_methods' => 'No hay Métodos Configurados',\n    'mfa_verify_no_methods_desc' => 'No se han encontrado métodos de autenticación en dos pasos para tu cuenta. Tendrás que configurar al menos un método antes de obtener acceso.',\n    'mfa_verify_use_totp' => 'Verificar usando una aplicación móvil',\n    'mfa_verify_use_backup_codes' => 'Verificar usando un código de respaldo',\n    'mfa_verify_backup_code' => 'Códigos de Respaldo',\n    'mfa_verify_backup_code_desc' => 'Introduzca uno de sus códigos de respaldo restantes a continuación:',\n    'mfa_verify_backup_code_enter_here' => 'Introduce el código de respaldo aquí',\n    'mfa_verify_totp_desc' => 'Introduzca el código, generado con tu aplicación móvil, a continuación:',\n    'mfa_setup_login_notification' => 'Método de dos factores configurado. Por favor, inicia sesión de nuevo utilizando el método configurado.',\n];\n"
  },
  {
    "path": "lang/es/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Cancelar',\n    'close' => 'Cerrar',\n    'confirm' => 'Confirmar',\n    'back' => 'Atrás',\n    'save' => 'Guardar',\n    'continue' => 'Continuar',\n    'select' => 'Seleccionar',\n    'toggle_all' => 'Marcarlos todos',\n    'more' => 'Más',\n\n    // Form Labels\n    'name' => 'Nombre',\n    'description' => 'Descripción',\n    'role' => 'Rol',\n    'cover_image' => 'Imagen de portada',\n    'cover_image_description' => 'Esta imagen debe ser de aproximadamente 440x250px aunque será escalada y recortada para adaptarse a la interfaz de usuario en diferentes escenarios según sea necesario, por lo que las dimensiones en pantalla diferirán.',\n\n    // Actions\n    'actions' => 'Acciones',\n    'view' => 'Ver',\n    'view_all' => 'Ver todos',\n    'new' => 'Nuevo',\n    'create' => 'Crear',\n    'update' => 'Actualizar',\n    'edit' => 'Editar',\n    'archive' => 'Archivar',\n    'unarchive' => 'Desarchivar',\n    'sort' => 'Ordenar',\n    'move' => 'Mover',\n    'copy' => 'Copiar',\n    'reply' => 'Responder',\n    'delete' => 'Borrar',\n    'delete_confirm' => 'Confirmar borrado',\n    'search' => 'Buscar',\n    'search_clear' => 'Limpiar búsqueda',\n    'reset' => 'Resetear',\n    'remove' => 'Remover',\n    'add' => 'Añadir',\n    'configure' => 'Configurar',\n    'manage' => 'Gestionar',\n    'fullscreen' => 'Pantalla completa',\n    'favourite' => 'Añadir a favoritos',\n    'unfavourite' => 'Eliminar de favoritos',\n    'next' => 'Siguiente',\n    'previous' => 'Anterior',\n    'filter_active' => 'Filtro activo:',\n    'filter_clear' => 'Limpiar filtro',\n    'download' => 'Descargar',\n    'open_in_tab' => 'Abrir en una nueva pestaña',\n    'open' => 'Abrir',\n\n    // Sort Options\n    'sort_options' => 'Opciones de ordenación',\n    'sort_direction_toggle' => 'Cambiar el Orden',\n    'sort_ascending' => 'Ordenar Ascendentemente',\n    'sort_descending' => 'Ordenar Descendentemente',\n    'sort_name' => 'Nombre',\n    'sort_default' => 'Predeterminada',\n    'sort_created_at' => 'Fecha de Creación',\n    'sort_updated_at' => 'Fecha de Modificación',\n\n    // Misc\n    'deleted_user' => 'Usuario borrado',\n    'no_activity' => 'Ninguna actividad para mostrar',\n    'no_items' => 'No hay elementos disponibles',\n    'back_to_top' => 'Volver arriba',\n    'skip_to_main_content' => 'Ir al contenido principal',\n    'toggle_details' => 'Alternar detalles',\n    'toggle_thumbnails' => 'Alternar miniaturas',\n    'details' => 'Detalles',\n    'grid_view' => 'Vista en Cuadrícula',\n    'list_view' => 'Vista en Lista',\n    'default' => 'Predeterminada',\n    'breadcrumb' => 'Rastro de migas de pan',\n    'status' => 'Estado',\n    'status_active' => 'Activo',\n    'status_inactive' => 'Inactive',\n    'never' => 'Nunca',\n    'none' => 'Ninguno',\n\n    // Header\n    'homepage' => 'Página de Inicio',\n    'header_menu_expand' => 'Expandir el Menú de la Cabecera',\n    'profile_menu' => 'Menú de Perfil',\n    'view_profile' => 'Ver Perfil',\n    'edit_profile' => 'Editar Perfil',\n    'dark_mode' => 'Modo Oscuro',\n    'light_mode' => 'Modo Claro',\n    'global_search' => 'Búsqueda Global',\n\n    // Layout tabs\n    'tab_info' => 'Información',\n    'tab_info_label' => 'Pestaña: Mostrar Información Secundaria',\n    'tab_content' => 'Contenido',\n    'tab_content_label' => 'Pestaña: Mostrar Contenido Primario',\n\n    // Email Content\n    'email_action_help' => 'Si está teniendo problemas clicando en el botón \":actionText\", copie y pegue la siguiente URL en su navegador web:',\n    'email_rights' => 'Todos los derechos reservados',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Política de privacidad',\n    'terms_of_service' => 'Términos de Servicio',\n\n    // OpenSearch\n    'opensearch_description' => 'Buscar :appName',\n];\n"
  },
  {
    "path": "lang/es/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Seleccionar Imagen',\n    'image_list' => 'Lista de imágenes',\n    'image_details' => 'Detalles de la imagen',\n    'image_upload' => 'Subir imagen',\n    'image_intro' => 'Aquí puede seleccionar y administrar las imágenes que se han subido previamente al sistema.',\n    'image_intro_upload' => 'Suba una nueva imagen arrastrando un archivo de imagen en esta ventana, o usando el botón \"Subir imagen\" de arriba.',\n    'image_all' => 'Todas',\n    'image_all_title' => 'Ver todas las imágenes',\n    'image_book_title' => 'Ver las imágenes subidas a este libro',\n    'image_page_title' => 'Ver las imágenes subidas a esta página',\n    'image_search_hint' => 'Buscar por nombre de imagen',\n    'image_uploaded' => 'Subido el :uploadedDate',\n    'image_uploaded_by' => 'Subida por :userName',\n    'image_uploaded_to' => 'Subida a :pageLink',\n    'image_updated' => 'Actualizado :updateDate',\n    'image_load_more' => 'Cargar más',\n    'image_image_name' => 'Nombre de imagen',\n    'image_delete_used' => 'Esta imagen está siendo utilizada en las páginas mostradas a continuación.',\n    'image_delete_confirm_text' => '¿Estás seguro de que quieres eliminar esta imagen?',\n    'image_select_image' => 'Seleccionar Imagen',\n    'image_dropzone' => 'Arrastre las imágenes o hacer click aquí para Subir',\n    'image_dropzone_drop' => 'Arrastre las imágenes aquí para subirlas',\n    'images_deleted' => 'Imágenes borradas',\n    'image_preview' => 'Previsualización de la imagen',\n    'image_upload_success' => 'Imagen subida éxitosamente',\n    'image_update_success' => 'Detalles de la imagen actualizados exitosamente',\n    'image_delete_success' => 'Imagen borrada exitosamente',\n    'image_replace' => 'Sustituir imagen',\n    'image_replace_success' => 'Imagen actualizada correctamente',\n    'image_rebuild_thumbs' => 'Regenerar distintos tamaños',\n    'image_rebuild_thumbs_success' => '¡Imágenes de distinto tamaño regeneradas correctamente!',\n\n    // Code Editor\n    'code_editor' => 'Editar Código',\n    'code_language' => 'Lenguaje del Código',\n    'code_content' => 'Contenido del Código',\n    'code_session_history' => 'Historial de la sesión',\n    'code_save' => 'Guardar Código',\n];\n"
  },
  {
    "path": "lang/es/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'General',\n    'advanced' => 'Avanzado',\n    'none' => 'Ninguno',\n    'cancel' => 'Cancelar',\n    'save' => 'Guardar',\n    'close' => 'Cerrar',\n    'apply' => 'Aplicar',\n    'undo' => 'Deshacer',\n    'redo' => 'Rehacer',\n    'left' => 'Izquierda',\n    'center' => 'Centro',\n    'right' => 'Derecha',\n    'top' => 'Arriba',\n    'middle' => 'Medio',\n    'bottom' => 'Abajo',\n    'width' => 'Anchura',\n    'height' => 'Altura',\n    'More' => 'Más',\n    'select' => 'Seleccionar...',\n\n    // Toolbar\n    'formats' => 'Formatos',\n    'header_large' => 'Encabezado grande',\n    'header_medium' => 'Encabezado medio',\n    'header_small' => 'Encabezado pequeño',\n    'header_tiny' => 'Encabezado muy pequeño',\n    'paragraph' => 'Párrafo',\n    'blockquote' => 'Cita',\n    'inline_code' => 'Código en línea',\n    'callouts' => 'Llamadas',\n    'callout_information' => 'Información',\n    'callout_success' => 'Éxito',\n    'callout_warning' => 'Advertencia',\n    'callout_danger' => 'Peligro',\n    'bold' => 'Negrita',\n    'italic' => 'Cursiva',\n    'underline' => 'Subrayado',\n    'strikethrough' => 'Tachado',\n    'superscript' => 'Superíndice',\n    'subscript' => 'Subíndice',\n    'text_color' => 'Color de texto',\n    'highlight_color' => 'Color de resaltado',\n    'custom_color' => 'Color personalizado',\n    'remove_color' => 'Eliminar color',\n    'background_color' => 'Color de fondo',\n    'align_left' => 'Alinear a la izquierda',\n    'align_center' => 'Alinear al centro',\n    'align_right' => 'Alinear a la derecha',\n    'align_justify' => 'Justificado',\n    'list_bullet' => 'Lista sin ordenar',\n    'list_numbered' => 'Lista ordenada',\n    'list_task' => 'Lista de tareas',\n    'indent_increase' => 'Aumentar sangría',\n    'indent_decrease' => 'Reducir sangría',\n    'table' => 'Tabla',\n    'insert_image' => 'Importar imagen',\n    'insert_image_title' => 'Insertar/Editar imagen',\n    'insert_link' => 'Insertar/editar enlace',\n    'insert_link_title' => 'Insertar/Editar enlace',\n    'insert_horizontal_line' => 'Insertar línea horizontal',\n    'insert_code_block' => 'Insertar bloque de código',\n    'edit_code_block' => 'Editar bloque de código',\n    'insert_drawing' => 'Insertar/editar dibujo',\n    'drawing_manager' => 'Gestor de dibujo',\n    'insert_media' => 'Insertar/editar medios',\n    'insert_media_title' => 'Insertar/Editar medios',\n    'clear_formatting' => 'Borrar formato',\n    'source_code' => 'Código fuente',\n    'source_code_title' => 'Código Fuente',\n    'fullscreen' => 'Pantalla completa',\n    'image_options' => 'Opciones de imagen',\n\n    // Tables\n    'table_properties' => 'Propiedades de tabla',\n    'table_properties_title' => 'Propiedades de Tabla',\n    'delete_table' => 'Eliminar tabla',\n    'table_clear_formatting' => 'Limpiar formato de tabla',\n    'resize_to_contents' => 'Redimensionar al contenido',\n    'row_header' => 'Fila de cabecera',\n    'insert_row_before' => 'Insertar fila arriba',\n    'insert_row_after' => 'Insertar fila abajo',\n    'delete_row' => 'Eliminar fila',\n    'insert_column_before' => 'Insertar columna a la izquierda',\n    'insert_column_after' => 'Insertar columna a la derecha',\n    'delete_column' => 'Eliminar columna',\n    'table_cell' => 'Celda',\n    'table_row' => 'Fila',\n    'table_column' => 'Columna',\n    'cell_properties' => 'Propiedades de la celda',\n    'cell_properties_title' => 'Propiedades de Celda',\n    'cell_type' => 'Tipo de celda',\n    'cell_type_cell' => 'Celda',\n    'cell_scope' => 'Ámbito',\n    'cell_type_header' => 'Celda de cabecera',\n    'merge_cells' => 'Combinar celdas',\n    'split_cell' => 'Dividir celda',\n    'table_row_group' => 'Grupo de filas',\n    'table_column_group' => 'Grupo de columnas',\n    'horizontal_align' => 'Alineación horizontal',\n    'vertical_align' => 'Alineación vertical',\n    'border_width' => 'Ancho del borde',\n    'border_style' => 'Estilo del borde',\n    'border_color' => 'Color del borde',\n    'row_properties' => 'Propiedades de fila',\n    'row_properties_title' => 'Propiedades de Fila',\n    'cut_row' => 'Cortar fila',\n    'copy_row' => 'Copiar fila',\n    'paste_row_before' => 'Pegar fila arriba',\n    'paste_row_after' => 'Pegar fila abajo',\n    'row_type' => 'Tipo de fila',\n    'row_type_header' => 'Encabezado',\n    'row_type_body' => 'Cuerpo',\n    'row_type_footer' => 'Pie',\n    'alignment' => 'Alineación',\n    'cut_column' => 'Cortar columna',\n    'copy_column' => 'Copiar columna',\n    'paste_column_before' => 'Pegar columna a la izquierda',\n    'paste_column_after' => 'Pegar columna a la derecha',\n    'cell_padding' => 'Relleno de la celda',\n    'cell_spacing' => 'Espaciado entre celdas',\n    'caption' => 'Leyenda',\n    'show_caption' => 'Mostrar leyenda',\n    'constrain' => 'Restringir proporciones',\n    'cell_border_solid' => 'Sólida',\n    'cell_border_dotted' => 'Punteada',\n    'cell_border_dashed' => 'Discontinua',\n    'cell_border_double' => 'Doble',\n    'cell_border_groove' => 'Surcos',\n    'cell_border_ridge' => 'Cresta',\n    'cell_border_inset' => 'Interno',\n    'cell_border_outset' => 'Externo',\n    'cell_border_none' => 'Ninguno',\n    'cell_border_hidden' => 'Oculto',\n\n    // Images, links, details/summary & embed\n    'source' => 'Origen',\n    'alt_desc' => 'Descripción alternativa',\n    'embed' => 'Incrustar',\n    'paste_embed' => 'Pegue su código incrustado a continuación:',\n    'url' => 'URL',\n    'text_to_display' => 'Texto para mostrar',\n    'title' => 'Titulo',\n    'browse_links' => 'Ver enlaces',\n    'open_link' => 'Abrir enlace',\n    'open_link_in' => 'Abrir enlace en...',\n    'open_link_current' => 'Ventana actual',\n    'open_link_new' => 'Nueva ventana',\n    'remove_link' => 'Eliminar enlace',\n    'insert_collapsible' => 'Insertar bloque plegable',\n    'collapsible_unwrap' => 'Desplegar',\n    'edit_label' => 'Editar etiqueta',\n    'toggle_open_closed' => 'Abrir/Cerrar',\n    'collapsible_edit' => 'Editar bloque plegable',\n    'toggle_label' => 'Cambiar etiqueta',\n\n    // About view\n    'about' => 'Acerca del editor',\n    'about_title' => 'Acerca del editor WYSIWYG',\n    'editor_license' => 'Licencia del editor y derechos de autor',\n    'editor_lexical_license' => 'Este editor está construido como una bifurcación de :lexicalLink que se distribuye bajo la licencia MIT.',\n    'editor_lexical_license_link' => 'Los detalles completos de la licencia se pueden encontrar aquí.',\n    'editor_tiny_license' => 'Este editor se construye usando :tinyLink que se proporciona bajo la licencia MIT.',\n    'editor_tiny_license_link' => 'Aquí encontrará los detalles de los derechos de autor y la licencia de TinyMCE.',\n    'save_continue' => 'Guardar Página y Continuar',\n    'callouts_cycle' => '(Siga presionando para alternar entre tipos)',\n    'link_selector' => 'Enlace a contenido',\n    'shortcuts' => 'Atajos',\n    'shortcut' => 'Atajo',\n    'shortcuts_intro' => 'Los siguientes atajos están disponibles en el editor:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Descripción',\n];\n"
  },
  {
    "path": "lang/es/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Creado Recientemente',\n    'recently_created_pages' => 'Páginas creadas recientemente',\n    'recently_updated_pages' => 'Páginas actualizadas recientemente',\n    'recently_created_chapters' => 'Capítulos recientemente creados',\n    'recently_created_books' => 'Libros recientemente creados',\n    'recently_created_shelves' => 'Estantes recientemente creados',\n    'recently_update' => 'Recientemente actualizado',\n    'recently_viewed' => 'Recientemente visto',\n    'recent_activity' => 'Actividad reciente',\n    'create_now' => 'Crear uno ahora',\n    'revisions' => 'Revisiones',\n    'meta_revision' => 'Revisión #:revisionCount',\n    'meta_created' => 'Creado :timeLength',\n    'meta_created_name' => 'Creado :timeLength por :user',\n    'meta_updated' => 'Actualizado :timeLength',\n    'meta_updated_name' => 'Actualizado :timeLength por :user',\n    'meta_owned_name' => 'Propiedad de :user',\n    'meta_reference_count' => 'Referido en :count página | Referido en :count paginas',\n    'entity_select' => 'Seleccione entidad',\n    'entity_select_lack_permission' => 'No tiene los permisos necesarios para seleccionar este elemento',\n    'images' => 'Imágenes',\n    'my_recent_drafts' => 'Mis borradores recientes',\n    'my_recently_viewed' => 'Mis visualizaciones recientes',\n    'my_most_viewed_favourites' => 'Mis favoritos más vistos',\n    'my_favourites' => 'Mis favoritos',\n    'no_pages_viewed' => 'No ha visto ninguna página',\n    'no_pages_recently_created' => 'Ninguna página ha sido creada recientemente',\n    'no_pages_recently_updated' => 'Ninguna página ha sido actualizada recientemente',\n    'export' => 'Exportar',\n    'export_html' => 'Archivo web',\n    'export_pdf' => 'Archivo PDF',\n    'export_text' => 'Archivo de texto',\n    'export_md' => 'Archivo Markdown',\n    'export_zip' => 'ZIP portable',\n    'default_template' => 'Plantilla de página por defecto',\n    'default_template_explain' => 'Asigne una plantilla de página que se utilizará como contenido predeterminado para todas las páginas creadas en este elemento. Tenga en cuenta que esto sólo se utilizará si el creador de páginas tiene acceso a la plantilla de página elegida.',\n    'default_template_select' => 'Seleccione una página de plantilla',\n    'import' => 'Importar',\n    'import_validate' => 'Validar importación',\n    'import_desc' => 'Importar libros, capítulos y páginas usando una exportación zip portable de la misma o distinta instancia. Seleccione un archivo ZIP para continuar. Después de que el archivo haya sido subido y validado, podrá configurar y confirmar la importación en la siguiente vista.',\n    'import_zip_select' => 'Seleccione archivo ZIP a subir',\n    'import_zip_validation_errors' => 'Se detectaron errores al validar el archivo ZIP proporcionado:',\n    'import_pending' => 'Importaciones pendientes',\n    'import_pending_none' => 'No se han iniciado importaciones.',\n    'import_continue' => 'Continuar importación',\n    'import_continue_desc' => 'Revise el contenido que debe importarse del archivo ZIP subido. Cuando esté listo, ejecute la importación para añadir su contenido a este sistema. El archivo de importación ZIP subido se eliminará automáticamente al terminar la importación correctamente.',\n    'import_details' => 'Detalles de la Importación',\n    'import_run' => 'Ejecutar Importación',\n    'import_size' => ':size tamaño archivo ZIP',\n    'import_uploaded_at' => 'Subido :relativeTime',\n    'import_uploaded_by' => 'Subido por',\n    'import_location' => 'Ubicación de Importación',\n    'import_location_desc' => 'Seleccione una ubicación de destino para el contenido importado. Necesitará los permisos pertinentes para crearlo dentro de la ubicación que elija.',\n    'import_delete_confirm' => '¿Está seguro de que desea eliminar esta importación?',\n    'import_delete_desc' => 'Esto eliminará el archivo ZIP de importación subido y no se puede deshacer.',\n    'import_errors' => 'Errores de Importación',\n    'import_errors_desc' => 'Se han producido los siguientes errores durante el intento de importación:',\n    'breadcrumb_siblings_for_page' => 'Navegar por páginas del mismo nivel',\n    'breadcrumb_siblings_for_chapter' => 'Navegar por capítulos del mismo nivel',\n    'breadcrumb_siblings_for_book' => 'Navegar por libros del mismo nivel',\n    'breadcrumb_siblings_for_bookshelf' => 'Navegar por estantes del mismo nivel',\n\n    // Permissions and restrictions\n    'permissions' => 'Permisos',\n    'permissions_desc' => 'Establezca los permisos aquí para anular los permisos por defecto proporcionados por los roles de usuario.',\n    'permissions_book_cascade' => 'Los permisos establecidos en los libros se aplicarán a sus capítulos y páginas, a menos que tengan sus propios permisos definidos.',\n    'permissions_chapter_cascade' => 'Los permisos establecidos en los capítulos se aplicarán a sus páginas, a menos que tengan sus propios permisos definidos.',\n    'permissions_save' => 'Guardar permisos',\n    'permissions_owner' => 'Propietario',\n    'permissions_role_everyone_else' => 'Todos los demás',\n    'permissions_role_everyone_else_desc' => 'Establecer permisos para todos los roles sin permisos específicos asignados.',\n    'permissions_role_override' => 'Reemplazar permisos para el rol',\n    'permissions_inherit_defaults' => 'Heredar valores por defecto',\n\n    // Search\n    'search_results' => 'Resultados de búsqueda',\n    'search_total_results_found' => 'Se han encontrado :count resultados|Se han encontrado :count resultados en total',\n    'search_clear' => 'Limpiar resultados',\n    'search_no_pages' => 'Ninguna página encontrada para la búsqueda',\n    'search_for_term' => 'Búsqueda por :term',\n    'search_more' => 'Más Resultados',\n    'search_advanced' => 'Búsqueda Avanzada',\n    'search_terms' => 'Términos de búsqueda',\n    'search_content_type' => 'Tipo de Contenido',\n    'search_exact_matches' => 'Coincidencias Exactas',\n    'search_tags' => 'Búsquedas Etiquetadas',\n    'search_options' => 'Opciones',\n    'search_viewed_by_me' => 'Vistas por mí',\n    'search_not_viewed_by_me' => 'No vistas por mí',\n    'search_permissions_set' => 'Permisos ajustados',\n    'search_created_by_me' => 'Creadas por mí',\n    'search_updated_by_me' => 'Actualizadas por mí',\n    'search_owned_by_me' => 'De mi propiedad',\n    'search_date_options' => 'Opciones de fecha',\n    'search_updated_before' => 'Actualizadas antes de',\n    'search_updated_after' => 'Actualizadas después de',\n    'search_created_before' => 'Creadas antes de',\n    'search_created_after' => 'Creadas después de',\n    'search_set_date' => 'fecha',\n    'search_update' => 'Actualizar Búsqueda',\n\n    // Shelves\n    'shelf' => 'Estante',\n    'shelves' => 'Estantes',\n    'x_shelves' => ':count estante|:count estantes',\n    'shelves_empty' => 'No hay estantes creados',\n    'shelves_create' => 'Crear estante',\n    'shelves_popular' => 'Estantes populares',\n    'shelves_new' => 'Estantes nuevos',\n    'shelves_new_action' => 'Nuevo estante',\n    'shelves_popular_empty' => 'Los estantes más populares aparecerán aquí.',\n    'shelves_new_empty' => 'Los libros más recientes aparecerán aquí.',\n    'shelves_save' => 'Guardar estante',\n    'shelves_books' => 'Libros en este estante',\n    'shelves_add_books' => 'Añadir libros a este estante',\n    'shelves_drag_books' => 'Arrastra los libros a continuación para añadirlos a este estante',\n    'shelves_empty_contents' => 'Este estante no tiene libros asignados',\n    'shelves_edit_and_assign' => 'Editar el estante para asignar libros',\n    'shelves_edit_named' => 'Editar estante :name',\n    'shelves_edit' => 'Editar estante',\n    'shelves_delete' => 'Eliminar estante',\n    'shelves_delete_named' => 'Eliminar estante :name',\n    'shelves_delete_explain' => \"Esto eliminará el estante con el nombre ':name'. Los libros que contenga no se eliminarán.\",\n    'shelves_delete_confirmation' => '¿Está seguro de que desea borrar este estante?',\n    'shelves_permissions' => 'Permisos del Estante',\n    'shelves_permissions_updated' => 'Permisos del estante actualizados',\n    'shelves_permissions_active' => 'Permisos del estante activos',\n    'shelves_permissions_cascade_warning' => 'Los permisos en los estantes no se aplican automáticamente a los libros que contengan. Esto se debe a que un libro puede existir en múltiples estantes. Sin embargo, los permisos pueden ser aplicados a los libros del estante utilizando la opción a continuación.',\n    'shelves_permissions_create' => 'Los permisos de creación de estantes sólo se utilizan para copiar los permisos a los libros contenidos utilizando la acción a continuación. No controlan la capacidad de crear libros.',\n    'shelves_copy_permissions_to_books' => 'Copiar permisos a los libros',\n    'shelves_copy_permissions' => 'Copiar permisos',\n    'shelves_copy_permissions_explain' => 'Esto aplicará los permisos de este estante para todos sus libros. Antes de activarlo, asegúrese de que todos los cambios de permisos para este estante han sido guardados.',\n    'shelves_copy_permission_success' => 'Permisos del estante copiados a :count libros',\n\n    // Books\n    'book' => 'Libro',\n    'books' => 'Libros',\n    'x_books' => ':count Libro|:count Libros',\n    'books_empty' => 'No hay libros creados',\n    'books_popular' => 'Libros populares',\n    'books_recent' => 'Libros recientes',\n    'books_new' => 'Libros nuevos',\n    'books_new_action' => 'Nuevo Libro',\n    'books_popular_empty' => 'Los libros más populares aparecerán aquí.',\n    'books_new_empty' => 'Los libros más recientes aparecerán aquí.',\n    'books_create' => 'Crear nuevo libro',\n    'books_delete' => 'Borrar libro',\n    'books_delete_named' => 'Borrar libro :bookName',\n    'books_delete_explain' => 'Esto borrará el libro con el nombre \\':bookName\\', Todos las páginas y capítulos serán borrados.',\n    'books_delete_confirmation' => '¿Está seguro de que desea borrar este libro?',\n    'books_edit' => 'Editar Libro',\n    'books_edit_named' => 'Editar Libro :bookName',\n    'books_form_book_name' => 'Nombre de libro',\n    'books_save' => 'Guardar libro',\n    'books_permissions' => 'Permisos del libro',\n    'books_permissions_updated' => 'Permisos del libro actualizados',\n    'books_empty_contents' => 'Ninguna página o capítulo ha sido creada para este libro.',\n    'books_empty_create_page' => 'Crear una nueva página',\n    'books_empty_sort_current_book' => 'Organizar el libro actual',\n    'books_empty_add_chapter' => 'Agregar un capítulo',\n    'books_permissions_active' => 'Permisos de libro activos',\n    'books_search_this' => 'Buscar en este libro',\n    'books_navigation' => 'Navegación de libro',\n    'books_sort' => 'Organizar contenido de libro',\n    'books_sort_desc' => 'Mueve capítulos y páginas dentro de un libro para reorganizar su contenido. Se pueden añadir otros libros que permiten mover fácilmente capítulos y páginas entre libros. Opcionalmente, se puede establecer una regla de ordenación automática para ordenar automáticamente el contenido de este libro cuando haya cambios.',\n    'books_sort_auto_sort' => 'Opción de ordenación automática',\n    'books_sort_auto_sort_active' => 'Opción de ordenación activa: sortName',\n    'books_sort_named' => 'Organizar libro :bookName',\n    'books_sort_name' => 'Organizar por Nombre',\n    'books_sort_created' => 'Organizar por Fecha de creación',\n    'books_sort_updated' => 'Organizar por Fecha de modificación',\n    'books_sort_chapters_first' => 'Capítulos al inicio',\n    'books_sort_chapters_last' => 'Capítulos al final ',\n    'books_sort_show_other' => 'Mostrar otros libros',\n    'books_sort_save' => 'Guardar nuevo orden',\n    'books_sort_show_other_desc' => 'Añada otros libros aquí para incluirlos en la ordenación, y permita una fácil reorganización entre libros.',\n    'books_sort_move_up' => 'Subir',\n    'books_sort_move_down' => 'Bajar',\n    'books_sort_move_prev_book' => 'Mover al libro anterior',\n    'books_sort_move_next_book' => 'Mover al siguiente libro',\n    'books_sort_move_prev_chapter' => 'Mover al capítulo anterior',\n    'books_sort_move_next_chapter' => 'Mover al siguiente capítulo',\n    'books_sort_move_book_start' => 'Mover al inicio del libro',\n    'books_sort_move_book_end' => 'Mover al final del libro',\n    'books_sort_move_before_chapter' => 'Mover a antes del capítulo',\n    'books_sort_move_after_chapter' => 'Mover a después del capítulo',\n    'books_copy' => 'Copiar Libro',\n    'books_copy_success' => 'Libro copiado correctamente',\n\n    // Chapters\n    'chapter' => 'Capítulo',\n    'chapters' => 'Capítulos',\n    'x_chapters' => ':count Capítulo|:count Capítulos',\n    'chapters_popular' => 'Capítulos populares',\n    'chapters_new' => 'Nuevo capítulo',\n    'chapters_create' => 'Crear nuevo capítulo',\n    'chapters_delete' => 'Borrar capítulo',\n    'chapters_delete_named' => 'Borrar capítulo :chapterName',\n    'chapters_delete_explain' => 'Esto eliminará el capítulo con el nombre \\':chapterName\\'. También se eliminarán todas las páginas que existen dentro de este capítulo.',\n    'chapters_delete_confirm' => '¿Está seguro de borrar este capítulo?',\n    'chapters_edit' => 'Editar capítulo',\n    'chapters_edit_named' => 'Editar capítulo :chapterName',\n    'chapters_save' => 'Guardar capítulo',\n    'chapters_move' => 'Mover capítulo',\n    'chapters_move_named' => 'Mover Capítulo :chapterName',\n    'chapters_copy' => 'Copiar Capítulo',\n    'chapters_copy_success' => 'Capítulo copiado correctamente',\n    'chapters_permissions' => 'Permisos de capítulo',\n    'chapters_empty' => 'No existen páginas en este capítulo.',\n    'chapters_permissions_active' => 'Permisos de capítulo activos',\n    'chapters_permissions_success' => 'Permisos de capítulo actualizados',\n    'chapters_search_this' => 'Buscar este capítulo',\n    'chapter_sort_book' => 'Organizar Libro',\n\n    // Pages\n    'page' => 'Página',\n    'pages' => 'Páginas',\n    'x_pages' => ':count Página|:count Páginas',\n    'pages_popular' => 'Páginas populares',\n    'pages_new' => 'Nueva página',\n    'pages_attachments' => 'Adjuntos',\n    'pages_navigation' => 'Navegación de página',\n    'pages_delete' => 'Borrar página',\n    'pages_delete_named' => 'Borrar página :pageName',\n    'pages_delete_draft_named' => 'Borrar borrador de página :pageName',\n    'pages_delete_draft' => 'Borrar borrador de página',\n    'pages_delete_success' => 'Página borrada',\n    'pages_delete_draft_success' => 'Borrador de página borrado',\n    'pages_delete_warning_template' => 'Esta página está en uso como plantilla de página predeterminada de libro o capítulo. Estos libros o capítulos ya no tendrán una plantilla de página predeterminada asignada después de eliminar esta página.',\n    'pages_delete_confirm' => '¿Está seguro de borrar esta página?',\n    'pages_delete_draft_confirm' => '¿Está seguro de que desea borrar este borrador de página?',\n    'pages_editing_named' => 'Editando página :pageName',\n    'pages_edit_draft_options' => 'Opciones de Borrador',\n    'pages_edit_save_draft' => 'Guardar borrador',\n    'pages_edit_draft' => 'Editar borrador de página',\n    'pages_editing_draft' => 'Editando borrador',\n    'pages_editing_page' => 'Editando página',\n    'pages_edit_draft_save_at' => 'Borrador guardado ',\n    'pages_edit_delete_draft' => 'Borrar borrador',\n    'pages_edit_delete_draft_confirm' => '¿Estás seguro de que deseas eliminar tus borradores de la página? Todos tus cambios, desde el último guardado completo, se perderán y el editor se actualizará con el estado de guardado de la última página.',\n    'pages_edit_discard_draft' => 'Descartar borrador',\n    'pages_edit_switch_to_markdown' => 'Cambiar a Editor Markdown',\n    'pages_edit_switch_to_markdown_clean' => '(Limpiar Contenido)',\n    'pages_edit_switch_to_markdown_stable' => '(Contenido Estable)',\n    'pages_edit_switch_to_wysiwyg' => 'Cambiar a Editor WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg' => 'Cambiar a nuevo editor WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(En prueba beta)',\n    'pages_edit_set_changelog' => 'Ajustar Log de cambios',\n    'pages_edit_enter_changelog_desc' => 'Introduzca una breve descripción de los cambios que ha realizado',\n    'pages_edit_enter_changelog' => 'Entrar al Log de cambios',\n    'pages_editor_switch_title' => 'Cambiar editor',\n    'pages_editor_switch_are_you_sure' => '¿Está seguro de que desea cambiar el editor de esta página?',\n    'pages_editor_switch_consider_following' => 'Considere lo siguiente al cambiar de editor:',\n    'pages_editor_switch_consideration_a' => 'Una vez guardado, el nuevo editor será utilizado por cualquier usuario en el futuro, incluyendo aquellos que no puedan cambiar el tipo de editor por sí mismos.',\n    'pages_editor_switch_consideration_b' => 'Esto puede llevar a una pérdida de detalle y sintaxis en ciertas circunstancias.',\n    'pages_editor_switch_consideration_c' => 'Cambios en etiquetas o en el registro de cambios, realizados desde el último guardado, no persistirán a través de este cambio.',\n    'pages_save' => 'Guardar página',\n    'pages_title' => 'Título de página',\n    'pages_name' => 'Nombre de página',\n    'pages_md_editor' => 'Editor',\n    'pages_md_preview' => 'Previsualizar',\n    'pages_md_insert_image' => 'Insertar Imagen',\n    'pages_md_insert_link' => 'Insertar link de entidad',\n    'pages_md_insert_drawing' => 'Insertar Dibujo',\n    'pages_md_show_preview' => 'Mostrar vista previa',\n    'pages_md_sync_scroll' => 'Sincronizar desplazamiento de vista previa',\n    'pages_md_plain_editor' => 'Editor de texto plano',\n    'pages_drawing_unsaved' => 'Encontrado dibujo sin guardar',\n    'pages_drawing_unsaved_confirm' => 'Se encontraron datos no guardados del dibujo de un intento de guardado fallido. ¿Desea restaurar y continuar editando el dibujo no guardado?',\n    'pages_not_in_chapter' => 'La página no está en un capítulo',\n    'pages_move' => 'Mover página',\n    'pages_copy' => 'Copiar página',\n    'pages_copy_desination' => 'Destino de la copia',\n    'pages_copy_success' => 'Página copiada a correctamente',\n    'pages_permissions' => 'Permisos de página',\n    'pages_permissions_success' => 'Permisos de página actualizados',\n    'pages_revision' => 'Revisión',\n    'pages_revisions' => 'Revisiones de página',\n    'pages_revisions_desc' => 'A continuación se listan todas las revisiones pasadas de esta página. Puede volver la vista atrás, comparar y restaurar versiones antiguas de la página si los permisos lo permiten. Es posible que el historial completo de la página no se refleje en esta sección. Dependiendo de la configuración del sistema, las viejas revisiones podrían ser eliminadas automáticamente.',\n    'pages_revisions_named' => 'Revisiones de página para :pageName',\n    'pages_revision_named' => 'Revisión de página para :pageName',\n    'pages_revision_restored_from' => 'Restaurado de #:id; :summary',\n    'pages_revisions_created_by' => 'Creado por',\n    'pages_revisions_date' => 'Fecha de revisión',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Número de Revisión',\n    'pages_revisions_numbered' => 'Revisión #:id',\n    'pages_revisions_numbered_changes' => 'Revisión #:id Cambios',\n    'pages_revisions_editor' => 'Tipo de Editor',\n    'pages_revisions_changelog' => 'Log de cambios',\n    'pages_revisions_changes' => 'Cambios',\n    'pages_revisions_current' => 'Versión actual',\n    'pages_revisions_preview' => 'Previsualizar',\n    'pages_revisions_restore' => 'Restaurar',\n    'pages_revisions_none' => 'Esta página no tiene revisiones',\n    'pages_copy_link' => 'Copiar Enlace',\n    'pages_edit_content_link' => 'Ir a la sección en el editor',\n    'pages_pointer_enter_mode' => 'Modo de selección de sección',\n    'pages_pointer_label' => 'Opciones de sección de página',\n    'pages_pointer_permalink' => 'Sección de enlace permanente de página',\n    'pages_pointer_include_tag' => 'Sección de página incluyendo etiqueta',\n    'pages_pointer_toggle_link' => 'Modo de enlace permanente, presiona para mostrar la etiqueta',\n    'pages_pointer_toggle_include' => 'Modo de etiqueta, presiona para mostrar enlace permanente',\n    'pages_permissions_active' => 'Permisos de página activos',\n    'pages_initial_revision' => 'Publicación inicial',\n    'pages_references_update_revision' => 'Actualización automática de enlaces internos',\n    'pages_initial_name' => 'Página nueva',\n    'pages_editing_draft_notification' => 'Está actualmente editando un borrador que fue guardado por última vez el :timeDiff.',\n    'pages_draft_edited_notification' => 'Esta página ha sido actualizada desde ese momento. Se recomienda que cancele este borrador.',\n    'pages_draft_page_changed_since_creation' => 'Esta página ha sido actualizada desde que se creó este borrador. Se recomienda descartar este borrador o tener cuidado de no sobrescribir ningún cambio en la página.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count usuarios han comenzado a editar esta página',\n        'start_b' => ':userName ha comenzado a editar esta página',\n        'time_a' => 'desde que la página fue actualizada',\n        'time_b' => 'en los últimos :minCount minutos',\n        'message' => ':start :time. ¡Ten cuidado de no sobreescribir los cambios del otro usuario!',\n    ],\n    'pages_draft_discarded' => '¡Borrador descartado! El editor ha sido actualizado con el contenido de la página actual',\n    'pages_draft_deleted' => '¡Borrador eliminado! El editor ha sido actualizado con el contenido actual de la página',\n    'pages_specific' => 'Página específica',\n    'pages_is_template' => 'Página es plantilla',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Mostrar/ocultar barra lateral',\n    'page_tags' => 'Etiquetas de Página',\n    'chapter_tags' => 'Etiquetas de Capítulo',\n    'book_tags' => 'Etiquetas de Libro',\n    'shelf_tags' => 'Etiquetas de Estante',\n    'tag' => 'Etiqueta',\n    'tags' =>  'Etiquetas',\n    'tags_index_desc' => 'Las etiquetas se pueden aplicar al contenido dentro del sistema para aplicar una forma flexible de categorización. Las etiquetas pueden tener tanto una clave como un valor, siendo el valor opcional. Una vez aplicado, el contenido puede ser consultado usando el nombre y el valor de la etiqueta.',\n    'tag_name' =>  'Nombre de la Etiqueta',\n    'tag_value' => 'Valor de la etiqueta (Opcional)',\n    'tags_explain' => \"Agrege algunas etiquetas para mejorar la categorización de su contenido. \\n Puede asignar un valor a una etiqueta para una organización a mayor detalle.\",\n    'tags_add' => 'Agregar otra etiqueta',\n    'tags_remove' => 'Eliminar esta etiqueta',\n    'tags_usages' => 'Uso total de etiquetas',\n    'tags_assigned_pages' => 'Asignadas a páginas',\n    'tags_assigned_chapters' => 'Asignadas a capitulos',\n    'tags_assigned_books' => 'Asignadas a libros',\n    'tags_assigned_shelves' => 'Asignadas a estantes',\n    'tags_x_unique_values' => ':count valores únicos',\n    'tags_all_values' => 'Todos los valores',\n    'tags_view_tags' => 'Ver etiquetas',\n    'tags_view_existing_tags' => 'Ver etiquetas existentes',\n    'tags_list_empty_hint' => 'Las etiquetas se pueden asignar a través de la barra lateral del editor de páginas o mientras se editan los detalles de un libro, capítulo o estante.',\n    'attachments' => 'Adjuntos',\n    'attachments_explain' => 'Subir ficheros o agregar enlaces para mostrar en la página. Estos son visibles en la barra lateral de la página.',\n    'attachments_explain_instant_save' => 'Los cambios son guardados de manera instantánea .',\n    'attachments_upload' => 'Subir Archivo',\n    'attachments_link' => 'Adjuntar Enlace',\n    'attachments_upload_drop' => 'También puedes arrastrar y soltar un archivo aquí para subirlo como un archivo adjunto.',\n    'attachments_set_link' => 'Ajustar Enlace',\n    'attachments_delete' => '¿Está seguro de que quiere eliminar este archivo adjunto?',\n    'attachments_dropzone' => 'Arrastre aquí archivos para subirlos',\n    'attachments_no_files' => 'No se han subido ficheros',\n    'attachments_explain_link' => 'Puede agregar un enlace si prefiere no subir un archivo. Puede ser un enlace a otra página o un enlace a un fichero en la nube.',\n    'attachments_link_name' => 'Nombre del Enlace',\n    'attachment_link' => 'Enlace adjunto',\n    'attachments_link_url' => 'Enlace a fichero',\n    'attachments_link_url_hint' => 'Url del sitio o fichero',\n    'attach' => 'Adjuntar',\n    'attachments_insert_link' => 'Añadir enlace al adjunto en la página',\n    'attachments_edit_file' => 'Editar fichero',\n    'attachments_edit_file_name' => 'Nombre del fichero',\n    'attachments_edit_drop_upload' => 'Arrastre a los ficheros o haga click aquí para subir y sobreescribir',\n    'attachments_order_updated' => 'Orden de adjuntos actualizado',\n    'attachments_updated_success' => 'Detalles de adjuntos actualizados',\n    'attachments_deleted' => 'Adjunto borrado',\n    'attachments_file_uploaded' => 'Fichero subido éxitosamente',\n    'attachments_file_updated' => 'Fichero actualizado éxitosamente',\n    'attachments_link_attached' => 'Enlace agregado éxitosamente a la página',\n    'templates' => 'Plantillas',\n    'templates_set_as_template' => 'La página es una plantilla',\n    'templates_explain_set_as_template' => 'Puede ajustar esta página como una plantilla, así su contenido puede emplearse al crear una nueva página. Otros usuarios podrán utilizar esta plantilla si tienen permisos de lectura sobre esta página.',\n    'templates_replace_content' => 'Reemplazar el contenido de la página',\n    'templates_append_content' => 'Añadir después del contenido de la página',\n    'templates_prepend_content' => 'Añadir antes del contenido de la página',\n\n    // Profile View\n    'profile_user_for_x' => 'Usuario para :time',\n    'profile_created_content' => 'Contenido creado',\n    'profile_not_created_pages' => ':userName no ha creado ninguna página',\n    'profile_not_created_chapters' => ':userName no ha creado ningún capítulo',\n    'profile_not_created_books' => ':userName no ha creado ningún libro',\n    'profile_not_created_shelves' => ':userName no ha creado ningún estante',\n\n    // Comments\n    'comment' => 'Comentario',\n    'comments' => 'Comentarios',\n    'comment_add' => 'Añadir Comentario',\n    'comment_none' => 'No hay comentarios para mostrar',\n    'comment_placeholder' => 'Introduzca su comentario aquí',\n    'comment_thread_count' => ':count hilo de comentarios|:count hilos de comentarios',\n    'comment_archived_count' => ':count Archivados',\n    'comment_archived_threads' => 'Hilos archivados',\n    'comment_save' => 'Guardar comentario',\n    'comment_new' => 'Nuevo Comentario',\n    'comment_created' => 'comentado :createDiff',\n    'comment_updated' => 'Actualizado :updateDiff por :username',\n    'comment_updated_indicator' => 'Actualizado',\n    'comment_deleted_success' => 'Comentario borrado',\n    'comment_created_success' => 'Comentario añadido',\n    'comment_updated_success' => 'Comentario actualizado',\n    'comment_archive_success' => 'Comentario archivado',\n    'comment_unarchive_success' => 'Comentario desarchivado',\n    'comment_view' => 'Ver comentario',\n    'comment_jump_to_thread' => 'Ir al hilo',\n    'comment_delete_confirm' => '¿Está seguro de que quiere borrar este comentario?',\n    'comment_in_reply_to' => 'En respuesta a :commentId',\n    'comment_reference' => 'Referencia',\n    'comment_reference_outdated' => '(obsoleto)',\n    'comment_editor_explain' => 'Estos son los comentarios que se han escrito en esta página. Los comentarios se pueden añadir y administrar cuando se ve la página guardada.',\n\n    // Revision\n    'revision_delete_confirm' => '¿Está seguro de que desea eliminar esta revisión?',\n    'revision_restore_confirm' => '¿Está seguro de que desea restaurar esta revisión? El contenido actual de la página será reemplazado.',\n    'revision_cannot_delete_latest' => 'No se puede eliminar la última revisión.',\n\n    // Copy view\n    'copy_consider' => 'Por favor, tenga en cuenta lo siguiente al copiar el contenido.',\n    'copy_consider_permissions' => 'Los ajustes de permisos personalizados no serán copiados.',\n    'copy_consider_owner' => 'Usted se convertirá en el dueño de todo el contenido copiado.',\n    'copy_consider_images' => 'Los archivos de imagen de de las páginas no serán duplicados y las imágenes originales conservarán su relación con la página a la que fueron subidos originalmente.',\n    'copy_consider_attachments' => 'Los archivos adjuntos de la página no serán copiados.',\n    'copy_consider_access' => 'Un cambio de ubicación, propietario o permisos puede resultar en que este contenido sea accesible para aquellos que anteriormente no tuvieran acceso.',\n\n    // Conversions\n    'convert_to_shelf' => 'Convertir a Estante',\n    'convert_to_shelf_contents_desc' => 'Puedes convertir este libro a un nuevo estante con el mismo contenido. Los capítulos contenidos en este libro se convertirán en libros nuevos. Si este libro contiene alguna página, que no esté en un capítulo, este libro será renombrado y contendrá tales páginas, y este libro pasará a formar parte del nuevo estante.',\n    'convert_to_shelf_permissions_desc' => 'Cualquier permiso establecido en este libro será copiado al nuevo estante y a todos los nuevos libros que no tengan sus propios permisos configurados. Tenga en cuenta que los permisos de los estantes no se aplican automáticamente sobre el contenido en su interior, como lo hacen para los libros.',\n    'convert_book' => 'Convertir Libro',\n    'convert_book_confirm' => '¿Está seguro de que desea borrar este libro?',\n    'convert_undo_warning' => 'Esto no puede revertirse de forma sencilla.',\n    'convert_to_book' => 'Convertir a Libro',\n    'convert_to_book_desc' => 'Puede convertir este capítulo en un nuevo libro con el mismo contenido. Cualquier permiso establecido en este capítulo será copiado al nuevo libro pero cualquier permiso heredado, del libro padre, no se copiará lo que podría conducir a un cambio de control de acceso.',\n    'convert_chapter' => 'Convertir Capítulo',\n    'convert_chapter_confirm' => '¿Estás seguro de que quieres convertir este capítulo?',\n\n    // References\n    'references' => 'Referencias',\n    'references_none' => 'No hay referencias a este elemento.',\n    'references_to_desc' => 'A continuación se muestran todas las páginas en el sistema que enlazan a este elemento.',\n\n    // Watch Options\n    'watch' => 'Suscribirme',\n    'watch_title_default' => 'Preferencias por defecto',\n    'watch_desc_default' => 'Revertir suscripciones a tus preferencias de notificación por defecto.',\n    'watch_title_ignore' => 'Ignorar',\n    'watch_desc_ignore' => 'Ignorar todas las notificaciones, incluyendo las de las preferencias a nivel de usuario.',\n    'watch_title_new' => 'Nuevas páginas',\n    'watch_desc_new' => 'Notificar cuando se crea una nueva página dentro de este elemento.',\n    'watch_title_updates' => 'Todas las actualizaciones de páginas',\n    'watch_desc_updates' => 'Notificar todos los cambios de páginas y páginas nuevas.',\n    'watch_desc_updates_page' => 'Notificar todos los cambios en la página.',\n    'watch_title_comments' => 'Todas las actualizaciones de páginas y comentarios',\n    'watch_desc_comments' => 'Notificar sobre todas las páginas nuevas, cambios de página y nuevos comentarios.',\n    'watch_desc_comments_page' => 'Notificar los cambios en las páginas y los nuevos comentarios.',\n    'watch_change_default' => 'Cambiar preferencias de notificación por defecto',\n    'watch_detail_ignore' => 'Ignorar notificaciones',\n    'watch_detail_new' => 'Suscripciones de nuevas páginas',\n    'watch_detail_updates' => 'Suscripciones de nuevas páginas y actualizaciones de páginas',\n    'watch_detail_comments' => 'Suscripciones de nuevas páginas, actualizaciones de páginas y comentarios',\n    'watch_detail_parent_book' => 'Subscripciones por libro contenedor',\n    'watch_detail_parent_book_ignore' => 'Ignorando a través del libro contenedor',\n    'watch_detail_parent_chapter' => 'Subscripciones por capítulo superior',\n    'watch_detail_parent_chapter_ignore' => 'Ignorar por capítulo superior',\n];\n"
  },
  {
    "path": "lang/es/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'No tienes permisos para visualizar la página solicitada.',\n    'permissionJson' => 'No tienes permisos para ejecutar la acción solicitada.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Un usuario con el correo electrónico :email ya existe pero con credenciales diferentes.',\n    'auth_pre_register_theme_prevention' => 'La cuenta de usuario no pudo ser registrada con los detalles proporcionados',\n    'email_already_confirmed' => 'El correo electrónico ya ha sido confirmado, intente acceder a la aplicación.',\n    'email_confirmation_invalid' => 'Este token de confirmación no es válido o ya ha sido usado, intente registrar uno nuevamente.',\n    'email_confirmation_expired' => 'El token de confirmación ha expirado, un nuevo email de confirmacón ha sido enviado.',\n    'email_confirmation_awaiting' => 'La dirección de correo electrónico de la cuenta en uso debe ser confirmada',\n    'ldap_fail_anonymous' => 'El acceso con LDAP ha fallado usando binding anónimo',\n    'ldap_fail_authed' => 'El acceso LDAP ha fallado usando el dn & contraseña enviados',\n    'ldap_extension_not_installed' => 'La extensión LDAP PHP no se encuentra instalada',\n    'ldap_cannot_connect' => 'No se puede conectar con el servidor ldap, la conexión inicial ha fallado',\n    'saml_already_logged_in' => 'Ya estás conectado',\n    'saml_no_email_address' => 'No se pudo encontrar una dirección de correo electrónico, para este usuario, en los datos proporcionados por el sistema de autenticación externo',\n    'saml_invalid_response_id' => 'La solicitud del sistema de autenticación externo no está reconocida por un proceso iniciado por esta aplicación. Navegar hacia atrás después de un inicio de sesión podría causar este problema.',\n    'saml_fail_authed' => 'El inicio de sesión con :system falló, el sistema no proporcionó una autorización correcta',\n    'oidc_already_logged_in' => 'Ya tenías la sesión iniciada',\n    'oidc_no_email_address' => 'No se pudo encontrar una dirección de correo electrónico, para este usuario, en los datos proporcionados por el sistema de autenticación externo',\n    'oidc_fail_authed' => 'El inicio de sesión con :system falló, el sistema no proporcionó una autorización correcta',\n    'social_no_action_defined' => 'Acción no definida',\n    'social_login_bad_response' => \"Se ha recibido un error durante el acceso con :socialAccount error: \\n:error\",\n    'social_account_in_use' => 'la cuenta :socialAccount ya se encuentra en uso, intente acceder a través de la opción :socialAccount .',\n    'social_account_email_in_use' => 'El correo electrónico :email ya se encuentra en uso. Si ya dispone de una cuenta puede acceder a través de su cuenta :socialAccount desde la configuración de perfil.',\n    'social_account_existing' => 'La cuenta :socialAccount ya se encuentra asignada a su perfil.',\n    'social_account_already_used_existing' => 'La cuenta :socialAccount ya está siendo usada por otro usuario.',\n    'social_account_not_used' => 'La cuenta :socialAccount no está asociada a ningún usuario. Por favor adjúntela a su configuración de perfil. ',\n    'social_account_register_instructions' => 'Si no dispone de una cuenta, puede registrar una cuenta usando la opción de :socialAccount .',\n    'social_driver_not_found' => 'Driver social no encontrado',\n    'social_driver_not_configured' => 'Su configuración :socialAccount no es correcta.',\n    'invite_token_expired' => 'Este enlace de invitación ha expirado. Puede resetear la contraseña de su cuenta como alternativa.',\n    'login_user_not_found' => 'No se pudo encontrar un usuario para esta acción.',\n\n    // System\n    'path_not_writable' => 'El fichero no pudo ser subido a la ruta :filePath . Asegúrese de que es escribible por el servidor.',\n    'cannot_get_image_from_url' => 'No se puede obtener la imagen desde :url',\n    'cannot_create_thumbs' => 'El servidor no puede crear la miniatura de la imagen. Compruebe que tiene la extensión PHP GD instalada.',\n    'server_upload_limit' => 'El servidor no permite la subida de ficheros de este tamaño. Intente subir un fichero de menor tamaño.',\n    'server_post_limit' => 'El servidor no puede recibir la cantidad de datos proporcionados. Inténtelo de nuevo con menos datos o un archivo más pequeño.',\n    'uploaded'  => 'El servidor no permite la subida de ficheros de este tamaño. Intente subir un fichero de menor tamaño.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Ha ocurrido un error al subir la imagen',\n    'image_upload_type_error' => 'El tipo de imagen que se quiere subir no es válido',\n    'image_upload_replace_type' => 'Las imágenes para sustituir deben ser del mismo tipo',\n    'image_upload_memory_limit' => 'No se pudo gestionar la subida de imágenes y/o crear miniaturas debido a los límites de recursos del sistema.',\n    'image_thumbnail_memory_limit' => 'Error al crear imágenes de distintos tamaños debido a los límites de los recursos del sistema.',\n    'image_gallery_thumbnail_memory_limit' => 'Error al crear imágenes de previsualización de la galería debido a los límites de los recursos del sistema.',\n    'drawing_data_not_found' => 'No se han podido cargar los datos del dibujo. Puede que el archivo de dibujo ya no exista o que no tenga permiso para acceder a él.',\n\n    // Attachments\n    'attachment_not_found' => 'No se encontró el adjunto',\n    'attachment_upload_error' => 'Ha ocurrido un error al subir el archivo adjunto',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Fallo al guardar borrador. Asegúrese de que tiene conexión a Internet antes de guardar este borrador',\n    'page_draft_delete_fail' => 'Error al eliminar el borrador de la página y obtener el último contenido guardado',\n    'page_custom_home_deletion' => 'No se puede borrar una página mientras esté configurada como página de inicio',\n\n    // Entities\n    'entity_not_found' => 'Entidad no encontrada',\n    'bookshelf_not_found' => 'Estante no encontrado',\n    'book_not_found' => 'Libro no encontrado',\n    'page_not_found' => 'Página no encontrada',\n    'chapter_not_found' => 'Capítulo no encontrado',\n    'selected_book_not_found' => 'El libro seleccionado no fue encontrado',\n    'selected_book_chapter_not_found' => 'El libro o capítulo seleccionado no fue encontrado',\n    'guests_cannot_save_drafts' => 'Los invitados no pueden guardar borradores',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'No se puede borrar el único administrador',\n    'users_cannot_delete_guest' => 'No se puede borrar el usuario invitado',\n    'users_could_not_send_invite' => 'No se pudo crear el usuario porque no se pudo enviar el correo de invitación',\n\n    // Roles\n    'role_cannot_be_edited' => 'Este rol no puede ser editado',\n    'role_system_cannot_be_deleted' => 'Este rol es un rol de sistema y no puede ser borrado',\n    'role_registration_default_cannot_delete' => 'Este rol no puede ser borrado mientras sea el rol por defecto de nuevos registros',\n    'role_cannot_remove_only_admin' => 'Este usuario es el único usuario asignado al rol de administrador. Asigna primero este rol a otro usuario antes de eliminarlo.',\n\n    // Comments\n    'comment_list' => 'Se ha producido un error al buscar los comentarios.',\n    'cannot_add_comment_to_draft' => 'No puedes añadir comentarios a un borrador.',\n    'comment_add' => 'Se ha producido un error al añadir el comentario.',\n    'comment_delete' => 'Se ha producido un error al eliminar el comentario.',\n    'empty_comment' => 'No se puede agregar un comentario vacío.',\n\n    // Error pages\n    '404_page_not_found' => 'Página no encontrada',\n    'sorry_page_not_found' => 'Lo sentimos, la página a la que intenta acceder no pudo ser encontrada.',\n    'sorry_page_not_found_permission_warning' => 'Si esperaba que esta página existiera, puede que no tenga permiso para verla.',\n    'image_not_found' => 'Imagen no encontrada',\n    'image_not_found_subtitle' => 'Lo sentimos, no se pudo encontrar el archivo de imagen que estaba buscando.',\n    'image_not_found_details' => 'Si esperaba que esta imagen existiera, podría haber sido eliminada.',\n    'return_home' => 'Volver a la página de inicio',\n    'error_occurred' => 'Ha ocurrido un error',\n    'app_down' => 'La aplicación :appName se encuentra caída en este momento',\n    'back_soon' => 'Volverá a estar operativa pronto.',\n\n    // Import\n    'import_zip_cant_read' => 'No se pudo leer el archivo ZIP.',\n    'import_zip_cant_decode_data' => 'No se pudo encontrar y decodificar el archivo data.json. en el archivo ZIP.',\n    'import_zip_no_data' => 'Los datos del archivo ZIP no contienen ningún libro, capítulo o contenido de página.',\n    'import_zip_data_too_large' => 'El contenido del ZIP data.json excede el tamaño máximo de carga configurado.',\n    'import_validation_failed' => 'Error al validar la importación del ZIP con errores:',\n    'import_zip_failed_notification' => 'Error al importar archivo ZIP.',\n    'import_perms_books' => 'Le faltan los permisos necesarios para crear libros.',\n    'import_perms_chapters' => 'Le faltan los permisos necesarios para crear capítulos.',\n    'import_perms_pages' => 'Le faltan los permisos necesarios para crear páginas.',\n    'import_perms_images' => 'Le faltan los permisos necesarios para crear imágenes.',\n    'import_perms_attachments' => 'Le faltan los permisos necesarios para crear adjuntos.',\n\n    // API errors\n    'api_no_authorization_found' => 'No se encontró ningún token de autorización en la solicitud',\n    'api_bad_authorization_format' => 'Se ha encontrado un token de autorización en la solicitud pero el formato era incorrecto',\n    'api_user_token_not_found' => 'No se ha encontrado un token API que corresponda con el token de autorización proporcionado',\n    'api_incorrect_token_secret' => 'El secreto proporcionado para el token API usado es incorrecto',\n    'api_user_no_api_permission' => 'El propietario del token API usado no tiene permiso para hacer llamadas API',\n    'api_user_token_expired' => 'El token de autorización usado ha caducado',\n    'api_cookie_auth_only_get' => 'Sólo se permiten peticiones GET cuando se utiliza el API con autenticación basada en cookies',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Error al enviar un email de prueba:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'La URL no coincide con los hosts SSR permitidos',\n];\n"
  },
  {
    "path": "lang/es/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Nuevo comentario en la página: :pageName',\n    'new_comment_intro' => 'Un usuario ha comentado en una página de :appName:',\n    'new_page_subject' => 'Nueva página: :pageName',\n    'new_page_intro' => 'Una nueva página ha sido creada en :appName:',\n    'updated_page_subject' => 'Página actualizada: :pageName',\n    'updated_page_intro' => 'Una página ha sido actualizada en :appName:',\n    'updated_page_debounce' => 'Para prevenir notificaciones en masa, durante un tiempo no se enviarán notificaciones para futuras ediciones de esta página por el mismo editor.',\n    'comment_mention_subject' => 'Ha sido mencionado en un comentario en la página: :pageName',\n    'comment_mention_intro' => 'Fue mencionado en un comentario en :appName:',\n\n    'detail_page_name' => 'Nombre de página:',\n    'detail_page_path' => 'Ruta de la página:',\n    'detail_commenter' => 'Autor del comentario:',\n    'detail_comment' => 'Comentario:',\n    'detail_created_by' => 'Creado por:',\n    'detail_updated_by' => 'Actualizado por:',\n\n    'action_view_comment' => 'Ver comentario',\n    'action_view_page' => 'Ver página',\n\n    'footer_reason' => 'Esta notificación fue enviada porque :link cubre este tipo de actividad para este artículo.',\n    'footer_reason_link' => 'sus preferencias de notificación',\n];\n"
  },
  {
    "path": "lang/es/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Anterior',\n    'next'     => 'Siguiente &raquo;',\n\n];\n"
  },
  {
    "path": "lang/es/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'La contraseña debe ser como mínimo de seis caracteres y coincidir con la confirmación.',\n    'user' => \"No podemos encontrar un usuario con esta dirección de correo electrónico.\",\n    'token' => 'El token de modificación de contraseña no es válido para esta dirección de correo electrónico.',\n    'sent' => '¡Hemos enviado a su cuenta de e-mail un enlace para restaurar su contraseña!',\n    'reset' => '¡Su contraseña ha sido restaurada!',\n\n];\n"
  },
  {
    "path": "lang/es/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Mi cuenta',\n\n    'shortcuts' => 'Accesos directos',\n    'shortcuts_interface' => 'Preferencias de acceso directo en la interfaz',\n    'shortcuts_toggle_desc' => 'Aquí puede activar o desactivar los accesos directos de la interfaz, utilizados para la navegación y las acciones.',\n    'shortcuts_customize_desc' => 'Puede personalizar cada uno de los accesos directos a continuación. Simplemente pulse la combinación de teclas deseada después de seleccionar la entrada para un acceso directo.',\n    'shortcuts_toggle_label' => 'Accesos directos habilitados',\n    'shortcuts_section_navigation' => 'Navegación',\n    'shortcuts_section_actions' => 'Acciones comunes',\n    'shortcuts_save' => 'Guardar accesos directos',\n    'shortcuts_overlay_desc' => 'Nota: Cuando se activan los accesos directos se puede mostrar la ayuda presionando la tecla \"?\" que resaltará los accesos rápidos disponibles para las acciones actualmente visibles en la pantalla.',\n    'shortcuts_update_success' => '¡Las preferencias de accesos directos han sido actualizadas!',\n    'shortcuts_overview_desc' => 'Gestione los atajos de teclado que puede utilizar para navegar por la interfaz de usuario del sistema.',\n\n    'notifications' => 'Preferencias de notificaciones',\n    'notifications_desc' => 'Controle las notificaciones por correo electrónico que recibe cuando se realiza cierta actividad dentro del sistema.',\n    'notifications_opt_own_page_changes' => 'Notificar sobre los cambios en las páginas en las que soy propietario',\n    'notifications_opt_own_page_comments' => 'Notificar sobre comentarios en las páginas en las que soy propietario',\n    'notifications_opt_comment_mentions' => 'Notificarme cuando he sido mencionado en un comentario',\n    'notifications_opt_comment_replies' => 'Notificar sobre respuestas a mis comentarios',\n    'notifications_save' => 'Guardar preferencias',\n    'notifications_update_success' => '¡Se han actualizado las preferencias de notificaciones!',\n    'notifications_watched' => 'Elementos vistos e ignorados',\n    'notifications_watched_desc' => 'A continuación se muestran los elementos que tienen preferencias personalizadas de monitorización. Para actualizar sus preferencias, vea el elemento y las opciones se mostrarán en la barra lateral.',\n\n    'auth' => 'Acceso y seguridad',\n    'auth_change_password' => 'Cambiar contraseña',\n    'auth_change_password_desc' => 'Cambie la contraseña que utiliza para iniciar sesión en la aplicación. Debe tener al menos 8 caracteres.',\n    'auth_change_password_success' => '¡La contraseña ha sido actualizada!',\n\n    'profile' => 'Detalles del perfil',\n    'profile_desc' => 'Administre los detalles de su cuenta que le representa a otros usuarios, además de los detalles que se utilizan para la comunicación y la personalización del sistema.',\n    'profile_view_public' => 'Ver perfil público',\n    'profile_name_desc' => 'Configure el nombre que será visible para otros usuarios del sistema a través de la actividad que realiza, y el contenido que posee.',\n    'profile_email_desc' => 'Este correo electrónico se utilizará para las notificaciones y, dependiendo de la autenticación activa del sistema, el acceso del sistema.',\n    'profile_email_no_permission' => 'Lamentablemente no tiene permiso para cambiar su dirección de correo electrónico. Si desea cambiar esto, necesitará pedir a un administrador que lo cambie por usted.',\n    'profile_avatar_desc' => 'Seleccione una imagen pública que verán los demás en el sistema. Idealmente esta imagen debe ser cuadrada y alrededor de 256px de anchura y altura.',\n    'profile_admin_options' => 'Opciones de administrador',\n    'profile_admin_options_desc' => 'Opciones adicionales de administrador, como por ejemplo administrar asignaciones de rol, se pueden encontrar para su cuenta de usuario en el área de \"Ajustes > Usuarios\" de la aplicación.',\n\n    'delete_account' => 'Eliminar cuenta',\n    'delete_my_account' => 'Eliminar mi cuenta',\n    'delete_my_account_desc' => 'Esto eliminará completamente su cuenta de usuario del sistema. No podrá recuperar esta cuenta o revertir esta acción. El contenido que ha creado, como páginas creadas e imágenes subidas, permanecerá.',\n    'delete_my_account_warning' => '¿Está seguro de que desea eliminar su cuenta?',\n];\n"
  },
  {
    "path": "lang/es/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Ajustes',\n    'settings_save' => 'Guardar ajustes',\n    'system_version' => 'Versión de BookStack',\n    'categories' => 'Categorías',\n\n    // App Settings\n    'app_customization' => 'Personalización',\n    'app_features_security' => 'Características y seguridad',\n    'app_name' => 'Nombre de la aplicación',\n    'app_name_desc' => 'Este nombre se muestra en la cabecera y en cualquier correo electrónico enviado por el sistema.',\n    'app_name_header' => 'Mostrar el nombre en la cabecera',\n    'app_public_access' => 'Acceso público',\n    'app_public_access_desc' => 'Activar esta opción permitirá a los visitantes que no hayan iniciado sesión, poder ver el contenido de tu BookStack.',\n    'app_public_access_desc_guest' => 'El acceso público para visitantes puede ser controlado a través del usuario \"Guest\".',\n    'app_public_access_toggle' => 'Permitir acceso público',\n    'app_public_viewing' => '¿Permitir acceso público?',\n    'app_secure_images' => 'Mayor seguridad para subir imágenes',\n    'app_secure_images_toggle' => 'Habilitar mayor seguridad en la subida de imágenes',\n    'app_secure_images_desc' => 'Por razones de rendimiento, todas las imágenes son públicas. Esta opción agrega una cadena de texto larga difícil de adivinar. Asegúrese que los índices de directorio no están habilitados para evitar el acceso fácil a las imágenes.',\n    'app_default_editor' => 'Editor predeterminado',\n    'app_default_editor_desc' => 'Seleccione qué editor se utilizará por defecto cuando se editen nuevas páginas. Esto se puede anular a nivel de página si los permisos lo permiten.',\n    'app_custom_html' => 'Contenido de cabecera HTML personalizado',\n    'app_custom_html_desc' => 'Cualquier contenido agregado aquí será insertado al final de la sección <head> de cada página. Esto es útil para sobreescribir estilos o agregar código para analíticas web.',\n    'app_custom_html_disabled_notice' => 'El contenido personalizado para la cabecera está deshabilitado en esta página de ajustes para permitir que cualquier cambio que rompa la funcionalidad pueda ser revertido.',\n    'app_logo' => 'Logo de la aplicación',\n    'app_logo_desc' => 'Se utiliza en la cabecera de la aplicación, entre otras áreas. Esta imagen debe ser de 86px de altura. Las imágenes grandes serán reducidas en tamaño.',\n    'app_icon' => 'Icono de la aplicación',\n    'app_icon_desc' => 'Se utiliza para las pestañas del navegador y los accesos directos. Debería ser una imagen PNG cuadrada de 256px.',\n    'app_homepage' => 'Página de inicio',\n    'app_homepage_desc' => 'Elija la vista que se mostrará en la página de inicio en lugar de la vista predeterminada. Se ignorarán los permisos de la página seleccionada.',\n    'app_homepage_select' => 'Elija una página',\n    'app_footer_links' => 'Enlaces de pie de página',\n    'app_footer_links_desc' => 'Añade enlaces para mostrar dentro del pie de página del sitio. Estos se mostrarán en la parte inferior de la mayoría de las páginas, incluyendo aquellas que no requieren estar registrado. Puede utilizar una etiqueta de \"trans::<key>\" para utilizar traducciones definidas por el sistema. Por ejemplo: el uso de \"trans::common.privacy_policy\" proporcionará el texto traducido \"Política de privacidad\" y \"trans::common.terms_of_service\" proporcionará el texto traducido \"Términos de servicio\".',\n    'app_footer_links_label' => 'Etiqueta del enlace',\n    'app_footer_links_url' => 'Dirección URL del enlace',\n    'app_footer_links_add' => 'Añadir enlace al pie de página',\n    'app_disable_comments' => 'Deshabilitar comentarios',\n    'app_disable_comments_toggle' => 'Deshabilitar comentarios',\n    'app_disable_comments_desc' => 'Deshabilita los comentarios en todas las páginas de la aplicación. <br> Los comentarios existentes no se muestran.',\n\n    // Color settings\n    'color_scheme' => 'Esquema de color de la aplicación',\n    'color_scheme_desc' => 'Establece los colores a usar en la interfaz de BookStack. Los colores pueden configurarse por separado para que los modos oscuros y claros se ajusten mejor al tema y garanticen la legibilidad.',\n    'ui_colors_desc' => 'Establece el color principal y el color de los enlaces para BookStack. El color principal se utiliza principalmente para la cabecera, botones y decoraciones de la interfaz. El color de los enlaces se utiliza para enlaces y acciones de texto, tanto dentro del contenido escrito como en la interfaz de Bookstack.',\n    'app_color' => 'Color principal',\n    'link_color' => 'Color de enlaces por defecto',\n    'content_colors_desc' => 'Establece los colores para todos los elementos en la jerarquía de la organización de la página. Se recomienda elegir colores con un brillo similar al predeterminado para mayor legibilidad.',\n    'bookshelf_color' => 'Color del estante',\n    'book_color' => 'Color del libro',\n    'chapter_color' => 'Color del capítulo',\n    'page_color' => 'Color de la página',\n    'page_draft_color' => 'Color del borrador de página',\n\n    // Registration Settings\n    'reg_settings' => 'Registro',\n    'reg_enable' => 'Habilitar registro',\n    'reg_enable_toggle' => 'Habilitar registro',\n    'reg_enable_desc' => 'Cuando se habilita el registro los usuarios podrán registrarse como usuarios de la aplicación. Al registrarse se les asigna un rol único por defecto.',\n    'reg_default_role' => 'Rol de usuario por defecto después del registro',\n    'reg_enable_external_warning' => 'La opción anterior no se utiliza mientras la autenticación LDAP o SAML externa esté activa. Las cuentas de usuario para los miembros no existentes se crearán automáticamente si la autenticación en el sistema externo en uso es exitosa.',\n    'reg_email_confirmation' => 'Confirmación por correo electrónico',\n    'reg_email_confirmation_toggle' => 'Requerir confirmación por correo electrónico',\n    'reg_confirm_email_desc' => 'Si se emplea la restricción por dominio, entonces se requerirá la confirmación por correo electrónico y esta opción será ignorada.',\n    'reg_confirm_restrict_domain' => 'Restricción por dominio',\n    'reg_confirm_restrict_domain_desc' => 'Introduzca una lista separada por comas de los dominios para cuentas de correo a los que se les permitirá el registro de usuarios. A los usuarios les será enviado un correo electrónico para confirmar la dirección antes de que se le permita interactuar con la aplicación. <br> Tenga en cuenta que los usuarios podrán cambiar sus direcciones de correo electrónico después de registrarse exitosamente.',\n    'reg_confirm_restrict_domain_placeholder' => 'Ninguna restricción establecida',\n\n    // Sorting Settings\n    'sorting' => 'Listas y ordenación',\n    'sorting_book_default' => 'Orden de libros por defecto',\n    'sorting_book_default_desc' => 'Seleccione la regla de ordenación predeterminada para aplicar a nuevos libros. Esto no afectará a los libros existentes, y puede ser anulado por libro.',\n    'sorting_rules' => 'Reglas de ordenación',\n    'sorting_rules_desc' => 'Son operaciones de ordenación predefinidas que se pueden aplicar al contenido en el sistema.',\n    'sort_rule_assigned_to_x_books' => 'Asignado a :count libro | Asignado a :count libros',\n    'sort_rule_create' => 'Crear regla de ordenación',\n    'sort_rule_edit' => 'Editar regla de ordenación',\n    'sort_rule_delete' => 'Eliminar regla de ordenación',\n    'sort_rule_delete_desc' => 'Eliminar esta regla de ordenación del sistema. Los Libros que utilicen este tipo se revertirán a la ordenación manual.',\n    'sort_rule_delete_warn_books' => 'Esta regla de ordenación se utiliza actualmente en :count libro(s). ¿Está seguro que desea eliminarla?',\n    'sort_rule_delete_warn_default' => 'Esta regla de ordenación se utiliza actualmente como predeterminado para los libros. ¿Está seguro de que desea eliminarla?',\n    'sort_rule_details' => 'Detalles de la regla de ordenación',\n    'sort_rule_details_desc' => 'Establezca un nombre para esta regla de ordenación, que aparecerá en las listas cuando los usuarios estén seleccionando un orden.',\n    'sort_rule_operations' => 'Operaciones de ordenación',\n    'sort_rule_operations_desc' => 'Configure las acciones de ordenación a realizar moviéndolas de la lista de operaciones disponibles. Al usarse, las operaciones se aplicarán en orden, de arriba a abajo. Cualquier cambio realizado aquí se aplicará a todos los libros asignados al guardar.',\n    'sort_rule_available_operations' => 'Operaciones disponibles',\n    'sort_rule_available_operations_empty' => 'No hay operaciones restantes',\n    'sort_rule_configured_operations' => 'Operaciones configuradas',\n    'sort_rule_configured_operations_empty' => 'Arrastrar/añadir operaciones desde la lista de \"Operaciones disponibles\"',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Nombre - Alfabético',\n    'sort_rule_op_name_numeric' => 'Nombre - Numérico',\n    'sort_rule_op_created_date' => 'Fecha de creación',\n    'sort_rule_op_updated_date' => 'Fecha de actualización',\n    'sort_rule_op_chapters_first' => 'Capítulos al inicio',\n    'sort_rule_op_chapters_last' => 'Capítulos al final',\n    'sorting_page_limits' => 'Límites de visualización por página',\n    'sorting_page_limits_desc' => 'Establecer cuántos elementos a mostrar por página en varias listas dentro del sistema. Normalmente una cantidad más baja rendirá mejor, mientras que una cantidad más alta evita la necesidad de hacer clic a través de varias páginas. Se recomienda utilizar un múltiplo de 6.',\n\n    // Maintenance settings\n    'maint' => 'Mantenimiento',\n    'maint_image_cleanup' => 'Limpiar imágenes',\n    'maint_image_cleanup_desc' => 'Analiza las páginas y sus revisiones para comprobar qué imágenes y dibujos están siendo utilizadas y cuales no son necesarias. Asegúrate de crear una copia completa de la base de datos y de las imágenes antes de lanzar esta opción.',\n    'maint_delete_images_only_in_revisions' => 'Elimina también imágenes que sólo existen en antiguas revisiones de páginas',\n    'maint_image_cleanup_run' => 'Lanzar limpieza',\n    'maint_image_cleanup_warning' => 'Se han encontrado :count imágenes posiblemente no utilizadas . ¿Estás seguro de querer borrar estas imágenes?',\n    'maint_image_cleanup_success' => '¡Se han encontrado y borrado :count imágenes posiblemente no utilizadas!',\n    'maint_image_cleanup_nothing_found' => '¡No se han encontrado imágenes sin utilizar, no se han borrado imágenes!',\n    'maint_send_test_email' => 'Enviar un correo electrónico de prueba',\n    'maint_send_test_email_desc' => 'Esto envía un correo electrónico de prueba a la dirección de correo electrónico especificada en tu perfil.',\n    'maint_send_test_email_run' => 'Enviar correo electrónico de prueba',\n    'maint_send_test_email_success' => 'Correo electrónico enviado a :address',\n    'maint_send_test_email_mail_subject' => 'Correo electrónico de prueba',\n    'maint_send_test_email_mail_greeting' => '¡El envío de correos electrónicos parece funcionar!',\n    'maint_send_test_email_mail_text' => '¡Enhorabuena! Al recibir esta notificación de correo electrónico, tu configuración de correo electrónico parece estar ajustada correctamente.',\n    'maint_recycle_bin_desc' => 'Los estantes, libros, capítulos y páginas eliminados se envían a la papelera de reciclaje para que puedan ser restauradas o eliminadas permanentemente. Los elementos más antiguos en la papelera de reciclaje pueden ser eliminados automáticamente después de un tiempo dependiendo de la configuración del sistema.',\n    'maint_recycle_bin_open' => 'Abrir papelera de reciclaje',\n    'maint_regen_references' => 'Regenerar referencias',\n    'maint_regen_references_desc' => 'Esta acción reconstruirá el índice de referencia de elementos cruzados dentro de la base de datos. Normalmente se gestiona automáticamente, pero esta acción puede ser útil para indexar el contenido antiguo o añadido mediante métodos no oficiales.',\n    'maint_regen_references_success' => '¡El índice de referencias ha sido regenerado!',\n    'maint_timeout_command_note' => 'Nota: Esta acción puede tardar en ejecutarse, lo que puede llevar a problemas de tiempo de espera en algunos entornos web. Como alternativa, esta acción se puede realizar desde una terminal.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Papelera de reciclaje',\n    'recycle_bin_desc' => 'Aquí puede restaurar elementos que hayan sido eliminados o elegir eliminarlos permanentemente del sistema. Esta lista no está filtrada a diferencia de las listas de actividad similares en el sistema donde se aplican los filtros de permisos.',\n    'recycle_bin_deleted_item' => 'Elemento eliminado',\n    'recycle_bin_deleted_parent' => 'Superior',\n    'recycle_bin_deleted_by' => 'Eliminado por',\n    'recycle_bin_deleted_at' => 'Fecha de eliminación',\n    'recycle_bin_permanently_delete' => 'Eliminar permanentemente',\n    'recycle_bin_restore' => 'Restaurar',\n    'recycle_bin_contents_empty' => 'La papelera de reciclaje está vacía',\n    'recycle_bin_empty' => 'Vaciar papelera de reciclaje',\n    'recycle_bin_empty_confirm' => 'Esto destruirá permanentemente todos los elementos de la papelera de reciclaje, incluyendo el contenido existente en cada elemento. ¿Está seguro de que desea vaciar la papelera de reciclaje?',\n    'recycle_bin_destroy_confirm' => 'Esta acción eliminará permanentemente este elemento del sistema, junto con los elementos secundarios listados a continuación, y no podrá restaurar este contenido de nuevo. ¿Está seguro de que desea eliminar permanentemente este elemento?',\n    'recycle_bin_destroy_list' => 'Elementos a eliminar',\n    'recycle_bin_restore_list' => 'Elementos a restaurar',\n    'recycle_bin_restore_confirm' => 'Esta acción restaurará el elemento eliminado, incluyendo cualquier elemento secundario, a su ubicación original. Si la ubicación original ha sido eliminada, y ahora está en la papelera de reciclaje, el elemento padre también tendrá que ser restaurado.',\n    'recycle_bin_restore_deleted_parent' => 'El padre de este elemento también ha sido eliminado. Estos permanecerán eliminados hasta que el padre también sea restaurado.',\n    'recycle_bin_restore_parent' => 'Restaurar superior',\n    'recycle_bin_destroy_notification' => 'Eliminados :count artículos de la papelera de reciclaje.',\n    'recycle_bin_restore_notification' => 'Restaurados :count artículos desde la papelera de reciclaje.',\n\n    // Audit Log\n    'audit' => 'Registro de auditoría',\n    'audit_desc' => 'Este registro de auditoría muestra una lista de actividades registradas en el sistema. Esta lista no está filtrada a diferencia de las listas de actividad similares en el sistema donde se aplican los filtros de permisos.',\n    'audit_event_filter' => 'Filtro de eventos',\n    'audit_event_filter_no_filter' => 'Sin filtro',\n    'audit_deleted_item' => 'Elemento eliminado',\n    'audit_deleted_item_name' => 'Nombre: :name',\n    'audit_table_user' => 'Usuario',\n    'audit_table_event' => 'Evento',\n    'audit_table_related' => 'Elemento o detalle relacionados',\n    'audit_table_ip' => 'Dirección IP',\n    'audit_table_date' => 'Fecha de la actividad',\n    'audit_date_from' => 'Rango de fecha desde',\n    'audit_date_to' => 'Rango de fecha hasta',\n\n    // Role Settings\n    'roles' => 'Roles',\n    'role_user_roles' => 'Roles de usuario',\n    'roles_index_desc' => 'Los roles se utilizan para agrupar usuarios y proporcionar permisos del sistema a sus miembros. Cuando un usuario es miembro de múltiples roles los privilegios otorgados se acumularán y el usuario heredará todas las habilidades.',\n    'roles_x_users_assigned' => ':count usuario asignado|:count usuarios asignados',\n    'roles_x_permissions_provided' => ':count permiso |:count permisos',\n    'roles_assigned_users' => 'Usuarios asignados',\n    'roles_permissions_provided' => 'Permisos proporcionados',\n    'role_create' => 'Crear nuevo rol',\n    'role_delete' => 'Borrar rol',\n    'role_delete_confirm' => 'Se borrará el rol con nombre  \\':roleName\\'.',\n    'role_delete_users_assigned' => 'Este rol tiene :userCount usuarios asignados. Si quisiera migrar los usuarios de este rol, seleccione un nuevo rol a continuación.',\n    'role_delete_no_migration' => \"No migrar usuarios\",\n    'role_delete_sure' => 'Está seguro que desea borrar este rol?',\n    'role_edit' => 'Editar rol',\n    'role_details' => 'Detalles de rol',\n    'role_name' => 'Nombre de rol',\n    'role_desc' => 'Descripción corta de rol',\n    'role_mfa_enforced' => 'Requiere autenticación en dos pasos',\n    'role_external_auth_id' => 'ID externo de autenticación',\n    'role_system' => 'Permisos de sistema',\n    'role_manage_users' => 'Gestionar usuarios',\n    'role_manage_roles' => 'Gestionar roles y permisos de roles',\n    'role_manage_entity_permissions' => 'Gestionar todos los permisos de libros, capítulos y páginas',\n    'role_manage_own_entity_permissions' => 'Gestionar permisos en libros, capítulos y páginas propias',\n    'role_manage_page_templates' => 'Administrar plantillas',\n    'role_access_api' => 'API de sistema de acceso',\n    'role_manage_settings' => 'Gestionar ajustes de la aplicación',\n    'role_export_content' => 'Exportar contenido',\n    'role_import_content' => 'Importar contenido',\n    'role_editor_change' => 'Cambiar editor de página',\n    'role_notifications' => 'Recibir y gestionar notificaciones',\n    'role_permission_note_users_and_roles' => 'Estos permisos proporcionarán también visibilidad y búsqueda de usuarios y roles en el sistema.',\n    'role_asset' => 'Permisos de contenido',\n    'roles_system_warning' => 'Tenga en cuenta que el acceso a cualquiera de los tres permisos anteriores puede permitir a un usuario alterar sus propios privilegios o los privilegios de otros en el sistema. Sólo asignar roles con estos permisos a usuarios de confianza.',\n    'role_asset_desc' => 'Estos permisos controlan el acceso por defecto a los contenidos del sistema. Los permisos de Libros, Capítulos y Páginas sobreescribiran estos permisos.',\n    'role_asset_admins' => 'A los administradores se les asigna automáticamente permisos para acceder a todo el contenido pero estas opciones podrían mostrar u ocultar opciones de la interfaz.',\n    'role_asset_image_view_note' => 'Esto se refiere a la visibilidad dentro del gestor de imágenes. El acceso a los archivos de imagen subidos dependerá de la opción de almacenamiento de imágenes del sistema.',\n    'role_asset_users_note' => 'Estos permisos proporcionarán también visibilidad y búsqueda de usuarios en el sistema.',\n    'role_all' => 'Todo',\n    'role_own' => 'Propio',\n    'role_controlled_by_asset' => 'Controlado por el contenido al que ha sido subido',\n    'role_save' => 'Guardar rol',\n    'role_users' => 'Usuarios en este rol',\n    'role_users_none' => 'No hay usuarios asignados a este rol',\n\n    // Users\n    'users' => 'Usuarios',\n    'users_index_desc' => 'Crear y administrar cuentas de usuario individuales dentro del sistema. Las cuentas de usuario se utilizan para el inicio de sesión y atribución de contenido y actividad. Los permisos de acceso se basan principalmente en roles, pero la propiedad del contenido del usuario, entre otros factores, también puede afectar a los permisos y el acceso.',\n    'user_profile' => 'Perfil de usuario',\n    'users_add_new' => 'Agregar nuevo usuario',\n    'users_search' => 'Buscar usuarios',\n    'users_latest_activity' => 'Actividad reciente',\n    'users_details' => 'Detalles de usuario',\n    'users_details_desc' => 'Ajusta un nombre público y email para este usuario. El email será empleado para acceder a la aplicación.',\n    'users_details_desc_no_email' => 'Ajusta un nombre público para este usuario para que pueda ser reconocido por otros.',\n    'users_role' => 'Roles de usuario',\n    'users_role_desc' => 'Selecciona los roles a los que será asignado este usuario. Si se asignan varios roles los permisos se acumularán y recibirá todas las habilidades de los roles asignados.',\n    'users_password' => 'Contraseña de usuario',\n    'users_password_desc' => 'Establezca una contraseña para iniciar sesión en la aplicación. Debe tener al menos 8 caracteres.',\n    'users_send_invite_text' => 'Puede enviar una invitación a este usuario por correo electrónico que le permitirá ajustar su propia contraseña, o puede usted ajustar su contraseña.',\n    'users_send_invite_option' => 'Enviar un correo electrónico de invitación',\n    'users_external_auth_id' => 'ID externo de autenticación',\n    'users_external_auth_id_desc' => 'Cuando un sistema de autenticación externa está en uso (como SAML2, OIDC o LDAP) este es el ID que vincula este usuario de BookStack a la cuenta del sistema de autenticación. Puede ignorar este campo si utiliza la autenticación por defecto basada en correo electrónico.',\n    'users_password_warning' => 'Solo debe rellenar este campo si desea cambiar la contraseña pora este usuario.',\n    'users_system_public' => 'Este usuario representa cualquier usuario invitado que visita la aplicación. No puede utilizarse para acceder pero es asignado automáticamente.',\n    'users_delete' => 'Borrar usuario',\n    'users_delete_named' => 'Borrar usuario :userName',\n    'users_delete_warning' => 'Se borrará completamente el usuario con el nombre \\':userName\\' del sistema.',\n    'users_delete_confirm' => '¿Está seguro que desea borrar este usuario?',\n    'users_migrate_ownership' => 'Cambiar propietario',\n    'users_migrate_ownership_desc' => 'Seleccione un usuario aquí si desea que otro usuario se convierta en el dueño de todos los elementos que actualmente son propiedad de este usuario.',\n    'users_none_selected' => 'Usuario no seleccionado',\n    'users_edit' => 'Editar usuario',\n    'users_edit_profile' => 'Editar perfil',\n    'users_avatar' => 'Avatar del usuario',\n    'users_avatar_desc' => 'Elige una imagen para representar a este usuario. Debe ser aproximadamente de 256px por lado.',\n    'users_preferred_language' => 'Idioma preferido',\n    'users_preferred_language_desc' => 'Esta opción cambiará el idioma de la interfaz de usuario en la aplicación. No afectará al contenido creado por los usuarios.',\n    'users_social_accounts' => 'Cuentas sociales',\n    'users_social_accounts_desc' => 'Ver el estado de las cuentas sociales conectadas para este usuario. Las cuentas sociales se pueden utilizar adicionalmente al sistema de autenticación primaria para el acceso al sistema.',\n    'users_social_accounts_info' => 'Aquí puede conectar sus otras cuentas para un acceso rápido y fácil a la aplicación. Desconectando una cuenta aquí no revoca accesos ya autorizados. Revoque el acceso desde los ajustes de perfil en la cuenta social conectada.',\n    'users_social_connect' => 'Conectar cuenta',\n    'users_social_disconnect' => 'Desconectar cuenta',\n    'users_social_status_connected' => 'Conectado',\n    'users_social_status_disconnected' => 'Desconectado',\n    'users_social_connected' => 'La cuenta :socialAccount ha sido añadida éxitosamente a su perfil.',\n    'users_social_disconnected' => 'La cuenta :socialAccount ha sido desconectada éxitosamente de su perfil.',\n    'users_api_tokens' => 'Tokens API',\n    'users_api_tokens_desc' => 'Crear y administrar los tokens de acceso utilizados para autenticar con la REST API de BookStack. Los permisos para el API se administran a través del usuario al que pertenece el token.',\n    'users_api_tokens_none' => 'No se han creado tokens API para este usuario',\n    'users_api_tokens_create' => 'Crear token',\n    'users_api_tokens_expires' => 'Expira',\n    'users_api_tokens_docs' => 'Documentación API',\n    'users_mfa' => 'Autenticación en dos pasos',\n    'users_mfa_desc' => 'La autenticación en dos pasos añade una capa de seguridad adicional a tu cuenta.',\n    'users_mfa_x_methods' => ':count método configurado|:count métodos configurados',\n    'users_mfa_configure' => 'Configurar métodos',\n\n    // API Tokens\n    'user_api_token_create' => 'Crear token API',\n    'user_api_token_name' => 'Nombre',\n    'user_api_token_name_desc' => 'Dale a tu token un nombre legible como un recordatorio futuro de su propósito.',\n    'user_api_token_expiry' => 'Fecha de expiración',\n    'user_api_token_expiry_desc' => 'Establece una fecha en la que este token expira. Después de esta fecha, las solicitudes realizadas usando este token ya no funcionarán. Dejar este campo en blanco fijará un vencimiento de 100 años en el futuro.',\n    'user_api_token_create_secret_message' => 'Inmediatamente después de crear este token se generarán y mostrarán sus correspondientes \"Token ID\" y \"Token Secret\". El \"Token Secret\" sólo se mostrará una vez, así que asegúrese de copiar el valor a un lugar seguro antes de proceder.',\n    'user_api_token' => 'Token API',\n    'user_api_token_id' => 'Token ID',\n    'user_api_token_id_desc' => 'Este es un identificador no editable generado por el sistema y único para este token que necesitará ser proporcionado en solicitudes de API.',\n    'user_api_token_secret' => 'Token secret',\n    'user_api_token_secret_desc' => 'Esta es una clave no editable generada por el sistema que necesitará ser proporcionada en solicitudes de API. Solo se monstraré esta vez así que guarde su valor en un lugar seguro.',\n    'user_api_token_created' => 'Token creado :timeAgo',\n    'user_api_token_updated' => 'Token actualizado :timeAgo',\n    'user_api_token_delete' => 'Borrar token',\n    'user_api_token_delete_warning' => 'Esto eliminará completamente este token API con el nombre \\':tokenName\\' del sistema.',\n    'user_api_token_delete_confirm' => '¿Está seguro de que desea borrar este API token?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Los Webhooks son una forma de enviar datos a URLs externas cuando ciertas acciones y eventos ocurren dentro del sistema, lo que permite la integración basada en eventos con plataformas externas como mensajería o sistemas de notificación.',\n    'webhooks_x_trigger_events' => ':count disparador de eventos|:count disparadores de eventos',\n    'webhooks_create' => 'Crear webhook',\n    'webhooks_none_created' => 'No hay webhooks creados.',\n    'webhooks_edit' => 'Editar webhook',\n    'webhooks_save' => 'Guardar webhook',\n    'webhooks_details' => 'Detalles del webhook',\n    'webhooks_details_desc' => 'Proporcione un nombre y un punto final POST como destino para los datos del webhook que se enviarán.',\n    'webhooks_events' => 'Eventos del webhook',\n    'webhooks_events_desc' => 'Seleccione todos los eventos que deberían activar este webhook.',\n    'webhooks_events_warning' => 'Tenga en cuenta que estos eventos se activarán para todos los eventos seleccionados, incluso si se aplican permisos personalizados. Asegúrese de que el uso de este webhook no exponga contenido confidencial.',\n    'webhooks_events_all' => 'Todos los eventos del sistema',\n    'webhooks_name' => 'Nombre del webhook',\n    'webhooks_timeout' => 'Tiempo de espera de webhook (segundos)',\n    'webhooks_endpoint' => 'Punto final del webhook',\n    'webhooks_active' => 'Webhook activo',\n    'webhook_events_table_header' => 'Eventos',\n    'webhooks_delete' => 'Eliminar webhook',\n    'webhooks_delete_warning' => 'Esto eliminará completamente este webhook, con el nombre \\':webhookName\\', del sistema.',\n    'webhooks_delete_confirm' => '¿Seguro que quieres eliminar este webhook?',\n    'webhooks_format_example' => 'Ejemplo de formato de webhook',\n    'webhooks_format_example_desc' => 'Los datos del Webhook se envían como una solicitud POST al punto final configurado como JSON siguiendo el formato mostrado a continuación. Las propiedades \"related_item\" y \"url\" son opcionales y dependerán del tipo de evento activado.',\n    'webhooks_status' => 'Estado del webhook',\n    'webhooks_last_called' => 'Última ejecución:',\n    'webhooks_last_errored' => 'Último error:',\n    'webhooks_last_error_message' => 'Último mensaje de error:',\n\n    // Licensing\n    'licenses' => 'Licencias',\n    'licenses_desc' => 'Esta página detalla información sobre la licencia de BookStack además de los proyectos y bibliotecas que se utilizan en BookStack. Muchos proyectos enumerados aquí pueden ser utilizados solo en un contexto de desarrollo.',\n    'licenses_bookstack' => 'Licencia BookStack',\n    'licenses_php' => 'Licencias de la biblioteca PHP',\n    'licenses_js' => 'Licencias de la biblioteca JavaScript',\n    'licenses_other' => 'Otras Licencias',\n    'license_details' => 'Datos de la licencia',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'Inglés',\n        'ar' => 'Árabe',\n        'bg' => 'Búlgaro',\n        'bs' => 'Bosnio',\n        'ca' => 'Català',\n        'cs' => 'Checo',\n        'cy' => 'Cymraeg',\n        'da' => 'Danés',\n        'de' => 'Alemán (informal)',\n        'de_informal' => 'Alemán (formal)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Francés',\n        'he' => 'עברית',\n        'hr' => 'Croata',\n        'hu' => 'Húngaro',\n        'id' => 'Indonesio',\n        'it' => 'Italiano',\n        'ja' => 'Japonés',\n        'ko' => 'Coreano',\n        'lt' => 'Lituano',\n        'lv' => 'Letón',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Holanda',\n        'pl' => 'Polaco',\n        'pt' => 'Portugués',\n        'pt_BR' => 'Portugués brasileño',\n        'ro' => 'Română',\n        'ru' => 'Ruso',\n        'sk' => 'Eslovaco',\n        'sl' => 'Esloveno',\n        'sv' => 'Sueco',\n        'tr' => 'Turco',\n        'uk' => 'Ucraniano',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Vietnamita',\n        'zh_CN' => 'Chino mandarín',\n        'zh_TW' => 'Chino tradicional',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/es/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => 'El :attribute debe ser aceptado.',\n    'active_url'           => 'El :attribute no es una URL válida.',\n    'after'                => 'El :attribute debe ser una fecha posterior :date.',\n    'alpha'                => 'El :attribute solo puede contener letras.',\n    'alpha_dash'           => 'El :attribute solo puede contener letras, números y guiones.',\n    'alpha_num'            => 'El :attribute solo puede contener letras y números.',\n    'array'                => 'El :attribute debe de ser un array.',\n    'backup_codes'         => 'El código suministrado no es válido o ya ha sido utilizado.',\n    'before'               => 'El :attribute debe ser una fecha anterior a  :date.',\n    'between'              => [\n        'numeric' => 'El :attribute debe estar entre :min y :max.',\n        'file'    => 'El :attribute debe estar entre :min y :max kilobytes.',\n        'string'  => 'El :attribute debe estar entre :min y :max caracteres.',\n        'array'   => 'El :attribute debe estar entre :min y :max items.',\n    ],\n    'boolean'              => 'El campo :attribute debe ser true o false.',\n    'confirmed'            => 'La confirmación de :attribute no concuerda.',\n    'date'                 => 'El :attribute no es una fecha válida.',\n    'date_format'          => 'El :attribute no coincide con el formato :format.',\n    'different'            => ':attribute y :other deben ser diferentes.',\n    'digits'               => ':attribute debe ser de :digits dígitos.',\n    'digits_between'       => ':attribute debe ser un valor entre :min y :max dígios.',\n    'email'                => ':attribute debe ser un correo electrónico válido.',\n    'ends_with' => 'El :attribute debe terminar con uno de los siguientes: :values',\n    'file'                 => 'El :attribute debe ser proporcionado como un archivo válido.',\n    'filled'               => 'El campo :attribute es requerido.',\n    'gt'                   => [\n        'numeric' => 'El :attribute debe ser mayor que :value.',\n        'file'    => 'El :attribute debe ser mayor que :value kilobytes.',\n        'string'  => 'El :attribute debe ser mayor que :value caracteres.',\n        'array'   => 'El :attribute debe tener más de :value elementos.',\n    ],\n    'gte'                  => [\n        'numeric' => 'El :attribute debe ser mayor o igual que :value.',\n        'file'    => 'El :attribute debe ser mayor o igual que :value kilobytes.',\n        'string'  => 'El :attribute debe ser mayor o igual que :value caracteres.',\n        'array'   => 'El :attribute debe tener :value o más elementos.',\n    ],\n    'exists'               => 'El :attribute seleccionado es inválido.',\n    'image'                => 'El :attribute debe ser una imagen.',\n    'image_extension'      => 'El :attribute debe tener una extensión de imagen válida y soportada.',\n    'in'                   => 'El selected :attribute es inválio.',\n    'integer'              => 'El :attribute debe ser un entero.',\n    'ip'                   => 'El :attribute debe ser una dirección IP válida.',\n    'ipv4'                 => 'El :attribute debe ser una dirección IPv4 válida.',\n    'ipv6'                 => 'El :attribute debe ser una dirección IPv6 válida.',\n    'json'                 => 'El :attribute debe ser una cadena JSON válida.',\n    'lt'                   => [\n        'numeric' => 'El :attribute debe ser menor que :value.',\n        'file'    => 'El :attribute debe ser menor que :value kilobytes.',\n        'string'  => 'El :attribute debe ser menor que :value caracteres.',\n        'array'   => 'El :attribute debe tener menos de :value elementos.',\n    ],\n    'lte'                  => [\n        'numeric' => 'El :attribute debe ser menor o igual que :value.',\n        'file'    => 'El :attribute debe ser menor o igual que :value kilobytes.',\n        'string'  => 'El :attribute debe ser menor o igual que :value caracteres.',\n        'array'   => 'El :attribute no debe tener más de :value elementos.',\n    ],\n    'max'                  => [\n        'numeric' => 'El :attribute no puede ser mayor que :max.',\n        'file'    => 'El :attribute no puede ser mayor que :max kilobytes.',\n        'string'  => 'El :attribute no puede ser mayor que :max carácteres.',\n        'array'   => 'El :attribute no puede contener más de :max items.',\n    ],\n    'mimes'                => 'El :attribute debe ser un fichero de tipo: :values.',\n    'min'                  => [\n        'numeric' => 'El :attribute debe ser al menos de :min.',\n        'file'    => 'El :attribute debe ser al menos :min kilobytes.',\n        'string'  => 'El :attribute debe ser al menos :min caracteres.',\n        'array'   => 'El :attribute debe tener como mínimo :min items.',\n    ],\n    'not_in'               => 'El :attribute seleccionado es inválio.',\n    'not_regex'            => 'El formato de :attribute es inválido.',\n    'numeric'              => 'El :attribute debe ser numérico.',\n    'regex'                => 'El formato de :attribute es inválido',\n    'required'             => 'El :attribute es requerido.',\n    'required_if'          => 'El :attribute es requerido cuando :other vale :value.',\n    'required_with'        => 'El campo :attribute es requerido cuando se encuentre entre los valores :values.',\n    'required_with_all'    => 'El campo :attribute es requerido cuando los valores sean :values.',\n    'required_without'     => 'El :attribute es requerido cuando no se encuentre entre los valores :values.',\n    'required_without_all' => 'El :attribute es requerido cuando ninguno de los valores :values están presentes.',\n    'same'                 => 'El :attribute y :other deben coincidir.',\n    'safe_url'             => 'El enlace proporcionado puede no ser seguro.',\n    'size'                 => [\n        'numeric' => ':attribute debe ser :size.',\n        'file'    => ':attribute debe ser :size kilobytes.',\n        'string'  => ':attribute debe ser :size caracteres.',\n        'array'   => ':attribute debe contener :size items.',\n    ],\n    'string'               => 'El atributo :attribute debe ser una cadena de texto.',\n    'timezone'             => 'El atributo :attribute debe ser una zona válida.',\n    'totp'                 => 'El código suministrado no es válido o ya ha expirado.',\n    'unique'               => 'El atributo :attribute ya ha sido tomado.',\n    'url'                  => 'El atributo :attribute tiene un formato inválido.',\n    'uploaded'             => 'El archivo no ha podido subirse. Es posible que el servidor no acepte archivos de este tamaño.',\n\n    'zip_file' => 'El :attribute necesita hacer referencia a un archivo dentro del ZIP.',\n    'zip_file_size' => 'El archivo :attribute no debe exceder :size MB.',\n    'zip_file_mime' => 'El :attribute necesita hacer referencia a un archivo de tipo :validTypes, encontrado :foundType.',\n    'zip_model_expected' => 'Se esperaba un objeto de datos, pero se encontró \":type\".',\n    'zip_unique' => 'El :attribute debe ser único para el tipo de objeto dentro del ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Requerida confirmación de contraseña',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/es_AR/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'creó la página',\n    'page_create_notification'    => 'Página creada correctamente',\n    'page_update'                 => 'página actualizada',\n    'page_update_notification'    => 'Página actualizada correctamente',\n    'page_delete'                 => 'borró la página',\n    'page_delete_notification'    => 'Página eliminada correctamente',\n    'page_restore'                => 'página restaurada',\n    'page_restore_notification'   => 'Página restaurada correctamente',\n    'page_move'                   => 'página movida',\n    'page_move_notification'      => 'Página movida correctamente',\n\n    // Chapters\n    'chapter_create'              => 'capítulo creado',\n    'chapter_create_notification' => 'Capítulo creado correctamente',\n    'chapter_update'              => 'capítulo actualizado',\n    'chapter_update_notification' => 'Capítulo actualizado correctamente',\n    'chapter_delete'              => 'capítulo borrado',\n    'chapter_delete_notification' => 'Capítulo eliminado correctamente',\n    'chapter_move'                => 'capítulo movido',\n    'chapter_move_notification' => 'Capítulo movido correctamente',\n\n    // Books\n    'book_create'                 => 'libro creado',\n    'book_create_notification'    => 'Libro creado correctamente',\n    'book_create_from_chapter'              => 'capítulo convertido en libro',\n    'book_create_from_chapter_notification' => 'Capítulo convertido en libro con éxito',\n    'book_update'                 => 'libro actualizado',\n    'book_update_notification'    => 'Libro actualizado correctamente',\n    'book_delete'                 => 'libro borrado',\n    'book_delete_notification'    => 'Libro eliminado correctamente',\n    'book_sort'                   => 'libro ordenado',\n    'book_sort_notification'      => 'Libro reordenado correctamente',\n\n    // Bookshelves\n    'bookshelf_create'            => 'estante creado',\n    'bookshelf_create_notification'    => 'Estante creado correctamente',\n    'bookshelf_create_from_book'    => 'libro convertido en estante',\n    'bookshelf_create_from_book_notification'    => 'Libro convertido en estante con éxito',\n    'bookshelf_update'                 => 'estante actualizado',\n    'bookshelf_update_notification'    => 'Estante actualizado correctamente',\n    'bookshelf_delete'                 => 'estante eliminado',\n    'bookshelf_delete_notification'    => 'Estante eliminado correctamente',\n\n    // Revisions\n    'revision_restore' => 'revisión restaurada',\n    'revision_delete' => 'revisión eliminada',\n    'revision_delete_notification' => 'Revisión eliminada correctamente',\n\n    // Favourites\n    'favourite_add_notification' => '\".name\" se añadió a sus favoritos',\n    'favourite_remove_notification' => '\".name\" se eliminó de sus favoritos',\n\n    // Watching\n    'watch_update_level_notification' => 'Preferencias de visualización actualizadas con éxito',\n\n    // Auth\n    'auth_login' => 'sesión iniciada',\n    'auth_register' => 'registrado como usuario nuevo',\n    'auth_password_reset_request' => 'cambio de contraseña de usuario solicitado',\n    'auth_password_reset_update' => 'restablecer contraseña de usuario',\n    'mfa_setup_method' => 'método MFA configurado',\n    'mfa_setup_method_notification' => 'Método de autenticación de múltiples factores configurado satisfactoriamente',\n    'mfa_remove_method' => 'método MFA eliminado',\n    'mfa_remove_method_notification' => 'Método de autenticación de múltiples factores eliminado satisfactoriamente',\n\n    // Settings\n    'settings_update' => 'ajustes actualizados',\n    'settings_update_notification' => 'Configuraciones actualizadas correctamente',\n    'maintenance_action_run' => 'ejecutar acción de mantenimiento',\n\n    // Webhooks\n    'webhook_create' => 'webhook creado',\n    'webhook_create_notification' => 'Webhook creado correctamente',\n    'webhook_update' => 'webhook actualizado',\n    'webhook_update_notification' => 'Webhook actualizado correctamente',\n    'webhook_delete' => 'webhook eliminado',\n    'webhook_delete_notification' => 'Webhook eliminado correctamente',\n\n    // Imports\n    'import_create' => 'importación creada',\n    'import_create_notification' => 'Importación cargada correctamente',\n    'import_run' => 'importación actualizada',\n    'import_run_notification' => 'Contenido importado correctamente',\n    'import_delete' => 'importación borrada',\n    'import_delete_notification' => 'Importación borrada correctamente',\n\n    // Users\n    'user_create' => 'usuario creado',\n    'user_create_notification' => 'Usuario creado correctamente',\n    'user_update' => 'usuario actualizado',\n    'user_update_notification' => 'Usuario actualizado con éxito',\n    'user_delete' => 'usuario eliminado',\n    'user_delete_notification' => 'El usuario fue eliminado correctamente',\n\n    // API Tokens\n    'api_token_create' => 'token de API creado',\n    'api_token_create_notification' => 'Token de API creado correctamente',\n    'api_token_update' => 'token de API actualizado',\n    'api_token_update_notification' => 'Token de API actualizado correctamente',\n    'api_token_delete' => 'API token eliminado',\n    'api_token_delete_notification' => 'Token de API eliminado correctamente',\n\n    // Roles\n    'role_create' => 'rol creado',\n    'role_create_notification' => 'Rol creado correctamente',\n    'role_update' => 'rol actualizado',\n    'role_update_notification' => 'Rol actualizado correctamente',\n    'role_delete' => 'rol eliminado',\n    'role_delete_notification' => 'Rol eliminado correctamente',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'papelera de reciclaje vaciada',\n    'recycle_bin_restore' => 'restaurado desde la papelera de reciclaje',\n    'recycle_bin_destroy' => 'eliminado de la papelera de reciclaje',\n\n    // Comments\n    'commented_on'                => 'comentado',\n    'comment_create'              => 'comentario agregado',\n    'comment_update'              => 'comentario actualizado',\n    'comment_delete'              => 'comentario eliminado',\n\n    // Sort Rules\n    'sort_rule_create' => 'regla de ordenación creada',\n    'sort_rule_create_notification' => 'Regla de ordenación creada correctamente',\n    'sort_rule_update' => 'regla de ordenación actualizada',\n    'sort_rule_update_notification' => 'Regla de ordenación actualizada correctamente',\n    'sort_rule_delete' => 'regla de ordenación eliminada',\n    'sort_rule_delete_notification' => 'Regla de ordenación eliminada correctamente',\n\n    // Other\n    'permissions_update'          => 'permisos actualizados',\n];\n"
  },
  {
    "path": "lang/es_AR/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Estas credenciales no concuerdan con nuestros registros.',\n    'throttle' => 'Demasiados intentos fallidos de inicio de sesión. Por favor intente nuevamente en :seconds segundos.',\n\n    // Login & Register\n    'sign_up' => 'Registrarse',\n    'log_in' => 'Iniciar sesión',\n    'log_in_with' => 'Acceder con :socialDriver',\n    'sign_up_with' => 'Registrarse con :socialDriver',\n    'logout' => 'Salir',\n\n    'name' => 'Nombre',\n    'username' => 'Nombre de usuario',\n    'email' => 'Correo electrónico',\n    'password' => 'Contraseña',\n    'password_confirm' => 'Confirmar contraseña',\n    'password_hint' => 'Debe contener al menos 8 caracteres',\n    'forgot_password' => '¿Olvidó la contraseña?',\n    'remember_me' => 'Recordarme',\n    'ldap_email_hint' => 'Por favor introduzca un correo electrónico para utilizar con esta cuenta.',\n    'create_account' => 'Crear una cuenta',\n    'already_have_account' => '¿Ya tiene una cuenta?',\n    'dont_have_account' => '¿No tiene una cuenta?',\n    'social_login' => 'Acceso con cuenta Social',\n    'social_registration' => 'Registro con cuenta Social',\n    'social_registration_text' => 'Registrar y entrar utilizando otro servicio.',\n\n    'register_thanks' => '¡Gracias por registrarse!',\n    'register_confirm' => 'Por favor verifique su correo electrónico y presione en el botón de confirmación enviado para acceder a :appName.',\n    'registrations_disabled' => 'Los registros están deshabilitados actualmente',\n    'registration_email_domain_invalid' => 'Este dominio de correo electrónico no tiene acceso a esta aplicación',\n    'register_success' => '¡Gracias por registrarse! Ahora se encuentra registrado y ha accedido a la aplicación.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Intentando iniciar sesión',\n    'auto_init_starting_desc' => 'Estamos contactando con su sistema de autenticación para comenzar el proceso de inicio de sesión. Si no hay progreso después de 5 segundos puede intentar hacer clic en el enlace de abajo.',\n    'auto_init_start_link' => 'Continuar con la autenticación',\n\n    // Password Reset\n    'reset_password' => 'Restablecer la contraseña',\n    'reset_password_send_instructions' => 'Introduzca su correo electrónico a continuación y se le enviará un correo electrónico con un enlace para la restauración',\n    'reset_password_send_button' => 'Enviar enlace de restauración',\n    'reset_password_sent' => 'Si la dirección de correo electrónico :email existe en el sistema, se enviará un enlace para restablecer la contraseña.',\n    'reset_password_success' => 'Su contraseña se restableció con éxito.',\n    'email_reset_subject' => 'Restauración de la contraseña de para la aplicación :appName',\n    'email_reset_text' => 'Ud. esta recibiendo este correo electrónico debido a que recibimos una solicitud de restauración de la contraseña de su cuenta.',\n    'email_reset_not_requested' => 'Si ud. no solicitó un cambio de contraseña, no se requiere ninguna acción.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Confirme su correo electrónico en :appName',\n    'email_confirm_greeting' => '¡Gracias por unirse a :appName!',\n    'email_confirm_text' => 'Por favor confirme su dirección de correo electrónico presionando en el siguiente botón:',\n    'email_confirm_action' => 'Confirmar correo electrónico',\n    'email_confirm_send_error' => 'Se pidió confirmación de correo electrónico pero el sistema no pudo enviar el correo electrónico. Contacte al administrador para asegurarse que el correo electrónico está configurado correctamente.',\n    'email_confirm_success' => '¡Su correo electrónico ha sido confirmado! Ahora debería poder iniciar sesión usando esta dirección de correo electrónico.',\n    'email_confirm_resent' => 'Correo electrónico de confirmación reenviado, Por favor verifique su bandeja de entrada.',\n    'email_confirm_thanks' => '¡Gracias por confirmar!',\n    'email_confirm_thanks_desc' => 'Por favor, espere un momento mientras se gestiona su confirmación. Si no se lo redirige después de 3 segundos, pulse el enlace \"Continuar\" para seguir.',\n\n    'email_not_confirmed' => 'Dirección de correo electrónico no confirmada',\n    'email_not_confirmed_text' => 'Su cuenta de correo electrónico todavía no ha sido confirmada.',\n    'email_not_confirmed_click_link' => 'Por favor verifique el correo electrónico con el enlace de confirmación que fue enviado luego de registrarse.',\n    'email_not_confirmed_resend' => 'Si no puede encontrar el correo electrónico, puede solicitar el renvío del correo electrónico de confirmación rellenando el formulario a continuación.',\n    'email_not_confirmed_resend_button' => 'Reenviar correo electrónico de confirmación',\n\n    // User Invite\n    'user_invite_email_subject' => 'Lo invitaron a unirse a :appName!',\n    'user_invite_email_greeting' => 'Se creó una cuenta para usted en :appName.',\n    'user_invite_email_text' => 'Presione el botón de abajo para establecer una contraseña y tener acceso access:',\n    'user_invite_email_action' => 'Establecer la contraseña de la cuenta',\n    'user_invite_page_welcome' => 'Bienvenido a :appName!',\n    'user_invite_page_text' => 'Para finalizar la cuenta y tener acceso debe establcer una contraseña que utilizará para ingresar a :appName en visitas futuras.',\n    'user_invite_page_confirm_button' => 'Confirmar Contraseña',\n    'user_invite_success_login' => 'Contraseña guardada, ¡ahora debería ser capaz de iniciar sesión usando su contraseña establecida para acceder a :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Configurar autenticación de múltiples factores',\n    'mfa_setup_desc' => 'Configure la autenticación de múltiples factores como una capa extra de seguridad para su cuenta de usuario.',\n    'mfa_setup_configured' => 'Ya está configurado',\n    'mfa_setup_reconfigure' => 'Reconfigurar',\n    'mfa_setup_remove_confirmation' => '¿Está seguro que desea eliminar este método de autenticación de múltiples factores?',\n    'mfa_setup_action' => 'Configuración',\n    'mfa_backup_codes_usage_limit_warning' => 'Quedan menos de 5 códigos de respaldo, Por favor, genere y guarde un nuevo conjunto antes de que se quede sin códigos para evitar que se bloquee su cuenta.',\n    'mfa_option_totp_title' => 'Aplicación móvil',\n    'mfa_option_totp_desc' => 'Para utilizar la autenticación en dos pasos necesitará una aplicación móvil que soporte TOTP como Google Authenticator, Authy o Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Códigos de Respaldo',\n    'mfa_option_backup_codes_desc' => 'Genera un conjunto de códigos de respaldo de un único uso que ingresará al iniciar sesión para verificar su identidad. Asegúrese de guardarlos en un lugar seguro.',\n    'mfa_gen_confirm_and_enable' => 'Confirmar y Activar',\n    'mfa_gen_backup_codes_title' => 'Configuración de Códigos de Respaldo',\n    'mfa_gen_backup_codes_desc' => 'Guarde la siguiente lista de códigos en un lugar seguro. Al acceder al sistema podrá usar uno de los códigos como un segundo mecanismo de autenticación.',\n    'mfa_gen_backup_codes_download' => 'Descargar Códigos',\n    'mfa_gen_backup_codes_usage_warning' => 'Cada código puede utilizarse sólo una vez',\n    'mfa_gen_totp_title' => 'Configuración de Aplicación móvil',\n    'mfa_gen_totp_desc' => 'Para utilizar la autenticación en dos pasos necesitará una aplicación móvil que soporte TOTP como Google Authenticator, Authy o Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Escanea el código QR mostrado a continuación usando tu aplicación de autenticación preferida para empezar.',\n    'mfa_gen_totp_verify_setup' => 'Verificar Configuración',\n    'mfa_gen_totp_verify_setup_desc' => 'Verifica que todo está funcionando introduciendo un código, generado en tu aplicación de autenticación, en el campo de texto a continuación:',\n    'mfa_gen_totp_provide_code_here' => 'Introduce aquí tu código generado por la aplicación',\n    'mfa_verify_access' => 'Verificar Acceso',\n    'mfa_verify_access_desc' => 'Su cuenta de usuario requiere que confirme su identidad a través de un nivel adicional de verificación antes de que se le conceda el acceso. Verifique su identidad usando uno de los métodos configurados para continuar.',\n    'mfa_verify_no_methods' => 'No hay Métodos Configurados',\n    'mfa_verify_no_methods_desc' => 'No se han encontrado métodos de autenticación de múltiples factores para su cuenta. Tendrá que configurar al menos un método antes de obtener acceso.',\n    'mfa_verify_use_totp' => 'Verificar usando una aplicación móvil',\n    'mfa_verify_use_backup_codes' => 'Verificar usando un código de respaldo',\n    'mfa_verify_backup_code' => 'Código de Respaldo',\n    'mfa_verify_backup_code_desc' => 'Introduzca uno de sus códigos de respaldo restantes a continuación:',\n    'mfa_verify_backup_code_enter_here' => 'Introduzca el código de respaldo aquí',\n    'mfa_verify_totp_desc' => 'A continuación, introduzca el código generado con su aplicación móvil:',\n    'mfa_setup_login_notification' => 'Método de dos factores configurado. Por favor, inicie sesión nuevamente utilizando el método configurado.',\n];\n"
  },
  {
    "path": "lang/es_AR/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Cancelar',\n    'close' => 'Cerrar',\n    'confirm' => 'Confirmar',\n    'back' => 'Atrás',\n    'save' => 'Guardar',\n    'continue' => 'Continuar',\n    'select' => 'Seleccionar',\n    'toggle_all' => 'Mostrar/Ocultar Todo',\n    'more' => 'Más',\n\n    // Form Labels\n    'name' => 'Nombre',\n    'description' => 'Descripción',\n    'role' => 'Rol',\n    'cover_image' => 'Imagen de cubierta',\n    'cover_image_description' => 'Esta imagen debe ser de 440x250px aproximadamente, aunque será escalada y recortada para adaptarse a la interfaz de usuario en los diferentes escenarios según sea necesario, por lo que las dimensiones en pantalla serán diferentes.',\n\n    // Actions\n    'actions' => 'Acciones',\n    'view' => 'Ver',\n    'view_all' => 'Ver todo',\n    'new' => 'Nuevo',\n    'create' => 'Crear',\n    'update' => 'Actualizar',\n    'edit' => 'Editar',\n    'archive' => 'Archivar',\n    'unarchive' => 'Desarchivar',\n    'sort' => 'Ordenar',\n    'move' => 'Mover',\n    'copy' => 'Copiar',\n    'reply' => 'Responder',\n    'delete' => 'Borrar',\n    'delete_confirm' => 'Confirmar eliminación',\n    'search' => 'Buscar',\n    'search_clear' => 'Limpiar búsqueda',\n    'reset' => 'Restablecer',\n    'remove' => 'Remover',\n    'add' => 'Agregar',\n    'configure' => 'Configurar',\n    'manage' => 'Administra',\n    'fullscreen' => 'Pantalla completa',\n    'favourite' => 'Favoritos',\n    'unfavourite' => 'Eliminar de favoritos',\n    'next' => 'Siguiente',\n    'previous' => 'Anterior',\n    'filter_active' => 'Filtro activo:',\n    'filter_clear' => 'Limpiar filtro',\n    'download' => 'Descargar',\n    'open_in_tab' => 'Abrir en una pestaña',\n    'open' => 'Abrir',\n\n    // Sort Options\n    'sort_options' => 'Opciones de Orden',\n    'sort_direction_toggle' => 'Cambiar Dirección de Orden',\n    'sort_ascending' => 'Orden Ascendente',\n    'sort_descending' => 'Orden Descendente',\n    'sort_name' => 'Nombre',\n    'sort_default' => 'Por defecto',\n    'sort_created_at' => 'Fecha de creación',\n    'sort_updated_at' => 'Fecha de actualización',\n\n    // Misc\n    'deleted_user' => 'Usuario borrado',\n    'no_activity' => 'Ninguna actividad para mostrar',\n    'no_items' => 'No hay elementos disponibles',\n    'back_to_top' => 'Volver arriba',\n    'skip_to_main_content' => 'Ir al contenido principal',\n    'toggle_details' => 'Mostrar/Ocultar Detalles',\n    'toggle_thumbnails' => 'Mostrar/Ocultar Miniaturas',\n    'details' => 'Detalles',\n    'grid_view' => 'Vista de grilla',\n    'list_view' => 'Vista de lista',\n    'default' => 'Por defecto',\n    'breadcrumb' => 'Miga de Pan',\n    'status' => 'Estado',\n    'status_active' => 'Activo',\n    'status_inactive' => 'Inactivo',\n    'never' => 'Nunca',\n    'none' => 'Ninguno',\n\n    // Header\n    'homepage' => 'Página de Inicio',\n    'header_menu_expand' => 'Expandir el Menú de Cabecera',\n    'profile_menu' => 'Menu del Perfil',\n    'view_profile' => 'Ver Perfil',\n    'edit_profile' => 'Editar Perfil',\n    'dark_mode' => 'Modo Oscuro',\n    'light_mode' => 'Modo Claro',\n    'global_search' => 'Búsqueda Global',\n\n    // Layout tabs\n    'tab_info' => 'Información',\n    'tab_info_label' => 'Pestaña: Mostrar Información Secundaria',\n    'tab_content' => 'Contenido',\n    'tab_content_label' => 'Pestaña: Mostrar Contenido Primario',\n\n    // Email Content\n    'email_action_help' => 'Si está teniendo problemas haga click en el botón \":actionText\", copie y pegue la siguiente URL en su navegador web:',\n    'email_rights' => 'Todos los derechos reservados',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Política de privacidad',\n    'terms_of_service' => 'Términos de Servicio',\n\n    // OpenSearch\n    'opensearch_description' => 'Buscar :appName',\n];\n"
  },
  {
    "path": "lang/es_AR/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Seleccionar Imagen',\n    'image_list' => 'Lista de imágenes',\n    'image_details' => 'Detalles de la imagen',\n    'image_upload' => 'Subir imagen',\n    'image_intro' => 'Aquí puede seleccionar y administrar las imágenes que previamente se subieron al sistema.',\n    'image_intro_upload' => 'Suba una nueva imagen arrastrando un archivo de imagen a esta ventana, o usando el botón \"Subir imagen\" de arriba.',\n    'image_all' => 'Todo',\n    'image_all_title' => 'Ver todas las imágenes',\n    'image_book_title' => 'Ver las imágenes subidas a este libro',\n    'image_page_title' => 'Ver las imágenes subidas a esta página',\n    'image_search_hint' => 'Buscar por nombre de imagen',\n    'image_uploaded' => 'Subido el :uploadedDate',\n    'image_uploaded_by' => 'Subida por :userName',\n    'image_uploaded_to' => 'Subida a :pageLink',\n    'image_updated' => 'Actualizada :updateDate',\n    'image_load_more' => 'Cargar más',\n    'image_image_name' => 'Nombre de imagen',\n    'image_delete_used' => 'Esta imagen esta siendo utilizada en las páginas a continuación.',\n    'image_delete_confirm_text' => '¿Está seguro que quiere eliminar esta imagen?',\n    'image_select_image' => 'Seleccionar Imagen',\n    'image_dropzone' => 'Arrastre las imágenes o hacer click aquí para Subir',\n    'image_dropzone_drop' => 'Arrastre las imágenes aquí para subirlas',\n    'images_deleted' => 'Imágenes borradas',\n    'image_preview' => 'Preview de la imagen',\n    'image_upload_success' => 'Imagen subida éxitosamente',\n    'image_update_success' => 'Detalles de la imagen actualizados exitosamente',\n    'image_delete_success' => 'Imagen borrada exitosamente',\n    'image_replace' => 'Reemplazar imagen',\n    'image_replace_success' => 'Imagen actualizada correctamente',\n    'image_rebuild_thumbs' => 'Regenerar Variaciones de Tamaño',\n    'image_rebuild_thumbs_success' => '¡Imágenes de distinto tamaño regeneradas correctamente!',\n\n    // Code Editor\n    'code_editor' => 'Editar Código',\n    'code_language' => 'Lenguaje del Código',\n    'code_content' => 'Contenido del Código',\n    'code_session_history' => 'Historial de la sesión',\n    'code_save' => 'Guardar Código',\n];\n"
  },
  {
    "path": "lang/es_AR/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'General',\n    'advanced' => 'Avanzado',\n    'none' => 'Ninguno',\n    'cancel' => 'Cancelar',\n    'save' => 'Guardar',\n    'close' => 'Cerrar',\n    'apply' => 'Aplicar',\n    'undo' => 'Deshacer',\n    'redo' => 'Rehacer',\n    'left' => 'Izquierda',\n    'center' => 'Centrar',\n    'right' => 'Derecha',\n    'top' => 'Arriba',\n    'middle' => 'Medio',\n    'bottom' => 'Abajo',\n    'width' => 'Ancho',\n    'height' => 'Altura',\n    'More' => 'Más',\n    'select' => 'Seleccionar...',\n\n    // Toolbar\n    'formats' => 'Formatos',\n    'header_large' => 'Encabezado grande',\n    'header_medium' => 'Encabezado mediano',\n    'header_small' => 'Encabezado chico',\n    'header_tiny' => 'Encabezado pequeño',\n    'paragraph' => 'Párrafo',\n    'blockquote' => 'Cita',\n    'inline_code' => 'Código en línea',\n    'callouts' => 'Llamadas',\n    'callout_information' => 'Información',\n    'callout_success' => 'Éxito',\n    'callout_warning' => 'Advertencia',\n    'callout_danger' => 'Peligro',\n    'bold' => 'Negrita',\n    'italic' => 'Cursiva',\n    'underline' => 'Subrayado',\n    'strikethrough' => 'Tachado',\n    'superscript' => 'Superíndice',\n    'subscript' => 'Subíndice',\n    'text_color' => 'Color del texto',\n    'highlight_color' => 'Color de resaltado',\n    'custom_color' => 'Color personalizado',\n    'remove_color' => 'Eliminar color',\n    'background_color' => 'Color de fondo',\n    'align_left' => 'Alinear a la izquierda',\n    'align_center' => 'Centrar',\n    'align_right' => 'Alinear a la derecha',\n    'align_justify' => 'Justificado',\n    'list_bullet' => 'Lista de viñetas',\n    'list_numbered' => 'Lista numerada',\n    'list_task' => 'Lista de tareas',\n    'indent_increase' => 'Aumentar sangría',\n    'indent_decrease' => 'Reducir sangría',\n    'table' => 'Tabla',\n    'insert_image' => 'Insertar Imagen',\n    'insert_image_title' => 'Insertar/Editar imagen',\n    'insert_link' => 'Insertar/editar enlace',\n    'insert_link_title' => 'Insertar/Editar Enlace',\n    'insert_horizontal_line' => 'Insertar línea horizontal',\n    'insert_code_block' => 'Insertar bloque de código',\n    'edit_code_block' => 'Editar bloque de código',\n    'insert_drawing' => 'Insertar/editar dibujo',\n    'drawing_manager' => 'Gestor de dibujo',\n    'insert_media' => 'Insertar/editar media',\n    'insert_media_title' => 'Insertar/Editar Media',\n    'clear_formatting' => 'Limpiar formato',\n    'source_code' => 'Código fuente',\n    'source_code_title' => 'Código Fuente',\n    'fullscreen' => 'Pantalla completa',\n    'image_options' => 'Opciones de imagen',\n\n    // Tables\n    'table_properties' => 'Propiedades de tabla',\n    'table_properties_title' => 'Propiedades de Tabla',\n    'delete_table' => 'Eliminar tabla',\n    'table_clear_formatting' => 'Limpiar el formato de tabla',\n    'resize_to_contents' => 'Redimensionar al contenido',\n    'row_header' => 'Cabecera de fila',\n    'insert_row_before' => 'Insertar fila arriba',\n    'insert_row_after' => 'Insertar fila abajo',\n    'delete_row' => 'Eliminar fila',\n    'insert_column_before' => 'Insertar columna a la izquierda',\n    'insert_column_after' => 'Insertar columna a la derecha',\n    'delete_column' => 'Eliminar columna',\n    'table_cell' => 'Celda',\n    'table_row' => 'Fila',\n    'table_column' => 'Columna',\n    'cell_properties' => 'Propiedades de la celda',\n    'cell_properties_title' => 'Propiedades de la Celda',\n    'cell_type' => 'Tipo de celda',\n    'cell_type_cell' => 'Celda',\n    'cell_scope' => 'Alcance',\n    'cell_type_header' => 'Celda de cabecera',\n    'merge_cells' => 'Combinar celdas',\n    'split_cell' => 'Dividir celda',\n    'table_row_group' => 'Grupo de filas',\n    'table_column_group' => 'Grupo de columnas',\n    'horizontal_align' => 'Alineación horizontal',\n    'vertical_align' => 'Alineación vertical',\n    'border_width' => 'Ancho del borde',\n    'border_style' => 'Estilo del borde',\n    'border_color' => 'Color del borde',\n    'row_properties' => 'Propiedades de fila',\n    'row_properties_title' => 'Propiedades de Fila',\n    'cut_row' => 'Cortar fila',\n    'copy_row' => 'Copiar fila',\n    'paste_row_before' => 'Pegar fila arriba',\n    'paste_row_after' => 'Pegar fila abajo',\n    'row_type' => 'Tipo de fila',\n    'row_type_header' => 'Encabezado',\n    'row_type_body' => 'Cuerpo',\n    'row_type_footer' => 'Pie',\n    'alignment' => 'Alineación',\n    'cut_column' => 'Cortar columna',\n    'copy_column' => 'Copiar columna',\n    'paste_column_before' => 'Pegar columna a la izquierda',\n    'paste_column_after' => 'Pegar columna a la derecha',\n    'cell_padding' => 'Relleno de la celda',\n    'cell_spacing' => 'Espaciado de celdas',\n    'caption' => 'Leyenda',\n    'show_caption' => 'Mostrar leyenda',\n    'constrain' => 'Restringir proporciones',\n    'cell_border_solid' => 'Sólida',\n    'cell_border_dotted' => 'Punteada',\n    'cell_border_dashed' => 'Discontinua',\n    'cell_border_double' => 'Doble',\n    'cell_border_groove' => 'Ranura',\n    'cell_border_ridge' => 'Reborde',\n    'cell_border_inset' => 'Recuadro',\n    'cell_border_outset' => 'Recuadro externo',\n    'cell_border_none' => 'Ninguno',\n    'cell_border_hidden' => 'Oculto',\n\n    // Images, links, details/summary & embed\n    'source' => 'Origen',\n    'alt_desc' => 'Descripción alternativa',\n    'embed' => 'Embeber',\n    'paste_embed' => 'Pegue su código embebido a continuación:',\n    'url' => 'URL',\n    'text_to_display' => 'Texto a mostrar',\n    'title' => 'Título',\n    'browse_links' => 'Explorar enlaces',\n    'open_link' => 'Abrir enlace',\n    'open_link_in' => 'Abrir enlace en...',\n    'open_link_current' => 'Ventana actual',\n    'open_link_new' => 'Ventana nueva',\n    'remove_link' => 'Eliminar enlace',\n    'insert_collapsible' => 'Insertar bloque desplegable',\n    'collapsible_unwrap' => 'Desplegar',\n    'edit_label' => 'Editar etiqueta',\n    'toggle_open_closed' => 'Alternar Abrir/Cerrar',\n    'collapsible_edit' => 'Editar bloque desplegable',\n    'toggle_label' => 'Cambiar etiqueta',\n\n    // About view\n    'about' => 'Acerca del editor',\n    'about_title' => 'Acerca del editor WYSIWYG',\n    'editor_license' => 'Licencia del editor y derechos de autor',\n    'editor_lexical_license' => 'Este editor está construido como una bifurcación de :lexicalLink que se distribuye bajo la licencia MIT.',\n    'editor_lexical_license_link' => 'Los detalles completos de la licencia se pueden encontrar aquí.',\n    'editor_tiny_license' => 'Este editor se construye usando :tinyLink provisto bajo la licencia MIT.',\n    'editor_tiny_license_link' => 'Aquí se muestran los detalles de los derechos de autor y la licencia de TinyMCE.',\n    'save_continue' => 'Guardar Página y Continuar',\n    'callouts_cycle' => '(Siga presionando para alternar entre tipos)',\n    'link_selector' => 'Enlace a contenido',\n    'shortcuts' => 'Atajos',\n    'shortcut' => 'Atajo',\n    'shortcuts_intro' => 'Los siguientes atajos están disponibles en el editor:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Descripción',\n];\n"
  },
  {
    "path": "lang/es_AR/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Creado recientemente',\n    'recently_created_pages' => 'Páginas creadas recientemente',\n    'recently_updated_pages' => 'Páginas actualizadas recientemente',\n    'recently_created_chapters' => 'Capítulos creados recientemente',\n    'recently_created_books' => 'Libros creados recientemente',\n    'recently_created_shelves' => 'Estantes creados recientemente',\n    'recently_update' => 'Actaulizado recientemente',\n    'recently_viewed' => 'Visto recientemente',\n    'recent_activity' => 'Actividad reciente',\n    'create_now' => 'Crear uno ahora',\n    'revisions' => 'Revisiones',\n    'meta_revision' => 'Revisión #:revisionCount',\n    'meta_created' => 'Creado el :timeLength',\n    'meta_created_name' => 'Creado el  :timeLength por :user',\n    'meta_updated' => 'Actualizado el :timeLength',\n    'meta_updated_name' => 'Actualizado el :timeLength por :user',\n    'meta_owned_name' => 'Propiedad de :user',\n    'meta_reference_count' => 'Referido en :count página | Referido en :count páginas',\n    'entity_select' => 'Seleccione entidad',\n    'entity_select_lack_permission' => 'No tiene los permisos necesarios para seleccionar este elemento',\n    'images' => 'Imágenes',\n    'my_recent_drafts' => 'Mis borradores recientes',\n    'my_recently_viewed' => 'Mis visualizaciones recientes',\n    'my_most_viewed_favourites' => 'Mis Favoritos Más Vistos',\n    'my_favourites' => 'Mis Favoritos',\n    'no_pages_viewed' => 'Ud. no ha visto ninguna página',\n    'no_pages_recently_created' => 'Ninguna página ha sido creada recientemente',\n    'no_pages_recently_updated' => 'Ninguna página ha sido actualizada recientemente',\n    'export' => 'Exportar',\n    'export_html' => 'Archivo web contenido',\n    'export_pdf' => 'Archivo PDF',\n    'export_text' => 'Archivo de texto plano',\n    'export_md' => 'Archivo Markdown',\n    'export_zip' => 'ZIP portable',\n    'default_template' => 'Plantilla de página predeterminada',\n    'default_template_explain' => 'Asigne una plantilla de página que se utilizará como contenido predeterminado para todas las páginas creadas en este elemento. Tenga en cuenta que esto sólo se utilizará si el creador de las páginas tiene acceso a la plantilla de página elegida.',\n    'default_template_select' => 'Seleccione una página de plantilla',\n    'import' => 'Importar',\n    'import_validate' => 'Validar importación',\n    'import_desc' => 'Importar libros, capítulos y páginas usando una exportación zip portable de la misma instancia u otra distinta. Seleccione un archivo ZIP para continuar. Después de que el archivo haya sido subido y validado, podrá configurar y confirmar la importación en la siguiente vista.',\n    'import_zip_select' => 'Seleccione archivo ZIP a subir',\n    'import_zip_validation_errors' => 'Se detectaron errores al validar el archivo ZIP proporcionado:',\n    'import_pending' => 'Importaciones pendientes',\n    'import_pending_none' => 'No se iniciaron importaciones.',\n    'import_continue' => 'Continuar la importación',\n    'import_continue_desc' => 'Revise el contenido que debe importarse del archivo ZIP subido. Cuando esté listo, ejecute la importación para añadir su contenido a este sistema. El archivo de importación ZIP subido se eliminará automáticamente al terminar la importación correctamente.',\n    'import_details' => 'Detalles de la Importación',\n    'import_run' => 'Ejecutar la importación',\n    'import_size' => ':size tamaño del archivo ZIP',\n    'import_uploaded_at' => 'Subido :relativeTime',\n    'import_uploaded_by' => 'Subido por',\n    'import_location' => 'Ubicación de Importación',\n    'import_location_desc' => 'Seleccione una ubicación de destino para el contenido importado. Necesitará los permisos pertinentes para crearlo dentro de la ubicación que elija.',\n    'import_delete_confirm' => '¿Está seguro de que desea eliminar esta importación?',\n    'import_delete_desc' => 'Esto eliminará el archivo ZIP de importación subido y no se puede deshacer.',\n    'import_errors' => 'Errores de Importación',\n    'import_errors_desc' => 'Se produjeron los siguientes errores durante el intento de importación:',\n    'breadcrumb_siblings_for_page' => 'Navegar por páginas del mismo nivel',\n    'breadcrumb_siblings_for_chapter' => 'Navegar por capítulos del mismo nivel',\n    'breadcrumb_siblings_for_book' => 'Navegar por libros del mismo nivel',\n    'breadcrumb_siblings_for_bookshelf' => 'Navegar por estantes del mismo nivel',\n\n    // Permissions and restrictions\n    'permissions' => 'Permisos',\n    'permissions_desc' => 'Establezca los permisos aquí para reemplazar los permisos predeterminados proporcionados por los roles de usuario.',\n    'permissions_book_cascade' => 'Los permisos establecidos en los libros se aplicarán a los capítulos contenidos y las páginas contenidas, a menos que tengan sus propios permisos definidos.',\n    'permissions_chapter_cascade' => 'Los permisos establecidos en los capítulos se aplicarán a las páginas contenidas, a menos que tengan sus propios permisos definidos.',\n    'permissions_save' => 'Guardar permisos',\n    'permissions_owner' => 'Propietario',\n    'permissions_role_everyone_else' => 'Todos los demás',\n    'permissions_role_everyone_else_desc' => 'Establecer permisos para todos los roles no específicamente reemplazados.',\n    'permissions_role_override' => 'Reemplazar permisos para el rol',\n    'permissions_inherit_defaults' => 'Heredar valores predeterminados',\n\n    // Search\n    'search_results' => 'Buscar resultados',\n    'search_total_results_found' => ':count resultados encontrados|:count total de resultados encontrados',\n    'search_clear' => 'Limpiar resultados',\n    'search_no_pages' => 'Ninguna página encontrada para la búsqueda',\n    'search_for_term' => 'Busqueda por :term',\n    'search_more' => 'Más resultados',\n    'search_advanced' => 'Búsqueda Avanzada',\n    'search_terms' => 'Términos de búsqueda',\n    'search_content_type' => 'Tipo de contenido',\n    'search_exact_matches' => 'Coincidencias exactas',\n    'search_tags' => 'Búsquedas de etiquetas',\n    'search_options' => 'Opciones',\n    'search_viewed_by_me' => 'Vistos por mí',\n    'search_not_viewed_by_me' => 'No vistos por mí',\n    'search_permissions_set' => 'Permisos establecidos',\n    'search_created_by_me' => 'Creado por mí',\n    'search_updated_by_me' => 'Actualizado por mí',\n    'search_owned_by_me' => 'De mi propiedad',\n    'search_date_options' => 'Opciones de fecha',\n    'search_updated_before' => 'Actualizado antes de',\n    'search_updated_after' => 'Actualizado después de',\n    'search_created_before' => 'Creado antes de',\n    'search_created_after' => 'Creado después de',\n    'search_set_date' => 'Esablecer fecha',\n    'search_update' => 'Actualizar búsqueda',\n\n    // Shelves\n    'shelf' => 'Estante',\n    'shelves' => 'Estantes',\n    'x_shelves' => ':count Estante|:count Estantes',\n    'shelves_empty' => 'No se crearon estantes',\n    'shelves_create' => 'Crear un estante nuevo',\n    'shelves_popular' => 'Estantes Populares',\n    'shelves_new' => 'Estantes Nuevos',\n    'shelves_new_action' => 'Estante Nuevo',\n    'shelves_popular_empty' => 'Los estantes más populares aparecerán aquí.',\n    'shelves_new_empty' => 'Los estantes mas nuevos aparecerán aquí.',\n    'shelves_save' => 'Guardar estantes',\n    'shelves_books' => 'Libros en este estante',\n    'shelves_add_books' => 'Agregar libros en este estante',\n    'shelves_drag_books' => 'Arrastre los libros aquí para añadirlos a este estante',\n    'shelves_empty_contents' => 'Este estante no tiene libros asignados a él',\n    'shelves_edit_and_assign' => 'Editar el estante para asignar libros',\n    'shelves_edit_named' => 'Editar Estante :name',\n    'shelves_edit' => 'Editar Estante',\n    'shelves_delete' => 'Eliminar Estante',\n    'shelves_delete_named' => 'Eliminar el Estante :name',\n    'shelves_delete_explain' => \"Esta acción eliminará el estante con el nombre ':name'. Los libros contenidos en él no se eliminarán.\",\n    'shelves_delete_confirmation' => '¿Está seguro que quiere eliminar este estante?',\n    'shelves_permissions' => 'Permisos del Estante',\n    'shelves_permissions_updated' => 'Permisos del Estante Actualizados',\n    'shelves_permissions_active' => 'Permisos Activos del Estante',\n    'shelves_permissions_cascade_warning' => 'Los permisos en los estantes no se aplican automáticamente a los libros que contengan. Esto se debe a que un libro puede existir en múltiples estantes. Sin embargo, los permisos pueden ser aplicados a los libros del estante utilizando la opción de abajo.',\n    'shelves_permissions_create' => 'Los permisos de creación de estantes sólo se utilizan para copiar los permisos a los libros contenidos utilizando la acción a continuación. No controlan la capacidad de crear libros.',\n    'shelves_copy_permissions_to_books' => 'Copiar Permisos a los Libros',\n    'shelves_copy_permissions' => 'Copiar Permisos',\n    'shelves_copy_permissions_explain' => 'Esta acción aplicará los permisos del estante a todos los libros dentro del mismo. Antes de activarlo, asegúrese de que todos los cambios de permisos para este estante fueron guardados.',\n    'shelves_copy_permission_success' => 'Permisos del estante copiados a :count libros',\n\n    // Books\n    'book' => 'Libro',\n    'books' => 'Libros',\n    'x_books' => ':count Libro|:count Libros',\n    'books_empty' => 'No hay libros creados',\n    'books_popular' => 'Libros populares',\n    'books_recent' => 'Libros recientes',\n    'books_new' => 'Libros nuevos',\n    'books_new_action' => 'Libro nuevo',\n    'books_popular_empty' => 'Los libros más populares aparecerán aquí.',\n    'books_new_empty' => 'Los libros creados más recientemente aparecerán aquí.',\n    'books_create' => 'Crear nuevo libro',\n    'books_delete' => 'Borrar libro',\n    'books_delete_named' => 'Borrar libro :bookName',\n    'books_delete_explain' => 'Esto borrará el libro con el nombre \\':bookName\\', Todas las páginas y capítulos serán removidos.',\n    'books_delete_confirmation' => '¿Está seguro de que desea borrar este libro?',\n    'books_edit' => 'Editar Libro',\n    'books_edit_named' => 'Editar Libro :bookName',\n    'books_form_book_name' => 'Nombre de libro',\n    'books_save' => 'Guardar libro',\n    'books_permissions' => 'permisos de libro',\n    'books_permissions_updated' => 'Permisos de libro actualizados',\n    'books_empty_contents' => 'Ninguna página o capítulo ha sido creada para este libro.',\n    'books_empty_create_page' => 'Crear una nueva página',\n    'books_empty_sort_current_book' => 'Organizar el libro actual',\n    'books_empty_add_chapter' => 'Agregar un capítulo',\n    'books_permissions_active' => 'Permisos de libro activados',\n    'books_search_this' => 'Buscar en este libro',\n    'books_navigation' => 'Navegación de libro',\n    'books_sort' => 'Organizar contenido de libro',\n    'books_sort_desc' => 'Mueve capítulos y páginas dentro de un libro para reorganizar su contenido. Se pueden añadir otros libros que permiten mover fácilmente capítulos y páginas entre libros. Opcionalmente, se puede establecer una regla de ordenación automática para el contenido de este libro cuando haya cambios.',\n    'books_sort_auto_sort' => 'Opción de ordenación automática',\n    'books_sort_auto_sort_active' => 'Opción de ordenación activa: sortName',\n    'books_sort_named' => 'Organizar libro :bookName',\n    'books_sort_name' => 'Organizar por nombre',\n    'books_sort_created' => 'Organizar por fecha de creación',\n    'books_sort_updated' => 'Organizar por fecha de actualización',\n    'books_sort_chapters_first' => 'Capítulos primero',\n    'books_sort_chapters_last' => 'Capítulos al final',\n    'books_sort_show_other' => 'Mostrar otros libros',\n    'books_sort_save' => 'Guardar nuevo orden',\n    'books_sort_show_other_desc' => 'Añada aquí otros libros para incluirlos en la ordenación, y permita una fácil reorganización entre libros.',\n    'books_sort_move_up' => 'Subir',\n    'books_sort_move_down' => 'Bajar',\n    'books_sort_move_prev_book' => 'Mover al libro anterior',\n    'books_sort_move_next_book' => 'Mover al libro siguiente',\n    'books_sort_move_prev_chapter' => 'Mover al capítulo anterior',\n    'books_sort_move_next_chapter' => 'Mover al capítulo siguiente',\n    'books_sort_move_book_start' => 'Mover al inicio del libro',\n    'books_sort_move_book_end' => 'Mover al final del libro',\n    'books_sort_move_before_chapter' => 'Mover a antes del capítulo',\n    'books_sort_move_after_chapter' => 'Mover a después del capítulo',\n    'books_copy' => 'Copiar Libro',\n    'books_copy_success' => 'Libro copiado correctamente',\n\n    // Chapters\n    'chapter' => 'Capítulo',\n    'chapters' => 'Capítulos',\n    'x_chapters' => ':count Capítulo|:count Capítulos',\n    'chapters_popular' => 'Capítulos populares',\n    'chapters_new' => 'Nuevo capítulo',\n    'chapters_create' => 'Crear nuevo capítulo',\n    'chapters_delete' => 'Borrar capítulo',\n    'chapters_delete_named' => 'Borrar capítulo :chapterName',\n    'chapters_delete_explain' => 'Esta acción eliminará el capítulo con el nombre \\':chapterName\\'. Todas las páginas que existen dentro del capítulo también se eliminarán.',\n    'chapters_delete_confirm' => '¿Está seguro de borrar este capítulo?',\n    'chapters_edit' => 'Editar capítulo',\n    'chapters_edit_named' => 'Editar capítulo :chapterName',\n    'chapters_save' => 'Guardar capítulo',\n    'chapters_move' => 'Mover capítulo',\n    'chapters_move_named' => 'Mover Capítulo :chapterName',\n    'chapters_copy' => 'Copiar Capítulo',\n    'chapters_copy_success' => 'Capítulo copiado correctamente',\n    'chapters_permissions' => 'Permisos de capítulo',\n    'chapters_empty' => 'No existen páginas en este capítulo.',\n    'chapters_permissions_active' => 'Permisos de capítulo activado',\n    'chapters_permissions_success' => 'Permisos de capítulo actualizados',\n    'chapters_search_this' => 'Buscar en este capítulo',\n    'chapter_sort_book' => 'Organizar Libro',\n\n    // Pages\n    'page' => 'Página',\n    'pages' => 'Páginas',\n    'x_pages' => ':count Página|:count Páginas',\n    'pages_popular' => 'Páginas populares',\n    'pages_new' => 'Nueva página',\n    'pages_attachments' => 'Adjuntos',\n    'pages_navigation' => 'Navegación de página',\n    'pages_delete' => 'Borrar página',\n    'pages_delete_named' => 'Borrar página :pageName',\n    'pages_delete_draft_named' => 'Borrar borrador de página :pageName',\n    'pages_delete_draft' => 'Borrar borrador de página',\n    'pages_delete_success' => 'Página borrada',\n    'pages_delete_draft_success' => 'Borrador de página borrado',\n    'pages_delete_warning_template' => 'Esta página está en uso como plantilla de página predeterminada de libro o capítulo. Estos libros o capítulos ya no tendrán una plantilla de página predeterminada asignada después de eliminar esta página.',\n    'pages_delete_confirm' => '¿Está seguro de borrar esta página?',\n    'pages_delete_draft_confirm' => 'Está seguro de que desea borrar este borrador de página?',\n    'pages_editing_named' => 'Editando página :pageName',\n    'pages_edit_draft_options' => 'Opciones de borrador',\n    'pages_edit_save_draft' => 'Guardar borrador',\n    'pages_edit_draft' => 'Editar borrador de página',\n    'pages_editing_draft' => 'Editando borrador',\n    'pages_editing_page' => 'Editando página',\n    'pages_edit_draft_save_at' => 'Borrador guardado el ',\n    'pages_edit_delete_draft' => 'Borrar borrador',\n    'pages_edit_delete_draft_confirm' => '¡Está seguro que quiere eliminar los cambios realizados en el borrador? Se perderán todos los cambios hechos desde el último guardado completo y el editor se actualizará con el último estado de la página que se haya guardado.',\n    'pages_edit_discard_draft' => 'Descartar borrador',\n    'pages_edit_switch_to_markdown' => 'Cambiar a Editor Markdown',\n    'pages_edit_switch_to_markdown_clean' => '(Limpiar Contenido)',\n    'pages_edit_switch_to_markdown_stable' => '(Contenido Estable)',\n    'pages_edit_switch_to_wysiwyg' => 'Cambiar a Editor WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg' => 'Cambiar a nuevo editor WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(En prueba beta)',\n    'pages_edit_set_changelog' => 'Establecer cambios de registro',\n    'pages_edit_enter_changelog_desc' => 'Introduzca una breve descripción de los cambios que ha realizado',\n    'pages_edit_enter_changelog' => 'Entrar en cambio de registro',\n    'pages_editor_switch_title' => 'Cambiar editor',\n    'pages_editor_switch_are_you_sure' => '¿Está seguro de que desea cambiar el editor en esta página?',\n    'pages_editor_switch_consider_following' => 'Considere lo siguiente cuando cambie de editor:',\n    'pages_editor_switch_consideration_a' => 'Una vez guardado, el nuevo editor será utilizado por todos los usuarios nuevos, incluyendo aquellos que quizás no tengan permisos para cambiar su editor.',\n    'pages_editor_switch_consideration_b' => 'Esto puede llevar a una pérdida de detalle y sintaxis en ciertas circunstancias.',\n    'pages_editor_switch_consideration_c' => 'Cambios en la etiqueta o en el registro de cambios, realizados desde la última vez que se guardan, no persistirán a través de este cambio.',\n    'pages_save' => 'Guardar página',\n    'pages_title' => 'Título de página',\n    'pages_name' => 'Nombre de página',\n    'pages_md_editor' => 'Editor',\n    'pages_md_preview' => 'Previsualizar',\n    'pages_md_insert_image' => 'Insertar Imagen',\n    'pages_md_insert_link' => 'Insertar link de entidad',\n    'pages_md_insert_drawing' => 'Insertar Dibujo',\n    'pages_md_show_preview' => 'Mostrar vista previa',\n    'pages_md_sync_scroll' => 'Sincronizar desplazamiento de vista previa',\n    'pages_md_plain_editor' => 'Editor de texto plano',\n    'pages_drawing_unsaved' => 'Encontrado dibujo sin guardar',\n    'pages_drawing_unsaved_confirm' => 'Se encontraron datos del dibujo no guardados durante un intento de guardado fallido anterior. ¿Desea restaurar y continuar editando el dibujo no guardado?',\n    'pages_not_in_chapter' => 'La página no esá en el capítulo',\n    'pages_move' => 'Mover página',\n    'pages_copy' => 'Copiar página',\n    'pages_copy_desination' => 'Destino de la copia',\n    'pages_copy_success' => 'Página copiada con éxito',\n    'pages_permissions' => 'Permisos de página',\n    'pages_permissions_success' => 'Permisos de página actualizados',\n    'pages_revision' => 'Revisión',\n    'pages_revisions' => 'Revisiones de página',\n    'pages_revisions_desc' => 'A continuación se listan todas las revisiones anteriores de esta página. Puede volver la vista atrás, comparar y restaurar versiones antiguas de la página si los permisos lo permiten. Es posible que el historial completo de la página no se refleje en esta sección. Dependiendo de la configuración del sistema, las revisiones viejas podrían eliminarse automáticamente.',\n    'pages_revisions_named' => 'Revisiones de página para :pageName',\n    'pages_revision_named' => 'Revisión de ágina para :pageName',\n    'pages_revision_restored_from' => 'Restaurado desde #:id; :summary',\n    'pages_revisions_created_by' => 'Creado por',\n    'pages_revisions_date' => 'Fecha de revisión',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Número de Revisión',\n    'pages_revisions_numbered' => 'Revisión #:id',\n    'pages_revisions_numbered_changes' => 'Cambios de Revisión #:id',\n    'pages_revisions_editor' => 'Tipo de Editor',\n    'pages_revisions_changelog' => 'Registro de cambios',\n    'pages_revisions_changes' => 'Cambios',\n    'pages_revisions_current' => 'Versión actual',\n    'pages_revisions_preview' => 'Previsualizar',\n    'pages_revisions_restore' => 'Restaurar',\n    'pages_revisions_none' => 'Esta página no tiene revisiones',\n    'pages_copy_link' => 'Copiar enlace',\n    'pages_edit_content_link' => 'Ir a la sección en el editor',\n    'pages_pointer_enter_mode' => 'Modo de selección de sección',\n    'pages_pointer_label' => 'Opciones de sección de página',\n    'pages_pointer_permalink' => 'Sección de enlace permanente de página',\n    'pages_pointer_include_tag' => 'Incluir Etiqueta a Sección de Página',\n    'pages_pointer_toggle_link' => 'Modo de enlace permanente, presiona para mostrar la etiqueta',\n    'pages_pointer_toggle_include' => 'Modo de etiqueta, presiona para mostrar enlace permanente',\n    'pages_permissions_active' => 'Permisos de página activos',\n    'pages_initial_revision' => 'Publicación inicial',\n    'pages_references_update_revision' => 'Actualización automática de enlaces internos',\n    'pages_initial_name' => 'Página nueva',\n    'pages_editing_draft_notification' => 'Usted está actualmente editando un borrador que fue guardado por última vez el :timeDiff.',\n    'pages_draft_edited_notification' => 'Esta página ha sido actualizada desde aquel momento. Se recomienda que cancele este borrador.',\n    'pages_draft_page_changed_since_creation' => 'Esta página fue actualizada desde que se creó este borrador. Se recomienda descartar este borrador o tener cuidado de no sobrescribir ningún cambio en la página.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count usuarios han comenzado a editar esta página',\n        'start_b' => ':userName ha comenzado a editar esta página',\n        'time_a' => 'desde que las página fue actualizada',\n        'time_b' => 'en los últimos :minCount minutos',\n        'message' => ':start :time. Ten cuidado de no sobreescribir los cambios del otro usuario',\n    ],\n    'pages_draft_discarded' => '¡Borrador descartado! El editor se actualizó con el contenido actual de la página',\n    'pages_draft_deleted' => '¡Borrador eliminado! El editor se actualizó con el contenido actual de la página',\n    'pages_specific' => 'Página Específica',\n    'pages_is_template' => 'Plantilla de Página',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Mostrar/ocultar barra lateral',\n    'page_tags' => 'Etiquetas de página',\n    'chapter_tags' => 'Etiquetas de capítulo',\n    'book_tags' => 'Etiquetas de libro',\n    'shelf_tags' => 'Etiquetas de Estante',\n    'tag' => 'Etiqueta',\n    'tags' =>  'Etiquetas',\n    'tags_index_desc' => 'Las etiquetas se pueden aplicar al contenido dentro del sistema para lograr una forma flexible de categorización. Las etiquetas pueden tener tanto una clave como un valor, siendo el valor opcional. Una vez aplicado, el contenido puede ser consultado usando el nombre y el valor de la etiqueta.',\n    'tag_name' =>  'Nombre de etiqueta',\n    'tag_value' => 'Valor de la etiqueta (Opcional)',\n    'tags_explain' => \"Agregar algunas etiquetas para mejorar la categorización de su contenido. \\n Se puede asignar un valor a una etiqueta para una organizacón con mayor detalle.\",\n    'tags_add' => 'Agregar otra etiqueta',\n    'tags_remove' => 'Eliminar esta etiqueta',\n    'tags_usages' => 'Uso total de etiquetas',\n    'tags_assigned_pages' => 'Asignadas a páginas',\n    'tags_assigned_chapters' => 'Asignadas a capítulos',\n    'tags_assigned_books' => 'Asignadas a libros',\n    'tags_assigned_shelves' => 'Asignadas a estantes',\n    'tags_x_unique_values' => ':count valores únicos',\n    'tags_all_values' => 'Todos los valores',\n    'tags_view_tags' => 'Ver etiquetas',\n    'tags_view_existing_tags' => 'Ver etiquetas existentes',\n    'tags_list_empty_hint' => 'Las etiquetas se pueden asignar a través de la barra lateral del editor de páginas o mientras se editan los detalles de un libro, capítulo o estante.',\n    'attachments' => 'Adjuntos',\n    'attachments_explain' => 'Subir archivos o agregar enlaces para mostrar en la página. Estos son visibles en la barra lateral de la página.',\n    'attachments_explain_instant_save' => 'Los cambios se guardan de manera instantánea.',\n    'attachments_upload' => 'Archivo adjuntado',\n    'attachments_link' => 'Adjuntar enlace',\n    'attachments_upload_drop' => 'También puedes arrastrar y soltar un archivo aquí para subirlo como un archivo adjunto.',\n    'attachments_set_link' => 'Establecer enlace',\n    'attachments_delete' => '¿Está seguro que desea eliminar el archivo adjunto?',\n    'attachments_dropzone' => 'Arrastre aquí archivos para subirlos',\n    'attachments_no_files' => 'No se adjuntó ningún archivo',\n    'attachments_explain_link' => 'Usted puede agregar un enlace o si lo prefiere puede agregar un archivo. Esto puede ser un enlace a otra página o un enlace a un archivo en la nube.',\n    'attachments_link_name' => 'Nombre del enlace',\n    'attachment_link' => 'Enlace adjunto',\n    'attachments_link_url' => 'Enlace a archivo',\n    'attachments_link_url_hint' => 'URL del sitio o archivo',\n    'attach' => 'Adjuntar',\n    'attachments_insert_link' => 'Agregar el Enlace del adjunto a la Página',\n    'attachments_edit_file' => 'Editar archivo',\n    'attachments_edit_file_name' => 'Nombre del archivo',\n    'attachments_edit_drop_upload' => 'Arrastre los archivos o presione aquí para subir o sobreescribir',\n    'attachments_order_updated' => 'Orden de adjuntos actualizado',\n    'attachments_updated_success' => 'Detalles de adjuntos actualizados',\n    'attachments_deleted' => 'Adjunto borrado',\n    'attachments_file_uploaded' => 'Archivo subido exitosamente',\n    'attachments_file_updated' => 'Archivo actualizado exitosamente',\n    'attachments_link_attached' => 'Enlace agregado exitosamente a la página',\n    'templates' => 'Plantillas',\n    'templates_set_as_template' => 'La Página es una plantilla',\n    'templates_explain_set_as_template' => 'Puede establecer esta página como plantilla para que el contenido pueda utilizarse al crear otras páginas. Otros usuarios podrán utilizar esta plantilla si tienen permisos para ver de esta página.',\n    'templates_replace_content' => 'Reemplazar el contenido de la página',\n    'templates_append_content' => 'Incorporar al fina del contenido de la página',\n    'templates_prepend_content' => 'Incorporar al principio del contenido de la página',\n\n    // Profile View\n    'profile_user_for_x' => 'Usuario para :time',\n    'profile_created_content' => 'Contenido creado',\n    'profile_not_created_pages' => ':userName no ha creado páginas',\n    'profile_not_created_chapters' => ':userName no ha creado capítulos',\n    'profile_not_created_books' => ':userName no ha creado libros',\n    'profile_not_created_shelves' => ':userName no ha creado estantes',\n\n    // Comments\n    'comment' => 'Comentario',\n    'comments' => 'Comentarios',\n    'comment_add' => 'Agregar comentario',\n    'comment_none' => 'No hay comentarios para mostrar',\n    'comment_placeholder' => 'DEjar un comentario aquí',\n    'comment_thread_count' => ':count Hilo de Comentarios|:count Hilos de Comentarios',\n    'comment_archived_count' => ':count Archivados',\n    'comment_archived_threads' => 'Hilos Archivados',\n    'comment_save' => 'Guardar comentario',\n    'comment_new' => 'Nuevo comentario',\n    'comment_created' => 'comentado :createDiff',\n    'comment_updated' => 'Actualizado :updateDiff por :username',\n    'comment_updated_indicator' => 'Actualizado',\n    'comment_deleted_success' => 'Comentario borrado',\n    'comment_created_success' => 'Comentario agregado',\n    'comment_updated_success' => 'Comentario actualizado',\n    'comment_archive_success' => 'Comentario archivado',\n    'comment_unarchive_success' => 'Comentario no archivado',\n    'comment_view' => 'Ver comentario',\n    'comment_jump_to_thread' => 'Ir al hilo',\n    'comment_delete_confirm' => '¿Está seguro que quiere borrar este comentario?',\n    'comment_in_reply_to' => 'En respuesta a :commentId',\n    'comment_reference' => 'Referencia',\n    'comment_reference_outdated' => '(Obsoleto)',\n    'comment_editor_explain' => 'Estos son los comentarios que se escribieron en esta página. Los comentarios se pueden añadir y administrar cuando se visualiza la página guardada.',\n\n    // Revision\n    'revision_delete_confirm' => '¿Está seguro de que quiere eliminar esta revisión?',\n    'revision_restore_confirm' => '¿Está seguro de que quiere restaurar esta revisión? Se reemplazará el contenido de la página actual.',\n    'revision_cannot_delete_latest' => 'No se puede eliminar la última revisión.',\n\n    // Copy view\n    'copy_consider' => 'Por favor, tenga en cuenta lo siguiente al copiar el contenido.',\n    'copy_consider_permissions' => 'Los ajustes de permisos personalizados no serán copiados.',\n    'copy_consider_owner' => 'Usted se convertirá en el dueño de todo el contenido copiado.',\n    'copy_consider_images' => 'Los archivos de imagen de la página no serán duplicados y las imágenes originales conservarán su relación con la página a la que fueron subidos originalmente.',\n    'copy_consider_attachments' => 'Los archivos adjuntos de la página no serán copiados.',\n    'copy_consider_access' => 'Un cambio de ubicación, propietario o permisos puede resultar en que este contenido sea accesible para aquellos que anteriormente no tuvieran acceso.',\n\n    // Conversions\n    'convert_to_shelf' => 'Convertir a Estante',\n    'convert_to_shelf_contents_desc' => 'Puedes convertir este libro a un nuevo estante con el mismo contenido. Los capítulos contenidos en este libro se convertirán en libros nuevos. Si este libro contiene alguna página, que no esté en un capítulo, este libro será renombrado y contendrá tales páginas, y este libro pasará a formar parte del nuevo estante.',\n    'convert_to_shelf_permissions_desc' => 'Cualquier permiso establecido en este libro será copiado al nuevo estante y a todos los nuevos libros que no tengan sus propios permisos configurados. Tenga en cuenta que los permisos de los estantes no se aplican automáticamente sobre el contenido en su interior, como lo hacen para los libros.',\n    'convert_book' => 'Convertir Libro',\n    'convert_book_confirm' => '¿Está seguro de que desea convertir este libro?',\n    'convert_undo_warning' => 'Esto no se puede deshacer de forma sencilla.',\n    'convert_to_book' => 'Convertir a Libro',\n    'convert_to_book_desc' => 'Puede convertir este capítulo en un nuevo libro con el mismo contenido. Cualquier permiso establecido en este capítulo será copiado al nuevo libro pero cualquier permiso heredado, del libro padre, no se copiará lo que podría derivar en un cambio en el control de acceso.',\n    'convert_chapter' => 'Convertir Capítulo',\n    'convert_chapter_confirm' => '¿Está seguro de que quiere convertir este capítulo?',\n\n    // References\n    'references' => 'Referencias',\n    'references_none' => 'No hay referencias a este elemento.',\n    'references_to_desc' => 'A continuación se muestra todo el contenido conocido en el sistema que enlaza a este elemento.',\n\n    // Watch Options\n    'watch' => 'Suscribirme',\n    'watch_title_default' => 'Preferencias Predeterminadas',\n    'watch_desc_default' => 'Revertir las suscripciones a solo tus preferencias de notificación predeterminadas.',\n    'watch_title_ignore' => 'Ignorar',\n    'watch_desc_ignore' => 'Ignorar todas las notificaciones, incluyendo las de preferencias a nivel de usuario.',\n    'watch_title_new' => 'Páginas Nuevas',\n    'watch_desc_new' => 'Notificar cuando se crea una nueva página dentro de este elemento.',\n    'watch_title_updates' => 'Todas las actualizaciones de página',\n    'watch_desc_updates' => 'Notificar todas las páginas nuevas y todos los cambios de página.',\n    'watch_desc_updates_page' => 'Notificar todos los cambios de página.',\n    'watch_title_comments' => 'Todas las actualizaciones y comentarios de página',\n    'watch_desc_comments' => 'Notificar todas las páginas nuevas, todos los cambios de página y todos los nuevos comentarios.',\n    'watch_desc_comments_page' => 'Notificar los cambios en las páginas y los nuevos comentarios.',\n    'watch_change_default' => 'Cambiar preferencias de notificación predeterminadas',\n    'watch_detail_ignore' => 'Ignorando notificaciones',\n    'watch_detail_new' => 'Suscripciones de nuevas páginas',\n    'watch_detail_updates' => 'Suscripciones de nuevas páginas y actualizaciones de páginas',\n    'watch_detail_comments' => 'Suscripciones de nuevas páginas, actualizaciones de páginas y comentarios',\n    'watch_detail_parent_book' => 'Subscripciones por libro contenedor',\n    'watch_detail_parent_book_ignore' => 'Ignorando a través del libro contenedor',\n    'watch_detail_parent_chapter' => 'Subscripciones por capítulo contenedor',\n    'watch_detail_parent_chapter_ignore' => 'Ignorar por capítulo contenedor',\n];\n"
  },
  {
    "path": "lang/es_AR/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Ud. no tiene permisos para visualizar la página solicitada.',\n    'permissionJson' => 'Ud. no tiene permisos para ejecutar la acción solicitada.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Un usuario con el email :email ya existe pero con credenciales diferentes.',\n    'auth_pre_register_theme_prevention' => 'La cuenta de usuario no pudo ser registrada con los detalles proporcionados',\n    'email_already_confirmed' => 'El email ya ha sido confirmado, Intente loguearse en la aplicación.',\n    'email_confirmation_invalid' => 'Este token de confirmación no e válido o ya ha sido usado,Intente registrar uno nuevamente.',\n    'email_confirmation_expired' => 'El token de confirmación ha expirado, Un nuevo email de confirmacón ha sido enviado.',\n    'email_confirmation_awaiting' => 'La dirección de correo electrónico de la cuenta en uso debe ser confirmada',\n    'ldap_fail_anonymous' => 'El acceso con LDAP ha fallado usando binding anónimo',\n    'ldap_fail_authed' => 'El acceso LDAP usando el dn & password detallados',\n    'ldap_extension_not_installed' => 'La extensión LDAP PHP no se encuentra instalada',\n    'ldap_cannot_connect' => 'No se puede conectar con el servidor ldap, la conexión inicial ha fallado',\n    'saml_already_logged_in' => 'Ya estás conectado',\n    'saml_no_email_address' => 'No se pudo encontrar una dirección de correo electrónico, para este usuario, en los datos proporcionados por el sistema de autenticación externo',\n    'saml_invalid_response_id' => 'La solicitud del sistema de autenticación externo no está reconocida por un proceso iniciado por esta aplicación. Navegar hacia atrás después de un inicio de sesión podría causar este problema.',\n    'saml_fail_authed' => 'El inicio de sesión con :system falló, el sistema no proporcionó una autorización correcta',\n    'oidc_already_logged_in' => 'Ya está conectado',\n    'oidc_no_email_address' => 'No se pudo encontrar una dirección de correo electrónico para este usuario en los datos proporcionados por el sistema de autenticación externo',\n    'oidc_fail_authed' => 'El inicio de sesión con :system falló, el sistema no proporcionó una autorización correcta',\n    'social_no_action_defined' => 'Acción no definida',\n    'social_login_bad_response' => \"SE recibió un Error durante el acceso con :socialAccount : \\n:error\",\n    'social_account_in_use' => 'la cuenta :socialAccount ya se encuentra en uso, intente loguearse a través de la opcón :socialAccount .',\n    'social_account_email_in_use' => 'El email :email ya se encuentra en uso. Si ud. ya dispone de una cuenta puede loguearse a través de su cuenta :socialAccount desde la configuración de perfil.',\n    'social_account_existing' => 'La cuenta :socialAccount ya se encuentra asignada a su perfil.',\n    'social_account_already_used_existing' => 'La cuenta :socialAccount ya se encuentra usada por otro usuario.',\n    'social_account_not_used' => 'La cuenta :socialAccount no está asociada a ningún usuario. Por favor adjuntela a su configuración de perfil. ',\n    'social_account_register_instructions' => 'Si no dispone de una cuenta, puede registrar una cuenta usando la opción de :socialAccount .',\n    'social_driver_not_found' => 'Driver social no encontrado',\n    'social_driver_not_configured' => 'Su configuración :socialAccount no es correcta.',\n    'invite_token_expired' => 'El enace de la esta invitación expiró. Puede intentar restablecer la contraseña de su cuenta',\n    'login_user_not_found' => 'No se pudo encontrar un usuario para esta acción.',\n\n    // System\n    'path_not_writable' => 'La ruta :filePath no pudo ser cargada. Asegurese de que es escribible por el servidor.',\n    'cannot_get_image_from_url' => 'No se puede obtener la imagen desde :url',\n    'cannot_create_thumbs' => 'El servidor no puede crear la imagen miniatura. Por favor chequee que tiene la extensión GD instalada.',\n    'server_upload_limit' => 'El servidor no permite la subida de ficheros de este tamañ. Por favor intente con un fichero de menor tamañ.',\n    'server_post_limit' => 'El servidor no puede recibir la cantidad de datos proporcionados. Inténtelo de nuevo con menos datos o un archivo más pequeño.',\n    'uploaded'  => 'El servidor no permite subir archivos de este tamaño. Por favor intente un tamaño menor.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Ha ocurrido un error al subir la imagen',\n    'image_upload_type_error' => 'El tipo de imagen subida es inválido.',\n    'image_upload_replace_type' => 'Los reemplazos de archivos de imágenes deben ser del mismo tipo',\n    'image_upload_memory_limit' => 'No se pudo gestionar la subida de imágenes y/o crear miniaturas debido a los límites de recursos del sistema.',\n    'image_thumbnail_memory_limit' => 'Error al crear imágenes de distintos tamaños debido a los límites de los recursos del sistema.',\n    'image_gallery_thumbnail_memory_limit' => 'Error al crear imágenes de previsualización de la galería debido a los límites de los recursos del sistema.',\n    'drawing_data_not_found' => 'No se pudieron cargar los datos del dibujo. Es probable que el archivo de dibujo ya no exista o que no tenga permiso para acceder a él.',\n\n    // Attachments\n    'attachment_not_found' => 'No se encuentra el objeto adjunto',\n    'attachment_upload_error' => 'Ocurrió un error al subir el archivo adjunto',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Fallo al guardar borrador. Asegurese de que tiene conexión a Internet antes de guardar este borrador',\n    'page_draft_delete_fail' => 'Error al eliminar el borrador de la página y obtener el último contenido guardado',\n    'page_custom_home_deletion' => 'No se puede eliminar una página cuando está configurada como página de inicio',\n\n    // Entities\n    'entity_not_found' => 'Entidad no encontrada',\n    'bookshelf_not_found' => 'Estante no encontrado',\n    'book_not_found' => 'Libro no encontrado',\n    'page_not_found' => 'Página no encontrada',\n    'chapter_not_found' => 'Capítulo no encontrado',\n    'selected_book_not_found' => 'El libro seleccionado no fue encontrado',\n    'selected_book_chapter_not_found' => 'El libro o capítulo seleccionado no fue encontrado',\n    'guests_cannot_save_drafts' => 'Los invitados no pueden guardar los borradores',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'No se puede borrar el único administrador',\n    'users_cannot_delete_guest' => 'No se puede borrar el usuario invitado',\n    'users_could_not_send_invite' => 'No se creó el usuario porque no se pudo enviar el correo de invitación',\n\n    // Roles\n    'role_cannot_be_edited' => 'Este rol no puede ser editado',\n    'role_system_cannot_be_deleted' => 'Este rol es un rol de sistema y no puede ser borrado',\n    'role_registration_default_cannot_delete' => 'Este rol no puede ser borrado mientras sea el rol por defecto de registro',\n    'role_cannot_remove_only_admin' => 'Este usuario es el único asignado al rol de administrador. Asigne el rol de administrador a otro usuario antes de intentar eliminarlo.',\n\n    // Comments\n    'comment_list' => 'Se produjo un error al obtener los comentarios.',\n    'cannot_add_comment_to_draft' => 'No puede gregar comentarios a un borrador.',\n    'comment_add' => 'Se produjo un error al agregar o actualizar el comentario.',\n    'comment_delete' => 'Se produjo un error al borrar el comentario.',\n    'empty_comment' => 'No se puede agregar un comentario vacío.',\n\n    // Error pages\n    '404_page_not_found' => 'Página no encontrada',\n    'sorry_page_not_found' => 'Lo sentimos, la página que intenta acceder no pudo ser encontrada.',\n    'sorry_page_not_found_permission_warning' => 'Si esperaba que esta página existiera, puede que no tenga permiso para verla.',\n    'image_not_found' => 'No se encuentra la imagen',\n    'image_not_found_subtitle' => 'Lo siento, no se pudo encontrar la imagen que busca.',\n    'image_not_found_details' => 'Si esperaba que esta imagen exista es probable que se haya eliminado.',\n    'return_home' => 'Volver al home',\n    'error_occurred' => 'Ha ocurrido un error',\n    'app_down' => 'La aplicación :appName se encuentra caída en este momento',\n    'back_soon' => 'Volverá a estar operativa en corto tiempo.',\n\n    // Import\n    'import_zip_cant_read' => 'No se pudo leer el archivo ZIP.',\n    'import_zip_cant_decode_data' => 'No se pudo encontrar ni decodificar el contenido del archivo ZIP data.json.',\n    'import_zip_no_data' => 'Los datos del archivo ZIP no tienen un libro, un capítulo o contenido de página en su contenido.',\n    'import_zip_data_too_large' => 'El contenido del ZIP data.json excede el tamaño máximo de carga configurado.',\n    'import_validation_failed' => 'Error al validar la importación del ZIP con los errores:',\n    'import_zip_failed_notification' => 'Error al importar archivo ZIP.',\n    'import_perms_books' => 'Le faltan los permisos necesarios para crear libros.',\n    'import_perms_chapters' => 'Le faltan los permisos necesarios para crear capítulos.',\n    'import_perms_pages' => 'Le faltan los permisos necesarios para crear páginas.',\n    'import_perms_images' => 'Le faltan los permisos necesarios para crear imágenes.',\n    'import_perms_attachments' => 'Le faltan los permisos necesarios para crear adjuntos.',\n\n    // API errors\n    'api_no_authorization_found' => 'No se encontró ningún token de autorización en la solicitud',\n    'api_bad_authorization_format' => 'Se ha encontrado un token de autorización en la solicitud pero el formato era incorrecto',\n    'api_user_token_not_found' => 'No se ha encontrado un token API que corresponda con el token de autorización proporcionado',\n    'api_incorrect_token_secret' => 'El secreto proporcionado para el token API usado es incorrecto',\n    'api_user_no_api_permission' => 'El propietario del token API usado no tiene permiso para hacer llamadas API',\n    'api_user_token_expired' => 'El token de autorización usado ha caducado',\n    'api_cookie_auth_only_get' => 'Sólo se permiten peticiones GET cuando se utiliza el API con autenticación basada en cookies',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Error al enviar un email de prueba:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'La URL no coincide con los hosts SSR configurados como permitidos',\n];\n"
  },
  {
    "path": "lang/es_AR/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Nuevo comentario en la página: :pageName',\n    'new_comment_intro' => 'Un usuario comentó en una página de :appName:',\n    'new_page_subject' => 'Página nueva: :pageName',\n    'new_page_intro' => 'Se creó una nueva página en :appName:',\n    'updated_page_subject' => 'Página actualizada: :pageName',\n    'updated_page_intro' => 'Se actualizó una página en :appName:',\n    'updated_page_debounce' => 'Para evitar una avalancha de notificaciones, durante un tiempo no se enviarán notificaciones sobre más ediciones de esta página por el mismo editor.',\n    'comment_mention_subject' => 'Ha sido mencionado en un comentario en la página: :pageName',\n    'comment_mention_intro' => 'Fue mencionado en un comentario en :appName:',\n\n    'detail_page_name' => 'Nombre de la página:',\n    'detail_page_path' => 'Ruta de la página:',\n    'detail_commenter' => 'Comentarista:',\n    'detail_comment' => 'Comentario:',\n    'detail_created_by' => 'Creado por:',\n    'detail_updated_by' => 'Actualizado por:',\n\n    'action_view_comment' => 'Ver comentario',\n    'action_view_page' => 'Ver página',\n\n    'footer_reason' => 'Esta notificación le fue enviada porque :link cubre este tipo de actividad para este elemento.',\n    'footer_reason_link' => 'nuestras preferencias de notificación',\n];\n"
  },
  {
    "path": "lang/es_AR/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Anterior',\n    'next'     => 'Siguiente &raquo;',\n\n];\n"
  },
  {
    "path": "lang/es_AR/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'La contraseña debe ser como mínimo de seis caracteres y coincidir con la confirmación.',\n    'user' => \"No podemos encontrar un usuario con esta dirección de correo electrónico.\",\n    'token' => 'El token para restablecer la contraseña no es válido para esta dirección de correo electrónico.',\n    'sent' => '¡Hemos enviado a su cuenta de correo electrónico un enlace para restaurar su contraseña!',\n    'reset' => '¡Su contraseña fue restaurada!',\n\n];\n"
  },
  {
    "path": "lang/es_AR/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Mi cuenta',\n\n    'shortcuts' => 'Atajos',\n    'shortcuts_interface' => 'Preferencias de atajos en la interfaz',\n    'shortcuts_toggle_desc' => 'Aquí puede activar o desactivar los accesos rápidos de la interfaz, utilizados para la navegación y las acciones.',\n    'shortcuts_customize_desc' => 'Puede personalizar cada uno de los atajos a continuación. Simplemente pulse la combinación de teclas deseada después de seleccionar la entrada para un atajo.',\n    'shortcuts_toggle_label' => 'Atajos de teclado habilitados',\n    'shortcuts_section_navigation' => 'Navegación',\n    'shortcuts_section_actions' => 'Acciones comunes',\n    'shortcuts_save' => 'Guardar atajos',\n    'shortcuts_overlay_desc' => 'Nota: Cuando se activan los atajos de teclado se puede visualizar la ayuda presionando la tecla \"?\", que resaltará los atajos disponibles para las acciones visibles actualmente en la pantalla.',\n    'shortcuts_update_success' => '¡Se actualizaron las preferencias de atajos de teclado!',\n    'shortcuts_overview_desc' => 'Gestione los atajos de teclado que puede utilizar para navegar por la interfaz de usuario del sistema.',\n\n    'notifications' => 'Preferencias de notificaciones',\n    'notifications_desc' => 'Controle las notificaciones por correo electrónico que recibe cuando se realiza cierta actividad dentro del sistema.',\n    'notifications_opt_own_page_changes' => 'Notificar sobre los cambios en las páginas de las que soy propietario',\n    'notifications_opt_own_page_comments' => 'Notificar sobre comentarios en las páginas de las que soy propietario',\n    'notifications_opt_comment_mentions' => 'Notificarme cuando he sido mencionado en un comentario',\n    'notifications_opt_comment_replies' => 'Notificar sobre respuestas a mis comentarios',\n    'notifications_save' => 'Guardar preferencias',\n    'notifications_update_success' => '¡Se actualizaron las preferencias de notificaciones!',\n    'notifications_watched' => 'Elementos vigilados e ignorados',\n    'notifications_watched_desc' => 'A continuación se muestran los elementos que tienen preferencias personalizadas de monitorización. Para actualizar sus preferencias, vea el artículo y las opciones se mostrarán en la barra lateral.',\n\n    'auth' => 'Acceso y seguridad',\n    'auth_change_password' => 'Cambiar contraseña',\n    'auth_change_password_desc' => 'Cambie la contraseña que utiliza para iniciar sesión en la aplicación. Debe tener al menos 8 caracteres.',\n    'auth_change_password_success' => '¡Se actualizó la contraseña!',\n\n    'profile' => 'Detalles del perfil',\n    'profile_desc' => 'Administre los detalles de su cuenta que lo representan ante otros usuarios, además de los detalles que se utilizan para la comunicación y la personalización del sistema.',\n    'profile_view_public' => 'Ver Perfil Público',\n    'profile_name_desc' => 'Configure el nombre que será visible para otros usuarios del sistema a través de la actividad que realiza, y el contenido que posee.',\n    'profile_email_desc' => 'Este correo electrónico se utilizará para las notificaciones y, dependiendo de la autenticación activa del sistema, para el acceso al sistema.',\n    'profile_email_no_permission' => 'Lamentablemente no tiene permiso para cambiar su dirección de correo electrónico. Si desea cambiar esto, necesitará pedir a un administrador que lo cambie por usted.',\n    'profile_avatar_desc' => 'Seleccione una imagen que se utilizará para representarlo ante los demás en el sistema. Idealmente esta imagen debe ser cuadrada y alrededor de 256px de ancho y alto.',\n    'profile_admin_options' => 'Opciones de Administrador',\n    'profile_admin_options_desc' => 'Opciones adicionales de nivel de administrador, como aquellas para administrar asignaciones de rol, se pueden encontrar para su cuenta de usuario en el área de \"Ajustes > Usuarios\" de la aplicación.',\n\n    'delete_account' => 'Eliminar Cuenta',\n    'delete_my_account' => 'Eliminar Mi Cuenta',\n    'delete_my_account_desc' => 'Esto eliminará completamente su cuenta de usuario del sistema. No podrá recuperar esta cuenta o revertir esta acción. El contenido que ha creado, como páginas creadas e imágenes subidas, permanecerá.',\n    'delete_my_account_warning' => '¿Está seguro de que desea eliminar su cuenta?',\n];\n"
  },
  {
    "path": "lang/es_AR/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Ajustes',\n    'settings_save' => 'Guardar ajustes',\n    'system_version' => 'Versión del Sistema',\n    'categories' => 'Categorías',\n\n    // App Settings\n    'app_customization' => 'Personalización',\n    'app_features_security' => 'Características y Seguridad',\n    'app_name' => 'Nombre de aplicación',\n    'app_name_desc' => 'Este nombre se muestra en la cabecera y en cualquier correo electrónico de la aplicación',\n    'app_name_header' => '¿Mostrar el nombre de la aplicación en la cabecera?',\n    'app_public_access' => 'Acceso Público',\n    'app_public_access_desc' => 'Habilitar esta opción permitirá a los visitantes, que no estén autenticados, acceder al contenido en la instancia de BookStack.',\n    'app_public_access_desc_guest' => 'El acceso de visitantes públicos se puede controlar mediante el usuario \"Guest/Invitado\".',\n    'app_public_access_toggle' => 'Permitir el acceso público',\n    'app_public_viewing' => '¿Permitir vista pública?',\n    'app_secure_images' => '¿Habilitar mayor seguridad para subir imágenes?',\n    'app_secure_images_toggle' => 'Habilitar seguridad alta para subir imágenes',\n    'app_secure_images_desc' => 'Por razones de rendimiento, todas las imágenes son públicas. Esta opción agrega una cadena larga difícil de adivinar, asegúrese que los índices de directorios no están habilitados para prevenir el acceso fácil a las imágenes.',\n    'app_default_editor' => 'Editor de Página por defecto',\n    'app_default_editor_desc' => 'Seleccione qué editor se utilizará por defecto cuando se editen nuevas páginas. Esta acción se puede anular al nivel de página si los permisos lo permiten.',\n    'app_custom_html' => 'Contenido de cabecera HTML personalizable',\n    'app_custom_html_desc' => 'Cualquier contenido agregado aquí será agregado al final de la sección <head> de cada página. Esto es útil para sobreescribir estilos o agregar código para analíticas.',\n    'app_custom_html_disabled_notice' => 'El contenido personailzado para la cabecera HTML está deshabilitado en esta configuración para garantizar que cualquier cambio importante se pueda revertir.',\n    'app_logo' => 'Logo de la aplicación',\n    'app_logo_desc' => 'Esto se utiliza en la cabecera de la aplicación, entre otras áreas. La imagen debe ser de 86px de altura. Se reducirá el tamaño de las imágenes grandes.',\n    'app_icon' => 'Icono de la aplicación',\n    'app_icon_desc' => 'Este ícono se utiliza para las pestañas del navegador y los accesos directos. Debería ser una imagen cuadrada de 256px en formato PNG.',\n    'app_homepage' => 'Página de inicio de la Aplicación',\n    'app_homepage_desc' => 'Seleccione una página de inicio para mostrar en lugar de la vista por defecto. Se ignoran los permisos de página para las páginas seleccionadas.',\n    'app_homepage_select' => 'Seleccione una página',\n    'app_footer_links' => 'Enlaces de pie de página',\n    'app_footer_links_desc' => 'Añade enlaces para mostrar dentro del pie de página del sitio. Estos se mostrarán en la parte inferior de la mayoría de las páginas, incluyendo aquellas que no requieren estar registrado. Puede utilizar una etiqueta de \"trans::<key>\" para utilizar traducciones definidas por el sistema. Por ejemplo: el uso de \"trans::common.privacy_policy\" proporcionará el texto traducido \"Política de privacidad\" y \"trans::common.terms_of_service\" proporcionará el texto traducido \"Términos de servicio\".',\n    'app_footer_links_label' => 'Etiqueta del enlace',\n    'app_footer_links_url' => 'Dirección URL del enlace',\n    'app_footer_links_add' => 'Añadir enlace al pie de página',\n    'app_disable_comments' => 'Deshabilitar comentarios',\n    'app_disable_comments_toggle' => 'Deshabilitar comentarios',\n    'app_disable_comments_desc' => 'Deshabilitar comentarios en todas las páginas de la aplicación. Los comentarios existentes no se muestran.',\n\n    // Color settings\n    'color_scheme' => 'Esquema de color de la aplicación',\n    'color_scheme_desc' => 'Establece los colores a usar en la interfaz de la aplicación. Los colores pueden configurar por separado para que los modos oscuros y claros se ajusten mejor al tema y garanticen la legibilidad.',\n    'ui_colors_desc' => 'Establece el color principal y el color por defecto de los enlaces de la aplicación. El color principal se usa principalmente para la cabecera, botones y decoraciones de la interfaz. El color por defecto de los enlaces se utiliza para enlaces y acciones de texto, tanto dentro del contenido escrito como en la interfaz de la aplicación.',\n    'app_color' => 'Color principal',\n    'link_color' => 'Color por defecto de los enlaces',\n    'content_colors_desc' => 'Establece los colores para todos los elementos en la jerarquía de organización de la página. Se recomienda elegir colores con un brillo similar al predeterminado para mayor legibilidad.',\n    'bookshelf_color' => 'Color del estante',\n    'book_color' => 'Color del libro',\n    'chapter_color' => 'Color del capítulo',\n    'page_color' => 'Color de la página',\n    'page_draft_color' => 'Color del borrador de página',\n\n    // Registration Settings\n    'reg_settings' => 'Ajustes de registro',\n    'reg_enable' => 'Habilitar Registro',\n    'reg_enable_toggle' => 'Habilitar registro',\n    'reg_enable_desc' => 'Cuando se habilita el registro, el usuario podrá crear su usuario en la aplicación. Con el regsitro, se le otorga un rol de usuario único y por defecto.',\n    'reg_default_role' => 'Rol de usuario por defecto despúes del registro',\n    'reg_enable_external_warning' => 'La opción anterior no se utiliza mientras la autenticación LDAP o SAML externa esté activa. Las cuentas de usuario para los miembros no existentes se crearán automáticamente si la autenticación en el sistema externo en uso es exitosa.',\n    'reg_email_confirmation' => 'Confirmación de correo electrónico',\n    'reg_email_confirmation_toggle' => 'Requerir confirmación de correo electrónico',\n    'reg_confirm_email_desc' => 'Si se utiliza la restricción por dominio, entonces se requerirá la confirmación por correo electrónico y se ignorará el valor a continuación.',\n    'reg_confirm_restrict_domain' => 'Restringir registro al dominio',\n    'reg_confirm_restrict_domain_desc' => 'Introduzca una lista separada por comas de los correos electrónicos del dominio a los que les gustaría restringir el registro por dominio. A los usuarios les será enviado un correo elctrónico para confirmar la dirección antes de que se le permita interactuar con la aplicación. <br> Note que a los usuarios se les permitirá cambiar sus direcciones de correo electrónico luego de un registro éxioso.',\n    'reg_confirm_restrict_domain_placeholder' => 'Ninguna restricción establecida',\n\n    // Sorting Settings\n    'sorting' => 'Listas y ordenación',\n    'sorting_book_default' => 'Orden de libros por defecto',\n    'sorting_book_default_desc' => 'Seleccione la regla de ordenación predeterminada para aplicar a nuevos libros. Esto no afectará a los libros existentes, y puede ser anulado por libro.',\n    'sorting_rules' => 'Reglas de Ordenación',\n    'sorting_rules_desc' => 'Son operaciones de ordenación predefinidas que se pueden aplicar al contenido en el sistema.',\n    'sort_rule_assigned_to_x_books' => 'Asignado a :count libro | Asignado a :count libros',\n    'sort_rule_create' => 'Crear regla de ordenación',\n    'sort_rule_edit' => 'Editar regla de ordenación',\n    'sort_rule_delete' => 'Eliminar regla de ordenación',\n    'sort_rule_delete_desc' => 'Eliminar esta regla de ordenación del sistema. Los Libros que utilicen este tipo se revertirán a la ordenación manual.',\n    'sort_rule_delete_warn_books' => 'Esta regla de ordenación se utiliza actualmente en :count libro(s). ¿Está seguro que desea eliminarla?',\n    'sort_rule_delete_warn_default' => 'Esta regla de ordenación se utiliza actualmente como predeterminada para los libros. ¿Está seguro de que desea eliminarla?',\n    'sort_rule_details' => 'Detalles de la regla de ordenación',\n    'sort_rule_details_desc' => 'Establezca un nombre para esta regla de ordenación, que aparecerá en las listas cuando los usuarios estén seleccionando un orden.',\n    'sort_rule_operations' => 'Operaciones de ordenación',\n    'sort_rule_operations_desc' => 'Configure las acciones de ordenación a realizar moviéndolas de la lista de operaciones disponibles. Al usarse, las operaciones se aplicarán en orden, de arriba a abajo. Cualquier cambio realizado aquí se aplicará a todos los libros asignados al guardar.',\n    'sort_rule_available_operations' => 'Operaciones disponibles',\n    'sort_rule_available_operations_empty' => 'No hay operaciones pendientes',\n    'sort_rule_configured_operations' => 'Operaciones configuradas',\n    'sort_rule_configured_operations_empty' => 'Arrastrar/añadir operaciones desde la lista de \"Operaciones disponibles\"',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Nombre - Alfabético',\n    'sort_rule_op_name_numeric' => 'Nombre - Numérico',\n    'sort_rule_op_created_date' => 'Fecha de creación',\n    'sort_rule_op_updated_date' => 'Fecha de actualización',\n    'sort_rule_op_chapters_first' => 'Capítulos al inicio',\n    'sort_rule_op_chapters_last' => 'Capítulos al final',\n    'sorting_page_limits' => 'Límites de visualización por página',\n    'sorting_page_limits_desc' => 'Establecer cuántos elementos a mostrar por página en varias listas dentro del sistema. Normalmente una cantidad más baja rendirá mejor, mientras que una cantidad más alta evita la necesidad de hacer clic a través de varias páginas. Se recomienda utilizar un múltiplo de 6.',\n\n    // Maintenance settings\n    'maint' => 'Mantenimiento',\n    'maint_image_cleanup' => 'Limpiar imágenes',\n    'maint_image_cleanup_desc' => 'Analizar contenido de páginas y revisiones para detectar cuáles imágenes y dibujos están en uso y cuáles son redundantes. Asegúrese de crear un respaldo completo de imágenes y base de datos antes de ejecutar esta tarea.',\n    'maint_delete_images_only_in_revisions' => 'También elimina imágenes que sólo existen en antiguas revisiones de páginas',\n    'maint_image_cleanup_run' => 'Ejecutar limpieza',\n    'maint_image_cleanup_warning' => 'Se encontraron :count imágenes pontencialmente sin uso. Está seguro de que quiere eliminarlas?',\n    'maint_image_cleanup_success' => 'Se encontraron y se eliminaron :count imágenes pontencialmente sin uso!',\n    'maint_image_cleanup_nothing_found' => 'No se encotraron imágenes sin usar, Nada eliminado!',\n    'maint_send_test_email' => 'Enviar un correo electrónico de prueba',\n    'maint_send_test_email_desc' => 'Esto envía un correo electrónico de prueba a la dirección de correo electrónico especificada en tu perfil.',\n    'maint_send_test_email_run' => 'Enviar correo electrónico de prueba',\n    'maint_send_test_email_success' => 'Correo electrónico enviado a :address',\n    'maint_send_test_email_mail_subject' => 'Probar correo electrónico',\n    'maint_send_test_email_mail_greeting' => '¡El envío de correos electrónicos parece funcionar!',\n    'maint_send_test_email_mail_text' => '¡Enhorabuena! Al recibir esta notificación de correo electrónico, tu configuración de correo electrónico parece estar ajustada correctamente.',\n    'maint_recycle_bin_desc' => 'Los estantes, libros, capítulos y páginas eliminados se envían a la papelera de reciclaje para que puedan ser restauradas o eliminadas permanentemente. Los elementos más antiguos en la papelera de reciclaje pueden ser eliminados automáticamente después de un tiempo dependiendo de la configuración del sistema.',\n    'maint_recycle_bin_open' => 'Abrir papelera de reciclaje',\n    'maint_regen_references' => 'Regenerar Referencias',\n    'maint_regen_references_desc' => 'Esta acción reconstruirá el índice de referencia de elementos cruzados dentro de la base de datos. Normalmente se gestiona automáticamente, pero esta acción puede ser útil para indexar el contenido viejo o añadido mediante métodos no oficiales.',\n    'maint_regen_references_success' => '¡El índice de referencias fue regenerado!',\n    'maint_timeout_command_note' => 'Nota: Esta acción puede tardar en ejecutarse, lo que puede derivar en problemas de tiempo de espera en algunos entornos web. Como alternativa, esta acción se puede realizar usando un comando de terminal.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Papelera de Reciclaje',\n    'recycle_bin_desc' => 'Aquí puede restaurar elementos que hayan sido eliminados o elegir eliminarlos permanentemente del sistema. Esta lista no está filtrada a diferencia de las listas de actividad similares en el sistema donde se aplican los filtros de permisos.',\n    'recycle_bin_deleted_item' => 'Elemento Eliminado',\n    'recycle_bin_deleted_parent' => 'Padre',\n    'recycle_bin_deleted_by' => 'Eliminado por',\n    'recycle_bin_deleted_at' => 'Fecha de eliminación',\n    'recycle_bin_permanently_delete' => 'Eliminar permanentemente',\n    'recycle_bin_restore' => 'Restaurar',\n    'recycle_bin_contents_empty' => 'La papelera de reciclaje está vacía',\n    'recycle_bin_empty' => 'Vaciar Papelera de reciclaje',\n    'recycle_bin_empty_confirm' => 'Esto destruirá permanentemente todos los elementos de la papelera de reciclaje, incluyendo el contenido existente en cada elemento. ¿Está seguro de que desea vaciar la papelera de reciclaje?',\n    'recycle_bin_destroy_confirm' => 'Esta acción eliminará permanentemente este elemento del sistema, junto con los elementos secundarios listados a continuación, y no podrá restaurar este contenido de nuevo. ¿Está seguro de que desea eliminar permanentemente este elemento?',\n    'recycle_bin_destroy_list' => 'Elementos a destruir',\n    'recycle_bin_restore_list' => 'Elementos a restaurar',\n    'recycle_bin_restore_confirm' => 'Esta acción restaurará el elemento eliminado, incluyendo cualquier elemento secundario, a su ubicación original. Si la ubicación original ha sido eliminada, y ahora está en la papelera de reciclaje, el elemento padre también tendrá que ser restaurado.',\n    'recycle_bin_restore_deleted_parent' => 'El padre de este elemento también ha sido eliminado. Estos permanecerán eliminados hasta que el padre también sea restaurado.',\n    'recycle_bin_restore_parent' => 'Restaurar Padre',\n    'recycle_bin_destroy_notification' => 'Eliminados :count elementos de la papelera de reciclaje.',\n    'recycle_bin_restore_notification' => 'Restaurados :count elementos desde la papelera de reciclaje.',\n\n    // Audit Log\n    'audit' => 'Registro de Auditoría',\n    'audit_desc' => 'Este registro de auditoría muestra una lista de actividades rastreadas en el sistema. Esta lista no tiene filtrado a diferencia de listas de actividad similares en el sistema en los que se aplican filtros de permisos.',\n    'audit_event_filter' => 'Filtro de Eventos',\n    'audit_event_filter_no_filter' => 'Sin Filtro',\n    'audit_deleted_item' => 'Elemento borrado',\n    'audit_deleted_item_name' => 'Nombre: :name',\n    'audit_table_user' => 'Usuario',\n    'audit_table_event' => 'Evento',\n    'audit_table_related' => 'Elemento o detalle relacionados',\n    'audit_table_ip' => 'Dirección IP',\n    'audit_table_date' => 'Fecha de la Actividad',\n    'audit_date_from' => 'Inicio del Rango de Fecha',\n    'audit_date_to' => 'Final del Rango de Fecha',\n\n    // Role Settings\n    'roles' => 'Roles',\n    'role_user_roles' => 'Roles de usuario',\n    'roles_index_desc' => 'Los roles se utilizan para agrupar usuarios y proporcionar permisos del sistema a sus miembros. Cuando un usuario es miembro de múltiples roles los privilegios otorgados se acumularán y el usuario heredará todas las habilidades.',\n    'roles_x_users_assigned' => ':count usuario asignado|:count usuarios asignados',\n    'roles_x_permissions_provided' => ':count permiso |:count permisos',\n    'roles_assigned_users' => 'Usuarios Asignados',\n    'roles_permissions_provided' => 'Permisos Proporcionados',\n    'role_create' => 'Crear nuevo rol',\n    'role_delete' => 'Borrar rol',\n    'role_delete_confirm' => 'Se borrará el rol con nombre  \\':roleName\\'.',\n    'role_delete_users_assigned' => 'Este rol tiene :userCount usuarios asignados. Si ud. quisiera migrar los usuarios de este rol, seleccione un nuevo rol a continuación.',\n    'role_delete_no_migration' => \"No migrar usuarios\",\n    'role_delete_sure' => '¿Está seguro que desea borrar este rol?',\n    'role_edit' => 'Editar rol',\n    'role_details' => 'Detalles de rol',\n    'role_name' => 'Nombre de rol',\n    'role_desc' => 'Descripción corta de rol',\n    'role_mfa_enforced' => 'Requiere autenticación de múltiples factores',\n    'role_external_auth_id' => 'IDs de Autenticación Externa',\n    'role_system' => 'Permisos de sistema',\n    'role_manage_users' => 'Gestionar usuarios',\n    'role_manage_roles' => 'Gestionar roles y permisos de roles',\n    'role_manage_entity_permissions' => 'Gestionar todos los permisos de libros, capítulos y páginas',\n    'role_manage_own_entity_permissions' => 'Gestionar permisos en libro\n    s propios, capítulos y páginas',\n    'role_manage_page_templates' => 'Gestionar las plantillas de páginas',\n    'role_access_api' => 'API de sistema de acceso',\n    'role_manage_settings' => 'Gestionar ajustes de activos',\n    'role_export_content' => 'Exportar contenido',\n    'role_import_content' => 'Importar contenido',\n    'role_editor_change' => 'Cambiar editor de página',\n    'role_notifications' => 'Recibir y gestionar notificaciones',\n    'role_permission_note_users_and_roles' => 'Estos permisos proporcionarán también visibilidad y búsqueda de usuarios y roles en el sistema.',\n    'role_asset' => 'Permisos de activos',\n    'roles_system_warning' => 'Tenga en cuenta que el acceso a cualquiera de los tres permisos anteriores puede permitir a un usuario modificar sus propios privilegios o los privilegios de otros usuarios en el sistema. Asignar roles con estos permisos sólo a usuarios de comfianza.',\n    'role_asset_desc' => 'Estos permisos controlan el acceso por defecto a los activos del sistema. Permisos definidos en Libros, Capítulos y Páginas ignorarán estos permisos.',\n    'role_asset_admins' => 'Los administradores reciben automáticamente acceso a todo el contenido pero estas opciones pueden mostrar u ocultar opciones de UI.',\n    'role_asset_image_view_note' => 'Esto se refiere a la visibilidad dentro del gestor de imágenes. El acceso real a los archivos de imágenes subidos, dependerá de la opción de almacenamiento de imágenes del sistema.',\n    'role_asset_users_note' => 'Estos permisos proporcionarán también visibilidad y búsqueda de usuarios en el sistema.',\n    'role_all' => 'Todo',\n    'role_own' => 'Propio',\n    'role_controlled_by_asset' => 'Controlado por el activo al que ha sido subido',\n    'role_save' => 'Guardar rol',\n    'role_users' => 'Usuarios en este rol',\n    'role_users_none' => 'No hay usuarios asignados a este rol',\n\n    // Users\n    'users' => 'Usuarios',\n    'users_index_desc' => 'Crear y administrar cuentas de usuario individuales dentro del sistema. Las cuentas de usuario se utilizan para el inicio de sesión y atribución de contenido y actividad. Los permisos de acceso se basan principalmente en roles, pero la propiedad del contenido del usuario, entre otros factores, también puede afectar a los permisos y el acceso.',\n    'user_profile' => 'Perfil de usuario',\n    'users_add_new' => 'Agregar nuevo usuario',\n    'users_search' => 'Buscar usuarios',\n    'users_latest_activity' => 'Actividad Reciente',\n    'users_details' => 'Detalles del usuario',\n    'users_details_desc' => 'Asigne un nombre de visualización y una dirección de correo electrónico para este usuario. La dirección de correo electrónico se usará pra ingresar a la aplicación.',\n    'users_details_desc_no_email' => 'Asigne un nombre de visualización a este usuario para que los demás puedan reconocerlo.',\n    'users_role' => 'Roles de usuario',\n    'users_role_desc' => 'Selecciona los roles a los que será asignado este usuario. Si se asignan varios roles los permisos se acumularán y recibirá todas las habilidades de los roles asignados.',\n    'users_password' => 'Contraseña de Usuario',\n    'users_password_desc' => 'Establezca una contraseña para iniciar sesión en la aplicación. Debe tener al menos 8 caracteres.',\n    'users_send_invite_text' => 'Puede optar por enviar a este usuario un correo electrónico de invitación que les permita establecer su propia contraseña; de lo contrario, puede establecerla contraseña usted mismo.',\n    'users_send_invite_option' => 'Enviar correo electrónico de invitación al usuario.',\n    'users_external_auth_id' => 'ID externo de autenticación',\n    'users_external_auth_id_desc' => 'Cuando se usa un sistema de autenticación externa (como SAML2, OIDC o LDAP) este es el ID que vincula este usuario de BookStack a la cuenta del sistema de autenticación. Puede ignorar este campo si utiliza la autenticación por defecto basada en correo electrónico.',\n    'users_password_warning' => 'Solo complete lo siguiente si desea cambiar la contraseña para este usuario.',\n    'users_system_public' => 'Este usuario representa cualquier usuario invitado que visita la aplicación. No puede utilizarse para hacer login sino que es asignado automáticamente.',\n    'users_delete' => 'Borrar usuario',\n    'users_delete_named' => 'Borrar usuario :userName',\n    'users_delete_warning' => 'Se borrará completamente el usuario con el nombre \\':userName\\' del sistema.',\n    'users_delete_confirm' => '¿Está seguro que desea borrar este usuario?',\n    'users_migrate_ownership' => 'Cambiar Propietario',\n    'users_migrate_ownership_desc' => 'Seleccione un usuario aquí si desea que otro usuario se convierta en el dueño de todos los elementos que actualmente son propiedad de este usuario.',\n    'users_none_selected' => 'No hay usuario seleccionado',\n    'users_edit' => 'Editar Usuario',\n    'users_edit_profile' => 'Editar perfil',\n    'users_avatar' => 'Avatar del usuario',\n    'users_avatar_desc' => 'Esta imagen debe ser de aproximadamente 256px por lado.',\n    'users_preferred_language' => 'Lenguaje preferido',\n    'users_preferred_language_desc' => 'Esta opción cambiará el idioma de la interfaz de usuario en la aplicación. No afectará al contenido creado por los usuarios.',\n    'users_social_accounts' => 'Cuentas sociales',\n    'users_social_accounts_desc' => 'Vea el estado de las cuentas sociales conectadas para este usuario. Las cuentas sociales se pueden usar además del sistema de autenticación principal para el acceso al sistema.',\n    'users_social_accounts_info' => 'Aquí puede conectar sus otras cuentas para un acceso rápido y más fácil. Desconectando una cuenta aquí no revoca accesos ya autorizados. Revoque el acceso desde los ajustes de perfil en la cuenta social conectada.',\n    'users_social_connect' => 'Conectar cuenta',\n    'users_social_disconnect' => 'Desconectar cuenta',\n    'users_social_status_connected' => 'Conectado',\n    'users_social_status_disconnected' => 'Desconectado',\n    'users_social_connected' => 'La cuenta :socialAccount ha sido exitosamente añadida a su perfil.',\n    'users_social_disconnected' => 'La cuenta :socialAccount ha sido desconectada exitosamente de su perfil.',\n    'users_api_tokens' => 'Tokens API',\n    'users_api_tokens_desc' => 'Cree y administre los tokens de acceso utilizados para autenticarse con la API REST de BookStack. Los permisos para la API se gestionan a través del usuario al que pertenece el token.',\n    'users_api_tokens_none' => 'No se han creado tokens API para este usuario',\n    'users_api_tokens_create' => 'Crear token',\n    'users_api_tokens_expires' => 'Expira',\n    'users_api_tokens_docs' => 'Documentación API',\n    'users_mfa' => 'Autenticación de múltiples factores',\n    'users_mfa_desc' => 'Configure la autenticación de múltiples factores como una capa extra de seguridad para su cuenta de usuario.',\n    'users_mfa_x_methods' => ':count método configurado|:count métodos configurados',\n    'users_mfa_configure' => 'Configurar Métodos',\n\n    // API Tokens\n    'user_api_token_create' => 'Crear token API',\n    'user_api_token_name' => 'Nombre',\n    'user_api_token_name_desc' => 'Dale a tu token un nombre legible como un recordatorio futuro de su propósito.',\n    'user_api_token_expiry' => 'Fecha de expiración',\n    'user_api_token_expiry_desc' => 'Establece una fecha en la que este token expira. Después de esta fecha, las solicitudes realizadas usando este token ya no funcionarán. Dejar este campo en blanco fijará un vencimiento de 100 años en el futuro.',\n    'user_api_token_create_secret_message' => 'Luego de crear este token, inmediatamente se generará y mostrará el \"Token ID\" y el \"Token Secret\" correspondientes. El \"Token Secret\" se mostrará por única vez, asegúrese de copiar el valor a un lugar seguro antes de continuar.',\n    'user_api_token' => 'Token API',\n    'user_api_token_id' => 'Token ID',\n    'user_api_token_id_desc' => 'Este es un identificador no editable generado por el sistema y único para este token que necesitará ser proporcionado en solicitudes de API.',\n    'user_api_token_secret' => 'Clave de Token',\n    'user_api_token_secret_desc' => 'Esta es una clave no editable generada por el sistema que necesitará ser proporcionada en solicitudes de API. Solo se monstraré esta vez así que guarde su valor en un lugar seguro.',\n    'user_api_token_created' => 'Token creado :timeAgo',\n    'user_api_token_updated' => 'Token actualizado :timeAgo',\n    'user_api_token_delete' => 'Borrar token',\n    'user_api_token_delete_warning' => 'Esto eliminará completamente este token API con el nombre \\':tokenName\\' del sistema.',\n    'user_api_token_delete_confirm' => '¿Está seguro de que desea borrar este API token?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Los Webhooks son una forma de enviar datos a URLs externas cuando ciertas acciones y eventos ocurren dentro del sistema, lo que permite la integración basada en eventos con plataformas externas como mensajería o sistemas de notificación.',\n    'webhooks_x_trigger_events' => ':count evento disparador|:count eventos disparadores',\n    'webhooks_create' => 'Crear nuevo Webhook',\n    'webhooks_none_created' => 'No hay webhooks creados.',\n    'webhooks_edit' => 'Editar Webhook',\n    'webhooks_save' => 'Guardar Webhook',\n    'webhooks_details' => 'Detalles del Webhook',\n    'webhooks_details_desc' => 'Proporcione un nombre y un punto final de POST como destino para enviar los datos del webhook.',\n    'webhooks_events' => 'Eventos del Webhook',\n    'webhooks_events_desc' => 'Seleccione todos los eventos que deberían activar este webhook.',\n    'webhooks_events_warning' => 'Tenga en cuenta que estos eventos se activarán para todos los eventos seleccionados, incluso si se aplican permisos personalizados. Asegúrese de que el uso de este webhook no exponga contenido confidencial.',\n    'webhooks_events_all' => 'Todos los eventos del sistema',\n    'webhooks_name' => 'Nombre del Webhook',\n    'webhooks_timeout' => 'Tiempo de Espera de Solicitud del Webhook (Segundos)',\n    'webhooks_endpoint' => 'Punto final del Webhook',\n    'webhooks_active' => 'Webhook Activo',\n    'webhook_events_table_header' => 'Eventos',\n    'webhooks_delete' => 'Eliminar Webhook',\n    'webhooks_delete_warning' => 'Esto eliminará completamente del sistema este webhook con el nombre \\':webhookName\\'.',\n    'webhooks_delete_confirm' => '¿Está seguro que quiere eliminar este webhook?',\n    'webhooks_format_example' => 'Ejemplo de Formato de Webhook',\n    'webhooks_format_example_desc' => 'Los datos del Webhook, en formato JSON, se envían como una solicitud POST al punto final siguiendo el formato mostrado a continuación. Las propiedades \"related_item\" y \"url\" son opcionales y dependerán del tipo de evento activado.',\n    'webhooks_status' => 'Estado del Webhook',\n    'webhooks_last_called' => 'Última Ejecución:',\n    'webhooks_last_errored' => 'Último error:',\n    'webhooks_last_error_message' => 'Último mensaje de error:',\n\n    // Licensing\n    'licenses' => 'Licencias',\n    'licenses_desc' => 'Esta página detalla información sobre la licencia de BookStack además de los proyectos y bibliotecas que se utilizan en BookStack. Muchos proyectos enumerados aquí pueden ser utilizados solo en un contexto de desarrollo.',\n    'licenses_bookstack' => 'Licencia BookStack',\n    'licenses_php' => 'Licencias de Bibliotecas PHP',\n    'licenses_js' => 'Licencias de Bibliotecas JavaScript',\n    'licenses_other' => 'Otras Licencias',\n    'license_details' => 'Detalles de la licencia',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'Inglés',\n        'ar' => 'Árabe',\n        'bg' => 'Búlgaro',\n        'bs' => 'Bosnio',\n        'ca' => 'Català',\n        'cs' => 'Checo',\n        'cy' => 'Cymraeg',\n        'da' => 'Danés',\n        'de' => 'Alemán (informal)',\n        'de_informal' => 'Alemán (formal)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Francés',\n        'he' => 'עברית',\n        'hr' => 'Croata',\n        'hu' => 'Húngaro',\n        'id' => 'Indonesio',\n        'it' => 'Italiano',\n        'ja' => 'Japonés',\n        'ko' => 'Coreano',\n        'lt' => 'Lituano',\n        'lv' => 'Letón',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Holanda',\n        'pl' => 'Polaco',\n        'pt' => 'Portugués',\n        'pt_BR' => 'Portugués brasileño',\n        'ro' => 'Română',\n        'ru' => 'Ruso',\n        'sk' => 'Eslovaco',\n        'sl' => 'Esloveno',\n        'sv' => 'Sueco',\n        'tr' => 'Turco',\n        'uk' => 'Ucraniano',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Vietnamita',\n        'zh_CN' => 'Chino mandarín',\n        'zh_TW' => 'Chino tradicional',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/es_AR/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => 'El :attribute debe ser aceptado.',\n    'active_url'           => 'El :attribute no es una URl válida.',\n    'after'                => 'El :attribute debe ser una fecha posterior :date.',\n    'alpha'                => 'El :attribute solo puede contener letras.',\n    'alpha_dash'           => 'El :attribute solo puede contener letras, números y guiones.',\n    'alpha_num'            => 'El :attribute solo puede contener letras y número.',\n    'array'                => 'El :attribute debe de ser un array.',\n    'backup_codes'         => 'El código suministrado no es válido o ya fue utilizado.',\n    'before'               => 'El :attribute debe ser una fecha anterior a  :date.',\n    'between'              => [\n        'numeric' => 'El :attribute debe estar entre :min y :max.',\n        'file'    => 'El :attribute debe estar entre :min y :max kilobytes.',\n        'string'  => 'El :attribute debe estar entre :min y :max carácteres.',\n        'array'   => 'El :attribute debe estar entre :min y :max items.',\n    ],\n    'boolean'              => 'El campo :attribute debe ser true o false.',\n    'confirmed'            => 'La confirmación de :attribute no concuerda.',\n    'date'                 => 'El :attribute no es una fecha válida.',\n    'date_format'          => 'El :attribute no coincide con el formato :format.',\n    'different'            => ':attribute y :other deben ser diferentes.',\n    'digits'               => ':attribute debe ser de :digits dígitos.',\n    'digits_between'       => ':attribute debe ser un valor entre :min y :max dígios.',\n    'email'                => ':attribute debe ser una dirección álida.',\n    'ends_with' => 'El :attribute debe terminar con uno de los siguientes: :values',\n    'file'                 => 'El :attribute debe ser proporcionado como un archivo válido.',\n    'filled'               => 'El campo :attribute es requerido.',\n    'gt'                   => [\n        'numeric' => 'El :attribute debe ser mayor que :value.',\n        'file'    => 'El :attribute debe ser mayor que :value kilobytes.',\n        'string'  => 'El :attribute debe ser mayor que :value caracteres.',\n        'array'   => 'El :attribute debe tener más de :value objetos.',\n    ],\n    'gte'                  => [\n        'numeric' => 'El :attribute debe ser mayor o igual a :value.',\n        'file'    => 'El :attribute debe ser mayor o igual a :value kilobytes.',\n        'string'  => 'El :attribute debe ser mayor o igual a :value caracteres.',\n        'array'   => 'El :attribute debe tener :value objetos o más.',\n    ],\n    'exists'               => 'El :attribute seleccionado es inválido.',\n    'image'                => 'El :attribute debe ser una imagen.',\n    'image_extension'      => 'El :attribute debe tener una extensión de imagen válida y soportada.',\n    'in'                   => 'El selected :attribute es inválio.',\n    'integer'              => 'El :attribute debe ser un entero.',\n    'ip'                   => 'El :attribute debe ser una dirección IP álida.',\n    'ipv4'                 => 'El :attribute debe ser una dirección IPv4 válida.',\n    'ipv6'                 => 'El :attribute debe ser una dirección IPv6 válida.',\n    'json'                 => 'El :attribute debe ser una cadena JSON válida.',\n    'lt'                   => [\n        'numeric' => 'El :attribute debe ser menor que :value.',\n        'file'    => 'El :attribute debe ser menor que :value kilobytes.',\n        'string'  => 'El :attribute debe ser menor que :value caracteres.',\n        'array'   => 'El :attribute debe tener menos de :value objetos.',\n    ],\n    'lte'                  => [\n        'numeric' => 'El :attribute debe ser menor o igual a :value.',\n        'file'    => 'El :attribute debe ser menor o igual a :value kilobytes.',\n        'string'  => 'El :attribute debe ser menor o igual a :value caracteres.',\n        'array'   => 'El :attribute no debe tener más de :value objetos.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute no puede ser mayor que :max.',\n        'file'    => ':attribute no puede ser mayor que :max kilobytes.',\n        'string'  => ':attribute no puede ser mayor que :max carácteres.',\n        'array'   => ':attribute no puede contener más de :max items.',\n    ],\n    'mimes'                => ':attribute debe ser un fichero de tipo: :values.',\n    'min'                  => [\n        'numeric' => ':attribute debe ser al menos de :min.',\n        'file'    => ':attribute debe ser al menos :min kilobytes.',\n        'string'  => ':attribute debe ser al menos :min caracteres.',\n        'array'   => ':attribute debe tener como mínimo :min items.',\n    ],\n    'not_in'               => ':attribute seleccionado es inválido.',\n    'not_regex'            => 'El formato de :attribute es inválido.',\n    'numeric'              => ':attribute debe ser numérico.',\n    'regex'                => ':attribute con formato inválido',\n    'required'             => ':attribute es requerido.',\n    'required_if'          => ':attribute es requerido cuando :other vale :value.',\n    'required_with'        => 'El campo :attribute es requerido cuando se encuentre entre los valores :values.',\n    'required_with_all'    => 'El campo :attribute es requerido cuando los valores sean :values.',\n    'required_without'     => ':attribute es requerido cuando no se encuentre entre los valores :values.',\n    'required_without_all' => ':attribute es requerido cuando ninguno de los valores :values están presentes.',\n    'same'                 => ':attribute y :other deben coincidir.',\n    'safe_url'             => 'El enlace provisto puede ser inseguro.',\n    'size'                 => [\n        'numeric' => ':attribute debe ser :size.',\n        'file'    => ':attribute debe ser :size kilobytes.',\n        'string'  => ':attribute debe ser :size caracteres.',\n        'array'   => ':attribute debe contener :size items.',\n    ],\n    'string'               => 'El atributo :attribute debe ser una cadena.',\n    'timezone'             => 'El atributo :attribute debe ser una zona válida.',\n    'totp'                 => 'El código suministrado no es válido o ya expiró.',\n    'unique'               => 'El atributo :attribute ya ha sido tomado.',\n    'url'                  => 'El atributo :attribute tiene un formato inválido.',\n    'uploaded'             => 'El archivo no se pudo subir. Puede ser que el servidor no acepte archivos de este tamaño.',\n\n    'zip_file' => 'El :attribute necesita hacer referencia a un archivo dentro del ZIP.',\n    'zip_file_size' => 'El archivo :attribute no debe exceder :size MB.',\n    'zip_file_mime' => 'El :attribute necesita hacer referencia a un archivo de tipo :validTypes, encontrado :foundType.',\n    'zip_model_expected' => 'Se esperaba un objeto de datos, pero se encontró \":type\".',\n    'zip_unique' => 'El :attribute debe ser único para el tipo de objeto dentro del ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Confirmación de Password requerida',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/et/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'lisas lehe',\n    'page_create_notification'    => 'Leht on lisatud',\n    'page_update'                 => 'muutis lehte',\n    'page_update_notification'    => 'Leht on muudetud',\n    'page_delete'                 => 'kustutas lehe',\n    'page_delete_notification'    => 'Leht on kustutatud',\n    'page_restore'                => 'taastas lehe',\n    'page_restore_notification'   => 'Leht on taastatud',\n    'page_move'                   => 'liigutas lehte',\n    'page_move_notification'      => 'Leht on liigutatud',\n\n    // Chapters\n    'chapter_create'              => 'lisas peatüki',\n    'chapter_create_notification' => 'Peatükk on lisatud',\n    'chapter_update'              => 'muutis peatükki',\n    'chapter_update_notification' => 'Peatükk on muudetud',\n    'chapter_delete'              => 'kustutas peatüki',\n    'chapter_delete_notification' => 'Peatükk on kustutatud',\n    'chapter_move'                => 'liigutas peatükki',\n    'chapter_move_notification' => 'Peatükk on liigutatud',\n\n    // Books\n    'book_create'                 => 'lisas raamatu',\n    'book_create_notification'    => 'Raamat on lisatud',\n    'book_create_from_chapter'              => 'muutis peatüki raamatuks',\n    'book_create_from_chapter_notification' => 'Peatükk on muudetud raamatuks',\n    'book_update'                 => 'muutis raamatut',\n    'book_update_notification'    => 'Raamat on muudetud',\n    'book_delete'                 => 'kustutas raamatu',\n    'book_delete_notification'    => 'Raamat on kustutatud',\n    'book_sort'                   => 'sorteeris raamatut',\n    'book_sort_notification'      => 'Raamat on sorteeritud',\n\n    // Bookshelves\n    'bookshelf_create'            => 'lisas riiuli',\n    'bookshelf_create_notification'    => 'Riiul on lisatud',\n    'bookshelf_create_from_book'    => 'muutis raamatu riiuliks',\n    'bookshelf_create_from_book_notification'    => 'Raamat on muudetud riiuliks',\n    'bookshelf_update'                 => 'muutis riiulit',\n    'bookshelf_update_notification'    => 'Riiul on muudetud',\n    'bookshelf_delete'                 => 'kustutas riiuli',\n    'bookshelf_delete_notification'    => 'Riiul on kustutatud',\n\n    // Revisions\n    'revision_restore' => 'taastas redaktsiooni',\n    'revision_delete' => 'kustutas redaktsiooni',\n    'revision_delete_notification' => 'Redaktsioon on kustutatud',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" lisati su lemmikute hulka',\n    'favourite_remove_notification' => '\":name\" eemaldati su lemmikute hulgast',\n\n    // Watching\n    'watch_update_level_notification' => 'Jälgimise eelistused edukalt salvestatud',\n\n    // Auth\n    'auth_login' => 'logis sisse',\n    'auth_register' => 'registreerus uue kasutajana',\n    'auth_password_reset_request' => 'soovis parooli lähtestamist',\n    'auth_password_reset_update' => 'lähtestas kasutaja parooli',\n    'mfa_setup_method' => 'seadistas mitmeastmelise autentimise meetodi',\n    'mfa_setup_method_notification' => 'Mitmeastmeline autentimine seadistatud',\n    'mfa_remove_method' => 'eemaldas mitmeastmelise autentimise meetodi',\n    'mfa_remove_method_notification' => 'Mitmeastmeline autentimine eemaldatud',\n\n    // Settings\n    'settings_update' => 'uuendas seadeid',\n    'settings_update_notification' => 'Seaded uuendatud',\n    'maintenance_action_run' => 'käivitas hooldustegevuse',\n\n    // Webhooks\n    'webhook_create' => 'lisas veebihaagi',\n    'webhook_create_notification' => 'Veebihaak on lisatud',\n    'webhook_update' => 'muutis veebihaaki',\n    'webhook_update_notification' => 'Veebihaak on muudetud',\n    'webhook_delete' => 'kustutas veebihaagi',\n    'webhook_delete_notification' => 'Veebihaak on kustutatud',\n\n    // Imports\n    'import_create' => 'lisas impordi',\n    'import_create_notification' => 'Import on üles laaditud',\n    'import_run' => 'muutis importi',\n    'import_run_notification' => 'Sisu on imporditud',\n    'import_delete' => 'kustutas impordi',\n    'import_delete_notification' => 'Import on kustutatud',\n\n    // Users\n    'user_create' => 'lisas kasutaja',\n    'user_create_notification' => 'Kasutaja on lisatud',\n    'user_update' => 'muutis kasutajat',\n    'user_update_notification' => 'Kasutaja on muudetud',\n    'user_delete' => 'kustutas kasutaja',\n    'user_delete_notification' => 'Kasutaja on kustutatud',\n\n    // API Tokens\n    'api_token_create' => 'lisas API tunnuse',\n    'api_token_create_notification' => 'API tunnus on lisatud',\n    'api_token_update' => 'muutis API tunnust',\n    'api_token_update_notification' => 'API tunnus on muudetud',\n    'api_token_delete' => 'kustutas API tunnuse',\n    'api_token_delete_notification' => 'API tunnus on kustutatud',\n\n    // Roles\n    'role_create' => 'lisas rolli',\n    'role_create_notification' => 'Roll on lisatud',\n    'role_update' => 'muutis rolli',\n    'role_update_notification' => 'Roll on muudetud',\n    'role_delete' => 'kustutas rolli',\n    'role_delete_notification' => 'Roll on kustutatud',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'tühjendas prügikasti',\n    'recycle_bin_restore' => 'taastas prügikastist',\n    'recycle_bin_destroy' => 'eemaldas prügikastist',\n\n    // Comments\n    'commented_on'                => 'kommenteeris lehte',\n    'comment_create'              => 'lisas kommentaari',\n    'comment_update'              => 'muutis kommentaari',\n    'comment_delete'              => 'kustutas kommentaari',\n\n    // Sort Rules\n    'sort_rule_create' => 'lisas sorteerimisreegli',\n    'sort_rule_create_notification' => 'Sorteerimisreegel on lisatud',\n    'sort_rule_update' => 'muutis sorteerimisreeglit',\n    'sort_rule_update_notification' => 'Sorteerimisreegel on muudetud',\n    'sort_rule_delete' => 'kustutas sorteerimisreegli',\n    'sort_rule_delete_notification' => 'Sorteerimisreegel on kustutatud',\n\n    // Other\n    'permissions_update'          => 'muutis õiguseid',\n];\n"
  },
  {
    "path": "lang/et/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Kasutajanimi ja parool ei klapi.',\n    'throttle' => 'Liiga palju sisselogimiskatseid. Proovi uuesti :seconds sekundi pärast.',\n\n    // Login & Register\n    'sign_up' => 'Registreeru',\n    'log_in' => 'Logi sisse',\n    'log_in_with' => 'Logi sisse :socialDriver abil',\n    'sign_up_with' => 'Registreeru :socialDriver abil',\n    'logout' => 'Logi välja',\n\n    'name' => 'Nimi',\n    'username' => 'Kasutajanimi',\n    'email' => 'E-post',\n    'password' => 'Parool',\n    'password_confirm' => 'Kinnita parool',\n    'password_hint' => 'Peab olema vähemalt 8 tähemärki pikk',\n    'forgot_password' => 'Unustasid parooli?',\n    'remember_me' => 'Jäta mind meelde',\n    'ldap_email_hint' => 'Sisesta kasutajakonto e-posti aadress.',\n    'create_account' => 'Loo konto',\n    'already_have_account' => 'Kasutajakonto juba olemas?',\n    'dont_have_account' => 'Sul ei ole veel kontot?',\n    'social_login' => 'Sisene läbi sotsiaalmeedia',\n    'social_registration' => 'Registreeru läbi sotsiaalmeedia',\n    'social_registration_text' => 'Registreeru ja logi sisse välise teenuse kaudu.',\n\n    'register_thanks' => 'Aitäh, et registreerusid!',\n    'register_confirm' => 'Vaata oma postkasti ja klõpsa kinnitusnupul, et rakendusele :appName ligi pääseda.',\n    'registrations_disabled' => 'Registreerumine on hetkel keelatud',\n    'registration_email_domain_invalid' => 'Sellel e-posti domeenil ei ole rakendusele ligipääsu',\n    'register_success' => 'Aitäh, et registreerusid! Oled nüüd sisse logitud.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Sisselogimiskatse',\n    'auto_init_starting_desc' => 'Sisselogimise protsessi alustamiseks autentimissüsteemiga ühendumine. Kui 5 sekundi jooksul edasiminekut ei ole, proovi alloleval lingil klikkida.',\n    'auto_init_start_link' => 'Jätka autentimisega',\n\n    // Password Reset\n    'reset_password' => 'Lähtesta parool',\n    'reset_password_send_instructions' => 'Siseta oma e-posti aadress ning sulle saadetakse link parooli lähtestamiseks.',\n    'reset_password_send_button' => 'Saada lähtestamise link',\n    'reset_password_sent' => 'Kui süsteemis leidub e-posti aadress :email, saadetakse sinna link parooli lähtestamiseks.',\n    'reset_password_success' => 'Sinu parool on edukalt lähtestatud.',\n    'email_reset_subject' => 'Lähtesta oma :appName parool',\n    'email_reset_text' => 'Said selle e-kirja, sest meile laekus soov sinu konto parooli lähtestamiseks.',\n    'email_reset_not_requested' => 'Kui sa ei soovinud parooli lähtestada, ei pea sa rohkem midagi tegema.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Kinnita oma :appName konto e-posti aadress',\n    'email_confirm_greeting' => 'Aitäh, et liitusid rakendusega :appName!',\n    'email_confirm_text' => 'Palun kinnita oma e-posti aadress, klõpsates alloleval nupul:',\n    'email_confirm_action' => 'Kinnita e-posti aadress',\n    'email_confirm_send_error' => 'E-posti aadressi kinnitamine on vajalik, aga e-kirja saatmine ebaõnnestus. Võta ühendust administraatoriga.',\n    'email_confirm_success' => 'E-posti aadress on kinnitatud! Nüüd saad selle aadressiga sisse logida.',\n    'email_confirm_resent' => 'Kinnituskiri on saadetud, vaata oma postkasti.',\n    'email_confirm_thanks' => 'Aitäh, et kinnitasid!',\n    'email_confirm_thanks_desc' => 'Palun oota hetke, kuni kinnitust töödeldakse. Kui sind 3 sekundi jooksul ümber ei suunata, klõpsa jätkamiseks allpool \"Jätka\" linki.',\n\n    'email_not_confirmed' => 'E-posti aadress ei ole kinnitatud',\n    'email_not_confirmed_text' => 'Sinu e-posti aadress ei ole veel kinnitatud.',\n    'email_not_confirmed_click_link' => 'Klõpsa lingil e-kirjas, mis saadeti sulle pärast registreerumist.',\n    'email_not_confirmed_resend' => 'Kui sa ei leia e-kirja, siis saad alloleva vormi abil selle uuesti saata.',\n    'email_not_confirmed_resend_button' => 'Saada kinnituskiri uuesti',\n\n    // User Invite\n    'user_invite_email_subject' => 'Sind on kutsutud liituma rakendusega :appName!',\n    'user_invite_email_greeting' => 'Sulle on loodud kasutajakonto rakenduses :appName.',\n    'user_invite_email_text' => 'Vajuta allolevale nupule, et seada parool ja ligipääs saada:',\n    'user_invite_email_action' => 'Sea konto parool',\n    'user_invite_page_welcome' => 'Tere tulemast rakendusse :appName!',\n    'user_invite_page_text' => 'Registreerumise lõpetamiseks ja ligipääsu saamiseks pead seadma parooli, millega edaspidi rakendusse sisse logid.',\n    'user_invite_page_confirm_button' => 'Kinnita parool',\n    'user_invite_success_login' => 'Parool seatud, nüüd on sul selle parooli abil ligipääs rakendusele :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Seadista mitmeastmeline autentimine',\n    'mfa_setup_desc' => 'Seadista mitmeastmeline autentimine, et oma kasutajakonto turvalisust tõsta.',\n    'mfa_setup_configured' => 'Juba seadistatud',\n    'mfa_setup_reconfigure' => 'Seadista ümber',\n    'mfa_setup_remove_confirmation' => 'Kas oled kindel, et soovid selle mitmeastmelise autentimise meetodi eemaldada?',\n    'mfa_setup_action' => 'Seadista',\n    'mfa_backup_codes_usage_limit_warning' => 'Sul on vähem kui 5 varukoodi järel. Genereeri ja hoiusta uus komplekt enne, kui nad otsa saavad, et vältida oma kasutajakontole ligipääsu kaotamist.',\n    'mfa_option_totp_title' => 'Mobiilirakendus',\n    'mfa_option_totp_desc' => 'Mitmeastmelise autentimise kasutamiseks on sul vaja TOTP-toega mobiilirakendust, nagu Google Authenticator, Authy või Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Varukoodid',\n    'mfa_option_backup_codes_desc' => 'Genereerib komplekti ühekordseid tagavarakoode, mille abil saad sisselogimisel oma isikut tuvastada. Hoia neid kindlas ja turvalises kohas.',\n    'mfa_gen_confirm_and_enable' => 'Kinnita ja lülita sisse',\n    'mfa_gen_backup_codes_title' => 'Varukoodide seadistamine',\n    'mfa_gen_backup_codes_desc' => 'Hoiusta allolevad koodid turvalises kohas. Saad neid kasutada sisselogimisel sekundaarse autentimismeetodina.',\n    'mfa_gen_backup_codes_download' => 'Laadi koodid alla',\n    'mfa_gen_backup_codes_usage_warning' => 'Igat koodi saab ainult ühe korra kasutada',\n    'mfa_gen_totp_title' => 'Mobiilirakenduse seadistamine',\n    'mfa_gen_totp_desc' => 'Mitmeastmelise autentimise kasutamiseks on sul vaja TOTP-toega mobiilirakendust, nagu Google Authenticator, Authy või Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Alustamiseks skaneeri allolevat QR-koodi oma eelistatud rakendusega.',\n    'mfa_gen_totp_verify_setup' => 'Kontrolli seadistust',\n    'mfa_gen_totp_verify_setup_desc' => 'Veendu, et kõik toimib korrektselt, sisestades oma rakenduse genereeritud koodi allolevasse tekstikasti:',\n    'mfa_gen_totp_provide_code_here' => 'Sisesta rakenduse genereeritud kood siia',\n    'mfa_verify_access' => 'Kinnita ligipääs',\n    'mfa_verify_access_desc' => 'Sinu konto nõuab ligipääsuks täiendava kinnitusmeetodi abil oma isiku tuvastamist. Jätkamiseks vali üks järgnevatest meetoditest.',\n    'mfa_verify_no_methods' => 'Ühtegi meetodit pole seadistatud',\n    'mfa_verify_no_methods_desc' => 'Sinu kontole pole lisatud ühtegi mitmeastmelise autentimise meetodit. Ligipääsu saamiseks pead seadistama vähemalt ühe meetodi.',\n    'mfa_verify_use_totp' => 'Tuvasta mobiilirakendusega',\n    'mfa_verify_use_backup_codes' => 'Tuvasta varukoodiga',\n    'mfa_verify_backup_code' => 'Varukood',\n    'mfa_verify_backup_code_desc' => 'Sisesta allpool üks oma järelejäänud varukoodidest:',\n    'mfa_verify_backup_code_enter_here' => 'Sisesta varukood siia',\n    'mfa_verify_totp_desc' => 'Sisesta oma mobiilirakenduse poolt genereeritud kood allpool:',\n    'mfa_setup_login_notification' => 'Mitmeastmeline autentimine seadistatud. Logi nüüd uuesti sisse, kasutades seadistatud meetodit.',\n];\n"
  },
  {
    "path": "lang/et/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Tühista',\n    'close' => 'Sulge',\n    'confirm' => 'Kinnita',\n    'back' => 'Tagasi',\n    'save' => 'Salvesta',\n    'continue' => 'Jätka',\n    'select' => 'Vali',\n    'toggle_all' => 'Vaheta kõik',\n    'more' => 'Rohkem',\n\n    // Form Labels\n    'name' => 'Pealkiri',\n    'description' => 'Kirjeldus',\n    'role' => 'Roll',\n    'cover_image' => 'Kaanepilt',\n    'cover_image_description' => 'See pildifail peaks olema umbes 440x250px, ehkki seda skaleeritakse ja lõigatakse vastavalt vajadusele, et see mahuks erinevatesse kasutajaliidestesse, seega tegelikud kuvamõõdud võivad erineda.',\n\n    // Actions\n    'actions' => 'Tegevused',\n    'view' => 'Vaata',\n    'view_all' => 'Vaata kõiki',\n    'new' => 'Uus',\n    'create' => 'Lisa',\n    'update' => 'Uuenda',\n    'edit' => 'Muuda',\n    'archive' => 'Arhiveeri',\n    'unarchive' => 'Taasta arhiivist',\n    'sort' => 'Sorteeri',\n    'move' => 'Liiguta',\n    'copy' => 'Kopeeri',\n    'reply' => 'Vasta',\n    'delete' => 'Kustuta',\n    'delete_confirm' => 'Kinnita kustutamine',\n    'search' => 'Otsi',\n    'search_clear' => 'Tühjenda otsing',\n    'reset' => 'Taasta',\n    'remove' => 'Eemalda',\n    'add' => 'Lisa',\n    'configure' => 'Seadista',\n    'manage' => 'Halda',\n    'fullscreen' => 'Täisekraan',\n    'favourite' => 'Lemmik',\n    'unfavourite' => 'Eemalda lemmik',\n    'next' => 'Järgmine',\n    'previous' => 'Eelmine',\n    'filter_active' => 'Aktiivne filter:',\n    'filter_clear' => 'Tühjenda filter',\n    'download' => 'Laadi alla',\n    'open_in_tab' => 'Ava vahelehel',\n    'open' => 'Ava',\n\n    // Sort Options\n    'sort_options' => 'Sorteerimise valikud',\n    'sort_direction_toggle' => 'Sorteerimise suund',\n    'sort_ascending' => 'Sorteeri kasvavalt',\n    'sort_descending' => 'Sorteeri kahanevalt',\n    'sort_name' => 'Pealkiri',\n    'sort_default' => 'Vaikimisi',\n    'sort_created_at' => 'Loomise aeg',\n    'sort_updated_at' => 'Muutmise aeg',\n\n    // Misc\n    'deleted_user' => 'Kustutatud kasutaja',\n    'no_activity' => 'Pole tegevusi, mida näidata',\n    'no_items' => 'Ühtegi elementi pole',\n    'back_to_top' => 'Tagasi üles',\n    'skip_to_main_content' => 'Otse põhisisu juurde',\n    'toggle_details' => 'Näita detaile',\n    'toggle_thumbnails' => 'Näita eelvaateid',\n    'details' => 'Detailid',\n    'grid_view' => 'Tabelivaade',\n    'list_view' => 'Loendivaade',\n    'default' => 'Vaikimisi',\n    'breadcrumb' => 'Jäljerida',\n    'status' => 'Staatus',\n    'status_active' => 'Aktiivne',\n    'status_inactive' => 'Mitteaktiivne',\n    'never' => 'Mitte kunagi',\n    'none' => 'Puudub',\n\n    // Header\n    'homepage' => 'Avaleht',\n    'header_menu_expand' => 'Laienda päisemenüü',\n    'profile_menu' => 'Profiilimenüü',\n    'view_profile' => 'Vaata profiili',\n    'edit_profile' => 'Muuda profiili',\n    'dark_mode' => 'Tume režiim',\n    'light_mode' => 'Hele režiim',\n    'global_search' => 'Globaalne otsing',\n\n    // Layout tabs\n    'tab_info' => 'Info',\n    'tab_info_label' => 'Sakk: Näita sekundaarset infot',\n    'tab_content' => 'Sisu',\n    'tab_content_label' => 'Sakk: Näita primaarset sisu',\n\n    // Email Content\n    'email_action_help' => 'Kui sul on probleeme \":actionText\" nupu vajutamisega, kopeeri allolev URL oma veebilehitsejasse:',\n    'email_rights' => 'Kõik õigused kaitstud',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Privaatsus',\n    'terms_of_service' => 'Kasutustingimused',\n\n    // OpenSearch\n    'opensearch_description' => 'Otsi :appName',\n];\n"
  },
  {
    "path": "lang/et/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Pildifaili valik',\n    'image_list' => 'Pildifailide nimekiri',\n    'image_details' => 'Pildi andmed',\n    'image_upload' => 'Laadi pilt üles',\n    'image_intro' => 'Siin saad valida ja hallata pilte, mis on eelnevalt süsteemi üles laaditud.',\n    'image_intro_upload' => 'Laadi uus pilt üles pildifaili sellesse aknasse lohistades või ülal \"Laadi pilt üles\" nupu abil.',\n    'image_all' => 'Kõik',\n    'image_all_title' => 'Vaata kõiki pildifaile',\n    'image_book_title' => 'Vaata sellesse raamatusse laaditud pildifaile',\n    'image_page_title' => 'Vaata sellele lehele laaditud pildifaile',\n    'image_search_hint' => 'Otsi pildifaili nime järgi',\n    'image_uploaded' => 'Üles laaditud :uploadedDate',\n    'image_uploaded_by' => 'Lisatud :userName poolt',\n    'image_uploaded_to' => 'Lisatud lehele :pageLink',\n    'image_updated' => 'Lisatud :updateDate',\n    'image_load_more' => 'Lae rohkem',\n    'image_image_name' => 'Pildifaili nimi',\n    'image_delete_used' => 'Seda pildifaili kasutavad järgmised lehed.',\n    'image_delete_confirm_text' => 'Kas oled kindel, et soovid selle pildifaili kustutada?',\n    'image_select_image' => 'Vali pildifail',\n    'image_dropzone' => 'Üleslaadimiseks lohista pildid või klõpsa siin',\n    'image_dropzone_drop' => 'Üleslaadimiseks lohista pildid siia',\n    'images_deleted' => 'Pildifailid kustutatud',\n    'image_preview' => 'Pildi eelvaade',\n    'image_upload_success' => 'Pildifail üles laaditud',\n    'image_update_success' => 'Pildifaili andmed muudetud',\n    'image_delete_success' => 'Pildifail kustutatud',\n    'image_replace' => 'Asenda pilt',\n    'image_replace_success' => 'Pildifail on uuendatud',\n    'image_rebuild_thumbs' => 'Taastekita eelvaated',\n    'image_rebuild_thumbs_success' => 'Pildi eelvaated edukalt taastekitatud!',\n\n    // Code Editor\n    'code_editor' => 'Muuda koodi',\n    'code_language' => 'Koodi keel',\n    'code_content' => 'Koodi sisu',\n    'code_session_history' => 'Sessiooni ajalugu',\n    'code_save' => 'Salvesta kood',\n];\n"
  },
  {
    "path": "lang/et/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Üldine',\n    'advanced' => 'Lisavalikud',\n    'none' => 'Puudub',\n    'cancel' => 'Tühista',\n    'save' => 'Salvesta',\n    'close' => 'Sulge',\n    'apply' => 'Rakenda',\n    'undo' => 'Võta tagasi',\n    'redo' => 'Korda',\n    'left' => 'Vasakul',\n    'center' => 'Keskel',\n    'right' => 'Paremal',\n    'top' => 'Ülal',\n    'middle' => 'Keskel',\n    'bottom' => 'All',\n    'width' => 'Laius',\n    'height' => 'Kõrgus',\n    'More' => 'Rohkem',\n    'select' => 'Vali...',\n\n    // Toolbar\n    'formats' => 'Vormindamine',\n    'header_large' => 'Suur pealkiri',\n    'header_medium' => 'Keskmine pealkiri',\n    'header_small' => 'Väike pealkiri',\n    'header_tiny' => 'Tilluke pealkiri',\n    'paragraph' => 'Paragrahv',\n    'blockquote' => 'Tsitaat',\n    'inline_code' => 'Tekstisisene kood',\n    'callouts' => 'Teated',\n    'callout_information' => 'Info',\n    'callout_success' => 'Edu',\n    'callout_warning' => 'Hoiatus',\n    'callout_danger' => 'Oht',\n    'bold' => 'Rasvane kiri',\n    'italic' => 'Kaldkiri',\n    'underline' => 'Allajoonitud',\n    'strikethrough' => 'Läbikriipsutus',\n    'superscript' => 'Ülaindeks',\n    'subscript' => 'Alaindeks',\n    'text_color' => 'Teksti värv',\n    'highlight_color' => 'Esiletõstuvärv',\n    'custom_color' => 'Kohandatud värv',\n    'remove_color' => 'Eemalda värv',\n    'background_color' => 'Taustavärv',\n    'align_left' => 'Joonda vasakule',\n    'align_center' => 'Joonda keskele',\n    'align_right' => 'Joonda paremale',\n    'align_justify' => 'Rööpjoondus',\n    'list_bullet' => 'Punktloetelu',\n    'list_numbered' => 'Numbriline loetelu',\n    'list_task' => 'Ülesannete nimekiri',\n    'indent_increase' => 'Suurenda taanet',\n    'indent_decrease' => 'Vähenda taanet',\n    'table' => 'Tabel',\n    'insert_image' => 'Sisesta pilt',\n    'insert_image_title' => 'Sisesta/Muuda pilti',\n    'insert_link' => 'Sisesta/muuda linki',\n    'insert_link_title' => 'Sisesta/muuda linki',\n    'insert_horizontal_line' => 'Sisesta vahejoon',\n    'insert_code_block' => 'Sisesta koodiplokk',\n    'edit_code_block' => 'Muuda koodiplokki',\n    'insert_drawing' => 'Sisesta/muuda joonist',\n    'drawing_manager' => 'Jooniste haldur',\n    'insert_media' => 'Sisesta/muuda meediat',\n    'insert_media_title' => 'Sisesta/muuda meediat',\n    'clear_formatting' => 'Eemalda vormindus',\n    'source_code' => 'Lähtekood',\n    'source_code_title' => 'Lähtekood',\n    'fullscreen' => 'Täisekraan',\n    'image_options' => 'Pildi valikud',\n\n    // Tables\n    'table_properties' => 'Tabeli omadused',\n    'table_properties_title' => 'Tabeli omadused',\n    'delete_table' => 'Kustuta tabel',\n    'table_clear_formatting' => 'Eemalda tabeli vormindus',\n    'resize_to_contents' => 'Muuda suurus sisule vastavaks',\n    'row_header' => 'Päiserida',\n    'insert_row_before' => 'Sisesta rida enne',\n    'insert_row_after' => 'Sisesta rida pärast',\n    'delete_row' => 'Kustuta rida',\n    'insert_column_before' => 'Sisesta veerg enne',\n    'insert_column_after' => 'Sisesta veerg pärast',\n    'delete_column' => 'Kustuta veerg',\n    'table_cell' => 'Lahter',\n    'table_row' => 'Rida',\n    'table_column' => 'Veerg',\n    'cell_properties' => 'Lahtri omadused',\n    'cell_properties_title' => 'Lahtri omadused',\n    'cell_type' => 'Lahtri tüüp',\n    'cell_type_cell' => 'Lahter',\n    'cell_scope' => 'Ulatus',\n    'cell_type_header' => 'Päiselahter',\n    'merge_cells' => 'Ühenda lahtrid',\n    'split_cell' => 'Eralda lahtrid',\n    'table_row_group' => 'Rea grupp',\n    'table_column_group' => 'Veeru grupp',\n    'horizontal_align' => 'Horisontaalne joondus',\n    'vertical_align' => 'Vertikaalne joondus',\n    'border_width' => 'Raami laius',\n    'border_style' => 'Raami stiil',\n    'border_color' => 'Raami värv',\n    'row_properties' => 'Rea omadused',\n    'row_properties_title' => 'Rea omadused',\n    'cut_row' => 'Lõika rida',\n    'copy_row' => 'Kopeeri rida',\n    'paste_row_before' => 'Kleebi rida enne',\n    'paste_row_after' => 'Kleebi rida pärast',\n    'row_type' => 'Rea tüüp',\n    'row_type_header' => 'Päis',\n    'row_type_body' => 'Sisu',\n    'row_type_footer' => 'Jalus',\n    'alignment' => 'Joondus',\n    'cut_column' => 'Lõika veerg',\n    'copy_column' => 'Kopeeri veerg',\n    'paste_column_before' => 'Kleebi veerg enne',\n    'paste_column_after' => 'Kleebi veerg pärast',\n    'cell_padding' => 'Lahtrite polsterdus',\n    'cell_spacing' => 'Lahtrite vahe',\n    'caption' => 'Tiitel',\n    'show_caption' => 'Näita tiitlit',\n    'constrain' => 'Piira proportsioone',\n    'cell_border_solid' => 'Pidevjoon',\n    'cell_border_dotted' => 'Punktiirjoon',\n    'cell_border_dashed' => 'Katkendjoon',\n    'cell_border_double' => 'Topeltjoon',\n    'cell_border_groove' => 'Vagu',\n    'cell_border_ridge' => 'Hari',\n    'cell_border_inset' => 'Süvistatud',\n    'cell_border_outset' => 'Tõstetud',\n    'cell_border_none' => 'Puudub',\n    'cell_border_hidden' => 'Peidetud',\n\n    // Images, links, details/summary & embed\n    'source' => 'Allikas',\n    'alt_desc' => 'Alternatiivne kirjeldus',\n    'embed' => 'Manusta',\n    'paste_embed' => 'Sisesta manustamiskood:',\n    'url' => 'URL',\n    'text_to_display' => 'Kuvatav tekst',\n    'title' => 'Pealkiri',\n    'browse_links' => 'Sirve linke',\n    'open_link' => 'Ava link',\n    'open_link_in' => 'Ava link...',\n    'open_link_current' => 'Samas aknas',\n    'open_link_new' => 'Uues aknas',\n    'remove_link' => 'Eemalda link',\n    'insert_collapsible' => 'Lisa kokkupandav plokk',\n    'collapsible_unwrap' => 'Paki lahti',\n    'edit_label' => 'Muuda silti',\n    'toggle_open_closed' => 'Avatud/suletud',\n    'collapsible_edit' => 'Muuda kokkupandavat plokki',\n    'toggle_label' => 'Näita silti',\n\n    // About view\n    'about' => 'Redaktori info',\n    'about_title' => 'Info WYSIWYG redaktori kohta',\n    'editor_license' => 'Redaktori litsents ja autoriõigused',\n    'editor_lexical_license' => 'See redaktor on loodud :lexicalLink põhjal, mis on saadaval MIT litsentsi alusel.',\n    'editor_lexical_license_link' => 'Täielikud litsentsitingimused on leitavad siin.',\n    'editor_tiny_license' => 'See redaktor on loodud :tinyLink abil, mis on saadaval MIT litsentsi alusel.',\n    'editor_tiny_license_link' => 'TinyMCE autoriõigused ja litsents on saadaval siin.',\n    'save_continue' => 'Salvesta leht ja jätka',\n    'callouts_cycle' => '(Vajuta, et tüüpide vahel valida)',\n    'link_selector' => 'Link sisule',\n    'shortcuts' => 'Otseteed',\n    'shortcut' => 'Otsetee',\n    'shortcuts_intro' => 'Redaktoris on saadaval järgmised otseteed:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Kirjeldus',\n];\n"
  },
  {
    "path": "lang/et/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Hiljuti lisatud',\n    'recently_created_pages' => 'Hiljuti lisatud lehed',\n    'recently_updated_pages' => 'Hiljuti muudetud lehed',\n    'recently_created_chapters' => 'Hiljuti lisatud peatükid',\n    'recently_created_books' => 'Hiljuti lisatud raamatud',\n    'recently_created_shelves' => 'Hiljuti lisatud riiulid',\n    'recently_update' => 'Hiljuti muudetud',\n    'recently_viewed' => 'Viimati vaadatud',\n    'recent_activity' => 'Hiljutised tegevused',\n    'create_now' => 'Lisa uus',\n    'revisions' => 'Redaktsioonid',\n    'meta_revision' => 'Redaktsioon #:revisionCount',\n    'meta_created' => 'Lisatud :timeLength',\n    'meta_created_name' => 'Lisatud :timeLength kasutaja :user poolt',\n    'meta_updated' => 'Muudetud :timeLength',\n    'meta_updated_name' => 'Muudetud :timeLength kasutaja :user poolt',\n    'meta_owned_name' => 'Kuulub kasutajale :user',\n    'meta_reference_count' => 'Viidatud :count objekti poolt|Viidatud :count objekti poolt',\n    'entity_select' => 'Objekti valik',\n    'entity_select_lack_permission' => 'Sul pole õiguseid selle objekti valimiseks',\n    'images' => 'Pildid',\n    'my_recent_drafts' => 'Minu hiljutised mustandid',\n    'my_recently_viewed' => 'Minu viimati vaadatud',\n    'my_most_viewed_favourites' => 'Minu enim vaadatud lemmikud',\n    'my_favourites' => 'Minu lemmikud',\n    'no_pages_viewed' => 'Sa pole veel ühtegi lehte vaadanud',\n    'no_pages_recently_created' => 'Hiljuti pole ühtegi lehte lisatud',\n    'no_pages_recently_updated' => 'Hiljuti pole ühtegi lehte muudetud',\n    'export' => 'Ekspordi',\n    'export_html' => 'HTML-fail',\n    'export_pdf' => 'PDF fail',\n    'export_text' => 'Tekstifail',\n    'export_md' => 'Markdown fail',\n    'export_zip' => 'Portatiivne ZIP',\n    'default_template' => 'Vaikimisi lehe mall',\n    'default_template_explain' => 'Vali lehe mall, mida kasutatakse kõigi selle objekti sees loodud lehtede vaikimisi sisuna. Pea meeles, et seda kasutatakse ainult siis, kui lehe loojal on valitud malli vaatamise õigus.',\n    'default_template_select' => 'Vali mall',\n    'import' => 'Import',\n    'import_validate' => 'Valideeri import',\n    'import_desc' => 'Impordi raamatud, peatükid ja lehed portatiivse ZIP-formaadi abil samast või erinevast instantsist. Jätkamiseks vali ZIP-fail. Pärast faili üleslaadimist ja valideerimist saad importimise seadistada ja kinnitada.',\n    'import_zip_select' => 'Vali ZIP-fail üleslaadimiseks',\n    'import_zip_validation_errors' => 'Valitud ZIP-faili valideerimisel tekkisid vead:',\n    'import_pending' => 'Ootel impordid',\n    'import_pending_none' => 'Ühtegi importi pole alustatud.',\n    'import_continue' => 'Jätka importimist',\n    'import_continue_desc' => 'Vaata üle sisu, mida üleslaaditud ZIP-failist importida. Kui oled valmis, käivita import, et sisu süsteemi lisada. Eduka impordi korral kustutatakse üleslaaditud ZIP-fail automaatselt.',\n    'import_details' => 'Importimise andmed',\n    'import_run' => 'Käivita import',\n    'import_size' => ':size Imporditud ZIP-faili maht',\n    'import_uploaded_at' => 'Üleslaaditud :relativeTime',\n    'import_uploaded_by' => 'Üles laadis',\n    'import_location' => 'Importimise asukoht',\n    'import_location_desc' => 'Vali imporditud sisu jaoks sihtkoht. Sul on vaja valitud asukohas sisu lisamiseks vajalikke õiguseid.',\n    'import_delete_confirm' => 'Kas oled kindel, et soovid selle impordi kustutada?',\n    'import_delete_desc' => 'See kustutab üleslaaditud ZIP-faili, ja seda ei saa tagasi võtta.',\n    'import_errors' => 'Importimise vead',\n    'import_errors_desc' => 'Importimisel esinesid järgmised vead:',\n    'breadcrumb_siblings_for_page' => 'Sirvi teisi lehti',\n    'breadcrumb_siblings_for_chapter' => 'Sirvi teisi peatükke',\n    'breadcrumb_siblings_for_book' => 'Sirvi teisi raamatuid',\n    'breadcrumb_siblings_for_bookshelf' => 'Sirvi teisi riiuleid',\n\n    // Permissions and restrictions\n    'permissions' => 'Õigused',\n    'permissions_desc' => 'Sea siin õigused, et kirjutada üle rollide vaikimisi õigused.',\n    'permissions_book_cascade' => 'Raamatutele seatud õigused rakenduvad automaatselt peatükkidele ja lehtedele, kui neile pole seatud oma õiguseid.',\n    'permissions_chapter_cascade' => 'Peatükkidele seatud õigused rakenduvad automaatselt lehtedele, kui neile pole seatud oma õiguseid.',\n    'permissions_save' => 'Salvesta õigused',\n    'permissions_owner' => 'Omanik',\n    'permissions_role_everyone_else' => 'Kõik muud',\n    'permissions_role_everyone_else_desc' => 'Sea õigused kõigile rollidele, mida pole üle kirjutatud.',\n    'permissions_role_override' => 'Kirjuta rolli õigused üle',\n    'permissions_inherit_defaults' => 'Päri vaikimisi seaded',\n\n    // Search\n    'search_results' => 'Otsingutulemused',\n    'search_total_results_found' => 'leitud :count vaste|leitud :count vastet',\n    'search_clear' => 'Tühjenda otsing',\n    'search_no_pages' => 'Otsing ei leidnud ühtegi lehte',\n    'search_for_term' => 'Otsi terminit :term',\n    'search_more' => 'Rohkem tulemusi',\n    'search_advanced' => 'Täpsem otsing',\n    'search_terms' => 'Otsinguterminid',\n    'search_content_type' => 'Sisu tüüp',\n    'search_exact_matches' => 'Täpsed vasted',\n    'search_tags' => 'Sildi otsing',\n    'search_options' => 'Valikud',\n    'search_viewed_by_me' => 'Minu vaadatud',\n    'search_not_viewed_by_me' => 'Minu vaatamata',\n    'search_permissions_set' => 'Õigused seatud',\n    'search_created_by_me' => 'Minu lisatud',\n    'search_updated_by_me' => 'Minu muudetud',\n    'search_owned_by_me' => 'Minu omad',\n    'search_date_options' => 'Kuupäeva valikud',\n    'search_updated_before' => 'Muudetud enne kui',\n    'search_updated_after' => 'Muudetud hiljem kui',\n    'search_created_before' => 'Lisatud enne kui',\n    'search_created_after' => 'Lisatud hiljem kui',\n    'search_set_date' => 'Vali kuupäev',\n    'search_update' => 'Värskenda otsingutulemusi',\n\n    // Shelves\n    'shelf' => 'Riiul',\n    'shelves' => 'Riiulid',\n    'x_shelves' => ':count riiul|:count riiulit',\n    'shelves_empty' => 'Ühtegi riiulit pole lisatud',\n    'shelves_create' => 'Lisa uus riiul',\n    'shelves_popular' => 'Populaarsed riiulid',\n    'shelves_new' => 'Uued riiulid',\n    'shelves_new_action' => 'Uus riiul',\n    'shelves_popular_empty' => 'Siia tulevad kõige populaarsemad riiulid.',\n    'shelves_new_empty' => 'Siia tulevad hiljuti lisatud riiulid.',\n    'shelves_save' => 'Salvesta riiul',\n    'shelves_books' => 'Raamatud sellel riiulil',\n    'shelves_add_books' => 'Lisa sellele riiulile raamatuid',\n    'shelves_drag_books' => 'Lohista raamatuid siia, et neid sellele riiulile lisada',\n    'shelves_empty_contents' => 'Sellel riiulil ei ole ühtegi raamatut',\n    'shelves_edit_and_assign' => 'Muuda riiulit, et siia raamatuid lisada',\n    'shelves_edit_named' => 'Muuda riiulit :name',\n    'shelves_edit' => 'Muuda riiulit',\n    'shelves_delete' => 'Kustuta riiul',\n    'shelves_delete_named' => 'Kustuta riiul :name',\n    'shelves_delete_explain' => \"See kustutab riiuli nimega ':name'. Raamatuid, mis on sellel riiulil, ei kustutata.\",\n    'shelves_delete_confirmation' => 'Kas oled kindel, et soovid selle riiuli kustutada?',\n    'shelves_permissions' => 'Riiuli õigused',\n    'shelves_permissions_updated' => 'Riiuli õigused muudetud',\n    'shelves_permissions_active' => 'Riiuli õigused on aktiivsed',\n    'shelves_permissions_cascade_warning' => 'Riiuli õigused ei rakendu automaatselt sellel olevatele raamatutele, kuna raamat võib olla korraga mitmel riiulil. Alloleva valiku abil saab aga riiuli õigused kopeerida raamatutele.',\n    'shelves_permissions_create' => 'Riiuli lisamise õiguseid kasutatakse ainult alloleva tegevuse kaudu õiguste raamatutele kopeerimiseks. Need ei piira raamatute lisamist.',\n    'shelves_copy_permissions_to_books' => 'Kopeeri õigused raamatutele',\n    'shelves_copy_permissions' => 'Kopeeri õigused',\n    'shelves_copy_permissions_explain' => 'See rakendab riiuli praegused õigused kõigile sellel olevatele raamatutele. Enne jätkamist veendu, et riiuli õiguste muudatused oleks salvestatud.',\n    'shelves_copy_permission_success' => 'Riiuli õigused kopeeritud :count raamatule',\n\n    // Books\n    'book' => 'Raamat',\n    'books' => 'Raamatud',\n    'x_books' => ':count raamat|:count raamatut',\n    'books_empty' => 'Ühtegi raamatut pole lisatud',\n    'books_popular' => 'Populaarsed raamatud',\n    'books_recent' => 'Hiljutised raamatud',\n    'books_new' => 'Uued raamatud',\n    'books_new_action' => 'Uus raamat',\n    'books_popular_empty' => 'Siia tulevad kõige populaarsemad raamatud.',\n    'books_new_empty' => 'Siia tulevad hiljuti lisatud raamatud.',\n    'books_create' => 'Lisa uus raamat',\n    'books_delete' => 'Kustuta raamat',\n    'books_delete_named' => 'Kustuta raamat :bookName',\n    'books_delete_explain' => 'See kustutab raamatu nimega \\':bookName\\'. Kõik lehed ja peatükid kustutatakse samuti.',\n    'books_delete_confirmation' => 'Kas oled kindel, et soovid selle raamatu kustutada?',\n    'books_edit' => 'Muuda raamatut',\n    'books_edit_named' => 'Muuda raamatut :bookName',\n    'books_form_book_name' => 'Raamatu pealkiri',\n    'books_save' => 'Salvesta raamat',\n    'books_permissions' => 'Raamatu õigused',\n    'books_permissions_updated' => 'Raamatu õigused muudetud',\n    'books_empty_contents' => 'Ühtegi lehte ega peatükki pole lisatud.',\n    'books_empty_create_page' => 'Lisa uus leht',\n    'books_empty_sort_current_book' => 'Sorteeri raamat',\n    'books_empty_add_chapter' => 'Lisa uus peatükk',\n    'books_permissions_active' => 'Raamatu õigused on aktiivsed',\n    'books_search_this' => 'Otsi sellest raamatust',\n    'books_navigation' => 'Raamatu sisukord',\n    'books_sort' => 'Sorteeri raamatu sisu',\n    'books_sort_desc' => 'Liiguta raamatu sees peatükke ja lehti, et selle sisu ümber organiseerida. Saad lisada teisi raamatuid, mis võimaldab peatükke ja lehti lihtsasti raamatute vahel liigutada. Lisaks saad määrata automaatse sorteerimise reegli, et selle raamatu sisu muudatuste puhul automaatselt järjestada.',\n    'books_sort_auto_sort' => 'Automaatne sorteerimine',\n    'books_sort_auto_sort_active' => 'Automaatne sorteerimine aktiivne: :sortName',\n    'books_sort_named' => 'Sorteeri raamat :bookName',\n    'books_sort_name' => 'Sorteeri nime järgi',\n    'books_sort_created' => 'Sorteeri loomisaja järgi',\n    'books_sort_updated' => 'Sorteeri muutmisaja järgi',\n    'books_sort_chapters_first' => 'Peatükid eespool',\n    'books_sort_chapters_last' => 'Peatükid tagapool',\n    'books_sort_show_other' => 'Näita teisi raamatuid',\n    'books_sort_save' => 'Salvesta uus järjekord',\n    'books_sort_show_other_desc' => 'Lisa siia teisi raamatuid, et neid järjestamisel kasutada ja võimaldada raamatutevahelist organiseerimist.',\n    'books_sort_move_up' => 'Liiguta üles',\n    'books_sort_move_down' => 'Liiguta alla',\n    'books_sort_move_prev_book' => 'Liiguta eelmisesse raamatusse',\n    'books_sort_move_next_book' => 'Liiguta järgmisesse raamatusse',\n    'books_sort_move_prev_chapter' => 'Liiguta eelmisesse peatükki',\n    'books_sort_move_next_chapter' => 'Liiguta järgmisesse peatükki',\n    'books_sort_move_book_start' => 'Liiguta raamatu algusesse',\n    'books_sort_move_book_end' => 'Liiguta raamatu lõppu',\n    'books_sort_move_before_chapter' => 'Liiguta peatüki ette',\n    'books_sort_move_after_chapter' => 'Liiguta peatüki järele',\n    'books_copy' => 'Kopeeri raamat',\n    'books_copy_success' => 'Raamat on kopeeritud',\n\n    // Chapters\n    'chapter' => 'Peatükk',\n    'chapters' => 'Peatükid',\n    'x_chapters' => ':count peatükk|:count peatükki',\n    'chapters_popular' => 'Populaarsed peatükid',\n    'chapters_new' => 'Uus peatükk',\n    'chapters_create' => 'Lisa uus peatükk',\n    'chapters_delete' => 'Kustuta peatükk',\n    'chapters_delete_named' => 'Kustuta peatükk :chapterName',\n    'chapters_delete_explain' => 'See kustutab peatüki nimega \\':chapterName\\'. Kõik lehed selles peatükis kustutatakse samuti.',\n    'chapters_delete_confirm' => 'Kas oled kindel, et soovid selle peatüki kustutada?',\n    'chapters_edit' => 'Muuda peatükki',\n    'chapters_edit_named' => 'Muuda peatükki :chapterName',\n    'chapters_save' => 'Salvesta peatükk',\n    'chapters_move' => 'Liiguta peatükk',\n    'chapters_move_named' => 'Liiguta peatükk :chapterName',\n    'chapters_copy' => 'Kopeeri peatükk',\n    'chapters_copy_success' => 'Peatükk on kopeeritud',\n    'chapters_permissions' => 'Peatüki õigused',\n    'chapters_empty' => 'Selles peatükis ei ole lehti.',\n    'chapters_permissions_active' => 'Peatüki õigused on aktiivsed',\n    'chapters_permissions_success' => 'Peatüki õigused muudetud',\n    'chapters_search_this' => 'Otsi sellest peatükist',\n    'chapter_sort_book' => 'Sorteeri raamat',\n\n    // Pages\n    'page' => 'Leht',\n    'pages' => 'Lehed',\n    'x_pages' => ':count leht|:count lehte',\n    'pages_popular' => 'Populaarsed lehed',\n    'pages_new' => 'Uus leht',\n    'pages_attachments' => 'Manused',\n    'pages_navigation' => 'Lehe sisukord',\n    'pages_delete' => 'Kustuta leht',\n    'pages_delete_named' => 'Kustuta leht :pageName',\n    'pages_delete_draft_named' => 'Kustuta mustand :pageName',\n    'pages_delete_draft' => 'Kustuta mustand',\n    'pages_delete_success' => 'Leht kustutatud',\n    'pages_delete_draft_success' => 'Mustand kustutatud',\n    'pages_delete_warning_template' => 'See leht on valitud raamatu või peatüki lehe malliks. Pärast selle lehe kustutamist ei ole vastavatel raamatutel või peatükkidel enam vaikimisi lehe malli.',\n    'pages_delete_confirm' => 'Kas oled kindel, et soovid selle lehe kustutada?',\n    'pages_delete_draft_confirm' => 'Kas oled kindel, et soovid selle mustandi kustutada?',\n    'pages_editing_named' => 'Lehe :pageName muutmine',\n    'pages_edit_draft_options' => 'Mustandi valikud',\n    'pages_edit_save_draft' => 'Salvesta mustand',\n    'pages_edit_draft' => 'Muuda mustandit',\n    'pages_editing_draft' => 'Mustandi muutmine',\n    'pages_editing_page' => 'Lehe muutmine',\n    'pages_edit_draft_save_at' => 'Mustand salvestatud ',\n    'pages_edit_delete_draft' => 'Kustuta mustand',\n    'pages_edit_delete_draft_confirm' => 'Kas oled kindel, et soovid mustandi muudatused kustutada? Kõik viimasest salvestamisest saadik tehtud muudatused kaovad ning redaktorisse laetakse viimati salvestatud seis.',\n    'pages_edit_discard_draft' => 'Loobu mustandist',\n    'pages_edit_switch_to_markdown' => 'Kasuta Markdown redaktorit',\n    'pages_edit_switch_to_markdown_clean' => '(Puhas sisu)',\n    'pages_edit_switch_to_markdown_stable' => '(Stabiilne sisu)',\n    'pages_edit_switch_to_wysiwyg' => 'Kasuta WYSIWYG redaktorit',\n    'pages_edit_switch_to_new_wysiwyg' => 'Kasuta uut tekstiredaktorit',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(beetatestimisel)',\n    'pages_edit_set_changelog' => 'Muudatuste logi',\n    'pages_edit_enter_changelog_desc' => 'Sisesta tehtud muudatuste lühikirjeldus',\n    'pages_edit_enter_changelog' => 'Salvesta muudatuste logi',\n    'pages_editor_switch_title' => 'Vaheta redaktorit',\n    'pages_editor_switch_are_you_sure' => 'Kas oled kindel, et soovid selle lehe redaktorit muuta?',\n    'pages_editor_switch_consider_following' => 'Redaktori muutmisel pea meeles järgnevat:',\n    'pages_editor_switch_consideration_a' => 'Pärast salvestamist kasutatakse valitud redaktorit ka tulevikus, sh. olukordades, kus ei pruugi olla võimalik redaktori tüüpi muuta.',\n    'pages_editor_switch_consideration_b' => 'See võib teatud olukordades põhjustada detailide ja süntaksi kaotsiminekut.',\n    'pages_editor_switch_consideration_c' => 'Viimasest salvestamisest saadik tehtud siltide ja muudatuste logi muudatused ei jää alles.',\n    'pages_save' => 'Salvesta leht',\n    'pages_title' => 'Lehe pealkiri',\n    'pages_name' => 'Lehe nimetus',\n    'pages_md_editor' => 'Redaktor',\n    'pages_md_preview' => 'Eelvaade',\n    'pages_md_insert_image' => 'Lisa pilt',\n    'pages_md_insert_link' => 'Lisa viide',\n    'pages_md_insert_drawing' => 'Lisa joonis',\n    'pages_md_show_preview' => 'Näita eelvaadet',\n    'pages_md_sync_scroll' => 'Sünkrooni eelvaate kerimine',\n    'pages_md_plain_editor' => 'Lihttekstiredaktor',\n    'pages_drawing_unsaved' => 'Leiti salvestamata joonis',\n    'pages_drawing_unsaved_confirm' => 'Varasemast ebaõnnestunud salvestuskatsest leiti salvestamata joonis. Kas soovid salvestamata joonise taastada ja selle muutmist jätkata?',\n    'pages_not_in_chapter' => 'Leht ei kuulu peatüki alla',\n    'pages_move' => 'Liiguta leht',\n    'pages_copy' => 'Kopeeri leht',\n    'pages_copy_desination' => 'Kopeerimise sihtpunkt',\n    'pages_copy_success' => 'Leht on kopeeritud',\n    'pages_permissions' => 'Lehe õigused',\n    'pages_permissions_success' => 'Lehe õigused muudetud',\n    'pages_revision' => 'Redaktsioon',\n    'pages_revisions' => 'Lehe redaktsioonid',\n    'pages_revisions_desc' => 'Allpool on toodud kõik selle lehe eelmised redaktsioonid. Kui õigused lubavad, saad sa vanu redaktsioone vaadata, võrrelda ja taastada. Siin ei pruugi kajastuda lehe täielik ajalugu, kuna sõltuvalt süsteemi seadistusest võidakse vanu redaktsioone automaatselt kustutada.',\n    'pages_revisions_named' => 'Lehe :pageName redaktsioonid',\n    'pages_revision_named' => 'Lehe :pageName redaktsioon',\n    'pages_revision_restored_from' => 'Taastatud redaktsioonist #:id; :summary',\n    'pages_revisions_created_by' => 'Autor',\n    'pages_revisions_date' => 'Redaktsiooni aeg',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Redaktsiooni number',\n    'pages_revisions_numbered' => 'Redaktsioon #:id',\n    'pages_revisions_numbered_changes' => 'Redaktsiooni #:id muudatused',\n    'pages_revisions_editor' => 'Redaktori tüüp',\n    'pages_revisions_changelog' => 'Muudatuste ajalugu',\n    'pages_revisions_changes' => 'Muudatused',\n    'pages_revisions_current' => 'Praegune versioon',\n    'pages_revisions_preview' => 'Eelvaade',\n    'pages_revisions_restore' => 'Taasta',\n    'pages_revisions_none' => 'Sellel lehel ei ole redaktsioone',\n    'pages_copy_link' => 'Kopeeri link',\n    'pages_edit_content_link' => 'Hüppa redaktoris sektsioonini',\n    'pages_pointer_enter_mode' => 'Ava sektsiooni valiku režiim',\n    'pages_pointer_label' => 'Lehe sektsiooni valikud',\n    'pages_pointer_permalink' => 'Lehe sektsiooni permalink',\n    'pages_pointer_include_tag' => 'Lehe sektsiooni viitesilt',\n    'pages_pointer_toggle_link' => 'Permalingi režiim, vajuta viitesildi kuvamiseks',\n    'pages_pointer_toggle_include' => 'Viitesildi režiim, vajuta permalingi kuvamiseks',\n    'pages_permissions_active' => 'Lehe õigused on aktiivsed',\n    'pages_initial_revision' => 'Esimene redaktsioon',\n    'pages_references_update_revision' => 'Seesmiste linkide automaatne uuendamine',\n    'pages_initial_name' => 'Uus leht',\n    'pages_editing_draft_notification' => 'Sa muudad mustandit, mis salvestati viimati :timeDiff.',\n    'pages_draft_edited_notification' => 'Seda lehte on sellest ajast saadid uuendatud. Soovitame mustandist loobuda.',\n    'pages_draft_page_changed_since_creation' => 'Seda lehte on pärast mustandi loomist muudetud. Soovitame mustandi ära visata või olla hoolikas, et mitte lehe muudatusi üle kirjutada.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count kasutajat on selle lehe muutmist alustanud',\n        'start_b' => ':userName alustas selle lehe muutmist',\n        'time_a' => 'lehe viimasest muutmisest alates',\n        'time_b' => 'viimase :minCount minuti jooksul',\n        'message' => ':start :time. Ärge teineteise muudatusi üle kirjutage!',\n    ],\n    'pages_draft_discarded' => 'Mustand ära visatud! Redaktorisse laeti lehe värske sisu',\n    'pages_draft_deleted' => 'Mustand kustutatud! Redaktorisse laeti lehe värske sisu',\n    'pages_specific' => 'Spetsiifiline leht',\n    'pages_is_template' => 'Lehe mall',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Kuva/peida külgriba',\n    'page_tags' => 'Lehe sildid',\n    'chapter_tags' => 'Peatüki sildid',\n    'book_tags' => 'Raamatu sildid',\n    'shelf_tags' => 'Riiuli sildid',\n    'tag' => 'Silt',\n    'tags' =>  'Sildid',\n    'tags_index_desc' => 'Silte saab kasutada süsteemis oleva sisu paindlikuks kategoriseerimiseks. Siltidel saab olla nii võti kui väärtus, millest viimane on valikuline. Pärast määramist saab sisu otsida sildi nime ja väärtuse kaudu.',\n    'tag_name' =>  'Sildi nimi',\n    'tag_value' => 'Sildi väärtus (valikuline)',\n    'tags_explain' => \"Lisa silte, et sisu paremini organiseerida.\\nVeel täpsemaks organiseerimiseks saad siltidele väärtuseid määrata.\",\n    'tags_add' => 'Lisa veel üks silt',\n    'tags_remove' => 'Eemalda see silt',\n    'tags_usages' => 'Siltide kasutus',\n    'tags_assigned_pages' => 'Lisatud lehtedele',\n    'tags_assigned_chapters' => 'Lisatud peatükkidele',\n    'tags_assigned_books' => 'Lisatud raamatutele',\n    'tags_assigned_shelves' => 'Lisatud riiulitele',\n    'tags_x_unique_values' => ':count unikaalset',\n    'tags_all_values' => 'Kõik väärtused',\n    'tags_view_tags' => 'Vaata silte',\n    'tags_view_existing_tags' => 'Vaata olemasolevaid silte',\n    'tags_list_empty_hint' => 'Silte saab lisada lehe redaktori külgmenüü kaudu või raamatu, peatüki või riiuli andmeid muutes.',\n    'attachments' => 'Manused',\n    'attachments_explain' => 'Laadi üles faile või lisa linke, mida lehel kuvada. Need on nähtavad külgmenüüs.',\n    'attachments_explain_instant_save' => 'Muudatused salvestatakse koheselt.',\n    'attachments_upload' => 'Laadi fail üles',\n    'attachments_link' => 'Lisa link',\n    'attachments_upload_drop' => 'Saad ka faile siia lohistada, et neid manusena üles laadida.',\n    'attachments_set_link' => 'Määra link',\n    'attachments_delete' => 'Kas oled kindel, et soovid selle manuse kustutada?',\n    'attachments_dropzone' => 'Lohista failid üleslaadimiseks siia',\n    'attachments_no_files' => 'Üleslaaditud faile ei ole',\n    'attachments_explain_link' => 'Faili üleslaadimise asemel saad lingi lisada. See võib viidata teisele lehele või failile kuskil pilves.',\n    'attachments_link_name' => 'Lingi nimi',\n    'attachment_link' => 'Manuse link',\n    'attachments_link_url' => 'Link failile',\n    'attachments_link_url_hint' => 'Lehekülje või faili URL',\n    'attach' => 'Lisa',\n    'attachments_insert_link' => 'Lisa manuse link lehele',\n    'attachments_edit_file' => 'Muuda faili',\n    'attachments_edit_file_name' => 'Faili nimi',\n    'attachments_edit_drop_upload' => 'Manuse üle kirjutamiseks lohista failid või klõpsa siin',\n    'attachments_order_updated' => 'Manuste järjekord muudetud',\n    'attachments_updated_success' => 'Manuse andmed muudetud',\n    'attachments_deleted' => 'Manus kustutatud',\n    'attachments_file_uploaded' => 'Fail on üles laaditud',\n    'attachments_file_updated' => 'Fail on muudetud',\n    'attachments_link_attached' => 'Link on lehele lisatud',\n    'templates' => 'Mallid',\n    'templates_set_as_template' => 'Leht on mall',\n    'templates_explain_set_as_template' => 'Sa saad määrata selle lehe malliks, nii et selle sisu saab kasutada uute lehtede lisamisel. Kui teistel kasutajatel on selle lehe vaatamiseks õigus, saavad ka nemad seda mallina kasutada.',\n    'templates_replace_content' => 'Asenda lehe sisu',\n    'templates_append_content' => 'Lisa lehe sisu järele',\n    'templates_prepend_content' => 'Lisa lehe sisu ette',\n\n    // Profile View\n    'profile_user_for_x' => 'Kasutaja olnud :time',\n    'profile_created_content' => 'Lisatud sisu',\n    'profile_not_created_pages' => ':userName ei ole ühtegi lehte lisanud',\n    'profile_not_created_chapters' => ':userName ei ole ühtegi peatükki lisanud',\n    'profile_not_created_books' => ':userName ei ole ühtegi raamatut lisanud',\n    'profile_not_created_shelves' => ':userName ei ole ühtegi riiulit lisanud',\n\n    // Comments\n    'comment' => 'Kommentaar',\n    'comments' => 'Kommentaarid',\n    'comment_add' => 'Lisa kommentaar',\n    'comment_none' => 'Pole kommentaare, mida kuvada',\n    'comment_placeholder' => 'Jäta siia kommentaar',\n    'comment_thread_count' => ':count kommentaarilõim|:count kommentaarilõime',\n    'comment_archived_count' => ':count arhiveeritud',\n    'comment_archived_threads' => 'Arhiveeritud lõimed',\n    'comment_save' => 'Salvesta kommentaar',\n    'comment_new' => 'Uus kommentaar',\n    'comment_created' => 'kommenteeris :createDiff',\n    'comment_updated' => 'Muudetud :updateDiff :username poolt',\n    'comment_updated_indicator' => 'Uuendatud',\n    'comment_deleted_success' => 'Kommentaar kustutatud',\n    'comment_created_success' => 'Kommentaar lisatud',\n    'comment_updated_success' => 'Kommentaar muudetud',\n    'comment_archive_success' => 'Kommentaar arhiveeritud',\n    'comment_unarchive_success' => 'Kommentaar arhiivist taastatud',\n    'comment_view' => 'Vaata kommentaari',\n    'comment_jump_to_thread' => 'Hüppa lõime juurde',\n    'comment_delete_confirm' => 'Kas oled kindel, et soovid selle kommentaari kustutada?',\n    'comment_in_reply_to' => 'Vastus kommentaarile :commentId',\n    'comment_reference' => 'Viide',\n    'comment_reference_outdated' => '(Aegunud)',\n    'comment_editor_explain' => 'Siin on lehele jäetud kommentaarid. Kommentaare saab lisada ja hallata salvestatud lehte vaadates.',\n\n    // Revision\n    'revision_delete_confirm' => 'Kas oled kindel, et soovid selle redaktsiooni kustutada?',\n    'revision_restore_confirm' => 'Kas oled kindel, et soovid selle redaktsiooni taastada? Lehe praegune sisu asendatakse.',\n    'revision_cannot_delete_latest' => 'Kõige viimast redaktsiooni ei saa kustutada.',\n\n    // Copy view\n    'copy_consider' => 'Sisu kopeerimisel pea järgnevat meeles.',\n    'copy_consider_permissions' => 'Kohandatud õiguseid ei kopeerita.',\n    'copy_consider_owner' => 'Sind määratakse kopeeritud sisu omanikuks.',\n    'copy_consider_images' => 'Lehel olevaid pildifaile ei dubleerita. Pildid säilitavad viite lehele, millele nad algselt lisati.',\n    'copy_consider_attachments' => 'Lehe manuseid ei kopeerita.',\n    'copy_consider_access' => 'Asukoha, omaniku või õiguste muudatused võivad teha sisu kättesaadavaks neile, kellel varem sellele ligipääs puudus.',\n\n    // Conversions\n    'convert_to_shelf' => 'Muuda riiuliks',\n    'convert_to_shelf_contents_desc' => 'Saad muuta selle raamatu uueks, sama sisuga riiuliks. Raamatu peatükid muudetakse uuteks raamatuteks. Kui raamat sisaldab lehti, mis ei kuulu ühegi peatüki alla, nimetatakse see raamat ümber ning see saab uue riiuli osana sisaldama neid lehti.',\n    'convert_to_shelf_permissions_desc' => 'Raamatule määratud õigused kopeeritakse uuele riiulile ja kõigile uutele raamatutele, millel ei ole endal määratud õiguseid. Pane tähele, et riiulitele määratud õigused ei rakendu automaatselt sisule, nagu raamatute puhul.',\n    'convert_book' => 'Muuda raamat',\n    'convert_book_confirm' => 'Kas oled kindel, et soovid selle raamatu muuta?',\n    'convert_undo_warning' => 'Seda ei saa lihtsasti tagasi võtta.',\n    'convert_to_book' => 'Muuda raamatuks',\n    'convert_to_book_desc' => 'Saad muuta selle peatüki uueks, sama sisuga raamatuks. Peatükile määratud õigused kopeeritakse uuele raamatule, aga olemasolevalt raamatult pärit õiguseid ei kopeerita, mis võib põhjustada muudatusi ligipääsudes.',\n    'convert_chapter' => 'Muud peatükk',\n    'convert_chapter_confirm' => 'Kas oled kindel, et soovid selle peatüki muuta?',\n\n    // References\n    'references' => 'Viited',\n    'references_none' => 'Sellele objektile ei ole viiteid.',\n    'references_to_desc' => 'Allpool on kogu teadaolev sisu süsteemis, mis sellele objektile viitab.',\n\n    // Watch Options\n    'watch' => 'Jälgi',\n    'watch_title_default' => 'Vaikimisi eelistused',\n    'watch_desc_default' => 'Lähtesta jälgimine vaikimisi eelistustele.',\n    'watch_title_ignore' => 'Ignoreeri',\n    'watch_desc_ignore' => 'Ignoreeri kõiki teavitusi, ka kasutaja tasemel määratud eelistusi.',\n    'watch_title_new' => 'Uued lehed',\n    'watch_desc_new' => 'Teavita, kui sellesse objekti lisatakse uus leht.',\n    'watch_title_updates' => 'Kõik lehed',\n    'watch_desc_updates' => 'Teavita kõigist uutest lehtedest ja lehtede muudatustest.',\n    'watch_desc_updates_page' => 'Teavita kõigist lehtede muudatustest.',\n    'watch_title_comments' => 'Kõik lehed ja kommentaarid',\n    'watch_desc_comments' => 'Teavita kõigist uutest lehtedest, lehtede muudatustest ja uutest kommentaaridest.',\n    'watch_desc_comments_page' => 'Teavita lehtede muudatustest ja uutest kommentaaridest.',\n    'watch_change_default' => 'Muuda vaikimisi teavituste eelistusi',\n    'watch_detail_ignore' => 'Teavitusi ignoreeritakse',\n    'watch_detail_new' => 'Jälgitakse uusi lehti',\n    'watch_detail_updates' => 'Jälgitakse uusi lehti ja muudatusi',\n    'watch_detail_comments' => 'Jälgitakse uusi lehti, muudatusi ja kommentaare',\n    'watch_detail_parent_book' => 'Jälgitakse raamatu kaudu',\n    'watch_detail_parent_book_ignore' => 'Ignoreeritakse raamatu kaudu',\n    'watch_detail_parent_chapter' => 'Jälgitakse peatüki kaudu',\n    'watch_detail_parent_chapter_ignore' => 'Ignoreeritakse peatüki kaudu',\n];\n"
  },
  {
    "path": "lang/et/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Sul puudub õigus selle lehe vaatamiseks.',\n    'permissionJson' => 'Sul puudub õigus selle tegevuse teostamiseks.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'See e-posti aadress on juba seotud teise kasutajaga.',\n    'auth_pre_register_theme_prevention' => 'Etteantud detailidega kasutajakontot ei õnnestunud registreerida',\n    'email_already_confirmed' => 'E-posti aadress on juba kinnitatud. Proovi sisse logida.',\n    'email_confirmation_invalid' => 'Kinnituslink ei ole kehtiv või on seda juba kasutatud. Proovi uuesti registreeruda.',\n    'email_confirmation_expired' => 'Kinnituslink on aegunud. Sulle saadeti aadressi kinnitamiseks uus e-kiri.',\n    'email_confirmation_awaiting' => 'Selle kasutajakonto e-posti aadress vajab kinnitamist',\n    'ldap_fail_anonymous' => 'LDAP anonüümne ligipääs ebaõnnestus',\n    'ldap_fail_authed' => 'LDAP ligipääs antud nime ja parooliga ebaõnnestus',\n    'ldap_extension_not_installed' => 'PHP LDAP laiendus ei ole paigaldatud',\n    'ldap_cannot_connect' => 'Ühendus LDAP serveriga ebaõnnestus',\n    'saml_already_logged_in' => 'Juba sisse logitud',\n    'saml_no_email_address' => 'Selle kasutaja e-posti aadressi ei õnnestunud välisest autentimissüsteemist leida',\n    'saml_invalid_response_id' => 'Välisest autentimissüsteemist tulnud päringut ei algatatud sellest rakendusest. Seda viga võib põhjustada pärast sisselogimist tagasi liikumine.',\n    'saml_fail_authed' => 'Sisenemine :system kaudu ebaõnnestus, süsteem ei andnud volitust',\n    'oidc_already_logged_in' => 'Juba sisse logitud',\n    'oidc_no_email_address' => 'Selle kasutaja e-posti aadressi ei õnnestunud välisest autentimissüsteemist leida',\n    'oidc_fail_authed' => 'Sisenemine :system kaudu ebaõnnestus, süsteem ei andnud volitust',\n    'social_no_action_defined' => 'Tegevus on defineerimata',\n    'social_login_bad_response' => \":socialAccount kaudu sisselogimisel tekkis viga: \\n:error\",\n    'social_account_in_use' => 'See :socialAccount konto on juba kasutusel, proovi :socialAccount kaudu sisse logida.',\n    'social_account_email_in_use' => 'E-posti aadress :email on juba kasutusel. Kui sul on juba kasutajakonto, saad oma :socialAccount konto siduda profiili seadetes.',\n    'social_account_existing' => 'See :socialAccount konto on juba seotud su profiiliga.',\n    'social_account_already_used_existing' => 'See :socialAccount konto on juba seotud teise kasutajaga.',\n    'social_account_not_used' => 'See :socialAccount konto ei ole seotud ühegi kasutajaga. Seosta see oma profiili seadetes. ',\n    'social_account_register_instructions' => 'Kui sul pole veel kasutajakontot, saad selle registreerida :socialAccount kaudu.',\n    'social_driver_not_found' => 'Sotsiaalmeedia kontode draiverit ei leitud',\n    'social_driver_not_configured' => 'Sinu :socialAccount konto seaded ei ole korrektsed.',\n    'invite_token_expired' => 'Link on aegunud. Võid selle asemel proovida oma konto parooli lähtestada.',\n    'login_user_not_found' => 'Selle tegevuse jaoks ei leitud kasutajat.',\n\n    // System\n    'path_not_writable' => 'Faili asukohaga :filePath ei õnnestunud üles laadida. Veendu, et serveril on kirjutusõigused.',\n    'cannot_get_image_from_url' => 'Ei suutnud laadida pilti aadressilt :url',\n    'cannot_create_thumbs' => 'Server ei saa piltide eelvaateid tekitada. Veendu, et PHP GD laiendus on paigaldatud.',\n    'server_upload_limit' => 'Server ei luba nii suurte failide üleslaadimist. Proovi väiksema failiga.',\n    'server_post_limit' => 'Server ei saa etteantud andmemahtu vastu võtta. Proovi uuesti väiksema failiga.',\n    'uploaded'  => 'Server ei luba nii suurte failide üleslaadimist. Proovi väiksema failiga.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Pildi üleslaadimisel tekkis viga',\n    'image_upload_type_error' => 'Pildifaili tüüp ei ole korrektne',\n    'image_upload_replace_type' => 'Pildifaili asendused peavad olema sama tüüpi',\n    'image_upload_memory_limit' => 'Pildi üleslaadimine ja/või eelvaadete tekitamine ebaõnnestus süsteemsete ressursipiirangute tõttu.',\n    'image_thumbnail_memory_limit' => 'Pildi eelvaadete tekitamine ebaõnnestus süsteemsete ressursipiirangute tõttu.',\n    'image_gallery_thumbnail_memory_limit' => 'Galerii eelvaadete tekitamine ebaõnnestus süsteemsete ressursipiirangute tõttu.',\n    'drawing_data_not_found' => 'Joonise andmeid ei õnnestunud laadida. Joonist ei pruugi enam eksisteerida, või sul puuduvad õigused selle vaatamiseks.',\n\n    // Attachments\n    'attachment_not_found' => 'Manust ei leitud',\n    'attachment_upload_error' => 'Manuse faili üleslaadimisel tekkis viga',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Mustandi salvestamine ebaõnnestus. Kontrolli oma internetiühendust',\n    'page_draft_delete_fail' => 'Mustandi kustutamine ja lehe salvestatud seisu laadimine ebaõnnestus',\n    'page_custom_home_deletion' => 'Ei saa kustutada lehte, mis on määratud avaleheks',\n\n    // Entities\n    'entity_not_found' => 'Objekti ei leitud',\n    'bookshelf_not_found' => 'Riiulit ei leitud',\n    'book_not_found' => 'Raamatut ei leitud',\n    'page_not_found' => 'Lehte ei leitud',\n    'chapter_not_found' => 'Peatükki ei leitud',\n    'selected_book_not_found' => 'Valitud raamatut ei leitud',\n    'selected_book_chapter_not_found' => 'Valitud raamatut või peatükki ei leitud',\n    'guests_cannot_save_drafts' => 'Külalised ei saa mustandeid salvestada',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Ainsat administraatorit ei saa kustutada',\n    'users_cannot_delete_guest' => 'Külaliskasutajat ei saa kustutada',\n    'users_could_not_send_invite' => 'Kasutajat ei õnnestunud luua, kuna kutse e-kirja saatmine ebaõnnestus',\n\n    // Roles\n    'role_cannot_be_edited' => 'Seda rolli ei saa muuta',\n    'role_system_cannot_be_deleted' => 'See roll on süsteemne ja seda ei saa kustutada',\n    'role_registration_default_cannot_delete' => 'Seda rolli ei saa kustutada, kuna see on seatud uute kasutajate vaikimisi rolliks',\n    'role_cannot_remove_only_admin' => 'See kasutaja on ainus, kellel on administraatori roll. Enne kustutamist lisa administraatori roll mõnele teisele kasutajale.',\n\n    // Comments\n    'comment_list' => 'Kommentaaride pärimisel tekkis viga.',\n    'cannot_add_comment_to_draft' => 'Mustandile ei saa kommentaare lisada.',\n    'comment_add' => 'Kommentaari lisamisel / muutmisel tekkis viga.',\n    'comment_delete' => 'Kommentaari kustutamisel tekkis viga.',\n    'empty_comment' => 'Tühja kommentaari ei saa lisada.',\n\n    // Error pages\n    '404_page_not_found' => 'Lehekülge ei leitud',\n    'sorry_page_not_found' => 'Vabandust, soovitud lehekülge ei leitud.',\n    'sorry_page_not_found_permission_warning' => 'Kui see lehekülg peaks kindlalt olemas olema, ei pruugi sul olla õigust selle vaatamiseks.',\n    'image_not_found' => 'Pildifaili ei leitud',\n    'image_not_found_subtitle' => 'Vabandust, soovitud pildifaili ei leitud.',\n    'image_not_found_details' => 'Kui sa eeldasid, et see pildifail on olemas, võib see olla kustutatud.',\n    'return_home' => 'Tagasi avalehele',\n    'error_occurred' => 'Tekkis viga',\n    'app_down' => ':appName on hetkel maas',\n    'back_soon' => 'See on varsti tagasi.',\n\n    // Import\n    'import_zip_cant_read' => 'ZIP-faili lugemine ebaõnnestus.',\n    'import_zip_cant_decode_data' => 'ZIP-failist ei leitud data.json sisu.',\n    'import_zip_no_data' => 'ZIP-failist ei leitud raamatute, peatükkide või lehtede sisu.',\n    'import_zip_data_too_large' => 'ZIP-faili data.json sisu ületab rakenduses seadistatud maksimaalse failisuuruse.',\n    'import_validation_failed' => 'Imporditud ZIP-faili valideerimine ebaõnnestus vigadega:',\n    'import_zip_failed_notification' => 'ZIP-faili importimine ebaõnnestus.',\n    'import_perms_books' => 'Sul puuduvad õigused raamatute lisamiseks.',\n    'import_perms_chapters' => 'Sul puuduvad õigused peatükkide lisamiseks.',\n    'import_perms_pages' => 'Sul puuduvad õigused lehtede lisamiseks.',\n    'import_perms_images' => 'Sul puuduvad õigused piltide lisamiseks.',\n    'import_perms_attachments' => 'Sul puuduvad õigused manuste lisamiseks.',\n\n    // API errors\n    'api_no_authorization_found' => 'Päringust ei leitud volitustunnust',\n    'api_bad_authorization_format' => 'Päringust leiti volitustunnus, aga see ei olnud korrektses formaadis',\n    'api_user_token_not_found' => 'Volitustunnusele vastavat API tunnust ei leitud',\n    'api_incorrect_token_secret' => 'API tunnusele lisatud salajane võti ei ole korrektne',\n    'api_user_no_api_permission' => 'Selle API tunnuse omanikul ei ole õigust API päringuid teha',\n    'api_user_token_expired' => 'Volitustunnus on aegunud',\n    'api_cookie_auth_only_get' => 'Küpsistega autentimisel on API kasutamisel lubatud ainult GET päringud',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Test e-kirja saatmisel tekkis viga:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'URL ei klapi ühegi lubatud SSR hostiga',\n];\n"
  },
  {
    "path": "lang/et/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Uus kommentaar lehel: :pageName',\n    'new_comment_intro' => 'Rakenduses :appName kommenteeriti lehte:',\n    'new_page_subject' => 'Uus leht: :pageName',\n    'new_page_intro' => 'Rakenduses :appName lisati uus leht:',\n    'updated_page_subject' => 'Muudetud leht: :pageName',\n    'updated_page_intro' => 'Rakenduses :appName muudeti lehte:',\n    'updated_page_debounce' => 'Et vältida liigseid teavitusi, ei saadeta sulle mõnda aega teavitusi selle lehe muutmiste kohta sama kasutaja poolt.',\n    'comment_mention_subject' => 'Sind mainiti kommentaaris lehel: :pageName',\n    'comment_mention_intro' => 'Sind mainiti kommentaaris rakenduses :appName:',\n\n    'detail_page_name' => 'Lehe nimetus:',\n    'detail_page_path' => 'Lehe asukoht:',\n    'detail_commenter' => 'Kommenteerija:',\n    'detail_comment' => 'Kommentaar:',\n    'detail_created_by' => 'Autor:',\n    'detail_updated_by' => 'Muutja:',\n\n    'action_view_comment' => 'Vaata kommentaari',\n    'action_view_page' => 'Vaata lehte',\n\n    'footer_reason' => 'See teavitus saadeti sulle, sest :link sisaldavad selle objekti kohta sellist tegevust.',\n    'footer_reason_link' => 'sinu teavituste eelistused',\n];\n"
  },
  {
    "path": "lang/et/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Eelmine',\n    'next'     => 'Järgmine &raquo;',\n\n];\n"
  },
  {
    "path": "lang/et/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Paroolides peab olema vähemalt kaheksa tähemärki ja nad peavad omavahel ühtima.',\n    'user' => \"Sellise e-posti aadressiga kasutajat ei leitud.\",\n    'token' => 'Parooli lähtestamise link ei kehti selle e-posti aadressiga.',\n    'sent' => 'Parooli lähtestamise link saadeti e-postiga!',\n    'reset' => 'Parool on lähtestatud!',\n\n];\n"
  },
  {
    "path": "lang/et/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Minu konto',\n\n    'shortcuts' => 'Kiirklahvid',\n    'shortcuts_interface' => 'Kasutajaliidese eelistused',\n    'shortcuts_toggle_desc' => 'Siit saad sisse ja välja lülitada navigeerimiseks ja tegevusteks kasutatavad kiirklahvid.',\n    'shortcuts_customize_desc' => 'Allpool saad iga kiirklahvi kohandada. Pärast kiirklahvile vastava tekstivälja valimist vajuta lihtsalt soovitud klahvikombinatsiooni.',\n    'shortcuts_toggle_label' => 'Kiirklahvid sisse lülitatud',\n    'shortcuts_section_navigation' => 'Navigatsioon',\n    'shortcuts_section_actions' => 'Sagedased tegevused',\n    'shortcuts_save' => 'Salvesta kiirklahvid',\n    'shortcuts_overlay_desc' => 'Märkus: Kui kiirklahvid on sisse lülitatud, saab \"?\" vajutades kuvada abiinfo, mis märgib ära kõigi hetkel ekraanil nähtavate tegevuste kiirklahvid.',\n    'shortcuts_update_success' => 'Kiirklahvide eelistused on salvestatud!',\n    'shortcuts_overview_desc' => 'Halda klaviatuuri kiirklahve süsteemi kasutajaliideses navigeerimiseks.',\n\n    'notifications' => 'Teavituste eelistused',\n    'notifications_desc' => 'Halda e-posti teavitusi, mis saadetakse teatud tegevuste puhul.',\n    'notifications_opt_own_page_changes' => 'Teavita muudatustest minu lehtedel',\n    'notifications_opt_own_page_comments' => 'Teavita kommentaaridest minu lehtedel',\n    'notifications_opt_comment_mentions' => 'Teavita mind, kui mind kommentaaris mainitakse',\n    'notifications_opt_comment_replies' => 'Teavita vastustest minu kommentaaridele',\n    'notifications_save' => 'Salvesta eelistused',\n    'notifications_update_success' => 'Teavituste eelistused on salvestatud!',\n    'notifications_watched' => 'Jälgitud ja ignoreeritud objektid',\n    'notifications_watched_desc' => 'Allpool on objektid, millele on määratud kohaldatud jälgimise eelistused. Eelistuste muutmiseks ava vastav objekt ning leia jälgimise valikud külgmenüüs.',\n\n    'auth' => 'Ligipääs ja turvalisus',\n    'auth_change_password' => 'Muuda parool',\n    'auth_change_password_desc' => 'Muuda parooli, millega rakendusse sisse logid. See peab olema vähemalt 8 tähemärki.',\n    'auth_change_password_success' => 'Parool on muudetud!',\n\n    'profile' => 'Profiili detailid',\n    'profile_desc' => 'Halda oma konto andmeid, mis sind teistele kasutajatele esindab, lisaks andmetele, mida kasutatakse suhtluseks ja süsteemi kohaldamiseks.',\n    'profile_view_public' => 'Vaata avalikku profiili',\n    'profile_name_desc' => 'Seadista oma avalik nimi, mis on nähtav teistele kasutajatele sinu tehtud tegevuste ja sulle kuuluva sisu kaudu.',\n    'profile_email_desc' => 'Seda e-posti aadressi kasutatakse teavituste saatmiseks ning, sõltuvalt aktiivsest autentimismeetodist, süsteemile ligipääsemiseks.',\n    'profile_email_no_permission' => 'Kahjuks ei ole sul luba oma e-posti aadressi muuta. Kui soovid seda muuta, pead administraatoriga ühendust võtma.',\n    'profile_avatar_desc' => 'Vali pilt, mis sind süsteemis teistele kasutajatele esindab. Ideaalis peaks see pilt olema ruudukujuline ning 256px kõrge ja lai.',\n    'profile_admin_options' => 'Administraatori valikud',\n    'profile_admin_options_desc' => 'Täiendavad administraatori-taseme valikud, näiteks rollide haldamiseks, on rakenduse \"Seaded > Kasutajad\" all sinu kasutajakonto vaates.',\n\n    'delete_account' => 'Kustuta konto',\n    'delete_my_account' => 'Kustuta minu konto',\n    'delete_my_account_desc' => 'See kustutab su kasutajakonto süsteemist. Sa ei saa kontot taastada ega seda tegevust tagasi võtta. Sinu loodud sisu, näiteks lisatud lehed ja üleslaaditud pildid, jäävad alles.',\n    'delete_my_account_warning' => 'Kas oled kindel, et soovid oma konto kustutada?',\n];\n"
  },
  {
    "path": "lang/et/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Seaded',\n    'settings_save' => 'Salvesta seaded',\n    'system_version' => 'Süsteemi versioon',\n    'categories' => 'Kategooriad',\n\n    // App Settings\n    'app_customization' => 'Kohandamine',\n    'app_features_security' => 'Funktsioonid ja turvalisus',\n    'app_name' => 'Rakenduse nimi',\n    'app_name_desc' => 'Seda nime näidatakse päises ja kõigis süsteemsetes e-kirjades.',\n    'app_name_header' => 'Näita nime päises',\n    'app_public_access' => 'Avalik ligipääs',\n    'app_public_access_desc' => 'Selle sisselülitamine võimaldab külalistel ilma sisselogimata ligipääsu su BookStack\\'i sisule.',\n    'app_public_access_desc_guest' => 'Sisselogimata kasutajate ligipääsu saab seadistada \"Külaline\" kasutaja kaudu.',\n    'app_public_access_toggle' => 'Luba avalik ligipääs',\n    'app_public_viewing' => 'Luba avalik ligipääs?',\n    'app_secure_images' => 'Turvalisem piltide üleslaadimine',\n    'app_secure_images_toggle' => 'Lülita sisse turvalisem piltide üleslaadimine',\n    'app_secure_images_desc' => 'Jõudluse kaalutlustel on kõik pildifailid avalikult kättesaadavad. See valik lisab pildifailide URL-ide ette juhugenereeritud, raskesti arvatava stringi. Ligipääsu piiramiseks veendu, et kataloogide indekseerimine ei oleks lubatud.',\n    'app_default_editor' => 'Vaikimisi lehe redaktor',\n    'app_default_editor_desc' => 'Vali, millist redaktorit vaikimisi uute lehtede jaoks kasutada. Seda valikut saab õiguste olemasolul iga lehe jaoks eraldi muuta.',\n    'app_custom_html' => 'Kohandatud HTML päise sisu',\n    'app_custom_html_desc' => 'Siia lisatud sisu lisatakse iga lehe <head> sektsiooni lõppu. See võimaldab stiile üle laadida või lisada analüütika koodi.',\n    'app_custom_html_disabled_notice' => 'Kohandatud HTML päise sisu on sellel lehel välja lülitatud, et probleemseid muudatusi saaks tagasi võtta.',\n    'app_logo' => 'Rakenduse logo',\n    'app_logo_desc' => 'Seda kasutatakse muuhulgas rakenduse päises. Pildifail peaks olema 86px kõrge. Suuremad pildid tehakse väiksemaks.',\n    'app_icon' => 'Rakenduse ikoon',\n    'app_icon_desc' => 'Seda ikooni kasutatakse brauseri sakkidel ja järjehoidjate ikoonidena. See peaks olema 256px ruudukujuline PNG.',\n    'app_homepage' => 'Rakenduse avaleht',\n    'app_homepage_desc' => 'Vali leht, mida näidata avalehel vaikimisi vaate asemel. Valitud lehele ei rakendata ligipääsuõiguseid.',\n    'app_homepage_select' => 'Vali leht',\n    'app_footer_links' => 'Lingid jaluses',\n    'app_footer_links_desc' => 'Lisa rakenduse jalusesse linke. Neid näidatakse enamike lehtede jaluses, kaasa arvatud need, mis ei vaja sisselogimist. Võid kasutada märgendit \"trans::<key>\", et kasutada süsteemseid tõlkeid. Näiteks \"trans::common.privacy_policy\" tekitab tõlgitud teksti \"Privaatsus\" ning \"trans::common.terms_of_service\" tekitab tõlgitud teksti \"Kasutustingimused\".',\n    'app_footer_links_label' => 'Lingi tekst',\n    'app_footer_links_url' => 'Lingi URL',\n    'app_footer_links_add' => 'Lisa link',\n    'app_disable_comments' => 'Keela kommentaarid',\n    'app_disable_comments_toggle' => 'Keela kommentaarid',\n    'app_disable_comments_desc' => 'Keelab kommentaarid kogu rakenduses. <br>Olemasolevaid kommentaare ei näidata.',\n\n    // Color settings\n    'color_scheme' => 'Rakenduse värvid',\n    'color_scheme_desc' => 'Määra rakenduse kasutajaliidese värvid. Tumeda ja heleda režiimi värve saab sobivuse ja loetavuse huvides eraldi seadistada.',\n    'ui_colors_desc' => 'Määra rakenduse põhivärv ja vaikimisi linkide värv. Põhivärvi kasutatakse peamiselt päise, nuppude ning kasutajaliidese dekoratsioonide jaoks. Vaikimisi linkide värvi kasutatakse tekstipõhiste linkide ja tegevuste jaoks, nii kirjalikus sisus kui rakenduse kasutajaliideses.',\n    'app_color' => 'Põhivärv',\n    'link_color' => 'Vaikimisi linkide värv',\n    'content_colors_desc' => 'Määra värvid erinevatele sisuelementidele. Loetavuse huvides on soovituslik valida värvid, mille heledus on sarnane vaikimisi värvidele.',\n    'bookshelf_color' => 'Riiuli värv',\n    'book_color' => 'Raamatu värv',\n    'chapter_color' => 'Peatüki värv',\n    'page_color' => 'Lehe värv',\n    'page_draft_color' => 'Mustandi värv',\n\n    // Registration Settings\n    'reg_settings' => 'Registreerumine',\n    'reg_enable' => 'Luba registreerumine',\n    'reg_enable_toggle' => 'Luba registreerumine',\n    'reg_enable_desc' => 'Kui registreerumine on lubatud, saavad kasutajad ise endale rakenduse konto tekitada, ning neile antakse vaikimisi roll.',\n    'reg_default_role' => 'Vaikimisi roll uutele kasutajatele',\n    'reg_enable_external_warning' => 'Ülalolevat valikut ignoreeritakse, kui väline LDAP või SAML autentimine on aktiivne. Kui autentimine välise süsteemi vastu on edukas, genereeritakse puuduvad kasutajadkontod automaatselt.',\n    'reg_email_confirmation' => 'E-posti aadressi kinnitus',\n    'reg_email_confirmation_toggle' => 'Nõua e-posti aadressi kinnitamist',\n    'reg_confirm_email_desc' => 'Kui domeeni piirang on kasutusel, siis on e-posti aadressi kinnitamine nõutud ja seda seadet ignoreeritakse.',\n    'reg_confirm_restrict_domain' => 'Domeeni piirang',\n    'reg_confirm_restrict_domain_desc' => 'Sisesta komaga eraldatud nimekiri e-posti domeenidest, millega soovid registreerumist piirata. Kasutajale saadetakse aadressi kinnitamiseks e-kiri, enne kui neil lubatakse rakendust kasutada.<br>Pane tähele, et kasutajad saavad pärast edukat registreerumist oma e-posti aadressi muuta.',\n    'reg_confirm_restrict_domain_placeholder' => 'Piirangut ei ole',\n\n    // Sorting Settings\n    'sorting' => 'Loendid ja järjestamine',\n    'sorting_book_default' => 'Vaikimisi raamatute sorteerimise reegel',\n    'sorting_book_default_desc' => 'Vali vaikimisi uutele raamatutele rakenduv sorteerimisreegel. See ei mõjuta olemasolevaid raamatuid ning seda saab raamatupõhiselt muuta.',\n    'sorting_rules' => 'Sorteerimisreeglid',\n    'sorting_rules_desc' => 'Need on eeldefineeritud sorteerimistoimingud, mida saab süsteemis olevale sisule rakendada.',\n    'sort_rule_assigned_to_x_books' => 'Määratud :count raamatule|Määratud :count raamatule',\n    'sort_rule_create' => 'Lisa sorteerimisreegel',\n    'sort_rule_edit' => 'Muuda sorteerimisreeglit',\n    'sort_rule_delete' => 'Kustuta sorteerimisreegel',\n    'sort_rule_delete_desc' => 'Eemalda sorteerimisreegel süsteemist. Seda reeglit kasutavad raamatud jäävad käsitsi sorteerituks.',\n    'sort_rule_delete_warn_books' => 'Seda reeglit kasutab hetkel :count raamat(ut). Kas oled kindel, et soovid selle kustutada?',\n    'sort_rule_delete_warn_default' => 'See reegel on hetkel raamatute vaikimisi sorteerimisreegel. Kas oled kindel, et soovid selle kustutada?',\n    'sort_rule_details' => 'Sorteerimisreegli andmed',\n    'sort_rule_details_desc' => 'Määra sorteerimisreegli nimi, mida kasutatakse reegli valimise loendites.',\n    'sort_rule_operations' => 'Sorteerimistoimingud',\n    'sort_rule_operations_desc' => 'Seadista sorteerimistoimingud, liigutades neid saadaval toimingute loendist. Toimingud rakendatakse järjest, ülevalt alla. Siin tehtud muudatused rakenduvad salvestamisel kõigile määratud raamatutele.',\n    'sort_rule_available_operations' => 'Saadaval toimingud',\n    'sort_rule_available_operations_empty' => 'Rohkem toiminguid pole',\n    'sort_rule_configured_operations' => 'Seadistatud toimingud',\n    'sort_rule_configured_operations_empty' => 'Lisa toiminguid \"Saadaval toimingud\" loendist',\n    'sort_rule_op_asc' => '(kasvavalt)',\n    'sort_rule_op_desc' => '(kahanevalt)',\n    'sort_rule_op_name' => 'Nimi - tähestikuline',\n    'sort_rule_op_name_numeric' => 'Nimi - numbriline',\n    'sort_rule_op_created_date' => 'Loomise aeg',\n    'sort_rule_op_updated_date' => 'Muutmise aeg',\n    'sort_rule_op_chapters_first' => 'Peatükid eespool',\n    'sort_rule_op_chapters_last' => 'Peatükid tagapool',\n    'sorting_page_limits' => 'Leheküljepõhised kuvalimiidid',\n    'sorting_page_limits_desc' => 'Vali, mitu objekti erinevates nimekirjades ühel lehel kuvada. Madalam väärtus tähendab reeglina paremat jõudlust, samas kui kõrgem väärtus väldib vajadust mitmeid lehti läbi klikkida. Soovituslik on kasutada 6-ga jaguvat väärtust.',\n\n    // Maintenance settings\n    'maint' => 'Hooldus',\n    'maint_image_cleanup' => 'Pildifailide koristus',\n    'maint_image_cleanup_desc' => 'Kontrollib lehtede ja redaktsioonide sisu, et leida pilte ja jooniseid, mis enam kasutusel ei ole. Enne selle käivitamist tee andmebaasist ja pildifailidest täielik varukoopia.',\n    'maint_delete_images_only_in_revisions' => 'Kustuta ka pildifailid, mis on kasutusel ainult vanades redaktsioonides',\n    'maint_image_cleanup_run' => 'Käivita koristus',\n    'maint_image_cleanup_warning' => 'Leiti :count potentsiaalselt kasutamata pildifaili. Kas oled kindel, et soovid need kustutada?',\n    'maint_image_cleanup_success' => 'Leiti ja kustutati :count potentsiaalselt kasutamata pildifaili!',\n    'maint_image_cleanup_nothing_found' => 'Kasutamata pildifaile ei leitud, pole midagi kustutada!',\n    'maint_send_test_email' => 'Saada testimiseks e-kiri',\n    'maint_send_test_email_desc' => 'See saadab testimiseks e-kirja su profiilis märgitud aadressile.',\n    'maint_send_test_email_run' => 'Saada test e-kiri',\n    'maint_send_test_email_success' => 'E-kiri saadetud aadressile :address',\n    'maint_send_test_email_mail_subject' => 'Test e-kiri',\n    'maint_send_test_email_mail_greeting' => 'E-kirjade saatmine tundub toimivat!',\n    'maint_send_test_email_mail_text' => 'Hea töö! Kui sa selle e-kirja kätte said, on su e-posti seaded õigesti määratud.',\n    'maint_recycle_bin_desc' => 'Kustutatud riiulid, raamatud, peatükid ja lehed saadetakse prügikasti, et neid saaks taastada või lõplikult kustutada. Vanemad objektid võidakse teatud aja järel automaatselt prügikastist kustutada.',\n    'maint_recycle_bin_open' => 'Ava prügikast',\n    'maint_regen_references' => 'Genereeri viited uuesti',\n    'maint_regen_references_desc' => 'See tegevus taastekitab andmebaasis objektidevahelised viited. Enamasti tehakse seda automaatselt, aga antud tegevus võimaldab indekseerida vanemat sisu või sisu, mis lisati mitteametlike meetodite abil.',\n    'maint_regen_references_success' => 'Viiteindeks genereeritud!',\n    'maint_timeout_command_note' => 'Märkus: See tegevus võib aega võtta, mis võib teatud veebikeskkondades põhjustada aegumise vigu. Alternatiivina võib seda tegevust käivitada käsurealt.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Prügikast',\n    'recycle_bin_desc' => 'Siin saad taastada kustutatud objekte, või neid süsteemist lõplikult eemaldada. Nimekiri on filtreerimata, mitte nagu mujal tegevusloendites, kus rakenduvad õigused.',\n    'recycle_bin_deleted_item' => 'Kustutatud objekt',\n    'recycle_bin_deleted_parent' => 'Ülemobjekt',\n    'recycle_bin_deleted_by' => 'Kustutaja',\n    'recycle_bin_deleted_at' => 'Kustutamise aeg',\n    'recycle_bin_permanently_delete' => 'Kustuta lõplikult',\n    'recycle_bin_restore' => 'Taasta',\n    'recycle_bin_contents_empty' => 'Prügikast on hetkel tühi',\n    'recycle_bin_empty' => 'Tühjenda prügikast',\n    'recycle_bin_empty_confirm' => 'See kustutab lõplikult kõik objektid prügikastis, kaasa arvatud nende sisu. Kas oled kindel, et soovid prügikasti tühjendada?',\n    'recycle_bin_destroy_confirm' => 'See kustutab lõplikult süsteemist valitud objekti koos loetletud alamobjektidega, ja seda sisu ei ole enam võimalik taastada. Kas oled kindel, et soovid selle objekti kustutada?',\n    'recycle_bin_destroy_list' => 'Kustutatavad objektid',\n    'recycle_bin_restore_list' => 'Taastatavad objektid',\n    'recycle_bin_restore_confirm' => 'See taastab valitud objekti koos kõigi alamobjektidega nende algsesse asukohta. Kui see asukoht on ka vahepeal kustutatud ja on nüüd prügikastis, tuleb ka see taastada.',\n    'recycle_bin_restore_deleted_parent' => 'Selle objekti ülemobjekt on ka kustutatud. Taastada ei saa enne, kui ülemobjekt on taastatud.',\n    'recycle_bin_restore_parent' => 'Taasta ülemobjekt',\n    'recycle_bin_destroy_notification' => 'Prügikastist kustutati :count objekti.',\n    'recycle_bin_restore_notification' => 'Prügikastist taastati :count objekti.',\n\n    // Audit Log\n    'audit' => 'Auditilogi',\n    'audit_desc' => 'Auditilogi kuvab nimekirja tegevustest, mida süsteem jälgib. See nimekiri on filtreerimata, erinevalt muudest loenditest süsteemis, kus rakenduvad õigused.',\n    'audit_event_filter' => 'Sündmuse filter',\n    'audit_event_filter_no_filter' => 'Ilma filtrita',\n    'audit_deleted_item' => 'Kustutatud objekt',\n    'audit_deleted_item_name' => 'Nimi: :name',\n    'audit_table_user' => 'Kasutaja',\n    'audit_table_event' => 'Sündmus',\n    'audit_table_related' => 'Seotud objekt või detail',\n    'audit_table_ip' => 'IP-aadress',\n    'audit_table_date' => 'Tegevuse aeg',\n    'audit_date_from' => 'Kuupäev alates',\n    'audit_date_to' => 'Kuupäev kuni',\n\n    // Role Settings\n    'roles' => 'Rollid',\n    'role_user_roles' => 'Kasutaja rollid',\n    'roles_index_desc' => 'Rolle saab kasutada kasutajate grupeerimiseks ja liikmetele süsteemsete õiguste andmiseks. Kui kasutaja on mitme rolli liige, siis õigused kombineeritakse ning kasutaja saab kõik õigused.',\n    'roles_x_users_assigned' => ':count kasutaja|:count kasutajat',\n    'roles_x_permissions_provided' => ':count õigus|:count õigust',\n    'roles_assigned_users' => 'Määratud kasutajad',\n    'roles_permissions_provided' => 'Antud õigused',\n    'role_create' => 'Lisa uus roll',\n    'role_delete' => 'Kustuta roll',\n    'role_delete_confirm' => 'See kustutab rolli nimega \\':roleName\\'.',\n    'role_delete_users_assigned' => 'Selle rolliga on seotud :userCount kasutajat. Kui soovid neile selle asemel uue rolli määrata, siis vali see allpool.',\n    'role_delete_no_migration' => \"Ära määra uut rolli\",\n    'role_delete_sure' => 'Kas oled kindel, et soovid selle rolli kustutada?',\n    'role_edit' => 'Muuda rolli',\n    'role_details' => 'Rolli detailid',\n    'role_name' => 'Rolli nimi',\n    'role_desc' => 'Rolli lühike kirjeldus',\n    'role_mfa_enforced' => 'Vajab mitmeastmelist autentimist',\n    'role_external_auth_id' => 'Välise autentimise ID-d',\n    'role_system' => 'Süsteemsed õigused',\n    'role_manage_users' => 'Kasutajate haldamine',\n    'role_manage_roles' => 'Rollide ja õiguste haldamine',\n    'role_manage_entity_permissions' => 'Kõigi raamatute, peatükkide ja lehtede õiguste haldamine',\n    'role_manage_own_entity_permissions' => 'Oma raamatute, peatükkide ja lehtede õiguste haldamine',\n    'role_manage_page_templates' => 'Mallide haldamine',\n    'role_access_api' => 'Süsteemi API ligipääs',\n    'role_manage_settings' => 'Rakenduse seadete haldamine',\n    'role_export_content' => 'Sisu eksport',\n    'role_import_content' => 'Imporditud sisu',\n    'role_editor_change' => 'Lehe redaktori muutmine',\n    'role_notifications' => 'Võta vastu ja halda teavitusi',\n    'role_permission_note_users_and_roles' => 'Need õigused lubavad ka süsteemis olevaid kasutajaid ja rolle vaadata ja otsida.',\n    'role_asset' => 'Sisu õigused',\n    'roles_system_warning' => 'Pane tähele, et ülalolevad kolm õigust võimaldavad kasutajal enda või teiste kasutajate õiguseid muuta. Määra nende õigustega roll ainult usaldusväärsetele kasutajatele.',\n    'role_asset_desc' => 'Need load kontrollivad vaikimisi ligipääsu süsteemis olevale sisule. Raamatute, peatükkide ja lehtede õigused rakenduvad esmajärjekorras.',\n    'role_asset_admins' => 'Administraatoritel on automaatselt ligipääs kogu sisule, aga need valikud võivad peida või näidata kasutajaliidese elemente.',\n    'role_asset_image_view_note' => 'See käib nähtavuse kohta pildifailide halduris. Tegelik ligipääs üleslaaditud pildifailidele sõltub süsteemsest piltide salvestamise valikust.',\n    'role_asset_users_note' => 'Need õigused lubavad ka süsteemis olevaid kasutajaid vaadata ja otsida.',\n    'role_all' => 'Kõik',\n    'role_own' => 'Enda omad',\n    'role_controlled_by_asset' => 'Õigused määratud seotud objekti kaudu',\n    'role_save' => 'Salvesta roll',\n    'role_users' => 'Selle rolliga kasutajad',\n    'role_users_none' => 'Seda rolli ei ole hetkel ühelgi kasutajal',\n\n    // Users\n    'users' => 'Kasutajad',\n    'users_index_desc' => 'Loo ja halda süsteemi kasutajakontosid. Kontosid kasutatakse sisselogimiseks ning sisu ja tegevuse omistamiseks. Ligipääsuload on enamasti rollipõhised, aga sisu omandus ja muud faktorid võivad samuti mõjutada õiguseid ja ligipääsu.',\n    'user_profile' => 'Kasutajaprofiil',\n    'users_add_new' => 'Lisa uus kasutaja',\n    'users_search' => 'Otsi kasutajaid',\n    'users_latest_activity' => 'Viimane tegevus',\n    'users_details' => 'Kasutaja andmed',\n    'users_details_desc' => 'Määra kasutajale nimi ja e-posti aadress. E-posti aadressi kasutatakse rakendusse sisse logimiseks.',\n    'users_details_desc_no_email' => 'Määra kasutajale nimi, mille järgi teised ta ära tunnevad.',\n    'users_role' => 'Kasutaja rollid',\n    'users_role_desc' => 'Vali, millised rollid sellel kasutajal on. Kui talle on valitud mitu rolli, siis nende õigused kombineeritakse ja kasutaja saab kõigi rollide õigused.',\n    'users_password' => 'Kasutaja parool',\n    'users_password_desc' => 'Määra parool, millega rakendusse sisse logida. See peab olema vähemalt 8 tähemärki.',\n    'users_send_invite_text' => 'Sa võid kasutajale saata e-postiga kutse, mis võimaldab neil ise parooli seada. Vastasel juhul määra parool ise.',\n    'users_send_invite_option' => 'Saada e-postiga kutse',\n    'users_external_auth_id' => 'Välise autentimise ID',\n    'users_external_auth_id_desc' => 'Kui kasutusel on väline autentimissüsteem (nagu SAML2, OIDC või LDAP), siis see ID ühendab selle BookStack kasutaja autentimissüsteemi kasutajakontoga. Kui kasutad vaikimisi e-posti põhist autentimist, võid seda välja ignoreerida.',\n    'users_password_warning' => 'Täida allolevad väljad ainult siis, kui soovid selle kasutaja parooli muuta.',\n    'users_system_public' => 'See kasutaja tähistab kõiki külalisi, kes su rakendust vaatavad. Selle kontoga ei saa sisse logida, see määratakse automaatselt.',\n    'users_delete' => 'Kustuta kasutaja',\n    'users_delete_named' => 'Kustuta kasutaja :userName',\n    'users_delete_warning' => 'See kustutab kasutaja nimega \\':userName\\' süsteemist täielikult.',\n    'users_delete_confirm' => 'Kas oled kindel, et soovid selle kasutaja kustutada?',\n    'users_migrate_ownership' => 'Teisalda omandus',\n    'users_migrate_ownership_desc' => 'Vali siin kasutaja, kui soovid talle üle viia kõik selle kasutaja objektid.',\n    'users_none_selected' => 'Kasutaja valimata',\n    'users_edit' => 'Muuda kasutajat',\n    'users_edit_profile' => 'Muuda profiili',\n    'users_avatar' => 'Kasutaja profiilipilt',\n    'users_avatar_desc' => 'Vali sellele kasutajale profiilipilt. See peaks olema umbes 256x256 pikslit.',\n    'users_preferred_language' => 'Eelistatud keel',\n    'users_preferred_language_desc' => 'See valik muudab rakenduse kasutajaliidese keelt. Kasutajate loodud sisu see ei mõjuta.',\n    'users_social_accounts' => 'Sotsiaalmeedia kontod',\n    'users_social_accounts_desc' => 'Vaata selle kasutaja ühendatud sotsiaalmeedia kontode seisundit. Sotsiaalmeedia kontosid saab kasutada süsteemile ligipääsemiseks, lisaks primaarsele autentimissüsteemile.',\n    'users_social_accounts_info' => 'Siin saad seostada teised kontod, millega kiiremini ja lihtsamini sisse logida. Siit konto eemaldamine ei tühista varem lubatud ligipääsu. Ligipääsu saad tühistada ühendatud konto profiili seadetest.',\n    'users_social_connect' => 'Lisa konto',\n    'users_social_disconnect' => 'Eemalda konto',\n    'users_social_status_connected' => 'Ühendatud',\n    'users_social_status_disconnected' => 'Ühendus katkestatud',\n    'users_social_connected' => ':socialAccount konto lisati su profiilile.',\n    'users_social_disconnected' => ':socialAccount konto eemaldati su profiililt.',\n    'users_api_tokens' => 'API tunnused',\n    'users_api_tokens_desc' => 'Lisa ja halda BookStack REST API-ga autentimiseks mõeldud ligipääsutunnuseid. API kasutamise õigused on määratud ksautaja kaudu, kellele ligipääsutunnus kuulub.',\n    'users_api_tokens_none' => 'Sellel kasutajal pole API tunnuseid',\n    'users_api_tokens_create' => 'Lisa tunnus',\n    'users_api_tokens_expires' => 'Aegub',\n    'users_api_tokens_docs' => 'API dokumentatsioon',\n    'users_mfa' => 'Mitmeastmeline autentimine',\n    'users_mfa_desc' => 'Seadista mitmeastmeline autentimine, et oma kasutajakonto turvalisust tõsta.',\n    'users_mfa_x_methods' => ':count meetod seadistatud|:count meetodit seadistatud',\n    'users_mfa_configure' => 'Seadista meetodid',\n\n    // API Tokens\n    'user_api_token_create' => 'Lisa API tunnus',\n    'user_api_token_name' => 'Nimi',\n    'user_api_token_name_desc' => 'Anna oma tunnusele inimloetav nimi, et selle eesmärk paremini meeles püsiks.',\n    'user_api_token_expiry' => 'Kehtiv kuni',\n    'user_api_token_expiry_desc' => 'Määra kuupäev, millal see tunnus aegub. Pärast seda kuupäeva ei saa selle tunnusega enam päringuid teha. Välja tühjaks jätmine määrab aegumiskuupäeva 100 aastat tulevikku.',\n    'user_api_token_create_secret_message' => 'Kohe pärast selle tunnuse loomist genereeritakse ja kuvatakse tunnuse ID ja salajane võti. Võtit kuvatakse ainult ühe korra, seega kopeeri selle väärtus enne jätkamist turvalisse kohta.',\n    'user_api_token' => 'API tunnus',\n    'user_api_token_id' => 'Tunnuse ID',\n    'user_api_token_id_desc' => 'See on API tunnuse süsteemne mittemuudetav identifikaator, mis tuleb API päringutele kaasa panna.',\n    'user_api_token_secret' => 'Tunnuse võti',\n    'user_api_token_secret_desc' => 'See on API tunnuse salajane võti, mis tuleb API päringutele kaasa panna. Seda kuvatakse ainult ühe korra, seega kopeeri see turvalisse kohta.',\n    'user_api_token_created' => 'Tunnus lisatud :timeAgo',\n    'user_api_token_updated' => 'Tunnus muudetud :timeAgo',\n    'user_api_token_delete' => 'Kustuta tunnus',\n    'user_api_token_delete_warning' => 'See kustutab API tunnuse nimega \\':tokenName\\' süsteemist.',\n    'user_api_token_delete_confirm' => 'Kas oled kindel, et soovid selle API tunnuse kustutada?',\n\n    // Webhooks\n    'webhooks' => 'Veebihaagid',\n    'webhooks_index_desc' => 'Veebihaakide abil saab teatud süsteemis toimunud tegevuste ja sündmuste puhul saata andmeid välistele URL-idele, mis võimaldab integreerida väliseid platvorme, nagu sõnumi- või teavitussüsteemid.',\n    'webhooks_x_trigger_events' => ':count sündmus|:count sündmust',\n    'webhooks_create' => 'Lisa uus veebihaak',\n    'webhooks_none_created' => 'Ühtegi veebihaaki pole lisatud.',\n    'webhooks_edit' => 'Muuda veebihaaki',\n    'webhooks_save' => 'Salvesta veebihaak',\n    'webhooks_details' => 'Veebihaagi seaded',\n    'webhooks_details_desc' => 'Sisesta kasutajasõbralik nimi ja POST lõpp-punkt, kuhu veebihaagi andmeid saadetakse.',\n    'webhooks_events' => 'Veebihaagi sündmused',\n    'webhooks_events_desc' => 'Vali kõik sündmused, mille peale seda veebihaaki peaks käivitama.',\n    'webhooks_events_warning' => 'Pea meeles, et veebihaak käivitatakse kõigi valitud sündmuste peale, isegi kui on seatud kohandatud õigused. Hoolitse selle eest, et veebihaak ei teeks avalikuks konfidentsiaalset sisu.',\n    'webhooks_events_all' => 'Kõik süsteemsed sündmused',\n    'webhooks_name' => 'Veebihaagi nimi',\n    'webhooks_timeout' => 'Veebihaagi päringu aegumine (sekundit)',\n    'webhooks_endpoint' => 'Veebihaagi lõpp-punkt',\n    'webhooks_active' => 'Veebihaak aktiivne',\n    'webhook_events_table_header' => 'Sündmused',\n    'webhooks_delete' => 'Kustuta veebihaak',\n    'webhooks_delete_warning' => 'See kustutab veebihaagi nimega \\':webhookName\\' süsteemist.',\n    'webhooks_delete_confirm' => 'Kas oled kindel, et soovid selle veebihaagi kustutada?',\n    'webhooks_format_example' => 'Veebihaagi formaadi näidis',\n    'webhooks_format_example_desc' => 'Veebihaagi andmed saadetakse POST-päringuga seadistatud lõpp-punktile allpool toodud JSON-formaadis. Omadused \"related_item\" ja \"url\" on valikulised ja sõltuvad sündmusest, mis veebihaagi käivitas.',\n    'webhooks_status' => 'Veebihaagi staatus',\n    'webhooks_last_called' => 'Viimati käivitatud:',\n    'webhooks_last_errored' => 'Viimati ebaõnnestunud:',\n    'webhooks_last_error_message' => 'Viimane veateade:',\n\n    // Licensing\n    'licenses' => 'Litsentsid',\n    'licenses_desc' => 'See leht koondab litsentsiinfot BookStack\\'i ja selles kasutatud projektide ja teekide kohta. Paljusid loetletud teekidest võidakse kasutada ainult arenduse kontekstis.',\n    'licenses_bookstack' => 'BookStack\\'i litsents',\n    'licenses_php' => 'PHP teekide litsentsid',\n    'licenses_js' => 'JavaScript teekide litsentsid',\n    'licenses_other' => 'Muud litsentsid',\n    'license_details' => 'Litsentsi detailid',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English (inglise keel)',\n        'ar' => 'العربية (araabia keel)',\n        'bg' => 'Bǎlgarski (bulgaaria keel)',\n        'bs' => 'Bosanski (bosnia keel)',\n        'ca' => 'Català (katalaani keel)',\n        'cs' => 'Česky (tšehhi keel)',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk (taani keel)',\n        'de' => 'Deutsch (saksa keel)',\n        'de_informal' => 'Deutsch (Du) (mitteformaalne saksa keel)',\n        'el' => 'ελληνικά',\n        'es' => 'Español (hispaania keel)',\n        'es_AR' => 'Español Argentina (Argentiina hispaania keel)',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français (prantsuse keel)',\n        'he' => 'עברית (heebrea keel)',\n        'hr' => 'Hrvatski (horvaadi keel)',\n        'hu' => 'Magyar (ungari keel)',\n        'id' => 'Bahasa Indonesia (indoneesia keel)',\n        'it' => 'Italiano (itaalia keel)',\n        'ja' => '日本語 (jaapani keel)',\n        'ko' => '한국어 (korea keel)',\n        'lt' => 'Lietuvių Kalba (leedu keel)',\n        'lv' => 'Latviešu Valoda (läti keel)',\n        'nb' => 'Norsk (Bokmål) (norra keel)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands (hollandi keel)',\n        'pl' => 'Polski (poola keel)',\n        'pt' => 'Português (portugali keel)',\n        'pt_BR' => 'Português do Brasil (Brasiilia portugali keel)',\n        'ro' => 'Română',\n        'ru' => 'Русский (vene keel)',\n        'sk' => 'Slovensky',\n        'sl' => 'Sloveenia',\n        'sv' => 'Rootsi',\n        'tr' => 'Türgi',\n        'uk' => 'Ukraina',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Vietnami',\n        'zh_CN' => 'Hiina (lihtsustatud)',\n        'zh_TW' => 'Hiina (traditsiooniline)',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/et/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute peab olema aktsepteeritud.',\n    'active_url'           => ':attribute ei ole kehtiv URL.',\n    'after'                => ':attribute peab olema kuupäev pärast :date.',\n    'alpha'                => ':attribute võib sisaldada ainult tähti.',\n    'alpha_dash'           => ':attribute võib sisaldada ainult tähti, numbreid, sidekriipse ja alakriipse.',\n    'alpha_num'            => ':attribute võib sisaldada ainult tähti ja numbreid.',\n    'array'                => ':attribute peab olema massiiv.',\n    'backup_codes'         => 'Kood ei ole korrektne või on seda juba kasutatud.',\n    'before'               => ':attribute peab olema kuupäev enne :date.',\n    'between'              => [\n        'numeric' => ':attribute peab jääma vahemikku :min ja :max.',\n        'file'    => ':attribute peab olema :min ja :max kilobaidi vahel.',\n        'string'  => ':attribute peab olema :min ja :max tähemärgi vahel.',\n        'array'   => ':attribute peab olema :min ja :max elemendi vahel.',\n    ],\n    'boolean'              => ':attribute peab olema tõene või väär.',\n    'confirmed'            => ':attribute kinnitus ei kattu.',\n    'date'                 => ':attribute ei ole kehtiv kuupäev.',\n    'date_format'          => ':attribute ei ühti formaadiga :format.',\n    'different'            => ':attribute ja :other peavad olema erinevad.',\n    'digits'               => ':attribute peab olema :digits-kohaline arv.',\n    'digits_between'       => ':attribute peab olema :min ja :max numbri vahel.',\n    'email'                => ':attribute peab olema kehtiv e-posti aadress.',\n    'ends_with' => ':attribute lõpus peab olema üks järgmistest väärtustest: :values',\n    'file'                 => ':attribute peab olema sobiv fail.',\n    'filled'               => ':attribute väli on kohustuslik.',\n    'gt'                   => [\n        'numeric' => ':attribute peab olema suurem kui :value.',\n        'file'    => ':attribute peab olema suurem kui :value kilobaiti.',\n        'string'  => ':attribute peab sisaldama rohkem kui :value tähemärki.',\n        'array'   => ':attribute peab sisaldama rohkem kui :value elementi.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute peab olema suurem kui või võrdne :value.',\n        'file'    => ':attribute peab olema :value kilobaiti või rohkem.',\n        'string'  => ':attribute peab sisaldama :value või rohkem tähemärki.',\n        'array'   => ':attribute peab sisaldama :value või rohkem elementi.',\n    ],\n    'exists'               => 'Valitud :attribute on vigane.',\n    'image'                => ':attribute peab olema pildifail.',\n    'image_extension'      => ':attribute peab olema lubatud ja toetatud pildiformaadis.',\n    'in'                   => 'Valitud :attribute on vigane.',\n    'integer'              => ':attribute peab olema täisarv.',\n    'ip'                   => ':attribute peab olema kehtiv IP-aadress.',\n    'ipv4'                 => ':attribute peab olema kehtiv IPv4 aadress.',\n    'ipv6'                 => ':attribute peab olema kehtiv IPv6 aadress.',\n    'json'                 => ':attribute peab olema kehtiv JSON-vormingus tekst.',\n    'lt'                   => [\n        'numeric' => ':attribute peab olema väiksem kui :value.',\n        'file'    => ':attribute peab olema väiksem kui :value kilobaiti.',\n        'string'  => ':attribute peab sisaldama vähem kui :value tähemärki.',\n        'array'   => ':attribute peab sisaldama vähem kui :value elementi.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute peab olema :value või vähem.',\n        'file'    => ':attribute peab olema :value kilobaiti või vähem.',\n        'string'  => ':attribute peab sisaldama :value või vähem tähemärki.',\n        'array'   => ':attribute ei tohi sisaldada rohkem kui :value elementi.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute ei tohi olla suurem kui :max.',\n        'file'    => ':attribute ei tohi olla suurem kui :max kilobaiti.',\n        'string'  => ':attribute ei tohi sisaldada rohkem kui :max tähemärki.',\n        'array'   => ':attribute ei tohi sisaldada rohkem kui :max elementi.',\n    ],\n    'mimes'                => ':attribute peab olema seda tüüpi fail: :values.',\n    'min'                  => [\n        'numeric' => ':attribute peab olema vähemalt :min.',\n        'file'    => ':attribute peab olema vähemalt :min kilobaiti.',\n        'string'  => ':attribute peab sisaldama vähemalt :min tähemärki.',\n        'array'   => ':attribute peab sisaldama vähemalt :min elementi.',\n    ],\n    'not_in'               => 'Valitud :attribute on vigane.',\n    'not_regex'            => ':attribute on vigases formaadis.',\n    'numeric'              => ':attribute peab olema arv.',\n    'regex'                => ':attribute on vigases formaadis.',\n    'required'             => ':attribute on kohustuslik.',\n    'required_if'          => ':attribute on kohustuslik, kui :other on :value.',\n    'required_with'        => ':attribute on kohustuslik, kui :values on olemas.',\n    'required_with_all'    => ':attribute on kohustuslik, kui :values on olemas.',\n    'required_without'     => ':attribute on kohustuslik, kui :values ei ole olemas.',\n    'required_without_all' => ':attribute on kohustuslik, kui :values on valimata.',\n    'same'                 => ':attribute ja :other peavad klappima.',\n    'safe_url'             => 'Link ei pruugi olla turvaline.',\n    'size'                 => [\n        'numeric' => ':attribute peab olema :size.',\n        'file'    => ':attribute peab olema :size kilobaiti.',\n        'string'  => ':attribute peab sisaldama :size tähemärki.',\n        'array'   => ':attribute peab sisaldama :size elemente.',\n    ],\n    'string'               => ':attribute peab olema string.',\n    'timezone'             => ':attribute peab olema kehtiv ajavöönd.',\n    'totp'                 => 'Kood ei ole korrektne või on aegunud.',\n    'unique'               => ':attribute on juba võetud.',\n    'url'                  => ':attribute on vigases formaadis.',\n    'uploaded'             => 'Faili üleslaadimine ebaõnnestus. Server ei pruugi sellise suurusega faile vastu võtta.',\n\n    'zip_file' => ':attribute peab viitama failile ZIP-arhiivi sees.',\n    'zip_file_size' => 'Fail :attribute ei tohi olla suurem kui :size MB.',\n    'zip_file_mime' => ':attribute peab viitama :validTypes tüüpi failile, leiti :foundType.',\n    'zip_model_expected' => 'Oodatud andmete asemel leiti \":type\".',\n    'zip_unique' => ':attribute peab olema ZIP-arhiivi piires objekti tüübile unikaalne.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Parooli kinnitus on nõutud',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/eu/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'sortu orria',\n    'page_create_notification'    => 'Orria ongi sortu da',\n    'page_update'                 => 'eguneratu orrialdea',\n    'page_update_notification'    => 'Orria egoki egunerauta',\n    'page_delete'                 => 'ezabatu orrialdea',\n    'page_delete_notification'    => 'Ondo ezabatu da',\n    'page_restore'                => 'leheneratu orria',\n    'page_restore_notification'   => 'Dokumentua behar bezala leheneratuta',\n    'page_move'                   => 'mugitu orrialdea',\n    'page_move_notification'      => 'Page successfully moved',\n\n    // Chapters\n    'chapter_create'              => 'kapitulua sortu',\n    'chapter_create_notification' => 'Kapitulua egoki sortuta',\n    'chapter_update'              => 'eguneratu kapitulua',\n    'chapter_update_notification' => 'Kapitulua egoki eguneratuta',\n    'chapter_delete'              => 'kapitulua ezabatu',\n    'chapter_delete_notification' => 'Kapitulua egoki ezabatua',\n    'chapter_move'                => 'kapitulua mugituta',\n    'chapter_move_notification' => 'Chapter successfully moved',\n\n    // Books\n    'book_create'                 => 'liburua sortuta',\n    'book_create_notification'    => 'Liburua ongi sortu da',\n    'book_create_from_chapter'              => 'bihurtu kapitulua liburu',\n    'book_create_from_chapter_notification' => 'Kapitulua ongi bilakatu da liburu',\n    'book_update'                 => 'liburua eguneratuta',\n    'book_update_notification'    => 'Liburua egoki eguneratua',\n    'book_delete'                 => 'liburua ezabatua',\n    'book_delete_notification'    => 'Liburua egoki ezabatua',\n    'book_sort'                   => 'liburua sailkatua',\n    'book_sort_notification'      => 'Liburua ongi bersailaktu da',\n\n    // Bookshelves\n    'bookshelf_create'            => 'apalategia sortuta',\n    'bookshelf_create_notification'    => 'Apalategia egoki sortuta',\n    'bookshelf_create_from_book'    => 'liburua apalategi bihurtuta',\n    'bookshelf_create_from_book_notification'    => 'Kapitulua ongi bilakatu da liburu',\n    'bookshelf_update'                 => 'apalategia eguneratuta',\n    'bookshelf_update_notification'    => 'Erabiltzailea egoki eguneratua',\n    'bookshelf_delete'                 => 'apalategia ezabatua',\n    'bookshelf_delete_notification'    => 'Apalategia egoki ezabatua',\n\n    // Revisions\n    'revision_restore' => 'restored revision',\n    'revision_delete' => 'deleted revision',\n    'revision_delete_notification' => 'Revision successfully deleted',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" zure gogoetara gehitua izan da',\n    'favourite_remove_notification' => '\":name\" zure gogokoetatik ezabatua izan da',\n\n    // Watching\n    'watch_update_level_notification' => 'Watch preferences successfully updated',\n\n    // Auth\n    'auth_login' => 'logged in',\n    'auth_register' => 'registered as new user',\n    'auth_password_reset_request' => 'requested user password reset',\n    'auth_password_reset_update' => 'reset user password',\n    'mfa_setup_method' => 'configured MFA method',\n    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',\n    'mfa_remove_method' => 'removed MFA method',\n    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',\n\n    // Settings\n    'settings_update' => 'updated settings',\n    'settings_update_notification' => 'Settings successfully updated',\n    'maintenance_action_run' => 'ran maintenance action',\n\n    // Webhooks\n    'webhook_create' => 'sortu webhook',\n    'webhook_create_notification' => 'Webhook egoki sortua',\n    'webhook_update' => 'webhook eguneratua',\n    'webhook_update_notification' => 'Webhook egoki eguneratua',\n    'webhook_delete' => 'webhook ezabatua',\n    'webhook_delete_notification' => 'Webhook egoki ezabatua',\n\n    // Imports\n    'import_create' => 'created import',\n    'import_create_notification' => 'Import successfully uploaded',\n    'import_run' => 'updated import',\n    'import_run_notification' => 'Content successfully imported',\n    'import_delete' => 'deleted import',\n    'import_delete_notification' => 'Import successfully deleted',\n\n    // Users\n    'user_create' => 'created user',\n    'user_create_notification' => 'User successfully created',\n    'user_update' => 'updated user',\n    'user_update_notification' => 'Erabiltzailea egoki eguneratua',\n    'user_delete' => 'deleted user',\n    'user_delete_notification' => 'Erabiltzailea egoki ezabatua',\n\n    // API Tokens\n    'api_token_create' => 'created API token',\n    'api_token_create_notification' => 'API token successfully created',\n    'api_token_update' => 'updated API token',\n    'api_token_update_notification' => 'API token successfully updated',\n    'api_token_delete' => 'deleted API token',\n    'api_token_delete_notification' => 'API token successfully deleted',\n\n    // Roles\n    'role_create' => 'created role',\n    'role_create_notification' => 'Role successfully created',\n    'role_update' => 'updated role',\n    'role_update_notification' => 'Role successfully updated',\n    'role_delete' => 'deleted role',\n    'role_delete_notification' => 'Role successfully deleted',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'emptied recycle bin',\n    'recycle_bin_restore' => 'restored from recycle bin',\n    'recycle_bin_destroy' => 'removed from recycle bin',\n\n    // Comments\n    'commented_on'                => 'iruzkinak',\n    'comment_create'              => 'added comment',\n    'comment_update'              => 'updated comment',\n    'comment_delete'              => 'deleted comment',\n\n    // Sort Rules\n    'sort_rule_create' => 'created sort rule',\n    'sort_rule_create_notification' => 'Sort rule successfully created',\n    'sort_rule_update' => 'updated sort rule',\n    'sort_rule_update_notification' => 'Sort rule successfully updated',\n    'sort_rule_delete' => 'deleted sort rule',\n    'sort_rule_delete_notification' => 'Sort rule successfully deleted',\n\n    // Other\n    'permissions_update'          => 'eguneratu baimenak',\n];\n"
  },
  {
    "path": "lang/eu/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Kredentzial hauek ez dira egokiak.',\n    'throttle' => 'Login saiakera kopurua pasa duzu. Mesedez, saiatu berriz :seconds segundu barru.',\n\n    // Login & Register\n    'sign_up' => 'Izena eman',\n    'log_in' => 'Saioa hasi',\n    'log_in_with' => ':socialDriver bidez hasi saioa',\n    'sign_up_with' => ':socialDriver bidez izena eman',\n    'logout' => 'Logout',\n\n    'name' => 'Izena',\n    'username' => 'Erabiltzaile izena',\n    'email' => 'Eposta',\n    'password' => 'Pasahitza',\n    'password_confirm' => 'Berretsi pasahitza',\n    'password_hint' => 'Gutxienez 8 karaktere izan behar ditu',\n    'forgot_password' => 'Pasahitza ahaztu duzu?',\n    'remember_me' => 'Gogoratu',\n    'ldap_email_hint' => 'Mesedez, email bat sartu kontu hau erabiltzeko.',\n    'create_account' => 'Sortu kontua',\n    'already_have_account' => 'Dagoeneko kontu bat al duzu?',\n    'dont_have_account' => 'Ez duzu konturik?',\n    'social_login' => 'Kanpoko logina',\n    'social_registration' => 'Erregistroa kanpotik',\n    'social_registration_text' => 'Erregistratu eta saioa hasi beste zerbitzu batekin.',\n\n    'register_thanks' => 'Mila esker erregistroagatik!',\n    'register_confirm' => 'Mesedez, begiratu posta elektronikoari eta klik egin baieztapen botoia :appName sartzeko.',\n    'registrations_disabled' => 'Erregistroa ez dago gaituta',\n    'registration_email_domain_invalid' => 'Posta elektronikoko domeinu hori ez dago eskuragarri aplikazio honetarako',\n    'register_success' => 'Eskerrik asko izen emateagatik! Orain izena emanda eta saioa hasita zaude.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Attempting Login',\n    'auto_init_starting_desc' => 'We\\'re contacting your authentication system to start the login process. If there\\'s no progress after 5 seconds you can try clicking the link below.',\n    'auto_init_start_link' => 'Proceed with authentication',\n\n    // Password Reset\n    'reset_password' => 'Pasahitza berrezarri',\n    'reset_password_send_instructions' => 'Sartu zure posta elektronikoa eta posta elektroniko bat bidaliko dizute pasahitza berritzeko esteka batekin.',\n    'reset_password_send_button' => 'Bidali Reset Link',\n    'reset_password_sent' => 'Posta elektronikora bidaliko da posta elektronikoko :email helbide hori sisteman aurkitzen bada.',\n    'reset_password_success' => 'Zure pasahitza egoki berrezarri da.',\n    'email_reset_subject' => 'Berrezarri zure :appName pasahitza',\n    'email_reset_text' => 'E-mail hau jasotzen ari zara, zure konturako pasahitz eskaera jaso dugulako.',\n    'email_reset_not_requested' => 'Zuk ez baduzu pasahitza berresartzea eskatu, ez duzu ezer egin beharrik.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Baieztatu zure emaila hemen :appName',\n    'email_confirm_greeting' => 'Eskerrik asko :appName proiektuan batzeagatik!',\n    'email_confirm_text' => 'Mesedez, baieztatu zure email helbidea beheko botoi hau kilkatuz:',\n    'email_confirm_action' => 'Berrespen e-maila',\n    'email_confirm_send_error' => 'Posta elektronikoaren baieztapenak behar da, baina sistemak ezin izan du posta elektronikoa bidali. Administratzailearekin harremanetan jarri email ezarpenak ongi dauden baieztatzeko.',\n    'email_confirm_success' => 'Zure posta elektronikoa baieztatu da! Helbide elektroniko hau erabili dezakezu saioa hasteko.',\n    'email_confirm_resent' => 'Eragiketa baieztatzeko email bat bidali dizugu. Mesedez, begiratu zure posta elektronikoa.',\n    'email_confirm_thanks' => 'Thanks for confirming!',\n    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the \"Continue\" link below to proceed.',\n\n    'email_not_confirmed' => 'Email helbidea ez da baieztatu',\n    'email_not_confirmed_text' => 'Your email address has not yet been confirmed.',\n    'email_not_confirmed_click_link' => 'Please click the link in the email that was sent shortly after you registered.',\n    'email_not_confirmed_resend' => 'If you cannot find the email you can re-send the confirmation email by submitting the form below.',\n    'email_not_confirmed_resend_button' => 'Birbidali baieztapen mezua',\n\n    // User Invite\n    'user_invite_email_subject' => ':appName sartzera gonbidatu zaituzte!',\n    'user_invite_email_greeting' => 'An account has been created for you on :appName.',\n    'user_invite_email_text' => 'Click the button below to set an account password and gain access:',\n    'user_invite_email_action' => 'Kontuko pasahitzam jarri',\n    'user_invite_page_welcome' => 'Ongi etorri :appName -era!',\n    'user_invite_page_text' => 'To finalise your account and gain access you need to set a password which will be used to log-in to :appName on future visits.',\n    'user_invite_page_confirm_button' => 'Berretsi pasahitza',\n    'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Setup Multi-Factor Authentication',\n    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'mfa_setup_configured' => 'Dagoeneko konfiguratuta',\n    'mfa_setup_reconfigure' => 'Berrezarri',\n    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',\n    'mfa_setup_action' => 'Konfigurazioa',\n    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',\n    'mfa_option_totp_title' => 'Aplikazio mugikorra',\n    'mfa_option_totp_desc' => 'To use multi-factor authentication you\\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Backup Codes',\n    'mfa_option_backup_codes_desc' => 'Generates a set of one-time-use backup codes which you\\'ll enter on login to verify your identity. Make sure to store these in a safe & secure place.',\n    'mfa_gen_confirm_and_enable' => 'Baieztatu eta gaitu',\n    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',\n    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\\'ll be able to use one of the codes as a second authentication mechanism.',\n    'mfa_gen_backup_codes_download' => 'Download Codes',\n    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',\n    'mfa_gen_totp_title' => 'Mobile App Setup',\n    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',\n    'mfa_gen_totp_verify_setup' => 'Verify Setup',\n    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',\n    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',\n    'mfa_verify_access' => 'Verify Access',\n    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\\'re granted access. Verify using one of your configured methods to continue.',\n    'mfa_verify_no_methods' => 'No Methods Configured',\n    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\\'ll need to set up at least one method before you gain access.',\n    'mfa_verify_use_totp' => 'Verify using a mobile app',\n    'mfa_verify_use_backup_codes' => 'Verify using a backup code',\n    'mfa_verify_backup_code' => 'Backup Code',\n    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',\n    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',\n    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',\n    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',\n];\n"
  },
  {
    "path": "lang/eu/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Ezeztatu',\n    'close' => 'Close',\n    'confirm' => 'Berretsi',\n    'back' => 'Itzuli',\n    'save' => 'Gorde',\n    'continue' => 'Jarraitu',\n    'select' => 'Aukeratu',\n    'toggle_all' => 'Txandakatu denak',\n    'more' => 'Gehiago',\n\n    // Form Labels\n    'name' => 'Izena',\n    'description' => 'Deskribapena',\n    'role' => 'Rola',\n    'cover_image' => 'Azaleko irudia',\n    'cover_image_description' => 'This image should be approximately 440x250px although it will be flexibly scaled & cropped to fit the user interface in different scenarios as required, so actual dimensions for display will differ.',\n\n    // Actions\n    'actions' => 'Ekintzak',\n    'view' => 'Ikusi',\n    'view_all' => 'Ikusi denak',\n    'new' => 'New',\n    'create' => 'Sortu',\n    'update' => 'Eguneratu',\n    'edit' => 'Editatu',\n    'archive' => 'Archive',\n    'unarchive' => 'Un-Archive',\n    'sort' => 'Ordenatu',\n    'move' => 'Mugitu',\n    'copy' => 'Kopiatu',\n    'reply' => 'Erantzun',\n    'delete' => 'Ezabatu',\n    'delete_confirm' => 'Ezabatzea baieztatu',\n    'search' => 'Bilatu',\n    'search_clear' => 'Bilaketa testua garbitu',\n    'reset' => 'Berrezarri',\n    'remove' => 'Ezabatu',\n    'add' => 'Gehitu',\n    'configure' => 'Konfiguratu',\n    'manage' => 'Manage',\n    'fullscreen' => 'Pantaila osoa',\n    'favourite' => 'Gogokoa',\n    'unfavourite' => 'Desatsegina',\n    'next' => 'Hurrengoa',\n    'previous' => 'Aurrekoa',\n    'filter_active' => 'Iragazki aktiboa:',\n    'filter_clear' => 'Iragazkia garbitu',\n    'download' => 'Download',\n    'open_in_tab' => 'Open in Tab',\n    'open' => 'Open',\n\n    // Sort Options\n    'sort_options' => 'Ordenatzeko aukerak',\n    'sort_direction_toggle' => 'Ordenatzeko aukerak erakutsi',\n    'sort_ascending' => 'Ordenatu (behetik gorantz)',\n    'sort_descending' => 'Ordenatu (goitik beherantz)',\n    'sort_name' => 'Izena',\n    'sort_default' => 'Lehenetsia',\n    'sort_created_at' => 'Sorrera data',\n    'sort_updated_at' => 'Eguneratze data',\n\n    // Misc\n    'deleted_user' => 'Erabiltzailea ezabatu',\n    'no_activity' => 'Ekintzarik ez erakusteko',\n    'no_items' => 'Ez dago elementurik eskuragarri',\n    'back_to_top' => 'Itzuli gora',\n    'skip_to_main_content' => 'Joan eduki nagusira',\n    'toggle_details' => 'Ireki xehetasunak',\n    'toggle_thumbnails' => 'Ireki azaleko irudia',\n    'details' => 'Xehetasunak',\n    'grid_view' => 'Lauki sare modua',\n    'list_view' => 'Zerrenda Ikuspegia',\n    'default' => 'Lehenetsia',\n    'breadcrumb' => 'Nabigazioko aztarnak',\n    'status' => 'Egoera',\n    'status_active' => 'Aktiboa',\n    'status_inactive' => 'Inaktibo',\n    'never' => 'Inoiz ez',\n    'none' => 'Bat ere ez',\n\n    // Header\n    'homepage' => 'Homepage',\n    'header_menu_expand' => 'Zabaldu goiburuko menua',\n    'profile_menu' => 'Perfileko menua',\n    'view_profile' => 'Ikusi profila',\n    'edit_profile' => 'Editatu profila',\n    'dark_mode' => 'Modu iluna',\n    'light_mode' => 'Modu argia',\n    'global_search' => 'Global Search',\n\n    // Layout tabs\n    'tab_info' => 'Info',\n    'tab_info_label' => 'Tab: Erakutsi bigarren mailako informazioa',\n    'tab_content' => 'Edukia',\n    'tab_content_label' => 'Tab: Erakutsi eduki nagusia',\n\n    // Email Content\n    'email_action_help' => 'Arazoak badituzu \":actionText\" botoiarekin, kopiatu eta itsatsi URL hau nabigatzailean:',\n    'email_rights' => 'Eskubide guztiak erreserbatuta',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Pribatutasun politika',\n    'terms_of_service' => 'Zerbitzu-baldintzak',\n\n    // OpenSearch\n    'opensearch_description' => 'Search :appName',\n];\n"
  },
  {
    "path": "lang/eu/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Irudia aukeratu',\n    'image_list' => 'Image List',\n    'image_details' => 'Image Details',\n    'image_upload' => 'Upload Image',\n    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',\n    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the \"Upload Image\" button above.',\n    'image_all' => 'Guztiak',\n    'image_all_title' => 'Irudi guztiak ikusi',\n    'image_book_title' => 'Liburu honetan igotako irudiak ikusi',\n    'image_page_title' => 'Orrialde honetan igotako irudiak ikusi',\n    'image_search_hint' => 'Izenarekin bilatu',\n    'image_uploaded' => 'Igoera data :uploadedDate',\n    'image_uploaded_by' => 'Uploaded by :userName',\n    'image_uploaded_to' => 'Uploaded to :pageLink',\n    'image_updated' => 'Updated :updateDate',\n    'image_load_more' => 'Kargatu gehiago',\n    'image_image_name' => 'Irudiaren izena',\n    'image_delete_used' => 'Irudi hau honako orrialdetan erabili da.',\n    'image_delete_confirm_text' => 'Ziur irudi hau ezabatu nahi duzula?',\n    'image_select_image' => 'Irudia aukeratu',\n    'image_dropzone' => 'Irudiak bota edo klikatu hemen igotzeko',\n    'image_dropzone_drop' => 'Drop images here to upload',\n    'images_deleted' => 'Ezabatutako irudiak',\n    'image_preview' => 'Irudiaren aurrebista',\n    'image_upload_success' => 'Irudia zuzen eguneratu da',\n    'image_update_success' => 'Irudiaren xehetasunak egioki eguneratu dira',\n    'image_delete_success' => 'Irudia ondo ezabatu da',\n    'image_replace' => 'Replace Image',\n    'image_replace_success' => 'Image file successfully updated',\n    'image_rebuild_thumbs' => 'Regenerate Size Variations',\n    'image_rebuild_thumbs_success' => 'Image size variations successfully rebuilt!',\n\n    // Code Editor\n    'code_editor' => 'Kodea editatu',\n    'code_language' => 'Kode hizkuntza',\n    'code_content' => 'Kode edukia',\n    'code_session_history' => 'Saioaren historia',\n    'code_save' => 'Kodea gorde',\n];\n"
  },
  {
    "path": "lang/eu/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Orokorra',\n    'advanced' => 'Aurreratua',\n    'none' => 'Bat ere ez',\n    'cancel' => 'Ezeztatu',\n    'save' => 'Gorde',\n    'close' => 'Itxi',\n    'apply' => 'Apply',\n    'undo' => 'Desegin',\n    'redo' => 'Berregin',\n    'left' => 'Ezkerra',\n    'center' => 'Erdian',\n    'right' => 'Eskubi',\n    'top' => 'Goi',\n    'middle' => 'Erdia',\n    'bottom' => 'Behean',\n    'width' => 'Zabalera',\n    'height' => 'Altuera',\n    'More' => 'Gehiago',\n    'select' => 'Aukeratu...',\n\n    // Toolbar\n    'formats' => 'Formatuak',\n    'header_large' => 'Goiburu luzea',\n    'header_medium' => 'Goiburu ertaina',\n    'header_small' => 'Goiburu txikia',\n    'header_tiny' => 'Tiny goiburua',\n    'paragraph' => 'Paragrafoa',\n    'blockquote' => 'Aipu blokea',\n    'inline_code' => 'Kodea',\n    'callouts' => 'Legendak',\n    'callout_information' => 'Informazioa',\n    'callout_success' => 'Eginda',\n    'callout_warning' => 'Kontuz',\n    'callout_danger' => 'Arriskua',\n    'bold' => 'Lodia',\n    'italic' => 'Etzana',\n    'underline' => 'Azpimarratua',\n    'strikethrough' => 'Marratua',\n    'superscript' => 'Gain-eskripta',\n    'subscript' => 'Azpi-script',\n    'text_color' => 'Testuaren kolorea',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Kolore pertsonalizatua',\n    'remove_color' => 'Kolorea ezabatu',\n    'background_color' => 'Atzeko planoaren kolorea',\n    'align_left' => 'Lerrokatu ezkerrean',\n    'align_center' => 'Lerrokatu erdian',\n    'align_right' => 'Lerrokatu eskuinean',\n    'align_justify' => 'Justifikatuta',\n    'list_bullet' => 'Buletdun zerrenda',\n    'list_numbered' => 'Zenbakitutako zerrenda',\n    'list_task' => 'Zereginen zerrenda',\n    'indent_increase' => 'Handitu koska',\n    'indent_decrease' => 'Txikitu koska',\n    'table' => 'Taula',\n    'insert_image' => 'Irudia txertatu',\n    'insert_image_title' => 'Aldatu/Txertatu irudia',\n    'insert_link' => 'Txertatu/aldatu esteka',\n    'insert_link_title' => 'Txertatu/Aldatu esteka',\n    'insert_horizontal_line' => 'Txertatu linea horizontala',\n    'insert_code_block' => 'Txertatu kode-blokea',\n    'edit_code_block' => 'Edit code block',\n    'insert_drawing' => 'Txertatu marrazki berria',\n    'drawing_manager' => 'Marrazki kudeaketa',\n    'insert_media' => 'Txertatu/aldatu media',\n    'insert_media_title' => 'Aldatu/Txertatu irudia',\n    'clear_formatting' => 'Garbitu formatua',\n    'source_code' => 'Iturburu kodea',\n    'source_code_title' => 'Iturburu kodea',\n    'fullscreen' => 'Pantaila osoa',\n    'image_options' => 'Irudiaren aukerak',\n\n    // Tables\n    'table_properties' => 'Taularen propietateak',\n    'table_properties_title' => 'Taularen propietateak',\n    'delete_table' => 'Ezabatu taula',\n    'table_clear_formatting' => 'Clear table formatting',\n    'resize_to_contents' => 'Resize to contents',\n    'row_header' => 'Row header',\n    'insert_row_before' => 'Insert row before',\n    'insert_row_after' => 'Insert row after',\n    'delete_row' => 'Delete row',\n    'insert_column_before' => 'Insert column before',\n    'insert_column_after' => 'Insert column after',\n    'delete_column' => 'Delete column',\n    'table_cell' => 'Cell',\n    'table_row' => 'Row',\n    'table_column' => 'Column',\n    'cell_properties' => 'Cell properties',\n    'cell_properties_title' => 'Cell Properties',\n    'cell_type' => 'Cell type',\n    'cell_type_cell' => 'Cell',\n    'cell_scope' => 'Scope',\n    'cell_type_header' => 'Header cell',\n    'merge_cells' => 'Merge cells',\n    'split_cell' => 'Split cell',\n    'table_row_group' => 'Row Group',\n    'table_column_group' => 'Column Group',\n    'horizontal_align' => 'Horizontal align',\n    'vertical_align' => 'Vertical align',\n    'border_width' => 'Border width',\n    'border_style' => 'Border style',\n    'border_color' => 'Border color',\n    'row_properties' => 'Row properties',\n    'row_properties_title' => 'Row Properties',\n    'cut_row' => 'Cut row',\n    'copy_row' => 'Copy row',\n    'paste_row_before' => 'Paste row before',\n    'paste_row_after' => 'Paste row after',\n    'row_type' => 'Row type',\n    'row_type_header' => 'Header',\n    'row_type_body' => 'Body',\n    'row_type_footer' => 'Footer',\n    'alignment' => 'Alignment',\n    'cut_column' => 'Cut column',\n    'copy_column' => 'Copy column',\n    'paste_column_before' => 'Paste column before',\n    'paste_column_after' => 'Paste column after',\n    'cell_padding' => 'Cell padding',\n    'cell_spacing' => 'Cell spacing',\n    'caption' => 'Caption',\n    'show_caption' => 'Show caption',\n    'constrain' => 'Constrain proportions',\n    'cell_border_solid' => 'Solid',\n    'cell_border_dotted' => 'Dotted',\n    'cell_border_dashed' => 'Dashed',\n    'cell_border_double' => 'Double',\n    'cell_border_groove' => 'Groove',\n    'cell_border_ridge' => 'Ridge',\n    'cell_border_inset' => 'Inset',\n    'cell_border_outset' => 'Outset',\n    'cell_border_none' => 'None',\n    'cell_border_hidden' => 'Hidden',\n\n    // Images, links, details/summary & embed\n    'source' => 'Source',\n    'alt_desc' => 'Alternative description',\n    'embed' => 'Embed',\n    'paste_embed' => 'Paste your embed code below:',\n    'url' => 'URL',\n    'text_to_display' => 'Text to display',\n    'title' => 'Title',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Open link',\n    'open_link_in' => 'Open link in...',\n    'open_link_current' => 'Current window',\n    'open_link_new' => 'New window',\n    'remove_link' => 'Remove link',\n    'insert_collapsible' => 'Insert collapsible block',\n    'collapsible_unwrap' => 'Unwrap',\n    'edit_label' => 'Edit label',\n    'toggle_open_closed' => 'Toggle open/closed',\n    'collapsible_edit' => 'Edit collapsible block',\n    'toggle_label' => 'Toggle label',\n\n    // About view\n    'about' => 'About the editor',\n    'about_title' => 'About the WYSIWYG Editor',\n    'editor_license' => 'Editor License & Copyright',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under the MIT license.',\n    'editor_tiny_license_link' => 'The copyright and license details of TinyMCE can be found here.',\n    'save_continue' => 'Save Page & Continue',\n    'callouts_cycle' => '(Keep pressing to toggle through types)',\n    'link_selector' => 'Link to content',\n    'shortcuts' => 'Shortcuts',\n    'shortcut' => 'Shortcut',\n    'shortcuts_intro' => 'The following shortcuts are available in the editor:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Description',\n];\n"
  },
  {
    "path": "lang/eu/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Duela gutxi sortuak',\n    'recently_created_pages' => 'Berriki sortutako Orrialdeak',\n    'recently_updated_pages' => 'Berriki aldatutako Orrialdeak',\n    'recently_created_chapters' => 'Berriki sortutako Kapituluak',\n    'recently_created_books' => 'Berriki sortutako Liburuak',\n    'recently_created_shelves' => 'Berriki sortutako Apalak',\n    'recently_update' => 'Azken eguneraketak',\n    'recently_viewed' => 'Oraintsu ikusiak',\n    'recent_activity' => 'Duela gutxiko Jarduera',\n    'create_now' => 'Sortu bat orain',\n    'revisions' => 'Berrikuspenak',\n    'meta_revision' => '#:revisionCount Berrikuspen',\n    'meta_created' => 'Sortua :timeLength',\n    'meta_created_name' => ':timeLength sortua. Erabiltzailea :user',\n    'meta_updated' => 'Aldatua :timeLength',\n    'meta_updated_name' => ':timeLength aldatuta. Erabiltzailea :user',\n    'meta_owned_name' => ':user da jabea',\n    'meta_reference_count' => 'Referenced by :count item|Referenced by :count items',\n    'entity_select' => 'Aukeratutako entitatea',\n    'entity_select_lack_permission' => 'You don\\'t have the required permissions to select this item',\n    'images' => 'Irudiak',\n    'my_recent_drafts' => 'Nire azken zirriborroak',\n    'my_recently_viewed' => 'Nik Ikusitako azkenak',\n    'my_most_viewed_favourites' => 'Nire gehien ikusitako gogokoak',\n    'my_favourites' => 'Nire Gogokoenak',\n    'no_pages_viewed' => 'Ez daukazu ikusiriko orririk',\n    'no_pages_recently_created' => 'Ez da orrialderik sortu azkenaldian',\n    'no_pages_recently_updated' => 'Ez da orrialderik aldatu azkenaldian',\n    'export' => 'Esportatu',\n    'export_html' => 'Daukan web artxiboa',\n    'export_pdf' => 'PDF fitxategia',\n    'export_text' => 'Testu lauko fitxategiak',\n    'export_md' => 'Markdown fitxategia',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Default Page Template',\n    'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',\n    'default_template_select' => 'Select a template page',\n    'import' => 'Import',\n    'import_validate' => 'Validate Import',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'Select ZIP file to upload',\n    'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',\n    'import_pending' => 'Pending Imports',\n    'import_pending_none' => 'No imports have been started.',\n    'import_continue' => 'Continue Import',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Run Import',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Uploaded by',\n    'import_location' => 'Import Location',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'Import Errors',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Baimenak',\n    'permissions_desc' => 'Ezarri baimenak hemen, erabiltzaileen rolek ematen dituzten baimenak gainidazteko.',\n    'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.',\n    'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.',\n    'permissions_save' => 'Gorde baimenak',\n    'permissions_owner' => 'Jabea',\n    'permissions_role_everyone_else' => 'Everyone Else',\n    'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',\n    'permissions_role_override' => 'Gainidatzi baimenak rol honi',\n    'permissions_inherit_defaults' => 'Inherit defaults',\n\n    // Search\n    'search_results' => 'Bilaketaren emaitzak',\n    'search_total_results_found' => ':count emaitza aurkitu dira|:count emaitza aurkitu dira guztira',\n    'search_clear' => 'Bilaketa testua garbitu',\n    'search_no_pages' => 'Ez da orririk aurkitu zure bilaketan',\n    'search_for_term' => 'Bilatu honen arabera :term',\n    'search_more' => 'Emaitza gehiago',\n    'search_advanced' => 'Bilaketa aurreratua',\n    'search_terms' => 'Bilaketa-hitza',\n    'search_content_type' => 'Eduki Mota',\n    'search_exact_matches' => 'Bat etortze zehatza',\n    'search_tags' => 'Etiketa bilaketak',\n    'search_options' => 'Aukerak',\n    'search_viewed_by_me' => 'Nik ikusiak',\n    'search_not_viewed_by_me' => 'Nik ikusi ez ditudanak',\n    'search_permissions_set' => 'Baimenak',\n    'search_created_by_me' => 'Nik sortuak',\n    'search_updated_by_me' => 'Nik eguneratuak',\n    'search_owned_by_me' => 'Nire jabetazkoak',\n    'search_date_options' => 'Data aukerak',\n    'search_updated_before' => 'Aurretik eguneratuak',\n    'search_updated_after' => 'Ondoren eguneratuak',\n    'search_created_before' => 'Aurretik sortuak',\n    'search_created_after' => 'Ondoren sortuak',\n    'search_set_date' => 'Data finkatu',\n    'search_update' => 'Eguneratu bilaketa',\n\n    // Shelves\n    'shelf' => 'Apalategia',\n    'shelves' => 'Apalategiak',\n    'x_shelves' => ':count Apalategi|:count Apalategi',\n    'shelves_empty' => 'Ez da inolako apalategirik sortu',\n    'shelves_create' => 'Apalategi berria sortu',\n    'shelves_popular' => 'Apalategi esanguratsuak',\n    'shelves_new' => 'Apalategi berriak',\n    'shelves_new_action' => 'Apalategi berria',\n    'shelves_popular_empty' => 'Apalategi ikusienak hemen agertuko dira.',\n    'shelves_new_empty' => 'Berriki sorturiko apalategiak hemen agertuko dira.',\n    'shelves_save' => 'Gorde apalategia',\n    'shelves_books' => 'Apalategi honetako liburuak',\n    'shelves_add_books' => 'Gehitu liburuak apalategi honetara',\n    'shelves_drag_books' => 'Drag books below to add them to this shelf',\n    'shelves_empty_contents' => 'Apalategi honek ez dauka libururik',\n    'shelves_edit_and_assign' => 'Apalategia editatu liburuak gehitzeko',\n    'shelves_edit_named' => 'Edit Shelf :name',\n    'shelves_edit' => 'Edit Shelf',\n    'shelves_delete' => 'Delete Shelf',\n    'shelves_delete_named' => 'Delete Shelf :name',\n    'shelves_delete_explain' => \"This will delete the shelf with the name ':name'. Contained books will not be deleted.\",\n    'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',\n    'shelves_permissions' => 'Apalategi baimenak',\n    'shelves_permissions_updated' => 'Apalategi baimenak eguneratuta',\n    'shelves_permissions_active' => 'Apalategi baimenak aktibatuta',\n    'shelves_permissions_cascade_warning' => 'Apaletako baimenak ez dira automatikoki hauen barneko liburuetan gordeko. Liburu bat apalategi askotan egon daitekeelako. Hala ere, baimenak apalategiko liburutara kopiatu daitezke, behean agertzen den aukera erabiliz.',\n    'shelves_permissions_create' => 'Shelf create permissions are only used for copying permissions to child books using the action below. They do not control the ability to create books.',\n    'shelves_copy_permissions_to_books' => 'Kopiatu baimenak liburura',\n    'shelves_copy_permissions' => 'Gorde baimenak',\n    'shelves_copy_permissions_explain' => 'Honek apalategi honen egungo baimen-konfigurazioa aplikatuko die barruan dauden liburu guztiei. Aktibatu aurretik, ziurtatu apaletan aldaketak gorde direla.',\n    'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',\n\n    // Books\n    'book' => 'Liburua',\n    'books' => 'Liburuak',\n    'x_books' => ':count Liburu|:count Liburu',\n    'books_empty' => 'Ez da orrialderik sortu',\n    'books_popular' => 'Liburu ikusienak',\n    'books_recent' => 'Azken liburuak',\n    'books_new' => 'Liburu berriak',\n    'books_new_action' => 'Liburu berria',\n    'books_popular_empty' => 'Apalategi ikusienak hemen agertuko dira.',\n    'books_new_empty' => 'Berriki sorturiko apalategiak hemen agertuko dira.',\n    'books_create' => 'Liburu berria sortu',\n    'books_delete' => 'Liburua ezabatu',\n    'books_delete_named' => ':bookName liburua ezabatuta',\n    'books_delete_explain' => 'Honek \\':bookName\\' liburua ezabatuko du. bere orrialde eta kapitulu guztiak ezabatuak izango dira.',\n    'books_delete_confirmation' => 'Ziur zaude liburu hau ezabatu nahi duzula?',\n    'books_edit' => 'Editatu liburua',\n    'books_edit_named' => 'Editatu :bookName liburua',\n    'books_form_book_name' => 'Liburu izena',\n    'books_save' => 'Gorde Liburua',\n    'books_permissions' => 'Liburu baimenak',\n    'books_permissions_updated' => 'Liburu baimenak eguneratuta',\n    'books_empty_contents' => 'Ez da orri edo kapitulurik sortu liburu honentzat.',\n    'books_empty_create_page' => 'Sortu orrialde berria',\n    'books_empty_sort_current_book' => 'Ordenatu uneko liburu hau',\n    'books_empty_add_chapter' => 'Kapitulu berria gehitu',\n    'books_permissions_active' => 'Liburu baimenak aktibatuta',\n    'books_search_this' => 'Bilatu liburu hau',\n    'books_navigation' => 'Liburu nabigazioa',\n    'books_sort' => 'Ordenatu liburu edukiak',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Ordenatu :bookName liburua',\n    'books_sort_name' => 'Ordenatu izenaren arabera',\n    'books_sort_created' => 'Ordenatu argitaratze-dataren arabera',\n    'books_sort_updated' => 'Ordenatu aldaketa-dataren arabera',\n    'books_sort_chapters_first' => 'Lehen kapitulua',\n    'books_sort_chapters_last' => 'Azken kapitulua',\n    'books_sort_show_other' => 'Erakutsi beste liburuak',\n    'books_sort_save' => 'Gorde agindu berria',\n    'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.',\n    'books_sort_move_up' => 'Move Up',\n    'books_sort_move_down' => 'Move Down',\n    'books_sort_move_prev_book' => 'Move to Previous Book',\n    'books_sort_move_next_book' => 'Move to Next Book',\n    'books_sort_move_prev_chapter' => 'Move Into Previous Chapter',\n    'books_sort_move_next_chapter' => 'Move Into Next Chapter',\n    'books_sort_move_book_start' => 'Move to Start of Book',\n    'books_sort_move_book_end' => 'Move to End of Book',\n    'books_sort_move_before_chapter' => 'Move to Before Chapter',\n    'books_sort_move_after_chapter' => 'Move to After Chapter',\n    'books_copy' => 'Kopiatu liburua',\n    'books_copy_success' => 'Ondo kopiatu da',\n\n    // Chapters\n    'chapter' => 'Kapitulua',\n    'chapters' => 'Kapituluak',\n    'x_chapters' => ':count Kapitulu|:count Kapitulu',\n    'chapters_popular' => 'Kapitulu ikusienak',\n    'chapters_new' => 'Kapitulu berria',\n    'chapters_create' => 'Sortu kapitulu berria',\n    'chapters_delete' => 'Kapitulua ezabatu',\n    'chapters_delete_named' => ':chapterName kapitulua ezabatu',\n    'chapters_delete_explain' => 'This will delete the chapter with the name \\':chapterName\\'. All pages that exist within this chapter will also be deleted.',\n    'chapters_delete_confirm' => 'Ziur kapitulu hau ezabatu nahi duzula?',\n    'chapters_edit' => 'Kapitulua aldatu',\n    'chapters_edit_named' => ':chapterName kapitulua editatu',\n    'chapters_save' => 'Kapitulua gorde',\n    'chapters_move' => 'Kapitulua mugitu',\n    'chapters_move_named' => ':chapterName kapitulua mugitu',\n    'chapters_copy' => 'Kapitulua kopiatu',\n    'chapters_copy_success' => 'Kapitulua egoki kopiatua',\n    'chapters_permissions' => 'Kapitulu baimenak',\n    'chapters_empty' => 'Ez dago orrialderik kapitulu honetan.',\n    'chapters_permissions_active' => 'Liburu baimenak altan',\n    'chapters_permissions_success' => 'Liburu baimenak eguneratuta',\n    'chapters_search_this' => 'Kapitulu hau bilatu',\n    'chapter_sort_book' => 'Sort Book',\n\n    // Pages\n    'page' => 'Orria',\n    'pages' => 'Orriak',\n    'x_pages' => 'orrialde:count|:count orrialde',\n    'pages_popular' => 'Orrialde ikusienak',\n    'pages_new' => 'Orrialde berria',\n    'pages_attachments' => 'Eranskinak',\n    'pages_navigation' => 'Nabigazio orrialdea',\n    'pages_delete' => 'Ezabatu orria',\n    'pages_delete_named' => ':pageName Orria ezabatu',\n    'pages_delete_draft_named' => 'Delete Draft Page :pageName',\n    'pages_delete_draft' => 'Delete Draft Page',\n    'pages_delete_success' => 'Orria ezabatua',\n    'pages_delete_draft_success' => 'Draft page deleted',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'Ziur al zaude orri hau ezabatu nahi duzula?',\n    'pages_delete_draft_confirm' => 'Are you sure you want to delete this draft page?',\n    'pages_editing_named' => 'Editing Page :pageName',\n    'pages_edit_draft_options' => 'Draft Options',\n    'pages_edit_save_draft' => 'Gorde zirriborroa',\n    'pages_edit_draft' => 'Edit Page Draft',\n    'pages_editing_draft' => 'Editatu zirriborroa',\n    'pages_editing_page' => 'Editatu orrialdea',\n    'pages_edit_draft_save_at' => 'Draft saved at ',\n    'pages_edit_delete_draft' => 'Ezabatu zirriborroa',\n    'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.',\n    'pages_edit_discard_draft' => 'Baztertu zirriborroa',\n    'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',\n    'pages_edit_switch_to_markdown_clean' => '(Clean Content)',\n    'pages_edit_switch_to_markdown_stable' => '(Stable Content)',\n    'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Set Changelog',\n    'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\\'ve made',\n    'pages_edit_enter_changelog' => 'Enter Changelog',\n    'pages_editor_switch_title' => 'Switch Editor',\n    'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',\n    'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',\n    'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',\n    'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',\n    'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\\'t persist across this change.',\n    'pages_save' => 'Gorde orrialdea',\n    'pages_title' => 'Orrialdearen titulua',\n    'pages_name' => 'Orrialdearen izena',\n    'pages_md_editor' => 'Editorea',\n    'pages_md_preview' => 'Aurrebista',\n    'pages_md_insert_image' => 'Txertatu irudia',\n    'pages_md_insert_link' => 'Insert Entity Link',\n    'pages_md_insert_drawing' => 'Txertatu marrazki berria',\n    'pages_md_show_preview' => 'Show preview',\n    'pages_md_sync_scroll' => 'Sync preview scroll',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Unsaved Drawing Found',\n    'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',\n    'pages_not_in_chapter' => 'Page is not in a chapter',\n    'pages_move' => 'Move Page',\n    'pages_copy' => 'Copy Page',\n    'pages_copy_desination' => 'Copy Destination',\n    'pages_copy_success' => 'Page successfully copied',\n    'pages_permissions' => 'Page Permissions',\n    'pages_permissions_success' => 'Page permissions updated',\n    'pages_revision' => 'Revision',\n    'pages_revisions' => 'Page Revisions',\n    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',\n    'pages_revisions_named' => 'Page Revisions for :pageName',\n    'pages_revision_named' => 'Page Revision for :pageName',\n    'pages_revision_restored_from' => 'Restored from #:id; :summary',\n    'pages_revisions_created_by' => 'Sortzailea',\n    'pages_revisions_date' => 'Berrikuspen data',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Revision Number',\n    'pages_revisions_numbered' => 'Revision #:id',\n    'pages_revisions_numbered_changes' => 'Revision #:id Changes',\n    'pages_revisions_editor' => 'Editor Type',\n    'pages_revisions_changelog' => 'Aldaketen erregistroa',\n    'pages_revisions_changes' => 'Aldaketak',\n    'pages_revisions_current' => 'Uneko bertsioa',\n    'pages_revisions_preview' => 'Aurrebista',\n    'pages_revisions_restore' => 'Berreskuratu',\n    'pages_revisions_none' => 'This page has no revisions',\n    'pages_copy_link' => 'Copy Link',\n    'pages_edit_content_link' => 'Jump to section in editor',\n    'pages_pointer_enter_mode' => 'Enter section select mode',\n    'pages_pointer_label' => 'Page Section Options',\n    'pages_pointer_permalink' => 'Page Section Permalink',\n    'pages_pointer_include_tag' => 'Page Section Include Tag',\n    'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag',\n    'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink',\n    'pages_permissions_active' => 'Page Permissions Active',\n    'pages_initial_revision' => 'Initial publish',\n    'pages_references_update_revision' => 'System auto-update of internal links',\n    'pages_initial_name' => 'Orrialde berria',\n    'pages_editing_draft_notification' => 'You are currently editing a draft that was last saved :timeDiff.',\n    'pages_draft_edited_notification' => 'This page has been updated by since that time. It is recommended that you discard this draft.',\n    'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count users have started editing this page',\n        'start_b' => ':userName has started editing this page',\n        'time_a' => 'since the page was last updated',\n        'time_b' => 'in the last :minCount minutes',\n        'message' => ':start :time. Take care not to overwrite each other\\'s updates!',\n    ],\n    'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',\n    'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',\n    'pages_specific' => 'Specific Page',\n    'pages_is_template' => 'Orrialde txantiloia',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Toggle Sidebar',\n    'page_tags' => 'Orrialde etiketak',\n    'chapter_tags' => 'Kapitulu etiketak',\n    'book_tags' => 'Liburu etiketak',\n    'shelf_tags' => 'Apalategi etiketak',\n    'tag' => 'Etiketa',\n    'tags' =>  'Etiketak',\n    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',\n    'tag_name' =>  'Etiketa izena',\n    'tag_value' => 'Tag Value (Optional)',\n    'tags_explain' => \"Add some tags to better categorise your content. \\n You can assign a value to a tag for more in-depth organisation.\",\n    'tags_add' => 'Beste bat gehitu',\n    'tags_remove' => 'Remove this tag',\n    'tags_usages' => 'Total tag usages',\n    'tags_assigned_pages' => 'Assigned to Pages',\n    'tags_assigned_chapters' => 'Assigned to Chapters',\n    'tags_assigned_books' => 'Assigned to Books',\n    'tags_assigned_shelves' => 'Assigned to Shelves',\n    'tags_x_unique_values' => ':count unique values',\n    'tags_all_values' => 'Balio guztiak',\n    'tags_view_tags' => 'View Tags',\n    'tags_view_existing_tags' => 'View existing tags',\n    'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',\n    'attachments' => 'Eranskinak',\n    'attachments_explain' => 'Upload some files or attach some links to display on your page. These are visible in the page sidebar.',\n    'attachments_explain_instant_save' => 'Changes here are saved instantly.',\n    'attachments_upload' => 'Kargatu artxiboak',\n    'attachments_link' => 'Attach Link',\n    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',\n    'attachments_set_link' => 'Set Link',\n    'attachments_delete' => 'Are you sure you want to delete this attachment?',\n    'attachments_dropzone' => 'Drop files here to upload',\n    'attachments_no_files' => 'Ez da igo fitxategirik',\n    'attachments_explain_link' => 'You can attach a link if you\\'d prefer not to upload a file. This can be a link to another page or a link to a file in the cloud.',\n    'attachments_link_name' => 'Loturaren izena',\n    'attachment_link' => 'Attachment link',\n    'attachments_link_url' => 'Fitxategiarentzako esteka',\n    'attachments_link_url_hint' => 'Url of site or file',\n    'attach' => 'Attach',\n    'attachments_insert_link' => 'Add Attachment Link to Page',\n    'attachments_edit_file' => 'Edit File',\n    'attachments_edit_file_name' => 'Fitxategi izena',\n    'attachments_edit_drop_upload' => 'Drop files or click here to upload and overwrite',\n    'attachments_order_updated' => 'Attachment order updated',\n    'attachments_updated_success' => 'Attachment details updated',\n    'attachments_deleted' => 'Attachment deleted',\n    'attachments_file_uploaded' => 'File successfully uploaded',\n    'attachments_file_updated' => 'File successfully updated',\n    'attachments_link_attached' => 'Link successfully attached to page',\n    'templates' => 'Templates',\n    'templates_set_as_template' => 'Page is a template',\n    'templates_explain_set_as_template' => 'You can set this page as a template so its contents be utilized when creating other pages. Other users will be able to use this template if they have view permissions for this page.',\n    'templates_replace_content' => 'Replace page content',\n    'templates_append_content' => 'Append to page content',\n    'templates_prepend_content' => 'Prepend to page content',\n\n    // Profile View\n    'profile_user_for_x' => 'User for :time',\n    'profile_created_content' => 'Created Content',\n    'profile_not_created_pages' => ':userName has not created any pages',\n    'profile_not_created_chapters' => ':userName has not created any chapters',\n    'profile_not_created_books' => ':userName has not created any books',\n    'profile_not_created_shelves' => ':userName has not created any shelves',\n\n    // Comments\n    'comment' => 'Iruzkina',\n    'comments' => 'Iruzkinak',\n    'comment_add' => 'Iruzkina gehitu',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Utzi iruzkin bat hemen',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Iruzkina gorde',\n    'comment_new' => 'Iruzkin berria',\n    'comment_created' => 'commented :createDiff',\n    'comment_updated' => 'Updated :updateDiff by :username',\n    'comment_updated_indicator' => 'Updated',\n    'comment_deleted_success' => 'Comment deleted',\n    'comment_created_success' => 'Iruzkina gehituta',\n    'comment_updated_success' => 'Iruzkina gehituta',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Ziur zaude iruzkin hau ezabatu nahi duzula?',\n    'comment_in_reply_to' => 'In reply to :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',\n\n    // Revision\n    'revision_delete_confirm' => 'Ziur zaude hau ezabatu nahi duzula?',\n    'revision_restore_confirm' => 'Are you sure you want to restore this revision? The current page contents will be replaced.',\n    'revision_cannot_delete_latest' => 'Cannot delete the latest revision.',\n\n    // Copy view\n    'copy_consider' => 'Please consider the below when copying content.',\n    'copy_consider_permissions' => 'Custom permission settings will not be copied.',\n    'copy_consider_owner' => 'You will become the owner of all copied content.',\n    'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',\n    'copy_consider_attachments' => 'Page attachments will not be copied.',\n    'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',\n\n    // Conversions\n    'convert_to_shelf' => 'Apalategi bihurtu',\n    'convert_to_shelf_contents_desc' => 'Liburu hau apalategi berri baten bihur dezakezu eduki berarekin. Liburu honetako kapituluak liburu berri bihurtuko dira. Liburu honek kapitulu batean ez dagoen orriren bat baldin badu, liburu honi izena aldatuko zaio eta orri horiek izango ditu, eta liburu hau apal berriaren parte bihurtuko da.',\n    'convert_to_shelf_permissions_desc' => 'Liburu honetan ezarritako baimen guztiak apalategi berrira igaroko dira eta baimen propiorik ez duten haur-liburu berrietara kopiatuko dira. Kontuan izan apaletako baimenek ez dutela barruan askiesten, liburuek bezala.',\n    'convert_book' => 'Convert Book',\n    'convert_book_confirm' => 'Are you sure you want to convert this book?',\n    'convert_undo_warning' => 'This cannot be as easily undone.',\n    'convert_to_book' => 'Convert to Book',\n    'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',\n    'convert_chapter' => 'Convert Chapter',\n    'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',\n\n    // References\n    'references' => 'References',\n    'references_none' => 'There are no tracked references to this item.',\n    'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',\n\n    // Watch Options\n    'watch' => 'Watch',\n    'watch_title_default' => 'Default Preferences',\n    'watch_desc_default' => 'Revert watching to just your default notification preferences.',\n    'watch_title_ignore' => 'Ignore',\n    'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.',\n    'watch_title_new' => 'New Pages',\n    'watch_desc_new' => 'Notify when any new page is created within this item.',\n    'watch_title_updates' => 'All Page Updates',\n    'watch_desc_updates' => 'Notify upon all new pages and page changes.',\n    'watch_desc_updates_page' => 'Notify upon all page changes.',\n    'watch_title_comments' => 'All Page Updates & Comments',\n    'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.',\n    'watch_desc_comments_page' => 'Notify upon page changes and new comments.',\n    'watch_change_default' => 'Change default notification preferences',\n    'watch_detail_ignore' => 'Ignoring notifications',\n    'watch_detail_new' => 'Watching for new pages',\n    'watch_detail_updates' => 'Watching new pages and updates',\n    'watch_detail_comments' => 'Watching new pages, updates & comments',\n    'watch_detail_parent_book' => 'Watching via parent book',\n    'watch_detail_parent_book_ignore' => 'Ignoring via parent book',\n    'watch_detail_parent_chapter' => 'Watching via parent chapter',\n    'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter',\n];\n"
  },
  {
    "path": "lang/eu/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Ez duzu baimenik eskatutako baliabidean sartzeko.',\n    'permissionJson' => 'Ez duzu baimenik ekintza hau egiteko.',\n\n    // Auth\n    'error_user_exists_different_creds' => ':email kontuakin erabiltzaile bat badago, baina kredentzial ezberdinekin.',\n    'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',\n    'email_already_confirmed' => 'Email kontua berretsita dago, saiatu saioa hasten.',\n    'email_confirmation_invalid' => 'Berrezpen token hau ez da baliozkoa eta iada erabiltzen da, mesedez, saiatu berriz erregistroa burutzen.',\n    'email_confirmation_expired' => 'Berrezpen tokena iraungi da, berrezpen email berri bnat bidali da.',\n    'email_confirmation_awaiting' => 'Erabiltzen ari den kontuko emaiala berreztea falta da',\n    'ldap_fail_anonymous' => 'LDAP sarrerak akatsa eman du lotura anonimoa erabiliz',\n    'ldap_fail_authed' => 'LDAP sarrera akatsa eman du dn eta pasahitz hauekin',\n    'ldap_extension_not_installed' => 'PHP LDAP extentsioa ez dago instalatuta',\n    'ldap_cannot_connect' => 'Ezin izan da ldap zerbitzarira konektatu, hasierako konexioak huts egin du',\n    'saml_already_logged_in' => 'Saioa aurretik hasita dago',\n    'saml_no_email_address' => 'Ezin izan dugu posta helbiderik aurkitu erabiltzaile honentzat, kanpoko autentifikazio zerbitzuak bidalitako datuetan',\n    'saml_invalid_response_id' => 'Kanpoko egiazkotasun-sistemaren eskaria ez du onartzen aplikazio honek abiarazitako prozesu batek. Loginean atzera egitea izan daiteke arrazoia.',\n    'saml_fail_authed' => 'Login using :system failed, system did not provide successful authorization',\n    'oidc_already_logged_in' => 'Dagoeneko saioa hasita',\n    'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',\n    'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',\n    'social_no_action_defined' => 'No action defined',\n    'social_login_bad_response' => \"Error received during :socialAccount login: \\n:error\",\n    'social_account_in_use' => 'This :socialAccount account is already in use, Try logging in via the :socialAccount option.',\n    'social_account_email_in_use' => 'The email :email is already in use. If you already have an account you can connect your :socialAccount account from your profile settings.',\n    'social_account_existing' => 'This :socialAccount is already attached to your profile.',\n    'social_account_already_used_existing' => 'This :socialAccount account is already used by another user.',\n    'social_account_not_used' => 'This :socialAccount account is not linked to any users. Please attach it in your profile settings. ',\n    'social_account_register_instructions' => 'If you do not yet have an account, You can register an account using the :socialAccount option.',\n    'social_driver_not_found' => 'Social driver not found',\n    'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.',\n    'invite_token_expired' => 'This invitation link has expired. You can instead try to reset your account password.',\n    'login_user_not_found' => 'A user for this action could not be found.',\n\n    // System\n    'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.',\n    'cannot_get_image_from_url' => 'Cannot get image from :url',\n    'cannot_create_thumbs' => 'The server cannot create thumbnails. Please check you have the GD PHP extension installed.',\n    'server_upload_limit' => 'The server does not allow uploads of this size. Please try a smaller file size.',\n    'server_post_limit' => 'The server cannot receive the provided amount of data. Try again with less data or a smaller file.',\n    'uploaded'  => 'The server does not allow uploads of this size. Please try a smaller file size.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Errorea gertatu da irudia igotzerakoan',\n    'image_upload_type_error' => 'The image type being uploaded is invalid',\n    'image_upload_replace_type' => 'Image file replacements must be of the same type',\n    'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.',\n    'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.',\n    'image_gallery_thumbnail_memory_limit' => 'Failed to create gallery thumbnails due to system resource limits.',\n    'drawing_data_not_found' => 'Drawing data could not be loaded. The drawing file might no longer exist or you may not have permission to access it.',\n\n    // Attachments\n    'attachment_not_found' => 'Atxikia ez da aurkitu',\n    'attachment_upload_error' => 'An error occurred uploading the attachment file',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page',\n    'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content',\n    'page_custom_home_deletion' => 'Cannot delete a page while it is set as a homepage',\n\n    // Entities\n    'entity_not_found' => 'Entity not found',\n    'bookshelf_not_found' => 'Shelf not found',\n    'book_not_found' => 'Book not found',\n    'page_not_found' => 'Page not found',\n    'chapter_not_found' => 'Chapter not found',\n    'selected_book_not_found' => 'The selected book was not found',\n    'selected_book_chapter_not_found' => 'The selected Book or Chapter was not found',\n    'guests_cannot_save_drafts' => 'Guests cannot save drafts',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'You cannot delete the only admin',\n    'users_cannot_delete_guest' => 'You cannot delete the guest user',\n    'users_could_not_send_invite' => 'Could not create user since invite email failed to send',\n\n    // Roles\n    'role_cannot_be_edited' => 'This role cannot be edited',\n    'role_system_cannot_be_deleted' => 'This role is a system role and cannot be deleted',\n    'role_registration_default_cannot_delete' => 'This role cannot be deleted while set as the default registration role',\n    'role_cannot_remove_only_admin' => 'This user is the only user assigned to the administrator role. Assign the administrator role to another user before attempting to remove it here.',\n\n    // Comments\n    'comment_list' => 'An error occurred while fetching the comments.',\n    'cannot_add_comment_to_draft' => 'You cannot add comments to a draft.',\n    'comment_add' => 'An error occurred while adding / updating the comment.',\n    'comment_delete' => 'An error occurred while deleting the comment.',\n    'empty_comment' => 'Cannot add an empty comment.',\n\n    // Error pages\n    '404_page_not_found' => 'Ez da orrialdea aurkitu',\n    'sorry_page_not_found' => 'Sorry, The page you were looking for could not be found.',\n    'sorry_page_not_found_permission_warning' => 'If you expected this page to exist, you might not have permission to view it.',\n    'image_not_found' => 'Irudia Ez da Aurkitu',\n    'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',\n    'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',\n    'return_home' => 'Itzuli hasierara',\n    'error_occurred' => 'Akats bat gertatu da',\n    'app_down' => ':appName is down right now',\n    'back_soon' => 'It will be back up soon.',\n\n    // Import\n    'import_zip_cant_read' => 'Could not read ZIP file.',\n    'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',\n    'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Import ZIP failed to validate with errors:',\n    'import_zip_failed_notification' => 'Failed to import ZIP file.',\n    'import_perms_books' => 'You are lacking the required permissions to create books.',\n    'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',\n    'import_perms_pages' => 'You are lacking the required permissions to create pages.',\n    'import_perms_images' => 'You are lacking the required permissions to create images.',\n    'import_perms_attachments' => 'You are lacking the required permission to create attachments.',\n\n    // API errors\n    'api_no_authorization_found' => 'No authorization token found on the request',\n    'api_bad_authorization_format' => 'An authorization token was found on the request but the format appeared incorrect',\n    'api_user_token_not_found' => 'No matching API token was found for the provided authorization token',\n    'api_incorrect_token_secret' => 'The secret provided for the given used API token is incorrect',\n    'api_user_no_api_permission' => 'The owner of the used API token does not have permission to make API calls',\n    'api_user_token_expired' => 'The authorization token used has expired',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Error thrown when sending a test email:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'The URL does not match the configured allowed SSR hosts',\n];\n"
  },
  {
    "path": "lang/eu/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'New comment on page: :pageName',\n    'new_comment_intro' => 'A user has commented on a page in :appName:',\n    'new_page_subject' => 'New page: :pageName',\n    'new_page_intro' => 'A new page has been created in :appName:',\n    'updated_page_subject' => 'Updated page: :pageName',\n    'updated_page_intro' => 'A page has been updated in :appName:',\n    'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\\'t be sent notifications for further edits to this page by the same editor.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Page Name:',\n    'detail_page_path' => 'Page Path:',\n    'detail_commenter' => 'Commenter:',\n    'detail_comment' => 'Comment:',\n    'detail_created_by' => 'Created By:',\n    'detail_updated_by' => 'Updated By:',\n\n    'action_view_comment' => 'View Comment',\n    'action_view_page' => 'View Page',\n\n    'footer_reason' => 'This notification was sent to you because :link cover this type of activity for this item.',\n    'footer_reason_link' => 'your notification preferences',\n];\n"
  },
  {
    "path": "lang/eu/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Aurrekoa',\n    'next'     => 'Hurrengoa &raquo;',\n\n];\n"
  },
  {
    "path": "lang/eu/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Pasahitzak gutxienez sei karaktere izan behar ditu eta baldintzak bete behar ditu.',\n    'user' => \"Ezin izan dugu erabiltzailerik topatu posta-helbide honekin.\",\n    'token' => 'Pasahitza berritzeko token hau ez da baliagarria email helbide honentzat.',\n    'sent' => 'Zure pasahitza berrezartzeko esteka email bidez bidali dizugu!',\n    'reset' => 'Zure pasahitza berrezarri da!',\n\n];\n"
  },
  {
    "path": "lang/eu/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'My Account',\n\n    'shortcuts' => 'Lastertekla',\n    'shortcuts_interface' => 'UI Shortcut Preferences',\n    'shortcuts_toggle_desc' => 'Hemen, nabigaziorako eta ekintzetarako erabiltzen diren teklatu-sistemako lasterbideak gaitu edo desgaitu daitezke.',\n    'shortcuts_customize_desc' => 'Beheko lasterbide bakoitza pertsonalizatu dezakezu. Sakatu nahi duzun tekla konbinazioa lasterbide baterako sarrera aukeratu ondoren.',\n    'shortcuts_toggle_label' => 'Teklatu-lasterbideak aktibatuta',\n    'shortcuts_section_navigation' => 'Nabigazioa',\n    'shortcuts_section_actions' => 'Ohiko ekintzak',\n    'shortcuts_save' => 'Gorde lasterbideak',\n    'shortcuts_overlay_desc' => 'Oharra: Lasterbideak gaituta daudenean, \"?\" sakagailuaren bidez laguntzaileen gainjartze bat egongo da, eta horrek pantailan gaur egun ikus daitezkeen ekintzetarako dauden lasterbideak nabarmenduko ditu.',\n    'shortcuts_update_success' => 'Zure lehentasunak gorde dira!',\n    'shortcuts_overview_desc' => 'Manage keyboard shortcuts you can use to navigate the system user interface.',\n\n    'notifications' => 'Notification Preferences',\n    'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.',\n    'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own',\n    'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Notify upon replies to my comments',\n    'notifications_save' => 'Save Preferences',\n    'notifications_update_success' => 'Notification preferences have been updated!',\n    'notifications_watched' => 'Watched & Ignored Items',\n    'notifications_watched_desc' => 'Below are the items that have custom watch preferences applied. To update your preferences for these, view the item then find the watch options in the sidebar.',\n\n    'auth' => 'Access & Security',\n    'auth_change_password' => 'Change Password',\n    'auth_change_password_desc' => 'Change the password you use to log-in to the application. This must be at least 8 characters long.',\n    'auth_change_password_success' => 'Password has been updated!',\n\n    'profile' => 'Profile Details',\n    'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.',\n    'profile_view_public' => 'View Public Profile',\n    'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.',\n    'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.',\n    'profile_email_no_permission' => 'Unfortunately you don\\'t have permission to change your email address. If you want to change this, you\\'d need to ask an administrator to change this for you.',\n    'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.',\n    'profile_admin_options' => 'Administrator Options',\n    'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the \"Settings > Users\" area of the application.',\n\n    'delete_account' => 'Delete Account',\n    'delete_my_account' => 'Delete My Account',\n    'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\\'ve created, such as created pages and uploaded images, will remain.',\n    'delete_my_account_warning' => 'Are you sure you want to delete your account?',\n];\n"
  },
  {
    "path": "lang/eu/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Ezarpenak',\n    'settings_save' => 'Gorde aldaketak',\n    'system_version' => 'Sistema bertsioa',\n    'categories' => 'Kategoriak',\n\n    // App Settings\n    'app_customization' => 'Pertsonalizazioa',\n    'app_features_security' => 'Ezaugarriak eta Segurtasuna',\n    'app_name' => 'Aplikazioaren izena',\n    'app_name_desc' => 'Izen hau goiburuan eta bidaltzen diren emailetan agertuko da.',\n    'app_name_header' => 'Erakutsi izena goiburuan',\n    'app_public_access' => 'Sarbide publikoa',\n    'app_public_access_desc' => 'Aukera honen bitartez, bisitariek, saioa hasita ez dutenek, edukiak eskuratu ahal izango dituzte zure BookStacken instantzian.',\n    'app_public_access_desc_guest' => 'Bisitarien sarrera \"Guest\" erabiltzeile bidez kontrola daiteke.',\n    'app_public_access_toggle' => 'Baimendu sarbide publikoa',\n    'app_public_viewing' => 'Publikoki ikustea baimendu?',\n    'app_secure_images' => 'Goi Segurtasuneko irudiak',\n    'app_secure_images_toggle' => 'Goi Segurtasuneko irudiak aktibatu',\n    'app_secure_images_desc' => 'Antzezpen arrazoiengatik, irudi guztiak publikoak dira. Aukera honek ausazko lokarri bat gehitzen du irudiaren aurrean. Segurtasun-indizeek ezin dute sarrera erraza eragotzi.',\n    'app_default_editor' => 'Default Page Editor',\n    'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.',\n    'app_custom_html' => 'HTML pertsonalizatuko goiburu edukia',\n    'app_custom_html_desc' => 'Hemen sarturiko edozein eduki <head> eremuko behekaldean sartuko da orrialde guztietan. Honek estiloak gainditzeko edo analitika-kodea gehitzeko balio du.',\n    'app_custom_html_disabled_notice' => 'HTML edukiera desgaituta dago konfigurazio-orri honetan, edozein aldaketa eten daitekeela bermatzeko.',\n    'app_logo' => 'Aplikazioaren logoa',\n    'app_logo_desc' => 'This is used in the application header bar, among other areas. This image should be 86px in height. Large images will be scaled down.',\n    'app_icon' => 'Application Icon',\n    'app_icon_desc' => 'This icon is used for browser tabs and shortcut icons. This should be a 256px square PNG image.',\n    'app_homepage' => 'Aplikazioko hasiera orria',\n    'app_homepage_desc' => 'Aukeratu hasierako orriko bista, defektuzkoa beharrean. Orrialde baimenak ez dira kontutan hartuko aukeratutako orrialdeentzat.',\n    'app_homepage_select' => 'Aukeratu Orria',\n    'app_footer_links' => 'Beheko aldeko estekak',\n    'app_footer_links_desc' => 'Add links to show within the site footer. These will be displayed at the bottom of most pages, including those that do not require login. You can use a label of \"trans::<key>\" to use system-defined translations. For example: Using \"trans::common.privacy_policy\" will provide the translated text \"Privacy Policy\" and \"trans::common.terms_of_service\" will provide the translated text \"Terms of Service\".',\n    'app_footer_links_label' => 'Loturaren etiketa',\n    'app_footer_links_url' => 'Estekaren URLa',\n    'app_footer_links_add' => 'Gehitu oineko esteka',\n    'app_disable_comments' => 'Ezgaitu Iruzkinak',\n    'app_disable_comments_toggle' => 'Ezgaitu iruzkinak',\n    'app_disable_comments_desc' => 'Disables comments across all pages in the application. <br> Existing comments are not shown.',\n\n    // Color settings\n    'color_scheme' => 'Application Color Scheme',\n    'color_scheme_desc' => 'Set the colors to use in the application user interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.',\n    'ui_colors_desc' => 'Set the application primary color and default link color. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the application interface.',\n    'app_color' => 'Primary Color',\n    'link_color' => 'Default Link Color',\n    'content_colors_desc' => 'Set colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.',\n    'bookshelf_color' => 'Apal kolorea',\n    'book_color' => 'Liburu kolorea',\n    'chapter_color' => 'Kapitulu kolorea',\n    'page_color' => 'Orrialde kolorea',\n    'page_draft_color' => 'Zirriborro orrien kolorea',\n\n    // Registration Settings\n    'reg_settings' => 'Izen-ematea',\n    'reg_enable' => 'Baimendu izen ematea',\n    'reg_enable_toggle' => 'Baimendu izen ematea',\n    'reg_enable_desc' => 'When registration is enabled user will be able to sign themselves up as an application user. Upon registration they are given a single, default user role.',\n    'reg_default_role' => 'Default user role after registration',\n    'reg_enable_external_warning' => 'The option above is ignored while external LDAP or SAML authentication is active. User accounts for non-existing members will be auto-created if authentication, against the external system in use, is successful.',\n    'reg_email_confirmation' => 'Email baieztapena',\n    'reg_email_confirmation_toggle' => 'Emain baieztapena behar du',\n    'reg_confirm_email_desc' => 'If domain restriction is used then email confirmation will be required and this option will be ignored.',\n    'reg_confirm_restrict_domain' => 'Domeinu mugaketa',\n    'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.',\n    'reg_confirm_restrict_domain_placeholder' => 'Mugarik gabe',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Mantentze-lanak',\n    'maint_image_cleanup' => 'Garbitu irudiak',\n    'maint_image_cleanup_desc' => 'Scans page & revision content to check which images and drawings are currently in use and which images are redundant. Ensure you create a full database and image backup before running this.',\n    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',\n    'maint_image_cleanup_run' => 'Exekutatu garbiketa',\n    'maint_image_cleanup_warning' => ':count potentially unused images were found. Are you sure you want to delete these images?',\n    'maint_image_cleanup_success' => ':count potentially unused images found and deleted!',\n    'maint_image_cleanup_nothing_found' => 'Ez da erabili gabeko irudirik aurkitu, ez da ezer ezabatuko!',\n    'maint_send_test_email' => 'Frogako mezua bidali',\n    'maint_send_test_email_desc' => 'This sends a test email to your email address specified in your profile.',\n    'maint_send_test_email_run' => 'Frogako mezua bidali',\n    'maint_send_test_email_success' => 'Emaila bidali da :address helbidera',\n    'maint_send_test_email_mail_subject' => 'Email test-a',\n    'maint_send_test_email_mail_greeting' => 'Email delivery seems to work!',\n    'maint_send_test_email_mail_text' => 'Congratulations! As you received this email notification, your email settings seem to be configured properly.',\n    'maint_recycle_bin_desc' => 'Deleted shelves, books, chapters & pages are sent to the recycle bin so they can be restored or permanently deleted. Older items in the recycle bin may be automatically removed after a while depending on system configuration.',\n    'maint_recycle_bin_open' => 'Ireki zakarrontzia',\n    'maint_regen_references' => 'Regenerate References',\n    'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',\n    'maint_regen_references_success' => 'Reference index has been regenerated!',\n    'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Zakarrontzia',\n    'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',\n    'recycle_bin_deleted_item' => 'Ezabatutako edukiak',\n    'recycle_bin_deleted_parent' => 'Nagusia',\n    'recycle_bin_deleted_by' => 'Nork ezabatua',\n    'recycle_bin_deleted_at' => 'Ezabatutako unea',\n    'recycle_bin_permanently_delete' => 'Ezabatu betiko',\n    'recycle_bin_restore' => 'Berreskuratu',\n    'recycle_bin_contents_empty' => 'Zakarrontzia hutsik dago',\n    'recycle_bin_empty' => 'Hustu Zakarrontzia',\n    'recycle_bin_empty_confirm' => 'This will permanently destroy all items in the recycle bin including content contained within each item. Are you sure you want to empty the recycle bin?',\n    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?',\n    'recycle_bin_destroy_list' => 'Ezabatuko diren elementuak',\n    'recycle_bin_restore_list' => 'Berrezarriko diren elementuak',\n    'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.',\n    'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.',\n    'recycle_bin_restore_parent' => 'Aita berrezarri',\n    'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',\n    'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',\n\n    // Audit Log\n    'audit' => 'Auditoretza erregistroak',\n    'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',\n    'audit_event_filter' => 'Gertakari filtroa',\n    'audit_event_filter_no_filter' => 'Filtrorik ez',\n    'audit_deleted_item' => 'Ezabatutako edukiak',\n    'audit_deleted_item_name' => 'Izena :name',\n    'audit_table_user' => 'Erabiltzailea',\n    'audit_table_event' => 'Gertaera',\n    'audit_table_related' => 'Related Item or Detail',\n    'audit_table_ip' => 'IP helbidea',\n    'audit_table_date' => 'Azken aktibitate data',\n    'audit_date_from' => 'Data tartea',\n    'audit_date_to' => 'Data tartea',\n\n    // Role Settings\n    'roles' => 'Rolak',\n    'role_user_roles' => 'Erabiltzailearen rola',\n    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',\n    'roles_x_users_assigned' => ':count user assigned|:count users assigned',\n    'roles_x_permissions_provided' => ':count permission|:count permissions',\n    'roles_assigned_users' => 'Assigned Users',\n    'roles_permissions_provided' => 'Provided Permissions',\n    'role_create' => 'Rol berria sortu',\n    'role_delete' => 'Ezabatu Rol-a',\n    'role_delete_confirm' => 'This will delete the role with the name \\':roleName\\'.',\n    'role_delete_users_assigned' => 'This role has :userCount users assigned to it. If you would like to migrate the users from this role select a new role below.',\n    'role_delete_no_migration' => \"Ez migratu erabiltzaileak\",\n    'role_delete_sure' => 'Ziur zaude rol hau ezabatu nahi duzula?',\n    'role_edit' => 'Editatu rola',\n    'role_details' => 'Ireki xehetasunak',\n    'role_name' => 'Rol izena',\n    'role_desc' => 'Short Description of Role',\n    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',\n    'role_external_auth_id' => 'External Authentication IDs',\n    'role_system' => 'System Permissions',\n    'role_manage_users' => 'Erabiltzaileak kudeatu',\n    'role_manage_roles' => 'Manage roles & role permissions',\n    'role_manage_entity_permissions' => 'Manage all book, chapter & page permissions',\n    'role_manage_own_entity_permissions' => 'Manage permissions on own book, chapter & pages',\n    'role_manage_page_templates' => 'Kudeatu orrien txantiloiak',\n    'role_access_api' => 'Sistemako APIra sarrera',\n    'role_manage_settings' => 'Kudeatu aplikazio ezarpenak',\n    'role_export_content' => 'Exportatu edukia',\n    'role_import_content' => 'Import content',\n    'role_editor_change' => 'Change page editor',\n    'role_notifications' => 'Receive & manage notifications',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Fitxategi baimenak',\n    'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',\n    'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',\n    'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.',\n    'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'Guztiak',\n    'role_own' => 'Norberarenak',\n    'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',\n    'role_save' => 'Gorde rol-a',\n    'role_users' => 'Rol honetako erabiltzaileak',\n    'role_users_none' => 'No users are currently assigned to this role',\n\n    // Users\n    'users' => 'Erabiltzaileak',\n    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',\n    'user_profile' => 'Erabiltzailearen profila',\n    'users_add_new' => 'Erabiltzaile berri bat gehitu',\n    'users_search' => 'Erabiltzaileak bilatu',\n    'users_latest_activity' => 'Duela gutxiko Jarduera',\n    'users_details' => 'Erabiltzaile xehetasunak',\n    'users_details_desc' => 'Set a display name and an email address for this user. The email address will be used for logging into the application.',\n    'users_details_desc_no_email' => 'Set a display name for this user so others can recognise them.',\n    'users_role' => 'Erabiltzailearen rola',\n    'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.',\n    'users_password' => 'Erabiltzaile pasahitza',\n    'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',\n    'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.',\n    'users_send_invite_option' => 'Erabiltzailea gonbidatzeko emaila bidali',\n    'users_external_auth_id' => 'Kanpo autentikazioa IDa',\n    'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.',\n    'users_password_warning' => 'Only fill the below if you would like to change the password for this user.',\n    'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.',\n    'users_delete' => 'Ezabatu erabiltzailea',\n    'users_delete_named' => ':userName erabiltzailea ezabatu',\n    'users_delete_warning' => 'This will fully delete this user with the name \\':userName\\' from the system.',\n    'users_delete_confirm' => 'Are you sure you want to delete this user?',\n    'users_migrate_ownership' => 'Migrate Ownership',\n    'users_migrate_ownership_desc' => 'Select a user here if you want another user to become the owner of all items currently owned by this user.',\n    'users_none_selected' => 'Erabiltzailerik ez duzu aukeratu',\n    'users_edit' => 'Erabiltzaile editatu',\n    'users_edit_profile' => 'Editatu profila',\n    'users_avatar' => 'Erabiltzaile avatarra',\n    'users_avatar_desc' => 'Select an image to represent this user. This should be approx 256px square.',\n    'users_preferred_language' => 'Hobetsitako hizkuntza',\n    'users_preferred_language_desc' => 'This option will change the language used for the user-interface of the application. This will not affect any user-created content.',\n    'users_social_accounts' => 'Social Accounts',\n    'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.',\n    'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not revoke previously authorized access. Revoke access from your profile settings on the connected social account.',\n    'users_social_connect' => 'Kontua Konektatu',\n    'users_social_disconnect' => 'Deskonektatu kontua',\n    'users_social_status_connected' => 'Connected',\n    'users_social_status_disconnected' => 'Disconnected',\n    'users_social_connected' => ':socialAccount account was successfully attached to your profile.',\n    'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.',\n    'users_api_tokens' => 'API tokenak',\n    'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.',\n    'users_api_tokens_none' => 'No API tokens have been created for this user',\n    'users_api_tokens_create' => 'Sortu Tokena',\n    'users_api_tokens_expires' => 'Iraungita',\n    'users_api_tokens_docs' => 'API dokumentazioa',\n    'users_mfa' => 'Multi-Factor Authentication',\n    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'users_mfa_x_methods' => ':count method configured|:count methods configured',\n    'users_mfa_configure' => 'Configure Methods',\n\n    // API Tokens\n    'user_api_token_create' => 'Sortu Tokena',\n    'user_api_token_name' => 'Izena',\n    'user_api_token_name_desc' => 'Give your token a readable name as a future reminder of its intended purpose.',\n    'user_api_token_expiry' => 'Iraungitze data',\n    'user_api_token_expiry_desc' => 'Set a date at which this token expires. After this date, requests made using this token will no longer work. Leaving this field blank will set an expiry 100 years into the future.',\n    'user_api_token_create_secret_message' => 'Immediately after creating this token a \"Token ID\" & \"Token Secret\" will be generated and displayed. The secret will only be shown a single time so be sure to copy the value to somewhere safe and secure before proceeding.',\n    'user_api_token' => 'API Token',\n    'user_api_token_id' => 'Token ID',\n    'user_api_token_id_desc' => 'This is a non-editable system generated identifier for this token which will need to be provided in API requests.',\n    'user_api_token_secret' => 'Token Secret',\n    'user_api_token_secret_desc' => 'This is a system generated secret for this token which will need to be provided in API requests. This will only be displayed this one time so copy this value to somewhere safe and secure.',\n    'user_api_token_created' => 'Token created :timeAgo',\n    'user_api_token_updated' => 'Token updated :timeAgo',\n    'user_api_token_delete' => 'Delete Token',\n    'user_api_token_delete_warning' => 'This will fully delete this API token with the name \\':tokenName\\' from the system.',\n    'user_api_token_delete_confirm' => 'Are you sure you want to delete this API token?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',\n    'webhooks_x_trigger_events' => ':count trigger event|:count trigger events',\n    'webhooks_create' => 'Create New Webhook',\n    'webhooks_none_created' => 'No webhooks have yet been created.',\n    'webhooks_edit' => 'Edit Webhook',\n    'webhooks_save' => 'Save Webhook',\n    'webhooks_details' => 'Webhook Details',\n    'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',\n    'webhooks_events' => 'Webhook Events',\n    'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',\n    'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\\'t expose confidential content.',\n    'webhooks_events_all' => 'All system events',\n    'webhooks_name' => 'Webhook Name',\n    'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',\n    'webhooks_endpoint' => 'Webhook Endpoint',\n    'webhooks_active' => 'Webhook Active',\n    'webhook_events_table_header' => 'Gertaerak',\n    'webhooks_delete' => 'Delete Webhook',\n    'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \\':webhookName\\', from the system.',\n    'webhooks_delete_confirm' => 'Ziur zaude hau ezabatu nahi duzula?',\n    'webhooks_format_example' => 'Webhook Format Example',\n    'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The \"related_item\" and \"url\" properties are optional and will depend on the type of event triggered.',\n    'webhooks_status' => 'Webhook Status',\n    'webhooks_last_called' => 'Last Called:',\n    'webhooks_last_errored' => 'Last Errored:',\n    'webhooks_last_error_message' => 'Last Error Message:',\n\n    // Licensing\n    'licenses' => 'Licenses',\n    'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.',\n    'licenses_bookstack' => 'BookStack License',\n    'licenses_php' => 'PHP Library Licenses',\n    'licenses_js' => 'JavaScript Library Licenses',\n    'licenses_other' => 'Other Licenses',\n    'license_details' => 'License Details',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/eu/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute onartua izan behar du.',\n    'active_url'           => ':attribute ez da baliozko URLa.',\n    'after'                => ':attribute :date baino zaharragoa izan behar da.',\n    'alpha'                => ':attribute eremuak hizkiak solik izan ditzake.',\n    'alpha_dash'           => ':attribute eremuak letrak, zenbakiak, laburpenak eta azpizenbakiak bakarrik eduki ditzake.',\n    'alpha_num'            => ':attribute eremuak hizki eta zenbakiak solik izan ditzake.',\n    'array'                => ':attribute array bat izan behar da.',\n    'backup_codes'         => 'Kode hau ez da baliagarria edo iada erabilia izan da.',\n    'before'               => ':attribute :date baino berriagoa izan behar da.',\n    'between'              => [\n        'numeric' => ':min eta :max bitartean egon behar da :attribute.',\n        'file'    => ':min eta :max kilobytes tartean egon behar da :attribute.',\n        'string'  => ':min eta :max karaktere tartean egon behar da :attribute.',\n        'array'   => ':min eta :max item tartean egon behar da :attribute.',\n    ],\n    'boolean'              => ':attribute true edo false izan behar da.',\n    'confirmed'            => ':attribute berrezpena ez da aurkitu.',\n    'date'                 => ':attribute ez da baliozko data.',\n    'date_format'          => ':attribute ez da :format formatuan aurkitu.',\n    'different'            => ':attribute eta :other ezberdinak izan behar dira.',\n    'digits'               => ':attribute :digits digitu behar ditu.',\n    'digits_between'       => ':min eta :max digitu tartean egon behar da :attribute.',\n    'email'                => ':attribute baliozko email helbide bat izan behar da.',\n    'ends_with' => ':attribute ondorengo balio hauetako batekin bukatu behar da :values',\n    'file'                 => 'The :attribute must be provided as a valid file.',\n    'filled'               => ':attribute eremua beharrezkoa da.',\n    'gt'                   => [\n        'numeric' => 'The :attribute must be greater than :value.',\n        'file'    => 'The :attribute must be greater than :value kilobytes.',\n        'string'  => 'The :attribute must be greater than :value characters.',\n        'array'   => 'The :attribute must have more than :value items.',\n    ],\n    'gte'                  => [\n        'numeric' => 'The :attribute must be greater than or equal :value.',\n        'file'    => 'The :attribute must be greater than or equal :value kilobytes.',\n        'string'  => 'The :attribute must be greater than or equal :value characters.',\n        'array'   => 'The :attribute must have :value items or more.',\n    ],\n    'exists'               => 'The selected :attribute is invalid.',\n    'image'                => ':attribute irudi bat izan behar da.',\n    'image_extension'      => 'The :attribute must have a valid & supported image extension.',\n    'in'                   => 'The selected :attribute is invalid.',\n    'integer'              => ':attribute zenbaki oso bat izan behar da.',\n    'ip'                   => 'The :attribute must be a valid IP address.',\n    'ipv4'                 => 'The :attribute must be a valid IPv4 address.',\n    'ipv6'                 => 'The :attribute must be a valid IPv6 address.',\n    'json'                 => 'The :attribute must be a valid JSON string.',\n    'lt'                   => [\n        'numeric' => 'The :attribute must be less than :value.',\n        'file'    => 'The :attribute must be less than :value kilobytes.',\n        'string'  => 'The :attribute must be less than :value characters.',\n        'array'   => 'The :attribute must have less than :value items.',\n    ],\n    'lte'                  => [\n        'numeric' => 'The :attribute must be less than or equal :value.',\n        'file'    => 'The :attribute must be less than or equal :value kilobytes.',\n        'string'  => 'The :attribute must be less than or equal :value characters.',\n        'array'   => 'The :attribute must not have more than :value items.',\n    ],\n    'max'                  => [\n        'numeric' => 'The :attribute may not be greater than :max.',\n        'file'    => 'The :attribute may not be greater than :max kilobytes.',\n        'string'  => 'The :attribute may not be greater than :max characters.',\n        'array'   => 'The :attribute may not have more than :max items.',\n    ],\n    'mimes'                => 'The :attribute must be a file of type: :values.',\n    'min'                  => [\n        'numeric' => 'The :attribute must be at least :min.',\n        'file'    => 'The :attribute must be at least :min kilobytes.',\n        'string'  => 'The :attribute must be at least :min characters.',\n        'array'   => 'The :attribute must have at least :min items.',\n    ],\n    'not_in'               => 'The selected :attribute is invalid.',\n    'not_regex'            => ':attribute formatua baliogabea da.',\n    'numeric'              => ':attribute zenbaki bat izan behar da.',\n    'regex'                => ':attribute formatua baliogabea da.',\n    'required'             => ':attribute eremua beharrezkoa da.',\n    'required_if'          => 'The :attribute field is required when :other is :value.',\n    'required_with'        => 'The :attribute field is required when :values is present.',\n    'required_with_all'    => 'The :attribute field is required when :values is present.',\n    'required_without'     => 'The :attribute field is required when :values is not present.',\n    'required_without_all' => 'The :attribute field is required when none of :values are present.',\n    'same'                 => 'The :attribute and :other must match.',\n    'safe_url'             => 'The provided link may not be safe.',\n    'size'                 => [\n        'numeric' => 'The :attribute must be :size.',\n        'file'    => 'The :attribute must be :size kilobytes.',\n        'string'  => 'The :attribute must be :size characters.',\n        'array'   => 'The :attribute must contain :size items.',\n    ],\n    'string'               => ':attribute textua izan behar da.',\n    'timezone'             => 'The :attribute must be a valid zone.',\n    'totp'                 => 'The provided code is not valid or has expired.',\n    'unique'               => 'The :attribute has already been taken.',\n    'url'                  => 'The :attribute format is invalid.',\n    'uploaded'             => 'The file could not be uploaded. The server may not accept files of this size.',\n\n    'zip_file' => 'The :attribute needs to reference a file within the ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',\n    'zip_model_expected' => 'Data object expected but \":type\" found.',\n    'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Baieztatu zure pasahitza',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/fa/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'صفحه ایجاد شد',\n    'page_create_notification'    => 'صفحه با موفقیت ایجاد شد',\n    'page_update'                 => 'به روزرسانی صفحه',\n    'page_update_notification'    => 'صفحه با موفقیت به روزرسانی شد',\n    'page_delete'                 => 'حذف صفحه',\n    'page_delete_notification'    => 'صفحه با موفقیت حذف شد',\n    'page_restore'                => 'بازیابی صفحه',\n    'page_restore_notification'   => 'صفحه با موفقیت بازیابی شد',\n    'page_move'                   => 'انتقال صفحه',\n    'page_move_notification'      => 'صفحه با موفقیت جابه‌جا شد',\n\n    // Chapters\n    'chapter_create'              => 'ایجاد فصل',\n    'chapter_create_notification' => 'فصل با موفقیت ایجاد شد',\n    'chapter_update'              => 'به روزرسانی فصل',\n    'chapter_update_notification' => 'فصل با موفقیت به روزرسانی شد',\n    'chapter_delete'              => 'حذف فصل',\n    'chapter_delete_notification' => 'فصل با موفقیت حذف شد',\n    'chapter_move'                => 'انتقال فصل',\n    'chapter_move_notification' => 'فصل با موفقیت جابه‌جا شد',\n\n    // Books\n    'book_create'                 => 'ایجاد کتاب',\n    'book_create_notification'    => 'کتاب با موفقیت ایجاد شد',\n    'book_create_from_chapter'              => 'تبدیل فصل به کتاب',\n    'book_create_from_chapter_notification' => 'کتاب با موفقیت به یک قفسه تبدیل شد',\n    'book_update'                 => 'به روزرسانی کتاب',\n    'book_update_notification'    => 'کتاب با موفقیت به روزرسانی شد',\n    'book_delete'                 => 'حذف کتاب',\n    'book_delete_notification'    => 'کتاب با موفقیت حذف شد',\n    'book_sort'                   => 'مرتب سازی کتاب',\n    'book_sort_notification'      => 'کتاب با موفقیت مرتب سازی شد',\n\n    // Bookshelves\n    'bookshelf_create'            => 'ایجاد قفسه',\n    'bookshelf_create_notification'    => 'قفسه کتاب با موفقیت ایجاد شد',\n    'bookshelf_create_from_book'    => 'تبدیل کتاب به قفسه',\n    'bookshelf_create_from_book_notification'    => 'کتاب با موفقیت به یک قفسه تبدیل شد',\n    'bookshelf_update'                 => 'به روزرسانی قفسه',\n    'bookshelf_update_notification'    => 'قفسه با موفقیت به روزرسانی شد',\n    'bookshelf_delete'                 => 'قفسه حذف شده',\n    'bookshelf_delete_notification'    => 'قفسه کتاب با موفقیت حذف شد',\n\n    // Revisions\n    'revision_restore' => 'نسخه بازیابی شده',\n    'revision_delete' => 'نسخه حذف شده',\n    'revision_delete_notification' => 'نسخه مورد نظر با موفقیت حذف شد',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" به علاقه مندی های شما اضافه شد',\n    'favourite_remove_notification' => '\":name\" از علاقه مندی های شما حذف شد',\n\n    // Watching\n    'watch_update_level_notification' => 'تنظیمات نظارت با موفقیت بروز شد',\n\n    // Auth\n    'auth_login' => 'وارد شده',\n    'auth_register' => 'ثبت نام شده بعنوان کاربر جدید',\n    'auth_password_reset_request' => 'بازیابی درخواست شده رمز عبور کاربر',\n    'auth_password_reset_update' => 'بازیابی رمز عبور کاربر',\n    'mfa_setup_method' => 'متد MFA پیکربندی شده',\n    'mfa_setup_method_notification' => 'روش چند فاکتوری با موفقیت پیکربندی شد',\n    'mfa_remove_method' => 'روش MFA حذف شده',\n    'mfa_remove_method_notification' => 'روش چند فاکتوری با موفقیت حذف شد',\n\n    // Settings\n    'settings_update' => 'تنظیمات بروز شده',\n    'settings_update_notification' => 'تنظیمات با موفقیت به روز شد',\n    'maintenance_action_run' => 'فعالیت نگهداری اجرا شده',\n\n    // Webhooks\n    'webhook_create' => 'ایجاد وب هوک',\n    'webhook_create_notification' => 'وب هوک با موفقیت ایجاد شد',\n    'webhook_update' => 'به روزرسانی وب هوک',\n    'webhook_update_notification' => 'وب هوک با موفقیت بروزرسانی شد',\n    'webhook_delete' => 'حذف وب هوک',\n    'webhook_delete_notification' => 'وب هوک با موفقیت حذف شد',\n\n    // Imports\n    'import_create' => 'ورودی ایجاد شد',\n    'import_create_notification' => 'فایل با موفقیت آپلود شد',\n    'import_run' => 'آیتم واردشده بروزرسانی شد',\n    'import_run_notification' => 'محتوا با موفقیت انتقال یافت',\n    'import_delete' => 'آیتم ورودی حدف شده',\n    'import_delete_notification' => 'آیتم واردشده با موفقیت حذف شد',\n\n    // Users\n    'user_create' => 'کاربر ایجاد شده',\n    'user_create_notification' => 'کاربر با موفقیت به ایجاد شد',\n    'user_update' => 'کاربر بروز شده',\n    'user_update_notification' => 'کاربر با موفقیت به روز شد',\n    'user_delete' => 'کاربر حذف شده',\n    'user_delete_notification' => 'کاربر با موفقیت حذف شد',\n\n    // API Tokens\n    'api_token_create' => 'ایجاد توکن API',\n    'api_token_create_notification' => 'توکن api با موفقیت ایجاد شد',\n    'api_token_update' => 'توکن api بروز شده',\n    'api_token_update_notification' => 'توکن API با موفقیت بروزرسانی شد',\n    'api_token_delete' => 'توکن api حذف شده',\n    'api_token_delete_notification' => 'توکن API با موفقیت حذف شد',\n\n    // Roles\n    'role_create' => 'نقش ایجاد شده',\n    'role_create_notification' => 'نقش با موفقیت ایجاد شد',\n    'role_update' => 'نقش بروز شده',\n    'role_update_notification' => 'نقش با موفقیت به روز شد',\n    'role_delete' => 'نقش حذف شده',\n    'role_delete_notification' => 'نقش با موفقیت حذف شد',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'سطل زباله خالی',\n    'recycle_bin_restore' => 'از سطل بازیافت، بازآوری شده است',\n    'recycle_bin_destroy' => 'از سطل بازیافت حذف شده است',\n\n    // Comments\n    'commented_on'                => 'ثبت دیدگاه',\n    'comment_create'              => 'نظر اضافه شده',\n    'comment_update'              => 'نظر به روز شده',\n    'comment_delete'              => 'نظر حذف شده',\n\n    // Sort Rules\n    'sort_rule_create' => 'قانون مرتب‌سازی ایجاد شد',\n    'sort_rule_create_notification' => 'قانون مرتب‌سازی با موفقیت ایجاد شد',\n    'sort_rule_update' => 'قانون مرتب‌سازی به‌روزرسانی شد',\n    'sort_rule_update_notification' => 'قانون مرتب‌سازی با موفقیت به‌روزرسانی شد',\n    'sort_rule_delete' => 'قانون مرتب‌سازی حذف شد',\n    'sort_rule_delete_notification' => 'قانون مرتب‌سازی با موفقیت حذف شد',\n\n    // Other\n    'permissions_update'          => 'به روزرسانی مجوزها',\n];\n"
  },
  {
    "path": "lang/fa/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'مشخصات وارد شده با اطلاعات ما سازگار نیست.',\n    'throttle' => 'دفعات تلاش شما برای ورود بیش از حد مجاز است. لطفا پس از :seconds ثانیه مجددا تلاش فرمایید.',\n\n    // Login & Register\n    'sign_up' => 'ثبت نام',\n    'log_in' => 'ورود',\n    'log_in_with' => 'ورود با :socialDriver',\n    'sign_up_with' => 'ثبت نام با :socialDriver',\n    'logout' => 'خروج',\n\n    'name' => 'نام',\n    'username' => 'نام کاربری',\n    'email' => 'پست الکترونیک',\n    'password' => 'کلمه عبور',\n    'password_confirm' => 'تایید کلمه عبور',\n    'password_hint' => 'باید بیش از 8 کاراکتر باشد',\n    'forgot_password' => 'کلمه عبور خود را فراموش کرده اید؟',\n    'remember_me' => 'مرا به خاطر بسپار',\n    'ldap_email_hint' => 'لطفا برای استفاده از این حساب کاربری پست الکترونیک وارد نمایید.',\n    'create_account' => 'ایجاد حساب کاربری',\n    'already_have_account' => 'قبلا ثبت نام نموده اید؟',\n    'dont_have_account' => 'حساب کاربری ندارید؟',\n    'social_login' => 'ورود از طریق شبکه اجتماعی',\n    'social_registration' => 'ثبت نام از طریق شبکه اجتماعی',\n    'social_registration_text' => 'با استفاده از سرویس دیگری ثبت نام نموده و وارد سیستم شوید.',\n\n    'register_thanks' => 'از ثبت نام شما متشکریم!',\n    'register_confirm' => 'لطفا پست الکترونیک خود را بررسی نموده و برای دسترسی به:appName دکمه تایید را کلیک نمایید.',\n    'registrations_disabled' => 'ثبت نام در حال حاضر غیر فعال است',\n    'registration_email_domain_invalid' => 'دامنه پست الکترونیک به این برنامه دسترسی ندارد',\n    'register_success' => 'از ثبت نام شما سپاسگزاریم! شما اکنون ثبت نام کرده و وارد سیستم شده اید.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'تلاش برای ورود',\n    'auto_init_starting_desc' => 'برای شروع فرآیند ورود به سیستم با سیستم احراز هویت شما تماس می گیریم. اگر بعد از 5 ثانیه پیشرفتی حاصل نشد، می توانید روی لینک زیر کلیک کنید.',\n    'auto_init_start_link' => 'احراز هویت را ادامه دهید',\n\n    // Password Reset\n    'reset_password' => 'بازنشانی کلمه عبور',\n    'reset_password_send_instructions' => 'پست الکترونیک خود را در کادر زیر وارد نموده تا یک پیام حاوی لینک بازنشانی کلمه عبور دریافت نمایید.',\n    'reset_password_send_button' => 'ارسال لینک بازنشانی',\n    'reset_password_sent' => 'در صورت موجود بودن پست الکترونیک، یک لینک بازنشانی کلمه عبور برای شما ارسال خواهد شد.',\n    'reset_password_success' => 'کلمه عبور شما با موفقیت بازنشانی شد.',\n    'email_reset_subject' => 'بازنشانی کلمه عبور :appName',\n    'email_reset_text' => 'شما این پیام را به علت درخواست بازنشانی کلمه عبور دریافت می نمایید.',\n    'email_reset_not_requested' => 'در صورتی که درخواست بازنشانی کلمه عبور از سمت شما نمی باشد، نیاز به انجام هیچ فعالیتی ندارید.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'پست الکترونیک خود را در:appName تایید نمایید',\n    'email_confirm_greeting' => 'برای پیوستن به :appName متشکریم!',\n    'email_confirm_text' => 'لطفا با کلیک بر روی دکمه زیر پست الکترونیک خود را تایید نمایید:',\n    'email_confirm_action' => 'تایید پست الکترونیک',\n    'email_confirm_send_error' => 'تایید پست الکترونیک الزامی می باشد، اما سیستم قادر به ارسال پیام نمی باشد.',\n    'email_confirm_success' => 'ایمیل شما تایید شد! اکنون باید بتوانید با استفاده از این آدرس ایمیل وارد شوید.',\n    'email_confirm_resent' => 'پیام تایید پست الکترونیک مجدد ارسال گردید، لطفا صندوق ورودی خود را بررسی نمایید.',\n    'email_confirm_thanks' => 'تشکر بابت تایید!',\n    'email_confirm_thanks_desc' => 'لطفاً یک لحظه صبر کنید تا تأیید شما بررسی شود. اگر بعد از 3 ثانیه هدایت نشدید، بر روی لینک \"ادامه\" کلیک کنید تا ادامه دهید.',\n\n    'email_not_confirmed' => 'پست الکترونیک تایید نشده است',\n    'email_not_confirmed_text' => 'پست الکترونیک شما هنوز تایید نشده است.',\n    'email_not_confirmed_click_link' => 'لطفا بر روی لینک موجود در پیامی که بلافاصله پس از ثبت نام ارسال شده است کلیک نمایید.',\n    'email_not_confirmed_resend' => 'در صورتی که نمی توانید پیام را پیدا کنید، می توانید با ارسال فرم زیر، پیام تایید را مجدد دریافت نمایید.',\n    'email_not_confirmed_resend_button' => 'ارسال مجدد تایید پست الکترونیک',\n\n    // User Invite\n    'user_invite_email_subject' => 'از شما برای پیوستن به :appName دعوت شده است!',\n    'user_invite_email_greeting' => 'حساب کاربری برای شما در :appName ایجاد شده است.',\n    'user_invite_email_text' => 'برای تنظیم کلمه عبور و دسترسی به حساب کاربری بر روی دکمه زیر کلیک نمایید:',\n    'user_invite_email_action' => 'تنظیم کلمه عبور حساب‌کاربری',\n    'user_invite_page_welcome' => 'به :appName خوش آمدید!',\n    'user_invite_page_text' => 'برای نهایی کردن حساب کاربری خود در :appName و دسترسی به آن، می بایست یک کلمه عبور تنظیم نمایید.',\n    'user_invite_page_confirm_button' => 'تایید کلمه عبور',\n    'user_invite_success_login' => 'رمز عبور تنظیم شده است، اکنون باید بتوانید با استفاده از رمز عبور تعیین شده خود وارد شوید تا به :appName دسترسی پیدا کنید!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'تنظیم احراز هویت چند مرحله‌ای',\n    'mfa_setup_desc' => 'تنظیم احراز هویت چند مرحله ای یک لایه امنیتی دیگر به حساب شما اضافه میکند.',\n    'mfa_setup_configured' => 'هم اکنون تنظیم شده است.',\n    'mfa_setup_reconfigure' => 'تنظیم مجدد',\n    'mfa_setup_remove_confirmation' => 'از حذف احراز هویت چند مرحله ای اطمینان دارید؟',\n    'mfa_setup_action' => 'تنظیم',\n    'mfa_backup_codes_usage_limit_warning' => 'کمتر از 5 کد پشتیبان باقی مانده است، لطفاً قبل از تمام شدن کدها یک مجموعه جدید ایجاد و ذخیره کنید تا از قفل شدن حساب خود جلوگیری کنید.',\n    'mfa_option_totp_title' => 'برنامه ی موبایل',\n    'mfa_option_totp_desc' => 'برای استفاده از احراز هویت چند عاملی به یک برنامه موبایلی نیاز دارید که از TOTP پشتیبانی کند، مانند Google Authenticator، Authy یا Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'کدهای پشتیبان',\n    'mfa_option_backup_codes_desc' => 'این فرایند مجموعه‌ای از کدهای پشتیبان یک‌بار مصرف تولید می‌کند که هنگام ورود به سامانه جهت تأیید هویت باید از آن‌ها استفاده کنید. توصیه می‌شود این کدها را در محلّی امن و محفوظ نگهداری نمایید.',\n    'mfa_gen_confirm_and_enable' => 'تایید و فعال کنید',\n    'mfa_gen_backup_codes_title' => 'راه اندازی کدهای پشتیبان',\n    'mfa_gen_backup_codes_desc' => 'لیست کدهای زیر را در مکانی امن ذخیره کنید. هنگام دسترسی به سیستم، می توانید از یکی از کدها به عنوان مکانیزم احراز هویت دوم استفاده کنید.',\n    'mfa_gen_backup_codes_download' => 'دانلود کدها',\n    'mfa_gen_backup_codes_usage_warning' => 'هر کد فقط یک بار قابل استفاده است',\n    'mfa_gen_totp_title' => 'راه اندازی اپلیکیشن موبایل',\n    'mfa_gen_totp_desc' => 'برای استفاده از احراز هویت چند عاملی به یک برنامه موبایلی نیاز دارید که از TOTP پشتیبانی کند، مانند Google Authenticator، Authy یا Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'برای شروع، کد QR زیر را با استفاده از برنامه احراز هویت ترجیحی خود اسکن کنید.',\n    'mfa_gen_totp_verify_setup' => 'تأیید تنظیمات',\n    'mfa_gen_totp_verify_setup_desc' => 'با وارد کردن کدی که در برنامه احراز هویت شما ایجاد شده است، در کادر ورودی زیر، مطمئن شوید که همه کار می کنند:',\n    'mfa_gen_totp_provide_code_here' => 'کد تولید شده برنامه خود را در اینجا ارائه دهید',\n    'mfa_verify_access' => 'تأیید دسترسی',\n    'mfa_verify_access_desc' => 'قبل از اینکه به شما اجازه دسترسی داده شود، حساب کاربری شما از شما می خواهد که هویت خود را از طریق یک سطح تأیید اضافی تأیید کنید. برای ادامه، با استفاده از یکی از روش های پیکربندی شده خود، تأیید کنید.',\n    'mfa_verify_no_methods' => 'هیچ روشی پیکربندی نشده است',\n    'mfa_verify_no_methods_desc' => 'هیچ روش احراز هویت چند عاملی برای حساب شما یافت نشد. قبل از دسترسی، باید حداقل یک روش را تنظیم کنید.',\n    'mfa_verify_use_totp' => 'با استفاده از یک برنامه تلفن همراه تأیید کنید',\n    'mfa_verify_use_backup_codes' => 'با استفاده از یک کد پشتیبان تأیید کنید',\n    'mfa_verify_backup_code' => 'کد پشتیبان',\n    'mfa_verify_backup_code_desc' => 'یکی از کدهای پشتیبان باقی مانده خود را در زیر وارد کنید:',\n    'mfa_verify_backup_code_enter_here' => 'کد پشتیبان را در اینجا وارد کنید',\n    'mfa_verify_totp_desc' => 'کد ایجاد شده با استفاده از برنامه تلفن همراه خود را در زیر وارد کنید:',\n    'mfa_setup_login_notification' => 'روش چند عاملی پیکربندی شد، لطفاً اکنون دوباره با استفاده از روش پیکربندی شده وارد شوید.',\n];\n"
  },
  {
    "path": "lang/fa/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'لغو',\n    'close' => 'خروج',\n    'confirm' => 'تایید',\n    'back' => 'بازگشت',\n    'save' => 'ذخیره',\n    'continue' => 'ادامه',\n    'select' => 'انتخاب',\n    'toggle_all' => 'معکوس کردن همه',\n    'more' => 'بیشتر',\n\n    // Form Labels\n    'name' => 'نام',\n    'description' => 'توضیحات',\n    'role' => 'نقش',\n    'cover_image' => 'تصویر روی جلد',\n    'cover_image_description' => 'تصویر باید حدودا 250*440 پیکسل باشد اما از آنجا که متناسب با رابط کاربری کوچک و بزرگ شده و بریده میشود، پس ابعاد نمایش داده شده متفاوت خواهند بود.',\n\n    // Actions\n    'actions' => 'عملیات',\n    'view' => 'نمایش',\n    'view_all' => 'نمایش همه',\n    'new' => 'جدید',\n    'create' => 'ایجاد',\n    'update' => 'به‌روز رسانی',\n    'edit' => 'ويرايش',\n    'archive' => 'انتقال به بایگانی',\n    'unarchive' => 'فعّال‌سازی دوباره (خروج از بایگانی)',\n    'sort' => 'مرتب سازی',\n    'move' => 'جابجایی',\n    'copy' => 'کپی',\n    'reply' => 'پاسخ',\n    'delete' => 'حذف',\n    'delete_confirm' => 'تأیید حذف',\n    'search' => 'جستجو',\n    'search_clear' => 'پاک کردن جستجو',\n    'reset' => 'بازنشانی',\n    'remove' => 'حذف',\n    'add' => 'ﺍﻓﺰﻭﺩﻥ',\n    'configure' => 'پیکربندی کنید',\n    'manage' => 'مدیریت تنظیمات',\n    'fullscreen' => 'تمام صفحه',\n    'favourite' => 'علاقه‌مندی',\n    'unfavourite' => 'حذف از علاقه‌مندی',\n    'next' => 'بعدی',\n    'previous' => 'قبلى',\n    'filter_active' => 'فیلتر فعال:',\n    'filter_clear' => 'پاک کردن فیلتر',\n    'download' => 'دانلود',\n    'open_in_tab' => 'باز کردن در تب جدید',\n    'open' => 'بازکردن',\n\n    // Sort Options\n    'sort_options' => 'گزینه‌های مرتب سازی',\n    'sort_direction_toggle' => 'معکوس کردن جهت مرتب سازی',\n    'sort_ascending' => 'مرتب‌سازی صعودی',\n    'sort_descending' => 'مرتب‌سازی نزولی',\n    'sort_name' => 'نام',\n    'sort_default' => 'پیش‌فرض',\n    'sort_created_at' => 'تاریخ ایجاد',\n    'sort_updated_at' => 'تاریخ بروزرسانی',\n\n    // Misc\n    'deleted_user' => 'کاربر حذف شده',\n    'no_activity' => 'بایگانی برای نمایش وجود ندارد',\n    'no_items' => 'هیچ موردی در دسترس نیست',\n    'back_to_top' => 'بازگشت به بالا',\n    'skip_to_main_content' => 'رفتن به محتوای اصلی',\n    'toggle_details' => 'معکوس کردن اطلاعات',\n    'toggle_thumbnails' => 'معکوس کردن ریزعکس‌ها',\n    'details' => 'جزییات',\n    'grid_view' => 'نمایش شبکه‌ای',\n    'list_view' => 'نمای لیست',\n    'default' => 'پیش‌فرض',\n    'breadcrumb' => 'مسیر جاری',\n    'status' => 'وضعیت',\n    'status_active' => 'فعال',\n    'status_inactive' => 'غیر فعال',\n    'never' => 'هرگز',\n    'none' => 'هیچکدام',\n\n    // Header\n    'homepage' => 'صفحه اصلی',\n    'header_menu_expand' => 'گسترش منو',\n    'profile_menu' => 'منو پروفایل',\n    'view_profile' => 'مشاهده پروفایل',\n    'edit_profile' => 'ویرایش پروفایل',\n    'dark_mode' => 'حالت تاریک',\n    'light_mode' => 'حالت روشن',\n    'global_search' => 'جستجوی سراسری',\n\n    // Layout tabs\n    'tab_info' => 'اطلاعات',\n    'tab_info_label' => 'زبانه: نمایش اطلاعات ثانویه',\n    'tab_content' => 'محتوا',\n    'tab_content_label' => 'زبانه: نمایش محتوای اصلی',\n\n    // Email Content\n    'email_action_help' => 'اگر با دکمه بالا مشکلی دارید ، ادرس وبسایت *URLزیر را در مرورگر وب خود کپی و پیست کنید:',\n    'email_rights' => 'تمام حقوق محفوظ است',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'سیاست حفظ حریم خصوصی',\n    'terms_of_service' => 'شرایط خدمات',\n\n    // OpenSearch\n    'opensearch_description' => 'جست‌وجو در :appName',\n];\n"
  },
  {
    "path": "lang/fa/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'انتخاب تصویر',\n    'image_list' => 'لیست تصاویر',\n    'image_details' => 'جزئیات تصویر',\n    'image_upload' => 'بارگذاری تصویر',\n    'image_intro' => 'در اینجا می توانید تصاویری که قبلاً در سیستم آپلود شده اند را انتخاب و مدیریت کنید.',\n    'image_intro_upload' => 'با کشیدن یک فایل تصویری به این پنجره یا با استفاده از دکمه \"بارگذاری تصویر\" در بالا، یک تصویر جدید آپلود کنید.',\n    'image_all' => 'همه',\n    'image_all_title' => 'نمایش تمام تصاویر',\n    'image_book_title' => 'تصاویر بارگذاری شده در این کتاب را مشاهده کنید',\n    'image_page_title' => 'تصاویر بارگذاری شده در این صفحه را مشاهده کنید',\n    'image_search_hint' => 'جستجو بر اساس نام تصویر',\n    'image_uploaded' => 'بارگذاری شده :uploadedDate',\n    'image_uploaded_by' => 'بارگذاری شده توسط:userName',\n    'image_uploaded_to' => 'بارگذاری شده در صفحه :pageLink',\n    'image_updated' => 'به‌روزرسانی شده در:updateDate',\n    'image_load_more' => 'بارگذاری بیشتر',\n    'image_image_name' => 'نام تصویر',\n    'image_delete_used' => 'این تصویر در صفحات زیر استفاده شده است.',\n    'image_delete_confirm_text' => 'آیا مطمئن هستید که میخواهید این عکس را پاک کنید؟',\n    'image_select_image' => 'انتخاب تصویر',\n    'image_dropzone' => 'تصاویر را رها کنید یا برای بارگذاری اینجا را کلیک کنید',\n    'image_dropzone_drop' => 'تصویر را برای بارگذاری به اینجا بکشید و رها کنید',\n    'images_deleted' => 'تصاویر حذف شده',\n    'image_preview' => 'پیش نمایش تصویر',\n    'image_upload_success' => 'تصویر با موفقیت بارگذاری شد',\n    'image_update_success' => 'جزئیات تصویر با موفقیت به روز شد',\n    'image_delete_success' => 'تصویر با موفقیت حذف شد',\n    'image_replace' => 'جایگزینی تصویر',\n    'image_replace_success' => 'تصویر با موفقیت به روز شد',\n    'image_rebuild_thumbs' => 'بازتولید اندازه‌های گوناگونی از تصویر',\n    'image_rebuild_thumbs_success' => 'اندازه‌های گوناگونی از تصویر با موفقیت بازتولید شدند.',\n\n    // Code Editor\n    'code_editor' => 'ویرایش کد',\n    'code_language' => 'زبان کد',\n    'code_content' => 'محتوی کد',\n    'code_session_history' => 'تاریخچه جلسات',\n    'code_save' => 'ذخیره کد',\n];\n"
  },
  {
    "path": "lang/fa/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'عمومی',\n    'advanced' => 'پیشرفته',\n    'none' => 'هیچ کدام',\n    'cancel' => 'لغو',\n    'save' => 'ذخیره',\n    'close' => 'بستن',\n    'apply' => 'اعمال',\n    'undo' => 'برگشت',\n    'redo' => 'از نو',\n    'left' => 'چپ',\n    'center' => 'مرکز',\n    'right' => 'راست',\n    'top' => 'بالا',\n    'middle' => 'میانه',\n    'bottom' => 'پایین',\n    'width' => 'عرض',\n    'height' => 'ارتفاع',\n    'More' => 'بیشتر',\n    'select' => 'انتخاب...',\n\n    // Toolbar\n    'formats' => 'الگو',\n    'header_large' => 'عنوان بزرگ',\n    'header_medium' => 'عنوان متوسط',\n    'header_small' => 'عنوان کوچک',\n    'header_tiny' => 'هدر کوچک',\n    'paragraph' => 'پاراگراف',\n    'blockquote' => 'نقل قول',\n    'inline_code' => 'کد درون خطی',\n    'callouts' => 'قالب پیام و هشدار',\n    'callout_information' => 'اطلاعات',\n    'callout_success' => 'موفق',\n    'callout_warning' => 'هشدار',\n    'callout_danger' => 'خطر',\n    'bold' => 'توپر',\n    'italic' => 'ایتالیک',\n    'underline' => 'زیرخط',\n    'strikethrough' => 'خط خورده',\n    'superscript' => 'بالانویسی',\n    'subscript' => 'پایین نویسی',\n    'text_color' => 'رنگ متن',\n    'highlight_color' => 'رنگ هایلایت',\n    'custom_color' => 'رنگ دلخواه',\n    'remove_color' => 'حذف رنگ',\n    'background_color' => 'رنگ زمینه',\n    'align_left' => 'چپ چین',\n    'align_center' => 'وسط چین',\n    'align_right' => 'راست چین',\n    'align_justify' => 'همتراز',\n    'list_bullet' => 'لیست نشانه دار',\n    'list_numbered' => 'لیست عددی',\n    'list_task' => 'لیست کار',\n    'indent_increase' => 'افزایش تورفتگی',\n    'indent_decrease' => 'کاهش تورفتگی',\n    'table' => 'جدول',\n    'insert_image' => 'افزودن تصویر',\n    'insert_image_title' => 'افزودن/ویرایش تصویر',\n    'insert_link' => 'افزودن/ویرایش پیوند',\n    'insert_link_title' => 'افزودن/ویرایش پیوند',\n    'insert_horizontal_line' => 'افزودن خط افقی',\n    'insert_code_block' => 'افزودن بلوک کد',\n    'edit_code_block' => 'code block را ویرایش کنید',\n    'insert_drawing' => 'افزودن/ویرایش طرح',\n    'drawing_manager' => 'مدیریت طراحی',\n    'insert_media' => 'افزودن/ویرایش رسانه',\n    'insert_media_title' => 'افزودن/ویرایش رسانه',\n    'clear_formatting' => 'حذف قالب بندی',\n    'source_code' => 'کد منبع',\n    'source_code_title' => 'کد منبع',\n    'fullscreen' => 'تمام صفحه',\n    'image_options' => 'تنظیمات تصویر',\n\n    // Tables\n    'table_properties' => 'تنظیمات جدول',\n    'table_properties_title' => 'تنظیمات جدول',\n    'delete_table' => 'حذف جدول',\n    'table_clear_formatting' => 'حذف قالب‌بندی جدول',\n    'resize_to_contents' => 'تغییر اندازه بر اساس محتوا',\n    'row_header' => 'عنوان سطر',\n    'insert_row_before' => 'افزودن سطر به قبل',\n    'insert_row_after' => 'افزودن سطر به بعد',\n    'delete_row' => 'حذف سطر',\n    'insert_column_before' => 'افزودن ستون به قبل',\n    'insert_column_after' => 'افزودن ستون به بعد',\n    'delete_column' => 'حذف ستون',\n    'table_cell' => 'سلول',\n    'table_row' => 'سطر',\n    'table_column' => 'ستون',\n    'cell_properties' => 'تنظیمات سلول',\n    'cell_properties_title' => 'تنظیمات سلول',\n    'cell_type' => 'نوع سلول',\n    'cell_type_cell' => 'سلول',\n    'cell_scope' => 'محدوده',\n    'cell_type_header' => 'بالای سلول',\n    'merge_cells' => 'ادغام سلول ها',\n    'split_cell' => 'جداسازی سلول ها',\n    'table_row_group' => 'گروه بندی سطر',\n    'table_column_group' => 'گروه بندی ستون',\n    'horizontal_align' => 'تراز افقی',\n    'vertical_align' => 'تراز عمودی',\n    'border_width' => 'پهنای حاشیه',\n    'border_style' => 'سبک حاشیه',\n    'border_color' => 'رنگ حاشیه',\n    'row_properties' => 'تنظیمات سطر',\n    'row_properties_title' => 'تنظیمات سطر',\n    'cut_row' => 'برش سطر',\n    'copy_row' => 'کپی سطر',\n    'paste_row_before' => 'چسباندن سطر به قبل',\n    'paste_row_after' => 'چسباندن سطر به بعد',\n    'row_type' => 'نوع سطر',\n    'row_type_header' => 'سربرگ',\n    'row_type_body' => 'بدنه',\n    'row_type_footer' => 'پانوشت',\n    'alignment' => 'ترازبندی',\n    'cut_column' => 'برش ستون',\n    'copy_column' => 'کپی ستون',\n    'paste_column_before' => 'چسباندن ستون به قبل',\n    'paste_column_after' => 'چسباندن ستون به بعد',\n    'cell_padding' => 'حاشیه سلول',\n    'cell_spacing' => 'فاصله سلول',\n    'caption' => 'عنوان',\n    'show_caption' => 'مشاهده عنوان',\n    'constrain' => 'محدودسازی نسبت ها',\n    'cell_border_solid' => 'یکپارچه',\n    'cell_border_dotted' => 'نقطه چین',\n    'cell_border_dashed' => 'خط چین',\n    'cell_border_double' => 'دوتایی',\n    'cell_border_groove' => 'شیار',\n    'cell_border_ridge' => 'لبه',\n    'cell_border_inset' => 'داخل',\n    'cell_border_outset' => 'خارج',\n    'cell_border_none' => 'هیچ کدام',\n    'cell_border_hidden' => 'مخفی',\n\n    // Images, links, details/summary & embed\n    'source' => 'منبع',\n    'alt_desc' => 'توضیحات جایگزین',\n    'embed' => 'درج',\n    'paste_embed' => 'کد درج را در زیر وارد نمایید:',\n    'url' => 'آدرس',\n    'text_to_display' => 'متن جهت نمایش',\n    'title' => 'عنوان',\n    'browse_links' => 'مرور پیوندها',\n    'open_link' => 'بازکردن لینک',\n    'open_link_in' => 'باز کردن لینک در ...',\n    'open_link_current' => 'پنجره کنونی',\n    'open_link_new' => 'پنجره جدید',\n    'remove_link' => 'حذف لینک',\n    'insert_collapsible' => 'درج بلوک جمع شونده',\n    'collapsible_unwrap' => 'باز کردن',\n    'edit_label' => 'ویرایش برچسب',\n    'toggle_open_closed' => 'بستن/بازکردن',\n    'collapsible_edit' => 'ویرایش بلوک جمع شونده',\n    'toggle_label' => 'تغییر برچسب',\n\n    // About view\n    'about' => 'درباره ویرایشگر',\n    'about_title' => 'درباره ویرایشگر WYSIWYG',\n    'editor_license' => 'مجوز و حق کپی رایت ویرایشگر',\n    'editor_lexical_license' => 'این ویرایشگر بر پایه‌ی نسخه‌ای مشتق‌شده از «:lexicalLink» ساخته شده است که تحت مجوز MIT منتشر می‌شود.',\n    'editor_lexical_license_link' => 'جزئیات کامل مجوز را می‌توانید این‌جا مشاهده کنید.',\n    'editor_tiny_license' => 'این ویرایشگر توسط :tinyLink و تحت مجوز MIT ساخته شده است.',\n    'editor_tiny_license_link' => 'جزئیات کپی رایت و مجوز TinyMCE را می توانید در اینجا پیدا کنید.',\n    'save_continue' => 'ذخیره صفحه و ادامه',\n    'callouts_cycle' => '(جهت تغییر نوع ها چندین بار فشار دهید)',\n    'link_selector' => 'پیوند به محتوا',\n    'shortcuts' => 'کلیدهای میانبر',\n    'shortcut' => 'میانبر',\n    'shortcuts_intro' => 'میانبرهای قابل استفاده در این ویرایشگر:',\n    'windows_linux' => '(ویندوز/لینوکس)',\n    'mac' => '(مک)',\n    'description' => 'توضیحات',\n];\n"
  },
  {
    "path": "lang/fa/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'تازه ایجاد شده',\n    'recently_created_pages' => 'صفحه‌های تازه ایجاد شده',\n    'recently_updated_pages' => 'صفحه‌های تازه به‌روزرسانی‌شده',\n    'recently_created_chapters' => 'فصل‌های تازه ایجاد شده',\n    'recently_created_books' => 'کتاب های اخیرا ایجاد شده',\n    'recently_created_shelves' => 'قفسه کتاب های اخیرا ایجاد شده',\n    'recently_update' => 'اخیرا به روز شده',\n    'recently_viewed' => 'اخیرا مشاهده شده',\n    'recent_activity' => 'فعالیت‌های اخیر',\n    'create_now' => 'اکنون یکی ایجاد کنید',\n    'revisions' => 'بازبینی‌ها',\n    'meta_revision' => 'بازبینی #:revisionCount',\n    'meta_created' => 'ایجاد شده :timeLength',\n    'meta_created_name' => 'ایجاد شده :timeLength توسط :user',\n    'meta_updated' => 'به روزرسانی شده :timeLength',\n    'meta_updated_name' => 'به روزرسانی شده :timeLength توسط :user',\n    'meta_owned_name' => 'متعلق به :user',\n    'meta_reference_count' => 'در 1 صفحه به آن ارجاع داده شده|در :count صفحه به آن ارجاع داده شده',\n    'entity_select' => 'انتخاب موجودیت',\n    'entity_select_lack_permission' => 'شما مجوزهای لازم برای انتخاب این مورد را ندارید',\n    'images' => 'عکس‌ها',\n    'my_recent_drafts' => 'پیش نویس های اخیر من',\n    'my_recently_viewed' => 'بازدیدهای اخیر من',\n    'my_most_viewed_favourites' => 'محبوب ترین موارد مورد علاقه من',\n    'my_favourites' => 'مورد علاقه من',\n    'no_pages_viewed' => 'شما هیچ صفحه ای را مشاهده نکرده اید',\n    'no_pages_recently_created' => 'اخیرا هیچ صفحه ای ایجاد نشده است',\n    'no_pages_recently_updated' => 'اخیرا هیچ صفحه ای به روزرسانی نشده است',\n    'export' => 'خروجی',\n    'export_html' => 'فایل وب موجود است',\n    'export_pdf' => 'فایل PDF',\n    'export_text' => 'پرونده متنی ساده',\n    'export_md' => 'راهنما مارک‌دون',\n    'export_zip' => 'فایل فشرده‌ی زیپ',\n    'default_template' => 'قالب پیش‌فرض صفحه',\n    'default_template_explain' => 'قالبی برای صفحه تعیین کنید که به‌عنوان محتوای پیش‌فرض در تمام صفحاتی که در این مورد ایجاد می‌شوند، به‌کار رود. توجه داشته باشید این قالب تنها در صورتی اعمال می‌شود که سازندهٔ صفحه به صفحهٔ قالب انتخاب‌شده دسترسی نمایشی داشته باشد.',\n    'default_template_select' => 'انتخاب صفحهٔ قالب',\n    'import' => 'وارد کردن',\n    'import_validate' => 'اعتبارسنجی آیتم‌های واردشده',\n    'import_desc' => 'می‌توانید کتاب‌ها، فصل‌ها و صفحه‌ها را با استفاده از یک فایل فشردهٔ ZIP وارد کنید که از همین سامانه یا نمونه‌ای دیگر استخراج شده است. برای ادامه، فایل ZIP را انتخاب و بارگذاری کنید. پس از بارگذاری و اعتبارسنجی، در مرحلهٔ بعد می‌توانید تنظیمات ورود را انجام داده و آن را تأیید کنید.',\n    'import_zip_select' => 'انتخاب فایل ZIP برای بارگذاری',\n    'import_zip_validation_errors' => 'هنگام اعتبارسنجی فایل ZIP ارائه‌شده، خطاهایی شناسایی شد:',\n    'import_pending' => 'ورودی‌های در انتظار انتقال',\n    'import_pending_none' => 'هیچ انتقال ورودی آغاز نشده است.',\n    'import_continue' => 'ادامه فرآیند انتقال ورودی',\n    'import_continue_desc' => 'محتوای فایل ZIP بارگذاری‌شده را که قرار است وارد سامانه شود، مرور کنید. پس از اطمینان از صحت آن، انتقال را آغاز نمایید تا محتوا به این سامانه افزوده شود. توجه داشته باشید که پس از انتقال موفق، فایل ZIP بارگذاری‌شده به‌صورت خودکار حذف خواهد شد.',\n    'import_details' => 'جزئیات انتقال ورودی',\n    'import_run' => 'شروع فرایند انتقال ورودی',\n    'import_size' => 'حجم فایل ZIP واردشده:size',\n    'import_uploaded_at' => 'زمان بارگذاری:relativeTime',\n    'import_uploaded_by' => 'بارگذاری شده توسط',\n    'import_location' => 'مکان انتقال',\n    'import_location_desc' => 'برای محتوای واردشده، مقصدی انتخاب کنید. برای ایجاد محتوا در آن مقصد، داشتن مجوزهای لازم ضروری است.',\n    'import_delete_confirm' => 'مطمئن هستید که می‌خواهید آیتم واردشده را حدف کنید؟',\n    'import_delete_desc' => 'با انجام این کار، فایل ZIP واردشده حذف می‌شود و این عمل بازگشت‌ناپذیر است.',\n    'import_errors' => 'خطای انتقال ورودی',\n    'import_errors_desc' => 'در جریان تلاش برای انتقال ورودی، خطاهای زیر رخ داد:',\n    'breadcrumb_siblings_for_page' => 'پیمایش صفحات هم‌سطح',\n    'breadcrumb_siblings_for_chapter' => 'پیمایش فصل‌های هم‌سطح',\n    'breadcrumb_siblings_for_book' => 'پیمایش کتاب‌های هم‌سطح',\n    'breadcrumb_siblings_for_bookshelf' => 'پیمایش قفسه‌های هم‌سطح',\n\n    // Permissions and restrictions\n    'permissions' => 'مجوزها',\n    'permissions_desc' => 'مجوزها را در اینجا تنظیم کنید تا مجوزهای پیش فرض تنظیم شده برای نقش های کاربر را لغو کنید.',\n    'permissions_book_cascade' => 'مجوزهای تنظیم‌شده روی کتاب‌ها به‌طور خودکار به فصل‌ها و صفحات داخل آن اختصاص داده می‌شوند، مگر اینکه مجوزهای اختصاصی برای آن‌ها تعریف شده باشد.',\n    'permissions_chapter_cascade' => 'مجوزهای تنظیم‌شده روی فصل‌ها به‌طور خودکار به صفحات داخل آن اختصاص داده می‌شوند، مگر اینکه مجوزهای اختصاصی برای آن‌ها تعریف شده باشد.',\n    'permissions_save' => 'ذخيره مجوزها',\n    'permissions_owner' => 'مالک',\n    'permissions_role_everyone_else' => 'سایر کاربران',\n    'permissions_role_everyone_else_desc' => 'مجوزها را برای نقش‌هایی تنظیم کنید که به طور خاص لغو نشده‌اند.',\n    'permissions_role_override' => 'تنظیم مجدد مجوز برای نقش',\n    'permissions_inherit_defaults' => 'ارث بردن از مجوزهای پیش‌فرض',\n\n    // Search\n    'search_results' => 'نتایج جستجو',\n    'search_total_results_found' => 'نتیجه یافت شد:count | نتایج یافت شده:count',\n    'search_clear' => 'پاک کردن جستجو',\n    'search_no_pages' => 'هیچ صفحه ای با این جستجو مطابقت ندارد',\n    'search_for_term' => 'جستجو برای :term',\n    'search_more' => 'نتایج بیشتر',\n    'search_advanced' => 'جستجوی پیشرفته',\n    'search_terms' => 'عبارات جستجو',\n    'search_content_type' => 'نوع محتوا',\n    'search_exact_matches' => 'مطابقت کامل',\n    'search_tags' => 'جستجو در برچسب‌ها',\n    'search_options' => 'گزینه‌ها',\n    'search_viewed_by_me' => 'بازدید شده به وسیله من',\n    'search_not_viewed_by_me' => 'توسط من مشاهده نشده است',\n    'search_permissions_set' => 'مجوزها تنظیم شده است',\n    'search_created_by_me' => 'ایجاد شده توسط من',\n    'search_updated_by_me' => 'به روز شده توسط من',\n    'search_owned_by_me' => 'متعلق به من است',\n    'search_date_options' => 'تنظیمات تاریخ',\n    'search_updated_before' => 'قبلا به روز شده',\n    'search_updated_after' => 'پس از به روز رسانی',\n    'search_created_before' => 'ایجاد شده قبل از',\n    'search_created_after' => 'ایجاد شده پس از',\n    'search_set_date' => 'تنظیم تاریخ',\n    'search_update' => 'جستجو را به روز کنید',\n\n    // Shelves\n    'shelf' => 'قفسه',\n    'shelves' => 'قفسه‌ها',\n    'x_shelves' => ':count قفسه|:count قفسه‌ها',\n    'shelves_empty' => 'هیچ قفسه ای ایجاد نشده است',\n    'shelves_create' => 'ایجاد قفسه جدید',\n    'shelves_popular' => 'قفسه‌های محبوب',\n    'shelves_new' => 'قفسه‌های جدید',\n    'shelves_new_action' => 'قفسه جدید',\n    'shelves_popular_empty' => 'محبوب ترین قفسه ها در اینجا ظاهر می شوند.',\n    'shelves_new_empty' => 'جدیدترین قفسه های ایجاد شده در اینجا ظاهر می شوند.',\n    'shelves_save' => 'ذخیره قفسه',\n    'shelves_books' => 'کتاب‌های موجود در این قفسه',\n    'shelves_add_books' => 'کتاب ها را به این قفسه اضافه کنید',\n    'shelves_drag_books' => 'کتاب‌ها را به اینجا بکشید تا به این قفسه اضافه شوند',\n    'shelves_empty_contents' => 'این قفسه هیچ کتابی به آن اختصاص داده نشده است',\n    'shelves_edit_and_assign' => 'برای اختصاص کتاب‌ها، قفسه را ویرایش کنید',\n    'shelves_edit_named' => 'ویرایش قفسه :name',\n    'shelves_edit' => 'ویرایش قفسه',\n    'shelves_delete' => 'حذف قفسه',\n    'shelves_delete_named' => 'حذف قفسه :name',\n    'shelves_delete_explain' => \"با این کار قفسه کتاب با نام ':name' حذف می‌شود. کتاب های موجود حذف نمی‌شوند.\",\n    'shelves_delete_confirmation' => 'آیا مطمئن هستید که می‌خواهید این قفسه را حذف کنید؟',\n    'shelves_permissions' => 'مجوزهای قفسه',\n    'shelves_permissions_updated' => 'مجوزهای کانال بروزرسانی شد',\n    'shelves_permissions_active' => 'مجوزهای قفسه فعال است',\n    'shelves_permissions_cascade_warning' => 'مجوزهای موجود در قفسه‌ها به طور خودکار به کتاب‌های حاوی اطلاق نمی‌شوند. دلیل آن این است که یک کتاب می تواند در چندین قفسه وجود داشته باشد. با این حال، مجوزها را می‌توان با استفاده از گزینه پایین همین صفحه در کتاب‌های فرزند کپی کرد.',\n    'shelves_permissions_create' => 'مجوزهای «ایجاد» قفسه فقط برای کپی کردن مجوزها در کتاب‌های کودک با استفاده از عملکرد زیر استفاده می‌شوند. آنها توانایی ایجاد کتاب را کنترل نمی‌کنند.',\n    'shelves_copy_permissions_to_books' => 'کپی مجوزها در کتاب‌ها',\n    'shelves_copy_permissions' => 'کپی مجوزها',\n    'shelves_copy_permissions_explain' => 'با این کار تنظیمات مجوز فعلی این قفسه برای همه کتاب‌های موجود در آن اعمال می‌شود. قبل از فعال کردن، مطمئن شوید که هر گونه تغییر در مجوزهای این قفسه، ذخیره شده است.',\n    'shelves_copy_permission_success' => 'مجوزهای قفسه در :count کتاب کپی شد',\n\n    // Books\n    'book' => 'کتاب',\n    'books' => 'کتاب‌ها',\n    'x_books' => ':count کتاب|:count کتاب',\n    'books_empty' => 'هیچ کتابی ایجاد نشده است',\n    'books_popular' => 'کتاب های محبوب',\n    'books_recent' => 'کتاب های اخیر',\n    'books_new' => 'کتاب های جدید',\n    'books_new_action' => 'کتاب جدید',\n    'books_popular_empty' => 'محبوب ترین کتاب ها در اینجا ظاهر می شوند.',\n    'books_new_empty' => 'جدیدترین کتاب‌های ایجاد شده در اینجا ظاهر می‌شوند.',\n    'books_create' => 'ایجاد کتاب جدید',\n    'books_delete' => 'حذف کتاب',\n    'books_delete_named' => 'حذف کتاب:bookName',\n    'books_delete_explain' => 'با این کار کتابی با نام \\':bookName\\' حذف می شود. تمام صفحات و فصل ها حذف خواهند شد.',\n    'books_delete_confirmation' => 'آیا مطمئن هستید که می خواهید این کتاب را حذف کنید?',\n    'books_edit' => 'ویرایش کتاب',\n    'books_edit_named' => 'ویرایش کتاب:bookName',\n    'books_form_book_name' => 'نام کتاب',\n    'books_save' => 'ذخیره کتاب',\n    'books_permissions' => 'مجوزهای کتاب',\n    'books_permissions_updated' => 'مجوزهای کتاب به روز شد',\n    'books_empty_contents' => 'هیچ صفحه یا فصلی برای این کتاب ایجاد نشده است.',\n    'books_empty_create_page' => 'یک صفحه جدید ایجاد کنید',\n    'books_empty_sort_current_book' => 'کتاب فعلی را مرتب کنید',\n    'books_empty_add_chapter' => 'یک فصل اضافه کنید',\n    'books_permissions_active' => 'مجوزهای کتاب فعال است',\n    'books_search_this' => 'این کتاب را جستجو کنید',\n    'books_navigation' => 'ناوبری کتاب',\n    'books_sort' => 'مرتب‌سازی مطالب کتاب',\n    'books_sort_desc' => 'برای سامان‌دهی محتوای یک کتاب، می‌توانید فصل‌ها و صفحات آن را جابه‌جا کنید. همچنین می‌توانید کتاب‌های دیگری بیفزایید تا جابه‌جایی فصل‌ها و صفحات میان کتاب‌ها آسان شود. در صورت تمایل، می‌توانید قاعده‌ای برای مرتب‌سازی خودکار تعیین کنید تا محتوای کتاب در صورت ایجاد تغییرات، به طور خودکار مرتب شود.',\n    'books_sort_auto_sort' => 'گزینه مرتب‌سازی خودکار',\n    'books_sort_auto_sort_active' => 'مرتب‌سازی خودکار با قاعده: :sortName فعال است',\n    'books_sort_named' => 'مرتب‌سازی کتاب:bookName',\n    'books_sort_name' => 'مرتب‌سازی بر اساس نام',\n    'books_sort_created' => 'مرتب‌سازی بر اساس تاریخ ایجاد',\n    'books_sort_updated' => 'مرتب‌سازی بر اساس تاریخ به روز رسانی',\n    'books_sort_chapters_first' => 'فصل اول',\n    'books_sort_chapters_last' => 'فصل آخر',\n    'books_sort_show_other' => 'نمایش کتاب‌های دیگر',\n    'books_sort_save' => 'ذخیره سفارش جدید',\n    'books_sort_show_other_desc' => 'کتاب‌های دیگری را در اینجا اضافه کنید تا آنها را در عملیات مرتب‌سازی بگنجانید و به آسانی کتاب‌ها را مجددا سازماندهی کنید.',\n    'books_sort_move_up' => 'انتقال به بالا',\n    'books_sort_move_down' => 'انتقال به پایین',\n    'books_sort_move_prev_book' => 'انتقال به کتاب قبلی',\n    'books_sort_move_next_book' => 'انتقال به کتاب بعدی',\n    'books_sort_move_prev_chapter' => 'انتقال به داخل فصل قبلی',\n    'books_sort_move_next_chapter' => 'انتقال به داخل فصل بعدی',\n    'books_sort_move_book_start' => 'انتقال به ابتدای کتاب',\n    'books_sort_move_book_end' => 'انتقال به انتهای کتاب',\n    'books_sort_move_before_chapter' => 'انتقال به قبل فصل',\n    'books_sort_move_after_chapter' => 'انتقال به بعد فصل',\n    'books_copy' => 'کپی کتاب',\n    'books_copy_success' => 'کتاب با موفقیت کپی شد',\n\n    // Chapters\n    'chapter' => 'فصل',\n    'chapters' => 'فصل',\n    'x_chapters' => ':count فصل|:count فصل',\n    'chapters_popular' => 'فصل‌های محبوب',\n    'chapters_new' => 'فصل جدید',\n    'chapters_create' => 'ایجاد فصل جدید',\n    'chapters_delete' => 'حذف فصل',\n    'chapters_delete_named' => 'حذف فصل :chapterName',\n    'chapters_delete_explain' => 'با این کار فصلی با نام \\':chapterName\\' حذف می شود. تمامی صفحاتی که در این فصل وجود دارند نیز حذف خواهند شد.',\n    'chapters_delete_confirm' => 'آیا مطمئن هستید که می خواهید این فصل را حذف کنید؟',\n    'chapters_edit' => 'ویرایش فصل',\n    'chapters_edit_named' => 'ویرایش فصل :chapterName',\n    'chapters_save' => 'ذخیره فصل',\n    'chapters_move' => 'انتقال فصل',\n    'chapters_move_named' => 'انتقال فصل :chapterName',\n    'chapters_copy' => 'کپی فصل',\n    'chapters_copy_success' => 'فصل با موفقیت کپی شد',\n    'chapters_permissions' => 'مجوزهای فصل',\n    'chapters_empty' => 'در حال حاضر هیچ صفحه ای در این فصل وجود ندارد.',\n    'chapters_permissions_active' => 'مجوزهای فصل فعال است',\n    'chapters_permissions_success' => 'مجوزهای فصل به روز شد',\n    'chapters_search_this' => 'این فصل را جستجو کنید',\n    'chapter_sort_book' => 'مرتب‌سازی کتاب',\n\n    // Pages\n    'page' => 'صفحه',\n    'pages' => 'صفحات',\n    'x_pages' => ':count صفحه|:count صفحه',\n    'pages_popular' => 'صفحات محبوب',\n    'pages_new' => 'صفحه جدید',\n    'pages_attachments' => 'پیوست‌ها',\n    'pages_navigation' => 'پیمایش صفحه',\n    'pages_delete' => 'حذف صفحه',\n    'pages_delete_named' => 'حذف صفحه:pageName',\n    'pages_delete_draft_named' => 'حذف پیش نویس صفحه:pageName',\n    'pages_delete_draft' => 'حذف صفحه پیش نویس',\n    'pages_delete_success' => 'صفحه حذف شد',\n    'pages_delete_draft_success' => 'صفحه پیش نویس حذف شد',\n    'pages_delete_warning_template' => 'این صفحه هم‌اکنون به‌عنوان قالب پیش‌فرض صفحه برای یک کتاب یا فصل در حال استفاده است. پس از حذف این صفحه، کتاب‌ها یا فصل‌های مربوط دیگر قالب پیش‌فرض صفحه نخواهند داشت.',\n    'pages_delete_confirm' => 'آیا مطمئن هستید که می خواهید این صفحه را حذف کنید؟',\n    'pages_delete_draft_confirm' => 'آیا مطمئن هستید که می خواهید این صفحه پیش نویس را حذف کنید؟',\n    'pages_editing_named' => 'ویرایش صفحه :pageName',\n    'pages_edit_draft_options' => 'تنظیمات پیش‌نویس',\n    'pages_edit_save_draft' => 'ذخیره پیش نویس',\n    'pages_edit_draft' => 'ویرایش پیش نویس صفحه',\n    'pages_editing_draft' => 'در حال ویرایش پیش نویس',\n    'pages_editing_page' => 'در حال ویرایش صفحه',\n    'pages_edit_draft_save_at' => 'پیش نویس ذخیره شده در ',\n    'pages_edit_delete_draft' => 'حذف پیش نویس',\n    'pages_edit_delete_draft_confirm' => 'آیا از حذف تغییرات صفحه پیش‌نویس اطمینان دارید؟ تمامی تغییرات‌تان، از آخرین ذخیره‌سازی کامل، از بین خواهد رفت و ویرایش‌گر به آخرین وضعیت پیش‌نویس ذخیره شده بازگردانی خواهد شد.',\n    'pages_edit_discard_draft' => 'دور انداختن پیش نویس',\n    'pages_edit_switch_to_markdown' => 'به ویرایشگر Markdown بروید',\n    'pages_edit_switch_to_markdown_clean' => '(مطالب تمیز)',\n    'pages_edit_switch_to_markdown_stable' => '(محتوای پایدار)',\n    'pages_edit_switch_to_wysiwyg' => 'به ویرایشگر WYSIWYG بروید',\n    'pages_edit_switch_to_new_wysiwyg' => 'تغییر به ویرایشگر جدید WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(در مرحله آزمایش بتا)',\n    'pages_edit_set_changelog' => 'تنظیم تغییرات',\n    'pages_edit_enter_changelog_desc' => 'توضیح مختصری از تغییراتی که ایجاد کرده اید وارد کنید',\n    'pages_edit_enter_changelog' => 'وارد کردن تغییرات',\n    'pages_editor_switch_title' => 'ویرایشگر را تغییر دهید',\n    'pages_editor_switch_are_you_sure' => 'آیا مطمئن هستید که می خواهید ویرایشگر این صفحه را تغییر دهید؟',\n    'pages_editor_switch_consider_following' => 'هنگام تغییر ویرایشگر موارد زیر را در نظر بگیرید:',\n    'pages_editor_switch_consideration_a' => 'پس از ذخیره، گزینه ویرایشگر جدید توسط هر ویرایشگر آینده، از جمله ویرایشگرانی که ممکن است خودشان نتوانند نوع ویرایشگر را تغییر دهند، استفاده خواهد شد.',\n    'pages_editor_switch_consideration_b' => 'این به طور بالقوه می تواند منجر به از دست دادن جزئیات و نحو در شرایط خاص شود.',\n    'pages_editor_switch_consideration_c' => 'تغییرات برچسب‌ها یا تغییرات ثبت شده از آخرین ذخیره‌سازی انجام شده، در این تغییر باقی نمی‌مانند.',\n    'pages_save' => 'ذخیره صفحه',\n    'pages_title' => 'عنوان صفحه',\n    'pages_name' => 'نام صفحه',\n    'pages_md_editor' => 'ویرایشگر',\n    'pages_md_preview' => 'پيش نمايش',\n    'pages_md_insert_image' => 'درج تصویر',\n    'pages_md_insert_link' => 'پیوند نهاد را درج کنید',\n    'pages_md_insert_drawing' => 'درج طرح',\n    'pages_md_show_preview' => 'دیدن پیش نمایش',\n    'pages_md_sync_scroll' => 'هماهنگ سازی اسکرول پیش نمایش',\n    'pages_md_plain_editor' => 'ویرایشگر متن ساده',\n    'pages_drawing_unsaved' => 'نقاشی ذخیره نشده پیدا شد',\n    'pages_drawing_unsaved_confirm' => 'نسخه‌ای ذخیره‌نشده از طراحی‌های قبلی پیدا شد. آیا می‌خواهید این طراحی ذخیره‌نشده را بازیابی کنید و به ویرایش آن ادامه دهید؟',\n    'pages_not_in_chapter' => 'صفحه در یک فصل نیست',\n    'pages_move' => 'انتقال صفحه',\n    'pages_copy' => 'کپی صفحه',\n    'pages_copy_desination' => 'مقصد را کپی کنید',\n    'pages_copy_success' => 'صفحه با موفقیت کپی شد',\n    'pages_permissions' => 'مجوزهای صفحه',\n    'pages_permissions_success' => 'مجوزهای صفحه به روز شد',\n    'pages_revision' => 'تجدید نظر',\n    'pages_revisions' => 'ویرایش های صفحه',\n    'pages_revisions_desc' => 'لیست زیر تمامی ویرایش‌های قبلی این صفحه است. در صورت وجود مجوز دسترسی، می‌توانید نسخه‌های قدیمی صفحه را مشاهده، مقایسه و بازیابی کنید. تاریخچه کامل صفحه ممکن است به طور کامل در اینجا منعکس نشود زیرا بسته به پیکربندی سیستم، ویرایش های قدیمی می توانند به طور خودکار حذف شوند.',\n    'pages_revisions_named' => 'بازبینی صفحه برای :pageName',\n    'pages_revision_named' => 'ویرایش صفحه برای :pageName',\n    'pages_revision_restored_from' => 'بازیابی شده از #:id; :summary',\n    'pages_revisions_created_by' => 'ایجاد شده توسط',\n    'pages_revisions_date' => 'تاریخ تجدید نظر',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'شماره ویرایش',\n    'pages_revisions_numbered' => 'تجدید نظر #:id',\n    'pages_revisions_numbered_changes' => 'بازبینی #:id تغییرات',\n    'pages_revisions_editor' => 'نوع ویرایشگر',\n    'pages_revisions_changelog' => 'لیست تغییرات',\n    'pages_revisions_changes' => 'تغییرات',\n    'pages_revisions_current' => 'نسخه‌ی جاری',\n    'pages_revisions_preview' => 'پيش نمايش',\n    'pages_revisions_restore' => 'بازگرداندن',\n    'pages_revisions_none' => 'این صفحه هیچ ویرایشی ندارد',\n    'pages_copy_link' => 'کپی لینک',\n    'pages_edit_content_link' => 'پرش به قسمت در ویرایش‌گر',\n    'pages_pointer_enter_mode' => 'ورود به حالت انتخاب قسمت',\n    'pages_pointer_label' => 'گزینه‌های قسمت صفحه',\n    'pages_pointer_permalink' => 'لینک ثابت قسمت صفحه',\n    'pages_pointer_include_tag' => 'افزودن برچسب برای این بخش از صفحه',\n    'pages_pointer_toggle_link' => 'حالت پیوند دائمی؛ برای مشاهده برچسب افزوده شده، کلیک کنید',\n    'pages_pointer_toggle_include' => 'حالت افزودن برچسب؛ برای نمایش پیوند دائمی کلیک کنید',\n    'pages_permissions_active' => 'مجوزهای صفحه فعال است',\n    'pages_initial_revision' => 'انتشار اولیه',\n    'pages_references_update_revision' => 'به‌روز‌رسانی خودکار لینک‌های داخلی سیستم',\n    'pages_initial_name' => 'برگهٔ تازه',\n    'pages_editing_draft_notification' => 'شما در حال ویرایش پیش نویسی هستید که آخرین بار در :timeDiff ذخیره شده است.',\n    'pages_draft_edited_notification' => 'این صفحه از همان زمان به روز شده است. توصیه می شود از این پیش نویس صرف نظر کنید.',\n    'pages_draft_page_changed_since_creation' => 'این صفحه از زمان ایجاد این پیش نویس به روز شده است. توصیه می‌شود که این پیش‌نویس را کنار بگذارید یا مراقب باشید که تغییرات صفحه را بازنویسی نکنید.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count کاربران شروع به ویرایش این صفحه کرده اند',\n        'start_b' => ':userName ویرایش این صفحه را شروع کرده است',\n        'time_a' => 'از آخرین به روز رسانی صفحه',\n        'time_b' => 'در آخرین دقیقه :minCount',\n        'message' => ':start :time. مراقب باشید به روز رسانی های یکدیگر را بازنویسی نکنید!',\n    ],\n    'pages_draft_discarded' => 'پیش نویس حذف شد، ویرایشگر با محتوای صفحه فعلی به روز شده است',\n    'pages_draft_deleted' => 'پیش نویس حذف شد، ویرایشگر با محتوای صفحه فعلی به روز شده است',\n    'pages_specific' => 'صفحه خاص',\n    'pages_is_template' => 'الگوی صفحه',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'نمایش/پنهان‌سازی نوار کناری',\n    'page_tags' => 'برچسب‌های صفحه',\n    'chapter_tags' => 'برچسب‌های فصل',\n    'book_tags' => 'برچسب های کتاب',\n    'shelf_tags' => 'برچسب‌های قفسه',\n    'tag' => 'برچسب',\n    'tags' =>  'برچسب‌ها',\n    'tags_index_desc' => 'تگ ها را میتوان به محتوای داخل سیستم اعمال کرد تا فرم هماهنگی از طبقه‌بندی ایجاد شود. تگ ها می توانند شامل یک کلید و یک مقدار باشند، که مقدار آن انتخابی یا قابل خذف است. بعد از ایجاد تگ، محتوا را می توان توسط کلید یا مقدار هر تگ جستجو نمود.',\n    'tag_name' =>  'نام برچسب',\n    'tag_value' => 'مقدار برچسب (اختیاری)',\n    'tags_explain' => \"برای دسته بندی بهتر مطالب خود چند برچسب اضافه کنید.\\nمی توانید برای سازماندهی عمیق‌تر، یک مقدار به یک برچسب اختصاص دهید.\",\n    'tags_add' => 'یک برچسب دیگر اضافه کنید',\n    'tags_remove' => 'این برچسب را حذف کنید',\n    'tags_usages' => 'مجموع استفاده از برچسب',\n    'tags_assigned_pages' => 'به صفحات اختصاص داده شده است',\n    'tags_assigned_chapters' => 'اختصاص به فصل',\n    'tags_assigned_books' => 'به کتاب ها اختصاص داده شده است',\n    'tags_assigned_shelves' => 'به قفسه ها اختصاص داده شده است',\n    'tags_x_unique_values' => ':count مقادیر منحصر به فرد',\n    'tags_all_values' => 'همه ارزش ها',\n    'tags_view_tags' => 'مشاهده برچسب ها',\n    'tags_view_existing_tags' => 'مشاهده برچسب‌های موجود',\n    'tags_list_empty_hint' => 'برچسب ها را می توان از طریق نوار کناری ویرایشگر صفحه یا هنگام ویرایش جزئیات یک کتاب، فصل یا قفسه اختصاص داد.',\n    'attachments' => 'پیوست ها',\n    'attachments_explain' => 'چند فایل را آپلود کنید یا چند پیوند را برای نمایش در صفحه خود ضمیمه کنید. اینها در نوار کناری صفحه قابل مشاهده هستند.',\n    'attachments_explain_instant_save' => 'تغییرات در اینجا فورا ذخیره می شوند.',\n    'attachments_upload' => 'آپلود فایل',\n    'attachments_link' => 'پیوند را ضمیمه کنید',\n    'attachments_upload_drop' => 'می‌توانید فایلی را در اینجا بکشید و رها کنید تا آن را به عنوان پیوست آپلود کنید.',\n    'attachments_set_link' => 'پیوند را تنظیم کنید',\n    'attachments_delete' => 'آیا مطمئن هستید که می خواهید این پیوست را حذف کنید؟',\n    'attachments_dropzone' => 'فایل را برای بارگذاری به اینجا بکشید و رها کنید',\n    'attachments_no_files' => 'هیچ فایلی آپلود نشده است',\n    'attachments_explain_link' => 'اگر ترجیح می دهید فایلی را آپلود نکنید، می توانید پیوندی را پیوست کنید. این می تواند پیوندی به صفحه دیگر یا پیوندی به فایلی در فضای ابری باشد.',\n    'attachments_link_name' => 'نام پیوند',\n    'attachment_link' => 'لینک پیوست',\n    'attachments_link_url' => 'پیوند به فایل',\n    'attachments_link_url_hint' => 'آدرس سایت یا فایل',\n    'attach' => 'ضمیمه کنید',\n    'attachments_insert_link' => 'پیوند پیوست را به صفحه اضافه کنید',\n    'attachments_edit_file' => 'ویرایش فایل',\n    'attachments_edit_file_name' => 'نام فایل',\n    'attachments_edit_drop_upload' => 'فایل ها را رها کنید یا برای آپلود و بازنویسی اینجا کلیک کنید',\n    'attachments_order_updated' => 'سفارش پیوست به روز شد',\n    'attachments_updated_success' => 'جزئیات پیوست به روز شد',\n    'attachments_deleted' => 'پیوست حذف شد',\n    'attachments_file_uploaded' => 'فایل با موفقیت آپلود شد',\n    'attachments_file_updated' => 'فایل با موفقیت به روز شد',\n    'attachments_link_attached' => 'پیوند با موفقیت به صفحه پیوست شد',\n    'templates' => 'قالب ها',\n    'templates_set_as_template' => 'صفحه یک الگو است',\n    'templates_explain_set_as_template' => 'می توانید این صفحه را به عنوان یک الگو تنظیم کنید تا از محتویات آن هنگام ایجاد صفحات دیگر استفاده شود. سایر کاربران در صورت داشتن مجوز مشاهده برای این صفحه می توانند از این الگو استفاده کنند.',\n    'templates_replace_content' => 'محتوای صفحه را جایگزین کنید',\n    'templates_append_content' => 'به محتوای صفحه اضافه کنید',\n    'templates_prepend_content' => 'به محتوای صفحه اضافه کنید',\n\n    // Profile View\n    'profile_user_for_x' => 'کاربر برای :time',\n    'profile_created_content' => 'محتوا ایجاد کرد',\n    'profile_not_created_pages' => ':userName هیچ صفحه ای ایجاد نکرده است',\n    'profile_not_created_chapters' => ':userName هیچ فصلی ایجاد نکرده است',\n    'profile_not_created_books' => ':userName هیچ کتابی ایجاد نکرده است',\n    'profile_not_created_shelves' => ':userName هیچ قفسه ای ایجاد نکرده است',\n\n    // Comments\n    'comment' => 'اظهار نظر',\n    'comments' => 'نظرات',\n    'comment_add' => 'افزودن توضیح',\n    'comment_none' => 'نظری برای نمایش وجود ندارد',\n    'comment_placeholder' => 'اینجا نظر بدهید',\n    'comment_thread_count' => ':count رشته گفت‌وگو',\n    'comment_archived_count' => ':count مورد بایگانی‌شده',\n    'comment_archived_threads' => 'رشته‌ گفت‌وگوهای بایگانی‌شده',\n    'comment_save' => 'ذخیره نظر',\n    'comment_new' => 'نظر جدید',\n    'comment_created' => ':createDiff نظر داد',\n    'comment_updated' => 'به روز رسانی :updateDiff توسط :username',\n    'comment_updated_indicator' => 'به روز شده',\n    'comment_deleted_success' => 'نظر حذف شد',\n    'comment_created_success' => 'نظر اضافه شد',\n    'comment_updated_success' => 'نظر به روز شد',\n    'comment_archive_success' => 'نظر بایگانی شد',\n    'comment_unarchive_success' => 'نظر از بایگانی خارج شد',\n    'comment_view' => 'دیدن نظر',\n    'comment_jump_to_thread' => 'رفتن به رشته گفت‌وگو',\n    'comment_delete_confirm' => 'آیا مطمئن هستید که می خواهید این نظر را حذف کنید؟',\n    'comment_in_reply_to' => 'در پاسخ به :commentId',\n    'comment_reference' => 'مرجع',\n    'comment_reference_outdated' => '(نسخه قدیمی)',\n    'comment_editor_explain' => 'در اینجا نظراتی که در این صفحه گذاشته شده است، مشاهده می‌شود. نظرات را می‌توان در هنگام مشاهده صفحه ذخیره شده، اضافه و مدیریت کرد.',\n\n    // Revision\n    'revision_delete_confirm' => 'آیا مطمئن هستید که می خواهید این ویرایش را حذف کنید؟',\n    'revision_restore_confirm' => 'آیا مطمئن هستید که می خواهید این ویرایش را بازیابی کنید؟ محتوای صفحه فعلی جایگزین خواهد شد.',\n    'revision_cannot_delete_latest' => 'نمی توان آخرین نسخه را حذف کرد.',\n\n    // Copy view\n    'copy_consider' => 'لطفاً هنگام کپی کردن مطالب به موارد زیر توجه کنید.',\n    'copy_consider_permissions' => 'تنظیمات مجوز سفارشی کپی نخواهد شد.',\n    'copy_consider_owner' => 'شما مالک تمام محتوای کپی شده خواهید شد.',\n    'copy_consider_images' => 'فایل های تصویر صفحه تکراری نخواهند شد و تصاویر اصلی ارتباط خود را با صفحه ای که در ابتدا در آن آپلود شده اند حفظ می کنند.',\n    'copy_consider_attachments' => 'پیوست های صفحه کپی نمی شود.',\n    'copy_consider_access' => 'تغییر مکان، مالک یا مجوزها ممکن است منجر به دسترسی به این محتوا برای افرادی شود که قبلاً به آنها دسترسی نداشتند.',\n\n    // Conversions\n    'convert_to_shelf' => 'تبدیل به قفسه',\n    'convert_to_shelf_contents_desc' => 'شما می توانید این کتاب را به یک قفسه جدید با همان مطالب تبدیل کنید. فصل های موجود در این کتاب به کتاب های جدید تبدیل می شوند. اگر این کتاب حاوی صفحاتی باشد که در یک فصل نیستند، این کتاب تغییر نام داده و حاوی چنین صفحاتی است و این کتاب بخشی از قفسه جدید خواهد شد.',\n    'convert_to_shelf_permissions_desc' => 'هر گونه مجوز تنظیم شده در این کتاب در قفسه جدید و همه کتاب‌های فرزند جدید که مجوزهای خود را ندارند کپی می‌شود. توجه داشته باشید که مجوزهای موجود در قفسه‌ها مانند کتاب‌ها به طور خودکار به محتوای درون آن ها شامل نمی شود.',\n    'convert_book' => 'تبدیل کتاب',\n    'convert_book_confirm' => 'آیا از تبدیل این کتاب مطمئن هستید؟',\n    'convert_undo_warning' => 'برگشت دادن این فرایند به آسانی نخواهد بود.',\n    'convert_to_book' => 'تبدیل به کتاب',\n    'convert_to_book_desc' => 'می توانید این فصل را به یک کتاب جدید با همین مطالب تبدیل کنید. هر مجوزی که در این فصل تنظیم شده است در کتاب جدید کپی می شود، اما هر گونه مجوز ارثی، از کتاب والد، کپی نمی شود که می تواند منجر به تغییر کنترل دسترسی شود.',\n    'convert_chapter' => 'تبدیل فصل',\n    'convert_chapter_confirm' => 'آیا از تبدیل این فصل مطمئن هستید؟',\n\n    // References\n    'references' => 'مراجع',\n    'references_none' => 'هیچ رفرنسی برای این قلم یافت نشد.',\n    'references_to_desc' => 'در زیر تمام صفحات شناخته شده در سیستم که به این مورد پیوند دارند، نشان داده شده است.',\n\n    // Watch Options\n    'watch' => 'نظارت',\n    'watch_title_default' => 'تنظیمات پیش‌فرض',\n    'watch_desc_default' => 'نظارت را تنها به تنظیمات پیش فرض اعلان برگردانید.',\n    'watch_title_ignore' => 'نادیده‌گرفتن',\n    'watch_desc_ignore' => 'همه اعلان‌ها، از جمله اعلان‌های مربوط به تنظیمات سطح کاربر را نادیده بگیرید.',\n    'watch_title_new' => 'صفحات جدید',\n    'watch_desc_new' => 'هنگامی که صفحه جدیدی در این مورد ایجاد می‌شود، اطلاع بده.',\n    'watch_title_updates' => 'همه به روزرسانی‌های صفحه',\n    'watch_desc_updates' => 'تمام صفحات جدید و تغییرات صفحه را اطلاع بده.',\n    'watch_desc_updates_page' => 'همه تغییرات صفحه را اطلاع بده.',\n    'watch_title_comments' => 'همه به‌روزرسانی‌ها و نظرات',\n    'watch_desc_comments' => 'در مورد تمام صفحات جدید، تغییرات در صفحات و ثبت نظرات جدید، اطلاع بده.',\n    'watch_desc_comments_page' => 'در مورد تغییرات در صفحه و ثبت نظرات جدید، اطلاع بده.',\n    'watch_change_default' => 'تنظیمات پیش‌فرض اعلان‌ها را تغییر دهید',\n    'watch_detail_ignore' => 'نادیده گرفتن اعلان‌ها',\n    'watch_detail_new' => 'نظارت بر صفحات جدید',\n    'watch_detail_updates' => 'نظارت بر صفحات جدید و به‌روزرسانی‌ها',\n    'watch_detail_comments' => 'نظارت بر صفحات جدید، به‌روزرسانی‌ها و نظرات',\n    'watch_detail_parent_book' => 'نظارت از طریق کتاب والد',\n    'watch_detail_parent_book_ignore' => 'نادیده گرفتن (نظارت) از طریق کتاب والد',\n    'watch_detail_parent_chapter' => 'نظارت از طریق فصل والد',\n    'watch_detail_parent_chapter_ignore' => 'نادیده گرفتن (نظارت) از طریق فصل والد',\n];\n"
  },
  {
    "path": "lang/fa/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'شما مجوز مشاهده صفحه درخواست شده را ندارید.',\n    'permissionJson' => 'شما مجاز به انجام این عمل نیستید.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'کاربری با ایمیل :email از قبل وجود دارد اما دارای اطلاعات متفاوتی می باشد.',\n    'auth_pre_register_theme_prevention' => 'بر اساس اطلاعات ارائه شده، حساب کاربری نمی‌نواند ایجاد بشود',\n    'email_already_confirmed' => 'ایمیل قبلا تایید شده است، وارد سیستم شوید.',\n    'email_confirmation_invalid' => 'این کلمه عبور معتبر نمی باشد و یا قبلا استفاده شده است، لطفا دوباره ثبت نام نمایید.',\n    'email_confirmation_expired' => 'کلمه عبور منقضی شده است، یک ایمیل تایید جدید ارسال شد.',\n    'email_confirmation_awaiting' => 'آدرس ایمیل حساب مورد استفاده باید تایید شود',\n    'ldap_fail_anonymous' => 'دسترسی LDAP با استفاده از صحافی ناشناس انجام نشد',\n    'ldap_fail_authed' => 'دسترسی به LDAP با استفاده از جزئیات داده شده و رمز عبور انجام نشد',\n    'ldap_extension_not_installed' => 'افزونه PHP LDAP نصب نشده است',\n    'ldap_cannot_connect' => 'اتصال به سرور LDAP امکان‌پذیر نیست، اتصال اولیه برقرار نشد',\n    'saml_already_logged_in' => 'قبلا وارد سیستم شده اید',\n    'saml_no_email_address' => 'آدرس داده ای برای این کاربر در داده های ارائه شده توسط سیستم احراز هویت خارجی یافت نشد',\n    'saml_invalid_response_id' => 'درخواست ارسال‌شده از سامانه احراز هویت خارجی، توسط فرآیند آغازشده از سوی این نرم‌افزار شناسایی نشد. ممکن است بازگشت به صفحه قبل پس از ورود، موجب ایجاد این مشکل شده باشد.',\n    'saml_fail_authed' => 'ورود به سیستم :system انجام نشد، سیستم مجوز موفقیت آمیز ارائه نکرد',\n    'oidc_already_logged_in' => 'قبلا وارد شده اید',\n    'oidc_no_email_address' => 'آدرس ایمیلی برای این کاربر در داده های ارائه شده توسط سیستم احراز هویت خارجی یافت نشد',\n    'oidc_fail_authed' => 'ورود به سیستم با استفاده از :system انجام نشد، سیستم مجوز موفقیت آمیز ارائه نکرد',\n    'social_no_action_defined' => 'عملی تعریف نشده است',\n    'social_login_bad_response' => \"خطای دریافت شده در هنگام ورود به سیستم:\\n:error\",\n    'social_account_in_use' => 'این حساب :socialAccount از قبل در حال استفاده است، سعی کنید از طریق گزینه :socialAccount وارد سیستم شوید.',\n    'social_account_email_in_use' => 'ایمیل :email از قبل در حال استفاده است. اگر از قبل حساب کاربری دارید می توانید از تنظیمات نمایه خود :socialAccount خود را وصل کنید.',\n    'social_account_existing' => 'این :socialAccount از قبل به نمایه شما پیوست شده است.',\n    'social_account_already_used_existing' => 'این حساب :socialAccount قبلا توسط کاربر دیگری استفاده شده است.',\n    'social_account_not_used' => 'این حساب :socialAccount به هیچ کاربری پیوند ندارد. لطفا آن را در تنظیمات نمایه خود ضمیمه کنید. ',\n    'social_account_register_instructions' => 'اگر هنوز حساب کاربری ندارید ، می توانید با استفاده از گزینه :socialAccount حساب خود را ثبت کنید.',\n    'social_driver_not_found' => 'درایور شبکه اجتماعی یافت نشد',\n    'social_driver_not_configured' => 'تنظیمات شبکه اجتماعی :socialAccount به درستی پیکربندی نشده است.',\n    'invite_token_expired' => 'این پیوند دعوت منقضی شده است. در عوض می توانید سعی کنید رمز عبور حساب خود را بازنشانی کنید.',\n    'login_user_not_found' => 'کاربری برای این کار پیدا نمی‌شود.',\n\n    // System\n    'path_not_writable' => 'مسیر فایل :filePath را نمی توان در آن آپلود کرد. مطمئن شوید که روی سرور قابل نوشتن است.',\n    'cannot_get_image_from_url' => 'نمی توان تصویر را از :url دریافت کرد',\n    'cannot_create_thumbs' => 'سرور نمی تواند تصاویر کوچک ایجاد کند. لطفاً بررسی کنید که پسوند GD PHP را نصب کرده اید.',\n    'server_upload_limit' => 'سرور اجازه آپلود با این حجم را نمی دهد. لطفا فایلی با حجم کم‌تر را امتحان کنید.',\n    'server_post_limit' => 'سرور نمی‌تواند داده مقادیر ارائه شده داده را دریافت کند. با مقدار کمتر و فایل کوچکتر دوباره امتحان کنید.',\n    'uploaded'  => 'سرور اجازه آپلود در این اندازه را نمی دهد. لطفا اندازه فایل کوچکتر را امتحان کنید.',\n\n    // Drawing & Images\n    'image_upload_error' => 'هنگام آپلود تصویر خطایی روی داد',\n    'image_upload_type_error' => 'نوع تصویر در حال آپلود نامعتبر است',\n    'image_upload_replace_type' => 'جایگزینی فایل تصویری باید از یک نوع باشد',\n    'image_upload_memory_limit' => 'به دلیل محدودیت منابع سامانه، بارگذاری فایل و/یا ایجاد تصاویر کاور (thumbnails) ناموفق بود.',\n    'image_thumbnail_memory_limit' => 'به دلیل محدودیت منابع سیستم، تصاویر با اندازه گوناگون ایجاد نشدند.',\n    'image_gallery_thumbnail_memory_limit' => 'به دلیل محدودیت منابع سیستم، تصاویر کوچک گالری ایجاد نشد.',\n    'drawing_data_not_found' => 'داده های طرح قابل بارگذاری نیستند. ممکن است فایل طرح دیگر وجود نداشته باشد یا شما به آن دسترسی نداشته باشید.',\n\n    // Attachments\n    'attachment_not_found' => 'پیوست یافت نشد',\n    'attachment_upload_error' => 'هنگام آپلود فایل خطایی روی داد',\n\n    // Pages\n    'page_draft_autosave_fail' => 'پیش نویس ذخیره نشد. قبل از ذخیره این صفحه مطمئن شوید که به اینترنت متصل هستید',\n    'page_draft_delete_fail' => 'حذف پیش‌نویس و همچنین بازآوری محتوای صفحه فعلی، ناموفق بود',\n    'page_custom_home_deletion' => 'وقتی صفحه ای به عنوان صفحه اصلی تنظیم شده است، نمی توان آن را حذف کرد',\n\n    // Entities\n    'entity_not_found' => 'موجودیت یافت نشد',\n    'bookshelf_not_found' => 'قفسه پیدا نشد',\n    'book_not_found' => 'کتاب پیدا نشد',\n    'page_not_found' => 'صفحه یافت نشد',\n    'chapter_not_found' => 'فصل پیدا نشد',\n    'selected_book_not_found' => 'کتاب انتخابی یافت نشد',\n    'selected_book_chapter_not_found' => 'کتاب یا فصل انتخابی یافت نشد',\n    'guests_cannot_save_drafts' => 'مهمانان نمی توانند پیش نویس ها را ذخیره کنند',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'شما نمی توانید تنها ادمین را حذف کنید',\n    'users_cannot_delete_guest' => 'شما نمی توانید کاربر مهمان را حذف کنید',\n    'users_could_not_send_invite' => 'امکان ایجاد کاربر وجود ندارد؛ زیرا ارسال ایمیل دعوت با خطا مواجه شد.',\n\n    // Roles\n    'role_cannot_be_edited' => 'این نقش قابل ویرایش نیست',\n    'role_system_cannot_be_deleted' => 'این نقش یک نقش سیستمی است و قابل حذف نیست',\n    'role_registration_default_cannot_delete' => 'این نقش در حالی که به عنوان نقش پیش فرض ثبت نام تنظیم شده است قابل حذف نیست',\n    'role_cannot_remove_only_admin' => 'این کاربر تنها کاربری است که به نقش مدیر اختصاص داده شده است. قبل از تلاش برای حذف آن در اینجا، نقش مدیر را به کاربر دیگری اختصاص دهید.',\n\n    // Comments\n    'comment_list' => 'هنگام واکشی نظرات خطایی روی داد.',\n    'cannot_add_comment_to_draft' => 'شما نمی توانید نظراتی را به یک پیش نویس اضافه کنید.',\n    'comment_add' => 'هنگام افزودن/به‌روزرسانی نظر خطایی روی داد.',\n    'comment_delete' => 'هنگام حذف نظر خطایی روی داد.',\n    'empty_comment' => 'نمی توان یک نظر خالی اضافه کرد.',\n\n    // Error pages\n    '404_page_not_found' => 'صفحه یافت نشد',\n    'sorry_page_not_found' => 'با عرض پوزش، صفحه مورد نظر شما یافت نشد.',\n    'sorry_page_not_found_permission_warning' => 'اگر انتظار داشتید این صفحه وجود داشته باشد، ممکن است اجازه مشاهده آن را نداشته باشید.',\n    'image_not_found' => 'تصویر پیدا نشد',\n    'image_not_found_subtitle' => 'با عرض پوزش، فایل تصویری که به دنبال آن بودید یافت نشد.',\n    'image_not_found_details' => 'اگر انتظار داشتید این تصویر وجود داشته باشد، ممکن است حذف شده باشد.',\n    'return_home' => 'بازگشت به خانه',\n    'error_occurred' => 'خطایی رخ داد',\n    'app_down' => ':appName در حال حاضر قطع است',\n    'back_soon' => 'به زودی پشتیبان خواهد شد.',\n\n    // Import\n    'import_zip_cant_read' => 'امکان ایجاد کاربر وجود ندارد؛ زیرا ارسال ایمیل دعوت با خطا مواجه شد.',\n    'import_zip_cant_decode_data' => 'محتوای data.json در فایل ZIP پیدا یا رمزگشایی نشد.',\n    'import_zip_no_data' => 'داده‌های فایل ZIP فاقد محتوای کتاب، فصل یا صفحه مورد انتظار است.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'اعتبارسنجی فایل ZIP واردشده با خطا مواجه شد:',\n    'import_zip_failed_notification' => ' فایل ZIP وارد نشد.',\n    'import_perms_books' => 'شما مجوز لازم برای ایجاد کتاب را ندارید.',\n    'import_perms_chapters' => 'شما مجوز لازم برای ایجاد فصل را ندارید.',\n    'import_perms_pages' => 'شما مجوز لازم برای ایجاد صفحه را ندارید.',\n    'import_perms_images' => 'شما مجوز لازم برای ایجاد تصویر را ندارید.',\n    'import_perms_attachments' => 'شما مجوز لازم برای ایجاد پیوست را ندارید.',\n\n    // API errors\n    'api_no_authorization_found' => 'هیچ نشانه مجوزی در درخواست یافت نشد',\n    'api_bad_authorization_format' => 'یک نشانه مجوز در این درخواست یافت شد اما قالب نادرست به نظر می‌رسید',\n    'api_user_token_not_found' => 'هیچ نشانه API منطبقی برای کد مجوز ارائه شده یافت نشد',\n    'api_incorrect_token_secret' => 'راز ارائه شده برای کد API استفاده شده نادرست است',\n    'api_user_no_api_permission' => 'مالک نشانه API استفاده شده اجازه برقراری تماس های API را ندارد',\n    'api_user_token_expired' => 'رمز مجوز استفاده شده منقضی شده است',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'خطا در هنگام ارسال ایمیل آزمایشی:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'URL با میزبان های SSR مجاز پیکربندی شده، مطابقت ندارد',\n];\n"
  },
  {
    "path": "lang/fa/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'نظر جدید در صفحه: :pageName',\n    'new_comment_intro' => 'یک کاربر در صفحه نظر ارایه کرده است :appName:',\n    'new_page_subject' => 'صفحه جدید: :pageName',\n    'new_page_intro' => 'یک صفحه جدید ایجاد شده است در :appName:',\n    'updated_page_subject' => 'صفحه جدید: :pageName',\n    'updated_page_intro' => 'یک صفحه جدید ایجاد شده است در :appName:',\n    'updated_page_debounce' => 'برای جلوگیری از انبوه اعلان‌ها، برای مدتی اعلان‌ ویرایش‌هایی که توسط همان ویرایشگر در این صفحه انجام می‌شود، ارسال نخواهد شد.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'نام صفحه:',\n    'detail_page_path' => 'نام میسر صفحه:',\n    'detail_commenter' => 'نظر دهنده:',\n    'detail_comment' => 'نظر:',\n    'detail_created_by' => 'ایجاد شده توسط:',\n    'detail_updated_by' => 'به روزرسانی شده توسط:',\n\n    'action_view_comment' => 'مشاهده نظر',\n    'action_view_page' => 'مشاهده صفحه',\n\n    'footer_reason' => 'این اعلان برای شما ارسال شده است، زیرا پیوند (:link) فعالیتی از این نوع را برای این مورد پوشش می‌دهد.',\n    'footer_reason_link' => 'تنظیمات اطلاع‌رسانی شما',\n];\n"
  },
  {
    "path": "lang/fa/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; قبلی',\n    'next'     => 'بعدی &raquo;',\n\n];\n"
  },
  {
    "path": "lang/fa/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'گذرواژه باید حداقل هشت حرف و با تایید مطابقت داشته باشد.',\n    'user' => \"ما کاربری با این نشانی ایمیل نداریم.\",\n    'token' => 'مشخصه‌ی بازگردانی رمز عبور معتبر نیست.',\n    'sent' => 'لینک بازگردانی رمز عبور به ایمیل شما ارسال شد!',\n    'reset' => 'رمز عبور شما بازگردانی شد!',\n\n];\n"
  },
  {
    "path": "lang/fa/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'حساب کاربری من',\n\n    'shortcuts' => 'میانبرها',\n    'shortcuts_interface' => 'تنظیمات کلید‌های میانبر رابط کاربری',\n    'shortcuts_toggle_desc' => 'در اینجا می توانید میانبرهای سیستم را که برای پیمایش و ... استفاده می شود، فعال یا غیرفعال کنید.',\n    'shortcuts_customize_desc' => 'می توانید هر یک از میانبرهای زیر را سفارشی کنید. کافی است پس از انتخاب ورودی برای میانبر، کلید ترکیبی مورد نظر خود را فشار دهید.',\n    'shortcuts_toggle_label' => 'میانبرهای صفحه کلید فعال شد',\n    'shortcuts_section_navigation' => 'ناوبری و پیمایش',\n    'shortcuts_section_actions' => 'فعالیت/اقدامات مرسوم',\n    'shortcuts_save' => 'ذخیره کلیدهای میانبر',\n    'shortcuts_overlay_desc' => 'توجه: هنگامی که میانبرها فعال هستند، یک رابط کمکی با فشار دادن \"؟\" در دسترس است که میانبرهای موجود برای اقداماتی که در حال حاضر روی صفحه قابل مشاهده است را برجسته می‌کند.',\n    'shortcuts_update_success' => 'تنظیمات میانبر به روز شده است!',\n    'shortcuts_overview_desc' => 'مدیریت میانبرهای صفحه کلید برای پیمایش در رابط کاربری سیستم.',\n\n    'notifications' => 'تنظیمات اطلاع‌رسانی',\n    'notifications_desc' => 'تنظیمات اطلاعیه‌های ایمیلی هنگام انجام فعالیت‌های خاص در سیستم.',\n    'notifications_opt_own_page_changes' => 'در صورت تغییرات در صفحاتی که متعلق به من است، اطلاع بده',\n    'notifications_opt_own_page_comments' => 'در صورت ثبت نظر در صفحاتی که متعلق به من است، اطلاع بده',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'پس از درج پاسخ به روی نظراتی که من ثبت کرده‌ام، اطلاع بده',\n    'notifications_save' => 'ذخیره تنظیمات',\n    'notifications_update_success' => 'تنظیمات اعلان‌ها به روز شده است!',\n    'notifications_watched' => 'موارد مشاهده و رد شده',\n    'notifications_watched_desc' => 'در زیر آیتم‌هایی وجود دارد که تنظیمات «نظارت» سفارشی بر آن‌ها اعمال شده است. برای به‌روزرسانی تنظیمات خود در مورد هر کدام از این آیتم‌ها، روی آن کلیک کنید و سپس گزینه‌ی «نظارت» را در نوار کناری پیدا کنید.',\n\n    'auth' => 'دسترسی و امنیت',\n    'auth_change_password' => 'تغییر گذرواژه',\n    'auth_change_password_desc' => 'گذرواژه‌ای که برای ورود به برنامه استفاده می‌شود، تنظیم کنید. این عبارت باید حداقل 8 کاراکتر باشد.',\n    'auth_change_password_success' => 'رمز عبور به روز شد!',\n\n    'profile' => 'جزئیات پروفایل',\n    'profile_desc' => 'علاوه بر جزئیاتی که برای ارتباط و شخصی‌سازی سیستم استفاده می‌شود، جزییات حساب خود را که نشان دهنده شما برای سایر کاربران است، مدیریت کنید.',\n    'profile_view_public' => 'مشاهده پروفایل عمومی',\n    'profile_name_desc' => 'نام نمایشی خود را تنظیم کنید. این نام بسته به فعالیت شما و محتوای متعلق به شما، برای سایر کاربران سیستم قابل مشاهده است.',\n    'profile_email_desc' => 'این ایمیل برای اعلان‌ها و دسترسی به سیستم استفاده خواهد شد.',\n    'profile_email_no_permission' => 'متأسفانه شما اجازه تغییر آدرس ایمیل خود را ندارید. برای تغییر این بخش با ادمین سیستم در ارتباط باشید.',\n    'profile_avatar_desc' => 'تصویری را انتخاب کنید که برای نمایش حساب کاربری شما به دیگران در این سیستم استفاده شود. در حالت ایده‌آل، این تصویر باید مربع بوده و در عرض و ارتفاع حدود 256 پیکسل باشد.',\n    'profile_admin_options' => 'تنظیمات ادمین',\n    'profile_admin_options_desc' => 'گزینه‌های اضافی سطح ادمین، در قسمت «تنظیمات > کاربران» یافت می‌شوند.',\n\n    'delete_account' => 'حذف حساب کاربری',\n    'delete_my_account' => 'حذف حساب کاربری من',\n    'delete_my_account_desc' => 'با این کار حساب کاربری شما به طور کامل از سیستم حذف می‌شود. شما نمی‌توانید این حساب را بازیابی کنید. محتوایی که ایجاد کرده اید، مانند صفحات ایجاد شده و تصاویر آپلود شده، باقی خواهند ماند.',\n    'delete_my_account_warning' => 'آیا مطمئن هستید که می‌خواهید حساب کاربری خود را حذف کنید؟',\n];\n"
  },
  {
    "path": "lang/fa/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'تنظیمات',\n    'settings_save' => 'تنظیمات را ذخیره کن',\n    'system_version' => 'نسخه سیستم',\n    'categories' => 'دسته‌بندی‌ها',\n\n    // App Settings\n    'app_customization' => 'سفارشی‌سازی',\n    'app_features_security' => 'ویژگی‌ها و امنیت',\n    'app_name' => 'نام نرم‌افزار',\n    'app_name_desc' => 'این نام در هدر و در هر ایمیل ارسال شده توسط سیستم نشان داده شده است.',\n    'app_name_header' => 'نمایش نام در هدر',\n    'app_public_access' => 'دسترسی عمومی',\n    'app_public_access_desc' => 'فعال کردن این گزینه به بازدیدکنندگانی که وارد سیستم نشده‌اند اجازه می‌دهد تا به محتوای موجود در نمونه BookStack شما دسترسی داشته باشند.',\n    'app_public_access_desc_guest' => 'دسترسی بازدیدکنندگان عمومی را می توان از طریق کاربر \"مهمان\" کنترل کرد.',\n    'app_public_access_toggle' => 'اجازه دسترسی عمومی',\n    'app_public_viewing' => 'مشاهده عمومی مجاز است؟',\n    'app_secure_images' => 'آپلود تصویر با امنیت بالاتر',\n    'app_secure_images_toggle' => 'آپلود تصویر با امنیت بالاتر',\n    'app_secure_images_desc' => 'به دلایل عملکرد، همه تصاویر عمومی هستند. این گزینه یک رشته تصادفی و غیرقابل حدس زدن را در مقابل آدرس های تصویر اضافه می کند. برای جلوگیری از دسترسی آسان، اطمینان حاصل کنید که فهرست های دایرکتوری فعال نیستند.',\n    'app_default_editor' => 'ویرایشگر پیش فرض صفحه',\n    'app_default_editor_desc' => 'ویرایشگر پیش فرض در زمان ویرایش صفحات را انتخاب نمایید. این انتخاب می تواند جایگزین یک سطح صفحه که مجوز داده شده است، شود.',\n    'app_custom_html' => 'محتوای اصلی HTML سفارشی',\n    'app_custom_html_desc' => 'هر محتوای اضافه شده در اینجا در پایین بخش <head> هر صفحه درج می شود. این برای تغییر سبک ها یا اضافه کردن کد تجزیه و تحلیل مفید است.',\n    'app_custom_html_disabled_notice' => 'محتوای سر HTML سفارشی در این صفحه تنظیمات غیرفعال است تا اطمینان حاصل شود که هر گونه تغییر شکسته می تواند برگردانده شود.',\n    'app_logo' => 'لوگوی برنامه',\n    'app_logo_desc' => 'این مورد در نوار هدر برنامه و در میان سایر قسمت‌ها استفاده می‌شود. این تصویر باید 86 پیکسل ارتفاع داشته باشد. تصاویر بزرگ، کوچک نمایش داده می‌شوند.',\n    'app_icon' => 'آیکون برنامه',\n    'app_icon_desc' => 'این آیکون برای تب‌های مرورگر و نمادهای میانبر استفاده می‌شود. این مورد باید یک تصویر PNG مربعی ببه طول 256 پیکسل باشد.',\n    'app_homepage' => 'صفحه اصلی برنامه',\n    'app_homepage_desc' => 'به جای نمای پیش‌فرض، یک نمای را برای نمایش در صفحه اصلی انتخاب کنید. مجوزهای صفحه برای صفحات انتخابی نادیده گرفته می شود.',\n    'app_homepage_select' => 'یک صفحه را انتخاب کنید',\n    'app_footer_links' => 'پیوندهای پاورقی',\n    'app_footer_links_desc' => 'پیوندهایی را برای نمایش در پاورقی سایت اضافه کنید. اینها در پایین اکثر صفحات نمایش داده می شوند، از جمله صفحاتی که نیازی به ورود به سیستم ندارند. می توانید از برچسب \"trans::<key>\" برای استفاده از ترجمه های تعریف شده توسط سیستم استفاده کنید. به عنوان مثال: با استفاده از \"trans::common.privacy_policy\" متن ترجمه شده \"خط مشی رازداری\" و \"trans::common.terms_of_service\" متن ترجمه شده \"شرایط خدمات\" را ارائه می دهد.',\n    'app_footer_links_label' => 'برچسب پیوند',\n    'app_footer_links_url' => 'آدرس پیوند',\n    'app_footer_links_add' => 'پیوند پاورقی را اضافه کنید',\n    'app_disable_comments' => 'غیرفعال کردن نظرات',\n    'app_disable_comments_toggle' => 'نظرات را غیرفعال کنید',\n    'app_disable_comments_desc' => 'نظرات را در تمام صفحات برنامه غیرفعال می کند. <br> نظرات موجود نشان داده نمی شوند.',\n\n    // Color settings\n    'color_scheme' => 'ترکیب رنگی برنامه',\n    'color_scheme_desc' => 'رنگهایی که در رابط کاربری نرم‌افزار استفاده می شوند را انتخاب کنید. رنگها را میتوان برای حالت روشن یا تیره به صورت جداگانه تنظیم کرد تا هم با تم مورد استفاده سازگار بوده و هم خوانا باشند.',\n    'ui_colors_desc' => 'رنگ اصلی نرم‌افزار و رنگ پیش فرض پیوندها را انتخاب کنید. رنگ اصلی بیشتر برای بنر، کلیدها و عناصر تزیینی رابط کاربری استفاده می شوند. رنگ پیش فرض پیوند برای پیوندهای متنی و اکشن ها بکار میرود، هم در محتوای متنی و هم در رابط کاربری.',\n    'app_color' => 'رنگ اصلی',\n    'link_color' => 'رنگ پیش فرض پیوند',\n    'content_colors_desc' => 'رنگهای عناصر سلسه مراتب صفحه را انتخاب کنید. پیشنهاد می شود رنگهایی انتخاب گردند که با رنگ پیش فرض دارای روشنی مشابه باشند، تا به خوانایی کمک شود.',\n    'bookshelf_color' => 'رنگ قفسه',\n    'book_color' => 'رنگ کتاب',\n    'chapter_color' => 'رنگ فصل',\n    'page_color' => 'رنگ صفحه',\n    'page_draft_color' => 'رنگ پیش نویس صفحه',\n\n    // Registration Settings\n    'reg_settings' => 'ثبت نام',\n    'reg_enable' => 'فعال کردن ثبت نام',\n    'reg_enable_toggle' => 'فعال کردن ثبت نام',\n    'reg_enable_desc' => 'هنگامی که ثبت نام فعال باشد، کاربر می تواند خود را به عنوان کاربر برنامه ثبت نام کند. پس از ثبت نام به آنها یک نقش کاربر پیش فرض داده می شود.',\n    'reg_default_role' => 'نقش کاربر پیش فرض پس از ثبت نام',\n    'reg_enable_external_warning' => 'هنگامی که احراز هویت خارجی LDAP یا SAML فعال است، گزینه بالا نادیده گرفته می شود. در صورتی که احراز هویت، در برابر سیستم خارجی در حال استفاده، موفقیت آمیز باشد، حساب های کاربری برای اعضای غیر موجود به طور خودکار ایجاد می شود.',\n    'reg_email_confirmation' => 'تایید ایمیل',\n    'reg_email_confirmation_toggle' => 'نیاز به تایید ایمیل',\n    'reg_confirm_email_desc' => 'در صورت استفاده از محدودیت دامنه، تایید ایمیل مورد نیاز است و این گزینه نادیده گرفته می شود.',\n    'reg_confirm_restrict_domain' => 'محدودیت دامنه',\n    'reg_confirm_restrict_domain_desc' => 'فهرستی از دامنه‌های ایمیل جدا شده با کاما را وارد کنید که می‌خواهید ثبت نام را محدود کنید. قبل از اینکه به کاربران اجازه تعامل با برنامه داده شود، ایمیلی برای تأیید آدرس آنها ارسال می شود. <br> توجه داشته باشید که کاربران پس از ثبت نام موفق می توانند آدرس ایمیل خود را تغییر دهند.',\n    'reg_confirm_restrict_domain_placeholder' => 'بدون محدودیت',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'قانون پیش‌فرض مرتب‌سازی را برای کتاب‌های جدید انتخاب کنید. تغییر قانون بر ترتیب کتاب‌های موجود تأثیری ندارد و می‌تواند برای هر کتاب به‌صورت جداگانه تغییر یابد.',\n    'sorting_rules' => 'قوانین مرتب‌سازی',\n    'sorting_rules_desc' => 'این‌ها عملیات مرتب‌سازی از پیش تعریف‌شده‌ای هستند که می‌توانید آن‌ها را بر محتوای سیستم اعمال کنید.',\n    'sort_rule_assigned_to_x_books' => 'اختصاص داده شده به :count کتاب',\n    'sort_rule_create' => 'ایجاد قانون مرتب‌سازی',\n    'sort_rule_edit' => 'ویرایش قانون مرتب‌سازی',\n    'sort_rule_delete' => 'حذف قانون مرتب‌سازی',\n    'sort_rule_delete_desc' => 'این قانون مرتب‌سازی را از سیستم حذف کنید. کتاب‌هایی که از این شیوه مرتب‌سازی استفاده می‌کنند، به حالت مرتب‌سازی دستی بازخواهند گشت.',\n    'sort_rule_delete_warn_books' => 'در حال حاضر این قانون مرتب‌سازی برای :count کتاب به‌کار می‌رود. آیا مطمئن هستید که می‌خواهید آن را حذف کنید؟',\n    'sort_rule_delete_warn_default' => 'این قانون مرتب‌سازی هم‌اکنون به عنوان پیش‌فرض کتاب‌ها تعیین شده است. آیا مطمئن هستید که می‌خواهید آن را حذف کنید؟',\n    'sort_rule_details' => 'جزئیات قانون مرتب‌سازی',\n    'sort_rule_details_desc' => 'برای این قانون مرتب‌سازی یک نام انتخاب کنید. این نام هنگام انتخاب شیوه مرتب‌سازی در فهرست‌ها نمایش داده خواهد شد.',\n    'sort_rule_operations' => 'عملیات مرتب‌سازی',\n    'sort_rule_operations_desc' => 'عملیات مرتب‌سازی را پیکربندی کنید. برای این منظور، آن‌ها را در فهرست عملیاتِ در دسترس جابه‌جا کنید تا ترتیب اجرای آن‌ها مشخص شود. هنگام استفاده، این عملیات به‌ترتیب از بالا به پایین اعمال خواهند شد. هر تغییری که در این بخش ایجاد کنید، پس از ذخیره، برای همه کتاب‌های اختصاص‌یافته اجرا می‌شود.',\n    'sort_rule_available_operations' => 'عملیات موجود',\n    'sort_rule_available_operations_empty' => 'عملیاتی باقی نمانده است',\n    'sort_rule_configured_operations' => 'عملیات پیکربندی‌شده',\n    'sort_rule_configured_operations_empty' => 'عملیات را از فهرست «عملیات موجود» حذف یا اضافه کنید',\n    'sort_rule_op_asc' => '(صعودی)',\n    'sort_rule_op_desc' => '(نزولی)',\n    'sort_rule_op_name' => 'نام - الفبایی',\n    'sort_rule_op_name_numeric' => 'نام - عددی',\n    'sort_rule_op_created_date' => 'تاریخ ایجاد',\n    'sort_rule_op_updated_date' => 'تاریخ به‌روزرسانی',\n    'sort_rule_op_chapters_first' => 'ابتدا فصل‌ها',\n    'sort_rule_op_chapters_last' => 'فصل‌ها در آخر',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'نگهداری',\n    'maint_image_cleanup' => 'پاکسازی تصاویر',\n    'maint_image_cleanup_desc' => 'محتوای صفحه و بازبینی را اسکن می‌کند تا بررسی کند که کدام تصاویر و نقاشی‌ها در حال حاضر استفاده می‌شوند و کدام تصاویر اضافی هستند. قبل از اجرای این کار، مطمئن شوید که یک پایگاه داده کامل و یک نسخه پشتیبان از تصویر ایجاد کرده اید.',\n    'maint_delete_images_only_in_revisions' => 'همچنین تصاویری را که فقط در ویرایش های صفحه قدیمی وجود دارند حذف کنید',\n    'maint_image_cleanup_run' => 'پاکسازی را اجرا کنید',\n    'maint_image_cleanup_warning' => ':count تصاویر بالقوه استفاده نشده پیدا شد. آیا مطمئن هستید که می خواهید این تصاویر را حذف کنید؟',\n    'maint_image_cleanup_success' => ':count تصویر بالقوه استفاده نشده پیدا و حذف شد!',\n    'maint_image_cleanup_nothing_found' => 'هیچ تصویر استفاده نشده ای یافت نشد، چیزی حذف نشد!',\n    'maint_send_test_email' => 'یک ایمیل آزمایشی ارسال کنید',\n    'maint_send_test_email_desc' => 'این یک ایمیل آزمایشی به آدرس ایمیل شما مشخص شده در نمایه شما ارسال می کند.',\n    'maint_send_test_email_run' => 'ارسال ایمیل آزمایشی',\n    'maint_send_test_email_success' => 'ایمیل به آدرس :address ارسال شد',\n    'maint_send_test_email_mail_subject' => 'تست ایمیل',\n    'maint_send_test_email_mail_greeting' => 'به نظر می رسد تحویل ایمیل کار می کند!',\n    'maint_send_test_email_mail_text' => 'تبریک می گویم! با دریافت این اعلان ایمیل، به نظر می رسد تنظیمات ایمیل شما به درستی پیکربندی شده است.',\n    'maint_recycle_bin_desc' => 'قفسه‌ها، کتاب‌ها، فصل‌ها و صفحات حذف‌شده به سطل بازیافت فرستاده می‌شوند تا بتوان آن‌ها را بازیابی کرد یا برای همیشه حذف کرد. بسته به پیکربندی سیستم، اقلام قدیمی در سطل بازیافت ممکن است پس از مدتی به طور خودکار حذف شوند.',\n    'maint_recycle_bin_open' => 'سطل بازیافت را باز کنید',\n    'maint_regen_references' => 'تولید مجدد رفرنس ها',\n    'maint_regen_references_desc' => 'این عمل اندیس رفرنس های میان اقلامی موجود در دیتابیس را بازسازی خواهد کرد. چنین کاری معمولا به صورت خودکار انجام میشود، ولی این عمل برای اندیس گذاری محتویات قدیمی یا محتویایی که از طرق غیرمعمول اضافه شده اند مفید است.',\n    'maint_regen_references_success' => 'اندیس رفرنس ها بازسازی شدند!',\n    'maint_timeout_command_note' => 'توجه: این عملیات زمان بر خواهد بود، که ممکن در برخی محیطهای وب باعث از دسترس خارج شدن شوند. به عنوان گزینه جایگزین، این عمل را میتوان با استفاده از یک دستور ترمینال انجام داد.',\n\n    // Recycle Bin\n    'recycle_bin' => 'سطل زباله',\n    'recycle_bin_desc' => 'در اینجا می توانید مواردی را که حذف شده اند بازیابی کنید یا حذف دائمی آنها را از سیستم انتخاب کنید. این لیست برخلاف لیست‌های فعالیت مشابه در سیستمی که فیلترهای مجوز اعمال می‌شوند، فیلتر نشده است.',\n    'recycle_bin_deleted_item' => 'مورد حذف شده',\n    'recycle_bin_deleted_parent' => 'والد',\n    'recycle_bin_deleted_by' => 'حذف شده توسط',\n    'recycle_bin_deleted_at' => 'زمان حذف',\n    'recycle_bin_permanently_delete' => 'برای همیشه حذف کنید',\n    'recycle_bin_restore' => 'بازگرداندن',\n    'recycle_bin_contents_empty' => 'سطل بازیافت در حال حاضر خالی است',\n    'recycle_bin_empty' => 'سطل آشغال خالی',\n    'recycle_bin_empty_confirm' => 'این کار همه اقلام موجود در سطل بازیافت از جمله محتوای موجود در هر مورد را برای همیشه از بین می برد. آیا مطمئن هستید که می خواهید سطل بازیافت را خالی کنید؟',\n    'recycle_bin_destroy_confirm' => 'این کار تمامی عناصر زیر مجموعه آیتم که در پایین فهرست شده را به همراه خودش برای همیشه از سامانه پاک میکند و شما امکان بازیابی آن را نخواهید داشت. آیا مطمئن هستید که میخواهید برای همیشه این آیتم را پاک کنید؟',\n    'recycle_bin_destroy_list' => 'مواردی که باید نابود شوند',\n    'recycle_bin_restore_list' => 'مواردی که باید بازیابی شوند',\n    'recycle_bin_restore_confirm' => 'این اقدام، مورد حذف شده، از جمله هر عنصر فرزند، را به مکان اصلی خود باز می گرداند. اگر مکان اصلی از آن زمان حذف شده باشد، و اکنون در سطل بازیافت است، مورد اصلی نیز باید بازیابی شود.',\n    'recycle_bin_restore_deleted_parent' => 'والد این مورد نیز حذف شده است. تا زمانی که آن والد نیز بازیابی نشود، این موارد حذف خواهند شد.',\n    'recycle_bin_restore_parent' => 'بازیابی والد',\n    'recycle_bin_destroy_notification' => ':count تعداد از کل اقلام از سطل بازیافت حذف شده.',\n    'recycle_bin_restore_notification' => ':count تعداد از کل اقلام از سطل بازیافت بازیابی شده.',\n\n    // Audit Log\n    'audit' => 'گزارش حسابرسی',\n    'audit_desc' => 'این گزارش حسابرسی لیستی از فعالیت های ردیابی شده در سیستم را نمایش می دهد. این لیست برخلاف لیست‌های فعالیت مشابه در سیستمی که فیلترهای مجوز اعمال می‌شوند، فیلتر نشده است.',\n    'audit_event_filter' => 'فیلتر رویداد',\n    'audit_event_filter_no_filter' => 'بدون فیلتر',\n    'audit_deleted_item' => 'مورد حذف شده',\n    'audit_deleted_item_name' => 'نام  :name',\n    'audit_table_user' => 'نام كاربري',\n    'audit_table_event' => 'رویداد',\n    'audit_table_related' => 'مورد یا جزئیات مرتبط',\n    'audit_table_ip' => 'آدرس IP',\n    'audit_table_date' => 'تاریخ‌های فعالیت',\n    'audit_date_from' => 'محدوده تاریخ از',\n    'audit_date_to' => 'محدوده تاریخ تا',\n\n    // Role Settings\n    'roles' => 'نقش‌ها',\n    'role_user_roles' => 'نقش‌های کاربر',\n    'roles_index_desc' => 'نقش‌ها برای گروه‌بندی کاربران و ارائه مجوز سیستم به اعضای آن‌ها استفاده می‌شوند. هنگامی که یک کاربر عضو چندین نقش باشد، امتیازات اعطا شده روی هم قرار می‌گیرند و کاربر تمام مجوزها را به ارث می‌برد.',\n    'roles_x_users_assigned' => ':count کاربر اختصاص داده شده|:count کاربر اختصاص داده شده',\n    'roles_x_permissions_provided' => ':count مجوز|:count مجوز',\n    'roles_assigned_users' => 'کاربران اختصاص داده شده',\n    'roles_permissions_provided' => 'دسترسی های موجود',\n    'role_create' => 'نقش جدید ایجاد کنید',\n    'role_delete' => 'حذف نقش',\n    'role_delete_confirm' => 'با این کار نقش با نام \\':roleName\\' حذف می شود.',\n    'role_delete_users_assigned' => 'این نقش دارای :userCount کاربرانی است که به آن اختصاص داده شده است. اگر می خواهید کاربران را از این نقش مهاجرت کنید، نقش جدیدی را در زیر انتخاب کنید.',\n    'role_delete_no_migration' => \"کاربران را منتقل نکنید\",\n    'role_delete_sure' => 'آیا مطمئنید که می خواهید این نقش را حذف کنید؟',\n    'role_edit' => 'ویرایش نقش',\n    'role_details' => 'جزئیات نقش',\n    'role_name' => 'اسم نقش',\n    'role_desc' => 'شرح کوتاه نقش',\n    'role_mfa_enforced' => 'به احراز هویت چند عاملی نیاز دارد',\n    'role_external_auth_id' => 'شناسه های تأیید هویت خارجی',\n    'role_system' => 'مجوزهای سیستم',\n    'role_manage_users' => 'مدیریت کاربران',\n    'role_manage_roles' => 'نقش ها و مجوزهای نقش را مدیریت کنید',\n    'role_manage_entity_permissions' => 'تمام مجوزهای کتاب، فصل و صفحه را مدیریت کنید',\n    'role_manage_own_entity_permissions' => 'مجوزها را در کتاب، فصل و صفحات خود مدیریت کنید',\n    'role_manage_page_templates' => 'مدیریت قالب های صفحه',\n    'role_access_api' => 'دسترسی به API سیستم',\n    'role_manage_settings' => 'تنظیمات برنامه را مدیریت کنید',\n    'role_export_content' => 'صادرات محتوا',\n    'role_import_content' => 'وارد کردن محتوا',\n    'role_editor_change' => 'تغییر ویرایشگر صفحه',\n    'role_notifications' => 'دریافت و مدیریت اعلان‌ها',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'مجوزهای دارایی',\n    'roles_system_warning' => 'توجه داشته باشید که دسترسی به هر یک از سه مجوز فوق می‌تواند به کاربر اجازه دهد تا امتیازات خود یا امتیازات دیگران را در سیستم تغییر دهد. فقط نقش هایی را با این مجوزها به کاربران مورد اعتماد اختصاص دهید.',\n    'role_asset_desc' => 'این مجوزها دسترسی پیش‌فرض به دارایی‌های درون سیستم را کنترل می‌کنند. مجوزهای مربوط به کتاب‌ها، فصل‌ها و صفحات این مجوزها را لغو می‌کنند.',\n    'role_asset_admins' => 'به ادمین‌ها به‌طور خودکار به همه محتوا دسترسی داده می‌شود، اما این گزینه‌ها ممکن است گزینه‌های UI را نشان داده یا پنهان کنند.',\n    'role_asset_image_view_note' => 'این مربوط به مرئی بودن در بخش مدیر تصاویر است. دسترسی عملی به تصاویر آپلود شده بستگی به گزینه ذخیره‌سازی تصویر سیستم دارد.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'همه',\n    'role_own' => 'صاحب',\n    'role_controlled_by_asset' => 'توسط دارایی که در آن آپلود می شود کنترل می شود',\n    'role_save' => 'ذخیره نقش',\n    'role_users' => 'کاربران در این نقش',\n    'role_users_none' => 'در حال حاضر هیچ کاربری به این نقش اختصاص داده نشده است',\n\n    // Users\n    'users' => 'کاربران',\n    'users_index_desc' => 'ساخت و مدیریت حساب های کاربری درون سیستم. حساب های کاربری برای ورود به سیستم و تخصیص محتوا و فعالیت ها به کار می روند. اجازه های دسترسی عموما بر مبنای نقش کاربر هستند، ولی مالکیت محتوای کاربر (در کنار سایر عوامل) روی اجازه و دسترسی تاثیر گذارند.',\n    'user_profile' => 'پرونده کاربر',\n    'users_add_new' => 'افزودن کاربر جدید',\n    'users_search' => 'جستجوی کاربران',\n    'users_latest_activity' => 'آخرین فعالیت',\n    'users_details' => 'جزئیات کاربر',\n    'users_details_desc' => 'یک نام نمایشی و یک آدرس ایمیل برای این کاربر تنظیم کنید. آدرس ایمیل برای ورود به برنامه استفاده خواهد شد.',\n    'users_details_desc_no_email' => 'یک نام نمایشی برای این کاربر تنظیم کنید تا دیگران بتوانند آنها را تشخیص دهند.',\n    'users_role' => 'نقش های کاربر',\n    'users_role_desc' => 'انتخاب کنید که این کاربر به کدام نقش ها اختصاص داده شود. اگر یک کاربر به چندین نقش اختصاص داده شود، مجوزهای آن نقش‌ها روی هم قرار می‌گیرند و تمام توانایی‌های نقش‌های اختصاص داده شده را دریافت خواهند کرد.',\n    'users_password' => 'رمز عبور كاربر',\n    'users_password_desc' => 'رمز عبوری را که برای ورود به برنامه استفاده می شود تنظیم کنید. این باید حداقل 8 کاراکتر باشد.',\n    'users_send_invite_text' => 'می توانید انتخاب کنید که برای این کاربر یک ایمیل دعوت نامه ارسال شود که به آنها امکان می دهد رمز عبور خود را تعیین کنند در غیر این صورت می توانید رمز عبور خود را تعیین کنید.',\n    'users_send_invite_option' => 'ارسال ایمیل دعوت کاربر',\n    'users_external_auth_id' => 'شناسه احراز هویت خارجی',\n    'users_external_auth_id_desc' => 'هنگامی که یک سامانه احراز هویت خارجی مورد استفاده است (مانند SAML2، OIDC یا LDAP)، شناسه ای که کاربر BookStack را به حساب سامانه احراز هویت مرتبط میکند بدین‌گونه می‌باشد. اگر از احراز هویت پیش فرض ایمیلی استفاده می‌کنید، میتوانید این را در نظر نگیرید.',\n    'users_password_warning' => 'فقط در صورتی که مایل به تغییر رمز عبور این کاربر هستید، موارد زیر را پر کنید.',\n    'users_system_public' => 'این کاربر نماینده هر کاربر مهمانی است که از نمونه شما بازدید می کند. نمی توان از آن برای ورود استفاده کرد اما به طور خودکار اختصاص داده می شود.',\n    'users_delete' => 'حذف کاربر',\n    'users_delete_named' => 'حذف :userName',\n    'users_delete_warning' => 'با این کار این کاربر با نام \\':userName\\' به طور کامل از سیستم حذف می شود.',\n    'users_delete_confirm' => 'آیا مطمئن هستید که می خواهید این کاربر را حذف کنید؟',\n    'users_migrate_ownership' => 'انتقال مالکیت',\n    'users_migrate_ownership_desc' => 'اگر می‌خواهید کاربر دیگری مالک همه مواردی باشد که در حال حاضر متعلق به این کاربر است، کاربری را در اینجا انتخاب کنید.',\n    'users_none_selected' => 'هیچ کاربری انتخاب نشد',\n    'users_edit' => 'ویرایش کاربر',\n    'users_edit_profile' => 'ویرایش پروفایل',\n    'users_avatar' => 'آواتار کاربر',\n    'users_avatar_desc' => 'تصویری را برای نشان دادن این کاربر انتخاب کنید. این باید تقریباً 256 پیکسل مربع باشد.',\n    'users_preferred_language' => 'زبان ترجیحی',\n    'users_preferred_language_desc' => 'این گزینه زبان مورد استفاده برای رابط کاربری برنامه را تغییر می دهد. این روی محتوای ایجاد شده توسط کاربر تأثیری نخواهد داشت.',\n    'users_social_accounts' => 'حساب های اجتماعی',\n    'users_social_accounts_desc' => 'مشاهده وضعیت حساب‌های اجتماعی متصل به این کاربر. حساب‌های اجتماعی می‌توانند به عنوان تکمیلی به سیستم اصلی احراز هویت برای دسترسی به سیستم استفاده شوند.',\n    'users_social_accounts_info' => 'در اینجا می‌توانید حساب‌های دیگر خود را برای ورود سریع‌تر و آسان‌تر متصل کنید. قطع ارتباط حساب در اینجا، دسترسی مجاز قبلی را لغو نمی کند. دسترسی را از تنظیمات نمایه خود در حساب اجتماعی متصل لغو کنید.',\n    'users_social_connect' => 'اتصال حساب کاربری',\n    'users_social_disconnect' => 'قطع حساب',\n    'users_social_status_connected' => 'ارتباط برقرار شد',\n    'users_social_status_disconnected' => 'قطع اتصال',\n    'users_social_connected' => 'حساب :socialAccount با موفقیت به نمایه شما پیوست شد.',\n    'users_social_disconnected' => 'حساب :socialAccount با موفقیت از نمایه شما قطع شد.',\n    'users_api_tokens' => 'توکن‌های API',\n    'users_api_tokens_desc' => 'توکن های دسترسی مورد استفاده در BookStack REST API را بسازید و مدیریت کنید. اجازه‌های مربوط به API توسط کاربری که صاحب آنها است مدیریت می‌شوند.',\n    'users_api_tokens_none' => 'هیچ نشانه API برای این کاربر ایجاد نشده است',\n    'users_api_tokens_create' => 'ایجاد توکن',\n    'users_api_tokens_expires' => 'منقضی شده ها',\n    'users_api_tokens_docs' => 'مستندات API',\n    'users_mfa' => 'احراز هویت چند عاملی',\n    'users_mfa_desc' => 'تنظیم احراز هویت چند مرحله ای یک لایه امنیتی دیگر به حساب شما اضافه میکند.',\n    'users_mfa_x_methods' => ':count روش پیکربندی شده است|:count روش های پیکربندی شده',\n    'users_mfa_configure' => 'روش پیکربندی',\n\n    // API Tokens\n    'user_api_token_create' => 'ایجاد توکن API',\n    'user_api_token_name' => 'نام',\n    'user_api_token_name_desc' => 'توکن خود را به عنوان یادآوری هدف مورد نظر در آینده، نامی خوانا بدهید.',\n    'user_api_token_expiry' => 'تاریخ انقضا',\n    'user_api_token_expiry_desc' => 'تاریخی را تعیین کنید که در آن این توکن منقضی شود. پس از این تاریخ، درخواست‌هایی که با استفاده از این رمز انجام می‌شوند دیگر کار نمی‌کنند. خالی گذاشتن این فیلد باعث انقضای 100 سال آینده می شود.',\n    'user_api_token_create_secret_message' => 'بلافاصله پس از ایجاد این توکن یک \"شناسه رمز\" و \"رمز رمز\" تولید و نمایش داده می شود. راز فقط یک بار نشان داده می‌شود، بنابراین قبل از ادامه، حتماً مقدار را در جایی امن و مطمئن کپی کنید.',\n    'user_api_token' => 'توکن API',\n    'user_api_token_id' => 'شناسه توکن',\n    'user_api_token_id_desc' => 'این یک شناسه غیرقابل ویرایش است که برای این نشانه ایجاد شده است که باید در درخواست‌های API ارائه شود.',\n    'user_api_token_secret' => 'رمز توکن',\n    'user_api_token_secret_desc' => 'این یک راز ایجاد شده توسط سیستم برای این نشانه است که باید در درخواست های API ارائه شود. این فقط یک بار نمایش داده می شود، بنابراین این مقدار را در جایی امن و مطمئن کپی کنید.',\n    'user_api_token_created' => 'توکن ایجاد شد :timeAgo',\n    'user_api_token_updated' => 'توکن به روز شد :timeAgo',\n    'user_api_token_delete' => 'توکن را حذف کنید',\n    'user_api_token_delete_warning' => 'با این کار این نشانه API با نام \\':tokenName\\' به طور کامل از سیستم حذف می شود.',\n    'user_api_token_delete_confirm' => 'آیا مطمئن هستید که می خواهید این نشانه API را حذف کنید؟',\n\n    // Webhooks\n    'webhooks' => 'وب‌هوک‌ها',\n    'webhooks_index_desc' => 'وب هوک ها روشی برای ارسال داده به آدرس های اینترنتی خارج از سیستم، بر اساس اتفاقات خاصی درون سیستم هستند که امکان یکپارچه سازی مبتنی بر وقایع را با سایر سیستم ها، مثل سیستم پیام رسانی یا اطلاع رسانی، فراهم می کنند.',\n    'webhooks_x_trigger_events' => ':count trigger event|:count trigger events',\n    'webhooks_create' => 'ایجاد وب هوک جدید',\n    'webhooks_none_created' => 'هنوز هیچ وب هوکی ایجاد نشده است.',\n    'webhooks_edit' => 'ویرایش وب هوک',\n    'webhooks_save' => 'ذخیره وب هوک',\n    'webhooks_details' => 'جزئیات وب هوک',\n    'webhooks_details_desc' => 'یک نام کاربر پسند و یک نقطه پایانی POST به عنوان مکانی برای ارسال داده های وب هوک ارائه دهید.',\n    'webhooks_events' => 'رویدادهای وب هوک',\n    'webhooks_events_desc' => 'تمام رویدادهایی را که باید باعث فراخوانی این وب هوک شوند، انتخاب کنید.',\n    'webhooks_events_warning' => 'به خاطر داشته باشید که این رویدادها برای همه رویدادهای انتخابی فعال خواهند شد، حتی اگر مجوزهای سفارشی اعمال شوند. مطمئن شوید که استفاده از این وب هوک محتوای محرمانه را فاش نمی کند.',\n    'webhooks_events_all' => 'تمام رویدادهای سیستم',\n    'webhooks_name' => 'نام وب هوک',\n    'webhooks_timeout' => 'مهلت درخواست وب هوک (ثانیه)',\n    'webhooks_endpoint' => 'نقطه پایانی وب هوک',\n    'webhooks_active' => 'وب هوک فعال',\n    'webhook_events_table_header' => 'رویدادها',\n    'webhooks_delete' => 'حذف وب هوک',\n    'webhooks_delete_warning' => 'با این کار این وب هوک با نام \\':webhookName\\' به طور کامل از سیستم حذف می شود.',\n    'webhooks_delete_confirm' => 'آیا مطمئن هستید که می خواهید این وب هوک را حذف کنید؟',\n    'webhooks_format_example' => 'نمونه قالب وب هوک',\n    'webhooks_format_example_desc' => 'داده‌های وب هوک به‌عنوان یک درخواست POST به نقطه پایانی پیکربندی‌شده به‌عنوان JSON با فرمت زیر ارسال می‌شوند. ویژگی های \"related_item\" و \"url\" اختیاری هستند و به نوع رویداد راه‌اندازی شده بستگی دارد.',\n    'webhooks_status' => 'وضعیت وب هوک',\n    'webhooks_last_called' => 'آخرین تماس:',\n    'webhooks_last_errored' => 'آخرین خطا:',\n    'webhooks_last_error_message' => 'آخرین پیغام خطا:',\n\n    // Licensing\n    'licenses' => 'مجوز‌ها',\n    'licenses_desc' => 'این صفحه اطلاعات مجوز‌های بوک استک به همراه پروژه‌ها و کتابخانه‌های مورد استفاده با آن را به نمایش می‌گذارد. بسیاری از پروژه‌های فهرست شده ممکن است فقط در بحث توسعه به کار گرفته شده باشند.',\n    'licenses_bookstack' => 'مجوز‌های بوک استک',\n    'licenses_php' => 'مجوز‌های کتابخانه PHP',\n    'licenses_js' => 'مجوز‌های کتابخانه جاوا اسکریپت',\n    'licenses_other' => 'سایر مجوز‌ها',\n    'license_details' => 'جزئیات مجوز',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/fa/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute باید پذیرفته شده باشد.',\n    'active_url'           => 'آدرس :attribute معتبر نیست.',\n    'after'                => ':attribute باید تاریخی بعد از :date باشد.',\n    'alpha'                => ':attribute باید فقط حروف الفبا باشد.',\n    'alpha_dash'           => ':attribute باید فقط حروف الفبا، اعداد، خط تیره و زیرخط باشد.',\n    'alpha_num'            => ':attribute باید فقط حروف الفبا و اعداد باشد.',\n    'array'                => ':attribute باید آرایه باشد.',\n    'backup_codes'         => 'کد ارائه شده معتبر نیست یا قبلا استفاده شده است.',\n    'before'               => ':attribute باید تاریخی قبل از :date باشد.',\n    'between'              => [\n        'numeric' => ':attribute باید بین :min و :max باشد.',\n        'file'    => ':attribute باید بین :min و :max کیلوبایت باشد.',\n        'string'  => ':attribute باید بین :min و :max کاراکتر باشد.',\n        'array'   => ':attribute باید بین :min و :max آیتم باشد.',\n    ],\n    'boolean'              => ':attribute فقط می‌تواند true و یا false باشد.',\n    'confirmed'            => ':attribute با فیلد تکرار مطابقت ندارد.',\n    'date'                 => ':attribute یک تاریخ معتبر نیست.',\n    'date_format'          => ':attribute با الگوی :format مطابقت ندارد.',\n    'different'            => ':attribute و :other باید از یکدیگر متفاوت باشند.',\n    'digits'               => ':attribute باید :digits رقم باشد.',\n    'digits_between'       => ':attribute باید بین :min و :max رقم باشد.',\n    'email'                => ':attribute باید یک ایمیل معتبر باشد.',\n    'ends_with' => ':attribute باید با یکی از مقادیر زیر خاتمه یابد: :values',\n    'file'                 => ':attribute باید به عنوان یک فایل معتبر شناخته شود.',\n    'filled'               => ':attribute باید مقدار داشته باشد.',\n    'gt'                   => [\n        'numeric' => ':attribute باید بزرگتر از :value باشد.',\n        'file'    => ':attribute باید بزرگتر از :value کیلوبایت باشد.',\n        'string'  => ':attribute باید بیشتر از :value کاراکتر داشته باشد.',\n        'array'   => ':attribute باید بیشتر از :value آیتم داشته باشد.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute باید بزرگتر یا مساوی :value باشد.',\n        'file'    => ':attribute باید بزرگتر یا مساوی :value کیلوبایت باشد.',\n        'string'  => ':attribute باید بیشتر یا مساوی :value کاراکتر داشته باشد.',\n        'array'   => ':attribute باید بیشتر یا مساوی :value آیتم داشته باشد.',\n    ],\n    'exists'               => ':attribute انتخاب شده، معتبر نیست.',\n    'image'                => ':attribute باید یک تصویر معتبر باشد.',\n    'image_extension'      => ':attribute باید یک تصویر با فرمت معتبر باشد.',\n    'in'                   => ':attribute انتخاب شده، معتبر نیست.',\n    'integer'              => ':attribute باید عدد صحیح باشد.',\n    'ip'                   => ':attribute باید آدرس IP معتبر باشد.',\n    'ipv4'                 => ':attribute باید یک آدرس معتبر از نوع IPv4 باشد.',\n    'ipv6'                 => ':attribute باید یک آدرس معتبر از نوع IPv6 باشد.',\n    'json'                 => ':attribute باید یک رشته از نوع JSON باشد.',\n    'lt'                   => [\n        'numeric' => ':attribute باید کوچکتر از :value باشد.',\n        'file'    => ':attribute باید کوچکتر از :value کیلوبایت باشد.',\n        'string'  => ':attribute باید کمتر از :value کاراکتر داشته باشد.',\n        'array'   => ':attribute باید کمتر از :value آیتم داشته باشد.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute باید کوچکتر یا مساوی :value باشد.',\n        'file'    => ':attribute باید کوچکتر یا مساوی :value کیلوبایت باشد.',\n        'string'  => ':attribute باید کمتر یا مساوی :value کاراکتر داشته باشد.',\n        'array'   => ':attribute باید کمتر یا مساوی :value آیتم داشته باشد.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute نباید بزرگتر از :max باشد.',\n        'file'    => ':attribute نباید بزرگتر از :max کیلوبایت باشد.',\n        'string'  => ':attribute نباید بیشتر از :max کاراکتر داشته باشد.',\n        'array'   => ':attribute نباید بیشتر از :max آیتم داشته باشد.',\n    ],\n    'mimes'                => 'فرمت‌های معتبر فایل عبارتند از: :values.',\n    'min'                  => [\n        'numeric' => ':attribute نباید کوچکتر از :min باشد.',\n        'file'    => ':attribute نباید کوچکتر از :min کیلوبایت باشد.',\n        'string'  => ':attribute نباید کمتر از :min کاراکتر داشته باشد.',\n        'array'   => ':attribute نباید کمتر از :min آیتم داشته باشد.',\n    ],\n    'not_in'               => ':attribute انتخاب شده، معتبر نیست.',\n    'not_regex'            => 'فرمت :attribute معتبر نیست.',\n    'numeric'              => ':attribute باید عدد یا رشته‌ای از اعداد باشد.',\n    'regex'                => 'فرمت :attribute معتبر نیست.',\n    'required'             => ':attribute الزامی است.',\n    'required_if'          => 'هنگامی که :other برابر با :value است، فیلد :attribute الزامی است.',\n    'required_with'        => 'در صورت وجود فیلد :values، فیلد :attribute نیز الزامی است.',\n    'required_with_all'    => 'در صورت وجود فیلدهای :values، فیلد :attribute نیز الزامی است.',\n    'required_without'     => 'در صورت عدم وجود فیلد :values، فیلد :attribute الزامی است.',\n    'required_without_all' => 'در صورت عدم وجود هر یک از فیلدهای :values، فیلد :attribute الزامی است.',\n    'same'                 => ':attribute و :other باید همانند هم باشند.',\n    'safe_url'             => ':attribute معتبر نمی‌باشد.',\n    'size'                 => [\n        'numeric' => ':attribute باید برابر با :size باشد.',\n        'file'    => ':attribute باید برابر با :size کیلوبایت باشد.',\n        'string'  => ':attribute باید برابر با :size کاراکتر باشد.',\n        'array'   => ':attribute باید شامل :size آیتم باشد.',\n    ],\n    'string'               => 'فیلد :attribute باید متن باشد.',\n    'timezone'             => 'فیلد :attribute باید یک منطقه زمانی معتبر باشد.',\n    'totp'                 => 'کد ارائه شده معتبر نیست یا منقضی شده است.',\n    'unique'               => ':attribute قبلا انتخاب شده است.',\n    'url'                  => ':attribute معتبر نمی‌باشد.',\n    'uploaded'             => 'بارگذاری فایل :attribute موفقیت آمیز نبود.',\n\n    'zip_file' => 'ویژگی :attribute باید به یک فایل درون پرونده فشرده شده اشاره کند.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'ویژگی :attribute باید به فایلی با نوع :validTypes اشاره کند، اما نوع یافت‌شده :foundType است.',\n    'zip_model_expected' => 'سیستم در این بخش انتظار دریافت یک شیء داده‌ای را داشت، اما «:type» دریافت گردید',\n    'zip_unique' => 'برای هر نوع شیء در فایل ZIP، مقدار ویژگی :attribute باید یکتا و بدون تکرار باشد.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'تایید کلمه عبور اجباری می باشد',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/fi/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'loi sivun',\n    'page_create_notification'    => 'Sivu luotiin onnistuneesti',\n    'page_update'                 => 'päivitti sivun',\n    'page_update_notification'    => 'Sivu päivitettiin onnistuneesti',\n    'page_delete'                 => 'poisti sivun',\n    'page_delete_notification'    => 'Sivu poistettiin onnistuneesti',\n    'page_restore'                => 'palautti sivun',\n    'page_restore_notification'   => 'Sivu palautettiin onnistuneesti',\n    'page_move'                   => 'siirsi sivun',\n    'page_move_notification'      => 'Sivu siirrettiin onnistuneesti',\n\n    // Chapters\n    'chapter_create'              => 'loi luvun',\n    'chapter_create_notification' => 'Luku luotiin onnistuneesti',\n    'chapter_update'              => 'päivitti luvun',\n    'chapter_update_notification' => 'Luku päivitettiin onnistuneesti',\n    'chapter_delete'              => 'poisti luvun',\n    'chapter_delete_notification' => 'Sivu poistettiin onnistuneesti',\n    'chapter_move'                => 'siirsi luvun',\n    'chapter_move_notification' => 'Sivu siirrettiin onnistuneesti',\n\n    // Books\n    'book_create'                 => 'loi kirjan',\n    'book_create_notification'    => 'Kirja luotiin onnistuneesti',\n    'book_create_from_chapter'              => 'muunsi luvun kirjaksi',\n    'book_create_from_chapter_notification' => 'Luku muunnettiin onnistuneesti kirjaksi',\n    'book_update'                 => 'päivitti kirjan',\n    'book_update_notification'    => 'Kirja päivitettiin onnistuneesti',\n    'book_delete'                 => 'poisti kirjan',\n    'book_delete_notification'    => 'Kirja poistettiin onnistuneesti',\n    'book_sort'                   => 'järjesti kirjan',\n    'book_sort_notification'      => 'Kirja järjestettiin uudelleen onnistuneesti',\n\n    // Bookshelves\n    'bookshelf_create'            => 'loi hyllyn',\n    'bookshelf_create_notification'    => 'Hylly luotiin onnistuneesti',\n    'bookshelf_create_from_book'    => 'muunsi kirjan hyllyksi',\n    'bookshelf_create_from_book_notification'    => 'Kirja muunnettiin onnistuneesti hyllyksi',\n    'bookshelf_update'                 => 'päivitti hyllyn',\n    'bookshelf_update_notification'    => 'Hylly päivitettiin onnistuneesti',\n    'bookshelf_delete'                 => 'poisti hyllyn',\n    'bookshelf_delete_notification'    => 'Hylly poistettiin onnistuneesti',\n\n    // Revisions\n    'revision_restore' => 'palautti version',\n    'revision_delete' => 'poisti version',\n    'revision_delete_notification' => 'Versio poistettiin onnistuneesti',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" on lisätty suosikkeihisi',\n    'favourite_remove_notification' => '\":name\" on poistettu suosikeistasi',\n\n    // Watching\n    'watch_update_level_notification' => 'Seurannan asetukset päivitetty onnistuneesti',\n\n    // Auth\n    'auth_login' => 'kirjautui sisään',\n    'auth_register' => 'rekisteröityi uudeksi käyttäjäksi',\n    'auth_password_reset_request' => 'pyysi käyttäjän salasanan palautusta',\n    'auth_password_reset_update' => 'palautti käyttäjän salasana',\n    'mfa_setup_method' => 'määritti monivaiheisen tunnistaumisen menetelmän',\n    'mfa_setup_method_notification' => 'Monivaiheisen tunnistautumisen menetelmän määrittäminen onnistui',\n    'mfa_remove_method' => 'poisti monivaiheisen tunnistautumisen menetelmän',\n    'mfa_remove_method_notification' => 'Monivaiheisen tunnistautumisen menetelmä poistettiin onnistuneesti',\n\n    // Settings\n    'settings_update' => 'päivitti asetukset',\n    'settings_update_notification' => 'Asetukset päivitettiin onnistuneesti',\n    'maintenance_action_run' => 'suoritti huoltotoimenpiteen',\n\n    // Webhooks\n    'webhook_create' => 'loi toimintokutsun',\n    'webhook_create_notification' => 'Toimintokutsu luotiin onnistuneesti',\n    'webhook_update' => 'päivitti toimintokutsun',\n    'webhook_update_notification' => 'Toimintokutsu päivitettiin onnistuneesti',\n    'webhook_delete' => 'poisti toimintokutsun',\n    'webhook_delete_notification' => 'Toimintokutsu poistettiin onnistuneesti',\n\n    // Imports\n    'import_create' => 'luotu tuonti',\n    'import_create_notification' => 'Tuontitiedosto ladattiin onnistuneesti',\n    'import_run' => 'päivitetty tuonti',\n    'import_run_notification' => 'Sisällön tuonti onnistui',\n    'import_delete' => 'poistettu tuonti',\n    'import_delete_notification' => 'Tuontitiedosto poistettiin onnistuneesti',\n\n    // Users\n    'user_create' => 'loi käyttäjän',\n    'user_create_notification' => 'Käyttäjä luotiin onnistuneesti',\n    'user_update' => 'päivitti käyttäjän',\n    'user_update_notification' => 'Käyttäjä päivitettiin onnistuneesti',\n    'user_delete' => 'poisti käyttäjän',\n    'user_delete_notification' => 'Käyttäjä poistettiin onnistuneesti',\n\n    // API Tokens\n    'api_token_create' => 'loi API-tunnisteen',\n    'api_token_create_notification' => 'API-tunniste luotiin onnistuneesti',\n    'api_token_update' => 'päivitti API-tunnisteen',\n    'api_token_update_notification' => 'API-tunniste päivitettiin onnistuneesti',\n    'api_token_delete' => 'poisti API-tunnisteen',\n    'api_token_delete_notification' => 'API-tunniste poistettiin onnistuneesti',\n\n    // Roles\n    'role_create' => 'loi roolin',\n    'role_create_notification' => 'Rooli luotiin onnistuneesti',\n    'role_update' => 'päivitti roolin',\n    'role_update_notification' => 'Rooli päivitettiin onnistuneesti',\n    'role_delete' => 'poisti roolin',\n    'role_delete_notification' => 'Rooli poistettiin onnistuneesti',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'tyhjensi roskakorin',\n    'recycle_bin_restore' => 'palautti roskakorista',\n    'recycle_bin_destroy' => 'poisti roskakorista',\n\n    // Comments\n    'commented_on'                => 'kommentoi',\n    'comment_create'              => 'lisäsi kommentin',\n    'comment_update'              => 'päivitti kommentin',\n    'comment_delete'              => 'poisti kommentin',\n\n    // Sort Rules\n    'sort_rule_create' => 'luotu lajittelusääntö',\n    'sort_rule_create_notification' => 'Lajittelusääntö luotiin onnistuneesti',\n    'sort_rule_update' => 'päivitetty lajittelusääntö',\n    'sort_rule_update_notification' => 'Sort rule successfully updated',\n    'sort_rule_delete' => 'poistettu lajittelusääntö',\n    'sort_rule_delete_notification' => 'Lajittelusääntö poistettiin onnistuneesti',\n\n    // Other\n    'permissions_update'          => 'päivitti käyttöoikeudet',\n];\n"
  },
  {
    "path": "lang/fi/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Annettuja käyttäjätietoja ei löydy.',\n    'throttle' => 'Liikaa kirjautumisyrityksiä. Yritä uudelleen :seconds sekunnin päästä.',\n\n    // Login & Register\n    'sign_up' => 'Rekisteröidy',\n    'log_in' => 'Kirjaudu sisään',\n    'log_in_with' => 'Kirjaudu sisään palvelulla :socialDriver',\n    'sign_up_with' => 'Rekisteröidy palvelulla :socialDriver',\n    'logout' => 'Kirjaudu ulos',\n\n    'name' => 'Nimi',\n    'username' => 'Käyttäjätunnus',\n    'email' => 'Sähköposti',\n    'password' => 'Salasana',\n    'password_confirm' => 'Vahvista salasana',\n    'password_hint' => 'Tulee olla vähintään 8 merkkiä',\n    'forgot_password' => 'Unohditko salasanan?',\n    'remember_me' => 'Muista minut',\n    'ldap_email_hint' => 'Ole hyvä ja anna käyttäjätilin sähköpostiosoite.',\n    'create_account' => 'Luo käyttäjätili',\n    'already_have_account' => 'Onko sinulla jo käyttäjätili?',\n    'dont_have_account' => 'Eikö sinulla ole käyttäjätiliä?',\n    'social_login' => 'Kirjaudu sosiaalisen median käyttäjätilillä',\n    'social_registration' => 'Rekisteröidy sosiaalisen median käyttäjätilillä',\n    'social_registration_text' => 'Rekisteröidy ja kirjaudu sisään käyttämällä toista palvelua.',\n\n    'register_thanks' => 'Kiitos rekisteröitymisestä!',\n    'register_confirm' => 'Tarkista sähköpostisi ja paina vahvistuspainiketta päästäksesi sovellukseen :appName.',\n    'registrations_disabled' => 'Rekisteröityminen on tällä hetkellä pois käytöstä',\n    'registration_email_domain_invalid' => 'Tämän sähköpostiosoitteen verkkotunnuksella ei ole pääsyä tähän sovellukseen',\n    'register_success' => 'Kiitos liittymisestä! Olet nyt rekisteröitynyt ja kirjautunut sisään.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Kirjautumisyritys',\n    'auto_init_starting_desc' => 'Otamme yhteyttä tunnistautumisjärjestelmääsi aloittaaksemme kirjautumisprosessin. Jos 5 sekunnin jälkeen ei tapahdu mitään, voit yrittää klikata alla olevaa linkkiä.',\n    'auto_init_start_link' => 'Jatka tunnistautumisen avulla',\n\n    // Password Reset\n    'reset_password' => 'Palauta salasana',\n    'reset_password_send_instructions' => 'Syötä sähköpostiosoitteesi alla olevaan kenttään, niin sinulle lähetetään sähköpostiviesti, jossa on salasanan palautuslinkki.',\n    'reset_password_send_button' => 'Lähetä palautuslinkki',\n    'reset_password_sent' => 'Salasanan palautuslinkki lähetetään osoitteeseen :email, jos kyseinen sähköpostiosoite löytyy järjestelmästä.',\n    'reset_password_success' => 'Salasanasi on onnistuneesti palautettu.',\n    'email_reset_subject' => 'Palauta salasanasi sivustolle :appName',\n    'email_reset_text' => 'Saat tämän sähköpostiviestin, koska saimme käyttäjätiliäsi koskevan salasanan palautuspyynnön.',\n    'email_reset_not_requested' => 'Jos et ole pyytänyt salasanan palauttamista, mitään toimenpiteitä ei tarvita.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Vahvista sähköpostisi sovelluksessa :appName',\n    'email_confirm_greeting' => 'Kiitos liittymisestä sovellukseen :appName!',\n    'email_confirm_text' => 'Vahvista sähköpostiosoitteesi klikkaamalla alla olevaa painiketta:',\n    'email_confirm_action' => 'Vahvista sähköpostiosoite',\n    'email_confirm_send_error' => 'Sähköpostivahvistusta vaaditaan, mutta järjestelmä ei pystynyt lähettämään sähköpostia. Ota yhteyttä ylläpitäjään varmistaaksesi, että sähköpostiasetukset on määritetty oikein.',\n    'email_confirm_success' => 'Sähköpostisi on vahvistettu! Sinun pitäisi nyt pystyä kirjautumaan sisään tällä sähköpostiosoitteella.',\n    'email_confirm_resent' => 'Vahvistussähköposti on lähetetty uudelleen, tarkista saapuneet sähköpostisi.',\n    'email_confirm_thanks' => 'Kiitos vahvistuksesta!',\n    'email_confirm_thanks_desc' => 'Odota hetki, kun vahvistuksesi käsitellään. Jos sinua ei ohjata uudelleen 3 sekunnin kuluttua, paina alla olevaa \"Jatka\"-linkkiä.',\n\n    'email_not_confirmed' => 'Sähköpostiosoitetta ei ole vahvistettu',\n    'email_not_confirmed_text' => 'Sähköpostiosoitettasi ei ole vielä vahvistettu.',\n    'email_not_confirmed_click_link' => 'Klikkaa rekisteröitymisen jälkeen saapuneessa sähköpostissa olevaa vahvistuslinkkiä.',\n    'email_not_confirmed_resend' => 'Jos et löydä sähköpostia, voit lähettää sen uudelleen alla olevalla lomakkeella.',\n    'email_not_confirmed_resend_button' => 'Lähetä vahvistusviesti uudelleen',\n\n    // User Invite\n    'user_invite_email_subject' => 'Sinut on kutsuttu liittymään sivustoon :appName!',\n    'user_invite_email_greeting' => 'Sinulle on luotu käyttäjätili sivustolla :appName.',\n    'user_invite_email_text' => 'Klikkaa alla olevaa painiketta asettaaksesi tilin salasanan ja saadaksesi pääsyn:',\n    'user_invite_email_action' => 'Aseta käyttäjätilin salasana',\n    'user_invite_page_welcome' => 'Tervetuloa sivustolle :appName!',\n    'user_invite_page_text' => 'Viimeistelläksesi käyttäjätilisi ja saadaksesi pääsyn sinun on asetettava salasana, jolla kirjaudut jatkossa sivustolle :appName.',\n    'user_invite_page_confirm_button' => 'Vahvista salasana',\n    'user_invite_success_login' => 'Salasana asetettu, sinun pitäisi nyt pystyä kirjautumaan sivustolle :appName käyttämällä antamaasi salasanaa!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Määritä monivaiheinen tunnistautuminen',\n    'mfa_setup_desc' => 'Määritä monivaiheinen tunnistautuminen käyttäjätilisi turvallisuuden parantamiseksi.',\n    'mfa_setup_configured' => 'Määritetty',\n    'mfa_setup_reconfigure' => 'Uudelleenmäärittele',\n    'mfa_setup_remove_confirmation' => 'Oletko varma, että haluat poistaa tämän monivaiheisen tunnistautumisen menetelmän?',\n    'mfa_setup_action' => 'Asetukset',\n    'mfa_backup_codes_usage_limit_warning' => 'Sinulla on alle 5 varmistuskoodia jäljellä. Luo ja tallenna uusi sarja ennen kuin koodit loppuvat, jotta käyttäjätilisi ei lukitu.',\n    'mfa_option_totp_title' => 'Mobiilisovellus',\n    'mfa_option_totp_desc' => 'Jos haluat käyttää monivaiheista tunnistautumista, tarvitset mobiilisovelluksen, joka tukee TOTP:tä, kuten Google Authenticator, Authy tai Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Varmistuskoodit',\n    'mfa_option_backup_codes_desc' => 'Generates a set of one-time-use backup codes which you\\'ll enter on login to verify your identity. Make sure to store these in a safe & secure place.',\n    'mfa_gen_confirm_and_enable' => 'Vahvista ja ota käyttöön',\n    'mfa_gen_backup_codes_title' => 'Varmistuskoodien asetukset',\n    'mfa_gen_backup_codes_desc' => 'Säilytä alla oleva luettelo koodeista turvallisessa paikassa. Kun käytät järjestelmää, voit käyttää yhtä koodeista toisena tunnistautumistapana.',\n    'mfa_gen_backup_codes_download' => 'Lataa koodit',\n    'mfa_gen_backup_codes_usage_warning' => 'Jokainen koodi voidaan käyttää vain kerran',\n    'mfa_gen_totp_title' => 'Mobiilisovelluksen asetukset',\n    'mfa_gen_totp_desc' => 'Jos haluat käyttää monivaiheista tunnistautumista, tarvitset mobiilisovelluksen, joka tukee TOTP: tä, kuten Google Authenticator, Authy tai Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Skannaa alla oleva QR-koodi haluamallasi todennussovelluksella päästäksesi alkuun.',\n    'mfa_gen_totp_verify_setup' => 'Vahvista asetukset',\n    'mfa_gen_totp_verify_setup_desc' => 'Vahvista, että kaikki toimii syöttämällä tunnistautumissovelluksessa luotu koodi alla olevaan kenttään:',\n    'mfa_gen_totp_provide_code_here' => 'Anna sovelluksen luoma koodi',\n    'mfa_verify_access' => 'Vahvista pääsy',\n    'mfa_verify_access_desc' => 'Käyttäjätilisi vaatii kirjautumiseen monivaiheisen tunnistautumisen. Vahvista kirjautuminen jollakin määrittelemälläsi menetelmällä jatkaaksesi.',\n    'mfa_verify_no_methods' => 'Ei määriteltyjä Menetelmiä',\n    'mfa_verify_no_methods_desc' => 'Käyttäjätilillesi ei löytynyt monivaiheisen tunnistautumisen menetelmiä. Kirjautumiseen vaaditaan vähintään yksi menetelmä.',\n    'mfa_verify_use_totp' => 'Vahvista käyttämällä mobiilisovellusta',\n    'mfa_verify_use_backup_codes' => 'Vahvista käyttämällä varmistuskoodia',\n    'mfa_verify_backup_code' => 'Varmistuskoodi',\n    'mfa_verify_backup_code_desc' => 'Syötä yksi jäljellä olevista varmistukoodeistasi:',\n    'mfa_verify_backup_code_enter_here' => 'Syötä varmistuskoodi',\n    'mfa_verify_totp_desc' => 'Anna mobiilisovelluksella luotu koodi alle:',\n    'mfa_setup_login_notification' => 'Monivaiheisen tunnistautumisen menetelmä määritetty. Kirjaudu nyt uudelleen käyttämällä määritettyä menetelmää.',\n];\n"
  },
  {
    "path": "lang/fi/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Peruuta',\n    'close' => 'Sulje',\n    'confirm' => 'Vahvista',\n    'back' => 'Takaisin',\n    'save' => 'Tallenna',\n    'continue' => 'Jatka',\n    'select' => 'Valitse',\n    'toggle_all' => 'Vaihda kaikki',\n    'more' => 'Lisää',\n\n    // Form Labels\n    'name' => 'Nimi',\n    'description' => 'Kuvaus',\n    'role' => 'Rooli',\n    'cover_image' => 'Kansikuva',\n    'cover_image_description' => 'Kuvan tulee olla noin 440x250 pikselin kokoinen. Kuvan koko ja rajaus voi vaihdella käyttötilanteesta riippuen.',\n\n    // Actions\n    'actions' => 'Toiminnot',\n    'view' => 'Näytä',\n    'view_all' => 'Näytä kaikki',\n    'new' => 'Uusi',\n    'create' => 'Luo',\n    'update' => 'Päivitä',\n    'edit' => 'Muokkaa',\n    'archive' => 'Archive',\n    'unarchive' => 'Un-Archive',\n    'sort' => 'Järjestä',\n    'move' => 'Siirrä',\n    'copy' => 'Kopioi',\n    'reply' => 'Vastaa',\n    'delete' => 'Poista',\n    'delete_confirm' => 'Vahvista poistaminen',\n    'search' => 'Hae',\n    'search_clear' => 'Tyhjennä haku',\n    'reset' => 'Palauta',\n    'remove' => 'Poista',\n    'add' => 'Lisää',\n    'configure' => 'Määritä',\n    'manage' => 'Hallinnoi',\n    'fullscreen' => 'Koko näyttö',\n    'favourite' => 'Suosikki',\n    'unfavourite' => 'Poista suosikki',\n    'next' => 'Seuraava',\n    'previous' => 'Edellinen',\n    'filter_active' => 'Aktiivinen suodatin:',\n    'filter_clear' => 'Tyhjennä suodatin',\n    'download' => 'Lataa',\n    'open_in_tab' => 'Avaa välilehdessä',\n    'open' => 'Avaa',\n\n    // Sort Options\n    'sort_options' => 'Järjestyksen asetukset',\n    'sort_direction_toggle' => 'Järjestyssuunnan vaihto',\n    'sort_ascending' => 'Järjestä nousevasti',\n    'sort_descending' => 'Järjestä laskevasti',\n    'sort_name' => 'Nimi',\n    'sort_default' => 'Oletus',\n    'sort_created_at' => 'Luontipäiväys',\n    'sort_updated_at' => 'Päivityksen päiväys',\n\n    // Misc\n    'deleted_user' => 'Poistettu käyttäjä',\n    'no_activity' => 'Ei näytettävää toimintaa',\n    'no_items' => 'Ei kohteita saatavilla',\n    'back_to_top' => 'Palaa alkuun',\n    'skip_to_main_content' => 'Siirry pääsisältöön',\n    'toggle_details' => 'Näytä tiedot',\n    'toggle_thumbnails' => 'Näytä pienoiskuvat',\n    'details' => 'Tiedot',\n    'grid_view' => 'Ruudukkonäkymä',\n    'list_view' => 'Luettelonäkymä',\n    'default' => 'Oletus',\n    'breadcrumb' => 'Navigointipolku',\n    'status' => 'Tila',\n    'status_active' => 'Aktiivinen',\n    'status_inactive' => 'Ei aktiivinen',\n    'never' => 'Ei koskaan',\n    'none' => 'Ei mitään',\n\n    // Header\n    'homepage' => 'Kotisivu',\n    'header_menu_expand' => 'Laajenna päävalikko',\n    'profile_menu' => 'Profiilivalikko',\n    'view_profile' => 'Näytä profiili',\n    'edit_profile' => 'Muokkaa profiilia',\n    'dark_mode' => 'Tumma tila',\n    'light_mode' => 'Vaalea tila',\n    'global_search' => 'Yleishaku',\n\n    // Layout tabs\n    'tab_info' => 'Info',\n    'tab_info_label' => 'Välilehti: Näytä toissijaiset tiedot',\n    'tab_content' => 'Sisältö',\n    'tab_content_label' => 'Välilehti: Näytä ensisijainen sisältö',\n\n    // Email Content\n    'email_action_help' => 'Jos sinulla on ongelmia \":actionText\"-painikkeen klikkaamisessa, kopioi ja liitä alla oleva URL-osoite selaimeesi:',\n    'email_rights' => 'Kaikki oikeudet pidätetään',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Tietosuojaseloste',\n    'terms_of_service' => 'Palvelun käyttöehdot',\n\n    // OpenSearch\n    'opensearch_description' => 'Search :appName',\n];\n"
  },
  {
    "path": "lang/fi/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Kuvan valinta',\n    'image_list' => 'Kuvalista',\n    'image_details' => 'Kuvan tiedot',\n    'image_upload' => 'Lataa kuva',\n    'image_intro' => 'Täällä voit valita ja hallita kuvia, jotka on aiemmin ladattu järjestelmään.',\n    'image_intro_upload' => 'Lataa uusi kuva vetämällä kuvatiedosto tähän ikkunaan tai käyttämällä yllä olevaa \"Lataa kuva\" -painiketta.',\n    'image_all' => 'Kaikki',\n    'image_all_title' => 'Näytä kaikki kuvat',\n    'image_book_title' => 'Näytä tähän kirjaan ladatut kuvat',\n    'image_page_title' => 'Näytä tähän sivuun ladatut kuvat',\n    'image_search_hint' => 'Hae kuvan nimellä',\n    'image_uploaded' => 'Ladattu :uploadedDate',\n    'image_uploaded_by' => 'Lataaja :userName',\n    'image_uploaded_to' => 'Ladattu sivulle :pageLink',\n    'image_updated' => 'Päivitetty :updateDate',\n    'image_load_more' => 'Lataa lisää',\n    'image_image_name' => 'Kuvan nimi',\n    'image_delete_used' => 'Tätä kuvaa käytetään alla mainituilla sivuilla.',\n    'image_delete_confirm_text' => 'Haluatko varmasti poistaa tämän kuvan?',\n    'image_select_image' => 'Valitse kuva',\n    'image_dropzone' => 'Pudota kuvat tai lataa ne klikkaamalla tästä',\n    'image_dropzone_drop' => 'Pudota kuvat tähän ladattavaksi',\n    'images_deleted' => 'Kuvat poistettu',\n    'image_preview' => 'Kuvan esikatselu',\n    'image_upload_success' => 'Kuva ladattiin onnistuneesti',\n    'image_update_success' => 'Kuvan tiedot päivitettiin onnistuneesti',\n    'image_delete_success' => 'Kuva poistettiin onnistuneesti',\n    'image_replace' => 'Vaihda kuva',\n    'image_replace_success' => 'Kuvatiedosto päivitettiin onnistuneesti',\n    'image_rebuild_thumbs' => 'Luo kokovaihtoehdot uudelleen',\n    'image_rebuild_thumbs_success' => 'Kuvan kokovaihtoehdot luotiin onnistuneesti uudelleen!',\n\n    // Code Editor\n    'code_editor' => 'Muokkaa koodia',\n    'code_language' => 'Koodin kieli',\n    'code_content' => 'Koodin sisältö',\n    'code_session_history' => 'Istuntohistoria',\n    'code_save' => 'Tallenna koodi',\n];\n"
  },
  {
    "path": "lang/fi/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Yleinen',\n    'advanced' => 'Lisäasetukset',\n    'none' => 'Ei mitään',\n    'cancel' => 'Peruuta',\n    'save' => 'Tallenna',\n    'close' => 'Sulje',\n    'apply' => 'Apply',\n    'undo' => 'Kumoa',\n    'redo' => 'Tee uudelleen',\n    'left' => 'Vasen',\n    'center' => 'Keskellä',\n    'right' => 'Oikea',\n    'top' => 'Ylhäällä',\n    'middle' => 'Keskellä',\n    'bottom' => 'Alhaalla',\n    'width' => 'Leveys',\n    'height' => 'Korkeus',\n    'More' => 'Enemmän',\n    'select' => 'Valitse...',\n\n    // Toolbar\n    'formats' => 'Formaatit',\n    'header_large' => 'Iso otsikko',\n    'header_medium' => 'Keskikokoinen otsikko',\n    'header_small' => 'Pieni otsikko',\n    'header_tiny' => 'Hyvin pieni otsikko',\n    'paragraph' => 'Kappale',\n    'blockquote' => 'Sitaatti',\n    'inline_code' => 'Koodi',\n    'callouts' => 'Huomautukset',\n    'callout_information' => 'Tietoja',\n    'callout_success' => 'Onnistuminen',\n    'callout_warning' => 'Varoitus',\n    'callout_danger' => 'Vaara',\n    'bold' => 'Lihavointi',\n    'italic' => 'Kursivointi',\n    'underline' => 'Alleviivaus',\n    'strikethrough' => 'Yliviivaus',\n    'superscript' => 'Yläindeksi',\n    'subscript' => 'Alaindeksi',\n    'text_color' => 'Tekstin väri',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Mukautettu väri',\n    'remove_color' => 'Poista väri',\n    'background_color' => 'Taustaväri',\n    'align_left' => 'Tasaa vasemmalle',\n    'align_center' => 'Tasaa keskelle',\n    'align_right' => 'Tasaa oikealle',\n    'align_justify' => 'Tasaa molemmat reunat',\n    'list_bullet' => 'Lajittelematon lista',\n    'list_numbered' => 'Numeroitu lista',\n    'list_task' => 'Tehtävälista',\n    'indent_increase' => 'Lisää sisennystä',\n    'indent_decrease' => 'Vähennä sisennystä',\n    'table' => 'Taulukko',\n    'insert_image' => 'Lisää kuva',\n    'insert_image_title' => 'Lisää/muokkaa kuvaa',\n    'insert_link' => 'Lisää/muokkaa linkkiä',\n    'insert_link_title' => 'Lisää/muokkaa linkkiä',\n    'insert_horizontal_line' => 'Lisää vaakaviiva',\n    'insert_code_block' => 'Lisää koodilohko',\n    'edit_code_block' => 'Muokkaa koodilohkoa',\n    'insert_drawing' => 'Lisää/muokkaa piirrosta',\n    'drawing_manager' => 'Piirroksen hallinta',\n    'insert_media' => 'Lisää/muokkaa mediaa',\n    'insert_media_title' => 'Lisää/muokkaa mediaa',\n    'clear_formatting' => 'Poista muotoilu',\n    'source_code' => 'Lähdekoodi',\n    'source_code_title' => 'Lähdekoodi',\n    'fullscreen' => 'Koko näyttö',\n    'image_options' => 'Kuvan asetukset',\n\n    // Tables\n    'table_properties' => 'Taulukon ominaisuudet',\n    'table_properties_title' => 'Taulukon ominaisuudet',\n    'delete_table' => 'Poista taulukko',\n    'table_clear_formatting' => 'Poista taulukon muotoilut',\n    'resize_to_contents' => 'Sovita koko sisältöön',\n    'row_header' => 'Rivin otsikko',\n    'insert_row_before' => 'Lisää rivi ennen',\n    'insert_row_after' => 'Lisää rivi jälkeen',\n    'delete_row' => 'Poista rivi',\n    'insert_column_before' => 'Liitä sarake ennen',\n    'insert_column_after' => 'Lisää sarake jälkeen',\n    'delete_column' => 'Poista sarake',\n    'table_cell' => 'Solu',\n    'table_row' => 'Rivi',\n    'table_column' => 'Sarake',\n    'cell_properties' => 'Solun ominaisuudet',\n    'cell_properties_title' => 'Solun ominaisuudet',\n    'cell_type' => 'Solun tyyppi',\n    'cell_type_cell' => 'Solu',\n    'cell_scope' => 'Laajuus',\n    'cell_type_header' => 'Otsikkosolu',\n    'merge_cells' => 'Yhdistä solut',\n    'split_cell' => 'Jaa solu',\n    'table_row_group' => 'Riviryhmä',\n    'table_column_group' => 'Sarakeryhmä',\n    'horizontal_align' => 'Vaaka-asettelu',\n    'vertical_align' => 'Pystyasettelu',\n    'border_width' => 'Reunuksen leveys',\n    'border_style' => 'Reunuksen tyyli',\n    'border_color' => 'Reunuksen väri',\n    'row_properties' => 'Rivin ominaisuudet',\n    'row_properties_title' => 'Rivin ominaisuudet',\n    'cut_row' => 'Leikkaa rivi',\n    'copy_row' => 'Kopioi rivi',\n    'paste_row_before' => 'Liitä rivi ennen',\n    'paste_row_after' => 'Liitä rivi jälkeen',\n    'row_type' => 'Rivin tyyppi',\n    'row_type_header' => 'Ylätunniste',\n    'row_type_body' => 'Sisältö',\n    'row_type_footer' => 'Alatunniste',\n    'alignment' => 'Tasaus',\n    'cut_column' => 'Leikkaa sarake',\n    'copy_column' => 'Kopioi sarake',\n    'paste_column_before' => 'Liitä sarake ennen',\n    'paste_column_after' => 'Liitä sarake jälkeen',\n    'cell_padding' => 'Solun reunus',\n    'cell_spacing' => 'Solun välistys',\n    'caption' => 'Otsikko',\n    'show_caption' => 'Näytä otsikko',\n    'constrain' => 'Rajaa mittasuhteet',\n    'cell_border_solid' => 'Kiinteä',\n    'cell_border_dotted' => 'Pisteviiva',\n    'cell_border_dashed' => 'Katkoviiva',\n    'cell_border_double' => 'Kaksinkertainen viiva',\n    'cell_border_groove' => 'Ura',\n    'cell_border_ridge' => 'Harjanne',\n    'cell_border_inset' => 'Sisenevä',\n    'cell_border_outset' => 'Ulkoneva',\n    'cell_border_none' => 'Ei mitään',\n    'cell_border_hidden' => 'Piilotettu',\n\n    // Images, links, details/summary & embed\n    'source' => 'Lähde',\n    'alt_desc' => 'Vaihtoehtoinen kuvaus',\n    'embed' => 'Upota',\n    'paste_embed' => 'Liitä upotuskoodisi alle:',\n    'url' => 'URL-osoite',\n    'text_to_display' => 'Näytettävä teksti',\n    'title' => 'Otsikko',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Avaa linkki',\n    'open_link_in' => 'Avaa linkki...',\n    'open_link_current' => 'Nykyinen ikkuna',\n    'open_link_new' => 'Uusi ikkuna',\n    'remove_link' => 'Poista linkki',\n    'insert_collapsible' => 'Lisää kokoontaitettava lohko',\n    'collapsible_unwrap' => 'Poista ympäröivä lohko',\n    'edit_label' => 'Muokkaa nimikettä',\n    'toggle_open_closed' => 'Auki/kiinni',\n    'collapsible_edit' => 'Muokkaa kokoontaitettavaa lohkoa',\n    'toggle_label' => 'Näytä nimike',\n\n    // About view\n    'about' => 'Tietoja editorista',\n    'about_title' => 'Tietoja WYSIWYG-editorista',\n    'editor_license' => 'Editorin lisenssi ja tekijänoikeus',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'Tämä editori on rakennettu käyttäen sovellusta :tinyLink, joka on MIT-lisenssin alainen.',\n    'editor_tiny_license_link' => 'TinyMCE-editorin tekijänoikeus- ja lisenssitiedot löytyvät täältä.',\n    'save_continue' => 'Tallenna sivu ja jatka',\n    'callouts_cycle' => '(Pidä painettuna valitaksesi tyyppien välillä)',\n    'link_selector' => 'Linkki sisältöön',\n    'shortcuts' => 'Pikanäppäimet',\n    'shortcut' => 'Pikanäppäin',\n    'shortcuts_intro' => 'Editorissa on saatavilla seuraavat pikanäppäimet:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Kuvaus',\n];\n"
  },
  {
    "path": "lang/fi/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Viimeksi luodut',\n    'recently_created_pages' => 'Viimeksi luodut sivut',\n    'recently_updated_pages' => 'Viimeksi päivitetyt sivut',\n    'recently_created_chapters' => 'Viimeksi luodut luvut',\n    'recently_created_books' => 'Viimeksi luodut kirjat',\n    'recently_created_shelves' => 'Viimeksi luodut hyllyt',\n    'recently_update' => 'Viimeksi päivitetyt',\n    'recently_viewed' => 'Viimeksi katsotut',\n    'recent_activity' => 'Viimeaikainen toiminta',\n    'create_now' => 'Luo uusi',\n    'revisions' => 'Versiot',\n    'meta_revision' => 'Versio #:revisionCount',\n    'meta_created' => 'Luotu :timeLength',\n    'meta_created_name' => 'Luotu :timeLength käyttäjän :user toimesta',\n    'meta_updated' => 'Päivitetty :timeLength',\n    'meta_updated_name' => 'Päivitetty :timeLength käyttäjän :user toimesta',\n    'meta_owned_name' => 'Omistaja :user',\n    'meta_reference_count' => 'Viittaa :count kohteeseen|Viittaa :count kohteeseen',\n    'entity_select' => 'Kohteen valinta',\n    'entity_select_lack_permission' => 'Sinulla ei ole tarvittavia oikeuksia tämän kohteen valitsemiseen',\n    'images' => 'Kuvat',\n    'my_recent_drafts' => 'Viimeisimmät luonnokseni',\n    'my_recently_viewed' => 'Omat viimeksi katsotut',\n    'my_most_viewed_favourites' => 'Omat katsotuimmat suosikit',\n    'my_favourites' => 'Omat suosikit',\n    'no_pages_viewed' => 'Et ole katsonut yhtään sivua',\n    'no_pages_recently_created' => 'Yhtään sivua ei ole luotu äskettäin',\n    'no_pages_recently_updated' => 'Yhtään sivua ei ole päivitetty äskettäin',\n    'export' => 'Vie',\n    'export_html' => 'HTML-tiedosto',\n    'export_pdf' => 'PDF-tiedosto',\n    'export_text' => 'Tekstitiedosto',\n    'export_md' => 'Markdown-tiedosto',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Default Page Template',\n    'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',\n    'default_template_select' => 'Select a template page',\n    'import' => 'Import',\n    'import_validate' => 'Validate Import',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'Select ZIP file to upload',\n    'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',\n    'import_pending' => 'Pending Imports',\n    'import_pending_none' => 'No imports have been started.',\n    'import_continue' => 'Continue Import',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Run Import',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Uploaded by',\n    'import_location' => 'Import Location',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'Import Errors',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Käyttöoikeudet',\n    'permissions_desc' => 'Määritä tässä käyttöoikeudet ohittaaksesi käyttäjäroolien antamat oletusoikeudet.',\n    'permissions_book_cascade' => 'Kirjoille määritetyt käyttöoikeudet siirtyvät automaattisesti lukuihin ja -sivuihin, ellei niille ole määritelty omia käyttöoikeuksia.',\n    'permissions_chapter_cascade' => 'Luvuille määritetyt käyttöoikeudet siirtyvät automaattisesti sivuille, ellei niille ole määritelty omia käyttöoikeuksia.',\n    'permissions_save' => 'Tallenna käyttöoikeudet',\n    'permissions_owner' => 'Omistaja',\n    'permissions_role_everyone_else' => 'Kaikki muut',\n    'permissions_role_everyone_else_desc' => 'Aseta käyttöoikeudet kaikille rooleille, joita ei ole erikseen ohitettu.',\n    'permissions_role_override' => 'Ohita roolin käyttöoikeudet',\n    'permissions_inherit_defaults' => 'Peritään oletusarvot',\n\n    // Search\n    'search_results' => 'Hakutulokset',\n    'search_total_results_found' => 'löytyi :count osuma|löytyi :count osumaa',\n    'search_clear' => 'Tyhjennä haku',\n    'search_no_pages' => 'Haulla ei löytynyt yhtään sivua',\n    'search_for_term' => 'Hae sanaa :term',\n    'search_more' => 'Lisää tuloksia',\n    'search_advanced' => 'Tarkennettu haku',\n    'search_terms' => 'Hakusanat',\n    'search_content_type' => 'Sisältötyyppi',\n    'search_exact_matches' => 'Täsmälliset vastineet',\n    'search_tags' => 'Tunnisteen haut',\n    'search_options' => 'Valinnat',\n    'search_viewed_by_me' => 'Olen katsonut',\n    'search_not_viewed_by_me' => 'En ole katsonut',\n    'search_permissions_set' => 'Käyttöoikeudet asetettu',\n    'search_created_by_me' => 'Minun luomani',\n    'search_updated_by_me' => 'Minun päivittämäni',\n    'search_owned_by_me' => 'Minun omistamani',\n    'search_date_options' => 'Päiväyksen valinnat',\n    'search_updated_before' => 'Päivitetty ennen',\n    'search_updated_after' => 'Päivitetty jälkeen',\n    'search_created_before' => 'Luotu ennen',\n    'search_created_after' => 'Luotu jälkeen',\n    'search_set_date' => 'Aseta päiväys',\n    'search_update' => 'Päivitä haku',\n\n    // Shelves\n    'shelf' => 'Hylly',\n    'shelves' => 'Hyllyt',\n    'x_shelves' => ':count hylly|:count hyllyä',\n    'shelves_empty' => 'Hyllyjä ei ole luotu',\n    'shelves_create' => 'Luo uusi hylly',\n    'shelves_popular' => 'Suositut hyllyt',\n    'shelves_new' => 'Uudet hyllyt',\n    'shelves_new_action' => 'Uusi hylly',\n    'shelves_popular_empty' => 'Suosituimmat hyllyt näkyvät tässä.',\n    'shelves_new_empty' => 'Viimeksi luodut hyllyt näkyvät tässä.',\n    'shelves_save' => 'Tallenna hylly',\n    'shelves_books' => 'Tässä hyllyssä olevat kirjat',\n    'shelves_add_books' => 'Lisää kirjoja tähän hyllyyn',\n    'shelves_drag_books' => 'Vedä alla olevia kirjoja lisätäksesi ne tähän hyllyyn',\n    'shelves_empty_contents' => 'Tälle hyllylle ei ole lisätty kirjoja',\n    'shelves_edit_and_assign' => 'Muokkaa hyllyä lisätäksesi kirjoja',\n    'shelves_edit_named' => 'Muokkaa hyllyä :name',\n    'shelves_edit' => 'Muokkaa hyllyä',\n    'shelves_delete' => 'Poista hylly',\n    'shelves_delete_named' => 'Poista hylly :name',\n    'shelves_delete_explain' => \"Tämä poistaa hyllyn, jonka nimi on ':nimi'. Sisältyviä kirjoja ei poisteta.\",\n    'shelves_delete_confirmation' => 'Haluatko varmasti poistaa tämän hyllyn?',\n    'shelves_permissions' => 'Hyllyn käyttöoikeudet',\n    'shelves_permissions_updated' => 'Hyllyn käyttöoikeudet päivitetty',\n    'shelves_permissions_active' => 'Hyllyn käyttöoikeudet käytössä',\n    'shelves_permissions_cascade_warning' => 'Hyllyjen käyttöoikeudet eivät siirry automaattisesti kirjoihin. Tämä johtuu siitä, että kirja voi olla useammassa hyllyssä. Käyttöoikeudet voidaan kuitenkin kopioida kaikkiin hyllyn kirjoihin käyttämällä alla olevaa valintaa.',\n    'shelves_permissions_create' => 'Hyllyjen luontioikeuksia käytetään vain kopioidessa oikeuksia hyllyn kirjoihin alla olevan toiminnon avulla. Ne eivät vaikuta mahdollisuuteen luoda kirjoja.',\n    'shelves_copy_permissions_to_books' => 'Kopioi käyttöoikeudet kirjoihin',\n    'shelves_copy_permissions' => 'Kopioi käyttöoikeudet',\n    'shelves_copy_permissions_explain' => 'Tämä valinta siirtää hyllyn nykyiset käyttöoikeusasetukset kaikkiin hyllyssä oleviin kirjoihin. Varmista ennen aktivointia, että kaikki tämän hyllyn käyttöoikeuksiin tehdyt muutokset on tallennettu.',\n    'shelves_copy_permission_success' => 'Hyllyn käyttöoikeudet kopioitu :count kirjaan',\n\n    // Books\n    'book' => 'Kirja',\n    'books' => 'Kirjat',\n    'x_books' => ':count kirja|:count kirjaa',\n    'books_empty' => 'Kirjoja ei ole luotu',\n    'books_popular' => 'Suositut kirjat',\n    'books_recent' => 'Viimeisimmät kirjat',\n    'books_new' => 'Uudet kirjat',\n    'books_new_action' => 'Uusi kirja',\n    'books_popular_empty' => 'Suosituimmat kirjat näkyvät tässä.',\n    'books_new_empty' => 'Viimeksi luodut kirjat näkyvät tässä.',\n    'books_create' => 'Luo uusi kirja',\n    'books_delete' => 'Poista kirja',\n    'books_delete_named' => 'Poista kirja :bookName',\n    'books_delete_explain' => 'Tämä poistaa kirjan, jonka nimi on \\':bookName\\'. Kaikki sivut ja luvut poistetaan.',\n    'books_delete_confirmation' => 'Haluatko varmasti poistaa tämän kirjan?',\n    'books_edit' => 'Muokkaa kirjaa',\n    'books_edit_named' => 'Muokkaa kirjaa :bookName',\n    'books_form_book_name' => 'Kirjan nimi',\n    'books_save' => 'Tallenna kirja',\n    'books_permissions' => 'Kirjan käyttöoikeudet',\n    'books_permissions_updated' => 'Kirjan käyttöoikeudet päivitetty',\n    'books_empty_contents' => 'Tähän kirjaan ei ole luotu sivuja tai lukuja.',\n    'books_empty_create_page' => 'Luo uusi sivu',\n    'books_empty_sort_current_book' => 'Järjestä nykyistä kirjaa',\n    'books_empty_add_chapter' => 'Lisää luku',\n    'books_permissions_active' => 'Kirjan käyttöoikeudet käytössä',\n    'books_search_this' => 'Hae tästä kirjasta',\n    'books_navigation' => 'Kirjan navigaatio',\n    'books_sort' => 'Järjestä kirjan sisältö',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Järjestä kirja :bookName',\n    'books_sort_name' => 'Järjestä nimen mukaan',\n    'books_sort_created' => 'Järjestä luontipäiväyksen mukaan',\n    'books_sort_updated' => 'Järjestä päivityksen päiväyksen mukaan',\n    'books_sort_chapters_first' => 'Luvut ensin',\n    'books_sort_chapters_last' => 'Luvut viimeisenä',\n    'books_sort_show_other' => 'Näytä muita kirjoja',\n    'books_sort_save' => 'Tallenna uusi järjestys',\n    'books_sort_show_other_desc' => 'Voit lisätä tähän muita kirjoja järjestämistä varten ja mahdollistaa sujuvan kirjojen välisen uudelleenjärjestelyn.',\n    'books_sort_move_up' => 'Siirrä ylös',\n    'books_sort_move_down' => 'Siirrä alas',\n    'books_sort_move_prev_book' => 'Siirrä edelliseen kirjaan',\n    'books_sort_move_next_book' => 'Siirrä seuraavaan kirjaan',\n    'books_sort_move_prev_chapter' => 'Siirrä edelliseen lukuun',\n    'books_sort_move_next_chapter' => 'Siirrä seuraavaan lukuun',\n    'books_sort_move_book_start' => 'Siirrä kirjan alkuun',\n    'books_sort_move_book_end' => 'Siirrä kirjan loppuun',\n    'books_sort_move_before_chapter' => 'Siirrä luvun edelle',\n    'books_sort_move_after_chapter' => 'Siirrä luvun jälkeen',\n    'books_copy' => 'Kopioi kirja',\n    'books_copy_success' => 'Kirja kopioitiin onnistuneesti',\n\n    // Chapters\n    'chapter' => 'Luku',\n    'chapters' => 'Luvut',\n    'x_chapters' => ':count luku|:count lukua',\n    'chapters_popular' => 'Suositut luvut',\n    'chapters_new' => 'Uusi luku',\n    'chapters_create' => 'Luo uusi luku',\n    'chapters_delete' => 'Poista luku',\n    'chapters_delete_named' => 'Poista luku :chapterName',\n    'chapters_delete_explain' => 'Tämä poistaa luvun nimeltä \\':chapterName\\'. Myös kaikki luvun sisällä olevat sivut poistetaan.',\n    'chapters_delete_confirm' => 'Haluatko varmasti poistaa tämän luvun?',\n    'chapters_edit' => 'Muokkaa lukua',\n    'chapters_edit_named' => 'Muokkaa lukua :chapterName',\n    'chapters_save' => 'Tallenna luku',\n    'chapters_move' => 'Siirrä luku',\n    'chapters_move_named' => 'Siirrä lukua :chapterName',\n    'chapters_copy' => 'Kopioi luku',\n    'chapters_copy_success' => 'Luku kopioitiin onnistuneesti',\n    'chapters_permissions' => 'Luvun käyttöoikeudet',\n    'chapters_empty' => 'Tässä luvussa ei ole tällä hetkellä sivuja.',\n    'chapters_permissions_active' => 'Luvun käyttöoikeudet käytössä',\n    'chapters_permissions_success' => 'Luvun käyttöoikeudet päivitetty',\n    'chapters_search_this' => 'Hae tästä luvusta',\n    'chapter_sort_book' => 'Järjestä kirja',\n\n    // Pages\n    'page' => 'Sivu',\n    'pages' => 'Sivut',\n    'x_pages' => ':count sivu|:count sivua',\n    'pages_popular' => 'Suositut sivut',\n    'pages_new' => 'Uusi sivu',\n    'pages_attachments' => 'Liitteet',\n    'pages_navigation' => 'Sivun navigaatio',\n    'pages_delete' => 'Poista sivu',\n    'pages_delete_named' => 'Poista sivu :pageName',\n    'pages_delete_draft_named' => 'Poista luonnos :pageName',\n    'pages_delete_draft' => 'Poista luonnos',\n    'pages_delete_success' => 'Sivu poistettu',\n    'pages_delete_draft_success' => 'Luonnos poistettu',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'Oletko varma, että haluat poistaa tämän sivun?',\n    'pages_delete_draft_confirm' => 'Haluatko varmasti poistaa tämän luonnoksen?',\n    'pages_editing_named' => 'Muokataan sivua :pageName',\n    'pages_edit_draft_options' => 'Luonnoksen asetukset',\n    'pages_edit_save_draft' => 'Tallenna luonnos',\n    'pages_edit_draft' => 'Muokkaa luonnosta',\n    'pages_editing_draft' => 'Muokataan luonnosta',\n    'pages_editing_page' => 'Muokataan sivua',\n    'pages_edit_draft_save_at' => 'Luonnos tallennettu ',\n    'pages_edit_delete_draft' => 'Poista luonnos',\n    'pages_edit_delete_draft_confirm' => 'Oletko varma, että haluat poistaa luonnoksen muutokset? Kaikki muutokset viimeisimmästä tallennuksesta lähtien häviävät, ja editori päivitetään viimeisimpään tallennettuun sivun versioon.',\n    'pages_edit_discard_draft' => 'Hylkää luonnos',\n    'pages_edit_switch_to_markdown' => 'Vaihda Markdown-editoriin',\n    'pages_edit_switch_to_markdown_clean' => '(Puhdas sisältö)',\n    'pages_edit_switch_to_markdown_stable' => '(Vakaa sisältö)',\n    'pages_edit_switch_to_wysiwyg' => 'Vaihda WYSIWYG-editoriin',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Aseta muutosloki',\n    'pages_edit_enter_changelog_desc' => 'Kirjoita lyhyt kuvaus tekemistäsi muutoksista',\n    'pages_edit_enter_changelog' => 'Syötä muutosloki',\n    'pages_editor_switch_title' => 'Vaihda editoria',\n    'pages_editor_switch_are_you_sure' => 'Haluatko varmasti vaihtaa tämän sivun editorin?',\n    'pages_editor_switch_consider_following' => 'Ota huomioon seuraavat asiat, kun vaihdat editoria:',\n    'pages_editor_switch_consideration_a' => 'Tallennuksen jälkeen kaikki tulevat muokkaajat käyttävät uutta editorivaihtoehtoa, myös käyttäjät, jotka eivät ehkä pysty itse vaihtamaan editoria.',\n    'pages_editor_switch_consideration_b' => 'Tämä voi joissain tapauksissa johtaa yksityiskohtien ja muotoilujen häviämiseen.',\n    'pages_editor_switch_consideration_c' => 'Viimeisimmän tallennuksen jälkeen tehdyt tunniste- tai muutoslokimuutokset eivät säily.',\n    'pages_save' => 'Tallenna sivu',\n    'pages_title' => 'Sivun otsikko',\n    'pages_name' => 'Sivun nimi',\n    'pages_md_editor' => 'Editori',\n    'pages_md_preview' => 'Esikatselu',\n    'pages_md_insert_image' => 'Lisää kuva',\n    'pages_md_insert_link' => 'Lisää linkki',\n    'pages_md_insert_drawing' => 'Lisää piirustus',\n    'pages_md_show_preview' => 'Näytä esikatselu',\n    'pages_md_sync_scroll' => 'Vieritä esikatselua koodin vierityksen mukaan',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Tallentamaton piirustus löytyi',\n    'pages_drawing_unsaved_confirm' => 'Järjestelmä löysi tallentamattoman piirustuksen. Haluatko palauttaa piirustuksen ja jatkaa sen muokkaamista?',\n    'pages_not_in_chapter' => 'Sivu ei kuulu mihinkään lukuun',\n    'pages_move' => 'Siirrä sivu',\n    'pages_copy' => 'Kopioi sivu',\n    'pages_copy_desination' => 'Kopioinnin kohde',\n    'pages_copy_success' => 'Sivu kopioitiin onnistuneesti',\n    'pages_permissions' => 'Sivun käyttöoikeudet',\n    'pages_permissions_success' => 'Sivun käyttöoikeudet päivitetty',\n    'pages_revision' => 'Versio',\n    'pages_revisions' => 'Sivun versiot',\n    'pages_revisions_desc' => 'Alla on kaikki tämän sivun aiemmat versiot. Voit tarkastella, vertailla ja palauttaa vanhoja versioita, jos käyttöoikeudet sallivat. Sivun koko historia ei välttämättä näy kokonaan, sillä järjestelmän asetuksista riippuen vanhat versiot saatetaan poistaa automaattisesti.',\n    'pages_revisions_named' => 'Sivun :pageName versiot',\n    'pages_revision_named' => 'Sivun :pageName versio',\n    'pages_revision_restored_from' => 'Palautettu versiosta #:id; :summary',\n    'pages_revisions_created_by' => 'Luonut',\n    'pages_revisions_date' => 'Version päiväys',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Versionumero',\n    'pages_revisions_numbered' => 'Versio #:id',\n    'pages_revisions_numbered_changes' => 'Version #:id muutokset',\n    'pages_revisions_editor' => 'Editorin tyyppi',\n    'pages_revisions_changelog' => 'Muutoshistoria',\n    'pages_revisions_changes' => 'Muutokset',\n    'pages_revisions_current' => 'Nykyinen versio',\n    'pages_revisions_preview' => 'Esikatselu',\n    'pages_revisions_restore' => 'Palauta',\n    'pages_revisions_none' => 'Tällä sivulla ei ole versioita',\n    'pages_copy_link' => 'Kopioi linkki',\n    'pages_edit_content_link' => 'Siirry osioon editorissa',\n    'pages_pointer_enter_mode' => 'Siirry osion valintatilaan',\n    'pages_pointer_label' => 'Sivun osion valinnat',\n    'pages_pointer_permalink' => 'Sivun osion pysyvä linkki',\n    'pages_pointer_include_tag' => 'Sivun osion viitetunniste',\n    'pages_pointer_toggle_link' => 'Pysyvä linkki, valitse viitetunniste painamalla',\n    'pages_pointer_toggle_include' => 'Viitetunniste, valitse pysyvä linkki painamalla',\n    'pages_permissions_active' => 'Sivun käyttöoikeudet käytössä',\n    'pages_initial_revision' => 'Alkuperäinen julkaisu',\n    'pages_references_update_revision' => 'Sisäisten linkkien automaattinen päivitys',\n    'pages_initial_name' => 'Uusi sivu',\n    'pages_editing_draft_notification' => 'Muokkaat luonnosta, joka on viimeksi tallennettu :timeDiff.',\n    'pages_draft_edited_notification' => 'Tätä sivua on päivitetty myöhemmin. Tämä luonnos suositellaan poistettavaksi.',\n    'pages_draft_page_changed_since_creation' => 'Sivua on päivitetty tämän luonnoksen luomisen jälkeen. On suositeltavaa, että poistat tämän luonnoksen tai huolehdit siitä, ettet korvaa uusia sivun muutoksia.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count käyttäjää muokkaa tätä sivua',\n        'start_b' => ':userName muokkaa tätä sivua',\n        'time_a' => 'sivun viimeisimmän päivityksen jälkeen',\n        'time_b' => 'viimeisen :minCount minuutin aikana',\n        'message' => ':start :time. Huolehdi siitä, että ette ylikirjoita toistenne päivityksiä!',\n    ],\n    'pages_draft_discarded' => 'Luonnos on hylätty! Editoriin on päivitetty sivun nykyinen sisältö',\n    'pages_draft_deleted' => 'Luonnos on poistettu! Editoriin on päivitetty sivun nykyinen sisältö',\n    'pages_specific' => 'Tietty sivu',\n    'pages_is_template' => 'Mallipohja',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Näytä/piilota sivupalkki',\n    'page_tags' => 'Sivun tunnisteet',\n    'chapter_tags' => 'Lukujen tunnisteet',\n    'book_tags' => 'Kirjojen tunnisteet',\n    'shelf_tags' => 'Hyllyjen tunnisteet',\n    'tag' => 'Tunniste',\n    'tags' =>  'Tunnisteet',\n    'tags_index_desc' => 'Järjestelmässä oleva sisältöä voidaan luokitella joustavasti tunnisteiden avulla. Tunnisteilla voi olla sekä avain että arvo. Arvo on valinnainen. Tunnisteella merkittyjä sisältöjä voidaan hakea käyttämällä tunnisteen nimeä ja arvoa.',\n    'tag_name' =>  'Tunnisteen nimi',\n    'tag_value' => 'Tunnisteen arvo (valinnainen)',\n    'tags_explain' => \"Lisää tunnisteita sisällön luokittelua varten. \\n Tunnisteiden arvojen avulla luokittelua voi edelleen tarkentaa.\",\n    'tags_add' => 'Lisää uusi tunniste',\n    'tags_remove' => 'Poista tämä tunniste',\n    'tags_usages' => 'Tunnisteen käyttökerrat',\n    'tags_assigned_pages' => 'Lisätty sivuille',\n    'tags_assigned_chapters' => 'Lisätty lukuihin',\n    'tags_assigned_books' => 'Lisätty kirjoihin',\n    'tags_assigned_shelves' => 'Lisätty hyllyihin',\n    'tags_x_unique_values' => ':count yksilöllistä arvoa',\n    'tags_all_values' => 'Kaikki arvot',\n    'tags_view_tags' => 'Näytä tunnisteita',\n    'tags_view_existing_tags' => 'Näytä käytetyt tunnisteet',\n    'tags_list_empty_hint' => 'Tunnisteet voidaan määrittää editorin sivupalkissa tai kirjan, luvun tai hyllyn tietoja muokattaessa.',\n    'attachments' => 'Liitteet',\n    'attachments_explain' => 'Lataa tiedostoja tai liitä linkkejä, jotka näytetään sivulla. Nämä näkyvät sivun sivupalkissa.',\n    'attachments_explain_instant_save' => 'Tässä tehdyt muutokset tallentuvat välittömästi.',\n    'attachments_upload' => 'Lataa tiedosto',\n    'attachments_link' => 'Liitä linkki',\n    'attachments_upload_drop' => 'Vaihtoehtoisesti voit raahata ja pudottaa tiedoston tähän ladataksesi sen liitetiedostoksi.',\n    'attachments_set_link' => 'Aseta linkki',\n    'attachments_delete' => 'Haluatko varmasti poistaa tämän liitteen?',\n    'attachments_dropzone' => 'Pudota siirrettävät tiedostot tähän',\n    'attachments_no_files' => 'Yhtään tiedostoa ei ole ladattu',\n    'attachments_explain_link' => 'Voit antaa linkin, jos et halua ladata tiedostoa. Se voi olla linkki toiselle sivulle tai linkki pilvipalvelussa olevaan tiedostoon.',\n    'attachments_link_name' => 'Linkin nimi',\n    'attachment_link' => 'Liitteen linkki',\n    'attachments_link_url' => 'Linkki tiedostoon',\n    'attachments_link_url_hint' => 'Sivuston tai tiedoston osoite',\n    'attach' => 'Liitä',\n    'attachments_insert_link' => 'Lisää liitteen linkki sivulle',\n    'attachments_edit_file' => 'Muokkaa tiedostoa',\n    'attachments_edit_file_name' => 'Tiedoston nimi',\n    'attachments_edit_drop_upload' => 'Pudota tiedostoja tai klikkaa tästä ladataksesi ja korvataksesi',\n    'attachments_order_updated' => 'Liitteiden järjestys päivitetty',\n    'attachments_updated_success' => 'Liitteen tiedot päivitetty',\n    'attachments_deleted' => 'Liite poistettu',\n    'attachments_file_uploaded' => 'Tiedosto ladattiin onnistuneesti',\n    'attachments_file_updated' => 'Tiedosto päivitettiin onnistuneesti',\n    'attachments_link_attached' => 'Linkki liitettiin onnistuneesti sivulle',\n    'templates' => 'Mallipohjat',\n    'templates_set_as_template' => 'Sivu on mallipohja',\n    'templates_explain_set_as_template' => 'Voit tehdä tästä sivusta mallipohjan, jolloin sen sisältöä voidaan käyttää muiden sivujen luomisessa. Muut käyttäjät voivat käyttää tätä mallipohjaa, jos heillä on pääsyoikeus sivuun.',\n    'templates_replace_content' => 'Korvaa sivun sisältö',\n    'templates_append_content' => 'Liitä sivun sisällön jatkoksi',\n    'templates_prepend_content' => 'Liitä sivun sisällön alkuun',\n\n    // Profile View\n    'profile_user_for_x' => 'Ollut käyttäjä :time',\n    'profile_created_content' => 'Luotu sisältö',\n    'profile_not_created_pages' => ':userName ei ole luonut sivuja',\n    'profile_not_created_chapters' => ':userName ei ole luonut lukuja',\n    'profile_not_created_books' => ':userName ei ole luonut kirjoja',\n    'profile_not_created_shelves' => ':userName ei ole luonut hyllyjä',\n\n    // Comments\n    'comment' => 'Kommentti',\n    'comments' => 'Kommentit',\n    'comment_add' => 'Lisää kommentti',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Jätä kommentti tähän',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Tallenna kommentti',\n    'comment_new' => 'Uusi kommentti',\n    'comment_created' => 'kommentoi :createDiff',\n    'comment_updated' => 'Päivitetty :updateDiff :username toimesta',\n    'comment_updated_indicator' => 'Päivitetty',\n    'comment_deleted_success' => 'Kommentti poistettu',\n    'comment_created_success' => 'Kommentti lisätty',\n    'comment_updated_success' => 'Kommentti päivitetty',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Haluatko varmasti poistaa tämän kommentin?',\n    'comment_in_reply_to' => 'Vastaus kommenttiin :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Tässä ovat sivulle jätetyt komentit. Kommentteja voi lisätä ja hallita, kun tarkastelet tallennettua sivua.',\n\n    // Revision\n    'revision_delete_confirm' => 'Haluatko varmasti poistaa tämän version?',\n    'revision_restore_confirm' => 'Oletko varma, että haluat palauttaa tämän version? Sivun nykyinen sisältö korvataan.',\n    'revision_cannot_delete_latest' => 'Uusinta versiota ei voi poistaa.',\n\n    // Copy view\n    'copy_consider' => 'Ota huomioon seuraavat asiat sisältöä kopioidessasi.',\n    'copy_consider_permissions' => 'Mukautettuja käyttöoikeusasetuksia ei kopioida.',\n    'copy_consider_owner' => 'Sinusta tulee kaiken kopioidun sisällön omistaja.',\n    'copy_consider_images' => 'Sivun kuvatiedostoja ei kopioida, ja alkuperäiset kuvat säilyttävät suhteensa siihen sivuun, jolle ne alun perin ladattiin.',\n    'copy_consider_attachments' => 'Sivun liitteitä ei kopioida.',\n    'copy_consider_access' => 'Sijainnin, omistajan tai käyttöoikeuksien vaihtuminen voi johtaa siihen, että tämä sisältö on sellaisten käyttäjien saatavilla, joilla ei ole aiemmin ollut siihen pääsyä.',\n\n    // Conversions\n    'convert_to_shelf' => 'Muunna hyllyksi',\n    'convert_to_shelf_contents_desc' => 'Voit muuntaa tämän kirjan uudeksi hyllyksi, jossa on sama sisältö. Kirjan sisältämät luvut muunnetaan uusiksi kirjoiksi. Jos kirja sisältää sivuja, jotka eivät kuulu mihinkään lukuun, kirja nimetään uudelleen ja siitä tulee osa uutta hyllyä, joka sisältää kyseiset sivut.',\n    'convert_to_shelf_permissions_desc' => 'Kaikki tälle kirjalle asetetut käyttöoikeudet kopioidaan uuteen hyllyyn ja kaikkiin uusiin hyllyn kirjoihin, joilla ei ole omia käyttöoikeuksia. Huomaa, että hyllyjen käyttöoikeudet eivät siirry automaattisesti hyllyssä olevaan sisältöön, kuten kirjoissa.',\n    'convert_book' => 'Muunna kirja',\n    'convert_book_confirm' => 'Haluatko varmasti muuntaa tämän kirjan?',\n    'convert_undo_warning' => 'Tätä ei voi yhtä helposti perua.',\n    'convert_to_book' => 'Muunna kirjaksi',\n    'convert_to_book_desc' => 'Voit muuntaa tämän luvun uudeksi kirjaksi, jonka sisältö on sama. Kaikki tälle luvulle asetetut käyttöoikeudet kopioidaan uuteen kirjaan, mutta alkuperäisestä kirjasta perittyjä käyttöoikeuksia ei kopioida, mikä voi johtaa käyttöoikeuksien muuttumiseen.',\n    'convert_chapter' => 'Muunna luku',\n    'convert_chapter_confirm' => 'Haluatko varmasti muuntaa tämän luvun?',\n\n    // References\n    'references' => 'Viitteet',\n    'references_none' => 'Tähän kohteeseen ei ole viittauksia.',\n    'references_to_desc' => 'Lista kohteeseen viittaavasta sisällöstä.',\n\n    // Watch Options\n    'watch' => 'Seuraa',\n    'watch_title_default' => 'Oletusasetukset',\n    'watch_desc_default' => 'Palauta seuranta omiin oletusasetuksiin.',\n    'watch_title_ignore' => 'Ohita',\n    'watch_desc_ignore' => 'Ohita kaikki ilmoitukset, myös käyttäjäasetuksissa määritellyt ilmoitukset.',\n    'watch_title_new' => 'Uudet sivut',\n    'watch_desc_new' => 'Ilmoita, kun tähän kohteeseen luodaan uusi sivu.',\n    'watch_title_updates' => 'Kaikki sivupäivitykset',\n    'watch_desc_updates' => 'Ilmoita kaikista uusista sivuista ja sivujen muutoksista.',\n    'watch_desc_updates_page' => 'Ilmoita kaikista sivujen muutoksista.',\n    'watch_title_comments' => 'Kaikki sivupäivitykset ja kommentit',\n    'watch_desc_comments' => 'Ilmoita kaikista uusista sivuista, sivujen muutoksista ja uusista kommenteista.',\n    'watch_desc_comments_page' => 'Ilmoita sivujen muutoksista ja uusista kommenteista.',\n    'watch_change_default' => 'Muuta oletusilmoitusasetuksia',\n    'watch_detail_ignore' => 'Ilmoitusten ohittaminen',\n    'watch_detail_new' => 'Uusien sivujen seuraaminen',\n    'watch_detail_updates' => 'Uusien sivujen ja päivitysten seuraaminen',\n    'watch_detail_comments' => 'Uusien sivujen, päivitysten ja kommenttien seuraaminen',\n    'watch_detail_parent_book' => 'Seuraaminen kirjan perusteella',\n    'watch_detail_parent_book_ignore' => 'Huomioimatta jättäminen kirjan perusteella',\n    'watch_detail_parent_chapter' => 'Seuraaminen luvun perusteella',\n    'watch_detail_parent_chapter_ignore' => 'Huomioimatta jättäminen luvun perusteella',\n];\n"
  },
  {
    "path": "lang/fi/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Sinulla ei ole pääsyoikeutta pyydettyyn sivuun.',\n    'permissionJson' => 'Sinulla ei ole oikeutta suorittaa pyydettyä toimintoa.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Sähköpostiosoite :email on jo käytössä toisessa käyttäjätunnuksessa.',\n    'auth_pre_register_theme_prevention' => 'Käyttäjätiliä ei voitu rekisteröidä annetuille tiedoille',\n    'email_already_confirmed' => 'Sähköposti on jo vahvistettu, yritä kirjautua sisään.',\n    'email_confirmation_invalid' => 'Tämä vahvistuslinkki ei ole voimassa tai sitä on jo käytetty, yritä rekisteröityä uudelleen.',\n    'email_confirmation_expired' => 'Vahvistuslinkki on vanhentunut, uusi vahvistussähköposti on lähetetty.',\n    'email_confirmation_awaiting' => 'Tämän tilin sähköpostiosoite pitää vahvistaa',\n    'ldap_fail_anonymous' => 'Anonyymi LDAP-todennus epäonnistui',\n    'ldap_fail_authed' => 'LDAP-todennus epäonnistui annetulla nimellä ja salasanalla',\n    'ldap_extension_not_installed' => 'PHP:n LDAP-laajennusta ei ole asennettu',\n    'ldap_cannot_connect' => 'Yhteyttä LDAP-palvelimeen ei voida muodostaa, alustava yhteys epäonnistui',\n    'saml_already_logged_in' => 'Olet jo kirjautunut sisään',\n    'saml_no_email_address' => 'Tämän käyttäjän sähköpostiosoitetta ei löytynyt ulkoisesta todennuspalvelusta',\n    'saml_invalid_response_id' => 'Tämän sovelluksen käynnistämä prosessi ei tunnista ulkoisen todennusjärjestelmän pyyntöä.\nSovellus ei tunnista ulkoisen todennuspalvelun pyyntöä. Ongelman voi aiheuttaa siirtyminen selaimessa takaisin edelliseen näkymään kirjautumisen jälkeen.',\n    'saml_fail_authed' => 'Sisäänkirjautuminen :system käyttäen epäonnistui, järjestelmä ei antanut valtuutusta',\n    'oidc_already_logged_in' => 'Olet jo kirjautunut sisään',\n    'oidc_no_email_address' => 'Ulkoisen todennuspalvelun antamista tiedoista ei löytynyt tämän käyttäjän sähköpostiosoitetta',\n    'oidc_fail_authed' => 'Sisäänkirjautuminen :system käyttäen epäonnistui, järjestelmä ei antanut valtuutusta',\n    'social_no_action_defined' => 'Ei määriteltyä toimenpidettä',\n    'social_login_bad_response' => \"Virhe :socialAccount-kirjautumisen aikana: \\n:error\",\n    'social_account_in_use' => 'Tämä :socialAccount-tili on jo käytössä, yritä kirjautua sisään :socialAccount-vaihtoehdon kautta.',\n    'social_account_email_in_use' => 'Sähköposti :email on jo käytössä. Jos sinulla on jo sivustolla käyttäjätili, voit yhdistää :socialAccount-tilisi profiiliasetuksista.',\n    'social_account_existing' => 'Tämä :socialAccount-tili on jo liitetty profiiliisi.',\n    'social_account_already_used_existing' => 'Tämä :socialAccount-tili on jo toisen käyttäjän käytössä.',\n    'social_account_not_used' => 'Tätä :socialAccount-tiliä ei ole liitetty mihinkään käyttäjään. Voit liittää sen profiiliasetuksistasi. ',\n    'social_account_register_instructions' => 'Jos sinulla ei vielä ole käyttäjätiliä, voit rekisteröidä tilin käyttämällä :socialAccount-vaihtoehtoa.',\n    'social_driver_not_found' => 'Sosiaalisen median tilin ajuria ei löytynyt',\n    'social_driver_not_configured' => ':socialAccount-tilin asetuksia ei ole määritetty oikein.',\n    'invite_token_expired' => 'Tämä kutsulinkki on vanhentunut. Voit sen sijaan yrittää palauttaa tilisi salasanan.',\n    'login_user_not_found' => 'Käyttäjää tälle toiminnolle ei löytynyt.',\n\n    // System\n    'path_not_writable' => 'Tiedostopolkuun :filePath ei voitu ladata tiedostoa. Tarkista polun kirjoitusoikeudet.',\n    'cannot_get_image_from_url' => 'Kuvan hakeminen osoitteesta :url ei onnistu',\n    'cannot_create_thumbs' => 'Palvelin ei voi luoda pikkukuvia. Tarkista, että PHP:n GD-laajennus on asennettu.',\n    'server_upload_limit' => 'Palvelin ei salli näin suuria tiedostoja. Kokeile pienempää tiedostokokoa.',\n    'server_post_limit' => 'Palvelin ei pysty vastaanottamaan annettua tietomäärää. Yritä uudelleen pienemmällä tiedostolla.',\n    'uploaded'  => 'Palvelin ei salli näin suuria tiedostoja. Kokeile pienempää tiedostokokoa.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Kuvan lataamisessa tapahtui virhe',\n    'image_upload_type_error' => 'Ladattavan kuvan tyyppi on virheellinen',\n    'image_upload_replace_type' => 'Korvaavan kuvatiedoston tulee olla samaa tyyppiä kuin alkuperäinen kuva',\n    'image_upload_memory_limit' => 'Kuvan lataaminen ja/tai pikkukuvien luominen epäonnistui järjestelmän resurssirajoitusten vuoksi.',\n    'image_thumbnail_memory_limit' => 'Kuvan kokovaihtoehtojen luominen epäonnistui järjestelmän resurssirajoitusten vuoksi.',\n    'image_gallery_thumbnail_memory_limit' => 'Gallerian pikkukuvien luominen epäonnistui järjestelmän resurssirajoitusten vuoksi.',\n    'drawing_data_not_found' => 'Piirustuksen tietoja ei voitu ladata. Piirustustiedostoa ei ehkä ole enää olemassa tai sinulla ei ole oikeutta käyttää sitä.',\n\n    // Attachments\n    'attachment_not_found' => 'Liitettä ei löytynyt',\n    'attachment_upload_error' => 'Liitteen lataamisessa tapahtui virhe',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Luonnoksen tallentaminen epäonnistui. Varmista, että sinulla on toimiva internetyhteys ennen sivun tallentamista',\n    'page_draft_delete_fail' => 'Luonnoksen poistaminen ja sivun tallennetun sisällön noutaminen epäonnistui',\n    'page_custom_home_deletion' => 'Sivua ei voi poistaa, koska se on asetettu etusivuksi',\n\n    // Entities\n    'entity_not_found' => 'Kohdetta ei löydy',\n    'bookshelf_not_found' => 'Hyllyä ei löytynyt',\n    'book_not_found' => 'Kirjaa ei löytynyt',\n    'page_not_found' => 'Sivua ei löytynyt',\n    'chapter_not_found' => 'Lukua ei löytynyt',\n    'selected_book_not_found' => 'Valittua kirjaa ei löytynyt',\n    'selected_book_chapter_not_found' => 'Valittua kirjaa tai lukua ei löytynyt',\n    'guests_cannot_save_drafts' => 'Vieraat eivät voi tallentaa luonnoksia',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Ainoaa ylläpitäjää ei voi poistaa',\n    'users_cannot_delete_guest' => 'Vieraskäyttäjää ei voi poistaa',\n    'users_could_not_send_invite' => 'Käyttäjää ei voitu luoda kutsun lähettämisen jälkeen',\n\n    // Roles\n    'role_cannot_be_edited' => 'Tätä roolia ei voi muokata',\n    'role_system_cannot_be_deleted' => 'Tämä rooli on järjestelmärooli, eikä sitä voi poistaa',\n    'role_registration_default_cannot_delete' => 'Tätä roolia ei voi poistaa, kun se on asetettu oletusrooliksi uusille rekisteröityville käyttäjille',\n    'role_cannot_remove_only_admin' => 'Tämä käyttäjä on ainoa käyttäjä, jolle on määritetty ylläpitäjän rooli. Määritä ylläpitäjän rooli toiselle käyttäjälle, ennen kuin yrität poistaa tämän käyttäjän.',\n\n    // Comments\n    'comment_list' => 'Kommenttien noutamisessa tapahtui virhe.',\n    'cannot_add_comment_to_draft' => 'Luonnokseen ei voi lisätä kommentteja.',\n    'comment_add' => 'Kommentin lisäämisessä tai päivittämisessä tapahtui virhe.',\n    'comment_delete' => 'Kommentin poistamisessa tapahtui virhe.',\n    'empty_comment' => 'Tyhjää kommenttia ei voi lisätä.',\n\n    // Error pages\n    '404_page_not_found' => 'Sivua ei löydy',\n    'sorry_page_not_found' => 'Valitettavasti etsimääsi sivua ei löytynyt.',\n    'sorry_page_not_found_permission_warning' => 'Jos oletit, että tämä sivu on olemassa, sinulla ei ehkä ole lupaa tarkastella sitä.',\n    'image_not_found' => 'Kuvaa ei löytynyt',\n    'image_not_found_subtitle' => 'Valitettavasti etsimääsi kuvatiedostoa ei löytynyt.',\n    'image_not_found_details' => 'Jos oletit, että tämä kuva on olemassa, se on ehkä poistettu.',\n    'return_home' => 'Palaa etusivulle',\n    'error_occurred' => 'Tapahtui virhe',\n    'app_down' => ':appName on kaatunut',\n    'back_soon' => 'Se palautetaan pian.',\n\n    // Import\n    'import_zip_cant_read' => 'ZIP-tiedostoa ei voitu lukea.',\n    'import_zip_cant_decode_data' => 'ZIP-tiedoston data.json sisältöä ei löydy eikä sitä voitu purkaa.',\n    'import_zip_no_data' => 'ZIP-tiedostoilla ei ole odotettua kirjaa, lukua tai sivun sisältöä.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Tuonti ZIP epäonnistui virheiden kanssa:',\n    'import_zip_failed_notification' => 'ZIP-tiedoston tuominen epäonnistui.',\n    'import_perms_books' => 'Sinulla ei ole tarvittavia oikeuksia luoda kirjoja.',\n    'import_perms_chapters' => 'Sinulla ei ole tarvittavia oikeuksia luoda kappaleita.',\n    'import_perms_pages' => 'Sinulla ei ole tarvittavia oikeuksia luoda sivuja.',\n    'import_perms_images' => 'Sinulla ei ole tarvittavia oikeuksia luoda kuvia.',\n    'import_perms_attachments' => 'Sinulla ei ole tarvittavaa lupaa luoda liitteitä.',\n\n    // API errors\n    'api_no_authorization_found' => 'Pyynnöstä ei löytynyt valtuutuskoodia',\n    'api_bad_authorization_format' => 'Pyynnöstä löytyi valtuutuskoodi, mutta sen muoto oli virheellinen',\n    'api_user_token_not_found' => 'Annetulle valtuutuskoodille ei löytynyt vastaavaa API-tunnistetta',\n    'api_incorrect_token_secret' => 'API-tunnisteelle annettu salainen avain on virheellinen',\n    'api_user_no_api_permission' => 'Käytetyn API-tunnisteen omistajalla ei ole oikeutta tehdä API-kutsuja',\n    'api_user_token_expired' => 'Käytetty valtuutuskoodi on vanhentunut',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Virhe testisähköpostia lähetettäessä:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'URL-osoite ei vastaa määritettyjä sallittuja SSR-isäntiä',\n];\n"
  },
  {
    "path": "lang/fi/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Uusi kommentti sivulla: :pageName',\n    'new_comment_intro' => 'Käyttäjä on kommentoinut sivua sivustolla :appName:',\n    'new_page_subject' => 'Uusi sivu: :pageName',\n    'new_page_intro' => 'Uusi sivu on luotu sivustolla :appName:',\n    'updated_page_subject' => 'Päivitetty sivu: :pageName',\n    'updated_page_intro' => 'Sivu on päivitetty sivustolla :appName:',\n    'updated_page_debounce' => 'Useiden ilmoitusten välttämiseksi sinulle ei toistaiseksi lähetetä ilmoituksia saman toimittajan tekemistä uusista muokkauksista tälle sivulle.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Sivun nimi:',\n    'detail_page_path' => 'Sivun polku:',\n    'detail_commenter' => 'Kommentoija:',\n    'detail_comment' => 'Kommentti:',\n    'detail_created_by' => 'Luonut',\n    'detail_updated_by' => 'Päivittänyt',\n\n    'action_view_comment' => 'Näytä kommentti',\n    'action_view_page' => 'Näytä sivu',\n\n    'footer_reason' => 'Tämä ilmoitus lähetettiin sinulle, koska :link kattaa tämän tyyppisen toiminnan tälle kohteelle.',\n    'footer_reason_link' => 'omat ilmoitusasetukset',\n];\n"
  },
  {
    "path": "lang/fi/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Edellinen',\n    'next'     => 'Seuraava &raquo;',\n\n];\n"
  },
  {
    "path": "lang/fi/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Salasanan on oltava vähintään kahdeksan merkkiä pitkä ja täsmättävä vahvistuksen kanssa.',\n    'user' => \"Emme löydä käyttäjää, jolla on kyseinen sähköpostiosoite.\",\n    'token' => 'Salasanan palautuslinkki ei täsmää sähköpostin kanssa.',\n    'sent' => 'Olemme lähettäneet salasanasi palautuslinkin sähköpostitse!',\n    'reset' => 'Salasanasi on palautettu!',\n\n];\n"
  },
  {
    "path": "lang/fi/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Oma tili',\n\n    'shortcuts' => 'Pikanäppäimet',\n    'shortcuts_interface' => 'Käyttöliittymän pikanäppäinten asetukset',\n    'shortcuts_toggle_desc' => 'Tästä voit ottaa käyttöön tai poistaa käytöstä järjestelmän käyttöliittymän pikanäppäimet, joita käytetään navigointiin ja toimintoihin.',\n    'shortcuts_customize_desc' => 'Voit mukauttaa alla olevia pikanäppäimiä. Paina haluamaasi näppäinyhdistelmää sen jälkeen, kun olet valinnut pikanäppäimen syöttökentän.',\n    'shortcuts_toggle_label' => 'Pikanäppäimet käytössä',\n    'shortcuts_section_navigation' => 'Navigointi',\n    'shortcuts_section_actions' => 'Yleiset toiminnot',\n    'shortcuts_save' => 'Tallenna pikanäppäimet',\n    'shortcuts_overlay_desc' => 'Huomautus: kun pikanäppäimet ovat käytössä, voit painaa \"?\"-näppäintä, joka korostaa näytöllä näkyviin toimintoihin liitetyt pikanäppäimet.',\n    'shortcuts_update_success' => 'Pikanäppäinten asetukset on päivitetty!',\n    'shortcuts_overview_desc' => 'Hallitse pikanäppäimiä, joilla voit navigoida järjestelmän käyttöliittymässä.',\n\n    'notifications' => 'Ilmoitusasetukset',\n    'notifications_desc' => 'Hallitse järjestelmän toimintoihin liittyviä sähköposti-ilmoituksia.',\n    'notifications_opt_own_page_changes' => 'Ilmoita omistamilleni sivuille tehdyistä muutoksista',\n    'notifications_opt_own_page_comments' => 'Ilmoita omistamilleni sivuille tehdyistä kommenteista',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Ilmoita vastauksista kommentteihini',\n    'notifications_save' => 'Tallenna asetukset',\n    'notifications_update_success' => 'Ilmoitusasetukset on päivitetty!',\n    'notifications_watched' => 'Seuratut ja huomiotta jätetyt kohteet',\n    'notifications_watched_desc' => 'Alla oleviin kohteisiin sovelletaan muokautettuja seuranta-asetuksia. Voit päivittää kohteita koskevat asetukset avaamalla kohde ja valitsemalla seuranta-asetukset sivupalkista.',\n\n    'auth' => 'Pääsy ja turvallisuus',\n    'auth_change_password' => 'Vaihda salasana',\n    'auth_change_password_desc' => 'Vaihda kirjautumiseen käytettävä salasana. Salasanan on oltava vähintään 8 merkkiä pitkä.',\n    'auth_change_password_success' => 'Salasana on päivitetty!',\n\n    'profile' => 'Profiilitiedot',\n    'profile_desc' => 'Hallitse muille käyttäjille näkyviä käyttäjätilisi tietoja, sekä tietoja, joita käytetään viestintään ja järjestelmän personointiin.',\n    'profile_view_public' => 'Näytä julkinen profiili',\n    'profile_name_desc' => 'Määritä näyttönimesi, joka näkyy muille käyttäjille järjestelmässä suorittamiesi toimintojen ja omistamasi sisällön yhteydessä.',\n    'profile_email_desc' => 'Tätä sähköpostia käytetään ilmoituksiin ja käytössä olevasta tunnistautumistavasta riippuen järjestelmään pääsyyn.',\n    'profile_email_no_permission' => 'Valitettavasti sinulla ei ole lupaa muuttaa sähköpostiosoitettasi. Jos haluat muuttaa sen, sinun on pyydettävä ylläpitäjää muuttamaan se puolestasi.',\n    'profile_avatar_desc' => 'Valitse kuva, joka näkyy järjestelmän muille käyttäjille. Kuvan tulisi olla neliön muotoinen ja leveydeltään ja korkeudeltaan noin 256 pikseliä.',\n    'profile_admin_options' => 'Ylläpitäjän asetukset',\n    'profile_admin_options_desc' => 'Ylläpitäjän lisäasetukset, kuten roolien osoittamiseen liittyvät valinnat, löytyvät käyttäjätiliäsi varten järjestelmän kohdasta \"Asetukset > Käyttäjät\".',\n\n    'delete_account' => 'Poista käyttäjätili',\n    'delete_my_account' => 'Poista oma käyttäjätili',\n    'delete_my_account_desc' => 'Tämä poistaa käyttäjätilisi kokonaan järjestelmästä. Et voi palauttaa tiliäsi tai peruuttaa tätä toimenpidettä. Luomasi sisältö, kuten luodut sivut ja ladatut kuvat, säilyvät.',\n    'delete_my_account_warning' => 'Haluatko varmasti poistaa käyttäjätilisi?',\n];\n"
  },
  {
    "path": "lang/fi/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Asetukset',\n    'settings_save' => 'Tallenna asetukset',\n    'system_version' => 'Järjestelmän versio',\n    'categories' => 'Kategoriat',\n\n    // App Settings\n    'app_customization' => 'Mukauttaminen',\n    'app_features_security' => 'Ominaisuudet ja turvallisuus',\n    'app_name' => 'Sivuston nimi',\n    'app_name_desc' => 'Tämä nimi näkyy ylätunnisteessa ja kaikissa järjestelmän lähettämissä sähköpostiviesteissä.',\n    'app_name_header' => 'Näytä nimi ylätunnisteessa',\n    'app_public_access' => 'Julkinen pääsy',\n    'app_public_access_desc' => 'Ottamalla tämä asetus käyttöön vierailijat voivat lukea sisältöjä kirjautumatta sisään.',\n    'app_public_access_desc_guest' => 'Vierailijoiden pääsyoikeuksia voidaan hallinnoida \"Vierailija\"-käyttäjän asetuksista.',\n    'app_public_access_toggle' => 'Salli julkinen pääsy',\n    'app_public_viewing' => 'Salli julkinen katselu?',\n    'app_secure_images' => 'Turvallisemmat kuvien lataukset',\n    'app_secure_images_toggle' => 'Ota käyttöön turvallisemmat kuvien lataukset',\n    'app_secure_images_desc' => 'Paremman suorituskyvyn takia kaikki kuvat ovat julkisia. Tämä asetus lisää satunnaisen, vaikeasti arvattavan merkkijonon kuvien url-osoitteisiin. Varmista, että hakemistojen indeksit eivät ole palvelimen asetuksissa päällä, jotta niitä ei pääse selaamaan.',\n    'app_default_editor' => 'Sivujen oletuseditori',\n    'app_default_editor_desc' => 'Valitse editori, jota käytetään oletuksena uusia sivuja muokattaessa. Valinnan voi ohittaa sivutasolla, jos käyttäjän oikeudet sallivat sen.',\n    'app_custom_html' => 'HTML-otsakkeen mukautettu sisältö',\n    'app_custom_html_desc' => 'Tähän annettu sisältö lisätään jokaisen sivun <head>-osan loppuun. Tällä tavalla voit lisätä kätevästi esimerkiksi omia CSS-tyylejä tai analytiikkapalveluiden vaatimia koodeja.',\n    'app_custom_html_disabled_notice' => 'Mukautettu HTML-otsakkeen sisältö ei ole käytössä tällä asetussivulla, jotta kaikki virheitä aiheuttavat muutokset voidaan poistaa.',\n    'app_logo' => 'Sivuston logo',\n    'app_logo_desc' => 'Kuvaa käytetään esimerkiksi sivuston otsikkopalkissa. Kuvan korkeuden tulisi olla 86 pikseliä. Suuremmat kuvat skaalataan pienemmiksi.',\n    'app_icon' => 'Sivuston kuvake',\n    'app_icon_desc' => 'Kuvaketta käytetään selaimen välilehdissä ja pikakuvakkeissa. Kuvakkeen tulisi olla 256 pikselin neliönmuotoinen PNG-kuva.',\n    'app_homepage' => 'Sivuston kotisivu',\n    'app_homepage_desc' => 'Valitse näkymä, joka näytetään etusivuna oletusnäkymän sijaan. Sivun käyttöoikeuksia ei oteta huomioon valituilla sivuilla.',\n    'app_homepage_select' => 'Valitse sivu',\n    'app_footer_links' => 'Alatunnisteen linkit',\n    'app_footer_links_desc' => 'Lisää linkkejä sivuston alatunnisteeseen. Nämä näkyvät useimpien sivujen alareunassa, myös niiden, jotka eivät vaadi kirjautumista. Voit käyttää merkintää \"trans::<key>\" käyttääksesi järjestelmän määrittelemiä käännöksiä. Esimerkiksi käyttämällä \"trans::common.privacy_policy\" saadaan käännetty teksti \"Tietosuojaseloste\" ja \"trans::common.terms_of_service\" saadaan käännetty teksti \"Palvelun käyttöehdot\".',\n    'app_footer_links_label' => 'Linkin nimi',\n    'app_footer_links_url' => 'Linkin URL-osoite',\n    'app_footer_links_add' => 'Lisää alatunnisteen linkki',\n    'app_disable_comments' => 'Poista kommentit käytöstä',\n    'app_disable_comments_toggle' => 'Poista kommentit käytöstä',\n    'app_disable_comments_desc' => 'Poistaa kommentit käytöstä kaikilla sivuilla. <br> Lisättyjä kommentteja ei näytetä.',\n\n    // Color settings\n    'color_scheme' => 'Sivuston värimalli',\n    'color_scheme_desc' => 'Määritä sivuston käyttöliittymässä käytettävät värit. Värit voidaan määrittää erikseen tummalle ja vaalealle tilalle, jotta ne sopivat parhaiten teemaan ja varmistavat luettavuuden.',\n    'ui_colors_desc' => 'Aseta sivuston pääväri ja linkin oletusväri. Ensisijaista väriä käytetään pääasiassa yläpalkissa, painikkeissa ja käyttöliittymän koristeissa. Linkin oletusväriä käytetään tekstipohjaisissa linkeissä ja toiminnoissa sekä kirjoitetussa sisällössä että sivuston käyttöliittymässä.',\n    'app_color' => 'Pääväri',\n    'link_color' => 'Linkin oletusväri',\n    'content_colors_desc' => 'Määritä eri sisältötyyppien värit. Luettavuuden ja saavutettavuuden kannalta on suositeltavaa valita värit, joiden kirkkaus on samankaltainen kuin oletusvärien.',\n    'bookshelf_color' => 'Hyllyn väri',\n    'book_color' => 'Kirjan väri',\n    'chapter_color' => 'Luvun väri',\n    'page_color' => 'Sivun väri',\n    'page_draft_color' => 'Luonnoksen väri',\n\n    // Registration Settings\n    'reg_settings' => 'Rekisteröityminen',\n    'reg_enable' => 'Salli rekisteröityminen',\n    'reg_enable_toggle' => 'Salli rekisteröityminen',\n    'reg_enable_desc' => 'Kun rekisteröityminen on käytössä, vierailijat voivat rekisteröityä sivuston käyttäjiksi. Rekisteröitymisen yhteydessä heille annetaan oletuskäyttäjärooli.',\n    'reg_default_role' => 'Oletuskäyttäjärooli rekisteröitymisen jälkeen',\n    'reg_enable_external_warning' => 'Yllä olevaa vaihtoehtoa ei oteta huomioon, kun ulkoinen LDAP- tai SAML-todennus on käytössä. Käyttäjätilit luodaan automaattisesti, jos tunnistautuminen käytössä olevaan ulkoiseen järjestelmään onnistuu.',\n    'reg_email_confirmation' => 'Sähköpostivahvistus',\n    'reg_email_confirmation_toggle' => 'Vaadi sähköpostivahvistus',\n    'reg_confirm_email_desc' => 'Jos domain-rajoitus on käytössä, sähköpostivahvistus on oletuksena päällä, eikä tätä valintaa oteta huomioon.',\n    'reg_confirm_restrict_domain' => 'Domain-rajoitus',\n    'reg_confirm_restrict_domain_desc' => 'Kirjoita pilkulla erotettu luettelo sähköpostien domain-nimistä, joihin haluat rajoittaa rekisteröitymisen. Käyttäjille lähetetään sähköpostiviesti osoitteen vahvistamiseksi, ennen kuin he pääsevät käyttämään sivustoa. <br> Huomaa, että käyttäjät voivat muuttaa sähköpostiosoitteensa onnistuneen rekisteröinnin jälkeen.',\n    'reg_confirm_restrict_domain_placeholder' => 'Ei rajoituksia',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Huolto',\n    'maint_image_cleanup' => 'Siivoa kuvat',\n    'maint_image_cleanup_desc' => 'Tarkistaa kuvista ja luonnoksista mitkä kuvat ja piirustukset ovat tällä hetkellä käytössä ja mitkä ovat tarpeettomia. Varmista, että olet varmuuskopioinut tietokannan ja kuvat ennen tämän toiminnon suorittamista.',\n    'maint_delete_images_only_in_revisions' => 'Poista myös kuvat, jotka ovat olemassa vain vanhoissa sivujen versioissa',\n    'maint_image_cleanup_run' => 'Suorita siivous',\n    'maint_image_cleanup_warning' => ':count mahdollisesti käyttämätöntä kuvaa löytyi. Haluatko varmasti poistaa nämä kuvat?',\n    'maint_image_cleanup_success' => ':count mahdollisesti käyttämätöntä kuvaa löydetty ja poistettu!',\n    'maint_image_cleanup_nothing_found' => 'Käyttämättömiä kuvia ei löytynyt, mitään ei poistettu!',\n    'maint_send_test_email' => 'Lähetä testisähköposti',\n    'maint_send_test_email_desc' => 'Toiminto lähettää testisähköpostin profiilissasi määritettyyn sähköpostiosoitteeseen.',\n    'maint_send_test_email_run' => 'Lähetä testisähköposti',\n    'maint_send_test_email_success' => 'Sähköposti lähetetty osoitteeseen :address',\n    'maint_send_test_email_mail_subject' => 'Testisähköpostiviesti',\n    'maint_send_test_email_mail_greeting' => 'Sähköpostin lähetys näyttää toimivan!',\n    'maint_send_test_email_mail_text' => 'Onnittelut! Koska sait tämän sähköposti-ilmoituksen, sähköpostiasetuksesi näyttävät olevan oikein määritetty.',\n    'maint_recycle_bin_desc' => 'Poistetut hyllyt, kirjat, luvut ja sivut siirretään roskakoriin, josta ne voidaan palauttaa tai poistaa pysyvästi. Vanhemmat kohteet roskakorissa saatetaan poistaa automaattisesti jonkin ajan kuluttua järjestelmän asetuksista riippuen.',\n    'maint_recycle_bin_open' => 'Avaa roskakori',\n    'maint_regen_references' => 'Luo viitteet uudelleen',\n    'maint_regen_references_desc' => 'Tämä toiminto rakentaa sisältöjen väliset viittaukset uudelleen. Tämä tapahtuu yleensä automaattisesti, mutta tämä toiminto voi olla hyödyllinen indeksoitaessa vanhaa sisältöä tai vaihtoehtoisin menetelmin lisättyä sisältöä.',\n    'maint_regen_references_success' => 'Viiteindeksi on luotu uudelleen!',\n    'maint_timeout_command_note' => 'Huomautus: tämän toiminnon suorittaminen voi kestää jonkin aikaa, mikä voi johtaa aikakatkaisusta johtuviin ongelmiin joissakin verkkoympäristöissä. Vaihtoehtoisesti tämä toiminto voidaan suorittaa komentoriviltä.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Roskakori',\n    'recycle_bin_desc' => 'Tässä voit palauttaa poistetut kohteet tai poistaa ne pysyvästi järjestelmästä. Tämä luettelo on suodattamaton, toisin kuin järjestelmän vastaavat toimintoluettelot, joihin sovelletaan käyttöoikeussuodattimia.',\n    'recycle_bin_deleted_item' => 'Poistettu kohde',\n    'recycle_bin_deleted_parent' => 'Vanhempi',\n    'recycle_bin_deleted_by' => 'Poistanut',\n    'recycle_bin_deleted_at' => 'Poistoaika',\n    'recycle_bin_permanently_delete' => 'Poista pysyvästi',\n    'recycle_bin_restore' => 'Palauta',\n    'recycle_bin_contents_empty' => 'Roskakori on tällä hetkellä tyhjä',\n    'recycle_bin_empty' => 'Tyhjennä roskakori',\n    'recycle_bin_empty_confirm' => 'Tämä tuhoaa pysyvästi kaikki kohteet roskakorissa, mukaan lukien kunkin kohteen sisältämän sisällön. Haluatko varmasti tyhjentää roskakorin?',\n    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?',\n    'recycle_bin_destroy_list' => 'Poistettavat kohteet',\n    'recycle_bin_restore_list' => 'Palautettavat kohteet',\n    'recycle_bin_restore_confirm' => 'Tämä toiminto palauttaa poistetun kohteen, mukaan lukien kaikki siihen sisältyvät kohteet, alkuperäiseen sijaintiinsa. Jos alkuperäinen sijainti on sittemmin poistettu ja on nyt roskakorissa, myös sitä koskeva kohde on palautettava.',\n    'recycle_bin_restore_deleted_parent' => 'Kohde, johon tämä kohde sisältyy on myös poistettu. Kohteet pysyvät poistettuina, kunnes kyseinen vanhempi on palautettu.',\n    'recycle_bin_restore_parent' => 'Palauta vanhempi',\n    'recycle_bin_destroy_notification' => 'Poistettu yhteensä :count kohdetta roskakorista.',\n    'recycle_bin_restore_notification' => 'Palautettu yhteensä :count kohdetta roskakorista.',\n\n    // Audit Log\n    'audit' => 'Tarkastusloki',\n    'audit_desc' => 'Tämä tarkastusloki näyttää listauksen järjestelmässä suoritetuista toiminnoista. Lista on suodattamaton toisin kuin vastaavat järjestelmässä olevat listat, joihin sovelletaan käyttöoikeussuodattimia.',\n    'audit_event_filter' => 'Tapahtumasuodatin',\n    'audit_event_filter_no_filter' => 'Ei suodatinta',\n    'audit_deleted_item' => 'Poistettu kohde',\n    'audit_deleted_item_name' => 'Nimi: :name',\n    'audit_table_user' => 'Käyttäjä',\n    'audit_table_event' => 'Tapahtuma',\n    'audit_table_related' => 'Liittyvä kohde tai tieto',\n    'audit_table_ip' => 'IP-osoite',\n    'audit_table_date' => 'Toiminnan päiväys',\n    'audit_date_from' => 'Päiväys alkaen',\n    'audit_date_to' => 'Päiväys saakka',\n\n    // Role Settings\n    'roles' => 'Roolit',\n    'role_user_roles' => 'Käyttäjäroolit',\n    'roles_index_desc' => 'Rooleja käytetään käyttäjien ryhmittelyyn ja järjestelmän käyttöoikeuksien antamiseen. Kun käyttäjä on useamman roolin jäsen, hän saa kaikkien omien rooliensa kyvyt.',\n    'roles_x_users_assigned' => ':count käyttäjä osoitettu|:count käyttäjää osoitettu',\n    'roles_x_permissions_provided' => ':count käyttöoikeus|:count käyttöoikeutta',\n    'roles_assigned_users' => 'Osoitetut käyttäjät',\n    'roles_permissions_provided' => 'Annetut käyttöoikeudet',\n    'role_create' => 'Luo uusi rooli',\n    'role_delete' => 'Poista rooli',\n    'role_delete_confirm' => 'Tämä poistaa roolin \\':roleName\\'.',\n    'role_delete_users_assigned' => 'Tähän rooliin on osoitettu :userCount käyttäjää. Jos haluat siirtää käyttäjät tästä roolista, valitse uusi rooli alta.',\n    'role_delete_no_migration' => \"Älä siirrä käyttäjiä\",\n    'role_delete_sure' => 'Oletko varma, että haluat poistaa tämän roolin?',\n    'role_edit' => 'Muokkaa roolia',\n    'role_details' => 'Roolin tiedot',\n    'role_name' => 'Roolin nimi',\n    'role_desc' => 'Lyhyt kuvaus roolista',\n    'role_mfa_enforced' => 'Vaatii monivaiheisen tunnistautumisen',\n    'role_external_auth_id' => 'Ulkoisen tunnistautumisen tunnukset',\n    'role_system' => 'Järjestelmän käyttöoikeudet',\n    'role_manage_users' => 'Hallinnoi käyttäjiä',\n    'role_manage_roles' => 'Hallinnoi rooleja ja roolien käyttöoikeuksia',\n    'role_manage_entity_permissions' => 'Hallinnoi kaikkien kirjojen, lukujen ja sivujen käyttöoikeuksia',\n    'role_manage_own_entity_permissions' => 'Hallinnoi omien kirjojen, lukujen ja sivujen käyttöoikeuksia',\n    'role_manage_page_templates' => 'Hallinnoi mallipohjia',\n    'role_access_api' => 'Pääsy järjestelmän ohjelmointirajapintaan',\n    'role_manage_settings' => 'Hallinnoi sivuston asetuksia',\n    'role_export_content' => 'Vie sisältöjä',\n    'role_import_content' => 'Import content',\n    'role_editor_change' => 'Vaihda sivun editoria',\n    'role_notifications' => 'Vastaanota ja hallinnoi ilmoituksia',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Sisältöjen oikeudet',\n    'roles_system_warning' => 'Huomaa, että minkä tahansa edellä mainituista kolmesta käyttöoikeudesta voi antaa käyttäjälle mahdollisuuden muuttaa omia tai muiden järjestelmän käyttäjien oikeuksia. Anna näitä oikeuksia sisältävät roolit vain luotetuille käyttäjille.',\n    'role_asset_desc' => 'Näillä asetuksilla hallitaan oletuksena annettavia käyttöoikeuksia järjestelmässä oleviin sisältöihin. Yksittäisten kirjojen, lukujen ja sivujen käyttöoikeudet kumoavat nämä käyttöoikeudet.',\n    'role_asset_admins' => 'Ylläpitäjät saavat automaattisesti pääsyn kaikkeen sisältöön, mutta nämä vaihtoehdot voivat näyttää tai piilottaa käyttöliittymävalintoja.',\n    'role_asset_image_view_note' => 'Tämä tarkoittaa näkyvyyttä kuvien hallinnassa. Pääsy ladattuihin kuvatiedostoihin riippuu asetetusta kuvien tallennusvaihtoehdosta.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'Kaikki',\n    'role_own' => 'Omat',\n    'role_controlled_by_asset' => 'Määräytyy sen sisällön mukaan, johon ne on ladattu',\n    'role_save' => 'Tallenna rooli',\n    'role_users' => 'Käyttäjät tässä roolissa',\n    'role_users_none' => 'Yhtään käyttäjää ei ole osoitettuna tähän rooliin',\n\n    // Users\n    'users' => 'Käyttäjät',\n    'users_index_desc' => 'Luo ja hallinnoi yksittäisiä käyttäjätilejä järjestelmässä. Käyttäjätilejä käytetään kirjautumiseen sekä käyttöoikeuksien hallinnointiin. Käyttöoikeudet perustuvat ensisijaisesti rooleihin, mutta käyttöoikeuksiin voi vaikuttaa myös se, onko käyttäjä tietyn sisällön omistaja.',\n    'user_profile' => 'Käyttäjäprofiili',\n    'users_add_new' => 'Lisää uusi käyttäjä',\n    'users_search' => 'Hae käyttäjiä',\n    'users_latest_activity' => 'Viimeisin toiminta',\n    'users_details' => 'Käyttäjän tiedot',\n    'users_details_desc' => 'Aseta tälle käyttäjälle näyttönimi ja sähköpostiosoite. Sähköpostiosoitetta käytetään sovellukseen kirjautumiseen.',\n    'users_details_desc_no_email' => 'Aseta tälle käyttäjälle näyttönimi, jonka perusteella käyttäjä voidaan tunnistaa.',\n    'users_role' => 'Käyttäjäroolit',\n    'users_role_desc' => 'Valitse, mitä rooleja tälle käyttäjälle annetaan. Jos käyttäjälle on määritetty useita rooleja, näiden roolien käyttöoikeudet yhdistetään ja hän saa kaikki osoitettujen roolien kyvyt.',\n    'users_password' => 'Käyttäjän salasana',\n    'users_password_desc' => 'Aseta salasana, jota käytetään sovellukseen kirjautumiseen. Sen on oltava vähintään 8 merkkiä pitkä.',\n    'users_send_invite_text' => 'Voit lähettää käyttäjälle sähköpostilla kutsun ja antaa käyttäjän asettaa oman salasanansa. Vaihtoehtoisesti voit asettaa salasanan itse.',\n    'users_send_invite_option' => 'Lähetä kutsu',\n    'users_external_auth_id' => 'Ulkoisen tunnistautumisen tunnus',\n    'users_external_auth_id_desc' => 'Tätä tunnusta käytetään BookStack-käyttäjätilin ja ulkoisen tunnistautumisen (kuten SAML2, OIDC tai LDAP) kautta käytettävän tilin yhdistämiseen. Voit jättää tämän kentän huomiotta, jos käytät oletuksena sähköpostipohjaista todennusta.',\n    'users_password_warning' => 'Täytä alla oleva kenttä vain, jos haluat vaihtaa tämän käyttäjän salasanan.',\n    'users_system_public' => 'Tämä käyttäjä tarkoittaa kaikkia vieraita, jotka vierailevat sivustollasi. Sitä ei voi käyttää kirjautumiseen ja se annetaan automaattisesti.',\n    'users_delete' => 'Poista käyttäjä',\n    'users_delete_named' => 'Poista käyttäjä :userName',\n    'users_delete_warning' => 'Tämä poistaa käyttäjän \\':userName\\' kokonaan järjestelmästä.',\n    'users_delete_confirm' => 'Haluatko varmasti poistaa tämän käyttäjän?',\n    'users_migrate_ownership' => 'Omistusoikeuden siirto',\n    'users_migrate_ownership_desc' => 'Valitse käyttäjä, jolle haluat siirtää kaikki poistettavan käyttäjän omistamat sisällöt.',\n    'users_none_selected' => 'Yhtään käyttäjää ei ole valittu',\n    'users_edit' => 'Muokkaa käyttäjää',\n    'users_edit_profile' => 'Muokkaa profiilia',\n    'users_avatar' => 'Käyttäjän kuva',\n    'users_avatar_desc' => 'Valitse käyttäjän kuva. Kuvan tulisi olla noin 256 pikselin kokoinen neliö.',\n    'users_preferred_language' => 'Ensisijainen kieli',\n    'users_preferred_language_desc' => 'Tämä valinta vaihtaa sovelluksen käyttöliittymässä käytettävän kielen. Tämä ei vaikuta käyttäjän luomaan sisältöön.',\n    'users_social_accounts' => 'Sosiaalisen median tilit',\n    'users_social_accounts_desc' => 'Näytä tämän käyttäjän yhdistettyjen sosiaalisen median tilien tila. Sosiaalisen median tilejä voidaan käyttää ensisijaisen tunnistautumistavan ohella.',\n    'users_social_accounts_info' => 'Täällä voit yhdistää muut tilisi ja nopeuttaa kirjautumista. Yhteyden katkaisu tiliin ei peruuta palvelulle annettua käyttöoikeutta. Käyttöoikeus tulee peruuttaa yhdistetyn sosiaalisen median tilin asetuksista.',\n    'users_social_connect' => 'Yhdistä tili',\n    'users_social_disconnect' => 'Katkaise yhteys tiliin',\n    'users_social_status_connected' => 'Yhdistetty',\n    'users_social_status_disconnected' => 'Yhteys katkaistu',\n    'users_social_connected' => ':socialAccount-tili liitettiin onnistuneesti profiiliisi.',\n    'users_social_disconnected' => ':Yhteys socialAccount-tiliin katkaistiin onnistuneesti profiilistasi.',\n    'users_api_tokens' => 'API-tunnisteet',\n    'users_api_tokens_desc' => 'Luo ja hallinnoi tunnisteita, joita käytetään BookStack REST-rajapinnan todennukseen. Rajapinnan käyttöoikeuksia hallinnoidaan sen käyttäjän asetuksista, jolle tunniste kuuluu.',\n    'users_api_tokens_none' => 'Tälle käyttäjälle ei ole luotu API-tunnisteita',\n    'users_api_tokens_create' => 'Luo tunniste',\n    'users_api_tokens_expires' => 'Vanhenee',\n    'users_api_tokens_docs' => 'API-dokumentaatio',\n    'users_mfa' => 'Monivaiheinen tunnistautuminen',\n    'users_mfa_desc' => 'Paranna käyttäjätilisi turvallisuutta ja ota käyttöön monivaiheinen tunnistautuminen.',\n    'users_mfa_x_methods' => ':count menetelmä määritetty|:count menetelmää määritetty',\n    'users_mfa_configure' => 'Määritä menetelmiä',\n\n    // API Tokens\n    'user_api_token_create' => 'Luo uusi API-tunniste',\n    'user_api_token_name' => 'Nimi',\n    'user_api_token_name_desc' => 'Anna tunnisteelle helppolukuinen nimi, josta muistaa sen käyttötarkoituksen.',\n    'user_api_token_expiry' => 'Viimeinen voimassaolopäivä',\n    'user_api_token_expiry_desc' => 'Aseta päiväys, jolloin tämä tunniste vanhenee. Tämän päiväyksen jälkeen tätä tunnistetta käyttäen tehdyt pyynnöt eivät enää toimi. Tämän kentän jättäminen tyhjäksi asettaa voimassaolon päättymisen 100 vuoden päähän tulevaisuuteen.',\n    'user_api_token_create_secret_message' => 'Välittömästi tämän tunnisteen luomisen jälkeen luodaan ja näytetään \"Tunnisteen ID\" ja \"Tunnisteen salaisuus\". Salaisuus näytetään vain kerran, joten kopioi arvo jonnekin turvalliseen paikkaan ennen kuin jatkat.',\n    'user_api_token' => 'API-tunniste',\n    'user_api_token_id' => 'Tunnisteen ID',\n    'user_api_token_id_desc' => 'Tämä on järjestelmän luoma tunniste, jota ei voi muokata ja jota on käytettävä API-pyynnöissä.',\n    'user_api_token_secret' => 'Tunnisteen salaisuus',\n    'user_api_token_secret_desc' => 'Tämä on järjestelmän tälle tunnisteelle luoma salaisuus, jota on käytettävä API-pyynnöissä. Tämä näytetään vain kerran, joten kopioi tämä arvo jonnekin turvalliseen paikkaan.',\n    'user_api_token_created' => 'Tunniste luotu :timeAgo',\n    'user_api_token_updated' => 'Tunniste päivitetty :timeAgo',\n    'user_api_token_delete' => 'Poista tunniste',\n    'user_api_token_delete_warning' => 'Tämä poistaa API-tunnisteen \\':tokenName\\' kokonaan järjestelmästä.',\n    'user_api_token_delete_confirm' => 'Oletko varma, että haluat poistaa tämän API-tunnisteen?',\n\n    // Webhooks\n    'webhooks' => 'Toimintokutsut',\n    'webhooks_index_desc' => 'Toimintokutsut ovat tapa lähettää tietoja ulkoisiin URL-osoitteisiin, kun järjestelmässä tapahtuu tiettyjä toimintoja ja tapahtumia. Tämä mahdollistaa näihin tapahtumiin perustuvan integroinnin muihin alustoihin, esimerkiksi viesti- tai ilmoitusjärjestelmiin.',\n    'webhooks_x_trigger_events' => ':count käynnistävä tapahtuma|:count käynnistävää tapahtumaa',\n    'webhooks_create' => 'Luo uusi toimintokutsu',\n    'webhooks_none_created' => 'Toimintokutsuja ei ole luotu.',\n    'webhooks_edit' => 'Muokkaa toimintokutsua',\n    'webhooks_save' => 'Tallenna toimintokutsu',\n    'webhooks_details' => 'Toimintokutsun tiedot',\n    'webhooks_details_desc' => 'Anna selkeä nimi ja POST-päätepisteen sijainti, johon toimintokutsun tiedot lähetetään.',\n    'webhooks_events' => 'Toimintokutsun tapahtumat',\n    'webhooks_events_desc' => 'Valitse kaikki tapahtumat, jotka käynnistävät tämän toimintokutsun.',\n    'webhooks_events_warning' => 'Huomaa, että toimintokutsu käynnistetään kaikissa valituissa tapahtumissa, vaikka niihin liittyisi muokautettuja käyttöoikeuksia. Varmista, että tämän toimintokutsun käyttö ei paljasta luottamuksellista sisältöä.',\n    'webhooks_events_all' => 'Kaikki järjestelmän tapahtumat',\n    'webhooks_name' => 'Toimintokutsun nimi',\n    'webhooks_timeout' => 'Toimintokutsun aikakatkaisu (sekuntia)',\n    'webhooks_endpoint' => 'Toimintokutsun päätepiste',\n    'webhooks_active' => 'Toimintokutsu aktiivinen',\n    'webhook_events_table_header' => 'Tapahtumat',\n    'webhooks_delete' => 'Poista toimintokutsu',\n    'webhooks_delete_warning' => 'Tämä poistaa toimintokutsun \\':webhookName\\' kokonaan järjestelmästä.',\n    'webhooks_delete_confirm' => 'Oletko varma, että haluat poistaa tämän toimintokutsun?',\n    'webhooks_format_example' => 'Esimerkki toimintokutsusta',\n    'webhooks_format_example_desc' => 'Toimintokutsun tiedot lähetetään JSON-muodossa POST-pyyntönä määritettyyn päätepisteeseen alla näkyvän mallin mukaisesti. Ominaisuudet \"related_item\" ja \"url\" ovat valinnaisia ja riippuvat käynnistetyn tapahtuman tyypistä.',\n    'webhooks_status' => 'Toimintokutsun tila',\n    'webhooks_last_called' => 'Viimeksi kutsuttu:',\n    'webhooks_last_errored' => 'Viimeisin virhe:',\n    'webhooks_last_error_message' => 'Viimeisin virheviesti:',\n\n    // Licensing\n    'licenses' => 'Licenses',\n    'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.',\n    'licenses_bookstack' => 'BookStack License',\n    'licenses_php' => 'PHP Library Licenses',\n    'licenses_js' => 'JavaScript Library Licenses',\n    'licenses_other' => 'Other Licenses',\n    'license_details' => 'License Details',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/fi/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute tulee hyväksyä.',\n    'active_url'           => ':attribute ei ole kelvollinen URL.',\n    'after'                => ':attribute tulee olla päiväyksen :date jälkeinen päiväys.',\n    'alpha'                => ':attribute voi sisältää vain kirjaimia.',\n    'alpha_dash'           => ':attribute voi sisältää vain kirjaimia, numeroita, yhdys- ja alaviivoja.',\n    'alpha_num'            => ':attribute voi sisältää vain kirjaimia ja numeroita.',\n    'array'                => ':attribute tulee olla taulukkomuuttuja.',\n    'backup_codes'         => 'Annettu koodi ei ole kelvollinen tai se on jo käytetty.',\n    'before'               => ':attribute päiväyksen tulee olla ennen :date.',\n    'between'              => [\n        'numeric' => ':attribute tulee olla välillä :min ja :max.',\n        'file'    => ':attribute tulee olla :min - :max kilotavua.',\n        'string'  => ':attribute tulee olla :min - :max merkkiä pitkä.',\n        'array'   => ':attribute tulee sisältää :min - :max kohdetta.',\n    ],\n    'boolean'              => ':attribute tulee olla tosi tai epätosi.',\n    'confirmed'            => ':attribute vahvistus ei täsmää.',\n    'date'                 => ':attribute ei ole kelvollinen päiväys.',\n    'date_format'          => ':attribute ei täsmää muodon :format kanssa.',\n    'different'            => ':attribute ja :other tulee erota toisistaan.',\n    'digits'               => ':attribute tulee olla :digits numeroa pitkä.',\n    'digits_between'       => ':attribute tulee olla :min - :max numeroa.',\n    'email'                => ':attribute tulee olla kelvollinen sähköpostiosoite.',\n    'ends_with' => ':attribute arvon tulee päättyä johonkin seuraavista: :values',\n    'file'                 => ':attribute tulee olla kelvollinen tiedosto.',\n    'filled'               => 'Kenttä :attribute vaaditaan.',\n    'gt'                   => [\n        'numeric' => ':attribute tulee olla suurempi kuin :value.',\n        'file'    => ':attribute tulee olla suurempi kuin :value kilotavua.',\n        'string'  => ':attribute tulee olla suurempi kuin :value merkkiä.',\n        'array'   => ':attribute tulee sisältää vähintään :value kohdetta.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute on oltava suurempi tai samansuuruinen kuin :value.',\n        'file'    => ':attribute on oltava suurempi tai samansuuruinen kuin :value kilotavua.',\n        'string'  => ':attribute on oltava suurempi tai samansuuruinen kuin :value merkkiä.',\n        'array'   => ':attribute tulee sisältää vähintään :value kohdetta tai enemmän.',\n    ],\n    'exists'               => 'Valittu :attribute ei ole kelvollinen.',\n    'image'                => ':attribute on oltava kuva.',\n    'image_extension'      => ':-attribute tulee sisältää kelvollisen ja tuetun kuvan tiedostopäätteen.',\n    'in'                   => 'Valittu :attribute ei ole kelvollinen.',\n    'integer'              => ':attribute tulee olla kokonaisluku.',\n    'ip'                   => ':attribute tulee olla kelvollinen IP-osoite.',\n    'ipv4'                 => ':attribute tulee olla kelvollinen IPv4-osoite.',\n    'ipv6'                 => ':attribute tulee olla kelvollinen IPv6 -osoite.',\n    'json'                 => ':attribute tulee olla kelvollinen JSON-merkkijono.',\n    'lt'                   => [\n        'numeric' => ':attribute tulee olla vähemmän kuin :value.',\n        'file'    => ':attribute tulee olla vähemmän kuin :value kilotavua.',\n        'string'  => ':attribute tulee olla vähemmän kuin :value merkkiä.',\n        'array'   => ':attribute tulee sisältää vähemmän kuin :value kohdetta.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute tulee olla vähemmän tai yhtä suuri kuin :value.',\n        'file'    => ':attribute tulee olla vähemmän tai yhtä suuri kuin :value kilotavua.',\n        'string'  => ':attribute tulee olla vähemmän tai yhtä suuri kuin :value merkkiä.',\n        'array'   => ':attribute ei tule sisältää enempää kuin :value kohdetta.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute ei saa olla suurempi kuin :max.',\n        'file'    => ':attribute ei saa olla suurempi kuin :max kilotavua.',\n        'string'  => ':attribute ei saa olla suurempi kuin :max merkkiä.',\n        'array'   => ':attribute ei saa sisältää enempää kuin :max kohdetta.',\n    ],\n    'mimes'                => ':attribute tulee olla tiedosto jonka tyyppi on :values.',\n    'min'                  => [\n        'numeric' => ':attribute tulee olla vähintään :min.',\n        'file'    => ':attribute tulee olla vähintään :min kilotavua.',\n        'string'  => ':attribute tulee olla vähintään :min merkkiä.',\n        'array'   => ':attribute tulee sisältää vähintään :min kohdetta.',\n    ],\n    'not_in'               => 'Valittu :attribute ei ole kelvollinen.',\n    'not_regex'            => ':attribute muoto ei ole kelvollinen.',\n    'numeric'              => ':attribute tulee olla numero.',\n    'regex'                => ':attribute muoto ei ole kelvollinen.',\n    'required'             => 'Kenttä :attribute vaaditaan.',\n    'required_if'          => 'Kenttä :attribute vaaditaan, kun :other on :value.',\n    'required_with'        => 'Kenttä :attribute vaaditaan, kun :values on määritettynä.',\n    'required_with_all'    => 'Kenttä :attribute vaaditaan, kun kaikki näistä on määritettynä :values.',\n    'required_without'     => 'Kenttä :attribute vaaditaan, kun :values ei ole määritettynä.',\n    'required_without_all' => 'Kenttä :attribute vaaditaan, kun mikään näistä ei ole määritettynä :values.',\n    'same'                 => ':attribute ja :other tulee täsmätä.',\n    'safe_url'             => 'Annettu linkki ei ole mahdollisesti turvallinen.',\n    'size'                 => [\n        'numeric' => ':attribute tulee olla :size.',\n        'file'    => ':attribute tulee olla :size kilotavua.',\n        'string'  => ':attribute tulee olla :size merkkiä.',\n        'array'   => ':attribute tulee sisältää :size kohdetta.',\n    ],\n    'string'               => ':attribute tulee olla merkkijono.',\n    'timezone'             => ':attribute tulee olla kelvollinen aikavyöhyke.',\n    'totp'                 => 'Annettu koodi ei ole kelvollinen tai se on vanhentunut.',\n    'unique'               => ':attribute on jo käytössä.',\n    'url'                  => ':attribute muoto ei ole kelvollinen.',\n    'uploaded'             => 'Tiedostoa ei voitu ladata. Palvelin ei ehkä hyväksy tämän kokoisia tiedostoja.',\n\n    'zip_file' => 'Attribuutin :attribute on viitattava tiedostoon ZIP-tiedoston sisällä.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',\n    'zip_model_expected' => 'Data object expected but \":type\" found.',\n    'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Salasanan vahvistus vaaditaan',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/fr/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'a créé la page',\n    'page_create_notification'    => 'Page créée avec succès',\n    'page_update'                 => 'a modifié la page',\n    'page_update_notification'    => 'Page modifiée avec succès',\n    'page_delete'                 => 'a supprimé la page',\n    'page_delete_notification'    => 'Page supprimée avec succès',\n    'page_restore'                => 'a restauré la page',\n    'page_restore_notification'   => 'Page restaurée avec succès',\n    'page_move'                   => 'a déplacé la page',\n    'page_move_notification'      => 'Page déplacée avec succès',\n\n    // Chapters\n    'chapter_create'              => 'a créé le chapitre',\n    'chapter_create_notification' => 'Chapitre créé avec succès',\n    'chapter_update'              => 'a modifié le chapitre',\n    'chapter_update_notification' => 'Chapitre modifié avec succès',\n    'chapter_delete'              => 'a supprimé le chapitre',\n    'chapter_delete_notification' => 'Chapitre supprimé avec succès',\n    'chapter_move'                => 'a déplacé le chapitre',\n    'chapter_move_notification' => 'Chapitre déplacé avec succès',\n\n    // Books\n    'book_create'                 => 'a créé un livre',\n    'book_create_notification'    => 'Livre créé avec succès',\n    'book_create_from_chapter'              => 'chapitre converti en livre',\n    'book_create_from_chapter_notification' => 'Chapitre converti en livre avec succès',\n    'book_update'                 => 'a modifié le livre',\n    'book_update_notification'    => 'Livre modifié avec succès',\n    'book_delete'                 => 'a supprimé un livre',\n    'book_delete_notification'    => 'Livre supprimé avec succès',\n    'book_sort'                   => 'a réordonné le livre',\n    'book_sort_notification'      => 'Livre restauré avec succès',\n\n    // Bookshelves\n    'bookshelf_create'            => 'a créé l\\'étagère',\n    'bookshelf_create_notification'    => 'Étagère créée avec succès',\n    'bookshelf_create_from_book'    => 'livre converti en étagère',\n    'bookshelf_create_from_book_notification'    => 'Livre converti en étagère avec succès',\n    'bookshelf_update'                 => 'étagère mise à jour',\n    'bookshelf_update_notification'    => 'Étagère mise à jour avec succès',\n    'bookshelf_delete'                 => 'étagère supprimée',\n    'bookshelf_delete_notification'    => 'Étagère supprimée avec succès',\n\n    // Revisions\n    'revision_restore' => 'a restauré la révision',\n    'revision_delete' => 'révision supprimée',\n    'revision_delete_notification' => 'Révision supprimée avec succès',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" a été ajouté dans vos favoris',\n    'favourite_remove_notification' => '\":name\" a été supprimé de vos favoris',\n\n    // Watching\n    'watch_update_level_notification' => 'Préférences de surveillance mises à jour avec succès',\n\n    // Auth\n    'auth_login' => 'connecté',\n    'auth_register' => 'enregistré en tant que nouvel utilisateur',\n    'auth_password_reset_request' => 'demande de réinitialisation du mot de passe',\n    'auth_password_reset_update' => 'réinitialisation du mot de passe',\n    'mfa_setup_method' => 'méthode MFA configurée',\n    'mfa_setup_method_notification' => 'Méthode multi-facteurs configurée avec succès',\n    'mfa_remove_method' => 'méthode MFA supprimée',\n    'mfa_remove_method_notification' => 'Méthode multi-facteurs supprimée avec succès',\n\n    // Settings\n    'settings_update' => 'paramètres mis à jour',\n    'settings_update_notification' => 'Paramètres mis à jour avec succès',\n    'maintenance_action_run' => 'exécuter l\\'action de maintenance',\n\n    // Webhooks\n    'webhook_create' => 'Créer un Webhook',\n    'webhook_create_notification' => 'Webhook créé avec succès',\n    'webhook_update' => 'éditer un Webhook',\n    'webhook_update_notification' => 'Webhook modifié avec succès',\n    'webhook_delete' => 'supprimer un Webhook',\n    'webhook_delete_notification' => 'Webhook supprimé avec succès',\n\n    // Imports\n    'import_create' => 'import créé',\n    'import_create_notification' => 'Importation envoyée avec succès',\n    'import_run' => 'importation mise à jour',\n    'import_run_notification' => 'Contenu importé avec succès',\n    'import_delete' => 'import supprimé',\n    'import_delete_notification' => 'Importation supprimée avec succès',\n\n    // Users\n    'user_create' => 'utilisateur créé',\n    'user_create_notification' => 'Utilisateur créé avec succès',\n    'user_update' => 'utilisateur mis à jour',\n    'user_update_notification' => 'Utilisateur mis à jour avec succès',\n    'user_delete' => 'utilisateur supprimé',\n    'user_delete_notification' => 'Utilisateur supprimé avec succès',\n\n    // API Tokens\n    'api_token_create' => 'a créé un jeton API',\n    'api_token_create_notification' => 'Jeton d\\'API créé avec succès',\n    'api_token_update' => 'a mis à jour un jeton API',\n    'api_token_update_notification' => 'Jeton d\\'API mis à jour avec succès',\n    'api_token_delete' => 'a supprimé un jeton API',\n    'api_token_delete_notification' => 'Jeton d\\'API supprimé avec succès',\n\n    // Roles\n    'role_create' => 'rôle créé',\n    'role_create_notification' => 'Rôle créé avec succès',\n    'role_update' => 'rôle mis à jour',\n    'role_update_notification' => 'Rôle mis à jour avec succès',\n    'role_delete' => 'rôle supprimé',\n    'role_delete_notification' => 'Rôle supprimé avec succès',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'corbeille vidée',\n    'recycle_bin_restore' => 'restauré à partir de la corbeille',\n    'recycle_bin_destroy' => 'supprimé de la corbeille',\n\n    // Comments\n    'commented_on'                => 'a commenté',\n    'comment_create'              => 'Commentaire ajouté',\n    'comment_update'              => 'Commentaire mis à jour',\n    'comment_delete'              => 'Commentaire supprimé',\n\n    // Sort Rules\n    'sort_rule_create' => 'règle de tri crée',\n    'sort_rule_create_notification' => 'Règle de tri crée avec succès',\n    'sort_rule_update' => 'règle de tri mise à jour',\n    'sort_rule_update_notification' => 'Règle de tri mise à jour avec succès',\n    'sort_rule_delete' => 'règle de tri supprimée',\n    'sort_rule_delete_notification' => 'La règle de tri a été supprimée avec succès',\n\n    // Other\n    'permissions_update'          => 'a mis à jour les autorisations sur',\n];\n"
  },
  {
    "path": "lang/fr/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Ces informations ne correspondent à aucun compte.',\n    'throttle' => 'Trop d\\'essais, veuillez réessayer dans :seconds secondes.',\n\n    // Login & Register\n    'sign_up' => 'S\\'inscrire',\n    'log_in' => 'Se connecter',\n    'log_in_with' => 'Se connecter avec :socialDriver',\n    'sign_up_with' => 'S\\'inscrire avec :socialDriver',\n    'logout' => 'Se déconnecter',\n\n    'name' => 'Nom',\n    'username' => 'Nom d\\'utilisateur',\n    'email' => 'E-mail',\n    'password' => 'Mot de passe',\n    'password_confirm' => 'Confirmez le mot de passe',\n    'password_hint' => 'Doit être d\\'au moins 8 caractères',\n    'forgot_password' => 'Mot de passe oublié ?',\n    'remember_me' => 'Se souvenir de moi',\n    'ldap_email_hint' => 'Merci d\\'entrer une adresse e-mail pour ce compte.',\n    'create_account' => 'Créer un compte',\n    'already_have_account' => 'Vous avez déjà un compte ?',\n    'dont_have_account' => 'Vous n\\'avez pas de compte ?',\n    'social_login' => 'Connexion avec un réseau social',\n    'social_registration' => 'Inscription avec un réseau social',\n    'social_registration_text' => 'S\\'inscrire et se connecter avec un réseau social.',\n\n    'register_thanks' => 'Merci pour votre inscription !',\n    'register_confirm' => 'Vérifiez vos e-mails et cliquez sur le lien de confirmation pour rejoindre :appName.',\n    'registrations_disabled' => 'Les inscriptions sont désactivées pour le moment',\n    'registration_email_domain_invalid' => 'Cette adresse e-mail ne peut pas accéder à l\\'application',\n    'register_success' => 'Merci pour votre inscription. Vous êtes maintenant inscrit(e) et connecté(e)',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Tentative de connexion',\n    'auto_init_starting_desc' => 'Nous contactons votre système d\\'authentification pour démarrer le processus de connexion. S\\'il n\\'y a pas de progrès après 5 secondes, vous pouvez essayer de cliquer sur le lien ci-dessous.',\n    'auto_init_start_link' => 'Procéder à l\\'authentification',\n\n    // Password Reset\n    'reset_password' => 'Réinitialiser le mot de passe',\n    'reset_password_send_instructions' => 'Entrez votre adresse e-mail ci-dessous et un e-mail avec un lien de réinitialisation de mot de passe vous sera envoyé.',\n    'reset_password_send_button' => 'Envoyer un lien de réinitialisation',\n    'reset_password_sent' => 'Un lien de réinitialisation du mot de passe sera envoyé à :email si cette adresse e-mail est trouvée dans le système.',\n    'reset_password_success' => 'Votre mot de passe a été réinitialisé avec succès.',\n    'email_reset_subject' => 'Réinitialisez votre mot de passe pour :appName',\n    'email_reset_text' => 'Vous recevez cet e-mail parce que nous avons reçu une demande de réinitialisation pour votre compte.',\n    'email_reset_not_requested' => 'Si vous n\\'avez pas effectué cette demande, vous pouvez ignorer cet e-mail.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Confirmez votre adresse e-mail pour :appName',\n    'email_confirm_greeting' => 'Merci d\\'avoir rejoint :appName !',\n    'email_confirm_text' => 'Merci de confirmer en cliquant sur le lien ci-dessous :',\n    'email_confirm_action' => 'Confirmez votre adresse e-mail',\n    'email_confirm_send_error' => 'La confirmation par e-mail est requise mais le système n\\'a pas pu envoyer l\\'e-mail. Contactez l\\'administrateur système.',\n    'email_confirm_success' => 'Votre adresse e-mail a été confirmée ! Vous devriez maintenant pouvoir vous connecter en utilisant cette adresse e-mail.',\n    'email_confirm_resent' => 'L\\'e-mail de confirmation a été ré-envoyé. Vérifiez votre boîte de réception.',\n    'email_confirm_thanks' => 'Merci d\\'avoir confirmé !',\n    'email_confirm_thanks_desc' => 'Veuillez patienter un moment pendant que votre confirmation est traitée. Si vous n’êtes pas redirigé après 3 secondes, appuyez sur le lien « Continuer » ci-dessous pour continuer.',\n\n    'email_not_confirmed' => 'Adresse e-mail non confirmée',\n    'email_not_confirmed_text' => 'Votre adresse e-mail n\\'a pas été confirmée.',\n    'email_not_confirmed_click_link' => 'Merci de cliquer sur le lien dans l\\'e-mail qui vous a été envoyé après l\\'enregistrement.',\n    'email_not_confirmed_resend' => 'Si vous ne retrouvez plus l\\'e-mail, vous pouvez renvoyer un e-mail de confirmation en utilisant le formulaire ci-dessous.',\n    'email_not_confirmed_resend_button' => 'Renvoyer l\\'e-mail de confirmation',\n\n    // User Invite\n    'user_invite_email_subject' => 'Vous avez été invité(e) à rejoindre :appName !',\n    'user_invite_email_greeting' => 'Un compte vous a été créé sur :appName.',\n    'user_invite_email_text' => 'Cliquez sur le bouton ci-dessous pour renseigner le mot de passe et récupérer l\\'accès :',\n    'user_invite_email_action' => 'Renseignez le mot de passe de votre compte',\n    'user_invite_page_welcome' => 'Bienvenue dans :appName !',\n    'user_invite_page_text' => 'Pour finaliser votre compte et recevoir l\\'accès, vous devez renseigner le mot de passe qui sera utilisé pour la connexion à :appName les prochaines fois.',\n    'user_invite_page_confirm_button' => 'Confirmez le mot de passe',\n    'user_invite_success_login' => 'Mot de passe défini, vous devriez maintenant pouvoir vous connecter en utilisant votre mot de passe défini pour accéder à :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Authentification multi-facteurs',\n    'mfa_setup_desc' => 'Configurer l\\'authentification multi-facteurs ajoute une couche supplémentaire de sécurité à votre compte utilisateur.',\n    'mfa_setup_configured' => 'Déjà configuré',\n    'mfa_setup_reconfigure' => 'Reconfigurer',\n    'mfa_setup_remove_confirmation' => 'Êtes-vous sûr de vouloir supprimer cette méthode d\\'authentification multi-facteurs ?',\n    'mfa_setup_action' => 'Configuration',\n    'mfa_backup_codes_usage_limit_warning' => 'Il vous reste moins de 5 codes de secours, veuillez générer et stocker un nouveau jeu de codes afin d\\'éviter tout verrouillage de votre compte.',\n    'mfa_option_totp_title' => 'Application mobile',\n    'mfa_option_totp_desc' => 'Pour utiliser l\\'authentification multi-facteurs, vous aurez besoin d\\'une application mobile qui supporte TOTP comme Google Authenticator, Authy ou Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Codes de secours',\n    'mfa_option_backup_codes_desc' => 'Génère un ensemble de codes de sauvegarde à usage unique que vous saisirez lors de la connexion pour vérifier votre identité. Veillez à les conserver dans un endroit sûr.',\n    'mfa_gen_confirm_and_enable' => 'Confirmer et activer',\n    'mfa_gen_backup_codes_title' => 'Configuration des codes de secours',\n    'mfa_gen_backup_codes_desc' => 'Stockez la liste des codes ci-dessous dans un endroit sûr. Lorsque vous accédez au système, vous pourrez utiliser l\\'un des codes comme un deuxième mécanisme d\\'authentification.',\n    'mfa_gen_backup_codes_download' => 'Télécharger les codes',\n    'mfa_gen_backup_codes_usage_warning' => 'Chaque code ne peut être utilisé qu\\'une seule fois',\n    'mfa_gen_totp_title' => 'Configuration de l\\'application mobile',\n    'mfa_gen_totp_desc' => 'Pour utiliser l\\'authentification multi-facteurs, vous aurez besoin d\\'une application mobile qui supporte TOTP comme Google Authenticator, Authy ou Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Scannez le QR code ci-dessous avec votre application d\\'authentification préférée pour débuter.',\n    'mfa_gen_totp_verify_setup' => 'Vérifier la configuration',\n    'mfa_gen_totp_verify_setup_desc' => 'Vérifiez que tout fonctionne en utilisant un code généré par votre application d\\'authentification, dans la zone ci-dessous :',\n    'mfa_gen_totp_provide_code_here' => 'Fournissez le code généré par votre application ici',\n    'mfa_verify_access' => 'Vérifier l\\'accès',\n    'mfa_verify_access_desc' => 'Votre compte d\\'utilisateur vous demande de confirmer votre identité par un niveau supplémentaire de vérification avant que vous n\\'ayez accès. Vérifiez-la en utilisant l\\'une de vos méthodes configurées pour continuer.',\n    'mfa_verify_no_methods' => 'Aucune méthode configurée',\n    'mfa_verify_no_methods_desc' => 'Aucune méthode d\\'authentification multi-facteurs n\\'a pu être trouvée pour votre compte. Vous devez configurer au moins une méthode avant d\\'obtenir l\\'accès.',\n    'mfa_verify_use_totp' => 'Vérifier à l\\'aide d\\'une application mobile',\n    'mfa_verify_use_backup_codes' => 'Vérifier en utilisant un code de secours',\n    'mfa_verify_backup_code' => 'Code de secours',\n    'mfa_verify_backup_code_desc' => 'Entrez l\\'un de vos codes de secours restants ci-dessous :',\n    'mfa_verify_backup_code_enter_here' => 'Saisissez un code de secours ici',\n    'mfa_verify_totp_desc' => 'Entrez ci-dessous le code généré à l\\'aide de votre application mobile :',\n    'mfa_setup_login_notification' => 'Méthode multi-facteurs configurée. Veuillez maintenant vous reconnecter en utilisant la méthode configurée.',\n];\n"
  },
  {
    "path": "lang/fr/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Annuler',\n    'close' => 'Fermer',\n    'confirm' => 'Confirmer',\n    'back' => 'Retour',\n    'save' => 'Enregistrer',\n    'continue' => 'Continuer',\n    'select' => 'Sélectionner',\n    'toggle_all' => 'Tout sélectionner',\n    'more' => 'Montrer plus',\n\n    // Form Labels\n    'name' => 'Nom',\n    'description' => 'Description',\n    'role' => 'Rôle',\n    'cover_image' => 'Image de couverture',\n    'cover_image_description' => 'Cette image doit faire environ 440x250px. Elle sera mise à l\\'échelle et recadrée automatiquement pour s\\'adapter à l\\'interface utilisateur, si nécessaire, à différents emplacements.',\n\n    // Actions\n    'actions' => 'Actions',\n    'view' => 'Voir',\n    'view_all' => 'Tout afficher',\n    'new' => 'Nouveau',\n    'create' => 'Créer',\n    'update' => 'Modifier',\n    'edit' => 'Éditer',\n    'archive' => 'Archiver',\n    'unarchive' => 'Désarchiver',\n    'sort' => 'Trier',\n    'move' => 'Déplacer',\n    'copy' => 'Copier',\n    'reply' => 'Répondre',\n    'delete' => 'Supprimer',\n    'delete_confirm' => 'Confirmer la suppression',\n    'search' => 'Rechercher',\n    'search_clear' => 'Réinitialiser la recherche',\n    'reset' => 'Réinitialiser',\n    'remove' => 'Enlever',\n    'add' => 'Ajouter',\n    'configure' => 'Configurer',\n    'manage' => 'Gérer',\n    'fullscreen' => 'Plein écran',\n    'favourite' => 'Favoris',\n    'unfavourite' => 'Supprimer des favoris',\n    'next' => 'Suivant',\n    'previous' => 'Précédent',\n    'filter_active' => 'Filtre actif :',\n    'filter_clear' => 'Effacer le filtre',\n    'download' => 'Télécharger',\n    'open_in_tab' => 'Ouvrir dans un onglet',\n    'open' => 'Ouvert',\n\n    // Sort Options\n    'sort_options' => 'Options de tri',\n    'sort_direction_toggle' => 'Inverser la direction du tri',\n    'sort_ascending' => 'Tri ascendant',\n    'sort_descending' => 'Tri descendant',\n    'sort_name' => 'Nom',\n    'sort_default' => 'Défaut',\n    'sort_created_at' => 'Date de création',\n    'sort_updated_at' => 'Date de mise à jour',\n\n    // Misc\n    'deleted_user' => 'Utilisateur supprimé',\n    'no_activity' => 'Aucune activité',\n    'no_items' => 'Aucun élément',\n    'back_to_top' => 'Retour en haut',\n    'skip_to_main_content' => 'Passer au contenu principal',\n    'toggle_details' => 'Afficher les détails',\n    'toggle_thumbnails' => 'Afficher les vignettes',\n    'details' => 'Détails',\n    'grid_view' => 'Vue en grille',\n    'list_view' => 'Vue en liste',\n    'default' => 'Défaut',\n    'breadcrumb' => 'Fil d\\'Ariane',\n    'status' => 'Statut',\n    'status_active' => 'Actif',\n    'status_inactive' => 'Inactif',\n    'never' => 'Jamais',\n    'none' => 'Aucun',\n\n    // Header\n    'homepage' => 'Accueil',\n    'header_menu_expand' => 'Développer le menu',\n    'profile_menu' => 'Menu du profil',\n    'view_profile' => 'Voir le profil',\n    'edit_profile' => 'Modifier le profil',\n    'dark_mode' => 'Mode sombre',\n    'light_mode' => 'Mode clair',\n    'global_search' => 'Recherche',\n\n    // Layout tabs\n    'tab_info' => 'Informations',\n    'tab_info_label' => 'Onglet : Afficher les informations secondaires',\n    'tab_content' => 'Contenu',\n    'tab_content_label' => 'Onglet : Afficher le contenu principal',\n\n    // Email Content\n    'email_action_help' => 'Si vous rencontrez des problèmes pour cliquer sur le bouton \":actionText\", copiez et collez l\\'adresse ci-dessous dans votre navigateur :',\n    'email_rights' => 'Tous droits réservés',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Politique de confidentialité',\n    'terms_of_service' => 'Conditions d\\'utilisation',\n\n    // OpenSearch\n    'opensearch_description' => 'Recherche :appName',\n];\n"
  },
  {
    "path": "lang/fr/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Sélectionner une image',\n    'image_list' => 'Liste d\\'images',\n    'image_details' => 'Détails de l’Image',\n    'image_upload' => 'Téléverser une image',\n    'image_intro' => 'Ici, vous pouvez sélectionner et gérer les images qui ont été précédemment téléversées sur le système.',\n    'image_intro_upload' => 'Téléversez une nouvelle image en glissant un fichier image dans cette fenêtre, ou en utilisant le bouton \"Téléverser une image\" ci-dessus.',\n    'image_all' => 'Toutes',\n    'image_all_title' => 'Voir toutes les images',\n    'image_book_title' => 'Voir les images ajoutées à ce livre',\n    'image_page_title' => 'Voir les images ajoutées à cette page',\n    'image_search_hint' => 'Rechercher par nom d\\'image',\n    'image_uploaded' => 'Ajoutée le :uploadedDate',\n    'image_uploaded_by' => 'Ajouté par :userName',\n    'image_uploaded_to' => 'Téléversé vers :pagelink',\n    'image_updated' => 'Mis à jour le :updateDate',\n    'image_load_more' => 'Charger plus',\n    'image_image_name' => 'Nom de l\\'image',\n    'image_delete_used' => 'Cette image est utilisée dans les pages ci-dessous.',\n    'image_delete_confirm_text' => 'Êtes-vous sûr de vouloir supprimer cette image ?',\n    'image_select_image' => 'Sélectionner l\\'image',\n    'image_dropzone' => 'Glissez les images ici ou cliquez pour les ajouter',\n    'image_dropzone_drop' => 'Déposer des images ici pour les téléverser',\n    'images_deleted' => 'Images supprimées',\n    'image_preview' => 'Prévisualiser l\\'image',\n    'image_upload_success' => 'Image ajoutée avec succès',\n    'image_update_success' => 'Détails de l\\'image mis à jour',\n    'image_delete_success' => 'Image supprimée avec succès',\n    'image_replace' => 'Remplacer l\\'image',\n    'image_replace_success' => 'Image mise à jour avec succès',\n    'image_rebuild_thumbs' => 'Régénérer les variantes de taille',\n    'image_rebuild_thumbs_success' => 'Les variantes de taille d\\'image ont été reconstruites avec succès !',\n\n    // Code Editor\n    'code_editor' => 'Éditer le code',\n    'code_language' => 'Langage du code',\n    'code_content' => 'Contenu du code',\n    'code_session_history' => 'Historique de session',\n    'code_save' => 'Enregistrer le code',\n];\n"
  },
  {
    "path": "lang/fr/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Général',\n    'advanced' => 'Avancé',\n    'none' => 'Aucun',\n    'cancel' => 'Annuler',\n    'save' => 'Sauvegarder',\n    'close' => 'Fermer',\n    'apply' => 'Appliquer',\n    'undo' => 'Annuler',\n    'redo' => 'Rétablir',\n    'left' => 'Gauche',\n    'center' => 'Centre',\n    'right' => 'Droite',\n    'top' => 'Haut',\n    'middle' => 'Milieu',\n    'bottom' => 'Bas',\n    'width' => 'Largeur',\n    'height' => 'Hauteur',\n    'More' => 'Plus',\n    'select' => 'Sélectionner...',\n\n    // Toolbar\n    'formats' => 'Formats',\n    'header_large' => 'En-tête large',\n    'header_medium' => 'En-tête moyen',\n    'header_small' => 'Petite en-tête',\n    'header_tiny' => 'En-tête minuscule',\n    'paragraph' => 'Paragraphe',\n    'blockquote' => 'Bloc de citation',\n    'inline_code' => 'Ligne de code',\n    'callouts' => 'Légendes',\n    'callout_information' => 'Information',\n    'callout_success' => 'Succès',\n    'callout_warning' => 'Avertissement',\n    'callout_danger' => 'Danger',\n    'bold' => 'Gras',\n    'italic' => 'Italique',\n    'underline' => 'Souligner',\n    'strikethrough' => 'Barré',\n    'superscript' => 'Exposant',\n    'subscript' => 'Indice',\n    'text_color' => 'Couleur de texte',\n    'highlight_color' => 'Couleur de surlignage',\n    'custom_color' => 'Couleur personnalisée',\n    'remove_color' => 'Supprimer la couleur',\n    'background_color' => 'Couleur d\\'arrière-plan',\n    'align_left' => 'Aligner à gauche',\n    'align_center' => 'Aligner au centre',\n    'align_right' => 'Aligner à droite',\n    'align_justify' => 'Justifier',\n    'list_bullet' => 'Liste à puces',\n    'list_numbered' => 'Liste numérotée',\n    'list_task' => 'Liste de tâches',\n    'indent_increase' => 'Augmenter l\\'indentation',\n    'indent_decrease' => 'Diminuer l\\'indentation',\n    'table' => 'Tableau',\n    'insert_image' => 'Insérer une image',\n    'insert_image_title' => 'Insérer/Modifier une image',\n    'insert_link' => 'Insérer/modifier un lien',\n    'insert_link_title' => 'Insérer/Modifier un lien',\n    'insert_horizontal_line' => 'Insérer une ligne horizontale',\n    'insert_code_block' => 'Insérer un bloc de code',\n    'edit_code_block' => 'Modifier le bloc de code',\n    'insert_drawing' => 'Insérer/modifier un dessin',\n    'drawing_manager' => 'Gestionnaire de dessin',\n    'insert_media' => 'Insérer/modifier un média',\n    'insert_media_title' => 'Insérer/Modifier un média',\n    'clear_formatting' => 'Effacer le formatage',\n    'source_code' => 'Code source',\n    'source_code_title' => 'Code source',\n    'fullscreen' => 'Plein écran',\n    'image_options' => 'Options d\\'image',\n\n    // Tables\n    'table_properties' => 'Propriétés du tableau',\n    'table_properties_title' => 'Propriétés du tableau',\n    'delete_table' => 'Supprimer le tableau',\n    'table_clear_formatting' => 'Effacer toute la mise en forme',\n    'resize_to_contents' => 'Redimensionner au contenu',\n    'row_header' => 'En-tête de ligne',\n    'insert_row_before' => 'Insérer une ligne avant',\n    'insert_row_after' => 'Insérer une ligne après',\n    'delete_row' => 'Supprimer ligne',\n    'insert_column_before' => 'Insérer une colonne avant',\n    'insert_column_after' => 'Insérer une colonne après',\n    'delete_column' => 'Supprimer la colonne',\n    'table_cell' => 'Cellule',\n    'table_row' => 'Ligne',\n    'table_column' => 'Colonne',\n    'cell_properties' => 'Propriétés de la cellule',\n    'cell_properties_title' => 'Propriétés de la cellule',\n    'cell_type' => 'Type de cellule',\n    'cell_type_cell' => 'Cellule',\n    'cell_scope' => 'Champ',\n    'cell_type_header' => 'Cellule d\\'en-tête',\n    'merge_cells' => 'Fusionner les cellules',\n    'split_cell' => 'Scinder la cellule',\n    'table_row_group' => 'Groupe de ligne',\n    'table_column_group' => 'Groupe de colonnes',\n    'horizontal_align' => 'Aligner horizontalement',\n    'vertical_align' => 'Aligner verticalement',\n    'border_width' => 'Largeur de bordure',\n    'border_style' => 'Style de bordure',\n    'border_color' => 'Couleur de bordure',\n    'row_properties' => 'Propriétés de la ligne',\n    'row_properties_title' => 'Propriétés de la ligne',\n    'cut_row' => 'Couper la ligne',\n    'copy_row' => 'Copier la ligne',\n    'paste_row_before' => 'Coller la ligne avant',\n    'paste_row_after' => 'Coller la ligne après',\n    'row_type' => 'Type de ligne',\n    'row_type_header' => 'En-tête',\n    'row_type_body' => 'Corps',\n    'row_type_footer' => 'Pied de page',\n    'alignment' => 'Alignement',\n    'cut_column' => 'Couper la colonne',\n    'copy_column' => 'Copier la colonne',\n    'paste_column_before' => 'Coller la colonne avant',\n    'paste_column_after' => 'Coller la colonne après',\n    'cell_padding' => 'Marges intérieures de cellule',\n    'cell_spacing' => 'Espacement entre les cellules',\n    'caption' => 'Légende',\n    'show_caption' => 'Afficher la légende',\n    'constrain' => 'Conserver les proportions',\n    'cell_border_solid' => 'Continu',\n    'cell_border_dotted' => 'En pointillé',\n    'cell_border_dashed' => 'En tirets',\n    'cell_border_double' => 'En double trait',\n    'cell_border_groove' => 'En creux',\n    'cell_border_ridge' => 'En saillie',\n    'cell_border_inset' => 'En 3d lumière basse',\n    'cell_border_outset' => 'En 3d lumière haute',\n    'cell_border_none' => 'Aucun',\n    'cell_border_hidden' => 'Masquée',\n\n    // Images, links, details/summary & embed\n    'source' => 'Source',\n    'alt_desc' => 'Description alternative',\n    'embed' => 'Intégrer',\n    'paste_embed' => 'Collez votre code intégré ci-dessous :',\n    'url' => 'URL',\n    'text_to_display' => 'Texte à afficher',\n    'title' => 'Titre',\n    'browse_links' => 'Parcourir les liens',\n    'open_link' => 'Ouvrir le lien',\n    'open_link_in' => 'Ouvrir le lien dans...',\n    'open_link_current' => 'Fenêtre actuelle',\n    'open_link_new' => 'Nouvelle fenêtre',\n    'remove_link' => 'Retirer le lien',\n    'insert_collapsible' => 'Insérer un bloc repliable',\n    'collapsible_unwrap' => 'Sortir le contenu',\n    'edit_label' => 'Modifier le libellé',\n    'toggle_open_closed' => 'Basculer ouvert/fermé',\n    'collapsible_edit' => 'Modifier un bloc repliable',\n    'toggle_label' => 'Activer/désactiver le libellé',\n\n    // About view\n    'about' => 'À propos de l\\'éditeur',\n    'about_title' => 'À propos de l\\'éditeur WYSIWYG',\n    'editor_license' => 'Licence d\\'éditeur et droit d\\'auteur',\n    'editor_lexical_license' => 'Cet éditeur est construit comme un fork de :lexicalLink qui est distribué sous licence MIT.',\n    'editor_lexical_license_link' => 'Vous trouverez ici tous les détails de la licence.',\n    'editor_tiny_license' => 'Cet éditeur est construit en utilisant :tinyLink qui est fourni sous la licence MIT.',\n    'editor_tiny_license_link' => 'Vous trouverez ici les détails sur les droits d\\'auteur et les licences de TinyMCE.',\n    'save_continue' => 'Enregistrer et continuer',\n    'callouts_cycle' => '(Continuez d\\'appuyer pour basculer entre les types)',\n    'link_selector' => 'Lien vers le contenu',\n    'shortcuts' => 'Raccourcis',\n    'shortcut' => 'Raccourci',\n    'shortcuts_intro' => 'Les raccourcis suivants sont disponibles dans l\\'éditeur :',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Description',\n];\n"
  },
  {
    "path": "lang/fr/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Créé récemment',\n    'recently_created_pages' => 'Pages créées récemment',\n    'recently_updated_pages' => 'Pages mises à jour récemment',\n    'recently_created_chapters' => 'Chapitres créés récemment',\n    'recently_created_books' => 'Livres créés récemment',\n    'recently_created_shelves' => 'Étagères créées récemment',\n    'recently_update' => 'Mis à jour récemment',\n    'recently_viewed' => 'Vus récemment',\n    'recent_activity' => 'Activité récente',\n    'create_now' => 'En créer une maintenant',\n    'revisions' => 'Révisions',\n    'meta_revision' => 'Révision #:revisionCount',\n    'meta_created' => 'Créé :timeLength',\n    'meta_created_name' => 'Créé :timeLength par :user',\n    'meta_updated' => 'Mis à jour :timeLength',\n    'meta_updated_name' => 'Mis à jour :timeLength par :user',\n    'meta_owned_name' => 'Appartient à :user',\n    'meta_reference_count' => 'Référencé sur :count élément|Référencé sur :count éléments',\n    'entity_select' => 'Sélectionner l\\'entité',\n    'entity_select_lack_permission' => 'Vous n\\'avez pas les permissions requises pour sélectionner cet élément',\n    'images' => 'Images',\n    'my_recent_drafts' => 'Mes brouillons récents',\n    'my_recently_viewed' => 'Vus récemment',\n    'my_most_viewed_favourites' => 'Mes favoris les plus vus',\n    'my_favourites' => 'Mes favoris',\n    'no_pages_viewed' => 'Vous n\\'avez rien visité récemment',\n    'no_pages_recently_created' => 'Aucune page créée récemment',\n    'no_pages_recently_updated' => 'Aucune page mise à jour récemment',\n    'export' => 'Exporter',\n    'export_html' => 'Fichiers web',\n    'export_pdf' => 'Fichier PDF',\n    'export_text' => 'Document texte',\n    'export_md' => 'Fichiers Markdown',\n    'export_zip' => 'Export ZIP',\n    'default_template' => 'Modèle de page par défaut',\n    'default_template_explain' => 'Sélectionnez un modèle de page qui sera utilisé comme contenu par défaut pour les nouvelles pages créées dans cet élément. Gardez à l\\'esprit que le modèle ne sera utilisé que si le créateur de la page a accès au modèle sélectionné.',\n    'default_template_select' => 'Sélectionnez un modèle de page',\n    'import' => 'Importation',\n    'import_validate' => 'Valider l\\'import',\n    'import_desc' => 'Importez des livres, des chapitres et des pages à l\\'aide d\\'un export zip à partir de la même instance ou d\\'une instance différente. Sélectionnez un fichier ZIP pour continuer. Une fois le fichier téléchargé et validé, vous pourrez configurer et confirmer l\\'importation dans la vue suivante.',\n    'import_zip_select' => 'Sélectionnez le fichier ZIP à télécharger',\n    'import_zip_validation_errors' => 'Des erreurs ont été détectées lors de la validation du fichier ZIP fourni:',\n    'import_pending' => 'Importations en attente',\n    'import_pending_none' => 'Aucune importation n\\'a été commencée.',\n    'import_continue' => 'Continuer l\\'importation',\n    'import_continue_desc' => 'Examinez le contenu à importer à partir du fichier ZIP téléchargé. Lorsque vous êtes prêt, lancez l\\'importation pour ajouter son contenu à ce système. Le fichier d\\'importation ZIP téléchargé sera automatiquement supprimé si l\\'importation est réussie.',\n    'import_details' => 'Détails de l\\'importation',\n    'import_run' => 'Exécuter Importation',\n    'import_size' => ':size Taille du fichier ZIP à importer',\n    'import_uploaded_at' => ':relativeTime téléchargé',\n    'import_uploaded_by' => 'Téléchargé par',\n    'import_location' => 'Emplacement de l\\'importation',\n    'import_location_desc' => 'Sélectionnez un emplacement cible pour votre contenu importé. Vous aurez besoin des autorisations appropriées pour créer dans l\\'emplacement que vous choisissez.',\n    'import_delete_confirm' => 'Êtes-vous sûr de vouloir supprimer cette importation ?',\n    'import_delete_desc' => 'Ceci supprimera le fichier ZIP importé et ne pourra pas être annulé.',\n    'import_errors' => 'Erreurs d\\'importation',\n    'import_errors_desc' => 'Les erreurs suivantes se sont produites lors de la tentative d\\'importation :',\n    'breadcrumb_siblings_for_page' => 'Naviguer entre les pages voisines',\n    'breadcrumb_siblings_for_chapter' => 'Naviguer entre les chapitres voisins',\n    'breadcrumb_siblings_for_book' => 'Naviguer entre les livres voisins',\n    'breadcrumb_siblings_for_bookshelf' => 'Naviguer entre les étagères voisines',\n\n    // Permissions and restrictions\n    'permissions' => 'Autorisations',\n    'permissions_desc' => 'Définissez ici les permissions pour remplacer les permissions par défaut fournies par les rôles d\\'utilisateur.',\n    'permissions_book_cascade' => 'Les permissions définies sur les livres seront automatiquement mises en cascade dans les chapitres enfants et les pages, à moins qu\\'elles aient leurs propres permissions définies.',\n    'permissions_chapter_cascade' => 'Les permissions définies sur les chapitres seront automatiquement mises en cascade sur les pages enfants, à moins qu\\'elles aient leurs propres permissions définies.',\n    'permissions_save' => 'Enregistrer les permissions',\n    'permissions_owner' => 'Propriétaire',\n    'permissions_role_everyone_else' => 'Tous les autres',\n    'permissions_role_everyone_else_desc' => 'Définir les permissions pour tous les rôles qui ne sont pas spécifiquement remplacés.',\n    'permissions_role_override' => 'Remplacer les permissions pour le rôle',\n    'permissions_inherit_defaults' => 'Hériter les valeurs par défaut',\n\n    // Search\n    'search_results' => 'Résultats de recherche',\n    'search_total_results_found' => ':count résultats trouvés|:count résultats trouvés au total',\n    'search_clear' => 'Réinitialiser la recherche',\n    'search_no_pages' => 'Aucune page correspondant à cette recherche',\n    'search_for_term' => 'recherche pour :term',\n    'search_more' => 'Plus de résultats',\n    'search_advanced' => 'Recherche avancée',\n    'search_terms' => 'Termes de recherche',\n    'search_content_type' => 'Type de contenu',\n    'search_exact_matches' => 'Correspondances exactes',\n    'search_tags' => 'Recherche par tags',\n    'search_options' => 'Options',\n    'search_viewed_by_me' => 'Vu par moi',\n    'search_not_viewed_by_me' => 'Non vu par moi',\n    'search_permissions_set' => 'Ensemble d\\'autorisations',\n    'search_created_by_me' => 'Créé par moi',\n    'search_updated_by_me' => 'Mis à jour par moi',\n    'search_owned_by_me' => 'Créés par moi',\n    'search_date_options' => 'Recherche par date',\n    'search_updated_before' => 'Mis à jour avant',\n    'search_updated_after' => 'Mis à jour après',\n    'search_created_before' => 'Créé avant',\n    'search_created_after' => 'Créé après',\n    'search_set_date' => 'Choisir la date',\n    'search_update' => 'Actualiser la recherche',\n\n    // Shelves\n    'shelf' => 'Étagère',\n    'shelves' => 'Étagères',\n    'x_shelves' => ':count Étagère|:count Étagères',\n    'shelves_empty' => 'Aucune étagère n\\'a été créée',\n    'shelves_create' => 'Créer une nouvelle étagère',\n    'shelves_popular' => 'Étagères populaires',\n    'shelves_new' => 'Nouvelles étagères',\n    'shelves_new_action' => 'Nouvelle étagère',\n    'shelves_popular_empty' => 'Les étagères les plus populaires apparaîtront ici.',\n    'shelves_new_empty' => 'Les étagères les plus récentes apparaitront ici.',\n    'shelves_save' => 'Enregistrer l\\'étagère',\n    'shelves_books' => 'Livres sur cette étagère',\n    'shelves_add_books' => 'Ajouter des livres sur cette étagère',\n    'shelves_drag_books' => 'Déposez des livres ici pour les ajouter a cette étagère',\n    'shelves_empty_contents' => 'Aucun livre n\\'a été assigné à cette étagère',\n    'shelves_edit_and_assign' => 'Modifier cette étagère pour y ajouter des livres',\n    'shelves_edit_named' => 'Modifier l\\'étagère :name',\n    'shelves_edit' => 'Modifier l\\'étagère',\n    'shelves_delete' => 'Supprimer l\\'étagère',\n    'shelves_delete_named' => 'Supprimer l\\'étagère :name',\n    'shelves_delete_explain' => \"Ceci va supprimer l'étagère nommée ':name'. Les livres contenus dans cette étagère ne seront pas supprimés.\",\n    'shelves_delete_confirmation' => 'Êtes-vous sûr(e) de vouloir supprimer cette étagère ?',\n    'shelves_permissions' => 'Enregistrer les permissions',\n    'shelves_permissions_updated' => 'Permissions de l\\'étagère mises à jour',\n    'shelves_permissions_active' => 'Permissions de l\\'étagère activées',\n    'shelves_permissions_cascade_warning' => 'Les permissions sur les étagères ne sont pas automatiquement recopiées aux livres qu\\'elles contiennent, car un livre peut exister dans plusieurs étagères. Les permissions peuvent cependant être recopiées vers les livres contenus en utilisant l\\'option ci-dessous.',\n    'shelves_permissions_create' => 'Les permissions de création d\\'une étagère sont uniquement utilisées pour copier les permissions vers les livres enfants en utilisant l\\'action ci-dessous. Elles ne contrôlent pas la possibilité de créer des livres.',\n    'shelves_copy_permissions_to_books' => 'Copier les permissions vers les livres',\n    'shelves_copy_permissions' => 'Copier les permissions',\n    'shelves_copy_permissions_explain' => 'Ceci va appliquer les permissions actuelles de cette étagère à tous les livres qu\\'elle contient. Avant de continuer, assurez-vous que toutes les permissions de cette étagère ont été sauvegardées.',\n    'shelves_copy_permission_success' => 'Permissions de l\\'étagère transférées à :count livres',\n\n    // Books\n    'book' => 'Livre',\n    'books' => 'Livres',\n    'x_books' => ':count livre|:count livres',\n    'books_empty' => 'Aucun livre n\\'a été créé',\n    'books_popular' => 'Livres populaires',\n    'books_recent' => 'Livres récents',\n    'books_new' => 'Nouveaux livres',\n    'books_new_action' => 'Nouveau livre',\n    'books_popular_empty' => 'Les livres les plus populaires apparaîtront ici.',\n    'books_new_empty' => 'Les livres les plus récents apparaitront ici.',\n    'books_create' => 'Créer un nouveau livre',\n    'books_delete' => 'Supprimer un livre',\n    'books_delete_named' => 'Supprimer le livre :bookName',\n    'books_delete_explain' => 'Ceci va supprimer le livre nommé \\':bookName\\', tous les chapitres et pages seront supprimés.',\n    'books_delete_confirmation' => 'Êtes-vous sûr(e) de vouloir supprimer ce livre ?',\n    'books_edit' => 'Modifier le livre',\n    'books_edit_named' => 'Modifier le livre :bookName',\n    'books_form_book_name' => 'Nom du livre',\n    'books_save' => 'Enregistrer le livre',\n    'books_permissions' => 'Permissions du livre',\n    'books_permissions_updated' => 'Permissions du livre mises à jour',\n    'books_empty_contents' => 'Aucune page ou chapitre n\\'a été ajouté à ce livre.',\n    'books_empty_create_page' => 'Créer une nouvelle page',\n    'books_empty_sort_current_book' => 'Trier les pages du livre',\n    'books_empty_add_chapter' => 'Ajouter un chapitre',\n    'books_permissions_active' => 'Permissions de livre actives',\n    'books_search_this' => 'Rechercher dans ce livre',\n    'books_navigation' => 'Navigation dans le livre',\n    'books_sort' => 'Trier les contenus du livre',\n    'books_sort_desc' => 'Déplacer les pages et chapitres au sein d’un livre pour en réorganiser le contenu. D’autres livres peuvent être ajoutés pour faciliter le déplacement des pages et chapitres entre les livres. Facultativement, une règle de tri automatique peut être mise en place afin de trier le livre lorsqu’il est édité.',\n    'books_sort_auto_sort' => 'Option de tri automatique',\n    'books_sort_auto_sort_active' => 'Tri automatique actif : :sortName',\n    'books_sort_named' => 'Trier le livre :bookName',\n    'books_sort_name' => 'Trier par le nom',\n    'books_sort_created' => 'Trier par la date de création',\n    'books_sort_updated' => 'Trier par la date de mise à jour',\n    'books_sort_chapters_first' => 'Les chapitres en premier',\n    'books_sort_chapters_last' => 'Les chapitres en dernier',\n    'books_sort_show_other' => 'Afficher d\\'autres livres',\n    'books_sort_save' => 'Enregistrer l\\'ordre',\n    'books_sort_show_other_desc' => 'Ajoutez ici d\\'autres livres pour les inclure dans l\\'opération de tri, et permettez une réorganisation des livres croisés.',\n    'books_sort_move_up' => 'Remonter',\n    'books_sort_move_down' => 'Descendre',\n    'books_sort_move_prev_book' => 'Déplacer vers le livre précédent',\n    'books_sort_move_next_book' => 'Déplacer vers le livre suivant',\n    'books_sort_move_prev_chapter' => 'Déplacer vers le chapitre précédent',\n    'books_sort_move_next_chapter' => 'Déplacer au chapitre suivant',\n    'books_sort_move_book_start' => 'Déplacer au début du livre',\n    'books_sort_move_book_end' => 'Déplacer vers la fin du livre',\n    'books_sort_move_before_chapter' => 'Déplacer vers Avant le chapitre',\n    'books_sort_move_after_chapter' => 'Déplacer vers Après le chapitre',\n    'books_copy' => 'Copier le livre',\n    'books_copy_success' => 'Livre copié avec succès',\n\n    // Chapters\n    'chapter' => 'Chapitre',\n    'chapters' => 'Chapitres',\n    'x_chapters' => ':count chapitre|:count chapitres',\n    'chapters_popular' => 'Chapitres populaires',\n    'chapters_new' => 'Nouveau chapitre',\n    'chapters_create' => 'Créer un nouveau chapitre',\n    'chapters_delete' => 'Supprimer le chapitre',\n    'chapters_delete_named' => 'Supprimer le chapitre :chapterName',\n    'chapters_delete_explain' => 'Ceci supprimera le chapitre portant le nom \\':chapterName\\'. Toutes les pages qui existent dans ce chapitre seront également supprimées.',\n    'chapters_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer ce chapitre ?',\n    'chapters_edit' => 'Modifier le chapitre',\n    'chapters_edit_named' => 'Modifier le chapitre :chapterName',\n    'chapters_save' => 'Enregistrer le chapitre',\n    'chapters_move' => 'Déplacer le chapitre',\n    'chapters_move_named' => 'Déplacer le chapitre :chapterName',\n    'chapters_copy' => 'Copier le chapitre',\n    'chapters_copy_success' => 'Chapitre copié avec succès',\n    'chapters_permissions' => 'Permissions du chapitre',\n    'chapters_empty' => 'Il n\\'y a pas de page dans ce chapitre actuellement.',\n    'chapters_permissions_active' => 'Permissions du chapitre activées',\n    'chapters_permissions_success' => 'Permissions du chapitre mises à jour',\n    'chapters_search_this' => 'Rechercher dans ce chapitre',\n    'chapter_sort_book' => 'Trier le livre',\n\n    // Pages\n    'page' => 'Page',\n    'pages' => 'Pages',\n    'x_pages' => ':count Page|:count pages',\n    'pages_popular' => 'Pages populaires',\n    'pages_new' => 'Nouvelle page',\n    'pages_attachments' => 'Fichiers joints',\n    'pages_navigation' => 'Navigation dans la page',\n    'pages_delete' => 'Supprimer la page',\n    'pages_delete_named' => 'Supprimer la page :pageName',\n    'pages_delete_draft_named' => 'supprimer le brouillon de la page :pageName',\n    'pages_delete_draft' => 'Supprimer le brouillon',\n    'pages_delete_success' => 'Page supprimée',\n    'pages_delete_draft_success' => 'Brouillon supprimé',\n    'pages_delete_warning_template' => 'Cette page actuellement utilisée comme modèle de page par défaut de livre ou de chapitre. Ces livres ou chapitres n\\'auront plus de modèle de page par défaut assigné après la suppression de cette page.',\n    'pages_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cette page ?',\n    'pages_delete_draft_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer ce brouillon ?',\n    'pages_editing_named' => 'Modification de la page :pageName',\n    'pages_edit_draft_options' => 'Options du brouillon',\n    'pages_edit_save_draft' => 'Enregistrer le brouillon',\n    'pages_edit_draft' => 'Modifier le brouillon',\n    'pages_editing_draft' => 'Modification du brouillon',\n    'pages_editing_page' => 'Modification de la page',\n    'pages_edit_draft_save_at' => 'Brouillon enregistré à ',\n    'pages_edit_delete_draft' => 'Supprimer le brouillon',\n    'pages_edit_delete_draft_confirm' => 'Êtes-vous sûr de vouloir supprimer vos modifications de page brouillon ? Toutes vos modifications, depuis la dernière sauvegarde complète, seront perdues et l\\'éditeur sera mis à jour avec l\\'état de sauvegarde de la dernière page non-brouillon.',\n    'pages_edit_discard_draft' => 'Jeter le brouillon',\n    'pages_edit_switch_to_markdown' => 'Basculer vers l\\'éditeur Markdown',\n    'pages_edit_switch_to_markdown_clean' => '(Contenu nettoyé)',\n    'pages_edit_switch_to_markdown_stable' => '(Contenu stable)',\n    'pages_edit_switch_to_wysiwyg' => 'Basculer vers l\\'éditeur WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg' => 'Basculer vers le nouveau WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(En bêta-test)',\n    'pages_edit_set_changelog' => 'Journal des changements',\n    'pages_edit_enter_changelog_desc' => 'Entrez une brève description des changements effectués',\n    'pages_edit_enter_changelog' => 'Saisir les changements',\n    'pages_editor_switch_title' => 'Changer d\\'éditeur',\n    'pages_editor_switch_are_you_sure' => 'Êtes-vous sûr de vouloir modifier l\\'éditeur de cette page ?',\n    'pages_editor_switch_consider_following' => 'Considérez ce qui suit lors du changement d\\'éditeur :',\n    'pages_editor_switch_consideration_a' => 'Une fois enregistrée, le nouvel éditeur sera utilisé par tous les futurs éditeurs, y compris ceux qui ne seront pas en mesure de modifier le type de l\\'éditeur eux-mêmes.',\n    'pages_editor_switch_consideration_b' => 'Cela peut entraîner une perte de détail et de syntaxe dans certaines circonstances.',\n    'pages_editor_switch_consideration_c' => 'Les modifications apportées depuis la dernière sauvegarde, les balises ou le journal des modifications ne persisteront pas à travers cette modification.',\n    'pages_save' => 'Enregistrer la page',\n    'pages_title' => 'Titre de la page',\n    'pages_name' => 'Nom de la page',\n    'pages_md_editor' => 'Éditeur',\n    'pages_md_preview' => 'Prévisualisation',\n    'pages_md_insert_image' => 'Insérer une image',\n    'pages_md_insert_link' => 'Insérer un lien',\n    'pages_md_insert_drawing' => 'Insérer un dessin',\n    'pages_md_show_preview' => 'Prévisualisation',\n    'pages_md_sync_scroll' => 'Défilement prévisualisation',\n    'pages_md_plain_editor' => 'Éditeur texte brut',\n    'pages_drawing_unsaved' => 'Dessin non enregistré trouvé',\n    'pages_drawing_unsaved_confirm' => 'Des données de dessin non enregistrées ont été trouvées à partir d\\'une tentative de sauvegarde de dessin échouée. Voulez-vous restaurer et continuer à modifier ce dessin non sauvegardé ?',\n    'pages_not_in_chapter' => 'La page n\\'est pas dans un chapitre',\n    'pages_move' => 'Déplacer la page',\n    'pages_copy' => 'Copier la page',\n    'pages_copy_desination' => 'Destination de la copie',\n    'pages_copy_success' => 'Page copiée avec succès',\n    'pages_permissions' => 'Permissions de la page',\n    'pages_permissions_success' => 'Permissions de la page mises à jour',\n    'pages_revision' => 'Révision',\n    'pages_revisions' => 'Révisions de la page',\n    'pages_revisions_desc' => 'Vous trouverez sur la page ci-dessous toutes les anciennes révisions. Vous pouvez regarder, comparer et restaurer les anciennes versions de page si les autorisations le permettent. L’historique complet de la page peut ne pas être entièrement affiché ici, car selon la configuration du système, les anciennes révisions peuvent être supprimées automatiquement.',\n    'pages_revisions_named' => 'Révisions pour :pageName',\n    'pages_revision_named' => 'Révision pour :pageName',\n    'pages_revision_restored_from' => 'Restauré à partir de #:id; :summary',\n    'pages_revisions_created_by' => 'Créé par',\n    'pages_revisions_date' => 'Date de révision',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Numéro de révision',\n    'pages_revisions_numbered' => 'Révision #:id',\n    'pages_revisions_numbered_changes' => 'Modification #:id',\n    'pages_revisions_editor' => 'Type d\\'éditeur',\n    'pages_revisions_changelog' => 'Journal des changements',\n    'pages_revisions_changes' => 'Changements',\n    'pages_revisions_current' => 'Version actuelle',\n    'pages_revisions_preview' => 'Prévisualisation',\n    'pages_revisions_restore' => 'Restaurer',\n    'pages_revisions_none' => 'Cette page n\\'a aucune révision',\n    'pages_copy_link' => 'Copier le lien',\n    'pages_edit_content_link' => 'Aller à la section dans l\\'éditeur',\n    'pages_pointer_enter_mode' => 'Entrer en mode de sélection de section',\n    'pages_pointer_label' => 'Options de section de page',\n    'pages_pointer_permalink' => 'Lien permanent de la section de page',\n    'pages_pointer_include_tag' => 'Balise d\\'inclusion de la section de page',\n    'pages_pointer_toggle_link' => 'Mode Lien Permanent, Cliquer pour afficher la balise d\\'inclusion',\n    'pages_pointer_toggle_include' => 'Mode balise d\\'inclusion, cliquer pour afficher le lien permanent',\n    'pages_permissions_active' => 'Permissions de page actives',\n    'pages_initial_revision' => 'Publication initiale',\n    'pages_references_update_revision' => 'Mise à jour automatique des liens internes',\n    'pages_initial_name' => 'Nouvelle page',\n    'pages_editing_draft_notification' => 'Vous éditez actuellement un brouillon qui a été enregistré :timeDiff.',\n    'pages_draft_edited_notification' => 'La page a été mise à jour depuis votre dernière visite. Vous devriez jeter ce brouillon.',\n    'pages_draft_page_changed_since_creation' => 'Cette page a été mise à jour depuis que ce brouillon a été créé. Il est recommandé de supprimer ce brouillon ou de veiller à ne pas écraser toute modification de page.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count utilisateurs ont commencé à éditer cette page',\n        'start_b' => ':userName a commencé à éditer cette page',\n        'time_a' => 'depuis la dernière sauvegarde',\n        'time_b' => 'dans les :minCount dernières minutes',\n        'message' => ':start :time. Attention à ne pas écraser les mises à jour de quelqu\\'un d\\'autre !',\n    ],\n    'pages_draft_discarded' => 'Brouillon annulé ! L\\'éditeur a été mis à jour avec le contenu de la page actuelle',\n    'pages_draft_deleted' => 'Brouillon supprimé ! L\\'éditeur a été mis à jour avec le contenu de la page actuelle',\n    'pages_specific' => 'Page spécifique',\n    'pages_is_template' => 'Modèle de page',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Afficher/masquer la barre latérale',\n    'page_tags' => 'Étiquettes de la page',\n    'chapter_tags' => 'Étiquettes du chapitre',\n    'book_tags' => 'Étiquettes du livre',\n    'shelf_tags' => 'Étiquettes de l\\'étagère',\n    'tag' => 'Étiquette',\n    'tags' =>  'Étiquettes',\n    'tags_index_desc' => 'Les étiquettes peuvent être mises sur le contenu pour appliquer une forme flexible de catégorisation. Les étiquettes peuvent avoir à la fois une clé et une valeur, la valeur étant facultative. Une fois appliqué, le contenu peut ensuite être interrogé à l’aide du nom et de la valeur de l’étiquette.',\n    'tag_name' =>  'Nom de l’étiquette',\n    'tag_value' => 'Valeur du mot-clé (optionnel)',\n    'tags_explain' => \"Ajouter des mots-clés pour catégoriser votre contenu.\",\n    'tags_add' => 'Ajouter un autre mot-clé',\n    'tags_remove' => 'Supprimer le mot-clé',\n    'tags_usages' => 'Total des utilisations des mots-clés',\n    'tags_assigned_pages' => 'Attribuer aux pages',\n    'tags_assigned_chapters' => 'Attribuer aux chapitres',\n    'tags_assigned_books' => 'Attribuer aux livres',\n    'tags_assigned_shelves' => 'Attribuer aux étagères',\n    'tags_x_unique_values' => ':count valeurs uniques',\n    'tags_all_values' => 'Toutes les valeurs',\n    'tags_view_tags' => 'Voir les mots-clés',\n    'tags_view_existing_tags' => 'Voir les mots-clés existants',\n    'tags_list_empty_hint' => 'Les mots-clés peuvent être assignés via la barre latérale de l\\'éditeur de page ou lors de l\\'édition des détails d\\'un livre, d\\'un chapitre ou d\\'une étagère.',\n    'attachments' => 'Fichiers joints',\n    'attachments_explain' => 'Ajouter des fichiers ou des liens pour les afficher sur votre page. Ils seront affichés dans la barre latérale.',\n    'attachments_explain_instant_save' => 'Ces changements sont enregistrés immédiatement.',\n    'attachments_upload' => 'Téléverser un fichier',\n    'attachments_link' => 'Attacher un lien',\n    'attachments_upload_drop' => 'Vous pouvez également glisser-déposer un fichier ici pour le téléverser en tant que pièce jointe.',\n    'attachments_set_link' => 'Définir un lien',\n    'attachments_delete' => 'Êtes-vous sûr de vouloir supprimer la pièce jointe ?',\n    'attachments_dropzone' => 'Déposer des fichiers ici pour les téléverser',\n    'attachments_no_files' => 'Aucun fichier ajouté',\n    'attachments_explain_link' => 'Vous pouvez ajouter un lien si vous ne souhaitez pas téléverser un fichier.',\n    'attachments_link_name' => 'Nom du lien',\n    'attachment_link' => 'Lien de l\\'attachement',\n    'attachments_link_url' => 'Lien sur un fichier',\n    'attachments_link_url_hint' => 'URL du site ou du fichier',\n    'attach' => 'Ajouter',\n    'attachments_insert_link' => 'Ajouter un lien à la page',\n    'attachments_edit_file' => 'Modifier le fichier',\n    'attachments_edit_file_name' => 'Nom du fichier',\n    'attachments_edit_drop_upload' => 'Glissez un fichier ou cliquer pour mettre à jour le fichier',\n    'attachments_order_updated' => 'Ordre des fichiers joints mis à jour',\n    'attachments_updated_success' => 'Détails des fichiers joints mis à jour',\n    'attachments_deleted' => 'Fichier joint supprimé',\n    'attachments_file_uploaded' => 'Fichier ajouté avec succès',\n    'attachments_file_updated' => 'Fichier mis à jour avec succès',\n    'attachments_link_attached' => 'Lien attaché à la page avec succès',\n    'templates' => 'Modèles',\n    'templates_set_as_template' => 'La page est un modèle',\n    'templates_explain_set_as_template' => 'Vous pouvez définir cette page comme modèle pour que son contenu soit utilisé lors de la création d\\'autres pages. Les autres utilisateurs pourront utiliser ce modèle s\\'ils ont les permissions pour cette page.',\n    'templates_replace_content' => 'Remplacer le contenu de la page',\n    'templates_append_content' => 'Ajouter après le contenu de la page',\n    'templates_prepend_content' => 'Ajouter avant le contenu de la page',\n\n    // Profile View\n    'profile_user_for_x' => 'Utilisateur depuis :time',\n    'profile_created_content' => 'Contenu créé',\n    'profile_not_created_pages' => ':userName n\\'a pas créé de page',\n    'profile_not_created_chapters' => ':userName n\\'a pas créé de chapitre',\n    'profile_not_created_books' => ':userName n\\'a pas créé de livre',\n    'profile_not_created_shelves' => ':userName n\\'a pas créé d\\'étagère',\n\n    // Comments\n    'comment' => 'Commentaire',\n    'comments' => 'Commentaires',\n    'comment_add' => 'Ajouter un commentaire',\n    'comment_none' => 'Aucun commentaire à afficher',\n    'comment_placeholder' => 'Entrez vos commentaires ici',\n    'comment_thread_count' => ':count Fil de commentaires|:count Fils de commentaires',\n    'comment_archived_count' => ':count Archivé',\n    'comment_archived_threads' => 'Fils archivés',\n    'comment_save' => 'Enregistrer le commentaire',\n    'comment_new' => 'Nouveau commentaire',\n    'comment_created' => 'commenté :createDiff',\n    'comment_updated' => 'Mis à jour :updateDiff par :username',\n    'comment_updated_indicator' => 'Mis à jour',\n    'comment_deleted_success' => 'Commentaire supprimé',\n    'comment_created_success' => 'Commentaire ajouté',\n    'comment_updated_success' => 'Commentaire mis à jour',\n    'comment_archive_success' => 'Commentaire archivé',\n    'comment_unarchive_success' => 'Commentaire désarchiver',\n    'comment_view' => 'Voir le commentaire',\n    'comment_jump_to_thread' => 'Aller au fil',\n    'comment_delete_confirm' => 'Êtes-vous sûr de vouloir supprimer ce commentaire ?',\n    'comment_in_reply_to' => 'En réponse à :commentId',\n    'comment_reference' => 'Référence',\n    'comment_reference_outdated' => '(Obsolète)',\n    'comment_editor_explain' => 'Voici les commentaires qui ont été laissés sur cette page. Les commentaires peuvent être ajoutés et gérés en visualisant la page enregistrée.',\n\n    // Revision\n    'revision_delete_confirm' => 'Êtes-vous sûr de vouloir supprimer cette révision ?',\n    'revision_restore_confirm' => 'Êtes-vous sûr de vouloir restaurer cette révision ? Le contenu courant de la page va être remplacé.',\n    'revision_cannot_delete_latest' => 'Impossible de supprimer la dernière révision.',\n\n    // Copy view\n    'copy_consider' => 'Veuillez prendre en compte ce qui suit lors de la copie du contenu.',\n    'copy_consider_permissions' => 'Les paramètres de permission personnalisés ne seront pas copiés.',\n    'copy_consider_owner' => 'Vous deviendrez le propriétaire de tout le contenu copié.',\n    'copy_consider_images' => 'Les fichiers image de la page ne seront pas dupliqués et les images originales conserveront leur relation avec la page vers laquelle elles ont été initialement téléchargées.',\n    'copy_consider_attachments' => 'Les pièces jointes de la page ne seront pas copiées.',\n    'copy_consider_access' => 'Un changement d\\'emplacement, de propriétaire ou d\\'autorisation peut rendre ce contenu accessible à ceux précédemment sans accès.',\n\n    // Conversions\n    'convert_to_shelf' => 'Convertir en étagère',\n    'convert_to_shelf_contents_desc' => 'Vous pouvez convertir ce livre en une nouvelle étagère avec le même contenu. Les chapitres contenus dans ce livre seront convertis en nouveaux livres. Si ce livre contient des pages, qui ne sont pas dans un chapitre, ce livre sera renommé et contiendra ces pages, et ce livre fera partie de la nouvelle étagère.',\n    'convert_to_shelf_permissions_desc' => 'Toutes les autorisations définies sur ce livre seront copiées sur la nouvelle étagère et sur tous les nouveaux livres enfants qui n\\'ont pas leurs propres permissions appliquées. Notez que les permissions sur les étagères ne font pas automatiquement cascade au contenu intérieur, comme elles le font pour les livres.',\n    'convert_book' => 'Convertir le livre',\n    'convert_book_confirm' => 'Êtes-vous sûr(e) de vouloir convertir ce livre ?',\n    'convert_undo_warning' => 'Cela ne peut pas être facilement annulé.',\n    'convert_to_book' => 'Convertir en livre',\n    'convert_to_book_desc' => 'Vous pouvez convertir ce chapitre en un nouveau livre avec le même contenu. Toutes les permissions définies dans ce chapitre seront copiées dans le nouveau livre mais toutes les permissions héritées du livre parent ne seront pas copiés, ce qui pourrait conduire à un changement de contrôle d\\'accès.',\n    'convert_chapter' => 'Convertir le chapitre',\n    'convert_chapter_confirm' => 'Êtes-vous sûr(e) de vouloir convertir ce chapitre ?',\n\n    // References\n    'references' => 'Références',\n    'references_none' => 'Il n\\'y a pas de références suivies à cet élément.',\n    'references_to_desc' => 'Vous trouverez ci-dessous le contenu connu du système qui a un lien vers cet élément.',\n\n    // Watch Options\n    'watch' => 'Suivre',\n    'watch_title_default' => 'Préférences par défaut',\n    'watch_desc_default' => 'Revenir à vos préférences de notification par défaut.',\n    'watch_title_ignore' => 'Ignorer',\n    'watch_desc_ignore' => 'Ignorer toutes les notifications, y compris celles des préférences de niveau utilisateur.',\n    'watch_title_new' => 'Nouvelles Pages',\n    'watch_desc_new' => 'Notifier quand une nouvelle page est créée dans cet élément.',\n    'watch_title_updates' => 'Toutes les mises à jour de page',\n    'watch_desc_updates' => 'Notifier toutes les nouvelles pages et les changements de page.',\n    'watch_desc_updates_page' => 'Notifier lors de toutes les modifications de page.',\n    'watch_title_comments' => 'Toutes les mises à jour et commentaires de page',\n    'watch_desc_comments' => 'Notifier toutes les nouvelles pages, les changements de page et les nouveaux commentaires.',\n    'watch_desc_comments_page' => 'Notifier les changements de page et les nouveaux commentaires.',\n    'watch_change_default' => 'Modifier les préférences de notification par défaut',\n    'watch_detail_ignore' => 'Ignorer les notifications',\n    'watch_detail_new' => 'Suivre les nouvelles pages',\n    'watch_detail_updates' => 'Suivre les nouvelles pages et mises à jour',\n    'watch_detail_comments' => 'Suivre les nouvelles pages, mises à jour et commentaires',\n    'watch_detail_parent_book' => 'Suivre via le livre parent',\n    'watch_detail_parent_book_ignore' => 'Ignorer via le livre parent',\n    'watch_detail_parent_chapter' => 'Suivre via le chapitre parent',\n    'watch_detail_parent_chapter_ignore' => 'Ignorer via le chapitre parent',\n];\n"
  },
  {
    "path": "lang/fr/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Vous n\\'avez pas les droits pour accéder à cette page.',\n    'permissionJson' => 'Vous n\\'avez pas les droits pour exécuter cette action.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Un utilisateur avec l\\'adresse :email existe déjà.',\n    'auth_pre_register_theme_prevention' => 'Le compte utilisateur n\\'a pas pu être enregistré avec les informations fournies',\n    'email_already_confirmed' => 'Cet e-mail a déjà été validé, vous pouvez vous connecter.',\n    'email_confirmation_invalid' => 'Cette confirmation est invalide. Veuillez essayer de vous inscrire à nouveau.',\n    'email_confirmation_expired' => 'Le jeton de confirmation a expiré. Un nouvel e-mail vous a été envoyé.',\n    'email_confirmation_awaiting' => 'L\\'adresse e-mail du compte utilisé doit être confirmée',\n    'ldap_fail_anonymous' => 'L\\'accès LDAP anonyme n\\'a pas abouti',\n    'ldap_fail_authed' => 'L\\'accès LDAP n\\'a pas abouti avec cet utilisateur et ce mot de passe',\n    'ldap_extension_not_installed' => 'L\\'extension PHP LDAP n\\'est pas installée',\n    'ldap_cannot_connect' => 'Impossible de se connecter au serveur LDAP, la connexion initiale a échoué',\n    'saml_already_logged_in' => 'Déjà connecté',\n    'saml_no_email_address' => 'Impossible de trouver une adresse e-mail, pour cet utilisateur, dans les données fournies par le système d\\'authentification externe',\n    'saml_invalid_response_id' => 'La requête du système d\\'authentification externe n\\'est pas reconnue par un processus démarré par cette application. Naviguer après une connexion peut causer ce problème.',\n    'saml_fail_authed' => 'Connexion avec :system échouée, le système n\\'a pas fourni l\\'autorisation réussie',\n    'oidc_already_logged_in' => 'Déjà connecté',\n    'oidc_no_email_address' => 'Impossible de trouver une adresse e-mail pour cet utilisateur, dans les données fournies par le système d\\'authentification externe',\n    'oidc_fail_authed' => 'La connexion en utilisant :system a échoué, le système n\\'a pas fourni d\\'autorisation avec succès',\n    'social_no_action_defined' => 'Pas d\\'action définie',\n    'social_login_bad_response' => \"Erreur pendant la tentative de connexion à :socialAccount : \\n:error\",\n    'social_account_in_use' => 'Ce compte :socialAccount est déjà utilisé. Essayez de vous connecter via :socialAccount.',\n    'social_account_email_in_use' => 'L\\'email :email est déjà utilisé. Si vous avez déjà un compte :socialAccount, vous pouvez le rattacher à votre profil existant.',\n    'social_account_existing' => 'Ce compte :socialAccount est déjà rattaché à votre profil.',\n    'social_account_already_used_existing' => 'Ce compte :socialAccount est déjà utilisé par un autre utilisateur.',\n    'social_account_not_used' => 'Ce compte :socialAccount n\\'est lié à aucun utilisateur. ',\n    'social_account_register_instructions' => 'Si vous n\\'avez pas encore de compte, vous pouvez en créer un avec l\\'option :socialAccount.',\n    'social_driver_not_found' => 'Pilote de compte de réseaux sociaux absent',\n    'social_driver_not_configured' => 'Vos préférences pour le compte :socialAccount sont incorrectes.',\n    'invite_token_expired' => 'Le lien de cette invitation a expiré. Vous pouvez essayer de réinitialiser votre mot de passe.',\n    'login_user_not_found' => 'Impossible de trouver un utilisateur pour cette action.',\n\n    // System\n    'path_not_writable' => 'Impossible d\\'écrire dans :filePath. Assurez-vous d\\'avoir les droits d\\'écriture sur le serveur',\n    'cannot_get_image_from_url' => 'Impossible de récupérer l\\'image depuis :url',\n    'cannot_create_thumbs' => 'Le serveur ne peut pas créer de miniature, vérifier que l\\'extension PHP GD est installée.',\n    'server_upload_limit' => 'La taille du fichier est trop grande.',\n    'server_post_limit' => 'Le serveur ne peut pas recevoir la quantité de données fournie. Réessayez avec moins de données ou un fichier plus petit.',\n    'uploaded'  => 'Le serveur n\\'autorise pas l\\'envoi d\\'un fichier de cette taille. Veuillez essayer avec une taille de fichier réduite.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Une erreur est survenue pendant l\\'envoi de l\\'image',\n    'image_upload_type_error' => 'Le format de l\\'image envoyée n\\'est pas valide',\n    'image_upload_replace_type' => 'Le fichier image doit être remplacé par une image du même type',\n    'image_upload_memory_limit' => 'Impossible de gérer le chargement de l\\'image et/ou la création de miniatures en raison de limitations de ressources du système.',\n    'image_thumbnail_memory_limit' => 'Impossible de créer les variations de taille d\\'image en raison des limitations de ressources du système.',\n    'image_gallery_thumbnail_memory_limit' => 'Impossible de créer les vignettes de la galerie en raison de limitations de ressources du système.',\n    'drawing_data_not_found' => 'Les données de dessin n\\'ont pas pu être chargées. Le fichier de dessin peut ne plus exister ou vous n\\'avez pas la permission d\\'y accéder.',\n\n    // Attachments\n    'attachment_not_found' => 'Fichier joint non trouvé',\n    'attachment_upload_error' => 'Une erreur s\\'est produite avec le téléversement du fichier joint',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Le brouillon n\\'a pas pu être enregistré. Vérifiez votre connexion internet',\n    'page_draft_delete_fail' => 'Impossible de supprimer le brouillon et de récupérer le contenu sauvegardé de la page actuelle',\n    'page_custom_home_deletion' => 'Impossible de supprimer une page définie comme page d\\'accueil',\n\n    // Entities\n    'entity_not_found' => 'Entité non trouvée',\n    'bookshelf_not_found' => 'Étagère introuvable',\n    'book_not_found' => 'Livre non trouvé',\n    'page_not_found' => 'Page non trouvée',\n    'chapter_not_found' => 'Chapitre non trouvé',\n    'selected_book_not_found' => 'Ce livre n\\'a pas été trouvé',\n    'selected_book_chapter_not_found' => 'Ce livre ou chapitre n\\'a pas été trouvé',\n    'guests_cannot_save_drafts' => 'Les invités ne peuvent pas enregistrer de brouillons',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Vous ne pouvez pas supprimer le dernier administrateur',\n    'users_cannot_delete_guest' => 'Vous ne pouvez pas supprimer l\\'utilisateur invité',\n    'users_could_not_send_invite' => 'Impossible de créer l\\'utilisateur à cause d\\'une erreur d\\'envoi de l\\'email d\\'invitation',\n\n    // Roles\n    'role_cannot_be_edited' => 'Ce rôle ne peut pas être modifié',\n    'role_system_cannot_be_deleted' => 'Ceci est un rôle du système et ne peut pas être supprimé',\n    'role_registration_default_cannot_delete' => 'Ce rôle ne peut pas être supprimé tant qu\\'il est le rôle par défaut',\n    'role_cannot_remove_only_admin' => 'Ceci est le seul compte administrateur. Assignez un nouvel administrateur avant de le supprimer ici.',\n\n    // Comments\n    'comment_list' => 'Une erreur s\\'est produite lors de la récupération des commentaires.',\n    'cannot_add_comment_to_draft' => 'Vous ne pouvez pas ajouter de commentaires à un brouillon.',\n    'comment_add' => 'Une erreur s\\'est produite lors de l\\'ajout du commentaire.',\n    'comment_delete' => 'Une erreur s\\'est produite lors de la suppression du commentaire.',\n    'empty_comment' => 'Impossible d\\'ajouter un commentaire vide.',\n\n    // Error pages\n    '404_page_not_found' => 'Page non trouvée',\n    'sorry_page_not_found' => 'Désolé, cette page n\\'a pas pu être trouvée.',\n    'sorry_page_not_found_permission_warning' => 'Si cette page est censée exister, il se peut que vous n\\'ayez pas l\\'autorisation de la consulter.',\n    'image_not_found' => 'Image non trouvée',\n    'image_not_found_subtitle' => 'Désolé, l\\'image que vous cherchez ne peut être trouvée.',\n    'image_not_found_details' => 'Si cette image était censée exister, il se pourrait qu\\'elle ait été supprimée.',\n    'return_home' => 'Retour à l\\'accueil',\n    'error_occurred' => 'Une erreur est survenue',\n    'app_down' => ':appName n\\'est pas en service pour le moment',\n    'back_soon' => 'Nous serons bientôt de retour.',\n\n    // Import\n    'import_zip_cant_read' => 'Impossible de lire le fichier ZIP.',\n    'import_zip_cant_decode_data' => 'Impossible de trouver et de décoder le contenu ZIP data.json.',\n    'import_zip_no_data' => 'Les données du fichier ZIP n\\'ont pas de livre, de chapitre ou de page attendus.',\n    'import_zip_data_too_large' => 'Le contenu du fichier ZIP pour data.json dépasse la taille maximale de téléversement autorisée.',\n    'import_validation_failed' => 'L\\'importation du ZIP n\\'a pas été validée avec les erreurs :',\n    'import_zip_failed_notification' => 'Impossible d\\'importer le fichier ZIP.',\n    'import_perms_books' => 'Vous n\\'avez pas les permissions requises pour créer des livres.',\n    'import_perms_chapters' => 'Vous n\\'avez pas les permissions requises pour créer des chapitres.',\n    'import_perms_pages' => 'Vous n\\'avez pas les permissions requises pour créer des pages.',\n    'import_perms_images' => 'Vous n\\'avez pas les permissions requises pour créer des images.',\n    'import_perms_attachments' => 'Vous n\\'avez pas les permissions requises pour créer des pièces jointes.',\n\n    // API errors\n    'api_no_authorization_found' => 'Aucun jeton d\\'autorisation trouvé pour la demande',\n    'api_bad_authorization_format' => 'Un jeton d\\'autorisation a été trouvé pour la requête, mais le format semble incorrect',\n    'api_user_token_not_found' => 'Aucun jeton API correspondant n\\'a été trouvé pour le jeton d\\'autorisation fourni',\n    'api_incorrect_token_secret' => 'Le secret fourni pour le jeton d\\'API utilisé est incorrect',\n    'api_user_no_api_permission' => 'Le propriétaire du jeton API utilisé n\\'a pas la permission de passer des requêtes API',\n    'api_user_token_expired' => 'Le jeton d\\'autorisation utilisé a expiré',\n    'api_cookie_auth_only_get' => 'Seules les requêtes GET sont autorisées lors de l’utilisation de l’API avec une authentification basée sur les cookies',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Erreur émise lors de l\\'envoi d\\'un e-mail de test :',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'L\\'URL ne correspond pas aux hôtes SSR autorisés configurés',\n];\n"
  },
  {
    "path": "lang/fr/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Nouveau commentaire sur la page : :pageName',\n    'new_comment_intro' => 'Un utilisateur a commenté une page dans :appName:',\n    'new_page_subject' => 'Nouvelle page: :pageName',\n    'new_page_intro' => 'Une nouvelle page a été créée dans :appName:',\n    'updated_page_subject' => 'Page mise à jour: :pageName',\n    'updated_page_intro' => 'Une page a été mise à jour dans :appName:',\n    'updated_page_debounce' => 'Pour éviter de nombreuses notifications, pendant un certain temps, vous ne recevrez pas de notifications pour d\\'autres modifications de cette page par le même éditeur.',\n    'comment_mention_subject' => 'Vous avez été mentionné dans un commentaire sur la page : :pageName',\n    'comment_mention_intro' => 'Vous avez été mentionné dans un commentaire sur :appName:',\n\n    'detail_page_name' => 'Nom de la page :',\n    'detail_page_path' => 'Chemin de la page :',\n    'detail_commenter' => 'Commenta·teur·trice :',\n    'detail_comment' => 'Commentaire :',\n    'detail_created_by' => 'Créé par :',\n    'detail_updated_by' => 'Mis à jour par :',\n\n    'action_view_comment' => 'Voir le commentaire',\n    'action_view_page' => 'Afficher la page',\n\n    'footer_reason' => 'Cette notification vous a été envoyée car :link couvre ce type d\\'activité pour cet élément.',\n    'footer_reason_link' => 'vos préférences de notification',\n];\n"
  },
  {
    "path": "lang/fr/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Précédent',\n    'next'     => 'Suivant &raquo;',\n\n];\n"
  },
  {
    "path": "lang/fr/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Les mots de passe doivent faire au moins 8 caractères et correspondre à la confirmation.',\n    'user' => \"Nous n'avons pas trouvé d'utilisateur avec cette adresse e-mail.\",\n    'token' => 'Le jeton de réinitialisation du mot de passe n\\'est pas valide pour cette adresse e-mail.',\n    'sent' => 'Nous vous avons envoyé un lien de réinitialisation de mot de passe par e-mail !',\n    'reset' => 'Votre mot de passe a été réinitialisé !',\n\n];\n"
  },
  {
    "path": "lang/fr/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Mon compte',\n\n    'shortcuts' => 'Raccourcis',\n    'shortcuts_interface' => 'Préférences de raccourcis de l\\'interface utilisateur',\n    'shortcuts_toggle_desc' => 'Ici vous pouvez activer ou désactiver les raccourcis clavier, utilisés pour la navigation et les actions.',\n    'shortcuts_customize_desc' => 'Vous pouvez personnaliser chaque raccourci ci-dessous. Il vous suffit d\\'appuyer sur la combinaison de touche choisie après avoir sélectionné l\\'entrée pour un raccourci.',\n    'shortcuts_toggle_label' => 'Raccourcis clavier activés',\n    'shortcuts_section_navigation' => 'Navigation',\n    'shortcuts_section_actions' => 'Actions communes',\n    'shortcuts_save' => 'Sauvegarder les raccourcis',\n    'shortcuts_overlay_desc' => 'Note : Lorsque les raccourcis sont activés, assistant est disponible en appuyant sur \"?\" qui mettra en surbrillance les raccourcis disponibles pour les actions actuellement visibles à l\\'écran.',\n    'shortcuts_update_success' => 'Les préférences de raccourci ont été mises à jour !',\n    'shortcuts_overview_desc' => 'Gérer les raccourcis clavier que vous pouvez utiliser pour naviguer dans l\\'interface utilisateur du système.',\n\n    'notifications' => 'Préférences de notification',\n    'notifications_desc' => 'Contrôlez les notifications par e-mail que vous recevez lorsque certaines activités sont effectuées dans le système.',\n    'notifications_opt_own_page_changes' => 'Notifier lors des modifications des pages que je possède',\n    'notifications_opt_own_page_comments' => 'Notifier lorsque les pages que je possède sont commentées',\n    'notifications_opt_comment_mentions' => 'Notifier lorsque je suis mentionné dans un commentaire',\n    'notifications_opt_comment_replies' => 'Notifier les réponses à mes commentaires',\n    'notifications_save' => 'Enregistrer les préférences',\n    'notifications_update_success' => 'Les préférences de notification ont été mises à jour !',\n    'notifications_watched' => 'Éléments surveillés et ignorés',\n    'notifications_watched_desc' => 'Voici les éléments qui ont des préférences de surveillance personnalisées appliquées. Pour mettre à jour vos préférences pour celles-ci, consultez l\\'élément puis trouvez les options de surveillance dans la barre latérale.',\n\n    'auth' => 'Accès et sécurité',\n    'auth_change_password' => 'Changer le mot de passe',\n    'auth_change_password_desc' => 'Changez le mot de passe que vous utilisez pour vous connecter à l\\'application. Il doit comporter au moins 8 caractères.',\n    'auth_change_password_success' => 'Le mot de passe a été mis à jour !',\n\n    'profile' => 'Détails du profil',\n    'profile_desc' => 'Gérez les détails de votre compte qui représentent comment les autres utilisateurs vous voient, en plus d\\'autres détails utilisés pour la communication et la personnalisation du système.',\n    'profile_view_public' => 'Voir le profil public',\n    'profile_name_desc' => 'Configurez votre nom d\\'affichage qui sera visible par les autres utilisateurs à travers l\\'activité que vous effectuez ainsi que le contenu que vous possédez.',\n    'profile_email_desc' => 'Ce courriel sera utilisé pour les notifications et, en fonction de l\\'authentification active du système, l\\'accès au système.',\n    'profile_email_no_permission' => 'Malheureusement, vous n\\'avez pas la permission de changer votre adresse e-mail. Si vous souhaitez la modifier, demandez à un administrateur de le faire pour vous.',\n    'profile_avatar_desc' => 'Sélectionnez une image qui sera utilisée pour vous représenter aux autres utilisateurs. Idéalement, cette image devrait être carrée et d\\'environ 256 pixels de largeur et de hauteur.',\n    'profile_admin_options' => 'Options Administrateur',\n    'profile_admin_options_desc' => 'Des options administrateurs supplémentaires, comme celles permettant de gérer les affectations de rôles, peuvent être trouvées pour votre compte dans la zone \"Paramètres > Utilisateurs\" de l\\'application.',\n\n    'delete_account' => 'Supprimer le compte',\n    'delete_my_account' => 'Supprimer mon compte',\n    'delete_my_account_desc' => 'Cela supprimera complètement votre compte utilisateur du système. Vous ne pourrez pas récupérer ce compte ou annuler cette action. Le contenu que vous avez créé, comme les pages créées et les images téléchargées, sera sauvegardé.',\n    'delete_my_account_warning' => 'Êtes-vous sûr(e) de vouloir supprimer votre compte ?',\n];\n"
  },
  {
    "path": "lang/fr/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Préférences',\n    'settings_save' => 'Enregistrer les préférences',\n    'system_version' => 'Version du système',\n    'categories' => 'Catégories',\n\n    // App Settings\n    'app_customization' => 'Personnalisation',\n    'app_features_security' => 'Fonctionnalités et sécurité',\n    'app_name' => 'Nom de l\\'application',\n    'app_name_desc' => 'Ce nom est affiché dans l\\'en-tête et les e-mails.',\n    'app_name_header' => 'Afficher le nom dans l\\'en-tête',\n    'app_public_access' => 'Accès public',\n    'app_public_access_desc' => 'L\\'activation de cette option permettra aux visiteurs, qui ne sont pas connectés, d\\'accéder au contenu de votre instance BookStack.',\n    'app_public_access_desc_guest' => 'L\\'accès pour les visiteurs publics peut être contrôlé par l\\'utilisateur \"Guest\".',\n    'app_public_access_toggle' => 'Autoriser l\\'accès public',\n    'app_public_viewing' => 'Accepter l\\'affichage public des pages ?',\n    'app_secure_images' => 'Ajout d\\'image sécurisé',\n    'app_secure_images_toggle' => 'Activer l\\'ajout d\\'image sécurisée',\n    'app_secure_images_desc' => 'Pour des questions de performances, toutes les images sont publiques. Cette option ajoute une chaîne aléatoire difficile à deviner dans les URLs des images.',\n    'app_default_editor' => 'Éditeur de page par défaut',\n    'app_default_editor_desc' => 'Sélectionnez l\\'éditeur qui sera utilisé par défaut lors de l\\'édition de nouvelles pages. Cela peut être remplacé au niveau de la page où les permissions sont autorisées.',\n    'app_custom_html' => 'HTML personnalisé dans l\\'en-tête',\n    'app_custom_html_desc' => 'Le contenu inséré ici sera ajouté en bas de la balise <head> de toutes les pages. Vous pouvez l\\'utiliser pour ajouter du CSS personnalisé ou un tracker analytique.',\n    'app_custom_html_disabled_notice' => 'Le contenu de l\\'en-tête HTML personnalisé est désactivé sur cette page de paramètres pour garantir que les modifications les plus récentes puissent être annulées.',\n    'app_logo' => 'Logo de l\\'application',\n    'app_logo_desc' => 'Celui-ci est utilisé dans la barre d\\'en-tête de l\\'application, entre autres zones. L\\'image doit être de 86 px de hauteur. Les plus grandes images seront réduites.',\n    'app_icon' => 'Icône de l\\'application',\n    'app_icon_desc' => 'Cette icône est utilisée pour les onglets du navigateur et les icônes de raccourci. Doit être une image PNG carrée de 256 px.',\n    'app_homepage' => 'Page d\\'accueil de l\\'application',\n    'app_homepage_desc' => 'Choisissez une page à afficher sur la page d\\'accueil au lieu de la vue par défaut. Les permissions sont ignorées pour les pages sélectionnées.',\n    'app_homepage_select' => 'Choisissez une page',\n    'app_footer_links' => 'Liens de pied de page',\n    'app_footer_links_desc' => 'Ajoutez des liens dans le pied de page du site. Ils seront affichés en bas de la plupart des pages, incluant celles qui ne nécesittent pas de connexion. Vous pouvez utiliser l\\'étiquette \"trans::<key>\" pour utiliser les traductions définies par le système. Par exemple, utiliser \"trans::common.privacy_policy\" fournira la traduction de \"Politique de Confidentalité\" et \"trans::common.terms_of_service\" fournira la traduction de \"Conditions d\\'utilisation\".',\n    'app_footer_links_label' => 'Libellé du lien',\n    'app_footer_links_url' => 'URL du lien',\n    'app_footer_links_add' => 'Ajouter un lien en pied de page',\n    'app_disable_comments' => 'Désactiver les commentaires',\n    'app_disable_comments_toggle' => 'Désactiver les commentaires',\n    'app_disable_comments_desc' => 'Désactive les commentaires sur toutes les pages de l\\'application. Les commentaires existants ne sont pas affichés.',\n\n    // Color settings\n    'color_scheme' => 'Schéma de couleurs de l\\'application',\n    'color_scheme_desc' => 'Défini les couleurs à utiliser dans l\\'interface utilisateur de l\\'application. Les couleurs peuvent être configurées séparément pour les modes sombre et clair pour mieux correspondre au thème et assurer la lisibilité.',\n    'ui_colors_desc' => 'Défini la couleur primaire de l\\'application et la couleur de lien par défaut. La couleur primaire est principalement utilisée pour la bannière d\\'en-tête, les boutons et les décorations de l\\'interface. La couleur par défaut du lien est utilisée pour les liens et les actions basées sur le texte, à la fois dans le contenu écrit et dans l\\'interface de l\\'application.',\n    'app_color' => 'Couleur primaire',\n    'link_color' => 'Couleur de lien par défaut',\n    'content_colors_desc' => 'Défini les couleurs pour tous les éléments de la hiérarchie d\\'organisation des pages. Choisir les couleurs avec une luminosité similaire aux couleurs par défaut est recommandé pour la lisibilité.',\n    'bookshelf_color' => 'Couleur des étagères',\n    'book_color' => 'Couleur des livres',\n    'chapter_color' => 'Couleur des chapitres',\n    'page_color' => 'Couleur des pages',\n    'page_draft_color' => 'Couleur des brouillons',\n\n    // Registration Settings\n    'reg_settings' => 'Préférence pour l\\'inscription',\n    'reg_enable' => 'Activer l\\'inscription',\n    'reg_enable_toggle' => 'Activer l\\'inscription',\n    'reg_enable_desc' => 'Lorsque l\\'inscription est activée, l\\'utilisateur pourra s\\'enregistrer en tant qu\\'utilisateur de l\\'application. Lors de l\\'inscription, ils se voient attribuer un rôle par défaut.',\n    'reg_default_role' => 'Rôle par défaut lors de l\\'inscription',\n    'reg_enable_external_warning' => 'L\\'option ci-dessus est ignorée lorsque l\\'authentification externe LDAP ou SAML est activée. Les comptes utilisateur pour les membres non existants seront créés automatiquement si l\\'authentification, par rapport au système externe utilisé, est réussie.',\n    'reg_email_confirmation' => 'Confirmation de l\\'e-mail',\n    'reg_email_confirmation_toggle' => 'Obliger la confirmation par e-mail ?',\n    'reg_confirm_email_desc' => 'Si la restriction de domaine est activée, la confirmation sera automatiquement obligatoire et cette valeur sera ignorée.',\n    'reg_confirm_restrict_domain' => 'Restreindre l\\'inscription à un domaine',\n    'reg_confirm_restrict_domain_desc' => 'Entrez une liste de domaines acceptés lors de l\\'inscription, séparés par une virgule. Les utilisateurs recevront un e-mail de confirmation à cette adresse. <br> Les utilisateurs pourront changer leur adresse après inscription s\\'ils le souhaitent.',\n    'reg_confirm_restrict_domain_placeholder' => 'Aucune restriction en place',\n\n    // Sorting Settings\n    'sorting' => 'Listes et tri',\n    'sorting_book_default' => 'Tri des livres par défaut',\n    'sorting_book_default_desc' => 'Sélectionnez le tri par défaut à mettre en place sur les nouveaux livres. Cela n’affectera pas les livres existants, et peut être redéfini dans les livres.',\n    'sorting_rules' => 'Règles de tri',\n    'sorting_rules_desc' => 'Ce sont les opérations de tri qui peuvent être appliquées au contenu du système.',\n    'sort_rule_assigned_to_x_books' => 'Assignée à :count livre|Assignée à :count livres',\n    'sort_rule_create' => 'Créer une règle de tri',\n    'sort_rule_edit' => 'Modifier une règle de tri',\n    'sort_rule_delete' => 'Supprimer une règle de tri',\n    'sort_rule_delete_desc' => 'Supprimer cette règle du système. Les livres l’utilisant précédemment reviendront au tri manuel.',\n    'sort_rule_delete_warn_books' => 'Cette règle est actuellement utilisée par :count livre(s). Êtes-vous sûr·e de vouloir la supprimer ?',\n    'sort_rule_delete_warn_default' => 'Cette règle est actuellement utilisée par défaut pour les livres. Êtes-vous sûr·e de vouloir la supprimer ?',\n    'sort_rule_details' => 'Détails de la Règle de Tri',\n    'sort_rule_details_desc' => 'Assignez un nom à cette règle, qui apparaîtra dans les listes servant à sélectionner une règle de tri.',\n    'sort_rule_operations' => 'Opérations de tri',\n    'sort_rule_operations_desc' => 'Configurez les actions de tri à appliquer en les déplaçant depuis la liste d’opérations. Lors de l’usage de cette règle, les opérations seront appliquées dans l’ordre, de haut en bas. Les changements effectués ici seront appliqués à tous les livres utilisant cette règle lors de la sauvegarde.',\n    'sort_rule_available_operations' => 'Opérations disponibles',\n    'sort_rule_available_operations_empty' => 'Aucune opération restante',\n    'sort_rule_configured_operations' => 'Opérations sélectionnées',\n    'sort_rule_configured_operations_empty' => 'Déplacez/ajoutez des opérations depuis la liste \"Opérations disponibles\"',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Nom - Alphabétique',\n    'sort_rule_op_name_numeric' => 'Nom - Numérique',\n    'sort_rule_op_created_date' => 'Date de création',\n    'sort_rule_op_updated_date' => 'Date de mise à jour',\n    'sort_rule_op_chapters_first' => 'Chapitres en premier',\n    'sort_rule_op_chapters_last' => 'Chapitres en dernier',\n    'sorting_page_limits' => 'Limite d\\'affichage par page',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Maintenance',\n    'maint_image_cleanup' => 'Nettoyer les images',\n    'maint_image_cleanup_desc' => 'Scanne le contenu des pages et des révisions pour vérifier les images, les dessins en cours d\\'utilisation et les doublons. Assurez-vous d\\'avoir une sauvegarde de la base de données et des images avant de lancer ceci.',\n    'maint_delete_images_only_in_revisions' => 'Supprimer également les images qui n\\'existent que dans les anciennes révisions de page',\n    'maint_image_cleanup_run' => 'Lancer le nettoyage',\n    'maint_image_cleanup_warning' => ':count images potentiellement inutilisées trouvées. Êtes-vous sûr de vouloir supprimer ces images ?',\n    'maint_image_cleanup_success' => ':count images potentiellement inutilisées trouvées et supprimées !',\n    'maint_image_cleanup_nothing_found' => 'Aucune image inutilisée trouvée, rien à supprimer !',\n    'maint_send_test_email' => 'Envoyer un e-mail de test',\n    'maint_send_test_email_desc' => 'Ceci envoie un e-mail de test à votre adresse e-mail spécifiée dans votre profil.',\n    'maint_send_test_email_run' => 'Envoyer un e-mail de test',\n    'maint_send_test_email_success' => 'E-mail envoyé à :address',\n    'maint_send_test_email_mail_subject' => 'E-mail de test',\n    'maint_send_test_email_mail_greeting' => 'L\\'envoi d\\'e-mail semble fonctionner !',\n    'maint_send_test_email_mail_text' => 'Félicitations ! Comme vous avez bien reçu cette notification, vos paramètres d\\'e-mail semblent être configurés correctement.',\n    'maint_recycle_bin_desc' => 'Les étagères, livres, chapitres et pages supprimés sont envoyés dans la corbeille afin qu\\'ils puissent être restaurés ou supprimés définitivement. Les éléments plus anciens de la corbeille peuvent être supprimés automatiquement après un certain temps selon la configuration du système.',\n    'maint_recycle_bin_open' => 'Ouvrir la corbeille',\n    'maint_regen_references' => 'Régénérer les références',\n    'maint_regen_references_desc' => 'Cette action reconstruira l\\'index des références croisées dans la base de données. Ceci est généralement géré automatiquement, mais cette action peut être utile pour indexer les anciens contenus ou contenus ajoutés par des méthodes non officielles.',\n    'maint_regen_references_success' => 'L\\'index de référence a été régénéré !',\n    'maint_timeout_command_note' => 'Note : Cette action peut prendre du temps pour s\\'exécuter, ce qui peut conduire à des problèmes d\\'expiration dans certains environnements Web. En tant qu\\'alternative, cette action peut être effectuée à l\\'aide d\\'une commande de terminal.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Corbeille',\n    'recycle_bin_desc' => 'Ici, vous pouvez restaurer les éléments qui ont été supprimés ou choisir de les effacer définitivement du système. Cette liste n\\'est pas filtrée contrairement aux listes d\\'activités similaires dans le système pour lesquelles les filtres d\\'autorisation sont appliqués.',\n    'recycle_bin_deleted_item' => 'Élément supprimé',\n    'recycle_bin_deleted_parent' => 'Parent',\n    'recycle_bin_deleted_by' => 'Supprimé par',\n    'recycle_bin_deleted_at' => 'Date de suppression',\n    'recycle_bin_permanently_delete' => 'Supprimer définitivement',\n    'recycle_bin_restore' => 'Restaurer',\n    'recycle_bin_contents_empty' => 'La corbeille est vide',\n    'recycle_bin_empty' => 'Vider la corbeille',\n    'recycle_bin_empty_confirm' => 'Cela détruira définitivement tous les éléments de la corbeille, y compris le contenu de chaque élément. Êtes-vous sûr de vouloir vider la corbeille ?',\n    'recycle_bin_destroy_confirm' => 'Cette action supprimera définitivement cet élément du système ainsi que tous les éléments enfants listés ci-dessous et vous ne pourrez plus restaurer ce contenu. Êtes-vous sûr de vouloir supprimer définitivement cet élément ?',\n    'recycle_bin_destroy_list' => 'Éléments à détruire',\n    'recycle_bin_restore_list' => 'Éléments à restaurer',\n    'recycle_bin_restore_confirm' => 'Cette action restaurera l\\'élément supprimé, y compris tous les éléments enfants, à leur emplacement d\\'origine. Si l\\'emplacement d\\'origine a été supprimé depuis et est maintenant dans la corbeille, l\\'élément parent devra également être restauré.',\n    'recycle_bin_restore_deleted_parent' => 'Le parent de cet élément a aussi été supprimé. Cet élément ne pourra être restauré sans que son parent le soit également.',\n    'recycle_bin_restore_parent' => 'Restaurer le parent',\n    'recycle_bin_destroy_notification' => ':count éléments supprimés de la corbeille au total.',\n    'recycle_bin_restore_notification' => ':count éléments restaurés de la corbeille au total.',\n\n    // Audit Log\n    'audit' => 'Journal d\\'audit',\n    'audit_desc' => 'Ce journal d\\'audit affiche un suivi des activités de l\\'application. Cette liste n\\'est pas filtrée contrairement aux suivis d\\'activités similaires de l\\'application où les filtres d\\'autorisation sont appliqués.',\n    'audit_event_filter' => 'Filtres d\\'événement',\n    'audit_event_filter_no_filter' => 'Pas de filtre',\n    'audit_deleted_item' => 'Élément supprimé',\n    'audit_deleted_item_name' => 'Nom: :name',\n    'audit_table_user' => 'Utilisateur',\n    'audit_table_event' => 'Événement',\n    'audit_table_related' => 'Élément concerné ou action réalisée',\n    'audit_table_ip' => 'Adresse IP',\n    'audit_table_date' => 'Horodatage',\n    'audit_date_from' => 'À partir du',\n    'audit_date_to' => 'Jusqu\\'au',\n\n    // Role Settings\n    'roles' => 'Rôles',\n    'role_user_roles' => 'Rôles des utilisateurs',\n    'roles_index_desc' => 'Les rôles sont utilisés pour regrouper les utilisateurs et fournir une autorisation système à leurs membres. Lorsqu\\'un utilisateur est membre de plusieurs rôles, les privilèges accordés se cumulent et l\\'utilisateur hérite de tous les droits d\\'accès.',\n    'roles_x_users_assigned' => ':count utilisateur assigné|:count utilisateurs assignés',\n    'roles_x_permissions_provided' => ':count permission|:count permissions',\n    'roles_assigned_users' => 'Utilisateurs assignés',\n    'roles_permissions_provided' => 'Permissions accordées',\n    'role_create' => 'Créer un nouveau rôle',\n    'role_delete' => 'Supprimer le rôle',\n    'role_delete_confirm' => 'Ceci va supprimer le rôle \\':roleName\\'.',\n    'role_delete_users_assigned' => 'Ce rôle a :userCount utilisateurs assignés. Vous pouvez choisir un rôle de remplacement pour ces utilisateurs.',\n    'role_delete_no_migration' => \"Ne pas assigner de nouveau rôle\",\n    'role_delete_sure' => 'Êtes-vous sûr de vouloir supprimer ce rôle ?',\n    'role_edit' => 'Modifier le rôle',\n    'role_details' => 'Détails du rôle',\n    'role_name' => 'Nom du rôle',\n    'role_desc' => 'Courte description du rôle',\n    'role_mfa_enforced' => 'Nécessite une authentification multi-facteurs',\n    'role_external_auth_id' => 'Identifiants d\\'authentification externes',\n    'role_system' => 'Permissions système',\n    'role_manage_users' => 'Gérer les utilisateurs',\n    'role_manage_roles' => 'Gérer les rôles et permissions',\n    'role_manage_entity_permissions' => 'Gérer les permissions sur les livres, chapitres et pages',\n    'role_manage_own_entity_permissions' => 'Gérer les permissions de ses propres livres, chapitres et pages',\n    'role_manage_page_templates' => 'Gérer les modèles de page',\n    'role_access_api' => 'Accès à l\\'API du système',\n    'role_manage_settings' => 'Gérer les préférences de l\\'application',\n    'role_export_content' => 'Exporter le contenu',\n    'role_import_content' => 'Importer le contenu',\n    'role_editor_change' => 'Changer l\\'éditeur de page',\n    'role_notifications' => 'Recevoir et gérer les notifications',\n    'role_permission_note_users_and_roles' => 'Ces autorisations permettront également l\\'accès à la consultation et la recherche des utilisateurs et des rôles dans le système.',\n    'role_asset' => 'Permissions des ressources',\n    'roles_system_warning' => 'Sachez que l\\'accès à l\\'une des trois permissions ci-dessus peut permettre à un utilisateur de modifier ses propres privilèges ou les privilèges des autres utilisateurs du système. N\\'attribuez uniquement des rôles avec ces permissions qu\\'à des utilisateurs de confiance.',\n    'role_asset_desc' => 'Ces permissions contrôlent l\\'accès par défaut des ressources dans le système. Les permissions dans les livres, les chapitres et les pages ignoreront ces permissions',\n    'role_asset_admins' => 'Les administrateurs ont automatiquement accès à tous les contenus mais les options suivantes peuvent afficher ou masquer certaines options de l\\'interface.',\n    'role_asset_image_view_note' => 'Cela concerne la visibilité dans le gestionnaire d\\'images. L\\'accès réel des fichiers d\\'image téléchargés dépendra de l\\'option de stockage d\\'images du système.',\n    'role_asset_users_note' => 'Ces autorisations permettront également l\\'accès à la consultation et la recherche des utilisateurs dans le système.',\n    'role_all' => 'Tous',\n    'role_own' => 'Propres',\n    'role_controlled_by_asset' => 'Contrôlé par les ressources les ayant envoyés',\n    'role_save' => 'Enregistrer le rôle',\n    'role_users' => 'Utilisateurs ayant ce rôle',\n    'role_users_none' => 'Aucun utilisateur avec ce rôle actuellement',\n\n    // Users\n    'users' => 'Utilisateurs',\n    'users_index_desc' => 'Créer et gérer des comptes utilisateur individuels au sein du système. Les comptes utilisateur sont employés pour la connexion et l\\'attribution du contenu et le suivi d\\'activité. Les permissions d\\'accès sont principalement basées sur les rôles, mais la propriété du contenu de l\\'utilisateur, entre autres facteurs, peut également affecter les permissions et l\\'accès.',\n    'user_profile' => 'Profil d\\'utilisateur',\n    'users_add_new' => 'Ajouter un nouvel utilisateur',\n    'users_search' => 'Rechercher les utilisateurs',\n    'users_latest_activity' => 'Dernière activité',\n    'users_details' => 'Informations de l\\'utilisateur',\n    'users_details_desc' => 'Définissez un nom et une adresse e-mail pour cet utilisateur. L\\'adresse e-mail sera utilisée pour se connecter à l\\'application.',\n    'users_details_desc_no_email' => 'Définissez un nom d\\'affichage pour cet utilisateur afin que les autres puissent le reconnaître.',\n    'users_role' => 'Rôles de l\\'utilisateur',\n    'users_role_desc' => 'Sélectionnez les rôles auxquels cet utilisateur sera affecté. Si un utilisateur est affecté à plusieurs rôles, les permissions de ces rôles s\\'empileront et ils recevront toutes les capacités des rôles affectés.',\n    'users_password' => 'Mot de passe de l\\'utilisateur',\n    'users_password_desc' => 'Définissez un mot de passe pour vous connecter à l\\'application. Il doit comporter au moins 8 caractères.',\n    'users_send_invite_text' => 'Vous pouvez choisir d\\'envoyer à cet utilisateur un e-mail d\\'invitation qui lui permet de définir son propre mot de passe, sinon vous pouvez définir son mot de passe vous-même.',\n    'users_send_invite_option' => 'Envoyer l\\'e-mail d\\'invitation',\n    'users_external_auth_id' => 'Identifiant d\\'authentification externe',\n    'users_external_auth_id_desc' => 'Lorsqu\\'un système d\\'authentification externe est utilisé (SAML2, OIDC ou LDAP) c\\'est l\\'identifiant unique qui relie cet utilisateur à un compte système d\\'authentification. Vous pouvez ignorer ce champ si vous utilisez l\\'authentification email par défaut.',\n    'users_password_warning' => 'Remplissez ce qui suit uniquement si vous souhaitez changer le mot de passe de cet utilisateur.',\n    'users_system_public' => 'Cet utilisateur représente les invités visitant votre instance. Il est assigné automatiquement aux invités.',\n    'users_delete' => 'Supprimer un utilisateur',\n    'users_delete_named' => 'Supprimer l\\'utilisateur :userName',\n    'users_delete_warning' => 'Ceci va supprimer \\':userName\\' du système.',\n    'users_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cet utilisateur ?',\n    'users_migrate_ownership' => 'Transférer la propriété',\n    'users_migrate_ownership_desc' => 'Sélectionnez un utilisateur ici si vous voulez qu\\'un autre utilisateur devienne le propriétaire de tous les éléments actuellement détenus par cet utilisateur.',\n    'users_none_selected' => 'Aucun utilisateur n\\'a été sélectionné',\n    'users_edit' => 'Modifier l\\'utilisateur',\n    'users_edit_profile' => 'Modifier le profil',\n    'users_avatar' => 'Avatar de l\\'utilisateur',\n    'users_avatar_desc' => 'Cette image doit être un carré d\\'environ 256 px.',\n    'users_preferred_language' => 'Langue préférée',\n    'users_preferred_language_desc' => 'Cette option changera la langue utilisée pour l\\'interface utilisateur de l\\'application. Ceci n\\'affectera aucun contenu créé par l\\'utilisateur.',\n    'users_social_accounts' => 'Réseaux sociaux',\n    'users_social_accounts_desc' => 'Voir l\\'état des comptes sociaux connectés pour cet utilisateur. Les comptes sociaux peuvent être utilisés en plus du système d\\'authentification principal pour l\\'accès au système.',\n    'users_social_accounts_info' => 'Vous pouvez connecter des réseaux sociaux à votre compte pour vous connecter plus rapidement. Déconnecter un compte n\\'enlèvera pas les accès autorisés précédemment sur votre compte de réseau social.',\n    'users_social_connect' => 'Connecter le compte',\n    'users_social_disconnect' => 'Déconnecter le compte',\n    'users_social_status_connected' => 'Connecté',\n    'users_social_status_disconnected' => 'Déconnecté',\n    'users_social_connected' => 'Votre compte :socialAccount a été ajouté avec succès.',\n    'users_social_disconnected' => 'Votre compte :socialAccount a été déconnecté avec succès',\n    'users_api_tokens' => 'Jetons API',\n    'users_api_tokens_desc' => 'Créer et gérer les jetons d\\'accès utilisés pour s\\'authentifier avec l\\'API REST de BookStack. Les permissions pour l\\'API sont gérées par l\\'utilisateur auquel le jeton appartient.',\n    'users_api_tokens_none' => 'Aucun jeton API n\\'a été créé pour cet utilisateur',\n    'users_api_tokens_create' => 'Créer un jeton',\n    'users_api_tokens_expires' => 'Expire',\n    'users_api_tokens_docs' => 'Documentation de l\\'API',\n    'users_mfa' => 'Authentification multi-facteurs',\n    'users_mfa_desc' => 'Configurer l\\'authentification multi-facteurs ajoute une couche supplémentaire de sécurité à votre compte utilisateur.',\n    'users_mfa_x_methods' => ':count méthode configurée|:count méthodes configurées',\n    'users_mfa_configure' => 'Méthode de configuration',\n\n    // API Tokens\n    'user_api_token_create' => 'Créer un nouveau jeton API',\n    'user_api_token_name' => 'Nom',\n    'user_api_token_name_desc' => 'Donnez à votre jeton un nom lisible pour l\\'identifier plus tard.',\n    'user_api_token_expiry' => 'Date d\\'expiration',\n    'user_api_token_expiry_desc' => 'Définissez une date à laquelle ce jeton expire. Après cette date, les demandes effectuées à l\\'aide de ce jeton ne fonctionneront plus. Le fait de laisser ce champ vide entraînera une expiration dans 100 ans.',\n    'user_api_token_create_secret_message' => 'Immédiatement après la création de ce jeton, un \"ID de jeton\" et \"Secret de jeton\" sera généré et affiché. Le secret ne sera affiché qu\\'une seule fois, alors assurez-vous de copier la valeur dans un endroit sûr et sécurisé avant de continuer.',\n    'user_api_token' => 'Jeton API',\n    'user_api_token_id' => 'Token ID',\n    'user_api_token_id_desc' => 'Il s\\'agit d\\'un identifiant généré par le système non modifiable pour ce jeton qui devra être fourni dans les demandes d\\'API.',\n    'user_api_token_secret' => 'Token Secret',\n    'user_api_token_secret_desc' => 'Il s\\'agit d\\'un secret généré par le système pour ce jeton, qui devra être fourni dans les demandes d\\'API. Cela ne sera affiché qu\\'une seule fois, alors copiez cette valeur dans un endroit sûr et sécurisé.',\n    'user_api_token_created' => 'Jeton créé :timeAgo',\n    'user_api_token_updated' => 'Jeton mis à jour :timeAgo',\n    'user_api_token_delete' => 'Supprimer le jeton',\n    'user_api_token_delete_warning' => 'Cela supprimera complètement le jeton d\\'API avec le nom \\':tokenName\\'.',\n    'user_api_token_delete_confirm' => 'Souhaitez-vous vraiment effacer ce jeton API ?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Les Webhooks sont un moyen d\\'envoyer des données à des URL externes lorsque certaines actions et événements se produisent dans le système, ce qui permet une intégration basée sur des événements avec des plates-formes externes telles que les systèmes de messagerie ou de notification.',\n    'webhooks_x_trigger_events' => ':count événement déclencheur|:count événements déclencheurs',\n    'webhooks_create' => 'Créer un nouveau Webhook',\n    'webhooks_none_created' => 'Aucun webhook n\\'a encore été créé.',\n    'webhooks_edit' => 'Éditer le Webhook',\n    'webhooks_save' => 'Enregistrer le Webhook',\n    'webhooks_details' => 'Détails du Webhook',\n    'webhooks_details_desc' => 'Renseignez un nom ainsi que votre endpoint POST sur lequel les données du webhook doivent être envoyées.',\n    'webhooks_events' => 'Événements du Webhook',\n    'webhooks_events_desc' => 'Sélectionnez tous les évènements qui doivent déclencher un appel sur ce webhook.',\n    'webhooks_events_warning' => 'Gardez à l\\'esprit que ces événements seront déclenchés pour chaque événement sélectionné, même si des permissions personnalisées sont appliquées. Vérifiez bien que l\\'utilisation de ce webhook n\\'exposera pas de contenu confidentiel.',\n    'webhooks_events_all' => 'Tous les événements système',\n    'webhooks_name' => 'Nom du Webhook',\n    'webhooks_timeout' => 'Délai d\\'expiration de requête du Webhook (en secondes)',\n    'webhooks_endpoint' => 'Point de terminaison du Webhook',\n    'webhooks_active' => 'Webhook actif',\n    'webhook_events_table_header' => 'Événements',\n    'webhooks_delete' => 'Supprimer le Webhook',\n    'webhooks_delete_warning' => 'Ceci supprimera complètement du système le webhook ayant le nom \\':webhookName\\'.',\n    'webhooks_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer ce webhook ?',\n    'webhooks_format_example' => 'Exemple de format de webhook',\n    'webhooks_format_example_desc' => 'Les données du webhook sont envoyées dans une requête POST vers l\\'endpoint au format JSON respectant le format ci-dessous. Les propriétés \"related_item\" et \"url\" sont optionnelles et dépendront du type d\\'événement déclenché.',\n    'webhooks_status' => 'Statut du webhook',\n    'webhooks_last_called' => 'Dernier appel :',\n    'webhooks_last_errored' => 'Dernier en erreur :',\n    'webhooks_last_error_message' => 'Dernier message d\\'erreur : ',\n\n    // Licensing\n    'licenses' => 'Licences',\n    'licenses_desc' => 'Cette page détaille les informations de licence pour BookStack ainsi que les projets et librairies utilisées dans BookStack. Nombre des projets listés peuvent n\\'être utilisés que dans un contexte de développement.',\n    'licenses_bookstack' => 'Licences BookStack',\n    'licenses_php' => 'Licences de librairies PHP',\n    'licenses_js' => 'Licences de librairies JavaScript',\n    'licenses_other' => 'Autres Licences',\n    'license_details' => 'Détails de la licence',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'Anglais',\n        'ar' => 'Arabe',\n        'bg' => 'Bulgare',\n        'bs' => 'Bosniaque',\n        'ca' => 'Catalan',\n        'cs' => 'Tchèque',\n        'cy' => 'Cymraeg',\n        'da' => 'Danois',\n        'de' => 'Allemand',\n        'de_informal' => 'Allemand (informel)',\n        'el' => 'ελληνικά',\n        'es' => 'Espagnol',\n        'es_AR' => 'Espagnol (Argentine)',\n        'et' => 'Estonien',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'Hébreu',\n        'hr' => 'Croate',\n        'hu' => 'Hongrois',\n        'id' => 'Indonésien',\n        'it' => 'Italien',\n        'ja' => 'Japonais',\n        'ko' => 'Coréen',\n        'lt' => 'Lituanien',\n        'lv' => 'Letton',\n        'nb' => 'Norvegien',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Néerlandais',\n        'pl' => 'Polonais',\n        'pt' => 'Portugais',\n        'pt_BR' => 'Portugais (Brésil)',\n        'ro' => 'Română',\n        'ru' => 'Russe',\n        'sk' => 'Slovaque',\n        'sl' => 'Slovène',\n        'sv' => 'Suédois',\n        'tr' => 'Turc',\n        'uk' => 'Ukrainien',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Vietnamien',\n        'zh_CN' => 'Chinois (simplifié)',\n        'zh_TW' => 'Mandarin de Taïwan',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/fr/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute doit être accepté.',\n    'active_url'           => ':attribute n\\'est pas une URL valide.',\n    'after'                => ':attribute doit être supérieur à :date.',\n    'alpha'                => ':attribute ne doit contenir que des lettres.',\n    'alpha_dash'           => ':attribute doit contenir uniquement des lettres, chiffres et traits d\\'union.',\n    'alpha_num'            => ':attribute doit contenir uniquement des chiffres et des lettres.',\n    'array'                => ':attribute doit être un tableau.',\n    'backup_codes'         => 'Le code fourni n\\'est pas valide ou a déjà été utilisé.',\n    'before'               => ':attribute doit être inférieur à :date.',\n    'between'              => [\n        'numeric' => ':attribute doit être compris entre :min et :max.',\n        'file'    => ':attribute doit être compris entre :min et :max Ko.',\n        'string'  => ':attribute doit être compris entre :min et :max caractères.',\n        'array'   => ':attribute doit être compris entre :min et :max éléments.',\n    ],\n    'boolean'              => ':attribute doit être vrai ou faux.',\n    'confirmed'            => ':attribute la confirmation n\\'est pas valide.',\n    'date'                 => ':attribute n\\'est pas une date valide.',\n    'date_format'          => ':attribute ne correspond pas au format :format.',\n    'different'            => ':attribute et :other doivent être différents l\\'un de l\\'autre.',\n    'digits'               => ':attribute doit être de longueur :digits.',\n    'digits_between'       => ':attribute doit avoir une longueur entre :min et :max.',\n    'email'                => ':attribute doit être une adresse e-mail valide.',\n    'ends_with' => ':attribute doit se terminer par une des valeurs suivantes : :values',\n    'file'                 => 'Le :attribute doit être un fichier valide.',\n    'filled'               => ':attribute est un champ requis.',\n    'gt'                   => [\n        'numeric' => ':attribute doit être plus grand que :value.',\n        'file'    => ':attribute doit être plus grand que :value Ko.',\n        'string'  => ':attribute doit être plus grand que :value caractères.',\n        'array'   => ':attribute doit avoir plus que :value éléments.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute doit être plus grand ou égal à :value.',\n        'file'    => ':attribute doit être plus grand ou égal à :value Ko.',\n        'string'  => ':attribute doit être plus grand ou égal à :value caractères.',\n        'array'   => ':attribute doit avoir :value éléments ou plus.',\n    ],\n    'exists'               => 'L\\'attribut :attribute est invalide.',\n    'image'                => ':attribute doit être une image.',\n    'image_extension'      => ':attribute doit avoir une extension d\\'image valide et supportée.',\n    'in'                   => 'L\\'attribut :attribute est invalide.',\n    'integer'              => ':attribute doit être un chiffre entier.',\n    'ip'                   => ':attribute doit être une adresse IP valide.',\n    'ipv4'                 => ':attribute doit être une adresse IPv4 valide.',\n    'ipv6'                 => ':attribute doit être une adresse IPv6 valide.',\n    'json'                 => ':attribute doit être une chaîne JSON valide.',\n    'lt'                   => [\n        'numeric' => ':attribute doit être plus petit que :value.',\n        'file'    => ':attribute doit être plus petit que :value Ko.',\n        'string'  => ':attribute doit être plus petit que :value caractères.',\n        'array'   => ':attribute doit avoir moins de :value éléments.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute doit être plus petit ou égal à :value.',\n        'file'    => ':attribute doit être plus petit ou égal à :value Ko.',\n        'string'  => ':attribute doit être plus petit ou égal à :value caractères.',\n        'array'   => ':attribute ne doit pas avoir plus de :value éléments.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute ne doit pas excéder :max.',\n        'file'    => ':attribute ne doit pas excéder :max Ko.',\n        'string'  => ':attribute ne doit pas excéder :max caractères.',\n        'array'   => ':attribute ne doit pas contenir plus de :max éléments.',\n    ],\n    'mimes'                => ':attribute doit être un fichier de type :values.',\n    'min'                  => [\n        'numeric' => ':attribute doit être au moins :min.',\n        'file'    => ':attribute doit faire au moins :min kilobytes.',\n        'string'  => ':attribute doit contenir au moins :min caractères.',\n        'array'   => ':attribute doit contenir au moins :min éléments.',\n    ],\n    'not_in'               => 'L\\'attribut sélectionné :attribute est invalide.',\n    'not_regex'            => ':attribute a un format invalide.',\n    'numeric'              => ':attribute doit être un nombre.',\n    'regex'                => ':attribute a un format invalide.',\n    'required'             => ':attribute est un champ requis.',\n    'required_if'          => ':attribute est requis si :other est :value.',\n    'required_with'        => ':attribute est requis si :values est présent.',\n    'required_with_all'    => ':attribute est requis si :values est présent.',\n    'required_without'     => ':attribute est requis si:values n\\'est pas présent.',\n    'required_without_all' => ':attribute est requis si aucun des valeurs :values n\\'est présente.',\n    'same'                 => ':attribute et :other doivent être identiques.',\n    'safe_url'             => 'Le lien fourni peut ne pas être sûr.',\n    'size'                 => [\n        'numeric' => ':attribute doit avoir la taille :size.',\n        'file'    => ':attribute doit peser :size kilobytes.',\n        'string'  => ':attribute doit contenir :size caractères.',\n        'array'   => ':attribute doit contenir :size éléments.',\n    ],\n    'string'               => ':attribute doit être une chaîne de caractères.',\n    'timezone'             => ':attribute doit être une zone valide.',\n    'totp'                 => 'Le code fourni n\\'est pas valide ou est expiré.',\n    'unique'               => ':attribute est déjà utilisé.',\n    'url'                  => ':attribute a un format invalide.',\n    'uploaded'             => 'Le fichier n\\'a pas pu être envoyé. Le serveur peut ne pas accepter des fichiers de cette taille.',\n\n    'zip_file' => 'L\\'attribut :attribute doit référencer un fichier dans le ZIP.',\n    'zip_file_size' => 'Le fichier :attribute ne doit pas dépasser :size Mo.',\n    'zip_file_mime' => ':attribute doit référencer un fichier de type :validTypes, trouvé :foundType.',\n    'zip_model_expected' => 'Objet de données attendu, mais \":type\" trouvé.',\n    'zip_unique' => 'L\\'attribut :attribute doit être unique pour le type d\\'objet dans le ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'La confirmation du mot de passe est requise',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/he/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'הדף נוצר',\n    'page_create_notification'    => 'הדף נוצר בהצלחה',\n    'page_update'                 => 'הדף עודכן',\n    'page_update_notification'    => 'הדף עודכן בהצלחה',\n    'page_delete'                 => 'הדף נמחק',\n    'page_delete_notification'    => 'הדף הוסר בהצלחה',\n    'page_restore'                => 'הדף שוחזר',\n    'page_restore_notification'   => 'הדף שוחזר בהצלחה',\n    'page_move'                   => 'דף הועבר',\n    'page_move_notification'      => 'הדף הוזז בהצלחה',\n\n    // Chapters\n    'chapter_create'              => 'פרק נוצר',\n    'chapter_create_notification' => 'הפרק נוצר בהצלחה',\n    'chapter_update'              => 'פרק עודכן',\n    'chapter_update_notification' => 'הפרק עודכן בהצלחה',\n    'chapter_delete'              => 'פרק נמחק',\n    'chapter_delete_notification' => 'הפרק נמחק בהצלחה',\n    'chapter_move'                => 'פרק הועבר',\n    'chapter_move_notification' => 'פרק הוזז בהצלחה',\n\n    // Books\n    'book_create'                 => 'ספר נוצר',\n    'book_create_notification'    => 'ספר נוצר בהצלחה',\n    'book_create_from_chapter'              => 'המר פרק לספר',\n    'book_create_from_chapter_notification' => 'הפרק הומר בהצלחה לספר',\n    'book_update'                 => 'ספר הועדכן',\n    'book_update_notification'    => 'ספר התעדכן בהצלחה',\n    'book_delete'                 => 'ספר נמחק',\n    'book_delete_notification'    => 'ספר נמחק בהצלחה',\n    'book_sort'                   => 'ספר ממויין',\n    'book_sort_notification'      => 'ספר מויין מחדש בהצלחה',\n\n    // Bookshelves\n    'bookshelf_create'            => 'מדף נוצר',\n    'bookshelf_create_notification'    => 'המדף נוצר בהצלחה',\n    'bookshelf_create_from_book'    => 'המר ספר למדף',\n    'bookshelf_create_from_book_notification'    => 'הספר הוסב בהצלחה למדף',\n    'bookshelf_update'                 => 'מדף עודכן',\n    'bookshelf_update_notification'    => 'מדף עודכן בהצלחה',\n    'bookshelf_delete'                 => 'מדף שנמחק',\n    'bookshelf_delete_notification'    => 'מדף נמחק בהצלחה',\n\n    // Revisions\n    'revision_restore' => 'גרסא שוחזרה',\n    'revision_delete' => 'גרסא הוסרה',\n    'revision_delete_notification' => 'גרסא הוסרה בהצלחה',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" הוסף למועדפים',\n    'favourite_remove_notification' => '\":name\" הוסר מהמועדפים',\n\n    // Watching\n    'watch_update_level_notification' => 'העדפות צפייה עודכנו בהצלחה',\n\n    // Auth\n    'auth_login' => 'מחובר',\n    'auth_register' => 'נרשם כמשתמש חדש',\n    'auth_password_reset_request' => 'בקשת איפוס סיסמה למשתמש בוצעה בהצלחה',\n    'auth_password_reset_update' => 'איפוס סיסמה למשתמש',\n    'mfa_setup_method' => 'הגדרת אימות דו-שלבי פעיל',\n    'mfa_setup_method_notification' => 'הגדרת אימות דו-שלבי בוצע בהצלחה',\n    'mfa_remove_method' => 'הגדרת אימות דו-שלבי הוסר',\n    'mfa_remove_method_notification' => 'אפשרות אימות דו-שלבי הוסר בהצלחה',\n\n    // Settings\n    'settings_update' => 'הגדרות עודכנו בהצלחה',\n    'settings_update_notification' => 'ההגדרות עודכנו בהצלחה',\n    'maintenance_action_run' => 'פעולות תחזוקה שהופעלו',\n\n    // Webhooks\n    'webhook_create' => 'webook נוצר',\n    'webhook_create_notification' => 'יצירת Webhook בוצעה בהצלחה',\n    'webhook_update' => 'webhook עודכן',\n    'webhook_update_notification' => 'webook עודכן בהצלחה',\n    'webhook_delete' => 'Webhook נמחק',\n    'webhook_delete_notification' => 'Webook נמחק בהצלחה',\n\n    // Imports\n    'import_create' => 'יבוא נוצר',\n    'import_create_notification' => 'יבוא עודכן בהצלחה',\n    'import_run' => 'יבוא עודכן',\n    'import_run_notification' => 'תוכן יובא בהצלחה',\n    'import_delete' => 'יבוא נמחק',\n    'import_delete_notification' => 'יבוא נמחק בהצלחה',\n\n    // Users\n    'user_create' => 'משתמש חדש נוצר',\n    'user_create_notification' => 'משתמש נוצר בהצלחה',\n    'user_update' => 'משתמש עודכן',\n    'user_update_notification' => 'משתמש עודכן בהצלחה',\n    'user_delete' => 'משתמש נמחק',\n    'user_delete_notification' => 'משתמש הוסר בהצלחה',\n\n    // API Tokens\n    'api_token_create' => 'API Token נוצר',\n    'api_token_create_notification' => 'API Token נוצר בהצלחה',\n    'api_token_update' => 'API Token עודכן',\n    'api_token_update_notification' => 'API Token עודכן בהצלחה',\n    'api_token_delete' => 'API Token נמחק',\n    'api_token_delete_notification' => 'API Token נמחק בהצלחה',\n\n    // Roles\n    'role_create' => 'תפקיד נוצר',\n    'role_create_notification' => 'תפקיד נוצר בהצלחה',\n    'role_update' => 'תפקיד עודכן',\n    'role_update_notification' => 'תפקיד עודכן בהצלחה',\n    'role_delete' => 'תפקיד נמחק',\n    'role_delete_notification' => 'תפקיד נמחק בהצלחה',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'סל המחזור רוקן',\n    'recycle_bin_restore' => 'שוחזר מסל המחזור',\n    'recycle_bin_destroy' => 'נמחק מסל המחזור',\n\n    // Comments\n    'commented_on'                => 'הגיב/ה על',\n    'comment_create'              => 'הערה הוספה',\n    'comment_update'              => 'תגובה הוספה',\n    'comment_delete'              => 'תגובה נמחקה',\n\n    // Sort Rules\n    'sort_rule_create' => 'נוצר חוק מיון',\n    'sort_rule_create_notification' => 'חוק מיון נוצר בהצלחה',\n    'sort_rule_update' => 'חוק מיון עודכן',\n    'sort_rule_update_notification' => 'חוק מיון עודכן בהצלחה',\n    'sort_rule_delete' => 'חוק מיון נמחק',\n    'sort_rule_delete_notification' => 'חוק מיון נמחק בהצלחה',\n\n    // Other\n    'permissions_update'          => 'הרשאות עודכנו',\n];\n"
  },
  {
    "path": "lang/he/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'פרטי ההתחברות אינם תואמים את הנתונים שלנו.',\n    'throttle' => 'נסיונות התחברות מהירים מדי, יש להמתין :seconds שניות ולנסות שנית.',\n\n    // Login & Register\n    'sign_up' => 'הרשמה למערכת',\n    'log_in' => 'התחבר למערכת',\n    'log_in_with' => 'התחבר באמצעות :socialDriver',\n    'sign_up_with' => 'הרשם באמצעות :socialDriver',\n    'logout' => 'התנתק',\n\n    'name' => 'שם',\n    'username' => 'שם משתמש',\n    'email' => 'אי-מייל',\n    'password' => 'סיסמא',\n    'password_confirm' => 'אימות סיסמא',\n    'password_hint' => '‏אורך הסיסמה חייב להיות לפחות 8 תווים',\n    'forgot_password' => 'שכחת סיסמא?',\n    'remember_me' => 'זכור אותי',\n    'ldap_email_hint' => 'אנא ציין כתובת אי-מייל לשימוש בחשבון זה',\n    'create_account' => 'צור חשבון',\n    'already_have_account' => 'יש לך כבר חשבון?',\n    'dont_have_account' => 'אין לך חשבון?',\n    'social_login' => 'התחברות באמצעות אתר חברתי',\n    'social_registration' => 'הרשמה באמצעות אתר חברתי',\n    'social_registration_text' => 'הרשם והתחבר באמצעות שירות אחר',\n\n    'register_thanks' => 'תודה על הרשמתך!',\n    'register_confirm' => 'יש לבדוק את תיבת המייל שלך ולאשר את ההרשמה על מנת להשתמש ב:appName',\n    'registrations_disabled' => 'הרשמה כרגע מבוטלת',\n    'registration_email_domain_invalid' => 'לא ניתן להרשם באמצעות המייל שסופק',\n    'register_success' => 'תודה על הרשמתך! ניתן כעת להתחבר',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'ניסיון התחברות',\n    'auto_init_starting_desc' => 'אנחנו יוצרים קשר עם מערכת האימות שלך להתחלת תהליך ההתחברות. במידה ולאחר 5 שניות לא בוצעה התחברות יש ללחוץ על הקישור מטה.',\n    'auto_init_start_link' => 'המשך עם האימות',\n\n    // Password Reset\n    'reset_password' => 'איפוס סיסמא',\n    'reset_password_send_instructions' => 'יש להזין את כתובת המייל למטה ואנו נשלח אלייך הוראות לאיפוס הסיסמא',\n    'reset_password_send_button' => 'שלח קישור לאיפוס סיסמא',\n    'reset_password_sent' => 'קישור לשחזור סיסמה יישלח ל:email אם כתובת המייל קיימת במערכת.',\n    'reset_password_success' => 'סיסמתך עודכנה בהצלחה',\n    'email_reset_subject' => 'איפוס סיסמא ב :appName',\n    'email_reset_text' => 'קישור זה נשלח עקב בקשה לאיפוס סיסמא בחשבון שלך',\n    'email_reset_not_requested' => 'אם לא ביקשת לאפס את סיסמתך, אפשר להתעלם ממייל זה',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'אמת אי-מייל ב :appName',\n    'email_confirm_greeting' => 'תודה שהצטרפת אל :appName!',\n    'email_confirm_text' => 'יש לאמת את כתובת המייל של על ידי לחיצה על הכפור למטה:',\n    'email_confirm_action' => 'אמת כתובת אי-מייל',\n    'email_confirm_send_error' => 'נדרש אימות אי-מייל אך שליחת האי-מייל אליך נכשלה. יש ליצור קשר עם מנהל המערכת כדי לוודא שאכן ניתן לשלוח מיילים.',\n    'email_confirm_success' => 'כתובת המייל שלך אומתה! כעת תוכל/י להתחבר באמצעות כתובת מייל זו.',\n    'email_confirm_resent' => 'אימות נשלח לאי-מייל שלך, יש לבדוק בתיבת הדואר הנכנס',\n    'email_confirm_thanks' => 'תודה על האישור!',\n    'email_confirm_thanks_desc' => 'בבקשה המתן בזמן שהאישוך שלך מטופל. במידה ולא הופנתה לאחר 3 שניות לחץ על \"המשך\" מטה בכדי להמשיך.',\n\n    'email_not_confirmed' => 'כתובת המייל לא אומתה',\n    'email_not_confirmed_text' => 'כתובת המייל שלך טרם אומתה',\n    'email_not_confirmed_click_link' => 'יש ללחוץ על הקישור אשר נשלח אליך לאחר ההרשמה',\n    'email_not_confirmed_resend' => 'אם אינך מוצא את המייל, ניתן לשלוח בשנית את האימות על ידי לחיצה על הכפתור למטה',\n    'email_not_confirmed_resend_button' => 'שלח שוב מייל אימות',\n\n    // User Invite\n    'user_invite_email_subject' => 'הוזמנת להצטרף ל:appName!',\n    'user_invite_email_greeting' => 'חשבון נוצר עבורך ב :appName.',\n    'user_invite_email_text' => 'לחץ על הכפתור מטה בכדי להגדיר סיסמת משתמש ולקבל גישה:',\n    'user_invite_email_action' => 'הגדר סיסמה לחשבון',\n    'user_invite_page_welcome' => 'ברוכים הבאים ל :appName!',\n    'user_invite_page_text' => 'על מנת לסיים את ההרשמה ולקבל גישה עלייך להגדיר סיסמה אשר תהיה בשימוש בהתחברות ל :appName בביקורים עתידיים.',\n    'user_invite_page_confirm_button' => 'אימות סיסמא',\n    'user_invite_success_login' => 'הסיסמה הוגדרה בהצלחה, כעת תוכלו לקבל גישה ל :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'הגדר אימות רב-שלבי',\n    'mfa_setup_desc' => 'הגדר אימות רב-שלבי כשכבת אבטחה נוספת עבור החשבון שלך.',\n    'mfa_setup_configured' => 'כבר הוגדר',\n    'mfa_setup_reconfigure' => 'הגדר מחדש',\n    'mfa_setup_remove_confirmation' => 'האם להסיר את אפשרות האימות הדו-שלבי הזאת?',\n    'mfa_setup_action' => 'הגדרה',\n    'mfa_backup_codes_usage_limit_warning' => 'נשאר לך פחות מ 5 קודי גיבוי, בבקשה חולל ואחסן סט חדש לפני שיגמרו לך הקודים בכדי למנוע נעילה מחוץ לחשבון שלך.',\n    'mfa_option_totp_title' => 'אפליקציה לנייד',\n    'mfa_option_totp_desc' => 'בכדי להשתמש באימות רב-שלבי תצטרך אפליקציית מובייל תומכת TOTP כמו Google Authenticator, Authy או Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'קודי גיבוי',\n    'mfa_option_backup_codes_desc' => 'יצירת מערך סיסמאות חד-פעמיות כגיבוי אשר תתקבשו להזין על מנת לאמת את הזהות שלכם, אנא וודאו כי הקודים האלו שמורים במקום בטוח.',\n    'mfa_gen_confirm_and_enable' => 'אישור והפעלה',\n    'mfa_gen_backup_codes_title' => 'הגדרת קודי גיבוי',\n    'mfa_gen_backup_codes_desc' => 'אנא שמור את רשימת הקודים הרשומים מטה במקום בטוח. בגישה למערכת תהיה אפשרות להשתמש באחד הקודים הללו כאמצעי זיהוי נוסף.',\n    'mfa_gen_backup_codes_download' => 'הורדת קודים',\n    'mfa_gen_backup_codes_usage_warning' => 'ניתן להשתמש בכל קוד פעם אחת בלבד',\n    'mfa_gen_totp_title' => 'הגדרת אפליקציה לזיהוי',\n    'mfa_gen_totp_desc' => 'על מנת להגדיר זיהוי דו-שלבי במכשיר נייד עלייך להשתמש באפליקציה התומכת ב TOTP כגון Google Authenticator, Authy או Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'סרוק את קוד ה QR באמצעות האפליקציה שבה מתבצע הזיהוי על מנת להתחיל.',\n    'mfa_gen_totp_verify_setup' => 'אשר את ההגדרה',\n    'mfa_gen_totp_verify_setup_desc' => 'על מנת לוודא כי הזיהוי הדו-שלבי עובד יש להכניס את הקוד, הוא מופיע לך על מסך האפליקציה, בשדה מטה:',\n    'mfa_gen_totp_provide_code_here' => 'אנא הכנס את הקוד כאן',\n    'mfa_verify_access' => 'אשר גישה',\n    'mfa_verify_access_desc' => 'חשבון המשתמש שלך דורש ממך לאת את הזהות שלך בשכבת הגנה נוספת על מנת לאפשר לך גישה. יש לאשר גישה דרך אחד האמצעים הקיימים על מנת להמשיך.',\n    'mfa_verify_no_methods' => 'אין אפשרויות אימות דו-שלבי מוגדרות',\n    'mfa_verify_no_methods_desc' => 'לא נמצאו אפשרויות ווידוא זהות עבור המשתמש שלך.\nנדרש לקנפג לפחות אחד על מנת לקבל גישה.',\n    'mfa_verify_use_totp' => 'אמת באמצעות אפליקציה',\n    'mfa_verify_use_backup_codes' => 'אמת באמצעות קוד גיבוי',\n    'mfa_verify_backup_code' => 'קוד גיבוי',\n    'mfa_verify_backup_code_desc' => 'הזן מטה אחד מקודי הגיבוי הנותרים לך:',\n    'mfa_verify_backup_code_enter_here' => 'הזן קוד גיבוי כאן',\n    'mfa_verify_totp_desc' => 'הזן את הקוד, שהונפק דרך האפליקציה שלך, מטה:',\n    'mfa_setup_login_notification' => 'אמצעי זיהוי זהות הוגדרו, אנא התחבר מחדש.',\n];\n"
  },
  {
    "path": "lang/he/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'ביטול',\n    'close' => 'סגור',\n    'confirm' => 'אישור',\n    'back' => 'אחורה',\n    'save' => 'שמור',\n    'continue' => 'המשך',\n    'select' => 'בחר',\n    'toggle_all' => 'סמן הכל',\n    'more' => 'עוד',\n\n    // Form Labels\n    'name' => 'שם',\n    'description' => 'תיאור',\n    'role' => 'תפקיד',\n    'cover_image' => 'תמונת נושא',\n    'cover_image_description' => 'התמונה צריכה להיות לפחות בגודל של 440 על 250 למרות שהיא תוצג בצורה דינמית ותותאם בהתאם להתאים לממשק המשתמש במצבים שונים על פי הדרישה, ולכן המימדים יהיו שונים.',\n\n    // Actions\n    'actions' => 'פעולות',\n    'view' => 'הצג',\n    'view_all' => 'הצג הכל',\n    'new' => 'חדש',\n    'create' => 'צור',\n    'update' => 'עדכן',\n    'edit' => 'ערוך',\n    'archive' => 'הכנס לארכיון',\n    'unarchive' => 'הוצא מארכיון',\n    'sort' => 'מיין',\n    'move' => 'הזז',\n    'copy' => 'העתק',\n    'reply' => 'השב',\n    'delete' => 'מחק',\n    'delete_confirm' => 'אשר מחיקה',\n    'search' => 'חיפוש',\n    'search_clear' => 'נקה חיפוש',\n    'reset' => 'איפוס',\n    'remove' => 'הסר',\n    'add' => 'הוסף',\n    'configure' => 'הגדרות',\n    'manage' => 'נהל',\n    'fullscreen' => 'מסך מלא',\n    'favourite' => 'מועדף',\n    'unfavourite' => 'בטל מועדף',\n    'next' => 'הבא',\n    'previous' => 'קודם',\n    'filter_active' => 'מסנן פעיל:',\n    'filter_clear' => 'נקה מסננים',\n    'download' => 'הורדה',\n    'open_in_tab' => 'פתח בכרטיסייה חדשה',\n    'open' => 'פתח',\n\n    // Sort Options\n    'sort_options' => 'אפשרויות מיון',\n    'sort_direction_toggle' => 'החלפת כיוון מיון',\n    'sort_ascending' => 'מיין בסדר עולה',\n    'sort_descending' => 'מיין בסדר יורד',\n    'sort_name' => 'שם',\n    'sort_default' => 'ברירת מחדל',\n    'sort_created_at' => 'תאריך יצירה',\n    'sort_updated_at' => 'תאריך עדכון',\n\n    // Misc\n    'deleted_user' => 'משתמש שנמחק',\n    'no_activity' => 'אין פעילות להציג',\n    'no_items' => 'אין פריטים זמינים',\n    'back_to_top' => 'בחזרה ללמעלה',\n    'skip_to_main_content' => 'דלג לתוכן העיקרי',\n    'toggle_details' => 'הצג/הסתר פרטים',\n    'toggle_thumbnails' => 'הצג/הסתר תמונות',\n    'details' => 'פרטים',\n    'grid_view' => 'תצוגת רשת',\n    'list_view' => 'תצוגת רשימה',\n    'default' => 'ברירת מחדל',\n    'breadcrumb' => 'סימון מסלול',\n    'status' => 'סטטוס',\n    'status_active' => 'פעיל',\n    'status_inactive' => 'לא פעיל',\n    'never' => 'אף פעם',\n    'none' => 'ללא',\n\n    // Header\n    'homepage' => 'דף הבית',\n    'header_menu_expand' => 'הרחב תפריט',\n    'profile_menu' => 'תפריט הפרופיל',\n    'view_profile' => 'הצג פרופיל',\n    'edit_profile' => 'ערוך פרופיל',\n    'dark_mode' => 'מצב לילה',\n    'light_mode' => 'מצב יום',\n    'global_search' => 'חיפוש כללי',\n\n    // Layout tabs\n    'tab_info' => 'מידע',\n    'tab_info_label' => 'כרטיסייה: הצג מידע משני',\n    'tab_content' => 'תוכן',\n    'tab_content_label' => 'כרטיסייה: הצג תוכן ראשי',\n\n    // Email Content\n    'email_action_help' => 'אם לא ניתן ללחות על כפתור ״:actionText״, יש להעתיק ולהדביק את הכתובת למטה אל דפדפן האינטרנט שלך:',\n    'email_rights' => 'כל הזכויות שמורות',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'מדיניות הפרטיות',\n    'terms_of_service' => 'תנאי שימוש',\n\n    // OpenSearch\n    'opensearch_description' => 'חפש :appName',\n];\n"
  },
  {
    "path": "lang/he/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'בחירת תמונה',\n    'image_list' => 'רשימת תמונות',\n    'image_details' => 'פרטי תמונה',\n    'image_upload' => 'העלאת תמונה',\n    'image_intro' => 'כאן ניתן לבחור ולנהל תמונות אשר הועלו למערכת.',\n    'image_intro_upload' => 'ניתן לגרור תמונות לחלון זה, או על ידי לחיצה על כפתור \"העלאת תמונות\" למעלה.',\n    'image_all' => 'הכל',\n    'image_all_title' => 'הצג את כל התמונות',\n    'image_book_title' => 'הצג תמונות שהועלו לספר זה',\n    'image_page_title' => 'הצג תמונות שהועלו לדף זה',\n    'image_search_hint' => 'חפש תמונה לפי שם',\n    'image_uploaded' => 'הועלה :uploadedDate',\n    'image_uploaded_by' => 'הועלה על ידי :userName',\n    'image_uploaded_to' => 'הועלה ל :pageLink',\n    'image_updated' => 'עודכן :updateDate',\n    'image_load_more' => 'טען עוד',\n    'image_image_name' => 'שם התמונה',\n    'image_delete_used' => 'תמונה זו בשימוש בדפים שמתחת',\n    'image_delete_confirm_text' => 'האם את/ה בטוח/ה שברצונך למחוק את התמונה הזו?',\n    'image_select_image' => 'בחר תמונה',\n    'image_dropzone' => 'גרור תמונות או לחץ כאן להעלאה',\n    'image_dropzone_drop' => 'גרור תמונות לכאן על מנת להעלאות אותם',\n    'images_deleted' => 'התמונות נמחקו',\n    'image_preview' => 'תצוגה מקדימה',\n    'image_upload_success' => 'התמונה עלתה בהצלחה',\n    'image_update_success' => 'פרטי התמונה עודכנו בהצלחה',\n    'image_delete_success' => 'התמונה נמחקה בהצלחה',\n    'image_replace' => 'החלפת תמונה',\n    'image_replace_success' => 'תמונה עודכנה בהצלחה',\n    'image_rebuild_thumbs' => 'יצר ווריאציות גודל מחדש',\n    'image_rebuild_thumbs_success' => 'ווריאציות גודל תמונה יוצרו מחדש בהצלחה!',\n\n    // Code Editor\n    'code_editor' => 'ערוך קוד',\n    'code_language' => 'שפת הקוד',\n    'code_content' => 'תוכן הקוד',\n    'code_session_history' => 'היסטורית ה-Session',\n    'code_save' => 'שמור קוד',\n];\n"
  },
  {
    "path": "lang/he/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'כללי',\n    'advanced' => 'הגדרות מתקדמות',\n    'none' => 'ללא',\n    'cancel' => 'ביטול',\n    'save' => 'שמור',\n    'close' => 'סגור',\n    'apply' => 'Apply',\n    'undo' => 'אחזר',\n    'redo' => 'בצע שוב',\n    'left' => 'שמאל',\n    'center' => 'מרכז',\n    'right' => 'ימין',\n    'top' => 'עליון',\n    'middle' => 'אמצע',\n    'bottom' => 'תחתון',\n    'width' => 'רוחב',\n    'height' => 'גובה',\n    'More' => 'עוד',\n    'select' => 'Select...',\n\n    // Toolbar\n    'formats' => 'פורמט',\n    'header_large' => 'מקטע עליון גדול',\n    'header_medium' => 'מקטע עליון בינוני',\n    'header_small' => 'מקטע עליון קטן',\n    'header_tiny' => 'מקטע עליון קטנטן',\n    'paragraph' => 'פסקה',\n    'blockquote' => 'ציטוט',\n    'inline_code' => 'קוד מוטבע',\n    'callouts' => 'בועית-הסבר',\n    'callout_information' => 'מידע',\n    'callout_success' => 'הצלחה',\n    'callout_warning' => 'אזהרה',\n    'callout_danger' => 'סכנה',\n    'bold' => 'מודגש',\n    'italic' => 'נטוי',\n    'underline' => 'קו תחתון',\n    'strikethrough' => 'קו חוצה',\n    'superscript' => 'כתב עילי',\n    'subscript' => 'כתב תחתי',\n    'text_color' => 'צבע טקסט',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'צבע מותאם',\n    'remove_color' => 'הסר צבע',\n    'background_color' => 'צבע רקע',\n    'align_left' => 'יישור לשמאל',\n    'align_center' => 'מרכוז',\n    'align_right' => 'יישור לימין',\n    'align_justify' => 'Justify',\n    'list_bullet' => 'רשימת תבליט',\n    'list_numbered' => 'רשימה ממוספרת',\n    'list_task' => 'Task list',\n    'indent_increase' => 'הגדל הזחה',\n    'indent_decrease' => 'הקטן הזחה',\n    'table' => 'טבלה',\n    'insert_image' => 'הכנס תמונה',\n    'insert_image_title' => 'Insert/Edit Image',\n    'insert_link' => 'Insert/edit link',\n    'insert_link_title' => 'Insert/Edit Link',\n    'insert_horizontal_line' => 'Insert horizontal line',\n    'insert_code_block' => 'Insert code block',\n    'edit_code_block' => 'Edit code block',\n    'insert_drawing' => 'Insert/edit drawing',\n    'drawing_manager' => 'Drawing manager',\n    'insert_media' => 'Insert/edit media',\n    'insert_media_title' => 'Insert/Edit Media',\n    'clear_formatting' => 'Clear formatting',\n    'source_code' => 'Source code',\n    'source_code_title' => 'Source Code',\n    'fullscreen' => 'Fullscreen',\n    'image_options' => 'Image options',\n\n    // Tables\n    'table_properties' => 'Table properties',\n    'table_properties_title' => 'Table Properties',\n    'delete_table' => 'Delete table',\n    'table_clear_formatting' => 'Clear table formatting',\n    'resize_to_contents' => 'Resize to contents',\n    'row_header' => 'Row header',\n    'insert_row_before' => 'Insert row before',\n    'insert_row_after' => 'Insert row after',\n    'delete_row' => 'Delete row',\n    'insert_column_before' => 'Insert column before',\n    'insert_column_after' => 'Insert column after',\n    'delete_column' => 'Delete column',\n    'table_cell' => 'Cell',\n    'table_row' => 'Row',\n    'table_column' => 'Column',\n    'cell_properties' => 'Cell properties',\n    'cell_properties_title' => 'Cell Properties',\n    'cell_type' => 'Cell type',\n    'cell_type_cell' => 'Cell',\n    'cell_scope' => 'Scope',\n    'cell_type_header' => 'Header cell',\n    'merge_cells' => 'Merge cells',\n    'split_cell' => 'Split cell',\n    'table_row_group' => 'Row Group',\n    'table_column_group' => 'Column Group',\n    'horizontal_align' => 'Horizontal align',\n    'vertical_align' => 'Vertical align',\n    'border_width' => 'Border width',\n    'border_style' => 'Border style',\n    'border_color' => 'Border color',\n    'row_properties' => 'Row properties',\n    'row_properties_title' => 'Row Properties',\n    'cut_row' => 'Cut row',\n    'copy_row' => 'Copy row',\n    'paste_row_before' => 'Paste row before',\n    'paste_row_after' => 'Paste row after',\n    'row_type' => 'Row type',\n    'row_type_header' => 'Header',\n    'row_type_body' => 'תוכן',\n    'row_type_footer' => 'Footer',\n    'alignment' => 'Alignment',\n    'cut_column' => 'Cut column',\n    'copy_column' => 'העתק עמודה',\n    'paste_column_before' => 'הדבק עמודה לפני',\n    'paste_column_after' => 'הדבק עמודה אחרי',\n    'cell_padding' => 'ריפוד תא',\n    'cell_spacing' => 'ריווח תא',\n    'caption' => 'כיתוב',\n    'show_caption' => 'הצג כיתוב',\n    'constrain' => 'הגדרת אילוצים',\n    'cell_border_solid' => 'Solid',\n    'cell_border_dotted' => 'Dotted',\n    'cell_border_dashed' => 'Dashed',\n    'cell_border_double' => 'Double',\n    'cell_border_groove' => 'Groove',\n    'cell_border_ridge' => 'Ridge',\n    'cell_border_inset' => 'Inset',\n    'cell_border_outset' => 'Outset',\n    'cell_border_none' => 'None',\n    'cell_border_hidden' => 'Hidden',\n\n    // Images, links, details/summary & embed\n    'source' => 'קוד מקור',\n    'alt_desc' => 'תיאור חלופי',\n    'embed' => 'הטמע',\n    'paste_embed' => 'הדבק את הקוד המוטמע מטה:',\n    'url' => 'קישור',\n    'text_to_display' => 'טקסט שיוצג',\n    'title' => 'כותרת',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Open link',\n    'open_link_in' => 'Open link in...',\n    'open_link_current' => 'החלון הנוכחי',\n    'open_link_new' => 'חלון חדש',\n    'remove_link' => 'Remove link',\n    'insert_collapsible' => 'הכנס מקטע הניתן לכיווץ',\n    'collapsible_unwrap' => 'בטל גלישת שורות',\n    'edit_label' => 'עריכת תווית',\n    'toggle_open_closed' => 'החלף פתיחה\\סגירה',\n    'collapsible_edit' => 'ערוך מקטע הניתן לכיווץ',\n    'toggle_label' => 'החלף תגית',\n\n    // About view\n    'about' => 'About the editor',\n    'about_title' => 'אודות עורך הטקסט הויזואלי',\n    'editor_license' => 'רשיון וזכויות העורך',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under the MIT license.',\n    'editor_tiny_license_link' => 'The copyright and license details of TinyMCE can be found here.',\n    'save_continue' => 'שמור עמוד והמשך',\n    'callouts_cycle' => '(Keep pressing to toggle through types)',\n    'link_selector' => 'קישור לתוכן',\n    'shortcuts' => 'קיצורי דרך',\n    'shortcut' => 'קיצור דרך',\n    'shortcuts_intro' => 'הקיצורים הבאים זמינים בעורך הטקסט:',\n    'windows_linux' => '(חלונות\\לינוקס)',\n    'mac' => '(מאק)',\n    'description' => 'תיאור',\n];\n"
  },
  {
    "path": "lang/he/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'נוצר לאחרונה',\n    'recently_created_pages' => 'דפים שנוצרו לאחרונה',\n    'recently_updated_pages' => 'דפים שעודכנו לאחרונה',\n    'recently_created_chapters' => 'פרקים שנוצרו לאחרונה',\n    'recently_created_books' => 'ספרים שנוצרו לאחרונה',\n    'recently_created_shelves' => 'מדפים שנוצרו לאחרונה',\n    'recently_update' => 'עודכן לאחרונה',\n    'recently_viewed' => 'נצפה לאחרונה',\n    'recent_activity' => 'פעילות לאחרונה',\n    'create_now' => 'צור אחד כעת',\n    'revisions' => 'עדכונים',\n    'meta_revision' => 'עדכון #:revisionCount',\n    'meta_created' => 'נוצר :timeLength',\n    'meta_created_name' => 'נוצר :timeLength על ידי :user',\n    'meta_updated' => 'עודכן :timeLength',\n    'meta_updated_name' => 'עודכן :timeLength על ידי :user',\n    'meta_owned_name' => 'בבעלות של :user',\n    'meta_reference_count' => '',\n    'entity_select' => 'בחר יישות',\n    'entity_select_lack_permission' => 'אין לך אישורים דרושים לבחירת פריט זה',\n    'images' => 'תמונות',\n    'my_recent_drafts' => 'הטיוטות האחרונות שלי',\n    'my_recently_viewed' => 'הנצפים לאחרונה שלי',\n    'my_most_viewed_favourites' => 'האהובים הנצפים ביותר שלי',\n    'my_favourites' => 'המועדפים שלי',\n    'no_pages_viewed' => 'לא צפית בדפים כלשהם',\n    'no_pages_recently_created' => 'לא נוצרו דפים לאחרונה',\n    'no_pages_recently_updated' => 'לא עודכנו דפים לאחרונה',\n    'export' => 'ייצוא',\n    'export_html' => 'דף אינטרנט',\n    'export_pdf' => 'קובץ PDF',\n    'export_text' => 'טקסט רגיל',\n    'export_md' => 'קובץ Markdown',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'תבנית דף ברירת מחדל',\n    'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',\n    'default_template_select' => 'בחר דף תבנית',\n    'import' => 'יבוא',\n    'import_validate' => 'תקף יבוא',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'בחר קובץ ZIP להעלאה',\n    'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',\n    'import_pending' => 'Pending Imports',\n    'import_pending_none' => 'No imports have been started.',\n    'import_continue' => 'המשך יבוא',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'פרטי יבוא',\n    'import_run' => 'להריץ יבוא',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'הועלה על ידי',\n    'import_location' => 'מיקום יבוא',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'תקלות יבוא',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'הרשאות',\n    'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.',\n    'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.',\n    'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.',\n    'permissions_save' => 'שמור הרשאות',\n    'permissions_owner' => 'בעלים',\n    'permissions_role_everyone_else' => 'Everyone Else',\n    'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',\n    'permissions_role_override' => 'Override permissions for role',\n    'permissions_inherit_defaults' => 'Inherit defaults',\n\n    // Search\n    'search_results' => 'תוצאות חיפוש',\n    'search_total_results_found' => ':count תוצאות נמצאו|:count סה״כ תוצאות',\n    'search_clear' => 'נקה חיפוש',\n    'search_no_pages' => 'לא נמצאו דפים התואמים לחיפוש',\n    'search_for_term' => 'חפש את :term',\n    'search_more' => 'תוצאות נוספות',\n    'search_advanced' => 'חיפוש מתקדם',\n    'search_terms' => 'מילות חיפוש',\n    'search_content_type' => 'סוג התוכן',\n    'search_exact_matches' => 'התאמות מדויקות',\n    'search_tags' => 'חפש בתגים',\n    'search_options' => 'אפשרויות',\n    'search_viewed_by_me' => 'נצפו על ידי',\n    'search_not_viewed_by_me' => 'שלא נצפו על ידי',\n    'search_permissions_set' => 'סט הרשאות',\n    'search_created_by_me' => 'שנוצרו על ידי',\n    'search_updated_by_me' => 'שעודכנו על ידי',\n    'search_owned_by_me' => 'בבעלות שלי',\n    'search_date_options' => 'אפשרויות תאריך',\n    'search_updated_before' => 'שעודכנו לפני',\n    'search_updated_after' => 'שעודכנו לאחר',\n    'search_created_before' => 'שנוצרו לפני',\n    'search_created_after' => 'שנוצרו לאחר',\n    'search_set_date' => 'הגדר תאריך',\n    'search_update' => 'עדכן חיפוש',\n\n    // Shelves\n    'shelf' => 'מדף',\n    'shelves' => 'מדפים',\n    'x_shelves' => ':count מדף|:count מדפים',\n    'shelves_empty' => 'לא נוצרו מדפים',\n    'shelves_create' => 'צור מדף חדש',\n    'shelves_popular' => 'מדפים פופולרים',\n    'shelves_new' => 'מדפים חדשים',\n    'shelves_new_action' => 'מדף חדש',\n    'shelves_popular_empty' => 'המדפים הפופולריים ביותר יופיעו כאן',\n    'shelves_new_empty' => 'המדפים שנוצרו לאחרונה יופיעו כאן',\n    'shelves_save' => 'שמור מדף',\n    'shelves_books' => 'ספרים במדף זה',\n    'shelves_add_books' => 'הוסף ספרים למדף זה',\n    'shelves_drag_books' => 'Drag books below to add them to this shelf',\n    'shelves_empty_contents' => 'במדף זה לא קיימים ספרים',\n    'shelves_edit_and_assign' => 'עריכת מדף להוספת ספרים',\n    'shelves_edit_named' => 'Edit Shelf :name',\n    'shelves_edit' => 'Edit Shelf',\n    'shelves_delete' => 'Delete Shelf',\n    'shelves_delete_named' => 'Delete Shelf :name',\n    'shelves_delete_explain' => \"This will delete the shelf with the name ':name'. Contained books will not be deleted.\",\n    'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',\n    'shelves_permissions' => 'Shelf Permissions',\n    'shelves_permissions_updated' => 'Shelf Permissions Updated',\n    'shelves_permissions_active' => 'Shelf Permissions Active',\n    'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',\n    'shelves_permissions_create' => 'Shelf create permissions are only used for copying permissions to child books using the action below. They do not control the ability to create books.',\n    'shelves_copy_permissions_to_books' => 'העתק הרשאות מדף אל הספרים',\n    'shelves_copy_permissions' => 'העתק הרשאות',\n    'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',\n    'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',\n\n    // Books\n    'book' => 'ספר',\n    'books' => 'ספרים',\n    'x_books' => ':count ספר|:count ספרים',\n    'books_empty' => 'לא נוצרו ספרים',\n    'books_popular' => 'ספרים פופולריים',\n    'books_recent' => 'ספרים אחרונים',\n    'books_new' => 'ספרים חדשים',\n    'books_new_action' => 'ספר חדש',\n    'books_popular_empty' => 'הספרים הפופולריים יופיעו כאן',\n    'books_new_empty' => 'הספרים שנוצרו לאחרונה יופיעו כאן',\n    'books_create' => 'צור ספר חדש',\n    'books_delete' => 'מחק ספר',\n    'books_delete_named' => 'מחק ספר :bookName',\n    'books_delete_explain' => 'פעולה זו תמחק את הספר :bookName, כל הדפים והפרקים ימחקו גם כן.',\n    'books_delete_confirmation' => 'האם ברצונך למחוק את הספר הזה?',\n    'books_edit' => 'ערוך ספר',\n    'books_edit_named' => 'עריכת ספר :bookName',\n    'books_form_book_name' => 'שם הספר',\n    'books_save' => 'שמור ספר',\n    'books_permissions' => 'הרשאות ספר',\n    'books_permissions_updated' => 'הרשאות הספר עודכנו',\n    'books_empty_contents' => 'לא נוצרו פרקים או דפים עבור ספר זה',\n    'books_empty_create_page' => 'צור דף חדש',\n    'books_empty_sort_current_book' => 'מיין את הספר הנוכחי',\n    'books_empty_add_chapter' => 'הוסף פרק',\n    'books_permissions_active' => 'הרשאות ספר פעילות',\n    'books_search_this' => 'חפש בספר זה',\n    'books_navigation' => 'ניווט בספר',\n    'books_sort' => 'מיין את תוכן הספר',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'מיין את הספר :bookName',\n    'books_sort_name' => 'מיין לפי שם',\n    'books_sort_created' => 'מיין לפי תאריך יצירה',\n    'books_sort_updated' => 'מיין לפי תאריך עדכון',\n    'books_sort_chapters_first' => 'פרקים בהתחלה',\n    'books_sort_chapters_last' => 'פרקים בסוף',\n    'books_sort_show_other' => 'הצג ספרים אחרונים',\n    'books_sort_save' => 'שמור את הסדר החדש',\n    'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.',\n    'books_sort_move_up' => 'Move Up',\n    'books_sort_move_down' => 'Move Down',\n    'books_sort_move_prev_book' => 'Move to Previous Book',\n    'books_sort_move_next_book' => 'Move to Next Book',\n    'books_sort_move_prev_chapter' => 'Move Into Previous Chapter',\n    'books_sort_move_next_chapter' => 'Move Into Next Chapter',\n    'books_sort_move_book_start' => 'Move to Start of Book',\n    'books_sort_move_book_end' => 'Move to End of Book',\n    'books_sort_move_before_chapter' => 'Move to Before Chapter',\n    'books_sort_move_after_chapter' => 'Move to After Chapter',\n    'books_copy' => 'העתק ספר',\n    'books_copy_success' => 'ספר הועתק בהצלחה',\n\n    // Chapters\n    'chapter' => 'פרק',\n    'chapters' => 'פרקים',\n    'x_chapters' => ':count פרק|:count פרקים',\n    'chapters_popular' => 'פרקים פופולריים',\n    'chapters_new' => 'פרק חדש',\n    'chapters_create' => 'צור פרק חדש',\n    'chapters_delete' => 'מחק פרק',\n    'chapters_delete_named' => 'מחק את פרק :chapterName',\n    'chapters_delete_explain' => 'This will delete the chapter with the name \\':chapterName\\'. All pages that exist within this chapter will also be deleted.',\n    'chapters_delete_confirm' => 'האם ברצונך למחוק פרק זה?',\n    'chapters_edit' => 'ערוך פרק',\n    'chapters_edit_named' => 'ערוך פרק :chapterName',\n    'chapters_save' => 'שמור פרק',\n    'chapters_move' => 'העבר פרק',\n    'chapters_move_named' => 'העבר פרק :chapterName',\n    'chapters_copy' => 'העתק פרק',\n    'chapters_copy_success' => 'פרק הועתק בהצלחה',\n    'chapters_permissions' => 'הרשאות פרק',\n    'chapters_empty' => 'לא נמצאו דפים בפרק זה.',\n    'chapters_permissions_active' => 'הרשאות פרק פעילות',\n    'chapters_permissions_success' => 'הרשאות פרק עודכנו',\n    'chapters_search_this' => 'חפש בפרק זה',\n    'chapter_sort_book' => 'Sort Book',\n\n    // Pages\n    'page' => 'דף',\n    'pages' => 'דפים',\n    'x_pages' => ':count דף|:count דפים',\n    'pages_popular' => 'דפים פופולריים',\n    'pages_new' => 'דף חדש',\n    'pages_attachments' => 'קבצים מצורפים',\n    'pages_navigation' => 'ניווט בדף',\n    'pages_delete' => 'מחק דף',\n    'pages_delete_named' => 'מחק דף :pageName',\n    'pages_delete_draft_named' => 'מחק טיוטת דף :pageName',\n    'pages_delete_draft' => 'מחק טיוטת דף',\n    'pages_delete_success' => 'הדף נמחק',\n    'pages_delete_draft_success' => 'טיוטת דף נמחקה',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'האם ברצונך למחוק דף זה?',\n    'pages_delete_draft_confirm' => 'האם ברצונך למחוק את טיוטת הדף הזה?',\n    'pages_editing_named' => 'עריכת דף :pageName',\n    'pages_edit_draft_options' => 'אפשרויות טיוטה',\n    'pages_edit_save_draft' => 'שמור טיוטה',\n    'pages_edit_draft' => 'ערוך טיוטת דף',\n    'pages_editing_draft' => 'עריכת טיוטה',\n    'pages_editing_page' => 'עריכת דף',\n    'pages_edit_draft_save_at' => 'טיוטה נשמרה ב ',\n    'pages_edit_delete_draft' => 'מחק טיוטה',\n    'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.',\n    'pages_edit_discard_draft' => 'התעלם מהטיוטה',\n    'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',\n    'pages_edit_switch_to_markdown_clean' => '(Clean Content)',\n    'pages_edit_switch_to_markdown_stable' => '(Stable Content)',\n    'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'הגדר יומן שינויים',\n    'pages_edit_enter_changelog_desc' => 'ציין תיאור קצר אודות השינויים שביצעת',\n    'pages_edit_enter_changelog' => 'הכנס יומן שינויים',\n    'pages_editor_switch_title' => 'Switch Editor',\n    'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',\n    'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',\n    'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',\n    'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',\n    'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\\'t persist across this change.',\n    'pages_save' => 'שמור דף',\n    'pages_title' => 'כותרת דף',\n    'pages_name' => 'שם הדף',\n    'pages_md_editor' => 'עורך',\n    'pages_md_preview' => 'תצוגה מקדימה',\n    'pages_md_insert_image' => 'הכנס תמונה',\n    'pages_md_insert_link' => 'הכנס קישור ליישות',\n    'pages_md_insert_drawing' => 'הכנס סרטוט',\n    'pages_md_show_preview' => 'Show preview',\n    'pages_md_sync_scroll' => 'Sync preview scroll',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Unsaved Drawing Found',\n    'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',\n    'pages_not_in_chapter' => 'דף אינו חלק מפרק',\n    'pages_move' => 'העבר דף',\n    'pages_copy' => 'העתק דף',\n    'pages_copy_desination' => 'העתק יעד',\n    'pages_copy_success' => 'הדף הועתק בהצלחה',\n    'pages_permissions' => 'הרשאות דף',\n    'pages_permissions_success' => 'הרשאות הדף עודכנו',\n    'pages_revision' => 'נוסח',\n    'pages_revisions' => 'נוסחי דף',\n    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',\n    'pages_revisions_named' => 'נוסחי דף עבור :pageName',\n    'pages_revision_named' => 'נוסח דף עבור :pageName',\n    'pages_revision_restored_from' => 'Restored from #:id; :summary',\n    'pages_revisions_created_by' => 'נוצר על ידי',\n    'pages_revisions_date' => 'תאריך נוסח',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Revision Number',\n    'pages_revisions_numbered' => 'נוסח #:id',\n    'pages_revisions_numbered_changes' => 'שינויי נוסח #:id',\n    'pages_revisions_editor' => 'Editor Type',\n    'pages_revisions_changelog' => 'יומן שינויים',\n    'pages_revisions_changes' => 'שינויים',\n    'pages_revisions_current' => 'גירסא נוכחית',\n    'pages_revisions_preview' => 'תצוגה מקדימה',\n    'pages_revisions_restore' => 'שחזר',\n    'pages_revisions_none' => 'לדף זה אין נוסחים',\n    'pages_copy_link' => 'העתק קישור',\n    'pages_edit_content_link' => 'Jump to section in editor',\n    'pages_pointer_enter_mode' => 'Enter section select mode',\n    'pages_pointer_label' => 'Page Section Options',\n    'pages_pointer_permalink' => 'Page Section Permalink',\n    'pages_pointer_include_tag' => 'Page Section Include Tag',\n    'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag',\n    'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink',\n    'pages_permissions_active' => 'הרשאות דף פעילות',\n    'pages_initial_revision' => 'פרסום ראשוני',\n    'pages_references_update_revision' => 'System auto-update of internal links',\n    'pages_initial_name' => 'דף חדש',\n    'pages_editing_draft_notification' => 'הינך עורך טיוטה אשר נשמרה לאחרונה ב :timeDiff',\n    'pages_draft_edited_notification' => 'דף זה עודכן מאז, מומלץ להתעלם מהטיוטה הזו.',\n    'pages_draft_page_changed_since_creation' => 'העמוד התעדכן מאז שהטיוטה נוצרה. מומלץ לבטל את הטיוטה או לשים לב לא לדרוס שינויים בעמוד.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count משתמשים החלו לערוך דף זה',\n        'start_b' => ':userName החל לערוך דף זה',\n        'time_a' => 'מאז שהדף עודכן לאחרונה',\n        'time_b' => 'ב :minCount דקות האחרונות',\n        'message' => ':start :time. יש לשים לב לא לדרוס שינויים של משתמשים אחרים!',\n    ],\n    'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',\n    'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',\n    'pages_specific' => 'דף ספציפי',\n    'pages_is_template' => 'תבנית דף',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Toggle Sidebar',\n    'page_tags' => 'תגיות דף',\n    'chapter_tags' => 'תגיות פרק',\n    'book_tags' => 'תגיות ספר',\n    'shelf_tags' => 'תגיות מדף',\n    'tag' => 'תגית',\n    'tags' =>  'תגיות',\n    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',\n    'tag_name' =>  'שם התווית',\n    'tag_value' => 'ערך התגית (אופציונאלי)',\n    'tags_explain' => \"הכנס תגיות על מנת לסדר את התוכן שלך. \\n  ניתן לציין ערך לתגית על מנת לבצע סידור יסודי יותר\",\n    'tags_add' => 'הוסף תגית נוספת',\n    'tags_remove' => 'מחק תווית',\n    'tags_usages' => 'Total tag usages',\n    'tags_assigned_pages' => 'Assigned to Pages',\n    'tags_assigned_chapters' => 'Assigned to Chapters',\n    'tags_assigned_books' => 'Assigned to Books',\n    'tags_assigned_shelves' => 'Assigned to Shelves',\n    'tags_x_unique_values' => ':count unique values',\n    'tags_all_values' => 'כל הערכים',\n    'tags_view_tags' => 'הצג תוויות',\n    'tags_view_existing_tags' => 'View existing tags',\n    'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',\n    'attachments' => 'קבצים מצורפים',\n    'attachments_explain' => 'צרף קבצים או קישורים על מנת להציגם בדף שלך. צירופים אלו יהיו זמינים בתפריט הצדדי של הדף',\n    'attachments_explain_instant_save' => 'שינויים נשמרים באופן מיידי',\n    'attachments_upload' => 'העלה קובץ',\n    'attachments_link' => 'צרף קישור',\n    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',\n    'attachments_set_link' => 'הגדר קישור',\n    'attachments_delete' => 'Are you sure you want to delete this attachment?',\n    'attachments_dropzone' => 'Drop files here to upload',\n    'attachments_no_files' => 'לא הועלו קבצים',\n    'attachments_explain_link' => 'ניתן לצרף קישור במקום העלאת קובץ, קישור זה יכול להוביל לדף אחר או לכל קובץ באינטרנט',\n    'attachments_link_name' => 'שם הקישור',\n    'attachment_link' => 'כתובת הקישור',\n    'attachments_link_url' => 'קישור לקובץ',\n    'attachments_link_url_hint' => 'כתובת האתר או הקובץ',\n    'attach' => 'צרף',\n    'attachments_insert_link' => 'Add Attachment Link to Page',\n    'attachments_edit_file' => 'ערוך קובץ',\n    'attachments_edit_file_name' => 'שם הקובץ',\n    'attachments_edit_drop_upload' => 'גרור קבצים או לחץ כאן על מנת להעלות קבצים במקום הקבצים הקיימים',\n    'attachments_order_updated' => 'סדר הקבצים עודכן',\n    'attachments_updated_success' => 'פרטי הקבצים עודכנו',\n    'attachments_deleted' => 'קובץ מצורף הוסר',\n    'attachments_file_uploaded' => 'הקובץ עלה בהצלחה',\n    'attachments_file_updated' => 'הקובץ עודכן בהצלחה',\n    'attachments_link_attached' => 'הקישור צורף לדף בהצלחה',\n    'templates' => 'תבניות',\n    'templates_set_as_template' => 'הגדר עמוד כתבנית',\n    'templates_explain_set_as_template' => 'ניתן להגדיר עמוד כתבנית כך שהתוכן שלו ישומש בעת יצירת עמודים אחרים. משתמשים אחרים יוכלו לראות את התבנית רק אם ברשותם הרשאות צפייה בעמוד הזה.',\n    'templates_replace_content' => 'החלף תוכן עמוד',\n    'templates_append_content' => 'הוסף בסוף תוכן העמוד',\n    'templates_prepend_content' => 'הוסף בתחילת תוכן העמוד',\n\n    // Profile View\n    'profile_user_for_x' => 'משתמש במערכת כ :time',\n    'profile_created_content' => 'תוכן שנוצר',\n    'profile_not_created_pages' => 'המשתמש :userName לא יצר דפים',\n    'profile_not_created_chapters' => 'המשתמש :userName לא יצר פרקים',\n    'profile_not_created_books' => 'המשתמש :userName לא יצר ספרים',\n    'profile_not_created_shelves' => 'המשתמש :userName לא יצר מדפים',\n\n    // Comments\n    'comment' => 'תגובה',\n    'comments' => 'תגובות',\n    'comment_add' => 'הוסף תגובה',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'השאר תגובה כאן',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'שמור תגובה',\n    'comment_new' => 'תגובה חדשה',\n    'comment_created' => 'הוגב :createDiff',\n    'comment_updated' => 'עודכן :updateDiff על ידי :username',\n    'comment_updated_indicator' => 'Updated',\n    'comment_deleted_success' => 'התגובה נמחקה',\n    'comment_created_success' => 'התגובה נוספה',\n    'comment_updated_success' => 'התגובה עודכנה',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'האם ברצונך למחוק תגובה זו?',\n    'comment_in_reply_to' => 'בתגובה ל :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',\n\n    // Revision\n    'revision_delete_confirm' => 'האם ברצונך למחוק נוסח זה?',\n    'revision_restore_confirm' => 'האם ברצונך לשחזר נוסח זה? תוכן הדף הנוכחי יעודכן לנוסח זה.',\n    'revision_cannot_delete_latest' => 'לא ניתן למחוק את הנוסח האחרון',\n\n    // Copy view\n    'copy_consider' => 'Please consider the below when copying content.',\n    'copy_consider_permissions' => 'Custom permission settings will not be copied.',\n    'copy_consider_owner' => 'You will become the owner of all copied content.',\n    'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',\n    'copy_consider_attachments' => 'Page attachments will not be copied.',\n    'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',\n\n    // Conversions\n    'convert_to_shelf' => 'Convert to Shelf',\n    'convert_to_shelf_contents_desc' => 'You can convert this book to a new shelf with the same contents. Chapters contained within this book will be converted to new books. If this book contains any pages, that are not in a chapter, this book will be renamed and contain such pages, and this book will become part of the new shelf.',\n    'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.',\n    'convert_book' => 'Convert Book',\n    'convert_book_confirm' => 'Are you sure you want to convert this book?',\n    'convert_undo_warning' => 'This cannot be as easily undone.',\n    'convert_to_book' => 'Convert to Book',\n    'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',\n    'convert_chapter' => 'Convert Chapter',\n    'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',\n\n    // References\n    'references' => 'References',\n    'references_none' => 'There are no tracked references to this item.',\n    'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',\n\n    // Watch Options\n    'watch' => 'Watch',\n    'watch_title_default' => 'Default Preferences',\n    'watch_desc_default' => 'Revert watching to just your default notification preferences.',\n    'watch_title_ignore' => 'Ignore',\n    'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.',\n    'watch_title_new' => 'New Pages',\n    'watch_desc_new' => 'Notify when any new page is created within this item.',\n    'watch_title_updates' => 'All Page Updates',\n    'watch_desc_updates' => 'Notify upon all new pages and page changes.',\n    'watch_desc_updates_page' => 'Notify upon all page changes.',\n    'watch_title_comments' => 'All Page Updates & Comments',\n    'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.',\n    'watch_desc_comments_page' => 'Notify upon page changes and new comments.',\n    'watch_change_default' => 'Change default notification preferences',\n    'watch_detail_ignore' => 'Ignoring notifications',\n    'watch_detail_new' => 'Watching for new pages',\n    'watch_detail_updates' => 'Watching new pages and updates',\n    'watch_detail_comments' => 'Watching new pages, updates & comments',\n    'watch_detail_parent_book' => 'Watching via parent book',\n    'watch_detail_parent_book_ignore' => 'Ignoring via parent book',\n    'watch_detail_parent_chapter' => 'Watching via parent chapter',\n    'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter',\n];\n"
  },
  {
    "path": "lang/he/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'אין לך הרשאות על מנת לצפות בדף המבוקש.',\n    'permissionJson' => 'אין לך הרשאות על מנת לבצע את הפעולה המבוקשת.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'משתמש עם המייל :email כבר קיים אך עם פרטי הזדהות שונים',\n    'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',\n    'email_already_confirmed' => 'המייל כבר אומת, אנא נסה להתחבר',\n    'email_confirmation_invalid' => 'מפתח האימות אינו תקין או שכבר נעשה בו שימוש, אנא נסה להרשם שנית',\n    'email_confirmation_expired' => 'מפתח האימות פג-תוקף, מייל אימות חדש נשלח שוב.',\n    'email_confirmation_awaiting' => 'The email address for the account in use needs to be confirmed',\n    'ldap_fail_anonymous' => 'גישת LDAP נדחתה בעת השימוש ב bind אנונימי',\n    'ldap_fail_authed' => 'LDAP access failed using given dn & password details',\n    'ldap_extension_not_installed' => 'הרחבת LDAP עבור PHP לא מותקנת',\n    'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed',\n    'saml_already_logged_in' => 'כבר מחובר',\n    'saml_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',\n    'saml_invalid_response_id' => 'The request from the external authentication system is not recognised by a process started by this application. Navigating back after a login could cause this issue.',\n    'saml_fail_authed' => 'Login using :system failed, system did not provide successful authorization',\n    'oidc_already_logged_in' => 'כבר מחובר',\n    'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',\n    'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',\n    'social_no_action_defined' => 'לא הוגדרה פעולה',\n    'social_login_bad_response' => \"Error received during :socialAccount login: \\n:error\",\n    'social_account_in_use' => 'החשבון :socialAccount כבר בשימוש. אנא נסה להתחבר באמצעות אפשרות :socialAccount',\n    'social_account_email_in_use' => 'המייל :email כבר נמצא בשימוש. אם כבר יש ברשותך חשבון ניתן להתחבר באמצעות :socialAccount ממסך הגדרות הפרופיל שלך.',\n    'social_account_existing' => 'ה - :socialAccount כבר מחובר לחשבון שלך.',\n    'social_account_already_used_existing' => 'This :socialAccount account is already used by another user.',\n    'social_account_not_used' => 'החשבון :socialAccount אינו מחובר למשתמש כלשהוא. אנא חבר אותו לחשבונך במסך הגדרות הפרופיל שלך.',\n    'social_account_register_instructions' => 'אם אין ברשותך חשבון, תוכל להרשם באמצעות :socialAccount',\n    'social_driver_not_found' => 'Social driver not found',\n    'social_driver_not_configured' => 'הגדרות ה :socialAccount אינן מוגדרות כראוי',\n    'invite_token_expired' => 'לינק ההזמנה פג. אתה יכול לנסות לאפס את סיסמת החשבון שלך במקום.',\n    'login_user_not_found' => 'A user for this action could not be found.',\n\n    // System\n    'path_not_writable' => 'לא ניתן להעלות את :filePath אנא ודא שניתן לכתוב למיקום זה',\n    'cannot_get_image_from_url' => 'לא ניתן לקבל תמונה מ :url',\n    'cannot_create_thumbs' => 'The server cannot create thumbnails. Please check you have the GD PHP extension installed.',\n    'server_upload_limit' => 'השרת אינו מרשה העלאת קבצים במשקל זה. אנא נסה להעלות קובץ קטן יותר.',\n    'server_post_limit' => 'The server cannot receive the provided amount of data. Try again with less data or a smaller file.',\n    'uploaded'  => 'השרת אינו מרשה העלאת קבצים במשקל זה. אנא נסה להעלות קובץ קטן יותר.',\n\n    // Drawing & Images\n    'image_upload_error' => 'התרחשה שגיאה במהלך העלאת התמונה',\n    'image_upload_type_error' => 'התמונה שהועלתה אינה תקינה',\n    'image_upload_replace_type' => 'Image file replacements must be of the same type',\n    'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.',\n    'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.',\n    'image_gallery_thumbnail_memory_limit' => 'Failed to create gallery thumbnails due to system resource limits.',\n    'drawing_data_not_found' => 'Drawing data could not be loaded. The drawing file might no longer exist or you may not have permission to access it.',\n\n    // Attachments\n    'attachment_not_found' => 'קובץ מצורף לא נמצא',\n    'attachment_upload_error' => 'An error occurred uploading the attachment file',\n\n    // Pages\n    'page_draft_autosave_fail' => 'שגיאה בשמירת הטיוטה. אנא ודא כי חיבור האינטרנט תקין לפני שמירת דף זה.',\n    'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content',\n    'page_custom_home_deletion' => 'לא ניתן למחוק דף אשר מוגדר כדף הבית',\n\n    // Entities\n    'entity_not_found' => 'פריט לא נמצא',\n    'bookshelf_not_found' => 'מדף לא נמצא',\n    'book_not_found' => 'ספר לא נמצא',\n    'page_not_found' => 'דף לא נמצא',\n    'chapter_not_found' => 'פרק לא נמצא',\n    'selected_book_not_found' => 'הספר שנבחר לא נמצא',\n    'selected_book_chapter_not_found' => 'הפרק או הספר שנבחר לא נמצאו',\n    'guests_cannot_save_drafts' => 'אורחים אינם יכולים לשמור סקיצות',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'אינך יכול למחוק את המנהל היחיד',\n    'users_cannot_delete_guest' => 'אינך יכול למחוק את משתמש האורח',\n    'users_could_not_send_invite' => 'Could not create user since invite email failed to send',\n\n    // Roles\n    'role_cannot_be_edited' => 'לא ניתן לערוך תפקיד זה',\n    'role_system_cannot_be_deleted' => 'תפקיד זה הינו תפקיד מערכת ולא ניתן למחיקה',\n    'role_registration_default_cannot_delete' => 'לא ניתן למחוק תפקיד זה מכיוון שהוא מוגדר כתפקיד ברירת המחדל בעת הרשמה',\n    'role_cannot_remove_only_admin' => 'משתמש זה הינו המשתמש היחיד המשוייך לפקיד המנהל. נסה לשייך את תפקיד המנהל למשתמש נוסף לפני הסרה כאן',\n\n    // Comments\n    'comment_list' => 'התרחשה שגיאה בעת שליפת התגובות',\n    'cannot_add_comment_to_draft' => 'אינך יכול להוסיף תגובות לסקיצה זו',\n    'comment_add' => 'התרחשה שגיאה בעת הוספה / עדכון התגובה',\n    'comment_delete' => 'התרחשה שגיאה בעת מחיקת התגובה',\n    'empty_comment' => 'לא ניתן להוסיף תגובה ריקה',\n\n    // Error pages\n    '404_page_not_found' => 'דף לא קיים',\n    'sorry_page_not_found' => 'מצטערים, הדף שחיפשת אינו קיים',\n    'sorry_page_not_found_permission_warning' => 'במידה וציפיתי שדף זה יהיה קיים, ייתכן וחסרות לך ההרשאות לראותו.',\n    'image_not_found' => 'תמונה לא נמצאה',\n    'image_not_found_subtitle' => 'מצטערים, לא היה ניתן למצוא את קובץ התמונה שחיפשת.',\n    'image_not_found_details' => 'במידה וציפית שתמונה זאת תהיה קיימת ייתכן והיא כבר נמחקה.',\n    'return_home' => 'בחזרה לדף הבית',\n    'error_occurred' => 'התרחשה שגיאה',\n    'app_down' => ':appName כרגע אינו זמין',\n    'back_soon' => 'מקווים שיחזור במהרה',\n\n    // Import\n    'import_zip_cant_read' => 'Could not read ZIP file.',\n    'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',\n    'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Import ZIP failed to validate with errors:',\n    'import_zip_failed_notification' => 'Failed to import ZIP file.',\n    'import_perms_books' => 'You are lacking the required permissions to create books.',\n    'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',\n    'import_perms_pages' => 'You are lacking the required permissions to create pages.',\n    'import_perms_images' => 'You are lacking the required permissions to create images.',\n    'import_perms_attachments' => 'You are lacking the required permission to create attachments.',\n\n    // API errors\n    'api_no_authorization_found' => 'No authorization token found on the request',\n    'api_bad_authorization_format' => 'An authorization token was found on the request but the format appeared incorrect',\n    'api_user_token_not_found' => 'No matching API token was found for the provided authorization token',\n    'api_incorrect_token_secret' => 'The secret provided for the given used API token is incorrect',\n    'api_user_no_api_permission' => 'The owner of the used API token does not have permission to make API calls',\n    'api_user_token_expired' => 'The authorization token used has expired',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Error thrown when sending a test email:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'The URL does not match the configured allowed SSR hosts',\n];\n"
  },
  {
    "path": "lang/he/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'תגובה חדשה בדף: :pageName',\n    'new_comment_intro' => 'משתמש רשם על עמוד ב :appName:',\n    'new_page_subject' => 'עמוד חדש: PageName',\n    'new_page_intro' => 'עמוד חדש נפתח ב:appName:',\n    'updated_page_subject' => 'עמוד עודכן :pageName',\n    'updated_page_intro' => 'דף עודכן ב:appName:',\n    'updated_page_debounce' => 'על מנת לעצור הצפת התראות, לזמן מסוים אתה לא תקבל התראות על שינויים עתידיים בדף זה על ידי אותו עורך.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'שם עמוד:',\n    'detail_page_path' => 'נתיב לעמוד:',\n    'detail_commenter' => 'יוצר התגובה:',\n    'detail_comment' => 'תגובה:',\n    'detail_created_by' => 'נוצר על ידי:',\n    'detail_updated_by' => 'עודכן על ידי:',\n\n    'action_view_comment' => 'צפה בתגובה',\n    'action_view_page' => 'הצג דף',\n\n    'footer_reason' => 'ההתראה נשלחה אליך בגלל :link לכסות סוג זה של פעילות עבור פריט זה.',\n    'footer_reason_link' => 'העדפות ההתראות שלך',\n];\n"
  },
  {
    "path": "lang/he/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; הקודם',\n    'next'     => 'הבא &raquo;',\n\n];\n"
  },
  {
    "path": "lang/he/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'הסיסמא חייבת להיות בעלת 8 תווים לפחות ולהתאים לאימות.',\n    'user' => \"לא נמצא משתמש קיים עם כתובת דוא\\\"ל זו.\",\n    'token' => 'הקישור לאיפוס הסיסמה לא תקף עבור כתובת דוא\"ל זו.',\n    'sent' => 'שלחנו לך דוא\"ל עם קישור לאיפוס הסיסמא!',\n    'reset' => 'איפוס הסיסמה הושלם בהצלחה!',\n\n];\n"
  },
  {
    "path": "lang/he/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'החשבון שלי',\n\n    'shortcuts' => 'קיצורי דרך',\n    'shortcuts_interface' => 'העדפות קיצורי ממשק משתמש',\n    'shortcuts_toggle_desc' => 'כאן תוכל להפעיל או לבטל קיצורי דרך לממשק מערכת המקלדת, המשמשים לניווט ולפעולות.',\n    'shortcuts_customize_desc' => 'אתה יכול להתאים אישית כל אחד מקיצורי הדרך למטה. פשוט לחץ על צירוף המקשים הרצוי לאחר בחירת הקלט לקיצור דרך.',\n    'shortcuts_toggle_label' => 'קיצורי מקשים מופעלים',\n    'shortcuts_section_navigation' => 'ניווט',\n    'shortcuts_section_actions' => 'פעולות נפוצות',\n    'shortcuts_save' => 'שמור קיצורי דרך',\n    'shortcuts_overlay_desc' => 'הערה: כאשר קיצורי דרך מופעלים, שכבת-על מסייעת זמינה באמצעות לחיצה על \"?\" אשר ידגיש את קיצורי הדרך הזמינים לפעולות הנראות כעת על המסך.',\n    'shortcuts_update_success' => 'העדפותיך נשמרו!',\n    'shortcuts_overview_desc' => 'ניהול קיצורי מקלדת שבשימוש לניווט מהיר בממשק המשתמש.',\n\n    'notifications' => 'הגדרת התראות',\n    'notifications_desc' => 'העדפות קבלת מייל והתראות כאשר מבוצעת פעולה מסויימת במערכת.',\n    'notifications_opt_own_page_changes' => 'עדכן אותי כאשר מתבצעים שינויים לדפים שבבעלותי',\n    'notifications_opt_own_page_comments' => 'עדכן אותי כאשר נוספות הערות לדפים שבבעלותי',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'עדכן אותי כאשר מתקבלות תגובות להערות שלי',\n    'notifications_save' => 'שמור העדפות',\n    'notifications_update_success' => 'הגדרת התראות עודכנו!',\n    'notifications_watched' => 'פריטים נצפים וברשימת התעלמות',\n    'notifications_watched_desc' => 'להן רשימת הפריטים אשר קיימת עבורם הגדרות מותאמות אישית. על מנת לעדכן רשימה זו, צפה בפריט ומצא את אפשרות הצפייה / מעקב בפאנל הצידי.',\n\n    'auth' => 'גישה ואבטחה',\n    'auth_change_password' => 'שינוי סיסמה',\n    'auth_change_password_desc' => 'שינוי הסיסמה לכניסה למערכת. הסיסמה חייבת להיות לפחות 8 תווים.',\n    'auth_change_password_success' => 'הסיסמה עודכנה בהצלחה!',\n\n    'profile' => 'פרטי הפרופיל',\n    'profile_desc' => 'נהל את פרטי החשבון שלך אשר יוצגו לאחרים במערכת, בנוסף לפרטים אשר נועדו להתאמה אישית במערכת ויצירת קשר.',\n    'profile_view_public' => 'צפה בפרופיל הציבורי',\n    'profile_name_desc' => 'הגדר את שם התצוגה שלך אשר יהיה זמין לצפייה לשאר משתמשי המערכת ובכל פעולה אשר תבצע במערכת, ובתוכן אשר שייך לך.',\n    'profile_email_desc' => 'כתובת הדוא\"ל תשמש להתראות, ובהתאם למערכות ההזדהות הפעילות לגישה למערכת.',\n    'profile_email_no_permission' => 'לצערנו אין לך גישה לביצוע שינוי כתובת הדוא\"ל שלך, אם ברצונך לבצע שינוי בכתובת אנא פנה למנהל המערכת על מנת שיבצע שינוי זה עבורך.',\n    'profile_avatar_desc' => 'אנא בחר תמונה שתייצג אותך כלפי אחרים במערכת, תמונה אידיאלית צריכה להיות בגובה ואורך של 256 פיקסלים.',\n    'profile_admin_options' => 'אפשרויות מנהל',\n    'profile_admin_options_desc' => 'אפשרויות ברמת מנהל נוספות, כגון הוספת תפקידים והרשאות למשתמשים, אפשר למצוא עבור המשתמש שלך באיזור \"הגדרות > משתמשים\" במערכת.',\n\n    'delete_account' => 'הסר חשבון',\n    'delete_my_account' => 'הסר את החשבון שלי',\n    'delete_my_account_desc' => 'פעולה זו תמחק את החשבון שלך כולל כל ההגדרות עבור החשבון שלך מהמערכת. אין דרך לשחזר את החשבון לאחר המחיקה. כל תוכן שיצרת כולל קבצים שהעלת לאתר ישארו פעילים.',\n    'delete_my_account_warning' => 'הפעולה אינה ניתנת לביטול, האם למחוק את החשבון?',\n];\n"
  },
  {
    "path": "lang/he/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'הגדרות',\n    'settings_save' => 'שמור הגדרות',\n    'system_version' => 'גירסת מערכת',\n    'categories' => 'קטגוריות',\n\n    // App Settings\n    'app_customization' => 'התאמה אישית',\n    'app_features_security' => 'מאפיינים ואבטחה',\n    'app_name' => 'שם היישום',\n    'app_name_desc' => 'השם הזה יופיע בכותרת ובכל אי-מייל הנשלח מהמערכת',\n    'app_name_header' => 'הצג שם בחלק העליון',\n    'app_public_access' => 'גישה ציבורית',\n    'app_public_access_desc' => 'הפעלת אפשרות זו תאפשר למשתמשים אשר אינם רשומים לגשת לתוכן שלך',\n    'app_public_access_desc_guest' => 'הגדרות הרשאה למשתמשים ציבוריים ניתנות לשינוי דרך משתמש מסוג ״אורח״',\n    'app_public_access_toggle' => 'אפשר גישה ציבורית',\n    'app_public_viewing' => 'לאפשר גישה ציבורית?',\n    'app_secure_images' => 'העלאת תמונות מאובטחת',\n    'app_secure_images_toggle' => 'אפשר העלאת תמונות מאובטחת',\n    'app_secure_images_desc' => 'משיקולי ביצועים, כל התמונות הינן ציבוריות. אפשרות זו מוסיפה מחרוזת אקראית שקשה לנחש לכל כתובת של תמונה. אנא ודא שאפשרות הצגת תוכן התיקייה מבוטל.',\n    'app_default_editor' => 'עורך דפים ברירת מחדל',\n    'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.',\n    'app_custom_html' => 'HTML מותאם אישית לחלק העליון',\n    'app_custom_html_desc' => 'כל קוד שיתווסף כאן, יופיע בתחתית תגית ה head של כל דף. חלק זה שימושי על מנת להגדיר עיצובי CSS והתקנת קוד Analytics',\n    'app_custom_html_disabled_notice' => 'קוד HTML מותאם מבוטל בדף ההגדרות על מנת לוודא ששינויים שגורמים לבעיה יוכלו להיות מבוטלים לאחר מכן',\n    'app_logo' => 'לוגו היישום',\n    'app_logo_desc' => 'This is used in the application header bar, among other areas. This image should be 86px in height. Large images will be scaled down.',\n    'app_icon' => 'Application Icon',\n    'app_icon_desc' => 'This icon is used for browser tabs and shortcut icons. This should be a 256px square PNG image.',\n    'app_homepage' => 'דף הבית של היישום',\n    'app_homepage_desc' => 'אנא בחר דף להצגה בדף הבית במקום דף ברירת המחדל. הרשאות הדף לא יחולו בדפים הנבחרים.',\n    'app_homepage_select' => 'בחר דף',\n    'app_footer_links' => 'קישורים בתחתית הדף',\n    'app_footer_links_desc' => 'הוסיפו קישורים שיוצגו בתחתית האתר. קישורים אלה יוצגו בתחתית רוב הדפים, לרבות אלה שלא מצריכים התחברות. תוכלו להשתמש בתווית \"trans::<key>\" כדי להשתמש בתרגומים המוגדרים על ידי המערכת. לדוגמה: שימוש ב\"trans::common.privacy_policy\" יספק את הטקסט המתורגם \"מדיניות פרטיות\" ו\"trans::common.terms_of_service\" יספק את הטקסט המתורגם \"תנאי השימוש\".',\n    'app_footer_links_label' => 'תווית הקישור',\n    'app_footer_links_url' => 'כתובת ה-URL של הקישור',\n    'app_footer_links_add' => 'הוספת קישור בתחתית הדף',\n    'app_disable_comments' => 'ביטול תגובות',\n    'app_disable_comments_toggle' => 'בטל תגובות',\n    'app_disable_comments_desc' => 'מבטל את התגובות לאורך כל היישום, תגובות קיימות לא יוצגו.',\n\n    // Color settings\n    'color_scheme' => 'Application Color Scheme',\n    'color_scheme_desc' => 'Set the colors to use in the application user interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.',\n    'ui_colors_desc' => 'Set the application primary color and default link color. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the application interface.',\n    'app_color' => 'Primary Color',\n    'link_color' => 'Default Link Color',\n    'content_colors_desc' => 'Set colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.',\n    'bookshelf_color' => 'צבע המדף',\n    'book_color' => 'צבע הספר',\n    'chapter_color' => 'צבע הפרק',\n    'page_color' => 'צבע העמוד',\n    'page_draft_color' => 'צבע טיוטת העמוד',\n\n    // Registration Settings\n    'reg_settings' => 'הרשמה',\n    'reg_enable' => 'אפשר הרשמה',\n    'reg_enable_toggle' => 'אפשר להרשם',\n    'reg_enable_desc' => 'כאשר אפשר להרשם משתמשים יוכלו להכנס באופן עצמאי. בעת ההרשמה המשתמש יקבל הרשאה יחידה כברירת מחדל.',\n    'reg_default_role' => 'הרשאה כברירת מחדל',\n    'reg_enable_external_warning' => 'האפשרות לעיל חסרת השפעה כאשר מתבצע שימוש באותנטיקציה חיצונית מסוג LDAP או SAML. חשבונות משתמש לחברים לא קיימים יווצרו באופן אוטומטי במידה ואותנטיקציה, הנוגדת את המערכת החיצונית בשימוש, מצליחה.',\n    'reg_email_confirmation' => 'אימות כתובת אי-מייל',\n    'reg_email_confirmation_toggle' => 'יש לאמת את כתובת המייל',\n    'reg_confirm_email_desc' => 'אם מופעלת הגבלה לדומיין ספציפי אז אימות המייל לא יבוצע',\n    'reg_confirm_restrict_domain' => 'הגבלה לדומיין ספציפי',\n    'reg_confirm_restrict_domain_desc' => 'הכנס דומיינים מופרדים בפסיק אשר עבורם תוגבל ההרשמה. משתמשים יקבלו אי-מייל על מנת לאמת את כתובת המייל שלהם. <br> לתשומת לבך: משתמש יוכל לשנות את כתובת המייל שלו לאחר ההרשמה',\n    'reg_confirm_restrict_domain_placeholder' => 'אין הגבלה לדומיין',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'תחזוקה',\n    'maint_image_cleanup' => 'ניקוי תמונות',\n    'maint_image_cleanup_desc' => 'סורק את הדפים והגרסאות על מנת למצוא אילו תמונות לא בשימוש. יש לוודא גיבוי מלא של מסד הנתונים והתמונות לפני הרצה',\n    'maint_delete_images_only_in_revisions' => 'מחק בנוסף תמונות שקיימות בגרסאות ישנות של הדף בלבד',\n    'maint_image_cleanup_run' => 'הפעל ניקוי תמונות',\n    'maint_image_cleanup_warning' => 'נמצאו כ :count תמונות אשר לא בשימוש האם ברצונך להמשיך?',\n    'maint_image_cleanup_success' => ':count תמונות שלא בשימוש נמחקו',\n    'maint_image_cleanup_nothing_found' => 'לא נמצאו תמונות אשר לא בשימוש, לא נמחקו קבצים כלל.',\n    'maint_send_test_email' => 'שלח דוא\"ל ניסיוני',\n    'maint_send_test_email_desc' => 'שולח דוא\"ל ניסיוני לכתובת הדוא\"ל המצוינת בפרופיל שלך.',\n    'maint_send_test_email_run' => 'שלח דוא\"ל ניסיוני',\n    'maint_send_test_email_success' => 'דוא\"ל נשלח לכתובת :address',\n    'maint_send_test_email_mail_subject' => 'דוא\"ל ניסיוני',\n    'maint_send_test_email_mail_greeting' => 'נראה ששליחת דוא\"ל עובדת!',\n    'maint_send_test_email_mail_text' => 'ברכות! מאחר וקיבלת התראת דוא\"ל זו, נראה שהגדרות הדוא\"ל שלך הוגדרו כמו שצריך.',\n    'maint_recycle_bin_desc' => 'מדפים, ספרים, פרקים חדשים שנמחקו נשלחים לסל המיחזור, כדי שתוכלו לאחזר אותם או למחוק אותם לצמיתות. ייתכן שפריטים ישנים יותר בסל המיחזור יימחקו באופן אוטומטי לאחר זמן-מה, בהסתמך על הגדרות המערכת.',\n    'maint_recycle_bin_open' => 'פתח את סל המיחזור',\n    'maint_regen_references' => 'Regenerate References',\n    'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',\n    'maint_regen_references_success' => 'Reference index has been regenerated!',\n    'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',\n\n    // Recycle Bin\n    'recycle_bin' => 'סל המיחזור',\n    'recycle_bin_desc' => 'כאן תוכלו לאחזר פריטים שנמחקו או לבחור למחוק אותם מהמערכת לצמיתות. רשימה זו לא מסוננת, בשונה מרשימות פעילות דומות במערכת, בהן מוחלים מסנני הרשאות.',\n    'recycle_bin_deleted_item' => 'פריט שנמחק',\n    'recycle_bin_deleted_parent' => 'הורה',\n    'recycle_bin_deleted_by' => 'נמחק על ידי',\n    'recycle_bin_deleted_at' => 'זמן המחיקה',\n    'recycle_bin_permanently_delete' => 'מחק לצמיתות',\n    'recycle_bin_restore' => 'אחזר',\n    'recycle_bin_contents_empty' => 'סל המיחזור כרגע ריק',\n    'recycle_bin_empty' => 'רוקן את סל המיחזור',\n    'recycle_bin_empty_confirm' => 'פעולה זו תשמיד לצמיתות את כל הפריטים בסל המיחזור, לרבות התוכן בכל פריט. אתם בטוחים שאתם מעוניינים לרוקן את סל המיחזור?',\n    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?',\n    'recycle_bin_destroy_list' => 'פריטים שיושמדו',\n    'recycle_bin_restore_list' => 'פריטים שיאוחזרו',\n    'recycle_bin_restore_confirm' => 'פעולה זו תאחזר את הפריט שנמחק, לרבות רכיבי-הבן שלו, למיקומו המקורי. אם המיקום המקורי נמחק מאז, וכעת נמצא בסל המיחזור, יש לאחזר גם את פריט-האב.',\n    'recycle_bin_restore_deleted_parent' => 'פריט-האב של פריט זה נמחק. פריטים אלה יישארו מחוקים עד שפריט-אב זה יאוחזר.',\n    'recycle_bin_restore_parent' => 'Restore Parent',\n    'recycle_bin_destroy_notification' => 'נמחקו בסה\"כ :count פריטים מסל המיחזור.',\n    'recycle_bin_restore_notification' => 'אוחזרו בסה\"כ :count פריטים מסל המיחזור.',\n\n    // Audit Log\n    'audit' => 'לוג בדיקה',\n    'audit_desc' => 'לוג בדיקה זה מציג רשימה של פעילויות שנוטרו במערכת. רשימה זו לא מסוננת, בשונה מרשימות פעילות דומות במערכת בהן מוחלים מסנני הרשאות.',\n    'audit_event_filter' => 'מסנן אירועים',\n    'audit_event_filter_no_filter' => 'ללא סינון',\n    'audit_deleted_item' => 'פריט שנמחק',\n    'audit_deleted_item_name' => 'שם: :name',\n    'audit_table_user' => 'משתמש',\n    'audit_table_event' => 'אירוע',\n    'audit_table_related' => 'פריט או פרט קשור',\n    'audit_table_ip' => 'IP Address',\n    'audit_table_date' => 'זמן הפעילות',\n    'audit_date_from' => 'טווח תאריכים החל מ...',\n    'audit_date_to' => 'טווח תאריכים עד ל...',\n\n    // Role Settings\n    'roles' => 'תפקידים',\n    'role_user_roles' => 'תפקידי משתמשים',\n    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',\n    'roles_x_users_assigned' => ':count user assigned|:count users assigned',\n    'roles_x_permissions_provided' => ':count permission|:count permissions',\n    'roles_assigned_users' => 'Assigned Users',\n    'roles_permissions_provided' => 'הרשאות שהוקצו',\n    'role_create' => 'צור תפקיד משתמש חדש',\n    'role_delete' => 'מחק תפקיד',\n    'role_delete_confirm' => 'פעולה זו תמחק את התפקיד: :roleName',\n    'role_delete_users_assigned' => 'לתפקיד :userCount יש משתמשים אשר משויכים אליו. אם ברצונך להעבירם לתפקיד אחר אנא בחר תפקיד מלמטה',\n    'role_delete_no_migration' => \"אל תעביר משתמשים לתפקיד\",\n    'role_delete_sure' => 'האם אתה בטוח שברצונך למחוק את התפקיד?',\n    'role_edit' => 'ערוך תפקיד',\n    'role_details' => 'פרטי תפקיד',\n    'role_name' => 'שם התפקיד',\n    'role_desc' => 'תיאור קצר של התפקיד',\n    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',\n    'role_external_auth_id' => 'ID-י אותנטיקציה חיצוניים',\n    'role_system' => 'הרשאות מערכת',\n    'role_manage_users' => 'ניהול משתמשים',\n    'role_manage_roles' => 'ניהול תפקידים והרשאות תפקידים',\n    'role_manage_entity_permissions' => 'נהל הרשאות ספרים, פרקים ודפים',\n    'role_manage_own_entity_permissions' => 'נהל הרשאות על ספרים, פרקים ודפים בבעלותך',\n    'role_manage_page_templates' => 'נהל תבניות דפים',\n    'role_access_api' => 'גש ל-API המערכת',\n    'role_manage_settings' => 'ניהול הגדרות יישום',\n    'role_export_content' => 'Export content',\n    'role_import_content' => 'Import content',\n    'role_editor_change' => 'שנה עורך עמודים',\n    'role_notifications' => 'ניהול התראות',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'הרשאות משאבים',\n    'roles_system_warning' => 'שימו לב לכך שגישה לכל אחת משלושת ההרשאות הנ\"ל יכולה לאפשר למשתמש לשנות את הפריווילגיות שלהם או של אחרים במערכת. הגדירו תפקידים להרשאות אלה למשתמשים בהם אתם בוטחים בלבד.',\n    'role_asset_desc' => 'הרשאות אלו שולטות בגישת ברירת המחדל למשאבים בתוך המערכת. הרשאות של ספרים, פרקים ודפים יגברו על הרשאות אלו.',\n    'role_asset_admins' => 'מנהלים מקבלים הרשאה מלאה לכל התוכן אך אפשרויות אלו עלולות להציג או להסתיר אפשרויות בממשק',\n    'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'הכל',\n    'role_own' => 'שלי',\n    'role_controlled_by_asset' => 'נשלטים על ידי המשאב אליו הועלו',\n    'role_save' => 'שמור תפקיד',\n    'role_users' => 'משתמשים משוייכים לתפקיד זה',\n    'role_users_none' => 'אין משתמשים המשוייכים לתפקיד זה',\n\n    // Users\n    'users' => 'משתמשים',\n    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',\n    'user_profile' => 'פרופיל משתמש',\n    'users_add_new' => 'הוסף משתמש חדש',\n    'users_search' => 'חפש משתמשים',\n    'users_latest_activity' => 'פעילות אחרונה',\n    'users_details' => 'פרטי משתמש',\n    'users_details_desc' => 'הגדר שם לתצוגה ומייל עבור משתמש זה. כתובת המייל תשמש על מנת להתחבר למערכת',\n    'users_details_desc_no_email' => 'הגדר שם לתצוגה כדי שאחרים יוכלו לזהות',\n    'users_role' => 'תפקידי משתמשים',\n    'users_role_desc' => 'בחר אילו תפקידים ישויכו למשתמש זה. אם המשתמש משוייך למספר תפקידים, ההרשאות יהיו כלל ההרשאות של כל התפקידים',\n    'users_password' => 'סיסמא',\n    'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',\n    'users_send_invite_text' => 'תוכלו לבחור לשלוח למשתמש זה דוא\"ל הזמנה, המאפשר להם להגדיר סיסמה משלהם. אחרת, תוכלו להגדיר את סיסמתם בעצמכם.',\n    'users_send_invite_option' => 'שלח דוא\"ל הזמנה למשתמש',\n    'users_external_auth_id' => 'זיהוי חיצוני - ID',\n    'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.',\n    'users_password_warning' => 'Only fill the below if you would like to change the password for this user.',\n    'users_system_public' => 'משתמש זה מייצג את כל האורחים שלא מזוהים אשר משתמשים במערכת. לא ניתן להתחבר למשתמש זה אך הוא מוגדר כברירת מחדל',\n    'users_delete' => 'מחק משתמש',\n    'users_delete_named' => 'מחק משתמש :userName',\n    'users_delete_warning' => 'פעולה זו תמחק את המשתמש \\':userName\\' מהמערכת',\n    'users_delete_confirm' => 'האם ברצונך למחוק משתמש זה?',\n    'users_migrate_ownership' => 'העבר בעלות',\n    'users_migrate_ownership_desc' => 'בחרו משתמש כאן במידה ואתם מעוניינים שמשתמש אחר יהפוך לבעלים של כל הפריטים שכרגע בבעלות משתמש זה.',\n    'users_none_selected' => 'לא נבחר משתמש',\n    'users_edit' => 'עריכת משתמש',\n    'users_edit_profile' => 'עריכת פרופיל',\n    'users_avatar' => 'תמונת משתמש',\n    'users_avatar_desc' => 'בחר תמונה אשר תייצג את המשתמש. על התמונה להיות ריבוע של 256px',\n    'users_preferred_language' => 'שפה מועדפת',\n    'users_preferred_language_desc' => 'אפשרות זו תשנע את השפה אשר מוצגת בממשק המערכת. פעולה זו לא תשנה את התוכן אשר נכתב על ידי המשתמשים.',\n    'users_social_accounts' => 'רשתות חברתיות',\n    'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.',\n    'users_social_accounts_info' => 'כן ניתן לשייך חשבונות אחרים שלך לחיבור וזיהוי קל ומהיר. ניתוק חשבון אינו מנתק גישה קיימת למערכת. לביצוע ניתוק יש לשנות את ההגדרה בהגדרות של חשבון הרשת החברתית',\n    'users_social_connect' => 'חיבור החשבון',\n    'users_social_disconnect' => 'ניתוק חשבון',\n    'users_social_status_connected' => 'Connected',\n    'users_social_status_disconnected' => 'Disconnected',\n    'users_social_connected' => 'חשבון :socialAccount חובר בהצלחה לחשבון שלך',\n    'users_social_disconnected' => ':socialAccount נותק בהצלחה מהחשבון שלך',\n    'users_api_tokens' => 'אסימוני API',\n    'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.',\n    'users_api_tokens_none' => 'לא נוצרו אסימוני API למשתמש זה',\n    'users_api_tokens_create' => 'צור אסימון',\n    'users_api_tokens_expires' => 'פג',\n    'users_api_tokens_docs' => 'תיעוד API',\n    'users_mfa' => 'Multi-Factor Authentication',\n    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'users_mfa_x_methods' => ':count method configured|:count methods configured',\n    'users_mfa_configure' => 'Configure Methods',\n\n    // API Tokens\n    'user_api_token_create' => 'צור אסימון API',\n    'user_api_token_name' => 'שם',\n    'user_api_token_name_desc' => 'תנו לאסימון שלכם שם קריא, כתזכורת עתידית למטרה המיועדת שלו.',\n    'user_api_token_expiry' => 'תאריך תפוגה',\n    'user_api_token_expiry_desc' => 'הגדירו תאריך בו יפוג תוקף אסימון זה. לאחר תאריך זה, בקשות שיעשו באמצעות אסימון זה לא יעבדו יותר. במידה ושדה זה יושאר ריק, תאריך התפוגה יוגדר לבעוד 100 שנים.',\n    'user_api_token_create_secret_message' => 'מיד לאחר יצירת אסימון זה, יווצרו ויוצגו \"ID אסימון\" ו\"סוד אסימון\". הסוד יוצג פעם אחת בלבד, לכן וודאו להעתיק את הערך למקום שמור ובטוח לפני שתמשיכו הלאה.',\n    'user_api_token' => 'אסימון API',\n    'user_api_token_id' => 'ID האסימון',\n    'user_api_token_id_desc' => 'זהו מזהה בלתי ניתן לעריכה לאסימון זה הנוצר על ידי המערכת, אשר יסופק בבקשות API.',\n    'user_api_token_secret' => 'סוד האסימון',\n    'user_api_token_secret_desc' => 'זהו סוד המיוצר על ידי המערכת לאסימון זה, אשר יסופק בבקשות API. סוד זה יוצג פעם אחת בלבד, לכן וודאו להעתיק ערך זה למקום שמור ובטוח.',\n    'user_api_token_created' => 'אסימון נוצר :timeAgo',\n    'user_api_token_updated' => 'אסימון עודכן :timeAgo',\n    'user_api_token_delete' => 'מחק אסימון',\n    'user_api_token_delete_warning' => 'פעולה זו תמחק לחלוטין את אסימון ה-API בשם \\':tokenName\\' מהמערכת.',\n    'user_api_token_delete_confirm' => 'האם אתם בטוחים שאתם מעוניינים למחוק אסימון API זה?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',\n    'webhooks_x_trigger_events' => ':count trigger event|:count trigger events',\n    'webhooks_create' => 'Create New Webhook',\n    'webhooks_none_created' => 'No webhooks have yet been created.',\n    'webhooks_edit' => 'Edit Webhook',\n    'webhooks_save' => 'Save Webhook',\n    'webhooks_details' => 'Webhook Details',\n    'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',\n    'webhooks_events' => 'Webhook Events',\n    'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',\n    'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\\'t expose confidential content.',\n    'webhooks_events_all' => 'All system events',\n    'webhooks_name' => 'Webhook Name',\n    'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',\n    'webhooks_endpoint' => 'Webhook Endpoint',\n    'webhooks_active' => 'Webhook Active',\n    'webhook_events_table_header' => 'Events',\n    'webhooks_delete' => 'Delete Webhook',\n    'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \\':webhookName\\', from the system.',\n    'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?',\n    'webhooks_format_example' => 'Webhook Format Example',\n    'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The \"related_item\" and \"url\" properties are optional and will depend on the type of event triggered.',\n    'webhooks_status' => 'Webhook Status',\n    'webhooks_last_called' => 'Last Called:',\n    'webhooks_last_errored' => 'Last Errored:',\n    'webhooks_last_error_message' => 'Last Error Message:',\n\n    // Licensing\n    'licenses' => 'Licenses',\n    'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.',\n    'licenses_bookstack' => 'BookStack License',\n    'licenses_php' => 'PHP Library Licenses',\n    'licenses_js' => 'JavaScript Library Licenses',\n    'licenses_other' => 'Other Licenses',\n    'license_details' => 'License Details',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/he/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => 'שדה :attribute חייב להיות מסומן.',\n    'active_url'           => 'שדה :attribute הוא לא כתובת אתר תקנית.',\n    'after'                => 'שדה :attribute חייב להיות תאריך אחרי :date.',\n    'alpha'                => 'שדה :attribute יכול להכיל אותיות בלבד.',\n    'alpha_dash'           => 'שדה :attribute יכול להכיל אותיות, מספרים ומקפים בלבד.',\n    'alpha_num'            => 'שדה :attribute יכול להכיל אותיות ומספרים בלבד.',\n    'array'                => 'שדה :attribute חייב להיות מערך.',\n    'backup_codes'         => 'קוד שהוזן לא תקין או שכבר השתמשו בו.',\n    'before'               => 'שדה :attribute חייב להיות תאריך לפני :date.',\n    'between'              => [\n        'numeric' => 'שדה :attribute חייב להיות בין :min ל-:max.',\n        'file'    => 'שדה :attribute חייב להיות בין :min ל-:max קילובייטים.',\n        'string'  => 'שדה :attribute חייב להיות בין :min ל-:max תווים.',\n        'array'   => 'שדה :attribute חייב להיות בין :min ל-:max פריטים.',\n    ],\n    'boolean'              => 'שדה :attribute חייב להיות אמת או שקר.',\n    'confirmed'            => 'שדה האישור של :attribute לא תואם.',\n    'date'                 => 'שדה :attribute אינו תאריך תקני.',\n    'date_format'          => 'שדה :attribute לא תואם את הפורמט :format.',\n    'different'            => 'שדה :attribute ושדה :other חייבים להיות שונים.',\n    'digits'               => 'שדה :attribute חייב להיות בעל :digits ספרות.',\n    'digits_between'       => 'שדה :attribute חייב להיות בין :min ו-:max ספרות.',\n    'email'                => 'שדה :attribute חייב להיות כתובת אימייל תקנית.',\n    'ends_with' => 'The :attribute must end with one of the following: :values',\n    'file'                 => 'The :attribute must be provided as a valid file.',\n    'filled'               => 'שדה :attribute הוא חובה.',\n    'gt'                   => [\n        'numeric' => 'The :attribute must be greater than :value.',\n        'file'    => 'The :attribute must be greater than :value kilobytes.',\n        'string'  => 'The :attribute must be greater than :value characters.',\n        'array'   => 'The :attribute must have more than :value items.',\n    ],\n    'gte'                  => [\n        'numeric' => 'The :attribute must be greater than or equal :value.',\n        'file'    => 'The :attribute must be greater than or equal :value kilobytes.',\n        'string'  => 'The :attribute must be greater than or equal :value characters.',\n        'array'   => 'The :attribute must have :value items or more.',\n    ],\n    'exists'               => 'בחירת ה-:attribute אינה תקפה.',\n    'image'                => 'שדה :attribute חייב להיות תמונה.',\n    'image_extension'      => 'שדה :attribute חייב להיות מסוג תמונה נתמך',\n    'in'                   => 'בחירת ה-:attribute אינה תקפה.',\n    'integer'              => 'שדה :attribute חייב להיות מספר שלם.',\n    'ip'                   => 'שדה :attribute חייב להיות כתובת IP תקנית.',\n    'ipv4'                 => 'The :attribute must be a valid IPv4 address.',\n    'ipv6'                 => 'The :attribute must be a valid IPv6 address.',\n    'json'                 => 'The :attribute must be a valid JSON string.',\n    'lt'                   => [\n        'numeric' => 'The :attribute must be less than :value.',\n        'file'    => 'The :attribute must be less than :value kilobytes.',\n        'string'  => 'The :attribute must be less than :value characters.',\n        'array'   => 'The :attribute must have less than :value items.',\n    ],\n    'lte'                  => [\n        'numeric' => 'The :attribute must be less than or equal :value.',\n        'file'    => 'The :attribute must be less than or equal :value kilobytes.',\n        'string'  => 'The :attribute must be less than or equal :value characters.',\n        'array'   => 'The :attribute must not have more than :value items.',\n    ],\n    'max'                  => [\n        'numeric' => 'שדה :attribute אינו יכול להיות גדול מ-:max.',\n        'file'    => 'שדה :attribute לא יכול להיות גדול מ-:max קילובייטים.',\n        'string'  => 'שדה :attribute לא יכול להיות גדול מ-:max characters.',\n        'array'   => 'שדה :attribute לא יכול להכיל יותר מ-:max פריטים.',\n    ],\n    'mimes'                => 'שדה :attribute צריך להיות קובץ מסוג: :values.',\n    'min'                  => [\n        'numeric' => 'שדה :attribute חייב להיות לפחות :min.',\n        'file'    => 'שדה :attribute חייב להיות לפחות :min קילובייטים.',\n        'string'  => 'שדה :attribute חייב להיות לפחות :min תווים.',\n        'array'   => 'שדה :attribute חייב להיות לפחות :min פריטים.',\n    ],\n    'not_in'               => 'בחירת ה-:attribute אינה תקפה.',\n    'not_regex'            => 'The :attribute format is invalid.',\n    'numeric'              => 'שדה :attribute חייב להיות מספר.',\n    'regex'                => 'שדה :attribute בעל פורמט שאינו תקין.',\n    'required'             => 'שדה :attribute הוא חובה.',\n    'required_if'          => 'שדה :attribute נחוץ כאשר :other הוא :value.',\n    'required_with'        => 'שדה :attribute נחוץ כאשר :values נמצא.',\n    'required_with_all'    => 'שדה :attribute נחוץ כאשר :values נמצא.',\n    'required_without'     => 'שדה :attribute נחוץ כאשר :values לא בנמצא.',\n    'required_without_all' => 'שדה :attribute נחוץ כאשר אף אחד מ-:values נמצאים.',\n    'same'                 => 'שדה :attribute ו-:other חייבים להיות זהים.',\n    'safe_url'             => 'The provided link may not be safe.',\n    'size'                 => [\n        'numeric' => 'שדה :attribute חייב להיות :size.',\n        'file'    => 'שדה :attribute חייב להיות :size קילובייטים.',\n        'string'  => 'שדה :attribute חייב להיות :size תווים.',\n        'array'   => 'שדה :attribute חייב להכיל :size פריטים.',\n    ],\n    'string'               => 'שדה :attribute חייב להיות מחרוזת.',\n    'timezone'             => 'שדה :attribute חייב להיות איזור תקני.',\n    'totp'                 => 'The provided code is not valid or has expired.',\n    'unique'               => 'שדה :attribute כבר תפוס.',\n    'url'                  => 'שדה :attribute בעל פורמט שאינו תקין.',\n    'uploaded'             => 'שדה :attribute ארעה שגיאה בעת ההעלאה.',\n\n    'zip_file' => 'The :attribute needs to reference a file within the ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',\n    'zip_model_expected' => 'Data object expected but \":type\" found.',\n    'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'נדרש אימות סיסמא',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/hr/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'kreirana stranica',\n    'page_create_notification'    => 'Stranica uspješno kreirana',\n    'page_update'                 => 'ažurirana stranica',\n    'page_update_notification'    => 'Stranica uspješno ažurirana',\n    'page_delete'                 => 'izbrisana stranica',\n    'page_delete_notification'    => 'Stranica je uspješno izbrisana',\n    'page_restore'                => 'obnovljena stranica',\n    'page_restore_notification'   => 'Stranica je uspješno obnovljena',\n    'page_move'                   => 'premještena stranica',\n    'page_move_notification'      => 'Stranica je uspješno premještene',\n\n    // Chapters\n    'chapter_create'              => 'stvoreno poglavlje',\n    'chapter_create_notification' => 'Poglavlje je uspješno stvoreno',\n    'chapter_update'              => 'ažurirano poglavlje',\n    'chapter_update_notification' => 'Poglavlje je uspješno ažurirano',\n    'chapter_delete'              => 'izbrisano poglavlje',\n    'chapter_delete_notification' => 'Poglavlje je uspješno izbrisano',\n    'chapter_move'                => 'premiješteno poglavlje',\n    'chapter_move_notification' => 'Poglavlje je uspješno premješteno',\n\n    // Books\n    'book_create'                 => 'stvorena knjiga',\n    'book_create_notification'    => 'Knjiga je uspješno stvorena',\n    'book_create_from_chapter'              => 'pretvori poglavlje u knjigu',\n    'book_create_from_chapter_notification' => 'Poglavlje je uspješno pretvoreno u knjigu',\n    'book_update'                 => 'ažurirana knjiga',\n    'book_update_notification'    => 'Knjiga je uspješno ažurirana',\n    'book_delete'                 => 'izbrisana knjiga',\n    'book_delete_notification'    => 'Knjiga je uspješno izbrisana',\n    'book_sort'                   => 'razvrstana knjiga',\n    'book_sort_notification'      => 'Knjiga je uspješno razvrstana',\n\n    // Bookshelves\n    'bookshelf_create'            => 'kreirana polica',\n    'bookshelf_create_notification'    => 'Polica uspješno kreirana',\n    'bookshelf_create_from_book'    => 'pretvorena knjiga u policu',\n    'bookshelf_create_from_book_notification'    => 'Poglavlje je uspješno pretvoreno u knjigu',\n    'bookshelf_update'                 => 'ažurirana polica',\n    'bookshelf_update_notification'    => 'Polica je uspješno ažurirana',\n    'bookshelf_delete'                 => 'izbrisana polica',\n    'bookshelf_delete_notification'    => 'Polica je uspješno izbrisana',\n\n    // Revisions\n    'revision_restore' => 'obnovljena revizija',\n    'revision_delete' => 'izbrisana revizija',\n    'revision_delete_notification' => 'Revizija uspješno obrisana',\n\n    // Favourites\n    'favourite_add_notification' => '\".ime\" će biti dodano u tvoje favorite',\n    'favourite_remove_notification' => '\".ime\" je uspješno maknuta iz tvojih favorita',\n\n    // Watching\n    'watch_update_level_notification' => 'Postavke gledanja uspješno ažurirane',\n\n    // Auth\n    'auth_login' => 'prijavljen',\n    'auth_register' => 'registriran kao novi korisnik',\n    'auth_password_reset_request' => 'zahtjev za resetiranje korisničke lozinke',\n    'auth_password_reset_update' => 'resetiraj korisničku lozinku',\n    'mfa_setup_method' => 'konfiguriran MFA način',\n    'mfa_setup_method_notification' => 'Metoda više-čimbenika je uspješno konfigurirana',\n    'mfa_remove_method' => 'uklonjen MFA način',\n    'mfa_remove_method_notification' => 'Metoda više-čimbenika je uspješno izbrisana',\n\n    // Settings\n    'settings_update' => 'ažurirane postavke',\n    'settings_update_notification' => 'Postavke uspješno ažurirane',\n    'maintenance_action_run' => 'izvršena akcija održavanja',\n\n    // Webhooks\n    'webhook_create' => 'web-dojavnik je kreiran',\n    'webhook_create_notification' => 'Web-dojavnik je uspješno kreiran',\n    'webhook_update' => 'web-dojavnik je ažuriran',\n    'webhook_update_notification' => 'Web- dojavnik je uspješno ažuriran',\n    'webhook_delete' => 'web- dojavnik izbrisan',\n    'webhook_delete_notification' => 'Web-dojavnik je uspješno izbrisan',\n\n    // Imports\n    'import_create' => 'created import',\n    'import_create_notification' => 'Import successfully uploaded',\n    'import_run' => 'updated import',\n    'import_run_notification' => 'Content successfully imported',\n    'import_delete' => 'deleted import',\n    'import_delete_notification' => 'Import successfully deleted',\n\n    // Users\n    'user_create' => 'kreirani korisnik',\n    'user_create_notification' => 'Korisnik je uspješno kreiran',\n    'user_update' => 'ažurirani korisnik',\n    'user_update_notification' => 'Korisnik je uspješno ažuriran',\n    'user_delete' => 'izbrisani korisnik',\n    'user_delete_notification' => 'Korisnik je uspješno uklonjen',\n\n    // API Tokens\n    'api_token_create' => 'created API token',\n    'api_token_create_notification' => 'API token uspješno kreiran',\n    'api_token_update' => 'updated API token',\n    'api_token_update_notification' => 'API token uspješno ažuriran',\n    'api_token_delete' => 'deleted API token',\n    'api_token_delete_notification' => 'API token uspješno izbrisan',\n\n    // Roles\n    'role_create' => 'kreirana uloga',\n    'role_create_notification' => 'Uloga uspješno stvorena',\n    'role_update' => 'ažurirana uloga',\n    'role_update_notification' => 'Uloga uspješno ažurirana',\n    'role_delete' => 'izbrisana uloga',\n    'role_delete_notification' => 'Uloga je uspješno izbrisana',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'koš za smeće ispražnjen',\n    'recycle_bin_restore' => 'reciklirano iz koša za smeće',\n    'recycle_bin_destroy' => 'uklonjeno iz koša za smeće',\n\n    // Comments\n    'commented_on'                => 'komentirano',\n    'comment_create'              => 'dodani komentar',\n    'comment_update'              => 'ažurirani komentar',\n    'comment_delete'              => 'obrisani komentar',\n\n    // Sort Rules\n    'sort_rule_create' => 'created sort rule',\n    'sort_rule_create_notification' => 'Sort rule successfully created',\n    'sort_rule_update' => 'updated sort rule',\n    'sort_rule_update_notification' => 'Sort rule successfully updated',\n    'sort_rule_delete' => 'deleted sort rule',\n    'sort_rule_delete_notification' => 'Sort rule successfully deleted',\n\n    // Other\n    'permissions_update'          => 'ažurirana dopuštenja',\n];\n"
  },
  {
    "path": "lang/hr/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Ove vjerodajnice ne podudaraju se s našim zapisima.',\n    'throttle' => 'Previše pokušaja prijave. Molimo vas da pokušate za :seconds sekundi.',\n\n    // Login & Register\n    'sign_up' => 'Registrirajte se',\n    'log_in' => 'Prijavite se',\n    'log_in_with' => 'Prijavite se sa :socialDriver',\n    'sign_up_with' => 'Registrirajte se sa :socialDriver',\n    'logout' => 'Odjavite se',\n\n    'name' => 'Ime',\n    'username' => 'Korisničko ime',\n    'email' => 'E-pošta',\n    'password' => 'Lozinka',\n    'password_confirm' => 'Potvrdite lozinku',\n    'password_hint' => 'Mora biti najmanje 8 znakova',\n    'forgot_password' => 'Zaboravili ste lozinku?',\n    'remember_me' => 'Zapamti me',\n    'ldap_email_hint' => 'Molimo upišite mail korišten za ovaj račun.',\n    'create_account' => 'Stvori račun',\n    'already_have_account' => 'Imate li već račun?',\n    'dont_have_account' => 'Nemate račun?',\n    'social_login' => 'Mrežna Prijava',\n    'social_registration' => 'Mrežna Registracija',\n    'social_registration_text' => 'Prijavite se putem drugih servisa.',\n\n    'register_thanks' => 'Zahvaljujemo na registraciji!',\n    'register_confirm' => 'Molimo, provjerite svoj email i kliknite gumb za potvrdu pristupa :appName.',\n    'registrations_disabled' => 'Registracije su trenutno onemogućene',\n    'registration_email_domain_invalid' => 'Ova e-mail adresa se ne može koristiti u ovoj aplikaciji',\n    'register_success' => 'Hvala na prijavi! Sada ste registrirani i prijavljeni.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Pokušaj Prijave',\n    'auto_init_starting_desc' => 'Kontaktiramo vaš sustav za autentifikaciju kako bismo započeli postupak prijave. Ako ne postoji napredak nakon 5 sekundi, možete pokušati kliknuti donju poveznicu.',\n    'auto_init_start_link' => 'Nastavite s autentifikacijom',\n\n    // Password Reset\n    'reset_password' => 'Promijenite lozinku',\n    'reset_password_send_instructions' => 'Upišite svoju e-mail adresu kako biste primili poveznicu za promjenu lozinke.',\n    'reset_password_send_button' => 'Pošalji poveznicu za promjenu lozinke',\n    'reset_password_sent' => 'Poveznica za promjenu lozinke poslat će se na :email adresu ako je u našem sustavu.',\n    'reset_password_success' => 'Vaša lozinka je uspješno promijenjena.',\n    'email_reset_subject' => 'Promijenite svoju :appName lozinku',\n    'email_reset_text' => 'Primili ste ovu poruku jer je zatražena promjena lozinke za vaš račun.',\n    'email_reset_not_requested' => 'Ako niste tražili promjenu lozinke slobodno zanemarite ovu poruku.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Potvrdite svoju e-mail adresu na :appName',\n    'email_confirm_greeting' => 'Hvala na prijavi :appName!',\n    'email_confirm_text' => 'Molimo potvrdite svoju e-mail adresu klikom na donji gumb.',\n    'email_confirm_action' => 'Potvrdi Email',\n    'email_confirm_send_error' => 'Potvrda e-mail adrese je obavezna, ali sustav ne može poslati e-mail. Javite se administratoru kako bi provjerio vaš e-mail.',\n    'email_confirm_success' => 'Vaša e-pošta je potvrđena! Sada biste se trebali moći prijaviti koristeći danu e-poštu.',\n    'email_confirm_resent' => 'Ponovno je poslana potvrda. Molimo, provjerite svoj inbox.',\n    'email_confirm_thanks' => 'Zahvaljujemo na potvrdi!',\n    'email_confirm_thanks_desc' => 'Molimo pričekajte trenutak dok se obrađuje vaša potvrda. Ako ne budete preusmjereni nakon 3 sekunde, pritisnite \"Nastavi\" poveznicu ispod kako biste nastavili.',\n\n    'email_not_confirmed' => 'E-mail adresa nije potvrđena.',\n    'email_not_confirmed_text' => 'Vaša e-mail adresa još nije potvrđena.',\n    'email_not_confirmed_click_link' => 'Molimo, kliknite na poveznicu koju ste primili kratko nakon registracije.',\n    'email_not_confirmed_resend' => 'Ako ne možete pronaći e-mail za postavljanje lozinke možete ga zatražiti ponovno ispunjavanjem ovog obrasca.',\n    'email_not_confirmed_resend_button' => 'Ponovno pošalji e-mail potvrde',\n\n    // User Invite\n    'user_invite_email_subject' => 'Pozvani ste pridružiti se :appName!',\n    'user_invite_email_greeting' => 'Vaš račun je kreiran za vas na :appName',\n    'user_invite_email_text' => 'Kliknite ispod da biste postavili račun i dobili pristup.',\n    'user_invite_email_action' => 'Postavite lozinku',\n    'user_invite_page_welcome' => 'Dobrodošli u :appName!',\n    'user_invite_page_text' => 'Da biste postavili račun i dobili pristup trebate unijeti lozinku kojom ćete se ubuduće prijaviti na :appName.',\n    'user_invite_page_confirm_button' => 'Potvrdite lozinku',\n    'user_invite_success_login' => 'Lozinka je postavljena, sada biste se trebali moći prijaviti koristeći postavljenu lozinku kako biste pristupili aplikaciji :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Postavite Višestruku Autentifikaciju',\n    'mfa_setup_desc' => 'Postavite višestruku provjeru autentičnosti kao dodatni sloj sigurnosti za svoj korisnički račun.',\n    'mfa_setup_configured' => 'Već postavljeno',\n    'mfa_setup_reconfigure' => 'Ponovno postavite',\n    'mfa_setup_remove_confirmation' => 'Jeste li sigurni da želite ukloniti ovu metodu višestruke provjere autentičnosti?',\n    'mfa_setup_action' => 'Postavke',\n    'mfa_backup_codes_usage_limit_warning' => 'Imate manje od 5 preostalih rezervnih kodova. Molimo generirajte i pohranite novi set prije nego što vam kodovi ponestanu kako biste izbjegli blokadu pristupa vašem računu.',\n    'mfa_option_totp_title' => 'Mobilna Aplikacija',\n    'mfa_option_totp_desc' => 'Da biste koristili višestruku provjeru autentičnosti, trebat će vam mobilna aplikacija koja podržava TOTP (Time-Based One-Time Password) kao što su Google Authenticator, Authy ili Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Rezervni Kodovi',\n    'mfa_option_backup_codes_desc' => 'Generates a set of one-time-use backup codes which you\\'ll enter on login to verify your identity. Make sure to store these in a safe & secure place.',\n    'mfa_gen_confirm_and_enable' => 'Potvrdi i Omogući',\n    'mfa_gen_backup_codes_title' => 'Postavke Rezervnih Kodova',\n    'mfa_gen_backup_codes_desc' => 'Spremite sljedeći popis kodova na sigurno mjesto. Prilikom pristupa sustavu, moći ćete koristiti jedan od ovih kodova kao drugi mehanizam autentifikacije.',\n    'mfa_gen_backup_codes_download' => 'Preuzmi Kodove',\n    'mfa_gen_backup_codes_usage_warning' => 'Pojedinačni kod se može koristiti samo jednom',\n    'mfa_gen_totp_title' => 'Postavka Mobilne Aplikacije',\n    'mfa_gen_totp_desc' => 'Da biste koristili višestruku provjeru autentičnosti, trebat će vam mobilna aplikacija koja podržava TOTP (Time-Based One-Time Password) kao što su Google Authenticator, Authy ili Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Skenirajte QR kod u nastavku koristeći svoju preferiranu aplikaciju za autentifikaciju kako biste započeli.',\n    'mfa_gen_totp_verify_setup' => 'Potvrdite Postavke',\n    'mfa_gen_totp_verify_setup_desc' => 'Potvrdite da sve radi unosom koda koji je generiran unutar vaše aplikacije za autentifikaciju u donje polje za unos:',\n    'mfa_gen_totp_provide_code_here' => 'Dostavite generirani kod ovdje',\n    'mfa_verify_access' => 'Potvrdite Pristup',\n    'mfa_verify_access_desc' => 'Vaš korisnički račun zahtijeva potvrdu vašeg identiteta putem dodatne razine provjere prije nego vam se omogući pristup. Molimo potvrdite korištenjem jedne od konfiguriranih metoda kako biste nastavili.',\n    'mfa_verify_no_methods' => 'Nema Postavljenih Metoda',\n    'mfa_verify_no_methods_desc' => 'Nisu pronađene metode višestruke provjere autentičnosti za vaš korisnički račun. Morat ćete postaviti barem jednu metodu prije nego dobijete pristup.',\n    'mfa_verify_use_totp' => 'Potvrda korištenjem mobilne aplikacije',\n    'mfa_verify_use_backup_codes' => 'Potvrda korištenjem rezervnog koda',\n    'mfa_verify_backup_code' => 'Rezervni Kod',\n    'mfa_verify_backup_code_desc' => 'Unesite jedan od preostalih rezervnih kodova u nastavku:',\n    'mfa_verify_backup_code_enter_here' => 'Ovdje unesite rezervni kod',\n    'mfa_verify_totp_desc' => 'Unesite kod generiran pomoću vaše mobilne aplikacije u nastavku:',\n    'mfa_setup_login_notification' => 'Višestruka metoda autentifikacije konfigurirana. Molimo prijavite se ponovno koristeći konfiguriranu metodu.',\n];\n"
  },
  {
    "path": "lang/hr/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Odustani',\n    'close' => 'Zatvori',\n    'confirm' => 'Potvrdi',\n    'back' => 'Natrag',\n    'save' => 'Spremi',\n    'continue' => 'Nastavi',\n    'select' => 'Odaberi',\n    'toggle_all' => 'Prebaci sve',\n    'more' => 'Više',\n\n    // Form Labels\n    'name' => 'Ime',\n    'description' => 'Opis',\n    'role' => 'Uloga',\n    'cover_image' => 'Naslovna slika',\n    'cover_image_description' => 'This image should be approximately 440x250px although it will be flexibly scaled & cropped to fit the user interface in different scenarios as required, so actual dimensions for display will differ.',\n\n    // Actions\n    'actions' => 'Aktivnost',\n    'view' => 'Pogled',\n    'view_all' => 'Pogledaj sve',\n    'new' => 'Novi',\n    'create' => 'Stvori',\n    'update' => 'Ažuriraj',\n    'edit' => 'Uredi',\n    'archive' => 'Archive',\n    'unarchive' => 'Un-Archive',\n    'sort' => 'Razvrstaj',\n    'move' => 'Makni',\n    'copy' => 'Kopiraj',\n    'reply' => 'Ponovi',\n    'delete' => 'Izbriši',\n    'delete_confirm' => 'Potvrdite brisanje',\n    'search' => 'Traži',\n    'search_clear' => 'Očisti pretragu',\n    'reset' => 'Ponovno postavi',\n    'remove' => 'Ukloni',\n    'add' => 'Dodaj',\n    'configure' => 'Konfiguriraj',\n    'manage' => 'Upravljaj',\n    'fullscreen' => 'Cijeli zaslon',\n    'favourite' => 'Favorit',\n    'unfavourite' => 'Ukloni iz favorita',\n    'next' => 'Dalje',\n    'previous' => 'Prethodno',\n    'filter_active' => 'Aktivni Filter:',\n    'filter_clear' => 'Poništi Filter',\n    'download' => 'Preuzmi',\n    'open_in_tab' => 'Otvori u Kartici',\n    'open' => 'Open',\n\n    // Sort Options\n    'sort_options' => 'Razvrstaj opcije',\n    'sort_direction_toggle' => 'Razvrstaj smjer prebacivanja',\n    'sort_ascending' => 'Razvrstaj uzlazno',\n    'sort_descending' => 'Razvrstaj silazno',\n    'sort_name' => 'Ime',\n    'sort_default' => 'Zadano',\n    'sort_created_at' => 'Datum',\n    'sort_updated_at' => 'Datum Ažuriranja',\n\n    // Misc\n    'deleted_user' => 'Izbrisani korisnik',\n    'no_activity' => 'Nema aktivnosti za pregled',\n    'no_items' => 'Nedostupno',\n    'back_to_top' => 'Natrag na vrh',\n    'skip_to_main_content' => 'Preskoči na glavni sadržaj',\n    'toggle_details' => 'Prebaci detalje',\n    'toggle_thumbnails' => 'Uključi minijature',\n    'details' => 'Detalji',\n    'grid_view' => 'Prikaz rešetke',\n    'list_view' => 'Prikaz popisa',\n    'default' => 'Zadano',\n    'breadcrumb' => 'Putokaz',\n    'status' => 'Status',\n    'status_active' => 'Aktivno',\n    'status_inactive' => 'Neaktivno',\n    'never' => 'Nikada',\n    'none' => 'Ništa',\n\n    // Header\n    'homepage' => 'Naslovna Stranica',\n    'header_menu_expand' => 'Proširi izbornik',\n    'profile_menu' => 'Profil',\n    'view_profile' => 'Vidi profil',\n    'edit_profile' => 'Uredite profil',\n    'dark_mode' => 'Tamni način',\n    'light_mode' => 'Svijetli način',\n    'global_search' => 'Globalno Pretraživanje',\n\n    // Layout tabs\n    'tab_info' => 'Info',\n    'tab_info_label' => 'Tab: pokaži sekundarne informacije',\n    'tab_content' => 'Sadržaj',\n    'tab_content_label' => 'Tab: pokaži primarni sadržaj',\n\n    // Email Content\n    'email_action_help' => 'Ako imate poteškoća s klikom na gumb \":actionText\", kopirajte i zalijepite donji URL u vaš preglednik.',\n    'email_rights' => 'Sva prava pridržana',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Politika privatnosti',\n    'terms_of_service' => 'Uvjeti korištenja',\n\n    // OpenSearch\n    'opensearch_description' => 'Search :appName',\n];\n"
  },
  {
    "path": "lang/hr/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Odabir slike',\n    'image_list' => 'Popis Slika',\n    'image_details' => 'Detalji Slike',\n    'image_upload' => 'Učitaj Sliku',\n    'image_intro' => 'Ovdje možete odabrati i upravljati slikama koje su prethodno prenesene u sustav.',\n    'image_intro_upload' => 'Prenesite novu sliku povlačenjem slikovne datoteke u ovaj prozor ili koristite gumb \"Učitaj sliku\" iznad.',\n    'image_all' => 'Sve',\n    'image_all_title' => 'Vidi sve slike',\n    'image_book_title' => 'Vidi slike dodane ovoj knjizi',\n    'image_page_title' => 'Vidi slike dodane ovoj stranici',\n    'image_search_hint' => 'Pretraži pomoću imena slike',\n    'image_uploaded' => 'Učitano :uploadedDate',\n    'image_uploaded_by' => 'Učitao/la :userName',\n    'image_uploaded_to' => 'Učitano na :pageLink',\n    'image_updated' => 'Ažurirano: :updateDate',\n    'image_load_more' => 'Učitaj više',\n    'image_image_name' => 'Ime slike',\n    'image_delete_used' => 'Ova slika korištena je na donjoj stranici.',\n    'image_delete_confirm_text' => 'Jeste li sigurni da želite obrisati sliku?',\n    'image_select_image' => 'Odaberi sliku',\n    'image_dropzone' => 'Prebacite sliku ili kliknite ovdje za prijenos',\n    'image_dropzone_drop' => 'Ovdje spustite slike za učitavanje',\n    'images_deleted' => 'Obrisane slike',\n    'image_preview' => 'Pregled slike',\n    'image_upload_success' => 'Slika je uspješno dodana',\n    'image_update_success' => 'Detalji slike su uspješno ažurirani',\n    'image_delete_success' => 'Slika je obrisana',\n    'image_replace' => 'Zamijeni Sliku',\n    'image_replace_success' => 'Datiteka slike je uspješno ažurirana',\n    'image_rebuild_thumbs' => 'Regenerate Size Variations',\n    'image_rebuild_thumbs_success' => 'Image size variations successfully rebuilt!',\n\n    // Code Editor\n    'code_editor' => 'Uredi kod',\n    'code_language' => 'Jezik koda',\n    'code_content' => 'Sadržaj koda',\n    'code_session_history' => 'Povijest sesije',\n    'code_save' => 'Spremi kod',\n];\n"
  },
  {
    "path": "lang/hr/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Općenito',\n    'advanced' => 'Napredno',\n    'none' => 'Ništa',\n    'cancel' => 'Otkaži',\n    'save' => 'Spremi',\n    'close' => 'Zatvori',\n    'apply' => 'Apply',\n    'undo' => 'Poništi',\n    'redo' => 'Ponovi',\n    'left' => 'Lijevo',\n    'center' => 'Centar',\n    'right' => 'Desno',\n    'top' => 'Vrh',\n    'middle' => 'Sredina',\n    'bottom' => 'Dno',\n    'width' => 'Širina',\n    'height' => 'Visina',\n    'More' => 'Više',\n    'select' => 'Odaberi...',\n\n    // Toolbar\n    'formats' => 'Formati',\n    'header_large' => 'Veliki Naslov',\n    'header_medium' => 'Srednji Naslov',\n    'header_small' => 'Mali Naslov',\n    'header_tiny' => 'Malecki Naslov',\n    'paragraph' => 'Odlomak',\n    'blockquote' => 'Blok citat',\n    'inline_code' => 'Ugrađeni Kod',\n    'callouts' => 'Zabilješke',\n    'callout_information' => 'Informacija',\n    'callout_success' => 'Uspjeh',\n    'callout_warning' => 'Upozorenje',\n    'callout_danger' => 'Opasnost',\n    'bold' => 'Podebljano',\n    'italic' => 'Kurziv',\n    'underline' => 'Podcrtano',\n    'strikethrough' => 'Precrtano',\n    'superscript' => 'Gornji indeks',\n    'subscript' => 'Donji indeks',\n    'text_color' => 'Boja teksta',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Prilagođena boja',\n    'remove_color' => 'Ukloni boju',\n    'background_color' => 'Boja pozadine',\n    'align_left' => 'Poravnaj lijevo',\n    'align_center' => 'Poravnaj po sredini',\n    'align_right' => 'Poravnaj desno',\n    'align_justify' => 'Poravnaj obostrano',\n    'list_bullet' => 'Popis s točkama',\n    'list_numbered' => 'Numerirani popis',\n    'list_task' => 'Lista zadataka',\n    'indent_increase' => 'Povećaj uvlaku',\n    'indent_decrease' => 'Smanji uvlaku',\n    'table' => 'Tablica',\n    'insert_image' => 'Umetni sliku',\n    'insert_image_title' => 'Umetni/Editiraj sliku',\n    'insert_link' => 'Umetni/editiraj poveznicu',\n    'insert_link_title' => 'Umetni/Editiraj poveznicu',\n    'insert_horizontal_line' => 'Ubaci vodoravnu liniju',\n    'insert_code_block' => 'Ubacivanje kodnog bloka',\n    'edit_code_block' => 'Editiraj kodni blok',\n    'insert_drawing' => 'Umetni/editiraj crtež',\n    'drawing_manager' => 'Upravitelj crteža',\n    'insert_media' => 'Umetni/editiraj medij',\n    'insert_media_title' => 'Umetni/Editiraj medij',\n    'clear_formatting' => 'Očisti sva oblikovanja',\n    'source_code' => 'Izvorni kod',\n    'source_code_title' => 'Izvorni Kod',\n    'fullscreen' => 'Puni zaslon',\n    'image_options' => 'Mogućnosti slike',\n\n    // Tables\n    'table_properties' => 'Svojstva tablice',\n    'table_properties_title' => 'Svojstva Tablice',\n    'delete_table' => 'Obriši tablicu',\n    'table_clear_formatting' => 'Clear table formatting',\n    'resize_to_contents' => 'Resize to contents',\n    'row_header' => 'Row header',\n    'insert_row_before' => 'Umetni redak prije',\n    'insert_row_after' => 'Umetni redak poslije',\n    'delete_row' => 'Izbriši red',\n    'insert_column_before' => 'Umetni stupac prije',\n    'insert_column_after' => 'Umetni stupac poslije',\n    'delete_column' => 'Izbriši stupac',\n    'table_cell' => 'Ćelija',\n    'table_row' => 'Red',\n    'table_column' => 'Stupac',\n    'cell_properties' => 'Svojstava ćelija',\n    'cell_properties_title' => 'Svojstava Ćelija',\n    'cell_type' => 'Vrsta Ćelije',\n    'cell_type_cell' => 'Ćelija',\n    'cell_scope' => 'Obuhvat',\n    'cell_type_header' => 'Naslovna Ćelija',\n    'merge_cells' => 'Spoji ćelije',\n    'split_cell' => 'Razdvoji ćelije',\n    'table_row_group' => 'Grupa Redaka',\n    'table_column_group' => 'Grupa Stupaca',\n    'horizontal_align' => 'Vodoravno poravnanje',\n    'vertical_align' => 'Uspravno poravnanje',\n    'border_width' => 'Širina obruba',\n    'border_style' => 'Stil obruba',\n    'border_color' => 'Boja obruba',\n    'row_properties' => 'Svojstava redaka',\n    'row_properties_title' => 'Svojstava Redaka',\n    'cut_row' => 'Izreži redak',\n    'copy_row' => 'Kopiraj redak',\n    'paste_row_before' => 'Zalijepi redak prije',\n    'paste_row_after' => 'Zalijepi redak nakon',\n    'row_type' => 'Vrsta redka',\n    'row_type_header' => 'Zaglavlje',\n    'row_type_body' => 'Tijelo',\n    'row_type_footer' => 'Podnožje',\n    'alignment' => 'Poravnanje',\n    'cut_column' => 'Izreži stupac',\n    'copy_column' => 'Kopiraj stupac',\n    'paste_column_before' => 'Zalijepi stupac prije',\n    'paste_column_after' => 'Zalijepi stupac poslije',\n    'cell_padding' => 'Unutarnji razmak ćelije',\n    'cell_spacing' => 'Razmak između ćelija',\n    'caption' => 'Natpis',\n    'show_caption' => 'Prikaži natpis',\n    'constrain' => 'Ograniči proporcije',\n    'cell_border_solid' => 'Puno',\n    'cell_border_dotted' => 'Točkasto',\n    'cell_border_dashed' => 'Isprekidano',\n    'cell_border_double' => 'Dvostruko',\n    'cell_border_groove' => 'Ponavljajući uzorak',\n    'cell_border_ridge' => 'Rub',\n    'cell_border_inset' => 'Unutarnji rub',\n    'cell_border_outset' => 'Vanjski rub',\n    'cell_border_none' => 'Ništa',\n    'cell_border_hidden' => 'Skriveno',\n\n    // Images, links, details/summary & embed\n    'source' => 'Izvor',\n    'alt_desc' => 'Alternativni opis',\n    'embed' => 'Umetnuto',\n    'paste_embed' => 'Zalijepite svoj ugrađeni kod u nastavku:',\n    'url' => 'URL',\n    'text_to_display' => 'Tekst za prikaz',\n    'title' => 'Naslov',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Otvori poveznicu',\n    'open_link_in' => 'Otvori poveznicu u...',\n    'open_link_current' => 'Trenutni prozor',\n    'open_link_new' => 'Novi prozor',\n    'remove_link' => 'Ukloni poveznicu',\n    'insert_collapsible' => 'Umetni skupljajući blok',\n    'collapsible_unwrap' => 'Odmotaj',\n    'edit_label' => 'Izmjeni oznaku',\n    'toggle_open_closed' => 'Promijeni otvoreno/zatvoreno',\n    'collapsible_edit' => 'Izmjeni skupljajući blok',\n    'toggle_label' => 'Promijeni oznaku',\n\n    // About view\n    'about' => 'O Editoru',\n    'about_title' => 'O WYSIWYG Editoru',\n    'editor_license' => 'Licenca i autorsko pravo uređivača',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'Ovaj uređivač je izrađen pomoću: tinyLink koji je dostupan pod MIT licencom.',\n    'editor_tiny_license_link' => 'Detalji o autorskim pravima i licenci za TinyMCE mogu se pronaći ovdje.',\n    'save_continue' => 'Spremi Stranicu i Nastavi',\n    'callouts_cycle' => '(Nastavite pritiskati kako biste prelazili kroz vrste)',\n    'link_selector' => 'Poveznica na sadržaj',\n    'shortcuts' => 'Prečaci',\n    'shortcut' => 'Prečac',\n    'shortcuts_intro' => 'Sljedeći prečaci su dostupni u uređivaču:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Opis',\n];\n"
  },
  {
    "path": "lang/hr/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Nedavno stvoreno',\n    'recently_created_pages' => 'Nedavno stvorene stranice',\n    'recently_updated_pages' => 'Nedavno ažurirane stranice',\n    'recently_created_chapters' => 'Nedavno stvorena poglavlja',\n    'recently_created_books' => 'Nedavno stvorene knjige',\n    'recently_created_shelves' => 'Nedavno stvorene police',\n    'recently_update' => 'Nedavno ažurirano',\n    'recently_viewed' => 'Nedavno viđeno',\n    'recent_activity' => 'Nedavna aktivnost',\n    'create_now' => 'Stvori sada',\n    'revisions' => 'Revizije',\n    'meta_revision' => 'Revizija #:revisionCount',\n    'meta_created' => 'Stvoreno :timeLength',\n    'meta_created_name' => 'Stvoreno :timeLength od :user',\n    'meta_updated' => 'Ažurirano :timeLength',\n    'meta_updated_name' => 'Ažurirano :timeLength od :user',\n    'meta_owned_name' => 'Vlasništvo :user',\n    'meta_reference_count' => 'Referenced by :count item|Referenced by :count items',\n    'entity_select' => 'Odaberi subjekt',\n    'entity_select_lack_permission' => 'Nemate potrebne ovlasti za odabir ovog elementa',\n    'images' => 'Slike',\n    'my_recent_drafts' => 'Nedavne skice',\n    'my_recently_viewed' => 'Nedavno viđeno',\n    'my_most_viewed_favourites' => 'Moji Najviše Pregledani Favoriti',\n    'my_favourites' => 'Moji Favoriti',\n    'no_pages_viewed' => 'Niste pogledali nijednu stranicu',\n    'no_pages_recently_created' => 'Nema nedavno stvorenih stranica',\n    'no_pages_recently_updated' => 'Nema nedavno ažuriranih stranica',\n    'export' => 'Izvoz',\n    'export_html' => 'Web File',\n    'export_pdf' => 'PDF Datoteka',\n    'export_text' => 'Text File',\n    'export_md' => 'Markdown Datoteka',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Default Page Template',\n    'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',\n    'default_template_select' => 'Select a template page',\n    'import' => 'Import',\n    'import_validate' => 'Validate Import',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'Select ZIP file to upload',\n    'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',\n    'import_pending' => 'Pending Imports',\n    'import_pending_none' => 'No imports have been started.',\n    'import_continue' => 'Continue Import',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Run Import',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Uploaded by',\n    'import_location' => 'Import Location',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'Import Errors',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Dopuštenja',\n    'permissions_desc' => 'Postavi dozvole ovdje kako bi prebrisao zadane dozvole koje su dodeljene korisničkim ulogama.',\n    'permissions_book_cascade' => 'Dozvole postavljene na knjige će se automatski prenositi na podređena poglavlja i stranice, osim ako imaju definirane posebne dozvole.',\n    'permissions_chapter_cascade' => 'Dozvole postavljene na poglavlja će se automatski prenositi na podređene stranice, osim ako imaju definirane posebne dozvole.',\n    'permissions_save' => 'Spremi dopuštenje',\n    'permissions_owner' => 'Vlasnik',\n    'permissions_role_everyone_else' => 'Svi Ostali',\n    'permissions_role_everyone_else_desc' => 'Postavi dozvole za sve uloge koje nisu posebno prebrisane.',\n    'permissions_role_override' => 'Prebriši dozvole za ulogu',\n    'permissions_inherit_defaults' => 'Naslijedi zadane postavke',\n\n    // Search\n    'search_results' => 'Pretraži rezultate',\n    'search_total_results_found' => ':count rezultat|:count ukupno pronađenih rezultata',\n    'search_clear' => 'Očisti pretragu',\n    'search_no_pages' => 'Nijedna stranica ne podudara se s ovim pretraživanjem',\n    'search_for_term' => 'Traži :term',\n    'search_more' => 'Više rezultata',\n    'search_advanced' => 'Napredno pretraživanje',\n    'search_terms' => 'Pretraži pojmove',\n    'search_content_type' => 'Vrsta sadržaja',\n    'search_exact_matches' => 'Podudarnosti',\n    'search_tags' => 'Označi pretragu',\n    'search_options' => 'Opcije',\n    'search_viewed_by_me' => 'Pregledano od mene',\n    'search_not_viewed_by_me' => 'Nije pregledano od mene',\n    'search_permissions_set' => 'Set dopuštenja',\n    'search_created_by_me' => 'Stvoreno od mene',\n    'search_updated_by_me' => 'Ažurirano od mene',\n    'search_owned_by_me' => 'Moje vlasništvo',\n    'search_date_options' => 'Opcije datuma',\n    'search_updated_before' => 'Ažurirano prije',\n    'search_updated_after' => 'Ažurirano nakon',\n    'search_created_before' => 'Stvoreno prije',\n    'search_created_after' => 'Stvoreno nakon',\n    'search_set_date' => 'Datumi',\n    'search_update' => 'Ažuriraj pretragu',\n\n    // Shelves\n    'shelf' => 'Polica',\n    'shelves' => 'Police',\n    'x_shelves' => ':count polica|:count polica',\n    'shelves_empty' => 'Nijedna polica nije stvorena',\n    'shelves_create' => 'Stvori novu policu',\n    'shelves_popular' => 'Popularne police',\n    'shelves_new' => 'Nove police',\n    'shelves_new_action' => 'Nova polica',\n    'shelves_popular_empty' => 'Najpopularnije police pojavit će se. ovdje.',\n    'shelves_new_empty' => 'Nedavno stvorene police pojavit će se ovdje.',\n    'shelves_save' => 'Spremi policu',\n    'shelves_books' => 'Knjige na ovoj polici',\n    'shelves_add_books' => 'Dodaj knjige na ovu policu',\n    'shelves_drag_books' => 'Povucite knjige ispod kako biste ih dodali na ovu policu',\n    'shelves_empty_contents' => 'Ova polica još nema dodijeljene knjige',\n    'shelves_edit_and_assign' => 'Uredi policu za dodavanje knjiga',\n    'shelves_edit_named' => 'Uredi Policu :name',\n    'shelves_edit' => 'Uredi Policu',\n    'shelves_delete' => 'Izbriši Policu',\n    'shelves_delete_named' => 'Izbriši Policu :name',\n    'shelves_delete_explain' => \"Ovo će izbrisati policu pod nazivom \\\":name\\\". Knjige koje se nalaze na polici neće biti izbrisane.\",\n    'shelves_delete_confirmation' => 'Jeste li sigurni da želite obrisati policu?',\n    'shelves_permissions' => 'Dozvole za policu',\n    'shelves_permissions_updated' => 'Ažurirana dopuštenja za Policu',\n    'shelves_permissions_active' => 'Aktivna Dopuštenja za Policu',\n    'shelves_permissions_cascade_warning' => 'Dozvole na policama se automatski ne prenose na knjige koje se nalaze na njima. To je zato što se jedna knjiga može nalaziti na više polica. Međutim, dozvole se mogu kopirati na podređene knjige koristeći opciju koja se nalazi ispod.',\n    'shelves_permissions_create' => 'Dozvole za stvaranje police koriste se samo za kopiranje dozvola na podređene knjige pomoću radnje u nastavku. Ne kontroliraju sposobnost stvaranja knjiga.',\n    'shelves_copy_permissions_to_books' => 'Kopiraj dopuštenja za knjige',\n    'shelves_copy_permissions' => 'Kopiraj dopuštenja',\n    'shelves_copy_permissions_explain' => 'Ovo će primijeniti trenutne postavke dozvola ove police na sve knjige koje se nalaze na njoj. Prije aktiviranja, provjerite jeste li spremili sve promjene dozvola na ovoj polici.',\n    'shelves_copy_permission_success' => 'Dozvole s police kopirane su na :count knjiga',\n\n    // Books\n    'book' => 'Knjiga',\n    'books' => 'Knjige',\n    'x_books' => ':count knjiga|:count knjiga',\n    'books_empty' => 'Nijedna knjiga nije stvorena',\n    'books_popular' => 'Popularne knjige',\n    'books_recent' => 'Nedavne knjige',\n    'books_new' => 'Nove knjige',\n    'books_new_action' => 'Nova knjiga',\n    'books_popular_empty' => 'Najpopularnije knjige pojavit će se ovdje.',\n    'books_new_empty' => 'Najnovije knjige pojavit će se ovdje.',\n    'books_create' => 'Stvori novu knjigu',\n    'books_delete' => 'Izbriši knjigu',\n    'books_delete_named' => 'Izbriši knjigu :bookName',\n    'books_delete_explain' => 'Ovaj korak će izbrisati knjigu \\':bookName\\'. Izbrisati će sve stranice i poglavlja.',\n    'books_delete_confirmation' => 'Jeste li sigurni da želite izbrisati ovu knjigu?',\n    'books_edit' => 'Uredi knjigu',\n    'books_edit_named' => 'Uredi knjigu :bookName',\n    'books_form_book_name' => 'Ime knjige',\n    'books_save' => 'Spremi knjigu',\n    'books_permissions' => 'Dopuštenja za knjigu',\n    'books_permissions_updated' => 'Ažurirana dopuštenja za knjigu',\n    'books_empty_contents' => 'U ovoj knjizi još nema stranica ni poglavlja.',\n    'books_empty_create_page' => 'Stvori novu stranicu',\n    'books_empty_sort_current_book' => 'Razvrstaj postojeće knjige',\n    'books_empty_add_chapter' => 'Dodaj poglavlje',\n    'books_permissions_active' => 'Aktivna dopuštenja za knjigu',\n    'books_search_this' => 'Traži knjigu',\n    'books_navigation' => 'Navigacija knjige',\n    'books_sort' => 'Razvrstaj sadržaj knjige',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Razvrstaj knjigu :bookName',\n    'books_sort_name' => 'Razvrstaj po imenu',\n    'books_sort_created' => 'Razvrstaj po datumu nastanka',\n    'books_sort_updated' => 'Razvrstaj po datumu ažuriranja',\n    'books_sort_chapters_first' => 'Prva poglavlja',\n    'books_sort_chapters_last' => 'Zadnja poglavlja',\n    'books_sort_show_other' => 'Pokaži ostale knjige',\n    'books_sort_save' => 'Spremi novi poredak',\n    'books_sort_show_other_desc' => 'Dodajte druge knjige ovdje kako biste ih uključili u sortiranje i omogućili jednostavno premeštanje između knjiga.',\n    'books_sort_move_up' => 'Pomakni gore',\n    'books_sort_move_down' => 'Pomakni dolje',\n    'books_sort_move_prev_book' => 'Pomakni na Prethodnu Knjigu',\n    'books_sort_move_next_book' => 'Pomakni na Slijedeću Knjigu',\n    'books_sort_move_prev_chapter' => 'Pomakni na Prethodno Poglavlje',\n    'books_sort_move_next_chapter' => 'Pomakni na Slijedeće Poglavlje',\n    'books_sort_move_book_start' => 'Pomakni na Početak Knjige',\n    'books_sort_move_book_end' => 'Pomakni na Kraj Knjige',\n    'books_sort_move_before_chapter' => 'Pomakni Prije Poglavlja',\n    'books_sort_move_after_chapter' => 'Pomakni Nakon Poglavlja',\n    'books_copy' => 'Kopiraj Knjigu',\n    'books_copy_success' => 'Knjiga je uspješno kopirana',\n\n    // Chapters\n    'chapter' => 'Poglavlje',\n    'chapters' => 'Poglavlja',\n    'x_chapters' => ':count poglavlje|:count poglavlja',\n    'chapters_popular' => 'Popularna poglavlja',\n    'chapters_new' => 'Novo poglavlje',\n    'chapters_create' => 'Stvori novo poglavlje',\n    'chapters_delete' => 'Izbriši poglavlje',\n    'chapters_delete_named' => 'Izbriši poglavlje :chapterName',\n    'chapters_delete_explain' => 'Ovaj korak briše poglavlje \\':chapterName\\'. Sve stranice u njemu će biti izbrisane.',\n    'chapters_delete_confirm' => 'Jeste li sigurni da želite izbrisati poglavlje?',\n    'chapters_edit' => 'Uredi poglavlje',\n    'chapters_edit_named' => 'Uredi poglavlje :chapterName',\n    'chapters_save' => 'Spremi poglavlje',\n    'chapters_move' => 'Premjesti poglavlje',\n    'chapters_move_named' => 'Premjesti poglavlje :chapterName',\n    'chapters_copy' => 'Kopiraj Poglavlje',\n    'chapters_copy_success' => 'Poglavlje je uspješno kopirano',\n    'chapters_permissions' => 'Dopuštenja za poglavlje',\n    'chapters_empty' => 'U ovom poglavlju nema stranica.',\n    'chapters_permissions_active' => 'Aktivna dopuštenja za poglavlje',\n    'chapters_permissions_success' => 'Ažurirana dopuštenja za poglavlje',\n    'chapters_search_this' => 'Pretraži poglavlje',\n    'chapter_sort_book' => 'Sortiraj knjigu',\n\n    // Pages\n    'page' => 'Stranica',\n    'pages' => 'Stranice',\n    'x_pages' => ':count stranice|:count stranica',\n    'pages_popular' => 'Popularne stranice',\n    'pages_new' => 'Nova stranica',\n    'pages_attachments' => 'Prilozi',\n    'pages_navigation' => 'Navigacija stranice',\n    'pages_delete' => 'Izbriši stranicu',\n    'pages_delete_named' => 'Izbriši stranicu :pageName',\n    'pages_delete_draft_named' => 'Izbriši nacrt stranice :pageName',\n    'pages_delete_draft' => 'Izbriši nacrt stranice',\n    'pages_delete_success' => 'Izbrisana stranica',\n    'pages_delete_draft_success' => 'Izbrisan nacrt stranice',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'Jeste li sigurni da želite izbrisati stranicu?',\n    'pages_delete_draft_confirm' => 'Jeste li sigurni da želite izbrisati nacrt stranice?',\n    'pages_editing_named' => 'Uređivanje stranice :pageName',\n    'pages_edit_draft_options' => 'Izrada skice',\n    'pages_edit_save_draft' => 'Spremi nacrt',\n    'pages_edit_draft' => 'Uredi nacrt stranice',\n    'pages_editing_draft' => 'Uređivanja nacrta',\n    'pages_editing_page' => 'Uređivanje stranice',\n    'pages_edit_draft_save_at' => 'Nacrt spremljen kao',\n    'pages_edit_delete_draft' => 'Izbriši nacrt',\n    'pages_edit_delete_draft_confirm' => 'Jeste li sigurni da želite izbrisati promjene na stranici koje niste spremljene? Sve promjene koje ste napravili od posljednjeg potpunog spremanja bit će izgubljene, a uređivač će biti ažuriran s najnovijim spremljenim stanjem stranice.',\n    'pages_edit_discard_draft' => 'Odbaci nacrt',\n    'pages_edit_switch_to_markdown' => 'Prebacite se na Markdown uređivač',\n    'pages_edit_switch_to_markdown_clean' => '(Čisti Sadržaj)',\n    'pages_edit_switch_to_markdown_stable' => '(Stabilan Sadržaj)',\n    'pages_edit_switch_to_wysiwyg' => 'Prebaci se na WYSIWYG uređivač',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Postavi dnevnik promjena',\n    'pages_edit_enter_changelog_desc' => 'Ukratko opišite promjene koje ste napravili',\n    'pages_edit_enter_changelog' => 'Unesi dnevnik promjena',\n    'pages_editor_switch_title' => 'Promijeni Uređivač',\n    'pages_editor_switch_are_you_sure' => 'Jeste li sigurni da želite promijeniti uređivač za ovu stranicu?',\n    'pages_editor_switch_consider_following' => 'Kada mijenjate uređivače, uzmite u obzir sljedeće faktore:',\n    'pages_editor_switch_consideration_a' => 'Nakon što se spremi, nova opcija uređivača bit će korištena od strane svih budućih urednika, uključujući one koji možda neće moći sami promijeniti vrstu uređivača.',\n    'pages_editor_switch_consideration_b' => 'To može potencijalno dovesti do gubitka detalja i sintakse u određenim situacijama.',\n    'pages_editor_switch_consideration_c' => 'Promjene oznaka ili dnevnika promjena napravljene nakon posljednjeg spremanja neće se zadržati nakon ove promjene.',\n    'pages_save' => 'Spremi stranicu',\n    'pages_title' => 'Naslov stranice',\n    'pages_name' => 'Ime stranice',\n    'pages_md_editor' => 'Uređivač',\n    'pages_md_preview' => 'Pregled',\n    'pages_md_insert_image' => 'Umetni sliku',\n    'pages_md_insert_link' => 'Umetni poveznicu',\n    'pages_md_insert_drawing' => 'Umetni crtež',\n    'pages_md_show_preview' => 'Prikaži pregled',\n    'pages_md_sync_scroll' => 'Sinkroniziraj pomicanje pregleda',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Pronađen je Nespremljen Crtež',\n    'pages_drawing_unsaved_confirm' => 'Pronađeni su nespremljeni podaci crteža iz prethodnog neuspjelog pokušaja spremanja crteža. Želite li obnoviti i nastaviti uređivati ovaj nespremljeni crtež?',\n    'pages_not_in_chapter' => 'Stranica nije u poglavlju',\n    'pages_move' => 'Premjesti stranicu',\n    'pages_copy' => 'Kopiraj stranicu',\n    'pages_copy_desination' => 'Kopiraj odredište',\n    'pages_copy_success' => 'Stranica je uspješno kopirana',\n    'pages_permissions' => 'Dopuštenja stranice',\n    'pages_permissions_success' => 'Ažurirana dopuštenja stranice',\n    'pages_revision' => 'Revizija',\n    'pages_revisions' => 'Revizija stranice',\n    'pages_revisions_desc' => 'Ispod su navedene sve prošle revizije ove stranice. Možete pregledati, usporediti i obnoviti stare verzije stranice ako dozvole to omogućuju. Cjelokupna povijest stranice možda nije potpuno prikazana ovdje, budući da, ovisno o konfiguraciji sustava, stare revizije mogu biti automatski izbrisane.',\n    'pages_revisions_named' => 'Revizije stranice :pageName',\n    'pages_revision_named' => 'Revizija stranice :pageName',\n    'pages_revision_restored_from' => 'Oporavak iz #:id; :summary',\n    'pages_revisions_created_by' => 'Stvoreno od',\n    'pages_revisions_date' => 'Datum revizije',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Broj revizije',\n    'pages_revisions_numbered' => 'Revizija #:id',\n    'pages_revisions_numbered_changes' => 'Revizija #:id Promjene',\n    'pages_revisions_editor' => 'Vrsta uređivača',\n    'pages_revisions_changelog' => 'Dnevnik promjena',\n    'pages_revisions_changes' => 'Promjene',\n    'pages_revisions_current' => 'Trenutna verzija',\n    'pages_revisions_preview' => 'Pregled',\n    'pages_revisions_restore' => 'Vrati',\n    'pages_revisions_none' => 'Ova stranica nema revizija',\n    'pages_copy_link' => 'Kopiraj poveznicu',\n    'pages_edit_content_link' => 'Skoči na odjeljak u uređivaču',\n    'pages_pointer_enter_mode' => 'Uđi u način odabira odjeljaka',\n    'pages_pointer_label' => 'Opcije odjeljka stranice',\n    'pages_pointer_permalink' => 'Permalink Odjeljka Stranice',\n    'pages_pointer_include_tag' => 'Uključi oznaku odjeljka stranice',\n    'pages_pointer_toggle_link' => 'Način permalinka, pritisnite za prikaz oznake uključivanja (include tag)',\n    'pages_pointer_toggle_include' => 'Način oznake uključivanja, Pritisnite za prikaz permalinka',\n    'pages_permissions_active' => 'Aktivna dopuštenja stranice',\n    'pages_initial_revision' => 'Početno objavljivanje',\n    'pages_references_update_revision' => 'Automatsko ažuriranje internih veza sustava',\n    'pages_initial_name' => 'Nova stranica',\n    'pages_editing_draft_notification' => 'Uređujete nacrt stranice posljednji put spremljen :timeDiff.',\n    'pages_draft_edited_notification' => 'Ova je stranica u međuvremenu ažurirana. Preporučujemo da odbacite ovaj nacrt.',\n    'pages_draft_page_changed_since_creation' => 'Ova stranica je ažurirana nakon što je ovaj nacrt stvoren. Preporučuje se da odbacite ovaj nacrt ili budite oprezni da ne prepišete nikakve promjene na stranici.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count korisnika koji uređuju ovu stranicu',\n        'start_b' => ':userName je počeo uređivati ovu stranicu',\n        'time_a' => 'otkad je stranica posljednji put ažurirana',\n        'time_b' => 'u zadnjih :minCount minuta',\n        'message' => ':start :time. Pripazite na uzajamna ažuriranja!',\n    ],\n    'pages_draft_discarded' => 'Nacrt je odbačen! Uređivač je ažuriran s trenutnim sadržajem stranice',\n    'pages_draft_deleted' => 'Nacrt je izbrisan! Uređivač je ažuriran s trenutnim sadržajem stranice',\n    'pages_specific' => 'Predlošci stranice',\n    'pages_is_template' => 'Predložak stranice',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Toggle Sidebar',\n    'page_tags' => 'Oznake stranice',\n    'chapter_tags' => 'Oznake poglavlja',\n    'book_tags' => 'Oznake knjiga',\n    'shelf_tags' => 'Oznake polica',\n    'tag' => 'Oznaka',\n    'tags' =>  'Oznake',\n    'tags_index_desc' => 'Oznake se mogu primijeniti na sadržaj unutar sustava kako bi se primijenila fleksibilna forma kategorizacije. Oznake mogu imati ključ i vrijednost, pri čemu je vrijednost opcionalna. Nakon primjene, sadržaj se može pretraživati koristeći ime oznake i vrijednost.',\n    'tag_name' =>  'Naziv Oznake',\n    'tag_value' => 'Oznaka vrijednosti (neobavezno)',\n    'tags_explain' => \"Dodajte neke oznake kako biste bolje kategorizirali svoj sadržaj. \\n Možete dodijeliti vrijednost oznaci za organizaciju s više detalja.\",\n    'tags_add' => 'Dodaj oznaku',\n    'tags_remove' => 'Makni oznaku',\n    'tags_usages' => 'Ukupna upotreba oznaka',\n    'tags_assigned_pages' => 'Dodijeljeno Stranicama',\n    'tags_assigned_chapters' => 'Dodijeljeno Poglavljima',\n    'tags_assigned_books' => 'Dodijeljeno Knjigama',\n    'tags_assigned_shelves' => 'Dodijeljeno Policama',\n    'tags_x_unique_values' => ':count jedinstvenih vrijednosti',\n    'tags_all_values' => 'Sve vrijednosti',\n    'tags_view_tags' => 'Pregledaj Oznake',\n    'tags_view_existing_tags' => 'Pregledaj postojeće oznake',\n    'tags_list_empty_hint' => 'Oznake se mogu dodijeliti putem bočne trake uređivača stranice ili prilikom uređivanja detalja knjige, poglavlja ili police.',\n    'attachments' => 'Prilozi',\n    'attachments_explain' => 'Dodajte datoteke ili poveznice za prikaz na vašoj stranici. Vidljive su na rubnoj oznaci stranice.',\n    'attachments_explain_instant_save' => 'Promjene se automatski spremaju.',\n    'attachments_upload' => 'Dodaj datoteku',\n    'attachments_link' => 'Dodaj poveznicu',\n    'attachments_upload_drop' => 'Alternativno, možete povući i ispustiti datoteku ovdje kako biste je pričvrstili.',\n    'attachments_set_link' => 'Postavi poveznicu',\n    'attachments_delete' => 'Jeste li sigurni da želite izbrisati ovu stavku?',\n    'attachments_dropzone' => 'Ovdje spustite datoteke za učitavanje',\n    'attachments_no_files' => 'Nijedna datoteka nije prenesena',\n    'attachments_explain_link' => 'Možete dodati poveznicu ako ne želite prenijeti datoteku. Poveznica može voditi na drugu stranicu ili datoteku.',\n    'attachments_link_name' => 'Ime poveznice',\n    'attachment_link' => 'Poveznica na privitak',\n    'attachments_link_url' => 'Poveznica na datoteku',\n    'attachments_link_url_hint' => 'Url ili stranica ili datoteka',\n    'attach' => 'Dodaj',\n    'attachments_insert_link' => 'Dodaj poveznicu na stranicu',\n    'attachments_edit_file' => 'Uredi datoteku',\n    'attachments_edit_file_name' => 'Ime datoteke',\n    'attachments_edit_drop_upload' => 'Dodaj datoteku ili klikni ovdje za prijenos',\n    'attachments_order_updated' => 'Ažurirani popis priloga',\n    'attachments_updated_success' => 'Ažurirani detalji priloga',\n    'attachments_deleted' => 'Izbrisani prilozi',\n    'attachments_file_uploaded' => 'Datoteka je uspješno prenešena',\n    'attachments_file_updated' => 'Datoteka je uspješno ažurirana',\n    'attachments_link_attached' => 'Poveznica je dodana na stranicu',\n    'templates' => 'Predlošci',\n    'templates_set_as_template' => 'Stranica je predložak',\n    'templates_explain_set_as_template' => 'Ovu stranicu možete postaviti pomoću predloška koji možete koristiti tijekom stvaranja drugih stranica. Ostali korisnici će ga također moći koristiti ako imaju dopuštenje.',\n    'templates_replace_content' => 'Zamjeni sadržaj stranice',\n    'templates_append_content' => 'Dodaj sadržaju stranice',\n    'templates_prepend_content' => 'Dodaj na sadržaj stranice',\n\n    // Profile View\n    'profile_user_for_x' => 'Korisnik za :time',\n    'profile_created_content' => 'Stvoreni sadržaj',\n    'profile_not_created_pages' => ':userName nije kreirao nijednu stranicu',\n    'profile_not_created_chapters' => ':userName nije kreirao nijedno poglavlje',\n    'profile_not_created_books' => ':userName nije kreirao nijednu knjigu',\n    'profile_not_created_shelves' => ':userName nije kreirao nijednu policu',\n\n    // Comments\n    'comment' => 'Komentar',\n    'comments' => 'Komentari',\n    'comment_add' => 'Dodaj komentar',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Komentar ostavi ovdje',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Spremi komentar',\n    'comment_new' => 'Novi komentar',\n    'comment_created' => 'komentirano :createDiff',\n    'comment_updated' => 'Ažurirano :updateDiff od :username',\n    'comment_updated_indicator' => 'Ažurirano',\n    'comment_deleted_success' => 'Izbrisani komentar',\n    'comment_created_success' => 'Dodani komentar',\n    'comment_updated_success' => 'Ažurirani komentar',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Jeste li sigurni da želite izbrisati ovaj komentar?',\n    'comment_in_reply_to' => 'Odgovor na :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Evo komentara koji su ostavljeni na ovoj stranici. Komentari se mogu dodavati i upravljati prilikom pregleda spremljene stranice.',\n\n    // Revision\n    'revision_delete_confirm' => 'Jeste li sigurni da želite izbrisati ovaj ispravak?',\n    'revision_restore_confirm' => 'Jeste li sigurni da želite vratiti ovaj ispravak? Trenutni sadržaj će biti zamijenjen.',\n    'revision_cannot_delete_latest' => 'Posljednji ispravak se ne može izbrisati.',\n\n    // Copy view\n    'copy_consider' => 'Molimo vas da uzmete u obzir sljedeće prilikom kopiranja sadržaja.',\n    'copy_consider_permissions' => 'Prilagođene postavke dozvola neće biti kopirane.',\n    'copy_consider_owner' => 'Postat ćete vlasnikom svih kopiranih sadržaja.',\n    'copy_consider_images' => 'Slikovne datoteke stranice neće biti duplicirane, a originalne slike će zadržati svoj odnos prema stranici na koju su prvobitno prenesene.',\n    'copy_consider_attachments' => 'Privici stranice neće biti kopirani.',\n    'copy_consider_access' => 'Promjena lokacije, vlasnika ili dozvola može rezultirati pristupom ovom sadržaju osobama koje ranije nisu imale pristup.',\n\n    // Conversions\n    'convert_to_shelf' => 'Pretvori u Policu',\n    'convert_to_shelf_contents_desc' => 'Možete pretvoriti ovu knjigu u novu policu s istim sadržajem. Poglavlja koja se nalaze unutar ove knjige bit će pretvorena u nove knjige. Ako ova knjiga sadrži bilo koje stranice koje nisu u poglavlju, ova knjiga će biti preimenovana i sadržavati takve stranice, a postat će dio nove police.',\n    'convert_to_shelf_permissions_desc' => 'Sve dozvole postavljene na ovu knjigu bit će kopirane na novu policu i sve nove podređene knjige koje nemaju vlastite dozvole. Napomena: Dozvole na policama se ne prenose automatski na sadržaj unutar njih, kao što je slučaj s knjigama.',\n    'convert_book' => 'Pretvori u Knjigu',\n    'convert_book_confirm' => 'Jeste li sigurni da želite pretvoriti ovu knjigu?',\n    'convert_undo_warning' => 'Ovo se ne može tako lako poništiti.',\n    'convert_to_book' => 'Pretvori u Knjigu',\n    'convert_to_book_desc' => 'Možete pretvoriti ovo poglavlje u novu knjigu s istim sadržajem. Sve postavljene dozvole na ovom poglavlju bit će kopirane u novu knjigu, ali nasljedne dozvole iz nadređene knjige neće biti kopirane, što može rezultirati promjenom kontrole pristupa.',\n    'convert_chapter' => 'Pretvori Poglavlje',\n    'convert_chapter_confirm' => 'Jeste li sigurni da želite pretvoriti ovo poglavlje?',\n\n    // References\n    'references' => 'Reference',\n    'references_none' => 'Nema praćenih referenci na ovu stavku.',\n    'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',\n\n    // Watch Options\n    'watch' => 'Prati',\n    'watch_title_default' => 'Zadane Postavke',\n    'watch_desc_default' => 'Vratite praćenje samo na vaše zadane postavke obavijesti.',\n    'watch_title_ignore' => 'Zanemari',\n    'watch_desc_ignore' => 'Ignorirajte sve obavijesti, uključujući one iz postavki na razini korisnika.',\n    'watch_title_new' => 'Nove Stranice',\n    'watch_desc_new' => 'Obavijesti kada se stvori nova stranica unutar ove stavke.',\n    'watch_title_updates' => 'Sve Promjene na Stranicama',\n    'watch_desc_updates' => 'Obavijesti o svim novim stranicama i promjenama na stranicama.',\n    'watch_desc_updates_page' => 'Obavijesti o svim promjenama na stranicama.',\n    'watch_title_comments' => 'Sve Promjene na Stranicama i Komentari',\n    'watch_desc_comments' => 'Obavijesti o svim novim stranicama, promjenama na stranicama i novim komentarima.',\n    'watch_desc_comments_page' => 'Obavijesti o promjenama na stranicama i novim komentarima.',\n    'watch_change_default' => 'Promijenite zadane postavke obavijesti',\n    'watch_detail_ignore' => 'Ignoriranje obavijesti',\n    'watch_detail_new' => 'Prati nove stranice',\n    'watch_detail_updates' => 'Prati nove stranice i ažuriranja',\n    'watch_detail_comments' => 'Prati nove stranice, ažuriranja i komentare',\n    'watch_detail_parent_book' => 'Prati putem nadređene knjige',\n    'watch_detail_parent_book_ignore' => 'Ignoriraj putem nadređene knjige',\n    'watch_detail_parent_chapter' => 'Prati puten nadređenog poglavlja',\n    'watch_detail_parent_chapter_ignore' => 'Ignoriraj putem nadređenog poglavlja',\n];\n"
  },
  {
    "path": "lang/hr/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Nemate dopuštenje za pristup traženoj stranici.',\n    'permissionJson' => 'Nemate potrebno dopuštenje.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Korisnik s mailom :email već postoji, ali s drugom vjerodajnicom.',\n    'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',\n    'email_already_confirmed' => 'Email je već potvrđen, pokušajte se logirati.',\n    'email_confirmation_invalid' => 'Ova vjerodajnica nije valjana ili je već bila korištena. Pokušajte se ponovno registrirati.',\n    'email_confirmation_expired' => 'Ova vjerodajnica je istekla. Poslan je novi email za pristup.',\n    'email_confirmation_awaiting' => 'Email adresa za račun koji se koristi mora biti potvrđen',\n    'ldap_fail_anonymous' => 'LDAP pristup nije uspio zbog anonimnosti',\n    'ldap_fail_authed' => 'LDAP pristup nije uspio',\n    'ldap_extension_not_installed' => 'LDAP PHP ekstenzija nije instalirana',\n    'ldap_cannot_connect' => 'Nemoguće pristupiti ldap serveru, problem s mrežom',\n    'saml_already_logged_in' => 'Već ste prijavljeni',\n    'saml_no_email_address' => 'Nismo pronašli email adresu za ovog korisnika u vanjskim sustavima',\n    'saml_invalid_response_id' => 'Sustav za autentifikaciju nije prepoznat. Ovaj problem možda je nastao zbog vraćanja nakon prijave.',\n    'saml_fail_authed' => 'Prijava pomoću :system nije uspjela zbog neuspješne autorizacije',\n    'oidc_already_logged_in' => 'Već ste prijavljeni',\n    'oidc_no_email_address' => 'Nije moguće pronaći adresu e-pošte za ovog korisnika u podacima koje pruža vanjski sustav za autentifikaciju',\n    'oidc_fail_authed' => 'Prijavljivanje putem :system nije uspjelo. Sustav nije uspješno odobrio autorizaciju',\n    'social_no_action_defined' => 'Nije definirana nijedna radnja',\n    'social_login_bad_response' => \"Greška primljena prilikom prijave putem :socialAccount: :error\",\n    'social_account_in_use' => 'Ovaj :socialAccount račun se već koristi. Pokušajte se prijaviti pomoću :socialAccount računa.',\n    'social_account_email_in_use' => 'Ovaj mail :email se već koristi. Ako već imate naš račun možete se prijaviti pomoću :socialAccount računa u postavkama vašeg profila.',\n    'social_account_existing' => 'Ovaj :socialAccount je već dodan u vaš profil.',\n    'social_account_already_used_existing' => 'Ovaj :socialAccount već koristi drugi korisnik.',\n    'social_account_not_used' => 'Ovaj :socialAccount račun ne koristi nijedan korisnik. Dodajte ga u postavke svog profila.',\n    'social_account_register_instructions' => 'Ako nemate račun možete se registrirati pomoću :socialAccount opcija.',\n    'social_driver_not_found' => 'Nije pronađeno',\n    'social_driver_not_configured' => 'Postavke vašeg :socialAccount računa nisu ispravno postavljene.',\n    'invite_token_expired' => 'Vaša pozivnica je istekla. Pokušajte ponovno postaviti lozinku.',\n    'login_user_not_found' => 'A user for this action could not be found.',\n\n    // System\n    'path_not_writable' => 'Datoteka :filePath ne može se prenijeti. Učinite je lakše prepoznatljivom vašem serveru.',\n    'cannot_get_image_from_url' => 'Nemoguće preuzeti sliku sa :url',\n    'cannot_create_thumbs' => 'Provjerite imate li instaliranu GD PHP ekstenziju.',\n    'server_upload_limit' => 'Prevelika količina za server. Pokušajte prenijeti manju veličinu.',\n    'server_post_limit' => 'The server cannot receive the provided amount of data. Try again with less data or a smaller file.',\n    'uploaded'  => 'Prevelika količina za server. Pokušajte prenijeti manju veličinu.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Problem s prenosom slike',\n    'image_upload_type_error' => 'Nepodržani format slike',\n    'image_upload_replace_type' => 'Zamjene slikovnih datoteka moraju biti iste vrste',\n    'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.',\n    'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.',\n    'image_gallery_thumbnail_memory_limit' => 'Failed to create gallery thumbnails due to system resource limits.',\n    'drawing_data_not_found' => 'Podaci o crtežu se ne mogu učitati. Datoteka crteža možda više ne postoji ili nemate dozvolu za pristupanje istoj.',\n\n    // Attachments\n    'attachment_not_found' => 'Prilozi nisu pronađeni',\n    'attachment_upload_error' => 'Došlo je do pogreške prilikom prijenosa datoteke privitka',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Problem sa spremanjem nacrta. Osigurajte stabilnu internetsku vezu.',\n    'page_draft_delete_fail' => 'Nije uspjelo brisanje privremene verzije stranice i dohvaćanje trenutno spremljenog sadržaja stranice',\n    'page_custom_home_deletion' => 'Stranica označena kao naslovnica ne može se izbrisati',\n\n    // Entities\n    'entity_not_found' => 'Nije pronađeno',\n    'bookshelf_not_found' => 'Polica nije pronađena',\n    'book_not_found' => 'Knjiga nije pronađena',\n    'page_not_found' => 'Stranica nije pronađena',\n    'chapter_not_found' => 'Poglavlje nije pronađeno',\n    'selected_book_not_found' => 'Odabrana knjiga nije pronađena',\n    'selected_book_chapter_not_found' => 'Odabrane knjige ili poglavlja nisu pronađena',\n    'guests_cannot_save_drafts' => 'Gosti ne mogu spremiti nacrte',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Ne možete izbrisati',\n    'users_cannot_delete_guest' => 'Ne možete izbrisati',\n    'users_could_not_send_invite' => 'Could not create user since invite email failed to send',\n\n    // Roles\n    'role_cannot_be_edited' => 'Ne može se urediti',\n    'role_system_cannot_be_deleted' => 'Sistemske postavke ne možete izbrisati',\n    'role_registration_default_cannot_delete' => 'Ne može se izbrisati',\n    'role_cannot_remove_only_admin' => 'Učinite drugog korisnika administratorom prije uklanjanja ove administratorske uloge.',\n\n    // Comments\n    'comment_list' => 'Pogreška prilikom dohvaćanja komentara.',\n    'cannot_add_comment_to_draft' => 'Ne možete ostaviti komentar na ovaj nacrt.',\n    'comment_add' => 'Greška prilikom dodavanja ili ažuriranja komentara.',\n    'comment_delete' => 'Greška prilikom brisanja komentara.',\n    'empty_comment' => 'Ne možete ostaviti prazan komentar.',\n\n    // Error pages\n    '404_page_not_found' => 'Stranica nije pronađena',\n    'sorry_page_not_found' => 'Žao nam je, stranica koju tražite nije pronađena.',\n    'sorry_page_not_found_permission_warning' => 'Ako smatrate da ova stranica još postoji, ali je ne vidite, moguće je da nemate omogućen pristup.',\n    'image_not_found' => 'Slika Nije Pronađena',\n    'image_not_found_subtitle' => 'Žao nam je, slikovna datoteka koju tražite nije pronađena.',\n    'image_not_found_details' => 'Ako ste očekivali da ova slika postoji, moguće je da je izbrisana.',\n    'return_home' => 'Povratak na početno',\n    'error_occurred' => 'Došlo je do pogreške',\n    'app_down' => ':appName trenutno nije dostupna',\n    'back_soon' => 'Uskoro će se vratiti.',\n\n    // Import\n    'import_zip_cant_read' => 'Could not read ZIP file.',\n    'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',\n    'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Import ZIP failed to validate with errors:',\n    'import_zip_failed_notification' => 'Failed to import ZIP file.',\n    'import_perms_books' => 'You are lacking the required permissions to create books.',\n    'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',\n    'import_perms_pages' => 'You are lacking the required permissions to create pages.',\n    'import_perms_images' => 'You are lacking the required permissions to create images.',\n    'import_perms_attachments' => 'You are lacking the required permission to create attachments.',\n\n    // API errors\n    'api_no_authorization_found' => 'Nije pronađena autorizacija',\n    'api_bad_authorization_format' => 'Pogreška prilikom autorizacije',\n    'api_user_token_not_found' => 'Format autorizacije nije podržan',\n    'api_incorrect_token_secret' => 'Netočan API token',\n    'api_user_no_api_permission' => 'Vlasnik API tokena nema potrebna dopuštenja',\n    'api_user_token_expired' => 'Autorizacija je istekla',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Pogreška prilikom slanja testnog email:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'URL se ne podudara s konfiguriranim dozvoljenim SSR domaćinima',\n];\n"
  },
  {
    "path": "lang/hr/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Novi komentar na stranici: :pageName',\n    'new_comment_intro' => 'Korisnik je komentirao stranicu u :appName:',\n    'new_page_subject' => 'Nova stranica: :pageName',\n    'new_page_intro' => 'Nova stranica je stvorena u :appName:',\n    'updated_page_subject' => 'ChatGPT\n\nAžurirana stranica: :pageName',\n    'updated_page_intro' => 'Stranica je ažurirana u :appName:',\n    'updated_page_debounce' => 'Kako biste spriječili velik broj obavijesti, nećete primati obavijesti o daljnjim izmjenama ove stranice od istog urednika neko vrijeme.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Naziv Stranice:',\n    'detail_page_path' => 'Page Path:',\n    'detail_commenter' => 'Komentator:',\n    'detail_comment' => 'Komentar:',\n    'detail_created_by' => 'Kreirao Korisnik:',\n    'detail_updated_by' => 'Ažurirao Korisnik:',\n\n    'action_view_comment' => 'Pogledaj Komentar',\n    'action_view_page' => 'Pogledaj Stranicu',\n\n    'footer_reason' => 'Ova obavijest vam je poslana jer :link pokriva ovu vrstu aktivnosti za ovu stavku.',\n    'footer_reason_link' => 'vaše postavke obavijesti',\n];\n"
  },
  {
    "path": "lang/hr/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Prethodno',\n    'next'     => 'Sljedeće &raquo;',\n\n];\n"
  },
  {
    "path": "lang/hr/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Lozinka mora imati najmanje 8 znakova i biti potvrđena.',\n    'user' => \"Ne možemo pronaći korisnika s tom adresom e-pošte.\",\n    'token' => 'Ponovno postavljanje lozinke nemoguće putem ove adrese.',\n    'sent' => 'Na vašu email adresu poslana je poveznica za ponovno postavljanje!',\n    'reset' => 'Vaša je lozinka ponovno postavljena!',\n\n];\n"
  },
  {
    "path": "lang/hr/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'My Account',\n\n    'shortcuts' => 'Prečaci',\n    'shortcuts_interface' => 'UI Shortcut Preferences',\n    'shortcuts_toggle_desc' => 'Ovdje možete omogućiti ili onemogućiti prečace tastature u korisničkom sučelju sustava koji se koriste za navigaciju i akcije.',\n    'shortcuts_customize_desc' => 'Možete prilagoditi svaki od prečaca u nastavku. Samo pritisnite željenu kombinaciju tipki nakon odabira polja za unos prečaca.',\n    'shortcuts_toggle_label' => 'Prečaci tipkovnice omogućeni',\n    'shortcuts_section_navigation' => 'Navigacija',\n    'shortcuts_section_actions' => 'Uobičajene radnje',\n    'shortcuts_save' => 'Spremi prečace',\n    'shortcuts_overlay_desc' => 'Napomena: Kada su prečaci tastature omogućeni, dostupan je pomoćni prikaz preko pritiska na znak \"?\" koji će istaknuti dostupne prečace za radnje trenutno vidljive na zaslonu.',\n    'shortcuts_update_success' => 'Postavke prečaca su ažurirane!',\n    'shortcuts_overview_desc' => 'Upravljajte prečacima tastature koje možete koristiti za navigaciju korisničkim sučeljem sustava.',\n\n    'notifications' => 'Postavke Obavijesti',\n    'notifications_desc' => 'Kontrolirajte e-mail obavijesti koje primate kada se određene aktivnosti izvrše unutar sustava.',\n    'notifications_opt_own_page_changes' => 'Obavijesti o promjenama na stranicama koje posjedujem',\n    'notifications_opt_own_page_comments' => 'ChatGPT\n\nObavijesti o komentarima na stranicama koje posjedujem',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Obavijesti o odgovorima na moje komentare',\n    'notifications_save' => 'Spremi Postavke',\n    'notifications_update_success' => 'Postavke obavijesti su ažurirane!',\n    'notifications_watched' => 'Praćene i ignorirane stavke',\n    'notifications_watched_desc' => 'Ispod su stavke na koje su primijenjene prilagođene postavke praćenja. Da biste ažurirali svoje postavke za ove stavke, pregledajte stavku, a zatim pronađite opcije praćenja u bočnoj traci.',\n\n    'auth' => 'Access & Security',\n    'auth_change_password' => 'Change Password',\n    'auth_change_password_desc' => 'Change the password you use to log-in to the application. This must be at least 8 characters long.',\n    'auth_change_password_success' => 'Password has been updated!',\n\n    'profile' => 'Profile Details',\n    'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.',\n    'profile_view_public' => 'View Public Profile',\n    'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.',\n    'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.',\n    'profile_email_no_permission' => 'Unfortunately you don\\'t have permission to change your email address. If you want to change this, you\\'d need to ask an administrator to change this for you.',\n    'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.',\n    'profile_admin_options' => 'Administrator Options',\n    'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the \"Settings > Users\" area of the application.',\n\n    'delete_account' => 'Delete Account',\n    'delete_my_account' => 'Delete My Account',\n    'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\\'ve created, such as created pages and uploaded images, will remain.',\n    'delete_my_account_warning' => 'Are you sure you want to delete your account?',\n];\n"
  },
  {
    "path": "lang/hr/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Postavke',\n    'settings_save' => 'Spremi postavke',\n    'system_version' => 'Sistemska Verzija',\n    'categories' => 'Kategorije',\n\n    // App Settings\n    'app_customization' => 'Prilagođavanje',\n    'app_features_security' => 'Značajke & Sigurnost',\n    'app_name' => 'Ime aplikacije',\n    'app_name_desc' => 'Ime je vidljivo u zaglavlju i svakoj sistemskoj poruci.',\n    'app_name_header' => 'Prikaži ime u zaglavlju',\n    'app_public_access' => 'Javni pristup',\n    'app_public_access_desc' => 'Omogućavanje ove postavke pristup sadržaju imat će svi posjetitelji BookStack čak i ako nisu prijavljeni.',\n    'app_public_access_desc_guest' => 'Javni pristup može se urediti putem opcije \"Gost\".',\n    'app_public_access_toggle' => 'Dozvoli javni pristup',\n    'app_public_viewing' => 'Dozvoljen javni pristup?',\n    'app_secure_images' => 'Visoka razina sigurnosti prijenosa slika',\n    'app_secure_images_toggle' => 'Omogući visoku sigurnost prijenosa slika',\n    'app_secure_images_desc' => 'Zbog specifične izvedbe sve su slike javne. Osigurajte da indeksi direktorija nisu omogućeni kako bi se spriječio neovlašten pristup.',\n    'app_default_editor' => 'Zadani Uređivač Stranica',\n    'app_default_editor_desc' => 'Odaberite koji uređivač će se koristiti kao zadani prilikom uređivanja novih stranica. Ovo se može prebrisati na razini pojedine stranice ukoliko dozvole to omogućuju.',\n    'app_custom_html' => 'Prilagođeni HTML sadržaj',\n    'app_custom_html_desc' => 'Sav sadržaj dodan ovdje bit će umetnut na dno <glavne> stranice. To je korisno za stiliziranje i dodavanje analitičkog koda.',\n    'app_custom_html_disabled_notice' => 'Prilagođeni HTML je onemogućen kako bi se osiguralo vraćanje promjena u slučaju kvara.',\n    'app_logo' => 'Logo aplikacije',\n    'app_logo_desc' => 'Ovo se koristi u zaglavlju aplikacije, među ostalim područjima. Ova slika treba biti visoka 86 piksela. Velike slike bit će smanjene.',\n    'app_icon' => 'Ikona Aplikacije',\n    'app_icon_desc' => 'Ova ikona se koristi za kartice preglednika i ikone prečaca. Trebala bi biti PNG slika kvadratnog oblika sa dimenzijama 256 piksela.',\n    'app_homepage' => 'Glavna stranica aplikacije',\n    'app_homepage_desc' => 'Odaberite prikaz svoje glavne stranice umjesto već zadane. Za odabrane stranice ne vrijede zadana dopuštenja.',\n    'app_homepage_select' => 'Odaberi stranicu',\n    'app_footer_links' => 'Podnožje',\n    'app_footer_links_desc' => 'Odaberite poveznice koje će biti vidljive u podnožju većina stranica čak i na nekima koje ne zahtijevaju prijavu. Na primjer, oznaku \"trans::common.privacy_policy\" možete koristiti za sistemski definirani prijevod teksta \"Politika Privatnosti\", a za \"Uvjete korištenja\" možete koristiti \"trans::common.terms_of_service\".',\n    'app_footer_links_label' => 'Oznaka veze',\n    'app_footer_links_url' => 'Oznaka URL',\n    'app_footer_links_add' => 'Dodaj vezu na podnožje',\n    'app_disable_comments' => 'Onemogući komentare',\n    'app_disable_comments_toggle' => 'Onemogući komentare',\n    'app_disable_comments_desc' => 'Onemogući komentare za sve stranice u aplikaciji. <br> Postojeći komentari nisu prikazani.',\n\n    // Color settings\n    'color_scheme' => 'Paleta Boje Aplikacije',\n    'color_scheme_desc' => 'Postavite boje koje će se koristiti u korisničkom sučelju aplikacije. Boje se mogu konfigurirati zasebno za tamni i svijetli način rada kako bi se najbolje uklopile u temu i osigurale čitljivost.',\n    'ui_colors_desc' => 'Postavite primarnu boju aplikacije i zadane boje veza. Primarna boja se uglavnom koristi za zaglavlje trake, gumbe i dekoracije sučelja. Zadana boja veza se koristi za tekstualne veze i radnje, kako unutar sadržaja tako i u korisničkom sučelju aplikacije.',\n    'app_color' => 'Primarnab Boja',\n    'link_color' => 'Zadana Boja Veze',\n    'content_colors_desc' => 'Postavite boje za sve elemente u hijerarhiji organizacije stranica. Preporučuje se odabir boja slične svjetlini kao zadane boje radi bolje čitljivosti.',\n    'bookshelf_color' => 'Boja police',\n    'book_color' => 'Boja knjige',\n    'chapter_color' => 'Boja poglavlja',\n    'page_color' => 'Boja stranice',\n    'page_draft_color' => 'Boja nacrta',\n\n    // Registration Settings\n    'reg_settings' => 'Registracija',\n    'reg_enable' => 'Omogući registraciju',\n    'reg_enable_toggle' => 'Omogući registraciju',\n    'reg_enable_desc' => 'Ako je omogućeno korisnik se može sam registrirati nakon čega će mu biti dodijeljena jedna od korisničkih uloga.',\n    'reg_default_role' => 'Zadaj ulogu korisnika nakon registracije',\n    'reg_enable_external_warning' => 'Gornja opcija se zanemaruje ako postoji LDAP ili SAML autorifikacija. Korisnički računi za nepostojeće članove automatski će se kreirati ako je vanjska provjera autentičnosti bila uspješna.',\n    'reg_email_confirmation' => 'Potvrda e maila',\n    'reg_email_confirmation_toggle' => 'Zahtjev za potvrdom e maila',\n    'reg_confirm_email_desc' => 'Ako postoje ograničenja domene potvrda e maila će se zahtijevati i ova će se opcija zanemariti.',\n    'reg_confirm_restrict_domain' => 'Ograničenja domene',\n    'reg_confirm_restrict_domain_desc' => 'Unesite popis email domena kojima želite ograničiti registraciju i odvojite ih zarezom. Korisnicima će se slati email prije interakcije s aplikacijom. <br> Uzmite u obzir da će korisnici moći koristiti druge e mail adrese nakon uspješne registracije.',\n    'reg_confirm_restrict_domain_placeholder' => 'Bez ograničenja',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Održavanje',\n    'maint_image_cleanup' => 'Čišćenje slika',\n    'maint_image_cleanup_desc' => 'Skenirajte sadržaj stranice i revizije kako biste provjerili koje slike i crteži se trenutno koriste i koje slike su suvišne. Pobrinite se napraviti potpunu sigurnosnu kopiju baze podataka i slika prije pokretanja ovog procesa.',\n    'maint_delete_images_only_in_revisions' => 'Izbriši slike koje postoje u prijašnjim revizijama',\n    'maint_image_cleanup_run' => 'Pokreni čišćenje',\n    'maint_image_cleanup_warning' => ':count moguće neiskorištene slike. Jeste li sigurni da želite izbrisati ove slike?',\n    'maint_image_cleanup_success' => ':count moguće neiskorištene slike su pronađene i izbrisane!',\n    'maint_image_cleanup_nothing_found' => 'Nema neiskorištenih slika, Ništa nije izbrisano!',\n    'maint_send_test_email' => 'Pošalji testni Email',\n    'maint_send_test_email_desc' => 'Na ovaj način šaljete testni Email na adresu navedenu u vašem profilu.',\n    'maint_send_test_email_run' => 'Pošalji testni email',\n    'maint_send_test_email_success' => 'Email je poslan na :address',\n    'maint_send_test_email_mail_subject' => 'Testni email',\n    'maint_send_test_email_mail_greeting' => 'Email se može koristiti!',\n    'maint_send_test_email_mail_text' => 'Čestitamo! Ako ste primili ovaj e mail znači da ćete ga moći koristiti.',\n    'maint_recycle_bin_desc' => 'Izbrisane police, knjige, poglavlja i stranice poslane su u Recycle bin i mogu biti vraćene ili trajno izbrisane. Starije stavke bit će automatski izbrisane nakon nekog vremena što ovisi o konfiguraciji sustava.',\n    'maint_recycle_bin_open' => 'Otvori Recycle Bin',\n    'maint_regen_references' => 'Regeneriraj Reference',\n    'maint_regen_references_desc' => 'Ova akcija će ponovno izgraditi indeks prekriženih referenci između stavki unutar baze podataka. Obično se to automatski obavlja, ali ova akcija može biti korisna za indeksiranje starih sadržaja ili sadržaja dodanih putem neoficijelnih metoda.',\n    'maint_regen_references_success' => 'Indeks referenci je ponovno izgrađen!',\n    'maint_timeout_command_note' => 'Napomena: Ova radnja može potrajati, što može dovesti do problema s prekidom veze (timeout) u nekim web okruženjima. Kao alternativa, ova radnja se može izvršiti putem naredbe u terminalu.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Koš za smeće',\n    'recycle_bin_desc' => 'Ovdje možete vratiti izbrisane stavke ili ih trajno ukloniti iz sustava. Popis nije filtriran kao što su to popisi u kojima su omogućeni filteri.',\n    'recycle_bin_deleted_item' => 'Izbrisane stavke',\n    'recycle_bin_deleted_parent' => 'Nadređeni',\n    'recycle_bin_deleted_by' => 'Izbrisano od',\n    'recycle_bin_deleted_at' => 'Vrijeme brisanja',\n    'recycle_bin_permanently_delete' => 'Trajno izbrisano',\n    'recycle_bin_restore' => 'Vrati',\n    'recycle_bin_contents_empty' => 'Recycle Bin je prazan',\n    'recycle_bin_empty' => 'Isprazni Recycle Bin',\n    'recycle_bin_empty_confirm' => 'Ovo će trajno obrisati sve stavke u Recycle Bin i sadržaje povezane s njima. Jeste li sigurni da želite isprazniti Recycle Bin?',\n    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?',\n    'recycle_bin_destroy_list' => 'Stavke koje treba izbrisati',\n    'recycle_bin_restore_list' => 'Stavke koje treba vratiti',\n    'recycle_bin_restore_confirm' => 'Ova radnja vraća izbrisane stavke i njene podređene elemente na prvobitnu lokaciju. Ako je nadređena stavka izbrisana i nju treba vratiti.',\n    'recycle_bin_restore_deleted_parent' => 'S obzirom da je nadređena stavka obrisana najprije treba vratiti nju.',\n    'recycle_bin_restore_parent' => 'Vrati Nadređenog',\n    'recycle_bin_destroy_notification' => 'Ukupno izbrisane :count stavke iz Recycle Bin',\n    'recycle_bin_restore_notification' => 'Ukupno vraćene :count stavke iz Recycle Bin',\n\n    // Audit Log\n    'audit' => 'Dnevnik revizije',\n    'audit_desc' => 'Ovaj dnevnik revizije prikazuje popis aktivnosti zabilježene u sustavu. Ovaj popis nije definiran budući da nisu postavljeni filteri.',\n    'audit_event_filter' => 'Filter događaja',\n    'audit_event_filter_no_filter' => 'Bez filtera',\n    'audit_deleted_item' => 'Izbrisane stavke',\n    'audit_deleted_item_name' => 'Ime: :name',\n    'audit_table_user' => 'Korisnik',\n    'audit_table_event' => 'Događaj',\n    'audit_table_related' => 'Povezana stavka ili detalj',\n    'audit_table_ip' => 'IP Adresa',\n    'audit_table_date' => 'Datum aktivnosti',\n    'audit_date_from' => 'Rangiraj datum od',\n    'audit_date_to' => 'Rangiraj datum do',\n\n    // Role Settings\n    'roles' => 'Uloge',\n    'role_user_roles' => 'Uloge korisnika',\n    'roles_index_desc' => 'Uloge se koriste za grupiranje korisnika i dodjeljivanje sistemskih dozvola njihovim članovima. Kada je korisnik član više uloga, privilegije se zbrajaju i korisnik nasljeđuje sve sposobnosti svake uloge.',\n    'roles_x_users_assigned' => ':count korisnik dodijeljen|:count korisnika dodijeljeno',\n    'roles_x_permissions_provided' => ':count dozvola|:count dozvole',\n    'roles_assigned_users' => 'Odredi Korisnika',\n    'roles_permissions_provided' => 'Dostavljene Dozvole',\n    'role_create' => 'Stvori novu ulogu',\n    'role_delete' => 'Izbriši ulogu',\n    'role_delete_confirm' => 'Ovo će izbrisati ulogu povezanu s imenom \\':roleName\\'.',\n    'role_delete_users_assigned' => 'Ova uloga dodijeljena je :userCount. Ako želite premjestiti korisnike iz ove uloge odaberite novu ulogu u nastavku.',\n    'role_delete_no_migration' => \"Ne migriraj korisnike\",\n    'role_delete_sure' => 'Jeste li sigurni da želite obrisati ovu ulogu?',\n    'role_edit' => 'Uredi ulogu',\n    'role_details' => 'Detalji uloge',\n    'role_name' => 'Ime uloge',\n    'role_desc' => 'Kratki opis uloge',\n    'role_mfa_enforced' => 'Zahtijeva višestruku provjeru autentičnosti',\n    'role_external_auth_id' => 'Autorizacija',\n    'role_system' => 'Dopuštenja sustava',\n    'role_manage_users' => 'Upravljanje korisnicima',\n    'role_manage_roles' => 'Upravljanje ulogama i dopuštenjima',\n    'role_manage_entity_permissions' => 'Upravljanje dopuštenjima nad knjigama, poglavljima i stranicama',\n    'role_manage_own_entity_permissions' => 'Upravljanje dopuštenjima vlastitih knjiga, poglavlja i stranica',\n    'role_manage_page_templates' => 'Upravljanje predlošcima stranica',\n    'role_access_api' => 'API pristup',\n    'role_manage_settings' => 'Upravljanje postavkama aplikacija',\n    'role_export_content' => 'Izvoz sadržaja',\n    'role_import_content' => 'Import content',\n    'role_editor_change' => 'Promijeni uređivač stranica',\n    'role_notifications' => 'Primanje i upravljanje obavijestima',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Upravljanje vlasništvom',\n    'roles_system_warning' => 'Uzmite u obzir da pristup bilo kojem od ovih dopuštenja dozvoljavate korisniku upravljanje dopuštenjima ostalih u sustavu. Ova dopuštenja dodijelite pouzdanim korisnicima.',\n    'role_asset_desc' => 'Ova dopuštenja kontroliraju zadane pristupe. Dopuštenja za knjige, poglavlja i stranice ih poništavaju.',\n    'role_asset_admins' => 'Administratori automatski imaju pristup svim sadržajima, ali ove opcije mogu prikazati ili sakriti korisnička sučelja.',\n    'role_asset_image_view_note' => 'Ovo se odnosi na vidljivost unutar upravitelja slika. Stvarni pristup uploadiranim slikovnim datotekama ovisit će o odabranim opcijama pohrane slika u sustavu.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'Sve',\n    'role_own' => 'Vlastito',\n    'role_controlled_by_asset' => 'Kontrolirano od strane vlasnika',\n    'role_save' => 'Spremi ulogu',\n    'role_users' => 'Korisnici u ovoj ulozi',\n    'role_users_none' => 'Trenutno nijedan korisnik nije u ovoj ulozi',\n\n    // Users\n    'users' => 'Korisnici',\n    'users_index_desc' => 'Kreirajte i upravljajte pojedinačnim korisničkim računima unutar sustava. Korisnički računi koriste se za prijavu i pripisivanje sadržaja i aktivnosti. Dozvole pristupa temelje se uglavnom na ulogama, ali vlasništvo korisničkog sadržaja, među ostalim faktorima, također može utjecati na dozvole i pristup.',\n    'user_profile' => 'Profil korisnika',\n    'users_add_new' => 'Dodajte novog korisnika',\n    'users_search' => 'Pretražite korisnike',\n    'users_latest_activity' => 'Zadnje aktivnosti',\n    'users_details' => 'Detalji korisnika',\n    'users_details_desc' => 'Postavite prikaz imena i email adrese za ovog korisnika. Email adresa koristit će se za prijavu u aplikaciju.',\n    'users_details_desc_no_email' => 'Postavite prikaz imena ovog korisnika da ga drugi mogu prepoznati.',\n    'users_role' => 'Uloge korisnika',\n    'users_role_desc' => 'Odaberite koje će uloge biti dodijeljene ovom korisniku. Ako korisnik ima više uloga njihova će se dopuštenja prilagoditi.',\n    'users_password' => 'Lozinka korisnika',\n    'users_password_desc' => 'Postavite lozinku koja se koristi za prijavu u aplikaciju. Lozinka mora imati najmanje 8 znakova.',\n    'users_send_invite_text' => 'Možete odabrati slanje e maila korisniku i dozvoliti mu da postavi svoju lozinku ili vi to možete učiniti za njega.',\n    'users_send_invite_option' => 'Pošaljite pozivnicu korisniku putem emaila',\n    'users_external_auth_id' => 'Vanjska autorizacija',\n    'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.',\n    'users_password_warning' => 'Only fill the below if you would like to change the password for this user.',\n    'users_system_public' => 'Ovaj korisnik predstavlja bilo kojeg gosta. Dodjeljuje se automatski.',\n    'users_delete' => 'Izbrišite korisnika',\n    'users_delete_named' => 'Izbrišite korisnika :userName',\n    'users_delete_warning' => 'Ovo će ukloniti korisnika \\':userName\\' iz sustava.',\n    'users_delete_confirm' => 'Jeste li sigurni da želite izbrisati ovog korisnika?',\n    'users_migrate_ownership' => 'Premjestite vlasništvo',\n    'users_migrate_ownership_desc' => 'Ovdje odaberite korisnika kojem ćete dodijeliti vlasništvo i sve stavke povezane s njim.',\n    'users_none_selected' => 'Nije odabran nijedan korisnik',\n    'users_edit' => 'Uredite korisnika',\n    'users_edit_profile' => 'Uredite profil',\n    'users_avatar' => 'Korisnički avatar',\n    'users_avatar_desc' => 'Odaberite sliku koja će predstavljati korisnika. Maksimalno 256px.',\n    'users_preferred_language' => 'Prioritetni jezik',\n    'users_preferred_language_desc' => 'Ova će opcija promijeniti jezik korisničkog sučelja. Neće utjecati na sadržaj.',\n    'users_social_accounts' => 'Računi društvenih mreža',\n    'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.',\n    'users_social_accounts_info' => 'Ovdje možete povezati račun s onim na društvenim mrežama za bržu i lakšu prijavu. Ako se odspojite ovdje to neće utjecati na prethodnu autorizaciju. Na postavkama računa vaše društvene mreže možete opozvati pristup.',\n    'users_social_connect' => 'Poveži račun',\n    'users_social_disconnect' => 'Odvoji račun',\n    'users_social_status_connected' => 'Connected',\n    'users_social_status_disconnected' => 'Disconnected',\n    'users_social_connected' => ':socialAccount račun je uspješno dodan vašem profilu.',\n    'users_social_disconnected' => ':socialAccount račun je uspješno odvojen od vašeg profila.',\n    'users_api_tokens' => 'API tokeni',\n    'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.',\n    'users_api_tokens_none' => 'Nijedan API token nije stvoren za ovog korisnika',\n    'users_api_tokens_create' => 'Stvori token',\n    'users_api_tokens_expires' => 'Isteklo',\n    'users_api_tokens_docs' => 'API dokumentacija',\n    'users_mfa' => 'Višefaktorska Provjera Vjerodostojnosti',\n    'users_mfa_desc' => 'Postavite višestruku provjeru autentičnosti kao dodatni sloj sigurnosti za svoj korisnički račun.',\n    'users_mfa_x_methods' => ':count metoda konfigurirano|:count metode konfigurirane',\n    'users_mfa_configure' => 'Konfiguriraj Metode',\n\n    // API Tokens\n    'user_api_token_create' => 'Stvori API token',\n    'user_api_token_name' => 'Ime',\n    'user_api_token_name_desc' => 'Imenujte svoj token na način da prepoznate njegovu svrhu.',\n    'user_api_token_expiry' => 'Datum isteka',\n    'user_api_token_expiry_desc' => 'Postavite datum kada token istječe. Ostavljanjem ovog polja praznim automatski se postavlja dugoročno razdoblje.',\n    'user_api_token_create_secret_message' => 'Odmah nakon kreiranja tokena prikazat će se \"Token ID\" i \"Token Secret\". To će se prikazati samo jednom i zato preporučujemo da ga spremite na sigurno.',\n    'user_api_token' => 'API token',\n    'user_api_token_id' => 'ID Tokena',\n    'user_api_token_id_desc' => 'Ovaj sistemski generiran identifikator ne može se uređivati i bit će potreban pri API zahtjevima.',\n    'user_api_token_secret' => 'Token Tajna',\n    'user_api_token_secret_desc' => 'Ovaj sistemski generirani Token Secret trebat ćete za API zahtjev. Prikazuje se samo prvi put i zato ga spremite na sigurno.',\n    'user_api_token_created' => 'Token kreiran :timeAgo',\n    'user_api_token_updated' => 'Token ažuriran :timeAgo',\n    'user_api_token_delete' => 'Izbriši token',\n    'user_api_token_delete_warning' => 'Ovo će potpuno izbrisati API token naziva \\':tokenName\\' iz našeg sustava.',\n    'user_api_token_delete_confirm' => 'Jeste li sigurni da želite izbrisati ovaj API token?',\n\n    // Webhooks\n    'webhooks' => 'Web-dojavnici',\n    'webhooks_index_desc' => 'Web-dojavnici su način slanja podataka na vanjske URL-ove kada se određene radnje i događaji dogode unutar sustava, omogućujući integraciju temeljenu na događajima s vanjskim platformama poput sustava za slanje poruka ili obavijesti.',\n    'webhooks_x_trigger_events' => ':count događaj okidača|:count događaji okidača',\n    'webhooks_create' => 'Kreiraj Novi Web-dojavnik',\n    'webhooks_none_created' => 'Nijedan web-dojavnik nije još kreiran.',\n    'webhooks_edit' => 'Uredi Web-dojavnik',\n    'webhooks_save' => 'Spasi Web-dojavnik',\n    'webhooks_details' => 'Detalji Web-dojavnika',\n    'webhooks_details_desc' => 'Navedite korisnički prijateljsko ime i POST krajnju točku kao lokaciju na koju će se slati podaci web-dojavnika.',\n    'webhooks_events' => 'Događaji Web-dojavnika',\n    'webhooks_events_desc' => 'Odaberite sve događaje koji bi trebali pokrenuti poziv za ovaj web-dojavnik.',\n    'webhooks_events_warning' => 'Imajte na umu da će se ovi događaji pokrenuti za sve odabrane događaje, čak i ako se primjenjuju prilagođene dozvole. Osigurajte se da upotreba ovog web-dojavnika neće otkriti povjerljiv sadržaj.',\n    'webhooks_events_all' => 'Svi sistemski događaji',\n    'webhooks_name' => 'Naziv Web-dojavnika',\n    'webhooks_timeout' => 'Vremensko ograničenje zahtjeva web-dojavnika (u sek.)',\n    'webhooks_endpoint' => 'Odredište Web-dojavnika',\n    'webhooks_active' => 'Web-dojavnik Aktivan',\n    'webhook_events_table_header' => 'Događaji',\n    'webhooks_delete' => 'Izbriši Web-dojavnik',\n    'webhooks_delete_warning' => 'Ovo će u potpunosti izbrisati web-dojavnik pod nazivom \":webhookName\" iz sustava.',\n    'webhooks_delete_confirm' => 'Jeste li sigurni da želite obrisati ovaj Web-dojavnik?',\n    'webhooks_format_example' => 'Primjer formata web-dojavnika',\n    'webhooks_format_example_desc' => 'Podaci web-dojavnika se šalju kao POST zahtjev na konfiguriranu krajnju točku kao JSON prema sljedećem formatu. Svojstva \"related_item\" i \"url\" su opcionalna i ovise o vrsti pokrenutog događaja.',\n    'webhooks_status' => 'Status Web-dojavnika',\n    'webhooks_last_called' => 'Zadnji Poziv:',\n    'webhooks_last_errored' => 'Zadnja pogreška:',\n    'webhooks_last_error_message' => 'Posljednja poruka o pogrešci:',\n\n    // Licensing\n    'licenses' => 'Licenses',\n    'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.',\n    'licenses_bookstack' => 'BookStack License',\n    'licenses_php' => 'PHP Library Licenses',\n    'licenses_js' => 'JavaScript Library Licenses',\n    'licenses_other' => 'Other Licenses',\n    'license_details' => 'License Details',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/hr/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute mora biti prihvaćen.',\n    'active_url'           => ':attribute nema valjan URL.',\n    'after'                => ':attribute mora biti nakon :date.',\n    'alpha'                => ':attribute može sadržavati samo slova.',\n    'alpha_dash'           => ':attribute  može sadržavati samo slova, brojeve, crtice i donje crtice.',\n    'alpha_num'            => ':attribute može sadržavati samo slova i brojeve.',\n    'array'                => ':attribute mora biti niz.',\n    'backup_codes'         => 'Navedeni kod nije valjan ili je već korišten.',\n    'before'               => ':attribute mora biti prije :date.',\n    'between'              => [\n        'numeric' => ':attribute mora biti između :min i :max.',\n        'file'    => ':attribute mora biti između :min i :max kilobajta.',\n        'string'  => ':attribute mora biti između :min i :max znakova.',\n        'array'   => ':attribute mora biti između :min i :max stavki',\n    ],\n    'boolean'              => ':attribute mora biti točno ili netočno.',\n    'confirmed'            => ':attribute potvrde se ne podudaraju.',\n    'date'                 => ':attribute nema valjani datum.',\n    'date_format'          => ':attribute ne odgovara formatu :format.',\n    'different'            => ':attribute i :other se moraju razlikovati.',\n    'digits'               => ':attribute mora biti :digits znakova.',\n    'digits_between'       => ':attribute mora biti između :min i :max znamenki.',\n    'email'                => ':attribute mora biti valjana email adresa.',\n    'ends_with' => ':attribute mora završiti s :values',\n    'file'                 => 'Polje :attribute mora biti priloženo kao valjana datoteka.',\n    'filled'               => ':attribute polje je obavezno.',\n    'gt'                   => [\n        'numeric' => ':attribute mora biti veći od :value.',\n        'file'    => ':attribute mora biti veći od :value  kilobajta.',\n        'string'  => ':attribute mora biti veći od :value znakova',\n        'array'   => ':attribute mora biti veći od :value stavki.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute mora biti veći ili jednak :value.',\n        'file'    => ':attribute mora biti veći ili jednak :value kilobajta.',\n        'string'  => ':attribute mora biti veći ili jednak :value znakova.',\n        'array'   => ':attribute mora imati :value stavki ili više.',\n    ],\n    'exists'               => 'Odabrani :attribute ne vrijedi.',\n    'image'                => ':attribute mora biti slika.',\n    'image_extension'      => ':attribute mora imati valjanu i podržanu ekstenziju.',\n    'in'                   => 'Odabrani :attribute ne vrijedi.',\n    'integer'              => ':attribute mora biti cijeli broj.',\n    'ip'                   => ':attribute mora biti valjana IP adresa.',\n    'ipv4'                 => ':attribute mora biti valjana IPv4 adresa.',\n    'ipv6'                 => ':attribute mora biti valjana IPv6 adresa.',\n    'json'                 => ':attribute mora biti valjani JSON niz.',\n    'lt'                   => [\n        'numeric' => ':attribute mora biti manji od :value.',\n        'file'    => ':attribute mora biti manji od :value kilobajta.',\n        'string'  => ':attribute mora biti manji od :value znakova.',\n        'array'   => ':attribute mora biti manji od :value stavki.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute mora biti manji ili jednak :value.',\n        'file'    => ':attribute mora biti manji ili jednak :value kilobajta.',\n        'string'  => ':attribute mora biti manji ili jednak :value znakova.',\n        'array'   => ':attribute mora imati više od :value stavki.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute ne smije biti veći od :max.',\n        'file'    => ':attribute ne smije biti veći od :max kilobajta.',\n        'string'  => ':attribute ne smije biti duži od :max znakova.',\n        'array'   => ':attribute ne smije imati više od :max stavki.',\n    ],\n    'mimes'                => ':attribute mora biti datoteka tipa: :values.',\n    'min'                  => [\n        'numeric' => ':attribute mora biti najmanje :min.',\n        'file'    => ':attribute mora imati najmanje :min kilobajta.',\n        'string'  => ':attribute mora imati najmanje :min znakova.',\n        'array'   => ':attribute mora imati najmanje :min stavki.',\n    ],\n    'not_in'               => 'Odabrani :attribute ne vrijedi.',\n    'not_regex'            => 'Format :attribute nije valjan.',\n    'numeric'              => ':attribute mora biti broj.',\n    'regex'                => 'Format :attribute nije valjan.',\n    'required'             => ':attribute polje je obavezno.',\n    'required_if'          => 'Polje :attribute je obavezno kada :other je :value.',\n    'required_with'        => 'Polje :attribute je potrebno kada :values je sadašnjost.',\n    'required_with_all'    => 'Polje :attribute je potrebno kada :values je sadašnjost.',\n    'required_without'     => 'Polje :attribute je potrebno kada :values nije sadašnjost.',\n    'required_without_all' => 'Polje :attribute je potrebno kada ništa od :values nije sadašnjost.',\n    'same'                 => ':attribute i :other se moraju podudarati.',\n    'safe_url'             => 'Navedena veza možda nije sigurna.',\n    'size'                 => [\n        'numeric' => ':attribute mora biti :size.',\n        'file'    => ':attribute mora biti :size kilobajta.',\n        'string'  => ':attribute mora biti :size znakova.',\n        'array'   => ':attribute mora sadržavati :size stavki.',\n    ],\n    'string'               => ':attribute mora biti niz.',\n    'timezone'             => ':attribute mora biti valjan.',\n    'totp'                 => 'Navedeni kod nije valjan ili je istekao.',\n    'unique'               => ':attribute se već koristi.',\n    'url'                  => 'Format :attribute nije valjan.',\n    'uploaded'             => 'Datoteka se ne može prenijeti. Server možda ne prihvaća datoteke te veličine.',\n\n    'zip_file' => 'The :attribute needs to reference a file within the ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',\n    'zip_model_expected' => 'Data object expected but \":type\" found.',\n    'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Potrebna potvrda lozinke',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/hu/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'létrehozta az oldalt',\n    'page_create_notification'    => 'Oldal sikeresen létrehozva',\n    'page_update'                 => 'frissítette az oldalt',\n    'page_update_notification'    => 'Oldal sikeresen frissítve',\n    'page_delete'                 => 'törölte az oldalt',\n    'page_delete_notification'    => 'Oldal sikeresen törölve',\n    'page_restore'                => 'visszaállította az oldalt',\n    'page_restore_notification'   => 'Oldal sikeresen visszaállítva',\n    'page_move'                   => 'áthelyezte az oldalt',\n    'page_move_notification'      => 'Oldal sikeresen áthelyezve',\n\n    // Chapters\n    'chapter_create'              => 'létrehozta a fejezetet',\n    'chapter_create_notification' => 'Fejezet sikeresen létrehozva',\n    'chapter_update'              => 'frissítette a fejezetet',\n    'chapter_update_notification' => 'Fejezet sikeresen frissítve',\n    'chapter_delete'              => 'törölte a fejezetet',\n    'chapter_delete_notification' => 'Fejezet sikeresen törölve',\n    'chapter_move'                => 'áthelyezte a fejezetet',\n    'chapter_move_notification' => 'Fejezet sikeresen áthelyezve',\n\n    // Books\n    'book_create'                 => 'létrehozott egy könyvet',\n    'book_create_notification'    => 'Könyv sikeresen létrehozva',\n    'book_create_from_chapter'              => 'könyvvé alakította a fejezetet',\n    'book_create_from_chapter_notification' => 'Fejezet sikeresen könyvvé lett alakítva',\n    'book_update'                 => 'frissítette a könyvet',\n    'book_update_notification'    => 'Könyv sikeresen frissítve',\n    'book_delete'                 => 'törölte a könyvet',\n    'book_delete_notification'    => 'Könyv sikeresen törölve',\n    'book_sort'                   => 'átrendezte a könyvet',\n    'book_sort_notification'      => 'Könyv sikeresen újrarendezve',\n\n    // Bookshelves\n    'bookshelf_create'            => 'létrehozta a polcot',\n    'bookshelf_create_notification'    => 'Könyvespolc sikeresen létrehozva',\n    'bookshelf_create_from_book'    => 'könyvet polccá alakított',\n    'bookshelf_create_from_book_notification'    => 'Könyv sikeresen polccá lett alakítva',\n    'bookshelf_update'                 => 'polcot frissített',\n    'bookshelf_update_notification'    => 'Polc sikeresen frissítve',\n    'bookshelf_delete'                 => 'polcot törölt',\n    'bookshelf_delete_notification'    => 'Polc sikeresen törölve',\n\n    // Revisions\n    'revision_restore' => 'visszaállította a változatot',\n    'revision_delete' => 'törölte a változatot',\n    'revision_delete_notification' => 'Változat sikeresen törölve',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" hozzáadva a kedvencekhez',\n    'favourite_remove_notification' => '\":name\" törölve a kedvencek közül',\n\n    // Watching\n    'watch_update_level_notification' => 'A megfigyelési beállítások sikeresen frissültek',\n\n    // Auth\n    'auth_login' => 'bejelentkezve',\n    'auth_register' => 'új felhasználóként regisztrált',\n    'auth_password_reset_request' => 'jelszó visszaállítást kért',\n    'auth_password_reset_update' => 'felhasználói jelszó visszaállítás',\n    'mfa_setup_method' => 'MFA módszert állított be',\n    'mfa_setup_method_notification' => 'Többfaktoros azonosítás sikeresen beállítva',\n    'mfa_remove_method' => 'MFA módszert törölt',\n    'mfa_remove_method_notification' => 'Többfaktoros azonosítás sikeresen törölve',\n\n    // Settings\n    'settings_update' => 'frissítette a beállításokat',\n    'settings_update_notification' => 'Beállítások sikeresen frissítve',\n    'maintenance_action_run' => 'karbantartási műveletet futtatott',\n\n    // Webhooks\n    'webhook_create' => 'webhookot hozott létre',\n    'webhook_create_notification' => 'Webhook sikeresen létrehozva',\n    'webhook_update' => 'webhookot frissített',\n    'webhook_update_notification' => 'Webhook sikeresen frissítve',\n    'webhook_delete' => 'webhookot törölt',\n    'webhook_delete_notification' => 'Webhook sikeresen törölve',\n\n    // Imports\n    'import_create' => 'import elkészült',\n    'import_create_notification' => 'Az import sikeresen feltöltötve',\n    'import_run' => 'import frissítve',\n    'import_run_notification' => 'A tartalmat sikeresen importáltam.',\n    'import_delete' => 'import törölve',\n    'import_delete_notification' => 'Az import sikeresen törölve',\n\n    // Users\n    'user_create' => 'létrehozta a felhasználót',\n    'user_create_notification' => 'Felhasználó sikeresen létrehozva',\n    'user_update' => 'frissítette a felhasználót',\n    'user_update_notification' => 'Felhasználó sikeresen frissítve',\n    'user_delete' => 'felhasználót törölt',\n    'user_delete_notification' => 'Felhasználó sikeresen eltávolítva',\n\n    // API Tokens\n    'api_token_create' => 'létrehozta az API tokent',\n    'api_token_create_notification' => 'API token sikeresen létrehozva',\n    'api_token_update' => 'frissítette az API tokent',\n    'api_token_update_notification' => 'API token sikeresen frissítve',\n    'api_token_delete' => 'törölte az API tokent',\n    'api_token_delete_notification' => 'API token sikeresen törölve',\n\n    // Roles\n    'role_create' => 'szerepkört hozott létre',\n    'role_create_notification' => 'Szerepkör sikeresen létrehozva',\n    'role_update' => 'frissítette a szerepkört',\n    'role_update_notification' => 'Szerepkör sikeresen frissítve',\n    'role_delete' => 'törölte a szerepkört',\n    'role_delete_notification' => 'Szerepkör sikeresen törölve',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'kiürítette a lomtárat',\n    'recycle_bin_restore' => 'lomtárból visszaállítva',\n    'recycle_bin_destroy' => 'lomtárból törölve',\n\n    // Comments\n    'commented_on'                => 'megjegyzést fűzött hozzá:',\n    'comment_create'              => 'hozzáadott hozzászólás',\n    'comment_update'              => 'frissített hozzászólás',\n    'comment_delete'              => 'megjegyzés törlése',\n\n    // Sort Rules\n    'sort_rule_create' => 'létrehozta a rendezési szabályt',\n    'sort_rule_create_notification' => 'Rendezési szabály sikeresen létrehozva',\n    'sort_rule_update' => 'frissítette a rendezési szabályt',\n    'sort_rule_update_notification' => 'Rendezési szabály sikeresen frissítve',\n    'sort_rule_delete' => 'törölte a rendezési szabályt',\n    'sort_rule_delete_notification' => 'Rendezési szabály sikeresen törölve',\n\n    // Other\n    'permissions_update'          => 'engedélyek frissítve',\n];\n"
  },
  {
    "path": "lang/hu/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Ezek a hitelesítő adatok nem egyeznek a rögzítettekkel.',\n    'throttle' => 'Túl sok bejelentkezési próbálkozás. :seconds múlva lehet újra megpróbálni.',\n\n    // Login & Register\n    'sign_up' => 'Regisztráció',\n    'log_in' => 'Bejelentkezés',\n    'log_in_with' => 'Bejelentkezés ezzel: :socialDriver',\n    'sign_up_with' => 'Regisztráció ezzel: :socialDriver',\n    'logout' => 'Kijelentkezés',\n\n    'name' => 'Név',\n    'username' => 'Felhasználónév',\n    'email' => 'Email',\n    'password' => 'Jelszó',\n    'password_confirm' => 'Jelszó megerősítése',\n    'password_hint' => 'Legalább 8 karakter hosszú legyen',\n    'forgot_password' => 'Elfelejtett jelszó?',\n    'remember_me' => 'Emlékezzen rám',\n    'ldap_email_hint' => 'A fiókhoz használt email cím megadása.',\n    'create_account' => 'Fiók létrehozása',\n    'already_have_account' => 'Rendelkezik már felhasználói fiókkal?',\n    'dont_have_account' => 'Még nincs felhasználói fiókja?',\n    'social_login' => 'Közösségi bejelentkezés',\n    'social_registration' => 'Közösségi regisztráció',\n    'social_registration_text' => 'Regisztráció és bejelentkezés másik szolgáltatással.',\n\n    'register_thanks' => 'Köszönjük a regisztrációt!',\n    'register_confirm' => 'Ellenőrizze a megadott e-mail címet, és kattintson a megerősítő gombra :appName eléréséhez.',\n    'registrations_disabled' => 'A regisztráció jelenleg le van tiltva',\n    'registration_email_domain_invalid' => 'Ebből az email tartományról nem lehet hozzáférni ehhez az alkalmazáshoz',\n    'register_success' => 'Köszönjük a regisztrációt! A regisztráció és a bejelentkezés megtörtént.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Bejelentkezési kísérlet',\n    'auto_init_starting_desc' => 'Kapcsolatba lépünk az azonosítási rendszereddel, hogy elkezdjük a bejelentkezési folyamatot. Ha 5 másodperc után sem történik előrelépés, próbálkozhatsz az alábbi linkre kattintva.',\n    'auto_init_start_link' => 'Folytatás azonosítással',\n\n    // Password Reset\n    'reset_password' => 'Jelszó visszaállítása',\n    'reset_password_send_instructions' => 'Adja meg az e-mail címet, amire a jelszó-visszaállító linket küldjük.',\n    'reset_password_send_button' => 'Visszaállító hivatkozás elküldése',\n    'reset_password_sent' => 'A jelszó-visszaállító linket e-mailben fogjuk elküldeni a(z) :email címre, ha beállításra került a rendszerben.',\n    'reset_password_success' => 'A jelszó sikeresen visszaállítva.',\n    'email_reset_subject' => ':appName jelszó visszaállítása',\n    'email_reset_text' => 'Ezt az e-mailt azért küldtük, mert egy jelszó-visszaállításra vonatkozó kérést kaptunk ebből a fiókból.',\n    'email_reset_not_requested' => 'Ha nem Ön kérte a jelszó visszaállítását, akkor nincs szükség további intézkedésre.',\n\n    // Email Confirmation\n    'email_confirm_subject' => ':appName alkalmazásban beállított email címet meg kell erősíteni',\n    'email_confirm_greeting' => ':appName köszöni a csatlakozást!',\n    'email_confirm_text' => 'Az email címet a lenti gombra kattintva lehet megerősíteni:',\n    'email_confirm_action' => 'Email megerősítése',\n    'email_confirm_send_error' => 'Az e-mail megerősítés kötelező, de a rendszer nem tudta elküldeni az e-mailt. Keresse fel az adminisztrátort, és gondoskodjon róla, hogy az e-mail helyesen van beállítva.',\n    'email_confirm_success' => 'Az Ön e-mail címe sikeresen meg lett erősítve, most már be tud jelentkezni az e-mail címe használatával.',\n    'email_confirm_resent' => 'Megerősítő e-mail újraküldve. Ellenőrizze a bejövő üzeneteit!',\n    'email_confirm_thanks' => 'Köszönjük a megerősítést!',\n    'email_confirm_thanks_desc' => 'Kérjük, várjon egy pillanatot, amíg a megerősítést kezeljük. Ha nem kerül átirányításra 3 másodperc után, kattintson a lenti \"Folytatás\" linkre a továbbhaladáshoz.',\n\n    'email_not_confirmed' => 'Az email cím nincs megerősítve',\n    'email_not_confirmed_text' => 'Az email cím még nincs megerősítve.',\n    'email_not_confirmed_click_link' => 'Kattintson a regisztráció után nem sokkal elküldött e-mailben található hivatkozásra.',\n    'email_not_confirmed_resend' => 'Ha nem érkezik meg a megerősítő email, a lenti űrlap beküldésével újra lehet küldeni.',\n    'email_not_confirmed_resend_button' => 'Megerősítő email újraküldése',\n\n    // User Invite\n    'user_invite_email_subject' => 'Ez egy meghívó :appName weboldalhoz!',\n    'user_invite_email_greeting' => 'Létre lett hozva egy fiók az :appName weboldalon.',\n    'user_invite_email_text' => 'Jelszó beállításához és hozzáféréshez a lenti gombra kell kattintani:',\n    'user_invite_email_action' => 'Fiók jelszó beállítása',\n    'user_invite_page_welcome' => ':appName üdvözöl!',\n    'user_invite_page_text' => 'A fiók véglegesítéséhez és a hozzáféréshez be kell állítani egy jelszót ami :appName weboldalon lesz használva a bejelentkezéshez.',\n    'user_invite_page_confirm_button' => 'Jelszó megerősítése',\n    'user_invite_success_login' => 'Jelszó beállítva. Most már be tudsz jelentkezni a beállított jelszóval a következő rendszerbe: :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Többlépcsős azonosítás beállítása',\n    'mfa_setup_desc' => 'Állítsa be a többlépcsős azonosítást egy extra biztonsági rétegként a felhasználói fiókjához.',\n    'mfa_setup_configured' => 'Már beállítva',\n    'mfa_setup_reconfigure' => 'Újrakonfigurálás',\n    'mfa_setup_remove_confirmation' => 'Biztosan ki szeretné kapcsolni a többlépcsős azonosítást?',\n    'mfa_setup_action' => 'Beállítások',\n    'mfa_backup_codes_usage_limit_warning' => 'Kevesebb, mint 5 visszaállítási kódja maradt. Kérem, hogy generáljon új kódokat, hogy csökkentse a rendszerből való kizárásának esélyét.',\n    'mfa_option_totp_title' => 'Mobilalkalmazás',\n    'mfa_option_totp_desc' => 'A többlépcsős azonosításhoz olyan mobilalkalmazásra lesz szükséged, amely támogatja a TOTP-t, például a Google Authenticator, az Authy vagy a Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Visszaállítási kulcsok',\n    'mfa_option_backup_codes_desc' => 'Egyszer használatos biztonsági kódokat hoz létre, amelyeket bejelentkezéskor kell megadnia személyazonosságának igazolására. Ügyeljen arra, hogy ezeket biztonságos helyen tárolja.',\n    'mfa_gen_confirm_and_enable' => 'Jóváhagyás és engedélyezés',\n    'mfa_gen_backup_codes_title' => 'Visszaállítási kódok beállítása',\n    'mfa_gen_backup_codes_desc' => 'Tárolja el egy biztonságos helyen az alábbi kódokat. Bejelentkezés során fel tudja használni őket másodlagos bejelentkezési kódként.',\n    'mfa_gen_backup_codes_download' => 'Kódok letöltése',\n    'mfa_gen_backup_codes_usage_warning' => 'A kódok egyszerhasználatosak',\n    'mfa_gen_totp_title' => 'Mobilalkalmazás beállítása',\n    'mfa_gen_totp_desc' => 'A többlépcsős azonosításhoz olyan mobilalkalmazásra lesz szükséged, amely támogatja a TOTP-t, például a Google Authenticator, az Authy vagy a Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Szkenneld be az alábbi QR-kódot az általad használt azonosító alkalmazásoddal, hogy használhasd az alkalmazást.',\n    'mfa_gen_totp_verify_setup' => 'Beállítások ellenőrzése',\n    'mfa_gen_totp_verify_setup_desc' => 'Ellenőrizd, hogy minden működik, azzal hogy beírod a kapott kódot amit az authentikátor alkalmazás generált az alábbi beviteli mezőbe:',\n    'mfa_gen_totp_provide_code_here' => 'Add meg az alkalmazás által generált kódot ide',\n    'mfa_verify_access' => 'Hozzáférés ellenőrzése',\n    'mfa_verify_access_desc' => 'Felhasználói fiókja megköveteli, hogy erősítse meg személyazonosságát egy további ellenőrzési szinttel, mielőtt hozzáférést kapna. A folytatáshoz használja az egyik konfigurált módszert.',\n    'mfa_verify_no_methods' => 'Nincs konfigurálva MFA',\n    'mfa_verify_no_methods_desc' => 'Nem található többlépcsős hitelesítési módszer a fiókjához. A hozzáféréshez legalább egy módszert be kell állítania.',\n    'mfa_verify_use_totp' => 'Ellenőrzés mobil alkalmazás használatával',\n    'mfa_verify_use_backup_codes' => 'Ellenőrzés visszaállítási kóddal',\n    'mfa_verify_backup_code' => 'Visszaállítási kód',\n    'mfa_verify_backup_code_desc' => 'Adjon meg egy még fel nem használt visszaállítási kódot:',\n    'mfa_verify_backup_code_enter_here' => 'Írd be a tartalék kódot',\n    'mfa_verify_totp_desc' => 'Írja be alább a mobilalkalmazásával generált kódot:',\n    'mfa_setup_login_notification' => 'Többfaktoros hitelesítés konfigurálva. Kérjük, most jelentkezzen be újra a konfigurált módszerrel.',\n];\n"
  },
  {
    "path": "lang/hu/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Mégsem',\n    'close' => 'Bezárás',\n    'confirm' => 'Megerősítés',\n    'back' => 'Vissza',\n    'save' => 'Mentés',\n    'continue' => 'Tovább',\n    'select' => 'Kiválasztás',\n    'toggle_all' => 'Összes átkapcsolása',\n    'more' => 'Több',\n\n    // Form Labels\n    'name' => 'Név',\n    'description' => 'Leírás',\n    'role' => 'Szerepkör',\n    'cover_image' => 'Borítókép',\n    'cover_image_description' => 'Ennek a képnek körülbelül 440 x 250 képpont méretűnek kell lennie, bár rugalmasan méretezhető és levágható, hogy a felhasználói felülethez illeszkedjen a különböző lehetőségek esetén, így a megjelenítés tényleges méretei eltérőek lesznek.',\n\n    // Actions\n    'actions' => 'Műveletek',\n    'view' => 'Megtekintés',\n    'view_all' => 'Összes megtekintése',\n    'new' => 'Új',\n    'create' => 'Létrehozás',\n    'update' => 'Frissítés',\n    'edit' => 'Szerkesztés',\n    'archive' => 'Archiválás',\n    'unarchive' => 'Archiválás visszavonása',\n    'sort' => 'Rendezés',\n    'move' => 'Áthelyezés',\n    'copy' => 'Másolás',\n    'reply' => 'Válasz',\n    'delete' => 'Törlés',\n    'delete_confirm' => 'Törlés megerősítése',\n    'search' => 'Keresés',\n    'search_clear' => 'Keresés törlése',\n    'reset' => 'Visszaállítás',\n    'remove' => 'Eltávolítás',\n    'add' => 'Hozzáadás',\n    'configure' => 'Beállítás',\n    'manage' => 'Kezelés',\n    'fullscreen' => 'Teljes képernyő',\n    'favourite' => 'Kedvencekhez ad',\n    'unfavourite' => 'Kedvencekből eltávolít',\n    'next' => 'Következő',\n    'previous' => 'Előző',\n    'filter_active' => 'Aktív szűrő:',\n    'filter_clear' => 'Szűrő törlése',\n    'download' => 'Letöltés',\n    'open_in_tab' => 'Megnyitás új tab-on',\n    'open' => 'Megnyitás',\n\n    // Sort Options\n    'sort_options' => 'Rendezési beállítások',\n    'sort_direction_toggle' => 'Rendezési irány váltása',\n    'sort_ascending' => 'Növekvő sorrend',\n    'sort_descending' => 'Csökkenő sorrend',\n    'sort_name' => 'Név',\n    'sort_default' => 'Alapértelmezett',\n    'sort_created_at' => 'Létrehozás dátuma',\n    'sort_updated_at' => 'Frissítés dátuma',\n\n    // Misc\n    'deleted_user' => 'Törölt felhasználó',\n    'no_activity' => 'Nincs megjeleníthető aktivitás',\n    'no_items' => 'Nincsenek elérhető elemek',\n    'back_to_top' => 'Oldal eleje',\n    'skip_to_main_content' => 'Ugrás a fő tartalomra',\n    'toggle_details' => 'Részletek átkapcsolása',\n    'toggle_thumbnails' => 'Bélyegképek átkapcsolása',\n    'details' => 'Részletek',\n    'grid_view' => 'Rács nézet',\n    'list_view' => 'Lista nézet',\n    'default' => 'Alapértelmezés szerinti',\n    'breadcrumb' => 'Morzsa',\n    'status' => 'Státusz',\n    'status_active' => 'Aktív',\n    'status_inactive' => 'Inaktív',\n    'never' => 'Soha',\n    'none' => 'Egyik sem',\n\n    // Header\n    'homepage' => 'Kezdőlap',\n    'header_menu_expand' => 'Menü megnyitása',\n    'profile_menu' => 'Profil menü',\n    'view_profile' => 'Profil megtekintése',\n    'edit_profile' => 'Profil szerkesztése',\n    'dark_mode' => 'Sötét mód',\n    'light_mode' => 'Világos mód',\n    'global_search' => 'Keresés mindenhol',\n\n    // Layout tabs\n    'tab_info' => 'Információ',\n    'tab_info_label' => 'Tab: Másodlagos információk megjelenítése',\n    'tab_content' => 'Tartalom',\n    'tab_content_label' => 'Tab: Elsődleges információk megjelenítése',\n\n    // Email Content\n    'email_action_help' => 'Probléma esetén a lenti \":actionText\" gombra kell kattintani, majd ki kell másolni a lenti webcímet és be kell illeszteni egy böngészőbe:',\n    'email_rights' => 'Minden jog fenntartva',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Adatvédelmi irányelvek',\n    'terms_of_service' => 'Felhasználási feltételek',\n\n    // OpenSearch\n    'opensearch_description' => 'Keresés :appName',\n];\n"
  },
  {
    "path": "lang/hu/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Kép kiválasztása',\n    'image_list' => 'Képek listája',\n    'image_details' => 'A kép részletei',\n    'image_upload' => 'Kép feltöltése',\n    'image_intro' => 'Itt választhatsz ki és kezelhetsz olyan képeket, amelyeket korábban feltöltöttek a rendszerbe.',\n    'image_intro_upload' => 'Húzz ide egy új képfájlt az új kép feltöltéséhez, vagy használd a fenti \"Kép feltöltése\" gombot.',\n    'image_all' => 'Összes',\n    'image_all_title' => 'Összes kép megtekintése',\n    'image_book_title' => 'A könyvhöz feltöltött képek megtekintése',\n    'image_page_title' => 'Az oldalra feltöltött képek megtekintése',\n    'image_search_hint' => 'Keresés kép neve alapján',\n    'image_uploaded' => 'Feltöltve ekkor: :uploadedDate',\n    'image_uploaded_by' => 'Feltöltötte :userName',\n    'image_uploaded_to' => 'Feltöltve ide: :pageLink',\n    'image_updated' => 'Frissítve ekkor: :updateDate',\n    'image_load_more' => 'Több betöltése',\n    'image_image_name' => 'Kép neve',\n    'image_delete_used' => 'Ez a kép a lenti oldalakon van használatban.',\n    'image_delete_confirm_text' => 'Biztosan törölhető ez a kép?',\n    'image_select_image' => 'Kép kiválasztása',\n    'image_dropzone' => 'Képek feltöltése ejtéssel vagy kattintással',\n    'image_dropzone_drop' => 'Húzza a képeket ide a feltöltéshez',\n    'images_deleted' => 'Képek törölve',\n    'image_preview' => 'Kép előnézete',\n    'image_upload_success' => 'Kép sikeresen feltöltve',\n    'image_update_success' => 'Kép részletei sikeresen frissítve',\n    'image_delete_success' => 'Kép sikeresen törölve',\n    'image_replace' => 'Kép cseréje',\n    'image_replace_success' => 'Képfájl sikeresen frissítve',\n    'image_rebuild_thumbs' => 'Méret variációk újragenerálása',\n    'image_rebuild_thumbs_success' => 'Kép méret változatok sikeresen újra lettek generálva!',\n\n    // Code Editor\n    'code_editor' => 'Kód szerkesztése',\n    'code_language' => 'Kód nyelve',\n    'code_content' => 'Kód tartalom',\n    'code_session_history' => 'Munkamenet előzményei',\n    'code_save' => 'Kód mentése',\n];\n"
  },
  {
    "path": "lang/hu/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Általános',\n    'advanced' => 'Haladó',\n    'none' => 'Egyik sem',\n    'cancel' => 'Mégsem',\n    'save' => 'Mentés',\n    'close' => 'Bezárás',\n    'apply' => 'Apply',\n    'undo' => 'Visszavonás',\n    'redo' => 'Újra',\n    'left' => 'Balra',\n    'center' => 'Középre',\n    'right' => 'Jobbra',\n    'top' => 'Fel',\n    'middle' => 'Középre',\n    'bottom' => 'Alulra',\n    'width' => 'Szélesség',\n    'height' => 'Magasság',\n    'More' => 'Több',\n    'select' => 'Kiválasztás...',\n\n    // Toolbar\n    'formats' => 'Formátumok',\n    'header_large' => 'Nagy címsor',\n    'header_medium' => 'Közepes címsor',\n    'header_small' => 'Kis címsor',\n    'header_tiny' => 'Apró címsor',\n    'paragraph' => 'Bekezdés',\n    'blockquote' => 'Idézet',\n    'inline_code' => 'Forráskód',\n    'callouts' => 'Szövegdobozok',\n    'callout_information' => 'Információ',\n    'callout_success' => 'Siker',\n    'callout_warning' => 'Figyelmeztetés',\n    'callout_danger' => 'Veszély',\n    'bold' => 'Félkövér',\n    'italic' => 'Dőlt',\n    'underline' => 'Aláhúzott',\n    'strikethrough' => 'Áthúzott szöveg',\n    'superscript' => 'Felső index',\n    'subscript' => 'Alsó index',\n    'text_color' => 'Szöveg szín',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Egyéni szín',\n    'remove_color' => 'Szín eltávolítása',\n    'background_color' => 'Háttérszín',\n    'align_left' => 'Balra igazítás',\n    'align_center' => 'Középre igazítás',\n    'align_right' => 'Jobbra igazítás',\n    'align_justify' => 'Sorkizárt',\n    'list_bullet' => 'Felsorolásjeles lista',\n    'list_numbered' => 'Számozott lista',\n    'list_task' => 'Feladatlista',\n    'indent_increase' => 'Behúzás növelése',\n    'indent_decrease' => 'Behúzás csökkentése',\n    'table' => 'Táblázat',\n    'insert_image' => 'Kép beszúrása',\n    'insert_image_title' => 'Kép beszúrása/szerkesztése',\n    'insert_link' => 'Hivatkozás beszúrása/szerkesztése',\n    'insert_link_title' => 'Hivatkozás Beszúrása/Szerkesztése',\n    'insert_horizontal_line' => 'Vízszintes vonal beszúrása',\n    'insert_code_block' => 'Kódrészlet beszúrása',\n    'edit_code_block' => 'Kódrészlet beszúrása',\n    'insert_drawing' => 'Rajz beszúrása/szerkesztése',\n    'drawing_manager' => 'Rajzkezelő',\n    'insert_media' => 'Media beszúrása/szerkesztése',\n    'insert_media_title' => 'Media Beszúrása/Szerkesztése',\n    'clear_formatting' => 'Formázás törlése',\n    'source_code' => 'Forráskód',\n    'source_code_title' => 'Forráskód',\n    'fullscreen' => 'Teljes képernyő',\n    'image_options' => 'Képbeállítások',\n\n    // Tables\n    'table_properties' => 'Táblázat tulajdonságai',\n    'table_properties_title' => 'Táblázat Tulajdonságai',\n    'delete_table' => 'Táblázat törlése',\n    'table_clear_formatting' => 'Tábla formázás törlése',\n    'resize_to_contents' => 'Átméretezés a tartalomhoz',\n    'row_header' => 'Sorfejléc',\n    'insert_row_before' => 'Sor beszúrása elé',\n    'insert_row_after' => 'Sor beszúrása mögé',\n    'delete_row' => 'Sor törlése',\n    'insert_column_before' => 'Oszlop beszúrása elé',\n    'insert_column_after' => 'Oszlop beszúrása utána',\n    'delete_column' => 'Oszlop törlése',\n    'table_cell' => 'Cella',\n    'table_row' => 'Sor',\n    'table_column' => 'Oszlop',\n    'cell_properties' => 'Cella tulajdonságai',\n    'cell_properties_title' => 'Cella Tulajdonságai',\n    'cell_type' => 'Cella típusa',\n    'cell_type_cell' => 'Cella',\n    'cell_scope' => 'Hatáskör',\n    'cell_type_header' => 'Címsor cella',\n    'merge_cells' => 'Cellák egyesítése',\n    'split_cell' => 'Cellák szétválasztása',\n    'table_row_group' => 'Sorcsoport',\n    'table_column_group' => 'Oszlopcsoport',\n    'horizontal_align' => 'Vízszintes elrendezés',\n    'vertical_align' => 'Függőleges elrendezés',\n    'border_width' => 'Szegély szélessége',\n    'border_style' => 'Szegély stílusa',\n    'border_color' => 'Szegély színe',\n    'row_properties' => 'Sor tulajdonságai',\n    'row_properties_title' => 'Sor Tulajdonságai',\n    'cut_row' => 'Sor kivágása',\n    'copy_row' => 'Sor másolása',\n    'paste_row_before' => 'Sor beillesztése elé',\n    'paste_row_after' => 'Sor beillesztése mögé',\n    'row_type' => 'Sor típusa',\n    'row_type_header' => 'Címsor',\n    'row_type_body' => 'Törzs',\n    'row_type_footer' => 'Lábléc',\n    'alignment' => 'Igazítás',\n    'cut_column' => 'Oszlop kivágása',\n    'copy_column' => 'Oszlop másolása',\n    'paste_column_before' => 'Oszlop beszúrása elé',\n    'paste_column_after' => 'Oszlop beszúrása utána',\n    'cell_padding' => 'Cellatávolság',\n    'cell_spacing' => 'Cellatávolság',\n    'caption' => 'Felirat',\n    'show_caption' => 'Képaláírás mutatása',\n    'constrain' => 'Arányok megőrzése',\n    'cell_border_solid' => 'Folyamatos',\n    'cell_border_dotted' => 'Pontozott',\n    'cell_border_dashed' => 'Szaggatott',\n    'cell_border_double' => 'Dupla',\n    'cell_border_groove' => 'Horony',\n    'cell_border_ridge' => 'Domború',\n    'cell_border_inset' => 'Behúzott',\n    'cell_border_outset' => 'Kiemelés',\n    'cell_border_none' => 'Egyik sem',\n    'cell_border_hidden' => 'Rejtett',\n\n    // Images, links, details/summary & embed\n    'source' => 'Forrás',\n    'alt_desc' => 'Alternatív leírás',\n    'embed' => 'Beágyazás',\n    'paste_embed' => 'Illeszd be a beágyazási kódot ide:',\n    'url' => 'URL',\n    'text_to_display' => 'Megjelenő szöveg',\n    'title' => 'Cím',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Hivatkozás megnyitása',\n    'open_link_in' => 'Hivatkozás megnyitása...',\n    'open_link_current' => 'Aktuális ablak',\n    'open_link_new' => 'Új ablak',\n    'remove_link' => 'Hivatkozás eltávolítása',\n    'insert_collapsible' => 'Illeszd be az összecsukható blokkot',\n    'collapsible_unwrap' => 'Kicsomagol',\n    'edit_label' => 'Címke szerkesztése',\n    'toggle_open_closed' => 'Nyitott/zárt váltása',\n    'collapsible_edit' => 'Összecsukható blokk szerkesztése',\n    'toggle_label' => 'Címke ki-be kapcsolása',\n\n    // About view\n    'about' => 'A szerkesztőről',\n    'about_title' => 'A WYSIWYG szerkesztőről',\n    'editor_license' => 'Szerkesztő Licensz és Copyright információi',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'Ez a szerkesztő az MIT licenc alatt szolgáltatott :tinyLink segítségével készült.',\n    'editor_tiny_license_link' => 'A TinyMCE szerzői jogi és licencinformációi itt találhatók.',\n    'save_continue' => 'Mentés és Folytatás',\n    'callouts_cycle' => '(Folyamatos lenyomva tartással válassza ki a típusok közötti váltást)',\n    'link_selector' => 'Tartalom hivatkozása',\n    'shortcuts' => 'Gyorsbillentyűk',\n    'shortcut' => 'Gyorsbillentyű',\n    'shortcuts_intro' => 'Az alábbi gyorsbillentyűk érhetők el a szerkesztőben:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Leírás',\n];\n"
  },
  {
    "path": "lang/hu/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Legutóbb létrehozott',\n    'recently_created_pages' => 'Legutóbb létrehozott oldalak',\n    'recently_updated_pages' => 'Legutóbb frissített oldalak',\n    'recently_created_chapters' => 'Legutóbb létrehozott fejezetek',\n    'recently_created_books' => 'Legutóbb létrehozott könyvek',\n    'recently_created_shelves' => 'Legutóbb létrehozott polcok',\n    'recently_update' => 'Legutóbb frissített',\n    'recently_viewed' => 'Legutóbb megtekintett',\n    'recent_activity' => 'Legutóbbi tevékenység',\n    'create_now' => 'Létrehozás most',\n    'revisions' => 'Változatok',\n    'meta_revision' => 'Változat #:revisionCount',\n    'meta_created' => 'Létrehozva :timeLength',\n    'meta_created_name' => ':user hozta létre :timeLength',\n    'meta_updated' => 'Frissítve :timeLength',\n    'meta_updated_name' => ':user frissítette :timeLength',\n    'meta_owned_name' => ':user tulajdona',\n    'meta_reference_count' => 'Hivatkozva a következő által: :count |Hivatkozva a következő által: :count',\n    'entity_select' => 'Entitás kiválasztása',\n    'entity_select_lack_permission' => 'Nincs meg a szükséges jogosultságod ennek az elemnek a kiválasztásához',\n    'images' => 'Képek',\n    'my_recent_drafts' => 'Legutóbbi vázlataim',\n    'my_recently_viewed' => 'Általam legutóbb megtekintett',\n    'my_most_viewed_favourites' => 'Legtöbbet Megtekintett Kedvencek',\n    'my_favourites' => 'Kedvencek',\n    'no_pages_viewed' => 'Még nincsenek általam megtekintett oldalak',\n    'no_pages_recently_created' => 'Nincsenek legutóbb létrehozott oldalak',\n    'no_pages_recently_updated' => 'Nincsenek legutóbb frissített oldalak',\n    'export' => 'Exportálás',\n    'export_html' => 'Önálló weblap',\n    'export_pdf' => 'PDF fájl',\n    'export_text' => 'Egyszerű szövegfájl',\n    'export_md' => 'Markdown jegyzetek',\n    'export_zip' => 'Hordozható ZIP',\n    'default_template' => 'Alapértelmezett oldalsablon',\n    'default_template_explain' => 'Rendeljen hozzá egy oldalsablont, amely alapértelmezett tartalomként lesz használva az ezen az elemen belül létrehozott összes oldalon. Ne feledje, hogy ezt csak akkor használja, ha az oldal készítője megtekintési hozzáféréssel rendelkezik a kiválasztott sablonoldalhoz.',\n    'default_template_select' => 'Válasszon ki egy oldalsablont',\n    'import' => 'Import',\n    'import_validate' => 'Validate Import',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'Select ZIP file to upload',\n    'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',\n    'import_pending' => 'Pending Imports',\n    'import_pending_none' => 'No imports have been started.',\n    'import_continue' => 'Continue Import',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Run Import',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Uploaded by',\n    'import_location' => 'Import Location',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'Import Errors',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Jogosultságok',\n    'permissions_desc' => 'Itt állítsa be az engedélyeket a felhasználói szerepkörök által biztosított alapértelmezett engedélyek felülbírálásához.',\n    'permissions_book_cascade' => 'A könyvekre beállított engedélyek automatikusan az alárendelt fejezetekhez és oldalakhoz kapcsolódnak, kivéve, ha saját engedélyekkel rendelkeznek.',\n    'permissions_chapter_cascade' => 'A fejezetekre beállított engedélyek automatikusan az alárendelt oldalakra lépnek át, hacsak nem rendelkeznek saját engedélyekkel.',\n    'permissions_save' => 'Jogosultságok mentése',\n    'permissions_owner' => 'Tulajdonos',\n    'permissions_role_everyone_else' => 'Mindenki más',\n    'permissions_role_everyone_else_desc' => 'Állítson be engedélyeket az összes, kifejezetten nem felülírt szerepkörhöz.',\n    'permissions_role_override' => 'A szerepkör engedélyeinek felülbírálása',\n    'permissions_inherit_defaults' => 'Alapértelmezett értékek öröklése',\n\n    // Search\n    'search_results' => 'Keresési eredmények',\n    'search_total_results_found' => ':count találat|összesen :count találat',\n    'search_clear' => 'Keresés törlése',\n    'search_no_pages' => 'Nincsenek a keresésnek megfelelő oldalak',\n    'search_for_term' => ':term keresése',\n    'search_more' => 'További eredmények',\n    'search_advanced' => 'Részletes keresés',\n    'search_terms' => 'Keresési kifejezések',\n    'search_content_type' => 'Tartalomtípus',\n    'search_exact_matches' => 'Pontos egyezések',\n    'search_tags' => 'Címke keresések',\n    'search_options' => 'Beállítások',\n    'search_viewed_by_me' => 'Általam megtekintett',\n    'search_not_viewed_by_me' => 'Általam nem megtekintett',\n    'search_permissions_set' => 'Jogosultságok beállítva',\n    'search_created_by_me' => 'Általam létrehozott',\n    'search_updated_by_me' => 'Általam frissített',\n    'search_owned_by_me' => 'Tulajdonomban lévő',\n    'search_date_options' => 'Dátum beállítások',\n    'search_updated_before' => 'Frissítve ez előtt',\n    'search_updated_after' => 'Frissítve ez után',\n    'search_created_before' => 'Létrehozva ez előtt',\n    'search_created_after' => 'Létrehozva ez után',\n    'search_set_date' => 'Dátum beállítása',\n    'search_update' => 'Keresés frissítése',\n\n    // Shelves\n    'shelf' => 'Polc',\n    'shelves' => 'Polcok',\n    'x_shelves' => ':count polc|:count polcok',\n    'shelves_empty' => 'Nincsenek könyvespolcok létrehozva',\n    'shelves_create' => 'Új polc létrehozása',\n    'shelves_popular' => 'Népszerű polcok',\n    'shelves_new' => 'Új polcok',\n    'shelves_new_action' => 'Új polc',\n    'shelves_popular_empty' => 'A legnépszerűbb polcok itt fognak megjelenni.',\n    'shelves_new_empty' => 'A legutoljára létrehozott polcok itt fognak megjelenni.',\n    'shelves_save' => 'Polc mentése',\n    'shelves_books' => 'Könyvek ezen a polcon',\n    'shelves_add_books' => 'Könyvek hozzáadása ehhez a polchoz',\n    'shelves_drag_books' => 'Könyveket áthúzással lehet elhelyezni ezen a polcon',\n    'shelves_empty_contents' => 'Ehhez a polchoz nincsenek könyvek rendelve',\n    'shelves_edit_and_assign' => 'Polc szerkesztése könyvek hozzárendeléséhez',\n    'shelves_edit_named' => ':name polc szerkesztése',\n    'shelves_edit' => 'Polc szerkesztése',\n    'shelves_delete' => 'Polc törlése',\n    'shelves_delete_named' => ':name polc törlése',\n    'shelves_delete_explain' => \"':name'. nevű polc ezzel le lesz törölve. A benne található könyvek nem lesznek törölve.\",\n    'shelves_delete_confirmation' => 'Biztosan törölhető ez a polc?',\n    'shelves_permissions' => 'Polc jogosultság',\n    'shelves_permissions_updated' => 'Polc jogosultságok frissítve',\n    'shelves_permissions_active' => 'Polc jogosultságok aktívak',\n    'shelves_permissions_cascade_warning' => 'A polcokhoz kapcsolódó jogosultságok nem kapcsolódnak automatikusan a tárolt könyvekhez. Ennek az az oka, hogy egy könyv több polcon is létezhet. Az engedélyek azonban lemásolhatók a gyermekkönyvekbe az alábbi lehetőség segítségével.',\n    'shelves_permissions_create' => 'A polclétrehozási jogosultságok csak az alárendelt könyvekbe való másoláshoz használhatók az alábbi művelettel. Nem szabályozzák a könyvek létrehozásának lehetőségét.',\n    'shelves_copy_permissions_to_books' => 'Jogosultság másolása könyvekre',\n    'shelves_copy_permissions' => 'Jogosultság másolása',\n    'shelves_copy_permissions_explain' => 'Ezzel a polc jelenlegi engedélybeállításait alkalmazza a benne található összes könyvre. Az aktiválás előtt győződjön meg arról, hogy a polc engedélyeinek módosításait elmentette.',\n    'shelves_copy_permission_success' => 'Könyvespolc jogosultságok átmásolva :count könyvre',\n\n    // Books\n    'book' => 'Könyv',\n    'books' => 'Könyvek',\n    'x_books' => ':count könyv|:count könyv',\n    'books_empty' => 'Nincsenek könyvek létrehozva',\n    'books_popular' => 'Népszerű könyvek',\n    'books_recent' => 'Legutóbbi könyvek',\n    'books_new' => 'Új könyvek',\n    'books_new_action' => 'Új könyv',\n    'books_popular_empty' => 'A legnépszerűbb könyvek itt fognak megjelenni.',\n    'books_new_empty' => 'A legutoljára létrehozott könyvek itt fognak megjelenni.',\n    'books_create' => 'Új könyv létrehozása',\n    'books_delete' => 'Könyv törlése',\n    'books_delete_named' => ':bookName könyv törlése',\n    'books_delete_explain' => '\\':bookName\\' nevű könyv törölve lesz. Minden oldal és fejezet el lesz távolítva.',\n    'books_delete_confirmation' => 'Biztosan törölhető ez a könyv?',\n    'books_edit' => 'Könyv szerkesztése',\n    'books_edit_named' => ':bookName könyv szerkesztése',\n    'books_form_book_name' => 'Könyv neve',\n    'books_save' => 'Könyv mentése',\n    'books_permissions' => 'Könyv jogosultságok',\n    'books_permissions_updated' => 'Könyv jogosultságok frissítve',\n    'books_empty_contents' => 'Ehhez a könyvhöz nincsenek oldalak vagy fejezetek létrehozva.',\n    'books_empty_create_page' => 'Új oldal létrehozása',\n    'books_empty_sort_current_book' => 'Aktuális könyv rendezése',\n    'books_empty_add_chapter' => 'Fejezet hozzáadása',\n    'books_permissions_active' => 'Könyv jogosultságok aktívak',\n    'books_search_this' => 'Keresés ebben a könyvben',\n    'books_navigation' => 'Könyv navigáció',\n    'books_sort' => 'Könyv tartalmak rendezése',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => ':bookName könyv rendezése',\n    'books_sort_name' => 'Rendezés név szerint',\n    'books_sort_created' => 'Rendezés létrehozás dátuma szerint',\n    'books_sort_updated' => 'Rendezés frissítés dátuma szerint',\n    'books_sort_chapters_first' => 'Fejezetek elől',\n    'books_sort_chapters_last' => 'Fejezetek hátul',\n    'books_sort_show_other' => 'Egyéb könyvek mutatása',\n    'books_sort_save' => 'Új elrendezés mentése',\n    'books_sort_show_other_desc' => 'Adjon hozzá más könyveket, hogy bevonja őket a rendezési műveletbe, és lehetővé tegye a könyvek közötti egyszerű átszervezést.',\n    'books_sort_move_up' => 'Mozgatás fel',\n    'books_sort_move_down' => 'Mozgatás le',\n    'books_sort_move_prev_book' => 'Mozgatás az előző könyvbe',\n    'books_sort_move_next_book' => 'Mozgatás a következő könyvbe',\n    'books_sort_move_prev_chapter' => 'Mozgatás az előző fejezetbe',\n    'books_sort_move_next_chapter' => 'Mozgatás a következő fejezetbe',\n    'books_sort_move_book_start' => 'Mozgatás a könyv elejére',\n    'books_sort_move_book_end' => 'Mozgatás a könyv végére',\n    'books_sort_move_before_chapter' => 'Morgazás a fejezet elé',\n    'books_sort_move_after_chapter' => 'Mozgatás a fejezet után',\n    'books_copy' => 'Könyv másolása',\n    'books_copy_success' => 'Könyv sikeresen lemásolva',\n\n    // Chapters\n    'chapter' => 'Fejezet',\n    'chapters' => 'Fejezetek',\n    'x_chapters' => ':count fejezet|:count fejezetek',\n    'chapters_popular' => 'Népszerű fejezetek',\n    'chapters_new' => 'Új fejezet',\n    'chapters_create' => 'Új fejezet létrehozása',\n    'chapters_delete' => 'Fejezet törlése',\n    'chapters_delete_named' => ':chapterName fejezet törlése',\n    'chapters_delete_explain' => 'A(z) \\':chapterName\\' törlésére készül. A fejezethez tartozó minden oldal is törlésre fog kerülni.',\n    'chapters_delete_confirm' => 'Biztosan törölhető ez a fejezet?',\n    'chapters_edit' => 'Fejezet szerkesztése',\n    'chapters_edit_named' => ':chapterName fejezet szerkesztése',\n    'chapters_save' => 'Fejezet mentése',\n    'chapters_move' => 'Fejezet áthelyezése',\n    'chapters_move_named' => ':chapterName fejezet áthelyezése',\n    'chapters_copy' => 'Fejezet másolása',\n    'chapters_copy_success' => 'Fejezet sikeresen lemásolva',\n    'chapters_permissions' => 'Fejezet jogosultságok',\n    'chapters_empty' => 'Jelenleg nincsenek oldalak ebben a fejezetben.',\n    'chapters_permissions_active' => 'Fejezet jogosultságok aktívak',\n    'chapters_permissions_success' => 'Fejezet jogosultságok frissítve',\n    'chapters_search_this' => 'Keresés ebben a fejezetben',\n    'chapter_sort_book' => 'Könyv rendezése',\n\n    // Pages\n    'page' => 'Oldal',\n    'pages' => 'Oldalak',\n    'x_pages' => ':count oldal|:count oldal',\n    'pages_popular' => 'Népszerű oldalak',\n    'pages_new' => 'Új oldal',\n    'pages_attachments' => 'Csatolmányok',\n    'pages_navigation' => 'Oldal navigáció',\n    'pages_delete' => 'Oldal törlése',\n    'pages_delete_named' => ':pageName oldal törlése',\n    'pages_delete_draft_named' => ':pageName vázlat oldal törlése',\n    'pages_delete_draft' => 'Vázlat oldal törlése',\n    'pages_delete_success' => 'Oldal törölve',\n    'pages_delete_draft_success' => 'Vázlat oldal törölve',\n    'pages_delete_warning_template' => 'Ez az oldal aktívan használatban van könyv vagy fejezet alapértelmezett oldalsablonjaként. Ezekhez a könyvekhez vagy fejezetekhez a továbbiakban nem lesz alapértelmezett oldalsablon hozzárendelve az oldal törlése után.',\n    'pages_delete_confirm' => 'Biztosan törölhető ez az oldal?',\n    'pages_delete_draft_confirm' => 'Biztosan törölhető ez a vázlatoldal?',\n    'pages_editing_named' => ':pageName oldal szerkesztése',\n    'pages_edit_draft_options' => 'Vázlatbeállítások',\n    'pages_edit_save_draft' => 'Vázlat mentése',\n    'pages_edit_draft' => 'Oldal vázlat szerkesztése',\n    'pages_editing_draft' => 'Vázlat szerkesztése',\n    'pages_editing_page' => 'Oldal szerkesztése',\n    'pages_edit_draft_save_at' => 'Vázlat elmentve:',\n    'pages_edit_delete_draft' => 'Vázlat törlése',\n    'pages_edit_delete_draft_confirm' => 'Biztos benne, hogy törölni kívánja az oldalmódosítások piszkozatát? Az utolsó teljes mentés óta végrehajtott összes módosítása elvész, és a szerkesztő frissül a legfrissebb, nem vázlatos mentési állapottal.',\n    'pages_edit_discard_draft' => 'Vázlat elvetése',\n    'pages_edit_switch_to_markdown' => 'Váltás Markdown szerkesztőre',\n    'pages_edit_switch_to_markdown_clean' => '(Tisztított tartalom)',\n    'pages_edit_switch_to_markdown_stable' => '(Stabil tartalom)',\n    'pages_edit_switch_to_wysiwyg' => 'Váltás a WYSIWYG szerkesztőre',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Változásnapló beállítása',\n    'pages_edit_enter_changelog_desc' => 'A végrehajtott módosítások rövid leírása',\n    'pages_edit_enter_changelog' => 'Változásnapló megadása',\n    'pages_editor_switch_title' => 'Szerkesztőváltás',\n    'pages_editor_switch_are_you_sure' => 'Biztosan módosítani szeretné ennek az oldalnak a szerkesztőjét?',\n    'pages_editor_switch_consider_following' => 'A szerkesztők módosításakor vegye figyelembe a következőket:',\n    'pages_editor_switch_consideration_a' => 'Mentés után az új szerkesztő opciót minden jövőbeli szerkesztő használni fogja, beleértve azokat is, amelyek esetleg nem tudják maguk módosítani a szerkesztő típusát.',\n    'pages_editor_switch_consideration_b' => 'Ez bizonyos körülmények között a részletek és a szintaxis elvesztéséhez vezethet.',\n    'pages_editor_switch_consideration_c' => 'A legutóbbi mentés óta végrehajtott címke- vagy változásnapló-módosítások nem maradnak fenn a módosítás során.',\n    'pages_save' => 'Oldal mentése',\n    'pages_title' => 'Oldal címe',\n    'pages_name' => 'Oldal neve',\n    'pages_md_editor' => 'Szerkesztő',\n    'pages_md_preview' => 'Előnézet',\n    'pages_md_insert_image' => 'Kép beillesztése',\n    'pages_md_insert_link' => 'Entitás hivatkozás beillesztése',\n    'pages_md_insert_drawing' => 'Rajz beillesztése',\n    'pages_md_show_preview' => 'Előnézet megjelenítése',\n    'pages_md_sync_scroll' => 'Előnézet pozíció szinkronizálása',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Nem mentett rajz található',\n    'pages_drawing_unsaved_confirm' => 'A rendszer nem mentett rajzadatokat talált egy korábbi sikertelen rajzmentési kísérletből. Szeretné visszaállítani és folytatni a nem mentett rajz szerkesztését?',\n    'pages_not_in_chapter' => 'Az oldal nincs fejezetben',\n    'pages_move' => 'Oldal áthelyezése',\n    'pages_copy' => 'Oldal másolása',\n    'pages_copy_desination' => 'Másolás célja',\n    'pages_copy_success' => 'Oldal sikeresen lemásolva',\n    'pages_permissions' => 'Oldal jogosultságok',\n    'pages_permissions_success' => 'Oldal jogosultságok frissítve',\n    'pages_revision' => 'Változat',\n    'pages_revisions' => 'Oldal változatai',\n    'pages_revisions_desc' => 'Az alábbiakban az oldal összes korábbi verziója látható. Visszatekinthet, összehasonlíthatja és visszaállíthatja a régi oldalverziókat, ha az engedélyek lehetővé teszik. Előfordulhat, hogy az oldal teljes előzménye itt nem jelenik meg teljes mértékben, mivel a rendszerkonfigurációtól függően a régi változatok automatikusan törlődnek.',\n    'pages_revisions_named' => ':pageName oldal változatai',\n    'pages_revision_named' => ':pageName oldal változata',\n    'pages_revision_restored_from' => 'Visszaállítva innen: #:id; :summary',\n    'pages_revisions_created_by' => 'Létrehozta:',\n    'pages_revisions_date' => 'Változat dátuma',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Változat száma',\n    'pages_revisions_numbered' => 'Változat #:id',\n    'pages_revisions_numbered_changes' => '#:id változat módosításai',\n    'pages_revisions_editor' => 'Szerkesztő típusa',\n    'pages_revisions_changelog' => 'Változásnapló',\n    'pages_revisions_changes' => 'Módosítások',\n    'pages_revisions_current' => 'Aktuális verzió',\n    'pages_revisions_preview' => 'Előnézet',\n    'pages_revisions_restore' => 'Visszaállítás',\n    'pages_revisions_none' => 'Ennek az oldalnak nincsenek változatai',\n    'pages_copy_link' => 'Hivatkozás másolása',\n    'pages_edit_content_link' => 'Ugrás a szakaszhoz a szerkesztőben',\n    'pages_pointer_enter_mode' => 'Lépjen be a szakaszválasztó módba',\n    'pages_pointer_label' => 'Oldalszakasz beállításai',\n    'pages_pointer_permalink' => 'Oldalszakasz állandó hivatkozás',\n    'pages_pointer_include_tag' => 'Oldalszakasz tartalmazza a címkét',\n    'pages_pointer_toggle_link' => 'Permalink mód, Nyomja meg az include tag megjelenítéséhez',\n    'pages_pointer_toggle_include' => 'Include tag mód, Nyomja meg az permalink megjelenítéséhez',\n    'pages_permissions_active' => 'Oldal jogosultságok aktívak',\n    'pages_initial_revision' => 'Kezdeti közzététel',\n    'pages_references_update_revision' => 'A belső hivatkozások automatikus frissítése',\n    'pages_initial_name' => 'Új oldal',\n    'pages_editing_draft_notification' => 'A jelenleg szerkesztett vázlat legutóbb ekkor volt elmentve: :timeDiff.',\n    'pages_draft_edited_notification' => 'Ezt az oldalt azóta már frissítették. Javasolt ennek a vázlatnak az elvetése.',\n    'pages_draft_page_changed_since_creation' => 'Ez az oldal a vázlat létrehozása óta frissült. Javasoljuk, hogy dobja el ezt a piszkozatot, vagy ügyeljen arra, hogy ne írja felül az oldal módosításait.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count felhasználók kezdte el szerkeszteni ezt az oldalt',\n        'start_b' => ':userName elkezdte szerkeszteni ezt az oldalt',\n        'time_a' => 'mióta az oldal utoljára frissítve volt',\n        'time_b' => 'az utolsó :minCount percben',\n        'message' => ':start :time. Ügyeljen arra, hogy ne írjuk felül egymás frissítéseit!',\n    ],\n    'pages_draft_discarded' => 'Vázlat elvetve! A szerkesztő frissítve lesz az oldal aktuális tartalmával',\n    'pages_draft_deleted' => 'Vázlat elvetve! A szerkesztő frissítve lesz az oldal aktuális tartalmával',\n    'pages_specific' => 'Egy bizonyos oldal',\n    'pages_is_template' => 'Oldalsablon',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Oldalsáv ki/be',\n    'page_tags' => 'Oldal címkék',\n    'chapter_tags' => 'Fejezet címkék',\n    'book_tags' => 'Könyv címkék',\n    'shelf_tags' => 'Polc címkék',\n    'tag' => 'Címke',\n    'tags' =>  'Címkék',\n    'tags_index_desc' => 'A címkék a rendszeren belüli tartalomra alkalmazhatók a kategorizálás rugalmas formája alkalmazása érdekében. A címkéknek kulcsuk és értékük is lehetnek, de az érték nem kötelező. Alkalmazása után a tartalom lekérdezhető a címkenév és érték használatával.',\n    'tag_name' =>  'Címkenév',\n    'tag_value' => 'Címke érték (nem kötelező)',\n    'tags_explain' => \"Címkék hozzáadása a tartalom jobb kategorizálásához.\\nA mélyebb szervezettség megvalósításához hozzá lehet rendelni egy értéket a címkéhez.\",\n    'tags_add' => 'Másik címke hozzáadása',\n    'tags_remove' => 'Címke eltávolítása',\n    'tags_usages' => 'Összes címkehasználat',\n    'tags_assigned_pages' => 'Oldalakhoz Rendelt',\n    'tags_assigned_chapters' => 'Fejezetekhez rendelt',\n    'tags_assigned_books' => 'Könyvekhez Rendelt',\n    'tags_assigned_shelves' => 'Polcokhoz Rendelt',\n    'tags_x_unique_values' => ':count egyedi érték',\n    'tags_all_values' => 'Összes érték',\n    'tags_view_tags' => 'Címkék megtekintése',\n    'tags_view_existing_tags' => 'Létező címkék megtekintése',\n    'tags_list_empty_hint' => 'A címkék hozzárendelhetők az oldalszerkesztő oldalsávján keresztül, vagy egy könyv, fejezet vagy polc adatainak szerkesztése közben.',\n    'attachments' => 'Csatolmányok',\n    'attachments_explain' => 'Az oldalon megjelenő fájlok feltöltése vagy hivatkozások csatolása. Az oldal oldalsávjában fognak megjelenni.',\n    'attachments_explain_instant_save' => 'Az itt történt módosítások azonnal el lesznek mentve.',\n    'attachments_upload' => 'Fájlfeltöltés',\n    'attachments_link' => 'Hivatkozás csatolása',\n    'attachments_upload_drop' => 'Alternatív megoldásként a fájlt ide húzva is fel lehet tölteni mellékletként.',\n    'attachments_set_link' => 'Hivatkozás beállítása',\n    'attachments_delete' => 'Biztosan törölhető ez a melléklet?',\n    'attachments_dropzone' => 'Húzza a file(oka)t ide a feltöltéshez',\n    'attachments_no_files' => 'Nincsenek fájlok feltöltve',\n    'attachments_explain_link' => 'Fájl feltöltése helyett hozzá lehet kapcsolni egy hivatkozást. Ez egy hivatkozás lesz egy másik oldalra vagy egy fájlra a felhőben.',\n    'attachments_link_name' => 'Hivatkozás neve',\n    'attachment_link' => 'Csatolmány hivatkozás',\n    'attachments_link_url' => 'Hivatkozás fájlra',\n    'attachments_link_url_hint' => 'Weboldal vagy fájl webcíme',\n    'attach' => 'Csatolás',\n    'attachments_insert_link' => 'Melléklet hivatkozás hozzáadása oldalhoz',\n    'attachments_edit_file' => 'Fájl szerkesztése',\n    'attachments_edit_file_name' => 'Fájl neve',\n    'attachments_edit_drop_upload' => 'Feltöltés és felülírás ejtéssel vagy kattintással',\n    'attachments_order_updated' => 'Csatolmány sorrend frissítve',\n    'attachments_updated_success' => 'Csatolmány részletei frissítve',\n    'attachments_deleted' => 'Csatolmány törölve',\n    'attachments_file_uploaded' => 'Fájl sikeresen feltöltve',\n    'attachments_file_updated' => 'Fájl sikeresen frissítve',\n    'attachments_link_attached' => 'Hivatkozás sikeresen hozzácsatolva az oldalhoz',\n    'templates' => 'Sablonok',\n    'templates_set_as_template' => 'Az oldal egy sablon',\n    'templates_explain_set_as_template' => 'Ez az oldal sablonnak lett beállítva, így a tartalma felhasználható más oldalak létrehozásakor. Más felhasználók is használhatják ezt a sablont ha megtekintési jogosultságuk van ehhez az oldalhoz.',\n    'templates_replace_content' => 'Oldal tartalmának cseréje',\n    'templates_append_content' => 'Hozzáfűzés az oldal tartalmához',\n    'templates_prepend_content' => 'Hozzáadás az oldal tartalmának elejéhez',\n\n    // Profile View\n    'profile_user_for_x' => 'Felhasználó ez óta: :time',\n    'profile_created_content' => 'Létrehozott tartalom',\n    'profile_not_created_pages' => ':userName még nem hozott létre oldalt',\n    'profile_not_created_chapters' => ':userName még nem hozott létre fejezetet',\n    'profile_not_created_books' => ':userName még nem hozott létre könyvet',\n    'profile_not_created_shelves' => ':userName még nem hozott létre polcot',\n\n    // Comments\n    'comment' => 'Megjegyzés',\n    'comments' => 'Megjegyzések',\n    'comment_add' => 'Megjegyzés hozzáadása',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Megjegyzés írása',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Megjegyzés mentése',\n    'comment_new' => 'Új megjegyzés',\n    'comment_created' => 'megjegyzést fűzött hozzá :createDiff',\n    'comment_updated' => 'Frissítve :updateDiff :username által',\n    'comment_updated_indicator' => 'Frissített',\n    'comment_deleted_success' => 'Megjegyzés törölve',\n    'comment_created_success' => 'Megjegyzés hozzáadva',\n    'comment_updated_success' => 'Megjegyzés frissítve',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Biztosan törölhető ez a megjegyzés?',\n    'comment_in_reply_to' => 'Válasz erre: :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Itt vannak az ezen az oldalon lévő megjegyzések. Megjegyzések hozzáadhatók és kezelhetők a mentett oldal megtekintésekor.',\n\n    // Revision\n    'revision_delete_confirm' => 'Biztosan törölhető ez a változat?',\n    'revision_restore_confirm' => 'Biztosan visszaállítható ez a változat? A oldal jelenlegi tartalma le lesz cserélve.',\n    'revision_cannot_delete_latest' => 'A legutolsó változat nem törölhető.',\n\n    // Copy view\n    'copy_consider' => 'Kérem, fontolja meg az alábbiakat, amikor tartalmat kíván másolni.',\n    'copy_consider_permissions' => 'Az egyéni engedélybeállítások nem kerülnek másolásra.',\n    'copy_consider_owner' => 'Minden lemásolt tartalomnak Ön lesz a tulajdonosa.',\n    'copy_consider_images' => 'Az oldalképfájlok nem duplikálódnak, és az eredeti képek megőrzik kapcsolatukat az eredetileg feltöltött oldallal.',\n    'copy_consider_attachments' => 'Az oldal mellékletei nem kerülnek másolásra.',\n    'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',\n\n    // Conversions\n    'convert_to_shelf' => 'Átalakítás polccá',\n    'convert_to_shelf_contents_desc' => 'Ezt a könyvet új polccá alakíthatja, azonos tartalommal. A könyvben található fejezetek új könyvekké lesznek átalakítva. Ha ez a könyv tartalmaz olyan oldalakat, amelyek nem szerepelnek egy fejezetben, akkor a könyv átnevezzük és tartalmaz ilyen oldalakat, és ez a könyv az új polc részévé válik.',\n    'convert_to_shelf_permissions_desc' => 'A könyvhöz beállított engedélyek át lesznek másolva az új polcra és az összes olyan új alárendelt könyvre, amelyek nem rendelkeznek saját engedélyekkel. Vegye figyelembe, hogy a polcokon lévő engedélyek nem kapcsolódnak automatikusan a tartalomhoz, ahogy a könyvek esetében.',\n    'convert_book' => 'Könyv átalakítása',\n    'convert_book_confirm' => 'Biztosan konvertálni szeretné ezt a könyvet?',\n    'convert_undo_warning' => 'Ezt nem lehet olyan könnyen visszavonni.',\n    'convert_to_book' => 'Átalakítás könyvvé',\n    'convert_to_book_desc' => 'Ezt a fejezetet új, azonos tartalmú könyvvé alakíthatja. Az ebben a fejezetben beállított engedélyek átmásolódnak az új könyvbe, de a szülőkönyvből származó örökölt engedélyek nem kerülnek másolásra, ami a hozzáférés-szabályozás megváltozásához vezethet.',\n    'convert_chapter' => 'Fejezet átalakítása',\n    'convert_chapter_confirm' => 'Biztosan át szeretnéd alakítani ezt a fejezetet?',\n\n    // References\n    'references' => 'Értékelések',\n    'references_none' => 'Nincsenek nyomon követett hivatkozások erre az elemre.',\n    'references_to_desc' => 'Az alábbiakban felsoroljuk az összes ismert tartalmat a rendszerben, amely erre az elemre hivatkozik.',\n\n    // Watch Options\n    'watch' => 'Megfigyelés',\n    'watch_title_default' => 'Alapértelmezett beállítások',\n    'watch_desc_default' => 'Állítsa vissza a megfigyelést az alapértelmezett értesítési beállításokra.',\n    'watch_title_ignore' => 'Mellőzés',\n    'watch_desc_ignore' => 'Figyelmen kívül hagyja az összes értesítést, beleértve a felhasználói szintű beállításokból származó értesítéseket is.',\n    'watch_title_new' => 'Új oldalak',\n    'watch_desc_new' => 'Értesítés, ha új oldal jön létre ezen az elemen belül.',\n    'watch_title_updates' => 'Minden oldal frissítése',\n    'watch_desc_updates' => 'Értesítés minden új oldalról és oldalváltozásról.',\n    'watch_desc_updates_page' => 'Értesítsen minden oldalváltozásról.',\n    'watch_title_comments' => 'Az oldal összes frissítése és megjegyzése',\n    'watch_desc_comments' => 'Értesítés minden új oldalról, oldalváltozásról és új megjegyzésről.',\n    'watch_desc_comments_page' => 'Értesítés az oldal változásairól és az új megjegyzésekről.',\n    'watch_change_default' => 'Az alapértelmezett értesítési beállítások módosítása',\n    'watch_detail_ignore' => 'Az értesítések figyelmen kívül hagyása',\n    'watch_detail_new' => 'Új oldalak figyelése',\n    'watch_detail_updates' => 'Új oldalak és frissítések figyelése',\n    'watch_detail_comments' => 'Új oldalak, frissítések és megjegyzések figyelése',\n    'watch_detail_parent_book' => 'Megfigyelés szülőkönyvből',\n    'watch_detail_parent_book_ignore' => 'Figyelmen kívül hagyás a szülőkönyvön keresztül',\n    'watch_detail_parent_chapter' => 'Megfigyelés szülő fejezetből',\n    'watch_detail_parent_chapter_ignore' => 'Figyelmen kívül hagyás a szülő fejezeten keresztül',\n];\n"
  },
  {
    "path": "lang/hu/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Nincs jogosultság a kért oldal eléréséhez.',\n    'permissionJson' => 'Nincs jogosultság a kért művelet végrehajtásához.',\n\n    // Auth\n    'error_user_exists_different_creds' => ':email címmel már létezik felhasználó, de más hitelesítő adatokkal.',\n    'auth_pre_register_theme_prevention' => 'A felhasználói fiók nem regisztrálható a megadott adatokkal',\n    'email_already_confirmed' => 'Az email cím már meg van erősítve, meg lehet próbálni a bejelentkezést.',\n    'email_confirmation_invalid' => 'A megerősítő vezérjel nem érvényes vagy használva volt. Meg kell próbálni újraregisztrálni.',\n    'email_confirmation_expired' => 'A megerősítő vezérjel lejárt. Egy új megerősítő email lett elküldve.',\n    'email_confirmation_awaiting' => 'A használatban lévő fiók email címét meg kell erősíteni',\n    'ldap_fail_anonymous' => 'Nem sikerült az LDAP elérése névtelen csatlakozással',\n    'ldap_fail_authed' => 'Az LDAP hozzáférés nem sikerült a megadott DN és jelszó beállításokkal',\n    'ldap_extension_not_installed' => 'LDAP PHP kiterjesztés nincs telepítve',\n    'ldap_cannot_connect' => 'Nem lehet kapcsolódni az LDAP kiszolgálóhoz, a kezdeti kapcsolatfelvétel nem sikerült',\n    'saml_already_logged_in' => 'Már bejelentkezett',\n    'saml_no_email_address' => 'Ehhez a felhasználóhoz nem található email cím a külső hitelesítő rendszer által átadott adatokban',\n    'saml_invalid_response_id' => 'A külső hitelesítő rendszerből érkező kérést nem ismerte fel az alkalmazás által indított folyamat. Bejelentkezés után az előző oldalra történő visszalépés okozhatja ezt a hibát.',\n    'saml_fail_authed' => 'Bejelentkezés :system használatával sikertelen, a rendszer nem biztosított sikeres hitelesítést',\n    'oidc_already_logged_in' => 'Már bejelentkezett',\n    'oidc_no_email_address' => 'Ehhez a felhasználóhoz nem található email cím a külső hitelesítő rendszer által átadott adatokban',\n    'oidc_fail_authed' => 'Bejelentkezés :system használatával sikertelen, a rendszer nem biztosított sikeres hitelesítést',\n    'social_no_action_defined' => 'Nincs művelet meghatározva',\n    'social_login_bad_response' => \"Hiba történt :socialAccount bejelentkezés közben:\\n:error\",\n    'social_account_in_use' => ':socialAccount fiók már használatban van. :socialAccount opción keresztül érdemes megpróbálni a bejelentkezést.',\n    'social_account_email_in_use' => ':email email cím már használatban van. Ha már van fiók létrehozva, :egy socialAccount fiókot hozzá lehet csatolni a profil beállításainál.',\n    'social_account_existing' => ':socialAccount már hozzá van kapcsolva a fiókhoz.',\n    'social_account_already_used_existing' => ':socialAccount fiókot már egy másik felhasználó használja.',\n    'social_account_not_used' => ':socialAccount fiók nincs felhasználóhoz kapcsolva. A hozzákapcsolást a profil oldalon lehet elvégezni. ',\n    'social_account_register_instructions' => ':socialAccount beállítása használatával is lehet fiókot regisztrálni, ha még nem volt fiók létrehozva.',\n    'social_driver_not_found' => 'Közösségi meghajtó nem található',\n    'social_driver_not_configured' => ':socialAccount közösségi beállítások nem megfelelőek.',\n    'invite_token_expired' => 'Ez a meghívó hivatkozás lejárt. Helyette meg lehet próbálni új jelszót megadni a fiókhoz.',\n    'login_user_not_found' => 'A művelethez nem található felhasználó.',\n\n    // System\n    'path_not_writable' => ':filePath elérési út nem tölthető fel. Ellenőrizni kell, hogy az útvonal a kiszolgáló számára írható.',\n    'cannot_get_image_from_url' => 'Nem lehet lekérni a képet innen: :url',\n    'cannot_create_thumbs' => 'A kiszolgáló nem tud létrehozni bélyegképeket. Ellenőrizni kell, hogy telepítve van-a a GD PHP kiterjesztés.',\n    'server_upload_limit' => 'A kiszolgáló nem engedélyez ilyen méretű feltöltéseket. Kisebb fájlmérettel kell próbálkozni.',\n    'server_post_limit' => 'A szerver nem tudja fogadni a megadott adatmennyiséget. Próbálkozz újra kevesebb adattal vagy egy kisebb fájllal.',\n    'uploaded'  => 'A kiszolgáló nem engedélyez ilyen méretű feltöltéseket. Kisebb fájlmérettel kell próbálkozni.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Hiba történt a kép feltöltése közben',\n    'image_upload_type_error' => 'A feltöltött kép típusa érvénytelen',\n    'image_upload_replace_type' => 'A cserélt képnek azonos típusúnak kell lennie',\n    'image_upload_memory_limit' => 'A rendszererőforrás-korlátok miatt nem sikerült kezelni a képfeltöltést és/vagy az indexképek létrehozását.',\n    'image_thumbnail_memory_limit' => 'A rendszererőforrás-korlátok miatt nem sikerült létrehozni a képméret-változatokat.',\n    'image_gallery_thumbnail_memory_limit' => 'A rendszererőforrás-korlátok miatt nem sikerült létrehozni a galéria bélyegképét.',\n    'drawing_data_not_found' => 'A rajzadatokat nem sikerült betölteni. Előfordulhat, hogy a rajzfájl már nem létezik, vagy nem rendelkezik hozzáférési engedéllyel.',\n\n    // Attachments\n    'attachment_not_found' => 'Csatolmány nem található',\n    'attachment_upload_error' => 'Hiba történt a melléklet feltöltésekor',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Nem sikerült a vázlat mentése. Mentés előtt állítsd helyre az internetkapcsolatot',\n    'page_draft_delete_fail' => 'Nem sikerült törölni az oldalvázlatot és lekérni az aktuális oldal mentett tartalmat',\n    'page_custom_home_deletion' => 'Nem lehet oldalt törölni ha kezdőlapnak van beállítva',\n\n    // Entities\n    'entity_not_found' => 'Entitás nem található',\n    'bookshelf_not_found' => 'Polc nem található',\n    'book_not_found' => 'Könyv nem található',\n    'page_not_found' => 'Oldal nem található',\n    'chapter_not_found' => 'Fejezet nem található',\n    'selected_book_not_found' => 'A kiválasztott könyv nem található',\n    'selected_book_chapter_not_found' => 'A kiválasztott könyv vagy fejezet nem található',\n    'guests_cannot_save_drafts' => 'Vendégek nem menthetnek el vázlatokat',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Nem lehet törölni az egyetlen adminisztrátort',\n    'users_cannot_delete_guest' => 'A vendég felhasználót nem lehet törölni',\n    'users_could_not_send_invite' => 'Could not create user since invite email failed to send',\n\n    // Roles\n    'role_cannot_be_edited' => 'Ezt a szerepkört nem lehet szerkeszteni',\n    'role_system_cannot_be_deleted' => 'Ez a szerepkör egy rendszer szerepkör ezért nem törölhető',\n    'role_registration_default_cannot_delete' => 'Ezt a szerepkört nem lehet törölni amíg alapértelmezés szerinti regisztrációs szerepkörnek van beállítva',\n    'role_cannot_remove_only_admin' => 'Ez a felhasználó az egyetlen, az adminisztrátor szerepkörhöz rendelt felhasználó. Eltávolítása előtt az adminisztrátor szerepkört át kell ruházni egy másik felhasználóra.',\n\n    // Comments\n    'comment_list' => 'Hiba történt a megjegyzések lekérése közben.',\n    'cannot_add_comment_to_draft' => 'Vázlathoz nem lehet megjegyzéseket fűzni.',\n    'comment_add' => 'Hiba történt a megjegyzés hozzáadása / frissítése közben.',\n    'comment_delete' => 'Hiba történt a megjegyzés törlése közben.',\n    'empty_comment' => 'Üres megjegyzést nem lehet hozzáadni.',\n\n    // Error pages\n    '404_page_not_found' => 'Oldal nem található',\n    'sorry_page_not_found' => 'Sajnáljuk, a keresett oldal nem található.',\n    'sorry_page_not_found_permission_warning' => 'Ha arra számított, hogy ez az oldal létezik, előfordulhat, hogy nincs engedélye a megtekintésére.',\n    'image_not_found' => 'A kép nem található',\n    'image_not_found_subtitle' => 'Sajnáljuk, a keresett kép nem található.',\n    'image_not_found_details' => 'Ha arra számított, hogy ez a kép létezik, akkor előfordulhat, hogy törölték.',\n    'return_home' => 'Vissza a kezdőlapra',\n    'error_occurred' => 'Hiba történt',\n    'app_down' => ':appName jelenleg nem üzemel',\n    'back_soon' => 'Hamarosan újra elérhető lesz.',\n\n    // Import\n    'import_zip_cant_read' => 'Could not read ZIP file.',\n    'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',\n    'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Import ZIP failed to validate with errors:',\n    'import_zip_failed_notification' => 'Failed to import ZIP file.',\n    'import_perms_books' => 'You are lacking the required permissions to create books.',\n    'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',\n    'import_perms_pages' => 'You are lacking the required permissions to create pages.',\n    'import_perms_images' => 'You are lacking the required permissions to create images.',\n    'import_perms_attachments' => 'You are lacking the required permission to create attachments.',\n\n    // API errors\n    'api_no_authorization_found' => 'A kérésben nem található hitelesítési vezérjel',\n    'api_bad_authorization_format' => 'A kérésben hitelesítési vezérjel található de a formátuma érvénytelennek tűnik',\n    'api_user_token_not_found' => 'A megadott hitelesítési vezérjelhez nem található egyező API vezérjel',\n    'api_incorrect_token_secret' => 'Az API tokenhez használt secret helytelen',\n    'api_user_no_api_permission' => 'A használt API vezérjel tulajdonosának nincs jogosultsága API hívások végrehajtásához',\n    'api_user_token_expired' => 'A használt hitelesítési vezérjel lejárt',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Hiba történt egy teszt email küldésekor:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'Az URL nem egyezik a konfigurált és engedélyezett SSR-állomásokkal',\n];\n"
  },
  {
    "path": "lang/hu/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Új megjegyzés ezen az oldalon: :pageName',\n    'new_comment_intro' => 'Egy felhasználó hozzászólt egy oldalon itt: :appName:',\n    'new_page_subject' => 'Új oldal: :pageName',\n    'new_page_intro' => 'Az új oldal létrehozása sikeres volt itt: :appName:',\n    'updated_page_subject' => 'Frissített oldal: :pageName',\n    'updated_page_intro' => 'Az oldal frissítése sikeres volt itt: :appName:',\n    'updated_page_debounce' => 'Az értesítések tömegének elkerülése érdekében egy ideig nem kap értesítést az oldal további szerkesztéseiről ugyanaz a szerkesztő.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Oldal neve:',\n    'detail_page_path' => 'Oldal helye:',\n    'detail_commenter' => 'Hozzászóló:',\n    'detail_comment' => 'Megjegyzés:',\n    'detail_created_by' => 'Készítette:',\n    'detail_updated_by' => 'Frissítette:',\n\n    'action_view_comment' => 'Hozzászólás megtekintése',\n    'action_view_page' => 'Oldal megtekintése',\n\n    'footer_reason' => 'Ezt az értesítést azért küldtük, mert a :link lefedi ezt a tevékenységtípust ehhez az elemhez.',\n    'footer_reason_link' => 'értesítési beállításait',\n];\n"
  },
  {
    "path": "lang/hu/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Előző',\n    'next'     => 'Következő &raquo;',\n\n];\n"
  },
  {
    "path": "lang/hu/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'A jelszónak legalább hat karakterből kell állnia, és egyeznie kell a megerősítéssel.',\n    'user' => \"Nem található felhasználó ezzel az e-mail címmel.\",\n    'token' => 'A jelszó visszaállító biztonsági kód nem érvényes ehhez az e-mail címhez.',\n    'sent' => 'E-mailben elküldtük a jelszó visszaállító hivatkozást!',\n    'reset' => 'A jelszó visszaállítva!',\n\n];\n"
  },
  {
    "path": "lang/hu/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Fiókom',\n\n    'shortcuts' => 'Billentyűparancsok',\n    'shortcuts_interface' => 'UI billentyűparancsok beállításai',\n    'shortcuts_toggle_desc' => 'Itt engedélyezheti vagy letilthatja a navigációhoz és műveletekhez használt billentyűparancsokat.',\n    'shortcuts_customize_desc' => 'Az alábbi billentyűparancsok testre szabhatók. Csak nyomja meg a kívánt billentyűkombinációt, miután kiválasztotta a billentyűparancshoz tartozó mezőt.',\n    'shortcuts_toggle_label' => 'A billentyűparancsok engedélyezve',\n    'shortcuts_section_navigation' => 'Navigáció',\n    'shortcuts_section_actions' => 'Gyakori műveletek',\n    'shortcuts_save' => 'Billentyűparancsok mentése',\n    'shortcuts_overlay_desc' => 'Megjegyzés: Amikor a gyorsbillentyűk engedélyezve vannak, egy segítő átfedés érhető el azzal, hogy a \"?\" billentyűt megnyomva kiemeli az aktuálisan látható képernyőn elérhető gyorsbillentyűket a műveletekhez.',\n    'shortcuts_update_success' => 'A gyorsbillentyű-beállítások frissítve lettek!',\n    'shortcuts_overview_desc' => 'A rendszerfelhasználói felületen történő navigálásához használható billentyűparancsok kezelése.',\n\n    'notifications' => 'Értesítési beállítások',\n    'notifications_desc' => 'Állítsd be az e-mail értesítéseket, amelyeket akkor kapsz, ha bizonyos tevékenység történik a rendszeren belül.',\n    'notifications_opt_own_page_changes' => 'Értesítsen változásokról az általam tulajdonolt oldalakon',\n    'notifications_opt_own_page_comments' => 'Értesítés a hozzászólásokról az általam tulajdonolt oldalakon',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Értesítsen válaszokról a hozzászólásaimra',\n    'notifications_save' => 'Beállítások mentése',\n    'notifications_update_success' => 'Az értesítési beállítások frissítve lettek!',\n    'notifications_watched' => 'Megfigyelt és figyelmen kívül hagyott elemek',\n    'notifications_watched_desc' => 'Az alábbi elemekre egyedi figyelési beállítások vannak alkalmazva. A beállítások frissítéséhez tekintsd meg az elemet, majd keresd a figyelési lehetőségeket az oldalsávban.',\n\n    'auth' => 'Hozzáférés és Biztonság',\n    'auth_change_password' => 'Jelszó módosítása',\n    'auth_change_password_desc' => 'Változtasd meg az alkalmazásba történő bejelentkezéshez használt jelszavadat. Ennek legalább 8 karakter hosszúnak kell lennie.',\n    'auth_change_password_success' => 'A jelszó frissítve lett!',\n\n    'profile' => 'Felhasználó részletei',\n    'profile_desc' => 'A kommunikációhoz és a rendszer személyre szabásához használt adatokon kívül kezelheti fiókja adatait, amelyek más felhasználók számára jelennek meg.',\n    'profile_view_public' => 'Nyilvános profil megtekintése',\n    'profile_name_desc' => 'Állítsd be a megjelenített nevedet, amely látható lesz a rendszer többi felhasználója számára az általad végzett tevékenység és a saját tartalom révén.',\n    'profile_email_desc' => 'Ezt az e-mail címet értesítésekre fogjuk használni, valamint az érvényben lévő beállítások függvényében hitelesítéshez is.',\n    'profile_email_no_permission' => 'Sajnos nincs jogosultságod az e-mail cím megváltoztatására. Ha szeretnéd ezt megváltoztatni, kérj meg egy adminisztrátort, hogy ezt megtegye helyetted.',\n    'profile_avatar_desc' => 'Válassz egy képet, amelyet a rendszerben használnál a neved mellett. A kép lehetőleg négyzet alakú és körülbelül 256px szélességű és magasságú legyen.',\n    'profile_admin_options' => 'Adminisztrátori beállítások',\n    'profile_admin_options_desc' => 'További adminisztrátori szintű lehetőségek, például a szerepkörök hozzárendelésének kezelése, megtalálhatóak a felhasználói fiókod beállításai között az \"Beállítások > Felhasználók\" területen az alkalmazásban.',\n\n    'delete_account' => 'Felhasználói fiók törlése',\n    'delete_my_account' => 'Törlöm a felhasználói fiókomat',\n    'delete_my_account_desc' => 'Ez véglegesen törölni fogja a felhasználói fiókodat a rendszerből. Nem lesz lehetőséged visszaállítani ezt a fiókot, vagy visszavonni ezt a műveletet. A létrehozott tartalmak, például az oldalak és feltöltött képek megmaradnak.',\n    'delete_my_account_warning' => 'Biztosan törölni szeretnéd a fiókodat?',\n];\n"
  },
  {
    "path": "lang/hu/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Beállítások',\n    'settings_save' => 'Beállítások mentése',\n    'system_version' => 'Rendszerverzió',\n    'categories' => 'Kategóriák',\n\n    // App Settings\n    'app_customization' => 'Személyre szabás',\n    'app_features_security' => 'Jellemzők és biztonság',\n    'app_name' => 'Alkalmazás neve',\n    'app_name_desc' => 'Ez a név meg fog jelenni a fejlécben és minden a rendszer által küldött emailben.',\n    'app_name_header' => 'Név mutatása a fejlécben',\n    'app_public_access' => 'Nyilvános hozzáférés',\n    'app_public_access_desc' => 'Ha engedélyezett, a nem bejelentkezett felhasználók is hozzá tudnak férni a BookStack példány tartalmaihoz.',\n    'app_public_access_desc_guest' => 'A nyilvános látogatók hozzáférése a \"Guest\" felhasználón keresztül irányítható.',\n    'app_public_access_toggle' => 'Nyilvános hozzáférés engedélyezése',\n    'app_public_viewing' => 'Nyilvános megtekintés engedélyezve?',\n    'app_secure_images' => 'Magasabb biztonságú képfeltöltés',\n    'app_secure_images_toggle' => 'Magasabb biztonságú képfeltöltés engedélyezése',\n    'app_secure_images_desc' => 'Teljesítmény optimalizálási okokból minden kép nyilvános. Ez a beállítás egy véletlenszerű, nehezen kitalálható karakterláncot illeszt a képek útvonalának elejére. Meg kell győződni róla, hogy a könnyű hozzáférés megakadályozása érdekében a könyvtár indexek nincsenek engedélyezve.',\n    'app_default_editor' => 'Alapértelmezett  oldal szerkesztő',\n    'app_default_editor_desc' => 'Válassza ki, hogy alapértelmezés szerint melyik szerkesztőt szeretné használni az új oldalak szerkesztésekor. Ezt felülírhatja oldalszintű szinten, amennyiben az engedélyek lehetővé teszik.',\n    'app_custom_html' => 'Egyéni HTML fejléc tartalom',\n    'app_custom_html_desc' => 'Az itt hozzáadott bármilyen tartalom be lesz illesztve minden oldal <head> szekciójának aljára. Ez hasznos a stílusok felülírásához van analitikai kódok hozzáadásához.',\n    'app_custom_html_disabled_notice' => 'Az egyéni HTML fejléc tartalom le van tiltva ezen a beállítási oldalon, hogy az esetleg hibásan megadott módosításokat vissza lehessen állítani.',\n    'app_logo' => 'Alkalmazás logó',\n    'app_logo_desc' => 'Ez az alkalmazás fejléc sávjában van használva többek között. Ennek a képnek 86 képpont magasnak kell lennie. A nagy képek át lesznek méretezve.',\n    'app_icon' => 'Alkalmazás ikon',\n    'app_icon_desc' => 'Ez az ikon a böngésző fülekhez és a gyorsikonokhoz használatos. Ez egy 256 képpont négyzet alakú PNG képnek kell lennie.',\n    'app_homepage' => 'Alkalmazás kezdőlapja',\n    'app_homepage_desc' => 'A kezdőlapon az alapértelmezés szerinti nézet helyett megjelenő nézet kiválasztása. A kiválasztott oldalakon figyelmen kívül lesznek hagyva az oldal engedélyek.',\n    'app_homepage_select' => 'Egy oldal kiválasztása',\n    'app_footer_links' => 'Lábléc linkek',\n    'app_footer_links_desc' => 'Adj hozzá linkeket a weboldal láblécéhez. Ezek a legtöbb oldalon megjelennek, beleértve azokat is, amelyekhez nincs szükség bejelentkezésre. Használhatsz egy \"trans::<key>\" címkét a rendszer által definiált fordítások használatához. Például: A \"trans::common.privacy_policy\" használata a lefordított szöveget (\"Adatvédelmi Irányelvek\") adja vissza, és a \"trans::common.terms_of_service\" a \"Szolgáltatási feltételek\" lefordított szöveget adja eredményül.',\n    'app_footer_links_label' => 'Link címke',\n    'app_footer_links_url' => 'Link URL',\n    'app_footer_links_add' => 'Lábléc hivatkozás hozzáadása',\n    'app_disable_comments' => 'Megjegyzések letiltása',\n    'app_disable_comments_toggle' => 'Megjegyzések letiltása',\n    'app_disable_comments_desc' => 'Megjegyzések letiltása az alkalmazás összes oldalán.<br>A már létező megjegyzések el lesznek rejtve.',\n\n    // Color settings\n    'color_scheme' => 'Alkalmazás színséma',\n    'color_scheme_desc' => 'Állítsd be a színeket az alkalmazás felhasználói felületén. A színeket külön-külön lehet konfigurálni a sötét és a világos módokhoz, hogy a legjobban illeszkedjenek a témához, és biztosítsák az olvashatóságot.',\n    'ui_colors_desc' => 'Állítsa be az alkalmazás elsődleges színét és alapértelmezett hivatkozási színét. Az elsődleges színt főként a fejléc szalaghirdetéséhez, a gombokhoz és a felület díszítéséhez használják. Az alapértelmezett hivatkozásszín a szöveges hivatkozásokhoz és műveletekhez használatos, mind az írott tartalomban, mind az alkalmazás felületén.',\n    'app_color' => 'Elsődleges szín',\n    'link_color' => 'Alapértelmezett link szín',\n    'content_colors_desc' => 'Beállítja az elemek színét az oldalszervezési hierarchiában. Az olvashatóság szempontjából javasolt az alapértelmezés szerinti színhez hasonló fényerősséget választani.',\n    'bookshelf_color' => 'Polc színe',\n    'book_color' => 'Könyv színe',\n    'chapter_color' => 'Fejezet színe',\n    'page_color' => 'Oldal színe',\n    'page_draft_color' => 'Oldalvázlat színe',\n\n    // Registration Settings\n    'reg_settings' => 'Regisztráció',\n    'reg_enable' => 'Regisztráció engedélyezése',\n    'reg_enable_toggle' => 'Regisztráció engedélyezése',\n    'reg_enable_desc' => 'Ha a regisztráció engedélyezett, akkor a felhasználó képes lesz bejelentkezni mint az alkalmazás egy felhasználója. Regisztráció után egy egyszerű, alapértelmezés szerinti felhasználói szerepkör lesz hozzárendelve.',\n    'reg_default_role' => 'Regisztráció utáni alapértelmezett felhasználói szerepkör',\n    'reg_enable_external_warning' => 'A fenti beállítási lehetőség nincs használatban, ha külső LDAP vagy SAML hitelesítés aktív. A nem létező tagok felhasználói fiókjai automatikusan létrejönnek ha a használatban lévő külső rendszeren sikeres a hitelesítés.',\n    'reg_email_confirmation' => 'Email megerősítés',\n    'reg_email_confirmation_toggle' => 'Email megerősítés szükséges',\n    'reg_confirm_email_desc' => 'Ha a tartomány korlátozás be van állítva, akkor email megerősítés szükséges és ez a beállítás figyelmen kívül lesz hagyva.',\n    'reg_confirm_restrict_domain' => 'Tartomány korlátozás',\n    'reg_confirm_restrict_domain_desc' => 'Azoknak az email tartományoknak a vesszővel elválasztott listája, melyekre a regisztráció korlátozva lesz. A felhasználók egy emailt fognak kapni, hogy megerősítsék az email címüket mielőtt használni kezdhetnék az alkalmazást.<br>Fontos tudni, hogy a felhasználók a sikeres regisztráció után megváltoztathatják az email címüket.',\n    'reg_confirm_restrict_domain_placeholder' => 'Nincs beállítva korlátozás',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Karbantartás',\n    'maint_image_cleanup' => 'Képek tisztítása',\n    'maint_image_cleanup_desc' => 'Végigolvassa az oldalakat és a tartalmak változatait, hogy leellenőrizze jelenleg mely képek és rajzok vannak használatban, és mely képek szerepelnek többször. A futtatása előtt feltétlen készíteni kell egy teljes adatbázis és lemezkép mentést.',\n    'maint_delete_images_only_in_revisions' => 'Törölje azokat a képeket is, amelyek csak a régi oldalverziókban léteznek',\n    'maint_image_cleanup_run' => 'Tisztítás futtatása',\n    'maint_image_cleanup_warning' => ':count potenciálisan nem használt képet találtam. Biztosan törölhetőek ezek a képek?',\n    'maint_image_cleanup_success' => ':count potenciálisan nem használt kép megtalálva és törölve!',\n    'maint_image_cleanup_nothing_found' => 'Nincsenek nem használt képek, semmi sem lett törölve!',\n    'maint_send_test_email' => 'Teszt e-mail küldése',\n    'maint_send_test_email_desc' => 'Ez elküld egy teszt emailt a profilban megadott email címre.',\n    'maint_send_test_email_run' => 'Teszt e-mail küldése',\n    'maint_send_test_email_success' => 'Email elküldve :address címre',\n    'maint_send_test_email_mail_subject' => 'Teszt e-mail',\n    'maint_send_test_email_mail_greeting' => 'Az email kézbesítés működőképesnek tűnik!',\n    'maint_send_test_email_mail_text' => 'Gratulálunk! Mivel ez az email figyelmeztetés megérkezett az email beállítások megfelelőek.',\n    'maint_recycle_bin_desc' => 'A törölt polcok, könyvek, fejezetek és oldalak a lomtárba kerülnek, így visszaállíthatók vagy véglegesen törölhetők. A rendszer konfigurációtól függően egy idő után a lomtárban lévő régebbi elemek automatikusan eltávolíthatók.',\n    'maint_recycle_bin_open' => 'Lomtár megnyitása',\n    'maint_regen_references' => 'Referenciák újragenerálása',\n    'maint_regen_references_desc' => 'Ez a művelet újraépíti az adatbázison belüli elemek közötti hivatkozási indexet. Ez általában automatikusan történik, de ez a művelet hasznos lehet régi vagy nem hivatalos módszerekkel hozzáadott tartalom indexeléséhez.',\n    'maint_regen_references_success' => 'A referenciaindex újragenerálásra került!',\n    'maint_timeout_command_note' => 'Megjegyzés: Ennek a műveletnek a futtatása időbe telhet, ami bizonyos webes környezetekben időtúllépési problémákhoz vezethet. Alternatív megoldásként ezt a műveletet terminálparancs segítségével is végrehajthatja.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Lomtár',\n    'recycle_bin_desc' => 'Itt visszaállíthatja a törölt elemeket, vagy dönthet úgy, hogy véglegesen eltávolítja őket a rendszerből. Ez a lista nem szűrhető, ellentétben a rendszer hasonló tevékenységlistáival, ahol engedélyszűrőket alkalmaznak.',\n    'recycle_bin_deleted_item' => 'Törölt elem',\n    'recycle_bin_deleted_parent' => 'Szülő',\n    'recycle_bin_deleted_by' => 'Törölte',\n    'recycle_bin_deleted_at' => 'Törlés ideje',\n    'recycle_bin_permanently_delete' => 'Végleges törlés',\n    'recycle_bin_restore' => 'Visszaállítás',\n    'recycle_bin_contents_empty' => 'A lomtár jelenleg üres',\n    'recycle_bin_empty' => 'Lomtár kiürítése',\n    'recycle_bin_empty_confirm' => 'Ezzel véglegesen megsemmisíti a lomtárban lévő összes elemet, beleértve az egyes tételekben található tartalmat is. Biztos benne, hogy ki akarja üríteni a lomtárat?',\n    'recycle_bin_destroy_confirm' => 'Ez a művelet véglegesen törli ezt az elemet a rendszerből az alább felsorolt összes alárendelt elemmel együtt, és nem fogja tudni visszaállítani ezt a tartalmat. Biztosan véglegesen törli ezt az elemet?',\n    'recycle_bin_destroy_list' => 'Megsemmisítendő elemek',\n    'recycle_bin_restore_list' => 'Visszaállítandó elemek',\n    'recycle_bin_restore_confirm' => 'Ez a művelet visszaállítja a törölt elemet, beleértve az utódelemeket is, az eredeti helyükre. Ha az eredeti helyet azóta törölték, és most a lomtárban van, akkor a szülőelemet is vissza kell állítani.',\n    'recycle_bin_restore_deleted_parent' => 'Ennek az elemnek a szülője is törölve lett. Ezek mindaddig törölve maradnak, amíg az adott szülőt is vissza nem állítják.',\n    'recycle_bin_restore_parent' => 'Szűlő visszaállítása',\n    'recycle_bin_destroy_notification' => 'Összesen :count elemet törölt a lomtárból.',\n    'recycle_bin_restore_notification' => 'Összesen :count elemet helyreállítottak a lomtárból.',\n\n    // Audit Log\n    'audit' => 'Audit napló',\n    'audit_desc' => 'Ez a napló a rendszerben nyomon követett tevékenységek listáját jeleníti meg. Ez a lista nem szűrhető, ellentétben a rendszer hasonló tevékenységlistáival, ahol engedélyszűrőket alkalmaznak.',\n    'audit_event_filter' => 'Eseményszűrő',\n    'audit_event_filter_no_filter' => 'Nincs szűrő',\n    'audit_deleted_item' => 'Törölt elem',\n    'audit_deleted_item_name' => 'Név: :name',\n    'audit_table_user' => 'Felhasználó',\n    'audit_table_event' => 'Esemény',\n    'audit_table_related' => 'Kapcsolódó elem vagy részlet',\n    'audit_table_ip' => 'IP Cím',\n    'audit_table_date' => 'Tevékenység időpontja',\n    'audit_date_from' => 'Kezdő dátum',\n    'audit_date_to' => 'Végdátum',\n\n    // Role Settings\n    'roles' => 'Szerepkörök',\n    'role_user_roles' => 'Felhasználói szerepkörök',\n    'roles_index_desc' => 'A szerepkörök a felhasználók csoportosítására és rendszerengedélyek biztosítására szolgálnak tagjaiknak. Ha egy felhasználó több szerepkör tagja, a megadott jogosultságok halmozódnak, és a felhasználó örökli az összes képességet.',\n    'roles_x_users_assigned' => ':count hozzárendelt felhasználó|:count hozzárendelt felhasználó',\n    'roles_x_permissions_provided' => ':count jogosultság|:count jogosultság',\n    'roles_assigned_users' => 'Hozzárendelt felhasználók',\n    'roles_permissions_provided' => 'Megadott jogosultságok',\n    'role_create' => 'Új szerepkör létrehozása',\n    'role_delete' => 'Szerepkör törlése',\n    'role_delete_confirm' => 'Ez törölni fogja \\':roleName\\' szerepkört.',\n    'role_delete_users_assigned' => 'Ehhez a szerepkörhöz :userCount felhasználó van hozzárendelve. Ha a felhasználókat át kell helyezni ebből a szerepkörből, akkor ki kell választani egy új szerepkört.',\n    'role_delete_no_migration' => \"Nincs felhasználó áthelyezés\",\n    'role_delete_sure' => 'Biztosan törölhető ez a szerepkör?',\n    'role_edit' => 'Szerepkör szerkesztése',\n    'role_details' => 'Szerepkör részletei',\n    'role_name' => 'Szerepkör neve',\n    'role_desc' => 'Szerepkör rövid leírása',\n    'role_mfa_enforced' => 'Kétlépcsős hitelesítés megkövetelése',\n    'role_external_auth_id' => 'Külső hitelesítés azonosítók',\n    'role_system' => 'Rendszer jogosultságok',\n    'role_manage_users' => 'Felhasználók kezelése',\n    'role_manage_roles' => 'Szerepkörök és szerepkör engedélyek kezelése',\n    'role_manage_entity_permissions' => 'Minden könyv, fejezet és oldalengedély kezelése',\n    'role_manage_own_entity_permissions' => 'Saját könyv, fejezet és oldalak engedélyeinek kezelése',\n    'role_manage_page_templates' => 'Oldalsablonok kezelése',\n    'role_access_api' => 'Hozzáférés a rendszer API-hoz',\n    'role_manage_settings' => 'Alkalmazás beállításainak kezelése',\n    'role_export_content' => 'Tartalom exportálása',\n    'role_import_content' => 'Import content',\n    'role_editor_change' => 'Oldalszerkesztő módosítása',\n    'role_notifications' => 'Értesítések fogadása és kezelése',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Eszköz jogosultságok',\n    'roles_system_warning' => 'Ne feledje, hogy a fenti három engedély bármelyikéhez való hozzáférés lehetővé teszi a felhasználó számára, hogy módosítsa saját vagy a rendszerben mások jogosultságait. Csak megbízható felhasználókhoz rendeljen szerepeket ezekkel az engedélyekkel.',\n    'role_asset_desc' => 'Ezek a jogosultságok vezérlik az alapértelmezés szerinti hozzáférést a rendszerben található eszközökhöz. A könyvek, fejezetek és oldalak jogosultságai felülírják ezeket a jogosultságokat.',\n    'role_asset_admins' => 'Az adminisztrátorok automatikusan hozzáférést kapnak minden tartalomhoz, de ezek a beállítások megjeleníthetnek vagy elrejthetnek felhasználói felület beállításokat.',\n    'role_asset_image_view_note' => 'Ez a képkezelőn belüli láthatóságra vonatkozik. A feltöltött képfájlok tényleges elérése a rendszerkép tárolási beállításától függ.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'Összes',\n    'role_own' => 'Saját',\n    'role_controlled_by_asset' => 'Az általuk feltöltött eszköz által ellenőrzött',\n    'role_save' => 'Szerepkör mentése',\n    'role_users' => 'Felhasználók ebben a szerepkörben',\n    'role_users_none' => 'Jelenleg nincsenek felhasználók hozzárendelve ehhez a szerepkörhöz',\n\n    // Users\n    'users' => 'Felhasználók',\n    'users_index_desc' => 'Egyéni felhasználói fiókok létrehozása és kezelése a rendszeren belül. A felhasználói fiókok a bejelentkezéshez, valamint a tartalom és tevékenység hozzárendeléséhez használatosak. A hozzáférési engedélyek elsősorban szerepalapúak, de a felhasználói tartalmak tulajdonlása – többek között – befolyásolhatja az engedélyeket és a hozzáférést.',\n    'user_profile' => 'Felhasználói profil',\n    'users_add_new' => 'Új felhasználó hozzáadása',\n    'users_search' => 'Felhasználók keresése',\n    'users_latest_activity' => 'Legújabb tevékenység',\n    'users_details' => 'Felhasználó részletei',\n    'users_details_desc' => 'Egy megjelenítendő név és email cím beállítása ennek a felhasználónak. Az email cím az alkalmazásba történő bejelentkezéshez lesz használva.',\n    'users_details_desc_no_email' => 'Egy megjelenítendő név beállítása ennek a felhasználónak amiről mások felismerik.',\n    'users_role' => 'Felhasználói szerepkörök',\n    'users_role_desc' => 'A felhasználó melyik szerepkörhöz lesz rendelve. Ha a felhasználó több szerepkörhöz van rendelve, akkor ezeknek a szerepköröknek a jogosultságai összeadódnak, és a a felhasználó a hozzárendelt szerepkörök minden képességét megkapja.',\n    'users_password' => 'Felhasználó jelszava',\n    'users_password_desc' => 'Az alkalmazásba bejelentkezéshez használható jelszó beállítása. Legalább 8 karakter hosszúnak kell lennie.',\n    'users_send_invite_text' => 'Lehetséges egy meghívó emailt küldeni ennek a felhasználónak ami lehetővé teszi, hogy beállíthassa a saját jelszavát. Máskülönben a jelszót az erre jogosult felhasználónak kell beállítania.',\n    'users_send_invite_option' => 'Felhasználó meghívó levél küldése',\n    'users_external_auth_id' => 'Külső hitelesítés azonosítója',\n    'users_external_auth_id_desc' => 'Ha külső hitelesítési rendszer van használatban (például SAML2, OIDC vagy LDAP), ez az az azonosító, amely a BookStack felhasználót a hitelesítési rendszerfiókhoz kapcsolja. Ha az alapértelmezett e-mail alapú hitelesítést használja, figyelmen kívül hagyhatja ezt a mezőt.',\n    'users_password_warning' => 'Csak akkor töltse ki az alábbi mezőt, ha módosítani szeretné ennek a felhasználónak a jelszavát.',\n    'users_system_public' => 'Ez a felhasználó bármelyik, a példányt megtekintő felhasználót képviseli. Nem lehet vele bejelentkezni de automatikusan hozzá lesz rendelve.',\n    'users_delete' => 'Felhasználó törlése',\n    'users_delete_named' => ':userName felhasználó törlése',\n    'users_delete_warning' => '\\':userName\\' felhasználó teljesen törölve lesz a rendszerből.',\n    'users_delete_confirm' => 'Biztosan törölhető ez a felhasználó?',\n    'users_migrate_ownership' => 'Tulajdonjog átruházása',\n    'users_migrate_ownership_desc' => 'Válasszon itt egy felhasználót, ha azt szeretné, hogy egy másik felhasználó legyen a tulajdonosa az összes, jelenleg a felhasználó tulajdonában lévő elemnek.',\n    'users_none_selected' => 'Nincs felhasználó kiválasztva',\n    'users_edit' => 'Felhasználó szerkesztése',\n    'users_edit_profile' => 'Profil szerkesztése',\n    'users_avatar' => 'Avatar használata',\n    'users_avatar_desc' => 'A felhasználót ábrázoló kép kiválasztása. Kb. 256px méretű négyzetes képnek kell lennie.',\n    'users_preferred_language' => 'Előnyben részesített nyelv',\n    'users_preferred_language_desc' => 'Ez a beállítás megváltoztatja az alkalmazás felhasználói felületén használt nyelvet. Nincs hatása a felhasználók által létrehozott tartalomra.',\n    'users_social_accounts' => 'Közösségi fiókok',\n    'users_social_accounts_desc' => 'Tekintse meg a felhasználó csatlakoztatott közösségi fiókjainak állapotát. A közösségi fiókok az elsődleges hitelesítési rendszer mellett használhatók a rendszerhez való hozzáféréshez.',\n    'users_social_accounts_info' => 'Itt lehet egyéb fiókokat hozzákapcsolni a gyorsabb és könnyebb bejelentkezés érdekében. Itt olyan fiókot lehet lecsatlakoztatni, melynek korábban nem volt engedélyezett hozzáférése. Visszavonja a hozzáférést a csatlakoztatott szociális fiók profilbeállításaiból.',\n    'users_social_connect' => 'Fiók csatlakoztatása',\n    'users_social_disconnect' => 'Fiók lecsatlakoztatása',\n    'users_social_status_connected' => 'Csatlakozva',\n    'users_social_status_disconnected' => 'Lecsatlakozva',\n    'users_social_connected' => ':socialAccount fiók sikeresen csatlakoztatva a profilhoz.',\n    'users_social_disconnected' => ':socialAccount fiók sikeresen lecsatlakoztatva a profilról.',\n    'users_api_tokens' => 'API vezérjelek',\n    'users_api_tokens_desc' => 'A BookStack REST API-val történő hitelesítéshez használt hozzáférési token létrehozása és kezelése. Az API engedélyeit azon a felhasználón keresztül kezelik, akihez a token tartozik.',\n    'users_api_tokens_none' => 'Ehhez a felhasználóhoz nincsenek létrehozva API vezérjelek',\n    'users_api_tokens_create' => 'Vezérjel létrehozása',\n    'users_api_tokens_expires' => 'Lejárat',\n    'users_api_tokens_docs' => 'API dokumentáció',\n    'users_mfa' => 'Többfaktoros hitelesítés',\n    'users_mfa_desc' => 'Állítsa be a többlépcsős azonosítást egy extra biztonsági rétegként a felhasználói fiókjához.',\n    'users_mfa_x_methods' => ':count metódus konfigurálva|:count metódus konfigurálva',\n    'users_mfa_configure' => 'Módszer beállítása',\n\n    // API Tokens\n    'user_api_token_create' => 'API vezérjel létrehozása',\n    'user_api_token_name' => 'Név',\n    'user_api_token_name_desc' => 'Adjon a tokennek egy olvasható nevet, hogy a jövőben emlékeztessen a tervezett céljára.',\n    'user_api_token_expiry' => 'Lejárati dátum',\n    'user_api_token_expiry_desc' => 'Dátum megadása ameddig a vezérjel érvényes. Ez után a dátum után az ezzel a vezérjellel történő kérések nem fognak működni. Üresen hagyva a lejárati idő 100 évre lesz beállítva.',\n    'user_api_token_create_secret_message' => 'Közvetlenül a token létrehozása után egy „Token ID” és „Token Secret” generálódik és jelenik meg. A Secret csak egyszer jelenik meg, ezért a folytatás előtt másolja át az értéket egy biztonságos helyre.',\n    'user_api_token' => 'API vezérjel',\n    'user_api_token_id' => 'Vezérjel azonosító',\n    'user_api_token_id_desc' => 'Ez egy nem szerkeszthető, a rendszer által létrehozott azonosító ehhez a vezérjelhez amire API kérésekben lehet szükség.',\n    'user_api_token_secret' => 'Vezérjel titkos kódja',\n    'user_api_token_secret_desc' => 'Ez egy rendszer által generált \"secret\" ehhez a tokenhez, amelyet meg kell adni az API-kérésekben. Ez csak most jelenik meg, ezért másolja ezt az értéket egy biztonságos helyre.',\n    'user_api_token_created' => 'Vezérjel létrehozva :timeAgo',\n    'user_api_token_updated' => 'Vezérjel frissítve :timeAgo',\n    'user_api_token_delete' => 'Vezérjel törlése',\n    'user_api_token_delete_warning' => '\\':tokenName\\' nevű API vezérjel teljesen törölve lesz a rendszerből.',\n    'user_api_token_delete_confirm' => 'Biztosan törölhető ez az API vezérjel?',\n\n    // Webhooks\n    'webhooks' => 'Webhook-ok',\n    'webhooks_index_desc' => 'A webhookok segítségével adatokat küldhetünk külső URL-ekre, amikor bizonyos műveletek és események történnek a rendszeren belül, ami lehetővé teszi az eseményalapú integrációt külső platformokkal, például üzenetküldő vagy értesítési rendszerekkel.',\n    'webhooks_x_trigger_events' => ':count kiváltó esemény|:count kiváltó esemény',\n    'webhooks_create' => 'Új webhook létrehozása',\n    'webhooks_none_created' => 'Még nincs létrehozva egy webhook sem.',\n    'webhooks_edit' => 'Webhook szerkesztése',\n    'webhooks_save' => 'Webhook mentése',\n    'webhooks_details' => 'Webhook részletei',\n    'webhooks_details_desc' => 'Adjon meg egy felhasználóbarát nevet és egy POST-végpontot a webhook-adatok elküldésének helyeként.',\n    'webhooks_events' => 'Webhook események',\n    'webhooks_events_desc' => 'Jelölje ki az összes eseményt, amely kiváltja a webhook meghívását.',\n    'webhooks_events_warning' => 'Ne feledje, hogy ezek az események az összes kiválasztott eseménynél aktiválódnak, még akkor is, ha egyéni engedélyeket alkalmaznak. Győződjön meg arról, hogy a webhook használata nem tesz közzé bizalmas tartalmat.',\n    'webhooks_events_all' => 'Minden rendszeresemény',\n    'webhooks_name' => 'Webhook neve',\n    'webhooks_timeout' => 'Webhook kérés időtúllépése (másodperc)',\n    'webhooks_endpoint' => 'Webhook végpont',\n    'webhooks_active' => 'Webhook aktív',\n    'webhook_events_table_header' => 'Események',\n    'webhooks_delete' => 'Webhook törlése',\n    'webhooks_delete_warning' => 'Ezzel a \\':webhookName\\' nevű webhookot teljesen törli a rendszerből.',\n    'webhooks_delete_confirm' => 'Biztosan törli ezt a webhookot?',\n    'webhooks_format_example' => 'Webhook formátum példa',\n    'webhooks_format_example_desc' => 'A Webhook-adatok POST-kérésként kerülnek elküldésre a konfigurált végponthoz JSON-ként az alábbi formátumban. A \"related_item\" és az \"url\" tulajdonság nem kötelező, és az aktivált esemény típusától függ.',\n    'webhooks_status' => 'Webhook állapota',\n    'webhooks_last_called' => 'Utolsó hívás:',\n    'webhooks_last_errored' => 'Utolsó hiba:',\n    'webhooks_last_error_message' => 'Utolsó hibaüzenet:',\n\n    // Licensing\n    'licenses' => 'Licenszek',\n    'licenses_desc' => 'Ez az oldal a BookStack licencinformációit részletezi, a BookStackben használt projekteken és könyvtárakon kívül. Sok felsorolt projekt csak fejlesztési környezetben használható.',\n    'licenses_bookstack' => 'BookStack Licensz',\n    'licenses_php' => 'PHP könyvtár licencek',\n    'licenses_js' => 'JavaScript könyvtár licencek',\n    'licenses_other' => 'Egyéb licencek',\n    'license_details' => 'Licenc részletek',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/hu/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute elfogadott kell legyen.',\n    'active_url'           => ':attribute nem érvényes webcím.',\n    'after'                => ':attribute dátumnak :date utáninak kell lennie.',\n    'alpha'                => ':attribute csak betűket tartalmazhat.',\n    'alpha_dash'           => ':attribute csak betűket, számokat és kötőjeleket tartalmazhat.',\n    'alpha_num'            => ':attribute csak betűket és számokat tartalmazhat.',\n    'array'                => ':attribute tömb kell legyen.',\n    'backup_codes'         => 'A megadott kód érvénytelen, vagy már felhasználták.',\n    'before'               => ':attribute dátumnak :date előttinek kell lennie.',\n    'between'              => [\n        'numeric' => ':attribute értékének :min és :max között kell lennie.',\n        'file'    => ':attribute értékének :min és :max kilobájt között kell lennie.',\n        'string'  => ':attribute hosszának :min és :max karakter között kell lennie.',\n        'array'   => ':attribute mennyiségének :min és :max elem között kell lennie.',\n    ],\n    'boolean'              => ':attribute mezőnek igaznak vagy hamisnak kell lennie.',\n    'confirmed'            => ':attribute megerősítés nem egyezik.',\n    'date'                 => ':attribute nem érvényes dátum.',\n    'date_format'          => ':attribute nem egyezik :format formátummal.',\n    'different'            => ':attribute és :other értékének különböznie kell.',\n    'digits'               => ':attribute :digits számból kell álljon.',\n    'digits_between'       => ':attribute hosszának :min és :max számjegy között kell lennie.',\n    'email'                => ':attribute érvényes email cím kell legyen.',\n    'ends_with' => ':attribute attribútumnak a következők egyikével kell végződnie: :values',\n    'file'                 => 'A(z) :attribute érvényes fájlnak kell lennie.',\n    'filled'               => ':attribute mező kötelező.',\n    'gt'                   => [\n        'numeric' => ':attribute nagyobb kell, hogy legyen, mint :value.',\n        'file'    => ':attribute nagyobb kell, hogy legyen, mint :value kilobájt.',\n        'string'  => ':attribute nagyobb kell legyen mint :value karakter.',\n        'array'   => ':attribute több, mint :value elemet kell, hogy tartalmazzon.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute attribútumnak :value értéknél nagyobbnak vagy vele egyenlőnek kell lennie.',\n        'file'    => 'A(z) :attribute mérete nem lehet kevesebb, mint :value kilobájt.',\n        'string'  => 'A(z) :attribute nagyobbnak, vagy egyenlőnek kell lennie, mint a :value karakter.',\n        'array'   => 'A(z) :attribute rendelkezzen :value vagy több elemmel.',\n    ],\n    'exists'               => 'A kiválasztott :attribute érvénytelen.',\n    'image'                => ':attribute kép kell legyen.',\n    'image_extension'      => 'A :attribute kép kiterjesztése érvényes és támogatott kell legyen.',\n    'in'                   => 'A kiválasztott :attribute érvénytelen.',\n    'integer'              => ':attribute egész szám kell legyen.',\n    'ip'                   => ':attribute érvényes IP cím kell legyen.',\n    'ipv4'                 => 'A(z) :attribute érvényes IPv4 címnek kell lennie.',\n    'ipv6'                 => 'A(z) :attribute érvényes IPv6 címnek kell lennie.',\n    'json'                 => 'A(z) :attribute érvényes JSON stringnek kell lennie.',\n    'lt'                   => [\n        'numeric' => 'A(z) :attribute kisebb kell, hogy legyen, mint :value.',\n        'file'    => 'A(z) :attribute kevesebbnek kell lennie, mint :value kilobájt.',\n        'string'  => 'A(z) :attribute rövidebb kell, hogy legyen, mint :value karakter.',\n        'array'   => 'A(z) :attribute kevesebb, mint :value elemet kell, hogy tartalmazzon.',\n    ],\n    'lte'                  => [\n        'numeric' => 'A(z) :attribute kisebb vagy egyenlő kell, hogy legyen, mint :value.',\n        'file'    => 'A(z) :attribute mérete nem lehet több, mint :value kilobájt.',\n        'string'  => 'A(z) :attribute hossza nem lehet több, mint :value karakter.',\n        'array'   => 'A(z) :attribute legfeljebb :value elemet kell, hogy tartalmazzon.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute nem lehet nagyobb mint :max.',\n        'file'    => ':attribute nem lehet nagyobb mint :max kilobájt.',\n        'string'  => ':attribute nem lehet nagyobb mint :max karakter.',\n        'array'   => ':attribute mennyisége nem lehet több mint :max elem.',\n    ],\n    'mimes'                => 'A :attribute típusa csak :values lehet.',\n    'min'                  => [\n        'numeric' => ':attribute legalább :min kell legyen.',\n        'file'    => ':attribute legalább :min kilobájt kell legyen.',\n        'string'  => ':attribute legalább :min karakter kell legyen.',\n        'array'   => ':attribute legalább :min elem kell legyen.',\n    ],\n    'not_in'               => 'A kiválasztott :attribute érvénytelen.',\n    'not_regex'            => ':attribute formátuma érvénytelen.',\n    'numeric'              => ':attribute szám kell legyen.',\n    'regex'                => ':attribute formátuma érvénytelen.',\n    'required'             => ':attribute mező kötelező.',\n    'required_if'          => ':attribute mező kötelező ha :other értéke :value.',\n    'required_with'        => ':attribute mező kötelező ha :values be van állítva.',\n    'required_with_all'    => ':attribute mező kötelező ha van :value.',\n    'required_without'     => ':attribute mező kötelező ha :values nincs beállítva.',\n    'required_without_all' => ':attribute mező kötelező ha egyik :values sincs beállítva.',\n    'same'                 => ':attribute és :other értékének egyeznie kell.',\n    'safe_url'             => 'Előfordulhat, hogy a megadott link nem biztonságos.',\n    'size'                 => [\n        'numeric' => ':attribute :size méretű kell legyen.',\n        'file'    => ':attribute :size kilobájt méretű kell legyen.',\n        'string'  => ':attribute :size karakter kell legyen.',\n        'array'   => ':attribute : size elemet kell tartalmazzon.',\n    ],\n    'string'               => ':attribute karaktersorozatnak kell legyen.',\n    'timezone'             => ':attribute érvényes zóna kell legyen.',\n    'totp'                 => 'A megadott kód érvénytelen vagy lejárt.',\n    'unique'               => ':attribute már elkészült.',\n    'url'                  => ':attribute formátuma érvénytelen.',\n    'uploaded'             => 'A fájlt nem lehet feltölteni. A kiszolgáló nem fogad el ilyen méretű fájlokat.',\n\n    'zip_file' => 'The :attribute needs to reference a file within the ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',\n    'zip_model_expected' => 'Data object expected but \":type\" found.',\n    'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Jelszó megerősítés szükséges',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/id/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'telah membuat halaman',\n    'page_create_notification'    => 'Jenis Halaman berhasil dibuat',\n    'page_update'                 => 'halaman telah diperbaharui',\n    'page_update_notification'    => 'Halaman berhasil diperbarui',\n    'page_delete'                 => 'halaman dihapus',\n    'page_delete_notification'    => 'Halaman berhasil dihapus',\n    'page_restore'                => 'halaman telah dipulihkan',\n    'page_restore_notification'   => 'Halaman berhasil dipulihkan',\n    'page_move'                   => 'halaman dipindahkan',\n    'page_move_notification'      => 'Halaman berhasil dipindahkan',\n\n    // Chapters\n    'chapter_create'              => 'membuat bab',\n    'chapter_create_notification' => 'Bab berhasil dibuat',\n    'chapter_update'              => 'bab diperbaharui',\n    'chapter_update_notification' => 'Bab berhasil diperbarui',\n    'chapter_delete'              => 'hapus bab',\n    'chapter_delete_notification' => 'Bab berhasil dihapus',\n    'chapter_move'                => 'bab dipindahkan',\n    'chapter_move_notification' => 'Bab berhasil dipindahkan',\n\n    // Books\n    'book_create'                 => 'membuat buku',\n    'book_create_notification'    => 'Buku berhasil dibuat',\n    'book_create_from_chapter'              => 'mengkonversi bab ke buku',\n    'book_create_from_chapter_notification' => 'Bab berhasil dikonversi menjadi buku',\n    'book_update'                 => 'update buku',\n    'book_update_notification'    => 'Buku berhasil diperbarui',\n    'book_delete'                 => 'hapus buku',\n    'book_delete_notification'    => 'Buku berhasil dihapus',\n    'book_sort'                   => 'buku yang diurutkan',\n    'book_sort_notification'      => 'Buku berhasil diurutkan',\n\n    // Bookshelves\n    'bookshelf_create'            => 'membuat rak',\n    'bookshelf_create_notification'    => 'Rak berhasil dibuat',\n    'bookshelf_create_from_book'    => 'mengkonversi buku ke rak',\n    'bookshelf_create_from_book_notification'    => 'Buku berhasil dikonversi menjadi rak',\n    'bookshelf_update'                 => 'memperbarui rak',\n    'bookshelf_update_notification'    => 'Rak berhasil diperbarui',\n    'bookshelf_delete'                 => 'menghapus rak',\n    'bookshelf_delete_notification'    => 'Rak berhasil dihapus',\n\n    // Revisions\n    'revision_restore' => 'revisi yang dipulihkan',\n    'revision_delete' => 'revisi yang dihapus',\n    'revision_delete_notification' => 'Revisi berhasil dihapus',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" telah ditambahkan ke favorit Anda',\n    'favourite_remove_notification' => '\":name\" telah dihapus dari favorit Anda',\n\n    // Watching\n    'watch_update_level_notification' => 'Preferensi pantauan berhasil diperbarui',\n\n    // Auth\n    'auth_login' => 'telah masuk',\n    'auth_register' => 'daftar sebagai user baru',\n    'auth_password_reset_request' => 'permintaan pengguna mengatur ulang kata sandi',\n    'auth_password_reset_update' => 'atur ulang kata sandi pengguna',\n    'mfa_setup_method' => 'metode MFA yang dikonfigurasi',\n    'mfa_setup_method_notification' => 'Metode multi-faktor sukses dikonfigurasi',\n    'mfa_remove_method' => 'menghapus metode MFA',\n    'mfa_remove_method_notification' => 'Metode multi-faktor sukses dihapus',\n\n    // Settings\n    'settings_update' => 'memperbarui setelan',\n    'settings_update_notification' => 'Pengaturan berhasil diperbarui',\n    'maintenance_action_run' => 'menjalankan tindakan pemeliharaan',\n\n    // Webhooks\n    'webhook_create' => 'membuat webhook',\n    'webhook_create_notification' => 'Webhook berhasil dibuat',\n    'webhook_update' => 'memperbarui webhook',\n    'webhook_update_notification' => 'Webhook berhasil diperbarui',\n    'webhook_delete' => 'menghapus webhook',\n    'webhook_delete_notification' => 'Webhook berhasil dihapus',\n\n    // Imports\n    'import_create' => 'telat membuat impor',\n    'import_create_notification' => 'Impor berhasil diunggah',\n    'import_run' => 'telah memperbarui impor',\n    'import_run_notification' => 'Konten berhasil diimpor',\n    'import_delete' => 'telah menghapus impor',\n    'import_delete_notification' => 'Impor berhasil dihapus',\n\n    // Users\n    'user_create' => 'pengguna yang dibuat',\n    'user_create_notification' => 'Pengguna berhasil dibuat',\n    'user_update' => 'perbarui Pengguna',\n    'user_update_notification' => 'Pengguna berhasil diperbarui',\n    'user_delete' => 'pengguna yang dihapus',\n    'user_delete_notification' => 'Pengguna berhasil dihapus',\n\n    // API Tokens\n    'api_token_create' => 'API token yang dibuat',\n    'api_token_create_notification' => 'Token API berhasil dibuat',\n    'api_token_update' => 'token API yang diperbarui',\n    'api_token_update_notification' => 'token API berhasil dirubah',\n    'api_token_delete' => 'token API yang dihapus',\n    'api_token_delete_notification' => 'token API berhasil dihapus ',\n\n    // Roles\n    'role_create' => 'telah membuat peran',\n    'role_create_notification' => 'Peran berhasil dibuat',\n    'role_update' => 'telah memperbarui peran',\n    'role_update_notification' => 'Peran berhasil diperbarui',\n    'role_delete' => 'telah menghapus peran',\n    'role_delete_notification' => 'Peran berhasil dihapus',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'telah mengosongkan tempat sampah',\n    'recycle_bin_restore' => 'telah mengembalikan dari tempat sampah',\n    'recycle_bin_destroy' => 'telah menghapus dari tempat sampah',\n\n    // Comments\n    'commented_on'                => 'berkomentar pada',\n    'comment_create'              => 'telah menambah komentar',\n    'comment_update'              => 'telah memperbarui komentar',\n    'comment_delete'              => 'telah menghapus komentar',\n\n    // Sort Rules\n    'sort_rule_create' => 'telah membuat aturan penyortiran',\n    'sort_rule_create_notification' => 'Aturan penyortiran berhasil dibuat',\n    'sort_rule_update' => 'telah mengubah aturan penyortiran',\n    'sort_rule_update_notification' => 'Aturan penyortiran berhasil diubah',\n    'sort_rule_delete' => 'telah menghapus aturan penyortiran',\n    'sort_rule_delete_notification' => 'Aturan penyortiran berhasil dihapus',\n\n    // Other\n    'permissions_update'          => 'izin diperbarui',\n];\n"
  },
  {
    "path": "lang/id/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Kredensial tidak cocok dengan catatan kami.',\n    'throttle' => 'Terlalu banyak upaya masuk. Silahkan mencoba lagi dalam :seconds detik.',\n\n    // Login & Register\n    'sign_up' => 'Daftar',\n    'log_in' => 'Gabung',\n    'log_in_with' => 'Masuk dengan :socialDriver',\n    'sign_up_with' => 'Daftar dengan :socialDriver',\n    'logout' => 'Keluar',\n\n    'name' => 'Nama',\n    'username' => 'Nama Pengguna',\n    'email' => 'Email',\n    'password' => 'Kata Sandi',\n    'password_confirm' => 'Konfirmasi Kata Sandi',\n    'password_hint' => 'Harus minimal 8 karakter',\n    'forgot_password' => 'Lupa Password?',\n    'remember_me' => 'Ingat saya',\n    'ldap_email_hint' => 'Harap masukkan email yang akan digunakan untuk akun ini.',\n    'create_account' => 'Membuat Akun',\n    'already_have_account' => 'Sudah punya akun?',\n    'dont_have_account' => 'Tidak punya akun?',\n    'social_login' => 'Masuk dengan sosial media',\n    'social_registration' => 'Daftar dengan sosial media',\n    'social_registration_text' => 'Daftar dan masuk menggunakan layanan lain.',\n\n    'register_thanks' => 'Terima kasih telah mendaftar!',\n    'register_confirm' => 'Silakan periksa email Anda dan klik tombol konfirmasi untuk mengakses :appName.',\n    'registrations_disabled' => 'Pendaftaran saat ini dinonaktifkan',\n    'registration_email_domain_invalid' => 'Domain email tersebut tidak memiliki akses ke aplikasi ini',\n    'register_success' => 'Terima kasih telah mendaftar! Anda sekarang terdaftar dan masuk.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Mencoba masuk',\n    'auto_init_starting_desc' => 'Kami sedang menghubungi sistem autentikasi Anda untuk memulai proses login. Jika tidak ada kemajuan setelah 5 detik, Anda dapat mencoba mengklik link di bawah ini.',\n    'auto_init_start_link' => 'Lanjutkan dengan otentikasi',\n\n    // Password Reset\n    'reset_password' => 'Atur ulang kata sandi',\n    'reset_password_send_instructions' => 'Masukkan email Anda di bawah ini dan Anda akan dikirimi email dengan tautan pengaturan ulang kata sandi.',\n    'reset_password_send_button' => 'Kirim Tautan Atur Ulang',\n    'reset_password_sent' => 'Tautan pengaturan ulang kata sandi akan dikirim ke :email jika alamat email ditemukan di sistem.',\n    'reset_password_success' => 'Kata sandi Anda telah berhasil diatur ulang.',\n    'email_reset_subject' => 'Atur ulang kata sandi :appName anda',\n    'email_reset_text' => 'Anda menerima email ini karena kami menerima permintaan pengaturan ulang kata sandi untuk akun Anda.',\n    'email_reset_not_requested' => 'Jika Anda tidak meminta pengaturan ulang kata sandi, tidak ada tindakan lebih lanjut yang diperlukan.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Konfirmasikan email Anda di :appName',\n    'email_confirm_greeting' => 'Terima kasih telah bergabung :appName!',\n    'email_confirm_text' => 'Silakan konfirmasi alamat email Anda dengan mengklik tombol di bawah ini:',\n    'email_confirm_action' => 'Konfirmasi email',\n    'email_confirm_send_error' => 'Konfirmasi email diperlukan tetapi sistem tidak dapat mengirim email. Hubungi admin untuk memastikan email disiapkan dengan benar.',\n    'email_confirm_success' => 'Email Anda sudah terkonfirmasi! Anda seharusnya sudah bisa masuk menggunakan email ini.',\n    'email_confirm_resent' => 'Email konfirmasi dikirim ulang, Harap periksa kotak masuk Anda.',\n    'email_confirm_thanks' => 'Terima kasih untuk mengkonfirmasi!',\n    'email_confirm_thanks_desc' => 'Harap tunggu sebentar, konfirmasi Anda sedang ditangani. Jika Anda tidak dipindahkan setelah 3 detik, tekan link \"Selanjutnya\" dibawah ini untuk melanjutkan.',\n\n    'email_not_confirmed' => 'Alamat Email Tidak Dikonfirmasi',\n    'email_not_confirmed_text' => 'Alamat email Anda belum dikonfirmasi.',\n    'email_not_confirmed_click_link' => 'Silakan klik link di email yang dikirimkan segera setelah Anda mendaftar.',\n    'email_not_confirmed_resend' => 'Jika Anda tidak dapat menemukan email tersebut, Anda dapat mengirim ulang email konfirmasi dengan mengirimkan formulir di bawah ini.',\n    'email_not_confirmed_resend_button' => 'Mengirimkan kembali email konfirmasi',\n\n    // User Invite\n    'user_invite_email_subject' => 'Anda telah diundang untuk bergabung di :appName!',\n    'user_invite_email_greeting' => 'Sebuah akun telah dibuat untuk Anda di :appName.',\n    'user_invite_email_text' => 'Klik tombol di bawah untuk mengatur kata sandi akun dan mendapatkan akses:',\n    'user_invite_email_action' => 'Atur Kata Sandi Akun',\n    'user_invite_page_welcome' => 'Selamat datang di :appName!',\n    'user_invite_page_text' => 'Untuk menyelesaikan akun Anda dan mendapatkan akses, Anda perlu mengatur kata sandi yang akan digunakan untuk masuk ke :appName pada kunjungan berikutnya.',\n    'user_invite_page_confirm_button' => 'Konfirmasi Kata sandi',\n    'user_invite_success_login' => 'Kata sandi diset, Anda seharusnya sudah bisa masuk menggunakan kata sandi yang sudah diset untuk mengakses :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Atur Multi-Factor Otentikasi',\n    'mfa_setup_desc' => 'Mengatur multi-factor otentikasi sebagai tambahan ekstra keamanan untuk akun Anda.',\n    'mfa_setup_configured' => 'Sudah dikonfigurasi',\n    'mfa_setup_reconfigure' => 'Konfigurasi ulang',\n    'mfa_setup_remove_confirmation' => 'Apakah Anda yakin ingin menghapus metode autentikasi multi-faktor ini?',\n    'mfa_setup_action' => 'Atur',\n    'mfa_backup_codes_usage_limit_warning' => 'Anda memiliki kurang dari 5 kode cadangan yang tersisa. Harap buat dan simpan set baru sebelum Anda kehabisan kode untuk mencegah akun Anda terkunci.',\n    'mfa_option_totp_title' => 'Aplikasi Seluler',\n    'mfa_option_totp_desc' => 'Untuk menggunakan autentikasi multi-faktor, Anda memerlukan aplikasi seluler yang mendukung TOTP seperti Google Authenticator, Authy, atau Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Kode Cadangan',\n    'mfa_option_backup_codes_desc' => 'Menghasilkan serangkaian kode cadangan sekali pakai yang akan Anda masukkan saat masuk untuk memverifikasi identitas Anda. Pastikan untuk menyimpannya di tempat yang aman.',\n    'mfa_gen_confirm_and_enable' => 'Konfirmasi dan Aktifkan',\n    'mfa_gen_backup_codes_title' => 'Pengaturan Kode Cadangan',\n    'mfa_gen_backup_codes_desc' => 'Simpan daftar kode di bawah ini di tempat yang aman. Saat mengakses sistem, Anda dapat menggunakan salah satu kode sebagai mekanisme autentikasi kedua.',\n    'mfa_gen_backup_codes_download' => 'Unduh Kode',\n    'mfa_gen_backup_codes_usage_warning' => 'Setiap kode hanya dapat digunakan satu kali',\n    'mfa_gen_totp_title' => 'Pengaturan Aplikasi Seluler',\n    'mfa_gen_totp_desc' => 'Untuk menggunakan autentikasi multi-faktor, Anda memerlukan aplikasi seluler yang mendukung TOTP seperti Google Authenticator, Authy, atau Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Pindai kode QR di bawah ini menggunakan aplikasi autentikasi pilihan Anda untuk memulai.',\n    'mfa_gen_totp_verify_setup' => 'Verifikasi Pengaturan',\n    'mfa_gen_totp_verify_setup_desc' => 'Verifikasi bahwa semuanya berfungsi dengan memasukkan kode yang dibuat dalam aplikasi autentikasi Anda pada kolom input di bawah ini:',\n    'mfa_gen_totp_provide_code_here' => 'Berikan kode yang dihasilkan aplikasi Anda di sini',\n    'mfa_verify_access' => 'Verifikasi Akses',\n    'mfa_verify_access_desc' => 'Akun pengguna Anda mengharuskan Anda mengonfirmasi identitas Anda melalui tingkat verifikasi tambahan sebelum Anda diberikan akses. Verifikasi menggunakan salah satu metode yang telah Anda konfigurasikan untuk melanjutkan.',\n    'mfa_verify_no_methods' => 'Tidak Ada Metode yang Dikonfigurasi',\n    'mfa_verify_no_methods_desc' => 'Tidak ada metode autentikasi multi-faktor yang ditemukan untuk akun Anda. Anda perlu menyiapkan setidaknya satu metode sebelum mendapatkan akses.',\n    'mfa_verify_use_totp' => 'Verifikasi menggunakan aplikasi seluler',\n    'mfa_verify_use_backup_codes' => 'Verifikasi menggunakan kode cadangan',\n    'mfa_verify_backup_code' => 'Kode Cadangan',\n    'mfa_verify_backup_code_desc' => 'Masukkan salah satu kode cadangan Anda yang tersisa di bawah ini:',\n    'mfa_verify_backup_code_enter_here' => 'Masukkan kode cadangan di sini',\n    'mfa_verify_totp_desc' => 'Masukkan kode yang dibuat menggunakan aplikasi seluler Anda di bawah ini:',\n    'mfa_setup_login_notification' => 'Metode multi-faktor dikonfigurasi. Silakan masuk lagi menggunakan metode yang dikonfigurasi.',\n];\n"
  },
  {
    "path": "lang/id/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Batal',\n    'close' => 'Tutup',\n    'confirm' => 'Konfirmasi',\n    'back' => 'Kembali',\n    'save' => 'Simpan',\n    'continue' => 'Lanjutkan',\n    'select' => 'Pilih',\n    'toggle_all' => 'Alihkan Semua',\n    'more' => 'Lebih banyak',\n\n    // Form Labels\n    'name' => 'Nama',\n    'description' => 'Deskripsi',\n    'role' => 'Peran',\n    'cover_image' => 'Sampul gambar',\n    'cover_image_description' => 'Gambar ini harus berukuran sekitar 440x250px walaupun nanti akan disesuaikan & terpotong secara fleksibel agar sesuai dengan tampilan pengguna, sehingga dimensi sebenarnya untuk tampilan akan berbeda.',\n\n    // Actions\n    'actions' => 'Tindakan',\n    'view' => 'Lihat',\n    'view_all' => 'Lihat Semua',\n    'new' => 'Buat Baru',\n    'create' => 'Buat',\n    'update' => 'Perbarui',\n    'edit' => 'Sunting',\n    'archive' => 'Buat Arsip',\n    'unarchive' => 'Batalkan Arsip',\n    'sort' => 'Sortir',\n    'move' => 'Pindahkan',\n    'copy' => 'Salin',\n    'reply' => 'Balas',\n    'delete' => 'Hapus',\n    'delete_confirm' => 'Konfirmasi Penghapusan',\n    'search' => 'Cari',\n    'search_clear' => 'Hapus Pencarian',\n    'reset' => 'Atur ulang',\n    'remove' => 'Hapus',\n    'add' => 'Tambah',\n    'configure' => 'Atur',\n    'manage' => 'Kelola',\n    'fullscreen' => 'Layar Penuh',\n    'favourite' => 'Favorit',\n    'unfavourite' => 'Batal favorit',\n    'next' => 'Selanjutnya',\n    'previous' => 'Sebelumnya',\n    'filter_active' => 'Filter Aktif:',\n    'filter_clear' => 'Hapus Filter',\n    'download' => 'Unduh',\n    'open_in_tab' => 'Buka di tab baru',\n    'open' => 'Buka',\n\n    // Sort Options\n    'sort_options' => 'Opsi Sortir',\n    'sort_direction_toggle' => 'Urutkan Arah Alihan',\n    'sort_ascending' => 'Sortir Menaik',\n    'sort_descending' => 'Sortir Menurun',\n    'sort_name' => 'Nama',\n    'sort_default' => 'Bawaan',\n    'sort_created_at' => 'Tanggal Dibuat',\n    'sort_updated_at' => 'Tanggal Diperbarui',\n\n    // Misc\n    'deleted_user' => 'Pengguna yang Dihapus',\n    'no_activity' => 'Tidak ada aktivitas untuk ditampilkan',\n    'no_items' => 'Tidak ada item yang tersedia',\n    'back_to_top' => 'Kembali ke atas',\n    'skip_to_main_content' => 'Lewatkan ke konten utama',\n    'toggle_details' => 'Rincian Alihan',\n    'toggle_thumbnails' => 'Alihkan Gambar Mini',\n    'details' => 'Rincian',\n    'grid_view' => 'Tampilan Bergaris',\n    'list_view' => 'Tampilan Daftar',\n    'default' => 'Bawaan',\n    'breadcrumb' => 'Breadcrumb',\n    'status' => 'Status',\n    'status_active' => 'Aktif',\n    'status_inactive' => 'Tidak Aktif',\n    'never' => 'Jangan Pernah',\n    'none' => 'Tidak Satupun',\n\n    // Header\n    'homepage' => 'Beranda',\n    'header_menu_expand' => 'Perluas Menu Tajuk',\n    'profile_menu' => 'Menu Profil',\n    'view_profile' => 'Tampilkan Profil',\n    'edit_profile' => 'Sunting Profil',\n    'dark_mode' => 'Mode Gelap',\n    'light_mode' => 'Mode Terang',\n    'global_search' => 'Pencarian Global',\n\n    // Layout tabs\n    'tab_info' => 'Informasi',\n    'tab_info_label' => 'Tab: Tampilkan Informasi Sekunder',\n    'tab_content' => 'Konten',\n    'tab_content_label' => 'Tab: Tampilkan Informasi Utama',\n\n    // Email Content\n    'email_action_help' => 'Jika Anda mengalami masalah saat mengklik tombol \":actionText\", salin dan tempel URL di bawah ke dalam peramban web Anda:',\n    'email_rights' => 'Hak cipta dilindungi',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Kebijakan Privasi',\n    'terms_of_service' => 'Ketentuan Layanan',\n\n    // OpenSearch\n    'opensearch_description' => 'Cari :appName',\n];\n"
  },
  {
    "path": "lang/id/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Pilih Gambar',\n    'image_list' => 'Daftar Gambar',\n    'image_details' => 'Detail Gambar',\n    'image_upload' => 'Unggah Gambar',\n    'image_intro' => 'Di sini Anda dapat memilih dan mengelola gambar yang sebelumnya diunggah ke sistem.',\n    'image_intro_upload' => 'Unggah gambar baru dengan menyeret berkas gambar ke jendela ini, atau dengan menggunakan tombol \"Unggah Gambar\" di atas.',\n    'image_all' => 'Semua',\n    'image_all_title' => 'Lihat semua gambar',\n    'image_book_title' => 'Lihat gambar untuk diunggah ke buku ini',\n    'image_page_title' => 'Lihat gambar yang diunggah ke halaman ini',\n    'image_search_hint' => 'Cari berdasarkan nama gambar',\n    'image_uploaded' => 'Diunggah :uploadedDate',\n    'image_uploaded_by' => 'Diunggah oleh :userName',\n    'image_uploaded_to' => 'Diunggah ke :pageLink',\n    'image_updated' => 'Diperbarui :updateDate',\n    'image_load_more' => 'Muat lebih banyak',\n    'image_image_name' => 'Muat lebih banyak',\n    'image_delete_used' => 'Gambar ini digunakan untuk halaman dibawah ini.',\n    'image_delete_confirm_text' => 'Anda yakin ingin menghapus gambar ini?',\n    'image_select_image' => 'Pilih gambar',\n    'image_dropzone' => 'Lepaskan gambar atau klik di sini untuk mengunggah',\n    'image_dropzone_drop' => 'Letakkan gambar di sini untuk diunggah',\n    'images_deleted' => 'Gambar Dihapus',\n    'image_preview' => 'Pratinjau Gambar',\n    'image_upload_success' => 'Gambar berhasil diunggah',\n    'image_update_success' => 'Detail gambar berhasil diperbarui',\n    'image_delete_success' => 'Gambar berhasil dihapus',\n    'image_replace' => 'Ganti Gambar',\n    'image_replace_success' => 'Detail gambar berhasil diperbarui',\n    'image_rebuild_thumbs' => 'Buat Ulang Variasi Ukuran',\n    'image_rebuild_thumbs_success' => 'Variasi ukuran gambar berhasil dibuat ulang!',\n\n    // Code Editor\n    'code_editor' => 'Edit Kode',\n    'code_language' => 'Bahasa Kode',\n    'code_content' => 'Konten Kode',\n    'code_session_history' => 'Konten Kode',\n    'code_save' => 'Simpan Kode',\n];\n"
  },
  {
    "path": "lang/id/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Umum',\n    'advanced' => 'Lanjutan',\n    'none' => 'Tidak Ada',\n    'cancel' => 'Batal',\n    'save' => 'Simpan',\n    'close' => 'Tutup',\n    'apply' => 'Terapkan',\n    'undo' => 'Undo',\n    'redo' => 'Ulangi',\n    'left' => 'Kiri',\n    'center' => 'Tengah',\n    'right' => 'Kanan',\n    'top' => 'Atas',\n    'middle' => 'Sedang',\n    'bottom' => 'Bawah',\n    'width' => 'Lebar',\n    'height' => 'Tinggi',\n    'More' => 'Lebih Banyak',\n    'select' => 'Pilih...',\n\n    // Toolbar\n    'formats' => 'Format',\n    'header_large' => 'Header Besar',\n    'header_medium' => 'Header Sedang',\n    'header_small' => 'Header Kecil',\n    'header_tiny' => 'Header Sangat Kecil',\n    'paragraph' => 'Paragraf',\n    'blockquote' => 'Kutipan blok',\n    'inline_code' => 'Kode sebaris',\n    'callouts' => 'Anotasi',\n    'callout_information' => 'Informasi',\n    'callout_success' => 'Sukses',\n    'callout_warning' => 'Peringatan',\n    'callout_danger' => 'Bahaya',\n    'bold' => 'Berani',\n    'italic' => 'Italic',\n    'underline' => 'Garis Bawah',\n    'strikethrough' => 'Coret',\n    'superscript' => 'Superskrip',\n    'subscript' => 'Berlangganan',\n    'text_color' => 'Warna teks',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Warna khusus',\n    'remove_color' => 'Hapus Warna',\n    'background_color' => 'Warna latar',\n    'align_left' => 'Rata Kiri',\n    'align_center' => 'Rata tengah',\n    'align_right' => 'Rata kanan',\n    'align_justify' => 'Rata kanan kiri',\n    'list_bullet' => 'Daftar poin',\n    'list_numbered' => 'Daftar bernomor',\n    'list_task' => 'Daftar tugas',\n    'indent_increase' => 'Tambah indentasi',\n    'indent_decrease' => 'Kurangi indentasi',\n    'table' => 'Tabel',\n    'insert_image' => 'Sisipkan gambar',\n    'insert_image_title' => 'Sisipkan/Ubah Gambar',\n    'insert_link' => 'Sisipkan/ubah tautan',\n    'insert_link_title' => 'Sisipkan/Ubah Tautan',\n    'insert_horizontal_line' => 'Sisipkan garis horizontal',\n    'insert_code_block' => 'Sisipkan blok kode',\n    'edit_code_block' => 'Ubah blok kode',\n    'insert_drawing' => 'Sisipkan/ubah gambaran',\n    'drawing_manager' => 'Manajer Gambaran',\n    'insert_media' => 'Sisipkan/ubah media',\n    'insert_media_title' => 'Sisipkan/Ubah Media',\n    'clear_formatting' => 'Bersihkan format',\n    'source_code' => 'Kode sumber',\n    'source_code_title' => 'Kode Sumber',\n    'fullscreen' => 'Layar penuh',\n    'image_options' => 'Opsi gambar',\n\n    // Tables\n    'table_properties' => 'Properti tabel',\n    'table_properties_title' => 'Properti Tabel',\n    'delete_table' => 'Hapus tabel',\n    'table_clear_formatting' => 'Bersihkan format tabel',\n    'resize_to_contents' => 'Sesuaikan dengan ukuran konten',\n    'row_header' => 'Judul baris',\n    'insert_row_before' => 'Sisipkan baris sebelum',\n    'insert_row_after' => 'Sisipkan baris setelah',\n    'delete_row' => 'Hapus baris',\n    'insert_column_before' => 'Sisipkan kolom sebelum',\n    'insert_column_after' => 'Sisipkan kolom sesudah',\n    'delete_column' => 'Hapus kolom',\n    'table_cell' => 'Sel',\n    'table_row' => 'Row',\n    'table_column' => 'Column',\n    'cell_properties' => 'Cell properties',\n    'cell_properties_title' => 'Cell Properties',\n    'cell_type' => 'Cell type',\n    'cell_type_cell' => 'Cell',\n    'cell_scope' => 'Scope',\n    'cell_type_header' => 'Header cell',\n    'merge_cells' => 'Merge cells',\n    'split_cell' => 'Split cell',\n    'table_row_group' => 'Row Group',\n    'table_column_group' => 'Column Group',\n    'horizontal_align' => 'Horizontal align',\n    'vertical_align' => 'Vertical align',\n    'border_width' => 'Border width',\n    'border_style' => 'Border style',\n    'border_color' => 'Border color',\n    'row_properties' => 'Row properties',\n    'row_properties_title' => 'Row Properties',\n    'cut_row' => 'Cut row',\n    'copy_row' => 'Copy row',\n    'paste_row_before' => 'Paste row before',\n    'paste_row_after' => 'Paste row after',\n    'row_type' => 'Row type',\n    'row_type_header' => 'Header',\n    'row_type_body' => 'Body',\n    'row_type_footer' => 'Footer',\n    'alignment' => 'Alignment',\n    'cut_column' => 'Cut column',\n    'copy_column' => 'Copy column',\n    'paste_column_before' => 'Paste column before',\n    'paste_column_after' => 'Paste column after',\n    'cell_padding' => 'Cell padding',\n    'cell_spacing' => 'Cell spacing',\n    'caption' => 'Caption',\n    'show_caption' => 'Show caption',\n    'constrain' => 'Constrain proportions',\n    'cell_border_solid' => 'Solid',\n    'cell_border_dotted' => 'Dotted',\n    'cell_border_dashed' => 'Dashed',\n    'cell_border_double' => 'Double',\n    'cell_border_groove' => 'Groove',\n    'cell_border_ridge' => 'Ridge',\n    'cell_border_inset' => 'Inset',\n    'cell_border_outset' => 'Outset',\n    'cell_border_none' => 'None',\n    'cell_border_hidden' => 'Hidden',\n\n    // Images, links, details/summary & embed\n    'source' => 'Source',\n    'alt_desc' => 'Alternative description',\n    'embed' => 'Embed',\n    'paste_embed' => 'Paste your embed code below:',\n    'url' => 'URL',\n    'text_to_display' => 'Text to display',\n    'title' => 'Title',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Open link',\n    'open_link_in' => 'Open link in...',\n    'open_link_current' => 'Current window',\n    'open_link_new' => 'New window',\n    'remove_link' => 'Remove link',\n    'insert_collapsible' => 'Insert collapsible block',\n    'collapsible_unwrap' => 'Unwrap',\n    'edit_label' => 'Edit label',\n    'toggle_open_closed' => 'Toggle open/closed',\n    'collapsible_edit' => 'Edit collapsible block',\n    'toggle_label' => 'Toggle label',\n\n    // About view\n    'about' => 'About the editor',\n    'about_title' => 'About the WYSIWYG Editor',\n    'editor_license' => 'Editor License & Copyright',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under the MIT license.',\n    'editor_tiny_license_link' => 'The copyright and license details of TinyMCE can be found here.',\n    'save_continue' => 'Save Page & Continue',\n    'callouts_cycle' => '(Keep pressing to toggle through types)',\n    'link_selector' => 'Link to content',\n    'shortcuts' => 'Shortcuts',\n    'shortcut' => 'Shortcut',\n    'shortcuts_intro' => 'The following shortcuts are available in the editor:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Description',\n];\n"
  },
  {
    "path": "lang/id/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Baru saja dibuat',\n    'recently_created_pages' => 'Halaman baru saja dibuat',\n    'recently_updated_pages' => 'Halaman baru saja diperbaharui',\n    'recently_created_chapters' => 'Bab baru saja dibuat',\n    'recently_created_books' => 'Buku baru saja dibuat',\n    'recently_created_shelves' => 'Rak baru saja dibuat',\n    'recently_update' => 'Baru saja diperbaharui',\n    'recently_viewed' => 'Baru saja dilihat',\n    'recent_activity' => 'Aktivitas Terbaru',\n    'create_now' => 'Buat Sekarang',\n    'revisions' => 'Revisi',\n    'meta_revision' => 'Revisi #:revisionCount',\n    'meta_created' => 'Dibuat :timeLength',\n    'meta_created_name' => 'Dibuat :timeLength oleh :user',\n    'meta_updated' => 'Diperbaharui :timeLength',\n    'meta_updated_name' => 'Diperbaharui :timeLength oleh :user',\n    'meta_owned_name' => 'Dimiliki oleh :user',\n    'meta_reference_count' => 'Referenced by :count item|Referenced by :count items',\n    'entity_select' => 'Pilihan Entitas',\n    'entity_select_lack_permission' => 'You don\\'t have the required permissions to select this item',\n    'images' => 'Gambar-gambar',\n    'my_recent_drafts' => 'Draf Terbaru Saya',\n    'my_recently_viewed' => 'Baru saja saya lihat',\n    'my_most_viewed_favourites' => 'Favorit Saya yang Paling Banyak Dilihat',\n    'my_favourites' => 'Favoritku',\n    'no_pages_viewed' => 'Anda belum melihat halaman apa pun',\n    'no_pages_recently_created' => 'Tidak ada halaman yang baru saja dibuat',\n    'no_pages_recently_updated' => 'Tidak ada halaman yang baru-baru ini diperbarui',\n    'export' => 'Ekspor',\n    'export_html' => 'File Web Berisi',\n    'export_pdf' => 'Dokumen PDF',\n    'export_text' => 'Dokumen Teks Biasa',\n    'export_md' => 'File Markdown',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Default Page Template',\n    'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',\n    'default_template_select' => 'Select a template page',\n    'import' => 'Import',\n    'import_validate' => 'Validate Import',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'Select ZIP file to upload',\n    'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',\n    'import_pending' => 'Pending Imports',\n    'import_pending_none' => 'No imports have been started.',\n    'import_continue' => 'Continue Import',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Run Import',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Uploaded by',\n    'import_location' => 'Import Location',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'Import Errors',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Izin',\n    'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.',\n    'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.',\n    'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.',\n    'permissions_save' => 'Simpan Izin',\n    'permissions_owner' => 'Pemilik',\n    'permissions_role_everyone_else' => 'Everyone Else',\n    'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',\n    'permissions_role_override' => 'Override permissions for role',\n    'permissions_inherit_defaults' => 'Inherit defaults',\n\n    // Search\n    'search_results' => 'Hasil Pencarian',\n    'search_total_results_found' => ':count hasil hitung ditemukan |:count hasil hitung total tang di temukan',\n    'search_clear' => 'Bersihkan pencarian',\n    'search_no_pages' => 'Tidak ada halaman yang cocok dengan pencarian ini',\n    'search_for_term' => 'Pencarian untuk :term',\n    'search_more' => 'Hasil lebih',\n    'search_advanced' => 'Pencarian Lanjutan',\n    'search_terms' => 'Cari Istilah',\n    'search_content_type' => 'Tipe Konten',\n    'search_exact_matches' => 'Pertandingan Persis',\n    'search_tags' => 'Pencarian Tag',\n    'search_options' => 'Pilihan',\n    'search_viewed_by_me' => 'Dilihat oleh saya',\n    'search_not_viewed_by_me' => 'Tidak dilihat oleh saya',\n    'search_permissions_set' => 'Izin ditetapkan',\n    'search_created_by_me' => 'Dibuat oleh saya',\n    'search_updated_by_me' => 'Diperbaharui oleh saya',\n    'search_owned_by_me' => 'Milik Saya',\n    'search_date_options' => 'Opsi Tanggal',\n    'search_updated_before' => 'Diperbaharui sebelum',\n    'search_updated_after' => 'Diperbaharui setelah',\n    'search_created_before' => 'Dibuat sebelum',\n    'search_created_after' => 'Dibuat setelah',\n    'search_set_date' => 'Atur Tanggal',\n    'search_update' => 'Perbaharui pencarian',\n\n    // Shelves\n    'shelf' => 'Rak',\n    'shelves' => 'Rak',\n    'x_shelves' => ':count Rak|:count Rak',\n    'shelves_empty' => 'Tidak ada rak yang dibuat',\n    'shelves_create' => 'Buat Rak baru',\n    'shelves_popular' => 'Rak Terpopuler',\n    'shelves_new' => 'Rak Baru',\n    'shelves_new_action' => 'Rak Baru',\n    'shelves_popular_empty' => 'Rak paling populer akan muncul di sini.',\n    'shelves_new_empty' => 'Rak yang paling baru dibuat akan muncul di sini.',\n    'shelves_save' => 'Simpan Rak',\n    'shelves_books' => 'Buku di rak ini',\n    'shelves_add_books' => 'Tambahkan buku ke rak ini',\n    'shelves_drag_books' => 'Drag books below to add them to this shelf',\n    'shelves_empty_contents' => 'Rak ini tidak memiliki buku yang ditugaskan padanya',\n    'shelves_edit_and_assign' => 'Edit rak untuk menetapkan buku',\n    'shelves_edit_named' => 'Edit Shelf :name',\n    'shelves_edit' => 'Edit Shelf',\n    'shelves_delete' => 'Delete Shelf',\n    'shelves_delete_named' => 'Delete Shelf :name',\n    'shelves_delete_explain' => \"This will delete the shelf with the name ':name'. Contained books will not be deleted.\",\n    'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',\n    'shelves_permissions' => 'Shelf Permissions',\n    'shelves_permissions_updated' => 'Shelf Permissions Updated',\n    'shelves_permissions_active' => 'Shelf Permissions Active',\n    'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',\n    'shelves_permissions_create' => 'Shelf create permissions are only used for copying permissions to child books using the action below. They do not control the ability to create books.',\n    'shelves_copy_permissions_to_books' => 'Salin Izin ke Buku',\n    'shelves_copy_permissions' => 'Salin Izin',\n    'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',\n    'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',\n\n    // Books\n    'book' => 'Buku',\n    'books' => 'Semua Buku',\n    'x_books' => ':count Buku|:count Semua Buku',\n    'books_empty' => 'Tidak ada buku yang telah dibuat',\n    'books_popular' => 'Buku Populer',\n    'books_recent' => 'Buku Terbaru',\n    'books_new' => 'Buku baru',\n    'books_new_action' => 'Buku baru',\n    'books_popular_empty' => 'Buku paling populer akan muncul di sini.',\n    'books_new_empty' => 'The most recently created books will appear here.',\n    'books_create' => 'Buat Buku Baru',\n    'books_delete' => 'Hapus Buku',\n    'books_delete_named' => 'Hapus buku :bookName',\n    'books_delete_explain' => 'Ini akan menghapus buku dengan nama \\': bookName\\'. Semua halaman dan bab akan dihapus.',\n    'books_delete_confirmation' => 'Apakah Anda yakin ingin menghapus buku ini?',\n    'books_edit' => 'Edit Buku',\n    'books_edit_named' => 'Sunting Buku :bookName',\n    'books_form_book_name' => 'Nama Buku',\n    'books_save' => 'Simpan Buku',\n    'books_permissions' => 'Izin Buku',\n    'books_permissions_updated' => 'Izin Buku Diperbarui',\n    'books_empty_contents' => 'Tidak ada halaman atau bab yang telah dibuat untuk buku ini.',\n    'books_empty_create_page' => 'Buat halaman baru',\n    'books_empty_sort_current_book' => 'Sortir buku saat ini',\n    'books_empty_add_chapter' => 'Tambahkan satu bab',\n    'books_permissions_active' => 'Izin Buku Aktif',\n    'books_search_this' => 'Cari buku ini',\n    'books_navigation' => 'Navigasi Buku',\n    'books_sort' => 'Sortir Isi Buku',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Sortir Buku :bookName',\n    'books_sort_name' => 'Diurutkan berdasarkan nama',\n    'books_sort_created' => 'Urutkan berdasarkan Tanggal Dibuat',\n    'books_sort_updated' => 'Urutkan berdasarkan Tanggal Diperbarui',\n    'books_sort_chapters_first' => 'Bab Pertama',\n    'books_sort_chapters_last' => 'Bab Terakhir',\n    'books_sort_show_other' => 'Tunjukkan Buku Lain',\n    'books_sort_save' => 'Simpan Pesanan Baru',\n    'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.',\n    'books_sort_move_up' => 'Move Up',\n    'books_sort_move_down' => 'Move Down',\n    'books_sort_move_prev_book' => 'Move to Previous Book',\n    'books_sort_move_next_book' => 'Move to Next Book',\n    'books_sort_move_prev_chapter' => 'Move Into Previous Chapter',\n    'books_sort_move_next_chapter' => 'Move Into Next Chapter',\n    'books_sort_move_book_start' => 'Move to Start of Book',\n    'books_sort_move_book_end' => 'Move to End of Book',\n    'books_sort_move_before_chapter' => 'Move to Before Chapter',\n    'books_sort_move_after_chapter' => 'Move to After Chapter',\n    'books_copy' => 'Copy Book',\n    'books_copy_success' => 'Book successfully copied',\n\n    // Chapters\n    'chapter' => 'Bab',\n    'chapters' => 'Bab',\n    'x_chapters' => ':count Bab |:count Bab',\n    'chapters_popular' => 'Bab Populer',\n    'chapters_new' => 'Bab Baru',\n    'chapters_create' => 'Buat Bab Baru',\n    'chapters_delete' => 'Hapur Bab',\n    'chapters_delete_named' => 'Hapus bab dengan nama :chapterName',\n    'chapters_delete_explain' => 'Ini akan menghapus chapter dengan nama \\':chapterName\\'. Semua halaman yang ada dalam bab ini juga akan dihapus.',\n    'chapters_delete_confirm' => 'Anda yakin ingin menghapus bab ini?',\n    'chapters_edit' => 'Edit Bab',\n    'chapters_edit_named' => 'Sunting Bab :chapterName',\n    'chapters_save' => 'Simpan Bab',\n    'chapters_move' => 'Pindahkan Bab',\n    'chapters_move_named' => 'Pindahkan Bab :chapterName',\n    'chapters_copy' => 'Copy Chapter',\n    'chapters_copy_success' => 'Chapter successfully copied',\n    'chapters_permissions' => 'Izin Bab',\n    'chapters_empty' => 'Saat ini tidak ada halaman dalam bab ini.',\n    'chapters_permissions_active' => 'Izin Bab Aktif',\n    'chapters_permissions_success' => 'Izin Bab Diperbarui',\n    'chapters_search_this' => 'Cari bab ini',\n    'chapter_sort_book' => 'Sort Book',\n\n    // Pages\n    'page' => 'Halaman',\n    'pages' => 'Semua Halaman',\n    'x_pages' => ':count Halaman|:count Semua Halaman',\n    'pages_popular' => 'Halaman Populer',\n    'pages_new' => 'Lembaran baru',\n    'pages_attachments' => 'Lampiran',\n    'pages_navigation' => 'Halaman Navigasi',\n    'pages_delete' => 'Hapus Halaman',\n    'pages_delete_named' => 'Hapus Halaman :pageName',\n    'pages_delete_draft_named' => 'Hapus Halaman Draf :pageName',\n    'pages_delete_draft' => 'Hapus Halaman Draf',\n    'pages_delete_success' => 'Halaman dihapus',\n    'pages_delete_draft_success' => 'Halaman draf dihapus',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'Anda yakin ingin menghapus halaman ini?',\n    'pages_delete_draft_confirm' => 'Anda yakin ingin menghapus halaman draf ini?',\n    'pages_editing_named' => 'Menyunting Halaman :pageName',\n    'pages_edit_draft_options' => 'Opsi Draf',\n    'pages_edit_save_draft' => 'Simpan Draf',\n    'pages_edit_draft' => 'Edit Halaman Draf',\n    'pages_editing_draft' => 'Mengedit Draf',\n    'pages_editing_page' => 'Mengedit Draf',\n    'pages_edit_draft_save_at' => 'Draf disimpan pada ',\n    'pages_edit_delete_draft' => 'Hapus Draf',\n    'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.',\n    'pages_edit_discard_draft' => 'Buang Draf',\n    'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',\n    'pages_edit_switch_to_markdown_clean' => '(Clean Content)',\n    'pages_edit_switch_to_markdown_stable' => '(Stable Content)',\n    'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Atur Changelog',\n    'pages_edit_enter_changelog_desc' => 'Masukkan deskripsi singkat tentang perubahan yang Anda buat',\n    'pages_edit_enter_changelog' => 'Masuk ke Changelog',\n    'pages_editor_switch_title' => 'Switch Editor',\n    'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',\n    'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',\n    'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',\n    'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',\n    'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\\'t persist across this change.',\n    'pages_save' => 'Simpan Halaman',\n    'pages_title' => 'Judul Halaman',\n    'pages_name' => 'Nama Halaman',\n    'pages_md_editor' => 'Editor',\n    'pages_md_preview' => 'Pratinjau',\n    'pages_md_insert_image' => 'Sisipkan Gambar',\n    'pages_md_insert_link' => 'Sisipkan Tautan Entitas',\n    'pages_md_insert_drawing' => 'Sisipkan Gambar',\n    'pages_md_show_preview' => 'Show preview',\n    'pages_md_sync_scroll' => 'Sync preview scroll',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Unsaved Drawing Found',\n    'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',\n    'pages_not_in_chapter' => 'Halaman tidak dalam satu bab',\n    'pages_move' => 'Pindahkan Halaman',\n    'pages_copy' => 'Salin Halaman',\n    'pages_copy_desination' => 'Salin Tujuan',\n    'pages_copy_success' => 'Halaman berhasil disalin',\n    'pages_permissions' => 'Izin Halaman',\n    'pages_permissions_success' => 'Izin halaman diperbarui',\n    'pages_revision' => 'Revisi',\n    'pages_revisions' => 'Revisi Halaman',\n    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',\n    'pages_revisions_named' => 'Revisi Halaman untuk :pageName',\n    'pages_revision_named' => 'Revisi Halaman untuk :pageName',\n    'pages_revision_restored_from' => 'Dipulihkan dari #:id; :summary',\n    'pages_revisions_created_by' => 'Dibuat Oleh',\n    'pages_revisions_date' => 'Tanggal Revisi',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Revision Number',\n    'pages_revisions_numbered' => 'Revisi #:id',\n    'pages_revisions_numbered_changes' => 'Revisi #:id Berubah',\n    'pages_revisions_editor' => 'Editor Type',\n    'pages_revisions_changelog' => 'Changelog',\n    'pages_revisions_changes' => 'Perubahan',\n    'pages_revisions_current' => 'Versi sekarang',\n    'pages_revisions_preview' => 'Pratinjau',\n    'pages_revisions_restore' => 'Mengembalikan',\n    'pages_revisions_none' => 'Halaman ini tidak memiliki revisi',\n    'pages_copy_link' => 'Salin tautan',\n    'pages_edit_content_link' => 'Jump to section in editor',\n    'pages_pointer_enter_mode' => 'Enter section select mode',\n    'pages_pointer_label' => 'Page Section Options',\n    'pages_pointer_permalink' => 'Page Section Permalink',\n    'pages_pointer_include_tag' => 'Page Section Include Tag',\n    'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag',\n    'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink',\n    'pages_permissions_active' => 'Izin Halaman Aktif',\n    'pages_initial_revision' => 'Penerbitan awal',\n    'pages_references_update_revision' => 'System auto-update of internal links',\n    'pages_initial_name' => 'Halaman Baru',\n    'pages_editing_draft_notification' => 'Anda sedang menyunting konsep yang terakhir disimpan :timeDiff.',\n    'pages_draft_edited_notification' => 'Halaman ini telah diperbarui sejak saat itu. Anda disarankan untuk membuang draf ini.',\n    'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count pengguna sudah mulai mengedit halaman ini',\n        'start_b' => ':userName telah memulai menyunting halaman ini',\n        'time_a' => 'semenjak halaman terakhir diperbarui',\n        'time_b' => 'di akhir :minCount menit',\n        'message' => ':start :time. Berhati-hatilah untuk tidak menimpa pembaruan satu sama lain!',\n    ],\n    'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',\n    'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',\n    'pages_specific' => 'Halaman Tertentu',\n    'pages_is_template' => 'Template Halaman',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Toggle Sidebar',\n    'page_tags' => 'Halaman Tag',\n    'chapter_tags' => 'Bab Tag',\n    'book_tags' => 'Tag Buku',\n    'shelf_tags' => 'Tag Rak',\n    'tag' => 'Tag',\n    'tags' =>  'Semua Tag',\n    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',\n    'tag_name' =>  'Nama Tag',\n    'tag_value' => 'Nilai Tag (opsional)',\n    'tags_explain' => \"Tambahkan beberapa tag untuk mengkategorikan konten Anda dengan lebih baik.\\n Anda dapat menetapkan nilai ke tag untuk pengaturan yang lebih mendalam.\",\n    'tags_add' => 'Tambahkan tag lain',\n    'tags_remove' => 'Hapus tag ini',\n    'tags_usages' => 'Total tag usages',\n    'tags_assigned_pages' => 'Assigned to Pages',\n    'tags_assigned_chapters' => 'Assigned to Chapters',\n    'tags_assigned_books' => 'Assigned to Books',\n    'tags_assigned_shelves' => 'Assigned to Shelves',\n    'tags_x_unique_values' => ':count unique values',\n    'tags_all_values' => 'All values',\n    'tags_view_tags' => 'View Tags',\n    'tags_view_existing_tags' => 'View existing tags',\n    'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',\n    'attachments' => 'Lampiran',\n    'attachments_explain' => 'Unggah beberapa berkas atau lampirkan beberapa tautan untuk ditampilkan di laman Anda. Ini terlihat di sidebar halaman.',\n    'attachments_explain_instant_save' => 'Perubahan di sini disimpan secara instan.',\n    'attachments_upload' => 'Unggah Berkas',\n    'attachments_link' => 'Lampirkan Tautan',\n    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',\n    'attachments_set_link' => 'Setel Tautan',\n    'attachments_delete' => 'Anda yakin ingin menghapus lampiran ini?',\n    'attachments_dropzone' => 'Drop files here to upload',\n    'attachments_no_files' => 'Tidak ada berkas yang telah diunggah',\n    'attachments_explain_link' => 'Anda dapat melampirkan sebuah tautan jika Anda memilih untuk tidak mengunggah berkas. Ini bisa berupa sebuah tautan ke halaman lain atau tautan ke sebuah berkas di cloud.',\n    'attachments_link_name' => 'Nama Tautan',\n    'attachment_link' => 'Lampiran Tautan',\n    'attachments_link_url' => 'Tautan ke file',\n    'attachments_link_url_hint' => 'Alamat url situs atau berkas',\n    'attach' => 'Melampirkan',\n    'attachments_insert_link' => 'Tambahkan Tautan Lampiran ke Halaman',\n    'attachments_edit_file' => 'Edit File',\n    'attachments_edit_file_name' => 'Nama file',\n    'attachments_edit_drop_upload' => 'Jatuhkan berkas atau klik di sini untuk mengunggah dan menimpa',\n    'attachments_order_updated' => 'Urutan lampiran diperbarui',\n    'attachments_updated_success' => 'Detail lampiran diperbarui',\n    'attachments_deleted' => 'Lampiran dihapus',\n    'attachments_file_uploaded' => 'Berkas berhasil diunggah',\n    'attachments_file_updated' => 'File berhasil diperbarui',\n    'attachments_link_attached' => 'Tautan berhasil dilampirkan ke halaman',\n    'templates' => 'Template',\n    'templates_set_as_template' => 'Halaman adalah template',\n    'templates_explain_set_as_template' => 'Anda dapat mengatur halaman ini sebagai template sehingga isinya dapat digunakan saat membuat halaman lain. Pengguna lain akan dapat menggunakan template ini jika mereka memiliki izin melihat halaman ini.',\n    'templates_replace_content' => 'Ganti Halaman Konten',\n    'templates_append_content' => 'Tambahkan ke halaman konten',\n    'templates_prepend_content' => 'Tambahkan ke halaman konten',\n\n    // Profile View\n    'profile_user_for_x' => 'Pengguna untuk :time',\n    'profile_created_content' => 'Konten yang Dibuat',\n    'profile_not_created_pages' => ':userName belum membuat halaman apa pun',\n    'profile_not_created_chapters' => ':userName belum membuat bab apa pun',\n    'profile_not_created_books' => ':userName belum membuat buku apa pun',\n    'profile_not_created_shelves' => ':userName belum membuat rak apa pun',\n\n    // Comments\n    'comment' => 'Komentar',\n    'comments' => 'Komentar',\n    'comment_add' => 'Tambah Komentar',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Tinggalkan komentar disini',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Simpan Komentar',\n    'comment_new' => 'Komentar Baru',\n    'comment_created' => 'dikomentari oleh :createDiff',\n    'comment_updated' => 'Diperbarui :updateDiff oleh :username',\n    'comment_updated_indicator' => 'Updated',\n    'comment_deleted_success' => 'Komentar telah dihapus',\n    'comment_created_success' => 'Komentar telah di tambahkan',\n    'comment_updated_success' => 'Komentar Telah diperbaharui',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Anda yakin ingin menghapus komentar ini?',\n    'comment_in_reply_to' => 'Sebagai balasan untuk :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',\n\n    // Revision\n    'revision_delete_confirm' => 'Anda yakin ingin menghapus revisi ini?',\n    'revision_restore_confirm' => 'Apakah Anda yakin ingin memulihkan revisi ini? Konten halaman saat ini akan diganti.',\n    'revision_cannot_delete_latest' => 'Tidak dapat menghapus revisi terakhir.',\n\n    // Copy view\n    'copy_consider' => 'Please consider the below when copying content.',\n    'copy_consider_permissions' => 'Custom permission settings will not be copied.',\n    'copy_consider_owner' => 'You will become the owner of all copied content.',\n    'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',\n    'copy_consider_attachments' => 'Page attachments will not be copied.',\n    'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',\n\n    // Conversions\n    'convert_to_shelf' => 'Convert to Shelf',\n    'convert_to_shelf_contents_desc' => 'You can convert this book to a new shelf with the same contents. Chapters contained within this book will be converted to new books. If this book contains any pages, that are not in a chapter, this book will be renamed and contain such pages, and this book will become part of the new shelf.',\n    'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.',\n    'convert_book' => 'Convert Book',\n    'convert_book_confirm' => 'Are you sure you want to convert this book?',\n    'convert_undo_warning' => 'This cannot be as easily undone.',\n    'convert_to_book' => 'Convert to Book',\n    'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',\n    'convert_chapter' => 'Convert Chapter',\n    'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',\n\n    // References\n    'references' => 'References',\n    'references_none' => 'There are no tracked references to this item.',\n    'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',\n\n    // Watch Options\n    'watch' => 'Watch',\n    'watch_title_default' => 'Default Preferences',\n    'watch_desc_default' => 'Revert watching to just your default notification preferences.',\n    'watch_title_ignore' => 'Ignore',\n    'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.',\n    'watch_title_new' => 'New Pages',\n    'watch_desc_new' => 'Notify when any new page is created within this item.',\n    'watch_title_updates' => 'All Page Updates',\n    'watch_desc_updates' => 'Notify upon all new pages and page changes.',\n    'watch_desc_updates_page' => 'Notify upon all page changes.',\n    'watch_title_comments' => 'All Page Updates & Comments',\n    'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.',\n    'watch_desc_comments_page' => 'Notify upon page changes and new comments.',\n    'watch_change_default' => 'Change default notification preferences',\n    'watch_detail_ignore' => 'Ignoring notifications',\n    'watch_detail_new' => 'Watching for new pages',\n    'watch_detail_updates' => 'Watching new pages and updates',\n    'watch_detail_comments' => 'Watching new pages, updates & comments',\n    'watch_detail_parent_book' => 'Watching via parent book',\n    'watch_detail_parent_book_ignore' => 'Ignoring via parent book',\n    'watch_detail_parent_chapter' => 'Watching via parent chapter',\n    'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter',\n];\n"
  },
  {
    "path": "lang/id/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Anda tidak memiliki izin untuk mengakses halaman yang diminta.',\n    'permissionJson' => 'Anda tidak memiliki izin untuk melakukan tindakan yang diminta.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Pengguna dengan email :email sudah ada tetapi dengan kredensial berbeda.',\n    'auth_pre_register_theme_prevention' => 'Akun pengguna tidak dapat didaftarkan untuk rincian yang diberikan',\n    'email_already_confirmed' => 'Email telah dikonfirmasi, Coba masuk.',\n    'email_confirmation_invalid' => 'Token konfirmasi ini tidak valid atau telah digunakan, Silakan coba mendaftar lagi.',\n    'email_confirmation_expired' => 'Token konfirmasi telah kedaluwarsa, Email konfirmasi baru telah dikirim.',\n    'email_confirmation_awaiting' => 'Alamat email untuk akun yang digunakan perlu dikonfirmasi',\n    'ldap_fail_anonymous' => 'Akses LDAP gagal menggunakan pengikatan anonim',\n    'ldap_fail_authed' => 'Akses LDAP gagal menggunakan rincian dn & sandi yang diberikan',\n    'ldap_extension_not_installed' => 'Ekstensi LDAP PHP tidak terpasang',\n    'ldap_cannot_connect' => 'Tidak dapat terhubung ke server ldap, Koneksi awal gagal',\n    'saml_already_logged_in' => 'Telah masuk',\n    'saml_no_email_address' => 'Tidak dapat menemukan sebuah alamat email untuk pengguna ini, dalam data yang diberikan oleh sistem autentikasi eksternal',\n    'saml_invalid_response_id' => 'Permintaan dari sistem otentikasi eksternal tidak dikenali oleh sebuah proses yang dimulai oleh aplikasi ini. Menavigasi kembali setelah masuk dapat menyebabkan masalah ini.',\n    'saml_fail_authed' => 'Masuk menggunakan :system gagal, sistem tidak memberikan otorisasi yang berhasil',\n    'oidc_already_logged_in' => 'Sudah masuk',\n    'oidc_no_email_address' => 'Tidak dapat menemukan alamat email untuk pengguna ini dalam data yang diberikan oleh sistem autentikasi eksternal',\n    'oidc_fail_authed' => 'Masuk menggunakan :system gagal, sistem tidak memberikan otorisasi yang berhasil',\n    'social_no_action_defined' => 'Tidak ada tindakan yang ditentukan',\n    'social_login_bad_response' => \"Kesalahan yang diterima selama masuk menggunakan :socialAccount : \\n:error\",\n    'social_account_in_use' => 'Akun :socialAccount ini sudah digunakan, Coba masuk melalui opsi :socialAccount.',\n    'social_account_email_in_use' => 'Email :email sudah digunakan. Jika Anda sudah memiliki akun, Anda dapat menghubungkan :socialAccount Anda dari pengaturan profil Anda.',\n    'social_account_existing' => 'Akun :socialAccount ini sudah dilampirkan ke profil Anda.',\n    'social_account_already_used_existing' => 'Akun :socialAccount ini sudah digunakan oleh pengguna lain.',\n    'social_account_not_used' => 'Akun :socialAccount ini tidak ditautkan ke pengguna mana pun. Harap lampirkan di dalam pengaturan profil Anda. ',\n    'social_account_register_instructions' => 'Jika Anda belum memiliki akun, Anda dapat mendaftarkan akun menggunakan opsi :socialAccount.',\n    'social_driver_not_found' => 'Pengemudi sosial tidak ditemukan',\n    'social_driver_not_configured' => 'Pengaturan sosial :socialAccount Anda tidak dikonfigurasi dengan benar.',\n    'invite_token_expired' => 'Tautan undangan ini telah kedaluwarsa. Sebagai gantinya, Anda dapat mencoba mengatur ulang kata sandi akun Anda.',\n    'login_user_not_found' => 'Pengguna untuk tindakan ini tidak dapat ditemukan.',\n\n    // System\n    'path_not_writable' => 'Jalur berkas :filePath tidak dapat diunggah. Pastikan berkas tersebut dapat ditulis ke server.',\n    'cannot_get_image_from_url' => 'Tidak dapat mengambil gambar dari :url',\n    'cannot_create_thumbs' => 'Server tidak dapat membuat thumbnail. Harap periksa apakah Anda telah memasang ekstensi GD PHP.',\n    'server_upload_limit' => 'Server tidak mengizinkan unggahan dengan ukuran ini. Harap coba ukuran berkas yang lebih kecil.',\n    'server_post_limit' => 'Server tidak dapat menerima jumlah data yang diberikan. Coba lagi dengan data yang lebih sedikit atau berkas yang lebih kecil.',\n    'uploaded'  => 'Server tidak mengizinkan unggahan dengan ukuran ini. Harap coba ukuran berkas yang lebih kecil.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Terjadi kesalahan saat mengunggah gambar',\n    'image_upload_type_error' => 'Jenis gambar yang diunggah tidak valid',\n    'image_upload_replace_type' => 'Penggantian file gambar harus berjenis sama',\n    'image_upload_memory_limit' => 'Gagal menangani pengunggahan gambar dan/atau membuat thumbnail karena keterbatasan sumber daya sistem.',\n    'image_thumbnail_memory_limit' => 'Gagal membuat variasi ukuran gambar karena keterbatasan sumber daya sistem.',\n    'image_gallery_thumbnail_memory_limit' => 'Gagal membuat thumbnail galeri karena keterbatasan sumber daya sistem.',\n    'drawing_data_not_found' => 'Data gambar tidak dapat dimuat. Berkas gambar mungkin sudah tidak ada atau Anda mungkin tidak memiliki izin untuk mengaksesnya.',\n\n    // Attachments\n    'attachment_not_found' => 'Lampiran tidak ditemukan',\n    'attachment_upload_error' => 'Terjadi kesalahan saat mengunggah berkas',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Gagal menyimpan draf. Pastikan Anda memiliki koneksi internet sebelum menyimpan halaman ini',\n    'page_draft_delete_fail' => 'Gagal menghapus draf halaman dan mengambil konten tersimpan halaman saat ini',\n    'page_custom_home_deletion' => 'Tidak dapat menghapus sebuah halaman saat diatur sebagai sebuah halaman beranda',\n\n    // Entities\n    'entity_not_found' => 'Entitas tidak ditemukan',\n    'bookshelf_not_found' => 'Rak tidak ditemukan',\n    'book_not_found' => 'Buku tidak ditemukan',\n    'page_not_found' => 'Halaman tidak ditemukan',\n    'chapter_not_found' => 'Bab tidak ditemukan',\n    'selected_book_not_found' => 'Buku yang dipilih tidak ditemukan',\n    'selected_book_chapter_not_found' => 'Buku atau Bab yang dipilih tidak ditemukan',\n    'guests_cannot_save_drafts' => 'Tamu tidak dapat menyimpan Draf',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Anda tidak dapat menghapus satu-satunya admin',\n    'users_cannot_delete_guest' => 'Anda tidak dapat menghapus pengguna tamu',\n    'users_could_not_send_invite' => 'Tidak dapat membuat pengguna karena email undangan gagal dikirim',\n\n    // Roles\n    'role_cannot_be_edited' => 'Peran ini tidak dapat disunting',\n    'role_system_cannot_be_deleted' => 'Peran ini adalah peran sistem dan tidak dapat dihapus',\n    'role_registration_default_cannot_delete' => 'Peran ini tidak dapat dihapus jika disetel sebagai peran pendaftaran default',\n    'role_cannot_remove_only_admin' => 'Pengguna ini adalah satu-satunya pengguna yang ditetapkan ke peran administrator. Tetapkan peran administrator untuk pengguna lain sebelum mencoba untuk menghapusnya di sini.',\n\n    // Comments\n    'comment_list' => 'Terjadi kesalahan saat mengambil komentar.',\n    'cannot_add_comment_to_draft' => 'Anda tidak dapat menambahkan komentar ke draf.',\n    'comment_add' => 'Terjadi kesalahan saat menambahkan / memperbarui komentar.',\n    'comment_delete' => 'Terjadi kesalahan saat menghapus komentar.',\n    'empty_comment' => 'Tidak dapat menambahkan komentar kosong.',\n\n    // Error pages\n    '404_page_not_found' => 'Halaman tidak ditemukan',\n    'sorry_page_not_found' => 'Maaf, Halaman yang Anda cari tidak dapat ditemukan.',\n    'sorry_page_not_found_permission_warning' => 'Jika Anda mengharapkan halaman ini ada, Anda mungkin tidak memiliki izin untuk melihatnya.',\n    'image_not_found' => 'Gambar tidak ditemukan',\n    'image_not_found_subtitle' => 'Maaf, Berkas gambar yang Anda cari tidak dapat ditemukan.',\n    'image_not_found_details' => 'Jika Anda mengharapkan gambar ini ada, gambar itu mungkin telah dihapus.',\n    'return_home' => 'Kembali ke home',\n    'error_occurred' => 'Terjadi kesalahan',\n    'app_down' => ':appName sedang down sekarang',\n    'back_soon' => 'Ini akan segera kembali.',\n\n    // Import\n    'import_zip_cant_read' => 'Tidak dapat membaca berkas ZIP.',\n    'import_zip_cant_decode_data' => 'Tidak dapat menemukan dan mendekode konten ZIP data.json.',\n    'import_zip_no_data' => 'Data berkas ZIP tidak berisi konten buku, bab, atau halaman yang diharapkan.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Impor ZIP gagal divalidasi dengan kesalahan:',\n    'import_zip_failed_notification' => 'Gagal mengimpor berkas ZIP.',\n    'import_perms_books' => 'Anda tidak memiliki izin yang diperlukan untuk membuat buku.',\n    'import_perms_chapters' => 'Anda tidak memiliki izin yang diperlukan untuk membuat bab.',\n    'import_perms_pages' => 'Anda tidak memiliki izin yang diperlukan untuk membuat halaman.',\n    'import_perms_images' => 'Anda tidak memiliki izin yang diperlukan untuk membuat gambar.',\n    'import_perms_attachments' => 'Anda tidak memiliki izin yang diperlukan untuk membuat lampiran.',\n\n    // API errors\n    'api_no_authorization_found' => 'Tidak ada token otorisasi yang ditemukan pada permintaan tersebut',\n    'api_bad_authorization_format' => 'Token otorisasi ditemukan pada permintaan tetapi formatnya salah',\n    'api_user_token_not_found' => 'Tidak ditemukan token API yang cocok untuk token otorisasi yang diberikan',\n    'api_incorrect_token_secret' => 'Rahasia yang diberikan untuk token API bekas yang diberikan salah',\n    'api_user_no_api_permission' => 'Pemilik token API yang digunakan tidak memiliki izin untuk melakukan panggilan API',\n    'api_user_token_expired' => 'Token otorisasi yang digunakan telah kedaluwarsa',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Kesalahan dilempar saat mengirim email uji:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'URL tidak cocok dengan host SSR yang diizinkan yang dikonfigurasi',\n];\n"
  },
  {
    "path": "lang/id/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Komentar baru di halaman: :pageName',\n    'new_comment_intro' => 'Seorang pengguna telah mengomentari halaman di :appName:',\n    'new_page_subject' => 'Halaman baru: :pageName',\n    'new_page_intro' => 'Halaman baru telah dibuat di :appName:',\n    'updated_page_subject' => 'Halaman yang diperbarui: :pageName',\n    'updated_page_intro' => 'Halaman telah diperbarui di :appName:',\n    'updated_page_debounce' => 'Untuk mencegah banyaknya pemberitahuan, untuk sementara Anda tidak akan dikirimi pemberitahuan untuk pengeditan lebih lanjut pada halaman ini oleh editor yang sama.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Nama Halaman:',\n    'detail_page_path' => 'Jalur Halaman:',\n    'detail_commenter' => 'Komentator:',\n    'detail_comment' => 'Komentar:',\n    'detail_created_by' => 'Dibuat Oleh:',\n    'detail_updated_by' => 'Diperbarui Oleh:',\n\n    'action_view_comment' => 'Lihat Komentar',\n    'action_view_page' => 'Lihat Halaman',\n\n    'footer_reason' => 'Pemberitahuan ini dikirimkan kepada Anda karena :link mencakup jenis aktivitas untuk item ini.',\n    'footer_reason_link' => 'preferensi notifikasi Anda',\n];\n"
  },
  {
    "path": "lang/id/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Sebelumnya',\n    'next'     => 'Lanjut &raquo;',\n\n];\n"
  },
  {
    "path": "lang/id/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Passwords must be at least eight characters and match the confirmation.',\n    'user' => \"Kami tidak dapat menemukan pengguna dengan alamat email tersebut.\",\n    'token' => 'Token setel ulang sandi tidak valid untuk alamat email ini.',\n    'sent' => 'Kami telah mengirimkan email tautan pengaturan ulang kata sandi Anda!',\n    'reset' => 'Kata sandi Anda telah disetel ulang!',\n\n];\n"
  },
  {
    "path": "lang/id/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Akun Saya',\n\n    'shortcuts' => 'Pintasan',\n    'shortcuts_interface' => 'UI Shortcut Preferences',\n    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',\n    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',\n    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',\n    'shortcuts_section_navigation' => 'Navigasi',\n    'shortcuts_section_actions' => 'Common Actions',\n    'shortcuts_save' => 'Simpan Pintasan',\n    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing \"?\" which will highlight the available shortcuts for actions currently visible on the screen.',\n    'shortcuts_update_success' => 'Shortcut preferences have been updated!',\n    'shortcuts_overview_desc' => 'Manage keyboard shortcuts you can use to navigate the system user interface.',\n\n    'notifications' => 'Notification Preferences',\n    'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.',\n    'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own',\n    'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Notify upon replies to my comments',\n    'notifications_save' => 'Save Preferences',\n    'notifications_update_success' => 'Notification preferences have been updated!',\n    'notifications_watched' => 'Watched & Ignored Items',\n    'notifications_watched_desc' => 'Below are the items that have custom watch preferences applied. To update your preferences for these, view the item then find the watch options in the sidebar.',\n\n    'auth' => 'Access & Security',\n    'auth_change_password' => 'Ubah Kata Sandi',\n    'auth_change_password_desc' => 'Change the password you use to log-in to the application. This must be at least 8 characters long.',\n    'auth_change_password_success' => 'Password has been updated!',\n\n    'profile' => 'Profile Details',\n    'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.',\n    'profile_view_public' => 'View Public Profile',\n    'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.',\n    'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.',\n    'profile_email_no_permission' => 'Unfortunately you don\\'t have permission to change your email address. If you want to change this, you\\'d need to ask an administrator to change this for you.',\n    'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.',\n    'profile_admin_options' => 'Administrator Options',\n    'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the \"Settings > Users\" area of the application.',\n\n    'delete_account' => 'Delete Account',\n    'delete_my_account' => 'Delete My Account',\n    'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\\'ve created, such as created pages and uploaded images, will remain.',\n    'delete_my_account_warning' => 'Are you sure you want to delete your account?',\n];\n"
  },
  {
    "path": "lang/id/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Pengaturan',\n    'settings_save' => 'Simpan Pengaturan',\n    'system_version' => 'Versi Sistem',\n    'categories' => 'Kategori',\n\n    // App Settings\n    'app_customization' => 'Kustomisasi',\n    'app_features_security' => 'Fitur & Keamanan',\n    'app_name' => 'Nama aplikasi',\n    'app_name_desc' => 'Nama ini ditampilkan di tajuk dan di semua email yang dikirim oleh sistem.',\n    'app_name_header' => 'Tampilkan nama di header',\n    'app_public_access' => 'Akses publik',\n    'app_public_access_desc' => 'Mengaktifkan opsi ini akan memungkinkan pengunjung, yang tidak masuk, untuk mengakses konten dalam contoh BookStack Anda.',\n    'app_public_access_desc_guest' => 'Akses untuk pengunjung umum dapat dikontrol melalui pengguna \"Tamu\".',\n    'app_public_access_toggle' => 'Izinkan akses publik',\n    'app_public_viewing' => 'Izinkan tontonan publik?',\n    'app_secure_images' => 'Unggahan Gambar Keamanan Lebih Tinggi',\n    'app_secure_images_toggle' => 'Aktifkan unggahan gambar dengan keamanan lebih tinggi',\n    'app_secure_images_desc' => 'Untuk alasan performa, semua gambar bersifat publik. Opsi ini menambahkan string acak yang sulit ditebak di depan url gambar. Pastikan indeks direktori tidak diaktifkan untuk mencegah akses mudah.',\n    'app_default_editor' => 'Editor Halaman Default',\n    'app_default_editor_desc' => 'Pilih editor yang akan digunakan secara default saat mengedit halaman baru. Pengaturan ini dapat diganti di tingkat halaman di mana itu diizinkan.',\n    'app_custom_html' => 'Kustom Konten HTML Head',\n    'app_custom_html_desc' => 'Konten apa pun yang ditambahkan di sini akan dimasukkan ke bagian bawah <head> bagian dari setiap halaman. Ini berguna untuk mengganti gaya atau menambahkan kode analitik.',\n    'app_custom_html_disabled_notice' => 'Kustom konten HTML Head dinonaktifkan pada halaman pengaturan ini untuk memastikan setiap perubahan yang mengganggu dapat dikembalikan.',\n    'app_logo' => 'Logo Aplikasi',\n    'app_logo_desc' => 'Ini digunakan di bilah tajuk aplikasi, di antara area lainnya. Gambar ini harus memiliki tinggi 86px. Gambar besar akan diperkecil.',\n    'app_icon' => 'Ikon Aplikasi',\n    'app_icon_desc' => 'Ikon digunakan pada tab browser dan ikon-ikon pintasan. Berkas harus berupa gambar IMG persegi dengan ukuran 256px.',\n    'app_homepage' => 'Beranda Aplikasi',\n    'app_homepage_desc' => 'Pilih tampilan untuk ditampilkan di beranda alih-alih tampilan default. Izin halaman diabaikan untuk halaman yang dipilih.',\n    'app_homepage_select' => 'Pilih halaman',\n    'app_footer_links' => 'Link Footer',\n    'app_footer_links_desc' => 'Tambahkan link untuk ditampilkan dalam footer situs. Ini akan ditampilkan di bagian bawah kebanyakan halaman, termasuk yang tidak memerlukan login. Anda dapat menggunakan label \"trans::<key>\" untuk menggunakan terjemahan yang ditentukan sistem. Sebagai contoh: Menggunakan \"trans::common.privacy_policy\" akan memberikan teks terjemahan \"Privacy Policy\" dan akan memberikan teks \"Terms of Service\".terjemahan trans::common.terms_of_service\".',\n    'app_footer_links_label' => 'Link Label',\n    'app_footer_links_url' => 'Link URL',\n    'app_footer_links_add' => 'Tambahkan Link Footer',\n    'app_disable_comments' => 'Nonaktifkan Komentar',\n    'app_disable_comments_toggle' => 'Nonaktifkan komentar',\n    'app_disable_comments_desc' => 'Menonaktifkan komentar di semua halaman dalam aplikasi. <br> Komentar yang ada tidak ditampilkan.',\n\n    // Color settings\n    'color_scheme' => 'Skema Warna Aplikasi',\n    'color_scheme_desc' => 'Atur warna-warna untuk digunakan dalam antar muka aplikasi. Warna-warna dapat diatur secara terpisah masing-masing untuk mode gelap dan terang agar sesuai dengan tema dan memastikan keterbacaan.',\n    'ui_colors_desc' => 'Atur warna primer aplikasi dan warna tautan default. Warna primer terutama digunakan untuk bilah tajuk, tombol, dan dekorasi antarmuka. Warna tautan default digunakan untuk tautan dan tindakan berbasis teks, baik dalam konten tertulis maupun dalam antarmuka aplikasi.',\n    'app_color' => 'Warna Utama',\n    'link_color' => 'Warna Tautan Default',\n    'content_colors_desc' => 'Tetapkan warna untuk semua elemen dalam hierarki organisasi halaman. Sebaiknya pilih warna dengan tingkat kecerahan yang mirip dengan warna default agar mudah dibaca.',\n    'bookshelf_color' => 'Warna Rak',\n    'book_color' => 'Warna Buku',\n    'chapter_color' => 'Warna Bab',\n    'page_color' => 'Warna Halaman',\n    'page_draft_color' => 'Warna Halaman Draf',\n\n    // Registration Settings\n    'reg_settings' => 'Pendaftaran',\n    'reg_enable' => 'Aktifkan Pendaftaran',\n    'reg_enable_toggle' => 'Aktifkan Pendaftaran',\n    'reg_enable_desc' => 'Saat pendaftaran diaktifkan, pengguna akan dapat mendaftar sendiri sebagai pengguna aplikasi. Setelah pendaftaran, mereka diberi peran pengguna default tunggal.',\n    'reg_default_role' => 'Peran pengguna default setelah pendaftaran',\n    'reg_enable_external_warning' => 'Opsi di atas diabaikan saat otentikasi LDAP atau SAML eksternal aktif. Akun pengguna untuk anggota yang tidak ada akan dibuat secara otomatis jika otentikasi, terhadap sistem eksternal yang digunakan, berhasil.',\n    'reg_email_confirmation' => 'Konfirmasi email',\n    'reg_email_confirmation_toggle' => 'Memerlukan konfirmasi email',\n    'reg_confirm_email_desc' => 'Jika batasan domain digunakan maka konfirmasi email akan diperlukan dan opsi ini akan diabaikan.',\n    'reg_confirm_restrict_domain' => 'Pembatasan Domain',\n    'reg_confirm_restrict_domain_desc' => 'Masukkan daftar domain email yang dipisahkan dengan koma yang ingin Anda batasi pendaftarannya. Pengguna akan dikirimi email untuk mengonfirmasi alamat mereka sebelum diizinkan untuk berinteraksi dengan aplikasi. <br> Perhatikan bahwa pengguna akan dapat mengubah alamat email mereka setelah pendaftaran berhasil.',\n    'reg_confirm_restrict_domain_placeholder' => 'Tidak ada batasan yang ditetapkan',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Pilih aturan sortir default yang akan diterapkan pada buku baru. Aturan ini tidak akan memengaruhi buku yang sudah ada, dan dapat diganti per buku.',\n    'sorting_rules' => 'Aturan Penyortiran',\n    'sorting_rules_desc' => 'Ini adalah operasi penyortiran yang telah ditetapkan sebelumnya yang dapat diterapkan pada konten dalam sistem.',\n    'sort_rule_assigned_to_x_books' => 'Dikaitkan ke :count Buku|Dikaitkan ke :count Buku',\n    'sort_rule_create' => 'Buat Aturan Penyortiran',\n    'sort_rule_edit' => 'Mengedit Aturan Penyortiran',\n    'sort_rule_delete' => 'Hapus Aturan Penyortiran',\n    'sort_rule_delete_desc' => 'Hapus aturan sortir ini dari sistem. Buku yang menggunakan sortir ini akan kembali ke sortir manual.',\n    'sort_rule_delete_warn_books' => 'Aturan sortir ini saat ini digunakan pada :count buku (banyak buku). Apakah Anda yakin ingin menghapus ini?',\n    'sort_rule_delete_warn_default' => 'Aturan sortir ini saat ini digunakan sebagai aturan baku untuk buku. Apakah Anda yakin ingin menghapusnya?',\n    'sort_rule_details' => 'Perincian Aturan Penyortiran',\n    'sort_rule_details_desc' => 'Tetapkan nama untuk aturan pengurutan ini, yang akan muncul dalam daftar saat pengguna memilih pengurutan.',\n    'sort_rule_operations' => 'Operasi Penyortiran',\n    'sort_rule_operations_desc' => 'Konfigurasikan tindakan sortir yang akan dilakukan dengan memindahkannya dari daftar operasi yang tersedia. Setelah digunakan, operasi akan diterapkan secara berurutan, dari atas ke bawah. Setiap perubahan yang dibuat di sini akan diterapkan ke semua buku yang ditetapkan setelah disimpan.',\n    'sort_rule_available_operations' => 'Operasi yang Tersedia',\n    'sort_rule_available_operations_empty' => 'Tidak ada operasi yang tersisa',\n    'sort_rule_configured_operations' => 'Operasi yang Sudah Dikonfigurasi',\n    'sort_rule_configured_operations_empty' => 'Seret/tambahkan operasi dari daftar \"Operasi yang Tersedia\"',\n    'sort_rule_op_asc' => '(Naik)',\n    'sort_rule_op_desc' => '(Turun)',\n    'sort_rule_op_name' => 'Nama - Alfabetis',\n    'sort_rule_op_name_numeric' => 'Nama - Numerik',\n    'sort_rule_op_created_date' => 'Tanggal Dibuat',\n    'sort_rule_op_updated_date' => 'Tanggal Pembaruan',\n    'sort_rule_op_chapters_first' => 'Bab di Urutan Pertama',\n    'sort_rule_op_chapters_last' => 'Bab di Urutan Terakhir',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Pemeliharaan',\n    'maint_image_cleanup' => 'Gambar Bersihkan',\n    'maint_image_cleanup_desc' => 'Pindai halaman & konten revisi untuk memeriksa gambar dan gambar mana yang saat ini digunakan dan gambar mana yang berlebihan. Pastikan Anda membuat database lengkap dan cadangan gambar sebelum menjalankan ini.',\n    'maint_delete_images_only_in_revisions' => 'Hapus juga gambar yang hanya ada di revisi halaman lama',\n    'maint_image_cleanup_run' => 'Jalankan Pembersihan',\n    'maint_image_cleanup_warning' => ':count ditemukan gambar yang berpotensi tidak digunakan. Anda yakin ingin menghapus gambar-gambar ini?',\n    'maint_image_cleanup_success' => ':count gambar yang mungkin tidak digunakan ditemukan dan dihapus!',\n    'maint_image_cleanup_nothing_found' => 'Tidak ada gambar yang tidak digunakan ditemukan, Tidak ada yang dihapus!',\n    'maint_send_test_email' => 'Kirim Email Tes',\n    'maint_send_test_email_desc' => 'Ini mengirimkan email percobaan ke alamat email Anda yang ditentukan di profil Anda.',\n    'maint_send_test_email_run' => 'Kirim email tes',\n    'maint_send_test_email_success' => 'Email dikirim ke :address',\n    'maint_send_test_email_mail_subject' => 'Uji Email',\n    'maint_send_test_email_mail_greeting' => 'Pengiriman email sepertinya berhasil!',\n    'maint_send_test_email_mail_text' => 'Selamat! Saat Anda menerima pemberitahuan email ini, pengaturan email Anda tampaknya telah dikonfigurasi dengan benar.',\n    'maint_recycle_bin_desc' => 'Rak, buku, bab & halaman yang dihapus dikirim ke recycle bin sehingga dapat dipulihkan atau dihapus secara permanen. Item lama di recycle bin dapat dihapus secara otomatis setelah beberapa saat tergantung pada konfigurasi sistem.',\n    'maint_recycle_bin_open' => 'Buka Tempat Sampah',\n    'maint_regen_references' => 'Perbarui Referensi',\n    'maint_regen_references_desc' => 'Tindakan ini akan membangun kembali indeks referensi lintas item dalam basis data. Hal ini biasanya ditangani secara otomatis, tetapi tindakan ini dapat berguna untuk mengindeks konten lama atau konten yang ditambahkan melalui metode tidak resmi.',\n    'maint_regen_references_success' => 'Indeks referensi telah diperbarui!',\n    'maint_timeout_command_note' => 'Catatan: Tindakan ini memerlukan waktu untuk dijalankan, yang dapat menyebabkan masalah batas waktu di beberapa lingkungan web. Sebagai alternatif, tindakan ini dapat dilakukan menggunakan perintah terminal.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Tempat Sampah',\n    'recycle_bin_desc' => 'Di sini Anda dapat memulihkan item yang telah dihapus atau memilih untuk menghapusnya secara permanen dari sistem. Daftar ini tidak difilter, tidak seperti daftar aktivitas serupa di sistem tempat filter izin diterapkan.',\n    'recycle_bin_deleted_item' => 'Item yang Dihapus',\n    'recycle_bin_deleted_parent' => 'Induk',\n    'recycle_bin_deleted_by' => 'Dihapus Oleh',\n    'recycle_bin_deleted_at' => 'Waktu Penghapusan',\n    'recycle_bin_permanently_delete' => 'Hapus Permanen',\n    'recycle_bin_restore' => 'Mengembalikan',\n    'recycle_bin_contents_empty' => 'Hapus Secara Permanen',\n    'recycle_bin_empty' => 'Kosongkan Tempat Sampah',\n    'recycle_bin_empty_confirm' => 'Ini akan menghancurkan secara permanen semua item di tempat sampah termasuk konten yang ada di dalam setiap item. Anda yakin ingin mengosongkan tempat sampah?',\n    'recycle_bin_destroy_confirm' => 'Tindakan ini akan menghapus item ini secara permanen dari sistem, beserta semua elemen turunan yang tercantum di bawah ini, dan Anda tidak akan dapat memulihkan konten ini. Apakah Anda yakin ingin menghapus item ini secara permanen?',\n    'recycle_bin_destroy_list' => 'Item yang akan Dihancurkan',\n    'recycle_bin_restore_list' => 'Item yang akan Dipulihkan',\n    'recycle_bin_restore_confirm' => 'Tindakan ini akan memulihkan item yang dihapus, termasuk semua elemen anak, ke lokasi aslinya. Jika lokasi asli telah dihapus, dan sekarang berada di keranjang sampah, item induk juga perlu dipulihkan.',\n    'recycle_bin_restore_deleted_parent' => 'Induk item ini juga telah dihapus. Ini akan tetap dihapus sampai induknya juga dipulihkan.',\n    'recycle_bin_restore_parent' => 'Pulihkan Induk',\n    'recycle_bin_destroy_notification' => 'Total :count item dari tempat sampah.',\n    'recycle_bin_restore_notification' => 'Total :count item yang dipulihkan dari tempat sampah.',\n\n    // Audit Log\n    'audit' => 'Log Audit',\n    'audit_desc' => 'Log audit ini menampilkan daftar aktivitas yang dilacak dalam sistem. Daftar ini tidak difilter, tidak seperti daftar aktivitas serupa di sistem tempat filter izin diterapkan.',\n    'audit_event_filter' => 'Filter Peristiwa',\n    'audit_event_filter_no_filter' => 'Tanpa Filter',\n    'audit_deleted_item' => 'Item yang Dihapus',\n    'audit_deleted_item_name' => 'Nama :name',\n    'audit_table_user' => 'Pengguna',\n    'audit_table_event' => 'Peristiwa',\n    'audit_table_related' => 'Item atau Detail Terkait',\n    'audit_table_ip' => 'Alamat IP',\n    'audit_table_date' => 'Tanggal Kegiatan',\n    'audit_date_from' => 'Rentang Tanggal Dari',\n    'audit_date_to' => 'Rentang Tanggal Sampai',\n\n    // Role Settings\n    'roles' => 'Peran',\n    'role_user_roles' => 'Peran Pengguna',\n    'roles_index_desc' => 'Peran digunakan untuk mengelompokkan pengguna & memberikan izin sistem kepada anggotanya. Jika pengguna menjadi anggota beberapa peran, hak istimewa yang diberikan akan bertumpuk dan pengguna akan mewarisi semua kemampuan.',\n    'roles_x_users_assigned' => ':count pengguna yang ditetapkan|:count pengguna-pengguna yang ditetapkan',\n    'roles_x_permissions_provided' => ':count izin|:count izin-izin',\n    'roles_assigned_users' => 'Pengguna yang Ditetapkan',\n    'roles_permissions_provided' => 'Izin yang diberikan',\n    'role_create' => 'Buat Peran Baru',\n    'role_delete' => 'Hapus Peran',\n    'role_delete_confirm' => 'Ini akan menghapus peran dengan nama \\':roleName\\'.',\n    'role_delete_users_assigned' => 'Peran ini memiliki :userCount pengguna yang ditugaskan padanya. Jika Anda ingin memindahkan pengguna dari peran ini pilih peran baru di bawah.',\n    'role_delete_no_migration' => \"Jangan migrasikan pengguna\",\n    'role_delete_sure' => 'Anda yakin ingin menghapus peran ini?',\n    'role_edit' => 'Edit Peran',\n    'role_details' => 'Detail Peran',\n    'role_name' => 'Nama peran',\n    'role_desc' => 'Deskripsi Singkat Peran',\n    'role_mfa_enforced' => 'Membutuhkan Otentikasi Multi Faktor',\n    'role_external_auth_id' => 'Otentikasi Eksternal IDs',\n    'role_system' => 'Izin Sistem',\n    'role_manage_users' => 'Kelola pengguna',\n    'role_manage_roles' => 'Kelola peran & izin peran',\n    'role_manage_entity_permissions' => 'Kelola semua izin buku, bab & halaman',\n    'role_manage_own_entity_permissions' => 'Kelola izin di buku, bab & halaman sendiri',\n    'role_manage_page_templates' => 'Kelola template halaman',\n    'role_access_api' => 'Akses Sistem API',\n    'role_manage_settings' => 'Kelola setelan aplikasi',\n    'role_export_content' => 'Ekspor konten',\n    'role_import_content' => 'Impor Konten',\n    'role_editor_change' => 'Ubah editor halaman',\n    'role_notifications' => 'Terima dan kelola notifikasi',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Izin Aset',\n    'roles_system_warning' => 'Ketahuilah bahwa akses ke salah satu dari tiga izin di atas dapat memungkinkan pengguna untuk mengubah hak mereka sendiri atau orang lain dalam sistem. Hanya tetapkan peran dengan izin ini untuk pengguna tepercaya.',\n    'role_asset_desc' => 'Izin ini mengontrol akses default ke aset dalam sistem. Izin pada Buku, Bab, dan Halaman akan menggantikan izin ini.',\n    'role_asset_admins' => 'Admin secara otomatis diberi akses ke semua konten tetapi opsi ini dapat menampilkan atau menyembunyikan opsi UI.',\n    'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'Semua',\n    'role_own' => 'Sendiri',\n    'role_controlled_by_asset' => 'Dikendalikan oleh aset tempat mereka diunggah',\n    'role_save' => 'Simpan Peran',\n    'role_users' => 'Peran berhasil diperbarui',\n    'role_users_none' => 'Saat ini tidak ada pengguna yang ditugaskan untuk peran ini',\n\n    // Users\n    'users' => 'Pengguna',\n    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',\n    'user_profile' => 'Profil Pengguna',\n    'users_add_new' => 'Tambahkan pengguna baru',\n    'users_search' => 'Cari Pengguna',\n    'users_latest_activity' => 'Aktivitas Terbaru',\n    'users_details' => 'Detail Pengguna',\n    'users_details_desc' => 'Tetapkan nama tampilan dan alamat email untuk pengguna ini. Alamat email akan digunakan untuk masuk ke aplikasi.',\n    'users_details_desc_no_email' => 'Tetapkan nama tampilan untuk pengguna ini agar orang lain dapat mengenalinya.',\n    'users_role' => 'Peran Pengguna',\n    'users_role_desc' => 'Pilih peran mana yang akan ditetapkan untuk pengguna ini. Jika pengguna ditetapkan ke beberapa peran, izin dari peran tersebut akan bertumpuk dan mereka akan menerima semua kemampuan dari peran yang ditetapkan.',\n    'users_password' => 'Kata Sandi Pengguna',\n    'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',\n    'users_send_invite_text' => 'Anda dapat memilih untuk mengirimi pengguna ini email undangan yang memungkinkan mereka menyetel sandi mereka sendiri, atau Anda dapat menyetel sandi mereka sendiri.',\n    'users_send_invite_option' => 'Kirim email undangan pengguna',\n    'users_external_auth_id' => 'Otentikasi Eksternal ID',\n    'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.',\n    'users_password_warning' => 'Only fill the below if you would like to change the password for this user.',\n    'users_system_public' => 'Pengguna ini mewakili semua pengguna tamu yang mengunjungi instance Anda. Ini tidak dapat digunakan untuk masuk tetapi ditetapkan secara otomatis.',\n    'users_delete' => 'Hapus pengguna',\n    'users_delete_named' => 'Hapus Pengguna :userName',\n    'users_delete_warning' => 'Ini sepenuhnya akan menghapus pengguna ini dengan nama \\':userName\\' dari sistem.',\n    'users_delete_confirm' => 'Apakah Anda yakin ingin menghapus pengguna ini?',\n    'users_migrate_ownership' => 'Migrasikan Kepemilikan',\n    'users_migrate_ownership_desc' => 'Pilih pengguna di sini jika Anda ingin pengguna lain menjadi pemilik semua item yang saat ini dimiliki oleh pengguna ini.',\n    'users_none_selected' => 'Tidak ada pengguna yang dipilih',\n    'users_edit' => 'Edit Pengguna',\n    'users_edit_profile' => 'Edit Profil',\n    'users_avatar' => 'Abatar Pengguna',\n    'users_avatar_desc' => 'Pilih gambar untuk mewakili pengguna ini. berukuran 256px.',\n    'users_preferred_language' => 'Bahasa Pilihan',\n    'users_preferred_language_desc' => 'Opsi ini akan mengubah bahasa yang digunakan untuk antarmuka pengguna aplikasi. Ini tidak akan memengaruhi konten yang dibuat pengguna.',\n    'users_social_accounts' => 'Akun Sosial',\n    'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.',\n    'users_social_accounts_info' => 'Di sini Anda dapat menghubungkan akun Anda yang lain untuk login yang lebih cepat dan mudah. Memutuskan akun di sini tidak mencabut akses resmi sebelumnya. Cabut akses dari pengaturan profil Anda pada akun sosial yang terhubung.',\n    'users_social_connect' => 'Hubungkan Akun',\n    'users_social_disconnect' => 'Putuskan Sambungan Akun',\n    'users_social_status_connected' => 'Terhubung',\n    'users_social_status_disconnected' => 'Terputus',\n    'users_social_connected' => ':socialAccount akun berhasil dilampirkan ke profil Anda.',\n    'users_social_disconnected' => ':socialAccount akun berhasil diputuskan dari profil Anda.',\n    'users_api_tokens' => 'Token API',\n    'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.',\n    'users_api_tokens_none' => 'Tidak ada token API yang telah dibuat untuk pengguna ini',\n    'users_api_tokens_create' => 'Buat Token',\n    'users_api_tokens_expires' => 'Kedaluwarsa',\n    'users_api_tokens_docs' => 'Dokumentasi API',\n    'users_mfa' => 'Otentikasi Multi Faktor',\n    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'users_mfa_x_methods' => ':count method configured|:count methods configured',\n    'users_mfa_configure' => 'Configure Methods',\n\n    // API Tokens\n    'user_api_token_create' => 'Buat Token API',\n    'user_api_token_name' => 'Nama',\n    'user_api_token_name_desc' => 'Berikan token Anda nama yang dapat dibaca sebagai pengingat masa depan akan tujuan yang dimaksudkan.',\n    'user_api_token_expiry' => 'Tanggal kadaluarsa',\n    'user_api_token_expiry_desc' => 'Setel tanggal token ini kedaluwarsa. Setelah tanggal ini, permintaan yang dibuat menggunakan token ini tidak akan berfungsi lagi. Mengosongkan bidang ini akan menetapkan masa berlaku 100 tahun ke depan.',\n    'user_api_token_create_secret_message' => 'Segera setelah membuat token ini, \"Token ID\" & \"Token Secret\" akan dibuat dan ditampilkan. Rahasianya hanya akan ditampilkan satu kali jadi pastikan untuk menyalin nilainya ke tempat yang aman dan terlindungi sebelum melanjutkan.',\n    'user_api_token' => 'Token API',\n    'user_api_token_id' => 'Token ID',\n    'user_api_token_id_desc' => 'Ini adalah sebuah pengenal yang dihasilkan oleh sistem yang tidak dapat disunting untuk token ini yang perlu untuk disediakan dalam permintaan API.',\n    'user_api_token_secret' => 'Token Secret',\n    'user_api_token_secret_desc' => 'Ini adalah rahasia yang dihasilkan sistem untuk token ini yang perlu disediakan dalam permintaan API. Ini hanya akan ditampilkan kali ini jadi salin nilai ini ke tempat yang aman dan terlindungi.',\n    'user_api_token_created' => 'Token dibuat :timeAgo',\n    'user_api_token_updated' => 'Token diperbarui :timeAgo',\n    'user_api_token_delete' => 'Hapus Token',\n    'user_api_token_delete_warning' => 'Ini akan sepenuhnya menghapus token API ini dengan nama \\': tokenName\\' dari sistem.',\n    'user_api_token_delete_confirm' => 'Anda yakin ingin menghapus token API ini?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',\n    'webhooks_x_trigger_events' => ':count trigger event|:count trigger events',\n    'webhooks_create' => 'Create New Webhook',\n    'webhooks_none_created' => 'No webhooks have yet been created.',\n    'webhooks_edit' => 'Edit Webhook',\n    'webhooks_save' => 'Save Webhook',\n    'webhooks_details' => 'Webhook Details',\n    'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',\n    'webhooks_events' => 'Webhook Events',\n    'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',\n    'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\\'t expose confidential content.',\n    'webhooks_events_all' => 'All system events',\n    'webhooks_name' => 'Webhook Name',\n    'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',\n    'webhooks_endpoint' => 'Webhook Endpoint',\n    'webhooks_active' => 'Webhook Active',\n    'webhook_events_table_header' => 'Events',\n    'webhooks_delete' => 'Delete Webhook',\n    'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \\':webhookName\\', from the system.',\n    'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?',\n    'webhooks_format_example' => 'Webhook Format Example',\n    'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The \"related_item\" and \"url\" properties are optional and will depend on the type of event triggered.',\n    'webhooks_status' => 'Webhook Status',\n    'webhooks_last_called' => 'Last Called:',\n    'webhooks_last_errored' => 'Last Errored:',\n    'webhooks_last_error_message' => 'Last Error Message:',\n\n    // Licensing\n    'licenses' => 'Licenses',\n    'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.',\n    'licenses_bookstack' => 'Lisensi BookStack',\n    'licenses_php' => 'Lisensi Pustaka PHP',\n    'licenses_js' => 'Lisensi Pustaka JavaScript',\n    'licenses_other' => 'Lisensi Lainnya',\n    'license_details' => 'Perincian Lisensi',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Katalan',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/id/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute harus diterima.',\n    'active_url'           => ':attribute bukan URL yang valid.',\n    'after'                => ':attribute harus setelah tanggal :date.',\n    'alpha'                => ':attribute hanya boleh berisi huruf.',\n    'alpha_dash'           => ':attribute hanya boleh berisi huruf, angka, tanda hubung, dan garis bawah.',\n    'alpha_num'            => ':attribute hanya boleh berisi huruf dan angka.',\n    'array'                => ':attribute harus berupa larik.',\n    'backup_codes'         => 'Kode yang diberikan tidak valid atau telah digunakan.',\n    'before'               => ':attribute harus tanggal sebelum :date.',\n    'between'              => [\n        'numeric' => ':attribute harus di antara :min dan :max.',\n        'file'    => ':attribute harus diantara :min dan :max kilobyte.',\n        'string'  => ':attribute harus memiliki karakter antara :min dan :max.',\n        'array'   => ':attribute harus memiliki item antara :min dan :max.',\n    ],\n    'boolean'              => ':attribute bidang harus berisi benar atau salah.',\n    'confirmed'            => ':attribute konfirmasi tidak sama.',\n    'date'                 => ':attribute bukan tanggal yang valid.',\n    'date_format'          => ':attribute tidak sesuai dengan format :format.',\n    'different'            => ':attribute dan :other harus berbeda.',\n    'digits'               => ':attribute harus :digits digit.',\n    'digits_between'       => ':attribute harus diantara :min dan :max digit.',\n    'email'                => ':attrtibute Harus alamat e-mail yang valid.',\n    'ends_with' => ':attribute harus diakhiri dengan salah satu dari berikut ini: :values',\n    'file'                 => ':attribute harus diberikan sebagai file yang valid.',\n    'filled'               => ':attribute bidang diperlukan.',\n    'gt'                   => [\n        'numeric' => ':attribute harus lebih besar dari :value.',\n        'file'    => ':attribute harus lebih besar dari :value kilobyte.',\n        'string'  => ':attribute harus lebih besar dari :value karakter.',\n        'array'   => ':attribute harus memiliki lebih dari item :value.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute harus lebih besar dari atau sama dengan :value.',\n        'file'    => ':attribute harus lebih besar dari atau sama dengan :value kilobyte.',\n        'string'  => ':attribute harus lebih besar dari atau sama dengan karakter :value.',\n        'array'   => ':attribute harus memiliki :value item atau lebih.',\n    ],\n    'exists'               => ':attribute yang dipilih tidak valid.',\n    'image'                => ':attribute harus berupa gambar.',\n    'image_extension'      => ':attribute harus memiliki ekstensi gambar yang valid & didukung.',\n    'in'                   => ':attribute yang dipilih tidak valid.',\n    'integer'              => ':attribute harus berupa bilangan bulat.',\n    'ip'                   => ':attribute harus berupa alamat IP yang valid.',\n    'ipv4'                 => ':attribute harus berupa alamat IPv4 yang valid.',\n    'ipv6'                 => ':attribute harus berupa alamat IPv6 yang valid.',\n    'json'                 => ':attribute harus berupa string JSON yang valid.',\n    'lt'                   => [\n        'numeric' => ':attribute harus kurang dari :value.',\n        'file'    => ':attribute harus kurang dari :value kilobyte.',\n        'string'  => ':attribute harus kurang dari :value karakter.',\n        'array'   => ':attribute harus memiliki kurang dari :value item.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute harus kurang dari atau sama dengan :value.',\n        'file'    => ':attribute harus kurang dari atau sama dengan :value kilobyte.',\n        'string'  => ':attribute harus kurang dari atau sama dengan :value karakter.',\n        'array'   => ':attribute tidak boleh memiliki lebih dari :value item.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute tidak boleh lebih dari :max.',\n        'file'    => ':attribute tidak boleh lebih dari :max kilobyte.',\n        'string'  => ':attribute tidak boleh lebih dari :max karakter.',\n        'array'   => ':attribute tidak boleh memiliki lebih dari :max item.',\n    ],\n    'mimes'                => ':attribute harus berupa file dengan tipe: :value.',\n    'min'                  => [\n        'numeric' => ':attribute minimal harus :min.',\n        'file'    => ':attribute minimal harus :min kilobyte.',\n        'string'  => ':attribute setidaknya harus :min karakter.',\n        'array'   => ':attribute minimal harus memiliki :min item.',\n    ],\n    'not_in'               => ':attribute yang dipilih tidak valid.',\n    'not_regex'            => ':attribute format tidak valid.',\n    'numeric'              => ':attribute harus berupa nomot.',\n    'regex'                => 'Format :attribute tidak valid.',\n    'required'             => ':attribute bidang harus diisi.',\n    'required_if'          => ':attribute Bidang harus diisi saat :other atau :value.',\n    'required_with'        => 'Bidang :attribute harus diisi jika ada :nilai.',\n    'required_with_all'    => 'Bidang :attribute harus diisi jika ada :values.',\n    'required_without'     => 'Bidang :attribute harus diisi jika :values tidak ada.',\n    'required_without_all' => 'Bidang :attribute harus diisi jika tidak ada :value yang ada.',\n    'same'                 => ':attribute dan :other harus sama.',\n    'safe_url'             => 'Tautan yang diberikan mungkin tidak aman.',\n    'size'                 => [\n        'numeric' => ':attribute harus berukuran :size.',\n        'file'    => ':attribute harus berukuran :size kilobyte.',\n        'string'  => ':attribute harus memiliki karakter berukuran :size.',\n        'array'   => ':attribute harus mengandung :size item.',\n    ],\n    'string'               => ':attribute harus berupa string.',\n    'timezone'             => ':attribute harus menjadi zona yang valid.',\n    'totp'                 => 'Kode yang diberikan tidak valid atau telah kedaluwarsa.',\n    'unique'               => ':attribute sudah diambil.',\n    'url'                  => ':attribute format tidak valid.',\n    'uploaded'             => 'Berkas tidak dapat diunggah. Server mungkin tidak menerima berkas dengan ukuran ini.',\n\n    'zip_file' => ':attribute perlu merujuk ke sebuah file yang terdapat di dalam arsip ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => ':attribute seharusnya berupa file dengan tipe :validTypes, tapi yang Anda unggah bertipe :foundType.',\n    'zip_model_expected' => 'Diharapkan sebuah objek data, namun yang ditemukan adalah \\':type\\'.',\n    'zip_unique' => ':attribute harus bersifat unik untuk setiap jenis objek dalam file ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Konfirmasi kata sandi diperlukan',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/is/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'stofna síðu',\n    'page_create_notification'    => 'Síða stofnuð',\n    'page_update'                 => 'nafnlaus síða',\n    'page_update_notification'    => 'Síða uppfærð',\n    'page_delete'                 => 'síðu eytt',\n    'page_delete_notification'    => 'Tókst að eyða síðu',\n    'page_restore'                => 'endurvirkja síðu',\n    'page_restore_notification'   => 'Síða endurvirkjuð',\n    'page_move'                   => 'síða færð',\n    'page_move_notification'      => 'Tókst að færa síðu',\n\n    // Chapters\n    'chapter_create'              => 'kafli búinn til',\n    'chapter_create_notification' => 'Tókst að búa til kafla',\n    'chapter_update'              => 'kafli uppfærður',\n    'chapter_update_notification' => 'Tókst að uppfæra kafla',\n    'chapter_delete'              => 'eyddur kafli',\n    'chapter_delete_notification' => 'Tókst að eyða kafla',\n    'chapter_move'                => 'færður kafli',\n    'chapter_move_notification' => 'Tókst að færa kafla',\n\n    // Books\n    'book_create'                 => 'stofnuð bók',\n    'book_create_notification'    => 'Tókst að stofna bók',\n    'book_create_from_chapter'              => 'kafla breytt í bók',\n    'book_create_from_chapter_notification' => 'Tókst að breyta kafla í bók',\n    'book_update'                 => 'uppfærð bók',\n    'book_update_notification'    => 'Tókst að uppfæra bók',\n    'book_delete'                 => 'eydd bók',\n    'book_delete_notification'    => 'Tókst að eyða bók',\n    'book_sort'                   => 'flokkuð bók',\n    'book_sort_notification'      => 'Tókst að endurflokka bók',\n\n    // Bookshelves\n    'bookshelf_create'            => 'stofna hillu',\n    'bookshelf_create_notification'    => 'Tókst að stofna hillu',\n    'bookshelf_create_from_book'    => 'bók breytt i hillu',\n    'bookshelf_create_from_book_notification'    => 'Tókst að breyta bók í hillu',\n    'bookshelf_update'                 => 'uppærð hilla',\n    'bookshelf_update_notification'    => 'Tókst að uppfæra hillu',\n    'bookshelf_delete'                 => 'eydd hilla',\n    'bookshelf_delete_notification'    => 'Tókst að eyða hillu',\n\n    // Revisions\n    'revision_restore' => 'útgáfa bakfærð',\n    'revision_delete' => 'útgáfu eytt',\n    'revision_delete_notification' => 'Tókst að eyða útgáfu',\n\n    // Favourites\n    'favourite_add_notification' => 'hefur verið bætt í eftirlæti',\n    'favourite_remove_notification' => 'hefur verið eytt úr eftirlæti',\n\n    // Watching\n    'watch_update_level_notification' => 'Fylgjast með hefur verið uppfært',\n\n    // Auth\n    'auth_login' => 'skráður inn',\n    'auth_register' => 'skráður sem nýr notandi',\n    'auth_password_reset_request' => 'bað um nýtt lykilorð',\n    'auth_password_reset_update' => 'endurstilla lykilorð',\n    'mfa_setup_method' => 'valin MFA aðferð',\n    'mfa_setup_method_notification' => 'Fjölauðkenningar aðferð stillt',\n    'mfa_remove_method' => 'fjarlægja MFA aðferð',\n    'mfa_remove_method_notification' => 'Fjölauðkenningar aðferð fjarlægð',\n\n    // Settings\n    'settings_update' => 'uppfæra stillingar',\n    'settings_update_notification' => 'Tókst að uppfæra stillingar',\n    'maintenance_action_run' => 'keyrði uppfærslu',\n\n    // Webhooks\n    'webhook_create' => 'webhook búin til',\n    'webhook_create_notification' => 'Tókst að búa til Webhook',\n    'webhook_update' => 'webhook uppfærður',\n    'webhook_update_notification' => 'Tókst að uppfæra Webhook',\n    'webhook_delete' => 'eyða Webhook',\n    'webhook_delete_notification' => 'Tókst að eyða Webhook',\n\n    // Imports\n    'import_create' => 'búa til innlestur',\n    'import_create_notification' => 'Innlestur tókst',\n    'import_run' => 'uppfæra innlestur',\n    'import_run_notification' => 'Tókst að lesa inn',\n    'import_delete' => 'innlestri eytt',\n    'import_delete_notification' => 'Tókst að eyða innlestri',\n\n    // Users\n    'user_create' => 'stofnaður notandi',\n    'user_create_notification' => 'Tókst að stofna notanda',\n    'user_update' => 'uppfærður notandi',\n    'user_update_notification' => 'Tókst að uppfæra notanda',\n    'user_delete' => 'eyddur notandi',\n    'user_delete_notification' => 'Tókst að eyða notanda',\n\n    // API Tokens\n    'api_token_create' => 'API token búið til',\n    'api_token_create_notification' => 'Tókst að búa til API tóka',\n    'api_token_update' => 'API tóki uppfærður',\n    'api_token_update_notification' => 'Tókst að uppfæra API tóka',\n    'api_token_delete' => 'eyddur API tóki',\n    'api_token_delete_notification' => 'Tókst að eyða API tóka',\n\n    // Roles\n    'role_create' => 'stofnað hlutverk',\n    'role_create_notification' => 'Tókst að stofna hlutverk',\n    'role_update' => 'hlutverk uppfært',\n    'role_update_notification' => 'Tókst að uppfæra hlutverk',\n    'role_delete' => 'eytt hlutverk',\n    'role_delete_notification' => 'Tókst að eyða hlutverki',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'tæmd ruslatunna',\n    'recycle_bin_restore' => 'endurheimt úr ruslatunnu',\n    'recycle_bin_destroy' => 'fjarlægt úr ruslatunnu',\n\n    // Comments\n    'commented_on'                => 'athugasemd á',\n    'comment_create'              => 'athugasemd bætt við',\n    'comment_update'              => 'athugasemd uppfærð',\n    'comment_delete'              => 'athugasemd eytt',\n\n    // Sort Rules\n    'sort_rule_create' => 'created sort rule',\n    'sort_rule_create_notification' => 'Sort rule successfully created',\n    'sort_rule_update' => 'updated sort rule',\n    'sort_rule_update_notification' => 'Sort rule successfully updated',\n    'sort_rule_delete' => 'deleted sort rule',\n    'sort_rule_delete_notification' => 'Sort rule successfully deleted',\n\n    // Other\n    'permissions_update'          => 'uppfærðar heimildir',\n];\n"
  },
  {
    "path": "lang/is/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Þeesi auðkenning er ekki á skrá.',\n    'throttle' => 'Of margar tilraunir til innskráningar. Reyndu aftur eftir :seconds sekúndur.',\n\n    // Login & Register\n    'sign_up' => 'Nýskrá',\n    'log_in' => 'Innskrá',\n    'log_in_with' => 'Innskrá með :socialDriver',\n    'sign_up_with' => 'Búa til aðgang með :socialDriver',\n    'logout' => 'Skrá út',\n\n    'name' => 'Nafn',\n    'username' => 'Notandanafn',\n    'email' => 'Netfang',\n    'password' => 'Lykilorð',\n    'password_confirm' => 'Staðfestu lykilorð',\n    'password_hint' => 'Verður að vera minnst 8 stafir',\n    'forgot_password' => 'Gleymt lykilorð?',\n    'remember_me' => 'Geyma innskráningarupplýsingar',\n    'ldap_email_hint' => 'Settu inn netfang til að nota þennan aðgang.',\n    'create_account' => 'Stofna aðgang',\n    'already_have_account' => 'Þegar með notandaaðgang?',\n    'dont_have_account' => 'Ekki með aðgang?',\n    'social_login' => 'Innskráning með samfélagsmiðli',\n    'social_registration' => 'Skráning samfélagsmiðils',\n    'social_registration_text' => 'Skráðu þig og innskrá með annari þjónustu.',\n\n    'register_thanks' => 'Takk fyrir að skrá þig!',\n    'register_confirm' => 'Skoðaðu tölvupóstinn þinn og smelltu á staðfestingarhlekkinn :appName.',\n    'registrations_disabled' => 'Skráningar eru óvirkar í augnablikinu',\n    'registration_email_domain_invalid' => 'Þetta lén hefur ekki aðgang að þessu forriti',\n    'register_success' => 'Takk fyrir að skrá þig, nú ertu innskráðursem notandi.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Reyni innskráningu',\n    'auto_init_starting_desc' => 'Reyni að tengjast auðkenningarþjónustu, ef ekkert gerist innan 5 sekúndna getur þú smellt á hlekkinn hér að neðan.',\n    'auto_init_start_link' => 'Halda áfram með auðkenningu',\n\n    // Password Reset\n    'reset_password' => 'Endurstilla lykilorð',\n    'reset_password_send_instructions' => 'Settu netfangið þitt hér að neðan og þú færð tölvupóst með endurstillingar hlekk.',\n    'reset_password_send_button' => 'Senda hlekk',\n    'reset_password_sent' => 'Endurstillingar hlekkur hefur verið sendur í tölvupósti :email ef netfangið er á skrá.',\n    'reset_password_success' => 'Lykilorðið þitt hefur verið endurstillt.',\n    'email_reset_subject' => 'Endurstilla :appName lykilorðið þitt',\n    'email_reset_text' => 'Þú fékkst þennan tölvupóst því að beðið var um endurstillingu lykilorðs á þínum aðgangi.',\n    'email_reset_not_requested' => 'Ef þú baðst ekki um endurstillingu lykilorðs þarftu ekki að gera neitt.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Staðfestu netfangið þitt á :appName',\n    'email_confirm_greeting' => 'Takk fyrir að skrá þig á :appName!',\n    'email_confirm_text' => 'Vinsamlegast staðfestu netfangið þitt með því að smella á hnappin hér fyrir neðan:',\n    'email_confirm_action' => 'Staðfesta netfang',\n    'email_confirm_send_error' => 'Staðfesting netfangs er nauðsynleg en kerfið gat ekki sent póst, vinsamlegast hafið samband við kerfisstjóra.',\n    'email_confirm_success' => 'Netfang þitt hefur verið staðfest, þú ættir nú að geta skráð þig inn með þessu netfangi.',\n    'email_confirm_resent' => 'Staðfestingar tölvupóstur hefur verið sendur, kíktu í póshólfið þitt.',\n    'email_confirm_thanks' => 'Takk fyrir að staðfesta!',\n    'email_confirm_thanks_desc' => 'Hinkraðu smá á meðan staðfestingin þín er í vinnslu, ef ekkert gerist eftir 3 sekúndur, smelltu á \"Halda áfram\" hlekkinn hér fyrir neðan.',\n\n    'email_not_confirmed' => 'Netfang hefur ekki verið staðfest',\n    'email_not_confirmed_text' => 'Netfangið þitt hefur ekki enn verið staðfest.',\n    'email_not_confirmed_click_link' => 'Vinsamlegast smelltu á hlekkinn sem barst þér í tölvupósti eftir skráningu.',\n    'email_not_confirmed_resend' => 'Ef þú finnur ekki tölvupóstinn sem var sendur á þig, getur þú fengið hann endursendann með því að fylla út formið hér að neðan.',\n    'email_not_confirmed_resend_button' => 'Endursenda staðfestingarpóst',\n\n    // User Invite\n    'user_invite_email_subject' => 'Þér hefur verið boðið að tengjast :appName!',\n    'user_invite_email_greeting' => 'Það hefur verið stofnaður aðgangur fyrir ig á :appName.',\n    'user_invite_email_text' => 'Smelltu á hnappinn fyrir neðan til að setja upp lykilorð og fá aðgang:',\n    'user_invite_email_action' => 'Settu inn lykilorð',\n    'user_invite_page_welcome' => 'Velkominn á :appName!',\n    'user_invite_page_text' => 'Til að ljúka við uppsetningu og fá aðgang að :appName verður þú að velja þér lykilorð.',\n    'user_invite_page_confirm_button' => 'Staðfestu lykilorð',\n    'user_invite_success_login' => 'Lykilorð klárt, nú ættir þú að geta skráð þig inn á :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Setja upp tvöfalda auðkenningu',\n    'mfa_setup_desc' => 'Tvöföld euðkenning er viðbótar vörn til að tryggja aðganginn þinn.',\n    'mfa_setup_configured' => 'Þegar uppsett',\n    'mfa_setup_reconfigure' => 'Endurstilla',\n    'mfa_setup_remove_confirmation' => 'Ertu viss um að þú viljið fjarlæga þessa auðkenningarleið?',\n    'mfa_setup_action' => 'Uppsetning',\n    'mfa_backup_codes_usage_limit_warning' => 'Þú átt færri en 5 tilraunir eftir. Búðu til og geymdu hjá þér fleiri tilraunir svo þú læsist ekki úti.',\n    'mfa_option_totp_title' => 'App',\n    'mfa_option_totp_desc' => 'Til að virkja tvöfalda auðkenningu verður þú að hafa app í símanum sem styður TOPT, til dæmis Google Authenticator, Authy eða Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Varakóðar',\n    'mfa_option_backup_codes_desc' => 'Býr til sett af einskiptis kóðum sem þú getur notað til að auðkenna þig með. Geymdu þessa kóða á öruggum stað.',\n    'mfa_gen_confirm_and_enable' => 'Staðfesta og virkja',\n    'mfa_gen_backup_codes_title' => 'Stillingar varakóða',\n    'mfa_gen_backup_codes_desc' => 'Geymdu listann af kóðum á öruggum stað. Þú getur notað þessa kóða sem auka auðkenningu.',\n    'mfa_gen_backup_codes_download' => 'Hala niður kóðum',\n    'mfa_gen_backup_codes_usage_warning' => 'Hver kóði getur bara verið notaður einu sinni',\n    'mfa_gen_totp_title' => 'Uppsetning Apps',\n    'mfa_gen_totp_desc' => 'Til að virkja tvöfalda auðkenningu verður þú að hafa app í símanum sem styður TOPT, til dæmis Google Authenticator, Authy eða Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Skannaðu QR kóðann með appinu sem þú notar fyrir tvöfalda auðkenningu.',\n    'mfa_gen_totp_verify_setup' => 'Staðfesta uppsetningu',\n    'mfa_gen_totp_verify_setup_desc' => 'Staðfestu að allt virki með því að setja inn kóða úr síma appinu þínu hér fyrir neðan:',\n    'mfa_gen_totp_provide_code_here' => 'Sláðu inn kóða úr auðkennningar appi',\n    'mfa_verify_access' => 'Staðfesta aðgang',\n    'mfa_verify_access_desc' => 'Aðgangurinn þinn þarf viðbótar auðkenningu, veldu auðkenningarleið.',\n    'mfa_verify_no_methods' => 'Engar aðferðir stilltar',\n    'mfa_verify_no_methods_desc' => 'Engin aukaauðkenningar aðferð fannst. Þú verður að setja upp minnst eina viðbótarauðkenningu til að halda áfram.',\n    'mfa_verify_use_totp' => 'Staðfestu með farsíma appi',\n    'mfa_verify_use_backup_codes' => 'Staðfesta með varakóða',\n    'mfa_verify_backup_code' => 'Varakóði',\n    'mfa_verify_backup_code_desc' => 'Settu inn einn af varakóðunum þínum hér að neðan:',\n    'mfa_verify_backup_code_enter_here' => 'Sláðu inn varakóða hér',\n    'mfa_verify_totp_desc' => 'Sláðu inn kóðann úr auðkenningar appinu úr símanum þínum:',\n    'mfa_setup_login_notification' => 'Tvöföld auðkenning stillt. Skráðu þig nú inn með euðkenningarleiðinni.',\n];\n"
  },
  {
    "path": "lang/is/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Hætta við',\n    'close' => 'Loka',\n    'confirm' => 'Staðfesta',\n    'back' => 'Til baka',\n    'save' => 'Vista',\n    'continue' => 'Halda áfram',\n    'select' => 'Velja',\n    'toggle_all' => 'Velja allt',\n    'more' => 'Meira',\n\n    // Form Labels\n    'name' => 'Nafn',\n    'description' => 'Lýsing',\n    'role' => 'Hlutverk',\n    'cover_image' => 'Forsíðumynd',\n    'cover_image_description' => 'Myndin ætti að fera u. þ. b 440x250px þótt hún verði sköluð og kroppuð eftir þörfum, þannig að endanleg stærð mun endurspegla það.',\n\n    // Actions\n    'actions' => 'Aðgerðir',\n    'view' => 'Skoða',\n    'view_all' => 'Skoða allt',\n    'new' => 'Ný',\n    'create' => 'Búa til',\n    'update' => 'Uppfæra',\n    'edit' => 'Breyta',\n    'archive' => 'Archive',\n    'unarchive' => 'Un-Archive',\n    'sort' => 'Flokka',\n    'move' => 'Færa',\n    'copy' => 'Afrita',\n    'reply' => 'Svara',\n    'delete' => 'Eyða',\n    'delete_confirm' => 'Staðfesta eyðingu',\n    'search' => 'Leita',\n    'search_clear' => 'Hreinsa leit',\n    'reset' => 'Endurstilla',\n    'remove' => 'Fjarlægja',\n    'add' => 'Bæta við',\n    'configure' => 'Stilla',\n    'manage' => 'Stýra',\n    'fullscreen' => 'Fylla skjá',\n    'favourite' => 'Eftirlæti',\n    'unfavourite' => 'Fjarlægja úr eftirlæti',\n    'next' => 'Næst',\n    'previous' => 'Fyrri',\n    'filter_active' => 'Virk sía:',\n    'filter_clear' => 'Hreinsa síu',\n    'download' => 'Hlaða niður',\n    'open_in_tab' => 'Opna í flipa',\n    'open' => 'Opna',\n\n    // Sort Options\n    'sort_options' => 'Valkostir röðunar',\n    'sort_direction_toggle' => 'Flokkunarátt',\n    'sort_ascending' => 'Raða vaxandi',\n    'sort_descending' => 'Raða minnkandi',\n    'sort_name' => 'Nafn',\n    'sort_default' => 'Sjálfgefið',\n    'sort_created_at' => 'Stofnað þann',\n    'sort_updated_at' => 'Uppfært þann',\n\n    // Misc\n    'deleted_user' => 'Eyddur notandi',\n    'no_activity' => 'Engin virkni til að sýna',\n    'no_items' => 'Engir hlutir tiltækir',\n    'back_to_top' => 'Fara efst',\n    'skip_to_main_content' => 'Fara í aðalefni',\n    'toggle_details' => 'Virkja nánari sýn',\n    'toggle_thumbnails' => 'Sýna smámynd',\n    'details' => 'Nánari upplýsingar',\n    'grid_view' => 'Grid View',\n    'list_view' => 'Lista sýn',\n    'default' => 'Sjálfgefið',\n    'breadcrumb' => 'Brauðmolar',\n    'status' => 'Staða',\n    'status_active' => 'Virkt',\n    'status_inactive' => 'Óvirkt',\n    'never' => 'Aldrei',\n    'none' => 'Engin',\n\n    // Header\n    'homepage' => 'Forsíða',\n    'header_menu_expand' => 'Leiðarstjórn',\n    'profile_menu' => 'Prófíll',\n    'view_profile' => 'Skoða prófíl',\n    'edit_profile' => 'Breyta prófíl',\n    'dark_mode' => 'Dimmsnið',\n    'light_mode' => 'Ljóssnið',\n    'global_search' => 'Heildarleit',\n\n    // Layout tabs\n    'tab_info' => 'Upplýsingar',\n    'tab_info_label' => 'Tab: Sýna fleiri upplýsingar',\n    'tab_content' => 'Innihald',\n    'tab_content_label' => 'Tab: Sýna aðalinnihald',\n\n    // Email Content\n    'email_action_help' => 'Ef þú átt í vandræðum með að smella á \":actionText\" hnappinn, afritaðu og límdu slóðina í vefskoðarann þinn:',\n    'email_rights' => 'Höfundaréttur varinn',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Persónuverndarstefna',\n    'terms_of_service' => 'Skilmálar þjónustu',\n\n    // OpenSearch\n    'opensearch_description' => 'Leita :appName',\n];\n"
  },
  {
    "path": "lang/is/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Myndaval',\n    'image_list' => 'Myndalisti',\n    'image_details' => 'Uplýsingar myndar',\n    'image_upload' => 'Hlaða upp mynd',\n    'image_intro' => 'Hér getur þú valið og stjórnað þeim myndum sem þegar hefur verið upphlaðið.',\n    'image_intro_upload' => 'Hladdu upp nýrri mynd með því að draga hana inn í þennan glugga eða nota \"Hlaða upp\" hnappinn hér fyrir ofan.',\n    'image_all' => 'Allar',\n    'image_all_title' => 'Skoða allar myndir',\n    'image_book_title' => 'Skoða þær myndir sem þegar hefur verið hlaðið upp í þessa bók',\n    'image_page_title' => 'Skoða þær myndir sem þegar hefur verið hlaðið upp á þessa síðu',\n    'image_search_hint' => 'Leita af myndum eftir nafni',\n    'image_uploaded' => 'Hlaðið upp þann :uploadedDate',\n    'image_uploaded_by' => 'Hlaðið upp af :userName',\n    'image_uploaded_to' => 'Hlaðið upp á :pageLink',\n    'image_updated' => 'Uppfært þann :updateDate',\n    'image_load_more' => 'Hlaða fleirum',\n    'image_image_name' => 'Nafn myndar',\n    'image_delete_used' => 'Þessi mynd er notuð á eftirfarandi síðum.',\n    'image_delete_confirm_text' => 'Ertu viss um að þú viljir eyða þessari mynd?',\n    'image_select_image' => 'Velja mynd',\n    'image_dropzone' => 'Dragðu myndir eða smelltu hér til að hlaða upp',\n    'image_dropzone_drop' => 'Dragðu myndir hingað til að hlaða upp',\n    'images_deleted' => 'Myndum eytt',\n    'image_preview' => 'Forskoðun mynda',\n    'image_upload_success' => 'Upphal myndar tókst',\n    'image_update_success' => 'Upplýsingar um mynd uppfærðar',\n    'image_delete_success' => 'Tókst að eyða mynd',\n    'image_replace' => 'Skipta um mynd',\n    'image_replace_success' => 'Tókst að skipta um skrá',\n    'image_rebuild_thumbs' => 'Endurgera stærðastillingar',\n    'image_rebuild_thumbs_success' => 'Tókst að endurgera stærðarstillingar!',\n\n    // Code Editor\n    'code_editor' => 'Breyta kóða',\n    'code_language' => 'Tungumál kóða',\n    'code_content' => 'Innihald kóða',\n    'code_session_history' => 'Saga lotu',\n    'code_save' => 'Vista kóða',\n];\n"
  },
  {
    "path": "lang/is/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Almennt',\n    'advanced' => 'Ítarlegt',\n    'none' => 'Engin',\n    'cancel' => 'Hætta við',\n    'save' => 'Vista',\n    'close' => 'Loka',\n    'apply' => 'Apply',\n    'undo' => 'Afturkalla',\n    'redo' => 'Endurgera',\n    'left' => 'Vinstri',\n    'center' => 'Miðja',\n    'right' => 'Hægri',\n    'top' => 'Efst',\n    'middle' => 'Miðja',\n    'bottom' => 'Neðst',\n    'width' => 'Breidd',\n    'height' => 'Hæð',\n    'More' => 'Meira',\n    'select' => 'Velja...',\n\n    // Toolbar\n    'formats' => 'Snið',\n    'header_large' => 'Stór fyrirsögn',\n    'header_medium' => 'Miðlungs fyrirsögn',\n    'header_small' => 'Lítil fyrirsögn',\n    'header_tiny' => 'Örsmá fyrirsögn',\n    'paragraph' => 'Málsgrein',\n    'blockquote' => 'Gæsalappir',\n    'inline_code' => 'Inline code',\n    'callouts' => 'Vitna í',\n    'callout_information' => 'Upplýsingar',\n    'callout_success' => 'Árangur',\n    'callout_warning' => 'Aðvörun',\n    'callout_danger' => 'Hætta',\n    'bold' => 'Feitletrað',\n    'italic' => 'Skáletrað',\n    'underline' => 'Undirstrikað',\n    'strikethrough' => 'Yfirstrikað',\n    'superscript' => 'Háletur',\n    'subscript' => 'Lágletur',\n    'text_color' => 'Litur texta',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Sérsniðinn litur',\n    'remove_color' => 'Fjarlægja lit',\n    'background_color' => 'Bakgrunnslitur',\n    'align_left' => 'Jafna til vinstri',\n    'align_center' => 'Miðju jafna',\n    'align_right' => 'Hægrijafna',\n    'align_justify' => 'Jafna',\n    'list_bullet' => 'Punkta listi',\n    'list_numbered' => 'Númeraður listi',\n    'list_task' => 'Aðgerðar listi',\n    'indent_increase' => 'Auka inndrátt',\n    'indent_decrease' => 'Minnka inndrátt',\n    'table' => 'Tafla',\n    'insert_image' => 'Setja inn mynd',\n    'insert_image_title' => 'Setja inn eða breyta mynd',\n    'insert_link' => 'Setja inn eða breyta hlekk',\n    'insert_link_title' => 'Setja inn eða breyta hlekk',\n    'insert_horizontal_line' => 'Setja inn lárétta línu',\n    'insert_code_block' => 'Setja inn kóðastubb',\n    'edit_code_block' => 'Breyta kóðastubb',\n    'insert_drawing' => 'Setja inn eða breyta teikningu',\n    'drawing_manager' => 'Teikningastjóri',\n    'insert_media' => 'Setja inn eða breyta miðlum',\n    'insert_media_title' => 'Setja inn eða breyta miðlum',\n    'clear_formatting' => 'Hreinsa forsnið',\n    'source_code' => 'Frumkóði',\n    'source_code_title' => 'Frumkóði',\n    'fullscreen' => 'Fullann skjá',\n    'image_options' => 'Myndastillingar',\n\n    // Tables\n    'table_properties' => 'Eiginleikar töflu',\n    'table_properties_title' => 'Eiginleikar töflu',\n    'delete_table' => 'Eyða töflu',\n    'table_clear_formatting' => 'Hreinsa forsnið töflu',\n    'resize_to_contents' => 'Endurstilla stærð innihalds',\n    'row_header' => 'Titill raðar',\n    'insert_row_before' => 'Líma röð á undan',\n    'insert_row_after' => 'Líma röð á eftir',\n    'delete_row' => 'Eyða röð',\n    'insert_column_before' => 'Líma dálk á undan',\n    'insert_column_after' => 'Líma dálk á eftir',\n    'delete_column' => 'Eyða dálki',\n    'table_cell' => 'Reitur',\n    'table_row' => 'Röð',\n    'table_column' => 'Dálkur',\n    'cell_properties' => 'Eigindi reitar',\n    'cell_properties_title' => 'Eigindi reitar',\n    'cell_type' => 'Gerð reitar',\n    'cell_type_cell' => 'Reitur',\n    'cell_scope' => 'Svið',\n    'cell_type_header' => 'For reitur',\n    'merge_cells' => 'Sameina reiti',\n    'split_cell' => 'Kljúfa reiti',\n    'table_row_group' => 'Hópur raðar',\n    'table_column_group' => 'Hópur dálks',\n    'horizontal_align' => 'Jafna lárétt',\n    'vertical_align' => 'Jafna lóðrétt',\n    'border_width' => 'Border width',\n    'border_style' => 'Útlit jaðars',\n    'border_color' => 'Litur jaðars',\n    'row_properties' => 'Eigindi raðar',\n    'row_properties_title' => 'Eigindi raðar',\n    'cut_row' => 'Klippa röð',\n    'copy_row' => 'Afrita röð',\n    'paste_row_before' => 'Líma röð á undan',\n    'paste_row_after' => 'Líma röð á eftir',\n    'row_type' => 'Gerð raðar',\n    'row_type_header' => 'Síðuhaus',\n    'row_type_body' => 'Meginmál',\n    'row_type_footer' => 'Neðanmál',\n    'alignment' => 'Jöfnun',\n    'cut_column' => 'Klippa dálk',\n    'copy_column' => 'Afrita dálk',\n    'paste_column_before' => 'Líma dálk á undan',\n    'paste_column_after' => 'Líma dálk á eftir',\n    'cell_padding' => 'Cell padding',\n    'cell_spacing' => 'Cell spacing',\n    'caption' => 'Fyrirsögn',\n    'show_caption' => 'Sýna fyrirsögn',\n    'constrain' => 'Constrain proportions',\n    'cell_border_solid' => 'Fyllt',\n    'cell_border_dotted' => 'Punkta',\n    'cell_border_dashed' => 'Strikað',\n    'cell_border_double' => 'Tvöfalt',\n    'cell_border_groove' => 'Groove',\n    'cell_border_ridge' => 'Ridge',\n    'cell_border_inset' => 'Inset',\n    'cell_border_outset' => 'Outset',\n    'cell_border_none' => 'Engin',\n    'cell_border_hidden' => 'Falin',\n\n    // Images, links, details/summary & embed\n    'source' => 'Uppruni',\n    'alt_desc' => 'Auka lýsing',\n    'embed' => 'Innfellt',\n    'paste_embed' => 'Límdu innfellda kóðann þinn að neðan:',\n    'url' => 'Vistfang',\n    'text_to_display' => 'Teksti til að sýna',\n    'title' => 'Titill',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Opna hlekk',\n    'open_link_in' => 'Opna hlekk í...',\n    'open_link_current' => 'Virkum glugga',\n    'open_link_new' => 'Nýjum glugga',\n    'remove_link' => 'Fjarlægja hlekk',\n    'insert_collapsible' => 'Insert collapsible block',\n    'collapsible_unwrap' => 'Unwrap',\n    'edit_label' => 'Breyta miða',\n    'toggle_open_closed' => 'Velja opið/lokað',\n    'collapsible_edit' => 'Edit collapsible block',\n    'toggle_label' => 'Sýna miða',\n\n    // About view\n    'about' => 'Um ritilinn',\n    'about_title' => 'Um WYSIWYG ritilinn',\n    'editor_license' => 'Leyfi og höfundaréttur ritilsins',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'Þessi ritill er smíðaður með :tinyLink sem er undir MIT leyfi.',\n    'editor_tiny_license_link' => 'Höfundarétt og leyfi TinyMCE má finna hér.',\n    'save_continue' => 'Vista síðu og halda áfram',\n    'callouts_cycle' => '(Keep pressing to toggle through types)',\n    'link_selector' => 'Hlekkur á innihald',\n    'shortcuts' => 'Flýtileiðir',\n    'shortcut' => 'Flýtileið',\n    'shortcuts_intro' => 'Eftirtaldar flýtileiðir eru aðgengilegar í ritlinum:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Lýsing',\n];\n"
  },
  {
    "path": "lang/is/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Nýlega búið til',\n    'recently_created_pages' => 'Nýlega stofnaðar síður',\n    'recently_updated_pages' => 'Nýlega uppfærðar síður',\n    'recently_created_chapters' => 'Nýlega stofnaðir kaflar',\n    'recently_created_books' => 'Nýlega stofnaðar bækur',\n    'recently_created_shelves' => 'Nýlega stofnaðar hillur',\n    'recently_update' => 'Nýlega uppfært',\n    'recently_viewed' => 'Nýlega skoðað',\n    'recent_activity' => 'Nýleg virkni',\n    'create_now' => 'Búðu til eina núna',\n    'revisions' => 'Útgáfur',\n    'meta_revision' => 'Úgáfa #:revisionCount',\n    'meta_created' => 'Búið til :timeLength',\n    'meta_created_name' => 'Búið til :timeLength af :user',\n    'meta_updated' => 'Uppfært :timeLength',\n    'meta_updated_name' => 'Uppfært :timeLength af :user',\n    'meta_owned_name' => 'Eigandi :user',\n    'meta_reference_count' => 'Vitnað í af :count item|Vitnað í af :count items',\n    'entity_select' => 'Entity Val',\n    'entity_select_lack_permission' => 'Þú hefur ekki nauðsynlegar aðgangsheimildir til að velja þetta',\n    'images' => 'Myndir',\n    'my_recent_drafts' => 'Nýlegur drögin mín',\n    'my_recently_viewed' => 'Síðast skoða af mér',\n    'my_most_viewed_favourites' => 'Mest skoðuðu eftirlætin',\n    'my_favourites' => 'Eftirlætin mín',\n    'no_pages_viewed' => 'Þú hefur ekki skoðað neinar síður',\n    'no_pages_recently_created' => 'Engar síður hafa verið búnar til nýlega',\n    'no_pages_recently_updated' => 'Engar síður hafa verið uppfærðar nýlega',\n    'export' => 'Flytja út',\n    'export_html' => 'Innifalin vefskrá',\n    'export_pdf' => 'PDF skrá',\n    'export_text' => 'Venjuleg textaskrá',\n    'export_md' => 'Markdown skrá',\n    'export_zip' => 'ZIP skrá',\n    'default_template' => 'Sjálfgefið síðusnið',\n    'default_template_explain' => 'Veldu síðusnið sem verður sjálgefið snið fyrir allar stofnaðar síður innan þessa hluta. Hafðu í huga að þetta verður aðeins notað ef sá sem stofnar síður er með heimild á þetta snið.',\n    'default_template_select' => 'Veldu sniðsíðu',\n    'import' => 'Flytja inn',\n    'import_validate' => 'Staðfesta innflutning',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'Veldu ZIP skrá til að hlaða upp',\n    'import_zip_validation_errors' => 'Greindar voru villur við að staðreyna uppgefina ZIP skrá:',\n    'import_pending' => 'Innflutningur í bið',\n    'import_pending_none' => 'Ekkert hefur verið flutt inn.',\n    'import_continue' => 'Halda áfram að flytja inn',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Nánari lýsing á innflutningi',\n    'import_run' => 'Keyra innflutning',\n    'import_size' => ':size Stærð ZIP skrár',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Upphlaðið af',\n    'import_location' => 'Staðsetning innflutnings',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Ertu viss um að þú viljir eyða þessum innflutningi?',\n    'import_delete_desc' => 'Þetta mun eyða innsendri ZIP skrá, þessa aðgerð er ekki hægt að afturkalla.',\n    'import_errors' => 'Villur í innflutningi',\n    'import_errors_desc' => 'Eftirfarandi villur komu upp við innflutning:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Réttindi',\n    'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.',\n    'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.',\n    'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.',\n    'permissions_save' => 'Vista réttindi',\n    'permissions_owner' => 'Eigandi',\n    'permissions_role_everyone_else' => 'Allir aðrir',\n    'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',\n    'permissions_role_override' => 'Yfirskrifa réttindi fyrir hlutverk',\n    'permissions_inherit_defaults' => 'Erfa sjálfgefið',\n\n    // Search\n    'search_results' => 'Leitarniðurstöður',\n    'search_total_results_found' => ':count result found|:count total results found',\n    'search_clear' => 'Hreinsa leit',\n    'search_no_pages' => 'Engar síður passa við þessa leit',\n    'search_for_term' => 'Leita að :term',\n    'search_more' => 'Fleiri niðurstöður',\n    'search_advanced' => 'Ítarleg leit',\n    'search_terms' => 'Leitarorð',\n    'search_content_type' => 'Efnistegund',\n    'search_exact_matches' => 'Nákvæm samsvörun',\n    'search_tags' => 'Leita í örmerkjum',\n    'search_options' => 'Valkostir',\n    'search_viewed_by_me' => 'Skoðað af mér',\n    'search_not_viewed_by_me' => 'Ekki skoðað af mér',\n    'search_permissions_set' => 'Réttindi stillt',\n    'search_created_by_me' => 'Búið til af mér',\n    'search_updated_by_me' => 'Uppfært af mér',\n    'search_owned_by_me' => 'Í minni eigu',\n    'search_date_options' => 'Dagsetningarval',\n    'search_updated_before' => 'Uppfært fyrir',\n    'search_updated_after' => 'Uppfært eftir',\n    'search_created_before' => 'Búið til fyrir',\n    'search_created_after' => 'Búið til eftir',\n    'search_set_date' => 'Dagsetning',\n    'search_update' => 'Uppfæra leit',\n\n    // Shelves\n    'shelf' => 'Hilla',\n    'shelves' => 'Hillur',\n    'x_shelves' => ':count Hilla|:count Hillur',\n    'shelves_empty' => 'Engar hillur hafa verið búnar til',\n    'shelves_create' => 'Búa til hillu',\n    'shelves_popular' => 'Vinsælar hillur',\n    'shelves_new' => 'Nýjar hillur',\n    'shelves_new_action' => 'Ný hilla',\n    'shelves_popular_empty' => 'Vinsælustu hillurnar munu birtast hér.',\n    'shelves_new_empty' => 'Nýjustu hillurnar munu birtast hér.',\n    'shelves_save' => 'Vista hillu',\n    'shelves_books' => 'Bækur í þessari hillu',\n    'shelves_add_books' => 'Bæta við bókum í þessa hillu',\n    'shelves_drag_books' => 'Dragðu bækur hér undir til að bæta þeim í þessa hillu',\n    'shelves_empty_contents' => 'Þessi hilla hefur engar bækur',\n    'shelves_edit_and_assign' => 'Breyttu hillu til að setja inn bækur',\n    'shelves_edit_named' => 'Breyta hillu :name',\n    'shelves_edit' => 'Breyta hillu',\n    'shelves_delete' => 'Eyða hillu',\n    'shelves_delete_named' => 'Eyða hillu :name',\n    'shelves_delete_explain' => \"Þetta mun eyða hillunni ':name'. Bókum í þessari hillu verður ekki eytt.\",\n    'shelves_delete_confirmation' => 'Ertu viss um að þú viljir eyða hillunni?',\n    'shelves_permissions' => 'Stillingar á réttindum á hillu',\n    'shelves_permissions_updated' => 'Réttindi á hillu uppfærð',\n    'shelves_permissions_active' => 'Réttindi á hillu virk',\n    'shelves_permissions_cascade_warning' => 'Réttindi á hillum yfirfærast ekki á bækurnar sem í hillunni eru. Þetta er vegna þess að ein bók getur verið í mörgum hillum. Réttindi geta hinsvegar verið afrituð niður á bækur með því að nota valmöguleikann hér fyrir neðan.',\n    'shelves_permissions_create' => 'Réttindi til að búa til hillu eru aðeins notuð til að afrita réttindi á undirliggjandi bækur með því að nota aðgerðina hér fyrir neðan. Þau stjórna ekki hvort hægt sé að búa til bækur.',\n    'shelves_copy_permissions_to_books' => 'Afrita réttindi á bækur',\n    'shelves_copy_permissions' => 'Afrita réttindi',\n    'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',\n    'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',\n\n    // Books\n    'book' => 'Bók',\n    'books' => 'Bækur',\n    'x_books' => ':count Bók:count Bækur',\n    'books_empty' => 'Engar bækur hafa verið búnar til',\n    'books_popular' => 'Vinsælar bækur',\n    'books_recent' => 'Nýlegar bækur',\n    'books_new' => 'Nýjar bækur',\n    'books_new_action' => 'Ný bók',\n    'books_popular_empty' => 'Vinsælustu bækurnar munu birtast hér.',\n    'books_new_empty' => 'The most recently created books will appear here.',\n    'books_create' => 'Búa til nýja bók',\n    'books_delete' => 'Eyða bók',\n    'books_delete_named' => 'Eyða bók :bookName',\n    'books_delete_explain' => 'Þetta mun eyða bók með nafninu \\':bookName\\'. Allar síður og allir kaflar verða fjarlægðir og eytt.',\n    'books_delete_confirmation' => 'Ertu viss um að þú viljir eyða þessari bók?',\n    'books_edit' => 'Breyta bók',\n    'books_edit_named' => 'Breyta bók :bookName',\n    'books_form_book_name' => 'Nafn bókar',\n    'books_save' => 'Vista bók',\n    'books_permissions' => 'Réttindastillingar bókar',\n    'books_permissions_updated' => 'Réttindastillingar bókar uppfærðar',\n    'books_empty_contents' => 'Engar síður eða kaflar hafa verið búin til fyrir þessa bók.',\n    'books_empty_create_page' => 'Búa til nýja síðu',\n    'books_empty_sort_current_book' => 'Raða núverandi bók',\n    'books_empty_add_chapter' => 'Bæta við kafla',\n    'books_permissions_active' => 'Réttindi bókar virk',\n    'books_search_this' => 'Leita í þessari bók',\n    'books_navigation' => 'Leiðartré bókar',\n    'books_sort' => 'Raða innihaldi bókar',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Raða bók :bookName',\n    'books_sort_name' => 'Raða eftir nafni',\n    'books_sort_created' => 'Raða eftir skráningar dagsetningu',\n    'books_sort_updated' => 'Raða eftir upphleðslu dagsetningu',\n    'books_sort_chapters_first' => 'Kaflar fyrst',\n    'books_sort_chapters_last' => 'Kaflar síðast',\n    'books_sort_show_other' => 'Sýna aðrar bækur',\n    'books_sort_save' => 'Vista nýja röð',\n    'books_sort_show_other_desc' => 'Bæta við öðrum bókum til að bæta þeim við röðunina.',\n    'books_sort_move_up' => 'Færa upp',\n    'books_sort_move_down' => 'Færa niður',\n    'books_sort_move_prev_book' => 'Færa í fyrri bók',\n    'books_sort_move_next_book' => 'Færa í næstu bók',\n    'books_sort_move_prev_chapter' => 'Færa inn í fyrri kafla',\n    'books_sort_move_next_chapter' => 'Færa inn í næsta kafla',\n    'books_sort_move_book_start' => 'Færa til byrjunar bókar',\n    'books_sort_move_book_end' => 'Færa í enda bókar',\n    'books_sort_move_before_chapter' => 'Færa í byrjun kafla',\n    'books_sort_move_after_chapter' => 'Færa í lok kafla',\n    'books_copy' => 'Afrita bók',\n    'books_copy_success' => 'Tókst að afrita bók',\n\n    // Chapters\n    'chapter' => 'Kafli',\n    'chapters' => 'Kaflar',\n    'x_chapters' => ':count Kafli|:count Kaflar',\n    'chapters_popular' => 'Vinsælir kaflar',\n    'chapters_new' => 'Nýr kafli',\n    'chapters_create' => 'Búa til nýjan kafla',\n    'chapters_delete' => 'Eyða kafla',\n    'chapters_delete_named' => 'Eyða kafla :chapterName',\n    'chapters_delete_explain' => 'Þetta mun eyða kafla með nafninu \\':chapterName\\'. Öllum blaðsíðum í þessum kafla verður einnig eytt.',\n    'chapters_delete_confirm' => 'Ertu viss um að þú viljir eyða þessum kafla?',\n    'chapters_edit' => 'Breyta kafla',\n    'chapters_edit_named' => 'Breyta kafla chapterName',\n    'chapters_save' => 'Vista kafla',\n    'chapters_move' => 'Færa kafla',\n    'chapters_move_named' => 'Færa kalfa :chapterName',\n    'chapters_copy' => 'Afrita kafla',\n    'chapters_copy_success' => 'Tókst að afrita kafla',\n    'chapters_permissions' => 'Réttindi á kafla',\n    'chapters_empty' => 'Engar síður eru eins og er í þessum kafla.',\n    'chapters_permissions_active' => 'Réttindi á kafla eru virk',\n    'chapters_permissions_success' => 'Réttindi á kafla hafa verið uppfærð',\n    'chapters_search_this' => 'Leita í þessum kafla',\n    'chapter_sort_book' => 'Raða bók',\n\n    // Pages\n    'page' => 'Síða',\n    'pages' => 'Síður',\n    'x_pages' => ':count Síða|:count Síður',\n    'pages_popular' => 'Vinsælar síður',\n    'pages_new' => 'Ný síða',\n    'pages_attachments' => 'Viðhengi',\n    'pages_navigation' => 'Síðuráp',\n    'pages_delete' => 'Eyða síðu',\n    'pages_delete_named' => 'Eyða síðu :pageName',\n    'pages_delete_draft_named' => 'Eyða drögum :pageName',\n    'pages_delete_draft' => 'Eyða uppkasti',\n    'pages_delete_success' => 'Síðu eytt',\n    'pages_delete_draft_success' => 'Uppkasti að síðu eytt',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'Ertu viss um að þú viljir eyða þessari síðu?',\n    'pages_delete_draft_confirm' => 'Ertu viss um að þú viljir eyða þessu uppkasti að síðu?',\n    'pages_editing_named' => 'Breyta síðu :pageName',\n    'pages_edit_draft_options' => 'Valkostir uppkasts',\n    'pages_edit_save_draft' => 'Vista uppkast',\n    'pages_edit_draft' => 'Breyta drögum',\n    'pages_editing_draft' => 'Breyta uppkasti',\n    'pages_editing_page' => 'Breyta síðu',\n    'pages_edit_draft_save_at' => 'Vista uppkast ',\n    'pages_edit_delete_draft' => 'Eyða uppkasti',\n    'pages_edit_delete_draft_confirm' => 'Ertu viss um að þú viljir eyða uppkasti síðu? Allar breytingar sem gerðar hafa verið frá síðustu vistun á síðunni munu tapast.',\n    'pages_edit_discard_draft' => 'Henda uppkasti',\n    'pages_edit_switch_to_markdown' => 'Færa þig yfir í Markdown ritil',\n    'pages_edit_switch_to_markdown_clean' => '(Hreinsa innihald)',\n    'pages_edit_switch_to_markdown_stable' => '(Stable Content)',\n    'pages_edit_switch_to_wysiwyg' => 'Skipta yfir í WYSIWYG ritil',\n    'pages_edit_switch_to_new_wysiwyg' => 'Skipta yfir í nýja WYSIWYG ritilinn',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Stilla breytingarskrá',\n    'pages_edit_enter_changelog_desc' => 'Skrifaðu stutta lýsingu á breytingunum sem þú gerðir',\n    'pages_edit_enter_changelog' => 'Færa í breytingaskrá',\n    'pages_editor_switch_title' => 'Skipta um ritil',\n    'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',\n    'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',\n    'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',\n    'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',\n    'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\\'t persist across this change.',\n    'pages_save' => 'Vista síðu',\n    'pages_title' => 'Titill síðu',\n    'pages_name' => 'Nafn síðu',\n    'pages_md_editor' => 'Ritill',\n    'pages_md_preview' => 'Forskoðun',\n    'pages_md_insert_image' => 'Setja inn mynd',\n    'pages_md_insert_link' => 'Insert Entity Link',\n    'pages_md_insert_drawing' => 'Setja inn teikningu',\n    'pages_md_show_preview' => 'Sýna forskoðun',\n    'pages_md_sync_scroll' => 'Sync preview scroll',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Unsaved Drawing Found',\n    'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',\n    'pages_not_in_chapter' => 'Síðan tilheyrir engum kafla',\n    'pages_move' => 'Færa síðu',\n    'pages_copy' => 'Afrita síðu',\n    'pages_copy_desination' => 'Áfangastaður afritunar',\n    'pages_copy_success' => 'Tókst að afrita síðu',\n    'pages_permissions' => 'Réttindi síðu',\n    'pages_permissions_success' => 'Réttindi síðu uppfærð',\n    'pages_revision' => 'Útgáfa',\n    'pages_revisions' => 'Útgáfur síðu',\n    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',\n    'pages_revisions_named' => 'Page Revisions for :pageName',\n    'pages_revision_named' => 'Page Revision for :pageName',\n    'pages_revision_restored_from' => 'Restored from #:id; :summary',\n    'pages_revisions_created_by' => 'Búið til af',\n    'pages_revisions_date' => 'Útgáfu dagsetning',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Útgáfunúmer',\n    'pages_revisions_numbered' => 'Útgáfu #:id',\n    'pages_revisions_numbered_changes' => 'Útgáfu #:id breytingar',\n    'pages_revisions_editor' => 'Gerð ritils',\n    'pages_revisions_changelog' => 'Breytingaskrá',\n    'pages_revisions_changes' => 'Breytingar',\n    'pages_revisions_current' => 'Núverandi útgáfa',\n    'pages_revisions_preview' => 'Forskoðun',\n    'pages_revisions_restore' => 'Endurheimta',\n    'pages_revisions_none' => 'Þessi síða hefur engar útgáfur',\n    'pages_copy_link' => 'Afrita hlekk',\n    'pages_edit_content_link' => 'Hoppa í staðsetningu í ritli',\n    'pages_pointer_enter_mode' => 'Enter section select mode',\n    'pages_pointer_label' => 'Page Section Options',\n    'pages_pointer_permalink' => 'Page Section Permalink',\n    'pages_pointer_include_tag' => 'Page Section Include Tag',\n    'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag',\n    'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink',\n    'pages_permissions_active' => 'Page Permissions Active',\n    'pages_initial_revision' => 'Fyrsta birting',\n    'pages_references_update_revision' => 'System auto-update of internal links',\n    'pages_initial_name' => 'Ný síða',\n    'pages_editing_draft_notification' => 'You are currently editing a draft that was last saved :timeDiff.',\n    'pages_draft_edited_notification' => 'This page has been updated by since that time. It is recommended that you discard this draft.',\n    'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count users have started editing this page',\n        'start_b' => ':userName has started editing this page',\n        'time_a' => 'since the page was last updated',\n        'time_b' => 'in the last :minCount minutes',\n        'message' => ':start :time. Take care not to overwrite each other\\'s updates!',\n    ],\n    'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',\n    'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',\n    'pages_specific' => 'Tilgreind síða',\n    'pages_is_template' => 'Forsnið síðu',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Toggle Sidebar',\n    'page_tags' => 'Page Tags',\n    'chapter_tags' => 'Chapter Tags',\n    'book_tags' => 'Book Tags',\n    'shelf_tags' => 'Shelf Tags',\n    'tag' => 'Tag',\n    'tags' =>  'Tags',\n    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',\n    'tag_name' =>  'Tag Name',\n    'tag_value' => 'Tag Value (Optional)',\n    'tags_explain' => \"Add some tags to better categorise your content. \\n You can assign a value to a tag for more in-depth organisation.\",\n    'tags_add' => 'Add another tag',\n    'tags_remove' => 'Remove this tag',\n    'tags_usages' => 'Total tag usages',\n    'tags_assigned_pages' => 'Assigned to Pages',\n    'tags_assigned_chapters' => 'Assigned to Chapters',\n    'tags_assigned_books' => 'Assigned to Books',\n    'tags_assigned_shelves' => 'Assigned to Shelves',\n    'tags_x_unique_values' => ':count unique values',\n    'tags_all_values' => 'All values',\n    'tags_view_tags' => 'View Tags',\n    'tags_view_existing_tags' => 'View existing tags',\n    'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',\n    'attachments' => 'Attachments',\n    'attachments_explain' => 'Upload some files or attach some links to display on your page. These are visible in the page sidebar.',\n    'attachments_explain_instant_save' => 'Changes here are saved instantly.',\n    'attachments_upload' => 'Upload File',\n    'attachments_link' => 'Attach Link',\n    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',\n    'attachments_set_link' => 'Set Link',\n    'attachments_delete' => 'Are you sure you want to delete this attachment?',\n    'attachments_dropzone' => 'Drop files here to upload',\n    'attachments_no_files' => 'No files have been uploaded',\n    'attachments_explain_link' => 'You can attach a link if you\\'d prefer not to upload a file. This can be a link to another page or a link to a file in the cloud.',\n    'attachments_link_name' => 'Link Name',\n    'attachment_link' => 'Attachment link',\n    'attachments_link_url' => 'Link to file',\n    'attachments_link_url_hint' => 'Url of site or file',\n    'attach' => 'Attach',\n    'attachments_insert_link' => 'Add Attachment Link to Page',\n    'attachments_edit_file' => 'Edit File',\n    'attachments_edit_file_name' => 'File Name',\n    'attachments_edit_drop_upload' => 'Drop files or click here to upload and overwrite',\n    'attachments_order_updated' => 'Attachment order updated',\n    'attachments_updated_success' => 'Attachment details updated',\n    'attachments_deleted' => 'Attachment deleted',\n    'attachments_file_uploaded' => 'File successfully uploaded',\n    'attachments_file_updated' => 'File successfully updated',\n    'attachments_link_attached' => 'Link successfully attached to page',\n    'templates' => 'Templates',\n    'templates_set_as_template' => 'Page is a template',\n    'templates_explain_set_as_template' => 'You can set this page as a template so its contents be utilized when creating other pages. Other users will be able to use this template if they have view permissions for this page.',\n    'templates_replace_content' => 'Replace page content',\n    'templates_append_content' => 'Append to page content',\n    'templates_prepend_content' => 'Prepend to page content',\n\n    // Profile View\n    'profile_user_for_x' => 'User for :time',\n    'profile_created_content' => 'Created Content',\n    'profile_not_created_pages' => ':userName has not created any pages',\n    'profile_not_created_chapters' => ':userName has not created any chapters',\n    'profile_not_created_books' => ':userName has not created any books',\n    'profile_not_created_shelves' => ':userName has not created any shelves',\n\n    // Comments\n    'comment' => 'Comment',\n    'comments' => 'Comments',\n    'comment_add' => 'Add Comment',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Leave a comment here',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Save Comment',\n    'comment_new' => 'New Comment',\n    'comment_created' => 'commented :createDiff',\n    'comment_updated' => 'Updated :updateDiff by :username',\n    'comment_updated_indicator' => 'Updated',\n    'comment_deleted_success' => 'Comment deleted',\n    'comment_created_success' => 'Comment added',\n    'comment_updated_success' => 'Comment updated',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Are you sure you want to delete this comment?',\n    'comment_in_reply_to' => 'In reply to :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',\n\n    // Revision\n    'revision_delete_confirm' => 'Are you sure you want to delete this revision?',\n    'revision_restore_confirm' => 'Are you sure you want to restore this revision? The current page contents will be replaced.',\n    'revision_cannot_delete_latest' => 'Cannot delete the latest revision.',\n\n    // Copy view\n    'copy_consider' => 'Please consider the below when copying content.',\n    'copy_consider_permissions' => 'Custom permission settings will not be copied.',\n    'copy_consider_owner' => 'You will become the owner of all copied content.',\n    'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',\n    'copy_consider_attachments' => 'Page attachments will not be copied.',\n    'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',\n\n    // Conversions\n    'convert_to_shelf' => 'Convert to Shelf',\n    'convert_to_shelf_contents_desc' => 'You can convert this book to a new shelf with the same contents. Chapters contained within this book will be converted to new books. If this book contains any pages, that are not in a chapter, this book will be renamed and contain such pages, and this book will become part of the new shelf.',\n    'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.',\n    'convert_book' => 'Convert Book',\n    'convert_book_confirm' => 'Are you sure you want to convert this book?',\n    'convert_undo_warning' => 'This cannot be as easily undone.',\n    'convert_to_book' => 'Convert to Book',\n    'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',\n    'convert_chapter' => 'Convert Chapter',\n    'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',\n\n    // References\n    'references' => 'References',\n    'references_none' => 'There are no tracked references to this item.',\n    'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',\n\n    // Watch Options\n    'watch' => 'Watch',\n    'watch_title_default' => 'Default Preferences',\n    'watch_desc_default' => 'Revert watching to just your default notification preferences.',\n    'watch_title_ignore' => 'Ignore',\n    'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.',\n    'watch_title_new' => 'New Pages',\n    'watch_desc_new' => 'Notify when any new page is created within this item.',\n    'watch_title_updates' => 'All Page Updates',\n    'watch_desc_updates' => 'Notify upon all new pages and page changes.',\n    'watch_desc_updates_page' => 'Notify upon all page changes.',\n    'watch_title_comments' => 'All Page Updates & Comments',\n    'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.',\n    'watch_desc_comments_page' => 'Notify upon page changes and new comments.',\n    'watch_change_default' => 'Change default notification preferences',\n    'watch_detail_ignore' => 'Ignoring notifications',\n    'watch_detail_new' => 'Watching for new pages',\n    'watch_detail_updates' => 'Watching new pages and updates',\n    'watch_detail_comments' => 'Watching new pages, updates & comments',\n    'watch_detail_parent_book' => 'Watching via parent book',\n    'watch_detail_parent_book_ignore' => 'Ignoring via parent book',\n    'watch_detail_parent_chapter' => 'Watching via parent chapter',\n    'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter',\n];\n"
  },
  {
    "path": "lang/is/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Þú hefur ekki heimild til að skoða þessa síðu.',\n    'permissionJson' => 'Þú hefur ekki heimild til að framkvæma þessa aðgerð.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Notandi með netfangið :email er nú þegar til.',\n    'auth_pre_register_theme_prevention' => 'Ekki var hægt að búa til aðgang með þessum upplýsingum',\n    'email_already_confirmed' => 'Netfang hefur þegar verið staðfest. Prófaðu að skrá þig inn.',\n    'email_confirmation_invalid' => 'Þessi staðfestingar tóki er ekki gildur eða hefur þegar verið notaður. Reyndu að skrá þig aftur.',\n    'email_confirmation_expired' => 'Staðfestingar tóki hefur runnið út. Nýr staðfestinga tölvupóstur hefur verið sendur.',\n    'email_confirmation_awaiting' => 'Eftir á að staðfest þetta netfang',\n    'ldap_fail_anonymous' => 'LDAP auðkenning virkaði ekki',\n    'ldap_fail_authed' => 'LDAP auðkenning virkaði ekki með að nota uppgefið dn & password',\n    'ldap_extension_not_installed' => 'LDAP PHP viðbót ekki uppsett',\n    'ldap_cannot_connect' => 'Næ ekki að tengjast Ldap þjóni. Fyrsta tenging mistókst',\n    'saml_already_logged_in' => 'Þegar innskráður',\n    'saml_no_email_address' => 'Fann ekki netfang fyrir þennan notanda í auðkenningar þjónustu',\n    'saml_invalid_response_id' => 'Beiðnin frá ytri auðkenningaraðila er óþekkt af kerfinu. Að fara tilbaka eftir innskráningu gæti valdið þessu vandamáli.',\n    'saml_fail_authed' => 'Innskráning sem notaði :system tókst ekki. Kerfið gaf ekki út gilda auðkenningu',\n    'oidc_already_logged_in' => 'Þegar skráður inn',\n    'oidc_no_email_address' => 'Fann ekki netfang fyrir þennan notanda í ytri auðkenningar þjónustu',\n    'oidc_fail_authed' => 'Innskráning sem notaði :system tókst ekki. Kerfið gaf ekki út gilda auðkenningu',\n    'social_no_action_defined' => 'Engin aðgerð skilgreind',\n    'social_login_bad_response' => \"Villa kom upp við auðkenninga á :socialAccount login\",\n    'social_account_in_use' => 'Þessi :socialAccount er þegar í notkun. Reyndu að skrá þig inn með :socialAccount.',\n    'social_account_email_in_use' => 'Netfangið :email er þegar í notkun. Ef þú ert nú þegar með aðgang getur þú tengt :socialAccount við hann í prófíl stillingum.',\n    'social_account_existing' => 'Þessi :socialAccount er nú þegar tengdur við prófílinn þinn.',\n    'social_account_already_used_existing' => 'Þessi :socialAccount reikningur er nú þegar í notkun hjá öðrum notanda.',\n    'social_account_not_used' => 'Þessi :socialAccount er ekki tengdur neinum notanda. Þú getur tengt hann við þig í prófíl stillingar. ',\n    'social_account_register_instructions' => 'Ef þú ert ekki nú þegar með aðgang, getur þú skrá þig með :socialAccount',\n    'social_driver_not_found' => 'Samfélagsviðbót fannst ekki',\n    'social_driver_not_configured' => 'Þínar :socialAccount er ekki rétt stilltar.',\n    'invite_token_expired' => 'Þess boðshlekkur er útrunninn, Prófa að endurstilla lykilorðið þitt.',\n    'login_user_not_found' => 'Enginn notandi fannst fyrir þessa aðgerð.',\n\n    // System\n    'path_not_writable' => 'Ekki var hægt að hlaða upp á slóðinni :filePath. Vertu viss um að slóðin sé skrifanleg.',\n    'cannot_get_image_from_url' => 'Get ekki sótt mynd frá :url',\n    'cannot_create_thumbs' => 'Netþjónninn getur ekki búið til smámyndir. Vertu viss um að þú hafir GD PHP viðbótina uppsetta.',\n    'server_upload_limit' => 'Þessi netþjónn leyfir ekki uphal af þessari stærð. Prófaðu minni skrá.',\n    'server_post_limit' => 'Netþjóninn getur ekki tekið á móti þessu magni gagna. Reyndu aftur með færri eða smærri gögnum.',\n    'uploaded'  => 'Þessi netþjónn leyfir ekki uphal af þessari stærð. Prófaðu minni skrá.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Villa kom upp við að hlaða upp mynd',\n    'image_upload_type_error' => 'Gerð myndar er ógild',\n    'image_upload_replace_type' => 'Myndin sem á að nota við útskipti þarf að vera sömu gerðar',\n    'image_upload_memory_limit' => 'Ekki var hægt að taka við upphali og eða búa til smámyndir þar sem ekki eru auðlindir til staðar.',\n    'image_thumbnail_memory_limit' => 'Ekki var hægt að búa til nokkrar stærðir myndarinnar vegna skorts á auðlindum.',\n    'image_gallery_thumbnail_memory_limit' => 'Ekki var hægt að búa til smámyndayfirlit vegna skorts á auðlindum.',\n    'drawing_data_not_found' => 'Ekki tóks að hlaða inn teikningagögnum. Það gæti vantað skránna eða að þú hafir ekki réttindi að henni.',\n\n    // Attachments\n    'attachment_not_found' => 'Viðhengi fannst ekki',\n    'attachment_upload_error' => 'Það kom upp villa við að hlaða upp viðhenginu',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Gat ekki vistað uppkast. Gættu að þú hafir tengingu við internetið áður en þú vistar þessa síðu',\n    'page_draft_delete_fail' => 'Ekki var hægt að eyða uppkasti og sækja fyrra innihald síðunar',\n    'page_custom_home_deletion' => 'Ekki er hægt að eyða síðu á meðan hún er valin sem sjálfgefin upphafssíða',\n\n    // Entities\n    'entity_not_found' => 'Entity fannst ekki',\n    'bookshelf_not_found' => 'Hilla fannst ekki',\n    'book_not_found' => 'Bók fannst ekki',\n    'page_not_found' => 'Síða fannst ekki',\n    'chapter_not_found' => 'Kafli fannst ekki',\n    'selected_book_not_found' => 'Valin bók fannst ekki',\n    'selected_book_chapter_not_found' => 'Valin bók eða kafli fannst ekki',\n    'guests_cannot_save_drafts' => 'Gestir geta ekki vistað drög',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Þú getur ekki eytt, bara kerfisstjóri',\n    'users_cannot_delete_guest' => 'Þú getur ekki eytt gesta notanda',\n    'users_could_not_send_invite' => 'Gat ekki stofnað notanda þar sem ekki tókst að senda staðfestingar tölvupóst',\n\n    // Roles\n    'role_cannot_be_edited' => 'Ekki er hægt að breyta þessu hlutverki',\n    'role_system_cannot_be_deleted' => 'Þetta er kerfis hlutverk og því ekki hægt að eyða því',\n    'role_registration_default_cannot_delete' => 'Ekki er hægt að eyða þessu hlutverki þar sem það er sjálfgefið kerfishlutverk við skráningu',\n    'role_cannot_remove_only_admin' => 'Þessi notandi er sá eini sem er með kerfisstjóra hlutverk. Bættu hlutverkinu við annann notanda áður en þú reynir að fjarlægja það héðan.',\n\n    // Comments\n    'comment_list' => 'Villa kom upp við að sækja athugasemdir.',\n    'cannot_add_comment_to_draft' => 'Þú getur ekki sett athugasemdir við drög.',\n    'comment_add' => 'Villa kom upp við að bæta við eða breyta athugasemdinni.',\n    'comment_delete' => 'Villa kom upp við að eyða athugasemdinni.',\n    'empty_comment' => 'Get ekki bætt við tómri athugasemd.',\n\n    // Error pages\n    '404_page_not_found' => 'Síða fannst ekki',\n    'sorry_page_not_found' => 'Síðan sem þú varst að leita að fannst því miður ekki.',\n    'sorry_page_not_found_permission_warning' => 'Ef þú átt von á að þessi síða sé til gæti verið að þú hafir ekki aðgang að henni.',\n    'image_not_found' => 'Fann ekki mynd',\n    'image_not_found_subtitle' => 'Myndin sem þú varst að leita að fannst því miður ekki.',\n    'image_not_found_details' => 'Ef þú heldur að þessi mynda hafi verið til, þá gæti henni hafa verið eytt.',\n    'return_home' => 'Fara á forsíðu',\n    'error_occurred' => 'Það kom upp villa',\n    'app_down' => ':appName er niðri í augnablikinu',\n    'back_soon' => 'Verð komin upp aftur fljótlega.',\n\n    // Import\n    'import_zip_cant_read' => 'Gat ekki lesið ZIP skrá.',\n    'import_zip_cant_decode_data' => 'Fann ekki ZIP data.json innihald.',\n    'import_zip_no_data' => 'ZIP skráin inniheldur ekkert efni.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'ZIP skráin stóðst ekki staðfestingu og skilaði villu:',\n    'import_zip_failed_notification' => 'Gat ekki lesið inn ZIP skrá.',\n    'import_perms_books' => 'Þú hefur ekki heimild til að búa til bækur.',\n    'import_perms_chapters' => 'Þú hefur ekki heimild til að búa til kafla.',\n    'import_perms_pages' => 'Þú hefur ekki heimild til að búa til síður.',\n    'import_perms_images' => 'Þú hefur ekki heimild til að búa til myndir.',\n    'import_perms_attachments' => 'Þú hefur ekki heimild til að búa til viðhengi.',\n\n    // API errors\n    'api_no_authorization_found' => 'Engin auðkenningar tóki fannst í aðgerðinni',\n    'api_bad_authorization_format' => 'Auðkenningar tóki fannst með aðgerðinni en snið hans er rangt',\n    'api_user_token_not_found' => 'Engin API tóki fannst á móti þessum auðkenningar tóka',\n    'api_incorrect_token_secret' => 'Leyndarmálið sem gefið var upp fyrir API tókann er rangt',\n    'api_user_no_api_permission' => 'Eigandi API tókans hefur ekki heimild til að gera API köll',\n    'api_user_token_expired' => 'Auðkenningar tókin er útrunninn',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Villa kom upp viðað reyna senda prufu tölvupóst:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'Þetta vistfang stemmir ekki við leyfða SSR biðlara',\n];\n"
  },
  {
    "path": "lang/is/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Ný athugasemd á síðu :pageName',\n    'new_comment_intro' => 'Notandi hefur sett inn athugasemd á síðu á :appName:',\n    'new_page_subject' => 'Ný síða á: :pageName',\n    'new_page_intro' => 'Ný síða hefur verið búin til á :appName:',\n    'updated_page_subject' => 'Uppfærð síða á: :pageName',\n    'updated_page_intro' => 'Síða hefur verið uppfærð á :appName:',\n    'updated_page_debounce' => 'Til að fyrirbyggja fjöldatilkynningar verður þér ekki sendar tilkynningar í smá stund um uppfærslu á þessari síðu frá sama höfundi.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Síðunafn:',\n    'detail_page_path' => 'Síðuslóð:',\n    'detail_commenter' => 'Notandi:',\n    'detail_comment' => 'Athugasemd:',\n    'detail_created_by' => 'Búið til af:',\n    'detail_updated_by' => 'Uppfært af:',\n\n    'action_view_comment' => 'Skoða athugasemd',\n    'action_view_page' => 'Skoða síðu',\n\n    'footer_reason' => 'Þessi tilkynning var send til þín vegna :link nær yfir þessa virkni á þessum hlut.',\n    'footer_reason_link' => 'stillingar á tilkynningum til þín',\n];\n"
  },
  {
    "path": "lang/is/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Fyrri',\n    'next'     => 'Næsta&raquo;',\n\n];\n"
  },
  {
    "path": "lang/is/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Lykilorð verður að vera að lágmarki 8 stafir og stemma saman.',\n    'user' => \"Enginn notandi finnst með þetta netfang.\",\n    'token' => 'Tókinn er ógildur fyrir þetta netfang.',\n    'sent' => 'Þér hefur verið sendur hlekkur í tölvupósti!',\n    'reset' => 'Lykilorðinu hefur verið breytt!',\n\n];\n"
  },
  {
    "path": "lang/is/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Minn aðgangur',\n\n    'shortcuts' => 'Flýtileiðir',\n    'shortcuts_interface' => 'UI, stillingar flýtileiða',\n    'shortcuts_toggle_desc' => 'Hér getur þú virkjað eða óvirkjað flýtilykla, notað fyrir leiðarstýringu og aðgerðir.',\n    'shortcuts_customize_desc' => 'Þú getur stillt alla flýtilyklana hér að neðan. Þú ýtir þá þann flýtilykil sem þú vilt nota eftir að þú hefur valið innsláttarleið.',\n    'shortcuts_toggle_label' => 'Flýtilyklar virkjaðir',\n    'shortcuts_section_navigation' => 'Leiðarstýring',\n    'shortcuts_section_actions' => 'Algengar aðgerðir',\n    'shortcuts_save' => 'Vista flýtilykla',\n    'shortcuts_overlay_desc' => 'Ath: þegar flýtilyklar eru virkjaðir er hægt að fá aðstoð með því að ýta á \"?\" sem mun yfirstrika þær flýtileiðir sem í boði eru.',\n    'shortcuts_update_success' => 'Stillingar flýtilykla hafa verið uppfærðar!',\n    'shortcuts_overview_desc' => 'Stjórna þeim flýtilyklum sem í boði eru.',\n\n    'notifications' => 'Stillingar tilkynninga',\n    'notifications_desc' => 'Stýrðu þeim tölvupóst tilkynningum sem þú færð þegar ákveðnar aðgerðir eru gerðar af kerfinu.',\n    'notifications_opt_own_page_changes' => 'Láta vita þegar gerðar eru breytingar á síðum sem ég á',\n    'notifications_opt_own_page_comments' => 'Láta vita þegar gerðar eru athugasmedir við síður sem ég á',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Láta vita þegar athugasemdum mínum er svarað',\n    'notifications_save' => 'Vista stillingar',\n    'notifications_update_success' => 'Stillingar á tilkynningum hafa verið uppfærðar!',\n    'notifications_watched' => 'Watched & Ignored Items',\n    'notifications_watched_desc' => 'Fyrir neðan eru hlutir sem hafa sérsniðna eftirfylgni stillta. Til að uppfæra stillingarnar fyrir þessa hluti skaltu skoða hann og og finna skoðastillinguna í hliðarstikunni.',\n\n    'auth' => 'Aðgangur og öryggi',\n    'auth_change_password' => 'Breyta lykilorði',\n    'auth_change_password_desc' => 'Breyta lykilorðinu sem þú notar til að skrá þig inn í hugbúnaðinn. Lykilorðið verður að vera a. m. k 8 stafa langt.',\n    'auth_change_password_success' => 'Lykilorði hefur verið breytt!',\n\n    'profile' => 'Upplýsingar um prófíl',\n    'profile_desc' => 'Stýra þeim upplýsingum um þig sem aðrir notendur sjá.',\n    'profile_view_public' => 'Skoða almennan prófíl',\n    'profile_name_desc' => 'Stilla þitt notendanafn sem er sýnlegt öðrum notendum.',\n    'profile_email_desc' => 'Þetta netfang verður notað fyrir tilkynningar og aðgang að kerfinu hafir þú valið svo.',\n    'profile_email_no_permission' => 'Þú hefur ekki heimild til að breyta netfanginu þinu. Ef þú vilt láta breyta því verður þú að hafa samband við kerfisstjóra.',\n    'profile_avatar_desc' => 'Veldu mynd til að sýna öðrum notendum. Helst þarf þessi mynd að vera ferköntuð og um það bil 256px bæði á breidd og hæð.',\n    'profile_admin_options' => 'Stillingar kerfisstjóra',\n    'profile_admin_options_desc' => 'Viðbótar kerfistjóra stillingar, til dæmis stjórnun á hlutverkum sem finna á í \"Stillingar > Notendur svæði hugbúnaðarins.',\n\n    'delete_account' => 'Eyða aðgangi',\n    'delete_my_account' => 'Eyða reikningi mínum',\n    'delete_my_account_desc' => 'Þetta mun eyða þínum aðgangi að hugbúnaðinum. Þú munt ekki geta enduheimt aðganginn. Efni sem þú hefur búið til eins og síður og þær myndir sem þú hefur sent inn munu halda sér.',\n    'delete_my_account_warning' => 'Ertu viss um að þú viljir eyða aðganginum þínum?',\n];\n"
  },
  {
    "path": "lang/is/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Stillingar',\n    'settings_save' => 'Vista stillingar',\n    'system_version' => 'Kerfisútgáfa',\n    'categories' => 'Flokkar',\n\n    // App Settings\n    'app_customization' => 'Sérsníða',\n    'app_features_security' => 'Eigindi og öryggi',\n    'app_name' => 'Nafn kerfis',\n    'app_name_desc' => 'Þetta nafn er sýnd í titli og í öllum tölvupóstum sem sendir eru.',\n    'app_name_header' => 'Sýna nafn í titli',\n    'app_public_access' => 'Almennur aðgangur',\n    'app_public_access_desc' => 'Með því að virkja þennan valmöguleika munu notendur sem eru ekki skráðir inn geta skoðað innihald vefsins.',\n    'app_public_access_desc_guest' => 'Hægt er að stýra almennum aðgangi í gegnum \"Guest\" notandann.',\n    'app_public_access_toggle' => 'Leyfa almennann aðgang',\n    'app_public_viewing' => 'Leyfa almenna skoðun?',\n    'app_secure_images' => 'Aukið öryggi á mynda upphal',\n    'app_secure_images_toggle' => 'Virkja aukið öryggi á mynda upphal',\n    'app_secure_images_desc' => 'For performance reasons, all images are public. This option adds a random, hard-to-guess string in front of image urls. Ensure directory indexes are not enabled to prevent easy access.',\n    'app_default_editor' => 'Sjálfgefin ritill',\n    'app_default_editor_desc' => 'Veldu hvaða ritil á að nota sjálfgefið þegar unnið er með nýjar síður. Hægt er að yfirskrifa þessa stillingu á hverri síðu ef viðkomandi er með réttindi.',\n    'app_custom_html' => 'Custom HTML Head Content',\n    'app_custom_html_desc' => 'Any content added here will be inserted into the bottom of the <head> section of every page. This is handy for overriding styles or adding analytics code.',\n    'app_custom_html_disabled_notice' => 'Custom HTML head content is disabled on this settings page to ensure any breaking changes can be reverted.',\n    'app_logo' => 'Lógó síðu',\n    'app_logo_desc' => 'This is used in the application header bar, among other areas. This image should be 86px in height. Large images will be scaled down.',\n    'app_icon' => 'Íkon síðu',\n    'app_icon_desc' => 'Þetta íkon er notað í tabs í vöfrum og sem flýtivísir á síðu. Þetta ætti að vera 256px PNG mynd.',\n    'app_homepage' => 'Heimasíða',\n    'app_homepage_desc' => 'Select a view to show on the homepage instead of the default view. Page permissions are ignored for selected pages.',\n    'app_homepage_select' => 'Veldu síðu',\n    'app_footer_links' => 'Neðangreins hlekkir',\n    'app_footer_links_desc' => 'Add links to show within the site footer. These will be displayed at the bottom of most pages, including those that do not require login. You can use a label of \"trans::<key>\" to use system-defined translations. For example: Using \"trans::common.privacy_policy\" will provide the translated text \"Privacy Policy\" and \"trans::common.terms_of_service\" will provide the translated text \"Terms of Service\".',\n    'app_footer_links_label' => 'Miði hlekks',\n    'app_footer_links_url' => 'Vistfang hlekks',\n    'app_footer_links_add' => 'Bæta við neðangreinshlekk',\n    'app_disable_comments' => 'Óvirkja athugasemdir',\n    'app_disable_comments_toggle' => 'Óvirkja athugasemdir',\n    'app_disable_comments_desc' => 'Disables comments across all pages in the application. <br> Existing comments are not shown.',\n\n    // Color settings\n    'color_scheme' => 'Litaþema hugbúnaðar',\n    'color_scheme_desc' => 'Set the colors to use in the application user interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.',\n    'ui_colors_desc' => 'Set the application primary color and default link color. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the application interface.',\n    'app_color' => 'Aðal litur',\n    'link_color' => 'Aðal litur hlekkja',\n    'content_colors_desc' => 'Set colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.',\n    'bookshelf_color' => 'Litur hillu',\n    'book_color' => 'Litur Bóka',\n    'chapter_color' => 'Litur kalfa',\n    'page_color' => 'Litur síðu',\n    'page_draft_color' => 'Litur draga',\n\n    // Registration Settings\n    'reg_settings' => 'Skráning',\n    'reg_enable' => 'Virkja skráningar',\n    'reg_enable_toggle' => 'Virkja skráningar',\n    'reg_enable_desc' => 'When registration is enabled user will be able to sign themselves up as an application user. Upon registration they are given a single, default user role.',\n    'reg_default_role' => 'Sjálfgefið hlutverk notanda eftir skráningu',\n    'reg_enable_external_warning' => 'The option above is ignored while external LDAP or SAML authentication is active. User accounts for non-existing members will be auto-created if authentication, against the external system in use, is successful.',\n    'reg_email_confirmation' => 'Tölvupóst staðfesting',\n    'reg_email_confirmation_toggle' => 'Krefast staðfestingar í tölvupósti',\n    'reg_confirm_email_desc' => 'If domain restriction is used then email confirmation will be required and this option will be ignored.',\n    'reg_confirm_restrict_domain' => 'Læsingar á lén',\n    'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.',\n    'reg_confirm_restrict_domain_placeholder' => 'Engin skilyrði sett',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Viðhald',\n    'maint_image_cleanup' => 'Taka til í myndum',\n    'maint_image_cleanup_desc' => 'Scans page & revision content to check which images and drawings are currently in use and which images are redundant. Ensure you create a full database and image backup before running this.',\n    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',\n    'maint_image_cleanup_run' => 'Keyra hreinsun',\n    'maint_image_cleanup_warning' => ':count potentially unused images were found. Are you sure you want to delete these images?',\n    'maint_image_cleanup_success' => ':count potentially unused images found and deleted!',\n    'maint_image_cleanup_nothing_found' => 'Engar ónotaðar myndir fundust, engu eytt!',\n    'maint_send_test_email' => 'Senda prufu tölvupóst',\n    'maint_send_test_email_desc' => 'Þessi aðgerð sendir prufu tölvupóst á netfangið sem stillt er á í prófílnum þínum.',\n    'maint_send_test_email_run' => 'Senda prufu tölvupóst',\n    'maint_send_test_email_success' => 'Tölvupóstur sendur á :address',\n    'maint_send_test_email_mail_subject' => 'Prufupóstur',\n    'maint_send_test_email_mail_greeting' => 'Það virðist virka að senda tölvupóst!',\n    'maint_send_test_email_mail_text' => 'Congratulations! As you received this email notification, your email settings seem to be configured properly.',\n    'maint_recycle_bin_desc' => 'Deleted shelves, books, chapters & pages are sent to the recycle bin so they can be restored or permanently deleted. Older items in the recycle bin may be automatically removed after a while depending on system configuration.',\n    'maint_recycle_bin_open' => 'Opna ruslatunnu',\n    'maint_regen_references' => 'Regenerate References',\n    'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',\n    'maint_regen_references_success' => 'Reference index has been regenerated!',\n    'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Ruslatunna',\n    'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',\n    'recycle_bin_deleted_item' => 'Eyddur hlutur',\n    'recycle_bin_deleted_parent' => 'Foreldri',\n    'recycle_bin_deleted_by' => 'Eytt af',\n    'recycle_bin_deleted_at' => 'Eytt þann',\n    'recycle_bin_permanently_delete' => 'Eyða varanlega',\n    'recycle_bin_restore' => 'Endurheimta',\n    'recycle_bin_contents_empty' => 'Ruslatunnan er tóm',\n    'recycle_bin_empty' => 'Tæma ruslatunnu',\n    'recycle_bin_empty_confirm' => 'This will permanently destroy all items in the recycle bin including content contained within each item. Are you sure you want to empty the recycle bin?',\n    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?',\n    'recycle_bin_destroy_list' => 'Hlutir til eyðingar',\n    'recycle_bin_restore_list' => 'Hlutir til endurheimtar',\n    'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.',\n    'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.',\n    'recycle_bin_restore_parent' => 'Endurheimta foreldri',\n    'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',\n    'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',\n\n    // Audit Log\n    'audit' => 'Ferilskrá',\n    'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',\n    'audit_event_filter' => 'Atburðasía',\n    'audit_event_filter_no_filter' => 'Engin sía',\n    'audit_deleted_item' => 'Eyddur hlutur',\n    'audit_deleted_item_name' => 'Nafn :name',\n    'audit_table_user' => 'Notandi',\n    'audit_table_event' => 'Atburður',\n    'audit_table_related' => 'Tengdur hlutur eða lýsing',\n    'audit_table_ip' => 'IP tala',\n    'audit_table_date' => 'Dagsetning virkni',\n    'audit_date_from' => 'Dagsetning frá',\n    'audit_date_to' => 'Dagsetning til',\n\n    // Role Settings\n    'roles' => 'Hlutverk',\n    'role_user_roles' => 'Notanda hlutverk',\n    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',\n    'roles_x_users_assigned' => ':count user assigned|:count users assigned',\n    'roles_x_permissions_provided' => ':count permission|:count permissions',\n    'roles_assigned_users' => 'Skilgreindir notendur',\n    'roles_permissions_provided' => 'Uppgefnar heimildir',\n    'role_create' => 'Búa til nýtt hlutverk',\n    'role_delete' => 'Eyða hlutverki',\n    'role_delete_confirm' => 'This will delete the role with the name \\':roleName\\'.',\n    'role_delete_users_assigned' => 'This role has :userCount users assigned to it. If you would like to migrate the users from this role select a new role below.',\n    'role_delete_no_migration' => \"Don't migrate users\",\n    'role_delete_sure' => 'Are you sure you want to delete this role?',\n    'role_edit' => 'Breyta hlutverki',\n    'role_details' => 'Lýsing á hlutverki',\n    'role_name' => 'Nafn hlutverks',\n    'role_desc' => 'Stutt lýsing á hlutverki',\n    'role_mfa_enforced' => 'Krefst tvöfaldrar auðkenningar',\n    'role_external_auth_id' => 'Ytri auðkenningarnúmer',\n    'role_system' => 'Réttindastillingar kerfis',\n    'role_manage_users' => 'Sýsla með notendur',\n    'role_manage_roles' => 'Stýra hlutverkum og réttindum hlutverka',\n    'role_manage_entity_permissions' => 'Stýra öllum bóka, kafla og síðu réttindum',\n    'role_manage_own_entity_permissions' => 'Stýra réttindum á eigin bókum, köflum og síðum',\n    'role_manage_page_templates' => 'Stýra síðu sníðmátum',\n    'role_access_api' => 'Access system API',\n    'role_manage_settings' => 'Manage app settings',\n    'role_export_content' => 'Flytja út efni',\n    'role_import_content' => 'Flytja inn efni',\n    'role_editor_change' => 'Skipta um ritil síðu',\n    'role_notifications' => 'Receive & manage notifications',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Asset Permissions',\n    'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',\n    'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',\n    'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.',\n    'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'Allt',\n    'role_own' => 'Eigin',\n    'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',\n    'role_save' => 'Vista hlutverk',\n    'role_users' => 'Notendur í þessu hlutverki',\n    'role_users_none' => 'Engir notendur eru eins og er í þessu hlutverki',\n\n    // Users\n    'users' => 'Notendur',\n    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',\n    'user_profile' => 'Prófíll notanda',\n    'users_add_new' => 'Bæta við nýjum notanda',\n    'users_search' => 'Leita að notendum',\n    'users_latest_activity' => 'Síðasta virkni',\n    'users_details' => 'Notendaupplýsingar',\n    'users_details_desc' => 'Set a display name and an email address for this user. The email address will be used for logging into the application.',\n    'users_details_desc_no_email' => 'Set a display name for this user so others can recognise them.',\n    'users_role' => 'Hlutverk notenda',\n    'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.',\n    'users_password' => 'Lykilorð notanda',\n    'users_password_desc' => 'Setja lykilorð sem þú notar til að skrá þig inn í hugbúnaðinn. Lykilorðið verður að vera a. m. k 8 stafa langt.',\n    'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.',\n    'users_send_invite_option' => 'Senda boð á notanda með tölvupósti',\n    'users_external_auth_id' => 'Ytra auðkenningar númer',\n    'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.',\n    'users_password_warning' => 'Only fill the below if you would like to change the password for this user.',\n    'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.',\n    'users_delete' => 'Eyða notanda',\n    'users_delete_named' => 'Eyða notanda :userName',\n    'users_delete_warning' => 'This will fully delete this user with the name \\':userName\\' from the system.',\n    'users_delete_confirm' => 'Ertu viss um að þú viljir eyða þessum notanda?',\n    'users_migrate_ownership' => 'Færa eignarhald',\n    'users_migrate_ownership_desc' => 'Select a user here if you want another user to become the owner of all items currently owned by this user.',\n    'users_none_selected' => 'Engin notandi valin',\n    'users_edit' => 'Breyta notanda',\n    'users_edit_profile' => 'Breyta prófíl',\n    'users_avatar' => 'Avatar notanda',\n    'users_avatar_desc' => 'Select an image to represent this user. This should be approx 256px square.',\n    'users_preferred_language' => 'Valið tungumál',\n    'users_preferred_language_desc' => 'This option will change the language used for the user-interface of the application. This will not affect any user-created content.',\n    'users_social_accounts' => 'Samfélagsmiðla reikningar',\n    'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.',\n    'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not revoke previously authorized access. Revoke access from your profile settings on the connected social account.',\n    'users_social_connect' => 'Tengja aðgang',\n    'users_social_disconnect' => 'Aftengja aðgang',\n    'users_social_status_connected' => 'Tengt',\n    'users_social_status_disconnected' => 'Aftengt',\n    'users_social_connected' => ':socialAccount account was successfully attached to your profile.',\n    'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.',\n    'users_api_tokens' => 'API tókar',\n    'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.',\n    'users_api_tokens_none' => 'No API tokens have been created for this user',\n    'users_api_tokens_create' => 'Búa til tóka',\n    'users_api_tokens_expires' => 'Rennur út',\n    'users_api_tokens_docs' => 'API Documentation',\n    'users_mfa' => 'Multi-Factor Authentication',\n    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'users_mfa_x_methods' => ':count method configured|:count methods configured',\n    'users_mfa_configure' => 'Configure Methods',\n\n    // API Tokens\n    'user_api_token_create' => 'Búa til API tóka',\n    'user_api_token_name' => 'Nafn',\n    'user_api_token_name_desc' => 'Give your token a readable name as a future reminder of its intended purpose.',\n    'user_api_token_expiry' => 'Rennur út þann',\n    'user_api_token_expiry_desc' => 'Set a date at which this token expires. After this date, requests made using this token will no longer work. Leaving this field blank will set an expiry 100 years into the future.',\n    'user_api_token_create_secret_message' => 'Immediately after creating this token a \"Token ID\" & \"Token Secret\" will be generated and displayed. The secret will only be shown a single time so be sure to copy the value to somewhere safe and secure before proceeding.',\n    'user_api_token' => 'API tóki',\n    'user_api_token_id' => 'Númer tóka',\n    'user_api_token_id_desc' => 'This is a non-editable system generated identifier for this token which will need to be provided in API requests.',\n    'user_api_token_secret' => 'Tóka leyndarmál',\n    'user_api_token_secret_desc' => 'This is a system generated secret for this token which will need to be provided in API requests. This will only be displayed this one time so copy this value to somewhere safe and secure.',\n    'user_api_token_created' => 'Tóki búinn til :timeAgo',\n    'user_api_token_updated' => 'Tóki uppfærður :timeAgo',\n    'user_api_token_delete' => 'Eyða tóka',\n    'user_api_token_delete_warning' => 'This will fully delete this API token with the name \\':tokenName\\' from the system.',\n    'user_api_token_delete_confirm' => 'Are you sure you want to delete this API token?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',\n    'webhooks_x_trigger_events' => ':count trigger event|:count trigger events',\n    'webhooks_create' => 'Búa til nýjann Webhook',\n    'webhooks_none_created' => 'Engir Webhooks hafa verið búnir til.',\n    'webhooks_edit' => 'Breyta Webhook',\n    'webhooks_save' => 'Vista Webhook',\n    'webhooks_details' => 'Upplýsingar um Webhook',\n    'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',\n    'webhooks_events' => 'Webhook atburðir',\n    'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',\n    'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\\'t expose confidential content.',\n    'webhooks_events_all' => 'Allir kerfis atburðir',\n    'webhooks_name' => 'Webhook nafn',\n    'webhooks_timeout' => 'Webhook tímamörk (Sekúndur)',\n    'webhooks_endpoint' => 'Webhook endapunktur',\n    'webhooks_active' => 'Webhook virkur',\n    'webhook_events_table_header' => 'Atburðir',\n    'webhooks_delete' => 'Eyða Webhook',\n    'webhooks_delete_warning' => 'Þessi aðgerð mun eyða þessum Webhook að fullu \\':webhookName\\',.',\n    'webhooks_delete_confirm' => 'Ertu viss um að þú viljir eyða þessum Webhook?',\n    'webhooks_format_example' => 'Dæmi um snið á Webhook',\n    'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The \"related_item\" and \"url\" properties are optional and will depend on the type of event triggered.',\n    'webhooks_status' => 'Staða á Webhook',\n    'webhooks_last_called' => 'Síðast kallað:',\n    'webhooks_last_errored' => 'Síðasta villa kom upp:',\n    'webhooks_last_error_message' => 'Síðustu villuskilaboð:',\n\n    // Licensing\n    'licenses' => 'Leyfi',\n    'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.',\n    'licenses_bookstack' => 'Bookstack leyfi',\n    'licenses_php' => 'PHP kóðasafnsleyfi',\n    'licenses_js' => 'Javascript kóðasafnsleyfi',\n    'licenses_other' => 'Önnur leyfi',\n    'license_details' => 'Upplýsingar um leyfi',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/is/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => 'Það verður að samþykkja :attribute.',\n    'active_url'           => ':attribute Er ekki gilt vistfang.',\n    'after'                => ':attribute verður að vera dagsetning eftir :date.',\n    'alpha'                => ':attribute má eingöngu innihalda stafi.',\n    'alpha_dash'           => ':attribute má eingöngu innihalda stafi, tölustafi, strik og undirstrik.',\n    'alpha_num'            => ':attribute má eingöngu innihalda stafi og tölustafi.',\n    'array'                => 'The :attribute must be an array.',\n    'backup_codes'         => 'Kóðinn sem þú gafst upp er ekki gildur eða hefur þegar verið notaður.',\n    'before'               => ':attribute verður að vera dagsetning á undan :date.',\n    'between'              => [\n        'numeric' => ':attribute verður að vera á milli :min og :max.',\n        'file'    => ':attribute verður að vera á milli :min og :max kílóbæti.',\n        'string'  => ':attribute verður að vera á milli :min og :max stafir.',\n        'array'   => ':attribute verður að vera á milli :min og :max fjöldi.',\n    ],\n    'boolean'              => ':attribute gildið verður að vera true og false.',\n    'confirmed'            => 'The :attribute confirmation does not match.',\n    'date'                 => 'The :attribute is not a valid date.',\n    'date_format'          => 'The :attribute does not match the format :format.',\n    'different'            => 'The :attribute and :other must be different.',\n    'digits'               => 'The :attribute must be :digits digits.',\n    'digits_between'       => 'The :attribute must be between :min and :max digits.',\n    'email'                => 'The :attribute must be a valid email address.',\n    'ends_with' => 'The :attribute must end with one of the following: :values',\n    'file'                 => 'The :attribute must be provided as a valid file.',\n    'filled'               => 'The :attribute field is required.',\n    'gt'                   => [\n        'numeric' => 'The :attribute must be greater than :value.',\n        'file'    => 'The :attribute must be greater than :value kilobytes.',\n        'string'  => 'The :attribute must be greater than :value characters.',\n        'array'   => 'The :attribute must have more than :value items.',\n    ],\n    'gte'                  => [\n        'numeric' => 'The :attribute must be greater than or equal :value.',\n        'file'    => 'The :attribute must be greater than or equal :value kilobytes.',\n        'string'  => 'The :attribute must be greater than or equal :value characters.',\n        'array'   => 'The :attribute must have :value items or more.',\n    ],\n    'exists'               => 'The selected :attribute is invalid.',\n    'image'                => 'The :attribute must be an image.',\n    'image_extension'      => 'The :attribute must have a valid & supported image extension.',\n    'in'                   => 'The selected :attribute is invalid.',\n    'integer'              => 'The :attribute must be an integer.',\n    'ip'                   => 'The :attribute must be a valid IP address.',\n    'ipv4'                 => 'The :attribute must be a valid IPv4 address.',\n    'ipv6'                 => 'The :attribute must be a valid IPv6 address.',\n    'json'                 => 'The :attribute must be a valid JSON string.',\n    'lt'                   => [\n        'numeric' => 'The :attribute must be less than :value.',\n        'file'    => 'The :attribute must be less than :value kilobytes.',\n        'string'  => 'The :attribute must be less than :value characters.',\n        'array'   => 'The :attribute must have less than :value items.',\n    ],\n    'lte'                  => [\n        'numeric' => 'The :attribute must be less than or equal :value.',\n        'file'    => 'The :attribute must be less than or equal :value kilobytes.',\n        'string'  => 'The :attribute must be less than or equal :value characters.',\n        'array'   => 'The :attribute must not have more than :value items.',\n    ],\n    'max'                  => [\n        'numeric' => 'The :attribute may not be greater than :max.',\n        'file'    => 'The :attribute may not be greater than :max kilobytes.',\n        'string'  => 'The :attribute may not be greater than :max characters.',\n        'array'   => 'The :attribute may not have more than :max items.',\n    ],\n    'mimes'                => 'The :attribute must be a file of type: :values.',\n    'min'                  => [\n        'numeric' => 'The :attribute must be at least :min.',\n        'file'    => 'The :attribute must be at least :min kilobytes.',\n        'string'  => 'The :attribute must be at least :min characters.',\n        'array'   => 'The :attribute must have at least :min items.',\n    ],\n    'not_in'               => 'The selected :attribute is invalid.',\n    'not_regex'            => 'The :attribute format is invalid.',\n    'numeric'              => 'The :attribute must be a number.',\n    'regex'                => 'The :attribute format is invalid.',\n    'required'             => 'The :attribute field is required.',\n    'required_if'          => 'The :attribute field is required when :other is :value.',\n    'required_with'        => 'The :attribute field is required when :values is present.',\n    'required_with_all'    => 'The :attribute field is required when :values is present.',\n    'required_without'     => 'The :attribute field is required when :values is not present.',\n    'required_without_all' => 'The :attribute field is required when none of :values are present.',\n    'same'                 => 'The :attribute and :other must match.',\n    'safe_url'             => 'The provided link may not be safe.',\n    'size'                 => [\n        'numeric' => 'The :attribute must be :size.',\n        'file'    => 'The :attribute must be :size kilobytes.',\n        'string'  => 'The :attribute must be :size characters.',\n        'array'   => 'The :attribute must contain :size items.',\n    ],\n    'string'               => 'The :attribute must be a string.',\n    'timezone'             => 'The :attribute must be a valid zone.',\n    'totp'                 => 'The provided code is not valid or has expired.',\n    'unique'               => 'The :attribute has already been taken.',\n    'url'                  => 'The :attribute format is invalid.',\n    'uploaded'             => 'The file could not be uploaded. The server may not accept files of this size.',\n\n    'zip_file' => 'The :attribute needs to reference a file within the ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',\n    'zip_model_expected' => 'Data object expected but \":type\" found.',\n    'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Staðfestingu á lykilorði er krafist',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/it/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'ha creato la pagina',\n    'page_create_notification'    => 'Pagina creata con successo',\n    'page_update'                 => 'ha aggiornato la pagina',\n    'page_update_notification'    => 'Pagina aggiornata con successo',\n    'page_delete'                 => 'ha eliminato la pagina',\n    'page_delete_notification'    => 'Pagina eliminata con successo',\n    'page_restore'                => 'ha ripristinato la pagina',\n    'page_restore_notification'   => 'Pagina ripristinata con successo',\n    'page_move'                   => 'ha spostato la pagina',\n    'page_move_notification'      => 'Pagina spostata con successo',\n\n    // Chapters\n    'chapter_create'              => 'ha creato il capitolo',\n    'chapter_create_notification' => 'Capitolo creato con successo',\n    'chapter_update'              => 'ha aggiornato il capitolo',\n    'chapter_update_notification' => 'Capitolo aggiornato con successo',\n    'chapter_delete'              => 'ha eliminato il capitolo',\n    'chapter_delete_notification' => 'Capitolo eliminato con successo',\n    'chapter_move'                => 'ha spostato il capitolo',\n    'chapter_move_notification' => 'Capitolo spostato con successo',\n\n    // Books\n    'book_create'                 => 'ha creato il libro',\n    'book_create_notification'    => 'Libro creato con successo',\n    'book_create_from_chapter'              => 'ha convertito da capitolo a libro',\n    'book_create_from_chapter_notification' => 'Capitolo convertito in libro con successo',\n    'book_update'                 => 'ha aggiornato il libro',\n    'book_update_notification'    => 'Libro aggiornato con successo',\n    'book_delete'                 => 'ha eliminato il libro',\n    'book_delete_notification'    => 'Libro eliminato con successo',\n    'book_sort'                   => 'ha ordinato il libro',\n    'book_sort_notification'      => 'Libro riordinato con successo',\n\n    // Bookshelves\n    'bookshelf_create'            => 'ha creato la libreria',\n    'bookshelf_create_notification'    => 'Libreria creata con successo',\n    'bookshelf_create_from_book'    => 'ha convertito il libro in libreria',\n    'bookshelf_create_from_book_notification'    => 'Libro convertito in libreria con successo',\n    'bookshelf_update'                 => 'ha aggiornato la libreria',\n    'bookshelf_update_notification'    => 'Libreria aggiornata con successo',\n    'bookshelf_delete'                 => 'ha eliminato la libreria',\n    'bookshelf_delete_notification'    => 'Libreria eliminata con successo',\n\n    // Revisions\n    'revision_restore' => 'ha ripristinato la revisione',\n    'revision_delete' => 'ha eliminato la revisione',\n    'revision_delete_notification' => 'Revisione eliminata con successo',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" è stato aggiunto ai tuoi preferiti',\n    'favourite_remove_notification' => '\":name\" è stato rimosso dai tuoi preferiti',\n\n    // Watching\n    'watch_update_level_notification' => 'Preferenze di monitoraggio aggiornate con successo',\n\n    // Auth\n    'auth_login' => 'connesso',\n    'auth_register' => 'registrato come nuovo utente',\n    'auth_password_reset_request' => 'ha richiesto di reimpostare la password utente',\n    'auth_password_reset_update' => 'ha reimpostato la password utente',\n    'mfa_setup_method' => 'ha configurato un metodo multi-fattore',\n    'mfa_setup_method_notification' => 'Metodo multi-fattore impostato con successo',\n    'mfa_remove_method' => 'ha rimosso un metodo multi-fattore',\n    'mfa_remove_method_notification' => 'Metodo multi-fattore rimosso con successo',\n\n    // Settings\n    'settings_update' => 'ha aggiornato le impostazioni',\n    'settings_update_notification' => 'Impostazioni aggiornate con successo',\n    'maintenance_action_run' => 'ha eseguito un\\'azione di manutenzione',\n\n    // Webhooks\n    'webhook_create' => 'ha creato un webhook',\n    'webhook_create_notification' => 'Webhook creato con successo',\n    'webhook_update' => 'ha aggiornato un webhook',\n    'webhook_update_notification' => 'Webhook aggiornato con successo',\n    'webhook_delete' => 'ha eliminato un webhook',\n    'webhook_delete_notification' => 'Webhook eliminato con successo',\n\n    // Imports\n    'import_create' => 'importazione creata',\n    'import_create_notification' => 'Importazione caricata con successo',\n    'import_run' => 'importazione aggiornata',\n    'import_run_notification' => 'Contenuto importato con successo',\n    'import_delete' => 'importazione cancellata',\n    'import_delete_notification' => 'Importazione eliminata con successo',\n\n    // Users\n    'user_create' => 'ha creato un utente',\n    'user_create_notification' => 'Utente creato con successo',\n    'user_update' => 'ha aggiornato un utente',\n    'user_update_notification' => 'Utente aggiornato con successo',\n    'user_delete' => 'ha eliminato un utente',\n    'user_delete_notification' => 'Utente rimosso con successo',\n\n    // API Tokens\n    'api_token_create' => 'ha creato un token API',\n    'api_token_create_notification' => 'Token API creato con successo',\n    'api_token_update' => 'ha aggiornato un token API',\n    'api_token_update_notification' => 'Token API aggiornato correttamente',\n    'api_token_delete' => 'ha eliminato token API',\n    'api_token_delete_notification' => 'Token API eliminato con successo',\n\n    // Roles\n    'role_create' => 'ha creato un ruolo',\n    'role_create_notification' => 'Ruolo creato con successo',\n    'role_update' => 'ha aggiornato un ruolo',\n    'role_update_notification' => 'Ruolo aggiornato con successo',\n    'role_delete' => 'ha eliminato un ruolo',\n    'role_delete_notification' => 'Ruolo eliminato con successo',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'ha svuotato il cestino',\n    'recycle_bin_restore' => 'ha ripristinato dal cestino',\n    'recycle_bin_destroy' => 'ha rimosso dal cestino',\n\n    // Comments\n    'commented_on'                => 'ha commentato in',\n    'comment_create'              => 'ha aggiunto un commento',\n    'comment_update'              => 'ha aggiornato un commento',\n    'comment_delete'              => 'ha rimosso un commento',\n\n    // Sort Rules\n    'sort_rule_create' => 'regola di ordinamento creata',\n    'sort_rule_create_notification' => 'Ordina regola creata con successo',\n    'sort_rule_update' => 'regola di ordinamento aggiornata',\n    'sort_rule_update_notification' => 'Regola di ordinamento aggiornata con successo',\n    'sort_rule_delete' => 'regola di ordinamento eliminata',\n    'sort_rule_delete_notification' => 'Regola di ordinamento eliminata con successo',\n\n    // Other\n    'permissions_update'          => 'ha aggiornate le autorizzazioni',\n];\n"
  },
  {
    "path": "lang/it/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Credenziali errate.',\n    'throttle' => 'Troppi tentativi di login. Riprova in :seconds secondi.',\n\n    // Login & Register\n    'sign_up' => 'Registrati',\n    'log_in' => 'Login',\n    'log_in_with' => 'Login con :socialDriver',\n    'sign_up_with' => 'Registrati con :socialDriver',\n    'logout' => 'Esci',\n\n    'name' => 'Nome',\n    'username' => 'Username',\n    'email' => 'Email',\n    'password' => 'Password',\n    'password_confirm' => 'Conferma password',\n    'password_hint' => 'Deve essere lunga almeno 8 caratteri',\n    'forgot_password' => 'Password dimenticata?',\n    'remember_me' => 'Ricordami',\n    'ldap_email_hint' => 'Inserisci un\\'email per usare quest\\'account.',\n    'create_account' => 'Crea un account',\n    'already_have_account' => 'Hai già un account?',\n    'dont_have_account' => 'Non hai un account?',\n    'social_login' => 'Login Social',\n    'social_registration' => 'Registrazione Social',\n    'social_registration_text' => 'Registrati e accedi utilizzando un altro servizio.',\n\n    'register_thanks' => 'Grazie per esserti registrato!',\n    'register_confirm' => 'Controlla la tua mail e clicca il pulsante di conferma per accedere a :appName.',\n    'registrations_disabled' => 'La registrazione è disabilitata al momento',\n    'registration_email_domain_invalid' => 'Questo dominio email non ha accesso a questa applicazione',\n    'register_success' => 'Grazie per la registrazione! Sei registrato e connesso.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Tentativo di accesso',\n    'auto_init_starting_desc' => 'Stiamo contattando il tuo sistema di autenticazione per avviare il processo di login. Se non ci sono progressi dopo 5 secondi, puoi provare a cliccare sul link qui sotto.',\n    'auto_init_start_link' => 'Procedi con l\\'autenticazione',\n\n    // Password Reset\n    'reset_password' => 'Reimposta la password',\n    'reset_password_send_instructions' => 'Inserisci il tuo indirizzo e ti verrà inviata una mail contenente un link per reimpostare la tua password.',\n    'reset_password_send_button' => 'Invia link di ripristino',\n    'reset_password_sent' => 'Se la mail verrà trovata nel sistema, verrà inviato a :email un link di ripristino della password.',\n    'reset_password_success' => 'La tua password è stata ripristinata correttamente.',\n    'email_reset_subject' => 'Reimposta la password di :appName',\n    'email_reset_text' => 'Stai ricevendo questa mail perché abbiamo ricevuto una richiesta di ripristino della password per il tuo account.',\n    'email_reset_not_requested' => 'Se non hai richiesto un ripristino della password, ignora questa mail.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Conferma email per :appName',\n    'email_confirm_greeting' => 'Grazie per esserti registrato a :appName!',\n    'email_confirm_text' => 'Conferma il tuo indirizzo email cliccando il pulsante sotto:',\n    'email_confirm_action' => 'Conferma email',\n    'email_confirm_send_error' => 'La conferma della mail è richiesta ma non è stato possibile mandare la mail. Contatta l\\'amministratore.',\n    'email_confirm_success' => 'La tua email è stata confermata! Ora dovresti essere in grado di effettuare il login utilizzando questo indirizzo email.',\n    'email_confirm_resent' => 'Mail di conferma reinviata, controlla la tua posta.',\n    'email_confirm_thanks' => 'Grazie per la conferma!',\n    'email_confirm_thanks_desc' => 'Attendi un momento mentre la conferma viene gestita. Se non vieni reindirizzato dopo 3 secondi, clicca sul link \"Continua\" qui sotto per procedere.',\n\n    'email_not_confirmed' => 'Indirizzo email non confermato',\n    'email_not_confirmed_text' => 'Il tuo indirizzo email non è ancora stato confermato.',\n    'email_not_confirmed_click_link' => 'Clicca il link nella mail mandata subito dopo la tua registrazione.',\n    'email_not_confirmed_resend' => 'Se non riesci a trovare la mail puoi rimandarla cliccando il pulsante sotto.',\n    'email_not_confirmed_resend_button' => 'Reinvia conferma',\n\n    // User Invite\n    'user_invite_email_subject' => 'Sei stato invitato a unirti a :appName!',\n    'user_invite_email_greeting' => 'Un account è stato creato per te su :appName.',\n    'user_invite_email_text' => 'Clicca sul pulsante qui sotto per impostare una password e ottenere l\\'accesso:',\n    'user_invite_email_action' => 'Imposta password',\n    'user_invite_page_welcome' => 'Benvenuto in :appName!',\n    'user_invite_page_text' => 'Per completare il tuo account e ottenere l\\'accesso, devi impostare una password che verrà utilizzata per accedere a :appName in futuro.',\n    'user_invite_page_confirm_button' => 'Conferma password',\n    'user_invite_success_login' => 'Password impostata, ora dovresti essere in grado di effettuare il login utilizzando la password impostata per accedere a :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Imposta autenticazione multi-fattore',\n    'mfa_setup_desc' => 'Imposta l\\'autenticazione multi-fattore come misura di sicurezza aggiuntiva per il tuo account.',\n    'mfa_setup_configured' => 'Già configurata',\n    'mfa_setup_reconfigure' => 'Riconfigura',\n    'mfa_setup_remove_confirmation' => 'Sei sicuro di voler rimuovere questo metodo di autenticazione multi-fattore?',\n    'mfa_setup_action' => 'Imposta',\n    'mfa_backup_codes_usage_limit_warning' => 'Hai meno di 5 codici di backup rimanenti. Genera e memorizza un nuovo set prima di esaurire i codici per evitare di essere bloccato dal tuo account.',\n    'mfa_option_totp_title' => 'App mobile',\n    'mfa_option_totp_desc' => 'Per utilizzare l\\'autenticazione multi-fattore avrai bisogno di un\\'applicazione mobile che supporti TOTP come Google Authenticator, Authy o Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Codici di backup',\n    'mfa_option_backup_codes_desc' => 'Genera un insieme di codici di backup monouso che inserirai al login per verificare la tua identità. Assicurati di conservarli in un luogo sicuro e sicuro.',\n    'mfa_gen_confirm_and_enable' => 'Conferma e abilita',\n    'mfa_gen_backup_codes_title' => 'Configurazione codici di backup',\n    'mfa_gen_backup_codes_desc' => 'Conserva l\\'elenco di codici qui sotto in un luogo sicuro. Quando accedi al sistema potrai utilizzare uno dei codici come meccanismo di autenticazione secondario.',\n    'mfa_gen_backup_codes_download' => 'Scarica codici',\n    'mfa_gen_backup_codes_usage_warning' => 'Ogni codice può essere utilizzato solo una volta',\n    'mfa_gen_totp_title' => 'Impostazione app mobile',\n    'mfa_gen_totp_desc' => 'Per utilizzare l\\'autenticazione multi-fattore avrai bisogno di un\\'applicazione mobile che supporti TOTP come Google Authenticator, Authy o Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Scansiona il codice QR qui sotto utilizzando la tua app di autenticazione preferita per iniziare.',\n    'mfa_gen_totp_verify_setup' => 'Verifica configurazione',\n    'mfa_gen_totp_verify_setup_desc' => 'Verifica che tutto funzioni inserendo un codice, generato all\\'interno della tua app di autenticazione, nella casella di testo sottostante:',\n    'mfa_gen_totp_provide_code_here' => 'Inserisci qui il codice generato dall\\'app',\n    'mfa_verify_access' => 'Verifica accesso',\n    'mfa_verify_access_desc' => 'Il tuo account utente richiede che tu confermi la tua identità tramite un ulteriore livello di verifica prima di ottenere l\\'accesso. Verifica usando uno dei tuoi metodi configurati per continuare.',\n    'mfa_verify_no_methods' => 'Nessun metodo configurato',\n    'mfa_verify_no_methods_desc' => 'Non è stato possibile trovare metodi di autenticazione multi-fattore per il tuo account. Devi impostare almeno un metodo prima di ottenere l\\'accesso.',\n    'mfa_verify_use_totp' => 'Verifica utilizzando un\\'app mobile',\n    'mfa_verify_use_backup_codes' => 'Verifica utilizzando un codice di backup',\n    'mfa_verify_backup_code' => 'Codice di backup',\n    'mfa_verify_backup_code_desc' => 'Inserisci uno dei tuoi rimanenti codici di backup qui sotto:',\n    'mfa_verify_backup_code_enter_here' => 'Inserisci qui il codice di backup',\n    'mfa_verify_totp_desc' => 'Inserisci il codice, generato tramite la tua app mobile, qui sotto:',\n    'mfa_setup_login_notification' => 'Metodo multi-fattore configurato, si prega di effettuare nuovamente il login utilizzando il metodo configurato.',\n];\n"
  },
  {
    "path": "lang/it/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Annulla',\n    'close' => 'Chiudi',\n    'confirm' => 'Conferma',\n    'back' => 'Indietro',\n    'save' => 'Salva',\n    'continue' => 'Continua',\n    'select' => 'Seleziona',\n    'toggle_all' => 'Attiva/disattiva tutto',\n    'more' => 'Altro',\n\n    // Form Labels\n    'name' => 'Nome',\n    'description' => 'Descrizione',\n    'role' => 'Ruolo',\n    'cover_image' => 'Immagine di copertina',\n    'cover_image_description' => 'L\\'immagine dovrebbe essere di circa 440x250px, anche se verrà ridimensionata e ritagliata in modo flessibile per adattarsi all\\'interfaccia utente in diversi scenari, quindi le dimensioni effettive per la visualizzazione saranno diverse.',\n\n    // Actions\n    'actions' => 'Azioni',\n    'view' => 'Visualizza',\n    'view_all' => 'Vedi tutto',\n    'new' => 'Nuovo',\n    'create' => 'Crea',\n    'update' => 'Aggiorna',\n    'edit' => 'Modifica',\n    'archive' => 'Archivia',\n    'unarchive' => 'Ripristina',\n    'sort' => 'Ordina',\n    'move' => 'Sposta',\n    'copy' => 'Copia',\n    'reply' => 'Rispondi',\n    'delete' => 'Elimina',\n    'delete_confirm' => 'Conferma eliminazione',\n    'search' => 'Cerca',\n    'search_clear' => 'Pulisci ricerca',\n    'reset' => 'Azzera',\n    'remove' => 'Rimuovi',\n    'add' => 'Aggiungi',\n    'configure' => 'Configura',\n    'manage' => 'Gestisci',\n    'fullscreen' => 'Schermo intero',\n    'favourite' => 'Aggiungi ai preferiti',\n    'unfavourite' => 'Rimuovi dai preferiti',\n    'next' => 'Successivo',\n    'previous' => 'Precedente',\n    'filter_active' => 'Filtro attivo:',\n    'filter_clear' => 'Pulisci filtro',\n    'download' => 'Scarica',\n    'open_in_tab' => 'Apri nella scheda',\n    'open' => 'Apri',\n\n    // Sort Options\n    'sort_options' => 'Opzioni ordinamento',\n    'sort_direction_toggle' => 'Inverti direzione ordinamento',\n    'sort_ascending' => 'Ordine crescente',\n    'sort_descending' => 'Ordine decrescente',\n    'sort_name' => 'Nome',\n    'sort_default' => 'Predefinito',\n    'sort_created_at' => 'Data creazione',\n    'sort_updated_at' => 'Data aggiornamento',\n\n    // Misc\n    'deleted_user' => 'Utente eliminato',\n    'no_activity' => 'Nessuna attività da mostrare',\n    'no_items' => 'Nessun elemento disponibile',\n    'back_to_top' => 'Torna in alto',\n    'skip_to_main_content' => 'Passa al contenuto principale',\n    'toggle_details' => 'Mostra dettagli',\n    'toggle_thumbnails' => 'Mostra miniature',\n    'details' => 'Dettagli',\n    'grid_view' => 'Visualizzazione griglia',\n    'list_view' => 'Visualizzazione lista',\n    'default' => 'Predefinito',\n    'breadcrumb' => 'Navigazione',\n    'status' => 'Stato',\n    'status_active' => 'Attivo',\n    'status_inactive' => 'Inattivo',\n    'never' => 'Mai',\n    'none' => 'Nessuno',\n\n    // Header\n    'homepage' => 'Homepage',\n    'header_menu_expand' => 'Espandi menù intestazione',\n    'profile_menu' => 'Menù del profilo',\n    'view_profile' => 'Visualizza profilo',\n    'edit_profile' => 'Modifica profilo',\n    'dark_mode' => 'Modalità scura',\n    'light_mode' => 'Modalità chiara',\n    'global_search' => 'Ricerca globale',\n\n    // Layout tabs\n    'tab_info' => 'Informazioni',\n    'tab_info_label' => 'Scheda: Mostra informazioni secondarie',\n    'tab_content' => 'Contenuto',\n    'tab_content_label' => 'Scheda: Mostra contenuto principale',\n\n    // Email Content\n    'email_action_help' => 'Se hai problemi nel cliccare il pulsante \":actionText\", copia e incolla l\\'URL qui sotto nel tuo browser:',\n    'email_rights' => 'Tutti i diritti riservati',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Norme sulla privacy',\n    'terms_of_service' => 'Condizioni del servizio',\n\n    // OpenSearch\n    'opensearch_description' => 'Cerca :appName',\n];\n"
  },
  {
    "path": "lang/it/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Seleziona un\\'immagine',\n    'image_list' => 'Elenco immagini',\n    'image_details' => 'Dettagli immagine',\n    'image_upload' => 'Carica immagine',\n    'image_intro' => 'Qui è possibile selezionare e gestire le immagini che sono state precedentemente caricate nel sistema.',\n    'image_intro_upload' => 'Carica una nuova immagine trascinando un file immagine in questa finestra oppure utilizzando il pulsante \"Carica immagine\" in alto.',\n    'image_all' => 'Tutte',\n    'image_all_title' => 'Visualizza tutte le immagini',\n    'image_book_title' => 'Visualizza immagini caricate in questo libro',\n    'image_page_title' => 'Visualizza immagini caricate in questa pagina',\n    'image_search_hint' => 'Cerca immagine per nome',\n    'image_uploaded' => 'Caricato :uploadedDate',\n    'image_uploaded_by' => 'Caricato da :userName',\n    'image_uploaded_to' => 'Caricato su :pageLink',\n    'image_updated' => 'Aggiornato il :updateDate',\n    'image_load_more' => 'Carica altre',\n    'image_image_name' => 'Nome immagine',\n    'image_delete_used' => 'Questa immagine è usata nelle pagine elencate.',\n    'image_delete_confirm_text' => 'Sei sicuro di voler eliminare questa immagine?',\n    'image_select_image' => 'Seleziona immagine',\n    'image_dropzone' => 'Rilascia immagini o clicca qui per caricarle',\n    'image_dropzone_drop' => 'Trascina qui le immagini da caricare',\n    'images_deleted' => 'Immagini eliminate',\n    'image_preview' => 'Anteprima immagine',\n    'image_upload_success' => 'Immagine caricata correttamente',\n    'image_update_success' => 'Dettagli immagine aggiornati correttamente',\n    'image_delete_success' => 'Immagine eliminata correttamente',\n    'image_replace' => 'Sostituisci immagine',\n    'image_replace_success' => 'File immagine aggiornato con successo',\n    'image_rebuild_thumbs' => 'Rigenera variazioni dimensione',\n    'image_rebuild_thumbs_success' => 'Variazioni di dimensione immagine ricostruite con successo!',\n\n    // Code Editor\n    'code_editor' => 'Modifica codice',\n    'code_language' => 'Linguaggio codice',\n    'code_content' => 'Contenuto codice',\n    'code_session_history' => 'Cronologia sessione',\n    'code_save' => 'Salva codice',\n];\n"
  },
  {
    "path": "lang/it/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Generale',\n    'advanced' => 'Avanzate',\n    'none' => 'Nessuno',\n    'cancel' => 'Annulla',\n    'save' => 'Salva',\n    'close' => 'Chiudi',\n    'apply' => 'Applica',\n    'undo' => 'Annulla',\n    'redo' => 'Ripeti',\n    'left' => 'Sinistra',\n    'center' => 'Centro',\n    'right' => 'Destra',\n    'top' => 'Alto',\n    'middle' => 'Centrale',\n    'bottom' => 'Basso',\n    'width' => 'Larghezza',\n    'height' => 'Altezza',\n    'More' => 'Altro',\n    'select' => 'Seleziona...',\n\n    // Toolbar\n    'formats' => 'Formati',\n    'header_large' => 'Intestazione grande',\n    'header_medium' => 'Intestazione media',\n    'header_small' => 'Intestazione piccola',\n    'header_tiny' => 'Intestazione minuscola',\n    'paragraph' => 'Paragrafo',\n    'blockquote' => 'Virgolettato',\n    'inline_code' => 'Codice in linea',\n    'callouts' => 'Didascalie',\n    'callout_information' => 'Informazione',\n    'callout_success' => 'Fatto',\n    'callout_warning' => 'Avviso',\n    'callout_danger' => 'Pericolo',\n    'bold' => 'Grassetto',\n    'italic' => 'Corsivo',\n    'underline' => 'Sottolineato',\n    'strikethrough' => 'Barrato',\n    'superscript' => 'Apice',\n    'subscript' => 'Pedice',\n    'text_color' => 'Colore del testo',\n    'highlight_color' => 'Colore evidenziazione',\n    'custom_color' => 'Colore personalizzato',\n    'remove_color' => 'Rimuovi colore',\n    'background_color' => 'Colore di sfondo',\n    'align_left' => 'Allinea a sinistra',\n    'align_center' => 'Allinea al centro',\n    'align_right' => 'Allinea a destra',\n    'align_justify' => 'Giustifica',\n    'list_bullet' => 'Elenco puntato',\n    'list_numbered' => 'Elenco numerato',\n    'list_task' => 'Elenco attività',\n    'indent_increase' => 'Aumenta rientro',\n    'indent_decrease' => 'Riduci rientro',\n    'table' => 'Tabella',\n    'insert_image' => 'Inserisci immagine',\n    'insert_image_title' => 'Inserisci/modifica immagine',\n    'insert_link' => 'Inserisci/modifica collegamento',\n    'insert_link_title' => 'Inserisci/modifica collegamento',\n    'insert_horizontal_line' => 'Inserisci riga orizzontale',\n    'insert_code_block' => 'Inserisci blocco di codice',\n    'edit_code_block' => 'Modifica blocco di codice',\n    'insert_drawing' => 'Inserisci/modifica disegno',\n    'drawing_manager' => 'Gestore disegni',\n    'insert_media' => 'Inserisci/modifica media',\n    'insert_media_title' => 'Inserisci/modifica media',\n    'clear_formatting' => 'Cancella formattazione',\n    'source_code' => 'Codice sorgente',\n    'source_code_title' => 'Codice sorgente',\n    'fullscreen' => 'Schermo intero',\n    'image_options' => 'Opzioni immagine',\n\n    // Tables\n    'table_properties' => 'Proprietà della tabella',\n    'table_properties_title' => 'Proprietà della tabella',\n    'delete_table' => 'Elimina tabella',\n    'table_clear_formatting' => 'Cancella formattazione tabella',\n    'resize_to_contents' => 'Ridimensiona al contenuto',\n    'row_header' => 'Intestazione riga',\n    'insert_row_before' => 'Inserisci riga sopra',\n    'insert_row_after' => 'Inserisci riga sotto',\n    'delete_row' => 'Elimina riga',\n    'insert_column_before' => 'Inserisci colonna prima',\n    'insert_column_after' => 'Inserisci colonna dopo',\n    'delete_column' => 'Elimina colonna',\n    'table_cell' => 'Cella',\n    'table_row' => 'Riga',\n    'table_column' => 'Colonna',\n    'cell_properties' => 'Proprietà cella',\n    'cell_properties_title' => 'Proprietà cella',\n    'cell_type' => 'Tipo di cella',\n    'cell_type_cell' => 'Cella',\n    'cell_scope' => 'Ambito',\n    'cell_type_header' => 'Cella intestazione',\n    'merge_cells' => 'Unisci celle',\n    'split_cell' => 'Dividi cella',\n    'table_row_group' => 'Gruppo riga',\n    'table_column_group' => 'Gruppo colonna',\n    'horizontal_align' => 'Allineamento orizzontale',\n    'vertical_align' => 'Allineamento verticale',\n    'border_width' => 'Spessore bordo',\n    'border_style' => 'Stile bordo',\n    'border_color' => 'Colore bordo',\n    'row_properties' => 'Proprietà riga',\n    'row_properties_title' => 'Proprietà riga',\n    'cut_row' => 'Taglia riga',\n    'copy_row' => 'Copia riga',\n    'paste_row_before' => 'Incolla riga prima',\n    'paste_row_after' => 'Incolla riga dopo',\n    'row_type' => 'Tipo riga',\n    'row_type_header' => 'Intestazione',\n    'row_type_body' => 'Corpo',\n    'row_type_footer' => 'Piè di pagina',\n    'alignment' => 'Allineamento',\n    'cut_column' => 'Taglia colonna',\n    'copy_column' => 'Copia colonna',\n    'paste_column_before' => 'Incolla colonna prima',\n    'paste_column_after' => 'Incolla colonna dopo',\n    'cell_padding' => 'Padding cella',\n    'cell_spacing' => 'Spaziatura cella',\n    'caption' => 'Didascalia',\n    'show_caption' => 'Mostra didascalia',\n    'constrain' => 'Mantieni proporzioni',\n    'cell_border_solid' => 'Pieno',\n    'cell_border_dotted' => 'Punteggiato',\n    'cell_border_dashed' => 'Tratteggiato',\n    'cell_border_double' => 'Doppio',\n    'cell_border_groove' => 'Bordo incassato',\n    'cell_border_ridge' => 'In rilievo',\n    'cell_border_inset' => 'Sfondo Incassato',\n    'cell_border_outset' => 'Sfondo in rilievo',\n    'cell_border_none' => 'Nessuno',\n    'cell_border_hidden' => 'Nascosto',\n\n    // Images, links, details/summary & embed\n    'source' => 'Sorgente',\n    'alt_desc' => 'Descrizione alternativa',\n    'embed' => 'Incorpora',\n    'paste_embed' => 'Incolla il tuo codice incorporato qui sotto:',\n    'url' => 'URL',\n    'text_to_display' => 'Testo da visualizzare',\n    'title' => 'Titolo',\n    'browse_links' => 'Sfoglia link',\n    'open_link' => 'Apri collegamento',\n    'open_link_in' => 'Apri collegamento in...',\n    'open_link_current' => 'Finestra corrente',\n    'open_link_new' => 'Nuova finestra',\n    'remove_link' => 'Rimuovi collegamento',\n    'insert_collapsible' => 'Inserisci blocco collassabile',\n    'collapsible_unwrap' => 'Espandi',\n    'edit_label' => 'Modifica etichetta',\n    'toggle_open_closed' => 'Espandi/comprimi',\n    'collapsible_edit' => 'Modifica blocco collassabile',\n    'toggle_label' => 'Attiva/disattiva etichetta',\n\n    // About view\n    'about' => 'Informazioni sull\\'editor',\n    'about_title' => 'Informazioni sull\\'editor di WYSIWYG',\n    'editor_license' => 'Licenza e copyright dell\\'editor',\n    'editor_lexical_license' => 'Questo editor è stato creato come fork di :lexicalLink ed è distribuito sotto la licenza MIT.',\n    'editor_lexical_license_link' => 'I dettagli della licenza sono disponibili qui.',\n    'editor_tiny_license' => 'Questo editor è realizzato usando :tinyLink che è fornito sotto la licenza MIT.',\n    'editor_tiny_license_link' => 'I dettagli del copyright e della licenza di TinyMCE sono disponibili qui.',\n    'save_continue' => 'Salva pagina e continua',\n    'callouts_cycle' => '(Continua a premere per passare da un tipo all\\'altro)',\n    'link_selector' => 'Link al contenuto',\n    'shortcuts' => 'Scorciatoie',\n    'shortcut' => 'Scorciatoia',\n    'shortcuts_intro' => 'Le seguenti scorciatoie sono disponibili nell\\'editor:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Descrizione',\n];\n"
  },
  {
    "path": "lang/it/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Creati di recente',\n    'recently_created_pages' => 'Pagine create di recente',\n    'recently_updated_pages' => 'Pagine aggiornate di recente',\n    'recently_created_chapters' => 'Capitoli creati di recente',\n    'recently_created_books' => 'Libri creati di recente',\n    'recently_created_shelves' => 'Librerie create di recente',\n    'recently_update' => 'Aggiornati di recente',\n    'recently_viewed' => 'Visti di recente',\n    'recent_activity' => 'Attività recente',\n    'create_now' => 'Creane uno ora',\n    'revisions' => 'Revisioni',\n    'meta_revision' => 'Revisione #:revisionCount',\n    'meta_created' => 'Creato :timeLength',\n    'meta_created_name' => 'Creato :timeLength da :user',\n    'meta_updated' => 'Aggiornato :timeLength',\n    'meta_updated_name' => 'Aggiornato :timeLength da :user',\n    'meta_owned_name' => 'Creati da :user',\n    'meta_reference_count' => 'Referenziato da :count item|Referenziato da :count items',\n    'entity_select' => 'Seleziona l\\'entità',\n    'entity_select_lack_permission' => 'Non hai i permessi necessari per selezionare questo elemento',\n    'images' => 'Immagini',\n    'my_recent_drafts' => 'Bozze recenti',\n    'my_recently_viewed' => 'Visti di recente',\n    'my_most_viewed_favourites' => 'I miei preferiti più visti',\n    'my_favourites' => 'I miei preferiti',\n    'no_pages_viewed' => 'Non hai visto nessuna pagina',\n    'no_pages_recently_created' => 'Nessuna pagina creata di recente',\n    'no_pages_recently_updated' => 'Nessuna pagina aggiornata di recente',\n    'export' => 'Esporta',\n    'export_html' => 'File contenuto web',\n    'export_pdf' => 'File PDF',\n    'export_text' => 'File di testo',\n    'export_md' => 'File Markdown',\n    'export_zip' => 'ZIP Portatile',\n    'default_template' => 'Modello di pagina predefinito',\n    'default_template_explain' => 'Assegna un modello di pagina che sarà usato come contenuto predefinito per tutte le pagine create in questo elemento. Tieni presente che potrà essere utilizzato solo se il creatore della pagina ha accesso alla pagina del modello scelto.',\n    'default_template_select' => 'Seleziona una pagina modello',\n    'import' => 'Importa',\n    'import_validate' => 'Convalida Importazione',\n    'import_desc' => 'Importa libri, capitoli e pagine utilizzando un\\'esportazione zip portatile dalla stessa istanza o da un\\'istanza diversa. Selezionare un file ZIP per procedere. Dopo che il file è stato caricato e convalidato, sarà possibile configurare e confermare l\\'importazione nella vista successiva.',\n    'import_zip_select' => 'Seleziona il file ZIP da caricare',\n    'import_zip_validation_errors' => 'Sono stati rilevati errori durante la convalida del file ZIP fornito:',\n    'import_pending' => 'Importazioni In Attesa',\n    'import_pending_none' => 'Non sono state avviate importazioni.',\n    'import_continue' => 'Continua l\\'importazione',\n    'import_continue_desc' => 'Esaminare il contenuto da importare dal file ZIP caricato. Quando è pronto, eseguire l\\'importazione per aggiungere i contenuti al sistema. Il file di importazione ZIP caricato verrà automaticamente rimosso quando l\\'importazione sarà riuscita.',\n    'import_details' => 'Dettagli dell\\'importazione',\n    'import_run' => 'Esegui Importazione',\n    'import_size' => 'Dimensione dello ZIP importato :size',\n    'import_uploaded_at' => 'Caricato il :relativeTime',\n    'import_uploaded_by' => 'Caricata da',\n    'import_location' => 'Posizione Di Importazione',\n    'import_location_desc' => 'Selezionare una posizione di destinazione per i contenuti importati. È necessario disporre delle autorizzazioni adeguate per creare all\\'interno della posizione scelta.',\n    'import_delete_confirm' => 'Sei sicuro di voler eliminare questa importazione?',\n    'import_delete_desc' => 'Questa operazione cancella il file ZIP di importazione caricato e non può essere annullata.',\n    'import_errors' => 'Errori di importazione',\n    'import_errors_desc' => 'Gli seguenti errori si sono verificati durante il tentativo di importazione:',\n    'breadcrumb_siblings_for_page' => 'Naviga tra le pagine correlate',\n    'breadcrumb_siblings_for_chapter' => 'Naviga tra i capitoli correlati',\n    'breadcrumb_siblings_for_book' => 'Naviga tra i libri correlati',\n    'breadcrumb_siblings_for_bookshelf' => 'Naviga tra le librerie correlate',\n\n    // Permissions and restrictions\n    'permissions' => 'Permessi',\n    'permissions_desc' => 'Imposta qui i permessi per sovrascrivere i permessi predefiniti forniti dai ruoli utente.',\n    'permissions_book_cascade' => 'I permessi impostati sui libri si trasmettono automaticamente a cascata ai capitoli e alle pagine figli, a meno che non siano stati definiti permessi propri.',\n    'permissions_chapter_cascade' => 'I permessi impostati sui capitoli si trasmettono automaticamente a cascata alle pagine figlie, a meno che non siano stati definiti permessi propri.',\n    'permissions_save' => 'Salva permessi',\n    'permissions_owner' => 'Proprietario',\n    'permissions_role_everyone_else' => 'Tutti gli altri',\n    'permissions_role_everyone_else_desc' => 'Imposta i permessi per tutti i ruoli non specificamente sovrascritti.',\n    'permissions_role_override' => 'Sovrascrivere i permessi per il ruolo',\n    'permissions_inherit_defaults' => 'Eredita predefinite',\n\n    // Search\n    'search_results' => 'Risultati della ricerca',\n    'search_total_results_found' => ':count risultato trovato|:count risultati trovati',\n    'search_clear' => 'Pulisci ricerca',\n    'search_no_pages' => 'Nessuna pagina corrisponde alla ricerca',\n    'search_for_term' => 'Ricerca per :term',\n    'search_more' => 'Altri risultati',\n    'search_advanced' => 'Ricerca avanzata',\n    'search_terms' => 'Termini di ricerca',\n    'search_content_type' => 'Tipo di contenuto',\n    'search_exact_matches' => 'Corrispondenza esatta',\n    'search_tags' => 'Ricerche per tag',\n    'search_options' => 'Opzioni',\n    'search_viewed_by_me' => 'Visti da me',\n    'search_not_viewed_by_me' => 'Non visti da me',\n    'search_permissions_set' => 'Permessi impostati',\n    'search_created_by_me' => 'Creati da me',\n    'search_updated_by_me' => 'Aggiornati da me',\n    'search_owned_by_me' => 'Creati da me',\n    'search_date_options' => 'Opzioni data',\n    'search_updated_before' => 'Aggiornati prima del',\n    'search_updated_after' => 'Aggiornati dopo il',\n    'search_created_before' => 'Creati prima del',\n    'search_created_after' => 'Creati dopo il',\n    'search_set_date' => 'Imposta data',\n    'search_update' => 'Aggiorna ricerca',\n\n    // Shelves\n    'shelf' => 'Libreria',\n    'shelves' => 'Librerie',\n    'x_shelves' => ':count libreria|:count librerie',\n    'shelves_empty' => 'Nessuna libreria creata',\n    'shelves_create' => 'Crea nuova libreria',\n    'shelves_popular' => 'Librerie popolari',\n    'shelves_new' => 'Nuove librerie',\n    'shelves_new_action' => 'Nuova libreria',\n    'shelves_popular_empty' => 'Le librerie più popolari appariranno qui.',\n    'shelves_new_empty' => 'Le librerie create più di recente appariranno qui.',\n    'shelves_save' => 'Salva libreria',\n    'shelves_books' => 'Libri in questa libreria',\n    'shelves_add_books' => 'Aggiungi libri a questa libreria',\n    'shelves_drag_books' => 'Trascina i libri qui sotto per aggiungerli a questa libreria',\n    'shelves_empty_contents' => 'Questa libreria non ha libri assegnati',\n    'shelves_edit_and_assign' => 'Modifica la libreria per assegnare i libri',\n    'shelves_edit_named' => 'Modifica libreria :name',\n    'shelves_edit' => 'Modifica libreria',\n    'shelves_delete' => 'Elimina libreria',\n    'shelves_delete_named' => 'Elimina libreria :name',\n    'shelves_delete_explain' => \"La libreria ':name' verrà eliminata. I libri al suo interno non verranno eliminati.\",\n    'shelves_delete_confirmation' => 'Sei sicuro di voler eliminare questa libreria?',\n    'shelves_permissions' => 'Permessi libreria',\n    'shelves_permissions_updated' => 'Permessi libreria aggiornati',\n    'shelves_permissions_active' => 'Permessi libreria attivi',\n    'shelves_permissions_cascade_warning' => 'I permessi delle librerie non si estendono automaticamente ai libri contenuti. Questo perché un libro può essere presente su più scaffali. I permessi possono comunque essere copiati ai libri al suo interno usando l\\'opzione sottostante.',\n    'shelves_permissions_create' => 'Le autorizzazioni per la creazione di librerie sono utilizzate solo per copiare le autorizzazioni ai libri figli utilizzando l\\'azione sottostante. Non controllano la capacità di creare libri.',\n    'shelves_copy_permissions_to_books' => 'Copia permessi ai libri',\n    'shelves_copy_permissions' => 'Copia permessi',\n    'shelves_copy_permissions_explain' => 'Verranno applicati tutti i permessi della libreria ai libri al suo interno. Prima dell\\'attivazione, assicurati di aver salvato le modifiche ai permessi di questa libreria.',\n    'shelves_copy_permission_success' => 'Permessi della libreria copiati in :count libri',\n\n    // Books\n    'book' => 'Libro',\n    'books' => 'Libri',\n    'x_books' => ':count libro|:count libri',\n    'books_empty' => 'Nessun libro creato',\n    'books_popular' => 'Libri popolari',\n    'books_recent' => 'Libri recenti',\n    'books_new' => 'Nuovi libri',\n    'books_new_action' => 'Nuovo libro',\n    'books_popular_empty' => 'I libri più popolari appariranno qui.',\n    'books_new_empty' => 'I libri creati di recente appariranno qui.',\n    'books_create' => 'Crea nuovo libro',\n    'books_delete' => 'Elimina libro',\n    'books_delete_named' => 'Elimina il libro :bookName',\n    'books_delete_explain' => 'Questo eliminerà il libro di nome \\':bookName\\'. Tutte le pagine e i capitoli saranno rimossi.',\n    'books_delete_confirmation' => 'Sei sicuro di voler eliminare questo libro?',\n    'books_edit' => 'Modifica libro',\n    'books_edit_named' => 'Modifica il libro :bookName',\n    'books_form_book_name' => 'Nome libro',\n    'books_save' => 'Salva libro',\n    'books_permissions' => 'Permessi libro',\n    'books_permissions_updated' => 'Permessi del libro aggiornati',\n    'books_empty_contents' => 'Non ci sono pagine o capitoli per questo libro.',\n    'books_empty_create_page' => 'Crea una nuova pagina',\n    'books_empty_sort_current_book' => 'Ordina il libro corrente',\n    'books_empty_add_chapter' => 'Aggiungi un capitolo',\n    'books_permissions_active' => 'Permessi libro attivi',\n    'books_search_this' => 'Cerca in questo libro',\n    'books_navigation' => 'Navigazione libro',\n    'books_sort' => 'Ordina il contenuto del libro',\n    'books_sort_desc' => 'Spostare i capitoli e le pagine di un libro per riorganizzarne il contenuto. Possono essere aggiunti altri libri che permettono di spostare facilmente capitoli e pagine tra i libri. Opzionalmente una regola di ordinamento automatico può essere impostata per ordinare automaticamente i contenuti di questo libro in caso di modifiche.',\n    'books_sort_auto_sort' => 'Opzione Ordinamento Automatico',\n    'books_sort_auto_sort_active' => 'Ordinamento Automatico Attivo: :sortName',\n    'books_sort_named' => 'Ordina il libro :bookName',\n    'books_sort_name' => 'Ordina per Nome',\n    'books_sort_created' => 'Ordina per Data di creazione',\n    'books_sort_updated' => 'Ordina per Data di aggiornamento',\n    'books_sort_chapters_first' => 'Capitoli per primi',\n    'books_sort_chapters_last' => 'Capitoli per ultimi',\n    'books_sort_show_other' => 'Mostra altri libri',\n    'books_sort_save' => 'Salva il nuovo ordine',\n    'books_sort_show_other_desc' => 'Aggiungi qui altri libri per includerli nell\\'operazione di ordinamento e consentire una facile riorganizzazione incrociata dei libri.',\n    'books_sort_move_up' => 'Muovi su',\n    'books_sort_move_down' => 'Muovi giù',\n    'books_sort_move_prev_book' => 'Passa al libro precedente',\n    'books_sort_move_next_book' => 'Passa al libro successivo',\n    'books_sort_move_prev_chapter' => 'Passa al capitolo precedente',\n    'books_sort_move_next_chapter' => 'Passa al capitolo successivo',\n    'books_sort_move_book_start' => 'Passa all\\'inizio del libro',\n    'books_sort_move_book_end' => 'Passa alla fine del libro',\n    'books_sort_move_before_chapter' => 'Passa al capitolo precedente',\n    'books_sort_move_after_chapter' => 'Passa al capitolo successivo',\n    'books_copy' => 'Copia libro',\n    'books_copy_success' => 'Libro copiato con successo',\n\n    // Chapters\n    'chapter' => 'Capitolo',\n    'chapters' => 'Capitoli',\n    'x_chapters' => ':count capitolo|:count capitoli',\n    'chapters_popular' => 'Capitoli popolari',\n    'chapters_new' => 'Nuovo capitolo',\n    'chapters_create' => 'Crea un nuovo capitolo',\n    'chapters_delete' => 'Elimina capitolo',\n    'chapters_delete_named' => 'Elimina il capitolo :chapterName',\n    'chapters_delete_explain' => 'Verrà eliminato il capitolo denominato \\':chapterName\\'. Saranno eliminate anche le pagine all\\'interno.',\n    'chapters_delete_confirm' => 'Sei sicuro di voler eliminare questo capitolo?',\n    'chapters_edit' => 'Elimina capitolo',\n    'chapters_edit_named' => 'Modifica il capitolo :chapterName',\n    'chapters_save' => 'Salva capitolo',\n    'chapters_move' => 'Sposta capitolo',\n    'chapters_move_named' => 'Sposta il capitolo :chapterName',\n    'chapters_copy' => 'Copia capitolo',\n    'chapters_copy_success' => 'Capitolo copiato con successo',\n    'chapters_permissions' => 'Permessi capitolo',\n    'chapters_empty' => 'Non ci sono pagine in questo capitolo.',\n    'chapters_permissions_active' => 'Permessi capitolo attivi',\n    'chapters_permissions_success' => 'Permessi capitolo aggiornati',\n    'chapters_search_this' => 'Cerca in questo capitolo',\n    'chapter_sort_book' => 'Ordina libro',\n\n    // Pages\n    'page' => 'Pagina',\n    'pages' => 'Pagine',\n    'x_pages' => ':count pagina|:count pagine',\n    'pages_popular' => 'Pagine popolari',\n    'pages_new' => 'Nuova pagina',\n    'pages_attachments' => 'Allegati',\n    'pages_navigation' => 'Navigazione pagine',\n    'pages_delete' => 'Elimina pagina',\n    'pages_delete_named' => 'Elimina la pagina :pageName',\n    'pages_delete_draft_named' => 'Elimina bozza della pagina :pageName',\n    'pages_delete_draft' => 'Elimina bozza pagina',\n    'pages_delete_success' => 'Pagina eliminata',\n    'pages_delete_draft_success' => 'Bozza di una pagina eliminata',\n    'pages_delete_warning_template' => 'Questa pagina è in uso come modello di pagina predefinito del libro o del capitolo. Questi libri o capitoli non avranno più un modello di pagina predefinito assegnato dopo che questa pagina sarà eliminata.',\n    'pages_delete_confirm' => 'Sei sicuro di voler eliminare questa pagina?',\n    'pages_delete_draft_confirm' => 'Sei sicuro di voler eliminare la bozza di questa pagina?',\n    'pages_editing_named' => 'Modifica :pageName',\n    'pages_edit_draft_options' => 'Opzioni bozza',\n    'pages_edit_save_draft' => 'Salva bozza',\n    'pages_edit_draft' => 'Modifica bozza della pagina',\n    'pages_editing_draft' => 'Modifica bozza',\n    'pages_editing_page' => 'Modifica pagina',\n    'pages_edit_draft_save_at' => 'Bozza salvata alle ',\n    'pages_edit_delete_draft' => 'Elimina bozza',\n    'pages_edit_delete_draft_confirm' => 'Si è sicuri di voler eliminare le modifiche alla pagina bozza? Tutte le modifiche apportate dall\\'ultimo salvataggio completo andranno perse e l\\'editor verrà aggiornato con l\\'ultimo stato di salvataggio della pagina non in bozza.',\n    'pages_edit_discard_draft' => 'Scarta bozza',\n    'pages_edit_switch_to_markdown' => 'Passa all\\'editor Markdown',\n    'pages_edit_switch_to_markdown_clean' => '(Contenuto Chiaro)',\n    'pages_edit_switch_to_markdown_stable' => '(Contenuto stabile)',\n    'pages_edit_switch_to_wysiwyg' => 'Passa all\\'editor WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg' => 'Passa al nuovo WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Test)',\n    'pages_edit_set_changelog' => 'Imposta changelog',\n    'pages_edit_enter_changelog_desc' => 'Inserisci una breve descrizione dei cambiamenti che hai apportato',\n    'pages_edit_enter_changelog' => 'Inserisci changelog',\n    'pages_editor_switch_title' => 'Cambia editor',\n    'pages_editor_switch_are_you_sure' => 'Sei sicuro di voler cambiare l\\'editor di questa pagina?',\n    'pages_editor_switch_consider_following' => 'Considera quanto segue quando si cambia editor:',\n    'pages_editor_switch_consideration_a' => 'Una volta salvata, la nuova opzione di editor sarà utilizzata da chi modificherà in futuro, inclusi quelli che potrebbero non essere in grado di cambiare il tipo di editor da soli.',\n    'pages_editor_switch_consideration_b' => 'Ciò può potenzialmente portare a una perdita di dettagli e sintassi in determinate circostanze.',\n    'pages_editor_switch_consideration_c' => 'Le modifiche ai tag o al changelog, fatte dall\\'ultimo salvataggio, non persisteranno in questa modifica.',\n    'pages_save' => 'Salva pagina',\n    'pages_title' => 'Titolo pagina',\n    'pages_name' => 'Nome pagina',\n    'pages_md_editor' => 'Editor',\n    'pages_md_preview' => 'Anteprima',\n    'pages_md_insert_image' => 'Inserisci immagine',\n    'pages_md_insert_link' => 'Inserisci collegamento entità',\n    'pages_md_insert_drawing' => 'Inserisci disegno',\n    'pages_md_show_preview' => 'Visualizza anteprima',\n    'pages_md_sync_scroll' => 'Sincronizza scorrimento anteprima',\n    'pages_md_plain_editor' => 'Editor di testo semplice',\n    'pages_drawing_unsaved' => 'Trovato disegno non salvato',\n    'pages_drawing_unsaved_confirm' => 'Sono stati trovati i dati di un disegno non salvato da un precedente tentativo di salvataggio di disegno non riuscito. Ripristinare e continuare a modificare questo disegno non salvato?',\n    'pages_not_in_chapter' => 'La pagina non è in un capitolo',\n    'pages_move' => 'Sposta pagina',\n    'pages_copy' => 'Copia pagina',\n    'pages_copy_desination' => 'Copia destinazione',\n    'pages_copy_success' => 'Pagina copiata correttamente',\n    'pages_permissions' => 'Permessi pagina',\n    'pages_permissions_success' => 'Permessi pagina aggiornati',\n    'pages_revision' => 'Versione',\n    'pages_revisions' => 'Revisioni pagina',\n    'pages_revisions_desc' => 'Di seguito sono elencate tutte le revisioni precedenti di questa pagina. È possibile consultare, confrontare e ripristinare le vecchie versioni della pagina, se hai i permessi. La cronologia completa della pagina potrebbe non essere riportata qui, poiché a seconda della configurazione del sistema le vecchie revisioni potrebbero essere cancellate automaticamente.',\n    'pages_revisions_named' => 'Revisioni della pagina :pageName',\n    'pages_revision_named' => 'Revisione della pagina :pageName',\n    'pages_revision_restored_from' => 'Ripristinato da #:id; :summary',\n    'pages_revisions_created_by' => 'Creata da',\n    'pages_revisions_date' => 'Data versione',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Numero revisione',\n    'pages_revisions_numbered' => 'Revisione #:id',\n    'pages_revisions_numbered_changes' => 'Modifiche revisione #:id',\n    'pages_revisions_editor' => 'Tipo di editor',\n    'pages_revisions_changelog' => 'Changelog',\n    'pages_revisions_changes' => 'Cambiamenti',\n    'pages_revisions_current' => 'Versione corrente',\n    'pages_revisions_preview' => 'Anteprima',\n    'pages_revisions_restore' => 'Ripristina',\n    'pages_revisions_none' => 'Questa pagina non ha revisioni',\n    'pages_copy_link' => 'Copia collegamento',\n    'pages_edit_content_link' => 'Vai alla sezione nell\\'editor',\n    'pages_pointer_enter_mode' => 'Vai alla modalità di selezione della sezione',\n    'pages_pointer_label' => 'Opzioni sezione pagina',\n    'pages_pointer_permalink' => 'Sezione pagina Permalink',\n    'pages_pointer_include_tag' => 'Sezione pagina includi tag',\n    'pages_pointer_toggle_link' => 'Modalità Permalink, premi per mostrare includi tag',\n    'pages_pointer_toggle_include' => 'Modalità includi tag, premi per mostrare permalink',\n    'pages_permissions_active' => 'Permessi pagina attivi',\n    'pages_initial_revision' => 'Pubblicazione iniziale',\n    'pages_references_update_revision' => 'Aggiornamento automatico di sistema dei collegamenti interni',\n    'pages_initial_name' => 'Nuova pagina',\n    'pages_editing_draft_notification' => 'Stai modificando una bozza che è stata salvata il :timeDiff.',\n    'pages_draft_edited_notification' => 'Questa pagina è stata aggiornata. Si consiglia di scartare questa bozza.',\n    'pages_draft_page_changed_since_creation' => 'Questa pagina è stata aggiornata da quando è stata creata questa bozza. Si consiglia di scartare questa bozza o di fare attenzione a non sovrascrivere alcun cambiamento alla pagina.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count utenti hanno iniziato a modificare questa pagina',\n        'start_b' => ':userName ha iniziato a modificare questa pagina',\n        'time_a' => 'dall\\'ultimo aggiornamento della pagina',\n        'time_b' => 'negli ultimi :minCount minuti',\n        'message' => ':start :time. Assicurati di non sovrascrivere le modifiche degli altri!',\n    ],\n    'pages_draft_discarded' => 'Bozza scartata! L\\'editor è stato aggiornato con il contenuto della pagina corrente',\n    'pages_draft_deleted' => 'Bozza eliminata! L\\'editor è stato aggiornato con il contenuto della pagina corrente',\n    'pages_specific' => 'Pagina specifica',\n    'pages_is_template' => 'Modello pagina',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Attiva/disattiva barra laterale',\n    'page_tags' => 'Tag pagina',\n    'chapter_tags' => 'Tag capitolo',\n    'book_tags' => 'Tag libro',\n    'shelf_tags' => 'Tag libreria',\n    'tag' => 'Tag',\n    'tags' =>  'Tag',\n    'tags_index_desc' => 'I tag possono essere applicati ai contenuti del sistema per applicare una forma flessibile di categorizzazione. I tag possono avere una chiave e un valore, il valore è opzionale. Una volta applicati, i contenuti possono essere cercati utilizzando il nome e il valore del tag.',\n    'tag_name' =>  'Nome del tag',\n    'tag_value' => 'Valore tag (opzionale)',\n    'tags_explain' => \"Aggiungi tag per categorizzare meglio il contenuto. \\n Puoi assegnare un valore ai tag per una migliore organizzazione.\",\n    'tags_add' => 'Aggiungi un altro tag',\n    'tags_remove' => 'Rimuovi questo tag',\n    'tags_usages' => 'Utilizzi totali dei tag',\n    'tags_assigned_pages' => 'Assegnato alle pagine',\n    'tags_assigned_chapters' => 'Assegnato ai capitoli',\n    'tags_assigned_books' => 'Assegnato ai libri',\n    'tags_assigned_shelves' => 'Assegnato alle librerie',\n    'tags_x_unique_values' => ':count valori univoci',\n    'tags_all_values' => 'Tutti i valori',\n    'tags_view_tags' => 'Visualizza tag',\n    'tags_view_existing_tags' => 'Usa i tag esistenti',\n    'tags_list_empty_hint' => 'I tag possono essere assegnati tramite la barra laterale dell\\'editor di pagina o durante la modifica dei dettagli di un libro, di un capitolo o di una libreria.',\n    'attachments' => 'Allegati',\n    'attachments_explain' => 'Carica alcuni file o allega dei collegamenti per visualizzarli nella pagina. Questi sono visibili nella barra laterale della pagina.',\n    'attachments_explain_instant_save' => 'I cambiamenti qui sono salvati istantaneamente.',\n    'attachments_upload' => 'Carica file',\n    'attachments_link' => 'Allega collegamento',\n    'attachments_upload_drop' => 'In alternativa puoi trascinare un file qui per caricarlo come allegato.',\n    'attachments_set_link' => 'Imposta collegamento',\n    'attachments_delete' => 'Vuoi davvero eliminare questo allegato?',\n    'attachments_dropzone' => 'Trascina qui i file da caricare',\n    'attachments_no_files' => 'Nessun file è stato caricato',\n    'attachments_explain_link' => 'Puoi allegare un collegamento se preferisci non caricare un file. Questo può essere un collegamento a un\\'altra pagina o a un file nel cloud.',\n    'attachments_link_name' => 'Nome collegamento',\n    'attachment_link' => 'Collegamento allegato',\n    'attachments_link_url' => 'Collegamento al file',\n    'attachments_link_url_hint' => 'Url del sito o del file',\n    'attach' => 'Allega',\n    'attachments_insert_link' => 'Aggiungi allegato collegamento alla pagina',\n    'attachments_edit_file' => 'Modifica file',\n    'attachments_edit_file_name' => 'Nome file',\n    'attachments_edit_drop_upload' => 'Trascina file qui o clicca per caricare e sovrascrivere',\n    'attachments_order_updated' => 'Ordine allegato aggiornato',\n    'attachments_updated_success' => 'Dettagli allegato aggiornati',\n    'attachments_deleted' => 'Allegato eliminato',\n    'attachments_file_uploaded' => 'File caricato correttamente',\n    'attachments_file_updated' => 'File aggiornato correttamente',\n    'attachments_link_attached' => 'Collegamento allegato correttamente alla pagina',\n    'templates' => 'Modello',\n    'templates_set_as_template' => 'La pagina è un modello',\n    'templates_explain_set_as_template' => 'Puoi impostare questa pagina come modello in modo da utilizzare il suo contenuto quando si creano altre pagine. Gli altri utenti potranno utilizzare questo modello se avranno i permessi di visualizzazione per questa pagina.',\n    'templates_replace_content' => 'Rimpiazza contenuto della pagina',\n    'templates_append_content' => 'Appendi al contenuto della pagina',\n    'templates_prepend_content' => 'Anteponi al contenuto della pagina',\n\n    // Profile View\n    'profile_user_for_x' => 'Utente da :time',\n    'profile_created_content' => 'Contenuti creati',\n    'profile_not_created_pages' => ':userName non ha creato pagine',\n    'profile_not_created_chapters' => ':userName non ha creato capitoli',\n    'profile_not_created_books' => ':userName non ha creato libri',\n    'profile_not_created_shelves' => ':userName non ha creato librerie',\n\n    // Comments\n    'comment' => 'Commento',\n    'comments' => 'Commenti',\n    'comment_add' => 'Aggiungi commento',\n    'comment_none' => 'Nessun commento da visualizzare',\n    'comment_placeholder' => 'Scrivi un commento',\n    'comment_thread_count' => ':count Commento Thread| :count Commenti Threads',\n    'comment_archived_count' => ':count Archiviato',\n    'comment_archived_threads' => 'Discussioni Archiviate',\n    'comment_save' => 'Salva commento',\n    'comment_new' => 'Nuovo commento',\n    'comment_created' => 'ha commentato :createDiff',\n    'comment_updated' => 'Aggiornato :updateDiff da :username',\n    'comment_updated_indicator' => 'Aggiornato',\n    'comment_deleted_success' => 'Commento eliminato',\n    'comment_created_success' => 'Commento aggiunto',\n    'comment_updated_success' => 'Commento aggiornato',\n    'comment_archive_success' => 'Commento archiviato',\n    'comment_unarchive_success' => 'Commento ripristinato',\n    'comment_view' => 'Visualizza commento',\n    'comment_jump_to_thread' => 'Vai al thread',\n    'comment_delete_confirm' => 'Sei sicuro di voler eliminare questo commento?',\n    'comment_in_reply_to' => 'In risposta a :commentId',\n    'comment_reference' => 'Riferimento',\n    'comment_reference_outdated' => '(Obsoleto)',\n    'comment_editor_explain' => 'Ecco i commenti che sono stati lasciati in questa pagina. I commenti possono essere aggiunti e gestiti quando si visualizza la pagina salvata.',\n\n    // Revision\n    'revision_delete_confirm' => 'Sei sicuro di voler eliminare questa revisione?',\n    'revision_restore_confirm' => 'Sei sicuro di voler ripristinare questa revisione? Il contenuto della pagina verrà rimpiazzato.',\n    'revision_cannot_delete_latest' => 'Impossibile eliminare l\\'ultima revisione.',\n\n    // Copy view\n    'copy_consider' => 'Considera quanto segue quando copi il contenuto.',\n    'copy_consider_permissions' => 'Le impostazioni dei permessi personalizzati non saranno copiate.',\n    'copy_consider_owner' => 'Diventerai il proprietario di tutti i contenuti copiati.',\n    'copy_consider_images' => 'I file delle immagini delle pagine non saranno duplicati e le immagini originali manterranno la loro relazione con la pagina su cui sono state originariamente caricate.',\n    'copy_consider_attachments' => 'Gli allegati della pagina non saranno copiati.',\n    'copy_consider_access' => 'Un cambiamento di posizione, di proprietario o di autorizzazioni potrebbe rendere questo contenuto accessibile a chi prima non aveva accesso.',\n\n    // Conversions\n    'convert_to_shelf' => 'Converti in libreria',\n    'convert_to_shelf_contents_desc' => 'Puoi convertire questo libro in una nuova libreria con gli stessi contenuti. I capitoli contenuti in questo libro saranno convertiti in nuovi libri. Se il libro contiene pagine che non fanno parte di un capitolo, questo libro verrà rinominato e conterrà tali pagine e diventerà parte della nuova libreria.',\n    'convert_to_shelf_permissions_desc' => 'Tutti i permessi impostati su questo libro saranno copiati sulla nuova libreria e su tutti i nuovi libri figli che non hanno i loro permessi applicati. Nota che i permessi delle librerie non si trasmettono automaticamente ai contenuti al loro interno, come avviene per i libri.',\n    'convert_book' => 'Converti libro',\n    'convert_book_confirm' => 'Sei sicuro di voler convertire questo libro?',\n    'convert_undo_warning' => 'Questo non può essere annullato con la stessa facilità.',\n    'convert_to_book' => 'Converti in libro',\n    'convert_to_book_desc' => 'È possibile convertire questo capitolo in un nuovo libro con gli stessi contenuti. Tutti i permessi impostati su questo capitolo saranno copiati nel nuovo libro, ma i permessi ereditati dal libro principale non saranno copiati, il che potrebbe portare a una modifica del controllo degli accessi.',\n    'convert_chapter' => 'Converti capitolo',\n    'convert_chapter_confirm' => 'Sei sicuro di voler convertire questo capitolo?',\n\n    // References\n    'references' => 'Riferimenti',\n    'references_none' => 'Non ci sono riferimenti tracciati a questa voce.',\n    'references_to_desc' => 'Di seguito sono elencati tutti i contenuti noti del sistema che rimandano a questo elemento.',\n\n    // Watch Options\n    'watch' => 'Osserva',\n    'watch_title_default' => 'Preferenze predefinite',\n    'watch_desc_default' => 'Ripristina l\\'osservazione alle tue preferenze di notifica predefinite.',\n    'watch_title_ignore' => 'Ignora',\n    'watch_desc_ignore' => 'Ignora tutte le notifiche, incluse quelle dalle preferenze a livello utente.',\n    'watch_title_new' => 'Nuove pagine',\n    'watch_desc_new' => 'Notifica quando viene creata una nuova pagina all\\'interno di questo elemento.',\n    'watch_title_updates' => 'Tutti gli aggiornamenti della pagina',\n    'watch_desc_updates' => 'Notifica tutte le nuove pagine e le modifiche alle pagine.',\n    'watch_desc_updates_page' => 'Notifica tutte le modifiche alla pagine.',\n    'watch_title_comments' => 'Tutti gli aggiornamenti delle pagine e i commenti',\n    'watch_desc_comments' => 'Notifica tutte le nuove pagine, le modifiche alle pagine e i nuovi commenti.',\n    'watch_desc_comments_page' => 'Notifica le modifiche alla pagina e i nuovi commenti.',\n    'watch_change_default' => 'Modifica le preferenze di notifica predefinite',\n    'watch_detail_ignore' => 'Ignorare le notifiche',\n    'watch_detail_new' => 'Osservare le nuove pagine',\n    'watch_detail_updates' => 'Osservare le nuove pagine e gli aggiornamenti',\n    'watch_detail_comments' => 'Osservare le nuove pagine, aggiornamenti e commenti',\n    'watch_detail_parent_book' => 'Osservare tramite il libro che lo contiene',\n    'watch_detail_parent_book_ignore' => 'Ignorare tramite il libro che lo contiene',\n    'watch_detail_parent_chapter' => 'Osservare tramite il capitolo che lo contiene',\n    'watch_detail_parent_chapter_ignore' => 'Ignorato tramite il capitolo che lo contiene',\n];\n"
  },
  {
    "path": "lang/it/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Non hai il permesso di accedere alla pagina richiesta.',\n    'permissionJson' => 'Non hai il permesso di eseguire l\\'azione richiesta.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Un utente con la mail :email esiste già ma con credenziali differenti.',\n    'auth_pre_register_theme_prevention' => 'Non è stato possibile registrare l\\'account utente coi dettagli forniti',\n    'email_already_confirmed' => 'La mail è già stata confermata, esegui il login.',\n    'email_confirmation_invalid' => 'Questo token di conferma non è valido o già stato utilizzato, registrati nuovamente.',\n    'email_confirmation_expired' => 'Il token di conferma è scaduto, è stata inviata una nuova mail di conferma.',\n    'email_confirmation_awaiting' => 'L\\'indirizzo email per l\\'account in uso deve essere confermato',\n    'ldap_fail_anonymous' => 'Accesso LDAP fallito usando bind anonimo',\n    'ldap_fail_authed' => 'Accesso LDAP fallito usando il dn e la password inseriti',\n    'ldap_extension_not_installed' => 'L\\'estensione PHP LDAP non è installata',\n    'ldap_cannot_connect' => 'Impossibile connettersi al server ldap, connessione iniziale fallita',\n    'saml_already_logged_in' => 'Già loggato',\n    'saml_no_email_address' => 'Impossibile trovare un indirizzo email per questo utente nei dati forniti dal sistema di autenticazione esterno',\n    'saml_invalid_response_id' => 'La richiesta dal sistema di autenticazione esterno non è riconosciuta da un processo iniziato da questa applicazione. Tornare indietro dopo un login potrebbe causare questo problema.',\n    'saml_fail_authed' => 'Accesso con :system non riuscito, il sistema non ha fornito l\\'autorizzazione corretta',\n    'oidc_already_logged_in' => 'Hai già effettuato il login',\n    'oidc_no_email_address' => 'Impossibile trovare un indirizzo email, per questo utente, nei dati forniti dal sistema di autenticazione esterno',\n    'oidc_fail_authed' => 'Accesso con :system non riuscito, il sistema non ha fornito l\\'autorizzazione',\n    'social_no_action_defined' => 'Nessuna azione definita',\n    'social_login_bad_response' => \"Ricevuto errore durante il login con :socialAccount : \\n:error\",\n    'social_account_in_use' => 'Questo account :socialAccount è già utilizzato, prova a loggarti usando l\\'opzione :socialAccount.',\n    'social_account_email_in_use' => 'La mail :email è già in uso. Se hai già un account puoi connettere il tuo account :socialAccount dalle impostazioni del tuo profilo.',\n    'social_account_existing' => 'Questo account :socialAccount è già connesso al tuo profilo.',\n    'social_account_already_used_existing' => 'Questo account :socialAccount è già utilizzato da un altro utente.',\n    'social_account_not_used' => 'Questo account :socialAccount non è collegato a nessun utente. Collegalo nelle impostazioni del profilo. ',\n    'social_account_register_instructions' => 'Se non hai ancora un account, puoi registrarti usando l\\'opzione :socialAccount.',\n    'social_driver_not_found' => 'Driver social non trovato',\n    'social_driver_not_configured' => 'Le impostazioni di :socialAccount non sono configurate correttamente.',\n    'invite_token_expired' => 'Il link di invito è scaduto. Puoi provare a resettare la password del tuo account.',\n    'login_user_not_found' => 'Impossibile trovare un utente per questa azione.',\n\n    // System\n    'path_not_writable' => 'Il percorso :filePath non è scrivibile. Controlla che abbia i permessi corretti.',\n    'cannot_get_image_from_url' => 'Impossibile scaricare immagine da :url',\n    'cannot_create_thumbs' => 'Il server non può creare miniature. Controlla che l\\'estensione GD sia installata.',\n    'server_upload_limit' => 'Il server non permette un upload di questa grandezza. Prova con un file più piccolo.',\n    'server_post_limit' => 'Il server non può ricevere la quantità di dati fornita. Riprovare con meno dati o con un file più piccolo.',\n    'uploaded'  => 'Il server non consente upload di questa grandezza. Prova un file più piccolo.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Si è verificato un errore nel caricamento dell\\'immagine',\n    'image_upload_type_error' => 'Il tipo di immagine caricata non è valido',\n    'image_upload_replace_type' => 'Le sostituzioni di file immagine devono essere dello stesso tipo',\n    'image_upload_memory_limit' => 'Impossibile gestire il caricamento d\\'immagini e/o creare miniature a causa dei limiti delle risorse di sistema.',\n    'image_thumbnail_memory_limit' => 'Impossibile creare variazioni delle dimensioni dell\\'immagine a causa dei limiti delle risorse di sistema.',\n    'image_gallery_thumbnail_memory_limit' => 'Impossibile creare le miniature della galleria a causa dei limiti delle risorse di sistema.',\n    'drawing_data_not_found' => 'Non è stato possibile caricare i dati del disegno. È possibile che il file del disegno non esista più o che non si abbia il permesso di accedervi.',\n\n    // Attachments\n    'attachment_not_found' => 'Allegato non trovato',\n    'attachment_upload_error' => 'Si è verificato un errore durante il caricamento del file allegato',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Impossibile salvare la bozza. Controlla di essere connesso ad internet prima di salvare questa pagina',\n    'page_draft_delete_fail' => 'Impossibile eliminare la bozza di pagina e recuperare i contenuti salvati nella pagina corrente',\n    'page_custom_home_deletion' => 'Impossibile eliminare una pagina quando è impostata come homepage',\n\n    // Entities\n    'entity_not_found' => 'Entità non trovata',\n    'bookshelf_not_found' => 'Libreria non trovata',\n    'book_not_found' => 'Libro non trovato',\n    'page_not_found' => 'Pagina non trovata',\n    'chapter_not_found' => 'Capitolo non trovato',\n    'selected_book_not_found' => 'Il libro selezionato non è stato trovato',\n    'selected_book_chapter_not_found' => 'Il libro o il capitolo selezionati non sono stati trovati',\n    'guests_cannot_save_drafts' => 'Gli ospiti non possono salvare bozze',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Non puoi eliminare l\\'unico admin',\n    'users_cannot_delete_guest' => 'Non puoi eliminare l\\'utente ospite',\n    'users_could_not_send_invite' => 'Impossibile creare l\\'utente poiché l\\'invio dell\\'email di invito non è riuscito',\n\n    // Roles\n    'role_cannot_be_edited' => 'Questo ruolo non può essere modificato',\n    'role_system_cannot_be_deleted' => 'Questo ruolo è di sistema e non può essere eliminato',\n    'role_registration_default_cannot_delete' => 'Questo ruolo non può essere eliminato finchè è impostato come ruolo di registrazione predefinito',\n    'role_cannot_remove_only_admin' => 'Questo utente è l\\'unico con assegnato il ruolo di amministratore. Assegna il ruolo di amministratore ad un altro utente prima di rimuoverlo.',\n\n    // Comments\n    'comment_list' => 'Si è verificato un errore durante il recupero dei commenti.',\n    'cannot_add_comment_to_draft' => 'Non puoi aggiungere commenti a una bozza.',\n    'comment_add' => 'Si è verificato un errore durante l\\'aggiunta / l\\'aggiornamento del commento.',\n    'comment_delete' => 'Si è verificato un errore durante l’eliminazione del commento.',\n    'empty_comment' => 'Impossibile aggiungere un commento vuoto.',\n\n    // Error pages\n    '404_page_not_found' => 'Pagina non trovata',\n    'sorry_page_not_found' => 'Siamo spiacenti, la pagina che stavi cercando non è stata trovata.',\n    'sorry_page_not_found_permission_warning' => 'Se pensi che questa pagina possa esistere, potresti non avere i permessi per visualizzarla.',\n    'image_not_found' => 'Immagine non trovata',\n    'image_not_found_subtitle' => 'Spiacente, l\\'immagine che stai cercando non è stata trovata.',\n    'image_not_found_details' => 'Se pensi che questa immagine possa esistere, potrebbe essere stata cancellata.',\n    'return_home' => 'Ritorna alla home',\n    'error_occurred' => 'Si è verificato un errore',\n    'app_down' => ':appName è offline al momento',\n    'back_soon' => 'Tornerà presto online.',\n\n    // Import\n    'import_zip_cant_read' => 'Impossibile leggere il file ZIP.',\n    'import_zip_cant_decode_data' => 'Impossibile trovare e decodificare il contenuto ZIP data.json.',\n    'import_zip_no_data' => 'I dati del file ZIP non hanno il contenuto previsto di libri, capitoli o pagine.',\n    'import_zip_data_too_large' => 'Il contenuto ZIP data.json supera la dimensione massima di upload configurata nell\\'applicazione.',\n    'import_validation_failed' => 'L\\'importazione ZIP non è stata convalidata con errori:',\n    'import_zip_failed_notification' => 'Impossibile importare il file ZIP.',\n    'import_perms_books' => 'Non hai i permessi necessari per creare libri.',\n    'import_perms_chapters' => 'Non hai i permessi necessari per creare capitoli.',\n    'import_perms_pages' => 'Non hai i permessi necessari per creare pagine.',\n    'import_perms_images' => 'Non hai i permessi necessari per creare immagini.',\n    'import_perms_attachments' => 'Non hai il permesso necessario per creare allegati.',\n\n    // API errors\n    'api_no_authorization_found' => 'Nessun token di autorizzazione trovato nella richiesta',\n    'api_bad_authorization_format' => 'Un token di autorizzazione è stato trovato nella richiesta, ma il formato sembra non corretto',\n    'api_user_token_not_found' => 'Nessun token API valido è stato trovato nel token di autorizzazione fornito',\n    'api_incorrect_token_secret' => 'Il token segreto fornito per il token API utilizzato non è corretto',\n    'api_user_no_api_permission' => 'Il proprietario del token API utilizzato non ha il permesso di effettuare chiamate API',\n    'api_user_token_expired' => 'Il token di autorizzazione utilizzato è scaduto',\n    'api_cookie_auth_only_get' => 'Solo le richieste GET sono consentite quando si utilizza l\\'API con autenticazione basata sui cookie',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Si è verificato un errore durante l\\'invio di una e-mail di prova:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'L\\'URL non corrisponde agli host SSR configurati',\n];\n"
  },
  {
    "path": "lang/it/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Nuovo commento sulla pagina: :pageName',\n    'new_comment_intro' => 'Un utente ha commentato una pagina in :appName:',\n    'new_page_subject' => 'Nuova pagina: :pageName',\n    'new_page_intro' => 'Una nuova pagina è stata creata in :appName:',\n    'updated_page_subject' => 'Pagina aggiornata: :pageName',\n    'updated_page_intro' => 'Una pagina è stata aggiornata in :appName:',\n    'updated_page_debounce' => 'Per evitare una massa di notifiche, per un po\\' non ti verranno inviate notifiche per ulteriori modifiche a questa pagina dallo stesso editor.',\n    'comment_mention_subject' => 'Sei stato menzionato in un commento nella pagina: :pageName',\n    'comment_mention_intro' => 'Sei stato menzionato in un commento su :appName:',\n\n    'detail_page_name' => 'Nome della pagina:',\n    'detail_page_path' => 'Percorso della pagina:',\n    'detail_commenter' => 'Commentatore:',\n    'detail_comment' => 'Commento:',\n    'detail_created_by' => 'Creato da:',\n    'detail_updated_by' => 'Aggiornato da:',\n\n    'action_view_comment' => 'Vedi commento',\n    'action_view_page' => 'Vedi pagina',\n\n    'footer_reason' => 'Questa notifica è stata inviata perché :link copre questo tipo di attività per questo elemento.',\n    'footer_reason_link' => 'le tue preferenze di notifica',\n];\n"
  },
  {
    "path": "lang/it/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Precedente',\n    'next'     => 'Successivo &raquo;',\n\n];\n"
  },
  {
    "path": "lang/it/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'La password deve avere almeno otto caratteri e corrispondere alla conferma.',\n    'user' => \"Nessun utente trovato per quella mail.\",\n    'token' => 'Il token per reimpostare la password non è valido per questo indirizzo email.',\n    'sent' => 'Ti abbiamo inviato via mail il link per reimpostare la password!',\n    'reset' => 'La tua password è stata reimpostata!',\n\n];\n"
  },
  {
    "path": "lang/it/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Il mio account',\n\n    'shortcuts' => 'Scorciatoie',\n    'shortcuts_interface' => 'Preferenze di scelta rapida dell\\'Interfaccia Utente',\n    'shortcuts_toggle_desc' => 'Qui puoi abilitare o disabilitare le scorciatoie dell\\'interfaccia di sistema da tastiera, utilizzate per la navigazione e le azioni.',\n    'shortcuts_customize_desc' => 'È possibile personalizzare ciascuna delle scorciatoie riportate di seguito. È sufficiente premere la combinazione di tasti desiderata dopo aver selezionato l\\'input per una scorciatoia.',\n    'shortcuts_toggle_label' => 'Scorciatoie da tastiera attivate',\n    'shortcuts_section_navigation' => 'Navigazione',\n    'shortcuts_section_actions' => 'Azioni comuni',\n    'shortcuts_save' => 'Salva scorciatoie',\n    'shortcuts_overlay_desc' => 'Nota: quando le scorciatoie sono abilitate, premendo \"?\" è possibile visualizzare le scorciatoie disponibili per le azioni attualmente visibili sullo schermo.',\n    'shortcuts_update_success' => 'Le preferenze delle scorciatoie sono state aggiornate!',\n    'shortcuts_overview_desc' => 'Gestisci le scorciatoie da tastiera che puoi usare per navigare nell\\'interfaccia utente di sistema.',\n\n    'notifications' => 'Preferenze notifiche',\n    'notifications_desc' => 'Controlla le notifiche email che ricevi quando viene eseguita una determinata attività all\\'interno del sistema.',\n    'notifications_opt_own_page_changes' => 'Notifica in caso di modifiche alle pagine che possiedo',\n    'notifications_opt_own_page_comments' => 'Notifica i commenti sulle pagine che possiedo',\n    'notifications_opt_comment_mentions' => 'Avvisami quando vengo menzionato in un commento',\n    'notifications_opt_comment_replies' => 'Notificare le risposte ai miei commenti',\n    'notifications_save' => 'Salva preferenze',\n    'notifications_update_success' => 'Le preferenze di notifica sono state aggiornate!',\n    'notifications_watched' => 'Oggetti osservati e ignorati',\n    'notifications_watched_desc' => 'Di seguito sono riportati gli elementi a cui sono state applicate le preferenze di monitoraggio personalizzate. Per aggiornare le preferenze, visualizza l\\'elemento e usa le opzioni di monitoraggio nella barra laterale.',\n\n    'auth' => 'Accesso e sicurezza',\n    'auth_change_password' => 'Modifica password',\n    'auth_change_password_desc' => 'Modifica la password che usi per accedere all\\'applicazione. Deve essere lunga almeno 8 caratteri.',\n    'auth_change_password_success' => 'La password è stata aggiornata!',\n\n    'profile' => 'Dettagli del profilo',\n    'profile_desc' => 'Gestisci i dettagli dell\\'account che ti rappresenta agli altri utenti, oltre ai dettagli utilizzati per la comunicazione e la personalizzazione del sistema.',\n    'profile_view_public' => 'Visualizza profilo pubblico',\n    'profile_name_desc' => 'Configura il tuo nome visualizzato che sarà visibile ad altri utenti del sistema attraverso l\\'attività che esegui e il contenuto che possiedi.',\n    'profile_email_desc' => 'Questa email verrà utilizzata per le notifiche e, a seconda dell\\'autenticazione al sistema attiva, per l\\'accesso al sistema.',\n    'profile_email_no_permission' => 'Purtroppo non hai il permesso di modificare il tuo indirizzo email. Se vuoi modificarlo, devi chiedere a un amministratore di farlo per te.',\n    'profile_avatar_desc' => 'Seleziona un\\'immagine che verrà usata per rappresentarti agli altri utenti del sistema. Idealmente questa immagine dovrebbe essere quadrata e di circa 256px in larghezza e altezza.',\n    'profile_admin_options' => 'Opzioni amministratore',\n    'profile_admin_options_desc' => 'Ulteriori opzioni di livello amministrativo, come quelle per gestire le assegnazioni dei ruoli, possono essere trovate per il tuo account utente nell\\'area \"Impostazioni > Utenti\".',\n\n    'delete_account' => 'Elimina account',\n    'delete_my_account' => 'Elimina il mio account',\n    'delete_my_account_desc' => 'Questa azione eliminerà completamente il tuo account utente dal sistema. Non sarai in grado di recuperare l\\'account né di annullare questa azione. Il contenuto che hai creato, come le pagine create e le immagini caricate, rimarrà nel sistema.',\n    'delete_my_account_warning' => 'Sei sicuro di voler eliminare il tuo account?',\n];\n"
  },
  {
    "path": "lang/it/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Impostazioni',\n    'settings_save' => 'Salva impostazioni',\n    'system_version' => 'Versione del sistema',\n    'categories' => 'Categorie',\n\n    // App Settings\n    'app_customization' => 'Personalizzazione',\n    'app_features_security' => 'Funzioni e sicurezza',\n    'app_name' => 'Nome applicazione',\n    'app_name_desc' => 'Questo nome è mostrato nell\\'intestazione e in tutte le email inviate dal sistema.',\n    'app_name_header' => 'Mostra il nome nell\\'header',\n    'app_public_access' => 'Accesso pubblico',\n    'app_public_access_desc' => 'Abilitando questa opzione, i visitatori, che non sono loggati, potranno accedere ai contenuti nella tua istanza BookStack.',\n    'app_public_access_desc_guest' => 'L\\'accesso ai visitatori pubblici può essere controllato attraverso l\\'utente \"Guest\".',\n    'app_public_access_toggle' => 'Permetti accesso pubblico',\n    'app_public_viewing' => 'Consentire la visione pubblica?',\n    'app_secure_images' => 'Sicurezza aggiuntiva per le immagini caricate',\n    'app_secure_images_toggle' => 'Abilita sicurezza aggiuntiva per le immagini caricate',\n    'app_secure_images_desc' => 'Per ragioni di prestazioni, tutte le immagini sono pubbliche. Questa opzione aggiunge una stringa casuale, difficile da indovinare, davanti agli url delle immagini. Assicurati che l\\'indicizzazione delle cartelle non sia abilitato per prevenire un accesso semplice.',\n    'app_default_editor' => 'Editor di pagina predefinito',\n    'app_default_editor_desc' => 'Seleziona quale editor sarà usato per impostazione predefinita quando modifichi nuove pagine. Questa impostazione potrà essere sovrascritta a livello di pagina dove i permessi lo consentono.',\n    'app_custom_html' => 'Contenuto Head HTML personalizzato',\n    'app_custom_html_desc' => 'Qualsiasi contenuto aggiunto qui verrà inserito alla fine della sezione <head> di tutte le pagine. Questo è utile per sovrascrivere lo stile o aggiungere il codice per gli analytics.',\n    'app_custom_html_disabled_notice' => 'Il contenuto Head HTML personalizzato è disabilitato su questa pagina delle impostazioni per garantire che eventuali modifiche distruttive possano essere annullate.',\n    'app_logo' => 'Logo applicazione',\n    'app_logo_desc' => 'Viene utilizzata nella barra di intestazione dell\\'applicazione, tra le altre aree. L\\'immagine deve avere un\\'altezza di 86px. Le immagini più grandi saranno ridimensionate.',\n    'app_icon' => 'Icona applicazione',\n    'app_icon_desc' => 'Questa icona viene utilizzata per le schede del browser e per le icone di collegamento. Deve essere un\\'immagine PNG quadrata di 256px.',\n    'app_homepage' => 'Homepage applicazione',\n    'app_homepage_desc' => 'Seleziona una pagina da mostrare nella home anzichè quella di default. I permessi della pagina sono ignorati per le pagine selezionate.',\n    'app_homepage_select' => 'Seleziona una pagina',\n    'app_footer_links' => 'Link a piè di pagina',\n    'app_footer_links_desc' => 'Aggiungi link da mostrare in basso nel sito. Questi saranno visibili in fondo alla maggior parte delle pagine, incluse quelle che non richiedono un autenticazione. Puoi usare l\\'etichetta \"trans::<chiave>\" per utilizzare le traduzioni implementate nella piattaforma. Esempio: usando \"trans::common.privacy_policy\" mostrerà il testo tradotto \"Norme sulla privacy\" e \"trans::common.terms_of_service\" mostrerà il testo tradotto \"Condizioni del Servizio\".',\n    'app_footer_links_label' => 'Etichetta del link',\n    'app_footer_links_url' => 'URL del link',\n    'app_footer_links_add' => 'Aggiungi link in basso',\n    'app_disable_comments' => 'Disabilita commenti',\n    'app_disable_comments_toggle' => 'Disabilita i commenti',\n    'app_disable_comments_desc' => 'Disabilita i commenti su tutte le pagine dell\\'applicazione. <br> I commenti esistenti non sono mostrati.',\n\n    // Color settings\n    'color_scheme' => 'Schema di colore dell\\'applicazione',\n    'color_scheme_desc' => 'Imposta i colori da utilizzare nell\\'interfaccia utente dell\\'applicazione. I colori possono essere configurati separatamente per le modalità scura e chiara, per adattarsi al meglio al tema e garantire la leggibilità.',\n    'ui_colors_desc' => 'Imposta il colore primario dell\\'applicazione e il colore predefinito dei collegamenti. Il colore primario è utilizzato principalmente per il banner dell\\'intestazione, i pulsanti e le decorazioni dell\\'interfaccia. Il colore predefinito dei collegamenti viene utilizzato per i collegamenti e le azioni basate sul testo, sia all\\'interno dei contenuti scritti che nell\\'interfaccia dell\\'applicazione.',\n    'app_color' => 'Colore principale',\n    'link_color' => 'Colore preferito del link',\n    'content_colors_desc' => 'Impostare i colori per tutti gli elementi nella gerarchia dell\\'organizzazione della pagina. Si consiglia di scegliere colori con una luminosità simile a quella dei colori predefiniti per garantire la leggibilità.',\n    'bookshelf_color' => 'Colore della libreria',\n    'book_color' => 'Colore del libro',\n    'chapter_color' => 'Colore del capitolo',\n    'page_color' => 'Colore della pagina',\n    'page_draft_color' => 'Colore della bozza',\n\n    // Registration Settings\n    'reg_settings' => 'Impostazioni registrazione',\n    'reg_enable' => 'Abilita registrazione',\n    'reg_enable_toggle' => 'Abilita registrazione',\n    'reg_enable_desc' => 'Quando la registrazione è abilitata, l\\utente sarà in grado di registrarsi autonomamente all\\'applicazione. Al momento della registrazione gli verrà associato un ruolo utente predefinito.',\n    'reg_default_role' => 'Ruolo predefinito dopo la registrazione',\n    'reg_enable_external_warning' => 'L\\'opzione precedente viene ignorata se l\\'autenticazione esterna tramite LDAP o SAML è attiva. Se l\\'autenticazione (effettuata sul sistema esterno) sarà valida, gli account di eventuali membri non registrati saranno creati in automatico.',\n    'reg_email_confirmation' => 'Conferma Email',\n    'reg_email_confirmation_toggle' => 'Richiedi conferma email',\n    'reg_confirm_email_desc' => 'Se la restrizione per dominio è usata la conferma della mail sarà richiesta e la scelta ignorata.',\n    'reg_confirm_restrict_domain' => 'Restringi la registrazione al dominio',\n    'reg_confirm_restrict_domain_desc' => 'Inserisci una lista separata da virgola di domini di email a cui vorresti restringere la registrazione. Agli utenti verrà inviata una mail per confermare il loro indirizzo prima che possano interagire con l\\'applicazione. <br> Nota che gli utenti saranno in grado di cambiare il loro indirizzo dopo aver completato la registrazione.',\n    'reg_confirm_restrict_domain_placeholder' => 'Nessuna restrizione impostata',\n\n    // Sorting Settings\n    'sorting' => 'Elenchi E Ordinamento',\n    'sorting_book_default' => 'Regola Di Ordinamento Libro Predefinita',\n    'sorting_book_default_desc' => 'Selezionare la regola di ordinamento predefinita da applicare ai nuovi libri. Questa regola non influisce sui libri esistenti e può essere modificata per ogni libro.',\n    'sorting_rules' => 'Regole di ordinamento',\n    'sorting_rules_desc' => 'Si tratta di operazioni di ordinamento predefinite applicabili ai contenuti del sistema.',\n    'sort_rule_assigned_to_x_books' => 'Assegnato a :count Book|Assegnato a :count Books',\n    'sort_rule_create' => 'Crea Regola Di Ordinamento',\n    'sort_rule_edit' => 'Modifica Regola di Ordinamento',\n    'sort_rule_delete' => 'Elimina Regola di Ordinamento',\n    'sort_rule_delete_desc' => 'Rimuove questa regola di ordinamento dal sistema. I libri che usano questo ordinamento torneranno all\\'ordinamento manuale.',\n    'sort_rule_delete_warn_books' => 'Questa regola di ordinamento è attualmente utilizzata su :count book(s). Sei sicuro di volerla eliminare?',\n    'sort_rule_delete_warn_default' => 'Questa regola di ordinamento è attualmente utilizzata come predefinita per i libri. Sei sicuro di volerla eliminare?',\n    'sort_rule_details' => 'Dettagli della regola di ordinamento',\n    'sort_rule_details_desc' => 'Imposta un nome per questa regola di ordinamento, apparirà nelle liste quando gli utenti selezionano un ordinamento.',\n    'sort_rule_operations' => 'Operazioni di ordinamento',\n    'sort_rule_operations_desc' => 'Configurare le azioni di ordinamento da eseguire spostandole dall\\'elenco delle operazioni disponibili. Al momento dell\\'utilizzo, le operazioni saranno applicate in ordine, dall\\'alto verso il basso. Le modifiche apportate qui saranno applicate a tutti i libri assegnati al momento del salvataggio.',\n    'sort_rule_available_operations' => 'Operazioni Disponibili',\n    'sort_rule_available_operations_empty' => 'Nessuna operazione rimanente',\n    'sort_rule_configured_operations' => 'Operazioni Configurate',\n    'sort_rule_configured_operations_empty' => 'Trascinare/aggiungere le operazioni dall\\'elenco “Operazioni disponibili”',\n    'sort_rule_op_asc' => '(Ascendente)',\n    'sort_rule_op_desc' => '(Discendente)',\n    'sort_rule_op_name' => 'Nome - Alfabetico',\n    'sort_rule_op_name_numeric' => 'Name - Numerico',\n    'sort_rule_op_created_date' => 'Data di creazione',\n    'sort_rule_op_updated_date' => 'Data di aggiornamento',\n    'sort_rule_op_chapters_first' => 'Capitoli Prima',\n    'sort_rule_op_chapters_last' => 'Capitoli dopo',\n    'sorting_page_limits' => 'Limiti Visualizzazione Per Pagina',\n    'sorting_page_limits_desc' => 'Imposta il numero di elementi da visualizzare per pagina nei vari elenchi del sistema. In genere, un numero inferiore garantisce prestazioni migliori, mentre un numero maggiore evita di dover sfogliare più pagine. Si consiglia di utilizzare un multiplo di 6.',\n\n    // Maintenance settings\n    'maint' => 'Manutenzione',\n    'maint_image_cleanup' => 'Pulizia immagini',\n    'maint_image_cleanup_desc' => 'Esegue la scansione del contenuto delle pagine e delle revisioni per verificare quali immagini e disegni sono attualmente in uso e quali immagini sono ridondanti. Assicurati di creare un backup completo del database e delle immagini prima di eseguire la pulizia.',\n    'maint_delete_images_only_in_revisions' => 'Elimina anche le immagini che esistono solo nelle vecchie revisioni della pagina',\n    'maint_image_cleanup_run' => 'Esegui pulizia',\n    'maint_image_cleanup_warning' => 'Sono state trovate :count immagini potenzialmente inutilizzate. Sei sicuro di voler eliminare queste immagini?',\n    'maint_image_cleanup_success' => ':count immagini potenzialmente inutilizzate trovate e eliminate!',\n    'maint_image_cleanup_nothing_found' => 'Nessuna immagine non utilizzata trovata, non è stato cancellato nulla!',\n    'maint_send_test_email' => 'Invia un\\'email di test',\n    'maint_send_test_email_desc' => 'Questo comando invia un\\'email di prova al tuo indirizzo email specificato nel tuo profilo.',\n    'maint_send_test_email_run' => 'Invia email di test',\n    'maint_send_test_email_success' => 'Email inviata a :address',\n    'maint_send_test_email_mail_subject' => 'Email di test',\n    'maint_send_test_email_mail_greeting' => 'L\\'invio delle email sembra funzionare!',\n    'maint_send_test_email_mail_text' => 'Congratulazioni! Siccome hai ricevuto questa notifica email, le tue impostazioni sembrano essere configurate correttamente.',\n    'maint_recycle_bin_desc' => 'Le librerie, i libri, i capitoli e le pagine cancellati vengono inviati al cestino in modo che possano essere ripristinati o eliminati definitivamente. Gli elementi più vecchi nel cestino possono essere automaticamente rimossi dopo un certo periodo, a seconda della configurazione del sistema.',\n    'maint_recycle_bin_open' => 'Apri il Cestino',\n    'maint_regen_references' => 'Rigenera riferimenti',\n    'maint_regen_references_desc' => 'Questa azione ricostruirà l\\'indice dei riferimenti incrociati all\\'interno del database. Di solito questa operazione è gestita automaticamente, ma può essere utile per indicizzare contenuti vecchi o aggiunti con metodi non ufficiali.',\n    'maint_regen_references_success' => 'L\\'indice di riferimento è stato rigenerato!',\n    'maint_timeout_command_note' => 'Nota: Questa azione può richiedere del tempo per essere eseguita e può causare problemi di timeout in alcuni ambienti web. In alternativa, questa azione può essere eseguita usando un comando da terminale.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Cestino',\n    'recycle_bin_desc' => 'Qui è possibile ripristinare gli elementi che sono stati eliminati o scegliere di rimuoverli definitivamente dal sistema. Questo elenco non è filtrato a differenza di elenchi di attività simili nel sistema in cui vengono applicati i filtri autorizzazioni.',\n    'recycle_bin_deleted_item' => 'Elemento eliminato',\n    'recycle_bin_deleted_parent' => 'Superiore',\n    'recycle_bin_deleted_by' => 'Cancellato da',\n    'recycle_bin_deleted_at' => 'Orario Cancellazione',\n    'recycle_bin_permanently_delete' => 'Elimina definitivamente',\n    'recycle_bin_restore' => 'Ripristina',\n    'recycle_bin_contents_empty' => 'Al momento il cestino è vuoto',\n    'recycle_bin_empty' => 'Svuota Cestino',\n    'recycle_bin_empty_confirm' => 'Questa operazione cancellerà definitivamente tutti gli elementi presenti nel cestino, inclusi i contenuti relativi a ciascun elemento. Sei sicuro di voler svuotare il cestino?',\n    'recycle_bin_destroy_confirm' => 'Questa azione eliminerà definitivamente questo elemento dal sistema, insieme a qualsiasi elemento figlio elencato di seguito, e non sarai in grado di ripristinare questo contenuto. Sei sicuro di voler eliminare definitivamente questo elemento?',\n    'recycle_bin_destroy_list' => 'Elementi da eliminare definitivamente',\n    'recycle_bin_restore_list' => 'Elementi da ripristinare',\n    'recycle_bin_restore_confirm' => 'Questa azione ripristinerà l\\'elemento eliminato, compresi gli elementi figli, nella loro posizione originale. Se la posizione originale è stata eliminata, ed è ora nel cestino, anche l\\'elemento padre dovrà essere ripristinato.',\n    'recycle_bin_restore_deleted_parent' => 'L\\'elemento padre di questo elemento è stato eliminato. Questo elemento rimarrà eliminato fino a che l\\'elemento padre non sarà ripristinato.',\n    'recycle_bin_restore_parent' => 'Ripristina Superiore',\n    'recycle_bin_destroy_notification' => 'Eliminati :count elementi dal cestino.',\n    'recycle_bin_restore_notification' => 'Ripristinati :count elementi dal cestino.',\n\n    // Audit Log\n    'audit' => 'Registro di Controllo',\n    'audit_desc' => 'Questo registro di controllo mostra la lista delle attività registrate dal sistema. Questa lista, a differenza di altre liste del sistema a cui vengono applicate dei filtri, è integrale.',\n    'audit_event_filter' => 'Filtro eventi',\n    'audit_event_filter_no_filter' => 'Nessun filtro',\n    'audit_deleted_item' => 'Elemento eliminato',\n    'audit_deleted_item_name' => 'Nome: :name',\n    'audit_table_user' => 'Utente',\n    'audit_table_event' => 'Evento',\n    'audit_table_related' => 'Elemento o dettaglio correlato',\n    'audit_table_ip' => 'Indirizzo IP',\n    'audit_table_date' => 'Data attività',\n    'audit_date_from' => 'Dalla data',\n    'audit_date_to' => 'Alla data',\n\n    // Role Settings\n    'roles' => 'Ruoli',\n    'role_user_roles' => 'Ruoli Utente',\n    'roles_index_desc' => 'I ruoli sono utilizzati per raggruppare gli utenti e fornire ai loro membri i permessi di sistema. Quando un utente è membro di più ruoli, i privilegi concessi si sovrappongono e l\\'utente eredita tutte le abilità.',\n    'roles_x_users_assigned' => ':count utente assegnato|:count utenti assegnati',\n    'roles_x_permissions_provided' => ':count permesso|:count permessi',\n    'roles_assigned_users' => 'Utenti assegnati',\n    'roles_permissions_provided' => 'Autorizzazioni fornite',\n    'role_create' => 'Crea nuovo ruolo',\n    'role_delete' => 'Elimina ruolo',\n    'role_delete_confirm' => 'Questo eliminerà il ruolo con il nome \\':roleName\\'.',\n    'role_delete_users_assigned' => 'Questo ruolo ha :userCount utenti assegnati. Se vuoi migrare gli utenti da questo ruolo selezionane uno nuovo sotto.',\n    'role_delete_no_migration' => \"Non migrare gli utenti\",\n    'role_delete_sure' => 'Sei sicuro di voler eliminare questo ruolo?',\n    'role_edit' => 'Modifica ruolo',\n    'role_details' => 'Dettagli ruolo',\n    'role_name' => 'Nome ruolo',\n    'role_desc' => 'Breve descrizione del ruolo',\n    'role_mfa_enforced' => 'Richiesta autenticazione multi-fattore',\n    'role_external_auth_id' => 'ID autenticazione esterna',\n    'role_system' => 'Permessi di sistema',\n    'role_manage_users' => 'Gestire gli utenti',\n    'role_manage_roles' => 'Gestire ruoli e permessi di ruoli',\n    'role_manage_entity_permissions' => 'Gestire tutti i permessi di libri, capitoli e pagine',\n    'role_manage_own_entity_permissions' => 'Gestire i permessi sui propri libri, capitoli e pagine',\n    'role_manage_page_templates' => 'Gestire modelli di pagina',\n    'role_access_api' => 'Accedere alle API di sistema',\n    'role_manage_settings' => 'Gestire impostazioni app',\n    'role_export_content' => 'Esportare contenuto',\n    'role_import_content' => 'Importa contenuto',\n    'role_editor_change' => 'Cambiare editor di pagina',\n    'role_notifications' => 'Ricevere e gestire le notifiche',\n    'role_permission_note_users_and_roles' => 'Queste autorizzazioni forniranno tecnicamente anche la visibilità e la ricerca di utenti e ruoli nel sistema.',\n    'role_asset' => 'Permessi entità',\n    'roles_system_warning' => 'Siate consapevoli che l\\'accesso a uno dei tre permessi qui sopra può consentire a un utente di modificare i propri privilegi o i privilegi di altri nel sistema. Assegna ruoli con questi permessi solo ad utenti fidati.',\n    'role_asset_desc' => 'Questi permessi controllano l\\'accesso predefinito alle entità. I permessi in libri, capitoli e pagine sovrascriveranno questi.',\n    'role_asset_admins' => 'Gli amministratori hanno automaticamente accesso a tutti i contenuti ma queste opzioni possono mostrare o nascondere le opzioni della UI.',\n    'role_asset_image_view_note' => 'Questo si riferisce alla visibilità all\\'interno del gestore delle immagini. L\\'accesso effettivo ai file di immagine caricati dipenderà dall\\'opzione di archiviazione delle immagini di sistema.',\n    'role_asset_users_note' => 'Queste autorizzazioni forniranno tecnicamente anche la visibilità e la ricerca di utenti nel sistema.',\n    'role_all' => 'Tutti',\n    'role_own' => 'Propri',\n    'role_controlled_by_asset' => 'Controllato dall\\'entità in cui sono caricati',\n    'role_save' => 'Salva ruolo',\n    'role_users' => 'Utenti in questo ruolo',\n    'role_users_none' => 'Nessun utente assegnato a questo ruolo',\n\n    // Users\n    'users' => 'Utenti',\n    'users_index_desc' => 'Crea e gestisci account utente individuali all\\'interno del sistema. Gli account utente sono utilizzati per il login e l\\'attribuzione di contenuti e attività. Le autorizzazioni di accesso sono principalmente basate sui ruoli, ma la proprietà dei contenuti dell\\'utente, insieme ad altri fattori, può influenzare le autorizzazioni e l\\'accesso.',\n    'user_profile' => 'Profilo utente',\n    'users_add_new' => 'Aggiungi Nuovo Utente',\n    'users_search' => 'Cerca utenti',\n    'users_latest_activity' => 'Ultima attività',\n    'users_details' => 'Dettagli utente',\n    'users_details_desc' => 'Imposta un nome e un indirizzo email per questo utente. L\\'indirizzo email verrà utilizzato per accedere all\\'applicazione.',\n    'users_details_desc_no_email' => 'Imposta un nome per questo utente così gli altri possono riconoscerlo.',\n    'users_role' => 'Ruoli utente',\n    'users_role_desc' => 'Seleziona a quali ruoli verrà assegnato questo utente. Se un utente è assegnato a più ruoli riceverà tutte le abilità dei ruoli assegnati.',\n    'users_password' => 'Password utente',\n    'users_password_desc' => 'Imposta una password usata per accedere all\\'applicazione. Deve essere lunga almeno 8 caratteri.',\n    'users_send_invite_text' => 'Puoi scegliere di inviare a questo utente un\\'email di invito che permette loro di impostare la propria password altrimenti puoi impostare la password tu stesso.',\n    'users_send_invite_option' => 'Invia email di invito',\n    'users_external_auth_id' => 'ID autenticazione esterna',\n    'users_external_auth_id_desc' => 'Quando è in uso un sistema di autenticazione esterno (come SAML2, OIDC o LDAP) questo è l\\'ID che collega questo utente BookStack all\\'account del sistema di autenticazione. È possibile ignorare questo campo se si utilizza l\\'autenticazione predefinita basata su email.',\n    'users_password_warning' => 'Compila la parte sottostante solo se desideri cambiare la password per questo utente.',\n    'users_system_public' => 'Questo utente rappresente qualsiasi ospite che visita il sito. Non può essere usato per effettuare il login ma è assegnato automaticamente.',\n    'users_delete' => 'Elimina utente',\n    'users_delete_named' => 'Elimina l\\'utente :userName',\n    'users_delete_warning' => 'Questo eliminerà completamente l\\'utente \\':userName\\' dal sistema.',\n    'users_delete_confirm' => 'Sei sicuro di voler eliminare questo utente?',\n    'users_migrate_ownership' => 'Cambia proprietario',\n    'users_migrate_ownership_desc' => 'Seleziona qui un utente se vuoi che un altro utente diventi il proprietario di tutti gli elementi attualmente di proprietà di questo utente.',\n    'users_none_selected' => 'Nessun utente selezionato',\n    'users_edit' => 'Modifica utente',\n    'users_edit_profile' => 'Modifica profilo',\n    'users_avatar' => 'Avatar utente',\n    'users_avatar_desc' => 'Quest\\'immagine dovrebbe essere quadrata e alta circa 256px.',\n    'users_preferred_language' => 'Lingua preferita',\n    'users_preferred_language_desc' => 'Questa opzione cambierà la lingua utilizzata per l\\'interfaccia utente dell\\'applicazione. Questo non influirà su alcun contenuto creato dall\\'utente.',\n    'users_social_accounts' => 'Account social',\n    'users_social_accounts_desc' => 'Visualizza lo stato degli account social connessi per questo utente. Gli account social possono essere utilizzati in aggiunta al sistema di autenticazione primaria per l\\'accesso al sistema.',\n    'users_social_accounts_info' => 'Qui puoi connettere gli altri account per un accesso più veloce e semplice. Disconnettere un account qui non rimuoverà le altre sessioni. Revoca l\\'accesso dal tuo profilo negli account social connessi.',\n    'users_social_connect' => 'Connetti account',\n    'users_social_disconnect' => 'Disconnetti account',\n    'users_social_status_connected' => 'Connesso',\n    'users_social_status_disconnected' => 'Disconnesso',\n    'users_social_connected' => 'L\\'account :socialAccount è stato connesso correttamente al tuo profilo.',\n    'users_social_disconnected' => 'L\\'account :socialAccount è stato disconnesso correttamente dal tuo profilo.',\n    'users_api_tokens' => 'Token API',\n    'users_api_tokens_desc' => 'Crea e gestisci i token di accesso utilizzati per autenticarsi con l\\'API REST di BookStack. I permessi per l\\'API sono gestiti tramite l\\'utente a cui appartiene il token.',\n    'users_api_tokens_none' => 'Nessun token API è stato creato per questo utente',\n    'users_api_tokens_create' => 'Crea token',\n    'users_api_tokens_expires' => 'Scade',\n    'users_api_tokens_docs' => 'Documentazione API',\n    'users_mfa' => 'Autenticazione multi-fattore',\n    'users_mfa_desc' => 'Imposta l\\'autenticazione multi-fattore come misura di sicurezza aggiuntiva per il tuo account.',\n    'users_mfa_x_methods' => ':count metodo configurato|:count metodi configurati',\n    'users_mfa_configure' => 'Configura metodi',\n\n    // API Tokens\n    'user_api_token_create' => 'Crea token API',\n    'user_api_token_name' => 'Nome',\n    'user_api_token_name_desc' => 'Assegna al tuo token un nome leggibile per ricordarne la funzionalità in futuro.',\n    'user_api_token_expiry' => 'Data di scadenza',\n    'user_api_token_expiry_desc' => 'Imposta una data di scadenza per questo token. Dopo questa data, le richieste che utilizzeranno questo token non funzioneranno più. Lasciando questo campo vuoto si imposterà la scadenza tra 100 anni.',\n    'user_api_token_create_secret_message' => 'Immediatamente dopo aver creato questo token, un \"ID del Token\" e una \"Chiave segreta Token\" saranno generati e mostrati. La chiave segreta verrà mostrata unicamente questa volta, assicurati, quindi, di copiare il valore in un posto sicuro prima di procedere.',\n    'user_api_token' => 'Token API',\n    'user_api_token_id' => 'ID del Token',\n    'user_api_token_id_desc' => 'Questo è un identificativo non modificabile generato dal sistema per questo token e che sarà necessario fornire per le richieste tramite API.',\n    'user_api_token_secret' => 'Chiave segreta token',\n    'user_api_token_secret_desc' => 'Questo è una chiave segreta generata dal sistema per questo token che sarà necessario fornire per le richieste via API. Questo valore sarà visibile unicamente in questo momento pertanto copialo in un posto sicuro.',\n    'user_api_token_created' => 'Token Aggiornato :timeAgo',\n    'user_api_token_updated' => 'Token aggiornato :timeAgo',\n    'user_api_token_delete' => 'Elimina token',\n    'user_api_token_delete_warning' => 'Questa operazione eliminerà irreversibilmente dal sistema il token API denominato \\':tokenName\\'.',\n    'user_api_token_delete_confirm' => 'Sei sicuri di voler eliminare questo token API?',\n\n    // Webhooks\n    'webhooks' => 'Webhook',\n    'webhooks_index_desc' => 'I webhook sono un modo per inviare dati a URL esterni quando si verificano determinate azioni ed eventi all\\'interno del sistema, consentendo l\\'integrazione basata sugli eventi con piattaforme esterne, come sistemi di messaggistica o di notifica.',\n    'webhooks_x_trigger_events' => ':count evento trigger|:count eventi trigger',\n    'webhooks_create' => 'Crea nuovo webhook',\n    'webhooks_none_created' => 'Nessun webhook è stato creato.',\n    'webhooks_edit' => 'Modifica webhook',\n    'webhooks_save' => 'Salva webhook',\n    'webhooks_details' => 'Dettagli webhook',\n    'webhooks_details_desc' => 'Fornisci un nome di facile utilizzo e un endpoint POST come posizione per i dati del webhook da inviare.',\n    'webhooks_events' => 'Eventi webhook',\n    'webhooks_events_desc' => 'Seleziona tutti gli eventi che dovrebbero attivare questo webhook da chiamare.',\n    'webhooks_events_warning' => 'Tieni presente che questi eventi saranno attivati per tutti gli eventi selezionati, anche se vengono applicati permessi personalizzati. Assicurati che l\\'uso di questo webhook non esporrà contenuti riservati.',\n    'webhooks_events_all' => 'Tutti gli eventi di sistema',\n    'webhooks_name' => 'Nome webhook',\n    'webhooks_timeout' => 'Timeout richiesta webhook (secondi)',\n    'webhooks_endpoint' => 'Endpoint webhook',\n    'webhooks_active' => 'Webhook attivo',\n    'webhook_events_table_header' => 'Eventi',\n    'webhooks_delete' => 'Elimina webhook',\n    'webhooks_delete_warning' => 'Questo eliminerà completamente questo webhook chiamato \\':webhookName\\' dal sistema.',\n    'webhooks_delete_confirm' => 'Sei sicuro di voler eliminare questo webhook?',\n    'webhooks_format_example' => 'Esempio di formato webhook',\n    'webhooks_format_example_desc' => 'I dati del webhook vengono inviati come richiesta POST all\\'endpoint configurato come JSON seguendo il formato sottostante. Le proprietà \"related_item\" e \"url\" sono opzionali e dipenderanno dal tipo di evento attivato.',\n    'webhooks_status' => 'Stato webhook',\n    'webhooks_last_called' => 'Ultima chiamata:',\n    'webhooks_last_errored' => 'Ultimo errore:',\n    'webhooks_last_error_message' => 'Ultimo messaggio di errore:',\n\n    // Licensing\n    'licenses' => 'Licenze',\n    'licenses_desc' => 'Questa pagina contiene informazioni dettagliate sulle licenze di BookStack, oltre ai progetti e alle librerie utilizzate all\\'interno di BookStack. Molti dei progetti elencati possono essere utilizzati solo in un contesto di sviluppo.',\n    'licenses_bookstack' => 'Licenza BookStack',\n    'licenses_php' => 'Licenze Librerie PHP',\n    'licenses_js' => 'Licenze Librerie JavaScript',\n    'licenses_other' => 'Altre Licenze',\n    'license_details' => 'Dettagli Licenza',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'Inglese',\n        'ar' => 'Arabo',\n        'bg' => 'Bulgaro',\n        'bs' => 'Bosniaco',\n        'ca' => 'Catalano',\n        'cs' => 'Ceco',\n        'cy' => 'Cymraeg',\n        'da' => 'Danese',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Spagnolo',\n        'es_AR' => 'Spagnolo d\\'Argentina',\n        'et' => 'Estone',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Francese',\n        'he' => 'Ebraico',\n        'hr' => 'Croato',\n        'hu' => 'Ungherese',\n        'id' => 'Indonesiano',\n        'it' => 'Italiano',\n        'ja' => 'Giapponese',\n        'ko' => 'Coreano',\n        'lt' => 'Lituano',\n        'lv' => 'Lettone',\n        'nb' => 'Norvegese (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Olandese',\n        'pl' => 'Polacco',\n        'pt' => 'Portoghese',\n        'pt_BR' => 'Portoghese Brasiliano',\n        'ro' => 'Română',\n        'ru' => 'Russo',\n        'sk' => 'Sloveno',\n        'sl' => 'Sloveno',\n        'sv' => 'Svedese',\n        'tr' => 'Turco',\n        'uk' => 'Ucraino',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Vietnamita',\n        'zh_CN' => 'Cinese semplificato',\n        'zh_TW' => 'Cinese tradizionale',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/it/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute deve essere accettato.',\n    'active_url'           => ':attribute non è uno URL valido.',\n    'after'                => ':attribute deve essere una data dopo il :date.',\n    'alpha'                => ':attribute deve contenere solo lettere.',\n    'alpha_dash'           => ':attribute deve contenere solo lettere, numeri, trattini e trattini bassi.',\n    'alpha_num'            => ':attribute deve contenere solo lettere e numeri.',\n    'array'                => ':attribute deve essere un array.',\n    'backup_codes'         => 'Il codice fornito non è valido o è già stato utilizzato.',\n    'before'               => ':attribute deve essere una data prima del :date.',\n    'between'              => [\n        'numeric' => 'Il campo :attribute deve essere tra :min e :max.',\n        'file'    => 'Il campo :attribute deve essere tra :min e :max kilobyte.',\n        'string'  => 'Il campo :attribute deve essere tra :min e :max caratteri.',\n        'array'   => 'Il campo :attribute deve avere tra :min e :max oggetti.',\n    ],\n    'boolean'              => ':attribute deve essere vero o falso.',\n    'confirmed'            => 'La conferma di :attribute non corrisponde.',\n    'date'                 => ':attribute non è una data valida.',\n    'date_format'          => 'Il campo :attribute non corrisponde al formato :format.',\n    'different'            => 'Il campo :attribute e :other devono essere differenti.',\n    'digits'               => 'Il campo :attribute deve essere di :digits numeri.',\n    'digits_between'       => 'Il campo :attribute deve essere tra i numeri :min e :max.',\n    'email'                => 'Il campo :attribute deve essere un indirizzo email valido.',\n    'ends_with' => ':attribute deve terminare con uno dei seguenti: :values',\n    'file'                 => ':attribute deve essere fornito come file valido.',\n    'filled'               => 'Il campo :attribute field is required.',\n    'gt'                   => [\n        'numeric' => ':attribute deve essere maggiore di :value.',\n        'file'    => ':attribute deve essere maggiore di :value kilobyte.',\n        'string'  => ':attribute deve essere maggiore di :value caratteri.',\n        'array'   => ':attribute deve avere più di :value elementi.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute deve essere maggiore o uguale a :value.',\n        'file'    => ':attribute deve essere maggiore o uguale a :value kilobyte.',\n        'string'  => ':attribute deve essere maggiore o uguale a :value caratteri.',\n        'array'   => ':attribute deve avere :value o più elementi.',\n    ],\n    'exists'               => 'Il campo :attribute selezionato non è valido.',\n    'image'                => 'Il campo :attribute deve essere un\\'immagine.',\n    'image_extension'      => ':attribute deve avere un\\'estensione immagine valida e supportata.',\n    'in'                   => 'Il campo :attribute selezionato non è valido.',\n    'integer'              => 'Il campo :attribute deve essere un intero.',\n    'ip'                   => 'Il campo :attribute deve essere un indirizzo IP valido.',\n    'ipv4'                 => ':attribute deve essere un indirizzo IPv4 valido.',\n    'ipv6'                 => ':attribute deve essere un indirizzo IPv6 valido.',\n    'json'                 => ':attribute deve essere una stringa JSON valida.',\n    'lt'                   => [\n        'numeric' => ':attribute deve essere inferiore a :value.',\n        'file'    => ':attribute deve essere inferiore a :value kilobyte.',\n        'string'  => ':attribute deve essere inferiore a :value caratteri.',\n        'array'   => ':attribute deve avere meno di :value elementi.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute deve essere minore o uguale :value.',\n        'file'    => ':attribute deve essere minore o uguale a :value kilobyte.',\n        'string'  => ':attribute deve essere minore o uguale a :value caratteri.',\n        'array'   => ':attribute non deve avere più di :value elementi.',\n    ],\n    'max'                  => [\n        'numeric' => 'Il campo :attribute non deve essere maggiore di :max.',\n        'file'    => 'Il campo :attribute non deve essere maggiore di :max kilobyte.',\n        'string'  => 'Il campo :attribute non deve essere maggiore di :max caratteri.',\n        'array'   => 'Il campo :attribute non deve avere più di :max oggetti.',\n    ],\n    'mimes'                => 'Il campo :attribute deve essere: :values.',\n    'min'                  => [\n        'numeric' => 'Il campo :attribute deve essere almeno :min.',\n        'file'    => 'Il campo :attribute deve essere almeno :min kilobyte.',\n        'string'  => 'Il campo :attribute deve essere almeno :min caratteri.',\n        'array'   => 'Il campo :attribute deve contenere almeno :min elementi.',\n    ],\n    'not_in'               => 'Il :attribute selezionato non è valido.',\n    'not_regex'            => 'Il formato di :attribute non è valido.',\n    'numeric'              => ':attribute deve essere un numero.',\n    'regex'                => 'Il formato di :attribute non è valido.',\n    'required'             => 'Il campo :attribute è richiesto.',\n    'required_if'          => 'Il campo :attribute è richiesto quando :other è :value.',\n    'required_with'        => 'Il campo :attribute è richiesto quando :values è presente.',\n    'required_with_all'    => 'Il campo :attribute è richiesto quando :values sono presenti.',\n    'required_without'     => 'Il campo :attribute è richiesto quando :values non è presente.',\n    'required_without_all' => 'Il campo :attribute è richiesto quando nessuno dei :values sono presenti.',\n    'same'                 => ':attribute e :other devono corrispondere.',\n    'safe_url'             => 'Il link inserito potrebbe non essere sicuro.',\n    'size'                 => [\n        'numeric' => 'Il campo :attribute deve essere :size.',\n        'file'    => 'Il campo :attribute deve essere :size kilobyte.',\n        'string'  => 'Il campo :attribute deve essere di :size caratteri.',\n        'array'   => 'Il campo :attribute deve contenere :size elementi.',\n    ],\n    'string'               => ':attribute deve essere una stringa.',\n    'timezone'             => ':attribute deve essere una zona valida.',\n    'totp'                 => 'Il codice fornito non è valido o è scaduto.',\n    'unique'               => ':attribute è già usato.',\n    'url'                  => 'Il formato :attribute non è valido.',\n    'uploaded'             => 'Il file non può essere caricato. Il server potrebbe non accettare file di questa dimensione.',\n\n    'zip_file' => 'L\\'attributo :attribute deve fare riferimento a un file all\\'interno dello ZIP.',\n    'zip_file_size' => 'Il file :attribute non deve superare :size MB.',\n    'zip_file_mime' => 'Il campo :attribute deve fare riferimento a un file di tipo :validTypes, trovato :foundType.',\n    'zip_model_expected' => 'Oggetto dati atteso ma \":type\" trovato.',\n    'zip_unique' => 'L\\'attributo :attribute deve essere univoco per il tipo di oggetto all\\'interno dello ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Conferma della password richiesta',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/ja/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'がページを作成:',\n    'page_create_notification'    => 'ページを作成しました',\n    'page_update'                 => 'がページを更新:',\n    'page_update_notification'    => 'ページを更新しました',\n    'page_delete'                 => 'がページを削除:',\n    'page_delete_notification'    => 'ページを削除しました',\n    'page_restore'                => 'がページを復元:',\n    'page_restore_notification'   => 'ページを復元しました',\n    'page_move'                   => 'がページを移動:',\n    'page_move_notification'      => 'ページを移動しました',\n\n    // Chapters\n    'chapter_create'              => 'がチャプターを作成:',\n    'chapter_create_notification' => 'チャプターを作成しました',\n    'chapter_update'              => 'がチャプターを更新:',\n    'chapter_update_notification' => 'チャプターを更新しました',\n    'chapter_delete'              => 'がチャプターを削除:',\n    'chapter_delete_notification' => 'チャプターを削除しました',\n    'chapter_move'                => 'がチャプターを移動:',\n    'chapter_move_notification' => 'チャプタを移動しました',\n\n    // Books\n    'book_create'                 => 'がブックを作成:',\n    'book_create_notification'    => 'ブックを作成しました',\n    'book_create_from_chapter'              => 'がチャプターをブックに変換:',\n    'book_create_from_chapter_notification' => 'チャプターをブックに変換しました',\n    'book_update'                 => 'がブックを更新:',\n    'book_update_notification'    => 'ブックを更新しました',\n    'book_delete'                 => 'がブックを削除:',\n    'book_delete_notification'    => 'ブックを削除しました',\n    'book_sort'                   => 'がブック内の並び順を変更:',\n    'book_sort_notification'      => 'ブック内の並び順を変更しました',\n\n    // Bookshelves\n    'bookshelf_create'            => 'が本棚を作成:',\n    'bookshelf_create_notification'    => '本棚を作成しました',\n    'bookshelf_create_from_book'    => 'がブックを本棚に変換:',\n    'bookshelf_create_from_book_notification'    => 'ブックを本棚に変換しました',\n    'bookshelf_update'                 => 'が本棚を更新:',\n    'bookshelf_update_notification'    => '本棚を更新しました',\n    'bookshelf_delete'                 => 'が本棚を削除:',\n    'bookshelf_delete_notification'    => '本棚を削除しました',\n\n    // Revisions\n    'revision_restore' => 'がリビジョンを復元',\n    'revision_delete' => 'がリビジョンを削除',\n    'revision_delete_notification' => 'リビジョンを削除しました',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\"がお気に入りに追加されました',\n    'favourite_remove_notification' => '\":name\"がお気に入りから削除されました',\n\n    // Watching\n    'watch_update_level_notification' => 'ウォッチ設定を更新しました',\n\n    // Auth\n    'auth_login' => 'がログイン',\n    'auth_register' => 'が新規ユーザ登録',\n    'auth_password_reset_request' => 'がパスワードリセットを要求',\n    'auth_password_reset_update' => 'がパスワードをリセット',\n    'mfa_setup_method' => 'が多要素認証を設定',\n    'mfa_setup_method_notification' => '多要素認証を設定しました',\n    'mfa_remove_method' => 'が多要素認証を削除',\n    'mfa_remove_method_notification' => '多要素認証を解除しました',\n\n    // Settings\n    'settings_update' => 'が設定を更新',\n    'settings_update_notification' => '設定を更新しました',\n    'maintenance_action_run' => 'がメンテナンス作業を実施',\n\n    // Webhooks\n    'webhook_create' => 'がWebhookを作成',\n    'webhook_create_notification' => 'Webhookを作成しました',\n    'webhook_update' => 'がWebhookを更新',\n    'webhook_update_notification' => 'Webhookを更新しました',\n    'webhook_delete' => 'がWebhookを削除',\n    'webhook_delete_notification' => 'Webhookを削除しました',\n\n    // Imports\n    'import_create' => 'がインポートを作成',\n    'import_create_notification' => 'インポートファイルが正常にアップロードされました',\n    'import_run' => 'がインポートを更新',\n    'import_run_notification' => 'コンテンツが正常にインポートされました',\n    'import_delete' => 'がインポートを削除',\n    'import_delete_notification' => 'インポートファイルが正常に削除されました',\n\n    // Users\n    'user_create' => 'がユーザを作成',\n    'user_create_notification' => 'ユーザーを作成しました',\n    'user_update' => 'がユーザを更新',\n    'user_update_notification' => 'ユーザーを更新しました',\n    'user_delete' => 'がユーザを削除',\n    'user_delete_notification' => 'ユーザーを削除しました',\n\n    // API Tokens\n    'api_token_create' => 'がAPIトークンを作成',\n    'api_token_create_notification' => 'APIトークンを作成しました',\n    'api_token_update' => 'がAPIトークンを更新',\n    'api_token_update_notification' => 'APIトークンを更新しました',\n    'api_token_delete' => 'がAPIトークンを削除',\n    'api_token_delete_notification' => 'APIトークンを削除しました',\n\n    // Roles\n    'role_create' => 'が役割を作成',\n    'role_create_notification' => '役割を作成しました',\n    'role_update' => 'が役割を更新',\n    'role_update_notification' => '役割を更新しました',\n    'role_delete' => 'が役割を削除',\n    'role_delete_notification' => '役割を削除しました',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'がゴミ箱を空にしました',\n    'recycle_bin_restore' => 'がゴミ箱から復元',\n    'recycle_bin_destroy' => 'がゴミ箱から完全に削除',\n\n    // Comments\n    'commented_on'                => 'がコメント:',\n    'comment_create'              => 'がコメントを追加',\n    'comment_update'              => 'がコメントを更新',\n    'comment_delete'              => 'がコメントを削除',\n\n    // Sort Rules\n    'sort_rule_create' => 'がソートルールを作成',\n    'sort_rule_create_notification' => 'ソートルールを作成しました',\n    'sort_rule_update' => 'がソートルールを更新',\n    'sort_rule_update_notification' => 'ソートルールを更新しました',\n    'sort_rule_delete' => 'がソートルールを削除',\n    'sort_rule_delete_notification' => 'ソートルールを削除しました',\n\n    // Other\n    'permissions_update'          => 'が権限を更新:',\n];\n"
  },
  {
    "path": "lang/ja/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'この資格情報は登録されていません。',\n    'throttle' => 'ログイン試行回数が制限を超えました。:seconds秒後に再試行してください。',\n\n    // Login & Register\n    'sign_up' => '新規登録',\n    'log_in' => 'ログイン',\n    'log_in_with' => ':socialDriverでログイン',\n    'sign_up_with' => ':socialDriverで登録',\n    'logout' => 'ログアウト',\n\n    'name' => '名前',\n    'username' => 'ユーザ名',\n    'email' => 'メールアドレス',\n    'password' => 'パスワード',\n    'password_confirm' => 'パスワード (確認)',\n    'password_hint' => '8文字以上で設定する必要があります',\n    'forgot_password' => 'パスワードをお忘れですか？',\n    'remember_me' => 'ログイン情報を保存する',\n    'ldap_email_hint' => 'このアカウントで使用するEメールアドレスを入力してください。',\n    'create_account' => 'アカウント作成',\n    'already_have_account' => 'すでにアカウントをお持ちですか？',\n    'dont_have_account' => '初めての登録ですか?',\n    'social_login' => 'SNSログイン',\n    'social_registration' => 'SNS登録',\n    'social_registration_text' => '他のサービスで登録 / ログインする',\n\n    'register_thanks' => '登録が完了しました！',\n    'register_confirm' => 'メール内の確認ボタンを押して、:appNameへアクセスしてください。',\n    'registrations_disabled' => '登録は現在停止中です。',\n    'registration_email_domain_invalid' => 'このEmailドメインでの登録は許可されていません。',\n    'register_success' => '登録が完了し、ログインできるようになりました！',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'ログイン試行中',\n    'auto_init_starting_desc' => 'ログイン プロセスを開始するために、認証システムに接続しています。5秒経過しても進行しない場合、下のリンクをクリックしてみてください。',\n    'auto_init_start_link' => '認証を進める',\n\n    // Password Reset\n    'reset_password' => 'パスワードリセット',\n    'reset_password_send_instructions' => '以下にEメールアドレスを入力すると、パスワードリセットリンクが記載されたメールが送信されます。',\n    'reset_password_send_button' => 'リセットリンクを送信',\n    'reset_password_sent' => 'メールアドレスがシステムで見つかった場合、パスワードリセットリンクが:emailに送信されます。',\n    'reset_password_success' => 'パスワードがリセットされました。',\n    'email_reset_subject' => ':appNameのパスワードをリセット',\n    'email_reset_text' => 'このメールは、パスワードリセットがリクエストされたため送信されています。',\n    'email_reset_not_requested' => 'もしパスワードリセットを希望しない場合、操作は不要です。',\n\n    // Email Confirmation\n    'email_confirm_subject' => ':appNameのメールアドレス確認',\n    'email_confirm_greeting' => ':appNameへ登録してくださりありがとうございます！',\n    'email_confirm_text' => '以下のボタンを押し、メールアドレスを確認してください:',\n    'email_confirm_action' => 'メールアドレスを確認',\n    'email_confirm_send_error' => 'Eメールの確認が必要でしたが、システム上でEメールの送信ができませんでした。管理者に連絡し、Eメールが正しく設定されていることを確認してください。',\n    'email_confirm_success' => 'メールアドレスが確認されました！このメールアドレスでログインできるようになりました。',\n    'email_confirm_resent' => '確認メールを再送信しました。受信トレイを確認してください。',\n    'email_confirm_thanks' => '確認いただきありがとうございます！',\n    'email_confirm_thanks_desc' => '確認のため、しばらくお待ちください。3秒後にリダイレクトされない場合は、下の「続ける」リンクを押して次に進んでください。',\n\n    'email_not_confirmed' => 'Eメールアドレスが確認できていません',\n    'email_not_confirmed_text' => 'Eメールアドレスの確認が完了していません。',\n    'email_not_confirmed_click_link' => '登録時に受信したメールを確認し、確認リンクをクリックしてください。',\n    'email_not_confirmed_resend' => 'Eメールが見つからない場合、以下のフォームから再送信してください。',\n    'email_not_confirmed_resend_button' => '確認メールを再送信',\n\n    // User Invite\n    'user_invite_email_subject' => ':appNameに招待されました！',\n    'user_invite_email_greeting' => ':appNameにあなたのアカウントが作成されました。',\n    'user_invite_email_text' => 'アカウントのパスワードを設定してアクセスできるようにするため、下のボタンをクリックしてください：',\n    'user_invite_email_action' => 'アカウントのパスワード設定',\n    'user_invite_page_welcome' => ':appNameへようこそ！',\n    'user_invite_page_text' => 'アカウントの設定を完了してアクセスするには、今後の訪問時に:appNameにログインするためのパスワードを設定する必要があります。',\n    'user_invite_page_confirm_button' => 'パスワードを確定',\n    'user_invite_success_login' => 'パスワードが設定されました。設定したパスワードで:appNameにログインできるようになりました！',\n\n    // Multi-factor Authentication\n    'mfa_setup' => '多要素認証を設定',\n    'mfa_setup_desc' => 'アカウントのセキュリティを強化するために、多要素認証を設定してください。',\n    'mfa_setup_configured' => '既に設定されています',\n    'mfa_setup_reconfigure' => '再設定',\n    'mfa_setup_remove_confirmation' => 'この多要素認証方法を削除してもよろしいですか？',\n    'mfa_setup_action' => '設定',\n    'mfa_backup_codes_usage_limit_warning' => '有効な確認コードは残り5つ以下です。アカウントのロックアウトを防ぐため、コードがなくなる前に新しいセットを生成して保存してください。',\n    'mfa_option_totp_title' => 'モバイルアプリ',\n    'mfa_option_totp_desc' => '多要素認証を使用するには、Google Authenticator、Authy、Microsoft AuthenticatorなどのTOTPをサポートするモバイルアプリケーションが必要です。',\n    'mfa_option_backup_codes_title' => '確認コード',\n    'mfa_option_backup_codes_desc' => 'ログイン時に本人確認のために追加入力する1回限りの確認コードセットを生成します。 これは安全な場所に保管してください。',\n    'mfa_gen_confirm_and_enable' => '確認して有効化',\n    'mfa_gen_backup_codes_title' => '確認コードの構成',\n    'mfa_gen_backup_codes_desc' => '以下のコードのリストを安全な場所に保管してください。システムにアクセスする際、コードのいずれかを第二の認証手段として使用できます。',\n    'mfa_gen_backup_codes_download' => 'コードをダウンロード',\n    'mfa_gen_backup_codes_usage_warning' => '各コードは一度だけ使用できます',\n    'mfa_gen_totp_title' => 'モバイルアプリの設定',\n    'mfa_gen_totp_desc' => '多要素認証を使用するには、Google Authenticator、Authy、Microsoft AuthenticatorなどのTOTPをサポートするモバイルアプリケーションが必要です。',\n    'mfa_gen_totp_scan' => '利用したい認証アプリで以下のQRコードをスキャンしてください。',\n    'mfa_gen_totp_verify_setup' => '設定を検証',\n    'mfa_gen_totp_verify_setup_desc' => '認証アプリで生成されたコードを下の入力ボックスに入力し、すべてが機能していることを確認してください。',\n    'mfa_gen_totp_provide_code_here' => 'アプリが生成したコードを入力',\n    'mfa_verify_access' => 'アクセスを確認',\n    'mfa_verify_access_desc' => 'このユーザーアカウントはアクセスを許可する前に追加の検証レベルで本人確認を行う必要があります。続行するには、設定されているいずれかの手段で検証してください。',\n    'mfa_verify_no_methods' => '手段が設定されていません',\n    'mfa_verify_no_methods_desc' => 'アカウントの多要素認証手段が見つかりませんでした。アクセスする前に、少なくとも1つの手段を設定する必要があります。',\n    'mfa_verify_use_totp' => 'モバイルアプリを利用して確認',\n    'mfa_verify_use_backup_codes' => '確認コードによる検証',\n    'mfa_verify_backup_code' => '確認コード',\n    'mfa_verify_backup_code_desc' => '残りの確認コードのいずれかを入力してください:',\n    'mfa_verify_backup_code_enter_here' => '確認コードを入力',\n    'mfa_verify_totp_desc' => 'モバイルアプリを利用して生成されたコードを入力してください:',\n    'mfa_setup_login_notification' => '多要素認証が構成されました。設定された手段を利用して再度ログインしてください。',\n];\n"
  },
  {
    "path": "lang/ja/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'キャンセル',\n    'close' => '閉じる',\n    'confirm' => '確認',\n    'back' => '戻る',\n    'save' => '保存',\n    'continue' => '続ける',\n    'select' => '選択',\n    'toggle_all' => '一括切替',\n    'more' => 'その他',\n\n    // Form Labels\n    'name' => '名称',\n    'description' => '概要',\n    'role' => '権限',\n    'cover_image' => 'カバー画像',\n    'cover_image_description' => 'この画像はおよそ440x250pxであるべきですが、必要に応じてさまざまなシナリオでユーザー・インターフェースに合うように柔軟に拡大・縮小されるため、実際の表示寸法は異なります。',\n\n    // Actions\n    'actions' => '実行',\n    'view' => '表示',\n    'view_all' => 'すべて表示',\n    'new' => '新規作成',\n    'create' => '作成',\n    'update' => '更新',\n    'edit' => '編集',\n    'archive' => 'アーカイブ',\n    'unarchive' => 'アーカイブ解除',\n    'sort' => '並び順',\n    'move' => '移動',\n    'copy' => 'コピー',\n    'reply' => '返信',\n    'delete' => '削除',\n    'delete_confirm' => '確認して削除',\n    'search' => '検索',\n    'search_clear' => '検索をクリア',\n    'reset' => 'リセット',\n    'remove' => '削除',\n    'add' => '追加',\n    'configure' => '設定',\n    'manage' => '管理',\n    'fullscreen' => '全画面',\n    'favourite' => 'お気に入り',\n    'unfavourite' => 'お気に入りから削除',\n    'next' => '次へ',\n    'previous' => '前へ',\n    'filter_active' => '有効なフィルター:',\n    'filter_clear' => 'フィルターを解除',\n    'download' => 'ダウンロード',\n    'open_in_tab' => 'タブで開く',\n    'open' => '開く',\n\n    // Sort Options\n    'sort_options' => '並べ替えオプション',\n    'sort_direction_toggle' => '並べ替え方向の切り替え',\n    'sort_ascending' => '昇順に並べ替え',\n    'sort_descending' => '降順に並べ替え',\n    'sort_name' => '名前',\n    'sort_default' => 'デフォルト',\n    'sort_created_at' => '作成日',\n    'sort_updated_at' => '更新日',\n\n    // Misc\n    'deleted_user' => '削除済みユーザ',\n    'no_activity' => '表示するアクティビティがありません',\n    'no_items' => 'アイテムはありません',\n    'back_to_top' => '上に戻る',\n    'skip_to_main_content' => 'メインコンテンツへスキップ',\n    'toggle_details' => '概要の表示切替',\n    'toggle_thumbnails' => 'サムネイルの切り替え',\n    'details' => '詳細',\n    'grid_view' => 'グリッド形式',\n    'list_view' => 'リスト形式',\n    'default' => 'デフォルト',\n    'breadcrumb' => 'パンくずリスト',\n    'status' => '状態',\n    'status_active' => '有効',\n    'status_inactive' => '無効',\n    'never' => '該当なし',\n    'none' => 'なし',\n\n    // Header\n    'homepage' => 'ホームページ',\n    'header_menu_expand' => 'ヘッダーメニューを展開',\n    'profile_menu' => 'プロフィールメニュー',\n    'view_profile' => 'プロフィール表示',\n    'edit_profile' => 'プロフィール編集',\n    'dark_mode' => 'ダークモード',\n    'light_mode' => 'ライトモード',\n    'global_search' => 'グローバル検索',\n\n    // Layout tabs\n    'tab_info' => '情報',\n    'tab_info_label' => 'タブ: サブコンテンツを表示',\n    'tab_content' => '内容',\n    'tab_content_label' => 'タブ: メインコンテンツを表示',\n\n    // Email Content\n    'email_action_help' => '\":actionText\" をクリックできない場合、以下のURLをコピーしブラウザで開いてください:',\n    'email_rights' => 'All rights reserved',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'プライバシーポリシー',\n    'terms_of_service' => '利用規約',\n\n    // OpenSearch\n    'opensearch_description' => ':appName を検索',\n];\n"
  },
  {
    "path": "lang/ja/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => '画像を選択',\n    'image_list' => '画像リスト',\n    'image_details' => '画像詳細',\n    'image_upload' => '画像をアップロード',\n    'image_intro' => 'ここでは、システムに以前アップロードされた画像を選択して管理できます。',\n    'image_intro_upload' => 'このウィンドウに画像ファイルをドラッグするか、上の「画像をアップロード」ボタンを使用して新しい画像をアップロードします。',\n    'image_all' => 'すべて',\n    'image_all_title' => '全ての画像を表示',\n    'image_book_title' => 'このブックにアップロードされた画像を表示',\n    'image_page_title' => 'このページにアップロードされた画像を表示',\n    'image_search_hint' => '画像名で検索',\n    'image_uploaded' => 'アップロード日時: :uploadedDate',\n    'image_uploaded_by' => 'アップロードユーザ: :userName',\n    'image_uploaded_to' => 'アップロード先: :pageLink',\n    'image_updated' => '更新日時: :updateDate',\n    'image_load_more' => 'さらに読み込む',\n    'image_image_name' => '画像名',\n    'image_delete_used' => 'この画像は以下のページで利用されています。',\n    'image_delete_confirm_text' => 'この画像を削除してもよろしいですか？',\n    'image_select_image' => '画像を選択',\n    'image_dropzone' => '画像をドロップするか、クリックしてアップロード',\n    'image_dropzone_drop' => 'アップロードする画像をここにドロップ',\n    'images_deleted' => '画像を削除しました',\n    'image_preview' => '画像プレビュー',\n    'image_upload_success' => '画像がアップロードされました',\n    'image_update_success' => '画像が更新されました',\n    'image_delete_success' => '画像が削除されました',\n    'image_replace' => '画像の差し替え',\n    'image_replace_success' => '画像を更新しました',\n    'image_rebuild_thumbs' => 'サイズバリエーションを再生成',\n    'image_rebuild_thumbs_success' => '画像サイズバリエーションの再構築に成功しました！',\n\n    // Code Editor\n    'code_editor' => 'コードを編集する',\n    'code_language' => 'プログラミング言語の選択',\n    'code_content' => 'プログラム内容',\n    'code_session_history' => 'セッション履歴',\n    'code_save' => 'プログラムを保存',\n];\n"
  },
  {
    "path": "lang/ja/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => '一般',\n    'advanced' => '詳細設定',\n    'none' => 'なし',\n    'cancel' => '取消',\n    'save' => '保存',\n    'close' => '閉じる',\n    'apply' => '適用',\n    'undo' => '元に戻す',\n    'redo' => 'やり直し',\n    'left' => '左寄せ',\n    'center' => '中央揃え',\n    'right' => '右寄せ',\n    'top' => '上',\n    'middle' => '中央',\n    'bottom' => '下',\n    'width' => '幅',\n    'height' => '高さ',\n    'More' => 'さらに表示',\n    'select' => '選択...',\n\n    // Toolbar\n    'formats' => '書式',\n    'header_large' => '大見出し',\n    'header_medium' => '中見出し',\n    'header_small' => '小見出し',\n    'header_tiny' => '極小見出し',\n    'paragraph' => '段落',\n    'blockquote' => '引用',\n    'inline_code' => 'インラインコード',\n    'callouts' => 'コールアウト',\n    'callout_information' => '情報',\n    'callout_success' => '成功',\n    'callout_warning' => '警告',\n    'callout_danger' => '危険',\n    'bold' => '太字',\n    'italic' => '斜体',\n    'underline' => '下線',\n    'strikethrough' => '取消線',\n    'superscript' => '上付き',\n    'subscript' => '下付き',\n    'text_color' => 'テキスト色',\n    'highlight_color' => 'テキスト背景色',\n    'custom_color' => 'カスタムカラー',\n    'remove_color' => '色設定を解除',\n    'background_color' => '背景色',\n    'align_left' => '左揃え',\n    'align_center' => '中央揃え',\n    'align_right' => '右揃え',\n    'align_justify' => '両端揃え',\n    'list_bullet' => '箇条書き',\n    'list_numbered' => '番号付き箇条書き',\n    'list_task' => 'タスクリスト',\n    'indent_increase' => 'インデントを増やす',\n    'indent_decrease' => 'インデントを減らす',\n    'table' => '表',\n    'insert_image' => '画像の挿入',\n    'insert_image_title' => '画像の挿入・編集',\n    'insert_link' => 'リンクの挿入・編集',\n    'insert_link_title' => 'リンクの挿入・編集',\n    'insert_horizontal_line' => '水平線を挿入',\n    'insert_code_block' => 'コードブロックを挿入',\n    'edit_code_block' => 'コードブロックを編集',\n    'insert_drawing' => '描画を挿入・編集',\n    'drawing_manager' => '描画マネージャー',\n    'insert_media' => 'メディアの挿入・編集',\n    'insert_media_title' => 'メディアの挿入・編集',\n    'clear_formatting' => '書式をクリア',\n    'source_code' => 'ソースコード',\n    'source_code_title' => 'ソースコード',\n    'fullscreen' => '全画面表示',\n    'image_options' => '画像オプション',\n\n    // Tables\n    'table_properties' => '表の詳細設定',\n    'table_properties_title' => '表の詳細設定',\n    'delete_table' => '表の削除',\n    'table_clear_formatting' => '表の書式をクリア',\n    'resize_to_contents' => '内容に合わせてリサイズ',\n    'row_header' => 'ヘッダー行',\n    'insert_row_before' => '上側に行を挿入',\n    'insert_row_after' => '下側に行を挿入',\n    'delete_row' => '行の削除',\n    'insert_column_before' => '左側に列を挿入',\n    'insert_column_after' => '右側に列を挿入',\n    'delete_column' => '列の削除',\n    'table_cell' => 'セル',\n    'table_row' => '行',\n    'table_column' => '列',\n    'cell_properties' => 'セルの詳細設定',\n    'cell_properties_title' => 'セルの詳細設定',\n    'cell_type' => 'セルタイプ',\n    'cell_type_cell' => 'セル',\n    'cell_scope' => '見出しとの関連',\n    'cell_type_header' => 'ヘッダーセル',\n    'merge_cells' => 'セルを結合',\n    'split_cell' => 'セルを分割',\n    'table_row_group' => '行グループ',\n    'table_column_group' => '列グループ',\n    'horizontal_align' => '水平方向の配置',\n    'vertical_align' => '垂直方向の配置',\n    'border_width' => '枠線幅',\n    'border_style' => '枠線スタイル',\n    'border_color' => '枠線の色',\n    'row_properties' => '行の詳細設定',\n    'row_properties_title' => '行の詳細設定',\n    'cut_row' => '行の切り取り',\n    'copy_row' => '行のコピー',\n    'paste_row_before' => '上側に行を貼り付け',\n    'paste_row_after' => '下側に行を貼り付け',\n    'row_type' => '行タイプ',\n    'row_type_header' => 'ヘッダー',\n    'row_type_body' => 'ボディー',\n    'row_type_footer' => 'フッター',\n    'alignment' => '配置',\n    'cut_column' => '列の切り取り',\n    'copy_column' => '列のコピー',\n    'paste_column_before' => '左側に列を貼り付け',\n    'paste_column_after' => '右側に列を貼り付け',\n    'cell_padding' => 'セル内余白（パディング）',\n    'cell_spacing' => 'セルの間隔',\n    'caption' => '表題',\n    'show_caption' => 'キャプションの表示',\n    'constrain' => '縦横比を保持する',\n    'cell_border_solid' => '実線',\n    'cell_border_dotted' => '点線',\n    'cell_border_dashed' => '破線',\n    'cell_border_double' => '二重線',\n    'cell_border_groove' => '掘り',\n    'cell_border_ridge' => '盛り',\n    'cell_border_inset' => '陥没',\n    'cell_border_outset' => '隆起',\n    'cell_border_none' => 'なし',\n    'cell_border_hidden' => 'なし（優先）',\n\n    // Images, links, details/summary & embed\n    'source' => '画像のソース',\n    'alt_desc' => '代替の説明文',\n    'embed' => '埋め込み',\n    'paste_embed' => '埋め込み用コードを下記に貼り付けてください。',\n    'url' => 'リンク先URL',\n    'text_to_display' => 'リンク元テキスト',\n    'title' => 'タイトル',\n    'browse_links' => 'エンティティを参照',\n    'open_link' => 'リンクを開く',\n    'open_link_in' => 'リンク先の表示場所',\n    'open_link_current' => '同じウィンドウ',\n    'open_link_new' => '新規ウィンドウ',\n    'remove_link' => 'リンクを削除',\n    'insert_collapsible' => '折りたたみブロックを追加',\n    'collapsible_unwrap' => 'ブロックの解除',\n    'edit_label' => 'ラベルを編集',\n    'toggle_open_closed' => '折りたたみ状態の切替',\n    'collapsible_edit' => '折りたたみブロックを編集',\n    'toggle_label' => 'ブロックのラベル',\n\n    // About view\n    'about' => 'エディタについて',\n    'about_title' => 'WYSIWYGエディタについて',\n    'editor_license' => 'エディタのライセンスと著作権',\n    'editor_lexical_license' => 'このエディタはMITライセンスの下で配布されている :lexicalLink のフォークとして構築されています。',\n    'editor_lexical_license_link' => '完全なライセンスの詳細はこちらをご覧ください。',\n    'editor_tiny_license' => 'このエディタはMITライセンスの下で提供される:tinyLinkを利用して構築されています。',\n    'editor_tiny_license_link' => 'TinyMCEの著作権およびライセンスの詳細は、こちらをご覧ください。',\n    'save_continue' => 'ページを保存して続行',\n    'callouts_cycle' => '(押し続けて種類を切り替え)',\n    'link_selector' => 'コンテンツへのリンク',\n    'shortcuts' => 'ショートカット',\n    'shortcut' => 'ショートカット',\n    'shortcuts_intro' => 'エディタでは次に示すショートカットが利用できます。',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => '説明',\n];\n"
  },
  {
    "path": "lang/ja/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => '最近作成',\n    'recently_created_pages' => '最近作成されたページ',\n    'recently_updated_pages' => '最近更新されたページ',\n    'recently_created_chapters' => '最近作成されたチャプター',\n    'recently_created_books' => '最近作成されたブック',\n    'recently_created_shelves' => '最近作成された本棚',\n    'recently_update' => '最近更新',\n    'recently_viewed' => '閲覧履歴',\n    'recent_activity' => 'アクティビティ',\n    'create_now' => '作成する',\n    'revisions' => '編集履歴',\n    'meta_revision' => 'リビジョン #:revisionCount',\n    'meta_created' => '作成: :timeLength',\n    'meta_created_name' => '作成: :timeLength (:user)',\n    'meta_updated' => '更新: :timeLength',\n    'meta_updated_name' => '更新: :timeLength (:user)',\n    'meta_owned_name' => '所有者: :user',\n    'meta_reference_count' => ':count 項目から参照|:count 項目から参照',\n    'entity_select' => 'エンティティ選択',\n    'entity_select_lack_permission' => 'この項目を選択するために必要な権限がありません',\n    'images' => '画像',\n    'my_recent_drafts' => '最近の下書き',\n    'my_recently_viewed' => '閲覧履歴',\n    'my_most_viewed_favourites' => '最も閲覧したお気に入り',\n    'my_favourites' => 'お気に入り',\n    'no_pages_viewed' => 'なにもページを閲覧していません',\n    'no_pages_recently_created' => '最近作成されたページはありません',\n    'no_pages_recently_updated' => '最近更新されたページはありません。',\n    'export' => 'エクスポート',\n    'export_html' => 'Webページ',\n    'export_pdf' => 'PDF',\n    'export_text' => 'テキストファイル',\n    'export_md' => 'Markdown',\n    'export_zip' => 'ポータブルZIP',\n    'default_template' => 'デフォルトページテンプレート',\n    'default_template_explain' => 'このアイテム内に新しいページを作成する際にデフォルトコンテンツとして使用されるページテンプレートを割り当てます。これはページ作成者が選択したテンプレートページへのアクセス権を持つ場合にのみ使用されることに注意してください。',\n    'default_template_select' => 'テンプレートページを選択',\n    'import' => 'インポート',\n    'import_validate' => 'インポートの検証',\n    'import_desc' => 'ポータブルzipエクスポートファイルを使用して、同一または別のインスタンスからブック、チャプタ、ページをインポートします。続行するにはZIPファイルを選択します。 ファイルをアップロードすると検証が行われ、次のビューでインポートの設定と確認ができます。',\n    'import_zip_select' => 'アップロードするZIPファイルの選択',\n    'import_zip_validation_errors' => '指定された ZIP ファイルの検証中にエラーが検出されました:',\n    'import_pending' => '保留中のインポート',\n    'import_pending_none' => 'インポートは行われていません。',\n    'import_continue' => 'インポートを続行',\n    'import_continue_desc' => 'アップロードされたZIPファイルからインポートされるコンテンツを確認してください。準備ができたらインポートを実行してこのシステムにコンテンツを追加します。 アップロードされたZIPファイルは、インポートが成功すると自動的に削除されます。',\n    'import_details' => 'インポートの詳細',\n    'import_run' => 'インポートを実行',\n    'import_size' => 'インポートZIPサイズ: :size',\n    'import_uploaded_at' => 'アップロード: :relativeTime',\n    'import_uploaded_by' => 'アップロードユーザ:',\n    'import_location' => 'インポートの場所',\n    'import_location_desc' => 'コンテンツをインポートする場所を選択します。選択した場所に作成するための権限を持つ必要があります。',\n    'import_delete_confirm' => 'このインポートを削除してもよろしいですか？',\n    'import_delete_desc' => 'アップロードされたインポートZIPファイルは削除され、元に戻すことはできません。',\n    'import_errors' => 'インポートエラー',\n    'import_errors_desc' => 'インポート中に次のエラーが発生しました：',\n    'breadcrumb_siblings_for_page' => '階層内のページ',\n    'breadcrumb_siblings_for_chapter' => '階層内のチャプタ',\n    'breadcrumb_siblings_for_book' => '階層内のブック',\n    'breadcrumb_siblings_for_bookshelf' => '階層内の棚',\n\n    // Permissions and restrictions\n    'permissions' => '権限',\n    'permissions_desc' => 'ユーザーの役割によって提供されるデフォルトの権限を上書きするため、ここで権限を設定します。',\n    'permissions_book_cascade' => 'ブックに設定された権限は、子チャプターや子ページに独自の権限が定義されていない限り、自動的に子チャプターや子ページに継承されます。',\n    'permissions_chapter_cascade' => 'チャプターに設定された権限は、子ページに独自の権限が定義されていない限り、自動的に子ページに継承されます。',\n    'permissions_save' => '権限を保存',\n    'permissions_owner' => '所有者',\n    'permissions_role_everyone_else' => 'その他の全員',\n    'permissions_role_everyone_else_desc' => '明示的に上書きされていないすべての役割の権限を設定します。',\n    'permissions_role_override' => '権限を上書きする役割',\n    'permissions_inherit_defaults' => 'デフォルトを継承',\n\n    // Search\n    'search_results' => '検索結果',\n    'search_total_results_found' => ':count件見つかりました',\n    'search_clear' => '検索をクリア',\n    'search_no_pages' => 'ページが見つかりませんでした。',\n    'search_for_term' => ':term の検索結果',\n    'search_more' => 'さらに表示',\n    'search_advanced' => '高度な検索',\n    'search_terms' => '検索語句',\n    'search_content_type' => '種類',\n    'search_exact_matches' => '完全一致',\n    'search_tags' => 'タグ検索',\n    'search_options' => 'オプション',\n    'search_viewed_by_me' => '自分が閲覧したことがある',\n    'search_not_viewed_by_me' => '自分が閲覧したことがない',\n    'search_permissions_set' => '権限が設定されている',\n    'search_created_by_me' => '自分が作成した',\n    'search_updated_by_me' => '自分が更新した',\n    'search_owned_by_me' => '自分が所有している',\n    'search_date_options' => '日付オプション',\n    'search_updated_before' => '以前に更新',\n    'search_updated_after' => '以降に更新',\n    'search_created_before' => '以前に作成',\n    'search_created_after' => '以降に更新',\n    'search_set_date' => '日付を設定',\n    'search_update' => 'フィルタを更新',\n\n    // Shelves\n    'shelf' => '本棚',\n    'shelves' => '本棚',\n    'x_shelves' => ':count 本棚|:count 本棚',\n    'shelves_empty' => '本棚が作成されていません',\n    'shelves_create' => '新しい本棚を作成',\n    'shelves_popular' => '人気の本棚',\n    'shelves_new' => '新しい本棚',\n    'shelves_new_action' => '新しい本棚',\n    'shelves_popular_empty' => 'ここに人気の本棚が表示されます。',\n    'shelves_new_empty' => '最近作成された本棚がここに表示されます。',\n    'shelves_save' => '本棚を保存',\n    'shelves_books' => 'この本棚のブック',\n    'shelves_add_books' => 'この本棚にブックを追加',\n    'shelves_drag_books' => '下にブックをドラッグしてこの本棚に追加',\n    'shelves_empty_contents' => 'この本棚にはブックが割り当てられていません。',\n    'shelves_edit_and_assign' => '本棚を編集してブックを割り当てる',\n    'shelves_edit_named' => '本棚「:name」を編集',\n    'shelves_edit' => '本棚を編集',\n    'shelves_delete' => '本棚を削除',\n    'shelves_delete_named' => '本棚「:name」を削除',\n    'shelves_delete_explain' => \"これにより、この本棚「:name」が削除されます。含まれているブックは削除されません。\",\n    'shelves_delete_confirmation' => '本当にこの本棚を削除してよろしいですか？',\n    'shelves_permissions' => '本棚の権限',\n    'shelves_permissions_updated' => '本棚の権限を更新しました',\n    'shelves_permissions_active' => '本棚の権限は有効です',\n    'shelves_permissions_cascade_warning' => '本棚の権限は含まれる本には自動的に継承されません。これは、1つのブックが複数の本棚に存在する可能性があるためです。ただし、以下のオプションを使用すると権限を子ブックにコピーできます。',\n    'shelves_permissions_create' => '本棚の作成権限は、以下のアクションを使用した子ブックへの権限コピーにのみ使用されます。これはブックの作成を制御するものではありません。',\n    'shelves_copy_permissions_to_books' => 'ブックに権限をコピー',\n    'shelves_copy_permissions' => '権限をコピー',\n    'shelves_copy_permissions_explain' => 'これにより、この本棚の現在の権限設定を本棚に含まれるすべてのブックに適用します。有効にする前に、この本棚の権限への変更が保存されていることを確認してください。',\n    'shelves_copy_permission_success' => '本棚の権限が:count個のブックにコピーされました',\n\n    // Books\n    'book' => 'ブック',\n    'books' => 'ブック',\n    'x_books' => ':count ブック',\n    'books_empty' => 'まだブックは作成されていません',\n    'books_popular' => '人気のブック',\n    'books_recent' => '最近のブック',\n    'books_new' => '新しいブック',\n    'books_new_action' => '新しいブック',\n    'books_popular_empty' => 'ここに人気のブックが表示されます。',\n    'books_new_empty' => '最近作成されたブックがここに表示されます。',\n    'books_create' => '新しいブックを作成',\n    'books_delete' => 'ブックを削除',\n    'books_delete_named' => 'ブック「:bookName」を削除',\n    'books_delete_explain' => '「:bookName」を削除すると、ブック内のページとチャプターも削除されます。',\n    'books_delete_confirmation' => '本当にこのブックを削除してよろしいですか？',\n    'books_edit' => 'ブックを編集',\n    'books_edit_named' => 'ブック「:bookName」を編集',\n    'books_form_book_name' => 'ブック名',\n    'books_save' => 'ブックを保存',\n    'books_permissions' => 'ブックの権限',\n    'books_permissions_updated' => 'ブックの権限を更新しました',\n    'books_empty_contents' => 'まだページまたはチャプターが作成されていません。',\n    'books_empty_create_page' => '新しいページを作成',\n    'books_empty_sort_current_book' => 'ブックの並び順を変更',\n    'books_empty_add_chapter' => 'チャプターを追加',\n    'books_permissions_active' => 'ブックの権限は有効です',\n    'books_search_this' => 'このブックから検索',\n    'books_navigation' => '目次',\n    'books_sort' => '並び順を変更',\n    'books_sort_desc' => 'ブック内のチャプタおよびページを移動して内容を再編成できます。他のブックを並べて、ブック間でチャプタやページを簡単に移動することもできます。オプションで自動ソートルールを設定すると、変更時にブックの内容を自動的にソートすることができます。',\n    'books_sort_auto_sort' => '自動ソートオプション',\n    'books_sort_auto_sort_active' => '自動ソート有効: :sortName',\n    'books_sort_named' => 'ブック「:bookName」を並べ替え',\n    'books_sort_name' => '名前で並べ替え',\n    'books_sort_created' => '作成日で並べ替え',\n    'books_sort_updated' => '更新日で並べ替え',\n    'books_sort_chapters_first' => 'チャプターを先に',\n    'books_sort_chapters_last' => 'チャプターを後に',\n    'books_sort_show_other' => '他のブックを表示',\n    'books_sort_save' => '並び順を保存',\n    'books_sort_show_other_desc' => 'これらのブックを並べ替え操作に追加すると、簡単にブック間の再編成が可能です。',\n    'books_sort_move_up' => '上に移動',\n    'books_sort_move_down' => '下に移動',\n    'books_sort_move_prev_book' => '前のブックに移動',\n    'books_sort_move_next_book' => '次のブックに移動',\n    'books_sort_move_prev_chapter' => '前のチャプター内に移動',\n    'books_sort_move_next_chapter' => '次のチャプター内に移動',\n    'books_sort_move_book_start' => '本の先頭に移動',\n    'books_sort_move_book_end' => '本の末尾に移動',\n    'books_sort_move_before_chapter' => 'チャプターの前に移動',\n    'books_sort_move_after_chapter' => 'チャプターの後に移動',\n    'books_copy' => 'ブックをコピー',\n    'books_copy_success' => 'ブックが正常にコピーされました',\n\n    // Chapters\n    'chapter' => 'チャプター',\n    'chapters' => 'チャプター',\n    'x_chapters' => ':count チャプター',\n    'chapters_popular' => '人気のチャプター',\n    'chapters_new' => 'チャプターを作成',\n    'chapters_create' => 'チャプターを作成',\n    'chapters_delete' => 'チャプターを削除',\n    'chapters_delete_named' => 'チャプター「:chapterName」を削除',\n    'chapters_delete_explain' => 'これにより、チャプター「:chapterName」が削除されます。このチャプターに存在するページもすべて削除されます。',\n    'chapters_delete_confirm' => 'チャプターを削除してよろしいですか？',\n    'chapters_edit' => 'チャプターを編集',\n    'chapters_edit_named' => 'チャプター「:chapterName」を編集',\n    'chapters_save' => 'チャプターを保存',\n    'chapters_move' => 'チャプターを移動',\n    'chapters_move_named' => 'チャプター「:chapterName」を移動',\n    'chapters_copy' => 'チャプターをコピー',\n    'chapters_copy_success' => 'チャプターが正常にコピーされました',\n    'chapters_permissions' => 'チャプター権限',\n    'chapters_empty' => 'まだチャプター内にページはありません。',\n    'chapters_permissions_active' => 'チャプターの権限は有効です',\n    'chapters_permissions_success' => 'チャプターの権限を更新しました',\n    'chapters_search_this' => 'このチャプターを検索',\n    'chapter_sort_book' => 'ブックを並べ替え',\n\n    // Pages\n    'page' => 'ページ',\n    'pages' => 'ページ',\n    'x_pages' => ':count ページ',\n    'pages_popular' => '人気のページ',\n    'pages_new' => 'ページを作成',\n    'pages_attachments' => '添付',\n    'pages_navigation' => 'ページナビゲーション',\n    'pages_delete' => 'ページを削除',\n    'pages_delete_named' => 'ページ :pageName を削除',\n    'pages_delete_draft_named' => 'ページ :pageName の下書きを削除',\n    'pages_delete_draft' => 'ページの下書きを削除',\n    'pages_delete_success' => 'ページを削除しました',\n    'pages_delete_draft_success' => 'ページの下書きを削除しました',\n    'pages_delete_warning_template' => 'このページは現在、ブックまたはチャプタのデフォルトページテンプレートとして使用されています。このページが削除されると、それらのアイテムでデフォルトのページテンプレートが割り当てられなくなります。',\n    'pages_delete_confirm' => 'このページを削除してもよろしいですか？',\n    'pages_delete_draft_confirm' => 'このページの下書きを削除してもよろしいですか？',\n    'pages_editing_named' => 'ページ :pageName を編集',\n    'pages_edit_draft_options' => '下書きオプション',\n    'pages_edit_save_draft' => '下書きを保存',\n    'pages_edit_draft' => 'ページの下書きを編集',\n    'pages_editing_draft' => '下書きを編集中',\n    'pages_editing_page' => 'ページを編集中',\n    'pages_edit_draft_save_at' => '下書きを保存済み: ',\n    'pages_edit_delete_draft' => '下書きを削除',\n    'pages_edit_delete_draft_confirm' => '本当にページの下書変更を削除しますか？ 最後の完全な保存以降の変更はすべて失われ、エディタは保存された最新のページ内容に復元されます。',\n    'pages_edit_discard_draft' => '下書きを破棄',\n    'pages_edit_switch_to_markdown' => 'Markdownエディタに切り替え',\n    'pages_edit_switch_to_markdown_clean' => '(クリーンなコンテンツ)',\n    'pages_edit_switch_to_markdown_stable' => '(安定したコンテンツ)',\n    'pages_edit_switch_to_wysiwyg' => 'WYSIWYGエディタに切り替え',\n    'pages_edit_switch_to_new_wysiwyg' => '新しいWYSIWYGエディタに切り替える',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '（ベータテスト版）',\n    'pages_edit_set_changelog' => '編集内容についての説明',\n    'pages_edit_enter_changelog_desc' => 'どのような変更を行ったのかを記録してください',\n    'pages_edit_enter_changelog' => '編集内容を入力',\n    'pages_editor_switch_title' => 'エディタの切り替え',\n    'pages_editor_switch_are_you_sure' => 'このページのエディタを変更してもよろしいですか？',\n    'pages_editor_switch_consider_following' => 'エディタを変更する場合には次の点に注意してください',\n    'pages_editor_switch_consideration_a' => '保存すると、新しいエディタはそれ自身で種類を変更できない可能性のあるエディタを含め、今後のエディタとして利用されます。',\n    'pages_editor_switch_consideration_b' => 'これにより、特定の状況で詳細と構文が失われる可能性があります。',\n    'pages_editor_switch_consideration_c' => '最後の保存以降に行われたタグまたは変更ログの変更は、この変更では保持されません。',\n    'pages_save' => 'ページを保存',\n    'pages_title' => 'ページタイトル',\n    'pages_name' => 'ページ名',\n    'pages_md_editor' => 'エディタ',\n    'pages_md_preview' => 'プレビュー',\n    'pages_md_insert_image' => '画像を挿入',\n    'pages_md_insert_link' => 'エンティティへのリンクを挿入',\n    'pages_md_insert_drawing' => '図を追加',\n    'pages_md_show_preview' => 'プレビューを表示',\n    'pages_md_sync_scroll' => 'プレビューとスクロールを同期',\n    'pages_md_plain_editor' => 'プレーンテキスト エディタ',\n    'pages_drawing_unsaved' => '未保存の図が見つかりました',\n    'pages_drawing_unsaved_confirm' => '以前に保存操作が失敗した、未保存の図が見つかりました。\n未保存の図面を復元して編集を続けますか？',\n    'pages_not_in_chapter' => 'チャプターが設定されていません',\n    'pages_move' => 'ページを移動',\n    'pages_copy' => 'ページをコピー',\n    'pages_copy_desination' => 'コピー先',\n    'pages_copy_success' => 'ページが正常にコピーされました',\n    'pages_permissions' => 'ページの権限設定',\n    'pages_permissions_success' => 'ページの権限を更新しました',\n    'pages_revision' => '編集履歴',\n    'pages_revisions' => '編集履歴',\n    'pages_revisions_desc' => '以下はこのページの過去の全リビジョンです。権限があれば、古いバージョンのページの見返しや比較、復元ができます。システムの設定によっては、古いリビジョンが自動削除されることがあるため、このページの全履歴がここに反映されないことがあります。',\n    'pages_revisions_named' => ':pageName のリビジョン',\n    'pages_revision_named' => ':pageName のリビジョン',\n    'pages_revision_restored_from' => '#:id :summary から復元',\n    'pages_revisions_created_by' => '作成者',\n    'pages_revisions_date' => '日付',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'リビジョン番号',\n    'pages_revisions_numbered' => 'リビジョン #:id',\n    'pages_revisions_numbered_changes' => 'リビジョン #:id の変更',\n    'pages_revisions_editor' => 'エディタの種類',\n    'pages_revisions_changelog' => '説明',\n    'pages_revisions_changes' => '変更点',\n    'pages_revisions_current' => '現在のバージョン',\n    'pages_revisions_preview' => 'プレビュー',\n    'pages_revisions_restore' => '復元',\n    'pages_revisions_none' => 'このページにはリビジョンがありません',\n    'pages_copy_link' => 'リンクをコピー',\n    'pages_edit_content_link' => 'エディタのセクションへ移動',\n    'pages_pointer_enter_mode' => 'セクション選択モードに入る',\n    'pages_pointer_label' => 'ページセクションのオプションポップアップ',\n    'pages_pointer_permalink' => 'ページセクションのパーマリンク',\n    'pages_pointer_include_tag' => 'ページセクションのインクルードタグ',\n    'pages_pointer_toggle_link' => 'パーマリンクモード。押下するとインクルードタグを表示',\n    'pages_pointer_toggle_include' => 'インクルードタグモード。押下するとパーマリンクを表示',\n    'pages_permissions_active' => 'ページの権限は有効です',\n    'pages_initial_revision' => '初回の公開',\n    'pages_references_update_revision' => '内部リンクのシステム自動更新',\n    'pages_initial_name' => '新規ページ',\n    'pages_editing_draft_notification' => ':timeDiffに保存された下書きを編集しています。',\n    'pages_draft_edited_notification' => 'このページは更新されています。下書きを破棄することを推奨します。',\n    'pages_draft_page_changed_since_creation' => 'この下書きが作成されてから、このページが更新されました。この下書きを破棄するか、ページの変更を上書きしないように注意することを推奨します。',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count人のユーザがページの編集を開始しました',\n        'start_b' => ':userNameがページの編集を開始しました',\n        'time_a' => '数秒前に保存されました',\n        'time_b' => ':minCount分前に保存されました',\n        'message' => ':start :time. 他のユーザによる更新を上書きしないよう注意してください。',\n    ],\n    'pages_draft_discarded' => '下書きは破棄されました。エディタは現在のページ内容へ復元されています。',\n    'pages_draft_deleted' => '下書きを削除しました。エディタは現在のページ内容へ復元されています。',\n    'pages_specific' => '特定のページ',\n    'pages_is_template' => 'ページテンプレート',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'サイドバーの切り替え',\n    'page_tags' => 'タグ',\n    'chapter_tags' => 'チャプターのタグ',\n    'book_tags' => 'ブックのタグ',\n    'shelf_tags' => '本棚のタグ',\n    'tag' => 'タグ',\n    'tags' =>  'タグ',\n    'tags_index_desc' => 'システム内のコンテンツにタグを適用して柔軟なカテゴリ分けを行うことができます。タグはキーと値の両方を持つことができ、値は任意です。タグを適用すると、タグの名前と値を使ってコンテンツを検索することができます。',\n    'tag_name' =>  'タグの名前',\n    'tag_value' => '内容 (オプション)',\n    'tags_explain' => \"タグを設定すると、コンテンツの管理が容易になります。\\nより高度な管理をしたい場合、タグに内容を設定できます。\",\n    'tags_add' => 'タグを追加',\n    'tags_remove' => 'このタグを削除',\n    'tags_usages' => 'タグの総使用回数',\n    'tags_assigned_pages' => '割り当てられているページの数',\n    'tags_assigned_chapters' => '割り当てられているチャプターの数',\n    'tags_assigned_books' => '割り当てられているブックの数',\n    'tags_assigned_shelves' => '割り当てられている本棚の数',\n    'tags_x_unique_values' => ':count個のユニークな値',\n    'tags_all_values' => '全ての値',\n    'tags_view_tags' => 'タグを表示',\n    'tags_view_existing_tags' => '既存のタグを表示',\n    'tags_list_empty_hint' => 'タグはページエディタのサイドバーまたはブック、チャプター、本棚の詳細を編集しているときに割り当てることができます。',\n    'attachments' => '添付ファイル',\n    'attachments_explain' => 'ファイルをアップロードまたはリンクを添付することができます。これらはサイドバーで確認できます。',\n    'attachments_explain_instant_save' => 'この変更は即座に保存されます。',\n    'attachments_upload' => 'アップロード',\n    'attachments_link' => 'リンクを添付',\n    'attachments_upload_drop' => 'ファイルをここにドラッグアンドドロップして添付ファイルとしてアップロードすることもできます。',\n    'attachments_set_link' => 'リンクを設定',\n    'attachments_delete' => 'この添付ファイルを削除してよろしいですか？',\n    'attachments_dropzone' => 'アップロードするファイルをここにドロップ',\n    'attachments_no_files' => 'ファイルはアップロードされていません',\n    'attachments_explain_link' => 'ファイルをアップロードしたくない場合、他のページやクラウド上のファイルへのリンクを添付できます。',\n    'attachments_link_name' => 'リンク名',\n    'attachment_link' => '添付リンク',\n    'attachments_link_url' => 'ファイルURL',\n    'attachments_link_url_hint' => 'WebサイトまたはファイルへのURL',\n    'attach' => '添付',\n    'attachments_insert_link' => '添付ファイルへのリンクをページに追加',\n    'attachments_edit_file' => 'ファイルを編集',\n    'attachments_edit_file_name' => 'ファイル名',\n    'attachments_edit_drop_upload' => 'ファイルをドロップするか、クリックしてアップロード',\n    'attachments_order_updated' => '添付ファイルの並び順が変更されました',\n    'attachments_updated_success' => '添付ファイルが更新されました',\n    'attachments_deleted' => '添付は削除されました',\n    'attachments_file_uploaded' => 'ファイルがアップロードされました',\n    'attachments_file_updated' => 'ファイルが更新されました',\n    'attachments_link_attached' => 'リンクがページへ添付されました',\n    'templates' => 'テンプレート',\n    'templates_set_as_template' => 'テンプレートに設定',\n    'templates_explain_set_as_template' => 'このページをテンプレートとして設定すると、他のページを作成する際にこの内容を利用することができます。他のユーザーは、このページの表示権限を持っていればこのテンプレートを使用できます。',\n    'templates_replace_content' => 'ページの内容を置換',\n    'templates_append_content' => 'ページの末尾に追加',\n    'templates_prepend_content' => 'ページの先頭に追加',\n\n    // Profile View\n    'profile_user_for_x' => ':time前に作成',\n    'profile_created_content' => '作成したコンテンツ',\n    'profile_not_created_pages' => ':userNameはページを作成していません',\n    'profile_not_created_chapters' => ':userNameはチャプターを作成していません',\n    'profile_not_created_books' => ':userNameはブックを作成していません',\n    'profile_not_created_shelves' => ':userNameは本棚を作成していません',\n\n    // Comments\n    'comment' => 'コメント',\n    'comments' => 'コメント',\n    'comment_add' => 'コメント追加',\n    'comment_none' => '表示するコメントがありません',\n    'comment_placeholder' => 'コメントを記入してください',\n    'comment_thread_count' => ':count 個のコメントスレッド|:count 個のコメントスレッド',\n    'comment_archived_count' => ':count 個のアーカイブ',\n    'comment_archived_threads' => 'アーカイブされたスレッド',\n    'comment_save' => 'コメントを保存',\n    'comment_new' => '新規コメント作成',\n    'comment_created' => 'コメントを作成しました :createDiff',\n    'comment_updated' => ':username により更新しました :updateDiff',\n    'comment_updated_indicator' => '編集済み',\n    'comment_deleted_success' => 'コメントを削除しました',\n    'comment_created_success' => 'コメントを追加しました',\n    'comment_updated_success' => 'コメントを更新しました',\n    'comment_archive_success' => 'コメントをアーカイブしました',\n    'comment_unarchive_success' => 'コメントのアーカイブを解除しました',\n    'comment_view' => 'コメントを表示',\n    'comment_jump_to_thread' => 'スレッドにジャンプ',\n    'comment_delete_confirm' => '本当にこのコメントを削除しますか?',\n    'comment_in_reply_to' => ':commentIdへ返信',\n    'comment_reference' => '参照箇所',\n    'comment_reference_outdated' => '（以前の記述）',\n    'comment_editor_explain' => 'ここにはページに付けられたコメントを表示します。 コメントの追加と管理は保存されたページの表示時に行うことができます。',\n\n    // Revision\n    'revision_delete_confirm' => 'このリビジョンを削除しますか？',\n    'revision_restore_confirm' => 'このリビジョンを復元してよろしいですか？現在のページの内容が置換されます。',\n    'revision_cannot_delete_latest' => '最新のリビジョンを削除できません。',\n\n    // Copy view\n    'copy_consider' => 'コンテンツをコピーする場合は以下の点にご注意ください。',\n    'copy_consider_permissions' => 'カスタム権限設定はコピーされません。',\n    'copy_consider_owner' => 'あなたはコピーされた全てのコンテンツの所有者になります。',\n    'copy_consider_images' => 'ページの画像ファイルは複製されず、元の画像は最初にアップロードされたページとの関係を保持します。',\n    'copy_consider_attachments' => 'ページの添付ファイルはコピーされません。',\n    'copy_consider_access' => '場所、所有者または権限を変更すると、以前アクセスできなかったユーザーがこのコンテンツにアクセスできるようになる可能性があります。',\n\n    // Conversions\n    'convert_to_shelf' => '本棚に変換',\n    'convert_to_shelf_contents_desc' => 'このブックを同じ内容の新しい棚に変換できます。このブックに含まれるチャプターは新しいブックに変換されます。このブックにチャプター内にないページが含まれている場合、このブックは名前が変更され、そのようなページを含む新しい本棚の一部となります。',\n    'convert_to_shelf_permissions_desc' => 'このブックに設定されているすべての権限は、新しい本棚と、独自の権限が適用されていないすべての新しい子ブックにコピーされます。本棚の権限はブックの場合のように、内部のコンテンツに自動的に継承されないことに注意してください。',\n    'convert_book' => 'ブックを変換',\n    'convert_book_confirm' => 'このブックを変換してもよろしいですか？',\n    'convert_undo_warning' => 'これは簡単には元に戻せません。',\n    'convert_to_book' => 'ブックに変換',\n    'convert_to_book_desc' => 'このチャプターを同じ内容の新しいブックに変換できます。このチャプターで設定された権限は新しいブックにコピーされますが、親ブックから継承された権限はコピーされないため、アクセス制御が変更される可能性があります。',\n    'convert_chapter' => 'チャプターを変換',\n    'convert_chapter_confirm' => 'このチャプターを変換してもよろしいですか？',\n\n    // References\n    'references' => '参照',\n    'references_none' => 'この項目への追跡された参照はありません。',\n    'references_to_desc' => 'この項目はシステム内の以下のコンテンツからリンクされています。',\n\n    // Watch Options\n    'watch' => 'ウォッチ',\n    'watch_title_default' => 'デフォルト設定',\n    'watch_desc_default' => 'デフォルトの通知設定に戻します。',\n    'watch_title_ignore' => '無効',\n    'watch_desc_ignore' => 'ユーザーの通知設定に関わらず、すべての通知を無効にします。',\n    'watch_title_new' => 'ページの作成',\n    'watch_desc_new' => 'このアイテム内に新しいページが作成されたときに通知します。',\n    'watch_title_updates' => 'すべてのページ更新',\n    'watch_desc_updates' => 'ページの作成や更新を通知します。',\n    'watch_desc_updates_page' => 'ページの更新を通知します。',\n    'watch_title_comments' => 'すべてのページ更新とコメント',\n    'watch_desc_comments' => 'ページの作成・更新、およびコメント追加を通知します。',\n    'watch_desc_comments_page' => 'ページの更新およびコメント追加を通知します。',\n    'watch_change_default' => 'デフォルトの通知設定を変更する',\n    'watch_detail_ignore' => '通知無効',\n    'watch_detail_new' => 'ページ作成をウォッチ',\n    'watch_detail_updates' => 'ページの作成と更新をウォッチ',\n    'watch_detail_comments' => 'ページの作成・更新とコメントをウォッチ',\n    'watch_detail_parent_book' => '親ブックでウォッチ',\n    'watch_detail_parent_book_ignore' => '親ブックで通知無効',\n    'watch_detail_parent_chapter' => '親チャプタでウォッチ',\n    'watch_detail_parent_chapter_ignore' => '親チャプタで通知無効',\n];\n"
  },
  {
    "path": "lang/ja/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'リクエストされたページへの権限がありません。',\n    'permissionJson' => '要求されたアクションを実行する権限がありません。',\n\n    // Auth\n    'error_user_exists_different_creds' => ':emailを持つユーザは既に存在しますが、資格情報が異なります。',\n    'auth_pre_register_theme_prevention' => '指定された内容によるユーザーアカウントの登録はできません。',\n    'email_already_confirmed' => 'Eメールは既に確認済みです。ログインしてください。',\n    'email_confirmation_invalid' => 'この確認トークンは無効か、または既に使用済みです。登録を再試行してください。',\n    'email_confirmation_expired' => '確認トークンは有効期限切れです。確認メールを再送しました。',\n    'email_confirmation_awaiting' => '使用中のアカウントのメールアドレスを確認する必要があります',\n    'ldap_fail_anonymous' => '匿名バインドを用いたLDAPアクセスに失敗しました',\n    'ldap_fail_authed' => '識別名, パスワードを用いたLDAPアクセスに失敗しました',\n    'ldap_extension_not_installed' => 'LDAP PHP extensionがインストールされていません',\n    'ldap_cannot_connect' => 'LDAPサーバに接続できませんでした',\n    'saml_already_logged_in' => '既にログインしています',\n    'saml_no_email_address' => '外部認証システムから提供されたデータに、このユーザーのメールアドレスが見つかりませんでした',\n    'saml_invalid_response_id' => '外部認証システムからの要求がアプリケーションによって開始されたプロセスによって認識されません。ログイン後に戻るとこの問題が発生する可能性があります。',\n    'saml_fail_authed' => ':systemを利用したログインに失敗しました。システムは正常な認証を提供しませんでした。',\n    'oidc_already_logged_in' => '既にログインしています',\n    'oidc_no_email_address' => '外部認証システムから提供されたデータに、このユーザーのメールアドレスが見つかりませんでした',\n    'oidc_fail_authed' => ':systemを利用したログインに失敗しました。システムは正常な認証を提供しませんでした。',\n    'social_no_action_defined' => 'アクションが定義されていません',\n    'social_login_bad_response' => \":socialAccountのログイン中にエラーが発生しました:\\n:error\",\n    'social_account_in_use' => ':socialAccountアカウントは既に使用されています。:socialAccountのオプションからログインを試行してください。',\n    'social_account_email_in_use' => ':emailは既に使用されています。ログイン後、プロフィール設定から:socialAccountアカウントを接続できます。',\n    'social_account_existing' => 'アカウント:socialAccountは既にあなたのプロフィールに接続されています。',\n    'social_account_already_used_existing' => 'この:socialAccountアカウントは既に他のユーザが使用しています。',\n    'social_account_not_used' => 'この:socialAccountアカウントはどのユーザにも接続されていません。プロフィール設定から接続できます。',\n    'social_account_register_instructions' => 'まだアカウントをお持ちでない場合、:socialAccountオプションから登録できます。',\n    'social_driver_not_found' => 'Social driverが見つかりません',\n    'social_driver_not_configured' => 'あなたの:socialAccount設定は正しく構成されていません。',\n    'invite_token_expired' => 'この招待リンクの有効期限が切れています。 代わりにアカウントのパスワードをリセットしてみてください。',\n    'login_user_not_found' => 'このアクションのユーザーが見つかりません。',\n\n    // System\n    'path_not_writable' => 'ファイルパス :filePath へアップロードできませんでした。サーバ上での書き込みが許可されているか確認してください。',\n    'cannot_get_image_from_url' => ':url から画像を取得できませんでした。',\n    'cannot_create_thumbs' => 'このサーバはサムネイルを作成できません。GD PHP extensionがインストールされていることを確認してください。',\n    'server_upload_limit' => 'このサイズの画像をアップロードすることは許可されていません。ファイルサイズを小さくし、再試行してください。',\n    'server_post_limit' => 'サーバーは提供されたデータ量を受け取ることができません。少ないデータまたは小さいファイルでもう一度お試しください。',\n    'uploaded'  => 'このサイズの画像をアップロードすることは許可されていません。ファイルサイズを小さくし、再試行してください。',\n\n    // Drawing & Images\n    'image_upload_error' => '画像アップロード時にエラーが発生しました。',\n    'image_upload_type_error' => 'アップロード中の画像の種類が無効です',\n    'image_upload_replace_type' => '画像ファイルの置き換えは同じ種類でなければなりません',\n    'image_upload_memory_limit' => 'システムリソースの制限により、画像のアップロードやサムネイルの作成に失敗しました。',\n    'image_thumbnail_memory_limit' => 'システムリソース制限のため、画像サイズのバリエーションを作成できませんでした。',\n    'image_gallery_thumbnail_memory_limit' => 'システムリソース制限のため、ギャラリーのサムネイルを作成できませんでした。',\n    'drawing_data_not_found' => '描画データを読み込めませんでした。描画ファイルが存在しないか、アクセス権限がありません。',\n\n    // Attachments\n    'attachment_not_found' => '添付ファイルが見つかりません',\n    'attachment_upload_error' => '添付ファイルのアップロード中にエラーが発生しました',\n\n    // Pages\n    'page_draft_autosave_fail' => '下書きの保存に失敗しました。インターネットへ接続してください。',\n    'page_draft_delete_fail' => '下書きページの削除および現在ページ内容の取得に失敗しました。',\n    'page_custom_home_deletion' => 'ホームページに設定されているページは削除できません',\n\n    // Entities\n    'entity_not_found' => 'エンティティが見つかりません',\n    'bookshelf_not_found' => '本棚が見つかりません',\n    'book_not_found' => 'ブックが見つかりません',\n    'page_not_found' => 'ページが見つかりません',\n    'chapter_not_found' => 'チャプターが見つかりません',\n    'selected_book_not_found' => '選択されたブックが見つかりません',\n    'selected_book_chapter_not_found' => '選択されたブック、またはチャプターが見つかりません',\n    'guests_cannot_save_drafts' => 'ゲストは下書きを保存できません',\n\n    // Users\n    'users_cannot_delete_only_admin' => '唯一の管理者を削除することはできません',\n    'users_cannot_delete_guest' => 'ゲストユーザを削除することはできません',\n    'users_could_not_send_invite' => '招待メールの送信に失敗したため、ユーザーを作成できませんでした。',\n\n    // Roles\n    'role_cannot_be_edited' => 'この役割は編集できません',\n    'role_system_cannot_be_deleted' => 'この役割はシステムで管理されているため、削除できません',\n    'role_registration_default_cannot_delete' => 'この役割を登録時のデフォルトに設定することはできません',\n    'role_cannot_remove_only_admin' => 'このユーザーは、管理者の役割に割り当てられている唯一のユーザーです。削除する前に別のユーザーに管理者の役割を割り当ててください。',\n\n    // Comments\n    'comment_list' => 'コメントを取得中にエラーが発生しました。',\n    'cannot_add_comment_to_draft' => '下書きにコメントは追加できません。',\n    'comment_add' => 'コメントの追加・更新中にエラーが発生しました。',\n    'comment_delete' => 'コメントを削除中にエラーが発生しました。',\n    'empty_comment' => '空のコメントは追加できません。',\n\n    // Error pages\n    '404_page_not_found' => 'ページが見つかりません',\n    'sorry_page_not_found' => 'ページを見つけることができませんでした。',\n    'sorry_page_not_found_permission_warning' => 'このページが存在すると思われる場合は、閲覧の権限がない可能性があります。',\n    'image_not_found' => '画像が見つかりません',\n    'image_not_found_subtitle' => '画像を見つけることができませんでした。',\n    'image_not_found_details' => 'この画像が存在することを予期していた場合は、削除された可能性があります。',\n    'return_home' => 'ホームに戻る',\n    'error_occurred' => 'エラーが発生しました',\n    'app_down' => ':appNameは現在停止しています',\n    'back_soon' => '回復までしばらくお待ちください。',\n\n    // Import\n    'import_zip_cant_read' => 'ZIPファイルを読み込めません。',\n    'import_zip_cant_decode_data' => 'ZIPファイル内に data.json が見つからないかデコードできませんでした。',\n    'import_zip_no_data' => 'ZIPファイルのデータにブック、チャプター、またはページコンテンツがありません。',\n    'import_zip_data_too_large' => 'ZIPに含まれる data.json が、アプリケーションで設定された最大アップロードサイズを超えています。',\n    'import_validation_failed' => 'エラーによりインポートZIPの検証に失敗しました:',\n    'import_zip_failed_notification' => 'ZIP ファイルのインポートに失敗しました。',\n    'import_perms_books' => 'ブックを作成するために必要な権限がありません。',\n    'import_perms_chapters' => 'チャプタを作成するために必要な権限がありません。',\n    'import_perms_pages' => 'ページを作成するために必要な権限がありません。',\n    'import_perms_images' => '画像を作成するために必要な権限がありません。',\n    'import_perms_attachments' => '添付ファイルを作成するために必要な権限がありません。',\n\n    // API errors\n    'api_no_authorization_found' => 'リクエストに認証トークンが見つかりません',\n    'api_bad_authorization_format' => 'リクエストに認証トークンが見つかりましたが、形式が正しくないようです',\n    'api_user_token_not_found' => '提供された認証トークンに一致するAPIトークンが見つかりませんでした',\n    'api_incorrect_token_secret' => '利用されたAPIトークンに対して提供されたシークレットが正しくありません',\n    'api_user_no_api_permission' => '使用されているAPIトークンの所有者には、API呼び出しを行う権限がありません',\n    'api_user_token_expired' => '認証トークンが期限切れです。',\n    'api_cookie_auth_only_get' => 'Cookie ベースの認証で API を使用する場合、GET リクエストのみが許可されます',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'テストメール送信時にエラーが発生しました:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'URLはサーバサイドリクエストが許可されたホストではありません。',\n];\n"
  },
  {
    "path": "lang/ja/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'ページへのコメント追加： :pageName',\n    'new_comment_intro' => ':appName でページにコメントが追加されました',\n    'new_page_subject' => 'ページの作成： :pageName',\n    'new_page_intro' => ':appName でページが作成されました',\n    'updated_page_subject' => 'ページの更新： :pageName',\n    'updated_page_intro' => ':appName でページが更新されました',\n    'updated_page_debounce' => '大量の通知を防ぐために、しばらくの間は同じユーザがこのページをさらに編集しても通知は送信されません。',\n    'comment_mention_subject' => 'ページのコメントであなたにメンションされています: :pageName',\n    'comment_mention_intro' => ':appName: のコメントであなたにメンションされました',\n\n    'detail_page_name' => 'ページ名：',\n    'detail_page_path' => 'ページパス：',\n    'detail_commenter' => 'コメントユーザ：',\n    'detail_comment' => 'コメント：',\n    'detail_created_by' => '作成ユーザ：',\n    'detail_updated_by' => '更新ユーザ：',\n\n    'action_view_comment' => 'コメントを表示',\n    'action_view_page' => 'ページを表示',\n\n    'footer_reason' => 'この項目のアクティビティは :link による対象となっているため、この通知が送信されました。',\n    'footer_reason_link' => '通知設定',\n];\n"
  },
  {
    "path": "lang/ja/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; 前',\n    'next'     => '次 &raquo;',\n\n];\n"
  },
  {
    "path": "lang/ja/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'パスワードは6文字以上である必要があります。',\n    'user' => \"このEメールアドレスに一致するユーザが見つかりませんでした。\",\n    'token' => 'このメールアドレスのパスワードリセットトークンは無効です。',\n    'sent' => 'パスワードリセットリンクを送信しました。',\n    'reset' => 'パスワードはリセットされました。',\n\n];\n"
  },
  {
    "path": "lang/ja/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'マイアカウント',\n\n    'shortcuts' => 'ショートカット',\n    'shortcuts_interface' => 'UIショートカット設定',\n    'shortcuts_toggle_desc' => 'ここでは、ナビゲーションやアクションに使用されるキーボードシステムインターフェイスのショートカットを有効または無効にすることができます。',\n    'shortcuts_customize_desc' => '以下の各ショートカットをカスタマイズできます。ショートカットの入力を選択した後、希望のキーの組み合わせを押してください。',\n    'shortcuts_toggle_label' => 'キーボードショートカットを有効にする',\n    'shortcuts_section_navigation' => 'ナビゲーション',\n    'shortcuts_section_actions' => '共通のアクション',\n    'shortcuts_save' => 'ショートカットを保存',\n    'shortcuts_overlay_desc' => '注：ショートカットが有効な場合はヘルパーオーバーレイが利用できます。「?」を押すと現在画面に表示されているアクションで利用可能なショートカットをハイライト表示します。',\n    'shortcuts_update_success' => 'ショートカットの設定を更新しました。',\n    'shortcuts_overview_desc' => 'システムのユーザーインターフェイスを操作するためのキーボードショートカットを管理します。',\n\n    'notifications' => '通知設定',\n    'notifications_desc' => 'システム内で特定のアクティビティが実行されたときに受信する電子メール通知を制御します。',\n    'notifications_opt_own_page_changes' => '自分が所有するページの変更を通知する',\n    'notifications_opt_own_page_comments' => '自分が所有するページへのコメントを通知する',\n    'notifications_opt_comment_mentions' => 'コメントでメンションされたときに通知する',\n    'notifications_opt_comment_replies' => '自分のコメントへの返信を通知する',\n    'notifications_save' => '設定を保存',\n    'notifications_update_success' => '通知設定を更新しました。',\n    'notifications_watched' => 'ウォッチ/通知無効 項目',\n    'notifications_watched_desc' => '以下はカスタムウォッチの設定が適用されている項目です。 これらの設定を更新するには、項目を表示してサイドバーのウォッチオプションを参照してください。',\n\n    'auth' => 'アクセス & セキュリティ',\n    'auth_change_password' => 'パスワードの変更',\n    'auth_change_password_desc' => 'アプリケーションにログインするために使用するパスワードを変更します。これは少なくとも8文字以上でなければなりません。',\n    'auth_change_password_success' => 'パスワードが更新されました！',\n\n    'profile' => 'プロフィール詳細',\n    'profile_desc' => 'コミュニケーションやシステムのパーソナライズでの使用および、他のユーザーに表示されるアカウントの詳細を管理します。',\n    'profile_view_public' => '公開プロフィールを表示',\n    'profile_name_desc' => '実行したアクティビティや所有しているコンテンツを通じて、システム内で他のユーザーに表示される表示名を設定します。',\n    'profile_email_desc' => 'このメールアドレスは通知、アクティブなシステム認証、システムアクセスに使用されます。',\n    'profile_email_no_permission' => '残念ながらメールアドレスを変更する権限がありません。 これを変更したい場合は管理者に変更を依頼する必要があります。',\n    'profile_avatar_desc' => 'システム内で他のユーザーに自分を表現するために使用される画像を選択します。 この画像は幅と高さが256pxの正方形が最適です。',\n    'profile_admin_options' => '管理者オプション',\n    'profile_admin_options_desc' => '役割の割り当て管理など、管理者レベルの追加オプションはアプリケーションの「設定 > ユーザー」エリアにあります。',\n\n    'delete_account' => 'アカウントを削除',\n    'delete_my_account' => '自身のアカウント削除',\n    'delete_my_account_desc' => 'システムからユーザーアカウントを完全に削除します。このアカウントを復元したり、この操作を元に戻したりすることはできません。 作成されたページやアップロードされた画像などのコンテンツは残ります。',\n    'delete_my_account_warning' => '本当にアカウントを削除しますか？',\n];\n"
  },
  {
    "path": "lang/ja/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => '設定',\n    'settings_save' => '設定を保存',\n    'system_version' => 'システムバージョン',\n    'categories' => 'カテゴリー',\n\n    // App Settings\n    'app_customization' => 'カスタマイズ',\n    'app_features_security' => '機能とセキュリティ',\n    'app_name' => 'アプリケーション名',\n    'app_name_desc' => 'この名前はヘッダーやEメール内で表示されます。',\n    'app_name_header' => 'ヘッダーにアプリケーション名を表示する',\n    'app_public_access' => 'パブリック・アクセス',\n    'app_public_access_desc' => 'このオプションを有効にすると、ログインしていない訪問者があなたのBookStackインスタンスのコンテンツにアクセスできるようになります。',\n    'app_public_access_desc_guest' => '一般の訪問者のアクセスは、「ゲスト」ユーザー権限を通じて制御することができます。',\n    'app_public_access_toggle' => 'パブリックアクセスを許可',\n    'app_public_viewing' => 'アプリケーションを公開する',\n    'app_secure_images' => '画像アップロード時のセキュリティを強化',\n    'app_secure_images_toggle' => 'より高いセキュリティの画像アップロードを可能にする',\n    'app_secure_images_desc' => 'パフォーマンスの観点から、全ての画像が公開になっています。このオプションを有効にすると、画像URLの先頭にランダムで推測困難な文字列が追加され、アクセスを困難にします。',\n    'app_default_editor' => 'デフォルトのページエディタ',\n    'app_default_editor_desc' => '新しいページを編集するときにデフォルトで使用されるエディタを選択してください。これは権限が許可されているページレベルで上書きできます。',\n    'app_custom_html' => 'カスタムheadタグ',\n    'app_custom_html_desc' => 'スタイルシートやアナリティクスコード追加したい場合、ここを編集します。これは<head>の最下部に挿入されます。',\n    'app_custom_html_disabled_notice' => '重大な変更を元に戻せるよう、この設定ページではカスタムのHTML headコンテンツが無効になっています。',\n    'app_logo' => 'ロゴ',\n    'app_logo_desc' => 'これはアプリケーションのヘッダーバー、およびその他のエリアで使用されます。この画像は高さが86pxであるべきです。大きな画像は縮小されます。',\n    'app_icon' => 'アプリケーション アイコン',\n    'app_icon_desc' => 'このアイコンはブラウザのタブとショートカットアイコンに使用されます。これは256pxの正方形PNG画像であるべきです。',\n    'app_homepage' => 'アプリケーションのホームページ',\n    'app_homepage_desc' => 'デフォルトのビューの代わりにホームページに表示するビューを選択します。選択したページの権限は無視されます。',\n    'app_homepage_select' => 'ページを選択',\n    'app_footer_links' => 'フッタのリンク',\n    'app_footer_links_desc' => 'サイトフッタ内に表示するリンクを追加します。これらはログインを必要としないページを含め、ほとんどのページの下部に表示されます。「trans::<key>」のラベルを使用して、システム定義の翻訳を使用できます。例えば「trans::common.privacy_policy」を使用すると翻訳されたテキスト「プライバシーポリシー」が提供され、「trans::common.terms_of_service」を使用すると翻訳されたテキスト「利用規約」が提供されます。',\n    'app_footer_links_label' => '表示するテキスト',\n    'app_footer_links_url' => 'リンク先の URL',\n    'app_footer_links_add' => 'フッタのリンクを追加',\n    'app_disable_comments' => 'コメントを無効にする',\n    'app_disable_comments_toggle' => 'コメントを無効にする',\n    'app_disable_comments_desc' => 'アプリケーション内のすべてのページのコメントを無効にします。既存のコメントは表示されません。',\n\n    // Color settings\n    'color_scheme' => 'アプリケーションの配色',\n    'color_scheme_desc' => 'アプリケーションのユーザーインターフェイスで使用する色を設定します。 色はダークモードとライトモードで個別に設定することができ、テーマへの適合と読みやすさを確保することができます。',\n    'ui_colors_desc' => 'アプリケーションのプライマリカラーとデフォルトリンクカラーを設定します。プライマリカラーは主にヘッダーバナー、ボタン、インターフェイスの装飾に使用されます。 デフォルトのリンク色はテキストベースのリンクとアクションに使用されます。これは作成されたコンテンツとアプリケーションインターフェイスの両方に適用されます。',\n    'app_color' => 'プライマリ色',\n    'link_color' => 'デフォルトのリンク色',\n    'content_colors_desc' => 'ページ構成階層の各要素に色を設定します。読みやすさを考慮して、デフォルトの色と同じような明るさの色を選ぶことをお勧めします。',\n    'bookshelf_color' => '本棚の色',\n    'book_color' => 'ブックの色',\n    'chapter_color' => 'チャプターの色',\n    'page_color' => 'ページの色',\n    'page_draft_color' => '下書きページの色',\n\n    // Registration Settings\n    'reg_settings' => '登録設定',\n    'reg_enable' => '登録を有効にする',\n    'reg_enable_toggle' => '登録を有効にする',\n    'reg_enable_desc' => '登録を有効にすると、ユーザーはアプリケーションユーザーとしてサインアップできるようになります。登録するとデフォルトの役割が1つ与えられます。',\n    'reg_default_role' => '新規登録時のデフォルト役割',\n    'reg_enable_external_warning' => '外部のLDAPまたはSAML認証が有効の場合、上記のオプションは無視されます。存在しないメンバーのユーザーアカウントは、使用している外部システムでの認証に成功した場合に自動的に作成されます。',\n    'reg_email_confirmation' => '確認メール',\n    'reg_email_confirmation_toggle' => 'メールによる確認を行う',\n    'reg_confirm_email_desc' => 'ドメイン制限を有効にしている場合はEメール認証が必須となり、この項目は無視されます。',\n    'reg_confirm_restrict_domain' => 'ドメイン制限',\n    'reg_confirm_restrict_domain_desc' => '特定のドメインのみ登録できるようにする場合、以下にカンマ区切りで入力します。設定された場合、Eメール認証が必須になります。<br>登録後、ユーザは自由にEメールアドレスを変更できます。',\n    'reg_confirm_restrict_domain_placeholder' => '制限しない',\n\n    // Sorting Settings\n    'sorting' => '一覧とソート',\n    'sorting_book_default' => 'ブックのデフォルトソートルール',\n    'sorting_book_default_desc' => '新しいブックに適用するデフォルトのソートルールを選択します。これは既存のブックには影響しません。ルールはブックごとに上書きすることができます。',\n    'sorting_rules' => 'ソートルール',\n    'sorting_rules_desc' => 'これらはシステム内のコンテンツに適用できる事前定義のソート操作です。',\n    'sort_rule_assigned_to_x_books' => ':count 個のブックに割当|:count 個のブックに割当',\n    'sort_rule_create' => 'ソートルールを作成',\n    'sort_rule_edit' => 'ソートルールの編集',\n    'sort_rule_delete' => 'ソートルールを削除',\n    'sort_rule_delete_desc' => 'このソートルールをシステムから削除します。これを使用しているブックは手動ソートに戻ります。',\n    'sort_rule_delete_warn_books' => 'このソートルールは現在 :count 個のブックで使用されています。削除してもよろしいですか？',\n    'sort_rule_delete_warn_default' => 'このソートルールは現在ブックのデフォルトソートに使用されています。削除してもよろしいですか？',\n    'sort_rule_details' => 'ソートルールの詳細',\n    'sort_rule_details_desc' => 'ユーザがソートを選択する際にリストに表示される、ソートルールの名前を設定します。',\n    'sort_rule_operations' => 'ソート操作',\n    'sort_rule_operations_desc' => '利用可能な操作のリストから、実行するソート操作を追加してソートルールを設定します。操作は上から下へ順番に適用されます。ここで変更を行うと、保存時に割り当てられたすべてのブックに適用されます。',\n    'sort_rule_available_operations' => '利用可能な操作',\n    'sort_rule_available_operations_empty' => '残った操作はありません',\n    'sort_rule_configured_operations' => '設定された操作',\n    'sort_rule_configured_operations_empty' => '「利用可能な操作」リストから操作をドラッグ/追加してください',\n    'sort_rule_op_asc' => '(昇順)',\n    'sort_rule_op_desc' => '(降順)',\n    'sort_rule_op_name' => '名前 - キャラクタ順',\n    'sort_rule_op_name_numeric' => '名前 - 数値順',\n    'sort_rule_op_created_date' => '作成日時',\n    'sort_rule_op_updated_date' => '更新日時',\n    'sort_rule_op_chapters_first' => 'チャプタを最初に',\n    'sort_rule_op_chapters_last' => 'チャプタを最後に',\n    'sorting_page_limits' => 'ページング表示制限',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'メンテナンス',\n    'maint_image_cleanup' => '画像のクリーンアップ',\n    'maint_image_cleanup_desc' => 'ページや履歴の内容をスキャンして、どの画像や図面が現在使用されているか、どの画像が余っているかをチェックします。この機能を実行する前に、データベースと画像の完全なバックアップを作成してください。',\n    'maint_delete_images_only_in_revisions' => 'また、古いページのリビジョンにしか存在しない画像も削除します。',\n    'maint_image_cleanup_run' => 'クリーンアップを実行',\n    'maint_image_cleanup_warning' => ':count 個、使用されていない可能性のある画像が見つかりました。これらの画像を削除してもよろしいですか？',\n    'maint_image_cleanup_success' => '使われていない可能性のある画像を:count個発見し、削除しました。',\n    'maint_image_cleanup_nothing_found' => '未使用の画像がないため、何も削除しませんでした。',\n    'maint_send_test_email' => 'テストメールを送信',\n    'maint_send_test_email_desc' => 'プロフィールに指定されたメールアドレスにテストメールを送信します。',\n    'maint_send_test_email_run' => 'テストメールを送信',\n    'maint_send_test_email_success' => ':addressにメールを送信しました',\n    'maint_send_test_email_mail_subject' => 'テストメール',\n    'maint_send_test_email_mail_greeting' => 'メール配信は正常に動作しているようです！',\n    'maint_send_test_email_mail_text' => 'おめでとうございます！この通知メールが届いたということは、あなたのメール設定は適切であると思われます。',\n    'maint_recycle_bin_desc' => '削除された本棚・ブック・チャプター・ページはごみ箱に送られるため、復元したり完全に削除したりできます。システムの設定によっては、ごみ箱の古いアイテムがしばらくすると自動的に削除される場合があります。',\n    'maint_recycle_bin_open' => 'ごみ箱を開く',\n    'maint_regen_references' => '参照を再生成',\n    'maint_regen_references_desc' => 'この操作により、データベース内の項目間参照インデックスが再構築されます。これは通常自動的に処理されますが、この操作は古いコンテンツや非公式の方法で追加されたコンテンツのインデックス作成に役立ちます。',\n    'maint_regen_references_success' => '参照インデックスが再生成されました！',\n    'maint_timeout_command_note' => '注意: この操作の実行には時間がかかる場合があり、一部のWeb環境ではタイムアウトの問題が発生する可能性があります。別の方法として、ターミナルコマンドを利用してこの操作を実行することもできます。',\n\n    // Recycle Bin\n    'recycle_bin' => 'ごみ箱',\n    'recycle_bin_desc' => '削除されたアイテムを復元するか、システムから完全に削除できます。このリストは、権限フィルターが適用されているシステム内の同様のアクティビティリストとは異なり、フィルタリングされていません。',\n    'recycle_bin_deleted_item' => '削除されたアイテム',\n    'recycle_bin_deleted_parent' => '親',\n    'recycle_bin_deleted_by' => '削除した人',\n    'recycle_bin_deleted_at' => '削除日時',\n    'recycle_bin_permanently_delete' => '完全に削除',\n    'recycle_bin_restore' => '復元',\n    'recycle_bin_contents_empty' => 'ごみ箱は現在空です',\n    'recycle_bin_empty' => 'ごみ箱を空にする',\n    'recycle_bin_empty_confirm' => 'ごみ箱のすべてのアイテムが、各アイテムに含まれるコンテンツも含めて完全に削除されます。本当にごみ箱を空にしますか？',\n    'recycle_bin_destroy_confirm' => 'この操作を行うと、子要素を含めた以下のリストに示すアイテムがシステムから完全に削除され、このコンテンツを復元できなくなります。このアイテムを完全に削除してもよろしいですか？',\n    'recycle_bin_destroy_list' => '削除されるアイテム',\n    'recycle_bin_restore_list' => '復元されるアイテム',\n    'recycle_bin_restore_confirm' => 'この操作により、すべての子要素を含む削除されたアイテムが元の場所に復元されます。元の場所が削除されてごみ箱に入っている場合は、親アイテムも復元する必要があります。',\n    'recycle_bin_restore_deleted_parent' => 'このアイテムの親も削除されました。これらは、その親が復元されるまで削除されたままになります。',\n    'recycle_bin_restore_parent' => '親を復元',\n    'recycle_bin_destroy_notification' => 'ごみ箱から合計:count個のアイテムを削除しました。',\n    'recycle_bin_restore_notification' => 'ごみ箱から合計:count個のアイテムを復元しました。',\n\n    // Audit Log\n    'audit' => '監査ログ',\n    'audit_desc' => 'この監査ログには、システムで追跡されているアクティビティのリストが表示されます。このリストは、権限フィルターが適用されているシステム内の同様のアクティビティリストとは異なり、フィルタリングされていません。',\n    'audit_event_filter' => 'イベントフィルター',\n    'audit_event_filter_no_filter' => 'フィルターなし',\n    'audit_deleted_item' => '削除されたアイテム',\n    'audit_deleted_item_name' => '名前: :name',\n    'audit_table_user' => 'ユーザー',\n    'audit_table_event' => 'イベント',\n    'audit_table_related' => '関連アイテムまたは詳細',\n    'audit_table_ip' => 'IPアドレス',\n    'audit_table_date' => 'アクティビティの日時',\n    'audit_date_from' => '開始日',\n    'audit_date_to' => '終了日',\n\n    // Role Settings\n    'roles' => '役割',\n    'role_user_roles' => '役割',\n    'roles_index_desc' => '役割は、ユーザーをグループ化しメンバーにシステム権限を与えるために使用されます。ユーザーが複数の役割のメンバーである場合、与えられた権限は積み重なり、ユーザーはすべての能力を継承します。',\n    'roles_x_users_assigned' => ':count人のユーザーに割り当て|:count人のユーザーに割り当て',\n    'roles_x_permissions_provided' => ':count件の権限|:count件の権限',\n    'roles_assigned_users' => '割り当てユーザ数',\n    'roles_permissions_provided' => '提供される権限数',\n    'role_create' => '役割を作成',\n    'role_delete' => '役割を削除',\n    'role_delete_confirm' => '役割「:roleName」を削除します。',\n    'role_delete_users_assigned' => 'この役割は:userCount人のユーザに付与されています。該当するユーザを他の役割へ移行できます。',\n    'role_delete_no_migration' => \"ユーザを移行しない\",\n    'role_delete_sure' => '本当に役割を削除してよろしいですか？',\n    'role_edit' => '役割を編集',\n    'role_details' => '概要',\n    'role_name' => '役割名',\n    'role_desc' => '役割の説明',\n    'role_mfa_enforced' => '多要素認証を要求する',\n    'role_external_auth_id' => '外部認証ID',\n    'role_system' => 'システム権限',\n    'role_manage_users' => 'ユーザ管理',\n    'role_manage_roles' => '役割と権限の管理',\n    'role_manage_entity_permissions' => '全てのブック, チャプター, ページに対する権限の管理',\n    'role_manage_own_entity_permissions' => '自身のブック, チャプター, ページに対する権限の管理',\n    'role_manage_page_templates' => 'ページテンプレートの管理',\n    'role_access_api' => 'システムのAPIへのアクセス',\n    'role_manage_settings' => 'アプリケーション設定の管理',\n    'role_export_content' => 'コンテンツのエクスポート',\n    'role_import_content' => 'コンテンツのインポート',\n    'role_editor_change' => 'ページエディタの変更',\n    'role_notifications' => '通知の受信と管理',\n    'role_permission_note_users_and_roles' => '技術的には、これらの権限によりシステムのユーザーおよび役割の可視性と検索も提供されます。',\n    'role_asset' => 'アセット権限',\n    'roles_system_warning' => '上記の3つの権限のいずれかを付与することは、ユーザーが自分の特権またはシステム内の他のユーザーの特権を変更できる可能性があることに注意してください。これらの権限は信頼できるユーザーにのみ割り当ててください。',\n    'role_asset_desc' => '各アセットに対するデフォルトの権限を設定します。ここで設定した権限が優先されます。',\n    'role_asset_admins' => '管理者にはすべてのコンテンツへのアクセス権が自動的に付与されますが、これらのオプションはUIオプションを表示または非表示にする場合があります。',\n    'role_asset_image_view_note' => 'これは画像マネージャー内の可視性に関連しています。アップロードされた画像ファイルへの実際のアクセスは、システムの画像保存オプションに依存します。',\n    'role_asset_users_note' => '技術的には、これらの権限によりシステム内のユーザーの可視性と検索も提供されます。',\n    'role_all' => '全て',\n    'role_own' => '自身',\n    'role_controlled_by_asset' => 'このアセットに対し、右記の操作を許可:',\n    'role_save' => '役割を保存',\n    'role_users' => 'この役割を持つユーザー',\n    'role_users_none' => 'この役割が付与されたユーザーはいません',\n\n    // Users\n    'users' => 'ユーザー',\n    'users_index_desc' => 'システム内で個々のユーザーアカウントを作成し、管理します。ユーザーアカウントは、ログインおよびコンテンツとアクティビティの帰属のために使用されます。アクセス許可は主に役割ベースですが、ユーザーコンテンツの所有権やその他の要因も、許可とアクセスに影響する場合があります。',\n    'user_profile' => 'ユーザプロフィール',\n    'users_add_new' => 'ユーザーを追加',\n    'users_search' => 'ユーザー検索',\n    'users_latest_activity' => '最新のアクティビティ',\n    'users_details' => 'ユーザーの詳細',\n    'users_details_desc' => 'このユーザーの表示名とメールアドレスを設定します。メールアドレスは、アプリケーションへのログインに使用されます。',\n    'users_details_desc_no_email' => 'このユーザーの表示名を設定して、他のユーザーが認識できるようにします。',\n    'users_role' => 'ユーザーの役割',\n    'users_role_desc' => 'このユーザーに割り当てる役割を選択します。ユーザーが複数の役割に割り当てられている場合は、それらの役割の権限が重ね合わされ、割り当てられた役割のすべての権限が与えられます。',\n    'users_password' => 'ユーザー パスワード',\n    'users_password_desc' => 'アプリケーションへのログインに利用するパスワードを設定してください。8文字以上である必要があります。',\n    'users_send_invite_text' => 'このユーザーに招待メールを送信してユーザー自身にパスワードを設定してもらうか、あなたがここでパスワードを設定するかを選択できます。',\n    'users_send_invite_option' => 'ユーザーに招待メールを送信',\n    'users_external_auth_id' => '外部認証ID',\n    'users_external_auth_id_desc' => '外部認証システム（SAML2、OIDC、LDAPなど）が使用されている場合、このBookStackユーザーを認証システムアカウントにリンクするIDです。デフォルトの電子メールベース認証を使用する場合は、このフィールドを無視できます。',\n    'users_password_warning' => 'このユーザーのパスワードを変更したい場合にのみ、以下を入力してください。',\n    'users_system_public' => 'このユーザーはアプリケーションにアクセスする全てのゲストを表します。ログインはできませんが、自動的に割り当てられます。',\n    'users_delete' => 'ユーザを削除',\n    'users_delete_named' => 'ユーザ「:userName」を削除',\n    'users_delete_warning' => 'ユーザ「:userName」を完全に削除します。',\n    'users_delete_confirm' => '本当にこのユーザを削除してよろしいですか？',\n    'users_migrate_ownership' => '所有権を移行',\n    'users_migrate_ownership_desc' => '別のユーザーをこのユーザーが現在所有しているすべてのアイテムの所有者にする場合は、ここでユーザーを選択します。',\n    'users_none_selected' => 'ユーザが選択されていません',\n    'users_edit' => 'ユーザー編集',\n    'users_edit_profile' => 'プロフィール編集',\n    'users_avatar' => 'アバター',\n    'users_avatar_desc' => '256pxの正方形である必要があります。',\n    'users_preferred_language' => '使用言語',\n    'users_preferred_language_desc' => 'このオプションは、アプリケーションのユーザーインターフェイスに使用される言語を変更します。これは、ユーザーが作成したコンテンツには影響しません。',\n    'users_social_accounts' => 'ソーシャルアカウント',\n    'users_social_accounts_desc' => 'このユーザーのソーシャルアカウントのステータスを表示します。システムアクセスのためのプライマリ認証システムに加えて ソーシャルアカウントを使用することができます。',\n    'users_social_accounts_info' => 'アカウントを接続すると、ログインが簡単になります。ここでアカウントの接続を解除すると、そのアカウントを経由したログインを禁止できます。接続解除後、各ソーシャルアカウントの設定にてこのアプリケーションへのアクセス許可を解除してください。',\n    'users_social_connect' => 'アカウントを接続',\n    'users_social_disconnect' => 'アカウントを接続解除',\n    'users_social_status_connected' => '接続済み',\n    'users_social_status_disconnected' => '未接続',\n    'users_social_connected' => '「:socialAccount」がプロフィールに接続されました。',\n    'users_social_disconnected' => '「:socialAccount」がプロフィールから接続解除されました。',\n    'users_api_tokens' => 'APIトークン',\n    'users_api_tokens_desc' => 'BookStack REST APIでの認証に使用するアクセストークンを作成・管理します。APIのパーミッションはトークンが属するユーザーを介して管理されます。',\n    'users_api_tokens_none' => 'このユーザーにはAPIトークンが作成されていません',\n    'users_api_tokens_create' => 'トークンを作成',\n    'users_api_tokens_expires' => '有効期限',\n    'users_api_tokens_docs' => 'APIドキュメント',\n    'users_mfa' => '多要素認証',\n    'users_mfa_desc' => 'アカウントのセキュリティを強化するために、多要素認証を設定してください。',\n    'users_mfa_x_methods' => ':count個の手段が設定されています|:count個の手段が設定されています',\n    'users_mfa_configure' => '手段を設定',\n\n    // API Tokens\n    'user_api_token_create' => 'APIトークンの作成',\n    'user_api_token_name' => '名前',\n    'user_api_token_name_desc' => '利用目的を忘れないよう、トークンに読みやすい名前を付けます。',\n    'user_api_token_expiry' => '有効期限',\n    'user_api_token_expiry_desc' => 'このトークンの有効期限が切れる日付を設定します。この日付を過ぎると、このトークンを使用したリクエストは機能しなくなります。このフィールドを空白のままにすると、100年先に有効期限が設定されます。',\n    'user_api_token_create_secret_message' => 'このトークンを作成するとすぐに、「トークンID」と「トークンシークレット」が生成されて表示されます。シークレットは一度しか表示されないため、続行する前に必ず値を安全な場所にコピーしてください。',\n    'user_api_token' => 'APIトークン',\n    'user_api_token_id' => 'トークンID',\n    'user_api_token_id_desc' => 'これは、システムが生成した編集不可能なトークンの識別子で、APIリクエストで提供する必要があります。',\n    'user_api_token_secret' => 'トークンシークレット',\n    'user_api_token_secret_desc' => 'これは、システムで生成されたトークンシークレットであり、APIリクエストで提供する必要があります。これは一度しか表示されないので、この値を安全な場所にコピーしてください。',\n    'user_api_token_created' => 'トークンの作成: :timeAgo',\n    'user_api_token_updated' => 'トークンの更新: :timeAgo',\n    'user_api_token_delete' => 'トークンを削除',\n    'user_api_token_delete_warning' => 'これにより、このAPIトークン「:tokenName」がシステムから完全に削除されます。',\n    'user_api_token_delete_confirm' => 'このAPIトークンを削除してもよろしいですか？',\n\n    // Webhooks\n    'webhooks' => 'Webhook',\n    'webhooks_index_desc' => 'Webhookは、システム内で特定のアクションやイベントが発生したときに外部URLにデータを送信する方法で、メッセージングシステムや通知システムなどの外部プラットフォームとのイベントベースの統合を可能にします。',\n    'webhooks_x_trigger_events' => ':count個のトリガーイベント|:count個のトリガーイベント',\n    'webhooks_create' => 'Webhookを作成',\n    'webhooks_none_created' => 'Webhookはまだ作成されていません。',\n    'webhooks_edit' => 'Webhookを編集',\n    'webhooks_save' => 'Webhookを保存',\n    'webhooks_details' => 'Webhookの詳細',\n    'webhooks_details_desc' => 'ユーザーフレンドリーな名前とWebhookデータの送信先にするPOSTエンドポイントを指定します。',\n    'webhooks_events' => 'Webhookのイベント',\n    'webhooks_events_desc' => 'このWebhookの呼び出しをトリガーするすべてのイベントを選択します。',\n    'webhooks_events_warning' => 'これらのイベントはカスタム権限が適用されている場合でも、選択したすべてのイベントに対してトリガーされることに注意してください。このWebhookの利用により機密コンテンツが公開されないことを確認してください。',\n    'webhooks_events_all' => '全てのシステムイベント',\n    'webhooks_name' => 'Webhook名',\n    'webhooks_timeout' => 'Webhookリクエストタイムアウト (秒)',\n    'webhooks_endpoint' => 'Webhookエンドポイント',\n    'webhooks_active' => '有効なWebhook',\n    'webhook_events_table_header' => 'イベント',\n    'webhooks_delete' => 'Webhookを削除',\n    'webhooks_delete_warning' => 'これにより、このWebhook「:webhookName」がシステムから完全に削除されます。',\n    'webhooks_delete_confirm' => 'このWebhookを削除してよろしいですか？',\n    'webhooks_format_example' => 'Webhookのフォーマット例',\n    'webhooks_format_example_desc' => 'Webhookのデータは、設定されたエンドポイントにPOSTリクエストにより以下のフォーマットのJSONで送信されます。related_item と url プロパティはオプションであり、トリガーされるイベントの種類によって異なります。',\n    'webhooks_status' => 'Webhookの状態',\n    'webhooks_last_called' => '最後の実行:',\n    'webhooks_last_errored' => '最後のエラー:',\n    'webhooks_last_error_message' => '最後のエラーのメッセージ:',\n\n    // Licensing\n    'licenses' => 'ライセンス',\n    'licenses_desc' => 'このページではBookStackとBookStackで使用されるプロジェクトやライブラリのライセンス情報を詳しく説明します。開発環境でのみ使用するものも多数含まれています。',\n    'licenses_bookstack' => 'BookStack ライセンス',\n    'licenses_php' => 'PHPライブラリライセンス',\n    'licenses_js' => 'JavaScriptライブラリライセンス',\n    'licenses_other' => 'その他のライセンス',\n    'license_details' => 'ライセンス詳細',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/ja/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attributeに同意する必要があります。',\n    'active_url'           => ':attributeは正しいURLではありません。',\n    'after'                => ':attributeは:date以降である必要があります。',\n    'alpha'                => ':attributeは文字のみが含められます。',\n    'alpha_dash'           => ':attributeは文字, 数値, ハイフンのみが含められます。',\n    'alpha_num'            => ':attributeは文字と数値のみが含められます。',\n    'array'                => ':attributeは配列である必要があります。',\n    'backup_codes'         => '提供されたコードは無効か、またはすでに使用されています。',\n    'before'               => ':attributeは:date以前である必要があります。',\n    'between'              => [\n        'numeric' => ':attributeは:min〜:maxである必要があります。',\n        'file'    => ':attributeは:min〜:maxキロバイトである必要があります。',\n        'string'  => ':attributeは:min〜:max文字である必要があります。',\n        'array'   => ':attributeは:min〜:max個である必要があります。',\n    ],\n    'boolean'              => ':attributeはtrueまたはfalseである必要があります。',\n    'confirmed'            => ':attributeの確認が一致しません。',\n    'date'                 => ':attributeは正しい日時ではありません。',\n    'date_format'          => ':attributeが:formatのフォーマットと一致しません。',\n    'different'            => ':attributeと:otherは異なる必要があります。',\n    'digits'               => ':attributeは:digitsデジットである必要があります',\n    'digits_between'       => ':attributeは:min〜:maxである必要があります。',\n    'email'                => ':attributeは正しいEメールアドレスである必要があります。',\n    'ends_with' => ':attributeは:valuesのいずれかで終わる必要があります。',\n    'file'                 => ':attributeは有効なファイルである必要があります。',\n    'filled'               => ':attributeは必須です。',\n    'gt'                   => [\n        'numeric' => ':attributeは:valueより大きな値である必要があります。',\n        'file'    => ':attributeは:valueキロバイトより大きなファイルである必要があります。',\n        'string'  => ':attributeは:value文字より長い必要があります。',\n        'array'   => ':attributeには:value個より多くのアイテムを指定する必要があります。',\n    ],\n    'gte'                  => [\n        'numeric' => ':attributeは:value以上の値である必要があります。',\n        'file'    => ':attributeは:valueキロバイト以上のファイルである必要があります。',\n        'string'  => ':attributeは:value文字以上である必要があります。',\n        'array'   => ':attributeには:value個以上のアイテムを指定する必要があります。',\n    ],\n    'exists'               => '選択された:attributeは不正です。',\n    'image'                => ':attributeは画像である必要があります。',\n    'image_extension'      => ':attributeは有効かつサポートされている拡張子の画像である必要があります。',\n    'in'                   => '選択された:attributeは不正です。',\n    'integer'              => ':attributeは数値である必要があります。',\n    'ip'                   => ':attributeは正しいIPアドレスである必要があります。',\n    'ipv4'                 => ':attributeは有効なIPv4アドレスである必要があります。',\n    'ipv6'                 => ':attributeは有効なIPv6アドレスである必要があります。',\n    'json'                 => ':attributeは有効なJSON文字列である必要があります。',\n    'lt'                   => [\n        'numeric' => ':attributeは:valueより小さな値である必要があります。',\n        'file'    => ':attributeは:valueキロバイトより小さなファイルである必要があります。',\n        'string'  => ':attributeは:value文字より短い必要があります。',\n        'array'   => ':attributeには:value個より少ないアイテムを指定する必要があります。',\n    ],\n    'lte'                  => [\n        'numeric' => ':attributeは:value以下の値である必要があります。',\n        'file'    => ':attributeは:valueキロバイト以下のファイルである必要があります。',\n        'string'  => ':attributeは:value文字以下である必要があります。',\n        'array'   => ':attributeには:value個以下のアイテムを指定する必要があります。',\n    ],\n    'max'                  => [\n        'numeric' => ':attributeは:maxを越えることができません。',\n        'file'    => ':attributeは:maxキロバイトを越えることができません。',\n        'string'  => ':attributeは:max文字をこえることができません。',\n        'array'   => ':attributeは:max個を越えることができません。',\n    ],\n    'mimes'                => ':attributeのファイルタイプは以下のみが許可されています: :values.',\n    'min'                  => [\n        'numeric' => ':attributeは:min以上である必要があります。',\n        'file'    => ':attributeは:minキロバイト以上である必要があります。',\n        'string'  => ':attributeは:min文字以上である必要があります。',\n        'array'   => ':attributeは:min個以上である必要があります。',\n    ],\n    'not_in'               => '選択された:attributeは不正です。',\n    'not_regex'            => ':attributeの形式は不正です。',\n    'numeric'              => ':attributeは数値である必要があります。',\n    'regex'                => ':attributeのフォーマットは不正です。',\n    'required'             => ':attributeは必須です。',\n    'required_if'          => ':otherが:valueである場合、:attributeは必須です。',\n    'required_with'        => ':valuesが設定されている場合、:attributeは必須です。',\n    'required_with_all'    => ':valuesが設定されている場合、:attributeは必須です。',\n    'required_without'     => ':valuesが設定されていない場合、:attributeは必須です。',\n    'required_without_all' => ':valuesが設定されていない場合、:attributeは必須です。',\n    'same'                 => ':attributeと:otherは一致している必要があります。',\n    'safe_url'             => '提供されたリンクは安全ではない可能性があります。',\n    'size'                 => [\n        'numeric' => ':attributeは:sizeである必要があります。',\n        'file'    => ':attributeは:sizeキロバイトである必要があります。',\n        'string'  => ':attributeは:size文字である必要があります。',\n        'array'   => ':attributeは:size個である必要があります。',\n    ],\n    'string'               => ':attributeは文字列である必要があります。',\n    'timezone'             => ':attributeは正しいタイムゾーンである必要があります。',\n    'totp'                 => '提供されたコードが無効または期限切れです。',\n    'unique'               => ':attributeは既に使用されています。',\n    'url'                  => ':attributeのフォーマットは不正です。',\n    'uploaded'             => 'ファイルをアップロードできませんでした。サーバーがこのサイズのファイルを受け付けていない可能性があります。',\n\n    'zip_file' => ':attribute はZIP 内のファイルを参照する必要があります。',\n    'zip_file_size' => ':attribute は :size MB を超えてはいけません。',\n    'zip_file_mime' => ':attribute は種別 :validType のファイルを参照する必要がありますが、種別 :foundType となっています。',\n    'zip_model_expected' => 'データオブジェクトが期待されますが、\":type\" が見つかりました。',\n    'zip_unique' => 'ZIP内のオブジェクトタイプに :attribute が一意である必要があります。',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'パスワードの確認は必須です。',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/ka/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'created page',\n    'page_create_notification'    => 'Page successfully created',\n    'page_update'                 => 'updated page',\n    'page_update_notification'    => 'Page successfully updated',\n    'page_delete'                 => 'deleted page',\n    'page_delete_notification'    => 'Page successfully deleted',\n    'page_restore'                => 'restored page',\n    'page_restore_notification'   => 'Page successfully restored',\n    'page_move'                   => 'moved page',\n    'page_move_notification'      => 'Page successfully moved',\n\n    // Chapters\n    'chapter_create'              => 'created chapter',\n    'chapter_create_notification' => 'Chapter successfully created',\n    'chapter_update'              => 'updated chapter',\n    'chapter_update_notification' => 'Chapter successfully updated',\n    'chapter_delete'              => 'deleted chapter',\n    'chapter_delete_notification' => 'Chapter successfully deleted',\n    'chapter_move'                => 'moved chapter',\n    'chapter_move_notification' => 'Chapter successfully moved',\n\n    // Books\n    'book_create'                 => 'created book',\n    'book_create_notification'    => 'Book successfully created',\n    'book_create_from_chapter'              => 'converted chapter to book',\n    'book_create_from_chapter_notification' => 'Chapter successfully converted to a book',\n    'book_update'                 => 'updated book',\n    'book_update_notification'    => 'Book successfully updated',\n    'book_delete'                 => 'deleted book',\n    'book_delete_notification'    => 'Book successfully deleted',\n    'book_sort'                   => 'sorted book',\n    'book_sort_notification'      => 'Book successfully re-sorted',\n\n    // Bookshelves\n    'bookshelf_create'            => 'created shelf',\n    'bookshelf_create_notification'    => 'Shelf successfully created',\n    'bookshelf_create_from_book'    => 'converted book to shelf',\n    'bookshelf_create_from_book_notification'    => 'Book successfully converted to a shelf',\n    'bookshelf_update'                 => 'updated shelf',\n    'bookshelf_update_notification'    => 'Shelf successfully updated',\n    'bookshelf_delete'                 => 'deleted shelf',\n    'bookshelf_delete_notification'    => 'Shelf successfully deleted',\n\n    // Revisions\n    'revision_restore' => 'restored revision',\n    'revision_delete' => 'deleted revision',\n    'revision_delete_notification' => 'Revision successfully deleted',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" has been added to your favourites',\n    'favourite_remove_notification' => '\":name\" has been removed from your favourites',\n\n    // Watching\n    'watch_update_level_notification' => 'Watch preferences successfully updated',\n\n    // Auth\n    'auth_login' => 'logged in',\n    'auth_register' => 'registered as new user',\n    'auth_password_reset_request' => 'requested user password reset',\n    'auth_password_reset_update' => 'reset user password',\n    'mfa_setup_method' => 'configured MFA method',\n    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',\n    'mfa_remove_method' => 'removed MFA method',\n    'mfa_remove_method_notification' => 'მულტი-ფაქტორული მეთოდი წარმატებით მოიხსნა',\n\n    // Settings\n    'settings_update' => 'updated settings',\n    'settings_update_notification' => 'Settings successfully updated',\n    'maintenance_action_run' => 'ran maintenance action',\n\n    // Webhooks\n    'webhook_create' => 'created webhook',\n    'webhook_create_notification' => 'Webhook successfully created',\n    'webhook_update' => 'updated webhook',\n    'webhook_update_notification' => 'Webhook successfully updated',\n    'webhook_delete' => 'deleted webhook',\n    'webhook_delete_notification' => 'Webhook successfully deleted',\n\n    // Imports\n    'import_create' => 'created import',\n    'import_create_notification' => 'Import successfully uploaded',\n    'import_run' => 'updated import',\n    'import_run_notification' => 'Content successfully imported',\n    'import_delete' => 'deleted import',\n    'import_delete_notification' => 'Import successfully deleted',\n\n    // Users\n    'user_create' => 'created user',\n    'user_create_notification' => 'User successfully created',\n    'user_update' => 'updated user',\n    'user_update_notification' => 'User successfully updated',\n    'user_delete' => 'deleted user',\n    'user_delete_notification' => 'User successfully removed',\n\n    // API Tokens\n    'api_token_create' => 'created API token',\n    'api_token_create_notification' => 'API token successfully created',\n    'api_token_update' => 'updated API token',\n    'api_token_update_notification' => 'API token successfully updated',\n    'api_token_delete' => 'deleted API token',\n    'api_token_delete_notification' => 'API token successfully deleted',\n\n    // Roles\n    'role_create' => 'created role',\n    'role_create_notification' => 'Role successfully created',\n    'role_update' => 'updated role',\n    'role_update_notification' => 'Role successfully updated',\n    'role_delete' => 'deleted role',\n    'role_delete_notification' => 'Role successfully deleted',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'emptied recycle bin',\n    'recycle_bin_restore' => 'restored from recycle bin',\n    'recycle_bin_destroy' => 'removed from recycle bin',\n\n    // Comments\n    'commented_on'                => 'commented on',\n    'comment_create'              => 'added comment',\n    'comment_update'              => 'updated comment',\n    'comment_delete'              => 'deleted comment',\n\n    // Sort Rules\n    'sort_rule_create' => 'created sort rule',\n    'sort_rule_create_notification' => 'Sort rule successfully created',\n    'sort_rule_update' => 'updated sort rule',\n    'sort_rule_update_notification' => 'Sort rule successfully updated',\n    'sort_rule_delete' => 'deleted sort rule',\n    'sort_rule_delete_notification' => 'Sort rule successfully deleted',\n\n    // Other\n    'permissions_update'          => 'updated permissions',\n];\n"
  },
  {
    "path": "lang/ka/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'These credentials do not match our records.',\n    'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',\n\n    // Login & Register\n    'sign_up' => 'Sign up',\n    'log_in' => 'Log in',\n    'log_in_with' => 'Login with :socialDriver',\n    'sign_up_with' => 'Sign up with :socialDriver',\n    'logout' => 'Logout',\n\n    'name' => 'Name',\n    'username' => 'Username',\n    'email' => 'Email',\n    'password' => 'Password',\n    'password_confirm' => 'Confirm Password',\n    'password_hint' => 'Must be at least 8 characters',\n    'forgot_password' => 'Forgot Password?',\n    'remember_me' => 'Remember Me',\n    'ldap_email_hint' => 'Please enter an email to use for this account.',\n    'create_account' => 'Create Account',\n    'already_have_account' => 'Already have an account?',\n    'dont_have_account' => 'Don\\'t have an account?',\n    'social_login' => 'Social Login',\n    'social_registration' => 'Social Registration',\n    'social_registration_text' => 'Register and sign in using another service.',\n\n    'register_thanks' => 'Thanks for registering!',\n    'register_confirm' => 'Please check your email and click the confirmation button to access :appName.',\n    'registrations_disabled' => 'Registrations are currently disabled',\n    'registration_email_domain_invalid' => 'That email domain does not have access to this application',\n    'register_success' => 'Thanks for signing up! You are now registered and signed in.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Attempting Login',\n    'auto_init_starting_desc' => 'We\\'re contacting your authentication system to start the login process. If there\\'s no progress after 5 seconds you can try clicking the link below.',\n    'auto_init_start_link' => 'Proceed with authentication',\n\n    // Password Reset\n    'reset_password' => 'Reset Password',\n    'reset_password_send_instructions' => 'Enter your email below and you will be sent an email with a password reset link.',\n    'reset_password_send_button' => 'Send Reset Link',\n    'reset_password_sent' => 'A password reset link will be sent to :email if that email address is found in the system.',\n    'reset_password_success' => 'Your password has been successfully reset.',\n    'email_reset_subject' => 'Reset your :appName password',\n    'email_reset_text' => 'You are receiving this email because we received a password reset request for your account.',\n    'email_reset_not_requested' => 'If you did not request a password reset, no further action is required.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Confirm your email on :appName',\n    'email_confirm_greeting' => 'Thanks for joining :appName!',\n    'email_confirm_text' => 'Please confirm your email address by clicking the button below:',\n    'email_confirm_action' => 'Confirm Email',\n    'email_confirm_send_error' => 'Email confirmation required but the system could not send the email. Contact the admin to ensure email is set up correctly.',\n    'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',\n    'email_confirm_resent' => 'Confirmation email resent, Please check your inbox.',\n    'email_confirm_thanks' => 'Thanks for confirming!',\n    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the \"Continue\" link below to proceed.',\n\n    'email_not_confirmed' => 'Email Address Not Confirmed',\n    'email_not_confirmed_text' => 'Your email address has not yet been confirmed.',\n    'email_not_confirmed_click_link' => 'Please click the link in the email that was sent shortly after you registered.',\n    'email_not_confirmed_resend' => 'If you cannot find the email you can re-send the confirmation email by submitting the form below.',\n    'email_not_confirmed_resend_button' => 'Resend Confirmation Email',\n\n    // User Invite\n    'user_invite_email_subject' => 'You have been invited to join :appName!',\n    'user_invite_email_greeting' => 'An account has been created for you on :appName.',\n    'user_invite_email_text' => 'Click the button below to set an account password and gain access:',\n    'user_invite_email_action' => 'Set Account Password',\n    'user_invite_page_welcome' => 'Welcome to :appName!',\n    'user_invite_page_text' => 'To finalise your account and gain access you need to set a password which will be used to log-in to :appName on future visits.',\n    'user_invite_page_confirm_button' => 'Confirm Password',\n    'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Setup Multi-Factor Authentication',\n    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'mfa_setup_configured' => 'Already configured',\n    'mfa_setup_reconfigure' => 'Reconfigure',\n    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',\n    'mfa_setup_action' => 'Setup',\n    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',\n    'mfa_option_totp_title' => 'Mobile App',\n    'mfa_option_totp_desc' => 'To use multi-factor authentication you\\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Backup Codes',\n    'mfa_option_backup_codes_desc' => 'Generates a set of one-time-use backup codes which you\\'ll enter on login to verify your identity. Make sure to store these in a safe & secure place.',\n    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',\n    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',\n    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\\'ll be able to use one of the codes as a second authentication mechanism.',\n    'mfa_gen_backup_codes_download' => 'Download Codes',\n    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',\n    'mfa_gen_totp_title' => 'Mobile App Setup',\n    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',\n    'mfa_gen_totp_verify_setup' => 'Verify Setup',\n    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',\n    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',\n    'mfa_verify_access' => 'Verify Access',\n    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\\'re granted access. Verify using one of your configured methods to continue.',\n    'mfa_verify_no_methods' => 'No Methods Configured',\n    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\\'ll need to set up at least one method before you gain access.',\n    'mfa_verify_use_totp' => 'Verify using a mobile app',\n    'mfa_verify_use_backup_codes' => 'Verify using a backup code',\n    'mfa_verify_backup_code' => 'Backup Code',\n    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',\n    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',\n    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',\n    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',\n];\n"
  },
  {
    "path": "lang/ka/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Cancel',\n    'close' => 'Close',\n    'confirm' => 'Confirm',\n    'back' => 'Back',\n    'save' => 'Save',\n    'continue' => 'Continue',\n    'select' => 'Select',\n    'toggle_all' => 'Toggle All',\n    'more' => 'More',\n\n    // Form Labels\n    'name' => 'Name',\n    'description' => 'Description',\n    'role' => 'Role',\n    'cover_image' => 'Cover image',\n    'cover_image_description' => 'This image should be approximately 440x250px although it will be flexibly scaled & cropped to fit the user interface in different scenarios as required, so actual dimensions for display will differ.',\n\n    // Actions\n    'actions' => 'Actions',\n    'view' => 'View',\n    'view_all' => 'View All',\n    'new' => 'New',\n    'create' => 'Create',\n    'update' => 'Update',\n    'edit' => 'Edit',\n    'archive' => 'Archive',\n    'unarchive' => 'Un-Archive',\n    'sort' => 'Sort',\n    'move' => 'Move',\n    'copy' => 'Copy',\n    'reply' => 'Reply',\n    'delete' => 'Delete',\n    'delete_confirm' => 'Confirm Deletion',\n    'search' => 'Search',\n    'search_clear' => 'Clear Search',\n    'reset' => 'Reset',\n    'remove' => 'Remove',\n    'add' => 'Add',\n    'configure' => 'Configure',\n    'manage' => 'Manage',\n    'fullscreen' => 'Fullscreen',\n    'favourite' => 'Favourite',\n    'unfavourite' => 'Unfavourite',\n    'next' => 'Next',\n    'previous' => 'Previous',\n    'filter_active' => 'Active Filter:',\n    'filter_clear' => 'Clear Filter',\n    'download' => 'Download',\n    'open_in_tab' => 'Open in Tab',\n    'open' => 'Open',\n\n    // Sort Options\n    'sort_options' => 'Sort Options',\n    'sort_direction_toggle' => 'Sort Direction Toggle',\n    'sort_ascending' => 'Sort Ascending',\n    'sort_descending' => 'Sort Descending',\n    'sort_name' => 'Name',\n    'sort_default' => 'Default',\n    'sort_created_at' => 'Created Date',\n    'sort_updated_at' => 'Updated Date',\n\n    // Misc\n    'deleted_user' => 'Deleted User',\n    'no_activity' => 'No activity to show',\n    'no_items' => 'No items available',\n    'back_to_top' => 'Back to top',\n    'skip_to_main_content' => 'Skip to main content',\n    'toggle_details' => 'Toggle Details',\n    'toggle_thumbnails' => 'Toggle Thumbnails',\n    'details' => 'Details',\n    'grid_view' => 'Grid View',\n    'list_view' => 'List View',\n    'default' => 'Default',\n    'breadcrumb' => 'Breadcrumb',\n    'status' => 'Status',\n    'status_active' => 'Active',\n    'status_inactive' => 'Inactive',\n    'never' => 'Never',\n    'none' => 'None',\n\n    // Header\n    'homepage' => 'Homepage',\n    'header_menu_expand' => 'Expand Header Menu',\n    'profile_menu' => 'Profile Menu',\n    'view_profile' => 'View Profile',\n    'edit_profile' => 'Edit Profile',\n    'dark_mode' => 'Dark Mode',\n    'light_mode' => 'Light Mode',\n    'global_search' => 'Global Search',\n\n    // Layout tabs\n    'tab_info' => 'Info',\n    'tab_info_label' => 'Tab: Show Secondary Information',\n    'tab_content' => 'Content',\n    'tab_content_label' => 'Tab: Show Primary Content',\n\n    // Email Content\n    'email_action_help' => 'If you’re having trouble clicking the \":actionText\" button, copy and paste the URL below into your web browser:',\n    'email_rights' => 'All rights reserved',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Privacy Policy',\n    'terms_of_service' => 'Terms of Service',\n\n    // OpenSearch\n    'opensearch_description' => 'Search :appName',\n];\n"
  },
  {
    "path": "lang/ka/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Image Select',\n    'image_list' => 'Image List',\n    'image_details' => 'Image Details',\n    'image_upload' => 'Upload Image',\n    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',\n    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the \"Upload Image\" button above.',\n    'image_all' => 'All',\n    'image_all_title' => 'View all images',\n    'image_book_title' => 'View images uploaded to this book',\n    'image_page_title' => 'View images uploaded to this page',\n    'image_search_hint' => 'Search by image name',\n    'image_uploaded' => 'Uploaded :uploadedDate',\n    'image_uploaded_by' => 'Uploaded by :userName',\n    'image_uploaded_to' => 'Uploaded to :pageLink',\n    'image_updated' => 'Updated :updateDate',\n    'image_load_more' => 'Load More',\n    'image_image_name' => 'Image Name',\n    'image_delete_used' => 'This image is used in the pages below.',\n    'image_delete_confirm_text' => 'Are you sure you want to delete this image?',\n    'image_select_image' => 'Select Image',\n    'image_dropzone' => 'Drop images or click here to upload',\n    'image_dropzone_drop' => 'Drop images here to upload',\n    'images_deleted' => 'Images Deleted',\n    'image_preview' => 'Image Preview',\n    'image_upload_success' => 'Image uploaded successfully',\n    'image_update_success' => 'Image details successfully updated',\n    'image_delete_success' => 'Image successfully deleted',\n    'image_replace' => 'Replace Image',\n    'image_replace_success' => 'Image file successfully updated',\n    'image_rebuild_thumbs' => 'Regenerate Size Variations',\n    'image_rebuild_thumbs_success' => 'Image size variations successfully rebuilt!',\n\n    // Code Editor\n    'code_editor' => 'Edit Code',\n    'code_language' => 'Code Language',\n    'code_content' => 'Code Content',\n    'code_session_history' => 'Session History',\n    'code_save' => 'Save Code',\n];\n"
  },
  {
    "path": "lang/ka/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'General',\n    'advanced' => 'Advanced',\n    'none' => 'None',\n    'cancel' => 'Cancel',\n    'save' => 'Save',\n    'close' => 'Close',\n    'apply' => 'Apply',\n    'undo' => 'Undo',\n    'redo' => 'Redo',\n    'left' => 'Left',\n    'center' => 'Center',\n    'right' => 'Right',\n    'top' => 'Top',\n    'middle' => 'Middle',\n    'bottom' => 'Bottom',\n    'width' => 'Width',\n    'height' => 'Height',\n    'More' => 'More',\n    'select' => 'Select...',\n\n    // Toolbar\n    'formats' => 'Formats',\n    'header_large' => 'Large Header',\n    'header_medium' => 'Medium Header',\n    'header_small' => 'Small Header',\n    'header_tiny' => 'Tiny Header',\n    'paragraph' => 'Paragraph',\n    'blockquote' => 'Blockquote',\n    'inline_code' => 'Inline code',\n    'callouts' => 'Callouts',\n    'callout_information' => 'Information',\n    'callout_success' => 'Success',\n    'callout_warning' => 'Warning',\n    'callout_danger' => 'Danger',\n    'bold' => 'Bold',\n    'italic' => 'Italic',\n    'underline' => 'Underline',\n    'strikethrough' => 'Strikethrough',\n    'superscript' => 'Superscript',\n    'subscript' => 'Subscript',\n    'text_color' => 'Text color',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Custom color',\n    'remove_color' => 'Remove color',\n    'background_color' => 'Background color',\n    'align_left' => 'Align left',\n    'align_center' => 'Align center',\n    'align_right' => 'Align right',\n    'align_justify' => 'Justify',\n    'list_bullet' => 'Bullet list',\n    'list_numbered' => 'Numbered list',\n    'list_task' => 'Task list',\n    'indent_increase' => 'Increase indent',\n    'indent_decrease' => 'Decrease indent',\n    'table' => 'Table',\n    'insert_image' => 'Insert image',\n    'insert_image_title' => 'Insert/Edit Image',\n    'insert_link' => 'Insert/edit link',\n    'insert_link_title' => 'Insert/Edit Link',\n    'insert_horizontal_line' => 'Insert horizontal line',\n    'insert_code_block' => 'Insert code block',\n    'edit_code_block' => 'Edit code block',\n    'insert_drawing' => 'Insert/edit drawing',\n    'drawing_manager' => 'Drawing manager',\n    'insert_media' => 'Insert/edit media',\n    'insert_media_title' => 'Insert/Edit Media',\n    'clear_formatting' => 'Clear formatting',\n    'source_code' => 'Source code',\n    'source_code_title' => 'Source Code',\n    'fullscreen' => 'Fullscreen',\n    'image_options' => 'Image options',\n\n    // Tables\n    'table_properties' => 'Table properties',\n    'table_properties_title' => 'Table Properties',\n    'delete_table' => 'Delete table',\n    'table_clear_formatting' => 'Clear table formatting',\n    'resize_to_contents' => 'Resize to contents',\n    'row_header' => 'Row header',\n    'insert_row_before' => 'Insert row before',\n    'insert_row_after' => 'Insert row after',\n    'delete_row' => 'Delete row',\n    'insert_column_before' => 'Insert column before',\n    'insert_column_after' => 'Insert column after',\n    'delete_column' => 'Delete column',\n    'table_cell' => 'Cell',\n    'table_row' => 'Row',\n    'table_column' => 'Column',\n    'cell_properties' => 'Cell properties',\n    'cell_properties_title' => 'Cell Properties',\n    'cell_type' => 'Cell type',\n    'cell_type_cell' => 'Cell',\n    'cell_scope' => 'Scope',\n    'cell_type_header' => 'Header cell',\n    'merge_cells' => 'Merge cells',\n    'split_cell' => 'Split cell',\n    'table_row_group' => 'Row Group',\n    'table_column_group' => 'Column Group',\n    'horizontal_align' => 'Horizontal align',\n    'vertical_align' => 'Vertical align',\n    'border_width' => 'Border width',\n    'border_style' => 'Border style',\n    'border_color' => 'Border color',\n    'row_properties' => 'Row properties',\n    'row_properties_title' => 'Row Properties',\n    'cut_row' => 'Cut row',\n    'copy_row' => 'Copy row',\n    'paste_row_before' => 'Paste row before',\n    'paste_row_after' => 'Paste row after',\n    'row_type' => 'Row type',\n    'row_type_header' => 'Header',\n    'row_type_body' => 'Body',\n    'row_type_footer' => 'Footer',\n    'alignment' => 'Alignment',\n    'cut_column' => 'Cut column',\n    'copy_column' => 'Copy column',\n    'paste_column_before' => 'Paste column before',\n    'paste_column_after' => 'Paste column after',\n    'cell_padding' => 'Cell padding',\n    'cell_spacing' => 'Cell spacing',\n    'caption' => 'Caption',\n    'show_caption' => 'Show caption',\n    'constrain' => 'Constrain proportions',\n    'cell_border_solid' => 'Solid',\n    'cell_border_dotted' => 'Dotted',\n    'cell_border_dashed' => 'Dashed',\n    'cell_border_double' => 'Double',\n    'cell_border_groove' => 'Groove',\n    'cell_border_ridge' => 'Ridge',\n    'cell_border_inset' => 'Inset',\n    'cell_border_outset' => 'Outset',\n    'cell_border_none' => 'None',\n    'cell_border_hidden' => 'Hidden',\n\n    // Images, links, details/summary & embed\n    'source' => 'Source',\n    'alt_desc' => 'Alternative description',\n    'embed' => 'Embed',\n    'paste_embed' => 'Paste your embed code below:',\n    'url' => 'URL',\n    'text_to_display' => 'Text to display',\n    'title' => 'Title',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Open link',\n    'open_link_in' => 'Open link in...',\n    'open_link_current' => 'Current window',\n    'open_link_new' => 'New window',\n    'remove_link' => 'Remove link',\n    'insert_collapsible' => 'Insert collapsible block',\n    'collapsible_unwrap' => 'Unwrap',\n    'edit_label' => 'Edit label',\n    'toggle_open_closed' => 'Toggle open/closed',\n    'collapsible_edit' => 'Edit collapsible block',\n    'toggle_label' => 'Toggle label',\n\n    // About view\n    'about' => 'About the editor',\n    'about_title' => 'About the WYSIWYG Editor',\n    'editor_license' => 'Editor License & Copyright',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under the MIT license.',\n    'editor_tiny_license_link' => 'The copyright and license details of TinyMCE can be found here.',\n    'save_continue' => 'Save Page & Continue',\n    'callouts_cycle' => '(Keep pressing to toggle through types)',\n    'link_selector' => 'Link to content',\n    'shortcuts' => 'Shortcuts',\n    'shortcut' => 'Shortcut',\n    'shortcuts_intro' => 'The following shortcuts are available in the editor:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Description',\n];\n"
  },
  {
    "path": "lang/ka/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Recently Created',\n    'recently_created_pages' => 'Recently Created Pages',\n    'recently_updated_pages' => 'Recently Updated Pages',\n    'recently_created_chapters' => 'Recently Created Chapters',\n    'recently_created_books' => 'Recently Created Books',\n    'recently_created_shelves' => 'Recently Created Shelves',\n    'recently_update' => 'Recently Updated',\n    'recently_viewed' => 'Recently Viewed',\n    'recent_activity' => 'Recent Activity',\n    'create_now' => 'Create one now',\n    'revisions' => 'Revisions',\n    'meta_revision' => 'Revision #:revisionCount',\n    'meta_created' => 'Created :timeLength',\n    'meta_created_name' => 'Created :timeLength by :user',\n    'meta_updated' => 'Updated :timeLength',\n    'meta_updated_name' => 'Updated :timeLength by :user',\n    'meta_owned_name' => 'Owned by :user',\n    'meta_reference_count' => 'Referenced by :count item|Referenced by :count items',\n    'entity_select' => 'Entity Select',\n    'entity_select_lack_permission' => 'You don\\'t have the required permissions to select this item',\n    'images' => 'Images',\n    'my_recent_drafts' => 'My Recent Drafts',\n    'my_recently_viewed' => 'My Recently Viewed',\n    'my_most_viewed_favourites' => 'My Most Viewed Favourites',\n    'my_favourites' => 'My Favourites',\n    'no_pages_viewed' => 'You have not viewed any pages',\n    'no_pages_recently_created' => 'No pages have been recently created',\n    'no_pages_recently_updated' => 'No pages have been recently updated',\n    'export' => 'Export',\n    'export_html' => 'Contained Web File',\n    'export_pdf' => 'PDF File',\n    'export_text' => 'Plain Text File',\n    'export_md' => 'Markdown File',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Default Page Template',\n    'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',\n    'default_template_select' => 'Select a template page',\n    'import' => 'Import',\n    'import_validate' => 'Validate Import',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'Select ZIP file to upload',\n    'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',\n    'import_pending' => 'Pending Imports',\n    'import_pending_none' => 'No imports have been started.',\n    'import_continue' => 'Continue Import',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Run Import',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Uploaded by',\n    'import_location' => 'Import Location',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'Import Errors',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Permissions',\n    'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.',\n    'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.',\n    'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.',\n    'permissions_save' => 'Save Permissions',\n    'permissions_owner' => 'Owner',\n    'permissions_role_everyone_else' => 'Everyone Else',\n    'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',\n    'permissions_role_override' => 'Override permissions for role',\n    'permissions_inherit_defaults' => 'Inherit defaults',\n\n    // Search\n    'search_results' => 'Search Results',\n    'search_total_results_found' => ':count result found|:count total results found',\n    'search_clear' => 'Clear Search',\n    'search_no_pages' => 'No pages matched this search',\n    'search_for_term' => 'Search for :term',\n    'search_more' => 'More Results',\n    'search_advanced' => 'Advanced Search',\n    'search_terms' => 'Search Terms',\n    'search_content_type' => 'Content Type',\n    'search_exact_matches' => 'Exact Matches',\n    'search_tags' => 'Tag Searches',\n    'search_options' => 'Options',\n    'search_viewed_by_me' => 'Viewed by me',\n    'search_not_viewed_by_me' => 'Not viewed by me',\n    'search_permissions_set' => 'Permissions set',\n    'search_created_by_me' => 'Created by me',\n    'search_updated_by_me' => 'Updated by me',\n    'search_owned_by_me' => 'Owned by me',\n    'search_date_options' => 'Date Options',\n    'search_updated_before' => 'Updated before',\n    'search_updated_after' => 'Updated after',\n    'search_created_before' => 'Created before',\n    'search_created_after' => 'Created after',\n    'search_set_date' => 'Set Date',\n    'search_update' => 'Update Search',\n\n    // Shelves\n    'shelf' => 'Shelf',\n    'shelves' => 'Shelves',\n    'x_shelves' => ':count Shelf|:count Shelves',\n    'shelves_empty' => 'No shelves have been created',\n    'shelves_create' => 'Create New Shelf',\n    'shelves_popular' => 'Popular Shelves',\n    'shelves_new' => 'New Shelves',\n    'shelves_new_action' => 'New Shelf',\n    'shelves_popular_empty' => 'The most popular shelves will appear here.',\n    'shelves_new_empty' => 'The most recently created shelves will appear here.',\n    'shelves_save' => 'Save Shelf',\n    'shelves_books' => 'Books on this shelf',\n    'shelves_add_books' => 'Add books to this shelf',\n    'shelves_drag_books' => 'Drag books below to add them to this shelf',\n    'shelves_empty_contents' => 'This shelf has no books assigned to it',\n    'shelves_edit_and_assign' => 'Edit shelf to assign books',\n    'shelves_edit_named' => 'Edit Shelf :name',\n    'shelves_edit' => 'Edit Shelf',\n    'shelves_delete' => 'Delete Shelf',\n    'shelves_delete_named' => 'Delete Shelf :name',\n    'shelves_delete_explain' => \"This will delete the shelf with the name ':name'. Contained books will not be deleted.\",\n    'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',\n    'shelves_permissions' => 'Shelf Permissions',\n    'shelves_permissions_updated' => 'Shelf Permissions Updated',\n    'shelves_permissions_active' => 'Shelf Permissions Active',\n    'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',\n    'shelves_permissions_create' => 'Shelf create permissions are only used for copying permissions to child books using the action below. They do not control the ability to create books.',\n    'shelves_copy_permissions_to_books' => 'Copy Permissions to Books',\n    'shelves_copy_permissions' => 'Copy Permissions',\n    'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',\n    'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',\n\n    // Books\n    'book' => 'Book',\n    'books' => 'Books',\n    'x_books' => ':count Book|:count Books',\n    'books_empty' => 'No books have been created',\n    'books_popular' => 'Popular Books',\n    'books_recent' => 'Recent Books',\n    'books_new' => 'New Books',\n    'books_new_action' => 'New Book',\n    'books_popular_empty' => 'The most popular books will appear here.',\n    'books_new_empty' => 'The most recently created books will appear here.',\n    'books_create' => 'Create New Book',\n    'books_delete' => 'Delete Book',\n    'books_delete_named' => 'Delete Book :bookName',\n    'books_delete_explain' => 'This will delete the book with the name \\':bookName\\'. All pages and chapters will be removed.',\n    'books_delete_confirmation' => 'Are you sure you want to delete this book?',\n    'books_edit' => 'Edit Book',\n    'books_edit_named' => 'Edit Book :bookName',\n    'books_form_book_name' => 'Book Name',\n    'books_save' => 'Save Book',\n    'books_permissions' => 'Book Permissions',\n    'books_permissions_updated' => 'Book Permissions Updated',\n    'books_empty_contents' => 'No pages or chapters have been created for this book.',\n    'books_empty_create_page' => 'Create a new page',\n    'books_empty_sort_current_book' => 'Sort the current book',\n    'books_empty_add_chapter' => 'Add a chapter',\n    'books_permissions_active' => 'Book Permissions Active',\n    'books_search_this' => 'Search this book',\n    'books_navigation' => 'Book Navigation',\n    'books_sort' => 'Sort Book Contents',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Sort Book :bookName',\n    'books_sort_name' => 'Sort by Name',\n    'books_sort_created' => 'Sort by Created Date',\n    'books_sort_updated' => 'Sort by Updated Date',\n    'books_sort_chapters_first' => 'Chapters First',\n    'books_sort_chapters_last' => 'Chapters Last',\n    'books_sort_show_other' => 'Show Other Books',\n    'books_sort_save' => 'Save New Order',\n    'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.',\n    'books_sort_move_up' => 'Move Up',\n    'books_sort_move_down' => 'Move Down',\n    'books_sort_move_prev_book' => 'Move to Previous Book',\n    'books_sort_move_next_book' => 'Move to Next Book',\n    'books_sort_move_prev_chapter' => 'Move Into Previous Chapter',\n    'books_sort_move_next_chapter' => 'Move Into Next Chapter',\n    'books_sort_move_book_start' => 'Move to Start of Book',\n    'books_sort_move_book_end' => 'Move to End of Book',\n    'books_sort_move_before_chapter' => 'Move to Before Chapter',\n    'books_sort_move_after_chapter' => 'Move to After Chapter',\n    'books_copy' => 'Copy Book',\n    'books_copy_success' => 'Book successfully copied',\n\n    // Chapters\n    'chapter' => 'Chapter',\n    'chapters' => 'Chapters',\n    'x_chapters' => ':count Chapter|:count Chapters',\n    'chapters_popular' => 'Popular Chapters',\n    'chapters_new' => 'New Chapter',\n    'chapters_create' => 'Create New Chapter',\n    'chapters_delete' => 'Delete Chapter',\n    'chapters_delete_named' => 'Delete Chapter :chapterName',\n    'chapters_delete_explain' => 'This will delete the chapter with the name \\':chapterName\\'. All pages that exist within this chapter will also be deleted.',\n    'chapters_delete_confirm' => 'Are you sure you want to delete this chapter?',\n    'chapters_edit' => 'Edit Chapter',\n    'chapters_edit_named' => 'Edit Chapter :chapterName',\n    'chapters_save' => 'Save Chapter',\n    'chapters_move' => 'Move Chapter',\n    'chapters_move_named' => 'Move Chapter :chapterName',\n    'chapters_copy' => 'Copy Chapter',\n    'chapters_copy_success' => 'Chapter successfully copied',\n    'chapters_permissions' => 'Chapter Permissions',\n    'chapters_empty' => 'No pages are currently in this chapter.',\n    'chapters_permissions_active' => 'Chapter Permissions Active',\n    'chapters_permissions_success' => 'Chapter Permissions Updated',\n    'chapters_search_this' => 'Search this chapter',\n    'chapter_sort_book' => 'Sort Book',\n\n    // Pages\n    'page' => 'Page',\n    'pages' => 'Pages',\n    'x_pages' => ':count Page|:count Pages',\n    'pages_popular' => 'Popular Pages',\n    'pages_new' => 'New Page',\n    'pages_attachments' => 'Attachments',\n    'pages_navigation' => 'Page Navigation',\n    'pages_delete' => 'Delete Page',\n    'pages_delete_named' => 'Delete Page :pageName',\n    'pages_delete_draft_named' => 'Delete Draft Page :pageName',\n    'pages_delete_draft' => 'Delete Draft Page',\n    'pages_delete_success' => 'Page deleted',\n    'pages_delete_draft_success' => 'Draft page deleted',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'Are you sure you want to delete this page?',\n    'pages_delete_draft_confirm' => 'Are you sure you want to delete this draft page?',\n    'pages_editing_named' => 'Editing Page :pageName',\n    'pages_edit_draft_options' => 'Draft Options',\n    'pages_edit_save_draft' => 'Save Draft',\n    'pages_edit_draft' => 'Edit Page Draft',\n    'pages_editing_draft' => 'Editing Draft',\n    'pages_editing_page' => 'Editing Page',\n    'pages_edit_draft_save_at' => 'Draft saved at ',\n    'pages_edit_delete_draft' => 'Delete Draft',\n    'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.',\n    'pages_edit_discard_draft' => 'Discard Draft',\n    'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',\n    'pages_edit_switch_to_markdown_clean' => '(Clean Content)',\n    'pages_edit_switch_to_markdown_stable' => '(Stable Content)',\n    'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Set Changelog',\n    'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\\'ve made',\n    'pages_edit_enter_changelog' => 'Enter Changelog',\n    'pages_editor_switch_title' => 'Switch Editor',\n    'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',\n    'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',\n    'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',\n    'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',\n    'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\\'t persist across this change.',\n    'pages_save' => 'Save Page',\n    'pages_title' => 'Page Title',\n    'pages_name' => 'Page Name',\n    'pages_md_editor' => 'Editor',\n    'pages_md_preview' => 'Preview',\n    'pages_md_insert_image' => 'Insert Image',\n    'pages_md_insert_link' => 'Insert Entity Link',\n    'pages_md_insert_drawing' => 'Insert Drawing',\n    'pages_md_show_preview' => 'Show preview',\n    'pages_md_sync_scroll' => 'Sync preview scroll',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Unsaved Drawing Found',\n    'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',\n    'pages_not_in_chapter' => 'Page is not in a chapter',\n    'pages_move' => 'Move Page',\n    'pages_copy' => 'Copy Page',\n    'pages_copy_desination' => 'Copy Destination',\n    'pages_copy_success' => 'Page successfully copied',\n    'pages_permissions' => 'Page Permissions',\n    'pages_permissions_success' => 'Page permissions updated',\n    'pages_revision' => 'Revision',\n    'pages_revisions' => 'Page Revisions',\n    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',\n    'pages_revisions_named' => 'Page Revisions for :pageName',\n    'pages_revision_named' => 'Page Revision for :pageName',\n    'pages_revision_restored_from' => 'Restored from #:id; :summary',\n    'pages_revisions_created_by' => 'Created By',\n    'pages_revisions_date' => 'Revision Date',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Revision Number',\n    'pages_revisions_numbered' => 'Revision #:id',\n    'pages_revisions_numbered_changes' => 'Revision #:id Changes',\n    'pages_revisions_editor' => 'Editor Type',\n    'pages_revisions_changelog' => 'Changelog',\n    'pages_revisions_changes' => 'Changes',\n    'pages_revisions_current' => 'Current Version',\n    'pages_revisions_preview' => 'Preview',\n    'pages_revisions_restore' => 'Restore',\n    'pages_revisions_none' => 'This page has no revisions',\n    'pages_copy_link' => 'Copy Link',\n    'pages_edit_content_link' => 'Jump to section in editor',\n    'pages_pointer_enter_mode' => 'Enter section select mode',\n    'pages_pointer_label' => 'Page Section Options',\n    'pages_pointer_permalink' => 'Page Section Permalink',\n    'pages_pointer_include_tag' => 'Page Section Include Tag',\n    'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag',\n    'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink',\n    'pages_permissions_active' => 'Page Permissions Active',\n    'pages_initial_revision' => 'Initial publish',\n    'pages_references_update_revision' => 'System auto-update of internal links',\n    'pages_initial_name' => 'New Page',\n    'pages_editing_draft_notification' => 'You are currently editing a draft that was last saved :timeDiff.',\n    'pages_draft_edited_notification' => 'This page has been updated by since that time. It is recommended that you discard this draft.',\n    'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count users have started editing this page',\n        'start_b' => ':userName has started editing this page',\n        'time_a' => 'since the page was last updated',\n        'time_b' => 'in the last :minCount minutes',\n        'message' => ':start :time. Take care not to overwrite each other\\'s updates!',\n    ],\n    'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',\n    'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',\n    'pages_specific' => 'Specific Page',\n    'pages_is_template' => 'Page Template',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Toggle Sidebar',\n    'page_tags' => 'Page Tags',\n    'chapter_tags' => 'Chapter Tags',\n    'book_tags' => 'Book Tags',\n    'shelf_tags' => 'Shelf Tags',\n    'tag' => 'Tag',\n    'tags' =>  'Tags',\n    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',\n    'tag_name' =>  'Tag Name',\n    'tag_value' => 'Tag Value (Optional)',\n    'tags_explain' => \"Add some tags to better categorise your content. \\n You can assign a value to a tag for more in-depth organisation.\",\n    'tags_add' => 'Add another tag',\n    'tags_remove' => 'Remove this tag',\n    'tags_usages' => 'Total tag usages',\n    'tags_assigned_pages' => 'Assigned to Pages',\n    'tags_assigned_chapters' => 'Assigned to Chapters',\n    'tags_assigned_books' => 'Assigned to Books',\n    'tags_assigned_shelves' => 'Assigned to Shelves',\n    'tags_x_unique_values' => ':count unique values',\n    'tags_all_values' => 'All values',\n    'tags_view_tags' => 'View Tags',\n    'tags_view_existing_tags' => 'View existing tags',\n    'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',\n    'attachments' => 'Attachments',\n    'attachments_explain' => 'Upload some files or attach some links to display on your page. These are visible in the page sidebar.',\n    'attachments_explain_instant_save' => 'Changes here are saved instantly.',\n    'attachments_upload' => 'Upload File',\n    'attachments_link' => 'Attach Link',\n    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',\n    'attachments_set_link' => 'Set Link',\n    'attachments_delete' => 'Are you sure you want to delete this attachment?',\n    'attachments_dropzone' => 'Drop files here to upload',\n    'attachments_no_files' => 'No files have been uploaded',\n    'attachments_explain_link' => 'You can attach a link if you\\'d prefer not to upload a file. This can be a link to another page or a link to a file in the cloud.',\n    'attachments_link_name' => 'Link Name',\n    'attachment_link' => 'Attachment link',\n    'attachments_link_url' => 'Link to file',\n    'attachments_link_url_hint' => 'Url of site or file',\n    'attach' => 'Attach',\n    'attachments_insert_link' => 'Add Attachment Link to Page',\n    'attachments_edit_file' => 'Edit File',\n    'attachments_edit_file_name' => 'File Name',\n    'attachments_edit_drop_upload' => 'Drop files or click here to upload and overwrite',\n    'attachments_order_updated' => 'Attachment order updated',\n    'attachments_updated_success' => 'Attachment details updated',\n    'attachments_deleted' => 'Attachment deleted',\n    'attachments_file_uploaded' => 'File successfully uploaded',\n    'attachments_file_updated' => 'File successfully updated',\n    'attachments_link_attached' => 'Link successfully attached to page',\n    'templates' => 'Templates',\n    'templates_set_as_template' => 'Page is a template',\n    'templates_explain_set_as_template' => 'You can set this page as a template so its contents be utilized when creating other pages. Other users will be able to use this template if they have view permissions for this page.',\n    'templates_replace_content' => 'Replace page content',\n    'templates_append_content' => 'Append to page content',\n    'templates_prepend_content' => 'Prepend to page content',\n\n    // Profile View\n    'profile_user_for_x' => 'User for :time',\n    'profile_created_content' => 'Created Content',\n    'profile_not_created_pages' => ':userName has not created any pages',\n    'profile_not_created_chapters' => ':userName has not created any chapters',\n    'profile_not_created_books' => ':userName has not created any books',\n    'profile_not_created_shelves' => ':userName has not created any shelves',\n\n    // Comments\n    'comment' => 'Comment',\n    'comments' => 'Comments',\n    'comment_add' => 'Add Comment',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Leave a comment here',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Save Comment',\n    'comment_new' => 'New Comment',\n    'comment_created' => 'commented :createDiff',\n    'comment_updated' => 'Updated :updateDiff by :username',\n    'comment_updated_indicator' => 'Updated',\n    'comment_deleted_success' => 'Comment deleted',\n    'comment_created_success' => 'Comment added',\n    'comment_updated_success' => 'Comment updated',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Are you sure you want to delete this comment?',\n    'comment_in_reply_to' => 'In reply to :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',\n\n    // Revision\n    'revision_delete_confirm' => 'Are you sure you want to delete this revision?',\n    'revision_restore_confirm' => 'Are you sure you want to restore this revision? The current page contents will be replaced.',\n    'revision_cannot_delete_latest' => 'Cannot delete the latest revision.',\n\n    // Copy view\n    'copy_consider' => 'Please consider the below when copying content.',\n    'copy_consider_permissions' => 'Custom permission settings will not be copied.',\n    'copy_consider_owner' => 'You will become the owner of all copied content.',\n    'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',\n    'copy_consider_attachments' => 'Page attachments will not be copied.',\n    'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',\n\n    // Conversions\n    'convert_to_shelf' => 'Convert to Shelf',\n    'convert_to_shelf_contents_desc' => 'You can convert this book to a new shelf with the same contents. Chapters contained within this book will be converted to new books. If this book contains any pages, that are not in a chapter, this book will be renamed and contain such pages, and this book will become part of the new shelf.',\n    'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.',\n    'convert_book' => 'Convert Book',\n    'convert_book_confirm' => 'Are you sure you want to convert this book?',\n    'convert_undo_warning' => 'This cannot be as easily undone.',\n    'convert_to_book' => 'Convert to Book',\n    'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',\n    'convert_chapter' => 'Convert Chapter',\n    'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',\n\n    // References\n    'references' => 'References',\n    'references_none' => 'There are no tracked references to this item.',\n    'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',\n\n    // Watch Options\n    'watch' => 'Watch',\n    'watch_title_default' => 'Default Preferences',\n    'watch_desc_default' => 'Revert watching to just your default notification preferences.',\n    'watch_title_ignore' => 'Ignore',\n    'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.',\n    'watch_title_new' => 'New Pages',\n    'watch_desc_new' => 'Notify when any new page is created within this item.',\n    'watch_title_updates' => 'All Page Updates',\n    'watch_desc_updates' => 'Notify upon all new pages and page changes.',\n    'watch_desc_updates_page' => 'Notify upon all page changes.',\n    'watch_title_comments' => 'All Page Updates & Comments',\n    'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.',\n    'watch_desc_comments_page' => 'Notify upon page changes and new comments.',\n    'watch_change_default' => 'Change default notification preferences',\n    'watch_detail_ignore' => 'Ignoring notifications',\n    'watch_detail_new' => 'Watching for new pages',\n    'watch_detail_updates' => 'Watching new pages and updates',\n    'watch_detail_comments' => 'Watching new pages, updates & comments',\n    'watch_detail_parent_book' => 'Watching via parent book',\n    'watch_detail_parent_book_ignore' => 'Ignoring via parent book',\n    'watch_detail_parent_chapter' => 'Watching via parent chapter',\n    'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter',\n];\n"
  },
  {
    "path": "lang/ka/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'You do not have permission to access the requested page.',\n    'permissionJson' => 'You do not have permission to perform the requested action.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'A user with the email :email already exists but with different credentials.',\n    'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',\n    'email_already_confirmed' => 'Email has already been confirmed, Try logging in.',\n    'email_confirmation_invalid' => 'This confirmation token is not valid or has already been used, Please try registering again.',\n    'email_confirmation_expired' => 'The confirmation token has expired, A new confirmation email has been sent.',\n    'email_confirmation_awaiting' => 'The email address for the account in use needs to be confirmed',\n    'ldap_fail_anonymous' => 'LDAP access failed using anonymous bind',\n    'ldap_fail_authed' => 'LDAP access failed using given dn & password details',\n    'ldap_extension_not_installed' => 'LDAP PHP extension not installed',\n    'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed',\n    'saml_already_logged_in' => 'Already logged in',\n    'saml_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',\n    'saml_invalid_response_id' => 'The request from the external authentication system is not recognised by a process started by this application. Navigating back after a login could cause this issue.',\n    'saml_fail_authed' => 'Login using :system failed, system did not provide successful authorization',\n    'oidc_already_logged_in' => 'Already logged in',\n    'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',\n    'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',\n    'social_no_action_defined' => 'No action defined',\n    'social_login_bad_response' => \"Error received during :socialAccount login: \\n:error\",\n    'social_account_in_use' => 'This :socialAccount account is already in use, Try logging in via the :socialAccount option.',\n    'social_account_email_in_use' => 'The email :email is already in use. If you already have an account you can connect your :socialAccount account from your profile settings.',\n    'social_account_existing' => 'This :socialAccount is already attached to your profile.',\n    'social_account_already_used_existing' => 'This :socialAccount account is already used by another user.',\n    'social_account_not_used' => 'This :socialAccount account is not linked to any users. Please attach it in your profile settings. ',\n    'social_account_register_instructions' => 'If you do not yet have an account, You can register an account using the :socialAccount option.',\n    'social_driver_not_found' => 'Social driver not found',\n    'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.',\n    'invite_token_expired' => 'This invitation link has expired. You can instead try to reset your account password.',\n    'login_user_not_found' => 'A user for this action could not be found.',\n\n    // System\n    'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.',\n    'cannot_get_image_from_url' => 'Cannot get image from :url',\n    'cannot_create_thumbs' => 'The server cannot create thumbnails. Please check you have the GD PHP extension installed.',\n    'server_upload_limit' => 'The server does not allow uploads of this size. Please try a smaller file size.',\n    'server_post_limit' => 'The server cannot receive the provided amount of data. Try again with less data or a smaller file.',\n    'uploaded'  => 'The server does not allow uploads of this size. Please try a smaller file size.',\n\n    // Drawing & Images\n    'image_upload_error' => 'An error occurred uploading the image',\n    'image_upload_type_error' => 'The image type being uploaded is invalid',\n    'image_upload_replace_type' => 'Image file replacements must be of the same type',\n    'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.',\n    'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.',\n    'image_gallery_thumbnail_memory_limit' => 'Failed to create gallery thumbnails due to system resource limits.',\n    'drawing_data_not_found' => 'Drawing data could not be loaded. The drawing file might no longer exist or you may not have permission to access it.',\n\n    // Attachments\n    'attachment_not_found' => 'Attachment not found',\n    'attachment_upload_error' => 'An error occurred uploading the attachment file',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page',\n    'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content',\n    'page_custom_home_deletion' => 'Cannot delete a page while it is set as a homepage',\n\n    // Entities\n    'entity_not_found' => 'Entity not found',\n    'bookshelf_not_found' => 'Shelf not found',\n    'book_not_found' => 'Book not found',\n    'page_not_found' => 'Page not found',\n    'chapter_not_found' => 'Chapter not found',\n    'selected_book_not_found' => 'The selected book was not found',\n    'selected_book_chapter_not_found' => 'The selected Book or Chapter was not found',\n    'guests_cannot_save_drafts' => 'Guests cannot save drafts',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'You cannot delete the only admin',\n    'users_cannot_delete_guest' => 'You cannot delete the guest user',\n    'users_could_not_send_invite' => 'Could not create user since invite email failed to send',\n\n    // Roles\n    'role_cannot_be_edited' => 'This role cannot be edited',\n    'role_system_cannot_be_deleted' => 'This role is a system role and cannot be deleted',\n    'role_registration_default_cannot_delete' => 'This role cannot be deleted while set as the default registration role',\n    'role_cannot_remove_only_admin' => 'This user is the only user assigned to the administrator role. Assign the administrator role to another user before attempting to remove it here.',\n\n    // Comments\n    'comment_list' => 'An error occurred while fetching the comments.',\n    'cannot_add_comment_to_draft' => 'You cannot add comments to a draft.',\n    'comment_add' => 'An error occurred while adding / updating the comment.',\n    'comment_delete' => 'An error occurred while deleting the comment.',\n    'empty_comment' => 'Cannot add an empty comment.',\n\n    // Error pages\n    '404_page_not_found' => 'Page Not Found',\n    'sorry_page_not_found' => 'Sorry, The page you were looking for could not be found.',\n    'sorry_page_not_found_permission_warning' => 'If you expected this page to exist, you might not have permission to view it.',\n    'image_not_found' => 'Image Not Found',\n    'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',\n    'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',\n    'return_home' => 'Return to home',\n    'error_occurred' => 'An Error Occurred',\n    'app_down' => ':appName is down right now',\n    'back_soon' => 'It will be back up soon.',\n\n    // Import\n    'import_zip_cant_read' => 'Could not read ZIP file.',\n    'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',\n    'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Import ZIP failed to validate with errors:',\n    'import_zip_failed_notification' => 'Failed to import ZIP file.',\n    'import_perms_books' => 'You are lacking the required permissions to create books.',\n    'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',\n    'import_perms_pages' => 'You are lacking the required permissions to create pages.',\n    'import_perms_images' => 'You are lacking the required permissions to create images.',\n    'import_perms_attachments' => 'You are lacking the required permission to create attachments.',\n\n    // API errors\n    'api_no_authorization_found' => 'No authorization token found on the request',\n    'api_bad_authorization_format' => 'An authorization token was found on the request but the format appeared incorrect',\n    'api_user_token_not_found' => 'No matching API token was found for the provided authorization token',\n    'api_incorrect_token_secret' => 'The secret provided for the given used API token is incorrect',\n    'api_user_no_api_permission' => 'The owner of the used API token does not have permission to make API calls',\n    'api_user_token_expired' => 'The authorization token used has expired',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Error thrown when sending a test email:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'The URL does not match the configured allowed SSR hosts',\n];\n"
  },
  {
    "path": "lang/ka/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'New comment on page: :pageName',\n    'new_comment_intro' => 'A user has commented on a page in :appName:',\n    'new_page_subject' => 'New page: :pageName',\n    'new_page_intro' => 'A new page has been created in :appName:',\n    'updated_page_subject' => 'Updated page: :pageName',\n    'updated_page_intro' => 'A page has been updated in :appName:',\n    'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\\'t be sent notifications for further edits to this page by the same editor.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Page Name:',\n    'detail_page_path' => 'Page Path:',\n    'detail_commenter' => 'Commenter:',\n    'detail_comment' => 'Comment:',\n    'detail_created_by' => 'Created By:',\n    'detail_updated_by' => 'Updated By:',\n\n    'action_view_comment' => 'View Comment',\n    'action_view_page' => 'View Page',\n\n    'footer_reason' => 'This notification was sent to you because :link cover this type of activity for this item.',\n    'footer_reason_link' => 'your notification preferences',\n];\n"
  },
  {
    "path": "lang/ka/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Previous',\n    'next'     => 'Next &raquo;',\n\n];\n"
  },
  {
    "path": "lang/ka/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Passwords must be at least eight characters and match the confirmation.',\n    'user' => \"We can't find a user with that e-mail address.\",\n    'token' => 'The password reset token is invalid for this email address.',\n    'sent' => 'We have e-mailed your password reset link!',\n    'reset' => 'Your password has been reset!',\n\n];\n"
  },
  {
    "path": "lang/ka/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'My Account',\n\n    'shortcuts' => 'Shortcuts',\n    'shortcuts_interface' => 'UI Shortcut Preferences',\n    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',\n    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',\n    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',\n    'shortcuts_section_navigation' => 'Navigation',\n    'shortcuts_section_actions' => 'Common Actions',\n    'shortcuts_save' => 'Save Shortcuts',\n    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing \"?\" which will highlight the available shortcuts for actions currently visible on the screen.',\n    'shortcuts_update_success' => 'Shortcut preferences have been updated!',\n    'shortcuts_overview_desc' => 'Manage keyboard shortcuts you can use to navigate the system user interface.',\n\n    'notifications' => 'Notification Preferences',\n    'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.',\n    'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own',\n    'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Notify upon replies to my comments',\n    'notifications_save' => 'Save Preferences',\n    'notifications_update_success' => 'Notification preferences have been updated!',\n    'notifications_watched' => 'Watched & Ignored Items',\n    'notifications_watched_desc' => 'Below are the items that have custom watch preferences applied. To update your preferences for these, view the item then find the watch options in the sidebar.',\n\n    'auth' => 'Access & Security',\n    'auth_change_password' => 'Change Password',\n    'auth_change_password_desc' => 'Change the password you use to log-in to the application. This must be at least 8 characters long.',\n    'auth_change_password_success' => 'Password has been updated!',\n\n    'profile' => 'Profile Details',\n    'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.',\n    'profile_view_public' => 'View Public Profile',\n    'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.',\n    'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.',\n    'profile_email_no_permission' => 'Unfortunately you don\\'t have permission to change your email address. If you want to change this, you\\'d need to ask an administrator to change this for you.',\n    'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.',\n    'profile_admin_options' => 'Administrator Options',\n    'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the \"Settings > Users\" area of the application.',\n\n    'delete_account' => 'Delete Account',\n    'delete_my_account' => 'Delete My Account',\n    'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\\'ve created, such as created pages and uploaded images, will remain.',\n    'delete_my_account_warning' => 'Are you sure you want to delete your account?',\n];\n"
  },
  {
    "path": "lang/ka/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Settings',\n    'settings_save' => 'Save Settings',\n    'system_version' => 'System Version',\n    'categories' => 'Categories',\n\n    // App Settings\n    'app_customization' => 'Customization',\n    'app_features_security' => 'Features & Security',\n    'app_name' => 'Application Name',\n    'app_name_desc' => 'This name is shown in the header and in any system-sent emails.',\n    'app_name_header' => 'Show name in header',\n    'app_public_access' => 'Public Access',\n    'app_public_access_desc' => 'Enabling this option will allow visitors, that are not logged-in, to access content in your BookStack instance.',\n    'app_public_access_desc_guest' => 'Access for public visitors can be controlled through the \"Guest\" user.',\n    'app_public_access_toggle' => 'Allow public access',\n    'app_public_viewing' => 'Allow public viewing?',\n    'app_secure_images' => 'Higher Security Image Uploads',\n    'app_secure_images_toggle' => 'Enable higher security image uploads',\n    'app_secure_images_desc' => 'For performance reasons, all images are public. This option adds a random, hard-to-guess string in front of image urls. Ensure directory indexes are not enabled to prevent easy access.',\n    'app_default_editor' => 'Default Page Editor',\n    'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.',\n    'app_custom_html' => 'Custom HTML Head Content',\n    'app_custom_html_desc' => 'Any content added here will be inserted into the bottom of the <head> section of every page. This is handy for overriding styles or adding analytics code.',\n    'app_custom_html_disabled_notice' => 'Custom HTML head content is disabled on this settings page to ensure any breaking changes can be reverted.',\n    'app_logo' => 'Application Logo',\n    'app_logo_desc' => 'This is used in the application header bar, among other areas. This image should be 86px in height. Large images will be scaled down.',\n    'app_icon' => 'Application Icon',\n    'app_icon_desc' => 'This icon is used for browser tabs and shortcut icons. This should be a 256px square PNG image.',\n    'app_homepage' => 'Application Homepage',\n    'app_homepage_desc' => 'Select a view to show on the homepage instead of the default view. Page permissions are ignored for selected pages.',\n    'app_homepage_select' => 'Select a page',\n    'app_footer_links' => 'Footer Links',\n    'app_footer_links_desc' => 'Add links to show within the site footer. These will be displayed at the bottom of most pages, including those that do not require login. You can use a label of \"trans::<key>\" to use system-defined translations. For example: Using \"trans::common.privacy_policy\" will provide the translated text \"Privacy Policy\" and \"trans::common.terms_of_service\" will provide the translated text \"Terms of Service\".',\n    'app_footer_links_label' => 'Link Label',\n    'app_footer_links_url' => 'Link URL',\n    'app_footer_links_add' => 'Add Footer Link',\n    'app_disable_comments' => 'Disable Comments',\n    'app_disable_comments_toggle' => 'Disable comments',\n    'app_disable_comments_desc' => 'Disables comments across all pages in the application. <br> Existing comments are not shown.',\n\n    // Color settings\n    'color_scheme' => 'Application Color Scheme',\n    'color_scheme_desc' => 'Set the colors to use in the application user interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.',\n    'ui_colors_desc' => 'Set the application primary color and default link color. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the application interface.',\n    'app_color' => 'Primary Color',\n    'link_color' => 'Default Link Color',\n    'content_colors_desc' => 'Set colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.',\n    'bookshelf_color' => 'Shelf Color',\n    'book_color' => 'Book Color',\n    'chapter_color' => 'Chapter Color',\n    'page_color' => 'Page Color',\n    'page_draft_color' => 'Page Draft Color',\n\n    // Registration Settings\n    'reg_settings' => 'Registration',\n    'reg_enable' => 'Enable Registration',\n    'reg_enable_toggle' => 'Enable registration',\n    'reg_enable_desc' => 'When registration is enabled user will be able to sign themselves up as an application user. Upon registration they are given a single, default user role.',\n    'reg_default_role' => 'Default user role after registration',\n    'reg_enable_external_warning' => 'The option above is ignored while external LDAP or SAML authentication is active. User accounts for non-existing members will be auto-created if authentication, against the external system in use, is successful.',\n    'reg_email_confirmation' => 'Email Confirmation',\n    'reg_email_confirmation_toggle' => 'Require email confirmation',\n    'reg_confirm_email_desc' => 'If domain restriction is used then email confirmation will be required and this option will be ignored.',\n    'reg_confirm_restrict_domain' => 'Domain Restriction',\n    'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.',\n    'reg_confirm_restrict_domain_placeholder' => 'No restriction set',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Maintenance',\n    'maint_image_cleanup' => 'Cleanup Images',\n    'maint_image_cleanup_desc' => 'Scans page & revision content to check which images and drawings are currently in use and which images are redundant. Ensure you create a full database and image backup before running this.',\n    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',\n    'maint_image_cleanup_run' => 'Run Cleanup',\n    'maint_image_cleanup_warning' => ':count potentially unused images were found. Are you sure you want to delete these images?',\n    'maint_image_cleanup_success' => ':count potentially unused images found and deleted!',\n    'maint_image_cleanup_nothing_found' => 'No unused images found, Nothing deleted!',\n    'maint_send_test_email' => 'Send a Test Email',\n    'maint_send_test_email_desc' => 'This sends a test email to your email address specified in your profile.',\n    'maint_send_test_email_run' => 'Send test email',\n    'maint_send_test_email_success' => 'Email sent to :address',\n    'maint_send_test_email_mail_subject' => 'Test Email',\n    'maint_send_test_email_mail_greeting' => 'Email delivery seems to work!',\n    'maint_send_test_email_mail_text' => 'Congratulations! As you received this email notification, your email settings seem to be configured properly.',\n    'maint_recycle_bin_desc' => 'Deleted shelves, books, chapters & pages are sent to the recycle bin so they can be restored or permanently deleted. Older items in the recycle bin may be automatically removed after a while depending on system configuration.',\n    'maint_recycle_bin_open' => 'Open Recycle Bin',\n    'maint_regen_references' => 'Regenerate References',\n    'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',\n    'maint_regen_references_success' => 'Reference index has been regenerated!',\n    'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Recycle Bin',\n    'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',\n    'recycle_bin_deleted_item' => 'Deleted Item',\n    'recycle_bin_deleted_parent' => 'Parent',\n    'recycle_bin_deleted_by' => 'Deleted By',\n    'recycle_bin_deleted_at' => 'Deletion Time',\n    'recycle_bin_permanently_delete' => 'Permanently Delete',\n    'recycle_bin_restore' => 'Restore',\n    'recycle_bin_contents_empty' => 'The recycle bin is currently empty',\n    'recycle_bin_empty' => 'Empty Recycle Bin',\n    'recycle_bin_empty_confirm' => 'This will permanently destroy all items in the recycle bin including content contained within each item. Are you sure you want to empty the recycle bin?',\n    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?',\n    'recycle_bin_destroy_list' => 'Items to be Destroyed',\n    'recycle_bin_restore_list' => 'Items to be Restored',\n    'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.',\n    'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.',\n    'recycle_bin_restore_parent' => 'Restore Parent',\n    'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',\n    'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',\n\n    // Audit Log\n    'audit' => 'Audit Log',\n    'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',\n    'audit_event_filter' => 'Event Filter',\n    'audit_event_filter_no_filter' => 'No Filter',\n    'audit_deleted_item' => 'Deleted Item',\n    'audit_deleted_item_name' => 'Name: :name',\n    'audit_table_user' => 'User',\n    'audit_table_event' => 'Event',\n    'audit_table_related' => 'Related Item or Detail',\n    'audit_table_ip' => 'IP Address',\n    'audit_table_date' => 'Activity Date',\n    'audit_date_from' => 'Date Range From',\n    'audit_date_to' => 'Date Range To',\n\n    // Role Settings\n    'roles' => 'Roles',\n    'role_user_roles' => 'User Roles',\n    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',\n    'roles_x_users_assigned' => ':count user assigned|:count users assigned',\n    'roles_x_permissions_provided' => ':count permission|:count permissions',\n    'roles_assigned_users' => 'Assigned Users',\n    'roles_permissions_provided' => 'Provided Permissions',\n    'role_create' => 'Create New Role',\n    'role_delete' => 'Delete Role',\n    'role_delete_confirm' => 'This will delete the role with the name \\':roleName\\'.',\n    'role_delete_users_assigned' => 'This role has :userCount users assigned to it. If you would like to migrate the users from this role select a new role below.',\n    'role_delete_no_migration' => \"Don't migrate users\",\n    'role_delete_sure' => 'Are you sure you want to delete this role?',\n    'role_edit' => 'Edit Role',\n    'role_details' => 'Role Details',\n    'role_name' => 'Role Name',\n    'role_desc' => 'Short Description of Role',\n    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',\n    'role_external_auth_id' => 'External Authentication IDs',\n    'role_system' => 'System Permissions',\n    'role_manage_users' => 'Manage users',\n    'role_manage_roles' => 'Manage roles & role permissions',\n    'role_manage_entity_permissions' => 'Manage all book, chapter & page permissions',\n    'role_manage_own_entity_permissions' => 'Manage permissions on own book, chapter & pages',\n    'role_manage_page_templates' => 'Manage page templates',\n    'role_access_api' => 'Access system API',\n    'role_manage_settings' => 'Manage app settings',\n    'role_export_content' => 'Export content',\n    'role_import_content' => 'Import content',\n    'role_editor_change' => 'Change page editor',\n    'role_notifications' => 'Receive & manage notifications',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Asset Permissions',\n    'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',\n    'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',\n    'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.',\n    'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'All',\n    'role_own' => 'Own',\n    'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',\n    'role_save' => 'Save Role',\n    'role_users' => 'Users in this role',\n    'role_users_none' => 'No users are currently assigned to this role',\n\n    // Users\n    'users' => 'Users',\n    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',\n    'user_profile' => 'User Profile',\n    'users_add_new' => 'Add New User',\n    'users_search' => 'Search Users',\n    'users_latest_activity' => 'Latest Activity',\n    'users_details' => 'User Details',\n    'users_details_desc' => 'Set a display name and an email address for this user. The email address will be used for logging into the application.',\n    'users_details_desc_no_email' => 'Set a display name for this user so others can recognise them.',\n    'users_role' => 'User Roles',\n    'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.',\n    'users_password' => 'User Password',\n    'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',\n    'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.',\n    'users_send_invite_option' => 'Send user invite email',\n    'users_external_auth_id' => 'External Authentication ID',\n    'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.',\n    'users_password_warning' => 'Only fill the below if you would like to change the password for this user.',\n    'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.',\n    'users_delete' => 'Delete User',\n    'users_delete_named' => 'Delete user :userName',\n    'users_delete_warning' => 'This will fully delete this user with the name \\':userName\\' from the system.',\n    'users_delete_confirm' => 'Are you sure you want to delete this user?',\n    'users_migrate_ownership' => 'Migrate Ownership',\n    'users_migrate_ownership_desc' => 'Select a user here if you want another user to become the owner of all items currently owned by this user.',\n    'users_none_selected' => 'No user selected',\n    'users_edit' => 'Edit User',\n    'users_edit_profile' => 'Edit Profile',\n    'users_avatar' => 'User Avatar',\n    'users_avatar_desc' => 'Select an image to represent this user. This should be approx 256px square.',\n    'users_preferred_language' => 'Preferred Language',\n    'users_preferred_language_desc' => 'This option will change the language used for the user-interface of the application. This will not affect any user-created content.',\n    'users_social_accounts' => 'Social Accounts',\n    'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.',\n    'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not revoke previously authorized access. Revoke access from your profile settings on the connected social account.',\n    'users_social_connect' => 'Connect Account',\n    'users_social_disconnect' => 'Disconnect Account',\n    'users_social_status_connected' => 'Connected',\n    'users_social_status_disconnected' => 'Disconnected',\n    'users_social_connected' => ':socialAccount account was successfully attached to your profile.',\n    'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.',\n    'users_api_tokens' => 'API Tokens',\n    'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.',\n    'users_api_tokens_none' => 'No API tokens have been created for this user',\n    'users_api_tokens_create' => 'Create Token',\n    'users_api_tokens_expires' => 'Expires',\n    'users_api_tokens_docs' => 'API Documentation',\n    'users_mfa' => 'Multi-Factor Authentication',\n    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'users_mfa_x_methods' => ':count method configured|:count methods configured',\n    'users_mfa_configure' => 'Configure Methods',\n\n    // API Tokens\n    'user_api_token_create' => 'Create API Token',\n    'user_api_token_name' => 'Name',\n    'user_api_token_name_desc' => 'Give your token a readable name as a future reminder of its intended purpose.',\n    'user_api_token_expiry' => 'Expiry Date',\n    'user_api_token_expiry_desc' => 'Set a date at which this token expires. After this date, requests made using this token will no longer work. Leaving this field blank will set an expiry 100 years into the future.',\n    'user_api_token_create_secret_message' => 'Immediately after creating this token a \"Token ID\" & \"Token Secret\" will be generated and displayed. The secret will only be shown a single time so be sure to copy the value to somewhere safe and secure before proceeding.',\n    'user_api_token' => 'API Token',\n    'user_api_token_id' => 'Token ID',\n    'user_api_token_id_desc' => 'This is a non-editable system generated identifier for this token which will need to be provided in API requests.',\n    'user_api_token_secret' => 'Token Secret',\n    'user_api_token_secret_desc' => 'This is a system generated secret for this token which will need to be provided in API requests. This will only be displayed this one time so copy this value to somewhere safe and secure.',\n    'user_api_token_created' => 'Token created :timeAgo',\n    'user_api_token_updated' => 'Token updated :timeAgo',\n    'user_api_token_delete' => 'Delete Token',\n    'user_api_token_delete_warning' => 'This will fully delete this API token with the name \\':tokenName\\' from the system.',\n    'user_api_token_delete_confirm' => 'Are you sure you want to delete this API token?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',\n    'webhooks_x_trigger_events' => ':count trigger event|:count trigger events',\n    'webhooks_create' => 'Create New Webhook',\n    'webhooks_none_created' => 'No webhooks have yet been created.',\n    'webhooks_edit' => 'Edit Webhook',\n    'webhooks_save' => 'Save Webhook',\n    'webhooks_details' => 'Webhook Details',\n    'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',\n    'webhooks_events' => 'Webhook Events',\n    'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',\n    'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\\'t expose confidential content.',\n    'webhooks_events_all' => 'All system events',\n    'webhooks_name' => 'Webhook Name',\n    'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',\n    'webhooks_endpoint' => 'Webhook Endpoint',\n    'webhooks_active' => 'Webhook Active',\n    'webhook_events_table_header' => 'Events',\n    'webhooks_delete' => 'Delete Webhook',\n    'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \\':webhookName\\', from the system.',\n    'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?',\n    'webhooks_format_example' => 'Webhook Format Example',\n    'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The \"related_item\" and \"url\" properties are optional and will depend on the type of event triggered.',\n    'webhooks_status' => 'Webhook Status',\n    'webhooks_last_called' => 'Last Called:',\n    'webhooks_last_errored' => 'Last Errored:',\n    'webhooks_last_error_message' => 'Last Error Message:',\n\n    // Licensing\n    'licenses' => 'Licenses',\n    'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.',\n    'licenses_bookstack' => 'BookStack License',\n    'licenses_php' => 'PHP Library Licenses',\n    'licenses_js' => 'JavaScript Library Licenses',\n    'licenses_other' => 'Other Licenses',\n    'license_details' => 'License Details',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/ka/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => 'The :attribute must be accepted.',\n    'active_url'           => 'The :attribute is not a valid URL.',\n    'after'                => 'The :attribute must be a date after :date.',\n    'alpha'                => 'The :attribute may only contain letters.',\n    'alpha_dash'           => 'The :attribute may only contain letters, numbers, dashes and underscores.',\n    'alpha_num'            => 'The :attribute may only contain letters and numbers.',\n    'array'                => 'The :attribute must be an array.',\n    'backup_codes'         => 'The provided code is not valid or has already been used.',\n    'before'               => 'The :attribute must be a date before :date.',\n    'between'              => [\n        'numeric' => 'The :attribute must be between :min and :max.',\n        'file'    => 'The :attribute must be between :min and :max kilobytes.',\n        'string'  => 'The :attribute must be between :min and :max characters.',\n        'array'   => 'The :attribute must have between :min and :max items.',\n    ],\n    'boolean'              => 'The :attribute field must be true or false.',\n    'confirmed'            => 'The :attribute confirmation does not match.',\n    'date'                 => 'The :attribute is not a valid date.',\n    'date_format'          => 'The :attribute does not match the format :format.',\n    'different'            => 'The :attribute and :other must be different.',\n    'digits'               => 'The :attribute must be :digits digits.',\n    'digits_between'       => 'The :attribute must be between :min and :max digits.',\n    'email'                => 'The :attribute must be a valid email address.',\n    'ends_with' => 'The :attribute must end with one of the following: :values',\n    'file'                 => 'The :attribute must be provided as a valid file.',\n    'filled'               => 'The :attribute field is required.',\n    'gt'                   => [\n        'numeric' => 'The :attribute must be greater than :value.',\n        'file'    => 'The :attribute must be greater than :value kilobytes.',\n        'string'  => 'The :attribute must be greater than :value characters.',\n        'array'   => 'The :attribute must have more than :value items.',\n    ],\n    'gte'                  => [\n        'numeric' => 'The :attribute must be greater than or equal :value.',\n        'file'    => 'The :attribute must be greater than or equal :value kilobytes.',\n        'string'  => 'The :attribute must be greater than or equal :value characters.',\n        'array'   => 'The :attribute must have :value items or more.',\n    ],\n    'exists'               => 'The selected :attribute is invalid.',\n    'image'                => 'The :attribute must be an image.',\n    'image_extension'      => 'The :attribute must have a valid & supported image extension.',\n    'in'                   => 'The selected :attribute is invalid.',\n    'integer'              => 'The :attribute must be an integer.',\n    'ip'                   => 'The :attribute must be a valid IP address.',\n    'ipv4'                 => 'The :attribute must be a valid IPv4 address.',\n    'ipv6'                 => 'The :attribute must be a valid IPv6 address.',\n    'json'                 => 'The :attribute must be a valid JSON string.',\n    'lt'                   => [\n        'numeric' => 'The :attribute must be less than :value.',\n        'file'    => 'The :attribute must be less than :value kilobytes.',\n        'string'  => 'The :attribute must be less than :value characters.',\n        'array'   => 'The :attribute must have less than :value items.',\n    ],\n    'lte'                  => [\n        'numeric' => 'The :attribute must be less than or equal :value.',\n        'file'    => 'The :attribute must be less than or equal :value kilobytes.',\n        'string'  => 'The :attribute must be less than or equal :value characters.',\n        'array'   => 'The :attribute must not have more than :value items.',\n    ],\n    'max'                  => [\n        'numeric' => 'The :attribute may not be greater than :max.',\n        'file'    => 'The :attribute may not be greater than :max kilobytes.',\n        'string'  => 'The :attribute may not be greater than :max characters.',\n        'array'   => 'The :attribute may not have more than :max items.',\n    ],\n    'mimes'                => 'The :attribute must be a file of type: :values.',\n    'min'                  => [\n        'numeric' => 'The :attribute must be at least :min.',\n        'file'    => 'The :attribute must be at least :min kilobytes.',\n        'string'  => 'The :attribute must be at least :min characters.',\n        'array'   => 'The :attribute must have at least :min items.',\n    ],\n    'not_in'               => 'The selected :attribute is invalid.',\n    'not_regex'            => 'The :attribute format is invalid.',\n    'numeric'              => 'The :attribute must be a number.',\n    'regex'                => 'The :attribute format is invalid.',\n    'required'             => 'The :attribute field is required.',\n    'required_if'          => 'The :attribute field is required when :other is :value.',\n    'required_with'        => 'The :attribute field is required when :values is present.',\n    'required_with_all'    => 'The :attribute field is required when :values is present.',\n    'required_without'     => 'The :attribute field is required when :values is not present.',\n    'required_without_all' => 'The :attribute field is required when none of :values are present.',\n    'same'                 => 'The :attribute and :other must match.',\n    'safe_url'             => 'The provided link may not be safe.',\n    'size'                 => [\n        'numeric' => 'The :attribute must be :size.',\n        'file'    => 'The :attribute must be :size kilobytes.',\n        'string'  => 'The :attribute must be :size characters.',\n        'array'   => 'The :attribute must contain :size items.',\n    ],\n    'string'               => 'The :attribute must be a string.',\n    'timezone'             => 'The :attribute must be a valid zone.',\n    'totp'                 => 'The provided code is not valid or has expired.',\n    'unique'               => 'The :attribute has already been taken.',\n    'url'                  => 'The :attribute format is invalid.',\n    'uploaded'             => 'The file could not be uploaded. The server may not accept files of this size.',\n\n    'zip_file' => 'The :attribute needs to reference a file within the ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',\n    'zip_model_expected' => 'Data object expected but \":type\" found.',\n    'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Password confirmation required',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/ko/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => '페이지 생성',\n    'page_create_notification'    => '페이지가 성공적으로 생성되었습니다',\n    'page_update'                 => '페이지 업데이트',\n    'page_update_notification'    => '페이지가 성공적으로 업데이트 되었습니다',\n    'page_delete'                 => '페이지 삭제',\n    'page_delete_notification'    => '페이지가 성공적으로 삭제되었습니다',\n    'page_restore'                => '페이지 복원',\n    'page_restore_notification'   => '페이지가 성공적으로 복원되었습니다',\n    'page_move'                   => '페이지 이동',\n    'page_move_notification'      => '페이지가 성공적으로 이동되었습니다',\n\n    // Chapters\n    'chapter_create'              => '챕터 생성',\n    'chapter_create_notification' => '챕터가 성공적으로 생성되었습니다.',\n    'chapter_update'              => '챕터 수정',\n    'chapter_update_notification' => '챕터가 성공적으로 수정되었습니다.',\n    'chapter_delete'              => '챕터 삭제',\n    'chapter_delete_notification' => '챕터가 성공적으로 삭제되었습니다.',\n    'chapter_move'                => '챕터 이동',\n    'chapter_move_notification' => '페이지가 성공적으로 이동되었습니다.',\n\n    // Books\n    'book_create'                 => '책 생성',\n    'book_create_notification'    => '책이 성공적으로 생성되었습니다.',\n    'book_create_from_chapter'              => '챕터를 책으로 변환',\n    'book_create_from_chapter_notification' => '챕터가 책으로 성공적으로 변환되었습니다.',\n    'book_update'                 => '업데이트된 책',\n    'book_update_notification'    => '책 수정함',\n    'book_delete'                 => '삭제된 책',\n    'book_delete_notification'    => '책이 성공적으로 삭제되었습니다.',\n    'book_sort'                   => '책 정렬',\n    'book_sort_notification'      => '책이 성공적으로 재정렬되었습니다.',\n\n    // Bookshelves\n    'bookshelf_create'            => '책장 만들기',\n    'bookshelf_create_notification'    => '책장을 성공적으로 생성했습니다.',\n    'bookshelf_create_from_book'    => '책을 책장으로 변환함',\n    'bookshelf_create_from_book_notification'    => '책을 성공적으로 책장으로 변환하였습니다.',\n    'bookshelf_update'                 => '책장 업데이트',\n    'bookshelf_update_notification'    => '책장이 성공적으로 업데이트 되었습니다.',\n    'bookshelf_delete'                 => '책장 삭제',\n    'bookshelf_delete_notification'    => '책장이 성공적으로 삭제되었습니다.',\n\n    // Revisions\n    'revision_restore' => '복원한 수정본',\n    'revision_delete' => '삭제한 수정본',\n    'revision_delete_notification' => '수정본을 잘 삭제함',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" 을 북마크에 추가하였습니다.',\n    'favourite_remove_notification' => '\":name\" 가 북마크에서 삭제되었습니다.',\n\n    // Watching\n    'watch_update_level_notification' => '주시 환경설정이 성공적으로 업데이트되었습니다.',\n\n    // Auth\n    'auth_login' => '로그인 완료',\n    'auth_register' => '신규 사용자 등록',\n    'auth_password_reset_request' => '사용자 비밀번호 초기화 요청',\n    'auth_password_reset_update' => '사용자 비밀번호 초기화',\n    'mfa_setup_method' => '다중인증(MFA)이 구성되었습니다.',\n    'mfa_setup_method_notification' => '다중 인증 설정함',\n    'mfa_remove_method' => 'MFA 메서드 제거',\n    'mfa_remove_method_notification' => '다중인증(MFA)이 성공적으로 제거되었습니다.',\n\n    // Settings\n    'settings_update' => '설정 변경',\n    'settings_update_notification' => '설졍 변경 성공',\n    'maintenance_action_run' => '유지관리 작업 실행',\n\n    // Webhooks\n    'webhook_create' => '웹 훅 생성',\n    'webhook_create_notification' => '웹 훅 생성함',\n    'webhook_update' => '웹 훅 수정',\n    'webhook_update_notification' => '웹 훅 설정이 수정되었습니다.',\n    'webhook_delete' => '웹 훅 지우기',\n    'webhook_delete_notification' => '웹 훅 삭제함',\n\n    // Imports\n    'import_create' => '컨텐츠 ZIP 파일이 생성되었습니다.',\n    'import_create_notification' => '컨텐츠 ZIP 파일이 업로드 되었습니다.',\n    'import_run' => '컨텐츠 ZIP 파일을 업데이트하였습니다.',\n    'import_run_notification' => '내용을 가져왔습니다.',\n    'import_delete' => '임포트 파일 삭제',\n    'import_delete_notification' => '임포트 파일을 삭제하였습니다.',\n\n    // Users\n    'user_create' => '사용자 생성',\n    'user_create_notification' => '사용자 생성 성공',\n    'user_update' => '사용자 갱신',\n    'user_update_notification' => '사용자가 업데이트되었습니다',\n    'user_delete' => '사용자 삭제',\n    'user_delete_notification' => '사용자가 삭제되었습니다',\n\n    // API Tokens\n    'api_token_create' => '생성된 API 토큰',\n    'api_token_create_notification' => 'API 토큰이 성공적으로 생성되었습니다.',\n    'api_token_update' => '갱신된 API 토큰',\n    'api_token_update_notification' => 'API 토큰이 성공적으로 업데이트되었습니다.',\n    'api_token_delete' => '삭제된 API 토큰',\n    'api_token_delete_notification' => 'API 토큰이 성공적으로 삭제되었습니다.',\n\n    // Roles\n    'role_create' => '역활 생성',\n    'role_create_notification' => '역할이 생성되었습니다',\n    'role_update' => '역활 갱신',\n    'role_update_notification' => '역할이 수정되었습니다',\n    'role_delete' => '역활 삭제',\n    'role_delete_notification' => '역할이 삭제되었습니다',\n\n    // Recycle Bin\n    'recycle_bin_empty' => '비운 휴지통',\n    'recycle_bin_restore' => '휴지통에서 복원됨',\n    'recycle_bin_destroy' => '휴지통에서 제거됨',\n\n    // Comments\n    'commented_on'                => '댓글 쓰기',\n    'comment_create'              => '댓글 생성',\n    'comment_update'              => '댓글 변경',\n    'comment_delete'              => '댓글 삭제',\n\n    // Sort Rules\n    'sort_rule_create' => '정렬 규칙 생성',\n    'sort_rule_create_notification' => '정렬 규칙이 성공적으로 생성되었습니다',\n    'sort_rule_update' => '정렬 규칙 업데이트',\n    'sort_rule_update_notification' => '정렬 규칙이 성공적으로 업데이트 되었습니다',\n    'sort_rule_delete' => '정렬 규칙 삭제',\n    'sort_rule_delete_notification' => '정렬 규칙이 성공적으로 삭제되었습니다',\n\n    // Other\n    'permissions_update'          => '권한 수정함',\n];\n"
  },
  {
    "path": "lang/ko/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => '자격 증명이 기록과 일치하지 않습니다.',\n    'throttle' => '로그인 시도가 너무 많습니다. :seconds초 후에 다시 시도해주세요.',\n\n    // Login & Register\n    'sign_up' => '가입',\n    'log_in' => '로그인',\n    'log_in_with' => ':socialDriver 소셜 계정으로 로그인',\n    'sign_up_with' => ':socialDriver 소셜 계정으로 가입',\n    'logout' => '로그아웃',\n\n    'name' => '이름',\n    'username' => '이용자명',\n    'email' => '전자우편 주소',\n    'password' => '비밀번호',\n    'password_confirm' => '비밀번호 확인',\n    'password_hint' => '8 글자를 넘어야 합니다.',\n    'forgot_password' => '비밀번호를 잊으셨나요?',\n    'remember_me' => '로그인 유지',\n    'ldap_email_hint' => '계정에 연결한 전자우편 주소를 입력하세요.',\n    'create_account' => '가입',\n    'already_have_account' => '계정이 있나요?',\n    'dont_have_account' => '계정이 없나요?',\n    'social_login' => '소셜 로그인',\n    'social_registration' => '소셜 계정으로 가입',\n    'social_registration_text' => '소셜 계정으로 가입하고 로그인합니다.',\n\n    'register_thanks' => '가입해 주셔서 감사합니다!',\n    'register_confirm' => '전자우편을 확인한 후 버튼을 눌러 :appName에 접근하세요.',\n    'registrations_disabled' => '가입 기능이 비활성화되어 있습니다',\n    'registration_email_domain_invalid' => '이 전자우편 주소로는 이 사이트에 접근할 수 없습니다.',\n    'register_success' => '가입했습니다! 이제 로그인할 수 있습니다.',\n\n    // Login auto-initiation\n    'auto_init_starting' => '로그인 시도 중',\n    'auto_init_starting_desc' => '로그인을 시작하기 위해 인증 시스템에 접근 중입니다. 5초 후에도 아무런 반응이 없다면 아래 링크를 클릭하세요.',\n    'auto_init_start_link' => '인증 진행',\n\n    // Password Reset\n    'reset_password' => '비밀번호 바꾸기',\n    'reset_password_send_instructions' => '전자우편 주소를 입력하세요. 이 주소로 해당 과정을 위한 링크를 보낼 것입니다.',\n    'reset_password_send_button' => '재설정 링크 보내기',\n    'reset_password_sent' => '비밀번호를 바꿀 수 있는 링크를 :email 전자우편 주소로 보낼 것입니다.',\n    'reset_password_success' => '비밀번호를 바꿨습니다.',\n    'email_reset_subject' => ':appName 비밀번호 바꾸기',\n    'email_reset_text' => '비밀번호를 바꿉니다.',\n    'email_reset_not_requested' => '비밀번호 재설정을 요청하지 않으셨다면 추가 조치가 필요하지 않습니다.',\n\n    // Email Confirmation\n    'email_confirm_subject' => ':appName 전자우편 인증을 확인합니다',\n    'email_confirm_greeting' => ':appName 서비스에 가입해 주셔서 감사합니다!',\n    'email_confirm_text' => '다음 버튼을 눌러 전자우편 주소를 확인하세요:',\n    'email_confirm_action' => '전자우편 확인',\n    'email_confirm_send_error' => '전자우편 확인이 필요하지만 이 시스템에서 전자우편을 발송하지 못했습니다. 전자우편 주소가 제대로 설정되었는지 확인하려면 관리자에게 연락을 해주세요.',\n    'email_confirm_success' => '전자우편 인증을 성공했습니다. 이 전자우편 주소로 로그인할 수 있습니다.',\n    'email_confirm_resent' => '전자우편 확인을 다시 보냈습니다. 우편함을 확인해주세요.',\n    'email_confirm_thanks' => '확인해 주셔서 감사합니다!',\n    'email_confirm_thanks_desc' => '확인이 처리되는 동안 잠시 기다려주세요. 3초 안에 리디렉션되지 않는다면 아내에 있는 \"계속\" 링크를 눌러서 진행하세요.',\n\n    'email_not_confirmed' => '전자우편 주소가 확인되지 않았습니다.',\n    'email_not_confirmed_text' => '전자우편 확인이 아직 완료되지 않았습니다.',\n    'email_not_confirmed_click_link' => '등록한 직후에 발송된 전자우편에 있는 확인 링크를 클릭하세요.',\n    'email_not_confirmed_resend' => '전자우편 확인을 위해 발송된 전자우편을 찾을 수 없다면 아래의 폼을 다시 발행하여 전자우편 확인을 재발송할 수 있습니다.',\n    'email_not_confirmed_resend_button' => '확인용 이메일 재전송',\n\n    // User Invite\n    'user_invite_email_subject' => ':appName 애플리케이션에서 초대를 받았습니다!',\n    'user_invite_email_greeting' => ':appName 애플리케이션에 가입한 기록이 있습니다.',\n    'user_invite_email_text' => '아래 버튼을 클릭하여 계정 비밀번호를 설정하고 접근 권한을 얻으세요:',\n    'user_invite_email_action' => '비밀번호 설정',\n    'user_invite_page_welcome' => ':appName 애플리케이션에 오신 것을 환영합니다!',\n    'user_invite_page_text' => '계정 설정을 마치고 접근 권한을 얻으려면 :appName 애플리케이션에 로그인할 때 사용할 비밀번호를 설정해야 합니다.',\n    'user_invite_page_confirm_button' => '비밀번호 확인',\n    'user_invite_success_login' => '비밀번호를 설정했습니다, 이제 입력한 비밀번호로 :appName 애플리케이션에 로그인할 수 있습니다!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => '다중 인증 설정',\n    'mfa_setup_desc' => '추가 보안 계층으로 다중 인증을 설정합니다.',\n    'mfa_setup_configured' => '이미 설정되었습니다',\n    'mfa_setup_reconfigure' => '재설정',\n    'mfa_setup_remove_confirmation' => '다중 인증을 해제할까요?',\n    'mfa_setup_action' => '설정',\n    'mfa_backup_codes_usage_limit_warning' => '남은 백업 코드가 다섯 개 미만입니다. 새 백업 코드 세트를 생성하지 않아 코드가 소진되면 계정이 잠길 수 있습니다.',\n    'mfa_option_totp_title' => '모바일 앱',\n    'mfa_option_totp_desc' => '다중 인증에는 Google Authenticator, Authy나 Microsoft Authenticator와 같은 TOTP 지원 모바일 앱이 필요합니다.',\n    'mfa_option_backup_codes_title' => '백업 코드',\n    'mfa_option_backup_codes_desc' => '로그인 시 인증에 사용되는 일회용 백업 코드를 만듭니다. 이 백업 코드는 안전한 곳에 보관하세요.',\n    'mfa_gen_confirm_and_enable' => '확인 및 활성화',\n    'mfa_gen_backup_codes_title' => '백업 코드 설정',\n    'mfa_gen_backup_codes_desc' => '코드 목록을 안전한 장소에 보관하세요. 코드 중 하나를 2FA에 쓸 수 있습니다.',\n    'mfa_gen_backup_codes_download' => '코드 설치',\n    'mfa_gen_backup_codes_usage_warning' => '각 코드는 한 번씩만 유효합니다.',\n    'mfa_gen_totp_title' => '모바일 앱 설정',\n    'mfa_gen_totp_desc' => '다중 인증에는 Google Authenticator, Authy나 Microsoft Authenticator와 같은 TOTP 지원 모바일 앱이 필요합니다.',\n    'mfa_gen_totp_scan' => '인증 앱으로 QR 코드를 스캔하세요.',\n    'mfa_gen_totp_verify_setup' => '설정 확인',\n    'mfa_gen_totp_verify_setup_desc' => '인증 앱에서 생성한 코드를 입력하세요:',\n    'mfa_gen_totp_provide_code_here' => '백업 코드를 입력하세요.',\n    'mfa_verify_access' => '접근 확인',\n    'mfa_verify_access_desc' => '사용자 계정에서는 액세스 권한을 부여받기 전에 추가 검증 수준을 통해 신원을 확인해야 합니다. 계속하려면 구성된 방법 중 하나를 사용하여 확인하세요.',\n    'mfa_verify_no_methods' => '설정한 방법이 없습니다.',\n    'mfa_verify_no_methods_desc' => '다중 인증을 설정하지 않았습니다. 접근 권한을 얻기 전에 하나 이상의 다중 인증을 설정해야 합니다.',\n    'mfa_verify_use_totp' => '모바일 앱으로 인증하기',\n    'mfa_verify_use_backup_codes' => '백업 코드로 인증하세요.',\n    'mfa_verify_backup_code' => '백업 코드',\n    'mfa_verify_backup_code_desc' => '나머지 백업 코드 중 하나를 입력하세요:',\n    'mfa_verify_backup_code_enter_here' => '백업 코드를 입력하세요.',\n    'mfa_verify_totp_desc' => '모바일 앱에서 생성한 백업 코드를 입력하세요:',\n    'mfa_setup_login_notification' => '다중 인증을 설정했습니다. 설정한 방법으로 다시 로그인하세요.',\n];\n"
  },
  {
    "path": "lang/ko/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => '취소',\n    'close' => '닫기',\n    'confirm' => '확인',\n    'back' => '뒤로',\n    'save' => '저장',\n    'continue' => '계속',\n    'select' => '선택',\n    'toggle_all' => '모두 보기',\n    'more' => '더 보기',\n\n    // Form Labels\n    'name' => '이름',\n    'description' => '설명',\n    'role' => '역할',\n    'cover_image' => '대표 이미지',\n    'cover_image_description' => '이 이미지는 대략 440x250px가 되어야 하지만, 필요에 따라 다양한 시나리오에서 사용자 인터페이스에 맞게 유연하게 크기 조절 및 자르기가 가능하므로 실제로 표시되는 크기는 다를 수 있습니다.',\n\n    // Actions\n    'actions' => '동작',\n    'view' => '보기',\n    'view_all' => '모두 보기',\n    'new' => '신규',\n    'create' => '만들기',\n    'update' => '바꾸기',\n    'edit' => '수정',\n    'archive' => '보관',\n    'unarchive' => '보관 해제',\n    'sort' => '정렬',\n    'move' => '이동',\n    'copy' => '복사',\n    'reply' => '답글',\n    'delete' => '삭제',\n    'delete_confirm' => '삭제',\n    'search' => '검색',\n    'search_clear' => '검색창 비우기',\n    'reset' => '리셋',\n    'remove' => '제거',\n    'add' => '추가',\n    'configure' => '구성',\n    'manage' => '관리',\n    'fullscreen' => '전체화면',\n    'favourite' => '즐겨찾기',\n    'unfavourite' => '즐겨찾기 해제',\n    'next' => '다음',\n    'previous' => '이전',\n    'filter_active' => '적용 필터:',\n    'filter_clear' => '필터 해제',\n    'download' => '내려받기',\n    'open_in_tab' => '탭에서 열기',\n    'open' => '열기',\n\n    // Sort Options\n    'sort_options' => '정렬 기준',\n    'sort_direction_toggle' => '순서 반전',\n    'sort_ascending' => '오름차순',\n    'sort_descending' => '내림차순',\n    'sort_name' => '이름',\n    'sort_default' => '기본값',\n    'sort_created_at' => '만든 날짜',\n    'sort_updated_at' => '갱신한 날짜',\n\n    // Misc\n    'deleted_user' => '삭제한 이용자',\n    'no_activity' => '활동 없음',\n    'no_items' => '항목 없음',\n    'back_to_top' => '맨 위로',\n    'skip_to_main_content' => '메인 항목으로',\n    'toggle_details' => '내용 보기',\n    'toggle_thumbnails' => '썸네일 보기',\n    'details' => '정보',\n    'grid_view' => '격자로 보기',\n    'list_view' => '목록으로 보기',\n    'default' => '기본 설정',\n    'breadcrumb' => '탐색 경로',\n    'status' => '상태',\n    'status_active' => '활성',\n    'status_inactive' => '비활성',\n    'never' => '안 함',\n    'none' => '없음',\n\n    // Header\n    'homepage' => '홈페이지',\n    'header_menu_expand' => '헤더 메뉴 펼치기',\n    'profile_menu' => '프로필',\n    'view_profile' => '프로필 보기',\n    'edit_profile' => '프로필 바꾸기',\n    'dark_mode' => '어두운 테마',\n    'light_mode' => '밝은 테마',\n    'global_search' => '전역 검색',\n\n    // Layout tabs\n    'tab_info' => '정보',\n    'tab_info_label' => '탭: 보조 정보 보이기',\n    'tab_content' => '내용',\n    'tab_content_label' => '탭: 우선 항목 보이기',\n\n    // Email Content\n    'email_action_help' => ':actionText를 클릭할 수 없을 때는 웹 브라우저에서 다음 링크로 접속할 수 있습니다.',\n    'email_rights' => '모든 권리는 보호됩니다.',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => '개인 정보 처리 방침',\n    'terms_of_service' => '서비스 이용 약관',\n\n    // OpenSearch\n    'opensearch_description' => '검색 :appName',\n];\n"
  },
  {
    "path": "lang/ko/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => '이미지 선택',\n    'image_list' => '이미지 목록',\n    'image_details' => '이미지 상세정보',\n    'image_upload' => '이미지 올려두기',\n    'image_intro' => '여기에서 이전에 시스템에 업로드한 이미지를 선택하고 관리할 수 있습니다.',\n    'image_intro_upload' => '이미지 파일을 이 창으로 끌어다 놓거나 위의 \\'이미지 업로드\\' 버튼을 사용하여 새 이미지를 업로드합니다.',\n    'image_all' => '모든 이미지',\n    'image_all_title' => '모든 이미지 보기',\n    'image_book_title' => '이 책에서 쓰고 있는 이미지',\n    'image_page_title' => '이 문서에서 쓰고 있는 이미지',\n    'image_search_hint' => '이미지 이름 검색',\n    'image_uploaded' => '올림 :uploadedDate',\n    'image_uploaded_by' => ':userName 이용자가 올려둠',\n    'image_uploaded_to' => ':pageLink 로 업로드됨',\n    'image_updated' => '갱신일 :updateDate',\n    'image_load_more' => '더 보기',\n    'image_image_name' => '이미지 이름',\n    'image_delete_used' => '이 이미지는 다음 문서들이 쓰고 있습니다.',\n    'image_delete_confirm_text' => '이 이미지를 정말 삭제하시겠습니까?',\n    'image_select_image' => '이미지 선택',\n    'image_dropzone' => '여기에 이미지를 드롭하거나 여기를 클릭하세요. 이미지를 올릴 수 있습니다.',\n    'image_dropzone_drop' => '업로드 할 이미지 파일을 여기에 놓으세요.',\n    'images_deleted' => '이미지 삭제함',\n    'image_preview' => '이미지 미리 보기',\n    'image_upload_success' => '이미지 올림',\n    'image_update_success' => '이미지 정보가 수정되었습니다.',\n    'image_delete_success' => '이미지가 삭제되었습니다.',\n    'image_replace' => '이미지 교체',\n    'image_replace_success' => '이미지가 수정되었습니다.',\n    'image_rebuild_thumbs' => '사이즈 변경 재생성하기',\n    'image_rebuild_thumbs_success' => '이미지 크기 변경이 성공적으로 완료되었습니다!',\n\n    // Code Editor\n    'code_editor' => '코드 수정',\n    'code_language' => '언어',\n    'code_content' => '내용',\n    'code_session_history' => '세션 기록',\n    'code_save' => '저장',\n];\n"
  },
  {
    "path": "lang/ko/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => '일반',\n    'advanced' => '고급',\n    'none' => '없음',\n    'cancel' => '취소',\n    'save' => '저장',\n    'close' => '닫기',\n    'apply' => '적용',\n    'undo' => '되돌리기',\n    'redo' => '다시 실행',\n    'left' => '왼쪽',\n    'center' => '가운데',\n    'right' => '오른쪽',\n    'top' => '위',\n    'middle' => '가운데',\n    'bottom' => '아래',\n    'width' => '너비',\n    'height' => '높이',\n    'More' => '더 보기',\n    'select' => '선택...',\n\n    // Toolbar\n    'formats' => '형식',\n    'header_large' => '큰 제목',\n    'header_medium' => '중간 제목',\n    'header_small' => '작은 제목',\n    'header_tiny' => '가장 작은 제목',\n    'paragraph' => '단락',\n    'blockquote' => '인용',\n    'inline_code' => '인라인 코드',\n    'callouts' => '범례',\n    'callout_information' => '정보',\n    'callout_success' => '성공',\n    'callout_warning' => '경고',\n    'callout_danger' => '위험',\n    'bold' => '굵게',\n    'italic' => '기울임체',\n    'underline' => '밑줄',\n    'strikethrough' => '취소선',\n    'superscript' => '윗첨자',\n    'subscript' => '아랫첨자',\n    'text_color' => '글자 색상',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => '사용자 지정 색상',\n    'remove_color' => '색상 제거',\n    'background_color' => '배경 색상',\n    'align_left' => '왼쪽 정렬',\n    'align_center' => '가운데 정렬',\n    'align_right' => '오른쪽 정렬',\n    'align_justify' => '양쪽 맞춤',\n    'list_bullet' => '글머리 기호 목록',\n    'list_numbered' => '번호 매기기 목록',\n    'list_task' => '작업 목록',\n    'indent_increase' => '들여쓰기 증가',\n    'indent_decrease' => '들여쓰기 감소',\n    'table' => '테이블',\n    'insert_image' => '이미지 삽입',\n    'insert_image_title' => '이미지 삽입/수정',\n    'insert_link' => '링크 삽입/수정',\n    'insert_link_title' => '링크 삽입/수정',\n    'insert_horizontal_line' => '수평선 삽입',\n    'insert_code_block' => '코드 블럭 삽입',\n    'edit_code_block' => '코드 블록 편집',\n    'insert_drawing' => '그리기 삽입/수정',\n    'drawing_manager' => '그리기 설정',\n    'insert_media' => '미디어 삽입/수정',\n    'insert_media_title' => '미디어 삽입/수정',\n    'clear_formatting' => '양식 초기화',\n    'source_code' => '소스 코드',\n    'source_code_title' => '소스 코드',\n    'fullscreen' => '전체화면',\n    'image_options' => '이미지 옵션',\n\n    // Tables\n    'table_properties' => '테이블 속성',\n    'table_properties_title' => '테이블 속성',\n    'delete_table' => '테이블 삭제',\n    'table_clear_formatting' => '테이블 형식 지우기',\n    'resize_to_contents' => '내용 크기 조정',\n    'row_header' => '행 머릿글',\n    'insert_row_before' => '앞에 행 추가',\n    'insert_row_after' => '뒤에 행 추가',\n    'delete_row' => '행 삭제',\n    'insert_column_before' => '앞에 열 추가',\n    'insert_column_after' => '뒤에 열 추가',\n    'delete_column' => '열 삭제',\n    'table_cell' => '셀',\n    'table_row' => '행',\n    'table_column' => '열',\n    'cell_properties' => '셀 속성',\n    'cell_properties_title' => '셀 속성',\n    'cell_type' => '셀 스타일',\n    'cell_type_cell' => '셀',\n    'cell_scope' => '범위',\n    'cell_type_header' => '헤더 셀',\n    'merge_cells' => '셀 병합',\n    'split_cell' => '셀 분할',\n    'table_row_group' => '행 그룹',\n    'table_column_group' => '열 그룹',\n    'horizontal_align' => '가로 맞춤',\n    'vertical_align' => '세로 맞춤',\n    'border_width' => '테두리 너비',\n    'border_style' => '테두리 스타일',\n    'border_color' => '테두리 색',\n    'row_properties' => '행 속성',\n    'row_properties_title' => '열 속성',\n    'cut_row' => '행 자르기',\n    'copy_row' => '행 복사',\n    'paste_row_before' => '앞에 행 붙여넣기',\n    'paste_row_after' => '다음 뒤에 행 붙여넣기',\n    'row_type' => '행 타입',\n    'row_type_header' => '머리글',\n    'row_type_body' => '본문',\n    'row_type_footer' => '바닥글',\n    'alignment' => '정렬',\n    'cut_column' => '열 잘라내기',\n    'copy_column' => '열 복사',\n    'paste_column_before' => '열 앞에 붙여넣기',\n    'paste_column_after' => '열 뒤에 붙여넣기',\n    'cell_padding' => '셀 패딩',\n    'cell_spacing' => '셀 간격',\n    'caption' => '캡션',\n    'show_caption' => '캡션 보기',\n    'constrain' => '비율 유지',\n    'cell_border_solid' => '단색',\n    'cell_border_dotted' => '점선',\n    'cell_border_dashed' => '파선',\n    'cell_border_double' => '겹선',\n    'cell_border_groove' => '테두리 음각',\n    'cell_border_ridge' => '테두리 양각',\n    'cell_border_inset' => '요소 음각',\n    'cell_border_outset' => '요소 양각',\n    'cell_border_none' => '없음',\n    'cell_border_hidden' => '숨김',\n\n    // Images, links, details/summary & embed\n    'source' => '원본',\n    'alt_desc' => '대체 설명',\n    'embed' => '포함',\n    'paste_embed' => '아래에 포함할 코드를 붙여넣습니다:',\n    'url' => 'URL',\n    'text_to_display' => '표시할 텍스트',\n    'title' => '제목',\n    'browse_links' => '링크 찾기',\n    'open_link' => '링크 열기',\n    'open_link_in' => '다음에서 링크 열기...',\n    'open_link_current' => '현재 창',\n    'open_link_new' => '새 창',\n    'remove_link' => '링크 제거',\n    'insert_collapsible' => '접을 수 있는 블록 삽입',\n    'collapsible_unwrap' => '언래핑',\n    'edit_label' => '레이블 수정',\n    'toggle_open_closed' => '열림/닫힘 전환',\n    'collapsible_edit' => '접을 수 있는 블록 편집',\n    'toggle_label' => '레이블 보이기/숨기기',\n\n    // About view\n    'about' => '이 편집기에 대하여',\n    'about_title' => 'WYSIWYG 편집기에 대하여',\n    'editor_license' => '편집기 라이선스 & 저작권',\n    'editor_lexical_license' => '이 편집기는 MIT 라이선스에 따라 배포되는 :lexicalLink의 포크로 만들어졌습니다.',\n    'editor_lexical_license_link' => '전체 라이센스 세부 사항은 여기에서 확인할 수 있습니다.',\n    'editor_tiny_license' => '이 편집기는 MIT 라이선스에 따라 제공되는 :tinyLink를 사용하여 제작되었습니다.',\n    'editor_tiny_license_link' => 'TinyMCE의 저작권 및 라이선스 세부 정보는 여기에서 확인할 수 있습니다.',\n    'save_continue' => '저장하고 계속하기',\n    'callouts_cycle' => '(계속 누르면 유형이 전환됩니다.)',\n    'link_selector' => '콘텐츠에 연결',\n    'shortcuts' => '단축키',\n    'shortcut' => '단축키',\n    'shortcuts_intro' => '편집기에서 사용할 수 있는 바로 가기는 다음과 같습니다:',\n    'windows_linux' => '(윈도우/리눅스)',\n    'mac' => '(맥)',\n    'description' => '상세정보',\n];\n"
  },
  {
    "path": "lang/ko/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => '최근에 수정함',\n    'recently_created_pages' => '최근에 만든 문서',\n    'recently_updated_pages' => '최근에 수정한 문서',\n    'recently_created_chapters' => '최근에 만든 챕터',\n    'recently_created_books' => '최근에 만든 책',\n    'recently_created_shelves' => '최근에 만든 책장',\n    'recently_update' => '최신 수정 목록',\n    'recently_viewed' => '최근에 본 목록',\n    'recent_activity' => '최근 활동 기록',\n    'create_now' => '바로 만들기',\n    'revisions' => '수정본',\n    'meta_revision' => '수정본 #:revisionCount',\n    'meta_created' => '생성 :timeLength',\n    'meta_created_name' => '생성 :timeLength, :user',\n    'meta_updated' => '수정 :timeLength',\n    'meta_updated_name' => '수정 :timeLength, :user',\n    'meta_owned_name' => ':user 소유',\n    'meta_reference_count' => '참조 대상 :count item|참조 대상 :count items',\n    'entity_select' => '항목 선택',\n    'entity_select_lack_permission' => '이 항목을 선택하기 위해 필요한 권한이 없습니다',\n    'images' => '이미지',\n    'my_recent_drafts' => '내 최근의 초안 문서',\n    'my_recently_viewed' => '내가 읽은 문서',\n    'my_most_viewed_favourites' => '내가 가장 많이 본 즐겨찾기',\n    'my_favourites' => '나의 즐겨찾기',\n    'no_pages_viewed' => '본 페이지가 없습니다.',\n    'no_pages_recently_created' => '최근에 생성된 페이지가 없습니다.',\n    'no_pages_recently_updated' => '최근 업데이트된 페이지가 없습니다.',\n    'export' => '내보내기',\n    'export_html' => '포함된 웹 파일',\n    'export_pdf' => 'PDF 파일',\n    'export_text' => '일반 텍스트 파일',\n    'export_md' => '마크다운 파일',\n    'export_zip' => '컨텐츠 ZIP 파일',\n    'default_template' => '기본 페이지 템플릿',\n    'default_template_explain' => '이 항목 내에서 생성되는 모든 페이지의 기본 콘텐츠로 사용할 페이지 템플릿을 지정합니다. 페이지 작성자가 선택한 템플릿 페이지를 볼 수 있는 권한이 있는 경우에만 이 항목이 사용된다는 점을 유의하세요.',\n    'default_template_select' => '템플릿 페이지 선택',\n    'import' => '컨텐츠 ZIP 파일 가져오기',\n    'import_validate' => '컨텐츠 ZIP 파일 검증하기',\n    'import_desc' => '같은 인스턴스나 다른 인스턴스에서 휴대용 zip 내보내기를 사용하여 책, 장 및 페이지를 가져옵니다. 진행하려면 ZIP 파일을 선택합니다. 파일을 업로드하고 검증한 후 다음 보기에서 가져오기를 구성하고 확인할 수 있습니다.',\n    'import_zip_select' => '업로드할 휴대용 압축 파일 선택',\n    'import_zip_validation_errors' => '컨텐츠 ZIP 파일을 검증하는 동안 오류가 감지되었습니다.',\n    'import_pending' => '컨텐츠 ZIP 파일 가져오기가 일시정지 되었습니다.',\n    'import_pending_none' => 'Portable ZIP 파일 가져오기가 시작되지 않았습니다.',\n    'import_continue' => '컨텐츠 ZIP 파일 가져오기가 시작되지 않았습니다.',\n    'import_continue_desc' => '업로드된 컨텐츠 ZIP 파일에서 가져올 내용을 검토합니다. 준비가 되면 가져오기를 실행하여 이 시스템에 내용을 추가합니다. 업로드된 ZIP 가져오기 파일은 가져오기가 성공하면 자동으로 제거됩니다.',\n    'import_details' => '컨텐츠 ZIP 파일 상세',\n    'import_run' => '가져오기 실행',\n    'import_size' => '컨텐츠 ZIP 파일 사이즈',\n    'import_uploaded_at' => '업로드되었습니다. :relativeTime',\n    'import_uploaded_by' => '업로드되었습니다.',\n    'import_location' => '가져올 경로',\n    'import_location_desc' => '가져온 콘텐츠에 대한 대상 위치를 선택하세요. 선택한 위치 내에서 만들려면 관련 권한이 필요합니다.',\n    'import_delete_confirm' => '정말 삭제하시겠습니까?',\n    'import_delete_desc' => '업로드된 ZIP 파일이 삭제되며, 실행 취소할 수 없습니다.',\n    'import_errors' => '가져오기 오류',\n    'import_errors_desc' => '가져오기 중 에러가 발생했습니다.',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => '권한',\n    'permissions_desc' => '여기에서 권한을 설정하여 사용자 역할에서 제공하는 기본 권한을 재정의합니다.',\n    'permissions_book_cascade' => '책에 설정된 권한은 자체 권한이 정의되어 있지 않은 한 하위 챕터와 페이지에 자동으로 계단식으로 적용됩니다.',\n    'permissions_chapter_cascade' => '챕터에 설정된 권한은 하위 페이지에 자체 권한이 정의되어 있지 않는 한 자동으로 계단식으로 적용됩니다.',\n    'permissions_save' => '권한 저장',\n    'permissions_owner' => '소유자',\n    'permissions_role_everyone_else' => '그 외 모든 사용자',\n    'permissions_role_everyone_else_desc' => '특별히 재정의되지 않은 모든 역할에 대한 권한을 설정.',\n    'permissions_role_override' => '역할에 대한 권한 재정의하기',\n    'permissions_inherit_defaults' => '기본값 상속',\n\n    // Search\n    'search_results' => '검색 결과',\n    'search_total_results_found' => ':count개|총 :count개',\n    'search_clear' => '검색 지우기',\n    'search_no_pages' => '결과 없음',\n    'search_for_term' => ':term 검색',\n    'search_more' => '더 많은 결과',\n    'search_advanced' => '고급 검색',\n    'search_terms' => '용어 검색',\n    'search_content_type' => '형식',\n    'search_exact_matches' => '정확히 일치',\n    'search_tags' => '태그 검색',\n    'search_options' => '선택',\n    'search_viewed_by_me' => '내가 읽음',\n    'search_not_viewed_by_me' => '내가 읽지 않음',\n    'search_permissions_set' => '권한 설정함',\n    'search_created_by_me' => '내가 만듦',\n    'search_updated_by_me' => '내가 수정함',\n    'search_owned_by_me' => '내가 소유함',\n    'search_date_options' => '날짜',\n    'search_updated_before' => '이전에 수정함',\n    'search_updated_after' => '이후에 수정함',\n    'search_created_before' => '이전에 만듦',\n    'search_created_after' => '이후에 만듦',\n    'search_set_date' => '날짜 설정',\n    'search_update' => '검색',\n\n    // Shelves\n    'shelf' => '책꽂이',\n    'shelves' => '책꽂이',\n    'x_shelves' => '책꽂이 :count개|총 :count개',\n    'shelves_empty' => '만든 책꽂이가 없습니다.',\n    'shelves_create' => '책꽂이 만들기',\n    'shelves_popular' => '많이 읽은 책꽂이',\n    'shelves_new' => '새로운 책꽂이',\n    'shelves_new_action' => '새 책꽂이',\n    'shelves_popular_empty' => '많이 읽은 책꽂이 목록',\n    'shelves_new_empty' => '새로운 책꽂이 목록',\n    'shelves_save' => '저장',\n    'shelves_books' => '이 책꽂이에 있는 책들',\n    'shelves_add_books' => '이 책꽂이에 책 추가',\n    'shelves_drag_books' => '책을 이 책장에 추가하려면 아래로 드래그하세요',\n    'shelves_empty_contents' => '이 책꽂이에 책이 없습니다.',\n    'shelves_edit_and_assign' => '책꽂이 바꾸기로 책을 추가하세요.',\n    'shelves_edit_named' => '책꽂이 편집 :name',\n    'shelves_edit' => '책꽂이 편집',\n    'shelves_delete' => '책꽂이 삭제',\n    'shelves_delete_named' => '책꽂이 삭제 :이름',\n    'shelves_delete_explain' => \"그러면 ':name'이라는 이름의 서가가 삭제됩니다. 포함된 책은 삭제되지 않습니다.\",\n    'shelves_delete_confirmation' => '이 책꽂이를 삭제하시겠습니까?',\n    'shelves_permissions' => '책꽂이 권한',\n    'shelves_permissions_updated' => '책꽂이 권한 업데이트됨',\n    'shelves_permissions_active' => '책꽂이 권한 활성화',\n    'shelves_permissions_cascade_warning' => '책꽂이에 대한 권한은 포함된 책에 자동으로 계단식으로 부여되지 않습니다. 한 권의 책이 여러 개의 책꽂이에 존재할 수 있기 때문입니다. 그러나 아래 옵션을 사용하여 권한을 하위 책으로 복사할 수 있습니다.',\n    'shelves_permissions_create' => '책꽂이 만들기 권한은 아래 작업을 사용하여 하위 책에 대한 권한을 복사하는 데만 사용됩니다. 책을 만드는 기능은 제어하지 않습니다.',\n    'shelves_copy_permissions_to_books' => '권한 맞춤',\n    'shelves_copy_permissions' => '실행',\n    'shelves_copy_permissions_explain' => '그러면 이 책꽂이의 현재 권한 설정이 이 책꽂이에 포함된 모든 책에 적용됩니다. 활성화하기 전에 이 책꽂이의 권한에 대한 변경 사항이 모두 저장되었는지 확인하세요.',\n    'shelves_copy_permission_success' => '책꽂이 권한이 복사됨 :count books',\n\n    // Books\n    'book' => '책',\n    'books' => '책',\n    'x_books' => '책 :count개|총 :count개',\n    'books_empty' => '만든 책이 없습니다.',\n    'books_popular' => '많이 읽은 책',\n    'books_recent' => '최근에 읽은 책',\n    'books_new' => '새로운 책',\n    'books_new_action' => '새 책',\n    'books_popular_empty' => '많이 읽은 책 목록',\n    'books_new_empty' => '새로운 책 목록',\n    'books_create' => '책 만들기',\n    'books_delete' => '책 삭제하기',\n    'books_delete_named' => ':bookName(을)를 지웁니다.',\n    'books_delete_explain' => ':bookName에 있는 모든 챕터와 문서도 지웁니다.',\n    'books_delete_confirmation' => '이 책을 지우시겠습니까?',\n    'books_edit' => '책 바꾸기',\n    'books_edit_named' => ':bookName(을)를 바꿉니다.',\n    'books_form_book_name' => '책 이름',\n    'books_save' => '저장',\n    'books_permissions' => '책 권한',\n    'books_permissions_updated' => '책의 권한이 수정되었습니다.',\n    'books_empty_contents' => '이 책에 챕터나 문서가 없습니다.',\n    'books_empty_create_page' => '문서 만들기',\n    'books_empty_sort_current_book' => '현재 책 정렬',\n    'books_empty_add_chapter' => '챕터 만들기',\n    'books_permissions_active' => '책 권한 적용됨',\n    'books_search_this' => '이 책에서 검색',\n    'books_navigation' => '목차',\n    'books_sort' => '책 내용 정렬',\n    'books_sort_desc' => '책 내의 챕터와 페이지를 이동하여 콘텐츠를 재구성할 수 있습니다. 다른 책들을 추가하여 책 간의 챕터와 페이지를 쉽게 이동할 수 있습니다. 선택적으로 자동 정렬 규칙을 설정하여 변경 시 이 책의 콘텐츠를 자동으로 정렬할 수 있습니다.',\n    'books_sort_auto_sort' => '자동 정렬 옵션',\n    'books_sort_auto_sort_active' => '현재 설정된 자동 정렬: :sortName',\n    'books_sort_named' => ':bookName 정렬',\n    'books_sort_name' => '제목',\n    'books_sort_created' => '만든 날짜',\n    'books_sort_updated' => '수정한 날짜',\n    'books_sort_chapters_first' => '챕터 우선',\n    'books_sort_chapters_last' => '문서 우선',\n    'books_sort_show_other' => '다른 책들',\n    'books_sort_save' => '적용',\n    'books_sort_show_other_desc' => '여기에 다른 책을 추가하여 정렬 작업에 포함시키고 책 간 재구성을 쉽게 할 수 있습니다.',\n    'books_sort_move_up' => '위로 이동',\n    'books_sort_move_down' => '아래로 이동',\n    'books_sort_move_prev_book' => '이전 책으로 이동',\n    'books_sort_move_next_book' => '다음 책으로 이동',\n    'books_sort_move_prev_chapter' => '이전 챕터로 이동',\n    'books_sort_move_next_chapter' => '다음 챕터로 이동',\n    'books_sort_move_book_start' => '책 시작 부분으로 이동',\n    'books_sort_move_book_end' => '책의 끝으로 이동',\n    'books_sort_move_before_chapter' => '이전 챕터로 이동',\n    'books_sort_move_after_chapter' => '챕터 뒤로 이동',\n    'books_copy' => '책 복사하기',\n    'books_copy_success' => '책을 복사하였습니다.',\n\n    // Chapters\n    'chapter' => '챕터',\n    'chapters' => '챕터',\n    'x_chapters' => '챕터 :count개|총 :count개',\n    'chapters_popular' => '많이 읽은 챕터',\n    'chapters_new' => '새 장',\n    'chapters_create' => '챕터 만들기',\n    'chapters_delete' => '챕터 삭제하기',\n    'chapters_delete_named' => ':chapterName(을)를 지웁니다.',\n    'chapters_delete_explain' => '\\':ChapterName\\'에 있는 모든 페이지도 지웁니다.',\n    'chapters_delete_confirm' => '이 챕터를 지울 건가요?',\n    'chapters_edit' => '챕터 수정하기',\n    'chapters_edit_named' => ':chapterName 바꾸기',\n    'chapters_save' => '저장',\n    'chapters_move' => '챕터 이동하기',\n    'chapters_move_named' => ':chapterName 이동하기',\n    'chapters_copy' => '챕터 복사하기',\n    'chapters_copy_success' => '챕터를 복사하였습니다.',\n    'chapters_permissions' => '챕터 권한',\n    'chapters_empty' => '이 챕터에 문서가 없습니다.',\n    'chapters_permissions_active' => '문서 권한 허용함',\n    'chapters_permissions_success' => '챕터의 권한을 수정하였습니다.',\n    'chapters_search_this' => '이 챕터에서 검색',\n    'chapter_sort_book' => '책 정렬하기',\n\n    // Pages\n    'page' => '페이지',\n    'pages' => '페이지',\n    'x_pages' => '문서 :count개|총 :count개',\n    'pages_popular' => '많이 읽은 문서',\n    'pages_new' => '새 페이지',\n    'pages_attachments' => '첨부',\n    'pages_navigation' => '목차',\n    'pages_delete' => '문서 삭제하기',\n    'pages_delete_named' => ':pageName 삭제하기',\n    'pages_delete_draft_named' => ':pageName 초안 문서 삭제하기',\n    'pages_delete_draft' => '초안 문서 삭제하기',\n    'pages_delete_success' => '문서 지움',\n    'pages_delete_draft_success' => '초안 문서 지움',\n    'pages_delete_warning_template' => '이 페이지는 책의 기본 페이지 템플릿으로 사용 중입니다. 이 페이지가 삭제되면 해당하는 책에 더 이상 기본 페이지 템플릿이 적용되지 않습니다.',\n    'pages_delete_confirm' => '이 문서를 지울 건가요?',\n    'pages_delete_draft_confirm' => '이 초안을 지울 건가요?',\n    'pages_editing_named' => ':pageName 수정',\n    'pages_edit_draft_options' => '초안 문서 옵션',\n    'pages_edit_save_draft' => '초안으로 저장',\n    'pages_edit_draft' => '초안 문서 수정',\n    'pages_editing_draft' => '초안 문서 수정',\n    'pages_editing_page' => '문서 수정',\n    'pages_edit_draft_save_at' => '보관함: ',\n    'pages_edit_delete_draft' => '초안 삭제',\n    'pages_edit_delete_draft_confirm' => '초안 페이지 변경 내용을 삭제하시겠습니까? 마지막 전체 저장 이후의 모든 변경 내용이 손실되고 편집기가 최신 페이지의 초안 저장 상태가 아닌 상태로 업데이트됩니다.',\n    'pages_edit_discard_draft' => '변경된 내용 삭제',\n    'pages_edit_switch_to_markdown' => '마크다운 편집기로 전환',\n    'pages_edit_switch_to_markdown_clean' => '(Clean Content)',\n    'pages_edit_switch_to_markdown_stable' => '(Stable Content)',\n    'pages_edit_switch_to_wysiwyg' => 'WYSIWYG 편집기로 전환',\n    'pages_edit_switch_to_new_wysiwyg' => '새 위지윅 편집기로 변경',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => '수정본 설명',\n    'pages_edit_enter_changelog_desc' => '수정본 설명',\n    'pages_edit_enter_changelog' => '변경 로그 입력란',\n    'pages_editor_switch_title' => '편집기 전환',\n    'pages_editor_switch_are_you_sure' => '이 페이지의 편집기를 변경하시겠어요?',\n    'pages_editor_switch_consider_following' => '편집기를 전환할 때에 다음 사항들을 고려하세요:',\n    'pages_editor_switch_consideration_a' => '저장된 새 편집기 옵션은 편집기 유형을 직접 변경할 수 없는 편집자를 포함하여 향후 모든 편집자가 사용할 수 있습니다.',\n    'pages_editor_switch_consideration_b' => '이로 인해 특정 상황에서는 상세 내용과 구문이 손실될 수 있습니다.',\n    'pages_editor_switch_consideration_c' => '마지막 저장 이후 변경된 태그 또는 변경 로그 변경 사항은 이 변경 사항에서 유지되지 않습니다.',\n    'pages_save' => '저장',\n    'pages_title' => '문서 제목',\n    'pages_name' => '문서 이름',\n    'pages_md_editor' => '에디터',\n    'pages_md_preview' => '미리 보기',\n    'pages_md_insert_image' => '이미지 추가',\n    'pages_md_insert_link' => '내부 링크',\n    'pages_md_insert_drawing' => '드로잉 추가',\n    'pages_md_show_preview' => '미리보기 표시',\n    'pages_md_sync_scroll' => '미리보기 스크롤 동기화',\n    'pages_md_plain_editor' => '플레인텍스트 편집기',\n    'pages_drawing_unsaved' => '저장되지 않은 드로잉 발견',\n    'pages_drawing_unsaved_confirm' => '이전에 실패한 드로잉 저장 시도에서 저장되지 않은 드로잉 데이터가 발견되었습니다. 이 저장되지 않은 드로잉을 복원하고 계속 편집하시겠습니까?',\n    'pages_not_in_chapter' => '챕터에 있는 문서가 아닙니다.',\n    'pages_move' => '문서 이동하기',\n    'pages_copy' => '문서 복제',\n    'pages_copy_desination' => '복제할 위치',\n    'pages_copy_success' => '복제함',\n    'pages_permissions' => '문서 권한',\n    'pages_permissions_success' => '문서 권한 바꿈',\n    'pages_revision' => '수정본',\n    'pages_revisions' => '문서 수정본',\n    'pages_revisions_desc' => '아래는 이 페이지의 모든 과거 개정 버전입니다. 권한이 허용하는 경우 이전 페이지 버전을 되돌아보고, 비교하고, 복원할 수 있습니다. 시스템 구성에 따라 이전 수정본이 자동으로 삭제될 수 있으므로 페이지의 전체 기록이 여기에 완전히 반영되지 않을 수 있습니다.',\n    'pages_revisions_named' => ':pageName 수정본',\n    'pages_revision_named' => ':pageName 수정본',\n    'pages_revision_restored_from' => '#:id; :summary에서 복구함',\n    'pages_revisions_created_by' => '만든 사용자',\n    'pages_revisions_date' => '수정한 날짜',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => '수정 번호',\n    'pages_revisions_numbered' => '수정본 #:id',\n    'pages_revisions_numbered_changes' => '수정본 #:id에서 바꾼 부분',\n    'pages_revisions_editor' => '편집기 유형',\n    'pages_revisions_changelog' => '설명',\n    'pages_revisions_changes' => '바꾼 부분',\n    'pages_revisions_current' => '현재 판본',\n    'pages_revisions_preview' => '미리 보기',\n    'pages_revisions_restore' => '복원',\n    'pages_revisions_none' => '수정본이 없습니다.',\n    'pages_copy_link' => '주소 복사',\n    'pages_edit_content_link' => '편집기의 섹션으로 이동',\n    'pages_pointer_enter_mode' => '섹션 선택 모드로 들어가기',\n    'pages_pointer_label' => '페이지 섹션 옵션',\n    'pages_pointer_permalink' => '페이지 섹션 퍼머링크',\n    'pages_pointer_include_tag' => '페이지 섹션 포함 태그 포함',\n    'pages_pointer_toggle_link' => '퍼머링크 모드, 포함 태그를 표시하려면 누릅니다.',\n    'pages_pointer_toggle_include' => '태그 포함 모드, 퍼머링크를 표시하려면 누릅니다.',\n    'pages_permissions_active' => '문서 권한 허용함',\n    'pages_initial_revision' => '최초 게시',\n    'pages_references_update_revision' => '시스템에서 내부 링크 자동 업데이트',\n    'pages_initial_name' => '제목 없음',\n    'pages_editing_draft_notification' => ':timeDiff에 초안 문서입니다.',\n    'pages_draft_edited_notification' => '최근에 수정한 문서이기 때문에 초안 문서를 폐기하는 편이 좋습니다.',\n    'pages_draft_page_changed_since_creation' => '최근에 수정한 문서이기 때문에 임시 저장문서를 폐기하는 편이 좋습니다.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count명이 이 문서를 수정하고 있습니다.',\n        'start_b' => ':userName이 이 문서를 수정하고 있습니다.',\n        'time_a' => '수정본이 생겼습니다.',\n        'time_b' => '(:minCount분 전)',\n        'message' => ':start :time. 다른 사용자의 수정본을 덮어쓰지 않도록 주의하세요.',\n    ],\n    'pages_draft_discarded' => '초안 폐기! 편집기가 현재 페이지 콘텐츠로 업데이트되었습니다.',\n    'pages_draft_deleted' => '초안이 삭제되었습니다! 편집기가 현재 페이지 콘텐츠로 업데이트되었습니다.',\n    'pages_specific' => '특정한 문서',\n    'pages_is_template' => '템플릿',\n\n    // Editor Sidebar\n    'toggle_sidebar' => '사이드바 토글',\n    'page_tags' => '페이지 태그',\n    'chapter_tags' => '장 태그',\n    'book_tags' => '책 태그',\n    'shelf_tags' => '책꽂이 태그',\n    'tag' => '태그',\n    'tags' =>  '태그',\n    'tags_index_desc' => '태그를 시스템 내의 콘텐츠에 적용하여 유연한 형태의 분류를 적용할 수 있습니다. 태그는 키와 값을 모두 가질 수 있으며 값은 선택 사항입니다. 태그가 적용되면 태그 이름과 값을 사용하여 콘텐츠를 쿼리할 수 있습니다.',\n    'tag_name' =>  '태그 이름',\n    'tag_value' => '리스트 값 (선택 사항)',\n    'tags_explain' => \"문서를 더 잘 분류하려면 태그를 추가하세요.\\n태그에 값을 할당하여 더욱 체계적으로 구성할 수 있습니다.\",\n    'tags_add' => '다른 태그 추가하기',\n    'tags_remove' => '이 태그 제거하기',\n    'tags_usages' => '전체 태그 이용량',\n    'tags_assigned_pages' => '| 페이지 태그 할당 |',\n    'tags_assigned_chapters' => '| 장 태그 할당 |',\n    'tags_assigned_books' => '| 책 태그 할당 |',\n    'tags_assigned_shelves' => '| 책꽂이 태그 할당 |',\n    'tags_x_unique_values' => ':count 중복 없는 값',\n    'tags_all_values' => '모든 값',\n    'tags_view_tags' => '태그 보기',\n    'tags_view_existing_tags' => '기존 태그 보기',\n    'tags_list_empty_hint' => '태그는 에디터 사이드바나 책, 챕터 또는 책꽂이 정보 편집에서 지정할 수 있습니다.',\n    'attachments' => '첨부 파일',\n    'attachments_explain' => '파일이나 링크를 첨부하세요. 정보 탭에 나타납니다.',\n    'attachments_explain_instant_save' => '여기에서 바꾼 내용은 바로 적용합니다.',\n    'attachments_upload' => '파일 올리기',\n    'attachments_link' => '링크로 첨부',\n    'attachments_upload_drop' => '또는 파일을 여기로 끌어다 놓아 첨부 파일로 업로드할 수도 있습니다.',\n    'attachments_set_link' => '링크 설정',\n    'attachments_delete' => '이 첨부 파일을 지울 건가요?',\n    'attachments_dropzone' => '업로드할 파일을 여기에 놓아주세요.',\n    'attachments_no_files' => '올린 파일 없음',\n    'attachments_explain_link' => '파일을 올리지 않고 링크로 첨부할 수 있습니다.',\n    'attachments_link_name' => '링크 이름',\n    'attachment_link' => '파일 주소',\n    'attachments_link_url' => '파일로 링크',\n    'attachments_link_url_hint' => '파일 주소',\n    'attach' => '파일 첨부',\n    'attachments_insert_link' => '페이지에 첨부파일 링크 추가',\n    'attachments_edit_file' => '파일 수정',\n    'attachments_edit_file_name' => '파일 이름',\n    'attachments_edit_drop_upload' => '여기에 파일을 드롭하거나 여기를 클릭하세요. 파일을 올리거나 덮어쓸 수 있습니다.',\n    'attachments_order_updated' => '첨부 순서 바꿈',\n    'attachments_updated_success' => '첨부 파일 정보 수정함',\n    'attachments_deleted' => '첨부 파일 삭제함',\n    'attachments_file_uploaded' => '파일 올림',\n    'attachments_file_updated' => '파일 바꿈',\n    'attachments_link_attached' => '링크 첨부함',\n    'templates' => '템플릿',\n    'templates_set_as_template' => '현재 페이지는 템플릿용 페이지 입니다.',\n    'templates_explain_set_as_template' => '템플릿은 보기 권한만 있어도 문서에 쓸 수 있습니다.',\n    'templates_replace_content' => '문서 대체',\n    'templates_append_content' => '문서 앞에 추가',\n    'templates_prepend_content' => '문서 뒤에 추가',\n\n    // Profile View\n    'profile_user_for_x' => ':time 전에 가입함',\n    'profile_created_content' => '활동한 이력',\n    'profile_not_created_pages' => ':userName(이)가 만든 문서 없음',\n    'profile_not_created_chapters' => ':userName(이)가 만든 챕터 없음',\n    'profile_not_created_books' => ':userName(이)가 만든 책 없음',\n    'profile_not_created_shelves' => ':userName(이)가 만든 책꽂이 없음',\n\n    // Comments\n    'comment' => '댓글',\n    'comments' => '댓글',\n    'comment_add' => '댓글 쓰기',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => '이곳에 댓글을 쓰세요...',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => '등록',\n    'comment_new' => '새 의견',\n    'comment_created' => '댓글 등록함 :createDiff',\n    'comment_updated' => ':username(이)가 댓글 수정함 :updateDiff',\n    'comment_updated_indicator' => '업데이트됨',\n    'comment_deleted_success' => '댓글 지움',\n    'comment_created_success' => '댓글 등록함',\n    'comment_updated_success' => '댓글 수정함',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => '이 댓글을 지울 건가요?',\n    'comment_in_reply_to' => ':commentId(을)를 향한 답글',\n    'comment_reference' => '참조',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => '이 페이지에 남겨진 댓글은 다음과 같습니다. 저장된 페이지를 볼 때 댓글을 추가하고 관리할 수 있습니다.',\n\n    // Revision\n    'revision_delete_confirm' => '이 수정본을 지울 건가요?',\n    'revision_restore_confirm' => '이 버전을 되돌릴 건가요? 현재 페이지는 대체됩니다.',\n    'revision_cannot_delete_latest' => '최신 수정본은 지울 수 없습니다.',\n\n    // Copy view\n    'copy_consider' => '항목을 복사할 때 다음을 고려하세요.',\n    'copy_consider_permissions' => '권한 설정은 복사되지 않습니다.',\n    'copy_consider_owner' => '복사한 항목의 소유자가 됩니다.',\n    'copy_consider_images' => '이미지 파일은 복사되지 않습니다. 올라가 있던 이미지가 사라지지 않습니다.',\n    'copy_consider_attachments' => '첨부 파일은 복사되지 않습니다.',\n    'copy_consider_access' => '경로, 소유자, 권한이 바뀌면 이 문서를 본 적 없는 사용자가 볼 수도 있습니다.',\n\n    // Conversions\n    'convert_to_shelf' => '책장으로 변환',\n    'convert_to_shelf_contents_desc' => '이 책을 동일한 내용의 새 서가로 변환할 수 있습니다. 이 책에 포함된 챕터는 새 책으로 변환됩니다. 이 책에 챕터에 포함되지 않은 페이지가 포함되어 있는 경우, 이 책의 이름이 변경되어 해당 페이지가 포함되며 이 책은 새 서가의 일부가 됩니다.',\n    'convert_to_shelf_permissions_desc' => '이 책에 설정된 모든 권한은 새 서가 및 자체 권한이 적용되지 않은 모든 새 책에 복사됩니다. 책에 대한 권한은 책에 대한 권한처럼 그 안의 콘텐츠로 자동 캐스케이드되지 않는다는 점에 유의하세요.',\n    'convert_book' => '책 변환',\n    'convert_book_confirm' => '이 책을 변환하시겠어요?',\n    'convert_undo_warning' => '이 작업은 되돌리기 어렵습니다.',\n    'convert_to_book' => '책으로 변환',\n    'convert_to_book_desc' => '이 챕터를 동일한 내용의 새 책으로 변환할 수 있습니다. 이 장에 설정된 모든 권한은 새 책에 복사되지만 상위 책에서 상속된 권한은 복사되지 않으므로 액세스 제어가 변경될 수 있습니다.',\n    'convert_chapter' => '챕터 변환',\n    'convert_chapter_confirm' => '이 챕터를 변환하시겠어요?',\n\n    // References\n    'references' => '참조',\n    'references_none' => '이 항목에 대한 추적된 참조가 없습니다.',\n    'references_to_desc' => '이 항목으로 연결되는 시스템에서 알려진 모든 콘텐츠가 아래에 나열되어 있습니다.',\n\n    // Watch Options\n    'watch' => '변경 알림 설정',\n    'watch_title_default' => '기본 설정',\n    'watch_desc_default' => '알림 설정을 기본값으로 되돌리기',\n    'watch_title_ignore' => '알림 설정 끄기',\n    'watch_desc_ignore' => '사용자 수준 환경설정의 알림을 포함한 모든 알림을 무시합니다.',\n    'watch_title_new' => '새로운 페이지',\n    'watch_desc_new' => '이 항목에 새 페이지가 생성되면 알림을 받습니다.',\n    'watch_title_updates' => '전체 페이지 업데이트',\n    'watch_desc_updates' => '모든 새 페이지와 페이지 변경 시 알림을 보냅니다.',\n    'watch_desc_updates_page' => '모든 페이지 변경 시 알림을 보냅니다.',\n    'watch_title_comments' => '모든 페이지 업데이트 및 댓글',\n    'watch_desc_comments' => '모든 새 페이지, 페이지 변경 및 새 댓글에 대해 알림을 보냅니다.',\n    'watch_desc_comments_page' => '페이지 변경 및 새 댓글이 있을 때 알림을 보냅니다.',\n    'watch_change_default' => '기본 알림 환경설정 변경하기',\n    'watch_detail_ignore' => '알림 무시하기',\n    'watch_detail_new' => '새 페이지 보기',\n    'watch_detail_updates' => '새 페이지 및 업데이트 보기',\n    'watch_detail_comments' => '새 페이지, 업데이트 및 댓글 보기',\n    'watch_detail_parent_book' => '상위 책을 통해 보기',\n    'watch_detail_parent_book_ignore' => '상위 책을 통한 무시하기',\n    'watch_detail_parent_chapter' => '상위 챕터를 통해 보기',\n    'watch_detail_parent_chapter_ignore' => '상위 챕터를 통해 무시하기',\n];\n"
  },
  {
    "path": "lang/ko/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => '요청된 페이지에 액세스할 수 있는 권한이 없습니다.',\n    'permissionJson' => '요청된 작업을 수행할 수 있는 권한이 없습니다.',\n\n    // Auth\n    'error_user_exists_different_creds' => '이메일 :email 이 이미 존재하지만 다른 자격 증명을 가진 사용자입니다.',\n    'auth_pre_register_theme_prevention' => '제공된 세부 정보로 사용자 계정을 등록할 수 없습니다',\n    'email_already_confirmed' => '이메일이 이미 확인되었으니 로그인해 보세요.',\n    'email_confirmation_invalid' => '이 확인 토큰이 유효하지 않거나 이미 사용되었습니다. 다시 등록해 주세요.',\n    'email_confirmation_expired' => '확인 토큰이 만료되었습니다. 새 확인 이메일이 전송되었습니다.',\n    'email_confirmation_awaiting' => '사용 중인 계정의 이메일 주소를 확인해야 합니다.',\n    'ldap_fail_anonymous' => '익명 바인딩을 사용하여 LDAP 액세스에 실패했습니다.',\n    'ldap_fail_authed' => '주어진 dn 및 암호 세부 정보를 사용하여 LDAP 액세스에 실패했습니다.',\n    'ldap_extension_not_installed' => 'LDAP PHP 확장이 설치되지 않았습니다.',\n    'ldap_cannot_connect' => 'LDAP 서버에 연결할 수 없음, 초기 연결 실패',\n    'saml_already_logged_in' => '이미 로그인했습니다.',\n    'saml_no_email_address' => '외부 인증 시스템에서 제공한 데이터에서 이 사용자의 이메일 주소를 찾을 수 없습니다.',\n    'saml_invalid_response_id' => '이 애플리케이션에서 시작한 프로세스에서 외부 인증 시스템의 요청을 인식하지 못합니다. 로그인 후 다시 이동하면 이 문제가 발생할 수 있습니다.',\n    'saml_fail_authed' => ':system 을 사용하여 로그인, 시스템이 성공적인 인증을 제공하지 않음',\n    'oidc_already_logged_in' => '이미 로그인했습니다.',\n    'oidc_no_email_address' => '외부 인증 시스템에서 제공한 데이터에서 이 사용자의 이메일 주소를 찾을 수 없습니다.',\n    'oidc_fail_authed' => ':system 을 사용하여 로그인, 시스템이 성공적인 인증을 제공하지 않음',\n    'social_no_action_defined' => '정의된 동작 없음',\n    'social_login_bad_response' => \":socialAccount 로 로그인 동안 에러가 발생했습니다: \\n:error\",\n    'social_account_in_use' => ':socialAccount(을)를 가진 사용자가 있습니다. :socialAccount 옵션을 통해 로그인해 보세요.',\n    'social_account_email_in_use' => '이메일 :email 은(는) 이미 사용 중입니다. 이미 계정이 있는 경우 프로필 설정에서 :socialAccount 계정을 연결할 수 있습니다.',\n    'social_account_existing' => '이 :socialAccount 는 이미 프로필에 연결되어 있습니다.',\n    'social_account_already_used_existing' => '이 :socialAccount 계정은 다른 사용자가 이미 사용하고 있습니다.',\n    'social_account_not_used' => '이 :socialAccount 계정은 어떤 사용자와도 연결되어 있지 않습니다. 프로필 설정에서 첨부하세요. ',\n    'social_account_register_instructions' => '아직 계정이 없는 경우 :socialAccount 옵션을 사용하여 계정을 등록할 수 있습니다.',\n    'social_driver_not_found' => '소셜 드라이버를 찾을 수 없습니다.',\n    'social_driver_not_configured' => '소셜 계정 :socialAccount 가(이) 올바르게 구성되지 않았습니다.',\n    'invite_token_expired' => '이 초대 링크가 만료되었습니다. 대신 계정 비밀번호 재설정을 시도해 보세요.',\n    'login_user_not_found' => '이 동작의 사용자를 찾을 수 없습니다.',\n\n    // System\n    'path_not_writable' => '파일 경로 :filePath 에 업로드할 수 없습니다. 서버에 저장이 가능한지 확인하세요.',\n    'cannot_get_image_from_url' => ':url 에서 이미지를 가져올 수 없습니다.',\n    'cannot_create_thumbs' => '서버에서 썸네일을 만들 수 없습니다. GD PHP 확장이 설치되어 있는지 확인하세요.',\n    'server_upload_limit' => '서버에서 이 크기의 업로드를 허용하지 않습니다. 더 작은 파일 크기를 시도해 보세요.',\n    'server_post_limit' => '서버가 제공된 데이터 양을 수신할 수 없습니다. 더 적은 데이터 또는 더 작은 파일로 다시 시도하세요.',\n    'uploaded'  => '서버에서 이 크기의 업로드를 허용하지 않습니다. 더 작은 파일 크기를 시도해 보세요.',\n\n    // Drawing & Images\n    'image_upload_error' => '이미지를 업로드하는 동안 오류가 발생했습니다.',\n    'image_upload_type_error' => '업로드 중인 이미지 유형이 유효하지 않습니다.',\n    'image_upload_replace_type' => '이미지 파일 교체는 반드시 동일한 유형이어야 합니다.',\n    'image_upload_memory_limit' => '시스템 리소스 제한으로 인해 이미지 업로드를 처리하거나 미리보기 이미지를 만들지 못했습니다.',\n    'image_thumbnail_memory_limit' => '시스템 리소스 제한으로 인해 이미지 크기 변형을 만들지 못했습니다.',\n    'image_gallery_thumbnail_memory_limit' => '시스템 리소스 제한으로 인해 갤러리 썸네일을 만들지 못했습니다.',\n    'drawing_data_not_found' => '드로잉 데이터를 로드할 수 없습니다. 드로잉 파일이 더 이상 존재하지 않거나 해당 파일에 액세스할 수 있는 권한이 없을 수 있습니다.',\n\n    // Attachments\n    'attachment_not_found' => '첨부 파일을 찾을 수 없습니다.',\n    'attachment_upload_error' => '첨부 파일을 업로드하는 동안 오류가 발생했습니다.',\n\n    // Pages\n    'page_draft_autosave_fail' => '초안을 저장하지 못했습니다. 이 페이지를 저장하기 전에 인터넷에 연결되어 있는지 확인하세요.',\n    'page_draft_delete_fail' => '페이지 초안을 삭제하고 현재 페이지에 저장된 콘텐츠를 가져오지 못했습니다.',\n    'page_custom_home_deletion' => '페이지가 홈페이지로 설정되어 있는 동안에는 삭제할 수 없습니다.',\n\n    // Entities\n    'entity_not_found' => '항목이 없습니다.',\n    'bookshelf_not_found' => '책장을 찾을 수 없음',\n    'book_not_found' => '책이 없습니다.',\n    'page_not_found' => '문서가 없습니다.',\n    'chapter_not_found' => '챕터가 없습니다.',\n    'selected_book_not_found' => '고른 책이 없습니다.',\n    'selected_book_chapter_not_found' => '고른 책이나 챕터가 없습니다.',\n    'guests_cannot_save_drafts' => 'Guest는 초안 문서를 보관할 수 없습니다.',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Admin을 삭제할 수 없습니다.',\n    'users_cannot_delete_guest' => 'Guest를 삭제할 수 없습니다.',\n    'users_could_not_send_invite' => '초대 이메일을 보내는 데 실패하여 사용자를 생성할 수 없습니다.',\n\n    // Roles\n    'role_cannot_be_edited' => '권한을 수정할 수 없습니다.',\n    'role_system_cannot_be_deleted' => '시스템 권한을 지울 수 없습니다.',\n    'role_registration_default_cannot_delete' => '가입한 사용자의 기본 권한을 지울 수 있어야 합니다.',\n    'role_cannot_remove_only_admin' => 'Admin을 가진 사용자가 적어도 한 명 있어야 합니다.',\n\n    // Comments\n    'comment_list' => '댓글을 가져오다 문제가 생겼습니다.',\n    'cannot_add_comment_to_draft' => '초안 문서에 댓글을 달 수 없습니다.',\n    'comment_add' => '댓글을 등록하다 문제가 생겼습니다.',\n    'comment_delete' => '댓글을 지우다 문제가 생겼습니다.',\n    'empty_comment' => '빈 댓글은 등록할 수 없습니다.',\n\n    // Error pages\n    '404_page_not_found' => '페이지를 찾을 수 없습니다.',\n    'sorry_page_not_found' => '문서를 못 찾았습니다.',\n    'sorry_page_not_found_permission_warning' => '문서를 볼 권한이 없습니다.',\n    'image_not_found' => '이미지를 찾을 수 없습니다',\n    'image_not_found_subtitle' => '이미지를 못 찾았습니다.',\n    'image_not_found_details' => '이미지가 지워졌을 수 있습니다.',\n    'return_home' => '처음으로 돌아가기',\n    'error_occurred' => '문제가 생겼습니다.',\n    'app_down' => ':appName에 문제가 생겼습니다.',\n    'back_soon' => '곧 돌아갑니다.',\n\n    // Import\n    'import_zip_cant_read' => 'ZIP 파일을 읽을 수 없습니다.',\n    'import_zip_cant_decode_data' => 'ZIP data.json 콘텐츠를 찾아서 디코딩할 수 없습니다.',\n    'import_zip_no_data' => '컨텐츠 ZIP 파일 데이터에 데이터가 비어있습니다.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => '컨텐츠 ZIP 파일을 가져오려다 실패했습니다. 이유:',\n    'import_zip_failed_notification' => '컨텐츠 ZIP 파일을 가져오지 못했습니다.',\n    'import_perms_books' => '책을 만드는 데 필요한 권한이 없습니다.',\n    'import_perms_chapters' => '챕터를 만드는 데 필요한 권한이 없습니다.',\n    'import_perms_pages' => '페이지를 만드는 데 필요한 권한이 없습니다.',\n    'import_perms_images' => '이미지를 만드는 데 필요한 권한이 없습니다.',\n    'import_perms_attachments' => '첨부 파일을 만드는 데 필요한 권한이 없습니다.',\n\n    // API errors\n    'api_no_authorization_found' => '요청에서 인증 토큰을 찾을 수 없습니다.',\n    'api_bad_authorization_format' => '요청에서 인증 토큰을 찾았으나 형식에 문제가 있습니다.',\n    'api_user_token_not_found' => '인증 토큰과 일치하는 API 토큰을 찾을 수 없습니다.',\n    'api_incorrect_token_secret' => 'API 토큰이 제공한 암호에 문제가 있습니다.',\n    'api_user_no_api_permission' => 'API 토큰의 소유자가 API를 호출할 권한이 없습니다.',\n    'api_user_token_expired' => '인증 토큰이 만료되었습니다.',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => '메일을 발송하는 도중 문제가 생겼습니다:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'URL이 구성된 허용된 SSR 호스트와 일치하지 않습니다.',\n];\n"
  },
  {
    "path": "lang/ko/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => '페이지에 새 댓글이 추가되었습니다: :pageName',\n    'new_comment_intro' => '사용자가 :appName: 의 페이지에 댓글을 달았습니다.',\n    'new_page_subject' => '새로운 페이지: :pageName',\n    'new_page_intro' => ':appName: 에 새 페이지가 생성되었습니다.',\n    'updated_page_subject' => '페이지 업데이트됨: :pageName',\n    'updated_page_intro' => ':appName: 에서 페이지가 업데이트되었습니다:',\n    'updated_page_debounce' => '알림이 한꺼번에 몰리는 것을 방지하기 위해 당분간 동일한 편집자가 이 페이지를 추가로 편집할 경우 알림이 전송되지 않습니다.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => '페이지 이름:',\n    'detail_page_path' => '페이지 경로:',\n    'detail_commenter' => '댓글 작성자:',\n    'detail_comment' => '댓글:',\n    'detail_created_by' => '작성자:',\n    'detail_updated_by' => '업데이트한 사람:',\n\n    'action_view_comment' => '댓글 보기',\n    'action_view_page' => '페이지 보기',\n\n    'footer_reason' => '이 알림이 전송된 이유는 :link 가 이 항목의 이러한 유형의 활동에 해당하기 때문입니다.',\n    'footer_reason_link' => '내 알림 환경설정',\n];\n"
  },
  {
    "path": "lang/ko/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; 이전',\n    'next'     => '다음 &raquo;',\n\n];\n"
  },
  {
    "path": "lang/ko/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => '최소 8글자 이상이어야 합니다.',\n    'user' => \"메일 주소를 가진 사용자가 없습니다.\",\n    'token' => '유효하지 않거나 만료된 토큰입니다.',\n    'sent' => '메일을 보냈습니다.',\n    'reset' => '패스워드가 초기화되었습니다.',\n\n];\n"
  },
  {
    "path": "lang/ko/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => '내 계정',\n\n    'shortcuts' => '단축키',\n    'shortcuts_interface' => 'UI 바로 가기 환경설정',\n    'shortcuts_toggle_desc' => '여기에서 탐색과 행동에 사용될 수 있는 키보드 단축키를 활성화하거나 비활성화할 수 있습니다.',\n    'shortcuts_customize_desc' => '아래에서 각 단축키를 사용자 지정할 수 있습니다. 바로가기에 대한 입력을 선택한 후 원하는 키 조합을 누르기만 하면 됩니다.',\n    'shortcuts_toggle_label' => '키보드 단축키가 활성화되었습니다.',\n    'shortcuts_section_navigation' => '탐색',\n    'shortcuts_section_actions' => '일반 작업',\n    'shortcuts_save' => '단축키 저장',\n    'shortcuts_overlay_desc' => '참고: 바로가기가 활성화된 경우 \"?\"를 누르면 현재 화면에 표시되는 작업에 대해 사용 가능한 바로가기를 강조 표시하는 도우미 오버레이를 사용할 수 있습니다.',\n    'shortcuts_update_success' => '단축키 설정이 수정되었습니다!',\n    'shortcuts_overview_desc' => '시스템 사용자 인터페이스를 탐색하는 데 사용할 수 있는 키보드 단축키를 관리합니다.',\n\n    'notifications' => '알림 환경설정',\n    'notifications_desc' => '시스템 내에서 특정 활동이 수행될 때 수신하는 이메일 알림을 제어합니다.',\n    'notifications_opt_own_page_changes' => '내가 소유한 페이지가 변경되면 알림 받기',\n    'notifications_opt_own_page_comments' => '내가 소유한 페이지에 댓글이 달렸을 때 알림 받기',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => '내 댓글에 대한 답글 알림 받기',\n    'notifications_save' => '환경설정 저장',\n    'notifications_update_success' => '알림 환경설정이 업데이트되었습니다!',\n    'notifications_watched' => '알람 설정 및 알람 무시한 항목',\n    'notifications_watched_desc' => '아래는 사용자 지정 알람 환경설정이 적용된 항목입니다. 이러한 항목에 대한 환경설정을 업데이트하려면 해당 항목을 본 다음 사이드바에서 알림 옵션을 찾습니다.',\n\n    'auth' => '액세스 및 보안',\n    'auth_change_password' => '패스워드 변경',\n    'auth_change_password_desc' => '애플리케이션에 로그인할 때 사용하는 패스워드를 변경합니다. 패스워드는 8자 이상이어야 합니다.',\n    'auth_change_password_success' => '패스워드가 업데이트되었습니다!',\n\n    'profile' => '프로필 세부 정보',\n    'profile_desc' => '커뮤니케이션 및 시스템 맞춤 설정에 사용되는 세부 정보 외에 다른 사용자에게 나를 나타내는 계정의 세부 정보를 관리합니다.',\n    'profile_view_public' => '공개 프로필 보기',\n    'profile_name_desc' => '내가 수행하는 활동과 내가 소유한 콘텐츠를 통해 시스템의 다른 사용자에게 표시되는 표시 이름을 구성합니다.',\n    'profile_email_desc' => '이 이메일은 알림 및 활성 시스템 인증에 따라 시스템 액세스에 사용됩니다.',\n    'profile_email_no_permission' => '안타깝게도 회원님에게는 이메일 주소를 변경할 수 있는 권한이 없습니다. 이메일 주소를 변경하려면 관리자에게 변경을 요청해야 합니다.',\n    'profile_avatar_desc' => '시스템에서 다른 사람들에게 자신을 나타내는 데 사용할 이미지를 선택합니다. 이 이미지는 256 x 256픽셀인 정사각형이 가장 이상적입니다.',\n    'profile_admin_options' => '관리자 옵션',\n    'profile_admin_options_desc' => '역할 할당 관리와 같은 추가 관리자 수준 옵션은 애플리케이션의 \"설정 > 사용자\" 영역에서 사용자 계정에 대해 찾을 수 있습니다.',\n\n    'delete_account' => '계정 삭제',\n    'delete_my_account' => '내 계정 삭제',\n    'delete_my_account_desc' => '이렇게 하면 시스템에서 사용자 계정이 완전히 삭제됩니다. 이 계정을 복구하거나 이 작업을 되돌릴 수 없습니다. 생성한 페이지와 업로드한 이미지 등 사용자가 만든 콘텐츠는 그대로 유지됩니다.',\n    'delete_my_account_warning' => '정말 계정을 삭제하시겠습니까?',\n];\n"
  },
  {
    "path": "lang/ko/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => '설정',\n    'settings_save' => '적용',\n    'system_version' => '시스템 버전',\n    'categories' => '카테고리',\n\n    // App Settings\n    'app_customization' => '어플리케이션 설정',\n    'app_features_security' => '기능 및 보안',\n    'app_name' => '애플리케이션 이름 (사이트 제목)',\n    'app_name_desc' => '이 이름은 헤더와 시스템에서 보낸 모든 이메일에 표시됩니다.',\n    'app_name_header' => '헤더에 이름 표시',\n    'app_public_access' => '공개 접근',\n    'app_public_access_desc' => '이 옵션을 활성화하면 로그인하지 않은 방문자가 BookStack 인스턴스의 내용에 접근할 수 있습니다.',\n    'app_public_access_desc_guest' => '일반 방문자의 액세스는 \"Guest\" 사용자를 통해 제어할 수 있습니다.',\n    'app_public_access_toggle' => '공개 액세스 허용',\n    'app_public_viewing' => '공개 열람을 허용할까요?',\n    'app_secure_images' => '보안을 강화하여 이미지 올려두기',\n    'app_secure_images_toggle' => '보안을 강화하여 이미지 올려두기 활성화',\n    'app_secure_images_desc' => '성능상의 이유로 모든 이미지는 공개됩니다. 이 옵션은 이미지 URL 앞에 추측하기 어려운 임의의 문자열을 추가합니다. 쉽게 액세스할 수 없도록 디렉토리 인덱스가 활성화되어 있지 않은지 확인하세요.',\n    'app_default_editor' => '기본 페이지 편집기',\n    'app_default_editor_desc' => '새 페이지를 편집할 때 기본으로 사용될 편집기를 선택합니다. 권한을 갖고 있다면 페이지마다 다르게 적용될 수 있습니다.',\n    'app_custom_html' => '사용자 지정 HTML 헤드 콘텐츠',\n    'app_custom_html_desc' => '여기에 추가된 모든 콘텐츠는 모든 페이지의 <head> 섹션 하단에 삽입됩니다. 스타일을 재정의하거나 분석 코드를 추가할 때 유용합니다.',\n    'app_custom_html_disabled_notice' => '이 설정 페이지에서는 사용자 지정 HTML 헤드 콘텐츠가 비활성화되어 변경 사항을 되돌릴 수 있습니다.',\n    'app_logo' => '애플리케이션 로고 (사이트 로고)',\n    'app_logo_desc' => '이 이미지는 애플리케이션 헤더 표시줄 등 여러 영역에서 사용됩니다. 이 이미지의 높이는 86픽셀이어야 합니다. 큰 이미지는 축소됩니다.',\n    'app_icon' => '애플리케이션 아이콘',\n    'app_icon_desc' => '이 아이콘은 브라우저 탭과 바로 가기 아이콘에 사용됩니다. 256픽셀의 정사각형 PNG 이미지여야 합니다.',\n    'app_homepage' => '애플리케이션 홈페이지',\n    'app_homepage_desc' => '기본 보기 대신 홈페이지에 표시할 보기를 선택합니다. 선택한 페이지에 대한 페이지 권한은 무시됩니다.',\n    'app_homepage_select' => '페이지를 선택하십시오',\n    'app_footer_links' => '바닥글 링크',\n    'app_footer_links_desc' => '사이트 바닥글에 표시할 링크를 추가합니다. 이 링크는 로그인이 필요 없는 페이지를 포함하여 대부분의 페이지 하단에 표시됩니다. \"trans::<key>\" 레이블을 사용하여 시스템 정의 번역을 사용할 수 있습니다. 예를 들면 다음과 같습니다: \"trans::common.privacy_policy\"를 사용하면 \"개인정보처리방침\"이라는 번역 텍스트가 제공되고 \"trans::common.terms_of_service\"를 사용하면 \"서비스 약관\"이라는 번역 텍스트가 제공됩니다.',\n    'app_footer_links_label' => '링크 레이블',\n    'app_footer_links_url' => '링크 URL',\n    'app_footer_links_add' => '바닥글 링크 추가',\n    'app_disable_comments' => '댓글 사용 안 함',\n    'app_disable_comments_toggle' => '댓글 사용 안 함',\n    'app_disable_comments_desc' => '애플리케이션의 모든 페이지에서 코멘트를 비활성화합니다. <br> 기존 댓글도 표시되지 않습니다.',\n\n    // Color settings\n    'color_scheme' => '애플리케이션 색상 구성표',\n    'color_scheme_desc' => '애플리케이션 사용자 인터페이스에서 사용할 색상을 설정합니다. 테마에 가장 잘 어울리고 가독성을 보장하기 위해 어두운 모드와 밝은 모드에 대해 색상을 별도로 구성할 수 있습니다.',\n    'ui_colors_desc' => '애플리케이션 기본 색상과 기본 링크 색상을 설정합니다. 기본 색상은 주로 헤더 배너, 버튼 및 인터페이스 장식에 사용됩니다. 기본 링크 색상은 작성된 콘텐츠와 애플리케이션 인터페이스 모두에서 텍스트 기반 링크 및 작업에 사용됩니다.',\n    'app_color' => '주 색상',\n    'link_color' => '기본 링크 색상',\n    'content_colors_desc' => '페이지 구성 계층 구조의 모든 요소에 대한 색상을 설정합니다. 가독성을 위해 기본 색상과 비슷한 밝기의 색상을 선택하는 것이 좋습니다.',\n    'bookshelf_color' => '책장 색상',\n    'book_color' => '책 색상',\n    'chapter_color' => '챕터 색상',\n    'page_color' => '페이지 색상',\n    'page_draft_color' => '초안 문서 색상',\n\n    // Registration Settings\n    'reg_settings' => '가입',\n    'reg_enable' => '가입 활성화',\n    'reg_enable_toggle' => '가입 허용',\n    'reg_enable_desc' => '가입한 사용자는 한 가지 권한을 가집니다.',\n    'reg_default_role' => '기본 권한',\n    'reg_enable_external_warning' => '외부 시스템이 LDAP나 SAML 인증이 활성화되어 있다면 설정과 관계없이 인증을 성공할 때 없는 계정을 만듭니다.',\n    'reg_email_confirmation' => '메일 주소 확인',\n    'reg_email_confirmation_toggle' => '메일 주소 확인',\n    'reg_confirm_email_desc' => '도메인 제한을 활성화하면 설정과 관계없이 메일 주소 확인이 필요합니다.',\n    'reg_confirm_restrict_domain' => '도메인 제한',\n    'reg_confirm_restrict_domain_desc' => '가입을 차단할 도메인을 쉼표로 구분하여 입력하세요. 사용자가 메일 주소 확인에 성공하면 메일 주소를 바꿀 수 있습니다.',\n    'reg_confirm_restrict_domain_placeholder' => '차단한 도메인 없음',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => '새로운 책에 적용할 기본 정렬 규칙을 선택하세요. 이 선택은 기존 책에는 영향을 주지 않고, 기존 책의 설정은 책마다 변경할 수 있습니다.',\n    'sorting_rules' => '정렬 규칙',\n    'sorting_rules_desc' => '현재 시스템에 미리 정의된 정렬 규칙의 목록입니다.',\n    'sort_rule_assigned_to_x_books' => ':count 책에 정렬 규칙 적용',\n    'sort_rule_create' => '정렬 규칙 생성하기',\n    'sort_rule_edit' => '정렬 규칙 수정하기',\n    'sort_rule_delete' => '정렬 규칙 삭제하기',\n    'sort_rule_delete_desc' => '시스템에서 이 정렬 규칙을 삭제합니다. 이 정렬 규칙을 사용하는 책들은 수동 정렬 사용으로 변경됩니다.',\n    'sort_rule_delete_warn_books' => '이 정렬 규칙은 현재 :count 권의 책에 적용되고 있습니다. 이 정렬 규칙을 정말 삭제하시겠습니까?',\n    'sort_rule_delete_warn_default' => '이 정렬 규칙은 현재 책들에 기본으로 적용되고 있습니다. 정말 삭제하시겠습니까?',\n    'sort_rule_details' => '정렬 규칙 세부사항',\n    'sort_rule_details_desc' => '이 정렬 규칙의 이름을 지어주세요. 이 이름은 사용자가 정렬할 때 나타납니다.',\n    'sort_rule_operations' => '정렬 규칙',\n    'sort_rule_operations_desc' => '사용 가능한 작업 목록에서 이동하여 수행할 정렬 작업을 구성합니다. 사용 시 위에서 아래로 순서대로 작업이 적용됩니다. 여기에서 변경한 내용은 저장 시 할당된 모든 책에 적용됩니다.',\n    'sort_rule_available_operations' => '사용 가능한 정렬 규칙',\n    'sort_rule_available_operations_empty' => '사용 가능한 정렬 규칙 없음',\n    'sort_rule_configured_operations' => '정렬 규칙 설정',\n    'sort_rule_configured_operations_empty' => '\"사용 가능한 정렬 규칙\"에서 추가하기',\n    'sort_rule_op_asc' => '(오름차순)',\n    'sort_rule_op_desc' => '(내림차순)',\n    'sort_rule_op_name' => '이름순 - 가나다',\n    'sort_rule_op_name_numeric' => '이름순 - 숫자',\n    'sort_rule_op_created_date' => '생성일',\n    'sort_rule_op_updated_date' => '수정일',\n    'sort_rule_op_chapters_first' => '챕터 우선 정렬',\n    'sort_rule_op_chapters_last' => '챕터 나중 정렬',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => '유지관리',\n    'maint_image_cleanup' => '이미지 정리',\n    'maint_image_cleanup_desc' => '중복인 이미지를 찾습니다. 실행하기 전에 이미지를 백업하세요.',\n    'maint_delete_images_only_in_revisions' => '지난 버전에만 있는 이미지 지우기',\n    'maint_image_cleanup_run' => '실행',\n    'maint_image_cleanup_warning' => '이미지 :count개를 지우시겠습니까?',\n    'maint_image_cleanup_success' => '이미지 :count개 삭제함',\n    'maint_image_cleanup_nothing_found' => '삭제한 것 없음',\n    'maint_send_test_email' => '테스트 메일 보내기',\n    'maint_send_test_email_desc' => '메일 주소로 테스트 메일을 전송합니다.',\n    'maint_send_test_email_run' => '테스트 메일 보내기',\n    'maint_send_test_email_success' => ':address 계정으로 이메일을 보냈습니다.',\n    'maint_send_test_email_mail_subject' => '테스트 메일',\n    'maint_send_test_email_mail_greeting' => '메일을 수신했습니다.',\n    'maint_send_test_email_mail_text' => '메일을 정상적으로 수신했습니다.',\n    'maint_recycle_bin_desc' => '지워진 콘텐츠는 휴지통에 들어가 복원하거나 영구 삭제할 수 있습니다. 오래된 항목은 자동으로 지워집니다.',\n    'maint_recycle_bin_open' => '휴지통 열기',\n    'maint_regen_references' => '참조 재생성',\n    'maint_regen_references_desc' => '이 작업은 데이터베이스 내에서 항목 간 참조 색인을 다시 생성합니다. 이 작업은 일반적으로 자동으로 처리되지만 오래된 콘텐츠나 비공식적인 방법으로 추가된 콘텐츠의 색인을 생성하는 데 유용할 수 있습니다.',\n    'maint_regen_references_success' => '참조 색인이 다시 생성되었습니다!',\n    'maint_timeout_command_note' => '참고: 이 작업을 실행하는 데 시간이 걸릴 수 있으며, 일부 웹 환경에서는 시간 초과 문제가 발생할 수 있습니다. 대신 터미널 명령을 사용하여 이 작업을 수행할 수 있습니다.',\n\n    // Recycle Bin\n    'recycle_bin' => '휴지통',\n    'recycle_bin_desc' => '항목을 복원하거나 영구 삭제할 수 있습니다. 권한 필터가 작동하지 않습니다.',\n    'recycle_bin_deleted_item' => '삭제한 항목',\n    'recycle_bin_deleted_parent' => '부모 항목',\n    'recycle_bin_deleted_by' => '삭제한 유저',\n    'recycle_bin_deleted_at' => '삭제한 시간',\n    'recycle_bin_permanently_delete' => '영구 삭제',\n    'recycle_bin_restore' => '복원',\n    'recycle_bin_contents_empty' => '휴지통이 비었습니다.',\n    'recycle_bin_empty' => '비우기',\n    'recycle_bin_empty_confirm' => '이렇게 하면 각 항목에 포함된 콘텐츠를 포함하여 휴지통에 있는 모든 항목이 영구적으로 삭제됩니다. 휴지통을 비우시겠습니까?',\n    'recycle_bin_destroy_confirm' => '이 작업을 수행하면 이 항목이 아래에 나열된 모든 하위 요소와 함께 시스템에서 영구적으로 삭제되며, 복원할 수 없습니다. 이 항목을 영구 삭제하시겠어요?',\n    'recycle_bin_destroy_list' => '영구 삭제함',\n    'recycle_bin_restore_list' => '복원함',\n    'recycle_bin_restore_confirm' => '원래 위치로 복원합니다. 원래 위치의 부모 항목이 지워졌을 경우 부모 항목도 복원해야 합니다.',\n    'recycle_bin_restore_deleted_parent' => '이 항목의 부모 항목이 지워졌습니다. 부모 항목을 먼저 복원하세요.',\n    'recycle_bin_restore_parent' => '부모 항목 복원',\n    'recycle_bin_destroy_notification' => ':count항목 삭제함',\n    'recycle_bin_restore_notification' => ':count항목 복원함',\n\n    // Audit Log\n    'audit' => '활동 기록',\n    'audit_desc' => '이 활동 로그는 시스템에서 추적된 활동 목록을 표시합니다. 이 목록은 권한 필터가 적용되는 시스템의 유사한 활동 목록과 달리 필터링되지 않습니다.',\n    'audit_event_filter' => '이벤트 필터',\n    'audit_event_filter_no_filter' => '필터 없음',\n    'audit_deleted_item' => '삭제한 항목',\n    'audit_deleted_item_name' => '이름: :name',\n    'audit_table_user' => '이용자',\n    'audit_table_event' => '이벤트',\n    'audit_table_related' => '관련 항목 또는 세부 사항',\n    'audit_table_ip' => 'IP 주소',\n    'audit_table_date' => '활동 기간',\n    'audit_date_from' => 'From',\n    'audit_date_to' => 'To',\n\n    // Role Settings\n    'roles' => '역할',\n    'role_user_roles' => '이용자 역할',\n    'roles_index_desc' => '역할은 사용자를 그룹화하고 구성원에게 시스템 권한을 제공하기 위해 사용됩니다. 사용자가 여러 역할의 구성원인 경우 부여된 권한이 중첩되며 모든 권한을 상속받게 됩니다.',\n    'roles_x_users_assigned' => ':count 명의 사용자가 할당됨|:count 명의 사용자가 할당됨',\n    'roles_x_permissions_provided' => ':count 개의 권한|:count 개의 권한',\n    'roles_assigned_users' => '할당된 사용자',\n    'roles_permissions_provided' => '제공된 권한',\n    'role_create' => '권한 만들기',\n    'role_delete' => '권한 제거',\n    'role_delete_confirm' => ':roleName(을)를 지웁니다.',\n    'role_delete_users_assigned' => '이 권한을 가진 사용자 :userCount명에 할당할 권한을 고르세요.',\n    'role_delete_no_migration' => \"할당하지 않음\",\n    'role_delete_sure' => '이 권한을 지울 건가요?',\n    'role_edit' => '역할 수정',\n    'role_details' => '역할 정보',\n    'role_name' => '역할 이름',\n    'role_desc' => '역할 설명',\n    'role_mfa_enforced' => '다중 인증 필요',\n    'role_external_auth_id' => '외부 인증 계정',\n    'role_system' => '시스템 권한',\n    'role_manage_users' => '이용자 관리하기',\n    'role_manage_roles' => '권한 관리',\n    'role_manage_entity_permissions' => '문서별 권한 관리',\n    'role_manage_own_entity_permissions' => '직접 만든 문서별 권한 관리',\n    'role_manage_page_templates' => '템플릿 관리',\n    'role_access_api' => '시스템 접근 API',\n    'role_manage_settings' => '사이트 설정 관리',\n    'role_export_content' => '항목 내보내기',\n    'role_import_content' => '내용 가져오기',\n    'role_editor_change' => '페이지 편집기 변경',\n    'role_notifications' => '알림 수신 및 관리',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => '권한 항목',\n    'roles_system_warning' => '위 세 권한은 자신의 권한이나 다른 유저의 권한을 바꿀 수 있습니다.',\n    'role_asset_desc' => '책, 챕터, 문서별 권한은 이 설정에 우선합니다.',\n    'role_asset_admins' => '관리자 권한은 어디든 접근할 수 있지만 이 설정은 사용자 인터페이스에서 해당 활동을 표시할지 결정합니다.',\n    'role_asset_image_view_note' => '이는 이미지 관리자 내 가시성과 관련이 있습니다. 업로드된 이미지 파일의 실제 접근은 시스템의 이미지 저장 설정에 따라 달라집니다.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => '모든 항목',\n    'role_own' => '직접 만든 항목',\n    'role_controlled_by_asset' => '저마다 다름',\n    'role_save' => '저장',\n    'role_users' => '이 역할을 가진 사용자들',\n    'role_users_none' => '역할이 부여된 사용자가 없습니다.',\n\n    // Users\n    'users' => '사용자',\n    'users_index_desc' => '시스템 내에서 개별 사용자 계정을 생성하고 관리합니다. 사용자 계정은 로그인과 콘텐츠 및 활동의 속성에 사용됩니다. 접근 권한은 주로 역할 기반이지만 사용자 콘텐츠 소유권도 권한 및 접근에 영향을 줄 수 있습니다.',\n    'user_profile' => '사용자 프로필',\n    'users_add_new' => '사용자 만들기',\n    'users_search' => '사용자 검색',\n    'users_latest_activity' => '최근 활동',\n    'users_details' => '사용자 정보',\n    'users_details_desc' => '메일 주소로 로그인합니다.',\n    'users_details_desc_no_email' => '사용자 이름을 바꿉니다.',\n    'users_role' => '사용자 권한',\n    'users_role_desc' => '이 사용자에게 할당될 역할을 선택합니다. 사용자가 여러 역할에 할당된 경우 해당 역할의 권한이 겹쳐서 적용됩니다. 할당된 역할의 모든 기능을 받게 됩니다.',\n    'users_password' => '사용자 패스워드',\n    'users_password_desc' => '패스워드는 8 글자를 넘어야 합니다.',\n    'users_send_invite_text' => '패스워드 설정을 권유하는 메일을 보내거나 내가 정할 수 있습니다.',\n    'users_send_invite_option' => '메일 보내기',\n    'users_external_auth_id' => '외부 인증 계정',\n    'users_external_auth_id_desc' => '외부 인증 시스템(예: SAML2, OIDC 또는 LDAP)을 사용 중인 경우 이 BookStack 사용자를 인증 시스템 계정에 연결하는 ID입니다. 기본 이메일 기반 인증을 사용하는 경우 이 필드를 무시할 수 있습니다.',\n    'users_password_warning' => '이 사용자의 비밀번호를 변경하려는 경우에만 아래 내용을 입력하세요.',\n    'users_system_public' => '계정 없는 모든 사용자에 할당한 사용자입니다. 이 사용자로 로그인할 수 없어요.',\n    'users_delete' => '사용자 삭제',\n    'users_delete_named' => ':userName 삭제',\n    'users_delete_warning' => ':userName에 관한 데이터를 지웁니다.',\n    'users_delete_confirm' => '이 사용자를 삭제하시겠습니까?',\n    'users_migrate_ownership' => '소유자 바꾸기',\n    'users_migrate_ownership_desc' => '선택한 사용자가 소유하고 있는 모든 항목을 다른 유저가 소유하게 합니다.',\n    'users_none_selected' => '선택한 유저 없음',\n    'users_edit' => '사용자 수정',\n    'users_edit_profile' => '프로필 바꾸기',\n    'users_avatar' => '프로필 이미지',\n    'users_avatar_desc' => '이미지 규격은 256x256px 내외입니다.',\n    'users_preferred_language' => '언어',\n    'users_preferred_language_desc' => '문서 내용에는 아무런 영향을 주지 않습니다.',\n    'users_social_accounts' => '소셜 계정',\n    'users_social_accounts_desc' => '이 사용자에 대해 연결된 소셜 계정의 상태를 봅니다. 소셜 계정은 시스템 액세스를 위한 기본 인증 시스템과 함께 사용할 수 있습니다.',\n    'users_social_accounts_info' => '다른 계정으로 간단하게 로그인하세요. 여기에서 계정 연결을 끊는 것과 소셜 계정에서 접근 권한을 취소하는 것은 다릅니다.',\n    'users_social_connect' => '계정 연결',\n    'users_social_disconnect' => '계정 연결 끊기',\n    'users_social_status_connected' => '연결되었습니다.',\n    'users_social_status_disconnected' => '연결 해제되었습니다.',\n    'users_social_connected' => ':socialAccount(와)과 연결했습니다.',\n    'users_social_disconnected' => ':socialAccount(와)과의 연결을 끊었습니다.',\n    'users_api_tokens' => 'API 토큰',\n    'users_api_tokens_desc' => 'BookStack REST API로 인증하는 데 사용되는 액세스 토큰을 생성하고 관리합니다. API에 대한 권한은 토큰이 속한 사용자를 통해 관리됩니다.',\n    'users_api_tokens_none' => '이 사용자를 위해 생성된 API 토큰이 없습니다.',\n    'users_api_tokens_create' => '토큰 만들기',\n    'users_api_tokens_expires' => '만료',\n    'users_api_tokens_docs' => 'API 설명서',\n    'users_mfa' => '다중 인증',\n    'users_mfa_desc' => '추가 보안 계층으로 다중 인증을 설정합니다.',\n    'users_mfa_x_methods' => ':count 설정함|:count 설정함',\n    'users_mfa_configure' => '설정',\n\n    // API Tokens\n    'user_api_token_create' => 'API 토큰 만들기',\n    'user_api_token_name' => '이름',\n    'user_api_token_name_desc' => '알아볼 수 있는 이름을 줍니다.',\n    'user_api_token_expiry' => '만료일',\n    'user_api_token_expiry_desc' => '이 날짜 이후에 이 토큰이 만든 요청은 작동하지 않습니다. 공백은 만료일을 100년 후로 둡니다.',\n    'user_api_token_create_secret_message' => '토큰을 만든 직후 \"Token ID\"와 \"Token Secret\"이 한 번만 표시되므로 안전한 장소에 보관하세요.',\n    'user_api_token' => 'API 토큰',\n    'user_api_token_id' => '토큰 ID',\n    'user_api_token_id_desc' => '토큰이 API 요청 시 제공해야 할 식별자입니다. 편집 불가능한 시스템이 생성합니다.',\n    'user_api_token_secret' => '토큰 암호',\n    'user_api_token_secret_desc' => '토큰이 API 요청 시 제공해야 할 암호입니다. 한 번만 표시되므로 안전한 장소에 보관하세요.',\n    'user_api_token_created' => ':timeAgo 전에 토큰 생성함',\n    'user_api_token_updated' => ':timeAgo 전에 토큰 갱신함',\n    'user_api_token_delete' => '토큰 삭제',\n    'user_api_token_delete_warning' => '\\':tokenName\\'을 시스템에서 삭제합니다.',\n    'user_api_token_delete_confirm' => '이 API 토큰을 삭제하시겠습니까?',\n\n    // Webhooks\n    'webhooks' => '웹 훅',\n    'webhooks_index_desc' => '웹훅은 시스템 내에서 특정 작업 및 이벤트가 발생할 때 외부 URL로 데이터를 전송하는 방법으로, 메시징 또는 알림 시스템과 같은 외부 플랫폼과 이벤트 기반 통합을 가능하게 합니다.',\n    'webhooks_x_trigger_events' => ':count 개의 이벤트 트리거|:count 개의 이벤트 트리거',\n    'webhooks_create' => '웹 훅 만들기',\n    'webhooks_none_created' => '웹 훅이 없습니다.',\n    'webhooks_edit' => '웹 훅 수정',\n    'webhooks_save' => '웹 훅 저장',\n    'webhooks_details' => '설명',\n    'webhooks_details_desc' => '보낼 웹 훅 데이터에 대한 웹 훅 이름과 POST 엔드포인트 경로를 제공합니다.',\n    'webhooks_events' => '이벤트',\n    'webhooks_events_desc' => '웹 훅 호출을 트리거할 이벤트를 모두 고르세요.',\n    'webhooks_events_warning' => '설정한 권한과 관계없이 모든 선택한 이벤트를 트리거합니다. 보안에 유의하세요.',\n    'webhooks_events_all' => '모든 시스템 이벤트',\n    'webhooks_name' => '웹 훅 이름',\n    'webhooks_timeout' => '요청 시간 제한 (초)',\n    'webhooks_endpoint' => '웹 훅 엔드포인트',\n    'webhooks_active' => '웹 훅 활성',\n    'webhook_events_table_header' => '이벤트',\n    'webhooks_delete' => '웹 훅 삭제',\n    'webhooks_delete_warning' => '\\':webhookName\\'을 시스템에서 지웁니다.',\n    'webhooks_delete_confirm' => '이 웹 훅을 지울 건가요?',\n    'webhooks_format_example' => '웹 훅 포맷 예시',\n    'webhooks_format_example_desc' => '웹 훅 데이터를 아래 형식에 따라 설정된 엔드포인트에 JSON POST로 전송합니다. 이벤트 유형에 따라 \"related_item\"과 \"url\"을 쓸 수 있습니다.',\n    'webhooks_status' => '웹 훅 상태',\n    'webhooks_last_called' => '마지막 호출:',\n    'webhooks_last_errored' => '마지막 에러:',\n    'webhooks_last_error_message' => '마지막 에러 메시지:',\n\n    // Licensing\n    'licenses' => '라이선스',\n    'licenses_desc' => '이 페이지에서는 BookStack 내에서 사용되는 프로젝트 및 라이브러리 외에 BookStack 라이선스 정보를 자세히 설명합니다. 나열된 프로젝트는 개발 용도로만 사용할 수 있습니다.',\n    'licenses_bookstack' => 'BookStack 라이선스',\n    'licenses_php' => 'PHP 라이브러리 라이선스',\n    'licenses_js' => 'JavaScript 라이브러리 라이선스',\n    'licenses_other' => '기타 라이선스',\n    'license_details' => '라이선스 세부 사항',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => '히브리어',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/ko/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute(을)를 허용하세요.',\n    'active_url'           => ':attribute이 유효한 URL이 아닙니다.',\n    'after'                => ':attribute(을)를 :date 후로 설정하세요.',\n    'alpha'                => ':attribute(을)를 문자로만 구성하세요.',\n    'alpha_dash'           => ':attribute(을)를 문자, 숫자, -, _로만 구성하세요.',\n    'alpha_num'            => ':attribute(을)를 문자, 숫자로만 구성하세요.',\n    'array'                => ':attribute(을)를 배열로 구성하세요.',\n    'backup_codes'         => '유효하지 않거나 사용 중인 코드입니다.',\n    'before'               => ':attribute(을)를 :date 전으로 설정하세요.',\n    'between'              => [\n        'numeric' => ':attribute(을)를 :min~:max(으)로 구성하세요.',\n        'file'    => ':attribute(을)를 :min~:max킬로바이트로 구성하세요.',\n        'string'  => ':attribute(을)를 :min~:max바이트로 구성하세요.',\n        'array'   => ':attribute(을)를 :min~:max개로 구성하세요.',\n    ],\n    'boolean'              => ':attribute(을)를 true나 false로만 구성하세요.',\n    'confirmed'            => ':attribute(와)과 다릅니다.',\n    'date'                 => ':attribute(을)를 유효한 날짜로 구성하세요.',\n    'date_format'          => ':attribute(은)는 :format(와)과 다릅니다.',\n    'different'            => ':attribute(와)과 :other(을)를 다르게 구성하세요.',\n    'digits'               => ':attribute(을)를 :digits자리로 구성하세요.',\n    'digits_between'       => ':attribute(을)를 :min~:max자리로 구성하세요.',\n    'email'                => ':attribute(을)를 유효한 메일 주소로 구성하세요.',\n    'ends_with' => ':attribute(을)를 :values(으)로 끝나게 구성하세요.',\n    'file'                 => ':attribute(을)를 유효한 파일로 설정하세요.',\n    'filled'               => ':attribute(을)를 구성하세요.',\n    'gt'                   => [\n        'numeric' => ':attribute(을)를 :value(이)가 넘게 구성하세요.',\n        'file'    => ':attribute(을)를 :value킬로바이트가 넘게 구성하세요.',\n        'string'  => ':attribute(을)를 :value바이트가 넘게 구성하세요.',\n        'array'   => ':attribute(을)를 :value개가 넘게 구성하세요.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute(을)를 적어도 :value(으)로 구성하세요.',\n        'file'    => ':attribute(을)를 적어도 :value킬로바이트로 구성하세요.',\n        'string'  => ':attribute(을)를 적어도 :value바이트로 구성하세요.',\n        'array'   => ':attribute(을)를 적어도 :value개로 구성하세요..',\n    ],\n    'exists'               => '고른 :attribute(이)가 유효하지 않습니다.',\n    'image'                => ':attribute(을)를 이미지로 구성하세요.',\n    'image_extension'      => ':attribute(을)를 유효한 이미지 확장자로 구성하세요.',\n    'in'                   => '고른 :attribute(이)가 유효하지 않습니다.',\n    'integer'              => ':attribute(을)를 정수로 구성하세요.',\n    'ip'                   => ':attribute(을)를 유효한 IP 주소로 구성하세요.',\n    'ipv4'                 => ':attribute(을)를 유효한 IPv4 주소로 구성하세요.',\n    'ipv6'                 => ':attribute(을)를 유효한 IPv6 주소로 구성하세요.',\n    'json'                 => ':attribute(을)를 유효한 JSON으로 구성하세요.',\n    'lt'                   => [\n        'numeric' => ':attribute(을)를 :value(이)가 안 되게 구성하세요.',\n        'file'    => ':attribute(을)를 :value킬로바이트가 안 되게 구성하세요.',\n        'string'  => ':attribute(을)를 :value바이트가 안 되게 구성하세요.',\n        'array'   => ':attribute(을)를 :value개가 안 되게 구성하세요.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute(을)를 많아야 :max(으)로 구성하세요.',\n        'file'    => ':attribute(을)를 많아야 :max킬로바이트로 구성하세요.',\n        'string'  => ':attribute(을)를 많아야 :max바이트로 구성하세요.',\n        'array'   => ':attribute(을)를 많아야 :max개로 구성하세요.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute(을)를 많아야 :max(으)로 구성하세요.',\n        'file'    => ':attribute(을)를 많아야 :max킬로바이트로 구성하세요.',\n        'string'  => ':attribute(을)를 많아야 :max바이트로 구성하세요.',\n        'array'   => ':attribute(을)를 많아야 :max개로 구성하세요.',\n    ],\n    'mimes'                => ':attribute(을)를 :values 형식으로 구성하세요.',\n    'min'                  => [\n        'numeric' => ':attribute(을)를 적어도 :value(으)로 구성하세요.',\n        'file'    => ':attribute(을)를 적어도 :value킬로바이트로 구성하세요.',\n        'string'  => ':attribute(을)를 적어도 :value바이트로 구성하세요.',\n        'array'   => ':attribute(을)를 적어도 :value개로 구성하세요..',\n    ],\n    'not_in'               => '고른 :attribute(이)가 유효하지 않습니다.',\n    'not_regex'            => ':attribute(은)는 유효하지 않은 형식입니다.',\n    'numeric'              => ':attribute(을)를 숫자로만 구성하세요.',\n    'regex'                => ':attribute(은)는 유효하지 않은 형식입니다.',\n    'required'             => ':attribute(을)를 구성하세요.',\n    'required_if'          => ':other(이)가 :value일 때 :attribute(을)를 구성해야 합니다.',\n    'required_with'        => ':values(이)가 있을 때 :attribute(을)를 구성해야 합니다.',\n    'required_with_all'    => ':values(이)가 모두 있을 때 :attribute(을)를 구성해야 합니다.',\n    'required_without'     => ':values(이)가 없을 때 :attribute(을)를 구성해야 합니다.',\n    'required_without_all' => ':values(이)가 모두 없을 때 :attribute(을)를 구성해야 합니다.',\n    'same'                 => ':attribute(와)과 :other(을)를 똑같이 구성하세요.',\n    'safe_url'             => '안전하지 않은 URL입니다.',\n    'size'                 => [\n        'numeric' => ':attribute(을)를 :size(으)로 구성하세요.',\n        'file'    => ':attribute(을)를 :size킬로바이트로 구성하세요.',\n        'string'  => ':attribute(을)를 :size바이트로 구성하세요.',\n        'array'   => ':attribute(을)를 :size개로 구성하세요..',\n    ],\n    'string'               => ':attribute(을)를 문자로 구성하세요.',\n    'timezone'             => ':attribute(을)를 유효한 시간대로 구성하세요.',\n    'totp'                 => '유효하지 않거나 만료된 코드입니다.',\n    'unique'               => ':attribute(은)는 이미 있습니다.',\n    'url'                  => ':attribute(은)는 유효하지 않은 형식입니다.',\n    'uploaded'             => '파일 크기가 서버에서 허용하는 수치를 넘습니다.',\n\n    'zip_file' => ':attribute은(는) 컨텐츠 ZIP 파일 내의 객체 유형에 대해 고유해야 합니다.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => ':attribute은(는)  :validTypes, found :foundType 유형의 파일을 참조해야 합니다.',\n    'zip_model_expected' => '데이터 객체가 필요하지만 \":type\" 타입이 발견되었습니다.',\n    'zip_unique' => ':attribute은(는) 컨텐츠 ZIP 파일 내의 객체 유형에 대해 고유해야 합니다.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => '같은 패스워드를 다시 입력하세요.',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/ku/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'created page',\n    'page_create_notification'    => 'Page successfully created',\n    'page_update'                 => 'updated page',\n    'page_update_notification'    => 'Page successfully updated',\n    'page_delete'                 => 'deleted page',\n    'page_delete_notification'    => 'Page successfully deleted',\n    'page_restore'                => 'restored page',\n    'page_restore_notification'   => 'Page successfully restored',\n    'page_move'                   => 'moved page',\n    'page_move_notification'      => 'Page successfully moved',\n\n    // Chapters\n    'chapter_create'              => 'created chapter',\n    'chapter_create_notification' => 'Chapter successfully created',\n    'chapter_update'              => 'updated chapter',\n    'chapter_update_notification' => 'Chapter successfully updated',\n    'chapter_delete'              => 'deleted chapter',\n    'chapter_delete_notification' => 'Chapter successfully deleted',\n    'chapter_move'                => 'moved chapter',\n    'chapter_move_notification' => 'Chapter successfully moved',\n\n    // Books\n    'book_create'                 => 'created book',\n    'book_create_notification'    => 'Book successfully created',\n    'book_create_from_chapter'              => 'converted chapter to book',\n    'book_create_from_chapter_notification' => 'Chapter successfully converted to a book',\n    'book_update'                 => 'updated book',\n    'book_update_notification'    => 'Book successfully updated',\n    'book_delete'                 => 'deleted book',\n    'book_delete_notification'    => 'Book successfully deleted',\n    'book_sort'                   => 'sorted book',\n    'book_sort_notification'      => 'Book successfully re-sorted',\n\n    // Bookshelves\n    'bookshelf_create'            => 'created shelf',\n    'bookshelf_create_notification'    => 'Shelf successfully created',\n    'bookshelf_create_from_book'    => 'converted book to shelf',\n    'bookshelf_create_from_book_notification'    => 'Book successfully converted to a shelf',\n    'bookshelf_update'                 => 'updated shelf',\n    'bookshelf_update_notification'    => 'Shelf successfully updated',\n    'bookshelf_delete'                 => 'deleted shelf',\n    'bookshelf_delete_notification'    => 'Shelf successfully deleted',\n\n    // Revisions\n    'revision_restore' => 'restored revision',\n    'revision_delete' => 'deleted revision',\n    'revision_delete_notification' => 'Revision successfully deleted',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" has been added to your favourites',\n    'favourite_remove_notification' => '\":name\" has been removed from your favourites',\n\n    // Watching\n    'watch_update_level_notification' => 'Watch preferences successfully updated',\n\n    // Auth\n    'auth_login' => 'logged in',\n    'auth_register' => 'registered as new user',\n    'auth_password_reset_request' => 'requested user password reset',\n    'auth_password_reset_update' => 'reset user password',\n    'mfa_setup_method' => 'configured MFA method',\n    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',\n    'mfa_remove_method' => 'removed MFA method',\n    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',\n\n    // Settings\n    'settings_update' => 'updated settings',\n    'settings_update_notification' => 'Settings successfully updated',\n    'maintenance_action_run' => 'ran maintenance action',\n\n    // Webhooks\n    'webhook_create' => 'created webhook',\n    'webhook_create_notification' => 'Webhook successfully created',\n    'webhook_update' => 'updated webhook',\n    'webhook_update_notification' => 'Webhook successfully updated',\n    'webhook_delete' => 'deleted webhook',\n    'webhook_delete_notification' => 'Webhook successfully deleted',\n\n    // Imports\n    'import_create' => 'created import',\n    'import_create_notification' => 'Import successfully uploaded',\n    'import_run' => 'updated import',\n    'import_run_notification' => 'Content successfully imported',\n    'import_delete' => 'deleted import',\n    'import_delete_notification' => 'Import successfully deleted',\n\n    // Users\n    'user_create' => 'created user',\n    'user_create_notification' => 'User successfully created',\n    'user_update' => 'updated user',\n    'user_update_notification' => 'User successfully updated',\n    'user_delete' => 'deleted user',\n    'user_delete_notification' => 'User successfully removed',\n\n    // API Tokens\n    'api_token_create' => 'created API token',\n    'api_token_create_notification' => 'API token successfully created',\n    'api_token_update' => 'updated API token',\n    'api_token_update_notification' => 'API token successfully updated',\n    'api_token_delete' => 'deleted API token',\n    'api_token_delete_notification' => 'API token successfully deleted',\n\n    // Roles\n    'role_create' => 'created role',\n    'role_create_notification' => 'Role successfully created',\n    'role_update' => 'updated role',\n    'role_update_notification' => 'Role successfully updated',\n    'role_delete' => 'deleted role',\n    'role_delete_notification' => 'Role successfully deleted',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'emptied recycle bin',\n    'recycle_bin_restore' => 'restored from recycle bin',\n    'recycle_bin_destroy' => 'removed from recycle bin',\n\n    // Comments\n    'commented_on'                => 'commented on',\n    'comment_create'              => 'added comment',\n    'comment_update'              => 'updated comment',\n    'comment_delete'              => 'deleted comment',\n\n    // Sort Rules\n    'sort_rule_create' => 'created sort rule',\n    'sort_rule_create_notification' => 'Sort rule successfully created',\n    'sort_rule_update' => 'updated sort rule',\n    'sort_rule_update_notification' => 'Sort rule successfully updated',\n    'sort_rule_delete' => 'deleted sort rule',\n    'sort_rule_delete_notification' => 'Sort rule successfully deleted',\n\n    // Other\n    'permissions_update'          => 'updated permissions',\n];\n"
  },
  {
    "path": "lang/ku/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'ئەم بەکارهێنەرە نەدۆزرایەوە.',\n    'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',\n\n    // Login & Register\n    'sign_up' => 'Sign up',\n    'log_in' => 'Log in',\n    'log_in_with' => 'Login with :socialDriver',\n    'sign_up_with' => 'Sign up with :socialDriver',\n    'logout' => 'Logout',\n\n    'name' => 'Name',\n    'username' => 'Username',\n    'email' => 'Email',\n    'password' => 'Password',\n    'password_confirm' => 'Confirm Password',\n    'password_hint' => 'Must be at least 8 characters',\n    'forgot_password' => 'Forgot Password?',\n    'remember_me' => 'Remember Me',\n    'ldap_email_hint' => 'Please enter an email to use for this account.',\n    'create_account' => 'Create Account',\n    'already_have_account' => 'Already have an account?',\n    'dont_have_account' => 'Don\\'t have an account?',\n    'social_login' => 'Social Login',\n    'social_registration' => 'Social Registration',\n    'social_registration_text' => 'Register and sign in using another service.',\n\n    'register_thanks' => 'Thanks for registering!',\n    'register_confirm' => 'Please check your email and click the confirmation button to access :appName.',\n    'registrations_disabled' => 'Registrations are currently disabled',\n    'registration_email_domain_invalid' => 'That email domain does not have access to this application',\n    'register_success' => 'Thanks for signing up! You are now registered and signed in.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Attempting Login',\n    'auto_init_starting_desc' => 'We\\'re contacting your authentication system to start the login process. If there\\'s no progress after 5 seconds you can try clicking the link below.',\n    'auto_init_start_link' => 'Proceed with authentication',\n\n    // Password Reset\n    'reset_password' => 'Reset Password',\n    'reset_password_send_instructions' => 'Enter your email below and you will be sent an email with a password reset link.',\n    'reset_password_send_button' => 'Send Reset Link',\n    'reset_password_sent' => 'A password reset link will be sent to :email if that email address is found in the system.',\n    'reset_password_success' => 'Your password has been successfully reset.',\n    'email_reset_subject' => 'Reset your :appName password',\n    'email_reset_text' => 'You are receiving this email because we received a password reset request for your account.',\n    'email_reset_not_requested' => 'If you did not request a password reset, no further action is required.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Confirm your email on :appName',\n    'email_confirm_greeting' => 'Thanks for joining :appName!',\n    'email_confirm_text' => 'Please confirm your email address by clicking the button below:',\n    'email_confirm_action' => 'ئیمێڵەکەت دووبارە بنووسەوە',\n    'email_confirm_send_error' => 'Email confirmation required but the system could not send the email. Contact the admin to ensure email is set up correctly.',\n    'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',\n    'email_confirm_resent' => 'Confirmation email resent, Please check your inbox.',\n    'email_confirm_thanks' => 'Thanks for confirming!',\n    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the \"Continue\" link below to proceed.',\n\n    'email_not_confirmed' => 'Email Address Not Confirmed',\n    'email_not_confirmed_text' => 'Your email address has not yet been confirmed.',\n    'email_not_confirmed_click_link' => 'Please click the link in the email that was sent shortly after you registered.',\n    'email_not_confirmed_resend' => 'If you cannot find the email you can re-send the confirmation email by submitting the form below.',\n    'email_not_confirmed_resend_button' => 'Resend Confirmation Email',\n\n    // User Invite\n    'user_invite_email_subject' => 'You have been invited to join :appName!',\n    'user_invite_email_greeting' => 'An account has been created for you on :appName.',\n    'user_invite_email_text' => 'Click the button below to set an account password and gain access:',\n    'user_invite_email_action' => 'Set Account Password',\n    'user_invite_page_welcome' => 'Welcome to :appName!',\n    'user_invite_page_text' => 'To finalise your account and gain access you need to set a password which will be used to log-in to :appName on future visits.',\n    'user_invite_page_confirm_button' => 'Confirm Password',\n    'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Setup Multi-Factor Authentication',\n    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'mfa_setup_configured' => 'Already configured',\n    'mfa_setup_reconfigure' => 'Reconfigure',\n    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',\n    'mfa_setup_action' => 'Setup',\n    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',\n    'mfa_option_totp_title' => 'Mobile App',\n    'mfa_option_totp_desc' => 'To use multi-factor authentication you\\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Backup Codes',\n    'mfa_option_backup_codes_desc' => 'Generates a set of one-time-use backup codes which you\\'ll enter on login to verify your identity. Make sure to store these in a safe & secure place.',\n    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',\n    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',\n    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\\'ll be able to use one of the codes as a second authentication mechanism.',\n    'mfa_gen_backup_codes_download' => 'Download Codes',\n    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',\n    'mfa_gen_totp_title' => 'Mobile App Setup',\n    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',\n    'mfa_gen_totp_verify_setup' => 'Verify Setup',\n    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',\n    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',\n    'mfa_verify_access' => 'Verify Access',\n    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\\'re granted access. Verify using one of your configured methods to continue.',\n    'mfa_verify_no_methods' => 'No Methods Configured',\n    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\\'ll need to set up at least one method before you gain access.',\n    'mfa_verify_use_totp' => 'Verify using a mobile app',\n    'mfa_verify_use_backup_codes' => 'Verify using a backup code',\n    'mfa_verify_backup_code' => 'Backup Code',\n    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',\n    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',\n    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',\n    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',\n];\n"
  },
  {
    "path": "lang/ku/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'لابردن',\n    'close' => 'داخستن',\n    'confirm' => 'دڵنیام',\n    'back' => 'گەڕانەوە',\n    'save' => 'پاشەکەوتکردن',\n    'continue' => 'بەردەوامبە',\n    'select' => 'هەڵبژێرە',\n    'toggle_all' => 'Toggle All',\n    'more' => 'زیاتر',\n\n    // Form Labels\n    'name' => 'ناو',\n    'description' => 'وردەکاری',\n    'role' => 'توانا',\n    'cover_image' => 'وێنەی کەڤەر',\n    'cover_image_description' => 'This image should be approximately 440x250px although it will be flexibly scaled & cropped to fit the user interface in different scenarios as required, so actual dimensions for display will differ.',\n\n    // Actions\n    'actions' => 'کردارەکان',\n    'view' => 'بینین',\n    'view_all' => 'بینینی هەموو',\n    'new' => 'نوێ',\n    'create' => 'دروستکردن',\n    'update' => 'نوێکردنەوە',\n    'edit' => 'دەسکاریکردن',\n    'archive' => 'Archive',\n    'unarchive' => 'Un-Archive',\n    'sort' => 'ڕیزکردن',\n    'move' => 'جوڵاندن',\n    'copy' => 'لەبەرگرتنەوە',\n    'reply' => 'وەڵامدانەوە',\n    'delete' => 'سڕینەوە',\n    'delete_confirm' => 'دڵنیام لە سڕینەوە',\n    'search' => 'گەڕان',\n    'search_clear' => 'پاککردنه‌وه‌ی گه‌ڕان',\n    'reset' => 'ڕێکخستنەوە',\n    'remove' => 'لابردن',\n    'add' => 'زیادکردن',\n    'configure' => 'شێوەپێدان',\n    'manage' => 'بەڕیوەبردن',\n    'fullscreen' => 'پڕ شاشە',\n    'favourite' => 'ئارەزووەکان',\n    'unfavourite' => 'Unfavourite',\n    'next' => 'دواتر',\n    'previous' => 'پێشتر',\n    'filter_active' => 'Active Filter:',\n    'filter_clear' => 'Clear Filter',\n    'download' => 'دابەزاندن',\n    'open_in_tab' => 'Open in Tab',\n    'open' => 'کردنه‌وه‌',\n\n    // Sort Options\n    'sort_options' => 'Sort Options',\n    'sort_direction_toggle' => 'Sort Direction Toggle',\n    'sort_ascending' => 'Sort Ascending',\n    'sort_descending' => 'Sort Descending',\n    'sort_name' => 'ناو',\n    'sort_default' => 'Default',\n    'sort_created_at' => 'Created Date',\n    'sort_updated_at' => 'Updated Date',\n\n    // Misc\n    'deleted_user' => 'Deleted User',\n    'no_activity' => 'No activity to show',\n    'no_items' => 'No items available',\n    'back_to_top' => 'گەڕانەوە بۆ سەرەوە',\n    'skip_to_main_content' => 'Skip to main content',\n    'toggle_details' => 'Toggle Details',\n    'toggle_thumbnails' => 'Toggle Thumbnails',\n    'details' => 'وردەکاریەکان',\n    'grid_view' => 'Grid View',\n    'list_view' => 'List View',\n    'default' => 'Default',\n    'breadcrumb' => 'Breadcrumb',\n    'status' => 'Status',\n    'status_active' => 'Active',\n    'status_inactive' => 'Inactive',\n    'never' => 'Never',\n    'none' => 'هیچ',\n\n    // Header\n    'homepage' => 'پەرەی سەرەكی',\n    'header_menu_expand' => 'Expand Header Menu',\n    'profile_menu' => 'Profile Menu',\n    'view_profile' => 'View Profile',\n    'edit_profile' => 'ڕێکخستنی دۆسیەی تایبەت',\n    'dark_mode' => 'جۆری ڕەش',\n    'light_mode' => 'دۆخی ڕووناک',\n    'global_search' => 'Global Search',\n\n    // Layout tabs\n    'tab_info' => 'Info',\n    'tab_info_label' => 'Tab: Show Secondary Information',\n    'tab_content' => 'ناوەڕۆک',\n    'tab_content_label' => 'Tab: Show Primary Content',\n\n    // Email Content\n    'email_action_help' => 'If you’re having trouble clicking the \":actionText\" button, copy and paste the URL below into your web browser:',\n    'email_rights' => 'All rights reserved',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'ڕێبازی مافی کەسیی',\n    'terms_of_service' => 'مەرجەکانی خزمەت',\n\n    // OpenSearch\n    'opensearch_description' => 'Search :appName',\n];\n"
  },
  {
    "path": "lang/ku/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Image Select',\n    'image_list' => 'Image List',\n    'image_details' => 'Image Details',\n    'image_upload' => 'Upload Image',\n    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',\n    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the \"Upload Image\" button above.',\n    'image_all' => 'All',\n    'image_all_title' => 'View all images',\n    'image_book_title' => 'View images uploaded to this book',\n    'image_page_title' => 'View images uploaded to this page',\n    'image_search_hint' => 'Search by image name',\n    'image_uploaded' => 'Uploaded :uploadedDate',\n    'image_uploaded_by' => 'Uploaded by :userName',\n    'image_uploaded_to' => 'Uploaded to :pageLink',\n    'image_updated' => 'Updated :updateDate',\n    'image_load_more' => 'Load More',\n    'image_image_name' => 'Image Name',\n    'image_delete_used' => 'This image is used in the pages below.',\n    'image_delete_confirm_text' => 'Are you sure you want to delete this image?',\n    'image_select_image' => 'Select Image',\n    'image_dropzone' => 'Drop images or click here to upload',\n    'image_dropzone_drop' => 'Drop images here to upload',\n    'images_deleted' => 'Images Deleted',\n    'image_preview' => 'Image Preview',\n    'image_upload_success' => 'Image uploaded successfully',\n    'image_update_success' => 'Image details successfully updated',\n    'image_delete_success' => 'Image successfully deleted',\n    'image_replace' => 'Replace Image',\n    'image_replace_success' => 'Image file successfully updated',\n    'image_rebuild_thumbs' => 'Regenerate Size Variations',\n    'image_rebuild_thumbs_success' => 'Image size variations successfully rebuilt!',\n\n    // Code Editor\n    'code_editor' => 'Edit Code',\n    'code_language' => 'Code Language',\n    'code_content' => 'Code Content',\n    'code_session_history' => 'Session History',\n    'code_save' => 'Save Code',\n];\n"
  },
  {
    "path": "lang/ku/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'General',\n    'advanced' => 'Advanced',\n    'none' => 'None',\n    'cancel' => 'Cancel',\n    'save' => 'Save',\n    'close' => 'Close',\n    'apply' => 'Apply',\n    'undo' => 'Undo',\n    'redo' => 'Redo',\n    'left' => 'Left',\n    'center' => 'Center',\n    'right' => 'Right',\n    'top' => 'Top',\n    'middle' => 'Middle',\n    'bottom' => 'Bottom',\n    'width' => 'Width',\n    'height' => 'Height',\n    'More' => 'More',\n    'select' => 'Select...',\n\n    // Toolbar\n    'formats' => 'Formats',\n    'header_large' => 'Large Header',\n    'header_medium' => 'Medium Header',\n    'header_small' => 'Small Header',\n    'header_tiny' => 'Tiny Header',\n    'paragraph' => 'Paragraph',\n    'blockquote' => 'Blockquote',\n    'inline_code' => 'Inline code',\n    'callouts' => 'Callouts',\n    'callout_information' => 'Information',\n    'callout_success' => 'Success',\n    'callout_warning' => 'Warning',\n    'callout_danger' => 'Danger',\n    'bold' => 'Bold',\n    'italic' => 'Italic',\n    'underline' => 'Underline',\n    'strikethrough' => 'Strikethrough',\n    'superscript' => 'Superscript',\n    'subscript' => 'Subscript',\n    'text_color' => 'Text color',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Custom color',\n    'remove_color' => 'Remove color',\n    'background_color' => 'Background color',\n    'align_left' => 'Align left',\n    'align_center' => 'Align center',\n    'align_right' => 'Align right',\n    'align_justify' => 'Justify',\n    'list_bullet' => 'Bullet list',\n    'list_numbered' => 'Numbered list',\n    'list_task' => 'Task list',\n    'indent_increase' => 'Increase indent',\n    'indent_decrease' => 'Decrease indent',\n    'table' => 'Table',\n    'insert_image' => 'Insert image',\n    'insert_image_title' => 'Insert/Edit Image',\n    'insert_link' => 'Insert/edit link',\n    'insert_link_title' => 'Insert/Edit Link',\n    'insert_horizontal_line' => 'Insert horizontal line',\n    'insert_code_block' => 'Insert code block',\n    'edit_code_block' => 'Edit code block',\n    'insert_drawing' => 'Insert/edit drawing',\n    'drawing_manager' => 'Drawing manager',\n    'insert_media' => 'Insert/edit media',\n    'insert_media_title' => 'Insert/Edit Media',\n    'clear_formatting' => 'Clear formatting',\n    'source_code' => 'Source code',\n    'source_code_title' => 'Source Code',\n    'fullscreen' => 'Fullscreen',\n    'image_options' => 'Image options',\n\n    // Tables\n    'table_properties' => 'Table properties',\n    'table_properties_title' => 'Table Properties',\n    'delete_table' => 'Delete table',\n    'table_clear_formatting' => 'Clear table formatting',\n    'resize_to_contents' => 'Resize to contents',\n    'row_header' => 'Row header',\n    'insert_row_before' => 'Insert row before',\n    'insert_row_after' => 'Insert row after',\n    'delete_row' => 'Delete row',\n    'insert_column_before' => 'Insert column before',\n    'insert_column_after' => 'Insert column after',\n    'delete_column' => 'Delete column',\n    'table_cell' => 'Cell',\n    'table_row' => 'Row',\n    'table_column' => 'Column',\n    'cell_properties' => 'Cell properties',\n    'cell_properties_title' => 'Cell Properties',\n    'cell_type' => 'Cell type',\n    'cell_type_cell' => 'Cell',\n    'cell_scope' => 'Scope',\n    'cell_type_header' => 'Header cell',\n    'merge_cells' => 'Merge cells',\n    'split_cell' => 'Split cell',\n    'table_row_group' => 'Row Group',\n    'table_column_group' => 'Column Group',\n    'horizontal_align' => 'Horizontal align',\n    'vertical_align' => 'Vertical align',\n    'border_width' => 'Border width',\n    'border_style' => 'Border style',\n    'border_color' => 'Border color',\n    'row_properties' => 'Row properties',\n    'row_properties_title' => 'Row Properties',\n    'cut_row' => 'Cut row',\n    'copy_row' => 'Copy row',\n    'paste_row_before' => 'Paste row before',\n    'paste_row_after' => 'Paste row after',\n    'row_type' => 'Row type',\n    'row_type_header' => 'Header',\n    'row_type_body' => 'Body',\n    'row_type_footer' => 'Footer',\n    'alignment' => 'Alignment',\n    'cut_column' => 'Cut column',\n    'copy_column' => 'Copy column',\n    'paste_column_before' => 'Paste column before',\n    'paste_column_after' => 'Paste column after',\n    'cell_padding' => 'Cell padding',\n    'cell_spacing' => 'Cell spacing',\n    'caption' => 'Caption',\n    'show_caption' => 'Show caption',\n    'constrain' => 'Constrain proportions',\n    'cell_border_solid' => 'Solid',\n    'cell_border_dotted' => 'Dotted',\n    'cell_border_dashed' => 'Dashed',\n    'cell_border_double' => 'Double',\n    'cell_border_groove' => 'Groove',\n    'cell_border_ridge' => 'Ridge',\n    'cell_border_inset' => 'Inset',\n    'cell_border_outset' => 'Outset',\n    'cell_border_none' => 'None',\n    'cell_border_hidden' => 'Hidden',\n\n    // Images, links, details/summary & embed\n    'source' => 'Source',\n    'alt_desc' => 'Alternative description',\n    'embed' => 'Embed',\n    'paste_embed' => 'Paste your embed code below:',\n    'url' => 'URL',\n    'text_to_display' => 'Text to display',\n    'title' => 'Title',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Open link',\n    'open_link_in' => 'Open link in...',\n    'open_link_current' => 'Current window',\n    'open_link_new' => 'New window',\n    'remove_link' => 'Remove link',\n    'insert_collapsible' => 'Insert collapsible block',\n    'collapsible_unwrap' => 'Unwrap',\n    'edit_label' => 'Edit label',\n    'toggle_open_closed' => 'Toggle open/closed',\n    'collapsible_edit' => 'Edit collapsible block',\n    'toggle_label' => 'Toggle label',\n\n    // About view\n    'about' => 'About the editor',\n    'about_title' => 'About the WYSIWYG Editor',\n    'editor_license' => 'Editor License & Copyright',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under the MIT license.',\n    'editor_tiny_license_link' => 'The copyright and license details of TinyMCE can be found here.',\n    'save_continue' => 'Save Page & Continue',\n    'callouts_cycle' => '(Keep pressing to toggle through types)',\n    'link_selector' => 'Link to content',\n    'shortcuts' => 'Shortcuts',\n    'shortcut' => 'Shortcut',\n    'shortcuts_intro' => 'The following shortcuts are available in the editor:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Description',\n];\n"
  },
  {
    "path": "lang/ku/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Recently Created',\n    'recently_created_pages' => 'Recently Created Pages',\n    'recently_updated_pages' => 'Recently Updated Pages',\n    'recently_created_chapters' => 'Recently Created Chapters',\n    'recently_created_books' => 'Recently Created Books',\n    'recently_created_shelves' => 'Recently Created Shelves',\n    'recently_update' => 'Recently Updated',\n    'recently_viewed' => 'Recently Viewed',\n    'recent_activity' => 'Recent Activity',\n    'create_now' => 'Create one now',\n    'revisions' => 'Revisions',\n    'meta_revision' => 'Revision #:revisionCount',\n    'meta_created' => 'Created :timeLength',\n    'meta_created_name' => 'Created :timeLength by :user',\n    'meta_updated' => 'Updated :timeLength',\n    'meta_updated_name' => 'Updated :timeLength by :user',\n    'meta_owned_name' => 'Owned by :user',\n    'meta_reference_count' => 'Referenced by :count item|Referenced by :count items',\n    'entity_select' => 'Entity Select',\n    'entity_select_lack_permission' => 'You don\\'t have the required permissions to select this item',\n    'images' => 'Images',\n    'my_recent_drafts' => 'My Recent Drafts',\n    'my_recently_viewed' => 'My Recently Viewed',\n    'my_most_viewed_favourites' => 'My Most Viewed Favourites',\n    'my_favourites' => 'My Favourites',\n    'no_pages_viewed' => 'You have not viewed any pages',\n    'no_pages_recently_created' => 'No pages have been recently created',\n    'no_pages_recently_updated' => 'No pages have been recently updated',\n    'export' => 'Export',\n    'export_html' => 'Contained Web File',\n    'export_pdf' => 'PDF File',\n    'export_text' => 'Plain Text File',\n    'export_md' => 'Markdown File',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Default Page Template',\n    'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',\n    'default_template_select' => 'Select a template page',\n    'import' => 'Import',\n    'import_validate' => 'Validate Import',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'Select ZIP file to upload',\n    'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',\n    'import_pending' => 'Pending Imports',\n    'import_pending_none' => 'No imports have been started.',\n    'import_continue' => 'Continue Import',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Run Import',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Uploaded by',\n    'import_location' => 'Import Location',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'Import Errors',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Permissions',\n    'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.',\n    'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.',\n    'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.',\n    'permissions_save' => 'Save Permissions',\n    'permissions_owner' => 'Owner',\n    'permissions_role_everyone_else' => 'Everyone Else',\n    'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',\n    'permissions_role_override' => 'Override permissions for role',\n    'permissions_inherit_defaults' => 'Inherit defaults',\n\n    // Search\n    'search_results' => 'Search Results',\n    'search_total_results_found' => ':count result found|:count total results found',\n    'search_clear' => 'Clear Search',\n    'search_no_pages' => 'No pages matched this search',\n    'search_for_term' => 'Search for :term',\n    'search_more' => 'More Results',\n    'search_advanced' => 'Advanced Search',\n    'search_terms' => 'Search Terms',\n    'search_content_type' => 'Content Type',\n    'search_exact_matches' => 'Exact Matches',\n    'search_tags' => 'Tag Searches',\n    'search_options' => 'Options',\n    'search_viewed_by_me' => 'Viewed by me',\n    'search_not_viewed_by_me' => 'Not viewed by me',\n    'search_permissions_set' => 'Permissions set',\n    'search_created_by_me' => 'Created by me',\n    'search_updated_by_me' => 'Updated by me',\n    'search_owned_by_me' => 'Owned by me',\n    'search_date_options' => 'Date Options',\n    'search_updated_before' => 'Updated before',\n    'search_updated_after' => 'Updated after',\n    'search_created_before' => 'Created before',\n    'search_created_after' => 'Created after',\n    'search_set_date' => 'Set Date',\n    'search_update' => 'Update Search',\n\n    // Shelves\n    'shelf' => 'Shelf',\n    'shelves' => 'Shelves',\n    'x_shelves' => ':count Shelf|:count Shelves',\n    'shelves_empty' => 'No shelves have been created',\n    'shelves_create' => 'Create New Shelf',\n    'shelves_popular' => 'Popular Shelves',\n    'shelves_new' => 'New Shelves',\n    'shelves_new_action' => 'New Shelf',\n    'shelves_popular_empty' => 'The most popular shelves will appear here.',\n    'shelves_new_empty' => 'The most recently created shelves will appear here.',\n    'shelves_save' => 'Save Shelf',\n    'shelves_books' => 'Books on this shelf',\n    'shelves_add_books' => 'Add books to this shelf',\n    'shelves_drag_books' => 'Drag books below to add them to this shelf',\n    'shelves_empty_contents' => 'This shelf has no books assigned to it',\n    'shelves_edit_and_assign' => 'Edit shelf to assign books',\n    'shelves_edit_named' => 'Edit Shelf :name',\n    'shelves_edit' => 'Edit Shelf',\n    'shelves_delete' => 'Delete Shelf',\n    'shelves_delete_named' => 'Delete Shelf :name',\n    'shelves_delete_explain' => \"This will delete the shelf with the name ':name'. Contained books will not be deleted.\",\n    'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',\n    'shelves_permissions' => 'Shelf Permissions',\n    'shelves_permissions_updated' => 'Shelf Permissions Updated',\n    'shelves_permissions_active' => 'Shelf Permissions Active',\n    'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',\n    'shelves_permissions_create' => 'Shelf create permissions are only used for copying permissions to child books using the action below. They do not control the ability to create books.',\n    'shelves_copy_permissions_to_books' => 'Copy Permissions to Books',\n    'shelves_copy_permissions' => 'Copy Permissions',\n    'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',\n    'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',\n\n    // Books\n    'book' => 'Book',\n    'books' => 'Books',\n    'x_books' => ':count Book|:count Books',\n    'books_empty' => 'No books have been created',\n    'books_popular' => 'Popular Books',\n    'books_recent' => 'Recent Books',\n    'books_new' => 'New Books',\n    'books_new_action' => 'New Book',\n    'books_popular_empty' => 'The most popular books will appear here.',\n    'books_new_empty' => 'The most recently created books will appear here.',\n    'books_create' => 'Create New Book',\n    'books_delete' => 'Delete Book',\n    'books_delete_named' => 'Delete Book :bookName',\n    'books_delete_explain' => 'This will delete the book with the name \\':bookName\\'. All pages and chapters will be removed.',\n    'books_delete_confirmation' => 'Are you sure you want to delete this book?',\n    'books_edit' => 'Edit Book',\n    'books_edit_named' => 'Edit Book :bookName',\n    'books_form_book_name' => 'Book Name',\n    'books_save' => 'Save Book',\n    'books_permissions' => 'Book Permissions',\n    'books_permissions_updated' => 'Book Permissions Updated',\n    'books_empty_contents' => 'No pages or chapters have been created for this book.',\n    'books_empty_create_page' => 'Create a new page',\n    'books_empty_sort_current_book' => 'Sort the current book',\n    'books_empty_add_chapter' => 'Add a chapter',\n    'books_permissions_active' => 'Book Permissions Active',\n    'books_search_this' => 'Search this book',\n    'books_navigation' => 'Book Navigation',\n    'books_sort' => 'Sort Book Contents',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Sort Book :bookName',\n    'books_sort_name' => 'Sort by Name',\n    'books_sort_created' => 'Sort by Created Date',\n    'books_sort_updated' => 'Sort by Updated Date',\n    'books_sort_chapters_first' => 'Chapters First',\n    'books_sort_chapters_last' => 'Chapters Last',\n    'books_sort_show_other' => 'Show Other Books',\n    'books_sort_save' => 'Save New Order',\n    'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.',\n    'books_sort_move_up' => 'Move Up',\n    'books_sort_move_down' => 'Move Down',\n    'books_sort_move_prev_book' => 'Move to Previous Book',\n    'books_sort_move_next_book' => 'Move to Next Book',\n    'books_sort_move_prev_chapter' => 'Move Into Previous Chapter',\n    'books_sort_move_next_chapter' => 'Move Into Next Chapter',\n    'books_sort_move_book_start' => 'Move to Start of Book',\n    'books_sort_move_book_end' => 'Move to End of Book',\n    'books_sort_move_before_chapter' => 'Move to Before Chapter',\n    'books_sort_move_after_chapter' => 'Move to After Chapter',\n    'books_copy' => 'Copy Book',\n    'books_copy_success' => 'Book successfully copied',\n\n    // Chapters\n    'chapter' => 'Chapter',\n    'chapters' => 'Chapters',\n    'x_chapters' => ':count Chapter|:count Chapters',\n    'chapters_popular' => 'Popular Chapters',\n    'chapters_new' => 'New Chapter',\n    'chapters_create' => 'Create New Chapter',\n    'chapters_delete' => 'Delete Chapter',\n    'chapters_delete_named' => 'Delete Chapter :chapterName',\n    'chapters_delete_explain' => 'This will delete the chapter with the name \\':chapterName\\'. All pages that exist within this chapter will also be deleted.',\n    'chapters_delete_confirm' => 'Are you sure you want to delete this chapter?',\n    'chapters_edit' => 'Edit Chapter',\n    'chapters_edit_named' => 'Edit Chapter :chapterName',\n    'chapters_save' => 'Save Chapter',\n    'chapters_move' => 'Move Chapter',\n    'chapters_move_named' => 'Move Chapter :chapterName',\n    'chapters_copy' => 'Copy Chapter',\n    'chapters_copy_success' => 'Chapter successfully copied',\n    'chapters_permissions' => 'Chapter Permissions',\n    'chapters_empty' => 'No pages are currently in this chapter.',\n    'chapters_permissions_active' => 'Chapter Permissions Active',\n    'chapters_permissions_success' => 'Chapter Permissions Updated',\n    'chapters_search_this' => 'Search this chapter',\n    'chapter_sort_book' => 'Sort Book',\n\n    // Pages\n    'page' => 'Page',\n    'pages' => 'Pages',\n    'x_pages' => ':count Page|:count Pages',\n    'pages_popular' => 'Popular Pages',\n    'pages_new' => 'New Page',\n    'pages_attachments' => 'Attachments',\n    'pages_navigation' => 'Page Navigation',\n    'pages_delete' => 'Delete Page',\n    'pages_delete_named' => 'Delete Page :pageName',\n    'pages_delete_draft_named' => 'Delete Draft Page :pageName',\n    'pages_delete_draft' => 'Delete Draft Page',\n    'pages_delete_success' => 'Page deleted',\n    'pages_delete_draft_success' => 'Draft page deleted',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'Are you sure you want to delete this page?',\n    'pages_delete_draft_confirm' => 'Are you sure you want to delete this draft page?',\n    'pages_editing_named' => 'Editing Page :pageName',\n    'pages_edit_draft_options' => 'Draft Options',\n    'pages_edit_save_draft' => 'Save Draft',\n    'pages_edit_draft' => 'Edit Page Draft',\n    'pages_editing_draft' => 'Editing Draft',\n    'pages_editing_page' => 'Editing Page',\n    'pages_edit_draft_save_at' => 'Draft saved at ',\n    'pages_edit_delete_draft' => 'Delete Draft',\n    'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.',\n    'pages_edit_discard_draft' => 'Discard Draft',\n    'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',\n    'pages_edit_switch_to_markdown_clean' => '(Clean Content)',\n    'pages_edit_switch_to_markdown_stable' => '(Stable Content)',\n    'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Set Changelog',\n    'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\\'ve made',\n    'pages_edit_enter_changelog' => 'Enter Changelog',\n    'pages_editor_switch_title' => 'Switch Editor',\n    'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',\n    'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',\n    'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',\n    'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',\n    'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\\'t persist across this change.',\n    'pages_save' => 'Save Page',\n    'pages_title' => 'Page Title',\n    'pages_name' => 'Page Name',\n    'pages_md_editor' => 'Editor',\n    'pages_md_preview' => 'Preview',\n    'pages_md_insert_image' => 'Insert Image',\n    'pages_md_insert_link' => 'Insert Entity Link',\n    'pages_md_insert_drawing' => 'Insert Drawing',\n    'pages_md_show_preview' => 'Show preview',\n    'pages_md_sync_scroll' => 'Sync preview scroll',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Unsaved Drawing Found',\n    'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',\n    'pages_not_in_chapter' => 'Page is not in a chapter',\n    'pages_move' => 'Move Page',\n    'pages_copy' => 'Copy Page',\n    'pages_copy_desination' => 'Copy Destination',\n    'pages_copy_success' => 'Page successfully copied',\n    'pages_permissions' => 'Page Permissions',\n    'pages_permissions_success' => 'Page permissions updated',\n    'pages_revision' => 'Revision',\n    'pages_revisions' => 'Page Revisions',\n    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',\n    'pages_revisions_named' => 'Page Revisions for :pageName',\n    'pages_revision_named' => 'Page Revision for :pageName',\n    'pages_revision_restored_from' => 'Restored from #:id; :summary',\n    'pages_revisions_created_by' => 'Created By',\n    'pages_revisions_date' => 'Revision Date',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Revision Number',\n    'pages_revisions_numbered' => 'Revision #:id',\n    'pages_revisions_numbered_changes' => 'Revision #:id Changes',\n    'pages_revisions_editor' => 'Editor Type',\n    'pages_revisions_changelog' => 'Changelog',\n    'pages_revisions_changes' => 'Changes',\n    'pages_revisions_current' => 'Current Version',\n    'pages_revisions_preview' => 'Preview',\n    'pages_revisions_restore' => 'Restore',\n    'pages_revisions_none' => 'This page has no revisions',\n    'pages_copy_link' => 'Copy Link',\n    'pages_edit_content_link' => 'Jump to section in editor',\n    'pages_pointer_enter_mode' => 'Enter section select mode',\n    'pages_pointer_label' => 'Page Section Options',\n    'pages_pointer_permalink' => 'Page Section Permalink',\n    'pages_pointer_include_tag' => 'Page Section Include Tag',\n    'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag',\n    'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink',\n    'pages_permissions_active' => 'Page Permissions Active',\n    'pages_initial_revision' => 'Initial publish',\n    'pages_references_update_revision' => 'System auto-update of internal links',\n    'pages_initial_name' => 'New Page',\n    'pages_editing_draft_notification' => 'You are currently editing a draft that was last saved :timeDiff.',\n    'pages_draft_edited_notification' => 'This page has been updated by since that time. It is recommended that you discard this draft.',\n    'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count users have started editing this page',\n        'start_b' => ':userName has started editing this page',\n        'time_a' => 'since the page was last updated',\n        'time_b' => 'in the last :minCount minutes',\n        'message' => ':start :time. Take care not to overwrite each other\\'s updates!',\n    ],\n    'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',\n    'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',\n    'pages_specific' => 'Specific Page',\n    'pages_is_template' => 'Page Template',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Toggle Sidebar',\n    'page_tags' => 'Page Tags',\n    'chapter_tags' => 'Chapter Tags',\n    'book_tags' => 'Book Tags',\n    'shelf_tags' => 'Shelf Tags',\n    'tag' => 'Tag',\n    'tags' =>  'Tags',\n    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',\n    'tag_name' =>  'Tag Name',\n    'tag_value' => 'Tag Value (Optional)',\n    'tags_explain' => \"Add some tags to better categorise your content. \\n You can assign a value to a tag for more in-depth organisation.\",\n    'tags_add' => 'Add another tag',\n    'tags_remove' => 'Remove this tag',\n    'tags_usages' => 'Total tag usages',\n    'tags_assigned_pages' => 'Assigned to Pages',\n    'tags_assigned_chapters' => 'Assigned to Chapters',\n    'tags_assigned_books' => 'Assigned to Books',\n    'tags_assigned_shelves' => 'Assigned to Shelves',\n    'tags_x_unique_values' => ':count unique values',\n    'tags_all_values' => 'All values',\n    'tags_view_tags' => 'View Tags',\n    'tags_view_existing_tags' => 'View existing tags',\n    'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',\n    'attachments' => 'Attachments',\n    'attachments_explain' => 'Upload some files or attach some links to display on your page. These are visible in the page sidebar.',\n    'attachments_explain_instant_save' => 'Changes here are saved instantly.',\n    'attachments_upload' => 'Upload File',\n    'attachments_link' => 'Attach Link',\n    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',\n    'attachments_set_link' => 'Set Link',\n    'attachments_delete' => 'Are you sure you want to delete this attachment?',\n    'attachments_dropzone' => 'Drop files here to upload',\n    'attachments_no_files' => 'No files have been uploaded',\n    'attachments_explain_link' => 'You can attach a link if you\\'d prefer not to upload a file. This can be a link to another page or a link to a file in the cloud.',\n    'attachments_link_name' => 'Link Name',\n    'attachment_link' => 'Attachment link',\n    'attachments_link_url' => 'Link to file',\n    'attachments_link_url_hint' => 'Url of site or file',\n    'attach' => 'Attach',\n    'attachments_insert_link' => 'Add Attachment Link to Page',\n    'attachments_edit_file' => 'Edit File',\n    'attachments_edit_file_name' => 'File Name',\n    'attachments_edit_drop_upload' => 'Drop files or click here to upload and overwrite',\n    'attachments_order_updated' => 'Attachment order updated',\n    'attachments_updated_success' => 'Attachment details updated',\n    'attachments_deleted' => 'Attachment deleted',\n    'attachments_file_uploaded' => 'File successfully uploaded',\n    'attachments_file_updated' => 'File successfully updated',\n    'attachments_link_attached' => 'Link successfully attached to page',\n    'templates' => 'Templates',\n    'templates_set_as_template' => 'Page is a template',\n    'templates_explain_set_as_template' => 'You can set this page as a template so its contents be utilized when creating other pages. Other users will be able to use this template if they have view permissions for this page.',\n    'templates_replace_content' => 'Replace page content',\n    'templates_append_content' => 'Append to page content',\n    'templates_prepend_content' => 'Prepend to page content',\n\n    // Profile View\n    'profile_user_for_x' => 'User for :time',\n    'profile_created_content' => 'Created Content',\n    'profile_not_created_pages' => ':userName has not created any pages',\n    'profile_not_created_chapters' => ':userName has not created any chapters',\n    'profile_not_created_books' => ':userName has not created any books',\n    'profile_not_created_shelves' => ':userName has not created any shelves',\n\n    // Comments\n    'comment' => 'Comment',\n    'comments' => 'Comments',\n    'comment_add' => 'Add Comment',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Leave a comment here',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Save Comment',\n    'comment_new' => 'New Comment',\n    'comment_created' => 'commented :createDiff',\n    'comment_updated' => 'Updated :updateDiff by :username',\n    'comment_updated_indicator' => 'Updated',\n    'comment_deleted_success' => 'Comment deleted',\n    'comment_created_success' => 'Comment added',\n    'comment_updated_success' => 'Comment updated',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Are you sure you want to delete this comment?',\n    'comment_in_reply_to' => 'In reply to :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',\n\n    // Revision\n    'revision_delete_confirm' => 'Are you sure you want to delete this revision?',\n    'revision_restore_confirm' => 'Are you sure you want to restore this revision? The current page contents will be replaced.',\n    'revision_cannot_delete_latest' => 'Cannot delete the latest revision.',\n\n    // Copy view\n    'copy_consider' => 'Please consider the below when copying content.',\n    'copy_consider_permissions' => 'Custom permission settings will not be copied.',\n    'copy_consider_owner' => 'You will become the owner of all copied content.',\n    'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',\n    'copy_consider_attachments' => 'Page attachments will not be copied.',\n    'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',\n\n    // Conversions\n    'convert_to_shelf' => 'Convert to Shelf',\n    'convert_to_shelf_contents_desc' => 'You can convert this book to a new shelf with the same contents. Chapters contained within this book will be converted to new books. If this book contains any pages, that are not in a chapter, this book will be renamed and contain such pages, and this book will become part of the new shelf.',\n    'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.',\n    'convert_book' => 'Convert Book',\n    'convert_book_confirm' => 'Are you sure you want to convert this book?',\n    'convert_undo_warning' => 'This cannot be as easily undone.',\n    'convert_to_book' => 'Convert to Book',\n    'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',\n    'convert_chapter' => 'Convert Chapter',\n    'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',\n\n    // References\n    'references' => 'References',\n    'references_none' => 'There are no tracked references to this item.',\n    'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',\n\n    // Watch Options\n    'watch' => 'Watch',\n    'watch_title_default' => 'Default Preferences',\n    'watch_desc_default' => 'Revert watching to just your default notification preferences.',\n    'watch_title_ignore' => 'Ignore',\n    'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.',\n    'watch_title_new' => 'New Pages',\n    'watch_desc_new' => 'Notify when any new page is created within this item.',\n    'watch_title_updates' => 'All Page Updates',\n    'watch_desc_updates' => 'Notify upon all new pages and page changes.',\n    'watch_desc_updates_page' => 'Notify upon all page changes.',\n    'watch_title_comments' => 'All Page Updates & Comments',\n    'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.',\n    'watch_desc_comments_page' => 'Notify upon page changes and new comments.',\n    'watch_change_default' => 'Change default notification preferences',\n    'watch_detail_ignore' => 'Ignoring notifications',\n    'watch_detail_new' => 'Watching for new pages',\n    'watch_detail_updates' => 'Watching new pages and updates',\n    'watch_detail_comments' => 'Watching new pages, updates & comments',\n    'watch_detail_parent_book' => 'Watching via parent book',\n    'watch_detail_parent_book_ignore' => 'Ignoring via parent book',\n    'watch_detail_parent_chapter' => 'Watching via parent chapter',\n    'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter',\n];\n"
  },
  {
    "path": "lang/ku/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'You do not have permission to access the requested page.',\n    'permissionJson' => 'You do not have permission to perform the requested action.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'A user with the email :email already exists but with different credentials.',\n    'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',\n    'email_already_confirmed' => 'Email has already been confirmed, Try logging in.',\n    'email_confirmation_invalid' => 'This confirmation token is not valid or has already been used, Please try registering again.',\n    'email_confirmation_expired' => 'The confirmation token has expired, A new confirmation email has been sent.',\n    'email_confirmation_awaiting' => 'The email address for the account in use needs to be confirmed',\n    'ldap_fail_anonymous' => 'LDAP access failed using anonymous bind',\n    'ldap_fail_authed' => 'LDAP access failed using given dn & password details',\n    'ldap_extension_not_installed' => 'LDAP PHP extension not installed',\n    'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed',\n    'saml_already_logged_in' => 'Already logged in',\n    'saml_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',\n    'saml_invalid_response_id' => 'The request from the external authentication system is not recognised by a process started by this application. Navigating back after a login could cause this issue.',\n    'saml_fail_authed' => 'Login using :system failed, system did not provide successful authorization',\n    'oidc_already_logged_in' => 'Already logged in',\n    'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',\n    'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',\n    'social_no_action_defined' => 'No action defined',\n    'social_login_bad_response' => \"Error received during :socialAccount login: \\n:error\",\n    'social_account_in_use' => 'This :socialAccount account is already in use, Try logging in via the :socialAccount option.',\n    'social_account_email_in_use' => 'The email :email is already in use. If you already have an account you can connect your :socialAccount account from your profile settings.',\n    'social_account_existing' => 'This :socialAccount is already attached to your profile.',\n    'social_account_already_used_existing' => 'This :socialAccount account is already used by another user.',\n    'social_account_not_used' => 'This :socialAccount account is not linked to any users. Please attach it in your profile settings. ',\n    'social_account_register_instructions' => 'If you do not yet have an account, You can register an account using the :socialAccount option.',\n    'social_driver_not_found' => 'Social driver not found',\n    'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.',\n    'invite_token_expired' => 'This invitation link has expired. You can instead try to reset your account password.',\n    'login_user_not_found' => 'A user for this action could not be found.',\n\n    // System\n    'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.',\n    'cannot_get_image_from_url' => 'Cannot get image from :url',\n    'cannot_create_thumbs' => 'The server cannot create thumbnails. Please check you have the GD PHP extension installed.',\n    'server_upload_limit' => 'The server does not allow uploads of this size. Please try a smaller file size.',\n    'server_post_limit' => 'The server cannot receive the provided amount of data. Try again with less data or a smaller file.',\n    'uploaded'  => 'The server does not allow uploads of this size. Please try a smaller file size.',\n\n    // Drawing & Images\n    'image_upload_error' => 'An error occurred uploading the image',\n    'image_upload_type_error' => 'The image type being uploaded is invalid',\n    'image_upload_replace_type' => 'Image file replacements must be of the same type',\n    'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.',\n    'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.',\n    'image_gallery_thumbnail_memory_limit' => 'Failed to create gallery thumbnails due to system resource limits.',\n    'drawing_data_not_found' => 'Drawing data could not be loaded. The drawing file might no longer exist or you may not have permission to access it.',\n\n    // Attachments\n    'attachment_not_found' => 'Attachment not found',\n    'attachment_upload_error' => 'An error occurred uploading the attachment file',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page',\n    'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content',\n    'page_custom_home_deletion' => 'Cannot delete a page while it is set as a homepage',\n\n    // Entities\n    'entity_not_found' => 'Entity not found',\n    'bookshelf_not_found' => 'Shelf not found',\n    'book_not_found' => 'Book not found',\n    'page_not_found' => 'Page not found',\n    'chapter_not_found' => 'Chapter not found',\n    'selected_book_not_found' => 'The selected book was not found',\n    'selected_book_chapter_not_found' => 'The selected Book or Chapter was not found',\n    'guests_cannot_save_drafts' => 'Guests cannot save drafts',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'You cannot delete the only admin',\n    'users_cannot_delete_guest' => 'You cannot delete the guest user',\n    'users_could_not_send_invite' => 'Could not create user since invite email failed to send',\n\n    // Roles\n    'role_cannot_be_edited' => 'This role cannot be edited',\n    'role_system_cannot_be_deleted' => 'This role is a system role and cannot be deleted',\n    'role_registration_default_cannot_delete' => 'This role cannot be deleted while set as the default registration role',\n    'role_cannot_remove_only_admin' => 'This user is the only user assigned to the administrator role. Assign the administrator role to another user before attempting to remove it here.',\n\n    // Comments\n    'comment_list' => 'An error occurred while fetching the comments.',\n    'cannot_add_comment_to_draft' => 'You cannot add comments to a draft.',\n    'comment_add' => 'An error occurred while adding / updating the comment.',\n    'comment_delete' => 'An error occurred while deleting the comment.',\n    'empty_comment' => 'Cannot add an empty comment.',\n\n    // Error pages\n    '404_page_not_found' => 'Page Not Found',\n    'sorry_page_not_found' => 'Sorry, The page you were looking for could not be found.',\n    'sorry_page_not_found_permission_warning' => 'If you expected this page to exist, you might not have permission to view it.',\n    'image_not_found' => 'Image Not Found',\n    'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',\n    'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',\n    'return_home' => 'Return to home',\n    'error_occurred' => 'An Error Occurred',\n    'app_down' => ':appName is down right now',\n    'back_soon' => 'It will be back up soon.',\n\n    // Import\n    'import_zip_cant_read' => 'Could not read ZIP file.',\n    'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',\n    'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Import ZIP failed to validate with errors:',\n    'import_zip_failed_notification' => 'Failed to import ZIP file.',\n    'import_perms_books' => 'You are lacking the required permissions to create books.',\n    'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',\n    'import_perms_pages' => 'You are lacking the required permissions to create pages.',\n    'import_perms_images' => 'You are lacking the required permissions to create images.',\n    'import_perms_attachments' => 'You are lacking the required permission to create attachments.',\n\n    // API errors\n    'api_no_authorization_found' => 'No authorization token found on the request',\n    'api_bad_authorization_format' => 'An authorization token was found on the request but the format appeared incorrect',\n    'api_user_token_not_found' => 'No matching API token was found for the provided authorization token',\n    'api_incorrect_token_secret' => 'The secret provided for the given used API token is incorrect',\n    'api_user_no_api_permission' => 'The owner of the used API token does not have permission to make API calls',\n    'api_user_token_expired' => 'The authorization token used has expired',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Error thrown when sending a test email:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'The URL does not match the configured allowed SSR hosts',\n];\n"
  },
  {
    "path": "lang/ku/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'New comment on page: :pageName',\n    'new_comment_intro' => 'A user has commented on a page in :appName:',\n    'new_page_subject' => 'New page: :pageName',\n    'new_page_intro' => 'A new page has been created in :appName:',\n    'updated_page_subject' => 'Updated page: :pageName',\n    'updated_page_intro' => 'A page has been updated in :appName:',\n    'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\\'t be sent notifications for further edits to this page by the same editor.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Page Name:',\n    'detail_page_path' => 'Page Path:',\n    'detail_commenter' => 'Commenter:',\n    'detail_comment' => 'Comment:',\n    'detail_created_by' => 'Created By:',\n    'detail_updated_by' => 'Updated By:',\n\n    'action_view_comment' => 'View Comment',\n    'action_view_page' => 'View Page',\n\n    'footer_reason' => 'This notification was sent to you because :link cover this type of activity for this item.',\n    'footer_reason_link' => 'your notification preferences',\n];\n"
  },
  {
    "path": "lang/ku/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Previous',\n    'next'     => 'Next &raquo;',\n\n];\n"
  },
  {
    "path": "lang/ku/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Passwords must be at least eight characters and match the confirmation.',\n    'user' => \"We can't find a user with that e-mail address.\",\n    'token' => 'The password reset token is invalid for this email address.',\n    'sent' => 'We have e-mailed your password reset link!',\n    'reset' => 'Your password has been reset!',\n\n];\n"
  },
  {
    "path": "lang/ku/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'My Account',\n\n    'shortcuts' => 'Shortcuts',\n    'shortcuts_interface' => 'UI Shortcut Preferences',\n    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',\n    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',\n    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',\n    'shortcuts_section_navigation' => 'Navigation',\n    'shortcuts_section_actions' => 'Common Actions',\n    'shortcuts_save' => 'Save Shortcuts',\n    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing \"?\" which will highlight the available shortcuts for actions currently visible on the screen.',\n    'shortcuts_update_success' => 'Shortcut preferences have been updated!',\n    'shortcuts_overview_desc' => 'Manage keyboard shortcuts you can use to navigate the system user interface.',\n\n    'notifications' => 'Notification Preferences',\n    'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.',\n    'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own',\n    'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Notify upon replies to my comments',\n    'notifications_save' => 'Save Preferences',\n    'notifications_update_success' => 'Notification preferences have been updated!',\n    'notifications_watched' => 'Watched & Ignored Items',\n    'notifications_watched_desc' => 'Below are the items that have custom watch preferences applied. To update your preferences for these, view the item then find the watch options in the sidebar.',\n\n    'auth' => 'Access & Security',\n    'auth_change_password' => 'Change Password',\n    'auth_change_password_desc' => 'Change the password you use to log-in to the application. This must be at least 8 characters long.',\n    'auth_change_password_success' => 'Password has been updated!',\n\n    'profile' => 'Profile Details',\n    'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.',\n    'profile_view_public' => 'View Public Profile',\n    'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.',\n    'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.',\n    'profile_email_no_permission' => 'Unfortunately you don\\'t have permission to change your email address. If you want to change this, you\\'d need to ask an administrator to change this for you.',\n    'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.',\n    'profile_admin_options' => 'Administrator Options',\n    'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the \"Settings > Users\" area of the application.',\n\n    'delete_account' => 'Delete Account',\n    'delete_my_account' => 'Delete My Account',\n    'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\\'ve created, such as created pages and uploaded images, will remain.',\n    'delete_my_account_warning' => 'Are you sure you want to delete your account?',\n];\n"
  },
  {
    "path": "lang/ku/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Settings',\n    'settings_save' => 'Save Settings',\n    'system_version' => 'System Version',\n    'categories' => 'Categories',\n\n    // App Settings\n    'app_customization' => 'Customization',\n    'app_features_security' => 'Features & Security',\n    'app_name' => 'Application Name',\n    'app_name_desc' => 'This name is shown in the header and in any system-sent emails.',\n    'app_name_header' => 'Show name in header',\n    'app_public_access' => 'Public Access',\n    'app_public_access_desc' => 'Enabling this option will allow visitors, that are not logged-in, to access content in your BookStack instance.',\n    'app_public_access_desc_guest' => 'Access for public visitors can be controlled through the \"Guest\" user.',\n    'app_public_access_toggle' => 'Allow public access',\n    'app_public_viewing' => 'Allow public viewing?',\n    'app_secure_images' => 'Higher Security Image Uploads',\n    'app_secure_images_toggle' => 'Enable higher security image uploads',\n    'app_secure_images_desc' => 'For performance reasons, all images are public. This option adds a random, hard-to-guess string in front of image urls. Ensure directory indexes are not enabled to prevent easy access.',\n    'app_default_editor' => 'Default Page Editor',\n    'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.',\n    'app_custom_html' => 'Custom HTML Head Content',\n    'app_custom_html_desc' => 'Any content added here will be inserted into the bottom of the <head> section of every page. This is handy for overriding styles or adding analytics code.',\n    'app_custom_html_disabled_notice' => 'Custom HTML head content is disabled on this settings page to ensure any breaking changes can be reverted.',\n    'app_logo' => 'Application Logo',\n    'app_logo_desc' => 'This is used in the application header bar, among other areas. This image should be 86px in height. Large images will be scaled down.',\n    'app_icon' => 'Application Icon',\n    'app_icon_desc' => 'This icon is used for browser tabs and shortcut icons. This should be a 256px square PNG image.',\n    'app_homepage' => 'Application Homepage',\n    'app_homepage_desc' => 'Select a view to show on the homepage instead of the default view. Page permissions are ignored for selected pages.',\n    'app_homepage_select' => 'Select a page',\n    'app_footer_links' => 'Footer Links',\n    'app_footer_links_desc' => 'Add links to show within the site footer. These will be displayed at the bottom of most pages, including those that do not require login. You can use a label of \"trans::<key>\" to use system-defined translations. For example: Using \"trans::common.privacy_policy\" will provide the translated text \"Privacy Policy\" and \"trans::common.terms_of_service\" will provide the translated text \"Terms of Service\".',\n    'app_footer_links_label' => 'Link Label',\n    'app_footer_links_url' => 'Link URL',\n    'app_footer_links_add' => 'Add Footer Link',\n    'app_disable_comments' => 'Disable Comments',\n    'app_disable_comments_toggle' => 'Disable comments',\n    'app_disable_comments_desc' => 'Disables comments across all pages in the application. <br> Existing comments are not shown.',\n\n    // Color settings\n    'color_scheme' => 'Application Color Scheme',\n    'color_scheme_desc' => 'Set the colors to use in the application user interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.',\n    'ui_colors_desc' => 'Set the application primary color and default link color. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the application interface.',\n    'app_color' => 'Primary Color',\n    'link_color' => 'Default Link Color',\n    'content_colors_desc' => 'Set colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.',\n    'bookshelf_color' => 'Shelf Color',\n    'book_color' => 'Book Color',\n    'chapter_color' => 'Chapter Color',\n    'page_color' => 'Page Color',\n    'page_draft_color' => 'Page Draft Color',\n\n    // Registration Settings\n    'reg_settings' => 'Registration',\n    'reg_enable' => 'Enable Registration',\n    'reg_enable_toggle' => 'Enable registration',\n    'reg_enable_desc' => 'When registration is enabled user will be able to sign themselves up as an application user. Upon registration they are given a single, default user role.',\n    'reg_default_role' => 'Default user role after registration',\n    'reg_enable_external_warning' => 'The option above is ignored while external LDAP or SAML authentication is active. User accounts for non-existing members will be auto-created if authentication, against the external system in use, is successful.',\n    'reg_email_confirmation' => 'Email Confirmation',\n    'reg_email_confirmation_toggle' => 'Require email confirmation',\n    'reg_confirm_email_desc' => 'If domain restriction is used then email confirmation will be required and this option will be ignored.',\n    'reg_confirm_restrict_domain' => 'Domain Restriction',\n    'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.',\n    'reg_confirm_restrict_domain_placeholder' => 'No restriction set',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Maintenance',\n    'maint_image_cleanup' => 'Cleanup Images',\n    'maint_image_cleanup_desc' => 'Scans page & revision content to check which images and drawings are currently in use and which images are redundant. Ensure you create a full database and image backup before running this.',\n    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',\n    'maint_image_cleanup_run' => 'Run Cleanup',\n    'maint_image_cleanup_warning' => ':count potentially unused images were found. Are you sure you want to delete these images?',\n    'maint_image_cleanup_success' => ':count potentially unused images found and deleted!',\n    'maint_image_cleanup_nothing_found' => 'No unused images found, Nothing deleted!',\n    'maint_send_test_email' => 'Send a Test Email',\n    'maint_send_test_email_desc' => 'This sends a test email to your email address specified in your profile.',\n    'maint_send_test_email_run' => 'Send test email',\n    'maint_send_test_email_success' => 'Email sent to :address',\n    'maint_send_test_email_mail_subject' => 'Test Email',\n    'maint_send_test_email_mail_greeting' => 'Email delivery seems to work!',\n    'maint_send_test_email_mail_text' => 'Congratulations! As you received this email notification, your email settings seem to be configured properly.',\n    'maint_recycle_bin_desc' => 'Deleted shelves, books, chapters & pages are sent to the recycle bin so they can be restored or permanently deleted. Older items in the recycle bin may be automatically removed after a while depending on system configuration.',\n    'maint_recycle_bin_open' => 'Open Recycle Bin',\n    'maint_regen_references' => 'Regenerate References',\n    'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',\n    'maint_regen_references_success' => 'Reference index has been regenerated!',\n    'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Recycle Bin',\n    'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',\n    'recycle_bin_deleted_item' => 'Deleted Item',\n    'recycle_bin_deleted_parent' => 'Parent',\n    'recycle_bin_deleted_by' => 'Deleted By',\n    'recycle_bin_deleted_at' => 'Deletion Time',\n    'recycle_bin_permanently_delete' => 'Permanently Delete',\n    'recycle_bin_restore' => 'Restore',\n    'recycle_bin_contents_empty' => 'The recycle bin is currently empty',\n    'recycle_bin_empty' => 'Empty Recycle Bin',\n    'recycle_bin_empty_confirm' => 'This will permanently destroy all items in the recycle bin including content contained within each item. Are you sure you want to empty the recycle bin?',\n    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?',\n    'recycle_bin_destroy_list' => 'Items to be Destroyed',\n    'recycle_bin_restore_list' => 'Items to be Restored',\n    'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.',\n    'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.',\n    'recycle_bin_restore_parent' => 'Restore Parent',\n    'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',\n    'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',\n\n    // Audit Log\n    'audit' => 'Audit Log',\n    'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',\n    'audit_event_filter' => 'Event Filter',\n    'audit_event_filter_no_filter' => 'No Filter',\n    'audit_deleted_item' => 'Deleted Item',\n    'audit_deleted_item_name' => 'Name: :name',\n    'audit_table_user' => 'User',\n    'audit_table_event' => 'Event',\n    'audit_table_related' => 'Related Item or Detail',\n    'audit_table_ip' => 'IP Address',\n    'audit_table_date' => 'Activity Date',\n    'audit_date_from' => 'Date Range From',\n    'audit_date_to' => 'Date Range To',\n\n    // Role Settings\n    'roles' => 'Roles',\n    'role_user_roles' => 'User Roles',\n    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',\n    'roles_x_users_assigned' => ':count user assigned|:count users assigned',\n    'roles_x_permissions_provided' => ':count permission|:count permissions',\n    'roles_assigned_users' => 'Assigned Users',\n    'roles_permissions_provided' => 'Provided Permissions',\n    'role_create' => 'Create New Role',\n    'role_delete' => 'Delete Role',\n    'role_delete_confirm' => 'This will delete the role with the name \\':roleName\\'.',\n    'role_delete_users_assigned' => 'This role has :userCount users assigned to it. If you would like to migrate the users from this role select a new role below.',\n    'role_delete_no_migration' => \"Don't migrate users\",\n    'role_delete_sure' => 'Are you sure you want to delete this role?',\n    'role_edit' => 'Edit Role',\n    'role_details' => 'Role Details',\n    'role_name' => 'Role Name',\n    'role_desc' => 'Short Description of Role',\n    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',\n    'role_external_auth_id' => 'External Authentication IDs',\n    'role_system' => 'System Permissions',\n    'role_manage_users' => 'Manage users',\n    'role_manage_roles' => 'Manage roles & role permissions',\n    'role_manage_entity_permissions' => 'Manage all book, chapter & page permissions',\n    'role_manage_own_entity_permissions' => 'Manage permissions on own book, chapter & pages',\n    'role_manage_page_templates' => 'Manage page templates',\n    'role_access_api' => 'Access system API',\n    'role_manage_settings' => 'Manage app settings',\n    'role_export_content' => 'Export content',\n    'role_import_content' => 'Import content',\n    'role_editor_change' => 'Change page editor',\n    'role_notifications' => 'Receive & manage notifications',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Asset Permissions',\n    'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',\n    'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',\n    'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.',\n    'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'All',\n    'role_own' => 'Own',\n    'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',\n    'role_save' => 'Save Role',\n    'role_users' => 'Users in this role',\n    'role_users_none' => 'No users are currently assigned to this role',\n\n    // Users\n    'users' => 'Users',\n    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',\n    'user_profile' => 'User Profile',\n    'users_add_new' => 'Add New User',\n    'users_search' => 'Search Users',\n    'users_latest_activity' => 'Latest Activity',\n    'users_details' => 'User Details',\n    'users_details_desc' => 'Set a display name and an email address for this user. The email address will be used for logging into the application.',\n    'users_details_desc_no_email' => 'Set a display name for this user so others can recognise them.',\n    'users_role' => 'User Roles',\n    'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.',\n    'users_password' => 'User Password',\n    'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',\n    'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.',\n    'users_send_invite_option' => 'Send user invite email',\n    'users_external_auth_id' => 'External Authentication ID',\n    'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.',\n    'users_password_warning' => 'Only fill the below if you would like to change the password for this user.',\n    'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.',\n    'users_delete' => 'Delete User',\n    'users_delete_named' => 'Delete user :userName',\n    'users_delete_warning' => 'This will fully delete this user with the name \\':userName\\' from the system.',\n    'users_delete_confirm' => 'Are you sure you want to delete this user?',\n    'users_migrate_ownership' => 'Migrate Ownership',\n    'users_migrate_ownership_desc' => 'Select a user here if you want another user to become the owner of all items currently owned by this user.',\n    'users_none_selected' => 'No user selected',\n    'users_edit' => 'Edit User',\n    'users_edit_profile' => 'Edit Profile',\n    'users_avatar' => 'User Avatar',\n    'users_avatar_desc' => 'Select an image to represent this user. This should be approx 256px square.',\n    'users_preferred_language' => 'Preferred Language',\n    'users_preferred_language_desc' => 'This option will change the language used for the user-interface of the application. This will not affect any user-created content.',\n    'users_social_accounts' => 'Social Accounts',\n    'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.',\n    'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not revoke previously authorized access. Revoke access from your profile settings on the connected social account.',\n    'users_social_connect' => 'Connect Account',\n    'users_social_disconnect' => 'Disconnect Account',\n    'users_social_status_connected' => 'Connected',\n    'users_social_status_disconnected' => 'Disconnected',\n    'users_social_connected' => ':socialAccount account was successfully attached to your profile.',\n    'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.',\n    'users_api_tokens' => 'API Tokens',\n    'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.',\n    'users_api_tokens_none' => 'No API tokens have been created for this user',\n    'users_api_tokens_create' => 'Create Token',\n    'users_api_tokens_expires' => 'Expires',\n    'users_api_tokens_docs' => 'API Documentation',\n    'users_mfa' => 'Multi-Factor Authentication',\n    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'users_mfa_x_methods' => ':count method configured|:count methods configured',\n    'users_mfa_configure' => 'Configure Methods',\n\n    // API Tokens\n    'user_api_token_create' => 'Create API Token',\n    'user_api_token_name' => 'Name',\n    'user_api_token_name_desc' => 'Give your token a readable name as a future reminder of its intended purpose.',\n    'user_api_token_expiry' => 'Expiry Date',\n    'user_api_token_expiry_desc' => 'Set a date at which this token expires. After this date, requests made using this token will no longer work. Leaving this field blank will set an expiry 100 years into the future.',\n    'user_api_token_create_secret_message' => 'Immediately after creating this token a \"Token ID\" & \"Token Secret\" will be generated and displayed. The secret will only be shown a single time so be sure to copy the value to somewhere safe and secure before proceeding.',\n    'user_api_token' => 'API Token',\n    'user_api_token_id' => 'Token ID',\n    'user_api_token_id_desc' => 'This is a non-editable system generated identifier for this token which will need to be provided in API requests.',\n    'user_api_token_secret' => 'Token Secret',\n    'user_api_token_secret_desc' => 'This is a system generated secret for this token which will need to be provided in API requests. This will only be displayed this one time so copy this value to somewhere safe and secure.',\n    'user_api_token_created' => 'Token created :timeAgo',\n    'user_api_token_updated' => 'Token updated :timeAgo',\n    'user_api_token_delete' => 'Delete Token',\n    'user_api_token_delete_warning' => 'This will fully delete this API token with the name \\':tokenName\\' from the system.',\n    'user_api_token_delete_confirm' => 'Are you sure you want to delete this API token?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',\n    'webhooks_x_trigger_events' => ':count trigger event|:count trigger events',\n    'webhooks_create' => 'Create New Webhook',\n    'webhooks_none_created' => 'No webhooks have yet been created.',\n    'webhooks_edit' => 'Edit Webhook',\n    'webhooks_save' => 'Save Webhook',\n    'webhooks_details' => 'Webhook Details',\n    'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',\n    'webhooks_events' => 'Webhook Events',\n    'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',\n    'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\\'t expose confidential content.',\n    'webhooks_events_all' => 'All system events',\n    'webhooks_name' => 'Webhook Name',\n    'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',\n    'webhooks_endpoint' => 'Webhook Endpoint',\n    'webhooks_active' => 'Webhook Active',\n    'webhook_events_table_header' => 'Events',\n    'webhooks_delete' => 'Delete Webhook',\n    'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \\':webhookName\\', from the system.',\n    'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?',\n    'webhooks_format_example' => 'Webhook Format Example',\n    'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The \"related_item\" and \"url\" properties are optional and will depend on the type of event triggered.',\n    'webhooks_status' => 'Webhook Status',\n    'webhooks_last_called' => 'Last Called:',\n    'webhooks_last_errored' => 'Last Errored:',\n    'webhooks_last_error_message' => 'Last Error Message:',\n\n    // Licensing\n    'licenses' => 'Licenses',\n    'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.',\n    'licenses_bookstack' => 'BookStack License',\n    'licenses_php' => 'PHP Library Licenses',\n    'licenses_js' => 'JavaScript Library Licenses',\n    'licenses_other' => 'Other Licenses',\n    'license_details' => 'License Details',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/ku/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => 'The :attribute must be accepted.',\n    'active_url'           => 'The :attribute is not a valid URL.',\n    'after'                => 'The :attribute must be a date after :date.',\n    'alpha'                => 'The :attribute may only contain letters.',\n    'alpha_dash'           => 'The :attribute may only contain letters, numbers, dashes and underscores.',\n    'alpha_num'            => 'The :attribute may only contain letters and numbers.',\n    'array'                => 'The :attribute must be an array.',\n    'backup_codes'         => 'The provided code is not valid or has already been used.',\n    'before'               => 'The :attribute must be a date before :date.',\n    'between'              => [\n        'numeric' => 'The :attribute must be between :min and :max.',\n        'file'    => 'The :attribute must be between :min and :max kilobytes.',\n        'string'  => 'The :attribute must be between :min and :max characters.',\n        'array'   => 'The :attribute must have between :min and :max items.',\n    ],\n    'boolean'              => 'The :attribute field must be true or false.',\n    'confirmed'            => 'The :attribute confirmation does not match.',\n    'date'                 => 'The :attribute is not a valid date.',\n    'date_format'          => 'The :attribute does not match the format :format.',\n    'different'            => 'The :attribute and :other must be different.',\n    'digits'               => 'The :attribute must be :digits digits.',\n    'digits_between'       => 'The :attribute must be between :min and :max digits.',\n    'email'                => 'The :attribute must be a valid email address.',\n    'ends_with' => 'The :attribute must end with one of the following: :values',\n    'file'                 => 'The :attribute must be provided as a valid file.',\n    'filled'               => 'The :attribute field is required.',\n    'gt'                   => [\n        'numeric' => 'The :attribute must be greater than :value.',\n        'file'    => 'The :attribute must be greater than :value kilobytes.',\n        'string'  => 'The :attribute must be greater than :value characters.',\n        'array'   => 'The :attribute must have more than :value items.',\n    ],\n    'gte'                  => [\n        'numeric' => 'The :attribute must be greater than or equal :value.',\n        'file'    => 'The :attribute must be greater than or equal :value kilobytes.',\n        'string'  => 'The :attribute must be greater than or equal :value characters.',\n        'array'   => 'The :attribute must have :value items or more.',\n    ],\n    'exists'               => 'The selected :attribute is invalid.',\n    'image'                => 'The :attribute must be an image.',\n    'image_extension'      => 'The :attribute must have a valid & supported image extension.',\n    'in'                   => 'The selected :attribute is invalid.',\n    'integer'              => 'The :attribute must be an integer.',\n    'ip'                   => 'The :attribute must be a valid IP address.',\n    'ipv4'                 => 'The :attribute must be a valid IPv4 address.',\n    'ipv6'                 => 'The :attribute must be a valid IPv6 address.',\n    'json'                 => 'The :attribute must be a valid JSON string.',\n    'lt'                   => [\n        'numeric' => 'The :attribute must be less than :value.',\n        'file'    => 'The :attribute must be less than :value kilobytes.',\n        'string'  => 'The :attribute must be less than :value characters.',\n        'array'   => 'The :attribute must have less than :value items.',\n    ],\n    'lte'                  => [\n        'numeric' => 'The :attribute must be less than or equal :value.',\n        'file'    => 'The :attribute must be less than or equal :value kilobytes.',\n        'string'  => 'The :attribute must be less than or equal :value characters.',\n        'array'   => 'The :attribute must not have more than :value items.',\n    ],\n    'max'                  => [\n        'numeric' => 'The :attribute may not be greater than :max.',\n        'file'    => 'The :attribute may not be greater than :max kilobytes.',\n        'string'  => 'The :attribute may not be greater than :max characters.',\n        'array'   => 'The :attribute may not have more than :max items.',\n    ],\n    'mimes'                => 'The :attribute must be a file of type: :values.',\n    'min'                  => [\n        'numeric' => 'The :attribute must be at least :min.',\n        'file'    => 'The :attribute must be at least :min kilobytes.',\n        'string'  => 'The :attribute must be at least :min characters.',\n        'array'   => 'The :attribute must have at least :min items.',\n    ],\n    'not_in'               => 'The selected :attribute is invalid.',\n    'not_regex'            => 'The :attribute format is invalid.',\n    'numeric'              => 'The :attribute must be a number.',\n    'regex'                => 'The :attribute format is invalid.',\n    'required'             => 'The :attribute field is required.',\n    'required_if'          => 'The :attribute field is required when :other is :value.',\n    'required_with'        => 'The :attribute field is required when :values is present.',\n    'required_with_all'    => 'The :attribute field is required when :values is present.',\n    'required_without'     => 'The :attribute field is required when :values is not present.',\n    'required_without_all' => 'The :attribute field is required when none of :values are present.',\n    'same'                 => 'The :attribute and :other must match.',\n    'safe_url'             => 'The provided link may not be safe.',\n    'size'                 => [\n        'numeric' => 'The :attribute must be :size.',\n        'file'    => 'The :attribute must be :size kilobytes.',\n        'string'  => 'The :attribute must be :size characters.',\n        'array'   => 'The :attribute must contain :size items.',\n    ],\n    'string'               => 'The :attribute must be a string.',\n    'timezone'             => 'The :attribute must be a valid zone.',\n    'totp'                 => 'The provided code is not valid or has expired.',\n    'unique'               => 'The :attribute has already been taken.',\n    'url'                  => 'The :attribute format is invalid.',\n    'uploaded'             => 'The file could not be uploaded. The server may not accept files of this size.',\n\n    'zip_file' => 'The :attribute needs to reference a file within the ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',\n    'zip_model_expected' => 'Data object expected but \":type\" found.',\n    'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Password confirmation required',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/lt/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'sukurtas puslapis',\n    'page_create_notification'    => 'Page successfully created',\n    'page_update'                 => 'atnaujintas puslapis',\n    'page_update_notification'    => 'Page successfully updated',\n    'page_delete'                 => 'ištrintas puslapis',\n    'page_delete_notification'    => 'Page successfully deleted',\n    'page_restore'                => 'atkurtas puslapis',\n    'page_restore_notification'   => 'Page successfully restored',\n    'page_move'                   => 'perkeltas puslapis',\n    'page_move_notification'      => 'Page successfully moved',\n\n    // Chapters\n    'chapter_create'              => 'sukurtas skyrius',\n    'chapter_create_notification' => 'Chapter successfully created',\n    'chapter_update'              => 'atnaujintas skyrius',\n    'chapter_update_notification' => 'Chapter successfully updated',\n    'chapter_delete'              => 'ištrintas skyrius',\n    'chapter_delete_notification' => 'Chapter successfully deleted',\n    'chapter_move'                => 'perkeltas skyrius',\n    'chapter_move_notification' => 'Chapter successfully moved',\n\n    // Books\n    'book_create'                 => 'sukurta knyga',\n    'book_create_notification'    => 'Book successfully created',\n    'book_create_from_chapter'              => 'converted chapter to book',\n    'book_create_from_chapter_notification' => 'Chapter successfully converted to a book',\n    'book_update'                 => 'atnaujinta knyga',\n    'book_update_notification'    => 'Book successfully updated',\n    'book_delete'                 => 'ištrinta knyga',\n    'book_delete_notification'    => 'Book successfully deleted',\n    'book_sort'                   => 'surūšiuota knyga',\n    'book_sort_notification'      => 'Book successfully re-sorted',\n\n    // Bookshelves\n    'bookshelf_create'            => 'created shelf',\n    'bookshelf_create_notification'    => 'Shelf successfully created',\n    'bookshelf_create_from_book'    => 'converted book to shelf',\n    'bookshelf_create_from_book_notification'    => 'Book successfully converted to a shelf',\n    'bookshelf_update'                 => 'updated shelf',\n    'bookshelf_update_notification'    => 'Shelf successfully updated',\n    'bookshelf_delete'                 => 'deleted shelf',\n    'bookshelf_delete_notification'    => 'Shelf successfully deleted',\n\n    // Revisions\n    'revision_restore' => 'restored revision',\n    'revision_delete' => 'deleted revision',\n    'revision_delete_notification' => 'Revision successfully deleted',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" has been added to your favourites',\n    'favourite_remove_notification' => '\":name\" has been removed from your favourites',\n\n    // Watching\n    'watch_update_level_notification' => 'Watch preferences successfully updated',\n\n    // Auth\n    'auth_login' => 'logged in',\n    'auth_register' => 'registered as new user',\n    'auth_password_reset_request' => 'requested user password reset',\n    'auth_password_reset_update' => 'reset user password',\n    'mfa_setup_method' => 'configured MFA method',\n    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',\n    'mfa_remove_method' => 'removed MFA method',\n    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',\n\n    // Settings\n    'settings_update' => 'updated settings',\n    'settings_update_notification' => 'Settings successfully updated',\n    'maintenance_action_run' => 'ran maintenance action',\n\n    // Webhooks\n    'webhook_create' => 'created webhook',\n    'webhook_create_notification' => 'Webhook successfully created',\n    'webhook_update' => 'updated webhook',\n    'webhook_update_notification' => 'Webhook successfully updated',\n    'webhook_delete' => 'deleted webhook',\n    'webhook_delete_notification' => 'Webhook successfully deleted',\n\n    // Imports\n    'import_create' => 'created import',\n    'import_create_notification' => 'Import successfully uploaded',\n    'import_run' => 'updated import',\n    'import_run_notification' => 'Content successfully imported',\n    'import_delete' => 'deleted import',\n    'import_delete_notification' => 'Import successfully deleted',\n\n    // Users\n    'user_create' => 'created user',\n    'user_create_notification' => 'User successfully created',\n    'user_update' => 'updated user',\n    'user_update_notification' => 'User successfully updated',\n    'user_delete' => 'deleted user',\n    'user_delete_notification' => 'User successfully removed',\n\n    // API Tokens\n    'api_token_create' => 'created API token',\n    'api_token_create_notification' => 'API token successfully created',\n    'api_token_update' => 'updated API token',\n    'api_token_update_notification' => 'API token successfully updated',\n    'api_token_delete' => 'deleted API token',\n    'api_token_delete_notification' => 'API token successfully deleted',\n\n    // Roles\n    'role_create' => 'created role',\n    'role_create_notification' => 'Role successfully created',\n    'role_update' => 'updated role',\n    'role_update_notification' => 'Role successfully updated',\n    'role_delete' => 'deleted role',\n    'role_delete_notification' => 'Role successfully deleted',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'emptied recycle bin',\n    'recycle_bin_restore' => 'restored from recycle bin',\n    'recycle_bin_destroy' => 'removed from recycle bin',\n\n    // Comments\n    'commented_on'                => 'pakomentavo',\n    'comment_create'              => 'added comment',\n    'comment_update'              => 'updated comment',\n    'comment_delete'              => 'deleted comment',\n\n    // Sort Rules\n    'sort_rule_create' => 'created sort rule',\n    'sort_rule_create_notification' => 'Sort rule successfully created',\n    'sort_rule_update' => 'updated sort rule',\n    'sort_rule_update_notification' => 'Sort rule successfully updated',\n    'sort_rule_delete' => 'deleted sort rule',\n    'sort_rule_delete_notification' => 'Sort rule successfully deleted',\n\n    // Other\n    'permissions_update'          => 'atnaujinti leidimai',\n];\n"
  },
  {
    "path": "lang/lt/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Šie įgaliojimai neatitinka mūsų įrašų.',\n    'throttle' => 'Per daug prisijungimo bandymų. Prašome pabandyti dar kartą po :seconds sekundžių.',\n\n    // Login & Register\n    'sign_up' => 'Užsiregistruoti',\n    'log_in' => 'Prisijungti',\n    'log_in_with' => 'Prisijungti su :socialDriver',\n    'sign_up_with' => 'Užsiregistruoti su :socialDriver',\n    'logout' => 'Atsijungti',\n\n    'name' => 'Pavadinimas',\n    'username' => 'Vartotojo vardas',\n    'email' => 'Elektroninis paštas',\n    'password' => 'Slaptažodis',\n    'password_confirm' => 'Patvirtinti slaptažodį',\n    'password_hint' => 'Must be at least 8 characters',\n    'forgot_password' => 'Pamiršote slaptažodį?',\n    'remember_me' => 'Prisimink mane',\n    'ldap_email_hint' => 'Prašome įvesti elektroninį paštą, kad galėtume naudotis šia paskyra.',\n    'create_account' => 'Sukurti paskyrą',\n    'already_have_account' => 'Jau turite paskyrą?',\n    'dont_have_account' => 'Neturite paskyros?',\n    'social_login' => 'Socialinis prisijungimas',\n    'social_registration' => 'Socialinė registracija',\n    'social_registration_text' => 'Užsiregistruoti ir prisijungti naudojantis kita paslauga.',\n\n    'register_thanks' => 'Ačiū, kad užsiregistravote!',\n    'register_confirm' => 'Prašome patikrinti savo elektroninį paštą ir paspausti patvirtinimo mygtuką, kad gautumėte leidimą į :appName.',\n    'registrations_disabled' => 'Registracijos šiuo metu negalimos',\n    'registration_email_domain_invalid' => 'Elektroninio pašto domenas neturi prieigos prie šios programos',\n    'register_success' => 'Ačiū už prisijungimą! Dabar jūs užsiregistravote ir prisijungėte.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Attempting Login',\n    'auto_init_starting_desc' => 'We\\'re contacting your authentication system to start the login process. If there\\'s no progress after 5 seconds you can try clicking the link below.',\n    'auto_init_start_link' => 'Proceed with authentication',\n\n    // Password Reset\n    'reset_password' => 'Pakeisti slaptažodį',\n    'reset_password_send_instructions' => 'Įveskite savo elektroninį paštą žemiau ir jums bus išsiųstas elektroninis laiškas su slaptažodžio nustatymo nuoroda.',\n    'reset_password_send_button' => 'Atsiųsti atsatymo nuorodą',\n    'reset_password_sent' => 'Slaptažodžio nustatymo nuoroda bus išsiųsta :email jeigu elektroninio pašto adresas bus rastas sistemoje.',\n    'reset_password_success' => 'Jūsų slaptažodis buvo sėkmingai atnaujintas.',\n    'email_reset_subject' => 'Atnaujinti jūsų :appName slaptažodį',\n    'email_reset_text' => 'Šį laišką gaunate, nes mes gavome slaptažodžio atnaujinimo užklausą iš jūsų paskyros.',\n    'email_reset_not_requested' => 'Jeigu jums nereikia slaptažodžio atnaujinimo, tolimesnių veiksmų atlikti nereikia.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Patvirtinkite savo elektroninį paštą :appName',\n    'email_confirm_greeting' => 'Ačiū už prisijungimą prie :appName!',\n    'email_confirm_text' => 'Prašome patvirtinti savo elektroninio pašto adresą paspaudus mygtuką žemiau:',\n    'email_confirm_action' => 'Patvirtinkite elektroninį paštą',\n    'email_confirm_send_error' => 'Būtinas elektroninio laiško patviritnimas, bet sistema negali išsiųsti laiško. Susisiekite su administratoriumi, kad užtikrintumėte, jog elektroninis paštas atsinaujino teisingai.',\n    'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',\n    'email_confirm_resent' => 'Elektroninio pašto patvirtinimas persiųstas, prašome patikrinti pašto dėžutę.',\n    'email_confirm_thanks' => 'Thanks for confirming!',\n    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the \"Continue\" link below to proceed.',\n\n    'email_not_confirmed' => 'Elektroninis paštas nepatvirtintas',\n    'email_not_confirmed_text' => 'Jūsų elektroninis paštas dar vis nepatvirtintas.',\n    'email_not_confirmed_click_link' => 'Prašome paspausti nuorodą elektroniniame pašte, kuri buvo išsiųsta iš karto po registracijos.',\n    'email_not_confirmed_resend' => 'Jeigu nerandate elektroninio laiško, galite dar kartą išsiųsti patvirtinimo elektroninį laišką, pateikdami žemiau esančią formą.',\n    'email_not_confirmed_resend_button' => 'Persiųsti patvirtinimo laišką',\n\n    // User Invite\n    'user_invite_email_subject' => 'Jūs buvote pakviestas prisijungti prie :appName!',\n    'user_invite_email_greeting' => 'Paskyra buvo sukurta jums :appName.',\n    'user_invite_email_text' => 'Paspauskite mygtuką žemiau, kad sukurtumėte paskyros slaptažodį ir gautumėte prieigą:',\n    'user_invite_email_action' => 'Sukurti paskyros slaptažodį',\n    'user_invite_page_welcome' => 'Sveiki atvykę į :appName!',\n    'user_invite_page_text' => 'Norėdami galutinai pabaigti paskyrą ir gauti prieigą jums reikia nustatyti slaptažodį, kuris bus naudojamas prisijungiant prie :appName ateities vizitų metu.',\n    'user_invite_page_confirm_button' => 'Patvirtinti slaptažodį',\n    'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Setup Multi-Factor Authentication',\n    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'mfa_setup_configured' => 'Already configured',\n    'mfa_setup_reconfigure' => 'Reconfigure',\n    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',\n    'mfa_setup_action' => 'Setup',\n    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',\n    'mfa_option_totp_title' => 'Mobile App',\n    'mfa_option_totp_desc' => 'To use multi-factor authentication you\\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Backup Codes',\n    'mfa_option_backup_codes_desc' => 'Generates a set of one-time-use backup codes which you\\'ll enter on login to verify your identity. Make sure to store these in a safe & secure place.',\n    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',\n    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',\n    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\\'ll be able to use one of the codes as a second authentication mechanism.',\n    'mfa_gen_backup_codes_download' => 'Download Codes',\n    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',\n    'mfa_gen_totp_title' => 'Mobile App Setup',\n    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',\n    'mfa_gen_totp_verify_setup' => 'Verify Setup',\n    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',\n    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',\n    'mfa_verify_access' => 'Verify Access',\n    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\\'re granted access. Verify using one of your configured methods to continue.',\n    'mfa_verify_no_methods' => 'No Methods Configured',\n    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\\'ll need to set up at least one method before you gain access.',\n    'mfa_verify_use_totp' => 'Verify using a mobile app',\n    'mfa_verify_use_backup_codes' => 'Verify using a backup code',\n    'mfa_verify_backup_code' => 'Backup Code',\n    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',\n    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',\n    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',\n    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',\n];\n"
  },
  {
    "path": "lang/lt/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Atšaukti',\n    'close' => 'Uždaryti',\n    'confirm' => 'Patvirtinti',\n    'back' => 'Grįžti',\n    'save' => 'Išsaugoti',\n    'continue' => 'Praleisti',\n    'select' => 'Pasirinkti',\n    'toggle_all' => 'Perjungti visus',\n    'more' => 'Daugiau',\n\n    // Form Labels\n    'name' => 'Pavadinimas',\n    'description' => 'Apibūdinimas',\n    'role' => 'Vaidmuo',\n    'cover_image' => 'Viršelio nuotrauka',\n    'cover_image_description' => 'This image should be approximately 440x250px although it will be flexibly scaled & cropped to fit the user interface in different scenarios as required, so actual dimensions for display will differ.',\n\n    // Actions\n    'actions' => 'Veiksmai',\n    'view' => 'Rodyti',\n    'view_all' => 'Rodyti visus',\n    'new' => 'New',\n    'create' => 'Sukurti',\n    'update' => 'Atnaujinti',\n    'edit' => 'Redaguoti',\n    'archive' => 'Archive',\n    'unarchive' => 'Un-Archive',\n    'sort' => 'Rūšiuoti',\n    'move' => 'Perkelti',\n    'copy' => 'Kopijuoti',\n    'reply' => 'Atsakyti',\n    'delete' => 'Ištrinti',\n    'delete_confirm' => 'Patvirtinti ištrynimą',\n    'search' => 'Paieška',\n    'search_clear' => 'Išvalyti paiešką',\n    'reset' => 'Atsatyti',\n    'remove' => 'Pašalinti',\n    'add' => 'Pridėti',\n    'configure' => 'Configure',\n    'manage' => 'Manage',\n    'fullscreen' => 'Visas ekranas',\n    'favourite' => 'Favourite',\n    'unfavourite' => 'Unfavourite',\n    'next' => 'Next',\n    'previous' => 'Previous',\n    'filter_active' => 'Active Filter:',\n    'filter_clear' => 'Clear Filter',\n    'download' => 'Download',\n    'open_in_tab' => 'Open in Tab',\n    'open' => 'Open',\n\n    // Sort Options\n    'sort_options' => 'Rūšiuoti pasirinkimus',\n    'sort_direction_toggle' => 'Rūšiuoti krypties perjungimus',\n    'sort_ascending' => 'Rūšiuoti didėjančia tvarka',\n    'sort_descending' => 'Rūšiuoti mažėjančia tvarka',\n    'sort_name' => 'Pavadinimas',\n    'sort_default' => 'Numatytas',\n    'sort_created_at' => 'Sukurta data',\n    'sort_updated_at' => 'Atnaujinta data',\n\n    // Misc\n    'deleted_user' => 'Ištrinti naudotoją',\n    'no_activity' => 'Nėra veiklų',\n    'no_items' => 'Nėra elementų',\n    'back_to_top' => 'Grįžti į pradžią',\n    'skip_to_main_content' => 'Skip to main content',\n    'toggle_details' => 'Perjungti detales',\n    'toggle_thumbnails' => 'Perjungti miniatūras',\n    'details' => 'Detalės',\n    'grid_view' => 'Tinklelio vaizdas',\n    'list_view' => 'Sąrašas',\n    'default' => 'Numatytas',\n    'breadcrumb' => 'Duonos rėžis',\n    'status' => 'Status',\n    'status_active' => 'Active',\n    'status_inactive' => 'Inactive',\n    'never' => 'Never',\n    'none' => 'None',\n\n    // Header\n    'homepage' => 'Homepage',\n    'header_menu_expand' => 'Plėsti antraštės meniu',\n    'profile_menu' => 'Profilio meniu',\n    'view_profile' => 'Rodyti porofilį',\n    'edit_profile' => 'Redaguoti profilį',\n    'dark_mode' => 'Tamsus rėžimas',\n    'light_mode' => 'Šviesus rėžimas',\n    'global_search' => 'Global Search',\n\n    // Layout tabs\n    'tab_info' => 'Informacija',\n    'tab_info_label' => 'Skirtukas: Rodyti antrinę informaciją',\n    'tab_content' => 'Turinys',\n    'tab_content_label' => 'Skirtukas: Rodyti pirminę informaciją',\n\n    // Email Content\n    'email_action_help' => 'Jeigu kyla problemų spaudžiant :actionText: mygtuką, nukopijuokite ir įklijuokite URL į savo naršyklę.',\n    'email_rights' => 'Visos teisės rezervuotos',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Privatumo politika',\n    'terms_of_service' => 'Paslaugų teikimo paslaugos',\n\n    // OpenSearch\n    'opensearch_description' => 'Search :appName',\n];\n"
  },
  {
    "path": "lang/lt/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Nuotraukų pasirinkimas',\n    'image_list' => 'Image List',\n    'image_details' => 'Image Details',\n    'image_upload' => 'Upload Image',\n    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',\n    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the \"Upload Image\" button above.',\n    'image_all' => 'Visi',\n    'image_all_title' => 'Rodyti visas nuotraukas',\n    'image_book_title' => 'Peržiūrėti nuotraukas, įkeltas į šią knygą',\n    'image_page_title' => 'Peržiūrėti nuotraukas, įkeltas į šį puslapį',\n    'image_search_hint' => 'Ieškoti pagal nuotraukos pavadinimą',\n    'image_uploaded' => 'Įkelta :uploadedDate',\n    'image_uploaded_by' => 'Uploaded by :userName',\n    'image_uploaded_to' => 'Uploaded to :pageLink',\n    'image_updated' => 'Updated :updateDate',\n    'image_load_more' => 'Rodyti daugiau',\n    'image_image_name' => 'Nuotraukos pavadinimas',\n    'image_delete_used' => 'Ši nuotrauka yra naudojama puslapyje žemiau.',\n    'image_delete_confirm_text' => 'Ar jūs esate tikri, kad norite ištrinti šią nuotrauką?',\n    'image_select_image' => 'Pasirinkti nuotrauką',\n    'image_dropzone' => 'Tempkite nuotraukas arba spauskite šia, kad įkeltumėte',\n    'image_dropzone_drop' => 'Drop images here to upload',\n    'images_deleted' => 'Nuotraukos ištrintos',\n    'image_preview' => 'Nuotraukų peržiūra',\n    'image_upload_success' => 'Nuotrauka įkelta sėkmingai',\n    'image_update_success' => 'Nuotraukos detalės sėkmingai atnaujintos',\n    'image_delete_success' => 'Nuotrauka sėkmingai ištrinti',\n    'image_replace' => 'Replace Image',\n    'image_replace_success' => 'Image file successfully updated',\n    'image_rebuild_thumbs' => 'Regenerate Size Variations',\n    'image_rebuild_thumbs_success' => 'Image size variations successfully rebuilt!',\n\n    // Code Editor\n    'code_editor' => 'Redaguoti kodą',\n    'code_language' => 'Kodo kalba',\n    'code_content' => 'Kodo turinys',\n    'code_session_history' => 'Sesijos istorija',\n    'code_save' => 'Išsaugoti kodą',\n];\n"
  },
  {
    "path": "lang/lt/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'General',\n    'advanced' => 'Advanced',\n    'none' => 'None',\n    'cancel' => 'Cancel',\n    'save' => 'Save',\n    'close' => 'Close',\n    'apply' => 'Apply',\n    'undo' => 'Undo',\n    'redo' => 'Redo',\n    'left' => 'Left',\n    'center' => 'Center',\n    'right' => 'Right',\n    'top' => 'Top',\n    'middle' => 'Middle',\n    'bottom' => 'Bottom',\n    'width' => 'Width',\n    'height' => 'Height',\n    'More' => 'More',\n    'select' => 'Select...',\n\n    // Toolbar\n    'formats' => 'Formats',\n    'header_large' => 'Large Header',\n    'header_medium' => 'Medium Header',\n    'header_small' => 'Small Header',\n    'header_tiny' => 'Tiny Header',\n    'paragraph' => 'Paragraph',\n    'blockquote' => 'Blockquote',\n    'inline_code' => 'Inline code',\n    'callouts' => 'Callouts',\n    'callout_information' => 'Informacija',\n    'callout_success' => 'Success',\n    'callout_warning' => 'Warning',\n    'callout_danger' => 'Danger',\n    'bold' => 'Bold',\n    'italic' => 'Italic',\n    'underline' => 'Underline',\n    'strikethrough' => 'Strikethrough',\n    'superscript' => 'Superscript',\n    'subscript' => 'Subscript',\n    'text_color' => 'Text color',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Custom color',\n    'remove_color' => 'Remove color',\n    'background_color' => 'Background color',\n    'align_left' => 'Align left',\n    'align_center' => 'Align center',\n    'align_right' => 'Align right',\n    'align_justify' => 'Justify',\n    'list_bullet' => 'Bullet list',\n    'list_numbered' => 'Numbered list',\n    'list_task' => 'Task list',\n    'indent_increase' => 'Increase indent',\n    'indent_decrease' => 'Decrease indent',\n    'table' => 'Lentelė',\n    'insert_image' => 'Insert image',\n    'insert_image_title' => 'Insert/Edit Image',\n    'insert_link' => 'Insert/edit link',\n    'insert_link_title' => 'Insert/Edit Link',\n    'insert_horizontal_line' => 'Insert horizontal line',\n    'insert_code_block' => 'Insert code block',\n    'edit_code_block' => 'Edit code block',\n    'insert_drawing' => 'Insert/edit drawing',\n    'drawing_manager' => 'Drawing manager',\n    'insert_media' => 'Insert/edit media',\n    'insert_media_title' => 'Insert/Edit Media',\n    'clear_formatting' => 'Clear formatting',\n    'source_code' => 'Source code',\n    'source_code_title' => 'Source Code',\n    'fullscreen' => 'Fullscreen',\n    'image_options' => 'Image options',\n\n    // Tables\n    'table_properties' => 'Table properties',\n    'table_properties_title' => 'Table Properties',\n    'delete_table' => 'Delete table',\n    'table_clear_formatting' => 'Clear table formatting',\n    'resize_to_contents' => 'Resize to contents',\n    'row_header' => 'Row header',\n    'insert_row_before' => 'Insert row before',\n    'insert_row_after' => 'Insert row after',\n    'delete_row' => 'Delete row',\n    'insert_column_before' => 'Insert column before',\n    'insert_column_after' => 'Insert column after',\n    'delete_column' => 'Delete column',\n    'table_cell' => 'Cell',\n    'table_row' => 'Row',\n    'table_column' => 'Column',\n    'cell_properties' => 'Cell properties',\n    'cell_properties_title' => 'Cell Properties',\n    'cell_type' => 'Cell type',\n    'cell_type_cell' => 'Cell',\n    'cell_scope' => 'Scope',\n    'cell_type_header' => 'Header cell',\n    'merge_cells' => 'Merge cells',\n    'split_cell' => 'Split cell',\n    'table_row_group' => 'Row Group',\n    'table_column_group' => 'Column Group',\n    'horizontal_align' => 'Horizontal align',\n    'vertical_align' => 'Vertical align',\n    'border_width' => 'Border width',\n    'border_style' => 'Border style',\n    'border_color' => 'Border color',\n    'row_properties' => 'Row properties',\n    'row_properties_title' => 'Row Properties',\n    'cut_row' => 'Cut row',\n    'copy_row' => 'Copy row',\n    'paste_row_before' => 'Paste row before',\n    'paste_row_after' => 'Paste row after',\n    'row_type' => 'Row type',\n    'row_type_header' => 'Header',\n    'row_type_body' => 'Body',\n    'row_type_footer' => 'Footer',\n    'alignment' => 'Alignment',\n    'cut_column' => 'Cut column',\n    'copy_column' => 'Copy column',\n    'paste_column_before' => 'Paste column before',\n    'paste_column_after' => 'Paste column after',\n    'cell_padding' => 'Cell padding',\n    'cell_spacing' => 'Cell spacing',\n    'caption' => 'Caption',\n    'show_caption' => 'Show caption',\n    'constrain' => 'Constrain proportions',\n    'cell_border_solid' => 'Solid',\n    'cell_border_dotted' => 'Dotted',\n    'cell_border_dashed' => 'Dashed',\n    'cell_border_double' => 'Double',\n    'cell_border_groove' => 'Groove',\n    'cell_border_ridge' => 'Ridge',\n    'cell_border_inset' => 'Inset',\n    'cell_border_outset' => 'Outset',\n    'cell_border_none' => 'None',\n    'cell_border_hidden' => 'Hidden',\n\n    // Images, links, details/summary & embed\n    'source' => 'Source',\n    'alt_desc' => 'Alternative description',\n    'embed' => 'Embed',\n    'paste_embed' => 'Paste your embed code below:',\n    'url' => 'URL',\n    'text_to_display' => 'Text to display',\n    'title' => 'Title',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Atverti nuorodą',\n    'open_link_in' => 'Atverti nuorodą...',\n    'open_link_current' => 'Current window',\n    'open_link_new' => 'Naujame lange',\n    'remove_link' => 'Pašalinti nuorodą',\n    'insert_collapsible' => 'Insert collapsible block',\n    'collapsible_unwrap' => 'Unwrap',\n    'edit_label' => 'Edit label',\n    'toggle_open_closed' => 'Toggle open/closed',\n    'collapsible_edit' => 'Edit collapsible block',\n    'toggle_label' => 'Toggle label',\n\n    // About view\n    'about' => 'Apie redaktorių',\n    'about_title' => 'Apie WYSIWYG redaktorių',\n    'editor_license' => 'Editor License & Copyright',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under the MIT license.',\n    'editor_tiny_license_link' => 'The copyright and license details of TinyMCE can be found here.',\n    'save_continue' => 'Išsaugoti puslapį ir tęsti',\n    'callouts_cycle' => '(Keep pressing to toggle through types)',\n    'link_selector' => 'Link to content',\n    'shortcuts' => 'Shortcuts',\n    'shortcut' => 'Shortcut',\n    'shortcuts_intro' => 'The following shortcuts are available in the editor:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Description',\n];\n"
  },
  {
    "path": "lang/lt/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Neseniai sukurtas',\n    'recently_created_pages' => 'Neseniai sukurti puslapiai',\n    'recently_updated_pages' => 'Neseniai atnaujinti puslapiai',\n    'recently_created_chapters' => 'Neseniai sukurti skyriai',\n    'recently_created_books' => 'Neseniai sukurtos knygos',\n    'recently_created_shelves' => 'Neseniai sukurtos lentynos',\n    'recently_update' => 'Neseniai atnaujinta',\n    'recently_viewed' => 'Neseniai peržiūrėta',\n    'recent_activity' => 'Paskutiniai veiksmai',\n    'create_now' => 'Sukurti vieną dabar',\n    'revisions' => 'Pataisymai',\n    'meta_revision' => 'Pataisymas #:revisionCount',\n    'meta_created' => 'Sukurta :timeLength',\n    'meta_created_name' => 'Sukurta :timeLength naudotojo :user',\n    'meta_updated' => 'Atnaujintas :timeLength',\n    'meta_updated_name' => 'Atnaujinta :timeLength naudotojo :user',\n    'meta_owned_name' => 'Priklauso :user',\n    'meta_reference_count' => 'Referenced by :count item|Referenced by :count items',\n    'entity_select' => 'Pasirinkti subjektą',\n    'entity_select_lack_permission' => 'You don\\'t have the required permissions to select this item',\n    'images' => 'Nuotraukos',\n    'my_recent_drafts' => 'Naujausi išsaugoti juodraščiai',\n    'my_recently_viewed' => 'Neseniai peržiūrėti',\n    'my_most_viewed_favourites' => 'My Most Viewed Favourites',\n    'my_favourites' => 'Mėgstamiausi',\n    'no_pages_viewed' => 'Jūs neperžiūrėjote nei vieno puslapio',\n    'no_pages_recently_created' => 'Nebuvos sukurta jokių puslapių',\n    'no_pages_recently_updated' => 'Nebuvo atnaujinta jokių puslapių',\n    'export' => 'Eksportuoti',\n    'export_html' => 'Sudėtinis žiniatinklio failas',\n    'export_pdf' => 'PDF failas',\n    'export_text' => 'Paprastas failo tekstas',\n    'export_md' => 'Markdown File',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Default Page Template',\n    'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',\n    'default_template_select' => 'Select a template page',\n    'import' => 'Import',\n    'import_validate' => 'Validate Import',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'Select ZIP file to upload',\n    'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',\n    'import_pending' => 'Pending Imports',\n    'import_pending_none' => 'No imports have been started.',\n    'import_continue' => 'Continue Import',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Run Import',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Uploaded by',\n    'import_location' => 'Import Location',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'Import Errors',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Leidimai',\n    'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.',\n    'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.',\n    'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.',\n    'permissions_save' => 'Išsaugoti leidimus',\n    'permissions_owner' => 'Savininkas',\n    'permissions_role_everyone_else' => 'Everyone Else',\n    'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',\n    'permissions_role_override' => 'Override permissions for role',\n    'permissions_inherit_defaults' => 'Inherit defaults',\n\n    // Search\n    'search_results' => 'Ieškoti rezultatų',\n    'search_total_results_found' => ':count rastas rezultatas|:count iš viso rezultatų rasta',\n    'search_clear' => 'Išvalyti paiešką',\n    'search_no_pages' => 'Nėra puslapių pagal šią paiešką',\n    'search_for_term' => 'Ieškoti pagal :term',\n    'search_more' => 'Daugiau rezultatų',\n    'search_advanced' => 'Išplėstinė paieška',\n    'search_terms' => 'Ieškoti terminų',\n    'search_content_type' => 'Turinio tipas',\n    'search_exact_matches' => 'Tikslūs atitikmenys',\n    'search_tags' => 'Žymių paieškos',\n    'search_options' => 'Parinktys',\n    'search_viewed_by_me' => 'Mano peržiūrėta',\n    'search_not_viewed_by_me' => 'Mano neperžiūrėta',\n    'search_permissions_set' => 'Nustatyti leidimus',\n    'search_created_by_me' => 'Mano sukurta',\n    'search_updated_by_me' => 'Mano atnaujinimas',\n    'search_owned_by_me' => 'Priklauso man',\n    'search_date_options' => 'Datos parinktys',\n    'search_updated_before' => 'Atnaujinta prieš',\n    'search_updated_after' => 'Atnaujinta po',\n    'search_created_before' => 'Sukurta prieš',\n    'search_created_after' => 'Sukurta po',\n    'search_set_date' => 'Nustatyti datą',\n    'search_update' => 'Atnaujinti paiešką',\n\n    // Shelves\n    'shelf' => 'Lentyna',\n    'shelves' => 'Lentynos',\n    'x_shelves' => ':count lentyna|:count lentynos',\n    'shelves_empty' => 'Nebuvo sukurtos jokios lentynos',\n    'shelves_create' => 'Sukurti naują lentyną',\n    'shelves_popular' => 'Populiarios lentynos',\n    'shelves_new' => 'Naujos lentynos',\n    'shelves_new_action' => 'Nauja lentyna',\n    'shelves_popular_empty' => 'Populiariausios knygos pasirodys čia.',\n    'shelves_new_empty' => 'Visai neseniai sukurtos lentynos pasirodys čia.',\n    'shelves_save' => 'Išsaugoti lenyną',\n    'shelves_books' => 'Knygos šioje lentynoje',\n    'shelves_add_books' => 'Pridėti knygas į šią lentyną',\n    'shelves_drag_books' => 'Drag books below to add them to this shelf',\n    'shelves_empty_contents' => 'Ši lentyną neturi jokių pridėtų knygų',\n    'shelves_edit_and_assign' => 'Redaguoti lentyną, kad pridėti knygų',\n    'shelves_edit_named' => 'Edit Shelf :name',\n    'shelves_edit' => 'Edit Shelf',\n    'shelves_delete' => 'Delete Shelf',\n    'shelves_delete_named' => 'Delete Shelf :name',\n    'shelves_delete_explain' => \"This will delete the shelf with the name ':name'. Contained books will not be deleted.\",\n    'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',\n    'shelves_permissions' => 'Shelf Permissions',\n    'shelves_permissions_updated' => 'Shelf Permissions Updated',\n    'shelves_permissions_active' => 'Shelf Permissions Active',\n    'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',\n    'shelves_permissions_create' => 'Shelf create permissions are only used for copying permissions to child books using the action below. They do not control the ability to create books.',\n    'shelves_copy_permissions_to_books' => 'Kopijuoti leidimus knygoms',\n    'shelves_copy_permissions' => 'Kopijuoti leidimus',\n    'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',\n    'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',\n\n    // Books\n    'book' => 'Knyga',\n    'books' => 'Knygos',\n    'x_books' => ':count knyga|:count knygos',\n    'books_empty' => 'Nebuvo sukurta jokių knygų',\n    'books_popular' => 'Populiarios knygos',\n    'books_recent' => 'Naujos knygos',\n    'books_new' => 'Naujos knygos',\n    'books_new_action' => 'Nauja knyga',\n    'books_popular_empty' => 'Čia pasirodys pačios populiariausios knygos.',\n    'books_new_empty' => 'Čia pasirodys naujausios sukurtos knygos',\n    'books_create' => 'Sukurti naują knygą',\n    'books_delete' => 'Ištrinti knygą',\n    'books_delete_named' => 'Ištrinti knygą :bookName',\n    'books_delete_explain' => 'Tai ištrins knygą su pavadinimu \\':bookName\\'. Visi puslapiai ir skyriai bus pašalinti.',\n    'books_delete_confirmation' => 'Ar jūs esate tikri, kad norite ištrinti šią knygą?',\n    'books_edit' => 'Redaguoti knygą',\n    'books_edit_named' => 'Redaguoti knygą :bookName',\n    'books_form_book_name' => 'Knygos pavadinimas',\n    'books_save' => 'Išsaugoti knygą',\n    'books_permissions' => 'Knygos leidimas',\n    'books_permissions_updated' => 'Knygos leidimas atnaujintas',\n    'books_empty_contents' => 'Jokių puslapių ar skyrių nebuvo skurta šiai knygai',\n    'books_empty_create_page' => 'Sukurti naują puslapį',\n    'books_empty_sort_current_book' => 'Rūšiuoti dabartinę knygą',\n    'books_empty_add_chapter' => 'Pridėti skyrių',\n    'books_permissions_active' => 'Knygos leidimas aktyvus',\n    'books_search_this' => 'Ieškoti šioje knygoje',\n    'books_navigation' => 'Knygos naršymas',\n    'books_sort' => 'Rūšiuoti pagal knygos turinį',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Rūšiuoti knygą :bookName',\n    'books_sort_name' => 'Rūšiuoti pagal vardą',\n    'books_sort_created' => 'Rūšiuoti pagal sukūrimo datą',\n    'books_sort_updated' => 'Rūšiuoti pagal atnaujinimo datą',\n    'books_sort_chapters_first' => 'Skyriaus pradžia',\n    'books_sort_chapters_last' => 'Skyriaus pabaiga',\n    'books_sort_show_other' => 'Rodyti kitas knygas',\n    'books_sort_save' => 'Išsaugoti naują įsakymą',\n    'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.',\n    'books_sort_move_up' => 'Move Up',\n    'books_sort_move_down' => 'Move Down',\n    'books_sort_move_prev_book' => 'Move to Previous Book',\n    'books_sort_move_next_book' => 'Move to Next Book',\n    'books_sort_move_prev_chapter' => 'Move Into Previous Chapter',\n    'books_sort_move_next_chapter' => 'Move Into Next Chapter',\n    'books_sort_move_book_start' => 'Move to Start of Book',\n    'books_sort_move_book_end' => 'Move to End of Book',\n    'books_sort_move_before_chapter' => 'Move to Before Chapter',\n    'books_sort_move_after_chapter' => 'Move to After Chapter',\n    'books_copy' => 'Copy Book',\n    'books_copy_success' => 'Book successfully copied',\n\n    // Chapters\n    'chapter' => 'Skyrius',\n    'chapters' => 'Skyriai',\n    'x_chapters' => ':count skyrius|:count skyriai',\n    'chapters_popular' => 'Populiarūs skyriai',\n    'chapters_new' => 'Naujas skyrius',\n    'chapters_create' => 'Sukurti naują skyrių',\n    'chapters_delete' => 'Ištrinti skyrių',\n    'chapters_delete_named' => 'Ištrinti skyrių :chapterName',\n    'chapters_delete_explain' => 'Tai ištrins skyrių su pavadinimu \\':chapterName\\. Visi puslapiai, esantys šiame skyriuje, taip pat bus ištrinti.',\n    'chapters_delete_confirm' => 'Ar esate tikri, jog norite ištrinti šį skyrių?',\n    'chapters_edit' => 'Redaguoti skyrių',\n    'chapters_edit_named' => 'Redaguoti skyrių :chapterName',\n    'chapters_save' => 'Išsaugoti skyrių',\n    'chapters_move' => 'Perkelti skyrių',\n    'chapters_move_named' => 'Perkelti skyrių :chapterName',\n    'chapters_copy' => 'Copy Chapter',\n    'chapters_copy_success' => 'Chapter successfully copied',\n    'chapters_permissions' => 'Skyriaus leidimai',\n    'chapters_empty' => 'Šiuo metu skyriuje nėra puslapių',\n    'chapters_permissions_active' => 'Skyriaus leidimai aktyvūs',\n    'chapters_permissions_success' => 'Skyriaus leidimai atnaujinti',\n    'chapters_search_this' => 'Ieškoti šio skyriaus',\n    'chapter_sort_book' => 'Sort Book',\n\n    // Pages\n    'page' => 'Puslapis',\n    'pages' => 'Puslapiai',\n    'x_pages' => ':count puslapis|:count puslapiai',\n    'pages_popular' => 'Populiarūs puslapiai',\n    'pages_new' => 'Naujas puslapis',\n    'pages_attachments' => 'Priedai',\n    'pages_navigation' => 'Puslapių navigacija',\n    'pages_delete' => 'Ištrinti puslapį',\n    'pages_delete_named' => 'Ištrinti puslapį :pageName',\n    'pages_delete_draft_named' => 'Ištrinti juodraščio puslapį :pageName',\n    'pages_delete_draft' => 'Ištrinti juodraščio puslapį',\n    'pages_delete_success' => 'Puslapis ištrintas',\n    'pages_delete_draft_success' => 'Juodraščio puslapis ištrintas',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'Ar esate tikri, kad norite ištrinti šį puslapį?',\n    'pages_delete_draft_confirm' => 'Ar esate tikri, kad norite ištrinti šį juodraščio puslapį?',\n    'pages_editing_named' => 'Redaguojamas puslapis :pageName',\n    'pages_edit_draft_options' => 'Juodrasčio pasirinkimai',\n    'pages_edit_save_draft' => 'Išsaugoti juodraštį',\n    'pages_edit_draft' => 'Redaguoti juodraščio puslapį',\n    'pages_editing_draft' => 'Redaguojamas juodraštis',\n    'pages_editing_page' => 'Redaguojamas puslapis',\n    'pages_edit_draft_save_at' => 'Juodraštis išsaugotas',\n    'pages_edit_delete_draft' => 'Ištrinti juodraštį',\n    'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.',\n    'pages_edit_discard_draft' => 'Išmesti juodraštį',\n    'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',\n    'pages_edit_switch_to_markdown_clean' => '(Clean Content)',\n    'pages_edit_switch_to_markdown_stable' => '(Stable Content)',\n    'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Nustatyti keitimo žurnalą',\n    'pages_edit_enter_changelog_desc' => 'Įveskite trumpus, jūsų atliktus, pokyčių aprašymus',\n    'pages_edit_enter_changelog' => 'Įeiti į keitimo žurnalą',\n    'pages_editor_switch_title' => 'Switch Editor',\n    'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',\n    'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',\n    'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',\n    'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',\n    'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\\'t persist across this change.',\n    'pages_save' => 'Išsaugoti puslapį',\n    'pages_title' => 'Puslapio antraštė',\n    'pages_name' => 'Puslapio pavadinimas',\n    'pages_md_editor' => 'Redaguotojas',\n    'pages_md_preview' => 'Peržiūra',\n    'pages_md_insert_image' => 'Įterpti nuotrauką',\n    'pages_md_insert_link' => 'Įterpti subjekto nuorodą',\n    'pages_md_insert_drawing' => 'Įterpti piešinį',\n    'pages_md_show_preview' => 'Show preview',\n    'pages_md_sync_scroll' => 'Sync preview scroll',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Unsaved Drawing Found',\n    'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',\n    'pages_not_in_chapter' => 'Puslapio nėra skyriuje',\n    'pages_move' => 'Perkelti puslapį',\n    'pages_copy' => 'Nukopijuoti puslapį',\n    'pages_copy_desination' => 'Nukopijuoti tikslą',\n    'pages_copy_success' => 'Puslapis sėkmingai nukopijuotas',\n    'pages_permissions' => 'Puslapio leidimai',\n    'pages_permissions_success' => 'Puslapio leidimai atnaujinti',\n    'pages_revision' => 'Peržiūra',\n    'pages_revisions' => 'Puslapio peržiūros',\n    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',\n    'pages_revisions_named' => 'Peržiūros puslapio :pageName',\n    'pages_revision_named' => 'Peržiūra puslapio :pageName',\n    'pages_revision_restored_from' => 'Atkurta iš #:id; :summary',\n    'pages_revisions_created_by' => 'Sukurta',\n    'pages_revisions_date' => 'Peržiūros data',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Revision Number',\n    'pages_revisions_numbered' => 'Peržiūra #:id',\n    'pages_revisions_numbered_changes' => 'Peržiūros #:id pokyčiai',\n    'pages_revisions_editor' => 'Editor Type',\n    'pages_revisions_changelog' => 'Keitimo žurnalas',\n    'pages_revisions_changes' => 'Pakeitimai',\n    'pages_revisions_current' => 'Dabartinė versija',\n    'pages_revisions_preview' => 'Peržiūra',\n    'pages_revisions_restore' => 'Atkurti',\n    'pages_revisions_none' => 'Šis puslapis neturi peržiūrų',\n    'pages_copy_link' => 'Kopijuoti nuorodą',\n    'pages_edit_content_link' => 'Jump to section in editor',\n    'pages_pointer_enter_mode' => 'Enter section select mode',\n    'pages_pointer_label' => 'Page Section Options',\n    'pages_pointer_permalink' => 'Page Section Permalink',\n    'pages_pointer_include_tag' => 'Page Section Include Tag',\n    'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag',\n    'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink',\n    'pages_permissions_active' => 'Puslapio leidimai aktyvūs',\n    'pages_initial_revision' => 'Pradinis skelbimas',\n    'pages_references_update_revision' => 'System auto-update of internal links',\n    'pages_initial_name' => 'Naujas puslapis',\n    'pages_editing_draft_notification' => 'Dabar jūs redaguojate juodraštį, kuris paskutinį kartą buvo išsaugotas :timeDiff',\n    'pages_draft_edited_notification' => 'Šis puslapis buvo redaguotas iki to laiko. Rekomenduojame jums išmesti šį juodraštį.',\n    'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count naudotojai pradėjo redaguoti šį puslapį',\n        'start_b' => ':userName pradėjo redaguoti šį puslapį',\n        'time_a' => 'nuo puslapio paskutinio atnaujinimo',\n        'time_b' => 'paskutinėmis :minCount minutėmis',\n        'message' => ':start :time. Pasistenkite neperrašyti vienas kito atnaujinimų!',\n    ],\n    'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',\n    'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',\n    'pages_specific' => 'Specifinis puslapis',\n    'pages_is_template' => 'Puslapio šablonas',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Toggle Sidebar',\n    'page_tags' => 'Puslapio žymos',\n    'chapter_tags' => 'Skyriaus žymos',\n    'book_tags' => 'Knygos žymos',\n    'shelf_tags' => 'Lentynų žymos',\n    'tag' => 'Žymos',\n    'tags' =>  'Žymės',\n    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',\n    'tag_name' =>  'Žymės pavadinimas',\n    'tag_value' => 'Žymos vertė (neprivaloma)',\n    'tags_explain' => \"Add some tags to better categorise your content. \\n You can assign a value to a tag for more in-depth organisation.\",\n    'tags_add' => 'Pridėti kitą žymą',\n    'tags_remove' => 'Pridėti kitą žymą',\n    'tags_usages' => 'Total tag usages',\n    'tags_assigned_pages' => 'Assigned to Pages',\n    'tags_assigned_chapters' => 'Assigned to Chapters',\n    'tags_assigned_books' => 'Assigned to Books',\n    'tags_assigned_shelves' => 'Assigned to Shelves',\n    'tags_x_unique_values' => ':count unique values',\n    'tags_all_values' => 'Visos reikšmės',\n    'tags_view_tags' => 'Peržiūrėti žymes',\n    'tags_view_existing_tags' => 'View existing tags',\n    'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',\n    'attachments' => 'Priedai',\n    'attachments_explain' => 'Įkelkite kelis failus arba pridėkite nuorodas savo puslapyje. Jie matomi puslapio šoninėje juostoje.',\n    'attachments_explain_instant_save' => 'Pakeitimai čia yra išsaugomi akimirksniu.',\n    'attachments_upload' => 'Įkelti failą',\n    'attachments_link' => 'Pridėti nuorodą',\n    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',\n    'attachments_set_link' => 'Nustatyti nuorodą',\n    'attachments_delete' => 'Ar esate tikri, kad norite ištrinti šį priedą?',\n    'attachments_dropzone' => 'Drop files here to upload',\n    'attachments_no_files' => 'Failai nebuvo įkelti',\n    'attachments_explain_link' => 'Jūs galite pridėti nuorodas, jei nenorite įkelti failo. Tai gali būti nuoroda į kitą puslapį arba nuoroda į failą debesyje.',\n    'attachments_link_name' => 'Nuorodos pavadinimas',\n    'attachment_link' => 'Priedo nuoroda',\n    'attachments_link_url' => 'Nuoroda į failą',\n    'attachments_link_url_hint' => 'URL į failą',\n    'attach' => 'Pridėti',\n    'attachments_insert_link' => 'Pridėti priedo nuorodą į puslapį',\n    'attachments_edit_file' => 'Redaguoti failą',\n    'attachments_edit_file_name' => 'Failo pavadinimas',\n    'attachments_edit_drop_upload' => 'Numesti failus arba spausti čia ir atsisiųsti ir perrašyti',\n    'attachments_order_updated' => 'Atnaujintas priedų išsidėstymas',\n    'attachments_updated_success' => 'Priedų detalės atnaujintos',\n    'attachments_deleted' => 'Priedas ištrintas',\n    'attachments_file_uploaded' => 'Failas sėkmingai įkeltas',\n    'attachments_file_updated' => 'Failas sėkmingai atnaujintas',\n    'attachments_link_attached' => 'Nuoroda sėkmingai pridėta puslapyje',\n    'templates' => 'Šablonai',\n    'templates_set_as_template' => 'Puslapis yra šablonas',\n    'templates_explain_set_as_template' => 'Jūs galite nustatyti šį puslapį kaip šabloną, jo turinys bus panaudotas, kuriant kitus puslapius. Kiti naudotojai galės naudotis šiuo šablonu, jei turės peržiūros leidimą šiam puslapiui.',\n    'templates_replace_content' => 'Pakeisti puslapio turinį',\n    'templates_append_content' => 'Papildyti puslapio turinį',\n    'templates_prepend_content' => 'Priklauso nuo puslapio turinio',\n\n    // Profile View\n    'profile_user_for_x' => 'Naudotojas :time',\n    'profile_created_content' => 'Sukurtas tyrinys',\n    'profile_not_created_pages' => ':userName nesukūrė jokio puslapio',\n    'profile_not_created_chapters' => ':userName nesukūrė jokio skyriaus',\n    'profile_not_created_books' => ':userName nesukūrė jokios knygos',\n    'profile_not_created_shelves' => ':userName nesukūrė jokių lentynų',\n\n    // Comments\n    'comment' => 'Komentaras',\n    'comments' => 'Komentarai',\n    'comment_add' => 'Pridėti komentarą',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Palikite komentarą čia',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Išsaugoti komentarą',\n    'comment_new' => 'Naujas komentaras',\n    'comment_created' => 'Pakomentuota :createDiff',\n    'comment_updated' => 'Atnaujinta :updateDiff pagal :username',\n    'comment_updated_indicator' => 'Updated',\n    'comment_deleted_success' => 'Komentaras ištrintas',\n    'comment_created_success' => 'Komentaras pridėtas',\n    'comment_updated_success' => 'Komentaras atnaujintas',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Esate tikri, kad norite ištrinti šį komentarą?',\n    'comment_in_reply_to' => 'Atsakydamas į :commentId',\n    'comment_reference' => 'Nuoroda',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',\n\n    // Revision\n    'revision_delete_confirm' => 'Esate tikri, kad norite ištrinti šią peržiūrą?',\n    'revision_restore_confirm' => 'Esate tikri, kad norite atkurti šią peržiūrą? Dabartinis puslapio turinys bus pakeistas.',\n    'revision_cannot_delete_latest' => 'Negalima išrinti vėliausios peržiūros',\n\n    // Copy view\n    'copy_consider' => 'Please consider the below when copying content.',\n    'copy_consider_permissions' => 'Custom permission settings will not be copied.',\n    'copy_consider_owner' => 'You will become the owner of all copied content.',\n    'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',\n    'copy_consider_attachments' => 'Page attachments will not be copied.',\n    'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',\n\n    // Conversions\n    'convert_to_shelf' => 'Convert to Shelf',\n    'convert_to_shelf_contents_desc' => 'You can convert this book to a new shelf with the same contents. Chapters contained within this book will be converted to new books. If this book contains any pages, that are not in a chapter, this book will be renamed and contain such pages, and this book will become part of the new shelf.',\n    'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.',\n    'convert_book' => 'Convert Book',\n    'convert_book_confirm' => 'Are you sure you want to convert this book?',\n    'convert_undo_warning' => 'This cannot be as easily undone.',\n    'convert_to_book' => 'Convert to Book',\n    'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',\n    'convert_chapter' => 'Convert Chapter',\n    'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',\n\n    // References\n    'references' => 'Nuorodos',\n    'references_none' => 'There are no tracked references to this item.',\n    'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',\n\n    // Watch Options\n    'watch' => 'Watch',\n    'watch_title_default' => 'Default Preferences',\n    'watch_desc_default' => 'Revert watching to just your default notification preferences.',\n    'watch_title_ignore' => 'Ignore',\n    'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.',\n    'watch_title_new' => 'New Pages',\n    'watch_desc_new' => 'Notify when any new page is created within this item.',\n    'watch_title_updates' => 'All Page Updates',\n    'watch_desc_updates' => 'Notify upon all new pages and page changes.',\n    'watch_desc_updates_page' => 'Notify upon all page changes.',\n    'watch_title_comments' => 'All Page Updates & Comments',\n    'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.',\n    'watch_desc_comments_page' => 'Notify upon page changes and new comments.',\n    'watch_change_default' => 'Change default notification preferences',\n    'watch_detail_ignore' => 'Ignoring notifications',\n    'watch_detail_new' => 'Watching for new pages',\n    'watch_detail_updates' => 'Watching new pages and updates',\n    'watch_detail_comments' => 'Watching new pages, updates & comments',\n    'watch_detail_parent_book' => 'Watching via parent book',\n    'watch_detail_parent_book_ignore' => 'Ignoring via parent book',\n    'watch_detail_parent_chapter' => 'Watching via parent chapter',\n    'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter',\n];\n"
  },
  {
    "path": "lang/lt/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Jūs neturite leidimo atidaryti šio puslapio.',\n    'permissionJson' => 'Jūs neturite leidimo atlikti prašomo veiksmo.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Naudotojo elektroninis paštas :email jau egzistuoja, bet su kitokiais įgaliojimais.',\n    'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',\n    'email_already_confirmed' => 'Elektroninis paštas jau buvo patvirtintas, pabandykite prisijungti.',\n    'email_confirmation_invalid' => 'Šis patvirtinimo prieigos raktas negalioja arba jau buvo panaudotas, prašome bandykite vėl registruotis.',\n    'email_confirmation_expired' => 'Šis patvirtinimo prieigos raktas baigė galioti, naujas patvirtinimo laiškas jau išsiųstas elektroniniu paštu.',\n    'email_confirmation_awaiting' => 'Elektroninio pašto adresą paskyrai reikia patvirtinti',\n    'ldap_fail_anonymous' => 'Nepavyko pasiekti LDAP naudojant anoniminį susiejimą',\n    'ldap_fail_authed' => 'Nepavyko pasiekti LDAP naudojant išsamią dn ir slaptažodžio informaciją',\n    'ldap_extension_not_installed' => 'LDAP PHP išplėtimas neįdiegtas',\n    'ldap_cannot_connect' => 'Negalima prisijungti prie LDAP serverio, nepavyko prisijungti',\n    'saml_already_logged_in' => 'Jau prisijungta',\n    'saml_no_email_address' => 'Nerandamas šio naudotojo elektroninio pašto adresas išorinės autentifikavimo sistemos pateiktuose duomenyse',\n    'saml_invalid_response_id' => 'Prašymas iš išorinės autentifikavimo sistemos nėra atpažintas proceso, kurį pradėjo ši programa. Naršymas po prisijungimo gali sukelti šią problemą.',\n    'saml_fail_authed' => 'Prisijungimas, naudojant :system nepavyko, sistema nepateikė sėkmingo leidimo.',\n    'oidc_already_logged_in' => 'Jau prisijungta',\n    'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',\n    'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',\n    'social_no_action_defined' => 'Neapibrėžtas joks veiksmas',\n    'social_login_bad_response' => \"Error received during :socialAccount login: \\n:error\",\n    'social_account_in_use' => 'Ši :socialAccount paskyra jau yra naudojama, pabandykite prisijungti per :socialAccount pasirinkimą.',\n    'social_account_email_in_use' => 'Elektroninis paštas :email jau yra naudojamas. Jei jūs jau turite paskyrą, galite prijungti savo :socialAccount paskyrą iš savo profilio nustatymų.',\n    'social_account_existing' => 'Šis :socialAccount jau yra pridėtas prie jūsų profilio.',\n    'social_account_already_used_existing' => 'Ši :socialAccount paskyra jau yra naudojama kito naudotojo.',\n    'social_account_not_used' => 'Ši :socialAccount paskyra nėra susieta su jokiais naudotojais. Prašome, pridėkite ją į savo profilio nustatymus.',\n    'social_account_register_instructions' => 'Jei dar neturite paskyros, galite užregistruoti paskyrą, naudojant :socialAccount pasirinkimą.',\n    'social_driver_not_found' => 'Socialinis diskas nerastas',\n    'social_driver_not_configured' => 'Jūsų :socialAccount socaliniai nustatymai sukonfigūruoti neteisingai.',\n    'invite_token_expired' => 'Ši kvietimo nuoroda baigė galioti. Vietoj to, jūs galite bandyti iš naujo nustatyti savo paskyros slaptažodį.',\n    'login_user_not_found' => 'A user for this action could not be found.',\n\n    // System\n    'path_not_writable' => 'Į failo kelią :filePath negalima įkelti. Įsitikinkite, kad jis yra įrašomas į serverį.',\n    'cannot_get_image_from_url' => 'Negalima gauti vaizdo iš :url',\n    'cannot_create_thumbs' => 'Serveris negali sukurti miniatiūros. Prašome patikrinkite, ar turite įdiegtą GD PHP plėtinį.',\n    'server_upload_limit' => 'Serveris neleidžia įkelti tokio dydžio failų. Prašome bandykite mažesnį failo dydį.',\n    'server_post_limit' => 'The server cannot receive the provided amount of data. Try again with less data or a smaller file.',\n    'uploaded'  => 'Serveris neleidžia įkelti tokio dydžio failų. Prašome bandykite mažesnį failo dydį.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Įvyko klaida įkeliant vaizdą',\n    'image_upload_type_error' => 'Vaizdo tipas, kurį norima įkelti, yra neteisingas',\n    'image_upload_replace_type' => 'Image file replacements must be of the same type',\n    'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.',\n    'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.',\n    'image_gallery_thumbnail_memory_limit' => 'Failed to create gallery thumbnails due to system resource limits.',\n    'drawing_data_not_found' => 'Drawing data could not be loaded. The drawing file might no longer exist or you may not have permission to access it.',\n\n    // Attachments\n    'attachment_not_found' => 'Priedas nerastas',\n    'attachment_upload_error' => 'An error occurred uploading the attachment file',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Juodraščio išsaugoti nepavyko. Įsitikinkite, jog turite interneto ryšį prieš išsaugant šį paslapį.',\n    'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content',\n    'page_custom_home_deletion' => 'Negalima ištrinti šio puslapio, kol jis yra nustatytas kaip pagrindinis puslapis',\n\n    // Entities\n    'entity_not_found' => 'Subjektas nerastas',\n    'bookshelf_not_found' => 'Shelf not found',\n    'book_not_found' => 'Knyga nerasta',\n    'page_not_found' => 'Puslapis nerastas',\n    'chapter_not_found' => 'Skyrius nerastas',\n    'selected_book_not_found' => 'Pasirinkta knyga nerasta',\n    'selected_book_chapter_not_found' => 'Pasirinkta knyga ar skyrius buvo nerasti',\n    'guests_cannot_save_drafts' => 'Svečiai negali išsaugoti juodraščių',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Negalite ištrinti vienintelio administratoriaus',\n    'users_cannot_delete_guest' => 'Negalite ištrinti svečio naudotojo',\n    'users_could_not_send_invite' => 'Could not create user since invite email failed to send',\n\n    // Roles\n    'role_cannot_be_edited' => 'Šio vaidmens negalima redaguoti',\n    'role_system_cannot_be_deleted' => 'Šis vaidmuo yra sistemos vaidmuo ir jo negalima ištrinti',\n    'role_registration_default_cannot_delete' => 'Šis vaidmuo negali būti ištrintas, kai yra nustatytas kaip numatytasis registracijos vaidmuo',\n    'role_cannot_remove_only_admin' => 'Šis naudotojas yra vienintelis naudotojas, kuriam yra paskirtas administratoriaus vaidmuo. Paskirkite administratoriaus vaidmenį kitam naudotojui prieš bandant jį pašalinti.',\n\n    // Comments\n    'comment_list' => 'Gaunant komentarus įvyko klaida.',\n    'cannot_add_comment_to_draft' => 'Negalite pridėti komentaro juodraštyje',\n    'comment_add' => 'Klaido įvyko pridedant/atnaujinant komantarą.',\n    'comment_delete' => 'Trinant komentarą įvyko klaida.',\n    'empty_comment' => 'Negalite pridėti tuščio komentaro.',\n\n    // Error pages\n    '404_page_not_found' => 'Puslapis nerastas',\n    'sorry_page_not_found' => 'Atleiskite, puslapis, kurio ieškote, nerastas.',\n    'sorry_page_not_found_permission_warning' => 'Jei tikėjotės, kad šis puslapis egzistuoja, galbūt neturite leidimo jo peržiūrėti.',\n    'image_not_found' => 'Paveikslėlis nerastas',\n    'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',\n    'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',\n    'return_home' => 'Grįžti į namus',\n    'error_occurred' => 'Įvyko klaida',\n    'app_down' => ':appName dabar yra apačioje',\n    'back_soon' => 'Tai sugrįž greitai',\n\n    // Import\n    'import_zip_cant_read' => 'Could not read ZIP file.',\n    'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',\n    'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Import ZIP failed to validate with errors:',\n    'import_zip_failed_notification' => 'Failed to import ZIP file.',\n    'import_perms_books' => 'You are lacking the required permissions to create books.',\n    'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',\n    'import_perms_pages' => 'You are lacking the required permissions to create pages.',\n    'import_perms_images' => 'You are lacking the required permissions to create images.',\n    'import_perms_attachments' => 'You are lacking the required permission to create attachments.',\n\n    // API errors\n    'api_no_authorization_found' => 'Užklausoje nerastas įgaliojimo prieigos raktas',\n    'api_bad_authorization_format' => 'Užklausoje rastas prieigos raktas, tačiau formatas yra neteisingas',\n    'api_user_token_not_found' => 'Pateiktam prieigos raktui nebuvo rastas atitinkamas API prieigos raktas',\n    'api_incorrect_token_secret' => 'Pateiktas panaudoto API žetono slėpinys yra neteisingas',\n    'api_user_no_api_permission' => 'API prieigos rakto savininkas neturi leidimo daryti API skambučius',\n    'api_user_token_expired' => 'Prieigos rakto naudojimas baigė galioti',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Siunčiant bandymo email: įvyko klaida',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'The URL does not match the configured allowed SSR hosts',\n];\n"
  },
  {
    "path": "lang/lt/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'New comment on page: :pageName',\n    'new_comment_intro' => 'A user has commented on a page in :appName:',\n    'new_page_subject' => 'Naujas puslapis: :pageName',\n    'new_page_intro' => 'A new page has been created in :appName:',\n    'updated_page_subject' => 'Updated page: :pageName',\n    'updated_page_intro' => 'A page has been updated in :appName:',\n    'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\\'t be sent notifications for further edits to this page by the same editor.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Puslapio pavadinimas:',\n    'detail_page_path' => 'Page Path:',\n    'detail_commenter' => 'Commenter:',\n    'detail_comment' => 'Komentaras:',\n    'detail_created_by' => 'Sukurta:',\n    'detail_updated_by' => 'Atnaujinta:',\n\n    'action_view_comment' => 'View Comment',\n    'action_view_page' => 'Peržiūrėti puslapį',\n\n    'footer_reason' => 'This notification was sent to you because :link cover this type of activity for this item.',\n    'footer_reason_link' => 'your notification preferences',\n];\n"
  },
  {
    "path": "lang/lt/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Ankstesnis',\n    'next'     => 'Kitas &raquo;',\n\n];\n"
  },
  {
    "path": "lang/lt/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Slaptažodis privalo būti mažiausiai aštuonių simbolių ir atitikti patvirtinimą.',\n    'user' => \"Nerastas vartotojas pagal šį el. pašto adresą.\",\n    'token' => 'Slaptažodžio nustatymo raktas yra neteisingas šiam elektroninio pašto adresui.',\n    'sent' => 'Elektroniniu paštu Jums išsiųsta slaptažodžio atkūrimo nuoroda!',\n    'reset' => 'Jūsų slaptažodis buvo atkurtas!',\n\n];\n"
  },
  {
    "path": "lang/lt/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Mano paskyra',\n\n    'shortcuts' => 'Shortcuts',\n    'shortcuts_interface' => 'UI Shortcut Preferences',\n    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',\n    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',\n    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',\n    'shortcuts_section_navigation' => 'Navigacija',\n    'shortcuts_section_actions' => 'Common Actions',\n    'shortcuts_save' => 'Save Shortcuts',\n    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing \"?\" which will highlight the available shortcuts for actions currently visible on the screen.',\n    'shortcuts_update_success' => 'Shortcut preferences have been updated!',\n    'shortcuts_overview_desc' => 'Manage keyboard shortcuts you can use to navigate the system user interface.',\n\n    'notifications' => 'Notification Preferences',\n    'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.',\n    'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own',\n    'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Notify upon replies to my comments',\n    'notifications_save' => 'Išsaugoti nuostatas',\n    'notifications_update_success' => 'Notification preferences have been updated!',\n    'notifications_watched' => 'Watched & Ignored Items',\n    'notifications_watched_desc' => 'Below are the items that have custom watch preferences applied. To update your preferences for these, view the item then find the watch options in the sidebar.',\n\n    'auth' => 'Prieiga ir saugumas',\n    'auth_change_password' => 'Pasikeisti slaptažodį',\n    'auth_change_password_desc' => 'Pakeistas slaptažodis bus naudojamas prisijungti prie aplikacijos. Slaptažodis turi būti bent 8 simbolių ilgio.',\n    'auth_change_password_success' => 'Slaptažodis atnaujintas!',\n\n    'profile' => 'Profilio informacija',\n    'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.',\n    'profile_view_public' => 'Rodyti viešąjį profilį',\n    'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.',\n    'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.',\n    'profile_email_no_permission' => 'Unfortunately you don\\'t have permission to change your email address. If you want to change this, you\\'d need to ask an administrator to change this for you.',\n    'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.',\n    'profile_admin_options' => 'Administrator Options',\n    'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the \"Settings > Users\" area of the application.',\n\n    'delete_account' => 'Ištrinti paskyrą',\n    'delete_my_account' => 'Ištrinti mano paskyrą',\n    'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\\'ve created, such as created pages and uploaded images, will remain.',\n    'delete_my_account_warning' => 'Are you sure you want to delete your account?',\n];\n"
  },
  {
    "path": "lang/lt/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Nustatymai',\n    'settings_save' => 'Išsaugoti nustatymus',\n    'system_version' => 'Sistemos versija',\n    'categories' => 'Kategorijos',\n\n    // App Settings\n    'app_customization' => 'Tinkinimas',\n    'app_features_security' => 'Funkcijos ir sauga',\n    'app_name' => 'Programos pavadinimas',\n    'app_name_desc' => 'Šis pavadinimas yra rodomas antraštėje ir bet kuriuose sistemos siunčiamuose elektroniniuose laiškuose.',\n    'app_name_header' => 'Rodyti pavadinimą antraštėje',\n    'app_public_access' => 'Vieša prieiga',\n    'app_public_access_desc' => 'Įjungus šią parinktį lankytojai, kurie nėra prisijungę, galės pasiekti BookStack egzemplioriaus turinį.',\n    'app_public_access_desc_guest' => 'Prieiga viešiems lankytojams gali būti kontroliuojama per \"Svečio\" naudotoją.',\n    'app_public_access_toggle' => 'Leisti viešą prieigą',\n    'app_public_viewing' => 'Leisti viešą žiūrėjimą?',\n    'app_secure_images' => 'Didesnio saugumo vaizdų įkėlimai',\n    'app_secure_images_toggle' => 'Įgalinti didesnio saugumo vaizdų įkėlimus',\n    'app_secure_images_desc' => 'Dėl veiklos priežasčių, visi vaizdai yra vieši. Šis pasirinkimas prideda atsitiktinę, sunkiai atspėjamą eilutę prieš vaizdo URL. Įsitikinkite, kad katalogų rodyklės neįgalintos, kad prieiga būtų lengvesnė.',\n    'app_default_editor' => 'Default Page Editor',\n    'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.',\n    'app_custom_html' => 'Pasirinktinis HTL antraštės turinys',\n    'app_custom_html_desc' => 'Bet koks čia pridedamas turinys bus prisegamas apačioje <antraštės> kiekvieno puslapio skyriuje. Tai yra patogu svarbesniems stiliams arba pridedant analizės kodą.',\n    'app_custom_html_disabled_notice' => 'Pasirinktinis HTML antraštės turinys yra išjungtas šiame nustatymų puslapyje užtikrinti, kad bet kokie negeri pokyčiai galėtų būti anuliuojami.',\n    'app_logo' => 'Programos logotipas',\n    'app_logo_desc' => 'This is used in the application header bar, among other areas. This image should be 86px in height. Large images will be scaled down.',\n    'app_icon' => 'Application Icon',\n    'app_icon_desc' => 'This icon is used for browser tabs and shortcut icons. This should be a 256px square PNG image.',\n    'app_homepage' => 'Programos pagrindinis puslapis',\n    'app_homepage_desc' => 'Pasirinkite vaizdą rodyti pagrindiniame paslapyje vietoj numatyto vaizdo. Puslapio leidimai yra ignoruojami pasirinktiems puslapiams.',\n    'app_homepage_select' => 'Pasirinkti puslapį',\n    'app_footer_links' => 'Poraštės nuorodos',\n    'app_footer_links_desc' => 'Pridėkite nuorodas, kurias norite pridėti svetainės poraštėje. Jos bus rodomos daugelio puslapių apačioje, įskaitant ir tuos, kurie nereikalauja prisijungimo. Jūs galite naudoti etiktę \"trans::<key>\", kad naudotis sistemos apibrėžtais vertimais. Pavyzdžiui: naudojimasis \"trans::common.privacy_policy\" bus pateiktas išverstu tekstu \"Privatumo Politika\" ir \"\"trans::common.terms_of_service\" bus pateikta išverstu tekstu \"Paslaugų Teikimo Sąlygos\".',\n    'app_footer_links_label' => 'Etiketės nuoroda',\n    'app_footer_links_url' => 'Nuoroda URL',\n    'app_footer_links_add' => 'Pridėti poraštes nuorodą',\n    'app_disable_comments' => 'Išjungti komentarus',\n    'app_disable_comments_toggle' => 'Išjungti komentarus',\n    'app_disable_comments_desc' => 'Išjungti komentarus visuose programos puslapiuose. <br> Esantys komentarai nerodomi.',\n\n    // Color settings\n    'color_scheme' => 'Application Color Scheme',\n    'color_scheme_desc' => 'Set the colors to use in the application user interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.',\n    'ui_colors_desc' => 'Set the application primary color and default link color. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the application interface.',\n    'app_color' => 'Primary Color',\n    'link_color' => 'Default Link Color',\n    'content_colors_desc' => 'Set colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.',\n    'bookshelf_color' => 'Lentynos spalva',\n    'book_color' => 'Knygos spalva',\n    'chapter_color' => 'Skyriaus spalva',\n    'page_color' => 'Puslapio spalva',\n    'page_draft_color' => 'Puslapio juodraščio spalva',\n\n    // Registration Settings\n    'reg_settings' => 'Registracija',\n    'reg_enable' => 'Įgalinti registraciją',\n    'reg_enable_toggle' => 'Įgalinti registraciją',\n    'reg_enable_desc' => 'Kai registracija yra įgalinta, naudotojai gali prisiregistruoti kaip programos naudotojai. Registruojantis jiems suteikiamas vienintelis, nematytasis naudotojo vaidmuo.',\n    'reg_default_role' => 'Numatytasis naudotojo vaidmuo po registracijos',\n    'reg_enable_external_warning' => 'Ankstesnė parinktis nepaisoma, kai išorinis LDAP arba SAML autentifikavimas yra aktyvus. Vartotojo paskyra neegzistuojantiems nariams bus automatiškai sukurta, jei autentifikavimas naudojant naudojamą išorinę sistemą bus sėkmingas.',\n    'reg_email_confirmation' => 'Elektroninio pašto patvirtinimas',\n    'reg_email_confirmation_toggle' => 'Reikalauja elektroninio pašto patvirtinimo',\n    'reg_confirm_email_desc' => 'Jei naudojamas domeno apribojimas, tada elektroninio pašto patvirtinimas bus reikalaujamas ir ši parinktis bus ignoruojama.',\n    'reg_confirm_restrict_domain' => 'Domeno apribojimas',\n    'reg_confirm_restrict_domain_desc' => 'Įveskite kableliais atskirtą elektroninio pašto domenų, kurių registravimą norite apriboti, sąrašą. Vartotojai išsiųs elektorinį laišką, kad patvirtintumėte jų adresą prieš leidžiant naudotis programa. <br> Prisiminkite, kad vartotojai galės pakeisti savo elektroninius paštus po sėkmingos registracijos.',\n    'reg_confirm_restrict_domain_placeholder' => 'Nėra jokių apribojimų',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Priežiūra',\n    'maint_image_cleanup' => 'Išvalykite vaizdus',\n    'maint_image_cleanup_desc' => 'Scans page & revision content to check which images and drawings are currently in use and which images are redundant. Ensure you create a full database and image backup before running this.',\n    'maint_delete_images_only_in_revisions' => 'Taip pat ištrinkite vaizdus, kurie yra tik senuose puslapių pataisymuose',\n    'maint_image_cleanup_run' => 'Paleisti valymą',\n    'maint_image_cleanup_warning' => ':count potencialiai nepanaudoti vaizdai rasti. Ar esate tikri, kad norite ištrinti šiuos vaizdus?',\n    'maint_image_cleanup_success' => ':count potencialiai nepanaudoti vaizdai rasti ir ištrinti!',\n    'maint_image_cleanup_nothing_found' => 'Nerasta nepanaudotų vaizdų, niekas neištrinta!',\n    'maint_send_test_email' => 'Siųsti bandomąjį elektroninį laišką',\n    'maint_send_test_email_desc' => 'ai siunčia bandomąjį elektroninį laišką elektroninio pašto adresu, nurodytu jūsų profilyje.',\n    'maint_send_test_email_run' => 'Siųsti bandomąjį elektroninį laišką',\n    'maint_send_test_email_success' => 'Elektroninis laiškas išsiųstas :address',\n    'maint_send_test_email_mail_subject' => 'Bandomasis elektroninis laiškas',\n    'maint_send_test_email_mail_greeting' => 'Elektroninio laiško pristatymas veikia!',\n    'maint_send_test_email_mail_text' => 'Sveikiname! Kadangi gavote šį elektroninio pašto pranešimą, jūsų elektroninio pašto nustatymai buvo sukonfigūruoti teisingai.',\n    'maint_recycle_bin_desc' => 'Ištrintos lentynos, knygos, skyriai ir puslapiai yra perkeliami į šiukšliadėžę tam, kad jie galėtų būti atkurti arba ištrinti visam laikui. Senesni elementai, esantys šiukšliadėžėje, gali būti automatiškai panaikinti po tam tikro laiko priklausomai nuo sistemos konfigūracijos.',\n    'maint_recycle_bin_open' => 'Atidaryti šiukšliadėžę',\n    'maint_regen_references' => 'Regenerate References',\n    'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',\n    'maint_regen_references_success' => 'Reference index has been regenerated!',\n    'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Šiukšliadėžė',\n    'recycle_bin_desc' => 'Čia gali atkurti elementus, kurie buvo ištrinti arba pasirinkti pašalinti juos iš sistemos visam laikui. Šis sąrašas yra nefiltruotas kaip kitie panašus veiklos sąrašai sistemoje, kuriems yra taikomi leidimo filtrai.',\n    'recycle_bin_deleted_item' => 'Ištrintas elementas',\n    'recycle_bin_deleted_parent' => 'Parent',\n    'recycle_bin_deleted_by' => 'Ištrynė',\n    'recycle_bin_deleted_at' => 'Panaikinimo laikas',\n    'recycle_bin_permanently_delete' => 'Ištrinti visam laikui',\n    'recycle_bin_restore' => 'Atkurti',\n    'recycle_bin_contents_empty' => 'Šiukšliadėžė šiuo metu yra tuščia',\n    'recycle_bin_empty' => 'Ištuštinti šiukšliadėžę',\n    'recycle_bin_empty_confirm' => 'Tai visam laikui sunaikins visus elementus, esančius šiukšliadėžėje, įskaitant kiekvieno elemento turinį. Ar esate tikri, jog norite ištuštinti šiukšliadėžę?',\n    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?',\n    'recycle_bin_destroy_list' => 'Elementai panaikinimui',\n    'recycle_bin_restore_list' => 'Elementai atkūrimui',\n    'recycle_bin_restore_confirm' => 'Šis veiksmas atkurs ištrintą elementą ir perkels jį atgal į jo originalią vietą. Jei originali vieta buvo ištrinta ir šiuo metu yra šiukšliadėžėje, ji taip pat turės būti atkurta.',\n    'recycle_bin_restore_deleted_parent' => 'Pagrindinis elementas buvo ištrintas. Šie elementai liks ištrinti iki tol, kol bus atkurtas pagrindinis elementas.',\n    'recycle_bin_restore_parent' => 'Restore Parent',\n    'recycle_bin_destroy_notification' => 'Ištrinti :count visus elementus, esančius šiukšliadėžėje.',\n    'recycle_bin_restore_notification' => 'Atkurti :count visus elementus, esančius šiukšliadėžėje.',\n\n    // Audit Log\n    'audit' => 'Audito seka',\n    'audit_desc' => 'Ši audito seka rodo sąrašą veiklų, rastų sistemoje. Šis sąrašas yra nefiltruotas kaip kitie panašus veiklos sąrašai sistemoje, kuriems yra taikomi leidimo filtrai.',\n    'audit_event_filter' => 'Įvykio filtras',\n    'audit_event_filter_no_filter' => 'Be filtrų',\n    'audit_deleted_item' => 'Ištrintas elementas',\n    'audit_deleted_item_name' => 'Vardas: :name',\n    'audit_table_user' => 'Naudotojas',\n    'audit_table_event' => 'Įvykis',\n    'audit_table_related' => 'Susijęs elementas arba detalė',\n    'audit_table_ip' => 'IP Address',\n    'audit_table_date' => 'Veiklos data',\n    'audit_date_from' => 'Datos seka nuo',\n    'audit_date_to' => 'Datos seka iki',\n\n    // Role Settings\n    'roles' => 'Vaidmenys',\n    'role_user_roles' => 'Naudotojo vaidmenys',\n    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',\n    'roles_x_users_assigned' => ':count user assigned|:count users assigned',\n    'roles_x_permissions_provided' => ':count permission|:count permissions',\n    'roles_assigned_users' => 'Assigned Users',\n    'roles_permissions_provided' => 'Provided Permissions',\n    'role_create' => 'Sukurti naują vaidmenį',\n    'role_delete' => 'Ištrinti vaidmenį',\n    'role_delete_confirm' => 'Tai ištrins vaidmenį vardu\\':roleName\\'.',\n    'role_delete_users_assigned' => 'Šis vaidmuo turi :userCount naudotojus priskirtus prie jo. Jeigu norite naudotojus perkelti iš šio vaidmens, pasirinkite naują vaidmenį apačioje.',\n    'role_delete_no_migration' => \"Don't migrate users\",\n    'role_delete_sure' => 'Ar esate tikri, jog norite ištrinti šį vaidmenį?',\n    'role_edit' => 'Redaguoti vaidmenį',\n    'role_details' => 'Vaidmens detalės',\n    'role_name' => 'Vaidmens pavadinimas',\n    'role_desc' => 'Trumpas vaidmens aprašymas',\n    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',\n    'role_external_auth_id' => 'Išorinio autentifikavimo ID',\n    'role_system' => 'Sistemos leidimai',\n    'role_manage_users' => 'Tvarkyti naudotojus',\n    'role_manage_roles' => 'Tvarkyti vaidmenis ir vaidmenų leidimus',\n    'role_manage_entity_permissions' => 'Tvarkyti visus knygų, skyrių ir puslapių leidimus',\n    'role_manage_own_entity_permissions' => 'Tvarkyti savo knygos, skyriaus ir puslapių leidimus',\n    'role_manage_page_templates' => 'Tvarkyti puslapių šablonus',\n    'role_access_api' => 'Gauti prieigą prie sistemos API',\n    'role_manage_settings' => 'Tvarkyti programos nustatymus',\n    'role_export_content' => 'Export content',\n    'role_import_content' => 'Import content',\n    'role_editor_change' => 'Change page editor',\n    'role_notifications' => 'Receive & manage notifications',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Nuosavybės leidimai',\n    'roles_system_warning' => 'Būkite sąmoningi, kad prieiga prie bet kurio iš trijų leidimų viršuje gali leisti naudotojui pakeisti jų pačių privilegijas arba kitų privilegijas sistemoje. Paskirkite vaidmenis su šiais leidimais tik patikimiems naudotojams.',\n    'role_asset_desc' => 'Šie leidimai kontroliuoja numatytą prieigą į nuosavybę, esančią sistemoje. Knygų, skyrių ir puslapių leidimai nepaisys šių leidimų.',\n    'role_asset_admins' => 'Administratoriams automatiškai yra suteikiama prieiga prie viso turinio, tačiau šie pasirinkimai gali rodyti arba slėpti vartotojo sąsajos parinktis.',\n    'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'Visi',\n    'role_own' => 'Nuosavi',\n    'role_controlled_by_asset' => 'Kontroliuojami nuosavybės, į kurią yra įkelti',\n    'role_save' => 'Išsaugoti vaidmenį',\n    'role_users' => 'Naudotojai šiame vaidmenyje',\n    'role_users_none' => 'Šiuo metu prie šio vaidmens nėra priskirta naudotojų',\n\n    // Users\n    'users' => 'Naudotojai',\n    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',\n    'user_profile' => 'Naudotojo profilis',\n    'users_add_new' => 'Pridėti naują naudotoją',\n    'users_search' => 'Ieškoti naudotojų',\n    'users_latest_activity' => 'Naujausia veikla',\n    'users_details' => 'Naudotojo detalės',\n    'users_details_desc' => 'Nustatykite rodomąjį vardą ir elektroninio pašto adresą šiam naudotojui. Šis elektroninio pašto adresas bus naudojamas prisijungimui prie aplikacijos.',\n    'users_details_desc_no_email' => 'Nustatykite rodomąjį vardą šiam naudotojui, kad kiti galėtų jį atpažinti.',\n    'users_role' => 'Naudotojo vaidmenys',\n    'users_role_desc' => 'Pasirinkite, prie kokių vaidmenų bus priskirtas šis naudotojas. Jeigu naudotojas yra priskirtas prie kelių vaidmenų, leidimai iš tų vaidmenų susidės ir jie gaus visus priskirtų vaidmenų gebėjimus.',\n    'users_password' => 'Naudotojo slaptažodis',\n    'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',\n    'users_send_invite_text' => 'Jūs galite pasirinkti nusiųsti šiam naudotojui kvietimą elektroniniu paštu, kuris leistų jiems patiems susikurti slaptažodį. Priešingu atveju slaptažodį galite sukurti patys.',\n    'users_send_invite_option' => 'Nusiųsti naudotojui kvietimą elektroniniu paštu',\n    'users_external_auth_id' => 'Išorinio autentifikavimo ID',\n    'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.',\n    'users_password_warning' => 'Only fill the below if you would like to change the password for this user.',\n    'users_system_public' => 'Šis naudotojas atstovauja svečius, kurie aplanko jūsų egzempliorių. Jis negali būti naudojamas prisijungimui, tačiau yra priskiriamas automatiškai.',\n    'users_delete' => 'Ištrinti naudotoją',\n    'users_delete_named' => 'Ištrinti naudotoją :userName',\n    'users_delete_warning' => 'Tai pilnai ištrins šį naudotoją vardu \\':userName\\' iš sistemos.',\n    'users_delete_confirm' => 'Ar esate tikri, jog norite ištrinti šį naudotoją?',\n    'users_migrate_ownership' => 'Perkelti nuosavybę',\n    'users_migrate_ownership_desc' => 'Pasirinkite naudotoją, jeigu norite, kad kitas naudotojas taptų visų elementų, šiuo metu priklausančių šiam naudotojui, savininku.',\n    'users_none_selected' => 'Naudotojas nepasirinktas',\n    'users_edit' => 'Redaguoti naudotoją',\n    'users_edit_profile' => 'Redaguoti profilį',\n    'users_avatar' => 'Naudotojo pseudoportretas',\n    'users_avatar_desc' => 'Pasirinkite nuotrauką, pavaizduojančią šį naudotoją. Nuotrauka turi būti maždaug 256px kvadratas.',\n    'users_preferred_language' => 'Norima kalba',\n    'users_preferred_language_desc' => 'Ši parinktis pakeis kalbą, naudojamą naudotojo sąsajoje aplikacijoje. Tai neturės įtakos jokiam vartotojo sukurtam turiniui.',\n    'users_social_accounts' => 'Socialinės paskyros',\n    'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.',\n    'users_social_accounts_info' => 'Čia galite susieti savo kitas paskyras greitesniam ir lengvesniam prisijungimui. Atjungus paskyrą čia neatšaukiama anksčiau leista prieiga. Atšaukite prieigą iš profilio nustatymų prijungtoje socialinėje paskyroje.',\n    'users_social_connect' => 'Susieti paskyrą',\n    'users_social_disconnect' => 'Atskirti paskyrą',\n    'users_social_status_connected' => 'Connected',\n    'users_social_status_disconnected' => 'Disconnected',\n    'users_social_connected' => ':socialAccount paskyra buvo sėkmingai susieta su jūsų profiliu.',\n    'users_social_disconnected' => ':socialAccount paskyra buvo sėkmingai atskirta nuo jūsu profilio.',\n    'users_api_tokens' => 'API sąsajos prieigos raktai',\n    'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.',\n    'users_api_tokens_none' => 'Jokie API sąsajos prieigos raktai nebuvo sukurti šiam naudotojui',\n    'users_api_tokens_create' => 'Sukurti prieigos raktą',\n    'users_api_tokens_expires' => 'Baigia galioti',\n    'users_api_tokens_docs' => 'API dokumentacija',\n    'users_mfa' => 'Multi-Factor Authentication',\n    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'users_mfa_x_methods' => ':count method configured|:count methods configured',\n    'users_mfa_configure' => 'Configure Methods',\n\n    // API Tokens\n    'user_api_token_create' => 'Sukurti API sąsajos prieigos raktą',\n    'user_api_token_name' => 'Pavadinimas',\n    'user_api_token_name_desc' => 'Suteikite savo prieigos raktui perskaitomą pavadinimą kaip priminimą ateičiai apie jo numatytą tikslą.',\n    'user_api_token_expiry' => 'Galiojimo laikas',\n    'user_api_token_expiry_desc' => 'Nustatykite datą kada šis prieigos raktas baigs galioti. Po šios datos, prašymai, atlikti naudojant šį prieigos raktą daugiau nebeveiks. Jeigu šį laukelį paliksite tuščią, galiojimo laikas bus nustatytas 100 metų į ateitį.',\n    'user_api_token_create_secret_message' => 'Iš karto sukūrus šį prieigos raktą, bus sukurtas ir rodomas \"Priegos rakto ID\" ir \"Prieigos rakto slėpinys\". Prieigos rakto slėpinys bus rodomas tik vieną kartą, todėl būtinai nukopijuokite jį kur nors saugioje vietoje.',\n    'user_api_token' => 'API sąsajos prieigos raktas',\n    'user_api_token_id' => 'Prieigos rakto ID',\n    'user_api_token_id_desc' => 'Tai neredaguojamas sistemos sugeneruotas identifikatorius šiam prieigos raktui, kurį reikės pateikti API užklausose.',\n    'user_api_token_secret' => 'Priegos rakto slėpinys',\n    'user_api_token_secret_desc' => 'Tai yra sistemos sukurtas šio priegos rakto slėpinys, kurią reikės pateikti API užklausose. Tai bus rodoma tik šį kartą, todėl nukopijuokite šią vertę į saugią vietą.',\n    'user_api_token_created' => 'Prieigos raktas sukurtas :timeAgo',\n    'user_api_token_updated' => 'Prieigos raktas atnaujintas :timeAgo',\n    'user_api_token_delete' => 'Ištrinti prieigos raktą',\n    'user_api_token_delete_warning' => 'Tai pilnai ištrins šį API sąsajos prieigos raktą pavadinimu \\':tokenName\\' iš sistemos.',\n    'user_api_token_delete_confirm' => 'Ar esate tikri, jog norite ištrinti šį API sąsajos prieigos raktą?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',\n    'webhooks_x_trigger_events' => ':count trigger event|:count trigger events',\n    'webhooks_create' => 'Create New Webhook',\n    'webhooks_none_created' => 'No webhooks have yet been created.',\n    'webhooks_edit' => 'Edit Webhook',\n    'webhooks_save' => 'Save Webhook',\n    'webhooks_details' => 'Webhook Details',\n    'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',\n    'webhooks_events' => 'Webhook Events',\n    'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',\n    'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\\'t expose confidential content.',\n    'webhooks_events_all' => 'All system events',\n    'webhooks_name' => 'Webhook Name',\n    'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',\n    'webhooks_endpoint' => 'Webhook Endpoint',\n    'webhooks_active' => 'Webhook Active',\n    'webhook_events_table_header' => 'Events',\n    'webhooks_delete' => 'Delete Webhook',\n    'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \\':webhookName\\', from the system.',\n    'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?',\n    'webhooks_format_example' => 'Webhook Format Example',\n    'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The \"related_item\" and \"url\" properties are optional and will depend on the type of event triggered.',\n    'webhooks_status' => 'Webhook Status',\n    'webhooks_last_called' => 'Last Called:',\n    'webhooks_last_errored' => 'Last Errored:',\n    'webhooks_last_error_message' => 'Last Error Message:',\n\n    // Licensing\n    'licenses' => 'Licenses',\n    'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.',\n    'licenses_bookstack' => 'BookStack License',\n    'licenses_php' => 'PHP Library Licenses',\n    'licenses_js' => 'JavaScript Library Licenses',\n    'licenses_other' => 'Other Licenses',\n    'license_details' => 'License Details',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/lt/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute turi būti priimtas.',\n    'active_url'           => ':attribute nėra tinkamas URL.',\n    'after'                => ':attribute turi būti data po :date.',\n    'alpha'                => ':attribute turi būti sudarytis tik iš raidžių.',\n    'alpha_dash'           => ':attribute turi būti sudarytas tik iš raidžių, skaičių, brūkšnelių ir pabraukimų.',\n    'alpha_num'            => ':attribute turi būti sudarytas tik iš raidžių ir skaičių.',\n    'array'                => ':attribute turi būti masyvas.',\n    'backup_codes'         => 'The provided code is not valid or has already been used.',\n    'before'               => ':attribute turi būti data anksčiau negu :date.',\n    'between'              => [\n        'numeric' => ':attribute turi būti tarp :min ir :max.',\n        'file'    => ':attribute turi būti tarp :min ir :max kilobaitų.',\n        'string'  => ':attribute turi būti tarp :min ir :max simbolių.',\n        'array'   => ':attribute turi turėti tarp :min ir :max elementų.',\n    ],\n    'boolean'              => ':attribute laukas turi būti tiesa arba melas.',\n    'confirmed'            => ':attribute patvirtinimas nesutampa.',\n    'date'                 => ':attribute nėra tinkama data.',\n    'date_format'          => ':attribute neatitinka formato :format.',\n    'different'            => ':attribute ir :other turi būti skirtingi.',\n    'digits'               => ':attribute turi būti :digits skaitmenų.',\n    'digits_between'       => ':attribute turi būti tarp :min ir :max skaitmenų.',\n    'email'                => ':attribute turi būti tinkamas elektroninio pašto adresas.',\n    'ends_with' => ':attribute turi pasibaigti vienu iš šių: :values',\n    'file'                 => 'The :attribute must be provided as a valid file.',\n    'filled'               => ':attribute laukas yra privalomas.',\n    'gt'                   => [\n        'numeric' => ':attribute turi būti didesnis negu :value.',\n        'file'    => ':attribute turi būti didesnis negu :value kilobaitai.',\n        'string'  => ':attribute turi būti didesnis negu :value simboliai.',\n        'array'   => ':attribute turi turėti daugiau negu :value elementus.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute turi būti didesnis negu arba lygus :value.',\n        'file'    => ':attribute turi būti didesnis negu arba lygus :value kilobaitams.',\n        'string'  => ':attribute turi būti didesnis negu arba lygus :value simboliams.',\n        'array'   => ':attribute turi turėti :value elementus arba daugiau.',\n    ],\n    'exists'               => 'Pasirinktas :attribute yra klaidingas.',\n    'image'                => ':attribute turi būti paveikslėlis.',\n    'image_extension'      => ':attribute turi būti tinkamas ir palaikomas vaizdo plėtinys.',\n    'in'                   => 'Pasirinktas :attribute yra klaidingas.',\n    'integer'              => ':attribute turi būti sveikasis skaičius.',\n    'ip'                   => ':attribute turi būti tinkamas IP adresas.',\n    'ipv4'                 => ':attribute turi būti tinkamas IPv4 adresas.',\n    'ipv6'                 => ':attribute turi būti tinkamas IPv6 adresas.',\n    'json'                 => ':attribute turi būti tinkama JSON eilutė.',\n    'lt'                   => [\n        'numeric' => ':attribute turi būti mažiau negu :value.',\n        'file'    => ':attribute turi būti mažiau negu :value kilobaitai.',\n        'string'  => ':attribute turi būti mažiau negu :value simboliai.',\n        'array'   => ':attribute turi turėti mažiau negu :value elementus.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute turi būti mažiau arba lygus :value.',\n        'file'    => ':attribute turi būti mažiau arba lygus :value kilobaitams.',\n        'string'  => ':attribute turi būti mažiau arba lygus :value simboliams.',\n        'array'   => ':attribute negali turėti daugiau negu :value elementų.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute negali būti didesnis negu :max.',\n        'file'    => ':attribute negali būti didesnis negu :max kilobaitai.',\n        'string'  => ':attribute negali būti didesnis negu :max simboliai.',\n        'array'   => ':attribute negali turėti daugiau negu :max elementų.',\n    ],\n    'mimes'                => ':attribute turi būti tipo failas: :values.',\n    'min'                  => [\n        'numeric' => ':attribute turi būti mažiausiai :min.',\n        'file'    => ':attribute turi būti mažiausiai :min kilobaitų.',\n        'string'  => ':attribute turi būti mažiausiai :min simbolių.',\n        'array'   => ':attribute turi turėti mažiausiai :min elementus.',\n    ],\n    'not_in'               => 'Pasirinktas :attribute yra klaidingas.',\n    'not_regex'            => ':attribute formatas yra klaidingas.',\n    'numeric'              => ':attribute turi būti skaičius.',\n    'regex'                => ':attribute formatas yra klaidingas.',\n    'required'             => ':attribute laukas yra privalomas.',\n    'required_if'          => ':attribute laukas yra privalomas kai :other yra :value.',\n    'required_with'        => ':attribute laukas yra privalomas kai :values yra.',\n    'required_with_all'    => ':attribute laukas yra privalomas kai :values yra.',\n    'required_without'     => ':attribute laukas yra privalomas kai nėra :values.',\n    'required_without_all' => ':attribute laukas yra privalomas kai nėra nei vienos :values.',\n    'same'                 => ':attribute ir :other turi sutapti.',\n    'safe_url'             => 'Pateikta nuoroda gali būti nesaugi.',\n    'size'                 => [\n        'numeric' => ':attribute turi būti :size.',\n        'file'    => ':attribute turi būti :size kilobaitų.',\n        'string'  => ':attribute turi būti :size simbolių.',\n        'array'   => ':attribute turi turėti :size elementus.',\n    ],\n    'string'               => ':attribute turi būti eilutė.',\n    'timezone'             => ':attribute turi būti tinkama zona.',\n    'totp'                 => 'The provided code is not valid or has expired.',\n    'unique'               => ':attribute jau yra paimtas.',\n    'url'                  => ':attribute formatas yra klaidingas.',\n    'uploaded'             => 'Šis failas negali būti įkeltas. Serveris gali nepriimti tokio dydžio failų.',\n\n    'zip_file' => 'The :attribute needs to reference a file within the ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',\n    'zip_model_expected' => 'Data object expected but \":type\" found.',\n    'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Reikalingas slaptažodžio patvirtinimas',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/lv/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'izveidoja lapu',\n    'page_create_notification'    => 'Lapa veiksmīgi izveidota',\n    'page_update'                 => 'atjaunoja lapu',\n    'page_update_notification'    => 'Lapa veiksmīgi atjaunināta',\n    'page_delete'                 => 'izdzēsa lapu',\n    'page_delete_notification'    => 'Lapa veiksmīgi dzēsta',\n    'page_restore'                => 'atjaunoja lapu',\n    'page_restore_notification'   => 'Lapa veiksmīgi atjaunota',\n    'page_move'                   => 'pārvietoja lapu',\n    'page_move_notification'      => 'Lapa veiksmīgi pārvietota',\n\n    // Chapters\n    'chapter_create'              => 'izveidoja nodaļu',\n    'chapter_create_notification' => 'Nodaļa veiksmīgi izveidota',\n    'chapter_update'              => 'atjaunoja nodaļu',\n    'chapter_update_notification' => 'Nodaļa veiksmīgi atjaunināta',\n    'chapter_delete'              => 'izdzēsa nodaļu',\n    'chapter_delete_notification' => 'Nodaļa veiksmīgi dzēsta',\n    'chapter_move'                => 'pārvietoja nodaļu',\n    'chapter_move_notification' => 'Nodaļa veiksmīgi pārvietota',\n\n    // Books\n    'book_create'                 => 'izveidoja grāmatu',\n    'book_create_notification'    => 'Grāmata veiksmīgi izveidota',\n    'book_create_from_chapter'              => 'pārveidojā nodaļu par grāmatu',\n    'book_create_from_chapter_notification' => 'Nodaļa veiksmīgi pārveidota par grāmatu',\n    'book_update'                 => 'atjaunoja grāmatu',\n    'book_update_notification'    => 'Grāmata veiksmīgi atjaunināta',\n    'book_delete'                 => 'izdzēsa grāmatu',\n    'book_delete_notification'    => 'Grāmata veiksmīgi dzēsta',\n    'book_sort'                   => 'kārtoja grāmatu',\n    'book_sort_notification'      => 'Grāmata veiksmīgi pārkārtota',\n\n    // Bookshelves\n    'bookshelf_create'            => 'izveidoja plauktu',\n    'bookshelf_create_notification'    => 'Plaukts veiksmīgi izveidots',\n    'bookshelf_create_from_book'    => 'pārveidoja grāmatu par plauktu',\n    'bookshelf_create_from_book_notification'    => 'Grāmata veiksmīgi pārveidota par plauktu',\n    'bookshelf_update'                 => 'atjaunoja plauktu',\n    'bookshelf_update_notification'    => 'Plaukts veiksmīgi atjaunināts',\n    'bookshelf_delete'                 => 'izdzēsa plauktu',\n    'bookshelf_delete_notification'    => 'Plaukts veiksmīgi dzēsts',\n\n    // Revisions\n    'revision_restore' => 'versija atjaunota',\n    'revision_delete' => 'versija dzēsta',\n    'revision_delete_notification' => 'Versija veiksmīgi dzēsta',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" ir pievienots jūsu favorītiem',\n    'favourite_remove_notification' => '\":name\" ir izņemts no jūsu favorītiem',\n\n    // Watching\n    'watch_update_level_notification' => 'Skatīšanas uzstādījumi veiksmīgi atjaunināti',\n\n    // Auth\n    'auth_login' => 'pieteicies',\n    'auth_register' => 'reģistrējies kā jauns lietotājs',\n    'auth_password_reset_request' => 'pieprasīja lietotāja paroles atiestatīšanu',\n    'auth_password_reset_update' => 'atiestatīja lietotāja paroli',\n    'mfa_setup_method' => 'uzstādīja MFA metodi',\n    'mfa_setup_method_notification' => '2FA funkcija aktivizēta',\n    'mfa_remove_method' => 'noņēma MFA metodi',\n    'mfa_remove_method_notification' => '2FA funkcija noņemta',\n\n    // Settings\n    'settings_update' => 'atjaunoja uzstādījumus',\n    'settings_update_notification' => 'Uzstādījums veiksmīgi atjaunināts',\n    'maintenance_action_run' => 'veica apkopes darbību',\n\n    // Webhooks\n    'webhook_create' => 'izveidoja webhook',\n    'webhook_create_notification' => 'Webhook veiksmīgi izveidots',\n    'webhook_update' => 'atjaunināja webhook',\n    'webhook_update_notification' => 'Webhook veiksmīgi atjaunināts',\n    'webhook_delete' => 'izdzēsa webhook',\n    'webhook_delete_notification' => 'Webhook veiksmīgi izdzēsts',\n\n    // Imports\n    'import_create' => 'izveidoja importu',\n    'import_create_notification' => 'Imports veiksmīgi augšupielādēts',\n    'import_run' => 'atjaunoja importu',\n    'import_run_notification' => 'Saturs veiksmīgi importēts',\n    'import_delete' => 'izdzēsa importu',\n    'import_delete_notification' => 'Imports veiksmīgi dzēsts',\n\n    // Users\n    'user_create' => 'izveidoja lietotāju',\n    'user_create_notification' => 'Lietotājs veiksmīgi izveidots',\n    'user_update' => 'atjaunoja lietotāju',\n    'user_update_notification' => 'Lietotājs veiksmīgi atjaunināts',\n    'user_delete' => 'dzēsa lietotāju',\n    'user_delete_notification' => 'Lietotājs veiksmīgi dzēsts',\n\n    // API Tokens\n    'api_token_create' => 'izveidoja API žetonu',\n    'api_token_create_notification' => 'API žetons veiksmīgi izveidots',\n    'api_token_update' => 'atjaunoja API žetonu',\n    'api_token_update_notification' => 'API žetons veiksmīgi atjaunināts',\n    'api_token_delete' => 'izdzēsa API žetonu',\n    'api_token_delete_notification' => 'API žetons veiksmīgi dzēsts',\n\n    // Roles\n    'role_create' => 'izveidoja lomu',\n    'role_create_notification' => 'Loma veiksmīgi izveidota',\n    'role_update' => 'atjaunoja lomu',\n    'role_update_notification' => 'Loma veiksmīgi atjaunināta',\n    'role_delete' => 'dzēsa lomu',\n    'role_delete_notification' => 'Loma veiksmīgi dzēsta',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'iztukšoja atkritni',\n    'recycle_bin_restore' => 'atjaunoja no atkritnes',\n    'recycle_bin_destroy' => 'izdzēsa no atkritnes',\n\n    // Comments\n    'commented_on'                => 'komentēts',\n    'comment_create'              => 'pievienoja komentāru',\n    'comment_update'              => 'atjaunoja komentārju',\n    'comment_delete'              => 'dzēsa komentāru',\n\n    // Sort Rules\n    'sort_rule_create' => 'izveidoja kārtošanas nosacījumu',\n    'sort_rule_create_notification' => 'Kārtošanas nosacījums veiksmīgi izveidots',\n    'sort_rule_update' => 'atjaunoja kārtošanas nosacījumu',\n    'sort_rule_update_notification' => 'Kārtošanas nosacījums veiksmīgi atjaunots',\n    'sort_rule_delete' => 'izdzēsa kārtošanas nosacījumu',\n    'sort_rule_delete_notification' => 'Kārtošanas nosacījums veiksmīgi dzēsts',\n\n    // Other\n    'permissions_update'          => 'atjaunoja atļaujas',\n];\n"
  },
  {
    "path": "lang/lv/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Šie reģistrācijas dati neatbilst mūsu ierakstiem.',\n    'throttle' => 'Pārāk daudz pieteikšanās mēģinājumu. Lūdzu, mēģiniet vēlreiz pēc :seconds seconds.',\n\n    // Login & Register\n    'sign_up' => 'Reģistrēties',\n    'log_in' => 'Ielogoties',\n    'log_in_with' => 'Ielogoties ar :socialDriver',\n    'sign_up_with' => 'Pieteikties ar :socialDriver',\n    'logout' => 'Iziet',\n\n    'name' => 'Vārds',\n    'username' => 'Lietotājvārds',\n    'email' => 'E-pasts',\n    'password' => 'Parole',\n    'password_confirm' => 'Apstiprināt paroli',\n    'password_hint' => 'Jābūt vismaz 8 rakstzīmēm',\n    'forgot_password' => 'Aizmirsta parole?',\n    'remember_me' => 'Atcerēties mani',\n    'ldap_email_hint' => 'Lūdzu ievadiet e-pastu, kuru izmantosiet šim profilam.',\n    'create_account' => 'Izveidot profilu',\n    'already_have_account' => 'Jau ir profils?',\n    'dont_have_account' => 'Nav profila?',\n    'social_login' => 'Pieteikšanās ar sociālo tīklu profilu',\n    'social_registration' => 'Reģistrēšanās ar sociālo profilu',\n    'social_registration_text' => 'Reģistrēties vai pieteikties izmantojot citu servisu.',\n\n    'register_thanks' => 'Paldies par reģistrāciju!',\n    'register_confirm' => 'Lūdzu, pārbaudiet savu e-pastu un nospiediet apstiprināšanas pogu, lai piekļūtu :appName.',\n    'registrations_disabled' => 'Reģistrācija ir izslēgta',\n    'registration_email_domain_invalid' => 'E-pasta domēnam nav piekļuves pie šīs aplikācijas',\n    'register_success' => 'Paldies par reģistrēšanos! Tagad varat pieslēgties.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Mēģina pierakstīties',\n    'auto_init_starting_desc' => 'Sazināmies ar jūsu autentifikācijas sistēmu, lai uzsāktu pierakstīšanās procesu. Ja 5 sekunžu laikā nekas nav noticis, mēģiniet klikšķināt zemāk esošo saiti.',\n    'auto_init_start_link' => 'Turpināt autentifikāciju',\n\n    // Password Reset\n    'reset_password' => 'Atiestatīt paroli',\n    'reset_password_send_instructions' => 'Ievadiet savu e-pastu zemāk un nosūtīsim e-pastu ar paroles atiestatīšanas saiti.',\n    'reset_password_send_button' => 'Nosūtīt atiestatīšanas saiti',\n    'reset_password_sent' => 'Paroles atiestatīšanas saite tiks nosūtīta uz :email, ja šāds e-pasts būs derīgs.',\n    'reset_password_success' => 'Jūsu parole ir veiksmīgi atiestatīta.',\n    'email_reset_subject' => 'Atiestatīt :appName paroli',\n    'email_reset_text' => 'Jūs saņemat šo e-pastu, jo mēs saņēmām Jūsu profila paroles atiestatīšanas pieprasījumu.',\n    'email_reset_not_requested' => 'Ja Jūs nepieprasījāt paroles atiestatīšanu, tad tālākas darbības nav nepieciešamas.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Apstiprinat savu :appName e-pastu',\n    'email_confirm_greeting' => 'Paldies, ka pievienojāties :appName!',\n    'email_confirm_text' => 'Lūdzu apstipriniet savu e-pastu nospiežot zemāk redzamo pogu:',\n    'email_confirm_action' => 'Apstiprināt e-pastu',\n    'email_confirm_send_error' => 'E-pasta apriprināšana ir nepieciešama, bet sistēma nevarēja e-pastu nosūtīt. Lūdzu sazinaties ar administratoru, lai pārliecinātos, ka e-pasts ir iestatīts pareizi.',\n    'email_confirm_success' => 'Jūsu epasta adrese ir apstiprināta! Jums tagad jābūt iespējai pieslēgties, izmantojot šo epasta adresi.',\n    'email_confirm_resent' => 'Apstiprinājuma vēstule tika nosūtīta. Lūdzu, pārbaudiet jūsu e-pastu.',\n    'email_confirm_thanks' => 'Paldies par apstiprinājumu!',\n    'email_confirm_thanks_desc' => 'Lūdzu uzgaidiet, kamēr jūsu apstiprinājums tiek apstrādāts. Ja netiekat novirzīts 3 sekunžu laikā, spiediet saiti \"Turpināt\", lai dotos tālāk.',\n\n    'email_not_confirmed' => 'E-pasts nav apstiprināts',\n    'email_not_confirmed_text' => 'Jūsu e-pasta adrese vēl nav apstiprināta.',\n    'email_not_confirmed_click_link' => 'Lūdzu, noklikšķiniet uz saiti nosūtītajā e-pastā pēc reģistrēšanās.',\n    'email_not_confirmed_resend' => 'Ja neredzi e-pastu, tad vari atkārtoti nosūtīt apstiprinājuma e-pastu iesniedzot zemāk redzamo formu.',\n    'email_not_confirmed_resend_button' => 'Atkārtoti nosūtīt apstiprinājuma e-pastu',\n\n    // User Invite\n    'user_invite_email_subject' => 'Tu esi uzaicināts pievienoties :appName!',\n    'user_invite_email_greeting' => 'Jūsu :appName profils ir izveidots.',\n    'user_invite_email_text' => 'Lūdzu, nospiediet zemāk redzamo pogu, lai izveidotu paroli un iegūtu piekļuvi:',\n    'user_invite_email_action' => 'Iestatīt profila paroli',\n    'user_invite_page_welcome' => 'Sveicināti :appName!',\n    'user_invite_page_text' => 'Lai pabeigtu profila izveidi un piekļūtu :appName ir jāizveido parole.',\n    'user_invite_page_confirm_button' => 'Apstiprināt paroli',\n    'user_invite_success_login' => 'Parole ir uzstādīta, jums tagad jābūt iespējai pieslēgties izmantojot uzstādīto paroli, lai piekļūtu :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Iestati divfaktoru autentifikāciju (2FA)',\n    'mfa_setup_desc' => 'Iestati divfaktoru autentifikāciju kā papildus drošību tavam lietotāja kontam.',\n    'mfa_setup_configured' => 'Divfaktoru autentifikācija jau ir nokonfigurēta',\n    'mfa_setup_reconfigure' => 'Mainīt 2FA konfigurāciju',\n    'mfa_setup_remove_confirmation' => 'Vai esi drošs, ka vēlies noņemt divfaktoru autentifikāciju?',\n    'mfa_setup_action' => 'Iestatījumi',\n    'mfa_backup_codes_usage_limit_warning' => 'Jums atlikuši mazāk kā 5 rezerves kodi. Lūdzu izveidojiet jaunu kodu komplektu pirms tie visi izlietoti, lai izvairītos no izslēgšanas no jūsu konta.',\n    'mfa_option_totp_title' => 'Mobilā aplikācija',\n    'mfa_option_totp_desc' => 'Lai lietotu vairākfaktoru autentifikāciju, jums būs nepieciešama mobilā aplikācija, kas atbalsta TOTP, piemēram, Google Authenticator, Authy vai Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Rezerves kodi',\n    'mfa_option_backup_codes_desc' => 'Izveido vienreizējas lietošanas rezerves kodus, ko var izmantot pierakstoties, lai apstiprinātu savu identitāti. Pārliecinieties, ka tie ir noglabāti drošā vietā.',\n    'mfa_gen_confirm_and_enable' => 'Apstiprināt un ieslēgt',\n    'mfa_gen_backup_codes_title' => 'Rezerves kodu iestatījumi',\n    'mfa_gen_backup_codes_desc' => 'Noglabājiet zemāk esošo kodu sarakstu drošā vietā. Kad piekļūsiet sistēmai, jūs varēsiet izmantot vienu no kodiem kā papildus autentifikācijas mehānismu.',\n    'mfa_gen_backup_codes_download' => 'Lejupielādēt kodus',\n    'mfa_gen_backup_codes_usage_warning' => 'Katru kodu var izmantot tikai vienreiz',\n    'mfa_gen_totp_title' => 'Mobilās aplikācijas iestatījumi',\n    'mfa_gen_totp_desc' => 'Lai lietotu vairākfaktoru autentifikāciju, jums būs nepieciešama mobilā aplikācija, kas atbalsta TOTP, piemēram, Google Authenticator, Authy vai Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Skenējiet zemāk esošo kvadrātkodu (QR) izmantojot savu autentifikācijas aplikāciju.',\n    'mfa_gen_totp_verify_setup' => 'Verificēt iestatījumus',\n    'mfa_gen_totp_verify_setup_desc' => 'Pārbaudiet, ka viss darbojas, zemāk esošajā laukā ievadot kodu, ko izveidojusi jūsu autentifikācijas aplikācijā:',\n    'mfa_gen_totp_provide_code_here' => 'Norādīet jūsu aplikācijā izveidoto kodu šeit',\n    'mfa_verify_access' => 'Verificēt piekļuvi',\n    'mfa_verify_access_desc' => 'Jūsu lietotāja kontam nepieciešams verificēt jūsu identitāti ar papildus pārbaudes līmeni pirms piešķirta piekļuve. Verificējiet, izmantojot vienu no uzstādītajām metodēm, lai turpinātu.',\n    'mfa_verify_no_methods' => 'Nav iestatīta neviena metode',\n    'mfa_verify_no_methods_desc' => 'Jūsu kontam nav iestatīta neviena vairākfaktoru autentifikācijas metode. Jums būs nepieciešams iestatīt vismaz vienu metodi, lai iegūtu piekļuvi.',\n    'mfa_verify_use_totp' => 'Verificēt, izmantojot mobilo aplikāciju',\n    'mfa_verify_use_backup_codes' => 'Verificēt, izmantojot rezerves kodu',\n    'mfa_verify_backup_code' => 'Rezerves kods',\n    'mfa_verify_backup_code_desc' => 'Zemāk ievadiet vienu no jūsu atlikušajiem rezerves kodiem:',\n    'mfa_verify_backup_code_enter_here' => 'Ievadiet rezerves kodu šeit',\n    'mfa_verify_totp_desc' => 'Zemāk ievadiet kodu, kas izveidots mobilajā aplikācijā:',\n    'mfa_setup_login_notification' => 'Vairākfaktoru metode iestatīta, lūdzu pieslēdzieties atkal izmantojot iestatīto metodi.',\n];\n"
  },
  {
    "path": "lang/lv/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Atcelt',\n    'close' => 'Aizvērt',\n    'confirm' => 'Apstiprināt',\n    'back' => 'Atpakaļ',\n    'save' => 'Saglabāt',\n    'continue' => 'Turpināt',\n    'select' => 'Atlasīt',\n    'toggle_all' => 'Iezīmēt visus',\n    'more' => 'Vairāk',\n\n    // Form Labels\n    'name' => 'Nosaukums',\n    'description' => 'Apraksts',\n    'role' => 'Loma',\n    'cover_image' => 'Vāka attēls',\n    'cover_image_description' => 'Šim attēlam jābūt apmēram 440x250px izmērā, taču tas tiks pielāgots lietotāja saskarnei dažādos scenārijos pēc nepieciešamības, un attēla izmēri tad var atšķirties.',\n\n    // Actions\n    'actions' => 'Darbības',\n    'view' => 'Skatīt',\n    'view_all' => 'Skatīt visus',\n    'new' => 'Jauns',\n    'create' => 'Izveidot',\n    'update' => 'Atjaunināt',\n    'edit' => 'Rediģēt',\n    'archive' => 'Archive',\n    'unarchive' => 'Un-Archive',\n    'sort' => 'Kārtot',\n    'move' => 'Pārvietot',\n    'copy' => 'Kopēt',\n    'reply' => 'Atbildēt',\n    'delete' => 'Dzēst',\n    'delete_confirm' => 'Apstipriniet dzēšanu',\n    'search' => 'Meklēt',\n    'search_clear' => 'Notīrīt meklēšanu',\n    'reset' => 'Atiestatīt',\n    'remove' => 'Noņemt',\n    'add' => 'Pievienot',\n    'configure' => 'Mainīt konfigurāciju',\n    'manage' => 'Pārvaldīt',\n    'fullscreen' => 'Pilnekrāns',\n    'favourite' => 'Pievienot favorītiem',\n    'unfavourite' => 'Noņemt no favorītiem',\n    'next' => 'Nākamais',\n    'previous' => 'Iepriekšējais',\n    'filter_active' => 'Aktīvais filtrs:',\n    'filter_clear' => 'Notīrīt filtru',\n    'download' => 'Lejupielādēt',\n    'open_in_tab' => 'Atvērt cilnē',\n    'open' => 'Atvērt',\n\n    // Sort Options\n    'sort_options' => 'Kārtošanas Opcijas',\n    'sort_direction_toggle' => 'Pārslēgt kārtošanas virzienu',\n    'sort_ascending' => 'Kārtot Augoši',\n    'sort_descending' => 'Kārtot Dilstoši',\n    'sort_name' => 'Vārds',\n    'sort_default' => 'Noklusējums',\n    'sort_created_at' => 'Izveidošanas Datums',\n    'sort_updated_at' => 'Atjaunināšanas datums',\n\n    // Misc\n    'deleted_user' => 'Dzēsts lietotājs',\n    'no_activity' => 'Nav skatāmu darbību',\n    'no_items' => 'Vienumi nav pieejami',\n    'back_to_top' => 'Uz augšu',\n    'skip_to_main_content' => 'Pāriet uz saturu',\n    'toggle_details' => 'Rādīt aprakstu',\n    'toggle_thumbnails' => 'Iezīmēt sīkatēlus',\n    'details' => 'Sīkāka informācija',\n    'grid_view' => 'Režģa Skats',\n    'list_view' => 'Saraksta Skats',\n    'default' => 'Noklusējums',\n    'breadcrumb' => 'Navigācija',\n    'status' => 'Statuss',\n    'status_active' => 'Aktīvs',\n    'status_inactive' => 'Neaktīvs',\n    'never' => 'Nekad',\n    'none' => 'Neviens',\n\n    // Header\n    'homepage' => 'Sākumlapa',\n    'header_menu_expand' => 'Izvērst galvenes izvēlni',\n    'profile_menu' => 'Profila izvēlne',\n    'view_profile' => 'Apskatīt profilu',\n    'edit_profile' => 'Rediģēt profilu',\n    'dark_mode' => 'Tumšais režīms',\n    'light_mode' => 'Gaišais režīms',\n    'global_search' => 'Vispārējā meklēšana',\n\n    // Layout tabs\n    'tab_info' => 'Informācija',\n    'tab_info_label' => 'Tab: Rādīt sekundāro informāciju',\n    'tab_content' => 'Saturs',\n    'tab_content_label' => 'Tab: Rādīt galveno saturu',\n\n    // Email Content\n    'email_action_help' => 'Ja ir problēmas noklikšķināt \":actionText\" pogu, nokopē un ievieto saiti savā interneta pārlūkā:',\n    'email_rights' => 'Visas tiesības aizsargātas',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Privātuma politika',\n    'terms_of_service' => 'Pakalpojuma noteikumi',\n\n    // OpenSearch\n    'opensearch_description' => 'Meklēt :appName',\n];\n"
  },
  {
    "path": "lang/lv/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Attēla izvēle',\n    'image_list' => 'Attēlu saraksts',\n    'image_details' => 'Attēla dati',\n    'image_upload' => 'Augšupielādēt attēlu',\n    'image_intro' => 'Šeit jūs varat izvēlēties un pārvaldīt attēlus, kuri iepriekš tika aplugšupielādēti sistēmā.',\n    'image_intro_upload' => 'Augšupielādējiet jaunu attēlu ievelkot attēla failu šajā logā vai izmantojot \"Augšupielādēt attēlu\" pogu augstāk.',\n    'image_all' => 'Visi',\n    'image_all_title' => 'Skatīt visus attēlus',\n    'image_book_title' => 'Apskatīt augšupielādētos attēlus šajā grāmatā',\n    'image_page_title' => 'Apskatīt augšupielādētos attēlus šajā lapā',\n    'image_search_hint' => 'Meklēt pēc attēla vārda',\n    'image_uploaded' => 'Augšupielādēts :uploadedDate',\n    'image_uploaded_by' => 'Augšupielādēja :userName',\n    'image_uploaded_to' => 'Augšupielādēja :pageLink',\n    'image_updated' => 'Atjaunots :updateDate',\n    'image_load_more' => 'Ielādēt vairāk',\n    'image_image_name' => 'Attēla nosaukums',\n    'image_delete_used' => 'Šis attēls ir ievietots zemāk redzamajās lapās.',\n    'image_delete_confirm_text' => 'Vai tiešām vēlaties dzēst šo attēlu?',\n    'image_select_image' => 'Atlasīt attēlu',\n    'image_dropzone' => 'Ievilkt attēlu vai klikšķinat šeit, lai augšupielādētu',\n    'image_dropzone_drop' => 'Ievelciet attēlus šeit, lai augšupielādētu',\n    'images_deleted' => 'Dzēstie attēli',\n    'image_preview' => 'Attēla priekšskatījums',\n    'image_upload_success' => 'Attēls ir veiksmīgi augšupielādēts',\n    'image_update_success' => 'Attēlā informācija ir veiksmīgi atjunināta',\n    'image_delete_success' => 'Attēls veiksmīgi dzēsts',\n    'image_replace' => 'Nomainīt bildi',\n    'image_replace_success' => 'Attēla fails veiksmīgi atjaunots',\n    'image_rebuild_thumbs' => 'No jauna izveidot attēla dažādu izmēru variantus',\n    'image_rebuild_thumbs_success' => 'Attēlu varianti veiksmīgi atjaunoti!',\n\n    // Code Editor\n    'code_editor' => 'Rediģēt kodu',\n    'code_language' => 'Koda valoda',\n    'code_content' => 'Koda teksts',\n    'code_session_history' => 'Sesijas vēsture',\n    'code_save' => 'Saglabāt kodu',\n];\n"
  },
  {
    "path": "lang/lv/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Vispārīgi',\n    'advanced' => 'Papildu iespējas',\n    'none' => 'Neviens',\n    'cancel' => 'Atcelt',\n    'save' => 'Saglabāt',\n    'close' => 'Aizvērt',\n    'apply' => 'Pielietot',\n    'undo' => 'Atsaukt',\n    'redo' => 'Atcelt atsaukšanu',\n    'left' => 'Pa kreisi',\n    'center' => 'Centrā',\n    'right' => 'Pa labi',\n    'top' => 'Augšā',\n    'middle' => 'Vidū',\n    'bottom' => 'Apakšā',\n    'width' => 'Platums',\n    'height' => 'Augstums',\n    'More' => 'Vairāk',\n    'select' => 'Atlasīt...',\n\n    // Toolbar\n    'formats' => 'Formāti',\n    'header_large' => 'Liels virsraksts',\n    'header_medium' => 'Vidējs virsraksts',\n    'header_small' => 'Mazs virsraksts',\n    'header_tiny' => 'Ļoti mazs virsraksts',\n    'paragraph' => 'Rindkopa',\n    'blockquote' => 'Citāts',\n    'inline_code' => 'Kods iekļauts rindā',\n    'callouts' => 'Norādes',\n    'callout_information' => 'Informācija',\n    'callout_success' => 'Veiksmīgi',\n    'callout_warning' => 'Brīdinājums',\n    'callout_danger' => 'Bīstami',\n    'bold' => 'Treknraksts',\n    'italic' => 'Slīpraksts',\n    'underline' => 'Pasvītrojums',\n    'strikethrough' => 'Pārsvītrojums',\n    'superscript' => 'Augšraksts',\n    'subscript' => 'Apakšraksts',\n    'text_color' => 'Teksta krāsa',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Pielāgot krāsu',\n    'remove_color' => 'Noņemt krāsu',\n    'background_color' => 'Fona krāsa',\n    'align_left' => 'Līdzināt pa kreisi',\n    'align_center' => 'Līdzināt pa vidu',\n    'align_right' => 'Līdzināt pa labi',\n    'align_justify' => 'Līdzināt gar abām malām',\n    'list_bullet' => 'Nenumurēts saraksts',\n    'list_numbered' => 'Numurēts saraksts',\n    'list_task' => 'Uzdevumu saraksts',\n    'indent_increase' => 'Palielināt atkāpi',\n    'indent_decrease' => 'Samazināt atkāpi',\n    'table' => 'Tabula',\n    'insert_image' => 'Ievietot attēlu',\n    'insert_image_title' => 'Ievietot/rediģēt attēlu',\n    'insert_link' => 'Ievietot/rediģēt saiti',\n    'insert_link_title' => 'Ievietot/rediģēt saiti',\n    'insert_horizontal_line' => 'Ievietot horizontālu līniju',\n    'insert_code_block' => 'Ievietot koda bloku',\n    'edit_code_block' => 'Rediģēt koda bloku',\n    'insert_drawing' => 'Ievietot/rediģēt zīmējumu',\n    'drawing_manager' => 'Zīmēšanas pārvaldnieks',\n    'insert_media' => 'Ievietot/rediģēt mediju',\n    'insert_media_title' => 'Ievietot/rediģēt mediju',\n    'clear_formatting' => 'Notīrīt noformējumu',\n    'source_code' => 'Pirmkods',\n    'source_code_title' => 'Pirmkods',\n    'fullscreen' => 'Pilnekrāns',\n    'image_options' => 'Attēla uzstādījumu',\n\n    // Tables\n    'table_properties' => 'Tabulas īpašības',\n    'table_properties_title' => 'Tabulas īpašības',\n    'delete_table' => 'Dzēst tabulu',\n    'table_clear_formatting' => 'Notīrīt tabulas formatējumu',\n    'resize_to_contents' => 'Pielāgot izmēru saturam',\n    'row_header' => 'Rindas galvene',\n    'insert_row_before' => 'Ievietot rindu augstāk',\n    'insert_row_after' => 'Ievietot rindu zemāk',\n    'delete_row' => 'Dzēst rindu',\n    'insert_column_before' => 'Ievietot kolonnu pirms',\n    'insert_column_after' => 'Ievietot kolonnu pēc',\n    'delete_column' => 'Dzēst kolonnu',\n    'table_cell' => 'Šūna',\n    'table_row' => 'Rinda',\n    'table_column' => 'Kolonna',\n    'cell_properties' => 'Šūnas īpašības',\n    'cell_properties_title' => 'Šūnas īpašības',\n    'cell_type' => 'Šūnas tips',\n    'cell_type_cell' => 'Šūna',\n    'cell_scope' => 'Darbības lauks',\n    'cell_type_header' => 'Galvenes šūna',\n    'merge_cells' => 'Sapludināt šūnas',\n    'split_cell' => 'Sadalīt šūnas',\n    'table_row_group' => 'Rindu grupa',\n    'table_column_group' => 'Kolonnu grupa',\n    'horizontal_align' => 'Horizontāls novietojums',\n    'vertical_align' => 'Vertikāls novietojums',\n    'border_width' => 'Apmales platums',\n    'border_style' => 'Apmales veids',\n    'border_color' => 'Apmales krāsa',\n    'row_properties' => 'Rindas īpašības',\n    'row_properties_title' => 'Rindas īpašības',\n    'cut_row' => 'Izgriezt rindu',\n    'copy_row' => 'Kopēt rindu',\n    'paste_row_before' => 'Ielīmēt rindu augstāk',\n    'paste_row_after' => 'Ielīmēt rindu zemāk',\n    'row_type' => 'Rindas tips',\n    'row_type_header' => 'Galvene',\n    'row_type_body' => 'Pamata saturs',\n    'row_type_footer' => 'Kājene',\n    'alignment' => 'Līdzināšana',\n    'cut_column' => 'Izgriezt kolonnu',\n    'copy_column' => 'Kopēt kolonnu',\n    'paste_column_before' => 'Ielīmēt kolonnu pirms',\n    'paste_column_after' => 'Ielīmēt kolonnu pēc',\n    'cell_padding' => 'Šūnu iekšējais attālums',\n    'cell_spacing' => 'Šūnu attālums',\n    'caption' => 'Virsraksts',\n    'show_caption' => 'Parādīt virsrakstu',\n    'constrain' => 'Saglabāt proporcijas',\n    'cell_border_solid' => 'Pilna',\n    'cell_border_dotted' => 'Punktēta',\n    'cell_border_dashed' => 'Raustīta līnija',\n    'cell_border_double' => 'Dubulta',\n    'cell_border_groove' => 'Iedoba',\n    'cell_border_ridge' => 'Izcelta',\n    'cell_border_inset' => 'Iespiesta',\n    'cell_border_outset' => 'Pacelta',\n    'cell_border_none' => 'Nekas',\n    'cell_border_hidden' => 'Paslēpts',\n\n    // Images, links, details/summary & embed\n    'source' => 'Avots',\n    'alt_desc' => 'Alternatīvais apraksts',\n    'embed' => 'Iekļaut',\n    'paste_embed' => 'Iekopējiet savu iekļaušanas kodu zemāk:',\n    'url' => 'URL',\n    'text_to_display' => 'Attēlojamais teksts',\n    'title' => 'Nosaukums',\n    'browse_links' => 'Pārlūkot saites',\n    'open_link' => 'Atvērt saiti',\n    'open_link_in' => 'Atvērt saiti...',\n    'open_link_current' => 'Šis logs',\n    'open_link_new' => 'Jauns logs',\n    'remove_link' => 'Noņemt saiti',\n    'insert_collapsible' => 'Ievietot sakļaujamu bloku',\n    'collapsible_unwrap' => 'Attīt',\n    'edit_label' => 'Rediģēt marķējumu',\n    'toggle_open_closed' => 'Pārslēgt atvērts/aizvērts',\n    'collapsible_edit' => 'Rediģēt sakļaujamu bloku',\n    'toggle_label' => 'Pārslēgt marķējumu',\n\n    // About view\n    'about' => 'Par redaktoru',\n    'about_title' => 'Par WYSIWYG redaktoru',\n    'editor_license' => 'Redaktora licence un autortiesības',\n    'editor_lexical_license' => 'Šis redaktors ir izveidots, izmantojot :tinyLink, kas ir publicēts ar MIT licenci.',\n    'editor_lexical_license_link' => 'Pilnu licences informāciju var atrast šeit.',\n    'editor_tiny_license' => 'Šis redaktors ir izveidots, izmantojot :tinyLink, kas ir publicēts ar MIT licenci.',\n    'editor_tiny_license_link' => 'TinyMCE autortiesības un licences detaļas var atrast šeit.',\n    'save_continue' => 'Saglabāt lapu un turpināt',\n    'callouts_cycle' => '(Turpiniet spiest, lai pārslēgtu tipus)',\n    'link_selector' => 'Saite uz saturu',\n    'shortcuts' => 'Saīsnes',\n    'shortcut' => 'Saīsne',\n    'shortcuts_intro' => 'Šajā redaktorā pieejamas šādas saīsnes:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Apraksts',\n];\n"
  },
  {
    "path": "lang/lv/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Nesen izveidots',\n    'recently_created_pages' => 'Nesen izveidotās lapas',\n    'recently_updated_pages' => 'Nesen atjauninātās lapas',\n    'recently_created_chapters' => 'Nesen izveidotās nodaļas',\n    'recently_created_books' => 'Nesen izveidotās grāmatas',\n    'recently_created_shelves' => 'Nesen izveidotie plaukti',\n    'recently_update' => 'Nesen atjaunināts',\n    'recently_viewed' => 'Nesen skatītie',\n    'recent_activity' => 'Pēdējās aktivitātes',\n    'create_now' => 'Izveidot tagad',\n    'revisions' => 'Revīzijas',\n    'meta_revision' => 'Revīzija #:revisionCount',\n    'meta_created' => 'Izveidots :timeLength',\n    'meta_created_name' => ':user izveidojis pirms :timeLength',\n    'meta_updated' => 'Atjaunināts :timeLength',\n    'meta_updated_name' => ':user atjauninājis pirms :timeLength',\n    'meta_owned_name' => 'Īpašnieks :user',\n    'meta_reference_count' => 'Atsauce :count vienumā|Atsauce :count vienumos',\n    'entity_select' => 'Izvēlēties vienumu',\n    'entity_select_lack_permission' => 'Jums nav nepieciešamās piekļuves tiesības, lai izvēlētu šo vienumu',\n    'images' => 'Attēli',\n    'my_recent_drafts' => 'Mani melnraksti',\n    'my_recently_viewed' => 'Mani nesen skatītie',\n    'my_most_viewed_favourites' => 'Mani biežāk skatītie favorīti',\n    'my_favourites' => 'Mani favorīti',\n    'no_pages_viewed' => 'Neviena lapa vēl nav skatīta',\n    'no_pages_recently_created' => 'Nav radīta neviena lapa',\n    'no_pages_recently_updated' => 'Nav atjaunināta neviena lapa',\n    'export' => 'Eksportēt',\n    'export_html' => 'Pilna satura web fails',\n    'export_pdf' => 'PDF fails',\n    'export_text' => 'Vienkāršs teksta fails',\n    'export_md' => 'Markdown fails',\n    'export_zip' => 'Pārceļams ZIP arhīvs',\n    'default_template' => 'Noklusētā lapas sagatave',\n    'default_template_explain' => 'Norādīt lapas sagatavi, kas tiks izmantota kā noklusētais saturs visām jaunājām lapām šajā grāmatā. Ņemiet vērā, ka tā tiks izmantota tikai tad, ja lapas veidotājam ir skatīšanas tiesības izvēlētajai sagatavei.',\n    'default_template_select' => 'Izvēlēt sagataves lapu',\n    'import' => 'Importēt',\n    'import_validate' => 'Pārbaudīt importu',\n    'import_desc' => 'Importēt grāmatas, nodaļas un lapas izmantojot pārceļamu ZIP arhīvu no šīs vai citas sistēmas instances. Izvēlietites ZIP failu, lai turpinātu. Kad fails ir augšupielādēts un pārbaudīts, jūs varēsiet veikt importa uzstādījumus un to apstiprināt nākamajā skatā.',\n    'import_zip_select' => 'Izvēlieties ZIP failu, ko augšupielādēt',\n    'import_zip_validation_errors' => 'Pārbaudot ZIP failu atrastas šādas kļūdas:',\n    'import_pending' => 'Gaidošie importi',\n    'import_pending_none' => 'Neviens imports nav uzsākts.',\n    'import_continue' => 'Turpināt importu',\n    'import_continue_desc' => 'Pārlūkot saturu, kas tiktu importēts no augšupielādētā ZIP faila. Kad esat gatavs, palaidiet importu, lai pievienotu tā saturu šai sistēmai. Augšupielādētais ZIP fails tiks automātiski izvākts pēc veiksmīga importa.',\n    'import_details' => 'Importa detaļas',\n    'import_run' => 'Palaist importu',\n    'import_size' => ':size importa ZIP izmērs',\n    'import_uploaded_at' => 'Augšupielādes laiks :relativeTime',\n    'import_uploaded_by' => 'Augšupielādēja',\n    'import_location' => 'Importa vieta',\n    'import_location_desc' => 'Izvēlieties mērķa vietu jūsu importētajam saturam. Jums būs nepieciešamas attiecīgās piekļuves tiesības, lai izveidotu saturu izvēlētajā vietā.',\n    'import_delete_confirm' => 'Vai tiešām vēlaties dzēst šo importu?',\n    'import_delete_desc' => 'Šis izdzēsīs augšupielādēto importa ZIP failu, un šo darbību nevarēs atcelt.',\n    'import_errors' => 'Importa kļūdas',\n    'import_errors_desc' => 'Importa mēģinājumā atgadījās šīs kļūdas:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Atļaujas',\n    'permissions_desc' => 'Uzstādīt tiesības šeit, lai aizvietotu noklusētās tiesības no lietotāju lomām.',\n    'permissions_book_cascade' => 'Piekļuves tiesības, kas uzstādītas grāmatām, automātiski tiks piešķirtas pakārtotajām nodaļām un lapām, ja vien tām nav atsevišķi norādītas savas piekļuves tiesības.',\n    'permissions_chapter_cascade' => 'Piekļuves tiesības, kas uzstādītas nodaļām, automātiski tiks piešķirtas pakārtotajām lapām, ja vien tām nav atsevišķi norādītas savas piekļuves tiesības.',\n    'permissions_save' => 'Saglabāt atļaujas',\n    'permissions_owner' => 'Īpašnieks',\n    'permissions_role_everyone_else' => 'Visi pārējie',\n    'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',\n    'permissions_role_override' => 'Aizvietot lomas tiesības',\n    'permissions_inherit_defaults' => 'Mantot noklusētās vērtības',\n\n    // Search\n    'search_results' => 'Meklēšanas rezultāti',\n    'search_total_results_found' => ':count meklēšanas rezultāts|:count meklēšanas rezultāti',\n    'search_clear' => 'Notīrīt meklēšanu',\n    'search_no_pages' => 'Neviena lapa neatbilst meklēšanai',\n    'search_for_term' => 'Meklēt :term',\n    'search_more' => 'Vairāk rezultāti',\n    'search_advanced' => 'Paplašināta meklēšana',\n    'search_terms' => 'Meklēšanas parametri',\n    'search_content_type' => 'Satura tips',\n    'search_exact_matches' => 'Precīza atbilstība',\n    'search_tags' => 'Birku meklēšana',\n    'search_options' => 'Iestatījumi',\n    'search_viewed_by_me' => 'Manis apskatītie',\n    'search_not_viewed_by_me' => 'Neesmu skatījis',\n    'search_permissions_set' => 'Iestatītās atļaujas',\n    'search_created_by_me' => 'Manis izveidotie',\n    'search_updated_by_me' => 'Manis atjauninātie',\n    'search_owned_by_me' => 'Es esmu īpašnieks',\n    'search_date_options' => 'Datuma iestatījumi',\n    'search_updated_before' => 'Atjaunināts pirms',\n    'search_updated_after' => 'Atjaunināts pēc',\n    'search_created_before' => 'Izveidots pirms',\n    'search_created_after' => 'Izveidots pēc',\n    'search_set_date' => 'Norādīt datumu',\n    'search_update' => 'Atjaunināt meklētāju',\n\n    // Shelves\n    'shelf' => 'Plaukts',\n    'shelves' => 'Plaukti',\n    'x_shelves' => ':count Plaukts|:count Plaukti',\n    'shelves_empty' => 'Neviens plaukts nav izveidots',\n    'shelves_create' => 'Izveidot jaunu plauktu',\n    'shelves_popular' => 'Populāri plaukti',\n    'shelves_new' => 'Jauni plaukti',\n    'shelves_new_action' => 'Jauns plaukts',\n    'shelves_popular_empty' => 'Populārākie plaukti tiks rādīti šeit.',\n    'shelves_new_empty' => 'Pēdējie izveidotie plaukti tiks rādīti šeit.',\n    'shelves_save' => 'Saglabāt plauktu',\n    'shelves_books' => 'Grāmatas šajā plauktā',\n    'shelves_add_books' => 'Pievienot grāmatas šim plauktam',\n    'shelves_drag_books' => 'Ievelciet grāmatas zemāk, lai novietotu tās šajā plauktā',\n    'shelves_empty_contents' => 'Šim gŗamatplauktam nav pievienotu grāmatu',\n    'shelves_edit_and_assign' => 'Labot plauktu, lai tam pievienotu grāmatas',\n    'shelves_edit_named' => 'Rediģēt plauktu :name',\n    'shelves_edit' => 'Rediģēt plauktu',\n    'shelves_delete' => 'Dzēst plauktu',\n    'shelves_delete_named' => 'Dzēst plauktu :name',\n    'shelves_delete_explain' => \"Tiks dzēsts plaukts ar nosaukumu \\\":name\\\". Tajā ievietotās grāmatas netiks dzēstas.\",\n    'shelves_delete_confirmation' => 'Vai esat pārliecināts, ka vēlaties dzēst šo plauktu?',\n    'shelves_permissions' => 'Plaukta atļaujas',\n    'shelves_permissions_updated' => 'Plaukta atļaujas atjauninātas',\n    'shelves_permissions_active' => 'Plaukta atļaujas ir aktīvas',\n    'shelves_permissions_cascade_warning' => 'Plauktu piekļuves tiesības netiek automātiski piešķirtas tajā esošajām grāmatām. Tas ir tāpēc, ka grāmata var vienlaicīgi atrasties vairākos plauktos. Tomēr piekļuves tiesības var nokopēt uz plauktam pievienotajām grāmatām, izmantojot zemāk atrodamo opciju.',\n    'shelves_permissions_create' => 'Plaukta veidošanas tiesības tiek izmantotas tikai, lai kopētu tiesības uz pakārtotajām grāmatām, izmantojot zemāk esošo darbību. Tās nekontrolē iespēju izveidot jaunas grāmatas.',\n    'shelves_copy_permissions_to_books' => 'Kopēt grāmatplaukta atļaujas uz grāmatām',\n    'shelves_copy_permissions' => 'Kopēt atļaujas',\n    'shelves_copy_permissions_explain' => 'Pašreizējās plaukta piekļuves tiesības tiks piemērotas visām tajā esošajām grāmatām. Pirms ieslēgšanas pārliecinieties, ka visas izmaiņas plaukta piekļuves tiesības ir saglabātas.',\n    'shelves_copy_permission_success' => 'Plaukta piekļuves tiesības kopētas uz :count grāmatām',\n\n    // Books\n    'book' => 'Grāmata',\n    'books' => 'Grāmatas',\n    'x_books' => ':count grāmata|:count grāmatas',\n    'books_empty' => 'Neviena grāmata nav izveidota',\n    'books_popular' => 'Populārās grāmatas',\n    'books_recent' => 'Nesenās grāmatas',\n    'books_new' => 'Jaunas grāmatas',\n    'books_new_action' => 'Jauna grāmata',\n    'books_popular_empty' => 'Populārākās grāmatas tiks rādītas šeit.',\n    'books_new_empty' => 'Pēdējās izveidotās grāmatas tiks rādītas šeit.',\n    'books_create' => 'Izveidot jaunu grāmatu',\n    'books_delete' => 'Dzēst grāmatu',\n    'books_delete_named' => 'Dzēst grāmatu :bookName',\n    'books_delete_explain' => 'Šī darbība izdzēsīs grāmatu \\':bookName\\'. Visas lapas un nodaļas tiks izdzēstas.',\n    'books_delete_confirmation' => 'Vai esat pārliecināts, ka vēlaties dzēst šo grāmatu?',\n    'books_edit' => 'Labot grāmatu',\n    'books_edit_named' => 'Labot grāmatu :bookName',\n    'books_form_book_name' => 'Grāmatas nosaukums',\n    'books_save' => 'Saglabāt grāmatu',\n    'books_permissions' => 'Grāmatas atļaujas',\n    'books_permissions_updated' => 'Grāmatas atļaujas atjauninātas',\n    'books_empty_contents' => 'Lapas vai nodaļas vēl nav izveidotas šai grāmatai.',\n    'books_empty_create_page' => 'Izveidot jaunu lapu',\n    'books_empty_sort_current_book' => 'Kārtot šo grāmatu',\n    'books_empty_add_chapter' => 'Pievienot nodaļu',\n    'books_permissions_active' => 'Grāmatas atļaujas ir aktīvas',\n    'books_search_this' => 'Meklēt šajā grāmatā',\n    'books_navigation' => 'Grāmatas navigācija',\n    'books_sort' => 'Kārtot grāmatas saturu',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Kārtot grāmatu :bookName',\n    'books_sort_name' => 'Kārtot pēc nosaukuma',\n    'books_sort_created' => 'Kārtot pēc izveidošanas datuma',\n    'books_sort_updated' => 'Kārtot pēc atjaunināšanas datuma',\n    'books_sort_chapters_first' => 'Nodaļas pirmās',\n    'books_sort_chapters_last' => 'Nodaļas pēdējās',\n    'books_sort_show_other' => 'Rādīt citas grāmatas',\n    'books_sort_save' => 'Saglabāt jauno kārtību',\n    'books_sort_show_other_desc' => 'Pievienojiet citas grāmatas šeit, lai tās iekļautu kārtošanā un pieļautu vienkāršāku satura organizēšanu starp grāmatām.',\n    'books_sort_move_up' => 'Pārvietot uz augšu',\n    'books_sort_move_down' => 'Pārvietot uz leju',\n    'books_sort_move_prev_book' => 'Pārvietot uz iepriekšējo grāmatu',\n    'books_sort_move_next_book' => 'Pārvietot uz nākamo grāmatu',\n    'books_sort_move_prev_chapter' => 'Pārvietot uz iepriekšējo nodaļu',\n    'books_sort_move_next_chapter' => 'Pārvietot uz nākamo nodaļu',\n    'books_sort_move_book_start' => 'Pārvietot uz grāmatas sākumu',\n    'books_sort_move_book_end' => 'Pārvietot uz grāmatas beigām',\n    'books_sort_move_before_chapter' => 'Pārvietot pirms nodaļas',\n    'books_sort_move_after_chapter' => 'Pārvietot pēc nodaļas',\n    'books_copy' => 'Kopēt grāmatu',\n    'books_copy_success' => 'Grāmata veiksmīgi nokopēta',\n\n    // Chapters\n    'chapter' => 'Nodaļa',\n    'chapters' => 'Nodaļas',\n    'x_chapters' => ':count nodaļa|:count nodaļas',\n    'chapters_popular' => 'Populāras nodaļas',\n    'chapters_new' => 'Jauna nodaļa',\n    'chapters_create' => 'Izveidot jaunu nodaļu',\n    'chapters_delete' => 'Dzēst nodaļu',\n    'chapters_delete_named' => 'Dzēst nodaļu :chapterName',\n    'chapters_delete_explain' => 'Šī darbība dzēsīs nodaļu \\':chapterName\\'. Visas tajā esošās lapas arī tiks dzēstas.',\n    'chapters_delete_confirm' => 'Vai esat pārliecināts, ka vēlaties dzēst šo nodaļu?',\n    'chapters_edit' => 'Labot nodaļu',\n    'chapters_edit_named' => 'Labot nodaļu :chapterName',\n    'chapters_save' => 'Saglabāt nodaļu',\n    'chapters_move' => 'Pārvietot nodaļu',\n    'chapters_move_named' => 'Pārvietot nodaļu :chapterName',\n    'chapters_copy' => 'Kopēt nodaļu',\n    'chapters_copy_success' => 'Nodaļa veiksmīgi nokopēta',\n    'chapters_permissions' => 'Nodaļas atļaujas',\n    'chapters_empty' => 'Šajā nodaļā nav pievienotu lapu.',\n    'chapters_permissions_active' => 'Nodaļas atļaujas ir aktīvas',\n    'chapters_permissions_success' => 'Nodaļas atļaujas ir atjauninātas',\n    'chapters_search_this' => 'Meklēt šajā nodaļā',\n    'chapter_sort_book' => 'Kārtot grāmatu',\n\n    // Pages\n    'page' => 'Lapa',\n    'pages' => 'Lapas',\n    'x_pages' => ':count lapa|:count lapas',\n    'pages_popular' => 'Populātas lapas',\n    'pages_new' => 'Jauna lapa',\n    'pages_attachments' => 'Pielikumi',\n    'pages_navigation' => 'Lapas navigācija',\n    'pages_delete' => 'Dzēst lapu',\n    'pages_delete_named' => 'Dzēst lapu :pageName',\n    'pages_delete_draft_named' => 'Dzēst :pageName melnrakstu',\n    'pages_delete_draft' => 'Dzēst melnrakstu',\n    'pages_delete_success' => 'Lapa ir dzēsta',\n    'pages_delete_draft_success' => 'Melnraksts ir dzēsts',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'Vai esat pārliecināts, ka vēlaties dzēst šo lapu?',\n    'pages_delete_draft_confirm' => 'Vai esat pārliecināts, ka vēlaties dzēst šo melnrakstu?',\n    'pages_editing_named' => 'Rediģē lapu :pageName',\n    'pages_edit_draft_options' => 'Melnraksta iestatījumi',\n    'pages_edit_save_draft' => 'Saglabāt melnrakstu',\n    'pages_edit_draft' => 'Labot melnrakstu',\n    'pages_editing_draft' => 'Labo melnrakstu',\n    'pages_editing_page' => 'Labo lapu',\n    'pages_edit_draft_save_at' => 'Melnraksts saglabāts ',\n    'pages_edit_delete_draft' => 'Dzēst melnrakstu',\n    'pages_edit_delete_draft_confirm' => 'Vai tiešām vēlaties dzēst savas uzmetuma izmaiņas? Visas jūsu izmaiņas kopš pēdējās saglabāšanas pazudīs un redaktors tiks atjaunos ar pēdējo saglabātu lapas saturu.',\n    'pages_edit_discard_draft' => 'Atmest malnrakstu',\n    'pages_edit_switch_to_markdown' => 'Pārslēgties uz Markdown redaktoru',\n    'pages_edit_switch_to_markdown_clean' => '(Iztīrītais saturs)',\n    'pages_edit_switch_to_markdown_stable' => '(Stabilais saturs)',\n    'pages_edit_switch_to_wysiwyg' => 'Pārslēgties uz WYSIWYG redaktoru',\n    'pages_edit_switch_to_new_wysiwyg' => 'Pārslēgties uz jauno WYSIWYG redaktoru',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Pievienot izmaiņu aprakstu',\n    'pages_edit_enter_changelog_desc' => 'Ievadi nelielu aprakstu par vaiktajām izmaiņām',\n    'pages_edit_enter_changelog' => 'Izmaiņu apraksts',\n    'pages_editor_switch_title' => 'Pārslēgt redaktoru',\n    'pages_editor_switch_are_you_sure' => 'Vai tiešām vēlaties pārslēgt šai lapai lietojamo redaktoru?',\n    'pages_editor_switch_consider_following' => 'Pārslēdzot redaktorus, ņemiet vērā:',\n    'pages_editor_switch_consideration_a' => 'Pēc saglabāšanas jaunā redaktora izvēle tiks izmantota nākotnē visiem lietotājiem, tai skaitā tiem, kam var nebūt tiesības pašiem mainīt redaktora veidu.',\n    'pages_editor_switch_consideration_b' => 'Tas var noteiktos apstākļos novest pie iespējamiem noformējuma un sintakses zudumiem.',\n    'pages_editor_switch_consideration_c' => 'Birku vai izmaiņu saraksta ieraksti, kas veikti kopš pēdējās saglabāšanas, nesaglabāsies kopā ar šīm izmaiņām.',\n    'pages_save' => 'Saglabāt lapu',\n    'pages_title' => 'Lapas virsraksts',\n    'pages_name' => 'Lapas nosaukums',\n    'pages_md_editor' => 'Redaktors',\n    'pages_md_preview' => 'Priekšskatījums',\n    'pages_md_insert_image' => 'Ievietot attēlu',\n    'pages_md_insert_link' => 'Ievietot vienuma saiti',\n    'pages_md_insert_drawing' => 'Ievietot zīmējumu',\n    'pages_md_show_preview' => 'Rādīt priekšskatu',\n    'pages_md_sync_scroll' => 'Sync preview scroll',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Atrasts nesaglabāts attēls',\n    'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',\n    'pages_not_in_chapter' => 'Lapa nav nodaļā',\n    'pages_move' => 'Pārvietot lapu',\n    'pages_copy' => 'Kopēt lapu',\n    'pages_copy_desination' => 'Kopijas mērķa vieta',\n    'pages_copy_success' => 'Lapa veiksmīgi nokopēta',\n    'pages_permissions' => 'Lapas atļaujas',\n    'pages_permissions_success' => 'Lapas atļaujas atjauninātas',\n    'pages_revision' => 'Revīzijas',\n    'pages_revisions' => 'Lapas revīzijas',\n    'pages_revisions_desc' => 'Zemāk norādītas visas šīs lapas pagātnes versijas. Jūs varat pārskatīt, salīdzināt un atjaunot vecākas versijas, ja to atļauj jūsu piekļuves tiesības. Pilna lapas vēsture varētu netikt attēlota, jo, atkarībā no sistēmas uzstādījumiem, vecākas versijas varētu būt dzēstas automātiski.',\n    'pages_revisions_named' => ':pageName lapas revīzijas',\n    'pages_revision_named' => ':pageName lapas revīzija',\n    'pages_revision_restored_from' => 'Atjaunots no #:id; :summary',\n    'pages_revisions_created_by' => 'Izveidoja',\n    'pages_revisions_date' => 'Revīzijas datums',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Versijas numurs',\n    'pages_revisions_numbered' => 'Revīzija #:id',\n    'pages_revisions_numbered_changes' => 'Revīzijas #:id izmaiņas',\n    'pages_revisions_editor' => 'Redaktora veids',\n    'pages_revisions_changelog' => 'Izmaiņu žurnāls',\n    'pages_revisions_changes' => 'Izmaiņas',\n    'pages_revisions_current' => 'Pašreizējā versija',\n    'pages_revisions_preview' => 'Priekšskatījums',\n    'pages_revisions_restore' => 'Atjaunot',\n    'pages_revisions_none' => 'Šai lapai nav revīziju',\n    'pages_copy_link' => 'Kopēt saiti',\n    'pages_edit_content_link' => 'Pārlekt uz sadaļu redaktorā',\n    'pages_pointer_enter_mode' => 'Enter section select mode',\n    'pages_pointer_label' => 'Lapas sadaļas uzstādījumi',\n    'pages_pointer_permalink' => 'Lapas sadaļas saite',\n    'pages_pointer_include_tag' => 'Lapas sadaļas iekļaušanas tags',\n    'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag',\n    'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink',\n    'pages_permissions_active' => 'Lapas atļaujas ir aktīvas',\n    'pages_initial_revision' => 'Sākotnējā publikācija',\n    'pages_references_update_revision' => 'Automātiska iekšējo saišu atjaunināšana',\n    'pages_initial_name' => 'Jauna lapa',\n    'pages_editing_draft_notification' => 'Jūs pašlaik veicat izmaiņas melnrakstā, kurš pēdējo reizi ir saglabāts :timeDiff.',\n    'pages_draft_edited_notification' => 'Šī lapa ir tikusi atjaunināta. Šo melnrakstu ieteicams atmest.',\n    'pages_draft_page_changed_since_creation' => 'Šī lapa ir izmainīta kopš šī uzmetuma izveidošanas. Ieteicams šo uzmetumu dzēst, lai netiktu pazaudētas veiktās izmaiņas.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count lietotāji pašlaik veic izmaiņas šajā lapā',\n        'start_b' => ':userName veic izmaiņas šajā lapā',\n        'time_a' => 'kopš šī lapa pēdējo reizi ir atjaunināta',\n        'time_b' => 'pēdējās :minCount minūtēs',\n        'message' => ':start :time. Esat uzmanīgi, lai neaizstātu viens otra izmaiņas!',\n    ],\n    'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',\n    'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',\n    'pages_specific' => 'Konkrēta lapa',\n    'pages_is_template' => 'Lapas šablons',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Pārslēgt sānjoslu',\n    'page_tags' => 'Lapas birkas',\n    'chapter_tags' => 'Nodaļas birkas',\n    'book_tags' => 'Grāmatas birkas',\n    'shelf_tags' => 'Plauktu birkas',\n    'tag' => 'Birka',\n    'tags' =>  'Birkas',\n    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',\n    'tag_name' =>  'Birkas nosaukums',\n    'tag_value' => 'Birkas papildvērtība (neobligāta)',\n    'tags_explain' => \"Pievieno birkas, lai precīzāk grupētu saturu.\\n Tu vari pievienot papildus vērtību birkai vēl precīzākai grupēšanai.\",\n    'tags_add' => 'Pievienot vēlvienu birku',\n    'tags_remove' => 'Noņemt šo birku',\n    'tags_usages' => 'Kopējais birku lietojums',\n    'tags_assigned_pages' => 'Pievienotas lapām',\n    'tags_assigned_chapters' => 'Pievienotas nodaļām',\n    'tags_assigned_books' => 'Pievienotas grāmatām',\n    'tags_assigned_shelves' => 'Pievienotas plauktiem',\n    'tags_x_unique_values' => ':count unikālas vērtības',\n    'tags_all_values' => 'Visas vērtības',\n    'tags_view_tags' => 'Skatīt birkas',\n    'tags_view_existing_tags' => 'Skatīt esošās birkas',\n    'tags_list_empty_hint' => 'Birkas var pievienot lapas redaktora sānu kolonnā vai rediģējot grāmatas, nodaļas vai plaukta detaļas.',\n    'attachments' => 'Pielikumi',\n    'attachments_explain' => 'Augšupielādējiet dažus failus vai pievieno saites, kas tiks parādītas jūsu lapā. Tie būs redzami lapas sānjoslā.',\n    'attachments_explain_instant_save' => 'Izmaiņas šeit tiek saglabātas nekavējoties.',\n    'attachments_upload' => 'Augšupielādēt failu',\n    'attachments_link' => 'Pievienot saiti',\n    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',\n    'attachments_set_link' => 'Uzstādīt saiti',\n    'attachments_delete' => 'Vai tiešām vēlaties dzēst šo pielikumu?',\n    'attachments_dropzone' => 'Ievelciet failus šeit, lai augšupielādētu',\n    'attachments_no_files' => 'Neviens fails nav augšupielādēts',\n    'attachments_explain_link' => 'Ja nevēlaties augšupielādēt failu, varat pievienot saiti. Tā var būt saite uz citu lapu vai saite uz failu mākonī.',\n    'attachments_link_name' => 'Saites nosaukums',\n    'attachment_link' => 'Pielikuma saite',\n    'attachments_link_url' => 'Saite uz failu',\n    'attachments_link_url_hint' => 'Web lapas vai faila URL',\n    'attach' => 'Pievienot',\n    'attachments_insert_link' => 'Pievienot pielikuma saiti lapai',\n    'attachments_edit_file' => 'Rediģēt failu',\n    'attachments_edit_file_name' => 'Faila nosaukums',\n    'attachments_edit_drop_upload' => 'Ievelc failus vai spied šeit, lai augšupielādētu vai aizstātu failus',\n    'attachments_order_updated' => 'Pielikuma secība ir atjaunināta',\n    'attachments_updated_success' => 'Pielikuma informācja ir atjaunināta',\n    'attachments_deleted' => 'Pielikums dzēsts',\n    'attachments_file_uploaded' => 'Fails veiksmīgi augšupielādēts',\n    'attachments_file_updated' => 'Fails veiksmīgi atjaunināts',\n    'attachments_link_attached' => 'Hipersaite veismīgi pievienota lapai',\n    'templates' => 'Šabloni',\n    'templates_set_as_template' => 'Šī lapa ir šablons',\n    'templates_explain_set_as_template' => 'Jūs varat iestatīt šo lapu kā veidni, lai tās saturs tiktu izmantots, veidojot citas lapas. Citi lietotāji varēs izmantot šo veidni, ja viņiem būs atļauja piekļūt šai lapai.',\n    'templates_replace_content' => 'Aizstāt lapas saturu',\n    'templates_append_content' => 'Pievienot lapas saturam (beigās)',\n    'templates_prepend_content' => 'Pievienot lapas saturam (sākumā)',\n\n    // Profile View\n    'profile_user_for_x' => 'Lietotājs jau :time',\n    'profile_created_content' => 'Izveidotais saturs',\n    'profile_not_created_pages' => ':userName nav izveidojis lapas',\n    'profile_not_created_chapters' => ':userName nav izveidojis nodalas',\n    'profile_not_created_books' => ':userName nav izveidojis grāmatas',\n    'profile_not_created_shelves' => ':userName nav izveidojis grāmatplauktus',\n\n    // Comments\n    'comment' => 'Komentārs',\n    'comments' => 'Komentāri',\n    'comment_add' => 'Pievienot komentāru',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Pievieno komentāru',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Saglabāt komentāru',\n    'comment_new' => 'Jauns komentārs',\n    'comment_created' => 'komentējis :createDiff',\n    'comment_updated' => ':username atjauninājis pirms :updateDiff',\n    'comment_updated_indicator' => 'Atjaunots',\n    'comment_deleted_success' => 'Komentārs ir dzēsts',\n    'comment_created_success' => 'Komentārs ir pievienots',\n    'comment_updated_success' => 'Komentārs ir atjaunināts',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Vai esat pārliecināts, ka vēlaties dzēst šo komentāru?',\n    'comment_in_reply_to' => 'Atbildēt uz :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',\n\n    // Revision\n    'revision_delete_confirm' => 'Vai esat pārliecināts, ka vēlaties dzēst šo revīziju?',\n    'revision_restore_confirm' => 'Vai tiešām vēlaties atjaunot šo revīziju? Pašreizējais lapas saturs tiks aizvietots.',\n    'revision_cannot_delete_latest' => 'Nevar dzēst pašreizējo revīziju.',\n\n    // Copy view\n    'copy_consider' => 'Kopējot saturu, lūdzu ņemiet vērā tālāk minēto.',\n    'copy_consider_permissions' => 'Pielāgoti tiesību uzstādījumi netiks nokopēti.',\n    'copy_consider_owner' => 'Jūs kļūsiet par visa kopētā satura īpašnieku.',\n    'copy_consider_images' => 'Lapas attēlu faili netiks kopēti un sākotnējie attēli saglabās savu saistību ar lapu, kurai tie tika sākotnēji pievienoti.',\n    'copy_consider_attachments' => 'Lapai pievienotie faili netiks nokopēti.',\n    'copy_consider_access' => 'Atrašanās vietas, īpašnieka vai piekļuves tiesību izmaiņas var padarīt šo saturu pieejamu citiem, kam iepriekš nav dota piekļuve.',\n\n    // Conversions\n    'convert_to_shelf' => 'Pārveidot par plauktu',\n    'convert_to_shelf_contents_desc' => 'Jūs varat pārveidot šo grāmatu par jaunu plauktu ar to pašu saturu. Nodaļas šajā grāmatā tiks pārveidots par jaunām grāmatām. Ja šī grāmata satur atsevišķas lapas, kas neietilpst nevienā nodaļā, tiks izveidota atsevišķa grāmata ar šādām lapām, kas tiks ievietota jaunajā plauktā.',\n    'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.',\n    'convert_book' => 'Pārveidot grāmatu',\n    'convert_book_confirm' => 'Vai tiešām vēlaties pārveidot šo grāmatu?',\n    'convert_undo_warning' => 'To nav iespiejāms vienkārši atcelt.',\n    'convert_to_book' => 'Pārveidot par grāmatu',\n    'convert_to_book_desc' => 'Jūs varat pārveidot šo nodaļu par grāmatu ar tādu pašu saturu. Visas piekļuves tiesības, kas uzstādītas šai nodaļai, tiks kopētas uz jauno grāmatu, taču piekļuves tiesības, kas tiek uzstādītas pašreizējai grāmatai kopumā, netiks kopētas, tā kā ir iespējams, ka jaunajai grāmati var būt citas piekļuves tiesības.',\n    'convert_chapter' => 'Pārveidot nodaļu',\n    'convert_chapter_confirm' => 'Vai tiešām vēlaties pārveidot šo nodaļu?',\n\n    // References\n    'references' => 'Atsauces',\n    'references_none' => 'Uz šo vienumu nav atrasta neviena atsauce.',\n    'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',\n\n    // Watch Options\n    'watch' => 'Vērot',\n    'watch_title_default' => 'Noklusētie uzstādījumi',\n    'watch_desc_default' => 'Revert watching to just your default notification preferences.',\n    'watch_title_ignore' => 'Ignorēt',\n    'watch_desc_ignore' => 'Ignorēt visus paziņojumus, tai skaitā arī tādus, kas uzstādīti lietotāja uzstādījumos.',\n    'watch_title_new' => 'Jaunas lapas',\n    'watch_desc_new' => 'Notify when any new page is created within this item.',\n    'watch_title_updates' => 'Visi lapu atjauninājumi',\n    'watch_desc_updates' => 'Paziņot par visām jaunām lapām un lapu izmaiņām.',\n    'watch_desc_updates_page' => 'Paziņot par visām lapu izmaiņām.',\n    'watch_title_comments' => 'Visi lapu atjauninājumi un komentāri',\n    'watch_desc_comments' => 'Paziņot par visām jaunām lapām, lapu izmaiņām un jauniem komentāriem.',\n    'watch_desc_comments_page' => 'Paziņot par lapu izmaiņām un jauniem komentāriem.',\n    'watch_change_default' => 'Izmainīt noklusētos paziņojumu uzstādījumus',\n    'watch_detail_ignore' => 'Ignorēt paziņojumus',\n    'watch_detail_new' => 'Vērot jaunas lapas',\n    'watch_detail_updates' => 'Vērot jaunas lapas un atjauninājumus',\n    'watch_detail_comments' => 'Vērot jaunas lapas, atjauninājumus un komentārus',\n    'watch_detail_parent_book' => 'Watching via parent book',\n    'watch_detail_parent_book_ignore' => 'Ignoring via parent book',\n    'watch_detail_parent_chapter' => 'Watching via parent chapter',\n    'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter',\n];\n"
  },
  {
    "path": "lang/lv/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Jums nav atļauts piekļūt šai lapai.',\n    'permissionJson' => 'Jums nav atļauts veikt konkrēto darbību.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Lietotājs ar epastu :email bet ar citiem piekļuves datiem jau eksistē.',\n    'auth_pre_register_theme_prevention' => 'Lietotāja kontu nevar reģistrēt ar norādītajām detaļām',\n    'email_already_confirmed' => 'Epasts jau ir apstiprināts, mēģini ielogoties.',\n    'email_confirmation_invalid' => 'Šis apstiprinājuma žetons nav derīgs vai jau ir izmantots. Lūdzu, mēģiniet reģistrēties vēlreiz.',\n    'email_confirmation_expired' => 'Apstiprinājuma žetona derīguma termiņš ir beidzies. Ir nosūtīts jauns apstiprinājuma e-pasts.',\n    'email_confirmation_awaiting' => 'Šī konta e-pasta adresei ir nepieciešms apstiprinājums',\n    'ldap_fail_anonymous' => 'LDAP piekļuve neveiksmīga izmantojot anonymous bind',\n    'ldap_fail_authed' => 'LDAP piekļuve neveiksmīga izmantojot norādīto dn un paroli',\n    'ldap_extension_not_installed' => 'LDAP PHP paplašinājums nav instalēts',\n    'ldap_cannot_connect' => 'Nav iespējams pieslēgties LDAP serverim, sākotnējais pieslēgums neveiksmīgs',\n    'saml_already_logged_in' => 'Jau ielogojies',\n    'saml_no_email_address' => 'Ārējās autentifikācijas sistēmas sniegtajos datos nevarēja atrast šī lietotāja e-pasta adresi',\n    'saml_invalid_response_id' => 'Ārējās autentifikācijas sistēmas pieprasījums neatpazīst procesu, kuru sākusi šī lietojumprogramma. Pārvietojoties atpakaļ pēc pieteikšanās var rasties šāda problēma.',\n    'saml_fail_authed' => 'Piekļuve ar :system neizdevās, sistēma nepieļāva veiksmīgu autorizāciju',\n    'oidc_already_logged_in' => 'Jau esat ielogojies',\n    'oidc_no_email_address' => 'Ārējās autentifikācijas sistēmas sniegtajos datos nevarēja atrast šī lietotāja e-pasta adresi',\n    'oidc_fail_authed' => 'Piekļuve ar :system neizdevās, sistēma nepieļāva veiksmīgu autorizāciju',\n    'social_no_action_defined' => 'Darbības nav definētas',\n    'social_login_bad_response' => \"Saņemta kļūda izmantojot :socialAccount piekļuvi:\\n:error\",\n    'social_account_in_use' => 'Šis :socialAccount konts jau tiek izmantots, mēģiniet ieiet ar :socialAccount piekļuves iespēju.',\n    'social_account_email_in_use' => 'Šis epasts :email jau tiek izmantots. Ja jums jau ir konts, jūs varat pieslēgt savu :socialAccount kontu savos profila uzstādījumos.',\n    'social_account_existing' => 'Šis :socialAccount konts jau ir piesaistīts jūsu profilam.',\n    'social_account_already_used_existing' => 'Šo :socialAccount konts jau ir piesaistīts citam lietotājam.',\n    'social_account_not_used' => 'Šis :socialAccount konts nav piesaistīts nevienam lietotājām. Lūdzu pievienojiet to savos profila uzstādījumos. ',\n    'social_account_register_instructions' => 'Ja jums vēl nav savs konts, jūs varat reģistrēt kontu izmantojot :socialAccount piekļuvi.',\n    'social_driver_not_found' => 'Sociālā tīkla savienojums nav atrasts',\n    'social_driver_not_configured' => 'Jūsu :socialAccount sociālie iestatījumi nav uzstādīti pareizi.',\n    'invite_token_expired' => 'Šī uzaicinājuma saite ir novecojusi. Tā vietā jūs varat mēģināt atiestatīt sava konta paroli.',\n    'login_user_not_found' => 'Šai darbībai netika atrasts lietotājs.',\n\n    // System\n    'path_not_writable' => 'Faila ceļā :filePath nav iespējams ielādēt failus. Lūdzu pārliecinieties, ka serverim tur ir rakstīšanas tiesības.',\n    'cannot_get_image_from_url' => 'Nevar iegūt bildi no :url',\n    'cannot_create_thumbs' => 'Serveris nevar izveidot samazinātus attēlus. Lūdzu pārbaudiet, vai ir uzstādīts PHP GD paplašinājums.',\n    'server_upload_limit' => 'Serveris neatļauj šāda izmēra failu ielādi. Lūdzu mēģiniet mazāka izmēra failu.',\n    'server_post_limit' => 'Serveris nevar apstrādāt šāda izmēra datus. Lūdzu mēģiniet vēlreiz ar mazāku datu apjomu vai mazāku failu.',\n    'uploaded'  => 'Serveris neatļauj šāda izmēra failu ielādi. Lūdzu mēģiniet mazāka izmēra failu.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Radās kļūda augšupielādējot attēlu',\n    'image_upload_type_error' => 'Ielādējamā attēla tips nav derīgs',\n    'image_upload_replace_type' => 'Aizvietojot attēlu tipiem ir jābūt vienādiem',\n    'image_upload_memory_limit' => 'Neizdevās apstrādāt attēla ielādi vai izveidot attēlu variantus sistēmas resursu ierobežojumu dēļ.',\n    'image_thumbnail_memory_limit' => 'Neizdevās izveidot attēla dažādu izmēru variantus sistēmas resursu ierobežojumu dēļ.',\n    'image_gallery_thumbnail_memory_limit' => 'Neizdevās izveidot galerijas sīktēlus sistēmas resursu ierobežojumu dēļ.',\n    'drawing_data_not_found' => 'Attēla datus nevarēja ielādēt. Attēla fails, iespējams, vairs neeksistē, vai arī jums varētu nebūt piekļuves tiesības tam.',\n\n    // Attachments\n    'attachment_not_found' => 'Pielikums nav atrasts',\n    'attachment_upload_error' => 'Radās kļūda augšupielādējot pievienoto failu',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Neizdevās saglabāt uzmetumu. Pārliecinieties, ka jūsu interneta pieslēgums ir aktīvs pirms saglabājiet šo lapu',\n    'page_draft_delete_fail' => 'Neizdevās izdzēst lapas melnrakstu un iegūt pašreizējās lapas saglabāto saturu',\n    'page_custom_home_deletion' => 'Nav iespējams izdzēst lapu kamēr tā ir uzstādīta kā sākumlapa',\n\n    // Entities\n    'entity_not_found' => 'Vienība nav atrasta',\n    'bookshelf_not_found' => 'Plaukts nav atrasts',\n    'book_not_found' => 'Grāmata nav atrasta',\n    'page_not_found' => 'Lapa nav atrasta',\n    'chapter_not_found' => 'Nodaļa nav atrasta',\n    'selected_book_not_found' => 'Iezīmētā grāmata nav atrasta',\n    'selected_book_chapter_not_found' => 'Izvēlētā grāmata vai nodaļa nav atrasta',\n    'guests_cannot_save_drafts' => 'Viesi nevar saglabāt melnrakstus',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Jūs nevarat dzēst vienīgo administratoru',\n    'users_cannot_delete_guest' => 'Jūs nevarat dzēst lietotāju \"viesis\"',\n    'users_could_not_send_invite' => 'Neizdevās izveidot lietotāju, jo neizdevās nosūtīt ielūguma epastu',\n\n    // Roles\n    'role_cannot_be_edited' => 'Šo lomu nevar rediģēt',\n    'role_system_cannot_be_deleted' => 'Šī ir sistēmas loma un nevar tikt izdzēsta',\n    'role_registration_default_cannot_delete' => 'Šī loma nevar tikt izdzēsta, kamēr tā uzstādīta kā noklusētā reģistrācijas loma',\n    'role_cannot_remove_only_admin' => 'Šis ir vienīgais lietotājs, kam norādīta administratora loma. Pievienojiet administratora lomu citam lietotājam pirms mēģiniet to izslēgt šeit.',\n\n    // Comments\n    'comment_list' => 'Radās kļūda ielasot komentārus.',\n    'cannot_add_comment_to_draft' => 'Melnrakstam nevar pievienot komentārus.',\n    'comment_add' => 'Radās kļūda pievienojot/atjaunojot komentāru.',\n    'comment_delete' => 'Radās kļūda dzēšot komentāru.',\n    'empty_comment' => 'Nevar pievienot tukšu komentāru.',\n\n    // Error pages\n    '404_page_not_found' => 'Lapa nav atrasta',\n    'sorry_page_not_found' => 'Atvainojiet, meklētā lapa nav atrasta.',\n    'sorry_page_not_found_permission_warning' => 'Ja šai lapai būtu bijis te jābūt, jums var nebūt pietiekamas piekļuves tiesības, lai to apskatītu.',\n    'image_not_found' => 'Attēls nav atrasts',\n    'image_not_found_subtitle' => 'Atvainojiet, meklētais attēla fails nav atrasts.',\n    'image_not_found_details' => 'Ja attēlam būtu jābūt pieejamam, iespējams, tas ir ticis izdzēsts.',\n    'return_home' => 'Atgriezties uz sākumu',\n    'error_occurred' => 'Radusies kļūda',\n    'app_down' => ':appName pagaidām nav pieejams',\n    'back_soon' => 'Drīz būs atkal pieejams.',\n\n    // Import\n    'import_zip_cant_read' => 'Nevarēja nolasīt ZIP failu.',\n    'import_zip_cant_decode_data' => 'Nevarēja atrast un nolasīt data.json saturu ZIP failā.',\n    'import_zip_no_data' => 'ZIP faila datos nav atrasts grāmatu, nodaļu vai lapu saturs.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'ZIP faila imports ir neveiksmīgs ar šādām kļūdām:',\n    'import_zip_failed_notification' => 'ZIP faila imports ir neveiksmīgs.',\n    'import_perms_books' => 'Jums nav nepieciešamo tiesību izveidot grāmatas.',\n    'import_perms_chapters' => 'Jums nav nepieciešamo tiesību izveidot nodaļas.',\n    'import_perms_pages' => 'Jums nav nepieciešamo tiesību izveidot lapas.',\n    'import_perms_images' => 'Jums nav nepieciešamo tiesību izviedot attēlus.',\n    'import_perms_attachments' => 'Jums nav nepieciešamo tiesību izveidot pielikumus.',\n\n    // API errors\n    'api_no_authorization_found' => 'Pieprasījumā nav atrasts autorizācijas žetons',\n    'api_bad_authorization_format' => 'Pieprasījumā atrasts autorizācijas žetons, taču tā formāts nav pareizs',\n    'api_user_token_not_found' => 'Nav atrasts norādītajam autorizācijas žetonam atbilstošs API žetons',\n    'api_incorrect_token_secret' => 'Norādītā slepenā atslēga izmantotajam API žetonam nav pareiza',\n    'api_user_no_api_permission' => 'Izmantotā API žetona īpašniekam nav tiesības veikt API izsaukumus',\n    'api_user_token_expired' => 'Autorizācijas žetona derīguma termiņš ir izbeidzies',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Radusies kļūda sūtot testa epastu:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'Adrese (URL) nesakrīt ar atļautajām SSR adresēm',\n];\n"
  },
  {
    "path": "lang/lv/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Jauns komentārs lapā: :pageName',\n    'new_comment_intro' => ':appName: lietotājs komentējis lapu',\n    'new_page_subject' => 'Jauna lapa: :pageName',\n    'new_page_intro' => 'Jauna lapa izveidota :appName:',\n    'updated_page_subject' => 'Atjaunināta lapa: :pageName',\n    'updated_page_intro' => 'Lapa atjaunināta :appName:',\n    'updated_page_debounce' => 'Lai novērstu pārliecīgu paziņojumu sūtīšanu, uz laiku jums tiks pārtraukti paziņojumi par turpmākiem šī lietotāja labojumiem šai lapai.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Lapas nosaukums:',\n    'detail_page_path' => 'Ceļš uz lapu:',\n    'detail_commenter' => 'Komentētājs:',\n    'detail_comment' => 'Komentārs:',\n    'detail_created_by' => 'Izveidoja:',\n    'detail_updated_by' => 'Atjaunināja:',\n\n    'action_view_comment' => 'Skatīt komentāru',\n    'action_view_page' => 'Skatīt lapu',\n\n    'footer_reason' => 'Šis paziņojums nosūtīts tāpēc, ka :link paredz šādu aktivitāti šai vienībai.',\n    'footer_reason_link' => 'paziņojumu vēlamie iestatījumi',\n];\n"
  },
  {
    "path": "lang/lv/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Iepriekšējais',\n    'next'     => 'Nākamais &raquo;',\n\n];\n"
  },
  {
    "path": "lang/lv/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Parolēm jābūt vismaz astoņu simbolu garām un jāatbilst apstiprinājumam.',\n    'user' => \"Mēs nevaram atrast lietotāju ar šādu e-pasta adresi.\",\n    'token' => 'Paroles atiestatīšanas atslēga neatbilst šai e-pasta adresei.',\n    'sent' => 'Esam nosūtījuši paroles atiestatīšanas saiti!',\n    'reset' => 'Parole ir atiestatīta!',\n\n];\n"
  },
  {
    "path": "lang/lv/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Mans konts',\n\n    'shortcuts' => 'Saīsnes',\n    'shortcuts_interface' => 'Saskarnes īsceļu iestatījumi',\n    'shortcuts_toggle_desc' => 'Šeit jūs varat ieslēgt vai izslēgt sistēmas saskarnes klaviatūras īsceļus, kas tiek izmantoti navigācijai un darbībām.',\n    'shortcuts_customize_desc' => 'Jūs varat pielāgot katru no zemāk esošajiem īsceļiem. Vienkārši nospiediet nepieciešamo pogu kombināciju pēc tam, kad izvēlēts īsceļa ievadlauks.',\n    'shortcuts_toggle_label' => 'Klaviatūras saīsnes ieslēgtas',\n    'shortcuts_section_navigation' => 'Navigācija',\n    'shortcuts_section_actions' => 'Biežākās darbības',\n    'shortcuts_save' => 'Saglabāt saīsnes',\n    'shortcuts_overlay_desc' => 'Piezīme: kad īsceļi ir ieslēgti, ir pieejams palīdzības lodziņš, kas parādās, nospiežot \"?\". Tas attēlos pieejamos īsceļus tajā brīdī pieejamajām darbībām.',\n    'shortcuts_update_success' => 'Saīsņu uzstādījumi ir saglabāt!',\n    'shortcuts_overview_desc' => 'Pārvaldīt klaviatūras īsceļus, ko var izmantot sistēmas saskarnes navigācijai.',\n\n    'notifications' => 'Paziņojumu iestatījumi',\n    'notifications_desc' => 'Pārvaldiet epasta paziņojumus, ko saņemsiet, kad sistēmā tiek veiktas noteiktas darbības.',\n    'notifications_opt_own_page_changes' => 'Paziņot par izmaiņām manās lapās',\n    'notifications_opt_own_page_comments' => 'Paziņot par komentāriem manās lapās',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Paziņot par atbildēm uz maniem komentāriem',\n    'notifications_save' => 'Saglabāt iestatījumus',\n    'notifications_update_success' => 'Paziņojumu iestatījumi ir atjaunoti!',\n    'notifications_watched' => 'Vērotie un ignorētie vienumi',\n    'notifications_watched_desc' => 'Zemāk ir vienumi, kam piemēroti īpaši vērošanas nosacījumi. Lai atjaunintātu savus uzstādījums šiem, apskatiet vienumu un tad sameklējiet vērošanas uzstādījumus sānu kolonnā.',\n\n    'auth' => 'Piekļuve un drošība',\n    'auth_change_password' => 'Mainīt paroli',\n    'auth_change_password_desc' => 'Mainīt paroli, ko izmantojat, lai piekļūtu aplikācijai. Tai jābūt vismaz 8 simbolus garai.',\n    'auth_change_password_success' => 'Parole ir nomainīta!',\n\n    'profile' => 'Profila informācija',\n    'profile_desc' => 'Pārvaldiet sava konta detaļas, kas jūs attēlo citiem lietotājiem, kā arī detaļas, kas tiek izmantotas saziņai un sistēmas pielāgošanai.',\n    'profile_view_public' => 'Skatīt publisko profilu',\n    'profile_name_desc' => 'Uzstādiet savu vārdu, kas tiks parādīts citiem lietotājiem sistēmā pie jūsu darbībām un jums piederošā satura.',\n    'profile_email_desc' => 'Šis epasts tiks izmantots paziņojumiem un sistēmas piekļuvei, atkarībā no sistēmā uzstādītās autentifikācijas metodes.',\n    'profile_email_no_permission' => 'Diemžēl jums nav tiesību mainīt savu epasta adresi. Ja vēlaties to mainīt, jums jāsazinās ar administratoru, lai tas nomaina šo adresi.',\n    'profile_avatar_desc' => 'Izvēlieties attēlu, kas tiks izmantots, lai jūs attēlotu citiem sistēmas lietotājiem. Ideālā gadījumā šim attēlam jābūt kvadrātaveida, apmēram 256px platumā un augstumā.',\n    'profile_admin_options' => 'Administratora iestatījumi',\n    'profile_admin_options_desc' => 'Papildus administratora iestatījumus, kā piemēram, lomu piešķiršanu, var atrast jūsu lietotāja kontā ejot uz \"Uzstādījumu > Lietotāji\".',\n\n    'delete_account' => 'Dzēst kontu',\n    'delete_my_account' => 'Izdzēst manu kontu',\n    'delete_my_account_desc' => 'Šī darbība pilnībā izdzēsīs jūsu lietotāja kontu no sistēmas. Jūs vairs nevarēsiet piekļūt kontam, atcelt šo darbību vai atjaunot kontu. Jūsu izveidotais saturs, piemēram, izveidotās lapas un augšupielādētie attēli, tiks saglabāts.',\n    'delete_my_account_warning' => 'Vai tiešām vēlaties dzēst savu kontu?',\n];\n"
  },
  {
    "path": "lang/lv/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Iestatījumi',\n    'settings_save' => 'Saglabāt iestatījumus',\n    'system_version' => 'Sistēmas versija',\n    'categories' => 'Kategorijas',\n\n    // App Settings\n    'app_customization' => 'Pielāgojumi',\n    'app_features_security' => 'Funkcijas un drošība',\n    'app_name' => 'Lietotnes nosaukums',\n    'app_name_desc' => 'Šis vārds tiks rādīts navigācijas joslā un sistēmas sūtītajis e-pastos.',\n    'app_name_header' => 'Rādīt vārdu navigācijas joslā',\n    'app_public_access' => 'Publiska piekļuve',\n    'app_public_access_desc' => 'Šīs opcijas ieslēgšana ļaus neautorizētiem apmeklētājiem piekļūt jūsu BookStack saturam.',\n    'app_public_access_desc_guest' => 'Publisku apmeklētāju piekļuvi var kontrolēt \"Guest\" (Viesa) lietotāja uzstādījumos.',\n    'app_public_access_toggle' => 'Atļaut publisku piekļuvi',\n    'app_public_viewing' => 'Atļaut publisku piekļuvi?',\n    'app_secure_images' => 'Paaugstinātas drošības attēlu ielāde',\n    'app_secure_images_toggle' => 'Ieslēgt paaugstinātas drošības attēlu ielādi',\n    'app_secure_images_desc' => 'Ātrdarbības nolūkos attēli ir publiski pieejami. Šī opcija pievieno nejaušu grūti uzminamu teksta virkni attēlu adresēs. Pārliecinieties kā ir izslēgta direktoriju pārlūkošana, lai nepieļautu vieglu piekļuvi šiem failiem.',\n    'app_default_editor' => 'Noklusētais lapu redaktors',\n    'app_default_editor_desc' => 'Izvēlieties noklusēto redaktoru jaunu lapu rediģēšanai. To iespējams norādīt arī lapu līmenī, ja piekļuves tiesības to atļauj.',\n    'app_custom_html' => 'Pielāgot HTML head saturu',\n    'app_custom_html_desc' => 'Šis saturs tiks pievienots <head> sadaļas apakšā visām lapām. Tas ir noderīgi papildinot CSS stilus vai pievienojot analītikas kodu.',\n    'app_custom_html_disabled_notice' => 'Pielāgots HTML head saturs ir izslēgts šajā uzstādījumu lapā, lai nodrošinātu, ka iespējams atcelt jebkādas kritiskas izmaiņas.',\n    'app_logo' => 'Lietotnes logo',\n    'app_logo_desc' => 'Tas tiek izmantots lietotnes galvenē un citās vietās. Attēlam jābūt 86px augstam, lieli attēli tiks samazināti.',\n    'app_icon' => 'Lietotnes ikona',\n    'app_icon_desc' => 'Ikona tiek izmantota pārlūka cilnēm un īsceļiem. Tai jābūt 256px kvadrātveida PNG attēlam.',\n    'app_homepage' => 'Aplikācijas sākumlapa',\n    'app_homepage_desc' => 'Izvēlēties skatu, ko rādīt sākumlapā noklusētā skata vietā. Lapas piekļuves tiesības izvēlētajai lapai netiks ņemtas vērā.',\n    'app_homepage_select' => 'Izvēlēties lapu',\n    'app_footer_links' => 'Kājenes saites',\n    'app_footer_links_desc' => 'Pievienot saites, ko attēlot lapas kājenē. Tās tiks attēlotas lielākās daļas lapu apakšā, ieskaitot tās, kas pieejamas bez reģistrācijas. Jūs varat izmantot nosaukumu \"trans::<key>\", lai izmantotu sistēmā definētus tulkojumus. Piemēram, \"trans::common.privacy_policy\" tiks aizvietots ar tulkoto tekstu \"Privātuma politika\" un \"trans::common.terms_of_service\" kļūs par \"Lietošanas noteikumi\".',\n    'app_footer_links_label' => 'Saites nosaukums',\n    'app_footer_links_url' => 'Saites URL',\n    'app_footer_links_add' => 'Pievienot kājenes saiti',\n    'app_disable_comments' => 'Izslēgt komentārus',\n    'app_disable_comments_toggle' => 'Izslēgt komentārus',\n    'app_disable_comments_desc' => 'Atslēdz komentārus visās aplikācijas lapās.<br> Jau eksistējoši komentāri netiks attēloti.',\n\n    // Color settings\n    'color_scheme' => 'Lietotnes krāsu shēma',\n    'color_scheme_desc' => 'Uzstādiet krāsas, ko izmantot aplikācijas lietotātja saskarnē. Krāsas var uzstādīt atsevišķi gaišajam un tumšajam režīmam, lai labāk iederētos vizuālajā tēmā un nodrošinātu lasāmību.',\n    'ui_colors_desc' => 'Uzstādiem aplikācijas primāro krāsu un noklusēto saišu krāsu. Primārā krāsa tiek izmantota galvenokārt lapas galvenē, uz pogām un saskarnes dekoratīvajos elementos. Noklusētā saišu krāsa tiek lietota teksta saitēm un darbībām gan izveidotajā saturā, gan aplikācijas saskarnē.',\n    'app_color' => 'Pamatkrāsa',\n    'link_color' => 'Noklusētā saišu krāsa',\n    'content_colors_desc' => 'Norādīt krāsas visiem lapas hierarhijas elementiem. Lasāmības labad ieteicams izvēlēties krāsas ar līdzīgu spilgtumu kā noklusētajām.',\n    'bookshelf_color' => 'Plaukta krāsa',\n    'book_color' => 'Grāmatas krāsa',\n    'chapter_color' => 'Nodaļas krāsa',\n    'page_color' => 'Lapas krāsa',\n    'page_draft_color' => 'Lapas uzmetuma krāsa',\n\n    // Registration Settings\n    'reg_settings' => 'Reģistrācija',\n    'reg_enable' => 'Iespējot reģistrāciju',\n    'reg_enable_toggle' => 'Iespējot reģistrāciju',\n    'reg_enable_desc' => 'Kad reģistrācija ir ieslēgta, lietotāji varēs paši reģistrēties kā aplikācijas lietotāji. Pēc reģistrācijas tiem tiks piešķirta noklusētā lietotāja loma.',\n    'reg_default_role' => 'Noklusētā lietotāja loma pēc reģistrācijas',\n    'reg_enable_external_warning' => 'Šis uzstādījums tiek ignorēts kamēr tiek izmantota ārēja LDAP vai SAML autentifikācija. Tiks izveidoti lietotāju konti neeksistējošiem leitotājiem, ja autentifikācija pret ārējo sistēmu būs veiksmīga.',\n    'reg_email_confirmation' => 'E-pasta apstiprinājums',\n    'reg_email_confirmation_toggle' => 'Pieprasīt epasta apstiprināšanu',\n    'reg_confirm_email_desc' => 'Ja ieslēgts domēnu ierobežojums, tad būs nepieciešama epasta apstiprināšana un šis uzstādījums tiks ignorēts.',\n    'reg_confirm_restrict_domain' => 'Domēnu ierobežojums',\n    'reg_confirm_restrict_domain_desc' => 'Ievadiet ar komatiem atdalītu sarakstu ar epasta domēniem, kam jūs gribētu atļaut reģistrāciju. Lietotājiem tiks nosūtīts epasts, lai apstiprinātu tā adresi pirms tiks ļauts darboties ar aplikāciju. <br> Ņemiet vērā, ka lietotāji varēs nomainīt savu epasta adresi pēc veiksmīgas reģistrācijas.',\n    'reg_confirm_restrict_domain_placeholder' => 'Nav ierobežojumu',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Izvēlieties noklusēto kārtošanas nosacījumu, ko pielietot jaunām grāmatām. Šis neskars jau esošas grāmatas, un to var izmainīt grāmatas iestatījumos.',\n    'sorting_rules' => 'Kārtošanas noteikumi',\n    'sorting_rules_desc' => 'Šīs ir iepriekš noteiktas kārtošanas darbības, ko var pielietot saturam šajā sistēmā.',\n    'sort_rule_assigned_to_x_books' => 'Pielietots :count grāmatai|Pielietots :count grāmatām',\n    'sort_rule_create' => 'Izveidot kārtošanas nosacījumu',\n    'sort_rule_edit' => 'Rediģēt kārtošanas nosacījumu',\n    'sort_rule_delete' => 'Dzēst kārtošanas nosacījumu',\n    'sort_rule_delete_desc' => 'Izņemt šo kārtošanas nosacījumu no sistēmas. Grāmatām tiks atjaunota manuāla kārtošana.',\n    'sort_rule_delete_warn_books' => 'Šis kārtošanas nosacījums pašlaik tiek izmantots :count grāmatām. Vai tiešām vēlaties to dzēst?',\n    'sort_rule_delete_warn_default' => 'Šis kārtošanas nosacījums pašlaik ir norādīts kā noklusētais. Vai tiešām vēlaties to dzēst?',\n    'sort_rule_details' => 'Kārtošanas nosacījuma detaļas',\n    'sort_rule_details_desc' => 'Uzstādiet nosaukumu šim kārtošanas nosacījumam, kas parādīsies sarakstā, kad lietotājs izvēlēsies kārtošanas veidu.',\n    'sort_rule_operations' => 'Kārtošanas darbības',\n    'sort_rule_operations_desc' => 'Konfigurējiet kārtošanas darbības pārvelkot tās no pieejamo darb\\'biu saraksta. Lietošanas procesā darbības tiks piemērotas saraksta kārtībā no augšas uz apakšu. Jekbādas izmaiņas šeit tiks piemērotas grāmatās, kur šis piemērots, pie saglabāšanas.',\n    'sort_rule_available_operations' => 'Pieejamās darbības',\n    'sort_rule_available_operations_empty' => 'Nav atlikušas darbības',\n    'sort_rule_configured_operations' => 'Uzstādītās darbības',\n    'sort_rule_configured_operations_empty' => 'Ievelciet/pievienojiet darbības no \"Pieejamo darbību\" saraksta',\n    'sort_rule_op_asc' => '(Pieaug.)',\n    'sort_rule_op_desc' => '(Dilst.)',\n    'sort_rule_op_name' => 'Nosaukums - alfabētiski',\n    'sort_rule_op_name_numeric' => 'Nosaukums - numuriski',\n    'sort_rule_op_created_date' => 'Izveidošanas datums',\n    'sort_rule_op_updated_date' => 'Atjaunināšanas datums',\n    'sort_rule_op_chapters_first' => 'Nodaļas pirmās',\n    'sort_rule_op_chapters_last' => 'Nodaļas pēdējās',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Apkope',\n    'maint_image_cleanup' => 'Tīrīt neizmantotās bildes',\n    'maint_image_cleanup_desc' => 'Pārbauda lapu un lapu versiju saturu, lai noteiktu, kuri attēli pašlaik tiek izmantoti, un kuri nav nepieciešami. Pārliecinieties, ka ir veikta pilna datubāzes un attēlu rezerves kopija pirms šīs darbības.',\n    'maint_delete_images_only_in_revisions' => 'Dzēst arī attēlus, kas izmantoti tikai vecās lapu satura versijās',\n    'maint_image_cleanup_run' => 'Veikt tīrīšanu',\n    'maint_image_cleanup_warning' => ':count iespējami neizmantoti attēli atrasti. Vai tiešām vēlaties izdzēst šos attēlus?',\n    'maint_image_cleanup_success' => ':count iespējami neizmantoti attēli atrasti un izdzēsti!',\n    'maint_image_cleanup_nothing_found' => 'Nav atrasti neizmantoti attēli, nekas netika izdzēsts!',\n    'maint_send_test_email' => 'Nosūtīt testa epastu',\n    'maint_send_test_email_desc' => 'Nosūtīt testa epastu uz jūsu profilā norādīto epasta adresi.',\n    'maint_send_test_email_run' => 'Nosūtīt testa epastu',\n    'maint_send_test_email_success' => 'Epasts nosūtīts uz :address',\n    'maint_send_test_email_mail_subject' => 'Testa epasts',\n    'maint_send_test_email_mail_greeting' => 'Izskatās, ka epasta piegāde strādā!',\n    'maint_send_test_email_mail_text' => 'Apsveicam! Tā kā jūs saņēmāt šo epasta paziņojumu, jūsu epasta uzstādījumi šķiet pareizi.',\n    'maint_recycle_bin_desc' => 'Dzēstie plaukti, grāmatas, nodaļas un lapas ir pārceltas uz miskasti, lai tos varētu atjaunot vai izdzēst pilnībā. Vecākas vienības miskastē var tikt automātiski dzēstas pēc kāda laika atkarībā no sistēmas uzstādījumiem.',\n    'maint_recycle_bin_open' => 'Atvērt miskasti',\n    'maint_regen_references' => 'Atjaunot atsauces',\n    'maint_regen_references_desc' => 'Šī darbība no jauna izveidos atsauču indeksu datubāzē. Tas parasti notiek automātiski, taču šī darbība var palīdzēt, lai indeksētu vecāku saturu vai saturu, kas pievienots, izmantojot nestandarta metodes.',\n    'maint_regen_references_success' => 'Atsauču indekss ir izveidots!',\n    'maint_timeout_command_note' => 'Piezīme: Šī darbība var prasīt ilgāku laiku, kas var radīt pieprasījuma laika kļūmes (timeout) pie noteiktiem interneta vietnes uzstādījumiem. Alternatīva var būt veikt šo darbību, izmantojot termināla komandu.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Miskaste',\n    'recycle_bin_desc' => 'Te jūs varat atjaunot dzēstās vienības vai arī izdzēst tās no sistēmas pilnībā. Šis saraksts nav filtrēts atšķirībā no līdzīgiem darbību sarakstiem sistēmā, kur ir piemēroti piekļuves tiesību filtri.',\n    'recycle_bin_deleted_item' => 'Dzēsta vienība',\n    'recycle_bin_deleted_parent' => 'Augstāks līmenis',\n    'recycle_bin_deleted_by' => 'Izdzēsa',\n    'recycle_bin_deleted_at' => 'Dzēšanas laiks',\n    'recycle_bin_permanently_delete' => 'Neatgriezeniski izdzēst',\n    'recycle_bin_restore' => 'Atjaunot',\n    'recycle_bin_contents_empty' => 'Miskaste ir tukša',\n    'recycle_bin_empty' => 'Iztīrīt miskasti',\n    'recycle_bin_empty_confirm' => 'Šī darbība pilnībā dzēsīs visas vienības miskastē, ieskaitot saturu, kas ievietots katrā no šīm vienībām. Vai tiešām vēlaties dzēst visu miskastes saturu?',\n    'recycle_bin_destroy_confirm' => 'Šī darbība pilnībā no sistēmas izdzēsis šo vienību kopā ar tai pakārtotajiem elementiem, un jūs nevarēsiet šo saturu atjaunot. Vai tiešām vēlaties pilnībā izdzēst šo vienību?',\n    'recycle_bin_destroy_list' => 'Dzēšamās vienības',\n    'recycle_bin_restore_list' => 'Atjaunojamās vienības',\n    'recycle_bin_restore_confirm' => 'Šī darbība atjaunos dzēsto vienību, tai skaitā visus tai pakārtotos elementus, uz tās sākotnējo atrašanās vietu. Ja sākotnējā atrašanās vieta ir izdzēsta un atrodas miskastē, būs nepieciešams atjaunot arī to.',\n    'recycle_bin_restore_deleted_parent' => 'Šo elementu saturošā vienība arī ir dzēsta. Tas paliks dzēsts līdz šī saturošā vienība arī ir atjaunota.',\n    'recycle_bin_restore_parent' => 'Atjaunot augstāku līmeni',\n    'recycle_bin_destroy_notification' => 'Dzēstas kopā :count vienības no miskastes.',\n    'recycle_bin_restore_notification' => 'Atjaunotas kopā :count vienības no miskastes.',\n\n    // Audit Log\n    'audit' => 'Auditācijas pieraksti',\n    'audit_desc' => 'Šie auditācijas pieraksti attēlo sarakstu ar sistēmā reģistrētajām aktivitātēm. Šis saraksts nav filtrēts atšķirībā no līdzīgiem aktivitāšu sarakstiem sistēmā, kur ir piemēroti atļauto darbību filtri.',\n    'audit_event_filter' => 'Notikumu filtrs',\n    'audit_event_filter_no_filter' => 'Bez filtra',\n    'audit_deleted_item' => 'Dzēsta vienība',\n    'audit_deleted_item_name' => 'Vārds: :name',\n    'audit_table_user' => 'Lietotājs',\n    'audit_table_event' => 'Notikums',\n    'audit_table_related' => 'Saistīta vienība vai detaļa',\n    'audit_table_ip' => 'IP adrese',\n    'audit_table_date' => 'Notikuma datums',\n    'audit_date_from' => 'Datums no',\n    'audit_date_to' => 'Datums līdz',\n\n    // Role Settings\n    'roles' => 'Grupas',\n    'role_user_roles' => 'Lietotāju grupas',\n    'roles_index_desc' => 'Lomas tiek izmantotas, lai sagrupētu lietotājus un piešķirtu piekļuves tiesības to dalībniekiem. Kad lietotājs ir vairāku lomu dalībnieks, piešķirtās tiesības tiks summētas un lietotājam būs visas tiesības.',\n    'roles_x_users_assigned' => ':count lietotājam piešķirts|:count lietotājiem piešķirts',\n    'roles_x_permissions_provided' => ':count tiesības|:count tiesības',\n    'roles_assigned_users' => 'Pievienotie lietotāji',\n    'roles_permissions_provided' => 'Piešķirtās tiesības',\n    'role_create' => 'Izveidot jaunu grupu',\n    'role_delete' => 'Dzēst grupu',\n    'role_delete_confirm' => 'Loma \\':roleName\\' tiks dzēsta.',\n    'role_delete_users_assigned' => 'Šajā grupā ir pievienoti :userCount lietotāji. Ja vēlaties pārvietot lietotājus no šīs grupas, tad izvēlaties kādu no zemāk redzamajām grupām.',\n    'role_delete_no_migration' => \"Nepārvietot lietotājus\",\n    'role_delete_sure' => 'Vai tiešām vēlaties dzēst grupu?',\n    'role_edit' => 'Rediģēt grupu',\n    'role_details' => 'Informācija par grupu',\n    'role_name' => 'Grupas nosaukums',\n    'role_desc' => 'Īss grupas apaksts',\n    'role_mfa_enforced' => 'Nepieciešama vairākfaktoru autentifikācija',\n    'role_external_auth_id' => 'Ārējais autentifikācijas ID',\n    'role_system' => 'Sistēmas atļaujas',\n    'role_manage_users' => 'Pārvaldīt lietotājus',\n    'role_manage_roles' => 'Pārvaldīt grupas un grupu atļaujas',\n    'role_manage_entity_permissions' => 'Pārvaldīt visu grāmatu, nodaļu un lapu atļaujas',\n    'role_manage_own_entity_permissions' => 'Pārvaldīt atļaujas savām grāmatām, nodaļām un lapām',\n    'role_manage_page_templates' => 'Pārvaldīt lapas veidnes',\n    'role_access_api' => 'Piekļūt sistēmas API',\n    'role_manage_settings' => 'Pārvaldīt iestatījumus',\n    'role_export_content' => 'Eksportēt saturu',\n    'role_import_content' => 'Importēt saturu',\n    'role_editor_change' => 'Mainīt lapu redaktoru',\n    'role_notifications' => 'Saņemt un pārvaldīt paziņojumus',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Resursa piekļuves tiesības',\n    'roles_system_warning' => 'Jebkuras no trīs augstāk redzamajām atļaujām dod iespēju lietotājam mainīt savas un citu lietotāju sistēmas atļaujas. Pievieno šīs grupu atļaujas tikai tiem lietotājiem, kuriem uzticies.',\n    'role_asset_desc' => 'Šīs piekļuves tiesības kontrolē noklusēto piekļuvi sistēmas resursiem. Grāmatām, nodaļām un lapām norādītās tiesības būs pārākas par šīm.',\n    'role_asset_admins' => 'Administratoriem automātiski ir piekļuve visam saturam, bet šie uzstādījumi var noslēpt vai parādīt lietotāja saskarnes iespējas.',\n    'role_asset_image_view_note' => 'Šis ir saistīts ar redzamību attēlu pārvaldniekā. Faktiskā piekļuve augšupielādēto attēlu failiem būs atkarīga no sistēmas attēlu glabātuves uzstādījuma.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'Visi',\n    'role_own' => 'Savi',\n    'role_controlled_by_asset' => 'Kontrolē resurss, uz ko tie ir augšupielādēti',\n    'role_save' => 'Saglabāt grupu',\n    'role_users' => 'Lietotāji šajā grupā',\n    'role_users_none' => 'Pagaidām neviens lietotājs nav pievienots šai grupai',\n\n    // Users\n    'users' => 'Lietotāji',\n    'users_index_desc' => 'Izveidot un pārvaldīt atsevišķus lietotāju kontus sistēmā. Lietotāju konti tiek izmantoti piekļuvei un satura un aktivitāšu piesaistei. Piekļuves tiesības ir pamatā balstītas uz lomām, bet lietotāju veidotā satura piederība, cita starpā, arī var ietekmēt piekļuvi un tiesības.',\n    'user_profile' => 'Lietotāja profils',\n    'users_add_new' => 'Pievienot jaunu lietotāju',\n    'users_search' => 'Meklēt lietotājus',\n    'users_latest_activity' => 'Pēdējās aktivitātes',\n    'users_details' => 'Lietotāja informācija',\n    'users_details_desc' => 'Uzstādīt attēlojamo vārdu un epast adresi šim lietotājam. Epasta adresi varēs izmantot, lai piekļūtu aplikācijai.',\n    'users_details_desc_no_email' => 'Uzstādiet attēlojamu vārdu šim lietotājam, lai citi varētu viņu atpazīt.',\n    'users_role' => 'Lietotāju grupas',\n    'users_role_desc' => 'Izvēlēties kurām grupām pievienot lietotāju. Ja lietotājs ir pievienots vairākām grupām, tad lietotājam būs pieejamas visu grupu atļaujas.',\n    'users_password' => 'Lietotāja parole',\n    'users_password_desc' => 'Uzstādiet paroli, ar ko piekļūt aplikācijai. Tai jābūt vismaz 8 simbolus garai.',\n    'users_send_invite_text' => 'Jūs varat izvēlētes vai nosūtīt šim lietotājam uzaicinājuma epastu, kas ļauj tam uzstādīt savu paroli pašam, vai arī varat uzstādīt paroli tagad.',\n    'users_send_invite_option' => 'Nosūtīt lietotāja uzaicinājuma epastu',\n    'users_external_auth_id' => 'Ārējais autentifikācijas ID',\n    'users_external_auth_id_desc' => 'Kad tiek izmantota ārēja autentifikācijas sistēma (piemēram, SAML2, OIDC vai LDAP), šis ir identifikators (ID), kas sasaista šo BookStack lietotāja kontu ar autentifikācijas sistēmas kontu. Jūs varat ignorēt šo lauku, ja izmantojat noklusēto autentifikāciju ar epasta adresi.',\n    'users_password_warning' => 'Aizpildiet tikai tad, ja vēlaties mainīt paroli šim lietotājam.',\n    'users_system_public' => 'Šis lietotājs apzīmē visus viesus, kas apmeklēs jūsu lapu. To nevar izmantot lapas piekļuvei un tas tiek norādīts automātiski.',\n    'users_delete' => 'Dzēst lietotāju',\n    'users_delete_named' => 'Dzēst lietotāju :userName',\n    'users_delete_warning' => 'Šī darbība pilnībā izdzēsīs lietotāju \\':userName\\' no sistēmas.',\n    'users_delete_confirm' => 'Vai tiešām vēlaties dzēst šo lietotāju?',\n    'users_migrate_ownership' => 'Pārcelt īpašumtiesības',\n    'users_migrate_ownership_desc' => 'Izvēlieties lietotāju, ja vēlaties citam lietotājam pārcelt pašlaik šim lietotājam piederošās vienības.',\n    'users_none_selected' => 'Nav izvēlēts lietotājs',\n    'users_edit' => 'Rediģēt lietotāju',\n    'users_edit_profile' => 'Rediģēt profilu',\n    'users_avatar' => 'Lietotāja attēls',\n    'users_avatar_desc' => 'Izvēlieties attēlu šim lietotājam. Tam vajadzētu būt apmēram 256px kvadrātam.',\n    'users_preferred_language' => 'Vēlamā valoda',\n    'users_preferred_language_desc' => 'Šis uzstādījums nomainīs valodu, kas izmantota aplikācijas lietotāja saskarnē. Tas neietekmēs neko no lietotāju radītā satura.',\n    'users_social_accounts' => 'Sociālie konti',\n    'users_social_accounts_desc' => 'Skatīt piesaistīto sociālo kontu statusu šim lietotājam. Sociālos kontus var izmantot piekļuvei papildus primārajai autentifikācijas sistēmai.',\n    'users_social_accounts_info' => 'Te jūs varat pieslēgt citus kontus ātrākai un ērtākai piekļuvei. Konta atvienošana no šejienes neatceļ šai aplikācijai dotās tiesības šī konta piekļuvei. Atvienojtiet piekļuvi arī no jūsu profila uzstādījumiem pievienotajā sociālajā kontā.',\n    'users_social_connect' => 'Pievienot kontu',\n    'users_social_disconnect' => 'Atvienot kontu',\n    'users_social_status_connected' => 'Savienots',\n    'users_social_status_disconnected' => 'Atvienots',\n    'users_social_connected' => ':socialAccount konts veiksmīgi pieslēgts jūsu profilam.',\n    'users_social_disconnected' => ':socialAccount konts veiksmīgi atslēgts no jūsu profila.',\n    'users_api_tokens' => 'API žetoni',\n    'users_api_tokens_desc' => 'Izveidot un pārvaldīt piekļuves žetonus, lai autentificētos ar BookStack REST API. API tiesības ir tās pašas, kas lietotāja kontam, kam pieder šis žetons.',\n    'users_api_tokens_none' => 'Šim lietotājam nav izveidotu API žetonu',\n    'users_api_tokens_create' => 'Izveidot žetonu',\n    'users_api_tokens_expires' => 'Derīguma termiņš',\n    'users_api_tokens_docs' => 'API dokumentācija',\n    'users_mfa' => 'Vairākfaktoru autentifikācija',\n    'users_mfa_desc' => 'Iestati vairākfaktoru autentifikāciju kā papildus drošības līmeni tavam lietotāja kontam.',\n    'users_mfa_x_methods' => ':count metode iestatīta|:count metodes iestatītas',\n    'users_mfa_configure' => 'Iestatīt metodes',\n\n    // API Tokens\n    'user_api_token_create' => 'Izveidot API žetonu',\n    'user_api_token_name' => 'Vārds',\n    'user_api_token_name_desc' => 'Uzstādiet nolasāmu nosaukumu savam žetonam, lai nākotnē atgadinātu par tā pielietojumu.',\n    'user_api_token_expiry' => 'Derīgs līdz',\n    'user_api_token_expiry_desc' => 'Uzstādiet datumu, kad beidzas žetona derīguma termiņš. Pieprasījumi, kas veikti pēc šī datuma ar šo žetonu vairs nedarbosies. Atstājot lauku tukšu, tiks uzstādīts derīguma termiņš 100 gadu nākotnē.',\n    'user_api_token_create_secret_message' => 'Uzreiz pēc žetona izveidošanas tiks parādīts žetona ID un žetona noslēpums. Šis noslēpums tiks attēlots tikai vienreiz, tāpēc pārliecinieties, ka tā vērtība ir nokopēta uz kādu citu drošu vietu pirms turpināšanas.',\n    'user_api_token' => 'API žetons',\n    'user_api_token_id' => 'Žetona ID',\n    'user_api_token_id_desc' => 'Šis ir neizmaināms sistēmas ģenerēts identifikators šim žetonam, kas būs jānorāda API pieprasījumos.',\n    'user_api_token_secret' => 'Žetona noslēpums',\n    'user_api_token_secret_desc' => 'Šis ir sistēmas ģenerēts noslēpums šim žetonam, ko būs nepieciešams norādīt API pieprasījumos. Tas tiks attēlots tikai vienu reizi, tāpēc nokopējiet to uz kādu citu drošu vietu.',\n    'user_api_token_created' => 'Žetons izveidots :timeAgo',\n    'user_api_token_updated' => 'Žetons atjaunināts :timeAgo',\n    'user_api_token_delete' => 'Dzēst žetonu',\n    'user_api_token_delete_warning' => 'Šī darbība pilnībā izdzēsīs API žetonu \\':tokenName\\' no sistēmas.',\n    'user_api_token_delete_confirm' => 'Vai tiešām vēlaties dzēst šo API žetonu?',\n\n    // Webhooks\n    'webhooks' => 'Webhook',\n    'webhooks_index_desc' => 'Webhook ir veids, kā nosūtīt datus ārējām adresēm (URL) pie noteiktām darbībām vai notikumiem sistēmā. Tas ļauj īstenot uz notikumiem balstītu integrāciju ar ārējām platformām, kā piemēram, apziņošanas sistēmām.',\n    'webhooks_x_trigger_events' => ':count notikums|:count notikumi',\n    'webhooks_create' => 'Izveidot jaunu webhook',\n    'webhooks_none_created' => 'Nav izveidots neviens webhook.',\n    'webhooks_edit' => 'Labot webhook',\n    'webhooks_save' => 'Saglabāt webhook',\n    'webhooks_details' => 'Webhook detaļas',\n    'webhooks_details_desc' => 'Norādiet lietotājiem draudzīgu nosaukumu un POST adresi (endpoint), uz ko nosūtīt webhook datus.',\n    'webhooks_events' => 'Webhook notikumi',\n    'webhooks_events_desc' => 'Izvēlieties visus notikumus, kas izsauks šo webhook.',\n    'webhooks_events_warning' => 'Ņemiet vērā, ka šie notikumi tiks palaisti visiem izvēlētajiem notikumiem, pat ja norādītas pielāgotas piekļuves tiesības. Pārliecineities, ka webhook lietošana neatklās ierobežotas pieejamības saturu.',\n    'webhooks_events_all' => 'Visi sistēmas notikumi',\n    'webhooks_name' => 'Webhook nosaukums',\n    'webhooks_timeout' => 'Webhook pieprasījuma laika ierobežojums (sekundēs)',\n    'webhooks_endpoint' => 'Webhook adrese (endpoint)',\n    'webhooks_active' => 'Webhook aktīvs',\n    'webhook_events_table_header' => 'Notikumi',\n    'webhooks_delete' => 'Dzēst webhook',\n    'webhooks_delete_warning' => 'Webhook ar nosaukumu \\':webhookName\\' tiks pilnībā dzēsts no sistēmas.',\n    'webhooks_delete_confirm' => 'Vai tiešām vēlaties dzēst šo webhook?',\n    'webhooks_format_example' => 'Webhook formāta piemērs',\n    'webhooks_format_example_desc' => 'Webhook dati tiek nosūtīti kā POST pieprasījums norādītajai endpoint adresei kā JSON tālāk norādītajā formātā. \"related_item\" un \"url\" īpašības nav obligātas un ir atkarīgas no palaistā notikuma veida.',\n    'webhooks_status' => 'Webhook statuss',\n    'webhooks_last_called' => 'Pēdejoreiz izsaukts:',\n    'webhooks_last_errored' => 'Pedējoreiz kļūda:',\n    'webhooks_last_error_message' => 'Pēdējais kļūdas paziņojums:',\n\n    // Licensing\n    'licenses' => 'Licences',\n    'licenses_desc' => 'Šī lapa attēlo BookStack licences informāciju, kā arī licences citiem projektiem un bibliotēkām, kas tiek izmantoti BookStack. Daļa no minētajiem projektiem var būt izmantoti tikai izstrādē.',\n    'licenses_bookstack' => 'BookStack licence',\n    'licenses_php' => 'PHP bibliotēku licences',\n    'licenses_js' => 'JavaScript bibliotēku licences',\n    'licenses_other' => 'Citas licences',\n    'license_details' => 'Licences informācija',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Katalāņu',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Igauņu',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/lv/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute ir jāapstiprina.',\n    'active_url'           => ':attribute nav derīgs URL.',\n    'after'                => ':attribute ir jābūt datumam pēc :date.',\n    'alpha'                => ':attribute var saturēt tikai burtus.',\n    'alpha_dash'           => ':attribute var saturēt tikai burtus, ciparus, domuzīmes un apakš svītras.',\n    'alpha_num'            => ':attribute var saturēt tikai burtus un ciparus.',\n    'array'                => ':attribute ir jābūt masīvam.',\n    'backup_codes'         => 'Ievadītais kods nav derīgs vai arī jau ir izmantots.',\n    'before'               => ':attribute jābūt datumam pirms :date.',\n    'between'              => [\n        'numeric' => ':attribute jābūt starp :min un :max.',\n        'file'    => ':attribute jābūt starp :min un :max kilobaitiem.',\n        'string'  => ':attribute jābūt starp :min un :max rakstzīmēm.',\n        'array'   => 'Atribūtam jābūt starp: min un: max vienumiem.',\n    ],\n    'boolean'              => ':attribute jābūt True vai False.',\n    'confirmed'            => ':attribute apstiprinājums nesakrīt.',\n    'date'                 => ':attribute nav derīgs datums.',\n    'date_format'          => ':attribute neatbilst formātam :format.',\n    'different'            => ':attribute un :other jābūt atšķirīgiem.',\n    'digits'               => ':attribute jābūt :digits cipariem.',\n    'digits_between'       => ':attribute jābūt starp :min un :max cipariem.',\n    'email'                => ':attribute jābūt derīgai e-pasta adresei.',\n    'ends_with' => ':attribute jābeidzas ar vienu no :values',\n    'file'                 => ':attribute jābūt derīgam failam.',\n    'filled'               => ':attribute lauks ir obligāts.',\n    'gt'                   => [\n        'numeric' => ':attribute jābūt lielākam kā :value.',\n        'file'    => ':attribute jābūt lielākam kā :value kilobaitiem.',\n        'string'  => ':attribute jābūt lielākam kā :value rakstzīmēm.',\n        'array'   => ':attribute jāsatur vairāk kā :value vienības.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute jābūt lielākam vai vienādam ar :value.',\n        'file'    => ':attribute jābūt lielākam vai vienādam ar :value kilobaitiem.',\n        'string'  => ':attribute jābūt lielākam vai vienādam ar :value rakstzīmēm.',\n        'array'   => ':attribute jāsatur :value vai vairāk vienumus.',\n    ],\n    'exists'               => 'Izvēlētais :attribute ir nederīgs.',\n    'image'                => ':attribute jābūt attēlam.',\n    'image_extension'      => ':attribute jābūt derīgam un atbalstītam bildes paplašinājumam.',\n    'in'                   => 'Iezīmētais :attribute ir nederīgs.',\n    'integer'              => ':attribute ir jābūt veselam skaitlim.',\n    'ip'                   => ':attribute jābūt derīgai IP adresei.',\n    'ipv4'                 => ':attribute jābūt derīgai IPv4 adresei.',\n    'ipv6'                 => ':attribute jābūt derīgai IPv6 adresei.',\n    'json'                 => ':attribute jābūt derīgai JSON virknei.',\n    'lt'                   => [\n        'numeric' => ':attribute jābūt mazākam par :value.',\n        'file'    => ':attribute jābūt mazāk kā :value kilobaitiem.',\n        'string'  => ':attribute jābūt mazāk kā :value rakstzīmēm.',\n        'array'   => ':attribute jāsatur mazāk kā :value vienības.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute jābūt mazākam vai vienādam ar :value.',\n        'file'    => ':attribute jābūt mazākam vai vienādam ar :value kilobaitiem.',\n        'string'  => ':attribute jābūt mazākam vai vienādam ar :value rakstzīmēm.',\n        'array'   => ':attribute nedrīkst pārsniegt :value vienības.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute nevar būt lielāks kā :max.',\n        'file'    => ':attribute nedrīkst būt lielāks kā :max kilobaiti.',\n        'string'  => ':attribute nedrīkst būt lielāks kā :max rakstzīmēm.',\n        'array'   => ':attribute nedrīkst būt lielāks kā :max vienumi.',\n    ],\n    'mimes'                => ':attribute jābūt faila tipam: :values.',\n    'min'                  => [\n        'numeric' => ':attribute ir jābūt vismaz :min.',\n        'file'    => ':attribute jābūt vismaz :min kilobaitiem.',\n        'string'  => ':attribute ir jābūt vismaz :min rakstzīmēm.',\n        'array'   => ':attribute ir jābūt vismaz :min vienībām.',\n    ],\n    'not_in'               => 'Izvēlētais: atribūts ir nederīgs.',\n    'not_regex'            => ':attribute formāts nav derīgs.',\n    'numeric'              => ':attribute ir jābūt skaitlim.',\n    'regex'                => ':attribute formāts nav derīgs.',\n    'required'             => ':attribute lauks ir obligāts.',\n    'required_if'          => ':attribute lauks ir nepieciešams, kad :other ir :value.',\n    'required_with'        => ':attribute lauks ir obligāts, ja ir :values.',\n    'required_with_all'    => ':attribute lauks ir obligāts, ja ir :values.',\n    'required_without'     => ':attribute lauks ir obligāts, ja nav :values.',\n    'required_without_all' => ':attribute lauks ir obligāts, ja nav neviena no :values.',\n    'same'                 => ':attribute un :other jāsakrīt.',\n    'safe_url'             => 'Norādītā saite var būt nedroša.',\n    'size'                 => [\n        'numeric' => ':attribute ir jābūt :size.',\n        'file'    => ':attribute jābūt :size kilobaiti.',\n        'string'  => ':attribute jābūt :size rakstzīmēm.',\n        'array'   => ':attribute jāsatur :size vienības.',\n    ],\n    'string'               => ':attribute jābūt teksta virknei.',\n    'timezone'             => ':attribute jābūt derīgai zonai.',\n    'totp'                 => 'Ievadītais kods nav derīgs.',\n    'unique'               => ':attribute jau ir aizņemts.',\n    'url'                  => ':attribute formāts nav derīgs.',\n    'uploaded'             => 'Fails netika ielādēts. Serveris nevar pieņemt šāda izmēra failus.',\n\n    'zip_file' => ':attribute ir jāatsaucas uz failu ZIP arhīvā.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => ':attribute ir jāatsaucas uz failu ar tipu :validTypes, bet atrasts :foundType.',\n    'zip_model_expected' => 'Sagaidīts datu objekts, bet atrasts \":type\".',\n    'zip_unique' => ':attribute jābūt unikālam šim objekta tipam ZIP arhīvā.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Nepieciešams paroles apstiprinājums',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/nb/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'opprettet side',\n    'page_create_notification'    => 'Siden ble opprettet',\n    'page_update'                 => 'oppdaterte side',\n    'page_update_notification'    => 'Siden ble oppdatert',\n    'page_delete'                 => 'slettet side',\n    'page_delete_notification'    => 'Siden ble slettet',\n    'page_restore'                => 'gjenopprettet side',\n    'page_restore_notification'   => 'Siden ble gjenopprettet',\n    'page_move'                   => 'flyttet side',\n    'page_move_notification'      => 'Siden ble flyttet',\n\n    // Chapters\n    'chapter_create'              => 'opprettet kapittel',\n    'chapter_create_notification' => 'Kapittelet ble opprettet',\n    'chapter_update'              => 'oppdaterte kapittel',\n    'chapter_update_notification' => 'Kapittelet ble oppdatert',\n    'chapter_delete'              => 'slettet kapittel',\n    'chapter_delete_notification' => 'Kapittelet ble slettet',\n    'chapter_move'                => 'flyttet kapittel\n    ',\n    'chapter_move_notification' => 'Kapitelet ble flyttet',\n\n    // Books\n    'book_create'                 => 'opprettet bok',\n    'book_create_notification'    => 'Boken ble opprettet',\n    'book_create_from_chapter'              => 'konverterte kapittelet til bok',\n    'book_create_from_chapter_notification' => 'Kapittelet ble konvertert til en bok',\n    'book_update'                 => 'oppdaterte bok',\n    'book_update_notification'    => 'Boken ble oppdatert',\n    'book_delete'                 => 'slettet bok',\n    'book_delete_notification'    => 'Boken ble slettet',\n    'book_sort'                   => 'sorterte bok',\n    'book_sort_notification'      => 'Boken ble omsortert',\n\n    // Bookshelves\n    'bookshelf_create'            => 'opprettet hylle',\n    'bookshelf_create_notification'    => 'Hylllen ble opprettet',\n    'bookshelf_create_from_book'    => 'endret fra bok til hylle',\n    'bookshelf_create_from_book_notification'    => 'Boken ble konvertert til en bokhylle',\n    'bookshelf_update'                 => 'oppdatert hylle',\n    'bookshelf_update_notification'    => 'Hyllen ble oppdatert',\n    'bookshelf_delete'                 => 'slettet hylle',\n    'bookshelf_delete_notification'    => 'Hyllen ble slettet',\n\n    // Revisions\n    'revision_restore' => 'gjenopprettet revisjon',\n    'revision_delete' => 'slettet revisjon',\n    'revision_delete_notification' => 'Revisjon slettet',\n\n    // Favourites\n    'favourite_add_notification' => '«:name» ble lagt til i dine favoritter',\n    'favourite_remove_notification' => '«:name» ble fjernet fra dine favoritter',\n\n    // Watching\n    'watch_update_level_notification' => 'Overvåkingsinnstillingene ble oppdatert',\n\n    // Auth\n    'auth_login' => 'logget inn',\n    'auth_register' => 'registrert som ny bruker',\n    'auth_password_reset_request' => 'etterspurt tilbakestilling av passord',\n    'auth_password_reset_update' => 'tilbakestill bruker passord',\n    'mfa_setup_method' => 'konfigurert MFA-metode',\n    'mfa_setup_method_notification' => 'Flerfaktor-metoden ble konfigurert',\n    'mfa_remove_method' => 'fjernet MFA-metode',\n    'mfa_remove_method_notification' => 'Flerfaktor-metoden ble fjernet',\n\n    // Settings\n    'settings_update' => 'oppdaterte innstillinger',\n    'settings_update_notification' => 'Innstillingene er oppdatert',\n    'maintenance_action_run' => 'kjørte vedlikeholdshandling',\n\n    // Webhooks\n    'webhook_create' => 'opprettet webhook',\n    'webhook_create_notification' => 'Webhook ble opprettet',\n    'webhook_update' => 'oppdatert webhook',\n    'webhook_update_notification' => 'Webhook ble oppdatert',\n    'webhook_delete' => 'slettet webhook',\n    'webhook_delete_notification' => 'Webhook ble slettet',\n\n    // Imports\n    'import_create' => 'import opprettet',\n    'import_create_notification' => 'Importen ble opplastet',\n    'import_run' => 'oppdatert import',\n    'import_run_notification' => 'Innhold importert',\n    'import_delete' => 'import slettet',\n    'import_delete_notification' => 'Importering ble slettet',\n\n    // Users\n    'user_create' => 'opprettet bruker',\n    'user_create_notification' => 'Bruker ble opprettet',\n    'user_update' => 'oppdatert bruker',\n    'user_update_notification' => 'Brukeren ble oppdatert',\n    'user_delete' => 'slettet bruker',\n    'user_delete_notification' => 'Brukeren ble fjernet',\n\n    // API Tokens\n    'api_token_create' => 'opprettet API-nøkkel',\n    'api_token_create_notification' => 'API-token er opprettet',\n    'api_token_update' => 'oppdaterte API-nøkkel',\n    'api_token_update_notification' => 'API-token oppdatert',\n    'api_token_delete' => 'slettet API-nøkkel',\n    'api_token_delete_notification' => 'API-token ble slettet',\n\n    // Roles\n    'role_create' => 'opprettet rolle',\n    'role_create_notification' => 'Rollen ble opprettet',\n    'role_update' => 'oppdatert rolle',\n    'role_update_notification' => 'Rollen ble oppdatert',\n    'role_delete' => 'slettet rolle',\n    'role_delete_notification' => 'Rollen ble fjernet',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'tømt resirkulering bin',\n    'recycle_bin_restore' => 'gjenopprettet fra papirkurven',\n    'recycle_bin_destroy' => 'fjernet fra papirkurven',\n\n    // Comments\n    'commented_on'                => 'kommenterte på',\n    'comment_create'              => 'lagt til kommentar',\n    'comment_update'              => 'oppdatert kommentar',\n    'comment_delete'              => 'slettet kommentar',\n\n    // Sort Rules\n    'sort_rule_create' => 'opprettet sorteringsregel',\n    'sort_rule_create_notification' => 'Sorteringsregel opprettet med suksess',\n    'sort_rule_update' => 'oppdatert sorteringsregel',\n    'sort_rule_update_notification' => 'Sorteringsregel oppdatert med suksess',\n    'sort_rule_delete' => 'slettet sorteringsregel',\n    'sort_rule_delete_notification' => 'Sorteringsregel slettet med suksess',\n\n    // Other\n    'permissions_update'          => 'oppdaterte tilganger',\n];\n"
  },
  {
    "path": "lang/nb/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Disse detaljene samsvarer ikke med det vi har på bok.',\n    'throttle' => 'For mange forsøk, prøv igjen om :seconds sekunder.',\n\n    // Login & Register\n    'sign_up' => 'Registrer deg',\n    'log_in' => 'Logg inn',\n    'log_in_with' => 'Logg inn med :socialDriver',\n    'sign_up_with' => 'Registrer med :socialDriver',\n    'logout' => 'Logg ut',\n\n    'name' => 'Navn',\n    'username' => 'Brukernavn',\n    'email' => 'E-post',\n    'password' => 'Passord',\n    'password_confirm' => 'Bekreft passord',\n    'password_hint' => 'Må være minst 8 tegn',\n    'forgot_password' => 'Glemt passord?',\n    'remember_me' => 'Husk meg',\n    'ldap_email_hint' => 'Oppgi en e-post for denne kontoen.',\n    'create_account' => 'Opprett konto',\n    'already_have_account' => 'Har du allerede en konto?',\n    'dont_have_account' => 'Mangler du en konto?',\n    'social_login' => 'Sosiale kontoer',\n    'social_registration' => 'Registrer via sosiale kontoer',\n    'social_registration_text' => 'Bruk en annen tjeneste for å registrere deg.',\n\n    'register_thanks' => 'Takk for at du registrerte deg!',\n    'register_confirm' => 'Sjekk e-posten din for informasjon som gir deg tilgang til :appName.',\n    'registrations_disabled' => 'Registrering er deaktivert.',\n    'registration_email_domain_invalid' => 'Du kan ikke bruke det domenet for å registrere en konto.',\n    'register_success' => 'Takk for registreringen! Du kan nå logge inn på tjenesten.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Forsøker innlogging',\n    'auto_init_starting_desc' => 'Vi kontakter autentiseringssystemet ditt for å påbegynne innloggingsprosessen. Dersom det ikke er noe fremdrift i løpet av fem sekunder kan du trykke på lenken under.',\n    'auto_init_start_link' => 'Fortsett med autentisering',\n\n    // Password Reset\n    'reset_password' => 'Nullstille passord',\n    'reset_password_send_instructions' => 'Oppgi e-posten som er koblet til kontoen din, så sender vi en epost hvor du kan nullstille passordet.',\n    'reset_password_send_button' => 'Send nullstillingslenke',\n    'reset_password_sent' => 'En nullstillingslenke ble sendt til :email om den eksisterer i systemet.',\n    'reset_password_success' => 'Passordet ble nullstilt.',\n    'email_reset_subject' => 'Nullstill ditt :appName passord',\n    'email_reset_text' => 'Du mottar denne eposten fordi det er blitt bedt om en nullstilling av passord på denne kontoen.',\n    'email_reset_not_requested' => 'Om det ikke var deg, så trenger du ikke foreta deg noe.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Bekreft epost-adressen for :appName',\n    'email_confirm_greeting' => 'Takk for at du registrerte deg for :appName!',\n    'email_confirm_text' => 'Bekreft e-posten din ved å trykke på knappen nedenfor:',\n    'email_confirm_action' => 'Bekreft e-post',\n    'email_confirm_send_error' => 'Bekreftelse er krevd av systemet, men systemet kan ikke sende disse. Kontakt admin for å løse problemet.',\n    'email_confirm_success' => 'Epost-adressen din er verifisert! Du kan nå logge inn ved å bruke denne ved innlogging.',\n    'email_confirm_resent' => 'Bekreftelsespost ble sendt, sjekk innboksen din.',\n    'email_confirm_thanks' => 'Takk for verifiseringen!',\n    'email_confirm_thanks_desc' => 'Vent et øyeblikk mens verifiseringen blir utført. Om du ikke blir videresendt i løpet av tre sekunder kan du trykke «Fortsett» nedenfor.',\n\n    'email_not_confirmed' => 'E-posten er ikke bekreftet.',\n    'email_not_confirmed_text' => 'Epost-adressen er ennå ikke bekreftet.',\n    'email_not_confirmed_click_link' => 'Trykk på lenken i e-posten du fikk vedrørende din registrering.',\n    'email_not_confirmed_resend' => 'Om du ikke finner den i innboksen eller søppelboksen, kan du få tilsendt ny ved å trykke på knappen under.',\n    'email_not_confirmed_resend_button' => 'Send bekreftelsespost på nytt',\n\n    // User Invite\n    'user_invite_email_subject' => 'Du har blitt invitert til :appName!',\n    'user_invite_email_greeting' => 'En konto har blitt opprettet for deg på :appName.',\n    'user_invite_email_text' => 'Trykk på knappen under for å opprette et sikkert passord:',\n    'user_invite_email_action' => 'Angi passord',\n    'user_invite_page_welcome' => 'Velkommen til :appName!',\n    'user_invite_page_text' => 'For å fullføre prosessen må du oppgi et passord som sikrer din konto på :appName for fremtidige besøk.',\n    'user_invite_page_confirm_button' => 'Bekreft passord',\n    'user_invite_success_login' => 'Passordet ble satt, du skal nå kunne logge inn med ditt nye passord for å få tilgang til :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Konfigurer flerfaktor-autentisering',\n    'mfa_setup_desc' => 'Konfigurer flerfaktor-autentisering som et ekstra lag med sikkerhet for brukerkontoen din.',\n    'mfa_setup_configured' => 'Allerede konfigurert',\n    'mfa_setup_reconfigure' => 'Omkonfigurer',\n    'mfa_setup_remove_confirmation' => 'Er du sikker på at du vil deaktivere denne flerfaktor-autentiseringsmetoden?',\n    'mfa_setup_action' => 'Konfigurasjon',\n    'mfa_backup_codes_usage_limit_warning' => 'Du har mindre enn 5 sikkerhetskoder igjen; vennligst generer og lagre ett nytt sett før du går tom for koder, for å unngå å bli låst ute av kontoen din.',\n    'mfa_option_totp_title' => 'Mobilapplikasjon',\n    'mfa_option_totp_desc' => 'For å bruke flerfaktorautentisering trenger du en mobilapplikasjon som støtter TOTP-teknologien, slik som Google Authenticator, Authy eller Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Sikkerhetskoder',\n    'mfa_option_backup_codes_desc' => 'Genererer et sett med engangskoder som du kan bruke ved innlogging for å bekrefte identiteten din. Sørg for å lagre disse på et sikkert sted.',\n    'mfa_gen_confirm_and_enable' => 'Bekreft og aktiver',\n    'mfa_gen_backup_codes_title' => 'Konfigurasjon av sikkerhetskoder',\n    'mfa_gen_backup_codes_desc' => 'Lagre nedeforstående liste med koder på et trygt sted. Når du skal ha tilgang til systemet kan du bruke en av disse som en faktor under innlogging.',\n    'mfa_gen_backup_codes_download' => 'Last ned koder',\n    'mfa_gen_backup_codes_usage_warning' => 'Hver kode kan kun brukes en gang',\n    'mfa_gen_totp_title' => 'Oppsett for mobilapplikasjon',\n    'mfa_gen_totp_desc' => 'For å bruke flerfaktorautentisering trenger du en mobilapplikasjon som støtter TOTP-teknologien, slik som Google Authenticator, Authy eller Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Scan QR-koden nedenfor med valgt TOTP-applikasjon for å starte.',\n    'mfa_gen_totp_verify_setup' => 'Bekreft oppsett',\n    'mfa_gen_totp_verify_setup_desc' => 'Bekreft at oppsettet fungerer ved å skrive inn koden fra TOTP-applikasjonen i boksen nedenfor:',\n    'mfa_gen_totp_provide_code_here' => 'Skriv inn den genererte koden her',\n    'mfa_verify_access' => 'Bekreft tilgang',\n    'mfa_verify_access_desc' => 'Brukerkontoen din krever at du bekrefter din identitet med en ekstra autentiseringsfaktor før du får tilgang. Bekreft identiteten med en av dine konfigurerte metoder for å fortsette.',\n    'mfa_verify_no_methods' => 'Ingen metoder er konfigurert',\n    'mfa_verify_no_methods_desc' => 'Ingen flerfaktorautentiseringsmetoder er satt opp for din konto. Du må sette opp minst en metode for å få tilgang.',\n    'mfa_verify_use_totp' => 'Bekreft med mobilapplikasjon',\n    'mfa_verify_use_backup_codes' => 'Bekreft med sikkerhetskode',\n    'mfa_verify_backup_code' => 'Sikkerhetskode',\n    'mfa_verify_backup_code_desc' => 'Skriv inn en av dine ubrukte sikkerhetskoder under:',\n    'mfa_verify_backup_code_enter_here' => 'Skriv inn sikkerhetskode her',\n    'mfa_verify_totp_desc' => 'Skriv inn koden, generert ved hjelp av mobilapplikasjonen, nedenfor:',\n    'mfa_setup_login_notification' => 'Flerfaktorautentisering er konfigurert, vennligst logg inn på nytt med denne metoden.',\n];\n"
  },
  {
    "path": "lang/nb/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Avbryt',\n    'close' => 'Lukk',\n    'confirm' => 'Bekreft',\n    'back' => 'Tilbake',\n    'save' => 'Lagre',\n    'continue' => 'Fortsett',\n    'select' => 'Velg',\n    'toggle_all' => 'Bytt alle',\n    'more' => 'Mer',\n\n    // Form Labels\n    'name' => 'Navn',\n    'description' => 'Beskrivelse',\n    'role' => 'Rolle',\n    'cover_image' => 'Forside',\n    'cover_image_description' => 'Dette bildet bør være omtrent 440x250px selv om det vil bli fleksibelt skalert og beskjært slik at brukergrensesnittet passer til forskjellige scenarier ved behov. Dette betyr at de faktiske dimensjonene for visning varierer.',\n\n    // Actions\n    'actions' => 'Handlinger',\n    'view' => 'Vis',\n    'view_all' => 'Vis alle',\n    'new' => 'Ny',\n    'create' => 'Opprett',\n    'update' => 'Oppdater',\n    'edit' => 'Rediger',\n    'archive' => 'Arkiver',\n    'unarchive' => 'Av-arkiver',\n    'sort' => 'Sortér',\n    'move' => 'Flytt',\n    'copy' => 'Kopier',\n    'reply' => 'Svar',\n    'delete' => 'Slett',\n    'delete_confirm' => 'Bekreft sletting',\n    'search' => 'Søk',\n    'search_clear' => 'Nullstill søk',\n    'reset' => 'Nullstill',\n    'remove' => 'Fjern',\n    'add' => 'Legg til',\n    'configure' => 'Konfigurer',\n    'manage' => 'Administrer',\n    'fullscreen' => 'Fullskjerm',\n    'favourite' => 'Favoriser',\n    'unfavourite' => 'Avfavoriser',\n    'next' => 'Neste',\n    'previous' => 'Forrige',\n    'filter_active' => 'Aktivt filter:',\n    'filter_clear' => 'Tøm filter',\n    'download' => 'Last ned',\n    'open_in_tab' => 'Åpne i fane',\n    'open' => 'Åpne',\n\n    // Sort Options\n    'sort_options' => 'Sorteringsalternativer',\n    'sort_direction_toggle' => 'Sorteringsretning',\n    'sort_ascending' => 'Stigende sortering',\n    'sort_descending' => 'Synkende sortering',\n    'sort_name' => 'Navn',\n    'sort_default' => 'Standard',\n    'sort_created_at' => 'Dato opprettet',\n    'sort_updated_at' => 'Dato oppdatert',\n\n    // Misc\n    'deleted_user' => 'Slett bruker',\n    'no_activity' => 'Ingen aktivitet å vise',\n    'no_items' => 'Ingen ting å vise',\n    'back_to_top' => 'Hopp til toppen',\n    'skip_to_main_content' => 'Gå til hovedinnhold',\n    'toggle_details' => 'Vis/skjul detaljer',\n    'toggle_thumbnails' => 'Vis/skjul miniatyrbilder',\n    'details' => 'Detaljer',\n    'grid_view' => 'Rutenettvisning',\n    'list_view' => 'Listevisning',\n    'default' => 'Standard',\n    'breadcrumb' => 'Brødsmuler',\n    'status' => 'Status',\n    'status_active' => 'Aktiv',\n    'status_inactive' => 'Inaktiv',\n    'never' => 'Aldri',\n    'none' => 'Ingen',\n\n    // Header\n    'homepage' => 'Hjemmeside',\n    'header_menu_expand' => 'Utvid toppmeny',\n    'profile_menu' => 'Profilmeny',\n    'view_profile' => 'Vis profil',\n    'edit_profile' => 'Endre Profile',\n    'dark_mode' => 'Kveldsmodus',\n    'light_mode' => 'Dagmodus',\n    'global_search' => 'Globalt søk',\n\n    // Layout tabs\n    'tab_info' => 'Informasjon',\n    'tab_info_label' => 'Fane: Vis tilleggsinfo',\n    'tab_content' => 'Innhold',\n    'tab_content_label' => 'Fane: Vis hovedinnhold',\n\n    // Email Content\n    'email_action_help' => 'Om du har problemer med å trykke på «:actionText»-knappen, bruk nettadressen under for å gå direkte dit:',\n    'email_rights' => 'Kopibeskyttet',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Personvernregler',\n    'terms_of_service' => 'Bruksvilkår',\n\n    // OpenSearch\n    'opensearch_description' => 'Søk :appName',\n];\n"
  },
  {
    "path": "lang/nb/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Velg bilde',\n    'image_list' => 'Bilde liste',\n    'image_details' => 'Bildedetaljer',\n    'image_upload' => 'Last opp bilde',\n    'image_intro' => 'Her kan du velge og behandle bilder som tidligere har blitt lastet opp til systemet.',\n    'image_intro_upload' => 'Last opp et nytt bilde ved å dra et bilde i dette vinduet, eller ved å bruke knappen \"Last opp bilde\" ovenfor.',\n    'image_all' => 'Alle',\n    'image_all_title' => 'Vis alle bilder',\n    'image_book_title' => 'Vis bilder som er lastet opp i denne boken',\n    'image_page_title' => 'Vis bilder lastet opp til denne siden',\n    'image_search_hint' => 'Søk på bilder etter navn',\n    'image_uploaded' => 'Opplastet :uploadedDate',\n    'image_uploaded_by' => 'Lastet opp av :userName',\n    'image_uploaded_to' => 'Lastet opp til :pageLink',\n    'image_updated' => 'Oppdatert :updateDate',\n    'image_load_more' => 'Last inn flere',\n    'image_image_name' => 'Bildenavn',\n    'image_delete_used' => 'Dette bildet er brukt på sidene nedenfor.',\n    'image_delete_confirm_text' => 'Vil du slette dette bildet?',\n    'image_select_image' => 'Velg bilde',\n    'image_dropzone' => 'Dra og slipp eller trykk her for å laste opp bilder',\n    'image_dropzone_drop' => 'Slipp bilder her for å laste opp',\n    'images_deleted' => 'Bilder slettet',\n    'image_preview' => 'Hurtigvisning av bilder',\n    'image_upload_success' => 'Bilde ble lastet opp',\n    'image_update_success' => 'Bildedetaljer ble oppdatert',\n    'image_delete_success' => 'Bilde ble slettet',\n    'image_replace' => 'Erstatt bilde',\n    'image_replace_success' => 'Bildefil ble oppdatert',\n    'image_rebuild_thumbs' => 'Regenerer størrelsesvarianter',\n    'image_rebuild_thumbs_success' => 'Variasjoner i bildestørrelse var gjenoppbygget!',\n\n    // Code Editor\n    'code_editor' => 'Endre kode',\n    'code_language' => 'Kodespråk',\n    'code_content' => 'Kodeinnhold',\n    'code_session_history' => 'Sesjonshistorikk',\n    'code_save' => 'Lagre kode',\n];\n"
  },
  {
    "path": "lang/nb/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Generelt',\n    'advanced' => 'Avansert',\n    'none' => 'Ingen',\n    'cancel' => 'Avbryt',\n    'save' => 'Lagre',\n    'close' => 'Lukk',\n    'apply' => 'Bruk',\n    'undo' => 'Angre',\n    'redo' => 'Gjør om',\n    'left' => 'Venstre',\n    'center' => 'Sentrert',\n    'right' => 'Høyre',\n    'top' => 'Topp',\n    'middle' => 'Sentrert',\n    'bottom' => 'Bunn',\n    'width' => 'Bredde',\n    'height' => 'Høyde',\n    'More' => 'Mer',\n    'select' => 'Velg …',\n\n    // Toolbar\n    'formats' => 'Formater',\n    'header_large' => 'Stor overskrift',\n    'header_medium' => 'Medium overskrift',\n    'header_small' => 'Liten overskrift',\n    'header_tiny' => 'Bitteliten overskrift',\n    'paragraph' => 'Avsnitt',\n    'blockquote' => 'Blokksitat',\n    'inline_code' => 'Kodesetning',\n    'callouts' => 'Notabene',\n    'callout_information' => 'Informasjon',\n    'callout_success' => 'Positiv',\n    'callout_warning' => 'Advarsel',\n    'callout_danger' => 'Negativ',\n    'bold' => 'Fet',\n    'italic' => 'Kursiv',\n    'underline' => 'Understrek',\n    'strikethrough' => 'Strek over',\n    'superscript' => 'Hevet skrift',\n    'subscript' => 'Senket skrift',\n    'text_color' => 'Tekstfarge',\n    'highlight_color' => 'Uthevingsfarge',\n    'custom_color' => 'Egenvalgt farge',\n    'remove_color' => 'Fjern farge',\n    'background_color' => 'Bakgrunnsfarge',\n    'align_left' => 'Venstrejustering',\n    'align_center' => 'Midtstilling',\n    'align_right' => 'Høyrejustering',\n    'align_justify' => 'Blokkjustering',\n    'list_bullet' => 'Punktliste',\n    'list_numbered' => 'Nummerert liste',\n    'list_task' => 'Oppgaveliste',\n    'indent_increase' => 'Øk innrykk',\n    'indent_decrease' => 'Redusér innrykk',\n    'table' => 'Tabell',\n    'insert_image' => 'Sett inn bilde',\n    'insert_image_title' => 'Sett inn/redigér bilde',\n    'insert_link' => 'Sett inn/redigér lenke',\n    'insert_link_title' => 'Sett inn/redigér lenke',\n    'insert_horizontal_line' => 'Sett inn horisontal linje',\n    'insert_code_block' => 'Sett inn kodeblokk',\n    'edit_code_block' => 'Redigér kodeblokk',\n    'insert_drawing' => 'Sett inn/redigér tegning',\n    'drawing_manager' => 'Tegningsbehandling',\n    'insert_media' => 'Sett inn/redigér media',\n    'insert_media_title' => 'Sett inn/redigér media',\n    'clear_formatting' => 'Rens formattering',\n    'source_code' => 'Kildekode',\n    'source_code_title' => 'Kildekode',\n    'fullscreen' => 'Fullskjerm',\n    'image_options' => 'Bildealternativer',\n\n    // Tables\n    'table_properties' => 'Tabellegenskaper',\n    'table_properties_title' => 'Tabellegenskaper',\n    'delete_table' => 'Slett tabell',\n    'table_clear_formatting' => 'Fjern tabellformatering',\n    'resize_to_contents' => 'Endre størrelsen til innhold',\n    'row_header' => 'Radoverskrift',\n    'insert_row_before' => 'Sett inn rad før',\n    'insert_row_after' => 'Sett inn rad etter',\n    'delete_row' => 'Slett rad',\n    'insert_column_before' => 'Sett inn kolonne før',\n    'insert_column_after' => 'Sett inn kolonne etter',\n    'delete_column' => 'Slett kolonne',\n    'table_cell' => 'Celle',\n    'table_row' => 'Rad',\n    'table_column' => 'Kolonne',\n    'cell_properties' => 'Celle-egenskaper',\n    'cell_properties_title' => 'Celle-egenskaper',\n    'cell_type' => 'Celletype',\n    'cell_type_cell' => 'Celle',\n    'cell_scope' => 'Omfang',\n    'cell_type_header' => 'Topptekst-celle',\n    'merge_cells' => 'Slå sammen celler',\n    'split_cell' => 'Del celle',\n    'table_row_group' => 'Radgruppe',\n    'table_column_group' => 'Kolonnegruppe',\n    'horizontal_align' => 'Horisontal justering',\n    'vertical_align' => 'Vertikal justering',\n    'border_width' => 'Kantbredde',\n    'border_style' => 'Kantstil',\n    'border_color' => 'Kantfarge',\n    'row_properties' => 'Radegenskaper',\n    'row_properties_title' => 'Radegenskaper',\n    'cut_row' => 'Klipp ut rad',\n    'copy_row' => 'Kopiér rad',\n    'paste_row_before' => 'Lim rad inn før',\n    'paste_row_after' => 'Lim rad inn etter',\n    'row_type' => 'Radtype',\n    'row_type_header' => 'Topptekst',\n    'row_type_body' => 'Hovedtekst',\n    'row_type_footer' => 'Bunntekst',\n    'alignment' => 'Justering',\n    'cut_column' => 'Klipp ut kolonne',\n    'copy_column' => 'Kopiér kolonne',\n    'paste_column_before' => 'Lim kolonne inn før',\n    'paste_column_after' => 'Lim kolonne inn etter',\n    'cell_padding' => 'Celleutfylling',\n    'cell_spacing' => 'Celleavstand',\n    'caption' => 'Overskrift',\n    'show_caption' => 'Vis overskrift',\n    'constrain' => 'Behold proporsjoner',\n    'cell_border_solid' => 'Heltrukket',\n    'cell_border_dotted' => 'Prikker',\n    'cell_border_dashed' => 'Stipler',\n    'cell_border_double' => 'Dobbel',\n    'cell_border_groove' => 'Rille',\n    'cell_border_ridge' => 'Kant',\n    'cell_border_inset' => 'Nedsenk',\n    'cell_border_outset' => 'Uthev',\n    'cell_border_none' => 'Ingen',\n    'cell_border_hidden' => 'Skjult bredde',\n\n    // Images, links, details/summary & embed\n    'source' => 'Kilde',\n    'alt_desc' => 'Alternativ beskrivelse',\n    'embed' => 'Bygg inn',\n    'paste_embed' => 'Lim inn koden din her:',\n    'url' => 'Nettlenke',\n    'text_to_display' => 'Synlig tekst',\n    'title' => 'Tittel',\n    'browse_links' => 'Bla gjennom linker',\n    'open_link' => 'Åpne lenke',\n    'open_link_in' => 'Åpne i ...',\n    'open_link_current' => 'Samme vindu',\n    'open_link_new' => 'Nytt vindu',\n    'remove_link' => 'Fjern lenke',\n    'insert_collapsible' => 'Sett inn sammenleggbar blokk',\n    'collapsible_unwrap' => 'Pakk ut',\n    'edit_label' => 'Rediger etikett',\n    'toggle_open_closed' => 'Veksle åpen/lukket',\n    'collapsible_edit' => 'Rediger sammenleggbar blokk',\n    'toggle_label' => 'Veksle etikettsynlighet',\n\n    // About view\n    'about' => 'Om tekstredigeringsprogrammet',\n    'about_title' => 'Om HDSEHDF-tekstredigeringsprogrammet',\n    'editor_license' => 'Tekstbehandlerlisens og opphavsrett',\n    'editor_lexical_license' => 'Denne redaktoren er bygget som en fork av :lexicalLink, som er distribuert under MIT-lisensen.',\n    'editor_lexical_license_link' => 'Full lisensinformasjon finner du her.',\n    'editor_tiny_license' => 'Denne tekstredigereren er laget med :tinyLink som er lisensiert under MIT.',\n    'editor_tiny_license_link' => 'Informasjon om opphavsrett og lisens for TinyMCE finnes her.',\n    'save_continue' => 'Lagre side og fortsett',\n    'callouts_cycle' => '(Fortsett å trykke for å veksle mellom typer)',\n    'link_selector' => 'Lenke til innhold',\n    'shortcuts' => 'Snarveier',\n    'shortcut' => 'Snarvei',\n    'shortcuts_intro' => 'Følgende snarveier er tilgjengelige i tekstredigeringsverktøyet:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(MacOS)',\n    'description' => 'Beskrivelse',\n];\n"
  },
  {
    "path": "lang/nb/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Nylig opprettet',\n    'recently_created_pages' => 'Nylig opprettede sider',\n    'recently_updated_pages' => 'Nylig oppdaterte sider',\n    'recently_created_chapters' => 'Nylig opprettede kapitler',\n    'recently_created_books' => 'Nylig opprettede bøker',\n    'recently_created_shelves' => 'Nylig opprettede bokhyller',\n    'recently_update' => 'Nylig oppdatert',\n    'recently_viewed' => 'Nylig vist',\n    'recent_activity' => 'Nylig aktivitet',\n    'create_now' => 'Opprett en nå',\n    'revisions' => 'Revisjoner',\n    'meta_revision' => 'Revisjon #:revisionCount',\n    'meta_created' => 'Opprettet :timeLength',\n    'meta_created_name' => 'Opprettet :timeLength av :user',\n    'meta_updated' => 'Oppdatert :timeLength',\n    'meta_updated_name' => 'Oppdatert :timeLength av :user',\n    'meta_owned_name' => 'Eies av :user',\n    'meta_reference_count' => 'Sitert på :count side|Sitert på :count sider',\n    'entity_select' => 'Velg entitet',\n    'entity_select_lack_permission' => 'Du har ikke tilgang til å velge dette elementet',\n    'images' => 'Bilder',\n    'my_recent_drafts' => 'Mine nylige utkast',\n    'my_recently_viewed' => 'Mine nylige visninger',\n    'my_most_viewed_favourites' => 'Mine mest sette favoritter',\n    'my_favourites' => 'Mine favoritter',\n    'no_pages_viewed' => 'Du har ikke sett på noen sider',\n    'no_pages_recently_created' => 'Ingen sider har nylig blitt opprettet',\n    'no_pages_recently_updated' => 'Ingen sider har nylig blitt oppdatert',\n    'export' => 'Eksporter',\n    'export_html' => 'Nettside med alt',\n    'export_pdf' => 'PDF Fil',\n    'export_text' => 'Tekstfil',\n    'export_md' => 'Markdownfil',\n    'export_zip' => 'Flyttbar ZIP',\n    'default_template' => 'Standard sidemal',\n    'default_template_explain' => 'Tildel en sidemal som vil bli brukt som standardinnhold for alle nye sider i denne boken. Husk dette vil kun bli brukt hvis sideskaperen har tilgang til den valgte malsiden.',\n    'default_template_select' => 'Velg en malside',\n    'import' => 'Import',\n    'import_validate' => 'Valider Import',\n    'import_desc' => 'Importer bøker, kapitler & sider ved å bruke en flyttbar zip-eksport fra samme eller en annen forekomst. Velg en ZIP-fil for å fortsette. Når filen har blitt lastet opp og validert vil du kunne konfigurere & bekrefte importen i neste visning.',\n    'import_zip_select' => 'Velg ZIP-filen som skal lastes opp',\n    'import_zip_validation_errors' => 'Feil ble funnet under validering av den angitte ZIP-filen:',\n    'import_pending' => 'Venter på import',\n    'import_pending_none' => 'Ingen importer er startet.',\n    'import_continue' => 'Fortsett import',\n    'import_continue_desc' => 'Gjennomgå innholdet på grunn av at det importeres fra den opplastede ZIP-filen. Når klar, kjøre importen for å legge til innholdet i dette systemet. Den opplastede ZIP-importfilen vil automatisk bli fjernet ved vellykket import.',\n    'import_details' => 'Importer detaljer',\n    'import_run' => 'Kjør Import',\n    'import_size' => ':size Import ZIP størrelse',\n    'import_uploaded_at' => 'Opplastet :relativeTime',\n    'import_uploaded_by' => 'Lastet opp av',\n    'import_location' => 'Import posisjon',\n    'import_location_desc' => 'Velg en mållokasjon for ditt importerte innhold. Du vil trenge relevante tillatelser for å opprette innenfor den posisjonen du velger.',\n    'import_delete_confirm' => 'Er du sikker på at du vil slette denne importen?',\n    'import_delete_desc' => 'Dette vil slette den opplastede importen av ZIP-filen og kan ikke angres.',\n    'import_errors' => 'Import feil',\n    'import_errors_desc' => 'Feil oppstod under importforsøket:',\n    'breadcrumb_siblings_for_page' => 'Naviger relaterte sider',\n    'breadcrumb_siblings_for_chapter' => 'Naviger relaterte kapitler',\n    'breadcrumb_siblings_for_book' => 'Naviger relaterte bøker',\n    'breadcrumb_siblings_for_bookshelf' => 'Naviger relaterte hyller',\n\n    // Permissions and restrictions\n    'permissions' => 'Tilganger',\n    'permissions_desc' => 'Endringer gjort her vil overstyre standardrettigheter gitt via brukerroller.',\n    'permissions_book_cascade' => 'Rettigheter satt på bøker vil automatisk arves ned til sidenivå. Du kan overstyre arv ved å definere egne rettigheter på kapitler eller sider.',\n    'permissions_chapter_cascade' => 'Rettigheter satt på kapitler vil automatisk arves ned til sider. Du kan overstyre arv ved å definere rettigheter på enkeltsider.',\n    'permissions_save' => 'Lagre tillatelser',\n    'permissions_owner' => 'Eier',\n    'permissions_role_everyone_else' => 'Alle andre',\n    'permissions_role_everyone_else_desc' => 'Angi rettigheter for alle roller som ikke blir overstyrt (arvede rettigheter).',\n    'permissions_role_override' => 'Overstyr rettigheter for rolle',\n    'permissions_inherit_defaults' => 'Arv standardrettigheter',\n\n    // Search\n    'search_results' => 'Søkeresultater',\n    'search_total_results_found' => ':count resultater funnet|:count totalt',\n    'search_clear' => 'Nullstill søk',\n    'search_no_pages' => 'Ingen sider passer med søket',\n    'search_for_term' => 'Søk etter :term',\n    'search_more' => 'Flere resultater',\n    'search_advanced' => 'Avansert søk',\n    'search_terms' => 'Søkeord',\n    'search_content_type' => 'Innholdstype',\n    'search_exact_matches' => 'Eksakte ord',\n    'search_tags' => 'Søk på merker',\n    'search_options' => 'Alternativer',\n    'search_viewed_by_me' => 'Sett av meg',\n    'search_not_viewed_by_me' => 'Ikke sett av meg',\n    'search_permissions_set' => 'Tilganger er angitt',\n    'search_created_by_me' => 'Opprettet av meg',\n    'search_updated_by_me' => 'Oppdatert av meg',\n    'search_owned_by_me' => 'Eid av meg',\n    'search_date_options' => 'Datoalternativer',\n    'search_updated_before' => 'Oppdatert før',\n    'search_updated_after' => 'Oppdatert etter',\n    'search_created_before' => 'Opprettet før',\n    'search_created_after' => 'Opprettet etter',\n    'search_set_date' => 'Angi dato',\n    'search_update' => 'Oppdater søk',\n\n    // Shelves\n    'shelf' => 'Hylle',\n    'shelves' => 'Hyller',\n    'x_shelves' => ':count hylle|:count hyller',\n    'shelves_empty' => 'Ingen bokhyller er opprettet',\n    'shelves_create' => 'Opprett ny bokhylle',\n    'shelves_popular' => 'Populære bokhyller',\n    'shelves_new' => 'Nye bokhyller',\n    'shelves_new_action' => 'Ny bokhylle',\n    'shelves_popular_empty' => 'De mest populære bokhyllene blir vist her.',\n    'shelves_new_empty' => 'Nylig opprettede bokhyller vises her.',\n    'shelves_save' => 'Lagre hylle',\n    'shelves_books' => 'Bøker på denne hyllen',\n    'shelves_add_books' => 'Legg til bøker på denne hyllen',\n    'shelves_drag_books' => 'Dra og slipp bøker nedenfor for å legge dem til i denne hyllen',\n    'shelves_empty_contents' => 'INgen bøker er stablet i denne hylla',\n    'shelves_edit_and_assign' => 'Endre hylla for å legge til bøker',\n    'shelves_edit_named' => 'Rediger :name (hylle)',\n    'shelves_edit' => 'Rediger hylle',\n    'shelves_delete' => 'Fjern hylle',\n    'shelves_delete_named' => 'Fjern :name (hylle)',\n    'shelves_delete_explain' => \"Dette vil fjerne hyllen «:name». Bøkene på hyllen vil ikke bli slettet fra systemet.\",\n    'shelves_delete_confirmation' => 'Er du sikker på at du vil fjerne denne hyllen?',\n    'shelves_permissions' => 'Hyllerettigheter',\n    'shelves_permissions_updated' => 'Oppdaterte hyllerettigheter',\n    'shelves_permissions_active' => 'Aktiverte hyllerettigheter',\n    'shelves_permissions_cascade_warning' => 'Rettigheter på en hylle blir ikke automatisk arvet av bøker på hylla. Dette er fordi en bok kan finnes på flere hyller samtidig. Rettigheter kan likevel kopieres til bøker på hylla ved å bruke alternativene under.',\n    'shelves_permissions_create' => 'Bokhylle-tillatelser brukes kun for kopiering av tillatelser til under-bøker ved hjelp av handlingen nedenfor. De kontrollerer ikke muligheten til å lage bøker.',\n    'shelves_copy_permissions_to_books' => 'Kopier tilganger til bøkene på hylla',\n    'shelves_copy_permissions' => 'Kopier tilganger',\n    'shelves_copy_permissions_explain' => 'Dette vil kopiere rettighetene på denne hylla til alle bøkene som er plassert på den. Før du starter kopieringen bør du sjekke at rettighetene på hylla er lagret først.',\n    'shelves_copy_permission_success' => 'Rettighetene ble kopiert til :count bøker',\n\n    // Books\n    'book' => 'Bok',\n    'books' => 'Bøker',\n    'x_books' => ':count bok|:count bøker',\n    'books_empty' => 'Ingen bøker er skrevet',\n    'books_popular' => 'Populære bøker',\n    'books_recent' => 'Nylige bøker',\n    'books_new' => 'Nye bøker',\n    'books_new_action' => 'Ny bok',\n    'books_popular_empty' => 'De mest populære bøkene',\n    'books_new_empty' => 'Siste utgivelser vises her.',\n    'books_create' => 'Skriv ny bok',\n    'books_delete' => 'Brenn bok',\n    'books_delete_named' => 'Brenn boken :bookName',\n    'books_delete_explain' => 'Dette vil brenne boken «:bookName». Alle sider i boken vil fordufte for godt.',\n    'books_delete_confirmation' => 'Er du sikker på at du vil brenne boken?',\n    'books_edit' => 'Endre bok',\n    'books_edit_named' => 'Endre boken :bookName',\n    'books_form_book_name' => 'Boktittel',\n    'books_save' => 'Lagre bok',\n    'books_permissions' => 'Boktilganger',\n    'books_permissions_updated' => 'Boktilganger oppdatert',\n    'books_empty_contents' => 'Ingen sider eller kapitler finnes i denne boken.',\n    'books_empty_create_page' => 'Skriv en ny side',\n    'books_empty_sort_current_book' => 'Sorter innholdet i boken',\n    'books_empty_add_chapter' => 'Start på nytt kapittel',\n    'books_permissions_active' => 'Boktilganger er aktive',\n    'books_search_this' => 'Søk i boken',\n    'books_navigation' => 'Boknavigasjon',\n    'books_sort' => 'Sorter bokinnhold',\n    'books_sort_desc' => 'Flytt kapitler og sider innen en bok for å reorganisere innholdet. Andre bøker kan legges til, noe som gjør det enkelt å flytte kapitler og sider mellom bøkene. Valgfritt kan en automatisk sorteringsregel settes for å automatisk sortere innholdet i denne boken ved endringer.',\n    'books_sort_auto_sort' => 'Automatisk sorteringsalternativ',\n    'books_sort_auto_sort_active' => 'Automatisk sortering aktiv: :sortName',\n    'books_sort_named' => 'Omorganisér :bookName (bok)',\n    'books_sort_name' => 'Sorter på navn',\n    'books_sort_created' => 'Sorter på opprettet dato',\n    'books_sort_updated' => 'Sorter på oppdatert dato',\n    'books_sort_chapters_first' => 'Kapitler først',\n    'books_sort_chapters_last' => 'Kapitler sist',\n    'books_sort_show_other' => 'Vis andre bøker',\n    'books_sort_save' => 'Lagre sortering',\n    'books_sort_show_other_desc' => 'Legg til andre bøker her for å inkludere dem i omorganiseringen og muliggjør enkel flytting på tvers av dem.',\n    'books_sort_move_up' => 'Flytt opp',\n    'books_sort_move_down' => 'Flytt ned',\n    'books_sort_move_prev_book' => 'Flytt til forrige bok',\n    'books_sort_move_next_book' => 'Flytt til neste bok',\n    'books_sort_move_prev_chapter' => 'Flytt inn i forrige kapittel',\n    'books_sort_move_next_chapter' => 'Flytt inn i neste kapittel',\n    'books_sort_move_book_start' => 'Flytt til starten av boken',\n    'books_sort_move_book_end' => 'Flytt til slutten av boken',\n    'books_sort_move_before_chapter' => 'Flytt før kapittel',\n    'books_sort_move_after_chapter' => 'Flytt etter kapittel',\n    'books_copy' => 'Kopiér bok',\n    'books_copy_success' => 'Boken ble kopiert',\n\n    // Chapters\n    'chapter' => 'Kapittel',\n    'chapters' => 'Kapitler',\n    'x_chapters' => ':count kapittel|:count kapitler',\n    'chapters_popular' => 'Populære kapitler',\n    'chapters_new' => 'Nytt kapittel',\n    'chapters_create' => 'Skriv nytt kapittel',\n    'chapters_delete' => 'Riv ut kapittel',\n    'chapters_delete_named' => 'Slett :chapterName (kapittel)',\n    'chapters_delete_explain' => 'Dette vil slette «:chapterName» (kapittel). Alle sider i kapittelet vil også slettes.',\n    'chapters_delete_confirm' => 'Er du sikker på at du vil slette dette kapittelet?',\n    'chapters_edit' => 'Redigér kapittel',\n    'chapters_edit_named' => 'Redigér :chapterName (kapittel)',\n    'chapters_save' => 'Lagre kapittel',\n    'chapters_move' => 'Flytt kapittel',\n    'chapters_move_named' => 'Flytt :chapterName (kapittel)',\n    'chapters_copy' => 'Kopiér kapittel',\n    'chapters_copy_success' => 'Kapitelet ble kopiert',\n    'chapters_permissions' => 'Kapitteltilganger',\n    'chapters_empty' => 'Det finnes ingen sider i dette kapittelet.',\n    'chapters_permissions_active' => 'Kapitteltilganger er aktivert',\n    'chapters_permissions_success' => 'Kapitteltilgager er oppdatert',\n    'chapters_search_this' => 'Søk i dette kapittelet',\n    'chapter_sort_book' => 'Omorganisér bok',\n\n    // Pages\n    'page' => 'Side',\n    'pages' => 'Sider',\n    'x_pages' => ':count side|:count sider',\n    'pages_popular' => 'Populære sider',\n    'pages_new' => 'Ny side',\n    'pages_attachments' => 'Vedlegg',\n    'pages_navigation' => 'Sidenavigasjon',\n    'pages_delete' => 'Slett side',\n    'pages_delete_named' => 'Slett :pageName (side)',\n    'pages_delete_draft_named' => 'Slett utkastet :pageName (side)',\n    'pages_delete_draft' => 'Slett utkastet',\n    'pages_delete_success' => 'Siden er slettet',\n    'pages_delete_draft_success' => 'Sideutkastet ble slettet',\n    'pages_delete_warning_template' => 'Denne siden er i aktiv bruk som en bok eller kapittelstandard sidemal. Disse bøkene eller kapitlene vil ikke lenger ha en standardmal som er tilordnet etter at denne siden er slettet.',\n    'pages_delete_confirm' => 'Er du sikker på at du vil slette siden?',\n    'pages_delete_draft_confirm' => 'Er du sikker på at du vil slette utkastet?',\n    'pages_editing_named' => 'Redigerer :pageName (side)',\n    'pages_edit_draft_options' => 'Utkastsalternativer',\n    'pages_edit_save_draft' => 'Lagre utkast',\n    'pages_edit_draft' => 'Redigér utkast',\n    'pages_editing_draft' => 'Redigerer utkast',\n    'pages_editing_page' => 'Redigerer side',\n    'pages_edit_draft_save_at' => 'Sist lagret ',\n    'pages_edit_delete_draft' => 'Slett utkast',\n    'pages_edit_delete_draft_confirm' => 'Er du sikker på at du vil slette utkastendringer i utkastet? Alle dine endringer, siden siste lagring vil gå tapt, og editoren vil bli oppdatert med den siste siden uten utkast til lagring.',\n    'pages_edit_discard_draft' => 'Tilbakestill endring',\n    'pages_edit_switch_to_markdown' => 'Bytt til Markdown tekstredigering',\n    'pages_edit_switch_to_markdown_clean' => '(Renset innhold)',\n    'pages_edit_switch_to_markdown_stable' => '(Urørt innhold)',\n    'pages_edit_switch_to_wysiwyg' => 'Bytt til WYSIWYG tekstredigering',\n    'pages_edit_switch_to_new_wysiwyg' => 'Bytt til ny WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(under Beta-testing)',\n    'pages_edit_set_changelog' => 'Angi endringslogg',\n    'pages_edit_enter_changelog_desc' => 'Gi en kort beskrivelse av endringene dine',\n    'pages_edit_enter_changelog' => 'Se endringslogg',\n    'pages_editor_switch_title' => 'Bytt tekstredigeringsprogram',\n    'pages_editor_switch_are_you_sure' => 'Er du sikker på at du vil bytte tekstredigeringsprogram for denne siden?',\n    'pages_editor_switch_consider_following' => 'Husk dette når du bytter tekstredigeringsprogram:',\n    'pages_editor_switch_consideration_a' => 'Når du bytter, vil den nye tekstredigereren bli satt for alle fremtidige redaktører. Dette inkluderer alle redaktører som ikke kan endre type selv.',\n    'pages_editor_switch_consideration_b' => 'Dette kan potensielt føre til tap av formatdetaljer eller syntaks i noen tilfeller.',\n    'pages_editor_switch_consideration_c' => 'Etikett- eller redigeringslogg-endringer loggført siden siste lagring vil ikke føres videre etter endringen.',\n    'pages_save' => 'Lagre side',\n    'pages_title' => 'Sidetittel',\n    'pages_name' => 'Sidenavn',\n    'pages_md_editor' => 'Tekstbehandler',\n    'pages_md_preview' => 'Forhåndsvisning',\n    'pages_md_insert_image' => 'Sett inn bilde',\n    'pages_md_insert_link' => 'Sett inn lenke',\n    'pages_md_insert_drawing' => 'Sett inn tegning',\n    'pages_md_show_preview' => 'Forhåndsvisning',\n    'pages_md_sync_scroll' => 'Synkroniser forhåndsvisningsrulle',\n    'pages_md_plain_editor' => 'Redigeringsverktøy for klartekst',\n    'pages_drawing_unsaved' => 'Ulagret tegning funnet',\n    'pages_drawing_unsaved_confirm' => 'Ulagret tegningsdata ble funnet fra en tidligere mislykket lagring. Vil du gjenopprette og fortsette å redigere denne ulagrede tegningen?',\n    'pages_not_in_chapter' => 'Siden tilhører ingen kapittel',\n    'pages_move' => 'Flytt side',\n    'pages_copy' => 'Kopiér side',\n    'pages_copy_desination' => 'Destinasjon',\n    'pages_copy_success' => 'Siden ble flyttet',\n    'pages_permissions' => 'Sidetilganger',\n    'pages_permissions_success' => 'Sidens tilganger ble endret',\n    'pages_revision' => 'Revisjon',\n    'pages_revisions' => 'Sidens revisjoner',\n    'pages_revisions_desc' => 'Oppført nedenfor er alle tidligere revisjoner av denne siden. Du kan se tilbake igjen, sammenligne og gjenopprette tidligere sideversjoner hvis du tillater det. Den hele sidens historikk kan kanskje ikke gjenspeiles fullstendig her, avhengig av systemkonfigurasjonen, kan gamle revisjoner bli slettet automatisk.',\n    'pages_revisions_named' => 'Revisjoner for :pageName',\n    'pages_revision_named' => 'Revisjoner for :pageName',\n    'pages_revision_restored_from' => 'Gjenopprettet fra #:id; :summary',\n    'pages_revisions_created_by' => 'Skrevet av',\n    'pages_revisions_date' => 'Revideringsdato',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Revisjonsnummer',\n    'pages_revisions_numbered' => 'Revisjon #:id',\n    'pages_revisions_numbered_changes' => 'Endringer på revisjon #:id',\n    'pages_revisions_editor' => 'Tekstredigeringstype',\n    'pages_revisions_changelog' => 'Endringslogg',\n    'pages_revisions_changes' => 'Endringer',\n    'pages_revisions_current' => 'Siste versjon',\n    'pages_revisions_preview' => 'Forhåndsvisning',\n    'pages_revisions_restore' => 'Gjenopprett',\n    'pages_revisions_none' => 'Denne siden har ingen revisjoner',\n    'pages_copy_link' => 'Kopier lenke',\n    'pages_edit_content_link' => 'Hopp til seksjonen i tekstbehandleren',\n    'pages_pointer_enter_mode' => 'Gå til seksjonen velg modus',\n    'pages_pointer_label' => 'Sidens seksjon alternativer',\n    'pages_pointer_permalink' => 'Sideseksjons permalenke',\n    'pages_pointer_include_tag' => 'Sideseksjonen inkluderer Tag',\n    'pages_pointer_toggle_link' => 'Permalenke modus, trykk for å vise inkluderer tag',\n    'pages_pointer_toggle_include' => 'Inkluder tag-modus, trykk for å vise permalenke',\n    'pages_permissions_active' => 'Sidetilganger er aktive',\n    'pages_initial_revision' => 'Første publisering',\n    'pages_references_update_revision' => 'Automatisk oppdatering av interne lenker',\n    'pages_initial_name' => 'Ny side',\n    'pages_editing_draft_notification' => 'Du skriver på et utkast som sist ble lagret :timeDiff.',\n    'pages_draft_edited_notification' => 'Siden har blitt endret siden du startet. Det anbefales at du forkaster dine endringer.',\n    'pages_draft_page_changed_since_creation' => 'Denne siden er blitt oppdatert etter at dette utkastet ble opprettet. Det anbefales at du forkaster dette utkastet, eller er ekstra forsiktig slik at du ikke overskriver noen sideendringer.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count forfattere har begynt å endre denne siden.',\n        'start_b' => ':userName skriver på siden for øyeblikket',\n        'time_a' => 'siden sist siden ble oppdatert',\n        'time_b' => 'i løpet av de siste :minCount minuttene',\n        'message' => ':start :time. Prøv å ikke overskriv hverandres endringer!',\n    ],\n    'pages_draft_discarded' => 'Utkastet er forkastet! Redigeringsprogrammet er oppdatert med gjeldende sideinnhold',\n    'pages_draft_deleted' => 'Utkast slettet! Redigeringsprogrammet er oppdatert med gjeldende sideinnhold',\n    'pages_specific' => 'Bestemt side',\n    'pages_is_template' => 'Sidemal',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Bytt sidestolpe',\n    'page_tags' => 'Sidemerker',\n    'chapter_tags' => 'Kapittelmerker',\n    'book_tags' => 'Bokmerker',\n    'shelf_tags' => 'Hyllemerker',\n    'tag' => 'Merke',\n    'tags' =>  'Merker',\n    'tags_index_desc' => 'Merker kan brukes på innhold i systemet for å anvende en kategorisering på en fleksibel måte. Etiketter kan ha både en nøkkel og verdi, med valgfri. Når det er brukt, kan innhold sjekkes ved hjelp av taggnavn og verdi.',\n    'tag_name' =>  'Merketittel',\n    'tag_value' => 'Merkeverdi (Valgfritt)',\n    'tags_explain' => \"Legg til merker for å kategorisere innholdet ditt. \\n Du kan legge til merkeverdier for å beskrive dem ytterligere.\",\n    'tags_add' => 'Legg til flere merker',\n    'tags_remove' => 'Fjern merke',\n    'tags_usages' => 'Totalt emneordbruk',\n    'tags_assigned_pages' => 'Tilordnet sider',\n    'tags_assigned_chapters' => 'Tildelt til kapitler',\n    'tags_assigned_books' => 'Tilordnet til bøker',\n    'tags_assigned_shelves' => 'Tilordnet hyller',\n    'tags_x_unique_values' => ':count unike verdier',\n    'tags_all_values' => 'Alle verdier',\n    'tags_view_tags' => 'Vis etiketter',\n    'tags_view_existing_tags' => 'Vis eksisterende etiketter',\n    'tags_list_empty_hint' => 'Etiketter kan tilordnes via sidepanelet, eller mens du redigerer detaljene for en hylle, bok eller kapittel.',\n    'attachments' => 'Vedlegg',\n    'attachments_explain' => 'Last opp vedlegg eller legg til lenker for å berike innholdet. Disse vil vises i sidestolpen på siden.',\n    'attachments_explain_instant_save' => 'Endringer her blir lagret med en gang.',\n    'attachments_upload' => 'Last opp vedlegg',\n    'attachments_link' => 'Fest lenke',\n    'attachments_upload_drop' => 'Alternativt kan du dra og slippe en fil her for å laste den opp som et vedlegg.',\n    'attachments_set_link' => 'Angi lenke',\n    'attachments_delete' => 'Er du sikker på at du vil fjerne vedlegget?',\n    'attachments_dropzone' => 'Slipp filer her for å laste opp',\n    'attachments_no_files' => 'Ingen vedlegg er lastet opp',\n    'attachments_explain_link' => 'Du kan feste lenker til denne. Det kan være henvisning til andre sider, bøker etc. eller lenker fra nettet.',\n    'attachments_link_name' => 'Lenkenavn',\n    'attachment_link' => 'Vedleggslenke',\n    'attachments_link_url' => 'Lenke til vedlegg',\n    'attachments_link_url_hint' => 'Adresse til lenke eller vedlegg',\n    'attach' => 'Fest',\n    'attachments_insert_link' => 'Fest vedleggslenke',\n    'attachments_edit_file' => 'Endre vedlegg',\n    'attachments_edit_file_name' => 'Vedleggsnavn',\n    'attachments_edit_drop_upload' => 'Dra og slipp eller trykk her for å oppdatere eller overskrive',\n    'attachments_order_updated' => 'Vedleggssortering endret',\n    'attachments_updated_success' => 'Vedleggsdetaljer endret',\n    'attachments_deleted' => 'Vedlegg fjernet',\n    'attachments_file_uploaded' => 'Vedlegg ble lastet opp',\n    'attachments_file_updated' => 'Vedlegget ble oppdatert',\n    'attachments_link_attached' => 'Lenken ble festet til siden',\n    'templates' => 'Maler',\n    'templates_set_as_template' => 'Siden er en mal',\n    'templates_explain_set_as_template' => 'Du kan angi denne siden som en mal slik at innholdet kan brukes når du oppretter andre sider. Andre brukere vil kunne bruke denne malen hvis de har visningstillatelser for denne siden.',\n    'templates_replace_content' => 'Bytt sideinnhold',\n    'templates_append_content' => 'Legg til neders på siden',\n    'templates_prepend_content' => 'Legg til øverst på siden',\n\n    // Profile View\n    'profile_user_for_x' => 'Medlem i :time',\n    'profile_created_content' => 'Har skrevet',\n    'profile_not_created_pages' => ':userName har ikke forfattet noen sider',\n    'profile_not_created_chapters' => ':userName har ikke opprettet noen kapitler',\n    'profile_not_created_books' => ':userName har ikke laget noen bøker',\n    'profile_not_created_shelves' => ':userName har ikke hengt opp noen hyller',\n\n    // Comments\n    'comment' => 'Kommentar',\n    'comments' => 'Kommentarer',\n    'comment_add' => 'Skriv kommentar',\n    'comment_none' => 'Ingen kommentarer å vise',\n    'comment_placeholder' => 'Skriv en kommentar her',\n    'comment_thread_count' => ':count Kommentar Tråd|:count Kommentar Tråder',\n    'comment_archived_count' => ':count Arkivert',\n    'comment_archived_threads' => 'Arkiverte tråder',\n    'comment_save' => 'Publiser kommentar',\n    'comment_new' => 'Ny kommentar',\n    'comment_created' => 'kommenterte :createDiff',\n    'comment_updated' => 'Oppdatert :updateDiff av :username',\n    'comment_updated_indicator' => 'Oppdatert',\n    'comment_deleted_success' => 'Kommentar fjernet',\n    'comment_created_success' => 'Kommentar skrevet',\n    'comment_updated_success' => 'Kommentar endret',\n    'comment_archive_success' => 'Kommentar arkivert',\n    'comment_unarchive_success' => 'Kommentar uarkivert',\n    'comment_view' => 'Vis kommentar',\n    'comment_jump_to_thread' => 'Gå til tråd',\n    'comment_delete_confirm' => 'Er du sikker på at du vil fjerne kommentaren?',\n    'comment_in_reply_to' => 'Som svar til :commentId',\n    'comment_reference' => 'Referanse',\n    'comment_reference_outdated' => '(Utdatert)',\n    'comment_editor_explain' => 'Her er kommentarene som er på denne siden. Kommentarer kan legges til og administreres når du ser på den lagrede siden.',\n\n    // Revision\n    'revision_delete_confirm' => 'Vil du slette revisjonen?',\n    'revision_restore_confirm' => 'Vil du gjenopprette revisjonen? Innholdet på siden vil bli overskrevet med denne revisjonen.',\n    'revision_cannot_delete_latest' => 'CKan ikke slette siste revisjon.',\n\n    // Copy view\n    'copy_consider' => 'Vennligst vurder nedenfor når du kopierer innholdet.',\n    'copy_consider_permissions' => 'Egendefinerte tilgangsinnstillinger vil ikke bli kopiert.',\n    'copy_consider_owner' => 'Du vil bli eier av alt kopiert innhold.',\n    'copy_consider_images' => 'Sidebildefiler vil ikke bli duplisert og de opprinnelige bildene beholder relasjonen til siden de opprinnelig ble lastet opp til.',\n    'copy_consider_attachments' => 'Sidevedlegg vil ikke bli kopiert.',\n    'copy_consider_access' => 'Endring av sted, eier eller rettigheter kan føre til at innholdet er tilgjengelig for dem som tidligere har vært uten adgang.',\n\n    // Conversions\n    'convert_to_shelf' => 'Konverter til bokhylle',\n    'convert_to_shelf_contents_desc' => 'Du kan konvertere denne boken til en ny hylle med samme innhold. Kapitteler i denne boken vil bli konvertert til nye bøker. Hvis boken inneholder noen sider, som ikke er i et kapitler, boka blir omdøpt og med slike sider, og boka blir en del av den nye bokhyllen.',\n    'convert_to_shelf_permissions_desc' => 'Eventuelle tillatelser som er satt på denne boka, vil bli kopiert til ny hylle og til alle nye under-bøker som ikke har egne tillatelser satt. Vær oppmerksom på at tillatelser på hyllene ikke skjuler automatisk innhold innenfor, da de gjør for bøker.',\n    'convert_book' => 'Konverter bok',\n    'convert_book_confirm' => 'Er du sikker på at du vil konvertere denne boken?',\n    'convert_undo_warning' => 'Dette kan ikke bli så lett å angre.',\n    'convert_to_book' => 'Konverter til bok',\n    'convert_to_book_desc' => 'Du kan konvertere kapittelet til en ny bok med samme innhold. Alle tillatelser som er angitt i dette kapittelet vil bli kopiert til den nye boken, men alle arvede tillatelser, fra overordnet bok vil ikke kopieres noe som kan føre til en endring av tilgangskontroll.',\n    'convert_chapter' => 'Konverter kapittel',\n    'convert_chapter_confirm' => 'Er du sikker på at du vil konvertere dette kapittelet?',\n\n    // References\n    'references' => 'Referanser',\n    'references_none' => 'Det er ingen sporede referanser til dette elementet.',\n    'references_to_desc' => 'Nedenfor vises alle de kjente sidene i systemet som lenker til denne oppføringen.',\n\n    // Watch Options\n    'watch' => 'Overvåk',\n    'watch_title_default' => 'Standardinnstillinger',\n    'watch_desc_default' => 'Bytt til dine standardinnstilleringer for varsling.',\n    'watch_title_ignore' => 'Ignorer',\n    'watch_desc_ignore' => 'Ignorer alle varslinger, inkludert de fra preferanser for brukernivå.',\n    'watch_title_new' => 'Nye sider',\n    'watch_desc_new' => 'Varsle når en ny side er opprettet innenfor dette elementet.',\n    'watch_title_updates' => 'Alle sideoppdateringer',\n    'watch_desc_updates' => 'Varsle på alle nye sider og endringer av siden.',\n    'watch_desc_updates_page' => 'Varsle ved alle sideendringer.',\n    'watch_title_comments' => 'Alle sideoppdateringer og kommentarer',\n    'watch_desc_comments' => 'Varsle om alle nye sider, endringer på side og nye kommentarer.',\n    'watch_desc_comments_page' => 'Varsle ved sideendringer og nye kommentarer.',\n    'watch_change_default' => 'Endre standard varslingsinnstillinger',\n    'watch_detail_ignore' => 'Ignorerer varsler',\n    'watch_detail_new' => 'Varsling for nye sider',\n    'watch_detail_updates' => 'Varsling for nye sider og oppdateringer',\n    'watch_detail_comments' => 'Varsling for nye sider, oppdateringer og kommentarer',\n    'watch_detail_parent_book' => 'Overvåker via overordnet bok',\n    'watch_detail_parent_book_ignore' => 'Ignorerer via overordnet bok',\n    'watch_detail_parent_chapter' => 'Overvåker via overordnet kapittel',\n    'watch_detail_parent_chapter_ignore' => 'Ignorerer via overordnet kapittel',\n];\n"
  },
  {
    "path": "lang/nb/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Du har ikke tilgang til å se denne siden.',\n    'permissionJson' => 'Du har ikke tilgang til å utføre denne handlingen.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'En konto med :email finnes allerede, men har andre detaljer.',\n    'auth_pre_register_theme_prevention' => 'Brukerkonto kunne ikke bli registrert for de angitte opplysningene',\n    'email_already_confirmed' => 'E-posten er allerede bekreftet, du kan forsøke å logge inn.',\n    'email_confirmation_invalid' => 'Denne bekreftelseskoden er allerede benyttet eller utgått. Prøv å registrere på nytt.',\n    'email_confirmation_expired' => 'Bekreftelseskoden er allerede utgått, en ny e-post er sendt.',\n    'email_confirmation_awaiting' => 'Du må bekrefte e-posten for denne kontoen.',\n    'ldap_fail_anonymous' => 'LDAP kan ikke benyttes med anonym tilgang for denne tjeneren.',\n    'ldap_fail_authed' => 'LDAP tilgang feilet med angitt DN',\n    'ldap_extension_not_installed' => 'LDAP PHP modulen er ikke installert.',\n    'ldap_cannot_connect' => 'Klarer ikke koble til LDAP på denne adressen',\n    'saml_already_logged_in' => 'Allerede logget inn',\n    'saml_no_email_address' => 'Denne kontoinformasjonen finnes ikke i det eksterne autentiseringssystemet.',\n    'saml_invalid_response_id' => 'Forespørselen fra det eksterne autentiseringssystemet gjenkjennes ikke av en prosess som startes av dette programmet. Å navigere tilbake etter pålogging kan forårsake dette problemet.',\n    'saml_fail_authed' => 'Innlogging gjennom :system feilet. Fikk ikke kontakt med autentiseringstjeneren.',\n    'oidc_already_logged_in' => 'Allerede logget inn',\n    'oidc_no_email_address' => 'Finner ikke en e-postadresse, for denne brukeren, i dataene som leveres av det eksterne autentiseringssystemet',\n    'oidc_fail_authed' => 'Innlogging ved hjelp av :system feilet, systemet ga ikke vellykket godkjenning',\n    'social_no_action_defined' => 'Ingen handlinger er definert',\n    'social_login_bad_response' => \"Feilmelding mottat fra :socialAccount innloggingstjeneste: \\n:error\",\n    'social_account_in_use' => 'Denne :socialAccount kontoen er allerede registrert, Prøv å logge inn med :socialAccount alternativet.',\n    'social_account_email_in_use' => 'E-posten :email er allerede i bruk. Har du allerede en konto hos :socialAccount kan dette angis fra profilsiden din.',\n    'social_account_existing' => 'Denne :socialAccount er allerede koblet til din konto.',\n    'social_account_already_used_existing' => 'Denne :socialAccount kontoen brukes allerede av noen andre.',\n    'social_account_not_used' => 'Denne :socialAccount konten er ikke koblet til noen konto, angi denne i profilinnstillingene dine. ',\n    'social_account_register_instructions' => 'Har du ikke en konto her ennå, kan du benytte :socialAccount alternativet for å registrere deg.',\n    'social_driver_not_found' => 'Autentiseringstjeneste fra sosiale medier er ikke installert',\n    'social_driver_not_configured' => 'Dine :socialAccount innstilliner er ikke angitt.',\n    'invite_token_expired' => 'Invitasjonslenken har utgått, du kan forsøke å be om nytt passord istede.',\n    'login_user_not_found' => 'En bruker for denne handlingen ble ikke funnet.',\n\n    // System\n    'path_not_writable' => 'Filstien :filePath aksepterer ikke filer, du må sjekke filstitilganger i systemet.',\n    'cannot_get_image_from_url' => 'Kan ikke hente bilde fra :url',\n    'cannot_create_thumbs' => 'Kan ikke opprette miniatyrbilder. GD PHP er ikke installert.',\n    'server_upload_limit' => 'Vedlegget er for stort, forsøk med et mindre vedlegg.',\n    'server_post_limit' => 'Serveren kan ikke motta det denne mengde data. Prøv igjen med mindre data eller en mindre fil.',\n    'uploaded'  => 'Tjenesten aksepterer ikke vedlegg som er så stor.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Bildet kunne ikke lastes opp, forsøk igjen.',\n    'image_upload_type_error' => 'Bildeformatet støttes ikke, forsøk med et annet format.',\n    'image_upload_replace_type' => 'Bildeerstatning må være av samme type',\n    'image_upload_memory_limit' => 'Kunne ikke håndtere bildeopplasting og/eller lage miniatyrbilder på grunn av systemressursgrensen.',\n    'image_thumbnail_memory_limit' => 'Kunne ikke opprette variasjoner i bildestørrelse på grunn av systemressursgrensen.',\n    'image_gallery_thumbnail_memory_limit' => 'Kunne ikke opprette miniatyrbilder på grunn av systemressursgrensene.',\n    'drawing_data_not_found' => 'Tegningsdata kunne ikke lastes. Det er mulig at tegningsfilen ikke finnes lenger, eller du har ikke rettigheter til å få tilgang til den.',\n\n    // Attachments\n    'attachment_not_found' => 'Vedlegget ble ikke funnet',\n    'attachment_upload_error' => 'En feil har oppstått ved opplasting av vedleggsfil',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Kunne ikke lagre utkastet, forsikre deg om at du er tilkoblet tjeneren (Har du nettilgang?)',\n    'page_draft_delete_fail' => 'Kunne ikke slette sideutkast og hente gjeldende side lagret innhold',\n    'page_custom_home_deletion' => 'Kan ikke slette en side som er satt som forside.',\n\n    // Entities\n    'entity_not_found' => 'Entitet ble ikke funnet',\n    'bookshelf_not_found' => 'Bokhyllen ble ikke funnet',\n    'book_not_found' => 'Boken ble ikke funnet',\n    'page_not_found' => 'Siden ble ikke funnet',\n    'chapter_not_found' => 'Kapittel ble ikke funnet',\n    'selected_book_not_found' => 'Den valgte boken eksisterer ikke',\n    'selected_book_chapter_not_found' => 'Den valgte boken eller kapittelet eksisterer ikke',\n    'guests_cannot_save_drafts' => 'Gjester kan ikke lagre utkast',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Du kan ikke slette den eneste administratoren',\n    'users_cannot_delete_guest' => 'Du kan ikke slette gjestebrukeren (Du kan deaktivere offentlig visning istede)',\n    'users_could_not_send_invite' => 'Kunne ikke opprette bruker fordi invitasjons e-post ikke kunne sendes',\n\n    // Roles\n    'role_cannot_be_edited' => 'Denne rollen kan ikke endres',\n    'role_system_cannot_be_deleted' => 'Denne systemrollen kan ikke slettes',\n    'role_registration_default_cannot_delete' => 'Du kan ikke slette en rolle som er satt som registreringsrolle (rollen nye kontoer får når de registrerer seg)',\n    'role_cannot_remove_only_admin' => 'Denne brukeren er den eneste brukeren som er tildelt administratorrollen. Tilordne administratorrollen til en annen bruker før du prøver å fjerne den her.',\n\n    // Comments\n    'comment_list' => 'Det oppstod en feil under henting av kommentarene.',\n    'cannot_add_comment_to_draft' => 'Du kan ikke legge til kommentarer i et utkast.',\n    'comment_add' => 'Det oppsto en feil da kommentaren skulle legges til / oppdateres.',\n    'comment_delete' => 'Det oppstod en feil under sletting av kommentaren.',\n    'empty_comment' => 'Kan ikke legge til en tom kommentar.',\n\n    // Error pages\n    '404_page_not_found' => 'Siden finnes ikke',\n    'sorry_page_not_found' => 'Beklager, siden du leter etter ble ikke funnet.',\n    'sorry_page_not_found_permission_warning' => 'Hvis du forventet at denne siden skulle eksistere, har du kanskje ikke tillatelse til å se den.',\n    'image_not_found' => 'Bildet ble ikke funnet',\n    'image_not_found_subtitle' => 'Beklager, bildefilen du ser etter ble ikke funnet.',\n    'image_not_found_details' => 'Om du forventet at dette bildet skal eksistere, er det mulig det er slettet.',\n    'return_home' => 'Gå til hovedside',\n    'error_occurred' => 'En feil oppsto',\n    'app_down' => ':appName er nede for øyeblikket',\n    'back_soon' => 'Den vil snart komme tilbake.',\n\n    // Import\n    'import_zip_cant_read' => 'Kunne ikke lese ZIP-filen.',\n    'import_zip_cant_decode_data' => 'Kunne ikke finne og dekode ZIP data.json innhold.',\n    'import_zip_no_data' => 'ZIP-fildata har ingen forventet bok, kapittel eller sideinnhold.',\n    'import_zip_data_too_large' => 'ZIP data.json innholdet overskrider maksimal filstørrelse for opplasting.',\n    'import_validation_failed' => 'Import av ZIP feilet i å validere med feil:',\n    'import_zip_failed_notification' => 'Kunne ikke importere ZIP-fil.',\n    'import_perms_books' => 'Du mangler nødvendige tillatelser for å lage bøker.',\n    'import_perms_chapters' => 'Du mangler de nødvendige tillatelsene for å opprette kapittel.',\n    'import_perms_pages' => 'Du mangler nødvendige tillatelser for å opprette sider.',\n    'import_perms_images' => 'Du mangler de nødvendige tillatelsene for å opprette bilder.',\n    'import_perms_attachments' => 'Du mangler nødvendig tillatelse for å opprette vedlegg.',\n\n    // API errors\n    'api_no_authorization_found' => 'Ingen autorisasjonstoken ble funnet på forespørselen',\n    'api_bad_authorization_format' => 'Det ble funnet et autorisasjonstoken på forespørselen, men formatet virket feil',\n    'api_user_token_not_found' => 'Ingen samsvarende API-token ble funnet for det angitte autorisasjonstokenet',\n    'api_incorrect_token_secret' => 'Hemmeligheten som er gitt for det gitte brukte API-tokenet er feil',\n    'api_user_no_api_permission' => 'Eieren av det brukte API-tokenet har ikke tillatelse til å ringe API-samtaler',\n    'api_user_token_expired' => 'Autorisasjonstokenet som er brukt, har utløpt',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Feil kastet når du sendte en test-e-post:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'URLen samsvarer ikke med de konfigurerte SSR-vertene',\n];\n"
  },
  {
    "path": "lang/nb/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Ny kommentar på siden: :pageName',\n    'new_comment_intro' => 'En bruker har kommentert en side i :appName:',\n    'new_page_subject' => 'Ny side: :pageName',\n    'new_page_intro' => 'En ny side er opprettet i :appName:',\n    'updated_page_subject' => 'Oppdatert side: :pageName',\n    'updated_page_intro' => 'En side er oppdatert i :appName:',\n    'updated_page_debounce' => 'For å forhindre mange varslinger, vil du ikke få nye varslinger for endringer på denne siden fra samme forfatter.',\n    'comment_mention_subject' => 'Du har blitt nevnt i en kommentar på siden: :pageName',\n    'comment_mention_intro' => 'Du har blitt nevnt i en kommentar på :appName:',\n\n    'detail_page_name' => 'Sidenavn:',\n    'detail_page_path' => 'Side bane:',\n    'detail_commenter' => 'Kommentar fra:',\n    'detail_comment' => 'Kommentar:',\n    'detail_created_by' => 'Opprettet av:',\n    'detail_updated_by' => 'Oppdatert av:',\n\n    'action_view_comment' => 'Vis kommentar',\n    'action_view_page' => 'Se side',\n\n    'footer_reason' => 'Denne meldingen ble sendt til deg fordi :link dekker denne typen aktivitet for dette elementet.',\n    'footer_reason_link' => 'dine varslingsinnstillinger',\n];\n"
  },
  {
    "path": "lang/nb/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Forrige',\n    'next'     => 'Neste &raquo;',\n\n];\n"
  },
  {
    "path": "lang/nb/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Passord må inneholde minst åtte tegn og samsvarer med bekreftelsen.',\n    'user' => \"Vi finner ikke en bruker med den e-postadressen.\",\n    'token' => 'Passordet for tilbakestilling av passord er ugyldig for denne e-postadressen.',\n    'sent' => 'Vi har sendt e-postadressen til tilbakestilling av passordet ditt!',\n    'reset' => 'Passordet ditt har blitt tilbakestilt!',\n\n];\n"
  },
  {
    "path": "lang/nb/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Min konto',\n\n    'shortcuts' => 'Snarveier',\n    'shortcuts_interface' => 'Innstillinger for UI snarveier',\n    'shortcuts_toggle_desc' => 'Her kan du aktivere eller deaktivere snarveier for tastatur system som brukes til navigasjon og handlinger.',\n    'shortcuts_customize_desc' => 'Du kan tilpasse hver av snarveiene nedenfor. Trykk på ønsket nøkkelkombinasjon etter å ha valgt inndata for en snarvei.',\n    'shortcuts_toggle_label' => 'Tastatursnarveier aktivert',\n    'shortcuts_section_navigation' => 'Navigasjon',\n    'shortcuts_section_actions' => 'Vanlige handlinger',\n    'shortcuts_save' => 'Lagre snarveier',\n    'shortcuts_overlay_desc' => 'Merk: Når snarveier er aktivert er et hjelperoverlegg tilgjengelig via å trykke \"?\" som vil fremheve de tilgjengelige snarveiene som for øyeblikket er synlige på skjermen.',\n    'shortcuts_update_success' => 'Snarvei innstillinger er oppdatert!',\n    'shortcuts_overview_desc' => 'Behandle tastatursnarveier du kan bruke for å navigere i systembrukergrensesnittet.',\n\n    'notifications' => 'Innstillinger for varsling',\n    'notifications_desc' => 'Kontroller e-postvarslene du mottar når en bestemt aktivitet utføres i systemet.',\n    'notifications_opt_own_page_changes' => 'Varsle ved endringer til sider jeg eier',\n    'notifications_opt_own_page_comments' => 'Varsle om kommentarer på sider jeg eier',\n    'notifications_opt_comment_mentions' => 'Varsle når jeg blir nevnt i en kommentar',\n    'notifications_opt_comment_replies' => 'Varsle ved svar på mine kommentarer',\n    'notifications_save' => 'Lagre innstillinger',\n    'notifications_update_success' => 'Varslingsinnstillingene er oppdatert!',\n    'notifications_watched' => 'Overvåka & ignorerte elementer',\n    'notifications_watched_desc' => 'Nedenfor er elementene som har egendefinerte varslingsinnstillinger i bruk. For å oppdatere innstillingene for disse, se elementet, finn varslingsalternativene i sidepanelet.',\n\n    'auth' => 'Tilgang og sikkerhet',\n    'auth_change_password' => 'Endre passord',\n    'auth_change_password_desc' => 'Endre passordet du bruker for å logge inn med. Dette må være minst 8 tegn langt.',\n    'auth_change_password_success' => 'Passordet har blitt oppdatert!',\n\n    'profile' => 'Profildetaljer',\n    'profile_desc' => 'Gi opplysningene om kontoen din som representerer deg til andre brukere, i tillegg til opplysninger som brukes til kommunikasjon og systempersonalisering.',\n    'profile_view_public' => 'Vis offentlig profil',\n    'profile_name_desc' => 'Tilpass visningsnavn som vil være synlig for andre brukere i systemet ved hjelp av aktiviteten du utfører, og innholdet du eier.',\n    'profile_email_desc' => 'Denne e-posten brukes for varsler, og avhengig av aktiv systemautentisering, systemtilgang.',\n    'profile_email_no_permission' => 'Du har dessverre ikke tillatelse til å endre e-postadressen din. Hvis du ønsker å endre dette, må du be om en administrator om å endre dette for deg.',\n    'profile_avatar_desc' => 'Velg et bilde som skal brukes til å representere deg selv til andre i systemet. Ideelt sett bør dette bildet være kvadratisk og ca. 256 px i bredde og høyde.',\n    'profile_admin_options' => 'Alternativer for administrator',\n    'profile_admin_options_desc' => 'Du finner flere alternativer på administratornivå, som de som skal administrere rolletildelinger, for din brukerkonto i området \"Innstillinger > Brukere\" i applikasjonen.',\n\n    'delete_account' => 'Slett konto',\n    'delete_my_account' => 'Slett kontoen min',\n    'delete_my_account_desc' => 'Dette vil slette din brukerkonto fra systemet. Du vil ikke kunne gjenopprette denne kontoen eller tilbakestille denne handlingen. Innhold du har opprettet, som f. eks. opprettede sider og opplastede bilder, vil forbli uendret.',\n    'delete_my_account_warning' => 'Er du sikker på at du vil slette kontoen din?',\n];\n"
  },
  {
    "path": "lang/nb/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Innstillinger',\n    'settings_save' => 'Lagre innstillinger',\n    'system_version' => 'System versjon',\n    'categories' => 'Kategorier',\n\n    // App Settings\n    'app_customization' => 'Tilpassing',\n    'app_features_security' => 'Funksjoner og sikkerhet',\n    'app_name' => 'Applikasjonsnavn',\n    'app_name_desc' => 'Dette navnet vises i overskriften og i alle e-postmeldinger som sendes av systemet.',\n    'app_name_header' => 'Vis navn i topptekst',\n    'app_public_access' => 'Offentlig tilgang',\n    'app_public_access_desc' => 'Hvis du aktiverer dette alternativet, kan besøkende, som ikke er logget på, få tilgang til innhold i din BookStack-forekomst.',\n    'app_public_access_desc_guest' => 'Tilgang for offentlige besøkende kan kontrolleres gjennom \"Gjest\" -brukeren.',\n    'app_public_access_toggle' => 'Tillat offentlig tilgang',\n    'app_public_viewing' => 'Tillat offentlig visning?',\n    'app_secure_images' => 'Høyere sikkerhet på bildeopplastinger',\n    'app_secure_images_toggle' => 'Enable høyere sikkerhet på bildeopplastinger',\n    'app_secure_images_desc' => 'Av ytelsesgrunner er alle bilder offentlige. Dette alternativet legger til en tilfeldig streng som er vanskelig å gjette foran bildets nettadresser. Forsikre deg om at katalogindekser ikke er aktivert for å forhindre enkel tilgang.',\n    'app_default_editor' => 'Standard sideredigeringsprogram',\n    'app_default_editor_desc' => 'Velg hvilken tekstbehandler som skal brukes som standard når du redigerer nye sider. Dette kan overskrives på et sidenivå der tillatelser tillates.',\n    'app_custom_html' => 'Tilpasset HTML-hodeinnhold',\n    'app_custom_html_desc' => 'Alt innhold som legges til her, blir satt inn i bunnen av <head> -delen på hver side. Dette er praktisk for å overstyre stiler eller legge til analysekode.',\n    'app_custom_html_disabled_notice' => 'Tilpasset HTML-hodeinnhold er deaktivert på denne innstillingssiden for å sikre at eventuelle endringer ødelegger noe, kan tilbakestilles.',\n    'app_logo' => 'Applikasjonslogo',\n    'app_logo_desc' => 'Dette brukes i programtoppfeltet blant andre områder. Dette bildet skal være 86px i høyde. Store bilder vil bli skalert ned.',\n    'app_icon' => 'Applikasjons ikon',\n    'app_icon_desc' => 'Dette ikonet brukes for nettleserfaner og snarveisikoner. Dette bør være et bilde på 256 px kvadrat PNG.',\n    'app_homepage' => 'Applikasjonens hjemmeside',\n    'app_homepage_desc' => 'Velg en visning som skal vises på hjemmesiden i stedet for standardvisningen. Sidetillatelser ignoreres for utvalgte sider.',\n    'app_homepage_select' => 'Velg en side',\n    'app_footer_links' => 'Fotlenker',\n    'app_footer_links_desc' => 'Legg til fotlenker i sidens fotområde. Disse vil vises nederst på de fleste sider, inkludert sider som ikke krever innlogging. Du kan bruke «trans::<key>» etiketter for system-definerte oversettelser. For eksempel: Bruk «trans::common.privacy_policy» for å vise teksten «Personvernregler» og «trans::common.terms_of_service» for å vise teksten «Bruksvilkår».',\n    'app_footer_links_label' => 'Lenketekst',\n    'app_footer_links_url' => 'Lenke',\n    'app_footer_links_add' => 'Legg til fotlenke',\n    'app_disable_comments' => 'Deaktiver kommentarer',\n    'app_disable_comments_toggle' => 'Deaktiver kommentarer',\n    'app_disable_comments_desc' => 'Deaktiver kommentarer på tvers av alle sidene i applikasjonen. <br> Eksisterende kommentarer vises ikke.',\n\n    // Color settings\n    'color_scheme' => 'Applikasjonens farge oppsett',\n    'color_scheme_desc' => 'Sett farger for å bruke i programmets brukergrensesnitt. Farger kan konfigureres separat for mørke og lysmoduser for å passe best inn temaet og sørge for lesbarhet.',\n    'ui_colors_desc' => 'Angi primær farge for programmet og standard link farge. Primær farge brukes hovedsakelig for toppbanner, knapper og grensesnittets dekorasjoner. Standardfargen for koblinger brukes for tekstbaserte lenker og handlinger, både i skriftlig innhold og i programgrensesnittet.',\n    'app_color' => 'Primær farge',\n    'link_color' => 'Standard koblingsfarge',\n    'content_colors_desc' => 'Angi farger for alle elementer i organiseringshierarkiet. Velger du farger med lik lysstyrke til standard farger anbefales for lesbarhet.',\n    'bookshelf_color' => 'Hyllefarge',\n    'book_color' => 'Bokfarge',\n    'chapter_color' => 'Kapittelfarge',\n    'page_color' => 'Sidefarge',\n    'page_draft_color' => 'Sideutkastsfarge',\n\n    // Registration Settings\n    'reg_settings' => 'Registrering',\n    'reg_enable' => 'Tillat registrering',\n    'reg_enable_toggle' => 'Tillat registrering',\n    'reg_enable_desc' => 'Når registrering er aktivert vil brukeren kunne registrere seg som applikasjonsbruker. Ved registrering får de en standard brukerrolle.',\n    'reg_default_role' => 'Standard brukerrolle etter registrering',\n    'reg_enable_external_warning' => 'Alternativet ovenfor ignoreres mens ekstern LDAP- eller SAML-autentisering er aktiv. Brukerkontoer for ikke-eksisterende medlemmer blir automatisk opprettet hvis autentisering mot det eksterne systemet i bruk lykkes.',\n    'reg_email_confirmation' => 'E-postbekreftelse',\n    'reg_email_confirmation_toggle' => 'Krev e-postbekreftelse',\n    'reg_confirm_email_desc' => 'Hvis domenebegrensning brukes, vil e-postbekreftelse være nødvendig, og dette alternativet vil bli ignorert.',\n    'reg_confirm_restrict_domain' => 'Domenebegrensning',\n    'reg_confirm_restrict_domain_desc' => 'Skriv inn en kommaseparert liste over e-postdomener du vil begrense registreringen til. Brukerne vil bli sendt en e-post for å bekrefte adressen deres før de får lov til å kommunisere med applikasjonen. <br> Vær oppmerksom på at brukere vil kunne endre e-postadressene sine etter vellykket registrering.',\n    'reg_confirm_restrict_domain_placeholder' => 'Ingen begrensninger er satt',\n\n    // Sorting Settings\n    'sorting' => 'Lister & Sortering',\n    'sorting_book_default' => 'Standard regel for boksortering',\n    'sorting_book_default_desc' => 'Velg standard sorteringsregelen som skal brukes for nye bøker. Dette vil ikke påvirke eksisterende bøker, og kan overstyres per bok.',\n    'sorting_rules' => 'Sorteringsregler',\n    'sorting_rules_desc' => 'Dette er forhåndsdefinerte sorteringsoperasjoner som kan brukes på innhold i systemet.',\n    'sort_rule_assigned_to_x_books' => 'Tildelt til :count bok|Tildelt til :count bøker',\n    'sort_rule_create' => 'Opprett sorteringsregel',\n    'sort_rule_edit' => 'Rediger sorteringsregel',\n    'sort_rule_delete' => 'Slett sorteringsregel',\n    'sort_rule_delete_desc' => 'Fjern denne sorteringsregelen fra systemet. Bøker som bruker denne sorteringsregelen vil gå tilbake til manuell sortering.',\n    'sort_rule_delete_warn_books' => 'Denne sorteringsregelen brukes for øyeblikket på :count bok/bøker. Er du sikker på at du vil slette denne?',\n    'sort_rule_delete_warn_default' => 'Denne sorteringsregelen brukes for øyeblikket som standard for bøker. Er du sikker på at du vil slette denne?',\n    'sort_rule_details' => 'Detaljer om sorteringsregel',\n    'sort_rule_details_desc' => 'Angi et navn for denne sorteringsregelen, som vil vises i lister når brukerne velger en sorteringsmetode.',\n    'sort_rule_operations' => 'Sorteringsoperasjoner',\n    'sort_rule_operations_desc' => 'Konfigurer sorteringshandlinger ved å flytte dem fra listen over tilgjengelige operasjoner. Ved bruk vil operasjonene bli brukt i rekkefølge, fra topp til bunn. Eventuelle endringer gjort her vil bli brukt for alle tildelte bøker når du lagrer.',\n    'sort_rule_available_operations' => 'Tilgjengelige operasjoner',\n    'sort_rule_available_operations_empty' => 'Ingen gjenværende operasjoner',\n    'sort_rule_configured_operations' => 'Konfigurerte operasjoner',\n    'sort_rule_configured_operations_empty' => 'Dra/legg til operasjoner fra listen \"Tilgjengelige operasjoner\"',\n    'sort_rule_op_asc' => '(Stigende)',\n    'sort_rule_op_desc' => '(Synkende)',\n    'sort_rule_op_name' => 'Navn - Alfabetisk',\n    'sort_rule_op_name_numeric' => 'Navn - Numerisk',\n    'sort_rule_op_created_date' => 'Dato opprettet',\n    'sort_rule_op_updated_date' => 'Dato oppdatert',\n    'sort_rule_op_chapters_first' => 'Kapitler først',\n    'sort_rule_op_chapters_last' => 'Kapitler sist',\n    'sorting_page_limits' => 'Visningsgrenser for hver side',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Vedlikehold',\n    'maint_image_cleanup' => 'Bildeopprydding',\n    'maint_image_cleanup_desc' => 'Skanner side og revisjonsinnhold for å sjekke hvilke bilder og tegninger som for øyeblikket er i bruk, og hvilke bilder som er overflødige. Forsikre deg om at du lager en full database og sikkerhetskopiering av bilder før du kjører denne.',\n    'maint_delete_images_only_in_revisions' => 'Slett også bilder som bare finnes i game siderevisjoner',\n    'maint_image_cleanup_run' => 'Kjør opprydding',\n    'maint_image_cleanup_warning' => ':count potensielt ubrukte bilder ble funnet. Er du sikker på at du vil slette disse bildene?',\n    'maint_image_cleanup_success' => ':count potensielt ubrukte bilder funnet og slettet!',\n    'maint_image_cleanup_nothing_found' => 'Ingen ubrukte bilder funnet, ingenting slettet!',\n    'maint_send_test_email' => 'Send en test-e-post',\n    'maint_send_test_email_desc' => 'Dette sender en test-e-post til din e-postadresse som er angitt i profilen din.',\n    'maint_send_test_email_run' => 'Send en test-e-post',\n    'maint_send_test_email_success' => 'Send en test-e-post til :address',\n    'maint_send_test_email_mail_subject' => 'Test-e-post',\n    'maint_send_test_email_mail_greeting' => 'E-postsending ser ut til å fungere!',\n    'maint_send_test_email_mail_text' => 'Gratulerer! Da du mottok dette e-postvarselet, ser det ut til at e-postinnstillingene dine er konfigurert riktig.',\n    'maint_recycle_bin_desc' => 'Slettede hyller, bøker, kapitler og sider kastes i papirkurven så de kan bli gjenopprettet eller slettet permanent. Eldre utgaver i papirkurven kan slettes automatisk etter en stund, avhengig av systemkonfigurasjonen.',\n    'maint_recycle_bin_open' => 'Åpne papirkurven',\n    'maint_regen_references' => 'Regenerer referanser',\n    'maint_regen_references_desc' => 'Denne handlingen gjenoppbygger referanseindeksen for krysselement i databasen. Dette håndteres vanligvis automatisk, men denne handlingen kan være nyttig for å indeksere gammelt innhold eller innhold lagt til via uoffisielle metoder.',\n    'maint_regen_references_success' => 'Referanseindeksen har blitt regenerert!',\n    'maint_timeout_command_note' => 'Merk: Denne handlingen kan ta tid å kjøre, noe som kan føre til tidsavbruddsmessige problemer i noen webomgivelser. Dette gjøres som et alternativ ved hjelp av en terminalkommando.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Papirkurven',\n    'recycle_bin_desc' => 'Her kan du gjenopprette ting du har kastet i papirkurven eller velge å slette dem permanent fra systemet. Denne listen er ikke filtrert i motsetning til lignende lister i systemet hvor tilgangskontroll overholdes.',\n    'recycle_bin_deleted_item' => 'Kastet element',\n    'recycle_bin_deleted_parent' => 'Overordnet',\n    'recycle_bin_deleted_by' => 'Kastet av',\n    'recycle_bin_deleted_at' => 'Kastet den',\n    'recycle_bin_permanently_delete' => 'Slett permanent',\n    'recycle_bin_restore' => 'Gjenopprett',\n    'recycle_bin_contents_empty' => 'Papirkurven er for øyeblikket tom',\n    'recycle_bin_empty' => 'Tøm papirkurven',\n    'recycle_bin_empty_confirm' => 'Dette vil slette alle elementene i papirkurven permanent. Dette inkluderer innhold i hvert element. Er du sikker på at du vil tømme papirkurven?',\n    'recycle_bin_destroy_confirm' => 'Denne handlingen vil slette dette elementet permanent fra systemet, sammen med alle underelementer listet nedenfor, og du vil ikke kunne gjenopprette dette innholdet. Er du sikker på at du vil slette dette permanent?',\n    'recycle_bin_destroy_list' => 'Elementer som skal slettes',\n    'recycle_bin_restore_list' => 'Elementer som skal gjenopprettes',\n    'recycle_bin_restore_confirm' => 'Denne handlingen vil hente opp elementet fra papirkurven, inkludert underliggende innhold, til sin opprinnelige sted. Om den opprinnelige plassen har blitt slettet i mellomtiden og nå befinner seg i papirkurven, vil også dette bli hentet opp igjen.',\n    'recycle_bin_restore_deleted_parent' => 'Det overordnede elementet var også kastet i papirkurven. Disse elementene vil forbli kastet inntil det overordnede også hentes opp igjen.',\n    'recycle_bin_restore_parent' => 'Gjenopprett overodnet',\n    'recycle_bin_destroy_notification' => 'Slettet :count elementer fra papirkurven.',\n    'recycle_bin_restore_notification' => 'Gjenopprettet :count elementer fra papirkurven.',\n\n    // Audit Log\n    'audit' => 'Revisjonslogg',\n    'audit_desc' => 'Denne revisjonsloggen viser en liste over aktiviteter som spores i systemet. Denne listen er ufiltrert i motsetning til lignende aktivitetslister i systemet der tillatelsesfiltre brukes.',\n    'audit_event_filter' => 'Hendelsesfilter',\n    'audit_event_filter_no_filter' => 'Ingen filter',\n    'audit_deleted_item' => 'Slettet ting',\n    'audit_deleted_item_name' => 'Navn: :name',\n    'audit_table_user' => 'Kontoholder',\n    'audit_table_event' => 'Hendelse',\n    'audit_table_related' => 'Relaterte elementer eller detaljer',\n    'audit_table_ip' => 'IP Adresse',\n    'audit_table_date' => 'Aktivitetsdato',\n    'audit_date_from' => 'Datoperiode fra',\n    'audit_date_to' => 'Datoperiode til',\n\n    // Role Settings\n    'roles' => 'Roller',\n    'role_user_roles' => 'Kontoroller',\n    'roles_index_desc' => 'Roller brukes til å gruppere brukere og gi systemtilgang til medlemmene. Når en bruker er medlem av flere roller, vil de tildelte rettighetene samles inn, og brukeren vil arve alle evner.',\n    'roles_x_users_assigned' => ':count bruker tildelt|:count brukere tildelt',\n    'roles_x_permissions_provided' => ':count tillatelse|:count tillatelser',\n    'roles_assigned_users' => 'Tilordnede brukere',\n    'roles_permissions_provided' => 'Tilbudte rettigheter',\n    'role_create' => 'Opprett ny rolle',\n    'role_delete' => 'Slett rolle',\n    'role_delete_confirm' => 'Dette vil slette rollen «:roleName».',\n    'role_delete_users_assigned' => 'Denne rollen har :userCount kontoer koblet opp mot seg. Velg hvilke rolle du vil flytte disse til.',\n    'role_delete_no_migration' => \"Ikke flytt kontoer\",\n    'role_delete_sure' => 'Er du sikker på at du vil slette rollen?',\n    'role_edit' => 'Endre rolle',\n    'role_details' => 'Rolledetaljer',\n    'role_name' => 'Rollenavn',\n    'role_desc' => 'Kort beskrivelse av rolle',\n    'role_mfa_enforced' => 'Krever flerfaktorautentisering',\n    'role_external_auth_id' => 'Ekstern godkjennings-ID',\n    'role_system' => 'Systemtilganger',\n    'role_manage_users' => 'Behandle kontoer',\n    'role_manage_roles' => 'Behandle roller og rolletilganger',\n    'role_manage_entity_permissions' => 'Behandle bok-, kapittel- og sidetilganger',\n    'role_manage_own_entity_permissions' => 'Behandle tilganger på egne verk',\n    'role_manage_page_templates' => 'Behandle sidemaler',\n    'role_access_api' => 'Systemtilgang API',\n    'role_manage_settings' => 'Behandle applikasjonsinnstillinger',\n    'role_export_content' => 'Eksporter innhold',\n    'role_import_content' => 'Import innhold',\n    'role_editor_change' => 'Endre sideredigering',\n    'role_notifications' => 'Motta og administrere varslinger',\n    'role_permission_note_users_and_roles' => 'Disse tillatelsene vil teknisk sett også gi mulighet til å se & søke etter brukere & roller i systemet.',\n    'role_asset' => 'Eiendomstillatelser',\n    'roles_system_warning' => 'Vær oppmerksom på at tilgang til noen av de ovennevnte tre tillatelsene kan tillate en bruker å endre sine egne rettigheter eller rettighetene til andre i systemet. Bare tildel roller med disse tillatelsene til pålitelige brukere.',\n    'role_asset_desc' => 'Disse tillatelsene kontrollerer standard tilgang til eiendelene i systemet. Tillatelser til bøker, kapitler og sider overstyrer disse tillatelsene.',\n    'role_asset_admins' => 'Administratorer får automatisk tilgang til alt innhold, men disse alternativene kan vise eller skjule UI-alternativer.',\n    'role_asset_image_view_note' => 'Dette gjelder synlighet innenfor bilde-administrasjonen. Faktisk tilgang på opplastede bildefiler vil være avhengig av valget for systemlagring av bildet.',\n    'role_asset_users_note' => 'Disse tillatelsene vil teknisk sett også gi mulighet til å se & søke etter brukere i systemet.',\n    'role_all' => 'Alle',\n    'role_own' => 'Egne',\n    'role_controlled_by_asset' => 'Kontrollert av eiendelen de er lastet opp til',\n    'role_save' => 'Lagre rolle',\n    'role_users' => 'Kontoholdere med denne rollen',\n    'role_users_none' => 'Ingen kontoholdere er gitt denne rollen',\n\n    // Users\n    'users' => 'Brukere',\n    'users_index_desc' => 'Opprett og administrer individuelle brukerkontoer innenfor systemet. Brukerkontoer brukes for innlogging og navngivelse av innhold og aktivitet. Tilgangstillatelser er primært rollebasert, men brukerinnhold eierskap, blant andre faktorer, kan også påvirke tillatelser og tilgang.',\n    'user_profile' => 'Profil',\n    'users_add_new' => 'Register ny konto',\n    'users_search' => 'Søk i kontoer',\n    'users_latest_activity' => 'Siste aktivitet',\n    'users_details' => 'Kontodetaljer',\n    'users_details_desc' => 'Angi et visningsnavn og en e-postadresse for denne kontoholderen. E-postadressen vil bli brukt til å logge på applikasjonen.',\n    'users_details_desc_no_email' => 'Angi et visningsnavn for denne kontoholderen slik at andre kan gjenkjenne dem.',\n    'users_role' => 'Roller',\n    'users_role_desc' => 'Velg hvilke roller denne kontoholderen vil bli tildelt. Hvis en kontoholderen er tildelt flere roller, vil tillatelsene fra disse rollene stable seg, og de vil motta alle evnene til de tildelte rollene.',\n    'users_password' => 'Passord',\n    'users_password_desc' => 'Angi et passord som brukes til å logge inn til programmet. Dette må være minst 8 tegn langt.',\n    'users_send_invite_text' => 'Du kan velge å sende denne kontoholderen en invitasjons-e-post som lar dem angi sitt eget passord, ellers kan du selv angi passordet.',\n    'users_send_invite_option' => 'Send invitasjonsmelding',\n    'users_external_auth_id' => 'Ekstern godkjennings-ID',\n    'users_external_auth_id_desc' => 'Når et eksternt autentiseringssystem er i bruk (som SAML2, OIDC eller LDAP) er dette er ID-en som kobles til denne Bookstack-brukeren til autentiseringssystemkontoen. Du kan ignorere dette feltet hvis du bruker standard e-postbasert autentisering.',\n    'users_password_warning' => 'Fyll bare ut nedenfor hvis du vil endre passordet for denne brukeren.',\n    'users_system_public' => 'Denne brukeren representerer alle gjester som besøker appliaksjonen din. Den kan ikke brukes til å logge på, men tildeles automatisk.',\n    'users_delete' => 'Slett konto',\n    'users_delete_named' => 'Slett kontoen :userName',\n    'users_delete_warning' => 'Dette vil fullstendig slette denne brukeren med navnet «:userName» fra systemet.',\n    'users_delete_confirm' => 'Er du sikker på at du vil slette denne kontoen?',\n    'users_migrate_ownership' => 'Overfør eierskap',\n    'users_migrate_ownership_desc' => 'Velg en bruker her, som du ønsker skal ta eierskap over alle elementene som er eid av denne brukeren.',\n    'users_none_selected' => 'Ingen bruker valgt',\n    'users_edit' => 'Rediger konto',\n    'users_edit_profile' => 'Rediger profil',\n    'users_avatar' => 'Kontobilde',\n    'users_avatar_desc' => 'Velg et bilde for å representere denne kontoholderen. Dette skal være omtrent 256px kvadrat.',\n    'users_preferred_language' => 'Foretrukket språk',\n    'users_preferred_language_desc' => 'Dette alternativet vil endre språket som brukes til brukergrensesnittet til applikasjonen. Dette påvirker ikke noe brukeropprettet innhold.',\n    'users_social_accounts' => 'Sosiale kontoer',\n    'users_social_accounts_desc' => 'Vis status for de tilkoblede sosiale kontoene for denne brukeren. Sosiale kontoer kan brukes i tillegg til det primære autentiseringssystemet for systemtilgang.',\n    'users_social_accounts_info' => 'Her kan du koble andre kontoer for raskere og enklere pålogging. Hvis du frakobler en konto her, tilbakekaller ikke dette tidligere autorisert tilgang. Tilbakekall tilgang fra profilinnstillingene dine på den tilkoblede sosiale kontoen.',\n    'users_social_connect' => 'Koble til konto',\n    'users_social_disconnect' => 'Koble fra konto',\n    'users_social_status_connected' => 'Tilkoblet',\n    'users_social_status_disconnected' => 'Frakoblet',\n    'users_social_connected' => ':socialAccount ble lagt til din konto.',\n    'users_social_disconnected' => ':socialAccount ble koblet fra din konto.',\n    'users_api_tokens' => 'API-nøkler',\n    'users_api_tokens_desc' => 'Opprett og håndter tilgangstokener som brukes til å godkjenne med BookStack REST API. Tillatelser til API blir administrert via brukeren som tokenet tilhører.',\n    'users_api_tokens_none' => 'Ingen API-nøkler finnes for denne kontoen',\n    'users_api_tokens_create' => 'Opprett nøkkel',\n    'users_api_tokens_expires' => 'Utløper',\n    'users_api_tokens_docs' => 'API-dokumentasjon',\n    'users_mfa' => 'Flerfaktorautentisering',\n    'users_mfa_desc' => 'Konfigurer flerfaktorautentisering som et ekstra lag med sikkerhet for din konto.',\n    'users_mfa_x_methods' => ':count metode konfigurert|:count metoder konfigurert',\n    'users_mfa_configure' => 'Konfigurer metoder',\n\n    // API Tokens\n    'user_api_token_create' => 'Opprett API-nøkkel',\n    'user_api_token_name' => 'Navn',\n    'user_api_token_name_desc' => 'Gi nøkkelen et lesbart navn som en fremtidig påminnelse om det tiltenkte formålet.',\n    'user_api_token_expiry' => 'Utløpsdato',\n    'user_api_token_expiry_desc' => 'Angi en dato da denne nøkkelen utløper. Etter denne datoen vil forespørsler som er gjort med denne nøkkelen ikke lenger fungere. Å la dette feltet stå tomt vil sette utløpsdato 100 år inn i fremtiden.',\n    'user_api_token_create_secret_message' => 'Umiddelbart etter å ha opprettet denne nøkkelen vil en identifikator og hemmelighet bli generert og vist. Hemmeligheten vil bare vises en gang, så husk å kopiere verdien til et trygt sted før du fortsetter.',\n    'user_api_token' => 'API-nøkkel',\n    'user_api_token_id' => 'Identifikator',\n    'user_api_token_id_desc' => 'Dette er en ikke-redigerbar systemgenerert identifikator for denne nøkkelen som må oppgis i API-forespørsler.',\n    'user_api_token_secret' => 'Hemmelighet',\n    'user_api_token_secret_desc' => 'Dette er en systemgenerert hemmelighet for denne nøkkelen som må leveres i API-forespørsler. Dette vises bare denne gangen, så kopier denne verdien til et trygt sted.',\n    'user_api_token_created' => 'Nøkkel opprettet :timeAgo',\n    'user_api_token_updated' => 'Nøkkel oppdatert :timeAgo',\n    'user_api_token_delete' => 'Slett nøkkel',\n    'user_api_token_delete_warning' => 'Dette vil slette API-nøkkelen \\':tokenName\\' fra systemet.',\n    'user_api_token_delete_confirm' => 'Sikker på at du vil slette nøkkelen?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhooks er en måte å sende data til eksterne nettadresser når bestemte handlinger og hendelser oppstår i systemet som gjør det mulig å integrer med eksterne plattformer som meldingssystemer eller varslingssystemer.',\n    'webhooks_x_trigger_events' => ':count utløsende hendelse:count utløsende hendelser',\n    'webhooks_create' => 'Lag ny Webhook',\n    'webhooks_none_created' => 'Ingen webhooks er opprettet ennå.',\n    'webhooks_edit' => 'Rediger webhook',\n    'webhooks_save' => 'Lagre Webhook',\n    'webhooks_details' => 'Webhook detaljer',\n    'webhooks_details_desc' => 'Gi et brukervennlig navn og et POST endepunkt som et sted der webhook-dataene skal sendes til.',\n    'webhooks_events' => 'Webhook hendelser',\n    'webhooks_events_desc' => 'Velg alle hendelsene som skal utløse denne webhook som skal kalles.',\n    'webhooks_events_warning' => 'Husk at disse hendelsene vil bli utløst for alle valgte hendelser, selv om egendefinerte tillatelser brukes. Pass på at bruk av denne webhooken ikke vil utsette konfidensiell innhold.',\n    'webhooks_events_all' => 'Alle systemhendelser',\n    'webhooks_name' => 'Webhook navn',\n    'webhooks_timeout' => 'Tidsavbrudd for Webhook forespørsler (sekunder)',\n    'webhooks_endpoint' => 'Webhook endepunkt',\n    'webhooks_active' => 'Webhook aktiv',\n    'webhook_events_table_header' => 'Hendelser',\n    'webhooks_delete' => 'Slett webhook',\n    'webhooks_delete_warning' => 'Dette vil slette webhook, med navnet \\':webhookName\\', fra systemet.',\n    'webhooks_delete_confirm' => 'Er du sikker på at du vil slette denne webhooken?',\n    'webhooks_format_example' => 'Webhook formattering eksempel',\n    'webhooks_format_example_desc' => 'Webhook-data sendes som en POST-forespørsel til det konfigurerte endepunktet som JSON ved hjelp av formatet nedenfor. «related_item» og «url» egenskaper er valgfrie og vil avhenge av hvilken type hendelse som utløses.',\n    'webhooks_status' => 'Webhook status',\n    'webhooks_last_called' => 'Sist ringt:',\n    'webhooks_last_errored' => 'Siste feil:',\n    'webhooks_last_error_message' => 'Siste feilmelding:',\n\n    // Licensing\n    'licenses' => 'Lisenser',\n    'licenses_desc' => 'Denne siden detaljerer lisensinformasjonen for BookStack, i tillegg til prosjektene & bibliotekene som brukes i BookStack. Mange av de oppførte prosjektene kan bare brukes i utviklingssammenheng.',\n    'licenses_bookstack' => 'BookStack lisens',\n    'licenses_php' => 'PHP Bibliotek lisenser',\n    'licenses_js' => 'JavaScript bibliotek-lisenser',\n    'licenses_other' => 'Andre lisenser',\n    'license_details' => 'Lisens detaljer',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/nb/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute må aksepteres.',\n    'active_url'           => ':attribute er ikke en godkjent URL.',\n    'after'                => ':attribute må være en dato etter :date.',\n    'alpha'                => ':attribute kan kun inneholde bokstaver.',\n    'alpha_dash'           => ':attribute kan kunne inneholde bokstaver, tall, bindestreker eller understreker.',\n    'alpha_num'            => ':attribute kan kun inneholde bokstaver og tall.',\n    'array'                => ':attribute må være en liste.',\n    'backup_codes'         => 'Den angitte koden er ikke gyldig, eller er allerede benyttet.',\n    'before'               => ':attribute må være en dato før :date.',\n    'between'              => [\n        'numeric' => ':attribute må være mellom :min og :max.',\n        'file'    => ':attribute må være mellom :min og :max kilobytes.',\n        'string'  => ':attribute må være mellom :min og :max tegn.',\n        'array'   => ':attribute må være mellom :min og :max ting.',\n    ],\n    'boolean'              => ':attribute feltet kan bare være sann eller falsk.',\n    'confirmed'            => ':attribute bekreftelsen samsvarer ikke.',\n    'date'                 => ':attribute er ikke en gyldig dato.',\n    'date_format'          => ':attribute samsvarer ikke med :format.',\n    'different'            => ':attribute og :other må være forskjellige.',\n    'digits'               => ':attribute må være :digits tall.',\n    'digits_between'       => ':attribute må være mellomg :min og :max tall.',\n    'email'                => ':attribute må være en gyldig e-post.',\n    'ends_with' => ':attribute må slutte med en av verdiene: :values',\n    'file'                 => 'Attributtet :attribute må angis som en gyldig fil.',\n    'filled'               => ':attribute feltet er påkrevd.',\n    'gt'                   => [\n        'numeric' => ':attribute må være større enn :value.',\n        'file'    => ':attribute må være større enn :value kilobytes.',\n        'string'  => ':attribute må være større enn :value tegn.',\n        'array'   => ':attribute må ha mer en :value ting.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute må være større enn eller lik :value.',\n        'file'    => ':attribute må være større enn eller lik :value kilobytes.',\n        'string'  => ':attribute må være større enn eller lik :value tegn.',\n        'array'   => ':attribute må ha :value eller flere ting.',\n    ],\n    'exists'               => 'Den valgte :attribute er ugyldig.',\n    'image'                => ':attribute må være et bilde.',\n    'image_extension'      => ':attribute må ha støttet formattype.',\n    'in'                   => 'Den valgte :attribute er ugyldig.',\n    'integer'              => ':attribute må være et heltall',\n    'ip'                   => ':attribute må være en gyldig IP adresse.',\n    'ipv4'                 => ':attribute må være en gyldig IPv4 adresse.',\n    'ipv6'                 => ':attribute må være en gyldig IPv6 adresse.',\n    'json'                 => ':attribute må være en gyldig JSON tekststreng.',\n    'lt'                   => [\n        'numeric' => ':attribute må være mindre enn :value.',\n        'file'    => ':attribute må være mindre enn :value kilobytes.',\n        'string'  => ':attribute må være mindre enn :value tegn.',\n        'array'   => ':attribute må ha mindre enn :value ting.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute må være mindre enn eller lik :value.',\n        'file'    => ':attribute må være mindre enn eller lik :value kilobytes.',\n        'string'  => ':attribute må være mindre enn eller lik :value characters.',\n        'array'   => ':attribute må ha mindre enn eller lik :value ting.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute kan ikke være større enn :max.',\n        'file'    => ':attribute kan ikke være større enn :max kilobytes.',\n        'string'  => ':attribute kan ikke være større enn :max tegn.',\n        'array'   => ':attribute kan ikke inneholde mer enn :max ting.',\n    ],\n    'mimes'                => ':attribute må være en fil av typen: :values.',\n    'min'                  => [\n        'numeric' => ':attribute må være på minst :min.',\n        'file'    => ':attribute må være på minst :min kilobytes.',\n        'string'  => ':attribute må være på minst :min tegn.',\n        'array'   => ':attribute må minst ha :min ting.',\n    ],\n    'not_in'               => 'Den valgte :attribute er ugyldig.',\n    'not_regex'            => ':attribute format er ugyldig.',\n    'numeric'              => ':attribute må være et nummer.',\n    'regex'                => ':attribute format er ugyldig.',\n    'required'             => ':attribute feltet er påkrevt.',\n    'required_if'          => ':attribute feltet er påkrevt når :other er :value.',\n    'required_with'        => ':attribute feltet er påkrevt når :values er tilgjengelig.',\n    'required_with_all'    => ':attribute feltet er påkrevt når :values er tilgjengelig',\n    'required_without'     => ':attribute feltet er påkrevt når :values ikke er tilgjengelig.',\n    'required_without_all' => ':attribute feltet er påkrevt når ingen av :values er tilgjengelig.',\n    'same'                 => ':attribute og :other må samsvare.',\n    'safe_url'             => 'Den angitte lenken kan være farlig.',\n    'size'                 => [\n        'numeric' => ':attribute må være :size.',\n        'file'    => ':attribute må være :size kilobytes.',\n        'string'  => ':attribute må være :size tegn.',\n        'array'   => ':attribute må inneholde :size ting.',\n    ],\n    'string'               => ':attribute må være en tekststreng.',\n    'timezone'             => ':attribute må være en tidssone.',\n    'totp'                 => 'Den angitte koden er ikke gyldig eller har utløpt.',\n    'unique'               => ':attribute har allerede blitt tatt.',\n    'url'                  => ':attribute format er ugyldig.',\n    'uploaded'             => 'kunne ikke lastes opp, tjeneren støtter ikke filer av denne størrelsen.',\n\n    'zip_file' => 'Attributtet :attribute må henvises til en fil i ZIP.',\n    'zip_file_size' => 'Filen :attribute må ikke overstige :size MB.',\n    'zip_file_mime' => 'Attributtet :attribute må referere en fil av typen :validTypes, som ble funnet :foundType.',\n    'zip_model_expected' => 'Data objekt forventet, men \":type\" funnet.',\n    'zip_unique' => 'Attributtet :attribute må være unikt for objekttypen i ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'passordbekreftelse er påkrevd',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/ne/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'पाना सिर्जना गरियो',\n    'page_create_notification'    => 'पाना सफलतापूर्वक सिर्जना गरियो',\n    'page_update'                 => 'पाना अद्यावधिक गरियो',\n    'page_update_notification'    => 'पाना सफलतापूर्वक अद्यावधिक गरियो',\n    'page_delete'                 => 'पाना हटाइयो',\n    'page_delete_notification'    => 'पाना सफलतापूर्वक हटाइयो',\n    'page_restore'                => 'पाना पुनर्स्थापित गरियो',\n    'page_restore_notification'   => 'पाना सफलतापूर्वक पुनर्स्थापित गरियो',\n    'page_move'                   => 'पाना सारियो',\n    'page_move_notification'      => 'पाना सफलतापूर्वक सारियो',\n\n    // Chapters\n    'chapter_create'              => 'अध्याय सिर्जना गरियो',\n    'chapter_create_notification' => 'अध्याय सफलतापूर्वक सिर्जना गरियो',\n    'chapter_update'              => 'अध्याय अद्यावधिक गरियो',\n    'chapter_update_notification' => 'अध्याय सफलतापूर्वक अद्यावधिक गरियो',\n    'chapter_delete'              => 'अध्याय हटाइयो',\n    'chapter_delete_notification' => 'अध्याय सफलतापूर्वक हटाइयो',\n    'chapter_move'                => 'अध्याय सारियो',\n    'chapter_move_notification' => 'अध्याय सफलतापूर्वक सारियो',\n\n    // Books\n    'book_create'                 => 'पुस्तक सिर्जना गरियो',\n    'book_create_notification'    => 'पुस्तक सफलतापूर्वक सिर्जना गरियो',\n    'book_create_from_chapter'              => 'अध्यायलाई पुस्तकमा परिणत गरियो',\n    'book_create_from_chapter_notification' => 'अध्यायलाई पुस्तकमा सफलतापूर्वक परिणत गरियो',\n    'book_update'                 => 'पुस्तक अद्यावधिक गरियो',\n    'book_update_notification'    => 'पुस्तक सफलतापूर्वक अद्यावधिक गरियो',\n    'book_delete'                 => 'पुस्तक हटाइयो',\n    'book_delete_notification'    => 'पुस्तक सफलतापूर्वक हटाइयो',\n    'book_sort'                   => 'पुस्तक क्रमबद्ध गरियो',\n    'book_sort_notification'      => 'पुस्तक सफलतापूर्वक क्रमबद्ध गरियो',\n\n    // Bookshelves\n    'bookshelf_create'            => 'दराज बनाइयो',\n    'bookshelf_create_notification'    => 'दराज सफलतापूर्वक बनाइयो',\n    'bookshelf_create_from_book'    => 'पुस्तकलाई दराजमा परिणत गरियो',\n    'bookshelf_create_from_book_notification'    => 'पुस्तकलाई दराजमा सफलतापूर्वक परिणत गरियो',\n    'bookshelf_update'                 => 'दराज अद्यावधिक गरियो',\n    'bookshelf_update_notification'    => 'दराज सफलतापूर्वक अद्यावधिक गरियो',\n    'bookshelf_delete'                 => 'दराज हटाइयो',\n    'bookshelf_delete_notification'    => 'दराज सफलतापूर्वक हटाइयो',\n\n    // Revisions\n    'revision_restore' => 'संशोधन पुनर्स्थापित गरियो',\n    'revision_delete' => 'संशोधन हटाइयो',\n    'revision_delete_notification' => 'संशोधन सफलतापूर्वक हटाइयो',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" तपाईंको मनपर्नेमा थपिएको छ',\n    'favourite_remove_notification' => '\":name\" तपाईंको मनपर्नेबाट हटाइएको छ',\n\n    // Watching\n    'watch_update_level_notification' => 'हेर्ने अभिरुचि सफलतापूर्वक अद्यावधिक गरियो',\n\n    // Auth\n    'auth_login' => 'लग इन भयो',\n    'auth_register' => 'नयाँ प्रयोगकर्ता रूपमा दर्ता गरियो',\n    'auth_password_reset_request' => 'प्रयोगकर्ताको पासवर्ड रिसेटको अनुरोध गरियो',\n    'auth_password_reset_update' => 'प्रयोगकर्ता पासवर्ड रिसेट गर्नुहोस्',\n    'mfa_setup_method' => 'MFA विधि सेटअप गरियो',\n    'mfa_setup_method_notification' => 'बहु-कारक प्रमाणीकरण विधि सफलतापूर्वक सेटअप गरियो',\n    'mfa_remove_method' => 'MFA हटाइयो',\n    'mfa_remove_method_notification' => 'बहु-कारक प्रमाणीकरण विधि सफलतापूर्वक हटाइयो',\n\n    // Settings\n    'settings_update' => 'सेटिङहरू अद्यावधिक गरियो',\n    'settings_update_notification' => 'सेटिङहरू सफलतापूर्वक अद्यावधिक गरियो',\n    'maintenance_action_run' => 'मर्मत कार्य सञ्चालन गरियो',\n\n    // Webhooks\n    'webhook_create' => 'वेबहुक सिर्जना गरियो',\n    'webhook_create_notification' => 'वेबहुक सफलतापूर्वक सिर्जना गरियो',\n    'webhook_update' => 'वेबहुकअद्यावधिक गरियो',\n    'webhook_update_notification' => 'वेबहुक सफलतापूर्वक अद्यावधिक गरियो',\n    'webhook_delete' => 'वेबहुक हटाइयो',\n    'webhook_delete_notification' => 'वेबहुक सफलतापूर्वक हटाइयो',\n\n    // Imports\n    'import_create' => 'आयात सिर्जना गरियो',\n    'import_create_notification' => 'आयात सफलतापूर्वक अपलोड गरियो',\n    'import_run' => 'आयात अद्यावधिक गरियो',\n    'import_run_notification' => 'सामग्री सफलतापूर्वक आयात गरियो',\n    'import_delete' => 'आयात हटाइयो',\n    'import_delete_notification' => 'आयात सफलतापूर्वक हटाइयो',\n\n    // Users\n    'user_create' => 'प्रयोगकर्ता सिर्जना गरियो',\n    'user_create_notification' => 'प्रयोगकर्ता सफलतापूर्वक सिर्जना गरियो',\n    'user_update' => 'प्रयोगकर्ता अद्यावधिक गरियो',\n    'user_update_notification' => 'प्रयोगकर्ता सफलतापूर्वक अद्यावधिक गरियो',\n    'user_delete' => 'प्रयोगकर्ता हटाइयो',\n    'user_delete_notification' => 'प्रयोगकर्ता सफलतापूर्वक हटाइयो',\n\n    // API Tokens\n    'api_token_create' => 'API टोकन सिर्जना गरियो',\n    'api_token_create_notification' => 'API टोकन सफलतापूर्वक सिर्जना गरियो',\n    'api_token_update' => 'API टोकन अद्यावधिक गरियो',\n    'api_token_update_notification' => 'API टोकन सफलतापूर्वक अद्यावधिक गरियो',\n    'api_token_delete' => 'API टोकन हटाइयो',\n    'api_token_delete_notification' => 'API टोकन सफलतापूर्वक हटाइयो',\n\n    // Roles\n    'role_create' => 'भूमिका सिर्जना गरियो',\n    'role_create_notification' => 'भूमिका सफलतापूर्वक सिर्जना गरियो',\n    'role_update' => 'भूमिका अद्यावधिक गरियो',\n    'role_update_notification' => 'भूमिका सफलतापूर्वक अद्यावधिक गरियो',\n    'role_delete' => 'भूमिका हटाइयो',\n    'role_delete_notification' => 'भूमिका सफलतापूर्वक हटाइयो',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'पुनः प्रयोगको डब्बा खाली गरियो',\n    'recycle_bin_restore' => 'पुनः प्रयोगको डब्बाबाट पुनर्स्थापित गरियो',\n    'recycle_bin_destroy' => 'पुनः प्रयोगको डब्बाबाट हटाइयो',\n\n    // Comments\n    'commented_on'                => 'मा टिप्पणी गरियो',\n    'comment_create'              => 'टिप्पणी थपियो',\n    'comment_update'              => 'टिप्पणी अद्यावधिक गरियो',\n    'comment_delete'              => 'टिप्पणी मेटाइयो',\n\n    // Sort Rules\n    'sort_rule_create' => 'क्रम नियम सिर्जना गरियो',\n    'sort_rule_create_notification' => 'क्रम नियम सफलतापूर्वक सिर्जना गरियो',\n    'sort_rule_update' => 'क्रम नियम अद्यावधिक गरियो',\n    'sort_rule_update_notification' => 'क्रम नियम सफलतापूर्वक अद्यावधिक गरियो',\n    'sort_rule_delete' => 'क्रम नियम हटाइयो',\n    'sort_rule_delete_notification' => 'क्रम नियम सफलतापूर्वक हटाइयो',\n\n    // Other\n    'permissions_update'          => 'अनुमतिहरू अद्यावधिक गरियो',\n];\n"
  },
  {
    "path": "lang/ne/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'यी प्रमाणिकरण जानकारी हाम्रो अभिलेखसँग मेल खाँदैन।',\n    'throttle' => 'लगइन प्रयासहरूको संख्या धेरै भएको छ। कृपया :seconds सेकेन्ड पछि पुनः प्रयास गर्नुहोस्।',\n\n    // Login & Register\n    'sign_up' => 'साइन अप गर्नुहोस्',\n    'log_in' => 'लग इन गर्नुहोस्',\n    'log_in_with' => ':socialDriver मार्फत लगइन गर्नुहोस्',\n    'sign_up_with' => ':socialDriver प्रयोग गरेर साइन अप गर्नुहोस्',\n    'logout' => 'लगआउट',\n\n    'name' => 'नाम',\n    'username' => 'प्रयोगकर्ता नाम',\n    'email' => 'ईमेल',\n    'password' => 'पासवर्ड',\n    'password_confirm' => 'पासवर्ड पक्का गर्नुहोस्',\n    'password_hint' => 'कम्तिमा 8 अङ्कको हुनुपर्छ',\n    'forgot_password' => 'पासवर्ड भुल्नुभयो?',\n    'remember_me' => 'मलाई सम्झनुहोस्',\n    'ldap_email_hint' => 'कृपया यस खाताको लागि प्रयोग गर्नको लागि इमेल प्रविष्ट गर्नुहोस्।',\n    'create_account' => 'खाता बनाउनुहोस्',\n    'already_have_account' => 'तपाईंको पहिले नै खाता छ?',\n    'dont_have_account' => 'के तपाईंको खाता छैन?',\n    'social_login' => 'सामाजिक लगइन',\n    'social_registration' => 'सामाजिक दर्ता',\n    'social_registration_text' => 'अर्को सेवाबाट दर्ता गर्नुहोस् र लगइन गर्नुहोस्।',\n\n    'register_thanks' => 'दर्ता गर्नुभएकोमा धन्यवाद!',\n    'register_confirm' => 'कृपया तपाईंको इमेल जाँच गर्नुहोस् र :appName मा पहुँच पाउनको लागि पुष्टिकरण बटनमा क्लिक गर्नुहोस्।',\n    'registrations_disabled' => 'दर्ता हाल बन्द गरिएको छ',\n    'registration_email_domain_invalid' => 'त्यो इमेल डोमेनलाई यस आवेदनमा पहुँच छैन',\n    'register_success' => 'साइन अप गर्नुभएकोमा धन्यवाद! तपाईं अब दर्ता र लगइन भइसकेका हुनुहुन्छ।',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'लगइन प्रयास गर्दै',\n    'auto_init_starting_desc' => 'हामी तपाईंको प्रमाणीकरण प्रणालीलाई लगइन प्रक्रिया सुरु गर्नका लागि सम्पर्क गर्दैछौं। यदि 5 सेकेन्डपछि प्रगति हुँदैन भने तलको लिङ्कमा क्लिक गर्न प्रयास गर्नुहोस्।',\n    'auto_init_start_link' => 'प्रमाणीकरणसँग अगाडि बढ्नुहोस्',\n\n    // Password Reset\n    'reset_password' => 'पासवर्ड रिसेट गर्नुहोस्',\n    'reset_password_send_instructions' => 'तपाईंको इमेल तल प्रविष्ट गर्नुहोस् र तपाईंलाई पासवर्ड रिसेट लिङ्क सहितको इमेल पठाइनेछ।',\n    'reset_password_send_button' => 'रिसेट लिङ्क पठाउनुहोस्',\n    'reset_password_sent' => ':email इमेलमा पासवर्ड रिसेट लिङ्क पठाइनेछ यदि त्यो इमेल ठेगाना प्रणालीमा फेला पारिन्छ भने।',\n    'reset_password_success' => 'तपाईंको पासवर्ड सफलतापूर्वक रिसेट गरिएको छ।',\n    'email_reset_subject' => ':appName पासवर्ड रिसेट गर्नुहोस्',\n    'email_reset_text' => 'तपाईं यो इमेल प्राप्त गर्दै हुनुहुन्छ किनकि हामीले तपाईंको खाताको लागि पासवर्ड रिसेट अनुरोध प्राप्त गर्यौं।',\n    'email_reset_not_requested' => 'यदि तपाईंले पासवर्ड रिसेट अनुरोध गर्नुभएको छैन भने, अगाडि कुनै कार्य आवश्यक पर्दैन।',\n\n    // Email Confirmation\n    'email_confirm_subject' => ':appName मा तपाईंको इमेल पुष्टि गर्नुहोस्',\n    'email_confirm_greeting' => ':appName मा सामेल हुनुभएकोमा धन्यवाद!',\n    'email_confirm_text' => 'कृपया तलको बटनमा क्लिक गरेर तपाईंको इमेल ठेगाना पुष्टि गर्नुहोस्:',\n    'email_confirm_action' => 'इमेल पुष्टि गर्नुहोस्',\n    'email_confirm_send_error' => 'इमेल पुष्टिकरण आवश्यक छ तर प्रणालीले इमेल पठाउन सकेन। इमेल सही तरिकाले सेटअप गरिएको छ भनी सुनिश्चित गर्न प्रशासकलाई सम्पर्क गर्नुहोस्।',\n    'email_confirm_success' => 'तपाईंको इमेल पुष्टि भएको छ! अब तपाईं यो इमेल ठेगाना प्रयोग गरेर लगइन गर्न सक्षम हुनुहुनेछ।',\n    'email_confirm_resent' => 'पुष्टिकरण इमेल पुनः पठाइएको छ, कृपया तपाईंको इनबक्स जाँच गर्नुहोस्।',\n    'email_confirm_thanks' => 'पुष्टिकरणको लागि धन्यवाद!',\n    'email_confirm_thanks_desc' => 'कृपया केही समय कुर्नुहोस् जबकि तपाईंको पुष्टिकरण प्रक्रिया गरिन्छ। यदि तपाईंलाई 3 सेकेन्ड पछि पुनः रिडिरेक्ट गरिएको छैन भने, तलको \"अगाडि बढ्नुहोस्\" लिङ्कमा क्लिक गर्नुहोस्।',\n\n    'email_not_confirmed' => 'इमेल ठेगाना पुष्टि गरिएको छैन',\n    'email_not_confirmed_text' => 'तपाईंको इमेल ठेगाना अझै पुष्टि भएको छैन।',\n    'email_not_confirmed_click_link' => 'कृपया तपाईंले दर्ता गर्दा पठाइएको इमेलमा रहेको लिङ्कमा क्लिक गर्नुहोस्।',\n    'email_not_confirmed_resend' => 'यदि तपाईंलाई इमेल भेट्न गाह्रो भइरहेको छ भने, तपाईं तलको फारम द्वारा पुष्टिकरण इमेल पुनः पठाउन सक्नुहुन्छ।',\n    'email_not_confirmed_resend_button' => 'पुष्टिकरण इमेल पुनः पठाउनुहोस्',\n\n    // User Invite\n    'user_invite_email_subject' => ':appName मा सामेल हुनका लागि तपाईंलाई आमन्त्रित गरिएको छ!',\n    'user_invite_email_greeting' => ':appName मा तपाईंको खाता सिर्जना गरिएको छ।',\n    'user_invite_email_text' => 'खाता पासवर्ड सेट गर्न र पहुँच प्राप्त गर्न तलको बटनमा क्लिक गर्नुहोस्:',\n    'user_invite_email_action' => 'खाता पासवर्ड सेट गर्नुहोस्',\n    'user_invite_page_welcome' => ':appName मा स्वागत छ!',\n    'user_invite_page_text' => 'तपाईंको खाता अन्तिम रूप दिन र पहुँच प्राप्त गर्न तपाईंलाई पासवर्ड सेट गर्न आवश्यक छ जुन भविष्यका भ्रमणमा :appName मा लगइन गर्न प्रयोग हुनेछ।',\n    'user_invite_page_confirm_button' => 'पासवर्ड पक्का गर्नुहोस्',\n    'user_invite_success_login' => 'पासवर्ड सेट गरिएको छ, तपाईं अब तपाईंको सेट गरिएको पासवर्ड प्रयोग गरेर :appName मा लगइन गर्न सक्षम हुनुहुनेछ!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'मल्टि-फ्याक्टर प्रमाणीकरण सेटअप गर्नुहोस्',\n    'mfa_setup_desc' => 'तपाईंको प्रयोगकर्ता खाता सुरक्षा थप गर्न मल्टि-फ्याक्टर प्रमाणीकरण सेटअप गर्नुहोस्।',\n    'mfa_setup_configured' => 'पहिले नै कन्फिगर गरिएको',\n    'mfa_setup_reconfigure' => 'पुनः कन्फिगर गर्नुहोस्',\n    'mfa_setup_remove_confirmation' => 'के तपाईं यो मल्टि-फ्याक्टर प्रमाणीकरण विधि हटाउन चाहानुहुन्छ?',\n    'mfa_setup_action' => 'सेटअप',\n    'mfa_backup_codes_usage_limit_warning' => 'तपाईंको ५ भन्दा कम ब्याकअप कोड बाँकी छन्, कृपया नयाँ सेट उत्पन्न गर्नुहोस् र सुरक्षित स्थानमा राख्नुहोस् ताकि तपाईंको खाता लकडाउन नहोस्।',\n    'mfa_option_totp_title' => 'मोबाइल एप',\n    'mfa_option_totp_desc' => 'मल्टि-फ्याक्टर प्रमाणीकरण प्रयोग गर्नको लागि तपाईंलाई Google Authenticator, Authy वा Microsoft Authenticator जस्ता TOTP समर्थित मोबाइल एपको आवश्यकता पर्छ।',\n    'mfa_option_backup_codes_title' => 'ब्याकअप कोड',\n    'mfa_option_backup_codes_desc' => 'एक सेट ब्याकअप कोड उत्पन्न गर्दछ जसलाई तपाईंले लगइन गर्दा आफ्नो पहिचान प्रमाणित गर्न प्रयोग गर्नुहुनेछ। यी सुरक्षित स्थानमा राख्नुहोस्।',\n    'mfa_gen_confirm_and_enable' => 'पुष्टिकरण र सक्षम गर्नुहोस्',\n    'mfa_gen_backup_codes_title' => 'ब्याकअप कोड सेटअप',\n    'mfa_gen_backup_codes_desc' => 'तलको कोडहरूको सूची सुरक्षित स्थानमा राख्नुहोस्। प्रणालीमा पहुँच गर्दा तपाईंले एक कोडलाई दोस्रो प्रमाणीकरण विधिका रूपमा प्रयोग गर्नुहुनेछ।',\n    'mfa_gen_backup_codes_download' => 'कोडहरू डाउनलोड गर्नुहोस्',\n    'mfa_gen_backup_codes_usage_warning' => 'प्रत्येक कोड एक पटक मात्र प्रयोग गर्न सकिन्छ।',\n    'mfa_gen_totp_title' => 'मोबाइल एप सेटअप',\n    'mfa_gen_totp_desc' => 'मल्टि-फ्याक्टर प्रमाणीकरण प्रयोग गर्नको लागि तपाईंलाई Google Authenticator, Authy वा Microsoft Authenticator जस्ता TOTP समर्थित मोबाइल एपको आवश्यकता पर्छ।',\n    'mfa_gen_totp_scan' => 'सुरु गर्नको लागि तलको QR कोड स्क्यान गर्नुहोस्।',\n    'mfa_gen_totp_verify_setup' => 'सेटअप प्रमाणित गर्नुहोस्',\n    'mfa_gen_totp_verify_setup_desc' => 'सुनिश्चित गर्नका लागि एक कोड प्रविष्ट गर्नुहोस्, तपाईंको प्रमाणीकरण एपबाट उत्पन्न गरिएको, तलको इनपुट बक्समा:',\n    'mfa_gen_totp_provide_code_here' => 'तपाईंको एपबाट उत्पन्न गरिएको कोड यहाँ प्रदान गर्नुहोस्।',\n    'mfa_verify_access' => 'पहुँच प्रमाणित गर्नुहोस्',\n    'mfa_verify_access_desc' => 'तपाईंको प्रयोगकर्ता खाता थप प्रमाणीकरणको माध्यमबाट आफ्नो पहिचान प्रमाणित गर्नको लागि आवश्यक छ। कृपया अगाडि बढ्नको लागि तपाईंको कन्फिगर गरिएको विधि प्रयोग गर्नुहोस्।',\n    'mfa_verify_no_methods' => 'कुनै तरिका कन्फिगर गरिएको छैन',\n    'mfa_verify_no_methods_desc' => 'तपाईंको खातामा मल्टि-फ्याक्टर प्रमाणीकरण विधिहरू फेला परेका छैनन्। तपाईंलाई पहुँच प्राप्त गर्न कम्तिमा एक विधि सेटअप गर्न आवश्यक छ।',\n    'mfa_verify_use_totp' => 'मोबाइल एप प्रयोग गरेर प्रमाणित गर्नुहोस्',\n    'mfa_verify_use_backup_codes' => 'ब्याकअप कोड प्रयोग गरेर प्रमाणित गर्नुहोस्',\n    'mfa_verify_backup_code' => 'ब्याकअप कोड',\n    'mfa_verify_backup_code_desc' => 'तल तपाईंको बाँकी रहेका ब्याकअप कोडहरू मध्ये एउटा प्रविष्ट गर्नुहोस्:',\n    'mfa_verify_backup_code_enter_here' => 'यहाँ ब्याकअप कोड प्रविष्ट गर्नुहोस्',\n    'mfa_verify_totp_desc' => 'तपाईंको मोबाइल एपबाट उत्पन्न गरिएको कोड तल प्रविष्ट गर्नुहोस्:',\n    'mfa_setup_login_notification' => 'मल्टि-फ्याक्टर विधि कन्फिगर गरिएको छ, कृपया अब कन्फिगर गरिएको विधि प्रयोग गरेर फेरि लगइन गर्नुहोस्।',\n];\n"
  },
  {
    "path": "lang/ne/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'रद्द गर्नुहोस्',\n    'close' => 'बन्द गर्नुहोस्',\n    'confirm' => 'पुष्टि गर्नुहोस्',\n    'back' => 'फिर्ता',\n    'save' => 'सेभ गर्नुहोस्',\n    'continue' => 'जारी राख्नुहोस्',\n    'select' => 'छान्नुहोस्',\n    'toggle_all' => 'सबै टगल गर्नुहोस्',\n    'more' => 'थप',\n\n    // Form Labels\n    'name' => 'नाम',\n    'description' => 'विवरण',\n    'role' => 'भूमिका',\n    'cover_image' => 'आवरण चित्र',\n    'cover_image_description' => 'यो चित्र करिब 440x250px हुनुपर्छ, यद्यपि यो प्रयोगकर्ता इन्टरफेसमा आवश्यकताअनुसार लचिलो रूपमा स्केल र काटिने भएकाले देखिने वास्तविक आकार फरक हुन सक्छ।',\n\n    // Actions\n    'actions' => 'कार्यहरू',\n    'view' => 'हेर्नुहोस्',\n    'view_all' => 'सबै हेर्नुहोस्',\n    'new' => 'नयाँ',\n    'create' => 'सिर्जना गर्नुहोस्',\n    'update' => 'अद्यावधिक गर्नुहोस्',\n    'edit' => 'सम्पादन गर्नुहोस्',\n    'archive' => 'अभिलेख राख्नुहोस्',\n    'unarchive' => 'अभिलेख हटाउनुहोस्',\n    'sort' => 'क्रमबद्ध गर्नुहोस्',\n    'move' => 'सार्नुहोस्',\n    'copy' => 'प्रतिलिपि गर्नुहोस्',\n    'reply' => 'जवाफ दिनुहोस्',\n    'delete' => 'हटाउनुहोस्',\n    'delete_confirm' => 'हटाउने पुष्टि गर्नुहोस्',\n    'search' => 'खोज्नुहोस्',\n    'search_clear' => 'खोज हटाउनुहोस्',\n    'reset' => 'रीसेट गर्नुहोस्',\n    'remove' => 'हटाउनुहोस्',\n    'add' => 'थप्नुहोस्',\n    'configure' => 'कन्फिगर गर्नुहोस्',\n    'manage' => 'व्यवस्थापन गर्नुहोस्',\n    'fullscreen' => 'पूर्ण स्क्रिन',\n    'favourite' => 'मनपर्ने',\n    'unfavourite' => 'मनपर्नेबाट हटाउनुहोस्',\n    'next' => 'अर्को',\n    'previous' => 'अघिल्लो',\n    'filter_active' => 'सक्रिय फिल्टर:',\n    'filter_clear' => 'फिल्टर हटाउनुहोस्',\n    'download' => 'डाउनलोड गर्नुहोस्',\n    'open_in_tab' => 'ट्याबमा खोल्नुहोस्',\n    'open' => 'खोल्नुहोस्',\n\n    // Sort Options\n    'sort_options' => 'क्रमबद्ध विकल्पहरू',\n    'sort_direction_toggle' => 'क्रमबद्ध दिशा टगल',\n    'sort_ascending' => 'बढ्दो क्रममा क्रमबद्ध गर्नुहोस्',\n    'sort_descending' => 'घट्दो क्रममा क्रमबद्ध गर्नुहोस्',\n    'sort_name' => 'नाम',\n    'sort_default' => 'पूर्वनिर्धारित',\n    'sort_created_at' => 'सिर्जना मिति',\n    'sort_updated_at' => 'अद्यावधिक मिति',\n\n    // Misc\n    'deleted_user' => 'हटाइएको प्रयोगकर्ता',\n    'no_activity' => 'देखाउनका लागि कुनै गतिविधि छैन',\n    'no_items' => 'कुनै वस्तुहरू उपलब्ध छैनन्',\n    'back_to_top' => 'शीर्षमा फर्कनुहोस्',\n    'skip_to_main_content' => 'मुख्य सामग्रीमा जानुहोस्',\n    'toggle_details' => 'विवरण टगल गर्नुहोस्',\n    'toggle_thumbnails' => 'थम्बनेल टगल गर्नुहोस्',\n    'details' => 'विवरण',\n    'grid_view' => 'ग्रिड दृश्य',\n    'list_view' => 'सूची दृश्य',\n    'default' => 'पूर्वनिर्धारित',\n    'breadcrumb' => 'ब्रेडक्रम्ब',\n    'status' => 'स्थिति',\n    'status_active' => 'सक्रिय',\n    'status_inactive' => 'निष्क्रिय',\n    'never' => 'कहिल्यै होइन',\n    'none' => 'कुनै पनि होइन',\n\n    // Header\n    'homepage' => 'गृहपृष्ठ',\n    'header_menu_expand' => 'हेडर मेनु विस्तार गर्नुहोस्',\n    'profile_menu' => 'प्रोफाइल मेनु',\n    'view_profile' => 'प्रोफाइल हेर्नुहोस्',\n    'edit_profile' => 'प्रोफाइल सम्पादन गर्नुहोस्',\n    'dark_mode' => 'गाढा मोड',\n    'light_mode' => 'हल्का मोड',\n    'global_search' => 'विश्वव्यापी खोज',\n\n    // Layout tabs\n    'tab_info' => 'जानकारी',\n    'tab_info_label' => 'ट्याब: द्वितीय जानकारी देखाउनुहोस्',\n    'tab_content' => 'सामग्री',\n    'tab_content_label' => 'ट्याब: प्राथमिक सामग्री देखाउनुहोस्',\n\n    // Email Content\n    'email_action_help' => 'यदि तपाईं \":actionText\" बटनमा क्लिक गर्न समस्या भइरहेको छ भने, तलको URL आफ्नो वेब ब्राउजरमा कपी गरेर पेस्ट गर्नुहोस्:',\n    'email_rights' => 'सर्वाधिकार सुरक्षित',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'गोपनीयता नीति',\n    'terms_of_service' => 'सेवा सर्तहरू',\n\n    // OpenSearch\n    'opensearch_description' => ':appName खोज्नुहोस्',\n];\n"
  },
  {
    "path": "lang/ne/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'चित्र चयन गर्नुहोस्',\n    'image_list' => 'चित्र सूची',\n    'image_details' => 'चित्र विवरण',\n    'image_upload' => 'चित्र अपलोड गर्नुहोस्',\n    'image_intro' => 'यहाँ तपाईंले पहिले अपलोड गरिएका चित्रहरू चयन र व्यवस्थापन गर्न सक्नुहुन्छ।',\n    'image_intro_upload' => '\"चित्र अपलोड गर्नुहोस्\" बटन प्रयोग गरेर वा चित्र फाइललाई यो विन्डोमा तानेर नयाँ चित्र अपलोड गर्नुहोस्।',\n    'image_all' => 'सबै',\n    'image_all_title' => 'सबै चित्रहरू हेर्नुहोस्',\n    'image_book_title' => 'यस पुस्तकमा अपलोड गरिएका चित्रहरू हेर्नुहोस्',\n    'image_page_title' => 'यस पृष्ठमा अपलोड गरिएका चित्रहरू हेर्नुहोस्',\n    'image_search_hint' => 'चित्र नामद्वारा खोज्नुहोस्',\n    'image_uploaded' => 'अपलोड गरिएको :uploadedDate',\n    'image_uploaded_by' => ':userName द्वारा अपलोड गरिएको',\n    'image_uploaded_to' => ':pageLink मा अपलोड गरिएको',\n    'image_updated' => 'अद्यावधिक गरिएको :updateDate',\n    'image_load_more' => 'थप लोड गर्नुहोस्',\n    'image_image_name' => 'चित्र नाम',\n    'image_delete_used' => 'यो चित्र तलका पृष्ठहरूमा प्रयोग भइरहेको छ।',\n    'image_delete_confirm_text' => 'के तपाईं यो चित्र मेटाउन निश्चित हुनुहुन्छ?',\n    'image_select_image' => 'चित्र चयन गर्नुहोस्',\n    'image_dropzone' => 'चित्र ड्रप गर्नुहोस् वा अपलोड गर्न यहाँ क्लिक गर्नुहोस्',\n    'image_dropzone_drop' => 'अपलोड गर्न यहाँ चित्र ड्रप गर्नुहोस्',\n    'images_deleted' => 'चित्रहरू मेटाइयो',\n    'image_preview' => 'चित्र पूर्वावलोकन',\n    'image_upload_success' => 'चित्र सफलतापूर्वक अपलोड गरियो',\n    'image_update_success' => 'चित्र विवरण सफलतापूर्वक अद्यावधिक गरियो',\n    'image_delete_success' => 'चित्र सफलतापूर्वक मेटाइयो',\n    'image_replace' => 'चित्र प्रतिस्थापन गर्नुहोस्',\n    'image_replace_success' => 'चित्र फाइल सफलतापूर्वक अद्यावधिक गरियो',\n    'image_rebuild_thumbs' => 'आकारका भेरिएसनहरू पुनर्निर्माण गर्नुहोस्',\n    'image_rebuild_thumbs_success' => 'चित्र आकार भेरिएसनहरू सफलतापूर्वक पुनर्निर्माण गरियो!',\n\n    // Code Editor\n    'code_editor' => 'कोड सम्पादन गर्नुहोस्',\n    'code_language' => 'कोड भाषा',\n    'code_content' => 'कोड सामग्री',\n    'code_session_history' => 'सेसन इतिहास',\n    'code_save' => 'कोड सेभ गर्नुहोस्',\n];\n"
  },
  {
    "path": "lang/ne/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'सामान्य',\n    'advanced' => 'उन्नत',\n    'none' => 'कुनै पनि छैन',\n    'cancel' => 'रद्द गर्नुहोस्',\n    'save' => 'सेभ गर्नुहोस्',\n    'close' => 'बन्द गर्नुहोस्',\n    'apply' => 'लागु गर्नुहोस्',\n    'undo' => 'पूर्ववत् गर्नुहोस्',\n    'redo' => 'पुन: गर्नुहोस्',\n    'left' => 'बायाँ',\n    'center' => 'केन्द्र',\n    'right' => 'दायाँ',\n    'top' => 'माथि',\n    'middle' => 'बीच',\n    'bottom' => 'तल',\n    'width' => 'चौडाइ',\n    'height' => 'उचाइ',\n    'More' => 'थप',\n    'select' => 'छान्नुहोस्...',\n\n    // Toolbar\n    'formats' => 'ढाँचा',\n    'header_large' => 'ठुलो शीर्षक',\n    'header_medium' => 'मध्यम शीर्षक',\n    'header_small' => 'सानो शीर्षक',\n    'header_tiny' => 'अत्यन्त सानो शीर्षक',\n    'paragraph' => 'प्याराग्राफ',\n    'blockquote' => 'ब्लकउद्धरण',\n    'inline_code' => 'इनलाइन कोड',\n    'callouts' => 'कौलआउटहरू',\n    'callout_information' => 'सूचना',\n    'callout_success' => 'सफलता',\n    'callout_warning' => 'चेतावनी',\n    'callout_danger' => 'खतरा',\n    'bold' => 'मोठो अक्षर',\n    'italic' => 'तेरियो',\n    'underline' => 'रेखाङ्कन',\n    'strikethrough' => 'रेखाले काटिएको',\n    'superscript' => 'सुपरस्क्रिप्ट',\n    'subscript' => 'सबस्क्रिप्ट',\n    'text_color' => 'पाठको रंग',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'अनुकूलित रंग',\n    'remove_color' => 'रंग हटाउनुहोस्',\n    'background_color' => 'पृष्ठभूमि रंग',\n    'align_left' => 'बायाँ संरेखण',\n    'align_center' => 'केन्द्र संरेखण',\n    'align_right' => 'दायाँ संरेखण',\n    'align_justify' => 'समायोजन',\n    'list_bullet' => 'बुलेट सूची',\n    'list_numbered' => 'संख्याबद्ध सूची',\n    'list_task' => 'कार्य सूची',\n    'indent_increase' => 'इनडेन्ट बढाउनुहोस्',\n    'indent_decrease' => 'इनडेन्ट घटाउनुहोस्',\n    'table' => 'टेबल',\n    'insert_image' => 'चित्र राख्नुहोस्',\n    'insert_image_title' => 'चित्र राख्नुहोस्/सम्पादन गर्नुहोस्',\n    'insert_link' => 'लिंक राख्नुहोस्/सम्पादन गर्नुहोस्',\n    'insert_link_title' => 'लिंक राख्नुहोस्/सम्पादन गर्नुहोस्',\n    'insert_horizontal_line' => 'क्षैतिज रेखा राख्नुहोस्',\n    'insert_code_block' => 'कोड ब्लक राख्नुहोस्',\n    'edit_code_block' => 'कोड ब्लक सम्पादन गर्नुहोस्',\n    'insert_drawing' => 'ड्रइङ राख्नुहोस्/सम्पादन गर्नुहोस्',\n    'drawing_manager' => 'ड्रइङ व्यवस्थापक',\n    'insert_media' => 'मिडिया राख्नुहोस्/सम्पादन गर्नुहोस्',\n    'insert_media_title' => 'मिडिया राख्नुहोस्/सम्पादन गर्नुहोस्',\n    'clear_formatting' => 'ढाँचा सफा गर्नुहोस्',\n    'source_code' => 'स्रोत कोड',\n    'source_code_title' => 'स्रोत कोड',\n    'fullscreen' => 'पूर्ण स्क्रीन',\n    'image_options' => 'चित्र विकल्प',\n\n    // Tables\n    'table_properties' => 'टेबल गुणहरू',\n    'table_properties_title' => 'टेबल गुणहरू',\n    'delete_table' => 'टेबल मेटाउनुहोस्',\n    'table_clear_formatting' => 'टेबलको ढाँचा सफा गर्नुहोस्',\n    'resize_to_contents' => 'सामग्री अनुसार आकार मिलाउनुहोस्',\n    'row_header' => 'पङ्क्ति शीर्षक',\n    'insert_row_before' => 'अगाडिको पङ्क्ति राख्नुहोस्',\n    'insert_row_after' => 'पछिको पङ्क्ति राख्नुहोस्',\n    'delete_row' => 'पङ्क्ति मेटाउनुहोस्',\n    'insert_column_before' => 'अगाडिको स्तम्भ राख्नुहोस्',\n    'insert_column_after' => 'पछिको स्तम्भ राख्नुहोस्',\n    'delete_column' => 'स्तम्भ मेटाउनुहोस्',\n    'table_cell' => 'सेल',\n    'table_row' => 'पङ्क्ति',\n    'table_column' => 'स्तम्भ',\n    'cell_properties' => 'सेल गुणहरू',\n    'cell_properties_title' => 'सेल गुणहरू',\n    'cell_type' => 'सेल प्रकार',\n    'cell_type_cell' => 'सेल',\n    'cell_scope' => 'स्कोप',\n    'cell_type_header' => 'शीर्षक सेल',\n    'merge_cells' => 'सेल मर्ज गर्नुहोस्',\n    'split_cell' => 'सेल विभाजन गर्नुहोस्',\n    'table_row_group' => 'पङ्क्ति समूह',\n    'table_column_group' => 'स्तम्भ समूह',\n    'horizontal_align' => 'क्षैतिज संरेखण',\n    'vertical_align' => 'उर्ध्वाधर संरेखण',\n    'border_width' => 'बोर्डर चौडाइ',\n    'border_style' => 'बोर्डर शैली',\n    'border_color' => 'बोर्डर रंग',\n    'row_properties' => 'पङ्क्ति गुणहरू',\n    'row_properties_title' => 'पङ्क्ति गुणहरू',\n    'cut_row' => 'पङ्क्ति काट्नुहोस्',\n    'copy_row' => 'पङ्क्ति कपी गर्नुहोस्',\n    'paste_row_before' => 'अगाडिको पङ्क्तिमा पेस्ट गर्नुहोस्',\n    'paste_row_after' => 'पछिको पङ्क्तिमा पेस्ट गर्नुहोस्',\n    'row_type' => 'पङ्क्ति प्रकार',\n    'row_type_header' => 'शीर्षक',\n    'row_type_body' => 'शरीर',\n    'row_type_footer' => 'तल',\n    'alignment' => 'संरेखण',\n    'cut_column' => 'स्तम्भ काट्नुहोस्',\n    'copy_column' => 'स्तम्भ कपी गर्नुहोस्',\n    'paste_column_before' => 'अगाडिको स्तम्भमा पेस्ट गर्नुहोस्',\n    'paste_column_after' => 'पछिको स्तम्भमा पेस्ट गर्नुहोस्',\n    'cell_padding' => 'सेल प्याडिङ',\n    'cell_spacing' => 'सेल स्पेसिङ',\n    'caption' => 'क्याप्सन',\n    'show_caption' => 'क्याप्सन देखाउनुहोस्',\n    'constrain' => 'अनुपात सीमित गर्नुहोस्',\n    'cell_border_solid' => 'ठोस',\n    'cell_border_dotted' => 'डटेड',\n    'cell_border_dashed' => 'ड्यास्ड',\n    'cell_border_double' => 'डबल',\n    'cell_border_groove' => 'ग्रूभ',\n    'cell_border_ridge' => 'रिज',\n    'cell_border_inset' => 'इनसेट',\n    'cell_border_outset' => 'आउटसेट',\n    'cell_border_none' => 'कुनै पनि छैन',\n    'cell_border_hidden' => 'लुकेको',\n\n    // Images, links, details/summary & embed\n    'source' => 'स्रोत',\n    'alt_desc' => 'वैकल्पिक विवरण',\n    'embed' => 'एम्बेड',\n    'paste_embed' => 'तपाईंको एम्बेड कोड तल टाँस्नुहोस्:',\n    'url' => 'URL',\n    'text_to_display' => 'देखाउने पाठ',\n    'title' => 'शीर्षक',\n    'browse_links' => 'लिंकहरू ब्राउज गर्नुहोस्',\n    'open_link' => 'लिंक खोल्नुहोस्',\n    'open_link_in' => 'लिंक खोल्नुहोस् ...',\n    'open_link_current' => 'हालको विन्डो',\n    'open_link_new' => 'नयाँ विन्डो',\n    'remove_link' => 'लिंक हटाउनुहोस्',\n    'insert_collapsible' => 'टुंगो लाग्ने ब्लक राख्नुहोस्',\n    'collapsible_unwrap' => 'अनर्याप गर्नुहोस्',\n    'edit_label' => 'लेबल सम्पादन गर्नुहोस्',\n    'toggle_open_closed' => 'खोल्ने/बन्द गर्ने टगल गर्नुहोस्',\n    'collapsible_edit' => 'टुंगो लाग्ने ब्लक सम्पादन गर्नुहोस्',\n    'toggle_label' => 'लेबल टगल गर्नुहोस्',\n\n    // About view\n    'about' => 'संपादकको बारेमा',\n    'about_title' => 'WYSIWYG संपादकको बारेमा',\n    'editor_license' => 'संपादक अनुमति र कपीराइट',\n    'editor_lexical_license' => 'यो संपादक :lexicalLink को फोर्कको रूपमा निर्माण गरिएको हो जुन MIT लाइसेन्स अन्तर्गत वितरण गरिएको छ।',\n    'editor_lexical_license_link' => 'पूरा लाइसेन्स विवरण यहाँ भेट्न सकिन्छ।',\n    'editor_tiny_license' => 'यो संपादक :tinyLink प्रयोग गरेर निर्माण गरिएको हो जुन MIT लाइसेन्स अन्तर्गत उपलब्ध छ।',\n    'editor_tiny_license_link' => 'TinyMCE को कपीराइट र लाइसेन्स विवरण यहाँ भेट्न सकिन्छ।',\n    'save_continue' => 'पृष्ठ सेभ गरी जारी राख्नुहोस्',\n    'callouts_cycle' => '(प्रकारहरू टगल गर्न थिचिरहनुहोस्)',\n    'link_selector' => 'सामग्रीमा लिंक',\n    'shortcuts' => 'सर्टकटहरू',\n    'shortcut' => 'सर्टकट',\n    'shortcuts_intro' => 'संपादकमा निम्न सर्टकटहरू उपलब्ध छन्:',\n    'windows_linux' => '(विन्डोज/लिनक्स)',\n    'mac' => '(म्याक)',\n    'description' => 'विवरण',\n];\n"
  },
  {
    "path": "lang/ne/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'हालैमा सिर्जना गरिएको',\n    'recently_created_pages' => 'हालैमा सिर्जना गरिएका पाना',\n    'recently_updated_pages' => 'हालैमा अद्यावधिक गरिएका पाना',\n    'recently_created_chapters' => 'हालैमा सिर्जना गरिएका अध्यायहरू',\n    'recently_created_books' => 'हालैमा सिर्जना गरिएका पुस्तकहरू',\n    'recently_created_shelves' => 'हालैमा सिर्जना गरिएका दराजहरू',\n    'recently_update' => 'हालैमा अद्यावधिक गरिएको',\n    'recently_viewed' => 'हालैमा हेरिएको',\n    'recent_activity' => 'हालको गतिविधि',\n    'create_now' => 'अहिले सिर्जना गर्नुहोस्',\n    'revisions' => 'संशोधनहरू',\n    'meta_revision' => 'संशोधन #:revisionCount',\n    'meta_created' => 'सिर्जना गरिएको :timeLength',\n    'meta_created_name' => ':user द्वारा सिर्जना गरिएको :timeLength',\n    'meta_updated' => 'अद्यावधिक गरिएको :timeLength',\n    'meta_updated_name' => ':user द्वारा अद्यावधिक गरिएको :timeLength',\n    'meta_owned_name' => ':user द्वारा स्वामित्व गरिएको',\n    'meta_reference_count' => ':count वस्तु द्वारा सन्दर्भित|:count वस्तुहरू द्वारा सन्दर्भित',\n    'entity_select' => 'इकाई चयन',\n    'entity_select_lack_permission' => 'तपाईंलाई यो वस्तु चयन गर्नको लागि आवश्यक अनुमति छैन',\n    'images' => 'छविहरू',\n    'my_recent_drafts' => 'मेरो हालका मस्यौदाहरू',\n    'my_recently_viewed' => 'मेरो हालै हेरिएका पाना',\n    'my_most_viewed_favourites' => 'मेरो सबैभन्दा हेरिएका मनपर्ने',\n    'my_favourites' => 'मेरो मनपर्ने',\n    'no_pages_viewed' => 'तपाईंले कुनै पाना हेरिसकेको छैन',\n    'no_pages_recently_created' => 'हालै कुनै पाना सिर्जना गरिएको छैन',\n    'no_pages_recently_updated' => 'हालै कुनै पाना अद्यावधिक गरिएको छैन',\n    'export' => 'निर्यात',\n    'export_html' => 'समाविष्ट वेब फाइल',\n    'export_pdf' => 'PDF फाइल',\n    'export_text' => 'साधारण टेक्स्ट फाइल',\n    'export_md' => 'Markdown फाइल',\n    'export_zip' => 'पोर्टेबल ZIP',\n    'default_template' => 'पूर्वनिर्धारित पृष्ठ ढाँचा',\n    'default_template_explain' => 'यस वस्तु भित्र सिर्जना गरिएका सबै पानाहरूको लागि पूर्वनिर्धारित सामग्रीको रूपमा प्रयोग हुने पृष्ठ ढाँचाको चयन गर्नुहोस्। ध्यान दिनुहोस् कि यो केवल प्रयोगकर्ता चयन गरेको ढाँचाको पृष्ठलाई हेर्ने अनुमति पाउँदा मात्र लागू हुनेछ।',\n    'default_template_select' => 'पृष्ठ ढाँचा चयन गर्नुहोस्',\n    'import' => 'आयात',\n    'import_validate' => 'आयात प्रमाणित गर्नुहोस्',\n    'import_desc' => 'पुस्तकहरू, अध्यायहरू र पृष्ठहरूलाई पोर्टेबल ZIP निर्यातको माध्यमबाट आयात गर्नुहोस्, चाहे त्यसैको, वा अन्य कुनै उदाहरणको। ZIP फाइल चयन गर्न जारी राख्नुहोस्। फाइल अपलोड र प्रमाणित भएपछि, तपाईंलाई आयातको कन्फिगर र पुष्टि गर्ने विकल्प हुनेछ।',\n    'import_zip_select' => 'अपलोड गर्न ZIP फाइल चयन गर्नुहोस्',\n    'import_zip_validation_errors' => 'प्रदान गरिएको ZIP फाइल प्रमाणित गर्दा निम्न त्रुटिहरू भेटिएका छन्:',\n    'import_pending' => 'पर्खाइमा रहेका आयातहरू',\n    'import_pending_none' => 'कुनै आयात सुरू गरिएको छैन।',\n    'import_continue' => 'आयात जारी राख्नुहोस्',\n    'import_continue_desc' => 'अपलोड गरिएको ZIP फाइलबाट आयात गरिने सामग्रीको समीक्षा गर्नुहोस्। तयार भएपछि, आयात सञ्चालन गर्नुहोस् र यसको सामग्री यस प्रणालीमा थप्नुहोस्। सफल आयातपछि, अपलोड गरिएको ZIP आयात फाइल स्वचालित रूपमा मेटाइनेछ।',\n    'import_details' => 'आयात विवरण',\n    'import_run' => 'आयात सञ्चालन गर्नुहोस्',\n    'import_size' => ':size आयात ZIP साइज',\n    'import_uploaded_at' => 'अपलोड गरिएको :relativeTime',\n    'import_uploaded_by' => 'द्वारा अपलोड गरिएको',\n    'import_location' => 'आयात स्थान',\n    'import_location_desc' => 'आयात गरिएको सामग्रीको लागि लक्ष्य स्थान चयन गर्नुहोस्। तपाईंले चयन गरेको स्थानमा सिर्जना गर्नको लागि तपाईंलाई सम्बन्धित अनुमतिहरू आवश्यक पर्नेछन्।',\n    'import_delete_confirm' => 'के तपाईं पक्का हुनुहुन्छ कि तपाईं यो आयात मेट्न चाहनुहुन्छ?',\n    'import_delete_desc' => 'यो अपलोड गरिएको आयात ZIP फाइल मेट्नेछ, र यो कार्य नकारात्मक हुन सक्दैन।',\n    'import_errors' => 'आयात त्रुटिहरू',\n    'import_errors_desc' => 'आयात प्रयासको क्रममा निम्न त्रुटिहरू उत्पन्न भएका छन्:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'अनुमतिहरू',\n    'permissions_desc' => 'यहाँ अनुमतिहरू सेट गर्नुहोस् जसले प्रयोगकर्ता भूमिकाद्वारा प्रदान गरिएका डिफल्ट अनुमतिहरूलाई ओभरराइड गर्दछ।',\n    'permissions_book_cascade' => 'पुस्तकहरूमा सेट गरिएका अनुमतिहरू स्वचालित रूपमा सन्तान अध्यायहरू र पृष्ठहरूमा लागू हुनेछन्, जबसम्म तिनीहरूले आफ्नै अनुमतिहरू परिभाषित नगरेका हुँदैन।',\n    'permissions_chapter_cascade' => 'अध्याीयमा सेट गरिएका अनुमतिहरू स्वचालित रूपमा सन्तान पृष्ठहरूमा लागू हुनेछन्, जबसम्म तिनीहरूले आफ्नै अनुमतिहरू परिभाषित नगरेका हुँदैन।',\n    'permissions_save' => 'अनुमतिहरू बचत गर्नुहोस्',\n    'permissions_owner' => 'स्वामी',\n    'permissions_role_everyone_else' => 'अरु सबै',\n    'permissions_role_everyone_else_desc' => 'प्रयोगकर्ता भूमिकामा विशेष रूपमा ओभरराइड नगरेको सबैका लागि अनुमतिहरू सेट गर्नुहोस्।',\n    'permissions_role_override' => 'भूमिकाका लागि अनुमतिहरू ओभरराइड गर्नुहोस्',\n    'permissions_inherit_defaults' => 'डिफल्टहरू मर्नुहोस्',\n\n    // Search\n    'search_results' => 'खोज परिणामहरू',\n    'search_total_results_found' => ':count परिणाम फेला परे|:count कुल परिणामहरू फेला परे',\n    'search_clear' => 'खोज मेटाउनुहोस्',\n    'search_no_pages' => 'कुनै पाना यस खोजसँग मेल खाएका छैनन्',\n    'search_for_term' => ':term को लागि खोजी गर्नुहोस्',\n    'search_more' => 'थप परिणामहरू',\n    'search_advanced' => 'उन्नत खोजी',\n    'search_terms' => 'खोजी शब्दहरू',\n    'search_content_type' => 'सामग्री प्रकार',\n    'search_exact_matches' => 'सटीक मेलहरू',\n    'search_tags' => 'ट्याग खोजी',\n    'search_options' => 'विकल्पहरू',\n    'search_viewed_by_me' => 'मैले हेरेको',\n    'search_not_viewed_by_me' => 'मैले नहेरेको',\n    'search_permissions_set' => 'अनुमतिहरू सेट गरिएका',\n    'search_created_by_me' => 'मैले सिर्जना गरेको',\n    'search_updated_by_me' => 'मैले अद्यावधिक गरेको',\n    'search_owned_by_me' => 'मैले स्वामित्व गरेको',\n    'search_date_options' => 'मिति विकल्पहरू',\n    'search_updated_before' => 'अद्यावधिक गरिएको अघि',\n    'search_updated_after' => 'अद्यावधिक गरिएको पछि',\n    'search_created_before' => 'सिर्जना गरिएको अघि',\n    'search_created_after' => 'सिर्जना गरिएको पछि',\n    'search_set_date' => 'मिति सेट गर्नुहोस्',\n    'search_update' => 'खोज अपडेट गर्नुहोस्',\n\n    // Shelves\n    'shelf' => 'दराज',\n    'shelves' => 'दराजहरू',\n    'x_shelves' => ':count दराज|:count दराजहरू',\n    'shelves_empty' => 'कुनै दराज सिर्जना गरिएको छैन',\n    'shelves_create' => 'नयाँ दराज सिर्जना गर्नुहोस्',\n    'shelves_popular' => 'लोकप्रिय दराजहरू',\n    'shelves_new' => 'नयाँ दराजहरू',\n    'shelves_new_action' => 'नयाँ दराज',\n    'shelves_popular_empty' => 'यहाँ सबैभन्दा लोकप्रिय दराजहरू देखिनेछन्।',\n    'shelves_new_empty' => 'यहाँ सबैभन्दा नयाँ सिर्जना गरिएका दराजहरू देखिनेछन्।',\n    'shelves_save' => 'दराज बचत गर्नुहोस्',\n    'shelves_books' => 'यस दराजमा पुस्तकहरू',\n    'shelves_add_books' => 'यस दराजमा पुस्तकहरू थप्नुहोस्',\n    'shelves_drag_books' => 'पुस्तकहरू तल तान्नुहोस् यस दराजमा थप्नका लागि',\n    'shelves_empty_contents' => 'यस दराजमा कुनै पुस्तकहरू असाइन गरिएको छैन',\n    'shelves_edit_and_assign' => 'दराज सम्पादन गरेर पुस्तकहरू असाइन गर्नुहोस्',\n    'shelves_edit_named' => 'दराज सम्पादन गर्नुहोस् :name',\n    'shelves_edit' => 'दराज सम्पादन गर्नुहोस्',\n    'shelves_delete' => 'दराज मेट्नुहोस्',\n    'shelves_delete_named' => 'दराज मेट्नुहोस् :name',\n    'shelves_delete_explain' => \"यसले ':name' नामक दराज मेट्नेछ। समावेश गरिएका पुस्तकहरू मेटिने छैनन्।\",\n    'shelves_delete_confirmation' => 'के तपाईं यकिन हुनुहुन्छ कि तपाईं यस दराजलाई मेट्न चाहनुहुन्छ?',\n    'shelves_permissions' => 'दराज अनुमतिहरू',\n    'shelves_permissions_updated' => 'दराज अनुमतिहरू अद्यावधिक गरिएका',\n    'shelves_permissions_active' => 'दराज अनुमतिहरू सक्रिय',\n    'shelves_permissions_cascade_warning' => 'दराजमा सेट गरिएका अनुमतिहरू स्वचालित रूपमा समाविष्ट पुस्तकहरूमा क्यास्केड हुँदैनन्। यो कारणले कि पुस्तक एक भन्दा धेरै दराजमा अस्तित्वमा हुन सक्छ।',\n    'shelves_permissions_create' => 'दराज सिर्जना अनुमतिहरू केवल बालक पुस्तकहरूमा अनुमतिहरूको कपी गर्नको लागि प्रयोग गरिन्छ। यी अनुमतिहरू पुस्तक सिर्जना गर्नको लागि छैन।',\n    'shelves_copy_permissions_to_books' => 'पुस्तकहरूमा अनुमतिहरूको कपी गर्नुहोस्',\n    'shelves_copy_permissions' => 'अनुमतिहरूको कपी गर्नुहोस्',\n    'shelves_copy_permissions_explain' => 'यसले यस दराजको वर्तमान अनुमतिहरूलाई यसमा समावेश गरिएका सबै पुस्तकहरूमा लागू गर्नेछ। कृपया सुनिश्चित गर्नुहोस् कि कुनै पनि परिवर्तनहरू सेभ भएका छन्।',\n    'shelves_copy_permission_success' => ':count पुस्तकहरूमा दराज अनुमतिहरू कपी गरिएका',\n\n    // Books\n    'book' => 'पुस्तक',\n    'books' => 'पुस्तकहरू',\n    'x_books' => ':count पुस्तक|:count पुस्तकहरू',\n    'books_empty' => 'कुनै पुस्तकहरू सिर्जना गरिएका छैनन्',\n    'books_popular' => 'लोकप्रिय पुस्तकहरू',\n    'books_recent' => 'हालका पुस्तकहरू',\n    'books_new' => 'नयाँ पुस्तकहरू',\n    'books_new_action' => 'नयाँ पुस्तक',\n    'books_popular_empty' => 'यहाँ सबैभन्दा लोकप्रिय पुस्तकहरू देखा पर्नेछन्।',\n    'books_new_empty' => 'यहाँ सबैभन्दा हालसालै सिर्जना गरिएका पुस्तकहरू देखा पर्नेछन्।',\n    'books_create' => 'नयाँ पुस्तक सिर्जना गर्नुहोस्',\n    'books_delete' => 'पुस्तक मेट्नुहोस्',\n    'books_delete_named' => 'पुस्तक मेट्नुहोस् :bookName',\n    'books_delete_explain' => 'यो पुस्तकलाई नाम \\':bookName\\' मेट्नेछ। सबै पृष्ठहरू र अध्यायहरू हटाइनेछन्।',\n    'books_delete_confirmation' => 'के तपाईं पक्का हुनुहुन्छ कि तपाईं यस पुस्तकलाई मेट्न चाहनुहुन्छ?',\n    'books_edit' => 'पुस्तक सम्पादन गर्नुहोस्',\n    'books_edit_named' => 'पुस्तक सम्पादन गर्नुहोस् :bookName',\n    'books_form_book_name' => 'पुस्तकको नाम',\n    'books_save' => 'पुस्तक बचत गर्नुहोस्',\n    'books_permissions' => 'पुस्तक अनुमतिहरू',\n    'books_permissions_updated' => 'पुस्तक अनुमतिहरू अद्यावधिक गरियो',\n    'books_empty_contents' => 'यस पुस्तकको लागि कुनै पृष्ठहरू वा अध्यायहरू सिर्जना गरिएका छैनन्।',\n    'books_empty_create_page' => 'नयाँ पृष्ठ सिर्जना गर्नुहोस्',\n    'books_empty_sort_current_book' => 'हालको पुस्तकलाई वर्गीकृत गर्नुहोस्',\n    'books_empty_add_chapter' => 'अध्याय थप्नुहोस्',\n    'books_permissions_active' => 'पुस्तक अनुमतिहरू सक्रिय छन्',\n    'books_search_this' => 'यस पुस्तकमा खोजी गर्नुहोस्',\n    'books_navigation' => 'पुस्तक नेभिगेशन',\n    'books_sort' => 'पुस्तक सामग्रीहरू वर्गीकृत गर्नुहोस्',\n    'books_sort_desc' => 'पुस्तकमा अध्यायहरू र पृष्ठहरूलाई पुनः व्यवस्थित गर्नका लागि सार्नुहोस्। अन्य पुस्तकहरू थप्न सकिन्छ जसले अध्याय र पृष्ठहरूलाई पुस्तकहरू बीच सजिलै सर्न मद्दत गर्दछ। वैकल्पिक रूपमा एक स्वचालित वर्गीकरण नियम सेट गर्न सकिन्छ जसले पुस्तकको सामग्रीहरू परिवर्तन भएपछि स्वत: वर्गीकृत गर्छ।',\n    'books_sort_auto_sort' => 'स्वचालित वर्गीकरण विकल्प',\n    'books_sort_auto_sort_active' => 'स्वचालित वर्गीकरण सक्रिय: :sortName',\n    'books_sort_named' => 'पुस्तक :bookName को वर्गीकरण गर्नुहोस्',\n    'books_sort_name' => 'नाम अनुसार वर्गीकृत गर्नुहोस्',\n    'books_sort_created' => 'सिर्जना मितिअनुसार वर्गीकृत गर्नुहोस्',\n    'books_sort_updated' => 'अद्यावधिक मितिअनुसार वर्गीकृत गर्नुहोस्',\n    'books_sort_chapters_first' => 'पहिले अध्यायहरू',\n    'books_sort_chapters_last' => 'अन्तिममा अध्यायहरू',\n    'books_sort_show_other' => 'अन्य पुस्तकहरू देखाउनुहोस्',\n    'books_sort_save' => 'नयाँ क्रम बचत गर्नुहोस्',\n    'books_sort_show_other_desc' => 'यहाँ अन्य पुस्तकहरू थप्नुहोस् जसले वर्गीकरण प्रक्रिया समावेश गर्न र पुस्तकहरू बीच सामग्री सजिलै पुनः व्यवस्थित गर्न मद्दत पुर्याउँछ।',\n    'books_sort_move_up' => 'माथि सार्नुहोस्',\n    'books_sort_move_down' => 'तल सार्नुहोस्',\n    'books_sort_move_prev_book' => 'अघिल्लो पुस्तकमा सार्नुहोस्',\n    'books_sort_move_next_book' => 'अर्को पुस्तकमा सार्नुहोस्',\n    'books_sort_move_prev_chapter' => 'अघिल्लो अध्यायमा सार्नुहोस्',\n    'books_sort_move_next_chapter' => 'अर्को अध्यायमा सार्नुहोस्',\n    'books_sort_move_book_start' => 'पुस्तकको सुरुवातमा सार्नुहोस्',\n    'books_sort_move_book_end' => 'पुस्तकको अन्त्यमा सार्नुहोस्',\n    'books_sort_move_before_chapter' => 'अध्यानको अघि सार्नुहोस्',\n    'books_sort_move_after_chapter' => 'अध्यानको पछि सार्नुहोस्',\n    'books_copy' => 'पुस्तक प्रतिलिपि गर्नुहोस्',\n    'books_copy_success' => 'पुस्तक सफलतापूर्वक प्रतिलिपि गरियो',\n\n    // Chapters\n    'chapter' => 'अध्याय',\n    'chapters' => 'अध्यायहरू',\n    'x_chapters' => ':count अध्याय|:count अध्यायहरू',\n    'chapters_popular' => 'लोकप्रिय अध्यायहरू',\n    'chapters_new' => 'नयाँ अध्याय',\n    'chapters_create' => 'नयाँ अध्याय सिर्जना गर्नुहोस्',\n    'chapters_delete' => 'अध्याय मेट्नुहोस्',\n    'chapters_delete_named' => 'अध्याय मेट्नुहोस् :chapterName',\n    'chapters_delete_explain' => 'यसले \\':chapterName\\' नामक अध्याय मेट्नेछ। यस अध्यायमा रहेका सबै पृष्ठहरू पनि मेटिनेछन्।',\n    'chapters_delete_confirm' => 'के तपाईं यस अध्यायलाई मेट्न चाहनुहुन्छ?',\n    'chapters_edit' => 'अध्याय सम्पादन गर्नुहोस्',\n    'chapters_edit_named' => 'अध्याय सम्पादन गर्नुहोस् :chapterName',\n    'chapters_save' => 'अध्याय बचत गर्नुहोस्',\n    'chapters_move' => 'अध्याय सार्नुहोस्',\n    'chapters_move_named' => 'अध्याय सार्नुहोस् :chapterName',\n    'chapters_copy' => 'अध्याय प्रतिलिपि गर्नुहोस्',\n    'chapters_copy_success' => 'अध्याय सफलतापूर्वक प्रतिलिपि गरिएको',\n    'chapters_permissions' => 'अध्याय अनुमतिहरू',\n    'chapters_empty' => 'हाल यस अध्यायमा कुनै पृष्ठहरू छैनन्।',\n    'chapters_permissions_active' => 'अध्याय अनुमतिहरू सक्रिय छन्',\n    'chapters_permissions_success' => 'अध्याय अनुमतिहरू अद्यावधिक गरिएका',\n    'chapters_search_this' => 'यस अध्यायको खोजी गर्नुहोस्',\n    'chapter_sort_book' => 'पुस्तक सॉर्ट गर्नुहोस्',\n\n    // Pages\n    'page' => 'पाना',\n    'pages' => 'पानाहरू',\n    'x_pages' => ':count पाना|:count पानाहरू',\n    'pages_popular' => 'लोकप्रिय पानाहरू',\n    'pages_new' => 'नयाँ पाना',\n    'pages_attachments' => 'जोडिएका फाइलहरू',\n    'pages_navigation' => 'पाना नेविगेसन',\n    'pages_delete' => 'पाना मेट्नुहोस्',\n    'pages_delete_named' => 'पाना मेट्नुहोस् :pageName',\n    'pages_delete_draft_named' => 'मस्यौदा पाना मेट्नुहोस् :pageName',\n    'pages_delete_draft' => 'मस्यौदा पाना मेट्नुहोस्',\n    'pages_delete_success' => 'पाना मेटियो',\n    'pages_delete_draft_success' => 'मस्यौदा पाना मेटियो',\n    'pages_delete_warning_template' => 'यो पाना पुस्तक वा अध्यायको डिफल्ट पृष्ठ ढाँचाको रूपमा सक्रिय छ। यो पाना मेटिएपछि, ती पुस्तक वा अध्यायहरूमा डिफल्ट पृष्ठ ढाँचाको असाइनमेन्ट हट्नेछ।',\n    'pages_delete_confirm' => 'के तपाईं यस पानालाई मेट्न चाहनुहुन्छ?',\n    'pages_delete_draft_confirm' => 'के तपाईं मस्यौदा पानालाई मेट्न चाहनुहुन्छ?',\n    'pages_editing_named' => 'पाना सम्पादन गर्दै :pageName',\n    'pages_edit_draft_options' => 'मस्यौदा विकल्पहरू',\n    'pages_edit_save_draft' => 'मस्यौदा बचत गर्नुहोस्',\n    'pages_edit_draft' => 'पाना मस्यौदा सम्पादन गर्नुहोस्',\n    'pages_editing_draft' => 'मस्यौदा सम्पादन गर्दै',\n    'pages_editing_page' => 'पाना सम्पादन गर्दै',\n    'pages_edit_draft_save_at' => 'मस्यौदा :time मा बचत गरिएको',\n    'pages_edit_delete_draft' => 'मस्यौदा मेट्नुहोस्',\n    'pages_edit_delete_draft_confirm' => 'के तपाईं आफ्नो मस्यौदा परिवर्तनहरू मेट्न चाहनुहुन्छ? सबै परिवर्तनहरू, अन्तिम पूर्ण बचतको पछि, हराउनेछन्।',\n    'pages_edit_discard_draft' => 'मस्यौदा त्याग्नुहोस्',\n    'pages_edit_switch_to_markdown' => 'Markdown सम्पादकमा स्विच गर्नुहोस्',\n    'pages_edit_switch_to_markdown_clean' => '(साफ सामग्री)',\n    'pages_edit_switch_to_markdown_stable' => '(स्थिर सामग्री)',\n    'pages_edit_switch_to_wysiwyg' => 'WYSIWYG सम्पादकमा स्विच गर्नुहोस्',\n    'pages_edit_switch_to_new_wysiwyg' => 'नयाँ WYSIWYG मा स्विच गर्नुहोस्',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(बीटा परीक्षणमा)',\n    'pages_edit_set_changelog' => 'चेंजलग सेट गर्नुहोस्',\n    'pages_edit_enter_changelog_desc' => 'तपाईंले गरेका परिवर्तनहरूको छोटो विवरण लेख्नुहोस्',\n    'pages_edit_enter_changelog' => 'चेंजलग लेख्नुहोस्',\n    'pages_editor_switch_title' => 'संपादक स्विच गर्नुहोस्',\n    'pages_editor_switch_are_you_sure' => 'के तपाईं पक्का हुनुहुन्छ कि तपाईं यस पानाको सम्पादक परिवर्तन गर्न चाहनुहुन्छ?',\n    'pages_editor_switch_consider_following' => 'सम्पादक परिवर्तन गर्दा निम्न कुरा ध्यानमा राख्नुहोस्:',\n    'pages_editor_switch_consideration_a' => 'एकपटक बचत भएपछि, नयाँ सम्पादक विकल्प भविष्यका सम्पादकहरूमा प्रयोग हुनेछ, जसमा त्यस्ता सम्पादकहरू पनि समावेश छन् जुन आफूले सम्पादकको प्रकार परिवर्तन गर्न सक्षम छैनन्।',\n    'pages_editor_switch_consideration_b' => 'यसले केही परिस्थितिहरूमा विवरण र सिन्ट्याक्सको हानि हुन सक्छ।',\n    'pages_editor_switch_consideration_c' => 'ट्याग वा चेंजलग परिवर्तनहरू, अन्तिम बचत पछि, यो परिवर्तनमा कायम रहनेछैन।',\n    'pages_save' => 'पाना बचत गर्नुहोस्',\n    'pages_title' => 'पाना शीर्षक',\n    'pages_name' => 'पाना नाम',\n    'pages_md_editor' => 'संपादक',\n    'pages_md_preview' => 'पूर्वावलोकन',\n    'pages_md_insert_image' => 'छवि समावेश गर्नुहोस्',\n    'pages_md_insert_link' => 'संगठन लिंक समावेश गर्नुहोस्',\n    'pages_md_insert_drawing' => 'चित्र समावेश गर्नुहोस्',\n    'pages_md_show_preview' => 'पूर्वावलोकन देखाउनुहोस्',\n    'pages_md_sync_scroll' => 'पूर्वावलोकन स्क्रोल सिंक गर्नुहोस्',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'असुरक्षित चित्र भेटियो',\n    'pages_drawing_unsaved_confirm' => 'अघिल्लो असफल चित्र बचत प्रयासबाट असुरक्षित चित्र डेटा भेटिएको छ। के तपाईं यस असुरक्षित चित्रलाई पुनर्स्थापना गरेर सम्पादन गर्न चाहनुहुन्छ?',\n    'pages_not_in_chapter' => 'पाना कुनै अध्यायमा छैन',\n    'pages_move' => 'पाना सार्नुहोस्',\n    'pages_copy' => 'पाना प्रतिलिपि गर्नुहोस्',\n    'pages_copy_desination' => 'प्रतिलिपि गन्तव्य',\n    'pages_copy_success' => 'पाना सफलतापूर्वक प्रतिलिपि गरियो',\n    'pages_permissions' => 'पाना अनुमतिहरू',\n    'pages_permissions_success' => 'पाना अनुमतिहरू अद्यावधिक गरिएका',\n    'pages_revision' => 'संशोधन',\n    'pages_revisions' => 'पाना संशोधनहरू',\n    'pages_revisions_desc' => 'तल सूचीबद्ध गरिएको छ यो पानाका सबै पुराना संशोधनहरू। तपाईं पुराना पृष्ठ संस्करणहरू फर्केर हेर्न, तुलना गर्न र पुनर्स्थापना गर्न सक्नुहुन्छ, यदि अनुमतिहरूले अनुमति दिएको छ भने। प्रणाली कन्फिगरेसनको आधारमा पुराना संशोधनहरू स्वचालित रूपमा मेटिने हुन सक्छ।',\n    'pages_revisions_named' => ':pageName का पाना संशोधनहरू',\n    'pages_revision_named' => ':pageName का पाना संशोधन',\n    'pages_revision_restored_from' => 'पुनर्स्थापित गरिएको #:id; :summary',\n    'pages_revisions_created_by' => 'द्वारा सिर्जना गरिएको',\n    'pages_revisions_date' => 'संशोधन मिति',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'संशोधन संख्या',\n    'pages_revisions_numbered' => 'संशोधन #:id',\n    'pages_revisions_numbered_changes' => 'संशोधन #:id परिवर्तनहरू',\n    'pages_revisions_editor' => 'संपादक प्रकार',\n    'pages_revisions_changelog' => 'चेंजलग',\n    'pages_revisions_changes' => 'परिवर्तनहरू',\n    'pages_revisions_current' => 'हालको संस्करण:',\n    'pages_revisions_preview' => 'पूर्वावलोकन गर्नुहोस्',\n    'pages_revisions_restore' => 'पुनर्स्थापित गर्नुहोस्',\n    'pages_revisions_none' => 'यस पानामा कुनै संशोधन छैन',\n    'pages_copy_link' => 'लिंक प्रतिलिपि गर्नुहोस्',\n    'pages_edit_content_link' => 'संपादकमा खण्डमा जानुहोस्',\n    'pages_pointer_enter_mode' => 'खण्ड चयन मोडमा जानुहोस्',\n    'pages_pointer_label' => 'पाना खण्ड विकल्पहरू',\n    'pages_pointer_permalink' => 'पाना खण्ड स्थायी लिंक',\n    'pages_pointer_include_tag' => 'पाना खण्ड समावेश ट्याग',\n    'pages_pointer_toggle_link' => 'स्थायी लिंक मोड, समावेश ट्याग देखाउनका लागि थिच्नुहोस्',\n    'pages_pointer_toggle_include' => 'समावेश ट्याग मोड, स्थायी लिंक देखाउनका लागि थिच्नुहोस्',\n    'pages_permissions_active' => 'पाना अनुमतिहरू सक्रिय छन्',\n    'pages_initial_revision' => 'प्रारम्भिक प्रकाशन',\n    'pages_references_update_revision' => 'आन्तरिक लिंकहरूको प्रणाली स्वचालित अद्यावधिक',\n    'pages_initial_name' => 'नयाँ पाना',\n    'pages_editing_draft_notification' => 'तपाईं हाल एक मस्यौदा सम्पादन गर्दै हुनुहुन्छ जुन अन्तिम पटक :timeDiff मा बचत गरिएको थियो।',\n    'pages_draft_edited_notification' => 'यो पाना त्यस समय पछि अद्यावधिक गरिएको छ। यस मस्यौदालाई त्याग्नु उचित हुनेछ।',\n    'pages_draft_page_changed_since_creation' => 'यो पाना मस्यौदा सिर्जना भएपछि अद्यावधिक गरिएको छ। यस मस्यौदालाई त्याग्नुपर्छ वा कुनै पाना परिवर्तनहरू मेटिन नदिनुहोस्।',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count प्रयोगकर्ताले यस पानालाई सम्पादन सुरु गरेका छन्',\n        'start_b' => ':userName ले यस पानालाई सम्पादन सुरु गरेका छन्',\n        'time_a' => 'पृष्ठ अन्तिम पटक अद्यावधिक भएको समयदेखि',\n        'time_b' => ':minCount मिनेटहरूको भित्र',\n        'message' => ':start :time। कृपया एकअर्काका अपडेटहरू मेट्नुहोस्!',\n    ],\n    'pages_draft_discarded' => 'मस्यौदा त्यागियो! सम्पादक वर्तमान पाना सामग्रीसँग अद्यावधिक गरिएको छ',\n    'pages_draft_deleted' => 'मस्यौदा मेटियो! सम्पादक वर्तमान पाना सामग्रीसँग अद्यावधिक गरिएको छ',\n    'pages_specific' => 'विशिष्ट पाना',\n    'pages_is_template' => 'पाना ढांचा',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'साइडबार टगल गर्नुहोस्',\n    'page_tags' => 'पाना ट्यागहरू',\n    'chapter_tags' => 'अध्याय ट्यागहरू',\n    'book_tags' => 'पुस्तक ट्यागहरू',\n    'shelf_tags' => 'शेल्फ ट्यागहरू',\n    'tag' => 'ट्याग',\n    'tags' =>  'ट्यागहरू',\n    'tags_index_desc' => 'ट्यागहरू सिस्टममा सामग्रीसँग लचिलो प्रकारको वर्गीकरण लागू गर्न प्रयोग गर्न सकिन्छ। ट्यागहरूमा किम्बो मान र मान हो सक्छ, जसले वैकल्पिक हुन्छ। एकपटक लागू भए पछि, सामग्रीलाई ट्यागको नाम र मान प्रयोग गरेर खोजी गर्न सकिन्छ।',\n    'tag_name' =>  'ट्याग नाम',\n    'tag_value' => 'ट्याग मान (वैकल्पिक)',\n    'tags_explain' => \"आफ्नो सामग्रीलाई राम्रोसँग वर्गीकरण गर्न केही ट्यागहरू थप्नुहोस्। \\n तपाई थप संगठनको लागि ट्यागको मान पनि असाइन गर्न सक्नुहुन्छ।\",\n    'tags_add' => 'अर्को ट्याग थप्नुहोस्',\n    'tags_remove' => 'यो ट्याग हटाउनुहोस्',\n    'tags_usages' => 'कुल ट्याग प्रयोगहरू',\n    'tags_assigned_pages' => 'पानामा असाइन गरिएको',\n    'tags_assigned_chapters' => 'अध्यायहरूमा असाइन गरिएको',\n    'tags_assigned_books' => 'पुस्तकहरूमा असाइन गरिएको',\n    'tags_assigned_shelves' => 'शेल्फहरूमा असाइन गरिएको',\n    'tags_x_unique_values' => ':count अनन्य मानहरू',\n    'tags_all_values' => 'सबै मानहरू',\n    'tags_view_tags' => 'ट्यागहरू हेर्नुहोस्',\n    'tags_view_existing_tags' => 'अस्तित्वमा रहेका ट्यागहरू हेर्नुहोस्',\n    'tags_list_empty_hint' => 'ट्यागहरू पृष्ठ सम्पादक साइडबार वा पुस्तक, अध्याय वा शेल्फको विवरण सम्पादन गर्दा असाइन गर्न सकिन्छ।',\n    'attachments' => 'जोडिएका फाइलहरू',\n    'attachments_explain' => 'केही फाइलहरू अपलोड गर्नुहोस् वा केही लिङ्कहरू जोड्नुहोस् जसलाई तपाईंको पानामा देखाउन चाहनुहुन्छ। यी पृष्ठ साइडबारमा देखिनेछन्।',\n    'attachments_explain_instant_save' => 'यहाँका परिवर्तनहरू तुरुन्तै बचत हुन्छन्।',\n    'attachments_upload' => 'फाइल अपलोड गर्नुहोस्',\n    'attachments_link' => 'लिङ्क जोड्नुहोस्',\n    'attachments_upload_drop' => 'वैकल्पिक रूपमा तपाईं यहाँ एक फाइल ड्र्याग र ड्रप गरेर अपलोड गर्न सक्नुहुन्छ।',\n    'attachments_set_link' => 'लिङ्क सेट गर्नुहोस्',\n    'attachments_delete' => 'के तपाईं यो जोडिएको फाइल मेट्न चाहनुहुन्छ?',\n    'attachments_dropzone' => 'यहाँ फाइलहरू ड्र्याग गर्न र अपलोड गर्नको लागि ड्रप गर्नुहोस्',\n    'attachments_no_files' => 'कुनै फाइलहरू अपलोड गरिएको छैन',\n    'attachments_explain_link' => 'यदि तपाईंले कुनै फाइल अपलोड नगरीकन लिङ्क जोड्न चाहनुहुन्छ भने, तपाईं यसलाई थप्न सक्नुहुन्छ। यो अर्को पृष्ठको लिङ्क वा क्लाउडमा राखिएको फाइलको लिङ्क हुन सक्छ।',\n    'attachments_link_name' => 'लिङ्कको नाम',\n    'attachment_link' => 'जोडिएको फाइल लिङ्क',\n    'attachments_link_url' => 'फाइलको लिङ्क',\n    'attachments_link_url_hint' => 'साइट वा फाइलको URL',\n    'attach' => 'जोड्नुहोस्',\n    'attachments_insert_link' => 'पृष्ठमा जोडिएको फाइलको लिङ्क थप्नुहोस्',\n    'attachments_edit_file' => 'फाइल सम्पादन गर्नुहोस्',\n    'attachments_edit_file_name' => 'फाइलको नाम',\n    'attachments_edit_drop_upload' => 'फाइलहरू ड्र्याग गर्नुहोस् वा यहाँ क्लिक गरेर अपलोड र ओभरराइट गर्नुहोस्',\n    'attachments_order_updated' => 'जोडिएको फाइलहरूको क्रम अद्यावधिक गरियो',\n    'attachments_updated_success' => 'जोडिएको फाइलको विवरण अद्यावधिक गरियो',\n    'attachments_deleted' => 'जोडिएको फाइल मेटियो',\n    'attachments_file_uploaded' => 'फाइल सफलतापूर्वक अपलोड गरिएको',\n    'attachments_file_updated' => 'फाइल सफलतापूर्वक अद्यावधिक गरिएको',\n    'attachments_link_attached' => 'लिङ्क सफलतापूर्वक पृष्ठसँग जोडिएको',\n    'templates' => 'ढाँचाहरू',\n    'templates_set_as_template' => 'पाना ढाँचाको रूपमा सेट गर्नुहोस्',\n    'templates_explain_set_as_template' => 'तपाईं यस पानालाई ढाँचाको रूपमा सेट गर्न सक्नुहुन्छ ताकि यसको सामग्रीलाई अन्य पानाहरू सिर्जना गर्दा प्रयोग गर्न सकिन्छ। अन्य प्रयोगकर्ताहरू यस ढाँचालाई तब मात्र प्रयोग गर्न सक्छन् जब उनीहरूलाई यस पानाको भ्यू अनुमति छ।',\n    'templates_replace_content' => 'पाना सामग्री प्रतिस्थापन गर्नुहोस्',\n    'templates_append_content' => 'पाना सामग्रीमा थप्नुहोस्',\n    'templates_prepend_content' => 'पाना सामग्री अगाडि थप्नुहोस्',\n\n    // Profile View\n    'profile_user_for_x' => ':time का लागि प्रयोगकर्ता',\n    'profile_created_content' => 'सिर्जना गरिएको सामग्री',\n    'profile_not_created_pages' => ':userName ले कुनै पानाहरू सिर्जना गरेका छैनन्',\n    'profile_not_created_chapters' => ':userName ले कुनै अध्यायहरू सिर्जना गरेका छैनन्',\n    'profile_not_created_books' => ':userName ले कुनै पुस्तकहरू सिर्जना गरेका छैनन्',\n    'profile_not_created_shelves' => ':userName ले कुनै शेल्फहरू सिर्जना गरेका छैनन्',\n\n    // Comments\n    'comment' => 'टिप्पणी',\n    'comments' => 'टिप्पणीहरू',\n    'comment_add' => 'टिप्पणी थप्नुहोस्',\n    'comment_none' => 'प्रदर्शन गर्न कुनै टिप्पणी छैन',\n    'comment_placeholder' => 'यहाँ टिप्पणी छोड्नुहोस्',\n    'comment_thread_count' => 'टिप्पणीहरू',\n    'comment_archived_count' => ':count पुरानो',\n    'comment_archived_threads' => 'पुरानो थ्रेडहरू',\n    'comment_save' => 'टिप्पणी सेभ गर्नुहोस्',\n    'comment_new' => 'नयाँ टिप्पणी',\n    'comment_created' => ':createDiff मा टिप्पणी गरियो',\n    'comment_updated' => ':updateDiff मा :username द्वारा अद्यावधिक गरिएको',\n    'comment_updated_indicator' => 'अद्यावधिक गरिएको',\n    'comment_deleted_success' => 'टिप्पणी मेटियो',\n    'comment_created_success' => 'टिप्पणी थपियो',\n    'comment_updated_success' => 'टिप्पणी अद्यावधिक गरियो',\n    'comment_archive_success' => 'टिप्पणी पुरानो गरियो',\n    'comment_unarchive_success' => 'टिप्पणी पुनः सक्रिय गरियो',\n    'comment_view' => 'टिप्पणी हेर्नुहोस्',\n    'comment_jump_to_thread' => 'थ्रेडमा जानुहोस्',\n    'comment_delete_confirm' => 'के तपाईं यस टिप्पणीलाई मेट्न चाहनुहुन्छ?',\n    'comment_in_reply_to' => ':commentId को जवाफमा',\n    'comment_reference' => 'सन्दर्भ',\n    'comment_reference_outdated' => '(अप्रचलित)',\n    'comment_editor_explain' => 'यहाँ पृष्ठमा छोडिएका टिप्पणीहरू छन्। बचत गरिएको पृष्ठ हेरिरहँदा टिप्पणीहरू थप्न र व्यवस्थापन गर्न सकिन्छ।',\n\n    // Revision\n    'revision_delete_confirm' => 'के तपाईं यस संशोधनलाई मेट्न चाहनुहुन्छ?',\n    'revision_restore_confirm' => 'के तपाईं यस संशोधनलाई पुनर्स्थापित गर्न चाहनुहुन्छ? हालको पृष्ठ सामग्री प्रतिस्थापित हुनेछ।',\n    'revision_cannot_delete_latest' => 'अन्तिम संशोधन मेट्न सकिदैन।',\n\n    // Copy view\n    'copy_consider' => 'कृपया सामग्री प्रतिलिपि गर्दा तलका कुराहरू विचार गर्नुहोस्।',\n    'copy_consider_permissions' => 'कस्टम अनुमति सेटिङहरू प्रतिलिपि गरिने छैन।',\n    'copy_consider_owner' => 'तपाईं सबै प्रतिलिपि गरिएका सामग्रीका मालिक बन्नुहुनेछ।',\n    'copy_consider_images' => 'पृष्ठ चित्र फाइलहरू नक्कल गरिने छैनन् र मौलिक चित्रहरूले ती पृष्ठसँगको सम्बन्ध कायम राख्नेछन् जहाँ तिनीहरू पहिले अपलोड गरिएको थिए।',\n    'copy_consider_attachments' => 'पृष्ठ जडानहरू प्रतिलिपि गरिने छैनन्।',\n    'copy_consider_access' => 'स्थान, मालिक वा अनुमतिहरूको परिवर्तनले यस सामग्रीलाई पहिले पहुँच नभएका प्रयोगकर्ताहरूलाई उपलब्ध गराउन सक्छ।',\n\n    // Conversions\n    'convert_to_shelf' => 'शेल्फमा रूपान्तरण गर्नुहोस्',\n    'convert_to_shelf_contents_desc' => 'तपाईं यस पुस्तकलाई समान सामग्रीसँग नयाँ शेल्फमा रूपान्तरण गर्न सक्नुहुन्छ। यस पुस्तकमा भएका अध्यायहरू नयाँ पुस्तकहरूमा रूपान्तरण गरिनेछन्। यदि यस पुस्तकमा कुनै पृष्ठहरू छन् जुन कुनै अध्यायमा छैनन् भने, यस पुस्तकको नाम परिवर्तन गरिनेछ र ती पृष्ठहरू समावेश गरिनेछन्, र यस पुस्तकलाई नयाँ शेल्फको हिस्सा बनाइनेछ।',\n    'convert_to_shelf_permissions_desc' => 'यस पुस्तकमा सेट गरिएका कुनै पनि अनुमतिहरू नयाँ शेल्फ र सबै नयाँ बाल पुस्तकहरूमा प्रतिलिपि गरिनेछन् जुन आफ्ना अनुमतिहरू लागू गरेका छैनन्। ध्यान दिनुहोस् कि शेल्फहरूमा अनुमतिहरू स्वत: सामग्रीमा लागू हुँदैनन्, जस्तै पुस्तकहरूमा।',\n    'convert_book' => 'पुस्तक रूपान्तरण गर्नुहोस्',\n    'convert_book_confirm' => 'के तपाईं पक्का हुनुहुन्छ कि तपाईं यस पुस्तकलाई रूपान्तरण गर्न चाहनुहुन्छ?',\n    'convert_undo_warning' => 'यो सजिलै उल्टाउन सकिँदैन।',\n    'convert_to_book' => 'पुस्तकमा रूपान्तरण गर्नुहोस्',\n    'convert_to_book_desc' => 'तपाईं यस अध्यायलाई समान सामग्रीसँग नयाँ पुस्तकमा रूपान्तरण गर्न सक्नुहुन्छ। यस अध्यायमा सेट गरिएका कुनै पनि अनुमतिहरू नयाँ पुस्तकमा प्रतिलिपि गरिनेछन्, तर कुनै पनि पितृ पुस्तकबाट परिग्रहीत अनुमतिहरू प्रतिलिपि गरिने छैनन्, जसले पहुँच नियन्त्रणमा परिवर्तन ल्याउन सक्छ।',\n    'convert_chapter' => 'अध्याय रूपान्तरण गर्नुहोस्',\n    'convert_chapter_confirm' => 'के तपाईं पक्का हुनुहुन्छ कि तपाईं यस अध्यायलाई रूपान्तरण गर्न चाहनुहुन्छ?',\n\n    // References\n    'references' => 'सन्दर्भहरू',\n    'references_none' => 'यस वस्तुमा कुनै ट्र्याक गरिएको सन्दर्भहरू छैनन्।',\n    'references_to_desc' => 'तल सूचीबद्ध गरिएको छ सबै जानिएको सामग्री प्रणालीमा जुन यस वस्तुसँग लिंक गरिएको छ।',\n\n    // Watch Options\n    'watch' => 'हेर्नुहोस्',\n    'watch_title_default' => 'पूर्वनिर्धारित प्राथमिकताहरू',\n    'watch_desc_default' => 'हेर्नुहोस् केवल तपाईंका पूर्वनिर्धारित सूचनाको प्राथमिकताहरूमा फर्कनुहोस्।',\n    'watch_title_ignore' => 'बेवास्ता गर्नुहोस्',\n    'watch_desc_ignore' => 'सभी सूचनाहरू बेवास्ता गर्नुहोस्, प्रयोगकर्ता-स्तरका प्राथमिकताहरू सहित।',\n    'watch_title_new' => 'नयाँ पृष्ठहरू',\n    'watch_desc_new' => 'जब यस वस्तुमा कुनै नयाँ पृष्ठ सिर्जना गरिन्छ भने सूचित गर्नुहोस्।',\n    'watch_title_updates' => 'सभी पृष्ठ अपडेटहरू',\n    'watch_desc_updates' => 'सभी नयाँ पृष्ठ र पृष्ठ परिवर्तनहरूमा सूचित गर्नुहोस्।',\n    'watch_desc_updates_page' => 'सभी पृष्ठ परिवर्तनहरूमा सूचित गर्नुहोस्।',\n    'watch_title_comments' => 'सभी पृष्ठ अपडेटहरू र टिप्पणियाँ',\n    'watch_desc_comments' => 'सभी नयाँ पृष्ठहरू, पृष्ठ परिवर्तनहरू र नयाँ टिप्पणीहरूमा सूचित गर्नुहोस्।',\n    'watch_desc_comments_page' => 'पृष्ठ परिवर्तनहरू र नयाँ टिप्पणीहरूमा सूचित गर्नुहोस्।',\n    'watch_change_default' => 'पूर्वनिर्धारित सूचनाका प्राथमिकताहरू परिवर्तन गर्नुहोस्',\n    'watch_detail_ignore' => 'सूचनाहरू बेवास्ता गर्दै',\n    'watch_detail_new' => 'नयाँ पृष्ठहरू हेर्नुहोस्',\n    'watch_detail_updates' => 'नयाँ पृष्ठहरू र अपडेटहरू हेर्नुहोस्',\n    'watch_detail_comments' => 'नयाँ पृष्ठहरू, अपडेटहरू र टिप्पणीहरू हेर्नुहोस्',\n    'watch_detail_parent_book' => 'पितृ पुस्तक मार्फत हेर्नुहोस्',\n    'watch_detail_parent_book_ignore' => 'पितृ पुस्तक मार्फत बेवास्ता गर्दै',\n    'watch_detail_parent_chapter' => 'पितृ अध्याय मार्फत हेर्नुहोस्',\n    'watch_detail_parent_chapter_ignore' => 'पितृ अध्याय मार्फत बेवास्ता गर्दै',\n];\n"
  },
  {
    "path": "lang/ne/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'तपाईंले अनुरोध गरिएको पाना पहुँच गर्नको लागि अनुमति प्राप्त गर्नुभएको छैन।',\n    'permissionJson' => 'तपाईंले अनुरोध गरिएको क्रियाकलाप गर्नको लागि अनुमति प्राप्त गर्नुभएको छैन।',\n\n    // Auth\n    'error_user_exists_different_creds' => 'इमेल :email संग पहिले नै प्रयोगकर्ता अस्तित्वमा छ तर फरक प्रमाणपत्रहरूका साथ।',\n    'auth_pre_register_theme_prevention' => 'प्रदत्त विवरणका लागि प्रयोगकर्ता खाता दर्ता गर्न सकिएन।',\n    'email_already_confirmed' => 'इमेल पहिले नै प्रमाणित भइसकेको छ, कृपया लगइन प्रयास गर्नुहोस्।',\n    'email_confirmation_invalid' => 'यो पुष्टि टोकन अवैध छ वा पहिले नै प्रयोग भइसकेको छ, कृपया पुनः दर्ता प्रयास गर्नुहोस्।',\n    'email_confirmation_expired' => 'पुष्टि टोकन समाप्त भइसकेको छ, नयाँ पुष्टि इमेल पठाइएको छ।',\n    'email_confirmation_awaiting' => 'यो खाताको इमेल ठेगाना प्रमाणित गर्न बाँकी छ।',\n    'ldap_fail_anonymous' => 'LDAP पहुँच अज्ञात बाइन्ड प्रयोग गरेर असफल भएको छ।',\n    'ldap_fail_authed' => 'LDAP पहुँच निर्दिष्ट dn र पासवर्ड विवरण प्रयोग गरेर असफल भएको छ।',\n    'ldap_extension_not_installed' => 'LDAP PHP एक्स्टेन्सन इन्स्टल गरिएको छैन।',\n    'ldap_cannot_connect' => 'LDAP सर्भरमा जडान गर्न सकिएन, आरम्भिक जडान असफल भएको छ।',\n    'saml_already_logged_in' => 'पहिले नै लगइन हुनुहुन्छ।',\n    'saml_no_email_address' => 'बाह्य प्रमाणीकरण प्रणालीले प्रदान गरेको डाटामा यस प्रयोगकर्ताको इमेल ठेगाना भेट्न सकिएन।',\n    'saml_invalid_response_id' => 'बाह्य प्रमाणीकरण प्रणालीबाट आएको अनुरोध यस एप्लिकेशनद्वारा सुरु गरिएको प्रक्रिया द्वारा मान्यता प्राप्त छैन। लगइन पछि फर्किने प्रयास गर्दा यो समस्या उत्पन्न हुन सक्छ।',\n    'saml_fail_authed' => ':system प्रयोग गरेर लगइन असफल भएको छ, प्रणालीले सफल प्रमाणिकरण प्रदान गरेको छैन।',\n    'oidc_already_logged_in' => 'पहिले नै लगइन हुनुहुन्छ।',\n    'oidc_no_email_address' => 'बाह्य प्रमाणीकरण प्रणालीले प्रदान गरेको डाटामा यस प्रयोगकर्ताको इमेल ठेगाना भेट्न सकिएन।',\n    'oidc_fail_authed' => ':system प्रयोग गरेर लगइन असफल भएको छ, प्रणालीले सफल प्रमाणिकरण प्रदान गरेको छैन।',\n    'social_no_action_defined' => 'कोई क्रियाकलाप परिभाषित गरिएको छैन।',\n    'social_login_bad_response' => \":socialAccount लगइनको समयमा त्रुटि प्राप्त: \\n:error\",\n    'social_account_in_use' => 'यो :socialAccount खाता पहिले नै प्रयोगमा छ, कृपया :socialAccount विकल्प मार्फत लगइन प्रयास गर्नुहोस्।',\n    'social_account_email_in_use' => 'इमेल :email पहिले नै प्रयोगमा छ। यदि तपाईंको खाता छ भने, तपाईं आफ्नो प्रोफाइल सेटिङमा :socialAccount खाता जडान गर्न सक्नुहुन्छ।',\n    'social_account_existing' => 'यो :socialAccount तपाईंको प्रोफाइलसँग पहिले नै जडान गरिएको छ।',\n    'social_account_already_used_existing' => 'यो :socialAccount खाता पहिले नै अर्को प्रयोगकर्ताद्वारा प्रयोगमा छ।',\n    'social_account_not_used' => 'यो :socialAccount खाता कुनै प्रयोगकर्तासँग जडान गरिएको छैन। कृपया यसलाई आफ्नो प्रोफाइल सेटिङमा जडान गर्नुहोस्।',\n    'social_account_register_instructions' => 'यदि तपाईंको खाता छैन भने, तपाईं :socialAccount विकल्प प्रयोग गरेर खाता दर्ता गर्न सक्नुहुन्छ।',\n    'social_driver_not_found' => 'सामाजिक ड्राइभर फेला पारिएको छैन।',\n    'social_driver_not_configured' => 'तपाईंको :socialAccount सामाजिक सेटिङ सही तरिकाले कन्फिगर गरिएको छैन।',\n    'invite_token_expired' => 'यो निमन्त्रणा लिंक समाप्त भइसकेको छ। तपाईं सट्टा आफ्नो खाता पासवर्ड रिसेट गर्न प्रयास गर्न सक्नुहुन्छ।',\n    'login_user_not_found' => 'यो क्रियाकलापका लागि प्रयोगकर्ता फेला पारिएको छैन।',\n\n    // System\n    'path_not_writable' => 'फाइल पथ :filePath मा अपलोड गर्न सकिएन। कृपया यो पथ सर्भरमा लेख्न योग्य बनाउन सुनिश्चित गर्नुहोस्।',\n    'cannot_get_image_from_url' => ':url बाट चित्र प्राप्त गर्न सकिएन।',\n    'cannot_create_thumbs' => 'सर्भरले थम्बनेल बनाउन सक्दैन। कृपया तपाईंको सिस्टममा GD PHP एक्स्टेन्सन इन्स्टल गरिएको छ भनेर जाँच गर्नुहोस्।',\n    'server_upload_limit' => 'सर्भरले यस आकारको अपलोड अनुमति दिंदैन। कृपया सानो फाइल आकारको प्रयास गर्नुहोस्।',\n    'server_post_limit' => 'सर्भरले दिएको डेटा आकार प्राप्त गर्न सक्दैन। कृपया कम डेटा वा सानो फाइलको प्रयास गर्नुहोस्।',\n    'uploaded'  => 'सर्भरले यस आकारको अपलोड अनुमति दिंदैन। कृपया सानो फाइल आकारको प्रयास गर्नुहोस्।',\n\n    // Drawing & Images\n    'image_upload_error' => 'चित्र अपलोड गर्दा त्रुटि भयो।',\n    'image_upload_type_error' => 'अपलोड गरिएको चित्र प्रकार अवैध छ।',\n    'image_upload_replace_type' => 'चित्र फाइल प्रतिस्थापनहरू समान प्रकारका हुनुपर्छ।',\n    'image_upload_memory_limit' => 'चित्र अपलोड गर्न र/वा थम्बनेल बनाउन असफल भएको छ, यो प्रणाली संसाधन सीमाहरूको कारणले हो।',\n    'image_thumbnail_memory_limit' => 'चित्रको आकार भिन्नताहरू बनाउन असफल भएको छ, यो प्रणाली संसाधन सीमाहरूको कारणले हो।',\n    'image_gallery_thumbnail_memory_limit' => 'ग्यालरी थम्बनेल बनाउन असफल भएको छ, यो प्रणाली संसाधन सीमाहरूको कारणले हो।',\n    'drawing_data_not_found' => 'चित्रको डाटा लोड गर्न सकिएन। चित्र फाइल अब अस्तित्वमा नभएको हुन सक्छ वा तपाईंलाई यसमा पहुँचको अनुमति नहुन सक्छ।',\n\n    // Attachments\n    'attachment_not_found' => 'जोडिएको फाइल फेला परेन।',\n    'attachment_upload_error' => 'जोडिएको फाइल अपलोड गर्दा त्रुटि भयो।',\n\n    // Pages\n    'page_draft_autosave_fail' => 'ड्राफ्ट बचत गर्न असफल भयो। यो पाना बचत गर्नु अघि कृपया इन्टरनेट जडान सुनिश्चित गर्नुहोस्।',\n    'page_draft_delete_fail' => 'पाना ड्राफ्ट मेटाउन र वर्तमान पाना सामग्री ल्याउन असफल भयो।',\n    'page_custom_home_deletion' => 'एक पाना लाई होमपाना को रूपमा सेट गर्दा मेटाउन सकिँदैन।',\n\n    // Entities\n    'entity_not_found' => 'इकाई फेला परेन।',\n    'bookshelf_not_found' => 'शेल्फ फेला परेन।',\n    'book_not_found' => 'पुस्तक फेला परेन।',\n    'page_not_found' => 'पाना फेला परेन।',\n    'chapter_not_found' => 'अध्याय फेला परेन।',\n    'selected_book_not_found' => 'चयन गरिएको पुस्तक फेला परेन।',\n    'selected_book_chapter_not_found' => 'चयन गरिएको पुस्तक वा अध्याय फेला परेन।',\n    'guests_cannot_save_drafts' => 'अतिथिहरू ड्राफ्टहरू बचत गर्न सक्दैनन्।',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'तपाईं केवल व्यवस्थापक भएको प्रयोगकर्तालाई मेटाउन सक्दैनौं।',\n    'users_cannot_delete_guest' => 'तपाईं अतिथि प्रयोगकर्तालाई मेटाउन सक्दैनौं।',\n    'users_could_not_send_invite' => 'प्रयोगकर्ता सिर्जना गर्न सकिएन, निमन्त्रणा इमेल पठाउन असफल भयो।',\n\n    // Roles\n    'role_cannot_be_edited' => 'यो भूमिका सम्पादन गर्न सकिँदैन।',\n    'role_system_cannot_be_deleted' => 'यो भूमिका एक प्रणाली भूमिका हो र मेटाउन सकिँदैन।',\n    'role_registration_default_cannot_delete' => 'यो भूमिका दर्ता गरेको डिफल्ट भूमिका भएकोले मेटाउन सकिँदैन।',\n    'role_cannot_remove_only_admin' => 'यो प्रयोगकर्ता व्यवस्थापक भूमिकामा मात्र एकमात्र प्रयोगकर्ता हो। यसलाई हटाउन प्रयास गर्नु अघि अर्को प्रयोगकर्तालाई व्यवस्थापक भूमिका दिनुहोस्।',\n\n    // Comments\n    'comment_list' => 'टिप्पणीहरू प्राप्त गर्दा त्रुटि भयो।',\n    'cannot_add_comment_to_draft' => 'तपाईं ड्राफ्टमा टिप्पणी थप्न सक्नुहुन्न।',\n    'comment_add' => 'टिप्पणी थप्दा / अद्यावधिक गर्दा त्रुटि भयो।',\n    'comment_delete' => 'टिप्पणी मेट्दा त्रुटि भयो।',\n    'empty_comment' => 'खाली टिप्पणी थप्न सकिँदैन।',\n\n    // Error pages\n    '404_page_not_found' => 'पाना फेला परेन।',\n    'sorry_page_not_found' => 'माफ गर्नुहोस्, तपाईंले खोज्नुभएको पाना फेला परेन।',\n    'sorry_page_not_found_permission_warning' => 'यदि तपाईंलाई यो पाना अस्तित्वमा हुनु पर्ने आशा थियो भने, तपाईंलाई यसलाई हेर्न अनुमति नहुन सक्छ।',\n    'image_not_found' => 'चित्र फेला परेन।',\n    'image_not_found_subtitle' => 'माफ गर्नुहोस्, तपाईंले खोज्नुभएको चित्र फाइल फेला परेन।',\n    'image_not_found_details' => 'यदि तपाईंले यो चित्र फेला पार्नु पर्ने आशा राख्नु भएको थियो भने, यो मेटिएको हुन सक्छ।',\n    'return_home' => 'गृहपृष्ठमा फर्कनुहोस्',\n    'error_occurred' => 'एउटा त्रुटि भयो।',\n    'app_down' => ':appName अहिले डाउन छ।',\n    'back_soon' => 'यो चाँडै पुनः सक्रिय हुनेछ।',\n\n    // Import\n    'import_zip_cant_read' => 'ZIP फाइल पढ्न सकिएन।',\n    'import_zip_cant_decode_data' => 'ZIP डाटा.json सामग्री पत्ता लाग्न र डिकोड गर्न सकिएन।',\n    'import_zip_no_data' => 'ZIP फाइल डाटामा अपेक्षित पुस्तक, अध्याय वा पाना सामग्री छैन।',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'आयात ZIP प्रमाणीकरण असफल भयो। त्रुटिहरू छन्:',\n    'import_zip_failed_notification' => 'ZIP फाइल आयात गर्न असफल भयो।',\n    'import_perms_books' => 'तपाईंलाई पुस्तकहरू सिर्जना गर्न आवश्यक अनुमति छैन।',\n    'import_perms_chapters' => 'तपाईंलाई अध्यायहरू सिर्जना गर्न आवश्यक अनुमति छैन।',\n    'import_perms_pages' => 'तपाईंलाई पाना सिर्जना गर्न आवश्यक अनुमति छैन।',\n    'import_perms_images' => 'तपाईंलाई चित्रहरू सिर्जना गर्न आवश्यक अनुमति छैन।',\n    'import_perms_attachments' => 'तपाईंलाई अनुलग्नकहरू सिर्जना गर्न आवश्यक अनुमति छैन।',\n\n    // API errors\n    'api_no_authorization_found' => 'अनुरोधमा कुनै प्रमाणीकरण टोकन फेला परेन।',\n    'api_bad_authorization_format' => 'अनुरोधमा प्रमाणीकरण टोकन फेला परे तापनि यसको ढाँचा गलत देखिन्छ।',\n    'api_user_token_not_found' => 'दिएको प्रमाणीकरण टोकनको लागि मिल्दो API टोकन फेला परेन।',\n    'api_incorrect_token_secret' => 'दिइएको API टोकनको लागि प्रदान गरिएको गोप्य सही छैन।',\n    'api_user_no_api_permission' => 'API टोकनको मालिकसँग API कल गर्ने अनुमति छैन।',\n    'api_user_token_expired' => 'प्रमाणीकरण टोकन समाप्त भइसकेको छ।',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'टेस्ट इमेल पठाउँदा त्रुटि:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'URL कन्फिगर गरिएका अनुमत SSR होस्टसँग मेल खाँदैन।',\n];\n"
  },
  {
    "path": "lang/ne/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'नयाँ टिप्पणी: :pageName पानामा',\n    'new_comment_intro' => 'एक प्रयोगकर्ताले :appName मा रहेको पानामा टिप्पणी गरेका छन्:',\n    'new_page_subject' => 'नयाँ पाना: :pageName',\n    'new_page_intro' => ':appName मा नयाँ पाना बनाइएको छ',\n    'updated_page_subject' => 'पाना अपडेट भयो: :pageName',\n    'updated_page_intro' => ':appName मा पाना अपडेट गरिएको छ',\n    'updated_page_debounce' => 'धेरै सूचना नपरोस् भनेर, केही समयको लागि एउटै सम्पादकबाट हुने थप सम्पादनहरूका सूचना तपाईंलाई पठाइने छैन।',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'पानाको नाम:',\n    'detail_page_path' => 'पानाको स्थान:',\n    'detail_commenter' => 'टिप्पणी गर्ने:',\n    'detail_comment' => 'टिप्पणी:',\n    'detail_created_by' => 'बनाउने व्यक्ति:',\n    'detail_updated_by' => 'अपडेट गर्ने व्यक्ति:',\n\n    'action_view_comment' => 'टिप्पणी हेर्नुहोस्',\n    'action_view_page' => 'पाना हेर्नुहोस्',\n\n    'footer_reason' => 'तपाईंलाई यो सूचना :link अनुसार पठाइएको हो, जुन यस प्रकारको गतिविधिमा लागु हुन्छ।',\n    'footer_reason_link' => 'तपाईंको सूचना प्राथमिकता',\n];\n"
  },
  {
    "path": "lang/ne/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; अघिल्लो',\n    'next'     => 'अर्को &raquo;',\n\n];\n"
  },
  {
    "path": "lang/ne/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'पासवर्ड कम्तिमा ८ वर्णको हुनु पर्छ र दाेहाेर्याइएकाे पासवर्ड संग मेल खानु पर्छ।',\n    'user' => \"हामीले त्यो इमेल ठेगाना भएको प्रयोगकर्ता फेला पार्न सकेनौं।\",\n    'token' => 'यस इमेल ठेगानाको लागि पासवर्ड रिसेट टोकन अमान्य छ।',\n    'sent' => 'हामीले तपाईंको पासवर्ड रिसेट लिङ्क इमेल गरेका छौं!',\n    'reset' => 'पासवर्ड रिसेट भयो!',\n\n];\n"
  },
  {
    "path": "lang/ne/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'मेरो खाता',\n\n    'shortcuts' => 'सर्टकटहरू',\n    'shortcuts_interface' => 'UI सर्टकट प्राथमिकताहरू',\n    'shortcuts_toggle_desc' => 'यहाँ तपाईं किबोर्ड प्रणाली इन्टरफेस सर्टकटहरू सक्षम वा असक्षम गर्न सक्नुहुन्छ, जुन नेभिगेशन र क्रियाकलापहरूको लागि प्रयोग गरिन्छ।',\n    'shortcuts_customize_desc' => 'तपाईं तलका प्रत्येक सर्टकटलाई अनुकूलित गर्न सक्नुहुन्छ। केवल सर्टकटको इनपुट चयन गरेपछि आफ्नो इच्छित कीबोर्ड संयोजन थिच्नुहोस्।',\n    'shortcuts_toggle_label' => 'किबोर्ड सर्टकट सक्षम छ',\n    'shortcuts_section_navigation' => 'नेभिगेशन',\n    'shortcuts_section_actions' => 'साधारण क्रियाकलापहरू',\n    'shortcuts_save' => 'सर्टकटहरू बचत गर्नुहोस्',\n    'shortcuts_overlay_desc' => 'नोट: जब सर्टकटहरू सक्षम हुन्छन्, तब \"?\" थिचेर एक सहायक ओभरले देखाइन्छ जसले स्क्रीनमा हाल देखिएका क्रियाकलापहरूको लागि उपलब्ध सर्टकटहरू हाइलाइट गर्दछ।',\n    'shortcuts_update_success' => 'सर्टकट प्राथमिकताहरू अपडेट गरिएका छन्!',\n    'shortcuts_overview_desc' => 'प्रणाली प्रयोगकर्ता इन्टरफेसमा नेभिगेट गर्न तपाईंले प्रयोग गर्न सक्ने किबोर्ड सर्टकटहरू व्यवस्थापन गर्नुहोस्।',\n\n    'notifications' => 'सूचना प्राथमिकताहरू',\n    'notifications_desc' => 'प्रणालीमा केही क्रियाकलापहरू गर्दा तपाईंलाई प्राप्त हुने इमेल सूचनाहरू नियन्त्रण गर्नुहोस्।',\n    'notifications_opt_own_page_changes' => 'मैले स्वामित्व राख्ने पृष्ठहरूमा परिवर्तन हुँदा सूचित गर्नुहोस्',\n    'notifications_opt_own_page_comments' => 'मैले स्वामित्व राख्ने पृष्ठहरूमा टिप्पणी हुँदा सूचित गर्नुहोस्',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'मेरो टिप्पणीहरूमा उत्तर आएको बेला सूचित गर्नुहोस्',\n    'notifications_save' => 'प्राथमिकताहरू बचत गर्नुहोस्',\n    'notifications_update_success' => 'सूचना प्राथमिकताहरू अपडेट गरिएका छन्!',\n    'notifications_watched' => 'हेर्ने र बेवास्ता गरिएका सामग्रीहरू',\n    'notifications_watched_desc' => 'तल ती सामग्रीहरू छन् जसमा कस्टम वाच प्राथमिकताहरू लागू गरिएका छन्। यीलाई अपडेट गर्नको लागि सामग्री हेरेर, साइडबारमा वाच विकल्पहरू फेला पार्नुहोस्।',\n\n    'auth' => 'प्रवेश र सुरक्षा',\n    'auth_change_password' => 'पासवर्ड परिवर्तन गर्नुहोस्',\n    'auth_change_password_desc' => 'तपाईंको एप्लिकेसनमा लगइन गर्न प्रयोग गरिने पासवर्ड परिवर्तन गर्नुहोस्। यो कम्तिमा ८ अक्षर लामो हुनुपर्छ।',\n    'auth_change_password_success' => 'पासवर्ड अपडेट गरियो!',\n\n    'profile' => 'प्रोफाइल विवरण',\n    'profile_desc' => 'तपाईंको खाता विवरण व्यवस्थापन गर्नुहोस् जसले तपाईंलाई अन्य प्रयोगकर्ताहरूको लागि प्रतिनिधित्व गर्दछ, साथै सम्पर्क र प्रणाली अनुकूलनका लागि प्रयोग गरिने विवरणहरू।',\n    'profile_view_public' => 'सार्वजनिक प्रोफाइल हेर्नुहोस्',\n    'profile_name_desc' => 'तपाईंको प्रदर्शन नाम कन्फिगर गर्नुहोस् जुन प्रणालीमा अन्य प्रयोगकर्ताहरूलाई तपाईंको क्रियाकलाप र स्वामित्व भएको सामग्रीमार्फत देखिनेछ।',\n    'profile_email_desc' => 'यो इमेल सूचनाहरूको लागि प्रयोग हुनेछ र, सक्रिय प्रणाली प्रमाणिकरणमा निर्भर गर्दै, प्रणाली प्रवेशको लागि पनि प्रयोग हुनेछ।',\n    'profile_email_no_permission' => 'दुर्भाग्यवश तपाईंलाई तपाईंको इमेल ठेगाना परिवर्तन गर्ने अनुमति छैन। यदि तपाईं यसलाई परिवर्तन गर्न चाहनुहुन्छ भने, तपाईंलाई एक व्यवस्थापकलाई अनुरोध गर्नु पर्नेछ।',\n    'profile_avatar_desc' => 'तपाईंको प्रतिनिधित्व गर्नको लागि एक छवि चयन गर्नुहोस् जुन प्रणालीमा अन्य प्रयोगकर्ताहरूलाई तपाईंको रूपमा देखाउनेछ। यस छविको आकार वर्गाकार र लगभग २५६px चौडाइ र उचाइ भएको हुनु पर्छ।',\n    'profile_admin_options' => 'व्यवस्थापक विकल्पहरू',\n    'profile_admin_options_desc' => 'अधिकार व्यवस्थापन जस्ता अतिरिक्त व्यवस्थापक-स्तरका विकल्पहरू तपाईंको प्रयोगकर्ता खाता \"सेटिंग्स > प्रयोगकर्ताहरू\" क्षेत्रमा फेला पार्न सकिन्छ।',\n\n    'delete_account' => 'खाता मेटाउनुहोस्',\n    'delete_my_account' => 'मेरो खाता मेटाउनुहोस्',\n    'delete_my_account_desc' => 'यसले तपाईंको प्रयोगकर्ता खाता प्रणालीबाट पूर्ण रूपमा मेटाउनेछ। तपाईं यो खाता पुन: प्राप्त गर्न वा यो क्रियाकलापलाई फर्काउन सक्नुहुन्न। तपाईंले सिर्जना गरेको सामग्री, जस्तै सिर्जना गरिएका पृष्ठहरू र अपलोड गरिएका चित्रहरू, बाँकी रहनेछन्।',\n    'delete_my_account_warning' => 'के तपाईं यो खाता मेटाउन निश्चित हुनुहुन्छ?',\n];\n"
  },
  {
    "path": "lang/ne/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'सेटिङ्ग',\n    'settings_save' => 'सेटिङ्ग सुरक्षित गर्नुहोस्',\n    'system_version' => 'सिस्टम संस्करण',\n    'categories' => 'क्याटोगोरीहरु',\n\n    // App Settings\n    'app_customization' => 'अनुकूलन',\n    'app_features_security' => 'फिचरहरू र सुरक्षा',\n    'app_name' => 'एप्लिकेसन नाम',\n    'app_name_desc' => 'यो नाम हेडरमा र कुनै पनि प्रणालीले पठाएको इमेलमा देखाइनेछ।',\n    'app_name_header' => 'हेडरमा नाम देखाउनुहोस्',\n    'app_public_access' => 'सार्वजनिक पहुँच',\n    'app_public_access_desc' => 'यो विकल्प सक्षम गर्दा, लगइन नगरेका आगन्तुकहरूले तपाईंको BookStack मा सामग्री पहुँच गर्न सक्नेछन्।',\n    'app_public_access_desc_guest' => 'सार्वजनिक आगन्तुकहरूको पहुँच \"Guest\" प्रयोगकर्ताबाट नियन्त्रण गर्न सकिन्छ।',\n    'app_public_access_toggle' => 'सार्वजनिक पहुँच अनुमति दिनुहोस्',\n    'app_public_viewing' => 'सार्वजनिक हेर्न अनुमति दिनुहोस्?',\n    'app_secure_images' => 'उच्च सुरक्षा छवि अपलोडहरू',\n    'app_secure_images_toggle' => 'उच्च सुरक्षा छवि अपलोडहरू सक्षम गर्नुहोस्',\n    'app_secure_images_desc' => 'प्रदर्शन कारणहरूका लागि, सबै छविहरू सार्वजनिक हुन्छन्। यो विकल्पले छवि URL अगाडि एउटा अनियमित, अनुमान गर्न गाह्रो स्ट्रिङ थप्छ। सजिलो पहुँच रोक्न निर्देशिका सूचीकरण निष्क्रिय गर्नुहोस्।',\n    'app_default_editor' => 'डिफल्ट पृष्ठ सम्पादक',\n    'app_default_editor_desc' => 'नयाँ पृष्ठ सम्पादन गर्दा डिफल्ट रूपमा प्रयोग हुने सम्पादक चयन गर्नुहोस्। अनुमति अनुसार पृष्ठ स्तरमा यो परिवर्तन गर्न सकिन्छ।',\n    'app_custom_html' => 'कस्टम HTML हेड सामग्री',\n    'app_custom_html_desc' => 'यहाँ थपिएको कुनै पनि सामग्री प्रत्येक पृष्ठको <head> सेक्सनको तल्लो भागमा समावेश हुनेछ। स्टाइल ओभरराइड वा एनालिटिक्स कोड थप्न उपयोगी।',\n    'app_custom_html_disabled_notice' => 'कस्टम HTML हेड सामग्री यस सेटिङ पृष्ठमा असक्षम गरिएको छ ताकि कुनै समस्या भएमा फर्काउन सकियोस्।',\n    'app_logo' => 'एप्लिकेसन लोगो',\n    'app_logo_desc' => 'यो एप्लिकेसन हेडर बार लगायत अन्य ठाउँहरूमा प्रयोग हुन्छ। यो छवि ८६px उचाइको हुनु पर्नेछ। ठूलो छविहरू सानो गरिनेछ।',\n    'app_icon' => 'एप्लिकेसन आइकन',\n    'app_icon_desc' => 'यो आइकन ब्राउजर ट्याब र छोटोमार्ग आइकनहरूका लागि प्रयोग हुन्छ। PNG २५६px वर्गाकार छवि हुनुपर्छ।',\n    'app_homepage' => 'एप्लिकेसन होमपेज',\n    'app_homepage_desc' => 'डिफल्ट दृश्यको सट्टा होमपेजमा देखाउनको लागि कुनै दृश्य चयन गर्नुहोस्। चयन गरिएका पृष्ठहरूको अनुमति बेवास्ता गरिनेछ।',\n    'app_homepage_select' => 'पृष्ठ चयन गर्नुहोस्',\n    'app_footer_links' => 'फुटर लिंकहरू',\n    'app_footer_links_desc' => 'साइटको फुटरमा देखाउन लिंकहरू थप्नुहोस्। यी प्रायः पृष्ठहरूको तल्लो भागमा देखिनेछन्, जसमा लगइन आवश्यक नभएका पृष्ठहरू पनि समावेश छन्। \"trans::<key>\" ले प्रणाली-परिभाषित अनुवाद प्रयोग गर्न सकिन्छ। उदाहरण: \"trans::common.privacy_policy\" ले \"गोपनीयता नीति\" र \"trans::common.terms_of_service\" ले \"सेवा सर्तहरू\" देखाउनेछ।',\n    'app_footer_links_label' => 'लिंक लेबल',\n    'app_footer_links_url' => 'लिंक URL',\n    'app_footer_links_add' => 'फुटर लिंक थप्नुहोस्',\n    'app_disable_comments' => 'टिप्पणीहरू असक्षम पार्नुहोस्',\n    'app_disable_comments_toggle' => 'टिप्पणीहरू असक्षम पार्नुहोस्',\n    'app_disable_comments_desc' => 'एप्लिकेसनका सबै पृष्ठहरूमा टिप्पणीहरू असक्षम पार्दछ। <br> अस्तित्वमा रहेका टिप्पणीहरू देखाइने छैनन्।',\n\n    // Color settings\n    'color_scheme' => 'एप्लिकेसन रंग योजना',\n    'color_scheme_desc' => 'एप्लिकेसनको प्रयोगकर्ता इन्टरफेसमा प्रयोग हुने रंगहरू सेट गर्नुहोस्। रंगहरू डार्क र लाइट मोडका लागि अलग्गै सेट गर्न सकिन्छ जसले विषयवस्तु र पठनीयता सुधार गर्छ।',\n    'ui_colors_desc' => 'एप्लिकेसनको मुख्य रंग र डिफल्ट लिंक रंग सेट गर्नुहोस्। मुख्य रंग मुख्य रूपमा हेडर ब्यानर, बटनहरू र इन्टरफेस सजावटमा प्रयोग हुन्छ। डिफल्ट लिंक रंग लेखिएको सामग्री र इन्टरफेस दुवैमा प्रयोग हुन्छ।',\n    'app_color' => 'मुख्य रंग',\n    'link_color' => 'डिफल्ट लिंक रंग',\n    'content_colors_desc' => 'पृष्ठ संगठन संरचनाका सबै तत्वहरूका लागि रंग सेट गर्नुहोस्। पठनीयताको लागि डिफल्ट रंगहरूसँग मिल्दोजुल्दो चमक छनौट गर्न सुझाव दिइन्छ।',\n    'bookshelf_color' => 'शेल्फ रंग',\n    'book_color' => 'पुस्तक रंग',\n    'chapter_color' => 'अध्याय रंग',\n    'page_color' => 'पृष्ठ रंग',\n    'page_draft_color' => 'पृष्ठ मसौदा रंग',\n\n    // Registration Settings\n    'reg_settings' => 'दर्ता',\n    'reg_enable' => 'दर्ता सक्षम गर्नुहोस्',\n    'reg_enable_toggle' => 'दर्ता सक्षम गर्नुहोस्',\n    'reg_enable_desc' => 'दर्ता सक्षम हुँदा प्रयोगकर्ताले आफैंलाई एप्लिकेसन प्रयोगकर्ताको रूपमा दर्ता गर्न सक्नेछन्। दर्ता हुँदा तिनीहरूलाई डिफल्ट प्रयोगकर्ता भूमिका दिइन्छ।',\n    'reg_default_role' => 'दर्ता पछि डिफल्ट प्रयोगकर्ता भूमिका',\n    'reg_enable_external_warning' => 'बाह्य LDAP वा SAML प्रमाणीकरण सक्रिय हुँदा माथि उल्लेखित विकल्प बेवास्ता गरिनेछ। प्रमाणीकरण सफल भएमा गैर-अस्तित्व प्रयोगकर्ताका खाताहरू स्वचालित सिर्जना हुनेछ।',\n    'reg_email_confirmation' => 'इमेल पुष्टि',\n    'reg_email_confirmation_toggle' => 'इमेल पुष्टि आवश्यक छ',\n    'reg_confirm_email_desc' => 'यदि डोमेन प्रतिबन्ध प्रयोग गरिएको छ भने इमेल पुष्टि आवश्यक हुनेछ र यो विकल्प बेवास्ता गरिनेछ।',\n    'reg_confirm_restrict_domain' => 'डोमेन प्रतिबन्ध',\n    'reg_confirm_restrict_domain_desc' => 'दर्ता सीमित गर्न चाहनु भएको इमेल डोमेन्सलाई अल्पविरामले छुट्याएर प्रविष्ट गर्नुहोस्। प्रयोगकर्ताहरूलाई ठेगाना पुष्टि गर्न इमेल पठाइनेछ। <br> दर्ता सफल भएपछि प्रयोगकर्ताले इमेल ठेगाना परिवर्तन गर्न सक्नेछन्।',\n    'reg_confirm_restrict_domain_placeholder' => 'कुनै प्रतिबन्ध छैन',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'नयाँ पुस्तकहरूमा लागु गर्न डिफल्ट क्रम नियम चयन गर्नुहोस्। यो अस्तित्वमा रहेका पुस्तकहरूमा असर पार्दैन र पुस्तक अनुसार ओभरराइड गर्न सकिन्छ।',\n    'sorting_rules' => 'क्रम नियमहरू',\n    'sorting_rules_desc' => 'यी पूर्वनिर्धारित क्रम सञ्चालनहरू हुन् जुन प्रणालीमा सामग्रीमा लागू गर्न सकिन्छ।',\n    'sort_rule_assigned_to_x_books' => ':count पुस्तकमा लागू गरिएको|:count पुस्तकहरूमा लागू गरिएको',\n    'sort_rule_create' => 'क्रम नियम सिर्जना गर्नुहोस्',\n    'sort_rule_edit' => 'क्रम नियम सम्पादन गर्नुहोस्',\n    'sort_rule_delete' => 'क्रम नियम मेटाउनुहोस्',\n    'sort_rule_delete_desc' => 'यस क्रम नियमलाई प्रणालीबाट हटाउनुहोस्। यस नियम प्रयोग गरिएका पुस्तकहरू म्यानुअल क्रमबद्धतामा फर्कनेछन्।',\n    'sort_rule_delete_warn_books' => 'यो क्रम नियम हाल :count पुस्तक(हरू) मा प्रयोग भैरहेको छ। के तपाईं पक्का यो मेटाउन चाहनुहुन्छ?',\n    'sort_rule_delete_warn_default' => 'यो क्रम नियम हाल पुस्तकहरूको डिफल्ट रूपमा प्रयोग भैरहेको छ। के तपाईं पक्का यो मेटाउन चाहनुहुन्छ?',\n    'sort_rule_details' => 'क्रम नियम विवरण',\n    'sort_rule_details_desc' => 'यस क्रम नियमको नाम सेट गर्नुहोस्, जुन प्रयोगकर्ताहरूले क्रम छनौट गर्दा सूचिमा देखिनेछ।',\n    'sort_rule_operations' => 'क्रम सञ्चालनहरू',\n    'sort_rule_operations_desc' => 'उपलब्ध सञ्चालनहरूको सूचीबाट क्रम क्रियाकलापहरू सेट गर्नुहोस्। प्रयोग गर्दा, माथिबाट तल सम्म क्रमसँगै लागू गरिनेछ। यहाँ गरिएको कुनै पनि परिवर्तन सुरक्षित गर्दा सबै लागू पुस्तकहरूमा लागु हुनेछ।',\n    'sort_rule_available_operations' => 'उपलब्ध सञ्चालनहरू',\n    'sort_rule_available_operations_empty' => 'कोही सञ्चालन बाँकी छैनन्',\n    'sort_rule_configured_operations' => 'कन्फिगर गरिएको सञ्चालनहरू',\n    'sort_rule_configured_operations_empty' => '\"उपलब्ध सञ्चालनहरू\" सूचीबाट सञ्चालनहरू तान्नुहोस्/थप्नुहोस्',\n    'sort_rule_op_asc' => '(Ascending)',\n    'sort_rule_op_desc' => '(Descending)',\n    'sort_rule_op_name' => 'नाम - वर्णानुक्रम',\n    'sort_rule_op_name_numeric' => 'नाम - सङ्ख्यात्मक',\n    'sort_rule_op_created_date' => 'सिर्जना मिति',\n    'sort_rule_op_updated_date' => 'अपडेट मिति',\n    'sort_rule_op_chapters_first' => 'पहिले अध्यायहरू',\n    'sort_rule_op_chapters_last' => 'अन्त्यमा अध्यायहरू',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'सम्भार',\n    'maint_image_cleanup' => 'छविहरू सफा गर्नुहोस्',\n    'maint_image_cleanup_desc' => 'पृष्ठ र संस्करण सामग्री स्क्यान गरी कुन छविहरू र चित्रहरू प्रयोगमा छन् र कुनहरू अनावश्यक छन् जाँच गर्दछ। यो सञ्चालन अघि पूर्ण डाटाबेस र छवि ब्याकअप बनाउनुहोस्।',\n    'maint_delete_images_only_in_revisions' => 'पुराना पृष्ठ संस्करणहरूमा मात्र रहेका छविहरू पनि मेटाउनुहोस्',\n    'maint_image_cleanup_run' => 'सफा गर्ने प्रक्रिया सुरु गर्नुहोस्',\n    'maint_image_cleanup_warning' => ':count सम्भावित अप्रयुक्त छविहरू फेला परे। के तपाईं पक्का यी छविहरू मेटाउन चाहनुहुन्छ?',\n    'maint_image_cleanup_success' => ':count सम्भावित अप्रयुक्त छविहरू फेला परे र मेटाइयो!',\n    'maint_image_cleanup_nothing_found' => 'कुनै अप्रयुक्त छवि फेला परेन, केही मेटाइएन!',\n    'maint_send_test_email' => 'परीक्षण इमेल पठाउनुहोस्',\n    'maint_send_test_email_desc' => 'यो तपाईको प्रोफाइलमा दिइएको इमेल ठेगानामा परीक्षण इमेल पठाउँछ।',\n    'maint_send_test_email_run' => 'परीक्षण इमेल पठाउनुहोस्',\n    'maint_send_test_email_success' => 'इमेल पठाइयो :address',\n    'maint_send_test_email_mail_subject' => 'परीक्षण इमेल',\n    'maint_send_test_email_mail_greeting' => 'इमेल वितरण सफल देखिन्छ!',\n    'maint_send_test_email_mail_text' => 'बधाई छ! तपाईंले यो इमेल प्राप्त गर्नुभएकोले तपाईका इमेल सेटिङहरू ठीकसँग कन्फिगर भएका छन्।',\n    'maint_recycle_bin_desc' => 'मेटाइएका शेल्फ, पुस्तक, अध्याय र पृष्ठहरू रीसायकल बिनमा पठाइन्छ जसबाट पुनर्स्थापना वा स्थायी मेटाई गर्न सकिन्छ। पुराना वस्तुहरू प्रणाली कन्फिगरेसन अनुसार स्वचालित रूपमा हटाउन सकिन्छ।',\n    'maint_recycle_bin_open' => 'रीसायकल बिन खोल्नुहोस्',\n    'maint_regen_references' => 'सन्दर्भहरू पुनः उत्पन्न गर्नुहोस्',\n    'maint_regen_references_desc' => 'यो क्रियाले डाटाबेस भित्र वस्तुहरू बीचको सन्दर्भ सूचकांक पुनः बनाउँछ। सामान्यतया यो स्वतः हुन्छ, तर पुराना वा अनअधिकारिक विधिबाट थपिएको सामग्रीलाई सूचीकृत गर्न उपयोगी हुन्छ।',\n    'maint_regen_references_success' => 'सन्दर्भ सूचकांक पुनः उत्पन्न गरियो!',\n    'maint_timeout_command_note' => 'सूचना: यो क्रियामा समय लाग्न सक्छ जसले केही वेब वातावरणहरूमा टाइमआउट समस्या ल्याउन सक्छ। विकल्पको रूपमा टर्मिनल कमाण्ड प्रयोग गरेर गर्न सकिन्छ।',\n\n    // Recycle Bin\n    'recycle_bin' => 'रीसायकल बिन',\n    'recycle_bin_desc' => 'यहाँ तपाईंले मेटाइएका वस्तुहरू पुनर्स्थापना गर्न वा प्रणालीबाट स्थायी रूपमा हटाउन सक्नुहुन्छ। यो सूची प्रणालीका अन्य गतिविधि सूचिहरू जस्तो फिल्टर नभएको छ।',\n    'recycle_bin_deleted_item' => 'मेटाइएको वस्तु',\n    'recycle_bin_deleted_parent' => 'मूल',\n    'recycle_bin_deleted_by' => 'मेटाउने व्यक्ति',\n    'recycle_bin_deleted_at' => 'मेटाइने समय',\n    'recycle_bin_permanently_delete' => 'स्थायी रूपमा मेटाउनुहोस्',\n    'recycle_bin_restore' => 'पुन: भण्डारण गर्नुहोस्',\n    'recycle_bin_contents_empty' => 'रिसायकल बिन हाल खाली छ',\n    'recycle_bin_empty' => 'रिसायकल बिन खाली गर्नुहोस्',\n    'recycle_bin_empty_confirm' => 'यसले रिसायकल बिनभित्रका सबै वस्तुहरू र तिनीहरूको सामग्री स्थायी रूपमा मेटाउनेछ। के तपाईं पक्का खाली गर्न चाहनुहुन्छ?',\n    'recycle_bin_destroy_confirm' => 'यस क्रियाले यो वस्तु र तल सूचीबद्ध कुनै पनि सन्तान तत्वहरूलाई स्थायी रूपमा प्रणालीबाट मेटाउनेछ र तपाईंले पुनः प्राप्त गर्न सक्नुहुने छैन। के तपाईं पक्का स्थायी रूपमा मेटाउन चाहनुहुन्छ?',\n    'recycle_bin_destroy_list' => 'मेटाइने वस्तुहरू',\n    'recycle_bin_restore_list' => 'पुनर्स्थापना गरिने वस्तुहरू',\n    'recycle_bin_restore_confirm' => 'यो क्रियाले मेटाइएको वस्तु र कुनै पनि सन्तान तत्वहरूलाई मूल स्थानमा पुनर्स्थापना गर्नेछ। यदि मूल स्थान पनि मेटाइएको छ र रिसायकल बिनमा छ भने मूल वस्तुलाई पनि पुनर्स्थापना गर्नुपर्नेछ।',\n    'recycle_bin_restore_deleted_parent' => 'यस वस्तुको मूल पनि मेटाइएको छ। मूल वस्तु पुनर्स्थापित नभएसम्म यो वस्तु मेटिएको नै रहनेछ।',\n    'recycle_bin_restore_parent' => 'मूल पुनर्स्थापना गर्नुहोस्',\n    'recycle_bin_destroy_notification' => 'रिसायकल बिनबाट कुल :count वस्तुहरू मेटाइयो।',\n    'recycle_bin_restore_notification' => 'रिसायकल बिनबाट कुल :count वस्तुहरू पुनर्स्थापित गरियो।',\n\n    // Audit Log\n    'audit' => 'अडिट लग',\n    'audit_desc' => 'यो अडिट लग प्रणालीमा ट्र्याक गरिएका गतिविधिहरूको सूची देखाउँछ। यो सूची प्रणालीका समान गतिविधि सूचीहरू भन्दा फरक फिल्टररहित हुन्छ।',\n    'audit_event_filter' => 'घटना फिल्टर',\n    'audit_event_filter_no_filter' => 'फिल्टर छैन',\n    'audit_deleted_item' => 'मेटाइएको वस्तु',\n    'audit_deleted_item_name' => 'नाम: :name',\n    'audit_table_user' => 'प्रयोगकर्ता',\n    'audit_table_event' => 'घटना',\n    'audit_table_related' => 'सम्बन्धित वस्तु वा विवरण',\n    'audit_table_ip' => 'IP ठेगाना',\n    'audit_table_date' => 'गतिविधि मिति',\n    'audit_date_from' => 'मिति दायरा सुरु',\n    'audit_date_to' => 'मिति दायरा अन्त्य',\n\n    // Role Settings\n    'roles' => 'भूमिकाहरू',\n    'role_user_roles' => 'प्रयोगकर्ता भूमिका',\n    'roles_index_desc' => 'भूमिकाहरू प्रयोगकर्ताहरूलाई समूहमा राख्न र उनीहरूको सदस्यलाई प्रणाली अनुमति दिन प्रयोग हुन्छ। यदि कुनै प्रयोगकर्ता धेरै भूमिका मा छ भने तिनका अधिकारहरू जोडिनेछन् र सबै क्षमता प्राप्त हुनेछन्।',\n    'roles_x_users_assigned' => ':count प्रयोगकर्तालाई भूमिका दिइयो|:count प्रयोगकर्ताहरूलाई भूमिका दिइयो',\n    'roles_x_permissions_provided' => ':count अनुमति दिइयो|:count अनुमति दिइयो',\n    'roles_assigned_users' => 'दिइएका प्रयोगकर्ताहरू',\n    'roles_permissions_provided' => 'दिइएका अनुमति',\n    'role_create' => 'नयाँ भूमिका सिर्जना गर्नुहोस्',\n    'role_delete' => 'भूमिका मेटाउनुहोस्',\n    'role_delete_confirm' => 'यसले \\':roleName\\' नामको भूमिका मेटाउनेछ।',\n    'role_delete_users_assigned' => 'यस भूमिकामा :userCount प्रयोगकर्ता छन्। यदि तपाईंले यी प्रयोगकर्ताहरूलाई अर्को भूमिकामा सार्न चाहनुहुन्छ भने तल नयाँ भूमिका चयन गर्नुहोस्।',\n    'role_delete_no_migration' => \"प्रयोगकर्ताहरू सार्नु हुँदैन\",\n    'role_delete_sure' => 'के तपाईं पक्का यो भूमिका मेटाउन चाहनुहुन्छ?',\n    'role_edit' => 'भूमिका सम्पादन गर्नुहोस्',\n    'role_details' => 'भूमिका विवरण',\n    'role_name' => 'भूमिका नाम',\n    'role_desc' => 'भूमिकाको संक्षिप्त विवरण',\n    'role_mfa_enforced' => 'बहु-फ्याक्टर प्रमाणीकरण आवश्यक',\n    'role_external_auth_id' => 'बाह्य प्रमाणीकरण ID हरू',\n    'role_system' => 'प्रणाली अनुमति',\n    'role_manage_users' => 'प्रयोगकर्ताहरू व्यवस्थापन गर्नुहोस्',\n    'role_manage_roles' => 'भूमिका र अनुमति व्यवस्थापन गर्नुहोस्',\n    'role_manage_entity_permissions' => 'सबै पुस्तक, अध्याय र पृष्ठ अनुमति व्यवस्थापन गर्नुहोस्',\n    'role_manage_own_entity_permissions' => 'आफ्नो पुस्तक, अध्याय र पृष्ठ अनुमति व्यवस्थापन गर्नुहोस्',\n    'role_manage_page_templates' => 'पृष्ठ टेम्प्लेट व्यवस्थापन गर्नुहोस्',\n    'role_access_api' => 'प्रणाली API पहुँच',\n    'role_manage_settings' => 'एप सेटिङ व्यवस्थापन गर्नुहोस्',\n    'role_export_content' => 'सामग्री निर्यात गर्नुहोस्',\n    'role_import_content' => 'सामग्री आयात गर्नुहोस्',\n    'role_editor_change' => 'पृष्ठ सम्पादक परिवर्तन गर्नुहोस्',\n    'role_notifications' => 'सूचनाहरू प्राप्त र व्यवस्थापन गर्नुहोस्',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'संपत्ति अनुमति',\n    'roles_system_warning' => 'माथिका कुनै पनि तीन अनुमति प्रयोगकर्ताले आफैं वा अरूका अधिकार परिवर्तन गर्न सक्छन्। यी अनुमति भएको भूमिका मात्र भरपर्दो प्रयोगकर्तालाई दिनुहोस्।',\n    'role_asset_desc' => 'यी अनुमतिले प्रणालीभित्र सम्पत्तिमा डिफल्ट पहुँच नियन्त्रण गर्छ। पुस्तक, अध्याय र पृष्ठमा अनुमति यी भन्दा प्राथमिक हुन्छ।',\n    'role_asset_admins' => 'प्रशासनकर्ताहरूलाई सबै सामग्रीमा स्वतः पहुँच दिइन्छ, यी विकल्पहरूले UI मा देखिने वा लुकेका विकल्पहरू मात्र प्रभाव पार्न सक्छ।',\n    'role_asset_image_view_note' => 'यो छवि व्यवस्थापक भित्रको दृश्यता सम्बन्धि हो। अपलोड गरिएको छविमा वास्तविक पहुँच प्रणालीको छवि भण्डारण विकल्प अनुसार हुन्छ।',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'सबै',\n    'role_own' => 'आफ्नो',\n    'role_controlled_by_asset' => 'अपलोड गरिएको सम्पत्तिले नियन्त्रण गरेको',\n    'role_save' => 'भूमिका सुरक्षित गर्नुहोस्',\n    'role_users' => 'यस भूमिकाका प्रयोगकर्ताहरू',\n    'role_users_none' => 'यो भूमिकामा हाल कुनै प्रयोगकर्ता छैन',\n\n    // Users\n    'users' => 'प्रयोगकर्ताहरू',\n    'users_index_desc' => 'प्रणालीमा व्यक्तिगत प्रयोगकर्ता खाता सिर्जना र व्यवस्थापन गर्नुहोस्। प्रयोगकर्ता खाता लगइन र सामग्री तथा गतिविधि जिम्मेवारीका लागि प्रयोग हुन्छ। पहुँच अनुमतिहरू मुख्यतया भूमिकामा आधारित छन् तर प्रयोगकर्ताको सामग्री स्वामित्वले पनि असर गर्न सक्छ।',\n    'user_profile' => 'प्रयोगकर्ता प्रोफाइल',\n    'users_add_new' => 'नयाँ प्रयोगकर्ता थप्नुहोस्',\n    'users_search' => 'प्रयोगकर्ताहरू खोज्नुहोस्',\n    'users_latest_activity' => 'हालैको गतिविधि',\n    'users_details' => 'प्रयोगकर्ता विवरण',\n    'users_details_desc' => 'यस प्रयोगकर्ताको प्रदर्शन नाम र इमेल ठेगाना सेट गर्नुहोस्। इमेल ठेगाना लगइनका लागि प्रयोग हुनेछ।',\n    'users_details_desc_no_email' => 'यो प्रयोगकर्तालाई अरूले चिन्नेगरी प्रदर्शन नाम सेट गर्नुहोस्।',\n    'users_role' => 'प्रयोगकर्ता भूमिका',\n    'users_role_desc' => 'यो प्रयोगकर्तालाई दिइने भूमिका चयन गर्नुहोस्। प्रयोगकर्ताले धेरै भूमिका पाएमा सबै भूमिका अधिकारहरू जोडिनेछन्।',\n    'users_password' => 'प्रयोगकर्ता पासवर्ड',\n    'users_password_desc' => 'लगइनका लागि कम्तिमा ८ वर्ण लामो पासवर्ड सेट गर्नुहोस्।',\n    'users_send_invite_text' => 'तपाईं यो प्रयोगकर्तालाई निमन्त्रणा इमेल पठाउन सक्नुहुन्छ जसले उनीहरूलाई आफ्नै पासवर्ड सेट गर्न अनुमति दिन्छ, नभए तपाईंले आफैं पासवर्ड सेट गर्न सक्नुहुन्छ।',\n    'users_send_invite_option' => 'प्रयोगकर्तालाई निमन्त्रणा इमेल पठाउनुहोस्',\n    'users_external_auth_id' => 'बाह्य प्रमाणीकरण ID',\n    'users_external_auth_id_desc' => 'जब बाह्य प्रमाणीकरण प्रणाली प्रयोग हुन्छ (जस्तै SAML2, OIDC, LDAP), यो ID ले यो BookStack प्रयोगकर्तालाई सम्बन्धित प्रणाली खातासँग जोड्छ। सामान्य इमेल प्रमाणीकरणमा यो फिल्ड आवश्यक छैन।',\n    'users_password_warning' => 'यो प्रयोगकर्ताको पासवर्ड परिवर्तन गर्न मात्र तल भर्नुहोस्।',\n    'users_system_public' => 'यो प्रयोगकर्ता कुनै पनि पाहुना प्रयोगकर्तालाई प्रतिनिधित्व गर्दछ। यसले लगइन गर्न सक्दैन तर स्वतः दिइन्छ।',\n    'users_delete' => 'प्रयोगकर्ता मेटाउनुहोस्',\n    'users_delete_named' => ':userName प्रयोगकर्ता मेटाउनुहोस्',\n    'users_delete_warning' => 'यसले \\':userName\\' नामको प्रयोगकर्तालाई प्रणालीबाट पूर्ण रूपमा मेटाउनेछ।',\n    'users_delete_confirm' => 'के तपाईं पक्का यो प्रयोगकर्ता मेटाउन चाहनुहुन्छ?',\n    'users_migrate_ownership' => 'स्वामित्व सार्नुहोस्',\n    'users_migrate_ownership_desc' => 'यहाँ अर्को प्रयोगकर्ता चयन गर्नुहोस् जसले यस प्रयोगकर्ताका सबै वस्तुहरूको स्वामित्व पाओस्।',\n    'users_none_selected' => 'कुनै प्रयोगकर्ता चयन गरिएको छैन',\n    'users_edit' => 'प्रयोगकर्ता सम्पादन गर्नुहोस्',\n    'users_edit_profile' => 'प्रोफाइल सम्पादन गर्नुहोस्',\n    'users_avatar' => 'प्रयोगकर्ता अवतार',\n    'users_avatar_desc' => 'यो प्रयोगकर्तालाई प्रतिनिधित्व गर्न एउटा चित्र चयन गर्नुहोस्। करिब २५६px वर्गाकार हुनु पर्छ।',\n    'users_preferred_language' => 'रुचाइको भाषा',\n    'users_preferred_language_desc' => 'यस विकल्पले एपको यूजर-इन्टरफेसको भाषा परिवर्तन गर्नेछ। प्रयोगकर्ताले सिर्जना गरेको सामग्रीमा असर पार्दैन।',\n    'users_social_accounts' => 'सामाजिक खाता',\n    'users_social_accounts_desc' => 'यो प्रयोगकर्ताका जडित सामाजिक खाताहरूको स्थिति हेर्नुहोस्। सामाजिक खाताहरू प्रमाणीकरणका लागि प्राथमिक प्रणालीसँगै प्रयोग गर्न सकिन्छ।',\n    'users_social_accounts_info' => 'यहाँ तपाईं आफ्नो अन्य खाताहरू छिटो र सजिलो लगइनका लागि जोड्न सक्नुहुन्छ। यहाँबाट खाता डिस्कनेक्ट गर्दा पूर्व अनुमति रद्द हुँदैन। अनुमति रद्द गर्न सामाजिक खाताको सेटिङ प्रयोग गर्नुहोस्।',\n    'users_social_connect' => 'खाता जडान गर्नुहोस्',\n    'users_social_disconnect' => 'खाता डिस्कनेक्ट गर्नुहोस्',\n    'users_social_status_connected' => 'जडान गरिएको',\n    'users_social_status_disconnected' => 'डिस्कनेक्ट गरिएको',\n    'users_social_connected' => ':socialAccount खाता सफलतापूर्वक प्रोफाइलमा जोडियो।',\n    'users_social_disconnected' => ':socialAccount खाता सफलतापूर्वक प्रोफाइलबाट हटाइयो।',\n    'users_api_tokens' => 'API टोकनहरू',\n    'users_api_tokens_desc' => 'BookStack REST API सँग प्रमाणीकरण गर्न प्रयोग गरिने पहुँच टोकनहरू सिर्जना र व्यवस्थापन गर्नुहोस्। API अनुमतिहरू टोकनधारक प्रयोगकर्ताबाट व्यवस्थापन हुन्छ।',\n    'users_api_tokens_none' => 'यस प्रयोगकर्ताका लागि कुनै API टोकन सिर्जना गरिएको छैन',\n    'users_api_tokens_create' => 'टोकन सिर्जना गर्नुहोस्',\n    'users_api_tokens_expires' => 'म्याद समाप्त',\n    'users_api_tokens_docs' => 'API कागजातहरू',\n    'users_mfa' => 'बहु-फ्याक्टर प्रमाणीकरण',\n    'users_mfa_desc' => 'तपाईंको प्रयोगकर्ता खाताको लागि थप सुरक्षा तहको रूपमा बहु-फ्याक्टर प्रमाणीकरण सेटअप गर्नुहोस्।',\n    'users_mfa_x_methods' => ':count विधि सेटअप गरिएको|:count विधिहरू सेटअप गरिएको',\n    'users_mfa_configure' => 'विधिहरू सेटअप गर्नुहोस्',\n\n    // API Tokens\n    'user_api_token_create' => 'API टोकन सिर्जना गर्नुहोस्',\n    'user_api_token_name' => 'नाम',\n    'user_api_token_name_desc' => 'यो टोकनको उद्देश्य सम्झनको लागि भविष्यमा सम्झन सकिने नाम दिनुहोस्।',\n    'user_api_token_expiry' => 'म्याद समाप्ति मिति',\n    'user_api_token_expiry_desc' => 'यो टोकनको म्याद समाप्त हुने मिति सेट गर्नुहोस्। यस मितिपछि, यस टोकनको प्रयोग गरेर गरिएका अनुरोधहरू काम गर्दैनन्। यो फिल्ड खाली छोड्दा भविष्यमा १०० वर्षको म्याद सेट हुनेछ।',\n    'user_api_token_create_secret_message' => 'यो टोकन सिर्जना गरेपछि \"Token ID\" र \"Token Secret\" जनरेट र प्रदर्शन गरिनेछ। यो गोप्य जानकारी एक पटक मात्र देखाइनेछ, त्यसैले कृपया यसलाई सुरक्षित स्थानमा प्रतिलिपि गर्नुहोस् र त्यसपछि मात्र अगाडि बढ्नुहोस्।',\n    'user_api_token' => 'API टोकन',\n    'user_api_token_id' => 'टोकन ID',\n    'user_api_token_id_desc' => 'यो टोकनको लागि प्रणालीद्वारा उत्पन्न गरिएको अ-सम्पादनयोग्य पहिचान हो, जुन API अनुरोधहरूमा प्रदान गर्न आवश्यक हुनेछ।',\n    'user_api_token_secret' => 'टोकन गोप्य जानकारी',\n    'user_api_token_secret_desc' => 'यो टोकनको लागि प्रणालीद्वारा उत्पन्न गरिएको गोप्य जानकारी हो, जुन API अनुरोधहरूमा प्रदान गर्न आवश्यक हुनेछ। यसलाई केवल एक पटक मात्र देखाइनेछ, त्यसैले कृपया यसलाई सुरक्षित स्थानमा प्रतिलिपि गर्नुहोस्।',\n    'user_api_token_created' => 'टोकन सिर्जना भएको :timeAgo',\n    'user_api_token_updated' => 'टोकन अपडेट भएको :timeAgo',\n    'user_api_token_delete' => 'टोकन मेटाउनुहोस्',\n    'user_api_token_delete_warning' => 'यसले \\':tokenName\\' नामको API टोकनलाई पूर्ण रूपमा प्रणालीबाट मेटाउनेछ।',\n    'user_api_token_delete_confirm' => 'के तपाईं पक्का यो API टोकन मेटाउन चाहनुहुन्छ?',\n\n    // Webhooks\n    'webhooks' => 'वेबहुक्स',\n    'webhooks_index_desc' => 'वेबहुक्स भनेको प्रणाली भित्रका केही क्रियाकलाप र घटनाहरू हुँदा बाह्य URL हरूमा डेटा पठाउने विधि हो, जसले बाह्य प्लेटफर्महरूसँग जस्तै सन्देश वा सूचनासम्बन्धी सिस्टमहरूसँग घटनामा आधारित एकीकरणलाई अनुमति दिन्छ।',\n    'webhooks_x_trigger_events' => ':count ट्रिगर घटना|:count ट्रिगर घटनाहरू',\n    'webhooks_create' => 'नयाँ वेबहुक सिर्जना गर्नुहोस्',\n    'webhooks_none_created' => 'अझै कुनै वेबहुक सिर्जना गरिएको छैन।',\n    'webhooks_edit' => 'वेबहुक सम्पादन गर्नुहोस्',\n    'webhooks_save' => 'वेबहुक बचत गर्नुहोस्',\n    'webhooks_details' => 'वेबहुक विवरण',\n    'webhooks_details_desc' => 'एक प्रयोगकर्ता मैत्री नाम र एक POST इन्डप्वाइंट दिनुहोस् जसलाई वेबहुकको डेटा पठाइने स्थानको रूपमा प्रयोग हुनेछ।',\n    'webhooks_events' => 'वेबहुक घटनाहरू',\n    'webhooks_events_desc' => 'यी घटनाहरू चयन गर्नुहोस् जसले यो वेबहुकलाई ट्रिगर गर्नुपर्नेछ।',\n    'webhooks_events_warning' => 'ध्यान दिनुहोस् कि यी घटनाहरू चयन गरेपछि सबै चयन गरिएका घटनाहरूको लागि वेबहुक ट्रिगर हुनेछ, भले नै कस्टम अनुमतिहरू लागू गरिएका छन्। यो वेबहुक प्रयोग गर्दा गोपनीय सामग्रीको जोखिम नहोस् भन्ने कुरा सुनिश्चित गर्नुहोस्।',\n    'webhooks_events_all' => 'सिस्टमका सबै घटनाहरू',\n    'webhooks_name' => 'वेबहुक नाम',\n    'webhooks_timeout' => 'वेबहुक अनुरोध म्याद समाप्ति (सेकेन्ड)',\n    'webhooks_endpoint' => 'वेबहुक इन्डप्वाइंट',\n    'webhooks_active' => 'वेबहुक सक्रिय',\n    'webhook_events_table_header' => 'घटनाहरू',\n    'webhooks_delete' => 'वेबहुक मेटाउनुहोस्',\n    'webhooks_delete_warning' => 'यसले \\':webhookName\\' नामको वेबहुकलाई प्रणालीबाट पूर्ण रूपमा मेटाउनेछ।',\n    'webhooks_delete_confirm' => 'के तपाईं पक्का यो वेबहुक मेटाउन चाहनुहुन्छ?',\n    'webhooks_format_example' => 'वेबहुक ढाँचाको उदाहरण',\n    'webhooks_format_example_desc' => 'वेबहुक डेटा POST अनुरोधको रूपमा JSON ढाँचामा निर्धारित इन्डप्वाइंटमा पठाइन्छ। \"related_item\" र \"url\" गुणहरू वैकल्पिक छन् र यो ट्रिगर गरिएको घटनाको प्रकारमा निर्भर गर्नेछ।',\n    'webhooks_status' => 'वेबहुक स्थिति',\n    'webhooks_last_called' => 'अन्तिम पटक कल गरिएको: ',\n    'webhooks_last_errored' => 'अन्तिम पटक एरर भएको: ',\n    'webhooks_last_error_message' => 'अन्तिम एरर सन्देश: ',\n\n    // Licensing\n    'licenses' => 'लाइसन्स',\n    'licenses_desc' => 'यस पृष्ठमा BookStack को लाइसेन्स जानकारी र BookStack भित्र प्रयोग भएका परियोजना र पुस्तकालयहरूको जानकारी दिइएको छ। सूचीबद्ध भएका धेरै परियोजनाहरूले केवल विकासको सन्दर्भमा मात्र प्रयोग गर्न सकिन्छ।',\n    'licenses_bookstack' => 'BookStack लाइसेन्स',\n    'licenses_php' => 'PHP पुस्तकालय लाइसेन्स',\n    'licenses_js' => 'JavaScript पुस्तकालय लाइसेन्स',\n    'licenses_other' => 'अन्य लाइसेन्स',\n    'license_details' => 'लाइसेन्स विवरण',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/ne/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute स्वीकार गर्नुपर्छ।',\n    'active_url'           => ':attribute मान्य URL होइन।',\n    'after'                => ':attribute मिति :date पछिको हुनुपर्छ।',\n    'alpha'                => ':attribute मा अक्षर मात्र हुनुपर्छ।',\n    'alpha_dash'           => ':attribute मा अक्षर, अंक, ड्यास (-) र अन्डरस्कोर (_) मात्र हुनुपर्छ।',\n    'alpha_num'            => ':attribute मा अक्षर र अंक मात्र हुनुपर्छ।',\n    'array'                => ':attribute array हुनुपर्छ।',\n    'backup_codes'         => 'दिइएको कोड गलत छ वा पहिल्यै प्रयोग भइसकेको छ।',\n    'before'               => ':attribute मिति :date भन्दा पहिला हुनुपर्छ।',\n    'between'              => [\n        'numeric' => ':attribute :min देखि :max बीचमा हुनुपर्छ।',\n        'file'    => ':attribute :min देखि :max किलोबाइट बीचमा हुनुपर्छ।',\n        'string'  => ':attribute :min देखि :max क्यारेक्टरबीच हुनुपर्छ।',\n        'array'   => ':attribute मा :min देखि :max वस्तुहरू हुनुपर्छ।',\n    ],\n    'boolean'              => ':attribute साँचो (true) वा झूटो (false) हुनुपर्छ।',\n    'confirmed'            => ':attribute पुष्टि मिलेन।',\n    'date'                 => ':attribute मान्य मिति होइन।',\n    'date_format'          => ':attribute ढाँचा :format सँग मेल खाँदैन।',\n    'different'            => ':attribute र :other फरक हुनुपर्छ।',\n    'digits'               => ':attribute मा ठीक :digits अंक हुनुपर्छ।',\n    'digits_between'       => ':attribute मा :min देखि :max अंक हुनुपर्छ।',\n    'email'                => ':attribute मान्य ईमेल ठेगाना हुनुपर्छ।',\n    'ends_with' => ':attribute यी मध्ये एकले अन्त्य हुनुपर्छ: :values',\n    'file'                 => ':attribute मान्य फाइल हुनुपर्छ।',\n    'filled'               => ':attribute आवश्यक छ।',\n    'gt'                   => [\n        'numeric' => ':attribute :value भन्दा बढी हुनुपर्छ।',\n        'file'    => ':attribute :value किलोबाइटभन्दा बढी हुनुपर्छ।',\n        'string'  => ':attribute :value क्यारेक्टरभन्दा बढी हुनुपर्छ।',\n        'array'   => ':attribute मा :value भन्दा बढी वस्तुहरू हुनुपर्छ।',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute :value भन्दा बढी वा बराबर हुनुपर्छ।',\n        'file'    => ':attribute :value किलोबाइटभन्दा बढी वा बराबर हुनुपर्छ।',\n        'string'  => ':attribute :value क्यारेक्टरभन्दा बढी वा बराबर हुनुपर्छ।',\n        'array'   => ':attribute मा कम्तीमा :value वस्तुहरू हुनुपर्छ।',\n    ],\n    'exists'               => 'चयन गरिएको :attribute अमान्य छ।',\n    'image'                => ':attribute एउटा तस्बिर हुनुपर्छ।',\n    'image_extension'      => ':attribute मा मान्य र समर्थित तस्बिर विस्तार (extension) हुनुपर्छ।',\n    'in'                   => 'चयन गरिएको :attribute अमान्य छ।',\n    'integer'              => ':attribute पूर्णांक (integer) हुनुपर्छ।',\n    'ip'                   => ':attribute मान्य IP ठेगाना हुनुपर्छ।',\n    'ipv4'                 => ':attribute मान्य IPv4 ठेगाना हुनुपर्छ।',\n    'ipv6'                 => ':attribute मान्य IPv6 ठेगाना हुनुपर्छ।',\n    'json'                 => ':attribute मान्य JSON स्ट्रिङ हुनुपर्छ।',\n    'lt'                   => [\n        'numeric' => ':attribute :value भन्दा कम हुनुपर्छ।',\n        'file'    => ':attribute :value किलोबाइटभन्दा कम हुनुपर्छ।',\n        'string'  => ':attribute :value क्यारेक्टरभन्दा कम हुनुपर्छ।',\n        'array'   => ':attribute मा :value भन्दा कम वस्तुहरू हुनुपर्छ।',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute :value भन्दा कम वा बराबर हुनुपर्छ।',\n        'file'    => ':attribute :value किलोबाइटभन्दा कम वा बराबर हुनुपर्छ।',\n        'string'  => ':attribute :value क्यारेक्टरभन्दा कम वा बराबर हुनुपर्छ।',\n        'array'   => ':attribute मा :value भन्दा बढी वस्तुहरू हुनु हुँदैन।',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute :max भन्दा बढी हुन सक्दैन।',\n        'file'    => ':attribute :max किलोबाइटभन्दा बढी हुन सक्दैन।',\n        'string'  => ':attribute :max क्यारेक्टरभन्दा बढी हुन सक्दैन।',\n        'array'   => ':attribute मा :max भन्दा बढी वस्तुहरू हुनु हुँदैन।',\n    ],\n    'mimes'                => ':attribute फाइलको प्रकार :values हुनुपर्छ।',\n    'min'                  => [\n        'numeric' => ':attribute कम्तीमा :min हुनुपर्छ।',\n        'file'    => ':attribute कम्तीमा :min किलोबाइट हुनुपर्छ।',\n        'string'  => ':attribute कम्तीमा :min क्यारेक्टर हुनुपर्छ।',\n        'array'   => ':attribute मा कम्तीमा :min वस्तुहरू हुनुपर्छ।',\n    ],\n    'not_in'               => 'चयन गरिएको :attribute अमान्य छ।',\n    'not_regex'            => ':attribute को ढाँचा अमान्य छ।',\n    'numeric'              => ':attribute संख्या हुनुपर्छ।',\n    'regex'                => ':attribute ढाँचा अमान्य छ।',\n    'required'             => ':attribute आवश्यक छ।',\n    'required_if'          => ':other :value हुँदा :attribute आवश्यक हुन्छ।',\n    'required_with'        => ':values भएमा :attribute आवश्यक छ।',\n    'required_with_all'    => ':values भएमा :attribute आवश्यक छ।',\n    'required_without'     => ':values नभएमा :attribute आवश्यक छ।',\n    'required_without_all' => ':values मध्ये कुनै पनि नभएमा :attribute आवश्यक छ।',\n    'same'                 => ':attribute र :other मिल्नुपर्छ।',\n    'safe_url'             => 'दिएको लिङ्क सुरक्षित नहुन सक्छ।',\n    'size'                 => [\n        'numeric' => ':attribute ठीक :size हुनुपर्छ।',\n        'file'    => ':attribute ठीक :size किलोबाइट हुनुपर्छ।',\n        'string'  => ':attribute ठीक :size क्यारेक्टर हुनुपर्छ।',\n        'array'   => ':attribute मा ठीक :size वस्तुहरू हुनुपर्छ।',\n    ],\n    'string'               => ':attribute स्ट्रिङ (पाठ) हुनुपर्छ।',\n    'timezone'             => ':attribute मान्य समय क्षेत्र (timezone) हुनुपर्छ।',\n    'totp'                 => 'दिएको कोड गलत छ वा सकिएको छ।',\n    'unique'               => ':attribute पहिल्यै प्रयोग भइसकेको छ।',\n    'url'                  => ':attribute को ढाँचा अमान्य छ।',\n    'uploaded'             => 'फाइल अपलोड हुन सकेन। सर्भरले यस्तो साइज स्वीकार नगर्न सक्छ।',\n\n    'zip_file' => ':attribute ले ZIP फाइलभित्रको फाइल देखाउनु पर्छ।',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => ':attribute मा :validTypes प्रकारको फाइल हुनुपर्छ, तर :foundType भेटियो।',\n    'zip_model_expected' => 'डेटा वस्तु चाहिएको थियो तर \":type\" भेटियो।',\n    'zip_unique' => ':attribute ZIP भित्रको वस्तु प्रकारको लागि अद्वितीय हुनुपर्छ।',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'पासवर्ड पुष्टि आवश्यक छ।',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/nl/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'maakte pagina',\n    'page_create_notification'    => 'Pagina succesvol aangemaakt',\n    'page_update'                 => 'wijzigde pagina',\n    'page_update_notification'    => 'Pagina succesvol bijgewerkt',\n    'page_delete'                 => 'verwijderde pagina',\n    'page_delete_notification'    => 'Pagina succesvol verwijderd',\n    'page_restore'                => 'herstelde pagina',\n    'page_restore_notification'   => 'Pagina succesvol hersteld',\n    'page_move'                   => 'verplaatste pagina',\n    'page_move_notification'      => 'Pagina succesvol verplaatst',\n\n    // Chapters\n    'chapter_create'              => 'maakte hoofdstuk',\n    'chapter_create_notification' => 'Hoofdstuk succesvol aangemaakt',\n    'chapter_update'              => 'wijzigde hoofdstuk',\n    'chapter_update_notification' => 'Hoofdstuk succesvol bijgewerkt',\n    'chapter_delete'              => 'verwijderde hoofdstuk',\n    'chapter_delete_notification' => 'Hoofdstuk succesvol verwijderd',\n    'chapter_move'                => 'verplaatste hoofdstuk',\n    'chapter_move_notification' => 'Hoofdstuk succesvol verplaatst',\n\n    // Books\n    'book_create'                 => 'maakte boek',\n    'book_create_notification'    => 'Boek succesvol aangemaakt',\n    'book_create_from_chapter'              => 'converteerde hoofdstuk naar boek',\n    'book_create_from_chapter_notification' => 'Hoofdstuk succesvol geconverteerd naar een boek',\n    'book_update'                 => 'wijzigde boek',\n    'book_update_notification'    => 'Boek succesvol bijgewerkt',\n    'book_delete'                 => 'verwijderde boek',\n    'book_delete_notification'    => 'Boek succesvol verwijderd',\n    'book_sort'                   => 'sorteerde boek',\n    'book_sort_notification'      => 'Boek succesvol opnieuw gesorteerd',\n\n    // Bookshelves\n    'bookshelf_create'            => 'maakte boekenplank aan',\n    'bookshelf_create_notification'    => 'Boekenplank succesvol aangemaakt',\n    'bookshelf_create_from_book'    => 'converteerde boek naar boekenplank',\n    'bookshelf_create_from_book_notification'    => 'Boek succesvol geconverteerd naar boekenplank',\n    'bookshelf_update'                 => 'werkte boekenplank bij',\n    'bookshelf_update_notification'    => 'Boekenplank succesvol bijgewerkt',\n    'bookshelf_delete'                 => 'verwijderde boekenplank',\n    'bookshelf_delete_notification'    => 'Boekenplank succesvol verwijderd',\n\n    // Revisions\n    'revision_restore' => 'herstelde revisie',\n    'revision_delete' => 'verwijderde revisie',\n    'revision_delete_notification' => 'Revisie succesvol verwijderd',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" is toegevoegd aan je favorieten',\n    'favourite_remove_notification' => '\":name\" is verwijderd uit je favorieten',\n\n    // Watching\n    'watch_update_level_notification' => 'Volg voorkeuren succesvol aangepast',\n\n    // Auth\n    'auth_login' => 'logde in',\n    'auth_register' => 'registreerde als nieuwe gebruiker',\n    'auth_password_reset_request' => 'vraagde een nieuw gebruikerswachtwoord aan',\n    'auth_password_reset_update' => 'stelde gebruikerswachtwoord opnieuw in',\n    'mfa_setup_method' => 'stelde meervoudige verificatie methode in',\n    'mfa_setup_method_notification' => 'Meervoudige verificatie methode succesvol geconfigureerd',\n    'mfa_remove_method' => 'verwijderde meervoudige verificatie methode',\n    'mfa_remove_method_notification' => 'Meervoudige verificatie methode is succesvol verwijderd',\n\n    // Settings\n    'settings_update' => 'werkte instellingen bij',\n    'settings_update_notification' => 'Instellingen succesvol bijgewerkt',\n    'maintenance_action_run' => 'voerde onderhoudsactie uit',\n\n    // Webhooks\n    'webhook_create' => 'maakte webhook aan',\n    'webhook_create_notification' => 'Webhook succesvol aangemaakt',\n    'webhook_update' => 'werkte webhook bij',\n    'webhook_update_notification' => 'Webhook succesvol bijgewerkt',\n    'webhook_delete' => 'verwijderde webhook',\n    'webhook_delete_notification' => 'Webhook succesvol verwijderd',\n\n    // Imports\n    'import_create' => 'maakte import',\n    'import_create_notification' => 'Import succesvol geüpload',\n    'import_run' => 'wijzigde import',\n    'import_run_notification' => 'Inhoud succesvol geïmporteerd',\n    'import_delete' => 'verwijderde import',\n    'import_delete_notification' => 'Import succesvol verwijderd',\n\n    // Users\n    'user_create' => 'maakte gebruiker aan',\n    'user_create_notification' => 'Gebruiker succesvol aangemaakt',\n    'user_update' => 'werkte gebruiker bij',\n    'user_update_notification' => 'Gebruiker succesvol bijgewerkt',\n    'user_delete' => 'verwijderde gebruiker',\n    'user_delete_notification' => 'Gebruiker succesvol verwijderd',\n\n    // API Tokens\n    'api_token_create' => 'API-token aangemaakt',\n    'api_token_create_notification' => 'API-token succesvol aangemaakt',\n    'api_token_update' => 'wijzigde API-token',\n    'api_token_update_notification' => 'API-token succesvol bijgewerkt',\n    'api_token_delete' => 'verwijderde API-token',\n    'api_token_delete_notification' => 'API-token succesvol verwijderd',\n\n    // Roles\n    'role_create' => 'maakte rol aan',\n    'role_create_notification' => 'Rol succesvol aangemaakt',\n    'role_update' => 'werkte rol bij',\n    'role_update_notification' => 'Rol succesvol bijgewerkt',\n    'role_delete' => 'verwijderde rol',\n    'role_delete_notification' => 'Rol succesvol verwijderd',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'leegde prullenbak',\n    'recycle_bin_restore' => 'herstelde van prullenbak',\n    'recycle_bin_destroy' => 'verwijderde van prullenbak',\n\n    // Comments\n    'commented_on'                => 'plaatste opmerking in',\n    'comment_create'              => 'voegde opmerking toe',\n    'comment_update'              => 'paste opmerking aan',\n    'comment_delete'              => 'verwijderde opmerking',\n\n    // Sort Rules\n    'sort_rule_create' => 'maakte soorteerregel',\n    'sort_rule_create_notification' => 'Sorteerregel succesvol aangemaakt',\n    'sort_rule_update' => 'wijzigde sorteerregel',\n    'sort_rule_update_notification' => 'Sorteerregel succesvol bijgewerkt',\n    'sort_rule_delete' => 'verwijderde sorteerregel',\n    'sort_rule_delete_notification' => 'Sorteerregel succesvol verwijderd',\n\n    // Other\n    'permissions_update'          => 'wijzigde machtigingen',\n];\n"
  },
  {
    "path": "lang/nl/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Deze inloggegevens zijn niet bij ons bekend.',\n    'throttle' => 'Te veel inlogpogingen! Probeer het opnieuw na :seconds seconden.',\n\n    // Login & Register\n    'sign_up' => 'Registreer',\n    'log_in' => 'Log in',\n    'log_in_with' => 'Log in met :socialDriver',\n    'sign_up_with' => 'Registreer met :socialDriver',\n    'logout' => 'Log uit',\n\n    'name' => 'Naam',\n    'username' => 'Gebruikersnaam',\n    'email' => 'E-mail',\n    'password' => 'Wachtwoord',\n    'password_confirm' => 'Wachtwoord Bevestigen',\n    'password_hint' => 'Moet uit minstens 8 tekens bestaan',\n    'forgot_password' => 'Wachtwoord vergeten?',\n    'remember_me' => 'Onthoud Mij',\n    'ldap_email_hint' => 'Geef een e-mailadres op voor dit account.',\n    'create_account' => 'Account aanmaken',\n    'already_have_account' => 'Heb je al een account?',\n    'dont_have_account' => 'Nog geen account?',\n    'social_login' => 'Aanmelden via een sociaal netwerk',\n    'social_registration' => 'Registratie via een sociaal netwerk',\n    'social_registration_text' => 'Registreer en log in met een andere service.',\n\n    'register_thanks' => 'Bedankt voor het registreren!',\n    'register_confirm' => 'Controleer je e-mail en klik op de bevestigingsknop om toegang te krijgen tot :appName.',\n    'registrations_disabled' => 'Registratie is momenteel niet mogelijk',\n    'registration_email_domain_invalid' => 'Dit e-maildomein wordt niet toegelaten tot deze applicatie',\n    'register_success' => 'Bedankt voor het aanmelden! Je bent nu geregistreerd en ingelogd.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Proberen in te loggen',\n    'auto_init_starting_desc' => 'We maken contact met je authenticatiesysteem om het inlogproces te starten. Als er na 5 seconden geen vooruitgang is, kun je proberen op de onderstaande link te klikken.',\n    'auto_init_start_link' => 'Ga verder met authenticatie',\n\n    // Password Reset\n    'reset_password' => 'Wachtwoord Herstellen',\n    'reset_password_send_instructions' => 'Geef je e-mailadres op en er wordt een link gestuurd om je wachtwoord te herstellen.',\n    'reset_password_send_button' => 'Stuur Herstel Link',\n    'reset_password_sent' => 'Een wachtwoordherstel-link zal worden verstuurd naar :email als dat e-mailadres in het systeem gevonden is.',\n    'reset_password_success' => 'Je wachtwoord is succesvol hersteld.',\n    'email_reset_subject' => 'Herstel je wachtwoord van :appName',\n    'email_reset_text' => 'Je ontvangt deze e-mail omdat we een wachtwoordherstelverzoek voor uw account hebben ontvangen.',\n    'email_reset_not_requested' => 'Als je geen wachtwoordherstel hebt aangevraagd, hoef je niets te doen.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Bevestig je e-mailadres op :appName',\n    'email_confirm_greeting' => 'Bedankt voor je aanmelding op :appName!',\n    'email_confirm_text' => 'Bevestig je e-mailadres door op onderstaande knop te drukken:',\n    'email_confirm_action' => 'Bevestig je e-mail',\n    'email_confirm_send_error' => 'Een e-mailbevestiging is vereist, maar het systeem kon de e-mail niet verzenden. Neem contact op met de beheerder.',\n    'email_confirm_success' => 'Je e-mailadres is bevestigd! Je zou nu moeten kunnen inloggen met dit e-mailadres.',\n    'email_confirm_resent' => 'Bevestigingsmail opnieuw verzonden, controleer je inbox.',\n    'email_confirm_thanks' => 'Bedankt voor de bevestiging!',\n    'email_confirm_thanks_desc' => 'Wacht even terwijl je bevestiging wordt behandeld. Als je na 3 seconden niet wordt doorverwezen, druk dan op de onderstaande link \"Doorgaan\" om verder te gaan.',\n\n    'email_not_confirmed' => 'E-mailadres nog niet bevestigd',\n    'email_not_confirmed_text' => 'Je e-mailadres is nog niet bevestigd.',\n    'email_not_confirmed_click_link' => 'Klik op de link in de e-mail die vlak na je registratie is verstuurd.',\n    'email_not_confirmed_resend' => 'Als je deze e-mail niet kunt vinden kun je deze met onderstaande formulier opnieuw verzenden.',\n    'email_not_confirmed_resend_button' => 'Bevestigingsmail opnieuw verzenden',\n\n    // User Invite\n    'user_invite_email_subject' => 'Je bent uitgenodigd voor :appName!',\n    'user_invite_email_greeting' => 'Er is een account voor je aangemaakt op :appName.',\n    'user_invite_email_text' => 'Klik op de onderstaande knop om een account wachtwoord in te stellen en toegang te krijgen:',\n    'user_invite_email_action' => 'Account wachtwoord instellen',\n    'user_invite_page_welcome' => 'Welkom bij :appName!',\n    'user_invite_page_text' => 'Om je registratie af te ronden en toegang te krijgen moet je een wachtwoord instellen dat gebruikt zal worden om in te loggen op :appName bij toekomstige bezoeken.',\n    'user_invite_page_confirm_button' => 'Wachtwoord Bevestigen',\n    'user_invite_success_login' => 'Wachtwoord ingesteld, je zou nu moeten kunnen inloggen met je ingestelde wachtwoord om toegang te krijgen tot :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Meervoudige verificatie instellen',\n    'mfa_setup_desc' => 'Stel meervoudige verificatie in als een extra beveiligingslaag voor je gebruikersaccount.',\n    'mfa_setup_configured' => 'Is al geconfigureerd',\n    'mfa_setup_reconfigure' => 'Herconfigureren',\n    'mfa_setup_remove_confirmation' => 'Weet je zeker dat je deze multi-factor authenticatie methode wilt verwijderen?',\n    'mfa_setup_action' => 'Instellen',\n    'mfa_backup_codes_usage_limit_warning' => 'Je hebt minder dan 5 back-upcodes over. Genereer en sla een nieuwe set op voordat je geen codes meer hebt om te voorkomen dat je buiten je account wordt gesloten.',\n    'mfa_option_totp_title' => 'Mobiele app',\n    'mfa_option_totp_desc' => 'Om meervoudige verificatie te gebruiken heb je een mobiele applicatie nodig die TOTP ondersteunt, zoals Google Authenticator, Authy of Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Back-up Codes',\n    'mfa_option_backup_codes_desc' => 'Genereert een set met eenmalige back-upcodes die je kan invoeren om je identiteit te bevestigen. Bewaar deze op een veilige plaats.',\n    'mfa_gen_confirm_and_enable' => 'Bevestigen en inschakelen',\n    'mfa_gen_backup_codes_title' => 'Back-up codes instellen',\n    'mfa_gen_backup_codes_desc' => 'Bewaar de onderstaande lijst met codes op een veilige plaats. Bij toegang tot het systeem kun je een van de codes gebruiken als tweede verificatiemechanisme.',\n    'mfa_gen_backup_codes_download' => 'Download Codes',\n    'mfa_gen_backup_codes_usage_warning' => 'Elke code kan slechts eenmaal gebruikt worden',\n    'mfa_gen_totp_title' => 'Mobiele app installatie',\n    'mfa_gen_totp_desc' => 'Om meervoudige verificatie te gebruiken heb je een mobiele applicatie nodig die TOTP ondersteunt, zoals Google Authenticator, Authy of Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Scan de onderstaande QR-code door gebruik te maken van je favoriete authenticatie-app om aan de slag te gaan.',\n    'mfa_gen_totp_verify_setup' => 'Installatie verifiëren',\n    'mfa_gen_totp_verify_setup_desc' => 'Controleer of alles werkt door het invoeren van een code, die wordt gegenereerd binnen je authenticatie-app, in het onderstaande invoerveld:',\n    'mfa_gen_totp_provide_code_here' => 'Vul je app-gegenereerde code hier in',\n    'mfa_verify_access' => 'Verifieer toegang',\n    'mfa_verify_access_desc' => 'Je moet je identiteit bevestigen via een extra verificatieniveau voordat je toegang krijgt tot je gebruikersaccount. Verifieer met een van de door jou geconfigureerde methoden om verder te gaan.',\n    'mfa_verify_no_methods' => 'Geen methode geconfigureerd',\n    'mfa_verify_no_methods_desc' => 'Er konden geen meervoudige verificatie methoden voor je account gevonden worden. Je zult minstens één methode moeten instellen voordat je toegang krijgt.',\n    'mfa_verify_use_totp' => 'Verifieer met een mobiele app',\n    'mfa_verify_use_backup_codes' => 'Verifieer met een back-up code',\n    'mfa_verify_backup_code' => 'Back-up code',\n    'mfa_verify_backup_code_desc' => 'Voer één van je resterende back-up codes hieronder in:',\n    'mfa_verify_backup_code_enter_here' => 'Voer hier de back-up code in',\n    'mfa_verify_totp_desc' => 'Voer de code, gegenereerd met je mobiele app, hieronder in:',\n    'mfa_setup_login_notification' => 'Meervoudige verificatie methode geconfigureerd, Gelieve opnieuw in te loggen met de geconfigureerde methode.',\n];\n"
  },
  {
    "path": "lang/nl/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Annuleer',\n    'close' => 'Sluit',\n    'confirm' => 'Bevestig',\n    'back' => 'Terug',\n    'save' => 'Opslaan',\n    'continue' => 'Doorgaan',\n    'select' => 'Selecteer',\n    'toggle_all' => 'Wissel Alles',\n    'more' => 'Meer',\n\n    // Form Labels\n    'name' => 'Naam',\n    'description' => 'Beschrijving',\n    'role' => 'Rol',\n    'cover_image' => 'Omslagfoto',\n    'cover_image_description' => 'Deze afbeelding moet ongeveer 440x250 pixels zijn, hoewel deze flexibel zal worden geschaald en bijgesneden naargelang dit nodig is in de verschillende scenario\\'s van de gebruikersinterface. De daadwerkelijk gebruikte afmetingen voor weergave zullen dan verschillen.',\n\n    // Actions\n    'actions' => 'Acties',\n    'view' => 'Bekijk',\n    'view_all' => 'Bekijk Alle',\n    'new' => 'Nieuw',\n    'create' => 'Aanmaken',\n    'update' => 'Bijwerken',\n    'edit' => 'Bewerk',\n    'archive' => 'Archiveer',\n    'unarchive' => 'Terughalen',\n    'sort' => 'Sorteer',\n    'move' => 'Verplaats',\n    'copy' => 'Kopieer',\n    'reply' => 'Beantwoord',\n    'delete' => 'Verwijder',\n    'delete_confirm' => 'Verwijdering bevestigen',\n    'search' => 'Zoek',\n    'search_clear' => 'Zoekopdracht wissen',\n    'reset' => 'Wissen',\n    'remove' => 'Verwijder',\n    'add' => 'Voeg toe',\n    'configure' => 'Configureer',\n    'manage' => 'Beheer',\n    'fullscreen' => 'Volledig scherm',\n    'favourite' => 'Favoriet',\n    'unfavourite' => 'Verwijderen als favoriet',\n    'next' => 'Volgende',\n    'previous' => 'Vorige',\n    'filter_active' => 'Actieve Filter:',\n    'filter_clear' => 'Wis Filter',\n    'download' => 'Download',\n    'open_in_tab' => 'Open als Tabblad',\n    'open' => 'Open',\n\n    // Sort Options\n    'sort_options' => 'Sorteeropties',\n    'sort_direction_toggle' => 'Sorteerrichting',\n    'sort_ascending' => 'Sorteer Oplopend',\n    'sort_descending' => 'Sorteer Aflopend',\n    'sort_name' => 'Naam',\n    'sort_default' => 'Standaard',\n    'sort_created_at' => 'Aanmaakdatum',\n    'sort_updated_at' => 'Bijwerkdatum',\n\n    // Misc\n    'deleted_user' => 'Verwijderde gebruiker',\n    'no_activity' => 'Geen activiteit om weer te geven',\n    'no_items' => 'Geen items beschikbaar',\n    'back_to_top' => 'Terug naar boven',\n    'skip_to_main_content' => 'Spring naar de hoofdinhoud',\n    'toggle_details' => 'Details weergeven',\n    'toggle_thumbnails' => 'Miniaturen weergeven',\n    'details' => 'Details',\n    'grid_view' => 'Grid Weergave',\n    'list_view' => 'Lijstweergave',\n    'default' => 'Standaard',\n    'breadcrumb' => 'Kruimelspoor',\n    'status' => 'Status',\n    'status_active' => 'Actief',\n    'status_inactive' => 'Inactief',\n    'never' => 'Nooit',\n    'none' => 'Geen',\n\n    // Header\n    'homepage' => 'Startpagina',\n    'header_menu_expand' => 'Header menu uitvouwen',\n    'profile_menu' => 'Profiel menu',\n    'view_profile' => 'Profiel weergeven',\n    'edit_profile' => 'Profiel bewerken',\n    'dark_mode' => 'Donkere modus',\n    'light_mode' => 'Lichte modus',\n    'global_search' => 'Algemene zoekopdracht',\n\n    // Layout tabs\n    'tab_info' => 'Info',\n    'tab_info_label' => 'Tabblad: Toon secundaire informatie',\n    'tab_content' => 'Inhoud',\n    'tab_content_label' => 'Tabblad: Toon primaire inhoud',\n\n    // Email Content\n    'email_action_help' => 'Als de knop \":actionText\" niet werkt, kopieer en plak de onderstaande URL in je web browser:',\n    'email_rights' => 'Alle rechten voorbehouden',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Privacybeleid',\n    'terms_of_service' => 'Algemene voorwaarden',\n\n    // OpenSearch\n    'opensearch_description' => 'Zoek in :appName',\n];\n"
  },
  {
    "path": "lang/nl/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Selecteer Afbeelding',\n    'image_list' => 'Afbeeldingslijst',\n    'image_details' => 'Afbeelding details',\n    'image_upload' => 'Upload afbeelding',\n    'image_intro' => 'Hier kan je eerder geüploade afbeeldingen selecteren en beheren.',\n    'image_intro_upload' => 'Sleep een afbeeldingsbestand naar dit venster of gebruik de \"Upload afbeelding\"-knop om een afbeelding te uploaden.',\n    'image_all' => 'Alles',\n    'image_all_title' => 'Alle afbeeldingen weergeven',\n    'image_book_title' => 'Bekijk afbeeldingen die naar dit boek zijn geüpload',\n    'image_page_title' => 'Bekijk afbeeldingen geüpload naar deze pagina',\n    'image_search_hint' => 'Zoek op afbeeldingsnaam',\n    'image_uploaded' => 'Geüpload op :uploadedDate',\n    'image_uploaded_by' => 'Geüpload door :userName',\n    'image_uploaded_to' => 'Geüpload naar :pageLink',\n    'image_updated' => ':updateDate bijgewerkt',\n    'image_load_more' => 'Laad meer',\n    'image_image_name' => 'Afbeeldingsnaam',\n    'image_delete_used' => 'Deze afbeelding is op onderstaande pagina\\'s in gebruik.',\n    'image_delete_confirm_text' => 'Weet je zeker dat je deze afbeelding wilt verwijderen?',\n    'image_select_image' => 'Kies afbeelding',\n    'image_dropzone' => 'Sleep afbeeldingen naar hier of klik hier om te uploaden',\n    'image_dropzone_drop' => 'Sleep hier de afbeeldingen naar toe',\n    'images_deleted' => 'Afbeeldingen verwijderd',\n    'image_preview' => 'Afbeelding voorbeeld',\n    'image_upload_success' => 'Afbeelding succesvol geüpload',\n    'image_update_success' => 'Afbeeldingsdetails succesvol bijgewerkt',\n    'image_delete_success' => 'Afbeelding succesvol verwijderd',\n    'image_replace' => 'Vervang Afbeelding',\n    'image_replace_success' => 'Afbeelding succesvol bijgewerkt',\n    'image_rebuild_thumbs' => 'Variaties in grootte opnieuw genereren',\n    'image_rebuild_thumbs_success' => 'Variaties in afbeeldingsgrootte succesvol herbouwd!',\n\n    // Code Editor\n    'code_editor' => 'Bewerk Code',\n    'code_language' => 'Codetaal',\n    'code_content' => 'Code Inhoud',\n    'code_session_history' => 'Sessie geschiedenis',\n    'code_save' => 'Sla code op',\n];\n"
  },
  {
    "path": "lang/nl/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Algemeen',\n    'advanced' => 'Geavanceerd',\n    'none' => 'Geen',\n    'cancel' => 'Annuleer',\n    'save' => 'Opslaan',\n    'close' => 'Sluit',\n    'apply' => 'Toepassen',\n    'undo' => 'Maak ongedaan',\n    'redo' => 'Opnieuw uitvoeren',\n    'left' => 'Links',\n    'center' => 'Centraal',\n    'right' => 'Rechts',\n    'top' => 'Boven',\n    'middle' => 'Midden',\n    'bottom' => 'Onder',\n    'width' => 'Breedte',\n    'height' => 'Hoogte',\n    'More' => 'Meer',\n    'select' => 'Selecteer...',\n\n    // Toolbar\n    'formats' => 'Stijlen',\n    'header_large' => 'Grote koptekst',\n    'header_medium' => 'Middelgrote koptekst',\n    'header_small' => 'Kleine koptekst',\n    'header_tiny' => 'Erg kleine koptekst',\n    'paragraph' => 'Standaard',\n    'blockquote' => 'Citaat',\n    'inline_code' => 'Inline code',\n    'callouts' => 'Markeringen',\n    'callout_information' => 'Informatie',\n    'callout_success' => 'Succes',\n    'callout_warning' => 'Waarschuwing',\n    'callout_danger' => 'Gevaar',\n    'bold' => 'Vet',\n    'italic' => 'Cursief',\n    'underline' => 'Onderstrepen',\n    'strikethrough' => 'Doorstrepen',\n    'superscript' => 'Superscript',\n    'subscript' => 'Subscript',\n    'text_color' => 'Tekstkleur',\n    'highlight_color' => 'Accentkleur',\n    'custom_color' => 'Aangepaste kleur',\n    'remove_color' => 'Verwijder kleur',\n    'background_color' => 'Tekstmarkeringskleur',\n    'align_left' => 'Links uitlijnen',\n    'align_center' => 'Centreren',\n    'align_right' => 'Rechts uitlijnen',\n    'align_justify' => 'Uitlijnen',\n    'list_bullet' => 'Opsommingstekens',\n    'list_numbered' => 'Genummerde lijst',\n    'list_task' => 'Takenlijst',\n    'indent_increase' => 'Inspringing vergroten',\n    'indent_decrease' => 'Inspringing verkleinen',\n    'table' => 'Tabel',\n    'insert_image' => 'Afbeelding invoegen',\n    'insert_image_title' => 'Afbeelding invoegen/bewerken',\n    'insert_link' => 'Link invoegen/bewerken',\n    'insert_link_title' => 'Link invoegen/bewerken',\n    'insert_horizontal_line' => 'Horizontale lijn invoegen',\n    'insert_code_block' => 'Codeblok invoegen',\n    'edit_code_block' => 'Bewerk codeblok',\n    'insert_drawing' => 'Tekening invoegen/bewerken',\n    'drawing_manager' => 'Beheer tekeningen',\n    'insert_media' => 'Media invoegen/bewerken',\n    'insert_media_title' => 'Media invoegen/bewerken',\n    'clear_formatting' => 'Opmaak wissen',\n    'source_code' => 'Broncode',\n    'source_code_title' => 'Broncode',\n    'fullscreen' => 'Volledig scherm',\n    'image_options' => 'Afbeeldingsopties',\n\n    // Tables\n    'table_properties' => 'Tabeleigenschappen',\n    'table_properties_title' => 'Tabeleigenschappen',\n    'delete_table' => 'Verwijder tabel',\n    'table_clear_formatting' => 'Tabel opmaak wissen',\n    'resize_to_contents' => 'Formaat aanpassen naar inhoud',\n    'row_header' => 'Koptekst rij',\n    'insert_row_before' => 'Rij boven invoegen',\n    'insert_row_after' => 'Rij onder invoegen',\n    'delete_row' => 'Rij verwijderen',\n    'insert_column_before' => 'Kolom links invoegen',\n    'insert_column_after' => 'Kolom rechts invoegen',\n    'delete_column' => 'Kolom verwijderen',\n    'table_cell' => 'Cel',\n    'table_row' => 'Rij',\n    'table_column' => 'Kolom',\n    'cell_properties' => 'Cel eigenschappen',\n    'cell_properties_title' => 'Cel Eigenschappen',\n    'cell_type' => 'Cel type',\n    'cell_type_cell' => 'Cel',\n    'cell_scope' => 'Bereik',\n    'cell_type_header' => 'Koptekst cel',\n    'merge_cells' => 'Cellen samenvoegen',\n    'split_cell' => 'Cel splitsen',\n    'table_row_group' => 'Rij groep',\n    'table_column_group' => 'Kolom groep',\n    'horizontal_align' => 'Horizontaal uitlijnen',\n    'vertical_align' => 'Verticaal uitlijnen',\n    'border_width' => 'Randbreedte',\n    'border_style' => 'Randstijl',\n    'border_color' => 'Randkleur',\n    'row_properties' => 'Rij eigenschappen',\n    'row_properties_title' => 'Rij Eigenschappen',\n    'cut_row' => 'Knip rij',\n    'copy_row' => 'Kopieer rij',\n    'paste_row_before' => 'Plak rij erboven',\n    'paste_row_after' => 'Plak rij eronder',\n    'row_type' => 'Rij type',\n    'row_type_header' => 'Koptekst',\n    'row_type_body' => 'Inhoud',\n    'row_type_footer' => 'Voettekst',\n    'alignment' => 'Uitlijning',\n    'cut_column' => 'Knip kolom',\n    'copy_column' => 'Kopieer kolom',\n    'paste_column_before' => 'Plak kolom links',\n    'paste_column_after' => 'Plak kolom rechts',\n    'cell_padding' => 'Cel opvulling',\n    'cell_spacing' => 'Cel afstand',\n    'caption' => 'Onderschrift',\n    'show_caption' => 'Onderschrift tonen',\n    'constrain' => 'Beperk verhoudingen',\n    'cell_border_solid' => 'Ononderbroken',\n    'cell_border_dotted' => 'Gestippeld',\n    'cell_border_dashed' => 'Gestreept',\n    'cell_border_double' => 'Dubbel',\n    'cell_border_groove' => 'Gegroefd',\n    'cell_border_ridge' => 'Geribbeld',\n    'cell_border_inset' => 'Inset',\n    'cell_border_outset' => 'Outset',\n    'cell_border_none' => 'Geen',\n    'cell_border_hidden' => 'Verborgen',\n\n    // Images, links, details/summary & embed\n    'source' => 'Bron',\n    'alt_desc' => 'Alternatieve beschrijving',\n    'embed' => 'Insluiten',\n    'paste_embed' => 'Plak je insluitcode hieronder:',\n    'url' => 'URL',\n    'text_to_display' => 'Weer te geven tekst',\n    'title' => 'Titel',\n    'browse_links' => 'Blader links',\n    'open_link' => 'Open koppeling',\n    'open_link_in' => 'Open koppeling in...',\n    'open_link_current' => 'Huidig venster',\n    'open_link_new' => 'Nieuw venster',\n    'remove_link' => 'Verwijder koppeling',\n    'insert_collapsible' => 'Voeg inklapbaar blok toe',\n    'collapsible_unwrap' => 'Uitpakken',\n    'edit_label' => 'Bewerk label',\n    'toggle_open_closed' => 'Schakel tussen open/dicht',\n    'collapsible_edit' => 'Bewerk inklapbaar blok',\n    'toggle_label' => 'Schakel label aan/uit',\n\n    // About view\n    'about' => 'Over de bewerker',\n    'about_title' => 'Over de WYSIWYG Bewerker',\n    'editor_license' => 'Bewerker Licentie & Copyright',\n    'editor_lexical_license' => 'Deze editor is gemaakt als een fork van :lexicalLink welke is verstrekt onder de MIT-licentie.',\n    'editor_lexical_license_link' => 'Volledige licentieinformatie kan hier gevonden worden.',\n    'editor_tiny_license' => 'Deze editor is gemaakt met behulp van :tinyLink welke is verstrekt onder de MIT-licentie.',\n    'editor_tiny_license_link' => 'De copyright- en licentiegegevens van TinyMCE vindt u hier.',\n    'save_continue' => 'Pagina opslaan en verdergaan',\n    'callouts_cycle' => '(Blijf drukken om door de types te wisselen)',\n    'link_selector' => 'Koppeling naar inhoud',\n    'shortcuts' => 'Snelkoppelingen',\n    'shortcut' => 'Snelkoppeling',\n    'shortcuts_intro' => 'De volgende sneltoetsen zijn beschikbaar in de bewerker:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Beschrijving',\n];\n"
  },
  {
    "path": "lang/nl/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Recent Aangemaakt',\n    'recently_created_pages' => 'Recent Aangemaakte Pagina\\'s',\n    'recently_updated_pages' => 'Recent bijgewerkte pagina\\'s',\n    'recently_created_chapters' => 'Recent Aangemaakte Hoofdstukken',\n    'recently_created_books' => 'Recent Aangemaakte Boeken',\n    'recently_created_shelves' => 'Recent Aangemaakte Boekenplanken',\n    'recently_update' => 'Recent bijgewerkt',\n    'recently_viewed' => 'Recent bekeken',\n    'recent_activity' => 'Recente activiteit',\n    'create_now' => 'Maak er nu één',\n    'revisions' => 'Revisies',\n    'meta_revision' => 'Revisie #:revisionCount',\n    'meta_created' => 'Gemaakt: :timeLength',\n    'meta_created_name' => 'Gemaakt: :timeLength door :user',\n    'meta_updated' => 'Bijgewerkt: :timeLength',\n    'meta_updated_name' => 'Bijgewerkt: :timeLength door :user',\n    'meta_owned_name' => 'Eigendom van :user',\n    'meta_reference_count' => 'Verwijzing in :count item|Verwijzing in :count items',\n    'entity_select' => 'Entiteit selecteren',\n    'entity_select_lack_permission' => 'Je hebt niet de vereiste machtiging om dit item te selecteren',\n    'images' => 'Afbeeldingen',\n    'my_recent_drafts' => 'Mijn recente concepten',\n    'my_recently_viewed' => 'Mijn recent bekeken',\n    'my_most_viewed_favourites' => 'Mijn meest bekeken favorieten',\n    'my_favourites' => 'Mijn favorieten',\n    'no_pages_viewed' => 'Je hebt nog geen pagina\\'s bekeken',\n    'no_pages_recently_created' => 'Er zijn geen recent gemaakte pagina\\'s',\n    'no_pages_recently_updated' => 'Er zijn geen pagina\\'s recent bijgewerkt',\n    'export' => 'Exporteer',\n    'export_html' => 'Ingesloten webbestand',\n    'export_pdf' => 'PDF bestand',\n    'export_text' => 'Normaal tekstbestand',\n    'export_md' => 'Markdown bestand',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Standaard Paginasjabloon',\n    'default_template_explain' => 'Ken een paginasjabloon toe die zal worden gebruikt als de standaardinhoud voor alle pagina\\'s die binnen dit item worden aangemaakt. Houd er rekening mee dat dit alleen zal worden gebruikt als de paginamaker leesrechten heeft voor de gekozen sjabloonpagina.',\n    'default_template_select' => 'Selecteer een sjabloonpagina',\n    'import' => 'Import',\n    'import_validate' => 'Valideer Import',\n    'import_desc' => 'Importeer boeken, hoofdstukken & pagina\\'s met een portable Zip-export van dezelfde, of een andere omgeving. Selecteer een Zip-bestand om door te gaan. Nadat het bestand is geüpload en gecontroleerd kunt u de import configureren en doorvoeren in de volgende weergave.',\n    'import_zip_select' => 'Selecteer een Zip-bestand om te uploaden',\n    'import_zip_validation_errors' => 'Er zijn fouten gevonden tijdens het controleren van het Zip-bestand:',\n    'import_pending' => 'Wachtende Imports',\n    'import_pending_none' => 'Er zijn geen imports gestart.',\n    'import_continue' => 'Importeren Voortzetten',\n    'import_continue_desc' => 'Controleer de inhoud die gaat worden geïmporteerd vanuit get geüploade Zip-bestand. Voer de import door om de inhoud toe te voegen aan dit systeem. Bij een succesvolle import zal het geüploade Zip-bestand automatisch verwijderd worden.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Import Doorvoeren',\n    'import_size' => ':size Import Zip Grootte',\n    'import_uploaded_at' => 'Geüpload :relativeTime',\n    'import_uploaded_by' => 'Geüpload door',\n    'import_location' => 'Importlocatie',\n    'import_location_desc' => 'Selecteer een locatie voor de geïmporteerde inhoud. Je hebt de bijbehorende machtigingen nodig op de importlocatie.',\n    'import_delete_confirm' => 'Weet je zeker dat je deze import wilt verwijderen?',\n    'import_delete_desc' => 'Dit zal het Zip-bestand van de import permanent verwijderen.',\n    'import_errors' => 'Importeerfouten',\n    'import_errors_desc' => 'De volgende fouten deden zich voor tijdens het importeren:',\n    'breadcrumb_siblings_for_page' => 'Navigeer pagina\\'s op hetzelfde niveau',\n    'breadcrumb_siblings_for_chapter' => 'Navigeer hoofdstukken op hetzelfde niveau',\n    'breadcrumb_siblings_for_book' => 'Navigeer boeken op hetzelfde niveau',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigeer boekenplanken op hetzelfde niveau',\n\n    // Permissions and restrictions\n    'permissions' => 'Machtigingen',\n    'permissions_desc' => 'Stel hier machtigingen in om de standaardmachtigingen van gebruikersrollen te overschrijven.',\n    'permissions_book_cascade' => 'Machtigingen voor boeken worden automatisch doorgegeven aan hoofdstukken en pagina\\'s, tenzij deze hun eigen machtigingen hebben.',\n    'permissions_chapter_cascade' => 'Machtigingen ingesteld op hoofdstukken zullen automatisch worden doorgegeven aan onderliggende pagina\\'s, tenzij deze hun eigen machtigingen hebben.',\n    'permissions_save' => 'Machtigingen opslaan',\n    'permissions_owner' => 'Eigenaar',\n    'permissions_role_everyone_else' => 'De rest',\n    'permissions_role_everyone_else_desc' => 'Stel machtigingen in voor alle rollen die niet specifiek overschreven zijn.',\n    'permissions_role_override' => 'Overschrijf machtigingen voor rol',\n    'permissions_inherit_defaults' => 'Standaardwaarden overnemen',\n\n    // Search\n    'search_results' => 'Zoekresultaten',\n    'search_total_results_found' => ':count resultaten gevonden|totaal :count resultaten gevonden',\n    'search_clear' => 'Zoekopdracht wissen',\n    'search_no_pages' => 'Geen pagina\\'s gevonden die overeenkomen met deze zoekopdracht',\n    'search_for_term' => 'Zoeken op :term',\n    'search_more' => 'Meer resultaten',\n    'search_advanced' => 'Uitgebreid zoeken',\n    'search_terms' => 'Zoektermen',\n    'search_content_type' => 'Inhoudstype',\n    'search_exact_matches' => 'Exacte overeenkomsten',\n    'search_tags' => 'Label Zoekopdrachten',\n    'search_options' => 'Opties',\n    'search_viewed_by_me' => 'Bekeken door mij',\n    'search_not_viewed_by_me' => 'Niet bekeken door mij',\n    'search_permissions_set' => 'Machtigingen ingesteld',\n    'search_created_by_me' => 'Door mij gemaakt',\n    'search_updated_by_me' => 'Door mij bijgewerkt',\n    'search_owned_by_me' => 'Eigendom van mij',\n    'search_date_options' => 'Datum opties',\n    'search_updated_before' => 'Bijgewerkt voor',\n    'search_updated_after' => 'Bijgewerkt na',\n    'search_created_before' => 'Gemaakt voor',\n    'search_created_after' => 'Gemaakt na',\n    'search_set_date' => 'Stel datum in',\n    'search_update' => 'Update zoekresultaten',\n\n    // Shelves\n    'shelf' => 'Boekenplank',\n    'shelves' => 'Boekenplanken',\n    'x_shelves' => ':count Boekenplank|:count Boekenplanken',\n    'shelves_empty' => 'Er zijn geen boekenplanken gemaakt',\n    'shelves_create' => 'Nieuwe boekenplank maken',\n    'shelves_popular' => 'Populaire boekenplanken',\n    'shelves_new' => 'Nieuwe boekenplanken',\n    'shelves_new_action' => 'Nieuwe boekenplank',\n    'shelves_popular_empty' => 'Hier worden de meest populaire boekenplanken weergegeven.',\n    'shelves_new_empty' => 'Hier worden de meest recent gemaakte boekenplanken weergegeven.',\n    'shelves_save' => 'Boekenplank opslaan',\n    'shelves_books' => 'Boeken op deze plank',\n    'shelves_add_books' => 'Voeg boeken toe aan deze plank',\n    'shelves_drag_books' => 'Sleep boeken hieronder om ze toe te voegen aan deze boekenplank',\n    'shelves_empty_contents' => 'Aan deze plank zijn geen boeken toegewezen',\n    'shelves_edit_and_assign' => 'Bewerk boekenplank om boeken toe te wijzen',\n    'shelves_edit_named' => 'Bewerk Boekenplank :name',\n    'shelves_edit' => 'Bewerk Boekenplank',\n    'shelves_delete' => 'Verwijder Boekenplank',\n    'shelves_delete_named' => 'Verwijder Boekenplank :name',\n    'shelves_delete_explain' => \"Dit zal de boekenplank met de naam ':naam' verwijderen. Boeken die op deze plank staan worden echter niet verwijderd.\",\n    'shelves_delete_confirmation' => 'Weet je zeker dat je deze boekenplank wilt verwijderen?',\n    'shelves_permissions' => 'Boekenplank Machtigingen',\n    'shelves_permissions_updated' => 'Boekenplank Machtigingen Bijgewerkt',\n    'shelves_permissions_active' => 'Machtigingen op Boekenplank Actief',\n    'shelves_permissions_cascade_warning' => 'De ingestelde machtigingen op deze boekenplank worden niet automatisch toegepast op de boeken van deze boekenplank. Dit is omdat een boek toegekend kan worden op meerdere boekenplanken. De machtigingen van deze boekenplank kunnen echter wel gekopieerd worden naar de boeken van deze boekenplank via de optie hieronder.',\n    'shelves_permissions_create' => '\\'Maak boekenplank\\' machtigingen worden enkel gebruikt om machtigingen te kopiëren naar boeken binnenin een boekenplank door gebruik te maken van onderstaande actie. Deze machtigingen laten niet toe om een nieuw boek aan te maken.',\n    'shelves_copy_permissions_to_books' => 'Kopieer Machtigingen naar Boeken',\n    'shelves_copy_permissions' => 'Kopieer Machtigingen',\n    'shelves_copy_permissions_explain' => 'Met deze actie worden de machtigingen van deze boekenplank gekopieerd naar alle boeken van deze boekenplank. Voor je deze actie uitvoert, moet je ervoor zorgen dat alle wijzigingen in de machtigingen van deze boekenplank zijn opgeslagen.',\n    'shelves_copy_permission_success' => 'Boekenplank machtigingen gekopieerd naar :count boeken',\n\n    // Books\n    'book' => 'Boek',\n    'books' => 'Boeken',\n    'x_books' => ':count Boek|:count Boeken',\n    'books_empty' => 'Er zijn geen boeken aangemaakt',\n    'books_popular' => 'Populaire boeken',\n    'books_recent' => 'Recente boeken',\n    'books_new' => 'Nieuwe boeken',\n    'books_new_action' => 'Nieuw boek',\n    'books_popular_empty' => 'Hier worden de meest populaire boeken weergegeven.',\n    'books_new_empty' => 'Hier worden de meest recent gemaakte boeken weergegeven.',\n    'books_create' => 'Nieuw boek maken',\n    'books_delete' => 'Boek verwijderen',\n    'books_delete_named' => 'Verwijder boek :bookName',\n    'books_delete_explain' => 'Deze actie verwijdert het boek \\':bookName\\', Alle pagina\\'s en hoofdstukken worden verwijderd.',\n    'books_delete_confirmation' => 'Weet je zeker dat je dit boek wilt verwijderen?',\n    'books_edit' => 'Boek bewerken',\n    'books_edit_named' => 'Bewerk boek :bookName',\n    'books_form_book_name' => 'Boek naam',\n    'books_save' => 'Boek opslaan',\n    'books_permissions' => 'Boek machtigingen',\n    'books_permissions_updated' => 'Boek Machtigingen Bijgewerkt',\n    'books_empty_contents' => 'Er zijn nog geen hoofdstukken en pagina\\'s voor dit boek gemaakt.',\n    'books_empty_create_page' => 'Nieuwe pagina maken',\n    'books_empty_sort_current_book' => 'Boek sorteren',\n    'books_empty_add_chapter' => 'Hoofdstuk toevoegen',\n    'books_permissions_active' => 'Machtigingen op Boek Actief',\n    'books_search_this' => 'Zoeken in dit boek',\n    'books_navigation' => 'Boek navigatie',\n    'books_sort' => 'Inhoud van het boek sorteren',\n    'books_sort_desc' => 'Verplaats hoofdstukken en pagina\\'s door het boek om ze te organiseren. Andere boeken kunnen worden toegevoegd zodat hoofdstukken en pagina\\'s gemakkelijk tussen boeken kunnen worden verplaatst. Het is mogelijk om een automatische sorteerregel in te stellen die de inhoud zal sorteren bij wijzigingen.',\n    'books_sort_auto_sort' => 'Automatisch Sorteren',\n    'books_sort_auto_sort_active' => 'Automatisch Sorteren Actief: :sortName',\n    'books_sort_named' => 'Sorteer boek :bookName',\n    'books_sort_name' => 'Sorteren op naam',\n    'books_sort_created' => 'Sorteren op datum van aanmaken',\n    'books_sort_updated' => 'Sorteren op datum van bijgewerkt',\n    'books_sort_chapters_first' => 'Hoofdstukken eerst',\n    'books_sort_chapters_last' => 'Hoofdstukken laatst',\n    'books_sort_show_other' => 'Bekijk andere boeken',\n    'books_sort_save' => 'Nieuwe volgorde opslaan',\n    'books_sort_show_other_desc' => 'Voeg hier andere boeken toe om ze op te nemen in de sortering, en om een gemakkelijke reorganisatie van boeken mogelijk te maken.',\n    'books_sort_move_up' => 'Verplaats naar boven',\n    'books_sort_move_down' => 'Verplaats naar beneden',\n    'books_sort_move_prev_book' => 'Verplaats naar het vorige boek',\n    'books_sort_move_next_book' => 'Verplaats naar het volgende boek',\n    'books_sort_move_prev_chapter' => 'Verplaats naar het vorige hoofdstuk',\n    'books_sort_move_next_chapter' => 'Verplaats naar het volgende hoofdstuk',\n    'books_sort_move_book_start' => 'Verplaats naar het begin van het boek',\n    'books_sort_move_book_end' => 'Verplaats naar het einde van het boek',\n    'books_sort_move_before_chapter' => 'Verplaats naar vóór het hoofdstuk',\n    'books_sort_move_after_chapter' => 'Verplaats naar áchter het hoofdstuk',\n    'books_copy' => 'Kopieer Boek',\n    'books_copy_success' => 'Boek succesvol gekopieerd',\n\n    // Chapters\n    'chapter' => 'Hoofdstuk',\n    'chapters' => 'Hoofdstukken',\n    'x_chapters' => ':count Hoofdstuk|:count Hoofdstukken',\n    'chapters_popular' => 'Populaire hoofdstukken',\n    'chapters_new' => 'Nieuw hoofdstuk',\n    'chapters_create' => 'Nieuw hoofdstuk maken',\n    'chapters_delete' => 'Hoofdstuk verwijderen',\n    'chapters_delete_named' => 'Verwijder hoofdstuk :chapterName',\n    'chapters_delete_explain' => 'Dit verwijdert het hoofdstuk met de naam \\':chapterName\\'. Alle pagina\\'s in dit hoofdstuk zullen ook worden verwijderd.',\n    'chapters_delete_confirm' => 'Weet je zeker dat je dit hoofdstuk wilt verwijderen?',\n    'chapters_edit' => 'Hoofdstuk aanpassen',\n    'chapters_edit_named' => 'Hoofdstuk :chapterName aanpassen',\n    'chapters_save' => 'Hoofdstuk opslaan',\n    'chapters_move' => 'Hoofdstuk verplaatsen',\n    'chapters_move_named' => 'Verplaatst hoofdstuk :chapterName',\n    'chapters_copy' => 'Kopieer Hoofdstuk',\n    'chapters_copy_success' => 'Hoofdstuk succesvol gekopieerd',\n    'chapters_permissions' => 'Hoofdstuk Machtigingen',\n    'chapters_empty' => 'Er zijn geen pagina\\'s in dit hoofdstuk aangemaakt.',\n    'chapters_permissions_active' => 'Hoofdstuk Machtigingen Actief',\n    'chapters_permissions_success' => 'Hoofdstuk Machtigingen Bijgewerkt',\n    'chapters_search_this' => 'Zoek in dit hoofdstuk',\n    'chapter_sort_book' => 'Sorteer Boek',\n\n    // Pages\n    'page' => 'Pagina',\n    'pages' => 'Pagina\\'s',\n    'x_pages' => ':count Pagina|:count Pagina\\'s',\n    'pages_popular' => 'Populaire pagina\\'s',\n    'pages_new' => 'Nieuwe pagina',\n    'pages_attachments' => 'Bijlages',\n    'pages_navigation' => 'Pagina navigatie',\n    'pages_delete' => 'Pagina verwijderen',\n    'pages_delete_named' => 'Verwijder pagina :pageName',\n    'pages_delete_draft_named' => 'Verwijder concept pagina :pageName',\n    'pages_delete_draft' => 'Verwijder concept pagina',\n    'pages_delete_success' => 'Pagina verwijderd',\n    'pages_delete_draft_success' => 'Concept verwijderd',\n    'pages_delete_warning_template' => 'Deze pagina wordt actief gebruikt als standaardsjabloon voor een boek of hoofdstuk. Nadat deze pagina is verwijderd, zullen deze boeken of hoofdstukken geen standaardsjabloon meer toegewezen hebben.',\n    'pages_delete_confirm' => 'Weet je zeker dat je deze pagina wilt verwijderen?',\n    'pages_delete_draft_confirm' => 'Weet je zeker dat je dit concept wilt verwijderen?',\n    'pages_editing_named' => 'Pagina :pageName aan het bewerken',\n    'pages_edit_draft_options' => 'Concept opties',\n    'pages_edit_save_draft' => 'Concept opslaan',\n    'pages_edit_draft' => 'Paginaconcept bewerken',\n    'pages_editing_draft' => 'Concept bewerken',\n    'pages_editing_page' => 'Concept bewerken',\n    'pages_edit_draft_save_at' => 'Concept opgeslagen op ',\n    'pages_edit_delete_draft' => 'Concept verwijderen',\n    'pages_edit_delete_draft_confirm' => 'Weet je zeker dat je de wijzigingen in je concept wilt verwijderen? Al je wijzigingen sinds de laatste succesvolle bewaring gaan verloren en de editor wordt bijgewerkt met de meest recente niet-concept versie van de pagina.',\n    'pages_edit_discard_draft' => 'Concept verwijderen',\n    'pages_edit_switch_to_markdown' => 'Schakel naar de Markdown Bewerker',\n    'pages_edit_switch_to_markdown_clean' => '(Opgeschoonde Inhoud)',\n    'pages_edit_switch_to_markdown_stable' => '(Stabiele Inhoud)',\n    'pages_edit_switch_to_wysiwyg' => 'Schakel naar de WYSIWYG Bewerker',\n    'pages_edit_switch_to_new_wysiwyg' => 'Schakel naar de nieuwe WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta-testfase)',\n    'pages_edit_set_changelog' => 'Logboek instellen',\n    'pages_edit_enter_changelog_desc' => 'Geef een korte omschrijving van de wijzigingen die je gemaakt hebt',\n    'pages_edit_enter_changelog' => 'Voeg toe aan logboek',\n    'pages_editor_switch_title' => 'Schakel Bewerker',\n    'pages_editor_switch_are_you_sure' => 'Weet je zeker dat je de bewerker voor deze pagina wilt wijzigen?',\n    'pages_editor_switch_consider_following' => 'Houd rekening met het volgende als u van bewerker verandert:',\n    'pages_editor_switch_consideration_a' => 'Eenmaal opgeslagen, zal de nieuwe bewerker keuze gebruikt worden door alle toekomstige gebruikers, ook diegene die zelf niet van bewerker type kunnen veranderen.',\n    'pages_editor_switch_consideration_b' => 'Dit kan mogelijks tot een verlies van detail en syntax leiden in bepaalde omstandigheden.',\n    'pages_editor_switch_consideration_c' => 'De veranderingen aan Labels of aan het wijzigingslogboek, sinds de laatste keer opslaan, zullen niet behouden blijven met deze wijziging.',\n    'pages_save' => 'Pagina opslaan',\n    'pages_title' => 'Pagina titel',\n    'pages_name' => 'Pagina naam',\n    'pages_md_editor' => 'Bewerker',\n    'pages_md_preview' => 'Voorbeeld',\n    'pages_md_insert_image' => 'Afbeelding invoegen',\n    'pages_md_insert_link' => 'Entiteit link invoegen',\n    'pages_md_insert_drawing' => 'Tekening invoegen',\n    'pages_md_show_preview' => 'Toon voorbeeld',\n    'pages_md_sync_scroll' => 'Synchroniseer scrollen van voorbeeld',\n    'pages_md_plain_editor' => 'Normale tekst bewerker',\n    'pages_drawing_unsaved' => 'Niet-opgeslagen Tekening Gevonden',\n    'pages_drawing_unsaved_confirm' => 'Er zijn niet-opgeslagen tekeninggegevens gevonden van een eerdere mislukte poging om de tekening op te slaan. Wil je deze niet-opgeslagen tekening herstellen en verder bewerken?',\n    'pages_not_in_chapter' => 'Pagina is niet in een hoofdstuk',\n    'pages_move' => 'Pagina Verplaatsen',\n    'pages_copy' => 'Pagina kopiëren',\n    'pages_copy_desination' => 'Kopieër bestemming',\n    'pages_copy_success' => 'Pagina succesvol gekopieerd',\n    'pages_permissions' => 'Pagina Machtigingen',\n    'pages_permissions_success' => 'Pagina machtigingen bijgewerkt',\n    'pages_revision' => 'Revisie',\n    'pages_revisions' => 'Pagina revisies',\n    'pages_revisions_desc' => 'Hieronder staan alle vorige versies van deze pagina. Je kunt oude paginaversies terugkijken, vergelijken en herstellen indien de rechten dit toelaten. De volledige geschiedenis van de pagina wordt hier mogelijk niet volledig weergegeven omdat, afhankelijk van de systeemconfiguratie, oude versies al automatisch verwijderd kunnen zijn.',\n    'pages_revisions_named' => 'Pagina revisies voor :pageName',\n    'pages_revision_named' => 'Pagina revisie voor :pageName',\n    'pages_revision_restored_from' => 'Hersteld van #:id; :samenvatting',\n    'pages_revisions_created_by' => 'Gemaakt door',\n    'pages_revisions_date' => 'Revisiedatum',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Versie Nummer',\n    'pages_revisions_numbered' => 'Revisie #:id',\n    'pages_revisions_numbered_changes' => 'Revisie #:id wijzigingen',\n    'pages_revisions_editor' => 'Bewerker Type',\n    'pages_revisions_changelog' => 'Logboek',\n    'pages_revisions_changes' => 'Wijzigingen',\n    'pages_revisions_current' => 'Huidige versie',\n    'pages_revisions_preview' => 'Voorbeeld',\n    'pages_revisions_restore' => 'Herstellen',\n    'pages_revisions_none' => 'Deze pagina heeft geen revisies',\n    'pages_copy_link' => 'Link kopiëren',\n    'pages_edit_content_link' => 'Ga naar sectie in bewerker',\n    'pages_pointer_enter_mode' => 'Open selectiemodus per onderdeel',\n    'pages_pointer_label' => 'Pagina Onderdeel Opties',\n    'pages_pointer_permalink' => 'Pagina Onderdeel Permalink',\n    'pages_pointer_include_tag' => 'Pagina Onderdeel Tag Toevoeging',\n    'pages_pointer_toggle_link' => 'Permalink modus, Druk om Tag Toevoeging te tonen',\n    'pages_pointer_toggle_include' => 'Tag Toevoeging modus, Druk om Permalink te tonen',\n    'pages_permissions_active' => 'Pagina Machtigingen Actief',\n    'pages_initial_revision' => 'Eerste publicatie',\n    'pages_references_update_revision' => 'Automatische systeemupdate van interne links',\n    'pages_initial_name' => 'Nieuwe pagina',\n    'pages_editing_draft_notification' => 'U bewerkt momenteel een concept dat voor het laatst is opgeslagen op :timeDiff.',\n    'pages_draft_edited_notification' => 'Deze pagina is sindsdien bijgewerkt. Het wordt aanbevolen dat je dit concept verwijderd.',\n    'pages_draft_page_changed_since_creation' => 'Deze pagina is bijgewerkt sinds het aanmaken van dit concept. Het wordt aanbevolen dat u dit concept verwijdert of ervoor zorgt dat u wijzigingen op de pagina niet overschrijft.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count gebruikers zijn begonnen deze pagina te bewerken',\n        'start_b' => ':userName is begonnen met het bewerken van deze pagina',\n        'time_a' => 'sinds de laatste pagina-update',\n        'time_b' => 'in de laatste :minCount minuten',\n        'message' => ':start :time. Let op om elkaars updates niet te overschrijven!',\n    ],\n    'pages_draft_discarded' => 'Concept verworpen! De bewerker is bijgewerkt met de huidige inhoud van de pagina',\n    'pages_draft_deleted' => 'Concept verwijderd! De bewerker is bijgewerkt met de huidige inhoud van de pagina',\n    'pages_specific' => 'Specifieke pagina',\n    'pages_is_template' => 'Paginasjabloon',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Zijbalk Tonen/Verbergen',\n    'page_tags' => 'Pagina Labels',\n    'chapter_tags' => 'Hoofdstuk Labels',\n    'book_tags' => 'Boek Labels',\n    'shelf_tags' => 'Boekenplank Labels',\n    'tag' => 'Label',\n    'tags' =>  'Labels',\n    'tags_index_desc' => 'Labels kunnen worden toegepast op inhoud binnen het systeem om een flexibele vorm van categorisering toe te passen. Labels kunnen zowel een sleutel als een waarde hebben, waarbij de waarde optioneel is. Eenmaal toegepast, kan de inhoud worden opgevraagd aan de hand van de naam en de waarde van het label.',\n    'tag_name' =>  'Labelnaam',\n    'tag_value' => 'Labelwaarde (Optioneel)',\n    'tags_explain' => \"Voeg enkele labels toe om je inhoud beter te categoriseren.\\nJe kunt een waarde aan een label toekennen voor een meer gedetailleerde organisatie.\",\n    'tags_add' => 'Voeg nog een label toe',\n    'tags_remove' => 'Verwijder deze label',\n    'tags_usages' => 'Totaal aantal label-toepassingen',\n    'tags_assigned_pages' => 'Toegewezen aan pagina\\'s',\n    'tags_assigned_chapters' => 'Toegewezen aan hoofdstukken',\n    'tags_assigned_books' => 'Toegewezen aan boeken',\n    'tags_assigned_shelves' => 'Toegewezen aan boekenplanken',\n    'tags_x_unique_values' => ':count unieke waarden',\n    'tags_all_values' => 'Alle waarden',\n    'tags_view_tags' => 'Bekijk Labels',\n    'tags_view_existing_tags' => 'Bekijk bestaande labels',\n    'tags_list_empty_hint' => 'Labels kunnen worden toegekend via de zijbalk van de pagina-bewerker of tijdens het bewerken van de details van een boek, hoofdstuk of boekenplank.',\n    'attachments' => 'Bijlages',\n    'attachments_explain' => 'Upload bijlages of voeg een link toe. Deze worden zichtbaar in het navigatiepaneel.',\n    'attachments_explain_instant_save' => 'Wijzigingen worden meteen opgeslagen.',\n    'attachments_upload' => 'Bestand uploaden',\n    'attachments_link' => 'Link toevoegen',\n    'attachments_upload_drop' => 'Je kan ook een bestand hiernaartoe slepen om het als bijlage te uploaden.',\n    'attachments_set_link' => 'Zet link',\n    'attachments_delete' => 'Weet je zeker dat je deze bijlage wilt verwijderen?',\n    'attachments_dropzone' => 'Sleep hier de bestanden naar toe',\n    'attachments_no_files' => 'Er zijn geen bestanden geüpload',\n    'attachments_explain_link' => 'Je kunt een hyperlink toevoegen als je geen bestanden wilt uploaden. Dit kan een link naar een andere pagina op deze website zijn, maar ook een link naar een andere website.',\n    'attachments_link_name' => 'Link naam',\n    'attachment_link' => 'Bijlage link',\n    'attachments_link_url' => 'Hyperlink naar bestand',\n    'attachments_link_url_hint' => 'URL van site of bestand',\n    'attach' => 'Toevoegen',\n    'attachments_insert_link' => 'Bijlage hyperlink toevoegen aan pagina',\n    'attachments_edit_file' => 'Bestand bewerken',\n    'attachments_edit_file_name' => 'Bestandsnaam',\n    'attachments_edit_drop_upload' => 'Sleep een bestand of klik hier om te uploaden en te overschrijven',\n    'attachments_order_updated' => 'De volgorde van de bijlages is bijgewerkt',\n    'attachments_updated_success' => 'Bijlage details bijgewerkt',\n    'attachments_deleted' => 'Bijlage verwijderd',\n    'attachments_file_uploaded' => 'Bestand succesvol geüpload',\n    'attachments_file_updated' => 'Bestand succesvol bijgewerkt',\n    'attachments_link_attached' => 'Hyperlink succesvol gekoppeld aan de pagina',\n    'templates' => 'Sjablonen',\n    'templates_set_as_template' => 'Pagina is een sjabloon',\n    'templates_explain_set_as_template' => 'Je kan deze pagina als sjabloon instellen zodat de inhoud gebruikt kan worden bij het maken van andere pagina\\'s. Andere gebruikers kunnen dit sjabloon gebruiken als ze de machtiging hebben om deze pagina te zien.',\n    'templates_replace_content' => 'Pagina-inhoud vervangen',\n    'templates_append_content' => 'Toevoegen aan pagina-inhoud',\n    'templates_prepend_content' => 'Voeg vooraan toe aan pagina-inhoud',\n\n    // Profile View\n    'profile_user_for_x' => 'Lid sinds :time',\n    'profile_created_content' => 'Gemaakte Inhoud',\n    'profile_not_created_pages' => ':userName heeft geen pagina\\'s gemaakt',\n    'profile_not_created_chapters' => ':userName heeft geen hoofdstukken gemaakt',\n    'profile_not_created_books' => ':userName heeft geen boeken gemaakt',\n    'profile_not_created_shelves' => ':userName heeft geen boekenplanken gemaakt',\n\n    // Comments\n    'comment' => 'Opmerking',\n    'comments' => 'Opmerkingen',\n    'comment_add' => 'Opmerking toevoegen',\n    'comment_none' => 'Geen opmerkingen om weer te geven',\n    'comment_placeholder' => 'Laat hier een opmerking achter',\n    'comment_thread_count' => ':count Opmerking Thread|:count Opmerking Threads',\n    'comment_archived_count' => ':count Gearchiveerd',\n    'comment_archived_threads' => 'Gearchiveerde Threads',\n    'comment_save' => 'Sla opmerking op',\n    'comment_new' => 'Nieuwe opmerking',\n    'comment_created' => 'opmerking gegeven :createDiff',\n    'comment_updated' => 'Updatet :updateDiff door :username',\n    'comment_updated_indicator' => 'Bijgewerkt',\n    'comment_deleted_success' => 'Opmerking verwijderd',\n    'comment_created_success' => 'Opmerking toegevoegd',\n    'comment_updated_success' => 'Opmerking bijgewerkt',\n    'comment_archive_success' => 'Opmerking gearchiveerd',\n    'comment_unarchive_success' => 'Opmerking teruggehaald',\n    'comment_view' => 'Opmerking weergeven',\n    'comment_jump_to_thread' => 'Ga naar thread',\n    'comment_delete_confirm' => 'Weet je zeker dat je deze opmerking wilt verwijderen?',\n    'comment_in_reply_to' => 'Als antwoord op :commentId',\n    'comment_reference' => 'Verwijzing',\n    'comment_reference_outdated' => '(Verouderd)',\n    'comment_editor_explain' => 'Hier zijn de opmerkingen die zijn achtergelaten op deze pagina. Opmerkingen kunnen worden toegevoegd en beheerd wanneer u de opgeslagen pagina bekijkt.',\n\n    // Revision\n    'revision_delete_confirm' => 'Weet je zeker dat je deze revisie wilt verwijderen?',\n    'revision_restore_confirm' => 'Weet je zeker dat je deze revisie wilt herstellen? De huidige pagina-inhoud wordt vervangen.',\n    'revision_cannot_delete_latest' => 'Kan de laatste revisie niet verwijderen.',\n\n    // Copy view\n    'copy_consider' => 'Houd rekening met het onderstaande wanneer u inhoud kopieert.',\n    'copy_consider_permissions' => 'Aangepaste machtigingsinstellingen worden niet gekopieerd.',\n    'copy_consider_owner' => 'Je wordt de eigenaar van alle gekopieerde inhoud.',\n    'copy_consider_images' => 'Afbeeldingsbestanden worden niet gedupliceerd & de originele afbeeldingen behouden hun koppeling met de pagina waarop ze oorspronkelijk werden geüpload.',\n    'copy_consider_attachments' => 'Pagina bijlagen worden niet gekopieerd.',\n    'copy_consider_access' => 'Een verandering van locatie, eigenaar of machtigingen kan ertoe leiden dat deze inhoud toegankelijk wordt voor personen die eerder geen toegang hadden.',\n\n    // Conversions\n    'convert_to_shelf' => 'Converteer naar Boekenplank',\n    'convert_to_shelf_contents_desc' => 'Je kunt dit boek converteren naar een nieuwe boekenplank met dezelfde inhoud. Hoofdstukken in dit boek zullen worden geconverteerd naar nieuwe boeken. Als dit boek pagina\\'s bevat, die niet in een hoofdstuk staan, zal dit boek een nieuwe naam krijgen en deze pagina\\'s bevatten, en zal dit boek deel gaan uitmaken van de nieuwe boekenplank.',\n    'convert_to_shelf_permissions_desc' => 'Elke machtiging ingesteld op dit boek zal gekopieerd worden naar de nieuwe boekenplank en naar alle nieuwe onderliggende boeken die geen eigen machtiging hebben afgedwongen. Merk op dat boekenplank-machtigingen niet automatisch overdragen naar inhoud binnenin de boekenplank, zoals dat wel gebeurd bij boeken.',\n    'convert_book' => 'Converteer Boek',\n    'convert_book_confirm' => 'Weet je zeker dat je dit boek wil converteren?',\n    'convert_undo_warning' => 'Dit kan niet eenvoudig ongedaan gemaakt worden.',\n    'convert_to_book' => 'Converteer naar Boek',\n    'convert_to_book_desc' => 'Je kan dit hoofdstuk converteren naar een nieuw boek met dezelfde inhoud. Alle machtigingen ingesteld op dit hoofdstuk zullen worden gekopieerd naar het nieuwe boek, maar alle geërfde machtigingen, van het bovenliggende boek, zullen niet worden gekopieerd, wat kan leiden tot een wijziging van de toegangscontrole.',\n    'convert_chapter' => 'Converteer Hoofdstuk',\n    'convert_chapter_confirm' => 'Weet je zeker dat je dit hoofdstuk wil converteren?',\n\n    // References\n    'references' => 'Verwijzingen',\n    'references_none' => 'Er zijn geen verwijzingen naar dit item bijgehouden.',\n    'references_to_desc' => 'Hieronder staat alle bekende inhoud in het systeem die gekoppeld is aan dit item.',\n\n    // Watch Options\n    'watch' => 'Volg',\n    'watch_title_default' => 'Standaard Voorkeuren',\n    'watch_desc_default' => 'Terugkeren naar alleen je standaardvoorkeuren voor meldingen.',\n    'watch_title_ignore' => 'Negeer',\n    'watch_desc_ignore' => 'Negeer alle meldingen, inclusief die van voorkeuren op gebruikersniveau.',\n    'watch_title_new' => 'Nieuwe pagina\\'s',\n    'watch_desc_new' => 'Geef een melding wanneer er een nieuwe pagina wordt gemaakt binnen dit item.',\n    'watch_title_updates' => 'Alle pagina updates',\n    'watch_desc_updates' => 'Geef een melding van alle nieuwe pagina\\'s en pagina wijzigingen.',\n    'watch_desc_updates_page' => 'Geef een melding van pagina wijzigingen.',\n    'watch_title_comments' => 'Alle Pagina Updates & Opmerkingen',\n    'watch_desc_comments' => 'Geef een melding van alle nieuwe pagina\\'s, pagina wijzigingen en nieuwe opmerkingen.',\n    'watch_desc_comments_page' => 'Geef een melding van pagina wijzigingen en nieuwe opmerkingen.',\n    'watch_change_default' => 'Standaardvoorkeuren voor meldingen wijzigen',\n    'watch_detail_ignore' => 'Meldingen negeren',\n    'watch_detail_new' => 'Nieuwe pagina\\'s aan het volgen',\n    'watch_detail_updates' => 'Nieuwe pagina\\'s en aanpassingen aan het volgen',\n    'watch_detail_comments' => 'Nieuwe pagina\\'s, aanpassingen en opmerkingen aan het volgen',\n    'watch_detail_parent_book' => 'Aan het volgen via bovenliggend boek',\n    'watch_detail_parent_book_ignore' => 'Aan het negeren via bovenliggend boek',\n    'watch_detail_parent_chapter' => 'Aan het volgen via bovenliggend hoofdstuk',\n    'watch_detail_parent_chapter_ignore' => 'Aan het negeren via bovenliggend hoofdstuk',\n];\n"
  },
  {
    "path": "lang/nl/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Je hebt geen machtiging om de gevraagde pagina te openen.',\n    'permissionJson' => 'Je hebt geen machtiging om de gevraagde actie uit te voeren.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Er bestaat al een gebruiker met het e-mailadres :email, maar met andere inloggegevens.',\n    'auth_pre_register_theme_prevention' => 'Het gebruikersaccount kon niet worden geregistreerd met de opgegeven informatie',\n    'email_already_confirmed' => 'Het e-mailadres is al bevestigd, probeer in te loggen.',\n    'email_confirmation_invalid' => 'Deze bevestigingstoken is niet geldig of al gebruikt, probeer opnieuw te registreren.',\n    'email_confirmation_expired' => 'Het bevestigingstoken is verlopen, Er is een nieuwe bevestigingsmail verzonden.',\n    'email_confirmation_awaiting' => 'Het e-mailadres van dit account moet worden bevestigd',\n    'ldap_fail_anonymous' => 'LDAP-toegang met \\'anonymous bind\\' is mislukt',\n    'ldap_fail_authed' => 'LDAP-toegang is mislukt met het opgegeven dn & wachtwoord',\n    'ldap_extension_not_installed' => 'LDAP PHP-extensie is niet geïnstalleerd',\n    'ldap_cannot_connect' => 'Kan geen verbinding maken met de ldap server, initiële verbinding is mislukt',\n    'saml_already_logged_in' => 'Reeds ingelogd',\n    'saml_no_email_address' => 'In de gegevens van het externe verificatiesysteem kon voor deze gebruiker geen e-mailadres gevonden worden',\n    'saml_invalid_response_id' => 'Het verzoek van het externe authenticatiesysteem wordt niet herkend door een proces dat door deze applicatie wordt gestart. Terugkeren na inloggen kan dit probleem veroorzaken.',\n    'saml_fail_authed' => 'Inloggen met :system mislukt, het systeem gaf geen succesvolle autorisatie',\n    'oidc_already_logged_in' => 'Reeds ingelogd',\n    'oidc_no_email_address' => 'In de gegevens van het externe verificatiesysteem kon voor deze gebruiker geen e-mailadres gevonden worden',\n    'oidc_fail_authed' => 'Inloggen met :system mislukt, systeem heeft geen succesvolle autorisatie gegeven',\n    'social_no_action_defined' => 'Geen actie gedefinieerd',\n    'social_login_bad_response' => \"Fout ontvangen tijdens :socialAccount login: \\n:error\",\n    'social_account_in_use' => 'Dit :socialAccount account is al in gebruik, Probeer in te loggen met de :socialAccount optie.',\n    'social_account_email_in_use' => 'Het e-mailadres :email is al in gebruik. Als je al een account hebt, kun je met een :socialAccount account verbinden in je profielinstellingen.',\n    'social_account_existing' => 'Dit :socialAccount is al gekoppeld aan je profiel.',\n    'social_account_already_used_existing' => 'Dit :socialAccount account is al gebruikt door een andere gebruiker.',\n    'social_account_not_used' => 'Dit :socialAccount account is niet gekoppeld aan een gebruiker. Koppel het via je profielinstellingen. ',\n    'social_account_register_instructions' => 'Als je nog geen account hebt, kun je je registreren met de :socialAccount optie.',\n    'social_driver_not_found' => 'Social driver niet gevonden',\n    'social_driver_not_configured' => 'Je :socialAccount instellingen zijn niet correct geconfigureerd.',\n    'invite_token_expired' => 'Deze uitnodigingslink is verlopen. Je kunt in plaats daarvan proberen je wachtwoord te herstellen.',\n    'login_user_not_found' => 'Er is geen gebruiker gevonden voor deze actie.',\n\n    // System\n    'path_not_writable' => 'Bestandspad :filePath kon niet naar geüpload worden. Zorg dat je schrijfrechten op de server hebt.',\n    'cannot_get_image_from_url' => 'Kon geen afbeelding verkrijgen van :url',\n    'cannot_create_thumbs' => 'De server kon geen miniaturen maken. Controleer of je de GD PHP extensie geïnstalleerd hebt.',\n    'server_upload_limit' => 'De server staat geen uploads van deze grootte toe. Probeer een kleinere bestandsgrootte.',\n    'server_post_limit' => 'De server kan de opgegeven hoeveelheid gegevens niet ontvangen. Probeer het opnieuw met minder gegevens of een kleiner bestand.',\n    'uploaded'  => 'De server staat geen uploads van deze grootte toe. Probeer een kleinere bestandsgrootte.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Er is een fout opgetreden bij het uploaden van de afbeelding',\n    'image_upload_type_error' => 'Het geüploade afbeeldingstype is ongeldig',\n    'image_upload_replace_type' => 'Afbeeldingen moeten van hetzelfde type zijn',\n    'image_upload_memory_limit' => 'Het uploaden van afbeeldingen en/of het maken van miniaturen is mislukt vanwege te beperkte systeemmiddelen.',\n    'image_thumbnail_memory_limit' => 'Het maken van variaties in afbeeldingsgrootte is mislukt vanwege te beperkte systeemmiddelen.',\n    'image_gallery_thumbnail_memory_limit' => 'Het maken van galerij miniaturen is mislukt vanwege te beperkte systeemmiddelen.',\n    'drawing_data_not_found' => 'De gegevens van de tekening konden niet worden geladen. Het tekenbestand bestaat misschien niet meer of je hebt geen machtiging om het te openen.',\n\n    // Attachments\n    'attachment_not_found' => 'Bijlage niet gevonden',\n    'attachment_upload_error' => 'Er is een fout opgetreden bij het uploaden van het bestand',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Kon het concept niet opslaan. Zorg ervoor dat je een werkende internetverbinding hebt',\n    'page_draft_delete_fail' => 'Het is niet gelukt om het concept van de pagina te verwijderen en de opgeslagen inhoud van de huidige pagina op te halen',\n    'page_custom_home_deletion' => 'Een pagina die als startpagina is ingesteld, kan niet verwijderd worden',\n\n    // Entities\n    'entity_not_found' => 'Entiteit niet gevonden',\n    'bookshelf_not_found' => 'Boekenplank niet gevonden',\n    'book_not_found' => 'Boek niet gevonden',\n    'page_not_found' => 'Pagina niet gevonden',\n    'chapter_not_found' => 'Hoofdstuk niet gevonden',\n    'selected_book_not_found' => 'Het geselecteerde boek is niet gevonden',\n    'selected_book_chapter_not_found' => 'Het geselecteerde boek of hoofdstuk is niet gevonden',\n    'guests_cannot_save_drafts' => 'Gasten kunnen geen concepten opslaan',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Je kunt niet het enige admin account verwijderen',\n    'users_cannot_delete_guest' => 'Je kunt het gastaccount niet verwijderen',\n    'users_could_not_send_invite' => 'Kan de gebruiker niet aanmaken, uitnodigingsmail kon niet worden verzonden',\n\n    // Roles\n    'role_cannot_be_edited' => 'Deze rol kan niet bewerkt worden',\n    'role_system_cannot_be_deleted' => 'Dit is een systeemrol en kan niet verwijderd worden',\n    'role_registration_default_cannot_delete' => 'Deze rol kan niet verwijerd worden zolang dit de standaardrol na registratie is.',\n    'role_cannot_remove_only_admin' => 'Deze gebruiker is de enige gebruiker die is toegewezen aan de beheerdersrol. Wijs de beheerdersrol toe aan een andere gebruiker voordat u probeert deze hier te verwijderen.',\n\n    // Comments\n    'comment_list' => 'Er is een fout opgetreden tijdens het ophalen van de opmerkingen.',\n    'cannot_add_comment_to_draft' => 'Je kunt geen opmerkingen toevoegen aan een concept.',\n    'comment_add' => 'Er is een fout opgetreden tijdens het aanpassen / toevoegen van de opmerking.',\n    'comment_delete' => 'Er is een fout opgetreden tijdens het verwijderen van de opmerking.',\n    'empty_comment' => 'Kan geen lege opmerking toevoegen.',\n\n    // Error pages\n    '404_page_not_found' => 'Pagina Niet Gevonden',\n    'sorry_page_not_found' => 'Sorry, de pagina die je zocht kan niet gevonden worden.',\n    'sorry_page_not_found_permission_warning' => 'Als je verwachtte dat deze pagina zou bestaan, heb je misschien geen machtiging om deze te bekijken.',\n    'image_not_found' => 'Afbeelding niet gevonden',\n    'image_not_found_subtitle' => 'Sorry, de afbeelding die je zocht is niet beschikbaar.',\n    'image_not_found_details' => 'Als je verwachtte dat deze afbeelding zou bestaan, dan is deze misschien verwijderd.',\n    'return_home' => 'Terug naar home',\n    'error_occurred' => 'Er Ging Iets Fout',\n    'app_down' => ':appName is nu niet beschikbaar',\n    'back_soon' => 'Komt snel weer online.',\n\n    // Import\n    'import_zip_cant_read' => 'Kon het Zip-bestand niet lezen.',\n    'import_zip_cant_decode_data' => 'Kon de data.json Zip-inhoud niet vinden of decoderen.',\n    'import_zip_no_data' => 'Zip-bestand bevat niet de verwachte boek, hoofdstuk of pagina-inhoud.',\n    'import_zip_data_too_large' => 'De inhoud van data.json in de ZIP overschrijdt de ingestelde maximum upload grootte.',\n    'import_validation_failed' => 'De validatie van het Zip-bestand is mislukt met de volgende fouten:',\n    'import_zip_failed_notification' => 'Importeren van het Zip-bestand is mislukt.',\n    'import_perms_books' => 'Je mist de vereiste machtigingen om boeken te maken.',\n    'import_perms_chapters' => 'Je mist de vereiste machtigingen om hoofdstukken te maken.',\n    'import_perms_pages' => 'Je mist de vereiste machtigingen om pagina\\'s te maken.',\n    'import_perms_images' => 'Je mist de vereiste machtigingen om afbeeldingen toe te voegen.',\n    'import_perms_attachments' => 'Je mist de vereiste machtigingen om bijlagen toe te voegen.',\n\n    // API errors\n    'api_no_authorization_found' => 'Geen autorisatie token gevonden',\n    'api_bad_authorization_format' => 'Een autorisatie token is gevonden, maar het formaat schijnt onjuist te zijn',\n    'api_user_token_not_found' => 'Er is geen overeenkomende API-token gevonden voor de opgegeven autorisatie token',\n    'api_incorrect_token_secret' => 'Het opgegeven geheim voor de API-token is onjuist',\n    'api_user_no_api_permission' => 'De eigenaar van de gebruikte API-token heeft geen machtiging om API calls te maken',\n    'api_user_token_expired' => 'De gebruikte autorisatie token is verlopen',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Fout opgetreden bij het verzenden van een test email:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'De URL komt niet overeen met de geconfigureerde toegestane SSR-hosts',\n];\n"
  },
  {
    "path": "lang/nl/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Nieuwe opmerking op pagina: :pageName',\n    'new_comment_intro' => 'Een gebruiker heeft een opmerking geplaatst op een pagina in :appName:',\n    'new_page_subject' => 'Nieuwe pagina: :pageName',\n    'new_page_intro' => 'Een nieuwe pagina is gemaakt in :appName:',\n    'updated_page_subject' => 'Aangepaste pagina: :pageName',\n    'updated_page_intro' => 'Een pagina werd aangepast in :appName:',\n    'updated_page_debounce' => 'Om een stortvloed aan meldingen te voorkomen, zul je een tijdje geen meldingen ontvangen voor verdere bewerkingen van deze pagina door dezelfde redacteur.',\n    'comment_mention_subject' => 'Je bent vermeld in een opmerking op pagina: :pageName',\n    'comment_mention_intro' => 'Je bent vermeld in een opmerking in :appName:',\n\n    'detail_page_name' => 'Pagina Naam:',\n    'detail_page_path' => 'Paginapad:',\n    'detail_commenter' => 'Commentator:',\n    'detail_comment' => 'Opmerking:',\n    'detail_created_by' => 'Gemaakt Door:',\n    'detail_updated_by' => 'Aangepast Door:',\n\n    'action_view_comment' => 'Bekijk Opmerking',\n    'action_view_page' => 'Bekijk Pagina',\n\n    'footer_reason' => 'Deze melding is naar je verzonden omdat :link dit type activiteit voor dit artikel dekt.',\n    'footer_reason_link' => 'je meldingsvoorkeuren',\n];\n"
  },
  {
    "path": "lang/nl/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Vorige',\n    'next'     => 'Volgende &raquo;',\n\n];\n"
  },
  {
    "path": "lang/nl/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Wachtwoorden moeten uit minstens acht tekens bestaan en overeenkomen met de bevestiging.',\n    'user' => \"We kunnen geen gebruiker vinden met dat e-mailadres.\",\n    'token' => 'De wachtwoordhersteltoken is ongeldig voor dit e-mailadres.',\n    'sent' => 'We hebben je een link gestuurd om je wachtwoord te herstellen!',\n    'reset' => 'Je wachtwoord is hersteld!',\n\n];\n"
  },
  {
    "path": "lang/nl/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Mijn account',\n\n    'shortcuts' => 'Snelkoppelingen',\n    'shortcuts_interface' => 'Snelkoppelingen',\n    'shortcuts_toggle_desc' => 'Hier kun je toetscombinaties voor de gebruikersinterface in- of uitschakelen voor navigatie en acties.',\n    'shortcuts_customize_desc' => 'Je kunt elk van de onderstaande toetsencombinaties aanpassen. Druk simpelweg op de gewenste toetscombinatie na het selecteren van de invoer voor een toetscombinatie.',\n    'shortcuts_toggle_label' => 'Toetsencombinaties ingeschakeld',\n    'shortcuts_section_navigation' => 'Navigatie',\n    'shortcuts_section_actions' => 'Gebruikelijke acties',\n    'shortcuts_save' => 'Sla Toetsencombinaties Op',\n    'shortcuts_overlay_desc' => 'Opmerking: Wanneer toetsencombinaties zijn ingeschakeld, is een overlay beschikbaar door op \"?\" te drukken, die de momenteel beschikbare toetscombinaties voor acties op het scherm markeert.',\n    'shortcuts_update_success' => 'Toetsencombinatievoorkeuren zijn bijgewerkt!',\n    'shortcuts_overview_desc' => 'Beheer toetsenbordsnelkoppelingen om door de gebruikersinterface van het systeem te navigeren.',\n\n    'notifications' => 'Melding Voorkeuren',\n    'notifications_desc' => 'Bepaal welke e-mailmeldingen je ontvangt wanneer bepaalde activiteiten in het systeem worden uitgevoerd.',\n    'notifications_opt_own_page_changes' => 'Geef melding bij wijzigingen aan pagina\\'s waarvan ik de eigenaar ben',\n    'notifications_opt_own_page_comments' => 'Geef melding van opmerkingen op pagina\\'s waarvan ik de eigenaar ben',\n    'notifications_opt_comment_mentions' => 'Geef een melding wanneer ik word vermeld in een opmerking',\n    'notifications_opt_comment_replies' => 'Geef melding van reacties op mijn opmerkingen',\n    'notifications_save' => 'Voorkeuren opslaan',\n    'notifications_update_success' => 'Voorkeuren voor meldingen zijn bijgewerkt!',\n    'notifications_watched' => 'Gevolgde & Genegeerde Items',\n    'notifications_watched_desc' => 'Hieronder staan de items waarvoor aangepaste volgvoorkeuren zijn toegepast. Om je voorkeuren voor deze items bij te werken, bekijk je het item en zoek je naar de volgopties in de zijbalk.',\n\n    'auth' => 'Toegang & Beveiliging',\n    'auth_change_password' => 'Wachtwoord Wijzigen',\n    'auth_change_password_desc' => 'Wijzig hier je wachtwoord die je gebruikt om in te loggen op de applicatie. Gebruik minimaal 8 tekens.',\n    'auth_change_password_success' => 'Wachtwoord is bijgewerkt!',\n\n    'profile' => 'Profielgegevens',\n    'profile_desc' => 'De gegevens van je account beheren die je representeren naar andere gebruikers toe, evenals gegevens die worden gebruikt voor communicatie en systeempersonalisatie.',\n    'profile_view_public' => 'Toon Publiek Profiel',\n    'profile_name_desc' => 'Configureer je weergavenaam die zichtbaar zal zijn voor andere gebruikers in het systeem via de activiteit die je uitvoert en de inhoud die je bezit.',\n    'profile_email_desc' => 'Dit e-mailadres wordt gebruikt voor meldingen en afhankelijk van de actieve systeemverificatie ook voor toegang tot het systeem.',\n    'profile_email_no_permission' => 'Helaas heb je geen toestemming om je e-mailadres te wijzigen. Als je dit wilt veranderen, moet je een beheerder vragen om dit voor je te veranderen.',\n    'profile_avatar_desc' => 'Selecteer een afbeelding waarmee je jezelf representeert naar anderen in het systeem. In het ideale geval is deze afbeelding vierkant en ongeveer 256 pixels breed en hoog.',\n    'profile_admin_options' => 'Beheerdersopties',\n    'profile_admin_options_desc' => 'Extra opties op beheerdersniveau, zoals die voor het beheren van roltoewijzingen zijn te vinden voor je gebruikersaccount in het gedeelte \"Instellingen > Gebruikers\" van de applicatie.',\n\n    'delete_account' => 'Verwijder Account',\n    'delete_my_account' => 'Verwijder Mijn Account',\n    'delete_my_account_desc' => 'Dit verwijdert je gebruikersaccount volledig uit het systeem. Niemand kan het account herstellen of deze actie ongedaan maken. Inhoud die je hebt gemaakt, zoals aangemaakte pagina\\'s en geüploade afbeeldingen, blijft bestaan.',\n    'delete_my_account_warning' => 'Weet je zeker dat je je account permanent wil verwijderen?',\n];\n"
  },
  {
    "path": "lang/nl/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Instellingen',\n    'settings_save' => 'Instellingen opslaan',\n    'system_version' => 'Systeem versie',\n    'categories' => 'Categorieën',\n\n    // App Settings\n    'app_customization' => 'Aanpassingen',\n    'app_features_security' => 'Functies en beveiliging',\n    'app_name' => 'Applicatienaam',\n    'app_name_desc' => 'Deze naam wordt getoond in de header en in alle door het systeem verstuurde e-mails.',\n    'app_name_header' => 'Toon naam in header',\n    'app_public_access' => 'Openbare toegang',\n    'app_public_access_desc' => 'Door deze optie in te schakelen kunnen bezoekers die niet ingelogd zijn toegang krijgen tot de inhoud van uw BookStack-omgeving.',\n    'app_public_access_desc_guest' => 'De toegang voor publieke bezoekers kan worden ingesteld via de \"Guest\" gebruiker.',\n    'app_public_access_toggle' => 'Openbare toegang toestaan',\n    'app_public_viewing' => 'Publieke bezichtigingen toestaan?',\n    'app_secure_images' => 'Uploaden van afbeeldingen met hogere beveiliging',\n    'app_secure_images_toggle' => 'Activeer uploaden van afbeeldingen met hogere beveiliging',\n    'app_secure_images_desc' => 'Om prestatieredenen zijn alle afbeeldingen openbaar. Deze optie voegt een willekeurige en moeilijk te raden tekst toe aan de URL\\'s van de afbeeldingen. Zorg ervoor dat \"directory indexes\" niet ingeschakeld zijn om eenvoudige toegang te voorkomen.',\n    'app_default_editor' => 'Standaard Pagina Bewerker',\n    'app_default_editor_desc' => 'Selecteer welke bewerker standaard zal worden gebruikt bij het bewerken van nieuwe pagina\\'s. Dit kan worden overschreven op paginaniveau als de rechten dat toestaan.',\n    'app_custom_html' => 'Aangepaste inhoud voor het HTML head-element',\n    'app_custom_html_desc' => 'Alle hieronder toegevoegde data wordt aan het einde van de <head> sectie van elke pagina toegevoegd. Gebruik dit om stijlen te overschrijven of analytische code toe te voegen.',\n    'app_custom_html_disabled_notice' => 'Bovenstaande wordt niet toegevoegd aan deze pagina om ervoor te zorgen dat je foutieve code steeds ongedaan kan maken.',\n    'app_logo' => 'Applicatielogo',\n    'app_logo_desc' => 'Dit wordt onder meer gebruikt in de kopbalk van de applicatie. Deze afbeelding dient 86px hoog te zijn. Grote afbeeldingen worden teruggeschaald.',\n    'app_icon' => 'Applicatiepictogram',\n    'app_icon_desc' => 'Dit pictogram wordt gebruikt voor browsertabbladen en snelkoppelingspictogrammen. Dit dient een 256px vierkante PNG-afbeelding te zijn.',\n    'app_homepage' => 'Applicatie startpagina',\n    'app_homepage_desc' => 'Selecteer een weergave om weer te geven op de startpagina in plaats van de standaard weergave. Paginamachtigingen worden genegeerd voor geselecteerde pagina\\'s.',\n    'app_homepage_select' => 'Selecteer een pagina',\n    'app_footer_links' => 'Voettekst hyperlinks',\n    'app_footer_links_desc' => 'Voeg hyperlinks toe aan de voettekst van de applicatie. Deze zullen onderaan de meeste pagina\\'s getoond worden, ook aan pagina\\'s die geen login vereisen. Je kunt een label van \"trans::<key>\" gebruiken om systeem-gedefinieerde vertalingen te gebruiken. Bijvoorbeeld: Het gebruik van \"trans::common.privacy_policy\" zal de vertaalde tekst \"Privacy Policy\" opleveren en \"trans::common.terms_of_service\" zal de vertaalde tekst \"Gebruiksvoorwaarden\" opleveren.',\n    'app_footer_links_label' => 'Link label',\n    'app_footer_links_url' => 'Link URL',\n    'app_footer_links_add' => 'Voettekst link toevoegen',\n    'app_disable_comments' => 'Opmerkingen uitschakelen',\n    'app_disable_comments_toggle' => 'Opmerkingen uitschakelen',\n    'app_disable_comments_desc' => 'Schakel opmerkingen uit op alle pagina\\'s in de applicatie. <br> Bestaande opmerkingen worden niet getoond.',\n\n    // Color settings\n    'color_scheme' => 'Kleurenschema van applicatie',\n    'color_scheme_desc' => 'Stel de kleuren in voor de gebruikersinterface van de applicatie. Kleuren kunnen afzonderlijk worden geconfigureerd voor donkere en lichte modi om zo goed mogelijk bij het thema te passen en de leesbaarheid te garanderen.',\n    'ui_colors_desc' => 'Stel de primaire kleur van de applicatie en de standaard hyperlinkkleur in. De primaire kleur wordt voornamelijk gebruikt voor de headerbanner, knoppen en interfacedecoraties. De standaard hyperlinkkleur wordt gebruikt voor tekstgebaseerde links en acties, zowel binnen geschreven inhoud als in de applicatie-interface.',\n    'app_color' => 'Primaire kleur',\n    'link_color' => 'Standaard hyperlinkkleur',\n    'content_colors_desc' => 'Stel kleuren in voor alle elementen in de hiërarchie van de pagina-organisatie. Voor de leesbaarheid is het aan te raden kleuren te kiezen met eenzelfde helderheid als de standaardkleuren.',\n    'bookshelf_color' => 'Kleur van de Boekenplank',\n    'book_color' => 'Kleur van het Boek',\n    'chapter_color' => 'Kleur van het Hoofdstuk',\n    'page_color' => 'Pagina kleur',\n    'page_draft_color' => 'Concept pagina kleur',\n\n    // Registration Settings\n    'reg_settings' => 'Registratie',\n    'reg_enable' => 'Registratie inschakelen',\n    'reg_enable_toggle' => 'Registratie inschakelen',\n    'reg_enable_desc' => 'Wanneer registratie is ingeschakeld, kunnen gebruikers zichzelf aanmelden als applicatiegebruiker. Bij registratie krijgen ze een enkele, standaard gebruikersrol.',\n    'reg_default_role' => 'Standaard rol na registratie',\n    'reg_enable_external_warning' => 'De optie hierboven wordt niet gebruikt terwijl externe LDAP- of SAML authenticatie actief is. Gebruikersaccounts voor niet-bestaande leden zullen automatisch worden aangemaakt wanneer authenticatie tegen het gebruikte externe systeem succesvol is.',\n    'reg_email_confirmation' => 'E-mail bevestiging',\n    'reg_email_confirmation_toggle' => 'E-mailbevestiging vereisen',\n    'reg_confirm_email_desc' => 'Als domeinrestricties aan staan dan is e-maibevestiging altijd nodig. Onderstaande instelling wordt dan genegeerd.',\n    'reg_confirm_restrict_domain' => 'Beperk registratie tot een domein',\n    'reg_confirm_restrict_domain_desc' => 'Geef een door komma-gescheiden lijst van domeinnamen op die gebruikt mogen worden bij registratie. Gebruikers dienen de ontvangen e-mail te bevestigen voordat ze toegang krijgen tot de applicatie. <br>Let op: Gebruikers kunnen na registratie hun e-mailadres nog steeds wijzigen.',\n    'reg_confirm_restrict_domain_placeholder' => 'Geen beperkingen ingesteld',\n\n    // Sorting Settings\n    'sorting' => 'Lijsten & Sorteren',\n    'sorting_book_default' => 'Standaard Sorteerregel Boek',\n    'sorting_book_default_desc' => 'Selecteer de standaard sorteerregel om toe te passen op nieuwe boeken. Dit heeft geen invloed op bestaande boeken, en kan per boek worden overschreven.',\n    'sorting_rules' => 'Sorteerregels',\n    'sorting_rules_desc' => 'Dit zijn vooraf ingestelde sorteeroperaties die kunnen worden toegepast op inhoud in het systeem.',\n    'sort_rule_assigned_to_x_books' => 'Toegepast op :count Boek|Toegepast op :count Boeken',\n    'sort_rule_create' => 'Maak sorteerregel',\n    'sort_rule_edit' => 'Bewerk sorteerregel',\n    'sort_rule_delete' => 'Verwijder sorteerregel',\n    'sort_rule_delete_desc' => 'Verwijder deze sorteerregel uit het systeem. Boeken die deze regel gebruiken zullen terugvallen op handmatige sortering.',\n    'sort_rule_delete_warn_books' => 'Deze sorteerregel wordt momenteel gebruikt door :count boek(en). Weet je zeker dat je deze wilt verwijderen?',\n    'sort_rule_delete_warn_default' => 'Deze sorteerregel wordt gebruikt als standaardregel voor boeken. Weet je zeker dat je deze wilt verwijderen?',\n    'sort_rule_details' => 'Details sorteerregel',\n    'sort_rule_details_desc' => 'Stel een naam in voor deze sorteerregel. Deze wordt weergegeven waar gebruikers een regel kunnen selecteren.',\n    'sort_rule_operations' => 'Sorteeroperaties',\n    'sort_rule_operations_desc' => 'Configureer de sorteeracties die moeten worden uitgevoerd door ze te verplaatsen van de lijst met beschikbare operaties. Bij gebruik worden de operaties toegepast in volgorde van boven naar beneden. Wijzigingen die hier worden gemaakt worden toegepast op alle toegewezen boeken.',\n    'sort_rule_available_operations' => 'Beschikbare Operaties',\n    'sort_rule_available_operations_empty' => 'Geen operaties over',\n    'sort_rule_configured_operations' => 'Ingestelde Operaties',\n    'sort_rule_configured_operations_empty' => 'Voeg operaties toe uit de \"Beschikbare Operaties\" lijst',\n    'sort_rule_op_asc' => '(Oplopend)',\n    'sort_rule_op_desc' => '(Aflopend)',\n    'sort_rule_op_name' => 'Naam - Alfabetisch',\n    'sort_rule_op_name_numeric' => 'Naam - Numeriek',\n    'sort_rule_op_created_date' => 'Aanmaakdatum',\n    'sort_rule_op_updated_date' => 'Bijwerkdatum',\n    'sort_rule_op_chapters_first' => 'Hoofdstukken Eerst',\n    'sort_rule_op_chapters_last' => 'Hoofdstukken Laatst',\n    'sorting_page_limits' => 'Weergavelimiet Per Pagina',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Onderhoud',\n    'maint_image_cleanup' => 'Afbeeldingen opschonen',\n    'maint_image_cleanup_desc' => 'Scant pagina- en revisieinhoud om te controleren welke afbeeldingen en tekeningen momenteel worden gebruikt en welke afbeeldingen overbodig zijn. Zorg ervoor dat je een volledige database- en afbeelding back-up maakt voordat je dit uitvoert.',\n    'maint_delete_images_only_in_revisions' => 'Ook afbeeldingen verwijderen die alleen in oude paginarevisies bestaan',\n    'maint_image_cleanup_run' => 'Opschonen uitvoeren',\n    'maint_image_cleanup_warning' => ':count potentieel ongebruikte afbeeldingen gevonden. Weet je zeker dat je deze afbeeldingen wilt verwijderen?',\n    'maint_image_cleanup_success' => ':count potentieel ongebruikte afbeeldingen gevonden en verwijderd!',\n    'maint_image_cleanup_nothing_found' => 'Geen ongebruikte afbeeldingen gevonden, niets verwijderd!',\n    'maint_send_test_email' => 'Stuur een test e-mail',\n    'maint_send_test_email_desc' => 'Dit verstuurt een test e-mail naar het e-mailadres dat je in je profiel hebt opgegeven.',\n    'maint_send_test_email_run' => 'Verzend test e-mail',\n    'maint_send_test_email_success' => 'E-mail verzonden naar :address',\n    'maint_send_test_email_mail_subject' => 'Test E-mail',\n    'maint_send_test_email_mail_greeting' => 'E-mailbezorging lijkt te werken!',\n    'maint_send_test_email_mail_text' => 'Gefeliciteerd! Nu je deze e-mailmelding hebt ontvangen, lijken je e-mailinstellingen correct te zijn geconfigureerd.',\n    'maint_recycle_bin_desc' => 'Verwijderde boekenplanken, boeken, hoofdstukken en pagina\\'s worden naar de prullenbak gestuurd waar ze hersteld of definitief verwijderd kunnen worden. Oudere items in de prullenbak kunnen automatisch worden verwijderd, afhankelijk van de systeemconfiguratie.',\n    'maint_recycle_bin_open' => 'Prullenbak openen',\n    'maint_regen_references' => 'Verwijzingen opnieuw genereren',\n    'maint_regen_references_desc' => 'Deze actie zal de kruisverwijzingen index binnen de database opnieuw opbouwen. Dit wordt doorgaans automatisch gedaan, maar deze actie kan nuttig zijn om oude inhoud of inhoud die via onofficiële methoden is toegevoegd te indexeren.',\n    'maint_regen_references_success' => 'Verwijzingenindex is opnieuw gegenereerd!',\n    'maint_timeout_command_note' => 'Let op: Het uitvoeren van deze actie kan enige tijd in beslag nemen, wat in sommige webomgevingen kan leiden tot time-outs. Als alternatief kan deze actie ook worden uitgevoerd met een terminal-commando.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Prullenbak',\n    'recycle_bin_desc' => 'Hier kun je items herstellen die zijn verwijderd of ervoor kiezen om ze permanent uit het systeem te verwijderen. Deze lijst is ongefilterd, in tegenstelling tot vergelijkbare activiteitenlijsten in het systeem waar machtigingenfilters worden toegepast.',\n    'recycle_bin_deleted_item' => 'Verwijderde Item',\n    'recycle_bin_deleted_parent' => 'Bovenliggende',\n    'recycle_bin_deleted_by' => 'Verwijderd door',\n    'recycle_bin_deleted_at' => 'Verwijderd op',\n    'recycle_bin_permanently_delete' => 'Permanent verwijderen',\n    'recycle_bin_restore' => 'Herstellen',\n    'recycle_bin_contents_empty' => 'De prullenbak is momenteel leeg',\n    'recycle_bin_empty' => 'Prullenbak legen',\n    'recycle_bin_empty_confirm' => 'Dit zal permanent alle items in de prullenbak vernietigen, inclusief de inhoud die in elk item zit. Weet je zeker dat je de prullenbak wil legen?',\n    'recycle_bin_destroy_confirm' => 'Deze actie zal dit item permanent verwijderen uit het systeem, samen met onderstaande onderliggende elementen, en u zal deze inhoud niet kunnen herstellen. Bent u zeker dat u dit item permanent wilt verwijderen?',\n    'recycle_bin_destroy_list' => 'Te vernietigen items',\n    'recycle_bin_restore_list' => 'Items te herstellen',\n    'recycle_bin_restore_confirm' => 'Deze actie herstelt het verwijderde item, inclusief alle onderliggende elementen, op hun oorspronkelijke locatie. Als de oorspronkelijke locatie sindsdien is verwijderd en zich nu in de prullenbak bevindt, zal ook het bovenliggende item moeten worden hersteld.',\n    'recycle_bin_restore_deleted_parent' => 'De bovenliggende map van dit item is ook verwijderd. Dit zal verwijderd blijven tot het bovenliggende ook hersteld is.',\n    'recycle_bin_restore_parent' => 'Herstel bovenliggende',\n    'recycle_bin_destroy_notification' => ':count items uit de prullenbak verwijderd.',\n    'recycle_bin_restore_notification' => ':count items uit de prullenbak hersteld.',\n\n    // Audit Log\n    'audit' => 'Controlelogboek',\n    'audit_desc' => 'Dit controle logboek toont een lijst van activiteiten die in het systeem zijn bijgehouden. Deze lijst is ongefilterd in tegenstelling tot soortgelijke activiteitenlijsten in het systeem waar machtigingfilters worden toegepast.',\n    'audit_event_filter' => 'Gebeurtenis filter',\n    'audit_event_filter_no_filter' => 'Geen filter',\n    'audit_deleted_item' => 'Verwijderd Item',\n    'audit_deleted_item_name' => 'Naam: :name',\n    'audit_table_user' => 'Gebruiker',\n    'audit_table_event' => 'Gebeurtenis',\n    'audit_table_related' => 'Gerelateerd Item of Detail',\n    'audit_table_ip' => 'IP-adres',\n    'audit_table_date' => 'Activiteit datum',\n    'audit_date_from' => 'Datum bereik vanaf',\n    'audit_date_to' => 'Datum bereik tot',\n\n    // Role Settings\n    'roles' => 'Rollen',\n    'role_user_roles' => 'Gebruikersrollen',\n    'roles_index_desc' => 'Rollen worden gebruikt om gebruikers te groeperen en systeemrechten te geven. Wanneer een gebruiker lid is van meerdere rollen worden de toegekende rechten samengevoegd en erft de gebruiker alle mogelijkheden.',\n    'roles_x_users_assigned' => ':count gebruiker toegewezen|:count gebruikers toegewezen',\n    'roles_x_permissions_provided' => ':count machtiging|:count machtigingen',\n    'roles_assigned_users' => 'Toegewezen Gebruikers',\n    'roles_permissions_provided' => 'Verleende Machtigingen',\n    'role_create' => 'Nieuwe Rol Maken',\n    'role_delete' => 'Rol Verwijderen',\n    'role_delete_confirm' => 'Dit verwijdert de rol met naam: \\':roleName\\'.',\n    'role_delete_users_assigned' => 'Er zijn :userCount gebruikers met deze rol. Selecteer hieronder een nieuwe rol als je deze gebruikers een andere rol wilt geven.',\n    'role_delete_no_migration' => \"Geen gebruikers migreren\",\n    'role_delete_sure' => 'Weet je zeker dat je deze rol wilt verwijderen?',\n    'role_edit' => 'Rol Bewerken',\n    'role_details' => 'Rol Details',\n    'role_name' => 'Rolnaam',\n    'role_desc' => 'Korte beschrijving van de rol',\n    'role_mfa_enforced' => 'Meervoudige verificatie verreist',\n    'role_external_auth_id' => 'Externe authenticatie ID\\'s',\n    'role_system' => 'Systeem Machtigingen',\n    'role_manage_users' => 'Gebruikers beheren',\n    'role_manage_roles' => 'Beheer rollen & machtigingen',\n    'role_manage_entity_permissions' => 'Beheer alle machtigingen voor boeken, hoofdstukken en pagina\\'s',\n    'role_manage_own_entity_permissions' => 'Beheer machtigingen van je eigen boek, hoofdstuk & pagina\\'s',\n    'role_manage_page_templates' => 'Paginasjablonen beheren',\n    'role_access_api' => 'Ga naar systeem API',\n    'role_manage_settings' => 'Beheer app instellingen',\n    'role_export_content' => 'Exporteer inhoud',\n    'role_import_content' => 'Importeer inhoud',\n    'role_editor_change' => 'Wijzig pagina bewerker',\n    'role_notifications' => 'Meldingen ontvangen & beheren',\n    'role_permission_note_users_and_roles' => 'Deze machtigingen geven technisch gezien toegang tot het weergeven van gebruikers & rollen binnen het systeem.',\n    'role_asset' => 'Asset Machtigingen',\n    'roles_system_warning' => 'Wees ervan bewust dat toegang tot een van de bovengenoemde drie machtigingen een gebruiker in staat kan stellen zijn eigen machtigingen of de machtigingen van anderen in het systeem kan wijzigen. Wijs alleen rollen toe met deze machtigingen aan vertrouwde gebruikers.',\n    'role_asset_desc' => 'Deze machtigingen bepalen de standaard toegang tot de assets binnen het systeem. Machtigingen op boeken, hoofdstukken en pagina\\'s overschrijven deze instelling.',\n    'role_asset_admins' => 'Beheerders krijgen automatisch toegang tot alle inhoud, maar deze opties kunnen gebruikersinterface opties tonen of verbergen.',\n    'role_asset_image_view_note' => 'Dit heeft betrekking op de zichtbaarheid binnen de afbeeldingsbeheerder. De werkelijke toegang tot geüploade afbeeldingsbestanden hangt af van de gekozen opslagmethode.',\n    'role_asset_users_note' => 'Deze machtigingen geven technisch gezien toegang tot het weergeven van gebruikers binnen het systeem.',\n    'role_all' => 'Alles',\n    'role_own' => 'Eigen',\n    'role_controlled_by_asset' => 'Gecontroleerd door de asset waar deze is geüpload',\n    'role_save' => 'Rol Opslaan',\n    'role_users' => 'Gebruikers in deze rol',\n    'role_users_none' => 'Geen enkele gebruiker heeft deze rol',\n\n    // Users\n    'users' => 'Gebruikers',\n    'users_index_desc' => 'Creëer en beheer individuele gebruikersaccounts binnen het systeem. Gebruikersaccounts worden gebruikt voor aanmelding en toekenning van inhoud en activiteiten. Toegangsmachtigingen zijn voornamelijk gebaseerd op rollen, maar het eigendom van gebruikersinhoud en andere factoren kunnen ook van invloed zijn op machtigingen en toegang.',\n    'user_profile' => 'Gebruikersprofiel',\n    'users_add_new' => 'Gebruiker toevoegen',\n    'users_search' => 'Gebruiker zoeken',\n    'users_latest_activity' => 'Laatste activiteit',\n    'users_details' => 'Gebruiker details',\n    'users_details_desc' => 'Stel een weergavenaam en e-mailadres in voor deze gebruiker. Het e-mailadres zal worden gebruikt om in te loggen.',\n    'users_details_desc_no_email' => 'Stel een weergavenaam in voor deze gebruiker zodat anderen deze kunnen herkennen.',\n    'users_role' => 'Gebruikersrollen',\n    'users_role_desc' => 'Selecteer aan welke rollen deze gebruiker zal worden toegewezen. Als een gebruiker aan meerdere rollen wordt toegewezen, worden de machtigingen van die rollen samengevoegd en krijgt hij alle mogelijkheden van de toegewezen rollen.',\n    'users_password' => 'Wachtwoord gebruiker',\n    'users_password_desc' => 'Stel een wachtwoord in om op de applicatie in te loggen. Dit moet minstens 8 tekens lang zijn.',\n    'users_send_invite_text' => 'Je kunt ervoor kiezen om deze gebruiker een uitnodigingsmail te sturen waarmee hij zijn eigen wachtwoord kan instellen, anders kun je zelf zijn wachtwoord instellen.',\n    'users_send_invite_option' => 'Stuur gebruiker uitnodigings e-mail',\n    'users_external_auth_id' => 'Externe authenticatie ID',\n    'users_external_auth_id_desc' => 'Wanneer een extern authenticatiesysteem wordt gebruikt (zoals SAML2, OIDC of LDAP) is dit de ID die deze BookStack-gebruiker koppelt aan het account van het authenticatiesysteem. Je kunt dit veld negeren als je de standaard op e-mail gebaseerde verificatie gebruikt.',\n    'users_password_warning' => 'Vul onderstaande velden alleen in als je het wachtwoord voor deze gebruiker wil wijzigen.',\n    'users_system_public' => 'Deze gebruiker vertegenwoordigt alle gastgebruikers die uw applicatie bezoeken. Hij kan niet worden gebruikt om in te loggen, maar wordt automatisch toegewezen.',\n    'users_delete' => 'Verwijder gebruiker',\n    'users_delete_named' => 'Verwijder gebruiker :userName',\n    'users_delete_warning' => 'Dit zal de gebruiker \\':userName\\' volledig uit het systeem verwijderen.',\n    'users_delete_confirm' => 'Weet je zeker dat je deze gebruiker wilt verwijderen?',\n    'users_migrate_ownership' => 'Draag eigendom over',\n    'users_migrate_ownership_desc' => 'Selecteer een gebruiker hier als u wilt dat een andere gebruiker de eigenaar wordt van alle items die momenteel eigendom zijn van deze gebruiker.',\n    'users_none_selected' => 'Geen gebruiker geselecteerd',\n    'users_edit' => 'Bewerk Gebruiker',\n    'users_edit_profile' => 'Bewerk Profiel',\n    'users_avatar' => 'Avatar',\n    'users_avatar_desc' => 'Selecteer een afbeelding om deze gebruiker voor te stellen. Deze moet ongeveer 256px breed en vierkant zijn.',\n    'users_preferred_language' => 'Voorkeurstaal',\n    'users_preferred_language_desc' => 'Deze optie wijzigt de taal die gebruikt wordt voor de gebruikersinterface. Dit heeft geen invloed op door gebruikers gemaakte inhoud.',\n    'users_social_accounts' => 'Sociale media accounts',\n    'users_social_accounts_desc' => 'Bekijk de status van de verbonden socialmedia-accounts voor deze gebruiker. socialmedia-accounts kunnen worden gebruikt naast het primaire authenticatiesysteem voor systeemtoegang.',\n    'users_social_accounts_info' => 'Hier kun je je andere accounts koppelen om sneller en eenvoudiger in te loggen. Als je hier een account loskoppelt, wordt de eerder gemachtigde toegang niet ingetrokken. Je kunt de toegang intrekken via je profielinstellingen op het gekoppelde socialemedia-account zelf.',\n    'users_social_connect' => 'Account Verbinden',\n    'users_social_disconnect' => 'Account Ontkoppelen',\n    'users_social_status_connected' => 'Verbonden',\n    'users_social_status_disconnected' => 'Verbroken',\n    'users_social_connected' => ':socialAccount account succesvol aan je profiel gekoppeld.',\n    'users_social_disconnected' => ':socialAccount account succesvol ontkoppeld van je profiel.',\n    'users_api_tokens' => 'API-Tokens',\n    'users_api_tokens_desc' => 'Creëer en beheer de toegangstokens die gebruikt worden om te authenticeren met de BookStack REST API. Machtigingen voor de API worden beheerd via de gebruiker waartoe het token behoort.',\n    'users_api_tokens_none' => 'Er zijn geen API-tokens gemaakt voor deze gebruiker',\n    'users_api_tokens_create' => 'Token aanmaken',\n    'users_api_tokens_expires' => 'Verloopt',\n    'users_api_tokens_docs' => 'API-Documentatie',\n    'users_mfa' => 'Meervoudige Verificatie',\n    'users_mfa_desc' => 'Stel meervoudige verificatie in als extra beveiligingslaag voor je gebruikersaccount.',\n    'users_mfa_x_methods' => ':count methode geconfigureerd|:count methoden geconfigureerd',\n    'users_mfa_configure' => 'Configureer methoden',\n\n    // API Tokens\n    'user_api_token_create' => 'API-token aanmaken',\n    'user_api_token_name' => 'Naam',\n    'user_api_token_name_desc' => 'Geef je token een leesbare naam als een toekomstige herinnering aan het beoogde doel.',\n    'user_api_token_expiry' => 'Vervaldatum',\n    'user_api_token_expiry_desc' => 'Stel een datum in waarop deze token verloopt. Na deze datum zullen aanvragen die met deze token zijn ingediend niet langer werken. Als dit veld leeg blijft, wordt een vervaldatum van 100 jaar in de toekomst ingesteld.',\n    'user_api_token_create_secret_message' => 'Onmiddellijk na het aanmaken van dit token zal een \"Token ID\" en \"Token Geheim\" worden gegenereerd en weergegeven. Het geheim zal slechts één keer getoond worden. Kopieer de waarde dus eerst op een veilige plaats voordat u doorgaat.',\n    'user_api_token' => 'API-Token',\n    'user_api_token_id' => 'Token ID',\n    'user_api_token_id_desc' => 'Dit is een niet-wijzigbare, door het systeem gegenereerde identificatiecode voor dit token, die in API-verzoeken moet worden verstrekt.',\n    'user_api_token_secret' => 'Geheime token sleutel',\n    'user_api_token_secret_desc' => 'Dit is een door het systeem gegenereerd geheim voor dit token dat in API-verzoeken zal moeten worden verstrekt. Dit zal slechts één keer worden weergegeven, dus kopieer deze waarde naar een veilige plaats.',\n    'user_api_token_created' => 'Token :timeAgo geleden aangemaakt',\n    'user_api_token_updated' => 'Token :timeAgo geleden bijgewerkt',\n    'user_api_token_delete' => 'Token Verwijderen',\n    'user_api_token_delete_warning' => 'Dit zal de API-token met de naam \\':tokenName\\' volledig uit het systeem verwijderen.',\n    'user_api_token_delete_confirm' => 'Weet je zeker dat je deze API-token wilt verwijderen?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhooks zijn een manier om gegevens naar externe URL\\'s te sturen wanneer bepaalde acties en gebeurtenissen in het systeem plaatsvinden, wat op gebeurtenissen gebaseerde integratie met externe platforms zoals berichten- of notificatiesystemen mogelijk maakt.',\n    'webhooks_x_trigger_events' => ':count trigger event|:count trigger events',\n    'webhooks_create' => 'Nieuwe Webhook Maken',\n    'webhooks_none_created' => 'Er zijn nog geen webhooks aangemaakt.',\n    'webhooks_edit' => 'Bewerk Webhook',\n    'webhooks_save' => 'Webhook opslaan',\n    'webhooks_details' => 'Webhook Details',\n    'webhooks_details_desc' => 'Geef een gebruiksvriendelijke naam en een POST eindpunt op als locatie waar de webhook gegevens naartoe gestuurd zullen worden.',\n    'webhooks_events' => 'Webhook gebeurtenissen',\n    'webhooks_events_desc' => 'Selecteer alle gebeurtenissen die deze webhook dient te activeren.',\n    'webhooks_events_warning' => 'Houd er rekening mee dat deze gebeurtenissen zullen worden geactiveerd voor alle geselecteerde gebeurtenissen, zelfs als aangepaste machtigingen zijn toegepast. Zorg ervoor dat het gebruik van deze webhook geen vertrouwelijke inhoud blootlegt.',\n    'webhooks_events_all' => 'Alle systeemgebeurtenissen',\n    'webhooks_name' => 'Webhook Naam',\n    'webhooks_timeout' => 'Webhook Verzoek Time-out (Seconden)',\n    'webhooks_endpoint' => 'Webhook Eindpunt',\n    'webhooks_active' => 'Webhook Actief',\n    'webhook_events_table_header' => 'Gebeurtenissen',\n    'webhooks_delete' => 'Verwijder Webhook',\n    'webhooks_delete_warning' => 'Dit zal de webhook met naam \\':webhookName\\' volledig verwijderen van het systeem.',\n    'webhooks_delete_confirm' => 'Weet je zeker dat je deze webhook wilt verwijderen?',\n    'webhooks_format_example' => 'Voorbeeld Webhook Formaat',\n    'webhooks_format_example_desc' => 'Webhook gegevens worden verzonden als een POST verzoek naar het geconfigureerde eindpunt als JSON volgens het onderstaande formaat. De \"related_item\" en \"url\" eigenschappen zijn optioneel en hangen af van het type gebeurtenis die geactiveerd wordt.',\n    'webhooks_status' => 'Webhook Status',\n    'webhooks_last_called' => 'Laatst Opgeroepen:',\n    'webhooks_last_errored' => 'Laatst Gefaald:',\n    'webhooks_last_error_message' => 'Laatste Foutmelding:',\n\n    // Licensing\n    'licenses' => 'Licenties',\n    'licenses_desc' => 'Deze pagina beschrijft licentie-informatie voor BookStack naast de projecten & bibliotheken die binnen BookStack worden gebruikt. Veel van de vermelde projecten worden alleen in een ontwikkelingscontext gebruikt.',\n    'licenses_bookstack' => 'BookStack Licentie',\n    'licenses_php' => 'PHP Bibliotheek Licenties',\n    'licenses_js' => 'JavaScript Bibliotheek Licenties',\n    'licenses_other' => 'Andere Licenties',\n    'license_details' => 'Licentie Details',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'Engels',\n        'ar' => 'العربية (Arabisch)',\n        'bg' => 'Bǎlgarski (Bulgaars)',\n        'bs' => 'Bosanski (Bosnisch)',\n        'ca' => 'Català (Catalaans)',\n        'cs' => 'Česky (Tsjechisch)',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk (Deens)',\n        'de' => 'Deutsch (Duits)',\n        'de_informal' => 'Deutsch (Du) (Informeel Duits)',\n        'el' => 'ελληνικά',\n        'es' => 'Español (Spaans)',\n        'es_AR' => 'Español Argentina (Argentijns Spaans)',\n        'et' => 'Eesti keel (Estisch)',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français (Frans)',\n        'he' => 'עברית (Hebreeuws)',\n        'hr' => 'Hrvatski (Kroatisch)',\n        'hu' => 'Magyar (Hongaars)',\n        'id' => 'Bahasa Indonesia (Indonesisch)',\n        'it' => 'Italiano (Italiaans)',\n        'ja' => '日本語 (Japans)',\n        'ko' => '한국어 (Koreaans)',\n        'lt' => 'Lietuvių Kalba (Litouws)',\n        'lv' => 'Latviešu Valoda (Lets)',\n        'nb' => 'Norsk (Bokmål) (Noors)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski (Pools)',\n        'pt' => 'Português (Portugees)',\n        'pt_BR' => 'Português do Brasil (Braziliaans-Portugees)',\n        'ro' => 'Română',\n        'ru' => 'Русский (Russisch)',\n        'sk' => 'Slovensky (Slowaaks)',\n        'sl' => 'Slovenščina (Sloveens)',\n        'sv' => 'Svenska (Zweeds)',\n        'tr' => 'Türkçe (Turks)',\n        'uk' => 'Українська (Oekraïens)',\n        'uz' => 'Oezbeeks',\n        'vi' => 'Tiếng Việt (Vietnamees)',\n        'zh_CN' => '简体中文 (Chinees)',\n        'zh_TW' => '繁體中文 (Traditioneel Chinees)',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/nl/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => 'Het :attribute moet geaccepteerd worden.',\n    'active_url'           => 'Het :attribute is geen geldige URL.',\n    'after'                => ':attribute moet een datum zijn later dan :date.',\n    'alpha'                => ':attribute mag alleen letters bevatten.',\n    'alpha_dash'           => ':attribute mag alleen letters, cijfers, streepjes en liggende streepjes bevatten.',\n    'alpha_num'            => ':attribute mag alleen letters en nummers bevatten.',\n    'array'                => ':attribute moet een reeks zijn.',\n    'backup_codes'         => 'De opgegeven code is niet geldig of eerder al gebruikt.',\n    'before'               => ':attribute moet een datum zijn voor :date.',\n    'between'              => [\n        'numeric' => ':attribute moet tussen de :min en :max zijn.',\n        'file'    => ':attribute moet tussen de :min en :max kilobytes zijn.',\n        'string'  => ':attribute moet tussen de :min en :max tekens zijn.',\n        'array'   => ':attribute moet tussen de :min en :max items bevatten.',\n    ],\n    'boolean'              => ':attribute moet ja of nee zijn.',\n    'confirmed'            => ':attribute bevestiging komt niet overeen.',\n    'date'                 => ':attribute is geen geldige datum.',\n    'date_format'          => ':attribute komt niet overeen met het formaat :format.',\n    'different'            => ':attribute en :other moeten verschillend zijn.',\n    'digits'               => ':attribute moet bestaan uit :digits cijfers.',\n    'digits_between'       => ':attribute moet tussen de :min en :max cijfers zijn.',\n    'email'                => ':attribute is geen geldig e-mailadres.',\n    'ends_with' => ':attribute moet eindigen met een van de volgende: :values',\n    'file'                 => 'Het :attribute moet als een geldig bestand opgegeven worden.',\n    'filled'               => ':attribute is verplicht.',\n    'gt'                   => [\n        'numeric' => ':attribute moet groter zijn dan :value.',\n        'file'    => ':attribute moet groter zijn dan :value kilobytes.',\n        'string'  => ':attribute moet meer dan :value tekens bevatten.',\n        'array'   => ':attribute moet meer dan :value items bevatten.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute moet groter of gelijk zijn aan :value.',\n        'file'    => ':attribute moet groter of gelijk zijn aan :value kilobytes.',\n        'string'  => ':attribute moet :value of meer tekens bevatten.',\n        'array'   => ':attribute moet :value items of meer bevatten.',\n    ],\n    'exists'               => ':attribute is ongeldig.',\n    'image'                => ':attribute moet een afbeelding zijn.',\n    'image_extension'      => ':attribute moet een geldige en ondersteunde afbeeldings-extensie hebben.',\n    'in'                   => ':attribute is ongeldig.',\n    'integer'              => ':attribute moet een getal zijn.',\n    'ip'                   => ':attribute moet een geldig IP-adres zijn.',\n    'ipv4'                 => ':attribute moet een geldig IPv4-adres zijn.',\n    'ipv6'                 => ':attribute moet een geldig IPv6-adres zijn.',\n    'json'                 => ':attribute moet een geldige JSON-string zijn.',\n    'lt'                   => [\n        'numeric' => ':attribute moet kleiner zijn dan :value.',\n        'file'    => ':attribute moet kleiner zijn dan :value kilobytes.',\n        'string'  => ':attribute moet minder dan :value tekens bevatten.',\n        'array'   => ':attribute moet minder dan :value items bevatten.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute moet kleiner of gelijk zijn aan :value.',\n        'file'    => ':attribute moet kleiner of gelijk zijn aan :value kilobytes.',\n        'string'  => ':attribute moet :value tekens of minder bevatten.',\n        'array'   => ':attribute mag niet meer dan :value items bevatten.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute mag niet groter zijn dan :max.',\n        'file'    => ':attribute mag niet groter zijn dan :max kilobytes.',\n        'string'  => ':attribute mag niet groter zijn dan :max tekens.',\n        'array'   => ':attribute mag niet meer dan :max items bevatten.',\n    ],\n    'mimes'                => ':attribute moet een bestand zijn van het type: :values.',\n    'min'                  => [\n        'numeric' => ':attribute moet minstens :min zijn.',\n        'file'    => ':attribute moet minstens :min kilobytes zijn.',\n        'string'  => ':attribute moet minstens :min karakters bevatten.',\n        'array'   => ':attribute moet minstens :min items bevatten.',\n    ],\n    'not_in'               => ':attribute is ongeldig.',\n    'not_regex'            => ':attribute formaat is ongeldig.',\n    'numeric'              => ':attribute moet een getal zijn.',\n    'regex'                => ':attribute formaat is ongeldig.',\n    'required'             => ':attribute veld is verplicht.',\n    'required_if'          => ':attribute veld is verplicht als :other gelijk is aan :value.',\n    'required_with'        => ':attribute veld is verplicht wanneer :values ingesteld is.',\n    'required_with_all'    => ':attribute veld is verplicht wanneer :values ingesteld is.',\n    'required_without'     => ':attribute veld is verplicht wanneer :values niet ingesteld is.',\n    'required_without_all' => ':attribute veld is verplicht wanneer geen van :values ingesteld zijn.',\n    'same'                 => ':attribute en :other moeten overeenkomen.',\n    'safe_url'             => 'De opgegeven link is mogelijk niet veilig.',\n    'size'                 => [\n        'numeric' => ':attribute moet :size zijn.',\n        'file'    => ':attribute moet :size kilobytes zijn.',\n        'string'  => ':attribute moet :size tekens bevatten.',\n        'array'   => ':attribute moet :size items bevatten.',\n    ],\n    'string'               => ':attribute moet tekst zijn.',\n    'timezone'             => ':attribute moet een geldige zone zijn.',\n    'totp'                 => 'De opgegeven code is niet geldig of verlopen.',\n    'unique'               => ':attribute is al in gebruik.',\n    'url'                  => ':attribute formaat is ongeldig.',\n    'uploaded'             => 'Het bestand kon niet worden geüpload. De server accepteert mogelijk geen bestanden van deze grootte.',\n\n    'zip_file' => 'Het \\':attribute\\' veld moet verwijzen naar een bestand in de ZIP.',\n    'zip_file_size' => 'Het bestand :attribute mag niet groter zijn dan :size MB.',\n    'zip_file_mime' => 'Het \\':attribute\\' veld moet verwijzen naar een bestand met het type :validTypes, vond :foundType.',\n    'zip_model_expected' => 'Dataobject verwacht maar vond \":type\".',\n    'zip_unique' => ':attribute moet uniek zijn voor het objecttype binnen de ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Wachtwoord bevestiging verplicht',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/nn/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'oppretta side',\n    'page_create_notification'    => 'Sida vart oppretta',\n    'page_update'                 => 'oppdaterte side',\n    'page_update_notification'    => 'Sida vart oppretta',\n    'page_delete'                 => 'sletta side',\n    'page_delete_notification'    => 'Sida vart sletta',\n    'page_restore'                => 'gjenoppretta side',\n    'page_restore_notification'   => 'Sida vart gjenoppretta',\n    'page_move'                   => 'flytta side',\n    'page_move_notification'      => 'Sida vart flytta',\n\n    // Chapters\n    'chapter_create'              => 'oppretta kapittel',\n    'chapter_create_notification' => 'Kapittelet vart oppretta',\n    'chapter_update'              => 'oppdaterte kapittel',\n    'chapter_update_notification' => 'Kapittelet vart oppdatert',\n    'chapter_delete'              => 'sletta kapittel',\n    'chapter_delete_notification' => 'Kapittelet vart sletta',\n    'chapter_move'                => 'flytta kapittel',\n    'chapter_move_notification' => 'Kapitelet vart flytta',\n\n    // Books\n    'book_create'                 => 'oppretta bok',\n    'book_create_notification'    => 'Boka vart oppretta',\n    'book_create_from_chapter'              => 'konverterte kapittelet til bok',\n    'book_create_from_chapter_notification' => 'Kapittelet vart konvertert til ei bok',\n    'book_update'                 => 'oppdaterte bok',\n    'book_update_notification'    => 'Boka vart oppdatert',\n    'book_delete'                 => 'sletta bok',\n    'book_delete_notification'    => 'Boka vart sletta',\n    'book_sort'                   => 'sorterte bok',\n    'book_sort_notification'      => 'Sorteringa vart endra',\n\n    // Bookshelves\n    'bookshelf_create'            => 'oppretta hylle',\n    'bookshelf_create_notification'    => 'Hylla vart oppretta',\n    'bookshelf_create_from_book'    => 'endra frå bok til hylle',\n    'bookshelf_create_from_book_notification'    => 'Boka vart konvertert til ei bokhylle',\n    'bookshelf_update'                 => 'oppdaterte hylle',\n    'bookshelf_update_notification'    => 'Hylla vart oppdatert',\n    'bookshelf_delete'                 => 'sletta hylle',\n    'bookshelf_delete_notification'    => 'Hylla vart sletta',\n\n    // Revisions\n    'revision_restore' => 'gjenoppretta revisjon',\n    'revision_delete' => 'sletta revisjon',\n    'revision_delete_notification' => 'Revisjon sletta',\n\n    // Favourites\n    'favourite_add_notification' => '«:name» vart lagt til i dine favorittar',\n    'favourite_remove_notification' => '«:name» vart fjerna frå dine favorittar',\n\n    // Watching\n    'watch_update_level_notification' => 'Overvakingsinnstillingane vart oppdatert',\n\n    // Auth\n    'auth_login' => 'logga inn',\n    'auth_register' => 'registrert som ny brukar',\n    'auth_password_reset_request' => 'ba om tilbakestilling av passord',\n    'auth_password_reset_update' => 'tilbakestill brukarpassord',\n    'mfa_setup_method' => 'konfigurert MFA-metode',\n    'mfa_setup_method_notification' => 'Fleirfaktor-metoden vart konfigurert',\n    'mfa_remove_method' => 'fjerna MFA-metode',\n    'mfa_remove_method_notification' => 'Fleirfaktor-metoden vart fjerna',\n\n    // Settings\n    'settings_update' => 'oppdaterte innstillingar',\n    'settings_update_notification' => 'Innstillingane er oppdatert',\n    'maintenance_action_run' => 'kjørte vedlikehaldshandling',\n\n    // Webhooks\n    'webhook_create' => 'oppretta webhook',\n    'webhook_create_notification' => 'Webhook vart oppretta',\n    'webhook_update' => 'oppdatert webhook',\n    'webhook_update_notification' => 'Webhook vart oppdatert',\n    'webhook_delete' => 'sletta webhook',\n    'webhook_delete_notification' => 'Webhook vart sletta',\n\n    // Imports\n    'import_create' => 'created import',\n    'import_create_notification' => 'Import successfully uploaded',\n    'import_run' => 'updated import',\n    'import_run_notification' => 'Content successfully imported',\n    'import_delete' => 'deleted import',\n    'import_delete_notification' => 'Import successfully deleted',\n\n    // Users\n    'user_create' => 'oppretta brukar',\n    'user_create_notification' => 'Brukar vart oppretta',\n    'user_update' => 'oppdatert brukar',\n    'user_update_notification' => 'Brukaren vart oppdatert',\n    'user_delete' => 'sletta brukar',\n    'user_delete_notification' => 'Brukaren vart fjerna',\n\n    // API Tokens\n    'api_token_create' => 'opprett API-nøkkel',\n    'api_token_create_notification' => 'API-token er oppretta',\n    'api_token_update' => 'oppdatert api token',\n    'api_token_update_notification' => 'API-token oppdatert',\n    'api_token_delete' => 'sletta api token',\n    'api_token_delete_notification' => 'API-token vart sletta',\n\n    // Roles\n    'role_create' => 'oppretta rolle',\n    'role_create_notification' => 'Rolla vart oppretta',\n    'role_update' => 'oppdatert rolle',\n    'role_update_notification' => 'Rolla vart oppdatert',\n    'role_delete' => 'sletta rolla',\n    'role_delete_notification' => 'Rolla vart fjerna',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'tømt søppelbøtta',\n    'recycle_bin_restore' => 'gjenoppretta frå søppelbøtta',\n    'recycle_bin_destroy' => 'fjerna frå søppelbøtta',\n\n    // Comments\n    'commented_on'                => 'kommenterte på',\n    'comment_create'              => 'lagt til kommentar',\n    'comment_update'              => 'oppdatert kommentar',\n    'comment_delete'              => 'sletta kommentar',\n\n    // Sort Rules\n    'sort_rule_create' => 'created sort rule',\n    'sort_rule_create_notification' => 'Sort rule successfully created',\n    'sort_rule_update' => 'updated sort rule',\n    'sort_rule_update_notification' => 'Sort rule successfully updated',\n    'sort_rule_delete' => 'deleted sort rule',\n    'sort_rule_delete_notification' => 'Sort rule successfully deleted',\n\n    // Other\n    'permissions_update'          => 'oppdaterte tilgangar',\n];\n"
  },
  {
    "path": "lang/nn/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Desse detaljane samsvarar ikkje med det me har på bok.',\n    'throttle' => 'For mange forsøk, prøv på nytt om :seconds sekunder.',\n\n    // Login & Register\n    'sign_up' => 'Registrer deg',\n    'log_in' => 'Logg inn',\n    'log_in_with' => 'Logg inn med :socialDriver',\n    'sign_up_with' => 'Registrer med :socialDriver',\n    'logout' => 'Logg ut',\n\n    'name' => 'Namn',\n    'username' => 'Brukarnamn',\n    'email' => 'E-post',\n    'password' => 'Passord',\n    'password_confirm' => 'Stadfest passord',\n    'password_hint' => 'Må vere minst 8 teikn',\n    'forgot_password' => 'Gløymt passord?',\n    'remember_me' => 'Hugs meg',\n    'ldap_email_hint' => 'Oppgi ein e-post for denne kontoen.',\n    'create_account' => 'Opprett konto',\n    'already_have_account' => 'Har du allereie ein konto?',\n    'dont_have_account' => 'Manglar du ein konto?',\n    'social_login' => 'Sosiale kontoar',\n    'social_registration' => 'Registrer via sosiale kontoar',\n    'social_registration_text' => 'Bruk ei anna teneste for å registrere deg.',\n\n    'register_thanks' => 'Takk for at du registrerte deg!',\n    'register_confirm' => 'Sjekk e-posten din for informasjon som gir deg tilgang til :appName.',\n    'registrations_disabled' => 'Registrering er deaktivert.',\n    'registration_email_domain_invalid' => 'Du kan ikkje bruke det domenet for å registrere ein konto',\n    'register_success' => 'Takk for registreringa! Du kan no logge inn på tenesta.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Prøver innlogging',\n    'auto_init_starting_desc' => 'Me kontaktar autentiseringssystemet ditt for å starte innloggingsprosessen. Dersom det ikkje er noko framdrift i løpet av fem sekunder kan du trykke på lenka under.',\n    'auto_init_start_link' => 'Fortsett med autentisering',\n\n    // Password Reset\n    'reset_password' => 'Nullstill passord',\n    'reset_password_send_instructions' => 'Oppgi e-posten kobla til kontoen din, så sender me ein e-post der du kan nullstille passordet.',\n    'reset_password_send_button' => 'Send nullstillingslenke',\n    'reset_password_sent' => 'Ei nullstillingslenke vart sendt til :email om den eksisterer i systemet.',\n    'reset_password_success' => 'Passordet vart nullstilt.',\n    'email_reset_subject' => 'Nullstill ditt :appName passord',\n    'email_reset_text' => 'Du mottar denne e-posten fordi det er blitt bedt om ei nullstilling av passord for denne kontoen.',\n    'email_reset_not_requested' => 'Om det ikkje var deg, så treng du ikkje gjere noko.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Stadfest e-postadressa for :appName',\n    'email_confirm_greeting' => 'Takk for at du registrerte deg på :appName!',\n    'email_confirm_text' => 'Stadfest e-posten din ved å trykke på knappen under:',\n    'email_confirm_action' => 'Stadfest e-post',\n    'email_confirm_send_error' => 'Stadfesting er krevd av systemet, men systemet kan ikkje sende desse. Kontakt admin for å løyse problemet.',\n    'email_confirm_success' => 'E-postadressa di er verifisert! Du kan no logge inn ved å bruke denne ved innlogging.',\n    'email_confirm_resent' => 'Stadfesting sendt på e-post, sjekk innboksen din.',\n    'email_confirm_thanks' => 'Takk for verifiseringa!',\n    'email_confirm_thanks_desc' => 'Vent litt medan me verifiserer. Om du ikkje vert sendt vidare i løpet av tre sekunder, kan du klikke på \"Fortsett\" under.',\n\n    'email_not_confirmed' => 'E-posten er ikkje stadfesta',\n    'email_not_confirmed_text' => 'E-postadressa er ennå ikkje stadfesta.',\n    'email_not_confirmed_click_link' => 'Trykk på lenka i e-posten du fekk då du registrerte deg.',\n    'email_not_confirmed_resend' => 'Finner du den ikkje i innboks eller useriøs e-post? Trykk på knappen under for å få ny.',\n    'email_not_confirmed_resend_button' => 'Send stadfesting på e-post på nytt',\n\n    // User Invite\n    'user_invite_email_subject' => 'Du har blitt invitert til :appName!',\n    'user_invite_email_greeting' => 'Ein konto har blitt oppretta for deg på :appName.',\n    'user_invite_email_text' => 'Trykk på knappen under for å opprette eit sikkert passord:',\n    'user_invite_email_action' => 'Skriv inn passord',\n    'user_invite_page_welcome' => 'Velkommen til :appName!',\n    'user_invite_page_text' => 'For å fullføre prosessen må du oppgi eit passord som sikrar din konto på :appName for neste besøk.',\n    'user_invite_page_confirm_button' => 'Stadfest passord',\n    'user_invite_success_login' => 'Passordet vart lagra, du skal nå kunne logge inn med ditt nye passord for å få tilgang til :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Konfigurer fleirfaktor-autentisering',\n    'mfa_setup_desc' => 'Konfigurer fleirfaktor-autentisering som eit ekstra lag med tryggleik for brukarkontoen din.',\n    'mfa_setup_configured' => 'Allereie konfigurert',\n    'mfa_setup_reconfigure' => 'Konfigurer på nytt',\n    'mfa_setup_remove_confirmation' => 'Er du sikker på at du vil deaktivere denne fleirfaktor-autentiseringsmetoden?',\n    'mfa_setup_action' => 'Konfigurasjon',\n    'mfa_backup_codes_usage_limit_warning' => 'Du har mindre enn 5 tryggleikskodar igjen; generer gjerne nye og lagre eit nytt sett før du går tom for kodar. Då slepper du å bli låst ute frå kontoen din.',\n    'mfa_option_totp_title' => 'Mobilapplikasjon',\n    'mfa_option_totp_desc' => 'For å bruka fleirfaktorautentisering treng du ein mobilapplikasjon som støttar TOTP-teknologien, slik som Google Authenticator, Authy eller Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Tryggleikskodar',\n    'mfa_option_backup_codes_desc' => 'Genererer et sett med engangs sikkerhetskoder som du skal logge inn for å bekrefte identiteten din. Sørg for å lagre disse på et sikkert og sikkert sted.',\n    'mfa_gen_confirm_and_enable' => 'Stadfest og aktiver',\n    'mfa_gen_backup_codes_title' => 'Konfigurasjon av tryggleikskodar',\n    'mfa_gen_backup_codes_desc' => 'Lagre lista under med kodar på ein trygg stad. Når du skal ha tilgang til systemet kan du bruka ein av desse som ein faktor under innlogging.',\n    'mfa_gen_backup_codes_download' => 'Last ned kodar',\n    'mfa_gen_backup_codes_usage_warning' => 'Kvar kode kan berre brukast ein gong',\n    'mfa_gen_totp_title' => 'Oppsett for mobilapplikasjon',\n    'mfa_gen_totp_desc' => 'For å bruka fleirfaktorautentisering treng du ein mobilapplikasjon som støttar TOTP-teknologien, slik som Google Authenticator, Authy eller Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Scan QR-koden nedanfor med vald TOTP-applikasjon for å starta.',\n    'mfa_gen_totp_verify_setup' => 'Stadfest oppsett',\n    'mfa_gen_totp_verify_setup_desc' => 'Stadfest at oppsettet fungerer ved å skrive inn koden fra TOTP-applikasjonen i boksen nedanfor:',\n    'mfa_gen_totp_provide_code_here' => 'Skriv inn den genererte koden her',\n    'mfa_verify_access' => 'Stadfest tilgang',\n    'mfa_verify_access_desc' => 'Brukarkontoen din krev at du stadfestar identiteten din med ein ekstra autentiseringsfaktor før du får tilgang. Stadfest identiteten med ein av dine konfigurerte metodar for å halda fram.',\n    'mfa_verify_no_methods' => 'Ingen metodar er konfigurert',\n    'mfa_verify_no_methods_desc' => 'Ingen fleirfaktorautentiseringsmetoder er satt opp for din konto. Du må setje opp minst ein metode for å få tilgang.',\n    'mfa_verify_use_totp' => 'Stadfest med mobilapplikasjon',\n    'mfa_verify_use_backup_codes' => 'Stadfest med tryggleikskode',\n    'mfa_verify_backup_code' => 'Tryggleikskode',\n    'mfa_verify_backup_code_desc' => 'Skriv inn ein av dei ubrukte tryggleikskodane dine under:',\n    'mfa_verify_backup_code_enter_here' => 'Skriv inn tryggleikskode her',\n    'mfa_verify_totp_desc' => 'Skriv inn koden, generert ved hjelp av mobilapplikasjonen, nedanfor:',\n    'mfa_setup_login_notification' => 'Fleirfaktorautentisering er konfigurert, vennlegast logg inn på nytt med denne metoden.',\n];\n"
  },
  {
    "path": "lang/nn/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Avbryt',\n    'close' => 'Lukk',\n    'confirm' => 'Stadfest',\n    'back' => 'Tilbake',\n    'save' => 'Lagre',\n    'continue' => 'Fortsett',\n    'select' => 'Vel',\n    'toggle_all' => 'Byt alle',\n    'more' => 'Meir',\n\n    // Form Labels\n    'name' => 'Namn',\n    'description' => 'Skildring',\n    'role' => 'Rolle',\n    'cover_image' => 'Framside',\n    'cover_image_description' => 'This image should be approximately 440x250px although it will be flexibly scaled & cropped to fit the user interface in different scenarios as required, so actual dimensions for display will differ.',\n\n    // Actions\n    'actions' => 'Handlingar',\n    'view' => 'Vis',\n    'view_all' => 'Vis alle',\n    'new' => 'Ny',\n    'create' => 'Opprett',\n    'update' => 'Oppdater',\n    'edit' => 'Rediger',\n    'archive' => 'Archive',\n    'unarchive' => 'Un-Archive',\n    'sort' => 'Sortér',\n    'move' => 'Flytt',\n    'copy' => 'Kopier',\n    'reply' => 'Svar',\n    'delete' => 'Slett',\n    'delete_confirm' => 'Stadfest sletting',\n    'search' => 'Søk',\n    'search_clear' => 'Nullstill søk',\n    'reset' => 'Nullstill',\n    'remove' => 'Fjern',\n    'add' => 'Legg til',\n    'configure' => 'Konfigurer',\n    'manage' => 'Administrer',\n    'fullscreen' => 'Fullskjerm',\n    'favourite' => 'Merk som favoritt',\n    'unfavourite' => 'Fjern som favoritt',\n    'next' => 'Neste',\n    'previous' => 'Førre',\n    'filter_active' => 'Aktivt filter:',\n    'filter_clear' => 'Tøm filter',\n    'download' => 'Last ned',\n    'open_in_tab' => 'Opne i fane',\n    'open' => 'Opne',\n\n    // Sort Options\n    'sort_options' => 'Sorteringsalternativ',\n    'sort_direction_toggle' => 'Sorteringsretning',\n    'sort_ascending' => 'Stigande sortering',\n    'sort_descending' => 'Synkande sortering',\n    'sort_name' => 'Namn',\n    'sort_default' => 'Standard',\n    'sort_created_at' => 'Dato oppretta',\n    'sort_updated_at' => 'Dato oppdatert',\n\n    // Misc\n    'deleted_user' => 'Slett brukar',\n    'no_activity' => 'Ingen aktivitet å vise',\n    'no_items' => 'Ingenting å vise',\n    'back_to_top' => 'Tilbake til toppen',\n    'skip_to_main_content' => 'Gå til hovudinnhald',\n    'toggle_details' => 'Vis/skjul detaljar',\n    'toggle_thumbnails' => 'Vis/skjul miniatyrbilete',\n    'details' => 'Detaljar',\n    'grid_view' => 'Rutenettvising',\n    'list_view' => 'Listevising',\n    'default' => 'Standard',\n    'breadcrumb' => 'Brødsmular',\n    'status' => 'Status',\n    'status_active' => 'Aktiv',\n    'status_inactive' => 'Inaktiv',\n    'never' => 'Aldri',\n    'none' => 'Ingen',\n\n    // Header\n    'homepage' => 'Heimeside',\n    'header_menu_expand' => 'Utvid toppmeny',\n    'profile_menu' => 'Profilmeny',\n    'view_profile' => 'Vis profil',\n    'edit_profile' => 'Endre profil',\n    'dark_mode' => 'Kveldsmodus',\n    'light_mode' => 'Dagmodus',\n    'global_search' => 'Globalt søk',\n\n    // Layout tabs\n    'tab_info' => 'Informasjon',\n    'tab_info_label' => 'Fane: Vis tilleggsinfo',\n    'tab_content' => 'Innhald',\n    'tab_content_label' => 'Fane: Vis hovudinnhald',\n\n    // Email Content\n    'email_action_help' => 'Om du har problem med å trykkja på \":actionText\"-knappen, bruk nettadressa under for å gå direkte dit:',\n    'email_rights' => 'Kopibeskytta',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Personvernreglar',\n    'terms_of_service' => 'Bruksvilkår',\n\n    // OpenSearch\n    'opensearch_description' => 'Search :appName',\n];\n"
  },
  {
    "path": "lang/nn/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Vel bilete',\n    'image_list' => 'Bileteliste',\n    'image_details' => 'Biletedetaljar',\n    'image_upload' => 'Last opp bilete',\n    'image_intro' => 'Her kan du velja og behandla bilete som tidlegare har vorte lasta opp til systemet.',\n    'image_intro_upload' => 'Last opp eit nytt bilete ved å dra eit bilete i dette vindauget, eller ved å bruka knappen \"Last opp bilete\" ovanfor.',\n    'image_all' => 'Alle',\n    'image_all_title' => 'Vis alle bilete',\n    'image_book_title' => 'Vis bilete som er lasta opp i denne boka',\n    'image_page_title' => 'Vis bilete lastet opp til denne sida',\n    'image_search_hint' => 'Søk på bilete etter namn',\n    'image_uploaded' => 'Lasta opp :uploadedDate',\n    'image_uploaded_by' => 'Lasta opp av :userName',\n    'image_uploaded_to' => 'Lasta opp til :pageLink',\n    'image_updated' => 'Oppdatert :updateDate',\n    'image_load_more' => 'Last inn fleire',\n    'image_image_name' => 'Biletenavn',\n    'image_delete_used' => 'Dette biletet er brukt på sidene nedanfor.',\n    'image_delete_confirm_text' => 'Vil du slette dette biletet?',\n    'image_select_image' => 'Velg bilete',\n    'image_dropzone' => 'Dra og slepp eller trykk her for å laste opp bilete',\n    'image_dropzone_drop' => 'Slepp bilete her for å laste opp',\n    'images_deleted' => 'Bilete sletta',\n    'image_preview' => 'Snøggvising av bilete',\n    'image_upload_success' => 'Bilete vart lasta opp',\n    'image_update_success' => 'Biletedetaljar vart oppdatert',\n    'image_delete_success' => 'Bilete vart sletta',\n    'image_replace' => 'Erstatt bilete',\n    'image_replace_success' => 'Biletefil vart oppdatert',\n    'image_rebuild_thumbs' => 'Regenerer ulike storleikar',\n    'image_rebuild_thumbs_success' => 'Bilete i ulike storleikar vart bygd på nytt!',\n\n    // Code Editor\n    'code_editor' => 'Endre kode',\n    'code_language' => 'Kodespråk',\n    'code_content' => 'Kodeinnhald',\n    'code_session_history' => 'Sesjonshistorikk',\n    'code_save' => 'Lagre kode',\n];\n"
  },
  {
    "path": "lang/nn/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Generelt',\n    'advanced' => 'Avansert',\n    'none' => 'Ingen',\n    'cancel' => 'Avbryt',\n    'save' => 'Lagre',\n    'close' => 'Lukk',\n    'apply' => 'Apply',\n    'undo' => 'Angre',\n    'redo' => 'Gjer om',\n    'left' => 'Venstre',\n    'center' => 'Sentrert',\n    'right' => 'Høgre',\n    'top' => 'Topp',\n    'middle' => 'Sentrert',\n    'bottom' => 'Botn',\n    'width' => 'Breidde',\n    'height' => 'Høgde',\n    'More' => 'Meir',\n    'select' => 'Velg …',\n\n    // Toolbar\n    'formats' => 'Formater',\n    'header_large' => 'Stor overskrift',\n    'header_medium' => 'Medium overskrift',\n    'header_small' => 'Lita overskrift',\n    'header_tiny' => 'Bittelita overskrift',\n    'paragraph' => 'Avsnitt',\n    'blockquote' => 'Blokksitat',\n    'inline_code' => 'Kodesetning',\n    'callouts' => 'Notabene',\n    'callout_information' => 'Informasjon',\n    'callout_success' => 'Positiv',\n    'callout_warning' => 'Advarsel',\n    'callout_danger' => 'Negativ',\n    'bold' => 'Feit',\n    'italic' => 'Kursiv',\n    'underline' => 'Understrek',\n    'strikethrough' => 'Strek over',\n    'superscript' => 'Heva skrift',\n    'subscript' => 'Senka skrift',\n    'text_color' => 'Tekstfarge',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Eigenvalgt farge',\n    'remove_color' => 'Fjern farge',\n    'background_color' => 'Bakgrunnsfarge',\n    'align_left' => 'Venstrejustering',\n    'align_center' => 'Midtstilling',\n    'align_right' => 'Høgrejustering',\n    'align_justify' => 'Blokkjustering',\n    'list_bullet' => 'Punktliste',\n    'list_numbered' => 'Nummerert liste',\n    'list_task' => 'Oppgaveliste',\n    'indent_increase' => 'Auk innrykk',\n    'indent_decrease' => 'Redusér innrykk',\n    'table' => 'Tabell',\n    'insert_image' => 'Sett inn bilde',\n    'insert_image_title' => 'Sett inn/redigér bilde',\n    'insert_link' => 'Sett inn/redigér lenke',\n    'insert_link_title' => 'Sett inn/redigér lenke',\n    'insert_horizontal_line' => 'Sett inn horisontal linje',\n    'insert_code_block' => 'Sett inn kodeblokk',\n    'edit_code_block' => 'Redigér kodeblokk',\n    'insert_drawing' => 'Sett inn/redigér tegning',\n    'drawing_manager' => 'Tegningsbehandling',\n    'insert_media' => 'Sett inn/redigér media',\n    'insert_media_title' => 'Sett inn/redigér media',\n    'clear_formatting' => 'Rens formattering',\n    'source_code' => 'Kildekode',\n    'source_code_title' => 'Kildekode',\n    'fullscreen' => 'Fullskjerm',\n    'image_options' => 'Bildealternativer',\n\n    // Tables\n    'table_properties' => 'Tabellegenskaper',\n    'table_properties_title' => 'Tabellegenskaper',\n    'delete_table' => 'Slett tabell',\n    'table_clear_formatting' => 'Clear table formatting',\n    'resize_to_contents' => 'Resize to contents',\n    'row_header' => 'Row header',\n    'insert_row_before' => 'Sett inn rad før',\n    'insert_row_after' => 'Sett inn rad etter',\n    'delete_row' => 'Slett rad',\n    'insert_column_before' => 'Sett inn kolonne før',\n    'insert_column_after' => 'Sett inn kolonne etter',\n    'delete_column' => 'Slett kolonne',\n    'table_cell' => 'Celle',\n    'table_row' => 'Rad',\n    'table_column' => 'Kolonne',\n    'cell_properties' => 'Celle-egenskaper',\n    'cell_properties_title' => 'Celle-egenskaper',\n    'cell_type' => 'Celletype',\n    'cell_type_cell' => 'Celle',\n    'cell_scope' => 'Omfang',\n    'cell_type_header' => 'Topptekst-celle',\n    'merge_cells' => 'Slå sammen celler',\n    'split_cell' => 'Del celle',\n    'table_row_group' => 'Radgruppe',\n    'table_column_group' => 'Kolonnegruppe',\n    'horizontal_align' => 'Horisontal justering',\n    'vertical_align' => 'Vertikal justering',\n    'border_width' => 'Kantbredde',\n    'border_style' => 'Kantstil',\n    'border_color' => 'Kantfarge',\n    'row_properties' => 'Radegenskaper',\n    'row_properties_title' => 'Radegenskaper',\n    'cut_row' => 'Klipp ut rad',\n    'copy_row' => 'Kopiér rad',\n    'paste_row_before' => 'Lim rad inn før',\n    'paste_row_after' => 'Lim rad inn etter',\n    'row_type' => 'Radtype',\n    'row_type_header' => 'Topptekst',\n    'row_type_body' => 'Hovedtekst',\n    'row_type_footer' => 'Bunntekst',\n    'alignment' => 'Justering',\n    'cut_column' => 'Klipp ut kolonne',\n    'copy_column' => 'Kopiér kolonne',\n    'paste_column_before' => 'Lim kolonne inn før',\n    'paste_column_after' => 'Lim kolonne inn etter',\n    'cell_padding' => 'Celleutfylling',\n    'cell_spacing' => 'Celleavstand',\n    'caption' => 'Overskrift',\n    'show_caption' => 'Vis overskrift',\n    'constrain' => 'Behold proporsjoner',\n    'cell_border_solid' => 'Heltrukket',\n    'cell_border_dotted' => 'Prikker',\n    'cell_border_dashed' => 'Stipler',\n    'cell_border_double' => 'Dobbel',\n    'cell_border_groove' => 'Rille',\n    'cell_border_ridge' => 'Kant',\n    'cell_border_inset' => 'Nedsenk',\n    'cell_border_outset' => 'Uthev',\n    'cell_border_none' => 'Ingen',\n    'cell_border_hidden' => 'Skjult bredde',\n\n    // Images, links, details/summary & embed\n    'source' => 'Kilde',\n    'alt_desc' => 'Alternativ beskrivelse',\n    'embed' => 'Bygg inn',\n    'paste_embed' => 'Lim inn koden din her:',\n    'url' => 'Nettlenke',\n    'text_to_display' => 'Synlig tekst',\n    'title' => 'Tittel',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Åpne lenke',\n    'open_link_in' => 'Åpne i ...',\n    'open_link_current' => 'Samme vindu',\n    'open_link_new' => 'Nytt vindu',\n    'remove_link' => 'Fjern lenke',\n    'insert_collapsible' => 'Sett inn sammenleggbar blokk',\n    'collapsible_unwrap' => 'Pakk ut',\n    'edit_label' => 'Rediger etikett',\n    'toggle_open_closed' => 'Veksle åpen/lukket',\n    'collapsible_edit' => 'Rediger sammenleggbar blokk',\n    'toggle_label' => 'Veksle etikettsynlighet',\n\n    // About view\n    'about' => 'Om tekstredigeringsprogrammet',\n    'about_title' => 'Om HDSEHDF-tekstredigeringsprogrammet',\n    'editor_license' => 'Tekstbehandlerlisens og opphavsrett',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'Denne tekstredigereren er laget med :tinyLink som er lisensiert under MIT.',\n    'editor_tiny_license_link' => 'Informasjon om opphavsrett og lisens for TinyMCE finnes her.',\n    'save_continue' => 'Lagre side og fortsett',\n    'callouts_cycle' => '(Fortsett å trykke for å veksle mellom typer)',\n    'link_selector' => 'Lenke til innhold',\n    'shortcuts' => 'Snarveier',\n    'shortcut' => 'Snarvei',\n    'shortcuts_intro' => 'Følgende snarveier er tilgjengelige i tekstredigeringsverktøyet:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(MacOS)',\n    'description' => 'Beskrivelse',\n];\n"
  },
  {
    "path": "lang/nn/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Nylig oppretta',\n    'recently_created_pages' => 'Nyleg oppretta sider',\n    'recently_updated_pages' => 'Nyleg oppdaterte sider',\n    'recently_created_chapters' => 'Nyleg oppretta kapitler',\n    'recently_created_books' => 'Nyleg oppretta bøker',\n    'recently_created_shelves' => 'Nyleg oppretta bokhyller',\n    'recently_update' => 'Nyleg oppdatert',\n    'recently_viewed' => 'Nyleg vist',\n    'recent_activity' => 'Nyleg aktivitet',\n    'create_now' => 'Opprett ein no',\n    'revisions' => 'Revisjonar',\n    'meta_revision' => 'Revisjon #:revisionCount',\n    'meta_created' => 'Oppretta :timeLength',\n    'meta_created_name' => 'Oppretta :timeLength av :user',\n    'meta_updated' => 'Oppdatert :timeLength',\n    'meta_updated_name' => 'Oppdatert :timeLength av :user',\n    'meta_owned_name' => 'Eigd av :user',\n    'meta_reference_count' => 'Sitert på :count side|Sitert på :count sider',\n    'entity_select' => 'Velg entitet',\n    'entity_select_lack_permission' => 'Du har ikkje tilgang til å velge dette elementet',\n    'images' => 'Bilete',\n    'my_recent_drafts' => 'Mine nylege utkast',\n    'my_recently_viewed' => 'Mine nylege visingar',\n    'my_most_viewed_favourites' => 'Mine mest sette favorittar',\n    'my_favourites' => 'Mine favorittar',\n    'no_pages_viewed' => 'Du har ikkje sett på nokre sider',\n    'no_pages_recently_created' => 'Ingen sider har nylig blitt oppretta',\n    'no_pages_recently_updated' => 'Ingen sider har nylig blitt oppdatert',\n    'export' => 'Eksporter',\n    'export_html' => 'Nettside med alt',\n    'export_pdf' => 'PDF-fil',\n    'export_text' => 'Tekstfil',\n    'export_md' => 'Markdownfil',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Default Page Template',\n    'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',\n    'default_template_select' => 'Select a template page',\n    'import' => 'Import',\n    'import_validate' => 'Validate Import',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'Select ZIP file to upload',\n    'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',\n    'import_pending' => 'Pending Imports',\n    'import_pending_none' => 'No imports have been started.',\n    'import_continue' => 'Continue Import',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Run Import',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Uploaded by',\n    'import_location' => 'Import Location',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'Import Errors',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Tilgongar',\n    'permissions_desc' => 'Endringar gjort her vil overstyra standardrettar gitt via brukarroller.',\n    'permissions_book_cascade' => 'Rettar sett på bøker vil automatisk arvast ned til sidenivå. Du kan overstyra arv ved å definera eigne rettar på kapittel eller sider.',\n    'permissions_chapter_cascade' => 'Rettar sett på kapittel vil automatisk arvast ned til sider. Du kan overstyra arv ved å definera rettar på enkeltsider.',\n    'permissions_save' => 'Lagre løyve',\n    'permissions_owner' => 'Eigar',\n    'permissions_role_everyone_else' => 'Alle andre',\n    'permissions_role_everyone_else_desc' => 'Angi rettar for alle roller som ikkje blir overstyrt (arva rettar).',\n    'permissions_role_override' => 'Overstyr rettar for rolle',\n    'permissions_inherit_defaults' => 'Arv standardrettar',\n\n    // Search\n    'search_results' => 'Søkeresultat',\n    'search_total_results_found' => ':count resultat funne|:count totalt',\n    'search_clear' => 'Nullstill søk',\n    'search_no_pages' => 'Ingen sider passar med søket',\n    'search_for_term' => 'Søk etter :term',\n    'search_more' => 'Fleire resultat',\n    'search_advanced' => 'Avansert søk',\n    'search_terms' => 'Søkeord',\n    'search_content_type' => 'Innhaldstype',\n    'search_exact_matches' => 'Eksakte ord',\n    'search_tags' => 'Søk på merke',\n    'search_options' => 'Alternativ',\n    'search_viewed_by_me' => 'Sett av meg',\n    'search_not_viewed_by_me' => 'Ikkje sett av meg',\n    'search_permissions_set' => 'Tilgongar er sett',\n    'search_created_by_me' => 'Oppretta av meg',\n    'search_updated_by_me' => 'Oppdatert av meg',\n    'search_owned_by_me' => 'Eigd av meg',\n    'search_date_options' => 'Datoalternativ',\n    'search_updated_before' => 'Oppdatert før',\n    'search_updated_after' => 'Oppdatert etter',\n    'search_created_before' => 'Oppretta før',\n    'search_created_after' => 'Oppretta etter',\n    'search_set_date' => 'Angi dato',\n    'search_update' => 'Oppdater søk',\n\n    // Shelves\n    'shelf' => 'Hylle',\n    'shelves' => 'Hyller',\n    'x_shelves' => ':count hylle|:count hyller',\n    'shelves_empty' => 'Ingen bokhyller er oppretta',\n    'shelves_create' => 'Opprett ny bokhylle',\n    'shelves_popular' => 'Populære bokhyller',\n    'shelves_new' => 'Nye bokhyller',\n    'shelves_new_action' => 'Ny bokhylle',\n    'shelves_popular_empty' => 'Dei mest populære bokhyllene blir vist her.',\n    'shelves_new_empty' => 'Nylig opprettede bokhyller vises her.',\n    'shelves_save' => 'Lagre hylle',\n    'shelves_books' => 'Bøker på denne hylla',\n    'shelves_add_books' => 'Legg til bøker på denne hyllen',\n    'shelves_drag_books' => 'Dra og slepp bøker nedanfor for å legge dei til i denne hylla',\n    'shelves_empty_contents' => 'Ingen bøker er stabla i denne hylla',\n    'shelves_edit_and_assign' => 'Endre hylla for å legge til bøker',\n    'shelves_edit_named' => 'Rediger :name (hylle)',\n    'shelves_edit' => 'Rediger hylle',\n    'shelves_delete' => 'Fjern hylle',\n    'shelves_delete_named' => 'Fjern :name (hylle)',\n    'shelves_delete_explain' => \"Dette vil fjerne hylla «:name». Bøkene på hylla vil ikkje bli sletta frå systemet.\",\n    'shelves_delete_confirmation' => 'Er du sikker på at du vil fjerne denne hylla?',\n    'shelves_permissions' => 'Hylletilgangar',\n    'shelves_permissions_updated' => 'Oppdaterte hylletilgangar',\n    'shelves_permissions_active' => 'Aktiverte hylletilgangar',\n    'shelves_permissions_cascade_warning' => 'Tilgangar på ei hylle vert ikkje automatisk arva av bøker på hylla. Dette er fordi ei bok kan finnast på fleire hyller samstundes. Tilgangar kan likevel verte kopiert til bøker på hylla ved å bruke alternativa under.',\n    'shelves_permissions_create' => 'Bokhylle-tilgangar vert brukt for kopiering av løyver til under-bøker ved hjelp av handlinga nedanfor. Dei kontrollerer ikkje rettane til å lage bøker.',\n    'shelves_copy_permissions_to_books' => 'Kopier tilgangar til bøkene på hylla',\n    'shelves_copy_permissions' => 'Kopier tilgangar',\n    'shelves_copy_permissions_explain' => 'Dette vil kopiere tilgangar på denne hylla til alle bøkene som er plassert på den. Før du starter kopieringen bør du sjekke at tilgangane på hylla er lagra.',\n    'shelves_copy_permission_success' => 'Løyver vart kopiert til :count bøker',\n\n    // Books\n    'book' => 'Bok',\n    'books' => 'Bøker',\n    'x_books' => ':count bok|:count bøker',\n    'books_empty' => 'Ingen bøker er skrevet',\n    'books_popular' => 'Populære bøker',\n    'books_recent' => 'Nylige bøker',\n    'books_new' => 'Nye bøker',\n    'books_new_action' => 'Ny bok',\n    'books_popular_empty' => 'De mest populære bøkene',\n    'books_new_empty' => 'Siste utgivelser vises her.',\n    'books_create' => 'Skriv ny bok',\n    'books_delete' => 'Brenn bok',\n    'books_delete_named' => 'Brenn boken :bookName',\n    'books_delete_explain' => 'Dette vil brenne boken «:bookName». Alle sider i boken vil fordufte for godt.',\n    'books_delete_confirmation' => 'Er du sikker på at du vil brenne boken?',\n    'books_edit' => 'Endre bok',\n    'books_edit_named' => 'Endre boken :bookName',\n    'books_form_book_name' => 'Boktittel',\n    'books_save' => 'Lagre bok',\n    'books_permissions' => 'Boktilganger',\n    'books_permissions_updated' => 'Boktilganger oppdatert',\n    'books_empty_contents' => 'Ingen sider eller kapittel finst i denne boka.',\n    'books_empty_create_page' => 'Skriv ei ny side',\n    'books_empty_sort_current_book' => 'Sorter innhaldet i boka',\n    'books_empty_add_chapter' => 'Start på nytt kapittel',\n    'books_permissions_active' => 'Boktilgangar er aktive',\n    'books_search_this' => 'Søk i boka',\n    'books_navigation' => 'Boknavigasjon',\n    'books_sort' => 'Sorter bokinnhald',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Omorganiser :bookName',\n    'books_sort_name' => 'Sorter på namn',\n    'books_sort_created' => 'Sorter på oppretta dato',\n    'books_sort_updated' => 'Sorter på oppdatert dato',\n    'books_sort_chapters_first' => 'Kapittel først',\n    'books_sort_chapters_last' => 'Kapittel sist',\n    'books_sort_show_other' => 'Vis andre bøker',\n    'books_sort_save' => 'Lagre sortering',\n    'books_sort_show_other_desc' => 'Legg til andre bøker her for å inkludere dei i omorganiseringa og gjer det enklare å flytte på tvers av dei.',\n    'books_sort_move_up' => 'Flytt opp',\n    'books_sort_move_down' => 'Flytt ned',\n    'books_sort_move_prev_book' => 'Flytt til førre bok',\n    'books_sort_move_next_book' => 'Flytt til neste bok',\n    'books_sort_move_prev_chapter' => 'Flytt inn i førre kapittel',\n    'books_sort_move_next_chapter' => 'Flytt inn i neste kapittel',\n    'books_sort_move_book_start' => 'Flytt til starten av boka',\n    'books_sort_move_book_end' => 'Flytt til slutten av boka',\n    'books_sort_move_before_chapter' => 'Flytt før kapittel',\n    'books_sort_move_after_chapter' => 'Flytt etter kapittel',\n    'books_copy' => 'Kopier bok',\n    'books_copy_success' => 'Boka vart kopiert',\n\n    // Chapters\n    'chapter' => 'Kapittel',\n    'chapters' => 'Kapittel',\n    'x_chapters' => ':count kapittel|:count kapittel',\n    'chapters_popular' => 'Populære kapittel',\n    'chapters_new' => 'Nytt kapittel',\n    'chapters_create' => 'Skriv nytt kapittel',\n    'chapters_delete' => 'Riv ut kapittel',\n    'chapters_delete_named' => 'Slett :chapterName',\n    'chapters_delete_explain' => 'Dette vil slette «:chapterName». Alle sider i kapittelet vil og verte sletta.',\n    'chapters_delete_confirm' => 'Er du sikker på at du vil slette dette kapittelet?',\n    'chapters_edit' => 'Rediger kapittel',\n    'chapters_edit_named' => 'Rediger :chapterName',\n    'chapters_save' => 'Lagre kapittel',\n    'chapters_move' => 'Flytt kapittel',\n    'chapters_move_named' => 'Flytt :chapterName',\n    'chapters_copy' => 'Kopier kapittel',\n    'chapters_copy_success' => 'Kapittelet vart kopiert',\n    'chapters_permissions' => 'Kapitteltilgongar',\n    'chapters_empty' => 'Det finnes ingen sider i dette kapittelet.',\n    'chapters_permissions_active' => 'Kapitteltilganger er aktivert',\n    'chapters_permissions_success' => 'Kapitteltilgager er oppdatert',\n    'chapters_search_this' => 'Søk i dette kapittelet',\n    'chapter_sort_book' => 'Omorganisér bok',\n\n    // Pages\n    'page' => 'Side',\n    'pages' => 'Sider',\n    'x_pages' => ':count side|:count sider',\n    'pages_popular' => 'Populære sider',\n    'pages_new' => 'Ny side',\n    'pages_attachments' => 'Vedlegg',\n    'pages_navigation' => 'Sidenavigasjon',\n    'pages_delete' => 'Slett side',\n    'pages_delete_named' => 'Slett :pageName (side)',\n    'pages_delete_draft_named' => 'Slett utkastet :pageName (side)',\n    'pages_delete_draft' => 'Slett utkastet',\n    'pages_delete_success' => 'Siden er slettet',\n    'pages_delete_draft_success' => 'Sideutkastet vart sletta',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'Er du sikker på at du vil slette siden?',\n    'pages_delete_draft_confirm' => 'Er du sikker på at du vil slette utkastet?',\n    'pages_editing_named' => 'Redigerer :pageName (side)',\n    'pages_edit_draft_options' => 'Utkastsalternativer',\n    'pages_edit_save_draft' => 'Lagre utkast',\n    'pages_edit_draft' => 'Redigér utkast',\n    'pages_editing_draft' => 'Redigerer utkast',\n    'pages_editing_page' => 'Redigerer side',\n    'pages_edit_draft_save_at' => 'Sist lagret ',\n    'pages_edit_delete_draft' => 'Slett utkast',\n    'pages_edit_delete_draft_confirm' => 'Er du sikker på at du vil slette utkastendringer i utkastet? Alle dine endringer, siden siste lagring vil gå tapt, og editoren vil bli oppdatert med den siste siden uten utkast til lagring.',\n    'pages_edit_discard_draft' => 'Tilbakestill endring',\n    'pages_edit_switch_to_markdown' => 'Bytt til Markdown tekstredigering',\n    'pages_edit_switch_to_markdown_clean' => '(Renset innhold)',\n    'pages_edit_switch_to_markdown_stable' => '(Urørt innhold)',\n    'pages_edit_switch_to_wysiwyg' => 'Bytt til WYSIWYG tekstredigering',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Angi endringslogg',\n    'pages_edit_enter_changelog_desc' => 'Gi ei kort skildring av endringane dine',\n    'pages_edit_enter_changelog' => 'Sjå endringslogg',\n    'pages_editor_switch_title' => 'Bytt tekstredigeringsprogram',\n    'pages_editor_switch_are_you_sure' => 'Er du sikker på at du vil bytte tekstredigeringsprogram for denne sida?',\n    'pages_editor_switch_consider_following' => 'Hugs dette når du byttar tekstredigeringsprogram:',\n    'pages_editor_switch_consideration_a' => 'Når du bytter, vil den nye tekstredigeraren bli valgt for alle framtidige redaktørar. Dette inkluderer alle redaktørar som ikkje kan endre type sjølv.',\n    'pages_editor_switch_consideration_b' => 'I visse tilfeller kan det føre til tap av detaljar og syntaks.',\n    'pages_editor_switch_consideration_c' => 'Etikett- eller redigeringslogg-endringar loggført sidan siste lagring vil ikkje føres vidare etter endringa.',\n    'pages_save' => 'Lagre side',\n    'pages_title' => 'Sidetittel',\n    'pages_name' => 'Sidenamn',\n    'pages_md_editor' => 'Tekstbehandlar',\n    'pages_md_preview' => 'Førehandsvising',\n    'pages_md_insert_image' => 'Sett inn bilete',\n    'pages_md_insert_link' => 'Sett inn lenke',\n    'pages_md_insert_drawing' => 'Sett inn tegning',\n    'pages_md_show_preview' => 'Førhandsvisning',\n    'pages_md_sync_scroll' => 'Synkroniser førehandsvisingsrulle',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Ulagra teikning funne',\n    'pages_drawing_unsaved_confirm' => 'Ulagra teikninga vart funne frå ei tidligare mislykka lagring. Vil du gjenopprette og fortsette å redigere denne ulagra teikninga?',\n    'pages_not_in_chapter' => 'Sida tilhøyrer ingen kapittel',\n    'pages_move' => 'Flytt sida',\n    'pages_copy' => 'Kopier side',\n    'pages_copy_desination' => 'Destinasjon',\n    'pages_copy_success' => 'Sida vart flytta',\n    'pages_permissions' => 'Sidetilgangar',\n    'pages_permissions_success' => 'Sidetilgangar vart endra',\n    'pages_revision' => 'Revisjon',\n    'pages_revisions' => 'Revisjonar for sida',\n    'pages_revisions_desc' => 'Nedanfor er alle tidlegare revisjonar av denne sida. Du kan sjå tilbake igjen, samanlikna og retta opp igjen tidlegare sideversjonar viss du tillet det. Den heile historikken til sida kan kanskje ikkje speglast fullstendig her. Avhengig av systemkonfigurasjonen, kan gamle revisjonar bli sletta automatisk.',\n    'pages_revisions_named' => 'Revisjonar for :pageName',\n    'pages_revision_named' => 'Revisjonar for :pageName',\n    'pages_revision_restored_from' => 'Gjenoppretta fra #:id; :summary',\n    'pages_revisions_created_by' => 'Skrive av',\n    'pages_revisions_date' => 'Revideringsdato',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Revisjonsnummer',\n    'pages_revisions_numbered' => 'Revisjon #:id',\n    'pages_revisions_numbered_changes' => 'Endringar på revisjon #:id',\n    'pages_revisions_editor' => 'Tekstredigeringstype',\n    'pages_revisions_changelog' => 'Endringslogg',\n    'pages_revisions_changes' => 'Endringar',\n    'pages_revisions_current' => 'Siste versjon',\n    'pages_revisions_preview' => 'Forhåndsvisning',\n    'pages_revisions_restore' => 'Gjenopprett',\n    'pages_revisions_none' => 'Denne siden har ingen revisjoner',\n    'pages_copy_link' => 'Kopier lenke',\n    'pages_edit_content_link' => 'Hopp til seksjonen i tekstbehandlaren',\n    'pages_pointer_enter_mode' => 'Gå til seksjonen velg modus',\n    'pages_pointer_label' => 'Sidens seksjon alternativer',\n    'pages_pointer_permalink' => 'Sideseksjons permalenke',\n    'pages_pointer_include_tag' => 'Sideseksjonen inkluderer Tag',\n    'pages_pointer_toggle_link' => 'Permalenke modus, trykk for å vise inkluderer tag',\n    'pages_pointer_toggle_include' => 'Inkluder tag-modus, trykk for å vise permalenke',\n    'pages_permissions_active' => 'Sidetilganger er aktive',\n    'pages_initial_revision' => 'Første publisering',\n    'pages_references_update_revision' => 'Automatisk oppdatering av interne lenker',\n    'pages_initial_name' => 'Ny side',\n    'pages_editing_draft_notification' => 'Du skriver på eit utkast som sist vart lagra :timeDiff.',\n    'pages_draft_edited_notification' => 'Siden har blitt endret siden du startet. Det anbefales at du forkaster dine endringer.',\n    'pages_draft_page_changed_since_creation' => 'Denne siden har blitt oppdatert etter at dette utkastet ble oppretta. Me trur det er lurt å forkaste dette utkastet, eller er ekstra forsiktig, slik at du ikkje overskriver andre sine sideendringar.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count forfattere har begynt å endre denne siden.',\n        'start_b' => ':userName skriver på siden for øyeblikket',\n        'time_a' => 'sidan sist sida vart oppdatert',\n        'time_b' => 'i løpet av de siste :minCount minuttene',\n        'message' => ':start :time. Prøv å ikke overskriv hverandres endringer!',\n    ],\n    'pages_draft_discarded' => 'Utkastet er forkastet! Redigeringsprogrammet er oppdatert med gjeldende sideinnhold',\n    'pages_draft_deleted' => 'Utkast sletta! Redigeringsprogrammet er oppdatert med gjeldande sideinnhald',\n    'pages_specific' => 'Bestemt side',\n    'pages_is_template' => 'Sidemal',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Vis/gøym sidepanelet',\n    'page_tags' => 'Sidemerker',\n    'chapter_tags' => 'Kapittelmerker',\n    'book_tags' => 'Bokmerker',\n    'shelf_tags' => 'Hyllemerker',\n    'tag' => 'Merke',\n    'tags' =>  'Merker',\n    'tags_index_desc' => 'Merker kan brukes på innhold i systemet for å anvende en kategorisering på en fleksibel måte. Etiketter kan ha både en nøkkel og verdi, med valgfri. Når det er brukt, kan innhold sjekkes ved hjelp av taggnavn og verdi.',\n    'tag_name' =>  'Merketittel',\n    'tag_value' => 'Merkeverdi (Valgfritt)',\n    'tags_explain' => \"Legg til merker for å kategorisere innholdet ditt. \\n Du kan legge til merkeverdier for å beskrive dem ytterligere.\",\n    'tags_add' => 'Legg til flere merker',\n    'tags_remove' => 'Fjern merke',\n    'tags_usages' => 'Totalt emneordbruk',\n    'tags_assigned_pages' => 'Tilordnet sider',\n    'tags_assigned_chapters' => 'Tildelt til kapitler',\n    'tags_assigned_books' => 'Tilordnet til bøker',\n    'tags_assigned_shelves' => 'Tilordnet hyller',\n    'tags_x_unique_values' => ':count unike verdier',\n    'tags_all_values' => 'Alle verdier',\n    'tags_view_tags' => 'Vis etiketter',\n    'tags_view_existing_tags' => 'Vis eksisterende etiketter',\n    'tags_list_empty_hint' => 'Etiketter kan tilordnes via sidepanelet, eller mens du redigerer detaljene for en hylle, bok eller kapittel.',\n    'attachments' => 'Vedlegg',\n    'attachments_explain' => 'Last opp vedlegg eller legg til lenker for å berike innholdet. Disse vil vises i sidestolpen på siden.',\n    'attachments_explain_instant_save' => 'Endringer her blir lagret med en gang.',\n    'attachments_upload' => 'Last opp vedlegg',\n    'attachments_link' => 'Fest lenke',\n    'attachments_upload_drop' => 'Alternativt kan du dra og slippe en fil her for å laste den opp som et vedlegg.',\n    'attachments_set_link' => 'Angi lenke',\n    'attachments_delete' => 'Er du sikker på at du vil fjerne vedlegget?',\n    'attachments_dropzone' => 'Slipp filer her for å laste opp',\n    'attachments_no_files' => 'Ingen vedlegg er lastet opp',\n    'attachments_explain_link' => 'Du kan feste lenker til denne. Det kan være henvisning til andre sider, bøker etc. eller lenker fra nettet.',\n    'attachments_link_name' => 'Lenkenavn',\n    'attachment_link' => 'Vedleggslenke',\n    'attachments_link_url' => 'Lenke til vedlegg',\n    'attachments_link_url_hint' => 'Adresse til lenke eller vedlegg',\n    'attach' => 'Fest',\n    'attachments_insert_link' => 'Fest vedleggslenke',\n    'attachments_edit_file' => 'Endre vedlegg',\n    'attachments_edit_file_name' => 'Vedleggsnavn',\n    'attachments_edit_drop_upload' => 'Dra og slipp eller trykk her for å oppdatere eller overskrive',\n    'attachments_order_updated' => 'Vedleggssortering endret',\n    'attachments_updated_success' => 'Vedleggsdetaljer endret',\n    'attachments_deleted' => 'Vedlegg fjernet',\n    'attachments_file_uploaded' => 'Vedlegg vart lasta opp',\n    'attachments_file_updated' => 'Vedlegget vart oppdatert',\n    'attachments_link_attached' => 'Lenka vart festa til sida',\n    'templates' => 'Maler',\n    'templates_set_as_template' => 'Siden er en mal',\n    'templates_explain_set_as_template' => 'Du kan angi denne siden som en mal slik at innholdet kan brukes når du oppretter andre sider. Andre brukere vil kunne bruke denne malen hvis de har visningstillatelser for denne siden.',\n    'templates_replace_content' => 'Bytt sideinnhold',\n    'templates_append_content' => 'Legg til neders på siden',\n    'templates_prepend_content' => 'Legg til øverst på siden',\n\n    // Profile View\n    'profile_user_for_x' => 'Medlem i :time',\n    'profile_created_content' => 'Har skrevet',\n    'profile_not_created_pages' => ':userName har ikke forfattet noen sider',\n    'profile_not_created_chapters' => ':userName har ikke opprettet noen kapitler',\n    'profile_not_created_books' => ':userName har ikke laget noen bøker',\n    'profile_not_created_shelves' => ':userName har ikke hengt opp noen hyller',\n\n    // Comments\n    'comment' => 'Kommentar',\n    'comments' => 'Kommentarer',\n    'comment_add' => 'Skriv kommentar',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Skriv en kommentar her',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Publiser kommentar',\n    'comment_new' => 'Ny kommentar',\n    'comment_created' => 'kommenterte :createDiff',\n    'comment_updated' => 'Oppdatert :updateDiff av :username',\n    'comment_updated_indicator' => 'Oppdatert',\n    'comment_deleted_success' => 'Kommentar fjernet',\n    'comment_created_success' => 'Kommentar skrevet',\n    'comment_updated_success' => 'Kommentar endret',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Er du sikker på at du vil fjerne kommentaren?',\n    'comment_in_reply_to' => 'Som svar til :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Her er kommentarene som er på denne siden. Kommentarer kan legges til og administreres når du ser på den lagrede siden.',\n\n    // Revision\n    'revision_delete_confirm' => 'Vil du slette revisjonen?',\n    'revision_restore_confirm' => 'Vil du gjenopprette revisjonen? Innholdet på siden vil bli overskrevet med denne revisjonen.',\n    'revision_cannot_delete_latest' => 'CKan ikke slette siste revisjon.',\n\n    // Copy view\n    'copy_consider' => 'Vennligst vurder nedenfor når du kopierer innholdet.',\n    'copy_consider_permissions' => 'Egendefinerte tilgangsinnstillinger vil ikke bli kopiert.',\n    'copy_consider_owner' => 'Du vil bli eier av alt kopiert innhold.',\n    'copy_consider_images' => 'Sidebildefiler vil ikkle bli duplisert og dei opprinnelege bileta beholder relasjonen til sida dei opprinnelig vart lasta opp til.',\n    'copy_consider_attachments' => 'Sidevedlegg vil ikke bli kopiert.',\n    'copy_consider_access' => 'Endring av sted, eier eller rettigheter kan føre til at innholdet er tilgjengelig for dem som tidligere har vært uten adgang.',\n\n    // Conversions\n    'convert_to_shelf' => 'Konverter til bokhylle',\n    'convert_to_shelf_contents_desc' => 'Du kan konvertere denne boken til en ny hylle med samme innhold. Kapitteler i denne boken vil bli konvertert til nye bøker. Hvis boken inneholder noen sider, som ikke er i et kapitler, boka blir omdøpt og med slike sider, og boka blir en del av den nye bokhyllen.',\n    'convert_to_shelf_permissions_desc' => 'Eventuelle tillatelser som er satt på denne boka, vil bli kopiert til ny hylle og til alle nye under-bøker som ikke har egne tillatelser satt. Vær oppmerksom på at tillatelser på hyllene ikke skjuler automatisk innhold innenfor, da de gjør for bøker.',\n    'convert_book' => 'Konverter bok',\n    'convert_book_confirm' => 'Er du sikker på at du vil konvertere denne boken?',\n    'convert_undo_warning' => 'Dette kan ikke bli så lett å angre.',\n    'convert_to_book' => 'Konverter til bok',\n    'convert_to_book_desc' => 'Du kan konvertere kapittelet til en ny bok med samme innhold. Alle tillatelser som er angitt i dette kapittelet vil bli kopiert til den nye boken, men alle arvede tillatelser, fra overordnet bok vil ikke kopieres noe som kan føre til en endring av tilgangskontroll.',\n    'convert_chapter' => 'Konverter kapittel',\n    'convert_chapter_confirm' => 'Er du sikker på at du vil konvertere dette kapittelet?',\n\n    // References\n    'references' => 'Referanser',\n    'references_none' => 'Det er ingen sporede referanser til dette elementet.',\n    'references_to_desc' => 'Nedanfor vises alle dei kjente sidene i systemet som lenker til denne oppføringa.',\n\n    // Watch Options\n    'watch' => 'Overvåk',\n    'watch_title_default' => 'Standardinnstillinger',\n    'watch_desc_default' => 'Bytt til dine standardinnstilleringer for varsling.',\n    'watch_title_ignore' => 'Ignorer',\n    'watch_desc_ignore' => 'Ignorer alle varslinger, inkludert de fra preferanser for brukernivå.',\n    'watch_title_new' => 'Nye sider',\n    'watch_desc_new' => 'Varsle når en ny side er opprettet innenfor dette elementet.',\n    'watch_title_updates' => 'Alle sideoppdateringer',\n    'watch_desc_updates' => 'Varsle på alle nye sider og endringer av siden.',\n    'watch_desc_updates_page' => 'Varsle ved alle sideendringer.',\n    'watch_title_comments' => 'Alle sideoppdateringer og kommentarer',\n    'watch_desc_comments' => 'Varsle om alle nye sider, endringer på side og nye kommentarer.',\n    'watch_desc_comments_page' => 'Varsle ved sideendringer og nye kommentarer.',\n    'watch_change_default' => 'Endre standard varslingsinnstillinger',\n    'watch_detail_ignore' => 'Ignorerer varsler',\n    'watch_detail_new' => 'Varsling for nye sider',\n    'watch_detail_updates' => 'Varsling for nye sider og oppdateringer',\n    'watch_detail_comments' => 'Varsling for nye sider, oppdateringer og kommentarer',\n    'watch_detail_parent_book' => 'Overvåker via overordnet bok',\n    'watch_detail_parent_book_ignore' => 'Ignorerer via overordnet bok',\n    'watch_detail_parent_chapter' => 'Overvåker via overordnet kapittel',\n    'watch_detail_parent_chapter_ignore' => 'Ignorerer via overordnet kapittel',\n];\n"
  },
  {
    "path": "lang/nn/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Du har ikkje tilgang til å sjå denne sida.',\n    'permissionJson' => 'Du har ikke tilgang til å utføre denne handlingen.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'En konto med :email finnes allerede, men har andre detaljer.',\n    'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',\n    'email_already_confirmed' => 'E-posten er allerede bekreftet, du kan forsøke å logge inn.',\n    'email_confirmation_invalid' => 'Denne bekreftelseskoden er allerede benyttet eller utgått. Prøv å registrere på nytt.',\n    'email_confirmation_expired' => 'Bekreftelseskoden er allerede utgått, en ny e-post er sendt.',\n    'email_confirmation_awaiting' => 'Du må bekrefte e-posten for denne kontoen.',\n    'ldap_fail_anonymous' => 'LDAP kan ikke benyttes med anonym tilgang for denne tjeneren.',\n    'ldap_fail_authed' => 'LDAP tilgang feilet med angitt DN',\n    'ldap_extension_not_installed' => 'LDAP PHP modulen er ikke installert.',\n    'ldap_cannot_connect' => 'Klarer ikke koble til LDAP på denne adressen',\n    'saml_already_logged_in' => 'Allerede logget inn',\n    'saml_no_email_address' => 'Denne kontoinformasjonen finnes ikke i det eksterne autentiseringssystemet.',\n    'saml_invalid_response_id' => 'Forespørselen fra det eksterne autentiseringssystemet gjenkjennes ikke av en prosess som startes av dette programmet. Å navigere tilbake etter pålogging kan forårsake dette problemet.',\n    'saml_fail_authed' => 'Innlogging gjennom :system feilet. Fikk ikke kontakt med autentiseringstjeneren.',\n    'oidc_already_logged_in' => 'Allerede logget inn',\n    'oidc_no_email_address' => 'Finner ikke en e-postadresse, for denne brukeren, i dataene som leveres av det eksterne autentiseringssystemet',\n    'oidc_fail_authed' => 'Innlogging ved hjelp av :system feilet, systemet ga ikke vellykket godkjenning',\n    'social_no_action_defined' => 'Ingen handlinger er definert',\n    'social_login_bad_response' => \"Feilmelding mottat fra :socialAccount innloggingstjeneste: \\n:error\",\n    'social_account_in_use' => 'Denne :socialAccount kontoen er allerede registrert, Prøv å logge inn med :socialAccount alternativet.',\n    'social_account_email_in_use' => 'E-posten :email er allerede i bruk. Har du allerede en konto hos :socialAccount kan dette angis fra profilsiden din.',\n    'social_account_existing' => 'Denne :socialAccount er allerede koblet til din konto.',\n    'social_account_already_used_existing' => 'Denne :socialAccount kontoen brukes allerede av noen andre.',\n    'social_account_not_used' => 'Denne :socialAccount konten er ikke koblet til noen konto, angi denne i profilinnstillingene dine. ',\n    'social_account_register_instructions' => 'Har du ikke en konto her ennå, kan du benytte :socialAccount alternativet for å registrere deg.',\n    'social_driver_not_found' => 'Autentiseringstjeneste fra sosiale medier er ikke installert',\n    'social_driver_not_configured' => 'Dine :socialAccount innstilliner er ikke angitt.',\n    'invite_token_expired' => 'Invitasjonslenken har utgått, du kan forsøke å be om nytt passord istede.',\n    'login_user_not_found' => 'A user for this action could not be found.',\n\n    // System\n    'path_not_writable' => 'Filstien :filePath aksepterer ikkje filer, du må sjekke filstitilganger i systemet.',\n    'cannot_get_image_from_url' => 'Kan ikkje hente bilete frå :url',\n    'cannot_create_thumbs' => 'Kan ikkje opprette miniatyrbilete. GD PHP er ikkje installert.',\n    'server_upload_limit' => 'Vedlegget er for stort, forsøk med et mindre vedlegg.',\n    'server_post_limit' => 'Serveren kan ikkje ta i mot denne mengda med data. Prøv igjen med mindre data eller ei mindre fil.',\n    'uploaded'  => 'Tjenesten aksepterer ikke vedlegg som er så stor.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Biletet kunne ikkje lastast opp, prøv igjen',\n    'image_upload_type_error' => 'Bileteformatet er ikkje støtta, prøv med eit anna format',\n    'image_upload_replace_type' => 'Bileteerstatning må vere av same type',\n    'image_upload_memory_limit' => 'Klarte ikkje å ta i mot bilete og lage miniatyrbilete grunna grenser knytt til systemet.',\n    'image_thumbnail_memory_limit' => 'Klarte ikkje å lage miniatyrbilete grunna grenser knytt til systemet.',\n    'image_gallery_thumbnail_memory_limit' => 'Klarte ikkje å lage miniatyrbilete grunna grenser knytt til systemet.',\n    'drawing_data_not_found' => 'Tegningsdata kunne ikke lastes. Det er mulig at tegningsfilen ikke finnes lenger, eller du har ikke rettigheter til å få tilgang til den.',\n\n    // Attachments\n    'attachment_not_found' => 'Vedlegget ble ikke funnet',\n    'attachment_upload_error' => 'En feil har oppstått ved opplasting av vedleggsfil',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Kunne ikke lagre utkastet, forsikre deg om at du er tilkoblet tjeneren (Har du nettilgang?)',\n    'page_draft_delete_fail' => 'Kunne ikke slette sideutkast og hente gjeldende side lagret innhold',\n    'page_custom_home_deletion' => 'Kan ikke slette en side som er satt som forside.',\n\n    // Entities\n    'entity_not_found' => 'Entitet ble ikke funnet',\n    'bookshelf_not_found' => 'Bokhyllen ble ikke funnet',\n    'book_not_found' => 'Boken ble ikke funnet',\n    'page_not_found' => 'Siden ble ikke funnet',\n    'chapter_not_found' => 'Kapittel ble ikke funnet',\n    'selected_book_not_found' => 'Den valgte boken eksisterer ikke',\n    'selected_book_chapter_not_found' => 'Den valgte boken eller kapittelet eksisterer ikke',\n    'guests_cannot_save_drafts' => 'Gjester kan ikke lagre utkast',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Du kan ikke kaste ut den eneste administratoren',\n    'users_cannot_delete_guest' => 'Du kan ikke slette gjestebrukeren (Du kan deaktivere offentlig visning istede)',\n    'users_could_not_send_invite' => 'Could not create user since invite email failed to send',\n\n    // Roles\n    'role_cannot_be_edited' => 'Denne rollen kan ikke endres',\n    'role_system_cannot_be_deleted' => 'Denne systemrollen kan ikke slettes',\n    'role_registration_default_cannot_delete' => 'Du kan ikke slette en rolle som er satt som registreringsrolle (rollen nye kontoer får når de registrerer seg)',\n    'role_cannot_remove_only_admin' => 'Denne brukeren er den eneste brukeren som er tildelt administratorrollen. Tilordne administratorrollen til en annen bruker før du prøver å fjerne den her.',\n\n    // Comments\n    'comment_list' => 'Det oppstod en feil under henting av kommentarene.',\n    'cannot_add_comment_to_draft' => 'Du kan ikke legge til kommentarer i et utkast.',\n    'comment_add' => 'Det oppsto en feil da kommentaren skulle legges til / oppdateres.',\n    'comment_delete' => 'Det oppstod en feil under sletting av kommentaren.',\n    'empty_comment' => 'Kan ikke legge til en tom kommentar.',\n\n    // Error pages\n    '404_page_not_found' => 'Siden finnes ikke',\n    'sorry_page_not_found' => 'Beklager, siden du leter etter ble ikke funnet.',\n    'sorry_page_not_found_permission_warning' => 'Hvis du forventet at denne siden skulle eksistere, har du kanskje ikke tillatelse til å se den.',\n    'image_not_found' => 'Bilete vart ikkje funne',\n    'image_not_found_subtitle' => 'Orsak, biletefila vart ikkje funne.',\n    'image_not_found_details' => 'Det kan sjå ut til at biletet du leiter etter er sletta.',\n    'return_home' => 'Gå til hovedside',\n    'error_occurred' => 'En feil oppsto',\n    'app_down' => ':appName er nede for øyeblikket',\n    'back_soon' => 'Den vil snart komme tilbake.',\n\n    // Import\n    'import_zip_cant_read' => 'Could not read ZIP file.',\n    'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',\n    'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Import ZIP failed to validate with errors:',\n    'import_zip_failed_notification' => 'Failed to import ZIP file.',\n    'import_perms_books' => 'You are lacking the required permissions to create books.',\n    'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',\n    'import_perms_pages' => 'You are lacking the required permissions to create pages.',\n    'import_perms_images' => 'You are lacking the required permissions to create images.',\n    'import_perms_attachments' => 'You are lacking the required permission to create attachments.',\n\n    // API errors\n    'api_no_authorization_found' => 'Ingen autorisasjonstoken ble funnet på forespørselen',\n    'api_bad_authorization_format' => 'Det ble funnet et autorisasjonstoken på forespørselen, men formatet virket feil',\n    'api_user_token_not_found' => 'Ingen samsvarende API-token ble funnet for det angitte autorisasjonstokenet',\n    'api_incorrect_token_secret' => 'Hemmeligheten som er gitt for det gitte brukte API-tokenet er feil',\n    'api_user_no_api_permission' => 'Eieren av det brukte API-tokenet har ikke tillatelse til å ringe API-samtaler',\n    'api_user_token_expired' => 'Autorisasjonstokenet som er brukt, har utløpt',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Feil kastet når du sendte en test-e-post:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'URLen samsvarer ikke med de konfigurerte SSR-vertene',\n];\n"
  },
  {
    "path": "lang/nn/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Ny kommentar på sida: :pageName',\n    'new_comment_intro' => 'Ein brukar har kommentert ei side i :appName:',\n    'new_page_subject' => 'Ny side: :pageName',\n    'new_page_intro' => 'Ei ny side vart oppretta i :appName:',\n    'updated_page_subject' => 'Oppdatert side: :pageName',\n    'updated_page_intro' => 'Ei side vart oppdatert i :appName:',\n    'updated_page_debounce' => 'For å forhindre mange varslingar, vil du ikkje få nye varslinger for endringar på denne siden frå same forfattar.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Sidenamn:',\n    'detail_page_path' => 'Sidenamn:',\n    'detail_commenter' => 'Kommentar frå:',\n    'detail_comment' => 'Kommentar:',\n    'detail_created_by' => 'Oppretta av:',\n    'detail_updated_by' => 'Oppdatert av:',\n\n    'action_view_comment' => 'Vis kommentar',\n    'action_view_page' => 'Sjå side',\n\n    'footer_reason' => 'Denne meldinga vart sendt til deg fordi :link dekker denne typen aktivitet for dette elementet.',\n    'footer_reason_link' => 'dine varslingsinnstillingar',\n];\n"
  },
  {
    "path": "lang/nn/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Førre',\n    'next'     => 'Neste &raquo;',\n\n];\n"
  },
  {
    "path": "lang/nn/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Passord må inneholde minst åtte tegn og samsvarer med bekreftelsen.',\n    'user' => \"Vi finner ikke en bruker med den e-postadressen.\",\n    'token' => 'Passordet for tilbakestilling av passord er ugyldig for denne e-postadressen.',\n    'sent' => 'Vi har sendt e-postadressen til tilbakestilling av passordet ditt!',\n    'reset' => 'Passordet ditt har blitt tilbakestilt!',\n\n];\n"
  },
  {
    "path": "lang/nn/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Min konto',\n\n    'shortcuts' => 'Snarveier',\n    'shortcuts_interface' => 'UI snarvegar',\n    'shortcuts_toggle_desc' => 'Her kan du aktivere eller deaktivere snarveier for tastatur system som brukes til navigasjon og handlinger.',\n    'shortcuts_customize_desc' => 'Du kan tilpasse hver av snarveiene nedenfor. Trykk på ønsket nøkkelkombinasjon etter å ha valgt inndata for en snarvei.',\n    'shortcuts_toggle_label' => 'Tastatursnarveier aktivert',\n    'shortcuts_section_navigation' => 'Navigasjon',\n    'shortcuts_section_actions' => 'Vanlige handlinger',\n    'shortcuts_save' => 'Lagre snarveier',\n    'shortcuts_overlay_desc' => 'Merk: Når snarveier er aktivert er et hjelperoverlegg tilgjengelig via å trykke \"?\" som vil fremheve de tilgjengelige snarveiene som for øyeblikket er synlige på skjermen.',\n    'shortcuts_update_success' => 'Snarvei innstillinger er oppdatert!',\n    'shortcuts_overview_desc' => 'Behandle tastatursnarveier du kan bruke for å navigere i systembrukergrensesnittet.',\n\n    'notifications' => 'Innstillinger for varsling',\n    'notifications_desc' => 'Kontroller e-postvarslene du mottar når en bestemt aktivitet utføres i systemet.',\n    'notifications_opt_own_page_changes' => 'Varsle ved endringer til sider jeg eier',\n    'notifications_opt_own_page_comments' => 'Varsle om kommentarer på sider jeg eier',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Varsle ved svar på mine kommentarer',\n    'notifications_save' => 'Lagre innstillinger',\n    'notifications_update_success' => 'Varslingsinnstillingene er oppdatert!',\n    'notifications_watched' => 'Overvåka & ignorerte elementer',\n    'notifications_watched_desc' => 'Nedenfor er elementene som har egendefinerte varslingsinnstillinger i bruk. For å oppdatere innstillingene for disse, se elementet, finn varslingsalternativene i sidepanelet.',\n\n    'auth' => 'Tilgang og tryggleik',\n    'auth_change_password' => 'Endre passord',\n    'auth_change_password_desc' => 'Endre passordet du brukar for å logge inn på programmet. Dette må vere minst 8 teikn langt.',\n    'auth_change_password_success' => 'Passordet har blitt oppdatert!',\n\n    'profile' => 'Profildetaljar',\n    'profile_desc' => 'Gi opplysningar om kontoen som representerer deg til andre brukarar, i tillegg til opplysninger som vert brukt til kommunikasjon og systempersonalisering.',\n    'profile_view_public' => 'Vis offentleg profil',\n    'profile_name_desc' => 'Tilpass visingsnavn som vil vere synlig for andre brukarar i systemet ved hjelp av aktiviteten du utfører, og innhaldet du eiger.',\n    'profile_email_desc' => 'Denne e-posten vert brukt til å sende varsler, og avhengig av aktiv systemautentisering, systemtilgang.',\n    'profile_email_no_permission' => 'Du har diverre ikkje løyve til å endre e-postadressa di. Om du ynskjer å endre, må du be ein administrator om å endre dette for deg.',\n    'profile_avatar_desc' => 'Velg eit profilbilete. Ideelt sett bør dette bildet vere kvadratisk og ca. 256 px i breidde og høgde.',\n    'profile_admin_options' => 'Alternativer for administrator',\n    'profile_admin_options_desc' => 'Du finner fleire alternativ på administratornivå, t. d. administrasjon av rolletildelinger, for din brukerkonto i området \"Innstillingar > Brukarar\" i applikasjonen.',\n\n    'delete_account' => 'Slett konto',\n    'delete_my_account' => 'Slett kontoen min',\n    'delete_my_account_desc' => 'Dette vil slette din brukarkonto frå systemet. Du vil ikkje kunne gjenopprette kontoen eller tilbakestille denne handlinga. Innhald du har oppretta, som t. d. sider og bilete, vil forbli uendret.',\n    'delete_my_account_warning' => 'Er du sikker på at du vil slette kontoen din?',\n];\n"
  },
  {
    "path": "lang/nn/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Innstillinger',\n    'settings_save' => 'Lagre innstillinger',\n    'system_version' => 'System versjon',\n    'categories' => 'Kategorier',\n\n    // App Settings\n    'app_customization' => 'Tilpassing',\n    'app_features_security' => 'Funksjonar og tryggleik',\n    'app_name' => 'Applikasjonsnavn',\n    'app_name_desc' => 'Dette navnet vises i overskriften og i alle e-postmeldinger som sendes av systemet.',\n    'app_name_header' => 'Vis navn i topptekst',\n    'app_public_access' => 'Offentlig tilgang',\n    'app_public_access_desc' => 'Hvis du aktiverer dette alternativet, kan besøkende, som ikke er logget på, få tilgang til innhold i din BookStack-forekomst.',\n    'app_public_access_desc_guest' => 'Tilgang for offentlige besøkende kan kontrolleres gjennom \"Gjest\" -brukeren.',\n    'app_public_access_toggle' => 'Tillat offentlig tilgang',\n    'app_public_viewing' => 'Tillat offentlig visning?',\n    'app_secure_images' => 'Høyere tryggleik på bileteopplastingar',\n    'app_secure_images_toggle' => 'Skru på høgare tryggleik på bileteopplastingar',\n    'app_secure_images_desc' => 'Av ytelsesgrunner er alle bilder offentlige. Dette alternativet legger til en tilfeldig streng som er vanskelig å gjette foran bildets nettadresser. Forsikre deg om at katalogindekser ikke er aktivert for å forhindre enkel tilgang.',\n    'app_default_editor' => 'Standard sideredigeringsprogram',\n    'app_default_editor_desc' => 'Velg hvilken tekstbehandler som skal brukes som standard når du redigerer nye sider. Dette kan overskrives på et sidenivå der tillatelser tillates.',\n    'app_custom_html' => 'Tilpasset HTML-hodeinnhold',\n    'app_custom_html_desc' => 'Alt innhold som legges til her, blir satt inn i bunnen av <head> -delen på hver side. Dette er praktisk for å overstyre stiler eller legge til analysekode.',\n    'app_custom_html_disabled_notice' => 'Tilpasset HTML-hodeinnhold er deaktivert på denne innstillingssiden for å sikre at eventuelle endringer ødelegger noe, kan tilbakestilles.',\n    'app_logo' => 'Applikasjonslogo',\n    'app_logo_desc' => 'Dette brukes i programtoppfeltet blant andre områder. Dette bildet skal være 86px i høyde. Store bilder vil bli skalert ned.',\n    'app_icon' => 'Applikasjons ikon',\n    'app_icon_desc' => 'Dette ikonet brukes for nettleserfaner og snarveisikoner. Dette bør være et bilde på 256 px kvadrat PNG.',\n    'app_homepage' => 'Applikasjonens hjemmeside',\n    'app_homepage_desc' => 'Velg en visning som skal vises på hjemmesiden i stedet for standardvisningen. Sidetillatelser ignoreres for utvalgte sider.',\n    'app_homepage_select' => 'Velg en side',\n    'app_footer_links' => 'Fotlenker',\n    'app_footer_links_desc' => 'Legg til fotlenker i sidens fotområde. Disse vil vises nederst på de fleste sider, inkludert sider som ikke krever innlogging. Du kan bruke «trans::<key>» etiketter for system-definerte oversettelser. For eksempel: Bruk «trans::common.privacy_policy» for å vise teksten «Personvernregler» og «trans::common.terms_of_service» for å vise teksten «Bruksvilkår».',\n    'app_footer_links_label' => 'Lenketekst',\n    'app_footer_links_url' => 'Lenke',\n    'app_footer_links_add' => 'Legg til fotlenke',\n    'app_disable_comments' => 'Deaktiver kommentarer',\n    'app_disable_comments_toggle' => 'Deaktiver kommentarer',\n    'app_disable_comments_desc' => 'Deaktiver kommentarer på tvers av alle sidene i applikasjonen. <br> Eksisterende kommentarer vises ikke.',\n\n    // Color settings\n    'color_scheme' => 'Applikasjonens farge oppsett',\n    'color_scheme_desc' => 'Sett farger for å bruke i programmets brukergrensesnitt. Farger kan konfigureres separat for mørke og lysmoduser for å passe best inn temaet og sørge for lesbarhet.',\n    'ui_colors_desc' => 'Angi primær farge for programmet og standard link farge. Primær farge brukes hovedsakelig for toppbanner, knapper og grensesnittets dekorasjoner. Standardfargen for koblinger brukes for tekstbaserte lenker og handlinger, både i skriftlig innhold og i programgrensesnittet.',\n    'app_color' => 'Primær farge',\n    'link_color' => 'Standard koblingsfarge',\n    'content_colors_desc' => 'Angi farger for alle elementer i organiseringshierarkiet. Velger du farger med lik lysstyrke til standard farger anbefales for lesbarhet.',\n    'bookshelf_color' => 'Hyllefarge',\n    'book_color' => 'Bokfarge',\n    'chapter_color' => 'Kapittelfarge',\n    'page_color' => 'Sidefarge',\n    'page_draft_color' => 'Sideutkastsfarge',\n\n    // Registration Settings\n    'reg_settings' => 'Registrering',\n    'reg_enable' => 'Tillat registrering',\n    'reg_enable_toggle' => 'Tillat registrering',\n    'reg_enable_desc' => 'Når registrering er aktivert vil brukeren kunne registrere seg som applikasjonsbruker. Ved registrering får de en standard brukerrolle.',\n    'reg_default_role' => 'Standard brukerrolle etter registrering',\n    'reg_enable_external_warning' => 'Alternativet ovenfor ignoreres mens ekstern LDAP- eller SAML-autentisering er aktiv. Brukerkontoer for ikke-eksisterende medlemmer blir automatisk opprettet hvis autentisering mot det eksterne systemet i bruk lykkes.',\n    'reg_email_confirmation' => 'E-postbekreftelse',\n    'reg_email_confirmation_toggle' => 'Krev e-postbekreftelse',\n    'reg_confirm_email_desc' => 'Hvis domenebegrensning brukes, vil e-postbekreftelse være nødvendig, og dette alternativet vil bli ignorert.',\n    'reg_confirm_restrict_domain' => 'Domenebegrensning',\n    'reg_confirm_restrict_domain_desc' => 'Skriv inn en kommaseparert liste over e-postdomener du vil begrense registreringen til. Brukerne vil bli sendt en e-post for å bekrefte adressen deres før de får lov til å kommunisere med applikasjonen. <br> Vær oppmerksom på at brukere vil kunne endre e-postadressene sine etter vellykket registrering.',\n    'reg_confirm_restrict_domain_placeholder' => 'Ingen begrensninger er satt',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Vedlikehold',\n    'maint_image_cleanup' => 'Rydd opp bilete',\n    'maint_image_cleanup_desc' => 'Skanner side og revisjonsinnhold for å sjekke kva bilete og teikninar som for er i bruk no, og kva bilete som er til overs. Sørg for å tryggleikskopiere heile databasen og alle bilete før du kjører denne.',\n    'maint_delete_images_only_in_revisions' => 'Slett også bilder som bare finnes i game siderevisjoner',\n    'maint_image_cleanup_run' => 'Kjør opprydding',\n    'maint_image_cleanup_warning' => ':count potensielt ubrukte bilder ble funnet. Er du sikker på at du vil slette disse bildene?',\n    'maint_image_cleanup_success' => ':count potensielt ubrukte bilder funnet og slettet!',\n    'maint_image_cleanup_nothing_found' => 'Ingen ubrukte bilder funnet, ingenting slettet!',\n    'maint_send_test_email' => 'Send en test-e-post',\n    'maint_send_test_email_desc' => 'Dette sender en test-e-post til din e-postadresse som er angitt i profilen din.',\n    'maint_send_test_email_run' => 'Send en test-e-post',\n    'maint_send_test_email_success' => 'Send en test-e-post til :address',\n    'maint_send_test_email_mail_subject' => 'Test-e-post',\n    'maint_send_test_email_mail_greeting' => 'E-postsending ser ut til å fungere!',\n    'maint_send_test_email_mail_text' => 'Gratulerer! Da du mottok dette e-postvarselet, ser det ut til at e-postinnstillingene dine er konfigurert riktig.',\n    'maint_recycle_bin_desc' => 'Slettede hyller, bøker, kapitler og sider kastes i papirkurven så de kan bli gjenopprettet eller slettet permanent. Eldre utgaver i papirkurven kan slettes automatisk etter en stund, avhengig av systemkonfigurasjonen.',\n    'maint_recycle_bin_open' => 'Åpne papirkurven',\n    'maint_regen_references' => 'Regenerer referanser',\n    'maint_regen_references_desc' => 'Denne handlingen gjenoppbygger referanseindeksen for krysselement i databasen. Dette håndteres vanligvis automatisk, men denne handlingen kan være nyttig for å indeksere gammelt innhold eller innhold lagt til via uoffisielle metoder.',\n    'maint_regen_references_success' => 'Referanseindeksen har blitt regenerert!',\n    'maint_timeout_command_note' => 'Merk: Denne handlingen kan ta tid å kjøre, noe som kan føre til tidsavbruddsmessige problemer i noen webomgivelser. Dette gjøres som et alternativ ved hjelp av en terminalkommando.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Papirkurven',\n    'recycle_bin_desc' => 'Her kan du gjenopprette ting du har kastet i papirkurven eller velge å slette dem permanent fra systemet. Denne listen er ikke filtrert i motsetning til lignende lister i systemet hvor tilgangskontroll overholdes.',\n    'recycle_bin_deleted_item' => 'Kastet element',\n    'recycle_bin_deleted_parent' => 'Overordnet',\n    'recycle_bin_deleted_by' => 'Kastet av',\n    'recycle_bin_deleted_at' => 'Kastet den',\n    'recycle_bin_permanently_delete' => 'Slett permanent',\n    'recycle_bin_restore' => 'Gjenopprett',\n    'recycle_bin_contents_empty' => 'Papirkurven er for øyeblikket tom',\n    'recycle_bin_empty' => 'Tøm papirkurven',\n    'recycle_bin_empty_confirm' => 'Dette vil slette alle elementene i papirkurven permanent. Dette inkluderer innhold i hvert element. Er du sikker på at du vil tømme papirkurven?',\n    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?',\n    'recycle_bin_destroy_list' => 'Elementer som skal slettes',\n    'recycle_bin_restore_list' => 'Elementer som skal gjenopprettes',\n    'recycle_bin_restore_confirm' => 'Denne handlingen vil hente opp elementet fra papirkurven, inkludert underliggende innhold, til sin opprinnelige sted. Om den opprinnelige plassen har blitt slettet i mellomtiden og nå befinner seg i papirkurven, vil også dette bli hentet opp igjen.',\n    'recycle_bin_restore_deleted_parent' => 'Det overordnede elementet var også kastet i papirkurven. Disse elementene vil forbli kastet inntil det overordnede også hentes opp igjen.',\n    'recycle_bin_restore_parent' => 'Gjenopprett overodnet',\n    'recycle_bin_destroy_notification' => 'Slettet :count elementer fra papirkurven.',\n    'recycle_bin_restore_notification' => 'Gjenopprettet :count elementer fra papirkurven.',\n\n    // Audit Log\n    'audit' => 'Revisjonslogg',\n    'audit_desc' => 'Denne revisjonsloggen viser en liste over aktiviteter som spores i systemet. Denne listen er ufiltrert i motsetning til lignende aktivitetslister i systemet der tillatelsesfiltre brukes.',\n    'audit_event_filter' => 'Hendelsesfilter',\n    'audit_event_filter_no_filter' => 'Ingen filter',\n    'audit_deleted_item' => 'Slettet ting',\n    'audit_deleted_item_name' => 'Navn: :name',\n    'audit_table_user' => 'Kontoholder',\n    'audit_table_event' => 'Hendelse',\n    'audit_table_related' => 'Relaterte elementer eller detaljer',\n    'audit_table_ip' => 'IP Adresse',\n    'audit_table_date' => 'Aktivitetsdato',\n    'audit_date_from' => 'Datoperiode fra',\n    'audit_date_to' => 'Datoperiode til',\n\n    // Role Settings\n    'roles' => 'Roller',\n    'role_user_roles' => 'Kontoroller',\n    'roles_index_desc' => 'Roller brukes til å gruppere brukere og gi systemtilgang til medlemmene. Når en bruker er medlem av flere roller, vil de tildelte rettighetene samles inn, og brukeren vil arve alle evner.',\n    'roles_x_users_assigned' => ':count bruker tildelt|:count brukere tildelt',\n    'roles_x_permissions_provided' => ':count tillatelse|:count tillatelser',\n    'roles_assigned_users' => 'Tilordnede brukere',\n    'roles_permissions_provided' => 'Tilbudte rettigheter',\n    'role_create' => 'Opprett ny rolle',\n    'role_delete' => 'Slett rolle',\n    'role_delete_confirm' => 'Dette vil slette rollen «:roleName».',\n    'role_delete_users_assigned' => 'Denne rollen har :userCount kontoer koblet opp mot seg. Velg hvilke rolle du vil flytte disse til.',\n    'role_delete_no_migration' => \"Ikke flytt kontoer\",\n    'role_delete_sure' => 'Er du sikker på at du vil slette rollen?',\n    'role_edit' => 'Endre rolle',\n    'role_details' => 'Rolledetaljer',\n    'role_name' => 'Rollenavn',\n    'role_desc' => 'Kort beskrivelse av rolle',\n    'role_mfa_enforced' => 'Krever flerfaktorautentisering',\n    'role_external_auth_id' => 'Ekstern godkjennings-ID',\n    'role_system' => 'Systemtilganger',\n    'role_manage_users' => 'Behandle kontoer',\n    'role_manage_roles' => 'Behandle roller og rolletilganger',\n    'role_manage_entity_permissions' => 'Behandle bok-, kapittel- og sidetilganger',\n    'role_manage_own_entity_permissions' => 'Behandle tilganger på egne verk',\n    'role_manage_page_templates' => 'Behandle sidemaler',\n    'role_access_api' => 'Systemtilgang API',\n    'role_manage_settings' => 'Behandle applikasjonsinnstillinger',\n    'role_export_content' => 'Eksporter innhold',\n    'role_import_content' => 'Import content',\n    'role_editor_change' => 'Endre sideredigering',\n    'role_notifications' => 'Motta og administrere varslinger',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Eiendomstillatelser',\n    'roles_system_warning' => 'Vær oppmerksom på at tilgang til noen av de ovennevnte tre tillatelsene kan tillate en bruker å endre sine egne rettigheter eller rettighetene til andre i systemet. Bare tildel roller med disse tillatelsene til pålitelige brukere.',\n    'role_asset_desc' => 'Disse tillatelsene kontrollerer standard tilgang til eiendelene i systemet. Tillatelser til bøker, kapitler og sider overstyrer disse tillatelsene.',\n    'role_asset_admins' => 'Administratorer får automatisk tilgang til alt innhold, men disse alternativene kan vise eller skjule UI-alternativer.',\n    'role_asset_image_view_note' => 'Dette gjelder synlighet innenfor bilde-administrasjonen. Faktisk tilgang på opplastede bildefiler vil være avhengig av valget for systemlagring av bildet.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'Alle',\n    'role_own' => 'Egne',\n    'role_controlled_by_asset' => 'Kontrollert av eiendelen de er lastet opp til',\n    'role_save' => 'Lagre rolle',\n    'role_users' => 'Kontoholdere med denne rollen',\n    'role_users_none' => 'Ingen kontoholdere er gitt denne rollen',\n\n    // Users\n    'users' => 'Brukere',\n    'users_index_desc' => 'Opprett og administrer individuelle brukerkontoer innenfor systemet. Brukerkontoer brukes for innlogging og navngivelse av innhold og aktivitet. Tilgangstillatelser er primært rollebasert, men brukerinnhold eierskap, blant andre faktorer, kan også påvirke tillatelser og tilgang.',\n    'user_profile' => 'Profil',\n    'users_add_new' => 'Register ny konto',\n    'users_search' => 'Søk i kontoer',\n    'users_latest_activity' => 'Siste aktivitet',\n    'users_details' => 'Kontodetaljer',\n    'users_details_desc' => 'Angi et visningsnavn og en e-postadresse for denne kontoholderen. E-postadressen vil bli brukt til å logge på applikasjonen.',\n    'users_details_desc_no_email' => 'Angi et visningsnavn for denne kontoholderen slik at andre kan gjenkjenne dem.',\n    'users_role' => 'Roller',\n    'users_role_desc' => 'Velg hvilke roller denne kontoholderen vil bli tildelt. Hvis en kontoholderen er tildelt flere roller, vil tillatelsene fra disse rollene stable seg, og de vil motta alle evnene til de tildelte rollene.',\n    'users_password' => 'Passord',\n    'users_password_desc' => 'Angi et passord som brukes til å logge inn til programmet. Dette må være minst 8 tegn langt.',\n    'users_send_invite_text' => 'Du kan velge å sende denne kontoholderen en invitasjons-e-post som lar dem angi sitt eget passord, ellers kan du selv angi passordet.',\n    'users_send_invite_option' => 'Send invitasjonsmelding',\n    'users_external_auth_id' => 'Ekstern godkjennings-ID',\n    'users_external_auth_id_desc' => 'Når eit eksternt autentiseringssystem er i bruk (som SAML2, OIDC eller LDAP) er dette ID-en som vert kobla til denneBookStack-brukaren til autentiseringssystemkontoen. Du kan ignorere dette feltet om du bruker standard e-postbasert autentisering.',\n    'users_password_warning' => 'Berre fyll ut under om du vil endre passordet til brukaren.',\n    'users_system_public' => 'Denne brukeren representerer alle gjester som besøker appliaksjonen din. Den kan ikke brukes til å logge på, men tildeles automatisk.',\n    'users_delete' => 'Slett konto',\n    'users_delete_named' => 'Slett kontoen :userName',\n    'users_delete_warning' => 'Dette vil fullstendig slette denne brukeren med navnet «:userName» fra systemet.',\n    'users_delete_confirm' => 'Er du sikker på at du vil slette denne kontoen?',\n    'users_migrate_ownership' => 'Overfør eierskap',\n    'users_migrate_ownership_desc' => 'Velg en bruker her, som du ønsker skal ta eierskap over alle elementene som er eid av denne brukeren.',\n    'users_none_selected' => 'Ingen bruker valgt',\n    'users_edit' => 'Rediger konto',\n    'users_edit_profile' => 'Rediger profil',\n    'users_avatar' => 'Kontobilde',\n    'users_avatar_desc' => 'Velg et bilde for å representere denne kontoholderen. Dette skal være omtrent 256px kvadrat.',\n    'users_preferred_language' => 'Foretrukket språk',\n    'users_preferred_language_desc' => 'Dette alternativet vil endre språket som brukes til brukergrensesnittet til applikasjonen. Dette påvirker ikke noe brukeropprettet innhold.',\n    'users_social_accounts' => 'Sosiale kontoer',\n    'users_social_accounts_desc' => 'Vis status for dei tilkoblede sosiale kontoane for denne brukaren. Sosiale kontoer kan brukast i tillegg til det primære autentiseringssystemet for systemtilgang.',\n    'users_social_accounts_info' => 'Her kan du koble andre kontoer for raskere og enklere pålogging. Hvis du frakobler en konto her, tilbakekaller ikke dette tidligere autorisert tilgang. Tilbakekall tilgang fra profilinnstillingene dine på den tilkoblede sosiale kontoen.',\n    'users_social_connect' => 'Koble til konto',\n    'users_social_disconnect' => 'Koble fra konto',\n    'users_social_status_connected' => 'Tilkobla',\n    'users_social_status_disconnected' => 'Fråkobla',\n    'users_social_connected' => ':socialAccount ble lagt til din konto.',\n    'users_social_disconnected' => ':socialAccount ble koblet fra din konto.',\n    'users_api_tokens' => 'API-nøkler',\n    'users_api_tokens_desc' => 'Opprett og handter tilgangstokenar som vert brukt til å godkjenne med BookStack REST API. Løyve til API blir administrert via brukaren som tokenet tilhører.',\n    'users_api_tokens_none' => 'Ingen API-nøkler finnes for denne kontoen',\n    'users_api_tokens_create' => 'Opprett nøkkel',\n    'users_api_tokens_expires' => 'Utløper',\n    'users_api_tokens_docs' => 'API-dokumentasjon',\n    'users_mfa' => 'Flerfaktorautentisering',\n    'users_mfa_desc' => 'Konfigurer flerfaktorautentisering som eit ekstra lag med tryggleik for din konto.',\n    'users_mfa_x_methods' => ':count metode konfigurert|:count metoder konfigurert',\n    'users_mfa_configure' => 'Konfigurer metoder',\n\n    // API Tokens\n    'user_api_token_create' => 'Opprett API-nøkkel',\n    'user_api_token_name' => 'Navn',\n    'user_api_token_name_desc' => 'Gi nøkkelen et lesbart navn som en fremtidig påminnelse om det tiltenkte formålet.',\n    'user_api_token_expiry' => 'Utløpsdato',\n    'user_api_token_expiry_desc' => 'Angi en dato da denne nøkkelen utløper. Etter denne datoen vil forespørsler som er gjort med denne nøkkelen ikke lenger fungere. Å la dette feltet stå tomt vil sette utløpsdato 100 år inn i fremtiden.',\n    'user_api_token_create_secret_message' => 'Umiddelbart etter å ha opprettet denne nøkkelen vil en identifikator og hemmelighet bli generert og vist. Hemmeligheten vil bare vises en gang, så husk å kopiere verdien til et trygt sted før du fortsetter.',\n    'user_api_token' => 'API-nøkkel',\n    'user_api_token_id' => 'Identifikator',\n    'user_api_token_id_desc' => 'Dette er en ikke-redigerbar systemgenerert identifikator for denne nøkkelen som må oppgis i API-forespørsler.',\n    'user_api_token_secret' => 'Hemmelighet',\n    'user_api_token_secret_desc' => 'Dette er en systemgenerert hemmelighet for denne nøkkelen som må leveres i API-forespørsler. Dette vises bare denne gangen, så kopier denne verdien til et trygt sted.',\n    'user_api_token_created' => 'Nøkkel opprettet :timeAgo',\n    'user_api_token_updated' => 'Nøkkel oppdatert :timeAgo',\n    'user_api_token_delete' => 'Slett nøkkel',\n    'user_api_token_delete_warning' => 'Dette vil slette API-nøkkelen \\':tokenName\\' fra systemet.',\n    'user_api_token_delete_confirm' => 'Sikker på at du vil slette nøkkelen?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhooks er en måte å sende data til eksterne nettadresser når bestemte handlinger og hendelser oppstår i systemet som gjør det mulig å integrer med eksterne plattformer som meldingssystemer eller varslingssystemer.',\n    'webhooks_x_trigger_events' => ':count utløsende hendelse:count utløsende hendelser',\n    'webhooks_create' => 'Lag ny Webhook',\n    'webhooks_none_created' => 'Ingen webhooks er opprettet ennå.',\n    'webhooks_edit' => 'Rediger webhook',\n    'webhooks_save' => 'Lagre Webhook',\n    'webhooks_details' => 'Webhook detaljer',\n    'webhooks_details_desc' => 'Gi et brukervennlig navn og et POST endepunkt som et sted der webhook-dataene skal sendes til.',\n    'webhooks_events' => 'Webhook hendelser',\n    'webhooks_events_desc' => 'Velg alle hendelsene som skal utløse denne webhook som skal kalles.',\n    'webhooks_events_warning' => 'Husk at disse hendelsene vil bli utløst for alle valgte hendelser, selv om egendefinerte tillatelser brukes. Pass på at bruk av denne webhooken ikke vil utsette konfidensiell innhold.',\n    'webhooks_events_all' => 'Alle systemhendelser',\n    'webhooks_name' => 'Webhook navn',\n    'webhooks_timeout' => 'Tidsavbrudd for Webhook forespørsler (sekunder)',\n    'webhooks_endpoint' => 'Webhook endepunkt',\n    'webhooks_active' => 'Webhook aktiv',\n    'webhook_events_table_header' => 'Hendelser',\n    'webhooks_delete' => 'Slett webhook',\n    'webhooks_delete_warning' => 'Dette vil slette webhook, med navnet \\':webhookName\\', fra systemet.',\n    'webhooks_delete_confirm' => 'Er du sikker på at du vil slette denne webhooken?',\n    'webhooks_format_example' => 'Webhook formattering eksempel',\n    'webhooks_format_example_desc' => 'Webhook-data sendes som en POST-forespørsel til det konfigurerte endepunktet som JSON ved hjelp av formatet nedenfor. «related_item» og «url» egenskaper er valgfrie og vil avhenge av hvilken type hendelse som utløses.',\n    'webhooks_status' => 'Webhook status',\n    'webhooks_last_called' => 'Sist ringt:',\n    'webhooks_last_errored' => 'Siste feil:',\n    'webhooks_last_error_message' => 'Siste feilmelding:',\n\n    // Licensing\n    'licenses' => 'Licenses',\n    'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.',\n    'licenses_bookstack' => 'BookStack License',\n    'licenses_php' => 'PHP Library Licenses',\n    'licenses_js' => 'JavaScript Library Licenses',\n    'licenses_other' => 'Other Licenses',\n    'license_details' => 'License Details',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/nn/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute må aksepteres.',\n    'active_url'           => ':attribute er ikke en godkjent URL.',\n    'after'                => ':attribute må være en dato etter :date.',\n    'alpha'                => ':attribute kan kun inneholde bokstaver.',\n    'alpha_dash'           => ':attribute kan kunne inneholde bokstaver, tall, bindestreker eller understreker.',\n    'alpha_num'            => ':attribute kan kun inneholde bokstaver og tall.',\n    'array'                => ':attribute må være en liste.',\n    'backup_codes'         => 'Den angitte koden er ikke gyldig, eller er allerede benyttet.',\n    'before'               => ':attribute må være en dato før :date.',\n    'between'              => [\n        'numeric' => ':attribute må være mellom :min og :max.',\n        'file'    => ':attribute må være mellom :min og :max kilobytes.',\n        'string'  => ':attribute må være mellom :min og :max tegn.',\n        'array'   => ':attribute må være mellom :min og :max ting.',\n    ],\n    'boolean'              => ':attribute feltet kan bare være sann eller falsk.',\n    'confirmed'            => ':attribute bekreftelsen samsvarer ikke.',\n    'date'                 => ':attribute er ikke en gyldig dato.',\n    'date_format'          => ':attribute samsvarer ikke med :format.',\n    'different'            => ':attribute og :other må være forskjellige.',\n    'digits'               => ':attribute må være :digits tall.',\n    'digits_between'       => ':attribute må være mellomg :min og :max tall.',\n    'email'                => ':attribute må være en gyldig e-post.',\n    'ends_with' => ':attribute må slutte med en av verdiene: :values',\n    'file'                 => 'Attributtet :attribute må angis som en gyldig fil.',\n    'filled'               => ':attribute feltet er påkrevd.',\n    'gt'                   => [\n        'numeric' => ':attribute må være større enn :value.',\n        'file'    => ':attribute må være større enn :value kilobytes.',\n        'string'  => ':attribute må være større enn :value tegn.',\n        'array'   => ':attribute må ha mer en :value ting.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute må være større enn eller lik :value.',\n        'file'    => ':attribute må være større enn eller lik :value kilobytes.',\n        'string'  => ':attribute må være større enn eller lik :value tegn.',\n        'array'   => ':attribute må ha :value eller flere ting.',\n    ],\n    'exists'               => 'Den valgte :attribute er ugyldig.',\n    'image'                => ':attribute må være et bilde.',\n    'image_extension'      => ':attribute må ha støttet formattype.',\n    'in'                   => 'Den valgte :attribute er ugyldig.',\n    'integer'              => ':attribute må være et heltall',\n    'ip'                   => ':attribute må være en gyldig IP adresse.',\n    'ipv4'                 => ':attribute må være en gyldig IPv4 adresse.',\n    'ipv6'                 => ':attribute må være en gyldig IPv6 adresse.',\n    'json'                 => ':attribute må være en gyldig JSON tekststreng.',\n    'lt'                   => [\n        'numeric' => ':attribute må være mindre enn :value.',\n        'file'    => ':attribute må være mindre enn :value kilobytes.',\n        'string'  => ':attribute må være mindre enn :value tegn.',\n        'array'   => ':attribute må ha mindre enn :value ting.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute må være mindre enn eller lik :value.',\n        'file'    => ':attribute må være mindre enn eller lik :value kilobytes.',\n        'string'  => ':attribute må være mindre enn eller lik :value characters.',\n        'array'   => ':attribute må ha mindre enn eller lik :value ting.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute kan ikke være større enn :max.',\n        'file'    => ':attribute kan ikke være større enn :max kilobytes.',\n        'string'  => ':attribute kan ikke være større enn :max tegn.',\n        'array'   => ':attribute kan ikke inneholde mer enn :max ting.',\n    ],\n    'mimes'                => ':attribute må være en fil av typen: :values.',\n    'min'                  => [\n        'numeric' => ':attribute må være på minst :min.',\n        'file'    => ':attribute må være på minst :min kilobytes.',\n        'string'  => ':attribute må være på minst :min tegn.',\n        'array'   => ':attribute må minst ha :min ting.',\n    ],\n    'not_in'               => 'Den valgte :attribute er ugyldig.',\n    'not_regex'            => ':attribute format er ugyldig.',\n    'numeric'              => ':attribute må være et nummer.',\n    'regex'                => ':attribute format er ugyldig.',\n    'required'             => ':attribute feltet er påkrevt.',\n    'required_if'          => ':attribute feltet er påkrevt når :other er :value.',\n    'required_with'        => ':attribute feltet er påkrevt når :values er tilgjengelig.',\n    'required_with_all'    => ':attribute feltet er påkrevt når :values er tilgjengelig',\n    'required_without'     => ':attribute feltet er påkrevt når :values ikke er tilgjengelig.',\n    'required_without_all' => ':attribute feltet er påkrevt når ingen av :values er tilgjengelig.',\n    'same'                 => ':attribute og :other må samsvare.',\n    'safe_url'             => 'Den angitte lenken kan være farlig.',\n    'size'                 => [\n        'numeric' => ':attribute må være :size.',\n        'file'    => ':attribute må være :size kilobytes.',\n        'string'  => ':attribute må være :size tegn.',\n        'array'   => ':attribute må inneholde :size ting.',\n    ],\n    'string'               => ':attribute må være en tekststreng.',\n    'timezone'             => ':attribute må være en tidssone.',\n    'totp'                 => 'Den angitte koden er ikke gyldig eller har utløpt.',\n    'unique'               => ':attribute har allerede blitt tatt.',\n    'url'                  => ':attribute format er ugyldig.',\n    'uploaded'             => 'kunne ikke lastes opp, tjeneren støtter ikke filer av denne størrelsen.',\n\n    'zip_file' => 'The :attribute needs to reference a file within the ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',\n    'zip_model_expected' => 'Data object expected but \":type\" found.',\n    'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'passordbekreftelse er påkrevd',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/pl/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'utworzono stronę',\n    'page_create_notification'    => 'Strona została utworzona',\n    'page_update'                 => 'zaktualizowano stronę',\n    'page_update_notification'    => 'Strona zaktualizowana pomyślnie',\n    'page_delete'                 => 'usunięto stronę',\n    'page_delete_notification'    => 'Strona została usunięta',\n    'page_restore'                => 'przywrócono stronę',\n    'page_restore_notification'   => 'Strona przywrócona pomyślnie',\n    'page_move'                   => 'przeniesiono stronę',\n    'page_move_notification'      => 'Strona przeniesiona pomyślnie',\n\n    // Chapters\n    'chapter_create'              => 'utworzono rozdział',\n    'chapter_create_notification' => 'Rozdział utworzony pomyślnie',\n    'chapter_update'              => 'zaktualizowano rozdział',\n    'chapter_update_notification' => 'Rozdział zaktualizowany pomyślnie',\n    'chapter_delete'              => 'usunięto rozdział',\n    'chapter_delete_notification' => 'Rozdział usunięty pomyślnie',\n    'chapter_move'                => 'przeniesiono rozdział',\n    'chapter_move_notification' => 'Rozdział przeniesiony pomyślnie',\n\n    // Books\n    'book_create'                 => 'utworzono książkę',\n    'book_create_notification'    => 'Książka utworzona pomyślnie',\n    'book_create_from_chapter'              => 'przekonwertowano rozdział na książkę',\n    'book_create_from_chapter_notification' => 'Rozdział został pomyślnie skonwertowany do książki',\n    'book_update'                 => 'zaktualizowano książkę',\n    'book_update_notification'    => 'Książka zaktualizowana pomyślnie',\n    'book_delete'                 => 'usunięto książkę',\n    'book_delete_notification'    => 'Książka usunięta pomyślnie',\n    'book_sort'                   => 'posortowano książkę',\n    'book_sort_notification'      => 'Książka posortowana pomyślnie',\n\n    // Bookshelves\n    'bookshelf_create'            => 'utworzyono półkę',\n    'bookshelf_create_notification'    => 'Półka utworzona pomyślnie',\n    'bookshelf_create_from_book'    => 'przekonwertowano książkę na półkę',\n    'bookshelf_create_from_book_notification'    => 'Książka została pomyślnie skonwertowana na półkę',\n    'bookshelf_update'                 => 'zaktualizowano półkę',\n    'bookshelf_update_notification'    => 'Półka zaktualizowana pomyślnie',\n    'bookshelf_delete'                 => 'usunięto półkę',\n    'bookshelf_delete_notification'    => 'Półka usunięta pomyślnie',\n\n    // Revisions\n    'revision_restore' => 'przywrócono wersję',\n    'revision_delete' => 'usunięto wersję',\n    'revision_delete_notification' => 'Wersja usunięta pomyślnie',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" został dodany do Twoich ulubionych',\n    'favourite_remove_notification' => '\":name\" został usunięty z ulubionych',\n\n    // Watching\n    'watch_update_level_notification' => 'Ustawienia obserwowania pomyślnie zaktualizowane',\n\n    // Auth\n    'auth_login' => 'zalogował się',\n    'auth_register' => 'zarejestrowany jako nowy użytkownik',\n    'auth_password_reset_request' => 'zażądał zresetowania hasła użytkownika',\n    'auth_password_reset_update' => 'zresetował hasło użytkownika',\n    'mfa_setup_method' => 'skonfigurował metodę MFA',\n    'mfa_setup_method_notification' => 'Metoda wieloskładnikowa została pomyślnie skonfigurowana',\n    'mfa_remove_method' => 'usunął metodę MFA',\n    'mfa_remove_method_notification' => 'Metoda wieloskładnikowa pomyślnie usunięta',\n\n    // Settings\n    'settings_update' => 'zaktualizowano ustawienia',\n    'settings_update_notification' => 'Ustawienia zaktualizowane pomyślnie',\n    'maintenance_action_run' => 'uruchomiono akcję konserwacji',\n\n    // Webhooks\n    'webhook_create' => 'utworzono webhook',\n    'webhook_create_notification' => 'Webhook utworzony pomyślnie',\n    'webhook_update' => 'zaktualizowano webhook',\n    'webhook_update_notification' => 'Webhook zaktualizowany pomyślnie',\n    'webhook_delete' => 'usunięto webhook',\n    'webhook_delete_notification' => 'Webhook usunięty pomyślnie',\n\n    // Imports\n    'import_create' => 'utworzono import',\n    'import_create_notification' => 'Import zakończony sukcesem',\n    'import_run' => 'zaktualizowano import',\n    'import_run_notification' => 'Zawartość pomyślnie zaimportowana',\n    'import_delete' => 'usunięto import',\n    'import_delete_notification' => 'Import usunięty',\n\n    // Users\n    'user_create' => 'utworzono użytkownika',\n    'user_create_notification' => 'Użytkownik utworzony pomyślnie',\n    'user_update' => 'zaktualizowano użytkownika',\n    'user_update_notification' => 'Użytkownik zaktualizowany pomyślnie',\n    'user_delete' => 'usunięto użytkownika',\n    'user_delete_notification' => 'Użytkownik pomyślnie usunięty',\n\n    // API Tokens\n    'api_token_create' => 'utworzono token API',\n    'api_token_create_notification' => 'Token API został poprawnie utworzony',\n    'api_token_update' => 'zaktualizowano token API',\n    'api_token_update_notification' => 'Token API został pomyślnie zaktualizowany',\n    'api_token_delete' => 'usunięto token API',\n    'api_token_delete_notification' => 'Token API został pomyślnie usunięty',\n\n    // Roles\n    'role_create' => 'utworzono rolę',\n    'role_create_notification' => 'Rola utworzona pomyślnie',\n    'role_update' => 'zaktualizowano rolę',\n    'role_update_notification' => 'Rola zaktualizowana pomyślnie',\n    'role_delete' => 'usunięto rolę',\n    'role_delete_notification' => 'Rola usunięta pomyślnie',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'opróżniono kosz',\n    'recycle_bin_restore' => 'przywrócono z kosza',\n    'recycle_bin_destroy' => 'usunięto z kosza',\n\n    // Comments\n    'commented_on'                => 'skomentował',\n    'comment_create'              => 'dodał komentarz',\n    'comment_update'              => 'zaktualizował komentarz',\n    'comment_delete'              => 'usunął komentarz',\n\n    // Sort Rules\n    'sort_rule_create' => 'utworzono regułę sortowania',\n    'sort_rule_create_notification' => 'Reguła sortowania została pomyślnie stworzona',\n    'sort_rule_update' => 'zaktualizowano regułę sortowania',\n    'sort_rule_update_notification' => 'Reguła sortowania została pomyślnie zaktualizowana',\n    'sort_rule_delete' => 'usunięto regułę sortowania',\n    'sort_rule_delete_notification' => 'Reguła sortowania została pomyślnie usunięta',\n\n    // Other\n    'permissions_update'          => 'zaktualizował uprawnienia',\n];\n"
  },
  {
    "path": "lang/pl/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Wprowadzone poświadczenia są nieprawidłowe.',\n    'throttle' => 'Zbyt wiele prób logowania. Spróbuj ponownie za :seconds s.',\n\n    // Login & Register\n    'sign_up' => 'Zarejestruj się',\n    'log_in' => 'Zaloguj się',\n    'log_in_with' => 'Zaloguj się za pomocą :socialDriver',\n    'sign_up_with' => 'Zarejestruj się za pomocą :socialDriver',\n    'logout' => 'Wyloguj',\n\n    'name' => 'Imię',\n    'username' => 'Nazwa użytkownika',\n    'email' => 'E-mail',\n    'password' => 'Hasło',\n    'password_confirm' => 'Potwierdź hasło',\n    'password_hint' => 'Musi mieć co najmniej 8 znaków',\n    'forgot_password' => 'Zapomniałeś hasła?',\n    'remember_me' => 'Zapamiętaj mnie',\n    'ldap_email_hint' => 'Wprowadź adres e-mail dla tego konta.',\n    'create_account' => 'Utwórz konto',\n    'already_have_account' => 'Masz już konto?',\n    'dont_have_account' => 'Nie masz konta?',\n    'social_login' => 'Logowanie za pomocą konta społecznościowego',\n    'social_registration' => 'Rejestracja za pomocą konta społecznościowego',\n    'social_registration_text' => 'Zarejestruj się za pomocą innej usługi.',\n\n    'register_thanks' => 'Dziękujemy za rejestrację!',\n    'register_confirm' => 'Sprawdź podany adres e-mail i kliknij w link, by uzyskać dostęp do :appName.',\n    'registrations_disabled' => 'Rejestracja jest obecnie zablokowana.',\n    'registration_email_domain_invalid' => 'Adresy e-mail z tej domeny nie mają dostępu do tej aplikacji',\n    'register_success' => 'Dziękujemy za rejestrację! Zostałeś zalogowany automatycznie.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Próba logowania',\n    'auto_init_starting_desc' => 'Łączymy się z twoim systemem uwierzytelniania w celu rozpoczęcia procesu logowania. Jeśli po 5 sekundach nie ma żadnych postępów, możesz spróbować kliknąć poniższy link.',\n    'auto_init_start_link' => 'Kontynuuj uwierzytelnianie',\n\n    // Password Reset\n    'reset_password' => 'Resetowanie hasła',\n    'reset_password_send_instructions' => 'Wprowadź adres e-mail powiązany z Twoim kontem, by otrzymać link do resetowania hasła.',\n    'reset_password_send_button' => 'Wyślij link do resetowania hasła',\n    'reset_password_sent' => 'Link z resetem hasła zostanie wysłany na :email jeśli mamy ten adres w systemie.',\n    'reset_password_success' => 'Hasło zostało zresetowane pomyślnie.',\n    'email_reset_subject' => 'Resetowanie hasła do :appName',\n    'email_reset_text' => 'Otrzymujesz tę wiadomość ponieważ ktoś zażądał zresetowania hasła do Twojego konta.',\n    'email_reset_not_requested' => 'Jeśli to nie Ty złożyłeś żądanie zresetowania hasła, zignoruj tę wiadomość.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Potwierdź swój adres e-mail w :appName',\n    'email_confirm_greeting' => 'Dziękujemy za dołączenie do :appName!',\n    'email_confirm_text' => 'Prosimy byś potwierdził swoje hasło klikając przycisk poniżej:',\n    'email_confirm_action' => 'Potwierdź e-mail',\n    'email_confirm_send_error' => 'Wymagane jest potwierdzenie hasła, lecz wiadomość nie mogła zostać wysłana. Skontaktuj się z administratorem w celu upewnienia się, że skrzynka została skonfigurowana prawidłowo.',\n    'email_confirm_success' => 'Twój e-mail został potwierdzony! Powinieneś teraz mieć możliwość zalogowania się za pomocą tego adresu e-mail.',\n    'email_confirm_resent' => 'E-mail z potwierdzeniem został wysłany ponownie, sprawdź swoją skrzynkę odbiorczą.',\n    'email_confirm_thanks' => 'Dzięki za potwierdzenie!',\n    'email_confirm_thanks_desc' => 'Poczekaj chwilę, Twoje potwierdzenie jest obsługiwane. Jeśli nie zostaniesz przekierowany po 3 sekundach, naciśnij poniższy link \"Kontynuuj\", aby kontynuować.',\n\n    'email_not_confirmed' => 'Adres e-mail nie został potwierdzony',\n    'email_not_confirmed_text' => 'Twój adres e-mail nie został jeszcze potwierdzony.',\n    'email_not_confirmed_click_link' => 'Aby potwierdzić swoje konto, kliknij link wysłany w wiadomości po rejestracji.',\n    'email_not_confirmed_resend' => 'Jeśli wiadomość do Ciebie nie dotarła, możesz wysłać ją ponownie, wypełniając formularz poniżej.',\n    'email_not_confirmed_resend_button' => 'Wyślij ponownie wiadomość z potwierdzeniem',\n\n    // User Invite\n    'user_invite_email_subject' => 'Zostałeś zaproszony do :appName!',\n    'user_invite_email_greeting' => 'Zostało dla Ciebie utworzone konto w :appName.',\n    'user_invite_email_text' => 'Kliknij przycisk poniżej, aby ustawić hasło do konta i uzyskać do niego dostęp:',\n    'user_invite_email_action' => 'Ustaw hasło do konta',\n    'user_invite_page_welcome' => 'Witaj w :appName!',\n    'user_invite_page_text' => 'Aby zakończyć tworzenie konta musisz ustawić hasło, które będzie używane do logowania do :appName w przyszłości.',\n    'user_invite_page_confirm_button' => 'Potwierdź hasło',\n    'user_invite_success_login' => 'Hasło ustawione, teraz powinieneś mieć możliwość logowania się przy użyciu ustawionego hasła, aby uzyskać dostęp do :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Skonfiguruj uwierzytelnianie wieloskładnikowe',\n    'mfa_setup_desc' => 'Skonfiguruj uwierzytelnianie wieloskładnikowe jako dodatkową warstwę bezpieczeństwa dla swojego konta użytkownika.',\n    'mfa_setup_configured' => 'Już skonfigurowane',\n    'mfa_setup_reconfigure' => 'Ponownie konfiguruj',\n    'mfa_setup_remove_confirmation' => 'Czy na pewno chcesz usunąć tę metodę uwierzytelniania wieloskładnikowego?',\n    'mfa_setup_action' => 'Konfiguracja',\n    'mfa_backup_codes_usage_limit_warning' => 'Pozostało Ci mniej niż 5 kodów zapasowych, Wygeneruj i przechowuj nowy zestaw zanim skończysz kody, aby zapobiec zablokowaniu się z konta.',\n    'mfa_option_totp_title' => 'Aplikacja mobilna',\n    'mfa_option_totp_desc' => 'Aby korzystać z uwierzytelniania wieloskładnikowego, potrzebujesz aplikacji mobilnej, która obsługuje TOTP, takiej jak Google Authenticator, Authy lub Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Kody zapasowe',\n    'mfa_option_backup_codes_desc' => 'Generuje zestaw jednorazowych kodów zapasowych, które wprowadzisz przy logowaniu, aby zweryfikować Twoją tożsamość. Upewnij się, że przechowujesz je w bezpiecznym miejscu.',\n    'mfa_gen_confirm_and_enable' => 'Potwierdź i włącz',\n    'mfa_gen_backup_codes_title' => 'Ustawienia kopii zapasowych kodów',\n    'mfa_gen_backup_codes_desc' => 'Przechowuj poniższą listę kodów w bezpiecznym miejscu. Przy dostępie do systemu będziesz mógł użyć jednego z kodów jako drugiego mechanizmu uwierzytelniania.',\n    'mfa_gen_backup_codes_download' => 'Pobierz kody',\n    'mfa_gen_backup_codes_usage_warning' => 'Każdy kod może być użyty tylko raz',\n    'mfa_gen_totp_title' => 'Ustawienia aplikacji mobilnej',\n    'mfa_gen_totp_desc' => 'Aby korzystać z uwierzytelniania wieloskładnikowego, potrzebujesz aplikacji mobilnej, która obsługuje TOTP, takiej jak Google Authenticator, Authy lub Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Zeskanuj poniższy kod QR za pomocą preferowanej aplikacji uwierzytelniającej, aby rozpocząć.',\n    'mfa_gen_totp_verify_setup' => 'Sprawdź ustawienia',\n    'mfa_gen_totp_verify_setup_desc' => 'Sprawdź, czy wszystko działa wprowadzając kod wygenerowany w twojej aplikacji uwierzytelniającej, w poniższym polu:',\n    'mfa_gen_totp_provide_code_here' => 'Tutaj podaj kod wygenerowany przez aplikację',\n    'mfa_verify_access' => 'Sprawdź dostęp',\n    'mfa_verify_access_desc' => 'Twoje konto wymaga potwierdzenia tożsamości poprzez dodatkowy poziom weryfikacji, zanim uzyskasz dostęp. Zweryfikuj za pomocą jednej z skonfigurowanych metod, aby kontynuować.',\n    'mfa_verify_no_methods' => 'Brak skonfigurowanych metod',\n    'mfa_verify_no_methods_desc' => 'Nie można znaleźć metod uwierzytelniania wieloskładnikowego. Musisz skonfigurować co najmniej jedną metodę zanim uzyskasz dostęp.',\n    'mfa_verify_use_totp' => 'Zweryfikuj używając aplikacji mobilnej',\n    'mfa_verify_use_backup_codes' => 'Zweryfikuj używając kodu zapasowego',\n    'mfa_verify_backup_code' => 'Kod zapasowy',\n    'mfa_verify_backup_code_desc' => 'Wprowadź poniżej jeden z pozostałych kodów zapasowych:',\n    'mfa_verify_backup_code_enter_here' => 'Wprowadź kod zapasowy tutaj',\n    'mfa_verify_totp_desc' => 'Wprowadź kod, wygenerowany przy użyciu aplikacji mobilnej poniżej:',\n    'mfa_setup_login_notification' => 'Metoda wieloskładnikowa skonfigurowana, zaloguj się ponownie za pomocą skonfigurowanej metody.',\n];\n"
  },
  {
    "path": "lang/pl/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Anuluj',\n    'close' => 'Zamknij',\n    'confirm' => 'Zatwierdź',\n    'back' => 'Wstecz',\n    'save' => 'Zapisz',\n    'continue' => 'Kontynuuj',\n    'select' => 'Wybierz',\n    'toggle_all' => 'Przełącz wszystko',\n    'more' => 'Więcej',\n\n    // Form Labels\n    'name' => 'Nazwa',\n    'description' => 'Opis',\n    'role' => 'Rola',\n    'cover_image' => 'Okładka',\n    'cover_image_description' => 'Ten obraz powinien być o rozmiarze około 440x250px, chociaż zostanie elastycznie przeskalowany i przycięty, aby dopasować interfejs użytkownika do różnych scenariuszy w zależności od potrzeb, więc faktyczne wymiary wyświetlania będą się różnić.',\n\n    // Actions\n    'actions' => 'Akcje',\n    'view' => 'Widok',\n    'view_all' => 'Zobacz wszystkie',\n    'new' => 'Nowe',\n    'create' => 'Utwórz',\n    'update' => 'Zaktualizuj',\n    'edit' => 'Edytuj',\n    'archive' => 'Archiwizuj',\n    'unarchive' => 'Wypakuj z archiwum',\n    'sort' => 'Sortuj',\n    'move' => 'Przenieś',\n    'copy' => 'Skopiuj',\n    'reply' => 'Odpowiedz',\n    'delete' => 'Usuń',\n    'delete_confirm' => 'Potwierdź usunięcie',\n    'search' => 'Szukaj',\n    'search_clear' => 'Wyczyść wyszukiwanie',\n    'reset' => 'Resetuj',\n    'remove' => 'Usuń',\n    'add' => 'Dodaj',\n    'configure' => 'Konfiguruj',\n    'manage' => 'Zarządzaj',\n    'fullscreen' => 'Pełny ekran',\n    'favourite' => 'Ulubione',\n    'unfavourite' => 'Usuń z ulubionych',\n    'next' => 'Dalej',\n    'previous' => 'Wstecz',\n    'filter_active' => 'Aktywny filtr:',\n    'filter_clear' => 'Wyczyść Filtr',\n    'download' => 'Pobierz',\n    'open_in_tab' => 'Otwórz w karcie',\n    'open' => 'Otwórz',\n\n    // Sort Options\n    'sort_options' => 'Opcje sortowania',\n    'sort_direction_toggle' => 'Przełącz kierunek sortowania',\n    'sort_ascending' => 'Sortuj rosnąco',\n    'sort_descending' => 'Sortuj malejąco',\n    'sort_name' => 'Nazwa',\n    'sort_default' => 'Domyślne',\n    'sort_created_at' => 'Data utworzenia',\n    'sort_updated_at' => 'Data aktualizacji',\n\n    // Misc\n    'deleted_user' => 'Użytkownik usunięty',\n    'no_activity' => 'Brak aktywności do wyświetlenia',\n    'no_items' => 'Brak elementów do wyświetlenia',\n    'back_to_top' => 'Powrót na górę',\n    'skip_to_main_content' => 'Przejdź do treści głównej',\n    'toggle_details' => 'Włącz/wyłącz szczegóły',\n    'toggle_thumbnails' => 'Włącz/wyłącz miniatury',\n    'details' => 'Szczegóły',\n    'grid_view' => 'Widok kafelkowy',\n    'list_view' => 'Widok listy',\n    'default' => 'Domyślny',\n    'breadcrumb' => 'Ścieżka nawigacji',\n    'status' => 'Status',\n    'status_active' => 'Aktywny',\n    'status_inactive' => 'Nieaktywny',\n    'never' => 'Nigdy',\n    'none' => 'Brak',\n\n    // Header\n    'homepage' => 'Strona domowa',\n    'header_menu_expand' => 'Rozwiń menu nagłówka',\n    'profile_menu' => 'Menu profilu',\n    'view_profile' => 'Zobacz profil',\n    'edit_profile' => 'Edytuj profil',\n    'dark_mode' => 'Tryb ciemny',\n    'light_mode' => 'Tryb jasny',\n    'global_search' => 'Wyszukiwanie globalne',\n\n    // Layout tabs\n    'tab_info' => 'Informacje',\n    'tab_info_label' => 'Zakładka: Pokaż informacje drugorzędne',\n    'tab_content' => 'Treść',\n    'tab_content_label' => 'Zakładka: Pokaż podstawową zawartość',\n\n    // Email Content\n    'email_action_help' => 'Jeśli masz problem z kliknięciem przycisku \":actionText\", skopiuj i wklej poniższy adres URL w nowej karcie swojej przeglądarki:',\n    'email_rights' => 'Wszelkie prawa zastrzeżone',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Polityka prywatności',\n    'terms_of_service' => 'Warunki usługi',\n\n    // OpenSearch\n    'opensearch_description' => 'Szukaj :appName',\n];\n"
  },
  {
    "path": "lang/pl/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Wybór obrazka',\n    'image_list' => 'Lista obrazów',\n    'image_details' => 'Szczegóły obrazu',\n    'image_upload' => 'Prześlij obraz',\n    'image_intro' => 'Tutaj możesz wybrać i zarządzać obrazami, które zostały wcześniej przesłane do systemu.',\n    'image_intro_upload' => 'Prześlij nowy obraz przeciągając plik obrazu do tego okna lub używając przycisku \"Prześlij obraz\" powyżej.',\n    'image_all' => 'Wszystkie',\n    'image_all_title' => 'Zobacz wszystkie obrazki',\n    'image_book_title' => 'Zobacz obrazki zapisane w tej książce',\n    'image_page_title' => 'Zobacz obrazki zapisane na tej stronie',\n    'image_search_hint' => 'Szukaj po nazwie obrazka',\n    'image_uploaded' => 'Przesłano :uploadedDate',\n    'image_uploaded_by' => 'Przesłane przez :userName',\n    'image_uploaded_to' => 'Przesłano do :pageLink',\n    'image_updated' => 'Zaktualizowano :updateDate',\n    'image_load_more' => 'Wczytaj więcej',\n    'image_image_name' => 'Nazwa obrazka',\n    'image_delete_used' => 'Ten obrazek jest używany na stronach wyświetlonych poniżej.',\n    'image_delete_confirm_text' => 'Czy na pewno chcesz usunąć ten obraz?',\n    'image_select_image' => 'Wybierz obrazek',\n    'image_dropzone' => 'Upuść obrazki tutaj lub kliknij by wybrać obrazki do przesłania',\n    'image_dropzone_drop' => 'Upuść obrazy tutaj, aby przesłać',\n    'images_deleted' => 'Usunięte obrazki',\n    'image_preview' => 'Podgląd obrazka',\n    'image_upload_success' => 'Obrazek przesłany pomyślnie',\n    'image_update_success' => 'Szczegóły obrazka zaktualizowane pomyślnie',\n    'image_delete_success' => 'Obrazek usunięty pomyślnie',\n    'image_replace' => 'Zastąp obraz',\n    'image_replace_success' => 'Plik obrazu zaktualizowany pomyślnie',\n    'image_rebuild_thumbs' => 'Zregeneruj warianty rozmiaru',\n    'image_rebuild_thumbs_success' => 'Warianty rozmiaru obrazu przebudowane pomyślnie!',\n\n    // Code Editor\n    'code_editor' => 'Edytuj kod',\n    'code_language' => 'Język kodu',\n    'code_content' => 'Zawartość kodu',\n    'code_session_history' => 'Historia sesji',\n    'code_save' => 'Zapisz kod',\n];\n"
  },
  {
    "path": "lang/pl/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Ogólne',\n    'advanced' => 'Zaawansowane',\n    'none' => 'Brak',\n    'cancel' => 'Anuluj',\n    'save' => 'Zapisz',\n    'close' => 'Zamknij',\n    'apply' => 'Zatwierdź',\n    'undo' => 'Cofnij',\n    'redo' => 'Ponów',\n    'left' => 'Lewa strona',\n    'center' => 'Centrum',\n    'right' => 'Prawa strona',\n    'top' => 'Góra',\n    'middle' => 'Środek',\n    'bottom' => 'Dół',\n    'width' => 'Szerokość',\n    'height' => 'Wysokość',\n    'More' => 'Więcej',\n    'select' => 'Wybierz...',\n\n    // Toolbar\n    'formats' => 'Formaty',\n    'header_large' => 'Duży Nagłówek',\n    'header_medium' => 'Średni Nagłówek',\n    'header_small' => 'Mały Nagłówek',\n    'header_tiny' => 'Bardzo Mały Nagłówek',\n    'paragraph' => 'Paragraf',\n    'blockquote' => 'Cytat',\n    'inline_code' => 'Kod źródłowy',\n    'callouts' => 'Objaśnienia',\n    'callout_information' => 'Informacja',\n    'callout_success' => 'Sukces',\n    'callout_warning' => 'Ostrzeżenie',\n    'callout_danger' => 'Niebezpieczeństwo',\n    'bold' => 'Pogrubienie',\n    'italic' => 'Kursywa',\n    'underline' => 'Podkreślenie',\n    'strikethrough' => 'Przekreślenie',\n    'superscript' => 'Indeks górny',\n    'subscript' => 'Indeks dolny',\n    'text_color' => 'Kolor tekstu',\n    'highlight_color' => 'Kolor podkreślenia',\n    'custom_color' => 'Kolor niestandardowy',\n    'remove_color' => 'Usuń kolor',\n    'background_color' => 'Kolor tła',\n    'align_left' => 'Wyrównaj do lewej',\n    'align_center' => 'Wyśrodkuj',\n    'align_right' => 'Wyrównaj do prawej',\n    'align_justify' => 'Wyjustuj',\n    'list_bullet' => 'Lista punktowana',\n    'list_numbered' => 'Lista numerowana',\n    'list_task' => 'Lista zadań',\n    'indent_increase' => 'Zwiększ wcięcie',\n    'indent_decrease' => 'Zmniejsz wcięcie',\n    'table' => 'Tabela',\n    'insert_image' => 'Wstaw obraz',\n    'insert_image_title' => 'Wstaw/Edytuj obraz',\n    'insert_link' => 'Wstaw/edytuj link',\n    'insert_link_title' => 'Wstaw/Edytuj Link',\n    'insert_horizontal_line' => 'Wstaw linię poziomą',\n    'insert_code_block' => 'Wstaw blok kodu',\n    'edit_code_block' => 'Edytuj blok kodu',\n    'insert_drawing' => 'Wstaw/Edytuj rysunek',\n    'drawing_manager' => 'Menedżer rysunków',\n    'insert_media' => 'Wstaw/edytuj multimedia',\n    'insert_media_title' => 'Wstaw/Edytuj Multimedia',\n    'clear_formatting' => 'Wyczyść formatowanie',\n    'source_code' => 'Kod źródłowy',\n    'source_code_title' => 'Kod Źródłowy',\n    'fullscreen' => 'Pełny ekran',\n    'image_options' => 'Opcje obrazu',\n\n    // Tables\n    'table_properties' => 'Właściwości tabeli',\n    'table_properties_title' => 'Właściwości Tabeli',\n    'delete_table' => 'Usuń tabelę',\n    'table_clear_formatting' => 'Wyczyść formatowanie tabeli',\n    'resize_to_contents' => 'Dostosuj rozmiar do zawartości',\n    'row_header' => 'Wiersz nagłówka',\n    'insert_row_before' => 'Wstaw wiersz przed',\n    'insert_row_after' => 'Wstaw wiersz za',\n    'delete_row' => 'Usuń wiersz',\n    'insert_column_before' => 'Wstaw kolumnę przed',\n    'insert_column_after' => 'Wstaw kolumnę za',\n    'delete_column' => 'Usuń kolumnę',\n    'table_cell' => 'Komórka',\n    'table_row' => 'Wiersz',\n    'table_column' => 'Kolumna',\n    'cell_properties' => 'Właściwości komórki',\n    'cell_properties_title' => 'Właściwości Komórki',\n    'cell_type' => 'Typ komórki',\n    'cell_type_cell' => 'Komórka',\n    'cell_scope' => 'Zakres',\n    'cell_type_header' => 'Komórka nagłówka',\n    'merge_cells' => 'Scal komórki',\n    'split_cell' => 'Podziel komórkę',\n    'table_row_group' => 'Grupa wierszy',\n    'table_column_group' => 'Grupa kolumn',\n    'horizontal_align' => 'Wyrównanie w poziomie',\n    'vertical_align' => 'Wyrównanie w pionie',\n    'border_width' => 'Szerokość obramowania',\n    'border_style' => 'Styl obramowania',\n    'border_color' => 'Kolor obramowania',\n    'row_properties' => 'Właściwości wiersza',\n    'row_properties_title' => 'Właściwości Wiersza',\n    'cut_row' => 'Wytnij wiersz',\n    'copy_row' => 'Kopiuj wiersz',\n    'paste_row_before' => 'Wklej wiersz przed',\n    'paste_row_after' => 'Wklej wiersz za',\n    'row_type' => 'Typ wiersza',\n    'row_type_header' => 'Nagłówek',\n    'row_type_body' => 'Ciało',\n    'row_type_footer' => 'Stopka',\n    'alignment' => 'Wyrównanie',\n    'cut_column' => 'Wytnij kolumnę',\n    'copy_column' => 'Kopiuj kolumnę',\n    'paste_column_before' => 'Wklej kolumnę przed',\n    'paste_column_after' => 'Wklej kolumnę za',\n    'cell_padding' => 'Wypełnienie komórki',\n    'cell_spacing' => 'Odstępy między komórkami',\n    'caption' => 'Opis',\n    'show_caption' => 'Pokaż opis',\n    'constrain' => 'Zachowaj proporcje',\n    'cell_border_solid' => 'Ciągły',\n    'cell_border_dotted' => 'Kropkowany',\n    'cell_border_dashed' => 'Kreskowany',\n    'cell_border_double' => 'Podwójny',\n    'cell_border_groove' => 'Rowek',\n    'cell_border_ridge' => 'Grzbiet',\n    'cell_border_inset' => 'Ramka',\n    'cell_border_outset' => 'Wypukły',\n    'cell_border_none' => 'Brak',\n    'cell_border_hidden' => 'Ukryty',\n\n    // Images, links, details/summary & embed\n    'source' => 'Źródło',\n    'alt_desc' => 'Alternatywny opis',\n    'embed' => 'Osadź',\n    'paste_embed' => 'Wklej kod osadzenia poniżej:',\n    'url' => 'Adres URL',\n    'text_to_display' => 'Tekst do wyświetlenia',\n    'title' => 'Tytuł',\n    'browse_links' => 'Przeglądaj linki',\n    'open_link' => 'Otwórz link',\n    'open_link_in' => 'Otwórz link w...',\n    'open_link_current' => 'Bieżące okno',\n    'open_link_new' => 'Nowe okno',\n    'remove_link' => 'Usuń link',\n    'insert_collapsible' => 'Wstaw zwijalny blok',\n    'collapsible_unwrap' => 'Rozwiń',\n    'edit_label' => 'Edytuj etykietę',\n    'toggle_open_closed' => 'Otwórz/zamknij',\n    'collapsible_edit' => 'Edytuj zwijalny blok',\n    'toggle_label' => 'Przełącz etykietę',\n\n    // About view\n    'about' => 'O edytorze',\n    'about_title' => 'O edytorze WYSIWYG',\n    'editor_license' => 'Licencja edytora i prawa autorskie',\n    'editor_lexical_license' => 'Ten edytor został zbudowany na podstawie :lexicalLink, który jest dystrybuowany na licencji MIT.',\n    'editor_lexical_license_link' => 'Pełne szczegóły licencji znajdziesz tutaj.',\n    'editor_tiny_license' => 'Ten edytor jest zbudowany przy użyciu :tinyLink, który jest udostępniany na licencji MIT.',\n    'editor_tiny_license_link' => 'Szczegóły dotyczące praw autorskich i licencji TinyMCE można znaleźć tutaj.',\n    'save_continue' => 'Zapisz stronę i kontynuuj',\n    'callouts_cycle' => '(Naciskaj dalej, aby przełączyć przez typy)',\n    'link_selector' => 'Link do treści',\n    'shortcuts' => 'Skróty',\n    'shortcut' => 'Skrót',\n    'shortcuts_intro' => 'Następujące skróty są dostępne w edytorze:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Opis',\n];\n"
  },
  {
    "path": "lang/pl/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Ostatnio utworzone',\n    'recently_created_pages' => 'Ostatnio utworzone strony',\n    'recently_updated_pages' => 'Ostatnio zaktualizowane strony',\n    'recently_created_chapters' => 'Ostatnio utworzone rozdziały',\n    'recently_created_books' => 'Ostatnio utworzone książki',\n    'recently_created_shelves' => 'Ostatnio utworzone półki',\n    'recently_update' => 'Ostatnio zaktualizowane',\n    'recently_viewed' => 'Ostatnio wyświetlane',\n    'recent_activity' => 'Ostatnia aktywność',\n    'create_now' => 'Utwórz teraz',\n    'revisions' => 'Wersje',\n    'meta_revision' => 'Wersja #:revisionCount',\n    'meta_created' => 'Utworzono :timeLength',\n    'meta_created_name' => 'Utworzono :timeLength przez :user',\n    'meta_updated' => 'Zaktualizowano :timeLength',\n    'meta_updated_name' => 'Zaktualizowano :timeLength przez :user',\n    'meta_owned_name' => 'Właściciel: :user',\n    'meta_reference_count' => 'Odniesienie w :count elemencie|Odniesienia w :count elementach',\n    'entity_select' => 'Wybór obiektu',\n    'entity_select_lack_permission' => 'Nie masz wymaganych uprawnień do wybrania tej pozycji',\n    'images' => 'Obrazki',\n    'my_recent_drafts' => 'Moje ostatnie wersje robocze',\n    'my_recently_viewed' => 'Moje ostatnio wyświetlane',\n    'my_most_viewed_favourites' => 'Moje najczęściej przeglądane ulubione',\n    'my_favourites' => 'Moje ulubione',\n    'no_pages_viewed' => 'Nie przeglądałeś jeszcze żadnych stron',\n    'no_pages_recently_created' => 'Nie utworzono ostatnio żadnych stron',\n    'no_pages_recently_updated' => 'Nie zaktualizowano ostatnio żadnych stron',\n    'export' => 'Eksportuj',\n    'export_html' => 'Plik HTML',\n    'export_pdf' => 'Plik PDF',\n    'export_text' => 'Plik tekstowy',\n    'export_md' => 'Pliki Markdown',\n    'export_zip' => 'Archiwum ZIP',\n    'default_template' => 'Domyślny szablon strony',\n    'default_template_explain' => 'Przypisz szablon strony, który będzie używany jako domyślna zawartość dla wszystkich stron utworzonych w tym elemencie. Pamiętaj, że będzie to używane tylko wtedy, gdy twórca strony ma dostęp do wybranej strony szablonu.',\n    'default_template_select' => 'Wybierz stronę szablonu',\n    'import' => 'Importuj',\n    'import_validate' => 'Zweryfikuj import',\n    'import_desc' => 'Importuj książki, rozdziały i strony za pomocą eksportu archiwum ZIP z tej samej lub innej instancji. Wybierz plik ZIP, aby kontynuować. Po przesłaniu i potwierdzeniu pliku będziesz mógł skonfigurować i potwierdzić import w następnym kroku.',\n    'import_zip_select' => 'Wybierz archiwum ZIP do wgrania',\n    'import_zip_validation_errors' => 'Podczas sprawdzania poprawności dostarczonego pliku ZIP wykryto błędy:',\n    'import_pending' => 'Oczekujące importy',\n    'import_pending_none' => 'Żaden import nie został uruchomiony.',\n    'import_continue' => 'Kontynuuj import',\n    'import_continue_desc' => 'Przejrzyj zawartość, która ma być zaimportowana z przesłanego pliku ZIP. Kiedy będziesz gotowy, uruchom import, aby dodać jego zawartość do systemu. Przesłane archiwum ZIP zostanie automatycznie usunięte po udanym importowaniu.',\n    'import_details' => 'Szczegóły importu',\n    'import_run' => 'Wykonaj import',\n    'import_size' => ':size wielkość importu ZIP',\n    'import_uploaded_at' => 'Przesłano :relativeTime',\n    'import_uploaded_by' => 'Przesłane przez',\n    'import_location' => 'Lokalizacja importu',\n    'import_location_desc' => 'Wybierz docelową lokalizację dla importowanej zawartości. Będziesz potrzebować odpowiednich uprawnień do tworzenia w wybranej lokalizacji.',\n    'import_delete_confirm' => 'Czy na pewno chcesz usunąć ten import?',\n    'import_delete_desc' => 'Spowoduje to usunięcie zaimportowanego archiwum ZIP. Tej operacji nie da się cofnąć.',\n    'import_errors' => 'Błędy importu',\n    'import_errors_desc' => 'Podczas próby importu wystąpiły następujące błędy:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Uprawnienia',\n    'permissions_desc' => 'Ustaw uprawnienia tutaj, aby nadpisać domyślne uprawnienia przyznane przez role użytkownika.',\n    'permissions_book_cascade' => 'Uprawnienia ustawione na książkach będą automatycznie nakładane na podrzędne rozdziały i strony, chyba że mają one ustawione własne uprawnienia.',\n    'permissions_chapter_cascade' => 'Uprawnienia ustawione w rozdziałach będą automatycznie nakładane na podrzędne strony, chyba że mają one ustawione własne uprawnienia.',\n    'permissions_save' => 'Zapisz uprawnienia',\n    'permissions_owner' => 'Właściciel',\n    'permissions_role_everyone_else' => 'Wszyscy inni',\n    'permissions_role_everyone_else_desc' => 'Ustaw uprawnienia dla wszystkich nienadpisywanych specjalnie ról.',\n    'permissions_role_override' => 'Nadpisz uprawnienia dla roli',\n    'permissions_inherit_defaults' => 'Dziedzicz wartości domyślne',\n\n    // Search\n    'search_results' => 'Wyniki wyszukiwania',\n    'search_total_results_found' => ':count znalezionych wyników|:count ogółem znalezionych wyników',\n    'search_clear' => 'Wyczyść wyszukiwanie',\n    'search_no_pages' => 'Brak stron pasujących do tego wyszukiwania',\n    'search_for_term' => 'Szukaj :term',\n    'search_more' => 'Więcej wyników',\n    'search_advanced' => 'Wyszukiwanie zaawansowane',\n    'search_terms' => 'Szukane frazy',\n    'search_content_type' => 'Rodzaj treści',\n    'search_exact_matches' => 'Dokładne dopasowanie',\n    'search_tags' => 'Tagi wyszukiwania',\n    'search_options' => 'Opcje',\n    'search_viewed_by_me' => 'Wyświetlone przeze mnie',\n    'search_not_viewed_by_me' => 'Niewyświetlone przeze mnie',\n    'search_permissions_set' => 'Zbiór uprawnień',\n    'search_created_by_me' => 'Utworzone przeze mnie',\n    'search_updated_by_me' => 'Zaktualizowane przeze mnie',\n    'search_owned_by_me' => 'Należące do mnie',\n    'search_date_options' => 'Opcje daty',\n    'search_updated_before' => 'Zaktualizowane przed',\n    'search_updated_after' => 'Zaktualizowane po',\n    'search_created_before' => 'Utworzone przed',\n    'search_created_after' => 'Utworzone po',\n    'search_set_date' => 'Ustaw datę',\n    'search_update' => 'Zaktualizuj wyszukiwanie',\n\n    // Shelves\n    'shelf' => 'Półka',\n    'shelves' => 'Półki',\n    'x_shelves' => ':count Półek|:count Półek',\n    'shelves_empty' => 'Brak utworzonych półek',\n    'shelves_create' => 'Utwórz półkę',\n    'shelves_popular' => 'Popularne półki',\n    'shelves_new' => 'Nowe półki',\n    'shelves_new_action' => 'Nowa półka',\n    'shelves_popular_empty' => 'Najpopularniejsze półki pojawią się w tym miejscu.',\n    'shelves_new_empty' => 'Tutaj pojawią się ostatnio utworzone półki.',\n    'shelves_save' => 'Zapisz półkę',\n    'shelves_books' => 'Książki na tej półce',\n    'shelves_add_books' => 'Dodaj książkę do tej półki',\n    'shelves_drag_books' => 'Przeciągnij książki poniżej, aby dodać je do tej półki',\n    'shelves_empty_contents' => 'Ta półka nie ma przypisanych żadnych książek',\n    'shelves_edit_and_assign' => 'Edytuj półkę aby przypisać książki',\n    'shelves_edit_named' => 'Edytuj półkę :name',\n    'shelves_edit' => 'Edytuj półkę',\n    'shelves_delete' => 'Usuń półkę',\n    'shelves_delete_named' => 'Usuń półkę :name',\n    'shelves_delete_explain' => \"Ta operacja usunie półkę o nazwie ':name'. Książki z tej półki nie zostaną usunięte.\",\n    'shelves_delete_confirmation' => 'Czy jesteś pewien, że chcesz usunąć tę półkę?',\n    'shelves_permissions' => 'Uprawnienia półki',\n    'shelves_permissions_updated' => 'Uprawnienia półki zostały zaktualizowane',\n    'shelves_permissions_active' => 'Uprawnienia półki są aktywne',\n    'shelves_permissions_cascade_warning' => 'Uprawnienia na półkach nie są automatycznie nakładane na zawartych w nich książkach. Dzieje się tak dlatego, że książka może istnieć na wielu półkach. Uprawnienia można jednak skopiować do książek podrzędnych, korzystając z opcji znajdującej się poniżej.',\n    'shelves_permissions_create' => 'Uprawnienia tworzenia półki są używane tylko do kopiowania uprawnień do książek podrzędnych za pomocą poniższej czynności. Nie kontrolują możliwości tworzenia książek.',\n    'shelves_copy_permissions_to_books' => 'Skopiuj uprawnienia do książek',\n    'shelves_copy_permissions' => 'Skopiuj uprawnienia',\n    'shelves_copy_permissions_explain' => 'To spowoduje zastosowanie obecnych ustawień uprawnień tej półki na wszystkich książkach w niej zawartych. Przed aktywacją upewnij się, że wszelkie zmiany w uprawnieniach tej półki zostały zapisane.',\n    'shelves_copy_permission_success' => 'Uprawnienia półki zostały skopiowane do :count książek',\n\n    // Books\n    'book' => 'Książka',\n    'books' => 'Książki',\n    'x_books' => ':count Książka|:count Książki',\n    'books_empty' => 'Brak utworzonych książek',\n    'books_popular' => 'Popularne książki',\n    'books_recent' => 'Ostatnie książki',\n    'books_new' => 'Nowe książki',\n    'books_new_action' => 'Nowa księga',\n    'books_popular_empty' => 'Najpopularniejsze książki pojawią się w tym miejscu.',\n    'books_new_empty' => 'Tutaj pojawią się ostatnio utworzone książki.',\n    'books_create' => 'Utwórz książkę',\n    'books_delete' => 'Usuń książkę',\n    'books_delete_named' => 'Usuń książkę :bookName',\n    'books_delete_explain' => 'To spowoduje usunięcie książki \\':bookName\\', Wszystkie strony i rozdziały zostaną usunięte.',\n    'books_delete_confirmation' => 'Czy na pewno chcesz usunąc tę książkę?',\n    'books_edit' => 'Edytuj książkę',\n    'books_edit_named' => 'Edytuj książkę :bookName',\n    'books_form_book_name' => 'Nazwa książki',\n    'books_save' => 'Zapisz książkę',\n    'books_permissions' => 'Uprawnienia książki',\n    'books_permissions_updated' => 'Zaktualizowano uprawnienia książki',\n    'books_empty_contents' => 'Brak stron lub rozdziałów w tej książce.',\n    'books_empty_create_page' => 'Utwórz nową stronę',\n    'books_empty_sort_current_book' => 'posortuj bieżącą książkę',\n    'books_empty_add_chapter' => 'Dodaj rozdział',\n    'books_permissions_active' => 'Uprawnienia książki są aktywne',\n    'books_search_this' => 'Wyszukaj w tej książce',\n    'books_navigation' => 'Nawigacja po książce',\n    'books_sort' => 'Sortuj zawartość książki',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Opcja automatycznego sortowania',\n    'books_sort_auto_sort_active' => 'Automatyczne sortowanie aktywne: :sortName',\n    'books_sort_named' => 'Sortuj książkę :bookName',\n    'books_sort_name' => 'Sortuj według nazwy',\n    'books_sort_created' => 'Sortuj według daty utworzenia',\n    'books_sort_updated' => 'Sortuj według daty modyfikacji',\n    'books_sort_chapters_first' => 'Rozdziały na początku',\n    'books_sort_chapters_last' => 'Rozdziały na końcu',\n    'books_sort_show_other' => 'Pokaż inne książki',\n    'books_sort_save' => 'Zapisz nową kolejność',\n    'books_sort_show_other_desc' => 'Dodaj tutaj inne książki, aby uwzględnić je w operacji sortowania i umożliwić łatwą wieloksiążkową reorganizację.',\n    'books_sort_move_up' => 'Przesuń w górę',\n    'books_sort_move_down' => 'Przesuń w dół',\n    'books_sort_move_prev_book' => 'Przenieś do poprzedniej książki',\n    'books_sort_move_next_book' => 'Przenieś do następnej książki',\n    'books_sort_move_prev_chapter' => 'Przenieś do poprzedniego rozdziału',\n    'books_sort_move_next_chapter' => 'Przenieś do następnego rozdziału',\n    'books_sort_move_book_start' => 'Przenieś na początek książki',\n    'books_sort_move_book_end' => 'Przenieś na koniec książki',\n    'books_sort_move_before_chapter' => 'Przenieś przed rozdział',\n    'books_sort_move_after_chapter' => 'Przenieś za rozdział',\n    'books_copy' => 'Skopiuj Książkę',\n    'books_copy_success' => 'Książka skopiowana pomyślnie',\n\n    // Chapters\n    'chapter' => 'Rozdział',\n    'chapters' => 'Rozdziały',\n    'x_chapters' => ':count Rozdział|:count Rozdziały',\n    'chapters_popular' => 'Popularne rozdziały',\n    'chapters_new' => 'Nowy rozdział',\n    'chapters_create' => 'Utwórz nowy rozdział',\n    'chapters_delete' => 'Usuń rozdział',\n    'chapters_delete_named' => 'Usuń rozdział :chapterName',\n    'chapters_delete_explain' => 'Spowoduje to usunięcie rozdziału o nazwie \\':chapterName\\'. Wszystkie strony, które istnieją w tym rozdziale, również zostaną usunięte.',\n    'chapters_delete_confirm' => 'Czy na pewno chcesz usunąć ten rozdział?',\n    'chapters_edit' => 'Edytuj rozdział',\n    'chapters_edit_named' => 'Edytuj rozdział :chapterName',\n    'chapters_save' => 'Zapisz rozdział',\n    'chapters_move' => 'Przenieś rozdział',\n    'chapters_move_named' => 'Przenieś rozdział :chapterName',\n    'chapters_copy' => 'Skopiuj Rozdział',\n    'chapters_copy_success' => 'Rozdział skopiowany pomyślnie',\n    'chapters_permissions' => 'Uprawienia rozdziału',\n    'chapters_empty' => 'Brak stron w tym rozdziale.',\n    'chapters_permissions_active' => 'Uprawnienia rozdziału są aktywne',\n    'chapters_permissions_success' => 'Zaktualizowano uprawnienia rozdziału',\n    'chapters_search_this' => 'Przeszukaj ten rozdział',\n    'chapter_sort_book' => 'Sortuj książkę',\n\n    // Pages\n    'page' => 'Strona',\n    'pages' => 'Strony',\n    'x_pages' => ':count stron',\n    'pages_popular' => 'Popularne strony',\n    'pages_new' => 'Nowa strona',\n    'pages_attachments' => 'Załączniki',\n    'pages_navigation' => 'Nawigacja po stronie',\n    'pages_delete' => 'Usuń stronę',\n    'pages_delete_named' => 'Usuń stronę :pageName',\n    'pages_delete_draft_named' => 'Usuń wersje robocze dla strony :pageName',\n    'pages_delete_draft' => 'Usuń wersje roboczą',\n    'pages_delete_success' => 'Strona usunięta pomyślnie',\n    'pages_delete_draft_success' => 'Werjsa robocza usunięta pomyślnie',\n    'pages_delete_warning_template' => 'Ta strona jest aktualnie używana jako domyślny szablon strony książki lub rozdziału. Po usunięciu tej strony te książki lub rozdziały nie będą już miały przypisanego domyślnego szablonu strony.',\n    'pages_delete_confirm' => 'Czy na pewno chcesz usunąć tę stronę?',\n    'pages_delete_draft_confirm' => 'Czy na pewno chcesz usunąć wersje roboczą strony?',\n    'pages_editing_named' => 'Edytowanie strony :pageName',\n    'pages_edit_draft_options' => 'Ustawienia wersji roboczej',\n    'pages_edit_save_draft' => 'Zapisano wersje roboczą o ',\n    'pages_edit_draft' => 'Edytuj wersje roboczą',\n    'pages_editing_draft' => 'Edytowanie wersji roboczej',\n    'pages_editing_page' => 'Edytowanie strony',\n    'pages_edit_draft_save_at' => 'Wersja robocza zapisana ',\n    'pages_edit_delete_draft' => 'Usuń wersje roboczą',\n    'pages_edit_delete_draft_confirm' => 'Czy na pewno chcesz usunąć zmiany wersji roboczej? Wszystkie Twoje zmiany, od ostatniego pełnego zapisu, zostaną utracone, a edytor zostanie zaktualizowany z najnowszym stanem zapisu.',\n    'pages_edit_discard_draft' => 'Porzuć wersje roboczą',\n    'pages_edit_switch_to_markdown' => 'Przełącz na edytor Markdown',\n    'pages_edit_switch_to_markdown_clean' => '(Czysta zawartość)',\n    'pages_edit_switch_to_markdown_stable' => '(Statyczna zawartość)',\n    'pages_edit_switch_to_wysiwyg' => 'Przełącz na edytor WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg' => 'Przełącz na nowy WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(W testach beta)',\n    'pages_edit_set_changelog' => 'Ustaw dziennik zmian',\n    'pages_edit_enter_changelog_desc' => 'Opisz zmiany, które zostały wprowadzone',\n    'pages_edit_enter_changelog' => 'Wyświetl dziennik zmian',\n    'pages_editor_switch_title' => 'Przełącz edytor',\n    'pages_editor_switch_are_you_sure' => 'Czy na pewno chcesz zmienić edytor dla tej strony?',\n    'pages_editor_switch_consider_following' => 'Przy zmianie edytorów pamiętaj, że:',\n    'pages_editor_switch_consideration_a' => 'Po zapisaniu nowo ustawiony edytor będzie używany przez przyszłych edytujących użytkowników, w tym również tych, którzy sami mogą nie być w stanie zmienić edytora.',\n    'pages_editor_switch_consideration_b' => 'Może to potencjalnie prowadzić do utraty szczegółów i składni w pewnych przypadkach.',\n    'pages_editor_switch_consideration_c' => 'Zmiany w znacznikach lub w dzienniku zmian, zrobione od ostatniego zapisu, nie zostaną zapamiętane przy tej zmianie.',\n    'pages_save' => 'Zapisz stronę',\n    'pages_title' => 'Tytuł strony',\n    'pages_name' => 'Nazwa strony',\n    'pages_md_editor' => 'Edytor',\n    'pages_md_preview' => 'Podgląd',\n    'pages_md_insert_image' => 'Wstaw obrazek',\n    'pages_md_insert_link' => 'Wstaw łącze do obiektu',\n    'pages_md_insert_drawing' => 'Wstaw rysunek',\n    'pages_md_show_preview' => 'Pokaż podgląd',\n    'pages_md_sync_scroll' => 'Synchronizuj przewijanie podglądu',\n    'pages_md_plain_editor' => 'Zwykły edytor',\n    'pages_drawing_unsaved' => 'Znaleziono niezapisany rysunek',\n    'pages_drawing_unsaved_confirm' => 'Znaleziono niezapisane dane rysowania z poprzedniej nieudanej próby zapisu. Czy chcesz przywrócić i kontynuować edycję tego niezapisanego rysunku?',\n    'pages_not_in_chapter' => 'Strona nie została umieszczona w rozdziale',\n    'pages_move' => 'Przenieś stronę',\n    'pages_copy' => 'Skopiuj stronę',\n    'pages_copy_desination' => 'Skopiuj do',\n    'pages_copy_success' => 'Strona została pomyślnie skopiowana',\n    'pages_permissions' => 'Uprawnienia strony',\n    'pages_permissions_success' => 'Zaktualizowano uprawnienia strony',\n    'pages_revision' => 'Wersja',\n    'pages_revisions' => 'Wersje strony',\n    'pages_revisions_desc' => 'Poniżej wymieniono wszystkie poprzednie wersje tej strony. Możesz cofnąć się wstecz, porównać i przywrócić stare wersje stron, jeśli pozwalają na to uprawnienia. Pełna historia strony może nie być całkowicie odzwierciedlona, ponieważ w zależności od konfiguracji systemu stare wersje mogą zostać automatycznie usunięte.',\n    'pages_revisions_named' => 'Wersje strony :pageName',\n    'pages_revision_named' => 'Wersja strony :pageName',\n    'pages_revision_restored_from' => 'Przywrócono z #:id; :summary',\n    'pages_revisions_created_by' => 'Utworzona przez',\n    'pages_revisions_date' => 'Data wersji',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Numer wersji',\n    'pages_revisions_numbered' => 'Wersja #:id',\n    'pages_revisions_numbered_changes' => 'Zmiany w wersji #:id',\n    'pages_revisions_editor' => 'Typ edytora',\n    'pages_revisions_changelog' => 'Dziennik zmian',\n    'pages_revisions_changes' => 'Zmiany',\n    'pages_revisions_current' => 'Obecna wersja',\n    'pages_revisions_preview' => 'Podgląd',\n    'pages_revisions_restore' => 'Przywróć',\n    'pages_revisions_none' => 'Ta strona nie posiada żadnych wersji',\n    'pages_copy_link' => 'Kopiuj link',\n    'pages_edit_content_link' => 'Przejdź do sekcji w edytorze',\n    'pages_pointer_enter_mode' => 'Aktywuj tryb wyboru sekcji',\n    'pages_pointer_label' => 'Sekcja opcji strony',\n    'pages_pointer_permalink' => 'Sekcja odnośnika strony',\n    'pages_pointer_include_tag' => 'Sekcja taga inkludującego',\n    'pages_pointer_toggle_link' => 'Tryb bezpośredniego linku, naciśnij aby zmienić na tryb tagu do inkludowania',\n    'pages_pointer_toggle_include' => 'Tryb tagu do inkludowania, naciśnij aby zmienić na tryb bezpośredniego linku',\n    'pages_permissions_active' => 'Uprawnienia strony są aktywne',\n    'pages_initial_revision' => 'Pierwsze wydanie',\n    'pages_references_update_revision' => 'Automatyczna aktualizacja wewnętrznych linków',\n    'pages_initial_name' => 'Nowa strona',\n    'pages_editing_draft_notification' => 'Edytujesz obecnie wersję roboczą, która była ostatnio zapisana :timeDiff.',\n    'pages_draft_edited_notification' => 'Od tego czasu ta strona była zmieniana. Zalecane jest odrzucenie tej wersji roboczej.',\n    'pages_draft_page_changed_since_creation' => 'Ta strona została zaktualizowana od czasu utworzenia tego szkicu. Zaleca się, aby odrzucić ten szkic lub nie nadpisywać żadnych zmian na stronie.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count użytkowników rozpoczęło edytowanie tej strony',\n        'start_b' => ':userName edytuje stronę',\n        'time_a' => 'od czasu ostatniej edycji',\n        'time_b' => 'w ciągu ostatnich :minCount minut',\n        'message' => ':start :time. Pamiętaj by nie nadpisywać czyichś zmian!',\n    ],\n    'pages_draft_discarded' => 'Wersja robocza odrzucona! Edytor został ustawiony na aktualną wersję strony',\n    'pages_draft_deleted' => 'Wersja robocza usunięta! Edytor został ustawiony na aktualną wersję strony',\n    'pages_specific' => 'Określona strona',\n    'pages_is_template' => 'Szablon strony',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Przełącz pasek boczny',\n    'page_tags' => 'Tagi strony',\n    'chapter_tags' => 'Tagi rozdziału',\n    'book_tags' => 'Tagi książki',\n    'shelf_tags' => 'Tagi półki',\n    'tag' => 'Tag',\n    'tags' =>  'Tagi',\n    'tags_index_desc' => 'Tagi mogą być stosowane do treści w systemie w celu umożliwienia elastycznej formy kategoryzacji. Tagi mogą mieć zarówno klucz, jak i wartość, przy czym wartość jest opcjonalna. Po zastosowaniu tagu treść może być wyszukana przy użyciu nazwy i wartości tagu.',\n    'tag_name' =>  'Nazwa tagu',\n    'tag_value' => 'Wartość tagu (opcjonalnie)',\n    'tags_explain' => \"Dodaj tagi by skategoryzować zawartość. \\n W celu dokładniejszej organizacji zawartości możesz dodać wartości do tagów.\",\n    'tags_add' => 'Dodaj kolejny tag',\n    'tags_remove' => 'Usuń ten tag',\n    'tags_usages' => 'Całkowite użycie tagów',\n    'tags_assigned_pages' => 'Przypisane do Stron',\n    'tags_assigned_chapters' => 'Przypisane do Rozdziałów',\n    'tags_assigned_books' => 'Przypisane do Książek',\n    'tags_assigned_shelves' => 'Przypisane do Półek',\n    'tags_x_unique_values' => ':count unikalnych wartości',\n    'tags_all_values' => 'Wszystkie wartości',\n    'tags_view_tags' => 'Zobacz Tagi',\n    'tags_view_existing_tags' => 'Zobacz istniejące tagi',\n    'tags_list_empty_hint' => 'Tagi mogą być przypisane przez pasek boczny edytora strony lub podczas edycji szczegółów książki, rozdziału lub półki.',\n    'attachments' => 'Załączniki',\n    'attachments_explain' => 'Prześlij kilka plików lub załącz linki. Będą one widoczne na pasku bocznym strony.',\n    'attachments_explain_instant_save' => 'Zmiany są zapisywane natychmiastowo.',\n    'attachments_upload' => 'Dodaj plik',\n    'attachments_link' => 'Dodaj link',\n    'attachments_upload_drop' => 'Alternatywnie możesz przeciągnąć i upuścić plik tutaj, aby przesłać go jako załącznik.',\n    'attachments_set_link' => 'Ustaw link',\n    'attachments_delete' => 'Jesteś pewien, że chcesz usunąć ten załącznik?',\n    'attachments_dropzone' => 'Upuść pliki tutaj, aby przesłać',\n    'attachments_no_files' => 'Nie przesłano żadnych plików',\n    'attachments_explain_link' => 'Możesz załączyć link jeśli nie chcesz przesyłać pliku. Może być to link do innej strony lub link do pliku w chmurze.',\n    'attachments_link_name' => 'Nazwa linku',\n    'attachment_link' => 'Link do załącznika',\n    'attachments_link_url' => 'Link do pliku',\n    'attachments_link_url_hint' => 'Strona lub plik',\n    'attach' => 'Załącz',\n    'attachments_insert_link' => 'Dodaj link do załącznika do strony',\n    'attachments_edit_file' => 'Edytuj plik',\n    'attachments_edit_file_name' => 'Nazwa pliku',\n    'attachments_edit_drop_upload' => 'Upuść pliki lub kliknij tutaj by przesłać pliki i nadpisać istniejące',\n    'attachments_order_updated' => 'Kolejność załączników zaktualizowana',\n    'attachments_updated_success' => 'Szczegóły załączników zaktualizowane',\n    'attachments_deleted' => 'Załącznik usunięty',\n    'attachments_file_uploaded' => 'Plik załączony pomyślnie',\n    'attachments_file_updated' => 'Plik zaktualizowany pomyślnie',\n    'attachments_link_attached' => 'Link pomyślnie dodany do strony',\n    'templates' => 'Szablony',\n    'templates_set_as_template' => 'Strona jest szablonem',\n    'templates_explain_set_as_template' => 'Możesz ustawić tę stronę jako szablon, tak aby jej zawartość była wykorzystywana przy tworzeniu innych stron. Inni użytkownicy będą mogli korzystać z tego szablonu, jeśli mają uprawnienia do przeglądania tej strony.',\n    'templates_replace_content' => 'Zmień zawartość strony',\n    'templates_append_content' => 'Dodaj do zawartośći strony na końcu',\n    'templates_prepend_content' => 'Dodaj do zawartośći strony na początku',\n\n    // Profile View\n    'profile_user_for_x' => 'Użytkownik od :time',\n    'profile_created_content' => 'Utworzona zawartość',\n    'profile_not_created_pages' => ':userName nie utworzył żadnych stron',\n    'profile_not_created_chapters' => ':userName nie utworzył żadnych rozdziałów',\n    'profile_not_created_books' => ':userName nie utworzył żadnych książek',\n    'profile_not_created_shelves' => ':userName nie utworzył żadnych półek',\n\n    // Comments\n    'comment' => 'Komentarz',\n    'comments' => 'Komentarze',\n    'comment_add' => 'Dodaj komentarz',\n    'comment_none' => 'Brak komentarzy do wyświetlenia',\n    'comment_placeholder' => 'Napisz swój komentarz tutaj',\n    'comment_thread_count' => ':count wątek komentarza|:count wątków komentarzy',\n    'comment_archived_count' => ':count zarchiwizowanych',\n    'comment_archived_threads' => 'Zarchiwizowane wątki',\n    'comment_save' => 'Zapisz komentarz',\n    'comment_new' => 'Nowy komentarz',\n    'comment_created' => 'Skomentowano :createDiff',\n    'comment_updated' => 'Zaktualizowano :updateDiff przez :username',\n    'comment_updated_indicator' => 'Zaktualizowano',\n    'comment_deleted_success' => 'Komentarz usunięty',\n    'comment_created_success' => 'Komentarz dodany',\n    'comment_updated_success' => 'Komentarz zaktualizowany',\n    'comment_archive_success' => 'Komentarz zarchiwizowany',\n    'comment_unarchive_success' => 'Komentarz usunięty z archiwum',\n    'comment_view' => 'Zobacz komentarz',\n    'comment_jump_to_thread' => 'Przejdź do wątku',\n    'comment_delete_confirm' => 'Czy na pewno chcesz usunąc ten komentarz?',\n    'comment_in_reply_to' => 'W odpowiedzi na :commentId',\n    'comment_reference' => 'Odwołania',\n    'comment_reference_outdated' => '(Przestarzałe)',\n    'comment_editor_explain' => 'Oto komentarze pozostawione na tej stronie. Komentarze mogą być dodawane i zarządzane podczas przeglądania zapisanej strony.',\n\n    // Revision\n    'revision_delete_confirm' => 'Czy na pewno chcesz usunąć tę wersję?',\n    'revision_restore_confirm' => 'Czu ma pewno chcesz przywrócić tą wersję? Aktualna zawartość strony zostanie nadpisana.',\n    'revision_cannot_delete_latest' => 'Nie można usunąć najnowszej wersji.',\n\n    // Copy view\n    'copy_consider' => 'Kopiując, weź pod uwagę poniższe rzeczy.',\n    'copy_consider_permissions' => 'Niestandardowe ustawienia uprawnień nie zostaną skopiowane.',\n    'copy_consider_owner' => 'Staniesz się właścicielem całej skopiowanej zawartości.',\n    'copy_consider_images' => 'Pliki obrazów znajdujących się na stronie nie będą zduplikowane i oryginalne obrazy zachowają swój związek ze stroną, do której zostały pierwotnie przesłane.',\n    'copy_consider_attachments' => 'Załączniki strony nie zostaną skopiowane.',\n    'copy_consider_access' => 'Zmiana lokalizacji, właściciela lub uprawnień może spowodować, że ta zawartość będzie dostępna dla tych, którzy wcześniej nie mieli dostępu.',\n\n    // Conversions\n    'convert_to_shelf' => 'Konwertuj na półkę',\n    'convert_to_shelf_contents_desc' => 'Możesz skonwertować tę książkę do nowej półki z tą samą zawartością. Rozdziały zawarte w tej książce zostaną skonwertowane na nowe książki. Jeśli ta książka zawiera jakieś strony, które nie znajdują się w rozdziale, wtedy nazwa tej książki zostanie zmieniona i będzie ona zawierać te strony, a sama książka stanie się częścią nowej półki.',\n    'convert_to_shelf_permissions_desc' => 'Wszelkie uprawnienia ustawione w tej książce zostaną skopiowane do nowej półki i do wszystkich nowych książek tej półki, które nie mają własnych uprawnień. Zauważ, że uprawnienia na półkach nie są automatycznie kaskadowane do ich zawartości, tak jak to ma miejsce w przypadku książek.',\n    'convert_book' => 'Konwertuj książkę',\n    'convert_book_confirm' => 'Czy na pewno chcesz skonwertować tę książkę?',\n    'convert_undo_warning' => 'Nie da się tego łatwo cofnąć.',\n    'convert_to_book' => 'Konwertuj na książkę',\n    'convert_to_book_desc' => 'Możesz skonwertować ten rozdział do nowej książki o tej samej treści. Wszelkie uprawnienia ustawione w tym rozdziale zostaną skopiowane do nowej książki, ale wszelkie dziedziczone uprawnienia z poprzedniej nadrzędnej książki nie będą skopiowane, co może doprowadzić do zmiany w kontroli dostępu.',\n    'convert_chapter' => 'Konwertuj rozdział',\n    'convert_chapter_confirm' => 'Czy na pewno chcesz skonwertować ten rozdział?',\n\n    // References\n    'references' => 'Odniesienia',\n    'references_none' => 'Brak śledzonych odwołań do tego elementu.',\n    'references_to_desc' => 'Wymienione poniżej są wszystkie znane treści w systemie, które nawiązują do tego elementu.',\n\n    // Watch Options\n    'watch' => 'Obserwuj',\n    'watch_title_default' => 'Domyślne ustawienia',\n    'watch_desc_default' => 'Przywróć do tylko domyślnych ustawień powiadomień.',\n    'watch_title_ignore' => 'Ignoruj',\n    'watch_desc_ignore' => 'Ignoruj wszystkie powiadomienia, w tym te z preferencji użytkownika.',\n    'watch_title_new' => 'Nowe strony',\n    'watch_desc_new' => 'Powiadom o utworzeniu nowej strony w tym elemencie.',\n    'watch_title_updates' => 'Wszystkie aktualizacje strony',\n    'watch_desc_updates' => 'Powiadom o wszystkich nowych stronach i zmianach strony.',\n    'watch_desc_updates_page' => 'Powiadom o wszystkich zmianach strony.',\n    'watch_title_comments' => 'Wszystkie aktualizacje strony i komentarze',\n    'watch_desc_comments' => 'Powiadom o wszystkich nowych stronach, zmianach na stronie i nowych komentarzach.',\n    'watch_desc_comments_page' => 'Powiadom o zmianach strony i nowych komentarzach.',\n    'watch_change_default' => 'Zmień domyślne ustawienia powiadomień',\n    'watch_detail_ignore' => 'Ignorowanie powiadomień',\n    'watch_detail_new' => 'Obserwowanie nowych stron',\n    'watch_detail_updates' => 'Obserwowanie nowych stron i aktualizacji',\n    'watch_detail_comments' => 'Obserwowanie nowych stron, aktualizacji i komentarzy',\n    'watch_detail_parent_book' => 'Obserwowanie przez książkę nadrzędną',\n    'watch_detail_parent_book_ignore' => 'Ignorowanie przez książkę nadrzędną',\n    'watch_detail_parent_chapter' => 'Obserwowanie przez rozdział nadrzędny',\n    'watch_detail_parent_chapter_ignore' => 'Ignorowanie przez rozdział nadrzędny',\n];\n"
  },
  {
    "path": "lang/pl/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Nie masz uprawnień do wyświetlenia tej strony.',\n    'permissionJson' => 'Nie masz uprawnień do wykonania tej akcji.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Użytkownik o adresie :email już istnieje, ale używa innych poświadczeń.',\n    'auth_pre_register_theme_prevention' => 'Konto użytkownika nie może być zarejestrowane z podanymi danymi',\n    'email_already_confirmed' => 'E-mail został potwierdzony, spróbuj się zalogować.',\n    'email_confirmation_invalid' => 'Ten token jest nieprawidłowy lub został już wykorzystany. Spróbuj zarejestrować się ponownie.',\n    'email_confirmation_expired' => 'Ten token potwierdzający wygasł. Wysłaliśmy Ci kolejny.',\n    'email_confirmation_awaiting' => 'Adres e-mail dla używanego konta musi zostać potwierdzony',\n    'ldap_fail_anonymous' => 'Dostęp LDAP przy użyciu anonimowego powiązania nie powiódł się',\n    'ldap_fail_authed' => 'Dostęp LDAP przy użyciu tego DN i hasła nie powiódł się',\n    'ldap_extension_not_installed' => 'Rozszerzenie LDAP PHP nie zostało zainstalowane',\n    'ldap_cannot_connect' => 'Nie można połączyć z serwerem LDAP, połączenie nie zostało ustanowione',\n    'saml_already_logged_in' => 'Już zalogowany',\n    'saml_no_email_address' => 'Nie można odnaleźć adresu email dla tego użytkownika w danych dostarczonych przez zewnętrzny system uwierzytelniania',\n    'saml_invalid_response_id' => 'Żądanie z zewnętrznego systemu uwierzytelniania nie zostało rozpoznane przez proces rozpoczęty przez tę aplikację. Cofnięcie po zalogowaniu mogło spowodować ten problem.',\n    'saml_fail_authed' => 'Logowanie przy użyciu :system nie powiodło się, system nie mógł pomyślnie ukończyć uwierzytelniania',\n    'oidc_already_logged_in' => 'Już zalogowany',\n    'oidc_no_email_address' => 'Nie można odnaleźć adresu email dla tego użytkownika w danych dostarczonych przez zewnętrzny system uwierzytelniania',\n    'oidc_fail_authed' => 'Logowanie przy użyciu :system nie powiodło się, system nie mógł pomyślnie ukończyć uwierzytelniania',\n    'social_no_action_defined' => 'Brak zdefiniowanej akcji',\n    'social_login_bad_response' => \"Podczas próby logowania :socialAccount wystąpił błąd: \\n:error\",\n    'social_account_in_use' => 'To konto :socialAccount jest już w użyciu. Spróbuj zalogować się za pomocą opcji :socialAccount.',\n    'social_account_email_in_use' => 'E-mail :email jest już w użyciu. Jeśli masz już konto, połącz konto :socialAccount z poziomu ustawień profilu.',\n    'social_account_existing' => 'Konto :socialAccount jest już połączone z Twoim profilem',\n    'social_account_already_used_existing' => 'Konto :socialAccount jest już używane przez innego użytkownika.',\n    'social_account_not_used' => 'To konto :socialAccount nie jest połączone z żadnym użytkownikiem. Połącz je ze swoim kontem w ustawieniach profilu. ',\n    'social_account_register_instructions' => 'Jeśli nie masz jeszcze konta, możesz zarejestrować je używając opcji :socialAccount.',\n    'social_driver_not_found' => 'Funkcja społecznościowa nie została odnaleziona',\n    'social_driver_not_configured' => 'Ustawienia konta :socialAccount nie są poprawne.',\n    'invite_token_expired' => 'Zaproszenie wygasło. Możesz spróować zresetować swoje hasło.',\n    'login_user_not_found' => 'Użytkownik dla tej akcji nie został znaleziony.',\n\n    // System\n    'path_not_writable' => 'Zapis do ścieżki :filePath jest niemożliwy. Upewnij się że aplikacja ma prawa do zapisu plików na serwerze.',\n    'cannot_get_image_from_url' => 'Nie można pobrać obrazka z :url',\n    'cannot_create_thumbs' => 'Serwer nie może utworzyć miniaturek. Upewnij się że rozszerzenie GD PHP zostało zainstalowane.',\n    'server_upload_limit' => 'Serwer nie pozwala na przyjęcie pliku o tym rozmiarze. Spróbuj przesłać plik o mniejszym rozmiarze.',\n    'server_post_limit' => 'Serwer nie może przyjąć tej ilości danych. Spróbuj ponownie z mniejszą ilością danych lub mniejszym plikiem.',\n    'uploaded'  => 'Serwer nie pozwala na przyjęcie pliku o tym rozmiarze. Spróbuj przesłać plik o mniejszym rozmiarze.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Wystąpił błąd podczas przesyłania obrazka',\n    'image_upload_type_error' => 'Typ przesłanego obrazka jest nieprwidłowy.',\n    'image_upload_replace_type' => 'Zamienniki plików graficznych muszą być tego samego typu',\n    'image_upload_memory_limit' => 'Nie udało się obsłużyć przesyłania zdjęć i/lub tworzenia miniatur ze względu na limity zasobów systemowych.',\n    'image_thumbnail_memory_limit' => 'Nie udało się utworzyć wariantów rozmiaru obrazu ze względu na limity zasobów systemowych.',\n    'image_gallery_thumbnail_memory_limit' => 'Nie udało się utworzyć miniatur galerii ze względu na limity zasobów systemowych.',\n    'drawing_data_not_found' => 'Nie można załadować danych rysunku. Plik rysunku może już nie istnieć lub nie masz uprawnień dostępu do niego.',\n\n    // Attachments\n    'attachment_not_found' => 'Nie znaleziono załącznika',\n    'attachment_upload_error' => 'Wystąpił błąd podczas przesyłania pliku załącznika',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Zapis wersji roboczej nie powiódł się. Upewnij się, że posiadasz połączenie z internetem.',\n    'page_draft_delete_fail' => 'Nie udało się usunąć wersji roboczej strony i pobrać bieżącej zawartości strony',\n    'page_custom_home_deletion' => 'Nie można usunąć strony, jeśli jest ona ustawiona jako strona główna',\n\n    // Entities\n    'entity_not_found' => 'Nie znaleziono obiektu',\n    'bookshelf_not_found' => 'Nie znaleziono półki',\n    'book_not_found' => 'Nie znaleziono książki',\n    'page_not_found' => 'Nie znaleziono strony',\n    'chapter_not_found' => 'Nie znaleziono rozdziału',\n    'selected_book_not_found' => 'Wybrana książka nie została znaleziona',\n    'selected_book_chapter_not_found' => 'Wybrana książka lub rozdział nie został znaleziony',\n    'guests_cannot_save_drafts' => 'Goście nie mogą zapisywać wersji roboczych',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Nie możesz usunąć jedynego administratora',\n    'users_cannot_delete_guest' => 'Nie możesz usunąć użytkownika-gościa',\n    'users_could_not_send_invite' => 'Nie można utworzyć użytkownika, ponieważ nie udało się wysłać wiadomości e-mail z zaproszeniem',\n\n    // Roles\n    'role_cannot_be_edited' => 'Ta rola nie może być edytowana',\n    'role_system_cannot_be_deleted' => 'Ta rola jest rolą systemową i nie może zostać usunięta',\n    'role_registration_default_cannot_delete' => 'Ta rola nie może zostać usunięta, dopóki jest ustawiona jako domyślna rola użytkownika',\n    'role_cannot_remove_only_admin' => 'Ten użytkownik jest jedynym użytkownikiem przypisanym do roli administratora. Przypisz rolę administratora innemu użytkownikowi przed próbą usunięcia.',\n\n    // Comments\n    'comment_list' => 'Wystąpił błąd podczas pobierania komentarzy.',\n    'cannot_add_comment_to_draft' => 'Nie możesz dodawać komentarzy do wersji roboczej.',\n    'comment_add' => 'Wystąpił błąd podczas dodwania / aktualizaowania komentarza.',\n    'comment_delete' => 'Wystąpił błąd podczas usuwania komentarza.',\n    'empty_comment' => 'Nie można dodać pustego komentarza.',\n\n    // Error pages\n    '404_page_not_found' => 'Strona nie została znaleziona',\n    'sorry_page_not_found' => 'Przepraszamy, ale strona której szukasz nie została znaleziona.',\n    'sorry_page_not_found_permission_warning' => 'Jeśli spodziewałeś się, że ta strona istnieje, prawdopodobnie nie masz uprawnień do jej wyświetlenia.',\n    'image_not_found' => 'Nie znaleziono obrazu',\n    'image_not_found_subtitle' => 'Przepraszamy, ale obraz którego szukasz nie został znaleziony.',\n    'image_not_found_details' => 'Jeśli spodziewałeś się, że ten obraz istnieje, mógł on zostać usunięty.',\n    'return_home' => 'Powrót do strony głównej',\n    'error_occurred' => 'Wystąpił błąd',\n    'app_down' => ':appName jest aktualnie wyłączona',\n    'back_soon' => 'Niedługo zostanie uruchomiona ponownie.',\n\n    // Import\n    'import_zip_cant_read' => 'Nie można odczytać archiwum ZIP.',\n    'import_zip_cant_decode_data' => 'Nie udało się odnaleźć i dekodować pliku data.json w zawartości archiwum ZIP.',\n    'import_zip_no_data' => 'Dane archiwum ZIP nie zawierają oczekiwanej zawartości książki, rozdziału lub strony.',\n    'import_zip_data_too_large' => 'Zawartość pliku data.json w archiwum ZIP przekracza maksymalny dopuszczalny rozmiar narzucony przez aktualną konfigurację aplikacji.',\n    'import_validation_failed' => 'Walidacja importu archiwum ZIP nie powiodła się z błędami:',\n    'import_zip_failed_notification' => 'Nie udało się zaimportować archiwum ZIP.',\n    'import_perms_books' => 'Brakuje Ci wymaganych uprawnień do tworzenia książek.',\n    'import_perms_chapters' => 'Brakuje Ci wymaganych uprawnień do tworzenia rozdziałów.',\n    'import_perms_pages' => 'Brakuje Ci wymaganych uprawnień do tworzenia stron.',\n    'import_perms_images' => 'Brakuje Ci wymaganych uprawnień do tworzenia zdjęć.',\n    'import_perms_attachments' => 'Brakuje Ci wymaganych uprawnień do tworzenia załączników.',\n\n    // API errors\n    'api_no_authorization_found' => 'Nie znaleziono tokenu autoryzacji dla żądania',\n    'api_bad_authorization_format' => 'Token autoryzacji został znaleziony w żądaniu, ale format okazał się nieprawidłowy',\n    'api_user_token_not_found' => 'Nie znaleziono pasującego tokenu API dla podanego tokenu autoryzacji',\n    'api_incorrect_token_secret' => 'Podany sekret dla tego API jest nieprawidłowy',\n    'api_user_no_api_permission' => 'Właściciel używanego tokenu API nie ma uprawnień do wykonywania zapytań do API',\n    'api_user_token_expired' => 'Token uwierzytelniania wygasł',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Błąd podczas wysyłania testowej wiadomości e-mail:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'Adres URL nie pasuje do skonfigurowanych dozwolonych hostów SSR',\n];\n"
  },
  {
    "path": "lang/pl/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Nowy komentarz na stronie: :pageName',\n    'new_comment_intro' => 'Użytkownik skomentował stronę w :appName:',\n    'new_page_subject' => 'Nowa strona: :pageName',\n    'new_page_intro' => 'Nowa strona została utworzona w :appName:',\n    'updated_page_subject' => 'Zaktualizowano stronę: :pageName',\n    'updated_page_intro' => 'Strona została zaktualizowana w :appName:',\n    'updated_page_debounce' => 'Aby zapobiec nadmiarowi powiadomień, przez jakiś czas nie będziesz otrzymywać powiadomień o dalszych edycjach tej strony przez tego samego edytora.',\n    'comment_mention_subject' => 'Zostałeś oznaczony w komentarzu na stronie: :pageName',\n    'comment_mention_intro' => 'Zostałeś oznaczony w komentarzu w :appName:',\n\n    'detail_page_name' => 'Nazwa strony:',\n    'detail_page_path' => 'Ścieżka strony:',\n    'detail_commenter' => 'Skomentował:',\n    'detail_comment' => 'Komentarz:',\n    'detail_created_by' => 'Utworzono przez:',\n    'detail_updated_by' => 'Zaktualizowano przez:',\n\n    'action_view_comment' => 'Pokaż komentarz',\n    'action_view_page' => 'Wyświetl stronę',\n\n    'footer_reason' => 'To powiadomienie zostało wysłane do Ciebie, ponieważ :link obejmuje ten typ aktywności dla tego elementu.',\n    'footer_reason_link' => 'ustawienia powiadomień',\n];\n"
  },
  {
    "path": "lang/pl/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Poprzednia',\n    'next'     => 'Następna &raquo;',\n\n];\n"
  },
  {
    "path": "lang/pl/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Hasło musi zawierać co najmniej 6 znaków i być zgodne z powtórzeniem.',\n    'user' => \"Nie znaleziono użytkownika o takim adresie e-mail.\",\n    'token' => 'Token resetowania hasła jest nieprawidłowy dla tego adresu e-mail.',\n    'sent' => 'Wysłaliśmy Ci link do resetowania hasła!',\n    'reset' => 'Twoje hasło zostało zresetowane!',\n\n];\n"
  },
  {
    "path": "lang/pl/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Moje konto',\n\n    'shortcuts' => 'Skróty',\n    'shortcuts_interface' => 'Ustawienia skrótów interfejsu użytkownika',\n    'shortcuts_toggle_desc' => 'Tutaj możesz włączyć lub wyłączyć interfejs skrótów klawiszowych używanych do nawigacji i akcji.',\n    'shortcuts_customize_desc' => 'Możesz dostosować każdy z poniższych skrótów. Wystarczy nacisnąć wybraną kombinację klawiszy po wybraniu wprowadzania dla danego skrótu.',\n    'shortcuts_toggle_label' => 'Skróty klawiszowe włączone',\n    'shortcuts_section_navigation' => 'Nawigacja',\n    'shortcuts_section_actions' => 'Częste działania',\n    'shortcuts_save' => 'Zapisz skróty',\n    'shortcuts_overlay_desc' => 'Uwaga: Gdy skróty są włączone, przez naciśnięcie \"?\" może być otworzona nakładka pomocnicza, która podświetli dostępne skróty dla akcji widocznych obecnie na ekranie.',\n    'shortcuts_update_success' => 'Ustawienia skrótów zostały zaktualizowane!',\n    'shortcuts_overview_desc' => 'Zarządzaj skrótami klawiaturowymi, które możesz użyć do nawigacji interfejsu użytkownika systemu.',\n\n    'notifications' => 'Preferencje powiadomień',\n    'notifications_desc' => 'Kontroluj otrzymywane powiadomienia e-mail, gdy określona aktywność jest wykonywana w systemie.',\n    'notifications_opt_own_page_changes' => 'Powiadom o zmianach na stronach, których jestem właścicielem',\n    'notifications_opt_own_page_comments' => 'Powiadom o komentarzach na stronach, których jestem właścicielem',\n    'notifications_opt_comment_mentions' => 'Powiadom, kiedy zostanę oznaczony w komentarzu',\n    'notifications_opt_comment_replies' => 'Powiadom o odpowiedziach na moje komentarze',\n    'notifications_save' => 'Zapisz preferencje',\n    'notifications_update_success' => 'Preferencje powiadomień zostały zaktualizowane!',\n    'notifications_watched' => 'Obserwowane i ignorowane elementy',\n    'notifications_watched_desc' => 'Poniżej znajdują się elementy, które mają własne preferencje obserwowania. Aby zaktualizować swoje preferencje, zobacz dany element, a następnie znajdź opcje obserwowania na pasku bocznym.',\n\n    'auth' => 'Dostęp i bezpieczeństwo',\n    'auth_change_password' => 'Zmień hasło',\n    'auth_change_password_desc' => 'Zmień hasło logowania do aplikacji. Hasło musi mieć minimum 8 znaków.',\n    'auth_change_password_success' => 'Hasło zostało zaktualizowane!',\n\n    'profile' => 'Szczegóły profilu',\n    'profile_desc' => 'Zarządzaj szczegółami swojego konta, które widzą inni użytkownicy, oprócz danych używanych do komunikacji i personalizacji systemu.',\n    'profile_view_public' => 'Zobacz profil publiczny',\n    'profile_name_desc' => 'Skonfiguruj wyświetlaną nazwę, która będzie widoczna dla innych użytkowników systemu poprzez wykonywane zadania i posiadaną treść.',\n    'profile_email_desc' => 'Ten e-mail będzie używany do powiadomień, a w zależności od wybranego sposobu uwierzytelniania, również do dostępu do systemu.',\n    'profile_email_no_permission' => 'Niestety nie masz uprawnień do zmiany adresu e-mail. Jeśli chcesz to zmienić, musisz poprosić administratora, aby zrobił to za ciebie.',\n    'profile_avatar_desc' => 'Wybierz obraz, który będzie cię reprezentował wśród innych w systemie. Obraz powinien być kwadratem o długości boku około 256 pikseli.',\n    'profile_admin_options' => 'Opcje administratora',\n    'profile_admin_options_desc' => 'Dodatkowe opcje na poziomie administratora dla twojego konta, takie jak zarządzanie przydzielaniem ról można znaleźć w sekcji \"Ustawienia > Użytkownicy\".',\n\n    'delete_account' => 'Usuń konto',\n    'delete_my_account' => 'Usuń moje konto',\n    'delete_my_account_desc' => 'Spowoduje to całkowite usunięcie twojego konta z systemu. Nie będziesz miał możliwości odzyskania konta lub cofnięcia tej czynności. Stworzona przez Ciebie zawartość, taka jak utworzone strony i przesłane obrazy, pozostanie niezmieniona.',\n    'delete_my_account_warning' => 'Jesteś pewny, że chcesz usunąć swoje konto?',\n];\n"
  },
  {
    "path": "lang/pl/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Ustawienia',\n    'settings_save' => 'Zapisz ustawienia',\n    'system_version' => 'Wersja Systemu',\n    'categories' => 'Kategorie',\n\n    // App Settings\n    'app_customization' => 'Dostosowywanie',\n    'app_features_security' => 'Funkcje i bezpieczeństwo',\n    'app_name' => 'Nazwa aplikacji',\n    'app_name_desc' => 'Ta nazwa jest wyświetlana w nagłówku i e-mailach.',\n    'app_name_header' => 'Pokaż nazwę aplikacji w nagłówku',\n    'app_public_access' => 'Dostęp publiczny',\n    'app_public_access_desc' => 'Włączenie tej opcji umożliwi niezalogowanym odwiedzającym dostęp do treści w Twojej instancji BookStack.',\n    'app_public_access_desc_guest' => 'Dostęp dla niezalogowanych odwiedzających jest kontrolowany poprzez użytkownika \"Guest\".',\n    'app_public_access_toggle' => 'Zezwalaj na dostęp publiczny',\n    'app_public_viewing' => 'Zezwolić na publiczne przeglądanie?',\n    'app_secure_images' => 'Bezpieczniejsze przesyłanie obrazów',\n    'app_secure_images_toggle' => 'Włącz wyższy poziom bezpieczeństwa dla obrazów',\n    'app_secure_images_desc' => 'Ze względu na wydajność systemu wszystkie obrazki są publiczne. Ta opcja dodaje trudny do odgadnięcia losowy ciąg znaków na początku nazwy obrazka. Upewnij się, że indeksowanie katalogów jest wyłączone, aby uniemożliwić łatwy dostęp do obrazków.',\n    'app_default_editor' => 'Domyślny edytor stron',\n    'app_default_editor_desc' => 'Wybierz, który edytor będzie domyślnie używany podczas edycji nowych stron. Może to być nadpisane na poziomie strony, na którym pozwalają na to uprawnienia.',\n    'app_custom_html' => 'Własna zawartość w tagu <head>',\n    'app_custom_html_desc' => 'Zawartość dodana tutaj zostanie dołączona na dole sekcji <head> każdej strony. Przydatne przy nadpisywaniu styli lub dodawaniu analityki.',\n    'app_custom_html_disabled_notice' => 'Niestandardowa zawartość nagłówka HTML jest wyłączona na tej stronie ustawień aby zapewnić, że wszystkie błedne zmiany (braking change) mogą zostać cofnięte.',\n    'app_logo' => 'Logo aplikacji',\n    'app_logo_desc' => 'Jest używany między innymi w pasku nagłówka aplikacji. Ten obraz powinien mieć wysokość 86px. Duże obrazy zostaną przeskalowane w dół.',\n    'app_icon' => 'Ikona aplikacji',\n    'app_icon_desc' => 'Ta ikona jest używana w zakładkach przeglądarki i ikonach skrótu. Powinien to być obraz PNG o wymiarach 256px.',\n    'app_homepage' => 'Strona główna',\n    'app_homepage_desc' => 'Wybierz widok, który będzie wyświetlany na stronie głównej zamiast w widoku domyślnego. Uprawnienia dostępowe są ignorowane dla wybranych stron.',\n    'app_homepage_select' => 'Wybierz stronę',\n    'app_footer_links' => 'Linki w stopce',\n    'app_footer_links_desc' => 'Dodaj linki do pokazania w stopce witryny. Będą one wyświetlane na dole większości stron, włącznie z tymi, które nie wymagają logowania. Możesz użyć etykiety \"trans::<key>\" aby użyć tłumaczeń zdefiniowanych przez system. Na przykład: Używanie \"trans::common.privacy_policy\" zapewni przetłumaczony tekst \"Polityka prywatności\" i \"trans::common.terms_of_service\" zapewni przetłumaczony tekst \"Warunki korzystania z usługi\".',\n    'app_footer_links_label' => 'Etykieta linku',\n    'app_footer_links_url' => 'URL odnośnika',\n    'app_footer_links_add' => 'Dodaj link w stopce',\n    'app_disable_comments' => 'Wyłącz komentarze',\n    'app_disable_comments_toggle' => 'Wyłącz komentowanie',\n    'app_disable_comments_desc' => 'Wyłącz komentarze na wszystkich stronach w aplikacji. Istniejące komentarze nie będą pokazywane.',\n\n    // Color settings\n    'color_scheme' => 'Schemat kolorów aplikacji',\n    'color_scheme_desc' => 'Ustaw kolory używane w interfejsie aplikacji. Kolory można skonfigurować oddzielnie dla trybu ciemnego i jasnego, aby najlepiej pasowały do motywu i zapewniały czytelność.',\n    'ui_colors_desc' => 'Ustaw podstawowy kolor aplikacji i domyślny kolor linku. Podstawowy kolor jest używany głównie w banerze aplikacji, przyciskach i interfejsie. Domyślny kolor linku jest używany dla tekstowych linków i akcji, zarówno w napisanych treściach, jak i w interfejsie aplikacji.',\n    'app_color' => 'Kolor podstawowy',\n    'link_color' => 'Domyślny kolor linku',\n    'content_colors_desc' => 'Ustaw kolory dla wszystkich elementów w hierarchii organizacji stron. Wybór kolorów o jasności podobnej do domyślnych kolorów jest zalecany dla czytelności.',\n    'bookshelf_color' => 'Kolor półki',\n    'book_color' => 'Kolor książki',\n    'chapter_color' => 'Kolor rozdziału',\n    'page_color' => 'Kolor strony',\n    'page_draft_color' => 'Kolor szkicu strony',\n\n    // Registration Settings\n    'reg_settings' => 'Ustawienia rejestracji',\n    'reg_enable' => 'Włącz rejestrację',\n    'reg_enable_toggle' => 'Włącz rejestrację',\n    'reg_enable_desc' => 'Przy włączonej rejestracji użytkownicy będą w stanie samodzielnie założyć sobie konto w systemie. Po rejestracji automatycznie otrzymają domyślną rolę.',\n    'reg_default_role' => 'Domyślna rola użytkownika po rejestracji',\n    'reg_enable_external_warning' => 'Powyższa opcja jest ignorowana, gdy zewnętrzne uwierzytelnianie LDAP lub SAML jest aktywne. Konta użytkowników dla nieistniejących użytkowników zostaną automatycznie utworzone, jeśli uwierzytelnianie za pomocą systemu zewnętrznego zakończy się sukcesem.',\n    'reg_email_confirmation' => 'Potwierdzenie adresu email',\n    'reg_email_confirmation_toggle' => 'Wymagaj potwierdzenia adresu email',\n    'reg_confirm_email_desc' => 'Jeśli restrykcje domenowe zostały ustawione, potwierdzenie adresu email stanie się konieczne i ta opcja zostanie zignorowana.',\n    'reg_confirm_restrict_domain' => 'Restrykcje domenowe',\n    'reg_confirm_restrict_domain_desc' => 'Wprowadź listę domen adresów email, rozdzieloną przecinkami, którym chciałbyś zezwolić na rejestrację. Wymusi to konieczność potwierdzenia adresu e-mail przez użytkownika przed uzyskaniem dostępu do aplikacji. <br> Pamiętaj, że użytkownicy będą mogli zmienić adres e-mail po rejestracji.',\n    'reg_confirm_restrict_domain_placeholder' => 'Brak restrykcji',\n\n    // Sorting Settings\n    'sorting' => 'Listy i sortowanie',\n    'sorting_book_default' => 'Domyślna reguła sortowania książek',\n    'sorting_book_default_desc' => 'Wybierz domyślną regułę sortowania dla nowych książek. To nie wpłynie na istniejące książki i może być nadpisane per książka.',\n    'sorting_rules' => 'Reguły sortowania',\n    'sorting_rules_desc' => 'Są to wstępnie zdefiniowane operacje sortowania, które mogą być stosowane do treści w systemie.',\n    'sort_rule_assigned_to_x_books' => 'Przypisane do :count książki|Przypisane do :count książek',\n    'sort_rule_create' => 'Utwórz regułę sortowania',\n    'sort_rule_edit' => 'Edytuj regułę sortowania',\n    'sort_rule_delete' => 'Usuń regułę sortowania',\n    'sort_rule_delete_desc' => 'Usuń tę regułę sortowania z systemu. Książki używające tej reguły powrócą do ręcznego sortowania.',\n    'sort_rule_delete_warn_books' => 'Ta reguła sortowania jest obecnie używana na :count książkach. Czy na pewno chcesz ją usunąć?',\n    'sort_rule_delete_warn_default' => 'Ta reguła sortowania jest obecnie używana jako domyślna dla książek. Czy na pewno chcesz ją usunąć?',\n    'sort_rule_details' => 'Szczegóły reguły sortowania',\n    'sort_rule_details_desc' => 'Ustaw nazwę dla tej reguły sortowania, która pojawi się na listach, gdy użytkownicy skorzystają z sortowania.',\n    'sort_rule_operations' => 'Operacje sortowania',\n    'sort_rule_operations_desc' => 'Skonfiguruj akcje sortowanie do wykonania, przenosząc je z listy dostępnych operacji. Po użyciu operacje zostaną zastosowane w kolejności od góry do dołu. Wszelkie zmiany wprowadzone tutaj zostaną zastosowane do wszystkich przypisanych książek po zapisaniu.',\n    'sort_rule_available_operations' => 'Dostępne operacje',\n    'sort_rule_available_operations_empty' => 'Brak pozostałych operacji',\n    'sort_rule_configured_operations' => 'Skonfigurowane operacje',\n    'sort_rule_configured_operations_empty' => 'Przeciągnij/dodaj operacje z listy \"Dostępne Operacje\"',\n    'sort_rule_op_asc' => '(rosnąco)',\n    'sort_rule_op_desc' => '(malejąco)',\n    'sort_rule_op_name' => 'Nazwa — alfabetycznie',\n    'sort_rule_op_name_numeric' => 'Nazwa — numerycznie',\n    'sort_rule_op_created_date' => 'Data utworzenia',\n    'sort_rule_op_updated_date' => 'Data aktualizacji',\n    'sort_rule_op_chapters_first' => 'Rozdziały na początku',\n    'sort_rule_op_chapters_last' => 'Rozdziały na końcu',\n    'sorting_page_limits' => 'Limity wyświetlania per strona',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Konserwacja',\n    'maint_image_cleanup' => 'Czyszczenie obrazków',\n    'maint_image_cleanup_desc' => 'Skanuje zawartość strony i poprzednie wersje, aby sprawdzić, które obrazy i rysunki są aktualnie używane, a które obrazy są zbędne. Przed uruchomieniem tej opcji należy utworzyć pełną kopię zapasową bazy danych i obrazków.',\n    'maint_delete_images_only_in_revisions' => 'Usuń również obrazy, które istnieją tylko w starych rewizjach strony',\n    'maint_image_cleanup_run' => 'Uruchom czyszczenie',\n    'maint_image_cleanup_warning' => 'Znaleziono :count potencjalnie niepotrzebnych obrazków. Czy na pewno chcesz je usunąć?',\n    'maint_image_cleanup_success' => ':count potencjalnie nieużywane obrazki zostały znalezione i usunięte!',\n    'maint_image_cleanup_nothing_found' => 'Nie znaleziono żadnych nieużywanych obrazków. Nic nie zostało usunięte!',\n    'maint_send_test_email' => 'Wyślij testową wiadomość e-mail',\n    'maint_send_test_email_desc' => 'Ta opcja wyśle wiadomość testową na adres e-mail podany w Twoim profilu.',\n    'maint_send_test_email_run' => 'Wyślij testową wiadomość e-mail',\n    'maint_send_test_email_success' => 'E-mail wysłany na adres :address',\n    'maint_send_test_email_mail_subject' => 'E-mail testowy',\n    'maint_send_test_email_mail_greeting' => 'Wygląda na to, że wysyłka wiadomości e-mail działa!',\n    'maint_send_test_email_mail_text' => 'Gratulacje! Otrzymałeś tego e-maila więc Twoje ustawienia poczty elektronicznej wydają się być prawidłowo skonfigurowane.',\n    'maint_recycle_bin_desc' => 'Usunięte półki, książki, rozdziały i strony są wysyłane do kosza, aby mogły zostać przywrócone lub trwale usunięte. Starsze przedmioty w koszu mogą zostać automatycznie usunięte po pewnym czasie w zależności od konfiguracji systemu.',\n    'maint_recycle_bin_open' => 'Otwórz kosz',\n    'maint_regen_references' => 'Zregeneruj odniesienia',\n    'maint_regen_references_desc' => 'Ta akcja przebuduje bazodanowy indeks referencji między pozycjami. Zazwyczaj jest to obsługiwane automatycznie, jednak ta akcja wciąż może być przydatna do indeksowania starej zawartości, lub dodanej nieoficjalnymi metodami.',\n    'maint_regen_references_success' => 'Indeks referencji został zregenerowany!',\n    'maint_timeout_command_note' => 'Uwaga: Ta akcja potrzebuje czasu na wykonanie, co może prowadzić do problemów z limitami czasu utrzymywania połączenia w niektórych środowiskach webowych. Alternatywnie ta akcja może być wykonana z użyciem polecenia terminalowego.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Kosz',\n    'recycle_bin_desc' => 'Tutaj możesz przywrócić elementy, które zostały usunięte lub usunąć je z systemu. Ta lista jest niefiltrowana w odróżnieniu od podobnych list aktywności w systemie, w którym stosowane są filtry uprawnień.',\n    'recycle_bin_deleted_item' => 'Usunięta pozycja',\n    'recycle_bin_deleted_parent' => 'Nadrzędny',\n    'recycle_bin_deleted_by' => 'Usunięty przez',\n    'recycle_bin_deleted_at' => 'Czas usunięcia',\n    'recycle_bin_permanently_delete' => 'Usuń trwale',\n    'recycle_bin_restore' => 'Przywróć',\n    'recycle_bin_contents_empty' => 'Kosz jest pusty',\n    'recycle_bin_empty' => 'Opróżnij kosz',\n    'recycle_bin_empty_confirm' => 'To na stałe zniszczy wszystkie przedmioty w koszu, w tym zawartość w każdym elemencie. Czy na pewno chcesz opróżnić kosz?',\n    'recycle_bin_destroy_confirm' => 'Ta akcja trwale usunie ten element z systemu, wraz z elementami podrzędnymi wymienionymi poniżej i nie będziesz już mógł przywrócić tej zawartości. Czy na pewno chcesz trwale usunąć ten element?',\n    'recycle_bin_destroy_list' => 'Elementy do usunięcia',\n    'recycle_bin_restore_list' => 'Elementy do przywrócenia',\n    'recycle_bin_restore_confirm' => 'Ta akcja przywróci usunięty element, w tym elementy podrzędne, do ich oryginalnej lokalizacji. Jeśli oryginalna lokalizacja została od tego czasu usunięta, a teraz znajduje się w koszu, element nadrzędny będzie również musiał zostać przywrócony.',\n    'recycle_bin_restore_deleted_parent' => 'Usunięto również nadrzędny element. Zostaną one usunięte, dopóki nie przywróci się tego nadrzędnego elementu.',\n    'recycle_bin_restore_parent' => 'Przywróć nadrzędne',\n    'recycle_bin_destroy_notification' => 'Usunięto :count przedmiotów z kosza.',\n    'recycle_bin_restore_notification' => 'Przywrócono :count przedmiotów z kosza.',\n\n    // Audit Log\n    'audit' => 'Dziennik audytu',\n    'audit_desc' => 'Ten dziennik audytu wyświetla listę działań śledzonych w systemie. Ta lista jest niefiltrowana w odróżnieniu od podobnych list aktywności w systemie, w którym stosowane są filtry uprawnień.',\n    'audit_event_filter' => 'Filtry Wydarzeń',\n    'audit_event_filter_no_filter' => 'Brak filtra',\n    'audit_deleted_item' => 'Usunięta pozycja',\n    'audit_deleted_item_name' => 'Nazwa: :name',\n    'audit_table_user' => 'Użytkownik',\n    'audit_table_event' => 'Wydarzenie',\n    'audit_table_related' => 'Powiązany element lub szczegóły',\n    'audit_table_ip' => 'Adres IP',\n    'audit_table_date' => 'Data Aktywności',\n    'audit_date_from' => 'Zakres dat od',\n    'audit_date_to' => 'Zakres dat do',\n\n    // Role Settings\n    'roles' => 'Role',\n    'role_user_roles' => 'Role użytkowników',\n    'roles_index_desc' => 'Role są używane do grupowania użytkowników i udzielania uprawnień systemowych ich członkom. Gdy użytkownik jest członkiem wielu ról, przyznane uprawnienia będą gromadzone, a użytkownik odziedziczy wszystkie możliwości.',\n    'roles_x_users_assigned' => ':count przypisany użytkownik|:count przypisanych użytkowników',\n    'roles_x_permissions_provided' => ':1 uprawnienie|:count uprawnień',\n    'roles_assigned_users' => 'Przypisani Użytkownicy',\n    'roles_permissions_provided' => 'Przyznawane Uprawnienia',\n    'role_create' => 'Utwórz nową rolę',\n    'role_delete' => 'Usuń rolę',\n    'role_delete_confirm' => 'To spowoduje usunięcie roli \\':roleName\\'.',\n    'role_delete_users_assigned' => 'Tę rolę ma przypisanych :userCount użytkowników. Jeśli chcesz zmigrować użytkowników z tej roli, wybierz nową poniżej.',\n    'role_delete_no_migration' => \"Nie migruj użytkowników\",\n    'role_delete_sure' => 'Czy na pewno chcesz usunąć tę rolę?',\n    'role_edit' => 'Edytuj rolę',\n    'role_details' => 'Szczegóły roli',\n    'role_name' => 'Nazwa roli',\n    'role_desc' => 'Krótki opis roli',\n    'role_mfa_enforced' => 'Wymaga uwierzytelniania wieloetapowego',\n    'role_external_auth_id' => 'Zewnętrzne identyfikatory uwierzytelniania',\n    'role_system' => 'Uprawnienia systemowe',\n    'role_manage_users' => 'Zarządzanie użytkownikami',\n    'role_manage_roles' => 'Zarządzanie rolami i uprawnieniami ról',\n    'role_manage_entity_permissions' => 'Zarządzanie uprawnieniami książek, rozdziałów i stron',\n    'role_manage_own_entity_permissions' => 'Zarządzanie uprawnieniami własnych książek, rozdziałów i stron',\n    'role_manage_page_templates' => 'Zarządzaj szablonami stron',\n    'role_access_api' => 'Dostęp do systemowego API',\n    'role_manage_settings' => 'Zarządzanie ustawieniami aplikacji',\n    'role_export_content' => 'Eksportuj zawartość',\n    'role_import_content' => 'Importuj zawartość',\n    'role_editor_change' => 'Zmień edytor strony',\n    'role_notifications' => 'Odbieranie i zarządzanie powiadomieniami',\n    'role_permission_note_users_and_roles' => 'Uprawnienia te mogą zapewnić również widoczność i wyszukiwanie użytkowników i ról w systemie.',\n    'role_asset' => 'Zarządzanie zasobami',\n    'roles_system_warning' => 'Pamiętaj, że dostęp do trzech powyższych uprawnień może pozwolić użytkownikowi na zmianę własnych uprawnień lub uprawnień innych osób w systemie. Przypisz tylko role z tymi uprawnieniami do zaufanych użytkowników.',\n    'role_asset_desc' => 'Te ustawienia kontrolują zarządzanie zasobami systemu. Uprawnienia książek, rozdziałów i stron nadpisują te ustawienia.',\n    'role_asset_admins' => 'Administratorzy mają automatycznie dostęp do wszystkich treści, ale te opcję mogą być pokazywać lub ukrywać opcje interfejsu użytkownika.',\n    'role_asset_image_view_note' => 'To odnosi się do widoczności w ramach menedżera obrazów. Rzeczywista możliwość dostępu do przesłanych plików obrazów będzie zależeć od systemowej opcji przechowywania obrazów.',\n    'role_asset_users_note' => 'Uprawnienia te mogą zapewnić również widoczność i wyszukiwanie użytkowników w systemie.',\n    'role_all' => 'Wszyscy',\n    'role_own' => 'Własne',\n    'role_controlled_by_asset' => 'Kontrolowane przez zasób, do którego zostały udostępnione',\n    'role_save' => 'Zapisz rolę',\n    'role_users' => 'Użytkownicy w tej roli',\n    'role_users_none' => 'Brak użytkowników zapisanych do tej roli',\n\n    // Users\n    'users' => 'Użytkownicy',\n    'users_index_desc' => 'Twórz indywidualne konta użytkowników w systemie i zarządzaj nimi. Konta użytkowników są używane do logowania i przypisywania treści i aktywności. Uprawnienia dostępu są przede wszystkim oparte na roli, ale posiadanie przez użytkownika zawartości, podobnie jak inne czynniki może również wpływać na uprawnienia i dostęp.',\n    'user_profile' => 'Profil użytkownika',\n    'users_add_new' => 'Dodaj użytkownika',\n    'users_search' => 'Wyszukaj użytkownika',\n    'users_latest_activity' => 'Ostatnia aktywność',\n    'users_details' => 'Szczegóły użytkownika',\n    'users_details_desc' => 'Ustaw wyświetlaną nazwę i adres e-mail dla tego użytkownika. Adres e-mail zostanie wykorzystany do zalogowania się do aplikacji.',\n    'users_details_desc_no_email' => 'Ustaw wyświetlaną nazwę dla tego użytkownika, aby inni mogli go rozpoznać.',\n    'users_role' => 'Role użytkownika',\n    'users_role_desc' => 'Wybierz role, do których ten użytkownik zostanie przypisany. Jeśli użytkownik jest przypisany do wielu ról, uprawnienia z tych ról zostaną nałożone i otrzyma wszystkie uprawnienia przypisanych ról.',\n    'users_password' => 'Hasło użytkownika',\n    'users_password_desc' => 'Ustaw hasło logowania do aplikacji. Hasło musi mieć przynajmniej 8 znaków.',\n    'users_send_invite_text' => 'Możesz wybrać wysłanie do tego użytkownika wiadomości e-mail z zaproszeniem, która pozwala mu ustawić własne hasło, w przeciwnym razie możesz ustawić je samemu.',\n    'users_send_invite_option' => 'Wyślij e-mail z zaproszeniem',\n    'users_external_auth_id' => 'Zewnętrzne identyfikatory autentykacji',\n    'users_external_auth_id_desc' => 'Gdy używany jest zewnętrzny system uwierzytelniania (np. SAML2, OIDC lub LDAP), to jest ID, które łączy tego użytkownika BookStack z kontem w systemie uwierzytelniania. Możesz zignorować to pole, jeśli używasz domyślnego uwierzytelniania e-mailem.',\n    'users_password_warning' => 'Wypełnij poniższe tylko, jeśli chcesz zmienić hasło dla tego użytkownika.',\n    'users_system_public' => 'Ten użytkownik reprezentuje każdego gościa odwiedzającego tę aplikację. Nie można się na niego zalogować, lecz jest przyznawany automatycznie.',\n    'users_delete' => 'Usuń użytkownika',\n    'users_delete_named' => 'Usuń :userName',\n    'users_delete_warning' => 'To usunie użytkownika \\':userName\\' z systemu.',\n    'users_delete_confirm' => 'Czy na pewno chcesz usunąć tego użytkownika?',\n    'users_migrate_ownership' => 'Migracja Własności',\n    'users_migrate_ownership_desc' => 'Wybierz użytkownika tutaj, jeśli chcesz, aby inny użytkownik stał się właścicielem wszystkich elementów będących obecnie w posiadaniu tego użytkownika.',\n    'users_none_selected' => 'Nie wybrano użytkownika',\n    'users_edit' => 'Edytuj użytkownika',\n    'users_edit_profile' => 'Edytuj profil',\n    'users_avatar' => 'Avatar użytkownika',\n    'users_avatar_desc' => 'Ten obrazek powinien posiadać wymiary 256x256px.',\n    'users_preferred_language' => 'Preferowany język',\n    'users_preferred_language_desc' => 'Opcja ta zmieni język używany w interfejsie użytkownika aplikacji. Nie wpłynie to na zawartość stworzoną przez użytkownika.',\n    'users_social_accounts' => 'Konta społecznościowe',\n    'users_social_accounts_desc' => 'Zobacz status połączonych kont społecznościowych dla tego użytkownika. Konta społecznościowe mogą być używane jako uzupełnienie podstawowego systemu uwierzytelniania w celu uzyskania dostępu do systemu.',\n    'users_social_accounts_info' => 'Tutaj możesz połączyć kilka kont społecznościowych w celu łatwiejszego i szybszego logowania. Odłączenie konta tutaj nie autoryzowało dostępu. Odwołaj dostęp z ustawień profilu na podłączonym koncie społecznościowym.',\n    'users_social_connect' => 'Podłącz konto',\n    'users_social_disconnect' => 'Odłącz konto',\n    'users_social_status_connected' => 'Połączono',\n    'users_social_status_disconnected' => 'Rozłączono',\n    'users_social_connected' => ':socialAccount zostało dodane do Twojego profilu.',\n    'users_social_disconnected' => ':socialAccount zostało odłączone od Twojego profilu.',\n    'users_api_tokens' => 'Tokeny API',\n    'users_api_tokens_desc' => 'Twórz i zarządzaj tokenami dostępu używanymi do uwierzytelniania z BookStack REST API. Uprawnienia dla API są zarządzane za pośrednictwem użytkownika, do którego należy token.',\n    'users_api_tokens_none' => 'Nie utworzono tokenów API dla tego użytkownika',\n    'users_api_tokens_create' => 'Utwórz token',\n    'users_api_tokens_expires' => 'Wygasa',\n    'users_api_tokens_docs' => 'Dokumentacja API',\n    'users_mfa' => 'Uwierzytelnianie wieloskładnikowe',\n    'users_mfa_desc' => 'Skonfiguruj uwierzytelnianie wieloskładnikowe jako dodatkową warstwę bezpieczeństwa dla swojego konta użytkownika.',\n    'users_mfa_x_methods' => ':count metoda skonfigurowana|:count metody skonfigurowane',\n    'users_mfa_configure' => 'Konfiguruj metody',\n\n    // API Tokens\n    'user_api_token_create' => 'Utwórz klucz API',\n    'user_api_token_name' => 'Nazwa',\n    'user_api_token_name_desc' => 'Nadaj swojemu tokenowi czytelną nazwę jako opisującego jego cel.',\n    'user_api_token_expiry' => 'Data ważności',\n    'user_api_token_expiry_desc' => 'Ustaw datę, kiedy ten token wygasa. Po tej dacie żądania wykonane przy użyciu tego tokenu nie będą już działać. Pozostawienie tego pola pustego, ustawi ważność na 100 lat.',\n    'user_api_token_create_secret_message' => 'Natychmiast po utworzeniu tego tokenu zostanie wygenerowany i wyświetlony \"Identyfikator tokenu\"\" i \"Token Secret\". Sekret zostanie wyświetlony tylko raz, więc przed kontynuacją upewnij się, że zostanie on skopiowany w bezpiecznie miejsce.',\n    'user_api_token' => 'Token API',\n    'user_api_token_id' => 'Token ID',\n    'user_api_token_id_desc' => 'Jest to nieedytowalny identyfikator wygenerowany przez system dla tego tokenu, który musi być dostarczony w żądaniach API.',\n    'user_api_token_secret' => 'Token Api',\n    'user_api_token_secret_desc' => 'To jest wygenerowany przez system sekretny token, który musi być dostarczony w żądaniach API. Token zostanie wyświetlany tylko raz, więc skopiuj go w bezpiecznie miejsce.',\n    'user_api_token_created' => 'Token utworzony :timeAgo',\n    'user_api_token_updated' => 'Token zaktualizowany :timeAgo',\n    'user_api_token_delete' => 'Usuń token',\n    'user_api_token_delete_warning' => 'Spowoduje to całkowite usunięcie tokenu API o nazwie \\':tokenName\\' z systemu.',\n    'user_api_token_delete_confirm' => 'Czy jesteś pewien, że chcesz usunąć ten token?',\n\n    // Webhooks\n    'webhooks' => 'Webhooki',\n    'webhooks_index_desc' => 'Webhooki to sposób na wysyłanie danych do zewnętrznych adresów URL, gdy pewne działania i zdarzenia zachodzą w ramach systemu, co umożliwia integrację zdarzeń w systemie z zewnętrznymi platformami, takimi jak systemy wysyłania wiadomości lub powiadamiania.',\n    'webhooks_x_trigger_events' => ':count zdarzenie wyzwalacza|:count zdarzeń wyzwalacza',\n    'webhooks_create' => 'Utwórz nowy Webhook',\n    'webhooks_none_created' => 'Nie utworzono jeszcze żadnych webhooków.',\n    'webhooks_edit' => 'Edytuj Webhook',\n    'webhooks_save' => 'Zapisz Webhook',\n    'webhooks_details' => 'Szczegóły Webhooka',\n    'webhooks_details_desc' => 'Podaj przyjazną nazwę i punkt końcowy POST jako adres docelowy wysłania dla danych webhooka.',\n    'webhooks_events' => 'Zdarzenia Webhook',\n    'webhooks_events_desc' => 'Zaznacz wszystkie zdarzenia, które powinny wyzwalać wywołanie tego webhooka.',\n    'webhooks_events_warning' => 'Pamiętaj, że te zdarzenia będą wyzwalane dla wszystkich wybranych wydarzeń, nawet jeśli zostaną zastosowane niestandardowe uprawnienia. Upewnij się, że korzystanie z tego webhooka nie spowoduje ujawnienia poufnych treści.',\n    'webhooks_events_all' => 'Wszystkie zdarzenia systemowe',\n    'webhooks_name' => 'Nazwa Webhooka',\n    'webhooks_timeout' => 'Limit czasu żądania Webhooka (w sekundach)',\n    'webhooks_endpoint' => 'Punkt Końcowy Webhooka',\n    'webhooks_active' => 'Webhook Aktywny',\n    'webhook_events_table_header' => 'Zdarzenia',\n    'webhooks_delete' => 'Usuń Webhook',\n    'webhooks_delete_warning' => 'Spowoduje to całkowite usunięcie z systemu tego webhooka o nazwie \\':webhookName\\'.',\n    'webhooks_delete_confirm' => 'Czy na pewno chcesz usunąć ten webhook?',\n    'webhooks_format_example' => 'Przykład Formatu Webhooka',\n    'webhooks_format_example_desc' => 'Dane webhooka są wysyłane jako zapytanie POST do skonfigurowanego punktu końcowego jako JSON zgodnie z poniższym formatem. Właściwości \"related_item\" i \"url\" są opcjonalne i będą zależeć od typu wywołanego zdarzenia.',\n    'webhooks_status' => 'Status Webhooka',\n    'webhooks_last_called' => 'Ostatnio Wyzwolony:',\n    'webhooks_last_errored' => 'Ostatni błąd:',\n    'webhooks_last_error_message' => 'Ostatni komunikat o błędzie:',\n\n    // Licensing\n    'licenses' => 'Licencje',\n    'licenses_desc' => 'Ta strona podaje szczegóły dotyczące licencji dla BookStack w powiązaniu z projektami i bibliotekami używanymi w BookStack. Wiele wymienionych projektów może zezwalać na wykorzystanie wyłącznie w kontekście rozwoju oprogramowania.',\n    'licenses_bookstack' => 'Licencja BookStack',\n    'licenses_php' => 'Licencje bibliotek PHP',\n    'licenses_js' => 'Licencje bibliotek JavaScript',\n    'licenses_other' => 'Inne licencje',\n    'license_details' => 'Szczegóły licencji',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Kataloński',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Estoński',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/pl/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute musi zostać zaakceptowany.',\n    'active_url'           => ':attribute nie jest prawidłowym adresem URL.',\n    'after'                => ':attribute musi być datą następującą po :date.',\n    'alpha'                => ':attribute może zawierać wyłącznie litery.',\n    'alpha_dash'           => ':attribute może zawierać wyłącznie litery, cyfry i myślniki.',\n    'alpha_num'            => ':attribute może zawierać wyłącznie litery i cyfry.',\n    'array'                => ':attribute musi być tablicą.',\n    'backup_codes'         => 'Podany kod jest nieprawidłowy lub został już użyty.',\n    'before'               => ':attribute musi być datą poprzedzającą :date.',\n    'between'              => [\n        'numeric' => ':attribute musi zawierać się w przedziale od :min do :max.',\n        'file'    => 'Waga :attribute musi zawierać się pomiędzy :min i :max kilobajtów.',\n        'string'  => 'Długość :attribute musi zawierać się pomiędzy :min i :max.',\n        'array'   => ':attribute musi mieć od :min do :max elementów.',\n    ],\n    'boolean'              => ':attribute musi być wartością prawda/fałsz.',\n    'confirmed'            => ':attribute i potwierdzenie muszą być zgodne.',\n    'date'                 => ':attribute nie jest prawidłową datą.',\n    'date_format'          => ':attribute musi mieć format :format.',\n    'different'            => ':attribute i :other muszą się różnić.',\n    'digits'               => ':attribute musi mieć :digits cyfr.',\n    'digits_between'       => ':attribute musi mieć od :min do :max cyfr.',\n    'email'                => ':attribute musi być prawidłowym adresem e-mail.',\n    'ends_with' => ':attribute musi kończyć się jedną z poniższych wartości: :values',\n    'file'                 => ':attribute musi być prawidłowym plikiem.',\n    'filled'               => ':attribute jest wymagany.',\n    'gt'                   => [\n        'numeric' => ':attribute musi być większy niż :value.',\n        'file'    => ':attribute musi mieć rozmiar większy niż :value kilobajtów.',\n        'string'  => ':attribute musi mieć więcej niż :value znaków.',\n        'array'   => ':attribute musi mieć więcej niż :value elementów.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute musi być większy lub równy :value.',\n        'file'    => ':attribute musi mieć rozmiar większy niż lub równy :value kilobajtów.',\n        'string'  => ':attribute musi mieć :value lub więcej znaków.',\n        'array'   => ':attribute musi mieć :value lub więcej elementów.',\n    ],\n    'exists'               => 'Wybrana wartość :attribute jest nieprawidłowa.',\n    'image'                => ':attribute musi być obrazkiem.',\n    'image_extension'      => ':attribute musi mieć prawidłowe i wspierane rozszerzenie',\n    'in'                   => 'Wybrana wartość :attribute jest nieprawidłowa.',\n    'integer'              => ':attribute musi być liczbą całkowitą.',\n    'ip'                   => ':attribute musi być prawidłowym adresem IP.',\n    'ipv4'                 => ':attribute musi być prawidłowym adresem IPv4.',\n    'ipv6'                 => ':attribute musi być prawidłowym adresem IPv6.',\n    'json'                 => ':attribute musi być prawidłowym ciągiem JSON.',\n    'lt'                   => [\n        'numeric' => ':attribute musi być mniejszy niż :value.',\n        'file'    => ':attribute musi mieć rozmiar mniejszy niż :value kilobajtów.',\n        'string'  => ':attribute musi mieć mniej niż :value znaków.',\n        'array'   => ':attribute musi mieć mniej niż :value elementów.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute musi być mniejszy lub równy :value.',\n        'file'    => ':attribute musi mieć rozmiar mniejszy lub równy:value kilobajtów.',\n        'string'  => ':attribute nie może mieć więcej niż :value znaków.',\n        'array'   => ':attribute nie może mieć więcej niż  :value elementów.',\n    ],\n    'max'                  => [\n        'numeric' => 'Wartość :attribute nie może być większa niż :max.',\n        'file'    => 'Wielkość :attribute nie może być większa niż :max kilobajtów.',\n        'string'  => 'Długość :attribute nie może być większa niż :max znaków.',\n        'array'   => 'Rozmiar :attribute nie może być większy niż :max elementów.',\n    ],\n    'mimes'                => ':attribute musi być plikiem typu: :values.',\n    'min'                  => [\n        'numeric' => 'Wartość :attribute nie może być mniejsza od :min.',\n        'file'    => 'Wielkość :attribute nie może być mniejsza niż :min kilobajtów.',\n        'string'  => 'Długość :attribute nie może być mniejsza niż :min znaków.',\n        'array'   => 'Rozmiar :attribute musi posiadać co najmniej :min elementy.',\n    ],\n    'not_in'               => 'Wartość :attribute jest nieprawidłowa.',\n    'not_regex'            => 'Format :attribute jest nieprawidłowy.',\n    'numeric'              => ':attribute musi być liczbą.',\n    'regex'                => 'Format :attribute jest nieprawidłowy.',\n    'required'             => 'Pole :attribute jest wymagane.',\n    'required_if'          => 'Pole :attribute jest wymagane jeśli :other ma wartość :value.',\n    'required_with'        => 'Pole :attribute jest wymagane jeśli :values zostało wprowadzone.',\n    'required_with_all'    => 'Pole :attribute jest wymagane jeśli :values są obecne.',\n    'required_without'     => 'Pole :attribute jest wymagane jeśli :values nie zostało wprowadzone.',\n    'required_without_all' => 'Pole :attribute jest wymagane jeśli żadna z wartości :values nie została podana.',\n    'same'                 => 'Pole :attribute i :other muszą być takie same.',\n    'safe_url'             => 'Podany link może nie być bezpieczny.',\n    'size'                 => [\n        'numeric' => ':attribute musi mieć długość :size.',\n        'file'    => ':attribute musi mieć :size kilobajtów.',\n        'string'  => ':attribute mmusi mieć długość :size znaków.',\n        'array'   => ':attribute musi posiadać :size elementów.',\n    ],\n    'string'               => ':attribute musi być ciągiem znaków.',\n    'timezone'             => ':attribute musi być prawidłową strefą czasową.',\n    'totp'                 => 'Podany kod jest nieprawidłowy lub wygasł.',\n    'unique'               => ':attribute zostało już zajęte.',\n    'url'                  => 'Format :attribute jest nieprawidłowy.',\n    'uploaded'             => 'Plik nie może zostać wysłany. Serwer nie akceptuje plików o takim rozmiarze.',\n\n    'zip_file' => ':attribute musi odnosić się do pliku w archiwum ZIP.',\n    'zip_file_size' => 'Plik :attribute nie może przekraczać :size MB.',\n    'zip_file_mime' => ':attribute musi odnosić się do pliku typu :validTypes. Znaleziono :foundType.',\n    'zip_model_expected' => 'Oczekiwano obiektu danych, ale znaleziono \":type\".',\n    'zip_unique' => ':attribute musi być unikalny dla typu obiektu w archiwum ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Potwierdzenie hasła jest wymagane.',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/pt/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'criou a página',\n    'page_create_notification'    => 'Página criada com sucesso',\n    'page_update'                 => 'página atualizada',\n    'page_update_notification'    => 'Página atualizada com sucesso.',\n    'page_delete'                 => 'página eliminada',\n    'page_delete_notification'    => 'Página excluída com sucesso.',\n    'page_restore'                => 'página restaurada',\n    'page_restore_notification'   => 'Página restaurada com sucesso',\n    'page_move'                   => 'página movida',\n    'page_move_notification'      => 'Página movida com sucesso',\n\n    // Chapters\n    'chapter_create'              => 'capítulo criado',\n    'chapter_create_notification' => 'Capítulo criado com sucesso',\n    'chapter_update'              => 'capítulo atualizado',\n    'chapter_update_notification' => 'Capítulo atualizado com sucesso',\n    'chapter_delete'              => 'capítulo excluído',\n    'chapter_delete_notification' => 'Capítulo excluído com sucesso',\n    'chapter_move'                => 'capítulo movido',\n    'chapter_move_notification' => 'Capítulo movido com sucesso',\n\n    // Books\n    'book_create'                 => 'livro criado',\n    'book_create_notification'    => 'Livro criado com sucesso',\n    'book_create_from_chapter'              => 'capítulo convertido para livro',\n    'book_create_from_chapter_notification' => 'Capítulo convertido em livro com sucesso',\n    'book_update'                 => 'livro atualizado',\n    'book_update_notification'    => 'Livro atualizado com sucesso',\n    'book_delete'                 => 'livro eliminado',\n    'book_delete_notification'    => 'Livro eliminado com sucesso',\n    'book_sort'                   => 'livro ordenado',\n    'book_sort_notification'      => 'Livro reordenado com sucesso',\n\n    // Bookshelves\n    'bookshelf_create'            => 'estante criada',\n    'bookshelf_create_notification'    => 'Estante criada com sucesso',\n    'bookshelf_create_from_book'    => 'livro convertido para estante',\n    'bookshelf_create_from_book_notification'    => 'Livro convertido em prateleira com sucesso',\n    'bookshelf_update'                 => 'estante atualizada',\n    'bookshelf_update_notification'    => 'Estante atualizada com sucesso',\n    'bookshelf_delete'                 => 'excluiu a estante',\n    'bookshelf_delete_notification'    => 'Estante eliminada com sucesso',\n\n    // Revisions\n    'revision_restore' => 'restaurou a revisão',\n    'revision_delete' => 'eliminou a revisão',\n    'revision_delete_notification' => 'Revisão eliminada com sucesso',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" foi adicionado aos seus favoritos',\n    'favourite_remove_notification' => '\":name\" foi removido dos seus favoritos',\n\n    // Watching\n    'watch_update_level_notification' => 'Ver preferências atualizadas com sucesso',\n\n    // Auth\n    'auth_login' => 'iniciou sessão',\n    'auth_register' => 'registado como novo utilizador',\n    'auth_password_reset_request' => 'pedido a redefinição da palavra-passe',\n    'auth_password_reset_update' => 'redifinir palavra-passe do utilizador',\n    'mfa_setup_method' => 'configurar método de duplo fator',\n    'mfa_setup_method_notification' => 'Método de autenticação por múltiplos-fatores configurado com sucesso',\n    'mfa_remove_method' => 'método de duplo fator removido',\n    'mfa_remove_method_notification' => 'Método de autenticação por múltiplos-fatores removido com sucesso',\n\n    // Settings\n    'settings_update' => 'configurações atualizadas',\n    'settings_update_notification' => 'Configurações atualizadas com sucesso',\n    'maintenance_action_run' => 'ação de manutenção executada',\n\n    // Webhooks\n    'webhook_create' => 'webhook criado',\n    'webhook_create_notification' => 'Webhook criado com sucesso',\n    'webhook_update' => 'atualizar um webhook',\n    'webhook_update_notification' => 'Webhook criado com sucesso',\n    'webhook_delete' => 'eliminar webhook',\n    'webhook_delete_notification' => 'Webhook criado com sucesso',\n\n    // Imports\n    'import_create' => 'importação criada',\n    'import_create_notification' => 'Importação carregada com sucesso',\n    'import_run' => 'importação atualizada',\n    'import_run_notification' => 'Conteúdo importado com sucesso',\n    'import_delete' => 'importação apagada',\n    'import_delete_notification' => 'Importação eliminada com sucesso',\n\n    // Users\n    'user_create' => 'utilizador criado',\n    'user_create_notification' => 'Utilizador criado com sucesso',\n    'user_update' => 'utilizador atualizado',\n    'user_update_notification' => 'Utilizador atualizado com sucesso',\n    'user_delete' => 'utilizador eliminado',\n    'user_delete_notification' => 'Utilizador removido com sucesso',\n\n    // API Tokens\n    'api_token_create' => 'token API criado',\n    'api_token_create_notification' => 'API token criado com sucesso',\n    'api_token_update' => 'token API atualizado',\n    'api_token_update_notification' => 'API token atualizado com sucesso',\n    'api_token_delete' => 'token API apagado',\n    'api_token_delete_notification' => 'API token atualizado com sucesso',\n\n    // Roles\n    'role_create' => 'cargo criado',\n    'role_create_notification' => 'Cargo criado com sucesso',\n    'role_update' => 'cargo atualizado',\n    'role_update_notification' => 'Cargo atualizado com sucesso',\n    'role_delete' => 'cargo eliminado',\n    'role_delete_notification' => 'Cargo excluído com sucesso',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'reciclagem vazia',\n    'recycle_bin_restore' => 'restaurado da reciclagem',\n    'recycle_bin_destroy' => 'removido da reciclagem',\n\n    // Comments\n    'commented_on'                => 'comentado a',\n    'comment_create'              => 'comentário adicionado',\n    'comment_update'              => 'comentário atualizado',\n    'comment_delete'              => 'comentário eliminado',\n\n    // Sort Rules\n    'sort_rule_create' => 'regra de ordenação criada',\n    'sort_rule_create_notification' => 'Regra de ordenação criada com sucesso',\n    'sort_rule_update' => 'regra de ordenação atualizada',\n    'sort_rule_update_notification' => 'Regra de ordenação atualizada com sucesso',\n    'sort_rule_delete' => 'regra de ordenação apagada',\n    'sort_rule_delete_notification' => 'Regra de ordenação apagada com sucesso',\n\n    // Other\n    'permissions_update'          => 'permissões atualizadas',\n];\n"
  },
  {
    "path": "lang/pt/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Estas credenciais não coincidem com os nossos registos.',\n    'throttle' => 'Demasiadas tentativas de acesso. Tente novamente em :seconds segundos.',\n\n    // Login & Register\n    'sign_up' => 'Registar',\n    'log_in' => 'Iniciar sessão',\n    'log_in_with' => 'Iniciar sessão com :socialDriver',\n    'sign_up_with' => 'Criar conta com :socialDriver',\n    'logout' => 'Terminar sessão',\n\n    'name' => 'Nome',\n    'username' => 'Nome de utilizador',\n    'email' => 'E-mail',\n    'password' => 'Palavra-passe',\n    'password_confirm' => 'Confirmar Palavra-passe',\n    'password_hint' => 'Deve ter no mínimo 8 caracteres',\n    'forgot_password' => 'Esqueceu-se da palavra-passe?',\n    'remember_me' => 'Lembrar-se de mim',\n    'ldap_email_hint' => 'Por favor insira um endereço de e-mail para esta conta.',\n    'create_account' => 'Criar Conta',\n    'already_have_account' => 'Já possui uma conta?',\n    'dont_have_account' => 'Não possui uma conta?',\n    'social_login' => 'Inicio de Sessão com Redes Sociais',\n    'social_registration' => 'Registo com Redes Sociais',\n    'social_registration_text' => 'Registe e inicie sessão com recurso a outro serviço.',\n\n    'register_thanks' => 'Obrigado por se registar!',\n    'register_confirm' => 'Por favor, verifique o seu e-mail e carregue no botão de confirmação para aceder :appName.',\n    'registrations_disabled' => 'Os registos estão temporariamente desativados',\n    'registration_email_domain_invalid' => 'O domínio de e-mail usado não tem acesso permitido a esta aplicação',\n    'register_success' => 'Obrigado por se registar! Você está agora registado e com a sessão iniciada.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Tentando inicar sessão',\n    'auto_init_starting_desc' => 'Estamos a aceder ao seu sistema de autenticação para iniciar o processo de login. Se não houver progresso após 5 segundos você pode tentar clicar no link abaixo.',\n    'auto_init_start_link' => 'Prosseguir com autenticação',\n\n    // Password Reset\n    'reset_password' => 'Redefinir Senha',\n    'reset_password_send_instructions' => 'Insira o seu endereço de e-mail abaixo, e uma mensagem com o link de redefinição de palavra-passe será lhe enviada.',\n    'reset_password_send_button' => 'Enviar o Link de Redefinição',\n    'reset_password_sent' => 'Um link de redefinição de palavra-passe será enviado para :email, se o endereço de e-mail for encontrado no sistema.',\n    'reset_password_success' => 'A sua palavra-passe foi redefinida com sucesso.',\n    'email_reset_subject' => 'Redefina a sua palavra-passe de :appName',\n    'email_reset_text' => 'Você recebeu este e-mail pois recebemos uma solicitação de redefinição de senha para a sua conta.',\n    'email_reset_not_requested' => 'Caso não tenha sido você a solicitar a redefinição de senha, ignore este e-mail.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Confirme o seu endereço de e-mail para :appName',\n    'email_confirm_greeting' => 'Obrigado por se registar em :appName!',\n    'email_confirm_text' => 'Por favor, confirme o seu endereço de e-mail ao carregar no botão abaixo:',\n    'email_confirm_action' => 'Confirmar E-mail',\n    'email_confirm_send_error' => 'A confirmação do endereço de e-mail é requerida, mas o sistema não pôde enviar a mensagem. Por favor, entre em contacto com o administrador para se certificar que o serviço de envio de e-mails está corretamente configurado.',\n    'email_confirm_success' => 'O seu endereço de email foi confirmado! Neste momento já poderá entrar usando este endereço de email.',\n    'email_confirm_resent' => 'E-mail de confirmação reenviado. Por favor, verifique a sua caixa de entrada.',\n    'email_confirm_thanks' => 'Obrigado por confirmar!',\n    'email_confirm_thanks_desc' => 'Por favor, aguarde um momento enquanto a sua confirmação é tratada. Se não for redirecionado após 3 segundos pressione \"Continuar\" para prosseguir.',\n\n    'email_not_confirmed' => 'Endereço de E-mail Não Confirmado',\n    'email_not_confirmed_text' => 'O seu endereço de e-mail ainda não foi confirmado.',\n    'email_not_confirmed_click_link' => 'Por favor, carregue no link que se encontra no e-mail que lhe foi enviado após o seu registo.',\n    'email_not_confirmed_resend' => 'Caso não encontre o e-mail poderá reenviar a confirmação utilizando o formulário abaixo.',\n    'email_not_confirmed_resend_button' => 'Reenviar o E-mail de Confirmação',\n\n    // User Invite\n    'user_invite_email_subject' => 'Você recebeu um convite para se juntar a :appName!',\n    'user_invite_email_greeting' => 'Uma conta foi criada para si em :appName.',\n    'user_invite_email_text' => 'Carregue no botão abaixo para definir uma palavra-passe de conta e obter acesso:',\n    'user_invite_email_action' => 'Defina a Palavra-passe da Conta',\n    'user_invite_page_welcome' => 'Bem-vindo(a) a :appName!',\n    'user_invite_page_text' => 'Para finalizar a sua conta e obter acesso, precisa de definir uma senha que será utilizada para efetuar login em :appName em visitas futuras.',\n    'user_invite_page_confirm_button' => 'Confirmar Palavra-Passe',\n    'user_invite_success_login' => 'Palavra passe definida, agora poderá entrar usado a sua nova palavra passe para acessar :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Configurar autenticação de múltiplos fatores',\n    'mfa_setup_desc' => 'Configure a autenticação multi-fatores como uma camada extra de segurança para sua conta de utilizador.',\n    'mfa_setup_configured' => 'Já configurado',\n    'mfa_setup_reconfigure' => 'Reconfigurar',\n    'mfa_setup_remove_confirmation' => 'Tem a certeza que deseja remover este método de autenticação de múltiplos fatores?',\n    'mfa_setup_action' => 'Configuração',\n    'mfa_backup_codes_usage_limit_warning' => 'Você tem menos de 5 códigos de backup restantes, Por favor, gere e armazene um novo conjunto antes de esgotar os códigos para evitar estar bloqueado para fora da sua conta.',\n    'mfa_option_totp_title' => 'Aplicação móvel',\n    'mfa_option_totp_desc' => 'Para usar a autenticação multi-fator, você precisa de uma aplicação móvel que suporte TOTP como o Autenticador do Google, Authy ou o autenticador Microsoft.',\n    'mfa_option_backup_codes_title' => 'Códigos de Backup',\n    'mfa_option_backup_codes_desc' => 'Gera um conjunto de códigos de reserva de utilização única, que deverá usar no início de sessão para verificar a sua identidade. Certifique-se de que os guarda num local seguro.',\n    'mfa_gen_confirm_and_enable' => 'Confirmar e ativar',\n    'mfa_gen_backup_codes_title' => 'Configuração dos Códigos de Backup',\n    'mfa_gen_backup_codes_desc' => 'Armazene a lista de códigos abaixo em um lugar seguro. Ao acessar o sistema você poderá usar um dos códigos como um segundo mecanismo de autenticação.',\n    'mfa_gen_backup_codes_download' => 'Transferir códigos',\n    'mfa_gen_backup_codes_usage_warning' => 'Cada código só pode ser usado uma vez',\n    'mfa_gen_totp_title' => 'Configuração da aplicação móvel',\n    'mfa_gen_totp_desc' => 'Para usar a autenticação multi-fator, precisará de uma aplicação móvel que suporte TOTP como o Autenticador do Google, Authy ou o autenticador Microsoft.',\n    'mfa_gen_totp_scan' => 'Leia o código QR abaixo usando a sua aplicação de autenticação preferida para começar.',\n    'mfa_gen_totp_verify_setup' => 'Verificar configuração',\n    'mfa_gen_totp_verify_setup_desc' => 'Verifique se funciona tudo, digitando um código, gerado dentro da sua aplicação de autenticação, na caixa de entrada abaixo:',\n    'mfa_gen_totp_provide_code_here' => 'Forneça aqui, o código gerado pela sua aplicação',\n    'mfa_verify_access' => 'Verificar Acesso',\n    'mfa_verify_access_desc' => 'Sua conta de usuário requer que você confirme sua identidade por meio de um nível adicional de verificação antes de conceder o acesso. Verifique o uso de um dos métodos configurados para continuar.',\n    'mfa_verify_no_methods' => 'Nenhum método configurado',\n    'mfa_verify_no_methods_desc' => 'Nenhum método de autenticação de vários fatores foi encontrado para a sua conta. Você precisará configurar pelo menos um método antes de ganhar acesso.',\n    'mfa_verify_use_totp' => 'Verificar usando uma aplicação móvel',\n    'mfa_verify_use_backup_codes' => 'Verificar usando código de backup',\n    'mfa_verify_backup_code' => 'Código de backup',\n    'mfa_verify_backup_code_desc' => 'Insira um dos seus códigos de backup restantes abaixo:',\n    'mfa_verify_backup_code_enter_here' => 'Insira o código de backup aqui',\n    'mfa_verify_totp_desc' => 'Digite abaixo, o código gerado através da sua aplicação móvel:',\n    'mfa_setup_login_notification' => 'Método de multi-fatores configurado, por favor faça login novamente usando o método configurado.',\n];\n"
  },
  {
    "path": "lang/pt/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Cancelar',\n    'close' => 'Fechar',\n    'confirm' => 'Confirmar',\n    'back' => 'Voltar',\n    'save' => 'Guardar',\n    'continue' => 'Continuar',\n    'select' => 'Selecionar',\n    'toggle_all' => 'Alternar Todos',\n    'more' => 'Mais',\n\n    // Form Labels\n    'name' => 'Nome',\n    'description' => 'Descrição',\n    'role' => 'Cargo',\n    'cover_image' => 'Imagem de capa',\n    'cover_image_description' => 'Esta imagem deve ser de aproximadamente 440x250px, embora seja escalada de forma flexível e cortada para caber na interface do usuário em diferentes cenários conforme necessário, então, dimensões atuais para exibição serão diferentes.',\n\n    // Actions\n    'actions' => 'Ações',\n    'view' => 'Visualizar',\n    'view_all' => 'Visualizar Todos',\n    'new' => 'Novo',\n    'create' => 'Criar',\n    'update' => 'Atualizar',\n    'edit' => 'Editar',\n    'archive' => 'Archive',\n    'unarchive' => 'Un-Archive',\n    'sort' => 'Ordenar',\n    'move' => 'Mover',\n    'copy' => 'Copiar',\n    'reply' => 'Responder',\n    'delete' => 'Eliminar',\n    'delete_confirm' => 'Confirmar eliminação',\n    'search' => 'Pesquisar',\n    'search_clear' => 'Limpar Pesquisa',\n    'reset' => 'Redefinir',\n    'remove' => 'Remover',\n    'add' => 'Adicionar',\n    'configure' => 'Configurar',\n    'manage' => 'Gerir',\n    'fullscreen' => 'Ecrã completo',\n    'favourite' => 'Favorito',\n    'unfavourite' => 'Retirar Favorito',\n    'next' => 'Próximo',\n    'previous' => 'Anterior',\n    'filter_active' => 'Filtro Ativo:',\n    'filter_clear' => 'Limpar Filtro',\n    'download' => 'Transferir',\n    'open_in_tab' => 'Abrir em novo separador',\n    'open' => 'Abrir',\n\n    // Sort Options\n    'sort_options' => 'Opções de Ordenação',\n    'sort_direction_toggle' => 'Alternar Direção de Ordenação',\n    'sort_ascending' => 'Ordenação Crescente',\n    'sort_descending' => 'Ordenação Decrescente',\n    'sort_name' => 'Nome',\n    'sort_default' => 'Padrão',\n    'sort_created_at' => 'Data de Criação',\n    'sort_updated_at' => 'Data de Atualização',\n\n    // Misc\n    'deleted_user' => 'Utilizador Eliminado',\n    'no_activity' => 'Nenhuma atividade a mostrar',\n    'no_items' => 'Nenhum item disponível',\n    'back_to_top' => 'Voltar ao topo',\n    'skip_to_main_content' => 'Avançar para o conteúdo principal',\n    'toggle_details' => 'Alternar Detalhes',\n    'toggle_thumbnails' => 'Alternar Miniaturas',\n    'details' => 'Detalhes',\n    'grid_view' => 'Visualização em Grade',\n    'list_view' => 'Visualização em Lista',\n    'default' => 'Padrão',\n    'breadcrumb' => 'Caminho',\n    'status' => 'Estado',\n    'status_active' => 'Ativo',\n    'status_inactive' => 'Inativo',\n    'never' => 'Nunca',\n    'none' => 'Nenhum',\n\n    // Header\n    'homepage' => 'Página inicial',\n    'header_menu_expand' => 'Expandir Menu de Cabeçalho',\n    'profile_menu' => 'Menu de Perfil',\n    'view_profile' => 'Visualizar Perfil',\n    'edit_profile' => 'Editar Perfil',\n    'dark_mode' => 'Modo Escuro',\n    'light_mode' => 'Modo Claro',\n    'global_search' => 'Pesquisa global',\n\n    // Layout tabs\n    'tab_info' => 'Informações',\n    'tab_info_label' => 'Separador: Mostrar Informação Secundária',\n    'tab_content' => 'Conteúdo',\n    'tab_content_label' => 'Separador: Mostrar Conteúdo Primário',\n\n    // Email Content\n    'email_action_help' => 'Se estiver com problemas ao carregar no botão \":actionText\", copie e cole o URL abaixo no seu navegador:',\n    'email_rights' => 'Todos os direitos reservados',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Política de Privacidade',\n    'terms_of_service' => 'Termos de Utilização',\n\n    // OpenSearch\n    'opensearch_description' => 'Procurar :appName',\n];\n"
  },
  {
    "path": "lang/pt/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Selecionar Imagem',\n    'image_list' => 'Lista de Imagens',\n    'image_details' => 'Detalhes da Imagem',\n    'image_upload' => 'Carregar Imagem',\n    'image_intro' => 'Aqui pode selecionar e gerir imagens que foram previamente enviadas para o sistema.',\n    'image_intro_upload' => 'Envie uma nova imagem, arrastando um arquivo de imagem para esta janela, ou usando o botão \"Enviar Imagem\" acima.',\n    'image_all' => 'Todas',\n    'image_all_title' => 'Visualizar todas as imagens',\n    'image_book_title' => 'Visualizar imagens relacionadas a este livro',\n    'image_page_title' => 'Visualizar imagens relacionadas a esta página',\n    'image_search_hint' => 'Pesquisar imagem por nome',\n    'image_uploaded' => 'Adicionada em :uploadedDate',\n    'image_uploaded_by' => 'Carregado por :userName',\n    'image_uploaded_to' => 'Carregado para :pageLink',\n    'image_updated' => 'Atualizado :updateDate',\n    'image_load_more' => 'Carregar Mais',\n    'image_image_name' => 'Nome da Imagem',\n    'image_delete_used' => 'Esta imagem é utilizada nas páginas abaixo.',\n    'image_delete_confirm_text' => 'Tem certeza de que deseja eliminar esta imagem?',\n    'image_select_image' => 'Selecionar Imagem',\n    'image_dropzone' => 'Arraste imagens ou carregue aqui para fazer upload',\n    'image_dropzone_drop' => 'Arraste para aqui um ficheiro para carregar',\n    'images_deleted' => 'Imagens Eliminadas',\n    'image_preview' => 'Pré-visualização de Imagem',\n    'image_upload_success' => 'Carregamento da imagem efetuado com sucesso',\n    'image_update_success' => 'Detalhes da imagem atualizados com sucesso',\n    'image_delete_success' => 'Imagem eliminada com sucesso',\n    'image_replace' => 'Substituir Imagem',\n    'image_replace_success' => 'Imagem carregada com sucesso',\n    'image_rebuild_thumbs' => 'Recriar Variação de Tamanho',\n    'image_rebuild_thumbs_success' => 'Variações de tamanho da imagem reconstruídas com sucesso!',\n\n    // Code Editor\n    'code_editor' => 'Editar Código',\n    'code_language' => 'Linguagem do Código',\n    'code_content' => 'Código',\n    'code_session_history' => 'Histórico de Sessão',\n    'code_save' => 'Guardar Código',\n];\n"
  },
  {
    "path": "lang/pt/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Geral',\n    'advanced' => 'Avançado',\n    'none' => 'Nenhum',\n    'cancel' => 'Cancelar',\n    'save' => 'Guardar',\n    'close' => 'Fechar',\n    'apply' => 'Apply',\n    'undo' => 'Anular',\n    'redo' => 'Refazer',\n    'left' => 'Esquerda',\n    'center' => 'Centro',\n    'right' => 'Direita',\n    'top' => 'Topo',\n    'middle' => 'Meio',\n    'bottom' => 'Fundo',\n    'width' => 'Largura',\n    'height' => 'Altura',\n    'More' => 'Mais',\n    'select' => 'Selecionar...',\n\n    // Toolbar\n    'formats' => 'Formatos',\n    'header_large' => 'Cabeçalho grande',\n    'header_medium' => 'Cabeçalho médio',\n    'header_small' => 'Cabeçalho pequeno',\n    'header_tiny' => 'Cabeçalho minúsculo',\n    'paragraph' => 'Parágrafo',\n    'blockquote' => 'Citação',\n    'inline_code' => 'Código embutido',\n    'callouts' => 'Balões',\n    'callout_information' => 'Informação',\n    'callout_success' => 'Sucesso',\n    'callout_warning' => 'Aviso',\n    'callout_danger' => 'Perigo',\n    'bold' => 'Negrito',\n    'italic' => 'Itálico',\n    'underline' => 'Sublinhado',\n    'strikethrough' => 'Rasurado',\n    'superscript' => 'Superior à linha',\n    'subscript' => 'Inferior à linha',\n    'text_color' => 'Cor do texto',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Cor personalizada',\n    'remove_color' => 'Remover cor',\n    'background_color' => 'Cor de fundo',\n    'align_left' => 'Alinhar à esquerda',\n    'align_center' => 'Alinhar ao centro',\n    'align_right' => 'Alinhar à direita',\n    'align_justify' => 'Justificar',\n    'list_bullet' => 'Lista com marcadores',\n    'list_numbered' => 'Lista numerada',\n    'list_task' => 'Lista de tarefas',\n    'indent_increase' => 'Aumentar recuo',\n    'indent_decrease' => 'Diminuir recuo',\n    'table' => 'Tabela',\n    'insert_image' => 'Inserir imagem',\n    'insert_image_title' => 'Inserir/Editar imagem',\n    'insert_link' => 'Inserir/editar link',\n    'insert_link_title' => 'Inserir/Editar link',\n    'insert_horizontal_line' => 'Inserir linha horizontal',\n    'insert_code_block' => 'Inserir código fonte',\n    'edit_code_block' => 'Inserir código fonte',\n    'insert_drawing' => 'Inserir/editar desenho',\n    'drawing_manager' => 'Gestor de desenho',\n    'insert_media' => 'Inserir/editar mídia',\n    'insert_media_title' => 'Inserir/Editar Mídia',\n    'clear_formatting' => 'Limpar formatação',\n    'source_code' => 'Código fonte',\n    'source_code_title' => 'Código Fonte',\n    'fullscreen' => 'Ecrã completo',\n    'image_options' => 'Opções da imagem',\n\n    // Tables\n    'table_properties' => 'Propriedades da tabela',\n    'table_properties_title' => 'Propriedades da Tabela',\n    'delete_table' => 'Eliminar tabela',\n    'table_clear_formatting' => 'Clear table formatting',\n    'resize_to_contents' => 'Resize to contents',\n    'row_header' => 'Row header',\n    'insert_row_before' => 'Inserir linha antes',\n    'insert_row_after' => 'Inserir linha depois',\n    'delete_row' => 'Eliminar linha',\n    'insert_column_before' => 'Insira coluna antes',\n    'insert_column_after' => 'Inserir coluna depois',\n    'delete_column' => 'Eliminar coluna',\n    'table_cell' => 'Célula',\n    'table_row' => 'Linha',\n    'table_column' => 'Coluna',\n    'cell_properties' => 'Propriedades da célula',\n    'cell_properties_title' => 'Propriedades da Célula',\n    'cell_type' => 'Tipo de célula',\n    'cell_type_cell' => 'Célula',\n    'cell_scope' => 'Âmbito',\n    'cell_type_header' => 'Célula do cabeçalho',\n    'merge_cells' => 'Unir células',\n    'split_cell' => 'Dividir célula',\n    'table_row_group' => 'Grupo de linhas',\n    'table_column_group' => 'Grupo de colunas',\n    'horizontal_align' => 'Alinhamento horizontal',\n    'vertical_align' => 'Alinhamento vertical',\n    'border_width' => 'Largura da borda',\n    'border_style' => 'Estilo da borda',\n    'border_color' => 'Cor da borda',\n    'row_properties' => 'Propriedades da célula',\n    'row_properties_title' => 'Propriedades da Célula',\n    'cut_row' => 'Cortar linha',\n    'copy_row' => 'Copiar linha',\n    'paste_row_before' => 'Colar linha antes',\n    'paste_row_after' => 'Colar linha depois',\n    'row_type' => 'Tipo de linha',\n    'row_type_header' => 'Cabeçalho',\n    'row_type_body' => 'Corpo',\n    'row_type_footer' => 'Rodapé',\n    'alignment' => 'Alinhamento',\n    'cut_column' => 'Cortar coluna',\n    'copy_column' => 'Copiar coluna',\n    'paste_column_before' => 'Colar coluna antes',\n    'paste_column_after' => 'Colar coluna depois',\n    'cell_padding' => 'Espaçamento da célula',\n    'cell_spacing' => 'Espaçamento entre células',\n    'caption' => 'Legenda',\n    'show_caption' => 'Mostrar legenda',\n    'constrain' => 'Restringir proporções',\n    'cell_border_solid' => 'Sólido',\n    'cell_border_dotted' => 'Pontilhado',\n    'cell_border_dashed' => 'Tracejado',\n    'cell_border_double' => 'Dupla',\n    'cell_border_groove' => 'Groove',\n    'cell_border_ridge' => 'Ridge',\n    'cell_border_inset' => 'Interna',\n    'cell_border_outset' => 'Externa',\n    'cell_border_none' => 'Nenhuma',\n    'cell_border_hidden' => 'Ocultada',\n\n    // Images, links, details/summary & embed\n    'source' => 'Fonte',\n    'alt_desc' => 'Descrição alternativa',\n    'embed' => 'Incorporar',\n    'paste_embed' => 'Cole seu código abaixo:',\n    'url' => 'URL',\n    'text_to_display' => 'Texto a ser exibido',\n    'title' => 'Título',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Abrir ligação',\n    'open_link_in' => 'Abrir ligação em...',\n    'open_link_current' => 'Janela atual',\n    'open_link_new' => 'Nova janela',\n    'remove_link' => 'Remover ligação',\n    'insert_collapsible' => 'Inserir bloco colapsável',\n    'collapsible_unwrap' => 'Unwrap',\n    'edit_label' => 'Editar etiqueta',\n    'toggle_open_closed' => 'Alternar aberto/fechado',\n    'collapsible_edit' => 'Editar bloco colapsável',\n    'toggle_label' => 'Alternar etiqueta',\n\n    // About view\n    'about' => 'Sobre o editor',\n    'about_title' => 'Sobre o Editor WYSIWYG',\n    'editor_license' => 'Editor da licença de direitos autorais',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'Este editor foi criado com :tinyLink que é fornecido sob a licença MIT.',\n    'editor_tiny_license_link' => 'Os dados relativos aos direitos de autor e à licença do TinyMCE podem ser encontrados aqui.',\n    'save_continue' => 'Salvar página e continuar',\n    'callouts_cycle' => '(Continue pressionando para alternar através de tipos)',\n    'link_selector' => 'Link para conteúdo',\n    'shortcuts' => 'Atalhos',\n    'shortcut' => 'Atalho',\n    'shortcuts_intro' => 'Os seguintes atalhos estão disponíveis no editor:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Descrição',\n];\n"
  },
  {
    "path": "lang/pt/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Criado recentemente',\n    'recently_created_pages' => 'Páginas Criadas Recentemente',\n    'recently_updated_pages' => 'Páginas Atualizadas Recentemente',\n    'recently_created_chapters' => 'Capítulos Criados Recentemente',\n    'recently_created_books' => 'Livros Criados Recentemente',\n    'recently_created_shelves' => 'Estantes Criadas Recentemente',\n    'recently_update' => 'Atualizados Recentemente',\n    'recently_viewed' => 'Visualizados Recentemente',\n    'recent_activity' => 'Atividade Recente',\n    'create_now' => 'Criar um agora',\n    'revisions' => 'Revisões',\n    'meta_revision' => 'Revisão #:revisionCount',\n    'meta_created' => 'Criado :timeLength',\n    'meta_created_name' => 'Criado :timeLength por :user',\n    'meta_updated' => 'Atualizado :timeLength',\n    'meta_updated_name' => 'Atualizado :timeLength por :user',\n    'meta_owned_name' => 'Propriedade de :user',\n    'meta_reference_count' => 'Referenced by :count item|Referenced by :count items',\n    'entity_select' => 'Seleção de Entidade',\n    'entity_select_lack_permission' => 'Não tem as permissões necessárias para selecionar este item',\n    'images' => 'Imagens',\n    'my_recent_drafts' => 'Os Meus Rascunhos Recentes',\n    'my_recently_viewed' => 'Visualizados Recentemente Por Mim',\n    'my_most_viewed_favourites' => 'Os Meus Favoritos Mais Visualizados',\n    'my_favourites' => 'Os Meus Favoritos',\n    'no_pages_viewed' => 'Você não viu nenhuma página',\n    'no_pages_recently_created' => 'Nenhuma página foi recentemente criada',\n    'no_pages_recently_updated' => 'Nenhuma página foi recentemente atualizada',\n    'export' => 'Exportar',\n    'export_html' => 'Arquivo Web contido',\n    'export_pdf' => 'Arquivo PDF',\n    'export_text' => 'Arquivo Texto',\n    'export_md' => 'Ficheiro Markdown',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Default Page Template',\n    'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',\n    'default_template_select' => 'Select a template page',\n    'import' => 'Import',\n    'import_validate' => 'Validate Import',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'Select ZIP file to upload',\n    'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',\n    'import_pending' => 'Pending Imports',\n    'import_pending_none' => 'No imports have been started.',\n    'import_continue' => 'Continue Import',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Run Import',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Uploaded by',\n    'import_location' => 'Import Location',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'Import Errors',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Permissões',\n    'permissions_desc' => 'Definir, aqui, as permissões para substituir o padrão fornecido por papéis de utilizadores.',\n    'permissions_book_cascade' => 'Permissões definidas em livros serão automaticamente convertidas para páginas e capítulos filhos, a menos que tenham suas próprias permissões definidas.',\n    'permissions_chapter_cascade' => 'Permissões definidas em capítulos serão automaticamente convertidas em páginas filhas, a menos que tenham suas próprias permissões definidas.',\n    'permissions_save' => 'Guardar Permissões',\n    'permissions_owner' => 'Proprietário',\n    'permissions_role_everyone_else' => 'Restante',\n    'permissions_role_everyone_else_desc' => 'Definir permissões para todos os papéis não substituídos especificamente.',\n    'permissions_role_override' => 'Substituir permissões para o papel',\n    'permissions_inherit_defaults' => 'Herdar padrões',\n\n    // Search\n    'search_results' => 'Resultado(s) da Pesquisa',\n    'search_total_results_found' => ':count resultado encontrado|:count resultados encontrados',\n    'search_clear' => 'Limpar Pesquisa',\n    'search_no_pages' => 'Nenhuma página corresponde à pesquisa',\n    'search_for_term' => 'Pesquisar por :term',\n    'search_more' => 'Mais Resultados',\n    'search_advanced' => 'Pesquisa Avançada',\n    'search_terms' => 'Termos da Pesquisa',\n    'search_content_type' => 'Tipo de Conteúdo',\n    'search_exact_matches' => 'Correspondências Exatas',\n    'search_tags' => 'Persquisar Tags',\n    'search_options' => 'Opções',\n    'search_viewed_by_me' => 'Visualizado por mim',\n    'search_not_viewed_by_me' => 'Não visualizado por mim',\n    'search_permissions_set' => 'Permissão definida',\n    'search_created_by_me' => 'Criado por mim',\n    'search_updated_by_me' => 'Atualizado por mim',\n    'search_owned_by_me' => 'Propriedade minha',\n    'search_date_options' => 'Opções de Data',\n    'search_updated_before' => 'Atualizado antes de',\n    'search_updated_after' => 'Atualizado depois de',\n    'search_created_before' => 'Criado antes de',\n    'search_created_after' => 'Criado depois de',\n    'search_set_date' => 'Definir Data',\n    'search_update' => 'Atualizar pesquisa',\n\n    // Shelves\n    'shelf' => 'Estante',\n    'shelves' => 'Estantes',\n    'x_shelves' => ':count Estante|:count Estantes',\n    'shelves_empty' => 'Nenhuma estante foi criada',\n    'shelves_create' => 'Criar Nova Estante',\n    'shelves_popular' => 'Estantes Populares',\n    'shelves_new' => 'Estantes Novas',\n    'shelves_new_action' => 'Nova Estante',\n    'shelves_popular_empty' => 'As estantes mais populares serão mostradas aqui.',\n    'shelves_new_empty' => 'As mais recentes estantes criadas serão mostradas aqui.',\n    'shelves_save' => 'Guardar Estante',\n    'shelves_books' => 'Livros nesta estante',\n    'shelves_add_books' => 'Adicionar livros a esta estante',\n    'shelves_drag_books' => 'Arraste os livros abaixo para adicioná-los a esta prateleira',\n    'shelves_empty_contents' => 'Esta estante não tem livros atribuídos',\n    'shelves_edit_and_assign' => 'Editar estante para atribuir livros',\n    'shelves_edit_named' => 'Editar Estante :name',\n    'shelves_edit' => 'Editar estante',\n    'shelves_delete' => 'Excluir estante',\n    'shelves_delete_named' => 'Excluir Estante :name',\n    'shelves_delete_explain' => \"A ação vai eliminar a estante ':name'. Os livros nela presentes não serão eliminados.\",\n    'shelves_delete_confirmation' => 'Tem a certeza que deseja eliminar esta estante?',\n    'shelves_permissions' => 'Permissões da Estante',\n    'shelves_permissions_updated' => 'Permissões da Estante Atualizada',\n    'shelves_permissions_active' => 'Permissões da Estante Ativas',\n    'shelves_permissions_cascade_warning' => 'As permissões nas estantes não são passadas automaticamente em efeito dominó para os livros contidos. Isto acontece porque um livro pode existir em várias estantes. As permissões podem, no entanto, ser copiadas para livros filhos usando a opção abaixo.',\n    'shelves_permissions_create' => 'As permissões de criação de prateleira são usadas apenas para copiar livros filhos usando a ação abaixo. Eles não controlam a capacidade de criar livros.',\n    'shelves_copy_permissions_to_books' => 'Copiar Permissões para Livros',\n    'shelves_copy_permissions' => 'Copiar Permissões',\n    'shelves_copy_permissions_explain' => 'Isto aplicará as configurações de permissões atuais desta estante a todos os livros nela contidos. Antes de ativar, assegure-se de que quaisquer alterações nas permissões desta estante foram guardadas.',\n    'shelves_copy_permission_success' => 'Permissões da estante copiadas para :count livros',\n\n    // Books\n    'book' => 'Livro',\n    'books' => 'Livros',\n    'x_books' => ':count Livro|:count Livros',\n    'books_empty' => 'Nenhum livro foi criado',\n    'books_popular' => 'Livros Populares',\n    'books_recent' => 'Livros Recentes',\n    'books_new' => 'Livros Novos',\n    'books_new_action' => 'Novo Livro',\n    'books_popular_empty' => 'Os livros mais populares serão mostrados aqui.',\n    'books_new_empty' => 'Os livros mais recentemente criados serão mostrados aqui.',\n    'books_create' => 'Criar Livro Novo',\n    'books_delete' => 'Eliminar Livro',\n    'books_delete_named' => 'Eliminar Livro :bookName',\n    'books_delete_explain' => 'A ação vai eliminar o livro com de nome \\':bookName\\'. Todas as páginas e capítulos serão também removidos.',\n    'books_delete_confirmation' => 'Tem a certeza que quer eliminar este livro?',\n    'books_edit' => 'Editar Livro',\n    'books_edit_named' => 'Editar Livro :bookName',\n    'books_form_book_name' => 'Nome do Livro',\n    'books_save' => 'Guardar Livro',\n    'books_permissions' => 'Permissões do Livro',\n    'books_permissions_updated' => 'Permissões do Livro Atualizadas',\n    'books_empty_contents' => 'Nenhuma página ou capítulo foram criados para este livro.',\n    'books_empty_create_page' => 'Criar uma nova página',\n    'books_empty_sort_current_book' => 'Ordenar o livro atual',\n    'books_empty_add_chapter' => 'Adicionar um capítulo',\n    'books_permissions_active' => 'Permissões do Livro Ativas',\n    'books_search_this' => 'Pesquisar neste livro',\n    'books_navigation' => 'Navegação do Livro',\n    'books_sort' => 'Ordenar Conteúdos do Livro',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Ordenar Livro :bookName',\n    'books_sort_name' => 'Ordenar por Nome',\n    'books_sort_created' => 'Ordenar por Data de Criação',\n    'books_sort_updated' => 'Ordenar por Data de Atualização',\n    'books_sort_chapters_first' => 'Capítulos Primeiro',\n    'books_sort_chapters_last' => 'Capítulos por Último',\n    'books_sort_show_other' => 'Mostrar Outros Livros',\n    'books_sort_save' => 'Guardar Nova Ordenação',\n    'books_sort_show_other_desc' => 'Adicione outros livros aqui para incluí-los na operação de classificação e permitir a reorganização fácil de todos os livros.',\n    'books_sort_move_up' => 'Mover para Cima',\n    'books_sort_move_down' => 'Mover para baixo',\n    'books_sort_move_prev_book' => 'Mover para o Livro Anterior',\n    'books_sort_move_next_book' => 'Mover para o próximo livro',\n    'books_sort_move_prev_chapter' => 'Mover para o Capítulo Anterior',\n    'books_sort_move_next_chapter' => 'Mover para o próximo Capítulo',\n    'books_sort_move_book_start' => 'Mover para o início do livro',\n    'books_sort_move_book_end' => 'Mover para o final do livro',\n    'books_sort_move_before_chapter' => 'Mover para Antes do Capítulo',\n    'books_sort_move_after_chapter' => 'Mover para Depois do Capítulo',\n    'books_copy' => 'Copiar livro',\n    'books_copy_success' => 'Livro criado com sucesso',\n\n    // Chapters\n    'chapter' => 'Capítulo',\n    'chapters' => 'Capítulos',\n    'x_chapters' => ':count Capítulo|:count Capítulos',\n    'chapters_popular' => 'Capítulos Populares',\n    'chapters_new' => 'Novo Capítulo',\n    'chapters_create' => 'Criar Novo Capítulo',\n    'chapters_delete' => 'Eliminar Capítulo',\n    'chapters_delete_named' => 'Eliminar Capítulo :chapterName',\n    'chapters_delete_explain' => 'Isto irá eliminar o capítulo com o nome \\':chapterName\\'. Todas as páginas existentes dentro do mesmo serão também eliminadas.',\n    'chapters_delete_confirm' => 'Tem certeza que deseja eliminar o capítulo?',\n    'chapters_edit' => 'Editar Capítulo',\n    'chapters_edit_named' => 'Editar Capítulo :chapterName',\n    'chapters_save' => 'Guardar Capítulo',\n    'chapters_move' => 'Mover Capítulo',\n    'chapters_move_named' => 'Mover Capítulo :chapterName',\n    'chapters_copy' => 'Copiar capítulo',\n    'chapters_copy_success' => 'Capítulo copiado com sucesso',\n    'chapters_permissions' => 'Permissões do Capítulo',\n    'chapters_empty' => 'Nenhuma página existente neste capítulo.',\n    'chapters_permissions_active' => 'Permissões de Capítulo Ativas',\n    'chapters_permissions_success' => 'Permissões de Capítulo Atualizadas',\n    'chapters_search_this' => 'Pesquisar neste Capítulo',\n    'chapter_sort_book' => 'Ordenar livro',\n\n    // Pages\n    'page' => 'Página',\n    'pages' => 'Páginas',\n    'x_pages' => ':count Página|:count Páginas',\n    'pages_popular' => 'Páginas Populares',\n    'pages_new' => 'Nova Página',\n    'pages_attachments' => 'Anexos',\n    'pages_navigation' => 'Navegação da Página',\n    'pages_delete' => 'Eliminar Página',\n    'pages_delete_named' => 'Eliminar Página :pageName',\n    'pages_delete_draft_named' => 'Eliminar Rascunho de Página de nome :pageName',\n    'pages_delete_draft' => 'Eliminar Rascunho de Página',\n    'pages_delete_success' => 'Página eliminada',\n    'pages_delete_draft_success' => 'Rascunho de página eliminado',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'Tem certeza que deseja eliminar a página?',\n    'pages_delete_draft_confirm' => 'Tem certeza que deseja eliminar o rascunho de página?',\n    'pages_editing_named' => 'A Editar a Página :pageName',\n    'pages_edit_draft_options' => 'Opções de Rascunho',\n    'pages_edit_save_draft' => 'Guardar Rascunho',\n    'pages_edit_draft' => 'Editar Rascunho de Página',\n    'pages_editing_draft' => 'A Editar Rascunho',\n    'pages_editing_page' => 'A Editar Página',\n    'pages_edit_draft_save_at' => 'Rascunho guardado em ',\n    'pages_edit_delete_draft' => 'Eliminar Rascunho',\n    'pages_edit_delete_draft_confirm' => 'Tem a certeza que deseja eliminar o rascunha da página? Todas as alterações, desde o último carregamento, será perdido e o editor será atualizado com o último estado guardo da página.',\n    'pages_edit_discard_draft' => 'Descartar Rascunho',\n    'pages_edit_switch_to_markdown' => 'Alternar para o editor Markdown',\n    'pages_edit_switch_to_markdown_clean' => '(Conteúdo Limitado)',\n    'pages_edit_switch_to_markdown_stable' => '(Conteúdo Estável)',\n    'pages_edit_switch_to_wysiwyg' => 'Alternar para o editor WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Relatar Alterações',\n    'pages_edit_enter_changelog_desc' => 'Digite uma breve descrição das alterações efetuadas por si',\n    'pages_edit_enter_changelog' => 'Inserir Alterações',\n    'pages_editor_switch_title' => 'Trocar de Editor',\n    'pages_editor_switch_are_you_sure' => 'Você tem certeza que deseja alterar o editor para esta página?',\n    'pages_editor_switch_consider_following' => 'Considere o seguinte ao alterar editores:',\n    'pages_editor_switch_consideration_a' => 'Uma vez alterada, o novo editor será usado por quaisquer editores futuros, incluindo aqueles que podem não ser capazes de mudar o tipo do editor.',\n    'pages_editor_switch_consideration_b' => 'Isso pode levar a uma perda de detalhes e sintaxe em certas circunstâncias.',\n    'pages_editor_switch_consideration_c' => 'Etiqueta ou alterações no \\'changelog\\', não guardadas, não persistem nesta alteração.',\n    'pages_save' => 'Guardar Página',\n    'pages_title' => 'Título da Página',\n    'pages_name' => 'Nome da Página',\n    'pages_md_editor' => 'Editor',\n    'pages_md_preview' => 'Pré-Visualização',\n    'pages_md_insert_image' => 'Inserir Imagem',\n    'pages_md_insert_link' => 'Inserir Link para Entidade',\n    'pages_md_insert_drawing' => 'Inserir Desenho',\n    'pages_md_show_preview' => 'Mostrar pré-visualização',\n    'pages_md_sync_scroll' => 'Sincronizar pré-visualização',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Encontrado um rascunho não guardado',\n    'pages_drawing_unsaved_confirm' => 'Dados de um rascunho não guardado foi encontrado de um tentativa anteriormente falhada. Deseja restaurar e continuar a edição desse rascunho?',\n    'pages_not_in_chapter' => 'A página não está dentro de um capítulo',\n    'pages_move' => 'Mover Página',\n    'pages_copy' => 'Copiar Página',\n    'pages_copy_desination' => 'Destino da Cópia',\n    'pages_copy_success' => 'Página copiada com sucesso',\n    'pages_permissions' => 'Permissões da Página',\n    'pages_permissions_success' => 'Permissões da Página atualizadas',\n    'pages_revision' => 'Revisão',\n    'pages_revisions' => 'Revisões da Página',\n    'pages_revisions_desc' => 'Abaixo estão listadas todas as revisões anteriores desta página. Pode analisar, comparar e restaurar versões de páginas antigas se as permissões o permitirem. O histórico completo da página pode não ser totalmente refletido aqui, uma vez que, dependendo da configuração do sistema, revisões antigas podem ser auto-excluídas.',\n    'pages_revisions_named' => 'Revisões de Página para :pageName',\n    'pages_revision_named' => 'Revisão de Página para :pageName',\n    'pages_revision_restored_from' => 'Recuperado de #:id; :summary',\n    'pages_revisions_created_by' => 'Criado por',\n    'pages_revisions_date' => 'Data da Revisão',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Número da Revisão',\n    'pages_revisions_numbered' => 'Revisão #:id',\n    'pages_revisions_numbered_changes' => 'Alterações da Revisão #:id',\n    'pages_revisions_editor' => 'Tipo de editor',\n    'pages_revisions_changelog' => 'Relatório de Alterações',\n    'pages_revisions_changes' => 'Alterações',\n    'pages_revisions_current' => 'Versão Atual',\n    'pages_revisions_preview' => 'Pré-Visualização',\n    'pages_revisions_restore' => 'Restaurar',\n    'pages_revisions_none' => 'Essa página não tem revisões',\n    'pages_copy_link' => 'Copiar Link',\n    'pages_edit_content_link' => 'Ir para a secção de edição',\n    'pages_pointer_enter_mode' => 'Inserir modo de seleção',\n    'pages_pointer_label' => 'Opções da Secção do Título',\n    'pages_pointer_permalink' => 'Ligação da Secção de Página',\n    'pages_pointer_include_tag' => 'Tag de Inclusão da Secção de Página',\n    'pages_pointer_toggle_link' => 'Modo de ligação, Pressionar para mostrar a tag de inclusão',\n    'pages_pointer_toggle_include' => 'Modo de tag de inclusão, Pressione para mostrar a ligação',\n    'pages_permissions_active' => 'Permissões de Página Ativas',\n    'pages_initial_revision' => 'Publicação Inicial',\n    'pages_references_update_revision' => 'Atualização automática do sistema de links internos',\n    'pages_initial_name' => 'Nova Página',\n    'pages_editing_draft_notification' => 'Você está atualmente a editar um rascunho que foi guardado pela última vez a :timeDiff.',\n    'pages_draft_edited_notification' => 'Esta página entretanto já foi atualizada. É recomendado que você descarte este rascunho.',\n    'pages_draft_page_changed_since_creation' => 'Esta página foi atualizada desde que este rascunho foi criado. É recomendável que descarte este rascunho ou tenha cuidado para não sobrescrever nenhuma alteração de página.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count usuários iniciaram a edição dessa página',\n        'start_b' => ':userName iniciou a edição desta página',\n        'time_a' => 'desde que a página foi atualizada pela última vez',\n        'time_b' => 'nos últimos :minCount minutos',\n        'message' => ':start :time. Tenha cuidado para não sobrescrever atualizações de outras pessoas!',\n    ],\n    'pages_draft_discarded' => 'Rascunho eliminado! O editor foi atualizado com o conteúdo atual da página',\n    'pages_draft_deleted' => 'Rascunho eliminado! O editor foi atualizado com o conteúdo atual da página',\n    'pages_specific' => 'Página Específica',\n    'pages_is_template' => 'Modelo de Página',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Alternar barra lateral',\n    'page_tags' => 'Etiquetas de Página',\n    'chapter_tags' => 'Etiquetas do Capítulo',\n    'book_tags' => 'Etiquetas do Livro',\n    'shelf_tags' => 'Etiquetas da Prateleira',\n    'tag' => 'Etiqueta',\n    'tags' =>  'Etiquetas',\n    'tags_index_desc' => 'As etiquetas podem ser aplicadas a conteúdos do sistema para aplicar uma forma flexível de categorização. As etiquetas podem ter uma chave e um valor, sendo o valor opcional. Uma vez aplicado, o conteúdo pode ser consultado através do nome e/ou do valor da etiqueta.',\n    'tag_name' =>  'Nome da Etiqueta',\n    'tag_value' => 'Valor da Etiqueta (Opcional)',\n    'tags_explain' => \"Adicione algumas etiquetas para melhor categorizar o seu conteúdo. \\n Você poderá atribuir valores às etiquetas para uma organização mais complexa.\",\n    'tags_add' => 'Adicionar outra etiqueta',\n    'tags_remove' => 'Remover esta etiqueta',\n    'tags_usages' => 'Total de marcadores usados',\n    'tags_assigned_pages' => 'Atribuído às páginas',\n    'tags_assigned_chapters' => 'Atribuído aos Capítulos',\n    'tags_assigned_books' => 'Atribuído a Livros',\n    'tags_assigned_shelves' => 'Atribuído a Prateleiras',\n    'tags_x_unique_values' => ':count valores únicos',\n    'tags_all_values' => 'Todos os valores',\n    'tags_view_tags' => 'Ver Marcadores',\n    'tags_view_existing_tags' => 'Ver marcadores existentes',\n    'tags_list_empty_hint' => 'As tags podem ser atribuídas através da barra lateral do editor de página ou ao editar os detalhes de um livro, capítulo ou prateleira.',\n    'attachments' => 'Anexos',\n    'attachments_explain' => 'Carregue alguns arquivos ou anexe links para serem exibidos na sua página. Eles estarão visíveis na barra lateral à direita.',\n    'attachments_explain_instant_save' => 'As mudanças são guardadas instantaneamente.',\n    'attachments_upload' => 'Carregamento de Arquivos',\n    'attachments_link' => 'Anexar Link',\n    'attachments_upload_drop' => 'Como alternativa, pode arrastar e soltar um arquivo aqui para carregá-lo como um anexo.',\n    'attachments_set_link' => 'Definir Link',\n    'attachments_delete' => 'Tem certeza de que deseja eliminar este anexo?',\n    'attachments_dropzone' => 'Arrasta para aqui um ficheiro para o carregar',\n    'attachments_no_files' => 'Nenhum arquivo foi enviado',\n    'attachments_explain_link' => 'Pode anexar um link se preferir não fazer o carregamento do arquivo. O link poderá ser para uma outra página ou para um arquivo na nuvem.',\n    'attachments_link_name' => 'Nome do Link',\n    'attachment_link' => 'Link do Anexo',\n    'attachments_link_url' => 'Link para o Arquivo',\n    'attachments_link_url_hint' => 'Url do sítio ou arquivo',\n    'attach' => 'Anexar',\n    'attachments_insert_link' => 'Adicionar Link de Anexo à Página',\n    'attachments_edit_file' => 'Editar Arquivo',\n    'attachments_edit_file_name' => 'Nome do Arquivo',\n    'attachments_edit_drop_upload' => 'Arraste arquivos para aqui ou carregue para anexar arquivos e sobrescreve-los',\n    'attachments_order_updated' => 'Ordem dos anexos atualizada',\n    'attachments_updated_success' => 'Detalhes dos anexos atualizados',\n    'attachments_deleted' => 'Anexo eliminado',\n    'attachments_file_uploaded' => 'Carregamento de arquivo efetuado com sucesso',\n    'attachments_file_updated' => 'Arquivo atualizado com sucesso',\n    'attachments_link_attached' => 'Link anexado com sucesso à página',\n    'templates' => 'Modelos',\n    'templates_set_as_template' => 'A página é um modelo',\n    'templates_explain_set_as_template' => 'Pode definir esta página como um modelo para que o seu conteúdo possa ser utilizado para criar outras páginas. Outros usuários poderão utilizar esta página como modelo se tiverem permissão para visualiza-la.',\n    'templates_replace_content' => 'Substituir conteúdo da página',\n    'templates_append_content' => 'Adicionar ao fim do conteúdo da página',\n    'templates_prepend_content' => 'Adicionar ao início do conteúdo da página',\n\n    // Profile View\n    'profile_user_for_x' => 'Utilizador por :time',\n    'profile_created_content' => 'Conteúdo Criado',\n    'profile_not_created_pages' => ':userName não criou páginas',\n    'profile_not_created_chapters' => ':userName não criou capítulos',\n    'profile_not_created_books' => ':userName não criou livros',\n    'profile_not_created_shelves' => ':userName não criou estantes',\n\n    // Comments\n    'comment' => 'Comentário',\n    'comments' => 'Comentários',\n    'comment_add' => 'Adicionar Comentário',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Digite aqui os seus comentários',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Guardar comentário',\n    'comment_new' => 'Comentário Novo',\n    'comment_created' => 'comentado :createDiff',\n    'comment_updated' => 'A editar :updateDiff por :username',\n    'comment_updated_indicator' => 'Atualizado',\n    'comment_deleted_success' => 'Comentário removido',\n    'comment_created_success' => 'Comentário adicionado',\n    'comment_updated_success' => 'Comentário editado',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Tem a certeza de que deseja eliminar este comentário?',\n    'comment_in_reply_to' => 'Em resposta à :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Aqui estão os comentários que foram deixados nesta página. Comentários podem ser adicionados e geridos ao visualizar a página guardada.',\n\n    // Revision\n    'revision_delete_confirm' => 'Tem a certeza de que deseja eliminar esta revisão?',\n    'revision_restore_confirm' => 'Tem a certeza que deseja restaurar esta revisão? O conteúdo atual da página será substituído.',\n    'revision_cannot_delete_latest' => 'Não é possível eliminar a revisão mais recente.',\n\n    // Copy view\n    'copy_consider' => 'Ao copiar conteúdo considere, por favor, a informação abaixo.',\n    'copy_consider_permissions' => 'Configurações personalizada não serão copiadas.',\n    'copy_consider_owner' => 'Você se tornará o proprietário de todos os conteúdos copiados.',\n    'copy_consider_images' => 'A imagem da página não será duplicada e as imagens originais manterão sua relação com a página para a qual foram enviadas originalmente.',\n    'copy_consider_attachments' => 'Anexos da página não serão copiados.',\n    'copy_consider_access' => 'Uma alteração de localização, proprietário ou permissões pode resultar em que este conteúdo seja acessível para aqueles previamente sem acesso.',\n\n    // Conversions\n    'convert_to_shelf' => 'Converter para prateleira',\n    'convert_to_shelf_contents_desc' => 'Você pode converter este livro em uma nova prateleira com o mesmo conteúdo. Capítulos contidos neste livro serão convertidos em novos livros. Se este livro contiver alguma página, que não estão em um capítulo, este livro será renomeado contendo essas páginas, e o mesmo se tornará parte da nova prateleira.',\n    'convert_to_shelf_permissions_desc' => 'Quaisquer permissões definidas neste livro serão copiadas para a nova prateleira e para todos os novos livros filhos que não tenham as suas próprias permissões impostas. Observe que as permissões nas prateleiras não são do tipo cascata como o são para os livros.',\n    'convert_book' => 'Converter Livro',\n    'convert_book_confirm' => 'Tem a certeza de que deseja converter este livro?',\n    'convert_undo_warning' => 'Isto não pode ser tão facilmente desfeito.',\n    'convert_to_book' => 'Converter para Livro',\n    'convert_to_book_desc' => 'Você pode converter este capítulo em um novo livro com o mesmo conteúdo. Qualquer permissão definida neste capítulo será copiada para o novo livro, mas para as permissões herdadas, do livro pai não será copiado que possa levar a uma mudança de controlo de acesso.',\n    'convert_chapter' => 'Converter Capítulo',\n    'convert_chapter_confirm' => 'Tem certeza de que deseja converter este capítulo?',\n\n    // References\n    'references' => 'Referências',\n    'references_none' => 'Não há referências registadas para este item.',\n    'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',\n\n    // Watch Options\n    'watch' => 'Ver',\n    'watch_title_default' => 'Preferências Predefinidas',\n    'watch_desc_default' => 'Reverter visualização para as preferências de notificação padrão.',\n    'watch_title_ignore' => 'Ignorar',\n    'watch_desc_ignore' => 'Ignorar todas as notificações, incluindo as de preferências de nível de usuário.',\n    'watch_title_new' => 'Novas Páginas',\n    'watch_desc_new' => 'Notificar quando qualquer nova página for criada dentro deste item.',\n    'watch_title_updates' => 'Todas as atualizações da página',\n    'watch_desc_updates' => 'Notificar sobre todas as novas páginas e alterações na página.',\n    'watch_desc_updates_page' => 'Notificar sobre todas as alterações da página.',\n    'watch_title_comments' => 'Todas as atualizações e comentários da página',\n    'watch_desc_comments' => 'Notificar sobre todas as novas páginas, alterações de página e novos comentários.',\n    'watch_desc_comments_page' => 'Notificar sobre alterações na página e novos comentários.',\n    'watch_change_default' => 'Alterar preferências padrão de notificação',\n    'watch_detail_ignore' => 'Ignorar notificações',\n    'watch_detail_new' => 'Watching for new pages',\n    'watch_detail_updates' => 'Watching new pages and updates',\n    'watch_detail_comments' => 'Watching new pages, updates & comments',\n    'watch_detail_parent_book' => 'Watching via parent book',\n    'watch_detail_parent_book_ignore' => 'A ignorar através do livro pai',\n    'watch_detail_parent_chapter' => 'Watching via parent chapter',\n    'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter',\n];\n"
  },
  {
    "path": "lang/pt/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Você não tem permissão para aceder à página requisitada.',\n    'permissionJson' => 'Você não tem permissão para realizar a ação requerida.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Um utilizador com o endereço de e-mail :email já existe mas com credenciais diferentes.',\n    'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',\n    'email_already_confirmed' => 'E-mail já foi confirmado. Tente iniciar sessão.',\n    'email_confirmation_invalid' => 'Este token de confirmação não é válido ou já foi utilizado. Por favor, tente registar-se novamente.',\n    'email_confirmation_expired' => 'O token de confirmação já expirou. Um novo e-mail foi enviado.',\n    'email_confirmation_awaiting' => 'O endereço de e-mail da conta em uso precisa ser confirmado',\n    'ldap_fail_anonymous' => 'O acesso LDAP falhou ao tentar usar o anonymous bind',\n    'ldap_fail_authed' => 'O acesso LDAP falhou ao tentar os detalhes do dn e senha fornecidos',\n    'ldap_extension_not_installed' => 'A extensão LDAP PHP não está instalada',\n    'ldap_cannot_connect' => 'Não foi possível conectar ao servidor LDAP. Conexão inicial falhou',\n    'saml_already_logged_in' => 'Sessão já iniciada',\n    'saml_no_email_address' => 'Não foi possível encontrar um endereço de e-mail para este utilizador nos dados providenciados pelo sistema de autenticação externa',\n    'saml_invalid_response_id' => 'A requisição do sistema de autenticação externa não foi reconhecia por um processo iniciado por esta aplicação. Navegar para o caminho anterior após o inicio de sessão pode provocar este problema.',\n    'saml_fail_authed' => 'Inicio de sessão com :system falhou. O sistema não forneceu uma autorização bem sucedida',\n    'oidc_already_logged_in' => 'Sessão já iniciada',\n    'oidc_no_email_address' => 'Não foi possível encontrar um endereço de e-mail para este utilizador nos dados providenciados pelo sistema de autenticação externo',\n    'oidc_fail_authed' => 'Inicio de sessão com :system falhou. O sistema não forneceu uma autorização bem sucedida',\n    'social_no_action_defined' => 'Nenhuma ação definida',\n    'social_login_bad_response' => \"Erro recebido durante o inicio de sessão :socialAccount: \\n:error\",\n    'social_account_in_use' => 'Esta conta :socialAccount já está em uso. Por favor, tente entrar utilizando a opção :socialAccount.',\n    'social_account_email_in_use' => 'O e-mail :email já está em uso. Se já possui uma conta poderá ligar a sua conta :socialAccount a partir das configurações do seu perfil.',\n    'social_account_existing' => 'Esta conta :socialAccount já está vinculada a este perfil.',\n    'social_account_already_used_existing' => 'Esta conta :socialAccount já está a ser utilizada por outro utilizador.',\n    'social_account_not_used' => 'Esta conta :socialAccount não está vinculada a nenhum utilizador. Por favor vincule a conta nas suas configurações de perfil. ',\n    'social_account_register_instructions' => 'Se não possui uma conta, poderá registar-se utilizando a opção :socialAccount.',\n    'social_driver_not_found' => 'Social driver não encontrado',\n    'social_driver_not_configured' => 'Os seus parâmetros sociais de :socialAccount não estão corretamente configurados.',\n    'invite_token_expired' => 'Este link de convite expirou. Alternativamente, pode tentar redefinir a senha da sua conta.',\n    'login_user_not_found' => 'A user for this action could not be found.',\n\n    // System\n    'path_not_writable' => 'O caminho do arquivo :filePath não pôde ser carregado. Certifique-se de que tem permissões de escrita no servidor.',\n    'cannot_get_image_from_url' => 'Não foi possível obter a imagem a partir de :url',\n    'cannot_create_thumbs' => 'O servidor não pôde criar as miniaturas de imagem. Por favor, verifique se a extensão GD PHP está instalada.',\n    'server_upload_limit' => 'O servidor não permite o carregamento de arquivos com esse tamanho. Por favor, tente fazer o carregamento de arquivos mais pequenos.',\n    'server_post_limit' => 'O servidor não pode receber a quantidade de dados fornecida. Tente novamente com menos dados ou um arquivo menor.',\n    'uploaded'  => 'O servidor não permite o carregamento de arquivos com esse tamanho. Por favor, tente fazer o carregamento de arquivos mais pequenos.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Ocorreu um erro no carregamento da imagem',\n    'image_upload_type_error' => 'O tipo de imagem enviada é inválida',\n    'image_upload_replace_type' => 'A imagem de substituição deverá ser do mesmo tipo que a anterior',\n    'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.',\n    'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.',\n    'image_gallery_thumbnail_memory_limit' => 'Failed to create gallery thumbnails due to system resource limits.',\n    'drawing_data_not_found' => 'Dados de desenho não puderam ser carregados. Talvez o arquivo de desenho não exista mais ou não tenha permissão para aceder-lhe.',\n\n    // Attachments\n    'attachment_not_found' => 'Anexo não encontrado',\n    'attachment_upload_error' => 'Ocorreu um erro no carregamento do ficheiro',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Falha ao tentar guardar o rascunho. Certifique-se que a conexão de Internet está funcional antes de tentar guardar esta página',\n    'page_draft_delete_fail' => 'Eliminação do rascunho de página e importação do conteúdo salvo da página falhou',\n    'page_custom_home_deletion' => 'Não é possível eliminar uma página que está definida como página inicial',\n\n    // Entities\n    'entity_not_found' => 'Entidade não encontrada',\n    'bookshelf_not_found' => 'Estante não encontrada',\n    'book_not_found' => 'Livro não encontrado',\n    'page_not_found' => 'Página não encontrada',\n    'chapter_not_found' => 'Capítulo não encontrado',\n    'selected_book_not_found' => 'O livro selecionado não foi encontrado',\n    'selected_book_chapter_not_found' => 'O Livro ou Capítulo selecionado não foi encontrado',\n    'guests_cannot_save_drafts' => 'Convidados não podem guardar rascunhos',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Não pode excluir o único administrador',\n    'users_cannot_delete_guest' => 'Não pode excluir o usuário convidado',\n    'users_could_not_send_invite' => 'Could not create user since invite email failed to send',\n\n    // Roles\n    'role_cannot_be_edited' => 'Este cargo não pode ser editado',\n    'role_system_cannot_be_deleted' => 'Este cargo é um cargo do sistema e não pode ser excluído',\n    'role_registration_default_cannot_delete' => 'Este cargo não poderá se excluído enquanto estiver definido como o cargo padrão',\n    'role_cannot_remove_only_admin' => 'Este utilizador é o único vinculado ao cargo de administrador. Atribua o cargo de administrador a outro antes de tentar removê-lo aqui.',\n\n    // Comments\n    'comment_list' => 'Ocorreu um erro ao recolher os comentários.',\n    'cannot_add_comment_to_draft' => 'Não pode adicionar comentários a um rascunho.',\n    'comment_add' => 'Ocorreu um erro ao adicionar/atualizar o comentário.',\n    'comment_delete' => 'Ocorreu um erro ao eliminar o comentário.',\n    'empty_comment' => 'Não é possível adicionar um comentário vazio.',\n\n    // Error pages\n    '404_page_not_found' => 'Página Não Encontrada',\n    'sorry_page_not_found' => 'Desculpe, a página que procura não foi encontrada.',\n    'sorry_page_not_found_permission_warning' => 'Se esperava que esta página existisse, talvez não tenha permissão para visualizá-la.',\n    'image_not_found' => 'Imagem não encontrada',\n    'image_not_found_subtitle' => 'Desculpe, o arquivo de imagem que estava à procura não foi encontrado.',\n    'image_not_found_details' => 'Se estava à espera que a mesma existisse é possível que tenha sido eliminada.',\n    'return_home' => 'Regressar à página inicial',\n    'error_occurred' => 'Ocorreu um Erro',\n    'app_down' => ':appName está fora do ar de momento',\n    'back_soon' => 'Voltaremos em breve.',\n\n    // Import\n    'import_zip_cant_read' => 'Could not read ZIP file.',\n    'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',\n    'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Import ZIP failed to validate with errors:',\n    'import_zip_failed_notification' => 'Failed to import ZIP file.',\n    'import_perms_books' => 'You are lacking the required permissions to create books.',\n    'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',\n    'import_perms_pages' => 'You are lacking the required permissions to create pages.',\n    'import_perms_images' => 'You are lacking the required permissions to create images.',\n    'import_perms_attachments' => 'You are lacking the required permission to create attachments.',\n\n    // API errors\n    'api_no_authorization_found' => 'Nenhum token de autorização encontrado na requisição',\n    'api_bad_authorization_format' => 'Um token de autorização foi encontrado na requisição, mas o formato parece incorreto',\n    'api_user_token_not_found' => 'Nenhum token de API correspondente foi encontrado para o token de autorização fornecido',\n    'api_incorrect_token_secret' => 'O segredo fornecido para o token de API usado está incorreto',\n    'api_user_no_api_permission' => 'O proprietário do token de API utilizado não tem permissão para fazer requisições de API',\n    'api_user_token_expired' => 'O token de autenticação expirou',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Erro lançado ao enviar um e-mail de teste:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'The URL does not match the configured allowed SSR hosts',\n];\n"
  },
  {
    "path": "lang/pt/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Novo comentário na página: :pageName',\n    'new_comment_intro' => 'Um utilizador comentou uma página em :appName:',\n    'new_page_subject' => 'Nova página: :pageName',\n    'new_page_intro' => 'Uma nova página foi criada em :appName:',\n    'updated_page_subject' => 'Página atualizada: :pageName',\n    'updated_page_intro' => 'Uma página foi atualizada em :appName:',\n    'updated_page_debounce' => 'Para evitar um grande volume de notificações, durante algum tempo não serão enviadas notificações de edições futuras para esta página através do mesmo editor.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Nome da Página:',\n    'detail_page_path' => 'Page Path:',\n    'detail_commenter' => 'Comentador:',\n    'detail_comment' => 'Comentário:',\n    'detail_created_by' => 'Criado Por:',\n    'detail_updated_by' => 'Atualizado Por:',\n\n    'action_view_comment' => 'Ver Comentário',\n    'action_view_page' => 'Ver Página',\n\n    'footer_reason' => 'Esta notificação foi enviada a você porque :link cobre este tipo de atividade para este item.',\n    'footer_reason_link' => 'suas preferências de notificação',\n];\n"
  },
  {
    "path": "lang/pt/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Anterior',\n    'next'     => 'Seguinte &raquo;',\n\n];\n"
  },
  {
    "path": "lang/pt/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'As palavras-passe devem ter no mínimo oito caracteres e serem iguais à confirmação.',\n    'user' => \"Não pudemos encontrar um utilizador com o endereço de e-mail fornecido.\",\n    'token' => 'O token de redefinição de senha é inválido para este endereço de e-mail.',\n    'sent' => 'Enviamos o link de redefinição de palavra-passe para o seu e-mail!',\n    'reset' => 'A sua palavra-passe foi redefinida com sucesso!',\n\n];\n"
  },
  {
    "path": "lang/pt/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'A Minha Conta',\n\n    'shortcuts' => 'Atalhos',\n    'shortcuts_interface' => 'Preferências de Atalho UI',\n    'shortcuts_toggle_desc' => 'Aqui pode ativar ou desativar os atalhos de teclado do sistema, usados para navegação e ações.',\n    'shortcuts_customize_desc' => 'Pode personalizar cada um dos atalhos abaixo. Pressione a combinação de tecla desejada após selecionar a entrada para um atalho.',\n    'shortcuts_toggle_label' => 'Atalhos de teclado ativados',\n    'shortcuts_section_navigation' => 'Navegação',\n    'shortcuts_section_actions' => 'Ações comuns',\n    'shortcuts_save' => 'Salvar Atalhos',\n    'shortcuts_overlay_desc' => 'Nota: Quando os atalhos estão ativados, um balão de ajuda ficará disponível pressionando \"?\" destacando os atalhos disponíveis para ações atualmente visíveis na tela.',\n    'shortcuts_update_success' => 'As suas preferências de atalhos foram guardadas!',\n    'shortcuts_overview_desc' => 'Gerir atalhos do teclado que podem ser usados para navegar pela interface do utilizador.',\n\n    'notifications' => 'Preferências das Notificações',\n    'notifications_desc' => 'Controlar as notificações via correio eletrónico quando certas atividades são executadas pelo sistema.',\n    'notifications_opt_own_page_changes' => 'Notificar quando páginas que possuo sofrem alterações',\n    'notifications_opt_own_page_comments' => 'Notificar quando comentam páginas que possuo',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Notificar respostas aos meus comentários',\n    'notifications_save' => 'Guardar preferências',\n    'notifications_update_success' => 'Preferências de notificação foram atualizadas!',\n    'notifications_watched' => 'Itens assistidos e ignorados',\n    'notifications_watched_desc' => 'Abaixo estão os itens que possuem preferências de seguir personalizadas aplicadas. Para atualizar suas preferências para estes, veja o item e encontre as opções de seguir na barra lateral.',\n\n    'auth' => 'Acesso e Segurança',\n    'auth_change_password' => 'Alterar Palavra-passe',\n    'auth_change_password_desc' => 'Altere a palavra-passe que você usa para entrar no aplicativo. Ela deve ter pelo menos 8 caracteres.',\n    'auth_change_password_success' => 'A palavra-passe foi atualizada!',\n\n    'profile' => 'Detalhes Do Perfil',\n    'profile_desc' => 'Gerencie os detalhes de sua conta que o representam para outros usuários, além de detalhes que são usados para personalização do sistema e comunicação.',\n    'profile_view_public' => 'Visualizar Perfil Público',\n    'profile_name_desc' => 'Configure o seu nome de exibição que será visível para outros usuários no sistema através da atividade que você executa e do conteúdo você tem.',\n    'profile_email_desc' => 'Este e-mail será usado para notificações e, dependendo da autenticação ativa do sistema, acesso do sistema.',\n    'profile_email_no_permission' => 'Infelizmente você não tem permissão para alterar seu correio eletrônico. Se você quiser mudar isso, você precisa pedir a um administrador para alterar por você.',\n    'profile_avatar_desc' => 'Selecione uma imagem que será usada para lhe representar aos outros usuários do sistema. Idealmente, esta imagem deve ser quadrada e sobre 256px em largura e altura.',\n    'profile_admin_options' => 'Opções de administrador',\n    'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the \"Settings > Users\" area of the application.',\n\n    'delete_account' => 'Excluir Conta',\n    'delete_my_account' => 'Excluir a Minha Conta',\n    'delete_my_account_desc' => 'Isto excluirá completamente sua conta de utilizador do sistema. Você não poderá recuperar esta conta ou reverter esta ação. O conteúdo que você criou, como páginas criadas e imagens carregadas, permanecerá.',\n    'delete_my_account_warning' => 'Tem certeza que deseja excluir sua conta?',\n];\n"
  },
  {
    "path": "lang/pt/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Configurações',\n    'settings_save' => 'Guardar Configurações',\n    'system_version' => 'Versão do sistema',\n    'categories' => 'Categorias',\n\n    // App Settings\n    'app_customization' => 'Personalização',\n    'app_features_security' => 'Recursos & Segurança',\n    'app_name' => 'Nome da Aplicação',\n    'app_name_desc' => 'Este nome será mostrado no cabeçalho e em e-mails.',\n    'app_name_header' => 'Mostrar o nome no cabeçalho',\n    'app_public_access' => 'Acesso Público',\n    'app_public_access_desc' => 'Ativar esta opção irá permitir que os visitantes que não estão autenticados, acedam ao conteúdo da sua instância do BookStack.',\n    'app_public_access_desc_guest' => 'O acesso de visitantes públicos pode ser controlado através do utilizador \"Convidado\".',\n    'app_public_access_toggle' => 'Permitir acesso público',\n    'app_public_viewing' => 'Permitir visualização pública?',\n    'app_secure_images' => 'Carregamento de Imagens mais Seguro',\n    'app_secure_images_toggle' => 'Ativar o carregamento de imagem mais seguro',\n    'app_secure_images_desc' => 'Por razões de performance, todas as imagens são públicas. Esta opção adiciona uma string aleatória na frente das Urls de imagens. Certifique-se de que os diretórios não possam ser indexados para prevenir acesso indesejado.',\n    'app_default_editor' => 'Editor de Página Padrão',\n    'app_default_editor_desc' => 'Selecione qual editor será usado por padrão ao editar novas páginas. Isto pode ser substituído a nível de página onde as permissões estiverem disponíveis.',\n    'app_custom_html' => 'Conteúdo personalizado para para o Head do HTML',\n    'app_custom_html_desc' => 'Quaisquer conteúdos aqui adicionados serão inseridos no final da secção <head> de cada página. Esta é uma maneira útil de sobrescrever estilos e adicionar códigos de análise de site.',\n    'app_custom_html_disabled_notice' => 'O conteúdo personalizado do <head> HTML está desativado nesta página de configurações, para garantir que quaisquer alterações que acabem maliciosas possam ser revertidas.',\n    'app_logo' => 'Logo da Aplicação',\n    'app_logo_desc' => 'Isto é usado na barra de cabeçalho da aplicação, entre outras áreas. Esta imagem deve ter 86px de altura. Imagens grandes serão redimensionadas.',\n    'app_icon' => 'Ícone da aplicação',\n    'app_icon_desc' => 'Este ícone é usado para guias e ícones de atalhos do navegador. A imagem para o ícone deve ser quadrada, de lado 256px e com o formato PNG.',\n    'app_homepage' => 'Página Inicial',\n    'app_homepage_desc' => 'Selecione uma opção para ser exibida como página inicial em vez da padrão. Permissões de página serão ignoradas para as páginas selecionadas.',\n    'app_homepage_select' => 'Selecione uma página',\n    'app_footer_links' => 'Links do Rodapé',\n    'app_footer_links_desc' => 'Adicionar links para mostrar dentro do rodapé do site. Estes serão exibidos no rodapé da maioria das páginas, incluindo as que não requerem autenticação. Pode utilizar uma etiqueta de \"trans::<key>\" para utilizar traduções definidas pelo sistema. Por exemplo: Utilizando \"trans::common.privacy_policy\" fornecerá o texto traduzido \"Política de Privacidade\" e \"trans::common.terms_of_service\" fornecerá o texto traduzido \"Termos de Serviço\".',\n    'app_footer_links_label' => 'Etiqueta do Link',\n    'app_footer_links_url' => 'URL do link',\n    'app_footer_links_add' => 'Adicionar Link de Rodapé',\n    'app_disable_comments' => 'Desativar Comentários',\n    'app_disable_comments_toggle' => 'Desativar comentários',\n    'app_disable_comments_desc' => 'Desativar comentários em todas as páginas da aplicação.<br> Comentários existentes não serão exibidos.',\n\n    // Color settings\n    'color_scheme' => 'Esquema de cores da aplicação',\n    'color_scheme_desc' => 'Defina as cores a serem utilizadas na aplicação. As cores podem ser configuradas separadamente para modos escuro e claro para melhor se adequar ao tema e garantir legibilidade.',\n    'ui_colors_desc' => 'Defina a cor primária e a cor padrão para links da aplicação. A cor principal é utilizada principalmente para o banner do cabeçalho, botões e decorações da interface. A cor padrão do link é usada para links e ações baseados em texto, tanto dentro do conteúdo escrito quanto na interface da aplicação.',\n    'app_color' => 'Cor primária',\n    'link_color' => 'Cor padrão do link',\n    'content_colors_desc' => 'Definir cores para todos os elementos na hierarquia da organização da página. Escolher cores com um brilho semelhante às cores padrão é recomendado para a legibilidade.',\n    'bookshelf_color' => 'Cor da Prateleira',\n    'book_color' => 'Cor do Livro',\n    'chapter_color' => 'Cor do Capítulo',\n    'page_color' => 'Cor da Página',\n    'page_draft_color' => 'Cor do Rascunho',\n\n    // Registration Settings\n    'reg_settings' => 'Inscrição',\n    'reg_enable' => 'Permitir inscrições',\n    'reg_enable_toggle' => 'Permitir inscrições',\n    'reg_enable_desc' => 'Quando o registo é ativado, os visitantes poderão registar se como utilizadores padrão da aplicação.',\n    'reg_default_role' => 'Papel por omissão apôs registo',\n    'reg_enable_external_warning' => 'A opção acima é ignorada enquanto a autenticação externa LDAP ou SAML estiver ativa. Contas de usuários para membros não existentes serão criadas automaticamente se a autenticação pelo sistema externo em uso for bem sucedida.',\n    'reg_email_confirmation' => 'Confirmação de E-mail',\n    'reg_email_confirmation_toggle' => 'Requerer confirmação de e-mail',\n    'reg_confirm_email_desc' => 'Em caso da restrição de domínios estar em uso, a confirmação de e-mail será requerida e esta opção será ignorada.',\n    'reg_confirm_restrict_domain' => 'Restrição de Domínios',\n    'reg_confirm_restrict_domain_desc' => 'Entre com uma lista separada por vírgulas de domínios de e-mails aos quais você deseja restringir o registo. Um e-mail de confirmação será enviado para o utilizador validar o seu respetivo endereço de e-mail antes de ser permitida a interação com a aplicação. <br> Note que os utilizadores serão capazes de alterar os seus endereços de e-mail após o sucesso na confirmação do registo.',\n    'reg_confirm_restrict_domain_placeholder' => 'Nenhuma restrição definida',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Manutenção',\n    'maint_image_cleanup' => 'Limpeza de Imagens',\n    'maint_image_cleanup_desc' => 'Examina páginas e reviste os seus conteúdos para verificar quais imagens e desenhos estão atualmente em uso e quais são redundantes. Certifique-se de criar uma cópia de segurança completa da base de dados e imagens antes de executar esta ação.',\n    'maint_delete_images_only_in_revisions' => 'Eliminar também imagens que existam apenas em revisões de página antigas',\n    'maint_image_cleanup_run' => 'Executar Limpeza',\n    'maint_image_cleanup_warning' => ':count imagens potencialmente não utilizadas foram encontradas. Tem certeza de que deseja eliminar estas imagens?',\n    'maint_image_cleanup_success' => ':count imagens potencialmente não utilizadas foram encontradas e eliminadas!',\n    'maint_image_cleanup_nothing_found' => 'Nenhuma imagem por utilizar foi encontrada, nada foi eliminado!',\n    'maint_send_test_email' => 'Enviar um E-mail de Teste',\n    'maint_send_test_email_desc' => 'Esta opção envia um e-mail de teste para o endereço especificado no seu perfil.',\n    'maint_send_test_email_run' => 'Enviar e-mail de teste',\n    'maint_send_test_email_success' => 'E-mail enviado para :address',\n    'maint_send_test_email_mail_subject' => 'E-mail de Teste',\n    'maint_send_test_email_mail_greeting' => 'O envio de e-mails parece funcionar!',\n    'maint_send_test_email_mail_text' => 'Parabéns! Já que recebeu esta notificação, as suas opções de e-mail parecem estar configuradas corretamente.',\n    'maint_recycle_bin_desc' => 'Estantes, livros, capítulos e páginas eliminados são mandados para a reciclagem podendo assim ser restaurados ou excluídos permanentemente. Itens mais antigos da podem vir a ser automaticamente removidos da reciclagem após um tempo, dependendo da configuração do sistema.',\n    'maint_recycle_bin_open' => 'Abrir Reciclagem',\n    'maint_regen_references' => 'Regerar Referências',\n    'maint_regen_references_desc' => 'Esta ação irá reconstruir o índice de referência no banco de dados. Isto geralmente é tratado automaticamente, mas esta ação pode ser útil para indexar conteúdo antigo ou conteúdo adicionado através de métodos não oficiais.',\n    'maint_regen_references_success' => 'Índice de referência foi regenerado!',\n    'maint_timeout_command_note' => 'Nota: Esta ação pode levar algum tempo para executar, retornando \\'timeout\\' em alguns ambientes web. Como alternativa, esta ação pode ser executada via terminal.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Reciclagem',\n    'recycle_bin_desc' => 'Aqui pode restaurar itens que foram eliminados ou eliminá-los permanentemente do sistema. Esta lista não é filtrada diferentemente de listas de atividades parecidas no sistema onde filtros de permissão são aplicados.',\n    'recycle_bin_deleted_item' => 'Item eliminado',\n    'recycle_bin_deleted_parent' => 'Parente',\n    'recycle_bin_deleted_by' => 'Eliminado por',\n    'recycle_bin_deleted_at' => 'Data de Eliminação',\n    'recycle_bin_permanently_delete' => 'Eliminar permanentemente',\n    'recycle_bin_restore' => 'Restaurar',\n    'recycle_bin_contents_empty' => 'A reciclagem está atualmente vazia',\n    'recycle_bin_empty' => 'Esvaziar Reciclagem',\n    'recycle_bin_empty_confirm' => 'Isto irá destruir permanentemente todos os itens na reciclagem inclusive o conteúdo de cada item. Tem certeza de que a deseja esvaziar?',\n    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?',\n    'recycle_bin_destroy_list' => 'Itens a serem Destruídos',\n    'recycle_bin_restore_list' => 'Itens a serem Restaurados',\n    'recycle_bin_restore_confirm' => 'Esta ação irá restaurar o item excluído, inclusive quaisquer elementos filhos, para o seu local original. Se a localização original tiver, entretanto, sido eliminada e estiver agora na reciclagem, o item pai também precisará de ser restaurado.',\n    'recycle_bin_restore_deleted_parent' => 'O parente deste item foi também eliminado. Estes permanecerão eliminados até que o parente seja também restaurado.',\n    'recycle_bin_restore_parent' => 'Restaurar Parente',\n    'recycle_bin_destroy_notification' => 'Eliminados no total :count itens da lixeira.',\n    'recycle_bin_restore_notification' => 'Restaurados no total :count itens da reciclagem.',\n\n    // Audit Log\n    'audit' => 'Registo de auditoria',\n    'audit_desc' => 'Este registo de auditoria exibe uma lista de atividades rastreadas no sistema. Esta lista não é filtrada ao contrário de listas de atividades semelhantes no sistema onde os filtros de permissão são aplicados.',\n    'audit_event_filter' => 'Filtro de Evento',\n    'audit_event_filter_no_filter' => 'Sem filtro',\n    'audit_deleted_item' => 'Item excluído',\n    'audit_deleted_item_name' => 'Nome: :name',\n    'audit_table_user' => 'Utilizador',\n    'audit_table_event' => 'Evento',\n    'audit_table_related' => 'Item ou Detalhe Relacionado',\n    'audit_table_ip' => 'Endereço de IP',\n    'audit_table_date' => 'Data da Atividade',\n    'audit_date_from' => 'Intervalo De',\n    'audit_date_to' => 'Intervalo Até',\n\n    // Role Settings\n    'roles' => 'Cargos',\n    'role_user_roles' => 'Cargos de Utilizador',\n    'roles_index_desc' => 'Papéis são usados para agrupar utilizadores & fornecer permissão ao sistema para os seus membros. Quando um utilizador é membro de múltiplas funções, os privilégios concedidos irão acumular e o utilizador herdará todas as habilidades.',\n    'roles_x_users_assigned' => ':count utilizadores atribuído|:count utilizadores atribuídos',\n    'roles_x_permissions_provided' => ':count permissão|:count permissões',\n    'roles_assigned_users' => 'Utilizadores atribuídos',\n    'roles_permissions_provided' => 'Permissões fornecidas',\n    'role_create' => 'Criar novo Cargo',\n    'role_delete' => 'Excluir Cargo',\n    'role_delete_confirm' => 'A ação vai eliminar o cargo de nome \\':roleName\\'.',\n    'role_delete_users_assigned' => 'Esse cargo tem :userCount utilizadores vinculados nele. Se quiser migrar utilizadores deste cargo para outro, selecione um novo cargo.',\n    'role_delete_no_migration' => \"Não migrar utilizadores\",\n    'role_delete_sure' => 'Tem certeza que deseja excluir este cargo?',\n    'role_edit' => 'Editar Cargo',\n    'role_details' => 'Detalhes do Cargo',\n    'role_name' => 'Nome do Cargo',\n    'role_desc' => 'Breve Descrição do Cargo',\n    'role_mfa_enforced' => 'Exige autenticação de múltiplos fatores',\n    'role_external_auth_id' => 'IDs de Autenticação Externa',\n    'role_system' => 'Permissões do Sistema',\n    'role_manage_users' => 'Gerir utilizadores',\n    'role_manage_roles' => 'Gerir cargos e permissões de cargos',\n    'role_manage_entity_permissions' => 'Gerir todos os livros, capítulos e permissões de páginas',\n    'role_manage_own_entity_permissions' => 'Gerir permissões de seu próprio livro, capítulo e paginas',\n    'role_manage_page_templates' => 'Gerir modelos de página',\n    'role_access_api' => 'Aceder à API do sistema',\n    'role_manage_settings' => 'Gerir as configurações da aplicação',\n    'role_export_content' => 'Exportar conteúdo',\n    'role_import_content' => 'Import content',\n    'role_editor_change' => 'Alterar editor de página',\n    'role_notifications' => 'Receive & manage notifications',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Permissões de Ativos',\n    'roles_system_warning' => 'Esteja ciente de que o acesso a qualquer uma das três permissões acima pode permitir que um utilizador altere os seus próprios privilégios ou privilégios de outros no sistema. Apenas atribua cargos com essas permissões a utilizadores de confiança.',\n    'role_asset_desc' => 'Estas permissões controlam o acesso padrão para os ativos dentro do sistema. Permissões em Livros, Capítulos e Páginas serão sobrescritas por estas permissões.',\n    'role_asset_admins' => 'Os administradores recebem automaticamente acesso a todo o conteúdo, mas estas opções podem mostrar ou ocultar as opções da Interface de Usuário.',\n    'role_asset_image_view_note' => 'Isto está relacionado com a visibilidade do gerenciador de imagens. O acesso real dos arquivos de imagem enviados dependerá da opção de armazenamento de imagens do sistema.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'Todos',\n    'role_own' => 'Próprio',\n    'role_controlled_by_asset' => 'Controlado pelo ativo para o qual eles são enviados',\n    'role_save' => 'Guardar Cargo',\n    'role_users' => 'Utilizadores com este cargo',\n    'role_users_none' => 'Nenhum utilizador está atualmente vinculado a este cargo',\n\n    // Users\n    'users' => 'Utilizadores',\n    'users_index_desc' => 'Crie & gira individualmente contas de utilizador no sistema. Contas de utilizador são usadas para iniciar sessão e atribuição de conteúdo & atividade. As permissões de acesso são principalmente baseadas em funções, mas a propriedade de conteúdo do utilizador, entre outros fatores, também pode afetar permissões e acesso.',\n    'user_profile' => 'Perfil do Utilizador',\n    'users_add_new' => 'Adicionar Novo Utilizador',\n    'users_search' => 'Pesquisar Utilizadores',\n    'users_latest_activity' => 'Última atividade',\n    'users_details' => 'Detalhes do Utilizador',\n    'users_details_desc' => 'Defina um nome de exibição e um endereço de e-mail para este utilizador. O endereço de e-mail será utilizado na autenticação da aplicação.',\n    'users_details_desc_no_email' => 'Defina um nome de exibição para este utilizador para que outros possam reconhecê-lo.',\n    'users_role' => 'Cargos do Utilizador',\n    'users_role_desc' => 'Selecione os cargos aos quais este utilizador será vinculado. Se um utilizador for vinculado a múltiplos cargos, as suas permissões serão empilhadas e ele receberá todas as habilidades dos cargos atribuídos.',\n    'users_password' => 'Palavra-passe do Utilizador',\n    'users_password_desc' => 'Defina uma palavra-passe para efetuar a autenticação na aplicação. Esta deve ter pelo menos 8 caracteres.',\n    'users_send_invite_text' => 'Pode escolher enviar a este utilizador um convite por e-mail que o possibilitará definir a sua própria palavra-passe, ou defina você mesmo uma.',\n    'users_send_invite_option' => 'Enviar convite por e-mail',\n    'users_external_auth_id' => 'ID de Autenticação Externa',\n    'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.',\n    'users_password_warning' => 'Only fill the below if you would like to change the password for this user.',\n    'users_system_public' => 'Este utilizador representa quaisquer convidados que visitam a aplicação. Não pode ser utilizado para efetuar autenticação, mas é automaticamente atribuído.',\n    'users_delete' => 'Eliminar Utilizador',\n    'users_delete_named' => 'Eliminar :userName',\n    'users_delete_warning' => 'A ação vai eliminar completamente o utilizador de nome \\':userName\\' do sistema.',\n    'users_delete_confirm' => 'Tem certeza que eliminar este utilizador?',\n    'users_migrate_ownership' => 'Migrar Posse',\n    'users_migrate_ownership_desc' => 'Selecione um utilizador aqui se desejar que outro se torne o proprietário de todos os itens atualmente pertencentes a este.',\n    'users_none_selected' => 'Nenhum utilizador selecionado',\n    'users_edit' => 'Editar Utilizador',\n    'users_edit_profile' => 'Editar Perfil',\n    'users_avatar' => 'Avatar do Utilizador',\n    'users_avatar_desc' => 'Defina uma imagem para representar este utilizador. Deve ser um quadrado com aproximadamente 256px de altura e largura.',\n    'users_preferred_language' => 'Linguagem de Preferência',\n    'users_preferred_language_desc' => 'Esta opção irá alterar o idioma utilizado para a interface de utilizador da aplicação. Isto não afetará nenhum conteúdo criado por utilizadores.',\n    'users_social_accounts' => 'Contas Sociais',\n    'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.',\n    'users_social_accounts_info' => 'Aqui pode ligar outras contas para acesso mais rápido. Desligar uma conta não retira a possibilidade de acesso usando-a. Para revogar o acesso ao perfil através da conta social, você deverá fazê-lo na sua conta social.',\n    'users_social_connect' => 'Contas Associadas',\n    'users_social_disconnect' => 'Dissociar Conta',\n    'users_social_status_connected' => 'Conectado',\n    'users_social_status_disconnected' => 'Desconectado',\n    'users_social_connected' => 'A conta:socialAccount foi associada com sucesso ao seu perfil.',\n    'users_social_disconnected' => 'A conta:socialAccount foi dissociada com sucesso de seu perfil.',\n    'users_api_tokens' => 'Tokens de API',\n    'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.',\n    'users_api_tokens_none' => 'Nenhum token de API foi criado para este utilizador',\n    'users_api_tokens_create' => 'Criar Token',\n    'users_api_tokens_expires' => 'Expira',\n    'users_api_tokens_docs' => 'Documentação da API',\n    'users_mfa' => 'Autenticação Multi-fator',\n    'users_mfa_desc' => 'Configure a autenticação multi-fatores como uma camada extra de segurança para sua conta de utilizador.',\n    'users_mfa_x_methods' => ':count método configurado|:count métodos configurados',\n    'users_mfa_configure' => 'Configurar Métodos',\n\n    // API Tokens\n    'user_api_token_create' => 'Criar Token de API',\n    'user_api_token_name' => 'Nome',\n    'user_api_token_name_desc' => 'Dê ao seu token um nome legível como um futuro lembrete de seu propósito.',\n    'user_api_token_expiry' => 'Data de Expiração',\n    'user_api_token_expiry_desc' => 'Defina uma data em que este token expira. Depois desta data, as requisições feitas usando este token deixarão de funcionar. Deixar este campo em branco definirá um prazo de 100 anos futuros.',\n    'user_api_token_create_secret_message' => 'Imediatamente após a criação deste token, um \"ID de token\" e \"Segredo de token\" serão gerados e exibidos. O segredo só será mostrado uma única vez, portanto, certifique-se de copiar o valor para algum lugar seguro antes de prosseguir.',\n    'user_api_token' => 'Token de API',\n    'user_api_token_id' => 'ID do Token',\n    'user_api_token_id_desc' => 'Este é um identificador de sistema não editável, gerado para este token, que precisará ser fornecido em solicitações de API.',\n    'user_api_token_secret' => 'Segredo do Token',\n    'user_api_token_secret_desc' => 'Este é um segredo de sistema gerado para este token que precisará ser fornecido em requisições de API. Isto só será mostrado nesta única vez, portanto, copie este valor para um lugar seguro.',\n    'user_api_token_created' => 'Token criado a :timeAgo',\n    'user_api_token_updated' => 'Token atualizado a :timeAgo',\n    'user_api_token_delete' => 'Eliminar Token',\n    'user_api_token_delete_warning' => 'Isto irá excluir completamente este token de API com o nome \\':tokenName\\' do sistema.',\n    'user_api_token_delete_confirm' => 'Tem certeza que deseja eliminar este token de API?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhooks são uma maneira de enviar dados para URLs externas quando certas ações e eventos ocorrem no sistema. Isto permite uma integração baseada em eventos com plataformas externas como mensagens ou sistemas de notificação.',\n    'webhooks_x_trigger_events' => ':count acionador|:count acionadores',\n    'webhooks_create' => 'Criar um novo webhook',\n    'webhooks_none_created' => 'Ainda nenhum webhooks foi criado.',\n    'webhooks_edit' => 'Editar Webhook',\n    'webhooks_save' => 'Guardar Webhook',\n    'webhooks_details' => 'Detalhes do Webhook',\n    'webhooks_details_desc' => 'Providencie um nome fácil e um endpoint POST para onde os dados do webhook serão enviados.',\n    'webhooks_events' => 'Eventos de Webhook',\n    'webhooks_events_desc' => 'Selecionar todos os eventos que devem acionar este webhook.',\n    'webhooks_events_warning' => 'Tenha em mente que esses eventos serão acionados para todos os eventos selecionados, mesmo se as permissões personalizadas forem aplicadas. Certifique-se de que o uso deste webhook não exponha conteúdo confidencial.',\n    'webhooks_events_all' => 'Todos os eventos do sistema',\n    'webhooks_name' => 'Nome do Webhook',\n    'webhooks_timeout' => 'Tempo máximo de solicitação do webhook(segundos)',\n    'webhooks_endpoint' => 'Endpoint do Webhook',\n    'webhooks_active' => 'Webhook ativo',\n    'webhook_events_table_header' => 'Eventos',\n    'webhooks_delete' => 'Eliminar Webhook',\n    'webhooks_delete_warning' => 'Isto irá excluir completamente, do sistema, este API token com o nome \\':tokenName\\'.',\n    'webhooks_delete_confirm' => 'Tem a certeza que deseja eliminar este webhook?',\n    'webhooks_format_example' => 'Exemplo de formato Webhook',\n    'webhooks_format_example_desc' => 'Os dados do Webhook são enviados como uma solicitação POST para o endpoint configurado em JSON seguindo o formato abaixo. As propriedades \"related_item\" e \"url\" são opcionais e dependerão do tipo de evento acionado.',\n    'webhooks_status' => 'Estado do webhook',\n    'webhooks_last_called' => 'Última chamada:',\n    'webhooks_last_errored' => 'Último erro:',\n    'webhooks_last_error_message' => 'Última mensagem de erro:',\n\n    // Licensing\n    'licenses' => 'Licenses',\n    'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.',\n    'licenses_bookstack' => 'BookStack License',\n    'licenses_php' => 'PHP Library Licenses',\n    'licenses_js' => 'JavaScript Library Licenses',\n    'licenses_other' => 'Other Licenses',\n    'license_details' => 'License Details',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Catalão',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/pt/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => 'O campo :attribute deve ser aceite.',\n    'active_url'           => 'O campo :attribute não é um URL válido.',\n    'after'                => 'O campo :attribute deve ser uma data posterior à data :date.',\n    'alpha'                => 'O campo :attribute deve conter apenas letras.',\n    'alpha_dash'           => 'O campo :attribute deve conter apenas letras, números, traços e sublinhado.',\n    'alpha_num'            => 'O campo :attribute deve conter apenas letras e números.',\n    'array'                => 'O campo :attribute deve ser uma lista(array).',\n    'backup_codes'         => 'O código fornecido não é válido ou já foi utilizado.',\n    'before'               => 'O campo :attribute deve ser uma data anterior à data :date.',\n    'between'              => [\n        'numeric' => 'O campo :attribute deve estar entre :min e :max.',\n        'file'    => 'O campo :attribute deve ter entre :min e :max kilobytes.',\n        'string'  => 'O campo :attribute deve ter entre :min e :max caracteres.',\n        'array'   => 'O campo :attribute deve ter entre :min e :max itens.',\n    ],\n    'boolean'              => 'O campo :attribute deve ser verdadeiro ou falso.',\n    'confirmed'            => 'O campo :attribute não é igual à sua confirmação.',\n    'date'                 => 'O campo :attribute não está num formato de data válido.',\n    'date_format'          => 'O campo :attribute não tem a formatação :format.',\n    'different'            => 'O campo :attribute e o campo :other devem ser diferentes.',\n    'digits'               => 'O campo :attribute deve ter :digits dígitos.',\n    'digits_between'       => 'O campo :attribute deve ter entre :min e :max dígitos.',\n    'email'                => 'O campo :attribute deve ser um endereço de e-mail válido.',\n    'ends_with' => 'O campo :attribute deve terminar com um dos seguintes: :values',\n    'file'                 => 'O :attribute deve ser um arquivo válido.',\n    'filled'               => 'O campo :attribute é requerido.',\n    'gt'                   => [\n        'numeric' => 'O campo :attribute deve ser maior que :value.',\n        'file'    => 'O campo :attribute deve ser maior que :value kilobytes.',\n        'string'  => 'O campo :attribute deve ser maior que :value caracteres.',\n        'array'   => 'O campo :attribute deve ter mais que :value itens.',\n    ],\n    'gte'                  => [\n        'numeric' => 'O campo :attribute deve ser maior ou igual a :value.',\n        'file'    => 'O campo :attribute deve ser maior ou igual a :value kilobytes.',\n        'string'  => 'O campo :attribute deve ser maior ou igual a :value caracteres.',\n        'array'   => 'O campo :attribute deve ter :value itens ou mais.',\n    ],\n    'exists'               => 'O campo :attribute selecionado não é válido.',\n    'image'                => 'O campo :attribute deve ser uma imagem.',\n    'image_extension'      => 'O campo :attribute deve ter uma extensão de imagem válida e suportada.',\n    'in'                   => 'O campo :attribute selecionado não é válido.',\n    'integer'              => 'O campo :attribute deve ser um número inteiro.',\n    'ip'                   => 'O campo :attribute deve ser um endereço IP válido.',\n    'ipv4'                 => 'O campo :attribute deve ser um endereço IPv4 válido.',\n    'ipv6'                 => 'O campo :attribute deve ser um endereço IPv6 válido.',\n    'json'                 => 'O campo :attribute deve ser uma string JSON válida.',\n    'lt'                   => [\n        'numeric' => 'O campo :attribute deve ser menor que :value.',\n        'file'    => 'O campo :attribute deve ser menor que :value kilobytes.',\n        'string'  => 'O campo :attribute deve ser menor que :value caracteres.',\n        'array'   => 'O campo :attribute deve conter menos que :value itens.',\n    ],\n    'lte'                  => [\n        'numeric' => 'O campo :attribute deve ser menor ou igual a :value.',\n        'file'    => 'O campo :attribute deve ser menor ou igual a :value kilobytes.',\n        'string'  => 'O campo :attribute deve ser menor ou igual a :value caracteres.',\n        'array'   => 'O campo :attribute não deve conter mais que :value itens.',\n    ],\n    'max'                  => [\n        'numeric' => 'O valor para o campo :attribute não deve ser maior que :max.',\n        'file'    => 'O valor para o campo :attribute não deve ter tamanho maior que :max kilobytes.',\n        'string'  => 'O valor para o campo :attribute não deve ter mais que :max caracteres.',\n        'array'   => 'O valor para o campo :attribute não deve ter mais que :max itens.',\n    ],\n    'mimes'                => 'O campo :attribute deve ser do tipo type: :values.',\n    'min'                  => [\n        'numeric' => 'O campo :attribute não deve ser menor que :min.',\n        'file'    => 'O campo :attribute não deve ter tamanho menor que :min kilobytes.',\n        'string'  => 'O campo :attribute não deve ter menos que :min caracteres.',\n        'array'   => 'O campo :attribute não deve ter menos que :min itens.',\n    ],\n    'not_in'               => 'O campo selecionado :attribute é inválido.',\n    'not_regex'            => 'O formato do campo :attribute é inválido.',\n    'numeric'              => 'O campo :attribute deve ser um número.',\n    'regex'                => 'O formato do campo :attribute é inválido.',\n    'required'             => 'O campo :attribute é requerido.',\n    'required_if'          => 'O campo :attribute é requerido quando o campo :other tem valor :value.',\n    'required_with'        => 'O campo :attribute é requerido quando os valores :values estiverem presentes.',\n    'required_with_all'    => 'O campo :attribute é requerido quando os valores :values estiverem presentes.',\n    'required_without'     => 'O campo :attribute é requerido quando os valores :values não estiverem presentes.',\n    'required_without_all' => 'O campo :attribute é requerido quando nenhum dos valores :values estiverem presentes.',\n    'same'                 => 'O campo :attribute e o campo :other devem ser iguais.',\n    'safe_url'             => 'O link fornecido poderá não ser seguro.',\n    'size'                 => [\n        'numeric' => 'O tamanho do campo :attribute deve ser :size.',\n        'file'    => 'O tamanho do arquivo :attribute deve ser de :size kilobytes.',\n        'string'  => 'O tamanho do campo :attribute deve ser de :size caracteres.',\n        'array'   => 'O campo :attribute deve conter :size itens.',\n    ],\n    'string'               => 'O campo :attribute deve ser uma string.',\n    'timezone'             => 'O campo :attribute deve conter uma timezone válida.',\n    'totp'                 => 'O código fornecido não é válido ou já expirou.',\n    'unique'               => 'Já existe um campo/dado de nome :attribute.',\n    'url'                  => 'O formato da URL :attribute é inválido.',\n    'uploaded'             => 'O arquivo não pôde ser carregado. O servidor pode não aceitar arquivos deste tamanho.',\n\n    'zip_file' => 'The :attribute needs to reference a file within the ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',\n    'zip_model_expected' => 'Data object expected but \":type\" found.',\n    'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Confirmação de senha requerida',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/pt_BR/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'criou a página',\n    'page_create_notification'    => 'Página criada com sucesso',\n    'page_update'                 => 'atualizou a página',\n    'page_update_notification'    => 'Página atualizada com sucesso',\n    'page_delete'                 => 'excluiu a página',\n    'page_delete_notification'    => 'Página excluída com sucesso',\n    'page_restore'                => 'restaurou a página',\n    'page_restore_notification'   => 'Página restaurada com sucesso',\n    'page_move'                   => 'moveu a página',\n    'page_move_notification'      => 'Página movida com sucesso',\n\n    // Chapters\n    'chapter_create'              => 'criou o capítulo',\n    'chapter_create_notification' => 'Capítulo criado com sucesso',\n    'chapter_update'              => 'atualizou o capítulo',\n    'chapter_update_notification' => 'Capítulo atualizado com sucesso',\n    'chapter_delete'              => 'excluiu o capítulo',\n    'chapter_delete_notification' => 'Capítulo excluída com sucesso',\n    'chapter_move'                => 'moveu o capítulo',\n    'chapter_move_notification' => 'Capítulo excluído com sucesso',\n\n    // Books\n    'book_create'                 => 'criou o livro',\n    'book_create_notification'    => 'Livro criado com sucesso',\n    'book_create_from_chapter'              => 'capítulo convertido em livro',\n    'book_create_from_chapter_notification' => 'Capítulo convertido em livro com sucesso',\n    'book_update'                 => 'atualizou o livro',\n    'book_update_notification'    => 'Livro atualizado com sucesso',\n    'book_delete'                 => 'excluiu o livro',\n    'book_delete_notification'    => 'Livro excluído com sucesso',\n    'book_sort'                   => 'ordenou o livro',\n    'book_sort_notification'      => 'Livro reordenado com sucesso',\n\n    // Bookshelves\n    'bookshelf_create'            => 'estante criada',\n    'bookshelf_create_notification'    => 'Estante criada com sucesso',\n    'bookshelf_create_from_book'    => 'livro convertido em estante',\n    'bookshelf_create_from_book_notification'    => 'Livro convertido com sucesso em uma estante',\n    'bookshelf_update'                 => 'estante atualizada',\n    'bookshelf_update_notification'    => 'Estante atualizada com sucesso',\n    'bookshelf_delete'                 => 'estante excluída',\n    'bookshelf_delete_notification'    => 'Estante excluída com sucesso',\n\n    // Revisions\n    'revision_restore' => 'revisão restaurada',\n    'revision_delete' => 'revisão excluída',\n    'revision_delete_notification' => 'Revisão excluída com sucesso',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" foi adicionada aos seus favoritos',\n    'favourite_remove_notification' => '\":name\" foi removida dos seus favoritos',\n\n    // Watching\n    'watch_update_level_notification' => 'Preferências de Observação atualizadas com sucesso',\n\n    // Auth\n    'auth_login' => 'conectado',\n    'auth_register' => 'registrado como novo usuário',\n    'auth_password_reset_request' => 'redefinir senha do usuário solicitado',\n    'auth_password_reset_update' => 'redefinir senha do usuário',\n    'mfa_setup_method' => 'método MFA configurado',\n    'mfa_setup_method_notification' => 'Método de multi-fatores configurado com sucesso',\n    'mfa_remove_method' => 'Método MFA removido',\n    'mfa_remove_method_notification' => 'Método de multi-fatores removido com sucesso',\n\n    // Settings\n    'settings_update' => 'configurações atualizadas',\n    'settings_update_notification' => 'Configurações atualizadas com sucesso',\n    'maintenance_action_run' => 'Ação de manutenção executada',\n\n    // Webhooks\n    'webhook_create' => 'webhook criado',\n    'webhook_create_notification' => 'Webhook criado com sucesso',\n    'webhook_update' => 'webhook atualizado',\n    'webhook_update_notification' => 'Webhook atualizado com sucesso',\n    'webhook_delete' => 'webhook excluído',\n    'webhook_delete_notification' => 'Webhook excluido com sucesso',\n\n    // Imports\n    'import_create' => 'importação criada',\n    'import_create_notification' => 'Importação carregada com sucesso',\n    'import_run' => 'importação atualizada',\n    'import_run_notification' => 'Conteúdo importado com sucesso',\n    'import_delete' => 'importação excluída',\n    'import_delete_notification' => 'Importação excluída com sucesso',\n\n    // Users\n    'user_create' => 'usuário criado',\n    'user_create_notification' => 'Usuário criado com sucesso',\n    'user_update' => 'usuário atualizado',\n    'user_update_notification' => 'Usuário atualizado com sucesso',\n    'user_delete' => 'usuário excluído',\n    'user_delete_notification' => 'Usuário removido com sucesso',\n\n    // API Tokens\n    'api_token_create' => 'token de API criado',\n    'api_token_create_notification' => 'Token de API criado com sucesso',\n    'api_token_update' => 'token de API atualizado',\n    'api_token_update_notification' => 'Token de API atualizado com sucesso',\n    'api_token_delete' => 'token de API excluído',\n    'api_token_delete_notification' => 'Token de API excluído com sucesso',\n\n    // Roles\n    'role_create' => 'perfil criado',\n    'role_create_notification' => 'Perfil criado com sucesso',\n    'role_update' => 'perfil atualizado',\n    'role_update_notification' => 'Perfil atualizado com sucesso',\n    'role_delete' => 'Excluir papel',\n    'role_delete_notification' => 'Perfil excluído com sucesso',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'lixeira esvaziada',\n    'recycle_bin_restore' => 'restaurado da lixeira',\n    'recycle_bin_destroy' => 'removido da lixeira',\n\n    // Comments\n    'commented_on'                => 'comentou em',\n    'comment_create'              => 'Adicionou comentário',\n    'comment_update'              => 'Atualizar descrição',\n    'comment_delete'              => 'Comentário deletado',\n\n    // Sort Rules\n    'sort_rule_create' => 'criou regra de ordenação',\n    'sort_rule_create_notification' => 'Regra de ordenação criada com sucesso',\n    'sort_rule_update' => 'atualizou regra de ordenação',\n    'sort_rule_update_notification' => 'Regra de ordenação atualizada com sucesso',\n    'sort_rule_delete' => 'excluiu regra de ordenação',\n    'sort_rule_delete_notification' => 'Regra de ordenação excluída com sucesso',\n\n    // Other\n    'permissions_update'          => 'atualizou permissões',\n];\n"
  },
  {
    "path": "lang/pt_BR/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'As credenciais fornecidas não puderam ser validadas em nossos registros.',\n    'throttle' => 'Muitas tentativas de login. Por favor, tente novamente em :seconds segundos.',\n\n    // Login & Register\n    'sign_up' => 'Criar Conta',\n    'log_in' => 'Entrar',\n    'log_in_with' => 'Entrar com :socialDriver',\n    'sign_up_with' => 'Cadastre-se com :socialDriver',\n    'logout' => 'Sair',\n\n    'name' => 'Nome',\n    'username' => 'Nome de Usuário',\n    'email' => 'E-mail',\n    'password' => 'Senha',\n    'password_confirm' => 'Confirmar Senha',\n    'password_hint' => 'Deve conter pelo menos 8 caracteres',\n    'forgot_password' => 'Esqueceu a senha?',\n    'remember_me' => 'Lembrar de mim',\n    'ldap_email_hint' => 'Por favor, digite um e-mail para essa conta.',\n    'create_account' => 'Criar Conta',\n    'already_have_account' => 'Já possui uma conta?',\n    'dont_have_account' => 'Não possui uma conta?',\n    'social_login' => 'Login Social',\n    'social_registration' => 'Cadastro Social',\n    'social_registration_text' => 'Cadastre-se e entre utilizando outro serviço.',\n\n    'register_thanks' => 'Obrigado por se cadastrar!',\n    'register_confirm' => 'Por favor, verifique seu e-mail e clique no botão de confirmação para acessar :appName.',\n    'registrations_disabled' => 'Cadastros estão temporariamente desabilitados',\n    'registration_email_domain_invalid' => 'O domínio de e-mail usado não tem acesso permitido a essa aplicação',\n    'register_success' => 'Obrigado por se cadastrar! Você agora encontra-se cadastrado(a) e logado(a).',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Tentando fazer login',\n    'auto_init_starting_desc' => 'Estamos entrando em contato com seu sistema de autenticação para iniciar o processo de login. Se não houver progresso após 5 segundos, você pode tentar clicar no link abaixo.',\n    'auto_init_start_link' => 'Prossiga com a autenticação',\n\n    // Password Reset\n    'reset_password' => 'Redefinir Senha',\n    'reset_password_send_instructions' => 'Insira seu e-mail abaixo e uma mensagem com o link de redefinição de senha lhe será enviada.',\n    'reset_password_send_button' => 'Enviar o Link de Redefinição',\n    'reset_password_sent' => 'Um link de redefinição de senha será enviado para :email se o endereço de e-mail for encontrado no sistema.',\n    'reset_password_success' => 'Sua senha foi redefinida com sucesso.',\n    'email_reset_subject' => 'Redefina a senha de :appName',\n    'email_reset_text' => 'Você recebeu esse e-mail pois recebemos uma solicitação de redefinição de senha para a sua conta.',\n    'email_reset_not_requested' => 'Caso não tenha sido você a solicitar a redefinição de senha, ignore esse e-mail.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Confirme seu e-mail para :appName',\n    'email_confirm_greeting' => 'Obrigado por se cadastrar em :appName!',\n    'email_confirm_text' => 'Por favor, confirme seu endereço de e-mail clicando no botão abaixo:',\n    'email_confirm_action' => 'Confirmar E-mail',\n    'email_confirm_send_error' => 'A confirmação de e-mail é requerida, mas o sistema não pôde enviar a mensagem. Por favor, entre em contato com o administrador para se certificar que o serviço de envio de e-mails está corretamente configurado.',\n    'email_confirm_success' => 'Seu e-mail foi confirmado! Agora você pode de entrar usando este endereço de e-mail.',\n    'email_confirm_resent' => 'E-mail de confirmação reenviado. Por favor, verifique sua caixa de entrada.',\n    'email_confirm_thanks' => 'Obrigado por confirmar!',\n    'email_confirm_thanks_desc' => 'Aguarde um momento enquanto sua confirmação é processada. Se você não for redirecionado após 3 segundos, pressione o link \"Continuar\" abaixo para continuar.',\n\n    'email_not_confirmed' => 'Endereço de E-mail Não Confirmado',\n    'email_not_confirmed_text' => 'Seu endereço de e-mail ainda não foi confirmado.',\n    'email_not_confirmed_click_link' => 'Por favor, clique no link no e-mail que foi enviado após o cadastro.',\n    'email_not_confirmed_resend' => 'Caso não encontre o e-mail você poderá reenviar a confirmação usando o formulário abaixo.',\n    'email_not_confirmed_resend_button' => 'Reenviar o E-mail de Confirmação',\n\n    // User Invite\n    'user_invite_email_subject' => 'Você recebeu um convite para :appName!',\n    'user_invite_email_greeting' => 'Uma conta foi criada para você em :appName.',\n    'user_invite_email_text' => 'Clique no botão abaixo para definir uma senha de conta e obter acesso:',\n    'user_invite_email_action' => 'Defina a Senha da Conta',\n    'user_invite_page_welcome' => 'Bem-vindo(a) a :appName!',\n    'user_invite_page_text' => 'Para finalizar sua conta e obter acesso, você precisa definir uma senha que será usada para efetuar login em :appName em futuras visitas.',\n    'user_invite_page_confirm_button' => 'Confirmar Senha',\n    'user_invite_success_login' => 'Senha definida, agora você pode fazer login usando sua senha para acessar :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Configurar autenticação multi-fator',\n    'mfa_setup_desc' => 'A autenticação multi-fator adiciona outra camada de segurança à sua conta.',\n    'mfa_setup_configured' => 'Configurado',\n    'mfa_setup_reconfigure' => 'Reconfigurar',\n    'mfa_setup_remove_confirmation' => 'Você tem certeza que deseja remover o método de autenticação de vários fatores?',\n    'mfa_setup_action' => 'Configurações',\n    'mfa_backup_codes_usage_limit_warning' => 'Você tem menos de 5 códigos de backup restantes, Por favor, gere e armazene um novo conjunto antes de esgotar suas opções de códigos de backup para evitar estar bloqueado para fora da sua conta.',\n    'mfa_option_totp_title' => 'Aplicativo Móvel',\n    'mfa_option_totp_desc' => 'Para usar a autenticação multi-fator, você precisará de um aplicativo móvel que suporte TOTP como o Google Authenticator, Authy ou o Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Códigos de backup',\n    'mfa_option_backup_codes_desc' => 'Gera um conjunto de códigos de backup de uso único que você inserirá no login para verificar sua identidade. Certifique-se de armazená-los em um local seguro e protegido.',\n    'mfa_gen_confirm_and_enable' => 'Confirmar e habilitar',\n    'mfa_gen_backup_codes_title' => 'Configuração dos Códigos de Backup',\n    'mfa_gen_backup_codes_desc' => 'Armazene a lista de códigos abaixo em um lugar seguro. Ao acessar o sistema você poderá usar um dos códigos como segundo mecanismo de autenticação.',\n    'mfa_gen_backup_codes_download' => 'Baixar códigos',\n    'mfa_gen_backup_codes_usage_warning' => 'Cada código só poderá ser usado uma vez',\n    'mfa_gen_totp_title' => 'Configuração de Aplicativos Móveis',\n    'mfa_gen_totp_desc' => 'Para usar a autenticação multi-fator, você precisará de um aplicativo móvel que suporte TOTP como o Google Authenticator, Authy ou o Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Leia o código QR abaixo usando o aplicativo de autenticação de sua preferência para começar.',\n    'mfa_gen_totp_verify_setup' => 'Verificar configuração',\n    'mfa_gen_totp_verify_setup_desc' => 'Verifique se tudo está funcionando digitando um código, gerado dentro do seu aplicativo de autenticação, na caixa de entrada abaixo:',\n    'mfa_gen_totp_provide_code_here' => 'Insira o código gerado pelo aplicativo aqui',\n    'mfa_verify_access' => 'Verificar Acesso',\n    'mfa_verify_access_desc' => 'Sua conta de usuário requer que você confirme sua identidade por meio de um nível adicional de verificação antes de conceder o acesso. Verifique o uso de um dos métodos configurados para continuar.',\n    'mfa_verify_no_methods' => 'Nenhum método configurado',\n    'mfa_verify_no_methods_desc' => 'Nenhum método de autenticação multi-fator foi encontrado em sua conta. Você precisará configurar pelo menos um método antes de ter acesso.',\n    'mfa_verify_use_totp' => 'Verificar usando um aplicativo móvel',\n    'mfa_verify_use_backup_codes' => 'Verificar usando um código de backup',\n    'mfa_verify_backup_code' => 'Código de backup',\n    'mfa_verify_backup_code_desc' => 'Insira um dos seus códigos de backup restantes abaixo:',\n    'mfa_verify_backup_code_enter_here' => 'Digite o código de backup',\n    'mfa_verify_totp_desc' => 'Digite o código, gerado através do seu aplicativo móvel, abaixo:',\n    'mfa_setup_login_notification' => 'Método de multi-fatores configurado, por favor faça login novamente usando o método configurado.',\n];\n"
  },
  {
    "path": "lang/pt_BR/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Cancelar',\n    'close' => 'Fechar',\n    'confirm' => 'Confirmar',\n    'back' => 'Voltar',\n    'save' => 'Salvar',\n    'continue' => 'Continuar',\n    'select' => 'Selecionar',\n    'toggle_all' => 'Alternar Tudo',\n    'more' => 'Mais\n',\n\n    // Form Labels\n    'name' => 'Nome\n',\n    'description' => 'Descrição\n',\n    'role' => 'Perfil',\n    'cover_image' => 'Imagem da capa',\n    'cover_image_description' => 'Esta imagem deve ter cerca de 440x250px, embora seja dimensionada e cortada de forma flexível para se ajustar à \"interface\" do usuário em diferentes cenários, conforme necessário, portanto, as dimensões reais para exibição serão diferentes.',\n\n    // Actions\n    'actions' => 'Ações\n',\n    'view' => 'Visualizar',\n    'view_all' => 'Visualizar Tudo\n',\n    'new' => 'Novo',\n    'create' => 'Criar',\n    'update' => 'Atualizar',\n    'edit' => 'Editar',\n    'archive' => 'Arquivar',\n    'unarchive' => 'Desarquivar',\n    'sort' => 'Ordenar',\n    'move' => 'Mover',\n    'copy' => 'Copiar',\n    'reply' => 'Responder',\n    'delete' => 'Excluir',\n    'delete_confirm' => 'Confirmar Exclusão',\n    'search' => 'Pesquisar',\n    'search_clear' => 'Limpar Pesquisa',\n    'reset' => 'Redefinir',\n    'remove' => 'Remover',\n    'add' => 'Adicionar',\n    'configure' => 'Configurar',\n    'manage' => 'Administrar',\n    'fullscreen' => 'Tela cheia',\n    'favourite' => 'Favorito',\n    'unfavourite' => 'Remover dos Favoritos',\n    'next' => 'Seguinte',\n    'previous' => 'Anterior',\n    'filter_active' => 'Filtro Ativo:',\n    'filter_clear' => 'Limpar Filtro',\n    'download' => 'Baixar ',\n    'open_in_tab' => 'Abrir na aba',\n    'open' => 'Abrir',\n\n    // Sort Options\n    'sort_options' => 'Opções de Ordenação',\n    'sort_direction_toggle' => 'Alternar Direção de Ordenação',\n    'sort_ascending' => 'Ordenação Crescente',\n    'sort_descending' => 'Ordenação Decrescente',\n    'sort_name' => 'Nome',\n    'sort_default' => 'Padrão',\n    'sort_created_at' => 'Data de Criação',\n    'sort_updated_at' => 'Data de Atualização',\n\n    // Misc\n    'deleted_user' => 'Usuário excluído',\n    'no_activity' => 'Nenhuma atividade a mostrar',\n    'no_items' => 'Nenhum item disponível',\n    'back_to_top' => 'Voltar ao topo',\n    'skip_to_main_content' => 'Ir para o conteúdo principal',\n    'toggle_details' => 'Alternar Detalhes',\n    'toggle_thumbnails' => 'Alternar Miniaturas',\n    'details' => 'Detalhes',\n    'grid_view' => 'Visualização em Grade',\n    'list_view' => 'Visualização em Lista',\n    'default' => 'Padrão',\n    'breadcrumb' => 'Caminho',\n    'status' => 'Status',\n    'status_active' => 'Ativo',\n    'status_inactive' => 'Inativo',\n    'never' => 'Nunca',\n    'none' => 'Nenhum',\n\n    // Header\n    'homepage' => 'Página inicial',\n    'header_menu_expand' => 'Expandir Cabeçalho do Menu',\n    'profile_menu' => 'Menu de Perfil',\n    'view_profile' => 'Visualizar Perfil',\n    'edit_profile' => 'Editar Perfil',\n    'dark_mode' => 'Modo Escuro',\n    'light_mode' => 'Modo Claro',\n    'global_search' => 'Pesquisa Global',\n\n    // Layout tabs\n    'tab_info' => 'Informações',\n    'tab_info_label' => 'Aba: Mostrar Informação Secundária',\n    'tab_content' => 'Conteúdo',\n    'tab_content_label' => 'Aba: Mostrar Conteúdo Primário',\n\n    // Email Content\n    'email_action_help' => 'Se você estiver tendo problemas ao clicar o botão \":actionText\", copie e cole a URL abaixo no seu navegador:',\n    'email_rights' => 'Todos os direitos reservados',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Políticas de Privacidade',\n    'terms_of_service' => 'Termos de Serviço',\n\n    // OpenSearch\n    'opensearch_description' => 'Procurar: nome do aplicativo',\n];\n"
  },
  {
    "path": "lang/pt_BR/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Selecionar Imagem',\n    'image_list' => 'Lista de imagens',\n    'image_details' => 'Detalhes da Imagem',\n    'image_upload' => 'Fazer upload de imagem',\n    'image_intro' => 'Aqui você pode selecionar e gerenciar imagens que foram previamente enviadas para o sistema.',\n    'image_intro_upload' => 'Faça upload de uma imagem arrastando um arquivo de imagem para esta janela, ou usando o botão \"Fazer upload de imagem\" acima.',\n    'image_all' => 'Todas',\n    'image_all_title' => 'Visualizar todas as imagens',\n    'image_book_title' => 'Visualizar imagens relacionadas a esse livro',\n    'image_page_title' => 'visualizar imagens relacionadas a essa página',\n    'image_search_hint' => 'Pesquisar imagem por nome',\n    'image_uploaded' => 'Adicionada em :uploadedDate',\n    'image_uploaded_by' => 'Enviado por :userName',\n    'image_uploaded_to' => 'Enviado para :pageLink',\n    'image_updated' => 'Atualizou :updateDate',\n    'image_load_more' => 'Carregar Mais',\n    'image_image_name' => 'Nome da Imagem',\n    'image_delete_used' => 'Essa imagem é usada nas páginas abaixo.',\n    'image_delete_confirm_text' => 'Tem certeza de que deseja excluir essa imagem?',\n    'image_select_image' => 'Selecionar Imagem',\n    'image_dropzone' => 'Arraste imagens ou clique aqui para fazer upload',\n    'image_dropzone_drop' => 'Arrastar imagens até aqui para fazer upload',\n    'images_deleted' => 'Imagens Excluídas',\n    'image_preview' => 'Pré-Visualização de Imagem',\n    'image_upload_success' => 'Upload de imagem efetuado com sucesso',\n    'image_update_success' => 'Detalhes da imagem atualizados com sucesso',\n    'image_delete_success' => 'Imagem excluída com sucesso',\n    'image_replace' => 'Substituir imagem',\n    'image_replace_success' => 'Arquivo de imagem atualizado com sucesso',\n    'image_rebuild_thumbs' => 'Gerar variações de tamanho',\n    'image_rebuild_thumbs_success' => 'Variações de tamanho da imagem geradas com sucesso!',\n\n    // Code Editor\n    'code_editor' => 'Editar Código',\n    'code_language' => 'Linguagem do Código',\n    'code_content' => 'Código',\n    'code_session_history' => 'Histórico de Sessão',\n    'code_save' => 'Salvar Código',\n];\n"
  },
  {
    "path": "lang/pt_BR/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Geral',\n    'advanced' => 'Avançado',\n    'none' => 'Nenhum',\n    'cancel' => 'Cancelar',\n    'save' => 'Salvar',\n    'close' => 'Fechar',\n    'apply' => 'Aplicar',\n    'undo' => 'Desfazer',\n    'redo' => 'Refazer',\n    'left' => 'Esquerda',\n    'center' => 'Centralizar',\n    'right' => 'Direita',\n    'top' => 'Topo',\n    'middle' => 'Meio',\n    'bottom' => 'Embaixo',\n    'width' => 'Largura',\n    'height' => 'Altura',\n    'More' => 'Mais',\n    'select' => 'Selecionar...',\n\n    // Toolbar\n    'formats' => 'Formatos',\n    'header_large' => 'Cabeçalho Grande',\n    'header_medium' => 'Cabeçalho Médio',\n    'header_small' => 'Cabeçalho Pequeno',\n    'header_tiny' => 'Cabeçalho Minúsculo',\n    'paragraph' => 'Parágrafo',\n    'blockquote' => 'Bloco de Citação',\n    'inline_code' => 'Código embutido',\n    'callouts' => 'Frase de destaque',\n    'callout_information' => 'Informação',\n    'callout_success' => 'Sucesso',\n    'callout_warning' => 'Atenção',\n    'callout_danger' => 'Perigo',\n    'bold' => 'Negrito',\n    'italic' => 'Itálico',\n    'underline' => 'Sublinhado',\n    'strikethrough' => 'Riscado',\n    'superscript' => 'Sobrescrito',\n    'subscript' => 'Subscrito',\n    'text_color' => 'Cor do texto',\n    'highlight_color' => 'Cor de destaque',\n    'custom_color' => 'Cor personalizada',\n    'remove_color' => 'Remover cor',\n    'background_color' => 'Cor de fundo',\n    'align_left' => 'Alinhar à esquerda',\n    'align_center' => 'Alinhar ao centro',\n    'align_right' => 'Alinhar à direita',\n    'align_justify' => 'Justificar',\n    'list_bullet' => 'Lista com marcadores',\n    'list_numbered' => 'Lista numerada',\n    'list_task' => 'Lista de tarefas',\n    'indent_increase' => 'Aumentar recuo',\n    'indent_decrease' => 'Diminuir recuo',\n    'table' => 'Tabela',\n    'insert_image' => 'Inserir Imagem',\n    'insert_image_title' => 'Inserir/Editar imagem',\n    'insert_link' => 'Inserir/editar link',\n    'insert_link_title' => 'Inserir/Editar link',\n    'insert_horizontal_line' => 'Insert horizontal line',\n    'insert_code_block' => 'Inserir/editar bloco de código',\n    'edit_code_block' => 'Editar bloco de código',\n    'insert_drawing' => 'Inserir/editar diagrama',\n    'drawing_manager' => 'Gerenciador de diagramas',\n    'insert_media' => 'Inserir/editar mídia',\n    'insert_media_title' => 'Inserir/Editar Mídia',\n    'clear_formatting' => 'Limpar formatação',\n    'source_code' => 'Código fonte',\n    'source_code_title' => 'Código fonte',\n    'fullscreen' => 'Tela cheia',\n    'image_options' => 'Opções de imagem',\n\n    // Tables\n    'table_properties' => 'Propriedades da tabela',\n    'table_properties_title' => 'Propriedades da Tabela',\n    'delete_table' => 'Excluir Tabela',\n    'table_clear_formatting' => 'Limpar formatação de tabela',\n    'resize_to_contents' => 'Redimensionar para o conteúdo',\n    'row_header' => 'Cabeçalho da linha',\n    'insert_row_before' => 'Inserir linha antes',\n    'insert_row_after' => 'Inserir linha depois',\n    'delete_row' => 'Excluir linha',\n    'insert_column_before' => 'Inserir coluna antes',\n    'insert_column_after' => 'Inserir coluna depois',\n    'delete_column' => 'Excluir coluna',\n    'table_cell' => 'Célula',\n    'table_row' => 'Linha',\n    'table_column' => 'Coluna',\n    'cell_properties' => 'Propriedades da célula',\n    'cell_properties_title' => 'Propriedades da Célula',\n    'cell_type' => 'Tipo de célula',\n    'cell_type_cell' => 'Célula',\n    'cell_scope' => 'Escopo',\n    'cell_type_header' => 'Célula do cabeçalho',\n    'merge_cells' => 'Mesclar células',\n    'split_cell' => 'Dividir célula',\n    'table_row_group' => 'Grupo de linha',\n    'table_column_group' => 'Grupo de coluna',\n    'horizontal_align' => 'Alinhamento Horizontal',\n    'vertical_align' => 'Alinhamento vertical',\n    'border_width' => 'Largura da borda',\n    'border_style' => 'Estilo da Borda',\n    'border_color' => 'Cor da borda',\n    'row_properties' => 'Propriedades da linha',\n    'row_properties_title' => 'Propriedades da Linha',\n    'cut_row' => 'Cortar linha',\n    'copy_row' => 'Copiar linha',\n    'paste_row_before' => 'Colar linha antes',\n    'paste_row_after' => 'Colar linha depois',\n    'row_type' => 'Tipo de linha',\n    'row_type_header' => 'Cabeçalho',\n    'row_type_body' => 'Corpo',\n    'row_type_footer' => 'Rodapé',\n    'alignment' => 'Alinhamento',\n    'cut_column' => 'Cortar coluna',\n    'copy_column' => 'Copiar Coluna',\n    'paste_column_before' => 'Colar coluna antes',\n    'paste_column_after' => 'Colar coluna depois',\n    'cell_padding' => 'Preenchimento da celula',\n    'cell_spacing' => 'Espaçamento entre células',\n    'caption' => 'Legenda',\n    'show_caption' => 'Mostrar legenda',\n    'constrain' => 'Restringir proporções',\n    'cell_border_solid' => 'Sólida',\n    'cell_border_dotted' => 'Pontilhado',\n    'cell_border_dashed' => 'Tracejado',\n    'cell_border_double' => 'Duplo',\n    'cell_border_groove' => 'Ranhura',\n    'cell_border_ridge' => 'Ondulado',\n    'cell_border_inset' => 'Inserir',\n    'cell_border_outset' => 'Saída',\n    'cell_border_none' => 'Nenhuma',\n    'cell_border_hidden' => 'Ocultado',\n\n    // Images, links, details/summary & embed\n    'source' => 'Fonte',\n    'alt_desc' => 'Descrição alternativa',\n    'embed' => 'Embutido',\n    'paste_embed' => 'Cole seu código abaixo de incorporação:',\n    'url' => 'URL',\n    'text_to_display' => 'Texto de exibição',\n    'title' => 'Título',\n    'browse_links' => 'Procurar links',\n    'open_link' => 'Link aberto',\n    'open_link_in' => 'Abrir link em...',\n    'open_link_current' => 'Janelas atuais',\n    'open_link_new' => 'Nova janela',\n    'remove_link' => 'Remover link',\n    'insert_collapsible' => 'Inserir bloco colapsável',\n    'collapsible_unwrap' => 'Desembrulhar',\n    'edit_label' => 'Editar etiqueta',\n    'toggle_open_closed' => 'Alternar aberto/fechado',\n    'collapsible_edit' => 'Inserir bloco colapsável',\n    'toggle_label' => 'Alternar etiqueta',\n\n    // About view\n    'about' => 'Sobre o editor',\n    'about_title' => 'Sobre o Editor WYSIWYG',\n    'editor_license' => 'Licença do Editor e Direitos Autorais',\n    'editor_lexical_license' => 'Este editor é criado como uma bifurcação de :lexicalLink distribuído sob a licença MIT.',\n    'editor_lexical_license_link' => 'Aqui podem ser encontrados detalhes da licença.',\n    'editor_tiny_license' => 'Este editor é construído usando :tinyLink que é fornecido sob a licença MIT.',\n    'editor_tiny_license_link' => 'Os dados relativos aos direitos de autor e à licença do TinyMCE podem ser encontrados aqui.',\n    'save_continue' => 'Salvar Página e Continuar',\n    'callouts_cycle' => '(Continue pressionando para alternar através de tipos)',\n    'link_selector' => 'Link para conteúdo',\n    'shortcuts' => 'Atalhos',\n    'shortcut' => 'Atalho',\n    'shortcuts_intro' => 'Os seguintes atalhos estão disponíveis no editor:',\n    'windows_linux' => 'Windows, Linux',\n    'mac' => '(Mac))',\n    'description' => 'Descrição',\n];\n"
  },
  {
    "path": "lang/pt_BR/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Criado Recentemente',\n    'recently_created_pages' => 'Páginas Recentemente Criadas',\n    'recently_updated_pages' => 'Páginas Recentemente Atualizadas',\n    'recently_created_chapters' => 'Capítulos Recentemente Criados',\n    'recently_created_books' => 'Livros Recentemente Criados',\n    'recently_created_shelves' => 'Estantes Criadas Recentemente',\n    'recently_update' => 'Recentemente Atualizado',\n    'recently_viewed' => 'Recentemente Visualizado',\n    'recent_activity' => 'Atividades recentes',\n    'create_now' => 'Criar agora',\n    'revisions' => 'Revisões',\n    'meta_revision' => 'Revisão #:revisionCount',\n    'meta_created' => 'Criado :timeLength',\n    'meta_created_name' => 'Criado :timeLength por :user',\n    'meta_updated' => 'Atualizado :timeLength',\n    'meta_updated_name' => 'Atualizado: :timeLength por :user',\n    'meta_owned_name' => 'Propriedade de :user',\n    'meta_reference_count' => 'Referenciado por :count item|Referenciado por :count itens',\n    'entity_select' => 'Seleção de entidade',\n    'entity_select_lack_permission' => 'Você não tem as permissões necessárias para selecionar este ‘item’',\n    'images' => 'Imagens',\n    'my_recent_drafts' => 'Meus Rascunhos Recentes',\n    'my_recently_viewed' => 'Visualizados Recentemente',\n    'my_most_viewed_favourites' => 'Meus Favoritos Mais Visualizados',\n    'my_favourites' => 'Favoritos',\n    'no_pages_viewed' => 'Você não visualizou nenhuma página',\n    'no_pages_recently_created' => 'Nenhuma página foi criada recentemente',\n    'no_pages_recently_updated' => 'Nenhuma página foi atualizada recentemente',\n    'export' => 'Exportar',\n    'export_html' => 'Arquivo Web Contido',\n    'export_pdf' => 'Arquivo PDF',\n    'export_text' => 'Arquivo de texto simples',\n    'export_md' => 'Arquivo de redução',\n    'export_zip' => 'ZIP portátil',\n    'default_template' => 'Modelo padrão de página',\n    'default_template_explain' => 'Atribuir o modelo de página que será usado como padrão para todas as páginas criadas neste livro. Tenha em mente que isto será usado apenas se o criador da página tiver acesso de visualização ao modelo de página escolhido.',\n    'default_template_select' => 'Selecione uma página de modelo',\n    'import' => 'Importação',\n    'import_validate' => 'Validar Importação',\n    'import_desc' => 'Importar livros, capítulos e páginas usando uma exportação zip portátil da mesma ou de uma instância diferente. Selecione um arquivo ZIP para prosseguir. Após a carga e validação do arquivo, você será capaz de configurar e confirmar a importação na próxima visualização.',\n    'import_zip_select' => 'Selecione o arquivo ZIP para carregar',\n    'import_zip_validation_errors' => 'Foram detectados erros ao validar o arquivo ZIP:',\n    'import_pending' => 'Importações pendentes',\n    'import_pending_none' => 'Nenhuma importação foi iniciada.',\n    'import_continue' => 'Continuar a importação',\n    'import_continue_desc' => 'Revise o conteúdo que deve ser importado do arquivo ZIP carregado. Quando estiver pronto, execute a importação para adicionar seu conteúdo a este sistema. O arquivo ZIP importado será automaticamente removido quando a importação for bem sucedida.',\n    'import_details' => 'Detalhes da importação',\n    'import_run' => 'Executar Importação',\n    'import_size' => ':size Tamanho do ZIP',\n    'import_uploaded_at' => 'Carregado :relativeTime',\n    'import_uploaded_by' => 'Enviado por',\n    'import_location' => 'Local da importação',\n    'import_location_desc' => 'Selecione um local para o conteúdo importado. Você precisa das permissões necessárias para criar no local escolhido.',\n    'import_delete_confirm' => 'Tem certeza que deseja excluir esta importação?',\n    'import_delete_desc' => 'Isto irá excluir o arquivo ZIP de importação carregado e não poderá ser desfeito.',\n    'import_errors' => 'Erros de importação',\n    'import_errors_desc' => 'Os seguintes erros ocorreram durante a tentativa de importação:',\n    'breadcrumb_siblings_for_page' => 'Navegue pelas páginas relacionadas',\n    'breadcrumb_siblings_for_chapter' => 'Navegue pelos capítulos relacionados',\n    'breadcrumb_siblings_for_book' => 'Navegue pelos livros relacionados',\n    'breadcrumb_siblings_for_bookshelf' => 'Navegue pelas estantes relacionadas',\n\n    // Permissions and restrictions\n    'permissions' => 'Permissões',\n    'permissions_desc' => 'Defina permissões aqui para substituir as permissões padrão fornecidas pelo perfil do usuário.',\n    'permissions_book_cascade' => 'As permissões definidas nos livros serão automaticamente transmitidas aos capítulos e páginas secundários, a menos que eles tenham suas próprias permissões definidas.',\n    'permissions_chapter_cascade' => 'As permissões definidas nos capítulos serão automaticamente transmitidas às páginas secundárias, a menos que elas tenham suas próprias permissões definidas.',\n    'permissions_save' => 'Salvar permissões',\n    'permissions_owner' => 'Dono',\n    'permissions_role_everyone_else' => 'Todos os Outros',\n    'permissions_role_everyone_else_desc' => 'Defina permissões para todos os perfis não substituídas especificamente.',\n    'permissions_role_override' => 'Substituir permissões para o perfil',\n    'permissions_inherit_defaults' => 'Herdar Padrões',\n\n    // Search\n    'search_results' => 'Resultado(s) da Pesquisa',\n    'search_total_results_found' => ':count resultado encontrado|:count resultados encontrados',\n    'search_clear' => 'Limpar Pesquisa',\n    'search_no_pages' => 'Nenhuma página corresponde a esta pesquisa',\n    'search_for_term' => 'Pesquisar por :term',\n    'search_more' => 'Mais resultados',\n    'search_advanced' => 'Pesquisa avançada',\n    'search_terms' => 'Termos da pesquisa',\n    'search_content_type' => 'Categoria de conteúdo',\n    'search_exact_matches' => 'Correspondências exatas',\n    'search_tags' => 'Pesquisar marcadores',\n    'search_options' => 'Opções',\n    'search_viewed_by_me' => 'Visto por mim',\n    'search_not_viewed_by_me' => 'Não visto por mim',\n    'search_permissions_set' => 'Permissões definidas',\n    'search_created_by_me' => 'Criados por mim',\n    'search_updated_by_me' => 'Atualizados por mim',\n    'search_owned_by_me' => 'Meus itens',\n    'search_date_options' => 'Opções de Data',\n    'search_updated_before' => 'Atualizado antes',\n    'search_updated_after' => 'Atualizado depois',\n    'search_created_before' => 'Criado antes',\n    'search_created_after' => 'Criado depois',\n    'search_set_date' => 'Definir Data',\n    'search_update' => 'Atualizar pesquisa',\n\n    // Shelves\n    'shelf' => 'Estante',\n    'shelves' => 'Estantes',\n    'x_shelves' => ':count Estante|:count Estantes',\n    'shelves_empty' => 'Nenhuma estante foi criada',\n    'shelves_create' => 'Criar Estante',\n    'shelves_popular' => 'Estantes Populares',\n    'shelves_new' => 'Novas Estantes',\n    'shelves_new_action' => 'Nova Estante',\n    'shelves_popular_empty' => 'As estantes mais populares serão exibidas aqui.',\n    'shelves_new_empty' => 'As estantes mais recentes serão exibidas aqui.',\n    'shelves_save' => 'Salvar Estante',\n    'shelves_books' => 'Livros nesta estante',\n    'shelves_add_books' => 'Adicionar livros a esta estante',\n    'shelves_drag_books' => 'Arraste os livros abaixo para adicioná-los a esta estante',\n    'shelves_empty_contents' => 'Esta estante não possui livros atribuídos a ela',\n    'shelves_edit_and_assign' => 'Editar estante para atribuir livros',\n    'shelves_edit_named' => 'Editar estante :name',\n    'shelves_edit' => 'Editar estante',\n    'shelves_delete' => 'Excluir estante',\n    'shelves_delete_named' => 'Excluir estante :name',\n    'shelves_delete_explain' => \"Isso excluirá a estante com o nome ':name'. Os livros contidos não serão excluídos.\",\n    'shelves_delete_confirmation' => 'Tem certeza de que deseja excluir esta estante de livros?',\n    'shelves_permissions' => 'Permissões da Estante',\n    'shelves_permissions_updated' => 'Permissões da estante atualizadas',\n    'shelves_permissions_active' => 'Permissões da estante ativada',\n    'shelves_permissions_cascade_warning' => 'As permissões nas estantes não são automaticamente em cascata para os livros contidos. Isso ocorre porque um livro pode existir em várias estantes. No entanto, as permissões podem ser copiadas para livros filhos usando a opção encontrada abaixo.',\n    'shelves_permissions_create' => 'As permissões de criação de estante são usadas apenas para copiar livros filhos usando a ação abaixo. Eles não controlam a capacidade de criar livros.',\n    'shelves_copy_permissions_to_books' => 'Copiar Permissões para Livros',\n    'shelves_copy_permissions' => 'Copiar permissões',\n    'shelves_copy_permissions_explain' => 'Isso aplicará as configurações de permissão atuais desta estante a todos os livros contidos nela. Antes de ativar, verifique se todas as alterações nas permissões desta estante foram salvas.',\n    'shelves_copy_permission_success' => 'Permissões da estante copiadas para :count books',\n\n    // Books\n    'book' => 'Livro',\n    'books' => 'Livros',\n    'x_books' => ':count Livro|:count Livros',\n    'books_empty' => 'Nenhum livro foi criado',\n    'books_popular' => 'Livros Populares',\n    'books_recent' => 'Livros Recentes',\n    'books_new' => 'Livros Novos',\n    'books_new_action' => 'Novo Livro',\n    'books_popular_empty' => 'Os livros mais populares aparecerão aqui.',\n    'books_new_empty' => 'Os livros criados mais recentemente aparecerão aqui.',\n    'books_create' => 'Criar Novo Livro',\n    'books_delete' => 'Excluir Livro',\n    'books_delete_named' => 'Excluir Livro :bookName',\n    'books_delete_explain' => 'A ação vai excluir o livro com o nome \\':bookName\\'. Todas as páginas e capítulos serão removidos.',\n    'books_delete_confirmation' => 'Você tem certeza que quer excluir o Livro?',\n    'books_edit' => 'Editar Livro',\n    'books_edit_named' => 'Editar Livro :bookName',\n    'books_form_book_name' => 'Nome do Livro',\n    'books_save' => 'Salvar Livro',\n    'books_permissions' => 'Permissões do Livro',\n    'books_permissions_updated' => 'Permissões do Livro Atualizadas',\n    'books_empty_contents' => 'Nenhuma página ou capítulo foram criados para este livro.',\n    'books_empty_create_page' => 'Criar uma nova página',\n    'books_empty_sort_current_book' => 'Ordenar o livro atual',\n    'books_empty_add_chapter' => 'Adicionar um capítulo',\n    'books_permissions_active' => 'Permissões do Livro Ativas',\n    'books_search_this' => 'Pesquisar neste livro',\n    'books_navigation' => 'Navegação do Livro',\n    'books_sort' => 'Ordenar Conteúdos do Livro',\n    'books_sort_desc' => 'Mova capítulos e páginas de um livro para reorganizar seu conteúdo. É possível acrescentar outros livros, o que permite uma movimentação fácil de capítulos e páginas entre livros. Opcionalmente, uma regra de ordenação automática pode ser definida para ordenar automaticamente o conteúdo deste livro após alterações.',\n    'books_sort_auto_sort' => 'Opção de ordenação automática',\n    'books_sort_auto_sort_active' => 'Ordenação automática ativa: :sortName',\n    'books_sort_named' => 'Ordenar Livro :bookName',\n    'books_sort_name' => 'Ordernar por Nome',\n    'books_sort_created' => 'Ordenar por Data de Criação',\n    'books_sort_updated' => 'Ordenar por Data de Atualização',\n    'books_sort_chapters_first' => 'Capítulos Primeiro',\n    'books_sort_chapters_last' => 'Capítulos por Último',\n    'books_sort_show_other' => 'Mostrar Outros Livros',\n    'books_sort_save' => 'Salvar Nova Ordenação',\n    'books_sort_show_other_desc' => 'Adicione outros livros aqui para incluí-los na operação de ordenação e permitir a reorganização fácil de todos os livros.',\n    'books_sort_move_up' => 'Mover para cima',\n    'books_sort_move_down' => 'Mover para baixo',\n    'books_sort_move_prev_book' => 'Mover para Livro Anterior',\n    'books_sort_move_next_book' => 'Mover para o Próximo Livro',\n    'books_sort_move_prev_chapter' => 'Mover para o Capítulo Anterior',\n    'books_sort_move_next_chapter' => 'Mover para o Próximo Capítulo',\n    'books_sort_move_book_start' => 'Mover para o Início do Livro',\n    'books_sort_move_book_end' => 'Mover para o Final do Livro',\n    'books_sort_move_before_chapter' => 'Mover para Antes do Capítulo',\n    'books_sort_move_after_chapter' => 'Mover para Depois do Capítulo',\n    'books_copy' => 'Copiar Livro',\n    'books_copy_success' => 'Livro criado com sucesso',\n\n    // Chapters\n    'chapter' => 'Capítulo',\n    'chapters' => 'Capítulos',\n    'x_chapters' => ':count Capítulo|:count Capítulos',\n    'chapters_popular' => 'Capítulos Populares',\n    'chapters_new' => 'Novo Capítulo',\n    'chapters_create' => 'Criar Novo Capítulo',\n    'chapters_delete' => 'Excluir Capítulo',\n    'chapters_delete_named' => 'Excluir Capítulo :chapterName',\n    'chapters_delete_explain' => 'Isto irá excluir o capítulo com o nome \\':chapterName\\'. Todas as páginas que existem neste capítulo também serão excluídas.',\n    'chapters_delete_confirm' => 'Tem certeza que deseja excluir o capítulo?',\n    'chapters_edit' => 'Editar Capítulo',\n    'chapters_edit_named' => 'Editar Capítulo :chapterName',\n    'chapters_save' => 'Salvar Capítulo',\n    'chapters_move' => 'Mover Capítulo',\n    'chapters_move_named' => 'Mover Capítulo :chapterName',\n    'chapters_copy' => 'Copiar Capítulo',\n    'chapters_copy_success' => 'Página copiada com sucesso',\n    'chapters_permissions' => 'Permissões do Capítulo',\n    'chapters_empty' => 'Nenhuma página existente nesse capítulo.',\n    'chapters_permissions_active' => 'Permissões de Capítulo Ativas',\n    'chapters_permissions_success' => 'Permissões de Capítulo Atualizadas',\n    'chapters_search_this' => 'Pesquisar neste Capítulo',\n    'chapter_sort_book' => 'Classificar livro',\n\n    // Pages\n    'page' => 'Página',\n    'pages' => 'Páginas',\n    'x_pages' => ':count Página|:count Páginas',\n    'pages_popular' => 'Páginas Populares',\n    'pages_new' => 'Nova Página',\n    'pages_attachments' => 'Anexos',\n    'pages_navigation' => 'Navegação da Página',\n    'pages_delete' => 'Excluir Página',\n    'pages_delete_named' => 'Excluir Página :pageName',\n    'pages_delete_draft_named' => 'Excluir Rascunho de Página de nome :pageName',\n    'pages_delete_draft' => 'Excluir Rascunho de Página',\n    'pages_delete_success' => 'Página excluída',\n    'pages_delete_draft_success' => 'Rascunho de página excluído',\n    'pages_delete_warning_template' => 'Está página atualmente esta atribuída como modelo de página padrão para algum livro ou capítulo. Estes livros ou capítulos não terão mais um modelo de página padrão atribuídos após essa página ser deletada.',\n    'pages_delete_confirm' => 'Tem certeza que deseja excluir a página?',\n    'pages_delete_draft_confirm' => 'Tem certeza que deseja excluir o rascunho de página?',\n    'pages_editing_named' => 'Editando a Página :pageName',\n    'pages_edit_draft_options' => 'Opções de Rascunho',\n    'pages_edit_save_draft' => 'Salvar Rascunho',\n    'pages_edit_draft' => 'Editar Rascunho de Página',\n    'pages_editing_draft' => 'Editando Rascunho',\n    'pages_editing_page' => 'Editando Página',\n    'pages_edit_draft_save_at' => 'Rascunho salvo em ',\n    'pages_edit_delete_draft' => 'Excluir Rascunho',\n    'pages_edit_delete_draft_confirm' => 'Tem certeza que deseja excluir as alterações nas páginas de rascunho? Todas as suas alterações, desde o último salvamento completo, serão perdidas e o editor será atualizado com o último estado de salvamento da página.',\n    'pages_edit_discard_draft' => 'Descartar Rascunho',\n    'pages_edit_switch_to_markdown' => 'Alternar para o Editor de Markdown',\n    'pages_edit_switch_to_markdown_clean' => '(Conteúdo Limpo)',\n    'pages_edit_switch_to_markdown_stable' => '(Conteúdo Estável)',\n    'pages_edit_switch_to_wysiwyg' => 'Alternar para o Editor WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg' => 'Mudar para o novo WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(Em teste beta)',\n    'pages_edit_set_changelog' => 'Relatar Alterações',\n    'pages_edit_enter_changelog_desc' => 'Digite uma breve descrição das alterações efetuadas por você',\n    'pages_edit_enter_changelog' => 'Insira Alterações',\n    'pages_editor_switch_title' => 'Trocar editor',\n    'pages_editor_switch_are_you_sure' => 'Você tem certeza que deseja alterar o editor para esta página?',\n    'pages_editor_switch_consider_following' => 'Considere o seguinte ao alterar editores:',\n    'pages_editor_switch_consideration_a' => 'Uma vez salva, a nova opção do editor será usada por quaisquer editores futuros, incluindo aqueles que podem não ser capazes de mudar o tipo do editor.',\n    'pages_editor_switch_consideration_b' => 'Isso pode levar a uma perda de detalhes e sintaxe em certas circunstâncias.',\n    'pages_editor_switch_consideration_c' => 'Marcadores ou alterações no log de mudanças, feitas desde o último salvamento, não persistem nesta alteração.',\n    'pages_save' => 'Salvar Página',\n    'pages_title' => 'Título da Página',\n    'pages_name' => 'Nome da Página',\n    'pages_md_editor' => 'Editor',\n    'pages_md_preview' => 'Pré-Visualização',\n    'pages_md_insert_image' => 'Inserir Imagem',\n    'pages_md_insert_link' => 'Inserir Link para Entidade',\n    'pages_md_insert_drawing' => 'Inserir Diagrama',\n    'pages_md_show_preview' => 'Mostrar pré-visualização',\n    'pages_md_sync_scroll' => 'Sincronizar pré-visualização',\n    'pages_md_plain_editor' => 'Editor de texto simples',\n    'pages_drawing_unsaved' => 'Diagrama não-salvo encontrado',\n    'pages_drawing_unsaved_confirm' => 'Foram encontrados dados não-salvos de uma tentativa anterior de salvar o diagrama. Você gostaria de restaurá-los e continuar editando este diagrama?',\n    'pages_not_in_chapter' => 'Página não está dentro de um capítulo',\n    'pages_move' => 'Mover Página',\n    'pages_copy' => 'Copiar Página',\n    'pages_copy_desination' => 'Destino da Cópia',\n    'pages_copy_success' => 'Página copiada com sucesso',\n    'pages_permissions' => 'Permissões da Página',\n    'pages_permissions_success' => 'Permissões da Página atualizadas',\n    'pages_revision' => 'Revisão',\n    'pages_revisions' => 'Revisões da Página',\n    'pages_revisions_desc' => 'Listadas abaixo estão todas as revisões anteriores desta página. Você pode rever, comparar e restaurar versões antigas de páginas se as permissões permitirem. O histórico completo da página pode não ser totalmente refletido aqui, pois, dependendo da configuração do sistema, as revisões antigas podem ser excluídas automaticamente.',\n    'pages_revisions_named' => 'Revisões de Página para :pageName',\n    'pages_revision_named' => 'Revisão de Página para :pageName',\n    'pages_revision_restored_from' => 'Restaurado de #:id; :summary',\n    'pages_revisions_created_by' => 'Criada por',\n    'pages_revisions_date' => 'Data da Revisão',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Número de revisão',\n    'pages_revisions_numbered' => 'Revisão #:id',\n    'pages_revisions_numbered_changes' => 'Alterações da Revisão #:id',\n    'pages_revisions_editor' => 'Tipo de editor',\n    'pages_revisions_changelog' => 'Relatório de Alterações',\n    'pages_revisions_changes' => 'Alterações',\n    'pages_revisions_current' => 'Versão Atual',\n    'pages_revisions_preview' => 'Pré-Visualização',\n    'pages_revisions_restore' => 'Restaurar',\n    'pages_revisions_none' => 'Essa página não tem revisões',\n    'pages_copy_link' => 'Copiar Link',\n    'pages_edit_content_link' => 'Ir para a seção do editor',\n    'pages_pointer_enter_mode' => 'Entrar em modo de seleção de seção',\n    'pages_pointer_label' => 'Opções de Seção de Página',\n    'pages_pointer_permalink' => 'Seção de Página Permalink',\n    'pages_pointer_include_tag' => 'Marcador de inclusão de seção de página',\n    'pages_pointer_toggle_link' => 'Modo permalink, pressione para mostrar a marcação incluída',\n    'pages_pointer_toggle_include' => 'Incluir o modo de marcação, pressione para mostrar o permalink',\n    'pages_permissions_active' => 'Permissões de Página Ativas',\n    'pages_initial_revision' => 'Publicação Inicial',\n    'pages_references_update_revision' => 'Atualização automática do sistema de links internos',\n    'pages_initial_name' => 'Nova Página',\n    'pages_editing_draft_notification' => 'Você está atualmente editando um rascunho que foi salvo da última vez em :timeDiff.',\n    'pages_draft_edited_notification' => 'Essa página foi atualizada desde então. É recomendado que você descarte esse rascunho.',\n    'pages_draft_page_changed_since_creation' => 'Esta página foi atualizada desde que este rascunho foi criado. É recomendável que você descarte este rascunho ou tenha cuidado para não sobrescrever nenhuma alteração de página.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count usuários iniciaram a edição dessa página',\n        'start_b' => ':userName iniciou a edição dessa página',\n        'time_a' => 'desde que a página foi atualizada pela última vez',\n        'time_b' => 'nos últimos :minCount minutos',\n        'message' => ':start :time. Tome cuidado para não sobrescrever atualizações de outras pessoas!',\n    ],\n    'pages_draft_discarded' => 'Rascunho descartado! O editor foi atualizado com o conteúdo da página atual',\n    'pages_draft_deleted' => 'Rascunho excluído! O editor foi atualizado com o conteúdo da página atual',\n    'pages_specific' => 'Página Específica',\n    'pages_is_template' => 'Modelo de Página',\n\n    // Editor Sidebar\n    'toggle_sidebar' => '',\n    'page_tags' => 'Marcadores de Página',\n    'chapter_tags' => 'Marcadores de Capítulo',\n    'book_tags' => 'Marcadores de Livro',\n    'shelf_tags' => 'Marcadores de Estante',\n    'tag' => 'Marcador',\n    'tags' =>  'Marcadores',\n    'tags_index_desc' => 'Os marcadores podem ser aplicadas ao conteúdo dentro do sistema para aplicar uma forma flexível de categorização. Os marcadores podem ter uma chave e um valor, sendo o valor opcional. Depois de aplicado, o conteúdo pode ser consultado usando o nome e o valor do marcador.',\n    'tag_name' =>  'Nome do marcador',\n    'tag_value' => 'Valor do marcador (Opcional)',\n    'tags_explain' => \"Adicione alguns marcadores para melhor categorizar seu conteúdo. \\n Você pode atribuir valores aos marcadores para uma organização mais complexa.\",\n    'tags_add' => 'Adicionar outro marcador',\n    'tags_remove' => 'Remover esse marcador',\n    'tags_usages' => 'Total de marcadores usados',\n    'tags_assigned_pages' => 'Atribuído às páginas',\n    'tags_assigned_chapters' => 'Atribuído aos Capítulos',\n    'tags_assigned_books' => 'Atribuído a Livros',\n    'tags_assigned_shelves' => 'Atribuído a Estantes',\n    'tags_x_unique_values' => ':count valores únicos',\n    'tags_all_values' => 'Todos os valores',\n    'tags_view_tags' => 'Ver Marcadores',\n    'tags_view_existing_tags' => 'Ver marcadores existentes',\n    'tags_list_empty_hint' => 'Os marcadores podem ser atribuídos através da barra lateral do editor de página ou ao editar os detalhes de um livro, capítulo ou estante.',\n    'attachments' => 'Anexos',\n    'attachments_explain' => 'Faça o upload de alguns arquivos ou anexe links para serem exibidos na sua página. Eles estarão visíveis na barra lateral à direita.',\n    'attachments_explain_instant_save' => 'Mudanças são salvas instantaneamente.',\n    'attachments_upload' => 'Upload de Arquivos',\n    'attachments_link' => 'Links Anexados',\n    'attachments_upload_drop' => 'Como alternativa, você pode arrastar e soltar um arquivo aqui para enviá-lo como um anexo.',\n    'attachments_set_link' => 'Definir Link',\n    'attachments_delete' => 'Tem certeza de que deseja excluir esse anexo?',\n    'attachments_dropzone' => 'Arraste os arquivos até aqui para fazer o upload',\n    'attachments_no_files' => 'Nenhum arquivo foi enviado',\n    'attachments_explain_link' => 'Você pode anexar um link se preferir não fazer o upload do arquivo. O link poderá ser para uma outra página ou para um arquivo na nuvem.',\n    'attachments_link_name' => 'Nome do Link',\n    'attachment_link' => 'Link para o Anexo',\n    'attachments_link_url' => 'Link para o Arquivo',\n    'attachments_link_url_hint' => 'URL do site ou arquivo',\n    'attach' => 'Anexar',\n    'attachments_insert_link' => 'Adicionar Link de Anexo à Página',\n    'attachments_edit_file' => 'Editar Arquivo',\n    'attachments_edit_file_name' => 'Nome do Arquivo',\n    'attachments_edit_drop_upload' => 'Arraste arquivos para cá ou clique para anexar arquivos e sobrescreve-los',\n    'attachments_order_updated' => 'Ordem dos anexos atualizada',\n    'attachments_updated_success' => 'Detalhes dos anexos atualizados',\n    'attachments_deleted' => 'Anexo excluído',\n    'attachments_file_uploaded' => 'Upload de arquivo efetuado com sucesso',\n    'attachments_file_updated' => 'Arquivo atualizado com sucesso',\n    'attachments_link_attached' => 'Link anexado com sucesso à página',\n    'templates' => 'Modelos',\n    'templates_set_as_template' => 'A Página é um Modelo',\n    'templates_explain_set_as_template' => 'Você pode definir esta página como um modelo para que seu conteúdo possa ser utilizado para criar outras páginas. Outros usuários poderão utilizar esta página como modelo se tiverem permissão para visualiza-la.',\n    'templates_replace_content' => 'Substituir conteúdo da página',\n    'templates_append_content' => 'Adicionar ao fim do conteúdo da página',\n    'templates_prepend_content' => 'Adicionar ao início do conteúdo da página',\n\n    // Profile View\n    'profile_user_for_x' => 'Usuário por :time',\n    'profile_created_content' => 'Conteúdo Criado',\n    'profile_not_created_pages' => ':userName não criou páginas',\n    'profile_not_created_chapters' => ':userName não criou capítulos',\n    'profile_not_created_books' => ':userName não criou livros',\n    'profile_not_created_shelves' => ':userName não criou estantes',\n\n    // Comments\n    'comment' => 'Comentário',\n    'comments' => 'Comentários',\n    'comment_add' => 'Adicionar Comentário',\n    'comment_none' => 'Nenhum comentário para exibir',\n    'comment_placeholder' => 'Digite seus comentários aqui',\n    'comment_thread_count' => ':count Tópico de Comentário|:count Tópicos de Comentários',\n    'comment_archived_count' => ':count Arquivado',\n    'comment_archived_threads' => 'Tópicos Arquivados',\n    'comment_save' => 'Salvar comentário',\n    'comment_new' => 'Novo Comentário',\n    'comment_created' => 'comentado :createDiff',\n    'comment_updated' => 'Editado :updateDiff por :username',\n    'comment_updated_indicator' => 'Atualizado',\n    'comment_deleted_success' => 'Comentário removido',\n    'comment_created_success' => 'Comentário adicionado',\n    'comment_updated_success' => 'Comentário editado',\n    'comment_archive_success' => 'Comentário arquivado',\n    'comment_unarchive_success' => 'Comentário desarquivado',\n    'comment_view' => 'Ver comentário',\n    'comment_jump_to_thread' => 'Ir para o tópico',\n    'comment_delete_confirm' => 'Você tem certeza de que deseja excluir este comentário?',\n    'comment_in_reply_to' => 'Em resposta à :commentId',\n    'comment_reference' => 'Referência',\n    'comment_reference_outdated' => '(Desatualizado)',\n    'comment_editor_explain' => 'Aqui estão os comentários que foram deixados nesta página. Comentários podem ser adicionados e gerenciados ao visualizar a página salva.',\n\n    // Revision\n    'revision_delete_confirm' => 'Tem certeza de que deseja excluir esta revisão?',\n    'revision_restore_confirm' => 'Tem certeza que deseja restaurar esta revisão? O conteúdo atual da página será substituído.',\n    'revision_cannot_delete_latest' => 'Não é possível excluir a revisão mais recente.',\n\n    // Copy view\n    'copy_consider' => 'Por favor, considere o abaixo ao copiar conteúdo.',\n    'copy_consider_permissions' => 'Configurações de permissão personalizada não serão copiadas.',\n    'copy_consider_owner' => 'Você se tornará o proprietário de todos os conteúdos copiados.',\n    'copy_consider_images' => 'A imagem da página não será duplicada e as imagens originais manterão sua relação com a página para a qual foram enviadas originalmente.',\n    'copy_consider_attachments' => 'Anexos de página não serão copiados.',\n    'copy_consider_access' => 'Uma alteração de localização, proprietário ou permissões pode resultar em que este conteúdo seja acessível para aqueles previamente sem acesso.',\n\n    // Conversions\n    'convert_to_shelf' => 'Converter para estante',\n    'convert_to_shelf_contents_desc' => 'Você pode converter este livro em uma nova estante com o mesmo conteúdo. Os capítulos contidos neste livro serão convertidos em novos livros. Se este livro contiver quaisquer páginas que não estejam em um capítulo, este livro será renomeado e conterá tais páginas, e este livro se tornará parte da nova estante.',\n    'convert_to_shelf_permissions_desc' => 'Todas as permissões definidas neste livro serão copiadas para a nova estante e para todos os novos livros filhos que não tiverem suas próprias permissões aplicadas. Observe que as permissões nas estantes não se propagam automaticamente para o conteúdo, como acontece com os livros.',\n    'convert_book' => 'Converter Livro',\n    'convert_book_confirm' => 'Tem certeza de que deseja converter este livro?',\n    'convert_undo_warning' => 'Isso não pode ser desfeito tão facilmente.',\n    'convert_to_book' => 'Converter em livro',\n    'convert_to_book_desc' => 'Você pode converter este capítulo em um novo livro com o mesmo conteúdo. Quaisquer permissões definidas neste capítulo serão copiadas para o novo livro, mas quaisquer permissões herdadas, do livro pai, não serão copiadas, o que pode levar a uma alteração do controle de acesso.',\n    'convert_chapter' => 'Converter capítulo',\n    'convert_chapter_confirm' => 'Tem certeza de que deseja converter este capítulo?',\n\n    // References\n    'references' => 'Referências',\n    'references_none' => 'Não há referências rastreadas para este item.',\n    'references_to_desc' => 'Abaixo estão todas as páginas conhecidas no sistema que estão vinculadas a este item.',\n\n    // Watch Options\n    'watch' => 'Acompanhar',\n    'watch_title_default' => 'Preferências padrão',\n    'watch_desc_default' => 'Reverter o acompanhamento apenas para suas preferências de notificação padrão.',\n    'watch_title_ignore' => 'Ignorar',\n    'watch_desc_ignore' => 'Ignorar todas as notificações, incluindo as de preferências de nível de usuário.',\n    'watch_title_new' => 'Novas Páginas',\n    'watch_desc_new' => 'Notificar quando qualquer nova página for criada dentro deste item.',\n    'watch_title_updates' => 'Todas as atualizações da página',\n    'watch_desc_updates' => 'Notificar sobre todas as novas páginas e alterações na página.',\n    'watch_desc_updates_page' => 'Notificar sobre todas as alterações da página.',\n    'watch_title_comments' => 'Todas as atualizações e comentários da página',\n    'watch_desc_comments' => 'Notificar sobre todas as novas páginas, alterações de página e novos comentários.',\n    'watch_desc_comments_page' => 'Notificar sobre alterações na página e novos comentários.',\n    'watch_change_default' => 'Alterar preferências padrão de notificação',\n    'watch_detail_ignore' => 'Ignorando notificações',\n    'watch_detail_new' => 'Acompanhando para novas páginas',\n    'watch_detail_updates' => 'Acompanhando novas páginas e atualizações',\n    'watch_detail_comments' => 'Acompanhando novas páginas, atualizações e comentários',\n    'watch_detail_parent_book' => 'Acompanhando através do livro pai',\n    'watch_detail_parent_book_ignore' => 'Ignorando através do livro pai',\n    'watch_detail_parent_chapter' => 'Acompanhando através do capítulo pai',\n    'watch_detail_parent_chapter_ignore' => 'Ignorando através do capítulo pai',\n];\n"
  },
  {
    "path": "lang/pt_BR/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Você não tem permissão para acessar a página solicitada.',\n    'permissionJson' => 'Você não tem permissão para realizar a ação solicitada.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Um usuário com o endereço eletrônico: endereço eletrônico já existe, mas com credenciais diferentes.',\n    'auth_pre_register_theme_prevention' => 'A conta do usuário não pôde ser registrada com os detalhes oferecidos',\n    'email_already_confirmed' => 'Endereço eletrônico já foi confirmado. Tente fazer o ‘login’.',\n    'email_confirmation_invalid' => 'Esse código de confirmação não é válido ou já foi utilizado. Por favor, tente cadastrar-se novamente.',\n    'email_confirmation_expired' => 'O código de confirmação já expirou. Uma nova mensagem eletrônica foi enviada.',\n    'email_confirmation_awaiting' => 'O endereço do correio eletrônico da conta em uso precisa ser confirmado',\n    'ldap_fail_anonymous' => 'O acesso LDAP falhou ao tentar usar o anonymous bind',\n    'ldap_fail_authed' => 'O acesso LDAP falhou ao tentar os detalhes do dn e senha fornecidos',\n    'ldap_extension_not_installed' => 'A extensão LDAP PHP não foi instalada',\n    'ldap_cannot_connect' => 'Não foi possível conectar ao servidor LDAP. Conexão inicial falhou',\n    'saml_already_logged_in' => '\\'Login\\' já efetuado',\n    'saml_no_email_address' => 'Não foi possível encontrar um endereço de mensagem eletrônica para este usuário nos dados providos pelo sistema de autenticação externa',\n    'saml_invalid_response_id' => 'A requisição do sistema de autenticação externa não foi reconhecida por um processo iniciado por esta aplicação. Após o \\'login\\', navegar para o caminho anterior pode causar um problema.',\n    'saml_fail_authed' => 'Login utilizando :system falhou. Sistema não forneceu autorização bem sucedida',\n    'oidc_already_logged_in' => '\\'Login\\' já efetuado',\n    'oidc_no_email_address' => 'Não foi possível encontrar um endereço de mensagem eletrônica para este usuário, nos dados fornecidos pelo sistema de autenticação externa',\n    'oidc_fail_authed' => 'Login usando :system falhou, o sistema não forneceu autorização com sucesso',\n    'social_no_action_defined' => 'Nenhuma ação definida',\n    'social_login_bad_response' => \"Erro recebido durante o 'login' :socialAccount: \\n: error\",\n    'social_account_in_use' => 'Essa conta :socialAccount já está em uso. Por favor, tente entrar utilizando a opção :socialAccount.',\n    'social_account_email_in_use' => 'O e-mail :email já está em uso. Se você já tem uma conta você poderá se conectar a conta :socialAccount a partir das configurações de seu perfil.',\n    'social_account_existing' => 'Essa conta :socialAccount já está vinculada a esse perfil.',\n    'social_account_already_used_existing' => 'Essa conta :socialAccount já está sendo utilizada por outro usuário.',\n    'social_account_not_used' => 'Essa conta :socialAccount não está vinculada a nenhum usuário. Por favor vincule a conta nas suas configurações de perfil. ',\n    'social_account_register_instructions' => 'Se você não tem uma conta, você poderá se cadastrar usando a opção: conta social.',\n    'social_driver_not_found' => 'Social driver não encontrado',\n    'social_driver_not_configured' => 'Seus parâmetros sociais de: conta social não estão configurados corretamente.',\n    'invite_token_expired' => 'Este link de convite expirou. Alternativamente, você pode tentar redefinir a senha da sua conta.',\n    'login_user_not_found' => 'Não foi possível encontrar um usuário para esta ação.',\n\n    // System\n    'path_not_writable' => 'O caminho de destino (:filePath) de upload de arquivo não possui permissão de escrita. Certifique-se que ele possui direitos de escrita no servidor.',\n    'cannot_get_image_from_url' => 'Não foi possível obter a imagem a partir de :url',\n    'cannot_create_thumbs' => 'O servidor não pôde criar as miniaturas de imagem. Por favor, verifique se a extensão GD PHP está instalada.',\n    'server_upload_limit' => 'O servidor não permite o ‘upload’ de arquivos com esse tamanho. Por favor, tente um tamanho de arquivo menor.',\n    'server_post_limit' => 'O servidor não pode receber a quantidade de dados fornecida. Tente novamente com menos dados ou um arquivo menor.',\n    'uploaded'  => 'O servidor não permite o ‘upload’ de arquivos com esse tamanho. Por favor, tente fazer o ‘upload’ de arquivos de menor tamanho.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Um erro ocorreu enquanto o servidor tentava efetuar o ‘upload’ da imagem',\n    'image_upload_type_error' => 'A categoria de imagem que está sendo enviada é inválida',\n    'image_upload_replace_type' => 'As substituições de arquivos de imagem devem ser do mesmo tipo',\n    'image_upload_memory_limit' => 'Falha ao processar o ‘upload’ de imagem e/ou criar miniaturas devido a limites de recursos do sistema.',\n    'image_thumbnail_memory_limit' => 'Falha ao criar variações de tamanho de imagem devido a limites de recursos do sistema.',\n    'image_gallery_thumbnail_memory_limit' => 'Falha ao criar miniaturas da galeria devido aos limites de recursos do sistema.',\n    'drawing_data_not_found' => 'Dados de diagrama não puderam ser carregados. O arquivo do diagrama pode não existir mais ou você não tenha permissão para acessá-lo.',\n\n    // Attachments\n    'attachment_not_found' => 'Documento não encontrado',\n    'attachment_upload_error' => 'Um erro ocorreu ao efetuar o ‘upload’ do arquivo anexado',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Falha ao tentar salvar o rascunho. Certifique-se de ter conexão de ‘internet’ antes de tentar salvar essa página',\n    'page_draft_delete_fail' => 'Falha ao excluir o rascunho da página e buscar conteúdo salvo na página atual',\n    'page_custom_home_deletion' => 'Não é possível deletar uma página definida como página inicial',\n\n    // Entities\n    'entity_not_found' => 'Entidade não encontrada\n',\n    'bookshelf_not_found' => 'Estante não encontrada',\n    'book_not_found' => 'Livro não encontrado',\n    'page_not_found' => 'Página não encontrada',\n    'chapter_not_found' => 'Capítulo não encontrado',\n    'selected_book_not_found' => 'O livro selecionado não foi encontrado',\n    'selected_book_chapter_not_found' => 'O Livro ou Capítulo selecionado não foi encontrado',\n    'guests_cannot_save_drafts' => 'Convidados não podem salvar rascunhos',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Você não pode excluir o único administrador',\n    'users_cannot_delete_guest' => 'Você não pode excluir o usuário convidado',\n    'users_could_not_send_invite' => 'Não foi possível criar o usuário porque o endereço eletrônico de convite não foi enviado',\n\n    // Roles\n    'role_cannot_be_edited' => 'Esse perfil não pode ser editado',\n    'role_system_cannot_be_deleted' => 'Este é um perfil do sistema e não pode ser excluído',\n    'role_registration_default_cannot_delete' => 'Esse perfil não poderá se excluído enquanto estiver registrado como perfil padrão de registro',\n    'role_cannot_remove_only_admin' => 'Este usuário é o único vinculado ao perfil de administrador. Atribua o perfil de administrador a outro usuário antes de tentar removê-lo daqui.',\n\n    // Comments\n    'comment_list' => 'Ocorreu um erro ao buscar os comentários.',\n    'cannot_add_comment_to_draft' => 'Você não pode adicionar comentários a um rascunho.',\n    'comment_add' => 'Ocorreu um erro ao adicionar / atualizar o comentário.',\n    'comment_delete' => 'Ocorreu um erro ao excluir o comentário.',\n    'empty_comment' => 'Não é possível adicionar um comentário vazio.',\n\n    // Error pages\n    '404_page_not_found' => 'Página Não Encontrada',\n    'sorry_page_not_found' => 'Desculpe, a página que você está procurando não pôde ser encontrada.',\n    'sorry_page_not_found_permission_warning' => 'Se você esperava que esta página existisse, talvez você não tenha permissão para visualizá-la.',\n    'image_not_found' => 'Imagem não encontrada',\n    'image_not_found_subtitle' => 'Desculpe, o arquivo de imagem que você estava procurando não pôde ser encontrado.',\n    'image_not_found_details' => 'Se você esperava que esta imagem existisse, ela pode ter sido excluída.',\n    'return_home' => 'Retornar à página inicial',\n    'error_occurred' => 'Ocorreu um Erro',\n    'app_down' => ':appName está fora do ar no momento',\n    'back_soon' => 'Vai estar de volta em breve.',\n\n    // Import\n    'import_zip_cant_read' => 'Não foi possível ler o arquivo ZIP.',\n    'import_zip_cant_decode_data' => 'Não foi possível encontrar e decodificar o conteúdo ZIP data.json.',\n    'import_zip_no_data' => 'Os dados do arquivo ZIP não têm o conteúdo esperado livro, capítulo ou página.',\n    'import_zip_data_too_large' => 'O conteúdo ZIP data.json excede o tamanho máximo de upload configurado para a aplicação.',\n    'import_validation_failed' => 'Falhou na validação da importação do ZIP com erros:',\n    'import_zip_failed_notification' => 'Falhou ao importar arquivo ZIP.',\n    'import_perms_books' => 'Você não tem as permissões necessárias para criar livros.',\n    'import_perms_chapters' => 'Você não tem as permissões necessárias para criar capítulos.',\n    'import_perms_pages' => 'Você não tem as permissões necessárias para criar páginas.',\n    'import_perms_images' => 'Está não tem permissões necessárias para criar imagens.',\n    'import_perms_attachments' => 'Você não tem a permissão necessária para criar anexos.',\n\n    // API errors\n    'api_no_authorization_found' => 'Nenhum código de autorização encontrado na requisição',\n    'api_bad_authorization_format' => 'Um código de autorização foi encontrado na requisição, mas o formato parece incorreto',\n    'api_user_token_not_found' => 'Nenhum código de API correspondente foi encontrado para o código de autorização fornecido',\n    'api_incorrect_token_secret' => 'O segredo fornecido para o código de API usado está incorreto',\n    'api_user_no_api_permission' => 'O proprietário do código de API utilizado não tem permissão para fazer requisições de API',\n    'api_user_token_expired' => 'O código de autenticação expirou',\n    'api_cookie_auth_only_get' => 'Somente solicitações GET são permitidas ao usar a API com autenticação baseada em cookies',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Erro encontrado ao enviar uma mensagem eletrônica de teste:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'A \\'URL\\' não corresponde aos anfitriões SSR configurados como permitidos ',\n];\n"
  },
  {
    "path": "lang/pt_BR/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Novo comentário na página: :pageName',\n    'new_comment_intro' => 'Um usuário comentou em uma página de :appName:',\n    'new_page_subject' => 'Nova página: :pageName',\n    'new_page_intro' => 'Uma nova página foi criada em :appName:',\n    'updated_page_subject' => 'Página atualizada: :pageName',\n    'updated_page_intro' => 'Uma página foi atualizada em :appName:',\n    'updated_page_debounce' => 'Para prevenir notificações em massa, por enquanto notificações não serão enviadas para você para próximas edições nessa página pelo mesmo editor.',\n    'comment_mention_subject' => 'Você foi mencionado em um comentário na página: :pageName',\n    'comment_mention_intro' => 'Você foi mencionado em um comentário sobre :appName:',\n\n    'detail_page_name' => 'Nome da Página:',\n    'detail_page_path' => 'Caminho da Página:',\n    'detail_commenter' => 'Comentador:',\n    'detail_comment' => 'Comentário:',\n    'detail_created_by' => 'Criado por: ',\n    'detail_updated_by' => 'Atualizado por:',\n\n    'action_view_comment' => 'Ver Comentário',\n    'action_view_page' => 'Ver Página',\n\n    'footer_reason' => 'Essa notificação foi enviada para você porque :link engloba esse tipo de atividade para este item.',\n    'footer_reason_link' => 'suas preferências de notificação',\n];\n"
  },
  {
    "path": "lang/pt_BR/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Anterior',\n    'next'     => 'Próximo &raquo;',\n\n];\n"
  },
  {
    "path": "lang/pt_BR/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Senhas devem ter ao menos oito caracteres e ser iguais à confirmação.',\n    'user' => \"Não pudemos encontrar um usuário com o e-mail fornecido.\",\n    'token' => 'O token de redefinição de senha é inválido para este endereço de e-mail.',\n    'sent' => 'Enviamos o link de redefinição de senha para o seu e-mail!',\n    'reset' => 'Sua senha foi redefinida com sucesso!',\n\n];\n"
  },
  {
    "path": "lang/pt_BR/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Minha conta',\n\n    'shortcuts' => 'Atalhos',\n    'shortcuts_interface' => 'Preferências de Atalho UI',\n    'shortcuts_toggle_desc' => 'Aqui você pode habilitar ou desabilitar os atalhos da interface do sistema de teclado, usados para navegação e ações.',\n    'shortcuts_customize_desc' => 'Você pode personalizar cada um dos atalhos abaixo. Basta pressionar a combinação de teclas desejada após selecionar a entrada para um atalho.',\n    'shortcuts_toggle_label' => 'Atalhos de teclado ativados',\n    'shortcuts_section_navigation' => 'Navegação',\n    'shortcuts_section_actions' => 'Ações Comuns',\n    'shortcuts_save' => 'Salvar Atalhos',\n    'shortcuts_overlay_desc' => 'Observação: quando os atalhos estão ativados, uma sobreposição auxiliar está disponível pressionando \"?\" que destacará os atalhos disponíveis para ações atualmente visíveis na tela.',\n    'shortcuts_update_success' => 'As preferências de atalho foram atualizadas!',\n    'shortcuts_overview_desc' => 'Gerencie os atalhos de teclado que você pode usar para navegar na interface de usuário do sistema.',\n\n    'notifications' => 'Preferências de notificação',\n    'notifications_desc' => 'Controle as notificações por e-mail que você recebe quando uma determinada atividade é executada no sistema.',\n    'notifications_opt_own_page_changes' => 'Notificar quando houver alterações em páginas que eu possuo',\n    'notifications_opt_own_page_comments' => 'Notificar comentários nas páginas que eu possuo',\n    'notifications_opt_comment_mentions' => 'Notificar quando eu for mencionado em um comentário',\n    'notifications_opt_comment_replies' => 'Notificar ao responder aos meus comentários',\n    'notifications_save' => 'Salvar Preferências',\n    'notifications_update_success' => 'Preferências de notificação foram atualizadas!',\n    'notifications_watched' => 'Itens assistidos e ignorados',\n    'notifications_watched_desc' => 'Abaixo estão os itens que possuem preferências de relógio personalizadas aplicadas. Para atualizar suas preferências para estes, veja o item e encontre as opções de relógio na barra lateral.',\n\n    'auth' => 'Acesso & Segurança',\n    'auth_change_password' => 'Mudar a senha',\n    'auth_change_password_desc' => 'Altere a senha que você usa para fazer login no aplicativo. Deve ter pelo menos 8 caracteres.',\n    'auth_change_password_success' => 'A senha foi atualizada!',\n\n    'profile' => 'Detalhes do Perfil',\n    'profile_desc' => 'Gerencie os detalhes da sua conta que o representam perante outros usuários, além dos detalhes que são utilizados para comunicação e personalização do sistema.',\n    'profile_view_public' => 'Visualizar Perfil Público',\n    'profile_name_desc' => 'Configure seu nome de exibição que ficará visível para outros usuários no sistema por meio da atividade que você realiza e do conteúdo de sua propriedade.',\n    'profile_email_desc' => 'Este e-mail será utilizado para notificações e, dependendo da autenticação ativa do sistema, acesso ao sistema.',\n    'profile_email_no_permission' => 'Infelizmente você não tem permissão para alterar seu endereço de e-mail. Se quiser alterar isso, você precisará pedir a um administrador para alterar isso para você.',\n    'profile_avatar_desc' => 'Selecione uma imagem que será usada para representar você mesmo para outras pessoas no sistema. Idealmente, esta imagem deve ser quadrada e ter cerca de 256px de largura e altura.',\n    'profile_admin_options' => 'Opções do administrador',\n    'profile_admin_options_desc' => 'Opções adicionais do nível administrador, como as que visam gerenciar as atribuições de perfis, podem ser encontradas para sua conta de usuário na área \"Configurações > Usuários\" do aplicativo.',\n\n    'delete_account' => 'Deletar conta',\n    'delete_my_account' => 'Deletar minha conta',\n    'delete_my_account_desc' => 'Isto excluirá completamente sua conta de usuário do sistema. Você não poderá recuperar esta conta ou reverter esta ação. O conteúdo que você criou, como páginas criadas e imagens carregadas, permanecerá.',\n    'delete_my_account_warning' => 'Tem certeza de que deseja deletar sua conta?',\n];\n"
  },
  {
    "path": "lang/pt_BR/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Configurações',\n    'settings_save' => 'Salvar Configurações',\n    'system_version' => 'Versão do Sistema',\n    'categories' => 'Categorias',\n\n    // App Settings\n    'app_customization' => 'Customização',\n    'app_features_security' => 'Recursos & Segurança',\n    'app_name' => 'Nome da Aplicação',\n    'app_name_desc' => 'Esse nome será mostrado no cabeçalho e nos e-mails.',\n    'app_name_header' => 'Mostrar o nome no cabeçalho',\n    'app_public_access' => 'Acesso Público',\n    'app_public_access_desc' => 'Habilitar esta opção irá permitir que visitantes, que não estão logados, acessem o conteúdo em sua instância do BookStack.',\n    'app_public_access_desc_guest' => 'O acesso de visitantes públicos pode ser controlado através do usuário \"Convidado\".',\n    'app_public_access_toggle' => 'Permitir acesso público',\n    'app_public_viewing' => 'Permitir visualização pública?',\n    'app_secure_images' => 'Upload de Imagens mais Seguro',\n    'app_secure_images_toggle' => 'Habilitar uploads de imagem mais seguro',\n    'app_secure_images_desc' => 'Por razões de performance, todas as imagens são públicas. Esta opção adiciona uma string randômica na frente das URLs de imagens. Certifique-se de que os diretórios não possam ser indexados para prevenir acesso indesejado.',\n    'app_default_editor' => 'Editor de Página Padrão',\n    'app_default_editor_desc' => 'Selecione qual editor será usado por padrão ao editar novas páginas. Isso pode ser substituído em um nível de página onde é permitido.',\n    'app_custom_html' => 'Conteúdo customizado para <head> HTML',\n    'app_custom_html_desc' => 'Qualquer conteúdo adicionado aqui será inserido ao final do <head> HTML de todas as páginas. Isso é útil para sobrescrever estilos e adicionar códigos de análise e estatística do site.',\n    'app_custom_html_disabled_notice' => 'O conteúdo customizado do <head> HTML está desabilitado nesta página de configurações para garantir que quaisquer alterações danosas possam ser revertidas.',\n    'app_logo' => 'Logo da Aplicação',\n    'app_logo_desc' => 'Isto é usado na barra de cabeçalho do aplicativo, entre outras áreas. Esta imagem deve ter 86px de altura. Imagens grandes serão reduzidas.',\n    'app_icon' => 'Ícone do Aplicativo',\n    'app_icon_desc' => 'Este ícone é usado para guias e ícones de atalhos do navegador. Deve ser uma imagem PNG quadrada de 256px.',\n    'app_homepage' => 'Página Inicial',\n    'app_homepage_desc' => 'Selecione uma opção para ser exibida como página inicial no lugar da página padrão. Permissões de página serão ignoradas para as páginas selecionadas.',\n    'app_homepage_select' => 'Selecione uma página',\n    'app_footer_links' => 'Links do Rodapé',\n    'app_footer_links_desc' => 'Adicionar links para mostrar dentro do rodapé do site. Estes serão exibidos na parte inferior da maioria das páginas, incluindo aqueles que não necessitam de login. Você pode usar uma etiqueta de \"trans::<key>\" para usar traduções definidas pelo sistema. Por exemplo: Usando \"trans::common.privacy_policy\" fornecerá o texto traduzido \"Política de Privacidade\" e \"trans::common.terms_of_service\" fornecerá o texto traduzido \"Termos de Serviço\".',\n    'app_footer_links_label' => 'Etiqueta do Link',\n    'app_footer_links_url' => 'URL do Link',\n    'app_footer_links_add' => 'Adicionar Link de Rodapé',\n    'app_disable_comments' => 'Desativar Comentários',\n    'app_disable_comments_toggle' => 'Desativar comentários',\n    'app_disable_comments_desc' => 'Desativar comentários em todas as páginas no aplicativo.<br> Comentários existentes não serão exibidos.',\n\n    // Color settings\n    'color_scheme' => 'Esquema de Cores do Aplicativo',\n    'color_scheme_desc' => 'Defina as cores a serem usadas na interface do usuário do aplicativo. As cores podem ser configuradas separadamente para modos escuro e claro para melhor se adequar ao tema e garantir legibilidade.',\n    'ui_colors_desc' => 'Defina a cor primária do aplicativo e a cor padrão para links. A cor principal é usada principalmente para o banner do cabeçalho, botões e decorações da interface. A cor padrão para links é usada para links e ações baseados em texto, tanto dentro do conteúdo escrito quanto na interface do aplicativo.',\n    'app_color' => 'Cor Primária',\n    'link_color' => 'Cor Padrão para Links',\n    'content_colors_desc' => 'Definir cores para todos os elementos na hierarquia da organização da página. Escolher cores com um brilho semelhante às cores padrão é recomendado para legibilidade.',\n    'bookshelf_color' => 'Cor da Estante',\n    'book_color' => 'Cor do Livro',\n    'chapter_color' => 'Cor do Capítulo',\n    'page_color' => 'Cor da Página',\n    'page_draft_color' => 'Cor do Rascunho',\n\n    // Registration Settings\n    'reg_settings' => 'Cadastro',\n    'reg_enable' => 'Habilitar Cadastro',\n    'reg_enable_toggle' => 'Habilitar cadastro',\n    'reg_enable_desc' => 'Quando o cadastro é habilitado, visitantes poderão cadastrar-se como usuários do aplicativo. Realizado o cadastro, recebem um único perfil padrão.',\n    'reg_default_role' => 'Perfil padrão para usuários após o cadastro',\n    'reg_enable_external_warning' => 'A opção acima é ignorada enquanto a autenticação externa LDAP ou SAML estiver ativa. Contas de usuários para membros não existentes serão criadas automaticamente se a autenticação pelo sistema externo em uso for bem sucedida.',\n    'reg_email_confirmation' => 'Confirmação de E-mail',\n    'reg_email_confirmation_toggle' => 'Requerer confirmação de e-mail',\n    'reg_confirm_email_desc' => 'Em caso da restrição de domínios estar em uso, a confirmação de e-mail será requerida e essa opção será ignorada.',\n    'reg_confirm_restrict_domain' => 'Restrição de Domínios',\n    'reg_confirm_restrict_domain_desc' => 'Entre com uma lista separada por vírgulas de domínios de e-mails aos quais você deseja restringir o cadastro. Um e-mail de confirmação será enviado para o usuário validar seu endereço de e-mail antes de ser permitido a interagir com a aplicação. <br> Note que os usuários serão capazes de alterar o seus endereços de e-mail após o sucesso na confirmação do cadastro.',\n    'reg_confirm_restrict_domain_placeholder' => 'Nenhuma restrição definida',\n\n    // Sorting Settings\n    'sorting' => 'Listas e classificações',\n    'sorting_book_default' => 'Regra padrão de classificação de livros',\n    'sorting_book_default_desc' => 'Selecione a regra de ordenação padrão a ser aplicada a novos livros. Isso não afetará os livros existentes e pode ser substituído para cada livro individualmente.',\n    'sorting_rules' => 'Regras de ordenação',\n    'sorting_rules_desc' => 'Estas são operações de ordenação pré-definidas que podem ser aplicadas a conteúdos no sistema.',\n    'sort_rule_assigned_to_x_books' => 'Atribuído a :count Livros|Atribuído a :count Livros',\n    'sort_rule_create' => 'Criar Regra de Ordenação',\n    'sort_rule_edit' => 'Editar Regra de Ordenação',\n    'sort_rule_delete' => 'Excluir Regra de Ordenação',\n    'sort_rule_delete_desc' => 'Remover esta regra de ordenação do sistema. Os livros usando este tipo serão revertidos para a ordenação manual.',\n    'sort_rule_delete_warn_books' => 'Esta regra de ordenação está sendo usada atualmente em :count livro(s). Tem certeza de que deseja excluí-la?',\n    'sort_rule_delete_warn_default' => 'Esta regra de ordenação é atualmente usada como padrão para livros. Tem certeza de que deseja excluí-la?',\n    'sort_rule_details' => 'Detalhes das Regras de Ordenação',\n    'sort_rule_details_desc' => 'Defina um nome para esta regra de ordenação, que aparecerá nas listas quando os usuários selecionarem uma ordenação.',\n    'sort_rule_operations' => 'Operações de Ordenação',\n    'sort_rule_operations_desc' => 'Configure as ações de ordenação a serem executadas movendo-as da lista de operações disponíveis. Ao usar, as operações serão aplicadas em ordem, de cima para baixo. Quaisquer alterações feitas aqui serão aplicadas a todos os livros atribuídos após salvar.',\n    'sort_rule_available_operations' => 'Operações Disponíveis',\n    'sort_rule_available_operations_empty' => 'Não há operações restantes',\n    'sort_rule_configured_operations' => 'Operações configuradas',\n    'sort_rule_configured_operations_empty' => 'Arrastar/adicionar operações da lista \"Operações Disponíveis\"',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Nome - Alfabético',\n    'sort_rule_op_name_numeric' => 'Nome - Numérico',\n    'sort_rule_op_created_date' => 'Data de Criação',\n    'sort_rule_op_updated_date' => 'Data de Atualização',\n    'sort_rule_op_chapters_first' => 'Capítulos Primeiro',\n    'sort_rule_op_chapters_last' => 'Capítulos por Último',\n    'sorting_page_limits' => 'Limites de exibição por página',\n    'sorting_page_limits_desc' => 'Defina quantos itens mostrar por página em várias listas no sistema. Normalmente, uma quantidade menor será mais eficiente, enquanto uma quantidade maior evita a necessidade de clicar em várias páginas. Recomenda-se usar um múltiplo de 6.',\n\n    // Maintenance settings\n    'maint' => 'Manutenção',\n    'maint_image_cleanup' => 'Limpeza de Imagens',\n    'maint_image_cleanup_desc' => 'Examina páginas e revisa seus conteúdos para verificar quais imagens e desenhos estão atualmente em uso e quais são redundantes. Certifique-se de criar um backup completo do banco de dados e imagens antes de executar esta ação.',\n    'maint_delete_images_only_in_revisions' => 'Também excluir imagens que existem apenas em revisões de página antigas',\n    'maint_image_cleanup_run' => 'Executar Limpeza',\n    'maint_image_cleanup_warning' => ':count imagens potencialmente não utilizadas foram encontradas. Tem certeza de que deseja excluir estas imagens?',\n    'maint_image_cleanup_success' => ':count imagens potencialmente não utilizadas foram encontradas e excluídas!',\n    'maint_image_cleanup_nothing_found' => 'Nenhuma imagem não utilizada foi encontrada, nada foi excluído!',\n    'maint_send_test_email' => 'Enviar um E-mail de Teste',\n    'maint_send_test_email_desc' => 'Esta opção envia um e-mail de teste para o endereço especificado no seu perfil.',\n    'maint_send_test_email_run' => 'Enviar e-mail de teste',\n    'maint_send_test_email_success' => 'E-mail enviado para :address',\n    'maint_send_test_email_mail_subject' => 'E-mail de Teste',\n    'maint_send_test_email_mail_greeting' => 'O envio de e-mails parece funcionar!',\n    'maint_send_test_email_mail_text' => 'Parabéns! Já que você recebeu esta notificação, suas opções de e-mail parecem estar configuradas corretamente.',\n    'maint_recycle_bin_desc' => 'Estantes, livros, capítulos e páginas excluídos são mandados para a lixeira podendo assim ser restaurados ou excluídos permanentemente. Itens mais antigos da lixeira podem vir a ser automaticamente removidos da lixeira após um tempo dependendo da configuração do sistema.',\n    'maint_recycle_bin_open' => 'Abrir Lixeira',\n    'maint_regen_references' => 'Regenerar referências',\n    'maint_regen_references_desc' => 'Essa ação reconstruirá o índice de referência entre itens no banco de dados. Isso geralmente é tratado automaticamente, mas essa ação pode ser útil para indexar conteúdo antigo ou adicionado por métodos não oficiais.',\n    'maint_regen_references_success' => 'O índice de referência foi regenerado!',\n    'maint_timeout_command_note' => 'Observação: essa ação pode levar algum tempo para ser executada, o que pode levar a problemas de tempo limite em alguns ambientes da Web. Como alternativa, esta ação pode ser executada usando um comando de terminal.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Lixeira',\n    'recycle_bin_desc' => 'Aqui você pode restaurar itens que foram excluídos ou escolher removê-los permanentemente do sistema. Esta lista não é filtrada diferentemente de listas de atividades similares no sistema onde filtros de permissão são aplicados.',\n    'recycle_bin_deleted_item' => 'Item excluído',\n    'recycle_bin_deleted_parent' => 'Parente',\n    'recycle_bin_deleted_by' => 'Excluído por',\n    'recycle_bin_deleted_at' => 'Momento de Exclusão',\n    'recycle_bin_permanently_delete' => 'Excluir permanentemente',\n    'recycle_bin_restore' => 'Restaurar',\n    'recycle_bin_contents_empty' => 'A lixeira está vazia',\n    'recycle_bin_empty' => 'Esvaziar Lixeira',\n    'recycle_bin_empty_confirm' => 'Isso irá destruir permanentemente todos os itens na lixeira inclusive o conteúdo de cada item. Tem certeza de que quer esvaziar a lixeira?',\n    'recycle_bin_destroy_confirm' => 'Esta ação excluirá permanentemente este item do sistema, juntamente com quaisquer elementos secundários listados abaixo, e você não poderá restaurar este conteúdo. Tem certeza de que deseja excluir permanentemente este item?',\n    'recycle_bin_destroy_list' => 'Itens a serem Destruídos',\n    'recycle_bin_restore_list' => 'Itens a serem restaurados',\n    'recycle_bin_restore_confirm' => 'Esta ação irá restaurar o item excluído, inclusive quaisquer elementos filhos, para seu local original. Se a localização original tiver, entretanto, sido eliminada e estiver agora na lixeira, o item pai também precisará ser restaurado.',\n    'recycle_bin_restore_deleted_parent' => 'O pai deste \\'item\\' também foi excluído. Eles permanecerão excluídos até que o pai também seja restaurado.',\n    'recycle_bin_restore_parent' => 'Restaurar Parente',\n    'recycle_bin_destroy_notification' => 'Excluído: conta o total de itens da lixeira.',\n    'recycle_bin_restore_notification' => 'Excluído: conta o total de itens da lixeira.',\n\n    // Audit Log\n    'audit' => 'Registro de auditoria',\n    'audit_desc' => 'Este log de auditoria exibe uma lista de atividades rastreadas no sistema. Essa lista não é filtrada, ao contrário de listas de atividades semelhantes no sistema em que os filtros de permissão são aplicados.',\n    'audit_event_filter' => 'Filtro de Eventos',\n    'audit_event_filter_no_filter' => 'Sem filtro',\n    'audit_deleted_item' => 'Item excluído',\n    'audit_deleted_item_name' => 'Nome: :name',\n    'audit_table_user' => 'Usuário',\n    'audit_table_event' => 'Evento',\n    'audit_table_related' => '\\'Item\\' ou Detalhe Relacionado',\n    'audit_table_ip' => 'Endereço IP',\n    'audit_table_date' => 'Data da Atividade',\n    'audit_date_from' => 'Período de',\n    'audit_date_to' => 'Para',\n\n    // Role Settings\n    'roles' => 'Perfis',\n    'role_user_roles' => 'Perfis de Usuários',\n    'roles_index_desc' => 'Os perfis são usados para agrupar usuários & fornecer permissão de sistema a seus membros. Quando um usuário possui vários perfis, os privilégios concedidos serão acumulados e o usuário herdará todas as habilidades.',\n    'roles_x_users_assigned' => ':count usuário atribuído|:count usuários atribuídos',\n    'roles_x_permissions_provided' => ':count permissão|:count permissões',\n    'roles_assigned_users' => 'Usuários atribuídos',\n    'roles_permissions_provided' => 'Permissões fornecidas',\n    'role_create' => 'Criar novo Perfil',\n    'role_delete' => 'Excluir Perfil',\n    'role_delete_confirm' => 'A ação vai excluír o perfil de nome \\':roleName\\'.',\n    'role_delete_users_assigned' => 'Esse perfil tem :userCount usuários vinculados a ele. Se quiser migrar usuários desse perfil para outro, selecione um novo perfil.',\n    'role_delete_no_migration' => \"Não migre os usuários\",\n    'role_delete_sure' => 'Tem certeza que deseja excluir esse perfil?',\n    'role_edit' => 'Editar Perfil',\n    'role_details' => 'Detalhes do Perfil',\n    'role_name' => 'Nome do Perfil',\n    'role_desc' => 'Breve Descrição do Perfil',\n    'role_mfa_enforced' => 'Requer Autenticação Multi-fator',\n    'role_external_auth_id' => 'IDs de Autenticação Externa',\n    'role_system' => 'Permissões do Sistema',\n    'role_manage_users' => 'Gerenciar usuários',\n    'role_manage_roles' => 'Gerenciar perfis e permissões de perfis',\n    'role_manage_entity_permissions' => 'Gerenciar todos os livros, capítulos e permissões de páginas',\n    'role_manage_own_entity_permissions' => 'Gerenciar permissões de seu próprio livro, capítulo e paginas',\n    'role_manage_page_templates' => 'Gerenciar modelos de página',\n    'role_access_api' => 'Acessar API do sistema',\n    'role_manage_settings' => 'Gerenciar configurações da aplicação',\n    'role_export_content' => 'Exportar conteúdo',\n    'role_import_content' => 'Importar conteúdo',\n    'role_editor_change' => 'Alterar página de edição',\n    'role_notifications' => 'Receber e gerenciar notificações',\n    'role_permission_note_users_and_roles' => 'Essas permissões tecnicamente também fornecerão visibilidade e busca de usuários e perfis no sistema.',\n    'role_asset' => 'Permissões de Ativos',\n    'roles_system_warning' => 'Esteja ciente de que o acesso a qualquer uma das três permissões acima pode permitir que um usuário altere seus próprios privilégios ou privilégios de outros usuários no sistema. Apenas atribua perfis com essas permissões para usuários confiáveis.',\n    'role_asset_desc' => 'Essas permissões controlam o acesso padrão para os ativos dentro do sistema. Permissões em Livros, Capítulos e Páginas serão sobrescritas por essas permissões.',\n    'role_asset_admins' => 'Administradores recebem automaticamente acesso a todo o conteúdo, mas essas opções podem mostrar ou ocultar as opções da Interface de Usuário.',\n    'role_asset_image_view_note' => 'Isso está relacionado à visibilidade no gerenciador de imagens. O acesso real dos arquivos de imagem carregados dependerá da opção de armazenamento de imagem do sistema.',\n    'role_asset_users_note' => 'Essas permissões tecnicamente também fornecerão visibilidade e busca de usuários do sistema.',\n    'role_all' => 'Todos',\n    'role_own' => 'Próprio',\n    'role_controlled_by_asset' => 'Controlado pelos ativos nos quais o upload foi realizado',\n    'role_save' => 'Salvar Perfil',\n    'role_users' => 'Usuários com este perfil',\n    'role_users_none' => 'Nenhum usuário está atualmente vinculado a este perfil',\n\n    // Users\n    'users' => 'Usuários',\n    'users_index_desc' => 'Crie e gerencie contas de usuários individuais dentro do sistema. As contas de usuário são usadas para login e atribuição de conteúdo e atividade. As permissões de acesso são baseadas principalmente no perfil, mas a propriedade do conteúdo do usuário, entre outros fatores, também pode afetar as permissões e o acesso.',\n    'user_profile' => 'Perfil do Usuário',\n    'users_add_new' => 'Adicionar Novo Usuário',\n    'users_search' => 'Pesquisar Usuários',\n    'users_latest_activity' => 'Última Atividade',\n    'users_details' => 'Detalhes do Usuário',\n    'users_details_desc' => 'Defina um nome de exibição e um endereço de e-mail para este usuário. O endereço de e-mail será usado para fazer login na aplicação.',\n    'users_details_desc_no_email' => 'Defina um nome de exibição para este usuário para que outros usuários possam reconhecê-lo',\n    'users_role' => 'Perfis do Usuário',\n    'users_role_desc' => 'Selecione os perfis aos quais este usuário será vinculado. Se um usuário for vinculado a múltiplos perfis, suas permissões serão empilhadas e ele receberá todas as habilidades dos perfis atribuídos.',\n    'users_password' => 'Senha do Usuário',\n    'users_password_desc' => 'Defina uma senha usada para fazer \\'login\\' no aplicativo. Deve ter pelo menos 8 caracteres.',\n    'users_send_invite_text' => 'Você pode escolher enviar a este usuário um convite por e-mail que o possibilitará definir sua própria senha, ou defina você uma senha.',\n    'users_send_invite_option' => 'Enviar convite por e-mail',\n    'users_external_auth_id' => 'ID de Autenticação Externa',\n    'users_external_auth_id_desc' => 'Quando um sistema de autenticação externo está em uso (como SAML2, OIDC ou LDAP), este é o ID que vincula este usuário do BookStack à conta do sistema de autenticação. Você pode ignorar este campo se estiver usando a autenticação padrão baseada em email.',\n    'users_password_warning' => 'Preencha o seguinte apenas se desejar alterar a senha deste usuário.',\n    'users_system_public' => 'Esse usuário representa quaisquer convidados que visitam o aplicativo. Ele não pode ser usado para login mas é automaticamente atribuído.',\n    'users_delete' => 'Excluir Usuário',\n    'users_delete_named' => 'Excluir :userName',\n    'users_delete_warning' => 'A ação vai excluir completamente o usuário de nome \\':userName\\' do sistema.',\n    'users_delete_confirm' => 'Tem certeza que deseja excluir esse usuário?',\n    'users_migrate_ownership' => 'Migrar propriedade',\n    'users_migrate_ownership_desc' => 'Selecione um usuário aqui, se você deseja que outro se torne o proprietário de todos os itens atualmente pertencentes a este usuário.',\n    'users_none_selected' => 'Nenhum usuário selecionado',\n    'users_edit' => 'Editar Usuário',\n    'users_edit_profile' => 'Editar Perfil',\n    'users_avatar' => 'Imagem de Usuário',\n    'users_avatar_desc' => 'Defina uma imagem para representar este usuário. Essa imagem deve ser um quadrado com aproximadamente 256px de altura e largura.',\n    'users_preferred_language' => 'Linguagem de Preferência',\n    'users_preferred_language_desc' => 'Esta opção irá alterar o idioma utilizado para a interface de usuário da aplicação. Isto não afetará nenhum conteúdo criado por usuários.',\n    'users_social_accounts' => 'Contas Sociais',\n    'users_social_accounts_desc' => 'Veja o status das contas sociais conectadas deste usuário. As contas sociais podem ser usadas além do sistema de autenticação principal para acesso ao sistema.',\n    'users_social_accounts_info' => 'Aqui você pode conectar outras contas para acesso mais rápido. Desconectar uma conta não retira a possibilidade de acesso usando-a. Para revogar o acesso ao perfil através da conta social, você deverá fazê-lo na sua conta social.',\n    'users_social_connect' => 'Contas Conectadas',\n    'users_social_disconnect' => 'Desconectar Conta',\n    'users_social_status_connected' => 'Conectado',\n    'users_social_status_disconnected' => 'Desconectado',\n    'users_social_connected' => 'Conta :socialAccount foi conectada com sucesso ao seu perfil.',\n    'users_social_disconnected' => 'Conta :socialAccount foi desconectada com sucesso de seu perfil.',\n    'users_api_tokens' => 'Tokens de API',\n    'users_api_tokens_desc' => 'Crie e gerencie os tokens de acesso usados para autenticação com a API REST do BookStack. As permissões para a API são gerenciadas pelo usuário ao qual o token pertence.',\n    'users_api_tokens_none' => 'Nenhum token de API foi criado para este usuário',\n    'users_api_tokens_create' => 'Criar Token',\n    'users_api_tokens_expires' => 'Expira',\n    'users_api_tokens_docs' => 'Documentação da API',\n    'users_mfa' => 'Autenticação de Múltiplos Fatores',\n    'users_mfa_desc' => 'A autenticação multi-fator adiciona outra camada de segurança à sua conta.',\n    'users_mfa_x_methods' => ':count método configurado|:count métodos configurados',\n    'users_mfa_configure' => 'Configurar Métodos',\n\n    // API Tokens\n    'user_api_token_create' => 'Criar Token de API',\n    'user_api_token_name' => 'Nome',\n    'user_api_token_name_desc' => 'Dê ao seu token um nome legível como um futuro lembrete de seu propósito.',\n    'user_api_token_expiry' => 'Data de Expiração',\n    'user_api_token_expiry_desc' => 'Defina uma data em que este token expira. Depois desta data, as requisições feitas usando este token não funcionarão mais. Deixar este campo em branco definirá um prazo de 100 anos futuros.',\n    'user_api_token_create_secret_message' => 'Imediatamente após a criação deste token, um \"ID de token\" e \"Secreto de token\" serão gerados e exibidos. O segredo só será mostrado uma única vez, portanto, certifique-se de copiar o valor para algum lugar seguro antes de prosseguir.',\n    'user_api_token' => 'Token de API',\n    'user_api_token_id' => 'ID do Token',\n    'user_api_token_id_desc' => 'Este é um identificador de sistema não editável, gerado para este token, que precisará ser fornecido em solicitações de API.',\n    'user_api_token_secret' => 'Segredo do Token',\n    'user_api_token_secret_desc' => 'Este é um segredo de sistema gerado para este token que precisará ser fornecido em requisições de API. Isto só será mostrado nesta única vez, portanto, copie este valor para um lugar seguro.',\n    'user_api_token_created' => 'Token Criado :timeAgo',\n    'user_api_token_updated' => 'Token Atualizado :timeAgo',\n    'user_api_token_delete' => 'Excluir Token',\n    'user_api_token_delete_warning' => 'Isto irá excluir completamente este token de API com o nome \\':tokenName\\' do sistema.',\n    'user_api_token_delete_confirm' => 'Você tem certeza que deseja excluir este token de API?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Os webhooks são uma maneira de enviar dados para URLs externos quando certas ações e eventos ocorrem dentro do sistema, o que permite a integração baseada em eventos com plataformas externas, como sistemas de mensagens ou notificação.',\n    'webhooks_x_trigger_events' => ':count evento de gatilho|:count eventos de gatilho',\n    'webhooks_create' => 'Criar novo webhook',\n    'webhooks_none_created' => 'Nenhum webhooks foi criado ainda.',\n    'webhooks_edit' => 'Editar webhook',\n    'webhooks_save' => 'Salvar webhook',\n    'webhooks_details' => 'Detalhes do Webhook',\n    'webhooks_details_desc' => 'Forneça um nome amigável e um endpoint POST como um local para que os dados de webhook sejam enviados.',\n    'webhooks_events' => 'Eventos de webhook',\n    'webhooks_events_desc' => 'Selecionar todos os eventos que devem acionar este webhook para serem chamados.',\n    'webhooks_events_warning' => 'Tenha em mente que esses eventos serão acionados para todos os eventos selecionados, mesmo se as permissões personalizadas forem aplicadas. Certifique-se de que o uso deste webhook não exponha conteúdo confidencial.',\n    'webhooks_events_all' => 'Todos eventos do sistema',\n    'webhooks_name' => 'Nome Webhook',\n    'webhooks_timeout' => 'Solicitação de Webhook Timeout (Segundos)',\n    'webhooks_endpoint' => 'Endpoint Webhook',\n    'webhooks_active' => 'Webhook ativo',\n    'webhook_events_table_header' => 'Eventos',\n    'webhooks_delete' => 'Excluir webhook',\n    'webhooks_delete_warning' => 'Isto irá excluir completamente este webhook, com o nome \":webhookName\" do sistema.',\n    'webhooks_delete_confirm' => 'Tem certeza que deseja excluir este webhook?',\n    'webhooks_format_example' => 'Exemplo de formato Webhook',\n    'webhooks_format_example_desc' => 'Os dados do Webhook são enviados como uma solicitação POST para o ponto de extremidade configurado como JSON seguindo o formato abaixo. As propriedades \"related_item\" e \"url\" são opcionais e dependerão do tipo de evento acionado.',\n    'webhooks_status' => 'Estado do \"Webhook\"',\n    'webhooks_last_called' => 'Última chamada:',\n    'webhooks_last_errored' => 'Último Erro:',\n    'webhooks_last_error_message' => 'Última mensagem de erro:',\n\n    // Licensing\n    'licenses' => 'Licenças',\n    'licenses_desc' => 'Esta página detalha informações da licença do BookStack, além dos projetos e bibliotecas usadas no BookStack. Muitos projectos listados só podem ser utilizados num contexto de desenvolvimento.',\n    'licenses_bookstack' => 'Licença do BookStack',\n    'licenses_php' => 'Licenças de Bibliotecas PHP',\n    'licenses_js' => 'Licenças de Bibliotecas JavaScript',\n    'licenses_other' => 'Outras licenças',\n    'license_details' => 'Detalhes da Licença',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/pt_BR/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => 'O campo :attribute deve ser aceito.',\n    'active_url'           => 'O campo :attribute não é uma URL válida.',\n    'after'                => 'O campo :attribute deve ser uma data posterior à data :date.',\n    'alpha'                => 'O campo :attribute deve conter apenas letras.',\n    'alpha_dash'           => 'O campo :attribute deve conter apenas letras, números, traços e underlines.',\n    'alpha_num'            => 'O campo :attribute deve conter apenas letras e números.',\n    'array'                => 'O campo :attribute deve ser uma array.',\n    'backup_codes'         => 'O código fornecido não é válido ou já foi usado.',\n    'before'               => 'O campo :attribute deve ser uma data anterior à data :date.',\n    'between'              => [\n        'numeric' => 'O campo :attribute deve estar entre :min e :max.',\n        'file'    => 'O campo :attribute deve ter entre :min e :max kilobytes.',\n        'string'  => 'O campo :attribute deve ter entre :min e :max caracteres.',\n        'array'   => 'O campo :attribute deve ter entre :min e :max itens.',\n    ],\n    'boolean'              => 'O campo :attribute deve ser verdadeiro ou falso.',\n    'confirmed'            => 'O campo :attribute não é igual à sua confirmação.',\n    'date'                 => 'O campo :attribute não está em um formato de data válido.',\n    'date_format'          => 'O campo :attribute não tem a formatação :format.',\n    'different'            => 'O campo :attribute e o campo :other devem ser diferentes.',\n    'digits'               => 'O campo :attribute deve ter :digits dígitos.',\n    'digits_between'       => 'O campo :attribute deve ter entre :min e :max dígitos.',\n    'email'                => 'O campo :attribute deve ser um e-mail válido.',\n    'ends_with' => 'O campo :attribute deve terminar com um dos seguintes: :values',\n    'file'                 => 'O :attribute deve ser um arquivo válido.',\n    'filled'               => 'O campo :attribute é requerido.',\n    'gt'                   => [\n        'numeric' => 'O campo :attribute deve ser maior que :value.',\n        'file'    => 'O campo :attribute deve ser maior que :value kilobytes.',\n        'string'  => 'O campo :attribute deve ser maior que :value caracteres.',\n        'array'   => 'O campo :attribute deve ter mais que :value itens.',\n    ],\n    'gte'                  => [\n        'numeric' => 'O campo :attribute deve ser maior ou igual a :value.',\n        'file'    => 'O campo :attribute deve ser maior ou igual a :value kilobytes.',\n        'string'  => 'O campo :attribute deve ser maior ou igual a :value caracteres.',\n        'array'   => 'O campo :attribute deve ter :value itens ou mais.',\n    ],\n    'exists'               => 'O campo :attribute selecionado não é válido.',\n    'image'                => 'O campo :attribute deve ser uma imagem.',\n    'image_extension'      => 'O campo :attribute deve ter uma extensão de imagem válida e suportada.',\n    'in'                   => 'O campo :attribute selecionado não é válido.',\n    'integer'              => 'O campo :attribute deve ser um número inteiro.',\n    'ip'                   => 'O campo :attribute deve ser um endereço IP válido.',\n    'ipv4'                 => 'O campo :attribute deve ser um endereço IPv4 válido.',\n    'ipv6'                 => 'O campo :attribute deve ser um endereço IPv6 válido.',\n    'json'                 => 'O campo :attribute deve ser uma string JSON válida.',\n    'lt'                   => [\n        'numeric' => 'O campo :attribute deve ser menor que :value.',\n        'file'    => 'O campo :attribute deve ser menor que :value kilobytes.',\n        'string'  => 'O campo :attribute deve ser menor que :value caracteres.',\n        'array'   => 'O campo :attribute deve conter menos que :value itens.',\n    ],\n    'lte'                  => [\n        'numeric' => 'O campo :attribute deve ser menor ou igual a :value.',\n        'file'    => 'O campo :attribute deve ser menor ou igual a :value kilobytes.',\n        'string'  => 'O campo :attribute deve ser menor ou igual a :value caracteres.',\n        'array'   => 'O campo :attribute não deve conter mais que :value itens.',\n    ],\n    'max'                  => [\n        'numeric' => 'O valor para o campo :attribute não deve ser maior que :max.',\n        'file'    => 'O valor para o campo :attribute não deve ter tamanho maior que :max kilobytes.',\n        'string'  => 'O valor para o campo :attribute não deve ter mais que :max caracteres.',\n        'array'   => 'O valor para o campo :attribute não deve ter mais que :max itens.',\n    ],\n    'mimes'                => 'O campo :attribute deve ser do tipo type: :values.',\n    'min'                  => [\n        'numeric' => 'O campo :attribute não deve ser menor que :min.',\n        'file'    => 'O campo :attribute não deve ter tamanho menor que :min kilobytes.',\n        'string'  => 'O campo :attribute não deve ter menos que :min caracteres.',\n        'array'   => 'O campo :attribute não deve ter menos que :min itens.',\n    ],\n    'not_in'               => 'O campo selecionado :attribute é inválido.',\n    'not_regex'            => 'O formato do campo :attribute é inválido.',\n    'numeric'              => 'O campo :attribute deve ser um número.',\n    'regex'                => 'O formato do campo :attribute é inválido.',\n    'required'             => 'O campo :attribute é requerido.',\n    'required_if'          => 'O campo :attribute é requerido quando o campo :other tem valor :value.',\n    'required_with'        => 'O campo :attribute é requerido quando os valores :values estiverem presentes.',\n    'required_with_all'    => 'O campo :attribute é requerido quando os valores :values estiverem presentes.',\n    'required_without'     => 'O campo :attribute é requerido quando os valores :values não estiverem presentes.',\n    'required_without_all' => 'O campo :attribute é requerido quando nenhum dos valores :values estiverem presentes.',\n    'same'                 => 'O campo :attribute e o campo :other devem ser iguais.',\n    'safe_url'             => 'O link fornecido pode não ser seguro.',\n    'size'                 => [\n        'numeric' => 'O tamanho do campo :attribute deve ser :size.',\n        'file'    => 'O tamanho do arquivo :attribute deve ser de :size kilobytes.',\n        'string'  => 'O tamanho do campo :attribute deve ser de :size caracteres.',\n        'array'   => 'O campo :attribute deve conter :size itens.',\n    ],\n    'string'               => 'O campo :attribute deve ser uma string.',\n    'timezone'             => 'O campo :attribute deve conter uma timezone válida.',\n    'totp'                 => 'O código fornecido não é válido ou expirou.',\n    'unique'               => 'Já existe um campo/dado de nome :attribute.',\n    'url'                  => 'O formato da URL :attribute é inválido.',\n    'uploaded'             => 'O arquivo não pôde ser carregado. O servidor pode não aceitar arquivos deste tamanho.',\n\n    'zip_file' => 'O :attribute precisa fazer referência a um arquivo do ZIP.',\n    'zip_file_size' => 'O arquivo :attribute não deve exceder :size MB.',\n    'zip_file_mime' => 'O :attribute precisa fazer referência a um arquivo do tipo :validTypes, encontrado :foundType.',\n    'zip_model_expected' => 'Objeto de dados esperado, mas \":type\" encontrado.',\n    'zip_unique' => 'O :attribute deve ser único para o tipo de objeto dentro do ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Confirmação de senha requerida',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/ro/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'a creat pagina',\n    'page_create_notification'    => 'Pagina creată cu succes',\n    'page_update'                 => 'a actualizat pagina',\n    'page_update_notification'    => 'Pagina a fost actualizată cu succes',\n    'page_delete'                 => 'a șters pagina',\n    'page_delete_notification'    => 'Pagina a fost ștearsă cu succes',\n    'page_restore'                => 'a restabilit pagina',\n    'page_restore_notification'   => 'Pagina a fost restaurată cu succes',\n    'page_move'                   => 'a mutat pagina',\n    'page_move_notification'      => 'Pagină mutată cu succes',\n\n    // Chapters\n    'chapter_create'              => 'a creat capitolul',\n    'chapter_create_notification' => 'Capitol creat cu succes',\n    'chapter_update'              => 'a actualizat capitolul',\n    'chapter_update_notification' => 'Capitolul a fost actualizat cu succes',\n    'chapter_delete'              => 'a șters capitolul',\n    'chapter_delete_notification' => 'Capitolul a fost șters cu succes',\n    'chapter_move'                => 'a mutat capitolul',\n    'chapter_move_notification' => 'Capitolul a fost mutat cu succes',\n\n    // Books\n    'book_create'                 => 'a creat cartea',\n    'book_create_notification'    => 'Carte creată cu succes',\n    'book_create_from_chapter'              => 'a convertit capitolul în carte',\n    'book_create_from_chapter_notification' => 'Capitol convertit cu succes într-o carte',\n    'book_update'                 => 'a actualizat cartea',\n    'book_update_notification'    => 'Carte actualizată cu succes',\n    'book_delete'                 => 'a șters cartea',\n    'book_delete_notification'    => 'Carte ștearsă cu succes',\n    'book_sort'                   => 'a sortat cartea',\n    'book_sort_notification'      => 'Carte reordonată cu succes',\n\n    // Bookshelves\n    'bookshelf_create'            => 'raft creat',\n    'bookshelf_create_notification'    => 'Raftul a fost creat cu succes',\n    'bookshelf_create_from_book'    => 'cartea a fost convertită in raft',\n    'bookshelf_create_from_book_notification'    => 'Carte transformată cu succes într-un raft',\n    'bookshelf_update'                 => 'raftul actualizat',\n    'bookshelf_update_notification'    => 'Raftul a fost actualizat cu succes',\n    'bookshelf_delete'                 => 'raft șters',\n    'bookshelf_delete_notification'    => 'Raftul a fost șters cu succes',\n\n    // Revisions\n    'revision_restore' => 'versiune restabilită',\n    'revision_delete' => 'revizie ștearsă',\n    'revision_delete_notification' => 'Revizuirea a fost ștearsă',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" a fost adăugat la favorite',\n    'favourite_remove_notification' => '\":name\" a fost eliminat din favorite',\n\n    // Watching\n    'watch_update_level_notification' => 'Preferințele de urmărire actualizate cu succes',\n\n    // Auth\n    'auth_login' => 'autentificat',\n    'auth_register' => 'înregistrat ca utilizator nou',\n    'auth_password_reset_request' => 'solicită utilizatorului resetarea parolei',\n    'auth_password_reset_update' => 'resetează parola utilizatorului',\n    'mfa_setup_method' => 'metoda MFA configurată',\n    'mfa_setup_method_notification' => 'Metoda multi-factor a fost configurată cu succes',\n    'mfa_remove_method' => 'metoda MFA eliminată',\n    'mfa_remove_method_notification' => 'Metoda multi-factor a fost configurată cu succes',\n\n    // Settings\n    'settings_update' => 'setări actualizate',\n    'settings_update_notification' => 'Setările au fost actualizate',\n    'maintenance_action_run' => 'rulează acțiunea de întreținere',\n\n    // Webhooks\n    'webhook_create' => 'a creat webhook',\n    'webhook_create_notification' => 'Webhook creat cu succes',\n    'webhook_update' => 'a actualizat webhook',\n    'webhook_update_notification' => 'Webhook actualizat cu succes',\n    'webhook_delete' => 'a șters webhook',\n    'webhook_delete_notification' => 'Webhook șters cu succes',\n\n    // Imports\n    'import_create' => 'created import',\n    'import_create_notification' => 'Import successfully uploaded',\n    'import_run' => 'updated import',\n    'import_run_notification' => 'Content successfully imported',\n    'import_delete' => 'deleted import',\n    'import_delete_notification' => 'Import successfully deleted',\n\n    // Users\n    'user_create' => 'utilizator creat',\n    'user_create_notification' => 'Utilizator creat cu succes',\n    'user_update' => 'utilizator actualizat',\n    'user_update_notification' => 'Utilizator actualizat cu succes',\n    'user_delete' => 'utilizator șters',\n    'user_delete_notification' => 'Utilizator eliminat cu succes',\n\n    // API Tokens\n    'api_token_create' => 'created API token',\n    'api_token_create_notification' => 'Token API creat cu succes',\n    'api_token_update' => 'updated API token',\n    'api_token_update_notification' => 'Token API actualizat cu succes',\n    'api_token_delete' => 'deleted API token',\n    'api_token_delete_notification' => 'Token API șters cu succes',\n\n    // Roles\n    'role_create' => 'rol creat',\n    'role_create_notification' => 'Rol creat cu succes',\n    'role_update' => 'rol actualizat',\n    'role_update_notification' => 'Rol actualizat cu succes',\n    'role_delete' => 'rol șters',\n    'role_delete_notification' => 'Rol şters cu succes',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'golește cos de gunoi',\n    'recycle_bin_restore' => 'restaurat din coșul de gunoi',\n    'recycle_bin_destroy' => 'eliminat din coșul de gunoi',\n\n    // Comments\n    'commented_on'                => 'a comentat la',\n    'comment_create'              => 'comentariu adăugat',\n    'comment_update'              => 'comentariu actualizat',\n    'comment_delete'              => 'comentariu șters',\n\n    // Sort Rules\n    'sort_rule_create' => 'created sort rule',\n    'sort_rule_create_notification' => 'Sort rule successfully created',\n    'sort_rule_update' => 'updated sort rule',\n    'sort_rule_update_notification' => 'Sort rule successfully updated',\n    'sort_rule_delete' => 'deleted sort rule',\n    'sort_rule_delete_notification' => 'Sort rule successfully deleted',\n\n    // Other\n    'permissions_update'          => 'a actualizat permisiunile',\n];\n"
  },
  {
    "path": "lang/ro/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Aceste credenţiale nu se potrivesc cu înregistrările noastre.',\n    'throttle' => 'Prea multe încercări de conectare. Vă rugăm să încercați din nou în :seconds secunde.',\n\n    // Login & Register\n    'sign_up' => 'Inregistrează-te',\n    'log_in' => 'Autentifică-te',\n    'log_in_with' => 'Autentifică-te cu :socialDriver',\n    'sign_up_with' => 'Inregistrează-te cu :socialDriver',\n    'logout' => 'Deconectează-te',\n\n    'name' => 'Nume',\n    'username' => 'Nume utilizator',\n    'email' => 'E-mail',\n    'password' => 'Parolă',\n    'password_confirm' => 'Confirmă parola',\n    'password_hint' => 'Trebuie să aibă cel puțin 8 caractere',\n    'forgot_password' => 'Ai uitat parola?',\n    'remember_me' => 'Amintește-ți de mine',\n    'ldap_email_hint' => 'Te rog să introduci o adresa de e-mail pentru a utiliza acest cont.',\n    'create_account' => 'Crează cont',\n    'already_have_account' => 'Ai deja cont?',\n    'dont_have_account' => 'Nu ai cont?',\n    'social_login' => 'Conectare folosind rețea socială',\n    'social_registration' => 'Înregistrare cu rețea socială',\n    'social_registration_text' => 'Înregistrați-vă și conectați-vă utilizând alt serviciu.',\n\n    'register_thanks' => 'Mulțumesc pentru înregistrare!',\n    'register_confirm' => 'Verifică-ți e-mailul și dă clic pe butonul de confirmare pentru a accesa :appName.',\n    'registrations_disabled' => 'Înregistrările sunt momentan dezactivate',\n    'registration_email_domain_invalid' => 'Acel domeniu de e-mail nu are acces la această aplicație',\n    'register_success' => 'Mulțumesc pentru înscriere! Acum sunteți înregistrat și conectat.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Se încearcă autentificarea',\n    'auto_init_starting_desc' => 'Vă contactăm sistemul de autentificare pentru a începe procesul de conectare. Dacă nu există niciun progres după 5 secunde, puteți încerca să dați clic pe linkul de mai jos.',\n    'auto_init_start_link' => 'Continuă cu autentificarea',\n\n    // Password Reset\n    'reset_password' => 'Resetează parola',\n    'reset_password_send_instructions' => 'Introduceți adresa de e-mail mai jos și vi se va trimite un e-mail cu un link pentru resetarea parolei.',\n    'reset_password_send_button' => 'Trimite linkul de resetare',\n    'reset_password_sent' => 'Un link pentru resetarea parolei va fi trimis la :email dacă acea adresă de e-mail este găsită în sistem.',\n    'reset_password_success' => 'Parola a fost resetată cu succes.',\n    'email_reset_subject' => 'Resetează parola ta :appName',\n    'email_reset_text' => 'Primești acest e-mail deoarece am primit o solicitare de resetare a parolei pentru contul tău.',\n    'email_reset_not_requested' => 'Dacă nu ai solicitat resetarea parolei, nu este necesară nicio acțiune suplimentară.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Confirmă e-mailul pentru :appName',\n    'email_confirm_greeting' => 'Mulțumim că te-ai alăturat :appName!',\n    'email_confirm_text' => 'Te rog să confirmi adresa ta de e-mail făcând clic pe butonul de mai jos:',\n    'email_confirm_action' => 'Confirmă emailul',\n    'email_confirm_send_error' => 'Este necesară confirmarea prin e-mail, dar sistemul nu a putut trimite e-mailul. Contactează administratorul pentru a te asigura că e-mailul este configurat corect.',\n    'email_confirm_success' => 'E-mailul a fost confirmat! Acum ar trebui să te poți autentifica folosind această adresă de e-mail.',\n    'email_confirm_resent' => 'E-mailul de confirmare a fost retrimis, te rugăm să îți verifici căsuța de e-mail.',\n    'email_confirm_thanks' => 'Mulțumim pentru confirmare!',\n    'email_confirm_thanks_desc' => 'Vă rugăm să așteptați un moment până când confirmarea dvs. este procesată. Dacă nu sunteți redirecționat după 3 secunde apăsați pe link-ul \"Continuați\" de mai jos pentru a fi redirecționat.',\n\n    'email_not_confirmed' => 'Adresa de e-mail neconfirmată',\n    'email_not_confirmed_text' => 'Adresa ta de e-mail nu a fost încă confirmată.',\n    'email_not_confirmed_click_link' => 'Accesează linkul din e-mailul care a fost trimis la scurt timp după ce te-ai înregistrat.',\n    'email_not_confirmed_resend' => 'Dacă nu găsești e-mailul, poți retrimite e-mailul de confirmare completând formularul de mai jos.',\n    'email_not_confirmed_resend_button' => 'Retrimite e-mail de confirmare',\n\n    // User Invite\n    'user_invite_email_subject' => 'Ai fost invitat să te alături :appName!',\n    'user_invite_email_greeting' => 'A fost creat un cont pentru tine pe :appName.',\n    'user_invite_email_text' => 'Fă clic pe butonul de mai jos pentru a seta o parolă de cont și a obține acces:',\n    'user_invite_email_action' => 'Setează parola contului',\n    'user_invite_page_welcome' => 'Bun venit la :appName!',\n    'user_invite_page_text' => 'Pentru a vă finaliza configurarea contului și a obține acces, trebuie să setezi o parolă care va fi folosită pentru a te conecta la :appName la vizitele viitoare.',\n    'user_invite_page_confirm_button' => 'Confirmă parola',\n    'user_invite_success_login' => 'Parola setată, acum ar trebui să te poți autentifica folosind parola setată pentru a accesa :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Configurează autentificarea cu mai mulți factori de autentificare',\n    'mfa_setup_desc' => 'Configurează autentificarea cu mai mulți factori ca un nivel suplimentar de securitate pentru contul tău de utilizator.',\n    'mfa_setup_configured' => 'Deja configurat',\n    'mfa_setup_reconfigure' => 'Reconfigurează',\n    'mfa_setup_remove_confirmation' => 'Sigur dorești să elimini această metodă de autentificare cu mai mulți factori?',\n    'mfa_setup_action' => 'Configurare',\n    'mfa_backup_codes_usage_limit_warning' => 'Mai ai mai puțin de 5 coduri de rezervă rămase. Vă rugăm să generați și să stocați un nou set înainte de a rămâne fără coduri pentru a preveni blocarea contului.',\n    'mfa_option_totp_title' => 'Aplicație mobilă',\n    'mfa_option_totp_desc' => 'Pentru a utiliza autentificarea multifactor, vei avea nevoie de o aplicație mobilă care acceptă TOTP, cum ar fi Google Authenticator, Authy sau Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Coduri de rezervă',\n    'mfa_option_backup_codes_desc' => 'Generates a set of one-time-use backup codes which you\\'ll enter on login to verify your identity. Make sure to store these in a safe & secure place.',\n    'mfa_gen_confirm_and_enable' => 'Confirmă și activează',\n    'mfa_gen_backup_codes_title' => 'Configurarea codurilor de rezervă',\n    'mfa_gen_backup_codes_desc' => 'Păstrează lista de coduri de mai jos într-un loc sigur. Când accesezi sistemul, vei putea folosi unul dintre coduri ca un al doilea mecanism de autentificare.',\n    'mfa_gen_backup_codes_download' => 'Descarcă codurile',\n    'mfa_gen_backup_codes_usage_warning' => 'Fiecare cod poate fi folosit o singură dată',\n    'mfa_gen_totp_title' => 'Configurare aplicație mobilă',\n    'mfa_gen_totp_desc' => 'Pentru a utiliza autentificarea cu mai mulți factori, vei avea nevoie de o aplicație mobilă care acceptă TOTP, cum ar fi Google Authenticator, Authy sau Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Scanează codul QR de mai jos folosind aplicația de autentificare preferată pentru a începe.',\n    'mfa_gen_totp_verify_setup' => 'Verifică configurarea',\n    'mfa_gen_totp_verify_setup_desc' => 'Verifică dacă totul funcționează introducând un cod, generat în aplicația de autentificare, în caseta de introducere de mai jos:',\n    'mfa_gen_totp_provide_code_here' => 'Furnizează aici codul generat de aplicație',\n    'mfa_verify_access' => 'Verifică accesul',\n    'mfa_verify_access_desc' => 'Contul tău de utilizator necesită să îți confirmi identitatea printr-un nivel suplimentar de verificare înainte de acordare acces. Verifică folosind una dintre metodele configurate pentru a continua.',\n    'mfa_verify_no_methods' => 'Nicio metodă configurată',\n    'mfa_verify_no_methods_desc' => 'Nu s-au găsit metode de autentificare cu mai mulți factori pentru contul tău. Va trebui să configurezi cel puțin o metodă înainte de a obține acces.',\n    'mfa_verify_use_totp' => 'Verifică folosind o aplicație mobilă',\n    'mfa_verify_use_backup_codes' => 'Verifică folosind un cod de rezervă',\n    'mfa_verify_backup_code' => 'Cod de rezervă',\n    'mfa_verify_backup_code_desc' => 'Introdu unul dintre codurile de rezervă rămase mai jos:',\n    'mfa_verify_backup_code_enter_here' => 'Introdu codul de rezervă aici',\n    'mfa_verify_totp_desc' => 'Introdu codul, generat folosind aplicația mobilă, mai jos:',\n    'mfa_setup_login_notification' => 'Metodă multifactorială configurată. Te rog să te conectezi din nou folosind metoda configurată.',\n];\n"
  },
  {
    "path": "lang/ro/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Anulează',\n    'close' => 'Închide',\n    'confirm' => 'Confirmă',\n    'back' => 'Înapoi',\n    'save' => 'Salvează',\n    'continue' => 'Continuă',\n    'select' => 'Selecteză',\n    'toggle_all' => 'Comutați toate',\n    'more' => 'Mai mult',\n\n    // Form Labels\n    'name' => 'Nume',\n    'description' => 'Descriere',\n    'role' => 'Rol',\n    'cover_image' => 'Imagine copertă',\n    'cover_image_description' => 'This image should be approximately 440x250px although it will be flexibly scaled & cropped to fit the user interface in different scenarios as required, so actual dimensions for display will differ.',\n\n    // Actions\n    'actions' => 'Acțiuni',\n    'view' => 'Vizualizare',\n    'view_all' => 'Vizualizează tot',\n    'new' => 'Noutăți',\n    'create' => 'Crează',\n    'update' => 'Actualzează',\n    'edit' => 'Editează',\n    'archive' => 'Archive',\n    'unarchive' => 'Un-Archive',\n    'sort' => 'Sortează',\n    'move' => 'Mută',\n    'copy' => 'Copiază',\n    'reply' => 'Răspunde',\n    'delete' => 'Șterge',\n    'delete_confirm' => 'Confirmă ștergerea',\n    'search' => 'Caută',\n    'search_clear' => 'Șterge căutarea',\n    'reset' => 'Resetează',\n    'remove' => 'Elimină',\n    'add' => 'Adaugă',\n    'configure' => 'Configurează',\n    'manage' => 'Gestionează',\n    'fullscreen' => 'Ecran complet',\n    'favourite' => 'Adaugă la favorite',\n    'unfavourite' => 'Șterge de la favorite',\n    'next' => 'Următorul',\n    'previous' => 'Anterior',\n    'filter_active' => 'Filtru activ:',\n    'filter_clear' => 'Șterge filtru',\n    'download' => 'Descarcă',\n    'open_in_tab' => 'Deschide in tab',\n    'open' => 'Open',\n\n    // Sort Options\n    'sort_options' => 'Opțiuni ordonare',\n    'sort_direction_toggle' => 'Comutare direcție sortare',\n    'sort_ascending' => 'Ordonează crescător',\n    'sort_descending' => 'Ordonează descrescător',\n    'sort_name' => 'Nume',\n    'sort_default' => 'Implicit',\n    'sort_created_at' => 'Data creării',\n    'sort_updated_at' => 'Data actualizării',\n\n    // Misc\n    'deleted_user' => 'Utilizator șters',\n    'no_activity' => 'Nicio activitate de afișat',\n    'no_items' => 'Niciun articol disponibil',\n    'back_to_top' => 'Înapoi sus',\n    'skip_to_main_content' => 'Treci la conținutul principal',\n    'toggle_details' => 'Comută detaliile',\n    'toggle_thumbnails' => 'Comută miniaturi',\n    'details' => 'Detalii',\n    'grid_view' => 'Vizualizare grilă',\n    'list_view' => 'Vizualizare listă',\n    'default' => 'Implicit',\n    'breadcrumb' => 'Navigare',\n    'status' => 'Status',\n    'status_active' => 'Activ',\n    'status_inactive' => 'Inactiv',\n    'never' => 'Niciodată',\n    'none' => 'Niciunul',\n\n    // Header\n    'homepage' => 'Acasă',\n    'header_menu_expand' => 'Extindere meniu antet',\n    'profile_menu' => 'Meniu profil',\n    'view_profile' => 'Vezi profil',\n    'edit_profile' => 'Editare profil',\n    'dark_mode' => 'Mod întunecat',\n    'light_mode' => 'Mod luminos',\n    'global_search' => 'Căutare Globală',\n\n    // Layout tabs\n    'tab_info' => 'Informații',\n    'tab_info_label' => 'Tab: Arată informații secundare',\n    'tab_content' => 'Conținut',\n    'tab_content_label' => 'Tab: Arată conţinutul primar',\n\n    // Email Content\n    'email_action_help' => 'Dacă întâmpini probleme la apăsarea butonului \":actionText\", copiază și inserează URL-ul de mai jos în browser-ul web:',\n    'email_rights' => 'Toate drepturile rezervate',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Politică de confidențialitate',\n    'terms_of_service' => 'Termeni și condiții',\n\n    // OpenSearch\n    'opensearch_description' => 'Search :appName',\n];\n"
  },
  {
    "path": "lang/ro/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Selectează imaginea',\n    'image_list' => 'Listă imagine',\n    'image_details' => 'Detalii imagine',\n    'image_upload' => 'Încarcă imaginea',\n    'image_intro' => 'Aici puteţi selecta şi gestiona imaginile care au fost încărcate anterior în sistem.',\n    'image_intro_upload' => 'Încărcați o imagine nouă trăgând o imagine în această fereastră sau utilizând butonul \"Încărcați Imaginea\" de mai sus.',\n    'image_all' => 'Tot',\n    'image_all_title' => 'Vezi toate imaginile',\n    'image_book_title' => 'Vezi imaginile încărcate în această carte',\n    'image_page_title' => 'Vezi imaginile încărcate în această pagină',\n    'image_search_hint' => 'Caută după numele imaginii',\n    'image_uploaded' => 'Încărcat la :uploadedDate',\n    'image_uploaded_by' => 'Încărcată de :userName',\n    'image_uploaded_to' => 'Încărcat în :pageLink',\n    'image_updated' => 'Actualizat :updateDate',\n    'image_load_more' => 'Încarcă mai mult',\n    'image_image_name' => 'Nume imagine',\n    'image_delete_used' => 'Această imagine este folosită în paginile de mai jos.',\n    'image_delete_confirm_text' => 'Ești sigur că vrei să ștergi această imagine?',\n    'image_select_image' => 'Selectează imaginea',\n    'image_dropzone' => 'Trage imaginile sau apasă aici pentru a le încărca',\n    'image_dropzone_drop' => 'Trageți fișierele aici pentru a le încărca',\n    'images_deleted' => 'Imagini șterse',\n    'image_preview' => 'Previzualizare imagine',\n    'image_upload_success' => 'Imaginea a fost încărcată cu succes',\n    'image_update_success' => 'Detalii imagine actualizate cu succes',\n    'image_delete_success' => 'Imaginea a fost ștearsă',\n    'image_replace' => 'Înlocuiți imaginea',\n    'image_replace_success' => 'Imaginea a fost actualizată',\n    'image_rebuild_thumbs' => 'Regenerate Size Variations',\n    'image_rebuild_thumbs_success' => 'Image size variations successfully rebuilt!',\n\n    // Code Editor\n    'code_editor' => 'Editare cod',\n    'code_language' => 'Limba codului',\n    'code_content' => 'Conținut cod',\n    'code_session_history' => 'Istoric sesiune',\n    'code_save' => 'Salvează cod',\n];\n"
  },
  {
    "path": "lang/ro/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'General',\n    'advanced' => 'Avansat',\n    'none' => 'Niciunul',\n    'cancel' => 'Anulează',\n    'save' => 'Salvează',\n    'close' => 'Închide',\n    'apply' => 'Apply',\n    'undo' => 'Anulează',\n    'redo' => 'Refă',\n    'left' => 'Stânga',\n    'center' => 'Centru',\n    'right' => 'Dreapta',\n    'top' => 'Sus',\n    'middle' => 'Mijloc',\n    'bottom' => 'Jos',\n    'width' => 'Lățime',\n    'height' => 'Înălțime',\n    'More' => 'Mai mult',\n    'select' => 'Selectează...',\n\n    // Toolbar\n    'formats' => 'Formate',\n    'header_large' => 'Antet mare',\n    'header_medium' => 'Antet mediu',\n    'header_small' => 'Antet mic',\n    'header_tiny' => 'Antet minuscul',\n    'paragraph' => 'Paragraf',\n    'blockquote' => 'Citat',\n    'inline_code' => 'Cod inline',\n    'callouts' => 'Indicații',\n    'callout_information' => 'Informație',\n    'callout_success' => 'Succes',\n    'callout_warning' => 'Avertisment',\n    'callout_danger' => 'Pericol',\n    'bold' => 'Îngroșat',\n    'italic' => 'Italic',\n    'underline' => 'Subliniat',\n    'strikethrough' => 'Tăiat cu o linie',\n    'superscript' => 'Scris sus',\n    'subscript' => 'Scris jos',\n    'text_color' => 'Culoare text',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Culoare personalizată',\n    'remove_color' => 'Elimină culoarea',\n    'background_color' => 'Culoare fundal',\n    'align_left' => 'Aliniere la stânga',\n    'align_center' => 'Aliniere în centru',\n    'align_right' => 'Aliniere la dreapta',\n    'align_justify' => 'Margini egale',\n    'list_bullet' => 'Listă cu puncte',\n    'list_numbered' => 'Listă numerotată',\n    'list_task' => 'Listă de sarcini',\n    'indent_increase' => 'Crește indentarea',\n    'indent_decrease' => 'Scade indentarea',\n    'table' => 'Tabel',\n    'insert_image' => 'Inserare imagine',\n    'insert_image_title' => 'Inserare/Editare Imagine',\n    'insert_link' => 'Inserare/editare link',\n    'insert_link_title' => 'Inserare/Editare link',\n    'insert_horizontal_line' => 'Inserează linie orizontală',\n    'insert_code_block' => 'Inserează bloc de cod',\n    'edit_code_block' => 'Editează blocul de cod',\n    'insert_drawing' => 'Inserare/editare desen',\n    'drawing_manager' => 'Manager de desene',\n    'insert_media' => 'Inserare/editare media',\n    'insert_media_title' => 'Inserare/Editare media',\n    'clear_formatting' => 'Șterge formatarea',\n    'source_code' => 'Cod sursă',\n    'source_code_title' => 'Cod sursă',\n    'fullscreen' => 'Ecran complet',\n    'image_options' => 'Opțiuni imagine',\n\n    // Tables\n    'table_properties' => 'Proprietăți tabel',\n    'table_properties_title' => 'Proprietăți tabel',\n    'delete_table' => 'Șterge tabel',\n    'table_clear_formatting' => 'Clear table formatting',\n    'resize_to_contents' => 'Resize to contents',\n    'row_header' => 'Row header',\n    'insert_row_before' => 'Inserează rând după',\n    'insert_row_after' => 'Inserează rând înainte',\n    'delete_row' => 'Șterge rând',\n    'insert_column_before' => 'Inserare coloană înainte',\n    'insert_column_after' => 'Inserare coloană după',\n    'delete_column' => 'Șterge coloana',\n    'table_cell' => 'Celulă',\n    'table_row' => 'Rând',\n    'table_column' => 'Coloană',\n    'cell_properties' => 'Proprietăți celulă',\n    'cell_properties_title' => 'Proprietăți celulă',\n    'cell_type' => 'Tip celulă',\n    'cell_type_cell' => 'Celulă',\n    'cell_scope' => 'Scop',\n    'cell_type_header' => 'Celulă antet',\n    'merge_cells' => 'Îmbină celule',\n    'split_cell' => 'Scindare celulă',\n    'table_row_group' => 'Grup rând',\n    'table_column_group' => 'Grup Coloană',\n    'horizontal_align' => 'Aliniere orizontală',\n    'vertical_align' => 'Aliniere verticală',\n    'border_width' => 'Lățime margine',\n    'border_style' => 'Stil margine',\n    'border_color' => 'Culoare margine',\n    'row_properties' => 'Proprietăți rând',\n    'row_properties_title' => 'Proprietăți rând',\n    'cut_row' => 'Taie rândul',\n    'copy_row' => 'Copiază rândul',\n    'paste_row_before' => 'Inserează rând înainte',\n    'paste_row_after' => 'Inserează rând după',\n    'row_type' => 'Tip rând',\n    'row_type_header' => 'Antet',\n    'row_type_body' => 'Corp',\n    'row_type_footer' => 'Subsol',\n    'alignment' => 'Aliniere',\n    'cut_column' => 'Șterge coloana',\n    'copy_column' => 'Copiază coloana',\n    'paste_column_before' => 'Lipiți coloana înainte',\n    'paste_column_after' => 'Lipește coloana după',\n    'cell_padding' => 'Spațiere celulă',\n    'cell_spacing' => 'Spațiere celulară',\n    'caption' => 'Titlu',\n    'show_caption' => 'Afișează legenda',\n    'constrain' => 'Proporții constrângeri',\n    'cell_border_solid' => 'Solid',\n    'cell_border_dotted' => 'Punctat',\n    'cell_border_dashed' => 'Liniuțe',\n    'cell_border_double' => 'Duble',\n    'cell_border_groove' => 'Groove',\n    'cell_border_ridge' => 'Creastă',\n    'cell_border_inset' => 'Inserat',\n    'cell_border_outset' => 'Început',\n    'cell_border_none' => 'Niciunul',\n    'cell_border_hidden' => 'Ascuns',\n\n    // Images, links, details/summary & embed\n    'source' => 'Sursă',\n    'alt_desc' => 'Descriere alternativă',\n    'embed' => 'Încorporează',\n    'paste_embed' => 'Lipește codul încorporat mai jos:',\n    'url' => 'URL',\n    'text_to_display' => 'Text de afișat',\n    'title' => 'Titlu',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Deschide link-ul',\n    'open_link_in' => 'Deschide link-ul în...',\n    'open_link_current' => 'Fereastra curentă',\n    'open_link_new' => 'Fereastră nouă',\n    'remove_link' => 'Elimină link-ul',\n    'insert_collapsible' => 'Inserează bloc colapsabil',\n    'collapsible_unwrap' => 'Desfășoară',\n    'edit_label' => 'Editează eticheta',\n    'toggle_open_closed' => 'Comută deschis/închis',\n    'collapsible_edit' => 'Editează bloc colapsabil',\n    'toggle_label' => 'Comută etichetă',\n\n    // About view\n    'about' => 'Despre editor',\n    'about_title' => 'Despre editorul WYSIWYG',\n    'editor_license' => 'Editor licență și drepturi de autor',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'Acest editor este construit folosind :tinyLink care este furnizat sub licența MIT.',\n    'editor_tiny_license_link' => 'Detaliile privind drepturile de autor şi licența TinyMCE pot fi consultate aici.',\n    'save_continue' => 'Salvează pagina și continuă',\n    'callouts_cycle' => '(Continuă să apăși pentru a comuta prin tipuri)',\n    'link_selector' => 'Link către conținut',\n    'shortcuts' => 'Scurtături',\n    'shortcut' => 'Scurtătură',\n    'shortcuts_intro' => 'Următoarele comenzi rapide sunt disponibile în editor:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Descriere',\n];\n"
  },
  {
    "path": "lang/ro/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Creat recent',\n    'recently_created_pages' => 'Pagini create recent',\n    'recently_updated_pages' => 'Pagini actualizate recent',\n    'recently_created_chapters' => 'Capitole create recent',\n    'recently_created_books' => 'Cărți create recent',\n    'recently_created_shelves' => 'Rafturi create recent',\n    'recently_update' => 'Actualizate recent',\n    'recently_viewed' => 'Vizualizate recent',\n    'recent_activity' => 'Activitate recentă',\n    'create_now' => 'Creează unul acum',\n    'revisions' => 'Revizii',\n    'meta_revision' => 'Revizuirea #:revisionCount',\n    'meta_created' => 'Creat :timeLength',\n    'meta_created_name' => 'Creat de :timeLength de :user',\n    'meta_updated' => 'Actualizat :timeLength',\n    'meta_updated_name' => 'Actualizat :timeLength de :user',\n    'meta_owned_name' => 'Deținut de :user',\n    'meta_reference_count' => 'Referenced by :count item|Referenced by :count items',\n    'entity_select' => 'Selectare entitate',\n    'entity_select_lack_permission' => 'Nu ai drepturile necesare pentru a selecta acest element',\n    'images' => 'Imagini',\n    'my_recent_drafts' => 'Ciornele mele recente',\n    'my_recently_viewed' => 'Vizualizarea mea recentă',\n    'my_most_viewed_favourites' => 'Favoritele mele cele mai vizualizate',\n    'my_favourites' => 'Favoritele Mele',\n    'no_pages_viewed' => 'Nu ai vizualizat nicio pagină',\n    'no_pages_recently_created' => 'Nicio pagină nu a fost creată recent',\n    'no_pages_recently_updated' => 'Nicio pagină nu a fost actualizată recent',\n    'export' => 'Exportă',\n    'export_html' => 'Fișier web inclus',\n    'export_pdf' => 'Fișier PDF',\n    'export_text' => 'Fișier text simplu',\n    'export_md' => 'Fișier Markdown',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Default Page Template',\n    'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',\n    'default_template_select' => 'Select a template page',\n    'import' => 'Import',\n    'import_validate' => 'Validate Import',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'Select ZIP file to upload',\n    'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',\n    'import_pending' => 'Pending Imports',\n    'import_pending_none' => 'No imports have been started.',\n    'import_continue' => 'Continue Import',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Run Import',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Uploaded by',\n    'import_location' => 'Import Location',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'Import Errors',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Permisiuni',\n    'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.',\n    'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.',\n    'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.',\n    'permissions_save' => 'Salvează permisiuni',\n    'permissions_owner' => 'Proprietar',\n    'permissions_role_everyone_else' => 'Toți ceilalți',\n    'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',\n    'permissions_role_override' => 'Override permissions for role',\n    'permissions_inherit_defaults' => 'Moștenește valorile implicite',\n\n    // Search\n    'search_results' => 'Rezultatele căutării',\n    'search_total_results_found' => ':count rezultat găsit|:count rezultate totale găsite',\n    'search_clear' => 'Șterge căutarea',\n    'search_no_pages' => 'Nicio pagină nu se potrivește cu această căutare',\n    'search_for_term' => 'Caută după :term',\n    'search_more' => 'Mai multe rezultate',\n    'search_advanced' => 'Căutare avansată',\n    'search_terms' => 'Termeni de căutare',\n    'search_content_type' => 'Tip conținut',\n    'search_exact_matches' => 'Potriviri exacte',\n    'search_tags' => 'Căutări cu etichete',\n    'search_options' => 'Opţiuni',\n    'search_viewed_by_me' => 'Vizualizat de mine',\n    'search_not_viewed_by_me' => 'Nevizualizate de mine',\n    'search_permissions_set' => 'Permisiuni setate',\n    'search_created_by_me' => 'Creat de mine',\n    'search_updated_by_me' => 'Actualizat de mine',\n    'search_owned_by_me' => 'Deținut de mine',\n    'search_date_options' => 'Opțiuni dată',\n    'search_updated_before' => 'Actualizat înainte de',\n    'search_updated_after' => 'Actualizat după',\n    'search_created_before' => 'Creat înainte de',\n    'search_created_after' => 'Creat după',\n    'search_set_date' => 'Setează data',\n    'search_update' => 'Actualizează căutarea',\n\n    // Shelves\n    'shelf' => 'Raft',\n    'shelves' => 'Rafturi',\n    'x_shelves' => ':count Raft|:count Rafturi',\n    'shelves_empty' => 'Nu a fost creat niciun raft',\n    'shelves_create' => 'Creează raft nou',\n    'shelves_popular' => 'Rafturi populare',\n    'shelves_new' => 'Rafturi noi',\n    'shelves_new_action' => 'Raft nou',\n    'shelves_popular_empty' => 'Cele mai populare rafturi vor apărea aici.',\n    'shelves_new_empty' => 'Cele mai recente rafturi vor apărea aici.',\n    'shelves_save' => 'Salvează raft',\n    'shelves_books' => 'Cărți pe acest raft',\n    'shelves_add_books' => 'Adaugă cărți pe acest raft',\n    'shelves_drag_books' => 'Trage cărțile mai jos pentru a le adăuga pe acest raft',\n    'shelves_empty_contents' => 'Acest raft nu are cărți atribuite lui',\n    'shelves_edit_and_assign' => 'Editare raft pentru a atribui cărți',\n    'shelves_edit_named' => 'Editează raft :name',\n    'shelves_edit' => 'Editați raftul',\n    'shelves_delete' => 'Ștergeți raft',\n    'shelves_delete_named' => 'Șterge raftul :name',\n    'shelves_delete_explain' => \"This will delete the shelf with the name ':name'. Contained books will not be deleted.\",\n    'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',\n    'shelves_permissions' => 'Permisiuni raft',\n    'shelves_permissions_updated' => 'Permisiunile raftului au fost actualizate',\n    'shelves_permissions_active' => 'Permisiuni raft active',\n    'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',\n    'shelves_permissions_create' => 'Shelf create permissions are only used for copying permissions to child books using the action below. They do not control the ability to create books.',\n    'shelves_copy_permissions_to_books' => 'Copiază permisiunile către cărți',\n    'shelves_copy_permissions' => 'Copiază permisiunile',\n    'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',\n    'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',\n\n    // Books\n    'book' => 'Carte',\n    'books' => 'Cărți',\n    'x_books' => ':count Carte|:count Cărți',\n    'books_empty' => 'Nicio carte nu a fost creată',\n    'books_popular' => 'Cărți populare',\n    'books_recent' => 'Cărți recente',\n    'books_new' => 'Cărți noi',\n    'books_new_action' => 'Carte nouă',\n    'books_popular_empty' => 'Cele mai populare cărți vor apărea aici.',\n    'books_new_empty' => 'Cele mai recente cărți create vor apărea aici.',\n    'books_create' => 'Creează o carte nouă',\n    'books_delete' => 'Șterge carte',\n    'books_delete_named' => 'Șterge cartea :bookName',\n    'books_delete_explain' => 'Aceasta operațiune va șterge cartea cu numele \\':bookName\\'. Toate paginile și capitolele vor fi eliminate.',\n    'books_delete_confirmation' => 'Ești sigur că vrei să ștergi această carte?',\n    'books_edit' => 'Editează carte',\n    'books_edit_named' => 'Editează cartea :bookName',\n    'books_form_book_name' => 'Nume carte',\n    'books_save' => 'Salvează cartea',\n    'books_permissions' => 'Permisiuni carte',\n    'books_permissions_updated' => 'Permisiuni carte actualizate',\n    'books_empty_contents' => 'Nu au fost create pagini sau capitole pentru această carte.',\n    'books_empty_create_page' => 'Creează pagină nouă',\n    'books_empty_sort_current_book' => 'Sortează cartea curentă',\n    'books_empty_add_chapter' => 'Adaugă un capitol',\n    'books_permissions_active' => 'Permisiuni carte active',\n    'books_search_this' => 'Caută în această carte',\n    'books_navigation' => 'Navigare carte',\n    'books_sort' => 'Sortează conținutul cărții',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Sortează cartea :bookName',\n    'books_sort_name' => 'Sortează după nume',\n    'books_sort_created' => 'Sortează după data creării',\n    'books_sort_updated' => 'Sortează după data actualizării',\n    'books_sort_chapters_first' => 'Capitole mai întâi',\n    'books_sort_chapters_last' => 'Capitole la final',\n    'books_sort_show_other' => 'Arată alte cărți',\n    'books_sort_save' => 'Salvează noua ordine',\n    'books_sort_show_other_desc' => 'Adăugaţi alte cărţi aici pentru a le include în operaţia de sortare şi permiteţi o reorganizare uşoară.',\n    'books_sort_move_up' => 'Mută în sus',\n    'books_sort_move_down' => 'Mută în jos',\n    'books_sort_move_prev_book' => 'Mutare în cartea anterioară',\n    'books_sort_move_next_book' => 'Mută în următoarea carte',\n    'books_sort_move_prev_chapter' => 'Mutați în capitolul anterior',\n    'books_sort_move_next_chapter' => 'Mergi la următorul capitol',\n    'books_sort_move_book_start' => 'Sari la începutul cârtii',\n    'books_sort_move_book_end' => 'Sari la sfârșitul cârtii',\n    'books_sort_move_before_chapter' => 'Move to Before Chapter',\n    'books_sort_move_after_chapter' => 'Move to After Chapter',\n    'books_copy' => 'Copiază cartea',\n    'books_copy_success' => 'Carte copiată cu succes',\n\n    // Chapters\n    'chapter' => 'Capitol',\n    'chapters' => 'Capitole',\n    'x_chapters' => ':count Capitol|:count Capitole',\n    'chapters_popular' => 'Capitole populare',\n    'chapters_new' => 'Capitol nou',\n    'chapters_create' => 'Creează un nou capitol',\n    'chapters_delete' => 'Șterge capitol',\n    'chapters_delete_named' => 'Şterge capitolul :chapterName',\n    'chapters_delete_explain' => 'Se va șterge capitolul cu numele \\':chapterName\\'. Toate paginile care există în acest capitol vor fi, de asemenea, șterse.',\n    'chapters_delete_confirm' => 'Ești sigur că vrei să ștergi acest capitol?',\n    'chapters_edit' => 'Editează capitol',\n    'chapters_edit_named' => 'Editați capitolul :chapterName',\n    'chapters_save' => 'Salvează capitolul',\n    'chapters_move' => 'Mută capitolul',\n    'chapters_move_named' => 'Mutați capitolul :chapterName',\n    'chapters_copy' => 'Copiază capitolul',\n    'chapters_copy_success' => 'Capitolul a fost copiat',\n    'chapters_permissions' => 'Permisiuni capitol',\n    'chapters_empty' => 'În prezent nu există pagini în acest capitol.',\n    'chapters_permissions_active' => 'Permisiuni capitol active',\n    'chapters_permissions_success' => 'Permisiuni capitol actualizate',\n    'chapters_search_this' => 'Caută în acest capitol',\n    'chapter_sort_book' => 'Ordonare carte',\n\n    // Pages\n    'page' => 'Pagină',\n    'pages' => 'Pagini',\n    'x_pages' => ':count Pagină|:count Pagini',\n    'pages_popular' => 'Pagini populare',\n    'pages_new' => 'Pagină nouă',\n    'pages_attachments' => 'Atașamente',\n    'pages_navigation' => 'Navigație pagină',\n    'pages_delete' => 'Șterge pagină',\n    'pages_delete_named' => 'Șterge pagina :pageNume',\n    'pages_delete_draft_named' => 'Șterge schița paginii :pageNume',\n    'pages_delete_draft' => 'Șterge ciorna',\n    'pages_delete_success' => 'Pagină ștearsă',\n    'pages_delete_draft_success' => 'Pagină ciornă ștearsă',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'Ești sigur că dorești să ștergi acestă pagină?',\n    'pages_delete_draft_confirm' => 'Ești sigur că vrei să ștergi această pagină schiță?',\n    'pages_editing_named' => 'Editare pagină :pageNume',\n    'pages_edit_draft_options' => 'Opțiuni ciornă',\n    'pages_edit_save_draft' => 'Salvare ciornă',\n    'pages_edit_draft' => 'Editare ciornă pagină',\n    'pages_editing_draft' => 'Editare ciornă',\n    'pages_editing_page' => 'Editare pagină',\n    'pages_edit_draft_save_at' => 'Ciornă salvată la ',\n    'pages_edit_delete_draft' => 'Șterge ciorna',\n    'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.',\n    'pages_edit_discard_draft' => 'Renunță la ciornă',\n    'pages_edit_switch_to_markdown' => 'Comută la editorul Markdown',\n    'pages_edit_switch_to_markdown_clean' => '(Curăță conținut)',\n    'pages_edit_switch_to_markdown_stable' => '(Conținut stabil)',\n    'pages_edit_switch_to_wysiwyg' => 'Comută la editorul WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Setare jurnal modificări',\n    'pages_edit_enter_changelog_desc' => 'Adaugă o scurtă descriere a modificărilor făcute',\n    'pages_edit_enter_changelog' => 'Intră în jurnalul de modificări',\n    'pages_editor_switch_title' => 'Schimbă editorul',\n    'pages_editor_switch_are_you_sure' => 'Ești sigur că vrei să schimbi editorul pentru această pagină?',\n    'pages_editor_switch_consider_following' => 'Ia în considerare următoarele la schimbarea editorilor:',\n    'pages_editor_switch_consideration_a' => 'Odată salvată, noua opțiune de editor va fi utilizată de orice editori viitori, inclusiv de cei care nu pot schimba însuși tipul de editor.',\n    'pages_editor_switch_consideration_b' => 'Acest lucru poate duce la pierderea detaliilor și a sintaxei în anumite circumstanțe.',\n    'pages_editor_switch_consideration_c' => 'Schimbări de etichete sau schimbări de jurnal de modificări, făcute de la ultima salvare, nu vor persista în această schimbare.',\n    'pages_save' => 'Salvează pagina',\n    'pages_title' => 'Titlu pagină',\n    'pages_name' => 'Nume pagină',\n    'pages_md_editor' => 'Editor',\n    'pages_md_preview' => 'Previzualizare',\n    'pages_md_insert_image' => 'Inserare imagine',\n    'pages_md_insert_link' => 'Inserează link-ul entității',\n    'pages_md_insert_drawing' => 'Inserează desen',\n    'pages_md_show_preview' => 'Arată previzualizarea',\n    'pages_md_sync_scroll' => 'Sync preview scroll',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Unsaved Drawing Found',\n    'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',\n    'pages_not_in_chapter' => 'Pagina nu este într-un capitol',\n    'pages_move' => 'Mută pagina',\n    'pages_copy' => 'Copiază pagina',\n    'pages_copy_desination' => 'Destinație copiere',\n    'pages_copy_success' => 'Pagină copiată cu succes',\n    'pages_permissions' => 'Permisiunile paginii',\n    'pages_permissions_success' => 'Permisiuni pagină actualizate',\n    'pages_revision' => 'Revizuire',\n    'pages_revisions' => 'Revizuiri pagină',\n    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',\n    'pages_revisions_named' => 'Revizuiri pagină pentru :pageName',\n    'pages_revision_named' => 'Revizuire pagină pentru :pageName',\n    'pages_revision_restored_from' => 'Restaurat de la #:id; :summary',\n    'pages_revisions_created_by' => 'Creat de',\n    'pages_revisions_date' => 'Data revizuirii',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Numărul reviziei',\n    'pages_revisions_numbered' => 'Revizuire #:id',\n    'pages_revisions_numbered_changes' => 'Revizuirea #:id Modificări',\n    'pages_revisions_editor' => 'Tip editor',\n    'pages_revisions_changelog' => 'Jurnal de modificări',\n    'pages_revisions_changes' => 'Modificări',\n    'pages_revisions_current' => 'Vrsiunea curentă',\n    'pages_revisions_preview' => 'Previzualizare',\n    'pages_revisions_restore' => 'Restaurare',\n    'pages_revisions_none' => 'Această pagină nu are revizuiri',\n    'pages_copy_link' => 'Copiază link',\n    'pages_edit_content_link' => 'Jump to section in editor',\n    'pages_pointer_enter_mode' => 'Enter section select mode',\n    'pages_pointer_label' => 'Page Section Options',\n    'pages_pointer_permalink' => 'Page Section Permalink',\n    'pages_pointer_include_tag' => 'Page Section Include Tag',\n    'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag',\n    'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink',\n    'pages_permissions_active' => 'Permisiuni carte active',\n    'pages_initial_revision' => 'Publicare inițiala',\n    'pages_references_update_revision' => 'System auto-update of internal links',\n    'pages_initial_name' => 'Pagină nouă',\n    'pages_editing_draft_notification' => 'Momentan editezi o schiță care a fost salvată ultima dată :timeDiff.',\n    'pages_draft_edited_notification' => 'Această pagină a fost actualizată de atunci. Este recomandat să aruncați această ciornă.',\n    'pages_draft_page_changed_since_creation' => 'Această pagină a fost actualizată de la crearea acestei ciorne. Este recomandat să renunțați la această schiță sau să aveți grijă să nu suprascrieți modificările paginii.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count utilizatori au început să editeze această pagină',\n        'start_b' => ':userName a început să editeze această pagină',\n        'time_a' => 'de când pagina a fost actualizată ultima dată',\n        'time_b' => 'în ultimele :minCount minute',\n        'message' => ':start :time. Aveți grijă să nu vă suprascrieți reciproc actualizările!',\n    ],\n    'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',\n    'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',\n    'pages_specific' => 'Pagina specifică',\n    'pages_is_template' => 'Şablon pagină',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Comutați bara laterală',\n    'page_tags' => 'Etichete pagină',\n    'chapter_tags' => 'Etichete capitol',\n    'book_tags' => 'Etichete carte',\n    'shelf_tags' => 'Etichete raft',\n    'tag' => 'Etichetă',\n    'tags' =>  'Etichete',\n    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',\n    'tag_name' =>  'Nume etichetă',\n    'tag_value' => 'Valoare etichetă (opțional)',\n    'tags_explain' => \"Adăugați unele etichete pentru a clasifica mai bine conținutul. \\n Puteți atribui o valoare unei etichete pentru o organizare mai aprofundată.\",\n    'tags_add' => 'Adaugă o altă etichetă',\n    'tags_remove' => 'Elimină această etichetă',\n    'tags_usages' => 'Total utilizări etichetă',\n    'tags_assigned_pages' => 'Atribuit paginilor',\n    'tags_assigned_chapters' => 'Atribuit capitolelor',\n    'tags_assigned_books' => 'Atribuit cărților',\n    'tags_assigned_shelves' => 'Atribuit rafturilor',\n    'tags_x_unique_values' => ':count valori unice',\n    'tags_all_values' => 'Toate valorile',\n    'tags_view_tags' => 'Vezi etichete',\n    'tags_view_existing_tags' => 'Vezi etichetele existente',\n    'tags_list_empty_hint' => 'Etichetele pot fi atribuite prin bara laterală a editorului de pagini sau în timpul editării detaliilor unei cărți, capitole sau raft.',\n    'attachments' => 'Atașamente',\n    'attachments_explain' => 'Încarcă unele fișiere sau atașează unele link-uri pentru a fi afișate pe pagina ta. Acestea sunt vizibile în bara laterală a paginii.',\n    'attachments_explain_instant_save' => 'Modificările de aici sunt salvate instant.',\n    'attachments_upload' => 'Încarcă fișier',\n    'attachments_link' => 'Atașare link',\n    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',\n    'attachments_set_link' => 'Setează link',\n    'attachments_delete' => 'Ești sigur că dorești să ștergi acest atașament?',\n    'attachments_dropzone' => 'Trageți fișierele aici pentru a le încărca',\n    'attachments_no_files' => 'Niciun fișier nu a fost încărcat',\n    'attachments_explain_link' => 'Poți atașa un link dacă ai prefera să nu încarci un fișier. Acesta poate fi un link către o altă pagină sau un link către un fișier în cloud.',\n    'attachments_link_name' => 'Nume link',\n    'attachment_link' => 'Link atașament',\n    'attachments_link_url' => 'Link către fișier',\n    'attachments_link_url_hint' => 'Url site sau fișier',\n    'attach' => 'Atașează',\n    'attachments_insert_link' => 'Adăugare link atașament la pagină',\n    'attachments_edit_file' => 'Editare fișier',\n    'attachments_edit_file_name' => 'Nume fișier',\n    'attachments_edit_drop_upload' => 'Trage fișierele aici sau apasă aici pentru a încărca și suprascrie',\n    'attachments_order_updated' => 'Ordine atașament actualizată',\n    'attachments_updated_success' => 'Detalii atașament actualizate',\n    'attachments_deleted' => 'Atașament șters',\n    'attachments_file_uploaded' => 'Fișier încărcat cu succes',\n    'attachments_file_updated' => 'Fișier actualizat cu succes',\n    'attachments_link_attached' => 'Link atașat cu succes la pagină',\n    'templates' => 'Șabloane',\n    'templates_set_as_template' => 'Pagina este un șablon',\n    'templates_explain_set_as_template' => 'Poți seta această pagină ca șablon astfel încât conținutul său să fie utilizat la crearea altor pagini. Alți utilizatori vor putea utiliza acest șablon dacă au permisiuni de vizualizare pentru această pagină.',\n    'templates_replace_content' => 'Înlocuiește conținutul paginii',\n    'templates_append_content' => 'Adaugă la conținutul paginii',\n    'templates_prepend_content' => 'Adaugă la începutul conținutului paginii',\n\n    // Profile View\n    'profile_user_for_x' => 'Utilizator pentru :time',\n    'profile_created_content' => 'Conținut creat',\n    'profile_not_created_pages' => ':userName nu a creat nicio pagină',\n    'profile_not_created_chapters' => ':userName nu a creat niciun capitol',\n    'profile_not_created_books' => ':userName nu a creat nicio carte',\n    'profile_not_created_shelves' => ':userName nu a creat niciun raft',\n\n    // Comments\n    'comment' => 'Comentariu',\n    'comments' => 'Comentarii',\n    'comment_add' => 'Adaugă comentariu',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Lasă un comentariu aici',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Salvează comentariul',\n    'comment_new' => 'Comentariu nou',\n    'comment_created' => 'comentat :createDiff',\n    'comment_updated' => 'Actualizat :updateDiff de :username',\n    'comment_updated_indicator' => 'Actualizat',\n    'comment_deleted_success' => 'Comentariu șters',\n    'comment_created_success' => 'Comentariu adăugat',\n    'comment_updated_success' => 'Comentariu actualizat',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Ești sigur că vrei să ștergi acest comentariu?',\n    'comment_in_reply_to' => 'Ca răspuns la :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',\n\n    // Revision\n    'revision_delete_confirm' => 'Ești sigur că vrei să ștergi această revizuire?',\n    'revision_restore_confirm' => 'Ești sigur că vei să restaurezi această revizuire? Conținutul paginii curente va fi înlocuit.',\n    'revision_cannot_delete_latest' => 'Nu se poate șterge ultima revizuire.',\n\n    // Copy view\n    'copy_consider' => 'Te rugăm să ie în considerare cele de mai jos atunci când copiezi conținut.',\n    'copy_consider_permissions' => 'Setările de permisiuni personalizate nu vor fi copiate.',\n    'copy_consider_owner' => 'Vei deveni proprietarul întregului conținut copiat.',\n    'copy_consider_images' => 'Fișierele imagine ale paginii nu vor fi duplicate, iar imaginile originale își vor păstra relația cu pagina în care au fost încărcate inițial.',\n    'copy_consider_attachments' => 'Atașamentele paginii nu vor fi copiate.',\n    'copy_consider_access' => 'O schimbare a locației, a proprietarului sau a permisiunilor poate duce la accesul acestui conținut pentru cei care nu aveau acces anterior.',\n\n    // Conversions\n    'convert_to_shelf' => 'Convertește în raft',\n    'convert_to_shelf_contents_desc' => 'Poți converti această carte într-un raft nou cu același conținut. Capitolele cuprinse în această carte vor fi convertite în cărți noi. Dacă această carte conține pagini, care nu sunt într-un capitol, această carte va fi redenumită și conține astfel de pagini, iar această carte va deveni parte a noului raft.',\n    'convert_to_shelf_permissions_desc' => 'Orice permisiuni stabilite în această carte vor fi copiate pe noul raft și la toate noile cărți copii care nu au drepturi impuse proprii. Țineți cont că permisiunile de pe rafturi nu se aplică în cascadă pentru conținut cuprins în ele, așa cum se întâmplă pentru cărți.',\n    'convert_book' => 'Convertește cartea',\n    'convert_book_confirm' => 'Ești sigur că vrei să convertești această carte?',\n    'convert_undo_warning' => 'Acest lucru nu poate fi anulat la fel de uşor.',\n    'convert_to_book' => 'Convertește în carte',\n    'convert_to_book_desc' => 'Poți converti acest capitol într-o nouă carte cu același conținut. Orice permisiuni stabilite pe acest capitol vor fi copiate în noua carte, dar orice permisiuni moștenite, din cartea părinte nu vor fi copiate, ceea ce ar putea duce la o schimbare a controlului de acces.',\n    'convert_chapter' => 'Convertește capitolul',\n    'convert_chapter_confirm' => 'Ești sigur că dorești să convertești acest capitol?',\n\n    // References\n    'references' => 'Referințe',\n    'references_none' => 'There are no tracked references to this item.',\n    'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',\n\n    // Watch Options\n    'watch' => 'Watch',\n    'watch_title_default' => 'Default Preferences',\n    'watch_desc_default' => 'Revert watching to just your default notification preferences.',\n    'watch_title_ignore' => 'Ignoră',\n    'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.',\n    'watch_title_new' => 'Pagina Nouă',\n    'watch_desc_new' => 'Notifică atunci când orice pagină nouă este creată în cadrul acestui element.',\n    'watch_title_updates' => 'Toate actualizările paginii',\n    'watch_desc_updates' => 'Notifică atunci când o pagină este editată sau creată.',\n    'watch_desc_updates_page' => 'Notifică la toate modificările paginii.',\n    'watch_title_comments' => 'Toate actualizările și comentariile paginii',\n    'watch_desc_comments' => 'Notifică la toate paginile noi, editările de pagină și comentariile noi.',\n    'watch_desc_comments_page' => 'Notifică la toate paginile noi, editările de pagină și comentariile noi.',\n    'watch_change_default' => 'Schimbă preferințele implicite de notificare',\n    'watch_detail_ignore' => 'Se ignoră notificările',\n    'watch_detail_new' => 'Urmărire pagini noi',\n    'watch_detail_updates' => 'Urmărire pagini noi şi actualizări',\n    'watch_detail_comments' => 'Urmărire pagini noi, actualizări și comentarii',\n    'watch_detail_parent_book' => 'Se uită prin cartea părinte',\n    'watch_detail_parent_book_ignore' => 'Ignorare prin intermediul cărţii părinte',\n    'watch_detail_parent_chapter' => 'Urmărire prin capitolul părinte',\n    'watch_detail_parent_chapter_ignore' => 'Urmărire prin capitolul părinte',\n];\n"
  },
  {
    "path": "lang/ro/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Nu ai permisiunea de a accesa pagina solicitată.',\n    'permissionJson' => 'Nu ai permisiunea de a efectua acțiunea solicitată.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Un utilizator cu adresa de e-mail :email există deja, dar cu acreditări diferite.',\n    'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',\n    'email_already_confirmed' => 'E-mailul a fost deja confirmat, încearcă să te conectezi.',\n    'email_confirmation_invalid' => 'Acest token de confirmare nu este valid sau a fost deja folosit, încercă să te înregistrezi din nou.',\n    'email_confirmation_expired' => 'Token-ul de confirmare a expirat, a fost trimis un nou e-mail de confirmare.',\n    'email_confirmation_awaiting' => 'Adresa de e-mail pentru contul utilizat trebuie să fie confirmată',\n    'ldap_fail_anonymous' => 'Accesul LDAP a eșuat utilizând legătura anonimă',\n    'ldap_fail_authed' => 'Accesul LDAP a eșuat folosind detaliile date dn și parolă',\n    'ldap_extension_not_installed' => 'Extensia LDAP PHP nu este instalată',\n    'ldap_cannot_connect' => 'Nu se poate conecta la serverul ldap, conexiunea inițială a eșuat',\n    'saml_already_logged_in' => 'Deja conectat',\n    'saml_no_email_address' => 'Nu s-a putut găsi o adresă de e-mail, pentru acest utilizator, în datele furnizate de sistemul extern de autentificare',\n    'saml_invalid_response_id' => 'Solicitarea de la sistemul extern de autentificare nu este recunoscută de un proces inițiat de această aplicație. Navigarea înapoi după o autentificare ar putea cauza această problemă.',\n    'saml_fail_authed' => 'Autentificarea folosind :system a eșuat, sistemul nu a furnizat autorizare cu succes',\n    'oidc_already_logged_in' => 'Deja conectat',\n    'oidc_no_email_address' => 'Nu s-a putut găsi o adresă de e-mail, pentru acest utilizator, în datele furnizate de sistemul extern de autentificare',\n    'oidc_fail_authed' => 'Autentificarea folosind :system a eșuat, sistemul nu a furnizat autorizare cu succes',\n    'social_no_action_defined' => 'Nicio acțiune definită',\n    'social_login_bad_response' => \"Eroare primită în timpul autentificării :socialAccount : \\n:error\",\n    'social_account_in_use' => 'Acest cont :socialAccount este deja utilizat, încercați să vă conectați prin intermediul opțiunii :socialCont.',\n    'social_account_email_in_use' => 'E-mailul :email este deja în uz. Dacă ai deja un cont, te poți conecta la contul :socialAccount din setările profilului.',\n    'social_account_existing' => 'Acest :socialAccount este deja atașat la profilul tău.',\n    'social_account_already_used_existing' => 'Acest cont :socialAccount este deja utilizat de un alt utilizator.',\n    'social_account_not_used' => 'Acest cont :socialAccount nu este legat de niciun utilizator. Te rugăm să îl atașezi în setările profilului. ',\n    'social_account_register_instructions' => 'Dacă nu ai încă un cont, poți înregistra un cont utilizând opțiunea :socialCont.',\n    'social_driver_not_found' => 'Driver social negăsit',\n    'social_driver_not_configured' => 'Setările tale sociale :socialAccount nu sunt configurate corect.',\n    'invite_token_expired' => 'Acest link de invitație a expirat. Poți încerca să îți resetezi parola contului.',\n    'login_user_not_found' => 'A user for this action could not be found.',\n\n    // System\n    'path_not_writable' => 'Calea fișierului :filePath nu a putut fi încărcată. Asigurați-vă că poate fi scrisă pe server.',\n    'cannot_get_image_from_url' => 'Nu se poate obține imaginea de la :url',\n    'cannot_create_thumbs' => 'Serverul nu poate crea miniaturi. Verifică dacă este instalată extensia GD PHP.',\n    'server_upload_limit' => 'Serverul nu permite încărcarea acestei dimensiuni. Te rog să încerci o dimensiune mai mică a fișierului.',\n    'server_post_limit' => 'The server cannot receive the provided amount of data. Try again with less data or a smaller file.',\n    'uploaded'  => 'Serverul nu permite încărcarea acestei dimensiuni. Te rog să încerci o dimensiune mai mică a fișierului.',\n\n    // Drawing & Images\n    'image_upload_error' => 'A apărut o eroare la încărcarea imaginii',\n    'image_upload_type_error' => 'Tipul de imagine încărcat nu este valid',\n    'image_upload_replace_type' => 'Inlocuirea fisierului de imagine trebuie sa fie de acelasi tip',\n    'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.',\n    'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.',\n    'image_gallery_thumbnail_memory_limit' => 'Failed to create gallery thumbnails due to system resource limits.',\n    'drawing_data_not_found' => 'Drawing data could not be loaded. The drawing file might no longer exist or you may not have permission to access it.',\n\n    // Attachments\n    'attachment_not_found' => 'Atașamentul nu a fost găsit',\n    'attachment_upload_error' => 'A apărut o eroare la încărcarea atașamentului',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Nu s-a reușit salvarea ciornei. Asigură-te că ai conexiune la internet înainte de a salva această pagină',\n    'page_draft_delete_fail' => 'Nu s-a putut șterge ciorna paginii și prelua pagina curentă salvată',\n    'page_custom_home_deletion' => 'Nu se poate șterge o pagină în timp ce este setată ca primă pagină',\n\n    // Entities\n    'entity_not_found' => 'Entitate negăsită',\n    'bookshelf_not_found' => 'Raftul nu a fost găsit',\n    'book_not_found' => 'Carte negăsită',\n    'page_not_found' => 'Pagină negăsită',\n    'chapter_not_found' => 'Capitol negăsit',\n    'selected_book_not_found' => 'Cartea selectată nu a fost găsită',\n    'selected_book_chapter_not_found' => 'Cartea selectată sau capitolul nu a fost găsit',\n    'guests_cannot_save_drafts' => 'Vizitatorii nu pot salva ciorne',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Nu poți șterge singurul administrator',\n    'users_cannot_delete_guest' => 'Nu se poate șterge utilizatorul \"Vizitator\"',\n    'users_could_not_send_invite' => 'Could not create user since invite email failed to send',\n\n    // Roles\n    'role_cannot_be_edited' => 'Acest rol nu poate fi editat',\n    'role_system_cannot_be_deleted' => 'Acest rol este un rol de sistem și nu poate fi șters',\n    'role_registration_default_cannot_delete' => 'Acest rol nu poate fi șters când este setat ca rol implicit de înregistrare',\n    'role_cannot_remove_only_admin' => 'Acest utilizator este singurul utilizator atribuit rolului de administrator. Atribuiți rolul de administrator unui alt utilizator înainte de a-l elimina aici.',\n\n    // Comments\n    'comment_list' => 'A apărut o eroare la preluarea comentariilor.',\n    'cannot_add_comment_to_draft' => 'Nu poți adăuga comentarii la o ciornă.',\n    'comment_add' => 'A apărut o eroare la adăugarea / actualizarea comentariului.',\n    'comment_delete' => 'A apărut o eroare la ștergerea comentariului.',\n    'empty_comment' => 'Nu se poate adăuga un comentariu gol.',\n\n    // Error pages\n    '404_page_not_found' => 'Pagina nu a fost găsită',\n    'sorry_page_not_found' => 'Ne pare rău, pagina pe care o cauți nu a putut fi găsită.',\n    'sorry_page_not_found_permission_warning' => 'Dacă te aștepți ca această pagină să existe, s-ar putea să nu ai permisiunea de a o vizualiza.',\n    'image_not_found' => 'Imagine negăsită',\n    'image_not_found_subtitle' => 'Ne pare rău, fișierul de imagine pe care îl cauți nu a putut fi găsit.',\n    'image_not_found_details' => 'Dacă te aștepți ca această imagine să existe, e posibil să fie ștearsă.',\n    'return_home' => 'Întoarce-te acasă',\n    'error_occurred' => 'A apărut o eroare',\n    'app_down' => ':appName nu funcționează acum',\n    'back_soon' => 'Va reveni în curând.',\n\n    // Import\n    'import_zip_cant_read' => 'Could not read ZIP file.',\n    'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',\n    'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Import ZIP failed to validate with errors:',\n    'import_zip_failed_notification' => 'Failed to import ZIP file.',\n    'import_perms_books' => 'You are lacking the required permissions to create books.',\n    'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',\n    'import_perms_pages' => 'You are lacking the required permissions to create pages.',\n    'import_perms_images' => 'You are lacking the required permissions to create images.',\n    'import_perms_attachments' => 'You are lacking the required permission to create attachments.',\n\n    // API errors\n    'api_no_authorization_found' => 'Nu s-a găsit niciun token de autorizare la cerere',\n    'api_bad_authorization_format' => 'A fost găsit un token de autorizare, dar formatul este incorect',\n    'api_user_token_not_found' => 'Nu a fost găsit niciun token API potrivit pentru codul de autorizare furnizat',\n    'api_incorrect_token_secret' => 'Secretul furnizat pentru token-ul API folosit este incorect',\n    'api_user_no_api_permission' => 'Proprietarul token-ului API folosit nu are permisiunea de a efectua apeluri API',\n    'api_user_token_expired' => 'Token-ul de autorizare utilizat a expirat',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Eroare la trimiterea unui e-mail de test:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'The URL does not match the configured allowed SSR hosts',\n];\n"
  },
  {
    "path": "lang/ro/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Comentariu nou pe pagina: :pageName',\n    'new_comment_intro' => 'Un utilizator a comentat pe o pagină în :appName:',\n    'new_page_subject' => 'Pagină nouă: :pageName',\n    'new_page_intro' => 'O nouă pagină a fost creată în :appName:',\n    'updated_page_subject' => 'Pagina actualizată: :pageName',\n    'updated_page_intro' => 'O nouă pagină a fost creată în :appName:',\n    'updated_page_debounce' => 'Pentru a preveni notificări în masă, pentru un timp nu veți primi notificări suplimentare la această pagină de către același editor.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Nume pagină:',\n    'detail_page_path' => 'Page Path:',\n    'detail_commenter' => 'Cine a comentat:',\n    'detail_comment' => 'Comentariu:',\n    'detail_created_by' => 'Creat de:',\n    'detail_updated_by' => 'Actualizat de:',\n\n    'action_view_comment' => 'Vizualizați comentariul',\n    'action_view_page' => 'Vezi pagina',\n\n    'footer_reason' => 'Această notificare ți-a fost trimisă deoarece :link acoperă acest tip de activitate pentru acest articol.',\n    'footer_reason_link' => 'preferințele dvs. de notificare',\n];\n"
  },
  {
    "path": "lang/ro/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Înapoi',\n    'next'     => 'Înainte &raquo;',\n\n];\n"
  },
  {
    "path": "lang/ro/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Parolele trebuie să aibă cel puțin opt caractere și să corespundă confirmării.',\n    'user' => \"Nu putem găsi un utilizator cu această adresă de e-mail.\",\n    'token' => 'Token-ul de resetare a parolei nu este valid pentru această adresă de e-mail.',\n    'sent' => 'Am trimis prin e-mail link-ul de resetare a parolei!',\n    'reset' => 'Parola ta a fost resetată!',\n\n];\n"
  },
  {
    "path": "lang/ro/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Contul meu',\n\n    'shortcuts' => 'Scurtături',\n    'shortcuts_interface' => 'UI Shortcut Preferences',\n    'shortcuts_toggle_desc' => 'Aici puteți activa sau dezactiva scurtăturile interfeței folosite pentru navigare și acțiuni.',\n    'shortcuts_customize_desc' => 'Puteți personaliza fiecare dintre scurtăturile de mai jos. Apăsați combinația de taste dorită după ce selectați intrarea pentru o scurtătură.',\n    'shortcuts_toggle_label' => 'Comenzi rapide activate',\n    'shortcuts_section_navigation' => 'Navigare',\n    'shortcuts_section_actions' => 'Acțiuni comune',\n    'shortcuts_save' => 'Salvează scurtăturile',\n    'shortcuts_overlay_desc' => 'Notă: Când comenzile rapide sunt activate popup de ajutor este disponibilă prin apăsarea \"?\" care va evidenția scurtăturile disponibile pentru acțiunile vizibile în prezent pe ecran.',\n    'shortcuts_update_success' => 'Preferințele dumneavoastră au fost actualizate!',\n    'shortcuts_overview_desc' => 'Gestionați scurtăturile de tastatură pe care le puteți utiliza pentru a naviga prin interfața.',\n\n    'notifications' => 'Preferințe de notificare',\n    'notifications_desc' => 'Controlați notificările prin e-mail pe care le primiți atunci când o anumită activitate este efectuată în sistem.',\n    'notifications_opt_own_page_changes' => 'Notifică la comentarii pe paginile pe care le dețin',\n    'notifications_opt_own_page_comments' => 'Notifică la comentarii pe paginile pe care le dețin',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Notifică la răspunsurile la comentariile mele',\n    'notifications_save' => 'Salvează Preferințe',\n    'notifications_update_success' => 'Preferințele de notificare au fost actualizate!',\n    'notifications_watched' => 'Articole urmărite și ignorate',\n    'notifications_watched_desc' => 'Mai jos sunt elementele care au fost aplicate preferințe personalizate. Pentru a actualiza preferințele pentru acestea, vizualizați elementul și apoi găsiți opțiunile de ceas în bara laterală.',\n\n    'auth' => 'Acces & Securitate',\n    'auth_change_password' => 'Schimbă Parola',\n    'auth_change_password_desc' => 'Change the password you use to log-in to the application. This must be at least 8 characters long.',\n    'auth_change_password_success' => 'Password has been updated!',\n\n    'profile' => 'Detalii Profil',\n    'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.',\n    'profile_view_public' => 'Vezi profilul Public',\n    'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.',\n    'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.',\n    'profile_email_no_permission' => 'Unfortunately you don\\'t have permission to change your email address. If you want to change this, you\\'d need to ask an administrator to change this for you.',\n    'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.',\n    'profile_admin_options' => 'Administrator Options',\n    'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the \"Settings > Users\" area of the application.',\n\n    'delete_account' => 'Delete Account',\n    'delete_my_account' => 'Delete My Account',\n    'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\\'ve created, such as created pages and uploaded images, will remain.',\n    'delete_my_account_warning' => 'Are you sure you want to delete your account?',\n];\n"
  },
  {
    "path": "lang/ro/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Setări',\n    'settings_save' => 'Salvează setările',\n    'system_version' => 'Versiune sistem',\n    'categories' => 'Categorii',\n\n    // App Settings\n    'app_customization' => 'Personalizare',\n    'app_features_security' => 'Caracteristici și securitate',\n    'app_name' => 'Numele aplicației',\n    'app_name_desc' => 'Acest nume este afișat în antet și în orice e-mail trimis de sistem.',\n    'app_name_header' => 'Arată numele în antet',\n    'app_public_access' => 'Acces public',\n    'app_public_access_desc' => 'Activarea acestei opțiuni va permite vizitatorilor, care nu sunt autentificați, să acceseze conținutul în instanța de BookStack.',\n    'app_public_access_desc_guest' => 'Accesul vizitatorilor publici poate fi controlat prin intermediul utilizatorului \"Vizitator\".',\n    'app_public_access_toggle' => 'Permite accesul public',\n    'app_public_viewing' => 'Permiți vizualizarea publică?',\n    'app_secure_images' => 'Încărcare imagini cu securitate mai mare',\n    'app_secure_images_toggle' => 'Activare încărcare imagini cu securitate mai mare',\n    'app_secure_images_desc' => 'Din motive de performanță, toate imaginile sunt publice. Această opțiune adaugă un șir de caractere greu de ghicit în fața url-urilor de imagine. Asigură-te că indexul directorilor nu este activat pentru a preveni accesul ușor.',\n    'app_default_editor' => 'Editor de pagină implicit',\n    'app_default_editor_desc' => 'Selectează editorul care va fi folosit în mod implicit la editarea paginilor noi. Acest lucru poate fi înlocuit la un nivel de pagină unde permisiunile permit.',\n    'app_custom_html' => 'Conținut header HTML personalizat',\n    'app_custom_html_desc' => 'Orice conținut adăugat aici va fi inserat în partea de jos a secțiunii <head> a fiecărei pagini. Acest lucru este util pentru a suprascrie stilurile sau pentru a adăuga cod analitic.',\n    'app_custom_html_disabled_notice' => 'Conținutul headerului HTML personalizat este dezactivat pe această pagină de setări pentru a asigura că modificările pot fi inversate.',\n    'app_logo' => 'Logo aplicație',\n    'app_logo_desc' => 'Acest lucru este folosit în bara de antet a aplicației, printre alte zone. Această imagine ar trebui să fie de 86px în înălțime. Imaginile mari vor fi scalate în jos.',\n    'app_icon' => 'Iconiță aplicație',\n    'app_icon_desc' => 'Această pictogramă este utilizată pentru tab-urile din browser și pictogramele de comenzi rapide. Aceasta ar trebui să fie o imagine PNG pătrată de 256px.',\n    'app_homepage' => 'Pagina principală a aplicației',\n    'app_homepage_desc' => 'Selectează o vizualizare pentru a afișa pe prima pagină în loc de vizualizarea implicită. Permisiunile paginii sunt ignorate pentru paginile selectate.',\n    'app_homepage_select' => 'Selectează o pagină',\n    'app_footer_links' => 'Link-uri de subsol',\n    'app_footer_links_desc' => 'Adaugă link-uri de afișat în subsolul site-ului. Acestea vor fi afișate în partea de jos a majorității paginilor, inclusiv cele care nu necesită autentificare. Poți folosi o etichetă de \"trans::<key>\" pentru a folosi traduceri definite de sistem. De exemplu: Folosind \"trans:common:common.privacy_policy\" va oferi textul tradus \"Politica de confidențialitate\" și \"trans:common.terms_of_service\" va furniza textul tradus \"Termenii serviciului\".',\n    'app_footer_links_label' => 'Etichetă link',\n    'app_footer_links_url' => 'URL link',\n    'app_footer_links_add' => 'Adăugare link subsol',\n    'app_disable_comments' => 'Dezactivează comentariile',\n    'app_disable_comments_toggle' => 'Dezactivează comentariile',\n    'app_disable_comments_desc' => 'Dezactivează comentariile pentru toate paginile aplicației. <br> Comentariile existente nu sunt afișate.',\n\n    // Color settings\n    'color_scheme' => 'Schema de culori a aplicației',\n    'color_scheme_desc' => 'Setați culorile pe care să le utilizați în interfață. Culorile pot fi configurate separat pentru modurile întuneric şi lumină pentru a se potrivi cel mai bine cu tema şi a asigura lizibilitatea.',\n    'ui_colors_desc' => 'Setaţi culoarea primară a aplicaţiei şi culoarea implicită a link-ului. Culoarea primară este utilizată în principal pentru banner-ul antet, butoane şi decoraţiunile interfeţei. Culoarea implicită a link-ului este utilizată pentru link-uri și acțiuni bazate pe text, atât în conținutul scris, cât și în interfața aplicației.',\n    'app_color' => 'Culoare primară',\n    'link_color' => 'Culoare link implicită',\n    'content_colors_desc' => 'Set colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.',\n    'bookshelf_color' => 'Culoare raft',\n    'book_color' => 'Culoare carte',\n    'chapter_color' => 'Culoare capitol',\n    'page_color' => 'Culoare pagină',\n    'page_draft_color' => 'Culoare pagină ciornă',\n\n    // Registration Settings\n    'reg_settings' => 'Înregistrare',\n    'reg_enable' => 'Permite înregistrarea',\n    'reg_enable_toggle' => 'Permite înregistrarea',\n    'reg_enable_desc' => 'Când înregistrarea este activată, utilizatorul va putea să se înregistreze ca utilizator al aplicației. La înregistrare, ei au un singur rol implicit de utilizator.',\n    'reg_default_role' => 'Rol utilizator implicit după înregistrare',\n    'reg_enable_external_warning' => 'Opțiunea de mai sus este ignorată în timp ce autentificarea externă LDAP sau SAML este activă. Conturile de utilizator pentru membrii neexistenți vor fi create automat dacă autentificarea, împotriva sistemului extern utilizat, este reușită.',\n    'reg_email_confirmation' => 'Confirmare e-mail',\n    'reg_email_confirmation_toggle' => 'Solicită confirmare e-mail',\n    'reg_confirm_email_desc' => 'Dacă este folosită restricționarea domeniului, atunci va fi necesară confirmarea e-mailului și această opțiune va fi ignorată.',\n    'reg_confirm_restrict_domain' => 'Restricționare domeniu',\n    'reg_confirm_restrict_domain_desc' => 'Introduceți o listă de domenii de e-mail separate prin virgulă la care doriți să restricționați înregistrarea. Utilizatorilor le va fi trimis un e-mail pentru a-și confirma adresa înainte de a putea interacționa cu aplicația. <br> Rețineți că utilizatorii vor putea să își schimbe adresele de e-mail după o înregistrare reușită.',\n    'reg_confirm_restrict_domain_placeholder' => 'Nicio restricție setată',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Mentenanţă',\n    'maint_image_cleanup' => 'Curățare imagini',\n    'maint_image_cleanup_desc' => 'Scanează pagina și conținutul revizuirii pentru a verifica ce imagini și desene sunt utilizate în prezent și care imagini sunt redundante. Asigurați-vă că ați creat o copie de rezervă a bazei de date și a imaginii complete înainte de a o rula.',\n    'maint_delete_images_only_in_revisions' => 'De asemenea, șterge imagini care există numai în vechile revizuiri ale paginii',\n    'maint_image_cleanup_run' => 'Rulează curățarea',\n    'maint_image_cleanup_warning' => ':count potenţiale imagini nefolosite au fost găsite. Sunteţi sigur că doriţi să ştergeţi aceste imagini?',\n    'maint_image_cleanup_success' => ':count potențiale imagini nefolosite găsite și șterse!',\n    'maint_image_cleanup_nothing_found' => 'Nu au fost găsite imagini nefolosite, nimic șters!',\n    'maint_send_test_email' => 'Trimite un e-mail de test',\n    'maint_send_test_email_desc' => 'Aceasta trimite un e-mail de test la adresa ta de e-mail specificată în profilul tău.',\n    'maint_send_test_email_run' => 'Trimite e-mail de test',\n    'maint_send_test_email_success' => 'E-mail trimis la :address',\n    'maint_send_test_email_mail_subject' => 'Email de test',\n    'maint_send_test_email_mail_greeting' => 'Livrarea e-mailului pare să funcţioneze!',\n    'maint_send_test_email_mail_text' => 'Felicitări! Deoarece ai primit această notificare prin e-mail, setările de e-mail par să fie configurate corespunzător.',\n    'maint_recycle_bin_desc' => 'Rafturile, cărțile, capitole și paginile șterse se trimit la coșul de gunoi pentru a putea fi restaurate sau șterse definitiv. Elementele mai vechi din coșul de gunoi pot fi eliminate automat după o vreme, în funcție de configurația sistemului.',\n    'maint_recycle_bin_open' => 'Deschide coșul de gunoi',\n    'maint_regen_references' => 'Regenerează referințe',\n    'maint_regen_references_desc' => 'Această acțiune va reconstrui indexul de referință al elementului încrucișat în baza de date. Acest lucru este de obicei manipulat automat, dar această acțiune poate fi utilă pentru a indexa conținutul vechi sau conținutul adăugat prin metode neoficiale.',\n    'maint_regen_references_success' => 'Indicele de referință a fost regenerat!',\n    'maint_timeout_command_note' => 'Notă: Această acțiune necesită timp pentru a funcționa, ceea ce poate duce la apariția unor probleme în unele medii web. Ca alternativă, această acțiune trebuie efectuată utilizând o comandă din terminal.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Coș de gunoi',\n    'recycle_bin_desc' => 'Aici poți restaura elementele care au fost șterse sau alege să le elimini definitiv din sistem. Această listă este nefiltrată spre deosebire de listele de activități similare din sistemul în care sunt aplicate filtrele de permisiuni.',\n    'recycle_bin_deleted_item' => 'Element șters',\n    'recycle_bin_deleted_parent' => 'Părinte',\n    'recycle_bin_deleted_by' => 'Șters de',\n    'recycle_bin_deleted_at' => 'Data ștergerii',\n    'recycle_bin_permanently_delete' => 'Ștergere permanentă',\n    'recycle_bin_restore' => 'Restaurează',\n    'recycle_bin_contents_empty' => 'Coșul de gunoi este în prezent gol',\n    'recycle_bin_empty' => 'Golește coșul de gunoi',\n    'recycle_bin_empty_confirm' => 'Aceasta va distruge definitiv toate elementele din coșul de gunoi, inclusiv conținutul conținut din fiecare element. Ești sigur că vrei să golești coșul de gunoi?',\n    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?',\n    'recycle_bin_destroy_list' => 'Elemente de distrus',\n    'recycle_bin_restore_list' => 'Elemente care vor fi restaurate',\n    'recycle_bin_restore_confirm' => 'Această acțiune va restaura elementul șters, inclusiv orice elemente copii, la locația lor originală. În cazul în care locația originală a fost de atunci ștearsă și acum este în coșul de reciclare, elementul părinte va trebui, de asemenea, să fie restaurat.',\n    'recycle_bin_restore_deleted_parent' => 'Părintele acestui element a fost de asemenea șters. Acestea vor rămâne șterse până când acel părinte este, de asemenea, restaurat.',\n    'recycle_bin_restore_parent' => 'Restaurează părinte',\n    'recycle_bin_destroy_notification' => ':count elemente șterse din coșul de gunoi.',\n    'recycle_bin_restore_notification' => ':count elemente restaurate din coșul de gunoi.',\n\n    // Audit Log\n    'audit' => 'Jurnal de audit',\n    'audit_desc' => 'Acest jurnal de audit afișează o listă de activități urmărite în sistem. Această listă este nefiltrată spre deosebire de listele de activități similare din sistemul în care sunt aplicate filtrele de permisiuni.',\n    'audit_event_filter' => 'Filtru eveniment',\n    'audit_event_filter_no_filter' => 'Niciun filtru',\n    'audit_deleted_item' => 'Element șters',\n    'audit_deleted_item_name' => 'Nume: :name',\n    'audit_table_user' => 'Utilizator',\n    'audit_table_event' => 'Eveniment',\n    'audit_table_related' => 'Articol asociat sau detalii',\n    'audit_table_ip' => 'Adresă IP',\n    'audit_table_date' => 'Data activității',\n    'audit_date_from' => 'Interval dată de la',\n    'audit_date_to' => 'Interval dată până la',\n\n    // Role Settings\n    'roles' => 'Roluri',\n    'role_user_roles' => 'Roluri utilizator',\n    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',\n    'roles_x_users_assigned' => ':count utilizator atribuibil:count utilizatori alocați',\n    'roles_x_permissions_provided' => ':count permission|:count permissions',\n    'roles_assigned_users' => 'Utilizator alocat',\n    'roles_permissions_provided' => 'Permisiuni furnizate',\n    'role_create' => 'Crează rol nou',\n    'role_delete' => 'Șterge rolul',\n    'role_delete_confirm' => 'Aceasta va șterge rolul cu numele \\':roleName\\'.',\n    'role_delete_users_assigned' => 'Acest rol are :userCount utilizatori asociați. Dacă vrei să migrezi utilizatorii din acest rol, selectează un nou rol mai jos.',\n    'role_delete_no_migration' => \"Nu migra utilizatorii\",\n    'role_delete_sure' => 'Ești sigur că vrei să ștergi acest rol?',\n    'role_edit' => 'Editează Rol',\n    'role_details' => 'Detalii rol',\n    'role_name' => 'Nume rol',\n    'role_desc' => 'Scurtă descriere a rolului',\n    'role_mfa_enforced' => 'Necesită autentificare multi-factor',\n    'role_external_auth_id' => 'ID-uri externe de autentificare',\n    'role_system' => 'Permisiuni de sistem',\n    'role_manage_users' => 'Gestionare utilizatori',\n    'role_manage_roles' => 'Gestionează roluri și permisiuni de rol',\n    'role_manage_entity_permissions' => 'Gestionează permisiunile pentru toată cartea, capitolul și paginile',\n    'role_manage_own_entity_permissions' => 'Gestionează permisiunile pe propria carte, capitol și pagini',\n    'role_manage_page_templates' => 'Gestionează șabloanele de pagină',\n    'role_access_api' => 'Accesează API sistem',\n    'role_manage_settings' => 'Gestionează setările aplicației',\n    'role_export_content' => 'Exportă conținut',\n    'role_import_content' => 'Import content',\n    'role_editor_change' => 'Schimbă editorul de pagină',\n    'role_notifications' => 'Primire și gestionare notificări',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Permisiuni active',\n    'roles_system_warning' => 'Fi conștient de faptul că accesul la oricare dintre cele trei permisiuni de mai sus poate permite unui utilizator să își modifice propriile privilegii sau privilegiile altor persoane din sistem. Atribuie doar roluri cu aceste permisiuni utilizatorilor de încredere.',\n    'role_asset_desc' => 'Aceste permisiuni controlează accesul implicit la activele din sistem. Permisiunile pe Cărți, Capitole și Pagini vor suprascrie aceste permisiuni.',\n    'role_asset_admins' => 'Administratorilor li se acordă automat acces la tot conținutul, dar aceste opțiuni pot afișa sau ascunde opțiunile UI.',\n    'role_asset_image_view_note' => 'Acest lucru se referă la vizibilitatea în managerul de imagini. Accesul efectiv al fișierelor de imagine încărcate va depinde de opțiunea de stocare a imaginilor din sistem.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'Tot',\n    'role_own' => 'Propriu',\n    'role_controlled_by_asset' => 'Controlat de activele pe care sunt încărcate',\n    'role_save' => 'Salvare rol',\n    'role_users' => 'Utilizatori cu acest rol',\n    'role_users_none' => 'Nici un utilizator nu este asociat acestui rol',\n\n    // Users\n    'users' => 'Utilizatori',\n    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',\n    'user_profile' => 'Profil utilizator',\n    'users_add_new' => 'Adaugă utilizator nou',\n    'users_search' => 'Căutare utilizatori',\n    'users_latest_activity' => 'Ultima activitate',\n    'users_details' => 'Detalii utilizator',\n    'users_details_desc' => 'Setează un nume de afișat și o adresă de e-mail pentru acest utilizator. Adresa de e-mail va fi utilizată pentru autentificarea în aplicație.',\n    'users_details_desc_no_email' => 'Setați un nume de afișat pentru acest utilizator, astfel încât alții să îl poată recunoaște.',\n    'users_role' => 'Roluri utilizator',\n    'users_role_desc' => 'Selectează rolurile cărora le va fi atribuit acest utilizator. Dacă un utilizator este atribuit mai multor roluri, permisiunile de la aceste roluri se vor stivui și vor primi toate abilitățile rolurilor atribuite.',\n    'users_password' => 'Parolă utilizator',\n    'users_password_desc' => 'Setează o parolă utilizată pentru autentificarea în aplicație. Aceasta trebuie să fie de cel puțin 8 caractere.',\n    'users_send_invite_text' => 'Poți alege să trimiți acestui utilizator un e-mail de invitație care să îi permită să își seteze propria parolă, altfel îi poți seta tu parola.',\n    'users_send_invite_option' => 'Trimite e-mail cu invitație utilizatorului',\n    'users_external_auth_id' => 'ID autentificare externă',\n    'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.',\n    'users_password_warning' => 'Only fill the below if you would like to change the password for this user.',\n    'users_system_public' => 'Acest utilizator reprezintă orice utilizator invitat care vizitează instanța dvs. Nu poate fi folosit pentru a vă autentifica, dar este atribuit automat.',\n    'users_delete' => 'Șterge utilizator',\n    'users_delete_named' => 'Șterge utilizatorul :userName',\n    'users_delete_warning' => 'Aceasta va șterge complet acest utilizator cu numele \\':userName\\' din sistem.',\n    'users_delete_confirm' => 'Ești sigur că vrei să ștergi acest utilizator?',\n    'users_migrate_ownership' => 'Migrare proprietate',\n    'users_migrate_ownership_desc' => 'Selectează un utilizator aici dacă vrei ca un alt utilizator să devină proprietarul tuturor articolelor deținute în prezent de acest utilizator.',\n    'users_none_selected' => 'Niciun utilizator selectat',\n    'users_edit' => 'Editare utilizator',\n    'users_edit_profile' => 'Editare profil',\n    'users_avatar' => 'Avatar utilizator',\n    'users_avatar_desc' => 'Selectează o imagine pentru a reprezenta acest utilizator. Aceasta ar trebui să fie un pătrat de aproximativ 256px.',\n    'users_preferred_language' => 'Limba preferată',\n    'users_preferred_language_desc' => 'Această opțiune va schimba limba utilizată pentru interfața de utilizare a aplicației. Acest lucru nu va afecta conținutul creat de utilizatori.',\n    'users_social_accounts' => 'Conturi sociale',\n    'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.',\n    'users_social_accounts_info' => 'Aici poți conecta celelalte conturi pentru o autentificare mai rapidă și mai ușoară. Deconectarea unui cont aici nu revocă accesul autorizat anterior. Revocă accesul din setările profilului tău de pe contul social conectat.',\n    'users_social_connect' => 'Conectare cont',\n    'users_social_disconnect' => 'Deconectare cont',\n    'users_social_status_connected' => 'Connected',\n    'users_social_status_disconnected' => 'Disconnected',\n    'users_social_connected' => ':socialAccount a fost atașat cu succes la profilul tău.',\n    'users_social_disconnected' => 'Contul :socialAccount a fost deconectat cu succes de la profilul tău.',\n    'users_api_tokens' => 'Token API',\n    'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.',\n    'users_api_tokens_none' => 'Nu au fost create token-uri API pentru acest utilizator',\n    'users_api_tokens_create' => 'Creare token',\n    'users_api_tokens_expires' => 'Expiră',\n    'users_api_tokens_docs' => 'Documentație API',\n    'users_mfa' => 'Autentificare multi-factor',\n    'users_mfa_desc' => 'Configurare autentificarea multi-factor ca un nivel suplimentar de securitate pentru contul tău de utilizator.',\n    'users_mfa_x_methods' => ':count metodă configurată|:count metode configurate',\n    'users_mfa_configure' => 'Configurare metode',\n\n    // API Tokens\n    'user_api_token_create' => 'Creare token API',\n    'user_api_token_name' => 'Nume',\n    'user_api_token_name_desc' => 'Dă-i tokenului un nume lizibil ca un memento viitor al scopului propus.',\n    'user_api_token_expiry' => 'Data expirării',\n    'user_api_token_expiry_desc' => 'Setează o dată la care acest token expiră. După această dată, cererile făcute folosind acest token nu vor mai funcționa. Lăsând acest câmp necompletat se va stabili un termen de expirare de 100 de ani în viitor.',\n    'user_api_token_create_secret_message' => 'Imediat după crearea acestui token, va fi generat și afișat un \"ID\" și \"Secret\". Secretul va fi afișat o singură dată, așa că fiți siguri să copiați valoarea într-un loc sigur și sigur înainte de a continua.',\n    'user_api_token' => 'Token API',\n    'user_api_token_id' => 'ID Token',\n    'user_api_token_id_desc' => 'Acesta este un identificator de sistem needitabil generat pentru acest token, care va trebui furnizat în cereri API.',\n    'user_api_token_secret' => 'Secret token',\n    'user_api_token_secret_desc' => 'Acesta este un secret generat de sistem pentru acest token, care va trebui furnizat în cereri API. Acest lucru va fi afișat doar o singură dată, așa că copiază această valoare undeva în siguranță și securizat.',\n    'user_api_token_created' => 'Token creat :timeAgo',\n    'user_api_token_updated' => 'Token actualizat :timeAgo',\n    'user_api_token_delete' => 'Șterge token',\n    'user_api_token_delete_warning' => 'Acest lucru va șterge complet acest token API cu numele \\':tokenName\\' din sistem.',\n    'user_api_token_delete_confirm' => 'Sigur dorești să ștergi acest token API?',\n\n    // Webhooks\n    'webhooks' => 'Webhook-uri',\n    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',\n    'webhooks_x_trigger_events' => ':count declanșator eveniment:count evenimente de declanșare',\n    'webhooks_create' => 'Creează un nou Webhook',\n    'webhooks_none_created' => 'Nu au fost create webhook-uri.',\n    'webhooks_edit' => 'Editare Webhook',\n    'webhooks_save' => 'Salvează Webhook-ul',\n    'webhooks_details' => 'Detalii Webhook',\n    'webhooks_details_desc' => 'Furnizează un nume prietenos de utilizator și un punct de reper POST ca locație pentru datele webhook-ului care vor fi trimise.',\n    'webhooks_events' => 'Evenimente Webhook',\n    'webhooks_events_desc' => 'Selectează toate evenimentele care vor declanșa acest webhook pentru a fi apelate.',\n    'webhooks_events_warning' => 'Ține cont că aceste evenimente vor fi declanșate pentru toate evenimentele selectate, chiar dacă sunt aplicate permisiuni personalizate. Asigură-te că utilizarea acestui webhook nu va expune conținut confidențial.',\n    'webhooks_events_all' => 'Toate evenimentele de sistem',\n    'webhooks_name' => 'Numele Webhook-ului',\n    'webhooks_timeout' => 'Timeout cerere Webhook (secunde)',\n    'webhooks_endpoint' => 'Endpoint Webhook',\n    'webhooks_active' => 'Webhook activ',\n    'webhook_events_table_header' => 'Evenimente',\n    'webhooks_delete' => 'Șterge Webhook-ul',\n    'webhooks_delete_warning' => 'Aceasta va șterge complet acest webhook, cu numele \\':webhookName\\', din sistem.',\n    'webhooks_delete_confirm' => 'Ești sigur că vrei să ștergi acest webhook?',\n    'webhooks_format_example' => 'Exemplu format Webhook',\n    'webhooks_format_example_desc' => 'Datele Webhook sunt trimise sub forma unei cereri POST pentru endpointul configurat ca JSON conform formatului de mai jos. Proprietățile \"elemente asociate\" și \"url\" sunt opționale și vor depinde de tipul de eveniment declanșat.',\n    'webhooks_status' => 'Starea Webhook-ului',\n    'webhooks_last_called' => 'Ultima apelare:',\n    'webhooks_last_errored' => 'Ultima eroare:',\n    'webhooks_last_error_message' => 'Ultimul mesaj de eroare:',\n\n    // Licensing\n    'licenses' => 'Licenses',\n    'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.',\n    'licenses_bookstack' => 'BookStack License',\n    'licenses_php' => 'PHP Library Licenses',\n    'licenses_js' => 'JavaScript Library Licenses',\n    'licenses_other' => 'Other Licenses',\n    'license_details' => 'License Details',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/ro/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute trebuie să fie acceptat.',\n    'active_url'           => ':attribute nu este un URL valid.',\n    'after'                => ':attribute trebuie să fie o dată după :date.',\n    'alpha'                => ':attribute poate conține doar litere.',\n    'alpha_dash'           => ':attribute poate conține doar litere, numere, cratime și underscore.',\n    'alpha_num'            => ':attribute poate conține doar litere și cifre.',\n    'array'                => ':attribute trebuie să fie un array.',\n    'backup_codes'         => 'Codul furnizat nu este valid sau a fost deja folosit.',\n    'before'               => ':attribute trebuie să fie o dată înainte de :date.',\n    'between'              => [\n        'numeric' => ':attribute trebuie să fie între :min şi :max.',\n        'file'    => ':attribute trebuie să aibă între :min şi :max kiloocteţi.',\n        'string'  => ':attribute trebuie să aibă între :min şi :max caractere.',\n        'array'   => ':attribute trebuie să aibă între :min şi :max elemente.',\n    ],\n    'boolean'              => 'Câmpul :attribute trebuie să fie adevărat sau fals.',\n    'confirmed'            => 'Confirmarea :attribute nu se potrivește.',\n    'date'                 => ':attribute nu este o dată validă.',\n    'date_format'          => ':attribute nu se potrivește cu formatul :format.',\n    'different'            => ':attribute și :other trebuie să fie diferite.',\n    'digits'               => ':attribute trebuie să aibă :digits cifre.',\n    'digits_between'       => ':attribute trebuie să aibă între :min şi :max cifre.',\n    'email'                => ':attribute trebuie să fie o adresă de e-mail validă.',\n    'ends_with' => ':attribute trebuie să se termine cu una dintre următoarele: :values',\n    'file'                 => ':attribute trebuie să fie furnizat ca un fişier valid.',\n    'filled'               => 'Câmpul :attribute este necesar.',\n    'gt'                   => [\n        'numeric' => ':attribute trebuie să fie mai mare decât :value.',\n        'file'    => ':attribute trebuie să fie mai mare decât :value kilobytes.',\n        'string'  => ':attribute trebuie să fie mai mare decât :value caractere.',\n        'array'   => ':attribute trebuie să aibă mai mult de :value elemente.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute trebuie să fie mai mare sau egal cu :value.',\n        'file'    => ':attribute trebuie să fie mai mare sau egal cu :value kilobytes.',\n        'string'  => ':attribute trebuie să fie mai mare sau egal cu :value caractere.',\n        'array'   => ':attribute trebuie să aibă :value elemente sau mai multe.',\n    ],\n    'exists'               => 'Atributul :attribute selectat nu este valid.',\n    'image'                => ':attribute trebuie să fie o imagine.',\n    'image_extension'      => ':attribute trebuie să aibă o extensie validă și suportată.',\n    'in'                   => ':attribute selectat nu este valid.',\n    'integer'              => ':attribute trebuie să fie un număr.',\n    'ip'                   => ':attribute trebuie să fie o adresă IP validă.',\n    'ipv4'                 => ':attribute trebuie să fie o adresă IPv4 validă.',\n    'ipv6'                 => ':attribute trebuie să fie o adresă IPv6 validă.',\n    'json'                 => ':attribute trebuie să fie un șir JSON valid.',\n    'lt'                   => [\n        'numeric' => ':attribute trebuie să fie mai mic decât :value.',\n        'file'    => ':attribute trebuie să fie mai mic de :value kilobytes.',\n        'string'  => ':attribute trebuie să aibă mai puţin de :value caractere.',\n        'array'   => ':attribute trebuie să aibă mai puţin de :value elemente.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute trebuie să fie mai mic sau egal cu :value.',\n        'file'    => ':attribute trebuie să fie mai mic sau egal cu :value kilobytes.',\n        'string'  => ':attribute trebuie să fie mai mic sau egal cu :value caractere.',\n        'array'   => ':attribute nu trebuie să aibă mai mult de :value elemente.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute nu poate fi mai mare de :max.',\n        'file'    => ':attribute nu poate fi mai mare de :max kilobytes.',\n        'string'  => ':attribute nu poate avea mai mult de :max caractere.',\n        'array'   => ':attribute nu poate avea mai mult de :max elemente.',\n    ],\n    'mimes'                => ':attribute trebuie să fie un fişier de tipul: :values.',\n    'min'                  => [\n        'numeric' => ':attribute trebuie să aibă cel puțin :min.',\n        'file'    => ':attribute trebuie să aibă cel puțin :min kiloocteţi.',\n        'string'  => ':attribute trebuie să aibă cel puțin :min caractere.',\n        'array'   => ':attribute trebuie să aibă cel puțin :min elemente.',\n    ],\n    'not_in'               => 'Câmpul :attribute selectat nu este valid.',\n    'not_regex'            => 'Câmpul :attribute nu este valid.',\n    'numeric'              => ':attribute trebuie să fie un număr.',\n    'regex'                => ':attribute nu este valid.',\n    'required'             => ':attribute este necesar.',\n    'required_if'          => 'Câmpul :attribute este obligatoriu atunci când :other este :value.',\n    'required_with'        => 'Câmpul :attribute este necesar când :values este prezent.',\n    'required_with_all'    => 'Câmpul :attribute este necesar când :values este prezent.',\n    'required_without'     => 'Câmpul :attribute este obligatoriu atunci când :values nu este prezent.',\n    'required_without_all' => 'Câmpul :attribute este necesar când niciuna dintre :values nu este prezentă.',\n    'same'                 => ':attribute și :other trebuie să se potrivească.',\n    'safe_url'             => 'Este posibil ca link-ul furnizat să nu fie sigur.',\n    'size'                 => [\n        'numeric' => ':attribute trebuie să fie :size.',\n        'file'    => ':attribute trebuie să aibă :size kilobiți.',\n        'string'  => ':attribute trebuie să aibă :size caractere.',\n        'array'   => 'Câmpul :attribute trebuie să aibă :size elemente.',\n    ],\n    'string'               => ':attribute trebuie să fie un șir de caractere.',\n    'timezone'             => ':attribute trebuie să fie o zonă validă.',\n    'totp'                 => 'Codul furnizat nu este valid sau a expirat.',\n    'unique'               => ':attribute a fost deja folosit.',\n    'url'                  => ':attribute nu este valid.',\n    'uploaded'             => 'Fişierul nu a putut fi încărcat. Serverul nu poate accepta fişiere de această dimensiune.',\n\n    'zip_file' => 'The :attribute needs to reference a file within the ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',\n    'zip_model_expected' => 'Data object expected but \":type\" found.',\n    'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Este necesară confirmarea parolei',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/ru/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'создал страницу',\n    'page_create_notification'    => 'Страница успешно создана',\n    'page_update'                 => 'обновил страницу',\n    'page_update_notification'    => 'Страница успешно обновлена',\n    'page_delete'                 => 'удалил страницу',\n    'page_delete_notification'    => 'Страница успешно удалена',\n    'page_restore'                => 'восстановил страницу',\n    'page_restore_notification'   => 'Страница успешно восстановлена',\n    'page_move'                   => 'переместил страницу',\n    'page_move_notification'      => 'Страница успешно перемещена',\n\n    // Chapters\n    'chapter_create'              => 'создал главу',\n    'chapter_create_notification' => 'Глава успешно создана',\n    'chapter_update'              => 'обновил главу',\n    'chapter_update_notification' => 'Глава успешно обновлена',\n    'chapter_delete'              => 'удалил главу',\n    'chapter_delete_notification' => 'Глава успешно удалена',\n    'chapter_move'                => 'переместил главу',\n    'chapter_move_notification' => 'Глава успешно перемещена',\n\n    // Books\n    'book_create'                 => 'создал книгу',\n    'book_create_notification'    => 'Книга успешно создана',\n    'book_create_from_chapter'              => 'преобразовал главу в книгу',\n    'book_create_from_chapter_notification' => 'Глава успешно преобразована в книгу',\n    'book_update'                 => 'обновил книгу',\n    'book_update_notification'    => 'Книга успешно обновлена',\n    'book_delete'                 => 'удалил книгу',\n    'book_delete_notification'    => 'Книга успешно удалена',\n    'book_sort'                   => 'отсортировал книгу',\n    'book_sort_notification'      => 'Книга успешно отсортирована',\n\n    // Bookshelves\n    'bookshelf_create'            => 'создал полку',\n    'bookshelf_create_notification'    => 'Полка успешно создана',\n    'bookshelf_create_from_book'    => 'преобразовал книгу в полку',\n    'bookshelf_create_from_book_notification'    => 'Книга успешно преобразована в полку',\n    'bookshelf_update'                 => 'обновил полку',\n    'bookshelf_update_notification'    => 'Полка успешно обновлена',\n    'bookshelf_delete'                 => 'удалил полку',\n    'bookshelf_delete_notification'    => 'Полка успешно удалена',\n\n    // Revisions\n    'revision_restore' => 'восстановил версию',\n    'revision_delete' => 'удалил версию',\n    'revision_delete_notification' => 'Версия успешно удалена',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" добавлено в избранное',\n    'favourite_remove_notification' => '\":name\" удалено из избранного',\n\n    // Watching\n    'watch_update_level_notification' => 'Настройки просмотра успешно обновлены',\n\n    // Auth\n    'auth_login' => 'вошёл',\n    'auth_register' => 'зарегистрировался как новый пользователь',\n    'auth_password_reset_request' => 'запросил смену пароля пользователя',\n    'auth_password_reset_update' => 'сбросил пароль пользователя',\n    'mfa_setup_method' => 'настроил метод МФА',\n    'mfa_setup_method_notification' => 'Многофакторный метод аутентификации успешно настроен',\n    'mfa_remove_method' => 'удалил метод МФА',\n    'mfa_remove_method_notification' => 'Многофакторный метод аутентификации успешно удален',\n\n    // Settings\n    'settings_update' => 'обновил настройки',\n    'settings_update_notification' => 'Настройки успешно обновлены',\n    'maintenance_action_run' => 'запустил техническое обслуживание',\n\n    // Webhooks\n    'webhook_create' => 'создал вебхук',\n    'webhook_create_notification' => 'Вебхук успешно создан',\n    'webhook_update' => 'обновил вебхук',\n    'webhook_update_notification' => 'Вебхук успешно обновлен',\n    'webhook_delete' => 'удалил вебхук',\n    'webhook_delete_notification' => 'Вебхук успешно удален',\n\n    // Imports\n    'import_create' => 'created import',\n    'import_create_notification' => 'Import successfully uploaded',\n    'import_run' => 'обновлен импорт',\n    'import_run_notification' => 'Content successfully imported',\n    'import_delete' => 'deleted import',\n    'import_delete_notification' => 'Import successfully deleted',\n\n    // Users\n    'user_create' => 'создал пользователя',\n    'user_create_notification' => 'Пользователь успешно создан',\n    'user_update' => 'обновил пользователя',\n    'user_update_notification' => 'Пользователь успешно обновлен',\n    'user_delete' => 'удалил пользователя',\n    'user_delete_notification' => 'Пользователь успешно удален',\n\n    // API Tokens\n    'api_token_create' => 'создан API токен',\n    'api_token_create_notification' => 'API токен успешно создан',\n    'api_token_update' => 'обновлён API токен',\n    'api_token_update_notification' => 'API токен успешно обновлен',\n    'api_token_delete' => 'обновил API токен',\n    'api_token_delete_notification' => 'API токен успешно удален',\n\n    // Roles\n    'role_create' => 'создал роль',\n    'role_create_notification' => 'Роль успешно создана',\n    'role_update' => 'обновил роль',\n    'role_update_notification' => 'Роль успешно обновлена',\n    'role_delete' => 'удалил роль',\n    'role_delete_notification' => 'Роль успешно удалена',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'очистил корзину',\n    'recycle_bin_restore' => 'восстановлено из корзины',\n    'recycle_bin_destroy' => 'удалено из корзины',\n\n    // Comments\n    'commented_on'                => 'прокомментировал',\n    'comment_create'              => 'добавил комментарий',\n    'comment_update'              => 'обновил комментарий',\n    'comment_delete'              => 'удалил комментарий',\n\n    // Sort Rules\n    'sort_rule_create' => 'создал правило сортировки',\n    'sort_rule_create_notification' => 'Правило сортировки успешно создано',\n    'sort_rule_update' => 'обновил правило сортировки',\n    'sort_rule_update_notification' => 'Правило сортировки успешно обновлено',\n    'sort_rule_delete' => 'удалил правило сортировки',\n    'sort_rule_delete_notification' => 'Правило сортировки успешно удалено',\n\n    // Other\n    'permissions_update'          => 'обновил разрешения',\n];\n"
  },
  {
    "path": "lang/ru/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Введенные вами данные не найдены в нашей базе.',\n    'throttle' => 'Слишком много попыток входа. Пожалуйста, повторите попытку через :seconds секунд.',\n\n    // Login & Register\n    'sign_up' => 'Регистрация',\n    'log_in' => 'Вход',\n    'log_in_with' => 'Вход с :socialDriver',\n    'sign_up_with' => 'Регистрация с :socialDriver',\n    'logout' => 'Выход',\n\n    'name' => 'Имя',\n    'username' => 'Логин',\n    'email' => 'Адрес электронной почты',\n    'password' => 'Пароль',\n    'password_confirm' => 'Подтверждение пароля',\n    'password_hint' => 'Не менее 8 символов',\n    'forgot_password' => 'Забыли пароль?',\n    'remember_me' => 'Запомнить меня',\n    'ldap_email_hint' => 'Введите адрес электронной почты для этой учетной записи.',\n    'create_account' => 'Создать аккаунт',\n    'already_have_account' => 'Уже есть аккаунт?',\n    'dont_have_account' => 'У вас нет аккаунта?',\n    'social_login' => 'Вход через Соцсеть',\n    'social_registration' => 'Регистрация через Соцсеть',\n    'social_registration_text' => 'Регистрация и вход через другой сервис.',\n\n    'register_thanks' => 'Благодарим за регистрацию!',\n    'register_confirm' => 'Проверьте свою электронную почту и нажмите кнопку подтверждения для доступа к :appName.',\n    'registrations_disabled' => 'Регистрация отключена',\n    'registration_email_domain_invalid' => 'Данный домен электронной почты недоступен для регистрации',\n    'register_success' => 'Спасибо за регистрацию! Регистрация и вход в систему выполнены.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Попытка входа',\n    'auto_init_starting_desc' => 'Мы связываемся с вашей системой аутентификации, для процесса входа. Если через 5 секунд ничего не произошло, вы можете попробовать нажать на ссылку ниже.',\n    'auto_init_start_link' => 'Повторить аутентификацию',\n\n    // Password Reset\n    'reset_password' => 'Сброс пароля',\n    'reset_password_send_instructions' => 'Введите свой адрес электронной почты ниже, и вам будет отправлено письмо со ссылкой для сброса пароля.',\n    'reset_password_send_button' => 'Сбросить пароль',\n    'reset_password_sent' => 'Ссылка для сброса пароля будет выслана на :email, если этот адрес находится в системе.',\n    'reset_password_success' => 'Ваш пароль был успешно сброшен.',\n    'email_reset_subject' => 'Сброс пароля от :appName',\n    'email_reset_text' => 'Вы получили это письмо, потому что запросили сброс пароля для вашей учетной записи.',\n    'email_reset_not_requested' => 'Если вы не запрашивали сброса пароля, то никаких дополнительных действий не требуется.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Подтвердите ваш почтовый адрес на :appName',\n    'email_confirm_greeting' => 'Благодарим за участие :appName!',\n    'email_confirm_text' => 'Пожалуйста, подтвердите свой адрес электронной почты нажав на кнопку ниже:',\n    'email_confirm_action' => 'Подтвердить адрес электронной почты',\n    'email_confirm_send_error' => 'Требуется подтверждение электронной почты, но система не может отправить письмо. Свяжитесь с администратором, чтобы убедиться, что адрес электронной почты настроен правильно.',\n    'email_confirm_success' => 'Ваш адрес электронной почты был подтвержден! Теперь вы можете войти в систему, используя этот адрес электронной почты.',\n    'email_confirm_resent' => 'Письмо с подтверждение выслано снова. Пожалуйста, проверьте ваш почтовый ящик.',\n    'email_confirm_thanks' => 'Спасибо за подтверждение!',\n    'email_confirm_thanks_desc' => 'Подождите, пока обработка вашего подтверждения будет завершена. Если вы не будете перенаправлены через 3 секунды, нажмите на ссылку «Продолжить» для продолжения.',\n\n    'email_not_confirmed' => 'Адрес электронной почты не подтвержден',\n    'email_not_confirmed_text' => 'Ваш email адрес все еще не подтвержден.',\n    'email_not_confirmed_click_link' => 'Пожалуйста, нажмите на ссылку в письме, которое было отправлено при регистрации.',\n    'email_not_confirmed_resend' => 'Если вы не можете найти электронное письмо, вы можете снова отправить его с подтверждением по форме ниже.',\n    'email_not_confirmed_resend_button' => 'Переотправить письмо с подтверждением',\n\n    // User Invite\n    'user_invite_email_subject' => 'Вас приглашают присоединиться к :appName!',\n    'user_invite_email_greeting' => 'Для вас создан аккаунт в :appName.',\n    'user_invite_email_text' => 'Нажмите кнопку ниже, чтобы задать пароль и получить доступ:',\n    'user_invite_email_action' => 'Установить пароль для аккаунта',\n    'user_invite_page_welcome' => 'Добро пожаловать в :appName!',\n    'user_invite_page_text' => 'Завершите настройку аккаунта, установите пароль для дальнейшего входа в :appName.',\n    'user_invite_page_confirm_button' => 'Подтвердите пароль',\n    'user_invite_success_login' => 'Пароль установлен, теперь вы можете войти в систему, используя установленный пароль для доступа к :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Многофакторная аутентификация',\n    'mfa_setup_desc' => 'Многофакторная аутентификация повышает степень безопасности вашей учетной записи.',\n    'mfa_setup_configured' => 'Настроено',\n    'mfa_setup_reconfigure' => 'Перенастроить',\n    'mfa_setup_remove_confirmation' => 'Вы уверены, что хотите удалить этот многофакторный метод аутентификации?',\n    'mfa_setup_action' => 'Настройка',\n    'mfa_backup_codes_usage_limit_warning' => 'У вас осталось менее 5 резервных кодов, пожалуйста, создайте и сохраните новый набор перед тем, как закончатся коды, чтобы предотвратить блокировку вашей учетной записи.',\n    'mfa_option_totp_title' => 'Мобильное приложение',\n    'mfa_option_totp_desc' => 'Для использования многофакторной аутентификации вам понадобится мобильное приложение, поддерживающее TOTP, например Google Authenticator, Authy или Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Резервные коды',\n    'mfa_option_backup_codes_desc' => 'Генерирует набор одноразовых резервных кодов, которые вы вводите при входе, чтобы проверить вашу личность. Не забудьте сохранить их в безопасном месте.',\n    'mfa_gen_confirm_and_enable' => 'Подтвердить и включить',\n    'mfa_gen_backup_codes_title' => 'Настройка резервных кодов',\n    'mfa_gen_backup_codes_desc' => 'Сохраните приведенный ниже список кодов в безопасном месте. При доступе к системе вы сможете использовать один из кодов в качестве второго механизма аутентификации.',\n    'mfa_gen_backup_codes_download' => 'Скачать коды',\n    'mfa_gen_backup_codes_usage_warning' => 'Каждый код может быть использован только один раз',\n    'mfa_gen_totp_title' => 'Настройка мобильного приложения',\n    'mfa_gen_totp_desc' => 'Для использования многофакторной аутентификации вам понадобится мобильное приложение, поддерживающее TOTP, например Google Authenticator, Authy или Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Отсканируйте QR-код, используя приложение для аутентификации.',\n    'mfa_gen_totp_verify_setup' => 'Проверить настройки',\n    'mfa_gen_totp_verify_setup_desc' => 'Проверьте, что все работает введя код, сгенерированный внутри вашего приложения для аутентификации, в поле ввода ниже:',\n    'mfa_gen_totp_provide_code_here' => 'Введите код, сгенерированный приложением',\n    'mfa_verify_access' => 'Подтвердите доступ',\n    'mfa_verify_access_desc' => 'Ваша учетная запись требует подтверждения личности на дополнительном уровне верификации, прежде чем вам будет предоставлен доступ. Для продолжения подтвердите вход, используя один из настроенных методов.',\n    'mfa_verify_no_methods' => 'Методы не настроены',\n    'mfa_verify_no_methods_desc' => 'Для вашей учетной записи не найдены многофакторные методы аутентификации. Вам нужно настроить хотя бы один метод, прежде чем получить доступ.',\n    'mfa_verify_use_totp' => 'Проверить используя мобильное приложение',\n    'mfa_verify_use_backup_codes' => 'Проверить используя резервный код',\n    'mfa_verify_backup_code' => 'Резервный код',\n    'mfa_verify_backup_code_desc' => 'Введите один из оставшихся резервных кодов ниже:',\n    'mfa_verify_backup_code_enter_here' => 'Введите резервный код',\n    'mfa_verify_totp_desc' => 'Введите код, сгенерированный с помощью мобильного приложения, ниже:',\n    'mfa_setup_login_notification' => 'Многофакторный метод аутентификации настроен, пожалуйста, войдите снова, используя сконфигурированный метод.',\n];\n"
  },
  {
    "path": "lang/ru/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Отмена',\n    'close' => 'Закрыть',\n    'confirm' => 'Применить',\n    'back' => 'Назад',\n    'save' => 'Сохранить',\n    'continue' => 'Продолжить',\n    'select' => 'Выбрать',\n    'toggle_all' => 'Переключить все',\n    'more' => 'Еще',\n\n    // Form Labels\n    'name' => 'Имя',\n    'description' => 'Описание',\n    'role' => 'Роль',\n    'cover_image' => 'Обложка',\n    'cover_image_description' => 'Это изображение должно быть приблизительно 440x250px, хотя оно и будет гибко масштабироваться и обрезаться, чтобы соответствовать пользовательскому интерфейсу в различных необходимых сценариях. Так что фактические размеры дисплея будут отличаться.',\n\n    // Actions\n    'actions' => 'Действия',\n    'view' => 'Просмотр',\n    'view_all' => 'Показать все',\n    'new' => 'Новый',\n    'create' => 'Создание',\n    'update' => 'Обновление',\n    'edit' => 'Редактировать',\n    'archive' => 'Archive',\n    'unarchive' => 'Un-Archive',\n    'sort' => 'Сортировать',\n    'move' => 'Переместить',\n    'copy' => 'Скопировать',\n    'reply' => 'Ответить',\n    'delete' => 'Удалить',\n    'delete_confirm' => 'Подтвердить удаление',\n    'search' => 'Поиск',\n    'search_clear' => 'Очистить поиск',\n    'reset' => 'Сбросить',\n    'remove' => 'Удалить',\n    'add' => 'Добавить',\n    'configure' => 'Настройка',\n    'manage' => 'Управлять',\n    'fullscreen' => 'На весь экран',\n    'favourite' => 'Избранное',\n    'unfavourite' => 'Убрать из избранного',\n    'next' => 'Следующая',\n    'previous' => 'Предыдущая',\n    'filter_active' => 'Активный фильтр:',\n    'filter_clear' => 'Сбросить фильтр',\n    'download' => 'Загрузить',\n    'open_in_tab' => 'Открыть во вкладке',\n    'open' => 'Открыть',\n\n    // Sort Options\n    'sort_options' => 'Параметры сортировки',\n    'sort_direction_toggle' => 'Переключить направление сортировки',\n    'sort_ascending' => 'По возрастанию',\n    'sort_descending' => 'По убыванию',\n    'sort_name' => 'По имени',\n    'sort_default' => 'По умолчанию',\n    'sort_created_at' => 'По дате создания',\n    'sort_updated_at' => 'По дате обновления',\n\n    // Misc\n    'deleted_user' => 'Удаленный пользователь',\n    'no_activity' => 'Нет действий для просмотра',\n    'no_items' => 'Нет доступных элементов',\n    'back_to_top' => 'Наверх',\n    'skip_to_main_content' => 'Перейти к основному контенту',\n    'toggle_details' => 'Подробности',\n    'toggle_thumbnails' => 'Миниатюры',\n    'details' => 'Детали',\n    'grid_view' => 'Вид сеткой',\n    'list_view' => 'Вид списком',\n    'default' => 'По умолчанию',\n    'breadcrumb' => 'Навигация',\n    'status' => 'Состояние',\n    'status_active' => 'Активен',\n    'status_inactive' => 'Неактивен',\n    'never' => 'Никогда',\n    'none' => 'Нет',\n\n    // Header\n    'homepage' => 'Главная страница',\n    'header_menu_expand' => 'Развернуть меню заголовка',\n    'profile_menu' => 'Меню профиля',\n    'view_profile' => 'Посмотреть профиль',\n    'edit_profile' => 'Редактировать профиль',\n    'dark_mode' => 'Темный режим',\n    'light_mode' => 'Светлый режим',\n    'global_search' => 'Глобальный поиск',\n\n    // Layout tabs\n    'tab_info' => 'Информация',\n    'tab_info_label' => 'Вкладка: Показать вторичную информацию',\n    'tab_content' => 'Содержание',\n    'tab_content_label' => 'Вкладка: Показать основной контент',\n\n    // Email Content\n    'email_action_help' => 'Если у вас возникли проблемы с нажатием кнопки \\':actionText\\', то скопируйте и вставьте указанный URL-адрес в свой браузер:',\n    'email_rights' => 'Все права защищены',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Политика конфиденциальности',\n    'terms_of_service' => 'Условия использования',\n\n    // OpenSearch\n    'opensearch_description' => 'Поиск :appName',\n];\n"
  },
  {
    "path": "lang/ru/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Выбрать изображение',\n    'image_list' => 'Список изображений',\n    'image_details' => 'Детали изображения',\n    'image_upload' => 'Загрузить изображение',\n    'image_intro' => 'Здесь вы можете выбрать и управлять изображениями, которые были ранее загружены в систему.',\n    'image_intro_upload' => 'Загрузите новое изображение, перетянув файл в это окно, или с помощью кнопки \"Загрузить изображение\" выше.',\n    'image_all' => 'Все',\n    'image_all_title' => 'Просмотр всех изображений',\n    'image_book_title' => 'Просмотр всех изображений, загруженных в эту книгу',\n    'image_page_title' => 'Просмотр всех изображений, загруженных на эту страницу',\n    'image_search_hint' => 'Поиск по названию изображения',\n    'image_uploaded' => 'Загружено :uploadedDate',\n    'image_uploaded_by' => 'Загружено :userName',\n    'image_uploaded_to' => 'Загружено на :pageLink',\n    'image_updated' => 'Обновлено :updateDate',\n    'image_load_more' => 'Загрузить еще',\n    'image_image_name' => 'Название изображения',\n    'image_delete_used' => 'Это изображение используется на странице ниже.',\n    'image_delete_confirm_text' => 'Вы уверены, что хотите удалить это изображение?',\n    'image_select_image' => 'Выбрать изображение',\n    'image_dropzone' => 'Перетащите изображение или кликните для загрузки',\n    'image_dropzone_drop' => 'Перетащите изображения сюда для загрузки',\n    'images_deleted' => 'Изображения удалены',\n    'image_preview' => 'Предпросмотр изображения',\n    'image_upload_success' => 'Изображение успешно загружено',\n    'image_update_success' => 'Детали изображения успешно обновлены',\n    'image_delete_success' => 'Изображение успешно удалено',\n    'image_replace' => 'Заменить изображение',\n    'image_replace_success' => 'Файл изображения успешно обновлён',\n    'image_rebuild_thumbs' => 'Пересоздать вариации размера',\n    'image_rebuild_thumbs_success' => 'Вариации размера изображения успешно установлены!',\n\n    // Code Editor\n    'code_editor' => 'Изменить код',\n    'code_language' => 'Язык кода',\n    'code_content' => 'Содержимое кода',\n    'code_session_history' => 'История сессии',\n    'code_save' => 'Сохранить код',\n];\n"
  },
  {
    "path": "lang/ru/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Общие',\n    'advanced' => 'Дополнительно',\n    'none' => 'Нет',\n    'cancel' => 'Отмена',\n    'save' => 'Сохранить',\n    'close' => 'Закрыть',\n    'apply' => 'Применить',\n    'undo' => 'Отменить',\n    'redo' => 'Повторить',\n    'left' => 'Слева',\n    'center' => 'По центру',\n    'right' => 'Справа',\n    'top' => 'Сверху',\n    'middle' => 'Посередине',\n    'bottom' => 'Снизу',\n    'width' => 'Ширина',\n    'height' => 'Высота',\n    'More' => 'Еще',\n    'select' => 'Выбрать...',\n\n    // Toolbar\n    'formats' => 'Форматы',\n    'header_large' => 'Крупный заголовок',\n    'header_medium' => 'Средний заголовок',\n    'header_small' => 'Небольшой заголовок',\n    'header_tiny' => 'Маленький заголовок',\n    'paragraph' => 'Обычный текст',\n    'blockquote' => 'Цитата',\n    'inline_code' => 'Встроенный код',\n    'callouts' => 'Выноска',\n    'callout_information' => 'Информация',\n    'callout_success' => 'Успех',\n    'callout_warning' => 'Предупреждение',\n    'callout_danger' => 'Ошибка',\n    'bold' => 'Жирный',\n    'italic' => 'Курсив',\n    'underline' => 'Подчёркнутый',\n    'strikethrough' => 'Зачёркнутый',\n    'superscript' => 'Надстрочный',\n    'subscript' => 'Подстрочный',\n    'text_color' => 'Цвет текста',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Пользовательский цвет',\n    'remove_color' => 'Удалить цвет',\n    'background_color' => 'Цвет фона',\n    'align_left' => 'По левому краю',\n    'align_center' => 'По центру',\n    'align_right' => 'По правому краю',\n    'align_justify' => 'По ширине',\n    'list_bullet' => 'Маркированный список',\n    'list_numbered' => 'Нумерованный список',\n    'list_task' => 'Список задач',\n    'indent_increase' => 'Увеличить отступ',\n    'indent_decrease' => 'Уменьшить отступ',\n    'table' => 'Таблица',\n    'insert_image' => 'Вставить изображение',\n    'insert_image_title' => 'Вставить/Редактировать изображение',\n    'insert_link' => 'Вставить/редактировать ссылку',\n    'insert_link_title' => 'Вставить/Редактировать ссылку',\n    'insert_horizontal_line' => 'Вставить горизонтальную линию',\n    'insert_code_block' => 'Вставить блок кода',\n    'edit_code_block' => 'Редактировать код',\n    'insert_drawing' => 'Вставить/редактировать схему',\n    'drawing_manager' => 'Менеджер схем',\n    'insert_media' => 'Вставить/редактировать медиафайл',\n    'insert_media_title' => 'Вставить/Редактировать медиафайл',\n    'clear_formatting' => 'Очистить форматирование',\n    'source_code' => 'Исходный код',\n    'source_code_title' => 'Исходный код',\n    'fullscreen' => 'Полноэкранный режим',\n    'image_options' => 'Параметры изображения',\n\n    // Tables\n    'table_properties' => 'Свойства таблицы',\n    'table_properties_title' => 'Свойства таблицы',\n    'delete_table' => 'Удалить таблицу',\n    'table_clear_formatting' => 'Очистить форматирование таблицы',\n    'resize_to_contents' => 'Изменить размер содержимого',\n    'row_header' => 'Заголовок строки',\n    'insert_row_before' => 'Вставить строку выше',\n    'insert_row_after' => 'Вставить строку ниже',\n    'delete_row' => 'Удалить строку',\n    'insert_column_before' => 'Вставить столбец слева',\n    'insert_column_after' => 'Вставить столбец справа',\n    'delete_column' => 'Удалить столбец',\n    'table_cell' => 'Ячейка',\n    'table_row' => 'Строка',\n    'table_column' => 'Столбец',\n    'cell_properties' => 'Свойства ячейки',\n    'cell_properties_title' => 'Свойства ячейки',\n    'cell_type' => 'Тип ячейки',\n    'cell_type_cell' => 'Ячейка',\n    'cell_scope' => 'Область охвата',\n    'cell_type_header' => 'Заголовок ячейки',\n    'merge_cells' => 'Объединить ячейки',\n    'split_cell' => 'Разделить ячейку',\n    'table_row_group' => 'Объединить строки',\n    'table_column_group' => 'Объединить столбцы',\n    'horizontal_align' => 'Выровнять по горизонтали',\n    'vertical_align' => 'Выровнять по вертикали',\n    'border_width' => 'Ширина границы',\n    'border_style' => 'Стиль границы',\n    'border_color' => 'Цвет границы',\n    'row_properties' => 'Свойства строки',\n    'row_properties_title' => 'Свойства строки',\n    'cut_row' => 'Вырезать строку',\n    'copy_row' => 'Копировать строку',\n    'paste_row_before' => 'Вставить строку выше',\n    'paste_row_after' => 'Вставить строку ниже',\n    'row_type' => 'Тип строки',\n    'row_type_header' => 'Заголовок',\n    'row_type_body' => 'Тело',\n    'row_type_footer' => 'Нижняя часть',\n    'alignment' => 'Выравнивание',\n    'cut_column' => 'Вырезать столбец',\n    'copy_column' => 'Копировать столбец',\n    'paste_column_before' => 'Вставить столбец слева',\n    'paste_column_after' => 'Вставить столбец справа',\n    'cell_padding' => 'Свойство cellpadding',\n    'cell_spacing' => 'Свойство cellspacing',\n    'caption' => 'Подпись',\n    'show_caption' => 'Показать',\n    'constrain' => 'Сохранять пропорции',\n    'cell_border_solid' => 'Сплошная',\n    'cell_border_dotted' => 'Точками',\n    'cell_border_dashed' => 'Пунктирная',\n    'cell_border_double' => 'Двойная сплошная',\n    'cell_border_groove' => 'Канавка',\n    'cell_border_ridge' => 'Хребет',\n    'cell_border_inset' => 'Вставка',\n    'cell_border_outset' => 'Начало',\n    'cell_border_none' => 'Нет',\n    'cell_border_hidden' => 'Прозрачная',\n\n    // Images, links, details/summary & embed\n    'source' => 'Источник',\n    'alt_desc' => 'Альтернативное описание',\n    'embed' => 'Код для вставки',\n    'paste_embed' => 'Введите код для вставки ниже:',\n    'url' => 'URL-адрес',\n    'text_to_display' => 'Текст для отображения',\n    'title' => 'Заголовок',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Открыть ссылку',\n    'open_link_in' => 'Открыть ссылку в...',\n    'open_link_current' => 'В текущем окне',\n    'open_link_new' => 'В новом окне',\n    'remove_link' => 'Удалить ссылку',\n    'insert_collapsible' => 'Вставить свернутый блок',\n    'collapsible_unwrap' => 'Удалить блок',\n    'edit_label' => 'Изменить метку',\n    'toggle_open_closed' => 'Развернуть/свернуть',\n    'collapsible_edit' => 'Редактировать свернутый блок',\n    'toggle_label' => 'Метка',\n\n    // About view\n    'about' => 'О редакторе',\n    'about_title' => 'О редакторе WYSIWYG',\n    'editor_license' => 'Лицензия редактора и авторские права',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'Этот редактор собран с помощью :tinyLink, который предоставляется под MIT лицензией.',\n    'editor_tiny_license_link' => 'Авторские права и подробности лицензии TinyMCE вы можете найти здесь.',\n    'save_continue' => 'Сохранить страницу и продолжить',\n    'callouts_cycle' => '(Держите нажатым для переключения типов)',\n    'link_selector' => 'Ссылка на содержимое',\n    'shortcuts' => 'Сочетания клавиш',\n    'shortcut' => 'Сочетания клавиш',\n    'shortcuts_intro' => 'Следующие сочетания клавиш доступны в редакторе:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Описание',\n];\n"
  },
  {
    "path": "lang/ru/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Недавно созданные',\n    'recently_created_pages' => 'Недавно созданные страницы',\n    'recently_updated_pages' => 'Недавно обновленные страницы',\n    'recently_created_chapters' => 'Недавно созданные главы',\n    'recently_created_books' => 'Недавно созданные книги',\n    'recently_created_shelves' => 'Недавно созданные полки',\n    'recently_update' => 'Недавно обновленные',\n    'recently_viewed' => 'Недавно просмотренные',\n    'recent_activity' => 'Недавние действия',\n    'create_now' => 'Создать сейчас',\n    'revisions' => 'Версии',\n    'meta_revision' => 'Версия #:revisionCount',\n    'meta_created' => 'Создано :timeLength',\n    'meta_created_name' => ':user создал :timeLength',\n    'meta_updated' => 'Обновлено :timeLength',\n    'meta_updated_name' => ':user обновил :timeLength',\n    'meta_owned_name' => 'Владелец :user',\n    'meta_reference_count' => 'Ссылается :count элемент|Ссылается :count элементов',\n    'entity_select' => 'Выбор объекта',\n    'entity_select_lack_permission' => 'У вас нет разрешения на выбор этого элемента',\n    'images' => 'Изображения',\n    'my_recent_drafts' => 'Мои последние черновики',\n    'my_recently_viewed' => 'Мои недавние просмотры',\n    'my_most_viewed_favourites' => 'Популярное избранное',\n    'my_favourites' => 'Мое избранное',\n    'no_pages_viewed' => 'Вы не просматривали ни одной страницы',\n    'no_pages_recently_created' => 'Нет недавно созданных страниц',\n    'no_pages_recently_updated' => 'Нет недавно обновленных страниц',\n    'export' => 'Экспорт',\n    'export_html' => 'Веб файл',\n    'export_pdf' => 'PDF файл',\n    'export_text' => 'Текстовый файл',\n    'export_md' => 'Файл Markdown',\n    'export_zip' => 'Портативный ZIP',\n    'default_template' => 'Шаблон страницы по умолчанию',\n    'default_template_explain' => 'Назначить шаблон страницы, который будет использоваться в качестве содержимого по умолчанию для всех страниц, созданных в этом элементе. Имейте в виду, что это будет работать, только если создатель страницы имеет доступ к выбранной странице шаблона.',\n    'default_template_select' => 'Выберите страницу шаблона',\n    'import' => 'Импорт',\n    'import_validate' => 'Проверка импорта',\n    'import_desc' => 'Импортировать книги, главы и страницы с помощью ZIP-файла, экспортированного из этого или другого источника. Выберите ZIP-файл, чтобы продолжить. После загрузки и проверки файла вы сможете настроить и подтвердить импорт в следующем окне.',\n    'import_zip_select' => 'Выберите ZIP файл для загрузки',\n    'import_zip_validation_errors' => 'Были обнаружены ошибки при проверке предоставленного ZIP файла:',\n    'import_pending' => 'Ожидается импорт',\n    'import_pending_none' => 'Импорт не был запущен.',\n    'import_continue' => 'Продолжить импорт',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Запустить импорт',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Uploaded by',\n    'import_location' => 'Import Location',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'Ошибки импорта',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Разрешения',\n    'permissions_desc' => 'Установите права доступа для переопределения прав, предоставленных ролями пользователей по-умолчанию.',\n    'permissions_book_cascade' => 'Права доступа, установленные для книг, автоматически распространяются на дочерние главы и страницы, если для них не определены собственные разрешения.',\n    'permissions_chapter_cascade' => 'Права доступа, установленные для глав, автоматически распространяются на дочерние страницы, если для них не определены собственные разрешения.',\n    'permissions_save' => 'Сохранить разрешения',\n    'permissions_owner' => 'Владелец',\n    'permissions_role_everyone_else' => 'Все остальные',\n    'permissions_role_everyone_else_desc' => 'Установить права доступа для всех ролей, которые не были специально переопределены.',\n    'permissions_role_override' => 'Переопределить права доступа для роли',\n    'permissions_inherit_defaults' => 'Наследовать по умолчанию',\n\n    // Search\n    'search_results' => 'Результаты поиска',\n    'search_total_results_found' => 'Найден :count результат|Найдено :count результата|Найдено :count результатов',\n    'search_clear' => 'Очистить поиск',\n    'search_no_pages' => 'Нет страниц, соответствующих этому поиску',\n    'search_for_term' => 'Искать :term',\n    'search_more' => 'Еще результаты',\n    'search_advanced' => 'Расширенный поиск',\n    'search_terms' => 'Поисковые запросы',\n    'search_content_type' => 'Тип содержимого',\n    'search_exact_matches' => 'Точные соответствия',\n    'search_tags' => 'Поиск по тегам',\n    'search_options' => 'Параметры',\n    'search_viewed_by_me' => 'Просмотрено мной',\n    'search_not_viewed_by_me' => 'Не просматривалось мной',\n    'search_permissions_set' => 'Набор разрешений',\n    'search_created_by_me' => 'Создано мной',\n    'search_updated_by_me' => 'Обновлено мной',\n    'search_owned_by_me' => 'Созданные мной',\n    'search_date_options' => 'Параметры даты',\n    'search_updated_before' => 'Обновлено до',\n    'search_updated_after' => 'Обновлено после',\n    'search_created_before' => 'Создано до',\n    'search_created_after' => 'Создано после',\n    'search_set_date' => 'Установить дату',\n    'search_update' => 'Обновить поиск',\n\n    // Shelves\n    'shelf' => 'Полка',\n    'shelves' => 'Полки',\n    'x_shelves' => ':count полка|:count полки|:count полок',\n    'shelves_empty' => 'Полки не созданы',\n    'shelves_create' => 'Создать новую полку',\n    'shelves_popular' => 'Популярные полки',\n    'shelves_new' => 'Новые полки',\n    'shelves_new_action' => 'Новая полка',\n    'shelves_popular_empty' => 'Популярные полки появятся здесь.',\n    'shelves_new_empty' => 'Последние созданные полки появятся здесь.',\n    'shelves_save' => 'Сохранить полку',\n    'shelves_books' => 'Книги из этой полки',\n    'shelves_add_books' => 'Добавить книгу в эту полку',\n    'shelves_drag_books' => 'Перетащите книги ниже, чтобы добавить их на эту полку',\n    'shelves_empty_contents' => 'На этой полке нет книг',\n    'shelves_edit_and_assign' => 'Изменить полку для привязки книг',\n    'shelves_edit_named' => 'Редактировать полку :name',\n    'shelves_edit' => 'Редактировать полку',\n    'shelves_delete' => 'Удалить полку',\n    'shelves_delete_named' => 'Удалить полку :name',\n    'shelves_delete_explain' => \"Это приведет к удалению полки с именем ':name'. Привязанные книги удалены не будут.\",\n    'shelves_delete_confirmation' => 'Вы уверены, что хотите удалить эту полку?',\n    'shelves_permissions' => 'Доступы к полке',\n    'shelves_permissions_updated' => 'Доступы к полке обновлены',\n    'shelves_permissions_active' => 'Действующие разрешения полки',\n    'shelves_permissions_cascade_warning' => 'Разрешения на полки не наследуются автоматически содержащимся в них книгам. Это происходит потому, что книга может находиться на нескольких полках. Однако разрешения могут быть установлены для книг полки с помощью опции, приведенной ниже.',\n    'shelves_permissions_create' => 'Разрешения полки на создание используется только для копирования разрешений на дочерние книги с помощью действия, описанного ниже. Они не контролируют возможность создавать книги.',\n    'shelves_copy_permissions_to_books' => 'Наследовать доступы книгам',\n    'shelves_copy_permissions' => 'Копировать доступы',\n    'shelves_copy_permissions_explain' => 'Это применит текущие настройки разрешений для этой полки ко всем книгам, содержащимся в ней. Перед активацией убедитесь, что все изменения разрешений этой полки были сохранены.',\n    'shelves_copy_permission_success' => 'Доступы полки скопированы для :count книг',\n\n    // Books\n    'book' => 'Книга',\n    'books' => 'Книги',\n    'x_books' => ':count книга|:count книги|:count книг',\n    'books_empty' => 'Нет созданных книг',\n    'books_popular' => 'Популярные книги',\n    'books_recent' => 'Недавние книги',\n    'books_new' => 'Новые книги',\n    'books_new_action' => 'Новая книга',\n    'books_popular_empty' => 'Здесь появятся самые популярные книги.',\n    'books_new_empty' => 'Здесь появятся самые последние созданные книги.',\n    'books_create' => 'Создать новую книгу',\n    'books_delete' => 'Удалить книгу',\n    'books_delete_named' => 'Удалить книгу :bookName',\n    'books_delete_explain' => 'Это удалит книги с именем \\':bookName\\'. Все разделы и страницы будут удалены.',\n    'books_delete_confirmation' => 'Вы действительно хотите удалить эту книгу?',\n    'books_edit' => 'Редактировать книгу',\n    'books_edit_named' => 'Редактировать книгу :bookName',\n    'books_form_book_name' => 'Название книги',\n    'books_save' => 'Сохранить книгу',\n    'books_permissions' => 'Разрешения на книгу',\n    'books_permissions_updated' => 'Разрешения на книгу обновлены',\n    'books_empty_contents' => 'Для этой книги нет страниц или разделов.',\n    'books_empty_create_page' => 'Создать новую страницу',\n    'books_empty_sort_current_book' => 'Сортировка текущей книги',\n    'books_empty_add_chapter' => 'Добавить главу',\n    'books_permissions_active' => 'Действующие разрешения книги',\n    'books_search_this' => 'Поиск в этой книге',\n    'books_navigation' => 'Навигация по книге',\n    'books_sort' => 'Сортировка содержимого книги',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Сортировка книги :bookName',\n    'books_sort_name' => 'По имени',\n    'books_sort_created' => 'По дате создания',\n    'books_sort_updated' => 'По дате обновления',\n    'books_sort_chapters_first' => 'Главы в начале',\n    'books_sort_chapters_last' => 'Главы в конце',\n    'books_sort_show_other' => 'Показать другие книги',\n    'books_sort_save' => 'Сохранить новый порядок',\n    'books_sort_show_other_desc' => 'Добавьте другие книги здесь, чтобы включить их в сортировку, и позволить легко реорганизовать книгу.',\n    'books_sort_move_up' => 'Переместить вверх',\n    'books_sort_move_down' => 'Переместить вниз',\n    'books_sort_move_prev_book' => 'Переместить в предыдущую книгу',\n    'books_sort_move_next_book' => 'Переместить в следующую книгу',\n    'books_sort_move_prev_chapter' => 'Переместить в предыдущую главу',\n    'books_sort_move_next_chapter' => 'Переместить в следующую главу',\n    'books_sort_move_book_start' => 'Переместить в начало книги',\n    'books_sort_move_book_end' => 'Переместить в конец книги',\n    'books_sort_move_before_chapter' => 'Переместить перед главой',\n    'books_sort_move_after_chapter' => 'Переместить после главы',\n    'books_copy' => 'Копировать книгу',\n    'books_copy_success' => 'Книга успешно скопирована',\n\n    // Chapters\n    'chapter' => 'Глава',\n    'chapters' => 'Главы',\n    'x_chapters' => ':count глава|:count главы|:count глав',\n    'chapters_popular' => 'Популярные главы',\n    'chapters_new' => 'Новая глава',\n    'chapters_create' => 'Создать новую главу',\n    'chapters_delete' => 'Удалить главу',\n    'chapters_delete_named' => 'Удалить главу :chapterName',\n    'chapters_delete_explain' => 'Это действие удалит главу с названием \\':chapterName\\'. Все страницы, которые существуют в этой главе, также будут удалены.',\n    'chapters_delete_confirm' => 'Вы действительно хотите удалить эту главу?',\n    'chapters_edit' => 'Редактировать главу',\n    'chapters_edit_named' => 'Редактировать главу :chapterName',\n    'chapters_save' => 'Сохранить главу',\n    'chapters_move' => 'Переместить главу',\n    'chapters_move_named' => 'Переместить главу :chapterName',\n    'chapters_copy' => 'Копировать главу',\n    'chapters_copy_success' => 'Глава успешно скопирована',\n    'chapters_permissions' => 'Разрешения главы',\n    'chapters_empty' => 'В этой главе нет страниц.',\n    'chapters_permissions_active' => 'Действующие разрешения главы',\n    'chapters_permissions_success' => 'Разрешения главы обновлены',\n    'chapters_search_this' => 'Искать в этой главе',\n    'chapter_sort_book' => 'Сортировать книгу',\n\n    // Pages\n    'page' => 'Страница',\n    'pages' => 'Страницы',\n    'x_pages' => ':count страница|:count страницы|:count страниц',\n    'pages_popular' => 'Популярные страницы',\n    'pages_new' => 'Новая страница',\n    'pages_attachments' => 'Вложения',\n    'pages_navigation' => 'Навигация на странице',\n    'pages_delete' => 'Удалить страницу',\n    'pages_delete_named' => 'Удалить страницу :pageName',\n    'pages_delete_draft_named' => 'Удалить черновик :pageName',\n    'pages_delete_draft' => 'Удалить черновик',\n    'pages_delete_success' => 'Страница удалена',\n    'pages_delete_draft_success' => 'Черновик удален',\n    'pages_delete_warning_template' => 'Эта страница активно используется как шаблон страницы по умолчанию для книги или главы. Эти книги или главы больше не будут иметь шаблон страницы по умолчанию, назначенный после удаления этой страницы.',\n    'pages_delete_confirm' => 'Вы действительно хотите удалить эту страницу?',\n    'pages_delete_draft_confirm' => 'Вы действительно хотите удалить этот черновик?',\n    'pages_editing_named' => 'Редактирование страницы :pageName',\n    'pages_edit_draft_options' => 'Параметры черновика',\n    'pages_edit_save_draft' => 'Сохранить черновик',\n    'pages_edit_draft' => 'Редактировать черновик',\n    'pages_editing_draft' => 'Редактирование черновика',\n    'pages_editing_page' => 'Редактирование страницы',\n    'pages_edit_draft_save_at' => 'Черновик сохранён в ',\n    'pages_edit_delete_draft' => 'Удалить черновик',\n    'pages_edit_delete_draft_confirm' => 'Вы уверены, что хотите удалить черновик с изменениями? Все изменения, внесенные вами с момента последнего полного сохранения, будут утеряны и редактор будет обновлен данными с последнего сохранения страницы.',\n    'pages_edit_discard_draft' => 'Отменить черновик',\n    'pages_edit_switch_to_markdown' => 'Переключиться на Markdown',\n    'pages_edit_switch_to_markdown_clean' => 'Только Markdown (с возможными потерями форматирования)',\n    'pages_edit_switch_to_markdown_stable' => 'Полное сохранение форматирования (HTML)',\n    'pages_edit_switch_to_wysiwyg' => 'Переключиться в WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(В бета-тестировании)',\n    'pages_edit_set_changelog' => 'Задать список изменений',\n    'pages_edit_enter_changelog_desc' => 'Введите краткое описание внесенных изменений',\n    'pages_edit_enter_changelog' => 'Введите список изменений',\n    'pages_editor_switch_title' => 'Переключить редактор',\n    'pages_editor_switch_are_you_sure' => 'Вы уверены, что хотите изменить редактор для этой страницы?',\n    'pages_editor_switch_consider_following' => 'При изменении редактора учитывайте следующее:',\n    'pages_editor_switch_consideration_a' => 'После сохранения новая опция редактора будет использоваться любыми пользователями, которые будут редактировать данную страницу, включая тех, которые не смогут самостоятельно изменить тип редактора.',\n    'pages_editor_switch_consideration_b' => 'Это потенциально может привести к потере деталей и синтаксиса при определенных обстоятельствах.',\n    'pages_editor_switch_consideration_c' => 'Изменения в тегах или журнале, сделанные с момента последнего сохранения, не сохраняются в этом изменении.',\n    'pages_save' => 'Сохранить страницу',\n    'pages_title' => 'Заголовок страницы',\n    'pages_name' => 'Название страницы',\n    'pages_md_editor' => 'Редактор',\n    'pages_md_preview' => 'Просмотр',\n    'pages_md_insert_image' => 'Вставить изображение',\n    'pages_md_insert_link' => 'Вставить ссылку на объект',\n    'pages_md_insert_drawing' => 'Вставить рисунок',\n    'pages_md_show_preview' => 'Предпросмотр',\n    'pages_md_sync_scroll' => 'Синхронизировать прокрутку',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Найден несохраненный чертеж',\n    'pages_drawing_unsaved_confirm' => 'Несохраненные данные были найдены из предыдущей неудачной попытки сохранения рисунка. Вы хотите восстановить и продолжить редактирование несохраненного рисунка?',\n    'pages_not_in_chapter' => 'Страница не находится в главе',\n    'pages_move' => 'Переместить страницу',\n    'pages_copy' => 'Скопировать страницу',\n    'pages_copy_desination' => 'Скопировать в',\n    'pages_copy_success' => 'Страница скопирована',\n    'pages_permissions' => 'Разрешения страницы',\n    'pages_permissions_success' => 'Pазрешения страницы обновлены',\n    'pages_revision' => 'Версия',\n    'pages_revisions' => 'Версии страницы',\n    'pages_revisions_desc' => 'Ниже перечислены все предыдущие изменения этой страницы. Вы можете посмотреть резервные копии, сравнить и восстановить старые версии страниц, если позволяют разрешения. Полная история страницы не может быть полностью отражена здесь, поскольку, в зависимости от системной конфигурации, старые версии могут быть удалены автоматически.',\n    'pages_revisions_named' => 'Версии страницы для :pageName',\n    'pages_revision_named' => 'Версия страницы для :pageName',\n    'pages_revision_restored_from' => 'Восстановлено из #:id; :summary',\n    'pages_revisions_created_by' => 'Создана',\n    'pages_revisions_date' => 'Дата версии',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Номер версии',\n    'pages_revisions_numbered' => 'Версия #:id',\n    'pages_revisions_numbered_changes' => 'Изменения в версии #:id',\n    'pages_revisions_editor' => 'Тип редактора',\n    'pages_revisions_changelog' => 'Список изменений',\n    'pages_revisions_changes' => 'Изменения',\n    'pages_revisions_current' => 'Текущая версия',\n    'pages_revisions_preview' => 'Просмотр',\n    'pages_revisions_restore' => 'Восстановить',\n    'pages_revisions_none' => 'У этой страницы нет других версий',\n    'pages_copy_link' => 'Копировать ссылку',\n    'pages_edit_content_link' => 'Перейти к разделу в редакторе',\n    'pages_pointer_enter_mode' => 'Войти в режим выбора раздела',\n    'pages_pointer_label' => 'Настройки раздела страницы',\n    'pages_pointer_permalink' => 'Постоянная ссылка на раздел страницы',\n    'pages_pointer_include_tag' => 'Раздел страницы с тегом',\n    'pages_pointer_toggle_link' => 'Режим постоянной ссылки. Нажмите, чтобы показать включение тега',\n    'pages_pointer_toggle_include' => 'Включить режим тега. Нажмите для отображения постоянной ссылки',\n    'pages_permissions_active' => 'Действующие разрешения на страницу',\n    'pages_initial_revision' => 'Первоначальное издание',\n    'pages_references_update_revision' => 'Система автоматически обновила внутренние ссылки',\n    'pages_initial_name' => 'Новая страница',\n    'pages_editing_draft_notification' => 'В настоящее время вы редактируете черновик, который был сохранён :timeDiff.',\n    'pages_draft_edited_notification' => 'Эта страница была обновлена до этого момента. Рекомендуется отменить этот черновик.',\n    'pages_draft_page_changed_since_creation' => 'Эта страница была обновлена с момента создания данного черновика. Рекомендуется выбросить этот черновик или следить за тем, чтобы не перезаписать все изменения на странице.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count пользователей начали редактирование этой страницы',\n        'start_b' => ':userName начал редактирование этой страницы',\n        'time_a' => 'поскольку последние страницы были обновлены',\n        'time_b' => 'за последние :minCount минут',\n        'message' => ':start :time. Будьте осторожны, чтобы не перезаписывать друг друга!',\n    ],\n    'pages_draft_discarded' => 'Черновик сброшен! Редактор обновлен текущим содержимым страницы',\n    'pages_draft_deleted' => 'Черновик удалён! Редактор обновлен текущим содержимым страницы',\n    'pages_specific' => 'Конкретная страница',\n    'pages_is_template' => 'Шаблон страницы',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Переключить боковую панель',\n    'page_tags' => 'Теги страницы',\n    'chapter_tags' => 'Теги главы',\n    'book_tags' => 'Теги книги',\n    'shelf_tags' => 'Теги полки',\n    'tag' => 'Тег',\n    'tags' =>  'Теги',\n    'tags_index_desc' => 'Теги могут быть применены к содержимому для гибкой категоризации. Теги могут иметь как ключ, так и значение, значение является необязательным. После применения содержимое может быть найдено с помощью имени тега и значения.',\n    'tag_name' =>  'Имя тега',\n    'tag_value' => 'Значение тега (опционально)',\n    'tags_explain' => \"Добавьте теги, чтобы лучше классифицировать ваш контент. \\\\n Вы можете присвоить значение тегу для более глубокой организации.\",\n    'tags_add' => 'Добавить тег',\n    'tags_remove' => 'Удалить этот тег',\n    'tags_usages' => 'Всего использовано тегов',\n    'tags_assigned_pages' => 'Назначено на страницы',\n    'tags_assigned_chapters' => 'Назначено на главы',\n    'tags_assigned_books' => 'Назначено на книги',\n    'tags_assigned_shelves' => 'Назначено на полки',\n    'tags_x_unique_values' => 'Уникальные значения: :count',\n    'tags_all_values' => 'Все значения',\n    'tags_view_tags' => 'Посмотреть теги',\n    'tags_view_existing_tags' => 'Просмотр имеющихся тегов',\n    'tags_list_empty_hint' => 'Теги можно присваивать через боковую панель редактора страниц или при редактировании сведений о книге, главе или полке.',\n    'attachments' => 'Вложения',\n    'attachments_explain' => 'Загрузите несколько файлов или добавьте ссылку для отображения на своей странице. Они видны на боковой панели страницы.',\n    'attachments_explain_instant_save' => 'Изменения здесь сохраняются мгновенно.',\n    'attachments_upload' => 'Загрузить файл',\n    'attachments_link' => 'Присоединить ссылку',\n    'attachments_upload_drop' => 'Или вы можете перетащить файл сюда, чтобы загрузить его в качестве вложения.',\n    'attachments_set_link' => 'Установить ссылку',\n    'attachments_delete' => 'Вы уверены, что хотите удалить это вложение?',\n    'attachments_dropzone' => 'Перетащите файлы сюда для загрузки',\n    'attachments_no_files' => 'Файлы не загружены',\n    'attachments_explain_link' => 'Вы можете присоединить ссылку, если вы предпочитаете не загружать файл. Это может быть ссылка на другую страницу или ссылка на файл в облаке.',\n    'attachments_link_name' => 'Название ссылки',\n    'attachment_link' => 'Ссылка на вложение',\n    'attachments_link_url' => 'Ссылка на файл',\n    'attachments_link_url_hint' => 'URL-адрес сайта или файла',\n    'attach' => 'Прикрепить',\n    'attachments_insert_link' => 'Добавить ссылку на вложение',\n    'attachments_edit_file' => 'Редактировать файл',\n    'attachments_edit_file_name' => 'Название файла',\n    'attachments_edit_drop_upload' => 'Перетащите файлы или нажмите здесь, чтобы загрузить и перезаписать',\n    'attachments_order_updated' => 'Порядок вложений обновлен',\n    'attachments_updated_success' => 'Детали вложения обновлены',\n    'attachments_deleted' => 'Вложение удалено',\n    'attachments_file_uploaded' => 'Файл успешно загружен',\n    'attachments_file_updated' => 'Файл успешно обновлен',\n    'attachments_link_attached' => 'Ссылка успешно присоединена к странице',\n    'templates' => 'Шаблоны',\n    'templates_set_as_template' => 'Страница является шаблоном',\n    'templates_explain_set_as_template' => 'Вы можете назначить эту страницу в качестве шаблона, её содержимое будет использоваться при создании других страниц. Пользователи смогут использовать этот шаблон в случае, если имеют разрешения на просмотр этой страницы.',\n    'templates_replace_content' => 'Заменить содержимое страницы',\n    'templates_append_content' => 'Добавить к содержанию страницы',\n    'templates_prepend_content' => 'Добавить в начало содержимого страницы',\n\n    // Profile View\n    'profile_user_for_x' => 'Пользователь уже :time',\n    'profile_created_content' => 'Созданный контент',\n    'profile_not_created_pages' => ':userName не создал ни одной страницы',\n    'profile_not_created_chapters' => ':userName не создал ни одной главы',\n    'profile_not_created_books' => ':userName не создал ни одной книги',\n    'profile_not_created_shelves' => ':userName не создал ни одной полки',\n\n    // Comments\n    'comment' => 'Комментарий',\n    'comments' => 'Комментарии',\n    'comment_add' => 'Комментировать',\n    'comment_none' => 'Нет комментариев для отображения',\n    'comment_placeholder' => 'Оставить комментарий здесь',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count архивировано',\n    'comment_archived_threads' => 'Архивированные темы',\n    'comment_save' => 'Сохранить комментарий',\n    'comment_new' => 'Новый комментарий',\n    'comment_created' => 'прокомментировал :createDiff',\n    'comment_updated' => 'Обновлен :updateDiff пользователем :username',\n    'comment_updated_indicator' => 'Обновлено',\n    'comment_deleted_success' => 'Комментарий удален',\n    'comment_created_success' => 'Комментарий добавлен',\n    'comment_updated_success' => 'Комментарий обновлен',\n    'comment_archive_success' => 'Комментарий заархивирован',\n    'comment_unarchive_success' => 'Комментарий разархивирован',\n    'comment_view' => 'Просмотреть комментарий',\n    'comment_jump_to_thread' => 'Перейти к теме',\n    'comment_delete_confirm' => 'Удалить этот комментарий?',\n    'comment_in_reply_to' => 'В ответ на :commentId',\n    'comment_reference' => 'Ссылка',\n    'comment_reference_outdated' => '(Устаревшее)',\n    'comment_editor_explain' => 'Вот комментарии, которые были оставлены на этой странице. Комментарии могут быть добавлены и управляться при просмотре сохраненной страницы.',\n\n    // Revision\n    'revision_delete_confirm' => 'Удалить эту версию?',\n    'revision_restore_confirm' => 'Вы уверены, что хотите восстановить эту версию? Текущее содержимое страницы будет заменено.',\n    'revision_cannot_delete_latest' => 'Нельзя удалить последнюю версию.',\n\n    // Copy view\n    'copy_consider' => 'При копировании содержимого, пожалуйста, учтите следующее.',\n    'copy_consider_permissions' => 'Пользовательские настройки прав доступа не будут скопированы.',\n    'copy_consider_owner' => 'Вы станете владельцем всего скопированного контента.',\n    'copy_consider_images' => 'Файлы изображений страницы не будут дублироваться и исходные изображения сохранят их отношение к странице, в которую они были загружены изначально.',\n    'copy_consider_attachments' => 'Вложения страницы не будут скопированы.',\n    'copy_consider_access' => 'Изменение положения, владельца или разрешений может привести к тому, что контент будет доступен пользователям, у которых не было доступа ранее.',\n\n    // Conversions\n    'convert_to_shelf' => 'Преобразовать в полку',\n    'convert_to_shelf_contents_desc' => 'Вы можете превратить эту книгу в новую полку с тем же содержимым. Главы, содержащиеся в этой книге, будут преобразованы в новые книги. Если эта книга содержит какие-либо страницы, которых нет в главе, она будет переименована и будет содержать такие страницы, и эта книга станет частью новой полки.',\n    'convert_to_shelf_permissions_desc' => 'Любые разрешения, установленные для этой книги, будут скопированы на новую полку и во все новые дочерние книги, для которых не применяются собственные разрешения. Обратите внимание, что разрешения на полки не применяются автоматически к содержимому внутри, как это происходит с книгами.',\n    'convert_book' => 'Преобразовать книгу',\n    'convert_book_confirm' => 'Вы уверены, что хотите преобразовать эту книгу?',\n    'convert_undo_warning' => 'Это не отменяется простым способом.',\n    'convert_to_book' => 'Преобразовать в книгу',\n    'convert_to_book_desc' => 'Вы можете преобразовать эту главу в новую книгу с тем же содержанием. Любые разрешения, установленные в этой главе, будут скопированы в новую книгу, но любые унаследованные разрешения из родительской книги не будут скопированы, что может привести к изменению контроля доступа.',\n    'convert_chapter' => 'Преобразовать главу',\n    'convert_chapter_confirm' => 'Вы уверены, что хотите преобразовать эту главу?',\n\n    // References\n    'references' => 'Ссылки',\n    'references_none' => 'Нет отслеживаемых ссылок на этот элемент.',\n    'references_to_desc' => 'Ниже перечислены все известные материалы в системе, которые ссылаются на этот элемент.',\n\n    // Watch Options\n    'watch' => 'Наблюдать',\n    'watch_title_default' => 'Свойства по умолчанию',\n    'watch_desc_default' => 'Вернуть просмотр только ваших настроек уведомлений по умолчанию.',\n    'watch_title_ignore' => 'Игнорировать',\n    'watch_desc_ignore' => 'Игнорировать все уведомления, включая уведомления из пользовательского уровня.',\n    'watch_title_new' => 'Новые страницы',\n    'watch_desc_new' => 'Уведомлять при создании новой страницы внутри этого элемента.',\n    'watch_title_updates' => 'Все обновления страницы',\n    'watch_desc_updates' => 'Уведомлять обо всех новых страницах и изменениях страницы.',\n    'watch_desc_updates_page' => 'Уведомлять о всех изменениях страницы.',\n    'watch_title_comments' => 'Все обновления и комментарии страниц',\n    'watch_desc_comments' => 'Уведомлять обо всех новых страницах, изменениях страниц и новых комментариях.',\n    'watch_desc_comments_page' => 'Уведомлять об изменениях страниц и новых комментариях.',\n    'watch_change_default' => 'Изменить настройки уведомлений по умолчанию',\n    'watch_detail_ignore' => 'Игнорирование уведомлений',\n    'watch_detail_new' => 'Наблюдение за новыми страницами',\n    'watch_detail_updates' => 'Просмотр новых страниц и обновлений',\n    'watch_detail_comments' => 'Просмотр новых страниц, обновлений и комментариев',\n    'watch_detail_parent_book' => 'Просмотр через родительскую книгу',\n    'watch_detail_parent_book_ignore' => 'Игнорирование через родительскую книгу',\n    'watch_detail_parent_chapter' => 'Просмотр через родительскую главу',\n    'watch_detail_parent_chapter_ignore' => 'Игнорирование через родительскую главу',\n];\n"
  },
  {
    "path": "lang/ru/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'У вас нет доступа к запрашиваемой странице.',\n    'permissionJson' => 'У вас нет разрешения для запрашиваемого действия.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Пользователь с электронной почтой :email уже существует, но с другими учетными данными.',\n    'auth_pre_register_theme_prevention' => 'Пользователь не может быть зарегистрирован по предоставленной информации',\n    'email_already_confirmed' => 'Адрес электронной почты уже был подтвержден, попробуйте войти в систему.',\n    'email_confirmation_invalid' => 'Этот токен подтверждения недействителен или уже используется. Повторите попытку регистрации.',\n    'email_confirmation_expired' => 'Истек срок действия токена. Отправлено новое письмо с подтверждением.',\n    'email_confirmation_awaiting' => 'Для используемой учетной записи необходимо подтвердить адрес электронной почты',\n    'ldap_fail_anonymous' => 'Недопустимый доступ LDAP с использованием анонимной привязки',\n    'ldap_fail_authed' => 'Не удалось получить доступ к LDAP, используя данные dn & password',\n    'ldap_extension_not_installed' => 'LDAP расширение для PHP не установлено',\n    'ldap_cannot_connect' => 'Не удается подключиться к серверу LDAP, не удалось выполнить начальное соединение',\n    'saml_already_logged_in' => 'Уже вошли в систему',\n    'saml_no_email_address' => 'Не удалось найти email для этого пользователя в данных, предоставленных внешней системой аутентификации',\n    'saml_invalid_response_id' => 'Запрос от внешней системы аутентификации не распознается процессом, запущенным этим приложением. Переход назад после входа в систему может вызвать эту проблему.',\n    'saml_fail_authed' => 'Вход с помощью :system не удался, система не предоставила успешную авторизацию',\n    'oidc_already_logged_in' => 'Вход в систему уже произведен',\n    'oidc_no_email_address' => 'Не удалось найти email этого пользователя в данных, предоставленных внешней системой аутентификации',\n    'oidc_fail_authed' => 'Вход в систему с помощью :system не удался, система не обеспечила успешную авторизацию',\n    'social_no_action_defined' => 'Действие не определено',\n    'social_login_bad_response' => \"При попытке входа с :socialAccount произошла ошибка: \\\\n:error\",\n    'social_account_in_use' => 'Этот :socialAccount аккаунт уже используется, попробуйте войти с параметрами :socialAccount.',\n    'social_account_email_in_use' => 'Электронный ящик :email уже используется. Если у вас уже есть учетная запись, вы можете подключить свою учетную запись :socialAccount из настроек своего профиля.',\n    'social_account_existing' => 'Этот :socialAccount уже привязан к вашему профилю.',\n    'social_account_already_used_existing' => 'Этот :socialAccount уже используется другим пользователем.',\n    'social_account_not_used' => 'Этот :socialAccount не связан ни с какими пользователями. Прикрепите его в настройках вашего профиля.',\n    'social_account_register_instructions' => 'Если у вас еще нет учетной записи, вы можете зарегистрироваться, используя параметр :socialAccount.',\n    'social_driver_not_found' => 'Драйвер для Соцсети не найден',\n    'social_driver_not_configured' => 'Настройки вашего :socialAccount заданы неправильно.',\n    'invite_token_expired' => 'Срок действия приглашения истек. Вместо этого вы можете попытаться сбросить пароль своей учетной записи.',\n    'login_user_not_found' => 'Пользователь для этого действия не найден.',\n\n    // System\n    'path_not_writable' => 'Невозможно загрузить файл по пути :filePath. Убедитесь что сервер доступен для записи.',\n    'cannot_get_image_from_url' => 'Не удается получить изображение из :url',\n    'cannot_create_thumbs' => 'Сервер не может создавать эскизы. Убедитесь, что у вас установлено расширение GD PHP.',\n    'server_upload_limit' => 'Сервер не разрешает загрузку файлов такого размера. Попробуйте уменьшить размер файла.',\n    'server_post_limit' => 'Сервер не может получить указанный объем данных. Повторите попытку, используя меньшее количество данных или файл меньшего размера.',\n    'uploaded'  => 'Сервер не позволяет загружать файлы такого размера. Пожалуйста, попробуйте файл меньше.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Произошла ошибка при загрузке изображения',\n    'image_upload_type_error' => 'Неправильный тип загружаемого изображения',\n    'image_upload_replace_type' => 'Замена файла изображения должна быть того же типа',\n    'image_upload_memory_limit' => 'Не удалось выполнить загрузку изображений и/или создать эскизы из-за ограничения системных ресурсов.',\n    'image_thumbnail_memory_limit' => 'Не удалось создать вариации размера изображения из-за ограничений системных ресурсов.',\n    'image_gallery_thumbnail_memory_limit' => 'Не удалось создать эскизы галереи из-за ограниченности системных ресурсов.',\n    'drawing_data_not_found' => 'Данные чертежа не могут быть загружены. Возможно, файл чертежа больше не существует или у вас нет разрешения на доступ к нему.',\n\n    // Attachments\n    'attachment_not_found' => 'Вложение не найдено',\n    'attachment_upload_error' => 'Произошла ошибка при загрузке вложенного файла',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Не удалось сохранить черновик. Перед сохранением этой страницы убедитесь, что у вас есть подключение к Интернету.',\n    'page_draft_delete_fail' => 'Не удалось удалить черновик страницы и получить текущее сохраненное содержимое страницы',\n    'page_custom_home_deletion' => 'Невозможно удалить страницу, пока она установлена как домашняя страница',\n\n    // Entities\n    'entity_not_found' => 'Объект не найден',\n    'bookshelf_not_found' => 'Полка не найдена',\n    'book_not_found' => 'Книга не найдена',\n    'page_not_found' => 'Страница не найдена',\n    'chapter_not_found' => 'Глава не найдена',\n    'selected_book_not_found' => 'Выбранная книга не найдена',\n    'selected_book_chapter_not_found' => 'Выбранная книга или глава не найдена',\n    'guests_cannot_save_drafts' => 'Гости не могут сохранять черновики',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Вы не можете удалить единственного администратора',\n    'users_cannot_delete_guest' => 'Вы не можете удалить гостевого пользователя',\n    'users_could_not_send_invite' => 'Could not create user since invite email failed to send',\n\n    // Roles\n    'role_cannot_be_edited' => 'Эта роль не может быть изменена',\n    'role_system_cannot_be_deleted' => 'Эта роль является системной и не может быть удалена',\n    'role_registration_default_cannot_delete' => 'Эта роль не может быть удалена, так как она установлена в качестве роли по умолчанию',\n    'role_cannot_remove_only_admin' => 'Этот пользователь единственный с правами администратора. Назначьте роль администратора другому пользователю, прежде чем удалить этого.',\n\n    // Comments\n    'comment_list' => 'Произошла ошибка при получении комментариев.',\n    'cannot_add_comment_to_draft' => 'Вы не можете добавлять комментарии к черновику.',\n    'comment_add' => 'Произошла ошибка при добавлении / обновлении комментария.',\n    'comment_delete' => 'Произошла ошибка при удалении комментария.',\n    'empty_comment' => 'Нельзя добавить пустой комментарий.',\n\n    // Error pages\n    '404_page_not_found' => 'Страница не найдена',\n    'sorry_page_not_found' => 'Извините, страница, которую вы искали, не найдена.',\n    'sorry_page_not_found_permission_warning' => 'Если вы ожидали что страница существует, возможно у вас нет прав для её просмотра.',\n    'image_not_found' => 'Изображение не найдено',\n    'image_not_found_subtitle' => 'К сожалению, файл изображения, который вы искали, не найден.',\n    'image_not_found_details' => 'Возможно данное изображение было удалено.',\n    'return_home' => 'вернуться на главную страницу',\n    'error_occurred' => 'Произошла ошибка',\n    'app_down' => ':appName в данный момент не доступно',\n    'back_soon' => 'Скоро восстановится.',\n\n    // Import\n    'import_zip_cant_read' => 'Could not read ZIP file.',\n    'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',\n    'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Import ZIP failed to validate with errors:',\n    'import_zip_failed_notification' => 'Failed to import ZIP file.',\n    'import_perms_books' => 'У вас недостаточно прав для создания книг.',\n    'import_perms_chapters' => 'У вас недостаточно прав для создания глав.',\n    'import_perms_pages' => 'У вас недостаточно прав для создания страниц.',\n    'import_perms_images' => 'У вас недостаточно прав для создания изображений.',\n    'import_perms_attachments' => 'У вас недостаточно прав для создания вложений.',\n\n    // API errors\n    'api_no_authorization_found' => 'Отсутствует токен авторизации в запросе',\n    'api_bad_authorization_format' => 'Токен авторизации найден, но формат запроса неверен',\n    'api_user_token_not_found' => 'Отсутствует соответствующий API токен для предоставленного токена авторизации',\n    'api_incorrect_token_secret' => 'Секрет, предоставленный для данного использованного API токена неверен',\n    'api_user_no_api_permission' => 'Владелец используемого API токена не имеет прав на выполнение вызовов API',\n    'api_user_token_expired' => 'Срок действия используемого токена авторизации истек',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Ошибка при отправке тестового письма:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'URL-адрес не соответствует настроенным разрешенным хостам SSR',\n];\n"
  },
  {
    "path": "lang/ru/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Новый комментарий на странице: :pageName',\n    'new_comment_intro' => 'Пользователь прокомментировал страницу в :appName:',\n    'new_page_subject' => 'Новая страница: :pageName',\n    'new_page_intro' => 'Новая страница была создана в :appName:',\n    'updated_page_subject' => 'Обновлена страница: :pageName',\n    'updated_page_intro' => 'Страница была обновлена в :appName:',\n    'updated_page_debounce' => 'Чтобы предотвратить массовые уведомления, в течение некоторого времени вы не будете получать уведомления о дальнейших правках этой страницы этим же редактором.',\n    'comment_mention_subject' => 'Вы были упомянуты в комментарии на странице: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Имя страницы:',\n    'detail_page_path' => 'Путь страницы:',\n    'detail_commenter' => 'Комментатор:',\n    'detail_comment' => 'Комментарий:',\n    'detail_created_by' => 'Создано:',\n    'detail_updated_by' => 'Обновлено:',\n\n    'action_view_comment' => 'Просмотреть комментарий',\n    'action_view_page' => 'Посмотреть страницу',\n\n    'footer_reason' => 'Это уведомление было отправлено, потому что :link покрывает этот тип активности для этого элемента.',\n    'footer_reason_link' => 'ваши настройки уведомлений',\n];\n"
  },
  {
    "path": "lang/ru/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Предыдущая',\n    'next'     => 'Следующая &raquo;',\n\n];\n"
  },
  {
    "path": "lang/ru/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Пароль должен содержать не менее восьми символов и совпадать с подтверждением.',\n    'user' => \"Пользователя с данным адресом электронной почты не существует.\",\n    'token' => 'Токен сброса пароля недействителен для этого адреса электронной почты.',\n    'sent' => 'Ссылка для сброса пароля отправлена на вашу почту!',\n    'reset' => 'Ваш пароль был сброшен!',\n\n];\n"
  },
  {
    "path": "lang/ru/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Моя учетная запись',\n\n    'shortcuts' => 'Горячие клавиши',\n    'shortcuts_interface' => 'Настройки горячих клавиш',\n    'shortcuts_toggle_desc' => 'Здесь вы можете включить или отключить горячие клавиши системного интерфейса, используемые для навигации и действий.',\n    'shortcuts_customize_desc' => 'Вы можете настроить каждую из горячих клавиш ниже. Просто нажмите комбинацию клавиш после выбора вставки для горячих клавиш.',\n    'shortcuts_toggle_label' => 'Горячие клавиши включены',\n    'shortcuts_section_navigation' => 'Навигация',\n    'shortcuts_section_actions' => 'Общие действия',\n    'shortcuts_save' => 'Сохранить горячие клавиши',\n    'shortcuts_overlay_desc' => 'Примечание: Когда горячие клавиши включены, вспомогательное наложение доступно через нажатие \"?\", которая будет подсвечивать доступные горячие клавиши для действий, видимых в настоящее время на экране.',\n    'shortcuts_update_success' => 'Настройки горячих клавиш были обновлены!',\n    'shortcuts_overview_desc' => 'Управление клавишами быстрого доступа, которые можно использовать для навигации по системному интерфейсу.',\n\n    'notifications' => 'Настройки уведомлений',\n    'notifications_desc' => 'Управляйте полученными по электронной почте уведомлениями при выполнении определенных действий в системе.',\n    'notifications_opt_own_page_changes' => 'Уведомлять об изменениях в собственных страницах',\n    'notifications_opt_own_page_comments' => 'Уведомлять о комментариях на собственных страницах',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Уведомлять об ответах на мои комментарии',\n    'notifications_save' => 'Сохранить настройки',\n    'notifications_update_success' => 'Настройки уведомлений были обновлены!',\n    'notifications_watched' => 'Просмотренные и игнорированные элементы',\n    'notifications_watched_desc' => 'Ниже приведены элементы, которые имеют пользовательские настройки наблюдения. Чтобы обновить ваши предпочтения, посмотрите этот пункт и найдите варианты наблюдения в боковой панели.',\n\n    'auth' => 'Доступ и безопасность',\n    'auth_change_password' => 'Изменить пароль',\n    'auth_change_password_desc' => 'Установите пароль для входа в приложение. Длина пароля должна быть не менее 8 символов.',\n    'auth_change_password_success' => 'Пароль был обновлен!',\n\n    'profile' => 'Детали профиля',\n    'profile_desc' => 'Управляйте деталями вашей учетной записи, что представляют вас другим пользователям, в дополнение к деталям, используемым для персонализации коммуникации и системы.',\n    'profile_view_public' => 'Просмотреть публичный профиль',\n    'profile_name_desc' => 'Настройте отображаемое имя, видимое другим пользователям системы через действия, что вы выполняете, и контент, которым вы владеете.',\n    'profile_email_desc' => 'Этот адрес электронной почты будет использоваться для уведомлений и, в зависимости от активной системы аутентификации, для доступа к системе.',\n    'profile_email_no_permission' => 'К сожалению, у вас нет разрешения на изменение адреса электронной почты. Если вам действительно необходимо его изменить, нужно попросить администратора сделать это.',\n    'profile_avatar_desc' => 'Выберите изображение, которое будет использоваться для представления себя другим в системе. По возможности это изображение должно быть квадратным и около 256 пикселей по ширине и высоте.',\n    'profile_admin_options' => 'Административные параметры',\n    'profile_admin_options_desc' => 'Дополнительные параметры уровня администратора, такие как управление назначением ролей, могут быть найдены для вашей учетной записи в области \"Настройки > Пользователи\".',\n\n    'delete_account' => 'Удалить аккаунт',\n    'delete_my_account' => 'Удалить мой аккаунт',\n    'delete_my_account_desc' => 'Это полностью удалит вашу учетную запись из системы. Вы не сможете отменить это действие или восстановить эту учетную запись. Созданный вами контент, такой как созданные страницы и загруженные изображения, останется без изменений.',\n    'delete_my_account_warning' => 'Вы уверены, что хотите удалить свой аккаунт?',\n];\n"
  },
  {
    "path": "lang/ru/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Настройки',\n    'settings_save' => 'Сохранить настройки',\n    'system_version' => 'Версия системы',\n    'categories' => 'Категории',\n\n    // App Settings\n    'app_customization' => 'Настройки',\n    'app_features_security' => 'Функционал и безопасность',\n    'app_name' => 'Название приложения',\n    'app_name_desc' => 'Название отображается в заголовках и сообщениях электронной почты отправленных системой.',\n    'app_name_header' => 'Отображать название приложения в заголовке',\n    'app_public_access' => 'Публичный доступ',\n    'app_public_access_desc' => 'Включение этой опции позволит неавторизованным посетителям получить доступ к содержимому вашего BookStack.',\n    'app_public_access_desc_guest' => 'Публичный доступ контролируется через настройки пользователя \"Guest\"',\n    'app_public_access_toggle' => 'Разрешить публичный доступ',\n    'app_public_viewing' => 'Разрешить публичный просмотр?',\n    'app_secure_images' => 'Загрузка изображений с высоким уровнем безопасности',\n    'app_secure_images_toggle' => 'Включить загрузку изображений с высоким уровнем безопасности',\n    'app_secure_images_desc' => 'Для высокой производительности все изображения являются общедоступными. Этот параметр добавляет случайную строку перед URL изображения. Убедитесь, что индексация каталогов отключена, для предотвращения легкого доступа.',\n    'app_default_editor' => 'Редактор страниц по умолчанию',\n    'app_default_editor_desc' => 'Выберите, какой редактор будет использоваться по умолчанию при редактировании новых страниц. Это может быть переопределено на уровне страницы, где разрешены права.',\n    'app_custom_html' => 'Пользовательский контент заголовка HTML',\n    'app_custom_html_desc' => 'Любой контент, добавленный здесь, будет вставлен в нижнюю часть раздела <head> каждой страницы. Это удобно для переопределения стилей или добавления кода аналитики.',\n    'app_custom_html_disabled_notice' => 'Пользовательский контент заголовка HTML отключен на этой странице, чтобы гарантировать отмену любых критических изменений.',\n    'app_logo' => 'Логотип приложения',\n    'app_logo_desc' => 'Используется в строке заголовка приложения, среди прочих областей. Это изображение должно быть 86px в высоте. Большие изображения будут масштабироваться вниз.',\n    'app_icon' => 'Иконка приложения',\n    'app_icon_desc' => 'Эта иконка используется для браузерных вкладок и иконок ярлыков. Должно быть 256px квадратное PNG изображение.',\n    'app_homepage' => 'Стартовая страница приложения',\n    'app_homepage_desc' => 'Выберите страницу, которая будет отображаться на главной странице вместо стандартной. Права на страницы игнорируются для выбранных страниц.',\n    'app_homepage_select' => 'Выберите страницу',\n    'app_footer_links' => 'Ссылки в нижней части страницы',\n    'app_footer_links_desc' => 'Добавьте ссылки для отображения в нижнем колонтитуле сайта. Они будут отображаться в нижней части большинства страниц, включая те, которые не требуют входа. Вы можете использовать метку \"trans::<key>\" для использования системных переводов. Например: Использование \"trans::common.privacy_policy\" обеспечит перевод текста \"Политика конфиденциальности\" и \"trans:common.terms_of_service\" предоставит переведенный текст \"Правила использования\".',\n    'app_footer_links_label' => 'Название ссылки',\n    'app_footer_links_url' => 'Адрес ссылки',\n    'app_footer_links_add' => 'Добавить ссылку',\n    'app_disable_comments' => 'Отключение комментариев',\n    'app_disable_comments_toggle' => 'Отключить комментарии',\n    'app_disable_comments_desc' => 'Отключение комментариев на всех страницах. Существующие комментарии будут скрыты.',\n\n    // Color settings\n    'color_scheme' => 'Цветовая схема приложения',\n    'color_scheme_desc' => 'Установите цвета для использования в пользовательском интерфейсе. Цвета могут быть настроены отдельно для темных и светлых режимов, чтобы наилучшим образом соответствовать теме и обеспечить разборчивость.',\n    'ui_colors_desc' => 'Задайте основной цвет приложения и цвет ссылок по умолчанию. Основной цвет используется в основном для баннера заголовка, кнопок и декораций интерфейса. Цвет ссылок по умолчанию используется для текстовых ссылок и действий как в письменном содержании, так и в прикладном интерфейсе.',\n    'app_color' => 'Основной цвет',\n    'link_color' => 'Цвет ссылки',\n    'content_colors_desc' => 'Задает цвета для всех элементов организационной иерархии страницы. Для удобства чтения рекомендуется выбирать цвета, яркость которых близка к цветам по умолчанию.',\n    'bookshelf_color' => 'Цвет полки',\n    'book_color' => 'Цвет книги',\n    'chapter_color' => 'Цвет главы',\n    'page_color' => 'Цвет страницы',\n    'page_draft_color' => 'Цвет черновика страницы',\n\n    // Registration Settings\n    'reg_settings' => 'Настройки регистрации',\n    'reg_enable' => 'Разрешить регистрацию',\n    'reg_enable_toggle' => 'Разрешить регистрацию',\n    'reg_enable_desc' => 'Если регистрация разрешена, пользователь сможет зарегистрироваться в системе самостоятельно. При регистрации назначается роль пользователя по умолчанию.',\n    'reg_default_role' => 'Роль пользователя по умолчанию после регистрации',\n    'reg_enable_external_warning' => 'Вышеуказанный параметр игнорируется, пока активна внешняя аутентификация LDAP или SAML. Учетные записи для несуществующих пользователей будут создаваться автоматически при условии успешной аутентификации на внешнем сервере.',\n    'reg_email_confirmation' => 'Подтверждение электронной почты',\n    'reg_email_confirmation_toggle' => 'Требовать подтверждение по электронной почте',\n    'reg_confirm_email_desc' => 'При использовании ограничения по домену - подтверждение обязательно, этот пункт игнорируется.',\n    'reg_confirm_restrict_domain' => 'Ограничить регистрацию по домену',\n    'reg_confirm_restrict_domain_desc' => 'Введите список доменов почты через запятую, для которых разрешена регистрация. Пользователям будет отправлено письмо для подтверждения адреса перед входом в приложение. <br> Обратите внимание, что пользователи смогут изменить свой адрес после регистрации.',\n    'reg_confirm_restrict_domain_placeholder' => 'Без ограничений',\n\n    // Sorting Settings\n    'sorting' => 'Списки и сортировка',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Выберите правило сортировки по умолчанию для новых книг. Это не повлияет на существующие книги, и может быть изменено для каждой книги отдельно.',\n    'sorting_rules' => 'Правила сортировки',\n    'sorting_rules_desc' => 'Выберите правило сортировки по умолчанию для новых книг. Это не повлияет на существующие книги и может быть изменено для каждой книги отдельно.',\n    'sort_rule_assigned_to_x_books' => 'Используется в :count книгах',\n    'sort_rule_create' => 'Создать правило сортировки',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Удалить правило сортировки',\n    'sort_rule_delete_desc' => 'Удалить это правило сортировки из системы. Книги, использующие эту сортировку, вернутся к ручной сортировке.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'Это правило сортировки используется по умолчанию для книг. Вы уверены, что хотите удалить его?',\n    'sort_rule_details' => 'Детали правила сортировки',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Доступные операции',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Перетащите/добавьте операции из списка \"Доступные операции\"',\n    'sort_rule_op_asc' => '(Возрастание)',\n    'sort_rule_op_desc' => '(Убывание)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'По нумерации',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Главы в начале',\n    'sort_rule_op_chapters_last' => 'Главы в конце',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Обслуживание',\n    'maint_image_cleanup' => 'Очистка изображений',\n    'maint_image_cleanup_desc' => 'Сканирует содержимое страниц и предыдущих версий и определяет изображения, которые не используются. Убедитесь, что у вас есть резервная копия базы данных и папки изображений перед запуском этой функции.',\n    'maint_delete_images_only_in_revisions' => 'Также удалять изображения, которые существуют только в старой версии страницы',\n    'maint_image_cleanup_run' => 'Выполнить очистку',\n    'maint_image_cleanup_warning' => 'Найдено :count возможно бесполезных изображений. Вы уверены, что хотите удалить эти изображения?',\n    'maint_image_cleanup_success' => ':count возможно бесполезных изображений было найдено и удалено!',\n    'maint_image_cleanup_nothing_found' => 'Не найдено ни одного бесполезного изображения!',\n    'maint_send_test_email' => 'Отправить тестовое письмо',\n    'maint_send_test_email_desc' => 'Отправить тестовое письмо на адрес электронной почты, указанный в профиле.',\n    'maint_send_test_email_run' => 'Отправить письмо',\n    'maint_send_test_email_success' => 'Письмо отправлено на :address',\n    'maint_send_test_email_mail_subject' => 'Проверка электронной почты',\n    'maint_send_test_email_mail_greeting' => 'Доставка электронной почты работает!',\n    'maint_send_test_email_mail_text' => 'Поздравляем! Поскольку вы получили это письмо, электронная почта настроена правильно.',\n    'maint_recycle_bin_desc' => 'Удаленные полки, книги, главы и страницы отправляются в корзину, чтобы они могли быть восстановлены или удалены навсегда. Более старые элементы в корзине могут быть автоматически удалены через некоторое время в зависимости от системной конфигурации.',\n    'maint_recycle_bin_open' => 'Открыть корзину',\n    'maint_regen_references' => 'Пересоздать ссылки',\n    'maint_regen_references_desc' => 'Это действие восстановит перекрёстный справочный индекс в базе данных. Обычно это действие обрабатывается автоматически, но оно может быть полезно для индексации старого контента или контента, добавляемого неофициальными методами.',\n    'maint_regen_references_success' => 'Справочный индекс был обновлен!',\n    'maint_timeout_command_note' => 'Примечание: Это действие может занять время для запуска, что может привести к возникновению проблем в некоторых web-средах. В качестве альтернативы, это действие можно выполнить с помощью терминальной команды.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Корзина',\n    'recycle_bin_desc' => 'Здесь вы можете восстановить удаленные элементы или навсегда удалить их из системы. Этот список не отфильтрован в отличие от аналогичных списков действий в системе, где применяются фильтры.',\n    'recycle_bin_deleted_item' => 'Удаленный элемент',\n    'recycle_bin_deleted_parent' => 'Родительский объект',\n    'recycle_bin_deleted_by' => 'Удалён',\n    'recycle_bin_deleted_at' => 'Время удаления',\n    'recycle_bin_permanently_delete' => 'Удалить навсегда',\n    'recycle_bin_restore' => 'Восстановить',\n    'recycle_bin_contents_empty' => 'На данный момент корзина пуста',\n    'recycle_bin_empty' => 'Очистить корзину',\n    'recycle_bin_empty_confirm' => 'Это действие навсегда уничтожит все элементы в корзине, включая содержимое, содержащееся в каждом элементе. Вы уверены, что хотите очистить корзину?',\n    'recycle_bin_destroy_confirm' => 'Это действие навсегда удалит этот элемент из системы, вместе с любыми дочерними элементами, перечисленными ниже, и вы не сможете восстановить этот контент. Вы уверены, что хотите навсегда удалить этот элемент?',\n    'recycle_bin_destroy_list' => 'Элементы для удаления',\n    'recycle_bin_restore_list' => 'Элементы для восстановления',\n    'recycle_bin_restore_confirm' => 'Это действие восстановит удаленный элемент, включая дочерние, в исходное место. Если исходное место было удалено и теперь находится в корзине, родительский элемент также необходимо будет восстановить.',\n    'recycle_bin_restore_deleted_parent' => 'Родитель этого элемента также был удален. Элементы будут удалены до тех пор, пока этот родитель не будет восстановлен.',\n    'recycle_bin_restore_parent' => 'Восстановить родительский объект',\n    'recycle_bin_destroy_notification' => 'Удалено :count элементов из корзины.',\n    'recycle_bin_restore_notification' => 'Восстановлено :count элементов из корзины',\n\n    // Audit Log\n    'audit' => 'Журнал аудита',\n    'audit_desc' => 'Этот журнал аудита отображает список действий, отслеживаемых в системе. Этот список не отфильтрован в отличие от аналогичных списков действий в системе, где применяются фильтры.',\n    'audit_event_filter' => 'Фильтр событий',\n    'audit_event_filter_no_filter' => 'Без фильтра',\n    'audit_deleted_item' => 'Удаленный элемент',\n    'audit_deleted_item_name' => 'Имя: :name',\n    'audit_table_user' => 'Пользователь',\n    'audit_table_event' => 'Событие',\n    'audit_table_related' => 'Связанный элемент',\n    'audit_table_ip' => 'IP-адрес',\n    'audit_table_date' => 'Дата действия',\n    'audit_date_from' => 'Диапазон даты от',\n    'audit_date_to' => 'Диапазон даты до',\n\n    // Role Settings\n    'roles' => 'Роли',\n    'role_user_roles' => 'Роли пользователей',\n    'roles_index_desc' => 'Роли используются для группировки пользователей и предоставления системных разрешений их участникам. Когда пользователь является членом нескольких ролей, предоставленные разрешения объединяются, и пользователь наследует все возможности.',\n    'roles_x_users_assigned' => ':count пользователь назначен|:count назначенных пользователей',\n    'roles_x_permissions_provided' => ':count разрешение|:count разрешений',\n    'roles_assigned_users' => 'Назначенные пользователи',\n    'roles_permissions_provided' => 'Предоставленные разрешения',\n    'role_create' => 'Добавить роль',\n    'role_delete' => 'Удалить роль',\n    'role_delete_confirm' => 'Это удалит роль с именем \\':roleName\\'.',\n    'role_delete_users_assigned' => 'Эта роль назначена :userCount пользователям. Если вы хотите перенести их, выберите новую роль ниже.',\n    'role_delete_no_migration' => \"Не переносить пользователей\",\n    'role_delete_sure' => 'Вы уверены что хотите удалить данную роль?',\n    'role_edit' => 'Редактировать роль',\n    'role_details' => 'Детали роли',\n    'role_name' => 'Название роли',\n    'role_desc' => 'Краткое описание роли',\n    'role_mfa_enforced' => 'Требует многофакторной аутентификации',\n    'role_external_auth_id' => 'Внешние ID авторизации',\n    'role_system' => 'Системные разрешения',\n    'role_manage_users' => 'Управление пользователями',\n    'role_manage_roles' => 'Управление ролями и правами на роли',\n    'role_manage_entity_permissions' => 'Управление правами на все книги, главы и страницы',\n    'role_manage_own_entity_permissions' => 'Управление разрешениями для собственных книг, глав и страниц',\n    'role_manage_page_templates' => 'Управление шаблонами страниц',\n    'role_access_api' => 'Доступ к системному API',\n    'role_manage_settings' => 'Управление настройками приложения',\n    'role_export_content' => 'Экспорт контента',\n    'role_import_content' => 'Import content',\n    'role_editor_change' => 'Изменение редактора страниц',\n    'role_notifications' => 'Получение и управление уведомлениями',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Права доступа к материалам',\n    'roles_system_warning' => 'Имейте в виду, что доступ к любому из указанных выше трех разрешений может позволить пользователю изменить свои собственные привилегии или привилегии других пользователей системы. Назначать роли с этими правами можно только доверенным пользователям.',\n    'role_asset_desc' => 'Эти разрешения контролируют доступ по умолчанию к параметрам внутри системы. Разрешения на книги, главы и страницы перезапишут эти разрешения.',\n    'role_asset_admins' => 'Администраторы автоматически получают доступ ко всему контенту, но эти опции могут отображать или скрывать параметры пользовательского интерфейса.',\n    'role_asset_image_view_note' => 'Это относится к видимости в менеджере изображений. Фактический доступ к загруженным файлам изображений будет зависеть от опции хранения системных изображений.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'Все',\n    'role_own' => 'Владелец',\n    'role_controlled_by_asset' => 'Контролируется активом, в который они загружены',\n    'role_save' => 'Сохранить роль',\n    'role_users' => 'Пользователи с данной ролью',\n    'role_users_none' => 'Нет пользователей с данной ролью',\n\n    // Users\n    'users' => 'Пользователи',\n    'users_index_desc' => 'Создание и управление индивидуальными учетными записями пользователей в системе. Учетные записи пользователя используются для входа и атрибуции контента и активности. Разрешения доступа в первую очередь основываются на роли, но владельцы контента могут влиять на разрешения и доступ.',\n    'user_profile' => 'Профиль пользователя',\n    'users_add_new' => 'Добавить пользователя',\n    'users_search' => 'Поиск пользователей',\n    'users_latest_activity' => 'Последние действия',\n    'users_details' => 'Данные пользователя',\n    'users_details_desc' => 'Укажите имя и адрес электронной почты для этого пользователя. Адрес электронной почты будет использоваться для входа в приложение.',\n    'users_details_desc_no_email' => 'Задайте имя для этого пользователя, чтобы другие могли его узнать.',\n    'users_role' => 'Роли пользователя',\n    'users_role_desc' => 'Назначьте роли пользователю. Если назначено несколько ролей, разрешения будут суммироваться и пользователь получит все права назначенных ролей.',\n    'users_password' => 'Пароль пользователя',\n    'users_password_desc' => 'Установите пароль для входа в приложение. Длина пароля должна быть не менее 8 символов.',\n    'users_send_invite_text' => 'Вы можете отправить этому пользователю письмо с приглашением, которое позволит ему установить пароль самостоятельно или задайте пароль сами.',\n    'users_send_invite_option' => 'Отправить пользователю письмо с приглашением',\n    'users_external_auth_id' => 'Внешний ID аутентификации',\n    'users_external_auth_id_desc' => 'Когда используется внешняя система аутентификации (например, SAML2, OIDC или LDAP), этот идентификатор будет использоваться для связывания пользователя BookStack с учетной записью системы аутентификации. Вы можете игнорировать это поле, если используете стандартную аутентификацию по электронной почте.',\n    'users_password_warning' => 'Заполните поля ниже только если вы хотите изменить пароль.',\n    'users_system_public' => 'Этот пользователь представляет любых гостевых пользователей, которые посещают ваше приложение. Он не может использоваться для входа в систему и назначается автоматически.',\n    'users_delete' => 'Удалить пользователя',\n    'users_delete_named' => 'Удалить пользователя :userName',\n    'users_delete_warning' => 'Это полностью удалит пользователя \\':userName\\' из системы.',\n    'users_delete_confirm' => 'Вы уверены что хотите удалить этого пользователя?',\n    'users_migrate_ownership' => 'Наследник контента',\n    'users_migrate_ownership_desc' => 'Выберите пользователя, если вы хотите, чтобы он стал владельцем всех элементов, в настоящее время принадлежащих удаляемому пользователю.',\n    'users_none_selected' => 'Пользователь не выбран',\n    'users_edit' => 'Редактировать пользователя',\n    'users_edit_profile' => 'Редактировать профиль',\n    'users_avatar' => 'Аватар пользователя',\n    'users_avatar_desc' => 'Выберите изображение. Изображение должно быть квадратным, размером около 256px.',\n    'users_preferred_language' => 'Предпочитаемый язык',\n    'users_preferred_language_desc' => 'Этот параметр изменит язык интерфейса приложения. Это не влияет на созданный пользователем контент.',\n    'users_social_accounts' => 'Аккаунты социальных сетей',\n    'users_social_accounts_desc' => 'Просмотр статуса подключенных социальных учетных записей для этого пользователя. Учетные записи социальных сетей могут использоваться в дополнение к системе первичной аутентификации для доступа к системе.',\n    'users_social_accounts_info' => 'Здесь вы можете подключить другие учетные записи для более быстрого и легкого входа в систему. Отключение учетной записи здесь не возможно. Отмените доступ к настройкам вашего профиля в подключенном социальном аккаунте.',\n    'users_social_connect' => 'Подключить аккаунт',\n    'users_social_disconnect' => 'Отключить аккаунт',\n    'users_social_status_connected' => 'Подключен',\n    'users_social_status_disconnected' => 'Отключен',\n    'users_social_connected' => ':socialAccount аккаунт успешно подключен к вашему профилю.',\n    'users_social_disconnected' => ':socialAccount аккаунт успешно отключен от вашего профиля.',\n    'users_api_tokens' => 'API токены',\n    'users_api_tokens_desc' => 'Создание и управление токенами доступа, используемыми для аутентификации с помощью BookStack REST API. Разрешения для API управляются пользователем, которому принадлежит токен.',\n    'users_api_tokens_none' => 'Для этого пользователя не создано API токенов',\n    'users_api_tokens_create' => 'Создать токен',\n    'users_api_tokens_expires' => 'Истекает',\n    'users_api_tokens_docs' => 'Документация',\n    'users_mfa' => 'Многофакторная аутентификация',\n    'users_mfa_desc' => 'Многофакторная аутентификация повышает степень безопасности вашей учетной записи.',\n    'users_mfa_x_methods' => 'методов настроено :count|методов сконфигурировано :count',\n    'users_mfa_configure' => 'Настройка методов',\n\n    // API Tokens\n    'user_api_token_create' => 'Создать токен',\n    'user_api_token_name' => 'Имя',\n    'user_api_token_name_desc' => 'Присвойте вашему токену читаемое имя, в качестве напоминания о его назначении в будущем.',\n    'user_api_token_expiry' => 'Истекает',\n    'user_api_token_expiry_desc' => 'Установите дату истечения срока действия этого токена. После наступления даты запросы, сделанные с использованием данного токена, больше не будут работать. Если оставить это поле пустым, срок действия истечет через 100 лет.',\n    'user_api_token_create_secret_message' => 'Сразу после создания этого токена будут сгенерированы и отображены идентификатор токена и секретный ключ. Секретный ключ будет показан только один раз, поэтому перед продолжением обязательно скопируйте значение в безопасное и надежное место.',\n    'user_api_token' => 'API токен',\n    'user_api_token_id' => 'Идентификатор токена',\n    'user_api_token_id_desc' => 'Это нередактируемый системный идентификатор для этого токена, который необходимо указывать в запросах API.',\n    'user_api_token_secret' => 'Секретный ключ',\n    'user_api_token_secret_desc' => 'Это сгенерированный системой секретный ключ для этого токена, который необходимо будет указывать в запросах API. Он будет показан только один раз, поэтому скопируйте это значение в безопасное и надежное место.',\n    'user_api_token_created' => 'Токен создан :timeAgo',\n    'user_api_token_updated' => 'Токен обновлён :timeAgo',\n    'user_api_token_delete' => 'Удалить токен',\n    'user_api_token_delete_warning' => 'Это полностью удалит API токен с именем \\':tokenName\\' из системы.',\n    'user_api_token_delete_confirm' => 'Вы уверены, что хотите удалить этот API токен?',\n\n    // Webhooks\n    'webhooks' => 'Вебхуки',\n    'webhooks_index_desc' => 'Webhooks - это способ посылать данные на внешние URL-адреса при возникновении определенных действий и событий в системе, которые позволяют интегрировать события с внешними платформами, такими как системы обмена сообщениями или уведомлениями.',\n    'webhooks_x_trigger_events' => ':count событие триггера|:count событий триггера',\n    'webhooks_create' => 'Создать вебхук',\n    'webhooks_none_created' => 'Вебхуки еще не созданы.',\n    'webhooks_edit' => 'Редактировать вебхук',\n    'webhooks_save' => 'Сохранить вебхук',\n    'webhooks_details' => 'Детали вебхука',\n    'webhooks_details_desc' => 'Укажите удобное для пользователя название и адрес для отправки данных вебхука с помощью POST.',\n    'webhooks_events' => 'События вебхука',\n    'webhooks_events_desc' => 'Выберите все события, которые должны вызывать этот вебхук.',\n    'webhooks_events_warning' => 'Имейте в виду, что эти события будут срабатывать для всех выбранных событий, даже если применяются пользовательские разрешения. Убедитесь, что использование этого вебхука не будет раскрывать конфиденциальные данные.',\n    'webhooks_events_all' => 'Все системные события',\n    'webhooks_name' => 'Имя вебхука',\n    'webhooks_timeout' => 'Таймаут запроса Webhook (секунды)',\n    'webhooks_endpoint' => 'Конечная точка вебхука',\n    'webhooks_active' => 'Вебхук активен',\n    'webhook_events_table_header' => 'События',\n    'webhooks_delete' => 'Удалить вебхук',\n    'webhooks_delete_warning' => 'Это полностью удалит этот вебхук с названием \\':webhookName\\' из системы.',\n    'webhooks_delete_confirm' => 'Вы уверены, что хотите удалить этот вебхук?',\n    'webhooks_format_example' => 'Пример вебхука',\n    'webhooks_format_example_desc' => 'Данные вебхука отправляются как POST запрос к настроенной конечной точке в виде JSON в соответствии с форматом ниже. Свойства \"related_item\" и \"url\" необязательны и зависят от типа вызванного события.',\n    'webhooks_status' => 'Состояние Webhook',\n    'webhooks_last_called' => 'Последний вызов:',\n    'webhooks_last_errored' => 'Последняя ошибка:',\n    'webhooks_last_error_message' => 'Последнее сообщение об ошибке:',\n\n    // Licensing\n    'licenses' => 'Лицензии',\n    'licenses_desc' => 'Эта страница содержит сведения о лицензиях для BookStack в дополнение к проектам и библиотекам, которые используются в BookStack. Многие перечисленные проекты могут использоваться только в контексте разработки.',\n    'licenses_bookstack' => 'Лицензия BookStack',\n    'licenses_php' => 'Лицензии PHP библиотек',\n    'licenses_js' => 'Лицензии JavaScript библиотек',\n    'licenses_other' => 'Прочие лицензии',\n    'license_details' => 'Подробности о лицензии',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/ru/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute должен быть принят.',\n    'active_url'           => ':attribute не является корректным URL.',\n    'after'                => ':attribute дата должна быть позже :date.',\n    'alpha'                => ':attribute может содержать только буквы.',\n    'alpha_dash'           => ':attribute может содержать только буквы, цифры и тире.',\n    'alpha_num'            => ':attribute должен содержать только буквы и цифры.',\n    'array'                => ':attribute должен быть массивом.',\n    'backup_codes'         => 'Указанный код недействителен или уже использован.',\n    'before'               => ':attribute дата должна быть до :date.',\n    'between'              => [\n        'numeric' => ':attribute должен быть между :min и :max.',\n        'file'    => ':attribute должен быть между :min и :max килобайт.',\n        'string'  => 'длина :attribute должна быть между :min и :max символами.',\n        'array'   => ':attribute должен содержать не менее :min и не более :max элементов.',\n    ],\n    'boolean'              => ':attribute поле может быть только true или false.',\n    'confirmed'            => ':attribute подтверждение не совпадает.',\n    'date'                 => ':attribute некорректные данные.',\n    'date_format'          => ':attribute не соответствует формату :format.',\n    'different'            => ':attribute и :other должны быть различны.',\n    'digits'               => ':attribute должен состоять из :digits цифр.',\n    'digits_between'       => ':attribute должен иметь от :min до :max цифр.',\n    'email'                => ':attribute должен быть корректным email адресом.',\n    'ends_with' => ':attribute должен заканчиваться одним из следующих: :values',\n    'file'                 => ':attribute должен быть указан как допустимый файл.',\n    'filled'               => ':attribute поле необходимо.',\n    'gt'                   => [\n        'numeric' => 'Значение :attribute должно быть больше чем :value.',\n        'file'    => 'Значение :attribute должно быть больше :value килобайт.',\n        'string'  => 'Значение :attribute должно быть больше :value символов.',\n        'array'   => 'Значение :attribute должно содержать больше :value элементов.',\n    ],\n    'gte'                  => [\n        'numeric' => 'Значение :attribute должно быть больше или равно :value.',\n        'file'    => 'Значение :attribute должно быть больше или равно :value килобайт.',\n        'string'  => 'Значение :attribute должно быть больше или равно :value символам.',\n        'array'   => ':attribute должен иметь :value элементов или больше.',\n    ],\n    'exists'               => 'выделенный :attribute некорректен.',\n    'image'                => ':attribute должен быть изображением.',\n    'image_extension'      => ':attribute должен быть исправным  и содержать расширение картинки',\n    'in'                   => 'выделенный :attribute некорректен.',\n    'integer'              => ':attribute должно быть целое число.',\n    'ip'                   => ':attribute должен быть корректным IP адресом.',\n    'ipv4'                 => ':attribute должен быть корректным IPv4-адресом.',\n    'ipv6'                 => ':attribute должен быть корректным IPv6-адресом.',\n    'json'                 => ':attribute должен быть допустимой строкой JSON.',\n    'lt'                   => [\n        'numeric' => 'Значение :attribute должно быть меньше, чем :value.',\n        'file'    => 'Значение :attribute должно быть меньше :value килобайт.',\n        'string'  => 'Значение :attribute должно быть меньше :value символов.',\n        'array'   => 'Значение :attribute должно быть меньше :value элементов.',\n    ],\n    'lte'                  => [\n        'numeric' => 'Значение :attribute должно быть меньше или равно :value.',\n        'file'    => 'Значение :attribute должно быть меньше или равно :value килобайт.',\n        'string'  => 'Значение :attribute должно быть меньше или равно :value символам.',\n        'array'   => 'Поле :attribute не должно содержать больше :value элементов.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute не может быть больше чем :max.',\n        'file'    => ':attribute не может быть больше чем :max килобайт.',\n        'string'  => ':attribute не может быть больше чем :max символов.',\n        'array'   => ':attribute не может содержать больше чем :max элементов.',\n    ],\n    'mimes'                => ':attribute должен быть файлом с типом: :values.',\n    'min'                  => [\n        'numeric' => 'Поле :attribute должно быть не менее :min.',\n        'file'    => ':attribute должен быть минимум :min килобайт.',\n        'string'  => ':attribute должен быть минимум :min символов.',\n        'array'   => ':attribute должен содержать хотя бы :min элементов.',\n    ],\n    'not_in'               => 'Выбранный :attribute некорректен.',\n    'not_regex'            => 'Формат :attribute некорректен.',\n    'numeric'              => ':attribute должен быть числом.',\n    'regex'                => 'Формат :attribute некорректен.',\n    'required'             => ':attribute обязательное поле.',\n    'required_if'          => ':attribute обязательное поле когда :other со значением :value.',\n    'required_with'        => ':attribute обязательное поле когда :values установлено.',\n    'required_with_all'    => ':attribute обязательное поле когда :values установлены.',\n    'required_without'     => ':attribute обязательное поле когда :values не установлены.',\n    'required_without_all' => ':attribute обязательное поле когда ни одно из :values не установлены.',\n    'same'                 => ':attribute и :other должны совпадать.',\n    'safe_url'             => 'Предоставленная ссылка может быть небезопасной.',\n    'size'                 => [\n        'numeric' => ':attribute должен быть :size.',\n        'file'    => ':attribute должен быть :size килобайт.',\n        'string'  => ':attribute должен быть :size символов.',\n        'array'   => ':attribute должен содержать :size элементов.',\n    ],\n    'string'               => ':attribute должен быть строкой.',\n    'timezone'             => ':attribute должен быть корректным часовым поясом.',\n    'totp'                 => 'Указанный код недействителен или истек.',\n    'unique'               => ':attribute уже есть.',\n    'url'                  => 'Формат :attribute некорректен.',\n    'uploaded'             => 'Не удалось загрузить файл. Сервер не может принимать файлы такого размера.',\n\n    'zip_file' => 'The :attribute needs to reference a file within the ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',\n    'zip_model_expected' => 'Data object expected but \":type\" found.',\n    'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Требуется подтверждение пароля',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/sk/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'vytvoril(a) stránku',\n    'page_create_notification'    => 'Stránka úspešne vytvorená',\n    'page_update'                 => 'aktualizoval(a) stránku',\n    'page_update_notification'    => 'Stránka úspešne aktualizovaná',\n    'page_delete'                 => 'odstránil(a) stránku',\n    'page_delete_notification'    => 'Stránka úspešne odstránená',\n    'page_restore'                => 'obnovil(a) stránku',\n    'page_restore_notification'   => 'Stránka úspešne obnovená',\n    'page_move'                   => 'presunul(a) stránku',\n    'page_move_notification'      => 'Stránka bola úspešne presunutá',\n\n    // Chapters\n    'chapter_create'              => 'vytvoril(a) kapitolu',\n    'chapter_create_notification' => 'Kapitola úspešne vytvorená',\n    'chapter_update'              => 'aktualizoval(a) kapitolu',\n    'chapter_update_notification' => 'Kapitola úspešne aktualizovaná',\n    'chapter_delete'              => 'odstránil(a) kapitolu',\n    'chapter_delete_notification' => 'Kapitola úspešne odstránená',\n    'chapter_move'                => 'presunul(a) kapitolu',\n    'chapter_move_notification' => 'Kapitola bola úspešne presunutá',\n\n    // Books\n    'book_create'                 => 'vytvoril(a) knihu',\n    'book_create_notification'    => 'Kniha úspešne vytvorená',\n    'book_create_from_chapter'              => 'kapitola konvertovaná na knihu',\n    'book_create_from_chapter_notification' => 'Kapitola úspešne konvertovaná na knihu',\n    'book_update'                 => 'aktualizoval(a) knihu',\n    'book_update_notification'    => 'Kniha úspešne aktualizovaná',\n    'book_delete'                 => 'odstránil(a) knihu',\n    'book_delete_notification'    => 'Kniha úspešne odstránená',\n    'book_sort'                   => 'zoradil(a) knihu',\n    'book_sort_notification'      => 'Kniha úspešne znovu zoradená',\n\n    // Bookshelves\n    'bookshelf_create'            => 'vytvoril(a) policu',\n    'bookshelf_create_notification'    => 'Polica úspešne vytvorená',\n    'bookshelf_create_from_book'    => 'kniha bola prevedená na policu',\n    'bookshelf_create_from_book_notification'    => 'Kniha úspešne konvertovaná na poličku',\n    'bookshelf_update'                 => 'aktualizoval(a) policu',\n    'bookshelf_update_notification'    => 'Polica bola úspešne aktualizovaná',\n    'bookshelf_delete'                 => 'odstránená polica',\n    'bookshelf_delete_notification'    => 'Polica bola úspešne odstránená',\n\n    // Revisions\n    'revision_restore' => 'restored revision',\n    'revision_delete' => 'odstránil(a) revíziu',\n    'revision_delete_notification' => 'Revízia úspešne odstránená',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" bol pridaný medzi obľúbené',\n    'favourite_remove_notification' => '\":name\" bol odstránený z obľúbených',\n\n    // Watching\n    'watch_update_level_notification' => 'Watch preferences successfully updated',\n\n    // Auth\n    'auth_login' => 'sa prihlásil(a)',\n    'auth_register' => 'registered as new user',\n    'auth_password_reset_request' => 'requested user password reset',\n    'auth_password_reset_update' => 'reset user password',\n    'mfa_setup_method' => 'configured MFA method',\n    'mfa_setup_method_notification' => 'Viacúrovňový spôsob overenia úspešne nastavený',\n    'mfa_remove_method' => 'removed MFA method',\n    'mfa_remove_method_notification' => 'Viacúrovňový spôsob overenia úspešne odstránený',\n\n    // Settings\n    'settings_update' => 'aktualizované nastavenia',\n    'settings_update_notification' => 'Nastavenia boli úspešne aktualizované',\n    'maintenance_action_run' => 'ran maintenance action',\n\n    // Webhooks\n    'webhook_create' => 'vytvoril(a) si webhook',\n    'webhook_create_notification' => 'Webhook úspešne vytvorený',\n    'webhook_update' => 'aktualizoval(a) si webhook',\n    'webhook_update_notification' => 'Webhook úspešne aktualizovaný',\n    'webhook_delete' => 'odstránil(a) si webhook',\n    'webhook_delete_notification' => 'Webhook úspešne odstránený',\n\n    // Imports\n    'import_create' => 'created import',\n    'import_create_notification' => 'Import successfully uploaded',\n    'import_run' => 'updated import',\n    'import_run_notification' => 'Content successfully imported',\n    'import_delete' => 'deleted import',\n    'import_delete_notification' => 'Import successfully deleted',\n\n    // Users\n    'user_create' => 'užívateľ vytvorený',\n    'user_create_notification' => 'User successfully created',\n    'user_update' => 'používateľ aktualizovaný',\n    'user_update_notification' => 'Používateľ úspešne upravený',\n    'user_delete' => 'odstránený používateľ',\n    'user_delete_notification' => 'Používateľ úspešne zmazaný',\n\n    // API Tokens\n    'api_token_create' => 'created API token',\n    'api_token_create_notification' => 'API token successfully created',\n    'api_token_update' => 'updated API token',\n    'api_token_update_notification' => 'API token successfully updated',\n    'api_token_delete' => 'deleted API token',\n    'api_token_delete_notification' => 'API token successfully deleted',\n\n    // Roles\n    'role_create' => 'created role',\n    'role_create_notification' => 'Rola úspešne vytvorená',\n    'role_update' => 'updated role',\n    'role_update_notification' => 'Rola úspešne aktualizovaná',\n    'role_delete' => 'odstrániť rolu',\n    'role_delete_notification' => 'Rola úspešne zmazaná',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'emptied recycle bin',\n    'recycle_bin_restore' => 'restored from recycle bin',\n    'recycle_bin_destroy' => 'removed from recycle bin',\n\n    // Comments\n    'commented_on'                => 'komentoval(a)',\n    'comment_create'              => 'pridal(a) komentár',\n    'comment_update'              => 'aktualizoval(a) komentár',\n    'comment_delete'              => 'odstrániť komentár',\n\n    // Sort Rules\n    'sort_rule_create' => 'created sort rule',\n    'sort_rule_create_notification' => 'Sort rule successfully created',\n    'sort_rule_update' => 'updated sort rule',\n    'sort_rule_update_notification' => 'Sort rule successfully updated',\n    'sort_rule_delete' => 'deleted sort rule',\n    'sort_rule_delete_notification' => 'Sort rule successfully deleted',\n\n    // Other\n    'permissions_update'          => 'aktualizované oprávnenia',\n];\n"
  },
  {
    "path": "lang/sk/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Tieto údaje sa nezhodujú s našimi záznamami.',\n    'throttle' => 'Priveľa pokusov o prihlásenie. Skúste znova o :seconds sekúnd.',\n\n    // Login & Register\n    'sign_up' => 'Registrácia',\n    'log_in' => 'Prihlásenie',\n    'log_in_with' => 'Prihlásiť sa cez :socialDriver',\n    'sign_up_with' => 'Registrovať sa cez :socialDriver',\n    'logout' => 'Odhlásenie',\n\n    'name' => 'Meno',\n    'username' => 'Používateľské meno',\n    'email' => 'E-mail',\n    'password' => 'Heslo',\n    'password_confirm' => 'Potvrdiť heslo',\n    'password_hint' => 'Musí obsahovať aspoň 8 znakov',\n    'forgot_password' => 'Zabudli ste heslo?',\n    'remember_me' => 'Zapamätať si ma',\n    'ldap_email_hint' => 'Zadajte prosím e-mail, ktorý sa má použiť pre tento účet.',\n    'create_account' => 'Vytvoriť účet',\n    'already_have_account' => 'Už máte svoj ​​účet?',\n    'dont_have_account' => 'Nemáte účet?',\n    'social_login' => 'Sociálne prihlásenie',\n    'social_registration' => 'Sociálna registrácia',\n    'social_registration_text' => 'Registrácia a prihlásenie pomocou inej služby.',\n\n    'register_thanks' => 'Ďakujeme za registráciu!',\n    'register_confirm' => 'Prosím, skontrolujte svoj e-mail a kliknite na potvrdzujúce tlačidlo pre prístup k :appName.',\n    'registrations_disabled' => 'Registrácie sú momentálne zablokované',\n    'registration_email_domain_invalid' => 'Táto e-mailová doména nemá prístup k tejto aplikácii',\n    'register_success' => 'Ďakujeme za registráciu! Teraz ste registrovaný a prihlásený.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Pokus o prihlásenie',\n    'auto_init_starting_desc' => 'Kontaktujeme váš overovací systém, aby sme spustili proces prihlásenia. Ak po 5 sekundách nedôjde k žiadnemu pokroku, môžete skúsiť kliknúť na odkaz nižšie.',\n    'auto_init_start_link' => 'Pokračujte v autentifikácii',\n\n    // Password Reset\n    'reset_password' => 'Resetovanie hesla',\n    'reset_password_send_instructions' => 'Nižšie zadajte svoj e-mail, na ktorý Vám zašleme odkaz pre resetovanie hesla.',\n    'reset_password_send_button' => 'Poslať odkaz na resetovanie hesla',\n    'reset_password_sent' => 'Odkaz na resetovanie hesla bude odoslaný na :email, ak sa táto e-mailová adresa nachádza v systéme.',\n    'reset_password_success' => 'Vaše heslo bolo úspešne resetované.',\n    'email_reset_subject' => 'Resetovanie Vášho hesla do :appName',\n    'email_reset_text' => 'Tento e-mail ste obdržali, pretože sme dostali požiadavku na resetovanie hesla pre Váš účet.',\n    'email_reset_not_requested' => 'Ak ste nepožiadali o resetovanie hesla, nemusíte robiť nič.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Potvrdiť e-mail na :appName',\n    'email_confirm_greeting' => 'Ďakujeme, že ste sa pridali k :appName!',\n    'email_confirm_text' => 'Prosím, potvrďte Vašu e-mailovú adresu kliknutím na tlačidlo nižšie:',\n    'email_confirm_action' => 'Potvrdiť e-mail',\n    'email_confirm_send_error' => 'Je požadované overenie e-mailu, ale systém nemohol e-mail odoslať. Kontaktujte administrátora, aby ste sa uistili, že je e-mail nastavený správne.',\n    'email_confirm_success' => 'Váš email bol potvrdený! Teraz by ste sa mali vedieť prihlásiť pomocou tejto e-mailovej adresy.',\n    'email_confirm_resent' => 'Potvrdzujúci e-mail bol poslaný znovu, skontrolujte prosím svoju e-mailovú schránku.',\n    'email_confirm_thanks' => 'Ďakujeme za potvrdenie!',\n    'email_confirm_thanks_desc' => 'Počkajte chvíľu, kým sa spracuje vaše potvrdenie. Ak nebudete presmerovaní do 3 sekúnd, pokračujte kliknutím na odkaz „Pokračovať“ nižšie.',\n\n    'email_not_confirmed' => 'E-mailová adresa nebola overená',\n    'email_not_confirmed_text' => 'Vaša e-mailová adresa nebola zatiaľ overená.',\n    'email_not_confirmed_click_link' => 'Prosím, kliknite na odkaz v e-maili, ktorý bol poslaný krátko po Vašej registrácii.',\n    'email_not_confirmed_resend' => 'Ak nemôžete nájsť e-mail, môžete znova odoslať overovací e-mail odoslaním doleuvedeného formulára.',\n    'email_not_confirmed_resend_button' => 'Znova odoslať overovací e-mail',\n\n    // User Invite\n    'user_invite_email_subject' => 'Dostali ste pozvánku na pripojenie sa k aplikácii :appName!',\n    'user_invite_email_greeting' => 'Účet pre :appName bol pre vás vytvorený.',\n    'user_invite_email_text' => 'Kliknutím na tlačidlo nižšie nastavíte heslo k účtu a získate prístup:',\n    'user_invite_email_action' => 'Nastaviť heslo k účtu',\n    'user_invite_page_welcome' => 'Vitajte v :appName!',\n    'user_invite_page_text' => 'Ak chcete dokončiť svoj účet a získať prístup, musíte nastaviť heslo, ktoré sa použije na prihlásenie do aplikácie :appName pri budúcich návštevách.',\n    'user_invite_page_confirm_button' => 'Potvrdiť heslo',\n    'user_invite_success_login' => 'Heslo je nastavené, teraz by ste sa mali vedieť prihlásiť pomocou svojho nastaveného hesla na prístup k :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Nastaviť viacúrovňové prihlasovanie',\n    'mfa_setup_desc' => 'Pre vyššiu úroveň bezpečnosti si nastavte viacúrovňové prihlasovanie.',\n    'mfa_setup_configured' => 'Už nastavené',\n    'mfa_setup_reconfigure' => 'Znovunastavenie',\n    'mfa_setup_remove_confirmation' => 'Ste si istý, že chcete odstrániť tento spôsob viacúrovňového overenia?',\n    'mfa_setup_action' => 'Nastaveine',\n    'mfa_backup_codes_usage_limit_warning' => 'Ostáva vám menej ako 5 záložných kódov. Vygenerujte a uložte si novú súpravu skôr, ako sa vám minú kódy, aby ste sa vyhli vymknutiu z vášho účtu.',\n    'mfa_option_totp_title' => 'Mobilná aplikácia',\n    'mfa_option_totp_desc' => 'Pre používanie viacúrovňového prihlasovania budete potrebovať mobilnú aplikáciu, ktorá podporuje TOPS ako napríklad Google Authenticator, Authy alebo Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Záložné kódy',\n    'mfa_option_backup_codes_desc' => 'Generates a set of one-time-use backup codes which you\\'ll enter on login to verify your identity. Make sure to store these in a safe & secure place.',\n    'mfa_gen_confirm_and_enable' => 'Potvrdiť a zapnúť',\n    'mfa_gen_backup_codes_title' => 'Nastavenie záložných kódov',\n    'mfa_gen_backup_codes_desc' => 'Uložte si tieto kódy na bezpečné miesto. Jeden z kódov budete môcť použiť ako druhý faktor overenia identiy na prihlásenie sa.',\n    'mfa_gen_backup_codes_download' => 'Stiahnuť kódy',\n    'mfa_gen_backup_codes_usage_warning' => 'Každý kód môže byť použitý len jeden krát',\n    'mfa_gen_totp_title' => 'Nastavenie mobilnej aplikácie',\n    'mfa_gen_totp_desc' => 'Pre používanie viacúrovňového prihlasovania budete potrebovať mobilnú aplikáciu, ktorá podporuje TOPS ako napríklad Google Authenticator, Authy alebo Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Naskenujte 1R k\\'d pomocou vašej mobilnej aplikácie.',\n    'mfa_gen_totp_verify_setup' => 'Overiť nastavenie',\n    'mfa_gen_totp_verify_setup_desc' => 'Overte, či všetko funguje zadaním kódu vygenerovaného vo vašej autentifikačnej aplikácii do vstupného poľa nižšie:',\n    'mfa_gen_totp_provide_code_here' => 'Sem vložte kód vygenerovaný vašou mobilnou aplikáciou',\n    'mfa_verify_access' => 'Overiť prístup',\n    'mfa_verify_access_desc' => 'Váš používateľský účet vyžaduje, aby ste pred udelením prístupu potvrdili svoju identitu prostredníctvom ďalšej úrovne overenia. Pokračujte overením pomocou jednej z vašich nakonfigurovaných metód.',\n    'mfa_verify_no_methods' => 'Žiadny spôsob nebol nastavený',\n    'mfa_verify_no_methods_desc' => 'Pre váš účet sa nenašli žiadne metódy viacfaktorovej autentifikácie. Pred získaním prístupu budete musieť nastaviť aspoň jednu metódu.',\n    'mfa_verify_use_totp' => 'Overiť pomocou mobilnej aplikácie',\n    'mfa_verify_use_backup_codes' => 'Overiť pomocou záložného kódu',\n    'mfa_verify_backup_code' => 'Záložný kód',\n    'mfa_verify_backup_code_desc' => 'Zadajte jeden zo zostávajúcich záložných kódov:',\n    'mfa_verify_backup_code_enter_here' => 'Zadajte záložný kód',\n    'mfa_verify_totp_desc' => 'Zadajte kód vygenerovaný vašou mobilnou aplikáciou:',\n    'mfa_setup_login_notification' => 'Viacfaktorová metóda je nakonfigurovaná. Teraz sa znova prihláste pomocou nakonfigurovanej metódy.',\n];\n"
  },
  {
    "path": "lang/sk/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Zrušiť',\n    'close' => 'Close',\n    'confirm' => 'Potvrdiť',\n    'back' => 'Späť',\n    'save' => 'Uložiť',\n    'continue' => 'Pokračovať',\n    'select' => 'Vybrať',\n    'toggle_all' => 'Prepnúť všetko',\n    'more' => 'Viac',\n\n    // Form Labels\n    'name' => 'Meno',\n    'description' => 'Popis',\n    'role' => 'Rola',\n    'cover_image' => 'Obal knihy',\n    'cover_image_description' => 'This image should be approximately 440x250px although it will be flexibly scaled & cropped to fit the user interface in different scenarios as required, so actual dimensions for display will differ.',\n\n    // Actions\n    'actions' => 'Akcie',\n    'view' => 'Zobraziť',\n    'view_all' => 'Zobraziť všetko',\n    'new' => 'Nový',\n    'create' => 'Vytvoriť',\n    'update' => 'Aktualizovať',\n    'edit' => 'Editovať',\n    'archive' => 'Archive',\n    'unarchive' => 'Un-Archive',\n    'sort' => 'Zoradiť',\n    'move' => 'Presunúť',\n    'copy' => 'Kopírovať',\n    'reply' => 'Odpovedať',\n    'delete' => 'Zmazať',\n    'delete_confirm' => 'Potvrdiť zmazanie',\n    'search' => 'Hľadať',\n    'search_clear' => 'Vyčistiť hľadanie',\n    'reset' => 'Resetovať',\n    'remove' => 'Odstrániť',\n    'add' => 'Pridať',\n    'configure' => 'Konfigurácia',\n    'manage' => 'Manage',\n    'fullscreen' => 'Celá obrazovka',\n    'favourite' => 'Pridať do obľúbených',\n    'unfavourite' => 'Odstrániť z obľúbených',\n    'next' => 'Ďalej',\n    'previous' => 'Späť',\n    'filter_active' => 'Aktívny filter:',\n    'filter_clear' => 'Bez filtrovania',\n    'download' => 'Stiahnuť',\n    'open_in_tab' => 'Otvoriť na novej karte',\n    'open' => 'Open',\n\n    // Sort Options\n    'sort_options' => 'Možnosti triedenia',\n    'sort_direction_toggle' => 'Zoradiť smerový prepínač',\n    'sort_ascending' => 'Zoradiť vzostupne',\n    'sort_descending' => 'Zoradiť zostupne',\n    'sort_name' => 'Meno',\n    'sort_default' => 'Východzie',\n    'sort_created_at' => 'Dátum vytvorenia',\n    'sort_updated_at' => 'Aktualizované dňa',\n\n    // Misc\n    'deleted_user' => 'Odstránený používateľ',\n    'no_activity' => 'Žiadna aktivita na zobrazenie',\n    'no_items' => 'Žiadne položky nie sú dostupné',\n    'back_to_top' => 'Späť nahor',\n    'skip_to_main_content' => 'Preskočiť na hlavný obsah',\n    'toggle_details' => 'Prepnúť detaily',\n    'toggle_thumbnails' => 'Prepnúť náhľady',\n    'details' => 'Podrobnosti',\n    'grid_view' => 'Zobrazenie v mriežke',\n    'list_view' => 'Zobraziť ako zoznam',\n    'default' => 'Predvolené',\n    'breadcrumb' => 'Breadcrumb',\n    'status' => 'Stav',\n    'status_active' => 'Aktívny',\n    'status_inactive' => 'Neaktívny',\n    'never' => 'Nikdy',\n    'none' => 'Žiadne',\n\n    // Header\n    'homepage' => 'Domovská stránka',\n    'header_menu_expand' => 'Rozbaliť menu v záhlaví',\n    'profile_menu' => 'Menu profilu',\n    'view_profile' => 'Zobraziť profil',\n    'edit_profile' => 'Upraviť profil',\n    'dark_mode' => 'Tmavý režim',\n    'light_mode' => 'Svetlý režim',\n    'global_search' => 'Globálne vyhľadávanie',\n\n    // Layout tabs\n    'tab_info' => 'Informácie',\n    'tab_info_label' => 'Tab: Zobraziť vedľajšie informácie',\n    'tab_content' => 'Obsah',\n    'tab_content_label' => 'Tab: Zobraziť hlavné informácie',\n\n    // Email Content\n    'email_action_help' => 'Ak máte problém klinkúť na tlačidlo \":actionText\", skopírujte a vložte URL uvedenú nižšie do Vášho prehliadača:',\n    'email_rights' => 'Všetky práva vyhradené',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Zásady ochrany osobných údajov',\n    'terms_of_service' => 'Podmienky používania',\n\n    // OpenSearch\n    'opensearch_description' => 'Search :appName',\n];\n"
  },
  {
    "path": "lang/sk/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Vybrať obrázok',\n    'image_list' => 'Image List',\n    'image_details' => 'Image Details',\n    'image_upload' => 'Nahrať obrázok',\n    'image_intro' => 'Tu môžete vybrať a spravovať obrázky, ktoré boli predtým nahrané do systému.',\n    'image_intro_upload' => 'Nahrajte nový obrázok pretiahnutím súboru obrázka do tohto okna alebo pomocou vyššie uvedeného tlačidla „Nahrať obrázok“.',\n    'image_all' => 'Všetko',\n    'image_all_title' => 'Zobraziť všetky obrázky',\n    'image_book_title' => 'Zobraziť obrázky nahrané do tejto knihy',\n    'image_page_title' => 'Zobraziť obrázky nahrané do tejto stránky',\n    'image_search_hint' => 'Hľadať obrázok podľa názvu',\n    'image_uploaded' => 'Nahrané :uploadedDate',\n    'image_uploaded_by' => 'Uploaded by :userName',\n    'image_uploaded_to' => 'Uploaded to :pageLink',\n    'image_updated' => 'Updated :updateDate',\n    'image_load_more' => 'Načítať viac',\n    'image_image_name' => 'Názov obrázka',\n    'image_delete_used' => 'Tento obrázok je použitý na stránkach uvedených nižšie.',\n    'image_delete_confirm_text' => 'Naozaj chcete vymazať tento obrázok?',\n    'image_select_image' => 'Vybrať obrázok',\n    'image_dropzone' => 'Presuňte obrázky sem alebo kliknite sem pre nahranie',\n    'image_dropzone_drop' => 'Sem presuňte obrázky, ktoré chcete nahrať',\n    'images_deleted' => 'Obrázky zmazané',\n    'image_preview' => 'Náhľad obrázka',\n    'image_upload_success' => 'Obrázok úspešne nahraný',\n    'image_update_success' => 'Detaily obrázka úspešne aktualizované',\n    'image_delete_success' => 'Obrázok úspešne zmazaný',\n    'image_replace' => 'Replace Image',\n    'image_replace_success' => 'Image file successfully updated',\n    'image_rebuild_thumbs' => 'Regenerate Size Variations',\n    'image_rebuild_thumbs_success' => 'Image size variations successfully rebuilt!',\n\n    // Code Editor\n    'code_editor' => 'Upraviť kód',\n    'code_language' => 'Kód jazyka',\n    'code_content' => 'Obsah kódu',\n    'code_session_history' => 'História relácií',\n    'code_save' => 'Ulož kód',\n];\n"
  },
  {
    "path": "lang/sk/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Všeobecné',\n    'advanced' => 'Pokročilé',\n    'none' => 'Žiadne',\n    'cancel' => 'Zrušiť',\n    'save' => 'Uložiť',\n    'close' => 'Zavrieť',\n    'apply' => 'Apply',\n    'undo' => 'Vrátiť späť',\n    'redo' => 'Obnoviť',\n    'left' => 'Vľavo',\n    'center' => 'Na stred',\n    'right' => 'Vpravo',\n    'top' => 'Nahor',\n    'middle' => 'Uprostred',\n    'bottom' => 'Dole',\n    'width' => 'Šírka',\n    'height' => 'Výška',\n    'More' => 'Viac',\n    'select' => 'Vybrať...',\n\n    // Toolbar\n    'formats' => 'Formáty',\n    'header_large' => 'Veľká hlavička',\n    'header_medium' => 'Stredná hlavička',\n    'header_small' => 'Malá hlavička',\n    'header_tiny' => 'Drobná hlavička',\n    'paragraph' => 'Odstavec',\n    'blockquote' => 'Citácia',\n    'inline_code' => 'Vložený kód',\n    'callouts' => 'Hlášky',\n    'callout_information' => 'Informácie',\n    'callout_success' => 'Úspech',\n    'callout_warning' => 'Upozornenie',\n    'callout_danger' => 'Nebezpečné',\n    'bold' => 'Tučné',\n    'italic' => 'Kurzíva',\n    'underline' => 'Podčiarknutie',\n    'strikethrough' => 'Prečiarknutie',\n    'superscript' => 'Horný index',\n    'subscript' => 'Dolný index',\n    'text_color' => 'Farba textu',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Vlastná farba',\n    'remove_color' => 'Odstrániť farbu',\n    'background_color' => 'Farba pozadia',\n    'align_left' => 'Zarovnať vľavo',\n    'align_center' => 'Zarovnať na stred',\n    'align_right' => 'Zarovnať vpravo',\n    'align_justify' => 'Do bloku',\n    'list_bullet' => 'Bodový zoznam',\n    'list_numbered' => 'Číslovaný zoznam',\n    'list_task' => 'Zoznam úloh',\n    'indent_increase' => 'Zvýšiť odsadenie',\n    'indent_decrease' => 'Zmenšiť odsadenie',\n    'table' => 'Tabuľka',\n    'insert_image' => 'Vložiť obrázok',\n    'insert_image_title' => 'Vložiť/Upraviť obrázok',\n    'insert_link' => 'Vložiť/Upraviť odkaz',\n    'insert_link_title' => 'Vložiť/Upraviť link',\n    'insert_horizontal_line' => 'Vložiť horizontálnu čiaru',\n    'insert_code_block' => 'Vložte blok kódu',\n    'edit_code_block' => 'Upraviť blok kódu',\n    'insert_drawing' => 'Vložiť/upraviť výkres',\n    'drawing_manager' => 'Manažér kreslenia',\n    'insert_media' => 'Vložiť/Upraviť média',\n    'insert_media_title' => 'Vložiť/Upraviť média',\n    'clear_formatting' => 'Vymazať formátovanie',\n    'source_code' => 'Zdrojový kód',\n    'source_code_title' => 'Zdrojový kód',\n    'fullscreen' => 'Celá obrazovka',\n    'image_options' => 'Možnosti obrázka',\n\n    // Tables\n    'table_properties' => 'Vlastnosti tabuľky',\n    'table_properties_title' => 'Vlastnosti tabuľky',\n    'delete_table' => 'Vymazať tabuľku',\n    'table_clear_formatting' => 'Clear table formatting',\n    'resize_to_contents' => 'Resize to contents',\n    'row_header' => 'Row header',\n    'insert_row_before' => 'Vložiť riadok pred',\n    'insert_row_after' => 'Vložiť riadok za',\n    'delete_row' => 'Vymazať riadok',\n    'insert_column_before' => 'Vložiť stĺpec pred',\n    'insert_column_after' => 'Vložiť stĺpec za',\n    'delete_column' => 'Vymazať stĺpec',\n    'table_cell' => 'Bunka',\n    'table_row' => 'Riadok',\n    'table_column' => 'Stĺpec',\n    'cell_properties' => 'Vlastnosti bunky',\n    'cell_properties_title' => 'Vlastnosti bunky',\n    'cell_type' => 'Typ bunky',\n    'cell_type_cell' => 'Bunka',\n    'cell_scope' => 'Rozsah',\n    'cell_type_header' => 'Bunka hlavičky',\n    'merge_cells' => 'Zlúčiť bunky',\n    'split_cell' => 'Rozdeliť bunku',\n    'table_row_group' => 'Skupina riadkov',\n    'table_column_group' => 'Skupina stĺpcov',\n    'horizontal_align' => 'Horizontálne zarovnanie',\n    'vertical_align' => 'Vertikálne zarovnanie',\n    'border_width' => 'Šírka orámovania',\n    'border_style' => 'Štýl orámovania',\n    'border_color' => 'Farba orámovania',\n    'row_properties' => 'Vlastnosti riadku',\n    'row_properties_title' => 'Vlastnosti riadku',\n    'cut_row' => 'Vystrihnúť riadok',\n    'copy_row' => 'Kopírovať riadok',\n    'paste_row_before' => 'Pridať riadok pred',\n    'paste_row_after' => 'Pridať riadok za',\n    'row_type' => 'Typ riadku',\n    'row_type_header' => 'Hlavička',\n    'row_type_body' => 'Telo',\n    'row_type_footer' => 'Päta',\n    'alignment' => 'Zarovnanie',\n    'cut_column' => 'Vystrihnúť stĺpec',\n    'copy_column' => 'Kopírovať stĺpec',\n    'paste_column_before' => 'Pridať stĺpec pred',\n    'paste_column_after' => 'Pridať stĺpec po',\n    'cell_padding' => 'Cell Rozostup',\n    'cell_spacing' => 'Cell Rozstup',\n    'caption' => 'Titulok',\n    'show_caption' => 'Zobraziť Titulok',\n    'constrain' => 'Obmedziť rozmery',\n    'cell_border_solid' => 'Plný',\n    'cell_border_dotted' => 'Bodkovaný',\n    'cell_border_dashed' => 'Prerušované',\n    'cell_border_double' => 'Dvojité',\n    'cell_border_groove' => 'Drážka',\n    'cell_border_ridge' => 'Hrebeň',\n    'cell_border_inset' => 'Príloha',\n    'cell_border_outset' => 'Počiatok',\n    'cell_border_none' => 'Žiadne',\n    'cell_border_hidden' => 'Skryté',\n\n    // Images, links, details/summary & embed\n    'source' => 'Zdroj',\n    'alt_desc' => 'Alternatívny popis',\n    'embed' => 'Vložka',\n    'paste_embed' => 'Vložte svoj kód na vloženie nižšie:',\n    'url' => 'URL',\n    'text_to_display' => 'Text na zobrazenie',\n    'title' => 'Názov',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Otvoriť odkaz',\n    'open_link_in' => 'Otvoriť odkaz v...',\n    'open_link_current' => 'Iba aktuálne okno',\n    'open_link_new' => 'Nové okno',\n    'remove_link' => 'Odstrániť odkaz',\n    'insert_collapsible' => 'Vložte skladací blok',\n    'collapsible_unwrap' => 'Rozbaliť',\n    'edit_label' => 'Upraviť menovku',\n    'toggle_open_closed' => 'Prepínač otvorené/zatvorené',\n    'collapsible_edit' => 'Upraviť skladací blok',\n    'toggle_label' => 'Prepnutie menovky',\n\n    // About view\n    'about' => 'O editore',\n    'about_title' => 'O WYSIWYG Editore',\n    'editor_license' => 'Licencia editora a autorské práva',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'Tento editor je vytvorený pomocou :tinyLink, ktorý je poskytovaný pod licenciou MIT.',\n    'editor_tiny_license_link' => 'Podrobnosti o autorských právach a licenciách TinyMCE nájdete tu.',\n    'save_continue' => 'Uložiť a pokračovať',\n    'callouts_cycle' => '(Podržte stlačené, aby ste prepínali medzi typmi)',\n    'link_selector' => 'Prejsť na obsah',\n    'shortcuts' => 'Skratky',\n    'shortcut' => 'Skratka',\n    'shortcuts_intro' => 'V editore sú k dispozícii nasledujúce skratky:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Popis',\n];\n"
  },
  {
    "path": "lang/sk/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Nedávno vytvorené',\n    'recently_created_pages' => 'Nedávno vytvorené stránky',\n    'recently_updated_pages' => 'Nedávno aktualizované stránky',\n    'recently_created_chapters' => 'Nedávno vytvorené kapitoly',\n    'recently_created_books' => 'Nedávno vytvorené knihy',\n    'recently_created_shelves' => 'Nedávno vytvorené knižnice',\n    'recently_update' => 'Nedávno aktualizované',\n    'recently_viewed' => 'Nedávno zobrazené',\n    'recent_activity' => 'Nedávna aktivita',\n    'create_now' => 'Vytvoriť teraz',\n    'revisions' => 'Revízie',\n    'meta_revision' => 'Upravené vydanie #:revisionCount',\n    'meta_created' => 'Vytvorené :timeLength',\n    'meta_created_name' => 'Vytvorené :timeLength používateľom :user',\n    'meta_updated' => 'Aktualizované :timeLength',\n    'meta_updated_name' => 'Aktualizované :timeLength používateľom :user',\n    'meta_owned_name' => 'Vlastník :user',\n    'meta_reference_count' => 'Referenced by :count item|Referenced by :count items',\n    'entity_select' => 'Entita vybraná',\n    'entity_select_lack_permission' => 'Na výber tejto položky nemáte potrebné povolenia',\n    'images' => 'Obrázky',\n    'my_recent_drafts' => 'Moje nedávne koncepty',\n    'my_recently_viewed' => 'Nedávno mnou zobrazené',\n    'my_most_viewed_favourites' => 'Moje najčastejšie zobrazené obľubené',\n    'my_favourites' => 'Moje obľúbené',\n    'no_pages_viewed' => 'Nepozreli ste si žiadne stránky',\n    'no_pages_recently_created' => 'Žiadne stránky neboli nedávno vytvorené',\n    'no_pages_recently_updated' => 'Žiadne stránky neboli nedávno aktualizované',\n    'export' => 'Exportovať',\n    'export_html' => 'Obsahovaný webový súbor',\n    'export_pdf' => 'PDF súbor',\n    'export_text' => 'Súbor s čistým textom',\n    'export_md' => 'Súbor Markdown',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Default Page Template',\n    'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',\n    'default_template_select' => 'Select a template page',\n    'import' => 'Import',\n    'import_validate' => 'Validate Import',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'Select ZIP file to upload',\n    'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',\n    'import_pending' => 'Pending Imports',\n    'import_pending_none' => 'No imports have been started.',\n    'import_continue' => 'Continue Import',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Run Import',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Uploaded by',\n    'import_location' => 'Import Location',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'Import Errors',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Oprávnenia',\n    'permissions_desc' => 'Tu nastavte povolenia na prepísanie predvolených povolení poskytnutých rolami používateľov.',\n    'permissions_book_cascade' => 'Povolenia nastavené pre knihy sa automaticky prenesú do podriadených kapitol a strán, pokiaľ nemajú definované vlastné povolenia.',\n    'permissions_chapter_cascade' => 'Povolenia nastavené pre kapitoly sa automaticky prenesú na podradené stránky, pokiaľ nemajú definované vlastné povolenia.',\n    'permissions_save' => 'Uložiť oprávnenia',\n    'permissions_owner' => 'Vlastník',\n    'permissions_role_everyone_else' => 'Všetci ostatní',\n    'permissions_role_everyone_else_desc' => 'Nastavte povolenia pre všetky roly, ktoré nie sú špecificky prepísané.',\n    'permissions_role_override' => 'Prepísať povolenia pre rolu',\n    'permissions_inherit_defaults' => 'Zdediť predvolené hodnoty',\n\n    // Search\n    'search_results' => 'Výsledky hľadania',\n    'search_total_results_found' => ':count výsledok found|:počet nájdených výsledkov',\n    'search_clear' => 'Vyčistiť hľadanie',\n    'search_no_pages' => 'Žiadne stránky nevyhovujú tomuto hľadaniu',\n    'search_for_term' => 'Hľadať :term',\n    'search_more' => 'Načítať ďalšie výsledky',\n    'search_advanced' => 'Rozšírené vyhľadávanie',\n    'search_terms' => 'Hľadané výrazy',\n    'search_content_type' => 'Typ obsahu',\n    'search_exact_matches' => 'Presná zhoda',\n    'search_tags' => 'Vyhľadávanie značiek',\n    'search_options' => 'Možnosti',\n    'search_viewed_by_me' => 'Videné mnou',\n    'search_not_viewed_by_me' => 'Nevidené mnou',\n    'search_permissions_set' => 'Oprávnenia',\n    'search_created_by_me' => 'Vytvorené mnou',\n    'search_updated_by_me' => 'Aktualizované mnou',\n    'search_owned_by_me' => 'Patriace mne',\n    'search_date_options' => 'Možnosti dátumu',\n    'search_updated_before' => 'Aktualizované pred',\n    'search_updated_after' => 'Aktualizované po',\n    'search_created_before' => 'Vytvorené pred',\n    'search_created_after' => 'Vytvorené po',\n    'search_set_date' => 'Nastaviť Dátum',\n    'search_update' => 'Aktualizujte vyhľadávanie',\n\n    // Shelves\n    'shelf' => 'Polica',\n    'shelves' => 'Police',\n    'x_shelves' => ':count Shelf|:count Police',\n    'shelves_empty' => 'Neboli vytvorené žiadne police',\n    'shelves_create' => 'Vytvoriť novú policu',\n    'shelves_popular' => 'Populárne police',\n    'shelves_new' => 'Nové police',\n    'shelves_new_action' => 'Nová polica',\n    'shelves_popular_empty' => 'Najpopulárnejšie police sa objavia tu.',\n    'shelves_new_empty' => 'Najpopulárnejšie police sa objavia tu.',\n    'shelves_save' => 'Uložiť policu',\n    'shelves_books' => 'Knihy na tejto polici',\n    'shelves_add_books' => 'Pridať knihy do tejto police',\n    'shelves_drag_books' => 'Potiahnite knihy nižšie a pridajte ich do tejto police',\n    'shelves_empty_contents' => 'Táto polica nemá priradené žiadne knihy',\n    'shelves_edit_and_assign' => 'Uprav policu a priraď knihy',\n    'shelves_edit_named' => 'Upraviť policu :name',\n    'shelves_edit' => 'Upraviť policu',\n    'shelves_delete' => 'Odstrániť policu',\n    'shelves_delete_named' => 'Odstrániť policu :name',\n    'shelves_delete_explain' => \"Týmto vymažete policu s názvom ': name'. Obsahované knihy sa neodstránia.\",\n    'shelves_delete_confirmation' => 'Ste si istý, že chcete zmazať túto policu?',\n    'shelves_permissions' => 'Povolenia police',\n    'shelves_permissions_updated' => 'Povolenia police aktualizované',\n    'shelves_permissions_active' => 'Povolenia police aktívne',\n    'shelves_permissions_cascade_warning' => 'Povolenia na poličkách sa automaticky nepriraďujú k obsiahnutým knihám. Je to preto, že kniha môže existovať na viacerých poličkách. Povolenia však možno skopírovať do kníh pomocou možnosti uvedenej nižšie.',\n    'shelves_permissions_create' => 'Shelf create permissions are only used for copying permissions to child books using the action below. They do not control the ability to create books.',\n    'shelves_copy_permissions_to_books' => 'Kopírovať oprávnenia pre knihy',\n    'shelves_copy_permissions' => 'Kopírovať oprávnenia',\n    'shelves_copy_permissions_explain' => 'Týmto sa použijú aktuálne nastavenia povolení tejto police na všetky knihy, ktoré obsahuje. Pred aktiváciou sa uistite, že všetky zmeny povolení tejto police boli uložené.',\n    'shelves_copy_permission_success' => 'Oprávnenia police boli skopírované {0}:count kníh|{1}:count kniha|[2,3,4]:count knihy|[5,*]:count kníh',\n\n    // Books\n    'book' => 'Kniha',\n    'books' => 'Knihy',\n    'x_books' => '{0}:count kníh|{1}:count kniha|[2,3,4]:count knihy|[5,*]:count kníh',\n    'books_empty' => 'Žiadne knihy neboli vytvorené',\n    'books_popular' => 'Populárne knihy',\n    'books_recent' => 'Nedávne knihy',\n    'books_new' => 'Nové knihy',\n    'books_new_action' => 'Nová kniha',\n    'books_popular_empty' => 'Najpopulárnejšie knihy sa objavia tu.',\n    'books_new_empty' => 'Najnovšie knihy sa zobrazia tu.',\n    'books_create' => 'Vytvoriť novú knihu',\n    'books_delete' => 'Zmazať knihu',\n    'books_delete_named' => 'Zmazať knihu :bookName',\n    'books_delete_explain' => 'Toto zmaže knihu s názvom \\':bookName\\', všetky stránky a kapitoly budú odstránené.',\n    'books_delete_confirmation' => 'Ste si istý, že chcete zmazať túto knihu?',\n    'books_edit' => 'Upraviť knihu',\n    'books_edit_named' => 'Upraviť knihu :bookName',\n    'books_form_book_name' => 'Názov knihy',\n    'books_save' => 'Uložiť knihu',\n    'books_permissions' => 'Oprávnenia knihy',\n    'books_permissions_updated' => 'Oprávnenia knihy aktualizované',\n    'books_empty_contents' => 'Pre túto knihu neboli vytvorené žiadne stránky alebo kapitoly.',\n    'books_empty_create_page' => 'Vytvoriť novú stránku',\n    'books_empty_sort_current_book' => 'Zoradiť aktuálnu knihu',\n    'books_empty_add_chapter' => 'Pridať kapitolu',\n    'books_permissions_active' => 'Oprávnenia knihy aktívne',\n    'books_search_this' => 'Hľadať v tejto knihe',\n    'books_navigation' => 'Navigácia knihy',\n    'books_sort' => 'Zoradiť obsah knihy',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Zoradiť knihu :bookName',\n    'books_sort_name' => 'Zoradiť podľa mena',\n    'books_sort_created' => 'Zoradiť podľa dátumu vytvorenia',\n    'books_sort_updated' => 'Zoradiť podľa dátumu aktualizácie',\n    'books_sort_chapters_first' => 'Kapitoly ako prvé',\n    'books_sort_chapters_last' => 'Kapitoly ako posledné',\n    'books_sort_show_other' => 'Zobraziť ostatné knihy',\n    'books_sort_save' => 'Uložiť nové zoradenie',\n    'books_sort_show_other_desc' => 'Pridajte ďalšie knihy, aby ste ich zahrnuli do operácie triedenia a umožnili jednoduchú reorganizáciu medzi knihami.',\n    'books_sort_move_up' => 'Posunúť vyššie',\n    'books_sort_move_down' => 'Posunúť nižšie',\n    'books_sort_move_prev_book' => 'Presun na predchádzajúcu knihu',\n    'books_sort_move_next_book' => 'Presun na nasledujúcu knihu',\n    'books_sort_move_prev_chapter' => 'Presun na predchádzajúcu kapitolu',\n    'books_sort_move_next_chapter' => 'Presun na ďalšiu kapitolu',\n    'books_sort_move_book_start' => 'Presun na začiatok knihy',\n    'books_sort_move_book_end' => 'Presun na koniec knihy',\n    'books_sort_move_before_chapter' => 'Prejsť na Pred kapitolou',\n    'books_sort_move_after_chapter' => 'Prejsť na Po kapitole',\n    'books_copy' => 'Kopírovať knihu',\n    'books_copy_success' => 'Kniha bola skopírovaná',\n\n    // Chapters\n    'chapter' => 'Kapitola',\n    'chapters' => 'Kapitoly',\n    'x_chapters' => '{0}:count Kapitol|{1}:count Kapitola|[2,3,4]:count Kapitoly|[5,*]:count Kapitol',\n    'chapters_popular' => 'Populárne kapitoly',\n    'chapters_new' => 'Nová kapitola',\n    'chapters_create' => 'Vytvoriť novú kapitolu',\n    'chapters_delete' => 'Zmazať kapitolu',\n    'chapters_delete_named' => 'Zmazať kapitolu :chapterName',\n    'chapters_delete_explain' => 'Týmto sa odstráni kapitola s názvom \\':chapterName\\'. Spolu s ňou sa odstránia všetky stránky v tejto kapitole.',\n    'chapters_delete_confirm' => 'Ste si istý, že chcete zmazať túto kapitolu?',\n    'chapters_edit' => 'Upraviť kapitolu',\n    'chapters_edit_named' => 'Upraviť kapitolu :chapterName',\n    'chapters_save' => 'Uložiť kapitolu',\n    'chapters_move' => 'Presunúť kapitolu',\n    'chapters_move_named' => 'Presunúť kapitolu :chapterName',\n    'chapters_copy' => 'Kopírovať kapitolu',\n    'chapters_copy_success' => 'Kapitola bola úspešne skopírovaná',\n    'chapters_permissions' => 'Oprávnenia kapitoly',\n    'chapters_empty' => 'V tejto kapitole nie sú teraz žiadne stránky.',\n    'chapters_permissions_active' => 'Oprávnenia kapitoly aktívne',\n    'chapters_permissions_success' => 'Oprávnenia kapitoly aktualizované',\n    'chapters_search_this' => 'Hladať v kapitole',\n    'chapter_sort_book' => 'Triediť knihu',\n\n    // Pages\n    'page' => 'Stránka',\n    'pages' => 'Stránky',\n    'x_pages' => ':count stránok',\n    'pages_popular' => 'Populárne stránky',\n    'pages_new' => 'Nová stránka',\n    'pages_attachments' => 'Prílohy',\n    'pages_navigation' => 'Navigácia',\n    'pages_delete' => 'Zmazať stránku',\n    'pages_delete_named' => 'Zmazať stránku :pageName',\n    'pages_delete_draft_named' => 'Zmazať koncept :pageName',\n    'pages_delete_draft' => 'Zmazať koncept',\n    'pages_delete_success' => 'Stránka zmazaná',\n    'pages_delete_draft_success' => 'Koncept stránky zmazaný',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'Ste si istý, že chcete zmazať túto stránku?',\n    'pages_delete_draft_confirm' => 'Ste si istý, že chcete zmazať tento koncept stránky?',\n    'pages_editing_named' => 'Upraviť stránku :pageName',\n    'pages_edit_draft_options' => 'Možnosti konceptu',\n    'pages_edit_save_draft' => 'Uložiť koncept',\n    'pages_edit_draft' => 'Upraviť koncept stránky',\n    'pages_editing_draft' => 'Upravuje sa koncept',\n    'pages_editing_page' => 'Upravuje sa stránka',\n    'pages_edit_draft_save_at' => 'Koncept uložený pod ',\n    'pages_edit_delete_draft' => 'Uložiť koncept',\n    'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.',\n    'pages_edit_discard_draft' => 'Zrušiť koncept',\n    'pages_edit_switch_to_markdown' => 'Prepnite na Markdown Editor',\n    'pages_edit_switch_to_markdown_clean' => '(Vyčistiť obsah)',\n    'pages_edit_switch_to_markdown_stable' => '(Stabilný obsah)',\n    'pages_edit_switch_to_wysiwyg' => 'Prepnite na WYSIWYG Editor',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Nastaviť záznam zmien',\n    'pages_edit_enter_changelog_desc' => 'Zadajte krátky popis zmien, ktoré ste urobili',\n    'pages_edit_enter_changelog' => 'Zadať záznam zmien',\n    'pages_editor_switch_title' => 'Prepnúť editor',\n    'pages_editor_switch_are_you_sure' => 'Naozaj chcete zmeniť editor pre túto stránku?',\n    'pages_editor_switch_consider_following' => 'Pri zmene editorov zvážte nasledujúce:',\n    'pages_editor_switch_consideration_a' => 'Po uložení budú novú možnosť editora používať všetci budúci editori vrátane tých, ktorí nemusia byť schopní sami zmeniť typ editora.',\n    'pages_editor_switch_consideration_b' => 'Toto môže za určitých okolností potenciálne viesť k strate podrobností a syntaxe.',\n    'pages_editor_switch_consideration_c' => 'Zmeny v značke alebo v protokole zmien vykonané od posledného uloženia nezostanú pri tejto zmene zachované.',\n    'pages_save' => 'Uložiť stránku',\n    'pages_title' => 'Titulok stránky',\n    'pages_name' => 'Názov stránky',\n    'pages_md_editor' => 'Editor',\n    'pages_md_preview' => 'Náhľad',\n    'pages_md_insert_image' => 'Vložiť obrázok',\n    'pages_md_insert_link' => 'Vložiť odkaz na entitu',\n    'pages_md_insert_drawing' => 'Vložiť kresbu',\n    'pages_md_show_preview' => 'Zobraziť náhľad',\n    'pages_md_sync_scroll' => 'Posúvanie ukážky synchronizácie',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Unsaved Drawing Found',\n    'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',\n    'pages_not_in_chapter' => 'Stránka nie je v kapitole',\n    'pages_move' => 'Presunúť stránku',\n    'pages_copy' => 'Kpoírovať stránku',\n    'pages_copy_desination' => 'Ciel kopírovania',\n    'pages_copy_success' => 'Stránka bola skopírovaná',\n    'pages_permissions' => 'Oprávnenia stránky',\n    'pages_permissions_success' => 'Oprávnenia stránky aktualizované',\n    'pages_revision' => 'Revízia',\n    'pages_revisions' => 'Revízie stránky',\n    'pages_revisions_desc' => 'Nižšie sú uvedené všetky predchádzajúce revízie tejto stránky. Ak to povolenia umožňujú, môžete sa pozrieť späť, porovnať a obnoviť staré verzie stránok. Úplná história stránky sa tu nemusí úplne prejaviť, pretože v závislosti od konfigurácie systému môžu byť staré revízie automaticky odstránené.',\n    'pages_revisions_named' => 'Revízie stránky :pageName',\n    'pages_revision_named' => 'Revízia stránky :pageName',\n    'pages_revision_restored_from' => 'Obnovené z #:id; :summary',\n    'pages_revisions_created_by' => 'Vytvoril',\n    'pages_revisions_date' => 'Dátum revízie',\n    'pages_revisions_number' => 'č.',\n    'pages_revisions_sort_number' => 'Číslo revízie',\n    'pages_revisions_numbered' => 'Revízia č. :id',\n    'pages_revisions_numbered_changes' => 'Zmeny revízie č. ',\n    'pages_revisions_editor' => 'Typ editora',\n    'pages_revisions_changelog' => 'Záznam zmien',\n    'pages_revisions_changes' => 'Zmeny',\n    'pages_revisions_current' => 'Aktuálna verzia',\n    'pages_revisions_preview' => 'Náhľad',\n    'pages_revisions_restore' => 'Obnoviť',\n    'pages_revisions_none' => 'Táto stránka nemá žiadne revízie',\n    'pages_copy_link' => 'Kopírovať odkaz',\n    'pages_edit_content_link' => 'Jump to section in editor',\n    'pages_pointer_enter_mode' => 'Enter section select mode',\n    'pages_pointer_label' => 'Page Section Options',\n    'pages_pointer_permalink' => 'Page Section Permalink',\n    'pages_pointer_include_tag' => 'Page Section Include Tag',\n    'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag',\n    'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink',\n    'pages_permissions_active' => 'Oprávnienia stránky aktívne',\n    'pages_initial_revision' => 'Prvé zverejnenie',\n    'pages_references_update_revision' => 'Automatická aktualizácia systému interných odkazov',\n    'pages_initial_name' => 'Nová stránka',\n    'pages_editing_draft_notification' => 'Práve upravujete koncept, ktorý bol naposledy uložený :timeDiff.',\n    'pages_draft_edited_notification' => 'Táto stránka bola odvtedy upravená. Odporúča sa odstrániť tento koncept.',\n    'pages_draft_page_changed_since_creation' => 'Táto stránka bola aktualizovaná od vytvorenia tohto konceptu. Odporúča sa, aby ste tento koncept zahodili alebo aby ste neprepísali žiadne zmeny stránky.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count používateľov začalo upravovať túto stránku',\n        'start_b' => ':userName začal upravovať túto stránku',\n        'time_a' => 'odkedy boli stránky naposledy aktualizované',\n        'time_b' => 'za posledných :minCount minút',\n        'message' => ':start :time. Dávajte pozor aby ste si navzájom neprepísali zmeny!',\n    ],\n    'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',\n    'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',\n    'pages_specific' => 'Konkrétna stránka',\n    'pages_is_template' => 'Šablóna stránky',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Toggle Sidebar',\n    'page_tags' => 'Štítky stránok',\n    'chapter_tags' => 'Štítky kapitol',\n    'book_tags' => 'Štítky kníh',\n    'shelf_tags' => 'Štítky knižníc',\n    'tag' => 'Štítok',\n    'tags' =>  'Štítky',\n    'tags_index_desc' => 'Značky možno použiť na obsah v rámci systému a použiť flexibilnú formu kategorizácie. Značky môžu mať kľúč aj hodnotu, pričom hodnota je voliteľná. Po použití je možné obsah vyhľadávať pomocou názvu a hodnoty značky.',\n    'tag_name' =>  'Názov štítku',\n    'tag_value' => 'Hodnota štítku (Voliteľné)',\n    'tags_explain' => \"Pridajte pár štítkov pre uľahčenie kategorizácie Vášho obsahu. \\n Štítku môžete priradiť hodnotu pre ešte lepšiu organizáciu.\",\n    'tags_add' => 'Pridať ďalší štítok',\n    'tags_remove' => 'Odstrániť tento štítok',\n    'tags_usages' => 'Celkové využitie značiek',\n    'tags_assigned_pages' => 'Priradené k stránkam',\n    'tags_assigned_chapters' => 'Priradené ku kapitolám',\n    'tags_assigned_books' => 'Priradené ku knihám',\n    'tags_assigned_shelves' => 'Priradené k poličkám',\n    'tags_x_unique_values' => ':count jedinečné hodnoty',\n    'tags_all_values' => 'Všetky hodnoty',\n    'tags_view_tags' => 'Zobraziť značky',\n    'tags_view_existing_tags' => 'Zobraziť existujúce značky',\n    'tags_list_empty_hint' => 'Značky je možné priradiť prostredníctvom postranného panela editora stránok alebo pri úprave podrobností o knihe, kapitole alebo poličke.',\n    'attachments' => 'Prílohy',\n    'attachments_explain' => 'Nahrajte nejaké súbory alebo priložte zopár odkazov pre zobrazenie na Vašej stránke. Budú viditeľné v bočnom paneli.',\n    'attachments_explain_instant_save' => 'Zmeny budú okamžite uložené.',\n    'attachments_upload' => 'Nahrať súbor',\n    'attachments_link' => 'Priložiť odkaz',\n    'attachments_upload_drop' => 'Prípadne môžete presunúť súbor myšou sem a nahrať ho ako prílohu.',\n    'attachments_set_link' => 'Nastaviť odkaz',\n    'attachments_delete' => 'Naozaj chcete odstrániť túto prílohu?',\n    'attachments_dropzone' => 'Sem presuňte súbory na nahratie',\n    'attachments_no_files' => 'Žiadne súbory neboli nahrané',\n    'attachments_explain_link' => 'Ak nechcete priložiť súbor, môžete priložiť odkaz. Môže to byť odkaz na inú stránku alebo odkaz na súbor v cloude.',\n    'attachments_link_name' => 'Názov odkazu',\n    'attachment_link' => 'Odkaz na prílohu',\n    'attachments_link_url' => 'Odkaz na súbor',\n    'attachments_link_url_hint' => 'Url stránky alebo súboru',\n    'attach' => 'Priložiť',\n    'attachments_insert_link' => 'Pridať odkaz na prílohu',\n    'attachments_edit_file' => 'Upraviť súbor',\n    'attachments_edit_file_name' => 'Názov súboru',\n    'attachments_edit_drop_upload' => 'Presuňte súbory sem alebo klinknite pre nahranie a prepis',\n    'attachments_order_updated' => 'Poradie príloh aktualizované',\n    'attachments_updated_success' => 'Detaily prílohy aktualizované',\n    'attachments_deleted' => 'Príloha zmazaná',\n    'attachments_file_uploaded' => 'Súbor úspešne nahraný',\n    'attachments_file_updated' => 'Súbor úspešne aktualizovaný',\n    'attachments_link_attached' => 'Odkaz úspešne pripojený k stránke',\n    'templates' => 'Šablóny',\n    'templates_set_as_template' => 'Táto stránka je šablóna',\n    'templates_explain_set_as_template' => 'Túto stránku môžete nastaviť ako šablónu, aby sa jej obsah použil pri vytváraní ďalších stránok. Ostatní používatelia budú môcť použiť túto šablónu, ak majú povolenia na zobrazenie tejto stránky.',\n    'templates_replace_content' => 'Nahradiť obsah',\n    'templates_append_content' => 'Pripojiť k obsahu stránky',\n    'templates_prepend_content' => 'Pridať pred obsah stránky',\n\n    // Profile View\n    'profile_user_for_x' => 'Používateľ už :time',\n    'profile_created_content' => 'Vytvorený obsah',\n    'profile_not_created_pages' => ':userName nevytvoril žiadne stránky',\n    'profile_not_created_chapters' => ':userName nevytvoril žiadne kapitoly',\n    'profile_not_created_books' => ':userName nevytvoril žiadne knihy',\n    'profile_not_created_shelves' => ':userName nevytvoril(a) žiadne kapitoly',\n\n    // Comments\n    'comment' => 'Komentár',\n    'comments' => 'Komentáre',\n    'comment_add' => 'Pridať komentár',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Tu zadajte svoje pripomienky',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Uložiť komentár',\n    'comment_new' => 'Nový komentár',\n    'comment_created' => 'komentované :createDiff',\n    'comment_updated' => 'Aktualizované :updateDiff užívateľom :username',\n    'comment_updated_indicator' => 'Updated',\n    'comment_deleted_success' => 'Komentár odstránený',\n    'comment_created_success' => 'Komentár pridaný',\n    'comment_updated_success' => 'Komentár aktualizovaný',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Ste si istý, že chcete odstrániť tento komentár?',\n    'comment_in_reply_to' => 'Odpovedať na :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',\n\n    // Revision\n    'revision_delete_confirm' => 'Naozaj chcete túto revíziu odstrániť?',\n    'revision_restore_confirm' => 'Naozaj chcete obnoviť túto revíziu? Aktuálny obsah stránky sa nahradí.',\n    'revision_cannot_delete_latest' => 'Nie je možné vymazať poslednú revíziu.',\n\n    // Copy view\n    'copy_consider' => 'Pri kopírovaní obsahu zvážte nižšie uvedené.',\n    'copy_consider_permissions' => 'Vlastné nastavenia povolení sa neskopírujú.',\n    'copy_consider_owner' => 'Stanete sa vlastníkom všetkého skopírovaného obsahu.',\n    'copy_consider_images' => 'Súbory obrázkov stránky nebudú duplikované a pôvodné obrázky si zachovajú svoj vzťah k stránke, na ktorú boli pôvodne nahrané.',\n    'copy_consider_attachments' => 'Prílohy strán sa neskopírujú.',\n    'copy_consider_access' => 'Zmena umiestnenia, vlastníka alebo povolení môže mať za následok to, že tento obsah bude prístupný tým, ktorí k nemu predtým prístup nemali.',\n\n    // Conversions\n    'convert_to_shelf' => 'Konvertovať na Shelf',\n    'convert_to_shelf_contents_desc' => 'Túto knihu môžete previesť na novú policu s rovnakým obsahom. Kapitoly obsiahnuté v tejto knihe budú prevedené do nových kníh. Ak táto kniha obsahuje nejaké strany, ktoré nie sú v kapitole, táto kniha bude premenovaná a bude obsahovať tieto strany a táto kniha sa stane súčasťou novej police.',\n    'convert_to_shelf_permissions_desc' => 'Všetky povolenia nastavené pre túto knihu sa skopírujú do novej police a do všetkých nových podriadených kníh, ktoré nemajú vynútené vlastné povolenia. Všimnite si, že povolenia na policiach sa automaticky neprenášajú na obsah v rámci nich, ako je to v prípade kníh.',\n    'convert_book' => 'Previesť knihu',\n    'convert_book_confirm' => 'Ste si istý, že chcete previesť túto knihu?',\n    'convert_undo_warning' => 'To sa nedá tak ľahko vrátiť späť.',\n    'convert_to_book' => 'Previesť na knihu',\n    'convert_to_book_desc' => 'Túto kapitolu môžete previesť do novej knihy s rovnakým obsahom. Všetky povolenia nastavené v tejto kapitole sa skopírujú do novej knihy, ale žiadne zdedené povolenia z nadradenej knihy sa neskopírujú, čo by mohlo viesť k zmene riadenia prístupu.',\n    'convert_chapter' => 'Previesť kapitolu',\n    'convert_chapter_confirm' => 'Ste si istý, že chcete previesť túto kapitolu?',\n\n    // References\n    'references' => 'Referencie',\n    'references_none' => 'Neexistujú žiadne sledované referencie na túto položku.',\n    'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',\n\n    // Watch Options\n    'watch' => 'Watch',\n    'watch_title_default' => 'Default Preferences',\n    'watch_desc_default' => 'Revert watching to just your default notification preferences.',\n    'watch_title_ignore' => 'Ignore',\n    'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.',\n    'watch_title_new' => 'New Pages',\n    'watch_desc_new' => 'Notify when any new page is created within this item.',\n    'watch_title_updates' => 'All Page Updates',\n    'watch_desc_updates' => 'Notify upon all new pages and page changes.',\n    'watch_desc_updates_page' => 'Notify upon all page changes.',\n    'watch_title_comments' => 'All Page Updates & Comments',\n    'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.',\n    'watch_desc_comments_page' => 'Notify upon page changes and new comments.',\n    'watch_change_default' => 'Change default notification preferences',\n    'watch_detail_ignore' => 'Ignoring notifications',\n    'watch_detail_new' => 'Watching for new pages',\n    'watch_detail_updates' => 'Watching new pages and updates',\n    'watch_detail_comments' => 'Watching new pages, updates & comments',\n    'watch_detail_parent_book' => 'Watching via parent book',\n    'watch_detail_parent_book_ignore' => 'Ignoring via parent book',\n    'watch_detail_parent_chapter' => 'Watching via parent chapter',\n    'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter',\n];\n"
  },
  {
    "path": "lang/sk/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Nemáte oprávnenie pre prístup k požadovanej stránke.',\n    'permissionJson' => 'Nemáte oprávnenie pre vykonanie požadovaného úkonu.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Používateľ s emailom :email už existuje, ale s inými údajmi.',\n    'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',\n    'email_already_confirmed' => 'Email bol už overený, skúste sa prihlásiť.',\n    'email_confirmation_invalid' => 'Tento potvrdzujúci token nie je platný alebo už bol použitý, skúste sa prosím registrovať znova.',\n    'email_confirmation_expired' => 'Potvrdzujúci token expiroval, bol odoslaný nový potvrdzujúci email.',\n    'email_confirmation_awaiting' => 'Potvrďte emailovú adresu pre užívateľský účet',\n    'ldap_fail_anonymous' => 'Prístup LDAP zlyhal',\n    'ldap_fail_authed' => 'Prístup LDAP zlyhal pomocou zadaných podrobností dn a hesla',\n    'ldap_extension_not_installed' => 'Rozšírenie LDAP PHP nie je nainštalované',\n    'ldap_cannot_connect' => 'Nedá sa pripojiť k serveru ldap, počiatočné pripojenie zlyhalo',\n    'saml_already_logged_in' => 'Používateľ sa už prihlásil',\n    'saml_no_email_address' => 'V údajoch poskytnutých externým overovacím systémom sa nepodarilo nájsť e-mailovú adresu tohto používateľa',\n    'saml_invalid_response_id' => 'Požiadavka z externého autentifikačného systému nie je rozpoznaná procesom spusteným touto aplikáciou. Tento problém môže spôsobiť navigácia späť po prihlásení.',\n    'saml_fail_authed' => 'Prihlásenie pomocou :system zlyhalo, systém neposkytol úspešnú autorizáciu',\n    'oidc_already_logged_in' => 'Používateľ sa už prihlásil',\n    'oidc_no_email_address' => 'V údajoch poskytnutých externým overovacím systémom sa nepodarilo nájsť e-mailovú adresu tohto používateľa',\n    'oidc_fail_authed' => 'Prihlásenie pomocou :system zlyhalo, systém neposkytol úspešnú autorizáciu',\n    'social_no_action_defined' => 'Nebola definovaná žiadna akcia',\n    'social_login_bad_response' => \"Pri prihlásení do účtu :socialAccount došlo k chybe:\\n:error\",\n    'social_account_in_use' => 'Tento :socialAccount účet sa už používa, skúste sa prihlásiť pomocou možnosti :socialAccount.',\n    'social_account_email_in_use' => 'Email :email sa už používa. Ak už máte účet, môžete pripojiť svoj :socialAccount účet v nastaveniach profilu.',\n    'social_account_existing' => 'Tento :socialAccount účet je už spojený s Vaším profilom.',\n    'social_account_already_used_existing' => 'Tento :socialAccount účet už používa iný používateľ.',\n    'social_account_not_used' => 'Tento :socialAccount účet nie je spojený so žiadnym používateľom. Pripojte ho prosím v nastaveniach Vášho profilu. ',\n    'social_account_register_instructions' => 'Ak zatiaľ nemáte účet, môžete sa registrovať pomocou možnosti :socialAccount.',\n    'social_driver_not_found' => 'Ovládač socialnych sietí nebol nájdený',\n    'social_driver_not_configured' => 'Nastavenia Vášho :socialAccount účtu nie sú správne.',\n    'invite_token_expired' => 'Platnosť tohto odkazu na pozvánku vypršala. Namiesto toho sa môžete pokúsiť obnoviť heslo účtu.',\n    'login_user_not_found' => 'A user for this action could not be found.',\n\n    // System\n    'path_not_writable' => 'Do cesty :filePath sa nedá nahrávať. Uistite sa, že je zapisovateľná serverom.',\n    'cannot_get_image_from_url' => 'Nedá sa získať obrázok z :url',\n    'cannot_create_thumbs' => 'Server nedokáže vytvoriť náhľady. Skontrolujte prosím, či máte nainštalované GD rozšírenie PHP.',\n    'server_upload_limit' => 'Server nedovoľuje nahrávanie súborov s takouto veľkosťou. Skúste prosím menší súbor.',\n    'server_post_limit' => 'The server cannot receive the provided amount of data. Try again with less data or a smaller file.',\n    'uploaded'  => 'Server nedovoľuje nahrávanie súborov s takouto veľkosťou. Skúste prosím menší súbor.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Pri nahrávaní obrázka nastala chyba',\n    'image_upload_type_error' => 'Typ nahrávaného obrázka je neplatný',\n    'image_upload_replace_type' => 'Image file replacements must be of the same type',\n    'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.',\n    'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.',\n    'image_gallery_thumbnail_memory_limit' => 'Failed to create gallery thumbnails due to system resource limits.',\n    'drawing_data_not_found' => 'Údaje výkresu sa nepodarilo načítať. Súbor výkresu už možno neexistuje alebo nemáte povolenie na prístup k nemu.',\n\n    // Attachments\n    'attachment_not_found' => 'Príloha nenájdená',\n    'attachment_upload_error' => 'Pri nahrávaní súboru prílohy nastala chyba',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Koncept nemohol byť uložený. Uistite sa, že máte pripojenie k internetu pre uložením tejto stránky',\n    'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content',\n    'page_custom_home_deletion' => 'Stránku nie je možné odstrániť, kým je nastavená ako domovská stránka',\n\n    // Entities\n    'entity_not_found' => 'Entita nenájdená',\n    'bookshelf_not_found' => 'Polica nenájdená',\n    'book_not_found' => 'Kniha nenájdená',\n    'page_not_found' => 'Stránka nenájdená',\n    'chapter_not_found' => 'Kapitola nenájdená',\n    'selected_book_not_found' => 'Vybraná kniha nebola nájdená',\n    'selected_book_chapter_not_found' => 'Vybraná kniha alebo kapitola nebola nájdená',\n    'guests_cannot_save_drafts' => 'Hosť nemôže ukladať koncepty',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Nemôžete zmazať posledného správcu',\n    'users_cannot_delete_guest' => 'Nemôžete zmazať hosťa',\n    'users_could_not_send_invite' => 'Could not create user since invite email failed to send',\n\n    // Roles\n    'role_cannot_be_edited' => 'Táto rola nemôže byť upravovaná',\n    'role_system_cannot_be_deleted' => 'Táto rola je systémová rola a nemôže byť zmazaná',\n    'role_registration_default_cannot_delete' => 'Táto rola nemôže byť zmazaná, pretože je nastavená ako prednastavená rola pri registrácii',\n    'role_cannot_remove_only_admin' => 'Tento používateľ je jediným používateľom priradeným k role správcu. Priraďte rolu správcu inému používateľovi skôr, ako sa ju pokúsite odstrániť tu.',\n\n    // Comments\n    'comment_list' => 'Pri načítaní komentárov sa vyskytla chyba',\n    'cannot_add_comment_to_draft' => 'Do konceptu nemôžete pridávať komentáre.',\n    'comment_add' => 'Počas pridávania komentára sa vyskytla chyba',\n    'comment_delete' => 'Pri odstraňovaní komentára došlo k chybe',\n    'empty_comment' => 'Nelze pridať prázdny komentár.',\n\n    // Error pages\n    '404_page_not_found' => 'Stránka nenájdená',\n    'sorry_page_not_found' => 'Prepáčte, stránka ktorú hľadáte nebola nájdená.',\n    'sorry_page_not_found_permission_warning' => 'Ak ste očakávali existenciu tejto stránky, možno nemáte povolenie na jej zobrazenie.',\n    'image_not_found' => 'Obrázok nebol nájdený',\n    'image_not_found_subtitle' => 'Ľutujeme, obrázok, ktorý ste hľadali, sa nepodarilo nájsť.',\n    'image_not_found_details' => 'Ak ste očakávali, že tento obrázok existuje, mohol byť odstránený.',\n    'return_home' => 'Vrátiť sa domov',\n    'error_occurred' => 'Nastala chyba',\n    'app_down' => ':appName je momentálne nedostupná',\n    'back_soon' => 'Čoskoro bude opäť dostupná.',\n\n    // Import\n    'import_zip_cant_read' => 'Could not read ZIP file.',\n    'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',\n    'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Import ZIP failed to validate with errors:',\n    'import_zip_failed_notification' => 'Failed to import ZIP file.',\n    'import_perms_books' => 'You are lacking the required permissions to create books.',\n    'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',\n    'import_perms_pages' => 'You are lacking the required permissions to create pages.',\n    'import_perms_images' => 'You are lacking the required permissions to create images.',\n    'import_perms_attachments' => 'You are lacking the required permission to create attachments.',\n\n    // API errors\n    'api_no_authorization_found' => 'V žiadosti sa nenašiel žiadny autorizačný token',\n    'api_bad_authorization_format' => 'V žiadosti sa našiel autorizačný token, ale formát sa zdal nesprávny',\n    'api_user_token_not_found' => 'Pre poskytnutý autorizačný token sa nenašiel žiadny zodpovedajúci token rozhrania API',\n    'api_incorrect_token_secret' => 'Secret poskytnutý pre daný token API je nesprávny',\n    'api_user_no_api_permission' => 'Vlastník použitého tokenu API nemá povolenie na uskutočňovanie volaní rozhrania API',\n    'api_user_token_expired' => 'Platnosť použitého autorizačného tokenu vypršala',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Chyba pri odosielaní testovacieho e-mailu:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'The URL does not match the configured allowed SSR hosts',\n];\n"
  },
  {
    "path": "lang/sk/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'New comment on page: :pageName',\n    'new_comment_intro' => 'A user has commented on a page in :appName:',\n    'new_page_subject' => 'New page: :pageName',\n    'new_page_intro' => 'A new page has been created in :appName:',\n    'updated_page_subject' => 'Updated page: :pageName',\n    'updated_page_intro' => 'A page has been updated in :appName:',\n    'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\\'t be sent notifications for further edits to this page by the same editor.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Page Name:',\n    'detail_page_path' => 'Page Path:',\n    'detail_commenter' => 'Commenter:',\n    'detail_comment' => 'Comment:',\n    'detail_created_by' => 'Created By:',\n    'detail_updated_by' => 'Updated By:',\n\n    'action_view_comment' => 'View Comment',\n    'action_view_page' => 'View Page',\n\n    'footer_reason' => 'This notification was sent to you because :link cover this type of activity for this item.',\n    'footer_reason_link' => 'your notification preferences',\n];\n"
  },
  {
    "path": "lang/sk/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Predchádzajúca',\n    'next'     => 'Ďalšia &raquo;',\n\n];\n"
  },
  {
    "path": "lang/sk/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Heslo musí obsahovať aspoň osem znakov a musí byť rovnaké ako potvrdzujúce.',\n    'user' => \"Nenašli sme používateľa s takou emailovou adresou.\",\n    'token' => 'Token na obnovenie hesla je pre túto e-mailovú adresu neplatný.',\n    'sent' => 'Poslali sme Vám email s odkazom na reset hesla!',\n    'reset' => 'Vaše heslo bolo resetované!',\n\n];\n"
  },
  {
    "path": "lang/sk/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'My Account',\n\n    'shortcuts' => 'Skratky',\n    'shortcuts_interface' => 'UI Shortcut Preferences',\n    'shortcuts_toggle_desc' => 'Tu môžete povoliť alebo zakázať klávesové skratky systémového rozhrania, ktoré sa používajú na navigáciu a akcie.',\n    'shortcuts_customize_desc' => 'Každú z nižšie uvedených skratiek si môžete prispôsobiť. Po výbere vstupu pre skratku stačí stlačiť požadovanú kombináciu klávesov.',\n    'shortcuts_toggle_label' => 'Klávesové skratky sú povolené',\n    'shortcuts_section_navigation' => 'Navigácia',\n    'shortcuts_section_actions' => 'Hlavné akcie',\n    'shortcuts_save' => 'Uložiť skratky',\n    'shortcuts_overlay_desc' => 'Poznámka: Keď sú zapnuté skratky, pomocné prekrytie je dostupné stlačením „?\", ktoré zvýrazní dostupné skratky akcií,, ktoré sú momentálne viditeľné na obrazovke.',\n    'shortcuts_update_success' => 'Predvoľby skratiek boli aktualizované!',\n    'shortcuts_overview_desc' => 'Manage keyboard shortcuts you can use to navigate the system user interface.',\n\n    'notifications' => 'Notification Preferences',\n    'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.',\n    'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own',\n    'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Notify upon replies to my comments',\n    'notifications_save' => 'Save Preferences',\n    'notifications_update_success' => 'Notification preferences have been updated!',\n    'notifications_watched' => 'Watched & Ignored Items',\n    'notifications_watched_desc' => 'Below are the items that have custom watch preferences applied. To update your preferences for these, view the item then find the watch options in the sidebar.',\n\n    'auth' => 'Access & Security',\n    'auth_change_password' => 'Change Password',\n    'auth_change_password_desc' => 'Change the password you use to log-in to the application. This must be at least 8 characters long.',\n    'auth_change_password_success' => 'Password has been updated!',\n\n    'profile' => 'Profile Details',\n    'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.',\n    'profile_view_public' => 'View Public Profile',\n    'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.',\n    'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.',\n    'profile_email_no_permission' => 'Unfortunately you don\\'t have permission to change your email address. If you want to change this, you\\'d need to ask an administrator to change this for you.',\n    'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.',\n    'profile_admin_options' => 'Administrator Options',\n    'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the \"Settings > Users\" area of the application.',\n\n    'delete_account' => 'Delete Account',\n    'delete_my_account' => 'Delete My Account',\n    'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\\'ve created, such as created pages and uploaded images, will remain.',\n    'delete_my_account_warning' => 'Are you sure you want to delete your account?',\n];\n"
  },
  {
    "path": "lang/sk/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Nastavenia',\n    'settings_save' => 'Uložiť nastavenia',\n    'system_version' => 'Verzia systému',\n    'categories' => 'Kategórie',\n\n    // App Settings\n    'app_customization' => 'Prispôsobenia',\n    'app_features_security' => 'Funkcie a bezpečnosť',\n    'app_name' => 'Názov aplikácia',\n    'app_name_desc' => 'Tento názov sa zobrazuje v hlavičke a v emailoch.',\n    'app_name_header' => 'Zobraziť názov aplikácie v hlavičke?',\n    'app_public_access' => 'Verejný prístup',\n    'app_public_access_desc' => 'Povolenie tejto možnosti umožní návštevníkom, ktorí nie sú prihlásení, prístup k obsahu vo vašej inštancii BookStack.',\n    'app_public_access_desc_guest' => 'Prístup pre verejných návštevníkov je možné ovládať prostredníctvom používateľa \"Hosť\".',\n    'app_public_access_toggle' => 'Povoliť verejný prístup',\n    'app_public_viewing' => 'Povoliť verejné zobrazenie?',\n    'app_secure_images' => 'Povoliť nahrávanie súborov so zvýšeným zabezpečením?',\n    'app_secure_images_toggle' => 'Povoliť nahrávanie obrázkov s vyšším zabezpečením',\n    'app_secure_images_desc' => 'Kvôli výkonu sú všetky obrázky verejné. Táto možnosť pridá pred URL obrázka náhodný, ťažko uhádnuteľný reťazec. Aby ste zabránili jednoduchému prístupu, uistite sa, že indexy priečinkov nie sú povolené.',\n    'app_default_editor' => 'Predvolený editor stránky',\n    'app_default_editor_desc' => 'Vyberte, ktorý editor sa bude používať ako predvolený pri úprave nových stránok. Je to možné prepísať na úrovni stránky, kde to umožňujú povolenia.',\n    'app_custom_html' => 'Vlastný HTML obsah hlavičky',\n    'app_custom_html_desc' => 'Všetok text pridaný sem bude vložený naspodok <head> sekcie na každej stránke. Môže sa to zísť pri zmene štýlu alebo pre pridanie analytického kódu.',\n    'app_custom_html_disabled_notice' => 'Vlastný obsah hlavičky HTML je na tejto stránke s nastaveniami zakázaný, aby sa zabezpečilo, že sa dajú vrátiť zmeny, ktoré nastali.',\n    'app_logo' => 'Logo aplikácie',\n    'app_logo_desc' => 'Používa sa to okrem iného v lište hlavičky aplikácie. Tento obrázok by mal mať výšku 86 pixelov. Veľké obrázky budú zmenšené.',\n    'app_icon' => 'Ikona aplikácie',\n    'app_icon_desc' => 'Táto ikona sa používa pre karty prehliadača a ikony odkazov. Mala by byť vo formáte štvorcového obrázku PNG s veľkosťou 256 pixelov.',\n    'app_homepage' => 'Domovská stránka aplikácie',\n    'app_homepage_desc' => 'Vyberte zobrazenie, ktoré sa má zobraziť na domovskej stránke namiesto predvoleného zobrazenia. Povolenia stránky sa pre vybraté stránky ignorujú.',\n    'app_homepage_select' => 'Vybrať stránku',\n    'app_footer_links' => 'Odkazy v pätičke',\n    'app_footer_links_desc' => 'Pridajte odkazy, ktoré sa majú zobraziť v päte lokality. Tieto sa zobrazia v spodnej časti väčšiny stránok, vrátane tých, ktoré nevyžadujú prihlásenie. Ak chcete použiť preklady definované systémom, môžete použiť označenie \"trans::<key>\". Napríklad: Použitie „trans::common.privacy_policy“ poskytne preložený text „Zásady ochrany osobných údajov“ a „trans::common.terms_of_service“ poskytne preložený text „Zmluvné podmienky“.',\n    'app_footer_links_label' => 'Označenie odkazu',\n    'app_footer_links_url' => 'URL odkaz',\n    'app_footer_links_add' => 'Pridať odkaz na pätu',\n    'app_disable_comments' => 'Zakázať komentáre',\n    'app_disable_comments_toggle' => 'Vypnúť komentáre',\n    'app_disable_comments_desc' => 'Zakázať komentáre na všetkých stránkach aplikácie. Existujúce komentáre sa nezobrazujú.',\n\n    // Color settings\n    'color_scheme' => 'Farebná schéma aplikácie',\n    'color_scheme_desc' => 'Nastavte farby, ktoré sa majú použiť v používateľskom rozhraní aplikácie. Farby možno konfigurovať oddelene pre tmavý a svetlý režim, aby čo najlepšie vyhovovali téme a zabezpečili čitateľnosť.',\n    'ui_colors_desc' => 'Nastavte primárnu farbu aplikácie a predvolenú farbu odkazu. Primárna farba sa používa hlavne pre banner hlavičky, tlačidlá a dekorácie rozhrania. Predvolená farba odkazu sa používa pre textové odkazy a akcie v rámci písaného obsahu aj v rozhraní aplikácie.',\n    'app_color' => 'Hlavná farba',\n    'link_color' => 'Predvolená farba odkazu',\n    'content_colors_desc' => 'Nastaví farby pre všetky prvky v hierarchii organizácie stránky. Kvôli čitateľnosti sa odporúča vybrať farby s podobným jasom ako predvolené farby.',\n    'bookshelf_color' => 'Farba police',\n    'book_color' => 'Farba knihy',\n    'chapter_color' => 'Farba kapitoly',\n    'page_color' => 'Farba stránky',\n    'page_draft_color' => 'Farba konceptu stránky',\n\n    // Registration Settings\n    'reg_settings' => 'Nastavenia registrácie',\n    'reg_enable' => 'Povolenie registrácie',\n    'reg_enable_toggle' => 'Povoliť registrácie',\n    'reg_enable_desc' => 'Keď je registrácia povolená, používateľ sa bude môcť prihlásiť ako používateľ aplikácie. Po registrácii dostane predvolenú používateľskú rolu.',\n    'reg_default_role' => 'Prednastavená používateľská rola po registrácii',\n    'reg_enable_external_warning' => 'Ak je aktívna externá autentifikácia LDAP alebo SAML, možnosť vyššie sa ignoruje. Používateľské účty pre neexistujúcich členov sa vytvoria automaticky, ak je overenie proti používanému externému systému úspešné.',\n    'reg_email_confirmation' => 'Potvrdenie e-mailom',\n    'reg_email_confirmation_toggle' => 'Vyžadovať potvrdenie e-mailom',\n    'reg_confirm_email_desc' => 'Ak je použité obmedzenie domény, potom bude vyžadované overenie emailu a hodnota nižšie bude ignorovaná.',\n    'reg_confirm_restrict_domain' => 'Obmedziť registráciu na doménu',\n    'reg_confirm_restrict_domain_desc' => 'Zadajte zoznam domén, pre ktoré chcete povoliť registráciu oddelených čiarkou. Používatelia dostanú email kvôli overeniu adresy predtým ako im bude dovolené používať aplikáciu. <br> Používatelia si budú môcť po úspešnej registrácii zmeniť svoju emailovú adresu.',\n    'reg_confirm_restrict_domain_placeholder' => 'Nie sú nastavené žiadne obmedzenia',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Údržba',\n    'maint_image_cleanup' => 'Prečistenie obrázkov',\n    'maint_image_cleanup_desc' => 'Skenovať obsah stránky a revízie, aby sa skontrolovalo, ktoré obrázky a návrhy sa momentálne používajú a ktoré obrázky sú nadbytočné. Pred spustením sa uistite, že ste vytvorili úplnú zálohu obrazu a databázy.',\n    'maint_delete_images_only_in_revisions' => 'Odstráňte aj obrázky, ktoré existujú iba v starých revíziách stránok',\n    'maint_image_cleanup_run' => 'Spustiť prečistenie',\n    'maint_image_cleanup_warning' => ':count nájdených potenciálne nepoužitých obrázkov. Naozaj chcete odstrániť tieto obrázky?',\n    'maint_image_cleanup_success' => ':count potenciálne nepoužité obrázky boli nájdené a odstránené!',\n    'maint_image_cleanup_nothing_found' => 'Žiadne nepoužit obrázky neboli nájdené. Nič sa nezmazalo!',\n    'maint_send_test_email' => 'Odoslať testovací email',\n    'maint_send_test_email_desc' => 'Týmto sa odošle testovací e-mail na vašu e-mailovú adresu uvedenú vo vašom profile.',\n    'maint_send_test_email_run' => 'Odoslať testovací email',\n    'maint_send_test_email_success' => 'Email odoslaný na :address',\n    'maint_send_test_email_mail_subject' => 'Testovací email',\n    'maint_send_test_email_mail_greeting' => 'Zdá sa, že doručovanie e-mailov funguje!',\n    'maint_send_test_email_mail_text' => 'Gratulujeme! Keď ste dostali toto e-mailové upozornenie, zdá sa, že vaše nastavenia e-mailu sú nakonfigurované správne.',\n    'maint_recycle_bin_desc' => 'Vymazané police, knihy, kapitoly a strany sa odošlú do koša, aby sa dali obnoviť alebo natrvalo odstrániť. Staršie položky z koša môžu byť po chvíli automaticky odstránené v závislosti od konfigurácie systému.',\n    'maint_recycle_bin_open' => 'Otvoriť kôš',\n    'maint_regen_references' => 'Obnoviť referencie',\n    'maint_regen_references_desc' => 'Táto akcia znovu vytvorí referenčný index medzi položkami v databáze. Toto sa zvyčajne vykonáva automaticky, ale táto akcia môže byť užitočná na indexovanie starého obsahu alebo obsahu pridaného neoficiálnymi metódami.',\n    'maint_regen_references_success' => 'Referenčný index bol vygenerovaný!',\n    'maint_timeout_command_note' => 'Poznámka: Spustenie tejto akcie môže chvíľu trvať, čo môže v niektorých webových prostrediach viesť k problémom s časovým limitom. Alternatívne sa táto akcia vykoná pomocou príkazu v termináli.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Kôš',\n    'recycle_bin_desc' => 'Tu môžete obnoviť položky, ktoré boli odstránené, alebo zvoliť ich trvalé odstránenie zo systému. Tento zoznam je nefiltrovaný na rozdiel od podobných zoznamov aktivít v systéme, kde sa používajú filtre povolení.',\n    'recycle_bin_deleted_item' => 'Odstránené položky',\n    'recycle_bin_deleted_parent' => 'Nadradené',\n    'recycle_bin_deleted_by' => 'Zmazal(a)',\n    'recycle_bin_deleted_at' => 'Čas odstránenia',\n    'recycle_bin_permanently_delete' => 'Natrvalo odstrániť',\n    'recycle_bin_restore' => 'Obnoviť',\n    'recycle_bin_contents_empty' => 'Kôš je aktuálne prázdny',\n    'recycle_bin_empty' => 'Vyprázdniť Kôš',\n    'recycle_bin_empty_confirm' => 'Tým sa natrvalo odstránia všetky položky v koši vrátane obsahu obsiahnutého v každej položke. Naozaj chcete vyprázdniť kôš?',\n    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?',\n    'recycle_bin_destroy_list' => 'Položky, ktoré budú odstránené',\n    'recycle_bin_restore_list' => 'Položky, ktoré budú obnovené',\n    'recycle_bin_restore_confirm' => 'Táto akcia obnoví odstránenú položku vrátane všetkých podradených prvkov na ich pôvodné miesto. Ak bolo pôvodné umiestnenie medzitým odstránené a teraz je v koši, bude potrebné obnoviť aj nadradenú položku.',\n    'recycle_bin_restore_deleted_parent' => 'Nadradená položka tejto položky bola tiež odstránená. Položka zostane odstránená, kým nebude obnovený aj nadradená položka.',\n    'recycle_bin_restore_parent' => 'Obnoviť nadradenú položku',\n    'recycle_bin_destroy_notification' => 'Vymazané :count položky z koša.',\n    'recycle_bin_restore_notification' => 'Obnovené :count položky z koša.',\n\n    // Audit Log\n    'audit' => 'Denník auditu',\n    'audit_desc' => 'Tento denník auditu zobrazuje zoznam aktivít sledovaných v systéme. Tento zoznam je nefiltrovaný na rozdiel od podobných zoznamov aktivít v systéme, kde sa používajú filtre povolení.',\n    'audit_event_filter' => 'Filter udalostí',\n    'audit_event_filter_no_filter' => 'Žiadny filter',\n    'audit_deleted_item' => 'Odstránená položka',\n    'audit_deleted_item_name' => 'Názov :name',\n    'audit_table_user' => 'Užívateľ',\n    'audit_table_event' => 'Udalosť',\n    'audit_table_related' => 'Súvisiaca položka alebo detail',\n    'audit_table_ip' => 'IP adresa',\n    'audit_table_date' => 'Dátum aktivity',\n    'audit_date_from' => 'Časový interval od',\n    'audit_date_to' => 'Časový interval',\n\n    // Role Settings\n    'roles' => 'Roly',\n    'role_user_roles' => 'Používateľské roly',\n    'roles_index_desc' => 'Roly sa používajú na zoskupovanie používateľov a poskytovanie systémových povolení ich členom. Keď je používateľ členom viacerých rolí, udelené privilégiá sa nahromadia a používateľ zdedí všetky schopnosti.',\n    'roles_x_users_assigned' => ':count user assigned|:count users assigned',\n    'roles_x_permissions_provided' => ':počet povolení|:počet povolení',\n    'roles_assigned_users' => 'Priradení užívatelia',\n    'roles_permissions_provided' => 'Poskytnuté povolenia',\n    'role_create' => 'Vytvoriť novú rolu',\n    'role_delete' => 'Zmazať rolu',\n    'role_delete_confirm' => 'Toto zmaže rolu menom \\':roleName\\'.',\n    'role_delete_users_assigned' => 'Túto rolu má priradenú :userCount používateľov. Ak chcete premigrovať používateľov z tejto roly, vyberte novú rolu nižšie.',\n    'role_delete_no_migration' => \"Nemigrovať používateľov\",\n    'role_delete_sure' => 'Ste si istý, že chcete zmazať túto rolu?',\n    'role_edit' => 'Upraviť rolu',\n    'role_details' => 'Detaily roly',\n    'role_name' => 'Názov roly',\n    'role_desc' => 'Krátky popis roly',\n    'role_mfa_enforced' => 'Vyžadovať viacfaktorové overenie',\n    'role_external_auth_id' => 'Externé autentifikačné ID',\n    'role_system' => 'Systémové oprávnenia',\n    'role_manage_users' => 'Spravovať používateľov',\n    'role_manage_roles' => 'Spravovať role a oprávnenia rolí',\n    'role_manage_entity_permissions' => 'Spravovať všetky oprávnenia kníh, kapitol a stránok',\n    'role_manage_own_entity_permissions' => 'Spravovať oprávnenia vlastných kníh, kapitol a stránok',\n    'role_manage_page_templates' => 'Spravovať šablóny',\n    'role_access_api' => 'API prístupového systému',\n    'role_manage_settings' => 'Spravovať nastavenia aplikácie',\n    'role_export_content' => 'Exportovať obsah',\n    'role_import_content' => 'Import content',\n    'role_editor_change' => 'Zmeniť editor stránky',\n    'role_notifications' => 'Receive & manage notifications',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Oprávnenia majetku',\n    'roles_system_warning' => 'Uvedomte si, že prístup ku ktorémukoľvek z vyššie uvedených troch povolení môže používateľovi umožniť zmeniť svoje vlastné privilégiá alebo privilégiá ostatných v systéme. Roly s týmito povoleniami priraďujte iba dôveryhodným používateľom.',\n    'role_asset_desc' => 'Tieto oprávnenia regulujú prednastavený prístup k zdroju v systéme. Oprávnenia pre knihy, kapitoly a stránky majú vyššiu prioritu.',\n    'role_asset_admins' => 'Správcovia majú automaticky prístup ku všetkému obsahu, ale tieto možnosti môžu zobraziť alebo skryť možnosti používateľského rozhrania.',\n    'role_asset_image_view_note' => 'Toto sa týka viditeľnosti v rámci správcu obrázkov. Skutočný prístup k nahratým súborom obrázkov bude závisieť od možnosti ukladania obrázkov systému.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'Všetko',\n    'role_own' => 'Vlastné',\n    'role_controlled_by_asset' => 'Regulované zdrojom, do ktorého sú nahrané',\n    'role_save' => 'Uložiť rolu',\n    'role_users' => 'Používatelia s touto rolou',\n    'role_users_none' => 'Žiadni používatelia nemajú priradenú túto rolu',\n\n    // Users\n    'users' => 'Používatelia',\n    'users_index_desc' => 'Vytvárajte a spravujte individuálne používateľské účty v rámci systému. Používateľské účty sa používajú na prihlásenie a priradenie obsahu a aktivity. Prístupové povolenia sú primárne založené na rolách, ale vlastníctvo obsahu používateľa popri iných faktoroch môže mať vplyv aj na povolenia a prístup.',\n    'user_profile' => 'Profil používateľa',\n    'users_add_new' => 'Pridať nového používateľa',\n    'users_search' => 'Hľadať medzi používateľmi',\n    'users_latest_activity' => 'Nedávna aktivita',\n    'users_details' => 'Údaje o používateľovi',\n    'users_details_desc' => 'Nastavte zobrazované meno a e-mailovú adresu pre tohto používateľa. E-mailová adresa bude slúžiť na prihlásenie do aplikácie.',\n    'users_details_desc_no_email' => 'Nastavte zobrazované meno pre tohto používateľa, aby ho ostatní mohli rozpoznať.',\n    'users_role' => 'Používateľské roly',\n    'users_role_desc' => 'Vyberte, ku ktorým rolám bude tento používateľ priradený. Ak je používateľ priradený k viacerým rolám, povolenia z týchto rolí sa nahromadia a získajú všetky schopnosti priradených rolí.',\n    'users_password' => 'Heslo používateľa',\n    'users_password_desc' => 'Nastavte heslo používané na prihlásenie do aplikácie. Musí mať aspoň 8 znakov.',\n    'users_send_invite_text' => 'Môžete sa rozhodnúť poslať tomuto používateľovi e-mail s pozvánkou, ktorý mu umožní nastaviť si vlastné heslo, v opačnom prípade mu ho môžete nastaviť sami.',\n    'users_send_invite_option' => 'Odoslať e-mail s pozvánkou pre používateľa',\n    'users_external_auth_id' => 'Externé autentifikačné ID',\n    'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.',\n    'users_password_warning' => 'Only fill the below if you would like to change the password for this user.',\n    'users_system_public' => 'Tento účet reprezentuje každého hosťovského používateľa, ktorý navštívi Vašu inštanciu. Nedá sa pomocou neho prihlásiť a je priradený automaticky.',\n    'users_delete' => 'Zmazať používateľa',\n    'users_delete_named' => 'Zmazať používateľa :userName',\n    'users_delete_warning' => ' Toto úplne odstráni používateľa menom \\':userName\\' zo systému.',\n    'users_delete_confirm' => 'Ste si istý, že chcete zmazať tohoto používateľa?',\n    'users_migrate_ownership' => 'Migrovať vlastníctvo',\n    'users_migrate_ownership_desc' => 'Tu vyberte používateľa, ak chcete, aby sa vlastníkom všetkých položiek aktuálne vlastnených týmto používateľom stal iný používateľ.',\n    'users_none_selected' => 'Nie je vybratý žiadny používateľ',\n    'users_edit' => 'Upraviť používateľa',\n    'users_edit_profile' => 'Upraviť profil',\n    'users_avatar' => 'Avatar používateľa',\n    'users_avatar_desc' => 'Tento obrázok by mal byť štvorec s rozmerom približne 256px.',\n    'users_preferred_language' => 'Preferovaný jazyk',\n    'users_preferred_language_desc' => 'Táto možnosť zmení jazyk používaný pre používateľské rozhranie aplikácie. Neovplyvní to žiadny obsah vytvorený používateľmi.',\n    'users_social_accounts' => 'Sociálne účty',\n    'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.',\n    'users_social_accounts_info' => 'Tu si môžete pripojiť iné účty pre rýchlejšie a jednoduchšie prihlásenie. Disconnecting an account here does not previously authorized access. Revoke access from your profile settings on the connected social account.',\n    'users_social_connect' => 'Pripojiť účet',\n    'users_social_disconnect' => 'Odpojiť účet',\n    'users_social_status_connected' => 'Connected',\n    'users_social_status_disconnected' => 'Disconnected',\n    'users_social_connected' => ':socialAccount účet bol úspešne pripojený k Vášmu profilu.',\n    'users_social_disconnected' => ':socialAccount účet bol úspešne odpojený od Vášho profilu.',\n    'users_api_tokens' => 'API Kľúče',\n    'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.',\n    'users_api_tokens_none' => 'Pre tohto používateľa neboli vytvorené žiadne tokeny API',\n    'users_api_tokens_create' => 'Vytvoriť token',\n    'users_api_tokens_expires' => 'Platnosť do',\n    'users_api_tokens_docs' => 'Dokumentácia API',\n    'users_mfa' => 'Viacstupňové overovanie',\n    'users_mfa_desc' => 'Pre vyššiu úroveň bezpečnosti si nastavte viacúrovňové prihlasovanie.',\n    'users_mfa_x_methods' => ':count nakonfigurované metódy|:count nakonfigurovaných metód',\n    'users_mfa_configure' => 'Konfigurovať metódy',\n\n    // API Tokens\n    'user_api_token_create' => 'Vytvoriť API token',\n    'user_api_token_name' => 'Názov',\n    'user_api_token_name_desc' => 'Dajte svojmu tokenu čitateľný názov ako budúcu pripomienku jeho zamýšľaného účelu.',\n    'user_api_token_expiry' => 'Dátum expirácie',\n    'user_api_token_expiry_desc' => 'Nastavte dátum, kedy platnosť tohto tokenu vyprší. Po tomto dátume už žiadosti uskutočnené pomocou tohto tokenu nebudú fungovať. Ak toto pole ponecháte prázdne, nastaví sa uplynutie platnosti o 100 rokov do budúcnosti.',\n    'user_api_token_create_secret_message' => 'Ihneď po vytvorení tohto tokenu sa vygeneruje a zobrazí \"Token ID\" a \"Token Secret\". Kľúč sa zobrazí iba raz, takže pred pokračovaním nezabudnite skopírovať hodnotu na bezpečné a zabezpečené miesto.',\n    'user_api_token' => 'API Token',\n    'user_api_token_id' => 'Token ID',\n    'user_api_token_id_desc' => 'Toto je neupraviteľný identifikátor vygenerovaný systémom pre tento token, ktorý bude potrebné poskytnúť v žiadostiach API.',\n    'user_api_token_secret' => 'Kľúč',\n    'user_api_token_secret_desc' => 'Toto je systémom vygenerovaný kľúč pre tento token, ktorý bude potrebné poskytnúť v žiadostiach API. Toto sa zobrazí iba raz, takže túto hodnotu skopírujte na bezpečné a bezpečné miesto.',\n    'user_api_token_created' => 'Token vytvorený :timeAgo',\n    'user_api_token_updated' => 'Token upravený :timeAgo',\n    'user_api_token_delete' => 'Zmazať Token',\n    'user_api_token_delete_warning' => 'Týmto sa tento token API s názvom \\':tokenName\\' úplne odstráni zo systému.',\n    'user_api_token_delete_confirm' => 'Určite chcete odstrániť tento token?',\n\n    // Webhooks\n    'webhooks' => 'Webhooky',\n    'webhooks_index_desc' => 'Webhooky predstavujú spôsob odosielania údajov na externé adresy URL, keď sa v systéme vyskytnú určité akcie a udalosti, čo umožňuje integráciu založenú na udalostiach s externými platformami, ako sú systémy na odosielanie správ alebo notifikačné systémy.',\n    'webhooks_x_trigger_events' => ':count trigger event|:count trigger events',\n    'webhooks_create' => 'Vytvoriť nový webhook',\n    'webhooks_none_created' => 'Žiadne webhooky zatiaľ neboli vytvorené.',\n    'webhooks_edit' => 'Upraviť Webhook',\n    'webhooks_save' => 'Uložiť Webhook',\n    'webhooks_details' => 'Detaily Webhooku',\n    'webhooks_details_desc' => 'Poskytnite užívateľsky prívetivý názov a koncový bod POST ako miesto, kam sa majú odosielať údaje webhooku.',\n    'webhooks_events' => 'Udalosti webhooku',\n    'webhooks_events_desc' => 'Vyberte všetky udalosti, ktoré by mali spustiť volanie tohto webhooku.',\n    'webhooks_events_warning' => 'Majte na pamäti, že tieto udalosti sa spustia pre všetky vybraté udalosti, aj keď sa používajú vlastné povolenia. Uistite sa, že používanie tohto webhooku neodhalí dôverný obsah.',\n    'webhooks_events_all' => 'Všetky systémové udalosti',\n    'webhooks_name' => 'Názov webhooku',\n    'webhooks_timeout' => 'Časový limit žiadosti webhooku (v sekundách)',\n    'webhooks_endpoint' => 'Koncový bod webhooku',\n    'webhooks_active' => 'Webhook je aktívny',\n    'webhook_events_table_header' => 'Udalosti',\n    'webhooks_delete' => 'Odstrániť webhook',\n    'webhooks_delete_warning' => 'Týmto sa tento webhook s názvom „:webhookName“ úplne odstráni zo systému.',\n    'webhooks_delete_confirm' => 'Naozaj chcete odstrániť tento webhook?',\n    'webhooks_format_example' => 'Príklad formátu webhooku',\n    'webhooks_format_example_desc' => 'Údaje webhooku sa odosielajú ako žiadosť POST do nakonfigurovaného koncového bodu ako JSON podľa nižšie uvedeného formátu. Vlastnosti „related_item“ a „url“ sú voliteľné a budú závisieť od typu spustenej udalosti.',\n    'webhooks_status' => 'Stav webhooku',\n    'webhooks_last_called' => 'Naposledy volané:',\n    'webhooks_last_errored' => 'Posledná chyba:',\n    'webhooks_last_error_message' => 'Posledná chybová správa:',\n\n    // Licensing\n    'licenses' => 'Licenses',\n    'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.',\n    'licenses_bookstack' => 'BookStack License',\n    'licenses_php' => 'PHP Library Licenses',\n    'licenses_js' => 'JavaScript Library Licenses',\n    'licenses_other' => 'Other Licenses',\n    'license_details' => 'License Details',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/sk/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute musí byť akceptovaný.',\n    'active_url'           => ':attribute nie je platná URL.',\n    'after'                => ':attribute musí byť dátum po :date.',\n    'alpha'                => ':attribute môže obsahovať iba písmená.',\n    'alpha_dash'           => ':attribute môže obsahovať iba písmená, čísla a pomlčky.',\n    'alpha_num'            => ':attribute môže obsahovať iba písmená a čísla.',\n    'array'                => ':attribute musí byť pole.',\n    'backup_codes'         => 'Poskytnutý kód nie je platný alebo už bol použitý.',\n    'before'               => ':attribute musí byť dátum pred :date.',\n    'between'              => [\n        'numeric' => ':attribute musí byť medzi :min a :max.',\n        'file'    => ':attribute musí byť medzi :min a :max kilobajtmi.',\n        'string'  => ':attribute musí byť medzi :min a :max znakmi.',\n        'array'   => ':attribute musí byť medzi :min a :max položkami.',\n    ],\n    'boolean'              => ':attribute pole musí byť true alebo false.',\n    'confirmed'            => ':attribute potvrdenie nesedí.',\n    'date'                 => ':attribute nie je platný dátum.',\n    'date_format'          => ':attribute nesedí s formátom :format.',\n    'different'            => ':attribute a :other musia byť rozdielne.',\n    'digits'               => ':attribute musí mať :digits číslic.',\n    'digits_between'       => ':attribute musí mať medzi :min a :max číslicami.',\n    'email'                => ':attribute musí byť platná emailová adresa.',\n    'ends_with' => ':attribute musí končiť jednou z nasledujúcich hodnôt :values',\n    'file'                 => ':attribute musí byť uvedený ako platný súbor.',\n    'filled'               => 'Políčko :attribute je povinné.',\n    'gt'                   => [\n        'numeric' => 'Hodnota :attribute musí byť väčšia ako :value.',\n        'file'    => ':attribute musí mať viac kilobajtov ako :value.',\n        'string'  => ':attribute musí mať viac znakov ako :value.',\n        'array'   => ':attribute musí mať viac ako :value položiek.',\n    ],\n    'gte'                  => [\n        'numeric' => 'Hodnota :attribute musí byť väčšia alebo rovná ako :value.',\n        'file'    => ':attribute musí mať rovnaký alebo väčší počet kilobajtov ako :value.',\n        'string'  => ':attribute musí byť väčší alebo rovnaký ako :value znakov.',\n        'array'   => ':attribute musí mať :value položiek alebo viac.',\n    ],\n    'exists'               => 'Vybraný :attribute nie je platný.',\n    'image'                => ':attribute musí byť obrázok.',\n    'image_extension'      => ':attribute musí mať platné a podporované rozšírenie obrázka.',\n    'in'                   => 'Vybraný :attribute je neplatný.',\n    'integer'              => ':attribute musí byť celé číslo.',\n    'ip'                   => ':attribute musí byť platná IP adresa.',\n    'ipv4'                 => ':attribute musí byť platná adresa IPv4.',\n    'ipv6'                 => ':attribute musí byť platná IPv6 adresa.',\n    'json'                 => ':attribute musí byť platný JSON reťazec.',\n    'lt'                   => [\n        'numeric' => 'Hodnota :attribute musí byť menšia ako :value.',\n        'file'    => ':attribute musí mať menej kilobajtov ako :value.',\n        'string'  => ':attribute musí mať menej znakov ako :value.',\n        'array'   => ':attribute musí mať menej položiek ako :value.',\n    ],\n    'lte'                  => [\n        'numeric' => 'Hodnota :attribute musí byť menšia alebo rovná ako :value.',\n        'file'    => ':attribute musí mať rovnaký alebo menší počet kilobajtov ako :value.',\n        'string'  => ':attribute musí mať rovnaký alebo menší počet znakov ako :value.',\n        'array'   => ':attribute nesmie mať viac ako :value položiek.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute nesmie byť väčší ako :max.',\n        'file'    => ':attribute nesmie byť väčší ako :max kilobajtov.',\n        'string'  => ':attribute nesmie byť dlhší ako :max znakov.',\n        'array'   => ':attribute nesmie mať viac ako :max položiek.',\n    ],\n    'mimes'                => ':attribute musí byť súbor typu: :values.',\n    'min'                  => [\n        'numeric' => ':attribute musí byť aspoň :min.',\n        'file'    => ':attribute musí mať aspoň :min kilobajtov.',\n        'string'  => ':attribute musí mať aspoň :min znakov.',\n        'array'   => ':attribute musí mať aspoň :min položiek.',\n    ],\n    'not_in'               => 'Vybraný :attribute je neplatný.',\n    'not_regex'            => ':attribute formát je neplatný.',\n    'numeric'              => ':attribute musí byť číslo.',\n    'regex'                => ':attribute formát je neplatný.',\n    'required'             => 'Políčko :attribute je povinné.',\n    'required_if'          => 'Políčko :attribute je povinné ak :other je :value.',\n    'required_with'        => 'Políčko :attribute je povinné ak :values existuje.',\n    'required_with_all'    => 'Políčko :attribute je povinné ak :values existuje.',\n    'required_without'     => 'Políčko :attribute je povinné aj :values neexistuje.',\n    'required_without_all' => 'Políčko :attribute je povinné ak ani jedno z :values neexistuje.',\n    'same'                 => ':attribute a :other musia byť rovnaké.',\n    'safe_url'             => 'Poskytnutý odkaz nemusí byť bezpečný.',\n    'size'                 => [\n        'numeric' => ':attribute musí byť :size.',\n        'file'    => ':attribute musí mať :size kilobajtov.',\n        'string'  => ':attribute musí mať :size znakov.',\n        'array'   => ':attribute musí obsahovať :size položiek.',\n    ],\n    'string'               => ':attribute musí byť reťazec.',\n    'timezone'             => ':attribute musí byť plantá časová zóna.',\n    'totp'                 => 'Poskytnutý kód nie je platný alebo už bol použitý.',\n    'unique'               => ':attribute je už použité.',\n    'url'                  => ':attribute formát je neplatný.',\n    'uploaded'             => 'Súbor sa nepodarilo nahrať. Server nemusí akceptovať súbory tejto veľkosti.',\n\n    'zip_file' => 'The :attribute needs to reference a file within the ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',\n    'zip_model_expected' => 'Data object expected but \":type\" found.',\n    'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Vyžaduje sa potvrdenie hesla',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/sl/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'ustvarjena stran',\n    'page_create_notification'    => 'Stran uspešno ustvarjena',\n    'page_update'                 => 'posodobljena stran',\n    'page_update_notification'    => 'Stran uspešno posodobljena',\n    'page_delete'                 => 'izbrisana stran',\n    'page_delete_notification'    => 'Stran uspešno izbrisana',\n    'page_restore'                => 'obnovljena stran',\n    'page_restore_notification'   => 'Stran uspešno obnovljena',\n    'page_move'                   => 'premaknjena stran',\n    'page_move_notification'      => 'Stran uspešno premaknjena',\n\n    // Chapters\n    'chapter_create'              => 'ustvarjeno poglavje',\n    'chapter_create_notification' => 'Poglavje uspešno ustvarjeno',\n    'chapter_update'              => 'posodobljeno poglavje',\n    'chapter_update_notification' => 'Poglavje uspešno posodobljeno',\n    'chapter_delete'              => 'izbrisano poglavje',\n    'chapter_delete_notification' => 'Poglavje uspešno izbrisano',\n    'chapter_move'                => 'premaknjeno poglavje',\n    'chapter_move_notification' => 'Poglavje uspešno premaknjeno',\n\n    // Books\n    'book_create'                 => 'knjiga ustvarjena',\n    'book_create_notification'    => 'Knjiga uspešno ustvarjena',\n    'book_create_from_chapter'              => 'poglavje pretvorjeno v knjigo',\n    'book_create_from_chapter_notification' => 'Poglavje uspešno pretvorjeno v knjigo',\n    'book_update'                 => 'knjiga posodobljena',\n    'book_update_notification'    => 'Knjiga uspešno posodobljena',\n    'book_delete'                 => 'izbrisana knjiga',\n    'book_delete_notification'    => 'Knjiga uspešno izbrisana',\n    'book_sort'                   => 'razvrščena knjiga',\n    'book_sort_notification'      => 'Knjiga uspešno razvrščena',\n\n    // Bookshelves\n    'bookshelf_create'            => 'knjižna polica ustvarjena',\n    'bookshelf_create_notification'    => 'Knjižna polica uspešno ustvarjena',\n    'bookshelf_create_from_book'    => 'knjiga pretvorjena v knjižno polico',\n    'bookshelf_create_from_book_notification'    => 'Knjiga uspešno pretvorjena v knjižno polico',\n    'bookshelf_update'                 => 'knjižna polica posodobljena',\n    'bookshelf_update_notification'    => 'Knjižna polica uspešno posodobljena',\n    'bookshelf_delete'                 => 'knjižna polica izbrisana',\n    'bookshelf_delete_notification'    => 'Knjižna polica uspešno izbrisana',\n\n    // Revisions\n    'revision_restore' => 'obnovljena različica',\n    'revision_delete' => 'izbrisana različica',\n    'revision_delete_notification' => 'Različica uspešno izbrisana',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" dodan med priljubljene',\n    'favourite_remove_notification' => '\":name\" odstranjen iz priljubljenih',\n\n    // Watching\n    'watch_update_level_notification' => 'Nastavitve spremljanja uspešno posodobljene',\n\n    // Auth\n    'auth_login' => 'prijavljen',\n    'auth_register' => 'registriran kot nov uporabnik',\n    'auth_password_reset_request' => 'zahteva ponastavitev uporabniškega gesla',\n    'auth_password_reset_update' => 'ponastavitev uporabniškega gesla',\n    'mfa_setup_method' => 'nastavljena metoda MFA',\n    'mfa_setup_method_notification' => 'Večfaktorska avtentikacija (MFA) uspešno nastavljena',\n    'mfa_remove_method' => 'odstranjena metoda MFA',\n    'mfa_remove_method_notification' => 'Večfaktorska avtentikacija (MFA) uspešno odstranjena',\n\n    // Settings\n    'settings_update' => 'posodobitev nastavitev',\n    'settings_update_notification' => 'Nastavitve uspešno posodobljene',\n    'maintenance_action_run' => 'zagnano vzdrževanje',\n\n    // Webhooks\n    'webhook_create' => 'ustvarjen webhook',\n    'webhook_create_notification' => 'Webhook uspešno ustvarjen',\n    'webhook_update' => 'posodobljen webhook',\n    'webhook_update_notification' => 'Webhook uspešno posodobljen',\n    'webhook_delete' => 'izbrisan webhook',\n    'webhook_delete_notification' => 'Webhook uspešno izbrisan',\n\n    // Imports\n    'import_create' => 'created import',\n    'import_create_notification' => 'Import successfully uploaded',\n    'import_run' => 'updated import',\n    'import_run_notification' => 'Content successfully imported',\n    'import_delete' => 'deleted import',\n    'import_delete_notification' => 'Import successfully deleted',\n\n    // Users\n    'user_create' => 'ustvarjen uporabnik',\n    'user_create_notification' => 'Uporabnik uspešno ustvarjen',\n    'user_update' => 'posodobljen uporabnik',\n    'user_update_notification' => 'Uporabnik uspešno posodobljen',\n    'user_delete' => 'uporabnik izbrisan',\n    'user_delete_notification' => 'Uporabnik uspešno izbrisan',\n\n    // API Tokens\n    'api_token_create' => 'ustvarjen žeton API',\n    'api_token_create_notification' => 'Žeton API uspešno ustvarjen',\n    'api_token_update' => 'posodobljen žeton API',\n    'api_token_update_notification' => 'Žeton API uspešno posodobljen',\n    'api_token_delete' => 'izbrisan žeton API',\n    'api_token_delete_notification' => 'Žeton API uspešno izbrisan',\n\n    // Roles\n    'role_create' => 'ustvarjena vloga',\n    'role_create_notification' => 'Vloga uspešno ustvarjena',\n    'role_update' => 'posodobljena vloga',\n    'role_update_notification' => 'Vloga uspešno posodobljena',\n    'role_delete' => 'izbrisana vloga',\n    'role_delete_notification' => 'Vloga uspešno izbrisana',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'izpraznjen koš',\n    'recycle_bin_restore' => 'obnovljeno iz koša',\n    'recycle_bin_destroy' => 'odstranjeno iz koša',\n\n    // Comments\n    'commented_on'                => 'komentar na',\n    'comment_create'              => 'dodan komentar',\n    'comment_update'              => 'posodobljen komentar',\n    'comment_delete'              => 'izbrisan komentar',\n\n    // Sort Rules\n    'sort_rule_create' => 'created sort rule',\n    'sort_rule_create_notification' => 'Sort rule successfully created',\n    'sort_rule_update' => 'updated sort rule',\n    'sort_rule_update_notification' => 'Sort rule successfully updated',\n    'sort_rule_delete' => 'deleted sort rule',\n    'sort_rule_delete_notification' => 'Sort rule successfully deleted',\n\n    // Other\n    'permissions_update'          => 'pravice so posodobljene',\n];\n"
  },
  {
    "path": "lang/sl/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Poverilnice se ne ujemajo s podatki v naši bazi.',\n    'throttle' => 'Prekoračili ste število možnih prijav. Poskusite znova čez :seconds sekund.',\n\n    // Login & Register\n    'sign_up' => 'Registracija',\n    'log_in' => 'Prijavi se',\n    'log_in_with' => 'Prijavi se z :socialDriver',\n    'sign_up_with' => 'Registriraj se z :socialDriver',\n    'logout' => 'Odjavi se',\n\n    'name' => 'Ime',\n    'username' => 'Uporabniško ime',\n    'email' => 'E-pošta',\n    'password' => 'Geslo',\n    'password_confirm' => 'Potrdi geslo',\n    'password_hint' => 'Must be at least 8 characters',\n    'forgot_password' => 'Pozabljeno geslo?',\n    'remember_me' => 'Zapomni si me',\n    'ldap_email_hint' => 'Prosimo vpišite e-poštni naslov za ta račun.',\n    'create_account' => 'Ustvari račun',\n    'already_have_account' => 'Že imate račun?',\n    'dont_have_account' => 'Nimaš računa?',\n    'social_login' => 'Prijava z računi družbenih omrežij',\n    'social_registration' => 'Registracija z družbenim omrežjem',\n    'social_registration_text' => 'Registriraj in prijavi se z uporabo drugih možnosti.',\n\n    'register_thanks' => 'Hvala za registracijo!',\n    'register_confirm' => 'Prosimo preverite vaš e-poštni predal in kliknite na potrditveni gumb za dostop :appName.',\n    'registrations_disabled' => 'Registracija trenutno ni mogoča',\n    'registration_email_domain_invalid' => 'Ta e-poštna domena nima dostopa do te aplikacije',\n    'register_success' => 'Hvala za registracijo! Sedaj ste registrirani in prijavljeni.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Attempting Login',\n    'auto_init_starting_desc' => 'Kontaktiram tvoj sistem za preverjanje pristnosti za pričetek procesa prijave. V primeru, da se nič ne zgodi v naslednjih 5 sekundah, klikni spodnjo povezavo.',\n    'auto_init_start_link' => 'Proceed with authentication',\n\n    // Password Reset\n    'reset_password' => 'Ponastavi geslo',\n    'reset_password_send_instructions' => 'Spodaj vpišite vaš e-poštni naslov in prejeli boste e-pošto s povezavo za ponastavitev gesla.',\n    'reset_password_send_button' => 'Pošlji povezavo za ponastavitev',\n    'reset_password_sent' => 'V kolikor e-poštni naslov :email obstaja v sistemu, bo nanj poslana povezava za ponastavitev gesla.',\n    'reset_password_success' => 'Vaše geslo je bilo uspešno spremenjeno.',\n    'email_reset_subject' => 'Ponastavi svoje :appName geslo',\n    'email_reset_text' => 'To e-poštno sporočilo ste prejeli, ker smo prejeli zahtevo za ponastavitev gesla za vaš račun.',\n    'email_reset_not_requested' => 'Če niste zahtevali ponastavitve gesla, vam ni potrebno ničesar storiti.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Potrdi svojo e-pošto za :appName',\n    'email_confirm_greeting' => 'Hvala ker ste se pridružili :appName!',\n    'email_confirm_text' => 'Potrdite svoj e-naslov s klikom spodnjega gumba:',\n    'email_confirm_action' => 'Potrdi e-pošto',\n    'email_confirm_send_error' => 'E-poštna potrditev je zahtevana ampak sistem ni mogel poslati e-pošte. Kontaktirajte administratorja, da zagotovite, da je e-pošta pravilno nastavljena.',\n    'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',\n    'email_confirm_resent' => 'Poslali smo vam potrditveno sporočilo. Prosimo preverite svojo elektronsko pošto.',\n    'email_confirm_thanks' => 'Thanks for confirming!',\n    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the \"Continue\" link below to proceed.',\n\n    'email_not_confirmed' => 'Elektronski naslov ni potrjen',\n    'email_not_confirmed_text' => 'Vaš e-naslov še ni bil potrjen.',\n    'email_not_confirmed_click_link' => 'Prosimo kliknite na link v e-poštnem sporočilu, ki ste ga prejeli kmalu po registraciji.',\n    'email_not_confirmed_resend' => 'Če ne najdete e-pošte jo lahko ponovno pošljete s potrditvijo obrazca.',\n    'email_not_confirmed_resend_button' => 'Ponovno pošlji potrditveno e-pošto',\n\n    // User Invite\n    'user_invite_email_subject' => 'Povabljen si bil da se pridružiš :appName!',\n    'user_invite_email_greeting' => 'Račun je bil ustvarjen zate na :appName.',\n    'user_invite_email_text' => 'Klikni na spodnji gumb, da si nastaviš geslo in dobiš dostop:',\n    'user_invite_email_action' => 'Nastavi geslo za račun',\n    'user_invite_page_welcome' => 'Dobrodošli na :appName!',\n    'user_invite_page_text' => 'Za zaključiti in pridobiti dostop si morate nastaviti geslo, ki bo uporabljeno za prijavo v :appName.',\n    'user_invite_page_confirm_button' => 'Potrdi geslo',\n    'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Setup Multi-Factor Authentication',\n    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'mfa_setup_configured' => 'Already configured',\n    'mfa_setup_reconfigure' => 'Reconfigure',\n    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',\n    'mfa_setup_action' => 'Setup',\n    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',\n    'mfa_option_totp_title' => 'Mobile App',\n    'mfa_option_totp_desc' => 'To use multi-factor authentication you\\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Backup Codes',\n    'mfa_option_backup_codes_desc' => 'Generates a set of one-time-use backup codes which you\\'ll enter on login to verify your identity. Make sure to store these in a safe & secure place.',\n    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',\n    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',\n    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\\'ll be able to use one of the codes as a second authentication mechanism.',\n    'mfa_gen_backup_codes_download' => 'Download Codes',\n    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',\n    'mfa_gen_totp_title' => 'Mobile App Setup',\n    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',\n    'mfa_gen_totp_verify_setup' => 'Verify Setup',\n    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',\n    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',\n    'mfa_verify_access' => 'Verify Access',\n    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\\'re granted access. Verify using one of your configured methods to continue.',\n    'mfa_verify_no_methods' => 'No Methods Configured',\n    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\\'ll need to set up at least one method before you gain access.',\n    'mfa_verify_use_totp' => 'Verify using a mobile app',\n    'mfa_verify_use_backup_codes' => 'Verify using a backup code',\n    'mfa_verify_backup_code' => 'Backup Code',\n    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',\n    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',\n    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',\n    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',\n];\n"
  },
  {
    "path": "lang/sl/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Prekliči',\n    'close' => 'Close',\n    'confirm' => 'Potrdi',\n    'back' => 'Nazaj',\n    'save' => 'Shrani',\n    'continue' => 'Naprej',\n    'select' => 'Izberi',\n    'toggle_all' => 'Vklopi vse',\n    'more' => 'Več',\n\n    // Form Labels\n    'name' => 'Naziv',\n    'description' => 'Opis',\n    'role' => 'Vloga',\n    'cover_image' => 'Naslovna slika',\n    'cover_image_description' => 'This image should be approximately 440x250px although it will be flexibly scaled & cropped to fit the user interface in different scenarios as required, so actual dimensions for display will differ.',\n\n    // Actions\n    'actions' => 'Dejanja',\n    'view' => 'Pogled',\n    'view_all' => 'Prikaži vse',\n    'new' => 'New',\n    'create' => 'Ustvari',\n    'update' => 'Posodobi',\n    'edit' => 'Uredi',\n    'archive' => 'Archive',\n    'unarchive' => 'Un-Archive',\n    'sort' => 'Razvrsti',\n    'move' => 'Premakni',\n    'copy' => 'Kopiraj',\n    'reply' => 'Odgovori',\n    'delete' => 'Izbriši',\n    'delete_confirm' => 'Potrdi brisanje',\n    'search' => 'Išči',\n    'search_clear' => 'Razveljavi iskanje',\n    'reset' => 'Ponastavi',\n    'remove' => 'Odstrani',\n    'add' => 'Dodaj',\n    'configure' => 'Configure',\n    'manage' => 'Manage',\n    'fullscreen' => 'Celozaslonski način',\n    'favourite' => 'Priljubljeno',\n    'unfavourite' => 'Ni priljubljeno',\n    'next' => 'Naprej',\n    'previous' => 'Nazaj',\n    'filter_active' => 'Active Filter:',\n    'filter_clear' => 'Počisti filter',\n    'download' => 'Download',\n    'open_in_tab' => 'Open in Tab',\n    'open' => 'Open',\n\n    // Sort Options\n    'sort_options' => 'Možnosti razvrščanja',\n    'sort_direction_toggle' => 'Preklopi smer razvrščanja',\n    'sort_ascending' => 'Razvrsti naraščajoče',\n    'sort_descending' => 'Razvrsti padajoče',\n    'sort_name' => 'Ime',\n    'sort_default' => 'Default',\n    'sort_created_at' => 'Datum nastanka',\n    'sort_updated_at' => 'Datum posodobitve',\n\n    // Misc\n    'deleted_user' => 'Izbrisan uporabnik',\n    'no_activity' => 'Ni aktivnosti za prikaz',\n    'no_items' => 'Na voljo ni nobenega elementa',\n    'back_to_top' => 'Nazaj na vrh',\n    'skip_to_main_content' => 'Skip to main content',\n    'toggle_details' => 'Preklopi podrobnosti',\n    'toggle_thumbnails' => 'Preklopi sličice',\n    'details' => 'Podrobnosti',\n    'grid_view' => 'Mrežni pogled',\n    'list_view' => 'Seznam',\n    'default' => 'Privzeto',\n    'breadcrumb' => 'Pot',\n    'status' => 'Status',\n    'status_active' => 'Active',\n    'status_inactive' => 'Inactive',\n    'never' => 'Never',\n    'none' => 'None',\n\n    // Header\n    'homepage' => 'Homepage',\n    'header_menu_expand' => 'Expand Header Menu',\n    'profile_menu' => 'Meni profila',\n    'view_profile' => 'Ogled profila',\n    'edit_profile' => 'Uredi profil',\n    'dark_mode' => 'Način temnega zaslona',\n    'light_mode' => 'Način svetlega zaslona',\n    'global_search' => 'Globalno iskanje',\n\n    // Layout tabs\n    'tab_info' => 'Informacije',\n    'tab_info_label' => 'Tab: Show Secondary Information',\n    'tab_content' => 'Vsebina',\n    'tab_content_label' => 'Tab: Show Primary Content',\n\n    // Email Content\n    'email_action_help' => 'V kolikor imate težave s klikom na gumb \":actionText\", kopirajte in prilepite spodnjo povezavo v vaš brskalnik:',\n    'email_rights' => 'Vse pravice pridržane',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Pravilnik o zasebnosti',\n    'terms_of_service' => 'Pogoji uporabe',\n\n    // OpenSearch\n    'opensearch_description' => 'Search :appName',\n];\n"
  },
  {
    "path": "lang/sl/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Izberi slike',\n    'image_list' => 'Image List',\n    'image_details' => 'Image Details',\n    'image_upload' => 'Upload Image',\n    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',\n    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the \"Upload Image\" button above.',\n    'image_all' => 'Vse',\n    'image_all_title' => 'Prikaži vse slike',\n    'image_book_title' => 'Prikaži slike naložene v to knjigo',\n    'image_page_title' => 'Preglej slike naložene na to stran',\n    'image_search_hint' => 'Iskanje po nazivu slike',\n    'image_uploaded' => 'Naloženo :uploadedDate',\n    'image_uploaded_by' => 'Uploaded by :userName',\n    'image_uploaded_to' => 'Uploaded to :pageLink',\n    'image_updated' => 'Updated :updateDate',\n    'image_load_more' => 'Dodatno naloži',\n    'image_image_name' => 'Ime slike',\n    'image_delete_used' => 'Ta slika je uporabljena na spodnjih straneh.',\n    'image_delete_confirm_text' => 'Ste prepričani, da želite izbrisati to sliko?',\n    'image_select_image' => 'Izberite sliko',\n    'image_dropzone' => 'Povlecite slike ali kliknite tukaj za nalaganje',\n    'image_dropzone_drop' => 'Drop images here to upload',\n    'images_deleted' => 'Slike so bile izbrisane',\n    'image_preview' => 'Predogled slike',\n    'image_upload_success' => 'Slika uspešno naložena',\n    'image_update_success' => 'Podatki slike uspešno posodobljeni',\n    'image_delete_success' => 'Slika uspešno izbrisana',\n    'image_replace' => 'Replace Image',\n    'image_replace_success' => 'Image file successfully updated',\n    'image_rebuild_thumbs' => 'Regenerate Size Variations',\n    'image_rebuild_thumbs_success' => 'Image size variations successfully rebuilt!',\n\n    // Code Editor\n    'code_editor' => 'Uredi kodo',\n    'code_language' => 'Koda jezika',\n    'code_content' => 'Koda vsebine',\n    'code_session_history' => 'Zgodovina seje',\n    'code_save' => 'Shrani kodo',\n];\n"
  },
  {
    "path": "lang/sl/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Splošno',\n    'advanced' => 'Napredno',\n    'none' => 'Brez',\n    'cancel' => 'Prekliči',\n    'save' => 'Shrani',\n    'close' => 'Zapri',\n    'apply' => 'Apply',\n    'undo' => 'Razveljavi',\n    'redo' => 'Ponovi',\n    'left' => 'Levo',\n    'center' => 'Sredinsko',\n    'right' => 'Desno',\n    'top' => 'Zgoraj',\n    'middle' => 'Sredina',\n    'bottom' => 'Spodaj',\n    'width' => 'Širina',\n    'height' => 'Višina',\n    'More' => 'Več',\n    'select' => 'Izberi ...',\n\n    // Toolbar\n    'formats' => 'Oblike',\n    'header_large' => 'Velika glava',\n    'header_medium' => 'Srednja glava',\n    'header_small' => 'Majhna glava',\n    'header_tiny' => 'Drobna glava',\n    'paragraph' => 'Odstavek',\n    'blockquote' => 'Navedek',\n    'inline_code' => 'Vgrajena koda',\n    'callouts' => 'Opombe',\n    'callout_information' => 'Informacija',\n    'callout_success' => 'Uspešno',\n    'callout_warning' => 'Opozorilo',\n    'callout_danger' => 'Nevarnost',\n    'bold' => 'Krepko',\n    'italic' => 'Ležeče',\n    'underline' => 'Podčrtano',\n    'strikethrough' => 'Prečrtano',\n    'superscript' => 'Nadpisano',\n    'subscript' => 'Podpisano',\n    'text_color' => 'Barva besedila',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Barva po meri',\n    'remove_color' => 'Odstrani barvo',\n    'background_color' => 'Barva ozadja',\n    'align_left' => 'Poravnaj levo',\n    'align_center' => 'Poravnaj na sredino',\n    'align_right' => 'Poravnaj desno',\n    'align_justify' => 'Poravnaj obojestransko',\n    'list_bullet' => 'Seznam z oznakami',\n    'list_numbered' => 'Oštevilčen seznam',\n    'list_task' => 'Seznam opravil',\n    'indent_increase' => 'Povečaj zamik',\n    'indent_decrease' => 'Zmanjšaj zamik',\n    'table' => 'Tabela',\n    'insert_image' => 'Vstavi sliko',\n    'insert_image_title' => 'Vstavi/Obdelaj sliko',\n    'insert_link' => 'Vstavi/Obdelaj povezavo',\n    'insert_link_title' => 'Vstavi/obdelaj povezavo',\n    'insert_horizontal_line' => 'Vstavi vodoravno črto',\n    'insert_code_block' => 'Insert code block',\n    'edit_code_block' => 'Edit code block',\n    'insert_drawing' => 'Vstavi/Obdelaj risbo',\n    'drawing_manager' => 'Drawing manager',\n    'insert_media' => 'Insert/edit media',\n    'insert_media_title' => 'Insert/Edit Media',\n    'clear_formatting' => 'Odstrani oblikovanje',\n    'source_code' => 'Izvorna koda',\n    'source_code_title' => 'Izvorna koda',\n    'fullscreen' => 'Celozaslonski način',\n    'image_options' => 'Možnosti za slike',\n\n    // Tables\n    'table_properties' => 'Lastnosti tabele',\n    'table_properties_title' => 'Lastnosti tabele',\n    'delete_table' => 'Izbriši tabelo',\n    'table_clear_formatting' => 'Odstrani oblikovanje tabele',\n    'resize_to_contents' => 'Prilagodi vsebini',\n    'row_header' => 'Glava vrstice',\n    'insert_row_before' => 'Vstavi vrstico pred',\n    'insert_row_after' => 'Vstavi vrstico po',\n    'delete_row' => 'Izbriši vrstico',\n    'insert_column_before' => 'Vstavi stolpec pred',\n    'insert_column_after' => 'Vstavi stolpec po',\n    'delete_column' => 'Izbriši stolpec',\n    'table_cell' => 'Celica',\n    'table_row' => 'Vrstica',\n    'table_column' => 'Stolpec',\n    'cell_properties' => 'Lastnosti celice',\n    'cell_properties_title' => 'Lastnosti celice',\n    'cell_type' => 'Vrsta celice',\n    'cell_type_cell' => 'Celica',\n    'cell_scope' => 'Obseg',\n    'cell_type_header' => 'Naslovna celica',\n    'merge_cells' => 'Združi celice',\n    'split_cell' => 'Razdeli celice',\n    'table_row_group' => 'Skupina vrstic',\n    'table_column_group' => 'Skupina stolpcev',\n    'horizontal_align' => 'Vodoravna poravnava',\n    'vertical_align' => 'Navpična poravnava',\n    'border_width' => 'Debelina roba',\n    'border_style' => 'Border style',\n    'border_color' => 'Border color',\n    'row_properties' => 'Row properties',\n    'row_properties_title' => 'Row Properties',\n    'cut_row' => 'Cut row',\n    'copy_row' => 'Copy row',\n    'paste_row_before' => 'Paste row before',\n    'paste_row_after' => 'Paste row after',\n    'row_type' => 'Row type',\n    'row_type_header' => 'Header',\n    'row_type_body' => 'Body',\n    'row_type_footer' => 'Footer',\n    'alignment' => 'Alignment',\n    'cut_column' => 'Cut column',\n    'copy_column' => 'Copy column',\n    'paste_column_before' => 'Paste column before',\n    'paste_column_after' => 'Paste column after',\n    'cell_padding' => 'Cell padding',\n    'cell_spacing' => 'Cell spacing',\n    'caption' => 'Caption',\n    'show_caption' => 'Show caption',\n    'constrain' => 'Constrain proportions',\n    'cell_border_solid' => 'Solid',\n    'cell_border_dotted' => 'Dotted',\n    'cell_border_dashed' => 'Dashed',\n    'cell_border_double' => 'Double',\n    'cell_border_groove' => 'Groove',\n    'cell_border_ridge' => 'Ridge',\n    'cell_border_inset' => 'Inset',\n    'cell_border_outset' => 'Outset',\n    'cell_border_none' => 'None',\n    'cell_border_hidden' => 'Hidden',\n\n    // Images, links, details/summary & embed\n    'source' => 'Source',\n    'alt_desc' => 'Alternative description',\n    'embed' => 'Embed',\n    'paste_embed' => 'Paste your embed code below:',\n    'url' => 'URL',\n    'text_to_display' => 'Text to display',\n    'title' => 'Title',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Open link',\n    'open_link_in' => 'Open link in...',\n    'open_link_current' => 'Current window',\n    'open_link_new' => 'New window',\n    'remove_link' => 'Remove link',\n    'insert_collapsible' => 'Insert collapsible block',\n    'collapsible_unwrap' => 'Odpri',\n    'edit_label' => 'Uredi oznako',\n    'toggle_open_closed' => 'Odpri/Zapri',\n    'collapsible_edit' => 'Uredi zložljivi blok',\n    'toggle_label' => 'Preklopi oznako',\n\n    // About view\n    'about' => 'O urejevalniku',\n    'about_title' => 'O Urejevalniku WYSIWYG',\n    'editor_license' => 'Licenca in avtorske pravice Urejevalnika',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'Urejevalnik je ustvarjen z uporabo :tinyLink pod pogoji licence MIT.',\n    'editor_tiny_license_link' => 'Podrobnosti o avtorskih pravicah in licenci za TinyMCE lahko preberete tukaj.',\n    'save_continue' => 'Shrani stran in Nadaljuj',\n    'callouts_cycle' => '(Večkrat pritisnite, da preklopite med vrstami opomb)',\n    'link_selector' => 'Povezava do vsebine',\n    'shortcuts' => 'Bližnjice',\n    'shortcut' => 'Bližnjica',\n    'shortcuts_intro' => 'Sledeče bližnjice so na voljo v urejevalniku:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Opis',\n];\n"
  },
  {
    "path": "lang/sl/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Nazadnje objavljeno',\n    'recently_created_pages' => 'Nazadnje objavljene strani',\n    'recently_updated_pages' => 'Nazadnje posodobljene strani',\n    'recently_created_chapters' => 'Nazadnje objavljena poglavja',\n    'recently_created_books' => 'Nazadnje objavljene knjige',\n    'recently_created_shelves' => 'Nazadnje ustvarjene police',\n    'recently_update' => 'Nazadnje posodobljeno',\n    'recently_viewed' => 'Nazadnje prikazano',\n    'recent_activity' => 'Nedavna dejavnost',\n    'create_now' => 'Ustvarite eno sedaj',\n    'revisions' => 'Revizije',\n    'meta_revision' => 'Številka revizije #:revisionCount',\n    'meta_created' => 'Ustvarjeno :timeLength',\n    'meta_created_name' => 'Ustvaril :timeLength uporabnik :user',\n    'meta_updated' => 'Posodobljeno :timeLength',\n    'meta_updated_name' => 'Posodobil :timeLength uporabnik :user',\n    'meta_owned_name' => 'V lasti :user',\n    'meta_reference_count' => 'Referenced by :count item|Referenced by :count items',\n    'entity_select' => 'Izbira entitete',\n    'entity_select_lack_permission' => 'You don\\'t have the required permissions to select this item',\n    'images' => 'Slike',\n    'my_recent_drafts' => 'Moji nedavni osnutki',\n    'my_recently_viewed' => 'Nedavno prikazano',\n    'my_most_viewed_favourites' => 'Največkrat gledane priljubljene strani',\n    'my_favourites' => 'Priljubljene',\n    'no_pages_viewed' => 'Niste si ogledali še nobene strani',\n    'no_pages_recently_created' => 'Nedavno ni bila ustvarjena nobena stran',\n    'no_pages_recently_updated' => 'Nedavno ni bila posodobljena nobena stran',\n    'export' => 'Izvozi',\n    'export_html' => 'Vsebuje spletno datoteko',\n    'export_pdf' => 'PDF datoteka (.pdf)',\n    'export_text' => 'Navadna besedilna datoteka',\n    'export_md' => 'Markdown File',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Default Page Template',\n    'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',\n    'default_template_select' => 'Select a template page',\n    'import' => 'Import',\n    'import_validate' => 'Validate Import',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'Select ZIP file to upload',\n    'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',\n    'import_pending' => 'Pending Imports',\n    'import_pending_none' => 'No imports have been started.',\n    'import_continue' => 'Continue Import',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Run Import',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Uploaded by',\n    'import_location' => 'Import Location',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'Import Errors',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Dovoljenja',\n    'permissions_desc' => 'Nastavi dovoljenja bolj podrobno, kot to določajo uporabniške vloge.',\n    'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.',\n    'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.',\n    'permissions_save' => 'Shrani dovoljenja',\n    'permissions_owner' => 'Lastnik',\n    'permissions_role_everyone_else' => 'Everyone Else',\n    'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',\n    'permissions_role_override' => 'Override permissions for role',\n    'permissions_inherit_defaults' => 'Inherit defaults',\n\n    // Search\n    'search_results' => 'Rezultati iskanja',\n    'search_total_results_found' => ':count najdenih rezultatov|:count skupno najdenih rezultatov',\n    'search_clear' => 'Počisti iskanje',\n    'search_no_pages' => 'Nobena stran se ne ujema z vašim iskanjem',\n    'search_for_term' => 'Išči :term',\n    'search_more' => 'Prikaži več rezultatov',\n    'search_advanced' => 'Napredno iskanje',\n    'search_terms' => 'Iskalni izrazi',\n    'search_content_type' => 'Vrsta vsebine',\n    'search_exact_matches' => 'Natančno ujemanje',\n    'search_tags' => 'Iskanje po oznakah',\n    'search_options' => 'Možnosti',\n    'search_viewed_by_me' => 'Ogledano',\n    'search_not_viewed_by_me' => 'Neogledano',\n    'search_permissions_set' => 'Nastavljena dovoljenja',\n    'search_created_by_me' => 'Ustvaril sem jaz',\n    'search_updated_by_me' => 'Posodobil sem jaz',\n    'search_owned_by_me' => 'Owned by me',\n    'search_date_options' => 'Možnosti datuma',\n    'search_updated_before' => 'Posodobljeno pred',\n    'search_updated_after' => 'Posodobljeno po',\n    'search_created_before' => 'Ustvarjeno pred',\n    'search_created_after' => 'Ustvarjeno po',\n    'search_set_date' => 'Nastavi datum',\n    'search_update' => 'Išči ponovno',\n\n    // Shelves\n    'shelf' => 'Polica',\n    'shelves' => 'Police',\n    'x_shelves' => ':count Polica|:count Police',\n    'shelves_empty' => 'Ustvarjena ni bila nobena polica',\n    'shelves_create' => 'Ustvari novo polico',\n    'shelves_popular' => 'Priljubljene police',\n    'shelves_new' => 'Nove police',\n    'shelves_new_action' => 'Nova polica',\n    'shelves_popular_empty' => 'Najbolj priljubljene police se bodo pojavile tukaj.',\n    'shelves_new_empty' => 'Nazadnje ustvarjene police se bodo pojavile tukaj.',\n    'shelves_save' => 'Shrani polico',\n    'shelves_books' => 'Knjige na tej polici',\n    'shelves_add_books' => 'Dodaj knjige na to polico',\n    'shelves_drag_books' => 'Drag books below to add them to this shelf',\n    'shelves_empty_contents' => 'Na tej polici ni nobene knjige',\n    'shelves_edit_and_assign' => 'Uredi knjižno polico za dodajanje knjig',\n    'shelves_edit_named' => 'Edit Shelf :name',\n    'shelves_edit' => 'Edit Shelf',\n    'shelves_delete' => 'Delete Shelf',\n    'shelves_delete_named' => 'Delete Shelf :name',\n    'shelves_delete_explain' => \"This will delete the shelf with the name ':name'. Contained books will not be deleted.\",\n    'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',\n    'shelves_permissions' => 'Shelf Permissions',\n    'shelves_permissions_updated' => 'Shelf Permissions Updated',\n    'shelves_permissions_active' => 'Shelf Permissions Active',\n    'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',\n    'shelves_permissions_create' => 'Shelf create permissions are only used for copying permissions to child books using the action below. They do not control the ability to create books.',\n    'shelves_copy_permissions_to_books' => 'Kopiraj dovoljenja na knjige',\n    'shelves_copy_permissions' => 'Dovoljenja kopiranja',\n    'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',\n    'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',\n\n    // Books\n    'book' => 'Knjiga',\n    'books' => 'Knjige',\n    'x_books' => ':count Knjiga|:count Knjig',\n    'books_empty' => 'Ustvarjena ni bila nobena knjiga',\n    'books_popular' => 'Priljubljene knjige',\n    'books_recent' => 'Zadnje knjige',\n    'books_new' => 'Nove knjige',\n    'books_new_action' => 'Nova knjiga',\n    'books_popular_empty' => 'Tukaj bodo prikazane najbolj priljubljene knjige.',\n    'books_new_empty' => 'Tukaj bodo prikazane nazadnje ustvarjene knjige.',\n    'books_create' => 'Ustvari novo knjigo',\n    'books_delete' => 'Izbriši knjigo',\n    'books_delete_named' => 'Izbriši knjigo :bookName',\n    'books_delete_explain' => 'S tem boste izbrisali knjigo z nazivom \\':bookName\\'. Vse strani in poglavja bodo odstranjena.',\n    'books_delete_confirmation' => 'Ali ste prepričani, da želite izbrisati to knjigo?',\n    'books_edit' => 'Uredi knjigo',\n    'books_edit_named' => 'Uredi knjigo :bookName',\n    'books_form_book_name' => 'Ime knjige',\n    'books_save' => 'Shrani knjigo',\n    'books_permissions' => 'Dovoljenja knjige',\n    'books_permissions_updated' => 'Posodobljena dovoljenja knjige',\n    'books_empty_contents' => 'V tej knjigi ni bila ustvarjena še nobena stran ali poglavje.',\n    'books_empty_create_page' => 'Ustvari novo stran',\n    'books_empty_sort_current_book' => 'Razvrsti trenutno knjigo',\n    'books_empty_add_chapter' => 'Dodaj poglavje',\n    'books_permissions_active' => 'Aktivna dovoljenja knjige',\n    'books_search_this' => 'Išči v tej knjigi',\n    'books_navigation' => 'Navigacija po knjigi',\n    'books_sort' => 'Razvrsti vsebino knjige',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Razvrsti knjigo :bookName',\n    'books_sort_name' => 'Razvrsti po imenu',\n    'books_sort_created' => 'Razvrsti po datumu nastanka',\n    'books_sort_updated' => 'Razvrsti po datumu posodobitve',\n    'books_sort_chapters_first' => 'Najprej poglavja',\n    'books_sort_chapters_last' => 'Nazadnje poglavja',\n    'books_sort_show_other' => 'Prikaži druge knjige',\n    'books_sort_save' => 'Shrani novo razvrstitev',\n    'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.',\n    'books_sort_move_up' => 'Move Up',\n    'books_sort_move_down' => 'Move Down',\n    'books_sort_move_prev_book' => 'Move to Previous Book',\n    'books_sort_move_next_book' => 'Move to Next Book',\n    'books_sort_move_prev_chapter' => 'Move Into Previous Chapter',\n    'books_sort_move_next_chapter' => 'Move Into Next Chapter',\n    'books_sort_move_book_start' => 'Move to Start of Book',\n    'books_sort_move_book_end' => 'Move to End of Book',\n    'books_sort_move_before_chapter' => 'Move to Before Chapter',\n    'books_sort_move_after_chapter' => 'Move to After Chapter',\n    'books_copy' => 'Copy Book',\n    'books_copy_success' => 'Book successfully copied',\n\n    // Chapters\n    'chapter' => 'Poglavje',\n    'chapters' => 'Poglavja',\n    'x_chapters' => ':count Poglavje|:count Poglavja',\n    'chapters_popular' => 'Priljubljena poglavja',\n    'chapters_new' => 'Novo poglavje',\n    'chapters_create' => 'Ustvari novo poglavje',\n    'chapters_delete' => 'Izbriši poglavje',\n    'chapters_delete_named' => 'Izbriši poglavje :chapterName',\n    'chapters_delete_explain' => 'Poglavje z imenom \":chapterName\" bo izbrisano. Vse strani znotraj poglavja bodo prav tako izbrisane.',\n    'chapters_delete_confirm' => 'Ste prepričani, da želite izbrisati to poglavje?',\n    'chapters_edit' => 'Uredi poglavje',\n    'chapters_edit_named' => 'Uredi poglavje :chapterName',\n    'chapters_save' => 'Shrani poglavje',\n    'chapters_move' => 'Premakni poglavje',\n    'chapters_move_named' => 'Premakni poglavje :chapterName',\n    'chapters_copy' => 'Copy Chapter',\n    'chapters_copy_success' => 'Chapter successfully copied',\n    'chapters_permissions' => 'Dovoljenja poglavij',\n    'chapters_empty' => 'V tem poglavju trenutno ni strani.',\n    'chapters_permissions_active' => 'Dovoljenja poglavij so aktivirana',\n    'chapters_permissions_success' => 'Posodobljena dovoljenja poglavij',\n    'chapters_search_this' => 'Išči v tem poglavju',\n    'chapter_sort_book' => 'Sort Book',\n\n    // Pages\n    'page' => 'Stran',\n    'pages' => 'Strani',\n    'x_pages' => ':count Stran|:count Strani',\n    'pages_popular' => 'Priljubljene knjige',\n    'pages_new' => 'Nova stran',\n    'pages_attachments' => 'Priponke',\n    'pages_navigation' => 'Navigacija po strani',\n    'pages_delete' => 'Izbriši stran',\n    'pages_delete_named' => 'Izbriši stran :pageName',\n    'pages_delete_draft_named' => 'Izbriši osnutek strani :pageName',\n    'pages_delete_draft' => 'Izbriši osnutek strani',\n    'pages_delete_success' => 'Stran izbirsana',\n    'pages_delete_draft_success' => 'Osnutek strani izbrisan',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'Ste prepričani, da želite izbrisati to stran?',\n    'pages_delete_draft_confirm' => 'Ali ste prepričani, da želite izbrisati ta osnutek?',\n    'pages_editing_named' => 'Urejanje strani :pageName',\n    'pages_edit_draft_options' => 'Možnosti osnutka',\n    'pages_edit_save_draft' => 'Shrani osnutek',\n    'pages_edit_draft' => 'Uredi osnutek strani',\n    'pages_editing_draft' => 'Urejanje osnutka',\n    'pages_editing_page' => 'Urejanje strani',\n    'pages_edit_draft_save_at' => 'Osnutek shranjen ob ',\n    'pages_edit_delete_draft' => 'Izbriši osnutek',\n    'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.',\n    'pages_edit_discard_draft' => 'Zavrzi osnutek',\n    'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',\n    'pages_edit_switch_to_markdown_clean' => '(Clean Content)',\n    'pages_edit_switch_to_markdown_stable' => '(Stable Content)',\n    'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Opiši spremembe na dokumentu',\n    'pages_edit_enter_changelog_desc' => 'Vnesite kratek opis sprememb, ki ste jih naredili',\n    'pages_edit_enter_changelog' => 'Vpišite vsebino sprememb',\n    'pages_editor_switch_title' => 'Switch Editor',\n    'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',\n    'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',\n    'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',\n    'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',\n    'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\\'t persist across this change.',\n    'pages_save' => 'Shrani stran',\n    'pages_title' => 'Naslov strani',\n    'pages_name' => 'Ime strani',\n    'pages_md_editor' => 'Urejevalnik',\n    'pages_md_preview' => 'Predogled',\n    'pages_md_insert_image' => 'Vstavi sliko',\n    'pages_md_insert_link' => 'Vnesi povezavo do objekta',\n    'pages_md_insert_drawing' => 'Vstavi risbo',\n    'pages_md_show_preview' => 'Show preview',\n    'pages_md_sync_scroll' => 'Sync preview scroll',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Unsaved Drawing Found',\n    'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',\n    'pages_not_in_chapter' => 'Stran ni v poglavju',\n    'pages_move' => 'Premakni stran',\n    'pages_copy' => 'Kopiraj stran',\n    'pages_copy_desination' => 'Destinacija kopije',\n    'pages_copy_success' => 'Stran uspešno kopirana',\n    'pages_permissions' => 'Dovoljenja strani',\n    'pages_permissions_success' => 'Posodobljena dovoljenja strani',\n    'pages_revision' => 'Revizija',\n    'pages_revisions' => 'Pregled strani',\n    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',\n    'pages_revisions_named' => 'Pregledi strani za :pageName',\n    'pages_revision_named' => 'Pregled strani za :pageName',\n    'pages_revision_restored_from' => 'Obnovljeno iz #:id; :summary',\n    'pages_revisions_created_by' => 'Ustvaril',\n    'pages_revisions_date' => 'Datum revizije',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Revision Number',\n    'pages_revisions_numbered' => 'Revizija št. :id',\n    'pages_revisions_numbered_changes' => 'Revizija št. #:id Changes',\n    'pages_revisions_editor' => 'Editor Type',\n    'pages_revisions_changelog' => 'Dnevnik sprememb',\n    'pages_revisions_changes' => 'Spremembe',\n    'pages_revisions_current' => 'Trenutna različica',\n    'pages_revisions_preview' => 'Predogled',\n    'pages_revisions_restore' => 'Obnovi',\n    'pages_revisions_none' => 'Ta stran nima popravkov',\n    'pages_copy_link' => 'Kopiraj povezavo',\n    'pages_edit_content_link' => 'Jump to section in editor',\n    'pages_pointer_enter_mode' => 'Enter section select mode',\n    'pages_pointer_label' => 'Page Section Options',\n    'pages_pointer_permalink' => 'Page Section Permalink',\n    'pages_pointer_include_tag' => 'Page Section Include Tag',\n    'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag',\n    'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink',\n    'pages_permissions_active' => 'Aktivna dovoljenja strani',\n    'pages_initial_revision' => 'Prvotno objavljeno',\n    'pages_references_update_revision' => 'System auto-update of internal links',\n    'pages_initial_name' => 'Nova stran',\n    'pages_editing_draft_notification' => 'Trenutno urejate osnutek, ki je bil nazadnje shranjen :timeDiff.',\n    'pages_draft_edited_notification' => 'Ta stran je odtlej posodobljena. Priporočamo, da zavržete ta osnutek.',\n    'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count uporabnikov je začelo urejati to stran',\n        'start_b' => ':userName je začel urejati to stran',\n        'time_a' => 'odkar je bila stran nazandnje posodobljena',\n        'time_b' => 'v zadnjih :minCount minutah',\n        'message' => ':start :time. Pazite, da ne boste prepisali posodobitev drug drugega!',\n    ],\n    'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',\n    'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',\n    'pages_specific' => 'Določena stran',\n    'pages_is_template' => 'Predloga strani',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Toggle Sidebar',\n    'page_tags' => 'Oznake strani',\n    'chapter_tags' => 'Oznake poglavja',\n    'book_tags' => 'Oznake knjige',\n    'shelf_tags' => 'Oznake police',\n    'tag' => 'Oznaka',\n    'tags' =>  'Oznake',\n    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',\n    'tag_name' =>  'Ime oznake',\n    'tag_value' => 'Vrednost oznake (opcijsko)',\n    'tags_explain' => \"Dodajte nekaj oznak za boljšo kategorizacijo vaše vsebine.\\nZ dodelitvijo oznake lahko poskrbite za bolj poglobljeno organizacijo.\",\n    'tags_add' => 'Dodaj drugo oznako',\n    'tags_remove' => 'Odstrani to oznako',\n    'tags_usages' => 'Total tag usages',\n    'tags_assigned_pages' => 'Assigned to Pages',\n    'tags_assigned_chapters' => 'Assigned to Chapters',\n    'tags_assigned_books' => 'Assigned to Books',\n    'tags_assigned_shelves' => 'Assigned to Shelves',\n    'tags_x_unique_values' => ':count unique values',\n    'tags_all_values' => 'All values',\n    'tags_view_tags' => 'View Tags',\n    'tags_view_existing_tags' => 'View existing tags',\n    'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',\n    'attachments' => 'Priponke',\n    'attachments_explain' => 'Naložite nekaj datotek ali pripnite nekaj povezav, da jih prikažete na vaši strani. Vidne so v stranski orodni vrstici.',\n    'attachments_explain_instant_save' => 'Spremembe tukaj so takoj shranjene.',\n    'attachments_upload' => 'Naloži datoteko',\n    'attachments_link' => 'Pripni povezavo',\n    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',\n    'attachments_set_link' => 'Nastavi povezavo',\n    'attachments_delete' => 'Ali ste prepričani, da želite izbrisati to priponko?',\n    'attachments_dropzone' => 'Drop files here to upload',\n    'attachments_no_files' => 'Nobena datoteka ni bila naložena',\n    'attachments_explain_link' => 'Lahko pripnete povezavo, če ne želite naložiti datoteke. Lahko je povezava na drugo stran ali povezava do dateteke v oblaku.',\n    'attachments_link_name' => 'Ime povezave',\n    'attachment_link' => 'Povezava priponke',\n    'attachments_link_url' => 'Povezava do datoteke',\n    'attachments_link_url_hint' => 'Url spletnega mesta ali datoteke',\n    'attach' => 'Pripni',\n    'attachments_insert_link' => 'Dodaj povezavo na priponko na stran',\n    'attachments_edit_file' => 'Uredi datoteko',\n    'attachments_edit_file_name' => 'Ime datoteke',\n    'attachments_edit_drop_upload' => 'Spustite datoteke ali kliknite tukaj, če želite naložiti in prepisati',\n    'attachments_order_updated' => 'Razvrščanje priponk posodobljeno',\n    'attachments_updated_success' => 'Podrobnosti priloge posodobljene',\n    'attachments_deleted' => 'Priponka izbirsana',\n    'attachments_file_uploaded' => 'Datoteka uspešno naložena',\n    'attachments_file_updated' => 'Datoteka uspešno posodobljena',\n    'attachments_link_attached' => 'Povezava uspešno dodana na stran',\n    'templates' => 'Predloge',\n    'templates_set_as_template' => 'Stran je predloga',\n    'templates_explain_set_as_template' => 'To stran lahko nastavite kot predlogo in njeno vsebino uporabite pri izdelavi drugih strani. Ostali uporabniki bodo lahko uporabljali to predlogo, če imajo dovoljenja za to stran.',\n    'templates_replace_content' => 'Zamenjaj vsebino strani',\n    'templates_append_content' => 'Dodajte k vsebini strani',\n    'templates_prepend_content' => 'Dodaj predpono k vsebini strani',\n\n    // Profile View\n    'profile_user_for_x' => 'Uporabnik že :time',\n    'profile_created_content' => 'Ustvarjena vsebina',\n    'profile_not_created_pages' => ':userName ni izdelal nobene strani',\n    'profile_not_created_chapters' => ':userName ni izdelal nobenega poglavja',\n    'profile_not_created_books' => ':userName ni objavil nobene knjige',\n    'profile_not_created_shelves' => ':userName ni izdelal nobene knjižne police',\n\n    // Comments\n    'comment' => 'Komentar',\n    'comments' => 'Komentarji',\n    'comment_add' => 'Dodaj komentar',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Dodaj komentar',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Shrani komentar',\n    'comment_new' => 'Nov kometar',\n    'comment_created' => 'komentirano :createDiff',\n    'comment_updated' => 'Posodobljeno :updateDiff od :username',\n    'comment_updated_indicator' => 'Updated',\n    'comment_deleted_success' => 'Komentar je izbrisan',\n    'comment_created_success' => 'Komentar dodan',\n    'comment_updated_success' => 'Komentar posodobljen',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Ste prepričani, da želite izbrisati ta komentar?',\n    'comment_in_reply_to' => 'Odgovor na :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',\n\n    // Revision\n    'revision_delete_confirm' => 'Ali ste prepričani, da želite izbrisati to revizijo?',\n    'revision_restore_confirm' => 'Ali ste prepričani da želite obnoviti to revizijo? Vsebina trenutne strani bo zamenjana.',\n    'revision_cannot_delete_latest' => 'Ne morem izbrisati zadnje revizije.',\n\n    // Copy view\n    'copy_consider' => 'Please consider the below when copying content.',\n    'copy_consider_permissions' => 'Custom permission settings will not be copied.',\n    'copy_consider_owner' => 'You will become the owner of all copied content.',\n    'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',\n    'copy_consider_attachments' => 'Page attachments will not be copied.',\n    'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',\n\n    // Conversions\n    'convert_to_shelf' => 'Convert to Shelf',\n    'convert_to_shelf_contents_desc' => 'You can convert this book to a new shelf with the same contents. Chapters contained within this book will be converted to new books. If this book contains any pages, that are not in a chapter, this book will be renamed and contain such pages, and this book will become part of the new shelf.',\n    'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.',\n    'convert_book' => 'Convert Book',\n    'convert_book_confirm' => 'Are you sure you want to convert this book?',\n    'convert_undo_warning' => 'This cannot be as easily undone.',\n    'convert_to_book' => 'Convert to Book',\n    'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',\n    'convert_chapter' => 'Convert Chapter',\n    'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',\n\n    // References\n    'references' => 'References',\n    'references_none' => 'There are no tracked references to this item.',\n    'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',\n\n    // Watch Options\n    'watch' => 'Watch',\n    'watch_title_default' => 'Default Preferences',\n    'watch_desc_default' => 'Revert watching to just your default notification preferences.',\n    'watch_title_ignore' => 'Ignore',\n    'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.',\n    'watch_title_new' => 'Nove strani',\n    'watch_desc_new' => 'Notify when any new page is created within this item.',\n    'watch_title_updates' => 'All Page Updates',\n    'watch_desc_updates' => 'Notify upon all new pages and page changes.',\n    'watch_desc_updates_page' => 'Notify upon all page changes.',\n    'watch_title_comments' => 'All Page Updates & Comments',\n    'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.',\n    'watch_desc_comments_page' => 'Notify upon page changes and new comments.',\n    'watch_change_default' => 'Change default notification preferences',\n    'watch_detail_ignore' => 'Ignoring notifications',\n    'watch_detail_new' => 'Watching for new pages',\n    'watch_detail_updates' => 'Watching new pages and updates',\n    'watch_detail_comments' => 'Watching new pages, updates & comments',\n    'watch_detail_parent_book' => 'Watching via parent book',\n    'watch_detail_parent_book_ignore' => 'Ignoring via parent book',\n    'watch_detail_parent_chapter' => 'Watching via parent chapter',\n    'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter',\n];\n"
  },
  {
    "path": "lang/sl/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Nimate pravic za dostop do želene strani.',\n    'permissionJson' => 'Nimate dovoljenja za izvedbo zahtevanega dejanja.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Uporabnik z e-pošto :email že obstaja, vendar z drugačnimi poverilnicami.',\n    'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',\n    'email_already_confirmed' => 'E-naslov je že bil potrjen, poskusite se prijaviti.',\n    'email_confirmation_invalid' => 'Ta potrditveni žeton ni veljaven ali je že bil uporabljen. Poizkusite znova.',\n    'email_confirmation_expired' => 'Potrditveni žeton je potekel. Nova potrditvena e-pošta je bila poslana.',\n    'email_confirmation_awaiting' => 'Potrebno je potrditi e-naslov',\n    'ldap_fail_anonymous' => 'Dostop do LDAP ni uspel z anonimno povezavo',\n    'ldap_fail_authed' => 'Neuspešen LDAP dostop z danimi podrobnostimi dn & gesla',\n    'ldap_extension_not_installed' => 'PHP razširitev za LDAP ni nameščena',\n    'ldap_cannot_connect' => 'Ne morem se povezati na LDAP strežnik, neuspešna začetna povezava',\n    'saml_already_logged_in' => 'Že prijavljen',\n    'saml_no_email_address' => 'Nisem našel e-naslova za tega uporabnika v podatkih iz zunanjega sistema za preverjanje pristnosti',\n    'saml_invalid_response_id' => 'Zahteva iz zunanjega sistema za preverjanje pristnosti ni prepoznana s strani procesa zagnanega s strani te aplikacije. Pomik nazaj po prijavi je lahko vzrok teh težav.',\n    'saml_fail_authed' => 'Prijava z uporabo :system ni uspela, sistem ni zagotovil uspešne avtorizacije',\n    'oidc_already_logged_in' => 'Already logged in',\n    'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',\n    'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',\n    'social_no_action_defined' => 'Akcija ni določena',\n    'social_login_bad_response' => \"Napaka pri :socialAccount prijavi:\\n:error\",\n    'social_account_in_use' => 'Ta :socialAccount je že v uporabi. Poskusite se prijaviti z :socialAccount možnostjo.',\n    'social_account_email_in_use' => 'Ta e-naslov :email je že v uporabi. Če že imate račun lahko povežete vaš :socialAccount v vaših nastavitvah profila.',\n    'social_account_existing' => 'Ta :socialAccount je že dodan vašemu profilu.',\n    'social_account_already_used_existing' => 'Ta :socialAccount je v uporabi s strani drugega uporabnika.',\n    'social_account_not_used' => 'Ta :socialAccount ni povezan z nobenim uporabnikom. Prosimo povežite ga v vaših nastavitvah profila. ',\n    'social_account_register_instructions' => 'Če še nimate računa, se lahko registrirate z uporabo :socialAccount.',\n    'social_driver_not_found' => 'Socialni vtičnik ni najden',\n    'social_driver_not_configured' => 'Vaše nastavitve :socialAccount niso pravilo nastavljene.',\n    'invite_token_expired' => 'Ta povezava je potekla. Namesto tega lahko ponastavite vaše geslo računa.',\n    'login_user_not_found' => 'A user for this action could not be found.',\n\n    // System\n    'path_not_writable' => 'Poti :filePath ni bilo mogoče naložiti. Prepričajte se, da je zapisljiva na strežnik.',\n    'cannot_get_image_from_url' => 'Ne morem pridobiti slike z :url',\n    'cannot_create_thumbs' => 'Strežnik ne more izdelati sličice. Prosimo preverite če imate GD PHP razširitev nameščeno.',\n    'server_upload_limit' => 'Strežnik ne dovoli nalaganj take velikosti. Prosimo poskusite z manjšo velikostjo datoteke.',\n    'server_post_limit' => 'The server cannot receive the provided amount of data. Try again with less data or a smaller file.',\n    'uploaded'  => 'Strežnik ne dovoli nalaganj take velikosti. Prosimo poskusite zmanjšati velikost datoteke.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Prišlo je do napake med nalaganjem slike',\n    'image_upload_type_error' => 'Napačen tip (format) slike',\n    'image_upload_replace_type' => 'Image file replacements must be of the same type',\n    'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.',\n    'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.',\n    'image_gallery_thumbnail_memory_limit' => 'Failed to create gallery thumbnails due to system resource limits.',\n    'drawing_data_not_found' => 'Drawing data could not be loaded. The drawing file might no longer exist or you may not have permission to access it.',\n\n    // Attachments\n    'attachment_not_found' => 'Priloga ni najdena',\n    'attachment_upload_error' => 'An error occurred uploading the attachment file',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Osnutka ni bilo mogoče shraniti. Pred shranjevanjem te strani se prepričajte, da imate internetno povezavo',\n    'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content',\n    'page_custom_home_deletion' => 'Ne morem izbrisati strani, ki je nastavljena kot domača stran',\n\n    // Entities\n    'entity_not_found' => 'Ne najdem tega objekta',\n    'bookshelf_not_found' => 'Shelf not found',\n    'book_not_found' => 'Knjiga ni najdena',\n    'page_not_found' => 'Stran ni najdena',\n    'chapter_not_found' => 'Poglavje ni najdeno',\n    'selected_book_not_found' => 'Izbrana knjiga ni najdena',\n    'selected_book_chapter_not_found' => 'Izbrana knjiga ali poglavje ni najdeno',\n    'guests_cannot_save_drafts' => 'Gosti ne morejo shranjevati osnutkov',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Ne morete odstraniti edinega administratorja',\n    'users_cannot_delete_guest' => 'Ne morete odstraniti uporabnika gost',\n    'users_could_not_send_invite' => 'Could not create user since invite email failed to send',\n\n    // Roles\n    'role_cannot_be_edited' => 'Te vloge mi možno urejati',\n    'role_system_cannot_be_deleted' => 'Ta vloga je sistemska in je ni možno brisati',\n    'role_registration_default_cannot_delete' => 'Te vloge ni možno brisati, dokler je nastavljena kot privzeta',\n    'role_cannot_remove_only_admin' => 'Ta uporabnik je edini administrator. Dodelite vlogo administratorja drugemu uporabniku, preden ga poskusite brisati.',\n\n    // Comments\n    'comment_list' => 'Napaka se je pojavila pri pridobivanju komentarjev.',\n    'cannot_add_comment_to_draft' => 'V osnutek ni možno dodajati komentarjev.',\n    'comment_add' => 'Napaka se je pojavila pri dodajanju / posodobitvi komentarjev.',\n    'comment_delete' => 'Napaka se je pojavila pri brisanju komentarja.',\n    'empty_comment' => 'Praznega komentarja ne morete objaviti.',\n\n    // Error pages\n    '404_page_not_found' => 'Strani ni mogoče najti',\n    'sorry_page_not_found' => 'Oprostite, strani ki jo iščete, ni mogoče najti.',\n    'sorry_page_not_found_permission_warning' => 'Stran ne obstaja ali pa za ogled nimaš ustreznih pravic.',\n    'image_not_found' => 'Image Not Found',\n    'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',\n    'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',\n    'return_home' => 'Vrni se domov',\n    'error_occurred' => 'Prišlo je do napake',\n    'app_down' => ':appName trenutno ni dosegljiva',\n    'back_soon' => 'Kmalu bo ponovno dosegljiva.',\n\n    // Import\n    'import_zip_cant_read' => 'Could not read ZIP file.',\n    'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',\n    'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Import ZIP failed to validate with errors:',\n    'import_zip_failed_notification' => 'Failed to import ZIP file.',\n    'import_perms_books' => 'You are lacking the required permissions to create books.',\n    'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',\n    'import_perms_pages' => 'You are lacking the required permissions to create pages.',\n    'import_perms_images' => 'You are lacking the required permissions to create images.',\n    'import_perms_attachments' => 'You are lacking the required permission to create attachments.',\n\n    // API errors\n    'api_no_authorization_found' => 'Avtorizacija ni bila najdena',\n    'api_bad_authorization_format' => 'Avtorizacija je bila najdena, vendar je v napačni obliki',\n    'api_user_token_not_found' => 'Za dano avtorizacijo ni bil najden noben ustrezen API',\n    'api_incorrect_token_secret' => 'Skrivnost, ki je bila dana za uporabljeni žeton API, je napačna',\n    'api_user_no_api_permission' => 'Lastnik API nima pravic za klicanje API',\n    'api_user_token_expired' => 'Avtorizacijski žeton je pretečen',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Napaka se je pojavila pri pošiljanju testne e-pošte:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'The URL does not match the configured allowed SSR hosts',\n];\n"
  },
  {
    "path": "lang/sl/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'New comment on page: :pageName',\n    'new_comment_intro' => 'A user has commented on a page in :appName:',\n    'new_page_subject' => 'New page: :pageName',\n    'new_page_intro' => 'A new page has been created in :appName:',\n    'updated_page_subject' => 'Updated page: :pageName',\n    'updated_page_intro' => 'A page has been updated in :appName:',\n    'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\\'t be sent notifications for further edits to this page by the same editor.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Page Name:',\n    'detail_page_path' => 'Page Path:',\n    'detail_commenter' => 'Commenter:',\n    'detail_comment' => 'Comment:',\n    'detail_created_by' => 'Created By:',\n    'detail_updated_by' => 'Updated By:',\n\n    'action_view_comment' => 'View Comment',\n    'action_view_page' => 'View Page',\n\n    'footer_reason' => 'This notification was sent to you because :link cover this type of activity for this item.',\n    'footer_reason_link' => 'your notification preferences',\n];\n"
  },
  {
    "path": "lang/sl/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Nazaj',\n    'next'     => 'Naprej &raquo;',\n\n];\n"
  },
  {
    "path": "lang/sl/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Gesla morajo biti najmanj osem znakov dolga in se morajo ujemati s potrditvijo.',\n    'user' => \"Ne moremo najti uporabnika s tem e-poštnim naslovom.\",\n    'token' => 'Žeton za ponastavitev gesla ni veljaven za ta e-poštni naslov.',\n    'sent' => 'Poslali smo vam povezavo za ponastavitev gesla!',\n    'reset' => 'Vaše geslo je bilo ponastavljeno!',\n\n];\n"
  },
  {
    "path": "lang/sl/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'My Account',\n\n    'shortcuts' => 'Shortcuts',\n    'shortcuts_interface' => 'UI Shortcut Preferences',\n    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',\n    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',\n    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',\n    'shortcuts_section_navigation' => 'Navigation',\n    'shortcuts_section_actions' => 'Common Actions',\n    'shortcuts_save' => 'Save Shortcuts',\n    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing \"?\" which will highlight the available shortcuts for actions currently visible on the screen.',\n    'shortcuts_update_success' => 'Shortcut preferences have been updated!',\n    'shortcuts_overview_desc' => 'Manage keyboard shortcuts you can use to navigate the system user interface.',\n\n    'notifications' => 'Notification Preferences',\n    'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.',\n    'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own',\n    'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Notify upon replies to my comments',\n    'notifications_save' => 'Save Preferences',\n    'notifications_update_success' => 'Notification preferences have been updated!',\n    'notifications_watched' => 'Watched & Ignored Items',\n    'notifications_watched_desc' => 'Below are the items that have custom watch preferences applied. To update your preferences for these, view the item then find the watch options in the sidebar.',\n\n    'auth' => 'Access & Security',\n    'auth_change_password' => 'Change Password',\n    'auth_change_password_desc' => 'Change the password you use to log-in to the application. This must be at least 8 characters long.',\n    'auth_change_password_success' => 'Password has been updated!',\n\n    'profile' => 'Profile Details',\n    'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.',\n    'profile_view_public' => 'View Public Profile',\n    'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.',\n    'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.',\n    'profile_email_no_permission' => 'Unfortunately you don\\'t have permission to change your email address. If you want to change this, you\\'d need to ask an administrator to change this for you.',\n    'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.',\n    'profile_admin_options' => 'Administrator Options',\n    'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the \"Settings > Users\" area of the application.',\n\n    'delete_account' => 'Delete Account',\n    'delete_my_account' => 'Delete My Account',\n    'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\\'ve created, such as created pages and uploaded images, will remain.',\n    'delete_my_account_warning' => 'Are you sure you want to delete your account?',\n];\n"
  },
  {
    "path": "lang/sl/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Nastavitve',\n    'settings_save' => 'Shrani nastavitve',\n    'system_version' => 'System Version',\n    'categories' => 'Categories',\n\n    // App Settings\n    'app_customization' => 'Prilagoditev',\n    'app_features_security' => 'Lastnosti & Varnost',\n    'app_name' => 'Ime aplikacije',\n    'app_name_desc' => 'To ime je prikazano v glavi in vsaki sistemski e-pošti.',\n    'app_name_header' => 'Prikaži ime v glavi',\n    'app_public_access' => 'Javni dostop',\n    'app_public_access_desc' => 'Če omogočite to možnost, bo obiskovalcem, ki niso prijavljeni, omogočen dostop do vsebine v BookStack.',\n    'app_public_access_desc_guest' => 'Dostop za javne obiskovalce je mogoče nadzorovati prek uporabnika \"Gost\".',\n    'app_public_access_toggle' => 'Dovoli javni dostop',\n    'app_public_viewing' => 'Dovoli javni pregled?',\n    'app_secure_images' => 'Nalaganje slik z večjo varnostjo',\n    'app_secure_images_toggle' => 'Omogoči nalaganje slik z večjo varnostjo',\n    'app_secure_images_desc' => 'Zaradi delovanja so vse slike javne. Ta možnost doda naključni, hard-to-guess niz pred Url-ji slike. Prepričajte se, da indeksi imenikov niso omogočeni, da preprečite enostaven dostop.',\n    'app_default_editor' => 'Default Page Editor',\n    'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.',\n    'app_custom_html' => 'Po meri HTML vsebina glave',\n    'app_custom_html_desc' => 'Katerakoli vsebina dodana tukaj, bo vstavljena na dno <head> dela vsake strani. To je uporabno za uporabo prevladujočih slogov ali dodajanje analitike.',\n    'app_custom_html_disabled_notice' => 'Po meri narejena HTML glava vsebine je onemogočena na tej strani z nastavitvami, da se zagotovi, da bodo morebitne zrušitve lahko povrnjene.',\n    'app_logo' => 'Logotip aplikacije',\n    'app_logo_desc' => 'This is used in the application header bar, among other areas. This image should be 86px in height. Large images will be scaled down.',\n    'app_icon' => 'Application Icon',\n    'app_icon_desc' => 'This icon is used for browser tabs and shortcut icons. This should be a 256px square PNG image.',\n    'app_homepage' => 'Domača stran aplikacije',\n    'app_homepage_desc' => 'Izberi pogled, da se pokaže na domači strani, namesto osnovnega pogleda. Dovoljenja strani so prezrta za izbrane strani.',\n    'app_homepage_select' => 'Izberi stran',\n    'app_footer_links' => 'Povezave v nogi',\n    'app_footer_links_desc' => 'Dodaj URL povezave, ki bodo na voljo v nogi spletne strani. Povezave bodo vidne na dnu večine strani, vključno s tistimi, ki ne zahtevajo prijave. Na voljo imate oznako \"trans::<key>\" za uporabo sistemskih prevodov. Na primer: uporaba oznake \"trans::common.privacy_policy\" bo poskrbela za prevod besedila \"Privacy Policy\" in oznaka \"trans::common.terms_of_service\" bo poskrbela za prevod besedila \"Terms of Service\".',\n    'app_footer_links_label' => 'Oznaka povezave',\n    'app_footer_links_url' => 'Naslov URL povezave',\n    'app_footer_links_add' => 'Dodaj povezavo v nogo',\n    'app_disable_comments' => 'Onemogoči komentarje',\n    'app_disable_comments_toggle' => 'Onemogoči komentarje',\n    'app_disable_comments_desc' => 'Onemogoči komentarje na vseh straneh v aplikaciji. <br> Obstoječi komentarji se ne prikazujejo.',\n\n    // Color settings\n    'color_scheme' => 'Application Color Scheme',\n    'color_scheme_desc' => 'Set the colors to use in the application user interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.',\n    'ui_colors_desc' => 'Set the application primary color and default link color. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the application interface.',\n    'app_color' => 'Primary Color',\n    'link_color' => 'Default Link Color',\n    'content_colors_desc' => 'Set colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.',\n    'bookshelf_color' => 'Barva police',\n    'book_color' => 'knjiga barv',\n    'chapter_color' => 'barvno poglavje',\n    'page_color' => 'Stran barv',\n    'page_draft_color' => 'stran osnutka barv',\n\n    // Registration Settings\n    'reg_settings' => 'registracija',\n    'reg_enable' => 'onemogočena registracija',\n    'reg_enable_toggle' => 'omogočena registracija',\n    'reg_enable_desc' => 'Ko je registracija omogočena, se bo uporabnik lahko prijavil sam kot uporabnik aplikacije. Po registraciji je uporabniku dodeljena privzeta vloga.',\n    'reg_default_role' => 'prevzeta uporabniška vloga po registraciji',\n    'reg_enable_external_warning' => 'Ta možnosti je prezrta, ko je aktivno zunanja LDAP ali SAML preverjanje pristnosti. Uporabniški računi za neobstoječe uporabnike bodo avtomatično ustvarjeni po uspešnem zunanjem preverjanju pristnosti.',\n    'reg_email_confirmation' => 'potrditev e-pošte',\n    'reg_email_confirmation_toggle' => 'potrebna potrditev e-pošte',\n    'reg_confirm_email_desc' => 'Če uporabite omejitev domene, bo potrebna potrditev e-pošte in ta možnost bo prezrta.',\n    'reg_confirm_restrict_domain' => 'omejitev domene',\n    'reg_confirm_restrict_domain_desc' => 'Vnesite seznam domen, ločenih z vejico, na katere želite omejiti registracijo. Uporabnik bo prejel e-pošto za potrditev naslova, preden bo omogočena interakcija z aplikacijo. <br> Upoštevajte, da uporabnik po uspešni registrciji lahko spremeni svoj e-poštni naslov.',\n    'reg_confirm_restrict_domain_placeholder' => 'Brez omejitev',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Vzdrževanje',\n    'maint_image_cleanup' => 'Odstrani /počisti slike',\n    'maint_image_cleanup_desc' => 'Pregleda vsebino strani in revizij ter ugotovi, katere slike in risbe so v uporabi in katere so odvečne. Preden to poženeš, naredi popolno varnostno kopijo podatkovne zbirke in slik.',\n    'maint_delete_images_only_in_revisions' => 'Izbriši tudi slike, ki obstajajo le v starih različicah strani',\n    'maint_image_cleanup_run' => 'Zaženi čiščenje',\n    'maint_image_cleanup_warning' => 'Najdenih je bilo :count verjetno neuporabljenih slik. Ali si prepričan, da želiš odstraniti izbrane slike?',\n    'maint_image_cleanup_success' => ':count verjetno neuporavljenih slik je bilo najdenih in izbrisanih!',\n    'maint_image_cleanup_nothing_found' => 'Ni bilo najdenih neuporabljenih slik, nič ni izbrisano!',\n    'maint_send_test_email' => 'Pošlji testno e-pismo',\n    'maint_send_test_email_desc' => 'To pošlje testno e-pošto na vaš e-poštni naslov, naveden v vašem profilu.',\n    'maint_send_test_email_run' => 'Pošlji testno sporočilo',\n    'maint_send_test_email_success' => 'e-pošta poslana na :naslov',\n    'maint_send_test_email_mail_subject' => 'Testno e-sporočilo',\n    'maint_send_test_email_mail_greeting' => 'Zdi se, da dostava e-pošte deluje!',\n    'maint_send_test_email_mail_text' => 'Čestitke! Če ste prejeli e-poštno obvestilo so bile vaše e-poštne nastavitve pravilno konfigurirane.',\n    'maint_recycle_bin_desc' => 'Izbrisane police, knjige, poglavja in strani se pošljejo v koš, da jih je mogoče obnoviti ali trajno izbrisati. Starejše predmete v košu lahko čez nekaj časa samodejno odstranite, odvisno od konfiguracije sistema.',\n    'maint_recycle_bin_open' => 'Odpri koš',\n    'maint_regen_references' => 'Regenerate References',\n    'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',\n    'maint_regen_references_success' => 'Reference index has been regenerated!',\n    'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Koš',\n    'recycle_bin_desc' => 'Tu lahko obnovite predmete, ki so bili izbrisani, ali pa jih trajno odstranite s sistema. Ta seznam je nefiltriran, za razliko od podobnih seznamov dejavnosti v sistemu, kjer se uporabljajo filtri dovoljenj.',\n    'recycle_bin_deleted_item' => 'Izbrisan element',\n    'recycle_bin_deleted_parent' => 'Parent',\n    'recycle_bin_deleted_by' => 'Izbrisal uporabnik',\n    'recycle_bin_deleted_at' => 'Čas izbrisa',\n    'recycle_bin_permanently_delete' => 'Trajno izbrišem?',\n    'recycle_bin_restore' => 'Obnovi',\n    'recycle_bin_contents_empty' => 'Koš je prazen',\n    'recycle_bin_empty' => 'Izprazni koš',\n    'recycle_bin_empty_confirm' => 'S tem boste trajno uničili vse predmete v košu, vključno z vsebino vsakega predmeta. Ali ste prepričani, da želite izprazniti koš?',\n    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?',\n    'recycle_bin_destroy_list' => 'Predmeti, ki naj bodo trajno izbrisani',\n    'recycle_bin_restore_list' => 'Predmeti, ki naj bodo obnovljeni',\n    'recycle_bin_restore_confirm' => 'S tem dejanjem boste izbrisani element, vključno z vsemi podrejenimi elementi, obnovili na prvotno mesto. Če je bilo prvotno mesto od takrat izbrisano in je zdaj v košu, bo treba obnoviti tudi nadrejeni element.',\n    'recycle_bin_restore_deleted_parent' => 'Nadrejeni element je bil prav tako izbrisan. Dokler se ne obnovi nadrejenega elementa, ni mogoče obnoviti njemu podrejenih elementov.',\n    'recycle_bin_restore_parent' => 'Restore Parent',\n    'recycle_bin_destroy_notification' => 'Izbrisano :count skupno število elementov iz koša.',\n    'recycle_bin_restore_notification' => 'Obnovljeno :count skupno število elementov iz koša.',\n\n    // Audit Log\n    'audit' => 'Dnevnik dogodkov',\n    'audit_desc' => 'Ta dnevnik dogodkov prikazuje seznam dejavnosti, ki jim sledi sistem. Seznam je nefiltriran, za razliko od podobnih seznamov dejavnosti v sistemu, kjer se uporabljajo filtri dovoljenj.',\n    'audit_event_filter' => 'Filter dogodkov',\n    'audit_event_filter_no_filter' => 'Ni filtra',\n    'audit_deleted_item' => 'Izbrisan element',\n    'audit_deleted_item_name' => 'Naziv: :name',\n    'audit_table_user' => 'Uporabnik',\n    'audit_table_event' => 'Dogodek',\n    'audit_table_related' => 'Povezani predmet ali podrobnost',\n    'audit_table_ip' => 'IP Address',\n    'audit_table_date' => 'Datum zadnje dejavnosti',\n    'audit_date_from' => 'Časovno obdobje od',\n    'audit_date_to' => 'Časovno obdobje do',\n\n    // Role Settings\n    'roles' => 'Vloge',\n    'role_user_roles' => 'Vloge uporabnika',\n    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',\n    'roles_x_users_assigned' => ':count uporabnik v tej vlogi|:count uporabnikov v tej vlogi',\n    'roles_x_permissions_provided' => ':count permission|:count permissions',\n    'roles_assigned_users' => 'Assigned Users',\n    'roles_permissions_provided' => 'Provided Permissions',\n    'role_create' => 'Ustvari novo vlogo',\n    'role_delete' => 'Brisanje vloge',\n    'role_delete_confirm' => 'Izbrisana bo vloga z imenom \\':roleName\\'.',\n    'role_delete_users_assigned' => 'Ta vloga ima dodeljenih :userCount uporabnikov. V kolikor želite uporabnike preseliti iz te vloge, spodaj izberite novo vlogo.',\n    'role_delete_no_migration' => \"Ne prenašaj uporabnikov\",\n    'role_delete_sure' => 'Ali ste prepričani, da želite izbrisati to vlogo?',\n    'role_edit' => 'Uredi vlogo',\n    'role_details' => 'Podrobnosti vloge',\n    'role_name' => 'Naziv vloge',\n    'role_desc' => 'Kratki opis vloge',\n    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',\n    'role_external_auth_id' => 'Zunanje dokazilo ID',\n    'role_system' => 'Sistemska dovoljenja',\n    'role_manage_users' => 'Upravljanje uporabnikov',\n    'role_manage_roles' => 'Upravljanje vlog in dovoljenja vlog',\n    'role_manage_entity_permissions' => 'Upravljanje dovoljenj vseh knjig, poglavij in strani',\n    'role_manage_own_entity_permissions' => 'Upravljanje dovoljenj za svojo knjigo, poglavje in strani',\n    'role_manage_page_templates' => 'Uredi predloge',\n    'role_access_api' => 'API za dostop do sistema',\n    'role_manage_settings' => 'Nastavitve za upravljanje',\n    'role_export_content' => 'Export content',\n    'role_import_content' => 'Import content',\n    'role_editor_change' => 'Change page editor',\n    'role_notifications' => 'Receive & manage notifications',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Sistemska dovoljenja',\n    'roles_system_warning' => 'Zavedajte se, da lahko dostop do kateregakoli od zgornjih treh dovoljenj uporabniku omogoči, da spremeni lastne privilegije ali privilegije drugih v sistemu. Vloge s temi dovoljenji dodelite samo zaupanja vrednim uporabnikom.',\n    'role_asset_desc' => 'Ta dovoljenja nadzorujejo privzeti dostop do sredstev v sistemu. Dovoljenja za knjige, poglavja in strani bodo razveljavila ta dovoljenja.',\n    'role_asset_admins' => 'Skrbniki samodejno pridobijo dostop do vseh vsebin, vendar lahko te možnosti prikažejo ali pa skrijejo možnosti uporabniškega vmesnika.',\n    'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'Vse',\n    'role_own' => 'Lasten',\n    'role_controlled_by_asset' => 'Nadzira ga sredstvo, v katerega so naloženi',\n    'role_save' => 'Shrani vlogo',\n    'role_users' => 'Uporabniki v tej vlogi',\n    'role_users_none' => 'Tej vlogi trenutno ni dodeljen noben uporabnik',\n\n    // Users\n    'users' => 'Uporabniki',\n    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',\n    'user_profile' => 'Uporabniški profil',\n    'users_add_new' => 'Dodaj novega uporabnika',\n    'users_search' => 'Išči uporabnike',\n    'users_latest_activity' => 'Zadnja dejavnost',\n    'users_details' => 'Podatki o uporabniku',\n    'users_details_desc' => 'Nastavite prikazno ime in e-poštni naslov za tega uporabnika. E-poštni naslov bo uporabljen za prijavo v aplikacijo.',\n    'users_details_desc_no_email' => ' Nastavite prikazno ime za tega uporabnika, da ga bodo drugi lahko prepoznali.',\n    'users_role' => 'Vloge uporabnika',\n    'users_role_desc' => 'Izberi vloge, ki bodo dodeljene uporabniku. Če je uporabniku dodeljenih več vlog, se dovoljenja združijo in prejmenjo vsa dovoljenja dodeljenih vlog.',\n    'users_password' => 'Uporabniško geslo',\n    'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',\n    'users_send_invite_text' => 'Uporabniku lahko pošljete e-poštno sporočilo s povabilom, ki mu omogoča, da nastavi svoje geslo, ali pa ga nastavite kar sami.',\n    'users_send_invite_option' => 'Pošlji uporabniku e-poštno povabilo',\n    'users_external_auth_id' => 'Zunanji ID za preverjanje pristnosti (Authentication ID)',\n    'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.',\n    'users_password_warning' => 'Only fill the below if you would like to change the password for this user.',\n    'users_system_public' => 'Ta uporabnik predstavlja vse gostujoče uporabnike, ki obiščejo vašo wiki stran. Za prijavo je ni mogoče uporabiti, ampak je dodeljena samodejno.',\n    'users_delete' => 'Brisanje uporabnika',\n    'users_delete_named' => 'Brisanje uporabnika :userName',\n    'users_delete_warning' => 'Iz sistema se bo popolnoma  izbrisal uporabnik z imenom \\':userName\\'',\n    'users_delete_confirm' => 'Ste prepričani, da želite izbrisati tega uporabnika?',\n    'users_migrate_ownership' => 'Prenesi lastništvo',\n    'users_migrate_ownership_desc' => 'Izberite uporabnika, če želite nanj prenesti lastništvo vseh vnosov.',\n    'users_none_selected' => 'Ni izbranega uporabnika',\n    'users_edit' => 'Uredi uporabnika',\n    'users_edit_profile' => 'Uredi profil',\n    'users_avatar' => 'Uporabnikov avatar',\n    'users_avatar_desc' => 'Izberi sliko, ki predstavlja uporabnika. Velikost mora biti približno 256px.',\n    'users_preferred_language' => 'Izbrani jezik',\n    'users_preferred_language_desc' => 'Ta možnost bo spremenila jezik, ki se uporablja za uporabniški vmesnik aplikacije. To ne bo vplivalo na nobeno vsebino, ki jo ustvari uporabnik.',\n    'users_social_accounts' => 'Družbene ikone / računi',\n    'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.',\n    'users_social_accounts_info' => 'Tu lahko za hitrejšo in lažjo prijavo povežete druge račune. Prekinitev povezave računa tukaj ne prekliče predhodno odobrenega dostopa. Prekličite dostop iz nastavitev profila v povezanem družabnem računu.',\n    'users_social_connect' => 'Povežite račun',\n    'users_social_disconnect' => 'Odklop računa',\n    'users_social_status_connected' => 'Connected',\n    'users_social_status_disconnected' => 'Disconnected',\n    'users_social_connected' => ':socialAccount račun je bil uspešno dodan vašemu profilu.',\n    'users_social_disconnected' => ':socialAccount račun je bil uspešno odstranjen iz vašega profila.',\n    'users_api_tokens' => 'API žeton',\n    'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.',\n    'users_api_tokens_none' => 'Nič API žetonov ni bilo ustvarjenih za uporabnika',\n    'users_api_tokens_create' => 'Ustvari žeton',\n    'users_api_tokens_expires' => 'Poteče',\n    'users_api_tokens_docs' => 'API dokumentacija',\n    'users_mfa' => 'Multi-Factor Authentication',\n    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'users_mfa_x_methods' => ':count method configured|:count methods configured',\n    'users_mfa_configure' => 'Configure Methods',\n\n    // API Tokens\n    'user_api_token_create' => 'Ustvari žeton',\n    'user_api_token_name' => 'Ime',\n    'user_api_token_name_desc' => 'Dajte žetonu berljivo ime kot prihodnji opomnik o predvidenem namenu.',\n    'user_api_token_expiry' => 'Datum poteka',\n    'user_api_token_expiry_desc' => 'Določi datum izteka uporabnosti žetona. Po tem datumu, zahteve poslane s tem žetonom, ne bodo več delovale. \nČe pustite to polje prazno, bo iztek uporabnosti 100.let .',\n    'user_api_token_create_secret_message' => 'Takoj po ustvarjanju tega žetona se ustvari in prikaže \"Token ID\" \"in\" Token Secret \". Skrivnost bo prikazana samo enkrat, zato se pred nadaljevanjem prepričajte o varnosti kopirnega mesta.',\n    'user_api_token' => 'API žeton',\n    'user_api_token_id' => 'Žeton ID',\n    'user_api_token_id_desc' => 'To je sistemski identifikator, ki ga ni mogoče urejati za ta žeton in ga je treba navesti v zahtevah za API.',\n    'user_api_token_secret' => 'Skrivnost žetona',\n    'user_api_token_secret_desc' => 'To je sistemsko ustvarjena skrivnost za ta žeton, ki jo bo treba navesti v zahtevah za API. To bo prikazano samo enkrat, zato kopirajte to vrednost na varno mesto.',\n    'user_api_token_created' => 'Žeton ustvarjen :timeAgo',\n    'user_api_token_updated' => 'Žeton posodobljen :timeAgo',\n    'user_api_token_delete' => 'Briši žeton',\n    'user_api_token_delete_warning' => 'Iz sistema se bo popolnoma  izbrisal API žeton z imenom \\':tokenName\\' ',\n    'user_api_token_delete_confirm' => 'Ali ste prepričani, da želite izbrisati ta API žeton?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',\n    'webhooks_x_trigger_events' => ':count trigger event|:count trigger events',\n    'webhooks_create' => 'Create New Webhook',\n    'webhooks_none_created' => 'No webhooks have yet been created.',\n    'webhooks_edit' => 'Edit Webhook',\n    'webhooks_save' => 'Save Webhook',\n    'webhooks_details' => 'Webhook Details',\n    'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',\n    'webhooks_events' => 'Webhook Events',\n    'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',\n    'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\\'t expose confidential content.',\n    'webhooks_events_all' => 'All system events',\n    'webhooks_name' => 'Webhook Name',\n    'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',\n    'webhooks_endpoint' => 'Webhook Endpoint',\n    'webhooks_active' => 'Webhook Active',\n    'webhook_events_table_header' => 'Events',\n    'webhooks_delete' => 'Delete Webhook',\n    'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \\':webhookName\\', from the system.',\n    'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?',\n    'webhooks_format_example' => 'Webhook Format Example',\n    'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The \"related_item\" and \"url\" properties are optional and will depend on the type of event triggered.',\n    'webhooks_status' => 'Webhook Status',\n    'webhooks_last_called' => 'Last Called:',\n    'webhooks_last_errored' => 'Last Errored:',\n    'webhooks_last_error_message' => 'Last Error Message:',\n\n    // Licensing\n    'licenses' => 'Licenses',\n    'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.',\n    'licenses_bookstack' => 'BookStack License',\n    'licenses_php' => 'PHP Library Licenses',\n    'licenses_js' => 'JavaScript Library Licenses',\n    'licenses_other' => 'Other Licenses',\n    'license_details' => 'License Details',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'danščina',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/sl/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute mora biti potrjen.',\n    'active_url'           => ':attribute ni veljaven URL.',\n    'after'                => ':attribute mora biti datum po :date.',\n    'alpha'                => ':attribute lahko vsebuje samo črke.',\n    'alpha_dash'           => ':attribute lahko vsebuje samo črke, številke, pomišljaje in podčrtaje.',\n    'alpha_num'            => ':attribute lahko vsebuje samo črke in številke.',\n    'array'                => ':attribute mora biti niz.',\n    'backup_codes'         => 'Podana koda ni veljavna ali je že uporabljena.',\n    'before'               => ':attribute mora biti datum pred :date.',\n    'between'              => [\n        'numeric' => ':attribute mora biti med :min in :max.',\n        'file'    => ':attribute mora biti med :min in :max kilobajti.',\n        'string'  => ':attribute mora biti med :min in :max znaki.',\n        'array'   => ':attribute mora imeti med :min in :max elementov.',\n    ],\n    'boolean'              => ':attribute polje mora biti pravilno ali napačno.',\n    'confirmed'            => ':attribute potrditev se ne ujema.',\n    'date'                 => ':attribute ni veljaven datum.',\n    'date_format'          => ':attribute se ne ujema z obliko :format.',\n    'different'            => ':attribute in :other morata biti različna.',\n    'digits'               => 'Atribut mora biti: števnik.',\n    'digits_between'       => ':attribute mora biti med :min in :max števkami.',\n    'email'                => ':attribute mora biti veljaven e-naslov.',\n    'ends_with' => 'The :attribute se mora končati z eno od določenih: :vrednost/values',\n    'file'                 => ':attribute ni veljavna datoteka.',\n    'filled'               => 'Polje ne sme biti prazno.',\n    'gt'                   => [\n        'numeric' => ':attribute mora biti večji kot :vrednost.',\n        'file'    => ':attribute mora biti večji kot :vrednost kilobytes',\n        'string'  => ':attribute mora biti večji kot :vrednost znakov',\n        'array'   => ':attribute mora biti večji kot :vrednost znakov',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute mora biti večji kot ali enak :vrednost.',\n        'file'    => ':attribute mora biti večji kot ali enak :vrednost kilobytes',\n        'string'  => ':attribute mora biti večji kot ali enak :vrednost znakov',\n        'array'   => ':attribute mora imeti :vrednost znakov ali več',\n    ],\n    'exists'               => 'Izbrani atribut je neveljaven.',\n    'image'                => ':attribute mora biti slika.',\n    'image_extension'      => ':attribute mora imeti veljavno & podprto slikovno pripono',\n    'in'                   => 'izbran :attribute je neveljaven.',\n    'integer'              => ':attribute mora biti celo število.',\n    'ip'                   => ':attribute mora biti veljaven IP naslov.',\n    'ipv4'                 => ':attribute mora biti veljaven IPv4 naslov.',\n    'ipv6'                 => ':attribute mora biti veljaven IPv6 naslov.',\n    'json'                 => ':attribute mora biti veljavna JSON povezava.',\n    'lt'                   => [\n        'numeric' => ':attribute mora biti manj kot :vrednost.',\n        'file'    => ':attribute mora biti manj kot :vrednost kilobytes',\n        'string'  => ':attribute mora biti manj kot :vrednost znakov',\n        'array'   => ':attribute mora imeti manj kot :vrednost znakov',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute mora biti manj kot ali enak :vrednost.',\n        'file'    => ':attribute mora biti manj kot ali enak :vrednost kilobytes',\n        'string'  => ':attribute mora biti manj kot ali enak :vrednost znakov',\n        'array'   => ':attribute ne sme imeti več kot :vrednost elementov',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute ne sme biti večja od :max.',\n        'file'    => ':attribute ne sme biti večja od :max kilobytes.',\n        'string'  => 'Atribut naj ne bo večji od: max znakov.',\n        'array'   => ':attribute ne sme imeti več kot :max elementov.',\n    ],\n    'mimes'                => 'Atribut mora biti datoteka vrste:: vrednost.',\n    'min'                  => [\n        'numeric' => ':attribute mora biti najmanj :min.',\n        'file'    => ':attribute mora biti najmanj :min KB.',\n        'string'  => ':attribute mora biti najmanj :min znakov.',\n        'array'   => ':attribute mora imeti vsaj :min elementov.',\n    ],\n    'not_in'               => 'Izbrani atribut je neveljaven.',\n    'not_regex'            => ':attribute oblika ni veljavna.',\n    'numeric'              => 'Atribut mora biti število.',\n    'regex'                => ':attribute oblika ni veljavna.',\n    'required'             => 'Polje :attribute je obvezno.',\n    'required_if'          => 'Polje atributa je obvezno, če: drugo je: vrednost.',\n    'required_with'        => 'Polje atributa je obvezno, ko: so prisotne vrednosti.',\n    'required_with_all'    => 'Polje atributa je obvezno, ko: so prisotne vrednosti.',\n    'required_without'     => 'Polje atributa je obvezno, če: vrednosti niso prisotne.',\n    'required_without_all' => 'Polje atributa je obvezno, če nobena od: vrednosti ni prisotna.',\n    'same'                 => 'Atribut in: drugi se morajo ujemati.',\n    'safe_url'             => 'Podana povezava morda ni varna.',\n    'size'                 => [\n        'numeric' => ':attribute mora biti :velikost.',\n        'file'    => ':attribute mora biti :velikost KB.',\n        'string'  => 'Atribut mora biti: velikost znakov.',\n        'array'   => ':attribute mora vsebovati :velikost elementov.',\n    ],\n    'string'               => ':attribute mora biti niz.',\n    'timezone'             => ':attribute mora biti veljavna cona.',\n    'totp'                 => 'Podana koda ni veljavna ali je zapadla.',\n    'unique'               => ':attribute je že zaseden.',\n    'url'                  => ':attribute oblika ni veljavna.',\n    'uploaded'             => 'Datoteke ni bilo mogoče naložiti. Strežnik morda ne sprejema datotek te velikosti.',\n\n    'zip_file' => 'The :attribute needs to reference a file within the ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',\n    'zip_model_expected' => 'Data object expected but \":type\" found.',\n    'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Potrditev gesla',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/sq/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'krijoi faqe',\n    'page_create_notification'    => 'Faqja u krijua me sukses',\n    'page_update'                 => 'përditësoi faqe',\n    'page_update_notification'    => 'Faqja u përditësua me sukses',\n    'page_delete'                 => 'fshiu faqe',\n    'page_delete_notification'    => 'Faqja u fshi me sukses',\n    'page_restore'                => 'riktheu faqe',\n    'page_restore_notification'   => 'Faqja u rikthye me sukses',\n    'page_move'                   => 'zhvendosi faqe',\n    'page_move_notification'      => 'Faqja u zhvendos me sukses',\n\n    // Chapters\n    'chapter_create'              => 'krijoi kapitull',\n    'chapter_create_notification' => 'Kapitulli u krijua me sukses',\n    'chapter_update'              => 'përditësoi kapitull',\n    'chapter_update_notification' => 'Kapitulli u përditësua me sukses',\n    'chapter_delete'              => 'fshiu kapitull',\n    'chapter_delete_notification' => 'Kapitulli u fshi me sukses',\n    'chapter_move'                => 'zhvendosi kapitull',\n    'chapter_move_notification' => 'Kapitulli u zhvendos me sukses',\n\n    // Books\n    'book_create'                 => 'krijoi libër',\n    'book_create_notification'    => 'Libri u krijua me sukses',\n    'book_create_from_chapter'              => 'konvertoi kapitullin në libër',\n    'book_create_from_chapter_notification' => 'Kapitulli u konvertua në libër me sukses',\n    'book_update'                 => 'përditësoi libër',\n    'book_update_notification'    => 'Libri u përditësua me sukses',\n    'book_delete'                 => 'fshiu libër',\n    'book_delete_notification'    => 'Libri u fshi me sukses',\n    'book_sort'                   => 'renditi libër',\n    'book_sort_notification'      => 'Libri u rendit me sukses',\n\n    // Bookshelves\n    'bookshelf_create'            => 'krijoi raft',\n    'bookshelf_create_notification'    => 'Rafti u krijua me sukses',\n    'bookshelf_create_from_book'    => 'konvertoi librin në raft',\n    'bookshelf_create_from_book_notification'    => 'Libri u konvertua ne raft me sukses',\n    'bookshelf_update'                 => 'përditësoi raftin',\n    'bookshelf_update_notification'    => 'Rafti u përditësua me sukses',\n    'bookshelf_delete'                 => 'fshiu raftin',\n    'bookshelf_delete_notification'    => 'Rafti u fshi me sukses',\n\n    // Revisions\n    'revision_restore' => 'riktheu rishikimin',\n    'revision_delete' => 'fshiu rishikimin',\n    'revision_delete_notification' => 'Rishikimi u fshi me sukses',\n\n    // Favourites\n    'favourite_add_notification' => '\":emri\" është shtuar në listën tuaj të të preferuarve',\n    'favourite_remove_notification' => '\":emri\" është hequr nga lista juaj e të preferuarve',\n\n    // Watching\n    'watch_update_level_notification' => 'Preferencat e orës u përditësuan me sukses',\n\n    // Auth\n    'auth_login' => 'loguar',\n    'auth_register' => 'regjistruar si përdorues i ri',\n    'auth_password_reset_request' => 'kërkoi rivendosjen e fjalëkalimit të përdoruesit',\n    'auth_password_reset_update' => 'rivendos fjalëkalimin e përdoruesit',\n    'mfa_setup_method' => 'konfiguroi metodën MFA',\n    'mfa_setup_method_notification' => 'Metoda Multi-factor u konfigurua me sukses',\n    'mfa_remove_method' => 'hoqi metodën MFA',\n    'mfa_remove_method_notification' => 'Metoda Multi-factor u hoq me sukses',\n\n    // Settings\n    'settings_update' => 'përditësoi cilësimet',\n    'settings_update_notification' => 'Cilësimet u përditësuan me sukses',\n    'maintenance_action_run' => 'u zhvillua veprim i mirëmbajtjes',\n\n    // Webhooks\n    'webhook_create' => 'u krijua uebhook',\n    'webhook_create_notification' => 'Uebhook-u u krijua me sukses',\n    'webhook_update' => 'përditësoi uebhook',\n    'webhook_update_notification' => 'Uebhook-u u përditësua me sukses',\n    'webhook_delete' => 'fshiu uebhook',\n    'webhook_delete_notification' => 'Uebhook-u u fshi me sukses',\n\n    // Imports\n    'import_create' => 'importi i krijuar',\n    'import_create_notification' => 'Importi u ngarkua me sukses',\n    'import_run' => 'Importi i përditësuar',\n    'import_run_notification' => 'Përmbajtja u importua me sukses',\n    'import_delete' => 'Importi i fshirë',\n    'import_delete_notification' => 'Importi u fshi me sukses',\n\n    // Users\n    'user_create' => 'krijoi përdorues',\n    'user_create_notification' => 'Përdoruesi u krijua me sukses',\n    'user_update' => 'përditësoi përdorues',\n    'user_update_notification' => 'Përdoruesi u përditësua me sukses',\n    'user_delete' => 'fshi përdorues',\n    'user_delete_notification' => 'Përdoruesi u fshi me sukses',\n\n    // API Tokens\n    'api_token_create' => 'Krijoi token API',\n    'api_token_create_notification' => 'Token API u krijua me sukses',\n    'api_token_update' => 'Token i përditësuar i API-t',\n    'api_token_update_notification' => 'Token API u përditësua me sukses',\n    'api_token_delete' => 'Fshiu tokenin API',\n    'api_token_delete_notification' => 'Token API u fshi me sukses',\n\n    // Roles\n    'role_create' => 'krijoi rol',\n    'role_create_notification' => 'Roli u krijua me sukses',\n    'role_update' => 'përditësoi rol',\n    'role_update_notification' => 'Roli u përditësua me sukses',\n    'role_delete' => 'fshiu rol',\n    'role_delete_notification' => 'Roli u fshi me sukses',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'boshatisi koshin e riciklimit',\n    'recycle_bin_restore' => 'riktheu nga koshi i riciklimit',\n    'recycle_bin_destroy' => 'fshiu nga koshi i riciklimit',\n\n    // Comments\n    'commented_on'                => 'komentoi në',\n    'comment_create'              => 'shtoi koment',\n    'comment_update'              => 'përditësoi koment',\n    'comment_delete'              => 'fshiu koment',\n\n    // Sort Rules\n    'sort_rule_create' => 'Rregull i krijuar renditjeje',\n    'sort_rule_create_notification' => 'Rregulli i renditjes u krijua me sukses',\n    'sort_rule_update' => 'rregulli i renditjes i përditësuar',\n    'sort_rule_update_notification' => 'Rregulli i renditjes u përditësua me sukses',\n    'sort_rule_delete' => 'rregulli i renditjes është fshirë',\n    'sort_rule_delete_notification' => 'Rregulli i renditjes u fshi me sukses',\n\n    // Other\n    'permissions_update'          => 'përditësoi lejet',\n];\n"
  },
  {
    "path": "lang/sq/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Këto kredenciale nuk përputhen me të dhënat tona.',\n    'throttle' => 'Shumë përpjekje për hyrje. Ju lutemi provoni përsëri në :seconds sekonda.',\n\n    // Login & Register\n    'sign_up' => 'Regjistrohu',\n    'log_in' => 'Logohu',\n    'log_in_with' => 'Logohu me :socialDriver',\n    'sign_up_with' => 'Regjistrohu me :socialDriver',\n    'logout' => 'Shkyçu',\n\n    'name' => 'Emri',\n    'username' => 'Emri i përdoruesit',\n    'email' => 'Email',\n    'password' => 'Fjalkalimi',\n    'password_confirm' => 'Konfirmo fjalëkalimin',\n    'password_hint' => 'Duhet të jetë të paktën 8 karaktere',\n    'forgot_password' => 'Keni harruar fjalëkalimin?',\n    'remember_me' => 'Më mbaj mend',\n    'ldap_email_hint' => 'Ju lutem fusni një email që do përdorni për këtë llogari.',\n    'create_account' => 'Krijo një llogari',\n    'already_have_account' => 'Keni një llogari?',\n    'dont_have_account' => 'Nuk keni akoma llogari?',\n    'social_login' => 'Kyçu me rrjete sociale',\n    'social_registration' => 'Regjistrohu me rrjete sociale',\n    'social_registration_text' => 'Regjistrohu dhe logohu duhet përdorur një shërbim tjetër.',\n\n    'register_thanks' => 'Faleminderit që u regjistruat!',\n    'register_confirm' => 'Ju lutem kontrolloni emai-in tuaj dhe klikoni te butoni i konfirmimit për të aksesuar :appName.',\n    'registrations_disabled' => 'Regjistrimet janë të mbyllura',\n    'registration_email_domain_invalid' => 'Ky domain email-i nuk ka akses te ky aplikacion',\n    'register_success' => 'Faleminderit që u regjistruar! Ju tani jeni të regjistruar dhe të loguar.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Përpjekje për t\\'u kyçur',\n    'auto_init_starting_desc' => 'Jemi duke kontaktuar sistemin e verifikimit për të filluar proçesin e kyçjes. Nëse nuk ka progres për 5 sekonda, klikoni linkun më poshtë.',\n    'auto_init_start_link' => 'Vazhdoni me verifikimin',\n\n    // Password Reset\n    'reset_password' => 'Rivendosni fjalëkalimin',\n    'reset_password_send_instructions' => 'Shkruani email-in tuaj më poshtë dhe do të merrni një link në email për të rikthyer fjalëkalimin.',\n    'reset_password_send_button' => 'Dërgo linkun e rikthimit të fjalëkalimit',\n    'reset_password_sent' => 'Një link për rikthimin e fjalëkalimit do ju dërgohet në :email nëse adresa e email-it ndodhet në sistem.',\n    'reset_password_success' => 'Fjalëkalimi juaj u rikthye me sukses.',\n    'email_reset_subject' => 'Rikthe fjalëkalimin për :appName',\n    'email_reset_text' => 'Ju po e merrni këtë email sepse ne morëm një kërkesë për rivendosjen e fjalëkalimit për llogarinë tuaj.',\n    'email_reset_not_requested' => 'Nëse nuk keni kërkuar rivendosjen e fjalëkalimit, nuk kërkohet asnjë veprim i mëtejshëm.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Konfirmo email-in tënd në :appName',\n    'email_confirm_greeting' => 'Faleminderit që u bashkuat me :appName!',\n    'email_confirm_text' => 'Ju lutemi konfirmoni adresën tuaj të email-it duke klikuar butonin më poshtë:',\n    'email_confirm_action' => 'Konfirmo email-in',\n    'email_confirm_send_error' => 'Kërkohet konfirmimi i email-it, por sistemi nuk mundi ta dërgonte email-in. Kontaktoni administratorin për t\\'u siguruar që email-i është konfiguruar saktë.',\n    'email_confirm_success' => 'Email-i juaj është konfirmuar! Tani duhet të jeni në gjendje të hyni në sistem duke përdorur këtë adresë email-i.',\n    'email_confirm_resent' => 'Emaili i konfirmimit u ridërgua, ju lutem kontrolloni kutinë tuaj postare.',\n    'email_confirm_thanks' => 'Faleminderit që konfirmuat!',\n    'email_confirm_thanks_desc' => 'Ju lutemi prisni një moment ndërsa konfirmimi juaj përpunohet. Nëse nuk ridrejtoheni pas 3 sekondash, shtypni linkun \"Vazhdo\" më poshtë për të vazhduar.',\n\n    'email_not_confirmed' => 'Adresa e email-it nuk është konfirmuar',\n    'email_not_confirmed_text' => 'Adresa juaj e email-it nuk është konfirmuar ende.',\n    'email_not_confirmed_click_link' => 'Ju lutemi klikoni linkun në emailin që ju është dërguar menjëherë pasi u regjistruat.',\n    'email_not_confirmed_resend' => 'Nëse nuk mund ta gjeni email-in, mund ta ridërgoni email-in e konfirmimit duke plotësuar formularin më poshtë.',\n    'email_not_confirmed_resend_button' => 'Ridërgo emailin e konfirmimit',\n\n    // User Invite\n    'user_invite_email_subject' => 'Je ftuar të bashkohesh me :appName!',\n    'user_invite_email_greeting' => 'Një llogari është krijuar për ty në :appName.',\n    'user_invite_email_text' => 'Klikoni butonin më poshtë për të vendosur një fjalëkalim llogarie dhe për të fituar akses:',\n    'user_invite_email_action' => 'Vendos fjalëkalimin e llogarisë',\n    'user_invite_page_welcome' => 'Mirë se vini në :appName!',\n    'user_invite_page_text' => 'Për të finalizuar llogarinë tuaj dhe për të fituar akses, duhet të vendosni një fjalëkalim i cili do të përdoret për t\\'u kyçur në :appName në vizitat e ardhshme.',\n    'user_invite_page_confirm_button' => 'Konfirmo fjalëkalimin',\n    'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Setup Multi-Factor Authentication',\n    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'mfa_setup_configured' => 'Already configured',\n    'mfa_setup_reconfigure' => 'Reconfigure',\n    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',\n    'mfa_setup_action' => 'Setup',\n    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',\n    'mfa_option_totp_title' => 'Mobile App',\n    'mfa_option_totp_desc' => 'To use multi-factor authentication you\\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Backup Codes',\n    'mfa_option_backup_codes_desc' => 'Generates a set of one-time-use backup codes which you\\'ll enter on login to verify your identity. Make sure to store these in a safe & secure place.',\n    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',\n    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',\n    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\\'ll be able to use one of the codes as a second authentication mechanism.',\n    'mfa_gen_backup_codes_download' => 'Download Codes',\n    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',\n    'mfa_gen_totp_title' => 'Mobile App Setup',\n    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',\n    'mfa_gen_totp_verify_setup' => 'Verify Setup',\n    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',\n    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',\n    'mfa_verify_access' => 'Verify Access',\n    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\\'re granted access. Verify using one of your configured methods to continue.',\n    'mfa_verify_no_methods' => 'No Methods Configured',\n    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\\'ll need to set up at least one method before you gain access.',\n    'mfa_verify_use_totp' => 'Verify using a mobile app',\n    'mfa_verify_use_backup_codes' => 'Verify using a backup code',\n    'mfa_verify_backup_code' => 'Backup Code',\n    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',\n    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',\n    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',\n    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',\n];\n"
  },
  {
    "path": "lang/sq/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Cancel',\n    'close' => 'Close',\n    'confirm' => 'Confirm',\n    'back' => 'Back',\n    'save' => 'Save',\n    'continue' => 'Continue',\n    'select' => 'Select',\n    'toggle_all' => 'Toggle All',\n    'more' => 'More',\n\n    // Form Labels\n    'name' => 'Name',\n    'description' => 'Description',\n    'role' => 'Role',\n    'cover_image' => 'Cover image',\n    'cover_image_description' => 'This image should be approximately 440x250px although it will be flexibly scaled & cropped to fit the user interface in different scenarios as required, so actual dimensions for display will differ.',\n\n    // Actions\n    'actions' => 'Actions',\n    'view' => 'View',\n    'view_all' => 'View All',\n    'new' => 'New',\n    'create' => 'Create',\n    'update' => 'Update',\n    'edit' => 'Edit',\n    'archive' => 'Archive',\n    'unarchive' => 'Un-Archive',\n    'sort' => 'Sort',\n    'move' => 'Move',\n    'copy' => 'Copy',\n    'reply' => 'Reply',\n    'delete' => 'Delete',\n    'delete_confirm' => 'Confirm Deletion',\n    'search' => 'Search',\n    'search_clear' => 'Clear Search',\n    'reset' => 'Reset',\n    'remove' => 'Remove',\n    'add' => 'Add',\n    'configure' => 'Configure',\n    'manage' => 'Manage',\n    'fullscreen' => 'Fullscreen',\n    'favourite' => 'Favourite',\n    'unfavourite' => 'Unfavourite',\n    'next' => 'Next',\n    'previous' => 'Previous',\n    'filter_active' => 'Active Filter:',\n    'filter_clear' => 'Clear Filter',\n    'download' => 'Download',\n    'open_in_tab' => 'Open in Tab',\n    'open' => 'Open',\n\n    // Sort Options\n    'sort_options' => 'Sort Options',\n    'sort_direction_toggle' => 'Sort Direction Toggle',\n    'sort_ascending' => 'Sort Ascending',\n    'sort_descending' => 'Sort Descending',\n    'sort_name' => 'Name',\n    'sort_default' => 'Default',\n    'sort_created_at' => 'Created Date',\n    'sort_updated_at' => 'Updated Date',\n\n    // Misc\n    'deleted_user' => 'Deleted User',\n    'no_activity' => 'No activity to show',\n    'no_items' => 'No items available',\n    'back_to_top' => 'Back to top',\n    'skip_to_main_content' => 'Skip to main content',\n    'toggle_details' => 'Toggle Details',\n    'toggle_thumbnails' => 'Toggle Thumbnails',\n    'details' => 'Details',\n    'grid_view' => 'Grid View',\n    'list_view' => 'List View',\n    'default' => 'Default',\n    'breadcrumb' => 'Breadcrumb',\n    'status' => 'Status',\n    'status_active' => 'Active',\n    'status_inactive' => 'Inactive',\n    'never' => 'Never',\n    'none' => 'None',\n\n    // Header\n    'homepage' => 'Homepage',\n    'header_menu_expand' => 'Expand Header Menu',\n    'profile_menu' => 'Profile Menu',\n    'view_profile' => 'Shiko profilin',\n    'edit_profile' => 'Ndrysho profilin',\n    'dark_mode' => 'Dark Mode',\n    'light_mode' => 'Light Mode',\n    'global_search' => 'Global Search',\n\n    // Layout tabs\n    'tab_info' => 'Info',\n    'tab_info_label' => 'Tab: Show Secondary Information',\n    'tab_content' => 'Content',\n    'tab_content_label' => 'Tab: Show Primary Content',\n\n    // Email Content\n    'email_action_help' => 'If you’re having trouble clicking the \":actionText\" button, copy and paste the URL below into your web browser:',\n    'email_rights' => 'All rights reserved',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Privacy Policy',\n    'terms_of_service' => 'Terms of Service',\n\n    // OpenSearch\n    'opensearch_description' => 'Search :appName',\n];\n"
  },
  {
    "path": "lang/sq/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Image Select',\n    'image_list' => 'Image List',\n    'image_details' => 'Image Details',\n    'image_upload' => 'Upload Image',\n    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',\n    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the \"Upload Image\" button above.',\n    'image_all' => 'All',\n    'image_all_title' => 'View all images',\n    'image_book_title' => 'View images uploaded to this book',\n    'image_page_title' => 'View images uploaded to this page',\n    'image_search_hint' => 'Search by image name',\n    'image_uploaded' => 'Uploaded :uploadedDate',\n    'image_uploaded_by' => 'Uploaded by :userName',\n    'image_uploaded_to' => 'Uploaded to :pageLink',\n    'image_updated' => 'Updated :updateDate',\n    'image_load_more' => 'Load More',\n    'image_image_name' => 'Image Name',\n    'image_delete_used' => 'This image is used in the pages below.',\n    'image_delete_confirm_text' => 'Are you sure you want to delete this image?',\n    'image_select_image' => 'Select Image',\n    'image_dropzone' => 'Drop images or click here to upload',\n    'image_dropzone_drop' => 'Drop images here to upload',\n    'images_deleted' => 'Images Deleted',\n    'image_preview' => 'Image Preview',\n    'image_upload_success' => 'Image uploaded successfully',\n    'image_update_success' => 'Image details successfully updated',\n    'image_delete_success' => 'Image successfully deleted',\n    'image_replace' => 'Replace Image',\n    'image_replace_success' => 'Image file successfully updated',\n    'image_rebuild_thumbs' => 'Regenerate Size Variations',\n    'image_rebuild_thumbs_success' => 'Image size variations successfully rebuilt!',\n\n    // Code Editor\n    'code_editor' => 'Edit Code',\n    'code_language' => 'Code Language',\n    'code_content' => 'Code Content',\n    'code_session_history' => 'Session History',\n    'code_save' => 'Save Code',\n];\n"
  },
  {
    "path": "lang/sq/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'General',\n    'advanced' => 'Advanced',\n    'none' => 'None',\n    'cancel' => 'Cancel',\n    'save' => 'Save',\n    'close' => 'Close',\n    'apply' => 'Apply',\n    'undo' => 'Undo',\n    'redo' => 'Redo',\n    'left' => 'Left',\n    'center' => 'Center',\n    'right' => 'Right',\n    'top' => 'Top',\n    'middle' => 'Middle',\n    'bottom' => 'Bottom',\n    'width' => 'Width',\n    'height' => 'Height',\n    'More' => 'More',\n    'select' => 'Select...',\n\n    // Toolbar\n    'formats' => 'Formats',\n    'header_large' => 'Large Header',\n    'header_medium' => 'Medium Header',\n    'header_small' => 'Small Header',\n    'header_tiny' => 'Tiny Header',\n    'paragraph' => 'Paragraph',\n    'blockquote' => 'Blockquote',\n    'inline_code' => 'Inline code',\n    'callouts' => 'Callouts',\n    'callout_information' => 'Information',\n    'callout_success' => 'Success',\n    'callout_warning' => 'Warning',\n    'callout_danger' => 'Danger',\n    'bold' => 'Bold',\n    'italic' => 'Italic',\n    'underline' => 'Underline',\n    'strikethrough' => 'Strikethrough',\n    'superscript' => 'Superscript',\n    'subscript' => 'Subscript',\n    'text_color' => 'Text color',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Custom color',\n    'remove_color' => 'Remove color',\n    'background_color' => 'Background color',\n    'align_left' => 'Align left',\n    'align_center' => 'Align center',\n    'align_right' => 'Align right',\n    'align_justify' => 'Justify',\n    'list_bullet' => 'Bullet list',\n    'list_numbered' => 'Numbered list',\n    'list_task' => 'Task list',\n    'indent_increase' => 'Increase indent',\n    'indent_decrease' => 'Decrease indent',\n    'table' => 'Table',\n    'insert_image' => 'Insert image',\n    'insert_image_title' => 'Insert/Edit Image',\n    'insert_link' => 'Insert/edit link',\n    'insert_link_title' => 'Insert/Edit Link',\n    'insert_horizontal_line' => 'Insert horizontal line',\n    'insert_code_block' => 'Insert code block',\n    'edit_code_block' => 'Edit code block',\n    'insert_drawing' => 'Insert/edit drawing',\n    'drawing_manager' => 'Drawing manager',\n    'insert_media' => 'Insert/edit media',\n    'insert_media_title' => 'Insert/Edit Media',\n    'clear_formatting' => 'Clear formatting',\n    'source_code' => 'Source code',\n    'source_code_title' => 'Source Code',\n    'fullscreen' => 'Fullscreen',\n    'image_options' => 'Image options',\n\n    // Tables\n    'table_properties' => 'Table properties',\n    'table_properties_title' => 'Table Properties',\n    'delete_table' => 'Delete table',\n    'table_clear_formatting' => 'Clear table formatting',\n    'resize_to_contents' => 'Resize to contents',\n    'row_header' => 'Row header',\n    'insert_row_before' => 'Insert row before',\n    'insert_row_after' => 'Insert row after',\n    'delete_row' => 'Delete row',\n    'insert_column_before' => 'Insert column before',\n    'insert_column_after' => 'Insert column after',\n    'delete_column' => 'Delete column',\n    'table_cell' => 'Cell',\n    'table_row' => 'Row',\n    'table_column' => 'Column',\n    'cell_properties' => 'Cell properties',\n    'cell_properties_title' => 'Cell Properties',\n    'cell_type' => 'Cell type',\n    'cell_type_cell' => 'Cell',\n    'cell_scope' => 'Scope',\n    'cell_type_header' => 'Header cell',\n    'merge_cells' => 'Merge cells',\n    'split_cell' => 'Split cell',\n    'table_row_group' => 'Row Group',\n    'table_column_group' => 'Column Group',\n    'horizontal_align' => 'Horizontal align',\n    'vertical_align' => 'Vertical align',\n    'border_width' => 'Border width',\n    'border_style' => 'Border style',\n    'border_color' => 'Border color',\n    'row_properties' => 'Row properties',\n    'row_properties_title' => 'Row Properties',\n    'cut_row' => 'Cut row',\n    'copy_row' => 'Copy row',\n    'paste_row_before' => 'Paste row before',\n    'paste_row_after' => 'Paste row after',\n    'row_type' => 'Row type',\n    'row_type_header' => 'Header',\n    'row_type_body' => 'Body',\n    'row_type_footer' => 'Footer',\n    'alignment' => 'Alignment',\n    'cut_column' => 'Cut column',\n    'copy_column' => 'Copy column',\n    'paste_column_before' => 'Paste column before',\n    'paste_column_after' => 'Paste column after',\n    'cell_padding' => 'Cell padding',\n    'cell_spacing' => 'Cell spacing',\n    'caption' => 'Caption',\n    'show_caption' => 'Show caption',\n    'constrain' => 'Constrain proportions',\n    'cell_border_solid' => 'Solid',\n    'cell_border_dotted' => 'Dotted',\n    'cell_border_dashed' => 'Dashed',\n    'cell_border_double' => 'Double',\n    'cell_border_groove' => 'Groove',\n    'cell_border_ridge' => 'Ridge',\n    'cell_border_inset' => 'Inset',\n    'cell_border_outset' => 'Outset',\n    'cell_border_none' => 'None',\n    'cell_border_hidden' => 'Hidden',\n\n    // Images, links, details/summary & embed\n    'source' => 'Source',\n    'alt_desc' => 'Alternative description',\n    'embed' => 'Embed',\n    'paste_embed' => 'Paste your embed code below:',\n    'url' => 'URL',\n    'text_to_display' => 'Text to display',\n    'title' => 'Title',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Open link',\n    'open_link_in' => 'Open link in...',\n    'open_link_current' => 'Current window',\n    'open_link_new' => 'New window',\n    'remove_link' => 'Remove link',\n    'insert_collapsible' => 'Insert collapsible block',\n    'collapsible_unwrap' => 'Unwrap',\n    'edit_label' => 'Edit label',\n    'toggle_open_closed' => 'Toggle open/closed',\n    'collapsible_edit' => 'Edit collapsible block',\n    'toggle_label' => 'Toggle label',\n\n    // About view\n    'about' => 'About the editor',\n    'about_title' => 'About the WYSIWYG Editor',\n    'editor_license' => 'Editor License & Copyright',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under the MIT license.',\n    'editor_tiny_license_link' => 'The copyright and license details of TinyMCE can be found here.',\n    'save_continue' => 'Save Page & Continue',\n    'callouts_cycle' => '(Keep pressing to toggle through types)',\n    'link_selector' => 'Link to content',\n    'shortcuts' => 'Shortcuts',\n    'shortcut' => 'Shortcut',\n    'shortcuts_intro' => 'The following shortcuts are available in the editor:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Description',\n];\n"
  },
  {
    "path": "lang/sq/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Recently Created',\n    'recently_created_pages' => 'Recently Created Pages',\n    'recently_updated_pages' => 'Recently Updated Pages',\n    'recently_created_chapters' => 'Recently Created Chapters',\n    'recently_created_books' => 'Recently Created Books',\n    'recently_created_shelves' => 'Recently Created Shelves',\n    'recently_update' => 'Recently Updated',\n    'recently_viewed' => 'Recently Viewed',\n    'recent_activity' => 'Recent Activity',\n    'create_now' => 'Create one now',\n    'revisions' => 'Revisions',\n    'meta_revision' => 'Revision #:revisionCount',\n    'meta_created' => 'Created :timeLength',\n    'meta_created_name' => 'Created :timeLength by :user',\n    'meta_updated' => 'Updated :timeLength',\n    'meta_updated_name' => 'Updated :timeLength by :user',\n    'meta_owned_name' => 'Owned by :user',\n    'meta_reference_count' => 'Referenced by :count item|Referenced by :count items',\n    'entity_select' => 'Entity Select',\n    'entity_select_lack_permission' => 'You don\\'t have the required permissions to select this item',\n    'images' => 'Images',\n    'my_recent_drafts' => 'My Recent Drafts',\n    'my_recently_viewed' => 'My Recently Viewed',\n    'my_most_viewed_favourites' => 'My Most Viewed Favourites',\n    'my_favourites' => 'My Favourites',\n    'no_pages_viewed' => 'You have not viewed any pages',\n    'no_pages_recently_created' => 'No pages have been recently created',\n    'no_pages_recently_updated' => 'No pages have been recently updated',\n    'export' => 'Export',\n    'export_html' => 'Contained Web File',\n    'export_pdf' => 'PDF File',\n    'export_text' => 'Plain Text File',\n    'export_md' => 'Markdown File',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Default Page Template',\n    'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',\n    'default_template_select' => 'Select a template page',\n    'import' => 'Import',\n    'import_validate' => 'Validate Import',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'Select ZIP file to upload',\n    'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',\n    'import_pending' => 'Pending Imports',\n    'import_pending_none' => 'No imports have been started.',\n    'import_continue' => 'Continue Import',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Run Import',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Uploaded by',\n    'import_location' => 'Import Location',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'Import Errors',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Permissions',\n    'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.',\n    'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.',\n    'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.',\n    'permissions_save' => 'Save Permissions',\n    'permissions_owner' => 'Owner',\n    'permissions_role_everyone_else' => 'Everyone Else',\n    'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',\n    'permissions_role_override' => 'Override permissions for role',\n    'permissions_inherit_defaults' => 'Inherit defaults',\n\n    // Search\n    'search_results' => 'Search Results',\n    'search_total_results_found' => ':count result found|:count total results found',\n    'search_clear' => 'Clear Search',\n    'search_no_pages' => 'No pages matched this search',\n    'search_for_term' => 'Search for :term',\n    'search_more' => 'More Results',\n    'search_advanced' => 'Advanced Search',\n    'search_terms' => 'Search Terms',\n    'search_content_type' => 'Content Type',\n    'search_exact_matches' => 'Exact Matches',\n    'search_tags' => 'Tag Searches',\n    'search_options' => 'Options',\n    'search_viewed_by_me' => 'Viewed by me',\n    'search_not_viewed_by_me' => 'Not viewed by me',\n    'search_permissions_set' => 'Permissions set',\n    'search_created_by_me' => 'Created by me',\n    'search_updated_by_me' => 'Updated by me',\n    'search_owned_by_me' => 'Owned by me',\n    'search_date_options' => 'Date Options',\n    'search_updated_before' => 'Updated before',\n    'search_updated_after' => 'Updated after',\n    'search_created_before' => 'Created before',\n    'search_created_after' => 'Created after',\n    'search_set_date' => 'Set Date',\n    'search_update' => 'Update Search',\n\n    // Shelves\n    'shelf' => 'Shelf',\n    'shelves' => 'Shelves',\n    'x_shelves' => ':count Shelf|:count Shelves',\n    'shelves_empty' => 'No shelves have been created',\n    'shelves_create' => 'Create New Shelf',\n    'shelves_popular' => 'Popular Shelves',\n    'shelves_new' => 'New Shelves',\n    'shelves_new_action' => 'New Shelf',\n    'shelves_popular_empty' => 'The most popular shelves will appear here.',\n    'shelves_new_empty' => 'The most recently created shelves will appear here.',\n    'shelves_save' => 'Save Shelf',\n    'shelves_books' => 'Books on this shelf',\n    'shelves_add_books' => 'Add books to this shelf',\n    'shelves_drag_books' => 'Drag books below to add them to this shelf',\n    'shelves_empty_contents' => 'This shelf has no books assigned to it',\n    'shelves_edit_and_assign' => 'Edit shelf to assign books',\n    'shelves_edit_named' => 'Edit Shelf :name',\n    'shelves_edit' => 'Edit Shelf',\n    'shelves_delete' => 'Delete Shelf',\n    'shelves_delete_named' => 'Delete Shelf :name',\n    'shelves_delete_explain' => \"This will delete the shelf with the name ':name'. Contained books will not be deleted.\",\n    'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',\n    'shelves_permissions' => 'Shelf Permissions',\n    'shelves_permissions_updated' => 'Shelf Permissions Updated',\n    'shelves_permissions_active' => 'Shelf Permissions Active',\n    'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',\n    'shelves_permissions_create' => 'Shelf create permissions are only used for copying permissions to child books using the action below. They do not control the ability to create books.',\n    'shelves_copy_permissions_to_books' => 'Copy Permissions to Books',\n    'shelves_copy_permissions' => 'Copy Permissions',\n    'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',\n    'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',\n\n    // Books\n    'book' => 'Book',\n    'books' => 'Books',\n    'x_books' => ':count Book|:count Books',\n    'books_empty' => 'No books have been created',\n    'books_popular' => 'Popular Books',\n    'books_recent' => 'Recent Books',\n    'books_new' => 'New Books',\n    'books_new_action' => 'New Book',\n    'books_popular_empty' => 'The most popular books will appear here.',\n    'books_new_empty' => 'The most recently created books will appear here.',\n    'books_create' => 'Create New Book',\n    'books_delete' => 'Delete Book',\n    'books_delete_named' => 'Delete Book :bookName',\n    'books_delete_explain' => 'This will delete the book with the name \\':bookName\\'. All pages and chapters will be removed.',\n    'books_delete_confirmation' => 'Are you sure you want to delete this book?',\n    'books_edit' => 'Edit Book',\n    'books_edit_named' => 'Edit Book :bookName',\n    'books_form_book_name' => 'Book Name',\n    'books_save' => 'Save Book',\n    'books_permissions' => 'Book Permissions',\n    'books_permissions_updated' => 'Book Permissions Updated',\n    'books_empty_contents' => 'No pages or chapters have been created for this book.',\n    'books_empty_create_page' => 'Create a new page',\n    'books_empty_sort_current_book' => 'Sort the current book',\n    'books_empty_add_chapter' => 'Add a chapter',\n    'books_permissions_active' => 'Book Permissions Active',\n    'books_search_this' => 'Search this book',\n    'books_navigation' => 'Book Navigation',\n    'books_sort' => 'Sort Book Contents',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Sort Book :bookName',\n    'books_sort_name' => 'Sort by Name',\n    'books_sort_created' => 'Sort by Created Date',\n    'books_sort_updated' => 'Sort by Updated Date',\n    'books_sort_chapters_first' => 'Chapters First',\n    'books_sort_chapters_last' => 'Chapters Last',\n    'books_sort_show_other' => 'Show Other Books',\n    'books_sort_save' => 'Save New Order',\n    'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.',\n    'books_sort_move_up' => 'Move Up',\n    'books_sort_move_down' => 'Move Down',\n    'books_sort_move_prev_book' => 'Move to Previous Book',\n    'books_sort_move_next_book' => 'Move to Next Book',\n    'books_sort_move_prev_chapter' => 'Move Into Previous Chapter',\n    'books_sort_move_next_chapter' => 'Move Into Next Chapter',\n    'books_sort_move_book_start' => 'Move to Start of Book',\n    'books_sort_move_book_end' => 'Move to End of Book',\n    'books_sort_move_before_chapter' => 'Move to Before Chapter',\n    'books_sort_move_after_chapter' => 'Move to After Chapter',\n    'books_copy' => 'Copy Book',\n    'books_copy_success' => 'Book successfully copied',\n\n    // Chapters\n    'chapter' => 'Chapter',\n    'chapters' => 'Chapters',\n    'x_chapters' => ':count Chapter|:count Chapters',\n    'chapters_popular' => 'Popular Chapters',\n    'chapters_new' => 'New Chapter',\n    'chapters_create' => 'Create New Chapter',\n    'chapters_delete' => 'Delete Chapter',\n    'chapters_delete_named' => 'Delete Chapter :chapterName',\n    'chapters_delete_explain' => 'This will delete the chapter with the name \\':chapterName\\'. All pages that exist within this chapter will also be deleted.',\n    'chapters_delete_confirm' => 'Are you sure you want to delete this chapter?',\n    'chapters_edit' => 'Edit Chapter',\n    'chapters_edit_named' => 'Edit Chapter :chapterName',\n    'chapters_save' => 'Save Chapter',\n    'chapters_move' => 'Move Chapter',\n    'chapters_move_named' => 'Move Chapter :chapterName',\n    'chapters_copy' => 'Copy Chapter',\n    'chapters_copy_success' => 'Chapter successfully copied',\n    'chapters_permissions' => 'Chapter Permissions',\n    'chapters_empty' => 'No pages are currently in this chapter.',\n    'chapters_permissions_active' => 'Chapter Permissions Active',\n    'chapters_permissions_success' => 'Chapter Permissions Updated',\n    'chapters_search_this' => 'Search this chapter',\n    'chapter_sort_book' => 'Sort Book',\n\n    // Pages\n    'page' => 'Page',\n    'pages' => 'Pages',\n    'x_pages' => ':count Page|:count Pages',\n    'pages_popular' => 'Popular Pages',\n    'pages_new' => 'New Page',\n    'pages_attachments' => 'Attachments',\n    'pages_navigation' => 'Page Navigation',\n    'pages_delete' => 'Delete Page',\n    'pages_delete_named' => 'Delete Page :pageName',\n    'pages_delete_draft_named' => 'Delete Draft Page :pageName',\n    'pages_delete_draft' => 'Delete Draft Page',\n    'pages_delete_success' => 'Page deleted',\n    'pages_delete_draft_success' => 'Draft page deleted',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'Are you sure you want to delete this page?',\n    'pages_delete_draft_confirm' => 'Are you sure you want to delete this draft page?',\n    'pages_editing_named' => 'Editing Page :pageName',\n    'pages_edit_draft_options' => 'Draft Options',\n    'pages_edit_save_draft' => 'Save Draft',\n    'pages_edit_draft' => 'Edit Page Draft',\n    'pages_editing_draft' => 'Editing Draft',\n    'pages_editing_page' => 'Editing Page',\n    'pages_edit_draft_save_at' => 'Draft saved at ',\n    'pages_edit_delete_draft' => 'Delete Draft',\n    'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.',\n    'pages_edit_discard_draft' => 'Discard Draft',\n    'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',\n    'pages_edit_switch_to_markdown_clean' => '(Clean Content)',\n    'pages_edit_switch_to_markdown_stable' => '(Stable Content)',\n    'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Set Changelog',\n    'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\\'ve made',\n    'pages_edit_enter_changelog' => 'Enter Changelog',\n    'pages_editor_switch_title' => 'Switch Editor',\n    'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',\n    'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',\n    'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',\n    'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',\n    'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\\'t persist across this change.',\n    'pages_save' => 'Save Page',\n    'pages_title' => 'Page Title',\n    'pages_name' => 'Page Name',\n    'pages_md_editor' => 'Editor',\n    'pages_md_preview' => 'Preview',\n    'pages_md_insert_image' => 'Insert Image',\n    'pages_md_insert_link' => 'Insert Entity Link',\n    'pages_md_insert_drawing' => 'Insert Drawing',\n    'pages_md_show_preview' => 'Show preview',\n    'pages_md_sync_scroll' => 'Sync preview scroll',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Unsaved Drawing Found',\n    'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',\n    'pages_not_in_chapter' => 'Page is not in a chapter',\n    'pages_move' => 'Move Page',\n    'pages_copy' => 'Copy Page',\n    'pages_copy_desination' => 'Copy Destination',\n    'pages_copy_success' => 'Page successfully copied',\n    'pages_permissions' => 'Page Permissions',\n    'pages_permissions_success' => 'Page permissions updated',\n    'pages_revision' => 'Revision',\n    'pages_revisions' => 'Page Revisions',\n    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',\n    'pages_revisions_named' => 'Page Revisions for :pageName',\n    'pages_revision_named' => 'Page Revision for :pageName',\n    'pages_revision_restored_from' => 'Restored from #:id; :summary',\n    'pages_revisions_created_by' => 'Created By',\n    'pages_revisions_date' => 'Revision Date',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Revision Number',\n    'pages_revisions_numbered' => 'Revision #:id',\n    'pages_revisions_numbered_changes' => 'Revision #:id Changes',\n    'pages_revisions_editor' => 'Editor Type',\n    'pages_revisions_changelog' => 'Changelog',\n    'pages_revisions_changes' => 'Changes',\n    'pages_revisions_current' => 'Current Version',\n    'pages_revisions_preview' => 'Preview',\n    'pages_revisions_restore' => 'Restore',\n    'pages_revisions_none' => 'This page has no revisions',\n    'pages_copy_link' => 'Copy Link',\n    'pages_edit_content_link' => 'Jump to section in editor',\n    'pages_pointer_enter_mode' => 'Enter section select mode',\n    'pages_pointer_label' => 'Page Section Options',\n    'pages_pointer_permalink' => 'Page Section Permalink',\n    'pages_pointer_include_tag' => 'Page Section Include Tag',\n    'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag',\n    'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink',\n    'pages_permissions_active' => 'Page Permissions Active',\n    'pages_initial_revision' => 'Initial publish',\n    'pages_references_update_revision' => 'System auto-update of internal links',\n    'pages_initial_name' => 'New Page',\n    'pages_editing_draft_notification' => 'You are currently editing a draft that was last saved :timeDiff.',\n    'pages_draft_edited_notification' => 'This page has been updated by since that time. It is recommended that you discard this draft.',\n    'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count users have started editing this page',\n        'start_b' => ':userName has started editing this page',\n        'time_a' => 'since the page was last updated',\n        'time_b' => 'in the last :minCount minutes',\n        'message' => ':start :time. Take care not to overwrite each other\\'s updates!',\n    ],\n    'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',\n    'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',\n    'pages_specific' => 'Specific Page',\n    'pages_is_template' => 'Page Template',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Toggle Sidebar',\n    'page_tags' => 'Page Tags',\n    'chapter_tags' => 'Chapter Tags',\n    'book_tags' => 'Book Tags',\n    'shelf_tags' => 'Shelf Tags',\n    'tag' => 'Tag',\n    'tags' =>  'Tags',\n    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',\n    'tag_name' =>  'Tag Name',\n    'tag_value' => 'Tag Value (Optional)',\n    'tags_explain' => \"Add some tags to better categorise your content. \\n You can assign a value to a tag for more in-depth organisation.\",\n    'tags_add' => 'Add another tag',\n    'tags_remove' => 'Remove this tag',\n    'tags_usages' => 'Total tag usages',\n    'tags_assigned_pages' => 'Assigned to Pages',\n    'tags_assigned_chapters' => 'Assigned to Chapters',\n    'tags_assigned_books' => 'Assigned to Books',\n    'tags_assigned_shelves' => 'Assigned to Shelves',\n    'tags_x_unique_values' => ':count unique values',\n    'tags_all_values' => 'All values',\n    'tags_view_tags' => 'View Tags',\n    'tags_view_existing_tags' => 'View existing tags',\n    'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',\n    'attachments' => 'Attachments',\n    'attachments_explain' => 'Upload some files or attach some links to display on your page. These are visible in the page sidebar.',\n    'attachments_explain_instant_save' => 'Changes here are saved instantly.',\n    'attachments_upload' => 'Upload File',\n    'attachments_link' => 'Attach Link',\n    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',\n    'attachments_set_link' => 'Set Link',\n    'attachments_delete' => 'Are you sure you want to delete this attachment?',\n    'attachments_dropzone' => 'Drop files here to upload',\n    'attachments_no_files' => 'No files have been uploaded',\n    'attachments_explain_link' => 'You can attach a link if you\\'d prefer not to upload a file. This can be a link to another page or a link to a file in the cloud.',\n    'attachments_link_name' => 'Link Name',\n    'attachment_link' => 'Attachment link',\n    'attachments_link_url' => 'Link to file',\n    'attachments_link_url_hint' => 'Url of site or file',\n    'attach' => 'Attach',\n    'attachments_insert_link' => 'Add Attachment Link to Page',\n    'attachments_edit_file' => 'Edit File',\n    'attachments_edit_file_name' => 'File Name',\n    'attachments_edit_drop_upload' => 'Drop files or click here to upload and overwrite',\n    'attachments_order_updated' => 'Attachment order updated',\n    'attachments_updated_success' => 'Attachment details updated',\n    'attachments_deleted' => 'Attachment deleted',\n    'attachments_file_uploaded' => 'File successfully uploaded',\n    'attachments_file_updated' => 'File successfully updated',\n    'attachments_link_attached' => 'Link successfully attached to page',\n    'templates' => 'Templates',\n    'templates_set_as_template' => 'Page is a template',\n    'templates_explain_set_as_template' => 'You can set this page as a template so its contents be utilized when creating other pages. Other users will be able to use this template if they have view permissions for this page.',\n    'templates_replace_content' => 'Replace page content',\n    'templates_append_content' => 'Append to page content',\n    'templates_prepend_content' => 'Prepend to page content',\n\n    // Profile View\n    'profile_user_for_x' => 'User for :time',\n    'profile_created_content' => 'Created Content',\n    'profile_not_created_pages' => ':userName has not created any pages',\n    'profile_not_created_chapters' => ':userName has not created any chapters',\n    'profile_not_created_books' => ':userName has not created any books',\n    'profile_not_created_shelves' => ':userName has not created any shelves',\n\n    // Comments\n    'comment' => 'Comment',\n    'comments' => 'Comments',\n    'comment_add' => 'Add Comment',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Leave a comment here',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Save Comment',\n    'comment_new' => 'New Comment',\n    'comment_created' => 'commented :createDiff',\n    'comment_updated' => 'Updated :updateDiff by :username',\n    'comment_updated_indicator' => 'Updated',\n    'comment_deleted_success' => 'Comment deleted',\n    'comment_created_success' => 'Comment added',\n    'comment_updated_success' => 'Comment updated',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Are you sure you want to delete this comment?',\n    'comment_in_reply_to' => 'In reply to :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',\n\n    // Revision\n    'revision_delete_confirm' => 'Are you sure you want to delete this revision?',\n    'revision_restore_confirm' => 'Are you sure you want to restore this revision? The current page contents will be replaced.',\n    'revision_cannot_delete_latest' => 'Cannot delete the latest revision.',\n\n    // Copy view\n    'copy_consider' => 'Please consider the below when copying content.',\n    'copy_consider_permissions' => 'Custom permission settings will not be copied.',\n    'copy_consider_owner' => 'You will become the owner of all copied content.',\n    'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',\n    'copy_consider_attachments' => 'Page attachments will not be copied.',\n    'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',\n\n    // Conversions\n    'convert_to_shelf' => 'Convert to Shelf',\n    'convert_to_shelf_contents_desc' => 'You can convert this book to a new shelf with the same contents. Chapters contained within this book will be converted to new books. If this book contains any pages, that are not in a chapter, this book will be renamed and contain such pages, and this book will become part of the new shelf.',\n    'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.',\n    'convert_book' => 'Convert Book',\n    'convert_book_confirm' => 'Are you sure you want to convert this book?',\n    'convert_undo_warning' => 'This cannot be as easily undone.',\n    'convert_to_book' => 'Convert to Book',\n    'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',\n    'convert_chapter' => 'Convert Chapter',\n    'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',\n\n    // References\n    'references' => 'References',\n    'references_none' => 'There are no tracked references to this item.',\n    'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',\n\n    // Watch Options\n    'watch' => 'Watch',\n    'watch_title_default' => 'Default Preferences',\n    'watch_desc_default' => 'Revert watching to just your default notification preferences.',\n    'watch_title_ignore' => 'Ignore',\n    'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.',\n    'watch_title_new' => 'New Pages',\n    'watch_desc_new' => 'Notify when any new page is created within this item.',\n    'watch_title_updates' => 'All Page Updates',\n    'watch_desc_updates' => 'Notify upon all new pages and page changes.',\n    'watch_desc_updates_page' => 'Notify upon all page changes.',\n    'watch_title_comments' => 'All Page Updates & Comments',\n    'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.',\n    'watch_desc_comments_page' => 'Notify upon page changes and new comments.',\n    'watch_change_default' => 'Change default notification preferences',\n    'watch_detail_ignore' => 'Ignoring notifications',\n    'watch_detail_new' => 'Watching for new pages',\n    'watch_detail_updates' => 'Watching new pages and updates',\n    'watch_detail_comments' => 'Watching new pages, updates & comments',\n    'watch_detail_parent_book' => 'Watching via parent book',\n    'watch_detail_parent_book_ignore' => 'Ignoring via parent book',\n    'watch_detail_parent_chapter' => 'Watching via parent chapter',\n    'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter',\n];\n"
  },
  {
    "path": "lang/sq/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'You do not have permission to access the requested page.',\n    'permissionJson' => 'You do not have permission to perform the requested action.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'A user with the email :email already exists but with different credentials.',\n    'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',\n    'email_already_confirmed' => 'Email has already been confirmed, Try logging in.',\n    'email_confirmation_invalid' => 'This confirmation token is not valid or has already been used, Please try registering again.',\n    'email_confirmation_expired' => 'The confirmation token has expired, A new confirmation email has been sent.',\n    'email_confirmation_awaiting' => 'The email address for the account in use needs to be confirmed',\n    'ldap_fail_anonymous' => 'LDAP access failed using anonymous bind',\n    'ldap_fail_authed' => 'LDAP access failed using given dn & password details',\n    'ldap_extension_not_installed' => 'LDAP PHP extension not installed',\n    'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed',\n    'saml_already_logged_in' => 'Already logged in',\n    'saml_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',\n    'saml_invalid_response_id' => 'The request from the external authentication system is not recognised by a process started by this application. Navigating back after a login could cause this issue.',\n    'saml_fail_authed' => 'Login using :system failed, system did not provide successful authorization',\n    'oidc_already_logged_in' => 'Already logged in',\n    'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',\n    'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',\n    'social_no_action_defined' => 'No action defined',\n    'social_login_bad_response' => \"Error received during :socialAccount login: \\n:error\",\n    'social_account_in_use' => 'This :socialAccount account is already in use, Try logging in via the :socialAccount option.',\n    'social_account_email_in_use' => 'The email :email is already in use. If you already have an account you can connect your :socialAccount account from your profile settings.',\n    'social_account_existing' => 'This :socialAccount is already attached to your profile.',\n    'social_account_already_used_existing' => 'This :socialAccount account is already used by another user.',\n    'social_account_not_used' => 'This :socialAccount account is not linked to any users. Please attach it in your profile settings. ',\n    'social_account_register_instructions' => 'If you do not yet have an account, You can register an account using the :socialAccount option.',\n    'social_driver_not_found' => 'Social driver not found',\n    'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.',\n    'invite_token_expired' => 'This invitation link has expired. You can instead try to reset your account password.',\n    'login_user_not_found' => 'A user for this action could not be found.',\n\n    // System\n    'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.',\n    'cannot_get_image_from_url' => 'Cannot get image from :url',\n    'cannot_create_thumbs' => 'The server cannot create thumbnails. Please check you have the GD PHP extension installed.',\n    'server_upload_limit' => 'The server does not allow uploads of this size. Please try a smaller file size.',\n    'server_post_limit' => 'The server cannot receive the provided amount of data. Try again with less data or a smaller file.',\n    'uploaded'  => 'The server does not allow uploads of this size. Please try a smaller file size.',\n\n    // Drawing & Images\n    'image_upload_error' => 'An error occurred uploading the image',\n    'image_upload_type_error' => 'The image type being uploaded is invalid',\n    'image_upload_replace_type' => 'Image file replacements must be of the same type',\n    'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.',\n    'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.',\n    'image_gallery_thumbnail_memory_limit' => 'Failed to create gallery thumbnails due to system resource limits.',\n    'drawing_data_not_found' => 'Drawing data could not be loaded. The drawing file might no longer exist or you may not have permission to access it.',\n\n    // Attachments\n    'attachment_not_found' => 'Attachment not found',\n    'attachment_upload_error' => 'An error occurred uploading the attachment file',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page',\n    'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content',\n    'page_custom_home_deletion' => 'Cannot delete a page while it is set as a homepage',\n\n    // Entities\n    'entity_not_found' => 'Entity not found',\n    'bookshelf_not_found' => 'Shelf not found',\n    'book_not_found' => 'Book not found',\n    'page_not_found' => 'Page not found',\n    'chapter_not_found' => 'Chapter not found',\n    'selected_book_not_found' => 'The selected book was not found',\n    'selected_book_chapter_not_found' => 'The selected Book or Chapter was not found',\n    'guests_cannot_save_drafts' => 'Guests cannot save drafts',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'You cannot delete the only admin',\n    'users_cannot_delete_guest' => 'You cannot delete the guest user',\n    'users_could_not_send_invite' => 'Could not create user since invite email failed to send',\n\n    // Roles\n    'role_cannot_be_edited' => 'This role cannot be edited',\n    'role_system_cannot_be_deleted' => 'This role is a system role and cannot be deleted',\n    'role_registration_default_cannot_delete' => 'This role cannot be deleted while set as the default registration role',\n    'role_cannot_remove_only_admin' => 'This user is the only user assigned to the administrator role. Assign the administrator role to another user before attempting to remove it here.',\n\n    // Comments\n    'comment_list' => 'An error occurred while fetching the comments.',\n    'cannot_add_comment_to_draft' => 'You cannot add comments to a draft.',\n    'comment_add' => 'An error occurred while adding / updating the comment.',\n    'comment_delete' => 'An error occurred while deleting the comment.',\n    'empty_comment' => 'Cannot add an empty comment.',\n\n    // Error pages\n    '404_page_not_found' => 'Page Not Found',\n    'sorry_page_not_found' => 'Sorry, The page you were looking for could not be found.',\n    'sorry_page_not_found_permission_warning' => 'If you expected this page to exist, you might not have permission to view it.',\n    'image_not_found' => 'Image Not Found',\n    'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',\n    'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',\n    'return_home' => 'Return to home',\n    'error_occurred' => 'An Error Occurred',\n    'app_down' => ':appName is down right now',\n    'back_soon' => 'It will be back up soon.',\n\n    // Import\n    'import_zip_cant_read' => 'Could not read ZIP file.',\n    'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',\n    'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Import ZIP failed to validate with errors:',\n    'import_zip_failed_notification' => 'Failed to import ZIP file.',\n    'import_perms_books' => 'You are lacking the required permissions to create books.',\n    'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',\n    'import_perms_pages' => 'You are lacking the required permissions to create pages.',\n    'import_perms_images' => 'You are lacking the required permissions to create images.',\n    'import_perms_attachments' => 'You are lacking the required permission to create attachments.',\n\n    // API errors\n    'api_no_authorization_found' => 'No authorization token found on the request',\n    'api_bad_authorization_format' => 'An authorization token was found on the request but the format appeared incorrect',\n    'api_user_token_not_found' => 'No matching API token was found for the provided authorization token',\n    'api_incorrect_token_secret' => 'The secret provided for the given used API token is incorrect',\n    'api_user_no_api_permission' => 'The owner of the used API token does not have permission to make API calls',\n    'api_user_token_expired' => 'The authorization token used has expired',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Error thrown when sending a test email:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'The URL does not match the configured allowed SSR hosts',\n];\n"
  },
  {
    "path": "lang/sq/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'New comment on page: :pageName',\n    'new_comment_intro' => 'A user has commented on a page in :appName:',\n    'new_page_subject' => 'New page: :pageName',\n    'new_page_intro' => 'A new page has been created in :appName:',\n    'updated_page_subject' => 'Updated page: :pageName',\n    'updated_page_intro' => 'A page has been updated in :appName:',\n    'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\\'t be sent notifications for further edits to this page by the same editor.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Page Name:',\n    'detail_page_path' => 'Page Path:',\n    'detail_commenter' => 'Commenter:',\n    'detail_comment' => 'Comment:',\n    'detail_created_by' => 'Created By:',\n    'detail_updated_by' => 'Updated By:',\n\n    'action_view_comment' => 'View Comment',\n    'action_view_page' => 'View Page',\n\n    'footer_reason' => 'This notification was sent to you because :link cover this type of activity for this item.',\n    'footer_reason_link' => 'your notification preferences',\n];\n"
  },
  {
    "path": "lang/sq/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Previous',\n    'next'     => 'Next &raquo;',\n\n];\n"
  },
  {
    "path": "lang/sq/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Passwords must be at least eight characters and match the confirmation.',\n    'user' => \"We can't find a user with that e-mail address.\",\n    'token' => 'The password reset token is invalid for this email address.',\n    'sent' => 'We have e-mailed your password reset link!',\n    'reset' => 'Your password has been reset!',\n\n];\n"
  },
  {
    "path": "lang/sq/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'My Account',\n\n    'shortcuts' => 'Shortcuts',\n    'shortcuts_interface' => 'UI Shortcut Preferences',\n    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',\n    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',\n    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',\n    'shortcuts_section_navigation' => 'Navigation',\n    'shortcuts_section_actions' => 'Common Actions',\n    'shortcuts_save' => 'Save Shortcuts',\n    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing \"?\" which will highlight the available shortcuts for actions currently visible on the screen.',\n    'shortcuts_update_success' => 'Shortcut preferences have been updated!',\n    'shortcuts_overview_desc' => 'Manage keyboard shortcuts you can use to navigate the system user interface.',\n\n    'notifications' => 'Notification Preferences',\n    'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.',\n    'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own',\n    'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Notify upon replies to my comments',\n    'notifications_save' => 'Save Preferences',\n    'notifications_update_success' => 'Notification preferences have been updated!',\n    'notifications_watched' => 'Watched & Ignored Items',\n    'notifications_watched_desc' => 'Below are the items that have custom watch preferences applied. To update your preferences for these, view the item then find the watch options in the sidebar.',\n\n    'auth' => 'Access & Security',\n    'auth_change_password' => 'Change Password',\n    'auth_change_password_desc' => 'Change the password you use to log-in to the application. This must be at least 8 characters long.',\n    'auth_change_password_success' => 'Password has been updated!',\n\n    'profile' => 'Profile Details',\n    'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.',\n    'profile_view_public' => 'View Public Profile',\n    'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.',\n    'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.',\n    'profile_email_no_permission' => 'Unfortunately you don\\'t have permission to change your email address. If you want to change this, you\\'d need to ask an administrator to change this for you.',\n    'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.',\n    'profile_admin_options' => 'Administrator Options',\n    'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the \"Settings > Users\" area of the application.',\n\n    'delete_account' => 'Delete Account',\n    'delete_my_account' => 'Delete My Account',\n    'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\\'ve created, such as created pages and uploaded images, will remain.',\n    'delete_my_account_warning' => 'Are you sure you want to delete your account?',\n];\n"
  },
  {
    "path": "lang/sq/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Settings',\n    'settings_save' => 'Save Settings',\n    'system_version' => 'System Version',\n    'categories' => 'Categories',\n\n    // App Settings\n    'app_customization' => 'Customization',\n    'app_features_security' => 'Features & Security',\n    'app_name' => 'Application Name',\n    'app_name_desc' => 'This name is shown in the header and in any system-sent emails.',\n    'app_name_header' => 'Show name in header',\n    'app_public_access' => 'Public Access',\n    'app_public_access_desc' => 'Enabling this option will allow visitors, that are not logged-in, to access content in your BookStack instance.',\n    'app_public_access_desc_guest' => 'Access for public visitors can be controlled through the \"Guest\" user.',\n    'app_public_access_toggle' => 'Allow public access',\n    'app_public_viewing' => 'Allow public viewing?',\n    'app_secure_images' => 'Higher Security Image Uploads',\n    'app_secure_images_toggle' => 'Enable higher security image uploads',\n    'app_secure_images_desc' => 'For performance reasons, all images are public. This option adds a random, hard-to-guess string in front of image urls. Ensure directory indexes are not enabled to prevent easy access.',\n    'app_default_editor' => 'Default Page Editor',\n    'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.',\n    'app_custom_html' => 'Custom HTML Head Content',\n    'app_custom_html_desc' => 'Any content added here will be inserted into the bottom of the <head> section of every page. This is handy for overriding styles or adding analytics code.',\n    'app_custom_html_disabled_notice' => 'Custom HTML head content is disabled on this settings page to ensure any breaking changes can be reverted.',\n    'app_logo' => 'Application Logo',\n    'app_logo_desc' => 'This is used in the application header bar, among other areas. This image should be 86px in height. Large images will be scaled down.',\n    'app_icon' => 'Application Icon',\n    'app_icon_desc' => 'This icon is used for browser tabs and shortcut icons. This should be a 256px square PNG image.',\n    'app_homepage' => 'Application Homepage',\n    'app_homepage_desc' => 'Select a view to show on the homepage instead of the default view. Page permissions are ignored for selected pages.',\n    'app_homepage_select' => 'Select a page',\n    'app_footer_links' => 'Footer Links',\n    'app_footer_links_desc' => 'Add links to show within the site footer. These will be displayed at the bottom of most pages, including those that do not require login. You can use a label of \"trans::<key>\" to use system-defined translations. For example: Using \"trans::common.privacy_policy\" will provide the translated text \"Privacy Policy\" and \"trans::common.terms_of_service\" will provide the translated text \"Terms of Service\".',\n    'app_footer_links_label' => 'Link Label',\n    'app_footer_links_url' => 'Link URL',\n    'app_footer_links_add' => 'Add Footer Link',\n    'app_disable_comments' => 'Disable Comments',\n    'app_disable_comments_toggle' => 'Disable comments',\n    'app_disable_comments_desc' => 'Disables comments across all pages in the application. <br> Existing comments are not shown.',\n\n    // Color settings\n    'color_scheme' => 'Application Color Scheme',\n    'color_scheme_desc' => 'Set the colors to use in the application user interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.',\n    'ui_colors_desc' => 'Set the application primary color and default link color. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the application interface.',\n    'app_color' => 'Primary Color',\n    'link_color' => 'Default Link Color',\n    'content_colors_desc' => 'Set colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.',\n    'bookshelf_color' => 'Shelf Color',\n    'book_color' => 'Book Color',\n    'chapter_color' => 'Chapter Color',\n    'page_color' => 'Page Color',\n    'page_draft_color' => 'Page Draft Color',\n\n    // Registration Settings\n    'reg_settings' => 'Registration',\n    'reg_enable' => 'Enable Registration',\n    'reg_enable_toggle' => 'Enable registration',\n    'reg_enable_desc' => 'When registration is enabled user will be able to sign themselves up as an application user. Upon registration they are given a single, default user role.',\n    'reg_default_role' => 'Default user role after registration',\n    'reg_enable_external_warning' => 'The option above is ignored while external LDAP or SAML authentication is active. User accounts for non-existing members will be auto-created if authentication, against the external system in use, is successful.',\n    'reg_email_confirmation' => 'Email Confirmation',\n    'reg_email_confirmation_toggle' => 'Require email confirmation',\n    'reg_confirm_email_desc' => 'If domain restriction is used then email confirmation will be required and this option will be ignored.',\n    'reg_confirm_restrict_domain' => 'Domain Restriction',\n    'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.',\n    'reg_confirm_restrict_domain_placeholder' => 'No restriction set',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Maintenance',\n    'maint_image_cleanup' => 'Cleanup Images',\n    'maint_image_cleanup_desc' => 'Scans page & revision content to check which images and drawings are currently in use and which images are redundant. Ensure you create a full database and image backup before running this.',\n    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',\n    'maint_image_cleanup_run' => 'Run Cleanup',\n    'maint_image_cleanup_warning' => ':count potentially unused images were found. Are you sure you want to delete these images?',\n    'maint_image_cleanup_success' => ':count potentially unused images found and deleted!',\n    'maint_image_cleanup_nothing_found' => 'No unused images found, Nothing deleted!',\n    'maint_send_test_email' => 'Send a Test Email',\n    'maint_send_test_email_desc' => 'This sends a test email to your email address specified in your profile.',\n    'maint_send_test_email_run' => 'Send test email',\n    'maint_send_test_email_success' => 'Email sent to :address',\n    'maint_send_test_email_mail_subject' => 'Test Email',\n    'maint_send_test_email_mail_greeting' => 'Email delivery seems to work!',\n    'maint_send_test_email_mail_text' => 'Congratulations! As you received this email notification, your email settings seem to be configured properly.',\n    'maint_recycle_bin_desc' => 'Deleted shelves, books, chapters & pages are sent to the recycle bin so they can be restored or permanently deleted. Older items in the recycle bin may be automatically removed after a while depending on system configuration.',\n    'maint_recycle_bin_open' => 'Open Recycle Bin',\n    'maint_regen_references' => 'Regenerate References',\n    'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',\n    'maint_regen_references_success' => 'Reference index has been regenerated!',\n    'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Recycle Bin',\n    'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',\n    'recycle_bin_deleted_item' => 'Deleted Item',\n    'recycle_bin_deleted_parent' => 'Parent',\n    'recycle_bin_deleted_by' => 'Deleted By',\n    'recycle_bin_deleted_at' => 'Deletion Time',\n    'recycle_bin_permanently_delete' => 'Permanently Delete',\n    'recycle_bin_restore' => 'Restore',\n    'recycle_bin_contents_empty' => 'The recycle bin is currently empty',\n    'recycle_bin_empty' => 'Empty Recycle Bin',\n    'recycle_bin_empty_confirm' => 'This will permanently destroy all items in the recycle bin including content contained within each item. Are you sure you want to empty the recycle bin?',\n    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?',\n    'recycle_bin_destroy_list' => 'Items to be Destroyed',\n    'recycle_bin_restore_list' => 'Items to be Restored',\n    'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.',\n    'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.',\n    'recycle_bin_restore_parent' => 'Restore Parent',\n    'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',\n    'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',\n\n    // Audit Log\n    'audit' => 'Audit Log',\n    'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',\n    'audit_event_filter' => 'Event Filter',\n    'audit_event_filter_no_filter' => 'No Filter',\n    'audit_deleted_item' => 'Deleted Item',\n    'audit_deleted_item_name' => 'Name: :name',\n    'audit_table_user' => 'User',\n    'audit_table_event' => 'Event',\n    'audit_table_related' => 'Related Item or Detail',\n    'audit_table_ip' => 'IP Address',\n    'audit_table_date' => 'Activity Date',\n    'audit_date_from' => 'Date Range From',\n    'audit_date_to' => 'Date Range To',\n\n    // Role Settings\n    'roles' => 'Roles',\n    'role_user_roles' => 'User Roles',\n    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',\n    'roles_x_users_assigned' => ':count user assigned|:count users assigned',\n    'roles_x_permissions_provided' => ':count permission|:count permissions',\n    'roles_assigned_users' => 'Assigned Users',\n    'roles_permissions_provided' => 'Provided Permissions',\n    'role_create' => 'Create New Role',\n    'role_delete' => 'Delete Role',\n    'role_delete_confirm' => 'This will delete the role with the name \\':roleName\\'.',\n    'role_delete_users_assigned' => 'This role has :userCount users assigned to it. If you would like to migrate the users from this role select a new role below.',\n    'role_delete_no_migration' => \"Don't migrate users\",\n    'role_delete_sure' => 'Are you sure you want to delete this role?',\n    'role_edit' => 'Edit Role',\n    'role_details' => 'Role Details',\n    'role_name' => 'Role Name',\n    'role_desc' => 'Short Description of Role',\n    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',\n    'role_external_auth_id' => 'External Authentication IDs',\n    'role_system' => 'System Permissions',\n    'role_manage_users' => 'Manage users',\n    'role_manage_roles' => 'Manage roles & role permissions',\n    'role_manage_entity_permissions' => 'Manage all book, chapter & page permissions',\n    'role_manage_own_entity_permissions' => 'Manage permissions on own book, chapter & pages',\n    'role_manage_page_templates' => 'Manage page templates',\n    'role_access_api' => 'Access system API',\n    'role_manage_settings' => 'Manage app settings',\n    'role_export_content' => 'Export content',\n    'role_import_content' => 'Import content',\n    'role_editor_change' => 'Change page editor',\n    'role_notifications' => 'Receive & manage notifications',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Asset Permissions',\n    'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',\n    'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',\n    'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.',\n    'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'All',\n    'role_own' => 'Own',\n    'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',\n    'role_save' => 'Save Role',\n    'role_users' => 'Users in this role',\n    'role_users_none' => 'No users are currently assigned to this role',\n\n    // Users\n    'users' => 'Users',\n    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',\n    'user_profile' => 'User Profile',\n    'users_add_new' => 'Add New User',\n    'users_search' => 'Search Users',\n    'users_latest_activity' => 'Latest Activity',\n    'users_details' => 'User Details',\n    'users_details_desc' => 'Set a display name and an email address for this user. The email address will be used for logging into the application.',\n    'users_details_desc_no_email' => 'Set a display name for this user so others can recognise them.',\n    'users_role' => 'User Roles',\n    'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.',\n    'users_password' => 'User Password',\n    'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',\n    'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.',\n    'users_send_invite_option' => 'Send user invite email',\n    'users_external_auth_id' => 'External Authentication ID',\n    'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.',\n    'users_password_warning' => 'Only fill the below if you would like to change the password for this user.',\n    'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.',\n    'users_delete' => 'Delete User',\n    'users_delete_named' => 'Delete user :userName',\n    'users_delete_warning' => 'This will fully delete this user with the name \\':userName\\' from the system.',\n    'users_delete_confirm' => 'Are you sure you want to delete this user?',\n    'users_migrate_ownership' => 'Migrate Ownership',\n    'users_migrate_ownership_desc' => 'Select a user here if you want another user to become the owner of all items currently owned by this user.',\n    'users_none_selected' => 'No user selected',\n    'users_edit' => 'Edit User',\n    'users_edit_profile' => 'Edit Profile',\n    'users_avatar' => 'User Avatar',\n    'users_avatar_desc' => 'Select an image to represent this user. This should be approx 256px square.',\n    'users_preferred_language' => 'Preferred Language',\n    'users_preferred_language_desc' => 'This option will change the language used for the user-interface of the application. This will not affect any user-created content.',\n    'users_social_accounts' => 'Social Accounts',\n    'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.',\n    'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not revoke previously authorized access. Revoke access from your profile settings on the connected social account.',\n    'users_social_connect' => 'Connect Account',\n    'users_social_disconnect' => 'Disconnect Account',\n    'users_social_status_connected' => 'Connected',\n    'users_social_status_disconnected' => 'Disconnected',\n    'users_social_connected' => ':socialAccount account was successfully attached to your profile.',\n    'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.',\n    'users_api_tokens' => 'API Tokens',\n    'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.',\n    'users_api_tokens_none' => 'No API tokens have been created for this user',\n    'users_api_tokens_create' => 'Create Token',\n    'users_api_tokens_expires' => 'Expires',\n    'users_api_tokens_docs' => 'API Documentation',\n    'users_mfa' => 'Multi-Factor Authentication',\n    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'users_mfa_x_methods' => ':count method configured|:count methods configured',\n    'users_mfa_configure' => 'Configure Methods',\n\n    // API Tokens\n    'user_api_token_create' => 'Create API Token',\n    'user_api_token_name' => 'Name',\n    'user_api_token_name_desc' => 'Give your token a readable name as a future reminder of its intended purpose.',\n    'user_api_token_expiry' => 'Expiry Date',\n    'user_api_token_expiry_desc' => 'Set a date at which this token expires. After this date, requests made using this token will no longer work. Leaving this field blank will set an expiry 100 years into the future.',\n    'user_api_token_create_secret_message' => 'Immediately after creating this token a \"Token ID\" & \"Token Secret\" will be generated and displayed. The secret will only be shown a single time so be sure to copy the value to somewhere safe and secure before proceeding.',\n    'user_api_token' => 'API Token',\n    'user_api_token_id' => 'Token ID',\n    'user_api_token_id_desc' => 'This is a non-editable system generated identifier for this token which will need to be provided in API requests.',\n    'user_api_token_secret' => 'Token Secret',\n    'user_api_token_secret_desc' => 'This is a system generated secret for this token which will need to be provided in API requests. This will only be displayed this one time so copy this value to somewhere safe and secure.',\n    'user_api_token_created' => 'Token created :timeAgo',\n    'user_api_token_updated' => 'Token updated :timeAgo',\n    'user_api_token_delete' => 'Delete Token',\n    'user_api_token_delete_warning' => 'This will fully delete this API token with the name \\':tokenName\\' from the system.',\n    'user_api_token_delete_confirm' => 'Are you sure you want to delete this API token?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',\n    'webhooks_x_trigger_events' => ':count trigger event|:count trigger events',\n    'webhooks_create' => 'Create New Webhook',\n    'webhooks_none_created' => 'No webhooks have yet been created.',\n    'webhooks_edit' => 'Edit Webhook',\n    'webhooks_save' => 'Save Webhook',\n    'webhooks_details' => 'Webhook Details',\n    'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',\n    'webhooks_events' => 'Webhook Events',\n    'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',\n    'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\\'t expose confidential content.',\n    'webhooks_events_all' => 'All system events',\n    'webhooks_name' => 'Webhook Name',\n    'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',\n    'webhooks_endpoint' => 'Webhook Endpoint',\n    'webhooks_active' => 'Webhook Active',\n    'webhook_events_table_header' => 'Events',\n    'webhooks_delete' => 'Delete Webhook',\n    'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \\':webhookName\\', from the system.',\n    'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?',\n    'webhooks_format_example' => 'Webhook Format Example',\n    'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The \"related_item\" and \"url\" properties are optional and will depend on the type of event triggered.',\n    'webhooks_status' => 'Webhook Status',\n    'webhooks_last_called' => 'Last Called:',\n    'webhooks_last_errored' => 'Last Errored:',\n    'webhooks_last_error_message' => 'Last Error Message:',\n\n    // Licensing\n    'licenses' => 'Licenses',\n    'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.',\n    'licenses_bookstack' => 'BookStack License',\n    'licenses_php' => 'PHP Library Licenses',\n    'licenses_js' => 'JavaScript Library Licenses',\n    'licenses_other' => 'Other Licenses',\n    'license_details' => 'License Details',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/sq/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => 'The :attribute must be accepted.',\n    'active_url'           => 'The :attribute is not a valid URL.',\n    'after'                => 'The :attribute must be a date after :date.',\n    'alpha'                => 'The :attribute may only contain letters.',\n    'alpha_dash'           => 'The :attribute may only contain letters, numbers, dashes and underscores.',\n    'alpha_num'            => 'The :attribute may only contain letters and numbers.',\n    'array'                => 'The :attribute must be an array.',\n    'backup_codes'         => 'The provided code is not valid or has already been used.',\n    'before'               => 'The :attribute must be a date before :date.',\n    'between'              => [\n        'numeric' => 'The :attribute must be between :min and :max.',\n        'file'    => 'The :attribute must be between :min and :max kilobytes.',\n        'string'  => 'The :attribute must be between :min and :max characters.',\n        'array'   => 'The :attribute must have between :min and :max items.',\n    ],\n    'boolean'              => 'The :attribute field must be true or false.',\n    'confirmed'            => 'The :attribute confirmation does not match.',\n    'date'                 => 'The :attribute is not a valid date.',\n    'date_format'          => 'The :attribute does not match the format :format.',\n    'different'            => 'The :attribute and :other must be different.',\n    'digits'               => 'The :attribute must be :digits digits.',\n    'digits_between'       => 'The :attribute must be between :min and :max digits.',\n    'email'                => 'The :attribute must be a valid email address.',\n    'ends_with' => 'The :attribute must end with one of the following: :values',\n    'file'                 => 'The :attribute must be provided as a valid file.',\n    'filled'               => 'The :attribute field is required.',\n    'gt'                   => [\n        'numeric' => 'The :attribute must be greater than :value.',\n        'file'    => 'The :attribute must be greater than :value kilobytes.',\n        'string'  => 'The :attribute must be greater than :value characters.',\n        'array'   => 'The :attribute must have more than :value items.',\n    ],\n    'gte'                  => [\n        'numeric' => 'The :attribute must be greater than or equal :value.',\n        'file'    => 'The :attribute must be greater than or equal :value kilobytes.',\n        'string'  => 'The :attribute must be greater than or equal :value characters.',\n        'array'   => 'The :attribute must have :value items or more.',\n    ],\n    'exists'               => 'The selected :attribute is invalid.',\n    'image'                => 'The :attribute must be an image.',\n    'image_extension'      => 'The :attribute must have a valid & supported image extension.',\n    'in'                   => 'The selected :attribute is invalid.',\n    'integer'              => 'The :attribute must be an integer.',\n    'ip'                   => 'The :attribute must be a valid IP address.',\n    'ipv4'                 => 'The :attribute must be a valid IPv4 address.',\n    'ipv6'                 => 'The :attribute must be a valid IPv6 address.',\n    'json'                 => 'The :attribute must be a valid JSON string.',\n    'lt'                   => [\n        'numeric' => 'The :attribute must be less than :value.',\n        'file'    => 'The :attribute must be less than :value kilobytes.',\n        'string'  => 'The :attribute must be less than :value characters.',\n        'array'   => 'The :attribute must have less than :value items.',\n    ],\n    'lte'                  => [\n        'numeric' => 'The :attribute must be less than or equal :value.',\n        'file'    => 'The :attribute must be less than or equal :value kilobytes.',\n        'string'  => 'The :attribute must be less than or equal :value characters.',\n        'array'   => 'The :attribute must not have more than :value items.',\n    ],\n    'max'                  => [\n        'numeric' => 'The :attribute may not be greater than :max.',\n        'file'    => 'The :attribute may not be greater than :max kilobytes.',\n        'string'  => 'The :attribute may not be greater than :max characters.',\n        'array'   => 'The :attribute may not have more than :max items.',\n    ],\n    'mimes'                => 'The :attribute must be a file of type: :values.',\n    'min'                  => [\n        'numeric' => 'The :attribute must be at least :min.',\n        'file'    => 'The :attribute must be at least :min kilobytes.',\n        'string'  => 'The :attribute must be at least :min characters.',\n        'array'   => 'The :attribute must have at least :min items.',\n    ],\n    'not_in'               => 'The selected :attribute is invalid.',\n    'not_regex'            => 'The :attribute format is invalid.',\n    'numeric'              => 'The :attribute must be a number.',\n    'regex'                => 'The :attribute format is invalid.',\n    'required'             => 'The :attribute field is required.',\n    'required_if'          => 'The :attribute field is required when :other is :value.',\n    'required_with'        => 'The :attribute field is required when :values is present.',\n    'required_with_all'    => 'The :attribute field is required when :values is present.',\n    'required_without'     => 'The :attribute field is required when :values is not present.',\n    'required_without_all' => 'The :attribute field is required when none of :values are present.',\n    'same'                 => 'The :attribute and :other must match.',\n    'safe_url'             => 'The provided link may not be safe.',\n    'size'                 => [\n        'numeric' => 'The :attribute must be :size.',\n        'file'    => 'The :attribute must be :size kilobytes.',\n        'string'  => 'The :attribute must be :size characters.',\n        'array'   => 'The :attribute must contain :size items.',\n    ],\n    'string'               => 'The :attribute must be a string.',\n    'timezone'             => 'The :attribute must be a valid zone.',\n    'totp'                 => 'The provided code is not valid or has expired.',\n    'unique'               => 'The :attribute has already been taken.',\n    'url'                  => 'The :attribute format is invalid.',\n    'uploaded'             => 'The file could not be uploaded. The server may not accept files of this size.',\n\n    'zip_file' => 'The :attribute needs to reference a file within the ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',\n    'zip_model_expected' => 'Data object expected but \":type\" found.',\n    'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Password confirmation required',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/sr/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'креирана страница',\n    'page_create_notification'    => 'Страница је успешно креирана',\n    'page_update'                 => 'ажурирана страница',\n    'page_update_notification'    => 'Страница је успешно ажурирана',\n    'page_delete'                 => 'обрисана страница',\n    'page_delete_notification'    => 'Страница је успешно обрисана',\n    'page_restore'                => 'обновљена страна',\n    'page_restore_notification'   => 'Страница је успешно обновљена',\n    'page_move'                   => 'премештена страна',\n    'page_move_notification'      => 'Страница је успешно померена',\n\n    // Chapters\n    'chapter_create'              => 'креирано поглавље',\n    'chapter_create_notification' => 'Поглавље је успешно креирано',\n    'chapter_update'              => 'ажурирано поглавље',\n    'chapter_update_notification' => 'Поглавље је успешно обновљено',\n    'chapter_delete'              => 'обрисано поглавље',\n    'chapter_delete_notification' => 'Поглавље је успешно обрисано',\n    'chapter_move'                => 'премештено поглавље',\n    'chapter_move_notification' => 'Поглавље успешно премештено',\n\n    // Books\n    'book_create'                 => 'креирана књига',\n    'book_create_notification'    => 'Књига је успешно креирана',\n    'book_create_from_chapter'              => 'конвертовано поглавље у књигу',\n    'book_create_from_chapter_notification' => 'Поглавље успешно конвертовано у књигу',\n    'book_update'                 => 'књига је ажурирана',\n    'book_update_notification'    => 'Књига је успешно ажурирана',\n    'book_delete'                 => 'књига је обрисана',\n    'book_delete_notification'    => 'Књига је успешно обрисана',\n    'book_sort'                   => 'сортирана књига',\n    'book_sort_notification'      => 'Књига је успешно поновно сортирана',\n\n    // Bookshelves\n    'bookshelf_create'            => 'креирана полица',\n    'bookshelf_create_notification'    => 'Полица је успешно креирана',\n    'bookshelf_create_from_book'    => 'конвертована је књига у полицу',\n    'bookshelf_create_from_book_notification'    => 'Књига је успешно конвертована у полицу',\n    'bookshelf_update'                 => 'ажурирана полица',\n    'bookshelf_update_notification'    => 'Полица је успешно ажурирана',\n    'bookshelf_delete'                 => 'обрисана је полица',\n    'bookshelf_delete_notification'    => 'Полица је успешно обрисана',\n\n    // Revisions\n    'revision_restore' => 'повраћена ревизија',\n    'revision_delete' => 'обрисана ревизија',\n    'revision_delete_notification' => 'Ревизија је успешно обрисана',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" је додато као ваше омиљено',\n    'favourite_remove_notification' => '\":name\" је уклоњено као ваше омиљено',\n\n    // Watching\n    'watch_update_level_notification' => 'Подешавање праћених предмета је успешно ажурирано',\n\n    // Auth\n    'auth_login' => 'пријављени',\n    'auth_register' => 'регистрован као нови корисник',\n    'auth_password_reset_request' => 'захтевано ресетовање корисничке лозинке',\n    'auth_password_reset_update' => 'ресетуј корисничку лозинку',\n    'mfa_setup_method' => 'конфигурисан МФА метод',\n    'mfa_setup_method_notification' => 'Вишефакторска метода је успешно конфигурисана',\n    'mfa_remove_method' => 'уклоњен МФА метод',\n    'mfa_remove_method_notification' => 'Вишефакторска метода је успешно уклоњена',\n\n    // Settings\n    'settings_update' => 'ажурирана подешавања',\n    'settings_update_notification' => 'Подешавања су успешно ажурирана',\n    'maintenance_action_run' => 'покренуо акцију одржавања',\n\n    // Webhooks\n    'webhook_create' => 'креиран вебхоок',\n    'webhook_create_notification' => 'Вебхоок је успешно креиран',\n    'webhook_update' => 'ажуриран вебхоок',\n    'webhook_update_notification' => 'Вебхоок је успешно ажуриран',\n    'webhook_delete' => 'обрисан вебхоок',\n    'webhook_delete_notification' => 'Вебхоок је успешно обрисан',\n\n    // Imports\n    'import_create' => 'креиран увоз',\n    'import_create_notification' => 'Import successfully uploaded',\n    'import_run' => 'ажуриран увоз',\n    'import_run_notification' => 'Content successfully imported',\n    'import_delete' => 'deleted import',\n    'import_delete_notification' => 'Import successfully deleted',\n\n    // Users\n    'user_create' => 'креирао корисника',\n    'user_create_notification' => 'Корисник је успешно креиран',\n    'user_update' => 'ажуриран корисник',\n    'user_update_notification' => 'Корисник је успешно ажуриран',\n    'user_delete' => 'избрисан корисника',\n    'user_delete_notification' => 'Корисник је успешно уклоњен',\n\n    // API Tokens\n    'api_token_create' => 'креирао апи токен',\n    'api_token_create_notification' => 'АПИ токен је успешно креиран',\n    'api_token_update' => 'ажуриран апи токен',\n    'api_token_update_notification' => 'АПИ токен је успешно ажуриран',\n    'api_token_delete' => 'обрисан апи токен',\n    'api_token_delete_notification' => 'АПИ токен је успешно избрисан',\n\n    // Roles\n    'role_create' => 'створена улога',\n    'role_create_notification' => 'Улога је успешно направљена',\n    'role_update' => 'ажурирана улога',\n    'role_update_notification' => 'Улога је успешно ажурирана',\n    'role_delete' => 'обрисана улога',\n    'role_delete_notification' => 'Улога је успешно избрисана',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'испражњена корпа за отпатке',\n    'recycle_bin_restore' => 'враћен из корпе за отпатке',\n    'recycle_bin_destroy' => 'уклоњен из корпе за отпатке',\n\n    // Comments\n    'commented_on'                => 'коментарисао',\n    'comment_create'              => 'додао/ла коментар',\n    'comment_update'              => 'ажуриран коментар',\n    'comment_delete'              => 'обрисан коментар',\n\n    // Sort Rules\n    'sort_rule_create' => 'created sort rule',\n    'sort_rule_create_notification' => 'Sort rule successfully created',\n    'sort_rule_update' => 'updated sort rule',\n    'sort_rule_update_notification' => 'Sort rule successfully updated',\n    'sort_rule_delete' => 'deleted sort rule',\n    'sort_rule_delete_notification' => 'Sort rule successfully deleted',\n\n    // Other\n    'permissions_update'          => 'ажуриране дозволе',\n];\n"
  },
  {
    "path": "lang/sr/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Ови акредитиви се не поклапају са нашом евиденцијом.',\n    'throttle' => 'Превише покушаја пријаве. Покушајте поново за :seconds секунди.',\n\n    // Login & Register\n    'sign_up' => 'Региструј се',\n    'log_in' => 'Пријави се',\n    'log_in_with' => 'Пријавите се са :socialDriver',\n    'sign_up_with' => 'Пријавите се са :socialDriver',\n    'logout' => 'Одјави се',\n\n    'name' => 'Име',\n    'username' => 'Корисничко име',\n    'email' => 'Е-пошта',\n    'password' => 'Лозинка',\n    'password_confirm' => 'Потврди лозинку',\n    'password_hint' => 'Мора да има најмање 8 знакова',\n    'forgot_password' => 'Заборавили сте лозинку?',\n    'remember_me' => 'Запамти ме',\n    'ldap_email_hint' => 'Унесите адресу е-поште коју ћете користити за овај налог.',\n    'create_account' => 'Направи налог',\n    'already_have_account' => 'Већ имате налог?',\n    'dont_have_account' => 'Немате налог?',\n    'social_login' => 'Пријава путем друштвених мрежа',\n    'social_registration' => 'Регистрација путем друштвених мрежа',\n    'social_registration_text' => 'Региструјте се и пријавите користећи другу услугу.',\n\n    'register_thanks' => 'Хвала на регистрацији!',\n    'register_confirm' => 'Проверите своју е-пошту и кликните на дугме за потврду да бисте приступили :appName.',\n    'registrations_disabled' => 'Регистрације су тренутно онемогућене',\n    'registration_email_domain_invalid' => 'Тај домен е-поште нема приступ овој апликацији',\n    'register_success' => 'Хвала што сте се пријавили! Сада сте регистровани и пријављени.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Покушај пријаве',\n    'auto_init_starting_desc' => 'Контактирамо ваш систем за аутентификацију да бисмо започели процес пријављивања. Ако нема напретка након 5 секунди, можете покушати да кликнете на везу испод.',\n    'auto_init_start_link' => 'Наставите са аутентификацијом',\n\n    // Password Reset\n    'reset_password' => 'Ресетуј лозинку',\n    'reset_password_send_instructions' => 'Унесите своју адресу е-поште испод и биће вам послата порука е-поште са везом за ресетовање лозинке.',\n    'reset_password_send_button' => 'Пошаљи везу за ресетовање',\n    'reset_password_sent' => 'Веза за ресетовање лозинке ће бити послата на :email ако се та адреса е-поште пронађе у систему.',\n    'reset_password_success' => 'Ваша лозинка је успешно ресетована.',\n    'email_reset_subject' => 'Ресетујте лозинку за :appName',\n    'email_reset_text' => 'Примили сте ову е-пошту јер смо примили захтев за ресетовање лозинке за ваш налог.',\n    'email_reset_not_requested' => 'Ако нисте захтевали ресетовање лозинке, нису потребне додатне радње.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Потврдите своју е-пошту на :appName',\n    'email_confirm_greeting' => 'Хвала што сте се придружили :appName!',\n    'email_confirm_text' => 'Молимо потврдите своју адресу е-поште кликом на дугме испод:',\n    'email_confirm_action' => 'Потврдите е-пошту',\n    'email_confirm_send_error' => 'Потребна је потврда е-поште, али систем није могао да пошаље е-пошту. Контактирајте администратора да бисте се уверили да је е-пошта исправно подешена.',\n    'email_confirm_success' => 'Ваша е-пошта је потврђена! Сада би требало да будете у могућности да се пријавите користећи ову адресу е-поште.',\n    'email_confirm_resent' => 'Потврда е-поште је поново послата, проверите пријемно сандуче.',\n    'email_confirm_thanks' => 'Хвала на потврди!',\n    'email_confirm_thanks_desc' => 'Сачекајте тренутак док се обради ваша потврда. Ако не будете преусмерени након 3 секунде, притисните доњу везу „Настави“ да бисте наставили.',\n\n    'email_not_confirmed' => 'Адреса е-поште није потврђена',\n    'email_not_confirmed_text' => 'Ваша адреса е-поште још није потврђена.',\n    'email_not_confirmed_click_link' => 'Кликните на везу у е-поруци која је послата убрзо након што сте се регистровали.',\n    'email_not_confirmed_resend' => 'Ако не можете да пронађете е-пошту, можете поново да пошаљете е-поруку за потврду тако што ћете послати образац испод.',\n    'email_not_confirmed_resend_button' => 'Пошаљи поново мејл за потврду',\n\n    // User Invite\n    'user_invite_email_subject' => 'Позвани сте да се придружите :appName!',\n    'user_invite_email_greeting' => 'За вас је креиран налог на :appName.',\n    'user_invite_email_text' => 'Кликните на дугме испод да бисте поставили лозинку за налог и добили приступ:',\n    'user_invite_email_action' => 'Подесите лозинку за налог',\n    'user_invite_page_welcome' => 'Добродошли у :appName!',\n    'user_invite_page_text' => 'Да бисте завршили креирање налога и добили приступ, потребно је да поставите лозинку која ће се користити за пријаву на :appName приликом будућих посета.',\n    'user_invite_page_confirm_button' => 'Потврди лозинку',\n    'user_invite_success_login' => 'Лозинка је постављена, сада би требало да будете у могућности да се пријавите користећи постављену лозинку за приступ :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Подешавање вишефакторске аутентификације',\n    'mfa_setup_desc' => 'Подесите вишефакторску аутентификацију као додатни ниво безбедности за ваш кориснички налог.',\n    'mfa_setup_configured' => 'Већ конфигурисано',\n    'mfa_setup_reconfigure' => 'Поново конфигуришите',\n    'mfa_setup_remove_confirmation' => 'Да ли сте сигурни да желите да уклоните овај метод вишефакторске аутентификације?',\n    'mfa_setup_action' => 'Подешавање',\n    'mfa_backup_codes_usage_limit_warning' => 'Преостало вам је мање од 5 резервних кодова. Генеришите и сачувајте нови сет пре него што вам понестане кодова како бисте спречили да останете без налога.',\n    'mfa_option_totp_title' => 'Aplikacije za mobilne uređaje',\n    'mfa_option_totp_desc' => 'Да бисте користили вишефакторску аутентификацију, биће вам потребна мобилна апликација која подржава ТОТП, као што јеGoogle Authenticator, Authy или Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Резервни кодови',\n    'mfa_option_backup_codes_desc' => 'Генерише скуп резервних кодова за једнократну употребу које ћете унети приликом пријављивања да бисте потврдили свој идентитет. Обавезно их чувајте на безбедном и безбедном месту.',\n    'mfa_gen_confirm_and_enable' => 'Потврдите и омогућите',\n    'mfa_gen_backup_codes_title' => 'Подешавање резервних кодова',\n    'mfa_gen_backup_codes_desc' => 'Чувајте доњу листу кодова на безбедном месту. Када приступате систему, моћи ћете да користите један од кодова као други механизам за аутентификацију.',\n    'mfa_gen_backup_codes_download' => 'Преузми кодове',\n    'mfa_gen_backup_codes_usage_warning' => 'Сваки код се може искористити једном',\n    'mfa_gen_totp_title' => 'Подешавање мобилне апликације',\n    'mfa_gen_totp_desc' => 'Да бисте користили вишефакторску аутентификацију, биће вам потребна мобилна апликација која подржава ТОТП, као што је Google Authenticator, Authy или Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Скенирајте QR код у наставку помоћу жељене апликације за аутентификацију да бисте започели.',\n    'mfa_gen_totp_verify_setup' => 'Верификуј подешавања',\n    'mfa_gen_totp_verify_setup_desc' => 'Проверите да ли све функционише тако што ћете унети код, генерисан у вашој апликацији за потврду идентитета, у поље за унос испод:',\n    'mfa_gen_totp_provide_code_here' => 'Овде унесите код који је генерисао апликација',\n    'mfa_verify_access' => 'Верификуј приступ',\n    'mfa_verify_access_desc' => 'Ваш кориснички налог захтева да потврдите свој идентитет путем додатног нивоа верификације пре него што вам се одобри приступ. Потврдите коришћењем једног од конфигурисаних метода да бисте наставили.',\n    'mfa_verify_no_methods' => 'Методе нису конфигурисане',\n    'mfa_verify_no_methods_desc' => 'Нису пронађене методе вишефакторске аутентификације за ваш налог. Мораћете да подесите најмање један метод пре него што добијете приступ.',\n    'mfa_verify_use_totp' => 'Верификација путем мобилне апликације',\n    'mfa_verify_use_backup_codes' => 'Проверите помоћу резервног кода',\n    'mfa_verify_backup_code' => 'Резервни код',\n    'mfa_verify_backup_code_desc' => 'Унесите један од преосталих резервних кодова у наставку:',\n    'mfa_verify_backup_code_enter_here' => 'Унеси резервни код овде',\n    'mfa_verify_totp_desc' => 'Унесите код, генерисан помоћу ваше мобилне апликације, у наставку:',\n    'mfa_setup_login_notification' => 'Вишефакторска метода је конфигурисана, сада се поново пријавите користећи конфигурисани метод.',\n];\n"
  },
  {
    "path": "lang/sr/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Поништи',\n    'close' => 'Затвори',\n    'confirm' => 'Потврди',\n    'back' => 'Назад',\n    'save' => 'Сачувај',\n    'continue' => 'Настави',\n    'select' => 'Изабери',\n    'toggle_all' => 'Сакриј/Прикажи све',\n    'more' => 'Више',\n\n    // Form Labels\n    'name' => 'Назив',\n    'description' => 'Опис',\n    'role' => 'Улога',\n    'cover_image' => 'Насловна слика',\n    'cover_image_description' => 'Ова слика би требало да буде приближно 440к250px иако ће бити флексибилно скалирана и исечена како би одговарала корисничком интерфејсу у различитим сценаријима по потреби, тако да ће се стварне димензије приказа разликовати.',\n\n    // Actions\n    'actions' => 'Радње',\n    'view' => 'Преглед',\n    'view_all' => 'Прикажи све',\n    'new' => 'Ново',\n    'create' => 'Креирај',\n    'update' => 'Ажурирање',\n    'edit' => 'Уреди',\n    'archive' => 'Архивирај',\n    'unarchive' => 'Un-Archive',\n    'sort' => 'Разврстај',\n    'move' => 'Премести',\n    'copy' => 'Умножи',\n    'reply' => 'Одговор',\n    'delete' => 'Обриши',\n    'delete_confirm' => 'Потврди брисање',\n    'search' => 'Претражи',\n    'search_clear' => 'Обриши претрагу',\n    'reset' => 'Ресетуј',\n    'remove' => 'Уклони',\n    'add' => 'Додај',\n    'configure' => 'Конфигуриши',\n    'manage' => 'Управљај',\n    'fullscreen' => 'Преко целог екрана',\n    'favourite' => 'Омиљено',\n    'unfavourite' => 'Уклони из \"Омиљено\"',\n    'next' => 'Даље',\n    'previous' => 'Претходно',\n    'filter_active' => 'Активни филтер:',\n    'filter_clear' => 'Уклони филтер',\n    'download' => 'Преузимања',\n    'open_in_tab' => 'Отвори у картици',\n    'open' => 'Отвори',\n\n    // Sort Options\n    'sort_options' => 'Опције сортирања',\n    'sort_direction_toggle' => 'Пребацивање смера сортирања',\n    'sort_ascending' => 'Поређај растуће',\n    'sort_descending' => 'Поређај опадајуће',\n    'sort_name' => 'Назив',\n    'sort_default' => 'Подразумевано',\n    'sort_created_at' => 'Датум креирања',\n    'sort_updated_at' => 'Датум ажурирања',\n\n    // Misc\n    'deleted_user' => 'Избрисан корисник',\n    'no_activity' => 'Нема активности за приказ',\n    'no_items' => 'Нема доступних ставки',\n    'back_to_top' => 'Назад на врх',\n    'skip_to_main_content' => 'Пређи на главни садржај',\n    'toggle_details' => 'Пребаци приказ детаља',\n    'toggle_thumbnails' => 'Пребаци минијатуре',\n    'details' => 'Детаљи',\n    'grid_view' => 'Приказ мреже',\n    'list_view' => 'Приказ листе',\n    'default' => 'Подразумевано',\n    'breadcrumb' => 'Навигација',\n    'status' => 'Стање',\n    'status_active' => 'Активан',\n    'status_inactive' => 'Неактивно',\n    'never' => 'Никад',\n    'none' => 'Ништа',\n\n    // Header\n    'homepage' => 'Почетна страна',\n    'header_menu_expand' => 'Проширите мени заглавља',\n    'profile_menu' => 'Мени профила',\n    'view_profile' => 'Погледај Профил',\n    'edit_profile' => 'Измени профил',\n    'dark_mode' => 'Тамни режим',\n    'light_mode' => 'Светли режим',\n    'global_search' => 'Глобална претрага',\n\n    // Layout tabs\n    'tab_info' => 'Информације',\n    'tab_info_label' => 'Картица: Прикажи секундарне информације',\n    'tab_content' => 'Садржај',\n    'tab_content_label' => 'Картица: Прикажи примарни садржај',\n\n    // Email Content\n    'email_action_help' => 'Ако имате проблема да кликнете на дугме \":actionText\" копирајте и налепите УРЛ доле у свој веб прегледач:',\n    'email_rights' => 'Сва права задржана',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Правила о приватности',\n    'terms_of_service' => 'Услови коришћења',\n\n    // OpenSearch\n    'opensearch_description' => 'Search :appName',\n];\n"
  },
  {
    "path": "lang/sr/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Изаберите слику',\n    'image_list' => 'Листа слика',\n    'image_details' => 'Детаљи слике',\n    'image_upload' => 'Додај слику',\n    'image_intro' => 'Овде можете изабрати и управљати сликама које су претходно отпремљене у систем.',\n    'image_intro_upload' => 'Отпремите нову слику тако што ћете превући датотеку слике у овај прозор или помоћу дугмета „Отпреми слику“ изнад.',\n    'image_all' => 'Све',\n    'image_all_title' => 'Прикажи све слике',\n    'image_book_title' => 'Погледајте слике отпремљене уз ову књигу',\n    'image_page_title' => 'Погледајте слике отпремљене на ову страницу',\n    'image_search_hint' => 'Претражите по имену слике',\n    'image_uploaded' => 'Отпремљено :uploadedDate',\n    'image_uploaded_by' => 'Поставио :userName',\n    'image_uploaded_to' => 'Отпремљено на :pageLink',\n    'image_updated' => 'Ажурирано :updateDate',\n    'image_load_more' => 'Учитај још',\n    'image_image_name' => 'Назив слике',\n    'image_delete_used' => 'Ова слика се користи на страницама испод.',\n    'image_delete_confirm_text' => 'Да ли си сигуран да желиш да избришеш ову слику?',\n    'image_select_image' => 'Изабери слику',\n    'image_dropzone' => 'Испустите слике или кликните овде да их отпремите',\n    'image_dropzone_drop' => 'Испустите слике да их отпремите',\n    'images_deleted' => 'Слике су избрисане',\n    'image_preview' => 'Слике су избрисане',\n    'image_upload_success' => 'Слика је успешно отпремљена',\n    'image_update_success' => 'Детаљи слике су успешно ажурирани',\n    'image_delete_success' => 'Слика је успешно избрисана',\n    'image_replace' => 'Замени слику',\n    'image_replace_success' => 'Датотека слике је успешно ажурирана',\n    'image_rebuild_thumbs' => 'Регенеришите варијације величине',\n    'image_rebuild_thumbs_success' => 'Варијације величине слике су успешно обновљене!',\n\n    // Code Editor\n    'code_editor' => 'Уреди код',\n    'code_language' => 'Језик кода',\n    'code_content' => 'Садржај кода',\n    'code_session_history' => 'Историја сесије',\n    'code_save' => 'Сачувај код',\n];\n"
  },
  {
    "path": "lang/sr/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Опште',\n    'advanced' => 'Напредно',\n    'none' => 'Ништа',\n    'cancel' => 'Поништи',\n    'save' => 'Сачувај',\n    'close' => 'Затвори',\n    'apply' => 'Примени',\n    'undo' => 'Опозови',\n    'redo' => 'Понови',\n    'left' => 'Лево',\n    'center' => 'Центар',\n    'right' => 'Десно',\n    'top' => 'Врх',\n    'middle' => 'Средина',\n    'bottom' => 'Дно',\n    'width' => 'Ширина',\n    'height' => 'Висина',\n    'More' => 'Више',\n    'select' => 'Изабери...',\n\n    // Toolbar\n    'formats' => 'Формати',\n    'header_large' => 'Велико заглавље',\n    'header_medium' => 'Средње заглавље',\n    'header_small' => 'Мало заглавље',\n    'header_tiny' => 'Малено заглавље',\n    'paragraph' => 'Параграф',\n    'blockquote' => 'Цитат',\n    'inline_code' => 'Уграђени код',\n    'callouts' => 'Облачићи',\n    'callout_information' => 'Информација',\n    'callout_success' => 'Успешно',\n    'callout_warning' => 'Упозорење',\n    'callout_danger' => 'Опасност',\n    'bold' => 'Подебљано',\n    'italic' => 'Курзив',\n    'underline' => 'Подвучено',\n    'strikethrough' => 'Прецртано',\n    'superscript' => 'Надскрипт',\n    'subscript' => 'Субкрипт',\n    'text_color' => 'Боја текста',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Боја текста',\n    'remove_color' => 'Уклоните боју',\n    'background_color' => 'Боја позадине',\n    'align_left' => 'Поравнај лево',\n    'align_center' => 'Поравнај по средини',\n    'align_right' => 'Поравнај деcно',\n    'align_justify' => 'Поравнај',\n    'list_bullet' => 'Листа ставки',\n    'list_numbered' => 'Нумерисана листа',\n    'list_task' => 'Листа задатака',\n    'indent_increase' => 'Повећај увлачење',\n    'indent_decrease' => 'Умањи увлачење',\n    'table' => 'Tabela',\n    'insert_image' => 'Уметни слику',\n    'insert_image_title' => 'Убаци/уреди слику',\n    'insert_link' => 'Убаци/измени везу',\n    'insert_link_title' => 'Убаци/измени везу',\n    'insert_horizontal_line' => 'Уметни водоравну линију',\n    'insert_code_block' => 'Убаците блок кода',\n    'edit_code_block' => 'Уредите блок кода',\n    'insert_drawing' => 'Уметните/уредите цртеж',\n    'drawing_manager' => 'Менаџер цртежа',\n    'insert_media' => 'Убаците/уредите медиј',\n    'insert_media_title' => 'Уметање/уређивање медија',\n    'clear_formatting' => 'Обриши форматирање',\n    'source_code' => 'Изворни код',\n    'source_code_title' => 'Изворни код',\n    'fullscreen' => 'Преко целог екрана',\n    'image_options' => 'Подешавање слика',\n\n    // Tables\n    'table_properties' => 'Својства табеле',\n    'table_properties_title' => 'Својства табеле',\n    'delete_table' => 'Обриши табелу',\n    'table_clear_formatting' => 'Обриши форматирање табеле',\n    'resize_to_contents' => 'Промени величину садржају',\n    'row_header' => 'Заглавље реда',\n    'insert_row_before' => 'Уметни ред испред',\n    'insert_row_after' => 'Уметните ред после',\n    'delete_row' => 'Обриши ред',\n    'insert_column_before' => 'Уметни колону испред',\n    'insert_column_after' => 'Уметни колону после',\n    'delete_column' => 'Обриши колону',\n    'table_cell' => 'Ћелија',\n    'table_row' => 'Ред',\n    'table_column' => 'Колона',\n    'cell_properties' => 'Својства ћелије',\n    'cell_properties_title' => 'Својства ћелије',\n    'cell_type' => 'Тип ћелије',\n    'cell_type_cell' => 'Ћелија',\n    'cell_scope' => 'Обим',\n    'cell_type_header' => 'Заглавље ћелије',\n    'merge_cells' => 'Обједини ћелије',\n    'split_cell' => 'Подели ћелију',\n    'table_row_group' => 'Група редова',\n    'table_column_group' => 'Група колона',\n    'horizontal_align' => 'Хоризонтално поравнање',\n    'vertical_align' => 'Вертикално поравнати',\n    'border_width' => 'Ширина ивице',\n    'border_style' => 'Стил ивице',\n    'border_color' => 'Боја ивице',\n    'row_properties' => 'Својства реда',\n    'row_properties_title' => 'Својства реда',\n    'cut_row' => 'Пресеци ред',\n    'copy_row' => 'Копирај ред',\n    'paste_row_before' => 'Налепите ред пре',\n    'paste_row_after' => 'Налепите ред после',\n    'row_type' => 'Врста реда',\n    'row_type_header' => 'Заглавље',\n    'row_type_body' => 'Садржај',\n    'row_type_footer' => 'Подножје',\n    'alignment' => 'Поравнавање',\n    'cut_column' => 'Изрежите колону',\n    'copy_column' => 'Копирај колону',\n    'paste_column_before' => 'Налепите колону пре',\n    'paste_column_after' => 'Налепите колону после',\n    'cell_padding' => 'Попуна ћелија',\n    'cell_spacing' => 'Размак ћелија',\n    'caption' => 'Опис',\n    'show_caption' => 'Прикажи титл',\n    'constrain' => 'Ограничавају размере',\n    'cell_border_solid' => 'Чврсто тело',\n    'cell_border_dotted' => 'Тачкасто',\n    'cell_border_dashed' => 'Испрекидана',\n    'cell_border_double' => 'Двострук',\n    'cell_border_groove' => 'Жлеб',\n    'cell_border_ridge' => 'Гребен',\n    'cell_border_inset' => 'Уметак',\n    'cell_border_outset' => 'Почетак',\n    'cell_border_none' => 'Без',\n    'cell_border_hidden' => 'Сакривено',\n\n    // Images, links, details/summary & embed\n    'source' => 'Извор',\n    'alt_desc' => 'Алтернативни опис',\n    'embed' => 'Угради',\n    'paste_embed' => 'Налепите свој код за уградњу испод:',\n    'url' => 'УРЛ',\n    'text_to_display' => 'Текст за приказ',\n    'title' => 'Наслов',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Отвори везу',\n    'open_link_in' => 'Отвори везу у...',\n    'open_link_current' => 'Тренутни прозор',\n    'open_link_new' => 'Нови Прозор',\n    'remove_link' => 'Уклони везу',\n    'insert_collapsible' => 'Уредите склопиви блок',\n    'collapsible_unwrap' => 'Одмотати',\n    'edit_label' => 'Уреди ознаку',\n    'toggle_open_closed' => 'Отварање/затварање',\n    'collapsible_edit' => 'Уредите склопиви блок',\n    'toggle_label' => 'Укључите ознаку',\n\n    // About view\n    'about' => 'О уређивачу',\n    'about_title' => 'О уређивачу WYSIWYG',\n    'editor_license' => 'Уредничка лиценца и ауторска права',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'Овај уређивач је направљен помоћу :tinyLink који је обезбеђен под МИТ лиценцом.',\n    'editor_tiny_license_link' => 'Детаље о ауторским правима и лиценци за ТиниМЦЕ можете пронаћи овде.',\n    'save_continue' => 'Сачувај страницу и настави',\n    'callouts_cycle' => '(Наставите да притискате да бисте прелазили између типова)',\n    'link_selector' => 'Линк до садржаја',\n    'shortcuts' => 'Пречице',\n    'shortcut' => 'Пречица',\n    'shortcuts_intro' => 'Следеће пречице су доступне у уређивачу:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Опис',\n];\n"
  },
  {
    "path": "lang/sr/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Недавно додато',\n    'recently_created_pages' => 'Недавно креиране странице',\n    'recently_updated_pages' => 'Недавно ажуриране странице',\n    'recently_created_chapters' => 'Недавно креирана поглавља',\n    'recently_created_books' => 'Недавно креиране књиге',\n    'recently_created_shelves' => 'Недавно креиране полице',\n    'recently_update' => 'Недавно ажурирано',\n    'recently_viewed' => 'Недавно прегледано',\n    'recent_activity' => 'Скорашња активност',\n    'create_now' => 'Направи један сада',\n    'revisions' => 'Ревизије',\n    'meta_revision' => 'Ревизија #:revisionCount',\n    'meta_created' => 'Направљено :timeLength',\n    'meta_created_name' => 'Направљено :timeLength од :user',\n    'meta_updated' => 'Ажурирано :timeLength',\n    'meta_updated_name' => 'Ажурирано :timeLength од :user',\n    'meta_owned_name' => 'Власништво :user',\n    'meta_reference_count' => 'Референтна од :count item|Референтна од :count items',\n    'entity_select' => 'Избор ентитета',\n    'entity_select_lack_permission' => 'Немате потребне дозволе да изаберете ову ставку',\n    'images' => 'Слике',\n    'my_recent_drafts' => 'Моји недавни нацрти',\n    'my_recently_viewed' => 'Моје недавно прегледано',\n    'my_most_viewed_favourites' => 'Моји најгледанији фаворити',\n    'my_favourites' => 'Моји омиљени',\n    'no_pages_viewed' => 'Нисте погледали ниједну страницу',\n    'no_pages_recently_created' => 'Недавно није направљена ниједна страница',\n    'no_pages_recently_updated' => 'Ниједна страница није недавно ажурирана',\n    'export' => 'Извоз',\n    'export_html' => 'Садржана веб датотека',\n    'export_pdf' => 'PDF датотека',\n    'export_text' => 'Датотеке чистог текста',\n    'export_md' => 'Markdown File',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Подразумевани шаблон странице',\n    'default_template_explain' => 'Доделите шаблон странице који ће се користити као подразумевани садржај за све странице креиране у оквиру ове ставке. Имајте на уму да ће се ово користити само ако креатор странице има приступ за преглед изабране странице шаблона.',\n    'default_template_select' => 'Изаберите страницу са шаблоном',\n    'import' => 'Import',\n    'import_validate' => 'Validate Import',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'Select ZIP file to upload',\n    'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',\n    'import_pending' => 'Pending Imports',\n    'import_pending_none' => 'No imports have been started.',\n    'import_continue' => 'Настави увоз',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Run Import',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Uploaded by',\n    'import_location' => 'Import Location',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'Import Errors',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Дозволе',\n    'permissions_desc' => 'Подесите дозволе овде да бисте заменили подразумеване дозволе које дају корисничке улоге.',\n    'permissions_book_cascade' => 'Дозволе постављене за књиге ће се аутоматски пребацивати на подређена поглавља и странице, осим ако немају дефинисане сопствене дозволе.',\n    'permissions_chapter_cascade' => 'Дозволе постављене на поглављима ће се аутоматски каскадно пребацивати на подређене странице, осим ако немају дефинисане сопствене дозволе.',\n    'permissions_save' => 'Сачувај дозволе',\n    'permissions_owner' => 'Власник',\n    'permissions_role_everyone_else' => 'Сви остали',\n    'permissions_role_everyone_else_desc' => 'Подесите дозволе за све улоге које нису посебно замењене.',\n    'permissions_role_override' => 'Замени дозволе за улогу',\n    'permissions_inherit_defaults' => 'Наследи подразумеване вредности',\n\n    // Search\n    'search_results' => 'Резултати претраге',\n    'search_total_results_found' => ':count пронађених резултата|:count укупно пронађених резултата',\n    'search_clear' => 'Обриши претрагу',\n    'search_no_pages' => 'No pages matched this search',\n    'search_for_term' => 'Search for :term',\n    'search_more' => 'More Results',\n    'search_advanced' => 'Advanced Search',\n    'search_terms' => 'Search Terms',\n    'search_content_type' => 'Content Type',\n    'search_exact_matches' => 'Exact Matches',\n    'search_tags' => 'Tag Searches',\n    'search_options' => 'Options',\n    'search_viewed_by_me' => 'Viewed by me',\n    'search_not_viewed_by_me' => 'Not viewed by me',\n    'search_permissions_set' => 'Permissions set',\n    'search_created_by_me' => 'Created by me',\n    'search_updated_by_me' => 'Updated by me',\n    'search_owned_by_me' => 'Owned by me',\n    'search_date_options' => 'Date Options',\n    'search_updated_before' => 'Updated before',\n    'search_updated_after' => 'Updated after',\n    'search_created_before' => 'Created before',\n    'search_created_after' => 'Created after',\n    'search_set_date' => 'Set Date',\n    'search_update' => 'Update Search',\n\n    // Shelves\n    'shelf' => 'Shelf',\n    'shelves' => 'Полице',\n    'x_shelves' => ':count Shelf|:count Shelves',\n    'shelves_empty' => 'No shelves have been created',\n    'shelves_create' => 'Create New Shelf',\n    'shelves_popular' => 'Popular Shelves',\n    'shelves_new' => 'New Shelves',\n    'shelves_new_action' => 'New Shelf',\n    'shelves_popular_empty' => 'The most popular shelves will appear here.',\n    'shelves_new_empty' => 'The most recently created shelves will appear here.',\n    'shelves_save' => 'Save Shelf',\n    'shelves_books' => 'Books on this shelf',\n    'shelves_add_books' => 'Add books to this shelf',\n    'shelves_drag_books' => 'Drag books below to add them to this shelf',\n    'shelves_empty_contents' => 'This shelf has no books assigned to it',\n    'shelves_edit_and_assign' => 'Edit shelf to assign books',\n    'shelves_edit_named' => 'Edit Shelf :name',\n    'shelves_edit' => 'Edit Shelf',\n    'shelves_delete' => 'Delete Shelf',\n    'shelves_delete_named' => 'Delete Shelf :name',\n    'shelves_delete_explain' => \"This will delete the shelf with the name ':name'. Contained books will not be deleted.\",\n    'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',\n    'shelves_permissions' => 'Shelf Permissions',\n    'shelves_permissions_updated' => 'Shelf Permissions Updated',\n    'shelves_permissions_active' => 'Shelf Permissions Active',\n    'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',\n    'shelves_permissions_create' => 'Shelf create permissions are only used for copying permissions to child books using the action below. They do not control the ability to create books.',\n    'shelves_copy_permissions_to_books' => 'Copy Permissions to Books',\n    'shelves_copy_permissions' => 'Copy Permissions',\n    'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',\n    'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',\n\n    // Books\n    'book' => 'Book',\n    'books' => 'Books',\n    'x_books' => ':count Book|:count Books',\n    'books_empty' => 'No books have been created',\n    'books_popular' => 'Popular Books',\n    'books_recent' => 'Recent Books',\n    'books_new' => 'New Books',\n    'books_new_action' => 'New Book',\n    'books_popular_empty' => 'The most popular books will appear here.',\n    'books_new_empty' => 'The most recently created books will appear here.',\n    'books_create' => 'Create New Book',\n    'books_delete' => 'Delete Book',\n    'books_delete_named' => 'Delete Book :bookName',\n    'books_delete_explain' => 'This will delete the book with the name \\':bookName\\'. All pages and chapters will be removed.',\n    'books_delete_confirmation' => 'Are you sure you want to delete this book?',\n    'books_edit' => 'Edit Book',\n    'books_edit_named' => 'Edit Book :bookName',\n    'books_form_book_name' => 'Book Name',\n    'books_save' => 'Save Book',\n    'books_permissions' => 'Book Permissions',\n    'books_permissions_updated' => 'Book Permissions Updated',\n    'books_empty_contents' => 'No pages or chapters have been created for this book.',\n    'books_empty_create_page' => 'Create a new page',\n    'books_empty_sort_current_book' => 'Sort the current book',\n    'books_empty_add_chapter' => 'Add a chapter',\n    'books_permissions_active' => 'Book Permissions Active',\n    'books_search_this' => 'Search this book',\n    'books_navigation' => 'Book Navigation',\n    'books_sort' => 'Sort Book Contents',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Sort Book :bookName',\n    'books_sort_name' => 'Sort by Name',\n    'books_sort_created' => 'Sort by Created Date',\n    'books_sort_updated' => 'Sort by Updated Date',\n    'books_sort_chapters_first' => 'Chapters First',\n    'books_sort_chapters_last' => 'Chapters Last',\n    'books_sort_show_other' => 'Show Other Books',\n    'books_sort_save' => 'Save New Order',\n    'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.',\n    'books_sort_move_up' => 'Move Up',\n    'books_sort_move_down' => 'Move Down',\n    'books_sort_move_prev_book' => 'Move to Previous Book',\n    'books_sort_move_next_book' => 'Move to Next Book',\n    'books_sort_move_prev_chapter' => 'Move Into Previous Chapter',\n    'books_sort_move_next_chapter' => 'Move Into Next Chapter',\n    'books_sort_move_book_start' => 'Move to Start of Book',\n    'books_sort_move_book_end' => 'Move to End of Book',\n    'books_sort_move_before_chapter' => 'Move to Before Chapter',\n    'books_sort_move_after_chapter' => 'Move to After Chapter',\n    'books_copy' => 'Copy Book',\n    'books_copy_success' => 'Book successfully copied',\n\n    // Chapters\n    'chapter' => 'Chapter',\n    'chapters' => 'Chapters',\n    'x_chapters' => ':count Chapter|:count Chapters',\n    'chapters_popular' => 'Popular Chapters',\n    'chapters_new' => 'New Chapter',\n    'chapters_create' => 'Create New Chapter',\n    'chapters_delete' => 'Delete Chapter',\n    'chapters_delete_named' => 'Delete Chapter :chapterName',\n    'chapters_delete_explain' => 'This will delete the chapter with the name \\':chapterName\\'. All pages that exist within this chapter will also be deleted.',\n    'chapters_delete_confirm' => 'Are you sure you want to delete this chapter?',\n    'chapters_edit' => 'Edit Chapter',\n    'chapters_edit_named' => 'Edit Chapter :chapterName',\n    'chapters_save' => 'Save Chapter',\n    'chapters_move' => 'Move Chapter',\n    'chapters_move_named' => 'Move Chapter :chapterName',\n    'chapters_copy' => 'Copy Chapter',\n    'chapters_copy_success' => 'Chapter successfully copied',\n    'chapters_permissions' => 'Chapter Permissions',\n    'chapters_empty' => 'No pages are currently in this chapter.',\n    'chapters_permissions_active' => 'Chapter Permissions Active',\n    'chapters_permissions_success' => 'Chapter Permissions Updated',\n    'chapters_search_this' => 'Search this chapter',\n    'chapter_sort_book' => 'Sort Book',\n\n    // Pages\n    'page' => 'Page',\n    'pages' => 'Pages',\n    'x_pages' => ':count Page|:count Pages',\n    'pages_popular' => 'Popular Pages',\n    'pages_new' => 'New Page',\n    'pages_attachments' => 'Attachments',\n    'pages_navigation' => 'Page Navigation',\n    'pages_delete' => 'Delete Page',\n    'pages_delete_named' => 'Delete Page :pageName',\n    'pages_delete_draft_named' => 'Delete Draft Page :pageName',\n    'pages_delete_draft' => 'Delete Draft Page',\n    'pages_delete_success' => 'Page deleted',\n    'pages_delete_draft_success' => 'Draft page deleted',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'Are you sure you want to delete this page?',\n    'pages_delete_draft_confirm' => 'Are you sure you want to delete this draft page?',\n    'pages_editing_named' => 'Editing Page :pageName',\n    'pages_edit_draft_options' => 'Draft Options',\n    'pages_edit_save_draft' => 'Save Draft',\n    'pages_edit_draft' => 'Edit Page Draft',\n    'pages_editing_draft' => 'Editing Draft',\n    'pages_editing_page' => 'Editing Page',\n    'pages_edit_draft_save_at' => 'Draft saved at ',\n    'pages_edit_delete_draft' => 'Delete Draft',\n    'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.',\n    'pages_edit_discard_draft' => 'Discard Draft',\n    'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',\n    'pages_edit_switch_to_markdown_clean' => '(Clean Content)',\n    'pages_edit_switch_to_markdown_stable' => '(Stable Content)',\n    'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Set Changelog',\n    'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\\'ve made',\n    'pages_edit_enter_changelog' => 'Enter Changelog',\n    'pages_editor_switch_title' => 'Switch Editor',\n    'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',\n    'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',\n    'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',\n    'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',\n    'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\\'t persist across this change.',\n    'pages_save' => 'Save Page',\n    'pages_title' => 'Page Title',\n    'pages_name' => 'Page Name',\n    'pages_md_editor' => 'Editor',\n    'pages_md_preview' => 'Preview',\n    'pages_md_insert_image' => 'Insert Image',\n    'pages_md_insert_link' => 'Insert Entity Link',\n    'pages_md_insert_drawing' => 'Insert Drawing',\n    'pages_md_show_preview' => 'Show preview',\n    'pages_md_sync_scroll' => 'Sync preview scroll',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Unsaved Drawing Found',\n    'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',\n    'pages_not_in_chapter' => 'Page is not in a chapter',\n    'pages_move' => 'Move Page',\n    'pages_copy' => 'Copy Page',\n    'pages_copy_desination' => 'Copy Destination',\n    'pages_copy_success' => 'Page successfully copied',\n    'pages_permissions' => 'Page Permissions',\n    'pages_permissions_success' => 'Page permissions updated',\n    'pages_revision' => 'Revision',\n    'pages_revisions' => 'Page Revisions',\n    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',\n    'pages_revisions_named' => 'Page Revisions for :pageName',\n    'pages_revision_named' => 'Page Revision for :pageName',\n    'pages_revision_restored_from' => 'Restored from #:id; :summary',\n    'pages_revisions_created_by' => 'Created By',\n    'pages_revisions_date' => 'Revision Date',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Revision Number',\n    'pages_revisions_numbered' => 'Revision #:id',\n    'pages_revisions_numbered_changes' => 'Revision #:id Changes',\n    'pages_revisions_editor' => 'Editor Type',\n    'pages_revisions_changelog' => 'Changelog',\n    'pages_revisions_changes' => 'Changes',\n    'pages_revisions_current' => 'Current Version',\n    'pages_revisions_preview' => 'Preview',\n    'pages_revisions_restore' => 'Restore',\n    'pages_revisions_none' => 'This page has no revisions',\n    'pages_copy_link' => 'Copy Link',\n    'pages_edit_content_link' => 'Jump to section in editor',\n    'pages_pointer_enter_mode' => 'Enter section select mode',\n    'pages_pointer_label' => 'Page Section Options',\n    'pages_pointer_permalink' => 'Page Section Permalink',\n    'pages_pointer_include_tag' => 'Page Section Include Tag',\n    'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag',\n    'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink',\n    'pages_permissions_active' => 'Page Permissions Active',\n    'pages_initial_revision' => 'Initial publish',\n    'pages_references_update_revision' => 'System auto-update of internal links',\n    'pages_initial_name' => 'New Page',\n    'pages_editing_draft_notification' => 'You are currently editing a draft that was last saved :timeDiff.',\n    'pages_draft_edited_notification' => 'This page has been updated by since that time. It is recommended that you discard this draft.',\n    'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count users have started editing this page',\n        'start_b' => ':userName has started editing this page',\n        'time_a' => 'since the page was last updated',\n        'time_b' => 'in the last :minCount minutes',\n        'message' => ':start :time. Take care not to overwrite each other\\'s updates!',\n    ],\n    'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',\n    'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',\n    'pages_specific' => 'Specific Page',\n    'pages_is_template' => 'Page Template',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Toggle Sidebar',\n    'page_tags' => 'Page Tags',\n    'chapter_tags' => 'Chapter Tags',\n    'book_tags' => 'Book Tags',\n    'shelf_tags' => 'Shelf Tags',\n    'tag' => 'Tag',\n    'tags' =>  'Tags',\n    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',\n    'tag_name' =>  'Tag Name',\n    'tag_value' => 'Tag Value (Optional)',\n    'tags_explain' => \"Add some tags to better categorise your content. \\n You can assign a value to a tag for more in-depth organisation.\",\n    'tags_add' => 'Add another tag',\n    'tags_remove' => 'Remove this tag',\n    'tags_usages' => 'Total tag usages',\n    'tags_assigned_pages' => 'Assigned to Pages',\n    'tags_assigned_chapters' => 'Assigned to Chapters',\n    'tags_assigned_books' => 'Assigned to Books',\n    'tags_assigned_shelves' => 'Assigned to Shelves',\n    'tags_x_unique_values' => ':count unique values',\n    'tags_all_values' => 'All values',\n    'tags_view_tags' => 'View Tags',\n    'tags_view_existing_tags' => 'View existing tags',\n    'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',\n    'attachments' => 'Attachments',\n    'attachments_explain' => 'Upload some files or attach some links to display on your page. These are visible in the page sidebar.',\n    'attachments_explain_instant_save' => 'Changes here are saved instantly.',\n    'attachments_upload' => 'Upload File',\n    'attachments_link' => 'Attach Link',\n    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',\n    'attachments_set_link' => 'Set Link',\n    'attachments_delete' => 'Are you sure you want to delete this attachment?',\n    'attachments_dropzone' => 'Drop files here to upload',\n    'attachments_no_files' => 'No files have been uploaded',\n    'attachments_explain_link' => 'You can attach a link if you\\'d prefer not to upload a file. This can be a link to another page or a link to a file in the cloud.',\n    'attachments_link_name' => 'Link Name',\n    'attachment_link' => 'Attachment link',\n    'attachments_link_url' => 'Link to file',\n    'attachments_link_url_hint' => 'Url of site or file',\n    'attach' => 'Attach',\n    'attachments_insert_link' => 'Add Attachment Link to Page',\n    'attachments_edit_file' => 'Edit File',\n    'attachments_edit_file_name' => 'File Name',\n    'attachments_edit_drop_upload' => 'Drop files or click here to upload and overwrite',\n    'attachments_order_updated' => 'Attachment order updated',\n    'attachments_updated_success' => 'Attachment details updated',\n    'attachments_deleted' => 'Attachment deleted',\n    'attachments_file_uploaded' => 'File successfully uploaded',\n    'attachments_file_updated' => 'File successfully updated',\n    'attachments_link_attached' => 'Link successfully attached to page',\n    'templates' => 'Templates',\n    'templates_set_as_template' => 'Page is a template',\n    'templates_explain_set_as_template' => 'You can set this page as a template so its contents be utilized when creating other pages. Other users will be able to use this template if they have view permissions for this page.',\n    'templates_replace_content' => 'Replace page content',\n    'templates_append_content' => 'Append to page content',\n    'templates_prepend_content' => 'Prepend to page content',\n\n    // Profile View\n    'profile_user_for_x' => 'User for :time',\n    'profile_created_content' => 'Created Content',\n    'profile_not_created_pages' => ':userName has not created any pages',\n    'profile_not_created_chapters' => ':userName has not created any chapters',\n    'profile_not_created_books' => ':userName has not created any books',\n    'profile_not_created_shelves' => ':userName has not created any shelves',\n\n    // Comments\n    'comment' => 'Comment',\n    'comments' => 'Comments',\n    'comment_add' => 'Add Comment',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Leave a comment here',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Save Comment',\n    'comment_new' => 'New Comment',\n    'comment_created' => 'commented :createDiff',\n    'comment_updated' => 'Updated :updateDiff by :username',\n    'comment_updated_indicator' => 'Updated',\n    'comment_deleted_success' => 'Comment deleted',\n    'comment_created_success' => 'Comment added',\n    'comment_updated_success' => 'Comment updated',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Are you sure you want to delete this comment?',\n    'comment_in_reply_to' => 'In reply to :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',\n\n    // Revision\n    'revision_delete_confirm' => 'Are you sure you want to delete this revision?',\n    'revision_restore_confirm' => 'Are you sure you want to restore this revision? The current page contents will be replaced.',\n    'revision_cannot_delete_latest' => 'Cannot delete the latest revision.',\n\n    // Copy view\n    'copy_consider' => 'Please consider the below when copying content.',\n    'copy_consider_permissions' => 'Custom permission settings will not be copied.',\n    'copy_consider_owner' => 'You will become the owner of all copied content.',\n    'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',\n    'copy_consider_attachments' => 'Page attachments will not be copied.',\n    'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',\n\n    // Conversions\n    'convert_to_shelf' => 'Convert to Shelf',\n    'convert_to_shelf_contents_desc' => 'You can convert this book to a new shelf with the same contents. Chapters contained within this book will be converted to new books. If this book contains any pages, that are not in a chapter, this book will be renamed and contain such pages, and this book will become part of the new shelf.',\n    'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.',\n    'convert_book' => 'Convert Book',\n    'convert_book_confirm' => 'Are you sure you want to convert this book?',\n    'convert_undo_warning' => 'This cannot be as easily undone.',\n    'convert_to_book' => 'Convert to Book',\n    'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',\n    'convert_chapter' => 'Convert Chapter',\n    'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',\n\n    // References\n    'references' => 'References',\n    'references_none' => 'There are no tracked references to this item.',\n    'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',\n\n    // Watch Options\n    'watch' => 'Watch',\n    'watch_title_default' => 'Default Preferences',\n    'watch_desc_default' => 'Revert watching to just your default notification preferences.',\n    'watch_title_ignore' => 'Ignore',\n    'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.',\n    'watch_title_new' => 'New Pages',\n    'watch_desc_new' => 'Notify when any new page is created within this item.',\n    'watch_title_updates' => 'All Page Updates',\n    'watch_desc_updates' => 'Notify upon all new pages and page changes.',\n    'watch_desc_updates_page' => 'Notify upon all page changes.',\n    'watch_title_comments' => 'All Page Updates & Comments',\n    'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.',\n    'watch_desc_comments_page' => 'Notify upon page changes and new comments.',\n    'watch_change_default' => 'Change default notification preferences',\n    'watch_detail_ignore' => 'Ignoring notifications',\n    'watch_detail_new' => 'Watching for new pages',\n    'watch_detail_updates' => 'Watching new pages and updates',\n    'watch_detail_comments' => 'Watching new pages, updates & comments',\n    'watch_detail_parent_book' => 'Watching via parent book',\n    'watch_detail_parent_book_ignore' => 'Ignoring via parent book',\n    'watch_detail_parent_chapter' => 'Watching via parent chapter',\n    'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter',\n];\n"
  },
  {
    "path": "lang/sr/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Немате дозволу да приступите овој страни.',\n    'permissionJson' => 'Немате овлашћење да извршите ову акцију.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Корисник са е-мејл адресом :email већ постоји са другим приступним подацима.',\n    'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',\n    'email_already_confirmed' => 'Email has already been confirmed, Try logging in.',\n    'email_confirmation_invalid' => 'This confirmation token is not valid or has already been used, Please try registering again.',\n    'email_confirmation_expired' => 'The confirmation token has expired, A new confirmation email has been sent.',\n    'email_confirmation_awaiting' => 'The email address for the account in use needs to be confirmed',\n    'ldap_fail_anonymous' => 'LDAP access failed using anonymous bind',\n    'ldap_fail_authed' => 'LDAP access failed using given dn & password details',\n    'ldap_extension_not_installed' => 'LDAP PHP extension not installed',\n    'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed',\n    'saml_already_logged_in' => 'Already logged in',\n    'saml_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',\n    'saml_invalid_response_id' => 'The request from the external authentication system is not recognised by a process started by this application. Navigating back after a login could cause this issue.',\n    'saml_fail_authed' => 'Login using :system failed, system did not provide successful authorization',\n    'oidc_already_logged_in' => 'Already logged in',\n    'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',\n    'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',\n    'social_no_action_defined' => 'No action defined',\n    'social_login_bad_response' => \"Error received during :socialAccount login: \\n:error\",\n    'social_account_in_use' => 'This :socialAccount account is already in use, Try logging in via the :socialAccount option.',\n    'social_account_email_in_use' => 'The email :email is already in use. If you already have an account you can connect your :socialAccount account from your profile settings.',\n    'social_account_existing' => 'This :socialAccount is already attached to your profile.',\n    'social_account_already_used_existing' => 'This :socialAccount account is already used by another user.',\n    'social_account_not_used' => 'This :socialAccount account is not linked to any users. Please attach it in your profile settings. ',\n    'social_account_register_instructions' => 'If you do not yet have an account, You can register an account using the :socialAccount option.',\n    'social_driver_not_found' => 'Social driver not found',\n    'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.',\n    'invite_token_expired' => 'This invitation link has expired. You can instead try to reset your account password.',\n    'login_user_not_found' => 'A user for this action could not be found.',\n\n    // System\n    'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.',\n    'cannot_get_image_from_url' => 'Cannot get image from :url',\n    'cannot_create_thumbs' => 'The server cannot create thumbnails. Please check you have the GD PHP extension installed.',\n    'server_upload_limit' => 'The server does not allow uploads of this size. Please try a smaller file size.',\n    'server_post_limit' => 'The server cannot receive the provided amount of data. Try again with less data or a smaller file.',\n    'uploaded'  => 'The server does not allow uploads of this size. Please try a smaller file size.',\n\n    // Drawing & Images\n    'image_upload_error' => 'An error occurred uploading the image',\n    'image_upload_type_error' => 'The image type being uploaded is invalid',\n    'image_upload_replace_type' => 'Image file replacements must be of the same type',\n    'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.',\n    'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.',\n    'image_gallery_thumbnail_memory_limit' => 'Failed to create gallery thumbnails due to system resource limits.',\n    'drawing_data_not_found' => 'Drawing data could not be loaded. The drawing file might no longer exist or you may not have permission to access it.',\n\n    // Attachments\n    'attachment_not_found' => 'Attachment not found',\n    'attachment_upload_error' => 'An error occurred uploading the attachment file',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page',\n    'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content',\n    'page_custom_home_deletion' => 'Cannot delete a page while it is set as a homepage',\n\n    // Entities\n    'entity_not_found' => 'Entity not found',\n    'bookshelf_not_found' => 'Shelf not found',\n    'book_not_found' => 'Књига није пронађена',\n    'page_not_found' => 'Страница није пронађена',\n    'chapter_not_found' => 'Поглавље није пронађено',\n    'selected_book_not_found' => 'Одабрана књига није пронађена',\n    'selected_book_chapter_not_found' => 'The selected Book or Chapter was not found',\n    'guests_cannot_save_drafts' => 'Гости не могу сачувати нацрте',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Не можете обрисати јединог администратора',\n    'users_cannot_delete_guest' => 'Не можете обрисати госта',\n    'users_could_not_send_invite' => 'Could not create user since invite email failed to send',\n\n    // Roles\n    'role_cannot_be_edited' => 'Ова улога се не може мењати',\n    'role_system_cannot_be_deleted' => 'Ово је системска улога и не може се мењати',\n    'role_registration_default_cannot_delete' => 'This role cannot be deleted while set as the default registration role',\n    'role_cannot_remove_only_admin' => 'This user is the only user assigned to the administrator role. Assign the administrator role to another user before attempting to remove it here.',\n\n    // Comments\n    'comment_list' => 'An error occurred while fetching the comments.',\n    'cannot_add_comment_to_draft' => 'You cannot add comments to a draft.',\n    'comment_add' => 'An error occurred while adding / updating the comment.',\n    'comment_delete' => 'An error occurred while deleting the comment.',\n    'empty_comment' => 'Cannot add an empty comment.',\n\n    // Error pages\n    '404_page_not_found' => 'Page Not Found',\n    'sorry_page_not_found' => 'Sorry, The page you were looking for could not be found.',\n    'sorry_page_not_found_permission_warning' => 'If you expected this page to exist, you might not have permission to view it.',\n    'image_not_found' => 'Image Not Found',\n    'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',\n    'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',\n    'return_home' => 'Return to home',\n    'error_occurred' => 'Догодила се грешка',\n    'app_down' => ':appName is down right now',\n    'back_soon' => 'It will be back up soon.',\n\n    // Import\n    'import_zip_cant_read' => 'Could not read ZIP file.',\n    'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',\n    'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Import ZIP failed to validate with errors:',\n    'import_zip_failed_notification' => 'Failed to import ZIP file.',\n    'import_perms_books' => 'You are lacking the required permissions to create books.',\n    'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',\n    'import_perms_pages' => 'You are lacking the required permissions to create pages.',\n    'import_perms_images' => 'You are lacking the required permissions to create images.',\n    'import_perms_attachments' => 'You are lacking the required permission to create attachments.',\n\n    // API errors\n    'api_no_authorization_found' => 'No authorization token found on the request',\n    'api_bad_authorization_format' => 'An authorization token was found on the request but the format appeared incorrect',\n    'api_user_token_not_found' => 'No matching API token was found for the provided authorization token',\n    'api_incorrect_token_secret' => 'The secret provided for the given used API token is incorrect',\n    'api_user_no_api_permission' => 'The owner of the used API token does not have permission to make API calls',\n    'api_user_token_expired' => 'The authorization token used has expired',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Error thrown when sending a test email:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'The URL does not match the configured allowed SSR hosts',\n];\n"
  },
  {
    "path": "lang/sr/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Нови коментар на станици: :pageName',\n    'new_comment_intro' => 'Корисник је коментарисао на страници у :appName:',\n    'new_page_subject' => 'Нова страница: :pageName',\n    'new_page_intro' => 'Нова страница је креирана у :appName:',\n    'updated_page_subject' => 'Ажурирана страница: :pageName',\n    'updated_page_intro' => 'Страница је ажурирана у :appName:',\n    'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\\'t be sent notifications for further edits to this page by the same editor.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Назив странице:',\n    'detail_page_path' => 'Путања странице:',\n    'detail_commenter' => 'Commenter:',\n    'detail_comment' => 'Коментар:',\n    'detail_created_by' => 'Креирао/ла:',\n    'detail_updated_by' => 'Отпремио/ла:',\n\n    'action_view_comment' => 'Погледај коментар',\n    'action_view_page' => 'Погледај страницу',\n\n    'footer_reason' => 'This notification was sent to you because :link cover this type of activity for this item.',\n    'footer_reason_link' => 'your notification preferences',\n];\n"
  },
  {
    "path": "lang/sr/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Previous',\n    'next'     => 'Next &raquo;',\n\n];\n"
  },
  {
    "path": "lang/sr/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Passwords must be at least eight characters and match the confirmation.',\n    'user' => \"We can't find a user with that e-mail address.\",\n    'token' => 'The password reset token is invalid for this email address.',\n    'sent' => 'We have e-mailed your password reset link!',\n    'reset' => 'Your password has been reset!',\n\n];\n"
  },
  {
    "path": "lang/sr/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'My Account',\n\n    'shortcuts' => 'Shortcuts',\n    'shortcuts_interface' => 'UI Shortcut Preferences',\n    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',\n    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',\n    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',\n    'shortcuts_section_navigation' => 'Navigation',\n    'shortcuts_section_actions' => 'Common Actions',\n    'shortcuts_save' => 'Save Shortcuts',\n    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing \"?\" which will highlight the available shortcuts for actions currently visible on the screen.',\n    'shortcuts_update_success' => 'Shortcut preferences have been updated!',\n    'shortcuts_overview_desc' => 'Manage keyboard shortcuts you can use to navigate the system user interface.',\n\n    'notifications' => 'Notification Preferences',\n    'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.',\n    'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own',\n    'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Notify upon replies to my comments',\n    'notifications_save' => 'Save Preferences',\n    'notifications_update_success' => 'Notification preferences have been updated!',\n    'notifications_watched' => 'Watched & Ignored Items',\n    'notifications_watched_desc' => 'Below are the items that have custom watch preferences applied. To update your preferences for these, view the item then find the watch options in the sidebar.',\n\n    'auth' => 'Access & Security',\n    'auth_change_password' => 'Change Password',\n    'auth_change_password_desc' => 'Change the password you use to log-in to the application. This must be at least 8 characters long.',\n    'auth_change_password_success' => 'Password has been updated!',\n\n    'profile' => 'Profile Details',\n    'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.',\n    'profile_view_public' => 'View Public Profile',\n    'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.',\n    'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.',\n    'profile_email_no_permission' => 'Unfortunately you don\\'t have permission to change your email address. If you want to change this, you\\'d need to ask an administrator to change this for you.',\n    'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.',\n    'profile_admin_options' => 'Administrator Options',\n    'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the \"Settings > Users\" area of the application.',\n\n    'delete_account' => 'Delete Account',\n    'delete_my_account' => 'Delete My Account',\n    'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\\'ve created, such as created pages and uploaded images, will remain.',\n    'delete_my_account_warning' => 'Are you sure you want to delete your account?',\n];\n"
  },
  {
    "path": "lang/sr/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Подешавања',\n    'settings_save' => 'Сачувај подешавања',\n    'system_version' => 'Верзија система',\n    'categories' => 'Категорије',\n\n    // App Settings\n    'app_customization' => 'Прилгођавање',\n    'app_features_security' => 'Својства и сигурност',\n    'app_name' => 'Назив апликације',\n    'app_name_desc' => 'Ово име се приказује у заглављу и у свим системским порукама е-поште.',\n    'app_name_header' => 'Прикажи назив у заглављу',\n    'app_public_access' => 'Javni pristup',\n    'app_public_access_desc' => 'Омогућавање ове опције ће омогућити посетиоцима, који нису пријављени, да приступе садржају у вашој Боокстак инстанци.',\n    'app_public_access_desc_guest' => 'Приступ за јавне посетиоце може се контролисати преко корисника „Гост“.',\n    'app_public_access_toggle' => 'Дозволи јавни приступ',\n    'app_public_viewing' => 'Дозволити јавно гледање?',\n    'app_secure_images' => 'Веће безбедност отпремања слика',\n    'app_secure_images_toggle' => 'Омогућите већу безбедност отпремања слика',\n    'app_secure_images_desc' => 'Из разлога перформанси, све слике су јавне. Ова опција додаје насумичан низ који је тешко погодити испред Урл-ова слике. Уверите се да индекси директоријума нису омогућени да бисте спречили лак приступ.',\n    'app_default_editor' => 'Подразумевани уређивач страница',\n    'app_default_editor_desc' => 'Изаберите који уређивач ће се подразумевано користити приликом уређивања нових страница. Ово се може заменити на нивоу странице где дозволе дозвољавају.',\n    'app_custom_html' => 'Прилагођени ХТМЛ садржај заглавља',\n    'app_custom_html_desc' => 'Сваки садржај који је додат овде биће уметнут у дно <head> одељка сваке странице. Ово је згодно за надјачавање стилова или додавање кода за анализу.',\n    'app_custom_html_disabled_notice' => 'Прилагођени садржај заглавља ХТМЛ-а је онемогућен на овој страници са подешавањима како би се осигурало да се све неоправдане промене могу поништити.',\n    'app_logo' => 'Логотип апликације',\n    'app_logo_desc' => 'Ово се користи у траци заглавља апликације, између осталих области. Висина ове слике треба да буде 86 пиксела. Велике слике ће бити смањене.',\n    'app_icon' => 'Икона апликације',\n    'app_icon_desc' => 'Ова икона се користи за картице претраживача и иконе пречица. Ово би требало да буде квадратна ПНГ слика величине 256 пиксела.',\n    'app_homepage' => 'Почетна страница апликације',\n    'app_homepage_desc' => 'Изаберите приказ који ће се приказати на почетној страници уместо подразумеваног приказа. Дозволе за страницу се занемарују за изабране странице.',\n    'app_homepage_select' => 'Изаберите страницу',\n    'app_footer_links' => 'Везе у подножју',\n    'app_footer_links_desc' => 'Додајте везе које ће се приказати у подножју сајта. Они ће бити приказани на дну већине страница, укључујући и оне које не захтевају пријаву. Можете користити ознаку \"trans::<key>\" да бисте користили системски дефинисане преводе. На пример: Коришћење \"trans::common.privacy_policy\"ће обезбедити преведени текст „Политика приватности“, а \"trans::common.terms_of_service\" ће обезбедити преведени текст „Услови коришћења услуге“.',\n    'app_footer_links_label' => 'Ознака линка',\n    'app_footer_links_url' => 'УРЛ линка',\n    'app_footer_links_add' => 'Додај везу у подножју',\n    'app_disable_comments' => 'Онемогући коментаре',\n    'app_disable_comments_toggle' => 'Онемогући коментаре',\n    'app_disable_comments_desc' => 'Онемогућава коментаре на свим страницама у апликацији. <br> Постојећи коментари нису приказани.',\n\n    // Color settings\n    'color_scheme' => 'Шема боја апликације',\n    'color_scheme_desc' => 'Подесите боје које ће се користити у корисничком интерфејсу апликације. Боје се могу засебно конфигурисати за тамне и светле режиме како би најбоље одговарале теми и осигурале читљивост.',\n    'ui_colors_desc' => 'Подесите примарну боју апликације и подразумевану боју везе. Примарна боја се углавном користи за банер заглавља, дугмад и декорацију интерфејса. Подразумевана боја везе се користи за везе и радње засноване на тексту, како унутар писаног садржаја, тако и у интерфејсу апликације.',\n    'app_color' => 'Примарна боја',\n    'link_color' => 'Подразумевана боја везе',\n    'content_colors_desc' => 'Подесите боје за све елементе у хијерархији организације страница. За читљивост се препоручује одабир боја сличне осветљености као и подразумеване боје.',\n    'bookshelf_color' => 'Боја полице',\n    'book_color' => 'Боја књиге',\n    'chapter_color' => 'Боја поглавља',\n    'page_color' => 'Боја странице',\n    'page_draft_color' => 'Боја нацрта странице',\n\n    // Registration Settings\n    'reg_settings' => 'Регистрација',\n    'reg_enable' => 'Дозволи регистрацију',\n    'reg_enable_toggle' => 'Омогући регистрацију',\n    'reg_enable_desc' => 'Када је регистрација омогућена, корисник ће моћи да се пријави као корисник апликације. Након регистрације добијају јединствену, подразумевану корисничку улогу.',\n    'reg_default_role' => 'Подразумевана корисничка улога након регистрације',\n    'reg_enable_external_warning' => 'Горња опција се занемарује док је активна екстерна ЛДАП или САМЛ аутентификација. Кориснички налози за непостојеће чланове биће аутоматски креирани ако је аутентификација, против спољашњег система који се користи, успешна.',\n    'reg_email_confirmation' => 'Потврђивање е-поште',\n    'reg_email_confirmation_toggle' => 'Захтевајте потврду е-поштом',\n    'reg_confirm_email_desc' => 'Ако се користи ограничење домена, биће потребна потврда путем е-поште и ова опција ће бити занемарена.',\n    'reg_confirm_restrict_domain' => 'Ограничени домени',\n    'reg_confirm_restrict_domain_desc' => 'Унесите листу домена е-поште одвојене зарезима на које желите да ограничите регистрацију. Корисницима ће бити послата е-порука да потврде своју адресу пре него што им буде дозвољено да комуницирају са апликацијом. <br> Имајте на уму да ће корисници моћи да промене своје адресе е-поште након успешне регистрације.',\n    'reg_confirm_restrict_domain_placeholder' => 'Нема постављених ограничења',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Одржавање',\n    'maint_image_cleanup' => 'Чишћење слика',\n    'maint_image_cleanup_desc' => 'Скенира садржај странице и ревидира садржај да би проверио које слике и цртежи су тренутно у употреби и које су слике сувишне. Уверите се да сте направили пуну базу података и резервну копију слике пре него што ово покренете.',\n    'maint_delete_images_only_in_revisions' => 'Такође избришите слике које постоје само у старим ревизијама странице',\n    'maint_image_cleanup_run' => 'Покрени чишћење',\n    'maint_image_cleanup_warning' => ':count пронађене су потенцијално неискоришћене слике. Да ли сте сигурни да желите да избришете ове слике?',\n    'maint_image_cleanup_success' => ':count potentially unused images found and deleted!',\n    'maint_image_cleanup_nothing_found' => 'No unused images found, Nothing deleted!',\n    'maint_send_test_email' => 'Send a Test Email',\n    'maint_send_test_email_desc' => 'This sends a test email to your email address specified in your profile.',\n    'maint_send_test_email_run' => 'Send test email',\n    'maint_send_test_email_success' => 'Email sent to :address',\n    'maint_send_test_email_mail_subject' => 'Test Email',\n    'maint_send_test_email_mail_greeting' => 'Email delivery seems to work!',\n    'maint_send_test_email_mail_text' => 'Congratulations! As you received this email notification, your email settings seem to be configured properly.',\n    'maint_recycle_bin_desc' => 'Deleted shelves, books, chapters & pages are sent to the recycle bin so they can be restored or permanently deleted. Older items in the recycle bin may be automatically removed after a while depending on system configuration.',\n    'maint_recycle_bin_open' => 'Open Recycle Bin',\n    'maint_regen_references' => 'Regenerate References',\n    'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',\n    'maint_regen_references_success' => 'Reference index has been regenerated!',\n    'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Recycle Bin',\n    'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',\n    'recycle_bin_deleted_item' => 'Deleted Item',\n    'recycle_bin_deleted_parent' => 'Parent',\n    'recycle_bin_deleted_by' => 'Deleted By',\n    'recycle_bin_deleted_at' => 'Deletion Time',\n    'recycle_bin_permanently_delete' => 'Permanently Delete',\n    'recycle_bin_restore' => 'Restore',\n    'recycle_bin_contents_empty' => 'The recycle bin is currently empty',\n    'recycle_bin_empty' => 'Empty Recycle Bin',\n    'recycle_bin_empty_confirm' => 'This will permanently destroy all items in the recycle bin including content contained within each item. Are you sure you want to empty the recycle bin?',\n    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?',\n    'recycle_bin_destroy_list' => 'Items to be Destroyed',\n    'recycle_bin_restore_list' => 'Items to be Restored',\n    'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.',\n    'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.',\n    'recycle_bin_restore_parent' => 'Restore Parent',\n    'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',\n    'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',\n\n    // Audit Log\n    'audit' => 'Audit Log',\n    'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',\n    'audit_event_filter' => 'Event Filter',\n    'audit_event_filter_no_filter' => 'No Filter',\n    'audit_deleted_item' => 'Избрисана ставка',\n    'audit_deleted_item_name' => 'Name: :name',\n    'audit_table_user' => 'Корисник',\n    'audit_table_event' => 'Догађај',\n    'audit_table_related' => 'Related Item or Detail',\n    'audit_table_ip' => 'ИП адреса',\n    'audit_table_date' => 'Датум активности',\n    'audit_date_from' => 'Date Range From',\n    'audit_date_to' => 'Date Range To',\n\n    // Role Settings\n    'roles' => 'Улоге',\n    'role_user_roles' => 'User Roles',\n    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',\n    'roles_x_users_assigned' => ':count user assigned|:count users assigned',\n    'roles_x_permissions_provided' => ':count permission|:count permissions',\n    'roles_assigned_users' => 'Assigned Users',\n    'roles_permissions_provided' => 'Provided Permissions',\n    'role_create' => 'Create New Role',\n    'role_delete' => 'Delete Role',\n    'role_delete_confirm' => 'Ово ће избрисати улогу са именом \\':roleName\\'.',\n    'role_delete_users_assigned' => 'Ова улога има :userCount корисника који су јој додељени. Ако желите да мигрирате кориснике са ове улоге, изаберите нову улогу испод.',\n    'role_delete_no_migration' => \"Немојте мигрирати кориснике\",\n    'role_delete_sure' => 'Да ли сте сигурни да желите да избришете ову улогу?',\n    'role_edit' => 'Уреди улогу',\n    'role_details' => 'Детаљи улоге',\n    'role_name' => 'Назив улоге',\n    'role_desc' => 'Кратак опис улоге',\n    'role_mfa_enforced' => 'Захтева вишефакторску аутентификацију',\n    'role_external_auth_id' => 'External Authentication IDs',\n    'role_system' => 'System Permissions',\n    'role_manage_users' => 'Manage users',\n    'role_manage_roles' => 'Manage roles & role permissions',\n    'role_manage_entity_permissions' => 'Manage all book, chapter & page permissions',\n    'role_manage_own_entity_permissions' => 'Manage permissions on own book, chapter & pages',\n    'role_manage_page_templates' => 'Manage page templates',\n    'role_access_api' => 'Access system API',\n    'role_manage_settings' => 'Manage app settings',\n    'role_export_content' => 'Export content',\n    'role_import_content' => 'Import content',\n    'role_editor_change' => 'Change page editor',\n    'role_notifications' => 'Receive & manage notifications',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Asset Permissions',\n    'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',\n    'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',\n    'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.',\n    'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'All',\n    'role_own' => 'Own',\n    'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',\n    'role_save' => 'Save Role',\n    'role_users' => 'Users in this role',\n    'role_users_none' => 'No users are currently assigned to this role',\n\n    // Users\n    'users' => 'Users',\n    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',\n    'user_profile' => 'User Profile',\n    'users_add_new' => 'Add New User',\n    'users_search' => 'Search Users',\n    'users_latest_activity' => 'Latest Activity',\n    'users_details' => 'User Details',\n    'users_details_desc' => 'Set a display name and an email address for this user. The email address will be used for logging into the application.',\n    'users_details_desc_no_email' => 'Set a display name for this user so others can recognise them.',\n    'users_role' => 'User Roles',\n    'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.',\n    'users_password' => 'User Password',\n    'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',\n    'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.',\n    'users_send_invite_option' => 'Send user invite email',\n    'users_external_auth_id' => 'External Authentication ID',\n    'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.',\n    'users_password_warning' => 'Only fill the below if you would like to change the password for this user.',\n    'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.',\n    'users_delete' => 'Delete User',\n    'users_delete_named' => 'Delete user :userName',\n    'users_delete_warning' => 'This will fully delete this user with the name \\':userName\\' from the system.',\n    'users_delete_confirm' => 'Are you sure you want to delete this user?',\n    'users_migrate_ownership' => 'Migrate Ownership',\n    'users_migrate_ownership_desc' => 'Select a user here if you want another user to become the owner of all items currently owned by this user.',\n    'users_none_selected' => 'No user selected',\n    'users_edit' => 'Edit User',\n    'users_edit_profile' => 'Edit Profile',\n    'users_avatar' => 'User Avatar',\n    'users_avatar_desc' => 'Select an image to represent this user. This should be approx 256px square.',\n    'users_preferred_language' => 'Preferred Language',\n    'users_preferred_language_desc' => 'This option will change the language used for the user-interface of the application. This will not affect any user-created content.',\n    'users_social_accounts' => 'Social Accounts',\n    'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.',\n    'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not revoke previously authorized access. Revoke access from your profile settings on the connected social account.',\n    'users_social_connect' => 'Connect Account',\n    'users_social_disconnect' => 'Disconnect Account',\n    'users_social_status_connected' => 'Connected',\n    'users_social_status_disconnected' => 'Disconnected',\n    'users_social_connected' => ':socialAccount account was successfully attached to your profile.',\n    'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.',\n    'users_api_tokens' => 'API Tokens',\n    'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.',\n    'users_api_tokens_none' => 'No API tokens have been created for this user',\n    'users_api_tokens_create' => 'Create Token',\n    'users_api_tokens_expires' => 'Expires',\n    'users_api_tokens_docs' => 'API Documentation',\n    'users_mfa' => 'Multi-Factor Authentication',\n    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'users_mfa_x_methods' => ':count method configured|:count methods configured',\n    'users_mfa_configure' => 'Configure Methods',\n\n    // API Tokens\n    'user_api_token_create' => 'Create API Token',\n    'user_api_token_name' => 'Name',\n    'user_api_token_name_desc' => 'Give your token a readable name as a future reminder of its intended purpose.',\n    'user_api_token_expiry' => 'Expiry Date',\n    'user_api_token_expiry_desc' => 'Set a date at which this token expires. After this date, requests made using this token will no longer work. Leaving this field blank will set an expiry 100 years into the future.',\n    'user_api_token_create_secret_message' => 'Immediately after creating this token a \"Token ID\" & \"Token Secret\" will be generated and displayed. The secret will only be shown a single time so be sure to copy the value to somewhere safe and secure before proceeding.',\n    'user_api_token' => 'API Token',\n    'user_api_token_id' => 'Token ID',\n    'user_api_token_id_desc' => 'This is a non-editable system generated identifier for this token which will need to be provided in API requests.',\n    'user_api_token_secret' => 'Token Secret',\n    'user_api_token_secret_desc' => 'This is a system generated secret for this token which will need to be provided in API requests. This will only be displayed this one time so copy this value to somewhere safe and secure.',\n    'user_api_token_created' => 'Token created :timeAgo',\n    'user_api_token_updated' => 'Token updated :timeAgo',\n    'user_api_token_delete' => 'Delete Token',\n    'user_api_token_delete_warning' => 'This will fully delete this API token with the name \\':tokenName\\' from the system.',\n    'user_api_token_delete_confirm' => 'Are you sure you want to delete this API token?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',\n    'webhooks_x_trigger_events' => ':count trigger event|:count trigger events',\n    'webhooks_create' => 'Create New Webhook',\n    'webhooks_none_created' => 'No webhooks have yet been created.',\n    'webhooks_edit' => 'Edit Webhook',\n    'webhooks_save' => 'Save Webhook',\n    'webhooks_details' => 'Webhook Details',\n    'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',\n    'webhooks_events' => 'Webhook Events',\n    'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',\n    'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\\'t expose confidential content.',\n    'webhooks_events_all' => 'All system events',\n    'webhooks_name' => 'Webhook Name',\n    'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',\n    'webhooks_endpoint' => 'Webhook Endpoint',\n    'webhooks_active' => 'Webhook Active',\n    'webhook_events_table_header' => 'Events',\n    'webhooks_delete' => 'Delete Webhook',\n    'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \\':webhookName\\', from the system.',\n    'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?',\n    'webhooks_format_example' => 'Webhook Format Example',\n    'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The \"related_item\" and \"url\" properties are optional and will depend on the type of event triggered.',\n    'webhooks_status' => 'Webhook Status',\n    'webhooks_last_called' => 'Last Called:',\n    'webhooks_last_errored' => 'Last Errored:',\n    'webhooks_last_error_message' => 'Last Error Message:',\n\n    // Licensing\n    'licenses' => 'Licenses',\n    'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.',\n    'licenses_bookstack' => 'BookStack License',\n    'licenses_php' => 'PHP Library Licenses',\n    'licenses_js' => 'JavaScript Library Licenses',\n    'licenses_other' => 'Other Licenses',\n    'license_details' => 'License Details',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/sr/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => 'The :attribute must be accepted.',\n    'active_url'           => 'The :attribute is not a valid URL.',\n    'after'                => 'The :attribute must be a date after :date.',\n    'alpha'                => 'The :attribute may only contain letters.',\n    'alpha_dash'           => 'The :attribute may only contain letters, numbers, dashes and underscores.',\n    'alpha_num'            => 'The :attribute may only contain letters and numbers.',\n    'array'                => ':attribute мора бити низ.',\n    'backup_codes'         => 'The provided code is not valid or has already been used.',\n    'before'               => 'The :attribute must be a date before :date.',\n    'between'              => [\n        'numeric' => 'The :attribute must be between :min and :max.',\n        'file'    => 'The :attribute must be between :min and :max kilobytes.',\n        'string'  => 'The :attribute must be between :min and :max characters.',\n        'array'   => 'The :attribute must have between :min and :max items.',\n    ],\n    'boolean'              => 'The :attribute field must be true or false.',\n    'confirmed'            => 'The :attribute confirmation does not match.',\n    'date'                 => 'The :attribute is not a valid date.',\n    'date_format'          => 'The :attribute does not match the format :format.',\n    'different'            => 'The :attribute and :other must be different.',\n    'digits'               => 'The :attribute must be :digits digits.',\n    'digits_between'       => 'The :attribute must be between :min and :max digits.',\n    'email'                => 'The :attribute must be a valid email address.',\n    'ends_with' => 'The :attribute must end with one of the following: :values',\n    'file'                 => 'The :attribute must be provided as a valid file.',\n    'filled'               => 'The :attribute field is required.',\n    'gt'                   => [\n        'numeric' => 'The :attribute must be greater than :value.',\n        'file'    => 'The :attribute must be greater than :value kilobytes.',\n        'string'  => 'The :attribute must be greater than :value characters.',\n        'array'   => 'The :attribute must have more than :value items.',\n    ],\n    'gte'                  => [\n        'numeric' => 'The :attribute must be greater than or equal :value.',\n        'file'    => 'The :attribute must be greater than or equal :value kilobytes.',\n        'string'  => 'The :attribute must be greater than or equal :value characters.',\n        'array'   => 'The :attribute must have :value items or more.',\n    ],\n    'exists'               => 'The selected :attribute is invalid.',\n    'image'                => 'The :attribute must be an image.',\n    'image_extension'      => 'The :attribute must have a valid & supported image extension.',\n    'in'                   => 'The selected :attribute is invalid.',\n    'integer'              => 'The :attribute must be an integer.',\n    'ip'                   => 'The :attribute must be a valid IP address.',\n    'ipv4'                 => 'The :attribute must be a valid IPv4 address.',\n    'ipv6'                 => 'The :attribute must be a valid IPv6 address.',\n    'json'                 => 'The :attribute must be a valid JSON string.',\n    'lt'                   => [\n        'numeric' => 'The :attribute must be less than :value.',\n        'file'    => 'The :attribute must be less than :value kilobytes.',\n        'string'  => 'The :attribute must be less than :value characters.',\n        'array'   => 'The :attribute must have less than :value items.',\n    ],\n    'lte'                  => [\n        'numeric' => 'The :attribute must be less than or equal :value.',\n        'file'    => 'The :attribute must be less than or equal :value kilobytes.',\n        'string'  => 'The :attribute must be less than or equal :value characters.',\n        'array'   => 'The :attribute must not have more than :value items.',\n    ],\n    'max'                  => [\n        'numeric' => 'The :attribute may not be greater than :max.',\n        'file'    => 'The :attribute may not be greater than :max kilobytes.',\n        'string'  => 'The :attribute may not be greater than :max characters.',\n        'array'   => 'The :attribute may not have more than :max items.',\n    ],\n    'mimes'                => 'The :attribute must be a file of type: :values.',\n    'min'                  => [\n        'numeric' => 'The :attribute must be at least :min.',\n        'file'    => 'The :attribute must be at least :min kilobytes.',\n        'string'  => 'The :attribute must be at least :min characters.',\n        'array'   => 'The :attribute must have at least :min items.',\n    ],\n    'not_in'               => 'The selected :attribute is invalid.',\n    'not_regex'            => 'The :attribute format is invalid.',\n    'numeric'              => 'The :attribute must be a number.',\n    'regex'                => 'The :attribute format is invalid.',\n    'required'             => 'The :attribute field is required.',\n    'required_if'          => 'The :attribute field is required when :other is :value.',\n    'required_with'        => 'The :attribute field is required when :values is present.',\n    'required_with_all'    => 'The :attribute field is required when :values is present.',\n    'required_without'     => 'The :attribute field is required when :values is not present.',\n    'required_without_all' => 'The :attribute field is required when none of :values are present.',\n    'same'                 => 'The :attribute and :other must match.',\n    'safe_url'             => 'The provided link may not be safe.',\n    'size'                 => [\n        'numeric' => 'The :attribute must be :size.',\n        'file'    => 'The :attribute must be :size kilobytes.',\n        'string'  => 'The :attribute must be :size characters.',\n        'array'   => 'The :attribute must contain :size items.',\n    ],\n    'string'               => 'The :attribute must be a string.',\n    'timezone'             => 'The :attribute must be a valid zone.',\n    'totp'                 => 'The provided code is not valid or has expired.',\n    'unique'               => 'The :attribute has already been taken.',\n    'url'                  => 'The :attribute format is invalid.',\n    'uploaded'             => 'The file could not be uploaded. The server may not accept files of this size.',\n\n    'zip_file' => 'The :attribute needs to reference a file within the ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',\n    'zip_model_expected' => 'Data object expected but \":type\" found.',\n    'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Password confirmation required',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/sv/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'skapade sidan',\n    'page_create_notification'    => 'Sidan har skapats',\n    'page_update'                 => 'uppdaterade sidan',\n    'page_update_notification'    => 'Sidan har uppdaterats',\n    'page_delete'                 => 'tog bort sidan',\n    'page_delete_notification'    => 'Sidan har tagits bort',\n    'page_restore'                => 'återställde sidan',\n    'page_restore_notification'   => 'Sidan har återställts',\n    'page_move'                   => 'flyttade sidan',\n    'page_move_notification'      => 'Sidan har flyttats',\n\n    // Chapters\n    'chapter_create'              => 'skapade kapitlet',\n    'chapter_create_notification' => 'Kapitlet har skapats',\n    'chapter_update'              => 'uppdaterade kapitlet',\n    'chapter_update_notification' => 'Kapitlet har uppdaterats',\n    'chapter_delete'              => 'tog bort kapitlet',\n    'chapter_delete_notification' => 'Kapitlet har tagits bort',\n    'chapter_move'                => 'flyttade kapitlet',\n    'chapter_move_notification' => 'Kapitlet har flyttats',\n\n    // Books\n    'book_create'                 => 'skapade boken',\n    'book_create_notification'    => 'Boken har skapats',\n    'book_create_from_chapter'              => 'konverterade kapitel till bok',\n    'book_create_from_chapter_notification' => 'Kapitlet har konverterats till en bok',\n    'book_update'                 => 'uppdaterade boken',\n    'book_update_notification'    => 'Boken har uppdaterats',\n    'book_delete'                 => 'tog bort boken',\n    'book_delete_notification'    => 'Boken har tagits bort',\n    'book_sort'                   => 'sorterade boken',\n    'book_sort_notification'      => 'Boken har sorterats om',\n\n    // Bookshelves\n    'bookshelf_create'            => 'skapade hylla',\n    'bookshelf_create_notification'    => 'Hyllan har skapats',\n    'bookshelf_create_from_book'    => 'konverterade bok till hylla',\n    'bookshelf_create_from_book_notification'    => 'Boken har konverterats till en hylla',\n    'bookshelf_update'                 => 'uppdaterade hyllan',\n    'bookshelf_update_notification'    => 'Hyllan har uppdaterats',\n    'bookshelf_delete'                 => 'raderade hyllan',\n    'bookshelf_delete_notification'    => 'Hyllan har tagits bort',\n\n    // Revisions\n    'revision_restore' => 'återställde version',\n    'revision_delete' => 'tog bort version',\n    'revision_delete_notification' => 'Versionen har tagits bort',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" har lagts till i dina favoriter',\n    'favourite_remove_notification' => '\":name\" har tagits bort från dina favoriter',\n\n    // Watching\n    'watch_update_level_notification' => 'Inställningarna för bevakning har uppdaterats',\n\n    // Auth\n    'auth_login' => 'loggade in',\n    'auth_register' => 'registrerad som ny användare',\n    'auth_password_reset_request' => 'begärd återställning av användarlösenord',\n    'auth_password_reset_update' => 'återställa användarens lösenord',\n    'mfa_setup_method' => 'konfigurerad MFA metod',\n    'mfa_setup_method_notification' => 'Multifaktor-metod har konfigurerats',\n    'mfa_remove_method' => 'tog bort MFA metod',\n    'mfa_remove_method_notification' => 'Multifaktor-metod har tagits bort',\n\n    // Settings\n    'settings_update' => 'uppdaterade inställningar',\n    'settings_update_notification' => 'Inställningarna har uppdaterats',\n    'maintenance_action_run' => 'körde underhållsåtgärder',\n\n    // Webhooks\n    'webhook_create' => 'skapade webhook',\n    'webhook_create_notification' => 'Webhook har skapats',\n    'webhook_update' => 'uppdaterade webhook',\n    'webhook_update_notification' => 'Webhook har uppdaterats',\n    'webhook_delete' => 'raderade webhook',\n    'webhook_delete_notification' => 'Webhook har tagits bort',\n\n    // Imports\n    'import_create' => 'import skapades',\n    'import_create_notification' => 'Import har laddats upp',\n    'import_run' => 'import uppdaterad',\n    'import_run_notification' => 'Innehållet har importerats',\n    'import_delete' => 'import borttagen',\n    'import_delete_notification' => 'Importen har tagits bort',\n\n    // Users\n    'user_create' => 'skapade användare',\n    'user_create_notification' => 'Användare skapades',\n    'user_update' => 'uppdaterad användare',\n    'user_update_notification' => 'Användaren har uppdaterats',\n    'user_delete' => 'raderad användare',\n    'user_delete_notification' => 'Användaren har tagits bort',\n\n    // API Tokens\n    'api_token_create' => 'skapade API-token',\n    'api_token_create_notification' => 'API-token har skapats',\n    'api_token_update' => 'uppdaterad API-token',\n    'api_token_update_notification' => 'API-token har uppdaterats',\n    'api_token_delete' => 'raderad API-token',\n    'api_token_delete_notification' => 'API-token har tagits bort',\n\n    // Roles\n    'role_create' => 'skapad roll',\n    'role_create_notification' => 'Rollen har skapats',\n    'role_update' => 'uppdaterad roll',\n    'role_update_notification' => 'Rollen har uppdaterats',\n    'role_delete' => 'raderad roll',\n    'role_delete_notification' => 'Rollen har tagits bort',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'tömd papperskorg',\n    'recycle_bin_restore' => 'återställd från papperskorgen',\n    'recycle_bin_destroy' => 'borttagen från papperskorgen',\n\n    // Comments\n    'commented_on'                => 'kommenterade',\n    'comment_create'              => 'lagt till kommentar',\n    'comment_update'              => 'uppdaterad kommentar',\n    'comment_delete'              => 'raderad kommentar',\n\n    // Sort Rules\n    'sort_rule_create' => 'sorteringsregel skapad',\n    'sort_rule_create_notification' => 'Sorteringsregel har skapats',\n    'sort_rule_update' => 'sorteringsregel uppdaterad',\n    'sort_rule_update_notification' => 'Sorteringsregel har uppdaterats',\n    'sort_rule_delete' => 'sorteringsregel borttagen',\n    'sort_rule_delete_notification' => 'Sorteringsregel har tagits bort',\n\n    // Other\n    'permissions_update'          => 'uppdaterade behörigheter',\n];\n"
  },
  {
    "path": "lang/sv/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Uppgifterna stämmer inte överens med våra register.',\n    'throttle' => 'För många inloggningsförsök. Prova igen om :seconds sekunder.',\n\n    // Login & Register\n    'sign_up' => 'Skapa konto',\n    'log_in' => 'Logga in',\n    'log_in_with' => 'Logga in med :socialDriver',\n    'sign_up_with' => 'Registera dig med :socialDriver',\n    'logout' => 'Logga ut',\n\n    'name' => 'Namn',\n    'username' => 'Användarnamn',\n    'email' => 'E-post',\n    'password' => 'Lösenord',\n    'password_confirm' => 'Bekräfta lösenord',\n    'password_hint' => 'Måste vara minst 8 tecken',\n    'forgot_password' => 'Glömt lösenord?',\n    'remember_me' => 'Kom ihåg mig',\n    'ldap_email_hint' => 'Vänligen ange en e-postadress att använda till kontot.',\n    'create_account' => 'Skapa konto',\n    'already_have_account' => 'Har du redan ett konto?',\n    'dont_have_account' => 'Har du ingen användare?',\n    'social_login' => 'Logga in genom socialt medie',\n    'social_registration' => 'Registrera dig genom socialt media',\n    'social_registration_text' => 'Registrera dig och logga in genom en annan tjänst.',\n\n    'register_thanks' => 'Tack för din registrering!',\n    'register_confirm' => 'Vänligen kontrollera din mail och klicka på bekräftelselänken för att få tillgång till :appName.',\n    'registrations_disabled' => 'Registrering är för närvarande avstängd',\n    'registration_email_domain_invalid' => 'Den e-postadressen har inte tillgång till den här applikationen',\n    'register_success' => 'Tack för din registrering! Du är nu registerad och inloggad.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Försöker Logga In',\n    'auto_init_starting_desc' => 'Vi kontaktar ditt autentiseringssystem för att starta inloggningsprocessen. Om inget händer efter 5 sekunder kan du prova att klicka på länken nedan.',\n    'auto_init_start_link' => 'Fortsätt med autentisering',\n\n    // Password Reset\n    'reset_password' => 'Återställ lösenord',\n    'reset_password_send_instructions' => 'Ange din e-postadress nedan så skickar vi ett mail med en länk för att återställa ditt lösenord.',\n    'reset_password_send_button' => 'Skicka återställningslänk',\n    'reset_password_sent' => 'En länk för återställning av lösenord kommer att skickas till :email om den e-postadressen finns i systemet.',\n    'reset_password_success' => 'Ditt lösenord har återställts.',\n    'email_reset_subject' => 'Återställ ditt lösenord till :appName',\n    'email_reset_text' => 'Du får detta mail eftersom vi fått en begäran om att återställa lösenordet till ditt konto.',\n    'email_reset_not_requested' => 'Om du inte begärt att få ditt lösenord återställt behöver du inte göra någonting',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Bekräfta din e-post på :appName',\n    'email_confirm_greeting' => 'Tack för att du gått med i :appName!',\n    'email_confirm_text' => 'Vänligen bekräfta din e-postadress genom att klicka på knappen nedan:',\n    'email_confirm_action' => 'Bekräfta e-post',\n    'email_confirm_send_error' => 'E-posten behöver bekräftas men systemet kan inte skicka mail. Kontakta adminstratören för att kontrollera att allt är konfigurerat korrekt.',\n    'email_confirm_success' => 'Din e-postadress har bekräftats! Du bör nu kunna logga in med denna e-postadress.',\n    'email_confirm_resent' => 'Bekräftelsemailet har skickats på nytt, kolla din mail',\n    'email_confirm_thanks' => 'Tack för att du bekräftade!',\n    'email_confirm_thanks_desc' => 'Vänta en stund medan din bekräftelse hanteras. Om du inte omdirigeras efter 3 sekunder tryck på \"Fortsätt\" länken nedan för att fortsätta.',\n\n    'email_not_confirmed' => 'E-posadress ej bekräftad',\n    'email_not_confirmed_text' => 'Din e-postadress har inte bekräftats ännu.',\n    'email_not_confirmed_click_link' => 'Vänligen klicka på länken i det mail du fick strax efter att du registerade dig.',\n    'email_not_confirmed_resend' => 'Om du inte hittar mailet kan du begära en ny bekräftelse genom att fylla i formuläret nedan.',\n    'email_not_confirmed_resend_button' => 'Skicka bekräftelse på nytt',\n\n    // User Invite\n    'user_invite_email_subject' => 'Du har blivit inbjuden att gå med i :appName!',\n    'user_invite_email_greeting' => 'Ett konto har skapats för dig i :appName.',\n    'user_invite_email_text' => 'Klicka på knappen nedan för att ange ett lösenord och få tillgång:',\n    'user_invite_email_action' => 'Ange kontolösenord',\n    'user_invite_page_welcome' => 'Välkommen till :appName!',\n    'user_invite_page_text' => 'För att slutföra ditt konto och få åtkomst måste du ange ett lösenord som kommer att användas för att logga in på :appName vid framtida besök.',\n    'user_invite_page_confirm_button' => 'Bekräfta lösenord',\n    'user_invite_success_login' => 'Lösenord inställt, du bör nu kunna logga in med ditt inställda lösenord för att komma åt :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Konfigurera multifaktorsautentisering',\n    'mfa_setup_desc' => 'Konfigurera multifaktorsautentisering som ett extra skydd för ditt konto.',\n    'mfa_setup_configured' => 'Redan konfigurerad',\n    'mfa_setup_reconfigure' => 'Omkonfigurera',\n    'mfa_setup_remove_confirmation' => 'Är du säker på att du vill ta bort denna multifaktorautentiseringsmetod?',\n    'mfa_setup_action' => 'Konfigurera',\n    'mfa_backup_codes_usage_limit_warning' => 'Du har mindre än 5 reservkoder kvar, Vänligen generera och lagra en nya innan du får slut på koder för att förhindra att du inte kommer åt ditt konto.',\n    'mfa_option_totp_title' => 'Mobilapp',\n    'mfa_option_totp_desc' => 'För att använda multifaktorautentisering behöver du en mobil app som stöder TOTP så som Google Authenticator, Authy eller Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Reservkoder',\n    'mfa_option_backup_codes_desc' => 'Genererar en uppsättning engångsbaserade säkerhetskopieringskoder som du anger vid inloggningen för att verifiera din identitet. Se till att förvara dessa på en säker och säker plats.',\n    'mfa_gen_confirm_and_enable' => 'Bekräfta och aktivera',\n    'mfa_gen_backup_codes_title' => 'Konfiguration av reservkoder',\n    'mfa_gen_backup_codes_desc' => 'Spara nedanstående koder på en säker plats. När du använder systemet kommer du att kunna använda en av koderna som en andra autentiseringsmekanism.',\n    'mfa_gen_backup_codes_download' => 'Ladda ner koder',\n    'mfa_gen_backup_codes_usage_warning' => 'Varje kod kan endast användas en gång',\n    'mfa_gen_totp_title' => 'Konfiguration av mobilapp',\n    'mfa_gen_totp_desc' => 'För att använda multifaktorautentisering behöver du en mobil app som stöder TOTP så som Google Authenticator, Authy eller Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Skanna QR-koden nedan med din föredragna autentiseringsapp för att komma igång.',\n    'mfa_gen_totp_verify_setup' => 'Verifiera konfiguration',\n    'mfa_gen_totp_verify_setup_desc' => 'Kontrollera att allt fungerar genom att ange en kod, genererad i din autentiseringsapp, i rutan nedan:',\n    'mfa_gen_totp_provide_code_here' => 'Ange din appgenererade kod här',\n    'mfa_verify_access' => 'Verifiera åtkomst',\n    'mfa_verify_access_desc' => 'Ditt användarkonto kräver att du bekräftar din identitet via en ytterligare verifieringsmetod innan du får tillgång. Verifiera genom en av dina konfigurerade metoder för att fortsätta.',\n    'mfa_verify_no_methods' => 'Inga metoder konfigurerade',\n    'mfa_verify_no_methods_desc' => 'Inga multifaktorautentiseringsmetoder kunde hittas för ditt konto. Du måste konfigurera minst en metod innan du får tillgång.',\n    'mfa_verify_use_totp' => 'Verifiera med en mobilapp',\n    'mfa_verify_use_backup_codes' => 'Verifiera med en reservkod',\n    'mfa_verify_backup_code' => 'Reservkod',\n    'mfa_verify_backup_code_desc' => 'Ange en av dina återstående reservkoder nedan:',\n    'mfa_verify_backup_code_enter_here' => 'Ange reservkod här',\n    'mfa_verify_totp_desc' => 'Ange koden, som genereras med din mobilapp, nedan:',\n    'mfa_setup_login_notification' => 'Multifaktormetod konfigurerad, Logga nu in igen med den konfigurerade metoden.',\n];\n"
  },
  {
    "path": "lang/sv/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Avbryt',\n    'close' => 'Stäng',\n    'confirm' => 'Bekräfta',\n    'back' => 'Bakåt',\n    'save' => 'Spara',\n    'continue' => 'Fortsätt',\n    'select' => 'Välj',\n    'toggle_all' => 'Ändra alla',\n    'more' => 'Mer',\n\n    // Form Labels\n    'name' => 'Namn',\n    'description' => 'Beskrivning',\n    'role' => 'Roll',\n    'cover_image' => 'Omslagsbild',\n    'cover_image_description' => 'Denna bild bör vara ungefär 440x250px även om den kommer att vara flexibelt skalad och beskuren för att passa användargränssnittet i olika scenarier där så krävs, kommer faktiska visningsmått att skilja sig.',\n\n    // Actions\n    'actions' => 'Åtgärder',\n    'view' => 'Visa',\n    'view_all' => 'Visa alla',\n    'new' => 'Ny',\n    'create' => 'Skapa',\n    'update' => 'Uppdatera',\n    'edit' => 'Redigera',\n    'archive' => 'Arkivera',\n    'unarchive' => 'Avarkivera',\n    'sort' => 'Sortera',\n    'move' => 'Flytta',\n    'copy' => 'Kopiera',\n    'reply' => 'Svara',\n    'delete' => 'Ta bort',\n    'delete_confirm' => 'Bekräfta radering',\n    'search' => 'Sök',\n    'search_clear' => 'Rensa sökning',\n    'reset' => 'Återställ',\n    'remove' => 'Radera',\n    'add' => 'Lägg till',\n    'configure' => 'Konfigurera',\n    'manage' => 'Hantera',\n    'fullscreen' => 'Helskärm',\n    'favourite' => 'Favorit',\n    'unfavourite' => 'Ta bort favorit',\n    'next' => 'Nästa',\n    'previous' => 'Föregående',\n    'filter_active' => 'Aktivt filter:',\n    'filter_clear' => 'Rensa filter',\n    'download' => 'Ladda ner',\n    'open_in_tab' => 'Öppna i flik',\n    'open' => 'Öppna',\n\n    // Sort Options\n    'sort_options' => 'Sorteringsalternativ',\n    'sort_direction_toggle' => 'Växla sorteringsriktning',\n    'sort_ascending' => 'Sortera stigande',\n    'sort_descending' => 'Sortera fallande',\n    'sort_name' => 'Namn',\n    'sort_default' => 'Standard',\n    'sort_created_at' => 'Skapad',\n    'sort_updated_at' => 'Uppdaterad',\n\n    // Misc\n    'deleted_user' => 'Borttagen användare',\n    'no_activity' => 'Ingen aktivitet att visa',\n    'no_items' => 'Inga tillgängliga föremål',\n    'back_to_top' => 'Tillbaka till toppen',\n    'skip_to_main_content' => 'Hoppa till huvudinnehåll',\n    'toggle_details' => 'Växla detaljer',\n    'toggle_thumbnails' => 'Växla miniatyrer',\n    'details' => 'Information',\n    'grid_view' => 'Rutnätsvy',\n    'list_view' => 'Listvy',\n    'default' => 'Förvald',\n    'breadcrumb' => 'Brödsmula',\n    'status' => 'Status',\n    'status_active' => 'Aktiv',\n    'status_inactive' => 'Inaktiv',\n    'never' => 'Aldrig',\n    'none' => 'Inga',\n\n    // Header\n    'homepage' => 'Startsida',\n    'header_menu_expand' => 'Expandera sidhuvudsmenyn',\n    'profile_menu' => 'Profilmeny',\n    'view_profile' => 'Visa profil',\n    'edit_profile' => 'Redigera profil',\n    'dark_mode' => 'Mörkt läge',\n    'light_mode' => 'Ljust läge',\n    'global_search' => 'Global sökning',\n\n    // Layout tabs\n    'tab_info' => 'Information',\n    'tab_info_label' => 'Flik: Visa sekundär information',\n    'tab_content' => 'Innehåll',\n    'tab_content_label' => 'Flik: Visa primärt innehåll',\n\n    // Email Content\n    'email_action_help' => 'Om du har problem att klicka på \":actionText\"-knappen, kopiera och klistra in URL\\'n nedan i din webbläsare:',\n    'email_rights' => 'Alla rättigheter är reserverade',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Integritetspolicy',\n    'terms_of_service' => 'Användarvillkor',\n\n    // OpenSearch\n    'opensearch_description' => 'Sök :appName',\n];\n"
  },
  {
    "path": "lang/sv/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Val av bild',\n    'image_list' => 'Bildlista',\n    'image_details' => 'Bilddetaljer',\n    'image_upload' => 'Ladda upp bild',\n    'image_intro' => 'Här kan du välja och hantera bilder som tidigare har laddats upp till systemet.',\n    'image_intro_upload' => 'Ladda upp en ny bild genom att dra en bildfil till detta fönster, eller genom att använda knappen \"Ladda upp bild\" ovan.',\n    'image_all' => 'Alla',\n    'image_all_title' => 'Visa alla bilder',\n    'image_book_title' => 'Visa bilder som laddats upp till den aktuella boken',\n    'image_page_title' => 'Visa bilder som laddats upp till den aktuella sidan',\n    'image_search_hint' => 'Sök efter bildens namn',\n    'image_uploaded' => 'Laddades upp :uploadedDate',\n    'image_uploaded_by' => 'Uppladdad av :userName',\n    'image_uploaded_to' => 'Uppladdad till :pageLink',\n    'image_updated' => 'Uppdaterad :updateDate',\n    'image_load_more' => 'Ladda fler',\n    'image_image_name' => 'Bildnamn',\n    'image_delete_used' => 'Den här bilden används på nedanstående sidor.',\n    'image_delete_confirm_text' => 'Är du säker på att du vill radera denna bild?',\n    'image_select_image' => 'Välj bild',\n    'image_dropzone' => 'Släpp bilder här eller klicka för att ladda upp',\n    'image_dropzone_drop' => 'Släpp filer här för att ladda upp dem',\n    'images_deleted' => 'Bilder borttagna',\n    'image_preview' => 'Förhandsgranskning',\n    'image_upload_success' => 'Bilden har laddats upp',\n    'image_update_success' => 'Bildens uppgifter har ändrats',\n    'image_delete_success' => 'Bilden har tagits bort',\n    'image_replace' => 'Ersätt bild',\n    'image_replace_success' => 'Bildfilen har uppdaterats',\n    'image_rebuild_thumbs' => 'Återskapa variationer av bildstorlekar',\n    'image_rebuild_thumbs_success' => 'Variationer av bildstorlekar har återskapats!',\n\n    // Code Editor\n    'code_editor' => 'Redigera kod',\n    'code_language' => 'Språk',\n    'code_content' => 'Kod',\n    'code_session_history' => 'Sessionshistorik',\n    'code_save' => 'Spara',\n];\n"
  },
  {
    "path": "lang/sv/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Allmän',\n    'advanced' => 'Avancerad',\n    'none' => 'Inga',\n    'cancel' => 'Avbryt',\n    'save' => 'Spara',\n    'close' => 'Stäng',\n    'apply' => 'Tillämpa',\n    'undo' => 'Ångra',\n    'redo' => 'Gör om',\n    'left' => 'Vänster',\n    'center' => 'Mitten',\n    'right' => 'Höger',\n    'top' => 'Topp',\n    'middle' => 'Mitt',\n    'bottom' => 'Botten',\n    'width' => 'Bredd',\n    'height' => 'Höjd',\n    'More' => 'Mer',\n    'select' => 'Välj...',\n\n    // Toolbar\n    'formats' => 'Format',\n    'header_large' => 'Stor rubrik',\n    'header_medium' => 'Mellanstor rubrik',\n    'header_small' => 'Mindre rubrik',\n    'header_tiny' => 'Liten rubrik',\n    'paragraph' => 'Paragraf',\n    'blockquote' => 'Blockcitat',\n    'inline_code' => 'Inline-kod',\n    'callouts' => 'Anslag',\n    'callout_information' => 'Information',\n    'callout_success' => 'Slutfört',\n    'callout_warning' => 'Varning',\n    'callout_danger' => 'Fara',\n    'bold' => 'Fetstil',\n    'italic' => 'Kursiv',\n    'underline' => 'Understruken',\n    'strikethrough' => 'Genomstruken',\n    'superscript' => 'Upphöjd',\n    'subscript' => 'Nedsänkt',\n    'text_color' => 'Textfärg',\n    'highlight_color' => 'Markera färg',\n    'custom_color' => 'Anpassad färg',\n    'remove_color' => 'Ta bort färg',\n    'background_color' => 'Bakgrundsfärg',\n    'align_left' => 'Vänsterjustera',\n    'align_center' => 'Centrera',\n    'align_right' => 'Högerjustera',\n    'align_justify' => 'Marginaljustera',\n    'list_bullet' => 'Punktlista',\n    'list_numbered' => 'Numrerad lista',\n    'list_task' => 'Checklista',\n    'indent_increase' => 'Öka indrag',\n    'indent_decrease' => 'Minska indrag',\n    'table' => 'Tabell',\n    'insert_image' => 'Infoga bild',\n    'insert_image_title' => 'Infoga/redigera bild',\n    'insert_link' => 'Infoga/redigera länk',\n    'insert_link_title' => 'Infoga/redigera länk',\n    'insert_horizontal_line' => 'Infoga horisontell linje',\n    'insert_code_block' => 'Infoga kodblock',\n    'edit_code_block' => 'Redigera kodblock',\n    'insert_drawing' => 'Infoga/redigera ritning',\n    'drawing_manager' => 'Ritningshanterare',\n    'insert_media' => 'Infoga/redigera media',\n    'insert_media_title' => 'Infoga/redigera media',\n    'clear_formatting' => 'Rensa formatering',\n    'source_code' => 'Källkod',\n    'source_code_title' => 'Källkod',\n    'fullscreen' => 'Helskärm',\n    'image_options' => 'Bildalternativ',\n\n    // Tables\n    'table_properties' => 'Tabellegenskaper',\n    'table_properties_title' => 'Tabellegenskaper',\n    'delete_table' => 'Ta bort tabell',\n    'table_clear_formatting' => 'Rensa tabell-formatering',\n    'resize_to_contents' => 'Ändra storlek till innehåll',\n    'row_header' => 'Radrubrik',\n    'insert_row_before' => 'Infoga rad före',\n    'insert_row_after' => 'Infoga rad efter',\n    'delete_row' => 'Ta bort rad',\n    'insert_column_before' => 'Infoga kolumn före',\n    'insert_column_after' => 'Infoga kolumn efter',\n    'delete_column' => 'Ta bort kolumn',\n    'table_cell' => 'Cell',\n    'table_row' => 'Rad',\n    'table_column' => 'Kolumn',\n    'cell_properties' => 'Cellegenskaper',\n    'cell_properties_title' => 'Cellegenskaper',\n    'cell_type' => 'Celltyp',\n    'cell_type_cell' => 'Cell',\n    'cell_scope' => 'Omfattning',\n    'cell_type_header' => 'Rubrikcell',\n    'merge_cells' => 'Sammanfoga celler',\n    'split_cell' => 'Dela cell',\n    'table_row_group' => 'Radgrupp',\n    'table_column_group' => 'Kolumngrupp',\n    'horizontal_align' => 'Horisontell justering',\n    'vertical_align' => 'Vertikal justering',\n    'border_width' => 'Kantbredd',\n    'border_style' => 'Kantstil',\n    'border_color' => 'Kantfärg',\n    'row_properties' => 'Radegenskaper',\n    'row_properties_title' => 'Radegenskaper',\n    'cut_row' => 'Klipp rad',\n    'copy_row' => 'Kopiera rad',\n    'paste_row_before' => 'Infoga rad före',\n    'paste_row_after' => 'Klistra in rad efter',\n    'row_type' => 'Radtyp',\n    'row_type_header' => 'Rubrik',\n    'row_type_body' => 'Brödtext',\n    'row_type_footer' => 'Sidfot',\n    'alignment' => 'Justering',\n    'cut_column' => 'Klipp kolumn',\n    'copy_column' => 'Kopiera kolumn',\n    'paste_column_before' => 'Infoga kolumn före',\n    'paste_column_after' => 'Infoga kolumn före',\n    'cell_padding' => 'Cellfyllning',\n    'cell_spacing' => 'Cellavstånd',\n    'caption' => 'Bildtext',\n    'show_caption' => 'Visa bildtext',\n    'constrain' => 'Begränsa proportioner',\n    'cell_border_solid' => 'Solid',\n    'cell_border_dotted' => 'Punktad',\n    'cell_border_dashed' => 'Streckad',\n    'cell_border_double' => 'Dubbel',\n    'cell_border_groove' => 'Skåra',\n    'cell_border_ridge' => 'Upphöjning',\n    'cell_border_inset' => 'Infälld',\n    'cell_border_outset' => 'Utfälld',\n    'cell_border_none' => 'Ingen',\n    'cell_border_hidden' => 'Dold',\n\n    // Images, links, details/summary & embed\n    'source' => 'Källa',\n    'alt_desc' => 'Alternativ beskrivning',\n    'embed' => 'Bädda in',\n    'paste_embed' => 'Klistra in din inbäddningskod nedan:',\n    'url' => 'URL',\n    'text_to_display' => 'Text som ska visas',\n    'title' => 'Titel',\n    'browse_links' => 'Bläddra bland länkar',\n    'open_link' => 'Öppna länk',\n    'open_link_in' => 'Öppna länk i...',\n    'open_link_current' => 'Aktuellt fönster',\n    'open_link_new' => 'Nytt fönster',\n    'remove_link' => 'Radera länk',\n    'insert_collapsible' => 'Infoga hopfällbart block',\n    'collapsible_unwrap' => 'Expandera',\n    'edit_label' => 'Redigera etikett',\n    'toggle_open_closed' => 'Växla mellan öppen/stängd',\n    'collapsible_edit' => 'Redigera hopfällbart block',\n    'toggle_label' => 'Visa eller dölj etikett',\n\n    // About view\n    'about' => 'Om redigeraren',\n    'about_title' => 'Om WYSIWYG-redigeraren',\n    'editor_license' => 'Licens och upphovsrätt för redigerare',\n    'editor_lexical_license' => 'Denna editor är byggd som en fork av :lexicalLink som distribueras under MIT-licensen.',\n    'editor_lexical_license_link' => 'Fullständiga licensdetaljer hittas här.',\n    'editor_tiny_license' => 'Denna redigerare är byggd med :tinyLink som tillhandahålls under MIT licensen.',\n    'editor_tiny_license_link' => 'Upphovsrätten och licensuppgifterna för TinyMCE hittar du här.',\n    'save_continue' => 'Spara sida & fortsätt',\n    'callouts_cycle' => '(Fortsätt trycka för att växla mellan typer)',\n    'link_selector' => 'Länka till innehåll',\n    'shortcuts' => 'Genvägar',\n    'shortcut' => 'Genväg',\n    'shortcuts_intro' => 'Följande genvägar finns i redigeraren:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Beskrivning',\n];\n"
  },
  {
    "path": "lang/sv/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Nyligen skapat',\n    'recently_created_pages' => 'Sidor som skapats nyligen',\n    'recently_updated_pages' => 'Sidor som uppdaterats nyligen',\n    'recently_created_chapters' => 'Kapitel som skapats nyligen',\n    'recently_created_books' => 'Böcker som skapats nyligen',\n    'recently_created_shelves' => 'Hyllor som skapats nyligen',\n    'recently_update' => 'Nyligen uppdaterat',\n    'recently_viewed' => 'Nyligen läst',\n    'recent_activity' => 'Aktivitet',\n    'create_now' => 'Skapa en nu',\n    'revisions' => 'Revisioner',\n    'meta_revision' => 'Revision #:revisionCount',\n    'meta_created' => 'Skapad :timeLength',\n    'meta_created_name' => 'Skapad :timeLength av :user',\n    'meta_updated' => 'Uppdaterad :timeLength',\n    'meta_updated_name' => 'Uppdaterad :timeLength av :user',\n    'meta_owned_name' => 'Ägs av :user',\n    'meta_reference_count' => 'Refererad till av :count item|Referenced by :count items',\n    'entity_select' => 'Välj enhet',\n    'entity_select_lack_permission' => 'Du har inte den behörighet som krävs för att välja det här objektet',\n    'images' => 'Bilder',\n    'my_recent_drafts' => 'Mina nyaste utkast',\n    'my_recently_viewed' => 'Mina senast visade sidor',\n    'my_most_viewed_favourites' => 'Mina mest visade favoriter',\n    'my_favourites' => 'Mina favoriter',\n    'no_pages_viewed' => 'Du har inte visat några sidor',\n    'no_pages_recently_created' => 'Inga sidor har skapats nyligen',\n    'no_pages_recently_updated' => 'Inga sidor har uppdaterats nyligen',\n    'export' => 'Exportera',\n    'export_html' => 'Webb-fil',\n    'export_pdf' => 'PDF-fil',\n    'export_text' => 'Textfil',\n    'export_md' => 'Markdown-fil',\n    'export_zip' => 'Portabel ZIP',\n    'default_template' => 'Förvald sidmall',\n    'default_template_explain' => 'Tilldela en sidmall som kommer att användas som standardinnehåll för alla sidor som skapats inom detta objekt. Tänk på att detta endast kommer att användas om skaparen av sidan har tillgång till den valda mallsidan.',\n    'default_template_select' => 'Välj en sidmall',\n    'import' => 'Importera',\n    'import_validate' => 'Validera import',\n    'import_desc' => 'Importera böcker, kapitel och sidor med hjälp av en portabel ZIP-export från samma eller en annan instans. Välj en ZIP-fil för att fortsätta. Efter att filen har laddats upp och validerats kan du konfigurera och bekräfta importen i nästa vy.',\n    'import_zip_select' => 'Välj ZIP-fil att ladda upp',\n    'import_zip_validation_errors' => 'Fel upptäcktes vid validering av den angivna ZIP-filen:',\n    'import_pending' => 'Väntande importer',\n    'import_pending_none' => 'Ingen import har påbörjats.',\n    'import_continue' => 'Fortsätt import',\n    'import_continue_desc' => 'Granska innehållet som skall importeras från den uppladdade ZIP-filen. När du är redo, kör importen för att lägga till innehållet. Den uppladdade ZIP-baserade importfilen kommer automatiskt att tas bort vid lyckad import.',\n    'import_details' => 'Importdetaljer',\n    'import_run' => 'Kör import',\n    'import_size' => ':size ZIP-storlek',\n    'import_uploaded_at' => 'Uppladdad :relativeTime',\n    'import_uploaded_by' => 'Uppladdad av',\n    'import_location' => 'Importplats',\n    'import_location_desc' => 'Välj en målplats för ditt importerade innehåll. Du behöver relevanta behörigheter för att skapa på den plats du väljer.',\n    'import_delete_confirm' => 'Är du säker på att du vill ta bort denna import?',\n    'import_delete_desc' => 'Detta kommer att ta bort den uppladdade ZIP-baserade importfilen och kan inte ångras.',\n    'import_errors' => 'Importfel',\n    'import_errors_desc' => 'Följande fel inträffade under importförsöket:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Rättigheter',\n    'permissions_desc' => 'Sätt rättigheter här för att åsidosätta de standardrättigheter som tillhandahålls av användarroller.',\n    'permissions_book_cascade' => 'Rättigheter som sätts på böcker kommer automatiskt att ärvas av underkapitel och sidor, såvida de inte har sina egna rättigheter definierade.',\n    'permissions_chapter_cascade' => 'Rättigheter som sätts på kapitel kommer automatiskt att ärvas av underliggande sidor, såvida de inte har sina egna rättigheter definierade.',\n    'permissions_save' => 'Spara rättigheter',\n    'permissions_owner' => 'Ägare',\n    'permissions_role_everyone_else' => 'Alla andra',\n    'permissions_role_everyone_else_desc' => 'Ställ in rättigheter för alla roller som inte uttryckligen har åsidosatts.',\n    'permissions_role_override' => 'Åsidosätt rättigheter för roll',\n    'permissions_inherit_defaults' => 'Ärv standardrättigheter',\n\n    // Search\n    'search_results' => 'Sökresultat',\n    'search_total_results_found' => ':count resultat|:count resultat',\n    'search_clear' => 'Rensa sökning',\n    'search_no_pages' => 'Inga sidor matchade sökningen',\n    'search_for_term' => 'Sök efter :term',\n    'search_more' => 'Fler resultat',\n    'search_advanced' => 'Avancerad sök',\n    'search_terms' => 'Söktermer',\n    'search_content_type' => 'Innehållstyp',\n    'search_exact_matches' => 'Exakta matchningar',\n    'search_tags' => 'Taggar',\n    'search_options' => 'Alternativ',\n    'search_viewed_by_me' => 'Visade av mig',\n    'search_not_viewed_by_me' => 'Ej visade av mig',\n    'search_permissions_set' => 'Har anpassade rättigheter',\n    'search_created_by_me' => 'Skapade av mig',\n    'search_updated_by_me' => 'Uppdaterade av mig',\n    'search_owned_by_me' => 'Ägs av mig',\n    'search_date_options' => 'Datumalternativ',\n    'search_updated_before' => 'Uppdaterade före',\n    'search_updated_after' => 'Uppdaterade efter',\n    'search_created_before' => 'Skapade före',\n    'search_created_after' => 'Skapade efter',\n    'search_set_date' => 'Ange datum',\n    'search_update' => 'Uppdatera sökning',\n\n    // Shelves\n    'shelf' => 'Hylla',\n    'shelves' => 'Hyllor',\n    'x_shelves' => ':count hylla|:count hyllor',\n    'shelves_empty' => 'Du har inte skapat någon hylla',\n    'shelves_create' => 'Skapa ny hylla',\n    'shelves_popular' => 'Populära hyllor',\n    'shelves_new' => 'Nya hyllor',\n    'shelves_new_action' => 'Ny hylla',\n    'shelves_popular_empty' => 'De populäraste hyllorna kommer hamna här',\n    'shelves_new_empty' => 'De senast skapade hyllorna kommer hamna här',\n    'shelves_save' => 'Spara hylla',\n    'shelves_books' => 'Böcker i denna hylla',\n    'shelves_add_books' => 'Lägg till böcker till hyllan',\n    'shelves_drag_books' => 'Drag böckerna nedan för att lägga till dem i den här hyllan',\n    'shelves_empty_contents' => 'Denna hylla har inga böcker än',\n    'shelves_edit_and_assign' => 'Redigera hyllan för att lägga till böcker',\n    'shelves_edit_named' => 'Redigera hyllan :name',\n    'shelves_edit' => 'Redigera hylla',\n    'shelves_delete' => 'Ta bort hylla',\n    'shelves_delete_named' => 'Ta bort hyllan :name',\n    'shelves_delete_explain' => \"Detta kommer att ta bort hyllan med namnet ':name'. Böcker i hyllan kommer inte tas bort.\",\n    'shelves_delete_confirmation' => 'Är du säker på att du vill ta bort den här hyllan?',\n    'shelves_permissions' => 'Rättigheter för hylla',\n    'shelves_permissions_updated' => 'Rättigheter för hylla uppdaterades',\n    'shelves_permissions_active' => 'Rättigheter för hylla aktiverade',\n    'shelves_permissions_cascade_warning' => 'Rättigheter för hyllor ärvs inte automatiskt ner till böckerna i hyllorna. Detta beror på att en bok kan finnas från flera hyllor. Rättigheter kan däremot kopieras ner till en bok i hyllan med hjälp av alternativet nedan.',\n    'shelves_permissions_create' => 'Behörigheter för att skapa hyllor används endast för att kopiera behörigheter till underliggande böcker via åtgärden nedan. De styr inte möjligheten att skapa böcker.',\n    'shelves_copy_permissions_to_books' => 'Kopiera rättigheter till böcker',\n    'shelves_copy_permissions' => 'Kopiera rättigheter',\n    'shelves_copy_permissions_explain' => 'Detta kommer att tillämpa rättigheterna från den här hyllan på alla böcker den innehåller. Se till att eventuella ändringar sparats innan tillämpningen genomförs.',\n    'shelves_copy_permission_success' => 'Rättigheter för hyllan kopierades till :count böcker',\n\n    // Books\n    'book' => 'Bok',\n    'books' => 'Böcker',\n    'x_books' => ':count bok|:count böcker',\n    'books_empty' => 'Inga böcker har skapats',\n    'books_popular' => 'Populära böcker',\n    'books_recent' => 'Nya böcker',\n    'books_new' => 'Nya böcker',\n    'books_new_action' => 'Ny bok',\n    'books_popular_empty' => 'De mest populära böckerna kommer att visas här.',\n    'books_new_empty' => 'De senaste böckerna som skapats kommer att visas här.',\n    'books_create' => 'Skapa ny bok',\n    'books_delete' => 'Ta bort bok',\n    'books_delete_named' => 'Ta bort boken :bookName',\n    'books_delete_explain' => 'Du håller på att ta bort boken \\':bookName\\'. Alla sidor och kapitel kommer också att tas bort.',\n    'books_delete_confirmation' => 'Är du säker på att du vill ta bort boken?',\n    'books_edit' => 'Redigera bok',\n    'books_edit_named' => 'Redigera bok :bookName',\n    'books_form_book_name' => 'Bokens namn',\n    'books_save' => 'Spara bok',\n    'books_permissions' => 'Rättigheter för boken',\n    'books_permissions_updated' => 'Bokens rättigheter har uppdaterats',\n    'books_empty_contents' => 'Det finns inga sidor eller kapitel i den här boken.',\n    'books_empty_create_page' => 'Skapa en ny sida',\n    'books_empty_sort_current_book' => 'Sortera aktuell bok',\n    'books_empty_add_chapter' => 'Lägg till kapitel',\n    'books_permissions_active' => 'Anpassade rättigheter är i bruk',\n    'books_search_this' => 'Sök i boken',\n    'books_navigation' => 'Navigering',\n    'books_sort' => 'Sortera bokens innehåll',\n    'books_sort_desc' => 'Flytta kapitel och sidor inom en bok för att omorganisera dess innehåll. Andra böcker kan läggas till, vilket gör det enkelt att flytta kapitel och sidor mellan böcker. Du kan även ställa in en regel som automatiskt sorterar bokens innehåll vid ändringar.',\n    'books_sort_auto_sort' => 'Automatiskt sorteringsalternativ',\n    'books_sort_auto_sort_active' => 'Aktiv automatisk sorteringsregel: :sortName',\n    'books_sort_named' => 'Sortera boken :bookName',\n    'books_sort_name' => 'Sortera utifrån namn',\n    'books_sort_created' => 'Sortera utifrån skapelse',\n    'books_sort_updated' => 'Sortera utifrån uppdatering',\n    'books_sort_chapters_first' => 'Kapitel först',\n    'books_sort_chapters_last' => 'Kapitel sist',\n    'books_sort_show_other' => 'Visa andra böcker',\n    'books_sort_save' => 'Spara ordning',\n    'books_sort_show_other_desc' => 'Lägg till andra böcker här för att inkludera dem i sorteringsåtgärden och möjliggöra enkel omorganisering mellan böcker.',\n    'books_sort_move_up' => 'Flytta upp',\n    'books_sort_move_down' => 'Flytta ned',\n    'books_sort_move_prev_book' => 'Gå till förgående bok',\n    'books_sort_move_next_book' => 'Gå till nästa bok',\n    'books_sort_move_prev_chapter' => 'Gå till förgående kapitel',\n    'books_sort_move_next_chapter' => 'Gå till nästa kapitel',\n    'books_sort_move_book_start' => 'Gå till början av boken',\n    'books_sort_move_book_end' => 'Gå till slutet av boken',\n    'books_sort_move_before_chapter' => 'Gå till innan kapitlet',\n    'books_sort_move_after_chapter' => 'Gå till efter kapitlet',\n    'books_copy' => 'Kopiera bok',\n    'books_copy_success' => 'Boken har kopierats',\n\n    // Chapters\n    'chapter' => 'Kapitel',\n    'chapters' => 'Kapitel',\n    'x_chapters' => ':count kapitel|:count kapitel',\n    'chapters_popular' => 'Populära kapitel',\n    'chapters_new' => 'Nytt kapitel',\n    'chapters_create' => 'Skapa nytt kapitel',\n    'chapters_delete' => 'Radera kapitel',\n    'chapters_delete_named' => 'Radera kapitlet :chapterName',\n    'chapters_delete_explain' => 'Detta kommer att ta bort kapitlet med namnet \\':chapterName\\'. Alla sidor som finns inom detta kapitel kommer också att raderas.',\n    'chapters_delete_confirm' => 'Är du säker på att du vill ta bort det här kapitlet?',\n    'chapters_edit' => 'Redigera kapitel',\n    'chapters_edit_named' => 'Redigera kapitel :chapterName',\n    'chapters_save' => 'Spara kapitel',\n    'chapters_move' => 'Flytta kapitel',\n    'chapters_move_named' => 'Flytta kapitel :chapterName',\n    'chapters_copy' => 'Kopiera kapitel',\n    'chapters_copy_success' => 'Kapitel har kopierats',\n    'chapters_permissions' => 'Rättigheter för kapitel',\n    'chapters_empty' => 'Det finns inga sidor i det här kapitlet.',\n    'chapters_permissions_active' => 'Anpassade rättigheter är i bruk',\n    'chapters_permissions_success' => 'Rättigheterna för kapitlet har uppdaterats',\n    'chapters_search_this' => 'Sök i detta kapitel',\n    'chapter_sort_book' => 'Sortera bok',\n\n    // Pages\n    'page' => 'Sida',\n    'pages' => 'Sidor',\n    'x_pages' => ':count sida|:count sidor',\n    'pages_popular' => 'Populära sidor',\n    'pages_new' => 'Ny sida',\n    'pages_attachments' => 'Bilagor',\n    'pages_navigation' => 'Navigering',\n    'pages_delete' => 'Ta bort sida',\n    'pages_delete_named' => 'Ta bort sidan :pageName',\n    'pages_delete_draft_named' => 'Ta bort utkastet :pageName',\n    'pages_delete_draft' => 'Ta bort utkast',\n    'pages_delete_success' => 'Sidan har tagits bort',\n    'pages_delete_draft_success' => 'Utkastet har tagits bort',\n    'pages_delete_warning_template' => 'Denna sida används för närvarande som standardsidmall för en bok eller ett kapitel. Dessa böcker eller kapitel kommer inte längre ha någon tilldelad standardsidmall om sidan tas bort.',\n    'pages_delete_confirm' => 'Är du säker på att du vill ta bort den här sidan?',\n    'pages_delete_draft_confirm' => 'Är du säker på att du vill ta bort det här utkastet?',\n    'pages_editing_named' => 'Redigerar sida :pageName',\n    'pages_edit_draft_options' => 'Inställningar för utkast',\n    'pages_edit_save_draft' => 'Spara utkast',\n    'pages_edit_draft' => 'Redigera utkast',\n    'pages_editing_draft' => 'Redigerar utkast',\n    'pages_editing_page' => 'Redigerar sida',\n    'pages_edit_draft_save_at' => 'Utkastet sparades ',\n    'pages_edit_delete_draft' => 'Ta bort utkast',\n    'pages_edit_delete_draft_confirm' => 'Är du säker på att du vill ta bort dina utkaständringar? Alla ändringar du gjort sedan den senaste fullständiga sparningen kommer att gå förlorade, och redigeraren kommer att uppdateras med det senaste icke-utkastet av sidan.',\n    'pages_edit_discard_draft' => 'Ta bort utkastet',\n    'pages_edit_switch_to_markdown' => 'Växla till Markdown-redigerare',\n    'pages_edit_switch_to_markdown_clean' => '(Rent innehåll)',\n    'pages_edit_switch_to_markdown_stable' => '(Stabilt innehåll)',\n    'pages_edit_switch_to_wysiwyg' => 'Växla till WYSIWYG-redigerare',\n    'pages_edit_switch_to_new_wysiwyg' => 'Växla till ny WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Beskriv dina ändringar',\n    'pages_edit_enter_changelog_desc' => 'Ange en kort beskrivning av de ändringar du har gjort',\n    'pages_edit_enter_changelog' => 'Ändringslogg',\n    'pages_editor_switch_title' => 'Växla redigerare',\n    'pages_editor_switch_are_you_sure' => 'Är du säker på att du vill ändra redigerare för denna sida?',\n    'pages_editor_switch_consider_following' => 'Tänk på följande när du byter redigerare:',\n    'pages_editor_switch_consideration_a' => 'När du har sparat kommer den nya redigeraren att användas vid alla framtida redigeringar, inklusive de som kanske inte själva kan ändra redigerare.',\n    'pages_editor_switch_consideration_b' => 'Detta kan potentiellt leda till förlust av detaljer och syntax under vissa omständigheter.',\n    'pages_editor_switch_consideration_c' => 'Osparade ändringar av taggar eller ändringsloggar kommer att gå förlorade.',\n    'pages_save' => 'Spara sida',\n    'pages_title' => 'Sidtitel',\n    'pages_name' => 'Sidans namn',\n    'pages_md_editor' => 'Redigerare',\n    'pages_md_preview' => 'Förhandsvisa',\n    'pages_md_insert_image' => 'Infoga bild',\n    'pages_md_insert_link' => 'Infoga länk',\n    'pages_md_insert_drawing' => 'Infoga teckning',\n    'pages_md_show_preview' => 'Visa förhandsgranskning',\n    'pages_md_sync_scroll' => 'Sync preview scroll',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Osparad ritning hittades',\n    'pages_drawing_unsaved_confirm' => 'Osparade ritningsdata hittades från ett tidigare misslyckat sparförsök. Vill du återställa och fortsätta redigera den osparade ritningen?',\n    'pages_not_in_chapter' => 'Sidan ligger inte i något kapitel',\n    'pages_move' => 'Flytta sida',\n    'pages_copy' => 'Kopiera sida',\n    'pages_copy_desination' => 'Destination',\n    'pages_copy_success' => 'Sidan har kopierats',\n    'pages_permissions' => 'Rättigheter för sida',\n    'pages_permissions_success' => 'Rättigheterna för sidan har uppdaterats',\n    'pages_revision' => 'Revidering',\n    'pages_revisions' => 'Sidrevisioner',\n    'pages_revisions_desc' => 'Nedan listas alla tidigare versioner av denna sida. Du kan granska, jämföra och återställa äldre versioner om du har behörighet. Den fullständiga versionshistoriken kanske inte visas i sin helhet här, eftersom äldre versioner kan ha raderats automatiskt beroende på systemets konfiguration.',\n    'pages_revisions_named' => 'Sidrevisioner för :pageName',\n    'pages_revision_named' => 'Sidrevision för :pageName',\n    'pages_revision_restored_from' => 'Återställd från #:id; :summary',\n    'pages_revisions_created_by' => 'Skapad av',\n    'pages_revisions_date' => 'Revisionsdatum',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Revisionsnummer',\n    'pages_revisions_numbered' => 'Revisions #:id',\n    'pages_revisions_numbered_changes' => 'Revision #:id ändringar',\n    'pages_revisions_editor' => 'Typ av redigerare',\n    'pages_revisions_changelog' => 'Ändringslogg',\n    'pages_revisions_changes' => 'Ändringar',\n    'pages_revisions_current' => 'Nuvarande version',\n    'pages_revisions_preview' => 'Förhandsgranska',\n    'pages_revisions_restore' => 'Återställ',\n    'pages_revisions_none' => 'Sidan har inga revisioner',\n    'pages_copy_link' => 'Kopiera länk',\n    'pages_edit_content_link' => 'Hoppa till sektionen i redigeraren',\n    'pages_pointer_enter_mode' => 'Ange markeringsläge för sektion',\n    'pages_pointer_label' => 'Alternativ för sidsektion',\n    'pages_pointer_permalink' => 'Page Section Permalink',\n    'pages_pointer_include_tag' => 'Page Section Include Tag',\n    'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag',\n    'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink',\n    'pages_permissions_active' => 'Anpassade rättigheter är i bruk',\n    'pages_initial_revision' => 'Första publicering',\n    'pages_references_update_revision' => 'Automatisk uppdatering av interna länkar',\n    'pages_initial_name' => 'Ny sida',\n    'pages_editing_draft_notification' => 'Du redigerar just nu ett utkast som senast sparades :timeDiff.',\n    'pages_draft_edited_notification' => 'Denna sida har uppdaterats sen dess. Vi rekommenderar att du förkastar dina ändringar.',\n    'pages_draft_page_changed_since_creation' => 'Denna sida har uppdaterats sedan detta utkast skapades. Det rekommenderas att du slänger detta utkast eller försäkrar att du inte skriver över några sidändringar.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count har börjat redigera den här sidan',\n        'start_b' => ':userName har börjat redigera den här sidan',\n        'time_a' => 'sedan sidan senast uppdaterades',\n        'time_b' => 'under de senaste :minCount minuterna',\n        'message' => ':start :time. Var försiktiga så att ni inte skriver över varandras ändringar!',\n    ],\n    'pages_draft_discarded' => 'Utkastet har tagits bort. Redigeringsverktyget har uppdaterats med aktuellt sidinnehåll',\n    'pages_draft_deleted' => 'Utkastet har raderats. Redigeringsverktyget har uppdaterats med aktuellt sidinnehåll',\n    'pages_specific' => 'Specifik sida',\n    'pages_is_template' => 'Sidmall',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Visa/Dölj sidopanel',\n    'page_tags' => 'Sidtaggar',\n    'chapter_tags' => 'Kapiteltaggar',\n    'book_tags' => 'Boktaggar',\n    'shelf_tags' => 'Hylltaggar',\n    'tag' => 'Tagg',\n    'tags' =>  'Taggar',\n    'tags_index_desc' => 'Taggar kan användas på innehåll inom systemet för att skapa en flexibel form av kategorisering. Taggar kan ha både en nyckel och ett värde, där värdet är valfritt. När en tagg har tilldelats kan innehållet sökas fram med hjälp av taggens namn och värde.',\n    'tag_name' =>  'Etikettnamn',\n    'tag_value' => 'Taggvärde (Frivilligt)',\n    'tags_explain' => \"Lägg till taggar för att kategorisera ditt innehåll bättre. \\n Du kan tilldela ett värde till en tagg för ännu bättre organisering.\",\n    'tags_add' => 'Lägg till ännu en tagg',\n    'tags_remove' => 'Ta bort denna etikett',\n    'tags_usages' => 'Totalt antal taggar',\n    'tags_assigned_pages' => 'Tilldelad till sidor',\n    'tags_assigned_chapters' => 'Tilldelad till kapitel',\n    'tags_assigned_books' => 'Tilldelad till böcker',\n    'tags_assigned_shelves' => 'Tilldelad till hyllor',\n    'tags_x_unique_values' => ':count unika värden',\n    'tags_all_values' => 'Alla värden',\n    'tags_view_tags' => 'Visa taggar',\n    'tags_view_existing_tags' => 'Visa befintliga taggar',\n    'tags_list_empty_hint' => 'Taggar kan tilldelas via sideditorns sidofält eller medan du redigerar detaljerna i en bok, kapitel eller hylla.',\n    'attachments' => 'Bilagor',\n    'attachments_explain' => 'Ladda upp filer eller bifoga länkar till ditt innehåll. Dessa visas i sidokolumnen.',\n    'attachments_explain_instant_save' => 'Ändringar här sparas omgående.',\n    'attachments_upload' => 'Ladda upp fil',\n    'attachments_link' => 'Bifoga länk',\n    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',\n    'attachments_set_link' => 'Ange länk',\n    'attachments_delete' => 'Är du säker på att du vill ta bort bilagan?',\n    'attachments_dropzone' => 'Släpp filer här för uppladdning',\n    'attachments_no_files' => 'Inga filer har laddats upp',\n    'attachments_explain_link' => 'Du kan bifoga en länk om du inte vill ladda upp en fil. Detta kan vara en länk till en annan sida eller till en fil i molnet.',\n    'attachments_link_name' => 'Länknamn',\n    'attachment_link' => 'Länk till bilaga',\n    'attachments_link_url' => 'Länk till fil',\n    'attachments_link_url_hint' => 'URL till sida eller fil',\n    'attach' => 'Bifoga',\n    'attachments_insert_link' => 'Lägg till bilagelänk till sida',\n    'attachments_edit_file' => 'Redigera fil',\n    'attachments_edit_file_name' => 'Filnamn',\n    'attachments_edit_drop_upload' => 'Släpp filer här eller klicka för att ladda upp och skriva över',\n    'attachments_order_updated' => 'Ordningen på bilagorna har uppdaterats',\n    'attachments_updated_success' => 'Bilagan har uppdaterats',\n    'attachments_deleted' => 'Bilagan har tagits bort',\n    'attachments_file_uploaded' => 'Filen har laddats upp',\n    'attachments_file_updated' => 'Filen har uppdaterats',\n    'attachments_link_attached' => 'Länken har bifogats till sidan',\n    'templates' => 'Mallar',\n    'templates_set_as_template' => 'Sidan är en mall',\n    'templates_explain_set_as_template' => 'Du kan använda denna sida som en mall så att dess innehåll kan användas när du skapar andra sidor. Andra användare kommer att kunna använda denna mall om de har visningsrättigheter för den här sidan.',\n    'templates_replace_content' => 'Ersätt sidinnehåll',\n    'templates_append_content' => 'Lägg till till sidans innehåll',\n    'templates_prepend_content' => 'Lägg till före sidans innehåll',\n\n    // Profile View\n    'profile_user_for_x' => 'Användare i :time',\n    'profile_created_content' => 'Skapat innehåll',\n    'profile_not_created_pages' => ':userName har inte skapat några sidor',\n    'profile_not_created_chapters' => ':userName har inte skapat några kapitel',\n    'profile_not_created_books' => ':userName har inte skapat några böcker',\n    'profile_not_created_shelves' => ':userName har inte skapat några hyllor',\n\n    // Comments\n    'comment' => 'Kommentar',\n    'comments' => 'Kommentarer',\n    'comment_add' => 'Lägg till kommentar',\n    'comment_none' => 'Inga kommentarer att visa',\n    'comment_placeholder' => 'Lämna en kommentar här',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Spara kommentar',\n    'comment_new' => 'Ny kommentar',\n    'comment_created' => 'kommenterade :createDiff',\n    'comment_updated' => 'Uppdaterade :updateDiff av :username',\n    'comment_updated_indicator' => 'Uppdaterad',\n    'comment_deleted_success' => 'Kommentar borttagen',\n    'comment_created_success' => 'Kommentaren har sparats',\n    'comment_updated_success' => 'Kommentaren har uppdaterats',\n    'comment_archive_success' => 'Arkivera kommentar',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'Visa kommentar',\n    'comment_jump_to_thread' => 'Hoppa till tråd',\n    'comment_delete_confirm' => 'Är du säker på att du vill ta bort den här kommentaren?',\n    'comment_in_reply_to' => 'Som svar på :commentId',\n    'comment_reference' => 'Referens',\n    'comment_reference_outdated' => '(Utdaterad)',\n    'comment_editor_explain' => 'Här är kommentarer som lämnats på denna sida. Kommentarer kan läggas till och hanteras när den sparade sidan visas.',\n\n    // Revision\n    'revision_delete_confirm' => 'Är du säker på att du vill radera den här versionen?',\n    'revision_restore_confirm' => 'Är du säker på att du vill använda denna revision? Det nuvarande innehållet kommer att ersättas.',\n    'revision_cannot_delete_latest' => 'Det går inte att ta bort den senaste versionen.',\n\n    // Copy view\n    'copy_consider' => 'Tänk på nedan när du kopierar innehåll.',\n    'copy_consider_permissions' => 'Anpassade behörighetsinställningar kommer inte att kopieras.',\n    'copy_consider_owner' => 'Du kommer att bli ägare till allt kopierat innehåll.',\n    'copy_consider_images' => 'Bildfiler för sidan kommer inte att dupliceras och de ursprungliga bilderna kommer att behålla sin relation till den sida de ursprungligen laddades upp till.',\n    'copy_consider_attachments' => 'Sidans bifogade filer kommer inte att kopieras.',\n    'copy_consider_access' => 'Ändring av plats, ägare eller behörigheter kan leda till att detta innehåll blir tillgängligt för dem som tidigare inte haft åtkomst.',\n\n    // Conversions\n    'convert_to_shelf' => 'Konvertera till hylla',\n    'convert_to_shelf_contents_desc' => 'Du kan konvertera denna bok till en ny hylla med samma innehåll. Kapitlen inom denna bok konverteras till nya böcker. Om denna bok innehåller sidor som inte är i ett kapitel så kommer denna bok att döpas om och innehålla dessa sidor. Denna bok blir då en del av den nya hyllan.',\n    'convert_to_shelf_permissions_desc' => 'Alla behörigheter som ställs in på denna bok kommer att kopieras till den nya hyllan och till alla nya underböcker som inte har egna behörigheter applicerade. Observera att behörigheter på hyllor inte automatisk ärvs av innehåll inom hyllan, så som med böcker.',\n    'convert_book' => 'Konvertera bok',\n    'convert_book_confirm' => 'Är du säker på att du vill konvertera boken?',\n    'convert_undo_warning' => 'Detta kan inte ångras lika lätt.',\n    'convert_to_book' => 'Konvertera till bok',\n    'convert_to_book_desc' => 'Du kan konvertera detta kapitel till en ny bok med samma innehåll. Eventuella behörigheter som angetts på detta kapitel kommer att kopieras till den nya boken men ärvda behörigheter från föräldraboken kommer inte att kopieras vilket kan leda till skillnader i åtkomsten.',\n    'convert_chapter' => 'Konvertera kapitel',\n    'convert_chapter_confirm' => 'Är du säker på att du vill konvertera det här kapitlet?',\n\n    // References\n    'references' => 'Referenser',\n    'references_none' => 'Det finns inga referenser kopplade till detta objekt.',\n    'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',\n\n    // Watch Options\n    'watch' => 'Följ',\n    'watch_title_default' => 'Standardinställningar',\n    'watch_desc_default' => 'Revert watching to just your default notification preferences.',\n    'watch_title_ignore' => 'Ignorera',\n    'watch_desc_ignore' => 'Ignorera samtliga meddelanden, även sådana som styrs av användarens egna inställningar.',\n    'watch_title_new' => 'Nya sidor',\n    'watch_desc_new' => 'Meddela när en ny sida skapas inom detta objekt.',\n    'watch_title_updates' => 'Alla siduppdateringar',\n    'watch_desc_updates' => 'Meddela vid alla nya sidor och siduppdateringar.',\n    'watch_desc_updates_page' => 'Meddela alla siduppdateringar.',\n    'watch_title_comments' => 'All Page Updates & Comments',\n    'watch_desc_comments' => 'Meddela vid alla nya sidor, siduppdateringar och nya kommentarer.',\n    'watch_desc_comments_page' => 'Meddela vid siduppdateringar och nya kommentarer.',\n    'watch_change_default' => 'Ändra standardinställningar för meddelanden',\n    'watch_detail_ignore' => 'Ignorera meddelanden',\n    'watch_detail_new' => 'Watching for new pages',\n    'watch_detail_updates' => 'Watching new pages and updates',\n    'watch_detail_comments' => 'Watching new pages, updates & comments',\n    'watch_detail_parent_book' => 'Watching via parent book',\n    'watch_detail_parent_book_ignore' => 'Ignoring via parent book',\n    'watch_detail_parent_chapter' => 'Watching via parent chapter',\n    'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter',\n];\n"
  },
  {
    "path": "lang/sv/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Du har inte tillgång till den här sidan.',\n    'permissionJson' => 'Du har inte rätt att utföra den här åtgärden.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'En användare med adressen :email finns redan.',\n    'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',\n    'email_already_confirmed' => 'E-posten har redan bekräftats, prova att logga in.',\n    'email_confirmation_invalid' => 'Denna bekräftelsekod är inte giltig eller har redan använts. Vänligen prova att registrera dig på nytt.',\n    'email_confirmation_expired' => 'Denna bekräftelsekod har gått ut. Vi har skickat dig en ny.',\n    'email_confirmation_awaiting' => 'E-postadressen för det konto som används måste bekräftas',\n    'ldap_fail_anonymous' => 'LDAP-inloggning misslyckades med anonym bindning',\n    'ldap_fail_authed' => 'LDAP-inloggning misslyckades med angivna dn- och lösenordsuppgifter',\n    'ldap_extension_not_installed' => 'LDAP PHP-tillägg inte installerat',\n    'ldap_cannot_connect' => 'Kan inte ansluta till ldap-servern. Anslutningen misslyckades',\n    'saml_already_logged_in' => 'Redan inloggad',\n    'saml_no_email_address' => 'Kunde inte hitta en e-postadress för den här användaren i data som tillhandahålls av det externa autentiseringssystemet',\n    'saml_invalid_response_id' => 'En begäran från det externa autentiseringssystemet känns inte igen av en process som startats av denna applikation. Att navigera bakåt efter en inloggning kan orsaka detta problem.',\n    'saml_fail_authed' => 'Inloggning med :system misslyckades, systemet godkände inte auktoriseringen',\n    'oidc_already_logged_in' => 'Redan inloggad',\n    'oidc_no_email_address' => 'Kunde inte hitta en e-postadress för den här användaren i den data som tillhandahölls av det externa autentiseringssystemet',\n    'oidc_fail_authed' => 'Inloggning med :system misslyckades, systemet presenterade inte en godkänd auktorisering',\n    'social_no_action_defined' => 'Ingen åtgärd definierad',\n    'social_login_bad_response' => \"Ett fel inträffade vid inloggning genom :socialAccount: \\n:error\",\n    'social_account_in_use' => 'Detta konto från :socialAccount används redan. Testa att logga in med :socialAccount istället.',\n    'social_account_email_in_use' => 'E-posten :email används redan. Om du redan har ett konto kan du ansluta ditt konto från :socialAccount via dina profilinställningar.',\n    'social_account_existing' => 'Detta konto från :socialAccount är redan länkat till din profil.',\n    'social_account_already_used_existing' => 'Detta konto från :socialAccount används redan av en annan användare.',\n    'social_account_not_used' => 'Detta konto från :socialAccount är inte länkat till någon användare. Vänligen anslut via dina profilinställningar. ',\n    'social_account_register_instructions' => 'Om du inte har något konto ännu kan du registerar dig genom att välja :socialAccount.',\n    'social_driver_not_found' => 'Drivrutinen för den här tjänsten hittades inte',\n    'social_driver_not_configured' => 'Dina inställningar för :socialAccount är inte korrekta.',\n    'invite_token_expired' => 'Denna inbjudningslänk har löpt ut. Du kan istället försöka återställa ditt kontos lösenord.',\n    'login_user_not_found' => 'En användare för denna åtgärd kunde inte hittas.',\n\n    // System\n    'path_not_writable' => 'Kunde inte ladda upp till sökvägen :filePath. Kontrollera att webbservern har skrivåtkomst.',\n    'cannot_get_image_from_url' => 'Kan inte hämta bild från :url',\n    'cannot_create_thumbs' => 'Servern kan inte skapa miniatyrer. Kontrollera att du har PHPs GD-tillägg aktiverat.',\n    'server_upload_limit' => 'Servern tillåter inte så här stora filer. Prova en mindre fil.',\n    'server_post_limit' => 'Servern kan inte ta emot den angivna mängden data. Försök igen med mindre data eller en mindre fil.',\n    'uploaded'  => 'Servern tillåter inte så här stora filer. Prova en mindre fil.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Ett fel inträffade vid uppladdningen',\n    'image_upload_type_error' => 'Filtypen du försöker ladda upp är ogiltig',\n    'image_upload_replace_type' => 'Bilder som skall ersättas måste vara av samma filtyp',\n    'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.',\n    'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.',\n    'image_gallery_thumbnail_memory_limit' => 'Misslyckades att skapa galleriminiatyrer på grund av otillräckliga systemresurser.',\n    'drawing_data_not_found' => 'Drawing data could not be loaded. The drawing file might no longer exist or you may not have permission to access it.',\n\n    // Attachments\n    'attachment_not_found' => 'Bilagan hittades ej',\n    'attachment_upload_error' => 'An error occurred uploading the attachment file',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Kunde inte spara utkastet. Kontrollera att du är ansluten till internet.',\n    'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content',\n    'page_custom_home_deletion' => 'Det går inte att ta bort sidan medan den används som startsida',\n\n    // Entities\n    'entity_not_found' => 'Innehållet hittades inte',\n    'bookshelf_not_found' => 'Hyllan hittades inte',\n    'book_not_found' => 'Boken hittades inte',\n    'page_not_found' => 'Sidan hittades inte',\n    'chapter_not_found' => 'Kapitlet hittades inte',\n    'selected_book_not_found' => 'Den valda boken hittades inte',\n    'selected_book_chapter_not_found' => 'Den valda boken eller kapitlet hittades inte',\n    'guests_cannot_save_drafts' => 'Gäster kan inte spara utkast',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Du kan inte ta bort den enda admin-användaren',\n    'users_cannot_delete_guest' => 'Du kan inte ta bort gästanvändaren',\n    'users_could_not_send_invite' => 'Could not create user since invite email failed to send',\n\n    // Roles\n    'role_cannot_be_edited' => 'Den här rollen kan inte redigeras',\n    'role_system_cannot_be_deleted' => 'Det här är en systemroll och kan därför inte tas bort',\n    'role_registration_default_cannot_delete' => 'Det går inte att ta bort rollen medan den används som standardroll.',\n    'role_cannot_remove_only_admin' => 'Detta är den enda användaren med administratörsroll. Gör någon annan användare till administratör innan du tar bort denna.',\n\n    // Comments\n    'comment_list' => 'Ett fel inträffade då kommentarer skulle hämtas.',\n    'cannot_add_comment_to_draft' => 'Du kan inte kommentera ett utkast.',\n    'comment_add' => 'Ett fel inträffade då kommentaren skulle sparas.',\n    'comment_delete' => 'Ett fel inträffade då kommentaren skulle tas bort.',\n    'empty_comment' => 'Kan inte lägga till en tom kommentar.',\n\n    // Error pages\n    '404_page_not_found' => 'Sidan hittades inte',\n    'sorry_page_not_found' => 'Tyvärr gick det inte att hitta sidan du söker.',\n    'sorry_page_not_found_permission_warning' => 'Om du förväntade dig att denna sida skulle existera, kanske du inte har behörighet att se den.',\n    'image_not_found' => 'Bilden hittades inte',\n    'image_not_found_subtitle' => 'Tyvärr gick det inte att hitta bilden du letade efter.',\n    'image_not_found_details' => 'Om du förväntade dig att den här bilden skulle finnas kan den ha tagits bort.',\n    'return_home' => 'Återvänd till startsidan',\n    'error_occurred' => 'Ett fel inträffade',\n    'app_down' => ':appName är nere just nu',\n    'back_soon' => 'Vi är snart tillbaka.',\n\n    // Import\n    'import_zip_cant_read' => 'Kunde inte läsa ZIP-filen.',\n    'import_zip_cant_decode_data' => 'Kunde inte hitta och avkoda ZIP data.json innehåll.',\n    'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'ZIP-filen kunde inte valideras med fel:',\n    'import_zip_failed_notification' => 'Det gick inte att importera ZIP-fil.',\n    'import_perms_books' => 'Du saknar behörighet att skapa böcker.',\n    'import_perms_chapters' => 'Du saknar behörighet att skapa kapitel.',\n    'import_perms_pages' => 'Du saknar behörighet att skapa sidor.',\n    'import_perms_images' => 'Du saknar behörighet för att skapa bilder.',\n    'import_perms_attachments' => 'Du saknar behörighet att skapa bilagor.',\n\n    // API errors\n    'api_no_authorization_found' => 'Ingen auktoriseringstoken hittades på denna begäran',\n    'api_bad_authorization_format' => 'En auktoriseringstoken hittades på denna begäran men formatet verkade felaktigt',\n    'api_user_token_not_found' => 'Ingen matchande API-token hittades för den angivna auktoriseringstoken',\n    'api_incorrect_token_secret' => 'Hemligheten för den angivna API-token är felaktig',\n    'api_user_no_api_permission' => 'Ägaren av den använda API-token har inte behörighet att göra API-anrop',\n    'api_user_token_expired' => 'Den använda auktoriseringstoken har löpt ut',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Ett fel uppstod när ett test mail skulle skickas:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'URL matchar inte de konfigurerade tillåtna SSR-värdarna',\n];\n"
  },
  {
    "path": "lang/sv/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Ny kommentar på sidan: :pageName',\n    'new_comment_intro' => 'En användare har kommenterat en sida i :appName:',\n    'new_page_subject' => 'Ny sida: :pageName',\n    'new_page_intro' => 'En ny sida har blivit skapad i :appName:',\n    'updated_page_subject' => 'Uppdaterad sida: :pageName',\n    'updated_page_intro' => 'En sida har blivit uppdaterad i :appName:',\n    'updated_page_debounce' => 'För att förhindra en massa notiser, så kommer det inte skickas nya notiser på ett tag för ytterligare ändringar till denna sida av samma skribent.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Sidonamn:',\n    'detail_page_path' => 'Sidosökväg:',\n    'detail_commenter' => 'Kommentars-skapare:',\n    'detail_comment' => 'Kommentar:',\n    'detail_created_by' => 'Skapad av:',\n    'detail_updated_by' => 'Uppdaterad av:',\n\n    'action_view_comment' => 'Visa kommentar',\n    'action_view_page' => 'Visa sida',\n\n    'footer_reason' => 'Detta meddelande skickades till dig eftersom :link täcker denna typ av aktivitet för detta objekt.',\n    'footer_reason_link' => 'dina notifikationspreferenser',\n];\n"
  },
  {
    "path": "lang/sv/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Föregående',\n    'next'     => 'Nästa &raquo;',\n\n];\n"
  },
  {
    "path": "lang/sv/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Lösenord måste vara minst sex tecken långa och anges likadant två gånger.',\n    'user' => \"Det finns ingen användare med den e-postadressen.\",\n    'token' => 'Lösenordsåterställningstoken är ogiltig för denna e-postadress.',\n    'sent' => 'Vi har mailat dig en länk för att återställa ditt lösenord!',\n    'reset' => 'Ditt lösenord har blivit återställt!',\n\n];\n"
  },
  {
    "path": "lang/sv/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Mitt Konto',\n\n    'shortcuts' => 'Genvägar',\n    'shortcuts_interface' => 'UI Shortcut Preferences',\n    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',\n    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',\n    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',\n    'shortcuts_section_navigation' => 'Navigation',\n    'shortcuts_section_actions' => 'Common Actions',\n    'shortcuts_save' => 'Spara genvägar',\n    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing \"?\" which will highlight the available shortcuts for actions currently visible on the screen.',\n    'shortcuts_update_success' => 'Shortcut preferences have been updated!',\n    'shortcuts_overview_desc' => 'Manage keyboard shortcuts you can use to navigate the system user interface.',\n\n    'notifications' => 'Notification Preferences',\n    'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.',\n    'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own',\n    'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Notify upon replies to my comments',\n    'notifications_save' => 'Save Preferences',\n    'notifications_update_success' => 'Notification preferences have been updated!',\n    'notifications_watched' => 'Watched & Ignored Items',\n    'notifications_watched_desc' => 'Below are the items that have custom watch preferences applied. To update your preferences for these, view the item then find the watch options in the sidebar.',\n\n    'auth' => 'Access & Security',\n    'auth_change_password' => 'Change Password',\n    'auth_change_password_desc' => 'Change the password you use to log-in to the application. This must be at least 8 characters long.',\n    'auth_change_password_success' => 'Lösenordet har uppdaterats!',\n\n    'profile' => 'Profildetaljer',\n    'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.',\n    'profile_view_public' => 'Visa publik profil',\n    'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.',\n    'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.',\n    'profile_email_no_permission' => 'Unfortunately you don\\'t have permission to change your email address. If you want to change this, you\\'d need to ask an administrator to change this for you.',\n    'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.',\n    'profile_admin_options' => 'Administrator Options',\n    'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the \"Settings > Users\" area of the application.',\n\n    'delete_account' => 'Radera konto',\n    'delete_my_account' => 'Radera mitt konto',\n    'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\\'ve created, such as created pages and uploaded images, will remain.',\n    'delete_my_account_warning' => 'Are you sure you want to delete your account?',\n];\n"
  },
  {
    "path": "lang/sv/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Inställningar',\n    'settings_save' => 'Spara inställningar',\n    'system_version' => 'Systemversion',\n    'categories' => 'Kategorier',\n\n    // App Settings\n    'app_customization' => 'Sidanpassning',\n    'app_features_security' => 'Funktioner och säkerhet',\n    'app_name' => 'Applikationsnamn',\n    'app_name_desc' => 'Namnet visas i sidhuvdet och i eventuella mail.',\n    'app_name_header' => 'Visa applikationsnamn i sidhuvudet?',\n    'app_public_access' => 'Offentlig åtkomst',\n    'app_public_access_desc' => 'Om du aktiverar detta alternativ låter du icke inloggade besökare komma åt innehåll på din sida',\n    'app_public_access_desc_guest' => 'Åtkomst för icke inloggade besökare kan styras via användaren \"Guest\".',\n    'app_public_access_toggle' => 'Tillåt offentlig åtkomst',\n    'app_public_viewing' => 'Tillåt publikt innehåll?',\n    'app_secure_images' => 'Aktivera högre säkerhet för bilduppladdningar?',\n    'app_secure_images_toggle' => 'Aktivera säkrare bilduppladdningar',\n    'app_secure_images_desc' => 'Av prestandaskäl är alla bilder publika. Det här alternativet lägger till en slumpmässig, svårgissad sträng framför alla bild-URL:er. Se till att kataloglistning inte är aktivt för att förhindra åtkomst.',\n    'app_default_editor' => 'Standard-sidredigerare',\n    'app_default_editor_desc' => 'Välj vilken redigerare som ska användas som standard vid redigering av nya sidor. Detta kan ändras enskilt för sidor som tillåter det.',\n    'app_custom_html' => 'Anpassat HTML-huvudinnehåll',\n    'app_custom_html_desc' => 'Innehåll i det här fältet placeras längst ner i <head>-sektionen på varje sida. Detta kan användas för att skriva över stilmallar eller lägga in spårningskoder.',\n    'app_custom_html_disabled_notice' => 'Anpassat innehåll i HTML-huvudet är inaktiverat på denna inställningssida för att säkerställa att eventuella ändringar som påverkar funktionaliteten kan återställas.',\n    'app_logo' => 'Applikationslogotyp',\n    'app_logo_desc' => 'Detta används bland annat i applikationshuvudet. Bilden bör vara 86 pixlar i höjd. Stora bilder skalas ned.',\n    'app_icon' => 'Applikationsikon',\n    'app_icon_desc' => 'Den här ikonen syns i webbläsarens flikar, bokmärken och genvägar. Den skall vara 256x256 pixlar i PNG format.',\n    'app_homepage' => 'Startsida',\n    'app_homepage_desc' => 'Välj en vy att använda som startsida istället för standardvyn. Behörigheter för valda sidor kommer att ignoreras.',\n    'app_homepage_select' => 'Välj en sida',\n    'app_footer_links' => 'Sidfotslänkar',\n    'app_footer_links_desc' => 'Lägg till länkar som visas i sidfoten. Dessa kommer att visas längst ner på de flesta sidor, inklusive de som inte kräver inloggning. Du kan använda en etikett av \"trans::<key>\" för att använda systemdefinierade översättningar. Exempelvis översätts \"trans::common.privacy_policy\" till \"Integritetspolicy\" och \"trans::common.terms_of_service\" till \"Användarvillkor\".',\n    'app_footer_links_label' => 'Länketikett',\n    'app_footer_links_url' => 'Länk URL',\n    'app_footer_links_add' => 'Lägg till sidfotslänk',\n    'app_disable_comments' => 'Inaktivera kommentarer',\n    'app_disable_comments_toggle' => 'Inaktivera kommentarer',\n    'app_disable_comments_desc' => 'Inaktivera kommentarer på alla sidor i applikationen. Befintliga kommentarer visas inte.',\n\n    // Color settings\n    'color_scheme' => 'Programmets färgschema',\n    'color_scheme_desc' => 'Ställ in de färger som ska användas i applikationens användargränssnitt. Färger kan konfigureras separat för mörka och ljusa lägen för att bäst passa temat och säkerställa läsbarhet.',\n    'ui_colors_desc' => 'Ange applikationens primära färg och standard färg för länkar. Den primära färgen används främst för huvudrubriken, knappar och gränssnitt dekorationer. Standardfärgen på länken används för textbaserade länkar och åtgärder, både inom skriftligt innehåll och i applikationsgränssnittet.',\n    'app_color' => 'Primärfärg',\n    'link_color' => 'Standardfärg för länkar',\n    'content_colors_desc' => 'Ange färger för alla element i sidans organisationshierarki. Det rekommenderas att välja färger med liknande ljusstyrka som standardfärgerna för att bibehålla läsbarheten.',\n    'bookshelf_color' => 'Hyllfärg',\n    'book_color' => 'Bokfärg',\n    'chapter_color' => 'Kapitelfärg',\n    'page_color' => 'Sidfärg',\n    'page_draft_color' => 'Färg på sidutkast',\n\n    // Registration Settings\n    'reg_settings' => 'Registrering',\n    'reg_enable' => 'Aktivera registrering',\n    'reg_enable_toggle' => 'Aktivera registrering',\n    'reg_enable_desc' => 'När registrering är aktiverad kan användaren själv registrera sig och logga in i applikationen. Vid registrering tilldelas de en förvald användarroll.',\n    'reg_default_role' => 'Förvald användarroll efter registrering',\n    'reg_enable_external_warning' => 'Alternativet ovan ignoreras när extern LDAP- eller SAML-autentisering är aktiverad. Användarkonton för icke-existerande medlemmar kommer att skapas automatiskt, om autentisering mot det externa system som används lyckas.',\n    'reg_email_confirmation' => 'E-postbekräftelse',\n    'reg_email_confirmation_toggle' => 'Kräv e-postbekräftelse',\n    'reg_confirm_email_desc' => 'Om domänbegränsning används kommer e-postbekräftelse att krävas och detta alternativ kommer att ignoreras.',\n    'reg_confirm_restrict_domain' => 'Begränsning av domän',\n    'reg_confirm_restrict_domain_desc' => 'Ange en kommaseparerad lista över e-postdomäner som du vill begränsa registrering för. Användare kommer att få ett e-postmeddelande för att bekräfta sin e-postadress innan de tillåts att logga in. <br> Notera att användare kommer att kunna ändra sin e-postadress efter lyckad registrering.',\n    'reg_confirm_restrict_domain_placeholder' => 'Ingen begränsning inställd',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Välj standard sorteringsregel som skall tillämpas på nya böcker. Detta påverkar inte befintliga böcker och kan åsidosättas per bok.',\n    'sorting_rules' => 'Sorteringsregler',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Tilldelad till :count bok|Tilldelad till :count böcker',\n    'sort_rule_create' => 'Skapa sorteringsregel',\n    'sort_rule_edit' => 'Redigera sorteringsregel',\n    'sort_rule_delete' => 'Ta bort sorteringsregel',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Detaljer för sorteringsregler',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Tillgängliga åtgärder',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Namn - Alfabetisk ordning',\n    'sort_rule_op_name_numeric' => 'Namn - Numerisk ordning',\n    'sort_rule_op_created_date' => 'Datum skapat',\n    'sort_rule_op_updated_date' => 'Datum uppdaterat',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Underhåll',\n    'maint_image_cleanup' => 'Rensa bilder',\n    'maint_image_cleanup_desc' => 'Söker igenom innehåll i sidor & revisioner för att se vilka bilder och teckningar som är i bruk och vilka som är överflödiga. Se till att ta en komplett backup av databas och bilder innan du kör detta.',\n    'maint_delete_images_only_in_revisions' => 'Ta också bort bilder som bara finns i gamla sidrevideringar',\n    'maint_image_cleanup_run' => 'Kör rensning',\n    'maint_image_cleanup_warning' => 'Hittade :count bilder som potentiellt inte används. Vill du verkligen ta bort dessa bilder?',\n    'maint_image_cleanup_success' => 'Hittade och raderade :count bilder som potentiellt inte används!',\n    'maint_image_cleanup_nothing_found' => 'Hittade inga oanvända bilder, så inget har raderats!',\n    'maint_send_test_email' => 'Skicka ett testmail',\n    'maint_send_test_email_desc' => 'Detta skickar ett testmeddelande till den e-postadress som anges i din profil.',\n    'maint_send_test_email_run' => 'Skicka testmail',\n    'maint_send_test_email_success' => 'E-post skickat till :address',\n    'maint_send_test_email_mail_subject' => 'Testmejl',\n    'maint_send_test_email_mail_greeting' => 'E-postleverans verkar fungera!',\n    'maint_send_test_email_mail_text' => 'Grattis! Eftersom du fick detta e-postmeddelande verkar dina e-postinställningar vara korrekt konfigurerade.',\n    'maint_recycle_bin_desc' => 'Borttagna hyllor, böcker, kapitel & sidor skickas till papperskorgen så att de kan återställas eller raderas permanent. Äldre objekt i papperskorgen kan automatiskt tas bort efter ett tag beroende på systemkonfiguration.',\n    'maint_recycle_bin_open' => 'Öppna papperskorgen',\n    'maint_regen_references' => 'Regenerera referenser',\n    'maint_regen_references_desc' => 'Den här åtgärden kommer att bygga om referensindex för kopplade objekt i databasen. Detta hanteras vanligtvis automatiskt, men denna åtgärd kan vara användbar för att indexera gammalt innehåll eller innehåll som lagts till via inofficiella metoder.',\n    'maint_regen_references_success' => 'Referensindex har regenererats!',\n    'maint_timeout_command_note' => 'Obs: Denna åtgärd kan ta tid att köra, vilket kan leda till timeoutproblem i vissa webbmiljöer. Som ett alternativ kan denna åtgärd utföras med ett terminalkommando.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Papperskorgen',\n    'recycle_bin_desc' => 'Här kan du återställa objekt som har tagits bort eller välja att permanent ta bort dem från systemet. Denna lista är ofiltrerad till skillnad från liknande aktivitetslistor i systemet där behörighetsfilter tillämpas.',\n    'recycle_bin_deleted_item' => 'Raderat objekt',\n    'recycle_bin_deleted_parent' => 'Överordnad',\n    'recycle_bin_deleted_by' => 'Borttagen av',\n    'recycle_bin_deleted_at' => 'Tid för borttagning',\n    'recycle_bin_permanently_delete' => 'Radera permanent',\n    'recycle_bin_restore' => 'Återställ',\n    'recycle_bin_contents_empty' => 'Papperskorgen är för närvarande tom',\n    'recycle_bin_empty' => 'Töm papperskorgen',\n    'recycle_bin_empty_confirm' => 'Detta kommer permanent att förstöra alla objekt i papperskorgen inklusive innehåll som finns i varje objekt. Är du säker du vill tömma papperskorgen?',\n    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?',\n    'recycle_bin_destroy_list' => 'Objekt som ska förstöras',\n    'recycle_bin_restore_list' => 'Objekt som ska återställas',\n    'recycle_bin_restore_confirm' => 'Denna åtgärd kommer att återställa det raderade objektet, inklusive alla underordnade element, till deras ursprungliga plats. Om den ursprungliga platsen har tagits bort sedan dess, och är nu i papperskorgen, kommer det överordnade objektet också att behöva återställas.',\n    'recycle_bin_restore_deleted_parent' => 'Föräldern till det här objektet har också tagits bort. Dessa kommer att förbli raderade tills den förälder är återställd.',\n    'recycle_bin_restore_parent' => 'Återställ överordnad',\n    'recycle_bin_destroy_notification' => 'Raderade :count totala objekt från papperskorgen.',\n    'recycle_bin_restore_notification' => 'Återställt :count totala objekt från papperskorgen.',\n\n    // Audit Log\n    'audit' => 'Auditlogg',\n    'audit_desc' => 'Denna granskningslogg visar en lista över aktiviteter som spåras i systemet. Denna lista är ofiltrerad till skillnad från liknande aktivitetslistor i systemet där behörighetsfilter tillämpas.',\n    'audit_event_filter' => 'Händelse Filter',\n    'audit_event_filter_no_filter' => 'Inget filter',\n    'audit_deleted_item' => 'Raderat objekt',\n    'audit_deleted_item_name' => 'Namn: :name',\n    'audit_table_user' => 'Användare',\n    'audit_table_event' => 'Händelse',\n    'audit_table_related' => 'Relaterat objekt eller detalj',\n    'audit_table_ip' => 'IP-adress',\n    'audit_table_date' => 'Datum för senaste aktiviteten',\n    'audit_date_from' => 'Datumintervall från',\n    'audit_date_to' => 'Datumintervall till',\n\n    // Role Settings\n    'roles' => 'Roller',\n    'role_user_roles' => 'Användarroller',\n    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',\n    'roles_x_users_assigned' => ':count user assigned|:count users assigned',\n    'roles_x_permissions_provided' => ':count permission|:count permissions',\n    'roles_assigned_users' => 'Assigned Users',\n    'roles_permissions_provided' => 'Provided Permissions',\n    'role_create' => 'Skapa ny roll',\n    'role_delete' => 'Ta bort roll',\n    'role_delete_confirm' => 'Rollen med namn \\':roleName\\' kommer att tas bort.',\n    'role_delete_users_assigned' => 'Det finns :userCount användare som tillhör den här rollen. Om du vill migrera användarna från den här rollen, välj en ny roll nedan.',\n    'role_delete_no_migration' => \"Migrera inte användare\",\n    'role_delete_sure' => 'Är du säker på att du vill ta bort den här rollen?',\n    'role_edit' => 'Redigera roll',\n    'role_details' => 'Om rollen',\n    'role_name' => 'Rollens namn',\n    'role_desc' => 'Kort beskrivning av rollen',\n    'role_mfa_enforced' => 'Kräver multifaktorsautentisering',\n    'role_external_auth_id' => 'Externa autentiserings-ID:n',\n    'role_system' => 'Systemrättigheter',\n    'role_manage_users' => 'Hanter användare',\n    'role_manage_roles' => 'Hantera roller & rättigheter',\n    'role_manage_entity_permissions' => 'Hantera rättigheter för alla böcker, kapitel och sidor',\n    'role_manage_own_entity_permissions' => 'Hantera rättigheter för egna böcker, kapitel och sidor',\n    'role_manage_page_templates' => 'Hantera mallar',\n    'role_access_api' => 'Åtkomst till systemets API',\n    'role_manage_settings' => 'Hantera appinställningar',\n    'role_export_content' => 'Exportera innehåll',\n    'role_import_content' => 'Import content',\n    'role_editor_change' => 'Ändra sidredigerare',\n    'role_notifications' => 'Receive & manage notifications',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Tillgång till innehåll',\n    'roles_system_warning' => 'Var medveten om att åtkomst till någon av ovanstående tre behörigheter kan tillåta en användare att ändra sina egna rättigheter eller andras rättigheter i systemet. Tilldela endast roller med dessa behörigheter till betrodda användare.',\n    'role_asset_desc' => 'Det här är standardinställningarna för allt innehåll i systemet. Eventuella anpassade rättigheter på böcker, kapitel och sidor skriver över dessa inställningar.',\n    'role_asset_admins' => 'Administratörer har automatisk tillgång till allt innehåll men dessa alternativ kan visa och dölja vissa gränssnittselement',\n    'role_asset_image_view_note' => 'Detta avser synlighet inom bildhanteraren. Faktisk åtkomst för uppladdade bildfiler kommer att bero på alternativ för bildlagring.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'Alla',\n    'role_own' => 'Egna',\n    'role_controlled_by_asset' => 'Kontrolleras av den sida de laddas upp till',\n    'role_save' => 'Spara roll',\n    'role_users' => 'Användare med denna roll',\n    'role_users_none' => 'Inga användare tillhör den här rollen',\n\n    // Users\n    'users' => 'Användare',\n    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',\n    'user_profile' => 'Användarprofil',\n    'users_add_new' => 'Lägg till användare',\n    'users_search' => 'Sök användare',\n    'users_latest_activity' => 'Senaste aktivitet',\n    'users_details' => 'Användarinformation',\n    'users_details_desc' => 'Ange ett visningsnamn och en e-postadress för den här användaren. E-postadressen kommer att användas vid inloggningen.',\n    'users_details_desc_no_email' => 'Ange ett visningsnamn för den här användaren så att andra kan känna igen den.',\n    'users_role' => 'Användarroller',\n    'users_role_desc' => 'Välj vilka roller den här användaren ska tilldelas. Om en användare har tilldelats flera roller kommer behörigheterna från dessa roller att staplas och de kommer att få alla rättigheter i de tilldelade rollerna.',\n    'users_password' => 'Användarlösenord',\n    'users_password_desc' => 'Ange ett lösenord som ska användas för att logga in på sidan. Lösenordet måste vara minst 8 tecken långt.',\n    'users_send_invite_text' => 'Du kan välja att skicka denna användare ett e-postmeddelande som tillåter dem att ställa in sitt eget lösenord, eller så kan du ställa in deras lösenord själv.',\n    'users_send_invite_option' => 'Skicka e-post med inbjudan',\n    'users_external_auth_id' => 'Externt ID för autentisering',\n    'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.',\n    'users_password_warning' => 'Only fill the below if you would like to change the password for this user.',\n    'users_system_public' => 'Den här användaren representerar eventuella gäster som använder systemet. Den kan inte användas för att logga in utan tilldeles automatiskt.',\n    'users_delete' => 'Ta bort användare',\n    'users_delete_named' => 'Ta bort användaren :userName',\n    'users_delete_warning' => 'Detta kommer att ta bort användaren \\':userName\\' från systemet helt och hållet.',\n    'users_delete_confirm' => 'Är du säker på att du vill ta bort användaren?',\n    'users_migrate_ownership' => 'Överför ägarskap',\n    'users_migrate_ownership_desc' => 'Välj en användare här om du vill att en annan användare ska bli ägare till alla objekt som för närvarande ägs av denna användare.',\n    'users_none_selected' => 'Ingen användare vald',\n    'users_edit' => 'Redigera användare',\n    'users_edit_profile' => 'Redigera profil',\n    'users_avatar' => 'Avatar',\n    'users_avatar_desc' => 'Bilden bör vara kvadratisk och ca 256px stor.',\n    'users_preferred_language' => 'Föredraget språk',\n    'users_preferred_language_desc' => 'Det här alternativet kommer att ändra det språk som används i användargränssnittet. Detta påverkar inget användarskapat innehåll.',\n    'users_social_accounts' => 'Anslutna konton',\n    'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.',\n    'users_social_accounts_info' => 'Här kan du ansluta dina andra konton för snabbare och smidigare inloggning. Om du kopplar från en tjänst här kommer de behörigheter som tidigare givits inte att tas bort - ta bort behörigheter genom att logga in på ditt konto på tjänsten i fråga.',\n    'users_social_connect' => 'Anslut konto',\n    'users_social_disconnect' => 'Koppla från konto',\n    'users_social_status_connected' => 'Ansluten',\n    'users_social_status_disconnected' => 'Bortkopplad',\n    'users_social_connected' => ':socialAccount har kopplats till ditt konto.',\n    'users_social_disconnected' => ':socialAccount har kopplats bort från ditt konto.',\n    'users_api_tokens' => 'API-nyckel',\n    'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.',\n    'users_api_tokens_none' => 'Inga API-tokens har skapats för den här användaren',\n    'users_api_tokens_create' => 'Skapa token',\n    'users_api_tokens_expires' => 'Förfaller',\n    'users_api_tokens_docs' => 'API-dokumentation',\n    'users_mfa' => 'Multifaktorautentisering',\n    'users_mfa_desc' => 'Konfigurera multifaktorsautentisering som ett extra skydd för ditt konto.',\n    'users_mfa_x_methods' => ':count metod konfigurerad|:count metoder konfigurerade',\n    'users_mfa_configure' => 'Konfigurera metoder',\n\n    // API Tokens\n    'user_api_token_create' => 'Skapa API-nyckel',\n    'user_api_token_name' => 'Namn',\n    'user_api_token_name_desc' => 'Ge din token ett läsbart namn som en framtida påminnelse om dess avsedda syfte.',\n    'user_api_token_expiry' => 'Förfallodatum',\n    'user_api_token_expiry_desc' => 'Ange ett datum då denna token går ut. Efter detta datum kommer förfrågningar som görs med denna token inte längre att fungera. Lämnar du detta fält tomt kommer utgångsdatum att sättas 100 år in i framtiden.',\n    'user_api_token_create_secret_message' => 'Omedelbart efter att du skapat denna token kommer ett \"Token ID\" & \"Token Secret\" att genereras och visas. Token Secret kommer bara att visas en enda gång så se till att kopiera värdet till en säker plats innan du fortsätter.',\n    'user_api_token' => 'API-nyckel',\n    'user_api_token_id' => 'Token ID',\n    'user_api_token_id_desc' => 'Detta är en icke-redigerbar systemgenererad identifierare för denna token som måste tillhandahållas i API-förfrågningar.',\n    'user_api_token_secret' => 'Token Secret',\n    'user_api_token_secret_desc' => 'Detta är en systemgenererad hemlighet för denna token som måste tillhandahållas i API-förfrågningar. Denna kommer bara att visas en gång så kopiera detta värde till en säker plats.',\n    'user_api_token_created' => 'Token skapad :timeAgo',\n    'user_api_token_updated' => 'Token Uppdaterad :timeAgo',\n    'user_api_token_delete' => 'Ta bort token',\n    'user_api_token_delete_warning' => 'Detta kommer att helt ta bort denna API-token med namnet \\':tokenName\\' från systemet.',\n    'user_api_token_delete_confirm' => 'Är du säker på att du vill ta bort denna API-token?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',\n    'webhooks_x_trigger_events' => ':count trigger event|:count trigger events',\n    'webhooks_create' => 'Skapa ny webhook',\n    'webhooks_none_created' => 'Inga webhooks har skapats än.',\n    'webhooks_edit' => 'Redigera webhook',\n    'webhooks_save' => 'Spara webhook',\n    'webhooks_details' => 'Webhook-detaljer',\n    'webhooks_details_desc' => 'Ange ett användarvänligt namn och en POST-slutpunkt dit data för denna webhook ska skickas.',\n    'webhooks_events' => 'Webhook-händelser',\n    'webhooks_events_desc' => 'Välja alla händelser som ska trigga ett anrop av denna webhook.',\n    'webhooks_events_warning' => 'Tänk på att dessa händelser kommer att triggas för alla valda händelser, även om anpassade behörigheter tillämpas. Se till att användningen av denna webhook inte avslöjar konfidentiellt innehåll.',\n    'webhooks_events_all' => 'Alla systemhändelser',\n    'webhooks_name' => 'Namn på webhook',\n    'webhooks_timeout' => 'Tidsgräns för webhook överskreds (sekunder)',\n    'webhooks_endpoint' => 'Webhook-slutpunkt',\n    'webhooks_active' => 'Webhook aktiv',\n    'webhook_events_table_header' => 'Händelser',\n    'webhooks_delete' => 'Ta bort webhook',\n    'webhooks_delete_warning' => 'Detta kommer att ta bort följande webhook från systemet: \\':webhookName\\'.',\n    'webhooks_delete_confirm' => 'Är du säker på att du vill ta bort denna webhook?',\n    'webhooks_format_example' => 'Exempel på Webhook-format',\n    'webhooks_format_example_desc' => 'Webhook-data skickas som en POST-begäran till den konfigurerade slutpunkten enligt nedanstående JSON-format. Egenskaperna \"related_item\" och \"url\" är valfria samt beroende av den utlösta händelsens typ.',\n    'webhooks_status' => 'Webhook-status',\n    'webhooks_last_called' => 'Senast anropad:',\n    'webhooks_last_errored' => 'Senast felande:',\n    'webhooks_last_error_message' => 'Senaste felmeddelande:',\n\n    // Licensing\n    'licenses' => 'Licenser',\n    'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.',\n    'licenses_bookstack' => 'BookStack licens',\n    'licenses_php' => 'Licenser för PHP-bibliotek',\n    'licenses_js' => 'Licenser för JavaScript-bibliotek',\n    'licenses_other' => 'Andra licenser',\n    'license_details' => 'Licensinformation',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Katalanska',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Danska',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenska',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/sv/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute måste godkännas.',\n    'active_url'           => ':attribute är inte en giltig URL.',\n    'after'                => ':attribute måste vara efter :date.',\n    'alpha'                => ':attribute får bara innehålla bokstäver.',\n    'alpha_dash'           => ':attribute får bara innehålla bokstäver, siffror och bindestreck.',\n    'alpha_num'            => ':attribute får bara innehålla bokstäver och siffror.',\n    'array'                => ':attribute måste vara en array.',\n    'backup_codes'         => 'Den angivna koden är inte giltig eller har redan använts.',\n    'before'               => ':attribute måste vara före :date.',\n    'between'              => [\n        'numeric' => ':attribute måste vara mellan :min och :max.',\n        'file'    => ':attribute måste vara mellan :min och :max kilobyte stor.',\n        'string'  => ':attribute måste vara mellan :min och :max tecken.',\n        'array'   => ':attribute måste innehålla mellan :min och :max poster.',\n    ],\n    'boolean'              => ':attribute måste vara sant eller falskt.',\n    'confirmed'            => 'Bekräftelsen av :attribute stämmer inte.',\n    'date'                 => ':attribute är inte ett giltigt datum.',\n    'date_format'          => ':attribute matchar inte formatet :format.',\n    'different'            => ':attribute och :other måste vara olika.',\n    'digits'               => ':attribute måste vara :digits siffror.',\n    'digits_between'       => ':attribute måste vara mellan :min och :max siffror.',\n    'email'                => ':attribute måste vara en giltig e-postadress.',\n    'ends_with' => ':attribute måste sluta med något av följande: :values',\n    'file'                 => ':attribute måste anges som en giltig fil.',\n    'filled'               => ':attribute är obligatoriskt.',\n    'gt'                   => [\n        'numeric' => ':attribute måste vara större än :value.',\n        'file'    => ':attribute måste vara större än :value kilobytes.',\n        'string'  => ':attribute måste vara större än :value tecken.',\n        'array'   => ':attribute måste ha mer än :value objekt.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute måste vara större än eller likamed :value.',\n        'file'    => ':attribute måste vara större än eller lika med :value kilobytes.',\n        'string'  => ':attribute måste vara större än eller lika med :value tecken.',\n        'array'   => ':attribute måste ha :value objekt eller mer.',\n    ],\n    'exists'               => 'Valt värde för :attribute är ogiltigt.',\n    'image'                => ':attribute måste vara en bild.',\n    'image_extension'      => ':attribute måste ha ett giltigt filtillägg.',\n    'in'                   => 'Vald :attribute är ogiltigt.',\n    'integer'              => ':attribute måste vara en integer.',\n    'ip'                   => ':attribute måste vara en giltig IP-adress.',\n    'ipv4'                 => ':attribute måste vara en giltig IPv4-adress.',\n    'ipv6'                 => ':attribute måste vara en giltig IPv6-adress.',\n    'json'                 => ':attribute måste vara en giltig JSON-sträng.',\n    'lt'                   => [\n        'numeric' => ':attribute måste vara mindre än :value.',\n        'file'    => ':attribute måste vara mindre än :value kilobytes.',\n        'string'  => ':attribute måste vara mindre än :value tecken.',\n        'array'   => ':attribute måste ha mindre än :value objekt.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute måste vara mindre än eller lika :value.',\n        'file'    => ':attribute måste vara mindre än eller lika med :value kilobytes.',\n        'string'  => ':attribute måste vara mindre än eller lika med :value tecken.',\n        'array'   => ':attribute får inte innehålla mer än :max objekt.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute får inte vara större än :max.',\n        'file'    => ':attribute får inte vara större än :max kilobyte.',\n        'string'  => ':attribute får inte vara längre än :max tecken.',\n        'array'   => ':attribute får inte ha fler än :max poster.',\n    ],\n    'mimes'                => ':attribute måste vara en fil av typen: :values.',\n    'min'                  => [\n        'numeric' => ':attribute måste vara minst :min.',\n        'file'    => ':attribute måste vara minst :min kilobyte stor.',\n        'string'  => ':attribute måste vara minst :min tecken.',\n        'array'   => ':attribute måste ha minst :min poster.',\n    ],\n    'not_in'               => 'Vald :attribute är inte giltig',\n    'not_regex'            => 'Formatet på :attribute är ogiltigt.',\n    'numeric'              => ':attribute måste vara ett nummer.',\n    'regex'                => ':attribute har ett ogiltigt format.',\n    'required'             => ':attribute är obligatoriskt.',\n    'required_if'          => ':attribute är obligatoriskt när :other är :value.',\n    'required_with'        => ':attribute är obligatoriskt när :values finns.',\n    'required_with_all'    => ':attribute är obligatoriskt när :values finns.',\n    'required_without'     => ':attribute är obligatoriskt när :values inte finns.',\n    'required_without_all' => ':attribute är obligatirskt när ingen av :values finns.',\n    'same'                 => ':attribute och :other måste stämma överens.',\n    'safe_url'             => 'Den angivna länken kanske inte är säker.',\n    'size'                 => [\n        'numeric' => ':attribute måste vara :size.',\n        'file'    => ':attribute måste vara :size kilobyte.',\n        'string'  => ':attribute måste vara :size tecken.',\n        'array'   => ':attribute måste innehålla :size poster.',\n    ],\n    'string'               => ':attribute måste vara en sträng.',\n    'timezone'             => ':attribute måste vara en giltig tidszon.',\n    'totp'                 => 'Den angivna koden är inte giltig eller har löpt ut.',\n    'unique'               => ':attribute är upptaget',\n    'url'                  => 'Formatet på :attribute är ogiltigt.',\n    'uploaded'             => 'Filen kunde inte laddas upp. Servern kanske inte tillåter filer med denna storlek.',\n\n    'zip_file' => 'The :attribute needs to reference a file within the ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',\n    'zip_model_expected' => 'Data object expected but \":type\" found.',\n    'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Lösenordet måste bekräftas',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/tk/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'created page',\n    'page_create_notification'    => 'Page successfully created',\n    'page_update'                 => 'updated page',\n    'page_update_notification'    => 'Page successfully updated',\n    'page_delete'                 => 'deleted page',\n    'page_delete_notification'    => 'Page successfully deleted',\n    'page_restore'                => 'restored page',\n    'page_restore_notification'   => 'Page successfully restored',\n    'page_move'                   => 'moved page',\n    'page_move_notification'      => 'Page successfully moved',\n\n    // Chapters\n    'chapter_create'              => 'created chapter',\n    'chapter_create_notification' => 'Chapter successfully created',\n    'chapter_update'              => 'updated chapter',\n    'chapter_update_notification' => 'Chapter successfully updated',\n    'chapter_delete'              => 'deleted chapter',\n    'chapter_delete_notification' => 'Chapter successfully deleted',\n    'chapter_move'                => 'moved chapter',\n    'chapter_move_notification' => 'Chapter successfully moved',\n\n    // Books\n    'book_create'                 => 'created book',\n    'book_create_notification'    => 'Book successfully created',\n    'book_create_from_chapter'              => 'converted chapter to book',\n    'book_create_from_chapter_notification' => 'Chapter successfully converted to a book',\n    'book_update'                 => 'updated book',\n    'book_update_notification'    => 'Book successfully updated',\n    'book_delete'                 => 'deleted book',\n    'book_delete_notification'    => 'Book successfully deleted',\n    'book_sort'                   => 'sorted book',\n    'book_sort_notification'      => 'Book successfully re-sorted',\n\n    // Bookshelves\n    'bookshelf_create'            => 'created shelf',\n    'bookshelf_create_notification'    => 'Shelf successfully created',\n    'bookshelf_create_from_book'    => 'converted book to shelf',\n    'bookshelf_create_from_book_notification'    => 'Book successfully converted to a shelf',\n    'bookshelf_update'                 => 'updated shelf',\n    'bookshelf_update_notification'    => 'Shelf successfully updated',\n    'bookshelf_delete'                 => 'deleted shelf',\n    'bookshelf_delete_notification'    => 'Shelf successfully deleted',\n\n    // Revisions\n    'revision_restore' => 'restored revision',\n    'revision_delete' => 'deleted revision',\n    'revision_delete_notification' => 'Revision successfully deleted',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" has been added to your favourites',\n    'favourite_remove_notification' => '\":name\" has been removed from your favourites',\n\n    // Watching\n    'watch_update_level_notification' => 'Watch preferences successfully updated',\n\n    // Auth\n    'auth_login' => 'logged in',\n    'auth_register' => 'registered as new user',\n    'auth_password_reset_request' => 'requested user password reset',\n    'auth_password_reset_update' => 'reset user password',\n    'mfa_setup_method' => 'configured MFA method',\n    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',\n    'mfa_remove_method' => 'removed MFA method',\n    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',\n\n    // Settings\n    'settings_update' => 'updated settings',\n    'settings_update_notification' => 'Settings successfully updated',\n    'maintenance_action_run' => 'ran maintenance action',\n\n    // Webhooks\n    'webhook_create' => 'created webhook',\n    'webhook_create_notification' => 'Webhook successfully created',\n    'webhook_update' => 'updated webhook',\n    'webhook_update_notification' => 'Webhook successfully updated',\n    'webhook_delete' => 'deleted webhook',\n    'webhook_delete_notification' => 'Webhook successfully deleted',\n\n    // Imports\n    'import_create' => 'created import',\n    'import_create_notification' => 'Import successfully uploaded',\n    'import_run' => 'updated import',\n    'import_run_notification' => 'Content successfully imported',\n    'import_delete' => 'deleted import',\n    'import_delete_notification' => 'Import successfully deleted',\n\n    // Users\n    'user_create' => 'created user',\n    'user_create_notification' => 'User successfully created',\n    'user_update' => 'updated user',\n    'user_update_notification' => 'User successfully updated',\n    'user_delete' => 'deleted user',\n    'user_delete_notification' => 'User successfully removed',\n\n    // API Tokens\n    'api_token_create' => 'created API token',\n    'api_token_create_notification' => 'API token successfully created',\n    'api_token_update' => 'updated API token',\n    'api_token_update_notification' => 'API token successfully updated',\n    'api_token_delete' => 'deleted API token',\n    'api_token_delete_notification' => 'API token successfully deleted',\n\n    // Roles\n    'role_create' => 'created role',\n    'role_create_notification' => 'Role successfully created',\n    'role_update' => 'updated role',\n    'role_update_notification' => 'Role successfully updated',\n    'role_delete' => 'deleted role',\n    'role_delete_notification' => 'Role successfully deleted',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'emptied recycle bin',\n    'recycle_bin_restore' => 'restored from recycle bin',\n    'recycle_bin_destroy' => 'removed from recycle bin',\n\n    // Comments\n    'commented_on'                => 'commented on',\n    'comment_create'              => 'added comment',\n    'comment_update'              => 'updated comment',\n    'comment_delete'              => 'deleted comment',\n\n    // Sort Rules\n    'sort_rule_create' => 'created sort rule',\n    'sort_rule_create_notification' => 'Sort rule successfully created',\n    'sort_rule_update' => 'updated sort rule',\n    'sort_rule_update_notification' => 'Sort rule successfully updated',\n    'sort_rule_delete' => 'deleted sort rule',\n    'sort_rule_delete_notification' => 'Sort rule successfully deleted',\n\n    // Other\n    'permissions_update'          => 'updated permissions',\n];\n"
  },
  {
    "path": "lang/tk/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'These credentials do not match our records.',\n    'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',\n\n    // Login & Register\n    'sign_up' => 'Sign up',\n    'log_in' => 'Log in',\n    'log_in_with' => 'Login with :socialDriver',\n    'sign_up_with' => 'Sign up with :socialDriver',\n    'logout' => 'Logout',\n\n    'name' => 'Name',\n    'username' => 'Username',\n    'email' => 'Email',\n    'password' => 'Password',\n    'password_confirm' => 'Confirm Password',\n    'password_hint' => 'Must be at least 8 characters',\n    'forgot_password' => 'Forgot Password?',\n    'remember_me' => 'Remember Me',\n    'ldap_email_hint' => 'Please enter an email to use for this account.',\n    'create_account' => 'Create Account',\n    'already_have_account' => 'Already have an account?',\n    'dont_have_account' => 'Don\\'t have an account?',\n    'social_login' => 'Social Login',\n    'social_registration' => 'Social Registration',\n    'social_registration_text' => 'Register and sign in using another service.',\n\n    'register_thanks' => 'Thanks for registering!',\n    'register_confirm' => 'Please check your email and click the confirmation button to access :appName.',\n    'registrations_disabled' => 'Registrations are currently disabled',\n    'registration_email_domain_invalid' => 'That email domain does not have access to this application',\n    'register_success' => 'Thanks for signing up! You are now registered and signed in.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Attempting Login',\n    'auto_init_starting_desc' => 'We\\'re contacting your authentication system to start the login process. If there\\'s no progress after 5 seconds you can try clicking the link below.',\n    'auto_init_start_link' => 'Proceed with authentication',\n\n    // Password Reset\n    'reset_password' => 'Reset Password',\n    'reset_password_send_instructions' => 'Enter your email below and you will be sent an email with a password reset link.',\n    'reset_password_send_button' => 'Send Reset Link',\n    'reset_password_sent' => 'A password reset link will be sent to :email if that email address is found in the system.',\n    'reset_password_success' => 'Your password has been successfully reset.',\n    'email_reset_subject' => 'Reset your :appName password',\n    'email_reset_text' => 'You are receiving this email because we received a password reset request for your account.',\n    'email_reset_not_requested' => 'If you did not request a password reset, no further action is required.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Confirm your email on :appName',\n    'email_confirm_greeting' => 'Thanks for joining :appName!',\n    'email_confirm_text' => 'Please confirm your email address by clicking the button below:',\n    'email_confirm_action' => 'Confirm Email',\n    'email_confirm_send_error' => 'Email confirmation required but the system could not send the email. Contact the admin to ensure email is set up correctly.',\n    'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',\n    'email_confirm_resent' => 'Confirmation email resent, Please check your inbox.',\n    'email_confirm_thanks' => 'Thanks for confirming!',\n    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the \"Continue\" link below to proceed.',\n\n    'email_not_confirmed' => 'Email Address Not Confirmed',\n    'email_not_confirmed_text' => 'Your email address has not yet been confirmed.',\n    'email_not_confirmed_click_link' => 'Please click the link in the email that was sent shortly after you registered.',\n    'email_not_confirmed_resend' => 'If you cannot find the email you can re-send the confirmation email by submitting the form below.',\n    'email_not_confirmed_resend_button' => 'Resend Confirmation Email',\n\n    // User Invite\n    'user_invite_email_subject' => 'You have been invited to join :appName!',\n    'user_invite_email_greeting' => 'An account has been created for you on :appName.',\n    'user_invite_email_text' => 'Click the button below to set an account password and gain access:',\n    'user_invite_email_action' => 'Set Account Password',\n    'user_invite_page_welcome' => 'Welcome to :appName!',\n    'user_invite_page_text' => 'To finalise your account and gain access you need to set a password which will be used to log-in to :appName on future visits.',\n    'user_invite_page_confirm_button' => 'Confirm Password',\n    'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Setup Multi-Factor Authentication',\n    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'mfa_setup_configured' => 'Already configured',\n    'mfa_setup_reconfigure' => 'Reconfigure',\n    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',\n    'mfa_setup_action' => 'Setup',\n    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',\n    'mfa_option_totp_title' => 'Mobile App',\n    'mfa_option_totp_desc' => 'To use multi-factor authentication you\\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Backup Codes',\n    'mfa_option_backup_codes_desc' => 'Generates a set of one-time-use backup codes which you\\'ll enter on login to verify your identity. Make sure to store these in a safe & secure place.',\n    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',\n    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',\n    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\\'ll be able to use one of the codes as a second authentication mechanism.',\n    'mfa_gen_backup_codes_download' => 'Download Codes',\n    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',\n    'mfa_gen_totp_title' => 'Mobile App Setup',\n    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',\n    'mfa_gen_totp_verify_setup' => 'Verify Setup',\n    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',\n    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',\n    'mfa_verify_access' => 'Verify Access',\n    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\\'re granted access. Verify using one of your configured methods to continue.',\n    'mfa_verify_no_methods' => 'No Methods Configured',\n    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\\'ll need to set up at least one method before you gain access.',\n    'mfa_verify_use_totp' => 'Verify using a mobile app',\n    'mfa_verify_use_backup_codes' => 'Verify using a backup code',\n    'mfa_verify_backup_code' => 'Backup Code',\n    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',\n    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',\n    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',\n    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',\n];\n"
  },
  {
    "path": "lang/tk/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Cancel',\n    'close' => 'Close',\n    'confirm' => 'Confirm',\n    'back' => 'Back',\n    'save' => 'Save',\n    'continue' => 'Continue',\n    'select' => 'Select',\n    'toggle_all' => 'Toggle All',\n    'more' => 'More',\n\n    // Form Labels\n    'name' => 'Name',\n    'description' => 'Description',\n    'role' => 'Role',\n    'cover_image' => 'Cover image',\n    'cover_image_description' => 'This image should be approximately 440x250px although it will be flexibly scaled & cropped to fit the user interface in different scenarios as required, so actual dimensions for display will differ.',\n\n    // Actions\n    'actions' => 'Actions',\n    'view' => 'View',\n    'view_all' => 'View All',\n    'new' => 'New',\n    'create' => 'Create',\n    'update' => 'Update',\n    'edit' => 'Edit',\n    'archive' => 'Archive',\n    'unarchive' => 'Un-Archive',\n    'sort' => 'Sort',\n    'move' => 'Move',\n    'copy' => 'Copy',\n    'reply' => 'Reply',\n    'delete' => 'Delete',\n    'delete_confirm' => 'Confirm Deletion',\n    'search' => 'Search',\n    'search_clear' => 'Clear Search',\n    'reset' => 'Reset',\n    'remove' => 'Remove',\n    'add' => 'Add',\n    'configure' => 'Configure',\n    'manage' => 'Manage',\n    'fullscreen' => 'Fullscreen',\n    'favourite' => 'Favourite',\n    'unfavourite' => 'Unfavourite',\n    'next' => 'Next',\n    'previous' => 'Previous',\n    'filter_active' => 'Active Filter:',\n    'filter_clear' => 'Clear Filter',\n    'download' => 'Download',\n    'open_in_tab' => 'Open in Tab',\n    'open' => 'Open',\n\n    // Sort Options\n    'sort_options' => 'Sort Options',\n    'sort_direction_toggle' => 'Sort Direction Toggle',\n    'sort_ascending' => 'Sort Ascending',\n    'sort_descending' => 'Sort Descending',\n    'sort_name' => 'Name',\n    'sort_default' => 'Default',\n    'sort_created_at' => 'Created Date',\n    'sort_updated_at' => 'Updated Date',\n\n    // Misc\n    'deleted_user' => 'Deleted User',\n    'no_activity' => 'No activity to show',\n    'no_items' => 'No items available',\n    'back_to_top' => 'Back to top',\n    'skip_to_main_content' => 'Skip to main content',\n    'toggle_details' => 'Toggle Details',\n    'toggle_thumbnails' => 'Toggle Thumbnails',\n    'details' => 'Details',\n    'grid_view' => 'Grid View',\n    'list_view' => 'List View',\n    'default' => 'Default',\n    'breadcrumb' => 'Breadcrumb',\n    'status' => 'Status',\n    'status_active' => 'Active',\n    'status_inactive' => 'Inactive',\n    'never' => 'Never',\n    'none' => 'None',\n\n    // Header\n    'homepage' => 'Homepage',\n    'header_menu_expand' => 'Expand Header Menu',\n    'profile_menu' => 'Profile Menu',\n    'view_profile' => 'View Profile',\n    'edit_profile' => 'Edit Profile',\n    'dark_mode' => 'Dark Mode',\n    'light_mode' => 'Light Mode',\n    'global_search' => 'Global Search',\n\n    // Layout tabs\n    'tab_info' => 'Info',\n    'tab_info_label' => 'Tab: Show Secondary Information',\n    'tab_content' => 'Content',\n    'tab_content_label' => 'Tab: Show Primary Content',\n\n    // Email Content\n    'email_action_help' => 'If you’re having trouble clicking the \":actionText\" button, copy and paste the URL below into your web browser:',\n    'email_rights' => 'All rights reserved',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Privacy Policy',\n    'terms_of_service' => 'Terms of Service',\n\n    // OpenSearch\n    'opensearch_description' => 'Search :appName',\n];\n"
  },
  {
    "path": "lang/tk/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Image Select',\n    'image_list' => 'Image List',\n    'image_details' => 'Image Details',\n    'image_upload' => 'Upload Image',\n    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',\n    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the \"Upload Image\" button above.',\n    'image_all' => 'All',\n    'image_all_title' => 'View all images',\n    'image_book_title' => 'View images uploaded to this book',\n    'image_page_title' => 'View images uploaded to this page',\n    'image_search_hint' => 'Search by image name',\n    'image_uploaded' => 'Uploaded :uploadedDate',\n    'image_uploaded_by' => 'Uploaded by :userName',\n    'image_uploaded_to' => 'Uploaded to :pageLink',\n    'image_updated' => 'Updated :updateDate',\n    'image_load_more' => 'Load More',\n    'image_image_name' => 'Image Name',\n    'image_delete_used' => 'This image is used in the pages below.',\n    'image_delete_confirm_text' => 'Are you sure you want to delete this image?',\n    'image_select_image' => 'Select Image',\n    'image_dropzone' => 'Drop images or click here to upload',\n    'image_dropzone_drop' => 'Drop images here to upload',\n    'images_deleted' => 'Images Deleted',\n    'image_preview' => 'Image Preview',\n    'image_upload_success' => 'Image uploaded successfully',\n    'image_update_success' => 'Image details successfully updated',\n    'image_delete_success' => 'Image successfully deleted',\n    'image_replace' => 'Replace Image',\n    'image_replace_success' => 'Image file successfully updated',\n    'image_rebuild_thumbs' => 'Regenerate Size Variations',\n    'image_rebuild_thumbs_success' => 'Image size variations successfully rebuilt!',\n\n    // Code Editor\n    'code_editor' => 'Edit Code',\n    'code_language' => 'Code Language',\n    'code_content' => 'Code Content',\n    'code_session_history' => 'Session History',\n    'code_save' => 'Save Code',\n];\n"
  },
  {
    "path": "lang/tk/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'General',\n    'advanced' => 'Advanced',\n    'none' => 'None',\n    'cancel' => 'Cancel',\n    'save' => 'Save',\n    'close' => 'Close',\n    'apply' => 'Apply',\n    'undo' => 'Undo',\n    'redo' => 'Redo',\n    'left' => 'Left',\n    'center' => 'Center',\n    'right' => 'Right',\n    'top' => 'Top',\n    'middle' => 'Middle',\n    'bottom' => 'Bottom',\n    'width' => 'Width',\n    'height' => 'Height',\n    'More' => 'More',\n    'select' => 'Select...',\n\n    // Toolbar\n    'formats' => 'Formats',\n    'header_large' => 'Large Header',\n    'header_medium' => 'Medium Header',\n    'header_small' => 'Small Header',\n    'header_tiny' => 'Tiny Header',\n    'paragraph' => 'Paragraph',\n    'blockquote' => 'Blockquote',\n    'inline_code' => 'Inline code',\n    'callouts' => 'Callouts',\n    'callout_information' => 'Information',\n    'callout_success' => 'Success',\n    'callout_warning' => 'Warning',\n    'callout_danger' => 'Danger',\n    'bold' => 'Bold',\n    'italic' => 'Italic',\n    'underline' => 'Underline',\n    'strikethrough' => 'Strikethrough',\n    'superscript' => 'Superscript',\n    'subscript' => 'Subscript',\n    'text_color' => 'Text color',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Custom color',\n    'remove_color' => 'Remove color',\n    'background_color' => 'Background color',\n    'align_left' => 'Align left',\n    'align_center' => 'Align center',\n    'align_right' => 'Align right',\n    'align_justify' => 'Justify',\n    'list_bullet' => 'Bullet list',\n    'list_numbered' => 'Numbered list',\n    'list_task' => 'Task list',\n    'indent_increase' => 'Increase indent',\n    'indent_decrease' => 'Decrease indent',\n    'table' => 'Table',\n    'insert_image' => 'Insert image',\n    'insert_image_title' => 'Insert/Edit Image',\n    'insert_link' => 'Insert/edit link',\n    'insert_link_title' => 'Insert/Edit Link',\n    'insert_horizontal_line' => 'Insert horizontal line',\n    'insert_code_block' => 'Insert code block',\n    'edit_code_block' => 'Edit code block',\n    'insert_drawing' => 'Insert/edit drawing',\n    'drawing_manager' => 'Drawing manager',\n    'insert_media' => 'Insert/edit media',\n    'insert_media_title' => 'Insert/Edit Media',\n    'clear_formatting' => 'Clear formatting',\n    'source_code' => 'Source code',\n    'source_code_title' => 'Source Code',\n    'fullscreen' => 'Fullscreen',\n    'image_options' => 'Image options',\n\n    // Tables\n    'table_properties' => 'Table properties',\n    'table_properties_title' => 'Table Properties',\n    'delete_table' => 'Delete table',\n    'table_clear_formatting' => 'Clear table formatting',\n    'resize_to_contents' => 'Resize to contents',\n    'row_header' => 'Row header',\n    'insert_row_before' => 'Insert row before',\n    'insert_row_after' => 'Insert row after',\n    'delete_row' => 'Delete row',\n    'insert_column_before' => 'Insert column before',\n    'insert_column_after' => 'Insert column after',\n    'delete_column' => 'Delete column',\n    'table_cell' => 'Cell',\n    'table_row' => 'Row',\n    'table_column' => 'Column',\n    'cell_properties' => 'Cell properties',\n    'cell_properties_title' => 'Cell Properties',\n    'cell_type' => 'Cell type',\n    'cell_type_cell' => 'Cell',\n    'cell_scope' => 'Scope',\n    'cell_type_header' => 'Header cell',\n    'merge_cells' => 'Merge cells',\n    'split_cell' => 'Split cell',\n    'table_row_group' => 'Row Group',\n    'table_column_group' => 'Column Group',\n    'horizontal_align' => 'Horizontal align',\n    'vertical_align' => 'Vertical align',\n    'border_width' => 'Border width',\n    'border_style' => 'Border style',\n    'border_color' => 'Border color',\n    'row_properties' => 'Row properties',\n    'row_properties_title' => 'Row Properties',\n    'cut_row' => 'Cut row',\n    'copy_row' => 'Copy row',\n    'paste_row_before' => 'Paste row before',\n    'paste_row_after' => 'Paste row after',\n    'row_type' => 'Row type',\n    'row_type_header' => 'Header',\n    'row_type_body' => 'Body',\n    'row_type_footer' => 'Footer',\n    'alignment' => 'Alignment',\n    'cut_column' => 'Cut column',\n    'copy_column' => 'Copy column',\n    'paste_column_before' => 'Paste column before',\n    'paste_column_after' => 'Paste column after',\n    'cell_padding' => 'Cell padding',\n    'cell_spacing' => 'Cell spacing',\n    'caption' => 'Caption',\n    'show_caption' => 'Show caption',\n    'constrain' => 'Constrain proportions',\n    'cell_border_solid' => 'Solid',\n    'cell_border_dotted' => 'Dotted',\n    'cell_border_dashed' => 'Dashed',\n    'cell_border_double' => 'Double',\n    'cell_border_groove' => 'Groove',\n    'cell_border_ridge' => 'Ridge',\n    'cell_border_inset' => 'Inset',\n    'cell_border_outset' => 'Outset',\n    'cell_border_none' => 'None',\n    'cell_border_hidden' => 'Hidden',\n\n    // Images, links, details/summary & embed\n    'source' => 'Source',\n    'alt_desc' => 'Alternative description',\n    'embed' => 'Embed',\n    'paste_embed' => 'Paste your embed code below:',\n    'url' => 'URL',\n    'text_to_display' => 'Text to display',\n    'title' => 'Title',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Open link',\n    'open_link_in' => 'Open link in...',\n    'open_link_current' => 'Current window',\n    'open_link_new' => 'New window',\n    'remove_link' => 'Remove link',\n    'insert_collapsible' => 'Insert collapsible block',\n    'collapsible_unwrap' => 'Unwrap',\n    'edit_label' => 'Edit label',\n    'toggle_open_closed' => 'Toggle open/closed',\n    'collapsible_edit' => 'Edit collapsible block',\n    'toggle_label' => 'Toggle label',\n\n    // About view\n    'about' => 'About the editor',\n    'about_title' => 'About the WYSIWYG Editor',\n    'editor_license' => 'Editor License & Copyright',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under the MIT license.',\n    'editor_tiny_license_link' => 'The copyright and license details of TinyMCE can be found here.',\n    'save_continue' => 'Save Page & Continue',\n    'callouts_cycle' => '(Keep pressing to toggle through types)',\n    'link_selector' => 'Link to content',\n    'shortcuts' => 'Shortcuts',\n    'shortcut' => 'Shortcut',\n    'shortcuts_intro' => 'The following shortcuts are available in the editor:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Description',\n];\n"
  },
  {
    "path": "lang/tk/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Recently Created',\n    'recently_created_pages' => 'Recently Created Pages',\n    'recently_updated_pages' => 'Recently Updated Pages',\n    'recently_created_chapters' => 'Recently Created Chapters',\n    'recently_created_books' => 'Recently Created Books',\n    'recently_created_shelves' => 'Recently Created Shelves',\n    'recently_update' => 'Recently Updated',\n    'recently_viewed' => 'Recently Viewed',\n    'recent_activity' => 'Recent Activity',\n    'create_now' => 'Create one now',\n    'revisions' => 'Revisions',\n    'meta_revision' => 'Revision #:revisionCount',\n    'meta_created' => 'Created :timeLength',\n    'meta_created_name' => 'Created :timeLength by :user',\n    'meta_updated' => 'Updated :timeLength',\n    'meta_updated_name' => 'Updated :timeLength by :user',\n    'meta_owned_name' => 'Owned by :user',\n    'meta_reference_count' => 'Referenced by :count item|Referenced by :count items',\n    'entity_select' => 'Entity Select',\n    'entity_select_lack_permission' => 'You don\\'t have the required permissions to select this item',\n    'images' => 'Images',\n    'my_recent_drafts' => 'My Recent Drafts',\n    'my_recently_viewed' => 'My Recently Viewed',\n    'my_most_viewed_favourites' => 'My Most Viewed Favourites',\n    'my_favourites' => 'My Favourites',\n    'no_pages_viewed' => 'You have not viewed any pages',\n    'no_pages_recently_created' => 'No pages have been recently created',\n    'no_pages_recently_updated' => 'No pages have been recently updated',\n    'export' => 'Export',\n    'export_html' => 'Contained Web File',\n    'export_pdf' => 'PDF File',\n    'export_text' => 'Plain Text File',\n    'export_md' => 'Markdown File',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Default Page Template',\n    'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',\n    'default_template_select' => 'Select a template page',\n    'import' => 'Import',\n    'import_validate' => 'Validate Import',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'Select ZIP file to upload',\n    'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',\n    'import_pending' => 'Pending Imports',\n    'import_pending_none' => 'No imports have been started.',\n    'import_continue' => 'Continue Import',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Run Import',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Uploaded by',\n    'import_location' => 'Import Location',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'Import Errors',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Permissions',\n    'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.',\n    'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.',\n    'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.',\n    'permissions_save' => 'Save Permissions',\n    'permissions_owner' => 'Owner',\n    'permissions_role_everyone_else' => 'Everyone Else',\n    'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',\n    'permissions_role_override' => 'Override permissions for role',\n    'permissions_inherit_defaults' => 'Inherit defaults',\n\n    // Search\n    'search_results' => 'Search Results',\n    'search_total_results_found' => ':count result found|:count total results found',\n    'search_clear' => 'Clear Search',\n    'search_no_pages' => 'No pages matched this search',\n    'search_for_term' => 'Search for :term',\n    'search_more' => 'More Results',\n    'search_advanced' => 'Advanced Search',\n    'search_terms' => 'Search Terms',\n    'search_content_type' => 'Content Type',\n    'search_exact_matches' => 'Exact Matches',\n    'search_tags' => 'Tag Searches',\n    'search_options' => 'Options',\n    'search_viewed_by_me' => 'Viewed by me',\n    'search_not_viewed_by_me' => 'Not viewed by me',\n    'search_permissions_set' => 'Permissions set',\n    'search_created_by_me' => 'Created by me',\n    'search_updated_by_me' => 'Updated by me',\n    'search_owned_by_me' => 'Owned by me',\n    'search_date_options' => 'Date Options',\n    'search_updated_before' => 'Updated before',\n    'search_updated_after' => 'Updated after',\n    'search_created_before' => 'Created before',\n    'search_created_after' => 'Created after',\n    'search_set_date' => 'Set Date',\n    'search_update' => 'Update Search',\n\n    // Shelves\n    'shelf' => 'Shelf',\n    'shelves' => 'Shelves',\n    'x_shelves' => ':count Shelf|:count Shelves',\n    'shelves_empty' => 'No shelves have been created',\n    'shelves_create' => 'Create New Shelf',\n    'shelves_popular' => 'Popular Shelves',\n    'shelves_new' => 'New Shelves',\n    'shelves_new_action' => 'New Shelf',\n    'shelves_popular_empty' => 'The most popular shelves will appear here.',\n    'shelves_new_empty' => 'The most recently created shelves will appear here.',\n    'shelves_save' => 'Save Shelf',\n    'shelves_books' => 'Books on this shelf',\n    'shelves_add_books' => 'Add books to this shelf',\n    'shelves_drag_books' => 'Drag books below to add them to this shelf',\n    'shelves_empty_contents' => 'This shelf has no books assigned to it',\n    'shelves_edit_and_assign' => 'Edit shelf to assign books',\n    'shelves_edit_named' => 'Edit Shelf :name',\n    'shelves_edit' => 'Edit Shelf',\n    'shelves_delete' => 'Delete Shelf',\n    'shelves_delete_named' => 'Delete Shelf :name',\n    'shelves_delete_explain' => \"This will delete the shelf with the name ':name'. Contained books will not be deleted.\",\n    'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',\n    'shelves_permissions' => 'Shelf Permissions',\n    'shelves_permissions_updated' => 'Shelf Permissions Updated',\n    'shelves_permissions_active' => 'Shelf Permissions Active',\n    'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',\n    'shelves_permissions_create' => 'Shelf create permissions are only used for copying permissions to child books using the action below. They do not control the ability to create books.',\n    'shelves_copy_permissions_to_books' => 'Copy Permissions to Books',\n    'shelves_copy_permissions' => 'Copy Permissions',\n    'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',\n    'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',\n\n    // Books\n    'book' => 'Book',\n    'books' => 'Books',\n    'x_books' => ':count Book|:count Books',\n    'books_empty' => 'No books have been created',\n    'books_popular' => 'Popular Books',\n    'books_recent' => 'Recent Books',\n    'books_new' => 'New Books',\n    'books_new_action' => 'New Book',\n    'books_popular_empty' => 'The most popular books will appear here.',\n    'books_new_empty' => 'The most recently created books will appear here.',\n    'books_create' => 'Create New Book',\n    'books_delete' => 'Delete Book',\n    'books_delete_named' => 'Delete Book :bookName',\n    'books_delete_explain' => 'This will delete the book with the name \\':bookName\\'. All pages and chapters will be removed.',\n    'books_delete_confirmation' => 'Are you sure you want to delete this book?',\n    'books_edit' => 'Edit Book',\n    'books_edit_named' => 'Edit Book :bookName',\n    'books_form_book_name' => 'Book Name',\n    'books_save' => 'Save Book',\n    'books_permissions' => 'Book Permissions',\n    'books_permissions_updated' => 'Book Permissions Updated',\n    'books_empty_contents' => 'No pages or chapters have been created for this book.',\n    'books_empty_create_page' => 'Create a new page',\n    'books_empty_sort_current_book' => 'Sort the current book',\n    'books_empty_add_chapter' => 'Add a chapter',\n    'books_permissions_active' => 'Book Permissions Active',\n    'books_search_this' => 'Search this book',\n    'books_navigation' => 'Book Navigation',\n    'books_sort' => 'Sort Book Contents',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Sort Book :bookName',\n    'books_sort_name' => 'Sort by Name',\n    'books_sort_created' => 'Sort by Created Date',\n    'books_sort_updated' => 'Sort by Updated Date',\n    'books_sort_chapters_first' => 'Chapters First',\n    'books_sort_chapters_last' => 'Chapters Last',\n    'books_sort_show_other' => 'Show Other Books',\n    'books_sort_save' => 'Save New Order',\n    'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.',\n    'books_sort_move_up' => 'Move Up',\n    'books_sort_move_down' => 'Move Down',\n    'books_sort_move_prev_book' => 'Move to Previous Book',\n    'books_sort_move_next_book' => 'Move to Next Book',\n    'books_sort_move_prev_chapter' => 'Move Into Previous Chapter',\n    'books_sort_move_next_chapter' => 'Move Into Next Chapter',\n    'books_sort_move_book_start' => 'Move to Start of Book',\n    'books_sort_move_book_end' => 'Move to End of Book',\n    'books_sort_move_before_chapter' => 'Move to Before Chapter',\n    'books_sort_move_after_chapter' => 'Move to After Chapter',\n    'books_copy' => 'Copy Book',\n    'books_copy_success' => 'Book successfully copied',\n\n    // Chapters\n    'chapter' => 'Chapter',\n    'chapters' => 'Chapters',\n    'x_chapters' => ':count Chapter|:count Chapters',\n    'chapters_popular' => 'Popular Chapters',\n    'chapters_new' => 'New Chapter',\n    'chapters_create' => 'Create New Chapter',\n    'chapters_delete' => 'Delete Chapter',\n    'chapters_delete_named' => 'Delete Chapter :chapterName',\n    'chapters_delete_explain' => 'This will delete the chapter with the name \\':chapterName\\'. All pages that exist within this chapter will also be deleted.',\n    'chapters_delete_confirm' => 'Are you sure you want to delete this chapter?',\n    'chapters_edit' => 'Edit Chapter',\n    'chapters_edit_named' => 'Edit Chapter :chapterName',\n    'chapters_save' => 'Save Chapter',\n    'chapters_move' => 'Move Chapter',\n    'chapters_move_named' => 'Move Chapter :chapterName',\n    'chapters_copy' => 'Copy Chapter',\n    'chapters_copy_success' => 'Chapter successfully copied',\n    'chapters_permissions' => 'Chapter Permissions',\n    'chapters_empty' => 'No pages are currently in this chapter.',\n    'chapters_permissions_active' => 'Chapter Permissions Active',\n    'chapters_permissions_success' => 'Chapter Permissions Updated',\n    'chapters_search_this' => 'Search this chapter',\n    'chapter_sort_book' => 'Sort Book',\n\n    // Pages\n    'page' => 'Page',\n    'pages' => 'Pages',\n    'x_pages' => ':count Page|:count Pages',\n    'pages_popular' => 'Popular Pages',\n    'pages_new' => 'New Page',\n    'pages_attachments' => 'Attachments',\n    'pages_navigation' => 'Page Navigation',\n    'pages_delete' => 'Delete Page',\n    'pages_delete_named' => 'Delete Page :pageName',\n    'pages_delete_draft_named' => 'Delete Draft Page :pageName',\n    'pages_delete_draft' => 'Delete Draft Page',\n    'pages_delete_success' => 'Page deleted',\n    'pages_delete_draft_success' => 'Draft page deleted',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'Are you sure you want to delete this page?',\n    'pages_delete_draft_confirm' => 'Are you sure you want to delete this draft page?',\n    'pages_editing_named' => 'Editing Page :pageName',\n    'pages_edit_draft_options' => 'Draft Options',\n    'pages_edit_save_draft' => 'Save Draft',\n    'pages_edit_draft' => 'Edit Page Draft',\n    'pages_editing_draft' => 'Editing Draft',\n    'pages_editing_page' => 'Editing Page',\n    'pages_edit_draft_save_at' => 'Draft saved at ',\n    'pages_edit_delete_draft' => 'Delete Draft',\n    'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.',\n    'pages_edit_discard_draft' => 'Discard Draft',\n    'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',\n    'pages_edit_switch_to_markdown_clean' => '(Clean Content)',\n    'pages_edit_switch_to_markdown_stable' => '(Stable Content)',\n    'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Set Changelog',\n    'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\\'ve made',\n    'pages_edit_enter_changelog' => 'Enter Changelog',\n    'pages_editor_switch_title' => 'Switch Editor',\n    'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',\n    'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',\n    'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',\n    'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',\n    'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\\'t persist across this change.',\n    'pages_save' => 'Save Page',\n    'pages_title' => 'Page Title',\n    'pages_name' => 'Page Name',\n    'pages_md_editor' => 'Editor',\n    'pages_md_preview' => 'Preview',\n    'pages_md_insert_image' => 'Insert Image',\n    'pages_md_insert_link' => 'Insert Entity Link',\n    'pages_md_insert_drawing' => 'Insert Drawing',\n    'pages_md_show_preview' => 'Show preview',\n    'pages_md_sync_scroll' => 'Sync preview scroll',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Unsaved Drawing Found',\n    'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',\n    'pages_not_in_chapter' => 'Page is not in a chapter',\n    'pages_move' => 'Move Page',\n    'pages_copy' => 'Copy Page',\n    'pages_copy_desination' => 'Copy Destination',\n    'pages_copy_success' => 'Page successfully copied',\n    'pages_permissions' => 'Page Permissions',\n    'pages_permissions_success' => 'Page permissions updated',\n    'pages_revision' => 'Revision',\n    'pages_revisions' => 'Page Revisions',\n    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',\n    'pages_revisions_named' => 'Page Revisions for :pageName',\n    'pages_revision_named' => 'Page Revision for :pageName',\n    'pages_revision_restored_from' => 'Restored from #:id; :summary',\n    'pages_revisions_created_by' => 'Created By',\n    'pages_revisions_date' => 'Revision Date',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Revision Number',\n    'pages_revisions_numbered' => 'Revision #:id',\n    'pages_revisions_numbered_changes' => 'Revision #:id Changes',\n    'pages_revisions_editor' => 'Editor Type',\n    'pages_revisions_changelog' => 'Changelog',\n    'pages_revisions_changes' => 'Changes',\n    'pages_revisions_current' => 'Current Version',\n    'pages_revisions_preview' => 'Preview',\n    'pages_revisions_restore' => 'Restore',\n    'pages_revisions_none' => 'This page has no revisions',\n    'pages_copy_link' => 'Copy Link',\n    'pages_edit_content_link' => 'Jump to section in editor',\n    'pages_pointer_enter_mode' => 'Enter section select mode',\n    'pages_pointer_label' => 'Page Section Options',\n    'pages_pointer_permalink' => 'Page Section Permalink',\n    'pages_pointer_include_tag' => 'Page Section Include Tag',\n    'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag',\n    'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink',\n    'pages_permissions_active' => 'Page Permissions Active',\n    'pages_initial_revision' => 'Initial publish',\n    'pages_references_update_revision' => 'System auto-update of internal links',\n    'pages_initial_name' => 'New Page',\n    'pages_editing_draft_notification' => 'You are currently editing a draft that was last saved :timeDiff.',\n    'pages_draft_edited_notification' => 'This page has been updated by since that time. It is recommended that you discard this draft.',\n    'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count users have started editing this page',\n        'start_b' => ':userName has started editing this page',\n        'time_a' => 'since the page was last updated',\n        'time_b' => 'in the last :minCount minutes',\n        'message' => ':start :time. Take care not to overwrite each other\\'s updates!',\n    ],\n    'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',\n    'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',\n    'pages_specific' => 'Specific Page',\n    'pages_is_template' => 'Page Template',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Toggle Sidebar',\n    'page_tags' => 'Page Tags',\n    'chapter_tags' => 'Chapter Tags',\n    'book_tags' => 'Book Tags',\n    'shelf_tags' => 'Shelf Tags',\n    'tag' => 'Tag',\n    'tags' =>  'Tags',\n    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',\n    'tag_name' =>  'Tag Name',\n    'tag_value' => 'Tag Value (Optional)',\n    'tags_explain' => \"Add some tags to better categorise your content. \\n You can assign a value to a tag for more in-depth organisation.\",\n    'tags_add' => 'Add another tag',\n    'tags_remove' => 'Remove this tag',\n    'tags_usages' => 'Total tag usages',\n    'tags_assigned_pages' => 'Assigned to Pages',\n    'tags_assigned_chapters' => 'Assigned to Chapters',\n    'tags_assigned_books' => 'Assigned to Books',\n    'tags_assigned_shelves' => 'Assigned to Shelves',\n    'tags_x_unique_values' => ':count unique values',\n    'tags_all_values' => 'All values',\n    'tags_view_tags' => 'View Tags',\n    'tags_view_existing_tags' => 'View existing tags',\n    'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',\n    'attachments' => 'Attachments',\n    'attachments_explain' => 'Upload some files or attach some links to display on your page. These are visible in the page sidebar.',\n    'attachments_explain_instant_save' => 'Changes here are saved instantly.',\n    'attachments_upload' => 'Upload File',\n    'attachments_link' => 'Attach Link',\n    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',\n    'attachments_set_link' => 'Set Link',\n    'attachments_delete' => 'Are you sure you want to delete this attachment?',\n    'attachments_dropzone' => 'Drop files here to upload',\n    'attachments_no_files' => 'No files have been uploaded',\n    'attachments_explain_link' => 'You can attach a link if you\\'d prefer not to upload a file. This can be a link to another page or a link to a file in the cloud.',\n    'attachments_link_name' => 'Link Name',\n    'attachment_link' => 'Attachment link',\n    'attachments_link_url' => 'Link to file',\n    'attachments_link_url_hint' => 'Url of site or file',\n    'attach' => 'Attach',\n    'attachments_insert_link' => 'Add Attachment Link to Page',\n    'attachments_edit_file' => 'Edit File',\n    'attachments_edit_file_name' => 'File Name',\n    'attachments_edit_drop_upload' => 'Drop files or click here to upload and overwrite',\n    'attachments_order_updated' => 'Attachment order updated',\n    'attachments_updated_success' => 'Attachment details updated',\n    'attachments_deleted' => 'Attachment deleted',\n    'attachments_file_uploaded' => 'File successfully uploaded',\n    'attachments_file_updated' => 'File successfully updated',\n    'attachments_link_attached' => 'Link successfully attached to page',\n    'templates' => 'Templates',\n    'templates_set_as_template' => 'Page is a template',\n    'templates_explain_set_as_template' => 'You can set this page as a template so its contents be utilized when creating other pages. Other users will be able to use this template if they have view permissions for this page.',\n    'templates_replace_content' => 'Replace page content',\n    'templates_append_content' => 'Append to page content',\n    'templates_prepend_content' => 'Prepend to page content',\n\n    // Profile View\n    'profile_user_for_x' => 'User for :time',\n    'profile_created_content' => 'Created Content',\n    'profile_not_created_pages' => ':userName has not created any pages',\n    'profile_not_created_chapters' => ':userName has not created any chapters',\n    'profile_not_created_books' => ':userName has not created any books',\n    'profile_not_created_shelves' => ':userName has not created any shelves',\n\n    // Comments\n    'comment' => 'Comment',\n    'comments' => 'Comments',\n    'comment_add' => 'Add Comment',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Leave a comment here',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Save Comment',\n    'comment_new' => 'New Comment',\n    'comment_created' => 'commented :createDiff',\n    'comment_updated' => 'Updated :updateDiff by :username',\n    'comment_updated_indicator' => 'Updated',\n    'comment_deleted_success' => 'Comment deleted',\n    'comment_created_success' => 'Comment added',\n    'comment_updated_success' => 'Comment updated',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Are you sure you want to delete this comment?',\n    'comment_in_reply_to' => 'In reply to :commentId',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',\n\n    // Revision\n    'revision_delete_confirm' => 'Are you sure you want to delete this revision?',\n    'revision_restore_confirm' => 'Are you sure you want to restore this revision? The current page contents will be replaced.',\n    'revision_cannot_delete_latest' => 'Cannot delete the latest revision.',\n\n    // Copy view\n    'copy_consider' => 'Please consider the below when copying content.',\n    'copy_consider_permissions' => 'Custom permission settings will not be copied.',\n    'copy_consider_owner' => 'You will become the owner of all copied content.',\n    'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',\n    'copy_consider_attachments' => 'Page attachments will not be copied.',\n    'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',\n\n    // Conversions\n    'convert_to_shelf' => 'Convert to Shelf',\n    'convert_to_shelf_contents_desc' => 'You can convert this book to a new shelf with the same contents. Chapters contained within this book will be converted to new books. If this book contains any pages, that are not in a chapter, this book will be renamed and contain such pages, and this book will become part of the new shelf.',\n    'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.',\n    'convert_book' => 'Convert Book',\n    'convert_book_confirm' => 'Are you sure you want to convert this book?',\n    'convert_undo_warning' => 'This cannot be as easily undone.',\n    'convert_to_book' => 'Convert to Book',\n    'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',\n    'convert_chapter' => 'Convert Chapter',\n    'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',\n\n    // References\n    'references' => 'References',\n    'references_none' => 'There are no tracked references to this item.',\n    'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',\n\n    // Watch Options\n    'watch' => 'Watch',\n    'watch_title_default' => 'Default Preferences',\n    'watch_desc_default' => 'Revert watching to just your default notification preferences.',\n    'watch_title_ignore' => 'Ignore',\n    'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.',\n    'watch_title_new' => 'New Pages',\n    'watch_desc_new' => 'Notify when any new page is created within this item.',\n    'watch_title_updates' => 'All Page Updates',\n    'watch_desc_updates' => 'Notify upon all new pages and page changes.',\n    'watch_desc_updates_page' => 'Notify upon all page changes.',\n    'watch_title_comments' => 'All Page Updates & Comments',\n    'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.',\n    'watch_desc_comments_page' => 'Notify upon page changes and new comments.',\n    'watch_change_default' => 'Change default notification preferences',\n    'watch_detail_ignore' => 'Ignoring notifications',\n    'watch_detail_new' => 'Watching for new pages',\n    'watch_detail_updates' => 'Watching new pages and updates',\n    'watch_detail_comments' => 'Watching new pages, updates & comments',\n    'watch_detail_parent_book' => 'Watching via parent book',\n    'watch_detail_parent_book_ignore' => 'Ignoring via parent book',\n    'watch_detail_parent_chapter' => 'Watching via parent chapter',\n    'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter',\n];\n"
  },
  {
    "path": "lang/tk/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'You do not have permission to access the requested page.',\n    'permissionJson' => 'You do not have permission to perform the requested action.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'A user with the email :email already exists but with different credentials.',\n    'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',\n    'email_already_confirmed' => 'Email has already been confirmed, Try logging in.',\n    'email_confirmation_invalid' => 'This confirmation token is not valid or has already been used, Please try registering again.',\n    'email_confirmation_expired' => 'The confirmation token has expired, A new confirmation email has been sent.',\n    'email_confirmation_awaiting' => 'The email address for the account in use needs to be confirmed',\n    'ldap_fail_anonymous' => 'LDAP access failed using anonymous bind',\n    'ldap_fail_authed' => 'LDAP access failed using given dn & password details',\n    'ldap_extension_not_installed' => 'LDAP PHP extension not installed',\n    'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed',\n    'saml_already_logged_in' => 'Already logged in',\n    'saml_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',\n    'saml_invalid_response_id' => 'The request from the external authentication system is not recognised by a process started by this application. Navigating back after a login could cause this issue.',\n    'saml_fail_authed' => 'Login using :system failed, system did not provide successful authorization',\n    'oidc_already_logged_in' => 'Already logged in',\n    'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',\n    'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',\n    'social_no_action_defined' => 'No action defined',\n    'social_login_bad_response' => \"Error received during :socialAccount login: \\n:error\",\n    'social_account_in_use' => 'This :socialAccount account is already in use, Try logging in via the :socialAccount option.',\n    'social_account_email_in_use' => 'The email :email is already in use. If you already have an account you can connect your :socialAccount account from your profile settings.',\n    'social_account_existing' => 'This :socialAccount is already attached to your profile.',\n    'social_account_already_used_existing' => 'This :socialAccount account is already used by another user.',\n    'social_account_not_used' => 'This :socialAccount account is not linked to any users. Please attach it in your profile settings. ',\n    'social_account_register_instructions' => 'If you do not yet have an account, You can register an account using the :socialAccount option.',\n    'social_driver_not_found' => 'Social driver not found',\n    'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.',\n    'invite_token_expired' => 'This invitation link has expired. You can instead try to reset your account password.',\n    'login_user_not_found' => 'A user for this action could not be found.',\n\n    // System\n    'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.',\n    'cannot_get_image_from_url' => 'Cannot get image from :url',\n    'cannot_create_thumbs' => 'The server cannot create thumbnails. Please check you have the GD PHP extension installed.',\n    'server_upload_limit' => 'The server does not allow uploads of this size. Please try a smaller file size.',\n    'server_post_limit' => 'The server cannot receive the provided amount of data. Try again with less data or a smaller file.',\n    'uploaded'  => 'The server does not allow uploads of this size. Please try a smaller file size.',\n\n    // Drawing & Images\n    'image_upload_error' => 'An error occurred uploading the image',\n    'image_upload_type_error' => 'The image type being uploaded is invalid',\n    'image_upload_replace_type' => 'Image file replacements must be of the same type',\n    'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.',\n    'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.',\n    'image_gallery_thumbnail_memory_limit' => 'Failed to create gallery thumbnails due to system resource limits.',\n    'drawing_data_not_found' => 'Drawing data could not be loaded. The drawing file might no longer exist or you may not have permission to access it.',\n\n    // Attachments\n    'attachment_not_found' => 'Attachment not found',\n    'attachment_upload_error' => 'An error occurred uploading the attachment file',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page',\n    'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content',\n    'page_custom_home_deletion' => 'Cannot delete a page while it is set as a homepage',\n\n    // Entities\n    'entity_not_found' => 'Entity not found',\n    'bookshelf_not_found' => 'Shelf not found',\n    'book_not_found' => 'Book not found',\n    'page_not_found' => 'Page not found',\n    'chapter_not_found' => 'Chapter not found',\n    'selected_book_not_found' => 'The selected book was not found',\n    'selected_book_chapter_not_found' => 'The selected Book or Chapter was not found',\n    'guests_cannot_save_drafts' => 'Guests cannot save drafts',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'You cannot delete the only admin',\n    'users_cannot_delete_guest' => 'You cannot delete the guest user',\n    'users_could_not_send_invite' => 'Could not create user since invite email failed to send',\n\n    // Roles\n    'role_cannot_be_edited' => 'This role cannot be edited',\n    'role_system_cannot_be_deleted' => 'This role is a system role and cannot be deleted',\n    'role_registration_default_cannot_delete' => 'This role cannot be deleted while set as the default registration role',\n    'role_cannot_remove_only_admin' => 'This user is the only user assigned to the administrator role. Assign the administrator role to another user before attempting to remove it here.',\n\n    // Comments\n    'comment_list' => 'An error occurred while fetching the comments.',\n    'cannot_add_comment_to_draft' => 'You cannot add comments to a draft.',\n    'comment_add' => 'An error occurred while adding / updating the comment.',\n    'comment_delete' => 'An error occurred while deleting the comment.',\n    'empty_comment' => 'Cannot add an empty comment.',\n\n    // Error pages\n    '404_page_not_found' => 'Page Not Found',\n    'sorry_page_not_found' => 'Sorry, The page you were looking for could not be found.',\n    'sorry_page_not_found_permission_warning' => 'If you expected this page to exist, you might not have permission to view it.',\n    'image_not_found' => 'Image Not Found',\n    'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',\n    'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',\n    'return_home' => 'Return to home',\n    'error_occurred' => 'An Error Occurred',\n    'app_down' => ':appName is down right now',\n    'back_soon' => 'It will be back up soon.',\n\n    // Import\n    'import_zip_cant_read' => 'Could not read ZIP file.',\n    'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',\n    'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Import ZIP failed to validate with errors:',\n    'import_zip_failed_notification' => 'Failed to import ZIP file.',\n    'import_perms_books' => 'You are lacking the required permissions to create books.',\n    'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',\n    'import_perms_pages' => 'You are lacking the required permissions to create pages.',\n    'import_perms_images' => 'You are lacking the required permissions to create images.',\n    'import_perms_attachments' => 'You are lacking the required permission to create attachments.',\n\n    // API errors\n    'api_no_authorization_found' => 'No authorization token found on the request',\n    'api_bad_authorization_format' => 'An authorization token was found on the request but the format appeared incorrect',\n    'api_user_token_not_found' => 'No matching API token was found for the provided authorization token',\n    'api_incorrect_token_secret' => 'The secret provided for the given used API token is incorrect',\n    'api_user_no_api_permission' => 'The owner of the used API token does not have permission to make API calls',\n    'api_user_token_expired' => 'The authorization token used has expired',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Error thrown when sending a test email:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'The URL does not match the configured allowed SSR hosts',\n];\n"
  },
  {
    "path": "lang/tk/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'New comment on page: :pageName',\n    'new_comment_intro' => 'A user has commented on a page in :appName:',\n    'new_page_subject' => 'New page: :pageName',\n    'new_page_intro' => 'A new page has been created in :appName:',\n    'updated_page_subject' => 'Updated page: :pageName',\n    'updated_page_intro' => 'A page has been updated in :appName:',\n    'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\\'t be sent notifications for further edits to this page by the same editor.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Page Name:',\n    'detail_page_path' => 'Page Path:',\n    'detail_commenter' => 'Commenter:',\n    'detail_comment' => 'Comment:',\n    'detail_created_by' => 'Created By:',\n    'detail_updated_by' => 'Updated By:',\n\n    'action_view_comment' => 'View Comment',\n    'action_view_page' => 'View Page',\n\n    'footer_reason' => 'This notification was sent to you because :link cover this type of activity for this item.',\n    'footer_reason_link' => 'your notification preferences',\n];\n"
  },
  {
    "path": "lang/tk/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Previous',\n    'next'     => 'Next &raquo;',\n\n];\n"
  },
  {
    "path": "lang/tk/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Passwords must be at least eight characters and match the confirmation.',\n    'user' => \"We can't find a user with that e-mail address.\",\n    'token' => 'The password reset token is invalid for this email address.',\n    'sent' => 'We have e-mailed your password reset link!',\n    'reset' => 'Your password has been reset!',\n\n];\n"
  },
  {
    "path": "lang/tk/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'My Account',\n\n    'shortcuts' => 'Shortcuts',\n    'shortcuts_interface' => 'UI Shortcut Preferences',\n    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',\n    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',\n    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',\n    'shortcuts_section_navigation' => 'Navigation',\n    'shortcuts_section_actions' => 'Common Actions',\n    'shortcuts_save' => 'Save Shortcuts',\n    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing \"?\" which will highlight the available shortcuts for actions currently visible on the screen.',\n    'shortcuts_update_success' => 'Shortcut preferences have been updated!',\n    'shortcuts_overview_desc' => 'Manage keyboard shortcuts you can use to navigate the system user interface.',\n\n    'notifications' => 'Notification Preferences',\n    'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.',\n    'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own',\n    'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Notify upon replies to my comments',\n    'notifications_save' => 'Save Preferences',\n    'notifications_update_success' => 'Notification preferences have been updated!',\n    'notifications_watched' => 'Watched & Ignored Items',\n    'notifications_watched_desc' => 'Below are the items that have custom watch preferences applied. To update your preferences for these, view the item then find the watch options in the sidebar.',\n\n    'auth' => 'Access & Security',\n    'auth_change_password' => 'Change Password',\n    'auth_change_password_desc' => 'Change the password you use to log-in to the application. This must be at least 8 characters long.',\n    'auth_change_password_success' => 'Password has been updated!',\n\n    'profile' => 'Profile Details',\n    'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.',\n    'profile_view_public' => 'View Public Profile',\n    'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.',\n    'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.',\n    'profile_email_no_permission' => 'Unfortunately you don\\'t have permission to change your email address. If you want to change this, you\\'d need to ask an administrator to change this for you.',\n    'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.',\n    'profile_admin_options' => 'Administrator Options',\n    'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the \"Settings > Users\" area of the application.',\n\n    'delete_account' => 'Delete Account',\n    'delete_my_account' => 'Delete My Account',\n    'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\\'ve created, such as created pages and uploaded images, will remain.',\n    'delete_my_account_warning' => 'Are you sure you want to delete your account?',\n];\n"
  },
  {
    "path": "lang/tk/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Settings',\n    'settings_save' => 'Save Settings',\n    'system_version' => 'System Version',\n    'categories' => 'Categories',\n\n    // App Settings\n    'app_customization' => 'Customization',\n    'app_features_security' => 'Features & Security',\n    'app_name' => 'Application Name',\n    'app_name_desc' => 'This name is shown in the header and in any system-sent emails.',\n    'app_name_header' => 'Show name in header',\n    'app_public_access' => 'Public Access',\n    'app_public_access_desc' => 'Enabling this option will allow visitors, that are not logged-in, to access content in your BookStack instance.',\n    'app_public_access_desc_guest' => 'Access for public visitors can be controlled through the \"Guest\" user.',\n    'app_public_access_toggle' => 'Allow public access',\n    'app_public_viewing' => 'Allow public viewing?',\n    'app_secure_images' => 'Higher Security Image Uploads',\n    'app_secure_images_toggle' => 'Enable higher security image uploads',\n    'app_secure_images_desc' => 'For performance reasons, all images are public. This option adds a random, hard-to-guess string in front of image urls. Ensure directory indexes are not enabled to prevent easy access.',\n    'app_default_editor' => 'Default Page Editor',\n    'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.',\n    'app_custom_html' => 'Custom HTML Head Content',\n    'app_custom_html_desc' => 'Any content added here will be inserted into the bottom of the <head> section of every page. This is handy for overriding styles or adding analytics code.',\n    'app_custom_html_disabled_notice' => 'Custom HTML head content is disabled on this settings page to ensure any breaking changes can be reverted.',\n    'app_logo' => 'Application Logo',\n    'app_logo_desc' => 'This is used in the application header bar, among other areas. This image should be 86px in height. Large images will be scaled down.',\n    'app_icon' => 'Application Icon',\n    'app_icon_desc' => 'This icon is used for browser tabs and shortcut icons. This should be a 256px square PNG image.',\n    'app_homepage' => 'Application Homepage',\n    'app_homepage_desc' => 'Select a view to show on the homepage instead of the default view. Page permissions are ignored for selected pages.',\n    'app_homepage_select' => 'Select a page',\n    'app_footer_links' => 'Footer Links',\n    'app_footer_links_desc' => 'Add links to show within the site footer. These will be displayed at the bottom of most pages, including those that do not require login. You can use a label of \"trans::<key>\" to use system-defined translations. For example: Using \"trans::common.privacy_policy\" will provide the translated text \"Privacy Policy\" and \"trans::common.terms_of_service\" will provide the translated text \"Terms of Service\".',\n    'app_footer_links_label' => 'Link Label',\n    'app_footer_links_url' => 'Link URL',\n    'app_footer_links_add' => 'Add Footer Link',\n    'app_disable_comments' => 'Disable Comments',\n    'app_disable_comments_toggle' => 'Disable comments',\n    'app_disable_comments_desc' => 'Disables comments across all pages in the application. <br> Existing comments are not shown.',\n\n    // Color settings\n    'color_scheme' => 'Application Color Scheme',\n    'color_scheme_desc' => 'Set the colors to use in the application user interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.',\n    'ui_colors_desc' => 'Set the application primary color and default link color. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the application interface.',\n    'app_color' => 'Primary Color',\n    'link_color' => 'Default Link Color',\n    'content_colors_desc' => 'Set colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.',\n    'bookshelf_color' => 'Shelf Color',\n    'book_color' => 'Book Color',\n    'chapter_color' => 'Chapter Color',\n    'page_color' => 'Page Color',\n    'page_draft_color' => 'Page Draft Color',\n\n    // Registration Settings\n    'reg_settings' => 'Registration',\n    'reg_enable' => 'Enable Registration',\n    'reg_enable_toggle' => 'Enable registration',\n    'reg_enable_desc' => 'When registration is enabled user will be able to sign themselves up as an application user. Upon registration they are given a single, default user role.',\n    'reg_default_role' => 'Default user role after registration',\n    'reg_enable_external_warning' => 'The option above is ignored while external LDAP or SAML authentication is active. User accounts for non-existing members will be auto-created if authentication, against the external system in use, is successful.',\n    'reg_email_confirmation' => 'Email Confirmation',\n    'reg_email_confirmation_toggle' => 'Require email confirmation',\n    'reg_confirm_email_desc' => 'If domain restriction is used then email confirmation will be required and this option will be ignored.',\n    'reg_confirm_restrict_domain' => 'Domain Restriction',\n    'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.',\n    'reg_confirm_restrict_domain_placeholder' => 'No restriction set',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Maintenance',\n    'maint_image_cleanup' => 'Cleanup Images',\n    'maint_image_cleanup_desc' => 'Scans page & revision content to check which images and drawings are currently in use and which images are redundant. Ensure you create a full database and image backup before running this.',\n    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',\n    'maint_image_cleanup_run' => 'Run Cleanup',\n    'maint_image_cleanup_warning' => ':count potentially unused images were found. Are you sure you want to delete these images?',\n    'maint_image_cleanup_success' => ':count potentially unused images found and deleted!',\n    'maint_image_cleanup_nothing_found' => 'No unused images found, Nothing deleted!',\n    'maint_send_test_email' => 'Send a Test Email',\n    'maint_send_test_email_desc' => 'This sends a test email to your email address specified in your profile.',\n    'maint_send_test_email_run' => 'Send test email',\n    'maint_send_test_email_success' => 'Email sent to :address',\n    'maint_send_test_email_mail_subject' => 'Test Email',\n    'maint_send_test_email_mail_greeting' => 'Email delivery seems to work!',\n    'maint_send_test_email_mail_text' => 'Congratulations! As you received this email notification, your email settings seem to be configured properly.',\n    'maint_recycle_bin_desc' => 'Deleted shelves, books, chapters & pages are sent to the recycle bin so they can be restored or permanently deleted. Older items in the recycle bin may be automatically removed after a while depending on system configuration.',\n    'maint_recycle_bin_open' => 'Open Recycle Bin',\n    'maint_regen_references' => 'Regenerate References',\n    'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',\n    'maint_regen_references_success' => 'Reference index has been regenerated!',\n    'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Recycle Bin',\n    'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',\n    'recycle_bin_deleted_item' => 'Deleted Item',\n    'recycle_bin_deleted_parent' => 'Parent',\n    'recycle_bin_deleted_by' => 'Deleted By',\n    'recycle_bin_deleted_at' => 'Deletion Time',\n    'recycle_bin_permanently_delete' => 'Permanently Delete',\n    'recycle_bin_restore' => 'Restore',\n    'recycle_bin_contents_empty' => 'The recycle bin is currently empty',\n    'recycle_bin_empty' => 'Empty Recycle Bin',\n    'recycle_bin_empty_confirm' => 'This will permanently destroy all items in the recycle bin including content contained within each item. Are you sure you want to empty the recycle bin?',\n    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?',\n    'recycle_bin_destroy_list' => 'Items to be Destroyed',\n    'recycle_bin_restore_list' => 'Items to be Restored',\n    'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.',\n    'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.',\n    'recycle_bin_restore_parent' => 'Restore Parent',\n    'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',\n    'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',\n\n    // Audit Log\n    'audit' => 'Audit Log',\n    'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',\n    'audit_event_filter' => 'Event Filter',\n    'audit_event_filter_no_filter' => 'No Filter',\n    'audit_deleted_item' => 'Deleted Item',\n    'audit_deleted_item_name' => 'Name: :name',\n    'audit_table_user' => 'User',\n    'audit_table_event' => 'Event',\n    'audit_table_related' => 'Related Item or Detail',\n    'audit_table_ip' => 'IP Address',\n    'audit_table_date' => 'Activity Date',\n    'audit_date_from' => 'Date Range From',\n    'audit_date_to' => 'Date Range To',\n\n    // Role Settings\n    'roles' => 'Roles',\n    'role_user_roles' => 'User Roles',\n    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',\n    'roles_x_users_assigned' => ':count user assigned|:count users assigned',\n    'roles_x_permissions_provided' => ':count permission|:count permissions',\n    'roles_assigned_users' => 'Assigned Users',\n    'roles_permissions_provided' => 'Provided Permissions',\n    'role_create' => 'Create New Role',\n    'role_delete' => 'Delete Role',\n    'role_delete_confirm' => 'This will delete the role with the name \\':roleName\\'.',\n    'role_delete_users_assigned' => 'This role has :userCount users assigned to it. If you would like to migrate the users from this role select a new role below.',\n    'role_delete_no_migration' => \"Don't migrate users\",\n    'role_delete_sure' => 'Are you sure you want to delete this role?',\n    'role_edit' => 'Edit Role',\n    'role_details' => 'Role Details',\n    'role_name' => 'Role Name',\n    'role_desc' => 'Short Description of Role',\n    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',\n    'role_external_auth_id' => 'External Authentication IDs',\n    'role_system' => 'System Permissions',\n    'role_manage_users' => 'Manage users',\n    'role_manage_roles' => 'Manage roles & role permissions',\n    'role_manage_entity_permissions' => 'Manage all book, chapter & page permissions',\n    'role_manage_own_entity_permissions' => 'Manage permissions on own book, chapter & pages',\n    'role_manage_page_templates' => 'Manage page templates',\n    'role_access_api' => 'Access system API',\n    'role_manage_settings' => 'Manage app settings',\n    'role_export_content' => 'Export content',\n    'role_import_content' => 'Import content',\n    'role_editor_change' => 'Change page editor',\n    'role_notifications' => 'Receive & manage notifications',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Asset Permissions',\n    'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',\n    'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',\n    'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.',\n    'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'All',\n    'role_own' => 'Own',\n    'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',\n    'role_save' => 'Save Role',\n    'role_users' => 'Users in this role',\n    'role_users_none' => 'No users are currently assigned to this role',\n\n    // Users\n    'users' => 'Users',\n    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',\n    'user_profile' => 'User Profile',\n    'users_add_new' => 'Add New User',\n    'users_search' => 'Search Users',\n    'users_latest_activity' => 'Latest Activity',\n    'users_details' => 'User Details',\n    'users_details_desc' => 'Set a display name and an email address for this user. The email address will be used for logging into the application.',\n    'users_details_desc_no_email' => 'Set a display name for this user so others can recognise them.',\n    'users_role' => 'User Roles',\n    'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.',\n    'users_password' => 'User Password',\n    'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',\n    'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.',\n    'users_send_invite_option' => 'Send user invite email',\n    'users_external_auth_id' => 'External Authentication ID',\n    'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.',\n    'users_password_warning' => 'Only fill the below if you would like to change the password for this user.',\n    'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.',\n    'users_delete' => 'Delete User',\n    'users_delete_named' => 'Delete user :userName',\n    'users_delete_warning' => 'This will fully delete this user with the name \\':userName\\' from the system.',\n    'users_delete_confirm' => 'Are you sure you want to delete this user?',\n    'users_migrate_ownership' => 'Migrate Ownership',\n    'users_migrate_ownership_desc' => 'Select a user here if you want another user to become the owner of all items currently owned by this user.',\n    'users_none_selected' => 'No user selected',\n    'users_edit' => 'Edit User',\n    'users_edit_profile' => 'Edit Profile',\n    'users_avatar' => 'User Avatar',\n    'users_avatar_desc' => 'Select an image to represent this user. This should be approx 256px square.',\n    'users_preferred_language' => 'Preferred Language',\n    'users_preferred_language_desc' => 'This option will change the language used for the user-interface of the application. This will not affect any user-created content.',\n    'users_social_accounts' => 'Social Accounts',\n    'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.',\n    'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not revoke previously authorized access. Revoke access from your profile settings on the connected social account.',\n    'users_social_connect' => 'Connect Account',\n    'users_social_disconnect' => 'Disconnect Account',\n    'users_social_status_connected' => 'Connected',\n    'users_social_status_disconnected' => 'Disconnected',\n    'users_social_connected' => ':socialAccount account was successfully attached to your profile.',\n    'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.',\n    'users_api_tokens' => 'API Tokens',\n    'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.',\n    'users_api_tokens_none' => 'No API tokens have been created for this user',\n    'users_api_tokens_create' => 'Create Token',\n    'users_api_tokens_expires' => 'Expires',\n    'users_api_tokens_docs' => 'API Documentation',\n    'users_mfa' => 'Multi-Factor Authentication',\n    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'users_mfa_x_methods' => ':count method configured|:count methods configured',\n    'users_mfa_configure' => 'Configure Methods',\n\n    // API Tokens\n    'user_api_token_create' => 'Create API Token',\n    'user_api_token_name' => 'Name',\n    'user_api_token_name_desc' => 'Give your token a readable name as a future reminder of its intended purpose.',\n    'user_api_token_expiry' => 'Expiry Date',\n    'user_api_token_expiry_desc' => 'Set a date at which this token expires. After this date, requests made using this token will no longer work. Leaving this field blank will set an expiry 100 years into the future.',\n    'user_api_token_create_secret_message' => 'Immediately after creating this token a \"Token ID\" & \"Token Secret\" will be generated and displayed. The secret will only be shown a single time so be sure to copy the value to somewhere safe and secure before proceeding.',\n    'user_api_token' => 'API Token',\n    'user_api_token_id' => 'Token ID',\n    'user_api_token_id_desc' => 'This is a non-editable system generated identifier for this token which will need to be provided in API requests.',\n    'user_api_token_secret' => 'Token Secret',\n    'user_api_token_secret_desc' => 'This is a system generated secret for this token which will need to be provided in API requests. This will only be displayed this one time so copy this value to somewhere safe and secure.',\n    'user_api_token_created' => 'Token created :timeAgo',\n    'user_api_token_updated' => 'Token updated :timeAgo',\n    'user_api_token_delete' => 'Delete Token',\n    'user_api_token_delete_warning' => 'This will fully delete this API token with the name \\':tokenName\\' from the system.',\n    'user_api_token_delete_confirm' => 'Are you sure you want to delete this API token?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',\n    'webhooks_x_trigger_events' => ':count trigger event|:count trigger events',\n    'webhooks_create' => 'Create New Webhook',\n    'webhooks_none_created' => 'No webhooks have yet been created.',\n    'webhooks_edit' => 'Edit Webhook',\n    'webhooks_save' => 'Save Webhook',\n    'webhooks_details' => 'Webhook Details',\n    'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',\n    'webhooks_events' => 'Webhook Events',\n    'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',\n    'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\\'t expose confidential content.',\n    'webhooks_events_all' => 'All system events',\n    'webhooks_name' => 'Webhook Name',\n    'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',\n    'webhooks_endpoint' => 'Webhook Endpoint',\n    'webhooks_active' => 'Webhook Active',\n    'webhook_events_table_header' => 'Events',\n    'webhooks_delete' => 'Delete Webhook',\n    'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \\':webhookName\\', from the system.',\n    'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?',\n    'webhooks_format_example' => 'Webhook Format Example',\n    'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The \"related_item\" and \"url\" properties are optional and will depend on the type of event triggered.',\n    'webhooks_status' => 'Webhook Status',\n    'webhooks_last_called' => 'Last Called:',\n    'webhooks_last_errored' => 'Last Errored:',\n    'webhooks_last_error_message' => 'Last Error Message:',\n\n    // Licensing\n    'licenses' => 'Licenses',\n    'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.',\n    'licenses_bookstack' => 'BookStack License',\n    'licenses_php' => 'PHP Library Licenses',\n    'licenses_js' => 'JavaScript Library Licenses',\n    'licenses_other' => 'Other Licenses',\n    'license_details' => 'License Details',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/tk/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => 'The :attribute must be accepted.',\n    'active_url'           => 'The :attribute is not a valid URL.',\n    'after'                => 'The :attribute must be a date after :date.',\n    'alpha'                => 'The :attribute may only contain letters.',\n    'alpha_dash'           => 'The :attribute may only contain letters, numbers, dashes and underscores.',\n    'alpha_num'            => 'The :attribute may only contain letters and numbers.',\n    'array'                => 'The :attribute must be an array.',\n    'backup_codes'         => 'The provided code is not valid or has already been used.',\n    'before'               => 'The :attribute must be a date before :date.',\n    'between'              => [\n        'numeric' => 'The :attribute must be between :min and :max.',\n        'file'    => 'The :attribute must be between :min and :max kilobytes.',\n        'string'  => 'The :attribute must be between :min and :max characters.',\n        'array'   => 'The :attribute must have between :min and :max items.',\n    ],\n    'boolean'              => 'The :attribute field must be true or false.',\n    'confirmed'            => 'The :attribute confirmation does not match.',\n    'date'                 => 'The :attribute is not a valid date.',\n    'date_format'          => 'The :attribute does not match the format :format.',\n    'different'            => 'The :attribute and :other must be different.',\n    'digits'               => 'The :attribute must be :digits digits.',\n    'digits_between'       => 'The :attribute must be between :min and :max digits.',\n    'email'                => 'The :attribute must be a valid email address.',\n    'ends_with' => 'The :attribute must end with one of the following: :values',\n    'file'                 => 'The :attribute must be provided as a valid file.',\n    'filled'               => 'The :attribute field is required.',\n    'gt'                   => [\n        'numeric' => 'The :attribute must be greater than :value.',\n        'file'    => 'The :attribute must be greater than :value kilobytes.',\n        'string'  => 'The :attribute must be greater than :value characters.',\n        'array'   => 'The :attribute must have more than :value items.',\n    ],\n    'gte'                  => [\n        'numeric' => 'The :attribute must be greater than or equal :value.',\n        'file'    => 'The :attribute must be greater than or equal :value kilobytes.',\n        'string'  => 'The :attribute must be greater than or equal :value characters.',\n        'array'   => 'The :attribute must have :value items or more.',\n    ],\n    'exists'               => 'The selected :attribute is invalid.',\n    'image'                => 'The :attribute must be an image.',\n    'image_extension'      => 'The :attribute must have a valid & supported image extension.',\n    'in'                   => 'The selected :attribute is invalid.',\n    'integer'              => 'The :attribute must be an integer.',\n    'ip'                   => 'The :attribute must be a valid IP address.',\n    'ipv4'                 => 'The :attribute must be a valid IPv4 address.',\n    'ipv6'                 => 'The :attribute must be a valid IPv6 address.',\n    'json'                 => 'The :attribute must be a valid JSON string.',\n    'lt'                   => [\n        'numeric' => 'The :attribute must be less than :value.',\n        'file'    => 'The :attribute must be less than :value kilobytes.',\n        'string'  => 'The :attribute must be less than :value characters.',\n        'array'   => 'The :attribute must have less than :value items.',\n    ],\n    'lte'                  => [\n        'numeric' => 'The :attribute must be less than or equal :value.',\n        'file'    => 'The :attribute must be less than or equal :value kilobytes.',\n        'string'  => 'The :attribute must be less than or equal :value characters.',\n        'array'   => 'The :attribute must not have more than :value items.',\n    ],\n    'max'                  => [\n        'numeric' => 'The :attribute may not be greater than :max.',\n        'file'    => 'The :attribute may not be greater than :max kilobytes.',\n        'string'  => 'The :attribute may not be greater than :max characters.',\n        'array'   => 'The :attribute may not have more than :max items.',\n    ],\n    'mimes'                => 'The :attribute must be a file of type: :values.',\n    'min'                  => [\n        'numeric' => 'The :attribute must be at least :min.',\n        'file'    => 'The :attribute must be at least :min kilobytes.',\n        'string'  => 'The :attribute must be at least :min characters.',\n        'array'   => 'The :attribute must have at least :min items.',\n    ],\n    'not_in'               => 'The selected :attribute is invalid.',\n    'not_regex'            => 'The :attribute format is invalid.',\n    'numeric'              => 'The :attribute must be a number.',\n    'regex'                => 'The :attribute format is invalid.',\n    'required'             => 'The :attribute field is required.',\n    'required_if'          => 'The :attribute field is required when :other is :value.',\n    'required_with'        => 'The :attribute field is required when :values is present.',\n    'required_with_all'    => 'The :attribute field is required when :values is present.',\n    'required_without'     => 'The :attribute field is required when :values is not present.',\n    'required_without_all' => 'The :attribute field is required when none of :values are present.',\n    'same'                 => 'The :attribute and :other must match.',\n    'safe_url'             => 'The provided link may not be safe.',\n    'size'                 => [\n        'numeric' => 'The :attribute must be :size.',\n        'file'    => 'The :attribute must be :size kilobytes.',\n        'string'  => 'The :attribute must be :size characters.',\n        'array'   => 'The :attribute must contain :size items.',\n    ],\n    'string'               => 'The :attribute must be a string.',\n    'timezone'             => 'The :attribute must be a valid zone.',\n    'totp'                 => 'The provided code is not valid or has expired.',\n    'unique'               => 'The :attribute has already been taken.',\n    'url'                  => 'The :attribute format is invalid.',\n    'uploaded'             => 'The file could not be uploaded. The server may not accept files of this size.',\n\n    'zip_file' => 'The :attribute needs to reference a file within the ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',\n    'zip_model_expected' => 'Data object expected but \":type\" found.',\n    'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Password confirmation required',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/tr/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'sayfa oluşturdu',\n    'page_create_notification'    => 'Sayfa Başarıyla Oluşturuldu',\n    'page_update'                 => 'sayfayı güncelledi',\n    'page_update_notification'    => 'Sayfa başarıyla güncellendi',\n    'page_delete'                 => 'sayfa silindi',\n    'page_delete_notification'    => 'Sayfa başarıyla silindi',\n    'page_restore'                => 'sayfayı eski haline getirdi',\n    'page_restore_notification'   => 'Sayfa Başarıyla Eski Haline Getirildi',\n    'page_move'                   => 'sayfa taşındı',\n    'page_move_notification'      => 'Sayfa başarıyla taşındı',\n\n    // Chapters\n    'chapter_create'              => 'bölüm oluşturdu',\n    'chapter_create_notification' => 'Bölüm başarıyla oluşturuldu',\n    'chapter_update'              => 'bölümü güncelledi',\n    'chapter_update_notification' => 'Bölüm başarıyla güncellendi',\n    'chapter_delete'              => 'bölümü sildi',\n    'chapter_delete_notification' => 'Bölüm başarıyla silindi',\n    'chapter_move'                => 'bölümü taşıdı',\n    'chapter_move_notification' => 'Bölüm başarıyla taşındı',\n\n    // Books\n    'book_create'                 => 'kitap oluşturdu',\n    'book_create_notification'    => 'Kitap başarıyla oluşturuldu',\n    'book_create_from_chapter'              => 'converted chapter to book',\n    'book_create_from_chapter_notification' => 'Bölüm başarıyla kitaba dönüştürüldü',\n    'book_update'                 => 'güncellenen kitap',\n    'book_update_notification'    => 'Kitap başarıyla güncellendi',\n    'book_delete'                 => 'kitabı sildi',\n    'book_delete_notification'    => 'Kitap başarıyla silindi',\n    'book_sort'                   => 'kitabı sıraladı',\n    'book_sort_notification'      => 'Kitap başarıyla yeniden sıralandı',\n\n    // Bookshelves\n    'bookshelf_create'            => 'kitaplık oluşturuldu',\n    'bookshelf_create_notification'    => 'Kitaplık başarıyla oluşturuldu',\n    'bookshelf_create_from_book'    => 'converted book to shelf',\n    'bookshelf_create_from_book_notification'    => 'Kitap başarıyla kitaplığa dönüştürüldü',\n    'bookshelf_update'                 => 'updated shelf',\n    'bookshelf_update_notification'    => 'Kitaplık başarıyla güncellendi',\n    'bookshelf_delete'                 => 'deleted shelf',\n    'bookshelf_delete_notification'    => 'Kitaplık başarıyla silindi',\n\n    // Revisions\n    'revision_restore' => 'geri yüklenen revizyon',\n    'revision_delete' => 'silinmiş revizyon',\n    'revision_delete_notification' => 'Değişiklik başarıyla silindi',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" favorilerinize eklendi',\n    'favourite_remove_notification' => '\":name\" favorilerinizden çıkarıldı',\n\n    // Watching\n    'watch_update_level_notification' => 'Watch preferences successfully updated',\n\n    // Auth\n    'auth_login' => 'oturum açıldı',\n    'auth_register' => 'yeni kullanıcı olarak kayıt yapıldı',\n    'auth_password_reset_request' => 'talep edilmiş kullanıcı parola sıfırlamaları',\n    'auth_password_reset_update' => 'Kullanıcı parolasını sıfırla',\n    'mfa_setup_method' => 'uygulanan MFA yöntemi',\n    'mfa_setup_method_notification' => 'Çok aşamalı kimlik doğrulama yöntemi başarıyla yapılandırıldı',\n    'mfa_remove_method' => 'kaldırılan MFA yöntemi',\n    'mfa_remove_method_notification' => 'Çok aşamalı kimlik doğrulama yöntemi başarıyla kaldırıldı',\n\n    // Settings\n    'settings_update' => 'güncellenmiş ayarlar',\n    'settings_update_notification' => 'Ayarlar başarıyla güncellendi',\n    'maintenance_action_run' => 'bakım işlemine başla',\n\n    // Webhooks\n    'webhook_create' => 'web kancası oluşturuldu',\n    'webhook_create_notification' => 'Web kancası başarıyla oluşturuldu',\n    'webhook_update' => 'web kancası güncellendi',\n    'webhook_update_notification' => 'Web kancası başarıyla güncellendi',\n    'webhook_delete' => 'web kancası silindi',\n    'webhook_delete_notification' => 'Web kancası başarıyla silindi',\n\n    // Imports\n    'import_create' => 'created import',\n    'import_create_notification' => 'Import successfully uploaded',\n    'import_run' => 'updated import',\n    'import_run_notification' => 'Content successfully imported',\n    'import_delete' => 'deleted import',\n    'import_delete_notification' => 'Import successfully deleted',\n\n    // Users\n    'user_create' => 'oluşturan kullanıcı',\n    'user_create_notification' => 'Kullanıcı başarıyla oluşturuldu',\n    'user_update' => 'updated user',\n    'user_update_notification' => 'Kullanıcı başarıyla güncellendi',\n    'user_delete' => 'kullanıcı silindi',\n    'user_delete_notification' => 'Kullanıcı başarıyla silindi',\n\n    // API Tokens\n    'api_token_create' => 'created API token',\n    'api_token_create_notification' => 'API anahtarı başarıyla oluşturuldu',\n    'api_token_update' => 'updated API token',\n    'api_token_update_notification' => 'API anahtarı başarıyla güncellendi',\n    'api_token_delete' => 'deleted API token',\n    'api_token_delete_notification' => 'API anahtarı başarıyla silindi',\n\n    // Roles\n    'role_create' => 'oluşturulan rol',\n    'role_create_notification' => 'Rol başarıyla oluşturuldu',\n    'role_update' => 'güncellenmiş rol',\n    'role_update_notification' => 'Rol başarıyla güncellendi',\n    'role_delete' => 'deleted role',\n    'role_delete_notification' => 'Rol başarıyla silindi',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'emptied recycle bin',\n    'recycle_bin_restore' => 'çöp kutusundan geri getirilen',\n    'recycle_bin_destroy' => 'çöp kutusundan kaldırılan',\n\n    // Comments\n    'commented_on'                => 'yorum yaptı',\n    'comment_create'              => 'eklenen yorum',\n    'comment_update'              => 'güncellenen yorum',\n    'comment_delete'              => 'silinen yorum',\n\n    // Sort Rules\n    'sort_rule_create' => 'created sort rule',\n    'sort_rule_create_notification' => 'Sort rule successfully created',\n    'sort_rule_update' => 'updated sort rule',\n    'sort_rule_update_notification' => 'Sort rule successfully updated',\n    'sort_rule_delete' => 'deleted sort rule',\n    'sort_rule_delete_notification' => 'Sort rule successfully deleted',\n\n    // Other\n    'permissions_update'          => 'güncellenmiş izinler',\n];\n"
  },
  {
    "path": "lang/tr/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Girdiğiniz bilgiler kayıtlarımızla uyuşmuyor.',\n    'throttle' => 'Çok fazla giriş yapmaya çalıştınız. Lütfen :seconds saniye içinde tekrar deneyin.',\n\n    // Login & Register\n    'sign_up' => 'Kaydol',\n    'log_in' => 'Giriş Yap',\n    'log_in_with' => ':socialDriver ile giriş yap',\n    'sign_up_with' => ':socialDriver ile kaydol',\n    'logout' => 'Çıkış Yap',\n\n    'name' => 'İsim',\n    'username' => 'Kullanıcı Adı',\n    'email' => 'E-posta',\n    'password' => 'Şifre',\n    'password_confirm' => 'Şifreyi Onaylayın',\n    'password_hint' => 'En az 8 karakter olmalı',\n    'forgot_password' => 'Şifrenizi mi unuttunuz?',\n    'remember_me' => 'Beni Hatırla',\n    'ldap_email_hint' => 'Bu hesap için kullanmak istediğiniz e-posta adresini giriniz.',\n    'create_account' => 'Hesap Oluştur',\n    'already_have_account' => 'Zaten bir hesabınız var mı?',\n    'dont_have_account' => 'Hesabınız yok mu?',\n    'social_login' => 'Diğer Servisler ile Giriş Yapın',\n    'social_registration' => 'Diğer Servisler ile Kaydolun',\n    'social_registration_text' => 'Başka bir servis aracılığıyla kaydolun ve giriş yapın.',\n\n    'register_thanks' => 'Kaydolduğunuz için teşekkürler!',\n    'register_confirm' => ':appName erişimi için lütfen e-posta adresinizi kontrol edin ve size gönderilen doğrulama bağlantısına tıklayın.',\n    'registrations_disabled' => 'Kayıtlar devre dışı bırakılmıştır',\n    'registration_email_domain_invalid' => 'Bu e-posta sağlayıcısının uygulamaya erişim izni bulunmuyor',\n    'register_success' => 'Kaydolduğunuz için teşekkürler! Artık kayıtlı bir kullanıcı olarak giriş yaptınız.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Oturum açılmaya çalışılıyor',\n    'auto_init_starting_desc' => 'Oturum açma işlemini başlatmak için kimlik doğrulama sisteminizle iletişime geçiyoruz. Eğer 5 saniye sonra herhangi bir ilerleme olmazsa aşağıdaki bağlantıya tıklamayı deneyebilirsiniz.',\n    'auto_init_start_link' => 'Kimlik doğrulama ile devam edin',\n\n    // Password Reset\n    'reset_password' => 'Şifreyi Sıfırla',\n    'reset_password_send_instructions' => 'Aşağıya gireceğiniz e-posta adresine şifre sıfırlama bağlantısı gönderilecektir.',\n    'reset_password_send_button' => 'Sıfırlama Bağlantısını Gönder',\n    'reset_password_sent' => 'Şifre sıfırlama bağlantısı, :email adresinin sistemde bulunması durumunda e-posta olarak gönderilecektir.',\n    'reset_password_success' => 'Şifreniz başarıyla sıfırlandı.',\n    'email_reset_subject' => ':appName şifrenizi sıfırlayın',\n    'email_reset_text' => 'Hesap şifrenizi sıfırlama isteğinde bulunduğunuz için bu e-postayı aldınız.',\n    'email_reset_not_requested' => 'Şifre sıfırlama isteğinde bulunmadıysanız herhangi bir işlem yapmanıza gerek yoktur.',\n\n    // Email Confirmation\n    'email_confirm_subject' => ':appName için girdiğiniz e-posta adresini doğrulayın',\n    'email_confirm_greeting' => ':appName uygulamasına katıldığınız için teşekkürler!',\n    'email_confirm_text' => 'Lütfen aşağıdaki butona tıklayarak e-posta adresinizi doğrulayın:',\n    'email_confirm_action' => 'E-posta Adresini Doğrula',\n    'email_confirm_send_error' => 'E-posta adresinin doğrulanması gerekiyor fakat sistem, doğrulama bağlantısını göndermeyi başaramadı. E-posta adresinin doğru bir şekilde ayarlığından emin olmak için yöneticiyle iletişime geçin.',\n    'email_confirm_success' => 'Email hesabınız onaylandı. Email adresinizi kullanarak giriş yapabilirsiniz.',\n    'email_confirm_resent' => 'Doğrulama e-postası tekrar gönderildi, lütfen gelen kutunuzu kontrol ediniz.',\n    'email_confirm_thanks' => 'Onayladığınız için teşekkürler!',\n    'email_confirm_thanks_desc' => 'Lütfen onayınız işlenirken bir dakika bekleyin. Eğer 3 saniye sonra yönlendirilmediyseniz; devam etmek için aşağıdaki \"Devam\" linkine basınız.',\n\n    'email_not_confirmed' => 'E-posta Adresi Doğrulanmadı',\n    'email_not_confirmed_text' => 'E-posta adresiniz henüz doğrulanmadı.',\n    'email_not_confirmed_click_link' => 'Lütfen kaydolduktan hemen sonra size gönderilen e-postadaki bağlantıya tıklayın.',\n    'email_not_confirmed_resend' => 'Eğer e-postayı bulamıyorsanız, aşağıdaki formu doldurarak doğrulama e-postasının tekrar gönderilmesini sağlayabilirsiniz.',\n    'email_not_confirmed_resend_button' => 'Doğrulama E-postasını Tekrar Gönder',\n\n    // User Invite\n    'user_invite_email_subject' => ':appName uygulamasına davet edildiniz!',\n    'user_invite_email_greeting' => ':appName üzerinde sizin için bir hesap oluşturuldu.',\n    'user_invite_email_text' => 'Hesap şifrenizi belirlemek ve hesabınıza erişim sağlayabilmek için aşağıdaki butona tıklayın:',\n    'user_invite_email_action' => 'Hesap Şifresini Belirleyin',\n    'user_invite_page_welcome' => ':appName uygulamasına hoş geldiniz!',\n    'user_invite_page_text' => 'Hesap kurulumunuzu tamamlamak ve gelecekteki :appName ziyaretlerinizde hesabınıza erişim sağlayabilmeniz için bir şifre belirlemeniz gerekiyor.',\n    'user_invite_page_confirm_button' => 'Şifreyi Onayla',\n    'user_invite_success_login' => 'Şifre belirlendi, :appName! uygulamasına giriş yapmak için belirlediğiniz şifreyi kullanabilirsiniz',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Çok Aşamalı Kimlik Doğrulama',\n    'mfa_setup_desc' => 'Hesabınıza ekstra bir güvenlik katmanı daha eklemek için çok aşamalı kimlik doğrulamayı kurunuz.',\n    'mfa_setup_configured' => 'Zaten yapılandırıldı',\n    'mfa_setup_reconfigure' => 'Yeniden yapılandır',\n    'mfa_setup_remove_confirmation' => '2 adımlı doğrulamayı kaldırmak istediğinize emin misiniz?',\n    'mfa_setup_action' => 'Ayarlar',\n    'mfa_backup_codes_usage_limit_warning' => 'Kalan yedekleme kodu sayınız 5\\'ten az, hesabınızın kilitlenip kullanım dışı kalmaması için lütfen kodlarınız bitmeden yeni kod üretip saklayınız.',\n    'mfa_option_totp_title' => 'Mobil Uygulama',\n    'mfa_option_totp_desc' => 'Çok aşamalı kimlik doğrulamayı kullanabilmek için Google Authenticator, Authy veya Microsoft Authenticator gibi TOTP destekleyen bir mobil uygulamaya ihtiyacınız olacaktır.',\n    'mfa_option_backup_codes_title' => 'Yedekleme Kodları',\n    'mfa_option_backup_codes_desc' => 'Generates a set of one-time-use backup codes which you\\'ll enter on login to verify your identity. Make sure to store these in a safe & secure place.',\n    'mfa_gen_confirm_and_enable' => 'Onayla ve aktive et',\n    'mfa_gen_backup_codes_title' => 'Yedekleme Kodları Kurulumu',\n    'mfa_gen_backup_codes_desc' => 'Aşağıdaki kod listesini güvenli bir yerde sakla. Sisteme giriş yaparken kodlardan birini ikinci bir kimlik doğrulama mekanizması olarak kullanabileceksin.',\n    'mfa_gen_backup_codes_download' => 'İndirme Kodları',\n    'mfa_gen_backup_codes_usage_warning' => 'Her kod tek seferlik kullanılabilir',\n    'mfa_gen_totp_title' => 'Mobil Uygulama Kurulumu',\n    'mfa_gen_totp_desc' => 'Çok aşamalı kimlik doğrulamayı kullanabilmek için Google Authenticator, Authy veya Microsoft Authenticator gibi TOTP destekleyen bir mobil uygulamaya ihtiyacınız olacaktır.',\n    'mfa_gen_totp_scan' => 'Başlamak için aşağıdaki QR kodunu tercih ettiğin kimlik doğrulama uygulamasında tara.',\n    'mfa_gen_totp_verify_setup' => 'Kurulumu Doğrula',\n    'mfa_gen_totp_verify_setup_desc' => 'Aşağıdaki kutuya kimlik doğrulama uygulamanızda üretilmiş olan kodu girerek hepsini doğrulayabilirsiniz:',\n    'mfa_gen_totp_provide_code_here' => 'Uygulamada üretilen kodunuzu buraya giriniz',\n    'mfa_verify_access' => 'Girişi Doğrula',\n    'mfa_verify_access_desc' => 'Giriş yapmadan önce ek güvenlik doğrulaması amacıyla kimliğinizin doğrulanması gerekmektedir. Aşağıda belirtilen yöntemlerden birini kullanarak devam ediniz.',\n    'mfa_verify_no_methods' => 'Hiçbir Yöntem Ayarlanmadı',\n    'mfa_verify_no_methods_desc' => 'Hesabınızda çok aşamalı kimlik doğrulama yöntemi bulunamadı. Giriş yapabilmek için en az bir tane yöntemi ayarlamanız gerekmektedir.',\n    'mfa_verify_use_totp' => 'Mobil uygulama kullanarak doğrula',\n    'mfa_verify_use_backup_codes' => 'Yedekleme kodu kullanarak doğrula',\n    'mfa_verify_backup_code' => 'Yedekleme Kodu',\n    'mfa_verify_backup_code_desc' => 'Kalan yedekleme kodlarınızdan birini giriniz:',\n    'mfa_verify_backup_code_enter_here' => 'Yedekleme kodunuzu buraya giriniz',\n    'mfa_verify_totp_desc' => 'Mobil uygulamada üretilmiş kodu aşağıya giriniz:',\n    'mfa_setup_login_notification' => '2 adımlı doğrulama ayarlandı, Lütfen 2 adımlı doğrulama kullanarak yeniden giriş yapınız.',\n];\n"
  },
  {
    "path": "lang/tr/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'İptal',\n    'close' => 'Kapat',\n    'confirm' => 'Onayla',\n    'back' => 'Geri',\n    'save' => 'Kaydet',\n    'continue' => 'Devam',\n    'select' => 'Seç',\n    'toggle_all' => 'Tümünü Aç/Kapat',\n    'more' => 'Daha Fazla',\n\n    // Form Labels\n    'name' => 'İsim',\n    'description' => 'Açıklama',\n    'role' => 'Rol',\n    'cover_image' => 'Kapak resmi',\n    'cover_image_description' => 'This image should be approximately 440x250px although it will be flexibly scaled & cropped to fit the user interface in different scenarios as required, so actual dimensions for display will differ.',\n\n    // Actions\n    'actions' => 'İşlemler',\n    'view' => 'Görüntüle',\n    'view_all' => 'Hepsini Göster',\n    'new' => 'Yeni',\n    'create' => 'Oluştur',\n    'update' => 'Güncelle',\n    'edit' => 'Düzenle',\n    'archive' => 'Archive',\n    'unarchive' => 'Un-Archive',\n    'sort' => 'Sırala',\n    'move' => 'Taşı',\n    'copy' => 'Kopyala',\n    'reply' => 'Yanıtla',\n    'delete' => 'Sil',\n    'delete_confirm' => 'Silmeyi Onayla',\n    'search' => 'Ara',\n    'search_clear' => 'Aramayı Temizle',\n    'reset' => 'Sıfırla',\n    'remove' => 'Kaldır',\n    'add' => 'Ekle',\n    'configure' => 'Yapılandır',\n    'manage' => 'Manage',\n    'fullscreen' => 'Tam Ekran',\n    'favourite' => 'Favoriye ekle',\n    'unfavourite' => 'Favorilerden çıkar',\n    'next' => 'Sonraki',\n    'previous' => 'Önceki',\n    'filter_active' => 'Aktif Filtre:',\n    'filter_clear' => 'Filtreyi Kaldır',\n    'download' => 'İndir',\n    'open_in_tab' => 'Sekmede aç',\n    'open' => 'Open',\n\n    // Sort Options\n    'sort_options' => 'Sıralama Seçenekleri',\n    'sort_direction_toggle' => 'Sıralama Yönünü Değiştir',\n    'sort_ascending' => 'Artan Sıralama',\n    'sort_descending' => 'Azalan Sıralama',\n    'sort_name' => 'İsim',\n    'sort_default' => 'Varsayılan',\n    'sort_created_at' => 'Oluşturulma Tarihi',\n    'sort_updated_at' => 'Güncellenme Tarihi',\n\n    // Misc\n    'deleted_user' => 'Silinmiş Kullanıcı',\n    'no_activity' => 'Gösterilecek aktivite yok',\n    'no_items' => 'Herhangi bir öge bulunamadı',\n    'back_to_top' => 'Başa dön',\n    'skip_to_main_content' => 'Ana içeriğe geç',\n    'toggle_details' => 'Detayları Göster/Gizle',\n    'toggle_thumbnails' => 'Ön İzleme Görsellerini Göster/Gizle',\n    'details' => 'Detaylar',\n    'grid_view' => 'Izgara Görünümü',\n    'list_view' => 'Liste Görünümü',\n    'default' => 'Varsayılan',\n    'breadcrumb' => 'Gezinti Menüsü',\n    'status' => 'Durum',\n    'status_active' => 'Aktif',\n    'status_inactive' => 'Aktif değil',\n    'never' => 'Hiçbir zaman',\n    'none' => 'Hiçbiri',\n\n    // Header\n    'homepage' => 'Ana sayfa',\n    'header_menu_expand' => 'Başlık Menüsünü Genişlet',\n    'profile_menu' => 'Profil Menüsü',\n    'view_profile' => 'Profili Görüntüle',\n    'edit_profile' => 'Profili Düzenle',\n    'dark_mode' => 'Gece Teması',\n    'light_mode' => 'Aydınlık Modu',\n    'global_search' => 'Genel Arama',\n\n    // Layout tabs\n    'tab_info' => 'Bilgi',\n    'tab_info_label' => 'Sekme: İkincil Bilgileri Göster',\n    'tab_content' => 'İçerik',\n    'tab_content_label' => 'Sekme: Birincil Bilgileri Göster',\n\n    // Email Content\n    'email_action_help' => 'Eğer \":actionText\" butonuna tıklamakta zorluk çekiyorsanız, aşağıda bulunan linki kopyalayıp tarayıcınıza yapıştırabilirsiniz.',\n    'email_rights' => 'Tüm hakları saklıdır',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Gizlilik Politikası',\n    'terms_of_service' => 'Hizmet Şartları',\n\n    // OpenSearch\n    'opensearch_description' => 'Search :appName',\n];\n"
  },
  {
    "path": "lang/tr/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Görsel Seç',\n    'image_list' => 'Görsel Listesi',\n    'image_details' => 'Görsel Detayları',\n    'image_upload' => 'Görsel Yükle',\n    'image_intro' => 'Burada sisteme daha önce yüklenmiş görselleri seçebilir veya yönetebilirsiniz.',\n    'image_intro_upload' => 'Bir resim dosyasını bu pencereye sürükleyerek veya yukarıdaki \"Resim Yükle\" düğmesini kullanarak yeni bir resim yükleyin.',\n    'image_all' => 'Hepsi',\n    'image_all_title' => 'Bütün görselleri görüntüle',\n    'image_book_title' => 'Bu kitaba ait görselleri görüntüle',\n    'image_page_title' => 'Bu sayfaya ait görselleri görüntüle',\n    'image_search_hint' => 'Görsel adıyla ara',\n    'image_uploaded' => ':uploadedDate tarihinde yüklendi',\n    'image_uploaded_by' => ':userName tarafından yüklendi',\n    'image_uploaded_to' => ':pageLink \\'e yüklendi',\n    'image_updated' => ':updateDate \\'de güncellendi',\n    'image_load_more' => 'Devamını Göster',\n    'image_image_name' => 'Görsel Adı',\n    'image_delete_used' => 'Bu görsel aşağıda bulunan sayfalarda kullanılmış.',\n    'image_delete_confirm_text' => 'Bu resmi silmek istediğinizden emin misiniz?',\n    'image_select_image' => 'Görsel Seç',\n    'image_dropzone' => 'Görselleri sürükleyin ya da seçin',\n    'image_dropzone_drop' => 'Yüklemek için görselleri buraya bırakın',\n    'images_deleted' => 'Görseller Silindi',\n    'image_preview' => 'Görsel Ön İzlemesi',\n    'image_upload_success' => 'Görsel başarıyla yüklendi',\n    'image_update_success' => 'Görsel detayları başarıyla güncellendi',\n    'image_delete_success' => 'Görsel başarıyla silindi',\n    'image_replace' => 'Görseli Değiştir',\n    'image_replace_success' => 'Görsel dosyası başarıyla güncellendi',\n    'image_rebuild_thumbs' => 'Regenerate Size Variations',\n    'image_rebuild_thumbs_success' => 'Image size variations successfully rebuilt!',\n\n    // Code Editor\n    'code_editor' => 'Kodu Düzenle',\n    'code_language' => 'Kod Dili',\n    'code_content' => 'Kod İçeriği',\n    'code_session_history' => 'Oturum Geçmişi',\n    'code_save' => 'Kodu Kaydet',\n];\n"
  },
  {
    "path": "lang/tr/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Genel',\n    'advanced' => 'Gelişmiş seçenekler',\n    'none' => 'Hiçbiri',\n    'cancel' => 'İptal',\n    'save' => 'Kaydet',\n    'close' => 'Kapat',\n    'apply' => 'Apply',\n    'undo' => 'Geri al',\n    'redo' => 'Yeniden yap',\n    'left' => 'Sol',\n    'center' => 'Merkez',\n    'right' => 'Sağ',\n    'top' => 'Üst',\n    'middle' => 'Orta',\n    'bottom' => 'Alt',\n    'width' => 'Genişlik',\n    'height' => 'Yükseklik',\n    'More' => 'Daha fazla',\n    'select' => 'Seç...',\n\n    // Toolbar\n    'formats' => 'Formatlar',\n    'header_large' => 'Büyük başlık',\n    'header_medium' => 'Normal Başlık',\n    'header_small' => 'Küçük Başlık',\n    'header_tiny' => 'Minik Başlık',\n    'paragraph' => 'Paragraf',\n    'blockquote' => 'Blok alıntı',\n    'inline_code' => 'Satır içi kod',\n    'callouts' => 'Belirtme çizgileri',\n    'callout_information' => 'Bilgi',\n    'callout_success' => 'Başarılı',\n    'callout_warning' => 'Uyarı',\n    'callout_danger' => 'Tehlike',\n    'bold' => 'Kalın',\n    'italic' => 'İtalik',\n    'underline' => 'Altı çizili',\n    'strikethrough' => 'Üstü çizili',\n    'superscript' => 'Üst simge',\n    'subscript' => 'Alt simge',\n    'text_color' => 'Metin rengi',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Özel renk',\n    'remove_color' => 'Rengi kaldır',\n    'background_color' => 'Arka plan rengi',\n    'align_left' => 'Sola hizala',\n    'align_center' => 'Merkeze hizala',\n    'align_right' => 'Sağa hizala',\n    'align_justify' => 'İki yana yasla',\n    'list_bullet' => 'Noktalı liste',\n    'list_numbered' => 'Numaralı liste',\n    'list_task' => 'Görev listesi',\n    'indent_increase' => 'Girintiyi artır',\n    'indent_decrease' => 'Girintiyi azalt',\n    'table' => 'Tablo',\n    'insert_image' => 'Resim ekle',\n    'insert_image_title' => 'Resim Ekle/Düzenle',\n    'insert_link' => 'Bağlantı ekle/düzenle',\n    'insert_link_title' => 'Bağlantı Ekle/Düzenle',\n    'insert_horizontal_line' => 'Yatay çizgi ekle',\n    'insert_code_block' => 'Kod bloğu ekle',\n    'edit_code_block' => 'Kod bloğu düzenle',\n    'insert_drawing' => 'Çizim ekle/düzenle',\n    'drawing_manager' => 'Çizim yöneticisi',\n    'insert_media' => 'Medya ekle/düzenle',\n    'insert_media_title' => 'Medya Ekle/Düzenle',\n    'clear_formatting' => 'Biçimlendirmeyi temizle',\n    'source_code' => 'Kaynak kodu',\n    'source_code_title' => 'Kaynak Kodu',\n    'fullscreen' => 'Tam ekran',\n    'image_options' => 'Resim seçenekleri',\n\n    // Tables\n    'table_properties' => 'Tablo özellikleri',\n    'table_properties_title' => 'Tablo Özellikleri',\n    'delete_table' => 'Tabloyu sil',\n    'table_clear_formatting' => 'Clear table formatting',\n    'resize_to_contents' => 'Resize to contents',\n    'row_header' => 'Row header',\n    'insert_row_before' => 'Üste satır ekle',\n    'insert_row_after' => 'Alta satır ekle',\n    'delete_row' => 'Satırı sil',\n    'insert_column_before' => 'Sola sütun ekle',\n    'insert_column_after' => 'Sağa sütun ekle',\n    'delete_column' => 'Sütunu sil',\n    'table_cell' => 'Hücre',\n    'table_row' => 'Satır',\n    'table_column' => 'Sütun',\n    'cell_properties' => 'Hücre özellikleri',\n    'cell_properties_title' => 'Hücre Özellikleri',\n    'cell_type' => 'Hücre tipi',\n    'cell_type_cell' => 'Hücre',\n    'cell_scope' => 'Kapsam',\n    'cell_type_header' => 'Başlık hücresi',\n    'merge_cells' => 'Hücreleri birleştir',\n    'split_cell' => 'Hücreyi böl',\n    'table_row_group' => 'Satır Grubu',\n    'table_column_group' => 'Sütun Grubu',\n    'horizontal_align' => 'Yatay hizalama',\n    'vertical_align' => 'Dikey hizalama',\n    'border_width' => 'Kenarlık genişliği',\n    'border_style' => 'Kenarlık stili',\n    'border_color' => 'Kenarlık rengi',\n    'row_properties' => 'Satır özellikleri',\n    'row_properties_title' => 'Satır Özellikleri',\n    'cut_row' => 'Satırı kes',\n    'copy_row' => 'Satırı kopyala',\n    'paste_row_before' => 'Üste satırı yapıştır',\n    'paste_row_after' => 'Alta satırı yapıştır',\n    'row_type' => 'Satır tipi',\n    'row_type_header' => 'Üstbaşlık',\n    'row_type_body' => 'Gövde',\n    'row_type_footer' => 'Altbilgi',\n    'alignment' => 'Hizalama',\n    'cut_column' => 'Sütünu kes',\n    'copy_column' => 'Sütunu kopyala',\n    'paste_column_before' => 'Sütünu sola yapıştır',\n    'paste_column_after' => 'Sütunu sağa yapıştır',\n    'cell_padding' => 'Hücre boşluğu',\n    'cell_spacing' => 'Hücre aralığı',\n    'caption' => 'Başlık',\n    'show_caption' => 'Açıklamayı göster',\n    'constrain' => 'Oranları sınırla',\n    'cell_border_solid' => 'Düz',\n    'cell_border_dotted' => 'Noktalı',\n    'cell_border_dashed' => 'Kesik çizgili',\n    'cell_border_double' => 'Çift',\n    'cell_border_groove' => 'Oyuk',\n    'cell_border_ridge' => 'Sırt',\n    'cell_border_inset' => 'İçe Dönük',\n    'cell_border_outset' => 'Dışa Dönük',\n    'cell_border_none' => 'Hiçbiri',\n    'cell_border_hidden' => 'Gizli',\n\n    // Images, links, details/summary & embed\n    'source' => 'Kaynak',\n    'alt_desc' => 'Alternatif açıklama',\n    'embed' => 'Yerleştir',\n    'paste_embed' => 'Gömme kodunuzu aşağı yapıştırın:',\n    'url' => 'URL',\n    'text_to_display' => 'Görüntülenecek metin',\n    'title' => 'Başlık',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Bağlantıyı aç',\n    'open_link_in' => 'Bağlantıyı şurada aç...',\n    'open_link_current' => 'Geçerli pencere',\n    'open_link_new' => 'Yeni pencere',\n    'remove_link' => 'Bağlantıyı kaldır',\n    'insert_collapsible' => 'Küçültülebilir blok ekle',\n    'collapsible_unwrap' => 'Aç',\n    'edit_label' => 'Etiketi düzenle',\n    'toggle_open_closed' => 'Değiştir açık/kapalı',\n    'collapsible_edit' => 'Küçültülebilir bloğu düzenle',\n    'toggle_label' => 'Etiketleri aç/kapa',\n\n    // About view\n    'about' => 'Editör hakkında',\n    'about_title' => 'WYSIWYG editor hakkında',\n    'editor_license' => 'Editor Lisans ve Telif Hakkı',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'Bu düzenleyici, MIT lisansı altında sağlanan :tinyLink kullanılarak oluşturulmuştur.',\n    'editor_tiny_license_link' => 'TinyMCE telif ve lisans bilgilerini burada bulabilirsiniz.',\n    'save_continue' => 'Kaydet & Devam Et',\n    'callouts_cycle' => '(Türler arasında geçiş için basmaya devam ediniz)',\n    'link_selector' => 'İçeriğe bağlantı',\n    'shortcuts' => 'Kısayollar',\n    'shortcut' => 'Kısayol',\n    'shortcuts_intro' => 'Aşağıdaki kısayollar editörde kullanılabilir:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Açıklama',\n];\n"
  },
  {
    "path": "lang/tr/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Yakın Zamanda Oluşturuldu',\n    'recently_created_pages' => 'Yakın Zamanda Oluşturulan Sayfalar',\n    'recently_updated_pages' => 'Yakın Zamanda Güncellenen Sayfalar',\n    'recently_created_chapters' => 'Yakın Zamanda Oluşturulan Bölümler',\n    'recently_created_books' => 'Yakın Zamanda Oluşturulan Kitaplar',\n    'recently_created_shelves' => 'Yakın Zamanda Oluşturulan Kitaplıklar',\n    'recently_update' => 'Yakın Zamanda Güncellenmiş',\n    'recently_viewed' => 'Yakın Zamanda Görüntülenmiş',\n    'recent_activity' => 'Son Hareketler',\n    'create_now' => 'Hemen bir tane oluştur',\n    'revisions' => 'Revizyonlar',\n    'meta_revision' => 'Revizyon #:revisionCount',\n    'meta_created' => ':timeLength Oluşturuldu',\n    'meta_created_name' => ':user tarafından :timeLength oluşturuldu',\n    'meta_updated' => ':timeLength güncellendi',\n    'meta_updated_name' => ':user tarafından :timeLength güncellendi',\n    'meta_owned_name' => ':user kişisine ait',\n    'meta_reference_count' => 'Referenced by :count item|Referenced by :count items',\n    'entity_select' => 'Öge Seçimi',\n    'entity_select_lack_permission' => 'Bu öğeyi seçmek için gerekli izinlere sahip değilsiniz',\n    'images' => 'Görseller',\n    'my_recent_drafts' => 'Son Taslaklarım',\n    'my_recently_viewed' => 'Son Görüntülediklerim',\n    'my_most_viewed_favourites' => 'En Çok Görüntülenen Favoriler',\n    'my_favourites' => 'Favorilerim',\n    'no_pages_viewed' => 'Herhangi bir sayfa görüntülemediniz',\n    'no_pages_recently_created' => 'Yakın zamanda bir sayfa oluşturulmadı',\n    'no_pages_recently_updated' => 'Yakın zamanda bir sayfa güncellenmedi',\n    'export' => 'Dışa Aktar',\n    'export_html' => 'Web Dosyası',\n    'export_pdf' => 'PDF Dosyası',\n    'export_text' => 'Düz Metin Dosyası',\n    'export_md' => 'Markdown Dosyası',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Default Page Template',\n    'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',\n    'default_template_select' => 'Select a template page',\n    'import' => 'Import',\n    'import_validate' => 'Validate Import',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'Select ZIP file to upload',\n    'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',\n    'import_pending' => 'Pending Imports',\n    'import_pending_none' => 'No imports have been started.',\n    'import_continue' => 'Continue Import',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Run Import',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Uploaded by',\n    'import_location' => 'Import Location',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'Import Errors',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'İzinler',\n    'permissions_desc' => 'Kullanıcı rolleri tarafından sağlanan varsayılan izinleri geçersiz kılmak için izinleri buradan ayarlayın.',\n    'permissions_book_cascade' => 'Kitaplarda ayarlanan izinler, kendi izinleri tanımlanmadığı sürece otomatik olarak alt bölümlere ve sayfalara aktarılır.',\n    'permissions_chapter_cascade' => 'Bölümlerde ayarlanan izinler, kendi izinleri tanımlanmadığı sürece otomatik olarak alt sayfalara aktarılır.',\n    'permissions_save' => 'İzinleri Kaydet',\n    'permissions_owner' => 'Sahip',\n    'permissions_role_everyone_else' => 'Diğer Herkes',\n    'permissions_role_everyone_else_desc' => 'Özellikle geçersiz kılınmamış tüm roller için izinleri ayarlayın.',\n    'permissions_role_override' => 'Override permissions for role',\n    'permissions_inherit_defaults' => 'Varsayılanları devral',\n\n    // Search\n    'search_results' => 'Arama Sonuçları',\n    'search_total_results_found' => ':count sonuç bulundu |:count toplam sonuç bulundu',\n    'search_clear' => 'Aramayı Temizle',\n    'search_no_pages' => 'Bu aramayla ilgili herhangi bir sayfa bulunamadı',\n    'search_for_term' => ':term için Ara',\n    'search_more' => 'Daha Fazla Sonuç',\n    'search_advanced' => 'Gelişmiş Arama',\n    'search_terms' => 'Terimleri Ara',\n    'search_content_type' => 'İçerik Türü',\n    'search_exact_matches' => 'Tam Eşleşmeler',\n    'search_tags' => 'Etiket Aramaları',\n    'search_options' => 'Ayarlar',\n    'search_viewed_by_me' => 'Görüntülediklerim',\n    'search_not_viewed_by_me' => 'Görüntülemediklerim',\n    'search_permissions_set' => 'İzinler ayarlanmış',\n    'search_created_by_me' => 'Oluşturduklarım',\n    'search_updated_by_me' => 'Güncellediklerim',\n    'search_owned_by_me' => 'Bana ait',\n    'search_date_options' => 'Tarih Seçenekleri',\n    'search_updated_before' => 'Önce güncellendi',\n    'search_updated_after' => 'Sonra güncellendi',\n    'search_created_before' => 'Önce oluşturuldu',\n    'search_created_after' => 'Sonra oluşturuldu',\n    'search_set_date' => 'Tarihi Ayarla',\n    'search_update' => 'Aramayı Güncelle',\n\n    // Shelves\n    'shelf' => 'Kitaplık',\n    'shelves' => 'Kitaplıklar',\n    'x_shelves' => ':count Kitaplık|:count Kitaplık',\n    'shelves_empty' => 'Hiç kitaplık oluşturulmamış',\n    'shelves_create' => 'Yeni Kitaplık Oluştur',\n    'shelves_popular' => 'Popüler Kitaplıklar',\n    'shelves_new' => 'Yeni Kitaplıklar',\n    'shelves_new_action' => 'Yeni Kitaplık',\n    'shelves_popular_empty' => 'En popüler kitaplıklar burada görüntülenecek.',\n    'shelves_new_empty' => 'En son oluşturulmuş kitaplıklar burada görüntülenecek.',\n    'shelves_save' => 'Kitaplığı Kaydet',\n    'shelves_books' => 'Bu kitaplıktaki kitaplar',\n    'shelves_add_books' => 'Bu kitaplığa kitap ekle',\n    'shelves_drag_books' => 'Kitapları, bu kitaplığa eklemek için aşağıya sürükleyin',\n    'shelves_empty_contents' => 'Bu kitaplıkta hiç kitap bulunamadı',\n    'shelves_edit_and_assign' => 'Kitap eklemek için kitaplığı düzenleyin',\n    'shelves_edit_named' => ':name Kitaplığını Düzenle',\n    'shelves_edit' => 'Kitaplığı Düzenle',\n    'shelves_delete' => 'Kitaplığı Sil',\n    'shelves_delete_named' => ':name Kitaplığını Sil',\n    'shelves_delete_explain' => \"Bu işlem ':name' isimli kitaplığı silecektir. İçerdiği kitaplar silinmeyecektir.\",\n    'shelves_delete_confirmation' => 'Bu kitaplığı silmek istediğinize emin misiniz?',\n    'shelves_permissions' => 'Kitaplık İzinleri',\n    'shelves_permissions_updated' => 'Kitaplık İzinleri Güncellendi',\n    'shelves_permissions_active' => 'Kitaplık İzinleri Aktif',\n    'shelves_permissions_cascade_warning' => 'Kitaplıktaki izinler otomatik olarak içerilen kitaplara kademelendirilmez. Bunun nedeni, bir kitabın birden fazla kitaplıkta bulunabilmesidir. Ancak izinler, aşağıda bulunan seçenek kullanılarak alt kitaplara kopyalanabilir.',\n    'shelves_permissions_create' => 'Shelf create permissions are only used for copying permissions to child books using the action below. They do not control the ability to create books.',\n    'shelves_copy_permissions_to_books' => 'İzinleri Kitaplara Kopyala',\n    'shelves_copy_permissions' => 'İzinleri Kopyala',\n    'shelves_copy_permissions_explain' => 'Bu işlem sonucunda kitaplığınızın izinleri, içerdiği kitaplara da aynen uygulanır. Aktifleştirmeden önce bu kitaplığa ait izinleri kaydettiğinizden emin olun.',\n    'shelves_copy_permission_success' => 'Kitaplık izinleri :count adet kitaba kopyalandı',\n\n    // Books\n    'book' => 'Kitap',\n    'books' => 'Kitaplar',\n    'x_books' => ':count Kitap|:count Kitap',\n    'books_empty' => 'Hiç kitap oluşturulmamış',\n    'books_popular' => 'Popüler Kitaplar',\n    'books_recent' => 'En Son Kitaplar',\n    'books_new' => 'Yeni Kitaplar',\n    'books_new_action' => 'Yeni Kitap',\n    'books_popular_empty' => 'En popüler kitaplar burada görüntülenecek.',\n    'books_new_empty' => 'En yeni kitaplar burada görüntülenecek.',\n    'books_create' => 'Yeni Kitap Oluştur',\n    'books_delete' => 'Kitabı Sil',\n    'books_delete_named' => ':bookName Kitabını Sil',\n    'books_delete_explain' => 'Bu işlem \\':bookName\\' kitabını silecektir. Ayrıca kitaba ait bütün sayfalar ve bölümler de silinecektir.',\n    'books_delete_confirmation' => 'Bu kitabı silmek istediğinize emin misiniz?',\n    'books_edit' => 'Kitabı Düzenle',\n    'books_edit_named' => ':bookName Kitabını Düzenle',\n    'books_form_book_name' => 'Kitap Adı',\n    'books_save' => 'Kitabı Kaydet',\n    'books_permissions' => 'Kitap İzinleri',\n    'books_permissions_updated' => 'Kitap İzinleri Güncellendi',\n    'books_empty_contents' => 'Bu kitaba ait sayfa veya bölüm oluşturulmamış.',\n    'books_empty_create_page' => 'Yeni bir sayfa oluştur',\n    'books_empty_sort_current_book' => 'Mevcut kitabı sırala',\n    'books_empty_add_chapter' => 'Yeni bir bölüm ekle',\n    'books_permissions_active' => 'Kitap İzinleri Aktif',\n    'books_search_this' => 'Bu kitapta ara',\n    'books_navigation' => 'Kitap Navigasyonu',\n    'books_sort' => 'Kitap İçeriklerini Sırala',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => ':bookName Kitabını Sırala',\n    'books_sort_name' => 'İsme Göre Sırala',\n    'books_sort_created' => 'Oluşturulma Tarihine Göre Sırala',\n    'books_sort_updated' => 'Güncelleme Tarihine Göre Sırala',\n    'books_sort_chapters_first' => 'Önce Bölümler',\n    'books_sort_chapters_last' => 'En Son Bölümler',\n    'books_sort_show_other' => 'Diğer Kitapları Göster',\n    'books_sort_save' => 'Yeni Düzeni Kaydet',\n    'books_sort_show_other_desc' => 'Sıralama işlemine dahil etmek ve kitaplar arası yeniden düzenlemeyi kolaylaştırmak için diğer kitapları buraya ekleyin.',\n    'books_sort_move_up' => 'Yukarı Taşı',\n    'books_sort_move_down' => 'Aşağı Taşı',\n    'books_sort_move_prev_book' => 'Move to Previous Book',\n    'books_sort_move_next_book' => 'Move to Next Book',\n    'books_sort_move_prev_chapter' => 'Önceki Bölüme Git',\n    'books_sort_move_next_chapter' => 'Sonraki Bölüme Git',\n    'books_sort_move_book_start' => 'Kitap Başlangıcına Git',\n    'books_sort_move_book_end' => 'Kitap Sonuna Git',\n    'books_sort_move_before_chapter' => 'Move to Before Chapter',\n    'books_sort_move_after_chapter' => 'Move to After Chapter',\n    'books_copy' => 'Kitabı Kopyala',\n    'books_copy_success' => 'Kitap başarıyla kopyalandı',\n\n    // Chapters\n    'chapter' => 'Bölüm',\n    'chapters' => 'Bölümler',\n    'x_chapters' => ':count Bölüm|:count Bölüm',\n    'chapters_popular' => 'Popüler Bölümler',\n    'chapters_new' => 'Yeni Bölüm',\n    'chapters_create' => 'Yeni Bölüm Oluştur',\n    'chapters_delete' => 'Bölümü Sil',\n    'chapters_delete_named' => ':chapterName Bölümünü Sil',\n    'chapters_delete_explain' => 'This will delete the chapter with the name \\':chapterName\\'. All pages that exist within this chapter will also be deleted.',\n    'chapters_delete_confirm' => 'Bölümü silmek istediğinize emin misiniz?',\n    'chapters_edit' => 'Bölümü Düzenle',\n    'chapters_edit_named' => ':chapterName Bölümünü Düzenle',\n    'chapters_save' => 'Bölümü Kaydet',\n    'chapters_move' => 'Bölümü Taşı',\n    'chapters_move_named' => ':chapterName Bölümünü Taşı',\n    'chapters_copy' => 'Bölümü kopyala',\n    'chapters_copy_success' => 'Bölüm başarıyla kopyalandı',\n    'chapters_permissions' => 'Bölüm İzinleri',\n    'chapters_empty' => 'Bu bölümde henüz bir sayfa bulunmuyor.',\n    'chapters_permissions_active' => 'Bölüm İzinleri Aktif',\n    'chapters_permissions_success' => 'Bölüm İzinleri Güncellendi',\n    'chapters_search_this' => 'Bu bölümde ara',\n    'chapter_sort_book' => 'Kitap Sırala',\n\n    // Pages\n    'page' => 'Sayfa',\n    'pages' => 'Sayfalar',\n    'x_pages' => ':count Sayfa|:count Sayfa',\n    'pages_popular' => 'Popüler Sayfalar',\n    'pages_new' => 'Yeni Sayfa',\n    'pages_attachments' => 'Ekler',\n    'pages_navigation' => 'Sayfa Navigasyonu',\n    'pages_delete' => 'Sayfayı Sil',\n    'pages_delete_named' => ':pageName Sayfasını Sil',\n    'pages_delete_draft_named' => ':pageName Sayfa Taslağını Sil',\n    'pages_delete_draft' => 'Sayfa Taslağını Sil',\n    'pages_delete_success' => 'Sayfa silindi',\n    'pages_delete_draft_success' => 'Sayfa taslağı silindi',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'Bu sayfayı silmek istediğinize emin misiniz?',\n    'pages_delete_draft_confirm' => 'Bu sayfa taslağını silmek istediğinize emin misiniz?',\n    'pages_editing_named' => ':pageName Sayfası Düzenleniyor',\n    'pages_edit_draft_options' => 'Taslak Seçenekleri',\n    'pages_edit_save_draft' => 'Taslağı Kaydet',\n    'pages_edit_draft' => 'Sayfa Taslağını Düzenle',\n    'pages_editing_draft' => 'Taslak Düzenleniyor',\n    'pages_editing_page' => 'Sayfa Düzenleniyor',\n    'pages_edit_draft_save_at' => 'Taslak kaydedildi ',\n    'pages_edit_delete_draft' => 'Taslağı Sil',\n    'pages_edit_delete_draft_confirm' => 'Taslak sayfa değişikliklerinizi silmek istediğinizden emin misiniz? Son tam kaydetmeden bu yana yaptığınız tüm değişiklikler kaybolacak ve düzenleyici en son sayfanın taslak olmayan kaydetme durumuyla güncellenecektir.',\n    'pages_edit_discard_draft' => 'Taslağı Yoksay',\n    'pages_edit_switch_to_markdown' => 'Markdown Editörüne Geç',\n    'pages_edit_switch_to_markdown_clean' => '(Temiz İçerik)',\n    'pages_edit_switch_to_markdown_stable' => '(Kararlı İçerik)',\n    'pages_edit_switch_to_wysiwyg' => 'WYSIWYG düzenleyiciye geç',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'Değişim Günlüğünü Ayarla',\n    'pages_edit_enter_changelog_desc' => 'Yaptığınız değişiklikler hakkında kısa bir açıklama girin',\n    'pages_edit_enter_changelog' => 'Değişim Günlüğünü Yazın',\n    'pages_editor_switch_title' => 'Düzenleyici Değiştir',\n    'pages_editor_switch_are_you_sure' => 'Bu sayfa için düzenleyiciyi değiştirmek istediğinizden emin misiniz?',\n    'pages_editor_switch_consider_following' => 'Düzenleyiciyi değiştirirken aşağıdakilere dikkat edin:',\n    'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',\n    'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',\n    'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\\'t persist across this change.',\n    'pages_save' => 'Sayfayı Kaydet',\n    'pages_title' => 'Sayfa Başlığı',\n    'pages_name' => 'Sayfa İsmi',\n    'pages_md_editor' => 'Düzenleyici',\n    'pages_md_preview' => 'Ön İzleme',\n    'pages_md_insert_image' => 'Görsel Ekle',\n    'pages_md_insert_link' => 'Öge Bağlantısı Ekle',\n    'pages_md_insert_drawing' => 'Çizim Ekle',\n    'pages_md_show_preview' => 'Önizlemeyi göster',\n    'pages_md_sync_scroll' => 'Sync preview scroll',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Unsaved Drawing Found',\n    'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',\n    'pages_not_in_chapter' => 'Bu sayfa, bir bölüme ait değil',\n    'pages_move' => 'Sayfayı Taşı',\n    'pages_copy' => 'Sayfayı Kopyala',\n    'pages_copy_desination' => 'Kopyalama Hedefi',\n    'pages_copy_success' => 'Sayfa başarıyla kopyalandı',\n    'pages_permissions' => 'Sayfa İzinleri',\n    'pages_permissions_success' => 'Sayfa izinleri güncellendi',\n    'pages_revision' => 'Revizyon',\n    'pages_revisions' => 'Sayfa Revizyonları',\n    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',\n    'pages_revisions_named' => ':pageName için Sayfa Revizyonları',\n    'pages_revision_named' => ':pageName için Sayfa Revizyonu',\n    'pages_revision_restored_from' => 'Restored from #:id; :summary',\n    'pages_revisions_created_by' => 'Revize Eden',\n    'pages_revisions_date' => 'Revizyon Tarihi',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Revizyon Numarası',\n    'pages_revisions_numbered' => 'Revizyon #:id',\n    'pages_revisions_numbered_changes' => 'Revizyon #:id Değişiklikleri',\n    'pages_revisions_editor' => 'Düzenleyici Türü',\n    'pages_revisions_changelog' => 'Değişim Günlüğü',\n    'pages_revisions_changes' => 'Değişiklikler',\n    'pages_revisions_current' => 'Şimdiki Sürüm',\n    'pages_revisions_preview' => 'Ön İzleme',\n    'pages_revisions_restore' => 'Geri Dön',\n    'pages_revisions_none' => 'Bu sayfaya ait herhangi bir revizyon bulunamadı',\n    'pages_copy_link' => 'Bağlantıyı kopyala',\n    'pages_edit_content_link' => 'Düzenleyicide bölüme atla',\n    'pages_pointer_enter_mode' => 'Enter section select mode',\n    'pages_pointer_label' => 'Page Section Options',\n    'pages_pointer_permalink' => 'Page Section Permalink',\n    'pages_pointer_include_tag' => 'Page Section Include Tag',\n    'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag',\n    'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink',\n    'pages_permissions_active' => 'Sayfa İzinleri Aktif',\n    'pages_initial_revision' => 'İlk yayın',\n    'pages_references_update_revision' => 'System auto-update of internal links',\n    'pages_initial_name' => 'Yeni Sayfa',\n    'pages_editing_draft_notification' => 'Şu anda en son :timeDiff tarihinde kaydedilmiş olan taslağı düzenliyorsunuz.',\n    'pages_draft_edited_notification' => 'Bu sayfa o zamandan bu zamana güncellenmiş, bu nedenle bu taslağı yok saymanız önerilir.',\n    'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count kullanıcı, bu sayfayı düzenlemeye başladı',\n        'start_b' => ':userName, bu sayfayı düzenlemeye başladı',\n        'time_a' => 'sayfa son güncellendiğinden beri',\n        'time_b' => 'son :minCount dakikada',\n        'message' => ':start :time. Düzenlemelerinizin çakışmamasına dikkat edin!',\n    ],\n    'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',\n    'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',\n    'pages_specific' => 'Spesifik Sayfa',\n    'pages_is_template' => 'Sayfa Şablonu',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Toggle Sidebar',\n    'page_tags' => 'Sayfa Etiketleri',\n    'chapter_tags' => 'Bölüm Etiketleri',\n    'book_tags' => 'Kitap Etiketleri',\n    'shelf_tags' => 'Kitaplık Etiketleri',\n    'tag' => 'Etiket',\n    'tags' =>  'Etiketler',\n    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',\n    'tag_name' =>  'Etiket İsmi',\n    'tag_value' => 'Etiket Değeri (Opsiyonel)',\n    'tags_explain' => \"İçeriğinizi daha iyi kategorize etmek için etiket ekleyin. Etiketlere değer atayarak daha derinlemesine bir düzen elde edebilirsiniz.\",\n    'tags_add' => 'Başka etiket ekle',\n    'tags_remove' => 'Bu etiketi sil',\n    'tags_usages' => 'Toplam etiket kullanımı',\n    'tags_assigned_pages' => 'Sayfalara Atandı',\n    'tags_assigned_chapters' => 'Bölümlere Atandı',\n    'tags_assigned_books' => 'Kitaplara Atandı',\n    'tags_assigned_shelves' => 'Kitaplıklara Atandı',\n    'tags_x_unique_values' => ':count unique values',\n    'tags_all_values' => 'Tüm değerler',\n    'tags_view_tags' => 'Etiketleri Göster',\n    'tags_view_existing_tags' => 'Mevcut etiketleri göster',\n    'tags_list_empty_hint' => 'Etiketleri sayfa editörü yan menüsünden veya kitap, bölüm veya rafları düzenlerken ekleyebilirsiniz.',\n    'attachments' => 'Ekler',\n    'attachments_explain' => 'Sayfanızda göstermek için dosyalar yükleyin veya bağlantılar ekleyin. Bunlar, sayfaya ait yan menüde gösterilecektir.',\n    'attachments_explain_instant_save' => 'Burada yapılan değişiklikler anında kaydedilir.',\n    'attachments_upload' => 'Dosya Yükle',\n    'attachments_link' => 'Link Ekle',\n    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',\n    'attachments_set_link' => 'Bağlantıyı Ata',\n    'attachments_delete' => 'Bu eki silmek istediğinize emin misiniz?',\n    'attachments_dropzone' => 'Yüklemek için dosyaları buraya bırakın',\n    'attachments_no_files' => 'Hiçbir dosya yüklenmedi',\n    'attachments_explain_link' => 'Eğer dosya yüklememeyi tercih ederseniz bağlantı ekleyebilirsiniz. Bu bağlantı başka bir sayfanın veya bulut depolamadaki bir dosyanın bağlantısı olabilir.',\n    'attachments_link_name' => 'Bağlantı Adı',\n    'attachment_link' => 'Ek bağlantısı',\n    'attachments_link_url' => 'Dosya bağlantısı',\n    'attachments_link_url_hint' => 'Dosyanın veya sitenin url adresi',\n    'attach' => 'Ekle',\n    'attachments_insert_link' => 'Sayfaya Bağlantı Ekle',\n    'attachments_edit_file' => 'Dosyayı Düzenle',\n    'attachments_edit_file_name' => 'Dosya Adı',\n    'attachments_edit_drop_upload' => 'Üzerine yazılacak dosyaları sürükleyin veya seçin',\n    'attachments_order_updated' => 'Ek sıralaması güncellendi',\n    'attachments_updated_success' => 'Ek detayları güncellendi',\n    'attachments_deleted' => 'Ek silindi',\n    'attachments_file_uploaded' => 'Dosya başarıyla yüklendi',\n    'attachments_file_updated' => 'Dosya başarıyla güncellendi',\n    'attachments_link_attached' => 'Bağlantı, sayfaya başarıyla eklendi',\n    'templates' => 'Şablonlar',\n    'templates_set_as_template' => 'Bu sayfa, bir şablondur',\n    'templates_explain_set_as_template' => 'Başka sayfalar oluştururken bu sayfanın içeriğini kullanabilmek için bu sayfayı bir şablon olarak ayarlayabilirsiniz. Bu sayfayı görüntüleme yetkisi olan kullanıcılar da bu şablonu kullanabileceklerdir.',\n    'templates_replace_content' => 'Sayfa içeriğini değiştir',\n    'templates_append_content' => 'Sayfa içeriğine ekle',\n    'templates_prepend_content' => 'Sayfa içeriğinin başına ekle',\n\n    // Profile View\n    'profile_user_for_x' => 'Üyelik süresi: :time',\n    'profile_created_content' => 'Oluşturulan İçerik',\n    'profile_not_created_pages' => ':userName herhangi bir sayfa oluşturmamış',\n    'profile_not_created_chapters' => ':userName herhangi bir bölüm oluşturmamış',\n    'profile_not_created_books' => ':userName herhangi bir kitap oluşturmamış',\n    'profile_not_created_shelves' => ':userName herhangi bir kitaplık oluşturmamış',\n\n    // Comments\n    'comment' => 'Yorum',\n    'comments' => 'Yorumlar',\n    'comment_add' => 'Yorum Ekle',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Buraya bir yorum yazın',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Yorumu Gönder',\n    'comment_new' => 'Yeni Yorum',\n    'comment_created' => ':createDiff yorum yaptı',\n    'comment_updated' => ':username tarafından :updateDiff güncellendi',\n    'comment_updated_indicator' => 'Güncellendi',\n    'comment_deleted_success' => 'Yorum silindi',\n    'comment_created_success' => 'Yorum gönderildi',\n    'comment_updated_success' => 'Yorum güncellendi',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Bu yorumu silmek istediğinize emin misiniz?',\n    'comment_in_reply_to' => ':commentId yorumuna yanıt olarak',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'İşte bu sayfaya bırakılan yorumlar. Kaydedilen sayfayı görüntülerken yorumlar eklenebilir ve yönetilebilir.',\n\n    // Revision\n    'revision_delete_confirm' => 'Bu revizyonu silmek istediğinize emin misiniz?',\n    'revision_restore_confirm' => 'Bu revizyonu yeniden yüklemek istediğinize emin misiniz? Sayfanın şu anki içeriği değiştirilecektir.',\n    'revision_cannot_delete_latest' => 'Son revizyonu silemezsiniz.',\n\n    // Copy view\n    'copy_consider' => 'İçeriği kopyalarken aşağıdakileri hesaba katınız.',\n    'copy_consider_permissions' => 'Özel izin ayarları kopyalanmayacak.',\n    'copy_consider_owner' => 'Tüm kopyalanan içeriğin sahibi olacaksınız.',\n    'copy_consider_images' => 'Sayfa resim dosyalarının ikinci bir kopyası oluşturulmayıp, resimlerin ilk yüklendiği sayfadaki bağlantısı tutulacaktır.',\n    'copy_consider_attachments' => 'Sayfa ekleri kopyalanmayacak.',\n    'copy_consider_access' => 'Konum, sahiplik veya izinlerde yapılan bir değişiklik önceden erişimi olmayanlara erişim hakkı kazandırabilir.',\n\n    // Conversions\n    'convert_to_shelf' => 'Kitaplığa Dünüştür',\n    'convert_to_shelf_contents_desc' => 'Bu kitabı aynı içeriğe sahip yeni bir rafa dönüştürebilirsiniz. Bu kitapta yer alan bölümler yeni kitaplığa dönüştürülecektir. Bu kitapta bölüm olmayan herhangi bir sayfa varsa, bu kitap yeniden adlandırılacak ve bu sayfaları içerecek ve bu kitap yeni kitaplığın bir parçası haline gelecektir.',\n    'convert_to_shelf_permissions_desc' => 'Bu kitapta ayarlanan tüm izinler yeni kitaplığa ve kendi izinleri uygulanmayan tüm yeni alt kitaplara kopyalanacaktır. Kitaplıklardaki izinlerin, kitaplarda olduğu gibi içindeki içeriğe otomatik olarak kademelendirilmediğini unutmayın.',\n    'convert_book' => 'Kitabı Dönüştür',\n    'convert_book_confirm' => 'Bu kitabı dönüştürmek istediğinize emin misiniz?',\n    'convert_undo_warning' => 'Bu kolay kolay geri alınamaz.',\n    'convert_to_book' => 'Kitaba Dönüştür',\n    'convert_to_book_desc' => 'Bu bölümü aynı içeriğe sahip yeni bir kitaba dönüştürebilirsiniz. Bu bölümde ayarlanan tüm izinler yeni kitaba kopyalanacaktır, ancak ana kitaptan devralınan izinler kopyalanmayacaktır, bu da erişim kontrolünün değişmesine neden olabilir.',\n    'convert_chapter' => 'Bölümü Dönüştür',\n    'convert_chapter_confirm' => 'Bu bölümü dönüştürmek istediğinizden emin misiniz?',\n\n    // References\n    'references' => 'Referanslar',\n    'references_none' => 'Bu öğeye ilişkin takip edilen bir referans bulunmamaktadır.',\n    'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',\n\n    // Watch Options\n    'watch' => 'Watch',\n    'watch_title_default' => 'Default Preferences',\n    'watch_desc_default' => 'Revert watching to just your default notification preferences.',\n    'watch_title_ignore' => 'Ignore',\n    'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.',\n    'watch_title_new' => 'New Pages',\n    'watch_desc_new' => 'Notify when any new page is created within this item.',\n    'watch_title_updates' => 'All Page Updates',\n    'watch_desc_updates' => 'Notify upon all new pages and page changes.',\n    'watch_desc_updates_page' => 'Notify upon all page changes.',\n    'watch_title_comments' => 'All Page Updates & Comments',\n    'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.',\n    'watch_desc_comments_page' => 'Notify upon page changes and new comments.',\n    'watch_change_default' => 'Change default notification preferences',\n    'watch_detail_ignore' => 'Ignoring notifications',\n    'watch_detail_new' => 'Watching for new pages',\n    'watch_detail_updates' => 'Watching new pages and updates',\n    'watch_detail_comments' => 'Watching new pages, updates & comments',\n    'watch_detail_parent_book' => 'Watching via parent book',\n    'watch_detail_parent_book_ignore' => 'Ignoring via parent book',\n    'watch_detail_parent_chapter' => 'Watching via parent chapter',\n    'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter',\n];\n"
  },
  {
    "path": "lang/tr/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Bu sayfaya erişim izniniz bulunmuyor.',\n    'permissionJson' => 'Bu işlemi yapmaya yetkiniz bulunmuyor.',\n\n    // Auth\n    'error_user_exists_different_creds' => ':email e-posta adresine sahip bir kullanıcı zaten var.',\n    'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',\n    'email_already_confirmed' => 'E-posta adresi zaten doğrulanmış, giriş yapmayı deneyin.',\n    'email_confirmation_invalid' => 'Bu doğrulama kodu ya geçersiz ya da daha önce kullanılmış, lütfen tekrar kaydolmayı deneyin.',\n    'email_confirmation_expired' => 'Doğrulama kodunun süresi doldu, yeni bir doğrulama kodu e-posta adresine gönderildi.',\n    'email_confirmation_awaiting' => 'Bu hesaba ait e-posta adresinin doğrulanması gerekiyor',\n    'ldap_fail_anonymous' => 'Anonim olarak gerçekleştirilmeye çalışılan LDAP erişimi başarısız oldu',\n    'ldap_fail_authed' => 'Verilen bilgiler kullanılarak gerçekleştirilmeye çalışılan LDAP erişimi başarısız oldu',\n    'ldap_extension_not_installed' => 'LDAP PHP eklentisi kurulu değil',\n    'ldap_cannot_connect' => 'LDAP sunucusuna bağlanılamadı, ilk bağlantı başarısız oldu',\n    'saml_already_logged_in' => 'Zaten giriş yapılmış',\n    'saml_no_email_address' => 'Harici kimlik doğrulama sisteminden gelen veriler, bu kullanıcının e-posta adresini içermiyor',\n    'saml_invalid_response_id' => 'Harici doğrulama sistemi tarafından sağlanan bir veri talebi, bu uygulama tarafından başlatılan bir işlem tarafından tanınamadı. Giriş yaptıktan sonra geri dönmek bu soruna yol açmış olabilir.',\n    'saml_fail_authed' => ':system kullanarak giriş yapma başarısız oldu; sistem, başarılı bir kimlik doğrulama sağlayamadı',\n    'oidc_already_logged_in' => 'Zaten oturum açılmış',\n    'oidc_no_email_address' => 'Harici kimlik doğrulama sisteminden gelen veriler, bu kullanıcının e-posta adresini içermiyor',\n    'oidc_fail_authed' => ':system kullanarak giriş yapma başarısız oldu; sistem, başarılı bir kimlik doğrulama sağlayamadı',\n    'social_no_action_defined' => 'Herhangi bir eylem tanımlanmamış',\n    'social_login_bad_response' => \":socialAccount girişi sırasında bir hata meydana geldi: \\n:error\",\n    'social_account_in_use' => 'Bu :socialAccount zaten kullanımda, :socialAccount hesabıyla giriş yapmayı deneyin.',\n    'social_account_email_in_use' => ':email e-posta adresi zaten kullanılıyor. Zaten bir hesabınız varsa, :socialAccount hesabınızı profil ayarlarınızdan mevcut hesabınıza bağlayabilirsiniz.',\n    'social_account_existing' => 'Bu :socialAccount zaten hesabınıza bağlanmış.',\n    'social_account_already_used_existing' => 'Bu :socialAccount, başka bir kullanıcı tarafından kullanılıyor.',\n    'social_account_not_used' => 'Bu :socialAccount hesabı hiç bir kullanıcıya bağlanmamış. Lütfen profil ayarlarınızdan mevcut hesabınıza bağlayınız. ',\n    'social_account_register_instructions' => 'Hâlâ bir hesabınız yoksa, :socialAccount aracılığıyla kaydolabilirsiniz.',\n    'social_driver_not_found' => 'Social driver bulunamadı',\n    'social_driver_not_configured' => ':socialAccount ayarlarınız doğru bir şekilde ayarlanmadı.',\n    'invite_token_expired' => 'Davetiye bağlantısının süresi doldu. Bunun yerine parolanızı sıfırlamayı deneyebilirsiniz.',\n    'login_user_not_found' => 'A user for this action could not be found.',\n\n    // System\n    'path_not_writable' => ':filePath dosya yolu yüklenemedi. Sunucuya yazılabilir olduğundan emin olun.',\n    'cannot_get_image_from_url' => ':url adresindeki görsel alınamadı',\n    'cannot_create_thumbs' => 'Sunucu, görsel ön izlemelerini oluşturamadı. Lütfen GD PHP eklentisinin kurulu olduğundan emin olun.',\n    'server_upload_limit' => 'Sunucu bu boyutta dosya yüklemenize izin vermiyor. Lütfen daha küçük bir dosya deneyin.',\n    'server_post_limit' => 'The server cannot receive the provided amount of data. Try again with less data or a smaller file.',\n    'uploaded'  => 'Sunucu bu boyutta dosya yüklemenize izin vermiyor. Lütfen daha küçük bir dosya deneyin.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Görsel yüklenirken bir hata meydana geldi',\n    'image_upload_type_error' => 'Yüklemeye çalıştığınız dosya türü geçersizdir',\n    'image_upload_replace_type' => 'Görsel dosyası değişimleri, aynı dosya uzantı tipinde olmalı',\n    'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.',\n    'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.',\n    'image_gallery_thumbnail_memory_limit' => 'Failed to create gallery thumbnails due to system resource limits.',\n    'drawing_data_not_found' => 'Çizim verileri yüklenemedi. Çizim dosyası artık mevcut olmayabilir veya erişim izniniz olmayabilir.',\n\n    // Attachments\n    'attachment_not_found' => 'Ek bulunamadı',\n    'attachment_upload_error' => 'Ekler yüklenirken bir hata oluştu',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Taslak kaydetme başarısız oldu. Bu sayfayı kaydetmeden önce internet bağlantınız olduğundan emin olun',\n    'page_draft_delete_fail' => 'Sayfa taslağı silinemedi ve geçerli sayfanın kayıtlı içeriği getirilemedi',\n    'page_custom_home_deletion' => 'Bu sayfa, \"Ana Sayfa\" olarak ayarlandığı için silinemez',\n\n    // Entities\n    'entity_not_found' => 'Öge bulunamadı',\n    'bookshelf_not_found' => 'Kitaplık bulunamadı',\n    'book_not_found' => 'Kitap bulunamadı',\n    'page_not_found' => 'Sayfa bulunamadı',\n    'chapter_not_found' => 'Bölüm bulunamadı',\n    'selected_book_not_found' => 'Seçilen kitap bulunamadı',\n    'selected_book_chapter_not_found' => 'Seçilen kitap veya bölüm bulunamadı',\n    'guests_cannot_save_drafts' => 'Misafirler taslak kaydedemezler',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Tek olan yöneticiyi silemezsiniz',\n    'users_cannot_delete_guest' => 'Misafir kullanıyıcıyı silemezsiniz',\n    'users_could_not_send_invite' => 'Could not create user since invite email failed to send',\n\n    // Roles\n    'role_cannot_be_edited' => 'Bu rol düzenlenemez',\n    'role_system_cannot_be_deleted' => 'Bu rol, bir sistem rolüdür ve silinemez',\n    'role_registration_default_cannot_delete' => 'Bu rol, kaydolan üyelere varsayılan olarak atandığı için silinemez',\n    'role_cannot_remove_only_admin' => 'Bu kullanıcı, yönetici rolü olan tek kullanıcı olduğu için silinemez. Bu kullanıcıyı silmek için önce başka bir kullanıcıya yönetici rolü atayın.',\n\n    // Comments\n    'comment_list' => 'Yorumlar yüklenirken bir hata oluştu.',\n    'cannot_add_comment_to_draft' => 'Taslaklara yorum ekleyemezsiniz.',\n    'comment_add' => 'Yorum eklerken/güncellerken bir hata olıuştu.',\n    'comment_delete' => 'Yorum silinirken bir hata oluştu.',\n    'empty_comment' => 'Boş bir yorum ekleyemezsiniz.',\n\n    // Error pages\n    '404_page_not_found' => 'Sayfa Bulunamadı',\n    'sorry_page_not_found' => 'Üzgünüz, aradığınız sayfa bulunamıyor.',\n    'sorry_page_not_found_permission_warning' => 'Bu sayfanın var olduğunu düşünüyorsanız, görüntüleme iznine sahip olmayabilirsiniz.',\n    'image_not_found' => 'Görsel Bulunamadı',\n    'image_not_found_subtitle' => 'Üzgünüz, aradığınız görsel dosyası bulunamadı.',\n    'image_not_found_details' => 'Bu resmin var olmasını bekliyorsanız silinmiş olabilir.',\n    'return_home' => 'Ana sayfaya dön',\n    'error_occurred' => 'Bir Hata Oluştu',\n    'app_down' => ':appName şu anda erişilemez durumda',\n    'back_soon' => 'En kısa sürede tekrar erişilebilir duruma gelecektir.',\n\n    // Import\n    'import_zip_cant_read' => 'Could not read ZIP file.',\n    'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',\n    'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Import ZIP failed to validate with errors:',\n    'import_zip_failed_notification' => 'Failed to import ZIP file.',\n    'import_perms_books' => 'You are lacking the required permissions to create books.',\n    'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',\n    'import_perms_pages' => 'You are lacking the required permissions to create pages.',\n    'import_perms_images' => 'You are lacking the required permissions to create images.',\n    'import_perms_attachments' => 'You are lacking the required permission to create attachments.',\n\n    // API errors\n    'api_no_authorization_found' => 'Yapılan istekte, yetkilendirme anahtarı bulunamadı',\n    'api_bad_authorization_format' => 'Yapılan istekte bir yetkilendirme anahtarı bulundu fakat doğru görünmüyor',\n    'api_user_token_not_found' => 'Sağlanan yetkilendirme anahtarı ile eşleşen bir API anahtarı bulunamadı',\n    'api_incorrect_token_secret' => 'Kullanılan API için sağlanan gizli anahtar doğru değil',\n    'api_user_no_api_permission' => 'Kullanılan API anahtarının sahibi API çağrısı yapmak için izne sahip değil',\n    'api_user_token_expired' => 'Kullanılan yetkilendirme anahtarının süresi doldu',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Test e-postası gönderilirken bir hata meydana geldi:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'The URL does not match the configured allowed SSR hosts',\n];\n"
  },
  {
    "path": "lang/tr/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'New comment on page: :pageName',\n    'new_comment_intro' => 'A user has commented on a page in :appName:',\n    'new_page_subject' => 'Yeni sayfa :pageName',\n    'new_page_intro' => 'A new page has been created in :appName:',\n    'updated_page_subject' => 'Updated page: :pageName',\n    'updated_page_intro' => 'A page has been updated in :appName:',\n    'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\\'t be sent notifications for further edits to this page by the same editor.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Page Name:',\n    'detail_page_path' => 'Page Path:',\n    'detail_commenter' => 'Commenter:',\n    'detail_comment' => 'Comment:',\n    'detail_created_by' => 'Created By:',\n    'detail_updated_by' => 'Updated By:',\n\n    'action_view_comment' => 'View Comment',\n    'action_view_page' => 'View Page',\n\n    'footer_reason' => 'This notification was sent to you because :link cover this type of activity for this item.',\n    'footer_reason_link' => 'your notification preferences',\n];\n"
  },
  {
    "path": "lang/tr/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Önceki',\n    'next'     => 'Sonraki &raquo;',\n\n];\n"
  },
  {
    "path": "lang/tr/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Şifreniz en az 6 karakterden oluşmalı ve her iki şifre de birbiri ile eşleşmelidir.',\n    'user' => \"Bu e-posta adresine sahip bir kullanıcı bulamadık.\",\n    'token' => 'Şifre sıfırlama anahtarı, bu e-posta adresi için geçersizdir.',\n    'sent' => 'Şifre sıfırlama bağlantısını e-posta adresinize gönderdik!',\n    'reset' => 'Şifreniz sıfırlandı!',\n\n];\n"
  },
  {
    "path": "lang/tr/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'My Account',\n\n    'shortcuts' => 'Kısayollar',\n    'shortcuts_interface' => 'UI Shortcut Preferences',\n    'shortcuts_toggle_desc' => 'Burada, gezinme ve eylemler için kullanılan klavye sistem arayüzü kısayollarını etkinleştirebilir veya devre dışı bırakabilirsiniz.',\n    'shortcuts_customize_desc' => 'Aşağıdaki kısayolların her birini özelleştirebilirsiniz. Bir kısayol için girişi seçtikten sonra istediğiniz tuş kombinasyonuna basmanız yeterlidir.',\n    'shortcuts_toggle_label' => 'Klavye kısayolları etkinleştirildi',\n    'shortcuts_section_navigation' => 'Navigasyon',\n    'shortcuts_section_actions' => 'Ortak Eylemler',\n    'shortcuts_save' => 'Kısayolları Kaydet',\n    'shortcuts_overlay_desc' => 'Not: Kısayollar etkinleştirildiğinde, \"?\" tuşuna basılarak o anda ekranda görünen eylemler için mevcut kısayolları vurgulayan bir yardımcı yer paylaşımı kullanılabilir.',\n    'shortcuts_update_success' => 'Kısayol tercihleri güncellendi!',\n    'shortcuts_overview_desc' => 'Manage keyboard shortcuts you can use to navigate the system user interface.',\n\n    'notifications' => 'Notification Preferences',\n    'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.',\n    'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own',\n    'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Notify upon replies to my comments',\n    'notifications_save' => 'Save Preferences',\n    'notifications_update_success' => 'Notification preferences have been updated!',\n    'notifications_watched' => 'Watched & Ignored Items',\n    'notifications_watched_desc' => 'Below are the items that have custom watch preferences applied. To update your preferences for these, view the item then find the watch options in the sidebar.',\n\n    'auth' => 'Access & Security',\n    'auth_change_password' => 'Change Password',\n    'auth_change_password_desc' => 'Change the password you use to log-in to the application. This must be at least 8 characters long.',\n    'auth_change_password_success' => 'Password has been updated!',\n\n    'profile' => 'Profile Details',\n    'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.',\n    'profile_view_public' => 'View Public Profile',\n    'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.',\n    'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.',\n    'profile_email_no_permission' => 'Unfortunately you don\\'t have permission to change your email address. If you want to change this, you\\'d need to ask an administrator to change this for you.',\n    'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.',\n    'profile_admin_options' => 'Administrator Options',\n    'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the \"Settings > Users\" area of the application.',\n\n    'delete_account' => 'Delete Account',\n    'delete_my_account' => 'Delete My Account',\n    'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\\'ve created, such as created pages and uploaded images, will remain.',\n    'delete_my_account_warning' => 'Are you sure you want to delete your account?',\n];\n"
  },
  {
    "path": "lang/tr/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Ayarlar',\n    'settings_save' => 'Ayarları Kaydet',\n    'system_version' => 'Sistem Sürümü',\n    'categories' => 'Kategoriler',\n\n    // App Settings\n    'app_customization' => 'Özelleştirme',\n    'app_features_security' => 'Özellikler & Güvenlik',\n    'app_name' => 'Uygulama Adı',\n    'app_name_desc' => 'Bu isim, başlıkta ve sistem tarafından gönderilen bütün e-postalarda gösterilecektir.',\n    'app_name_header' => 'İsmi başlıkta göster',\n    'app_public_access' => 'Açık Erişim',\n    'app_public_access_desc' => 'Bu özelliği aktifleştirmek, giriş yapmamış misafir kullanıcıların BookStack uygulamanıza erişimini sağlar.',\n    'app_public_access_desc_guest' => 'Kayıtlı olmayan kullanıcılar için erişim yetkileri, \"Guest\" kullanıcısı üzerinden kontrol edilebilir.',\n    'app_public_access_toggle' => 'Açık erişime izin ver',\n    'app_public_viewing' => 'Herkese açık görüntülemeye izin verilsin mi?',\n    'app_secure_images' => 'Daha Güvenli Görsel Yüklemeleri',\n    'app_secure_images_toggle' => 'Daha güvenli görsel yüklemelerini aktifleştir',\n    'app_secure_images_desc' => 'Bütün görseller, performans sebepleri nedeniyle herkes tarafından görüntülenebilir durumdadır. Bu seçeneği aktif ederseniz; görsel bağlantılarının önüne rastgele, tahmini zor karakterler eklenmesini sağlarsınız. Kolay erişimin önlenmesi için dizin indekslerinin kapalı olduğundan emin olun.',\n    'app_default_editor' => 'Varsayılan Yazı Düzenleyici',\n    'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.',\n    'app_custom_html' => 'Özel HTML \"Head\" İçeriği',\n    'app_custom_html_desc' => 'Buraya yazacağınız içerik, <head> etiketinin içine ve en sonuna eklenecektir. Bu işlem, stil değişikliklerinin uygulanmasında ya da analytics kodlarının eklenmesinde yararlı olmaktadır.',\n    'app_custom_html_disabled_notice' => 'Olası hatalı değişikliklerin geriye alınabilmesi için bu sayfanın özelleştirilmiş HTML \"head\" içeriği devre dışı bırakıldı.',\n    'app_logo' => 'Uygulama Logosu',\n    'app_logo_desc' => 'This is used in the application header bar, among other areas. This image should be 86px in height. Large images will be scaled down.',\n    'app_icon' => 'Uygulama Simgesi',\n    'app_icon_desc' => 'This icon is used for browser tabs and shortcut icons. This should be a 256px square PNG image.',\n    'app_homepage' => 'Ana Sayfa',\n    'app_homepage_desc' => 'Varsayılan görünüm yerine ana sayfada görünmesi için bir görünüm seçin. Sayfa izinleri, burada seçeceğiniz sayfalar için yok sayılacaktır.',\n    'app_homepage_select' => 'Bir sayfa seçin',\n    'app_footer_links' => 'Altbilgi Bağlantıları',\n    'app_footer_links_desc' => 'Add links to show within the site footer. These will be displayed at the bottom of most pages, including those that do not require login. You can use a label of \"trans::<key>\" to use system-defined translations. For example: Using \"trans::common.privacy_policy\" will provide the translated text \"Privacy Policy\" and \"trans::common.terms_of_service\" will provide the translated text \"Terms of Service\".',\n    'app_footer_links_label' => 'Bağlantı Etiketi',\n    'app_footer_links_url' => 'Bağlantı adresi',\n    'app_footer_links_add' => 'Altbilgi Bağlantısı Ekle',\n    'app_disable_comments' => 'Yorumları Devre Dışı Bırak',\n    'app_disable_comments_toggle' => 'Yorumları devre dışı bırak',\n    'app_disable_comments_desc' => 'Bütün sayfalar için yorumları devre dışı bırakır. <br> Mevcut yorumlar gösterilmeyecektir.',\n\n    // Color settings\n    'color_scheme' => 'Application Color Scheme',\n    'color_scheme_desc' => 'Set the colors to use in the application user interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.',\n    'ui_colors_desc' => 'Set the application primary color and default link color. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the application interface.',\n    'app_color' => 'Birincil Renk',\n    'link_color' => 'Varsayılan Bağlantı Rengi',\n    'content_colors_desc' => 'Set colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.',\n    'bookshelf_color' => 'Raf Rengi',\n    'book_color' => 'Kitap Rengi',\n    'chapter_color' => 'Bölüm Rengi',\n    'page_color' => 'Sayfa Rengi',\n    'page_draft_color' => 'Sayfa Taslağı Rengi',\n\n    // Registration Settings\n    'reg_settings' => 'Kayıt İşlemleri',\n    'reg_enable' => 'Kayıtları Aktifleştir',\n    'reg_enable_toggle' => 'Kayıtları aktifleştir',\n    'reg_enable_desc' => 'Kullanıcılar kaydolduktan sonra sizin belirleyeceğiniz bir role otomatik olarak atanabilirler.',\n    'reg_default_role' => 'Kayıttan sonraki varsayılan kullanıcı rolü',\n    'reg_enable_external_warning' => 'Harici LDAP veya SAML kimlik doğrulaması etkinken yukarıdaki seçenek yok sayılır. Mevcut harici üyelere yönelik kimlik doğrulama başarılı olursa, mevcut olmayan üyelerin kullanıcı hesapları otomatik olarak oluşturulur.',\n    'reg_email_confirmation' => 'E-posta Doğrulaması',\n    'reg_email_confirmation_toggle' => 'E-posta doğrulamasını zorunlu kıl',\n    'reg_confirm_email_desc' => 'Eğer alan adı kısıtlaması kullanılıyorsa, bu seçenek yok sayılarak e-posta doğrulaması zorunlu kılınacaktır.',\n    'reg_confirm_restrict_domain' => 'Alan Adı Kısıtlaması',\n    'reg_confirm_restrict_domain_desc' => 'Kısıtlamak istediğiniz e-posta alan adlarını vigül ile ayırarak yazınız. Kullanıcılara, uygulamaya erişmeden önce adreslerini doğrulamaları için bir e-posta gönderilecektir. <br> Başarılı bir kayıt işleminden sonra kullanıcıların e-posta adreslerini değiştirebileceklerini unutmayın.',\n    'reg_confirm_restrict_domain_placeholder' => 'Hiçbir kısıtlama tanımlanmamış',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Bakım',\n    'maint_image_cleanup' => 'Görselleri Temizle',\n    'maint_image_cleanup_desc' => 'Sayfaları ve revizyon içeriklerini tarayarak hangi görsellerin ve çizimlerin kullanımda olduğunu ve hangilerinin gereksiz olduğunu tespit eder. Bunu başlatmadan önce veritabanının ve görsellerin tam bir yedeğinin alındığından emin olun.',\n    'maint_delete_images_only_in_revisions' => 'Eski sayfa revizyonlarındaki görselleri de sil',\n    'maint_image_cleanup_run' => 'Temizliği Başlat',\n    'maint_image_cleanup_warning' => 'Muhtemelen kullanılmayan :count adet görsel bulundu. Bu görselleri silmek istediğinize emin misiniz?',\n    'maint_image_cleanup_success' => 'Muhtemelen kullanılmayan :count adet görsel bulundu ve silindi!',\n    'maint_image_cleanup_nothing_found' => 'Kullanılmayan görsel bulunamadığından hiçbir şey silinmedi!',\n    'maint_send_test_email' => 'Deneme E-postası Gönder',\n    'maint_send_test_email_desc' => 'Bu işlem, profilinize tanımladığınız e-posta adresine bir deneme e-postası gönderir.',\n    'maint_send_test_email_run' => 'Deneme e-postasını gönder',\n    'maint_send_test_email_success' => 'E-posta, :address adresine gönderildi',\n    'maint_send_test_email_mail_subject' => 'Deneme E-postası',\n    'maint_send_test_email_mail_greeting' => 'E-posta iletimi çalışıyor gibi görünüyor!',\n    'maint_send_test_email_mail_text' => 'Tebrikler! Eğer bu e-posta bildirimini alıyorsanız, e-posta ayarlarınız doğru bir şekilde ayarlanmış demektir.',\n    'maint_recycle_bin_desc' => 'Silinen raflar, kitaplar, bölümler ve sayfalar geri dönüşüm kutusuna gönderilir, böylece geri yüklenebilir veya kalıcı olarak silinebilir. Geri dönüşüm kutusundaki daha eski öğeler, sistem yapılandırmasına bağlı olarak bir süre sonra otomatik olarak kaldırılabilir.',\n    'maint_recycle_bin_open' => 'Geri Dönüşüm Kutusunu Aç',\n    'maint_regen_references' => 'Referansları Yeniden Oluştur',\n    'maint_regen_references_desc' => 'Bu eylem, veritabanındaki çapraz öğe referans dizinini yeniden oluşturur. Bu işlem genellikle otomatik olarak gerçekleştirilir ancak bu eylem eski içerikleri veya resmi olmayan yöntemlerle eklenen içerikleri indekslemek için yararlı olabilir.',\n    'maint_regen_references_success' => 'Referans dizini yeniden oluşturuldu!',\n    'maint_timeout_command_note' => 'Not: Bu eylemin çalışması zaman alabilir, bu da bazı web ortamlarında zaman aşımı sorunlarına yol açabilir. Alternatif olarak, bu eylem bir terminal komutu kullanılarak gerçekleştirilebilir.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Geri Dönüşüm Kutusu',\n    'recycle_bin_desc' => 'Burada silinen öğeleri geri yükleyebilir veya bunları sistemden kalıcı olarak kaldırmayı seçebilirsiniz. Bu liste, izin filtrelerinin uygulandığı sistemdeki benzer etkinlik listelerinden farklı olarak filtrelenmez.',\n    'recycle_bin_deleted_item' => 'Silinen öge',\n    'recycle_bin_deleted_parent' => 'Üst',\n    'recycle_bin_deleted_by' => 'Tarafından silindi',\n    'recycle_bin_deleted_at' => 'Silinme Zamanı',\n    'recycle_bin_permanently_delete' => 'Kalıcı Olarak Sil',\n    'recycle_bin_restore' => 'Geri Yükle',\n    'recycle_bin_contents_empty' => 'Geri dönüşüm kutusu boş',\n    'recycle_bin_empty' => 'Geri Dönüşüm Kutusunu Boşalt',\n    'recycle_bin_empty_confirm' => 'Bu işlem, her bir öğenin içinde bulunan içerik de dahil olmak üzere geri dönüşüm kutusundaki tüm öğeleri kalıcı olarak imha edecektir. Geri dönüşüm kutusunu boşaltmak istediğinizden emin misiniz?',\n    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?',\n    'recycle_bin_destroy_list' => 'Kalıcı Olarak Silinecek Öğeler',\n    'recycle_bin_restore_list' => 'Geri Yüklenecek Öğeler',\n    'recycle_bin_restore_confirm' => 'Bu eylem, tüm alt öğeler dahil olmak üzere silinen öğeyi orijinal konumlarına geri yükleyecektir. Orijinal konum o zamandan beri silinmişse ve şimdi geri dönüşüm kutusunda bulunuyorsa, üst öğenin de geri yüklenmesi gerekecektir.',\n    'recycle_bin_restore_deleted_parent' => 'Bu öğenin üst öğesi de silindi. Bunlar, üst öğe de geri yüklenene kadar silinmiş olarak kalacaktır.',\n    'recycle_bin_restore_parent' => 'Restore Parent',\n    'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',\n    'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',\n\n    // Audit Log\n    'audit' => 'Denetim Kaydı',\n    'audit_desc' => 'Bu denetim günlüğü, sistemde izlenen etkinliklerin bir listesini görüntüler. Bu liste, izin filtrelerinin uygulandığı sistemdeki benzer etkinlik listelerinden farklı olarak filtrelenmez.',\n    'audit_event_filter' => 'Etkinlik Filtresi',\n    'audit_event_filter_no_filter' => 'Filtre Yok',\n    'audit_deleted_item' => 'Silinen Öge',\n    'audit_deleted_item_name' => 'Isim: :name',\n    'audit_table_user' => 'Kullanıcı',\n    'audit_table_event' => 'Etkinlik',\n    'audit_table_related' => 'İlgili Öğe veya Detay',\n    'audit_table_ip' => 'IP Adresi',\n    'audit_table_date' => 'Aktivite Tarihi',\n    'audit_date_from' => 'Tarih Aralığından',\n    'audit_date_to' => 'Tarih Aralığına',\n\n    // Role Settings\n    'roles' => 'Roller',\n    'role_user_roles' => 'Kullanıcı Rolleri',\n    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',\n    'roles_x_users_assigned' => ':count user assigned|:count users assigned',\n    'roles_x_permissions_provided' => ':count permission|:count permissions',\n    'roles_assigned_users' => 'Assigned Users',\n    'roles_permissions_provided' => 'Provided Permissions',\n    'role_create' => 'Yeni Rol Oluştur',\n    'role_delete' => 'Rolü Sil',\n    'role_delete_confirm' => 'Bu işlem, \\':roleName\\' adlı rolü silecektir.',\n    'role_delete_users_assigned' => 'Bu role atanmış :userCount adet kullanıcı var. Eğer bu kullanıcıların rollerini değiştirmek istiyorsanız, aşağıdan yeni bir rol seçin.',\n    'role_delete_no_migration' => \"Kullanıcıları taşıma\",\n    'role_delete_sure' => 'Bu rolü silmek istediğinize emin misiniz?',\n    'role_edit' => 'Rolü Düzenle',\n    'role_details' => 'Rol Detayları',\n    'role_name' => 'Rol Adı',\n    'role_desc' => 'Rolün Kısa Tanımı',\n    'role_mfa_enforced' => 'Çok Aşamalı Kimlik Doğrulama Gerekiyor',\n    'role_external_auth_id' => 'Harici Doğrulama Kimlikleri',\n    'role_system' => 'Sistem Yetkileri',\n    'role_manage_users' => 'Kullanıcıları yönet',\n    'role_manage_roles' => 'Rolleri ve rol izinlerini yönet',\n    'role_manage_entity_permissions' => 'Bütün kitap, bölüm ve sayfa izinlerini yönet',\n    'role_manage_own_entity_permissions' => 'Kendine ait kitabın, bölümün ve sayfaların izinlerini yönet',\n    'role_manage_page_templates' => 'Sayfa şablonlarını yönet',\n    'role_access_api' => 'Sistem programlama arayüzüne (API) eriş',\n    'role_manage_settings' => 'Uygulama ayarlarını yönet',\n    'role_export_content' => 'İçeriği dışa aktar',\n    'role_import_content' => 'Import content',\n    'role_editor_change' => 'Yazı editörünü değiştir',\n    'role_notifications' => 'Receive & manage notifications',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Varlık Yetkileri',\n    'roles_system_warning' => 'Yukarıdaki üç izinden herhangi birine erişimin, kullanıcının kendi ayrıcalıklarını veya sistemdeki diğerlerinin ayrıcalıklarını değiştirmesine izin verebileceğini unutmayın. Yalnızca bu izinlere sahip rolleri güvenilir kullanıcılara atayın.',\n    'role_asset_desc' => 'Bu izinler, sistem içindeki varlıklara varsayılan erişim izinlerini ayarlar. Kitaplar, bölümler ve sayfalar üzerindeki izinler, buradaki izinleri geçersiz kılar.',\n    'role_asset_admins' => 'Yöneticilere otomatik olarak bütün içeriğe erişim yetkisi verilir ancak bu seçenekler, kullanıcı arayüzündeki bazı seçeneklerin gösterilmesine veya gizlenmesine neden olabilir.',\n    'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'Hepsi',\n    'role_own' => 'Kendine Ait',\n    'role_controlled_by_asset' => 'Yüklendikleri varlık tarafından kontrol ediliyor',\n    'role_save' => 'Rolü Kaydet',\n    'role_users' => 'Bu roldeki kullanıcılar',\n    'role_users_none' => 'Bu role henüz bir kullanıcı atanmadı',\n\n    // Users\n    'users' => 'Kullanıcılar',\n    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',\n    'user_profile' => 'Kullanıcı Profili',\n    'users_add_new' => 'Yeni Kullanıcı Ekle',\n    'users_search' => 'Kullanıcı Ara',\n    'users_latest_activity' => 'Son Etkinlik',\n    'users_details' => 'Kullanıcı Detayları',\n    'users_details_desc' => 'Bu kullanıcı için gösterilecek bir isim ve e-posta adresi belirleyin. Buraya yazacağınız e-posta adresi, uygulamaya giriş yaparken kullanılacaktır.',\n    'users_details_desc_no_email' => 'Diğer kullanıcılar tarafından tanınabilmesi için bir isim belirleyin.',\n    'users_role' => 'Kullanıcı Rolleri',\n    'users_role_desc' => 'Bu kullanıcının hangi rollere atanacağını belirleyin. Birden fazla role sahip kullanıcılar, atandığı bütün rollerin yetkilerine sahip olurlar.',\n    'users_password' => 'Kullanıcı Şifresi',\n    'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',\n    'users_send_invite_text' => 'Bu kullanıcıya kendi şifresini belirleyebilmesi için bir davetiye e-postası gönderebilir ya da kullanıcının şifresini kendiniz belirleyebilirsiniz.',\n    'users_send_invite_option' => 'Kullanıcıya davetiye e-postası gönder',\n    'users_external_auth_id' => 'Harici Doğrulama Kimliği',\n    'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.',\n    'users_password_warning' => 'Only fill the below if you would like to change the password for this user.',\n    'users_system_public' => 'Bu kullanıcı, uygulamanızı ziyaret eden bütün misafir kullanıcıları temsil eder. Giriş yapmak için kullanılamaz ancak otomatik olarak atanır.',\n    'users_delete' => 'Kullanıcıyı Sil',\n    'users_delete_named' => ':userName kullanıcısını sil ',\n    'users_delete_warning' => 'Bu işlem \\':userName\\' kullanıcısını sistemden tamamen silecektir.',\n    'users_delete_confirm' => 'Bu kullanıcıyı tamamen silmek istediğinize emin misiniz?',\n    'users_migrate_ownership' => 'Sahipliği Taşıyın',\n    'users_migrate_ownership_desc' => 'Başka bir kullanıcının şu anda bu kullanıcıya ait olan tüm öğelerin sahibi olmasını istiyorsanız buradan bir kullanıcı seçin.',\n    'users_none_selected' => 'Hiçbir kullanıcı seçilmedi',\n    'users_edit' => 'Kullanıcıyı Düzenle',\n    'users_edit_profile' => 'Profili Düzenle',\n    'users_avatar' => 'Avatar',\n    'users_avatar_desc' => 'Bu kullanıcıyı temsil eden bir görsel seçin. Bu görsel yaklaşık 256px boyutunda bir kare olmalıdır.',\n    'users_preferred_language' => 'Tercih Edilen Dil',\n    'users_preferred_language_desc' => 'Bu seçenek, kullanıcı arayüzünün dilini değiştirmek için kullanılır. Burada yapılan değişiklik herhangi bir kullanıcı tarafından oluşturulmuş içeriği etkilemeyecektir.',\n    'users_social_accounts' => 'Sosyal Hesaplar',\n    'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.',\n    'users_social_accounts_info' => 'Buraya diğer hesaplarınızı ekleyerek, uygulamaya daha hızlı ve kolay bir giriş sağlayabilirsiniz. Bir hesabın bağlantısını kesmek daha önce sahip olduğunuz erişimi kaldırmaz. Bağlı sosyal hesabınızın erişimini, profil ayarlarınızdan kaldırabilirsiniz.',\n    'users_social_connect' => 'Hesabı Bağla',\n    'users_social_disconnect' => 'Hesabın Bağlantısını Kes',\n    'users_social_status_connected' => 'Connected',\n    'users_social_status_disconnected' => 'Disconnected',\n    'users_social_connected' => ':socialAccount hesabı, profilinize başarıyla bağlandı.',\n    'users_social_disconnected' => ':socialAccount hesabınızın profilinizle ilişiği başarıyla kesildi.',\n    'users_api_tokens' => 'API Anahtarları',\n    'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.',\n    'users_api_tokens_none' => 'Bu kullanıcı için oluşturulmuş herhangi bir API anahtarı bulunamadı',\n    'users_api_tokens_create' => 'Anahtar Oluştur',\n    'users_api_tokens_expires' => 'Bitiş süresi',\n    'users_api_tokens_docs' => 'API Dokümantasyonu',\n    'users_mfa' => 'Çok Aşamalı Kimlik Doğrulama',\n    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',\n    'users_mfa_x_methods' => ':count method configured|:count methods configured',\n    'users_mfa_configure' => 'Yöntemleri Yapılandır',\n\n    // API Tokens\n    'user_api_token_create' => 'API Anahtarı Oluştur',\n    'user_api_token_name' => 'İsim',\n    'user_api_token_name_desc' => 'Anahtarınıza gelecekte ne amaçla kullanıldığını hatırlatması açısından anlamlı bir isim veriniz.',\n    'user_api_token_expiry' => 'Bitiş Tarihi',\n    'user_api_token_expiry_desc' => 'Bu anahtarın süresinin dolduğu bir tarih belirleyin. Bu tarihten sonra, bu anahtar kullanılarak yapılan istekler artık çalışmaz. Bu alanı boş bırakmak, bitiş tarihini 100 yıl sonrası yapar.',\n    'user_api_token_create_secret_message' => 'Bu anahtar oluşturulduktan hemen sonra bir \"ID Anahtarı\" ve \"Gizli Anahtar\" üretilip görüntülenecektir. Gizli anahtar sadece bir defa gösterilecektir, bu yüzden devam etmeden önce bu değeri güvenli bir yere kopyaladığınızdan emin olun.',\n    'user_api_token' => 'API Erişim Anahtarı',\n    'user_api_token_id' => 'Anahtar ID',\n    'user_api_token_id_desc' => 'Bu, API isteklerini karşılamak için sistem tarafından oluşturulmuş ve sonradan düzenlenemeyen bir tanımlayıcıdır.',\n    'user_api_token_secret' => 'Gizli Anahtar',\n    'user_api_token_secret_desc' => 'Bu, API isteklerinde sağlanması gereken anahtar için sistem tarafından oluşturulan bir gizli anahtardır. Bu anahtar sadece bir defa görüntülenecektir, bu nedenle bu değeri güvenli bir yere kopyalayın.',\n    'user_api_token_created' => 'Anahtar :timeAgo Oluşturuldu',\n    'user_api_token_updated' => 'Anahtar :timeAgo Güncellendi',\n    'user_api_token_delete' => 'Anahtarı Sil',\n    'user_api_token_delete_warning' => 'Bu işlem \\':tokenName\\' adındaki API anahtarını sistemden tamamen silecektir.',\n    'user_api_token_delete_confirm' => 'Bu API anahtarını silmek istediğinize emin misiniz?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',\n    'webhooks_x_trigger_events' => ':count trigger event|:count trigger events',\n    'webhooks_create' => 'Create New Webhook',\n    'webhooks_none_created' => 'No webhooks have yet been created.',\n    'webhooks_edit' => 'Webhook\\'u Düzenle',\n    'webhooks_save' => 'Webhook\\'u Kaydet',\n    'webhooks_details' => 'Webhook Detayları',\n    'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',\n    'webhooks_events' => 'Webhook Olayları',\n    'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',\n    'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\\'t expose confidential content.',\n    'webhooks_events_all' => 'Tüm sistem olayları',\n    'webhooks_name' => 'Webhook Adı',\n    'webhooks_timeout' => 'Webhook İsteği Zaman Aşımı (Saniyeler)',\n    'webhooks_endpoint' => 'Webhook Endpoint',\n    'webhooks_active' => 'Webhook Aktif',\n    'webhook_events_table_header' => 'Etkinlikler',\n    'webhooks_delete' => 'Web Kancasını Sil',\n    'webhooks_delete_warning' => 'Bu, \\':webhookName\\' adıyla bu webhook, sistemden tamamen silecektir.',\n    'webhooks_delete_confirm' => 'Bu web kancası silmek istediğinize emin misiniz?',\n    'webhooks_format_example' => 'Webhook Biçimlendirme Örneği',\n    'webhooks_format_example_desc' => 'Webhook verileri, yapılandırılmış uç noktaya aşağıdaki formatı izleyen JSON olarak bir POST isteği olarak gönderilir. \"related_item\" ve \"url\" özellikleri isteğe bağlıdır ve tetiklenen olayın türüne bağlı olacaktır.',\n    'webhooks_status' => 'Webhook Durumu',\n    'webhooks_last_called' => 'Last Called:',\n    'webhooks_last_errored' => 'Last Errored:',\n    'webhooks_last_error_message' => 'Son Hata Mesajı:',\n\n    // Licensing\n    'licenses' => 'Licenses',\n    'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.',\n    'licenses_bookstack' => 'BookStack License',\n    'licenses_php' => 'PHP Library Licenses',\n    'licenses_js' => 'JavaScript Library Licenses',\n    'licenses_other' => 'Other Licenses',\n    'license_details' => 'License Details',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Danca',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'İbranice',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovence',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/tr/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute kabul edilmelidir.',\n    'active_url'           => ':attribute, geçerli bir URL adresi değildir.',\n    'after'                => ':attribute tarihi, :date tarihinden sonraki bir tarih olmalıdır.',\n    'alpha'                => ':attribute sadece harflerden oluşabilir.',\n    'alpha_dash'           => ':attribute sadece harf, rakam ve tirelerden oluşabilir.',\n    'alpha_num'            => ':attribute sadece harflerden ve rakamlardan oluşabilir.',\n    'array'                => ':attribute bir dizi olmalıdır.',\n    'backup_codes'         => 'Girilen kod geçersiz veya daha önce kullanılmış.',\n    'before'               => ':attribute tarihi, :date tarihinden önceki bir tarih olmalıdır.',\n    'between'              => [\n        'numeric' => ':attribute değeri, :min ve :max değerleri arasında olmalıdır.',\n        'file'    => ':attribute, :min ve :max kilobyte boyutları arasında olmalıdır.',\n        'string'  => ':attribute, :min ve :max karakter arasında olmalıdır.',\n        'array'   => ':attribute, :min ve :max öge arasında olmalıdır.',\n    ],\n    'boolean'              => ':attribute değeri true veya false olmalıdır.',\n    'confirmed'            => ':attribute doğrulaması eşleşmiyor.',\n    'date'                 => ':attribute geçerli bir tarih değil.',\n    'date_format'          => ':attribute formatı, :format formatına uymuyor.',\n    'different'            => ':attribute ve :other birbirinden farklı olmalıdır.',\n    'digits'               => ':attribute, :digits basamaklı olmalıdır.',\n    'digits_between'       => ':attribute, en az :min ve en fazla :max basamaklı olmalıdır.',\n    'email'                => ':attribute, geçerli bir e-posta adresi olmalıdır.',\n    'ends_with' => ':attribute, şunlardan birisiyle bitmelidir: :values',\n    'file'                 => 'Geçerli bir dosya olara :attribute sağlanmalıdır.',\n    'filled'               => ':attribute alanı zorunludur.',\n    'gt'                   => [\n        'numeric' => ':attribute, :max değerinden büyük olmalıdır.',\n        'file'    => ':attribute, :value kilobayttan büyük olmalıdır.',\n        'string'  => ':attribute, :value karakterden fazla olmalıdır.',\n        'array'   => ':attribute, :value ögeden daha fazla öge içermelidir.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute, :value değerinden büyük veya bu değere eşit olmalıdır.',\n        'file'    => ':attribute, en az :value kilobayt olmalıdır.',\n        'string'  => ':attribute, en az :value karakter içermelidir.',\n        'array'   => ':attribute, en az :value öge içermelidir.',\n    ],\n    'exists'               => 'Seçilen :attribute geçersiz.',\n    'image'                => ':attribute, bir görsel olmalıdır.',\n    'image_extension'      => ':attribute, geçerli ve desteklenen bir görsel uzantısına sahip olmalıdır.',\n    'in'                   => 'Seçilen :attribute geçersizdir.',\n    'integer'              => ':attribute, bir tam sayı olmalıdır.',\n    'ip'                   => ':attribute, geçerli bir IP adresi olmalıdır.',\n    'ipv4'                 => ':attribute, geçerli bir IPv4 adresi olmalıdır.',\n    'ipv6'                 => ':attribute, geçerli bir IPv6 adresi olmalıdır.',\n    'json'                 => ':attribute, geçerli bir JSON dizimi olmalıdır.',\n    'lt'                   => [\n        'numeric' => ':attribute, :value değerinden küçük olmalıdır.',\n        'file'    => ':attribute, :value kilobayttan küçük olmalıdır.',\n        'string'  => ':attribute, :value karakterden küçük olmalıdır.',\n        'array'   => ':attribute, :value ögeden az olmalıdır.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute, en fazla :value değerine eşit olmalıdır.',\n        'file'    => ':attribute, en fazla :value kilobayt olmalıdır.',\n        'string'  => ':attribute, en fazla :value karakter içermelidir.',\n        'array'   => ':attribute, en fazla :value öge içermelidir.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute, :max değerinden büyük olmayabilir.',\n        'file'    => ':attribute, :max kilobayttan büyük olmayabilir.',\n        'string'  => ':attribute, :max karakterden daha fazla karakter içermiyor olabilir.',\n        'array'   => ':attribute, :max ögeden daha fazla öge içermiyor olabilir.',\n    ],\n    'mimes'                => ':attribute, şu dosya tiplerinde olmalıdır: :values.',\n    'min'                  => [\n        'numeric' => ':attribute, :min değerinden az olmamalıdır.',\n        'file'    => ':attribute, :min kilobayttan küçük olmamalıdır.',\n        'string'  => ':attribute, en az :min karakter içermelidir.',\n        'array'   => ':attribute, en az :min öge içermelidir.',\n    ],\n    'not_in'               => 'Seçili :attribute geçersiz.',\n    'not_regex'            => ':attribute formatı geçersiz.',\n    'numeric'              => ':attribute, bir sayı olmalıdır.',\n    'regex'                => ':attribute formatı geçersiz.',\n    'required'             => ':attribute alanı zorunludur.',\n    'required_if'          => ':other alanının :value olması, :attribute alanını zorunlu kılar.',\n    'required_with'        => ':values değerinin mevcudiyeti, :attribute alanını zorunlu kılar.',\n    'required_with_all'    => ':values değerlerinin mevcudiyeti, :attribute alanını zorunlu kılar.',\n    'required_without'     => ':values değerinin bulunmuyor olması, :attribute alanını zorunlu kılar.',\n    'required_without_all' => ':values değerlerinden hiçbirinin bulunmuyor olması, :attribute alanını zorunlu kılar.',\n    'same'                 => ':attribute ve :other eşleşmelidir.',\n    'safe_url'             => 'Sağlanan bağlantı güvenli olmayabilir.',\n    'size'                 => [\n        'numeric' => ':attribute, :size boyutunda olmalıdır.',\n        'file'    => ':attribute, :size kilobayt olmalıdır.',\n        'string'  => ':attribute, :size karakter uzunluğunda olmalıdır.',\n        'array'   => ':attribute, :size sayıda öge içermelidir.',\n    ],\n    'string'               => ':attribute, string olmalıdır.',\n    'timezone'             => ':attribute, geçerli bir bölge olmalıdır.',\n    'totp'                 => 'Girilen kod geçersiz veya süresi dolmuş.',\n    'unique'               => ':attribute zaten alınmış.',\n    'url'                  => ':attribute formatı geçersiz.',\n    'uploaded'             => 'Dosya yüklemesi başarısız oldu. Sunucu, bu boyuttaki dosyaları kabul etmiyor olabilir.',\n\n    'zip_file' => 'The :attribute needs to reference a file within the ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',\n    'zip_model_expected' => 'Data object expected but \":type\" found.',\n    'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Şifre onayı zorunludur',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/uk/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'створено сторінку',\n    'page_create_notification'    => 'Сторінка успішно створена',\n    'page_update'                 => 'оновив сторінку',\n    'page_update_notification'    => 'Сторінка успішно оновлена',\n    'page_delete'                 => 'видалив сторінку',\n    'page_delete_notification'    => 'Сторінка успішно видалена',\n    'page_restore'                => 'відновив сторінку',\n    'page_restore_notification'   => 'Сторінка успішно відновлена',\n    'page_move'                   => 'перемістив сторінку',\n    'page_move_notification'      => 'Сторінку успішно перенесено',\n\n    // Chapters\n    'chapter_create'              => 'створив розділ',\n    'chapter_create_notification' => 'Розділ успішно створено',\n    'chapter_update'              => 'оновив розділ',\n    'chapter_update_notification' => 'Розділ успішно оновлено',\n    'chapter_delete'              => 'видалив розділ',\n    'chapter_delete_notification' => 'Розділ успішно видалено',\n    'chapter_move'                => 'перемістив розділ',\n    'chapter_move_notification' => 'Розділ успішно перенесений',\n\n    // Books\n    'book_create'                 => 'створив книгу',\n    'book_create_notification'    => 'Книгу успішно створено',\n    'book_create_from_chapter'              => 'перетворений розділ в книгу',\n    'book_create_from_chapter_notification' => 'Розділ успішно перетворений в книгу',\n    'book_update'                 => 'оновив книгу',\n    'book_update_notification'    => 'Книгу успішно оновлено',\n    'book_delete'                 => 'видалив книгу',\n    'book_delete_notification'    => 'Книгу успішно видалено',\n    'book_sort'                   => 'sorted книгу',\n    'book_sort_notification'      => 'Книгу успішно відновлено',\n\n    // Bookshelves\n    'bookshelf_create'            => 'створено полицю',\n    'bookshelf_create_notification'    => 'Полиця успішно створена',\n    'bookshelf_create_from_book'    => 'конвертовано книгу у полицю',\n    'bookshelf_create_from_book_notification'    => 'Книжку успішно конвертовано на полицю',\n    'bookshelf_update'                 => 'оновлено полицю',\n    'bookshelf_update_notification'    => 'Полиця успішно оновлена',\n    'bookshelf_delete'                 => 'видалена полиця',\n    'bookshelf_delete_notification'    => 'Полиця успішно видалена',\n\n    // Revisions\n    'revision_restore' => 'відновлено версію',\n    'revision_delete' => 'видалена версія',\n    'revision_delete_notification' => 'Версію успішно видалено',\n\n    // Favourites\n    'favourite_add_notification' => '\":ім\\'я\" було додане до ваших улюлених',\n    'favourite_remove_notification' => '\":ім\\'я\" було видалено з ваших улюблених',\n\n    // Watching\n    'watch_update_level_notification' => 'Налаштування перегляду успішно оновлено',\n\n    // Auth\n    'auth_login' => 'ввійшли',\n    'auth_register' => 'зареєстрований як новий користувач',\n    'auth_password_reset_request' => 'запит на скидання пароля користувача',\n    'auth_password_reset_update' => 'скинути пароль користувача',\n    'mfa_setup_method' => 'налаштований метод MFA',\n    'mfa_setup_method_notification' => 'Багатофакторний метод успішно налаштований',\n    'mfa_remove_method' => 'видалив метод MFA',\n    'mfa_remove_method_notification' => 'Багатофакторний метод успішно видалений',\n\n    // Settings\n    'settings_update' => 'оновлені налаштування',\n    'settings_update_notification' => 'Налаштування успішно оновлено',\n    'maintenance_action_run' => 'виконуються дії щодо обслуговування',\n\n    // Webhooks\n    'webhook_create' => 'створений вебхук',\n    'webhook_create_notification' => 'Веб хук успішно створений',\n    'webhook_update' => 'оновлений вебхук',\n    'webhook_update_notification' => 'Вебхуки успішно оновлено',\n    'webhook_delete' => 'видалений вебхук',\n    'webhook_delete_notification' => 'Вебхуки успішно видалено',\n\n    // Imports\n    'import_create' => 'створений імпорт',\n    'import_create_notification' => 'Імпорт успішно завантажений',\n    'import_run' => 'оновлений імпорт',\n    'import_run_notification' => 'Контент успішно імпортовано',\n    'import_delete' => 'видалений імпорт',\n    'import_delete_notification' => 'Імпорт успішно видалено',\n\n    // Users\n    'user_create' => 'створений користувач',\n    'user_create_notification' => 'Користувач успішно створений',\n    'user_update' => 'оновлений користувач',\n    'user_update_notification' => 'Користувача було успішно оновлено',\n    'user_delete' => 'вилучений користувач',\n    'user_delete_notification' => 'Користувача успішно видалено',\n\n    // API Tokens\n    'api_token_create' => 'створений APi токен',\n    'api_token_create_notification' => 'API токен успішно створений',\n    'api_token_update' => 'оновлено API токен',\n    'api_token_update_notification' => 'Токен API успішно оновлено',\n    'api_token_delete' => 'видалено API токен',\n    'api_token_delete_notification' => 'API-токен успішно видалено',\n\n    // Roles\n    'role_create' => 'створену роль',\n    'role_create_notification' => 'Роль успішно створена',\n    'role_update' => 'оновлена роль',\n    'role_update_notification' => 'Роль успішно оновлена',\n    'role_delete' => 'видалена роль',\n    'role_delete_notification' => 'Роль успішно видалена',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'очищено кошик',\n    'recycle_bin_restore' => 'відновлено із кошику',\n    'recycle_bin_destroy' => 'видалено з кошика',\n\n    // Comments\n    'commented_on'                => 'прокоментував',\n    'comment_create'              => 'додано коментар',\n    'comment_update'              => 'оновлено коментар',\n    'comment_delete'              => 'видалений коментар',\n\n    // Sort Rules\n    'sort_rule_create' => 'створене правило сортування',\n    'sort_rule_create_notification' => 'Правило сортування успішно створено',\n    'sort_rule_update' => 'оновлено правило сортування',\n    'sort_rule_update_notification' => 'Правило сортування успішно оновлено',\n    'sort_rule_delete' => 'видалено правило сортування',\n    'sort_rule_delete_notification' => 'Правило сортування успішно видалено',\n\n    // Other\n    'permissions_update'          => 'оновив дозволи',\n];\n"
  },
  {
    "path": "lang/uk/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Цей обліковий запис не знайдено.',\n    'throttle' => 'Забагато спроб входу в систему. Будь ласка, спробуйте ще раз через :seconds секунд.',\n\n    // Login & Register\n    'sign_up' => 'Реєстрація',\n    'log_in' => 'Увійти',\n    'log_in_with' => 'Увійти через :socialDriver',\n    'sign_up_with' => 'Зареєструватися через :socialDriver',\n    'logout' => 'Вихід',\n\n    'name' => 'Ім\\'я',\n    'username' => 'Логін',\n    'email' => 'Адреса електронної пошти',\n    'password' => 'Пароль',\n    'password_confirm' => 'Підтвердження пароля',\n    'password_hint' => 'Повинен бути щонайменше 8 символів',\n    'forgot_password' => 'Забули пароль?',\n    'remember_me' => 'Запам\\'ятати мене',\n    'ldap_email_hint' => 'Введіть email для цього облікового запису.',\n    'create_account' => 'Створити обліковий запис',\n    'already_have_account' => 'Вже є обліковий запис?',\n    'dont_have_account' => 'Немає облікового запису?',\n    'social_login' => 'Вхід через соціальну мережу',\n    'social_registration' => 'Реєстрація через соціальну мережу',\n    'social_registration_text' => 'Реєстрація і вхід через інший сервіс.',\n\n    'register_thanks' => 'Дякуємо за реєстрацію!',\n    'register_confirm' => 'Будь ласка, перевірте свою електронну пошту та натисніть кнопку підтвердження, щоб отримати доступ до :appName.',\n    'registrations_disabled' => 'Реєстрацію вимкнено',\n    'registration_email_domain_invalid' => 'Цей домен електронної пошти заборонений для реєстрації',\n    'register_success' => 'Дякуємо за реєстрацію! Ви зареєстровані та ввійшли в систему.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Спроба входу в систему',\n    'auto_init_starting_desc' => 'Ми пишемо вашій системі аутентифікації, щоб запустити процес авторизації. Якщо після 5 секунд Ви можете спробувати натиснути на посилання нижче.',\n    'auto_init_start_link' => 'Продовжити з автентифікацією',\n\n    // Password Reset\n    'reset_password' => 'Скинути пароль',\n    'reset_password_send_instructions' => 'Введіть адресу електронної пошти нижче, і вам буде надіслано електронне повідомлення з посиланням на зміну пароля.',\n    'reset_password_send_button' => 'Надіслати посилання для скидання пароля',\n    'reset_password_sent' => 'Посилання для скидання пароля буде надіслано на :email, якщо ця електронна адреса вказана в системі.',\n    'reset_password_success' => 'Ваш пароль успішно скинуто.',\n    'email_reset_subject' => 'Скинути ваш пароль :appName',\n    'email_reset_text' => 'Ви отримали цей електронний лист, оскільки до нас надійшов запит на скидання пароля для вашого облікового запису.',\n    'email_reset_not_requested' => 'Якщо ви не надсилали запит на скидання пароля, подальші дії не потрібні.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Підтвердьте свою електронну пошту на :appName',\n    'email_confirm_greeting' => 'Дякуємо, що приєдналися до :appName!',\n    'email_confirm_text' => 'Будь ласка, підтвердьте свою адресу електронної пошти, натиснувши кнопку нижче:',\n    'email_confirm_action' => 'Підтвердити Email',\n    'email_confirm_send_error' => 'Необхідно підтвердження електронною поштою, але система не змогла надіслати електронний лист. Зверніться до адміністратора, щоб правильно налаштувати електронну пошту.',\n    'email_confirm_success' => 'Ваша адреса електронної пошти була підтверджена! Тепер ви можете увійти в систему, використовуючи цю адресу електронної пошти.',\n    'email_confirm_resent' => 'Лист з підтвердженням надіслано, перевірте свою пошту.',\n    'email_confirm_thanks' => 'Дякуємо за підтвердження!',\n    'email_confirm_thanks_desc' => 'Будь ласка, зачекайте деякий час, поки підтвердження буде оброблено. Якщо ви не перенаправлені через 3 секунди, натисніть посилання \"Продовжити\" нижче, щоб продовжити.',\n\n    'email_not_confirmed' => 'Адресу електронної скриньки не підтверджено',\n    'email_not_confirmed_text' => 'Ваша електронна адреса ще не підтверджена.',\n    'email_not_confirmed_click_link' => 'Будь-ласка, натисніть на посилання в електронному листі, яке було надіслано після реєстрації.',\n    'email_not_confirmed_resend' => 'Якщо ви не можете знайти електронний лист, ви можете повторно надіслати підтвердження електронною поштою, на формі нижче.',\n    'email_not_confirmed_resend_button' => 'Повторне підтвердження електронної пошти',\n\n    // User Invite\n    'user_invite_email_subject' => 'Вас запросили приєднатися до :appName!',\n    'user_invite_email_greeting' => 'Для вас створено обліковий запис на :appName.',\n    'user_invite_email_text' => 'Натисніть кнопку нижче, щоб встановити пароль облікового запису та отримати доступ:',\n    'user_invite_email_action' => 'Встановити пароль облікового запису',\n    'user_invite_page_welcome' => 'Ласкаво просимо до :appName!',\n    'user_invite_page_text' => 'Для завершення процесу створення облікового запису та отримання доступу вам потрібно задати пароль, який буде використовуватися для входу в :appName в майбутньому.',\n    'user_invite_page_confirm_button' => 'Підтвердити пароль',\n    'user_invite_success_login' => 'Пароль встановлено, ви повинні увійти в систему, використовуючи свій встановлений пароль для доступу :appNam!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Налаштувати двофакторну автентифікацію',\n    'mfa_setup_desc' => 'Двофакторна аутентифікація додає ще один рівень безпеки для вашого облікового запису.',\n    'mfa_setup_configured' => 'Вже налаштовано',\n    'mfa_setup_reconfigure' => 'Переналаштувати',\n    'mfa_setup_remove_confirmation' => 'Ви впевнені, що хочете видалити цей метод багатофакторної автентифікації?',\n    'mfa_setup_action' => 'Встановлення',\n    'mfa_backup_codes_usage_limit_warning' => 'Залишилося менше 5 резервних кодів, Будь ласка, створіть та збережіть новий набір до того, як у вас не вистачає кодів, щоб запобігти блокуванню вашої обліковки.',\n    'mfa_option_totp_title' => 'Мобільний додаток',\n    'mfa_option_totp_desc' => 'Для використання багатофакторної автентифікації вам потрібен мобільний додаток, який підтримує TOTP такі як Google Authenticator, Authy або Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Резервні коди',\n    'mfa_option_backup_codes_desc' => 'Генерує набір резервних кодів для використання одноразового використання, які ви вводите в систему, щоб перевірити свою особу. Переконайтеся, що зберігайте їх в безпечному та безпечному місці.',\n    'mfa_gen_confirm_and_enable' => 'Підтвердити та увімкнути',\n    'mfa_gen_backup_codes_title' => 'Налаштування резервних кодів',\n    'mfa_gen_backup_codes_desc' => 'Зберігайте список кодів в безпечному місці. Для доступу до системи ви зможете використовувати один з кодів як другий механізм аутентифікації.',\n    'mfa_gen_backup_codes_download' => 'Завантажити коди',\n    'mfa_gen_backup_codes_usage_warning' => 'Кожний код можна використати лише один раз',\n    'mfa_gen_totp_title' => 'Налаштування мобільного додатка',\n    'mfa_gen_totp_desc' => 'Для використання багатофакторної автентифікації вам потрібен мобільний додаток, який підтримує TOTP такі як Google Authenticator, Authy або Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Проскануйте QR-код внизу за допомогою бажаного додатку для аутентифікації, щоб розпочати.',\n    'mfa_gen_totp_verify_setup' => 'Перевірка налаштувань',\n    'mfa_gen_totp_verify_setup_desc' => 'Переконайтеся, що все працює, ввівши код, згенерований у вашому додатку аутентифікації, в полі вводу нижче:',\n    'mfa_gen_totp_provide_code_here' => 'Вкажіть код вашої програми тут',\n    'mfa_verify_access' => 'Підтвердити доступ',\n    'mfa_verify_access_desc' => 'Ваш обліковий запис користувача вимагає підтвердження за допомогою додаткового рівня перевірки, перш ніж отримати доступ. Перевірте, використовуючи один з ваших налаштованих способів для продовження.',\n    'mfa_verify_no_methods' => 'Немає налаштованих методів',\n    'mfa_verify_no_methods_desc' => 'Для вашого облікового запису не знайдено жодних методів багатофакторної аутентифікації. Вам потрібно буде налаштувати хоча б один спосіб перш ніж отримати доступ.',\n    'mfa_verify_use_totp' => 'Перевірити за допомогою мобільного додатку',\n    'mfa_verify_use_backup_codes' => 'Перевірка використовуючи резервний код',\n    'mfa_verify_backup_code' => 'Резервний код',\n    'mfa_verify_backup_code_desc' => 'Введіть один з резервних кодів нижче:',\n    'mfa_verify_backup_code_enter_here' => 'Введіть резервний код тут',\n    'mfa_verify_totp_desc' => 'Введіть код, згенерований за допомогою мобільного додатку:',\n    'mfa_setup_login_notification' => 'Налаштовано багатофакторний метод аутентифікації. Будь ласка, зараз увійдіть в систему знову, використовуючи налаштований метод.',\n];\n"
  },
  {
    "path": "lang/uk/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Скасувати',\n    'close' => 'Закрити',\n    'confirm' => 'Застосувати',\n    'back' => 'Назад',\n    'save' => 'Зберегти',\n    'continue' => 'Продовжити',\n    'select' => 'Вибрати',\n    'toggle_all' => 'Увімкнути все',\n    'more' => 'Ще',\n\n    // Form Labels\n    'name' => 'Назва',\n    'description' => 'Опис',\n    'role' => 'Роль',\n    'cover_image' => 'Обкладинка',\n    'cover_image_description' => 'Це зображення має бути приблизно 440х250пікс, щоб його було легко масштабувати та обводити відповідно до інтерфейсу користувача у різних сценаріях, як це необхідно, тому реальні розміри для відображення відрізняються.',\n\n    // Actions\n    'actions' => 'Дії',\n    'view' => 'Подивитись',\n    'view_all' => 'Подивитись все',\n    'new' => 'Новий',\n    'create' => 'Створити',\n    'update' => 'Оновити',\n    'edit' => 'Редагувати',\n    'archive' => 'Архів',\n    'unarchive' => 'Розархівувати',\n    'sort' => 'Сортувати',\n    'move' => 'Перемістити',\n    'copy' => 'Копіювати',\n    'reply' => 'Відповісти',\n    'delete' => 'Видалити',\n    'delete_confirm' => 'Підтвердити видалення',\n    'search' => 'Шукати',\n    'search_clear' => 'Очистити пошук',\n    'reset' => 'Скинути',\n    'remove' => 'Видалити',\n    'add' => 'Додати',\n    'configure' => 'Налаштувати',\n    'manage' => 'Управління',\n    'fullscreen' => 'На весь екран',\n    'favourite' => 'Улюблене',\n    'unfavourite' => 'Прибрати з обраного',\n    'next' => 'Уперед',\n    'previous' => 'Назад',\n    'filter_active' => 'Активний фільтр:',\n    'filter_clear' => 'Очистити фільтр',\n    'download' => 'Завантажити',\n    'open_in_tab' => 'Відкрити в новій вкладці',\n    'open' => 'Відкрити',\n\n    // Sort Options\n    'sort_options' => 'Параметри сортування',\n    'sort_direction_toggle' => 'Перемикач напрямку сортування',\n    'sort_ascending' => 'За зростанням',\n    'sort_descending' => 'За спаданням',\n    'sort_name' => 'Ім\\'я',\n    'sort_default' => 'За замовчуванням',\n    'sort_created_at' => 'Дата створення',\n    'sort_updated_at' => 'Дата оновлення',\n\n    // Misc\n    'deleted_user' => 'Видалений користувач',\n    'no_activity' => 'Немає активності для показу',\n    'no_items' => 'Немає доступних елементів',\n    'back_to_top' => 'Повернутися до початку',\n    'skip_to_main_content' => 'Перейти до змісту',\n    'toggle_details' => 'Подробиці',\n    'toggle_thumbnails' => 'Мініатюри',\n    'details' => 'Деталі',\n    'grid_view' => 'Вигляд Сіткою',\n    'list_view' => 'Вигляд Списком',\n    'default' => 'За замовчуванням',\n    'breadcrumb' => 'Навігація',\n    'status' => 'Стан',\n    'status_active' => 'Активний',\n    'status_inactive' => 'Неактивний',\n    'never' => 'Ніколи',\n    'none' => 'Відсутньо',\n\n    // Header\n    'homepage' => 'Домашня Сторінка',\n    'header_menu_expand' => 'Розгорнути меню заголовка',\n    'profile_menu' => 'Меню профілю',\n    'view_profile' => 'Переглянути профіль',\n    'edit_profile' => 'Редагувати профіль',\n    'dark_mode' => 'Темний режим',\n    'light_mode' => 'Світлий режим',\n    'global_search' => 'Глобальний Пошук',\n\n    // Layout tabs\n    'tab_info' => 'Інфо',\n    'tab_info_label' => 'Вкладка: показувати додаткову інформацію',\n    'tab_content' => 'Вміст',\n    'tab_content_label' => 'Вкладка: Показати основний вміст',\n\n    // Email Content\n    'email_action_help' => 'Якщо у вас виникають проблеми при натисканні кнопки \":actionText\", скопіюйте та вставте URL у свій веб-браузер:',\n    'email_rights' => 'Всі права захищені',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Політика приватності',\n    'terms_of_service' => 'Умови використання',\n\n    // OpenSearch\n    'opensearch_description' => 'Шукати :appName',\n];\n"
  },
  {
    "path": "lang/uk/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Вибрати зображення',\n    'image_list' => 'Список зображень',\n    'image_details' => 'Деталі зображення',\n    'image_upload' => 'Завантажити зображення',\n    'image_intro' => 'Тут ви можете вибрати і керувати зображеннями, які раніше були завантажені в систему.',\n    'image_intro_upload' => 'Завантажте нове зображення, перетягуючи файл зображення до цього вікна або скориставшись кнопкою \"Завантажити зображення\" вище.',\n    'image_all' => 'Всі',\n    'image_all_title' => 'Переглянути всі зображення',\n    'image_book_title' => 'Переглянути зображення, завантажені в цю книгу',\n    'image_page_title' => 'Переглянути зображення, завантажені на цю сторінку',\n    'image_search_hint' => 'Пошук по імені зображення',\n    'image_uploaded' => 'Завантажено :uploadedDate',\n    'image_uploaded_by' => 'Завантажено :userName',\n    'image_uploaded_to' => 'Завантажено на :pageLink',\n    'image_updated' => 'Оновлено :updateDate',\n    'image_load_more' => 'Завантажити ще',\n    'image_image_name' => 'Назва зображення',\n    'image_delete_used' => 'Це зображення використовується на наступних сторінках.',\n    'image_delete_confirm_text' => 'Ви дійсно хочете видалити це зображення?',\n    'image_select_image' => 'Вибрати зображення',\n    'image_dropzone' => 'Перетягніть зображення, або натисніть тут для завантаження',\n    'image_dropzone_drop' => 'Перетягніть зображення сюди для завантаження',\n    'images_deleted' => 'Зображень видалено',\n    'image_preview' => 'Попередній перегляд зображення',\n    'image_upload_success' => 'Зображення завантажено успішно',\n    'image_update_success' => 'Деталі зображення успішно оновлені',\n    'image_delete_success' => 'Зображення успішно видалено',\n    'image_replace' => 'Замінити зображення',\n    'image_replace_success' => 'Файл зображення успішно оновлено',\n    'image_rebuild_thumbs' => 'Оновити розмір',\n    'image_rebuild_thumbs_success' => 'Варіації розміру зображень успішно перебудовані!',\n\n    // Code Editor\n    'code_editor' => 'Редагувати код',\n    'code_language' => 'Мова коду',\n    'code_content' => 'Вміст коду',\n    'code_session_history' => 'Історія сесії',\n    'code_save' => 'Зберегти Код',\n];\n"
  },
  {
    "path": "lang/uk/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Основне',\n    'advanced' => 'Розширений',\n    'none' => 'Відсутньо',\n    'cancel' => 'Скасувати',\n    'save' => 'Зберегти',\n    'close' => 'Закрити',\n    'apply' => 'Застосувати',\n    'undo' => 'Відмінити',\n    'redo' => 'Відновити',\n    'left' => 'Ліворуч',\n    'center' => 'По центру',\n    'right' => 'Праворуч',\n    'top' => 'Вгору',\n    'middle' => 'Посередині',\n    'bottom' => 'Внизу',\n    'width' => 'Ширина',\n    'height' => 'Висота',\n    'More' => 'Більше',\n    'select' => 'Вибрати…',\n\n    // Toolbar\n    'formats' => 'Формати',\n    'header_large' => 'Великий заголовок',\n    'header_medium' => 'Середній заголовок',\n    'header_small' => 'Маленький заголовок',\n    'header_tiny' => 'Крихітний заголовок',\n    'paragraph' => 'Параграф',\n    'blockquote' => 'Цитата',\n    'inline_code' => 'Вставка коду',\n    'callouts' => 'Виноски',\n    'callout_information' => 'Інформація',\n    'callout_success' => 'Успішно',\n    'callout_warning' => 'Попередження',\n    'callout_danger' => 'Небезпека',\n    'bold' => 'Жирний',\n    'italic' => 'Курсив',\n    'underline' => 'Підкреслений',\n    'strikethrough' => 'Закреслений',\n    'superscript' => 'Верхній індекс',\n    'subscript' => 'Нижній індекс',\n    'text_color' => 'Колір тексту',\n    'highlight_color' => 'Колір підсвічування',\n    'custom_color' => 'Власний колір',\n    'remove_color' => 'Видалити колір',\n    'background_color' => 'Колір фону',\n    'align_left' => 'Вирівняти по лівому краю',\n    'align_center' => 'Вирівняти по центру',\n    'align_right' => 'Вирівнювання по правому краю',\n    'align_justify' => 'За шириною',\n    'list_bullet' => 'Маркований список',\n    'list_numbered' => 'Нумерований список',\n    'list_task' => 'Список завдань',\n    'indent_increase' => 'Збільшити відступ',\n    'indent_decrease' => 'Зменшити відступ',\n    'table' => 'Таблиця',\n    'insert_image' => 'Вставити зображення',\n    'insert_image_title' => 'Вставити/Редагувати зображення',\n    'insert_link' => 'Вставити/редагувати посилання',\n    'insert_link_title' => 'Вставити/редагувати посилання',\n    'insert_horizontal_line' => 'Вставити горизонтальну лінію',\n    'insert_code_block' => 'Вставити блок коду',\n    'edit_code_block' => 'Редагування код блоку',\n    'insert_drawing' => 'Вставити/редагувати малюнок',\n    'drawing_manager' => 'Диспетчер малювання',\n    'insert_media' => 'Вставити/редагувати медіа',\n    'insert_media_title' => 'Вставити/редагувати медіа',\n    'clear_formatting' => 'Очистити форматування',\n    'source_code' => 'Вихідний код',\n    'source_code_title' => 'Вихідний код',\n    'fullscreen' => 'На весь екран',\n    'image_options' => 'Параметри зображень',\n\n    // Tables\n    'table_properties' => 'Властивості таблиці',\n    'table_properties_title' => 'Властивості таблиці',\n    'delete_table' => 'Видалити таблицю',\n    'table_clear_formatting' => 'Очистити форматування таблиці',\n    'resize_to_contents' => 'Змінити розмір до вмісту',\n    'row_header' => 'Заголовок рядка',\n    'insert_row_before' => 'Вставити рядок перед',\n    'insert_row_after' => 'Вставити рядок після',\n    'delete_row' => 'Видалити рядок',\n    'insert_column_before' => 'Вставити стовпець перед',\n    'insert_column_after' => 'Вставити стовпець після',\n    'delete_column' => 'Видалити стовпець',\n    'table_cell' => 'Комірка',\n    'table_row' => 'Рядок',\n    'table_column' => 'Стовпчик',\n    'cell_properties' => 'Властивості клітинки',\n    'cell_properties_title' => 'Властивості клітинки',\n    'cell_type' => 'Тип клітинки',\n    'cell_type_cell' => 'Комірка',\n    'cell_scope' => 'Область використання',\n    'cell_type_header' => 'Комірка заголовка',\n    'merge_cells' => 'Об\\'єднати комірки',\n    'split_cell' => 'Роз\\'єднати комірку',\n    'table_row_group' => 'Група рядків',\n    'table_column_group' => 'Група стовпців',\n    'horizontal_align' => 'Горизонтальне вирівнювання',\n    'vertical_align' => 'Вертикальне вирівнювання',\n    'border_width' => 'Ширина бордюру',\n    'border_style' => 'Стиль рамки',\n    'border_color' => 'Колір рамки',\n    'row_properties' => 'Властивості рядка',\n    'row_properties_title' => 'Властивості рядка',\n    'cut_row' => 'Вирізати рядок',\n    'copy_row' => 'Копіювати рядок',\n    'paste_row_before' => 'Вставити рядок перед',\n    'paste_row_after' => 'Вставити рядок після',\n    'row_type' => 'Тип рядка',\n    'row_type_header' => 'Заголовок',\n    'row_type_body' => 'Тіло',\n    'row_type_footer' => 'Низ',\n    'alignment' => 'Вирівнювання',\n    'cut_column' => 'Вирізати стовпчик',\n    'copy_column' => 'Копіювати стовпчик',\n    'paste_column_before' => 'Вставити стовпець перед',\n    'paste_column_after' => 'Вставити стовпець після',\n    'cell_padding' => 'Всередині клітинок',\n    'cell_spacing' => 'Інтервал між комірками',\n    'caption' => 'Підпис',\n    'show_caption' => 'Показати заголовок',\n    'constrain' => 'Обмеження пропорції',\n    'cell_border_solid' => 'Суцільний',\n    'cell_border_dotted' => 'В крапку',\n    'cell_border_dashed' => 'Пунктир',\n    'cell_border_double' => 'Подвійна',\n    'cell_border_groove' => 'Паз',\n    'cell_border_ridge' => 'Рідж',\n    'cell_border_inset' => 'Врізка',\n    'cell_border_outset' => 'Початок',\n    'cell_border_none' => 'Відсутньо',\n    'cell_border_hidden' => 'Прихований',\n\n    // Images, links, details/summary & embed\n    'source' => 'Вихідний код',\n    'alt_desc' => 'Альтернативний опис',\n    'embed' => 'Вбудувати',\n    'paste_embed' => 'Вставте ваш код вставки нижче:',\n    'url' => 'Адреса URL',\n    'text_to_display' => 'Текст для показу',\n    'title' => 'Назва',\n    'browse_links' => 'Переглянути посилання',\n    'open_link' => 'Відкрити посилання',\n    'open_link_in' => 'Відкрити посилання в...',\n    'open_link_current' => 'Поточне вікно',\n    'open_link_new' => 'Нове вікно',\n    'remove_link' => 'Видалити посилання',\n    'insert_collapsible' => 'Вставити згорнутий блок',\n    'collapsible_unwrap' => 'Розгорнути',\n    'edit_label' => 'Редагувати мітку',\n    'toggle_open_closed' => 'Перемкнути відкрити/закритий',\n    'collapsible_edit' => 'Редагування згорнутого блоку',\n    'toggle_label' => 'Перемкнути ярлики',\n\n    // About view\n    'about' => 'Про редактор',\n    'about_title' => 'Про WYSIWYG редактор',\n    'editor_license' => 'Ліцензія редактора і авторські права',\n    'editor_lexical_license' => 'Цей редактор побудований як форк :lexicalLink який поширюється під ліцензією MIT.',\n    'editor_lexical_license_link' => 'Тут ви можете знайти повну інформацію про ліцензію.',\n    'editor_tiny_license' => 'Цей редактор побудований за допомогою :tinylink, яке надається за ліцензією MIT.',\n    'editor_tiny_license_link' => 'Тут можна знайти авторські та умови ліцензії.',\n    'save_continue' => 'Зберегти і продовжити',\n    'callouts_cycle' => '(Тримайте натискання для перемикання між типами)',\n    'link_selector' => 'Перейти до вмісту',\n    'shortcuts' => 'Ярлики',\n    'shortcut' => 'Ярлик',\n    'shortcuts_intro' => 'Наступні ярлики доступні в редакторі:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Опис',\n];\n"
  },
  {
    "path": "lang/uk/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Недавно створено',\n    'recently_created_pages' => 'Нещодавно створені сторінки',\n    'recently_updated_pages' => 'Нещодавно оновлені сторінки',\n    'recently_created_chapters' => 'Нещодавно створені розділи',\n    'recently_created_books' => 'Нещодавно створені книги',\n    'recently_created_shelves' => 'Нещодавно створені полиці',\n    'recently_update' => 'Недавно оновлено',\n    'recently_viewed' => 'Недавно переглянуто',\n    'recent_activity' => 'Остання активність',\n    'create_now' => 'Створити зараз',\n    'revisions' => 'Версія',\n    'meta_revision' => 'Версія #:revisionCount',\n    'meta_created' => 'Створено :timeLength',\n    'meta_created_name' => ':user створив :timeLength',\n    'meta_updated' => 'Оновлено :timeLength',\n    'meta_updated_name' => ':user оновив :timeLength',\n    'meta_owned_name' => 'Власник :user',\n    'meta_reference_count' => 'Посилання на :count елемент|Посилання – :count елементів',\n    'entity_select' => 'Вибір об\\'єкта',\n    'entity_select_lack_permission' => 'У вас немає необхідних прав для вибору цього елемента',\n    'images' => 'Зображення',\n    'my_recent_drafts' => 'Мої останні чернетки',\n    'my_recently_viewed' => 'Мої недавні перегляди',\n    'my_most_viewed_favourites' => 'Мої найпопулярніші улюблені',\n    'my_favourites' => 'Моє обране',\n    'no_pages_viewed' => 'Ви не переглядали жодної сторінки',\n    'no_pages_recently_created' => 'Не було створено жодної сторінки',\n    'no_pages_recently_updated' => 'Немає недавно оновлених сторінок',\n    'export' => 'Експорт',\n    'export_html' => 'Вбудований веб-файл',\n    'export_pdf' => 'PDF файл',\n    'export_text' => 'Текстовий файл',\n    'export_md' => 'Файл розмітки',\n    'export_zip' => 'Портативний ZIP',\n    'default_template' => 'Типовий шаблон сторінки',\n    'default_template_explain' => 'Призначити шаблон сторінки, який буде використовуватися як типовий вміст для всіх сторінок, створених у цьому елементі. Майте на увазі, що ця сторінка буде використана лише у випадку, якщо вона має доступ до обраної сторінки шаблону.',\n    'default_template_select' => 'Виберіть сторінку шаблону',\n    'import' => 'Імпорт',\n    'import_validate' => 'Перевірка імпорту',\n    'import_desc' => 'Імпортувати книги, розділи і сторінки, використовуючи портативний zip-експорт з одного або іншого екземпляру. Виберіть ZIP-файл для продовження. Після завантаження файлу і підтвердження, ви зможете налаштувати та підтвердити імпорт в наступному вікні.',\n    'import_zip_select' => 'Виберіть ZIP-файл для завантаження',\n    'import_zip_validation_errors' => 'Під час перевірки вказаного ZIP-файлу були виявлені помилки:',\n    'import_pending' => 'Іпорт на розгляді',\n    'import_pending_none' => 'Імпорти не були розпочаті.',\n    'import_continue' => 'Продовжити імпорт',\n    'import_continue_desc' => 'Переглянути вміст через імпортування з завантаженого ZIP-файлу. Якщо готово, запустіть імпорт, щоб додати його вміст в цю систему. Завантажений ZIP файл імпорту буде автоматично вилучено при успішному імпорті.',\n    'import_details' => 'Імпортувати деталі',\n    'import_run' => 'Запустити імпорт',\n    'import_size' => ':size Розмір Імпорту ZIP',\n    'import_uploaded_at' => 'Завантажено :relativeTime',\n    'import_uploaded_by' => 'Завантажено',\n    'import_location' => 'Імпортувати місцезнаходження',\n    'import_location_desc' => 'Вибір цільового розташування для імпортованого вмісту. Вам потрібні відповідні дозволи для створення локації, яке ви обрали.',\n    'import_delete_confirm' => 'Ви впевнені, що бажаєте видалити цей імпорт?',\n    'import_delete_desc' => 'Це видалить завантажений імпорт файлу ZIP, і його не можна буде скасувати.',\n    'import_errors' => 'Помилки імпорту',\n    'import_errors_desc' => 'Під час спроби імпорту відбулися наступні помилки:',\n    'breadcrumb_siblings_for_page' => 'Переглянути інші сторінки',\n    'breadcrumb_siblings_for_chapter' => 'Переглянути інші розділи',\n    'breadcrumb_siblings_for_book' => 'Переглянути інші книги',\n    'breadcrumb_siblings_for_bookshelf' => 'Переглянути інші полиці',\n\n    // Permissions and restrictions\n    'permissions' => 'Дозволи',\n    'permissions_desc' => 'Встановіть тут дозволи, щоб перевизначити права за замовчуванням, які надаються ролями користувачів.',\n    'permissions_book_cascade' => 'Дозволи, встановлені на книги будуть автоматично каскадом до дитячих глав та сторінок, якщо вони не матимуть свої дозволи.',\n    'permissions_chapter_cascade' => 'Дозволи, встановлені для глав будуть автоматично каскадом на дочірні сторінки, якщо вони не матимуть своїх прав.',\n    'permissions_save' => 'Зберегти дозволи',\n    'permissions_owner' => 'Власник',\n    'permissions_role_everyone_else' => 'Всі інші',\n    'permissions_role_everyone_else_desc' => 'Встановити дозвіл для всіх ролей не спеціально перевизначений.',\n    'permissions_role_override' => 'Змінити права доступу для ролі',\n    'permissions_inherit_defaults' => 'Успадковувати за замовчуванням',\n\n    // Search\n    'search_results' => 'Результати пошуку',\n    'search_total_results_found' => ':count результатів знайдено|:count всього результатів знайдено',\n    'search_clear' => 'Очистити пошук',\n    'search_no_pages' => 'Немає сторінок, які відповідають цьому пошуку',\n    'search_for_term' => 'Шукати :term',\n    'search_more' => 'Більше результатів',\n    'search_advanced' => 'Розширений пошук',\n    'search_terms' => 'Пошукові фрази',\n    'search_content_type' => 'Тип вмісту',\n    'search_exact_matches' => 'Точна відповідність',\n    'search_tags' => 'Пошукові теги',\n    'search_options' => 'Параметри',\n    'search_viewed_by_me' => 'Переглянуто мною',\n    'search_not_viewed_by_me' => 'Не переглянуто мною',\n    'search_permissions_set' => 'Налаштування дозволів',\n    'search_created_by_me' => 'Створено мною',\n    'search_updated_by_me' => 'Оновлено мною',\n    'search_owned_by_me' => 'Належать мені',\n    'search_date_options' => 'Параметри дати',\n    'search_updated_before' => 'Оновлено до',\n    'search_updated_after' => 'Оновлено після',\n    'search_created_before' => 'Створено до',\n    'search_created_after' => 'Створено після',\n    'search_set_date' => 'Встановити дату',\n    'search_update' => 'Оновити пошук',\n\n    // Shelves\n    'shelf' => 'Полиця',\n    'shelves' => 'Полиці',\n    'x_shelves' => ':count Полиця|:count Полиць',\n    'shelves_empty' => 'Жодних полиць не було створено',\n    'shelves_create' => 'Створити нову полицю',\n    'shelves_popular' => 'Популярні полиці',\n    'shelves_new' => 'Нові полиці',\n    'shelves_new_action' => 'Нова полиця',\n    'shelves_popular_empty' => 'Найпопулярніші полиці з\\'являться тут.',\n    'shelves_new_empty' => 'Тут будуть з\\'являтися останні створені полиці.',\n    'shelves_save' => 'Зберегти полицю',\n    'shelves_books' => 'Книги на цій полиці',\n    'shelves_add_books' => 'Додати книги до цієї полиці',\n    'shelves_drag_books' => 'Перетягніть книги нижче, щоб додати їх до цієї полиці',\n    'shelves_empty_contents' => 'Ця полиця не має призначених їй книг',\n    'shelves_edit_and_assign' => 'Редагувати полицю для присвоєння книг',\n    'shelves_edit_named' => 'Редагувати полицю :name',\n    'shelves_edit' => 'Редагувати полицю',\n    'shelves_delete' => 'Видалити полицю',\n    'shelves_delete_named' => 'Видалити полицю :name',\n    'shelves_delete_explain' => \"Це видалить полицю з ім'ям ':name'. Якщо містить книги, не буде видалено.\",\n    'shelves_delete_confirmation' => 'Ви упевнені, що хочете видалити цю полицю?',\n    'shelves_permissions' => 'Дозволи полиці',\n    'shelves_permissions_updated' => 'Дозволи полиці оновлено',\n    'shelves_permissions_active' => 'Дозволи полиці активні',\n    'shelves_permissions_cascade_warning' => 'Дозволи на полицях не каскадують автоматично до вміщених книг. Це тому, що книга може стояти на кількох полицях. Однак дозволи можна скопіювати до дочірніх книг за допомогою наведеної нижче опції.',\n    'shelves_permissions_create' => 'Створення привілеїв для копіювання дозволів для дочірніх книг, використовуючи наведену нижче дію. Вони не контролюють можливість створення книг.',\n    'shelves_copy_permissions_to_books' => 'Копіювати дозволи на книги',\n    'shelves_copy_permissions' => 'Копіювати дозволи',\n    'shelves_copy_permissions_explain' => 'Це застосує поточні налаштування дозволів цієї полиці до всіх книг, які містяться в ній. Перед активацією переконайтеся, що будь-які зміни в дозволах цієї полиці збережено.',\n    'shelves_copy_permission_success' => 'Права полиці скопійовано до :count книг',\n\n    // Books\n    'book' => 'Книга',\n    'books' => 'Книги',\n    'x_books' => ':count книга|:count книг',\n    'books_empty' => 'Немає створених книг',\n    'books_popular' => 'Популярні книги',\n    'books_recent' => 'Останні книги',\n    'books_new' => 'Нові книги',\n    'books_new_action' => 'Нова книга',\n    'books_popular_empty' => 'Найпопулярніші книги з\\'являться тут.',\n    'books_new_empty' => 'Найновіші книги з\\'являться тут.',\n    'books_create' => 'Створити нову книгу',\n    'books_delete' => 'Видалити книгу',\n    'books_delete_named' => 'Видалити книгу :bookName',\n    'books_delete_explain' => 'Це призведе до видалення книги з назвою \\':bookName\\'. Всі сторінки та розділи будуть видалені.',\n    'books_delete_confirmation' => 'Ви впевнені, що хочете видалити цю книгу?',\n    'books_edit' => 'Редагувати книгу',\n    'books_edit_named' => 'Редагувати книгу :bookName',\n    'books_form_book_name' => 'Назва книги',\n    'books_save' => 'Зберегти книгу',\n    'books_permissions' => 'Дозволи на книгу',\n    'books_permissions_updated' => 'Дозволи на книгу оновлено',\n    'books_empty_contents' => 'Для цієї книги не створено жодної сторінки або розділів.',\n    'books_empty_create_page' => 'Створити нову сторінку',\n    'books_empty_sort_current_book' => 'Сортувати поточну книгу',\n    'books_empty_add_chapter' => 'Додати розділ',\n    'books_permissions_active' => 'Діючі дозволи на книгу',\n    'books_search_this' => 'Шукати цю книгу',\n    'books_navigation' => 'Навігація по книзі',\n    'books_sort' => 'Сортувати вміст книги',\n    'books_sort_desc' => 'Перекладіть розділи та сторінки в межах книги, щоб реорганізувати вміст. Інші книги можна додати, що дозволяє легко переміщати глави та сторінки між книгами. При необхідності правило автоматичного сортування може бути встановлено для автоматичного сортування вмісту цієї книги при змінах.',\n    'books_sort_auto_sort' => 'Опція автоматичного сортування',\n    'books_sort_auto_sort_active' => 'Автосортування : :sortName',\n    'books_sort_named' => 'Сортувати книгу :bookName',\n    'books_sort_name' => 'Сортувати за назвою',\n    'books_sort_created' => 'Сортувати за датою створення',\n    'books_sort_updated' => 'Сортувати за датою оновлення',\n    'books_sort_chapters_first' => 'Спершу розділи',\n    'books_sort_chapters_last' => 'Розділи в кінці',\n    'books_sort_show_other' => 'Показати інші книги',\n    'books_sort_save' => 'Зберегти нове замовлення',\n    'books_sort_show_other_desc' => 'Додавайте інші книги, щоб включити їх у операцію сортування та дозволити легку повторну організацію перехресних книг.',\n    'books_sort_move_up' => 'Перемістити вгору',\n    'books_sort_move_down' => 'Перемістити нижче',\n    'books_sort_move_prev_book' => 'Перейти до попередньої книги',\n    'books_sort_move_next_book' => 'Перейти до наступної книги',\n    'books_sort_move_prev_chapter' => 'Перейти до попереднього розділу',\n    'books_sort_move_next_chapter' => 'Перейти до наступного розділу',\n    'books_sort_move_book_start' => 'Перейти до початку книги',\n    'books_sort_move_book_end' => 'Перейти до кінця книги',\n    'books_sort_move_before_chapter' => 'Перейти до розділу',\n    'books_sort_move_after_chapter' => 'Перехід в кінець розділу',\n    'books_copy' => 'Копіювати книгу',\n    'books_copy_success' => 'Сторінка успішно скопійована',\n\n    // Chapters\n    'chapter' => 'Розділ',\n    'chapters' => 'Розділи',\n    'x_chapters' => ':count розділ|:count розділів',\n    'chapters_popular' => 'Популярні розділи',\n    'chapters_new' => 'Новий розділ',\n    'chapters_create' => 'Створити новий розділ',\n    'chapters_delete' => 'Видалити розділ',\n    'chapters_delete_named' => 'Видалити розділ :chapterName',\n    'chapters_delete_explain' => 'Це видалить розділ під назвою \\':chapterName\\'. Усі сторінки, що існують у цьому розділі, також будуть видалені.',\n    'chapters_delete_confirm' => 'Ви впевнені, що хочете видалити цей розділ?',\n    'chapters_edit' => 'Редагувати розділ',\n    'chapters_edit_named' => 'Редагувати розділ :chapterName',\n    'chapters_save' => 'Зберегти розділ',\n    'chapters_move' => 'Перемістити розділ',\n    'chapters_move_named' => 'Перемістити розділ :chapterName',\n    'chapters_copy' => 'Копіювати розділ',\n    'chapters_copy_success' => 'Розділ успішно скопійовано',\n    'chapters_permissions' => 'Дозволи розділу',\n    'chapters_empty' => 'У цьому розділі немає сторінок.',\n    'chapters_permissions_active' => 'Діючі дозволи на розділ',\n    'chapters_permissions_success' => 'Дозволи на розділ оновлено',\n    'chapters_search_this' => 'Шукати в цьому розділі',\n    'chapter_sort_book' => 'Сортувати книгу',\n\n    // Pages\n    'page' => 'Сторінка',\n    'pages' => 'Сторінки',\n    'x_pages' => ':count сторінка|:count сторінок',\n    'pages_popular' => 'Популярні сторінки',\n    'pages_new' => 'Нова сторінка',\n    'pages_attachments' => 'Вкладення',\n    'pages_navigation' => 'Навігація по сторінці',\n    'pages_delete' => 'Видалити сторінку',\n    'pages_delete_named' => 'Видалити сторінку :pageName',\n    'pages_delete_draft_named' => 'Видалити чернетку :pageName',\n    'pages_delete_draft' => 'Видалити чернетку',\n    'pages_delete_success' => 'Сторінка видалена',\n    'pages_delete_draft_success' => 'Чернетка видалена',\n    'pages_delete_warning_template' => 'Ця сторінка використовується в якості шаблону сторінки за промовчанням. У цих книгах або розділах більше не буде встановлено шаблон стандартної сторінки, який використовується після того, як ця сторінка буде видалена.',\n    'pages_delete_confirm' => 'Ви впевнені, що хочете видалити цю сторінку?',\n    'pages_delete_draft_confirm' => 'Ви впевнені, що хочете видалити цю чернетку?',\n    'pages_editing_named' => 'Редагування сторінки :pageName',\n    'pages_edit_draft_options' => 'Параметри чернетки',\n    'pages_edit_save_draft' => 'Зберегти чернетку',\n    'pages_edit_draft' => 'Редагувати чернетку сторінки',\n    'pages_editing_draft' => 'Редагування чернетки',\n    'pages_editing_page' => 'Редагування сторінки',\n    'pages_edit_draft_save_at' => 'Чернетка збережена о ',\n    'pages_edit_delete_draft' => 'Видалити чернетку',\n    'pages_edit_delete_draft_confirm' => 'Ви дійсно бажаєте видалити зміни у чернетці? Всі зміни, починаючи з останнього повного збереження, будуть втрачені і редактор буде оновлений з останньою сторінкою збереження без чернетки.',\n    'pages_edit_discard_draft' => 'Відхилити чернетку',\n    'pages_edit_switch_to_markdown' => 'Змінити редактор на Markdown',\n    'pages_edit_switch_to_markdown_clean' => '(Очистити вміст)',\n    'pages_edit_switch_to_markdown_stable' => '(Стабілізувати вміст)',\n    'pages_edit_switch_to_wysiwyg' => 'Змінити редактор на WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg' => 'Перейти на новий WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(На бета-тестування)',\n    'pages_edit_set_changelog' => 'Встановити журнал змін',\n    'pages_edit_enter_changelog_desc' => 'Введіть короткий опис внесених вами змін',\n    'pages_edit_enter_changelog' => 'Введіть список змін',\n    'pages_editor_switch_title' => 'Змінити редактор',\n    'pages_editor_switch_are_you_sure' => 'Ви впевнені, що хочете змінити редактор цієї сторінки?',\n    'pages_editor_switch_consider_following' => 'Врахуйте наступне при зміні редакторів:',\n    'pages_editor_switch_consideration_a' => 'Після збереження нова опція редактора буде використовуватися будь-якими майбутніми редакторами, включаючи ті, які не можуть змінювати сам редактор редакторів.',\n    'pages_editor_switch_consideration_b' => 'Це може потенційно призвести до втрати деталізації та синтаксису за певних обставин.',\n    'pages_editor_switch_consideration_c' => 'Мітка або список змін, зроблених з часу останнього збереження, не буде зберігатися в цих змінах.',\n    'pages_save' => 'Зберегти сторінку',\n    'pages_title' => 'Заголовок сторінки',\n    'pages_name' => 'Назва сторінки',\n    'pages_md_editor' => 'Редактор',\n    'pages_md_preview' => 'Попередній перегляд',\n    'pages_md_insert_image' => 'Вставити зображення',\n    'pages_md_insert_link' => 'Вставити посилання на об\\'єкт',\n    'pages_md_insert_drawing' => 'Вставити малюнок',\n    'pages_md_show_preview' => 'Показати попередній перегляд',\n    'pages_md_sync_scroll' => 'Синхронізація прокручування попереднього перегляду',\n    'pages_md_plain_editor' => 'Текстовий редактор',\n    'pages_drawing_unsaved' => 'Знайдено незбережену чернетку',\n    'pages_drawing_unsaved_confirm' => 'Незбережені чернетки були знайдені з попередньої спроби зберегти звіт. Хочете відновити і продовжити редагування цієї чернетки?',\n    'pages_not_in_chapter' => 'Сторінка не знаходиться в розділі',\n    'pages_move' => 'Перемістити сторінку',\n    'pages_copy' => 'Копіювати сторінку',\n    'pages_copy_desination' => 'Ціль копіювання',\n    'pages_copy_success' => 'Сторінка успішно скопійована',\n    'pages_permissions' => 'Дозволи на сторінку',\n    'pages_permissions_success' => 'Дозволи на сторінку оновлено',\n    'pages_revision' => 'Версія',\n    'pages_revisions' => 'Версія сторінки',\n    'pages_revisions_desc' => 'Нижче наведено всі попередні версії цієї сторінки. Ви можете переглядати, порівнювати та відновлювати старі версії сторінок, якщо це дозволено. Повна історія сторінки може бути показана не повністю, оскільки, залежно від конфігурації системи, старі версії можуть автоматично видалятися.',\n    'pages_revisions_named' => 'Версії сторінки для :pageName',\n    'pages_revision_named' => 'Версія сторінки для :pageName',\n    'pages_revision_restored_from' => 'Відновлено з #:id; :summary',\n    'pages_revisions_created_by' => 'Створена',\n    'pages_revisions_date' => 'Дата версії',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Номер редакції',\n    'pages_revisions_numbered' => 'Версія #:id',\n    'pages_revisions_numbered_changes' => 'Зміни версії #:id',\n    'pages_revisions_editor' => 'Тип редактора',\n    'pages_revisions_changelog' => 'Історія змін',\n    'pages_revisions_changes' => 'Зміни',\n    'pages_revisions_current' => 'Поточна версія',\n    'pages_revisions_preview' => 'Попередній перегляд',\n    'pages_revisions_restore' => 'Відновити',\n    'pages_revisions_none' => 'Ця сторінка не має версій',\n    'pages_copy_link' => 'Копіювати посилання',\n    'pages_edit_content_link' => 'Перейти до розділу в редакторі',\n    'pages_pointer_enter_mode' => 'Введіть режим вибору розділу',\n    'pages_pointer_label' => 'Параметри розділу сторінки',\n    'pages_pointer_permalink' => 'Постійне посилання на секцію сторінок',\n    'pages_pointer_include_tag' => ' Секція сторінки включаючи тег',\n    'pages_pointer_toggle_link' => 'Постійне посилання, натисніть для включення тегу',\n    'pages_pointer_toggle_include' => 'Включити режим тегів, натисніть для відображення постійного посилання',\n    'pages_permissions_active' => 'Активні дозволи сторінки',\n    'pages_initial_revision' => 'Початкова публікація',\n    'pages_references_update_revision' => 'Автоматичне оновлення системних посилань',\n    'pages_initial_name' => 'Нова сторінка',\n    'pages_editing_draft_notification' => 'Ви наразі редагуєте чернетку, що була збережена останньою :timeDiff.',\n    'pages_draft_edited_notification' => 'З того часу ця сторінка була оновлена. Рекомендуємо відмовитися від цього проекту.',\n    'pages_draft_page_changed_since_creation' => 'Ця сторінка була оновлена, оскільки була створена ця чернетка. Рекомендується відхилити цей проект або перейматися тим, що ви не перезапишете будь-які зміни в сторінках.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count користувачі(в) почали редагувати цю сторінку',\n        'start_b' => ':userName розпочав редагування цієї сторінки',\n        'time_a' => 'з моменту останньої оновлення сторінки',\n        'time_b' => 'за останні :minCount хвилин',\n        'message' => ':start :time. Будьте обережні, щоб не перезаписати оновлення інших!',\n    ],\n    'pages_draft_discarded' => 'Чернетку відкинуто! Редактор був оновлений з поточним вмістом сторінки',\n    'pages_draft_deleted' => 'Чернетку видалено! Редактор був оновлений з поточною сторінкою вмісту',\n    'pages_specific' => 'Конкретна сторінка',\n    'pages_is_template' => 'Шаблон сторінки',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Перемикач бічної панелі',\n    'page_tags' => 'Теги сторінки',\n    'chapter_tags' => 'Теги розділів',\n    'book_tags' => 'Теги книг',\n    'shelf_tags' => 'Теги полиць',\n    'tag' => 'Тег',\n    'tags' =>  'Теги',\n    'tags_index_desc' => 'Теги можна застосовувати до вмісту в системі, щоб застосувати гнучку форму категоризації. Теги можуть мати як ключ, так і значення, при цьому значення є необов’язковим. Після застосування вміст можна запитувати за допомогою імені та значення тегу.',\n    'tag_name' =>  'Назва тегу',\n    'tag_value' => 'Значення тегу (необов\\'язково)',\n    'tags_explain' => \"Додайте кілька тегів, щоб краще класифікувати ваш вміст. \\n Ви можете присвоїти значення тегу для більш глибокої організації.\",\n    'tags_add' => 'Додати ще один тег',\n    'tags_remove' => 'Видалити цей тег',\n    'tags_usages' => 'Усього тегів використано',\n    'tags_assigned_pages' => 'Призначено до сторінок',\n    'tags_assigned_chapters' => 'Призначені до груп',\n    'tags_assigned_books' => 'Призначено до книг',\n    'tags_assigned_shelves' => 'Призначені до полиць',\n    'tags_x_unique_values' => ':count унікальних значень',\n    'tags_all_values' => 'Всі значення',\n    'tags_view_tags' => 'Перегляд міток',\n    'tags_view_existing_tags' => 'Перегляд існуючих тегів',\n    'tags_list_empty_hint' => 'Теги можуть бути призначені через бічну панель редактора сторінки, або під час редагування деталей книги, глави чи полиці.',\n    'attachments' => 'Вкладення',\n    'attachments_explain' => 'Завантажте файли, або додайте посилання, які відображатимуться на вашій сторінці. Їх буде видно на бічній панелі сторінки.',\n    'attachments_explain_instant_save' => 'Зміни тут зберігаються миттєво.',\n    'attachments_upload' => 'Завантажити файл',\n    'attachments_link' => 'Приєднати посилання',\n    'attachments_upload_drop' => 'Крім того, ви можете перетягувати файл тут і завантажити його в якості вкладення.',\n    'attachments_set_link' => 'Встановити посилання',\n    'attachments_delete' => 'Дійсно хочете видалити це вкладення?',\n    'attachments_dropzone' => 'Для завантаження перетягніть файли',\n    'attachments_no_files' => 'Файли не завантажені',\n    'attachments_explain_link' => 'Ви можете приєднати посилання, якщо не бажаєте завантажувати файл. Це може бути посилання на іншу сторінку або посилання на файл у хмарі.',\n    'attachments_link_name' => 'Назва посилання',\n    'attachment_link' => 'Посилання на вкладення',\n    'attachments_link_url' => 'Посилання на файл',\n    'attachments_link_url_hint' => 'URL-адреса сайту або файлу',\n    'attach' => 'Приєднати',\n    'attachments_insert_link' => 'Додати посилання на вкладення',\n    'attachments_edit_file' => 'Редагувати файл',\n    'attachments_edit_file_name' => 'Назва файлу',\n    'attachments_edit_drop_upload' => 'Перетягніть файли, або натисніть тут щоб завантажити та перезаписати',\n    'attachments_order_updated' => 'Порядок вкладень оновлено',\n    'attachments_updated_success' => 'Деталі вкладень оновлено',\n    'attachments_deleted' => 'Вкладення видалено',\n    'attachments_file_uploaded' => 'Файл успішно завантажений',\n    'attachments_file_updated' => 'Файл успішно оновлено',\n    'attachments_link_attached' => 'Посилання успішно додано до сторінки',\n    'templates' => 'Шаблони',\n    'templates_set_as_template' => 'Сторінка це шаблон',\n    'templates_explain_set_as_template' => 'Ви можете встановити цю сторінку як шаблон, щоб її вміст використовувався під час створення інших сторінок. Інші користувачі зможуть користуватися цим шаблоном, якщо вони мають права перегляду для цієї сторінки.',\n    'templates_replace_content' => 'Замінити вміст сторінки',\n    'templates_append_content' => 'Додати до вмісту сторінки',\n    'templates_prepend_content' => 'Додати на початок вмісту сторінки',\n\n    // Profile View\n    'profile_user_for_x' => 'Користувач вже :time',\n    'profile_created_content' => 'Створений контент',\n    'profile_not_created_pages' => ':userName не створив жодної сторінки',\n    'profile_not_created_chapters' => ':userName не створив жодного розділу',\n    'profile_not_created_books' => ':userName не створив жодної книги',\n    'profile_not_created_shelves' => ':userName не створив жодної полиці',\n\n    // Comments\n    'comment' => 'Коментар',\n    'comments' => 'Коментарі',\n    'comment_add' => 'Додати коментар',\n    'comment_none' => 'Немає коментарів для відображення',\n    'comment_placeholder' => 'Залиште коментар тут',\n    'comment_thread_count' => ':count тема коментарів|:count теми коментарів',\n    'comment_archived_count' => 'Архівовано :count',\n    'comment_archived_threads' => 'Архівовані теми',\n    'comment_save' => 'Зберегти коментар',\n    'comment_new' => 'Новий коментар',\n    'comment_created' => 'прокоментував :createDiff',\n    'comment_updated' => 'Оновлено :updateDiff користувачем :username',\n    'comment_updated_indicator' => 'Оновлено',\n    'comment_deleted_success' => 'Коментар видалено',\n    'comment_created_success' => 'Коментар додано',\n    'comment_updated_success' => 'Коментар оновлено',\n    'comment_archive_success' => 'Коментар архівовано',\n    'comment_unarchive_success' => 'Коментар розархівовано',\n    'comment_view' => 'Переглянути коментар',\n    'comment_jump_to_thread' => 'Перейти до теми',\n    'comment_delete_confirm' => 'Ви впевнені, що хочете видалити цей коментар?',\n    'comment_in_reply_to' => 'У відповідь на :commentId',\n    'comment_reference' => 'По посиланню',\n    'comment_reference_outdated' => '(Застарілий)',\n    'comment_editor_explain' => 'Ось коментарі, які залишилися на цій сторінці. Коментарі можна додати та керовані при перегляді збереженої сторінки.',\n\n    // Revision\n    'revision_delete_confirm' => 'Ви впевнені, що хочете видалити цю версію?',\n    'revision_restore_confirm' => 'Дійсно відновити цю версію? Вміст поточної сторінки буде замінено.',\n    'revision_cannot_delete_latest' => 'Неможливо видалити останню версію.',\n\n    // Copy view\n    'copy_consider' => 'Будь ласка, наведені нижче при копіюванні вмісту.',\n    'copy_consider_permissions' => 'Спеціальні налаштування дозволів не будуть скопійовані.',\n    'copy_consider_owner' => 'Ви станете власником всіх скопійованих матеріалів.',\n    'copy_consider_images' => 'Файли зображень сторінки не будуть дубльовані і оригінальні зображення збережуть зв\\'язок з сторінкою, до якої вони були завантажені.',\n    'copy_consider_attachments' => 'Вкладення сторінки не буде скопійовано.',\n    'copy_consider_access' => 'Зміна розташування або дозволів може призвести до доступу до цього вмісту без попереднього доступу.',\n\n    // Conversions\n    'convert_to_shelf' => 'Перетворити на полиця',\n    'convert_to_shelf_contents_desc' => 'Ви можете перетворити цю книгу на нову полицю з одним змістом. Розділи, що містяться в цій книзі, будуть перетворені в нові книги. Якщо ця книга містить будь-які сторінки, яких немає у главі, цю книгу буде перейменовано і містить такі сторінки, а ця книга стане частиною нової полиці.',\n    'convert_to_shelf_permissions_desc' => 'Будь-які дозволи, встановлені на цій книзі, будуть скопійовані в нову полицю та до всіх нових дитячих книг, які не мають прав на виконання. Зверніть увагу, що дозволи на полицях не автоматично каскад до вмісту в межах, як вони роблять для книг.',\n    'convert_book' => 'Перетворити книгу',\n    'convert_book_confirm' => 'Ви впевнені, що хочете конвертувати цю книгу?',\n    'convert_undo_warning' => 'Це не так легко відмінити.',\n    'convert_to_book' => 'Конвертувати в книгу',\n    'convert_to_book_desc' => 'Ви можете конвертувати цей розділ в нову книгу з одним контентом. Будь-які дозволи, встановлені на цьому розділі, будуть скопійовані в нову книгу, але будь-які успадковані дозволи, з батьківської книги не буде скопійований, що може призвести до зміни контролю доступу.',\n    'convert_chapter' => 'Перетворити розділ',\n    'convert_chapter_confirm' => 'Ви впевнені, що хочете конвертувати цей розділ?',\n\n    // References\n    'references' => 'Посилання',\n    'references_none' => 'Немає відслідковуваних посилань для цього елемента.',\n    'references_to_desc' => 'У списку наведений нижче всі відомі вміст системи, посилання на цей вузол.',\n\n    // Watch Options\n    'watch' => 'Дивитися',\n    'watch_title_default' => 'Властивості по замовчуванню',\n    'watch_desc_default' => 'Відновити перегляд лише до типових налаштувань сповіщень.',\n    'watch_title_ignore' => 'Ігнорувати',\n    'watch_desc_ignore' => 'Ігнорувати всі повідомлення, у тому числі з налаштувань рівня користувача.',\n    'watch_title_new' => 'Нові сторінки',\n    'watch_desc_new' => 'Сповіщати при створенні нової сторінки у цьому елементі.',\n    'watch_title_updates' => 'Оновлення всієї сторінки',\n    'watch_desc_updates' => 'Сповіщати про всі нові сторінки і зміни сторінок.',\n    'watch_desc_updates_page' => 'Сповіщати про зміни на всіх сторінках.',\n    'watch_title_comments' => 'Всі оновлення та коментарі до сторінки',\n    'watch_desc_comments' => 'Повідомляти про всі нові сторінки, зміни до сторінки і нові коментарі.',\n    'watch_desc_comments_page' => 'Повідомляти про зміни сторінки і нові коментарі.',\n    'watch_change_default' => 'Змінити налаштування сповіщень за замовчуванням',\n    'watch_detail_ignore' => 'Ігнорування сповіщень',\n    'watch_detail_new' => 'Перегляд нових сторінок',\n    'watch_detail_updates' => 'Перегляд нових сторінок і оновлень',\n    'watch_detail_comments' => 'Перегляд нових сторінок, оновлень та коментарів',\n    'watch_detail_parent_book' => 'Перегляд за допомогою батьківської книги',\n    'watch_detail_parent_book_ignore' => 'Ігнорування за допомогою батьківської книги',\n    'watch_detail_parent_chapter' => 'Перегляд через батьківську главу',\n    'watch_detail_parent_chapter_ignore' => 'Ігнорування через батьківську главу',\n];\n"
  },
  {
    "path": "lang/uk/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Ви не маєте дозволу на доступ до цієї сторінки.',\n    'permissionJson' => 'Ви не маєте дозволу виконувати заявлену дію.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Користувач з електронною адресою: електронна адреса вже існує, але з іншими обліковими даними.',\n    'auth_pre_register_theme_prevention' => 'Обліковий запис користувача не може бути зареєстрований за наданими деталями',\n    'email_already_confirmed' => 'Електронна пошта вже підтверджена, спробуйте увійти.',\n    'email_confirmation_invalid' => 'Цей токен підтвердження недійсний або вже був використаний, будь ласка, спробуйте знову зареєструватися.',\n    'email_confirmation_expired' => 'Термін дії токена підтвердження минув, новий електронний лист підтвердження був відправлений.',\n    'email_confirmation_awaiting' => 'Потрібно підтвердити адресу електронної пошти для облікового запису, який використовується',\n    'ldap_fail_anonymous' => 'LDAP-доступ невдалий, з використання анонімного зв\\'язку',\n    'ldap_fail_authed' => 'LDAP-доступ невдалий, використовуючи задані параметри dn та password',\n    'ldap_extension_not_installed' => 'Розширення PHP LDAP не встановлено',\n    'ldap_cannot_connect' => 'Неможливо підключитися до ldap-сервера, Помилка з\\'єднання',\n    'saml_already_logged_in' => 'Вже увійшли',\n    'saml_no_email_address' => 'Не вдалося знайти електронну адресу для цього користувача у даних, наданих зовнішньою системою аутентифікації',\n    'saml_invalid_response_id' => 'Запит із зовнішньої системи аутентифікації не розпізнається процесом, розпочатим цим додатком. Повернення назад після входу могла спричинити цю проблему.',\n    'saml_fail_authed' => 'Вхід із використанням «:system» не вдався, система не здійснила успішну авторизацію',\n    'oidc_already_logged_in' => 'Вже ввійшли в систему',\n    'oidc_no_email_address' => 'Не вдалося знайти адресу електронної пошти для цього користувача у даних, наданих зовнішньою системою автентифікації',\n    'oidc_fail_authed' => 'Увійти за допомогою :system не вдалося, система не надала успішної авторизації',\n    'social_no_action_defined' => 'Жодних дій не визначено',\n    'social_login_bad_response' => \"Помилка, отримана під час входу з :socialAccount помилка : \\n:error\",\n    'social_account_in_use' => 'Цей :socialAccount обліковий запис вже використовується, спробуйте ввійти з параметрами :socialAccount.',\n    'social_account_email_in_use' => 'Електронна пошта :email вже використовується. Якщо у вас вже є обліковий запис, ви можете підключити свій обліковий запис :socialAccount з налаштувань вашого профілю.',\n    'social_account_existing' => 'Цей :socialAccount вже додано до вашого профілю.',\n    'social_account_already_used_existing' => 'Цей обліковий запис :socialAccount вже використовується іншим користувачем.',\n    'social_account_not_used' => 'Цей обліковий запис :socialAccount account не пов\\'язаний з жодним користувачем. Будь ласка, додайте його в налаштуваннях вашого профілю. ',\n    'social_account_register_instructions' => 'Якщо у вас ще немає облікового запису, ви можете зареєструвати обліковий запис за допомогою параметра :socialAccount.',\n    'social_driver_not_found' => 'Драйвер для СоціальноїМережі не знайдено',\n    'social_driver_not_configured' => 'Ваші соціальні настройки :socialAccount не правильно налаштовані.',\n    'invite_token_expired' => 'Термін дії цього запрошення закінчився. Замість цього ви можете спробувати скинути пароль свого облікового запису.',\n    'login_user_not_found' => 'Користувач для цієї дії не знайдений.',\n\n    // System\n    'path_not_writable' => 'Не вдається завантажити шлях до файлу :filePath. Переконайтеся, що він доступний для запису на сервер.',\n    'cannot_get_image_from_url' => 'Неможливо отримати зображення з :url',\n    'cannot_create_thumbs' => 'Сервер не може створювати ескізи. Будь ласка, перевірте, чи встановлено розширення GD PHP.',\n    'server_upload_limit' => 'Сервер не дозволяє завантажувати файли такого розміру. Спробуйте менший розмір файлу.',\n    'server_post_limit' => 'Сервер не може отримати вказаний обсяг даних. Спробуйте ще раз з меншими даними або меншим файлом.',\n    'uploaded'  => 'Сервер не дозволяє завантажувати файли такого розміру. Спробуйте менший розмір файлу.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Виникла помилка під час завантаження зображення',\n    'image_upload_type_error' => 'Тип завантаженого зображення недійсний',\n    'image_upload_replace_type' => 'Замінники файлів зображень повинні мати однаковий тип',\n    'image_upload_memory_limit' => 'Не вдалося завантажити зображення і/або створити ескізи через обмеження системних ресурсів.',\n    'image_thumbnail_memory_limit' => 'Не вдалося створити варіації розміру зображення через обмеження системних ресурсів.',\n    'image_gallery_thumbnail_memory_limit' => 'Не вдалося створити галерею через обмеження системних ресурсів.',\n    'drawing_data_not_found' => 'Не вдалося завантажити дані малюнка. Файл малюнка може більше не існувати або у вас немає дозволу на доступ до нього.',\n\n    // Attachments\n    'attachment_not_found' => 'Вкладення не знайдено',\n    'attachment_upload_error' => 'Сталася помилка при завантаженні файлу',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Не вдалося зберегти чернетку. Перед збереженням цієї сторінки переконайтеся, що у вас є зв\\'язок з сервером.',\n    'page_draft_delete_fail' => 'Не вдалося видалити чернетку сторінки та отримати збережений вміст сторінки',\n    'page_custom_home_deletion' => 'Неможливо видалити сторінку, коли вона встановлена як домашня сторінка',\n\n    // Entities\n    'entity_not_found' => 'Об\\'єкт не знайдено',\n    'bookshelf_not_found' => 'Книжкова полиця не знайдена',\n    'book_not_found' => 'Книга не знайдена',\n    'page_not_found' => 'Сторінку не знайдено',\n    'chapter_not_found' => 'Розділ не знайдено',\n    'selected_book_not_found' => 'Вибрана книга не знайдена',\n    'selected_book_chapter_not_found' => 'Вибрана книга або глава не знайдена',\n    'guests_cannot_save_drafts' => 'Гості не можуть зберігати чернетки',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Ви не можете видалити єдиного адміністратора',\n    'users_cannot_delete_guest' => 'Ви не можете видалити гостьового користувача',\n    'users_could_not_send_invite' => 'Не вдалося створити користувача, оскільки не вдалося надіслати електронний лист із запрошенням',\n\n    // Roles\n    'role_cannot_be_edited' => 'Цю роль не можна редагувати',\n    'role_system_cannot_be_deleted' => 'Ця роль є системною, і її не можна видалити',\n    'role_registration_default_cannot_delete' => 'Цю роль не можна видалити, бо вона встановлена як роль реєстрації за умовчанням',\n    'role_cannot_remove_only_admin' => 'Цей користувач є єдиним користувачем, призначеним для ролі адміністратора. Призначте роль адміністратора іншому користувачеві, перш ніж спробувати його видалити.',\n\n    // Comments\n    'comment_list' => 'Під час отримання коментарів сталася помилка.',\n    'cannot_add_comment_to_draft' => 'Ви не можете додати коментарі до проекту.',\n    'comment_add' => 'Під час додавання/оновлення коментарів сталася помилка.',\n    'comment_delete' => 'Під час видалення коментаря сталася помилка.',\n    'empty_comment' => 'Неможливо додати порожній коментар.',\n\n    // Error pages\n    '404_page_not_found' => 'Сторінку не знайдено',\n    'sorry_page_not_found' => 'Вибачте, сторінку, яку ви шукали, не знайдено.',\n    'sorry_page_not_found_permission_warning' => 'Якщо ви очікували що ця сторінки існує – можливо у вас немає дозволу на її перегляд.',\n    'image_not_found' => 'Зображення не знайдено',\n    'image_not_found_subtitle' => 'Вибачте, файл зображення, що ви шукали, не знайдено.',\n    'image_not_found_details' => 'Якщо ви очікували існування цього зображення, його, можливо, було видалено.',\n    'return_home' => 'Повернутися на головну',\n    'error_occurred' => 'Виникла помилка',\n    'app_down' => ':appName зараз недоступний',\n    'back_soon' => 'Він повернеться найближчим часом.',\n\n    // Import\n    'import_zip_cant_read' => 'Не вдалося прочитати ZIP-файл.',\n    'import_zip_cant_decode_data' => 'Не вдалося знайти і розшифрувати контент ZIP data.json.',\n    'import_zip_no_data' => 'ZIP-файл не містить очікуваної книги, глави або вмісту сторінки.',\n    'import_zip_data_too_large' => 'Вміст ZIP data.json перевищує налаштований максимальний розмір додатка.',\n    'import_validation_failed' => 'Не вдалося виконати перевірку ZIP-адреси із помилками:',\n    'import_zip_failed_notification' => 'Не вдалося імпортувати ZIP-файл.',\n    'import_perms_books' => 'У Вас не вистачає необхідних прав для створення книг.',\n    'import_perms_chapters' => 'Вам не вистачає необхідних дозволів для створення розділів.',\n    'import_perms_pages' => 'У Вас немає необхідних прав для створення сторінок.',\n    'import_perms_images' => 'У Вас немає необхідних прав для створення зображень.',\n    'import_perms_attachments' => 'У Вас немає необхідних прав для створення вкладень.',\n\n    // API errors\n    'api_no_authorization_found' => 'У запиті не знайдено токен авторизації',\n    'api_bad_authorization_format' => 'У запиті знайдено токен авторизації, але формат недійсний',\n    'api_user_token_not_found' => 'Не знайдено відповідного API-токена для наданого токена авторизації',\n    'api_incorrect_token_secret' => 'Секрет, наданий для даного використовуваного токена API є неправильним',\n    'api_user_no_api_permission' => 'Власник використовуваного токена API не має дозволу здійснювати виклики API',\n    'api_user_token_expired' => 'Термін дії токена авторизації закінчився',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Помилка під час надсилання тестового електронного листа:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'URL-адреса не відповідає налаштованим дозволеним SSR хостів',\n];\n"
  },
  {
    "path": "lang/uk/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Новий коментар на сторінці: :pageName',\n    'new_comment_intro' => 'Користувач прокоментував на сторінці у :appName:',\n    'new_page_subject' => 'Нова сторінка: :pageName',\n    'new_page_intro' => 'Створено сторінку у :appName:',\n    'updated_page_subject' => 'Оновлено сторінку: :pageName',\n    'updated_page_intro' => 'Оновлено сторінку у :appName:',\n    'updated_page_debounce' => 'Для запобігання кількості сповіщень, деякий час ви не будете відправлені повідомлення для подальших змін на цій сторінці тим самим редактором.',\n    'comment_mention_subject' => 'Вас згадали в коментарях на сторінці: :pageName',\n    'comment_mention_intro' => 'Вас згадали в коментарі до :appName:',\n\n    'detail_page_name' => 'Назва сторінки:',\n    'detail_page_path' => 'Шлях до сторінки:',\n    'detail_commenter' => 'Коментатор:',\n    'detail_comment' => 'Коментар:',\n    'detail_created_by' => 'Створено:',\n    'detail_updated_by' => 'Оновлено:',\n\n    'action_view_comment' => 'Переглянути коментар',\n    'action_view_page' => 'Дивитись сторінку',\n\n    'footer_reason' => 'Дане повідомлення було надіслано вам тому, що :link покриває цю діяльність для цього елемента.',\n    'footer_reason_link' => 'ваші налаштування сповіщень',\n];\n"
  },
  {
    "path": "lang/uk/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Попередня',\n    'next'     => 'Наступна &raquo;',\n\n];\n"
  },
  {
    "path": "lang/uk/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Пароль повинен містити не менше восьми символів і збігатись з підтвердженням.',\n    'user' => \"Ми не можемо знайти користувача з цією адресою електронної пошти.\",\n    'token' => 'Токен скидання пароля недійсний для цієї адреси електронної пошти.',\n    'sent' => 'Ми надіслали Вам електронний лист із посиланням для скидання пароля!',\n    'reset' => 'Ваш пароль скинуто!',\n\n];\n"
  },
  {
    "path": "lang/uk/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'Мій аккаунт',\n\n    'shortcuts' => 'Ярлики',\n    'shortcuts_interface' => 'Налаштування ярлика інтерфейсу',\n    'shortcuts_toggle_desc' => 'Тут ви можете увімкнути або вимкнути ярлики інтерфейсу клавіатури, які використовуються для навігації та дій.',\n    'shortcuts_customize_desc' => 'Ви можете налаштувати кожен з ярликів нижче. Просто натисніть на комбінацію бажаного ключа після вибору вводу для ярлика.',\n    'shortcuts_toggle_label' => 'Клавіатурні скорочення увімкнено',\n    'shortcuts_section_navigation' => 'Навігація',\n    'shortcuts_section_actions' => 'Загальні дії',\n    'shortcuts_save' => 'Зберегти ярлики',\n    'shortcuts_overlay_desc' => 'Примітка: якщо ярлики ввімкнено, допоміжне накладання доступне, натиснувши \"?\" який виділить доступні ярлики для дій, які зараз видно на екрані.',\n    'shortcuts_update_success' => 'Налаштування ярликів оновлено!',\n    'shortcuts_overview_desc' => 'Керуйте комбінаціями клавіатур можна використовувати для навігації інтерфейсу системи користувача.',\n\n    'notifications' => 'Налаштування сповіщень',\n    'notifications_desc' => 'Контролюйте сповіщення по електронній пошті, які ви отримуєте, коли виконується певна активність у системі.',\n    'notifications_opt_own_page_changes' => 'Повідомляти при змінах сторінок якими я володію',\n    'notifications_opt_own_page_comments' => 'Повідомляти при коментарях на моїх сторінках',\n    'notifications_opt_comment_mentions' => 'Сповіщати, якщо мене згадали у коментарі',\n    'notifications_opt_comment_replies' => 'Повідомляти про відповіді на мої коментарі',\n    'notifications_save' => 'Зберегти налаштування',\n    'notifications_update_success' => 'Налаштування сповіщень було оновлено!',\n    'notifications_watched' => 'Переглянуті та ігноровані елементи',\n    'notifications_watched_desc' => 'Нижче наведені предмети, які мають застосовані налаштування перегляду. Щоб оновити ваші налаштування для них, перегляньте елемент, а потім знайдіть параметри перегляду на бічній панелі.',\n\n    'auth' => 'Доступ і безпека',\n    'auth_change_password' => 'Змінити пароль',\n    'auth_change_password_desc' => 'Змініть пароль, який ви використовуєте для входу в програму. Це має бути щонайменше 8 символів.',\n    'auth_change_password_success' => 'Пароль оновлено!',\n\n    'profile' => 'Деталі профілю',\n    'profile_desc' => 'Керуйте інформацією вашого облікового запису, що представляє вам інші користувачі, додатково до деталей, що використовуються для комунікації та особистості системи.',\n    'profile_view_public' => 'Перегляд загального профілю',\n    'profile_name_desc' => 'Налаштування відображуваного імена, які будуть видимі іншим користувачам в системі через здійснені вами дії та власний контент.',\n    'profile_email_desc' => 'Цей email буде використовуватися для сповіщення і в залежності від активної автентифікації системи, доступу до системи.',\n    'profile_email_no_permission' => 'На жаль, у вас немає дозволу на зміну адреси електронної пошти. Якщо ви хочете змінити це, ви маєте попросити адміністратора змінити це для вас.',\n    'profile_avatar_desc' => 'Виберіть зображення, яке буде використовуватися для того, щоб представити себе іншим у системі. В ідеалі це зображення має бути квадратне і близько 256пікс в ширину і висоту.',\n    'profile_admin_options' => 'Параметри адміністратора',\n    'profile_admin_options_desc' => 'Додаткові параметри на рівні адміністратора, так як і для управління призначеннями роль, ви можете знайти для вашого облікового запису в області \"Налаштування> Користувачі\" додатку.',\n\n    'delete_account' => 'Видалити обліковий запис',\n    'delete_my_account' => 'Видалити мій обліковий запис',\n    'delete_my_account_desc' => 'Це повністю видалить ваш обліковий запис з системи. Ви не зможете відновити або скасувати цю дію. Контент, який Ви створили, наприклад створені сторінки і вивантажені зображення, залишиться.',\n    'delete_my_account_warning' => 'Ви впевнені, що хочете видалити свій обліковий запис?',\n];\n"
  },
  {
    "path": "lang/uk/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Налаштування',\n    'settings_save' => 'Зберегти налаштування',\n    'system_version' => 'Версія',\n    'categories' => 'Категорії',\n\n    // App Settings\n    'app_customization' => 'Налаштування',\n    'app_features_security' => 'Особливості та безпека',\n    'app_name' => 'Назва програми',\n    'app_name_desc' => 'Ця назва показується у заголовку та в усіх листах.',\n    'app_name_header' => 'Показати назву програми в заголовку',\n    'app_public_access' => 'Публічний доступ',\n    'app_public_access_desc' => 'Увімкнення цієї опції дозволить відвідувачам, які не увійшли в систему, отримати доступ до вмісту у вашому екземплярі BookStack.',\n    'app_public_access_desc_guest' => 'Доступ для публічних відвідувачів можна контролювати через користувача \"Гість\".',\n    'app_public_access_toggle' => 'Дозволити публічний доступ',\n    'app_public_viewing' => 'Дозволити публічний перегляд?',\n    'app_secure_images' => 'Вищі налаштування безпеки для зображень',\n    'app_secure_images_toggle' => 'Увімкунти вищі налаштування безпеки для завантаження зображень',\n    'app_secure_images_desc' => 'З міркувань продуктивності всі зображення є загальнодоступними. Цей параметр додає випадковий, важко передбачуваний рядок перед URL-адресами зображень. Переконайтеся, що індексація каталогів не активована, щоб запобігти легкому доступу.',\n    'app_default_editor' => 'Стандартний редактор сторінок',\n    'app_default_editor_desc' => 'Виберіть, який редактор буде використовуватися за замовчуванням під час редагування нових сторінок. Це можна перевизначити на рівні дозволів сторінки.',\n    'app_custom_html' => 'Користувацький вміст HTML-заголовку',\n    'app_custom_html_desc' => 'Будь-який доданий тут вміст буде вставлено в нижню частину розділу <head> кожної сторінки. Це зручно для перевизначення стилів, або додавання коду аналітики.',\n    'app_custom_html_disabled_notice' => 'На цій сторінці налаштувань відключений користувацький вміст заголовка HTML, щоб гарантувати, що будь-які невдалі зміни можна буде відновити.',\n    'app_logo' => 'Логотип програми',\n    'app_logo_desc' => 'Це використовується в панелі заголовка програми, серед інших областей. Це зображення має бути 86 пікселів у висоту. Великі зображення будуть зменшені.',\n    'app_icon' => 'Значок додатка',\n    'app_icon_desc' => 'Цей значок використовується для вкладок браузера та піктограм ярликів. Це має бути квадратне зображення PNG на 256px.',\n    'app_homepage' => 'Домашня сторінка програми',\n    'app_homepage_desc' => 'Виберіть сторінку, яка показуватиметься на домашній сторінці замість перегляду за замовчуванням. Права на сторінку не враховуються для вибраних сторінок.',\n    'app_homepage_select' => 'Вибрати сторінку',\n    'app_footer_links' => 'Посилання нижньої частини сайту',\n    'app_footer_links_desc' => 'Додайте посилання до нижньої частини сайту. Вони будуть відображатися в нижній частині більшості сторінок, включаючи ті, що не потребують входу. Для використання системних перекладів ви можете скористатися мітками \"trans::<key>\". Наприклад: додавання \"trans:common.privacy_policy\" покаже перекладений текст \"Політика конфіденційності\" а \"trans:common.terms_of_service\" покаже перекладений текст \"Умови надання послуг\".',\n    'app_footer_links_label' => 'Назва посилання',\n    'app_footer_links_url' => 'URL посилання',\n    'app_footer_links_add' => 'Додати посилання до нижньої частини сайту',\n    'app_disable_comments' => 'Вимкнути коментарі',\n    'app_disable_comments_toggle' => 'Вимкнути коментарі',\n    'app_disable_comments_desc' => 'Вимкнути коментарі на всіх сторінках програми. Існуючі коментарі не відображаються.',\n\n    // Color settings\n    'color_scheme' => 'Колірна схема застосунку',\n    'color_scheme_desc' => 'Встановіть кольори для використання в інтерфейсі користувача програми. Кольори можуть бути налаштовані окремо для темних і світлих режимів, щоб найкращим чином відповідати темі й забезпечити розбірливість.',\n    'ui_colors_desc' => 'Встановіть основний колір програми та колір посилання за замовчуванням. Основний колір в основному використовується для банера заголовка, кнопок і прикрас інтерфейсу. Колір посилання за замовчуванням використовується для текстових посилань і дій, як всередині письмового вмісту, так і в інтерфейсі програми.',\n    'app_color' => 'Головний колір',\n    'link_color' => 'Колір посилання за замовчуванням',\n    'content_colors_desc' => 'Установіть кольори для всіх елементів в ієрархії організації сторінки. Для зручності читання рекомендується вибирати кольори з такою ж яскравістю, як і кольори за замовчуванням.',\n    'bookshelf_color' => 'Колір полиці',\n    'book_color' => 'Колір книги',\n    'chapter_color' => 'Колір глави',\n    'page_color' => 'Колір сторінки',\n    'page_draft_color' => 'Колір чернетки',\n\n    // Registration Settings\n    'reg_settings' => 'Реєстрація',\n    'reg_enable' => 'Дозвіл на реєстрацію',\n    'reg_enable_toggle' => 'Дозволити реєстрацію',\n    'reg_enable_desc' => 'При включенні реєстрації відвідувач зможе зареєструватися як користувач програми. Після реєстрації їм надається єдина роль користувача за замовчуванням.',\n    'reg_default_role' => 'Роль користувача за умовчанням після реєстрації',\n    'reg_enable_external_warning' => 'Цей параметр ігнорується, якщо активна зовнішня автентифікація LDAP або SAML. Облікові записи користувачів для неіснуючих учасників будуть створені автоматично, якщо аутентифікація у зовнішній системі буде успішною.',\n    'reg_email_confirmation' => 'Підтвердження електронною поштою',\n    'reg_email_confirmation_toggle' => 'Необхідне підтвердження електронною поштою',\n    'reg_confirm_email_desc' => 'Якщо використовується обмеження домену, то підтвердження електронною поштою буде потрібно, а нижче значення буде проігноровано.',\n    'reg_confirm_restrict_domain' => 'Обмеження по домену',\n    'reg_confirm_restrict_domain_desc' => 'Введіть список розділених комами доменів електронної пошти, до яких ви хочете обмежити реєстрацію. Користувачам буде надіслано електронне повідомлення для підтвердження своєї адреси, перш ніж дозволяти взаємодіяти з додатком. <br> Зауважте, що користувачі зможуть змінювати свої електронні адреси після успішної реєстрації.',\n    'reg_confirm_restrict_domain_placeholder' => 'Не встановлено обмежень',\n\n    // Sorting Settings\n    'sorting' => 'Списки і сортування',\n    'sorting_book_default' => 'Типовий порядок сортування книги',\n    'sorting_book_default_desc' => 'Виберіть правило сортування за замовчуванням для застосування нових книг. Це не вплине на існуючі книги, і може бути перевизначено для кожної книги.',\n    'sorting_rules' => 'Сортувати правила',\n    'sorting_rules_desc' => 'Це попередньо визначені операції сортування, які можуть бути застосовані до вмісту в системі.',\n    'sort_rule_assigned_to_x_books' => 'Призначено :count книгу|Призначення на :count книг(и)',\n    'sort_rule_create' => 'Створити правило сортування',\n    'sort_rule_edit' => 'Змінити правило сортування',\n    'sort_rule_delete' => 'Видалити правило сортування',\n    'sort_rule_delete_desc' => 'Видалення даного правила сортування з системи. Книги за допомогою цього сортування будуть повернутися до ручного сортування.',\n    'sort_rule_delete_warn_books' => 'Це правило сортування використовується на :count книг(у,и). Ви впевнені, що хочете видалити це?',\n    'sort_rule_delete_warn_default' => 'Правило сортування в даний час використовується як правило за замовчуванням для книг. Ви впевнені, що хочете видалити це?',\n    'sort_rule_details' => 'Опис правил сортування',\n    'sort_rule_details_desc' => 'Вкажіть ім\\'я для цього правила сортування, яке буде відображатися в списках при виборі сортування користувачем.',\n    'sort_rule_operations' => 'Операції сортування',\n    'sort_rule_operations_desc' => 'Налаштуйте дії, які слід виконати, переміщаючи їх зі списку доступних операцій. Після використання операцій буде застосовано відповідно до самого низу. Будь-які зміни, внесені сюди, будуть застосовані до всіх призначених книг при збереженні.',\n    'sort_rule_available_operations' => 'Доступні операції',\n    'sort_rule_available_operations_empty' => 'Не залишилось операцій',\n    'sort_rule_configured_operations' => 'Налаштовані операції',\n    'sort_rule_configured_operations_empty' => 'Перетягніть операції зі списку \"Доступні операції\"',\n    'sort_rule_op_asc' => '(За зростанням)',\n    'sort_rule_op_desc' => '(За спаданням)',\n    'sort_rule_op_name' => 'Назва - за алфавітом',\n    'sort_rule_op_name_numeric' => 'Назва - Числове',\n    'sort_rule_op_created_date' => 'Дата створення',\n    'sort_rule_op_updated_date' => 'Дата оновлення',\n    'sort_rule_op_chapters_first' => 'Спочатку розділи',\n    'sort_rule_op_chapters_last' => 'Розділи останні',\n    'sorting_page_limits' => 'Обмеження відображення сторінок',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Обслуговування',\n    'maint_image_cleanup' => 'Очищення зображень',\n    'maint_image_cleanup_desc' => 'Сканує вміст сторінки та версій, щоб перевірити, які зображення та малюнки в даний час використовуються, а також які зображення зайві. Переконайтеся, що ви створили повну резервну копію бази даних та зображення, перш ніж запускати це.',\n    'maint_delete_images_only_in_revisions' => 'Також видалити зображення, що існують лише в старих версіях сторінки',\n    'maint_image_cleanup_run' => 'Запустити очищення',\n    'maint_image_cleanup_warning' => ':count потенційно невикористаних зображень було знайдено. Ви впевнені, що хочете видалити ці зображення?',\n    'maint_image_cleanup_success' => ':count потенційно невикористані зображення знайдено і видалено!',\n    'maint_image_cleanup_nothing_found' => 'Не знайдено невикористовуваних зображень, нічого не видалено!',\n    'maint_send_test_email' => 'Надіслати тестове повідомлення',\n    'maint_send_test_email_desc' => 'Надіслати тестового листа на адресу електронної пошти, що вказана у вашому профілі.',\n    'maint_send_test_email_run' => 'Надіслати тестовий лист',\n    'maint_send_test_email_success' => 'Лист відправлений на : адреса',\n    'maint_send_test_email_mail_subject' => 'Перевірка електронної пошти',\n    'maint_send_test_email_mail_greeting' => 'Доставляння електронної пошти працює!',\n    'maint_send_test_email_mail_text' => 'Вітаємо! Оскільки ви отримали цього листа, поштова скринька налаштована правильно.',\n    'maint_recycle_bin_desc' => 'Видалені полиці, книги, розділи та сторінки попадають кошик, щоб їх можна було відновити або видалити остаточно. Старіші елементи з кошика можна автоматично видаляти через деякий час, залежно від налаштувань системи.',\n    'maint_recycle_bin_open' => 'Відкрити кошик',\n    'maint_regen_references' => 'Перегенерувати посилання',\n    'maint_regen_references_desc' => 'Ця дія перебудує міжелементний посилальний індекс у базі даних. Зазвичай це виконується автоматично, але ця дія може бути корисною для індексування старого вмісту або вмісту, доданого неофіційними методами.',\n    'maint_regen_references_success' => 'Індекс посилань перестворений!',\n    'maint_timeout_command_note' => 'Примітка: Ця дія може зайняти час для запуску, що може призвести до тимчасових проблем в деяких веб-середовищах. Як альтернативу, цю дію виконуються за допомогою термінальної команди.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Кошик',\n    'recycle_bin_desc' => 'Тут ви можете відновити видалені елементи, або назавжди видалити їх із системи. Цей список нефільтрований, на відміну від подібних списків активності в системі, де застосовуються фільтри дозволів.',\n    'recycle_bin_deleted_item' => 'Виадлений елемент',\n    'recycle_bin_deleted_parent' => 'Батьківський',\n    'recycle_bin_deleted_by' => 'Ким видалено',\n    'recycle_bin_deleted_at' => 'Час видалення',\n    'recycle_bin_permanently_delete' => 'Видалити остаточно',\n    'recycle_bin_restore' => 'Відновити',\n    'recycle_bin_contents_empty' => 'Зараз кошик порожній',\n    'recycle_bin_empty' => 'Очистити кошик',\n    'recycle_bin_empty_confirm' => 'Це назавжди знищить усі елементи в кошику, включаючи вміст кожного елементу. Ви впевнені, що хочете очистити кошик?',\n    'recycle_bin_destroy_confirm' => 'Ця дія назавжди видалить цей елемент з системи, разом з будь-яким дочірнім елементом, перерахованим нижче, і ви не зможете відновити цей контент. Ви дійсно бажаєте остаточно видалити цей елемент?',\n    'recycle_bin_destroy_list' => 'Елементи для знищення',\n    'recycle_bin_restore_list' => 'Елементи для відновлення',\n    'recycle_bin_restore_confirm' => 'Ця дія відновить видалений елемент у початкове місце, включаючи всі дочірні елементи. Якщо вихідне розташування відтоді було видалено, і знаходиться у кошику, батьківський елемент також потрібно буде відновити.',\n    'recycle_bin_restore_deleted_parent' => 'Батьківський елемент цього об\\'єкта також був видалений. Вони залишатимуться видаленими, доки батьківський елемент також не буде відновлений.',\n    'recycle_bin_restore_parent' => 'Відновити батьківську',\n    'recycle_bin_destroy_notification' => 'Видалено :count елементів із кошика.',\n    'recycle_bin_restore_notification' => 'Відновлено :count елементів із кошика.',\n\n    // Audit Log\n    'audit' => 'Журнал аудиту',\n    'audit_desc' => 'Цей журнал аудиту показує список відстежуваних у системі дій. Цей список нефільтрований, на відміну від подібних списків активності в системі, де застосовуються фільтри дозволів.',\n    'audit_event_filter' => 'Фільтр подій',\n    'audit_event_filter_no_filter' => 'Без фільтра',\n    'audit_deleted_item' => 'Видалений елемент',\n    'audit_deleted_item_name' => 'Назва: :name',\n    'audit_table_user' => 'Користувач',\n    'audit_table_event' => 'Подія',\n    'audit_table_related' => 'Пов’язаний елемент',\n    'audit_table_ip' => 'IP-адреса',\n    'audit_table_date' => 'Дата активності',\n    'audit_date_from' => 'Діапазон дат від',\n    'audit_date_to' => 'Діапазон дат до',\n\n    // Role Settings\n    'roles' => 'Ролі',\n    'role_user_roles' => 'Ролі користувача',\n    'roles_index_desc' => 'Ролі використовуються для групування користувачів і надання системних дозволів їхнім учасникам. Якщо користувач є членом кількох ролей, надані привілеї сумуються, і користувач успадковує всі здібності.',\n    'roles_x_users_assigned' => ':count користувач призначений|:count користувачів призначених',\n    'roles_x_permissions_provided' => ':count дозвіл|:count дозволів',\n    'roles_assigned_users' => 'Призначені користувачі',\n    'roles_permissions_provided' => 'Надані доступи',\n    'role_create' => 'Створити нову роль',\n    'role_delete' => 'Видалити роль',\n    'role_delete_confirm' => 'Це призведе до видалення ролі з назвою \\':roleName\\'.',\n    'role_delete_users_assigned' => 'Цій ролі належать :userCount користувачі(в). Якщо ви хочете перенести користувачів із цієї ролі, виберіть нову роль нижче.',\n    'role_delete_no_migration' => \"Не мігрувати користувачів\",\n    'role_delete_sure' => 'Ви впевнені, що хочете видалити цю роль?',\n    'role_edit' => 'Редагувати роль',\n    'role_details' => 'Деталі ролі',\n    'role_name' => 'Назва ролі',\n    'role_desc' => 'Короткий опис ролі',\n    'role_mfa_enforced' => 'Потрібна двофактова автентифікація',\n    'role_external_auth_id' => 'Зовнішні ID автентифікації',\n    'role_system' => 'Системні дозволи',\n    'role_manage_users' => 'Керування користувачами',\n    'role_manage_roles' => 'Керування правами ролей та ролями',\n    'role_manage_entity_permissions' => 'Керування всіма правами на книги, розділи та сторінки',\n    'role_manage_own_entity_permissions' => 'Керування дозволами на власну книгу, розділ та сторінки',\n    'role_manage_page_templates' => 'Управління шаблонами сторінок',\n    'role_access_api' => 'Доступ до системного API',\n    'role_manage_settings' => 'Керування налаштуваннями програми',\n    'role_export_content' => 'Вміст експорту',\n    'role_import_content' => 'Імпортувати вміст',\n    'role_editor_change' => 'Змінити редактор сторінок',\n    'role_notifications' => 'Отримувати та керувати повідомленнями',\n    'role_permission_note_users_and_roles' => 'Ці дозволи технічно також забезпечать видимість і пошук ролей у системі.',\n    'role_asset' => 'Дозволи',\n    'roles_system_warning' => 'Майте на увазі, що доступ до будь-якого з вищезазначених трьох дозволів може дозволити користувачеві змінювати власні привілеї або привілеї інших в системі. Ролі з цими дозволами призначайте лише довіреним користувачам.',\n    'role_asset_desc' => 'Ці дозволи контролюють стандартні доступи всередині системи. Права на книги, розділи та сторінки перевизначать ці дозволи.',\n    'role_asset_admins' => 'Адміністратори автоматично отримують доступ до всього вмісту, але ці параметри можуть відображати або приховувати параметри інтерфейсу користувача.',\n    'role_asset_image_view_note' => 'Це стосується видимості в менеджері зображень. Фактичний доступ завантажуваних зображень буде залежний від опції зберігання системних зображень.',\n    'role_asset_users_note' => 'Ці дозволи технічно також забезпечать видимість і пошук користувачів і ролей у системі.',\n    'role_all' => 'Все',\n    'role_own' => 'Власне',\n    'role_controlled_by_asset' => 'Контролюється за об\\'єктом, до якого вони завантажуються',\n    'role_save' => 'Зберегти роль',\n    'role_users' => 'Користувачі в цій ролі',\n    'role_users_none' => 'Наразі жоден користувач не призначений для цієї ролі',\n\n    // Users\n    'users' => 'Користувачі',\n    'users_index_desc' => 'Створюйте та керуйте індивідуальними обліковими записами користувачів у системі. Облікові записи користувачів використовуються для входу та атрибуції вмісту та активності. Дозволи доступу в основному залежать від ролей, але право власності на вміст користувача, серед інших факторів, також може впливати на дозволи та доступ.',\n    'user_profile' => 'Профіль користувача',\n    'users_add_new' => 'Додати нового користувача',\n    'users_search' => 'Пошук користувачів',\n    'users_latest_activity' => 'Остання активність',\n    'users_details' => 'Відомості про користувача',\n    'users_details_desc' => 'Встановіть ім\\'я та електронну адресу для цього користувача. Адреса електронної пошти буде використана для входу до програми.',\n    'users_details_desc_no_email' => 'Встановіть ім\\'я для цього користувача, щоб інші могли його розпізнати.',\n    'users_role' => 'Ролі користувача',\n    'users_role_desc' => 'Виберіть, до яких ролей буде призначено цього користувача. Якщо користувачеві призначено декілька ролей, дозволи з цих ролей будуть складатись і вони отримуватимуть усі можливості призначених ролей.',\n    'users_password' => 'Пароль користувача',\n    'users_password_desc' => 'Встановіть пароль для входу. Він повинен містити принаймні 5 символів.',\n    'users_send_invite_text' => 'Ви можете надіслати цьому користувачеві лист із запрошенням, що дозволить йому встановити пароль власноруч, або ви можете встановити йому пароль самостійно.',\n    'users_send_invite_option' => 'Надіслати листа із запрошенням користувачу',\n    'users_external_auth_id' => 'Зовнішній ID автентифікації',\n    'users_external_auth_id_desc' => 'Коли використовується зовнішня система аутентифікації (наприклад, SAML2, OIDC або LDAP), це ідентифікатор, який пов\\'язує цього користувача BookStack з обліковим записом системи аутентифікації. Ви можете ігнорувати це поле, якщо використовуєте типову автентифікацію на основі електронної пошти.',\n    'users_password_warning' => 'Заповніть поле нижче, лише якщо ви хочете змінити пароль для цього користувача.',\n    'users_system_public' => 'Цей користувач представляє будь-яких гостьових користувачів, які відвідують ваш екземпляр. Його не можна використовувати для входу, але він призначається автоматично.',\n    'users_delete' => 'Видалити користувача',\n    'users_delete_named' => 'Видалити користувача :userName',\n    'users_delete_warning' => 'Це повне видалення цього користувача з ім\\'ям \\':userName\\' з системи.',\n    'users_delete_confirm' => 'Ви впевнені, що хочете видалити цього користувача?',\n    'users_migrate_ownership' => 'Право власності при міграції',\n    'users_migrate_ownership_desc' => 'Виберіть тут користувача, якщо ви хочете, щоб інший користувач став власником усіх елементів, які зараз належать цьому користувачеві.',\n    'users_none_selected' => 'Не вибрано жодного користувача',\n    'users_edit' => 'Редагувати користувача',\n    'users_edit_profile' => 'Редагувати профіль',\n    'users_avatar' => 'Аватар користувача',\n    'users_avatar_desc' => 'Це квадратне зображення має бути приблизно 256px.',\n    'users_preferred_language' => 'Бажана мова',\n    'users_preferred_language_desc' => 'Цей параметр змінить мову інтерфейсу користувача в програмі. Не вплине на створений користувачем вміст.',\n    'users_social_accounts' => 'Соціальні акаунти',\n    'users_social_accounts_desc' => 'Перегляд стану підключених соціальних облікових записів для цього користувача. Соціальні акаунти можуть використовуватися на додаток до первинної системи аутентифікації для доступу до системи.',\n    'users_social_accounts_info' => 'Тут ви можете підключити інші облікові записи для швидшого та легшого входу. Від\\'єднання соціального облікового запису тут не дозволяється. Скасуйте доступ із налаштувань вашого профілю в пов\\'язаній соціальній мережі.',\n    'users_social_connect' => 'Підключити обліковий запис',\n    'users_social_disconnect' => 'Від\\'єднати обліковий запис',\n    'users_social_status_connected' => 'Під’єднано',\n    'users_social_status_disconnected' => 'Від\\'єднано',\n    'users_social_connected' => 'Обліковий запис :socialAccount успішно додано до вашого профілю.',\n    'users_social_disconnected' => 'Обліковий запис :socialAccount був успішно відключений від вашого профілю.',\n    'users_api_tokens' => 'API токени',\n    'users_api_tokens_desc' => 'Створюйте та керуйте токенами доступу, які використовуються для автентифікації за допомогою BookStack REST API. Дозволи для API управляються через користувача, якому належить токен.',\n    'users_api_tokens_none' => 'Жодного токена API не створено для цього користувача',\n    'users_api_tokens_create' => 'Створити токен',\n    'users_api_tokens_expires' => 'Закінчується',\n    'users_api_tokens_docs' => 'Документація API',\n    'users_mfa' => 'Багатофакторна Автентифікація',\n    'users_mfa_desc' => 'Двофакторна аутентифікація додає ще один рівень безпеки для вашого облікового запису.',\n    'users_mfa_x_methods' => ':count метод налаштовано|:count методів налаштовано',\n    'users_mfa_configure' => 'Налаштувати Методи',\n\n    // API Tokens\n    'user_api_token_create' => 'Створити токен API',\n    'user_api_token_name' => 'Назва',\n    'user_api_token_name_desc' => 'Дайте своєму токену читабельну назву як майбутнє нагадування про його пряме призначення.',\n    'user_api_token_expiry' => 'Дата закінчення',\n    'user_api_token_expiry_desc' => 'Встановіть дату закінчення терміну дії цього токена. Після цієї дати запити, зроблені за допомогою цього токена, більше не працюватимуть. Якщо залишити це поле порожнім, термін дії токена закінчиться через 100 років.',\n    'user_api_token_create_secret_message' => 'Відразу після створення цього токена буде створено та показано «Ідентифікатор токена» та «Ключ токена». Ключ буде показано лише один раз, тому перед тим, як продовжити, не забудьте скопіювати значення ключа в надійне та безпечне місце.',\n    'user_api_token' => 'Токен API',\n    'user_api_token_id' => 'Ідентифікатор (ID) токена',\n    'user_api_token_id_desc' => 'Системний ідентифікатор цього токена, який потрібно буде вказати в запитах API. Його редагування неможливе.',\n    'user_api_token_secret' => 'Ключ токена',\n    'user_api_token_secret_desc' => 'Це ключ, згенерований системою для цього токена, його потрібно буде надати в запитах API. Він буде видимий лише цього разу, тому скопіюйте це значення в безпечне та надійне місце.',\n    'user_api_token_created' => 'Токен створено :timeAgo',\n    'user_api_token_updated' => 'Токен оновлено :timeAgo',\n    'user_api_token_delete' => 'Видалити токен',\n    'user_api_token_delete_warning' => 'Ця дія повністю видалить цей токен API із назвою \\':tokenName\\' з системи.',\n    'user_api_token_delete_confirm' => 'Дійсно хочете видалити цей токен API?',\n\n    // Webhooks\n    'webhooks' => 'Веб-хуки',\n    'webhooks_index_desc' => 'Вебхуки – це спосіб надсилання даних на зовнішні URL-адреси, коли в системі відбуваються певні дії та події, що дозволяє інтегрувати події на основі зовнішніх платформ, таких як системи обміну повідомленнями чи сповіщення.',\n    'webhooks_x_trigger_events' => ':count тригерна подія|:count тригерних подій',\n    'webhooks_create' => 'Створити новий Веб-хук',\n    'webhooks_none_created' => 'Немає створених Веб-хуків.',\n    'webhooks_edit' => 'Редагувати Веб-хук',\n    'webhooks_save' => 'Зберегти Веб-хук',\n    'webhooks_details' => 'Деталі вебхуків',\n    'webhooks_details_desc' => 'Вкажіть дружнє ім\\'я користувача та кінцеву точку POST як місце для надсилання даних вебхуків.',\n    'webhooks_events' => 'Події вебхуків',\n    'webhooks_events_desc' => 'Оберіть всі події, які мають викликати цей web-хук, щоб бути викликані.',\n    'webhooks_events_warning' => 'Майте на увазі, що ці події будуть запущені для всіх вибраних подій, навіть якщо використовуються користувацькі дозволи. Переконайтеся, що використання цього вебхука не розкриє конфіденційний контент.',\n    'webhooks_events_all' => 'Всі системні події',\n    'webhooks_name' => 'Назва вебхука',\n    'webhooks_timeout' => 'Час очікування запиту веб хука (в секундах)',\n    'webhooks_endpoint' => 'Webhook кінцевої точки',\n    'webhooks_active' => 'Веб хук активний',\n    'webhook_events_table_header' => 'Події',\n    'webhooks_delete' => 'Видалити Webhook',\n    'webhooks_delete_warning' => 'Ця дія повністю видалить цей токен Api із назвою \\':tokenName\\' з системи.',\n    'webhooks_delete_confirm' => 'Ви впевнені, що хочете видалити цей веб хук?',\n    'webhooks_format_example' => 'Приклад формату веб хука',\n    'webhooks_format_example_desc' => 'Дані веб хука надсилаються як POST запит до налаштованої кінцевої точки у вигляді JSON з відповідним форматом. Властивості \"related_item\" і \"url\" є необов\\'язковими та залежатимуть від типу події.',\n    'webhooks_status' => 'Статус веб хука',\n    'webhooks_last_called' => 'Останній виклик:',\n    'webhooks_last_errored' => 'Остання помилка:',\n    'webhooks_last_error_message' => 'Останнє повідомлення про помилку:',\n\n    // Licensing\n    'licenses' => 'Ліцензії',\n    'licenses_desc' => 'На цій сторінці детально описано ліцензійну інформацію для BookStack на додаток до проектів і бібліотек, які використовуються в BookStack. Багато проектів із списку можна використовувати лише в контексті розробки.',\n    'licenses_bookstack' => 'Ліцензія BookStack',\n    'licenses_php' => 'Ліцензії на бібліотеки PHP',\n    'licenses_js' => 'Ліцензії бібліотеки JavaScript',\n    'licenses_other' => 'Інші ліцензії',\n    'license_details' => 'Про ліцензію',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/uk/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => 'Ви повинні прийняти :attribute.',\n    'active_url'           => 'Поле :attribute не є правильним URL.',\n    'after'                => 'Поле :attribute має містити дату не раніше :date.',\n    'alpha'                => 'Поле :attribute має містити лише літери.',\n    'alpha_dash'           => 'Поле :attribute має містити лише літери, цифри, дефіси та підкреслення.',\n    'alpha_num'            => 'Поле :attribute має містити лише літери та цифри.',\n    'array'                => 'Поле :attribute має бути масивом.',\n    'backup_codes'         => 'Наданий код є недійсним або вже використаний.',\n    'before'               => 'Поле :attribute має містити дату не пізніше :date.',\n    'between'              => [\n        'numeric' => 'Поле :attribute має бути між :min та :max.',\n        'file'    => 'Розмір файлу в полі :attribute має бути не менше :min та не більше :max кілобайт.',\n        'string'  => 'Текст в полі :attribute має бути не менше :min та не більше :max символів.',\n        'array'   => 'Поле :attribute має містити від :min до :max елементів.',\n    ],\n    'boolean'              => 'Поле :attribute повинне містити true чи false.',\n    'confirmed'            => 'Поле :attribute не збігається з підтвердженням.',\n    'date'                 => 'Поле :attribute не є датою.',\n    'date_format'          => 'Поле :attribute не відповідає формату :format.',\n    'different'            => 'Поля :attribute та :other повинні бути різними.',\n    'digits'               => 'Довжина цифрового поля :attribute повинна дорівнювати :digits.',\n    'digits_between'       => 'Довжина цифрового поля :attribute повинна бути від :min до :max.',\n    'email'                => 'Поле :attribute повинне містити коректну електронну адресу.',\n    'ends_with' => 'Поле :attribute має закінчуватися одним з наступних значень: :values',\n    'file'                 => 'Поле :attribute повинне містити коректний файл.',\n    'filled'               => 'Поле :attribute є обов\\'язковим для заповнення.',\n    'gt'                   => [\n        'numeric' => 'Поле :attribute має бути більше ніж :value.',\n        'file'    => 'Поле :attribute має бути більше ніж :value кілобайт.',\n        'string'  => 'Поле :attribute має бути більше ніж :value символів.',\n        'array'   => 'Поле :attribute має містити більше ніж :value елементів.',\n    ],\n    'gte'                  => [\n        'numeric' => 'Поле :attribute має дорівнювати чи бути більше ніж :value.',\n        'file'    => 'Поле :attribute має дорівнювати чи бути більше ніж :value кілобайт.',\n        'string'  => 'Поле :attribute має дорівнювати чи бути більше ніж :value символів.',\n        'array'   => 'Поле :attribute має містити :value чи більше елементів.',\n    ],\n    'exists'               => 'Вибране для :attribute значення не коректне.',\n    'image'                => 'Поле :attribute має містити зображення.',\n    'image_extension'      => 'Поле :attribute має містити дійсне та підтримуване розширення зображення.',\n    'in'                   => 'Вибране для :attribute значення не коректне.',\n    'integer'              => 'Поле :attribute має містити ціле число.',\n    'ip'                   => 'Поле :attribute має містити IP адресу.',\n    'ipv4'                 => 'Поле :attribute має містити IPv4 адресу.',\n    'ipv6'                 => 'Поле :attribute має містити IPv6 адресу.',\n    'json'                 => 'Дані поля :attribute мають бути в форматі JSON.',\n    'lt'                   => [\n        'numeric' => 'Поле :attribute має бути менше ніж :value.',\n        'file'    => 'Поле :attribute має бути менше ніж :value кілобайт.',\n        'string'  => 'Поле :attribute має бути менше ніж :value символів.',\n        'array'   => 'Поле :attribute має містити менше ніж :value елементів.',\n    ],\n    'lte'                  => [\n        'numeric' => 'Поле :attribute має дорівнювати чи бути менше ніж :value.',\n        'file'    => 'Поле :attribute має дорівнювати чи бути менше ніж :value кілобайт.',\n        'string'  => 'Поле :attribute має дорівнювати чи бути менше ніж :value символів.',\n        'array'   => 'Поле :attribute має містити не більше ніж :value елементів.',\n    ],\n    'max'                  => [\n        'numeric' => 'Поле :attribute має бути не більше :max.',\n        'file'    => 'Файл в полі :attribute має бути не більше :max кілобайт.',\n        'string'  => 'Текст в полі :attribute повинен мати довжину не більшу за :max.',\n        'array'   => 'Поле :attribute повинне містити не більше :max елементів.',\n    ],\n    'mimes'                => 'Поле :attribute повинне містити файл одного з типів: :values.',\n    'min'                  => [\n        'numeric' => 'Поле :attribute повинне бути не менше :min.',\n        'file'    => 'Розмір файлу в полі :attribute має бути не меншим :min кілобайт.',\n        'string'  => 'Текст в полі :attribute повинен містити не менше :min символів.',\n        'array'   => 'Поле :attribute повинне містити не менше :min елементів.',\n    ],\n    'not_in'               => 'Вибране для :attribute значення не коректне.',\n    'not_regex'            => 'Формат поля :attribute не вірний.',\n    'numeric'              => 'Поле :attribute повинно містити число.',\n    'regex'                => 'Поле :attribute має хибний формат.',\n    'required'             => 'Поле :attribute є обов\\'язковим для заповнення.',\n    'required_if'          => 'Поле :attribute є обов\\'язковим для заповнення, коли :other є рівним :value.',\n    'required_with'        => 'Поле :attribute є обов\\'язковим для заповнення, коли :values вказано.',\n    'required_with_all'    => 'Поле :attribute є обов\\'язковим для заповнення, коли :values вказано.',\n    'required_without'     => 'Поле :attribute є обов\\'язковим для заповнення, коли :values не вказано.',\n    'required_without_all' => 'Поле :attribute є обов\\'язковим для заповнення, коли :values не вказано.',\n    'same'                 => 'Поля :attribute та :other мають збігатися.',\n    'safe_url'             => 'Надане посилання може бути небезпечним.',\n    'size'                 => [\n        'numeric' => 'Поле :attribute має бути довжини :size.',\n        'file'    => 'Файл в полі :attribute має бути розміром :size кілобайт.',\n        'string'  => 'Текст в полі :attribute повинен містити :size символів.',\n        'array'   => 'Поле :attribute повинне містити :size елементів.',\n    ],\n    'string'               => 'Поле :attribute повинне містити текст.',\n    'timezone'             => 'Поле :attribute повинне містити коректну часову зону.',\n    'totp'                 => 'Наданий код не є дійсним або прострочений.',\n    'unique'               => 'Вказане значення поля :attribute вже існує.',\n    'url'                  => 'Формат поля :attribute неправильний.',\n    'uploaded'             => 'Не вдалося завантажити файл. Сервер може не приймати файли такого розміру.',\n\n    'zip_file' => 'Поле :attribute повинне вказувати файл в ZIP.',\n    'zip_file_size' => 'Файл :attribute не повинен перевищувати :size МБ.',\n    'zip_file_mime' => 'Поле :attribute повинне посилатись на файл типу :validtypes, знайдений :foundType.',\n    'zip_model_expected' => 'Очікувався об’єкт даних, але знайдено \":type\".',\n    'zip_unique' => 'Поле :attribute має бути унікальним для типу об\\'єкта в ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Необхідне підтвердження пароля',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/uz/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'yaratilgan sahifa',\n    'page_create_notification'    => 'Sahifa muvaffaqiyatli yaratildi',\n    'page_update'                 => 'yangilangan sahifa',\n    'page_update_notification'    => 'Sahifa muvaffaqiyatli yangilandi',\n    'page_delete'                 => 'o‘chirilgan sahifa',\n    'page_delete_notification'    => 'Sahifa muvaffaqiyatli o‘chirildi',\n    'page_restore'                => 'tiklangan sahifa',\n    'page_restore_notification'   => 'Sahifa muvaffaqiyatli qayta tiklandi',\n    'page_move'                   => 'ko‘chirilgan sahifa',\n    'page_move_notification'      => 'Page successfully moved',\n\n    // Chapters\n    'chapter_create'              => 'yaratilgan bo‘lim',\n    'chapter_create_notification' => 'Bo‘lim muvaffaqiyatli yaratildi',\n    'chapter_update'              => 'yangilangan bo‘lim',\n    'chapter_update_notification' => 'Bo‘lim muvaffaqiyatli yangilandi',\n    'chapter_delete'              => 'o‘chirilgan bo‘lim',\n    'chapter_delete_notification' => 'Bo‘lim muvaffaqiyatli o‘chirildi',\n    'chapter_move'                => 'ko‘chirilgan bo‘lim',\n    'chapter_move_notification' => 'Bo‘lim muvaffaqiyatli ko‘chirildi',\n\n    // Books\n    'book_create'                 => 'yaratilgan kitob',\n    'book_create_notification'    => 'Kitob muvaffaqiyatli yaratildi',\n    'book_create_from_chapter'              => 'bo‘lim kitobga o‘girildi',\n    'book_create_from_chapter_notification' => 'Bo‘lim kitobga muvaffaqiyatli o‘girildi',\n    'book_update'                 => 'yangilangan kitob',\n    'book_update_notification'    => 'Kitob muvaffaqiyatli yangilandi',\n    'book_delete'                 => 'o‘chirilgan kitob',\n    'book_delete_notification'    => 'Kitob muvaffaqiyatli o‘chirildi',\n    'book_sort'                   => 'tartiblangan kitob',\n    'book_sort_notification'      => 'Kitob muvaffaqiyatli qayta tartiblandi',\n\n    // Bookshelves\n    'bookshelf_create'            => 'Javon yaratildi',\n    'bookshelf_create_notification'    => 'Javon muvaffaqiyatli yaratildi',\n    'bookshelf_create_from_book'    => 'kitob javonga o‘girildi',\n    'bookshelf_create_from_book_notification'    => 'kitob javonga muvaffaqiyatli o‘girildi',\n    'bookshelf_update'                 => 'updated shelf',\n    'bookshelf_update_notification'    => 'Shelf successfully updated',\n    'bookshelf_delete'                 => 'deleted shelf',\n    'bookshelf_delete_notification'    => 'Shelf successfully deleted',\n\n    // Revisions\n    'revision_restore' => 'restored revision',\n    'revision_delete' => 'deleted revision',\n    'revision_delete_notification' => 'Revision successfully deleted',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" sevimlilaringizga qo‘shildi',\n    'favourite_remove_notification' => '\":name\" sevimlilaringizdan olib tashlandi',\n\n    // Watching\n    'watch_update_level_notification' => 'Watch preferences successfully updated',\n\n    // Auth\n    'auth_login' => 'logged in',\n    'auth_register' => 'registered as new user',\n    'auth_password_reset_request' => 'requested user password reset',\n    'auth_password_reset_update' => 'reset user password',\n    'mfa_setup_method' => 'configured MFA method',\n    'mfa_setup_method_notification' => 'Multi-faktor uslubi muvaffaqiyatli sozlandi',\n    'mfa_remove_method' => 'removed MFA method',\n    'mfa_remove_method_notification' => 'Multi-faktor uslubi muvaffaqiyatli o‘chirildi',\n\n    // Settings\n    'settings_update' => 'updated settings',\n    'settings_update_notification' => 'Settings successfully updated',\n    'maintenance_action_run' => 'ran maintenance action',\n\n    // Webhooks\n    'webhook_create' => 'yaratilgan webhook',\n    'webhook_create_notification' => 'Webhook muvaffaqiyatli yaratildi',\n    'webhook_update' => 'yangilangan webhook',\n    'webhook_update_notification' => 'Webhook muvaffaqiyatli yangilandi',\n    'webhook_delete' => 'o‘chirilgan webhook',\n    'webhook_delete_notification' => 'Webhook muvaffaqiyatli o‘chirildi',\n\n    // Imports\n    'import_create' => 'created import',\n    'import_create_notification' => 'Import successfully uploaded',\n    'import_run' => 'updated import',\n    'import_run_notification' => 'Content successfully imported',\n    'import_delete' => 'deleted import',\n    'import_delete_notification' => 'Import successfully deleted',\n\n    // Users\n    'user_create' => 'created user',\n    'user_create_notification' => 'User successfully created',\n    'user_update' => 'updated user',\n    'user_update_notification' => 'Foydalanuvchi muvaffaqiyatli yangilandi',\n    'user_delete' => 'deleted user',\n    'user_delete_notification' => 'Foydalanuvchi muvaffaqiyatli olib tashlandi',\n\n    // API Tokens\n    'api_token_create' => 'created API token',\n    'api_token_create_notification' => 'API token successfully created',\n    'api_token_update' => 'updated API token',\n    'api_token_update_notification' => 'API token successfully updated',\n    'api_token_delete' => 'deleted API token',\n    'api_token_delete_notification' => 'API token successfully deleted',\n\n    // Roles\n    'role_create' => 'created role',\n    'role_create_notification' => 'Role successfully created',\n    'role_update' => 'updated role',\n    'role_update_notification' => 'Role successfully updated',\n    'role_delete' => 'deleted role',\n    'role_delete_notification' => 'Role successfully deleted',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'emptied recycle bin',\n    'recycle_bin_restore' => 'restored from recycle bin',\n    'recycle_bin_destroy' => 'removed from recycle bin',\n\n    // Comments\n    'commented_on'                => 'fikr qoldirdi',\n    'comment_create'              => 'added comment',\n    'comment_update'              => 'updated comment',\n    'comment_delete'              => 'deleted comment',\n\n    // Sort Rules\n    'sort_rule_create' => 'created sort rule',\n    'sort_rule_create_notification' => 'Sort rule successfully created',\n    'sort_rule_update' => 'updated sort rule',\n    'sort_rule_update_notification' => 'Sort rule successfully updated',\n    'sort_rule_delete' => 'deleted sort rule',\n    'sort_rule_delete_notification' => 'Sort rule successfully deleted',\n\n    // Other\n    'permissions_update'          => 'yangilangan huquqlar',\n];\n"
  },
  {
    "path": "lang/uz/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Uchbu ma‘lumotlar, bizdagi ma‘lumotlarga mos kelmadi.',\n    'throttle' => 'Kirishga urinishlar juda ko‘p. Iltimos :seconds soniyadan so‘ng urinib ko‘ring.',\n\n    // Login & Register\n    'sign_up' => 'Ro‘yxatdan o‘tish',\n    'log_in' => 'Kirish',\n    'log_in_with' => ':socialDriver orqali kirish',\n    'sign_up_with' => ':socialDriver orqali ro‘yxatdan o‘tish',\n    'logout' => 'Chiqish',\n\n    'name' => 'Ism',\n    'username' => 'Foydalanuvchi nomi',\n    'email' => 'Elektron pochta',\n    'password' => 'Parol',\n    'password_confirm' => 'Parolni tasdiqlash',\n    'password_hint' => 'Kamida 8 belgi bo‘lishi kerak',\n    'forgot_password' => 'Parolni unutdingizmi?',\n    'remember_me' => 'Eslab qoling',\n    'ldap_email_hint' => 'Ush hisob bilan o‘tish uchun emailni kiritish.',\n    'create_account' => 'Profil yaratildi',\n    'already_have_account' => 'Profilingiz bormi?',\n    'dont_have_account' => 'Profilingiz yo‘qmi?',\n    'social_login' => 'Ijtimoiy tarmoqlar orqali kirish',\n    'social_registration' => 'Ijtimoiy tarmoqlar orqali ro‘yxatdan o‘tish',\n    'social_registration_text' => 'Boshqa tarmoqdan foydalanish.',\n\n    'register_thanks' => 'Ro‘yxatdan o‘tganingiz uchun rahmat!',\n    'register_confirm' => ':appName dan foydalanish uchun iltimos emailingizga yuborilgan xatni ochib, tasdiqlovchi link orqali o‘ting.',\n    'registrations_disabled' => 'Hozirda ro‘yxatdan o‘tish yopilgan',\n    'registration_email_domain_invalid' => 'Ush domendagi email bilan ro‘yxatdan o‘tib bo‘lmaydi',\n    'register_success' => 'Ro‘yxatdan o‘tganingiz uchun rahmat! Endi siz ushbu hisob bilan saytga kirishingiz mumkin.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Kirishga urinish',\n    'auto_init_starting_desc' => 'Kirish jarayonini boshlash uchun kirish orqali murojaat qilyapmiz. Agar 5 soniyadan keyin hech qanday o‘zgarish bo‘lmasa, havolani bosib ko‘ tiklash mumkin.',\n    'auto_init_start_link' => 'Kirish uchun bosing',\n\n    // Password Reset\n    'reset_password' => 'Parolni qayta tiklash',\n    'reset_password_send_instructions' => 'Parolni tiklash manzilini olish uchun emailingizni maydonga kiriting.',\n    'reset_password_send_button' => 'Tiklash manzilini yuborish',\n    'reset_password_sent' => 'Agar tizimda ushbu elektron pochta manzili topilsa, parolni tiklash havolasi :email manziliga yuboriladi.',\n    'reset_password_success' => 'Parolingiz yaxshilandi.',\n    'email_reset_subject' => ':appName parolingizni tiklash',\n    'email_reset_text' => 'Profilingiz uchun parolni oʻ Soʻrovini olganimiz uchun sizga bu xat keldi.',\n    'email_reset_not_requested' => 'Agar sizga parolni tiklashni so‘ramagan bo‘lsangiz, boshqa hech qanday harakat talab qilish mumkin.',\n\n    // Email Confirmation\n    'email_confirm_subject' => ':appName orqali elektron pochtangizni tasdiqlang',\n    'email_confirm_greeting' => ':appName\\'ga qo‘shilganingiz uchun tashakkur!',\n    'email_confirm_text' => 'Quyidagi tugmani bosish orqali elektron pochta manzilingizni tasdiqlang:',\n    'email_confirm_action' => 'E-pochta manzilini tasdiqlash',\n    'email_confirm_send_error' => 'Elektron pochtani tasdiqlash talab qilinadi, lekin tizim elektron pochta xabarini yubora olmadi. Elektron pochta toʻgʻri sozlanganligiga ishonch hosil qilish uchun administrator bilan bogʻlaning.',\n    'email_confirm_success' => 'Emailingiz tasdiqlandi! Endi siz ushbu elektron pochta manzilidan foydalanib tizimga kirishingiz kerak.',\n    'email_confirm_resent' => 'Tasdiqlash xati qayta yuborildi. Iltimos, pochta qutingizni tekshiring.',\n    'email_confirm_thanks' => 'Tasdiqlaganingiz uchun tashakkur!',\n    'email_confirm_thanks_desc' => 'Tasdiqlash jarayoni tugaguncha biroz kuting. Agar 3 soniyadan keyin qayta yoʻnaltirilmasangiz, davom etish uchun quyidagi “Davom etish” havolasini bosing.',\n\n    'email_not_confirmed' => 'Elektron pochta manzili tasdiqlanmagan',\n    'email_not_confirmed_text' => 'Sizning elektron pochta manzilingiz hali tasdiqlanmagan.',\n    'email_not_confirmed_click_link' => 'Roʻyxatdan oʻtganingizdan soʻng, elektron pochtaga yuborilgan havolani bosing.',\n    'email_not_confirmed_resend' => 'Agar elektron pochta manzilini topa olmasangiz, quyidagi shaklni yuborish orqali tasdiqlash xatini qayta yuborishingiz mumkin.',\n    'email_not_confirmed_resend_button' => 'Tasdiqlash xatini qayta yuborish',\n\n    // User Invite\n    'user_invite_email_subject' => 'Siz :appName ilovasiga qo‘shilishga taklif qilindingiz!',\n    'user_invite_email_greeting' => 'Siz uchun :appName ilovasida hisob yaratildi.',\n    'user_invite_email_text' => 'Hisob qaydnomasi parolini o‘rnatish va unga kirish uchun quyidagi tugmani bosing:',\n    'user_invite_email_action' => 'Hisob parolini o‘rnating',\n    'user_invite_page_welcome' => ':appName ga xush kelibsiz!',\n    'user_invite_page_text' => 'Hisob qaydnomangizni yakunlash va kirish huquqini qo‘lga kiritish uchun parolni o‘rnatishingiz kerak, undan keyingi tashriflaringizda :appName tizimiga kirish uchun foydalaniladi.',\n    'user_invite_page_confirm_button' => 'Parolni tasdiqlang',\n    'user_invite_success_login' => 'Parol o‘rnatilgan, endi siz o‘rnatilgan parolingizdan foydalanib tizimga kirishingiz kerak: appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Ko‘p faktorli autentifikatsiyani sozlash',\n    'mfa_setup_desc' => 'Ko‘p faktorli autentifikatsiyani foydalanuvchi hisobingiz uchun qo‘shimcha xavfsizlik qatlami sifatida o‘rnatish.',\n    'mfa_setup_configured' => 'Allaqachon sozlangan',\n    'mfa_setup_reconfigure' => 'Qayta sozlang',\n    'mfa_setup_remove_confirmation' => 'Haqiqatan ham bu koʻp faktorli autentifikatsiya usulini olib tashlamoqchimisiz?',\n    'mfa_setup_action' => 'Sozlash; O‘rnatish',\n    'mfa_backup_codes_usage_limit_warning' => 'Sizda 5 tadan kam zaxira kodingiz qoldi. Profilingiz bloklanib qolmasligi uchun kodlar tugashidan oldin yangi to‘plamni yarating va saqlang.',\n    'mfa_option_totp_title' => 'Mobil ilova',\n    'mfa_option_totp_desc' => 'Ko‘p faktorli autentifikatsiyadan foydalanish uchun sizga Google Authenticator, Authy yoki Microsoft Authenticator kabi OTPni qo‘llab-quvvatlaydigan mobil ilova kerak bo‘ladi.',\n    'mfa_option_backup_codes_title' => 'Zaxira kodlari',\n    'mfa_option_backup_codes_desc' => 'Shaxsingizni tasdiqlash uchun tizimga kirishda kiritadigan bir martalik zaxira kodlari to\\'plamini yaratadi. Bularni xavfsiz va ishonchli joyda saqlang.',\n    'mfa_gen_confirm_and_enable' => 'Tasdiqlash va yoqish',\n    'mfa_gen_backup_codes_title' => 'Zaxira kodlarini sozlash',\n    'mfa_gen_backup_codes_desc' => 'Quyidagi kodlar ro‘yxatini xavfsiz joyda saqlang. Tizimga kirishda siz kodlardan birini ikkinchi autentifikatsiya mexanizmi sifatida ishlatishingiz mumkin.',\n    'mfa_gen_backup_codes_download' => 'Kodlarni yuklab olish',\n    'mfa_gen_backup_codes_usage_warning' => 'Har bir kod faqat bir marta ishlatilishi mumkin',\n    'mfa_gen_totp_title' => 'Mobil ilovani sozlash',\n    'mfa_gen_totp_desc' => 'Ko‘p faktorli autentifikatsiyadan foydalanish uchun sizga Google Authenticator, Authy yoki Microsoft Authenticator kabi TOTPni qo‘llab-quvvatlaydigan mobil ilova kerak bo‘ladi.',\n    'mfa_gen_totp_scan' => 'Ishni boshlash uchun siz tanlagan autentifikatsiya ilovasi yordamida quyidagi QR kodni skanerlang.',\n    'mfa_gen_totp_verify_setup' => 'Oʻrnatishni tasdiqlang',\n    'mfa_gen_totp_verify_setup_desc' => 'Quyidagi kiritish maydoniga autentifikatsiya ilovangizda yaratilgan kodni kiritish orqali hammasi ishlayotganiga ishonch hosil qiling:',\n    'mfa_gen_totp_provide_code_here' => 'Bu yerda ilovangiz tomonidan yaratilgan kodni kiriting',\n    'mfa_verify_access' => 'Kirishni tasdiqlang',\n    'mfa_verify_access_desc' => 'Sizning foydalanuvchi hisobingiz sizga ruxsat berishdan oldin shaxsingizni tasdiqlashning qoʻshimcha darajasi orqali tasdiqlashingizni talab qiladi. Davom etish uchun sozlangan usullardan biri yordamida tasdiqlang.',\n    'mfa_verify_no_methods' => 'Hech qanday usul sozlanmagan',\n    'mfa_verify_no_methods_desc' => 'Profilingiz uchun ko‘p faktorli autentifikatsiya usullari topilmadi. Kirishdan oldin kamida bitta usulni sozlashingiz kerak.',\n    'mfa_verify_use_totp' => 'Mobil ilova yordamida tasdiqlang',\n    'mfa_verify_use_backup_codes' => 'Zaxira kod yordamida tasdiqlang',\n    'mfa_verify_backup_code' => 'Zaxira kodi',\n    'mfa_verify_backup_code_desc' => 'Qolgan zaxira kodlaringizdan birini pastga kiriting:',\n    'mfa_verify_backup_code_enter_here' => 'Bu yerga zaxira kodini kiriting',\n    'mfa_verify_totp_desc' => 'Quyida mobil ilovangiz yordamida yaratilgan kodni kiriting:',\n    'mfa_setup_login_notification' => 'Ko‘p faktorli usul sozlangan. Iltimos, endi sozlangan usul yordamida qayta kiring.',\n];\n"
  },
  {
    "path": "lang/uz/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Bekor qilsih',\n    'close' => 'Yopish',\n    'confirm' => 'Tasdiqlang',\n    'back' => 'Orqaga',\n    'save' => 'Saqlash',\n    'continue' => 'Davom etish',\n    'select' => 'Tanlang',\n    'toggle_all' => 'Hammasini almashtirish',\n    'more' => 'Ko‘proq',\n\n    // Form Labels\n    'name' => 'Nom',\n    'description' => 'Tavsif',\n    'role' => 'Rol',\n    'cover_image' => 'Muqova rasmi',\n    'cover_image_description' => 'This image should be approximately 440x250px although it will be flexibly scaled & cropped to fit the user interface in different scenarios as required, so actual dimensions for display will differ.',\n\n    // Actions\n    'actions' => 'Harakatlar',\n    'view' => 'Ko‘rinish',\n    'view_all' => 'Hammasini ko‘rish',\n    'new' => 'Yangi',\n    'create' => 'Yaratish',\n    'update' => 'Yangilash',\n    'edit' => 'Tahrirlash',\n    'archive' => 'Archive',\n    'unarchive' => 'Un-Archive',\n    'sort' => 'Saralash',\n    'move' => 'Ko‘chirish',\n    'copy' => 'Nusxalash',\n    'reply' => 'Javob berish',\n    'delete' => 'Oʻchirish',\n    'delete_confirm' => 'Oʻchirishni tasdiqlang',\n    'search' => 'Qidirish',\n    'search_clear' => 'Qidiruvni tozalash',\n    'reset' => 'Qayta o‘rnatish',\n    'remove' => 'O‘chirish',\n    'add' => 'Qo‘shish',\n    'configure' => 'Sozlash',\n    'manage' => 'Boshqarish',\n    'fullscreen' => 'To‘liq ekran',\n    'favourite' => 'Sevimli',\n    'unfavourite' => 'Sevimli emas',\n    'next' => 'Keyingisi',\n    'previous' => 'Oldingi',\n    'filter_active' => 'Faol filtr:',\n    'filter_clear' => 'Filtrni tozalash',\n    'download' => 'Yuklab olish',\n    'open_in_tab' => 'Tabda ochish',\n    'open' => 'Open',\n\n    // Sort Options\n    'sort_options' => 'Saralash opsiyalari',\n    'sort_direction_toggle' => 'Saralash yoʻnalishini almashtirish',\n    'sort_ascending' => 'O‘sish bo‘yicha tartiblash',\n    'sort_descending' => 'Kamayish bo‘yicha tartiblash',\n    'sort_name' => 'Nomi',\n    'sort_default' => 'Standart',\n    'sort_created_at' => 'Yaratilgan sana',\n    'sort_updated_at' => 'Yangilangan sana',\n\n    // Misc\n    'deleted_user' => 'O‘chirilgan foydalanuvchi',\n    'no_activity' => 'Ko‘rsatiladigan faollik yo‘q',\n    'no_items' => 'Hech narsa mavjud emas',\n    'back_to_top' => 'Yuqoriga qaytish',\n    'skip_to_main_content' => 'Asosiy tarkibga o‘tish',\n    'toggle_details' => 'Tafsilotlarni almashtirish',\n    'toggle_thumbnails' => 'Eskizlarni almashtirish',\n    'details' => 'Tafsilotlar',\n    'grid_view' => 'To‘r ko‘rinishi',\n    'list_view' => 'Roʻyxat koʻrinishi',\n    'default' => 'Standart',\n    'breadcrumb' => 'Non bo‘laklari',\n    'status' => 'Holat',\n    'status_active' => 'Faol',\n    'status_inactive' => 'Faol emas',\n    'never' => 'Hech qachon',\n    'none' => 'Yo‘q',\n\n    // Header\n    'homepage' => 'Bosh sahifa',\n    'header_menu_expand' => 'Sarlavha menyusini kengaytirish',\n    'profile_menu' => 'Profil menyusi',\n    'view_profile' => 'Profilni ko‘rish',\n    'edit_profile' => 'Profilni tahrirlash',\n    'dark_mode' => 'Qorong‘i rejim',\n    'light_mode' => 'Nur rejimi',\n    'global_search' => 'Global qidiruv',\n\n    // Layout tabs\n    'tab_info' => 'Ma‘lumot',\n    'tab_info_label' => 'Yorliq: Ikkilamchi ma‘lumotni ko‘rsatish',\n    'tab_content' => 'Tarkib',\n    'tab_content_label' => 'Yorliq: Asosiy tarkibni ko‘rsatish',\n\n    // Email Content\n    'email_action_help' => 'Agar siz \":actionText\" tugmasini bosishda muammoga duch kelsangiz, quyidagi URL manzilidan nusxa oling va veb-brauzeringizga joylashtiring:',\n    'email_rights' => 'Barcha huquqlar himoyalangan',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Maxfiylik siyosati',\n    'terms_of_service' => 'Xizmat ko‘rsatish shartlari',\n\n    // OpenSearch\n    'opensearch_description' => 'Search :appName',\n];\n"
  },
  {
    "path": "lang/uz/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Rasmni tanlash',\n    'image_list' => 'Rasmlar roʻyxati',\n    'image_details' => 'Tasvir tafsilotlari',\n    'image_upload' => 'Rasm yuklash',\n    'image_intro' => 'Bu yerda siz avvalroq tizimga yuklangan rasmlarni tanlashingiz va boshqarishingiz mumkin.',\n    'image_intro_upload' => 'Tasvir faylini ushbu oynaga sudrab yoki yuqoridagi \"Rasmni yuklash\" tugmasini bosib yangi rasmni yuklang.',\n    'image_all' => 'Barchasi',\n    'image_all_title' => 'Barcha rasmlarni ko‘rish',\n    'image_book_title' => 'Ush kitobga yuklangan barcha rasmlarni ko‘rish',\n    'image_page_title' => 'Ush sahifaga yuklangan barcha rasmlarni ko‘rish',\n    'image_search_hint' => 'Rasmni nomi bo‘yicha izlash',\n    'image_uploaded' => ':uploadedDate sanada yuklangan',\n    'image_uploaded_by' => ':userName tomonidan yuklangan',\n    'image_uploaded_to' => ':pageLink manziliga yuklangan',\n    'image_updated' => 'Yangilangan: updateDate',\n    'image_load_more' => 'Yana yuklash',\n    'image_image_name' => 'Rasm nomi',\n    'image_delete_used' => 'Ushbu rasm quyidagi sahifalarda qo‘llaniladi.',\n    'image_delete_confirm_text' => 'Haqiqatan ham bu rasmni oʻchirib tashlamoqchimisiz?',\n    'image_select_image' => 'Rasmni tanlash',\n    'image_dropzone' => 'Rasmlarni tortib, tashlang yoki yuklash uchun shu yerni bosing',\n    'image_dropzone_drop' => 'Yuklash uchun rasmlarni shu yerga tashlang',\n    'images_deleted' => 'Tasvirlar oʻchirildi',\n    'image_preview' => 'Tasvirni oldindan ko‘rish',\n    'image_upload_success' => 'Rasm muvaffaqiyatli yuklandi',\n    'image_update_success' => 'Rasm tafsilotlari muvaffaqiyatli yangilandi',\n    'image_delete_success' => 'Rasm muvaffaqiyatli oʻchirildi',\n    'image_replace' => 'Rasmni almashtirish',\n    'image_replace_success' => 'Rasm fayli muvaffaqiyatli yangilandi',\n    'image_rebuild_thumbs' => 'Regenerate Size Variations',\n    'image_rebuild_thumbs_success' => 'Image size variations successfully rebuilt!',\n\n    // Code Editor\n    'code_editor' => 'Kodni tahrirlash',\n    'code_language' => 'Kod tili',\n    'code_content' => 'Kod matni',\n    'code_session_history' => 'Sessiya tarixi',\n    'code_save' => 'Kod saqlanadi',\n];\n"
  },
  {
    "path": "lang/uz/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Umumiy',\n    'advanced' => 'Murakkab',\n    'none' => 'Yo‘q',\n    'cancel' => 'Bekor qilish',\n    'save' => 'Saqlash',\n    'close' => 'Yopish',\n    'apply' => 'Apply',\n    'undo' => 'Bekor qilish',\n    'redo' => 'Qayta qiling',\n    'left' => 'Chapga',\n    'center' => 'Markaz',\n    'right' => 'To‘g‘ri',\n    'top' => 'Yuqori',\n    'middle' => 'O‘rta',\n    'bottom' => 'Pastki',\n    'width' => 'Kengligi',\n    'height' => 'Balandligi',\n    'More' => 'Ko‘proq',\n    'select' => 'Tanlang...',\n\n    // Toolbar\n    'formats' => 'Formatlar',\n    'header_large' => 'Katta sarlavha',\n    'header_medium' => 'O‘rta sarlavha',\n    'header_small' => 'Kichik sarlavha',\n    'header_tiny' => 'Kichkina sarlavha',\n    'paragraph' => 'Paragraf',\n    'blockquote' => 'Blok tirnoq',\n    'inline_code' => 'Inline kod',\n    'callouts' => 'Qo‘ng‘iroqlar',\n    'callout_information' => 'Ma`lumot',\n    'callout_success' => 'Muvaffaqiyat',\n    'callout_warning' => 'Ogohlantirish',\n    'callout_danger' => 'Xavfli',\n    'bold' => 'Qalin',\n    'italic' => 'Kursiv',\n    'underline' => 'tagiga chizish',\n    'strikethrough' => 'Chizilgan',\n    'superscript' => 'Yuqori yozuv',\n    'subscript' => 'Subscript',\n    'text_color' => 'Matn rangi',\n    'highlight_color' => 'Highlight color',\n    'custom_color' => 'Maxsus rang',\n    'remove_color' => 'Rangni olib tashlash',\n    'background_color' => 'Fon rangi',\n    'align_left' => 'Chapga tekislash',\n    'align_center' => 'Markazni tekislaash',\n    'align_right' => 'O‘ngga tekislaash',\n    'align_justify' => 'Ogohlantirish',\n    'list_bullet' => 'Belgilar ro‘yxati',\n    'list_numbered' => 'Raqamlangan ro‘yxat',\n    'list_task' => 'Vazifalar ro‘yxati',\n    'indent_increase' => 'Chiziqni oshirish',\n    'indent_decrease' => 'Chiziqni kamaytirish',\n    'table' => 'Jadval',\n    'insert_image' => 'Rasm kiritish',\n    'insert_image_title' => 'Tasvirni kiritish/tahrirlash',\n    'insert_link' => 'Havolani kiritish/tahrirlash',\n    'insert_link_title' => 'Havola kiritish/tahrirlash',\n    'insert_horizontal_line' => 'Gorizontal chiziqni kiritish',\n    'insert_code_block' => 'Kod blokini kiritish',\n    'edit_code_block' => 'Kod blokini tahrirlash',\n    'insert_drawing' => 'Chizmani kiritish/tahrirlash',\n    'drawing_manager' => 'Chizma menejeri',\n    'insert_media' => 'Mediani kiritish/tahrirlash',\n    'insert_media_title' => 'Media qo‘shish/tahrirlash',\n    'clear_formatting' => 'Formatlashni tozalash',\n    'source_code' => 'Manba kodi',\n    'source_code_title' => 'Manba kodi',\n    'fullscreen' => 'To‘liq ekran',\n    'image_options' => 'Rasm variantlari',\n\n    // Tables\n    'table_properties' => 'Jadval xususiyatlari',\n    'table_properties_title' => 'Jadval xususiyatlari',\n    'delete_table' => 'Jadvalni o‘chirish',\n    'table_clear_formatting' => 'Clear table formatting',\n    'resize_to_contents' => 'Resize to contents',\n    'row_header' => 'Row header',\n    'insert_row_before' => 'Oldinga qator kiritish',\n    'insert_row_after' => 'Keyingi qatorni kiritish',\n    'delete_row' => 'Qatorni o‘chirish',\n    'insert_column_before' => 'Oldinga ustunni kiritish',\n    'insert_column_after' => 'Ustunni keyin kiritish',\n    'delete_column' => 'Ustunni o‘chirish',\n    'table_cell' => 'Katak',\n    'table_row' => 'Qator',\n    'table_column' => 'Ustun',\n    'cell_properties' => 'Katak xossalari',\n    'cell_properties_title' => 'Katak xususiyatlari',\n    'cell_type' => 'Katak turi',\n    'cell_type_cell' => 'Katak',\n    'cell_scope' => 'Qo‘llash doirasi',\n    'cell_type_header' => 'Sarlavha katagi',\n    'merge_cells' => 'Kataklarni birlashtirish',\n    'split_cell' => 'Bo‘lingan katak',\n    'table_row_group' => 'Qator guruhi',\n    'table_column_group' => 'Ustunlar guruhi',\n    'horizontal_align' => 'Gorizontal tekislash',\n    'vertical_align' => 'Vertikal tekislash',\n    'border_width' => 'Chegara kengligi',\n    'border_style' => 'Chegara uslubi',\n    'border_color' => 'Chegara rangi',\n    'row_properties' => 'Qator xususiyatlari',\n    'row_properties_title' => 'Qator xususiyatlari',\n    'cut_row' => 'Qatorni kesib olish',\n    'copy_row' => 'Qatorni nusxalash',\n    'paste_row_before' => 'Oldinga qatorni joylashtirish',\n    'paste_row_after' => 'Keyin qatorni qo‘yish',\n    'row_type' => 'Qator turi',\n    'row_type_header' => 'Sarlavha',\n    'row_type_body' => 'Tana',\n    'row_type_footer' => 'Altbilgi',\n    'alignment' => 'Hizalama',\n    'cut_column' => 'Kesilgan ustun',\n    'copy_column' => 'Ustunni nusxalash',\n    'paste_column_before' => 'Oldinga ustun qo‘yish',\n    'paste_column_after' => 'Ustunni keyin joylashtirish',\n    'cell_padding' => 'Katak to‘plami',\n    'cell_spacing' => 'Katak oralig‘i',\n    'caption' => 'Sarlavha',\n    'show_caption' => 'Sarlavhani ko‘rsatish',\n    'constrain' => 'Proportionlarni cheklash',\n    'cell_border_solid' => 'Qattiq',\n    'cell_border_dotted' => 'Nuqtali',\n    'cell_border_dashed' => 'Chiziqli',\n    'cell_border_double' => 'Ikki marta',\n    'cell_border_groove' => 'Groove',\n    'cell_border_ridge' => 'Ridge',\n    'cell_border_inset' => 'Kiritilgan',\n    'cell_border_outset' => 'Boshlanish',\n    'cell_border_none' => 'Yo‘q',\n    'cell_border_hidden' => 'Yashirin',\n\n    // Images, links, details/summary & embed\n    'source' => 'Manba',\n    'alt_desc' => 'Muqobil tavsif',\n    'embed' => 'Oʻrnatish',\n    'paste_embed' => 'O‘rnatish kodingizni pastga qo‘ying:',\n    'url' => 'URL',\n    'text_to_display' => 'Ko‘rish uchun matn',\n    'title' => 'Sarlavha',\n    'browse_links' => 'Browse links',\n    'open_link' => 'Havolani ochish',\n    'open_link_in' => 'Havolani ochish...',\n    'open_link_current' => 'Joriy oyna',\n    'open_link_new' => 'Yangi oyna',\n    'remove_link' => 'Havolani olib tashlang',\n    'insert_collapsible' => 'Yig‘iladigan blokni joylashtiring',\n    'collapsible_unwrap' => 'Oʻramni yech',\n    'edit_label' => 'Yorliqni tahrirlash',\n    'toggle_open_closed' => 'Ochiq/yopiq o‘tish',\n    'collapsible_edit' => 'Yig‘iladigan blokni tahrirlash',\n    'toggle_label' => 'Yorliqni almashtirish',\n\n    // About view\n    'about' => 'Muharrir haqida',\n    'about_title' => 'WYSIWYG muharriri haqida',\n    'editor_license' => 'Muharrir litsenziyasi va mualliflik huquqi',\n    'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',\n    'editor_lexical_license_link' => 'Full license details can be found here.',\n    'editor_tiny_license' => 'Ushbu muharrir MIT litsenziyasi ostida taqdim etilgan :tinyLink yordamida yaratilgan.',\n    'editor_tiny_license_link' => 'TinyMCE mualliflik huquqi va litsenziya tafsilotlarini bu yerda topishingiz mumkin.',\n    'save_continue' => 'Sahifani saqlang va Davom eting',\n    'callouts_cycle' => '(Turlar bo‘yicha o‘tish uchun bosing)',\n    'link_selector' => 'Kontentga havola',\n    'shortcuts' => 'Qisqa klavishlar',\n    'shortcut' => 'Yorliq',\n    'shortcuts_intro' => 'Tahrirlovchida quyidagi yorliqlar mavjud:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Tavsif',\n];\n"
  },
  {
    "path": "lang/uz/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Oxirgi yaratilgan',\n    'recently_created_pages' => 'Oxirgi yaratilgan sahifalar',\n    'recently_updated_pages' => 'Oxirgi yangilangan sahifalar',\n    'recently_created_chapters' => 'Oxirgi yaratilgan bo\\'limlar',\n    'recently_created_books' => 'Oxirgi yaratilgan kitoblar',\n    'recently_created_shelves' => 'Oxirgi yaratilgan kitobjavonlar',\n    'recently_update' => 'Oxirgi yangilangan',\n    'recently_viewed' => 'Oxirgi koʻrilgan',\n    'recent_activity' => 'Oxirgi faolliklar',\n    'create_now' => 'Yangi yaratish',\n    'revisions' => 'Revizionlar',\n    'meta_revision' => '#:revisionCount reviziya',\n    'meta_created' => ':timeLength da yaratilgan',\n    'meta_created_name' => ':user tomonidan :timeLength da yaratilgan',\n    'meta_updated' => ':timeLength da yangilangan',\n    'meta_updated_name' => ':user tomonidan :timeLength da yangilangan',\n    'meta_owned_name' => 'Muallif: foydalanuvchi',\n    'meta_reference_count' => 'Referenced by :count item|Referenced by :count items',\n    'entity_select' => 'Ob\\'ektni tanlash',\n    'entity_select_lack_permission' => 'Sizda bu elementni tanlash uchun kerakli ruxsatlar yo‘q',\n    'images' => 'Rasmlar',\n    'my_recent_drafts' => 'Mening keyingi qoralamalarim',\n    'my_recently_viewed' => 'Mening keyingi ko‘rganlarim',\n    'my_most_viewed_favourites' => 'Mening eng sevimli sevimliga qo‘shganlarim',\n    'my_favourites' => 'Mening sevimlilarim',\n    'no_pages_viewed' => 'Siz hech qaysi sahifani koʻrmagansiz',\n    'no_pages_recently_created' => 'Siz hali hech qanday sahifa yaratmagansiz',\n    'no_pages_recently_updated' => 'Siz hali sahifalarni yangilamagansiz',\n    'export' => 'Eksport',\n    'export_html' => 'HTML holatida',\n    'export_pdf' => 'PDF holatida',\n    'export_text' => 'Oddiy matn holatida',\n    'export_md' => 'Markdown fayli holatida',\n    'export_zip' => 'Portable ZIP',\n    'default_template' => 'Default Page Template',\n    'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',\n    'default_template_select' => 'Select a template page',\n    'import' => 'Import',\n    'import_validate' => 'Validate Import',\n    'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\\'ll be able to configure & confirm the import in the next view.',\n    'import_zip_select' => 'Select ZIP file to upload',\n    'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',\n    'import_pending' => 'Pending Imports',\n    'import_pending_none' => 'No imports have been started.',\n    'import_continue' => 'Continue Import',\n    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',\n    'import_details' => 'Import Details',\n    'import_run' => 'Run Import',\n    'import_size' => ':size Import ZIP Size',\n    'import_uploaded_at' => 'Uploaded :relativeTime',\n    'import_uploaded_by' => 'Uploaded by',\n    'import_location' => 'Import Location',\n    'import_location_desc' => 'Select a target location for your imported content. You\\'ll need the relevant permissions to create within the location you choose.',\n    'import_delete_confirm' => 'Are you sure you want to delete this import?',\n    'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',\n    'import_errors' => 'Import Errors',\n    'import_errors_desc' => 'The follow errors occurred during the import attempt:',\n    'breadcrumb_siblings_for_page' => 'Navigate siblings for page',\n    'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',\n    'breadcrumb_siblings_for_book' => 'Navigate siblings for book',\n    'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',\n\n    // Permissions and restrictions\n    'permissions' => 'Huquqlar',\n    'permissions_desc' => 'Foydalanuvchi rollari tomonidan berilgan standart ruxsatlarni bekor qilish uchun bu yerda ruxsatlarni o‘rnating.',\n    'permissions_book_cascade' => 'Kitoblarga oʻrnatilgan ruxsatlar, agar ularda oʻz ruxsatnomalari belgilanmagan boʻlsa, avtomatik ravishda bolalar boʻlimlari va sahifalariga oʻtadi.',\n    'permissions_chapter_cascade' => 'Bo\\'limlarda o\\'rnatilgan ruxsatlar, agar ular o\\'zlarining ruxsatlari aniqlanmagan bo\\'lsa, avtomatik ravishda pastki sahifalarga o\\'tadi.',\n    'permissions_save' => 'Ruxsatlarni saqlash',\n    'permissions_owner' => 'Muallifi',\n    'permissions_role_everyone_else' => 'Boshqa hamma',\n    'permissions_role_everyone_else_desc' => 'Maxsus bekor qilinmagan barcha rollar uchun ruxsatlarni o\\'rnating.',\n    'permissions_role_override' => 'Rol uchun ruxsatlarni bekor qilish',\n    'permissions_inherit_defaults' => 'Standartlarni meros qilib olish',\n\n    // Search\n    'search_results' => 'Qidiruv yordam',\n    'search_total_results_found' => ':son natija topildi|:topilgan jami natijalar soni',\n    'search_clear' => 'Qidiruvni tozalash',\n    'search_no_pages' => 'Qidiruvga mos sahifalar topilmadi',\n    'search_for_term' => ':term bo‘yicha haqida',\n    'search_more' => 'Qo‘shimcha ilova',\n    'search_advanced' => 'Kengay olingan',\n    'search_terms' => 'Qidiruv parametrlari',\n    'search_content_type' => 'Kontent turi',\n    'search_exact_matches' => 'Mos kelgan hudud',\n    'search_tags' => 'Teg haqida',\n    'search_options' => 'Opsiyalar',\n    'search_viewed_by_me' => 'Men kuzatuvdan ko‘rilgan',\n    'search_not_viewed_by_me' => 'Men kuzatuvdan ko‘rilmagan',\n    'search_permissions_set' => 'Ruxsatlar oʻrnatilgan',\n    'search_created_by_me' => 'Men tomonidan yaratilgan',\n    'search_updated_by_me' => 'Men tomonidan yangilangan',\n    'search_owned_by_me' => 'Menga tegishli',\n    'search_date_options' => 'Sana opsiyalari',\n    'search_updated_before' => 'Oldin yangilangan',\n    'search_updated_after' => 'Keyin yangilangan',\n    'search_created_before' => 'Oldin yaratilgan',\n    'search_created_after' => 'keyin yaratilgan',\n    'search_set_date' => 'Sana belgilash',\n    'search_update' => 'Qidiruvni yangilash',\n\n    // Shelves\n    'shelf' => 'Raf',\n    'shelves' => 'Tokchalar',\n    'x_shelves' => ':count Shelf|:count Raflar',\n    'shelves_empty' => 'Hech qanday javon yaratilmagan',\n    'shelves_create' => 'Yangi javon yaratish',\n    'shelves_popular' => 'Mashhur javonlar',\n    'shelves_new' => 'Yangi javonlar',\n    'shelves_new_action' => 'Yangi javon',\n    'shelves_popular_empty' => 'Bu erda eng mashhur javonlar paydo bo\\'ladi.',\n    'shelves_new_empty' => 'Eng so\\'nggi yaratilgan javonlar bu erda paydo bo\\'ladi.',\n    'shelves_save' => 'Rafni saqlang',\n    'shelves_books' => 'Bu javonda kitoblar',\n    'shelves_add_books' => 'Ushbu javonga kitob qo\\'shing',\n    'shelves_drag_books' => 'Kitoblarni ushbu javonga qo‘shish uchun ularni pastga torting',\n    'shelves_empty_contents' => 'Bu javonda unga hech qanday kitob ajratilmagan',\n    'shelves_edit_and_assign' => 'Kitoblarni belgilash uchun javonni tahrirlang',\n    'shelves_edit_named' => 'Rafni tahrirlash: nom',\n    'shelves_edit' => 'Rafni tahrirlash',\n    'shelves_delete' => 'Rafni o\\'chirish',\n    'shelves_delete_named' => 'Rafni o\\'chirish: nomi',\n    'shelves_delete_explain' => \"This will delete the shelf with the name ':name'. Contained books will not be deleted.\",\n    'shelves_delete_confirmation' => 'Haqiqatan ham bu javonni oʻchirib tashlamoqchimisiz?',\n    'shelves_permissions' => 'Rafga ruxsatlar',\n    'shelves_permissions_updated' => 'Rafga ruxsatlar yangilandi',\n    'shelves_permissions_active' => 'Rafga ruxsatlar faol',\n    'shelves_permissions_cascade_warning' => 'Javonlardagi ruxsatlar avtomatik ravishda saqlangan kitoblarga o\\'tmaydi. Buning sababi, kitob bir nechta javonlarda mavjud bo\\'lishi mumkin. Ruxsatlarni quyida joylashgan variantdan foydalanib, bolalar kitoblariga nusxalash mumkin.',\n    'shelves_permissions_create' => 'Javon yaratish ruxsatlari faqat quyidagi amal yordamida bolalar kitoblariga ruxsatlarni nusxalash uchun ishlatiladi. Ular kitob yaratish qobiliyatini nazorat qilmaydi.',\n    'shelves_copy_permissions_to_books' => 'Kitoblarga koʻchirish ruxsatnomalari',\n    'shelves_copy_permissions' => 'Nusxa olish uchun ruxsatlar',\n    'shelves_copy_permissions_explain' => 'Bu javonning joriy ruxsat sozlamalarini undagi barcha kitoblarga qo‘llaydi. Faollashtirishdan oldin ushbu javon ruxsatnomalariga kiritilgan har qanday o\\'zgarishlar saqlanganligiga ishonch hosil qiling.',\n    'shelves_copy_permission_success' => 'Raf ruxsatlari :count kitoblariga nusxalandi',\n\n    // Books\n    'book' => 'Kitob',\n    'books' => 'Kitoblar',\n    'x_books' => ':count Book|:count Books',\n    'books_empty' => 'Kitob yaratilmagan',\n    'books_popular' => 'Ommabop kitoblar',\n    'books_recent' => 'Oxirgi kitoblar',\n    'books_new' => 'Yangi kitoblar',\n    'books_new_action' => 'Yangi kitob',\n    'books_popular_empty' => 'Eng ommabop kitoblar shu yerda aks etadi.',\n    'books_new_empty' => 'Eng shaxsiy kitoblar bu yerda aks etadi.',\n    'books_create' => 'Yangi kitob',\n    'books_delete' => 'Kitobni o‘chirish',\n    'books_delete_named' => ':bookName kitobni o‘chirish',\n    'books_delete_explain' => 'Bu \\':bookName\\' nomli kitobni o\\'chirib tashlaydi. Barcha sahifalar va bo\\'limlar o\\'chiriladi.',\n    'books_delete_confirmation' => 'Haqiqatan ham bu kitobni oʻchirib tashlamoqchimisiz?',\n    'books_edit' => 'Kitobni tahrirlash',\n    'books_edit_named' => 'Kitobni tahrirlash: kitob nomi',\n    'books_form_book_name' => 'Kitob nomi',\n    'books_save' => 'Kitobni saqlash',\n    'books_permissions' => 'Kitob ruxsatnomalari',\n    'books_permissions_updated' => 'Kitob ruxsatnomalari yangilandi',\n    'books_empty_contents' => 'Ushbu kitob uchun hech qanday sahifa yoki bob yaratilmagan.',\n    'books_empty_create_page' => 'Yangi sahifa yarating',\n    'books_empty_sort_current_book' => 'Joriy kitobni tartiblang',\n    'books_empty_add_chapter' => 'Bo\\'lim qo\\'shing',\n    'books_permissions_active' => 'Kitob ruxsatlari faol',\n    'books_search_this' => 'Ushbu kitobni qidiring',\n    'books_navigation' => 'Kitob navigatsiya',\n    'books_sort' => 'Kitob tarkibini saralash',\n    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\\'s contents upon changes.',\n    'books_sort_auto_sort' => 'Auto Sort Option',\n    'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',\n    'books_sort_named' => 'Kitobni tartiblash: kitob nomi',\n    'books_sort_name' => 'Nomi bo\\'yicha saralash',\n    'books_sort_created' => 'Yaratilgan sana bo\\'yicha saralash',\n    'books_sort_updated' => 'Yangilangan sana bo\\'yicha saralash',\n    'books_sort_chapters_first' => 'Birinchi bo\\'limlar',\n    'books_sort_chapters_last' => 'Oxirgi bo\\'limlar',\n    'books_sort_show_other' => 'Boshqa kitoblarni ko\\'rsatish',\n    'books_sort_save' => 'Yangi buyurtmani saqlash',\n    'books_sort_show_other_desc' => 'Boshqa kitoblarni saralash jarayoniga qo\\'shish uchun bu yerga qo\\'shing va kitoblar o\\'rtasida osongina qayta tashkil etishga ruxsat bering.',\n    'books_sort_move_up' => 'Yuqoriga harakatlanmoq',\n    'books_sort_move_down' => 'Pastga siljiting',\n    'books_sort_move_prev_book' => 'Oldingi kitobga o\\'tish',\n    'books_sort_move_next_book' => 'Keyingi kitobga o\\'ting',\n    'books_sort_move_prev_chapter' => 'Oldingi bo\\'limga o\\'tish',\n    'books_sort_move_next_chapter' => 'Keyingi bo\\'limga o\\'tish',\n    'books_sort_move_book_start' => 'Kitobning boshiga o\\'ting',\n    'books_sort_move_book_end' => 'Kitobning oxiriga o\\'ting',\n    'books_sort_move_before_chapter' => 'Oldingi bo\\'limga o\\'ting',\n    'books_sort_move_after_chapter' => 'Keyingi bo\\'limga o\\'ting',\n    'books_copy' => 'Kitobni nusxalash',\n    'books_copy_success' => 'Kitob muvaffaqiyatli nusxalandi',\n\n    // Chapters\n    'chapter' => 'Bob',\n    'chapters' => 'Boblar',\n    'x_chapters' => ':count bo\\'lim|:boblarni hisoblash',\n    'chapters_popular' => 'Mashhur bo\\'limlar',\n    'chapters_new' => 'Yangi bo\\'lim',\n    'chapters_create' => 'Yangi bo\\'lim yaratish',\n    'chapters_delete' => 'Bo\\'limni o\\'chirish',\n    'chapters_delete_named' => 'Bo\\'limni o\\'chirish: bo\\'limName',\n    'chapters_delete_explain' => 'Bu \\':chapterName\\' nomli bo\\'limni o\\'chiradi. Ushbu bo\\'limda mavjud bo\\'lgan barcha sahifalar ham o\\'chiriladi.',\n    'chapters_delete_confirm' => 'Haqiqatan ham bu bobni oʻchirib tashlamoqchimisiz?',\n    'chapters_edit' => 'Bo\\'limni tahrirlash',\n    'chapters_edit_named' => 'Bobni tahrirlang: bo\\'lim nomi',\n    'chapters_save' => 'Bo\\'limni saqlash',\n    'chapters_move' => 'Bo\\'limni ko\\'chirish',\n    'chapters_move_named' => 'Bo\\'limni ko\\'chiring: bo\\'lim nomi',\n    'chapters_copy' => 'Bobni nusxalash',\n    'chapters_copy_success' => 'Bob muvaffaqiyatli nusxalandi',\n    'chapters_permissions' => 'Bo\\'limga ruxsatlar',\n    'chapters_empty' => 'Hozirda bu bobda hech qanday sahifa yoʻq.',\n    'chapters_permissions_active' => 'Bo\\'lim ruxsatlari faol',\n    'chapters_permissions_success' => 'Bo\\'lim ruxsatlari yangilandi',\n    'chapters_search_this' => 'Ushbu bo\\'limni qidiring',\n    'chapter_sort_book' => 'Tartiblash kitobi',\n\n    // Pages\n    'page' => 'Sahifa',\n    'pages' => 'Sahifalar',\n    'x_pages' => ':count Page|:Sahifalarni hisoblash',\n    'pages_popular' => 'Mashhur sahifalar',\n    'pages_new' => 'Yangi sahifa',\n    'pages_attachments' => 'Qo\\'shimchalar',\n    'pages_navigation' => 'Sahifa navigatsiyasi',\n    'pages_delete' => 'Sahifani o\\'chirish',\n    'pages_delete_named' => 'Sahifani o\\'chirish :pageName',\n    'pages_delete_draft_named' => 'Qoralama sahifani oʻchirish: pageName',\n    'pages_delete_draft' => 'Qoralama sahifani oʻchirish',\n    'pages_delete_success' => 'Sahifa oʻchirildi',\n    'pages_delete_draft_success' => 'Qoralama sahifa oʻchirildi',\n    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',\n    'pages_delete_confirm' => 'Haqiqatan ham bu sahifani oʻchirib tashlamoqchimisiz?',\n    'pages_delete_draft_confirm' => 'Haqiqatan ham bu qoralama sahifani oʻchirib tashlamoqchimisiz?',\n    'pages_editing_named' => 'Sahifani tahrirlash :pageName',\n    'pages_edit_draft_options' => 'Qoralama variantlari',\n    'pages_edit_save_draft' => 'Qoralamani saqlash',\n    'pages_edit_draft' => 'Sahifa qoralamasini tahrirlash',\n    'pages_editing_draft' => 'Qoralamani tahrirlash',\n    'pages_editing_page' => 'Tahrirlash sahifasi',\n    'pages_edit_draft_save_at' => 'Qoralama saqlangan',\n    'pages_edit_delete_draft' => 'Qoralamani oʻchirish',\n    'pages_edit_delete_draft_confirm' => 'Haqiqatan ham qoralama sahifadagi oʻzgarishlarni oʻchirib tashlamoqchimisiz? Oxirgi toʻliq saqlashdan buyon barcha oʻzgarishlaringiz yoʻqoladi va muharrir soʻnggi sahifaning qoralama saqlanmagan holati bilan yangilanadi.',\n    'pages_edit_discard_draft' => 'Qoralamani bekor qilish',\n    'pages_edit_switch_to_markdown' => 'Markdown muharririga o\\'ting',\n    'pages_edit_switch_to_markdown_clean' => '(Toza tarkib)',\n    'pages_edit_switch_to_markdown_stable' => '(Barqaror tarkib)',\n    'pages_edit_switch_to_wysiwyg' => 'WYSIWYG muharririga o\\'ting',\n    'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',\n    'pages_edit_set_changelog' => 'O\\'zgarishlar jurnalini o\\'rnating',\n    'pages_edit_enter_changelog_desc' => 'Siz kiritgan o\\'zgarishlarning qisqacha tavsifini kiriting',\n    'pages_edit_enter_changelog' => 'O\\'zgarishlar jurnaliga kiring',\n    'pages_editor_switch_title' => 'Muharrirni almashtirish',\n    'pages_editor_switch_are_you_sure' => 'Haqiqatan ham bu sahifa muharririni oʻzgartirmoqchimisiz?',\n    'pages_editor_switch_consider_following' => 'Tahrirlovchilarni o\\'zgartirishda quyidagilarga e\\'tibor bering:',\n    'pages_editor_switch_consideration_a' => 'Saqlangandan so\\'ng, yangi tahrirlovchi opsiyasidan kelajakdagi muharrirlar, jumladan, tahrirlovchi turini o\\'zlari o\\'zgartira olmaydiganlar ham foydalanadi.',\n    'pages_editor_switch_consideration_b' => 'Bu ma\\'lum holatlarda tafsilotlar va sintaksisning yo\\'qolishiga olib kelishi mumkin.',\n    'pages_editor_switch_consideration_c' => 'Oxirgi saqlashdan keyin kiritilgan teg yoki oʻzgarishlar jurnali oʻzgarishlari bu oʻzgarish davomida saqlanib qolmaydi.',\n    'pages_save' => 'Sahifani saqlash',\n    'pages_title' => 'Sahifa sarlavhasi',\n    'pages_name' => 'Sahifa nomi',\n    'pages_md_editor' => 'muharrir',\n    'pages_md_preview' => 'Ko‘rib chiqish',\n    'pages_md_insert_image' => 'Rasm kiritish',\n    'pages_md_insert_link' => 'Ob\\'ekt havolasini kiriting',\n    'pages_md_insert_drawing' => 'Chizma kiritish',\n    'pages_md_show_preview' => 'Ko‘rish',\n    'pages_md_sync_scroll' => 'Sinxronizatsiyani oldindan ko\\'rish aylantirish',\n    'pages_md_plain_editor' => 'Plaintext editor',\n    'pages_drawing_unsaved' => 'Saqlanmagan chizma topildi',\n    'pages_drawing_unsaved_confirm' => 'Saqlanmagan chizma maʼlumotlari avvalgi muvaffaqiyatsiz chizmani saqlash urinishidan topildi. Ushbu saqlanmagan chizmani qayta tiklash va tahrirlashni davom ettirmoqchimisiz?',\n    'pages_not_in_chapter' => 'Sahifa bir bobda emas',\n    'pages_move' => 'Sahifani ko\\'chirish',\n    'pages_copy' => 'Sahifani nusxalash',\n    'pages_copy_desination' => 'Belgilangan joydan nusxa oling',\n    'pages_copy_success' => 'Sahifa muvaffaqiyatli nusxalandi',\n    'pages_permissions' => 'Sahifa ruxsatnomalari',\n    'pages_permissions_success' => 'Sahifa ruxsatnomalari yangilandi',\n    'pages_revision' => 'Qayta ko\\'rib chiqish',\n    'pages_revisions' => 'Sahifani tahrirlash',\n    'pages_revisions_desc' => 'Quyida ushbu sahifaning barcha o\\'tgan tahrirlari keltirilgan. Ruxsatlar ruxsat bersa, eski sahifa versiyalarini qayta ko\\'rib chiqishingiz, solishtirishingiz va tiklashingiz mumkin. Sahifaning toʻliq tarixi bu yerda toʻliq aks ettirilmasligi mumkin, chunki tizim konfiguratsiyasiga qarab, eski tahrirlar avtomatik ravishda oʻchirilishi mumkin.',\n    'pages_revisions_named' => ':pageName uchun sahifa tahrirlari',\n    'pages_revision_named' => ':pageName uchun sahifa tahriri',\n    'pages_revision_restored_from' => '#:id dan tiklangan; : xulosa',\n    'pages_revisions_created_by' => 'Tomonidan yaratilgan',\n    'pages_revisions_date' => 'Tekshirish sanasi',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Tahrir raqami',\n    'pages_revisions_numbered' => 'Tahrir #: identifikator',\n    'pages_revisions_numbered_changes' => 'Tahrir #: id o\\'zgarishlari',\n    'pages_revisions_editor' => 'Muharrir turi',\n    'pages_revisions_changelog' => 'O\\'zgarishlar jurnali',\n    'pages_revisions_changes' => 'O\\'zgarishlar',\n    'pages_revisions_current' => 'Joriy versiya',\n    'pages_revisions_preview' => 'Ko‘rib chiqish',\n    'pages_revisions_restore' => 'Qayta tiklash',\n    'pages_revisions_none' => 'Bu sahifada hech qanday tahrir yo\\'q',\n    'pages_copy_link' => 'Havolani nusxalash',\n    'pages_edit_content_link' => 'Tahrirlovchida bo\\'limga o\\'tish',\n    'pages_pointer_enter_mode' => 'Bo\\'limni tanlash rejimiga kiring',\n    'pages_pointer_label' => 'Sahifa bo\\'limi parametrlari',\n    'pages_pointer_permalink' => 'Sahifa bo\\'limi doimiy havola',\n    'pages_pointer_include_tag' => 'Sahifa bo\\'limi tegni o\\'z ichiga oladi',\n    'pages_pointer_toggle_link' => 'Doimiy havola rejimi, Oʻz ichiga tegni koʻrsatish uchun bosing',\n    'pages_pointer_toggle_include' => 'Teg rejimini qo\\'shish, doimiy havolani ko\\'rsatish uchun bosing',\n    'pages_permissions_active' => 'Sahifa ruxsatlari faol',\n    'pages_initial_revision' => 'Dastlabki nashr',\n    'pages_references_update_revision' => 'Tizim ichki havolalarni avtomatik yangilash',\n    'pages_initial_name' => 'Yangi sahifa',\n    'pages_editing_draft_notification' => 'Siz hozirda oxirgi saqlangan qoralamani tahrir qilyapsiz :timeDiff.',\n    'pages_draft_edited_notification' => 'Ushbu sahifa o\\'sha paytdan beri yangilangan. Ushbu qoralamani bekor qilish tavsiya etiladi.',\n    'pages_draft_page_changed_since_creation' => 'Bu qoralama yaratilganidan beri bu sahifa yangilandi. Ushbu qoralamadan voz kechishingiz yoki sahifadagi o\\'zgarishlarni qayta yozmaslikka harakat qilishingiz tavsiya etiladi.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count foydalanuvchilari ushbu sahifani tahrirlashni boshladilar',\n        'start_b' => ':userName bu sahifani tahrirlashni boshladi',\n        'time_a' => 'sahifa oxirgi yangilanganidan beri',\n        'time_b' => 'oxirgi :minCount daqiqada',\n        'message' => ':Boshlanish vaqti. Bir-biringizning yangilanishlarini qayta yozmaslikka ehtiyot bo\\'ling!',\n    ],\n    'pages_draft_discarded' => 'Qoralama bekor qilindi! Tahrirlovchi joriy sahifa mazmuni bilan yangilandi',\n    'pages_draft_deleted' => 'Qoralama oʻchirildi! Tahrirlovchi joriy sahifa mazmuni bilan yangilandi',\n    'pages_specific' => 'Maxsus sahifa',\n    'pages_is_template' => 'Sahifa shabloni',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Toggle Sidebar',\n    'page_tags' => 'Sahifa teglari',\n    'chapter_tags' => 'Bo\\'lim teglari',\n    'book_tags' => 'Kitob teglari',\n    'shelf_tags' => 'Raf teglari',\n    'tag' => 'teg',\n    'tags' =>  'Tags',\n    'tags_index_desc' => 'Teglar turkumlashning moslashuvchan shaklini qo\\'llash uchun tizim ichidagi tarkibga qo\\'llanilishi mumkin. Teglar ham kalitga, ham qiymatga ega bo\\'lishi mumkin, qiymat ixtiyoriydir. Qo\\'llanilgandan so\\'ng, kontent teg nomi va qiymatidan foydalanib so\\'ralishi mumkin.',\n    'tag_name' =>  'Tag Name',\n    'tag_value' => 'Teg qiymati (ixtiyoriy)',\n    'tags_explain' => \"Add some tags to better categorise your content. \\n You can assign a value to a tag for more in-depth organisation.\",\n    'tags_add' => 'Boshqa teg qo\\'shing',\n    'tags_remove' => 'Ushbu tegni olib tashlang',\n    'tags_usages' => 'Jami teg foydalanish',\n    'tags_assigned_pages' => 'Sahifalarga tayinlangan',\n    'tags_assigned_chapters' => 'Bo\\'limlarga tayinlangan',\n    'tags_assigned_books' => 'Kitoblarga tayinlangan',\n    'tags_assigned_shelves' => 'Raflarga tayinlangan',\n    'tags_x_unique_values' => ': noyob qiymatlarni hisoblash',\n    'tags_all_values' => 'Barcha qadriyatlar',\n    'tags_view_tags' => 'Teglarni ko\\'rish',\n    'tags_view_existing_tags' => 'Mavjud teglarni ko\\'rish',\n    'tags_list_empty_hint' => 'Teglar sahifa muharririning yon paneli orqali yoki kitob, bob yoki javon tafsilotlarini tahrirlashda tayinlanishi mumkin.',\n    'attachments' => 'Qo\\'shimchalar',\n    'attachments_explain' => 'Ba\\'zi fayllarni yuklang yoki sahifangizda ko\\'rsatish uchun havolalarni qo\\'shing. Ular sahifaning yon panelida ko\\'rinadi.',\n    'attachments_explain_instant_save' => 'Bu yerdagi o\\'zgarishlar bir zumda saqlanadi.',\n    'attachments_upload' => 'Faylni yuklash',\n    'attachments_link' => 'Havolani biriktiring',\n    'attachments_upload_drop' => 'Shu bilan bir qatorda, faylni ilova sifatida yuklash uchun bu yerga sudrab olib tashlashingiz mumkin.',\n    'attachments_set_link' => 'Bog\\'lanishni o\\'rnatish',\n    'attachments_delete' => 'Haqiqatan ham bu biriktirmani oʻchirib tashlamoqchimisiz?',\n    'attachments_dropzone' => 'Yuklash uchun fayllarni bu yerga tashlang',\n    'attachments_no_files' => 'Hech qanday fayl yuklanmagan',\n    'attachments_explain_link' => 'Agar fayl yuklamaslikni xohlasangiz, havolani biriktirishingiz mumkin. Bu boshqa sahifaga havola yoki bulutdagi faylga havola bo\\'lishi mumkin.',\n    'attachments_link_name' => 'Havola nomi',\n    'attachment_link' => 'Qo\\'shimcha havola',\n    'attachments_link_url' => 'Faylga havola',\n    'attachments_link_url_hint' => 'Sayt yoki faylning URL manzili',\n    'attach' => 'Biriktiring',\n    'attachments_insert_link' => 'Sahifaga havola qo\\'shing',\n    'attachments_edit_file' => 'Faylni tahrirlash',\n    'attachments_edit_file_name' => 'Fayl nomi',\n    'attachments_edit_drop_upload' => 'Fayllarni tashlab yuboring yoki yuklash va ustiga yozish uchun shu yerni bosing',\n    'attachments_order_updated' => 'Biriktirish tartibi yangilandi',\n    'attachments_updated_success' => 'Birikma tafsilotlari yangilandi',\n    'attachments_deleted' => 'Biriktirma oʻchirildi',\n    'attachments_file_uploaded' => 'Fayl muvaffaqiyatli yuklandi',\n    'attachments_file_updated' => 'Fayl muvaffaqiyatli yangilandi',\n    'attachments_link_attached' => 'Havola sahifaga muvaffaqiyatli biriktirildi',\n    'templates' => 'Shablonlar',\n    'templates_set_as_template' => 'Sahifa shablondir',\n    'templates_explain_set_as_template' => 'Siz ushbu sahifani shablon sifatida sozlashingiz mumkin, shunda uning mazmuni boshqa sahifalarni yaratishda foydalaniladi. Boshqa foydalanuvchilar ushbu sahifani koʻrish ruxsatiga ega boʻlsa, ushbu andozadan foydalanishlari mumkin.',\n    'templates_replace_content' => 'Sahifa tarkibini almashtiring',\n    'templates_append_content' => 'Sahifa tarkibiga qo\\'shing',\n    'templates_prepend_content' => 'Sahifa mazmuniga oldin qo\\'ying',\n\n    // Profile View\n    'profile_user_for_x' => 'Foydalanuvchi uchun: time',\n    'profile_created_content' => 'Yaratilgan tarkib',\n    'profile_not_created_pages' => ':userName hech qanday sahifa yaratmagan',\n    'profile_not_created_chapters' => ':userName hech qanday bob yaratmagan',\n    'profile_not_created_books' => ':userName hech qanday kitob yaratmagan',\n    'profile_not_created_shelves' => ':userName hech qanday javon yaratmagan',\n\n    // Comments\n    'comment' => 'Izoh',\n    'comments' => 'Izohlar',\n    'comment_add' => 'Fikr qo\\'shish',\n    'comment_none' => 'No comments to display',\n    'comment_placeholder' => 'Bu yerda fikr qoldiring',\n    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',\n    'comment_archived_count' => ':count Archived',\n    'comment_archived_threads' => 'Archived Threads',\n    'comment_save' => 'Fikrni saqlash',\n    'comment_new' => 'Yangi izoh',\n    'comment_created' => 'izoh berdi:createDiff',\n    'comment_updated' => 'Yangilangan :updateDiff tomonidan :username',\n    'comment_updated_indicator' => 'Yangilangan',\n    'comment_deleted_success' => 'Fikr o‘chirildi',\n    'comment_created_success' => 'Fikr qo\\'shildi',\n    'comment_updated_success' => 'Fikr yangilandi',\n    'comment_archive_success' => 'Comment archived',\n    'comment_unarchive_success' => 'Comment un-archived',\n    'comment_view' => 'View comment',\n    'comment_jump_to_thread' => 'Jump to thread',\n    'comment_delete_confirm' => 'Haqiqatan ham bu fikrni oʻchirib tashlamoqchimisiz?',\n    'comment_in_reply_to' => ':commentId ga javoban',\n    'comment_reference' => 'Reference',\n    'comment_reference_outdated' => '(Outdated)',\n    'comment_editor_explain' => 'Mana shu sahifada qolgan izohlar. Saqlangan sahifani ko\\'rishda sharhlar qo\\'shilishi va boshqarilishi mumkin.',\n\n    // Revision\n    'revision_delete_confirm' => 'Haqiqatan ham bu tahrirni oʻchirib tashlamoqchimisiz?',\n    'revision_restore_confirm' => 'Haqiqatan ham bu tahrirni qayta tiklamoqchimisiz? Joriy sahifa mazmuni almashtiriladi.',\n    'revision_cannot_delete_latest' => 'Oxirgi versiyani oʻchirib boʻlmadi.',\n\n    // Copy view\n    'copy_consider' => 'Kontentni nusxalashda quyidagini hisobga oling.',\n    'copy_consider_permissions' => 'Maxsus ruxsat sozlamalari nusxalanmaydi.',\n    'copy_consider_owner' => 'Siz barcha nusxalangan kontent egasiga aylanasiz.',\n    'copy_consider_images' => 'Sahifa tasvirlari fayllari takrorlanmaydi va asl tasvirlar dastlab yuklangan sahifaga aloqasini saqlab qoladi.',\n    'copy_consider_attachments' => 'Sahifa qo\\'shimchalari nusxalanmaydi.',\n    'copy_consider_access' => 'Joylashuv, egasi yoki ruxsatlarning oʻzgarishi ushbu kontentga avval ruxsati boʻlmaganlar uchun ochiq boʻlishiga olib kelishi mumkin.',\n\n    // Conversions\n    'convert_to_shelf' => 'Rafga aylantirish',\n    'convert_to_shelf_contents_desc' => 'Siz ushbu kitobni bir xil tarkibga ega yangi javonga aylantirishingiz mumkin. Ushbu kitobdagi boblar yangi kitoblarga aylantiriladi. Agar bu kitobda bobda bo\\'lmagan sahifalar bo\\'lsa, bu kitob nomi o\\'zgartiriladi va shunday sahifalarni o\\'z ichiga oladi va bu kitob yangi javonning bir qismiga aylanadi.',\n    'convert_to_shelf_permissions_desc' => 'Bu kitobga oʻrnatilgan har qanday ruxsatlar yangi javonga va oʻz ruxsatnomalariga ega boʻlmagan barcha yangi bolalar kitoblariga koʻchiriladi. Esda tutingki, javonlardagi ruxsatlar kitoblar uchun bo\\'lgani kabi ichki kontentga avtomatik ravishda kaskad bo\\'lmaydi.',\n    'convert_book' => 'Kitobni aylantirish',\n    'convert_book_confirm' => 'Haqiqatan ham bu kitobni aylantirmoqchimisiz?',\n    'convert_undo_warning' => 'Buni osonlikcha qaytarib bo\\'lmaydi.',\n    'convert_to_book' => 'Kitobga aylantirish',\n    'convert_to_book_desc' => 'Siz ushbu bobni xuddi shu mazmundagi yangi kitobga aylantirishingiz mumkin. Ushbu bobda oʻrnatilgan har qanday ruxsatnomalar yangi kitobga koʻchiriladi, lekin ota-ona kitobidan meros qilib olingan ruxsatlar koʻchirilmaydi, bu esa kirishni boshqarishni oʻzgartirishga olib kelishi mumkin.',\n    'convert_chapter' => 'Bo\\'limni aylantirish',\n    'convert_chapter_confirm' => 'Haqiqatan ham bu bobni aylantirmoqchimisiz?',\n\n    // References\n    'references' => 'Ma\\'lumotnomalar',\n    'references_none' => 'Bu elementga kuzatilgan havolalar mavjud emas.',\n    'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',\n\n    // Watch Options\n    'watch' => 'Tomosha qiling',\n    'watch_title_default' => 'Standart sozlamalar',\n    'watch_desc_default' => 'Tomosha qilishni faqat birlamchi bildirishnoma sozlamalaringizga qaytaring.',\n    'watch_title_ignore' => 'E\\'tibor bermaslik',\n    'watch_desc_ignore' => 'Barcha bildirishnomalarga, jumladan, foydalanuvchi darajasidagi sozlamalarga e\\'tibor bermang.',\n    'watch_title_new' => 'Yangi sahifalar',\n    'watch_desc_new' => 'Ushbu element ichida har qanday yangi sahifa yaratilganda xabar bering.',\n    'watch_title_updates' => 'Barcha sahifa yangilanishlari',\n    'watch_desc_updates' => 'Barcha yangi sahifalar va sahifa o\\'zgarishlari haqida xabar bering.',\n    'watch_desc_updates_page' => 'Barcha sahifa o\\'zgarishlari haqida xabar bering.',\n    'watch_title_comments' => 'Barcha sahifa yangilanishlari va sharhlar',\n    'watch_desc_comments' => 'Barcha yangi sahifalar, sahifa o\\'zgarishlari va yangi sharhlar haqida xabar bering.',\n    'watch_desc_comments_page' => 'Sahifadagi o\\'zgarishlar va yangi sharhlar haqida xabar bering.',\n    'watch_change_default' => 'Standart bildirishnoma sozlamalarini o\\'zgartiring',\n    'watch_detail_ignore' => 'Bildirishnomalarga e\\'tibor bermaslik',\n    'watch_detail_new' => 'Yangi sahifalarni tomosha qilish',\n    'watch_detail_updates' => 'Yangi sahifalar va yangilanishlarni tomosha qilish',\n    'watch_detail_comments' => 'Yangi sahifalar, yangilanishlar va sharhlarni tomosha qilish',\n    'watch_detail_parent_book' => 'Ota-onalar kitobi orqali tomosha qilish',\n    'watch_detail_parent_book_ignore' => 'Ota-ona kitobi orqali e\\'tiborsizlik',\n    'watch_detail_parent_chapter' => 'Ota-onalar bo\\'limi orqali tomosha qilish',\n    'watch_detail_parent_chapter_ignore' => 'Ota-bob orqali e\\'tiborsizlik',\n];\n"
  },
  {
    "path": "lang/uz/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Sizda soʻralgan sahifaga kirish ruxsatingiz yoʻq.',\n    'permissionJson' => 'Sizda soʻralgan amalni bajarish uchun ruxsat yoʻq.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'E-pochta manzili boʻlgan foydalanuvchi allaqachon mavjud, ammo hisob ma\\'lumotlari boshqacha.',\n    'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',\n    'email_already_confirmed' => 'Elektron pochta allaqachon tasdiqlangan, tizimga kiring.',\n    'email_confirmation_invalid' => 'Bu tasdiqlovchi token yaroqsiz yoki allaqachon ishlatilgan. Iltimos, qayta roʻyxatdan oʻtishga urinib koʻring.',\n    'email_confirmation_expired' => 'Tasdiqlash belgisi muddati tugadi, yangi tasdiqlovchi elektron pochta xabari yuborildi.',\n    'email_confirmation_awaiting' => 'Amaldagi hisob uchun elektron pochta manzili tasdiqlanishi kerak',\n    'ldap_fail_anonymous' => 'Anonim ulanishdan foydalanib, LDAP ruxsati amalga oshmadi',\n    'ldap_fail_authed' => 'Berilgan dn va parol maʼlumotlari yordamida LDAP kirish amalga oshmadi',\n    'ldap_extension_not_installed' => 'LDAP PHP kengaytmasi oʻrnatilmagan',\n    'ldap_cannot_connect' => 'Ldap serveriga ulanib boʻlmadi, Dastlabki ulanish amalga oshmadi',\n    'saml_already_logged_in' => 'Allaqachon tizimga kirgan',\n    'saml_no_email_address' => 'Tashqi autentifikatsiya tizimi tomonidan taqdim etilgan maʼlumotlarda ushbu foydalanuvchi uchun elektron pochta manzili topilmadi',\n    'saml_invalid_response_id' => 'Tashqi autentifikatsiya tizimidagi so‘rov ushbu ilova tomonidan boshlangan jarayon tomonidan tan olinmaydi. Kirishdan keyin orqaga qaytish bu muammoga olib kelishi mumkin.',\n    'saml_fail_authed' => ':tizim yordamida tizimga kirish amalga oshmadi, tizim muvaffaqiyatli avtorizatsiyani taqdim etmadi',\n    'oidc_already_logged_in' => 'Allaqachon tizimga kirgan',\n    'oidc_no_email_address' => 'Tashqi autentifikatsiya tizimi tomonidan taqdim etilgan maʼlumotlarda ushbu foydalanuvchi uchun elektron pochta manzili topilmadi',\n    'oidc_fail_authed' => ':tizim yordamida tizimga kirish amalga oshmadi, tizim muvaffaqiyatli avtorizatsiyani taqdim etmadi',\n    'social_no_action_defined' => 'Hech qanday harakat belgilanmagan',\n    'social_login_bad_response' => \":socialAccount login paytida xatolik qabul qilindi: \\n:xato\",\n    'social_account_in_use' => 'Bu :socialAccount hisobi allaqachon ishlatilmoqda, :socialAccount opsiyasi orqali tizimga kiring.',\n    'social_account_email_in_use' => 'Elektron pochta: elektron pochta allaqachon ishlatilmoqda. Agar sizda allaqachon hisob qaydnomangiz boʻlsa, profil sozlamalaringizdan :socialAccount hisobingizni ulashingiz mumkin.',\n    'social_account_existing' => 'Bu :socialAccount allaqachon profilingizga biriktirilgan.',\n    'social_account_already_used_existing' => 'Bu :socialAccount hisobi allaqachon boshqa foydalanuvchi tomonidan foydalanilgan.',\n    'social_account_not_used' => 'Bu :socialAccount hisobi hech qanday foydalanuvchi bilan bog‘lanmagan. Iltimos, uni profil sozlamalaringizga biriktiring.',\n    'social_account_register_instructions' => 'Agar sizda hali hisob qaydnomangiz boʻlmasa, :socialAccount opsiyasidan foydalanib hisob qaydnomangizni roʻyxatdan oʻtkazishingiz mumkin.',\n    'social_driver_not_found' => 'Ijtimoiy haydovchi topilmadi',\n    'social_driver_not_configured' => 'Sizning :socialAccount ijtimoiy sozlamalaringiz toʻgʻri sozlanmagan.',\n    'invite_token_expired' => 'Bu taklif havolasi muddati tugagan. Buning oʻrniga hisobingiz parolini tiklashga urinib koʻrishingiz mumkin.',\n    'login_user_not_found' => 'A user for this action could not be found.',\n\n    // System\n    'path_not_writable' => 'Fayl yoʻli :filePath faylini yuklab boʻlmadi. Uning serverga yozilishi mumkinligiga ishonch hosil qiling.',\n    'cannot_get_image_from_url' => ':url dan rasmni olib boʻlmadi',\n    'cannot_create_thumbs' => 'Server eskiz yarata olmaydi. GD PHP kengaytmasi oʻrnatilganligini tekshiring.',\n    'server_upload_limit' => 'Server bunday hajmdagi yuklashga ruxsat bermaydi. Kichikroq fayl hajmini sinab koʻring.',\n    'server_post_limit' => 'The server cannot receive the provided amount of data. Try again with less data or a smaller file.',\n    'uploaded'  => 'Server bunday hajmdagi yuklashga ruxsat bermaydi. Kichikroq fayl hajmini sinab koʻring.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Rasmni yuklashda xatolik yuz berdi',\n    'image_upload_type_error' => 'Yuklanayotgan rasm turi yaroqsiz',\n    'image_upload_replace_type' => 'Tasvir faylini almashtirish bir xil turdagi boʻlishi kerak',\n    'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.',\n    'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.',\n    'image_gallery_thumbnail_memory_limit' => 'Failed to create gallery thumbnails due to system resource limits.',\n    'drawing_data_not_found' => 'Chizma maʼlumotlarini yuklab boʻlmadi. Chizma fayli endi mavjud boʻlmasligi yoki unga kirishga ruxsatingiz boʻlmasligi mumkin.',\n\n    // Attachments\n    'attachment_not_found' => 'Biriktirma topilmadi',\n    'attachment_upload_error' => 'Biriktirilgan faylni yuklashda xatolik yuz berdi',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Qoralama saqlanmadi. Ushbu sahifani saqlashdan oldin internet aloqangiz borligiga ishonch hosil qiling',\n    'page_draft_delete_fail' => 'Sahifaning qoralamasini o‘chirib bo‘lmadi va joriy sahifada saqlangan kontentni olib bo‘lmadi',\n    'page_custom_home_deletion' => 'Bosh sahifa sifatida belgilangan sahifani oʻchirib boʻlmaydi',\n\n    // Entities\n    'entity_not_found' => 'Ob\\'ekt topilmadi',\n    'bookshelf_not_found' => 'Raf topilmadi',\n    'book_not_found' => 'Kitob topilmadi',\n    'page_not_found' => 'sahifa topilmadi',\n    'chapter_not_found' => 'Boʻlim topilmadi',\n    'selected_book_not_found' => 'Tanlangan kitob topilmadi',\n    'selected_book_chapter_not_found' => 'Tanlangan kitob yoki bob topilmadi',\n    'guests_cannot_save_drafts' => 'Mehmonlar qoralamalarni saqlay olmaydi',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Siz yagona administratorni oʻchira olmaysiz',\n    'users_cannot_delete_guest' => 'Siz mehmon foydalanuvchini oʻchira olmaysiz',\n    'users_could_not_send_invite' => 'Could not create user since invite email failed to send',\n\n    // Roles\n    'role_cannot_be_edited' => 'Bu rolni tahrirlab bo‘lmaydi',\n    'role_system_cannot_be_deleted' => 'Bu rol tizim rolidir va uni oʻchirib boʻlmaydi',\n    'role_registration_default_cannot_delete' => 'Standart ro‘yxatga olish roli sifatida belgilangan bo‘lsa, bu rolni o‘chirib bo‘lmaydi',\n    'role_cannot_remove_only_admin' => 'Bu foydalanuvchi administrator roliga tayinlangan yagona foydalanuvchi hisoblanadi. Administrator rolini bu yerda olib tashlashdan oldin boshqa foydalanuvchiga tayinlang.',\n\n    // Comments\n    'comment_list' => 'Sharhlarni olishda xatolik yuz berdi.',\n    'cannot_add_comment_to_draft' => 'Siz qoralamaga izoh qo‘sha olmaysiz.',\n    'comment_add' => 'Sharh qo‘shish/yangilashda xatolik yuz berdi.',\n    'comment_delete' => 'Fikrni o‘chirishda xatolik yuz berdi.',\n    'empty_comment' => 'Boʻsh fikr qoʻshib boʻlmaydi.',\n\n    // Error pages\n    '404_page_not_found' => 'Sahifa topilmadi',\n    'sorry_page_not_found' => 'Kechirasiz, siz izlayotgan sahifa topilmadi.',\n    'sorry_page_not_found_permission_warning' => 'Agar siz ushbu sahifa mavjudligini kutgan boʻlsangiz, uni koʻrish uchun ruxsatingiz boʻlmasligi mumkin.',\n    'image_not_found' => 'Rasm topilmadi',\n    'image_not_found_subtitle' => 'Kechirasiz, siz izlayotgan rasm fayli topilmadi.',\n    'image_not_found_details' => 'Agar siz ushbu rasm mavjudligini kutgan boʻlsangiz, u oʻchirilgan boʻlishi mumkin.',\n    'return_home' => 'Uyga qaytish',\n    'error_occurred' => 'Xatolik yuz berdi',\n    'app_down' => ':appName hozir ishlamayapti',\n    'back_soon' => 'Tez orada zaxiralanadi.',\n\n    // Import\n    'import_zip_cant_read' => 'Could not read ZIP file.',\n    'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',\n    'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Import ZIP failed to validate with errors:',\n    'import_zip_failed_notification' => 'Failed to import ZIP file.',\n    'import_perms_books' => 'You are lacking the required permissions to create books.',\n    'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',\n    'import_perms_pages' => 'You are lacking the required permissions to create pages.',\n    'import_perms_images' => 'You are lacking the required permissions to create images.',\n    'import_perms_attachments' => 'You are lacking the required permission to create attachments.',\n\n    // API errors\n    'api_no_authorization_found' => 'So‘rovda hech qanday avtorizatsiya belgisi topilmadi',\n    'api_bad_authorization_format' => 'So‘rovda avtorizatsiya belgisi topildi, lekin format noto‘g‘ri ko‘rindi',\n    'api_user_token_not_found' => 'Taqdim etilgan avtorizatsiya tokeniga mos keladigan API tokeni topilmadi',\n    'api_incorrect_token_secret' => 'Foydalanilgan API tokeni uchun berilgan sir notoʻgʻri',\n    'api_user_no_api_permission' => 'Foydalanilgan API tokeni egasi API qoʻngʻiroqlarini amalga oshirishga ruxsatga ega emas',\n    'api_user_token_expired' => 'Amaldagi avtorizatsiya tokeni muddati tugagan',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Sinov xatini yuborishda xatolik yuz berdi:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'URL sozlangan ruxsat etilgan SSR xostlariga mos kelmaydi',\n];\n"
  },
  {
    "path": "lang/uz/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => ':pageName sahifada yangi izoh',\n    'new_comment_intro' => ':appName ichida foydalanuvchi izoh qoldirdi:',\n    'new_page_subject' => ':pageName yangi sahifa.',\n    'new_page_intro' => ':appName ichida yangi sahifa yaratildi:',\n    'updated_page_subject' => ':pageName sahifasi yangilandi',\n    'updated_page_intro' => ':appName ichida sahifa yangilandi:',\n    'updated_page_debounce' => 'Xabarnomalar koʻp boʻlishining oldini olish uchun bir muncha vaqt oʻsha muharrir tomonidan ushbu sahifaga keyingi tahrirlar haqida bildirishnomalar yuborilmaydi.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Sahifa nomi:',\n    'detail_page_path' => 'Page Path:',\n    'detail_commenter' => 'Izoh egasi:',\n    'detail_comment' => 'Izoh:',\n    'detail_created_by' => 'Tomonidan yaratildi:',\n    'detail_updated_by' => 'Tomonidan tahrirlandi:',\n\n    'action_view_comment' => 'Izohlarni ko‘rish',\n    'action_view_page' => 'Sahifani ko‘rish',\n\n    'footer_reason' => ':link ushbu element uchun ushbu turdagi faoliyatni qamrab olgani uchun sizga bildirishnoma yuborildi.',\n    'footer_reason_link' => 'Bildirishnoma sozlamalari',\n];\n"
  },
  {
    "path": "lang/uz/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Oldingi',\n    'next'     => 'Kegingi &raquo;',\n\n];\n"
  },
  {
    "path": "lang/uz/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Parollar kamida sakkiz belgidan iborat bo\\'lishi va tasdiqlashga mos kelishi kerak.',\n    'user' => \"Bunday elektron pochta manziliga ega foydalanuvchini topa olmadik.\",\n    'token' => 'Ushbu e-pochta manzili uchun parolni tiklash tokeni yaroqsiz.',\n    'sent' => 'Parolni tiklash havolasini elektron pochta orqali yubordik!',\n    'reset' => 'Parolingiz qayta tiklandi!',\n\n];\n"
  },
  {
    "path": "lang/uz/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'My Account',\n\n    'shortcuts' => 'Qisqa klavishlar',\n    'shortcuts_interface' => 'UI Shortcut Preferences',\n    'shortcuts_toggle_desc' => 'Bu erda siz navigatsiya va harakatlar uchun ishlatiladigan klaviatura tizimi interfeysi yorliqlarini yoqishingiz yoki oʻchirishingiz mumkin.',\n    'shortcuts_customize_desc' => 'Quyidagi yorliqlarning har birini sozlashingiz mumkin. Qisqa klavish uchun kiritishni tanlagandan soʻng, kerakli tugmalar birikmasini bosing.',\n    'shortcuts_toggle_label' => 'Klaviatura yorliqlari yoqilgan',\n    'shortcuts_section_navigation' => 'Navigatsiya',\n    'shortcuts_section_actions' => 'Umumiy harakatlar',\n    'shortcuts_save' => 'Yorliqlarni saqlash',\n    'shortcuts_overlay_desc' => 'Eslatma: Yorliqlar yoqilgan boʻlsa, \"?\" tugmasini bosish orqali yordamchi qatlam mavjud boʻladi. hozirda ekranda koʻrinadigan amallar uchun mavjud yorliqlarni ajratib koʻrsatadi.',\n    'shortcuts_update_success' => 'Qisqa klavishlar sozlamalari yangilandi!',\n    'shortcuts_overview_desc' => 'Tizim foydalanuvchi interfeysida harakatlanish uchun foydalanishingiz mumkin boʻlgan klaviatura yorliqlarini boshqaring.',\n\n    'notifications' => 'Bildirishnoma sozlamalari',\n    'notifications_desc' => 'Tizimda muayyan harakatlar amalga oshirilganda qabul qilinadigan elektron pochta xabarnomalarini boshqaring.',\n    'notifications_opt_own_page_changes' => 'Menga tegishli boʻlgan sahifalarimdagi oʻzgarishlar haqida xabar bering',\n    'notifications_opt_own_page_comments' => 'Menga tegishli sahifalardagi sharhlar haqida xabar bering',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Fikrlarimga javoblarim haqida xabar bering',\n    'notifications_save' => 'Afzalliklarni saqlash',\n    'notifications_update_success' => 'Bildirishnoma sozlamalari yangilandi!',\n    'notifications_watched' => 'Koʻrilgan va e\\'tiborga olinmagan narsalar',\n    'notifications_watched_desc' => 'Quyida maxsus soat sozlamalari qoʻllaniladigan elementlar mavjud. Bular uchun afzalliklaringizni yangilash uchun elementni koʻring, soʻngra yon paneldagi tomosha parametrlarini toping.',\n\n    'auth' => 'Access & Security',\n    'auth_change_password' => 'Change Password',\n    'auth_change_password_desc' => 'Change the password you use to log-in to the application. This must be at least 8 characters long.',\n    'auth_change_password_success' => 'Password has been updated!',\n\n    'profile' => 'Profile Details',\n    'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.',\n    'profile_view_public' => 'View Public Profile',\n    'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.',\n    'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.',\n    'profile_email_no_permission' => 'Unfortunately you don\\'t have permission to change your email address. If you want to change this, you\\'d need to ask an administrator to change this for you.',\n    'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.',\n    'profile_admin_options' => 'Administrator Options',\n    'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the \"Settings > Users\" area of the application.',\n\n    'delete_account' => 'Delete Account',\n    'delete_my_account' => 'Delete My Account',\n    'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\\'ve created, such as created pages and uploaded images, will remain.',\n    'delete_my_account_warning' => 'Are you sure you want to delete your account?',\n];\n"
  },
  {
    "path": "lang/uz/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Sozlamalar',\n    'settings_save' => 'Sozlamalarni saqlash',\n    'system_version' => 'Tizim versiyasi',\n    'categories' => 'Kategoriyalar',\n\n    // App Settings\n    'app_customization' => 'Moslashtirish',\n    'app_features_security' => 'Xususiyatlar va xavfsizlik',\n    'app_name' => 'Ilova nomi',\n    'app_name_desc' => 'Ushbu nom sarlavhada va tizim tomonidan yuborilgan har qanday elektron pochta xabarlarida ko\\'rsatilgan.',\n    'app_name_header' => 'Sarlavhada nomni ko\\'rsatish',\n    'app_public_access' => 'Umumiy foydalanish imkoniyati',\n    'app_public_access_desc' => 'Ushbu parametr yoqilsa, tizimga kirmagan tashrif buyuruvchilarga BookStack nusxangiz tarkibiga kirishga ruxsat beriladi.',\n    'app_public_access_desc_guest' => 'Ommaviy tashrif buyuruvchilar uchun kirishni \"Mehmon\" foydalanuvchisi orqali boshqarish mumkin.',\n    'app_public_access_toggle' => 'Umumiy foydalanishga ruxsat bering',\n    'app_public_viewing' => 'Hammaga ochiq koʻrishga ruxsat berilsinmi?',\n    'app_secure_images' => 'Yuqori darajadagi xavfsizlik tasvirini yuklash',\n    'app_secure_images_toggle' => 'Yuqori darajadagi xavfsizlik tasvirini yuklashni yoqing',\n    'app_secure_images_desc' => 'Ishlash sabablariga ko\\'ra, barcha tasvirlar ommaviydir. Ushbu parametr tasvir URL manzillari oldiga tasodifiy, taxmin qilish qiyin bo\\'lgan qatorni qo\\'shadi. Oson kirishni oldini olish uchun katalog indekslari yoqilmaganligiga ishonch hosil qiling.',\n    'app_default_editor' => 'Standart sahifa muharriri',\n    'app_default_editor_desc' => 'Yangi sahifalarni tahrirlashda sukut bo\\'yicha qaysi muharrir ishlatilishini tanlang. Bu ruxsatlar ruxsat etilgan sahifa darajasida bekor qilinishi mumkin.',\n    'app_custom_html' => 'Maxsus HTML bosh tarkibi',\n    'app_custom_html_desc' => 'Bu yerga qo\\'shilgan har qanday kontent har bir sahifaning <head> bo\\'limining pastki qismiga kiritiladi. Bu uslublarni bekor qilish yoki analitik kodni qo\\'shish uchun qulay.',\n    'app_custom_html_disabled_notice' => 'Har qanday buzilgan oʻzgarishlarni qaytarib olish uchun maxsus HTML bosh kontenti ushbu sozlamalar sahifasida oʻchirib qoʻyilgan.',\n    'app_logo' => 'Ilova logotipi',\n    'app_logo_desc' => 'Bu boshqa sohalar qatorida dastur sarlavhasi satrida ishlatiladi. Ushbu rasm balandligi 86px bo\\'lishi kerak. Katta tasvirlar kichraytiriladi.',\n    'app_icon' => 'Ilova belgisi',\n    'app_icon_desc' => 'Ushbu belgi brauzer yorliqlari va yorliqlar uchun ishlatiladi. Bu 256px kvadrat PNG tasvir bo\\'lishi kerak.',\n    'app_homepage' => 'Ilova bosh sahifasi',\n    'app_homepage_desc' => 'Bosh sahifada standart koʻrinish oʻrniga koʻrsatish uchun koʻrinishni tanlang. Tanlangan sahifalar uchun sahifa ruxsatnomalari hisobga olinmaydi.',\n    'app_homepage_select' => 'Sahifani tanlang',\n    'app_footer_links' => 'Altbilgi havolalari',\n    'app_footer_links_desc' => 'Sayt altbilgisida ko\\'rsatish uchun havolalarni qo\\'shing. Ular ko\\'pchilik sahifalarning pastki qismida, jumladan, kirishni talab qilmaydigan sahifalarda ko\\'rsatiladi. Tizim tomonidan belgilangan tarjimalardan foydalanish uchun \"trans::<key>\" yorlig\\'idan foydalanishingiz mumkin. Masalan: \"trans::common.privacy_policy\" dan foydalanish \"Maxfiylik siyosati\" tarjima qilingan matnni va \"trans::common.terms_of_service\" tarjima qilingan \"Xizmat shartlari\" matnini taqdim etadi.',\n    'app_footer_links_label' => 'Havola yorlig\\'i',\n    'app_footer_links_url' => 'Havola URL',\n    'app_footer_links_add' => 'Altbilgi havolasini qo\\'shing',\n    'app_disable_comments' => 'Fikrlarni o‘chirib qo‘yish',\n    'app_disable_comments_toggle' => 'Fikrlarni o\\'chirib qo\\'ying',\n    'app_disable_comments_desc' => 'Ilovaning barcha sahifalarida sharhlarni o\\'chirib qo\\'yadi. <br> Mavjud sharhlar ko\\'rsatilmaydi.',\n\n    // Color settings\n    'color_scheme' => 'Ilova rang sxemasi',\n    'color_scheme_desc' => 'Ilova foydalanuvchi interfeysida foydalanish uchun ranglarni o\\'rnating. Mavzuga eng mos kelishi va tushunarliligini ta\\'minlash uchun ranglar qorong\\'u va yorug\\'lik rejimlari uchun alohida sozlanishi mumkin.',\n    'ui_colors_desc' => 'Ilovaning asosiy rangini va standart havola rangini o\\'rnating. Asosiy rang asosan sarlavhali banner, tugmalar va interfeys bezaklari uchun ishlatiladi. Standart havola rangi yozma tarkibda ham, ilova interfeysida ham matnga asoslangan havolalar va harakatlar uchun ishlatiladi.',\n    'app_color' => 'Asosiy rang',\n    'link_color' => 'Standart havola rangi',\n    'content_colors_desc' => 'Sahifani tashkil etish ierarxiyasidagi barcha elementlar uchun ranglarni o\\'rnating. O\\'qish uchun standart ranglarga o\\'xshash yorqinlikdagi ranglarni tanlash tavsiya etiladi.',\n    'bookshelf_color' => 'Raf rangi',\n    'book_color' => 'Kitob rangi',\n    'chapter_color' => 'Bo\\'lim rangi',\n    'page_color' => 'Sahifa rangi',\n    'page_draft_color' => 'Sahifa qoralama rangi',\n\n    // Registration Settings\n    'reg_settings' => 'Roʻyxatdan oʻtish',\n    'reg_enable' => 'Ro‘yxatdan o‘tishni yoqing',\n    'reg_enable_toggle' => 'Ro‘yxatdan o‘tishni yoqish',\n    'reg_enable_desc' => 'Ro\\'yxatdan o\\'tish yoqilganda, foydalanuvchi o\\'zini dastur foydalanuvchisi sifatida ro\\'yxatdan o\\'tkazishi mumkin bo\\'ladi. Roʻyxatdan oʻtgandan soʻng ularga yagona, standart foydalanuvchi roli beriladi.',\n    'reg_default_role' => 'Ro\\'yxatdan o\\'tgandan keyin standart foydalanuvchi roli',\n    'reg_enable_external_warning' => 'Tashqi LDAP yoki SAML autentifikatsiyasi faol bo\\'lganda yuqoridagi parametr e\\'tiborga olinmaydi. Mavjud bo\\'lmagan a\\'zolar uchun foydalanuvchi hisoblari, agar foydalanilayotgan tashqi tizimga qarshi autentifikatsiya muvaffaqiyatli bo\\'lsa, avtomatik yaratiladi.',\n    'reg_email_confirmation' => 'Elektron pochtani tasdiqlash',\n    'reg_email_confirmation_toggle' => 'Elektron pochta orqali tasdiqlashni talab qiling',\n    'reg_confirm_email_desc' => 'Agar domen cheklovi ishlatilsa, elektron pochta orqali tasdiqlash talab qilinadi va bu parametr e\\'tiborga olinmaydi.',\n    'reg_confirm_restrict_domain' => 'Domenni cheklash',\n    'reg_confirm_restrict_domain_desc' => 'Roʻyxatdan oʻtishni cheklamoqchi boʻlgan elektron pochta domenlarining vergul bilan ajratilgan roʻyxatini kiriting. Ilova bilan ishlashga ruxsat berishdan oldin foydalanuvchilarga manzillarini tasdiqlash uchun elektron pochta xabari yuboriladi. <br> E\\'tibor bering, foydalanuvchilar muvaffaqiyatli ro\\'yxatdan o\\'tgandan so\\'ng elektron pochta manzillarini o\\'zgartirishi mumkin.',\n    'reg_confirm_restrict_domain_placeholder' => 'Cheklov oʻrnatilmagan',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\\'t affect existing books, and can be overridden per-book.',\n    'sorting_rules' => 'Sort Rules',\n    'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',\n    'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',\n    'sort_rule_create' => 'Create Sort Rule',\n    'sort_rule_edit' => 'Edit Sort Rule',\n    'sort_rule_delete' => 'Delete Sort Rule',\n    'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',\n    'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',\n    'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',\n    'sort_rule_details' => 'Sort Rule Details',\n    'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',\n    'sort_rule_operations' => 'Sort Operations',\n    'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',\n    'sort_rule_available_operations' => 'Available Operations',\n    'sort_rule_available_operations_empty' => 'No operations remaining',\n    'sort_rule_configured_operations' => 'Configured Operations',\n    'sort_rule_configured_operations_empty' => 'Drag/add operations from the \"Available Operations\" list',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => 'Name - Alphabetical',\n    'sort_rule_op_name_numeric' => 'Name - Numeric',\n    'sort_rule_op_created_date' => 'Created Date',\n    'sort_rule_op_updated_date' => 'Updated Date',\n    'sort_rule_op_chapters_first' => 'Chapters First',\n    'sort_rule_op_chapters_last' => 'Chapters Last',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Xizmat',\n    'maint_image_cleanup' => 'Tasvirlarni tozalash',\n    'maint_image_cleanup_desc' => 'Qaysi rasm va chizmalardan foydalanilayotganini va qaysi rasmlar ortiqcha ekanligini tekshirish uchun sahifa va tahrir tarkibini skanerlaydi. Buni ishga tushirishdan oldin to\\'liq ma\\'lumotlar bazasi va rasmning zaxira nusxasini yaratganingizga ishonch hosil qiling.',\n    'maint_delete_images_only_in_revisions' => 'Faqat eski sahifa tahrirlarida mavjud bo\\'lgan rasmlarni ham o\\'chiring',\n    'maint_image_cleanup_run' => 'Tozalashni ishga tushiring',\n    'maint_image_cleanup_warning' => ':potentsial foydalanilmagan rasmlar soni topildi. Haqiqatan ham bu rasmlarni oʻchirib tashlamoqchimisiz?',\n    'maint_image_cleanup_success' => ':topilgan va oʻchirilgan potentsial foydalanilmagan rasmlarni hisoblang!',\n    'maint_image_cleanup_nothing_found' => 'Foydalanilmayotgan rasmlar topilmadi, hech narsa o\\'chirilmadi!',\n    'maint_send_test_email' => 'Test elektron pochta xabarini yuboring',\n    'maint_send_test_email_desc' => 'Bu sizning profilingizda ko\\'rsatilgan elektron pochta manzilingizga test elektron pochta xabarini yuboradi.',\n    'maint_send_test_email_run' => 'Test elektron pochta xabarini yuboring',\n    'maint_send_test_email_success' => 'Elektron pochta manzili: manzilga yuborildi',\n    'maint_send_test_email_mail_subject' => 'Test elektron pochta',\n    'maint_send_test_email_mail_greeting' => 'Elektron pochta orqali yetkazib berish ishlayotganga o‘xshaydi!',\n    'maint_send_test_email_mail_text' => 'Tabriklaymiz! Ushbu e-pochta xabarnomasini olganingizdan so\\'ng, sizning elektron pochta sozlamalaringiz to\\'g\\'ri sozlanganga o\\'xshaydi.',\n    'maint_recycle_bin_desc' => 'O\\'chirilgan javonlar, kitoblar, boblar va sahifalar qayta tiklanishi yoki butunlay o\\'chirilishi uchun axlat qutisiga yuboriladi. Chiqindi qutisidagi eski narsalar tizim konfiguratsiyasiga qarab bir muncha vaqt o\\'tgach avtomatik ravishda olib tashlanishi mumkin.',\n    'maint_recycle_bin_open' => 'Chiqindi qutisini oching',\n    'maint_regen_references' => 'Ma\\'lumotnomalarni qayta tiklash',\n    'maint_regen_references_desc' => 'Ushbu harakat ma\\'lumotlar bazasida o\\'zaro mos yozuvlar indeksini qayta quradi. Bu odatda avtomatik tarzda amalga oshiriladi, ammo bu amal eski tarkibni yoki norasmiy usullar orqali qo\\'shilgan tarkibni indekslash uchun foydali bo\\'lishi mumkin.',\n    'maint_regen_references_success' => 'Malumot indeksi qayta tiklandi!',\n    'maint_timeout_command_note' => 'Eslatma: Ushbu amalni bajarish uchun vaqt kerak bo\\'lishi mumkin, bu esa ba\\'zi veb-muhitlarda vaqt tugashiga olib kelishi mumkin. Shu bilan bir qatorda, bu harakat terminal buyrug\\'i yordamida amalga oshiriladi.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Chiqindi qutisi',\n    'recycle_bin_desc' => 'Bu erda siz o\\'chirilgan narsalarni qayta tiklashingiz yoki ularni tizimdan butunlay olib tashlashni tanlashingiz mumkin. Ruxsat filtrlari qo\\'llaniladigan tizimdagi o\\'xshash harakatlar ro\\'yxatidan farqli o\\'laroq, bu ro\\'yxat filtrlanmagan.',\n    'recycle_bin_deleted_item' => 'O\\'chirilgan element',\n    'recycle_bin_deleted_parent' => 'Ota-ona',\n    'recycle_bin_deleted_by' => 'tomonidan oʻchirilgan',\n    'recycle_bin_deleted_at' => 'O\\'chirish vaqti',\n    'recycle_bin_permanently_delete' => 'Doimiy o\\'chirish',\n    'recycle_bin_restore' => 'Qayta tiklash',\n    'recycle_bin_contents_empty' => 'Qayta ishlash qutisi hozir bo\\'sh',\n    'recycle_bin_empty' => 'Chiqindi qutisini bo\\'shatish',\n    'recycle_bin_empty_confirm' => 'Bu axlat qutisidagi barcha narsalarni, shu jumladan har bir element ichidagi kontentni butunlay yo\\'q qiladi. Chiqindi qutisini bo\\'shatishga ishonchingiz komilmi?',\n    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?',\n    'recycle_bin_destroy_list' => 'Yo\\'q qilinishi kerak bo\\'lgan narsalar',\n    'recycle_bin_restore_list' => 'Qayta tiklanadigan narsalar',\n    'recycle_bin_restore_confirm' => 'Bu amal oʻchirilgan elementni, shu jumladan har qanday yordamchi elementlarni asl joyiga tiklaydi. Agar asl joy o\\'chirilgan bo\\'lsa va hozir axlat qutisida bo\\'lsa, asosiy element ham tiklanishi kerak bo\\'ladi.',\n    'recycle_bin_restore_deleted_parent' => 'Bu elementning ota-onasi ham oʻchirib tashlangan. Ular ota-ona ham tiklanmaguncha oʻchirib tashlanadi.',\n    'recycle_bin_restore_parent' => 'Ota-onani tiklash',\n    'recycle_bin_destroy_notification' => 'Oʻchirildi: axlat qutisidan jami elementlarni sanash.',\n    'recycle_bin_restore_notification' => 'Qayta tiklandi: axlat qutisidagi jami narsalarni sanash.',\n\n    // Audit Log\n    'audit' => 'Audit jurnali',\n    'audit_desc' => 'Ushbu audit jurnali tizimda kuzatilgan harakatlar ro\\'yxatini ko\\'rsatadi. Ruxsat filtrlari qo\\'llaniladigan tizimdagi o\\'xshash harakatlar ro\\'yxatidan farqli o\\'laroq, bu ro\\'yxat filtrlanmagan.',\n    'audit_event_filter' => 'Voqea filtri',\n    'audit_event_filter_no_filter' => 'Filtr yo\\'q',\n    'audit_deleted_item' => 'O\\'chirilgan element',\n    'audit_deleted_item_name' => 'Ism: :ism',\n    'audit_table_user' => 'Foydalanuvchi',\n    'audit_table_event' => 'Tadbir',\n    'audit_table_related' => 'Tegishli element yoki tafsilotlar',\n    'audit_table_ip' => 'IP manzili',\n    'audit_table_date' => 'Faoliyat sanasi',\n    'audit_date_from' => 'Sana diapazoni boshlab',\n    'audit_date_to' => 'Sana oraligʻi',\n\n    // Role Settings\n    'roles' => 'Rollar',\n    'role_user_roles' => 'Foydalanuvchi rollari',\n    'roles_index_desc' => 'Rollar foydalanuvchilarni guruhlash va ularning a\\'zolariga tizim ruxsatini berish uchun ishlatiladi. Agar foydalanuvchi bir nechta rollarning a\\'zosi bo\\'lsa, berilgan imtiyozlar to\\'planadi va foydalanuvchi barcha qobiliyatlarni meros qilib oladi.',\n    'roles_x_users_assigned' => ': tayinlangan foydalanuvchini hisoblash|: tayinlangan foydalanuvchilarni hisoblash',\n    'roles_x_permissions_provided' => ':count ruxsati|:ruxsat soni',\n    'roles_assigned_users' => 'Belgilangan foydalanuvchilar',\n    'roles_permissions_provided' => 'Taqdim etilgan ruxsatnomalar',\n    'role_create' => 'Yangi rol yaratish',\n    'role_delete' => 'Rolni o\\'chirish',\n    'role_delete_confirm' => 'Bu \\':roleName\\' nomli rolni o\\'chirib tashlaydi.',\n    'role_delete_users_assigned' => 'Bu rolga :userCount foydalanuvchilari tayinlangan. Agar siz ushbu roldan foydalanuvchilarni koʻchirmoqchi boʻlsangiz, quyida yangi rolni tanlang.',\n    'role_delete_no_migration' => \"Don\\\\'t migrate users\",\n    'role_delete_sure' => 'Haqiqatan ham bu rolni oʻchirib tashlamoqchimisiz?',\n    'role_edit' => 'Rolni tahrirlash',\n    'role_details' => 'Rol tafsilotlari',\n    'role_name' => 'Rol nomi',\n    'role_desc' => 'Rolning qisqacha tavsifi',\n    'role_mfa_enforced' => 'Ko\\'p faktorli autentifikatsiyani talab qiladi',\n    'role_external_auth_id' => 'Tashqi autentifikatsiya identifikatorlari',\n    'role_system' => 'Tizim ruxsatnomalari',\n    'role_manage_users' => 'Foydalanuvchilarni boshqarish',\n    'role_manage_roles' => 'Rol va rol ruxsatnomalarini boshqaring',\n    'role_manage_entity_permissions' => 'Barcha kitob, bob va sahifa ruxsatlarini boshqaring',\n    'role_manage_own_entity_permissions' => 'Shaxsiy kitob, bob va sahifalar uchun ruxsatlarni boshqaring',\n    'role_manage_page_templates' => 'Sahifa shablonlarini boshqarish',\n    'role_access_api' => 'Kirish tizimi API',\n    'role_manage_settings' => 'Ilova sozlamalarini boshqaring',\n    'role_export_content' => 'Kontentni eksport qilish',\n    'role_import_content' => 'Import content',\n    'role_editor_change' => 'Sahifa muharririni o\\'zgartirish',\n    'role_notifications' => 'Bildirishnomalarni qabul qilish va boshqarish',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Obyektga ruxsatlar',\n    'roles_system_warning' => 'Shuni yodda tutingki, yuqoridagi uchta ruxsatdan birortasiga kirish foydalanuvchiga o\\'z imtiyozlarini yoki tizimdagi boshqalarning imtiyozlarini o\\'zgartirishi mumkin. Ishonchli foydalanuvchilarga faqat ushbu ruxsatlarga ega rollarni tayinlang.',\n    'role_asset_desc' => 'Bu ruxsatlar tizim ichidagi aktivlarga standart kirishni nazorat qiladi. Kitoblar, boblar va sahifalardagi ruxsatlar bu ruxsatlarni bekor qiladi.',\n    'role_asset_admins' => 'Administratorlarga avtomatik ravishda barcha kontentga kirish huquqi beriladi, lekin bu parametrlar UI parametrlarini koʻrsatishi yoki yashirishi mumkin.',\n    'role_asset_image_view_note' => 'Bu tasvir menejeridagi ko\\'rinishga tegishli. Yuklangan rasm fayllariga haqiqiy kirish tizim tasvirini saqlash opsiyasiga bog\\'liq bo\\'ladi.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'Hammasi',\n    'role_own' => 'Shaxsiy',\n    'role_controlled_by_asset' => 'Ular yuklangan obyekt tomonidan nazorat qilinadi',\n    'role_save' => 'Rolni saqlash',\n    'role_users' => 'Ushbu roldagi foydalanuvchilar',\n    'role_users_none' => 'Hozirda bu rolga hech qanday foydalanuvchi tayinlanmagan',\n\n    // Users\n    'users' => 'Foydalanuvchilar',\n    'users_index_desc' => 'Tizimda individual foydalanuvchi hisoblarini yarating va boshqaring. Foydalanuvchi hisoblari tizimga kirish va kontent va faoliyat atributi uchun ishlatiladi. Kirish ruxsatlari asosan rolga asoslangan, lekin foydalanuvchi kontentiga egalik, boshqa omillar qatori, ruxsat va kirishga ham ta\\'sir qilishi mumkin.',\n    'user_profile' => 'Foydalanuvchi profili',\n    'users_add_new' => 'Yangi foydalanuvchi qo\\'shish',\n    'users_search' => 'Foydalanuvchilarni qidirish',\n    'users_latest_activity' => 'Oxirgi faoliyat',\n    'users_details' => 'Foydalanuvchi tafsilotlari',\n    'users_details_desc' => 'Ushbu foydalanuvchi uchun ko\\'rsatiladigan nom va elektron pochta manzilini o\\'rnating. Elektron pochta manzili ilovaga kirish uchun ishlatiladi.',\n    'users_details_desc_no_email' => 'Bu foydalanuvchini boshqalar tanib olishi uchun ko‘rsatiladigan nomni o‘rnating.',\n    'users_role' => 'Foydalanuvchi rollari',\n    'users_role_desc' => 'Ushbu foydalanuvchi qaysi rollarga tayinlanishini tanlang. Agar foydalanuvchi bir nechta rollarga tayinlangan bo\\'lsa, bu rollarning ruxsatlari yig\\'iladi va ular tayinlangan rollarning barcha qobiliyatlarini oladi.',\n    'users_password' => 'Foydalanuvchi paroli',\n    'users_password_desc' => 'Ilovaga kirish uchun ishlatiladigan parolni o\\'rnating. Bu kamida 8 ta belgidan iborat boʻlishi kerak.',\n    'users_send_invite_text' => 'Siz ushbu foydalanuvchiga oʻz parolini oʻrnatishga imkon beruvchi taklif e-pochtasini yuborishni tanlashingiz mumkin, aks holda uning parolini oʻzingiz belgilashingiz mumkin.',\n    'users_send_invite_option' => 'Foydalanuvchi taklifnomasini elektron pochta orqali yuboring',\n    'users_external_auth_id' => 'Tashqi autentifikatsiya identifikatori',\n    'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.',\n    'users_password_warning' => 'Only fill the below if you would like to change the password for this user.',\n    'users_system_public' => 'Bu foydalanuvchi sizning misolingizga tashrif buyurgan har qanday mehmon foydalanuvchilarini ifodalaydi. U tizimga kirish uchun ishlatilmaydi, lekin avtomatik ravishda tayinlanadi.',\n    'users_delete' => 'Foydalanuvchini oʻchirish',\n    'users_delete_named' => 'Foydalanuvchini o\\'chirish :userName',\n    'users_delete_warning' => 'Bu \\':userName\\' nomli foydalanuvchini tizimdan butunlay o\\'chirib tashlaydi.',\n    'users_delete_confirm' => 'Bu foydalanuvchini oʻchirib tashlamoqchimisiz?',\n    'users_migrate_ownership' => 'Egalikni ko‘chirish',\n    'users_migrate_ownership_desc' => 'Agar boshqa foydalanuvchi ushbu foydalanuvchiga tegishli barcha elementlarning egasi boʻlishini istasangiz, bu yerda foydalanuvchini tanlang.',\n    'users_none_selected' => 'Hech qanday foydalanuvchi tanlanmagan',\n    'users_edit' => 'Foydalanuvchini tahrirlash',\n    'users_edit_profile' => 'Profilni tahrirlash',\n    'users_avatar' => 'Foydalanuvchi avatar',\n    'users_avatar_desc' => 'Ushbu foydalanuvchini ifodalash uchun rasmni tanlang. Bu taxminan 256px kvadrat bo\\'lishi kerak.',\n    'users_preferred_language' => 'Afzal til',\n    'users_preferred_language_desc' => 'Ushbu parametr ilovaning foydalanuvchi interfeysi uchun ishlatiladigan tilni o\\'zgartiradi. Bu foydalanuvchi tomonidan yaratilgan kontentga ta\\'sir qilmaydi.',\n    'users_social_accounts' => 'Ijtimoiy hisoblar',\n    'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.',\n    'users_social_accounts_info' => 'Bu yerda siz tezroq va osonroq kirish uchun boshqa hisoblaringizni ulashingiz mumkin. Bu yerda hisobni uzish avval ruxsat berilgan ruxsatni bekor qilmaydi. Ulangan ijtimoiy hisob qaydnomangizdagi profil sozlamalaringizdan kirishni bekor qiling.',\n    'users_social_connect' => 'Hisobni ulash',\n    'users_social_disconnect' => 'Hisobni o\\'chirish',\n    'users_social_status_connected' => 'Connected',\n    'users_social_status_disconnected' => 'Disconnected',\n    'users_social_connected' => ':socialAccount hisobi profilingizga muvaffaqiyatli biriktirildi.',\n    'users_social_disconnected' => ':socialAccount hisobi profilingizdan muvaffaqiyatli uzildi.',\n    'users_api_tokens' => 'API tokenlari',\n    'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.',\n    'users_api_tokens_none' => 'Bu foydalanuvchi uchun API tokenlari yaratilmagan',\n    'users_api_tokens_create' => 'Token yaratish',\n    'users_api_tokens_expires' => 'Muddati tugaydi',\n    'users_api_tokens_docs' => 'API hujjatlari',\n    'users_mfa' => 'Ko\\'p faktorli autentifikatsiya',\n    'users_mfa_desc' => 'Ko\\'p faktorli autentifikatsiyani foydalanuvchi hisobingiz uchun qo\\'shimcha xavfsizlik qatlami sifatida o\\'rnating.',\n    'users_mfa_x_methods' => ':count usuli tuzilgan|:count usullari sozlangan',\n    'users_mfa_configure' => 'Usullarni sozlash',\n\n    // API Tokens\n    'user_api_token_create' => 'API tokenini yarating',\n    'user_api_token_name' => 'Ism',\n    'user_api_token_name_desc' => 'Belgilangan maqsadni kelajakda eslatish uchun o\\'qilishi mumkin bo\\'lgan nom bering.',\n    'user_api_token_expiry' => 'Quyidagi sanagacha foydalanilsin',\n    'user_api_token_expiry_desc' => 'Ushbu tokenning amal qilish muddati tugash sanasini belgilang. Bu sanadan keyin ushbu token yordamida qilingan soʻrovlar ishlamaydi. Bu maydonni boʻsh qoldirish kelajakda 100 yil oʻtib muddatini belgilaydi.',\n    'user_api_token_create_secret_message' => 'Ushbu token yaratilgandan so\\'ng darhol \"Token ID\" va \"Token Secret\" yaratiladi va ko\\'rsatiladi. Sir faqat bir marta ko\\'rsatiladi, shuning uchun davom etishdan oldin qiymatni xavfsiz va xavfsiz joyga nusxalashni unutmang.',\n    'user_api_token' => 'API tokeni',\n    'user_api_token_id' => 'Token ID',\n    'user_api_token_id_desc' => 'Bu token uchun tahrir qilib boʻlmaydigan tizim tomonidan yaratilgan identifikator boʻlib, API soʻrovlarida taqdim etilishi kerak.',\n    'user_api_token_secret' => 'Token siri',\n    'user_api_token_secret_desc' => 'Bu API so\\'rovlarida taqdim etilishi kerak bo\\'lgan ushbu token uchun yaratilgan tizim siridir. Bu faqat bir marta ko\\'rsatiladi, shuning uchun bu qiymatni xavfsiz va xavfsiz joyga nusxalang.',\n    'user_api_token_created' => 'Token yaratilgan: timeAgo',\n    'user_api_token_updated' => 'Token yangilandi: timeAgo',\n    'user_api_token_delete' => 'Tokenni oʻchirish',\n    'user_api_token_delete_warning' => 'Bu \\':tokenName\\' nomli ushbu API tokenini tizimdan butunlay oʻchirib tashlaydi.',\n    'user_api_token_delete_confirm' => 'Haqiqatan ham ushbu API tokenini oʻchirib tashlamoqchimisiz?',\n\n    // Webhooks\n    'webhooks' => 'Webhuklar',\n    'webhooks_index_desc' => 'Veb-huklar tizim ichida ma\\'lum harakatlar va hodisalar sodir bo\\'lganda tashqi URL manzillariga ma\\'lumotlarni yuborish usuli bo\\'lib, xabar almashish yoki bildirishnoma tizimlari kabi tashqi platformalar bilan voqealarga asoslangan integratsiyani ta\\'minlaydi.',\n    'webhooks_x_trigger_events' => ':count trigger hodisasi|: count trigger voqealari',\n    'webhooks_create' => 'Yangi Webhook yaratish',\n    'webhooks_none_created' => 'Hali hech qanday vebhuk yaratilmagan.',\n    'webhooks_edit' => 'Webhook-ni tahrirlash',\n    'webhooks_save' => 'Webhook-ni saqlang',\n    'webhooks_details' => 'Webhook tafsilotlari',\n    'webhooks_details_desc' => 'Webhook ma\\'lumotlari yuboriladigan joy sifatida foydalanuvchiga qulay nom va POST so\\'nggi nuqtasini taqdim eting.',\n    'webhooks_events' => 'Webhook voqealari',\n    'webhooks_events_desc' => 'Ushbu veb-hukni chaqirishi kerak bo\\'lgan barcha hodisalarni tanlang.',\n    'webhooks_events_warning' => 'Shuni esda tutingki, bu hodisalar, hatto maxsus ruxsatlar qo\\'llanilsa ham, tanlangan barcha hodisalar uchun ishga tushadi. Ushbu vebhukdan foydalanish maxfiy kontentni oshkor qilmasligiga ishonch hosil qiling.',\n    'webhooks_events_all' => 'Barcha tizim hodisalari',\n    'webhooks_name' => 'Webhook nomi',\n    'webhooks_timeout' => 'Webhook so\\'rovining kutish vaqti (soniyalar)',\n    'webhooks_endpoint' => 'Webhook oxirgi nuqtasi',\n    'webhooks_active' => 'Webhook faol',\n    'webhook_events_table_header' => 'Voqealar',\n    'webhooks_delete' => 'Webhook-ni o\\'chirish',\n    'webhooks_delete_warning' => 'Bu \\':webhookName\\' nomli ushbu vebhukni tizimdan butunlay o\\'chirib tashlaydi.',\n    'webhooks_delete_confirm' => 'Haqiqatan ham bu vebhukni oʻchirib tashlamoqchimisiz?',\n    'webhooks_format_example' => 'Webhook formatiga misol',\n    'webhooks_format_example_desc' => 'Webhook maʼlumotlari POST soʻrovi sifatida sozlangan soʻnggi nuqtaga quyidagi formatga muvofiq JSON sifatida yuboriladi. \"Related_item\" va \"url\" xususiyatlari ixtiyoriy va ishga tushirilgan hodisa turiga bog\\'liq bo\\'ladi.',\n    'webhooks_status' => 'Webhook holati',\n    'webhooks_last_called' => 'Oxirgi qo\\'ng\\'iroq:',\n    'webhooks_last_errored' => 'Oxirgi xato:',\n    'webhooks_last_error_message' => 'Oxirgi xato xabari:',\n\n    // Licensing\n    'licenses' => 'Licenses',\n    'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.',\n    'licenses_bookstack' => 'BookStack License',\n    'licenses_php' => 'PHP Library Licenses',\n    'licenses_js' => 'JavaScript Library Licenses',\n    'licenses_other' => 'Other Licenses',\n    'license_details' => 'License Details',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Dansk',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/uz/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute qabul qilinishi kerak.',\n    'active_url'           => ':attribute qiymati to‘g‘ri URL emas.',\n    'after'                => ':attribute qiymati :date sanadan kegingi sana bo‘lishi kerak.',\n    'alpha'                => ':attribute qiymati faqat harflardan iborat bo‘lishi kerak.',\n    'alpha_dash'           => ':attribute qiymati faqat harflar, raqamlar chiziqcha va ostki chiziqdan iborat bo‘lishi kerak.',\n    'alpha_num'            => ':attribute qiymati faqat harflar va raqamlardan iborat bo‘lishi kerak.',\n    'array'                => ':attribute qiymati massiv bo‘lishi kerak.',\n    'backup_codes'         => 'Kiritilgan kod to‘g‘ri emas yoki ishlatib bo‘lingan.',\n    'before'               => ':attribute qiymati :date sanadan oldingi sana bo‘lishi kerak.',\n    'between'              => [\n        'numeric' => ':attribute qiymati :min va :max orasida bo‘lishi kerak.',\n        'file'    => ':attribute hajmi :min va :max kilobayt orasida bo‘lishi kerak.',\n        'string'  => ':attribute uzunligi soni :min va :max orasida bo‘lishi kerak.',\n        'array'   => ':attribute soni :min va :max orasida bo‘lishi kerak.',\n    ],\n    'boolean'              => ':attribute qiymati faqat «true» yoki «false» bo`lishi kerak.',\n    'confirmed'            => ':attribute tasdiqlash qiymati mos emas.',\n    'date'                 => ':attribute qiymati sana emas.',\n    'date_format'          => ':attribute qiymati :format formatdagi sana emas.',\n    'different'            => ':attribute va :other qiymatlari har xil bo‘lishi kerak.',\n    'digits'               => ':attribute qiymati :digits raqamlarda iborat bo‘lishi kerak.',\n    'digits_between'       => ':attribute qiymati :min va :max orasidagi raqamlarda iborat bo‘lishi kerak.',\n    'email'                => ':attribute qiymati email bo‘lishi kerak.',\n    'ends_with' => ':attribute qiymati quyidagilarda biri bo‘lishi kerak: :values ',\n    'file'                 => ':attribute fayl bo‘lishi kerak.',\n    'filled'               => ':attribute qiymatini kiritish majburiy.',\n    'gt'                   => [\n        'numeric' => ':attribute qiymati :value\\'dan katta bo‘lishi kerak.',\n        'file'    => ':attribute hajmi :value kilobaytdan katta bo‘lishi kerak.',\n        'string'  => ':attribute uzunligi :value\\'dan katta bo‘lishi kerak.',\n        'array'   => ':attribute soni :value\\'dan katta bo‘lishi kerak.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute qiymati :value\\'dan katta yoki teng bo‘lishi kerak.',\n        'file'    => ':attribute hajmi :value kilobaytdan katta yoki teng bo‘lishi kerak.',\n        'string'  => ':attribute uzunligi :value\\'dan katta yoki teng bo‘lishi kerak.',\n        'array'   => ':attribute soni :value\\'dan katta yoki teng bo‘lishi kerak.',\n    ],\n    'exists'               => ':attribute\\'ning tanlangan qiymati to‘g‘ri emas.',\n    'image'                => ':attribute rasm bo‘lishi kerak.',\n    'image_extension'      => ':attribute rasm bo‘lishi va to‘g‘ri formatda bo‘lishi kerak.',\n    'in'                   => ':attribute qiymati noto‘g‘ri.',\n    'integer'              => ':attribute qiymati butun son bo‘lishi kerak.',\n    'ip'                   => ':attribute qiymati IP manzil bo‘lishi kerak.',\n    'ipv4'                 => ':attribute qiymati IPv4 manzil bo‘lishi kerak.',\n    'ipv6'                 => ':attribute qiymati IPv6 manzil bo‘lishi kerak.',\n    'json'                 => ':attribute qiymati JSON formatida bo‘lishi kerak.',\n    'lt'                   => [\n        'numeric' => ':attribute qiymati :value\\'dan kichik bo‘lishi kerak.',\n        'file'    => ':attribute hajmi :value kilobaytdan kichik bo‘lishi kerak.',\n        'string'  => ':attribute uzunligi :value\\'dan kichik bo‘lishi kerak.',\n        'array'   => ':attribute soni :value\\'dan kichik bo‘lishi kerak.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute qiymati :value\\'dan kichik yoki teng bo‘lishi kerak.',\n        'file'    => ':attribute hajmi :value kilobaytdan kichik yoki teng bo‘lishi kerak.',\n        'string'  => ':attribute uzunligi :value\\'dan kichik yoki teng bo‘lishi kerak.',\n        'array'   => ':attribute soni :value\\'dan kichik yoki teng bo‘lishi kerak.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute qiymati maksimum :value bo‘lishi kerak.',\n        'file'    => ':attribute hajmi maksimum :value kilobayt bo‘lishi kerak.',\n        'string'  => ':attribute uzunligi maksimum :value bo‘lishi kerak.',\n        'array'   => ':attribute soni maksimum :value bo‘lishi kerak.',\n    ],\n    'mimes'                => ':attribute fayl mime turi quyidagilardan biri bo‘lishi kerak: :values.',\n    'min'                  => [\n        'numeric' => ':attribute qiymati minimum :value bo‘lishi kerak.',\n        'file'    => ':attribute hajmi minimum :value kilobayt bo‘lishi kerak.',\n        'string'  => ':attribute uzunligi minimum :value bo‘lishi kerak.',\n        'array'   => ':attribute soni minimum :value bo‘lishi kerak.',\n    ],\n    'not_in'               => 'selected :attribute qiymati noto‘g‘ri.',\n    'not_regex'            => ':attribute formati noto‘g‘ri.',\n    'numeric'              => ':attribute qiymati raqam bo‘lishi kerak.',\n    'regex'                => ':attribute formati noto‘g‘ri.',\n    'required'             => ':attribute\\'ni kiritish majburiy.',\n    'required_if'          => ':other qiymati :value bo‘lganda :attribute\\'ni kiritish majburiy.',\n    'required_with'        => ':values kiritilgan holatlarda :attribute\\'ni kiritish majburiy.',\n    'required_with_all'    => ':values kiritilgan holatlarda :attribute\\'ni kiritish majburiy.',\n    'required_without'     => ':values kiritilmagan holatlarda :attribute\\'ni kiritish majburiy.',\n    'required_without_all' => ':values kiritilmagan holatlarda :attribute\\'ni kiritish majburiy.',\n    'same'                 => ':attribute va :other qiymatlari teng bo‘lishi shart.',\n    'safe_url'             => 'Kiritilgan manzil xavsiz emas.',\n    'size'                 => [\n        'numeric' => ':attribute qiymati :value bo‘lishi kerak.',\n        'file'    => ':attribute hajmi :value kilobayt bo‘lishi kerak.',\n        'string'  => ':attribute uzunligi :value bo‘lishi kerak.',\n        'array'   => ':attribute soni :value bo‘lishi kerak.',\n    ],\n    'string'               => ':attribute qiymati matn bo‘lishi kerak.',\n    'timezone'             => ':attribute qiymati to‘g‘ri vaqt zonasi bo‘lishi kerak.',\n    'totp'                 => 'Kiritilgan xavsizlik kodi notpo‘g‘ri yoki eskirgan.',\n    'unique'               => ':attribute qiymati allaqachon mavjud.',\n    'url'                  => ':attribute URL formatida emas.',\n    'uploaded'             => 'Faylni yuklashda xatolik. Server bunday hajmdagi faylllarni yuklamasligi mumkin.',\n\n    'zip_file' => ':attribute ZIP ichidagi faylga havola qilishi kerak.',\n    'zip_file_size' => ':attribute fayli :size MB dan oshmasligi kerak.',\n    'zip_file_mime' => ':attribute :validTypes turidagi faylga havola qilishi kerak, lekin :foundType turida keldi.',\n    'zip_model_expected' => 'Ma\\'lumotlar obyekti kutilmoqda, ammo \":type\" topildi.',\n    'zip_unique' => ':attribute ZIP ichidagi obyekt turi uchun noyob bo\\'lishi kerak.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Takroriy parolni to‘ldirish majburiy',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/vi/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => 'đã tạo trang',\n    'page_create_notification'    => 'Trang đã được tạo thành công',\n    'page_update'                 => 'đã cập nhật trang',\n    'page_update_notification'    => 'Trang đã được cập nhật thành công',\n    'page_delete'                 => 'đã xóa trang',\n    'page_delete_notification'    => 'Trang đã được xóa thành công',\n    'page_restore'                => 'đã khôi phục trang',\n    'page_restore_notification'   => 'Trang đã được khôi phục thành công',\n    'page_move'                   => 'đã di chuyển trang',\n    'page_move_notification'      => 'Đã di chuyển trang thành công',\n\n    // Chapters\n    'chapter_create'              => 'đã tạo chương',\n    'chapter_create_notification' => 'Chương đã được tạo thành công',\n    'chapter_update'              => 'đã cập nhật chương',\n    'chapter_update_notification' => 'Chương đã được cập nhật thành công',\n    'chapter_delete'              => 'đã xóa chương',\n    'chapter_delete_notification' => 'Chương đã được xóa thành công',\n    'chapter_move'                => 'đã di chuyển chương',\n    'chapter_move_notification' => 'Đã chuyển chương thành công',\n\n    // Books\n    'book_create'                 => 'đã tạo sách',\n    'book_create_notification'    => 'Sách đã được tạo thành công',\n    'book_create_from_chapter'              => 'chuyển chương thành sách',\n    'book_create_from_chapter_notification' => 'Chuyển chương thành sách thành công',\n    'book_update'                 => 'đã cập nhật sách',\n    'book_update_notification'    => 'Sách đã được cập nhật thành công',\n    'book_delete'                 => 'đã xóa sách',\n    'book_delete_notification'    => 'Sách đã được xóa thành công',\n    'book_sort'                   => 'đã sắp xếp sách',\n    'book_sort_notification'      => 'Sách đã được sắp xếp lại thành công',\n\n    // Bookshelves\n    'bookshelf_create'            => 'đã tạo giá sách',\n    'bookshelf_create_notification'    => 'Giá sách đã được tạo thành công',\n    'bookshelf_create_from_book'    => 'chuyển sách thành giá sách',\n    'bookshelf_create_from_book_notification'    => 'Chuyển sách thành giá sách thành công',\n    'bookshelf_update'                 => 'cập nhật giá sách',\n    'bookshelf_update_notification'    => 'Giá sách được cập nhật thành công',\n    'bookshelf_delete'                 => 'xoá giá sách',\n    'bookshelf_delete_notification'    => 'Xoá giá sách thành công',\n\n    // Revisions\n    'revision_restore' => 'đã khôi phục sửa đổi',\n    'revision_delete' => 'đã xóa bản sửa đổi',\n    'revision_delete_notification' => 'Bản sửa đổi đã được xóa thành công',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" đã được thêm vào danh sách yêu thích của bạn',\n    'favourite_remove_notification' => '\":name\" đã được gỡ khỏi danh sách yêu thích của bạn',\n\n    // Watching\n    'watch_update_level_notification' => 'Đã cập nhật tùy chọn xem thành công',\n\n    // Auth\n    'auth_login' => 'đăng nhập',\n    'auth_register' => 'đã đăng ký như người dùng mới',\n    'auth_password_reset_request' => 'yêu cầu người dùng đặt lại mật khẩu',\n    'auth_password_reset_update' => 'đặt lại mật khẩu người dùng',\n    'mfa_setup_method' => 'đã định cấu hình phương thức MFA',\n    'mfa_setup_method_notification' => 'Cấu hình xác thực nhiều bước thành công',\n    'mfa_remove_method' => 'loại bỏ phương thức MFA',\n    'mfa_remove_method_notification' => 'Đã gỡ xác thực nhiều bước',\n\n    // Settings\n    'settings_update' => 'cập nhật cài đặt',\n    'settings_update_notification' => 'Cài đặt đã cập nhật thành công',\n    'maintenance_action_run' => 'chạy hành động bảo trì',\n\n    // Webhooks\n    'webhook_create' => 'đã tạo webhook',\n    'webhook_create_notification' => 'Webhook đã được tạo thành công',\n    'webhook_update' => 'đã cập nhật webhook',\n    'webhook_update_notification' => 'Webhook đã được cập nhật thành công',\n    'webhook_delete' => 'đã xóa webhook',\n    'webhook_delete_notification' => 'Webhook đã được xóa thành công',\n\n    // Imports\n    'import_create' => 'đã tạo nhập',\n    'import_create_notification' => 'Tải lên nhập thành công',\n    'import_run' => 'đã nhập cập nhật',\n    'import_run_notification' => 'Nội dung đã được nhập thành công',\n    'import_delete' => 'Đã xóa nhập',\n    'import_delete_notification' => 'Nhập đã được xóa thành công',\n\n    // Users\n    'user_create' => 'đã tạo người dùng',\n    'user_create_notification' => 'Người dùng được tạo thành công',\n    'user_update' => 'người dùng được cập nhật',\n    'user_update_notification' => 'Người dùng được cập nhật thành công',\n    'user_delete' => 'người dùng đã bị xóa',\n    'user_delete_notification' => 'Người dùng đã được xóa thành công',\n\n    // API Tokens\n    'api_token_create' => 'Đã tạo Token API ',\n    'api_token_create_notification' => 'Token API  đã tạo thành công',\n    'api_token_update' => 'Đã cập nhật token API ',\n    'api_token_update_notification' => 'Token API  đã cập nhật thành công',\n    'api_token_delete' => 'Đã xóa token API',\n    'api_token_delete_notification' => 'Đã xóa token API thành công',\n\n    // Roles\n    'role_create' => 'Đã tạo vai trò',\n    'role_create_notification' => 'Vai trò mới đã được tạo thành công',\n    'role_update' => 'Vai trò đã cập nhật',\n    'role_update_notification' => 'Vai trò đã được cập nhật thành công',\n    'role_delete' => 'đã xóa vai trò',\n    'role_delete_notification' => 'Vai trò đã được xóa thành công',\n\n    // Recycle Bin\n    'recycle_bin_empty' => 'làm trống thùng rác',\n    'recycle_bin_restore' => 'khôi phục từ thùng rác',\n    'recycle_bin_destroy' => 'đã xóa khỏi thùng rác',\n\n    // Comments\n    'commented_on'                => 'đã bình luận về',\n    'comment_create'              => 'thêm bình luận',\n    'comment_update'              => 'cập nhật bình luận',\n    'comment_delete'              => 'đã xóa bình luận',\n\n    // Sort Rules\n    'sort_rule_create' => '',\n    'sort_rule_create_notification' => '',\n    'sort_rule_update' => 'xóa quy tắc sắp xếp',\n    'sort_rule_update_notification' => 'Đã cập nhật quy tắc sắp xếp thành công',\n    'sort_rule_delete' => 'xóa quy tắc sắp xếp',\n    'sort_rule_delete_notification' => 'Đã xóa quy tắc sắp xếp thành công',\n\n    // Other\n    'permissions_update'          => 'các quyền đã được cập nhật',\n];\n"
  },
  {
    "path": "lang/vi/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => 'Thông tin đăng nhập này không khớp với dữ liệu của chúng tôi.',\n    'throttle' => 'Quá nhiều lần đăng nhập sai. Vui lòng thử lại sau :seconds giây.',\n\n    // Login & Register\n    'sign_up' => 'Đăng ký',\n    'log_in' => 'Đăng nhập',\n    'log_in_with' => 'Đăng nhập với :socialDriver',\n    'sign_up_with' => 'Đăng kí với :socialDriver',\n    'logout' => 'Đăng xuất',\n\n    'name' => 'Tên',\n    'username' => 'Tên đăng nhập',\n    'email' => 'Email',\n    'password' => 'Mật khẩu',\n    'password_confirm' => 'Xác nhận mật khẩu',\n    'password_hint' => 'Phải có ít nhất 8 ký tự',\n    'forgot_password' => 'Quên Mật khẩu?',\n    'remember_me' => 'Ghi nhớ đăng nhập',\n    'ldap_email_hint' => 'Vui lòng điền một địa chỉ email để sử dụng tài khoản này.',\n    'create_account' => 'Tạo Tài khoản',\n    'already_have_account' => 'Bạn đã có tài khoản?',\n    'dont_have_account' => 'Bạn không có tài khoản?',\n    'social_login' => 'Đăng nhập bằng MXH',\n    'social_registration' => 'Đăng kí bằng MXH',\n    'social_registration_text' => 'Đăng kí và đăng nhập bằng dịch vụ khác.',\n\n    'register_thanks' => 'Cảm ơn bạn đã đăng ký!',\n    'register_confirm' => 'Vui lòng kiểm tra email và bấm vào nút xác nhận để truy cập :appName.',\n    'registrations_disabled' => 'Việc đăng kí đang bị tắt',\n    'registration_email_domain_invalid' => 'Tên miền của email không có quyền truy cập tới ứng dụng này',\n    'register_success' => 'Cảm ơn bạn đã đăng kí! Bạn đã được xác nhận và đăng nhập.',\n\n    // Login auto-initiation\n    'auto_init_starting' => 'Đang thử đăng nhập',\n    'auto_init_starting_desc' => 'Chúng tôi đang liên lạc với hệ thống xác thực của bạn để bắt đầu quá trình đăng nhập. Nếu sau 5 giây không có tiến triển, bạn có thể thử nhấp vào liên kết dưới đây.',\n    'auto_init_start_link' => 'Tiến hành xác thực',\n\n    // Password Reset\n    'reset_password' => 'Đặt lại mật khẩu',\n    'reset_password_send_instructions' => 'Nhập email vào ô dưới đây và bạn sẽ nhận được một email với liên kết để đặt lại mật khẩu.',\n    'reset_password_send_button' => 'Gửi liên kết đặt lại mật khẩu',\n    'reset_password_sent' => 'Một đường dẫn đặt lại mật khẩu sẽ được gửi tới :email nếu địa chỉ email đó tồn tại trong hệ thống.',\n    'reset_password_success' => 'Mật khẩu đã được đặt lại thành công.',\n    'email_reset_subject' => 'Đặt lại mật khẩu của :appName',\n    'email_reset_text' => 'Bạn nhận được email này bởi vì chúng tôi nhận được một yêu cầu đặt lại mật khẩu cho tài khoản của bạn.',\n    'email_reset_not_requested' => 'Nếu bạn không yêu cầu đặt lại mật khẩu, không cần có bất cứ hành động nào khác.',\n\n    // Email Confirmation\n    'email_confirm_subject' => 'Xác nhận email trên :appName',\n    'email_confirm_greeting' => 'Cảm ơn bạn đã tham gia :appName!',\n    'email_confirm_text' => 'Xin hãy xác nhận địa chỉa email bằng cách bấm vào nút dưới đây:',\n    'email_confirm_action' => 'Xác nhận Email',\n    'email_confirm_send_error' => 'Email xác nhận cần gửi nhưng hệ thống đã không thể gửi được email. Liên hệ với quản trị viên để chắc chắn email được thiết lập đúng.',\n    'email_confirm_success' => 'Email của bạn đã được xác nhận! Bạn có thể đăng nhập với email này ngay bây giờ.',\n    'email_confirm_resent' => 'Email xác nhận đã được gửi lại, Vui lòng kiểm tra hộp thư.',\n    'email_confirm_thanks' => 'Cảm ơn bạn đã xác nhận!',\n    'email_confirm_thanks_desc' => 'Vui lòng chờ trong giây lát khi yêu cầu xác nhận của bạn được xử lý. Nếu sau 3 giây bạn không được chuyển hướng, nhấp vào liên kết \"Tiếp tục\" dưới đây để tiếp tục.',\n\n    'email_not_confirmed' => 'Địa chỉ email chưa được xác nhận',\n    'email_not_confirmed_text' => 'Địa chỉ email của bạn hiện vẫn chưa được xác nhận.',\n    'email_not_confirmed_click_link' => 'Vui lòng bấm vào liên kết trong email được gửi trong thời gian ngắn ngay sau khi bạn đăng kí.',\n    'email_not_confirmed_resend' => 'Nếu bạn không tìm thấy e-mail bạn có thể yêu cầu gửi lại e-mail xác nhận bằng cách gửi mẫu dưới đây.',\n    'email_not_confirmed_resend_button' => 'Gửi lại e-mail xác nhận',\n\n    // User Invite\n    'user_invite_email_subject' => 'Bạn được mời tham gia :appName!',\n    'user_invite_email_greeting' => 'Một tài khoản đã được tạo dành cho bạn trên :appName.',\n    'user_invite_email_text' => 'Bấm vào nút dưới đây để đặt lại mật khẩu tài khoản và lấy quyền truy cập:',\n    'user_invite_email_action' => 'Đặt mật khẩu tài khoản',\n    'user_invite_page_welcome' => 'Chào mừng đến với :appName!',\n    'user_invite_page_text' => 'Để hoàn tất tài khoản và lấy quyền truy cập bạn cần đặt mật khẩu để sử dụng cho các lần đăng nhập sắp tới tại :appName.',\n    'user_invite_page_confirm_button' => 'Xác nhận Mật khẩu',\n    'user_invite_success_login' => 'Đã đặt mật khẩu, bạn có thể đăng nhập với mật khẩu trên để truy cập :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => 'Cài đặt xác thực nhiều bước',\n    'mfa_setup_desc' => 'Cài đặt xác thực nhiều bước như một lớp bảo mật khác cho tài khoản của bạn.',\n    'mfa_setup_configured' => 'Đã cài đặt',\n    'mfa_setup_reconfigure' => 'Cài đặt lại',\n    'mfa_setup_remove_confirmation' => 'Bạn có chắc muốn gỡ bỏ phương thức xác thực nhiều bước này?',\n    'mfa_setup_action' => 'Cài đặt',\n    'mfa_backup_codes_usage_limit_warning' => 'Bạn có ít hơn 5 mã dự phòng, Xin vui lòng tạo và lưu trữ bộ mã mới trước khi bạn dùng hết mã để tránh việc bị khóa quyền truy cập tài khoản.',\n    'mfa_option_totp_title' => 'Ứng dụng di động',\n    'mfa_option_totp_desc' => 'Để sử dụng xác thực đa lớp bạn cần ưng dụng trên điện thoại có hỗ trợ TOTP như Google Authenticator, Authy hoặc Microsoft Authenticator.',\n    'mfa_option_backup_codes_title' => 'Mã dự phòng',\n    'mfa_option_backup_codes_desc' => 'Tạo một bộ mã dự phòng sử dụng một lần mà bạn sẽ nhập khi đăng nhập để xác minh danh tính của mình. Hãy đảm bảo cất giữ những thứ này ở nơi an toàn và bảo mật.',\n    'mfa_gen_confirm_and_enable' => 'Xác nhận và Mở',\n    'mfa_gen_backup_codes_title' => 'Cài đặt Mã dự phòng',\n    'mfa_gen_backup_codes_desc' => 'Lưu trữ các mã dưới đây ở một nơi an toàn. Khi truy cập vào hệ thống bạn sẽ có thể sử dụng được một trong các đoạn mã đó như là một phương thức xác thực dự phòng.',\n    'mfa_gen_backup_codes_download' => 'Tải mã',\n    'mfa_gen_backup_codes_usage_warning' => 'Mỗi mã chỉ có thể sử dụng một lần',\n    'mfa_gen_totp_title' => 'Cài đặt ứng dụng di động',\n    'mfa_gen_totp_desc' => 'Để sử dụng xác thực nhiều bước, bạn cần một ứng dụng di động hỗ trợ TOTP ví dụ như Google Authenticator, Authy hoặc Microsoft Authenticator.',\n    'mfa_gen_totp_scan' => 'Quét mã QR dưới đây bằng ứng dụng xác thực mà bạn muốn để bắt đầu.',\n    'mfa_gen_totp_verify_setup' => 'Xác nhận cài đặt',\n    'mfa_gen_totp_verify_setup_desc' => 'Xác nhận rằng tất cả hoạt động bằng cách nhập vào một mã, được tạo ra bởi ứng dụng xác thực của bạn vào ô dưới đây:',\n    'mfa_gen_totp_provide_code_here' => 'Cung cấp mã bạn tạo được từ ứng dụng ở đây',\n    'mfa_verify_access' => 'Xác thực truy cập',\n    'mfa_verify_access_desc' => 'Tài khoản của bạn cần xác nhận danh tính của bạn thông qua một lớp xác thực bổ sung trước khi bạn được cấp quyền truy cập. Xác thực qua việc sử dụng một trong các phương thức để tiếp tục.',\n    'mfa_verify_no_methods' => 'Không có phương pháp nào được cấu hình',\n    'mfa_verify_no_methods_desc' => 'Tài khoản của bạn chưa đăng ký xác thực nhiều lớp. Bạn cần thiết lập ít nhất một phương pháp trước khi yêu cầu truy cập.',\n    'mfa_verify_use_totp' => 'Xác thực sử dụng mã di động',\n    'mfa_verify_use_backup_codes' => 'Xác thực sử dụng mã backup',\n    'mfa_verify_backup_code' => 'Mã dự phòng',\n    'mfa_verify_backup_code_desc' => 'Nhập một trong các mã dự phòng còn lại của bạn vào ô phía dưới:',\n    'mfa_verify_backup_code_enter_here' => 'Nhập mã xác thực của bạn tại đây',\n    'mfa_verify_totp_desc' => 'Nhập mã do ứng dụng di động của bạn tạo ra vào dưới đây:',\n    'mfa_setup_login_notification' => 'Đã cài đặt xác thực nhiều bước, bạn vui lòng đăng nhập lại sử dụng phương thức đã cài đặt.',\n];\n"
  },
  {
    "path": "lang/vi/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => 'Huỷ',\n    'close' => 'Đóng',\n    'confirm' => 'Xác nhận',\n    'back' => 'Quay lại',\n    'save' => 'Lưu',\n    'continue' => 'Tiếp tục',\n    'select' => 'Chọn',\n    'toggle_all' => 'Bật/tắt tất cả',\n    'more' => 'Thêm',\n\n    // Form Labels\n    'name' => 'Tên',\n    'description' => 'Mô tả',\n    'role' => 'Vai trò',\n    'cover_image' => 'Ảnh bìa',\n    'cover_image_description' => 'Hình ảnh này phải có kích thước khoảng 440x250px mặc dù nó sẽ được thu nhỏ & cắt xén linh hoạt để phù hợp với giao diện người dùng trong các trường hợp khác nhau theo yêu cầu, do đó kích thước thực tế để hiển thị sẽ khác nhau.',\n\n    // Actions\n    'actions' => 'Hành động',\n    'view' => 'Xem',\n    'view_all' => 'Xem tất cả',\n    'new' => 'Mới',\n    'create' => 'Tạo',\n    'update' => 'Cập nhật',\n    'edit' => 'Sửa',\n    'archive' => 'Lưu trữ',\n    'unarchive' => 'Bỏ lưu trữ',\n    'sort' => 'Sắp xếp',\n    'move' => 'Di chuyển',\n    'copy' => 'Sao chép',\n    'reply' => 'Trả lời',\n    'delete' => 'Xóa',\n    'delete_confirm' => 'Xác nhận Xóa',\n    'search' => 'Tìm kiếm',\n    'search_clear' => 'Xoá tìm kiếm',\n    'reset' => 'Thiết lập lại',\n    'remove' => 'Xóa bỏ',\n    'add' => 'Thêm',\n    'configure' => 'Cấu hình',\n    'manage' => 'Quản lý',\n    'fullscreen' => 'Toàn màn hình',\n    'favourite' => 'Yêu thích',\n    'unfavourite' => 'Bỏ yêu thích',\n    'next' => 'Tiếp theo',\n    'previous' => 'Trước đó',\n    'filter_active' => 'Bộ lọc có hiệu lực:',\n    'filter_clear' => 'Xóa bộ lọc',\n    'download' => 'Tải về',\n    'open_in_tab' => 'Mở trong thẻ mới',\n    'open' => 'Mở',\n\n    // Sort Options\n    'sort_options' => 'Tùy Chọn Sắp Xếp',\n    'sort_direction_toggle' => 'Đảo chiều sắp xếp',\n    'sort_ascending' => 'Sắp xếp tăng dần',\n    'sort_descending' => 'Sắp xếp giảm dần',\n    'sort_name' => 'Tên',\n    'sort_default' => 'Mặc định',\n    'sort_created_at' => 'Ngày Tạo',\n    'sort_updated_at' => 'Ngày cập nhật',\n\n    // Misc\n    'deleted_user' => 'Người dùng bị xóa',\n    'no_activity' => 'Không có hoạt động nào',\n    'no_items' => 'Không có mục nào khả dụng',\n    'back_to_top' => 'Lên đầu trang',\n    'skip_to_main_content' => 'Nhảy đến nội dung chính',\n    'toggle_details' => 'Bật/tắt chi tiết',\n    'toggle_thumbnails' => 'Bật/tắt ảnh ảnh nhỏ',\n    'details' => 'Chi tiết',\n    'grid_view' => 'Hiển thị dạng lưới',\n    'list_view' => 'Hiển thị dạng danh sách',\n    'default' => 'Mặc định',\n    'breadcrumb' => 'Đường dẫn liên kết',\n    'status' => 'Trạng thái',\n    'status_active' => 'Hoạt động',\n    'status_inactive' => 'Không hoạt động',\n    'never' => 'Không bao giờ',\n    'none' => 'Không',\n\n    // Header\n    'homepage' => 'Trang chủ',\n    'header_menu_expand' => 'Mở rộng Header Menu',\n    'profile_menu' => 'Menu Hồ sơ',\n    'view_profile' => 'Xem Hồ sơ',\n    'edit_profile' => 'Sửa Hồ sơ',\n    'dark_mode' => 'Chế độ Tối',\n    'light_mode' => 'Chế độ Sáng',\n    'global_search' => 'Tìm kiếm toàn trang',\n\n    // Layout tabs\n    'tab_info' => 'Thông tin',\n    'tab_info_label' => 'Tab: Hiển thị thông tin phụ',\n    'tab_content' => 'Nội dung',\n    'tab_content_label' => 'Tab: Hiển thị nội dung chính',\n\n    // Email Content\n    'email_action_help' => 'Nếu bạn gặp sự cố khi nhấp vào nút \": actionText\", hãy sao chép và dán URL bên dưới\nvào trình duyệt web của bạn:',\n    'email_rights' => 'Bản quyền đã được bảo hộ',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => 'Chính Sách Quyền Riêng Tư',\n    'terms_of_service' => 'Điều khoản Dịch vụ',\n\n    // OpenSearch\n    'opensearch_description' => 'Tìm kiếm :appName',\n];\n"
  },
  {
    "path": "lang/vi/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => 'Chọn Ảnh',\n    'image_list' => 'Danh sách hình ảnh',\n    'image_details' => 'Chi tiết hình ảnh',\n    'image_upload' => 'Tải ảnh lên',\n    'image_intro' => 'Bạn có thể lựa chọn và quản lý các hình ảnh đã được tải lên hệ thống từ trước ở đây.',\n    'image_intro_upload' => 'Tải lên ảnh mới bằng cách kéo và thả nó vào cửa sổ này, hoặc sử dụng nút tải ảnh ở bên trên.',\n    'image_all' => 'Tất cả',\n    'image_all_title' => 'Xem tất cả các ảnh',\n    'image_book_title' => 'Xem các ảnh đã được tải lên trong sách này',\n    'image_page_title' => 'Xem các ảnh đã được tải lên trong trang này',\n    'image_search_hint' => 'Tìm kiếm ảnh bằng tên',\n    'image_uploaded' => 'Đã tải lên :uploadedDate',\n    'image_uploaded_by' => 'Tải lên bởi :userName',\n    'image_uploaded_to' => 'Đã tải lên :pageLink',\n    'image_updated' => 'Đã cập nhật :updateDate',\n    'image_load_more' => 'Hiện thêm',\n    'image_image_name' => 'Tên Ảnh',\n    'image_delete_used' => 'Ảnh này được sử dụng trong các trang dưới đây.',\n    'image_delete_confirm_text' => 'Bạn có chắc chắn muốn xóa hình ảnh này?',\n    'image_select_image' => 'Chọn Ảnh',\n    'image_dropzone' => 'Thả các ảnh hoặc bấm vào đây để tải lên',\n    'image_dropzone_drop' => 'Kéo các tệp vào đây để tải lên',\n    'images_deleted' => 'Các ảnh đã được xóa',\n    'image_preview' => 'Xem trước Ảnh',\n    'image_upload_success' => 'Ảnh đã tải lên thành công',\n    'image_update_success' => 'Chi tiết ảnh được cập nhật thành công',\n    'image_delete_success' => 'Ảnh đã được xóa thành công',\n    'image_replace' => 'Thay thế hình ảnh',\n    'image_replace_success' => 'Đã cập nhật thành công tệp hình ảnh',\n    'image_rebuild_thumbs' => 'Tái tạo các biến thể kích thước',\n    'image_rebuild_thumbs_success' => 'Các biến thể kích thước hình ảnh được xây dựng lại thành công!',\n\n    // Code Editor\n    'code_editor' => 'Sửa Mã',\n    'code_language' => 'Ngôn ngữ Mã',\n    'code_content' => 'Nội dung Mã',\n    'code_session_history' => 'Lịch sử Phiên',\n    'code_save' => 'Lưu Mã',\n];\n"
  },
  {
    "path": "lang/vi/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => 'Tổng quát',\n    'advanced' => 'Nâng cao',\n    'none' => 'Không',\n    'cancel' => 'Hủy',\n    'save' => 'Lưu',\n    'close' => 'Đóng',\n    'apply' => 'Áp dụng',\n    'undo' => 'Hoàn tác',\n    'redo' => 'Làm lại',\n    'left' => 'Bên trái',\n    'center' => 'Chính giữa',\n    'right' => 'Bên phải',\n    'top' => 'Trên cùng',\n    'middle' => 'Giữa',\n    'bottom' => 'Dưới cùng',\n    'width' => 'Chiều rộng',\n    'height' => 'Chiều cao',\n    'More' => 'Thêm',\n    'select' => 'Chọn...',\n\n    // Toolbar\n    'formats' => 'Định dạng',\n    'header_large' => 'Tiêu đề lớn',\n    'header_medium' => 'Tiêu đề trung bình',\n    'header_small' => 'Tiêu đề nhỏ',\n    'header_tiny' => 'Tiêu đề cực nhỏ',\n    'paragraph' => 'Đoạn văn',\n    'blockquote' => 'Trích dẫn',\n    'inline_code' => 'Mã nội tuyến',\n    'callouts' => 'Chú thích',\n    'callout_information' => 'Thông tin',\n    'callout_success' => 'Thành công',\n    'callout_warning' => 'Cảnh báo',\n    'callout_danger' => 'Nguy hiểm',\n    'bold' => 'In đậm',\n    'italic' => 'In nghiêng',\n    'underline' => 'Gạch chân',\n    'strikethrough' => 'Gạch ngang',\n    'superscript' => 'Chỉ số trên',\n    'subscript' => 'Chỉ số dưới',\n    'text_color' => 'Màu chữ',\n    'highlight_color' => 'Màu đánh dấu',\n    'custom_color' => 'Màu tùy chỉnh',\n    'remove_color' => 'Xóa màu',\n    'background_color' => 'Màu nền',\n    'align_left' => 'Canh lề trái',\n    'align_center' => 'Căn giữa',\n    'align_right' => 'Căn lề phải',\n    'align_justify' => 'Căn đều',\n    'list_bullet' => 'Danh sách kiểu ký hiệu',\n    'list_numbered' => 'Danh sách kiểu số',\n    'list_task' => 'Danh sách tác vụ',\n    'indent_increase' => 'Tăng thụt lề',\n    'indent_decrease' => 'Giảm thụt lề',\n    'table' => 'Bảng',\n    'insert_image' => 'Chèn hình ảnh',\n    'insert_image_title' => 'Chèn/Sửa hình ảnh',\n    'insert_link' => 'Chèn/sửa liên kết',\n    'insert_link_title' => 'Chèn/sửa liên kết',\n    'insert_horizontal_line' => 'Chèn đường ngang',\n    'insert_code_block' => 'Chèn khối mã',\n    'edit_code_block' => 'Chỉnh sửa khối mã',\n    'insert_drawing' => 'Chèn/chỉnh sửa bản vẽ',\n    'drawing_manager' => 'Quản lý hình vẽ',\n    'insert_media' => 'Chèn/chỉnh sửa media',\n    'insert_media_title' => 'Chèn/chỉnh sửa media',\n    'clear_formatting' => 'Xóa định dạng',\n    'source_code' => 'Mã nguồn',\n    'source_code_title' => 'Mã Nguồn',\n    'fullscreen' => 'Toàn màn hình',\n    'image_options' => 'Tùy chọn hình ảnh',\n\n    // Tables\n    'table_properties' => 'Thuộc tính bảng',\n    'table_properties_title' => 'Thuộc tính bảng',\n    'delete_table' => 'Xóa bảng',\n    'table_clear_formatting' => 'Xóa định dạng bảng',\n    'resize_to_contents' => 'Thay đổi kích thước theo nội dung',\n    'row_header' => 'Tiêu đề hàng',\n    'insert_row_before' => 'Chèn thêm hàng ở trên',\n    'insert_row_after' => 'Chèn thêm hàng ở dưới',\n    'delete_row' => 'Xóa hàng',\n    'insert_column_before' => 'Chèn cột mới vào bên trái',\n    'insert_column_after' => 'Chèn cột mới vào bên phải',\n    'delete_column' => 'Xóa cột',\n    'table_cell' => 'Ô',\n    'table_row' => 'Hàng',\n    'table_column' => 'Cột',\n    'cell_properties' => 'Đặt thuộc tính ô',\n    'cell_properties_title' => 'Đặt thuộc tính ô',\n    'cell_type' => 'Kiểu ô',\n    'cell_type_cell' => 'Ô',\n    'cell_scope' => 'Phạm vi',\n    'cell_type_header' => 'Ô tiêu đề',\n    'merge_cells' => 'Sát nhập các ô',\n    'split_cell' => 'Chia tách ô',\n    'table_row_group' => 'Nhóm hàng',\n    'table_column_group' => 'Nhóm cột',\n    'horizontal_align' => 'Căn chỉnh theo chiều ngang',\n    'vertical_align' => 'Căn chỉnh theo chiều dọc',\n    'border_width' => 'Chiều rộng viền',\n    'border_style' => 'Kiểu đường viền',\n    'border_color' => 'Màu viền',\n    'row_properties' => 'Đặt thuộc tính hàng',\n    'row_properties_title' => 'Đặt thuộc tính hàng',\n    'cut_row' => 'Cắt hàng',\n    'copy_row' => 'Sao chép hàng',\n    'paste_row_before' => 'Dán hàng vào bên trên',\n    'paste_row_after' => 'Dán hàng vào bên dưới',\n    'row_type' => 'Kiểu hàng',\n    'row_type_header' => 'Tiêu đề',\n    'row_type_body' => 'Nội dung',\n    'row_type_footer' => 'Chân trang',\n    'alignment' => 'Canh lề',\n    'cut_column' => 'Cắt cột',\n    'copy_column' => 'Sao chép cột',\n    'paste_column_before' => 'Dán cột vào bên trái',\n    'paste_column_after' => 'Dán cột vào bên phải',\n    'cell_padding' => 'Đệm ô',\n    'cell_spacing' => 'Khoảng cách ô',\n    'caption' => 'Chú thích',\n    'show_caption' => 'Hiện chú thích',\n    'constrain' => 'Ràng buộc các thuộc tính',\n    'cell_border_solid' => 'Đặc',\n    'cell_border_dotted' => 'Chấm chấm',\n    'cell_border_dashed' => 'Nét đứt',\n    'cell_border_double' => 'Đôi',\n    'cell_border_groove' => 'Rãnh',\n    'cell_border_ridge' => 'Gờ',\n    'cell_border_inset' => 'Lõm',\n    'cell_border_outset' => 'Lồi',\n    'cell_border_none' => 'Không',\n    'cell_border_hidden' => 'Ẩn',\n\n    // Images, links, details/summary & embed\n    'source' => 'Nguồn',\n    'alt_desc' => 'Mô tả thay thế',\n    'embed' => 'Mã nhúng',\n    'paste_embed' => 'Dán mã nhúng của bạn vào bên dưới:',\n    'url' => 'Đường dẫn',\n    'text_to_display' => 'Văn bản hiển thị',\n    'title' => 'Tiêu đề',\n    'browse_links' => 'Duyệt liên kết',\n    'open_link' => 'Mở liên kết',\n    'open_link_in' => 'Mở liên kết trong...',\n    'open_link_current' => 'Cửa sổ hiện tại',\n    'open_link_new' => 'Cửa sổ mới',\n    'remove_link' => 'Loại bỏ liên kết',\n    'insert_collapsible' => 'Chèn khối có thể thu gọn',\n    'collapsible_unwrap' => 'Tháo bỏ',\n    'edit_label' => 'Chỉnh sửa nhãn',\n    'toggle_open_closed' => 'Chuyển đổi mở/đóng',\n    'collapsible_edit' => 'Chỉnh sửa khối có thể thu gọn',\n    'toggle_label' => 'Chuyển đổi nhãn',\n\n    // About view\n    'about' => 'Giới thiệu về trình soạn thảo',\n    'about_title' => 'Giới thiệu về trình soạn thảo WYSIWYG',\n    'editor_license' => 'Giấy phép & Bản quyền của trình soạn thảo',\n    'editor_lexical_license' => 'Trình soạn thảo này được xây dựng dựa trên :lexicalLink được phân phối theo giấy phép MIT.',\n    'editor_lexical_license_link' => 'Chi tiết giấy phép đầy đủ có thể tìm thấy tại đây.',\n    'editor_tiny_license' => 'Trình soạn thảo này được xây dựng bằng cách sử dụng :tinyLink theo giấy phép MIT.',\n    'editor_tiny_license_link' => 'Chi tiết về bản quyền và giấy phép của TinyMCE có thể được tìm thấy tại đây.',\n    'save_continue' => 'Lưu trang & Tiếp tục',\n    'callouts_cycle' => '(Nhấn tiếp để chuyển đổi giữa các loại)',\n    'link_selector' => 'Bộ chọn liên kết',\n    'shortcuts' => 'Phím tắt',\n    'shortcut' => 'Phím tắt',\n    'shortcuts_intro' => 'Các phím tắt sau có sẵn trong trình soạn thảo:',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => 'Mô tả',\n];\n"
  },
  {
    "path": "lang/vi/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => 'Được tạo gần đây',\n    'recently_created_pages' => 'Trang được tạo gần đây',\n    'recently_updated_pages' => 'Trang được cập nhật gần đây',\n    'recently_created_chapters' => 'Chương được tạo gần đây',\n    'recently_created_books' => 'Sách được tạo gần đây',\n    'recently_created_shelves' => 'Giá sách được tạo gần đây',\n    'recently_update' => 'Được cập nhật gần đây',\n    'recently_viewed' => 'Được xem gần đây',\n    'recent_activity' => 'Hoạt động gần đây',\n    'create_now' => 'Tạo ngay',\n    'revisions' => 'Phiên bản',\n    'meta_revision' => 'Phiên bản #:revisionCount',\n    'meta_created' => 'Được tạo :timeLength',\n    'meta_created_name' => 'Được tạo :timeLength bởi :user',\n    'meta_updated' => 'Được cập nhật :timeLength',\n    'meta_updated_name' => 'Được cập nhật :timeLength bởi :user',\n    'meta_owned_name' => 'Được sở hữu bởi :user',\n    'meta_reference_count' => 'Được tham chiếu bởi :count mục|Được tham chiếu bởi :count mục',\n    'entity_select' => 'Chọn thực thể',\n    'entity_select_lack_permission' => 'Bạn không có quyền để chọn mục này',\n    'images' => 'Ảnh',\n    'my_recent_drafts' => 'Bản nháp gần đây của tôi',\n    'my_recently_viewed' => 'Xem gần đây của tôi',\n    'my_most_viewed_favourites' => 'Yêu thích được tôi xem nhiều nhất',\n    'my_favourites' => 'Danh sách yêu thích của tôi',\n    'no_pages_viewed' => 'Bạn chưa xem bất cứ trang nào',\n    'no_pages_recently_created' => 'Không có trang nào được tạo gần đây',\n    'no_pages_recently_updated' => 'Không có trang nào được cập nhật gần đây',\n    'export' => 'Xuất',\n    'export_html' => 'Đang chứa tệp tin Web',\n    'export_pdf' => 'Tệp PDF',\n    'export_text' => 'Tệp văn bản thuần túy',\n    'export_md' => 'Tệp Markdown',\n    'export_zip' => 'ZIP di động',\n    'default_template' => 'Mẫu trang mặc định',\n    'default_template_explain' => 'Chỉ định một mẫu trang sẽ được sử dụng làm nội dung mặc định cho tất cả các trang được tạo trong mục này. Hãy chú ý rằng điều này sẽ chỉ được sử dụng nếu người tạo trang có quyền truy cập xem vào trang mẫu đã chọn.',\n    'default_template_select' => 'Chọn một trang mẫu',\n    'import' => 'Nhập',\n    'import_validate' => 'Xác thực nhập',\n    'import_desc' => 'Nhập sách, chương & trang bằng cách sử dụng tệp xuất zip di động từ cùng một phiên bản hoặc một phiên bản khác. Chọn tệp ZIP để tiếp tục. Sau khi tệp được tải lên và xác thực, bạn sẽ có thể cấu hình & xác nhận việc nhập trong chế độ xem tiếp theo.',\n    'import_zip_select' => 'Chọn tệp ZIP để tải lên',\n    'import_zip_validation_errors' => 'Đã phát hiện lỗi khi xác thực tệp ZIP được cung cấp:',\n    'import_pending' => 'Nhập đang chờ xử lý',\n    'import_pending_none' => 'Chưa có lượt nhập nào được bắt đầu.',\n    'import_continue' => 'Tiếp tục nhập',\n    'import_continue_desc' => 'Xem lại nội dung sẽ được nhập từ tệp ZIP đã tải lên. Khi sẵn sàng, hãy chạy nhập để thêm nội dung của nó vào hệ thống này. Tệp nhập ZIP đã tải lên sẽ tự động bị xóa khi nhập thành công.',\n    'import_details' => 'Chi tiết nhập',\n    'import_run' => 'Chạy nhập',\n    'import_size' => 'Kích thước tệp ZIP nhập: :size',\n    'import_uploaded_at' => 'Đã tải lên :relativeTime',\n    'import_uploaded_by' => 'Đã tải lên bởi',\n    'import_location' => 'Vị trí nhập',\n    'import_location_desc' => 'Chọn vị trí đích cho nội dung đã nhập của bạn. Bạn sẽ cần các quyền liên quan để tạo trong vị trí bạn chọn.',\n    'import_delete_confirm' => 'Bạn có chắc chắn muốn xóa lượt nhập này không?',\n    'import_delete_desc' => 'Thao tác này sẽ xóa tệp ZIP đã tải lên, và không thể hoàn tác.',\n    'import_errors' => 'Lỗi khi nhập dữ liệu',\n    'import_errors_desc' => 'Các lỗi sau đã xảy ra trong quá trình nhập:',\n    'breadcrumb_siblings_for_page' => 'Điều hướng anh chị em cho trang',\n    'breadcrumb_siblings_for_chapter' => 'Điều hướng anh chị em cho chương',\n    'breadcrumb_siblings_for_book' => 'Điều hướng anh chị em cho sách',\n    'breadcrumb_siblings_for_bookshelf' => 'Điều hướng anh chị em cho kệ sách',\n\n    // Permissions and restrictions\n    'permissions' => 'Quyền',\n    'permissions_desc' => 'Đặt quyền ở đây để ghi đè các quyền mặc định do vai trò người dùng cung cấp.',\n    'permissions_book_cascade' => 'Quyền được đặt trên sách sẽ tự động xếp tầng cho các chương và trang con, trừ khi chúng được xác định quyền riêng.',\n    'permissions_chapter_cascade' => 'Quyền được đặt trên các chương sẽ tự động xếp tầng cho các trang con, trừ khi chúng được xác định quyền riêng.',\n    'permissions_save' => 'Lưu quyền hạn',\n    'permissions_owner' => 'Chủ sở hữu',\n    'permissions_role_everyone_else' => 'Những người khác',\n    'permissions_role_everyone_else_desc' => 'Đặt quyền cho tất cả vai trò không được ghi đè cụ thể.',\n    'permissions_role_override' => 'Ghi đè quyền cho vai trò',\n    'permissions_inherit_defaults' => 'Kế thừa giá trị mặc định',\n\n    // Search\n    'search_results' => 'Kết quả Tìm kiếm',\n    'search_total_results_found' => 'Tìm thấy :count kết quả|:count tổng kết quả',\n    'search_clear' => 'Xóa tìm kiếm',\n    'search_no_pages' => 'Không trang nào khớp với tìm kiếm này',\n    'search_for_term' => 'Tìm kiếm cho :term',\n    'search_more' => 'Thêm kết quả',\n    'search_advanced' => 'Tìm kiếm Nâng cao',\n    'search_terms' => 'Cụm từ Tìm kiếm',\n    'search_content_type' => 'Kiểu Nội dung',\n    'search_exact_matches' => 'Hoàn toàn trùng khớp',\n    'search_tags' => 'Tìm kiếm Thẻ',\n    'search_options' => 'Tùy chọn',\n    'search_viewed_by_me' => 'Được xem bởi tôi',\n    'search_not_viewed_by_me' => 'Không được xem bởi tôi',\n    'search_permissions_set' => 'Phân quyền',\n    'search_created_by_me' => 'Được tạo bởi tôi',\n    'search_updated_by_me' => 'Được cập nhật bởi tôi',\n    'search_owned_by_me' => 'Của tôi',\n    'search_date_options' => 'Tùy chọn ngày',\n    'search_updated_before' => 'Đã được cập nhật trước đó',\n    'search_updated_after' => 'Đã được cập nhật sau',\n    'search_created_before' => 'Đã được tạo trước',\n    'search_created_after' => 'Đã được tạo sau',\n    'search_set_date' => 'Đặt ngày',\n    'search_update' => 'Cập nhật tìm kiếm',\n\n    // Shelves\n    'shelf' => 'Giá sách',\n    'shelves' => 'Giá sách',\n    'x_shelves' => ':count Giá sách|:count Giá sách',\n    'shelves_empty' => 'Không có giá sách nào được tạo',\n    'shelves_create' => 'Tạo Giá sách mới',\n    'shelves_popular' => 'Các Giá sách phổ biến',\n    'shelves_new' => 'Các Giá sách mới',\n    'shelves_new_action' => 'Giá sách mới',\n    'shelves_popular_empty' => 'Các giá sách phổ biến sẽ xuất hiện ở đây.',\n    'shelves_new_empty' => 'Các Giá sách được tạo gần đây sẽ xuất hiện ở đây.',\n    'shelves_save' => 'Lưu Giá sách',\n    'shelves_books' => 'Sách trên Giá sách này',\n    'shelves_add_books' => 'Thêm sách vào Giá sách này',\n    'shelves_drag_books' => 'Kéo sách bên dưới để thêm vào kệ sách này',\n    'shelves_empty_contents' => 'Giá sách này không có sách nào',\n    'shelves_edit_and_assign' => 'Chỉnh sửa kệ để gán sách',\n    'shelves_edit_named' => 'Chỉnh sửa kệ :name',\n    'shelves_edit' => 'Chỉnh sửa kệ',\n    'shelves_delete' => 'Xóa kệ',\n    'shelves_delete_named' => 'Xóa kệ :name',\n    'shelves_delete_explain' => \"Thao tác này sẽ xóa kệ có tên ':name'. Sách chứa sẽ không bị xóa.\",\n    'shelves_delete_confirmation' => 'Bạn có chắc chắn muốn xóa kệ sách này không?',\n    'shelves_permissions' => 'Quyền cho kệ sách',\n    'shelves_permissions_updated' => 'Quyền cho kệ sách đã được cập nhật',\n    'shelves_permissions_active' => 'Quyền của kệ đang hoạt động',\n    'shelves_permissions_cascade_warning' => 'Quyền trên kệ sách không tự động xếp theo các sách được chứa. Điều này là do một cuốn sách có thể tồn tại trên nhiều kệ. Tuy nhiên, quyền có thể được sao chép xuống sách con bằng cách sử dụng tùy chọn bên dưới.',\n    'shelves_permissions_create' => 'Quyền tạo giá sách chỉ được sử dụng để sao chép quyền vào sách con bằng cách sử dụng tác vụ bên dưới. Chúng không kiểm soát khả năng tạo ra sách.',\n    'shelves_copy_permissions_to_books' => 'Sao chép các quyền cho sách',\n    'shelves_copy_permissions' => 'Sao chép các quyền',\n    'shelves_copy_permissions_explain' => 'Thao tác này sẽ áp dụng cài đặt quyền hiện tại của giá sách này cho tất cả sách có trong đó. Trước khi kích hoạt, hãy đảm bảo mọi thay đổi đối với quyền của giá sách này đã được lưu.',\n    'shelves_copy_permission_success' => 'Đã sao chép quyền của kệ vào :count sách',\n\n    // Books\n    'book' => 'Sách',\n    'books' => 'Tất cả sách',\n    'x_books' => ':count Sách|:count Tất cả sách',\n    'books_empty' => 'Không có cuốn sách nào được tạo',\n    'books_popular' => 'Những cuốn sách phổ biến',\n    'books_recent' => 'Những cuốn sách gần đây',\n    'books_new' => 'Những cuốn sách mới',\n    'books_new_action' => 'Sách mới',\n    'books_popular_empty' => 'Những cuốn sách phổ biến nhất sẽ xuất hiện ở đây.',\n    'books_new_empty' => 'Những cuốn sách tạo gần đây sẽ được xuất hiện ở đây.',\n    'books_create' => 'Tạo cuốn sách mới',\n    'books_delete' => 'Xóa sách',\n    'books_delete_named' => 'Xóa sách :bookName',\n    'books_delete_explain' => 'Điều này sẽ xóa cuốn sách với tên \\':bookName\\'. Tất cả các trang và các chương sẽ bị xóa.',\n    'books_delete_confirmation' => 'Bạn có chắc chắn muốn xóa cuốn sách này?',\n    'books_edit' => 'Sửa sách',\n    'books_edit_named' => 'Sửa sách :bookName',\n    'books_form_book_name' => 'Tên sách',\n    'books_save' => 'Lưu sách',\n    'books_permissions' => 'Các quyền của cuốn sách',\n    'books_permissions_updated' => 'Các quyền của cuốn sách đã được cập nhật',\n    'books_empty_contents' => 'Không có trang hay chương nào được tạo cho cuốn sách này.',\n    'books_empty_create_page' => 'Tạo một trang mới',\n    'books_empty_sort_current_book' => 'Sắp xếp cuốn sách này',\n    'books_empty_add_chapter' => 'Thêm một chương mới',\n    'books_permissions_active' => 'Đang bật các quyền hạn từ Sách',\n    'books_search_this' => 'Tìm cuốn sách này',\n    'books_navigation' => 'Điều hướng cuốn sách',\n    'books_sort' => 'Sắp xếp nội dung cuốn sách',\n    'books_sort_desc' => 'Di chuyển các chương và trang trong một cuốn sách để sắp xếp lại nội dung của nó. Các sách khác có thể được thêm vào để dễ dàng di chuyển các chương và trang giữa các sách. Tùy chọn, một quy tắc sắp xếp tự động có thể được đặt để tự động sắp xếp nội dung cuốn sách này khi có thay đổi.',\n    'books_sort_auto_sort' => 'Tùy chọn sắp xếp tự động',\n    'books_sort_auto_sort_active' => 'Sắp xếp tự động đang hoạt động: :sortName',\n    'books_sort_named' => 'Sắp xếp sách :bookName',\n    'books_sort_name' => 'Sắp xếp theo tên',\n    'books_sort_created' => 'Sắp xếp theo ngày tạo',\n    'books_sort_updated' => 'Sắp xếp theo ngày cập nhật',\n    'books_sort_chapters_first' => 'Các Chương đầu',\n    'books_sort_chapters_last' => 'Các Chương cuối',\n    'books_sort_show_other' => 'Hiển thị các Sách khác',\n    'books_sort_save' => 'Lưu thứ tự mới',\n    'books_sort_show_other_desc' => 'Thêm các sách khác vào đây để đưa chúng vào thao tác sắp xếp và cho phép sắp xếp lại nhiều sách dễ dàng.',\n    'books_sort_move_up' => 'Đưa lên trên',\n    'books_sort_move_down' => 'Đưa xuống dưới',\n    'books_sort_move_prev_book' => 'Chuyển tới sách phía trước',\n    'books_sort_move_next_book' => 'Chuyển tới sách phía sau',\n    'books_sort_move_prev_chapter' => 'Chuyển sang chương trước',\n    'books_sort_move_next_chapter' => 'Chuyển sang chương tiếp theo',\n    'books_sort_move_book_start' => 'Di chuyển đến đầu sách',\n    'books_sort_move_book_end' => 'Di chuyển đến cuối sách',\n    'books_sort_move_before_chapter' => 'Chuyển về trước chương',\n    'books_sort_move_after_chapter' => 'Chuyển tới chương sau',\n    'books_copy' => 'Sao chép sách',\n    'books_copy_success' => 'Đã sao chép thành công',\n\n    // Chapters\n    'chapter' => 'Chương',\n    'chapters' => 'Các chương',\n    'x_chapters' => ':count Chương|:count Chương',\n    'chapters_popular' => 'Các Chương phổ biến',\n    'chapters_new' => 'Chương mới',\n    'chapters_create' => 'Tạo Chương mới',\n    'chapters_delete' => 'Xóa Chương',\n    'chapters_delete_named' => 'Xóa Chương :chapterName',\n    'chapters_delete_explain' => 'Hành động này sẽ xoá chương \\':chapterName\\'. Tất cả các trang trong chương này cũng sẽ bị xoá.',\n    'chapters_delete_confirm' => 'Bạn có chắc chắn muốn xóa chương này?',\n    'chapters_edit' => 'Sửa Chương',\n    'chapters_edit_named' => 'Sửa chương :chapterName',\n    'chapters_save' => 'Lưu Chương',\n    'chapters_move' => 'Di chuyển Chương',\n    'chapters_move_named' => 'Di chuyển Chương :chapterName',\n    'chapters_copy' => 'Sao chép chương',\n    'chapters_copy_success' => 'Chương đã được sao chép thành công',\n    'chapters_permissions' => 'Quyền hạn Chương',\n    'chapters_empty' => 'Không có trang nào hiện có trong chương này.',\n    'chapters_permissions_active' => 'Đang bật các quyền hạn từ Chương',\n    'chapters_permissions_success' => 'Quyền hạn Chương được cập nhật',\n    'chapters_search_this' => 'Tìm kiếm trong Chương này',\n    'chapter_sort_book' => 'Sắp xếp sách',\n\n    // Pages\n    'page' => 'Trang',\n    'pages' => 'Các trang',\n    'x_pages' => ':count Trang|:count Trang',\n    'pages_popular' => 'Các Trang phổ biến',\n    'pages_new' => 'Trang Mới',\n    'pages_attachments' => 'Các đính kèm',\n    'pages_navigation' => 'Điều hướng Trang',\n    'pages_delete' => 'Xóa Trang',\n    'pages_delete_named' => 'Xóa Trang :pageName',\n    'pages_delete_draft_named' => 'Xóa Trang Nháp :pageName',\n    'pages_delete_draft' => 'Xóa Trang Nháp',\n    'pages_delete_success' => 'Đã xóa Trang',\n    'pages_delete_draft_success' => 'Đã xóa trang Nháp',\n    'pages_delete_warning_template' => 'Trang này đang được sử dụng làm mẫu trang mặc định của sách hoặc chương. Hãy chú ý: Những cuốn sách hoặc chương này sẽ không còn được chỉ định mẫu trang mặc định sau khi trang này bị xóa.',\n    'pages_delete_confirm' => 'Bạn có chắc chắn muốn xóa trang này?',\n    'pages_delete_draft_confirm' => 'Bạn có chắc chắn muốn xóa trang nháp này?',\n    'pages_editing_named' => 'Đang chỉnh sửa Trang :pageName',\n    'pages_edit_draft_options' => 'Tùy chọn bản nháp',\n    'pages_edit_save_draft' => 'Lưu Nháp',\n    'pages_edit_draft' => 'Sửa trang nháp',\n    'pages_editing_draft' => 'Đang chỉnh sửa Nháp',\n    'pages_editing_page' => 'Đang chỉnh sửa Trang',\n    'pages_edit_draft_save_at' => 'Bản nháp đã lưu lúc ',\n    'pages_edit_delete_draft' => 'Xóa Bản nháp',\n    'pages_edit_delete_draft_confirm' => 'Bạn có chắc chắn muốn xóa các thay đổi trên trang nháp của mình không? Tất cả các thay đổi của bạn, kể từ lần lưu đầy đủ gần đây nhất, sẽ bị mất và trình chỉnh sửa sẽ được cập nhật với trạng thái lưu trang không phải bản nháp mới nhất.',\n    'pages_edit_discard_draft' => 'Hủy bỏ Bản nháp',\n    'pages_edit_switch_to_markdown' => 'Chuyển sang trình soạn thảo Markdown',\n    'pages_edit_switch_to_markdown_clean' => '(Nội dung sạch)',\n    'pages_edit_switch_to_markdown_stable' => '(Nội dung ổn định)',\n    'pages_edit_switch_to_wysiwyg' => 'Chuyển sang trình soạn thảo WYSIWYG',\n    'pages_edit_switch_to_new_wysiwyg' => 'Chuyển sang WYSIWYG mới',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '(Đang thử nghiệm Beta)',\n    'pages_edit_set_changelog' => 'Đặt Changelog',\n    'pages_edit_enter_changelog_desc' => 'Viết mô tả ngắn gọn cho các thay đổi mà bạn tạo',\n    'pages_edit_enter_changelog' => 'Viết Changelog',\n    'pages_editor_switch_title' => 'Đổi trình soạn thảo',\n    'pages_editor_switch_are_you_sure' => 'Bạn có chắc chắn muốn thay đổi trình soạn thảo cho trang này không?',\n    'pages_editor_switch_consider_following' => 'Hãy cân nhắc những điều sau đây khi thay đổi trình soạn thảo:',\n    'pages_editor_switch_consideration_a' => 'Sau khi lưu, tùy chọn trình soạn thảo mới sẽ được sử dụng bởi bất kỳ trình chỉnh sửa nào trong tương lai, kể cả những người không thể tự thay đổi loại trình chỉnh sửa.',\n    'pages_editor_switch_consideration_b' => 'Điều này có khả năng dẫn đến mất chi tiết và cú pháp trong một số trường hợp nhất định.',\n    'pages_editor_switch_consideration_c' => 'Các thay đổi thẻ hoặc nhật ký thay đổi, được thực hiện kể từ lần lưu cuối cùng, sẽ không tồn tại sau thay đổi này.',\n    'pages_save' => 'Lưu Trang',\n    'pages_title' => 'Tiêu đề Trang',\n    'pages_name' => 'Tên Trang',\n    'pages_md_editor' => 'Trình chỉnh sửa',\n    'pages_md_preview' => 'Xem trước',\n    'pages_md_insert_image' => 'Chèn hình ảnh',\n    'pages_md_insert_link' => 'Chèn liên kết thực thể',\n    'pages_md_insert_drawing' => 'Chèn bản vẽ',\n    'pages_md_show_preview' => 'Hiển thị bản xem trước',\n    'pages_md_sync_scroll' => 'Đồng bộ hóa cuộn xem trước',\n    'pages_md_plain_editor' => 'Trình soạn thảo văn bản thuần túy',\n    'pages_drawing_unsaved' => 'Tìm thấy bản vẽ chưa lưu',\n    'pages_drawing_unsaved_confirm' => 'Dữ liệu bản vẽ chưa lưu được tìm thấy từ lần lưu bản vẽ không thành công trước đó. Bạn có muốn khôi phục và tiếp tục chỉnh sửa bản vẽ chưa lưu này không?',\n    'pages_not_in_chapter' => 'Trang không nằm trong một chương',\n    'pages_move' => 'Di chuyển Trang',\n    'pages_copy' => 'Sao chép Trang',\n    'pages_copy_desination' => 'Sao lưu đến',\n    'pages_copy_success' => 'Trang được sao chép thành công',\n    'pages_permissions' => 'Quyền hạn Trang',\n    'pages_permissions_success' => 'Quyền hạn Trang được cập nhật',\n    'pages_revision' => 'Phiên bản',\n    'pages_revisions' => 'Phiên bản Trang',\n    'pages_revisions_desc' => 'Dưới đây là tất cả các bản sửa đổi trước đây của trang này. Bạn có thể xem lại, so sánh và khôi phục các phiên bản trang cũ nếu được phép. Lịch sử đầy đủ của trang có thể không được phản ánh đầy đủ ở đây vì, tùy thuộc vào cấu hình hệ thống, các bản sửa đổi cũ có thể tự động bị xóa.',\n    'pages_revisions_named' => 'Phiên bản Trang cho :pageName',\n    'pages_revision_named' => 'Phiên bản Trang cho :pageName',\n    'pages_revision_restored_from' => 'Khôi phục từ #:id; :summary',\n    'pages_revisions_created_by' => 'Tạo bởi',\n    'pages_revisions_date' => 'Ngày của Phiên bản',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => 'Số phiên bản',\n    'pages_revisions_numbered' => 'Phiên bản #:id',\n    'pages_revisions_numbered_changes' => 'Các thay đổi của phiên bản #:id',\n    'pages_revisions_editor' => 'Loại trình chỉnh sửa',\n    'pages_revisions_changelog' => 'Nhật ký thay đổi',\n    'pages_revisions_changes' => 'Các thay đổi',\n    'pages_revisions_current' => 'Phiên bản hiện tại',\n    'pages_revisions_preview' => 'Xem trước',\n    'pages_revisions_restore' => 'Khôi phục',\n    'pages_revisions_none' => 'Trang này không có phiên bản nào',\n    'pages_copy_link' => 'Sao chép Liên kết',\n    'pages_edit_content_link' => 'Chuyển đến phần trong trình chỉnh sửa',\n    'pages_pointer_enter_mode' => 'Vào chế độ chọn phần',\n    'pages_pointer_label' => 'Tùy chọn phần trang',\n    'pages_pointer_permalink' => 'Liên kết cố định phần trang',\n    'pages_pointer_include_tag' => 'Phần trang bao gồm thẻ',\n    'pages_pointer_toggle_link' => 'Chế độ Liên kết cố định, Nhấn để hiển thị thẻ bao gồm',\n    'pages_pointer_toggle_include' => 'Bao gồm chế độ thẻ, Nhấn để hiển thị liên kết cố định',\n    'pages_permissions_active' => 'Đang bật các quyền hạn từ Trang',\n    'pages_initial_revision' => 'Đăng bài mở đầu',\n    'pages_references_update_revision' => 'Hệ thống tự động cập nhật liên kết nội bộ',\n    'pages_initial_name' => 'Trang mới',\n    'pages_editing_draft_notification' => 'Bạn hiện đang chỉnh sửa một bản nháp được lưu cách đây :timeDiff.',\n    'pages_draft_edited_notification' => 'Trang này đã được cập nhật từ lúc đó. Bạn nên loại bỏ bản nháp này.',\n    'pages_draft_page_changed_since_creation' => 'Trang này đã được cập nhật kể từ khi bản nháp này được tạo. Bạn nên bỏ bản nháp này hoặc cẩn thận không ghi đè bất kỳ thay đổi nào của trang.',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count người dùng đang bắt đầu chỉnh sửa trang này',\n        'start_b' => ':userName đang bắt đầu chỉnh sửa trang này',\n        'time_a' => 'kể từ khi trang được cập nhật lần cuối',\n        'time_b' => 'trong :minCount phút cuối',\n        'message' => ':start :time. Hãy cẩn thận đừng ghi đè vào các bản cập nhật của nhau!',\n    ],\n    'pages_draft_discarded' => 'Bản nháp đã bị loại bỏ! Trình chỉnh sửa đã được cập nhật với nội dung trang hiện tại',\n    'pages_draft_deleted' => 'Bản nháp đã bị xóa! Trình chỉnh sửa đã được cập nhật với nội dung trang hiện tại',\n    'pages_specific' => 'Trang cụ thể',\n    'pages_is_template' => 'Biểu mẫu trang',\n\n    // Editor Sidebar\n    'toggle_sidebar' => 'Chuyển đổi thanh bên',\n    'page_tags' => 'Các Thẻ Trang',\n    'chapter_tags' => 'Các Thẻ Chương',\n    'book_tags' => 'Các Thẻ Sách',\n    'shelf_tags' => 'Các Thẻ Kệ',\n    'tag' => 'Nhãn',\n    'tags' =>  'Các Thẻ',\n    'tags_index_desc' => 'Thẻ có thể được áp dụng cho nội dung trong hệ thống để áp dụng một hình thức phân loại linh hoạt. Thẻ có thể có cả khóa và giá trị, với giá trị là tùy chọn. Sau khi được áp dụng, nội dung sau đó có thể được truy vấn bằng tên thẻ và giá trị.',\n    'tag_name' =>  'Tên Nhãn',\n    'tag_value' => 'Giá trị Thẻ (Tùy chọn)',\n    'tags_explain' => \"Thêm vài thẻ để phân loại nội dung của bạn tốt hơn. \\n Bạn có thể đặt giá trị cho thẻ để quản lí kĩ càng hơn.\",\n    'tags_add' => 'Thêm thẻ khác',\n    'tags_remove' => 'Xóa thẻ này',\n    'tags_usages' => 'Tổng số lượt sử dụng thẻ',\n    'tags_assigned_pages' => 'Đã gán cho Trang',\n    'tags_assigned_chapters' => 'Đã gán cho Chương',\n    'tags_assigned_books' => 'Đã gán cho Sách',\n    'tags_assigned_shelves' => 'Đã gán cho Kệ',\n    'tags_x_unique_values' => ':count giá trị duy nhất',\n    'tags_all_values' => 'Tất cả giá trị',\n    'tags_view_tags' => 'Xem Thẻ',\n    'tags_view_existing_tags' => 'Xem các thẻ hiện có',\n    'tags_list_empty_hint' => 'Thẻ có thể được gán thông qua thanh bên của trình chỉnh sửa trang hoặc trong khi chỉnh sửa chi tiết của sách, chương hoặc kệ.',\n    'attachments' => 'Các Đính kèm',\n    'attachments_explain' => 'Cập nhật một số tập tin và đính một số liên kết để hiển thị trên trang của bạn. Chúng được hiện trong sidebar của trang.',\n    'attachments_explain_instant_save' => 'Các thay đổi ở đây sẽ được lưu ngay lập tức.',\n    'attachments_upload' => 'Tải lên Tập tin',\n    'attachments_link' => 'Đính kèm Liên kết',\n    'attachments_upload_drop' => 'Ngoài ra, bạn có thể kéo và thả tệp vào đây để tải lên làm tệp đính kèm.',\n    'attachments_set_link' => 'Đặt Liên kết',\n    'attachments_delete' => 'Bạn có chắc chắn muốn xóa tập tin đính kèm này?',\n    'attachments_dropzone' => 'Thả tệp vào đây để tải lên',\n    'attachments_no_files' => 'Không có tập tin nào được tải lên',\n    'attachments_explain_link' => 'Bạn có thể đính kèm một liên kết nếu bạn lựa chọn không tải lên tập tin. Liên kết này có thể trỏ đến một trang khác hoặc một tập tin ở trên mạng (đám mây).',\n    'attachments_link_name' => 'Tên Liên kết',\n    'attachment_link' => 'Liên kết đính kèm',\n    'attachments_link_url' => 'Liên kết đến tập tin',\n    'attachments_link_url_hint' => 'URL của trang hoặc tập tin',\n    'attach' => 'Đính kèm',\n    'attachments_insert_link' => 'Thêm Đường dẫn Tập tin đính kèm vào Trang',\n    'attachments_edit_file' => 'Sửa tập tin',\n    'attachments_edit_file_name' => 'Tên tệp tin',\n    'attachments_edit_drop_upload' => 'Thả tập tin hoặc bấm vào đây để tải lên và ghi đè',\n    'attachments_order_updated' => 'Đã cập nhật thứ tự đính kèm',\n    'attachments_updated_success' => 'Đã cập nhật chi tiết đính kèm',\n    'attachments_deleted' => 'Đính kèm đã được xóa',\n    'attachments_file_uploaded' => 'Tập tin tải lên thành công',\n    'attachments_file_updated' => 'Tập tin cập nhật thành công',\n    'attachments_link_attached' => 'Liên kết được đính kèm đến trang thành công',\n    'templates' => 'Các Mẫu',\n    'templates_set_as_template' => 'Trang là một mẫu',\n    'templates_explain_set_as_template' => 'Bạn có thể đặt trang này làm mẫu, nội dung của nó sẽ được sử dụng lại khi tạo các trang mới. Người dùng khác có thể sử dụng mẫu này nếu họ có quyền hạn xem trang này.',\n    'templates_replace_content' => 'Thay thế nội dung trang',\n    'templates_append_content' => 'Viết vào nội dung trang',\n    'templates_prepend_content' => 'Thêm vào đầu nội dung trang',\n\n    // Profile View\n    'profile_user_for_x' => 'Đã là người dùng trong :time',\n    'profile_created_content' => 'Đã tạo nội dung',\n    'profile_not_created_pages' => ':userName chưa tạo bất kỳ trang nào',\n    'profile_not_created_chapters' => ':userName chưa tạo bất kì chương nào',\n    'profile_not_created_books' => ':userName chưa tạo bất cứ sách nào',\n    'profile_not_created_shelves' => ':userName chưa tạo bất kỳ giá sách nào',\n\n    // Comments\n    'comment' => 'Bình luận',\n    'comments' => 'Các bình luận',\n    'comment_add' => 'Thêm bình luận',\n    'comment_none' => 'Không có bình luận nào để hiển thị',\n    'comment_placeholder' => 'Đăng bình luận tại đây',\n    'comment_thread_count' => ':count Chuỗi bình luận|:count Chuỗi bình luận',\n    'comment_archived_count' => ':count Đã lưu trữ',\n    'comment_archived_threads' => 'Chuỗi đã lưu trữ',\n    'comment_save' => 'Lưu bình luận',\n    'comment_new' => 'Bình luận mới',\n    'comment_created' => 'đã bình luận :createDiff',\n    'comment_updated' => 'Đã cập nhật :updateDiff bởi :username',\n    'comment_updated_indicator' => 'Đã cập nhật',\n    'comment_deleted_success' => 'Bình luận đã bị xóa',\n    'comment_created_success' => 'Đã thêm bình luận',\n    'comment_updated_success' => 'Bình luận đã được cập nhật',\n    'comment_archive_success' => 'Đã lưu trữ bình luận',\n    'comment_unarchive_success' => 'Đã bỏ lưu trữ bình luận',\n    'comment_view' => 'Xem bình luận',\n    'comment_jump_to_thread' => 'Chuyển đến chuỗi',\n    'comment_delete_confirm' => 'Bạn có chắc bạn muốn xóa bình luận này?',\n    'comment_in_reply_to' => 'Trả lời cho :commentId',\n    'comment_reference' => 'Tham chiếu',\n    'comment_reference_outdated' => '(Đã lỗi thời)',\n    'comment_editor_explain' => 'Đây là những bình luận đã được để lại trên trang này. Bình luận có thể được thêm & quản lý khi xem trang đã lưu.',\n\n    // Revision\n    'revision_delete_confirm' => 'Bạn có chắc bạn muốn xóa phiên bản này?',\n    'revision_restore_confirm' => 'Bạn có chắc bạn muốn khôi phục phiên bản này? Nội dung trang hiện tại sẽ được thay thế.',\n    'revision_cannot_delete_latest' => 'Không thể xóa phiên bản mới nhất.',\n\n    // Copy view\n    'copy_consider' => 'Vui lòng xem xét những điều sau đây khi sao chép nội dung.',\n    'copy_consider_permissions' => 'Cài đặt quyền tùy chỉnh sẽ không được sao chép.',\n    'copy_consider_owner' => 'Bạn sẽ trở thành chủ sở hữu của tất cả nội dung được sao chép.',\n    'copy_consider_images' => 'Các tệp hình ảnh trang sẽ không được nhân đôi & các hình ảnh gốc sẽ giữ lại mối quan hệ của chúng với trang mà chúng được tải lên ban đầu.',\n    'copy_consider_attachments' => 'Các tệp đính kèm trang sẽ không được sao chép.',\n    'copy_consider_access' => 'Việc thay đổi vị trí, chủ sở hữu hoặc quyền có thể dẫn đến nội dung này có thể truy cập được đối với những người trước đây không có quyền truy cập.',\n\n    // Conversions\n    'convert_to_shelf' => 'Chuyển đổi thành Kệ sách',\n    'convert_to_shelf_contents_desc' => 'Bạn có thể chuyển đổi cuốn sách này thành một kệ sách mới với nội dung tương tự. Các chương có trong cuốn sách này sẽ được chuyển đổi thành các cuốn sách mới. Nếu cuốn sách này chứa bất kỳ trang nào không nằm trong một chương, cuốn sách này sẽ được đổi tên và chứa các trang đó, và cuốn sách này sẽ trở thành một phần của kệ sách mới.',\n    'convert_to_shelf_permissions_desc' => 'Bất kỳ quyền nào được đặt trên cuốn sách này sẽ được sao chép sang kệ sách mới và tất cả các cuốn sách con mới không có quyền riêng của chúng. Lưu ý rằng quyền trên kệ sách không tự động xếp tầng xuống nội dung bên trong, như đối với sách.',\n    'convert_book' => 'Chuyển đổi sách',\n    'convert_book_confirm' => 'Bạn có chắc chắn muốn chuyển đổi cuốn sách này?',\n    'convert_undo_warning' => 'Việc này không thể dễ dàng hoàn tác.',\n    'convert_to_book' => 'Chuyển đổi thành Sách',\n    'convert_to_book_desc' => 'Bạn có thể chuyển đổi chương này thành một cuốn sách mới với nội dung tương tự. Bất kỳ quyền nào được đặt trên chương này sẽ được sao chép sang cuốn sách mới nhưng bất kỳ quyền thừa kế nào từ cuốn sách mẹ sẽ không được sao chép, điều này có thể dẫn đến thay đổi kiểm soát truy cập.',\n    'convert_chapter' => 'Chuyển đổi Chương',\n    'convert_chapter_confirm' => 'Bạn có chắc chắn muốn chuyển đổi chương này?',\n\n    // References\n    'references' => 'Tham chiếu',\n    'references_none' => 'Không có tham chiếu nào được theo dõi đến mục này.',\n    'references_to_desc' => 'Dưới đây là tất cả nội dung đã biết trong hệ thống liên kết đến mục này.',\n\n    // Watch Options\n    'watch' => 'Theo dõi',\n    'watch_title_default' => 'Tùy chọn mặc định',\n    'watch_desc_default' => 'Khôi phục việc theo dõi chỉ về các tùy chọn thông báo mặc định của bạn.',\n    'watch_title_ignore' => 'Bỏ qua',\n    'watch_desc_ignore' => 'Bỏ qua tất cả các thông báo, bao gồm cả những thông báo từ tùy chọn cấp người dùng.',\n    'watch_title_new' => 'Trang mới',\n    'watch_desc_new' => 'Thông báo khi bất kỳ trang mới nào được tạo trong mục này.',\n    'watch_title_updates' => 'Tất cả cập nhật trang',\n    'watch_desc_updates' => 'Thông báo khi có tất cả các trang mới và thay đổi trang.',\n    'watch_desc_updates_page' => 'Thông báo khi có tất cả các thay đổi trang.',\n    'watch_title_comments' => 'Tất cả cập nhật trang & Bình luận',\n    'watch_desc_comments' => 'Thông báo khi có tất cả các trang mới, thay đổi trang và bình luận mới.',\n    'watch_desc_comments_page' => 'Thông báo khi có thay đổi trang và bình luận mới.',\n    'watch_change_default' => 'Thay đổi tùy chọn thông báo mặc định',\n    'watch_detail_ignore' => 'Đang bỏ qua thông báo',\n    'watch_detail_new' => 'Đang theo dõi các trang mới',\n    'watch_detail_updates' => 'Đang theo dõi các trang mới và cập nhật',\n    'watch_detail_comments' => 'Đang theo dõi các trang mới, cập nhật & bình luận',\n    'watch_detail_parent_book' => 'Đang theo dõi thông qua sách cha',\n    'watch_detail_parent_book_ignore' => 'Đang bỏ qua thông qua sách cha',\n    'watch_detail_parent_chapter' => 'Đang theo dõi thông qua chương cha',\n    'watch_detail_parent_chapter_ignore' => 'Đang bỏ qua thông qua chương cha',\n];\n"
  },
  {
    "path": "lang/vi/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => 'Bạn không có quyền truy cập đến trang này.',\n    'permissionJson' => 'Bạn không có quyền để thực hiện hành động này.',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Đã có người sử dụng email :email nhưng với thông tin định danh khác.',\n    'auth_pre_register_theme_prevention' => 'Tài khoản người dùng không thể đăng ký với các chi tiết được cung cấp',\n    'email_already_confirmed' => 'Email đã được xác nhận trước đó, Đang đăng nhập.',\n    'email_confirmation_invalid' => 'Token xác nhận này không hợp lệ hoặc đã được sử dụng trước đó, Xin hãy thử đăng ký lại.',\n    'email_confirmation_expired' => 'Token xác nhận đã hết hạn, Một email xác nhận mới đã được gửi.',\n    'email_confirmation_awaiting' => 'Địa chỉ email của tài khoản bạn đang sử dụng cần phải được xác nhận',\n    'ldap_fail_anonymous' => 'Truy cập đến LDAP sử dụng gán ẩn danh thất bại',\n    'ldap_fail_authed' => 'Truy cập đến LDAP sử dụng dn và mật khẩu thất bại',\n    'ldap_extension_not_installed' => 'Tiện ích mở rộng LDAP PHP chưa được cài đặt',\n    'ldap_cannot_connect' => 'Không thể kết nối đến máy chủ LDAP, mở đầu kết nối thất bại',\n    'saml_already_logged_in' => 'Đã đăng nhập',\n    'saml_no_email_address' => 'Không tìm thấy địa chỉ email cho người dùng này trong dữ liệu được cung cấp bới hệ thống xác thực ngoài',\n    'saml_invalid_response_id' => 'Yêu cầu từ hệ thống xác thực bên ngoài không được nhận diện bởi quy trình chạy cho ứng dụng này. Điều hướng trở lại sau khi đăng nhập có thể đã gây ra vấn đề này.',\n    'saml_fail_authed' => 'Đăng nhập sử dụng :system thất bại, hệ thống không cung cấp được sự xác thực thành công',\n    'oidc_already_logged_in' => 'Đã đăng nhập',\n    'oidc_no_email_address' => 'Không tìm thấy địa chỉ email cho người dùng này, trong dữ liệu được cung cấp bới hệ thống xác thực ngoài',\n    'oidc_fail_authed' => 'Đăng nhập sử dụng :system thất bại, hệ thống không cung cấp được sự xác thực thành công',\n    'social_no_action_defined' => 'Không có hành động được xác định',\n    'social_login_bad_response' => \"Xảy ra lỗi trong lúc đăng nhập :socialAccount: \\n:error\",\n    'social_account_in_use' => 'Tài khoản :socialAccount này đang được sử dụng, Vui lòng thử đăng nhập bằng tùy chọn :socialAccount.',\n    'social_account_email_in_use' => 'Địa chỉ email :email đã được sử dụng. Nếu bạn đã có tài khoản bạn có thể kết nối đến tài khoản :socialAccount của mình từ cài đặt cá nhân của bạn.',\n    'social_account_existing' => ':socialAccount đã được gắn với hồ sơ của bạn từ trước.',\n    'social_account_already_used_existing' => 'Tài khoản :socialAccount đã được sử dụng bởi một người dùng khác.',\n    'social_account_not_used' => 'Tài khoản :socialAccount này chưa được liên kết bởi bất cứ người dùng nào. Vui lòng liên kết nó tại cài đặt cá nhân của bạn. ',\n    'social_account_register_instructions' => 'Nếu bạn chưa có tài khoản, Bạn có thể đăng ký một tài khoản bằng tùy chọn :socialAccount.',\n    'social_driver_not_found' => 'Không tìm thấy driver cho MXH',\n    'social_driver_not_configured' => 'Cài đặt MXH :socialAccount của bạn đang không được cấu hình hợp lệ.',\n    'invite_token_expired' => 'Liên kết mời này đã hết hạn. Bạn có thể thử đặt lại mật khẩu của tài khoản.',\n    'login_user_not_found' => 'Không tìm thấy người dùng cho hành động này.',\n\n    // System\n    'path_not_writable' => 'Đường dẫn tệp tin :filePath không thể tải đến được. Đảm bảo rằng đường dẫn này có thể ghi được ở trên máy chủ.',\n    'cannot_get_image_from_url' => 'Không thể lấy ảnh từ :url',\n    'cannot_create_thumbs' => 'Máy chủ không thể tạo ảnh nhỏ. Vui lòng kiểm tra bạn đã cài đặt tiện ích mở rộng GD PHP.',\n    'server_upload_limit' => 'Máy chủ không cho phép tải lên kích thước này. Vui lòng thử lại với tệp tin nhỏ hơn.',\n    'server_post_limit' => 'Máy chủ không thể nhận lượng dữ liệu được cung cấp. Hãy thử lại với ít dữ liệu hoặc tệp nhỏ hơn.',\n    'uploaded'  => 'Máy chủ không cho phép tải lên kích thước này. Vui lòng thử lại với tệp tin nhỏ hơn.',\n\n    // Drawing & Images\n    'image_upload_error' => 'Đã xảy ra lỗi khi đang tải lên ảnh',\n    'image_upload_type_error' => 'Ảnh đang được tải lên không hợp lệ',\n    'image_upload_replace_type' => 'Các tệp hình ảnh thay thế phải cùng loại',\n    'image_upload_memory_limit' => 'Không xử lý được hình ảnh tải lên và/hoặc tạo hình thu nhỏ do giới hạn tài nguyên hệ thống.',\n    'image_thumbnail_memory_limit' => 'Không tạo được các biến thể kích thước hình ảnh do giới hạn tài nguyên hệ thống.',\n    'image_gallery_thumbnail_memory_limit' => 'Không tạo được hình thu nhỏ thư viện do giới hạn tài nguyên hệ thống.',\n    'drawing_data_not_found' => 'Không thể tải dữ liệu bản vẽ. Tệp bản vẽ có thể không còn tồn tại hoặc bạn không có quyền truy cập vào nó.',\n\n    // Attachments\n    'attachment_not_found' => 'Không tìm thấy đính kèm',\n    'attachment_upload_error' => 'Đã xảy ra lỗi khi tải tệp đính kèm',\n\n    // Pages\n    'page_draft_autosave_fail' => 'Lưu bản nháp thất bại. Đảm bảo rằng bạn có kết nối đến internet trước khi lưu trang này',\n    'page_draft_delete_fail' => 'Không thể xóa bản nháp trang và lấy nội dung đã lưu của trang hiện tại',\n    'page_custom_home_deletion' => 'Không thể xóa trang khi nó đang được đặt là trang chủ',\n\n    // Entities\n    'entity_not_found' => 'Không tìm thấy thực thể',\n    'bookshelf_not_found' => 'Không tìm thấy giá sách',\n    'book_not_found' => 'Không tìm thấy sách',\n    'page_not_found' => 'Không tìm thấy trang',\n    'chapter_not_found' => 'Không tìm thấy chương',\n    'selected_book_not_found' => 'Không tìm thấy sách được chọn',\n    'selected_book_chapter_not_found' => 'Không tìm thấy Sách hoặc Chương được chọn',\n    'guests_cannot_save_drafts' => 'Khách không thể lưu bản nháp',\n\n    // Users\n    'users_cannot_delete_only_admin' => 'Bạn không thể xóa quản trị viên duy nhất',\n    'users_cannot_delete_guest' => 'Bạn không thể xóa người dùng khách',\n    'users_could_not_send_invite' => 'Không thể tạo người dùng vì email mời không gửi được',\n\n    // Roles\n    'role_cannot_be_edited' => 'Không thể chỉnh sửa quyền này',\n    'role_system_cannot_be_deleted' => 'Quyền này là quyền hệ thống và không thể bị xóa',\n    'role_registration_default_cannot_delete' => 'Quyền này không thể bị xóa trong khi đang đặt là quyền mặc định khi đăng ký',\n    'role_cannot_remove_only_admin' => 'Người dùng này là người dùng duy nhất được chỉ định quyền quản trị viên. Gán quyền quản trị viên cho người dùng khác trước khi thử xóa người dùng này.',\n\n    // Comments\n    'comment_list' => 'Đã có lỗi xảy ra khi tải bình luận.',\n    'cannot_add_comment_to_draft' => 'Bạn không thể thêm bình luận vào bản nháp.',\n    'comment_add' => 'Đã xảy ra lỗi khi thêm / sửa bình luận.',\n    'comment_delete' => 'Đã xảy ra lỗi khi xóa bình luận.',\n    'empty_comment' => 'Không thể thêm bình luận bị bỏ trống.',\n\n    // Error pages\n    '404_page_not_found' => 'Không Tìm Thấy Trang',\n    'sorry_page_not_found' => 'Xin lỗi, Không tìm thấy trang bạn đang tìm kiếm.',\n    'sorry_page_not_found_permission_warning' => 'Nếu trang bạn tìm kiếm tồn tại, có thể bạn đang không có quyền truy cập.',\n    'image_not_found' => 'Không tìm thấy Ảnh',\n    'image_not_found_subtitle' => 'Rất tiếc, không thể tìm thấy Ảnh bạn đang tìm kiếm.',\n    'image_not_found_details' => 'Nếu bạn hi vọng ảnh này tồn tại, rất có thể nó đã bị xóa.',\n    'return_home' => 'Quay lại trang chủ',\n    'error_occurred' => 'Đã xảy ra lỗi',\n    'app_down' => ':appName hiện đang ngoại tuyến',\n    'back_soon' => 'Nó sẽ sớm hoạt động trở lại.',\n\n    // Import\n    'import_zip_cant_read' => 'Không thể đọc tệp ZIP.',\n    'import_zip_cant_decode_data' => 'Không thể tìm và giải mã nội dung ZIP data.json.',\n    'import_zip_no_data' => 'Dữ liệu tệp ZIP không có nội dung sách, chương hoặc trang mong đợi.',\n    'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',\n    'import_validation_failed' => 'Nhập tệp ZIP không hợp lệ với các lỗi:',\n    'import_zip_failed_notification' => 'Không thể nhập tệp ZIP.',\n    'import_perms_books' => 'Bạn không có quyền cần thiết để tạo sách.',\n    'import_perms_chapters' => 'Bạn không có quyền cần thiết để tạo chương.',\n    'import_perms_pages' => 'Bạn không có quyền cần thiết để tạo trang.',\n    'import_perms_images' => 'Bạn không có quyền cần thiết để tạo hình ảnh.',\n    'import_perms_attachments' => 'Bạn không có quyền cần thiết để tạo tệp đính kèm.',\n\n    // API errors\n    'api_no_authorization_found' => 'Không tìm thấy token ủy quyền trong yêu cầu',\n    'api_bad_authorization_format' => 'Đã tìm thấy một token ủy quyền trong yêu cầu nhưng định dạng hiển thị không hợp lệ',\n    'api_user_token_not_found' => 'Không tìm thấy token API nào khớp với token ủy quyền được cung cấp',\n    'api_incorrect_token_secret' => 'Mã bí mật được cung cấp cho token API đang được sử dụng không hợp lệ',\n    'api_user_no_api_permission' => 'Chủ của token API đang sử dụng không có quyền gọi API',\n    'api_user_token_expired' => 'Token sử dụng cho việc ủy quyền đã hết hạn',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => 'Lỗi khi gửi email thử:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'URL không khớp với các máy chủ SSR được cấu hình cho phép',\n];\n"
  },
  {
    "path": "lang/vi/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => 'Bình luận mới trên trang: :pageName',\n    'new_comment_intro' => 'Một người dùng đã bình luận trên một trang tại :appName:',\n    'new_page_subject' => 'Trang mới: :pageName',\n    'new_page_intro' => 'Một trang mới đã được khởi tạo trong :appName:',\n    'updated_page_subject' => 'Trang đã cập nhật: :pageName',\n    'updated_page_intro' => 'Một trang mới đã được cập nhật trong :appName:',\n    'updated_page_debounce' => 'Để tránh việc nhận quá nhiều thông báo, trong một thời gian, bạn sẽ không nhận được thông báo về những chỉnh sửa tiếp theo cho trang này từ cùng một biên tập viên.',\n    'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',\n    'comment_mention_intro' => 'You were mentioned in a comment on :appName:',\n\n    'detail_page_name' => 'Tên Trang:',\n    'detail_page_path' => 'Đường dẫn trang:',\n    'detail_commenter' => 'Người bình luận:',\n    'detail_comment' => 'Bình luận:',\n    'detail_created_by' => 'Tạo bởi:',\n    'detail_updated_by' => 'Cập nhật bởi:',\n\n    'action_view_comment' => 'Xem bình luận',\n    'action_view_page' => 'Xem Trang',\n\n    'footer_reason' => 'Đây là thông báo được gửi tới bạn bởi vì :link bao gồm hoạt động cho mục này.',\n    'footer_reason_link' => 'ưu tiên thông báo của bạn',\n];\n"
  },
  {
    "path": "lang/vi/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; Trước',\n    'next'     => 'Tiếp &raquo;',\n\n];\n"
  },
  {
    "path": "lang/vi/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => 'Mật khẩu phải có tối thiểu 8 ký tự và và phải trùng với mật khẩu xác nhận.',\n    'user' => \"Chúng tôi không tìm thấy người dùng với địa chỉ email đó.\",\n    'token' => 'Mã token đặt lại mật khẩu cho địa chỉ email này không hợp lệ.',\n    'sent' => 'Chúng tôi đã gửi email chứa liên kết đặt lại mật khẩu cho bạn!',\n    'reset' => 'Mật khẩu của bạn đã được đặt lại!',\n\n];\n"
  },
  {
    "path": "lang/vi/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => 'My Account',\n\n    'shortcuts' => 'Lối tắt',\n    'shortcuts_interface' => 'UI Shortcut Preferences',\n    'shortcuts_toggle_desc' => 'Tại đây, bạn có thể bật hoặc tắt các phím tắt trên giao diện hệ thống bàn phím, được sử dụng để điều hướng và thực hiện các thao tác.',\n    'shortcuts_customize_desc' => 'Bạn có thể tùy chỉnh từng phím tắt dưới đây. Chỉ cần nhấn tổ hợp phím mong muốn của bạn sau khi chọn đầu vào cho một phím tắt.',\n    'shortcuts_toggle_label' => 'Bật phím tắt',\n    'shortcuts_section_navigation' => 'Điều hướng',\n    'shortcuts_section_actions' => 'Hành động chung',\n    'shortcuts_save' => 'Lưu phím tắt',\n    'shortcuts_overlay_desc' => 'Lưu ý: Khi các phím tắt được bật, lớp phủ trợ giúp sẽ khả dụng bằng cách nhấn \"?\" sẽ làm nổi bật các lối tắt khả dụng cho các tác vụ hiện đang hiển thị trên màn hình.',\n    'shortcuts_update_success' => 'Các tùy chọn phím tắt đã được cập nhật!',\n    'shortcuts_overview_desc' => 'Manage keyboard shortcuts you can use to navigate the system user interface.',\n\n    'notifications' => 'Notification Preferences',\n    'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.',\n    'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own',\n    'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own',\n    'notifications_opt_comment_mentions' => 'Notify when I\\'m mentioned in a comment',\n    'notifications_opt_comment_replies' => 'Notify upon replies to my comments',\n    'notifications_save' => 'Save Preferences',\n    'notifications_update_success' => 'Notification preferences have been updated!',\n    'notifications_watched' => 'Watched & Ignored Items',\n    'notifications_watched_desc' => 'Below are the items that have custom watch preferences applied. To update your preferences for these, view the item then find the watch options in the sidebar.',\n\n    'auth' => 'Access & Security',\n    'auth_change_password' => 'Change Password',\n    'auth_change_password_desc' => 'Change the password you use to log-in to the application. This must be at least 8 characters long.',\n    'auth_change_password_success' => 'Password has been updated!',\n\n    'profile' => 'Profile Details',\n    'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.',\n    'profile_view_public' => 'View Public Profile',\n    'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.',\n    'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.',\n    'profile_email_no_permission' => 'Unfortunately you don\\'t have permission to change your email address. If you want to change this, you\\'d need to ask an administrator to change this for you.',\n    'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.',\n    'profile_admin_options' => 'Administrator Options',\n    'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the \"Settings > Users\" area of the application.',\n\n    'delete_account' => 'Delete Account',\n    'delete_my_account' => 'Delete My Account',\n    'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\\'ve created, such as created pages and uploaded images, will remain.',\n    'delete_my_account_warning' => 'Are you sure you want to delete your account?',\n];\n"
  },
  {
    "path": "lang/vi/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => 'Cài đặt',\n    'settings_save' => 'Lưu Cài đặt',\n    'system_version' => 'Phiên bản Hệ thống',\n    'categories' => 'Danh mục',\n\n    // App Settings\n    'app_customization' => 'Tuỳ biến',\n    'app_features_security' => 'Chức năng & Bảo mật',\n    'app_name' => 'Tên Ứng dụng',\n    'app_name_desc' => 'Tên này được hiển thị trong header và trong bất kỳ email hệ thống được gửi.',\n    'app_name_header' => 'Hiển thị tên trong header',\n    'app_public_access' => 'Quyền truy cập công khai',\n    'app_public_access_desc' => 'Bật tùy chọn này sẽ cho phép khách, người không cần đăng nhập, truy cập đến nội dung bản BookStack của bạn.',\n    'app_public_access_desc_guest' => 'Quyền truy cập của khách có thể được điều khiển thông qua người dùng \"Guest\".',\n    'app_public_access_toggle' => 'Cho phép truy cập công khai',\n    'app_public_viewing' => 'Cho phép xem công khai?',\n    'app_secure_images' => 'Bảo mật tốt hơn cho việc tải lên ảnh',\n    'app_secure_images_toggle' => 'Bật bảo mật tốt hơn cho các ảnh được tải lên',\n    'app_secure_images_desc' => 'Vì lý do hiệu năng, tất cả các ảnh đều được truy cập công khai. Tùy chọn này thêm một chuỗi ngẫu nhiên, khó đoán vào phần liên kết đến ảnh. Đảm bảo rằng tránh việc index thư mục để ngăn chặn việc truy cập đến ảnh một cách dễ dàng.',\n    'app_default_editor' => 'Trình soạn thảo mặc định',\n    'app_default_editor_desc' => 'Chọn trình soạn thảo nào sẽ được sử dụng theo mặc định khi chỉnh sửa trang mới. Điều này có thể bị ghi đè ở cấp độ trang nơi quyền cho phép.',\n    'app_custom_html' => 'Tùy chọn nội dung Head HTML',\n    'app_custom_html_desc' => 'Bất cứ nội dung nào được thêm vào đây sẽ được đưa vào phần cuối của khu vực <head> của mỗi trang. Tiện cho việc ghi đè style hoặc thêm mã phân tích dữ liệu.',\n    'app_custom_html_disabled_notice' => 'Nội dung tùy biến HTML head bị tắt tại trang cài đặt này để đảm bảo mọi thay đổi làm hỏng hệ thống có để được khôi phục.',\n    'app_logo' => 'Logo Ứng dụng',\n    'app_logo_desc' => 'Điều này được sử dụng trong thanh tiêu đề của ứng dụng, trong số những khu vực khác. Hình ảnh này nên có chiều cao 86px. Những hình ảnh lớn sẽ được thu nhỏ lại.',\n    'app_icon' => 'Biểu tượng ứng dụng',\n    'app_icon_desc' => 'Biểu tượng này được sử dụng cho các tab trình duyệt và các biểu tượng phím tắt. Đây phải là hình ảnh PNG hình vuông 256px.',\n    'app_homepage' => 'Trang chủ Ứng dụng',\n    'app_homepage_desc' => 'Chọn hiển thị để hiện tại trang chủ thay cho hiển thị mặc định. Quyền cho trang được bỏ qua cho các trang được chọn.',\n    'app_homepage_select' => 'Chọn một trang',\n    'app_footer_links' => 'Liên kết chân trang',\n    'app_footer_links_desc' => 'Thêm liên kết để hiển thị trong phần chân trang. Chúng sẽ được hiển thị ở dưới cùng của hầu hết các trang, bao gồm cả những trang không yêu cầu đăng nhập. Bạn có thể sử dụng nhãn \"trans::<key>\" để dùng bản dịch do hệ thống xác định. Ví dụ: Sử dụng \"trans::common.privacy_policy\" sẽ cung cấp văn bản được dịch là \"Chính sách quyền riêng tư\" và \"trans::common.terms_of_service\" sẽ cung cấp văn bản được dịch là \"Điều khoản dịch vụ\".',\n    'app_footer_links_label' => 'Nhãn liên kết',\n    'app_footer_links_url' => 'Địa chỉ liên kết',\n    'app_footer_links_add' => 'Thêm liên kết chân trang',\n    'app_disable_comments' => 'Tắt bình luận',\n    'app_disable_comments_toggle' => 'Tắt bình luận',\n    'app_disable_comments_desc' => 'Tắt các bình luận trên tất cả các trang của ứng dụng. <br> Các bình luận đã tồn tại sẽ không được hiển thị.',\n\n    // Color settings\n    'color_scheme' => 'Bảng màu ứng dụng',\n    'color_scheme_desc' => 'Đặt các màu được sử dụng trong giao diện người dùng của ứng dụng. Màu sắc có thể được cấu hình riêng cho chế độ tối và sáng để phù hợp nhất với chủ đề và đảm bảo khả năng đọc.',\n    'ui_colors_desc' => 'Đặt màu chính của ứng dụng và màu liên kết mặc định. Màu chính chủ yếu được sử dụng cho biểu ngữ đầu trang, nút và trang trí giao diện. Màu liên kết mặc định được sử dụng cho các liên kết và hành động dựa trên văn bản, cả trong nội dung đã viết và trong giao diện ứng dụng.',\n    'app_color' => 'Màu cơ bản',\n    'link_color' => 'Màu liên kết mặc định',\n    'content_colors_desc' => 'Đặt màu cho tất cả các thành phần trong phân cấp tổ chức trang. Nên chọn màu có độ sáng tương tự với màu mặc định để có thể đọc được.',\n    'bookshelf_color' => 'Màu Giá sách',\n    'book_color' => 'Màu Sách',\n    'chapter_color' => 'Màu Chương',\n    'page_color' => 'Màu Trang',\n    'page_draft_color' => 'Màu Trang Nháp',\n\n    // Registration Settings\n    'reg_settings' => 'Đăng ký',\n    'reg_enable' => 'Bật Đăng ký',\n    'reg_enable_toggle' => 'Bật đăng ký',\n    'reg_enable_desc' => 'Khi đăng ký được bật, người dùng sẽ có thể tự đăng ký để trở thành người dùng của ứng dụng. Khi đăng kí, người dùng sẽ được cấp một quyền sử dụng mặc định.',\n    'reg_default_role' => 'Quyền người dùng mặc định sau khi đăng kí',\n    'reg_enable_external_warning' => 'Tùy chọn trên bị bỏ qua khi xác thực từ bên ngoài LDAP hoặc SAML được bật. Tài khoản người dùng chưa phải là thành viên sẽ được tự động tạo nếu xác thực với hệ thống bên ngoài thành công.',\n    'reg_email_confirmation' => 'Xác nhận Email',\n    'reg_email_confirmation_toggle' => 'Yêu cầu xác nhận email',\n    'reg_confirm_email_desc' => 'Nếu giới hạn tên miền được sử dụng, xác nhận email là bắt buộc và tùy chọn này sẽ bị bỏ qua.',\n    'reg_confirm_restrict_domain' => 'Giới hạn tên miền',\n    'reg_confirm_restrict_domain_desc' => 'Điền dấu phẩy ngăn cách danh sách các tên miền email dành cho việc bạn muốn giới hạn đăng nhập. Người dùng sẽ nhận được email xác nhận địa chỉ của họ trước khi được phép tương tác với ứng dụng. <br> Lưu ý rằng người dùng có thể thay đổi địa chỉ email của họ sau khi đăng ký thành công.',\n    'reg_confirm_restrict_domain_placeholder' => 'Không có giới hạn nào được thiết lập',\n\n    // Sorting Settings\n    'sorting' => 'Lists & Sorting',\n    'sorting_book_default' => 'Default Book Sort Rule',\n    'sorting_book_default_desc' => 'Chọn quy tắc sắp xếp mặc định để áp dụng cho sách mới. Điều này sẽ không ảnh hưởng đến các sách hiện có và có thể được ghi đè cho từng sách.',\n    'sorting_rules' => 'Quy tắc sắp xếp',\n    'sorting_rules_desc' => 'Đây là các thao tác sắp xếp được xác định trước có thể được áp dụng cho nội dung trong hệ thống.',\n    'sort_rule_assigned_to_x_books' => 'Được gán cho :count sách|Được gán cho :count sách',\n    'sort_rule_create' => 'Tạo quy tắc sắp xếp',\n    'sort_rule_edit' => 'Chỉnh sửa quy tắc sắp xếp',\n    'sort_rule_delete' => 'Xóa quy tắc sắp xếp',\n    'sort_rule_delete_desc' => 'Xóa quy tắc sắp xếp này khỏi hệ thống. Sách sử dụng quy tắc này sẽ trở lại sắp xếp thủ công.',\n    'sort_rule_delete_warn_books' => 'Quy tắc sắp xếp này hiện đang được sử dụng trên :count sách. Bạn có chắc chắn muốn xóa quy tắc này không?',\n    'sort_rule_delete_warn_default' => 'Quy tắc sắp xếp này hiện đang được sử dụng làm mặc định cho sách. Bạn có chắc chắn muốn xóa quy tắc này không?',\n    'sort_rule_details' => 'Chi tiết quy tắc sắp xếp',\n    'sort_rule_details_desc' => 'Đặt tên cho quy tắc sắp xếp này, tên này sẽ xuất hiện trong danh sách khi người dùng chọn một sắp xếp.',\n    'sort_rule_operations' => 'Thao tác sắp xếp',\n    'sort_rule_operations_desc' => 'Cấu hình các hành động sắp xếp sẽ được thực hiện bằng cách di chuyển chúng từ danh sách các thao tác khả dụng. Khi sử dụng, các thao tác sẽ được áp dụng theo thứ tự, từ trên xuống dưới. Bất kỳ thay đổi nào được thực hiện ở đây sẽ được áp dụng cho tất cả các sách được gán khi lưu.',\n    'sort_rule_available_operations' => 'Thao tác khả dụng',\n    'sort_rule_available_operations_empty' => 'Không còn thao tác nào',\n    'sort_rule_configured_operations' => 'Thao tác đã cấu hình',\n    'sort_rule_configured_operations_empty' => 'Kéo/thêm thao tác từ danh sách \"Thao tác khả dụng\"',\n    'sort_rule_op_asc' => '(Tăng dần)',\n    'sort_rule_op_desc' => '(Giảm dần)',\n    'sort_rule_op_name' => 'Tên - Theo bảng chữ cái',\n    'sort_rule_op_name_numeric' => 'Tên - Theo số',\n    'sort_rule_op_created_date' => 'Ngày tạo',\n    'sort_rule_op_updated_date' => 'Ngày cập nhật',\n    'sort_rule_op_chapters_first' => 'Chương trước',\n    'sort_rule_op_chapters_last' => 'Chương sau',\n    'sorting_page_limits' => 'Per-Page Display Limits',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => 'Bảo trì',\n    'maint_image_cleanup' => 'Dọn dẹp ảnh',\n    'maint_image_cleanup_desc' => 'Quét nội dung trang và phiên bản để kiểm tra xem các ảnh và hình vẽ nào đang được sử dụng và ảnh nào dư thừa. Đảm bảo rằng bạn đã tạo bản sao lưu toàn bộ dữ liệu và ảnh trước khi chạy chức năng này.',\n    'maint_delete_images_only_in_revisions' => 'Cũng xóa hình ảnh chỉ tồn tại trong các phiên bản trang cũ',\n    'maint_image_cleanup_run' => 'Chạy Dọn dẹp',\n    'maint_image_cleanup_warning' => 'Đã tìm thấy :count ảnh có thể không được sử dụng. Bạn muốn chắc rằng muốn xóa các ảnh này?',\n    'maint_image_cleanup_success' => ':count ảnh có thể không được sử dụng đã được tìm thấy và xóa!',\n    'maint_image_cleanup_nothing_found' => 'Không tìm thấy ảnh nào không được sử dụng, Không có gì để xóa!',\n    'maint_send_test_email' => 'Gửi một email thử',\n    'maint_send_test_email_desc' => 'Chức năng này gửi một email thử đến địa chỉ email bạn chỉ định trong hồ sơ của mình.',\n    'maint_send_test_email_run' => 'Gửi email thử',\n    'maint_send_test_email_success' => 'Email đã được gửi đến :address',\n    'maint_send_test_email_mail_subject' => 'Thử Email',\n    'maint_send_test_email_mail_greeting' => 'Chức năng gửi email có vẻ đã hoạt động!',\n    'maint_send_test_email_mail_text' => 'Chúc mừng! Khi bạn nhận được email thông báo này, cài đặt email của bạn có vẻ đã được cấu hình đúng.',\n    'maint_recycle_bin_desc' => 'Giá, sách, chương & trang đã xóa được gửi vào thùng rác để có thể khôi phục hoặc xóa vĩnh viễn. Các mục cũ hơn trong thùng rác có thể được tự động xóa sau một thời gian tùy thuộc vào cấu hình hệ thống.',\n    'maint_recycle_bin_open' => 'Mở Thùng Rác',\n    'maint_regen_references' => 'Tạo lại tài liệu tham khảo',\n    'maint_regen_references_desc' => 'Hành động này sẽ tạo lại chỉ mục tham chiếu nhiều mục trong cơ sở dữ liệu. Thao tác này thường được xử lý tự động nhưng hành động này có thể hữu ích để lập chỉ mục nội dung cũ hoặc nội dung được thêm vào thông qua các phương pháp không chính thức.',\n    'maint_regen_references_success' => 'Chỉ mục tham chiếu đã được tạo lại!',\n    'maint_timeout_command_note' => 'Lưu ý: Hành động này có thể mất thời gian để chạy, điều này có thể dẫn đến các vấn đề về thời gian chờ trong một số môi trường web. Thay vào đó, hành động này được thực hiện bằng cách sử dụng một lệnh đầu cuối.',\n\n    // Recycle Bin\n    'recycle_bin' => 'Thùng Rác',\n    'recycle_bin_desc' => 'Tại đây, bạn có thể khôi phục các mục đã bị xóa hoặc chọn xóa chúng vĩnh viễn khỏi hệ thống. Danh sách này không được lọc không giống như các danh sách hoạt động tương tự trong hệ thống áp dụng các bộ lọc quyền.',\n    'recycle_bin_deleted_item' => 'Mục Đã Xóa',\n    'recycle_bin_deleted_parent' => 'Thư mục cha',\n    'recycle_bin_deleted_by' => 'Xóa Bởi',\n    'recycle_bin_deleted_at' => 'Thời điểm Xóa',\n    'recycle_bin_permanently_delete' => 'Xóa Vĩnh viễn',\n    'recycle_bin_restore' => 'Khôi phục',\n    'recycle_bin_contents_empty' => 'Thùng rác hiện đang trống',\n    'recycle_bin_empty' => 'Dọn dẹp Thùng Rác',\n    'recycle_bin_empty_confirm' => 'Thao tác này sẽ hủy vĩnh viễn tất cả các mục trong thùng rác bao gồm cả nội dung có trong mỗi mục. Bạn có chắc chắn muốn làm trống thùng rác?',\n    'recycle_bin_destroy_confirm' => 'Hành động này sẽ xóa vĩnh viễn mục này khỏi hệ thống, cùng với bất kỳ phần tử con nào được liệt kê bên dưới và bạn sẽ không thể khôi phục nội dung này. Bạn có chắc chắn muốn xóa vĩnh viễn mục này không?',\n    'recycle_bin_destroy_list' => 'Các mục sẽ bị hủy',\n    'recycle_bin_restore_list' => 'Các mục sẽ được khôi phục',\n    'recycle_bin_restore_confirm' => 'Hành động này sẽ khôi phục mục đã xóa, bao gồm mọi phần tử con, về vị trí ban đầu của chúng. Nếu vị trí ban đầu đã bị xóa và hiện đang nằm trong thùng rác, mục cha cũng sẽ cần được khôi phục.',\n    'recycle_bin_restore_deleted_parent' => 'Mục cha của mục này cũng đã bị xóa. Chúng sẽ vẫn bị xóa cho đến khi mục cha đó cũng được khôi phục.',\n    'recycle_bin_restore_parent' => 'Khôi phục mục cha',\n    'recycle_bin_destroy_notification' => 'Đã xóa :count tổng số mục khỏi thùng rác.',\n    'recycle_bin_restore_notification' => 'Đã khôi phục :count tổng số mục khỏi thùng rác.',\n\n    // Audit Log\n    'audit' => 'Nhật ký kiểm tra',\n    'audit_desc' => 'Nhật ký kiểm tra này hiển thị danh sách các hoạt động được theo dõi trong hệ thống. Danh sách này không được lọc không giống như các danh sách hoạt động tương tự trong hệ thống nơi các bộ lọc quyền được áp dụng.',\n    'audit_event_filter' => 'Bộ lọc sự kiện',\n    'audit_event_filter_no_filter' => 'Không Lọc',\n    'audit_deleted_item' => 'Mục Đã Xóa',\n    'audit_deleted_item_name' => 'Tên: :name',\n    'audit_table_user' => 'Người dùng',\n    'audit_table_event' => 'Sự kiện',\n    'audit_table_related' => 'Mục hoặc chi tiết liên quan',\n    'audit_table_ip' => 'Địa chỉ IP',\n    'audit_table_date' => 'Ngày hoạt động',\n    'audit_date_from' => 'Ngày từ khoảng',\n    'audit_date_to' => 'Ngày đến khoảng',\n\n    // Role Settings\n    'roles' => 'Quyền',\n    'role_user_roles' => 'Quyền người dùng',\n    'roles_index_desc' => 'Các quyền được sử dụng để nhóm người dùng và cung cấp quyền hệ thống cho các thành viên của họ. Khi một người dùng là thành viên của nhiều quyền, các đặc quyền được cấp sẽ chồng lên nhau và người dùng sẽ thừa hưởng tất cả các khả năng.',\n    'roles_x_users_assigned' => ':count người dùng được gán|:count người dùng được gán',\n    'roles_x_permissions_provided' => ':count quyền|:count quyền',\n    'roles_assigned_users' => 'Người dùng được gán',\n    'roles_permissions_provided' => 'Quyền được cung cấp',\n    'role_create' => 'Tạo quyền mới',\n    'role_delete' => 'Xóa quyền',\n    'role_delete_confirm' => 'Chức năng này sẽ xóa quyền với tên \\':roleName\\'.',\n    'role_delete_users_assigned' => 'Quyền này có :userCount người dùng được gán. Nếu bạn muốn di dời các người dùng từ quyền này hãy chọn một quyền mới bên dưới.',\n    'role_delete_no_migration' => \"Không di dời người dùng\",\n    'role_delete_sure' => 'Bạn có chắc rằng muốn xóa quyền này?',\n    'role_edit' => 'Sửa quyền',\n    'role_details' => 'Thông tin chi tiết Quyền',\n    'role_name' => 'Tên quyền',\n    'role_desc' => 'Thông tin vắn tắt của Quyền',\n    'role_mfa_enforced' => 'Yêu cầu xác thực đa yếu tố',\n    'role_external_auth_id' => 'Mã của xác thực ngoài',\n    'role_system' => 'Quyền Hệ thống',\n    'role_manage_users' => 'Quản lý người dùng',\n    'role_manage_roles' => 'Quản lý quyền và chức năng quyền',\n    'role_manage_entity_permissions' => 'Quản lý tất cả quyền của các sách, chương & trang',\n    'role_manage_own_entity_permissions' => 'Quản lý quyền trên sách, chương & trang bạn tạo ra',\n    'role_manage_page_templates' => 'Quản lý các mẫu trang',\n    'role_access_api' => 'Truy cập đến API hệ thống',\n    'role_manage_settings' => 'Quản lý cài đặt của ứng dụng',\n    'role_export_content' => 'Xuất nội dung',\n    'role_import_content' => 'Nhập nội dung',\n    'role_editor_change' => 'Thay đổi trình soạn thảo trang',\n    'role_notifications' => 'Nhận & quản lý thông báo',\n    'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',\n    'role_asset' => 'Quyền tài sản (asset)',\n    'roles_system_warning' => 'Cần lưu ý rằng việc truy cập vào bất kỳ ba quyền trên có thể cho phép người dùng thay đổi đặc quyền của chính họ hoặc đặc quyền của những người khác trong hệ thống. Chỉ gán các vai trò có các quyền này cho những người dùng đáng tin cậy.',\n    'role_asset_desc' => 'Các quyền này điều khiển truy cập mặc định tới tài sản (asset) nằm trong hệ thống. Quyền tại Sách, Chương và Trang sẽ ghi đè các quyền này.',\n    'role_asset_admins' => 'Quản trị viên được tự động cấp quyền truy cập đến toàn bộ nội dung, tuy nhiên các tùy chọn đó có thể hiện hoặc ẩn tùy chọn giao diện.',\n    'role_asset_image_view_note' => 'Điều này liên quan đến khả năng hiển thị trong trình quản lý hình ảnh. Quyền truy cập thực tế vào các tệp hình ảnh đã tải lên sẽ phụ thuộc vào tùy chọn lưu trữ hình ảnh của hệ thống.',\n    'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',\n    'role_all' => 'Tất cả',\n    'role_own' => 'Sở hữu',\n    'role_controlled_by_asset' => 'Kiểm soát các tài sản (asset) người dùng tải lên',\n    'role_save' => 'Lưu Quyền',\n    'role_users' => 'Người dùng được gán quyền này',\n    'role_users_none' => 'Không có người dùng nào hiện được gán quyền này',\n\n    // Users\n    'users' => 'Người dùng',\n    'users_index_desc' => 'Tạo và quản lý các tài khoản người dùng riêng lẻ trong hệ thống. Các tài khoản người dùng được sử dụng để đăng nhập và gán nội dung & hoạt động. Quyền truy cập chủ yếu dựa trên vai trò nhưng quyền sở hữu nội dung của người dùng, cùng với các yếu tố khác, cũng có thể ảnh hưởng đến quyền và truy cập.',\n    'user_profile' => 'Hồ sơ người dùng',\n    'users_add_new' => 'Thêm người dùng mới',\n    'users_search' => 'Tìm kiếm người dùng',\n    'users_latest_activity' => 'Hoạt động mới nhất',\n    'users_details' => 'Chi tiết người dùng',\n    'users_details_desc' => 'Hiển thị tên và địa chỉ email cho người dùng này. Địa chỉ email sẽ được sử dụng để đăng nhập vào ứng dụng.',\n    'users_details_desc_no_email' => 'Đặt tên cho người dùng này để giúp người dùng khác nhận ra họ.',\n    'users_role' => 'Quyền người dùng',\n    'users_role_desc' => 'Chọn quyền mà người dùng sẽ được gán. Nếu người dùng được gán nhiều quyền, các quyền hạn sẽ ghi đè lên nhau và họ sẽ nhận được tất cả các quyền hạn từ quyền được gán.',\n    'users_password' => 'Mật khẩu người dùng',\n    'users_password_desc' => 'Đặt mật khẩu được sử dụng để đăng nhập vào ứng dụng. Mật khẩu phải dài ít nhất 8 ký tự.',\n    'users_send_invite_text' => 'Bạn có thể chọn để gửi cho người dùng này một email mời, giúp họ có thể tự đặt mật khẩu cho chính họ. Nếu không bạn có thể đặt mật khẩu cho họ.',\n    'users_send_invite_option' => 'Gửi email mời người dùng',\n    'users_external_auth_id' => 'Mã của xác thực ngoài',\n    'users_external_auth_id_desc' => 'Khi một hệ thống xác thực bên ngoài đang được sử dụng (chẳng hạn như SAML2, OIDC hoặc LDAP) đây là ID liên kết người dùng BookStack này với tài khoản hệ thống xác thực. Bạn có thể bỏ qua trường này nếu sử dụng xác thực dựa trên email mặc định.',\n    'users_password_warning' => 'Chỉ điền vào phần bên dưới nếu bạn muốn thay đổi mật khẩu cho người dùng này.',\n    'users_system_public' => 'Người dùng này đại diện cho bất kỳ khách nào thăm trang của bạn. Nó được tự động gán và không thể dùng để đăng nhập.',\n    'users_delete' => 'Xóa Người dùng',\n    'users_delete_named' => 'Xóa người dùng :userName',\n    'users_delete_warning' => 'Chức năng này sẽ hoàn toàn xóa người dùng với tên \\':userName\\' khỏi hệ thống.',\n    'users_delete_confirm' => 'Bạn có chắc muốn xóa người dùng này không?',\n    'users_migrate_ownership' => 'Di chuyển quyền sở hữu',\n    'users_migrate_ownership_desc' => 'Chọn một người dùng tại đây nếu bạn muốn một người dùng khác trở thành chủ sở hữu của tất cả các mục hiện thuộc sở hữu của người dùng này.',\n    'users_none_selected' => 'Chưa chọn người dùng',\n    'users_edit' => 'Sửa người dùng',\n    'users_edit_profile' => 'Sửa Hồ sơ',\n    'users_avatar' => 'Ảnh đại diện',\n    'users_avatar_desc' => 'Chọn ảnh để đại diện cho người dùng này. Ảnh nên có kích cỡ hình vuông 256px.',\n    'users_preferred_language' => 'Ngôn ngữ ưu tiên',\n    'users_preferred_language_desc' => 'Tùy chọn này sẽ thay đổi ngôn ngữ sử dụng cho giao diện người dùng của ứng dụng. Nó sẽ không ảnh hưởng đến bất cứ nội dung nào người dùng tạo ra.',\n    'users_social_accounts' => 'Tài khoản MXH',\n    'users_social_accounts_desc' => 'Xem trạng thái của các tài khoản xã hội được kết nối cho người dùng này. Các tài khoản xã hội có thể được sử dụng ngoài hệ thống xác thực chính để truy cập hệ thống.',\n    'users_social_accounts_info' => 'Bạn có thể kết nối đến các tài khoản khác để đăng nhập nhanh chóng và dễ dàng. Ngắt kết nối đến một tài khoản ở đây không thu hồi việc ủy quyền truy cập trước đó. Thu hồi truy cập của các tài khoản kết nối MXH từ trang cài đặt hồ sơ của bạn.',\n    'users_social_connect' => 'Kết nối tài khoản',\n    'users_social_disconnect' => 'Ngắt kết nối tài khoản',\n    'users_social_status_connected' => 'Đã kết nối',\n    'users_social_status_disconnected' => 'Đã ngắt kết nối',\n    'users_social_connected' => 'Tài khoản :socialAccount đã được liên kết với hồ sơ của bạn thành công.',\n    'users_social_disconnected' => 'Tài khoản :socialAccount đã được ngắt kết nối khỏi hồ sơ của bạn thành công.',\n    'users_api_tokens' => 'Các Token API',\n    'users_api_tokens_desc' => 'Tạo và quản lý các mã thông báo truy cập được sử dụng để xác thực với API REST của BookStack. Quyền cho API được quản lý thông qua người dùng mà mã thông báo thuộc về.',\n    'users_api_tokens_none' => 'Không có Token API nào được tạo cho người dùng này',\n    'users_api_tokens_create' => 'Tạo Token',\n    'users_api_tokens_expires' => 'Hết hạn',\n    'users_api_tokens_docs' => 'Tài liệu API',\n    'users_mfa' => 'Xác thực đa yếu tố',\n    'users_mfa_desc' => 'Thiết lập xác thực đa yếu tố như một lớp bảo mật bổ sung cho tài khoản người dùng của bạn.',\n    'users_mfa_x_methods' => ':count phương thức đã cấu hình|:count phương thức đã cấu hình',\n    'users_mfa_configure' => 'Cấu hình phương thức',\n\n    // API Tokens\n    'user_api_token_create' => 'Tạo Token API',\n    'user_api_token_name' => 'Tên',\n    'user_api_token_name_desc' => 'Đặt cho token của bạn một tên dễ đọc để nhắc nhở mục đích sử dụng của nó trong tương lai.',\n    'user_api_token_expiry' => 'Ngày hết hạn',\n    'user_api_token_expiry_desc' => 'Đặt một ngày hết hạn cho token này. Sau ngày này, các yêu cầu được tạo khi sử dụng token này sẽ không còn hoạt động. Để trống trường này sẽ đặt ngày hết hạn sau 100 năm tới.',\n    'user_api_token_create_secret_message' => 'Ngay sau khi tạo token này một \"Mã Token\" & \"Mật khẩu Token\" sẽ được tạo và hiển thị. Mật khẩu sẽ chỉ được hiện một lần duy nhất nên hãy chắc rằng bạn sao lưu giá trị của nó ở nơi an toàn và bảo mật trước khi tiếp tục.',\n    'user_api_token' => 'Token API',\n    'user_api_token_id' => 'Mã Token',\n    'user_api_token_id_desc' => 'Đây là hệ thống sinh ra định danh không thể chỉnh sửa cho token này, thứ mà sẽ cần phải cung cấp khi yêu cầu API.',\n    'user_api_token_secret' => 'Mật khẩu Token',\n    'user_api_token_secret_desc' => 'Đây là mật khẩu được hệ thống tạo ra cho token để phục vụ cho các yêu cầu API này. Nó sẽ chỉ được hiển thị một lần duy nhất nên hãy sao lưu nó vào nơi nào đó an toàn và bảo mật.',\n    'user_api_token_created' => 'Token được tạo :timeAgo',\n    'user_api_token_updated' => 'Token được cập nhật :timeAgo',\n    'user_api_token_delete' => 'Xóa Token',\n    'user_api_token_delete_warning' => 'Chức năng này sẽ hoàn toàn xóa token API với tên \\':tokenName\\' khỏi hệ thống.',\n    'user_api_token_delete_confirm' => 'Bạn có chắc rằng muốn xóa token API này?',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhooks là một cách để gửi dữ liệu đến các URL bên ngoài khi một số hành động và sự kiện nhất định xảy ra trong hệ thống, cho phép tích hợp dựa trên sự kiện với các nền tảng bên ngoài như hệ thống nhắn tin hoặc thông báo.',\n    'webhooks_x_trigger_events' => ':count sự kiện kích hoạt|:count sự kiện kích hoạt',\n    'webhooks_create' => 'Tạo Webhook mới',\n    'webhooks_none_created' => 'Chưa có webhooks nào được tạo.',\n    'webhooks_edit' => 'Chỉnh sửa Webhook',\n    'webhooks_save' => 'Lưu Webhook',\n    'webhooks_details' => 'Chi tiết Webhook',\n    'webhooks_details_desc' => 'Cung cấp một tên thân thiện với người dùng và một điểm cuối POST làm vị trí để dữ liệu webhook được gửi đến.',\n    'webhooks_events' => 'Sự kiện Webhook',\n    'webhooks_events_desc' => 'Chọn tất cả các sự kiện sẽ kích hoạt webhook này được gọi.',\n    'webhooks_events_warning' => 'Hãy nhớ rằng các sự kiện này sẽ được kích hoạt cho tất cả các sự kiện đã chọn, ngay cả khi các quyền tùy chỉnh được áp dụng. Đảm bảo rằng việc sử dụng webhook này sẽ không làm lộ nội dung bí mật.',\n    'webhooks_events_all' => 'Tất cả các sự kiện hệ thống',\n    'webhooks_name' => 'Tên Webhook',\n    'webhooks_timeout' => 'Thời gian chờ yêu cầu Webhook (Giây)',\n    'webhooks_endpoint' => 'Điểm cuối Webhook',\n    'webhooks_active' => 'Webhook hoạt động',\n    'webhook_events_table_header' => 'Sự kiện',\n    'webhooks_delete' => 'Xóa Webhook',\n    'webhooks_delete_warning' => 'Điều này sẽ xóa hoàn toàn webhook này, với tên \\':webhookName\\', khỏi hệ thống.',\n    'webhooks_delete_confirm' => 'Bạn có chắc chắn muốn xóa webhook này không?',\n    'webhooks_format_example' => 'Ví dụ định dạng Webhook',\n    'webhooks_format_example_desc' => 'Dữ liệu webhook được gửi dưới dạng yêu cầu POST đến điểm cuối đã cấu hình dưới dạng JSON theo định dạng bên dưới. Các thuộc tính \"related_item\" và \"url\" là tùy chọn và sẽ phụ thuộc vào loại sự kiện được kích hoạt.',\n    'webhooks_status' => 'Trạng thái Webhook',\n    'webhooks_last_called' => 'Lần cuối được gọi:',\n    'webhooks_last_errored' => 'Lần cuối xảy ra lỗi:',\n    'webhooks_last_error_message' => 'Nội dung lỗi gần nhất:',\n\n    // Licensing\n    'licenses' => 'Giấy phép',\n    'licenses_desc' => 'Trang này trình bày chi tiết thông tin giấy phép cho BookStack ngoài các dự án & thư viện được sử dụng trong BookStack. Nhiều dự án được liệt kê có thể chỉ được sử dụng trong ngữ cảnh phát triển.',\n    'licenses_bookstack' => 'Giấy phép BookStack',\n    'licenses_php' => 'Giấy phép thư viện PHP',\n    'licenses_js' => 'Giấy phép thư viện JavaScript',\n    'licenses_other' => 'Các giấy phép khác',\n    'license_details' => 'Chi tiết giấy phép',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => 'Català',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => 'Đan Mạch',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/vi/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute phải được chấp nhận.',\n    'active_url'           => ':attribute không phải là một đường dẫn hợp lệ.',\n    'after'                => ':attribute phải là một ngày sau :date.',\n    'alpha'                => ':attribute chỉ được chứa chữ cái.',\n    'alpha_dash'           => ':attribute chỉ được chứa chữ cái, chữ số, gạch nối và gạch dưới.',\n    'alpha_num'            => ':attribute chỉ được chứa chữ cái hoặc chữ số.',\n    'array'                => ':attribute phải là một mảng.',\n    'backup_codes'         => 'Mã cung cấp không hợp lệ hoặc đã được sử dụng.',\n    'before'               => ':attribute phải là một ngày trước :date.',\n    'between'              => [\n        'numeric' => ':attribute phải nằm trong khoảng :min đến :max.',\n        'file'    => ':attribute phải nằm trong khoảng :min đến :max KB.',\n        'string'  => ':attribute phải trong khoảng :min đến :max ký tự.',\n        'array'   => ':attribute phải nằm trong khoảng :min đến :max mục.',\n    ],\n    'boolean'              => 'Trường :attribute phải có giá trị đúng hoặc sai.',\n    'confirmed'            => 'Xác nhận :attribute không khớp.',\n    'date'                 => ':attribute không phải là ngày hợp lệ.',\n    'date_format'          => ':attribute không khớp với định dạng :format.',\n    'different'            => ':attribute và :other phải khác nhau.',\n    'digits'               => ':attribute phải có :digits chữ số.',\n    'digits_between'       => ':attribute phải có từ :min đến :max chữ số.',\n    'email'                => ':attribute phải là địa chỉ email hợp lệ.',\n    'ends_with' => ':attribute phải kết thúc bằng một trong các ký tự: :values',\n    'file'                 => ':attribute phải được cung cấp dưới dạng tệp hợp lệ.',\n    'filled'               => 'Trường :attribute là bắt buộc.',\n    'gt'                   => [\n        'numeric' => ':attribute phải lớn hơn :value.',\n        'file'    => ':attribute phải lớn hơn :value KB.',\n        'string'  => ':attribute phải có nhiều hơn :value ký tự.',\n        'array'   => ':attribute phải có nhiều hơn :value mục.',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute phải lớn hơn hoặc bằng :value.',\n        'file'    => ':attribute phải lớn hơn hoặc bằng :value KB.',\n        'string'  => ':attribute phải có nhiều hơn hoặc bằng :value ký tự.',\n        'array'   => ':attribute phải có :value mục trở lên.',\n    ],\n    'exists'               => ':attribute đã chọn không hợp lệ.',\n    'image'                => ':attribute phải là ảnh.',\n    'image_extension'      => ':attribute phải có định dạng ảnh hợp lệ và được hỗ trợ.',\n    'in'                   => ':attribute đã chọn không hợp lệ.',\n    'integer'              => ':attribute phải là một số nguyên.',\n    'ip'                   => ':attribute phải là một địa chỉ IP hợp lệ.',\n    'ipv4'                 => ':attribute phải là địa chỉ IPv4 hợp lệ.',\n    'ipv6'                 => ':attribute phải là địa chỉ IPv6 hợp lệ.',\n    'json'                 => ':attribute phải là một chuỗi JSON hợp lệ.',\n    'lt'                   => [\n        'numeric' => ':attribute phải nhỏ hơn :value.',\n        'file'    => ':attribute phải nhỏ hơn :value KB.',\n        'string'  => ':attribute phải có it hơn :value ký tự.',\n        'array'   => ':attribute phải có ít hơn :value mục.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute phải nhỏ hơn hoặc bằng :value.',\n        'file'    => ':attribute phải nhỏ hơn hoặc bằng :value KB.',\n        'string'  => ':attribute phải có ít hơn hoặc bằng :value ký tự.',\n        'array'   => ':attribute không được có nhiều hơn :value mục.',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute không được lớn hơn :max.',\n        'file'    => ':attribute không được lớn hơn :max KB.',\n        'string'  => ':attribute không được nhiều hơn :max ký tự.',\n        'array'   => ':attribute không thể có nhiều hơn :max mục.',\n    ],\n    'mimes'                => ':attribute phải là tệp tin có kiểu: :values.',\n    'min'                  => [\n        'numeric' => ':attribute phải tối thiểu là :min.',\n        'file'    => ':attribute phải tối thiểu là :min KB.',\n        'string'  => ':attribute phải có tối thiểu :min ký tự.',\n        'array'   => ':attribute phải có tối thiểu :min mục.',\n    ],\n    'not_in'               => ':attribute đã chọn không hợp lệ.',\n    'not_regex'            => 'Định dạng của :attribute không hợp lệ.',\n    'numeric'              => ':attribute phải là một số.',\n    'regex'                => 'Định dạng của :attribute không hợp lệ.',\n    'required'             => 'Trường :attribute là bắt buộc.',\n    'required_if'          => 'Trường :attribute là bắt buộc khi :other là :value.',\n    'required_with'        => 'Trường :attribute là bắt buộc khi :values tồn tại.',\n    'required_with_all'    => 'Trường :attribute là bắt buộc khi :values tồn tại.',\n    'required_without'     => 'Trường :attribute là bắt buộc khi :values không tồn tại.',\n    'required_without_all' => 'Trường :attribute là bắt buộc khi không có bất cứ :values nào tồn tại.',\n    'same'                 => ':attribute và :other phải trùng khớp với nhau.',\n    'safe_url'             => 'Đường dẫn cung cấp có thể không an toàn.',\n    'size'                 => [\n        'numeric' => ':attribute phải có cỡ :size.',\n        'file'    => ':attribute phải có cỡ :size KB.',\n        'string'  => ':attribute phải có :size ký tự.',\n        'array'   => ':attribute phải chứa :size mục.',\n    ],\n    'string'               => ':attribute phải là một chuỗi.',\n    'timezone'             => ':attribute phải là một khu vực hợp lệ.',\n    'totp'                 => 'Mã cung cấp không hợp lệ hoặc đã hết hạn.',\n    'unique'               => ':attribute đã có người sử dụng.',\n    'url'                  => 'Định dạng của :attribute không hợp lệ.',\n    'uploaded'             => 'Tệp tin đã không được tải lên. Máy chủ không chấp nhận các tệp tin với dung lượng lớn như tệp tin trên.',\n\n    'zip_file' => ':attribute cần tham chiếu đến một tệp trong ZIP.',\n    'zip_file_size' => 'The file :attribute must not exceed :size MB.',\n    'zip_file_mime' => ':attribute cần tham chiếu đến một tệp có kiểu: :validTypes, tìm thấy :foundType.',\n    'zip_model_expected' => 'Đối tượng dữ liệu được mong đợi nhưng tìm thấy \":type\".',\n    'zip_unique' => ':attribute phải là duy nhất cho kiểu đối tượng trong ZIP.',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => 'Bắt buộc xác nhận mật khẩu',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/zh_CN/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => '页面已创建',\n    'page_create_notification'    => '页面创建成功',\n    'page_update'                 => '页面已更新',\n    'page_update_notification'    => '页面更新成功',\n    'page_delete'                 => '页面已删除',\n    'page_delete_notification'    => '页面删除成功',\n    'page_restore'                => '页面已恢复',\n    'page_restore_notification'   => '页面恢复成功',\n    'page_move'                   => '页面已移动',\n    'page_move_notification'      => '页面移动成功',\n\n    // Chapters\n    'chapter_create'              => '章节已创建',\n    'chapter_create_notification' => '章节创建成功',\n    'chapter_update'              => '章节已更新',\n    'chapter_update_notification' => '章节更新成功',\n    'chapter_delete'              => '章节已删除',\n    'chapter_delete_notification' => '章节删除成功',\n    'chapter_move'                => '章节已移动',\n    'chapter_move_notification' => '章节移动成功',\n\n    // Books\n    'book_create'                 => '书籍已创建',\n    'book_create_notification'    => '成功创建书籍',\n    'book_create_from_chapter'              => '将章节转换为书籍',\n    'book_create_from_chapter_notification' => '章节已成功转换为书籍',\n    'book_update'                 => '书籍已更新',\n    'book_update_notification'    => '书籍更新成功',\n    'book_delete'                 => '书籍已删除',\n    'book_delete_notification'    => '书籍删除成功',\n    'book_sort'                   => '书籍已排序',\n    'book_sort_notification'      => '书籍重新排序成功',\n\n    // Bookshelves\n    'bookshelf_create'            => '书架已创建',\n    'bookshelf_create_notification'    => '书架创建成功',\n    'bookshelf_create_from_book'    => '将书籍转换为书架',\n    'bookshelf_create_from_book_notification'    => '书籍已成功转换为书架',\n    'bookshelf_update'                 => '书架已更新',\n    'bookshelf_update_notification'    => '书架更新成功',\n    'bookshelf_delete'                 => '书架已删除',\n    'bookshelf_delete_notification'    => '书架删除成功',\n\n    // Revisions\n    'revision_restore' => '已还原修改',\n    'revision_delete' => '已删除修订',\n    'revision_delete_notification' => '修订删除成功',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" 已添加到您的收藏',\n    'favourite_remove_notification' => '\":name\" 已从您的收藏中删除',\n\n    // Watching\n    'watch_update_level_notification' => '关注偏好设置已更新成功',\n\n    // Auth\n    'auth_login' => '已登录',\n    'auth_register' => '已注册为新用户',\n    'auth_password_reset_request' => '已请求密码重置',\n    'auth_password_reset_update' => '重置用户密码',\n    'mfa_setup_method' => '已配置多重身份验证',\n    'mfa_setup_method_notification' => '多重身份认证设置成功',\n    'mfa_remove_method' => '已移除多重身份验证',\n    'mfa_remove_method_notification' => '多重身份认证已成功移除',\n\n    // Settings\n    'settings_update' => '设置已更新',\n    'settings_update_notification' => '设置更新成功',\n    'maintenance_action_run' => '维护操作已运行',\n\n    // Webhooks\n    'webhook_create' => 'Webhook 已创建',\n    'webhook_create_notification' => 'Webhook 创建成功',\n    'webhook_update' => 'Webhook 已更新',\n    'webhook_update_notification' => 'Webhook 更新成功',\n    'webhook_delete' => 'Webhook 已删除',\n    'webhook_delete_notification' => 'Webhook 删除成功',\n\n    // Imports\n    'import_create' => '创建导入',\n    'import_create_notification' => '导入上传成功',\n    'import_run' => '更新导入',\n    'import_run_notification' => '内容成功导入',\n    'import_delete' => '删除导入',\n    'import_delete_notification' => '导入删除成功',\n\n    // Users\n    'user_create' => '用户已创建',\n    'user_create_notification' => '用户创建成功',\n    'user_update' => '用户已更新',\n    'user_update_notification' => '用户更新成功',\n    'user_delete' => '用户已删除',\n    'user_delete_notification' => '成功移除用户',\n\n    // API Tokens\n    'api_token_create' => '已创建 API 令牌',\n    'api_token_create_notification' => '成功创建 API 令牌',\n    'api_token_update' => '已更新 API 令牌',\n    'api_token_update_notification' => 'API 令牌更新成功',\n    'api_token_delete' => '已删除 API 令牌',\n    'api_token_delete_notification' => 'API 令牌删除成功',\n\n    // Roles\n    'role_create' => '角色已创建',\n    'role_create_notification' => '角色创建成功',\n    'role_update' => '角色已更新',\n    'role_update_notification' => '角色更新成功',\n    'role_delete' => '角色已删除',\n    'role_delete_notification' => '角色删除成功',\n\n    // Recycle Bin\n    'recycle_bin_empty' => '回收站已清空',\n    'recycle_bin_restore' => '已从回收站中恢复',\n    'recycle_bin_destroy' => '已从回收站中移除',\n\n    // Comments\n    'commented_on'                => '评论',\n    'comment_create'              => '评论已添加',\n    'comment_update'              => '评论已更新',\n    'comment_delete'              => '评论已删除',\n\n    // Sort Rules\n    'sort_rule_create' => '创建排序规则',\n    'sort_rule_create_notification' => '排序规则创建成功',\n    'sort_rule_update' => '更新排序规则',\n    'sort_rule_update_notification' => '排序规则更新成功',\n    'sort_rule_delete' => '删除排序规则',\n    'sort_rule_delete_notification' => '排序规则删除成功',\n\n    // Other\n    'permissions_update'          => '权限已更新',\n];\n"
  },
  {
    "path": "lang/zh_CN/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => '用户名或密码错误。',\n    'throttle' => '您的登录次数过多，请在:seconds秒后重试。',\n\n    // Login & Register\n    'sign_up' => '注册',\n    'log_in' => '登录',\n    'log_in_with' => '使用 :socialDriver 账户登录',\n    'sign_up_with' => '通过 :socialDriver 账号登录',\n    'logout' => '注销',\n\n    'name' => '名称',\n    'username' => '用户名',\n    'email' => 'Email地址',\n    'password' => '密码',\n    'password_confirm' => '确认密码',\n    'password_hint' => '必须至少有 8 个字符',\n    'forgot_password' => '忘记密码?',\n    'remember_me' => '记住我',\n    'ldap_email_hint' => '请输入用于此账户的电子邮件。',\n    'create_account' => '创建账户',\n    'already_have_account' => '已经有账号了？',\n    'dont_have_account' => '您还没有账号吗？',\n    'social_login' => 'SNS登录',\n    'social_registration' => '使用社交网站账号注册',\n    'social_registration_text' => '使用其他服务注册并登录。',\n\n    'register_thanks' => '注册完成！',\n    'register_confirm' => '请点击查收您的Email，并点击确认。',\n    'registrations_disabled' => '注册目前被禁用',\n    'registration_email_domain_invalid' => '该Email域名无权访问此应用程序',\n    'register_success' => '感谢您注册！您现在已注册并登录。',\n\n    // Login auto-initiation\n    'auto_init_starting' => '尝试登录中',\n    'auto_init_starting_desc' => '我们正在联系您的身份验证系统以启动登录过程。如果 5 秒后还没有进展，您可以尝试点击下面的链接。',\n    'auto_init_start_link' => '继续进行身份验证',\n\n    // Password Reset\n    'reset_password' => '重置密码',\n    'reset_password_send_instructions' => '在下面输入您的Email地址，您将收到一封带有密码重置链接的邮件。',\n    'reset_password_send_button' => '发送重置链接',\n    'reset_password_sent' => '重置密码的链接将通过您的电子邮箱发送:email。',\n    'reset_password_success' => '您的密码已成功重置。',\n    'email_reset_subject' => '重置您的:appName密码',\n    'email_reset_text' => '您收到此电子邮件是因为我们收到了您的账户的密码重置请求。',\n    'email_reset_not_requested' => '如果您没有要求重置密码，则不需要采取进一步的操作。',\n\n    // Email Confirmation\n    'email_confirm_subject' => '确认您在:appName的Email地址',\n    'email_confirm_greeting' => '感谢您加入:appName！',\n    'email_confirm_text' => '请点击下面的按钮确认您的Email地址：',\n    'email_confirm_action' => '确认Email',\n    'email_confirm_send_error' => '需要Email验证，但系统无法发送电子邮件，请联系网站管理员。',\n    'email_confirm_success' => '您已成功验证电子邮件地址！您现在可以使用此电子邮件地址登录。',\n    'email_confirm_resent' => '验证邮件已重新发送，请检查收件箱。',\n    'email_confirm_thanks' => '感谢您的确认！',\n    'email_confirm_thanks_desc' => '请稍等，您的确认正在处理。如果您在3秒后未被重定向，请按下面的“继续“链接继续。',\n\n    'email_not_confirmed' => 'Email地址未验证',\n    'email_not_confirmed_text' => '您的电子邮件地址尚未确认。',\n    'email_not_confirmed_click_link' => '请检查注册时收到的电子邮件，然后点击确认链接。',\n    'email_not_confirmed_resend' => '如果找不到电子邮件，请通过下面的表单重新发送确认Email。',\n    'email_not_confirmed_resend_button' => '重新发送确认Email',\n\n    // User Invite\n    'user_invite_email_subject' => '您已受邀加入 :appName！',\n    'user_invite_email_greeting' => ':appName 已为您创建了一个账户。',\n    'user_invite_email_text' => '点击下面的按钮以设置账户密码并获得访问权限：',\n    'user_invite_email_action' => '设置账号密码',\n    'user_invite_page_welcome' => '欢迎来到 :appName！',\n    'user_invite_page_text' => '要完成您的账户并获得访问权限，您需要设置一个密码，该密码将在以后访问时用于登录 :appName。',\n    'user_invite_page_confirm_button' => '确认密码',\n    'user_invite_success_login' => '密码已设置，您现在可以使用您设置的密码登录 :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => '设置多重身份认证',\n    'mfa_setup_desc' => '设置多重身份认证能增加您账户的安全性。',\n    'mfa_setup_configured' => '已经设置过了',\n    'mfa_setup_reconfigure' => '重新配置',\n    'mfa_setup_remove_confirmation' => '您确定想要移除多重身份认证吗？',\n    'mfa_setup_action' => '设置',\n    'mfa_backup_codes_usage_limit_warning' => '您剩余的备用认证码少于 5 个，请在用完认证码之前生成并保存新的认证码，以防止您的账户被锁定。',\n    'mfa_option_totp_title' => '移动设备 App',\n    'mfa_option_totp_desc' => '要使用多重身份认证功能，您需要一个支持 TOTP（基于时间的一次性密码算法） 的移动设备 App，如谷歌身份验证器（Google Authenticator）、Authy 或微软身份验证器（Microsoft Authenticator）。',\n    'mfa_option_backup_codes_title' => '备用认证码',\n    'mfa_option_backup_codes_desc' => '生成一组一次性使用的备份码，您将在登录时输入以验证您的身份。 请确保将它们存储在一个安全可靠的地方。',\n    'mfa_gen_confirm_and_enable' => '确认并启用',\n    'mfa_gen_backup_codes_title' => '备用认证码设置',\n    'mfa_gen_backup_codes_desc' => '将下面的认证码存放在一个安全的地方。访问系统时，您可以使用其中的一个验证码进行二次认证。',\n    'mfa_gen_backup_codes_download' => '下载认证码',\n    'mfa_gen_backup_codes_usage_warning' => '每个认证码只能使用一次',\n    'mfa_gen_totp_title' => '移动设备 App',\n    'mfa_gen_totp_desc' => '要使用多重身份认证功能，您需要一个支持 TOTP（基于时间的一次性密码算法） 的移动设备 App，如谷歌身份验证器（Google Authenticator）、Authy 或微软身份验证器（Microsoft Authenticator）。',\n    'mfa_gen_totp_scan' => '要开始操作，请使用您的身份验证 App 扫描下面的二维码。',\n    'mfa_gen_totp_verify_setup' => '验证设置',\n    'mfa_gen_totp_verify_setup_desc' => '请在下面的框中输入您在身份验证 App 中生成的认证码来验证一切是否正常：',\n    'mfa_gen_totp_provide_code_here' => '在此输入您的 App 生成的认证码',\n    'mfa_verify_access' => '认证访问',\n    'mfa_verify_access_desc' => '您的账户要求您在访问前通过额外的验证确认您的身份。使用您设置的认证方法认证以继续。',\n    'mfa_verify_no_methods' => '没有设置认证方法',\n    'mfa_verify_no_methods_desc' => '您的账户没有设置多重身份认证。您需要至少设置一种才能访问。',\n    'mfa_verify_use_totp' => '使用移动设备 App 进行认证',\n    'mfa_verify_use_backup_codes' => '使用备用认证码进行认证',\n    'mfa_verify_backup_code' => '备用认证码',\n    'mfa_verify_backup_code_desc' => '在下面输入您的其中一个备用认证码：',\n    'mfa_verify_backup_code_enter_here' => '在这里输入备用认证码',\n    'mfa_verify_totp_desc' => '在下面输入您的移动 App 生成的认证码：',\n    'mfa_setup_login_notification' => '多重身份认证已设置，请使用新配置的方法重新登录。',\n];\n"
  },
  {
    "path": "lang/zh_CN/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => '取消',\n    'close' => '关闭',\n    'confirm' => '确认',\n    'back' => '返回',\n    'save' => '保存',\n    'continue' => '继续',\n    'select' => '选择',\n    'toggle_all' => '切换全部',\n    'more' => '更多',\n\n    // Form Labels\n    'name' => '名称',\n    'description' => '概要',\n    'role' => '角色',\n    'cover_image' => '封面图片',\n    'cover_image_description' => '此图像的大小应约为 440 x 250 像素，但会根据需要灵活缩放和裁剪以适应不同场景下的用户界面，因此实际显示尺寸会有所不同。',\n\n    // Actions\n    'actions' => '操作',\n    'view' => '浏览',\n    'view_all' => '查看全部',\n    'new' => '新',\n    'create' => '创建',\n    'update' => '更新',\n    'edit' => '编辑',\n    'archive' => '存档',\n    'unarchive' => '取消存档',\n    'sort' => '排序',\n    'move' => '移动',\n    'copy' => '复制',\n    'reply' => '回复',\n    'delete' => '删除',\n    'delete_confirm' => '确认删除',\n    'search' => '搜索',\n    'search_clear' => '清除搜索',\n    'reset' => '重置',\n    'remove' => '删除',\n    'add' => '添加',\n    'configure' => '配置',\n    'manage' => '管理',\n    'fullscreen' => '全屏',\n    'favourite' => '收藏',\n    'unfavourite' => '取消收藏',\n    'next' => '下一页',\n    'previous' => '上一页',\n    'filter_active' => '标签过滤器：',\n    'filter_clear' => '清除过滤器',\n    'download' => '下载',\n    'open_in_tab' => '在标签页中打开。',\n    'open' => '打开',\n\n    // Sort Options\n    'sort_options' => '排序选项',\n    'sort_direction_toggle' => '排序方向切换',\n    'sort_ascending' => '升序',\n    'sort_descending' => '降序',\n    'sort_name' => '名称',\n    'sort_default' => '默认',\n    'sort_created_at' => '创建时间',\n    'sort_updated_at' => '更新时间',\n\n    // Misc\n    'deleted_user' => '删除用户',\n    'no_activity' => '没有活动要显示',\n    'no_items' => '没有可用的项目',\n    'back_to_top' => '回到顶部',\n    'skip_to_main_content' => '跳转到主要内容',\n    'toggle_details' => '显示/隐藏详细信息',\n    'toggle_thumbnails' => '显示/隐藏缩略图',\n    'details' => '详细信息',\n    'grid_view' => '网格视图',\n    'list_view' => '列表视图',\n    'default' => '默认',\n    'breadcrumb' => '面包屑导航',\n    'status' => '状态',\n    'status_active' => '已激活',\n    'status_inactive' => '未激活',\n    'never' => '从未',\n    'none' => '无',\n\n    // Header\n    'homepage' => '主页',\n    'header_menu_expand' => '展开标头菜单',\n    'profile_menu' => '个人资料',\n    'view_profile' => '查看个人资料',\n    'edit_profile' => '编辑个人资料',\n    'dark_mode' => '深色模式',\n    'light_mode' => '浅色模式',\n    'global_search' => '全局搜索',\n\n    // Layout tabs\n    'tab_info' => '信息',\n    'tab_info_label' => '标签：显示次要信息',\n    'tab_content' => '内容',\n    'tab_content_label' => '标签：显示主要内容',\n\n    // Email Content\n    'email_action_help' => '如果您无法点击“:actionText”按钮，请将下面的网址复制到您的浏览器中打开：',\n    'email_rights' => '版权所有',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => '隐私政策',\n    'terms_of_service' => '服务条款',\n\n    // OpenSearch\n    'opensearch_description' => '搜索 :appName',\n];\n"
  },
  {
    "path": "lang/zh_CN/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => '选择图片',\n    'image_list' => '图片列表',\n    'image_details' => '图片详情',\n    'image_upload' => '上传图片',\n    'image_intro' => '您可以在此选择和管理以前上传到系统的图片。',\n    'image_intro_upload' => '通过将图片文件拖到此窗口或使用上面的“上传图片”按钮来上传新图片。',\n    'image_all' => '全部',\n    'image_all_title' => '查看所有图片',\n    'image_book_title' => '查看上传到本书的图片',\n    'image_page_title' => '查看上传到本页面的图片',\n    'image_search_hint' => '按图片名称搜索',\n    'image_uploaded' => '上传于 :uploadedDate',\n    'image_uploaded_by' => '由 :userName 上传',\n    'image_uploaded_to' => '上传到 :pageLink',\n    'image_updated' => ':updateDate 更新',\n    'image_load_more' => '显示更多',\n    'image_image_name' => '图片名称',\n    'image_delete_used' => '该图像用于以下页面。',\n    'image_delete_confirm_text' => '您确认要删除此图片吗？',\n    'image_select_image' => '选择图片',\n    'image_dropzone' => '拖放图片或点击此处上传',\n    'image_dropzone_drop' => '将图片拖放到此处上传',\n    'images_deleted' => '图片已删除',\n    'image_preview' => '图片预览',\n    'image_upload_success' => '图片上传成功',\n    'image_update_success' => '图片详细信息更新成功',\n    'image_delete_success' => '图片删除成功',\n    'image_replace' => '替换图片',\n    'image_replace_success' => '图片文件更新成功',\n    'image_rebuild_thumbs' => '重新生成大小变化',\n    'image_rebuild_thumbs_success' => '图像大小变化成功重建！',\n\n    // Code Editor\n    'code_editor' => '编辑代码',\n    'code_language' => '编程语言',\n    'code_content' => '代码内容',\n    'code_session_history' => '会话历史',\n    'code_save' => '保存代码',\n];\n"
  },
  {
    "path": "lang/zh_CN/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => '通用',\n    'advanced' => '高级',\n    'none' => '无',\n    'cancel' => '取消',\n    'save' => '保存',\n    'close' => '关闭',\n    'apply' => '应用',\n    'undo' => '撤销',\n    'redo' => '重做',\n    'left' => '左对齐',\n    'center' => '居中',\n    'right' => '右对齐',\n    'top' => '上对齐',\n    'middle' => '居中对齐',\n    'bottom' => '底端对齐',\n    'width' => '宽度',\n    'height' => '高度',\n    'More' => '更多',\n    'select' => '选择...',\n\n    // Toolbar\n    'formats' => '格式',\n    'header_large' => '大标题',\n    'header_medium' => '中标题',\n    'header_small' => '小标题',\n    'header_tiny' => '微标题',\n    'paragraph' => '段落',\n    'blockquote' => '块引用',\n    'inline_code' => '行内代码',\n    'callouts' => '标注',\n    'callout_information' => '信息',\n    'callout_success' => '成功',\n    'callout_warning' => '警告',\n    'callout_danger' => '危险',\n    'bold' => '加粗',\n    'italic' => '倾斜',\n    'underline' => '下划线',\n    'strikethrough' => '删除线',\n    'superscript' => '上标',\n    'subscript' => '下标',\n    'text_color' => '文本颜色',\n    'highlight_color' => '高亮颜色',\n    'custom_color' => '自定义颜色',\n    'remove_color' => '移除颜色',\n    'background_color' => '背景色',\n    'align_left' => '左对齐',\n    'align_center' => '居中',\n    'align_right' => '右对齐',\n    'align_justify' => '两端对齐',\n    'list_bullet' => '无序列表',\n    'list_numbered' => '有序列表',\n    'list_task' => '任务列表',\n    'indent_increase' => '增加缩进',\n    'indent_decrease' => '减少缩进',\n    'table' => '表格',\n    'insert_image' => '插入图片',\n    'insert_image_title' => '插入/编辑图片',\n    'insert_link' => '插入/编辑链接',\n    'insert_link_title' => '插入/编辑链接',\n    'insert_horizontal_line' => '插入水平线',\n    'insert_code_block' => '插入代码块',\n    'edit_code_block' => '编辑代码块',\n    'insert_drawing' => '插入/编辑绘图',\n    'drawing_manager' => '绘图管理器',\n    'insert_media' => '插入/编辑媒体',\n    'insert_media_title' => '插入/编辑媒体',\n    'clear_formatting' => '清除格式',\n    'source_code' => '源代码',\n    'source_code_title' => '源代码',\n    'fullscreen' => '全屏',\n    'image_options' => '图片选项',\n\n    // Tables\n    'table_properties' => '表格属性',\n    'table_properties_title' => '表格属性',\n    'delete_table' => '删除表格',\n    'table_clear_formatting' => '清除表格格式',\n    'resize_to_contents' => '根据内容调整大小',\n    'row_header' => '行头',\n    'insert_row_before' => '在上方插入行',\n    'insert_row_after' => '在下方插入行',\n    'delete_row' => '删除行',\n    'insert_column_before' => '在左侧插入列',\n    'insert_column_after' => '在右侧插入列',\n    'delete_column' => '删除列',\n    'table_cell' => '单元格',\n    'table_row' => '行',\n    'table_column' => '列',\n    'cell_properties' => '单元格属性',\n    'cell_properties_title' => '单元格属性',\n    'cell_type' => '单元格类型',\n    'cell_type_cell' => '单元格',\n    'cell_scope' => '范围',\n    'cell_type_header' => '表头',\n    'merge_cells' => '合并单元格',\n    'split_cell' => '拆分单元格',\n    'table_row_group' => '按行分组',\n    'table_column_group' => '按列分组',\n    'horizontal_align' => '水平对齐',\n    'vertical_align' => '垂直对齐',\n    'border_width' => '边框宽度',\n    'border_style' => '边框样式',\n    'border_color' => '边框颜色',\n    'row_properties' => '行属性',\n    'row_properties_title' => '行属性',\n    'cut_row' => '剪切行',\n    'copy_row' => '复制行',\n    'paste_row_before' => '在上方粘贴行',\n    'paste_row_after' => '在下方粘贴行',\n    'row_type' => '行类型',\n    'row_type_header' => '表头',\n    'row_type_body' => '表体',\n    'row_type_footer' => '表脚',\n    'alignment' => '对齐方式',\n    'cut_column' => '剪切列',\n    'copy_column' => '复制列',\n    'paste_column_before' => '在左侧粘贴列',\n    'paste_column_after' => '在右侧粘贴列',\n    'cell_padding' => '单元格间距',\n    'cell_spacing' => '单元格间距',\n    'caption' => '标题',\n    'show_caption' => '显示标题',\n    'constrain' => '保持宽高比',\n    'cell_border_solid' => '实线',\n    'cell_border_dotted' => '点虚线',\n    'cell_border_dashed' => '短虚线',\n    'cell_border_double' => '双实线',\n    'cell_border_groove' => '浮入',\n    'cell_border_ridge' => '浮出',\n    'cell_border_inset' => '陷入',\n    'cell_border_outset' => '突出',\n    'cell_border_none' => '无',\n    'cell_border_hidden' => '隐藏边框',\n\n    // Images, links, details/summary & embed\n    'source' => '来源',\n    'alt_desc' => '替代描述',\n    'embed' => '嵌入',\n    'paste_embed' => '在下面粘贴您的嵌入代码：',\n    'url' => '网址',\n    'text_to_display' => '要显示的文本',\n    'title' => '标题',\n    'browse_links' => '浏览链接',\n    'open_link' => '打开链接',\n    'open_link_in' => '打开链接于……',\n    'open_link_current' => '覆盖当前窗口',\n    'open_link_new' => '新建窗口',\n    'remove_link' => '移除链接',\n    'insert_collapsible' => '插入可折叠块',\n    'collapsible_unwrap' => '展开',\n    'edit_label' => '编辑标签',\n    'toggle_open_closed' => '切换打开/关闭',\n    'collapsible_edit' => '编辑可折叠块',\n    'toggle_label' => '切换标签',\n\n    // About view\n    'about' => '关于编辑器',\n    'about_title' => '关于所见即所得（WYSIWYG）编辑器',\n    'editor_license' => '编辑器许可证与版权信息',\n    'editor_lexical_license' => '该编辑器是 :lexicalLink 的一个分支构建的，并根据 MIT 许可证进行分发.',\n    'editor_lexical_license_link' => '许可证详细信息请点击此处',\n    'editor_tiny_license' => '此编辑器是用 :tinyLink 构建的，基于 MIT 许可证。',\n    'editor_tiny_license_link' => 'TinyMCE 的版权和许可证详细信息可以在这里找到。',\n    'save_continue' => '保存页面并继续',\n    'callouts_cycle' => '(继续按下以切换类型)',\n    'link_selector' => '链接到内容',\n    'shortcuts' => '快捷键',\n    'shortcut' => '快捷键',\n    'shortcuts_intro' => '编辑器中提供了以下快捷键：',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => '描述',\n];\n"
  },
  {
    "path": "lang/zh_CN/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => '最近创建',\n    'recently_created_pages' => '最近创建的页面',\n    'recently_updated_pages' => '最近更新的页面',\n    'recently_created_chapters' => '最近创建的章节',\n    'recently_created_books' => '最近创建的图书',\n    'recently_created_shelves' => '最近创建的书架',\n    'recently_update' => '最近更新',\n    'recently_viewed' => '最近查看',\n    'recent_activity' => '近期活动',\n    'create_now' => '立刻创建',\n    'revisions' => '修订历史',\n    'meta_revision' => '版本号 #:revisionCount',\n    'meta_created' => '创建于 :timeLength',\n    'meta_created_name' => '由 :user 创建于 :timeLength',\n    'meta_updated' => '更新于 :timeLength',\n    'meta_updated_name' => '由 :user 更新于 :timeLength',\n    'meta_owned_name' => '拥有者 :user',\n    'meta_reference_count' => '被 :count 个页面引用|被 :count 个页面引用',\n    'entity_select' => '选择项目',\n    'entity_select_lack_permission' => '您没有选择此项目所需的权限',\n    'images' => '图片',\n    'my_recent_drafts' => '我最近的草稿',\n    'my_recently_viewed' => '我最近看过',\n    'my_most_viewed_favourites' => '我浏览最多的收藏',\n    'my_favourites' => '我的收藏',\n    'no_pages_viewed' => '您尚未查看任何页面',\n    'no_pages_recently_created' => '最近没有页面被创建',\n    'no_pages_recently_updated' => '最近没有页面被更新',\n    'export' => '导出',\n    'export_html' => '网页文件',\n    'export_pdf' => 'PDF文件',\n    'export_text' => '纯文本文件',\n    'export_md' => 'Markdown 文件',\n    'export_zip' => '便携版本 ZIP',\n    'default_template' => '默认页面模板',\n    'default_template_explain' => '指定一个页面模板，该模板将作为此项目中所有页面的默认内容。请注意，仅当页面创建者具有对所选页面模板的查看访问权限时，此功能才会生效。',\n    'default_template_select' => '选择模板页面',\n    'import' => '导入',\n    'import_validate' => '验证导入',\n    'import_desc' => '使用便携式 zip 导出从相同或不同的实例导入书籍、章节和页面。选择一个 ZIP 文件以继续。文件上传并验证后，您就可以在下一个视图中配置和确认导入。',\n    'import_zip_select' => '选择要上传的 ZIP 文件',\n    'import_zip_validation_errors' => '验证提供的 ZIP 文件时检测到错误:',\n    'import_pending' => '等待导入',\n    'import_pending_none' => '尚未开始导入。',\n    'import_continue' => '继续导入',\n    'import_continue_desc' => '查看从上传的 ZIP 文件中导入的内容。准备就绪后，运行导入将其内容添加到本系统中。成功导入后，上传的 ZIP 导入文件将自动删除。',\n    'import_details' => '导入详情',\n    'import_run' => '执行导入',\n    'import_size' => ':size 导入 ZIP 文件大小',\n    'import_uploaded_at' => '上传时间 :relativeTime',\n    'import_uploaded_by' => '上传者',\n    'import_location' => '导入位置',\n    'import_location_desc' => '为导入的内容选择目标位置。您需要相关权限才能在所选位置内进行创建',\n    'import_delete_confirm' => '您确定要删除此导入吗?',\n    'import_delete_desc' => '这将删除上传的ZIP文件，不能撤消。',\n    'import_errors' => '导入错误',\n    'import_errors_desc' => '在尝试导入过程中出现了以下错误:',\n    'breadcrumb_siblings_for_page' => '导航页面',\n    'breadcrumb_siblings_for_chapter' => '导航章节',\n    'breadcrumb_siblings_for_book' => '导航书籍',\n    'breadcrumb_siblings_for_bookshelf' => '导航书架',\n\n    // Permissions and restrictions\n    'permissions' => '权限',\n    'permissions_desc' => '在此处设置权限以覆盖用户角色提供的默认权限。',\n    'permissions_book_cascade' => '书籍上设置的权限将自动应用到子章节和子页面，除非它们有自己的权限设置。',\n    'permissions_chapter_cascade' => '章节上设置的权限将自动应用到子页面，除非它们有自己的权限设置。',\n    'permissions_save' => '保存权限',\n    'permissions_owner' => '拥有者',\n    'permissions_role_everyone_else' => '其他所有人',\n    'permissions_role_everyone_else_desc' => '为所有未被特别覆盖的角色设置权限。',\n    'permissions_role_override' => '覆盖角色权限',\n    'permissions_inherit_defaults' => '继承默认值',\n\n    // Search\n    'search_results' => '搜索结果',\n    'search_total_results_found' => '共找到了:count个结果',\n    'search_clear' => '清除搜索',\n    'search_no_pages' => '没有找到相匹配的页面',\n    'search_for_term' => '“:term”的搜索结果',\n    'search_more' => '更多结果',\n    'search_advanced' => '高级搜索',\n    'search_terms' => '搜索关键词',\n    'search_content_type' => '种类',\n    'search_exact_matches' => '精确匹配',\n    'search_tags' => '标签搜索',\n    'search_options' => '选项',\n    'search_viewed_by_me' => '我看过的',\n    'search_not_viewed_by_me' => '我没看过的',\n    'search_permissions_set' => '权限设置',\n    'search_created_by_me' => '我创建的',\n    'search_updated_by_me' => '我更新的',\n    'search_owned_by_me' => '我拥有的',\n    'search_date_options' => '日期选项',\n    'search_updated_before' => '在此之前更新',\n    'search_updated_after' => '在此之后更新',\n    'search_created_before' => '在此之前创建',\n    'search_created_after' => '在此之后创建',\n    'search_set_date' => '设置日期',\n    'search_update' => '只显示更新操作',\n\n    // Shelves\n    'shelf' => '书架',\n    'shelves' => '书架',\n    'x_shelves' => ':count 书架|:count 书架',\n    'shelves_empty' => '当前未创建书架',\n    'shelves_create' => '创建新书架',\n    'shelves_popular' => '热门书架',\n    'shelves_new' => '新书架',\n    'shelves_new_action' => '新书架',\n    'shelves_popular_empty' => '最热门的书架',\n    'shelves_new_empty' => '最新创建的书架',\n    'shelves_save' => '保存书架',\n    'shelves_books' => '书籍已在此书架里',\n    'shelves_add_books' => '将书籍加入此书架',\n    'shelves_drag_books' => '拖动下面的书籍将其添加到此书架',\n    'shelves_empty_contents' => '这个书架没有分配书籍',\n    'shelves_edit_and_assign' => '编辑书架以分配书籍',\n    'shelves_edit_named' => '编辑书架 :name',\n    'shelves_edit' => '编辑书架',\n    'shelves_delete' => '删除书架',\n    'shelves_delete_named' => '删除书架 :name',\n    'shelves_delete_explain' => \"此操作将删除书架 ”:name”。书架中的书籍不会被删除。\",\n    'shelves_delete_confirmation' => '您确定要删除此书架吗？',\n    'shelves_permissions' => '书架权限',\n    'shelves_permissions_updated' => '书架权限已更新',\n    'shelves_permissions_active' => '书架权限已启用',\n    'shelves_permissions_cascade_warning' => '书架上的权限不会自动应用到书架里的书籍上，这是因为书籍可以在多个书架上存在。使用下面的选项可以将权限复制到书架里的书籍上。',\n    'shelves_permissions_create' => '书架创建权限仅用于使用下面的操作将权限复制到子书籍。这个权限不是用来控制创建书籍的。',\n    'shelves_copy_permissions_to_books' => '将权限复制到书籍',\n    'shelves_copy_permissions' => '复制权限',\n    'shelves_copy_permissions_explain' => '此操作会将此书架的当前权限设置应用于其中包含的所有书籍上。 启用前请确保已保存对此书架权限的任何更改。',\n    'shelves_copy_permission_success' => '书架权限已复制到 :count 本书籍上',\n\n    // Books\n    'book' => '书籍',\n    'books' => '书籍',\n    'x_books' => ':count 本书',\n    'books_empty' => '不存在已创建的书',\n    'books_popular' => '热门书籍',\n    'books_recent' => '最近的书',\n    'books_new' => '新书',\n    'books_new_action' => '新书',\n    'books_popular_empty' => '最受欢迎的书籍将出现在这里。',\n    'books_new_empty' => '最近创建的书籍将出现在这里。',\n    'books_create' => '创建书籍',\n    'books_delete' => '删除书籍',\n    'books_delete_named' => '删除书籍「:bookName」',\n    'books_delete_explain' => '此操作将删除书籍 “:bookName”。书籍中的所有的章节和页面都会被删除。',\n    'books_delete_confirmation' => '您确定要删除此书籍吗？',\n    'books_edit' => '编辑书籍',\n    'books_edit_named' => '编辑书籍「:bookName」',\n    'books_form_book_name' => '书名',\n    'books_save' => '保存书籍',\n    'books_permissions' => '书籍权限',\n    'books_permissions_updated' => '书籍权限已更新',\n    'books_empty_contents' => '本书目前没有页面或章节。',\n    'books_empty_create_page' => '创建页面',\n    'books_empty_sort_current_book' => '排序当前书籍',\n    'books_empty_add_chapter' => '添加章节',\n    'books_permissions_active' => '书籍权限已启用',\n    'books_search_this' => '搜索这本书',\n    'books_navigation' => '书籍导航',\n    'books_sort' => '排序书籍内容',\n    'books_sort_desc' => '在书籍内部移动章节与页面以重组内容；支持添加其他书籍，实现跨书籍便捷移动章节与页面；还可设置自动排序规则，在内容发生变更时自动对本书内容进行排序。',\n    'books_sort_auto_sort' => '自动排序选项',\n    'books_sort_auto_sort_active' => '自动排序已激活：::sortName',\n    'books_sort_named' => '排序书籍「:bookName」',\n    'books_sort_name' => '按名称排序',\n    'books_sort_created' => '创建时间排序',\n    'books_sort_updated' => '按更新时间排序',\n    'books_sort_chapters_first' => '章节正序',\n    'books_sort_chapters_last' => '章节倒序',\n    'books_sort_show_other' => '显示其他书籍',\n    'books_sort_save' => '保存新顺序',\n    'books_sort_show_other_desc' => '在此添加其他书籍进入排序界面，这样就可以轻松跨书籍重新排序。',\n    'books_sort_move_up' => '上移',\n    'books_sort_move_down' => '下移',\n    'books_sort_move_prev_book' => '移动到上一书籍',\n    'books_sort_move_next_book' => '移动到下一书籍',\n    'books_sort_move_prev_chapter' => '移动到上一章节',\n    'books_sort_move_next_chapter' => '移动到下一章节',\n    'books_sort_move_book_start' => '移动到书籍开头',\n    'books_sort_move_book_end' => '移动到书籍结尾',\n    'books_sort_move_before_chapter' => '移动到章节前',\n    'books_sort_move_after_chapter' => '移至章节后',\n    'books_copy' => '复制书籍',\n    'books_copy_success' => '书籍已成功复制',\n\n    // Chapters\n    'chapter' => '章节',\n    'chapters' => '章节',\n    'x_chapters' => ':count个章节',\n    'chapters_popular' => '热门章节',\n    'chapters_new' => '新章节',\n    'chapters_create' => '创建章节',\n    'chapters_delete' => '删除章节',\n    'chapters_delete_named' => '删除章节「:chapterName」',\n    'chapters_delete_explain' => '此操作将删除章节 “:chapterName”。章节中的所有页面都会被删除。',\n    'chapters_delete_confirm' => '您确定要删除此章节吗？',\n    'chapters_edit' => '编辑章节',\n    'chapters_edit_named' => '编辑章节「:chapterName」',\n    'chapters_save' => '保存章节',\n    'chapters_move' => '移动章节',\n    'chapters_move_named' => '移动章节「:chapterName」',\n    'chapters_copy' => '复制章节',\n    'chapters_copy_success' => '章节已成功复制',\n    'chapters_permissions' => '章节权限',\n    'chapters_empty' => '本章目前没有页面。',\n    'chapters_permissions_active' => '章节权限已启用',\n    'chapters_permissions_success' => '章节权限已更新',\n    'chapters_search_this' => '从本章节搜索',\n    'chapter_sort_book' => '排序书籍',\n\n    // Pages\n    'page' => '页面',\n    'pages' => '页面',\n    'x_pages' => ':count个页面',\n    'pages_popular' => '热门页面',\n    'pages_new' => '新页面',\n    'pages_attachments' => '附件',\n    'pages_navigation' => '页面导航',\n    'pages_delete' => '删除页面',\n    'pages_delete_named' => '删除页面“:pageName”',\n    'pages_delete_draft_named' => '删除草稿页面“:pageName”',\n    'pages_delete_draft' => '删除草稿页面',\n    'pages_delete_success' => '页面已删除',\n    'pages_delete_draft_success' => '草稿页面已删除',\n    'pages_delete_warning_template' => '此页面是当前书籍或章节的默认页面模板。删除此页面后，这些书籍或章节的默认页面模板将被取消。',\n    'pages_delete_confirm' => '您确定要删除此页面吗？',\n    'pages_delete_draft_confirm' => '您确定要删除此草稿页面吗？',\n    'pages_editing_named' => '正在编辑页面“:pageName”',\n    'pages_edit_draft_options' => '草稿选项',\n    'pages_edit_save_draft' => '保存草稿',\n    'pages_edit_draft' => '编辑页面草稿',\n    'pages_editing_draft' => '正在编辑草稿',\n    'pages_editing_page' => '正在编辑页面',\n    'pages_edit_draft_save_at' => '草稿保存于 ',\n    'pages_edit_delete_draft' => '删除草稿',\n    'pages_edit_delete_draft_confirm' => '您确定要删除您的草稿页面更改吗？自上次完整保存以来的所有更改都将丢失，编辑器将更新为最新非草稿页面。',\n    'pages_edit_discard_draft' => '放弃草稿',\n    'pages_edit_switch_to_markdown' => '切换到 Markdown 编辑器',\n    'pages_edit_switch_to_markdown_clean' => '（整理内容）',\n    'pages_edit_switch_to_markdown_stable' => '（保留内容）',\n    'pages_edit_switch_to_wysiwyg' => '切换到所见即所得编辑器',\n    'pages_edit_switch_to_new_wysiwyg' => '切换到新的所见即所得',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '（正在Beta测试中）',\n    'pages_edit_set_changelog' => '更新说明',\n    'pages_edit_enter_changelog_desc' => '输入对您所做更改的简要说明',\n    'pages_edit_enter_changelog' => '输入更新说明',\n    'pages_editor_switch_title' => '切换编辑器',\n    'pages_editor_switch_are_you_sure' => '您确定要更改此页面的编辑器吗？',\n    'pages_editor_switch_consider_following' => '更改编辑器时请注意以下事项：',\n    'pages_editor_switch_consideration_a' => '一旦保存，任何未来的编辑都将使用新的编辑器，包括那些没有权限自行更改编辑器类型的用户。',\n    'pages_editor_switch_consideration_b' => '在某些情况下这可能会导致丢失页面格式或功能损坏。',\n    'pages_editor_switch_consideration_c' => '上次保存后修改的标签和更改日志将不会被保存。',\n    'pages_save' => '保存页面',\n    'pages_title' => '页面标题',\n    'pages_name' => '页面名',\n    'pages_md_editor' => '编者',\n    'pages_md_preview' => '预览',\n    'pages_md_insert_image' => '插入图片',\n    'pages_md_insert_link' => '插入项目链接',\n    'pages_md_insert_drawing' => '插入图表',\n    'pages_md_show_preview' => '显示预览',\n    'pages_md_sync_scroll' => '同步预览滚动',\n    'pages_md_plain_editor' => '纯文本编辑器',\n    'pages_drawing_unsaved' => '找到未保存的绘图',\n    'pages_drawing_unsaved_confirm' => '从之前保存失败的绘图中发现了可恢复的数据。您想恢复并继续编辑这个未保存的绘图吗？',\n    'pages_not_in_chapter' => '本页面不在某章节中',\n    'pages_move' => '移动页面',\n    'pages_copy' => '复制页面',\n    'pages_copy_desination' => '复制目的地',\n    'pages_copy_success' => '页面复制完成',\n    'pages_permissions' => '页面权限',\n    'pages_permissions_success' => '页面权限已更新',\n    'pages_revision' => '修订',\n    'pages_revisions' => '页面修订',\n    'pages_revisions_desc' => '下面列出的是该页面的所有过去修订。如果权限允许，您可以回顾、比较和恢复旧的页面版本。页面的完整历史可能不会在这里完全反映出来，因为根据系统配置，旧的修订可能会被自动删除。',\n    'pages_revisions_named' => '“:pageName”页面修订',\n    'pages_revision_named' => '“:pageName”页面修订',\n    'pages_revision_restored_from' => '恢复到 #:id :summary',\n    'pages_revisions_created_by' => '创建者',\n    'pages_revisions_date' => '修订日期',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => '修订号',\n    'pages_revisions_numbered' => '修订 #:id',\n    'pages_revisions_numbered_changes' => '修改 #:id ',\n    'pages_revisions_editor' => '编辑器类型',\n    'pages_revisions_changelog' => '更新说明',\n    'pages_revisions_changes' => '查看更改',\n    'pages_revisions_current' => '当前版本',\n    'pages_revisions_preview' => '预览',\n    'pages_revisions_restore' => '恢复',\n    'pages_revisions_none' => '此页面没有修订',\n    'pages_copy_link' => '复制链接',\n    'pages_edit_content_link' => '跳转到编辑器中的部分',\n    'pages_pointer_enter_mode' => '进入部分选择模式',\n    'pages_pointer_label' => '页面部分选项',\n    'pages_pointer_permalink' => '页面部分永久链接',\n    'pages_pointer_include_tag' => '页面部分包含标签',\n    'pages_pointer_toggle_link' => '永久链接模式，按下显示包含标签',\n    'pages_pointer_toggle_include' => '包含标签模式，按下显示永久链接',\n    'pages_permissions_active' => '页面权限已启用',\n    'pages_initial_revision' => '初始发布',\n    'pages_references_update_revision' => '系统自动更新的内部链接',\n    'pages_initial_name' => '新页面',\n    'pages_editing_draft_notification' => '您正在编辑在 :timeDiff 内保存的草稿.',\n    'pages_draft_edited_notification' => '此后，此页面已经被更新，建议您放弃此草稿。',\n    'pages_draft_page_changed_since_creation' => '这个页面在您的草稿创建后被其他用户更新了，您目前的草稿不包含新的内容。建议您放弃此草稿，或是注意不要覆盖新的页面更改。',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count 位用户已开始编辑此页面',\n        'start_b' => '用户 “:userName” 已经开始编辑此页面',\n        'time_a' => '自页面上次更新以来',\n        'time_b' => '在最近 :minCount 分钟',\n        'message' => ':time :start。注意不要覆盖他人的更新！',\n    ],\n    'pages_draft_discarded' => '草稿已丢弃！编辑器已更新到当前页面内容',\n    'pages_draft_deleted' => '草稿已删除！编辑器已更新为当前页面内容',\n    'pages_specific' => '具体页面',\n    'pages_is_template' => '页面模板',\n\n    // Editor Sidebar\n    'toggle_sidebar' => '切换侧边栏',\n    'page_tags' => '页面标签',\n    'chapter_tags' => '章节标签',\n    'book_tags' => '书籍标签',\n    'shelf_tags' => '书架标签',\n    'tag' => '标签',\n    'tags' =>  '标签',\n    'tags_index_desc' => '标签是一种灵活的分类形式，可以应用于系统内的内容。标签可以有一个键和值，值是可选的。应用后就可以使用标签的名称和值来搜索内容。',\n    'tag_name' =>  '标签名称',\n    'tag_value' => '标签值 (可选)',\n    'tags_explain' => \"添加一些标签以更好地对您的内容进行分类。\\n您可以为标签分配一个值，以进行更好的进行管理。\",\n    'tags_add' => '添加另一个标签',\n    'tags_remove' => '删除此标签',\n    'tags_usages' => '标签总使用量',\n    'tags_assigned_pages' => '有这个标签的页面',\n    'tags_assigned_chapters' => '有这个标签的章节',\n    'tags_assigned_books' => '有这个标签的书籍',\n    'tags_assigned_shelves' => '有这个标签的书架',\n    'tags_x_unique_values' => ':count 个不重复项目',\n    'tags_all_values' => '所有值',\n    'tags_view_tags' => '查看标签',\n    'tags_view_existing_tags' => '查看已有的标签',\n    'tags_list_empty_hint' => '您可以在页面编辑器的侧边栏添加标签，或者在编辑书籍、章节、书架时添加。',\n    'attachments' => '附件',\n    'attachments_explain' => '上传一些文件或附加一些链接显示在您的网页上。这些在页面的侧边栏中可见。',\n    'attachments_explain_instant_save' => '这里的更改将立即保存。',\n    'attachments_upload' => '上传文件',\n    'attachments_link' => '附加链接',\n    'attachments_upload_drop' => '或者，您也可以将文件拖放到这里并将其作为附件上传',\n    'attachments_set_link' => '设置链接',\n    'attachments_delete' => '您确定要删除此附件吗？',\n    'attachments_dropzone' => '将文件拖放到此处上传',\n    'attachments_no_files' => '尚未上传文件',\n    'attachments_explain_link' => '如果您不想上传文件，则可以附加链接，这可以是指向其他页面的链接，也可以是指向云端文件的链接。',\n    'attachments_link_name' => '链接名',\n    'attachment_link' => '附件链接',\n    'attachments_link_url' => '链接到文件',\n    'attachments_link_url_hint' => '网站或文件的网址',\n    'attach' => '附加',\n    'attachments_insert_link' => '将附加链接添加到页面',\n    'attachments_edit_file' => '编辑文件',\n    'attachments_edit_file_name' => '文件名',\n    'attachments_edit_drop_upload' => '删除文件或点击这里上传并覆盖',\n    'attachments_order_updated' => '附件顺序已更新',\n    'attachments_updated_success' => '附件信息已更新',\n    'attachments_deleted' => '附件已删除',\n    'attachments_file_uploaded' => '附件上传成功',\n    'attachments_file_updated' => '附件更新成功',\n    'attachments_link_attached' => '链接成功附加到页面',\n    'templates' => '模板',\n    'templates_set_as_template' => '设置为模板',\n    'templates_explain_set_as_template' => '您可以将此页面设置为模板，以便在创建其他页面时利用其内容。 如果其他用户对此页面具有查看权限，则将可以使用此模板。',\n    'templates_replace_content' => '替换页面内容',\n    'templates_append_content' => '附加到页面内容',\n    'templates_prepend_content' => '追加到页面内容',\n\n    // Profile View\n    'profile_user_for_x' => '来这里:time了',\n    'profile_created_content' => '已创建内容',\n    'profile_not_created_pages' => ':userName尚未创建任何页面',\n    'profile_not_created_chapters' => ':userName尚未创建任何章节',\n    'profile_not_created_books' => ':userName尚未创建任何书籍',\n    'profile_not_created_shelves' => ':userName 尚未创建任何书架',\n\n    // Comments\n    'comment' => '评论',\n    'comments' => '评论',\n    'comment_add' => '添加评论',\n    'comment_none' => '没有要显示的评论',\n    'comment_placeholder' => '在这里评论',\n    'comment_thread_count' => ':count 条',\n    'comment_archived_count' => ':count 条评论已存档',\n    'comment_archived_threads' => '已存档的贴子',\n    'comment_save' => '保存评论',\n    'comment_new' => '新评论',\n    'comment_created' => '评论于 :createDiff',\n    'comment_updated' => '更新于 :updateDiff (:username)',\n    'comment_updated_indicator' => '已更新',\n    'comment_deleted_success' => '评论已删除',\n    'comment_created_success' => '评论已添加',\n    'comment_updated_success' => '评论已更新',\n    'comment_archive_success' => '评论已存档',\n    'comment_unarchive_success' => '评论已取消存档',\n    'comment_view' => '查看评论',\n    'comment_jump_to_thread' => '跳转到贴子',\n    'comment_delete_confirm' => '您确定要删除这条评论？',\n    'comment_in_reply_to' => '回复 :commentId',\n    'comment_reference' => '参考',\n    'comment_reference_outdated' => '（已过时）',\n    'comment_editor_explain' => '这里是此页面上留下的评论。查看已保存的页面时可以添加和管理评论。',\n\n    // Revision\n    'revision_delete_confirm' => '您确定要删除此修订版吗？',\n    'revision_restore_confirm' => '您确定要恢复到此修订版吗？恢复后当前页面内容将被替换。',\n    'revision_cannot_delete_latest' => '无法删除最新版本。',\n\n    // Copy view\n    'copy_consider' => '复制内容时请注意以下事项。',\n    'copy_consider_permissions' => '自定义权限设置将不会被复制。',\n    'copy_consider_owner' => '您将成为所有已复制内容的所有者。',\n    'copy_consider_images' => '页面中的图像文件不会被复制，原始图像将保留它们与最初上传到的页面的关系。',\n    'copy_consider_attachments' => '页面中的附件不会被复制。',\n    'copy_consider_access' => '改变位置、所有者或权限可能会导致此内容被以前无法访问的人访问。',\n\n    // Conversions\n    'convert_to_shelf' => '转换为书架',\n    'convert_to_shelf_contents_desc' => '你可以将这本书转换为具有相同内容的新书架。本书中的章节将被转换为书籍。如果这本书包含有任何不在章节分类中的页面，那么将会有一本单独的书籍包含这些页面，这本书也将成为新书架的一部分。',\n    'convert_to_shelf_permissions_desc' => '在这本书上设置的任何权限都将复制到所有未强制执行权限的新书架和新子书籍上。请注意，书架上的权限不会像书籍那样继承到内容物上。',\n    'convert_book' => '转换书籍',\n    'convert_book_confirm' => '您确定要转换此书籍吗？',\n    'convert_undo_warning' => '这可不能轻易撤消。',\n    'convert_to_book' => '转换为书籍',\n    'convert_to_book_desc' => '您可以将此章节转换为具有相同内容的新书籍。此章节中设置的任何权限都将复制到新书籍上，但从父书籍继承的任何权限都不会被复制，这可能会导致访问控制发生变化。',\n    'convert_chapter' => '转换章节',\n    'convert_chapter_confirm' => '您确定要转换此章节吗？',\n\n    // References\n    'references' => '引用',\n    'references_none' => '没有跟踪到对此项目的引用。',\n    'references_to_desc' => '下方列出了系统中链接到此项目的所有已知内容。',\n\n    // Watch Options\n    'watch' => '关注',\n    'watch_title_default' => '默认偏好设置',\n    'watch_desc_default' => '将关注设置恢复为默认通知偏好设置。',\n    'watch_title_ignore' => '忽略',\n    'watch_desc_ignore' => '忽略所有通知，包括来自用户级偏好的通知。',\n    'watch_title_new' => '新页面',\n    'watch_desc_new' => '在此项目中创建任何新页面时通知我。',\n    'watch_title_updates' => '所有页面更新',\n    'watch_desc_updates' => '在所有新页面和页面更改时通知我。',\n    'watch_desc_updates_page' => '在有页面发生更改时通知我。',\n    'watch_title_comments' => '所有页面更新和评论',\n    'watch_desc_comments' => '在有新页面、页面更改和新评论时通知我。',\n    'watch_desc_comments_page' => '在有页面更改和新评论时通知我。',\n    'watch_change_default' => '更改默认通知偏好',\n    'watch_detail_ignore' => '忽略通知',\n    'watch_detail_new' => '已关注新页面',\n    'watch_detail_updates' => '已关注新页面和更新',\n    'watch_detail_comments' => '已关注新页面、更新和评论',\n    'watch_detail_parent_book' => '已关注—继承自父书籍',\n    'watch_detail_parent_book_ignore' => '已忽略—继承自父书籍',\n    'watch_detail_parent_chapter' => '已关注—继承自父章节',\n    'watch_detail_parent_chapter_ignore' => '已忽略—继承自父章节',\n];\n"
  },
  {
    "path": "lang/zh_CN/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => '您无权访问所请求的页面。',\n    'permissionJson' => '您无权执行所请求的操作。',\n\n    // Auth\n    'error_user_exists_different_creds' => 'Email为 :email 的用户已经存在，但具有不同的凭据。',\n    'auth_pre_register_theme_prevention' => '无法根据所提供的信息注册帐户',\n    'email_already_confirmed' => 'Email已被确认，请尝试登录。',\n    'email_confirmation_invalid' => '此确认令牌无效或已被使用，请重新注册。',\n    'email_confirmation_expired' => '确认令牌已过期，已发送新的确认电子邮件。',\n    'email_confirmation_awaiting' => '需要认证账户的电子邮箱地址',\n    'ldap_fail_anonymous' => '使用匿名绑定的LDAP访问失败。',\n    'ldap_fail_authed' => '带有标识名称和密码的LDAP访问失败。',\n    'ldap_extension_not_installed' => '未安装LDAP PHP扩展程序',\n    'ldap_cannot_connect' => '无法连接到ldap服务器，初始连接失败',\n    'saml_already_logged_in' => '您已经登陆了',\n    'saml_no_email_address' => '无法找到有效Email地址，此用户数据由外部身份验证系统托管',\n    'saml_invalid_response_id' => '来自外部身份验证系统的请求没有被本应用程序认证，在登录后返回上一页可能会导致此问题。',\n    'saml_fail_authed' => '使用 :system 登录失败，登录系统未返回成功登录授权信息。',\n    'oidc_already_logged_in' => '您已经登陆了',\n    'oidc_no_email_address' => '无法找到有效的 Email 地址，此用户数据由外部身份验证系统托管',\n    'oidc_fail_authed' => '使用 :system 登录失败，登录系统未返回成功登录授权信息',\n    'social_no_action_defined' => '没有定义行为',\n    'social_login_bad_response' => \"在 :socialAccount 登录时遇到错误：\\n:error\",\n    'social_account_in_use' => ':socialAccount 账户已被使用，请尝试通过 :socialAccount 选项登录。',\n    'social_account_email_in_use' => 'Email :email 已经被使用。如果您已有账户，则可以在个人资料设置中绑定您的 :socialAccount。',\n    'social_account_existing' => ':socialAccount已经被绑定到您的账户。',\n    'social_account_already_used_existing' => ':socialAccount账户已经被其他用户使用。',\n    'social_account_not_used' => ':socialAccount账户没有绑定到任何用户，请在您的个人资料设置中绑定。',\n    'social_account_register_instructions' => '如果您还没有账户，您可以使用 :socialAccount 选项注册账户。',\n    'social_driver_not_found' => '未找到社交驱动程序',\n    'social_driver_not_configured' => '您的:socialAccount社交设置不正确。',\n    'invite_token_expired' => '此邀请链接已过期。 您可以尝试重置您的账户密码。',\n    'login_user_not_found' => '找不到执行此操作的用户。',\n\n    // System\n    'path_not_writable' => '无法上传到文件路径“:filePath”，请确保它可写入服务器。',\n    'cannot_get_image_from_url' => '无法从 :url 中获取图片',\n    'cannot_create_thumbs' => '服务器无法创建缩略图，请检查您是否安装了GD PHP扩展。',\n    'server_upload_limit' => '服务器不允许上传此大小的文件。 请尝试较小的文件。',\n    'server_post_limit' => '服务器无法接收所提供的数据量。请尝试使用较少的数据或较小的文件。',\n    'uploaded'  => '服务器不允许上传此大小的文件。 请尝试较小的文件。',\n\n    // Drawing & Images\n    'image_upload_error' => '上传图片时发生错误',\n    'image_upload_type_error' => '上传的图像类型无效',\n    'image_upload_replace_type' => '图片文件替换必须为相同的类型',\n    'image_upload_memory_limit' => '由于系统资源限制，无法处理图像上传和/或创建缩略图。',\n    'image_thumbnail_memory_limit' => '由于系统资源限制，无法创建图像大小变化。',\n    'image_gallery_thumbnail_memory_limit' => '由于系统资源限制，无法创建相册缩略图。',\n    'drawing_data_not_found' => '无法加载绘图数据。绘图文件可能不再存在，或者您可能没有权限访问它。',\n\n    // Attachments\n    'attachment_not_found' => '找不到附件',\n    'attachment_upload_error' => '上传附件时出错',\n\n    // Pages\n    'page_draft_autosave_fail' => '无法保存草稿，确保您在保存页面之前已经连接到互联网',\n    'page_draft_delete_fail' => '无法删除页面草稿并获取当前页面已保存的内容',\n    'page_custom_home_deletion' => '无法删除一个被设置为主页的页面',\n\n    // Entities\n    'entity_not_found' => '未找到项目',\n    'bookshelf_not_found' => '未找到书架',\n    'book_not_found' => '未找到书籍',\n    'page_not_found' => '未找到页面',\n    'chapter_not_found' => '未找到章节',\n    'selected_book_not_found' => '选中的书未找到',\n    'selected_book_chapter_not_found' => '未找到所选的书籍或章节',\n    'guests_cannot_save_drafts' => '访客不能保存草稿',\n\n    // Users\n    'users_cannot_delete_only_admin' => '您不能删除唯一的管理员账户',\n    'users_cannot_delete_guest' => '您不能删除访客用户',\n    'users_could_not_send_invite' => '由于邀请电子邮件发送失败，无法创建用户',\n\n    // Roles\n    'role_cannot_be_edited' => '无法编辑该角色',\n    'role_system_cannot_be_deleted' => '无法删除系统角色',\n    'role_registration_default_cannot_delete' => '无法删除设置为默认注册的角色',\n    'role_cannot_remove_only_admin' => '该用户是分配给管理员角色的唯一用户。 在尝试在此处删除管理员角色之前，请将其分配给其他用户。',\n\n    // Comments\n    'comment_list' => '提取评论时出现错误。',\n    'cannot_add_comment_to_draft' => '您不能为草稿添加评论。',\n    'comment_add' => '添加/更新评论时发生错误。',\n    'comment_delete' => '删除评论时发生错误。',\n    'empty_comment' => '不能添加空的评论。',\n\n    // Error pages\n    '404_page_not_found' => '无法找到页面',\n    'sorry_page_not_found' => '对不起，无法找到您想访问的页面。',\n    'sorry_page_not_found_permission_warning' => '如果您确认这个页面存在，则代表您可能没有查看权限。',\n    'image_not_found' => '未找到图片',\n    'image_not_found_subtitle' => '对不起，无法找到您想访问的图片。',\n    'image_not_found_details' => '原本放在这里的图片已被删除。',\n    'return_home' => '返回主页',\n    'error_occurred' => '出现错误',\n    'app_down' => ':appName现在正在关闭',\n    'back_soon' => '请耐心等待网站的恢复。',\n\n    // Import\n    'import_zip_cant_read' => '无法读取 ZIP 文件。',\n    'import_zip_cant_decode_data' => '无法找到并解码 ZIP data.json 内容。',\n    'import_zip_no_data' => 'ZIP 文件数据没有预期的书籍、章节或页面内容。',\n    'import_zip_data_too_large' => '超出最大上传大小。',\n    'import_validation_failed' => '导入 ZIP 验证失败，出现错误：',\n    'import_zip_failed_notification' => 'ZIP 文件导入失败。',\n    'import_perms_books' => '您缺少创建书籍所需的权限。',\n    'import_perms_chapters' => '您缺少创建章节所需的权限。',\n    'import_perms_pages' => '您缺少创建页面所需的权限。',\n    'import_perms_images' => '您缺少创建图片所需的权限。',\n    'import_perms_attachments' => '您缺少创建附件所需的权限。',\n\n    // API errors\n    'api_no_authorization_found' => '未在请求中找到授权令牌',\n    'api_bad_authorization_format' => '已在请求中找到授权令牌，但格式貌似不正确',\n    'api_user_token_not_found' => '未找到与提供的授权令牌匹配的 API 令牌',\n    'api_incorrect_token_secret' => '给已给出的API所提供的密钥不正确',\n    'api_user_no_api_permission' => '使用过的 API 令牌的所有者没有进行API 调用的权限',\n    'api_user_token_expired' => '所使用的身份令牌已过期',\n    'api_cookie_auth_only_get' => '使用基于 Cookie 的身份验证 API 时，仅允许 GET 请求。',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => '发送测试电子邮件时出现错误：',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'URL 与已配置的 SSR 主机不匹配',\n];\n"
  },
  {
    "path": "lang/zh_CN/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => '页面上有新评论：:pageName',\n    'new_comment_intro' => '一位用户在 :appName: 的页面上发表了评论',\n    'new_page_subject' => '新页面：:pageName',\n    'new_page_intro' => ':appName: 中创建了一个新页面',\n    'updated_page_subject' => '页面更新：:pageName',\n    'updated_page_intro' => ':appName: 中的一个页面已被更新',\n    'updated_page_debounce' => '为了防止出现大量通知，一段时间内您不会收到同一编辑者再次编辑本页面的通知。',\n    'comment_mention_subject' => '在页面中被提及：:pageName',\n    'comment_mention_intro' => '在 :appName 中被提及：',\n\n    'detail_page_name' => '页面名称：',\n    'detail_page_path' => '页面路径：',\n    'detail_commenter' => '评论者：',\n    'detail_comment' => '评论：',\n    'detail_created_by' => '创建者：',\n    'detail_updated_by' => '更新者：',\n\n    'action_view_comment' => '查看评论',\n    'action_view_page' => '查看页面',\n\n    'footer_reason' => '向您发送此通知是因为 :link 涵盖了该项目的此类活动。',\n    'footer_reason_link' => '个人通知偏好设置',\n];\n"
  },
  {
    "path": "lang/zh_CN/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; 上一页',\n    'next'     => '下一页 &raquo;',\n\n];\n"
  },
  {
    "path": "lang/zh_CN/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => '密码必须至少包含六个字符并与确认相符。',\n    'user' => \"使用该Email地址的用户不存在。\",\n    'token' => '重置密码链接无法发送至此邮件地址。',\n    'sent' => '我们已经通过Email发送您的密码重置链接！',\n    'reset' => '您的密码已被重置！',\n\n];\n"
  },
  {
    "path": "lang/zh_CN/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => '我的账户',\n\n    'shortcuts' => '快捷键',\n    'shortcuts_interface' => '界面快捷方式首选项',\n    'shortcuts_toggle_desc' => '你可以启用或禁用键盘系统界面快捷键，这些快捷键用于导航和操作。',\n    'shortcuts_customize_desc' => '您可以自定义下面的每个快捷键。选择快捷方式输入后按下您想使用的按键组合即可。',\n    'shortcuts_toggle_label' => '启用键盘快捷键',\n    'shortcuts_section_navigation' => '导航',\n    'shortcuts_section_actions' => '通用操作',\n    'shortcuts_save' => '保存快捷键',\n    'shortcuts_overlay_desc' => '注意：当快捷键启用时，可以按\"?\"键来打开帮助，它将突出显示当前屏幕上可见操作的快捷键。',\n    'shortcuts_update_success' => '快捷键设置已更新！',\n    'shortcuts_overview_desc' => '管理可用于导航系统用户界面的快捷键。',\n\n    'notifications' => '通知偏好',\n    'notifications_desc' => '控制在系统内发生某些活动时您会收到的电子邮件通知。',\n    'notifications_opt_own_page_changes' => '在我拥有的页面被修改时通知我',\n    'notifications_opt_own_page_comments' => '在我拥有的页面上有新评论时通知我',\n    'notifications_opt_comment_mentions' => '当我在评论中被提及时通知我',\n    'notifications_opt_comment_replies' => '在有人回复我的频率时通知我',\n    'notifications_save' => '保存偏好设置',\n    'notifications_update_success' => '通知偏好设置已更新！',\n    'notifications_watched' => '已关注和忽略的项目',\n    'notifications_watched_desc' => '下面是已应用自定义关注选项的项目。要更新您的关注设置，请查看该项目，然后在该项目的侧边栏中找到关注选项。',\n\n    'auth' => '访问与安全',\n    'auth_change_password' => '更改密码',\n    'auth_change_password_desc' => '更改用于登录本应用的密码。 长度必须至少为 8 个字符。',\n    'auth_change_password_success' => '密码已更新！',\n\n    'profile' => '个人资料详情',\n    'profile_desc' => '管理您的账户细节，这些细节会向其他用户展示，除此之外，还包括用于交流和系统个性化的细节。',\n    'profile_view_public' => '查看公开个人资料',\n    'profile_name_desc' => '配置您的显示名称，名称将通过您执行的活动和您拥有的内容在系统中对其他用户可见。',\n    'profile_email_desc' => '此电子邮件将用于通知，并根据活跃的系统身份验证，用于系统访问。',\n    'profile_email_no_permission' => '很抱歉，您没有权限更改电子邮件地址。 如果您想要更改，您需要请管理员为您更改。',\n    'profile_avatar_desc' => '选择一张图片，图片将在系统中用于向他人展示您的形象。理想情况下，这张图片应为方形，宽高约为256像素。',\n    'profile_admin_options' => '管理员选项',\n    'profile_admin_options_desc' => '针对用户账户，您可以在应用的「设置 > 用户」区域找到其他管理员级别选项，例如管理角色分配等。',\n\n    'delete_account' => '删除账户',\n    'delete_my_account' => '删除我的账户',\n    'delete_my_account_desc' => '此操作将完全删除您在系统中的用户账户。您将无法恢复此账户或撤销此操作。您创建的内容，如创建的页面和上传的图片，将保留下来。',\n    'delete_my_account_warning' => '您确定要删除您的账号吗？',\n];\n"
  },
  {
    "path": "lang/zh_CN/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => '设置',\n    'settings_save' => '保存设置',\n    'system_version' => '系统版本',\n    'categories' => '类别',\n\n    // App Settings\n    'app_customization' => '个性化',\n    'app_features_security' => '功能与安全',\n    'app_name' => '站点名称',\n    'app_name_desc' => '此名称将在网页头部和系统发送的电子邮件中显示。',\n    'app_name_header' => '在网页头部显示站点名称？',\n    'app_public_access' => '访问权限',\n    'app_public_access_desc' => '启用此选项将允许未登录的用户访问站点内容。',\n    'app_public_access_desc_guest' => '可以通过“访客”用户来控制公共访问者的访问。',\n    'app_public_access_toggle' => '允许公众访问',\n    'app_public_viewing' => '允许公众查看？',\n    'app_secure_images' => '启用更高安全性的图片上传？',\n    'app_secure_images_toggle' => '启用更高安全性的图片上传',\n    'app_secure_images_desc' => '出于性能原因，所有图像都是公开的。这个选项会在图像的网址前添加一个随机的，难以猜测的字符串，从而使直接访问变得困难。',\n    'app_default_editor' => '默认页面编辑器',\n    'app_default_editor_desc' => '选择在编辑新页面时默认使用哪个编辑器。这可以在页面权限处覆盖。',\n    'app_custom_html' => '自定义HTML头部内容',\n    'app_custom_html_desc' => '此处添加的任何内容都将插入到每个页面的<head>部分的底部，这对于覆盖样式或添加分析代码很方便。',\n    'app_custom_html_disabled_notice' => '在此设置页面上禁用了自定义HTML标题内容，以确保可以恢复所有重大更改。',\n    'app_logo' => '站点Logo',\n    'app_logo_desc' => '这会在应用程序标题栏等区域使用。此图片的高度应为 86 像素。大图像将按比例缩小。',\n    'app_icon' => '应用程序图标',\n    'app_icon_desc' => '此图标用于浏览器选项卡和快捷方式图标。这应该是一个 256 像素的正方形 PNG 图片。',\n    'app_homepage' => '站点主页',\n    'app_homepage_desc' => '选择要在主页上显示的页面来替换默认的页面，选定页面的访问权限将被忽略。',\n    'app_homepage_select' => '选择一个页面',\n    'app_footer_links' => '页脚链接',\n    'app_footer_links_desc' => '添加在网站页脚中显示的链接。这些链接将显示在大多数页面的底部，也包括不需要登录的页面。您可以使用标签\"trans::<key>\"来使用系统定义的翻译。例如：使用\"trans::common.privacy_policy\"将显示为“隐私政策”，而\"trans::common.terms_of_service\"将显示为“服务条款”。',\n    'app_footer_links_label' => '链接标签',\n    'app_footer_links_url' => '链接 URL',\n    'app_footer_links_add' => '添加页脚链接',\n    'app_disable_comments' => '禁用评论',\n    'app_disable_comments_toggle' => '禁用评论',\n    'app_disable_comments_desc' => '在站点的所有页面上禁用评论， <br> 已有评论也不会显示出来。',\n\n    // Color settings\n    'color_scheme' => '应用程序配色方案',\n    'color_scheme_desc' => '设置要在应用程序界面中使用的颜色。 可以为深色和浅色模式分别配置颜色，以适合主题并确保易读性。',\n    'ui_colors_desc' => '设置应用程序的主颜色和默认链接颜色。主颜色主要用于页眉横幅、按钮和界面装饰。默认链接颜色用于基于文本的链接和操作，包括编写界面和应用程序界面。',\n    'app_color' => '主颜色',\n    'link_color' => '默认链接颜色',\n    'content_colors_desc' => '为页面组织层次结构中的所有元素设置颜色。为了便于阅读，建议选择与默认颜色亮度相似的颜色。',\n    'bookshelf_color' => '书架颜色',\n    'book_color' => '书籍颜色',\n    'chapter_color' => '章节颜色',\n    'page_color' => '页面颜色',\n    'page_draft_color' => '页面草稿颜色',\n\n    // Registration Settings\n    'reg_settings' => '注册设置',\n    'reg_enable' => '启用注册',\n    'reg_enable_toggle' => '启用注册',\n    'reg_enable_desc' => '启用注册后，用户将可以自己注册为站点用户。 注册后，他们将获得一个默认的单一用户角色。',\n    'reg_default_role' => '注册后的默认用户角色',\n    'reg_enable_external_warning' => '当启用外部LDAP或者SAML认证时，上面的选项会被忽略。当使用外部系统认证认证成功时，将自动创建非现有会员的用户账户。',\n    'reg_email_confirmation' => '邮件确认',\n    'reg_email_confirmation_toggle' => '需要电子邮件确认',\n    'reg_confirm_email_desc' => '如果使用域名限制，则需要电子邮件验证，并且该值将被忽略。',\n    'reg_confirm_restrict_domain' => '域名限制',\n    'reg_confirm_restrict_domain_desc' => '输入您想要限制注册的电子邮件域名列表（即只允许使用这些电子邮件域名注册），多个域名用英文逗号隔开。在允许用户与应用程序交互之前，系统将向用户发送一封电子邮件以确认其电子邮件地址。<br>请注意，用户在注册成功后仍然可以更改他们的电子邮件地址。',\n    'reg_confirm_restrict_domain_placeholder' => '尚未设置限制',\n\n    // Sorting Settings\n    'sorting' => '列表和排序',\n    'sorting_book_default' => '默认排序规则',\n    'sorting_book_default_desc' => '选择要应用于新书的默认排序规则。这不会影响现有书，并且可以每本书覆盖。',\n    'sorting_rules' => '排序规则',\n    'sorting_rules_desc' => '这些是预定义的排序操作，可应用于系统中的内容。',\n    'sort_rule_assigned_to_x_books' => '分配给 :count Book|分配给 :count Books',\n    'sort_rule_create' => '创建排序规则',\n    'sort_rule_edit' => '编辑排序规则',\n    'sort_rule_delete' => '删除排序规则',\n    'sort_rule_delete_desc' => '从系统中删除这种排序规则。使用这种类型的书本将恢复到手动排序。',\n    'sort_rule_delete_warn_books' => '此排序规则目前用于:count book(s)。您确定要删除吗？',\n    'sort_rule_delete_warn_default' => '此排序规则目前被用作书籍的默认值。您确定要删除吗？',\n    'sort_rule_details' => '排序规则详细信息',\n    'sort_rule_details_desc' => '为此排序规则设置一个名称，当用户选择排序时，该名称将出现在列表中。',\n    'sort_rule_operations' => '排序选项',\n    'sort_rule_operations_desc' => '配置通过将它们从可用操作列表中移动来执行的排序操作。 一旦使用，操作将按顺序从上到下顺序进行。 这里所做的任何更改都将在保存时适用于所有分配的书本。',\n    'sort_rule_available_operations' => '可用操作',\n    'sort_rule_available_operations_empty' => '没有剩余操作',\n    'sort_rule_configured_operations' => '配置选项',\n    'sort_rule_configured_operations_empty' => '从“可用操作”列表中拖动/添加操作',\n    'sort_rule_op_asc' => '(Asc)',\n    'sort_rule_op_desc' => '(Desc)',\n    'sort_rule_op_name' => '名称-按字母顺序排序',\n    'sort_rule_op_name_numeric' => '名称-按数字顺序排序',\n    'sort_rule_op_created_date' => '创建时间',\n    'sort_rule_op_updated_date' => '更新时间',\n    'sort_rule_op_chapters_first' => '章节正序',\n    'sort_rule_op_chapters_last' => '章节倒序',\n    'sorting_page_limits' => '每页显示限制',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => '维护',\n    'maint_image_cleanup' => '清理图像',\n    'maint_image_cleanup_desc' => '扫描页面和修订内容以检查哪些图片是正在使用的以及哪些图片是多余的。确保在运行前完整备份数据库和图片。',\n    'maint_delete_images_only_in_revisions' => '同时删除只存在于旧的页面修订中的图片',\n    'maint_image_cleanup_run' => '运行清理',\n    'maint_image_cleanup_warning' => '发现了 :count 张可能未使用的图像。您确定要删除这些图像吗？',\n    'maint_image_cleanup_success' => '找到并删除了 :count 张可能未使用的图像！',\n    'maint_image_cleanup_nothing_found' => '找不到未使用的图像，没有删除！',\n    'maint_send_test_email' => '发送测试电子邮件',\n    'maint_send_test_email_desc' => '这将发送测试邮件到您的个人资料中指定的电子邮件地址。',\n    'maint_send_test_email_run' => '发送测试邮件',\n    'maint_send_test_email_success' => '电子邮件已发送至 :address',\n    'maint_send_test_email_mail_subject' => '测试电子邮件',\n    'maint_send_test_email_mail_greeting' => '邮件发送功能看起来工作正常！',\n    'maint_send_test_email_mail_text' => '恭喜！您收到了此邮件通知，您的电子邮件设置看起来已配置正确。',\n    'maint_recycle_bin_desc' => '被删除的书架、书籍、章节和页面会被存入回收站，您可以还原或永久删除它们。回收站中较旧的项目可能会在系统设置的一段时间后被自动删除。',\n    'maint_recycle_bin_open' => '打开回收站',\n    'maint_regen_references' => '重新生成引用',\n    'maint_regen_references_desc' => '此操作将重建数据库中的跨项目引用索引。这通常是自动处理的，但可能有助于索引旧内容或通过非官方方法添加的内容。',\n    'maint_regen_references_success' => '引用索引已重新生成！',\n    'maint_timeout_command_note' => '注意：执行此操作需要一些时间，这可能会导致在某些 Web 环境中出现超时问题。作为替代方案，此操作也可以在终端中执行。',\n\n    // Recycle Bin\n    'recycle_bin' => '回收站',\n    'recycle_bin_desc' => '在这里，您可以还原已删除的项目，或选择将其从系统中永久删除。与系统中过滤过的类似的活动记录不同，这个表会显示所有操作。',\n    'recycle_bin_deleted_item' => '被删除的项目',\n    'recycle_bin_deleted_parent' => '上级',\n    'recycle_bin_deleted_by' => '删除者',\n    'recycle_bin_deleted_at' => '删除时间',\n    'recycle_bin_permanently_delete' => '永久删除',\n    'recycle_bin_restore' => '恢复',\n    'recycle_bin_contents_empty' => '回收站当前为空',\n    'recycle_bin_empty' => '清空回收站',\n    'recycle_bin_empty_confirm' => '这将永久性销毁回收站中的所有项目（包括每个项目中包含的内容，例如图片）。您确定要清空回收站吗？',\n    'recycle_bin_destroy_confirm' => '此操作将从系统中永久删除此项目以及下面列出的所有子元素，并且您将无法还原此内容。您确定要永久删除该项目吗？',\n    'recycle_bin_destroy_list' => '要销毁的项目',\n    'recycle_bin_restore_list' => '要恢复的项目',\n    'recycle_bin_restore_confirm' => '此操作会将已删除的项目及其所有子元素恢复到原始位置。如果项目的原始位置已被删除，并且现在位于回收站中，则要恢复项目的上级项目也需要恢复。',\n    'recycle_bin_restore_deleted_parent' => '该项目的上级项目也已被删除。这些项目将保持被删除状态，直到上级项目被恢复。',\n    'recycle_bin_restore_parent' => '还原上级',\n    'recycle_bin_destroy_notification' => '从回收站中删除了 :count 个项目。',\n    'recycle_bin_restore_notification' => '从回收站中恢复了 :count 个项目。',\n\n    // Audit Log\n    'audit' => '审核日志',\n    'audit_desc' => '这份审核日志显示所有被系统跟踪的活动。与系统中过滤过的类似的活动记录不同，这个表会显示所有操作。',\n    'audit_event_filter' => '事件过滤器',\n    'audit_event_filter_no_filter' => '无过滤器',\n    'audit_deleted_item' => '被删除的项目',\n    'audit_deleted_item_name' => '名称: :name',\n    'audit_table_user' => '用户',\n    'audit_table_event' => '事件',\n    'audit_table_related' => '相关项目或详细信息',\n    'audit_table_ip' => 'IP 地址',\n    'audit_table_date' => '活动日期',\n    'audit_date_from' => '日期范围从',\n    'audit_date_to' => '日期范围至',\n\n    // Role Settings\n    'roles' => '角色',\n    'role_user_roles' => '用户角色',\n    'roles_index_desc' => '角色用于对用户进行分组并为其成员提供系统权限。当一个用户是多个角色的成员时，授予的权限将叠加，用户将继承所有角色的能力。',\n    'roles_x_users_assigned' => ':count 位用户已分配|:count 位用户已分配',\n    'roles_x_permissions_provided' => ':count 个权限|:count 个权限',\n    'roles_assigned_users' => '已分配用户',\n    'roles_permissions_provided' => '已提供权限',\n    'role_create' => '创建角色',\n    'role_delete' => '删除角色',\n    'role_delete_confirm' => '这将会删除名为 \\':roleName\\' 的角色.',\n    'role_delete_users_assigned' => '有:userCount位用户属于此角色。如果您想将此角色中的用户迁移，请在下面选择一个新角色。',\n    'role_delete_no_migration' => \"不要迁移用户\",\n    'role_delete_sure' => '您确定要删除这个角色？',\n    'role_edit' => '编辑角色',\n    'role_details' => '角色详细信息',\n    'role_name' => '角色名',\n    'role_desc' => '角色简述',\n    'role_mfa_enforced' => '需要多重身份认证',\n    'role_external_auth_id' => '外部身份认证ID',\n    'role_system' => '系统权限',\n    'role_manage_users' => '管理用户',\n    'role_manage_roles' => '管理角色与角色权限',\n    'role_manage_entity_permissions' => '管理所有书籍、章节和页面的权限',\n    'role_manage_own_entity_permissions' => '管理自己的书籍、章节和页面的权限',\n    'role_manage_page_templates' => '管理页面模板',\n    'role_access_api' => '访问系统 API',\n    'role_manage_settings' => '管理 App 设置',\n    'role_export_content' => '导出内容',\n    'role_import_content' => '导入内容',\n    'role_editor_change' => '更改页面编辑器',\n    'role_notifications' => '管理和接收通知',\n    'role_permission_note_users_and_roles' => '从技术上讲，这些权限还将提供对系统中用户和角色的可见性和搜索功能。',\n    'role_asset' => '资源许可',\n    'roles_system_warning' => '请注意，拥有以上三个权限中的任何一个都会允许用户更改自己的权限或系统中其他人的权限。 请只将拥有这些权限的角色分配给你信任的用户。',\n    'role_asset_desc' => '对系统内资源的默认访问许可将由这些权限控制。单独设置在书籍、章节和页面上的权限将覆盖这里的权限设定。',\n    'role_asset_admins' => '管理员可自动获得对所有内容的访问权限，但这些选项可能会显示或隐藏UI选项。',\n    'role_asset_image_view_note' => '这与图像管理器中的可见性有关。已经上传的图片的实际访问取决于系统图像存储选项。',\n    'role_asset_users_note' => '从技术上讲，这些权限还将提供对系统中用户和角色的可见性和搜索功能。',\n    'role_all' => '全部的',\n    'role_own' => '拥有的',\n    'role_controlled_by_asset' => '由其所在的资源来控制',\n    'role_save' => '保存角色',\n    'role_users' => '此角色的用户',\n    'role_users_none' => '目前没有用户被分配到这个角色',\n\n    // Users\n    'users' => '用户',\n    'users_index_desc' => '在系统内创建和管理个人用户账户。用户账户用于登录和内容及活动的归属。访问权限主要是基于角色的，但用户的内容所有权以及其他因素，也可能影响到权限和访问。',\n    'user_profile' => '用户资料',\n    'users_add_new' => '添加用户',\n    'users_search' => '搜索用户',\n    'users_latest_activity' => '最后活动',\n    'users_details' => '用户详细资料',\n    'users_details_desc' => '设置该用户的显示名称和电子邮件地址。 该电子邮件地址将用于登录本站。',\n    'users_details_desc_no_email' => '设置此用户的昵称，以便其他人识别。',\n    'users_role' => '用户角色',\n    'users_role_desc' => '选择将分配给该用户的角色。 如果将一个用户分配给多个角色，则这些角色的权限将堆叠在一起，并且他们将获得分配的角色的所有功能。',\n    'users_password' => '用户密码',\n    'users_password_desc' => '设置用于登录本应用的密码。 长度必须至少为 8 个字符。',\n    'users_send_invite_text' => '您可以向该用户发送邀请电子邮件，允许他们设置自己的密码，否则，您可以自己设置他们的密码。',\n    'users_send_invite_option' => '发送邀请用户电子邮件',\n    'users_external_auth_id' => '外部身份认证ID',\n    'users_external_auth_id_desc' => '当使用外部身份验证系统（例如 SAML2、OIDC 或 LDAP）时，这是将此 BookStack 用户连接到身份验证系统账户的 ID。 如果使用默认的电子邮件身份验证，您可以忽略此字段。',\n    'users_password_warning' => '如果您想更改此用户的密码，请填写以下内容：',\n    'users_system_public' => '此用户代表访问您的App的任何访客。它不能用于登录，而是自动分配。',\n    'users_delete' => '删除用户',\n    'users_delete_named' => '删除用户 :userName',\n    'users_delete_warning' => '这将从系统中完全删除名为 \\':userName\\' 的用户。',\n    'users_delete_confirm' => '您确定要删除这个用户？',\n    'users_migrate_ownership' => '迁移拥有权',\n    'users_migrate_ownership_desc' => '如果您想要当前用户拥有的全部项目转移到另一个用户（更改拥有者），请在此处选择一个用户。',\n    'users_none_selected' => '没有选中用户',\n    'users_edit' => '编辑用户',\n    'users_edit_profile' => '编辑资料',\n    'users_avatar' => '用户头像',\n    'users_avatar_desc' => '选择一张头像。 这张图片应该是约 256 像素的正方形。',\n    'users_preferred_language' => '语言',\n    'users_preferred_language_desc' => '此选项将更改用于应用程序用户界面的语言。 这不会影响任何用户创建的内容。',\n    'users_social_accounts' => '社交账户',\n    'users_social_accounts_desc' => '查看此用户已连接的社交账户状态。 除了主要认证系统外，社交账户也可用于系统访问。',\n    'users_social_accounts_info' => '在这里，您可以绑定您的其他账户，以便更快更轻松地登录。如果您选择解除绑定，之后将不能通过此社交账户登录，请设置社交账户来取消本App的访问权限。',\n    'users_social_connect' => '绑定账户',\n    'users_social_disconnect' => '解除绑定账户',\n    'users_social_status_connected' => '已连接',\n    'users_social_status_disconnected' => '已断开连接',\n    'users_social_connected' => ':socialAccount 账户已经成功绑定到您的资料。',\n    'users_social_disconnected' => ':socialAccount 账户已经成功解除绑定。',\n    'users_api_tokens' => 'API令牌',\n    'users_api_tokens_desc' => '创建和管理用于 BookStack REST API 认证的访问令牌。 API 的权限是通过令牌所属的用户管理的。',\n    'users_api_tokens_none' => '没有创建任何API令牌给此用户',\n    'users_api_tokens_create' => '创建令牌',\n    'users_api_tokens_expires' => '过期',\n    'users_api_tokens_docs' => 'API文档',\n    'users_mfa' => '多重身份认证',\n    'users_mfa_desc' => '设置多重身份认证能增加您账户的安全性。',\n    'users_mfa_x_methods' => ':count 个措施已配置|:count 个措施已配置',\n    'users_mfa_configure' => '配置安全措施',\n\n    // API Tokens\n    'user_api_token_create' => '创建 API 令牌',\n    'user_api_token_name' => '姓名',\n    'user_api_token_name_desc' => '请给您的可读令牌一个命名以在未来提醒您它的预期用途',\n    'user_api_token_expiry' => '过期期限',\n    'user_api_token_expiry_desc' => '请设置一个此令牌的过期时间，过期后此令牌所给出的请求将失效，若将此处留为空白将自动设置过期时间为100年。',\n    'user_api_token_create_secret_message' => '创建此令牌后会立即生成“令牌ID”和“令牌密钥”。该密钥只会显示一次，所以请确保在继续操作之前将密钥记录或复制到一个安全的地方。',\n    'user_api_token' => 'API令牌',\n    'user_api_token_id' => '令牌ID',\n    'user_api_token_id_desc' => '这是系统生成的一个不可编辑的令牌标识符，需要在API请求中才能提供。',\n    'user_api_token_secret' => '令牌密钥',\n    'user_api_token_secret_desc' => '这是此令牌系统生成的密钥，需要在API请求中才可以提供。 这只会显示一次，因此请将其复制到安全的地方。',\n    'user_api_token_created' => '创建的令牌:timeAgo',\n    'user_api_token_updated' => '令牌更新:timeAgo',\n    'user_api_token_delete' => '删除令牌',\n    'user_api_token_delete_warning' => '这将会从系统中完全删除名为 “:tokenName” 的 API 令牌',\n    'user_api_token_delete_confirm' => '您确定要删除此API令牌吗？',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhook 是一种在系统内发生某些操作和事件时将数据发送到外部 URL 的方法，它允许与外部平台（例如消息传递或通知系统）进行基于事件的集成。',\n    'webhooks_x_trigger_events' => ':count 个触发事件 |:count 个触发事件',\n    'webhooks_create' => '新建 Webhook',\n    'webhooks_none_created' => '尚未创建任何 Webhook。',\n    'webhooks_edit' => '编辑 Webhook',\n    'webhooks_save' => '保存 Webhook',\n    'webhooks_details' => 'Webhook 详情',\n    'webhooks_details_desc' => '提供一个用户友好的名称和一个 POST Endpoint 作为 Webhook 数据发送的位置。',\n    'webhooks_events' => 'Webhook 事件',\n    'webhooks_events_desc' => '选择所有应触发此 Webhook 的事件。',\n    'webhooks_events_warning' => '请记住，即使应用了自定义权限，所有选定的事件也仍然会被触发。 确保使用此 Webhook 不会泄露机密内容。',\n    'webhooks_events_all' => '所有系统事件',\n    'webhooks_name' => 'Webhook 名称',\n    'webhooks_timeout' => 'Webhook 请求超时（秒）',\n    'webhooks_endpoint' => 'Webhook Endpoint',\n    'webhooks_active' => '激活 Webhook',\n    'webhook_events_table_header' => '事件',\n    'webhooks_delete' => '删除 Webhook',\n    'webhooks_delete_warning' => '这将会从系统中完全删除名为 “:webhookName” 的 webhook。',\n    'webhooks_delete_confirm' => '您确定要删除此 Webhook 吗？',\n    'webhooks_format_example' => 'Webhook 格式示例',\n    'webhooks_format_example_desc' => 'Webhook 数据会以 POST 请求按照以下 JSON 格式发送到设置的 Endpoint。 “related_item” 和 “url” 属性是可选的，取决于触发的事件类型。',\n    'webhooks_status' => 'Webhook 状态',\n    'webhooks_last_called' => '最后一次调用：',\n    'webhooks_last_errored' => '最后一个错误：',\n    'webhooks_last_error_message' => '最后一个错误消息：',\n\n    // Licensing\n    'licenses' => '许可证',\n    'licenses_desc' => '除了 BookStack 中使用的项目和库之外，此页面还详细介绍了 BookStack 的许可证信息。列出的许多项目只能在开发环境中使用。',\n    'licenses_bookstack' => 'BookStack 许可证',\n    'licenses_php' => 'PHP 库许可证',\n    'licenses_js' => 'JavaScript 库许可证',\n    'licenses_other' => '其他许可证',\n    'license_details' => '许可证细节',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => '保加利亚语',\n        'bs' => 'Bosanski',\n        'ca' => '加泰罗尼亚语',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => '丹麦',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => 'עברית',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italien',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => '挪威语 (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/zh_CN/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => ':attribute 需要被同意。',\n    'active_url'           => ':attribute 并不是一个有效的网址',\n    'after'                => ':attribute 必须是在 :date 后的日期。',\n    'alpha'                => ':attribute 只能包含字母。',\n    'alpha_dash'           => ':attribute 只能包含字母、数字和短横线。',\n    'alpha_num'            => ':attribute 只能包含字母和数字。',\n    'array'                => ':attribute 必须是一个数组。',\n    'backup_codes'         => '您输入的认证码无效或已被使用。',\n    'before'               => ':attribute 必须是在 :date 前的日期。',\n    'between'              => [\n        'numeric' => ':attribute 必须在 :min 到 :max 之间。',\n        'file'    => ':attribute 必须为 :min 到 :max 之间。',\n        'string'  => ':attribute 必须在 :min 到 :max 个字符之间。',\n        'array'   => ':attribute 必须在 :min 到 :max 项之间.',\n    ],\n    'boolean'              => ':attribute 字段必须为真或假。',\n    'confirmed'            => ':attribute 确认不符。',\n    'date'                 => ':attribute 不是一个有效的日期。',\n    'date_format'          => ':attribute 不匹配格式 :format。',\n    'different'            => ':attribute 和 :other 必须不同。',\n    'digits'               => ':attribute 必须为:digits位数。',\n    'digits_between'       => ':attribute 必须为:min到:max位数。',\n    'email'                => ':attribute 必须是有效的电子邮件地址。',\n    'ends_with' => ':attribute 必须以: :values 后缀结尾。',\n    'file'                 => ':attribute 必须是一个有效的文件。',\n    'filled'               => ':attribute 字段是必需的。',\n    'gt'                   => [\n        'numeric' => ':attribute必须大于 :value.',\n        'file'    => ':attribute 必须大于 :value k',\n        'string'  => ':attribute 必须大于 :value 字符。',\n        'array'   => ':attribute 必须包含多个 :value 项目。',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute 必须大于或等于 :value.',\n        'file'    => ':attribute 必须大于或等于 :value k。',\n        'string'  => ':attribute 必须大于或等于 :value 字符。',\n        'array'   => ':attribute 必须具有 :value 项或更多',\n    ],\n    'exists'               => '选中的 :attribute 无效。',\n    'image'                => ':attribute 必须是一个图片。',\n    'image_extension'      => ':attribute 必须具有有效且受支持的图像扩展名。',\n    'in'                   => '选中的 :attribute 无效。',\n    'integer'              => ':attribute 必须是一个整数。',\n    'ip'                   => ':attribute 必须是一个有效的IP地址。',\n    'ipv4'                 => ':attribute 必须是有效的IPv4地址。',\n    'ipv6'                 => ':attribute必须是有效的IPv6地址。',\n    'json'                 => ':attribute 必须是JSON类型.',\n    'lt'                   => [\n        'numeric' => ':attribute 必须小于 :value.',\n        'file'    => ':attribute 必须小于 :value k。',\n        'string'  => ':attribute 必须小于 :value 字符。',\n        'array'   => ':attribute 必须小于 :value 项.',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute 必须小于或等于 :value.',\n        'file'    => ':attribute 必须小于或等于 :value k。',\n        'string'  => ':attribute 必须小于或等于 :value 字符。',\n        'array'   => ':attribute 不得超过 :value 项。',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute 不能超过:max。',\n        'file'    => ':attribute 不能超过:max KB。',\n        'string'  => ':attribute 不能超过:max个字符。',\n        'array'   => ':attribute 不能有超过:max项。',\n    ],\n    'mimes'                => ':attribute 必须是 :values 类型的文件。',\n    'min'                  => [\n        'numeric' => ':attribute 至少为:min。',\n        'file'    => ':attribute 至少为:min KB。',\n        'string'  => ':attribute 至少为:min个字符。',\n        'array'   => ':attribute 至少有:min项。',\n    ],\n    'not_in'               => '选中的 :attribute 无效。',\n    'not_regex'            => ':attribute 格式错误。',\n    'numeric'              => ':attribute 必须是一个数。',\n    'regex'                => ':attribute 格式无效。',\n    'required'             => ':attribute 字段是必需的。',\n    'required_if'          => '当:other为:value时，:attribute 字段是必需的。',\n    'required_with'        => '当:values存在时，:attribute 字段是必需的。',\n    'required_with_all'    => '当:values存在时，:attribute 字段是必需的。',\n    'required_without'     => '当:values不存在时，:attribute 字段是必需的。',\n    'required_without_all' => '当:values均不存在时，:attribute 字段是必需的。',\n    'same'                 => ':attribute 与 :other 必须匹配。',\n    'safe_url'             => '提供的链接可能不安全。',\n    'size'                 => [\n        'numeric' => ':attribute 必须为:size。',\n        'file'    => ':attribute 必须为:size KB。',\n        'string'  => ':attribute 必须为:size个字符。',\n        'array'   => ':attribute 必须包含:size项。',\n    ],\n    'string'               => ':attribute 必须是字符串。',\n    'timezone'             => ':attribute 必须是有效的区域。',\n    'totp'                 => '您输入的认证码无效或已过期。',\n    'unique'               => ':attribute 已经被使用。',\n    'url'                  => ':attribute 格式无效。',\n    'uploaded'             => '无法上传文件。 服务器可能不接受此大小的文件。',\n\n    'zip_file' => ':attribute 需要引用 ZIP 内的文件。',\n    'zip_file_size' => ':attribute 不能超过 :size MB 。',\n    'zip_file_mime' => ':attribute 需要引用类型为 :validTypes 的文件，找到 :foundType 。',\n    'zip_model_expected' => '预期的数据对象，但找到了 \":type\" 。',\n    'zip_unique' => '对于 ZIP 中的对象类型来说，:attribute 必须是唯一的。',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => '需要确认密码',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "lang/zh_TW/activities.php",
    "content": "<?php\n/**\n * Activity text strings.\n * Is used for all the text within activity logs & notifications.\n */\nreturn [\n\n    // Pages\n    'page_create'                 => '已建立頁面',\n    'page_create_notification'    => '頁面已建立成功',\n    'page_update'                 => '已更新頁面',\n    'page_update_notification'    => '頁面已更新成功',\n    'page_delete'                 => '已刪除頁面',\n    'page_delete_notification'    => '頁面已刪除成功',\n    'page_restore'                => '已還原頁面',\n    'page_restore_notification'   => '頁面已還原成功',\n    'page_move'                   => '已移動頁面',\n    'page_move_notification'      => '頁面已成功移動',\n\n    // Chapters\n    'chapter_create'              => '已建立章節',\n    'chapter_create_notification' => '章節已建立成功',\n    'chapter_update'              => '已更新章節',\n    'chapter_update_notification' => '章節已更新成功',\n    'chapter_delete'              => '已刪除章節',\n    'chapter_delete_notification' => '章節已刪除成功',\n    'chapter_move'                => '已移動章節',\n    'chapter_move_notification' => '章節已移動成功',\n\n    // Books\n    'book_create'                 => '已建立書本',\n    'book_create_notification'    => '書本已建立成功',\n    'book_create_from_chapter'              => '將章節轉爲書籍',\n    'book_create_from_chapter_notification' => '章節已轉換爲書籍',\n    'book_update'                 => '已更新書本',\n    'book_update_notification'    => '書本已更新成功',\n    'book_delete'                 => '已刪除書本',\n    'book_delete_notification'    => '書本已刪除成功',\n    'book_sort'                   => '已排序書本',\n    'book_sort_notification'      => '書本已重新排序成功',\n\n    // Bookshelves\n    'bookshelf_create'            => '已建立書棧',\n    'bookshelf_create_notification'    => '書棧已創建',\n    'bookshelf_create_from_book'    => '將書籍轉爲書棧',\n    'bookshelf_create_from_book_notification'    => '章節已轉爲書籍',\n    'bookshelf_update'                 => '更新書棧',\n    'bookshelf_update_notification'    => '書棧已更新',\n    'bookshelf_delete'                 => '刪除書棧',\n    'bookshelf_delete_notification'    => '書架已刪除',\n\n    // Revisions\n    'revision_restore' => '還原的版本',\n    'revision_delete' => '刪除的版本',\n    'revision_delete_notification' => '修訂已成功刪除',\n\n    // Favourites\n    'favourite_add_notification' => '\":name\" 已加入到你的最愛',\n    'favourite_remove_notification' => '\":name\" 已從你的最愛移除',\n\n    // Watching\n    'watch_update_level_notification' => '追蹤偏好設定已成功更新',\n\n    // Auth\n    'auth_login' => '已登入',\n    'auth_register' => '註冊為新用戶',\n    'auth_password_reset_request' => '請求重置用戶密碼',\n    'auth_password_reset_update' => '重置使用者密碼',\n    'mfa_setup_method' => '設定MFA方式',\n    'mfa_setup_method_notification' => '多重身份驗證已設定成功',\n    'mfa_remove_method' => '移除MFA方式',\n    'mfa_remove_method_notification' => '多重身份驗證已移除成功',\n\n    // Settings\n    'settings_update' => '更新設定',\n    'settings_update_notification' => '設定更新成功',\n    'maintenance_action_run' => '執行維護動作',\n\n    // Webhooks\n    'webhook_create' => '建立 Webhook',\n    'webhook_create_notification' => 'Webhook 已建立成功',\n    'webhook_update' => 'Webhook 已更新',\n    'webhook_update_notification' => 'Webhook 已更新成功',\n    'webhook_delete' => 'webhook 已刪除',\n    'webhook_delete_notification' => 'Webhook 已刪除成功',\n\n    // Imports\n    'import_create' => '已建立匯入',\n    'import_create_notification' => '成功上傳匯入',\n    'import_run' => '已更新匯入',\n    'import_run_notification' => '成功匯入內容',\n    'import_delete' => '已刪除匯入',\n    'import_delete_notification' => '匯入刪除成功',\n\n    // Users\n    'user_create' => '建立使用者',\n    'user_create_notification' => '使用者已成功建立。',\n    'user_update' => '更新使用者',\n    'user_update_notification' => '使用者已成功更新。',\n    'user_delete' => '已刪除使用者',\n    'user_delete_notification' => '使用者移除成功',\n\n    // API Tokens\n    'api_token_create' => '建立 API 權杖',\n    'api_token_create_notification' => '成功建立 API 權杖',\n    'api_token_update' => '已更新 API 權杖',\n    'api_token_update_notification' => '成功更新 API 權杖',\n    'api_token_delete' => '已刪除 API 權杖',\n    'api_token_delete_notification' => 'API 權杖已成功刪除',\n\n    // Roles\n    'role_create' => '創建角色',\n    'role_create_notification' => '建立角色成功',\n    'role_update' => '已更新角色',\n    'role_update_notification' => '更新角色成功',\n    'role_delete' => '已刪除角色',\n    'role_delete_notification' => '刪除角色成功',\n\n    // Recycle Bin\n    'recycle_bin_empty' => '清理資源回收筒',\n    'recycle_bin_restore' => '從資源回收筒復原',\n    'recycle_bin_destroy' => '從資源回收筒刪除',\n\n    // Comments\n    'commented_on'                => '評論',\n    'comment_create'              => '新增評論',\n    'comment_update'              => '更新評論',\n    'comment_delete'              => '已刪除之評論',\n\n    // Sort Rules\n    'sort_rule_create' => '已建立的排序規則',\n    'sort_rule_create_notification' => '排序規則已成功建立',\n    'sort_rule_update' => '已更新排序規則',\n    'sort_rule_update_notification' => '排序規則已成功更新',\n    'sort_rule_delete' => '已刪除排序規則',\n    'sort_rule_delete_notification' => '排序規則已成功刪除',\n\n    // Other\n    'permissions_update'          => '更新權限',\n];\n"
  },
  {
    "path": "lang/zh_TW/auth.php",
    "content": "<?php\n/**\n * Authentication Language Lines\n * The following language lines are used during authentication for various\n * messages that we need to display to the user.\n */\nreturn [\n\n    'failed' => '使用者名稱或密碼錯誤。',\n    'throttle' => '您的登入次數過多，請在 :seconds 秒後重試。',\n\n    // Login & Register\n    'sign_up' => '註冊',\n    'log_in' => '登入',\n    'log_in_with' => '以 :socialDriver 登入',\n    'sign_up_with' => '以 :socialDriver 註冊',\n    'logout' => '登出',\n\n    'name' => '名稱',\n    'username' => '使用者名稱',\n    'email' => '電子郵件',\n    'password' => '密碼',\n    'password_confirm' => '確認密碼',\n    'password_hint' => '密碼必須至少8個字元',\n    'forgot_password' => '忘記密碼？',\n    'remember_me' => '記住我',\n    'ldap_email_hint' => '請輸入此帳號使用的電子郵件。',\n    'create_account' => '建立帳號',\n    'already_have_account' => '已有帳號？',\n    'dont_have_account' => '沒有帳號？',\n    'social_login' => '社群網站登入',\n    'social_registration' => '使用社群網站帳號註冊',\n    'social_registration_text' => '使用其他服務註冊及登入。',\n\n    'register_thanks' => '感謝您的註冊！',\n    'register_confirm' => '請檢查您的電子郵件，並按下確認按鈕以使用 :appName 。',\n    'registrations_disabled' => '目前已停用註冊',\n    'registration_email_domain_invalid' => '這個電子郵件網域沒有權限使用',\n    'register_success' => '感謝您註冊！您已註冊完成並可登入。',\n\n    // Login auto-initiation\n    'auto_init_starting' => '嘗試登入中',\n    'auto_init_starting_desc' => '正在與認證系統連線以開始流程，若 5 秒鐘仍無回應，請嘗試點擊以下的連結。',\n    'auto_init_start_link' => '進行認證',\n\n    // Password Reset\n    'reset_password' => '重設密碼',\n    'reset_password_send_instructions' => '在下方輸入您的電子郵件，您將收到一封帶有密碼重設連結的郵件。',\n    'reset_password_send_button' => '發送重設連結',\n    'reset_password_sent' => '重設密碼的連結會發送至電子郵件 :email（如果此電子郵件在我們的系統中存在）',\n    'reset_password_success' => '您的密碼已成功重設。',\n    'email_reset_subject' => '重設您的 :appName 密碼',\n    'email_reset_text' => '您收到此電子郵件是因為我們收到了您的帳號的密碼重設請求。',\n    'email_reset_not_requested' => '如果您沒有要求重設密碼，則不需要採取進一步的操作。',\n\n    // Email Confirmation\n    'email_confirm_subject' => '確認您在 :appName 的電子郵件',\n    'email_confirm_greeting' => '感謝您加入 :appName！',\n    'email_confirm_text' => '請點選下面的按鈕來確認您的電子郵件地址：',\n    'email_confirm_action' => '確認電子郵件',\n    'email_confirm_send_error' => '需要電子郵件驗證，但系統無法傳送電子郵件。請與管理員聯絡以確保電子郵件正確設定。',\n    'email_confirm_success' => '您的電子郵箱已確認成功！您可以使用該電子郵箱地址進行登入了。',\n    'email_confirm_resent' => '確認電子郵件已重新傳送。請檢查您的收件匣。',\n    'email_confirm_thanks' => '完成驗證，謝謝。',\n    'email_confirm_thanks_desc' => '正在處理您的確認，請稍候。若三秒後沒有重新導向，請按下方的「繼續」連結繼續。',\n\n    'email_not_confirmed' => '電子郵件地址未確認',\n    'email_not_confirmed_text' => '您的電子郵件位址尚未確認。',\n    'email_not_confirmed_click_link' => '請檢查註冊時收到的電子郵件，然後點選確認連結。',\n    'email_not_confirmed_resend' => '如果找不到電子郵件，請透過下面的表單重新發送確認電子郵件。',\n    'email_not_confirmed_resend_button' => '重新傳送確認電子郵件',\n\n    // User Invite\n    'user_invite_email_subject' => '您被邀請加入 :appName！',\n    'user_invite_email_greeting' => '我們為您在 :appName 上建立了一個新帳號。',\n    'user_invite_email_text' => '請點擊下方按鈕來設定帳號密碼並取得存取權：',\n    'user_invite_email_action' => '設定帳號密碼',\n    'user_invite_page_welcome' => '歡迎使用 :appName！',\n    'user_invite_page_text' => '要完成設定您的帳號並取得存取權，您必須設定密碼，此密碼將用於登入 :appName。',\n    'user_invite_page_confirm_button' => '確認密碼',\n    'user_invite_success_login' => '密碼已設定完成，您可以使用改密碼來登入 :appName!',\n\n    // Multi-factor Authentication\n    'mfa_setup' => '設定雙重身份驗證',\n    'mfa_setup_desc' => '設定雙重身份驗證為您的帳戶多增加了一道防線',\n    'mfa_setup_configured' => '設定完成',\n    'mfa_setup_reconfigure' => '重新設定',\n    'mfa_setup_remove_confirmation' => '您確定要移除雙重身份驗證嗎？',\n    'mfa_setup_action' => '設置',\n    'mfa_backup_codes_usage_limit_warning' => '您只剩下不到5組備用驗證碼了，請重新生成新的備用驗證碼並妥善保存，以免日後無法登入您的賬號。',\n    'mfa_option_totp_title' => '手機App',\n    'mfa_option_totp_desc' => '您必須在行動裝置上安裝了支援TOTP的身份驗證程式（例如Google Authenticator, Authy 或是 Microsoft Authenticator）才能使用雙重身份驗證。',\n    'mfa_option_backup_codes_title' => '備用驗證碼',\n    'mfa_option_backup_codes_desc' => '產生一次使用的備份代碼，你可以在登入時用來辨識身份。請確認代碼被存放在安全的地方。',\n    'mfa_gen_confirm_and_enable' => '確認並啟用',\n    'mfa_gen_backup_codes_title' => '備援代碼設定',\n    'mfa_gen_backup_codes_desc' => '將以下代碼列表儲存在安全的地方。存取系統時，您可以使用其中一個代碼作為第二個身份驗證機制。',\n    'mfa_gen_backup_codes_download' => '下載代碼',\n    'mfa_gen_backup_codes_usage_warning' => '每個代碼都只能使用一次',\n    'mfa_gen_totp_title' => '行動裝置應用程式設定',\n    'mfa_gen_totp_desc' => '您必須在行動裝置上安裝支援 TOTP 的身份驗證應用程式（例如 Google Authenticator、Authy 或 Microsoft Authenticator）。',\n    'mfa_gen_totp_scan' => '使用您偏好的身份驗證應用程式掃描下方的 QR code 以開始流程。',\n    'mfa_gen_totp_verify_setup' => '驗證設定',\n    'mfa_gen_totp_verify_setup_desc' => '透過在下方的輸入方塊中輸入您的身份驗證應用程式中產生的代碼來驗證一切都正常：',\n    'mfa_gen_totp_provide_code_here' => '在此處填入您的應用程式產生的代碼',\n    'mfa_verify_access' => '驗證存取權',\n    'mfa_verify_access_desc' => '您的使用者帳號在您取得存取權前需要您透過額外的驗證層級確認您的身份。使用您設定的其中一種驗證方式繼續。',\n    'mfa_verify_no_methods' => '未設定任何方式',\n    'mfa_verify_no_methods_desc' => '您的帳號中找不到多重步驟驗證方式。在取得存取權前，您必須設定至少一種方式。',\n    'mfa_verify_use_totp' => '使用您的行動裝置進行驗證',\n    'mfa_verify_use_backup_codes' => '使用您的備用驗證碼進行驗證',\n    'mfa_verify_backup_code' => '備用驗證碼',\n    'mfa_verify_backup_code_desc' => '在下方輸入您剩下的其中一個備援代碼：',\n    'mfa_verify_backup_code_enter_here' => '在此處輸入備援代碼',\n    'mfa_verify_totp_desc' => '在下方輸入使用您行動裝置應用程式產生的代碼：',\n    'mfa_setup_login_notification' => '多因素認證已設定，請使用新的設定登入',\n];\n"
  },
  {
    "path": "lang/zh_TW/common.php",
    "content": "<?php\n/**\n * Common elements found throughout many areas of BookStack.\n */\nreturn [\n\n    // Buttons\n    'cancel' => '取消',\n    'close' => '關閉',\n    'confirm' => '確認',\n    'back' => '返回',\n    'save' => '儲存',\n    'continue' => '繼續',\n    'select' => '選取',\n    'toggle_all' => '切換全部',\n    'more' => '更多',\n\n    // Form Labels\n    'name' => '名稱',\n    'description' => '描述',\n    'role' => '角色',\n    'cover_image' => '封面圖片',\n    'cover_image_description' => '雖然圖片會在不同情境下自動調整顯示方式，但應接近 440x250 像素',\n\n    // Actions\n    'actions' => '動作',\n    'view' => '檢視',\n    'view_all' => '檢視全部',\n    'new' => '新增',\n    'create' => '建立',\n    'update' => '更新',\n    'edit' => '編輯',\n    'archive' => '歸檔',\n    'unarchive' => '取消封存',\n    'sort' => '排序',\n    'move' => '移動',\n    'copy' => '複製',\n    'reply' => '回覆',\n    'delete' => '刪除',\n    'delete_confirm' => '確認刪除',\n    'search' => '搜尋',\n    'search_clear' => '清除搜尋',\n    'reset' => '重設',\n    'remove' => '移除',\n    'add' => '新增',\n    'configure' => '配置',\n    'manage' => '管理',\n    'fullscreen' => '全螢幕',\n    'favourite' => '最愛',\n    'unfavourite' => '取消最愛',\n    'next' => '下一頁',\n    'previous' => '上一頁',\n    'filter_active' => '使用中的過濾器',\n    'filter_clear' => '清理過濾',\n    'download' => '下載',\n    'open_in_tab' => '在新分頁中開啟',\n    'open' => '開啟',\n\n    // Sort Options\n    'sort_options' => '排序選項',\n    'sort_direction_toggle' => '順序方向切換',\n    'sort_ascending' => '遞增排序',\n    'sort_descending' => '遞減排序',\n    'sort_name' => '名稱',\n    'sort_default' => '預設',\n    'sort_created_at' => '建立日期',\n    'sort_updated_at' => '更新日期',\n\n    // Misc\n    'deleted_user' => '已刪除使用者',\n    'no_activity' => '無活動可顯示',\n    'no_items' => '無可用項目',\n    'back_to_top' => '回到頂端',\n    'skip_to_main_content' => '跳到主內容',\n    'toggle_details' => '顯示／隱藏詳細資訊',\n    'toggle_thumbnails' => '顯示／隱藏縮圖',\n    'details' => '詳細資訊',\n    'grid_view' => '網格檢視',\n    'list_view' => '列表檢視',\n    'default' => '預設',\n    'breadcrumb' => '頁面路徑',\n    'status' => '狀態',\n    'status_active' => '啟用中',\n    'status_inactive' => '未啟用',\n    'never' => '永不',\n    'none' => '無',\n\n    // Header\n    'homepage' => '首頁',\n    'header_menu_expand' => '展開選單',\n    'profile_menu' => '個人資料選單',\n    'view_profile' => '檢視個人資料',\n    'edit_profile' => '編輯個人資料',\n    'dark_mode' => '深色模式',\n    'light_mode' => '淺色模式',\n    'global_search' => '全域搜尋',\n\n    // Layout tabs\n    'tab_info' => '資訊',\n    'tab_info_label' => '顯示次要訊息',\n    'tab_content' => '內容',\n    'tab_content_label' => '顯示主要內容',\n\n    // Email Content\n    'email_action_help' => '如果您無法點擊 \":actionText\" 按鈕，請將下方的網址複製並貼上到您的網路瀏覽器中：',\n    'email_rights' => '版權所有',\n\n    // Footer Link Options\n    // Not directly used but available for convenience to users.\n    'privacy_policy' => '隱私權政策',\n    'terms_of_service' => '服務條款',\n\n    // OpenSearch\n    'opensearch_description' => '搜尋 :appName',\n];\n"
  },
  {
    "path": "lang/zh_TW/components.php",
    "content": "<?php\n/**\n * Text used in custom JavaScript driven components.\n */\nreturn [\n\n    // Image Manager\n    'image_select' => '選取圖片',\n    'image_list' => '圖片列表',\n    'image_details' => '圖片詳細資訊',\n    'image_upload' => '上傳圖片',\n    'image_intro' => '您可以在這裡選取和管理上傳到系統的圖片。',\n    'image_intro_upload' => '透過拖曳圖檔至視窗中，或是使用下方的「上傳圖片」按鍵',\n    'image_all' => '全部',\n    'image_all_title' => '檢視所有圖片',\n    'image_book_title' => '檢視上傳到此書本的圖片',\n    'image_page_title' => '檢視上傳到此頁面的圖片',\n    'image_search_hint' => '以圖片名稱搜尋',\n    'image_uploaded' => '上傳於 :uploadedDate',\n    'image_uploaded_by' => '由 :username 上傳',\n    'image_uploaded_to' => '上傳到 :pageLink',\n    'image_updated' => ':updateDate 更新',\n    'image_load_more' => '載入更多',\n    'image_image_name' => '圖片名稱',\n    'image_delete_used' => '此圖片用於以下頁面。',\n    'image_delete_confirm_text' => '您確認想要刪除這個圖片？',\n    'image_select_image' => '選取圖片',\n    'image_dropzone' => '拖曳圖片或點擊此處上傳',\n    'image_dropzone_drop' => '將圖片拖放到此處上傳',\n    'images_deleted' => '圖片已刪除',\n    'image_preview' => '圖片預覽',\n    'image_upload_success' => '圖片上傳成功',\n    'image_update_success' => '圖片詳細資訊更新成功',\n    'image_delete_success' => '圖片刪除成功',\n    'image_replace' => '替換圖片',\n    'image_replace_success' => '圖片更新成功',\n    'image_rebuild_thumbs' => '重建影像縮圖',\n    'image_rebuild_thumbs_success' => '縮圖建立成功',\n\n    // Code Editor\n    'code_editor' => '編輯程式碼',\n    'code_language' => '程式語言',\n    'code_content' => '程式碼內容',\n    'code_session_history' => '工作階段歷史',\n    'code_save' => '儲存程式碼',\n];\n"
  },
  {
    "path": "lang/zh_TW/editor.php",
    "content": "<?php\n/**\n * Page Editor Lines\n * Contains text strings used within the user interface of the\n * WYSIWYG page editor. Some Markdown editor strings may still\n * exist in the 'entities' file instead since this was added later.\n */\nreturn [\n    // General editor terms\n    'general' => '通用',\n    'advanced' => '進階設定',\n    'none' => '無',\n    'cancel' => '取消',\n    'save' => '保存',\n    'close' => '關閉',\n    'apply' => '套用',\n    'undo' => '復原',\n    'redo' => '重做',\n    'left' => '左側',\n    'center' => '置中',\n    'right' => '右側',\n    'top' => '上方',\n    'middle' => '中間',\n    'bottom' => '底端',\n    'width' => '寬度',\n    'height' => '高度',\n    'More' => '更多',\n    'select' => '選擇...',\n\n    // Toolbar\n    'formats' => '格式',\n    'header_large' => '大標題',\n    'header_medium' => '中標題',\n    'header_small' => '小標題',\n    'header_tiny' => '小標題',\n    'paragraph' => '段落',\n    'blockquote' => '引用塊',\n    'inline_code' => '行內程式碼',\n    'callouts' => '圖說文字',\n    'callout_information' => '資訊',\n    'callout_success' => '成功',\n    'callout_warning' => '警告',\n    'callout_danger' => '危險',\n    'bold' => '粗體',\n    'italic' => '斜體',\n    'underline' => '底線',\n    'strikethrough' => '刪除線',\n    'superscript' => '上標',\n    'subscript' => '下標',\n    'text_color' => '文本顏色',\n    'highlight_color' => '突顯顏色',\n    'custom_color' => '自訂顏色',\n    'remove_color' => '移除颜色',\n    'background_color' => '背景顏色',\n    'align_left' => '置左',\n    'align_center' => '置中',\n    'align_right' => '置右',\n    'align_justify' => '兩端對齊',\n    'list_bullet' => '項目列表',\n    'list_numbered' => '編號列表',\n    'list_task' => '任務清單',\n    'indent_increase' => '增加縮進',\n    'indent_decrease' => '減少縮進',\n    'table' => '表格',\n    'insert_image' => '插入圖片',\n    'insert_image_title' => '插入/編輯圖像',\n    'insert_link' => '插入/編輯連結',\n    'insert_link_title' => '插入/編輯連結',\n    'insert_horizontal_line' => '插入水平線',\n    'insert_code_block' => '插入程式碼區塊',\n    'edit_code_block' => '編輯程式碼區塊',\n    'insert_drawing' => '插入/編輯向量圖',\n    'drawing_manager' => '向量圖管理員',\n    'insert_media' => '插入/編輯影片',\n    'insert_media_title' => '插入/編輯影片',\n    'clear_formatting' => '清除格式',\n    'source_code' => '原始碼',\n    'source_code_title' => '原始碼',\n    'fullscreen' => '全螢幕',\n    'image_options' => '圖片選項',\n\n    // Tables\n    'table_properties' => '表格屬性',\n    'table_properties_title' => '表格屬性',\n    'delete_table' => '刪除表格',\n    'table_clear_formatting' => '清除格式',\n    'resize_to_contents' => '將大小調整到符合內容',\n    'row_header' => '列標題',\n    'insert_row_before' => '插入上方列',\n    'insert_row_after' => '插入下方列',\n    'delete_row' => '刪除列',\n    'insert_column_before' => '插入欄位於左方',\n    'insert_column_after' => '插入欄位於右方',\n    'delete_column' => '刪除欄',\n    'table_cell' => '儲存格',\n    'table_row' => '行',\n    'table_column' => '欄',\n    'cell_properties' => '表格屬性',\n    'cell_properties_title' => '表格屬性',\n    'cell_type' => '儲存格類型',\n    'cell_type_cell' => '儲存格',\n    'cell_scope' => '範圍',\n    'cell_type_header' => '標題儲存格',\n    'merge_cells' => '合併儲存格',\n    'split_cell' => '分割儲存格',\n    'table_row_group' => '依行分組',\n    'table_column_group' => '依列分組',\n    'horizontal_align' => '水平對齊',\n    'vertical_align' => '垂直對齊',\n    'border_width' => '邊框寬度',\n    'border_style' => '邊框樣式',\n    'border_color' => '邊框顏色',\n    'row_properties' => '行屬性',\n    'row_properties_title' => '行屬性',\n    'cut_row' => '剪切行',\n    'copy_row' => '複製行',\n    'paste_row_before' => '插入上方行',\n    'paste_row_after' => '插入下方行',\n    'row_type' => '行類型',\n    'row_type_header' => '頁眉',\n    'row_type_body' => '正文',\n    'row_type_footer' => '页脚',\n    'alignment' => '對齊',\n    'cut_column' => '剪切列',\n    'copy_column' => '剪切列',\n    'paste_column_before' => '插入前方列',\n    'paste_column_after' => '插入後方列',\n    'cell_padding' => '單元格填充',\n    'cell_spacing' => '單元格間距',\n    'caption' => '標題',\n    'show_caption' => '顯示標題',\n    'constrain' => '維持比例',\n    'cell_border_solid' => '實線',\n    'cell_border_dotted' => '點線',\n    'cell_border_dashed' => '虛線',\n    'cell_border_double' => '兩倍',\n    'cell_border_groove' => '凹線',\n    'cell_border_ridge' => '浮出',\n    'cell_border_inset' => '嵌入',\n    'cell_border_outset' => '外嵌',\n    'cell_border_none' => '無',\n    'cell_border_hidden' => '隱藏',\n\n    // Images, links, details/summary & embed\n    'source' => '來源',\n    'alt_desc' => '替代描述',\n    'embed' => '內嵌',\n    'paste_embed' => '在下面黏貼您的嵌入代碼：',\n    'url' => '網址',\n    'text_to_display' => '要顯示的文字',\n    'title' => '標題',\n    'browse_links' => '瀏覽連結',\n    'open_link' => '開啟連結',\n    'open_link_in' => '打開連結於……',\n    'open_link_current' => '當前視窗',\n    'open_link_new' => '新視窗',\n    'remove_link' => '移除連結',\n    'insert_collapsible' => '插入可折疊塊',\n    'collapsible_unwrap' => '展開',\n    'edit_label' => '編輯標記',\n    'toggle_open_closed' => '切換打開/關閉',\n    'collapsible_edit' => '編輯可折疊塊',\n    'toggle_label' => '切換標籤',\n\n    // About view\n    'about' => '關於編輯器',\n    'about_title' => '關於所見即所得（WYSIWYG）編輯器',\n    'editor_license' => '編輯器許可證與版權信息',\n    'editor_lexical_license' => '這個編輯器是從 :lexicalLink 分支出來的，而 :lexicalLink 是以 MIT 授權條款散佈。',\n    'editor_lexical_license_link' => '完整的授權條款詳細資訊可在這裡找到。',\n    'editor_tiny_license' => '此編輯器是用 :tinyLink 構建的，基於 MIT 許可證。',\n    'editor_tiny_license_link' => 'TinyMCE 的版權和許可證詳細信息可以在這裡找到。',\n    'save_continue' => '保存頁面並繼續',\n    'callouts_cycle' => '(繼續按下以切換類型)',\n    'link_selector' => '鏈接到內容',\n    'shortcuts' => '快捷鍵',\n    'shortcut' => '快捷鍵',\n    'shortcuts_intro' => '編輯器中提供了以下快捷鍵：',\n    'windows_linux' => '(Windows/Linux)',\n    'mac' => '(Mac)',\n    'description' => '說明',\n];\n"
  },
  {
    "path": "lang/zh_TW/entities.php",
    "content": "<?php\n/**\n * Text used for 'Entities' (Document Structure Elements) such as\n * Books, Shelves, Chapters & Pages\n */\nreturn [\n\n    // Shared\n    'recently_created' => '最近建立',\n    'recently_created_pages' => '最近建立的頁面',\n    'recently_updated_pages' => '最近更新的頁面',\n    'recently_created_chapters' => '最近建立的章節',\n    'recently_created_books' => '最近建立的書本',\n    'recently_created_shelves' => '最近建立的書架',\n    'recently_update' => '最近更新',\n    'recently_viewed' => '最近檢視',\n    'recent_activity' => '近期活動',\n    'create_now' => '立即建立',\n    'revisions' => '修訂版本',\n    'meta_revision' => '修訂版本 #:revisionCount',\n    'meta_created' => '建立於 :timeLength',\n    'meta_created_name' => '由 :user 建立於 :timeLength',\n    'meta_updated' => '更新於 :timeLength',\n    'meta_updated_name' => '由 :user 更新於 :timeLength',\n    'meta_owned_name' => ':user 所擁有',\n    'meta_reference_count' => '被 :count 個項目引用',\n    'entity_select' => '選取項目',\n    'entity_select_lack_permission' => '你沒有權限使用此項目',\n    'images' => '圖片',\n    'my_recent_drafts' => '我最近的草稿',\n    'my_recently_viewed' => '我最近檢視',\n    'my_most_viewed_favourites' => '我瀏覽最多次的最愛',\n    'my_favourites' => '我的最愛',\n    'no_pages_viewed' => '您尚未看過任何頁面',\n    'no_pages_recently_created' => '最近未建立任何頁面',\n    'no_pages_recently_updated' => '最近沒有頁面被更新',\n    'export' => '匯出',\n    'export_html' => '網頁檔案',\n    'export_pdf' => 'PDF 檔案',\n    'export_text' => '純文字檔案',\n    'export_md' => 'Markdown 檔案',\n    'export_zip' => '可攜式 ZIP',\n    'default_template' => '預設頁面範本',\n    'default_template_explain' => '請設定一個頁面範本，作為新頁面的預設內容。請注意，這僅限於作者擁有頁面範本讀取權限時才能夠使用。',\n    'default_template_select' => '選擇一個頁面範本',\n    'import' => '匯入',\n    'import_validate' => '驗證匯入',\n    'import_desc' => '使用從相同或不同站點匯出的可攜式壓縮檔匯入書本、章節與頁面。選取 ZIP 檔案繼續。檔案上傳並通過驗證後，您就可以在下一個檢視中設定並確認匯入。',\n    'import_zip_select' => '選取要上傳的 ZIP 檔案',\n    'import_zip_validation_errors' => '驗證提供的 ZIP 檔案時偵測到錯誤：',\n    'import_pending' => '擱置中的匯入',\n    'import_pending_none' => '尚未開始匯入。',\n    'import_continue' => '繼續匯入',\n    'import_continue_desc' => '檢視要從上傳的 ZIP 檔匯入的內容。準備就緒後，執行匯入以將其內容加入本系統。成功匯入後，上傳的 ZIP 匯入檔案會自動移除。',\n    'import_details' => '匯入詳細資訊',\n    'import_run' => '執行匯入',\n    'import_size' => ':size 匯入 ZIP 大小',\n    'import_uploaded_at' => ':relativeTime 已上傳',\n    'import_uploaded_by' => '上傳者',\n    'import_location' => '匯入位置',\n    'import_location_desc' => '為您匯入的內容選取目標位置。您需要相關權限才能在您選擇的位置內建立。',\n    'import_delete_confirm' => '您確定要刪除此匯入嗎？',\n    'import_delete_desc' => '這將會刪除已上傳的匯入 ZIP 檔案，且無法還原。',\n    'import_errors' => '匯入錯誤',\n    'import_errors_desc' => '嘗試匯入時發生以下錯誤：',\n    'breadcrumb_siblings_for_page' => '瀏覽同層級頁面',\n    'breadcrumb_siblings_for_chapter' => '瀏覽同章節頁面',\n    'breadcrumb_siblings_for_book' => '瀏覽同書籍頁面',\n    'breadcrumb_siblings_for_bookshelf' => '瀏覽同書架頁面',\n\n    // Permissions and restrictions\n    'permissions' => '權限',\n    'permissions_desc' => '設定權限，並覆蓋角色預設權限',\n    'permissions_book_cascade' => '除非章節、頁面有自訂權限，否則書籍設定的權限將自動套用',\n    'permissions_chapter_cascade' => '除非章節、頁面有自訂權限，否則章節的權限將自動套用',\n    'permissions_save' => '儲存權限',\n    'permissions_owner' => '擁有者',\n    'permissions_role_everyone_else' => '所有其他人',\n    'permissions_role_everyone_else_desc' => '設定未被指派角色時的權限',\n    'permissions_role_override' => '覆蓋角色權限',\n    'permissions_inherit_defaults' => '繼承預設值',\n\n    // Search\n    'search_results' => '搜尋結果',\n    'search_total_results_found' => '找到了 :count 個結果 | 總共 :count 個結果',\n    'search_clear' => '清除搜尋',\n    'search_no_pages' => '沒有與此搜尋相符的頁面',\n    'search_for_term' => ':term 的搜尋結果',\n    'search_more' => '更多結果',\n    'search_advanced' => '進階搜尋',\n    'search_terms' => '搜尋字串',\n    'search_content_type' => '內容類型',\n    'search_exact_matches' => '精確符合',\n    'search_tags' => '標籤搜尋',\n    'search_options' => '選項',\n    'search_viewed_by_me' => '被我檢視',\n    'search_not_viewed_by_me' => '未被我檢視',\n    'search_permissions_set' => '權限設定',\n    'search_created_by_me' => '我建立的',\n    'search_updated_by_me' => '我更新的',\n    'search_owned_by_me' => '我所擁有的',\n    'search_date_options' => '日期選項',\n    'search_updated_before' => '在此之前更新',\n    'search_updated_after' => '在此之後更新',\n    'search_created_before' => '在此之前建立',\n    'search_created_after' => '在此之後建立',\n    'search_set_date' => '設定日期',\n    'search_update' => '更新搜尋結果',\n\n    // Shelves\n    'shelf' => '書架',\n    'shelves' => '書架',\n    'x_shelves' => ':count 書架 | :count 章節',\n    'shelves_empty' => '尚未建立書架',\n    'shelves_create' => '建立新書架',\n    'shelves_popular' => '熱門書架',\n    'shelves_new' => '新書架',\n    'shelves_new_action' => '建立新的書架',\n    'shelves_popular_empty' => '最受歡迎的書架將出現在這裡。',\n    'shelves_new_empty' => '最近建立的書架將出現在這裡。',\n    'shelves_save' => '儲存書架',\n    'shelves_books' => '此書架上的書本',\n    'shelves_add_books' => '新增書本至此書架',\n    'shelves_drag_books' => '拖曳書籍至下方，以便新增至此書架',\n    'shelves_empty_contents' => '此書架沒有分配任何書本',\n    'shelves_edit_and_assign' => '編輯書架以分配書本',\n    'shelves_edit_named' => '編輯書架',\n    'shelves_edit' => '編輯書架',\n    'shelves_delete' => '刪除書架',\n    'shelves_delete_named' => '刪除書架「:name」',\n    'shelves_delete_explain' => \"這將刪除名為「:name」的書架。但其中的書本不會被刪除。\",\n    'shelves_delete_confirmation' => '您確定要刪除此書架嗎？',\n    'shelves_permissions' => '書架權限',\n    'shelves_permissions_updated' => '已更新書架權限',\n    'shelves_permissions_active' => '已啟用書架權限',\n    'shelves_permissions_cascade_warning' => '因書籍可位於多個書架，因此書架權限不會自動套用至書籍上。但仍可透過下方的選項來複製權限到子書籍。',\n    'shelves_permissions_create' => '書架創建權限僅用於使用下面的操作將權限複製到子圖書。這個權限不是用來控制創建書籍的。',\n    'shelves_copy_permissions_to_books' => '將權限複製到書本',\n    'shelves_copy_permissions' => '複製權限',\n    'shelves_copy_permissions_explain' => '此操作會將此書架的當前權限設置應用於其中包含的所有圖書上。 啓用前請確保已保存對此書架權限的任何更改。',\n    'shelves_copy_permission_success' => '書架權限已複製到 :count 本書籍',\n\n    // Books\n    'book' => '書本',\n    'books' => '書本',\n    'x_books' => ':count 本書 | :count本書',\n    'books_empty' => '不存在已建立的書',\n    'books_popular' => '熱門書本',\n    'books_recent' => '近期書本',\n    'books_new' => '新書本',\n    'books_new_action' => '新書本',\n    'books_popular_empty' => '最受歡迎的書本將出現在這裡。',\n    'books_new_empty' => '最近建立的書本將出現在這裡。',\n    'books_create' => '建立新書本',\n    'books_delete' => '刪除書本',\n    'books_delete_named' => '刪除書本 :bookName',\n    'books_delete_explain' => '這將刪除書本「:bookName」。所有的章節和頁面都會被刪除。',\n    'books_delete_confirmation' => '您確定要刪除此書本嗎？',\n    'books_edit' => '編輯書本',\n    'books_edit_named' => '編輯書本「:bookName」',\n    'books_form_book_name' => '書本名稱',\n    'books_save' => '儲存書本',\n    'books_permissions' => '書本權限',\n    'books_permissions_updated' => '書本權限已更新',\n    'books_empty_contents' => '本書目前沒有頁面或章節。',\n    'books_empty_create_page' => '建立新頁面',\n    'books_empty_sort_current_book' => '排序目前書本',\n    'books_empty_add_chapter' => '新增章節',\n    'books_permissions_active' => '書本權限已啟用',\n    'books_search_this' => '搜尋此書本',\n    'books_navigation' => '書本導覽',\n    'books_sort' => '排序書本內容',\n    'books_sort_desc' => '在書籍中移動章節和頁面，重新安排其內容。可加入其他書籍，方便在書籍之間移動章節與頁面。可選擇設定自動排序規則，以便在變更時自動排序此書籍的內容。',\n    'books_sort_auto_sort' => '自動排序選項',\n    'books_sort_auto_sort_active' => '自動排序啟動：:sortName',\n    'books_sort_named' => '排序書本 :bookName',\n    'books_sort_name' => '按名稱排序',\n    'books_sort_created' => '按建立時間排序',\n    'books_sort_updated' => '按更新時間排序',\n    'books_sort_chapters_first' => '第一章',\n    'books_sort_chapters_last' => '最後一章',\n    'books_sort_show_other' => '顯示其他書本',\n    'books_sort_save' => '儲存新順序',\n    'books_sort_show_other_desc' => '新增書籍到排序清單中，以便跨書籍重組資料',\n    'books_sort_move_up' => '上移',\n    'books_sort_move_down' => '下移',\n    'books_sort_move_prev_book' => '移動至前一書籍',\n    'books_sort_move_next_book' => '移動至前一書籍',\n    'books_sort_move_prev_chapter' => '移動至前一章節',\n    'books_sort_move_next_chapter' => '移動至下一章節',\n    'books_sort_move_book_start' => '移動到書籍開頭',\n    'books_sort_move_book_end' => '移動到書籍結尾',\n    'books_sort_move_before_chapter' => '移動到章節之前',\n    'books_sort_move_after_chapter' => '移動到章節之後',\n    'books_copy' => '複製書籍',\n    'books_copy_success' => '書籍已成功被複製',\n\n    // Chapters\n    'chapter' => '章節',\n    'chapters' => '章節',\n    'x_chapters' => ':count個章節 | :count個章節',\n    'chapters_popular' => '熱門章節',\n    'chapters_new' => '新章節',\n    'chapters_create' => '建立章節',\n    'chapters_delete' => '刪除章節',\n    'chapters_delete_named' => '刪除章節 :chapterName',\n    'chapters_delete_explain' => '這將會刪除名稱為「:chapterName」的章節。此章節中的所有頁面都將會被刪除。',\n    'chapters_delete_confirm' => '您確定要刪除此章節嗎？',\n    'chapters_edit' => '編輯章節',\n    'chapters_edit_named' => '編輯章節「:chapterName」',\n    'chapters_save' => '儲存章節',\n    'chapters_move' => '移動章節',\n    'chapters_move_named' => '移動章節 :chapterName',\n    'chapters_copy' => '複製章節',\n    'chapters_copy_success' => '章節已成功被複製',\n    'chapters_permissions' => '章節權限',\n    'chapters_empty' => '本章目前沒有頁面。',\n    'chapters_permissions_active' => '章節權限已啟用',\n    'chapters_permissions_success' => '章節權限已更新',\n    'chapters_search_this' => '搜尋此章節',\n    'chapter_sort_book' => '排序書籍內容',\n\n    // Pages\n    'page' => '頁面',\n    'pages' => '頁面',\n    'x_pages' => ':count 頁 | :count 頁',\n    'pages_popular' => '熱門頁面',\n    'pages_new' => '新頁面',\n    'pages_attachments' => '附件',\n    'pages_navigation' => '頁面導覽',\n    'pages_delete' => '刪除頁面',\n    'pages_delete_named' => '刪除頁面 :pageName',\n    'pages_delete_draft_named' => '刪除草稿頁面 :pageName',\n    'pages_delete_draft' => '刪除草稿頁面',\n    'pages_delete_success' => '頁面已刪除',\n    'pages_delete_draft_success' => '草稿頁面已刪除',\n    'pages_delete_warning_template' => '此頁面是當前書籍或章節的默認頁面模板。刪除此頁面後，這些書籍或章節的默認頁面模板將被取消。',\n    'pages_delete_confirm' => '您確定要刪除此頁面嗎？',\n    'pages_delete_draft_confirm' => '您確定要刪除此草稿頁面嗎？',\n    'pages_editing_named' => '正在編輯頁面 :pageName',\n    'pages_edit_draft_options' => '草稿選項',\n    'pages_edit_save_draft' => '儲存草稿',\n    'pages_edit_draft' => '編輯頁面草稿',\n    'pages_editing_draft' => '正在編輯草稿',\n    'pages_editing_page' => '正在編輯頁面',\n    'pages_edit_draft_save_at' => '草稿儲存於 ',\n    'pages_edit_delete_draft' => '刪除草稿',\n    'pages_edit_delete_draft_confirm' => '您確定要刪除您的草稿頁面更改嗎？自上次完整保存以來的所有更改都將丟失，編輯器將更新為最新非草稿頁面。',\n    'pages_edit_discard_draft' => '放棄草稿',\n    'pages_edit_switch_to_markdown' => '切換到 Markdown 編輯器',\n    'pages_edit_switch_to_markdown_clean' => '(清除內容)',\n    'pages_edit_switch_to_markdown_stable' => '(保留內容)',\n    'pages_edit_switch_to_wysiwyg' => '切換到所見即所得編輯器',\n    'pages_edit_switch_to_new_wysiwyg' => '切換為新的所見即所得編輯器',\n    'pages_edit_switch_to_new_wysiwyg_desc' => '（仍在測試中）',\n    'pages_edit_set_changelog' => '設定變更日誌',\n    'pages_edit_enter_changelog_desc' => '輸入對您所做變動的簡易描述',\n    'pages_edit_enter_changelog' => '輸入變更日誌',\n    'pages_editor_switch_title' => '切換編輯器',\n    'pages_editor_switch_are_you_sure' => '你想要更改這頁所使用的編輯器嗎？',\n    'pages_editor_switch_consider_following' => '更換編輯器時請考慮以下事項：',\n    'pages_editor_switch_consideration_a' => '一旦選擇使用新的編輯器，其他頁面以及沒有權限更換編輯器的使用者都將使用新的編輯器',\n    'pages_editor_switch_consideration_b' => '在某些情況下，將遺失細部設定以及語法',\n    'pages_editor_switch_consideration_c' => '切換編輯器時，本次的標籤設定、版本修訂記錄將不會被保留',\n    'pages_save' => '儲存頁面',\n    'pages_title' => '頁面標題',\n    'pages_name' => '頁面名稱',\n    'pages_md_editor' => '編輯者',\n    'pages_md_preview' => '預覽',\n    'pages_md_insert_image' => '插入圖片',\n    'pages_md_insert_link' => '插入連結',\n    'pages_md_insert_drawing' => '插入繪圖',\n    'pages_md_show_preview' => '顯示預覽',\n    'pages_md_sync_scroll' => '預覽頁面同步捲動',\n    'pages_md_plain_editor' => '純文字編輯器',\n    'pages_drawing_unsaved' => '偵測到未儲存的繪圖',\n    'pages_drawing_unsaved_confirm' => '從之前保存失敗的繪圖中發現了可恢復的數據。您想恢復並繼續編輯這個未保存的繪圖嗎？',\n    'pages_not_in_chapter' => '頁面不在章節中',\n    'pages_move' => '移動頁面',\n    'pages_copy' => '複製頁面',\n    'pages_copy_desination' => '複製的目的地',\n    'pages_copy_success' => '頁面已成功複製',\n    'pages_permissions' => '頁面權限',\n    'pages_permissions_success' => '頁面權限已更新',\n    'pages_revision' => '修訂版本',\n    'pages_revisions' => '頁面修訂版本',\n    'pages_revisions_desc' => '下面列出的是該頁面的所有過去修訂。如果權限允許，您可以回顧、比較和恢復舊的頁面版本。頁面的完整歷史可能不會在這裡完全反映出來，因為根據系統配置，舊的修訂可能會被自動刪除。',\n    'pages_revisions_named' => ':pageName 頁面修訂版本',\n    'pages_revision_named' => ':pageName 頁面修訂版本',\n    'pages_revision_restored_from' => '從 #:id; :summary 復原',\n    'pages_revisions_created_by' => '建立者',\n    'pages_revisions_date' => '修訂日期',\n    'pages_revisions_number' => '#',\n    'pages_revisions_sort_number' => '修訂版號',\n    'pages_revisions_numbered' => '修訂版本 #:id',\n    'pages_revisions_numbered_changes' => '修訂版本 #:id 變更',\n    'pages_revisions_editor' => '編輯器類型',\n    'pages_revisions_changelog' => '變動日誌',\n    'pages_revisions_changes' => '變動',\n    'pages_revisions_current' => '目前版本',\n    'pages_revisions_preview' => '預覽',\n    'pages_revisions_restore' => '還原',\n    'pages_revisions_none' => '此頁面沒有修訂',\n    'pages_copy_link' => '複製連結',\n    'pages_edit_content_link' => '移動到編輯器區段',\n    'pages_pointer_enter_mode' => '進入區段選取模式',\n    'pages_pointer_label' => '頁面區段選項',\n    'pages_pointer_permalink' => '頁面區段永久連結',\n    'pages_pointer_include_tag' => '包含標籤的頁面區段',\n    'pages_pointer_toggle_link' => '永久連結模式，點擊顯示包含的標籤',\n    'pages_pointer_toggle_include' => '包含標籤模式，按下顯示永久鏈接',\n    'pages_permissions_active' => '頁面權限已啟用',\n    'pages_initial_revision' => '初次發布',\n    'pages_references_update_revision' => '系統自動更新的內部鏈接',\n    'pages_initial_name' => '新頁面',\n    'pages_editing_draft_notification' => '您正在編輯最後儲存為 :timeDiff 的草稿。',\n    'pages_draft_edited_notification' => '此頁面已經被更新過。建議您放棄此草稿。',\n    'pages_draft_page_changed_since_creation' => '這個頁面在您的草稿創建後被其他用戶更新了，您目前的草稿不包含新的內容。建議您放棄此草稿，或是注意不要覆蓋新的頁面更改。',\n    'pages_draft_edit_active' => [\n        'start_a' => ':count 位使用者已經開始編輯此頁面',\n        'start_b' => '使用者 :userName 已經開始編輯此頁面',\n        'time_a' => '自頁面上次更新以來',\n        'time_b' => '在最近:minCount分鐘',\n        'message' => ':start :time。注意不要覆寫其他人的更新！',\n    ],\n    'pages_draft_discarded' => '草稿已丟棄！編輯器已更新到當前頁面內容',\n    'pages_draft_deleted' => '草稿已刪除！編輯器已更新為當前頁面內容',\n    'pages_specific' => '特定頁面',\n    'pages_is_template' => '頁面模板',\n\n    // Editor Sidebar\n    'toggle_sidebar' => '切換側邊欄',\n    'page_tags' => '頁面標籤',\n    'chapter_tags' => '章節標籤',\n    'book_tags' => '書本標籤',\n    'shelf_tags' => '書架標籤',\n    'tag' => '標籤',\n    'tags' =>  '標籤',\n    'tags_index_desc' => '為內容加上標籤可靈活得進行分類。標籤可以設定名稱與數值，其中數值為選用。一旦標籤設定完成，便可透過標籤名稱與數值來進行搜尋。',\n    'tag_name' =>  '標籤名稱',\n    'tag_value' => '標籤值（選擇性）',\n    'tags_explain' => \"加入一些標籤以更好地對您的內容進行分類。 \\n 您可以為標籤分配一個值，以進行更深入的組織。\",\n    'tags_add' => '新增另一個標籤',\n    'tags_remove' => '移除此標籤',\n    'tags_usages' => '總用量',\n    'tags_assigned_pages' => '頁面',\n    'tags_assigned_chapters' => '章節',\n    'tags_assigned_books' => '書籍',\n    'tags_assigned_shelves' => '書架',\n    'tags_x_unique_values' => ':count 個不同的數值',\n    'tags_all_values' => '所有數值',\n    'tags_view_tags' => '檢視標籤',\n    'tags_view_existing_tags' => '檢視已存在的標籤',\n    'tags_list_empty_hint' => '可在編輯書架、書籍時設定標籤，或是在編輯章節、頁面時透過側邊欄設定',\n    'attachments' => '附件',\n    'attachments_explain' => '上傳一些檔案或附加連結以顯示在您的網頁上。將顯示在在頁面的側邊欄。',\n    'attachments_explain_instant_save' => '此處的變動將會立刻儲存。',\n    'attachments_upload' => '上傳檔案',\n    'attachments_link' => '附加連結',\n    'attachments_upload_drop' => '你也可以將檔案拖曳到此來上傳附加檔案',\n    'attachments_set_link' => '設定連結',\n    'attachments_delete' => '您確定要刪除此附件嗎？',\n    'attachments_dropzone' => '將檔案拖曳至此來上傳',\n    'attachments_no_files' => '尚未上傳檔案',\n    'attachments_explain_link' => '如果您不想上傳檔案，則可以附加連結。這可以是指向其他頁面的連結，也可以是指向雲端檔案的連結。',\n    'attachments_link_name' => '連結名稱',\n    'attachment_link' => '附件連結',\n    'attachments_link_url' => '連結到檔案',\n    'attachments_link_url_hint' => '網站或檔案的網址',\n    'attach' => '附加',\n    'attachments_insert_link' => '將附件連結新增到頁面',\n    'attachments_edit_file' => '編輯檔案',\n    'attachments_edit_file_name' => '檔案名稱',\n    'attachments_edit_drop_upload' => '拖曳檔案或點擊此處以上傳並覆寫',\n    'attachments_order_updated' => '附件順序已更新',\n    'attachments_updated_success' => '附件資訊已更新',\n    'attachments_deleted' => '附件已刪除',\n    'attachments_file_uploaded' => '附件上傳成功',\n    'attachments_file_updated' => '附件更新成功',\n    'attachments_link_attached' => '連結成功附加到頁面',\n    'templates' => '範本',\n    'templates_set_as_template' => '頁面為範本',\n    'templates_explain_set_as_template' => '您可以將此頁面設定為範本，以便在建立其他頁面時利用其內容。如果其他使用者對此頁面擁有檢視權限，則將可以使用此範本。',\n    'templates_replace_content' => '替換頁面內容',\n    'templates_append_content' => '附加到頁面內容',\n    'templates_prepend_content' => '前置頁面內容',\n\n    // Profile View\n    'profile_user_for_x' => '來這裡 :time 了',\n    'profile_created_content' => '已建立內容',\n    'profile_not_created_pages' => ':userName 尚未建立任何頁面',\n    'profile_not_created_chapters' => ':userName 尚未建立任何章節',\n    'profile_not_created_books' => ':userName 尚未建立任何書本',\n    'profile_not_created_shelves' => ':userName 沒有創建任何書架',\n\n    // Comments\n    'comment' => '評論',\n    'comments' => '評論',\n    'comment_add' => '新增評論',\n    'comment_none' => '無可顯示的留言',\n    'comment_placeholder' => '在這裡評論',\n    'comment_thread_count' => ':count 個留言討論串|:count 個留言討論串',\n    'comment_archived_count' => ':count 個已封存',\n    'comment_archived_threads' => '已封存的討論串',\n    'comment_save' => '儲存評論',\n    'comment_new' => '新評論',\n    'comment_created' => '評論於 :createDiff',\n    'comment_updated' => '由 :username 於 :updateDiff 更新',\n    'comment_updated_indicator' => '已更新',\n    'comment_deleted_success' => '評論已刪除',\n    'comment_created_success' => '評論已加入',\n    'comment_updated_success' => '評論已更新',\n    'comment_archive_success' => '留言已封存',\n    'comment_unarchive_success' => '留言已解除封存',\n    'comment_view' => '檢視留言',\n    'comment_jump_to_thread' => '前往討論串',\n    'comment_delete_confirm' => '您確定要刪除這則評論？',\n    'comment_in_reply_to' => '回覆 :commentId',\n    'comment_reference' => '參考資料',\n    'comment_reference_outdated' => '（過期）',\n    'comment_editor_explain' => '此為頁面上的評論。在查看檢視與儲存頁面的同時，可以新增和管理評論。',\n\n    // Revision\n    'revision_delete_confirm' => '您確定要刪除此修訂版本嗎？',\n    'revision_restore_confirm' => '您確定要還原此修訂版本嗎？ 目前頁面內容將被替換。',\n    'revision_cannot_delete_latest' => '無法刪除最新修訂版本。',\n\n    // Copy view\n    'copy_consider' => '複製內容時請注意以下事項',\n    'copy_consider_permissions' => '自定義權限設置將不會被複製。',\n    'copy_consider_owner' => '您將成為所有已複製內容的所有者。',\n    'copy_consider_images' => '頁面中的圖像文件不會被複製，原始圖像將保留它們與最初上傳到的頁面的關係。',\n    'copy_consider_attachments' => '頁面中的附件不會被複製。',\n    'copy_consider_access' => '改變位置、所有者或權限可能會導致此內容被以前無法訪問的人訪問。',\n\n    // Conversions\n    'convert_to_shelf' => '轉換成書架',\n    'convert_to_shelf_contents_desc' => '你可以將此書籍轉換成包含相同內容的新書架，書籍中的章節則會轉換成書籍。若書籍中有不屬於任何章節的頁面，這些頁面會自動轉換成新書架中的書籍。',\n    'convert_to_shelf_permissions_desc' => '在這本書上設置的任何權限都將複製到所有未強制執行權限的新書架和新子圖書上。請注意，書架上的權限不會像圖書那樣繼承到內容物上。',\n    'convert_book' => '轉換成書本',\n    'convert_book_confirm' => '您確定要轉換此書本嗎？',\n    'convert_undo_warning' => '這可不能輕易撤消。',\n    'convert_to_book' => '轉換成書籍',\n    'convert_to_book_desc' => '您可以將此章節轉換為具有相同內容的新書本。此章節中設置的任何權限都將複製到新書本上，但從父圖書繼承的任何權限都不會被複製，這可能會導致訪問控制發生變化。',\n    'convert_chapter' => '轉換章節',\n    'convert_chapter_confirm' => '您確定要轉換此章節嗎？',\n\n    // References\n    'references' => '引用',\n    'references_none' => '沒有跟蹤到對此項目的引用。',\n    'references_to_desc' => '下方列出了系統中鏈接到此項目的所有已知內容。',\n\n    // Watch Options\n    'watch' => '追蹤',\n    'watch_title_default' => '預設偏好設定',\n    'watch_desc_default' => '還原成預設的通知設定',\n    'watch_title_ignore' => '忽略',\n    'watch_desc_ignore' => '忽略所有通知，包括來自用戶級偏好的通知。',\n    'watch_title_new' => '新頁面',\n    'watch_desc_new' => '在此項目中創建任何新頁面時通知我。',\n    'watch_title_updates' => '所有頁面更新',\n    'watch_desc_updates' => '在所有新頁面和頁面更改時通知我。',\n    'watch_desc_updates_page' => '在有頁面發生更改時通知我。',\n    'watch_title_comments' => '所有頁面更新和評論',\n    'watch_desc_comments' => '在有新頁面、頁面更改和新評論時通知我。',\n    'watch_desc_comments_page' => '在有頁面更改和新評論時通知我。',\n    'watch_change_default' => '更改默認通知偏好',\n    'watch_detail_ignore' => '忽略通知',\n    'watch_detail_new' => '追蹤新頁面',\n    'watch_detail_updates' => '追蹤新頁面與異動',\n    'watch_detail_comments' => '追蹤新頁面、自動與評論',\n    'watch_detail_parent_book' => '已關注—繼承自父書本',\n    'watch_detail_parent_book_ignore' => '已忽略—繼承自父書本',\n    'watch_detail_parent_chapter' => '已關注—繼承自父章節',\n    'watch_detail_parent_chapter_ignore' => '已忽略—繼承自父章節',\n];\n"
  },
  {
    "path": "lang/zh_TW/errors.php",
    "content": "<?php\n/**\n * Text shown in error messaging.\n */\nreturn [\n\n    // Permissions\n    'permission' => '您沒有權限進入所請求的頁面。',\n    'permissionJson' => '您沒有權限執行所請求的動作。',\n\n    // Auth\n    'error_user_exists_different_creds' => '電子郵件為 :email 已存在，但帳號密碼不同。',\n    'auth_pre_register_theme_prevention' => '無法使用資料建立帳號',\n    'email_already_confirmed' => '已確認電子郵件，請嘗試登入。',\n    'email_confirmation_invalid' => '這個確認權杖無效或已被使用，請嘗試重新註冊。',\n    'email_confirmation_expired' => '這個確認權杖無效或已被使用，已傳送新的確認電子郵件。',\n    'email_confirmation_awaiting' => '用於此帳號的電子郵件地址需要確認',\n    'ldap_fail_anonymous' => '使用匿名綁定的 LDAP 存取失敗',\n    'ldap_fail_authed' => '使用指定的 DN 與密碼詳細資訊的 LDAP 存取失敗',\n    'ldap_extension_not_installed' => '未安裝 PHP 的 LDAP 擴充程式',\n    'ldap_cannot_connect' => '無法連線至 LDAP 伺服器，初始化連線失敗',\n    'saml_already_logged_in' => '已登入',\n    'saml_no_email_address' => '在外部認證系統提供的資料中找不到該使用者的電子郵件地址',\n    'saml_invalid_response_id' => '此應用程式啟動的處理程序無法識別來自外部認證系統的請求。登入後回上一頁可能會造成此問題。',\n    'saml_fail_authed' => '使用 :system 登入失敗，系統未提供成功的授權',\n    'oidc_already_logged_in' => '已登入',\n    'oidc_no_email_address' => '在外部認證系統提供的資料中找不到該使用者的電子郵件地址',\n    'oidc_fail_authed' => '使用 :system 登入失敗，系統未提供成功的授權',\n    'social_no_action_defined' => '未定義動作',\n    'social_login_bad_response' => \"在 :socialAccount 登入時遇到錯誤： \\n:error\",\n    'social_account_in_use' => ':socialAccount 帳號已被使用，請嘗試透過 :socialAccount 選項登入。',\n    'social_account_email_in_use' => '電子郵件 :email 已被使用。如果您已有帳號，您可以在您的個人設定中連結您的 :socialAccount 帳號。',\n    'social_account_existing' => '此 :socialAccount 已附加至您的個人資料。',\n    'social_account_already_used_existing' => '此 :socialAccount 帳號已經被其他使用者使用。',\n    'social_account_not_used' => '此 :socialAccount 帳號未連結至任何使用者。請至您的個人設定中連結。 ',\n    'social_account_register_instructions' => '如果您還沒有帳號，您可以使用 :socialAccount 選項註冊帳號。',\n    'social_driver_not_found' => '找不到社交驅動程式',\n    'social_driver_not_configured' => '您的 :socialAccount 社交設定不正確。',\n    'invite_token_expired' => '此邀請連結已過期。您可以嘗試重設您的帳號密碼。',\n    'login_user_not_found' => '使用者不存在',\n\n    // System\n    'path_not_writable' => '無法上傳到 :filePath 檔案路徑。請確定其對伺服器來說是可寫入的。',\n    'cannot_get_image_from_url' => '無法從 :url 取得圖片',\n    'cannot_create_thumbs' => '伺服器無法建立縮圖。請檢查您是否安裝了 PHP 的 GD 擴充程式。',\n    'server_upload_limit' => '伺服器不允許上傳這個大的檔案。請嘗試較小的檔案。',\n    'server_post_limit' => '伺服器無法處理提供的資料，請嘗試刪減內容或較小的檔案',\n    'uploaded'  => '伺服器不允許上傳這個大的檔案。請嘗試較小的檔案。',\n\n    // Drawing & Images\n    'image_upload_error' => '上傳圖片時發生錯誤',\n    'image_upload_type_error' => '上傳圖片類型無效',\n    'image_upload_replace_type' => '必須使用的檔案類型才能置換圖檔',\n    'image_upload_memory_limit' => '由於系統限制，無法處理上傳的檔案或縮圖',\n    'image_thumbnail_memory_limit' => '由於系統限制，無法建立不同尺寸的圖片',\n    'image_gallery_thumbnail_memory_limit' => '由於系統限制，無法建立縮圖',\n    'drawing_data_not_found' => '無法載入繪圖資料，繪圖檔案可能不存在，或您可能沒有權限存取它。',\n\n    // Attachments\n    'attachment_not_found' => '找不到附件',\n    'attachment_upload_error' => '上傳檔案時發生錯誤',\n\n    // Pages\n    'page_draft_autosave_fail' => '無法儲存草稿。請確保您在儲存此頁面前已連線至網際網路',\n    'page_draft_delete_fail' => '無法刪除草稿並取得最新的頁面存檔',\n    'page_custom_home_deletion' => '無法刪除被設定為首頁的頁面',\n\n    // Entities\n    'entity_not_found' => '找不到實體',\n    'bookshelf_not_found' => '未找到書架',\n    'book_not_found' => '找不到書本',\n    'page_not_found' => '找不到頁面',\n    'chapter_not_found' => '找不到章節',\n    'selected_book_not_found' => '找不到選定的書本',\n    'selected_book_chapter_not_found' => '找不到選定的書本或章節',\n    'guests_cannot_save_drafts' => '訪客無法儲存草稿',\n\n    // Users\n    'users_cannot_delete_only_admin' => '您不能刪除唯一的管理員帳號',\n    'users_cannot_delete_guest' => '您不能刪除訪客使用者',\n    'users_could_not_send_invite' => '由於寄送邀請電子郵件失敗，因此無法建立使用者',\n\n    // Roles\n    'role_cannot_be_edited' => '無法編輯這個角色',\n    'role_system_cannot_be_deleted' => '無法刪除系統角色',\n    'role_registration_default_cannot_delete' => '無法刪除設定預設註冊的角色',\n    'role_cannot_remove_only_admin' => '此使用者是唯一被指派為管理員角色的使用者。在試圖移除這裡前，請將管理員角色指派給其他使用者。',\n\n    // Comments\n    'comment_list' => '擷取評論時發生錯誤。',\n    'cannot_add_comment_to_draft' => '您無法新增評論到草稿中。',\n    'comment_add' => '新增／更新評論時發生錯誤。',\n    'comment_delete' => '刪除評論時發生錯誤。',\n    'empty_comment' => '無法新增空評論。',\n\n    // Error pages\n    '404_page_not_found' => '找不到頁面',\n    'sorry_page_not_found' => '抱歉，找不到您在尋找的頁面。',\n    'sorry_page_not_found_permission_warning' => '如果您確認這個頁面存在，則代表可能沒有查看它的權限。',\n    'image_not_found' => '找不到圖片',\n    'image_not_found_subtitle' => '對不起，無法找到您所看的圖片',\n    'image_not_found_details' => '原本的圖片可能已經被刪除',\n    'return_home' => '回到首頁',\n    'error_occurred' => '發生錯誤',\n    'app_down' => ':appName 離線中',\n    'back_soon' => '它應該很快就會重新上線。',\n\n    // Import\n    'import_zip_cant_read' => '無法讀取 ZIP 檔案。',\n    'import_zip_cant_decode_data' => '無法尋找並解碼 ZIP data.json 內容。',\n    'import_zip_no_data' => 'ZIP 檔案資料沒有預期的書本、章節或頁面內容。',\n    'import_zip_data_too_large' => 'ZIP 檔案 data.json 的內容超過了設定的應用程式最大上傳大小。',\n    'import_validation_failed' => '匯入 ZIP 驗證失敗，發生錯誤：',\n    'import_zip_failed_notification' => '匯入 ZIP 檔案失敗。',\n    'import_perms_books' => '您缺乏建立書本所需的權限。',\n    'import_perms_chapters' => '您缺乏建立章節所需的權限。',\n    'import_perms_pages' => '您缺乏建立頁面所需的權限。',\n    'import_perms_images' => '您缺乏建立影像所需的權限。',\n    'import_perms_attachments' => '您缺乏建立附件所需的權限。',\n\n    // API errors\n    'api_no_authorization_found' => '在請求上找不到授權權杖',\n    'api_bad_authorization_format' => '在請求中找到授權權杖，但格式似乎不正確',\n    'api_user_token_not_found' => '找不到與提供的授權權杖相符的 API 權杖',\n    'api_incorrect_token_secret' => '給定使用的 API 權杖的密碼錯誤',\n    'api_user_no_api_permission' => '使用的 API 權杖擁有者無權呼叫 API',\n    'api_user_token_expired' => '使用的授權權杖已過期',\n    'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication',\n\n    // Settings & Maintenance\n    'maintenance_test_email_failure' => '寄送測試電子郵件時發生錯誤:',\n\n    // HTTP errors\n    'http_ssr_url_no_match' => 'URL 與設置的 SSR 主機不符',\n];\n"
  },
  {
    "path": "lang/zh_TW/notifications.php",
    "content": "<?php\n/**\n * Text used for activity-based notifications.\n */\nreturn [\n\n    'new_comment_subject' => '頁面上有新評論：:pageName',\n    'new_comment_intro' => '一位用戶在 :appName: 的頁面上發表了評論',\n    'new_page_subject' => '新頁面：:pageName',\n    'new_page_intro' => ':appName: 中創建了一個新頁面',\n    'updated_page_subject' => '頁面更新：:pageName',\n    'updated_page_intro' => ':appName: 中的一個頁面已被更新',\n    'updated_page_debounce' => '為了防止出現大量通知，一段時間內您不會收到同一編輯者再次編輯本頁面的通知。',\n    'comment_mention_subject' => '您在以下頁面的留言中被提及：:pageName',\n    'comment_mention_intro' => '您再在 :appName: 的留言中被提及',\n\n    'detail_page_name' => '頁面名稱:',\n    'detail_page_path' => '頁面路徑：',\n    'detail_commenter' => '評論者:',\n    'detail_comment' => '評論:',\n    'detail_created_by' => '建立者:',\n    'detail_updated_by' => '更新者：',\n\n    'action_view_comment' => '檢視評論',\n    'action_view_page' => '查看頁面',\n\n    'footer_reason' => '向您發送此通知是因為 :link 涵蓋了該項目的此類活動。',\n    'footer_reason_link' => '個人偏好通知設定',\n];\n"
  },
  {
    "path": "lang/zh_TW/pagination.php",
    "content": "<?php\n/**\n * Pagination Language Lines\n * The following language lines are used by the paginator library to build\n * the simple pagination links.\n */\nreturn [\n\n    'previous' => '&laquo; 上一頁',\n    'next'     => '下一頁 &raquo;',\n\n];\n"
  },
  {
    "path": "lang/zh_TW/passwords.php",
    "content": "<?php\n/**\n * Password Reminder Language Lines\n * The following language lines are the default lines which match reasons\n * that are given by the password broker for a password update attempt has failed.\n */\nreturn [\n\n    'password' => '密碼必須至少八個字元，並與確認密碼相符。',\n    'user' => \"沒有使用這個電子郵件位址的使用者。\",\n    'token' => '這個電子郵件地址的密碼重設權杖無效。',\n    'sent' => '我們已經透過電子郵件發送您的密碼重設連結。',\n    'reset' => '您的密碼已被重設！',\n\n];\n"
  },
  {
    "path": "lang/zh_TW/preferences.php",
    "content": "<?php\n\n/**\n * Text used for user-preference specific views within bookstack.\n */\n\nreturn [\n    'my_account' => '帳號設定',\n\n    'shortcuts' => '快捷鍵',\n    'shortcuts_interface' => '快速鍵設定',\n    'shortcuts_toggle_desc' => '您可以在此處啟用或停用鍵盤系統介面快捷鍵，這些快捷鍵用於導覽與操作。',\n    'shortcuts_customize_desc' => '您可以自訂下方的每個快捷鍵。只要在選取快捷鍵輸入後按下您想要使用的按鍵組合即可。',\n    'shortcuts_toggle_label' => '鍵盤快捷鍵已啟用',\n    'shortcuts_section_navigation' => '導覽',\n    'shortcuts_section_actions' => '通用動作',\n    'shortcuts_save' => '儲存快捷鍵',\n    'shortcuts_overlay_desc' => '注意：當快捷鍵啟用時，可以按下「?」來使用小幫手覆蓋畫面。這將會在目前的畫面上突顯可見動作的快捷鍵。',\n    'shortcuts_update_success' => '快捷鍵偏好設定已更新！',\n    'shortcuts_overview_desc' => '你可使用鍵盤快速鍵來快速瀏覽系統界面',\n\n    'notifications' => '通知設定',\n    'notifications_desc' => '控制在系統有特定活動時，是否要接收電子郵件通知',\n    'notifications_opt_own_page_changes' => '當我的頁面有異動時發送通知',\n    'notifications_opt_own_page_comments' => '當我的頁面有評論時發送通知',\n    'notifications_opt_comment_mentions' => '當我在留言中被提及時通知我',\n    'notifications_opt_comment_replies' => '當我的評論有新的回覆時發送通知',\n    'notifications_save' => '儲存偏好設定',\n    'notifications_update_success' => '通知設定已更新',\n    'notifications_watched' => '追蹤與忽略的項目',\n    'notifications_watched_desc' => '以下是已經套用自訂追蹤設定的項目。若要調整，請至該項目側邊欄中的「追蹤」更新設定',\n\n    'auth' => '存取、安全',\n    'auth_change_password' => '變更密碼',\n    'auth_change_password_desc' => '修改登入時使用的密碼，密碼長度至少需要有 8 個字',\n    'auth_change_password_success' => '密碼已更新',\n\n    'profile' => '個人檔案',\n    'profile_desc' => '管理欲呈現給其他使用的個人資料，另外資料也用於交流與系統個人化',\n    'profile_view_public' => '檢視公開的個人檔案',\n    'profile_name_desc' => '設定欲公開顯示的名稱，系統中的其他使用者將會透過內容、活動記錄看到您設定的名稱',\n    'profile_email_desc' => 'Email 將用於使用者通知以及系統存取授權 (若有啟用)',\n    'profile_email_no_permission' => '很遺憾您沒有權限變更您的 email，如果有需要，請洽詢系統管理員協助變更 email',\n    'profile_avatar_desc' => ' 選一張圖片供顯示給其他使用者看，理想的圖片大小為 256x256 像素',\n    'profile_admin_options' => '管理選項',\n    'profile_admin_options_desc' => '系統管理層選項可在「設定」=>「使用者」找到，像是角色管理等',\n\n    'delete_account' => '刪除帳號',\n    'delete_my_account' => '刪除我的帳號',\n    'delete_my_account_desc' => '將從系統中刪除所有帳號資料，此動作無法復原。你所建立的內容、頁面以及上傳的圖片將會保留。',\n    'delete_my_account_warning' => '您確定要刪除您的帳戶嗎？',\n];\n"
  },
  {
    "path": "lang/zh_TW/settings.php",
    "content": "<?php\n/**\n * Settings text strings\n * Contains all text strings used in the general settings sections of BookStack\n * including users and roles.\n */\nreturn [\n\n    // Common Messages\n    'settings' => '設定',\n    'settings_save' => '儲存設定',\n    'system_version' => '系統版本',\n    'categories' => '分類',\n\n    // App Settings\n    'app_customization' => '自訂',\n    'app_features_security' => '功能與安全',\n    'app_name' => '應用程式名稱',\n    'app_name_desc' => '此名稱會在網頁頂端與任何系統傳送的電子郵件中出現。',\n    'app_name_header' => '在網頁頂端顯示名稱',\n    'app_public_access' => '公開存取',\n    'app_public_access_desc' => '啟用此選項將會允許未登入的訪客存取您 BookStack 站台中的內容。',\n    'app_public_access_desc_guest' => '可以透過「訪客」使用者控制公開訪客的存取。',\n    'app_public_access_toggle' => '允許公開存取',\n    'app_public_viewing' => '允許公開檢視？',\n    'app_secure_images' => '更高安全性的圖片上傳',\n    'app_secure_images_toggle' => '啟用更高安全性的圖片上傳',\n    'app_secure_images_desc' => '因為效能因素，所有圖片都是公開的。此選項會在圖片的網址前加入一串隨機且難以猜測的字串。確保未啟用目錄索引，讓直接進入變得更困難。',\n    'app_default_editor' => '預設頁面編輯器',\n    'app_default_editor_desc' => '選擇編輯頁面時預設使用的編輯器，這項設定值可被頁面的權限覆蓋',\n    'app_custom_html' => '自訂 HTML 標題內容',\n    'app_custom_html_desc' => '此處加入的任何內容都將插入到每個頁面的 <head> 部分的底部，這對於覆蓋樣式或加入分析程式碼很方便。',\n    'app_custom_html_disabled_notice' => '在此設定頁面上停用了自訂 HTML 標題內容，以確保任何重大變更都能被還原。',\n    'app_logo' => '應用程式圖示',\n    'app_logo_desc' => '這個設定會被使用在應用程式標題欄等區域；圖片的高度應為 86 像素，大型圖片將會按比例縮小。',\n    'app_icon' => '應用程式圖示',\n    'app_icon_desc' => '這個圖示將顯示在瀏覽器分頁以及捷徑，應為 256 像素的的正方形 PNG 圖片',\n    'app_homepage' => '應用程式首頁',\n    'app_homepage_desc' => '選取要作為首頁的頁面，這將會取代預設首頁。選定頁面的頁面權限將會被忽略。',\n    'app_homepage_select' => '選取頁面',\n    'app_footer_links' => '頁面註腳連結',\n    'app_footer_links_desc' => '新增連結以在網站註腳顯示。這些將會顯示在大多數頁面的底部，包含那些不需要登入的頁面。您可以使用 \"trans::<key>\" 標籤來使用系統定義的翻譯。舉例來說：使用 \"trans::common.privacy_policy\" 將會提供已翻譯的文字「隱私權政策」，以及 \"trans::common.terms_of_service\" 將會提供已翻譯的文字「服務條款」。',\n    'app_footer_links_label' => '連結標籤',\n    'app_footer_links_url' => '連結網址',\n    'app_footer_links_add' => '新增註腳連結',\n    'app_disable_comments' => '停用評論',\n    'app_disable_comments_toggle' => '停用評論',\n    'app_disable_comments_desc' => '在應用程式的所有頁面停用評論。<br>既有的評論將不會顯示。',\n\n    // Color settings\n    'color_scheme' => '應用程式配色',\n    'color_scheme_desc' => '設定使用者頁面欲使用的顏色。可分別針對淺色模式與暗色模式設定顏色，以確保容易閱讀',\n    'ui_colors_desc' => '設定頁面主要色彩以及頁面連結預設顏色。主要色彩用於標題橫幅、按鈕以及主要操作界面，頁面連結顏色，主要用於主頁面、編輯頁面中的文字連結、操作按鈕。',\n    'app_color' => '主要顏色',\n    'link_color' => '連結預設顏色',\n    'content_colors_desc' => '設定頁面層次結構中的元素顏色；為了提高可讀性，建議選擇亮度與預設顏色相似的顏色。',\n    'bookshelf_color' => '書架顔色',\n    'book_color' => '書本顔色',\n    'chapter_color' => '章節顔色',\n    'page_color' => '頁面顔色',\n    'page_draft_color' => '頁面草稿顏色',\n\n    // Registration Settings\n    'reg_settings' => '註冊',\n    'reg_enable' => '啟用註冊',\n    'reg_enable_toggle' => '啟用註冊',\n    'reg_enable_desc' => '啟用註冊後，使用者將可以自行註冊為應用程式的使用者。註冊後，他們將會得到一個預設的使用者角色。',\n    'reg_default_role' => '註冊後的預設使用者角色',\n    'reg_enable_external_warning' => '當外部 LDAP 或 SAML 身份驗證啟用時，將會忽略上述選項。如果外部身份驗證成功，將會自動在本系統建立使用者帳號。',\n    'reg_email_confirmation' => '電子郵件驗證',\n    'reg_email_confirmation_toggle' => '需要電子郵件驗證',\n    'reg_confirm_email_desc' => '如果使用網域限制，則需要電子郵件驗證，且此選項將被忽略。',\n    'reg_confirm_restrict_domain' => '網域限制',\n    'reg_confirm_restrict_domain_desc' => '輸入您想要限制註冊的電子郵件網域列表，以英文逗號分隔。在可以與應用程式互動前，使用者將會收到電子郵件以確認他們的電子郵件地址。<br>注意，使用者可以在註冊成功後變更他們的電子郵件地址。',\n    'reg_confirm_restrict_domain_placeholder' => '尚未設定限制',\n\n    // Sorting Settings\n    'sorting' => '清單與排序',\n    'sorting_book_default' => '預設書籍排序規則',\n    'sorting_book_default_desc' => '選取要套用至新書籍的預設排序規則。這不會影響現有書籍，並可按書籍覆寫。',\n    'sorting_rules' => '排序規則',\n    'sorting_rules_desc' => '這些是預先定義的排序作業，可套用於系統中的內容。',\n    'sort_rule_assigned_to_x_books' => '指定給 :count 本書',\n    'sort_rule_create' => '建立排序規則',\n    'sort_rule_edit' => '編輯排序規則',\n    'sort_rule_delete' => '刪除排序規則',\n    'sort_rule_delete_desc' => '從系統移除此排序規則。使用此排序的書籍將會還原為手動排序。',\n    'sort_rule_delete_warn_books' => '此排序規則目前用於 :count 本書。您確定您想要刪除嗎？',\n    'sort_rule_delete_warn_default' => '此排序規則目前為書籍的預設值。您確定您想要刪除嗎？',\n    'sort_rule_details' => '排序規則詳細資訊',\n    'sort_rule_details_desc' => '設定此排序規則的名稱，當使用者選取排序時，該名稱將會出現在清單中。',\n    'sort_rule_operations' => '排序選項',\n    'sort_rule_operations_desc' => '設定要執行的排序動作，方法是從可用的操作清單中移動它們。使用時，操作將依從上到下的順序套用。儲存時，在此處所做的任何變更都會套用至所有指定的書籍。',\n    'sort_rule_available_operations' => '可用操作',\n    'sort_rule_available_operations_empty' => '無剩餘操作',\n    'sort_rule_configured_operations' => '已設定的操作',\n    'sort_rule_configured_operations_empty' => '從「可用操作」清單中拖曳/新增操作',\n    'sort_rule_op_asc' => '（遞增）',\n    'sort_rule_op_desc' => '（遞減）',\n    'sort_rule_op_name' => '名稱 - 按字母順序排列',\n    'sort_rule_op_name_numeric' => '名稱 - 數字',\n    'sort_rule_op_created_date' => '建立日期',\n    'sort_rule_op_updated_date' => '更新日期',\n    'sort_rule_op_chapters_first' => '第一章',\n    'sort_rule_op_chapters_last' => '最後一章',\n    'sorting_page_limits' => '每頁顯示限制',\n    'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.',\n\n    // Maintenance settings\n    'maint' => '維護',\n    'maint_image_cleanup' => '清理圖片',\n    'maint_image_cleanup_desc' => '掃描頁面與修訂版本內容來檢查目前使用了哪些圖片，而哪些圖片又是多餘的。請確保您在執行這個動作前建立了完整的資料庫與映像檔備份。',\n    'maint_delete_images_only_in_revisions' => '也刪除僅存在於舊的頁面修訂版本中存在的圖片',\n    'maint_image_cleanup_run' => '執行清理',\n    'maint_image_cleanup_warning' => '發現了 :count 張可能未使用的圖片。您確定要刪除這些圖片嗎？',\n    'maint_image_cleanup_success' => '找到並刪除了 :count 張可能未使用的圖片！',\n    'maint_image_cleanup_nothing_found' => '找不到未使用的圖片，未刪除任何檔案！',\n    'maint_send_test_email' => '傳送測試電子郵件',\n    'maint_send_test_email_desc' => '這會將測試電子郵件傳送到您的個人資料中指定的電子郵件地址。',\n    'maint_send_test_email_run' => '傳送測試郵件',\n    'maint_send_test_email_success' => '電子郵件已傳送到 :address',\n    'maint_send_test_email_mail_subject' => '測試郵件',\n    'maint_send_test_email_mail_greeting' => '電子郵件傳遞似乎有效！',\n    'maint_send_test_email_mail_text' => '恭喜！您收到這封電子郵件通知時，代表您的電子郵件設定已正確設定。',\n    'maint_recycle_bin_desc' => '刪除的書架、書本、章節與頁面將會被傳送到回收桶，這樣仍可以還原或永久刪除。回收桶中較舊的項目可能會在一段時間後自動移除，取決於您的系統設定。',\n    'maint_recycle_bin_open' => '開啟回收桶',\n    'maint_regen_references' => '重新生成引用',\n    'maint_regen_references_desc' => '此操作將重建數據庫中的跨項目引用索引。這通常是自動處理的，但可能有助於索引舊內容或通過非官方方法添加的內容。',\n    'maint_regen_references_success' => '引用索引已重新生成！',\n    'maint_timeout_command_note' => '備註：這項操作需要較長的時間，可能導致多數的網路環境發生連線逾時的問題。若有需要，可以透過終端機指令來替代。',\n\n    // Recycle Bin\n    'recycle_bin' => '資源回收桶',\n    'recycle_bin_desc' => '在這裡，您可以還原已刪除的項目，或是選擇將其從系統中永久移除。與系統中套用了權限過濾條件類似的活動列表不同的是，此列表並未過濾。',\n    'recycle_bin_deleted_item' => '已刪除項目',\n    'recycle_bin_deleted_parent' => '上層',\n    'recycle_bin_deleted_by' => '刪除由',\n    'recycle_bin_deleted_at' => '刪除時間',\n    'recycle_bin_permanently_delete' => '永久刪除',\n    'recycle_bin_restore' => '還原',\n    'recycle_bin_contents_empty' => '回收桶目前是空的',\n    'recycle_bin_empty' => '清空回收桶',\n    'recycle_bin_empty_confirm' => '這將會永久破壞回收桶中的所有項目，包括每個項目中包含的內容。您確定您想要清空回收桶嗎？',\n    'recycle_bin_destroy_confirm' => '此操作將從系統中永久刪除此項目以及下面列出的所有子元素，並且您將無法還原此內容。您確定要永久刪除該項目嗎？',\n    'recycle_bin_destroy_list' => '要被銷毀的項目',\n    'recycle_bin_restore_list' => '要被還原的項目',\n    'recycle_bin_restore_confirm' => '此動作將會還原已被刪除的項目（包含任何下層元素）到其原始位置。如果原始位置已被刪除，且目前位於垃圾桶裡，那麼上層項目也需要被還原。',\n    'recycle_bin_restore_deleted_parent' => '此項目的上層項目也已被刪除。因此將會保持被刪除的狀態，直到上層項目也被還原。',\n    'recycle_bin_restore_parent' => '還原上層',\n    'recycle_bin_destroy_notification' => '已從回收桶刪除共 :count 個項目。',\n    'recycle_bin_restore_notification' => '已從回收桶還原共 :count 個項目。',\n\n    // Audit Log\n    'audit' => '稽核記錄',\n    'audit_desc' => '此稽核日誌會顯示被系統追蹤的活動列表。與系統中套用了權限過濾條件類似的活動列表不同的是，此列表並未過濾。',\n    'audit_event_filter' => '活動過濾條件',\n    'audit_event_filter_no_filter' => '無過濾條件',\n    'audit_deleted_item' => '已刪除的項目',\n    'audit_deleted_item_name' => '名稱：:name',\n    'audit_table_user' => '使用者',\n    'audit_table_event' => '活動',\n    'audit_table_related' => '相關的項目或詳細資訊',\n    'audit_table_ip' => 'IP 位址',\n    'audit_table_date' => '活動日期',\n    'audit_date_from' => '日期範圍，從',\n    'audit_date_to' => '日期範圍，到',\n\n    // Role Settings\n    'roles' => '角色',\n    'role_user_roles' => '使用者角色',\n    'roles_index_desc' => '「角色」用於將系統權限套用至使用者群組。當使用者擁有多角色時，\n使用者會自動繼承角色中的所有系統權限',\n    'roles_x_users_assigned' => ':count 位用戶已分配|:count 位用戶已分配',\n    'roles_x_permissions_provided' => ':count 個權限|:count 個權限',\n    'roles_assigned_users' => '已分配用戶',\n    'roles_permissions_provided' => '已提供權限',\n    'role_create' => '建立新角色',\n    'role_delete' => '刪除角色',\n    'role_delete_confirm' => '這將會刪除名為「:roleName」的角色.',\n    'role_delete_users_assigned' => '有 :userCount 位使用者屬於此角色。如果您想將此角色中的使用者遷移，請在下面選擇一個新角色。',\n    'role_delete_no_migration' => \"不要遷移使用者\",\n    'role_delete_sure' => '您確定要刪除此角色？',\n    'role_edit' => '編輯角色',\n    'role_details' => '角色詳細資訊',\n    'role_name' => '角色名稱',\n    'role_desc' => '角色簡短說明',\n    'role_mfa_enforced' => '多重身分驗證',\n    'role_external_auth_id' => '外部身份驗證 ID',\n    'role_system' => '系統權限',\n    'role_manage_users' => '管理使用者',\n    'role_manage_roles' => '管理角色與角色權限',\n    'role_manage_entity_permissions' => '管理所有書本、章節與頁面的權限',\n    'role_manage_own_entity_permissions' => '管理自己的書本、章節與頁面的權限',\n    'role_manage_page_templates' => '管理頁面範本',\n    'role_access_api' => '存取系統 API',\n    'role_manage_settings' => '管理應用程式設定',\n    'role_export_content' => '匯出內容',\n    'role_import_content' => '匯入內容',\n    'role_editor_change' => '重設頁面編輯器',\n    'role_notifications' => '管理和接收通知',\n    'role_permission_note_users_and_roles' => '這些權限在技術上亦將提供系統內使用者與角色的能見度及搜尋功能。',\n    'role_asset' => '資源權限',\n    'roles_system_warning' => '請注意，有上述三項權限中的任一項的使用者都可以更改自己或系統中其他人的權限。有這些權限的角色只應分配給受信任的使用者。',\n    'role_asset_desc' => '對系統內資源的預設權限將由這裡的權限控制。若有單獨設定在書本、章節和頁面上的權限，將會覆寫這裡的權限設定。',\n    'role_asset_admins' => '管理員會自動取得對所有內容的存取權，但這些選項可能會顯示或隱藏使用者介面的選項。',\n    'role_asset_image_view_note' => '這與圖像管理器中的可見性有關。已經上傳的圖片的實際訪問取決於系統圖像存儲選項。',\n    'role_asset_users_note' => '這些權限在技術上亦將提供系統內使用者的能見度及搜尋功能。',\n    'role_all' => '全部',\n    'role_own' => '擁有',\n    'role_controlled_by_asset' => '依據隸屬的資源來決定',\n    'role_save' => '儲存角色',\n    'role_users' => '屬於此角色的使用者',\n    'role_users_none' => '目前沒有使用者被分配到此角色',\n\n    // Users\n    'users' => '使用者',\n    'users_index_desc' => '在系統中創建和管理使用者帳號。使用者帳號用於紀錄登入與編輯活動；訪問權限則由使用者所歸屬的角色群組決定，但使用者是否具備內容的所有權以及其他因素，都可能會影響到存取權限。',\n    'user_profile' => '使用者個人資料',\n    'users_add_new' => '新增使用者',\n    'users_search' => '搜尋使用者',\n    'users_latest_activity' => '最新活動',\n    'users_details' => '使用者詳細資訊',\n    'users_details_desc' => '為此使用者設定顯示名稱與電子郵件地址。電子郵件地址將用於登入應用程式。',\n    'users_details_desc_no_email' => '為此使用者設定顯示名稱，這樣其他人才能認出該使用者。',\n    'users_role' => '使用者角色',\n    'users_role_desc' => '選取要分配的此使用者的角色。若使用者被分配到多個角色，則這些角色的權限將會堆疊，使用者將會取得被分配角色的所有功能。',\n    'users_password' => '使用者密碼',\n    'users_password_desc' => '設定用於登入應用程式的密碼。密碼必須至少 8 個字元長。',\n    'users_send_invite_text' => '您可以選擇向此使用者傳送邀請電子郵件，讓他們可以設定自己的密碼，您也可以自行設定他們的密碼。',\n    'users_send_invite_option' => '傳送邀請電子郵件給使用者',\n    'users_external_auth_id' => '外部身份驗證 ID',\n    'users_external_auth_id_desc' => '使用外部驗證系統時 (如 SAML2、OIDC、LDAP)，將使這個帳號與驗證系統帳號連結。若使用一般的 email 認證方式，可以忽略此欄位。',\n    'users_password_warning' => '如果您想更改此用戶的密碼，請填寫以下內容：',\n    'users_system_public' => '此使用者代表造訪您站台的任何訪客使用者。其不能用於登入，而會自動分配。',\n    'users_delete' => '刪除使用者',\n    'users_delete_named' => '刪除使用者 :userName',\n    'users_delete_warning' => '這將從系統中完全刪除名為「:userName」的使用者。',\n    'users_delete_confirm' => '您確定要刪除此使用者？',\n    'users_migrate_ownership' => '轉移所有權',\n    'users_migrate_ownership_desc' => '如果您希望其他使用者變成目前此使用者擁有的所有項目的新擁有者，請在此處選取新的使用者。',\n    'users_none_selected' => '未選取使用者',\n    'users_edit' => '編輯使用者',\n    'users_edit_profile' => '編輯個人資料',\n    'users_avatar' => '使用者大頭照',\n    'users_avatar_desc' => '選取一張代表此使用者的圖片。這應該是大約 256px 的正方形。',\n    'users_preferred_language' => '偏好語言',\n    'users_preferred_language_desc' => '此選項將會變更用於應用程式使用者介面的語言。不會影響任何使用者建立的內容。',\n    'users_social_accounts' => '社群網站帳號',\n    'users_social_accounts_desc' => '查看此用戶已連接的社交賬戶狀態。 除了主要認證系統外，社交賬戶也可用於系統訪問。',\n    'users_social_accounts_info' => '您可以在此處連結您其他的帳號以供快速登入。從此處取消連結帳號並不會撤銷先前已授權的存取。請從您連結的社群網站帳號的個人設定中撤銷存取權。',\n    'users_social_connect' => '連結帳號',\n    'users_social_disconnect' => '取消連結帳號',\n    'users_social_status_connected' => '已連接',\n    'users_social_status_disconnected' => '已斷開連接',\n    'users_social_connected' => ':socialAccount 帳號已經成功連結到您的個人資料。',\n    'users_social_disconnected' => ':socialAccount 帳號已經成功取消連結。',\n    'users_api_tokens' => 'API 權杖',\n    'users_api_tokens_desc' => '創建和管理用於 BookStack REST API 認證的訪問令牌。 API 的權限是通過令牌所屬的用戶管理的。',\n    'users_api_tokens_none' => '尚未為此使用者建立 API 權杖',\n    'users_api_tokens_create' => '建立權杖',\n    'users_api_tokens_expires' => '過期',\n    'users_api_tokens_docs' => 'API 文件',\n    'users_mfa' => '多重身分驗證',\n    'users_mfa_desc' => '設定多重身份驗證為您的帳戶多增加了一道防線',\n    'users_mfa_x_methods' => ':count 個措施已配置|:count 個措施已配置',\n    'users_mfa_configure' => '方式設置',\n\n    // API Tokens\n    'user_api_token_create' => '建立 API 權杖',\n    'user_api_token_name' => '名稱',\n    'user_api_token_name_desc' => '給您的權杖易於辨識的名稱，如此未來才能提醒其預期用途。',\n    'user_api_token_expiry' => '到期日',\n    'user_api_token_expiry_desc' => '設定此權杖的到期日。在此日期後，使用此權杖發出的請求將不再起作用。若將此欄留空，將會設定在100年後過期。',\n    'user_api_token_create_secret_message' => '建立此權杖後，將會立即生成並顯示「權杖 ID」與「權杖密碼」。該密碼將只會顯示一次，因此請在繼續操作前將其複製到安全的地方。',\n    'user_api_token' => 'API 權杖',\n    'user_api_token_id' => '權杖 ID',\n    'user_api_token_id_desc' => '這是此權杖由系統生成的不可編輯識別字串，必須在 API 請求中提供。',\n    'user_api_token_secret' => '權杖密碼',\n    'user_api_token_secret_desc' => '這是此權杖由系統生成的密碼，必須在 API 請求中提供。該密碼將只會顯示一次，因此請在繼續操作前將其複製到安全的地方。',\n    'user_api_token_created' => '權杖建立於:timeAgo',\n    'user_api_token_updated' => '權杖更新於:timeAgo',\n    'user_api_token_delete' => '刪除權杖',\n    'user_api_token_delete_warning' => '這將會從系統中完全刪除名為「:tokenName」的 API 權杖。',\n    'user_api_token_delete_confirm' => '您確定要刪除此 API 權杖嗎？',\n\n    // Webhooks\n    'webhooks' => 'Webhooks',\n    'webhooks_index_desc' => 'Webhook 是一種在系統內發生某些操作和事件時將數據發送到外部 URL 的方法，它允許與外部平台（例如消息傳遞或通知系統）進行基於事件的集成。',\n    'webhooks_x_trigger_events' => ':count 個觸發事件 |:count 個觸發事件',\n    'webhooks_create' => '建立 Webhook',\n    'webhooks_none_created' => '沒有已建立的 Webhook',\n    'webhooks_edit' => '設置 Webhook',\n    'webhooks_save' => '儲存 Webhook',\n    'webhooks_details' => 'WebHook 詳細資料',\n    'webhooks_details_desc' => '提供一個用戶友好的名稱和一個 POST Endpoint 作為 Webhook 數據發送的位置。',\n    'webhooks_events' => 'Webhook 事件',\n    'webhooks_events_desc' => '選擇所有應觸發此 Webhook 的事件。',\n    'webhooks_events_warning' => '請記住，即使應用了自定義權限，所有選定的事件也仍然會被觸發。 確保使用此 Webhook 不會洩露機密內容。',\n    'webhooks_events_all' => '全部系統活動',\n    'webhooks_name' => 'Webhook 名稱',\n    'webhooks_timeout' => 'Webhook 請求超時（秒）',\n    'webhooks_endpoint' => 'Webhook 端點',\n    'webhooks_active' => 'Webhook 啟用',\n    'webhook_events_table_header' => '事件',\n    'webhooks_delete' => '刪除 Webhook',\n    'webhooks_delete_warning' => '這將會從系統中完全刪除名為 “:webhookName” 的 webhook。',\n    'webhooks_delete_confirm' => '確定要刪除此 Webhook 嗎？',\n    'webhooks_format_example' => 'Webhook 格式範例',\n    'webhooks_format_example_desc' => 'Webhook 數據會以 POST 請求按照以下 JSON 格式發送到設置的 Endpoint。 “related_item” 和 “url” 屬性是可選的，取決於觸發的事件類型。',\n    'webhooks_status' => 'Webhook 狀態',\n    'webhooks_last_called' => '最後一次調用：',\n    'webhooks_last_errored' => '上次錯誤',\n    'webhooks_last_error_message' => '上次錯誤信息',\n\n    // Licensing\n    'licenses' => '授權',\n    'licenses_desc' => '本頁提供 BookStack 使用到的專案以及函式庫的詳細授權資料，其中部份專案及函式庫僅開開發環境中使用。',\n    'licenses_bookstack' => 'BookStack 授權',\n    'licenses_php' => 'PHP 函式庫授權',\n    'licenses_js' => 'JavaScript 函式庫授權',\n    'licenses_other' => '其它授權',\n    'license_details' => '詳細授權資料',\n\n    //! If editing translations files directly please ignore this in all\n    //! languages apart from en. Content will be auto-copied from en.\n    //!////////////////////////////////\n    'language_select' => [\n        'en' => 'English',\n        'ar' => 'العربية',\n        'bg' => 'Bǎlgarski',\n        'bs' => 'Bosanski',\n        'ca' => '加泰隆尼亞語',\n        'cs' => 'Česky',\n        'cy' => 'Cymraeg',\n        'da' => '丹麥',\n        'de' => 'Deutsch (Sie)',\n        'de_informal' => 'Deutsch (Du)',\n        'el' => 'ελληνικά',\n        'es' => 'Español',\n        'es_AR' => 'Español Argentina',\n        'et' => 'Eesti keel',\n        'eu' => 'Euskara',\n        'fa' => 'فارسی',\n        'fi' => 'Suomi',\n        'fr' => 'Français',\n        'he' => '希伯來語',\n        'hr' => 'Hrvatski',\n        'hu' => 'Magyar',\n        'id' => 'Bahasa Indonesia',\n        'it' => 'Italian',\n        'ja' => '日本語',\n        'ko' => '한국어',\n        'lt' => 'Lietuvių Kalba',\n        'lv' => 'Latviešu Valoda',\n        'nb' => 'Norsk (Bokmål)',\n        'ne' => 'नेपाली',\n        'nn' => 'Nynorsk',\n        'nl' => 'Nederlands',\n        'pl' => 'Polski',\n        'pt' => 'Português',\n        'pt_BR' => 'Português do Brasil',\n        'ro' => 'Română',\n        'ru' => 'Русский',\n        'sk' => 'Slovensky',\n        'sl' => 'Slovenščina',\n        'sv' => 'Svenska',\n        'tr' => 'Türkçe',\n        'uk' => 'Українська',\n        'uz' => 'O‘zbekcha',\n        'vi' => 'Tiếng Việt',\n        'zh_CN' => '简体中文',\n        'zh_TW' => '繁體中文',\n    ],\n    //!////////////////////////////////\n];\n"
  },
  {
    "path": "lang/zh_TW/validation.php",
    "content": "<?php\n/**\n * Validation Lines\n * The following language lines contain the default error messages used by\n * the validator class. Some of these rules have multiple versions such\n * as the size rules. Feel free to tweak each of these messages here.\n */\nreturn [\n\n    // Standard laravel validation lines\n    'accepted'             => '必須同意 :attribute。',\n    'active_url'           => ':attribute 並非有效的網址。',\n    'after'                => ':attribute 必須是在 :date 後的日期。',\n    'alpha'                => ':attribute 只能包含字母。',\n    'alpha_dash'           => ':attribute 只能包含字母、數字、破折號與底線。',\n    'alpha_num'            => ':attribute 只能包含字母和數字。',\n    'array'                => ':attribute 必須是陣列。',\n    'backup_codes'         => '提供的代碼無效或已被使用。',\n    'before'               => ':attribute 必須是在 :date 前的日期。',\n    'between'              => [\n        'numeric' => ':attribute 必須在 :min 到 :max 之間。',\n        'file'    => ':attribute 必須為 :min 到 :max KB。',\n        'string'  => ':attribute 必須在 :min 到 :max 個字元之間。',\n        'array'   => ':attribute 必須在 :min 到 :max 項之間。',\n    ],\n    'boolean'              => ':attribute 欄位必須為 true 或 false。',\n    'confirmed'            => ':attribute 確認不符。',\n    'date'                 => ':attribute 並非有效的日期。',\n    'date_format'          => ':attribute 與 :format 格式不相符。',\n    'different'            => ':attribute 和 :other 必須不同。',\n    'digits'               => ':attribute 必須為 :digits 位數。',\n    'digits_between'       => ':attribute 必須為 :min 到 :max 位數。',\n    'email'                => ':attribute 必須是有效的電子郵件地址。',\n    'ends_with' => ':attribute必須以下列之一結尾：:values',\n    'file'                 => ':attribute 必須作為有效檔案提供。',\n    'filled'               => ':attribute 欄位必填。',\n    'gt'                   => [\n        'numeric' => ':attribute 必須大於 :value。',\n        'file'    => ':attribute 必須大於 :value KB。',\n        'string'  => ':attribute 必須多於 :value 個字元。',\n        'array'   => ':attribute 必須包含多於 :value 個項目。',\n    ],\n    'gte'                  => [\n        'numeric' => ':attribute 必須大於或等於 :value。',\n        'file'    => ':attribute 必須大於或等於 :value KB。',\n        'string'  => ':attribute 必須多於或等於 :value 個字元。',\n        'array'   => ':attribute 必須有 :value 或更多項。',\n    ],\n    'exists'               => '選定的 :attribute 無效。',\n    'image'                => ':attribute 必須為圖片。',\n    'image_extension'      => ':attribute 必須包含有效且受支援的圖片副檔名。',\n    'in'                   => '選定的 :attribute 無效。',\n    'integer'              => ':attribute 必須是整數。',\n    'ip'                   => ':attribute 必須是有效的 IP 位置。',\n    'ipv4'                 => ':attribute 必須是有效的 IPv4 位置。',\n    'ipv6'                 => ':attribute 必須是有效的 IPv6 位置。',\n    'json'                 => ':attribute 必須是有效的 JSON 字串。',\n    'lt'                   => [\n        'numeric' => ':attribute 必須小於 :value。',\n        'file'    => ':attribute 必須小於 :value KB。',\n        'string'  => ':attribute 必須少於 :value 個字元。',\n        'array'   => ':attribute 必須少於 :value 個項目。',\n    ],\n    'lte'                  => [\n        'numeric' => ':attribute 必須小於或等於 :value。',\n        'file'    => ':attribute 必須小於或等於 :value KB。',\n        'string'  => ':attribute 必須少於或等於 :value 個字元。',\n        'array'   => ':attribute 不能超過 :value 個項目。',\n    ],\n    'max'                  => [\n        'numeric' => ':attribute 不能超過 :max。',\n        'file'    => ':attribute 不能超過 :max KB。',\n        'string'  => ':attribute 不能超過 :max 個字元。',\n        'array'   => ':attribute 不能有超過:max項。',\n    ],\n    'mimes'                => ':attribute 必須是 :values 類型的檔案。',\n    'min'                  => [\n        'numeric' => ':attribute 至少為:min。',\n        'file'    => ':attribute 必須至少為:min KB。',\n        'string'  => ':attribute 至少為:min個字元。',\n        'array'   => ':attribute 至少有:min項。',\n    ],\n    'not_in'               => '選中的 :attribute 無效。',\n    'not_regex'            => 'The :attribute格式無效。',\n    'numeric'              => ':attribute 必須是一個數。',\n    'regex'                => ':attribute 格式無效。',\n    'required'             => ':attribute 字段是必需的。',\n    'required_if'          => '當:other為:value時，:attribute 字段是必需的。',\n    'required_with'        => '當:values存在時，:attribute 字段是必需的。',\n    'required_with_all'    => '當:values存在時，:attribute 字段是必需的。',\n    'required_without'     => '當:values不存在時，:attribute 字段是必需的。',\n    'required_without_all' => '當:values均不存在時，:attribute 字段是必需的。',\n    'same'                 => ':attribute 與 :other 必須匹配。',\n    'safe_url'             => '提供的連結可能不安全。',\n    'size'                 => [\n        'numeric' => ':attribute 必須為:size。',\n        'file'    => ':attribute 必須為:size KB。',\n        'string'  => ':attribute 必須為:size個字元。',\n        'array'   => ':attribute 必須包含:size項。',\n    ],\n    'string'               => ':attribute 必須是字元串。',\n    'timezone'             => ':attribute 必須是有效的區域。',\n    'totp'                 => '提供的代碼無效或已過期。',\n    'unique'               => ':attribute 已經被使用。',\n    'url'                  => ':attribute 格式無效。',\n    'uploaded'             => '無法上傳文檔案， 伺服器可能不接受此大小的檔案。',\n\n    'zip_file' => ':attribute 需要參照 ZIP 中的檔案。',\n    'zip_file_size' => '檔案 :attribute 不能超過 :size MB。',\n    'zip_file_mime' => ':attribute 需要參照類型為 :validTypes 的檔案，找到 :foundType。',\n    'zip_model_expected' => '預期為資料物件，但找到「:type」。',\n    'zip_unique' => '對於 ZIP 中的物件類型，:attribute 必須是唯一的。',\n\n    // Custom validation lines\n    'custom' => [\n        'password-confirm' => [\n            'required_with' => '需要確認密碼',\n        ],\n    ],\n\n    // Custom validation attributes\n    'attributes' => [],\n];\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"private\": true,\n  \"scripts\": {\n    \"build:css:dev\": \"sass ./resources/sass:./public/dist --embed-sources\",\n    \"build:css:watch\": \"sass ./resources/sass:./public/dist --watch --embed-sources\",\n    \"build:css:production\": \"sass ./resources/sass:./public/dist -s compressed\",\n    \"build:js:dev\": \"node dev/build/esbuild.mjs\",\n    \"build:js:watch\": \"node dev/build/esbuild.mjs watch\",\n    \"build:js:production\": \"node dev/build/esbuild.mjs production\",\n    \"build\": \"npm-run-all --parallel build:*:dev\",\n    \"production\": \"npm-run-all --parallel build:*:production\",\n    \"dev\": \"npm-run-all --parallel build:*:watch\",\n    \"watch\": \"npm-run-all --parallel build:*:watch\",\n    \"permissions\": \"chown -R $USER:$USER bootstrap/cache storage public/uploads\",\n    \"lint\": \"eslint \\\"resources/**/*.js\\\" \\\"resources/**/*.mjs\\\"\",\n    \"fix\": \"eslint --fix \\\"resources/**/*.js\\\" \\\"resources/**/*.mjs\\\"\",\n    \"ts:lint\": \"tsc --noEmit\",\n    \"test\": \"jest\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"^10.0.1\",\n    \"@lezer/generator\": \"^1.8.0\",\n    \"@types/markdown-it\": \"^14.1.2\",\n    \"@types/sortablejs\": \"^1.15.9\",\n    \"chokidar-cli\": \"^3.0\",\n    \"esbuild\": \"^0.27.3\",\n    \"eslint\": \"^10.0.2\",\n    \"globals\": \"^17.4.0\",\n    \"jest\": \"^30.2.0\",\n    \"jest-environment-jsdom\": \"^30.2.0\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"sass\": \"^1.97.3\",\n    \"ts-jest\": \"^29.4.6\",\n    \"ts-node\": \"^10.9.2\",\n    \"typescript\": \"5.9.*\"\n  },\n  \"dependencies\": {\n    \"@codemirror/commands\": \"^6.10.2\",\n    \"@codemirror/lang-css\": \"^6.3.1\",\n    \"@codemirror/lang-html\": \"^6.4.11\",\n    \"@codemirror/lang-javascript\": \"^6.2.5\",\n    \"@codemirror/lang-json\": \"^6.0.2\",\n    \"@codemirror/lang-markdown\": \"^6.5.0\",\n    \"@codemirror/lang-php\": \"^6.0.2\",\n    \"@codemirror/lang-xml\": \"^6.1.0\",\n    \"@codemirror/language\": \"^6.12.2\",\n    \"@codemirror/legacy-modes\": \"^6.5.2\",\n    \"@codemirror/state\": \"^6.5.4\",\n    \"@codemirror/theme-one-dark\": \"^6.1.3\",\n    \"@codemirror/view\": \"^6.39.16\",\n    \"@lezer/highlight\": \"^1.2.3\",\n    \"@ssddanbrown/codemirror-lang-smarty\": \"^1.0.0\",\n    \"@ssddanbrown/codemirror-lang-twig\": \"^1.0.0\",\n    \"@types/jest\": \"^30.0.0\",\n    \"codemirror\": \"^6.0.2\",\n    \"idb-keyval\": \"^6.2.2\",\n    \"markdown-it\": \"^14.1.1\",\n    \"markdown-it-task-lists\": \"^2.1.1\",\n    \"snabbdom\": \"^3.6.3\",\n    \"sortablejs\": \"^1.15.7\"\n  }\n}\n"
  },
  {
    "path": "phpcs.xml",
    "content": "<?xml version=\"1.0\"?>\n<ruleset xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" name=\"PHP_CodeSniffer\" xsi:noNamespaceSchemaLocation=\"phpcs.xsd\">\n    <description>The coding standard for BookStack</description>\n\n    <file>app</file>\n    <file>bootstrap/app.php</file>\n    <file>database</file>\n    <file>public/index.php</file>\n    <file>routes</file>\n    <file>tests</file>\n\n    <arg name=\"basepath\" value=\".\"/>\n    <arg name=\"colors\"/>\n    <arg name=\"parallel\" value=\"75\"/>\n    <arg value=\"np\"/>\n\n    <rule ref=\"PSR12\"/>\n\n    <rule ref=\"PSR1.Methods.CamelCapsMethodName\">\n        <exclude-pattern>./tests/*</exclude-pattern>\n    </rule>\n\n    <rule ref=\"PSR1.Classes.ClassDeclaration.MultipleClasses\">\n        <exclude-pattern>./tests/*</exclude-pattern>\n    </rule>\n\n    <rule ref=\"PSR1.Classes.ClassDeclaration.MissingNamespace\">\n        <exclude-pattern>./database/*</exclude-pattern>\n    </rule>\n\n    <rule ref=\"PSR12.Files.FileHeader.IncorrectOrder\">\n        <exclude-pattern>./app/Config/*</exclude-pattern>\n    </rule>\n\n</ruleset>\n"
  },
  {
    "path": "phpstan.neon.dist",
    "content": "includes:\n    - ./vendor/larastan/larastan/extension.neon\n\nparameters:\n\n    paths:\n        - app\n\n    # The level 8 is the highest level\n    level: 3\n\n    phpVersion:\n        min: 80200\n        max: 80400\n\n    bootstrapFiles:\n      - bootstrap/phpstan.php\n\n    ignoreErrors:\n    #  - '#PHPDoc tag @throws with type .*?Psr\\\\SimpleCache\\\\InvalidArgumentException.*? is not subtype of Throwable#'\n\n    excludePaths:\n        - ./Config/**/*.php\n        - ./dev/**/*.php"
  },
  {
    "path": "phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/11.5/phpunit.xsd\"\n         bootstrap=\"vendor/autoload.php\"\n         displayDetailsOnTestsThatTriggerDeprecations=\"true\"\n         colors=\"true\">\n  <testsuites>\n    <testsuite name=\"Application Test Suite\">\n      <directory>./tests/</directory>\n    </testsuite>\n  </testsuites>\n  <php>\n    <server name=\"APP_ENV\" value=\"testing\"/>\n    <server name=\"APP_DEBUG\" value=\"false\"/>\n    <server name=\"APP_LANG\" value=\"en\"/>\n    <server name=\"APP_THEME\" value=\"none\"/>\n    <server name=\"APP_AUTO_LANG_PUBLIC\" value=\"true\"/>\n    <server name=\"APP_URL\" value=\"http://bookstack.dev\"/>\n    <server name=\"APP_TIMEZONE\" value=\"UTC\"/>\n    <server name=\"APP_DISPLAY_TIMEZONE\" value=\"UTC\"/>\n    <server name=\"ALLOWED_IFRAME_HOSTS\" value=\"\"/>\n    <server name=\"ALLOWED_IFRAME_SOURCES\" value=\"https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com\"/>\n    <server name=\"ALLOWED_SSR_HOSTS\" value=\"*\"/>\n    <server name=\"CACHE_DRIVER\" value=\"array\"/>\n    <server name=\"SESSION_DRIVER\" value=\"array\"/>\n    <server name=\"QUEUE_CONNECTION\" value=\"sync\"/>\n    <server name=\"DB_CONNECTION\" value=\"mysql_testing\"/>\n    <server name=\"BCRYPT_ROUNDS\" value=\"4\"/>\n    <server name=\"MAIL_DRIVER\" value=\"array\"/>\n    <server name=\"MAIL_PORT\" value=\"587\"/>\n    <server name=\"MAIL_VERIFY_SSL\" value=\"true\"/>\n    <server name=\"LOG_CHANNEL\" value=\"single\"/>\n    <server name=\"AUTH_METHOD\" value=\"standard\"/>\n    <server name=\"AUTH_AUTO_INITIATE\" value=\"false\"/>\n    <server name=\"DISABLE_EXTERNAL_SERVICES\" value=\"true\"/>\n    <server name=\"ALLOW_UNTRUSTED_SERVER_FETCHING\" value=\"false\"/>\n    <server name=\"CONTENT_FILTERING\" value=\"jhfa\"/>\n    <server name=\"ALLOW_CONTENT_SCRIPTS\" value=\"false\"/>\n    <server name=\"AVATAR_URL\" value=\"\"/>\n    <server name=\"LDAP_START_TLS\" value=\"false\"/>\n    <server name=\"LDAP_VERSION\" value=\"3\"/>\n    <server name=\"LDAP_DUMP_USER_DETAILS\" value=\"false\"/>\n    <server name=\"LDAP_DUMP_USER_GROUPS\" value=\"false\"/>\n    <server name=\"SESSION_SECURE_COOKIE\" value=\"null\"/>\n    <server name=\"STORAGE_TYPE\" value=\"local\"/>\n    <server name=\"STORAGE_ATTACHMENT_TYPE\" value=\"local\"/>\n    <server name=\"STORAGE_IMAGE_TYPE\" value=\"local\"/>\n    <server name=\"GITHUB_APP_ID\" value=\"aaaaaaaaaaaaaa\"/>\n    <server name=\"GITHUB_APP_SECRET\" value=\"aaaaaaaaaaaaaa\"/>\n    <server name=\"GITHUB_AUTO_REGISTER\" value=\"\"/>\n    <server name=\"GITHUB_AUTO_CONFIRM_EMAIL\" value=\"\"/>\n    <server name=\"GOOGLE_APP_ID\" value=\"aaaaaaaaaaaaaa\"/>\n    <server name=\"GOOGLE_APP_SECRET\" value=\"aaaaaaaaaaaaaa\"/>\n    <server name=\"GOOGLE_AUTO_REGISTER\" value=\"\"/>\n    <server name=\"GOOGLE_AUTO_CONFIRM_EMAIL\" value=\"\"/>\n    <server name=\"GOOGLE_SELECT_ACCOUNT\" value=\"\"/>\n    <server name=\"DEBUGBAR_ENABLED\" value=\"false\"/>\n    <server name=\"SAML2_ENABLED\" value=\"false\"/>\n    <server name=\"API_REQUESTS_PER_MIN\" value=\"180\"/>\n    <server name=\"LOG_FAILED_LOGIN_MESSAGE\" value=\"\"/>\n    <server name=\"LOG_FAILED_LOGIN_CHANNEL\" value=\"testing\"/>\n    <server name=\"WKHTMLTOPDF\" value=\"false\"/>\n    <server name=\"EXPORT_PDF_COMMAND\" value=\"false\"/>\n    <server name=\"APP_DEFAULT_DARK_MODE\" value=\"false\"/>\n    <server name=\"IP_ADDRESS_PRECISION\" value=\"4\"/>\n  </php>\n  <source>\n    <include>\n      <directory suffix=\".php\">app/</directory>\n    </include>\n  </source>\n</phpunit>\n"
  },
  {
    "path": "public/.htaccess",
    "content": "<IfModule mod_rewrite.c>\n    <IfModule mod_negotiation.c>\n        Options -MultiViews -Indexes\n    </IfModule>\n\n    RewriteEngine On\n\n    # Handle Authorization Header\n    RewriteCond %{HTTP:Authorization} .\n    RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]\n\n    # Redirect Trailing Slashes If Not A Folder...\n    RewriteCond %{REQUEST_FILENAME} !-d\n    RewriteCond %{REQUEST_URI} (.+)/$\n    RewriteRule ^ %1 [L,R=301]\n\n    # Send Requests To Front Controller...\n    RewriteCond %{REQUEST_FILENAME} !-d\n    RewriteCond %{REQUEST_FILENAME} !-f\n    RewriteRule ^ index.php [L]\n</IfModule>\n"
  },
  {
    "path": "public/index.php",
    "content": "<?php\n\nuse BookStack\\Http\\Request;\nuse Illuminate\\Contracts\\Http\\Kernel;\n\ndefine('LARAVEL_START', microtime(true));\n\n// Determine if the application is in maintenance mode...\nif (file_exists(__DIR__ . '/../storage/framework/maintenance.php')) {\n    require __DIR__ . '/../storage/framework/maintenance.php';\n}\n\n// Register the Composer autoloader...\nrequire __DIR__ . '/../vendor/autoload.php';\n\n\n// Run the application\n$app = require_once __DIR__ . '/../bootstrap/app.php';\n$app->alias('request', Request::class);\n\n$kernel = $app->make(Kernel::class);\n\n$response = tap($kernel->handle(\n    $request = Request::capture()\n))->send();\n\n$kernel->terminate($request, $response);\n"
  },
  {
    "path": "public/libs/tinymce/langs/README.md",
    "content": "This is where language files should be placed.\n\nPlease DO NOT translate these directly, use this service instead: https://crowdin.com/project/tinymce\n"
  },
  {
    "path": "public/libs/tinymce/license.txt",
    "content": "MIT License\n\nCopyright (c) 2022 Ephox Corporation DBA Tiny Technologies, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "public/libs/tinymce/skins/content/dark/content.js",
    "content": "tinymce.Resource.add('content/dark/content.css', \"body{background-color:#222f3e;color:#fff;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}a{color:#4099ff}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border=\\\"0\\\"]):not([style*=border-width]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border=\\\"0\\\"]):not([style*=border-style]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border=\\\"0\\\"]):not([style*=border-color]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-color]) th{border-color:#6d737b}figure{display:table;margin:1rem auto}figure figcaption{color:#8a8f97;display:block;margin-top:.25rem;text-align:center}hr{border-color:#6d737b;border-style:solid;border-width:1px 0 0 0}code{background-color:#6d737b;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #6d737b;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #6d737b;margin-right:1.5rem;padding-right:1rem}\")\n//# sourceMappingURL=content.js.map\n"
  },
  {
    "path": "public/libs/tinymce/skins/content/default/content.js",
    "content": "tinymce.Resource.add('content/default/content.css', \"body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border=\\\"0\\\"]):not([style*=border-width]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border=\\\"0\\\"]):not([style*=border-style]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border=\\\"0\\\"]):not([style*=border-color]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-color]) th{border-color:#ccc}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem}\")\n//# sourceMappingURL=content.js.map\n"
  },
  {
    "path": "public/libs/tinymce/skins/content/document/content.js",
    "content": "tinymce.Resource.add('content/document/content.css', \"@media screen{html{background:#f4f4f4;min-height:100%}}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif}@media screen{body{background-color:#fff;box-shadow:0 0 4px rgba(0,0,0,.15);box-sizing:border-box;margin:1rem auto 0;max-width:820px;min-height:calc(100vh - 1rem);padding:4rem 6rem 6rem 6rem}}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border=\\\"0\\\"]):not([style*=border-width]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border=\\\"0\\\"]):not([style*=border-style]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border=\\\"0\\\"]):not([style*=border-color]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-color]) th{border-color:#ccc}figure figcaption{color:#999;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem}\")\n//# sourceMappingURL=content.js.map\n"
  },
  {
    "path": "public/libs/tinymce/skins/content/tinymce-5/content.js",
    "content": "tinymce.Resource.add('content/tinymce-5/content.css', \"body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border=\\\"0\\\"]):not([style*=border-width]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border=\\\"0\\\"]):not([style*=border-style]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border=\\\"0\\\"]):not([style*=border-color]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-color]) th{border-color:#ccc}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem}\")\n//# sourceMappingURL=content.js.map\n"
  },
  {
    "path": "public/libs/tinymce/skins/content/tinymce-5-dark/content.js",
    "content": "tinymce.Resource.add('content/tinymce-5-dark/content.css', \"body{background-color:#2f3742;color:#dfe0e4;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}a{color:#4099ff}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border=\\\"0\\\"]):not([style*=border-width]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border=\\\"0\\\"]):not([style*=border-style]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border=\\\"0\\\"]):not([style*=border-color]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-color]) th{border-color:#6d737b}figure{display:table;margin:1rem auto}figure figcaption{color:#8a8f97;display:block;margin-top:.25rem;text-align:center}hr{border-color:#6d737b;border-style:solid;border-width:1px 0 0 0}code{background-color:#6d737b;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #6d737b;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #6d737b;margin-right:1.5rem;padding-right:1rem}\")\n//# sourceMappingURL=content.js.map\n"
  },
  {
    "path": "public/libs/tinymce/skins/content/writer/content.js",
    "content": "tinymce.Resource.add('content/writer/content.css', \"body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem auto;max-width:900px}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border=\\\"0\\\"]):not([style*=border-width]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border=\\\"0\\\"]):not([style*=border-style]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border=\\\"0\\\"]):not([style*=border-color]) td,table[border]:not([border=\\\"0\\\"]):not([style*=border-color]) th{border-color:#ccc}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem}\")\n//# sourceMappingURL=content.js.map\n"
  },
  {
    "path": "public/libs/tinymce/skins/ui/tinymce-5/content.inline.js",
    "content": "tinymce.Resource.add('ui/tinymce-5/content.inline.css', \".mce-content-body .mce-item-anchor{background:transparent url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'8'%20height%3D'12'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20d%3D'M0%200L8%200%208%2012%204.09117821%209%200%2012z'%2F%3E%3C%2Fsvg%3E%0A\\\") no-repeat center}.mce-content-body .mce-item-anchor:empty{cursor:default;display:inline-block;height:12px!important;padding:0 2px;-webkit-user-modify:read-only;-moz-user-modify:read-only;-webkit-user-select:all;-moz-user-select:all;user-select:all;width:8px!important}.mce-content-body .mce-item-anchor:not(:empty){background-position-x:2px;display:inline-block;padding-left:12px}.mce-content-body .mce-item-anchor[data-mce-selected]{outline-offset:1px}.tox-comments-visible .tox-comment[contenteditable=false]:not([data-mce-selected]),.tox-comments-visible span.tox-comment img:not([data-mce-selected]),.tox-comments-visible span.tox-comment span.mce-preview-object:not([data-mce-selected]),.tox-comments-visible span.tox-comment>audio:not([data-mce-selected]),.tox-comments-visible span.tox-comment>video:not([data-mce-selected]){outline:3px solid #ffe89d}.tox-comments-visible .tox-comment[contenteditable=false][data-mce-annotation-active=true]:not([data-mce-selected]){outline:3px solid #fed635}.tox-comments-visible span.tox-comment[data-mce-annotation-active=true] img:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true] span.mce-preview-object:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]>audio:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]>video:not([data-mce-selected]){outline:3px solid #fed635}.tox-comments-visible span.tox-comment:not([data-mce-selected]){background-color:#ffe89d;outline:0}.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]:not([data-mce-selected=inline-boundary]){background-color:#fed635}.tox-checklist>li:not(.tox-checklist--hidden){list-style:none;margin:.25em 0}.tox-checklist>li:not(.tox-checklist--hidden)::before{content:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-unchecked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2215%22%20height%3D%2215%22%20x%3D%22.5%22%20y%3D%22.5%22%20fill-rule%3D%22nonzero%22%20stroke%3D%22%234C4C4C%22%20rx%3D%222%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A\\\");cursor:pointer;height:1em;margin-left:-1.5em;margin-top:.125em;position:absolute;width:1em}.tox-checklist li:not(.tox-checklist--hidden).tox-checklist--checked::before{content:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-checked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2216%22%20height%3D%2216%22%20fill%3D%22%234099FF%22%20fill-rule%3D%22nonzero%22%20rx%3D%222%22%2F%3E%3Cpath%20id%3D%22Path%22%20fill%3D%22%23FFF%22%20fill-rule%3D%22nonzero%22%20d%3D%22M11.5703186%2C3.14417309%20C11.8516238%2C2.73724603%2012.4164781%2C2.62829933%2012.83558%2C2.89774797%20C13.260121%2C3.17069355%2013.3759736%2C3.72932262%2013.0909105%2C4.14168582%20L7.7580587%2C11.8560195%20C7.43776896%2C12.3193404%206.76483983%2C12.3852142%206.35607322%2C11.9948725%20L3.02491697%2C8.8138662%20C2.66090143%2C8.46625845%202.65798871%2C7.89594698%203.01850234%2C7.54483354%20C3.373942%2C7.19866177%203.94940006%2C7.19592841%204.30829608%2C7.5386474%20L6.85276923%2C9.9684299%20L11.5703186%2C3.14417309%20Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A\\\")}[dir=rtl] .tox-checklist>li:not(.tox-checklist--hidden)::before{margin-left:0;margin-right:-1.5em}code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;tab-size:4;-webkit-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.mce-content-body{overflow-wrap:break-word;word-wrap:break-word}.mce-content-body .mce-visual-caret{background-color:#000;background-color:currentColor;position:absolute}.mce-content-body .mce-visual-caret-hidden{display:none}.mce-content-body [data-mce-caret]{left:-1000px;margin:0;padding:0;position:absolute;right:auto;top:0}.mce-content-body .mce-offscreen-selection{left:-2000000px;max-width:1000000px;position:absolute}.mce-content-body [contentEditable=false]{cursor:default}.mce-content-body [contentEditable=true]{cursor:text}.tox-cursor-format-painter{cursor:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%0A%20%20%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M15%2C6%20C15%2C5.45%2014.55%2C5%2014%2C5%20L6%2C5%20C5.45%2C5%205%2C5.45%205%2C6%20L5%2C10%20C5%2C10.55%205.45%2C11%206%2C11%20L14%2C11%20C14.55%2C11%2015%2C10.55%2015%2C10%20L15%2C9%20L16%2C9%20L16%2C12%20L9%2C12%20L9%2C19%20C9%2C19.55%209.45%2C20%2010%2C20%20L11%2C20%20C11.55%2C20%2012%2C19.55%2012%2C19%20L12%2C14%20L18%2C14%20L18%2C7%20L15%2C7%20L15%2C6%20Z%22%2F%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M1%2C1%20L8.25%2C1%20C8.66421356%2C1%209%2C1.33578644%209%2C1.75%20L9%2C1.75%20C9%2C2.16421356%208.66421356%2C2.5%208.25%2C2.5%20L2.5%2C2.5%20L2.5%2C8.25%20C2.5%2C8.66421356%202.16421356%2C9%201.75%2C9%20L1.75%2C9%20C1.33578644%2C9%201%2C8.66421356%201%2C8.25%20L1%2C1%20Z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E%0A\\\"),default}div.mce-footnotes hr{margin-inline-end:auto;margin-inline-start:0;width:25%}div.mce-footnotes li>a.mce-footnotes-backlink{text-decoration:none}@media print{sup.mce-footnote a{color:#000;text-decoration:none}div.mce-footnotes{break-inside:avoid;width:100%}div.mce-footnotes li>a.mce-footnotes-backlink{display:none}}.mce-content-body figure.align-left{float:left}.mce-content-body figure.align-right{float:right}.mce-content-body figure.image.align-center{display:table;margin-left:auto;margin-right:auto}.mce-preview-object{border:1px solid gray;display:inline-block;line-height:0;margin:0 2px 0 2px;position:relative}.mce-preview-object .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.mce-preview-object[data-mce-selected=\\\"2\\\"] .mce-shim{display:none}.mce-content-body .mce-mergetag{cursor:default!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mce-content-body .mce-mergetag:hover{background-color:rgba(0,108,231,.1)}.mce-content-body .mce-mergetag-affix{background-color:rgba(0,108,231,.1);color:#006ce7}.mce-object{background:transparent url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M4%203h16a1%201%200%200%201%201%201v16a1%201%200%200%201-1%201H4a1%201%200%200%201-1-1V4a1%201%200%200%201%201-1zm1%202v14h14V5H5zm4.79%202.565l5.64%204.028a.5.5%200%200%201%200%20.814l-5.64%204.028a.5.5%200%200%201-.79-.407V7.972a.5.5%200%200%201%20.79-.407z%22%2F%3E%3C%2Fsvg%3E%0A\\\") no-repeat center;border:1px dashed #aaa}.mce-pagebreak{border:1px dashed #aaa;cursor:default;display:block;height:5px;margin-top:15px;page-break-before:always;width:100%}@media print{.mce-pagebreak{border:0}}.tiny-pageembed .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.tiny-pageembed[data-mce-selected=\\\"2\\\"] .mce-shim{display:none}.tiny-pageembed{display:inline-block;position:relative}.tiny-pageembed--16by9,.tiny-pageembed--1by1,.tiny-pageembed--21by9,.tiny-pageembed--4by3{display:block;overflow:hidden;padding:0;position:relative;width:100%}.tiny-pageembed--21by9{padding-top:42.857143%}.tiny-pageembed--16by9{padding-top:56.25%}.tiny-pageembed--4by3{padding-top:75%}.tiny-pageembed--1by1{padding-top:100%}.tiny-pageembed--16by9 iframe,.tiny-pageembed--1by1 iframe,.tiny-pageembed--21by9 iframe,.tiny-pageembed--4by3 iframe{border:0;height:100%;left:0;position:absolute;top:0;width:100%}.mce-content-body[data-mce-placeholder]{position:relative}.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before{color:rgba(34,47,62,.7);content:attr(data-mce-placeholder);position:absolute}.mce-content-body:not([dir=rtl])[data-mce-placeholder]:not(.mce-visualblocks)::before{left:1px}.mce-content-body[dir=rtl][data-mce-placeholder]:not(.mce-visualblocks)::before{right:1px}.mce-content-body div.mce-resizehandle{background-color:#4099ff;border-color:#4099ff;border-style:solid;border-width:1px;box-sizing:border-box;height:10px;position:absolute;width:10px;z-index:1298}.mce-content-body div.mce-resizehandle:hover{background-color:#4099ff}.mce-content-body div.mce-resizehandle:nth-of-type(1){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(2){cursor:nesw-resize}.mce-content-body div.mce-resizehandle:nth-of-type(3){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(4){cursor:nesw-resize}.mce-content-body .mce-resize-backdrop{z-index:10000}.mce-content-body .mce-clonedresizable{cursor:default;opacity:.5;outline:1px dashed #000;position:absolute;z-index:10001}.mce-content-body .mce-clonedresizable.mce-resizetable-columns td,.mce-content-body .mce-clonedresizable.mce-resizetable-columns th{border:0}.mce-content-body .mce-resize-helper{background:#555;background:rgba(0,0,0,.75);border:1px;border-radius:3px;color:#fff;display:none;font-family:sans-serif;font-size:12px;line-height:14px;margin:5px 10px;padding:5px;position:absolute;white-space:nowrap;z-index:10002}.tox-rtc-user-selection{position:relative}.tox-rtc-user-cursor{bottom:0;cursor:default;position:absolute;top:0;width:2px}.tox-rtc-user-cursor::before{background-color:inherit;border-radius:50%;content:'';display:block;height:8px;position:absolute;right:-3px;top:-3px;width:8px}.tox-rtc-user-cursor:hover::after{background-color:inherit;border-radius:100px;box-sizing:border-box;color:#fff;content:attr(data-user);display:block;font-size:12px;font-weight:700;left:-5px;min-height:8px;min-width:8px;padding:0 12px;position:absolute;top:-11px;white-space:nowrap;z-index:1000}.tox-rtc-user-selection--1 .tox-rtc-user-cursor{background-color:#2dc26b}.tox-rtc-user-selection--2 .tox-rtc-user-cursor{background-color:#e03e2d}.tox-rtc-user-selection--3 .tox-rtc-user-cursor{background-color:#f1c40f}.tox-rtc-user-selection--4 .tox-rtc-user-cursor{background-color:#3598db}.tox-rtc-user-selection--5 .tox-rtc-user-cursor{background-color:#b96ad9}.tox-rtc-user-selection--6 .tox-rtc-user-cursor{background-color:#e67e23}.tox-rtc-user-selection--7 .tox-rtc-user-cursor{background-color:#aaa69d}.tox-rtc-user-selection--8 .tox-rtc-user-cursor{background-color:#f368e0}.tox-rtc-remote-image{background:#eaeaea url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2236%22%20height%3D%2212%22%20viewBox%3D%220%200%2036%2012%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Ccircle%20cx%3D%226%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2218%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.33s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2230%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.66s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%3C%2Fsvg%3E%0A\\\") no-repeat center center;border:1px solid #ccc;min-height:240px;min-width:320px}.mce-match-marker{background:#aaa;color:#fff}.mce-match-marker-selected{background:#39f;color:#fff}.mce-match-marker-selected::-moz-selection{background:#39f;color:#fff}.mce-match-marker-selected::selection{background:#39f;color:#fff}.mce-content-body audio[data-mce-selected],.mce-content-body details[data-mce-selected],.mce-content-body embed[data-mce-selected],.mce-content-body img[data-mce-selected],.mce-content-body object[data-mce-selected],.mce-content-body table[data-mce-selected],.mce-content-body video[data-mce-selected]{outline:3px solid #b4d7ff}.mce-content-body hr[data-mce-selected]{outline:3px solid #b4d7ff;outline-offset:1px}.mce-content-body [contentEditable=false] [contentEditable=true]:focus{outline:3px solid #b4d7ff}.mce-content-body [contentEditable=false] [contentEditable=true]:hover{outline:3px solid #b4d7ff}.mce-content-body [contentEditable=false][data-mce-selected]{cursor:not-allowed;outline:3px solid #b4d7ff}.mce-content-body.mce-content-readonly [contentEditable=true]:focus,.mce-content-body.mce-content-readonly [contentEditable=true]:hover{outline:0}.mce-content-body [data-mce-selected=inline-boundary]{background-color:#b4d7ff}.mce-content-body .mce-edit-focus{outline:3px solid #b4d7ff}.mce-content-body td[data-mce-selected],.mce-content-body th[data-mce-selected]{position:relative}.mce-content-body td[data-mce-selected]::-moz-selection,.mce-content-body th[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body td[data-mce-selected]::selection,.mce-content-body th[data-mce-selected]::selection{background:0 0}.mce-content-body td[data-mce-selected] *,.mce-content-body th[data-mce-selected] *{outline:0;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{background-color:rgba(180,215,255,.7);border:1px solid rgba(180,215,255,.7);bottom:-1px;content:'';left:-1px;mix-blend-mode:multiply;position:absolute;right:-1px;top:-1px}@media screen and (-ms-high-contrast:active),(-ms-high-contrast:none){.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{border-color:rgba(0,84,180,.7)}}.mce-content-body img[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body img[data-mce-selected]::selection{background:0 0}.ephox-snooker-resizer-bar{background-color:#b4d7ff;opacity:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ephox-snooker-resizer-cols{cursor:col-resize}.ephox-snooker-resizer-rows{cursor:row-resize}.ephox-snooker-resizer-bar.ephox-snooker-resizer-bar-dragging{opacity:1}.mce-spellchecker-word{background-image:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%23ff0000'%20fill%3D'none'%20stroke-linecap%3D'round'%20stroke-opacity%3D'.75'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A\\\");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default;height:2rem}.mce-spellchecker-grammar{background-image:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%2300A835'%20fill%3D'none'%20stroke-linecap%3D'round'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A\\\");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc ul>li{list-style-type:none}[data-mce-block]{display:block}.mce-item-table:not([border]),.mce-item-table:not([border]) caption,.mce-item-table:not([border]) td,.mce-item-table:not([border]) th,.mce-item-table[border=\\\"0\\\"],.mce-item-table[border=\\\"0\\\"] caption,.mce-item-table[border=\\\"0\\\"] td,.mce-item-table[border=\\\"0\\\"] th,table[style*=\\\"border-width: 0px\\\"],table[style*=\\\"border-width: 0px\\\"] caption,table[style*=\\\"border-width: 0px\\\"] td,table[style*=\\\"border-width: 0px\\\"] th{border:1px dashed #bbb}.mce-visualblocks address,.mce-visualblocks article,.mce-visualblocks aside,.mce-visualblocks blockquote,.mce-visualblocks div:not([data-mce-bogus]),.mce-visualblocks dl,.mce-visualblocks figcaption,.mce-visualblocks figure,.mce-visualblocks h1,.mce-visualblocks h2,.mce-visualblocks h3,.mce-visualblocks h4,.mce-visualblocks h5,.mce-visualblocks h6,.mce-visualblocks hgroup,.mce-visualblocks ol,.mce-visualblocks p,.mce-visualblocks pre,.mce-visualblocks section,.mce-visualblocks ul{background-repeat:no-repeat;border:1px dashed #bbb;margin-left:3px;padding-top:10px}.mce-visualblocks p{background-image:url(data:image/gif;base64,R0lGODlhCQAJAJEAAAAAAP///7u7u////yH5BAEAAAMALAAAAAAJAAkAAAIQnG+CqCN/mlyvsRUpThG6AgA7)}.mce-visualblocks h1{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGu1JuxHoAfRNRW3TWXyF2YiRUAOw==)}.mce-visualblocks h2{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8Hybbx4oOuqgTynJd6bGlWg3DkJzoaUAAAOw==)}.mce-visualblocks h3{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIZjI8Hybbx4oOuqgTynJf2Ln2NOHpQpmhAAQA7)}.mce-visualblocks h4{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxInR0zqeAdhtJlXwV1oCll2HaWgAAOw==)}.mce-visualblocks h5{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjane4iq5GlW05GgIkIZUAAAOw==)}.mce-visualblocks h6{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjan04jep1iZ1XRlAo5bVgAAOw==)}.mce-visualblocks div:not([data-mce-bogus]){background-image:url(data:image/gif;base64,R0lGODlhEgAKAIABALu7u////yH5BAEAAAEALAAAAAASAAoAAAIfjI9poI0cgDywrhuxfbrzDEbQM2Ei5aRjmoySW4pAAQA7)}.mce-visualblocks section{background-image:url(data:image/gif;base64,R0lGODlhKAAKAIABALu7u////yH5BAEAAAEALAAAAAAoAAoAAAI5jI+pywcNY3sBWHdNrplytD2ellDeSVbp+GmWqaDqDMepc8t17Y4vBsK5hDyJMcI6KkuYU+jpjLoKADs=)}.mce-visualblocks article{background-image:url(data:image/gif;base64,R0lGODlhKgAKAIABALu7u////yH5BAEAAAEALAAAAAAqAAoAAAI6jI+pywkNY3wG0GBvrsd2tXGYSGnfiF7ikpXemTpOiJScasYoDJJrjsG9gkCJ0ag6KhmaIe3pjDYBBQA7)}.mce-visualblocks blockquote{background-image:url(data:image/gif;base64,R0lGODlhPgAKAIABALu7u////yH5BAEAAAEALAAAAAA+AAoAAAJPjI+py+0Knpz0xQDyuUhvfoGgIX5iSKZYgq5uNL5q69asZ8s5rrf0yZmpNkJZzFesBTu8TOlDVAabUyatguVhWduud3EyiUk45xhTTgMBBQA7)}.mce-visualblocks address{background-image:url(data:image/gif;base64,R0lGODlhLQAKAIABALu7u////yH5BAEAAAEALAAAAAAtAAoAAAI/jI+pywwNozSP1gDyyZcjb3UaRpXkWaXmZW4OqKLhBmLs+K263DkJK7OJeifh7FicKD9A1/IpGdKkyFpNmCkAADs=)}.mce-visualblocks pre{background-image:url(data:image/gif;base64,R0lGODlhFQAKAIABALu7uwAAACH5BAEAAAEALAAAAAAVAAoAAAIjjI+ZoN0cgDwSmnpz1NCueYERhnibZVKLNnbOq8IvKpJtVQAAOw==)}.mce-visualblocks figure{background-image:url(data:image/gif;base64,R0lGODlhJAAKAIAAALu7u////yH5BAEAAAEALAAAAAAkAAoAAAI0jI+py+2fwAHUSFvD3RlvG4HIp4nX5JFSpnZUJ6LlrM52OE7uSWosBHScgkSZj7dDKnWAAgA7)}.mce-visualblocks figcaption{border:1px dashed #bbb}.mce-visualblocks hgroup{background-image:url(data:image/gif;base64,R0lGODlhJwAKAIABALu7uwAAACH5BAEAAAEALAAAAAAnAAoAAAI3jI+pywYNI3uB0gpsRtt5fFnfNZaVSYJil4Wo03Hv6Z62uOCgiXH1kZIIJ8NiIxRrAZNMZAtQAAA7)}.mce-visualblocks aside{background-image:url(data:image/gif;base64,R0lGODlhHgAKAIABAKqqqv///yH5BAEAAAEALAAAAAAeAAoAAAItjI+pG8APjZOTzgtqy7I3f1yehmQcFY4WKZbqByutmW4aHUd6vfcVbgudgpYCADs=)}.mce-visualblocks ul{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIAAALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGuYnqUVSjvw26DzzXiqIDlVwAAOw==)}.mce-visualblocks ol{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybH6HHt0qourxC6CvzXieHyeWQAAOw==)}.mce-visualblocks dl{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybEOnmOvUoWznTqeuEjNSCqeGRUAOw==)}.mce-visualblocks:not([dir=rtl]) address,.mce-visualblocks:not([dir=rtl]) article,.mce-visualblocks:not([dir=rtl]) aside,.mce-visualblocks:not([dir=rtl]) blockquote,.mce-visualblocks:not([dir=rtl]) div:not([data-mce-bogus]),.mce-visualblocks:not([dir=rtl]) dl,.mce-visualblocks:not([dir=rtl]) figcaption,.mce-visualblocks:not([dir=rtl]) figure,.mce-visualblocks:not([dir=rtl]) h1,.mce-visualblocks:not([dir=rtl]) h2,.mce-visualblocks:not([dir=rtl]) h3,.mce-visualblocks:not([dir=rtl]) h4,.mce-visualblocks:not([dir=rtl]) h5,.mce-visualblocks:not([dir=rtl]) h6,.mce-visualblocks:not([dir=rtl]) hgroup,.mce-visualblocks:not([dir=rtl]) ol,.mce-visualblocks:not([dir=rtl]) p,.mce-visualblocks:not([dir=rtl]) pre,.mce-visualblocks:not([dir=rtl]) section,.mce-visualblocks:not([dir=rtl]) ul{margin-left:3px}.mce-visualblocks[dir=rtl] address,.mce-visualblocks[dir=rtl] article,.mce-visualblocks[dir=rtl] aside,.mce-visualblocks[dir=rtl] blockquote,.mce-visualblocks[dir=rtl] div:not([data-mce-bogus]),.mce-visualblocks[dir=rtl] dl,.mce-visualblocks[dir=rtl] figcaption,.mce-visualblocks[dir=rtl] figure,.mce-visualblocks[dir=rtl] h1,.mce-visualblocks[dir=rtl] h2,.mce-visualblocks[dir=rtl] h3,.mce-visualblocks[dir=rtl] h4,.mce-visualblocks[dir=rtl] h5,.mce-visualblocks[dir=rtl] h6,.mce-visualblocks[dir=rtl] hgroup,.mce-visualblocks[dir=rtl] ol,.mce-visualblocks[dir=rtl] p,.mce-visualblocks[dir=rtl] pre,.mce-visualblocks[dir=rtl] section,.mce-visualblocks[dir=rtl] ul{background-position-x:right;margin-right:3px}.mce-nbsp,.mce-shy{background:#aaa}.mce-shy::after{content:'-'}\")\n//# sourceMappingURL=content.inline.js.map\n"
  },
  {
    "path": "public/libs/tinymce/skins/ui/tinymce-5/content.js",
    "content": "tinymce.Resource.add('ui/tinymce-5/content.css', \".mce-content-body .mce-item-anchor{background:transparent url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'8'%20height%3D'12'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20d%3D'M0%200L8%200%208%2012%204.09117821%209%200%2012z'%2F%3E%3C%2Fsvg%3E%0A\\\") no-repeat center}.mce-content-body .mce-item-anchor:empty{cursor:default;display:inline-block;height:12px!important;padding:0 2px;-webkit-user-modify:read-only;-moz-user-modify:read-only;-webkit-user-select:all;-moz-user-select:all;user-select:all;width:8px!important}.mce-content-body .mce-item-anchor:not(:empty){background-position-x:2px;display:inline-block;padding-left:12px}.mce-content-body .mce-item-anchor[data-mce-selected]{outline-offset:1px}.tox-comments-visible .tox-comment[contenteditable=false]:not([data-mce-selected]),.tox-comments-visible span.tox-comment img:not([data-mce-selected]),.tox-comments-visible span.tox-comment span.mce-preview-object:not([data-mce-selected]),.tox-comments-visible span.tox-comment>audio:not([data-mce-selected]),.tox-comments-visible span.tox-comment>video:not([data-mce-selected]){outline:3px solid #ffe89d}.tox-comments-visible .tox-comment[contenteditable=false][data-mce-annotation-active=true]:not([data-mce-selected]){outline:3px solid #fed635}.tox-comments-visible span.tox-comment[data-mce-annotation-active=true] img:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true] span.mce-preview-object:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]>audio:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]>video:not([data-mce-selected]){outline:3px solid #fed635}.tox-comments-visible span.tox-comment:not([data-mce-selected]){background-color:#ffe89d;outline:0}.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]:not([data-mce-selected=inline-boundary]){background-color:#fed635}.tox-checklist>li:not(.tox-checklist--hidden){list-style:none;margin:.25em 0}.tox-checklist>li:not(.tox-checklist--hidden)::before{content:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-unchecked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2215%22%20height%3D%2215%22%20x%3D%22.5%22%20y%3D%22.5%22%20fill-rule%3D%22nonzero%22%20stroke%3D%22%234C4C4C%22%20rx%3D%222%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A\\\");cursor:pointer;height:1em;margin-left:-1.5em;margin-top:.125em;position:absolute;width:1em}.tox-checklist li:not(.tox-checklist--hidden).tox-checklist--checked::before{content:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-checked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2216%22%20height%3D%2216%22%20fill%3D%22%234099FF%22%20fill-rule%3D%22nonzero%22%20rx%3D%222%22%2F%3E%3Cpath%20id%3D%22Path%22%20fill%3D%22%23FFF%22%20fill-rule%3D%22nonzero%22%20d%3D%22M11.5703186%2C3.14417309%20C11.8516238%2C2.73724603%2012.4164781%2C2.62829933%2012.83558%2C2.89774797%20C13.260121%2C3.17069355%2013.3759736%2C3.72932262%2013.0909105%2C4.14168582%20L7.7580587%2C11.8560195%20C7.43776896%2C12.3193404%206.76483983%2C12.3852142%206.35607322%2C11.9948725%20L3.02491697%2C8.8138662%20C2.66090143%2C8.46625845%202.65798871%2C7.89594698%203.01850234%2C7.54483354%20C3.373942%2C7.19866177%203.94940006%2C7.19592841%204.30829608%2C7.5386474%20L6.85276923%2C9.9684299%20L11.5703186%2C3.14417309%20Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A\\\")}[dir=rtl] .tox-checklist>li:not(.tox-checklist--hidden)::before{margin-left:0;margin-right:-1.5em}code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;tab-size:4;-webkit-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.mce-content-body{overflow-wrap:break-word;word-wrap:break-word}.mce-content-body .mce-visual-caret{background-color:#000;background-color:currentColor;position:absolute}.mce-content-body .mce-visual-caret-hidden{display:none}.mce-content-body [data-mce-caret]{left:-1000px;margin:0;padding:0;position:absolute;right:auto;top:0}.mce-content-body .mce-offscreen-selection{left:-2000000px;max-width:1000000px;position:absolute}.mce-content-body [contentEditable=false]{cursor:default}.mce-content-body [contentEditable=true]{cursor:text}.tox-cursor-format-painter{cursor:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%0A%20%20%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M15%2C6%20C15%2C5.45%2014.55%2C5%2014%2C5%20L6%2C5%20C5.45%2C5%205%2C5.45%205%2C6%20L5%2C10%20C5%2C10.55%205.45%2C11%206%2C11%20L14%2C11%20C14.55%2C11%2015%2C10.55%2015%2C10%20L15%2C9%20L16%2C9%20L16%2C12%20L9%2C12%20L9%2C19%20C9%2C19.55%209.45%2C20%2010%2C20%20L11%2C20%20C11.55%2C20%2012%2C19.55%2012%2C19%20L12%2C14%20L18%2C14%20L18%2C7%20L15%2C7%20L15%2C6%20Z%22%2F%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M1%2C1%20L8.25%2C1%20C8.66421356%2C1%209%2C1.33578644%209%2C1.75%20L9%2C1.75%20C9%2C2.16421356%208.66421356%2C2.5%208.25%2C2.5%20L2.5%2C2.5%20L2.5%2C8.25%20C2.5%2C8.66421356%202.16421356%2C9%201.75%2C9%20L1.75%2C9%20C1.33578644%2C9%201%2C8.66421356%201%2C8.25%20L1%2C1%20Z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E%0A\\\"),default}div.mce-footnotes hr{margin-inline-end:auto;margin-inline-start:0;width:25%}div.mce-footnotes li>a.mce-footnotes-backlink{text-decoration:none}@media print{sup.mce-footnote a{color:#000;text-decoration:none}div.mce-footnotes{break-inside:avoid;width:100%}div.mce-footnotes li>a.mce-footnotes-backlink{display:none}}.mce-content-body figure.align-left{float:left}.mce-content-body figure.align-right{float:right}.mce-content-body figure.image.align-center{display:table;margin-left:auto;margin-right:auto}.mce-preview-object{border:1px solid gray;display:inline-block;line-height:0;margin:0 2px 0 2px;position:relative}.mce-preview-object .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.mce-preview-object[data-mce-selected=\\\"2\\\"] .mce-shim{display:none}.mce-content-body .mce-mergetag{cursor:default!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mce-content-body .mce-mergetag:hover{background-color:rgba(0,108,231,.1)}.mce-content-body .mce-mergetag-affix{background-color:rgba(0,108,231,.1);color:#006ce7}.mce-object{background:transparent url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M4%203h16a1%201%200%200%201%201%201v16a1%201%200%200%201-1%201H4a1%201%200%200%201-1-1V4a1%201%200%200%201%201-1zm1%202v14h14V5H5zm4.79%202.565l5.64%204.028a.5.5%200%200%201%200%20.814l-5.64%204.028a.5.5%200%200%201-.79-.407V7.972a.5.5%200%200%201%20.79-.407z%22%2F%3E%3C%2Fsvg%3E%0A\\\") no-repeat center;border:1px dashed #aaa}.mce-pagebreak{border:1px dashed #aaa;cursor:default;display:block;height:5px;margin-top:15px;page-break-before:always;width:100%}@media print{.mce-pagebreak{border:0}}.tiny-pageembed .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.tiny-pageembed[data-mce-selected=\\\"2\\\"] .mce-shim{display:none}.tiny-pageembed{display:inline-block;position:relative}.tiny-pageembed--16by9,.tiny-pageembed--1by1,.tiny-pageembed--21by9,.tiny-pageembed--4by3{display:block;overflow:hidden;padding:0;position:relative;width:100%}.tiny-pageembed--21by9{padding-top:42.857143%}.tiny-pageembed--16by9{padding-top:56.25%}.tiny-pageembed--4by3{padding-top:75%}.tiny-pageembed--1by1{padding-top:100%}.tiny-pageembed--16by9 iframe,.tiny-pageembed--1by1 iframe,.tiny-pageembed--21by9 iframe,.tiny-pageembed--4by3 iframe{border:0;height:100%;left:0;position:absolute;top:0;width:100%}.mce-content-body[data-mce-placeholder]{position:relative}.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before{color:rgba(34,47,62,.7);content:attr(data-mce-placeholder);position:absolute}.mce-content-body:not([dir=rtl])[data-mce-placeholder]:not(.mce-visualblocks)::before{left:1px}.mce-content-body[dir=rtl][data-mce-placeholder]:not(.mce-visualblocks)::before{right:1px}.mce-content-body div.mce-resizehandle{background-color:#4099ff;border-color:#4099ff;border-style:solid;border-width:1px;box-sizing:border-box;height:10px;position:absolute;width:10px;z-index:1298}.mce-content-body div.mce-resizehandle:hover{background-color:#4099ff}.mce-content-body div.mce-resizehandle:nth-of-type(1){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(2){cursor:nesw-resize}.mce-content-body div.mce-resizehandle:nth-of-type(3){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(4){cursor:nesw-resize}.mce-content-body .mce-resize-backdrop{z-index:10000}.mce-content-body .mce-clonedresizable{cursor:default;opacity:.5;outline:1px dashed #000;position:absolute;z-index:10001}.mce-content-body .mce-clonedresizable.mce-resizetable-columns td,.mce-content-body .mce-clonedresizable.mce-resizetable-columns th{border:0}.mce-content-body .mce-resize-helper{background:#555;background:rgba(0,0,0,.75);border:1px;border-radius:3px;color:#fff;display:none;font-family:sans-serif;font-size:12px;line-height:14px;margin:5px 10px;padding:5px;position:absolute;white-space:nowrap;z-index:10002}.tox-rtc-user-selection{position:relative}.tox-rtc-user-cursor{bottom:0;cursor:default;position:absolute;top:0;width:2px}.tox-rtc-user-cursor::before{background-color:inherit;border-radius:50%;content:'';display:block;height:8px;position:absolute;right:-3px;top:-3px;width:8px}.tox-rtc-user-cursor:hover::after{background-color:inherit;border-radius:100px;box-sizing:border-box;color:#fff;content:attr(data-user);display:block;font-size:12px;font-weight:700;left:-5px;min-height:8px;min-width:8px;padding:0 12px;position:absolute;top:-11px;white-space:nowrap;z-index:1000}.tox-rtc-user-selection--1 .tox-rtc-user-cursor{background-color:#2dc26b}.tox-rtc-user-selection--2 .tox-rtc-user-cursor{background-color:#e03e2d}.tox-rtc-user-selection--3 .tox-rtc-user-cursor{background-color:#f1c40f}.tox-rtc-user-selection--4 .tox-rtc-user-cursor{background-color:#3598db}.tox-rtc-user-selection--5 .tox-rtc-user-cursor{background-color:#b96ad9}.tox-rtc-user-selection--6 .tox-rtc-user-cursor{background-color:#e67e23}.tox-rtc-user-selection--7 .tox-rtc-user-cursor{background-color:#aaa69d}.tox-rtc-user-selection--8 .tox-rtc-user-cursor{background-color:#f368e0}.tox-rtc-remote-image{background:#eaeaea url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2236%22%20height%3D%2212%22%20viewBox%3D%220%200%2036%2012%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Ccircle%20cx%3D%226%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2218%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.33s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2230%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.66s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%3C%2Fsvg%3E%0A\\\") no-repeat center center;border:1px solid #ccc;min-height:240px;min-width:320px}.mce-match-marker{background:#aaa;color:#fff}.mce-match-marker-selected{background:#39f;color:#fff}.mce-match-marker-selected::-moz-selection{background:#39f;color:#fff}.mce-match-marker-selected::selection{background:#39f;color:#fff}.mce-content-body audio[data-mce-selected],.mce-content-body details[data-mce-selected],.mce-content-body embed[data-mce-selected],.mce-content-body img[data-mce-selected],.mce-content-body object[data-mce-selected],.mce-content-body table[data-mce-selected],.mce-content-body video[data-mce-selected]{outline:3px solid #b4d7ff}.mce-content-body hr[data-mce-selected]{outline:3px solid #b4d7ff;outline-offset:1px}.mce-content-body [contentEditable=false] [contentEditable=true]:focus{outline:3px solid #b4d7ff}.mce-content-body [contentEditable=false] [contentEditable=true]:hover{outline:3px solid #b4d7ff}.mce-content-body [contentEditable=false][data-mce-selected]{cursor:not-allowed;outline:3px solid #b4d7ff}.mce-content-body.mce-content-readonly [contentEditable=true]:focus,.mce-content-body.mce-content-readonly [contentEditable=true]:hover{outline:0}.mce-content-body [data-mce-selected=inline-boundary]{background-color:#b4d7ff}.mce-content-body .mce-edit-focus{outline:3px solid #b4d7ff}.mce-content-body td[data-mce-selected],.mce-content-body th[data-mce-selected]{position:relative}.mce-content-body td[data-mce-selected]::-moz-selection,.mce-content-body th[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body td[data-mce-selected]::selection,.mce-content-body th[data-mce-selected]::selection{background:0 0}.mce-content-body td[data-mce-selected] *,.mce-content-body th[data-mce-selected] *{outline:0;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{background-color:rgba(180,215,255,.7);border:1px solid rgba(180,215,255,.7);bottom:-1px;content:'';left:-1px;mix-blend-mode:multiply;position:absolute;right:-1px;top:-1px}@media screen and (-ms-high-contrast:active),(-ms-high-contrast:none){.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{border-color:rgba(0,84,180,.7)}}.mce-content-body img[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body img[data-mce-selected]::selection{background:0 0}.ephox-snooker-resizer-bar{background-color:#b4d7ff;opacity:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ephox-snooker-resizer-cols{cursor:col-resize}.ephox-snooker-resizer-rows{cursor:row-resize}.ephox-snooker-resizer-bar.ephox-snooker-resizer-bar-dragging{opacity:1}.mce-spellchecker-word{background-image:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%23ff0000'%20fill%3D'none'%20stroke-linecap%3D'round'%20stroke-opacity%3D'.75'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A\\\");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default;height:2rem}.mce-spellchecker-grammar{background-image:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%2300A835'%20fill%3D'none'%20stroke-linecap%3D'round'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A\\\");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc ul>li{list-style-type:none}[data-mce-block]{display:block}.mce-item-table:not([border]),.mce-item-table:not([border]) caption,.mce-item-table:not([border]) td,.mce-item-table:not([border]) th,.mce-item-table[border=\\\"0\\\"],.mce-item-table[border=\\\"0\\\"] caption,.mce-item-table[border=\\\"0\\\"] td,.mce-item-table[border=\\\"0\\\"] th,table[style*=\\\"border-width: 0px\\\"],table[style*=\\\"border-width: 0px\\\"] caption,table[style*=\\\"border-width: 0px\\\"] td,table[style*=\\\"border-width: 0px\\\"] th{border:1px dashed #bbb}.mce-visualblocks address,.mce-visualblocks article,.mce-visualblocks aside,.mce-visualblocks blockquote,.mce-visualblocks div:not([data-mce-bogus]),.mce-visualblocks dl,.mce-visualblocks figcaption,.mce-visualblocks figure,.mce-visualblocks h1,.mce-visualblocks h2,.mce-visualblocks h3,.mce-visualblocks h4,.mce-visualblocks h5,.mce-visualblocks h6,.mce-visualblocks hgroup,.mce-visualblocks ol,.mce-visualblocks p,.mce-visualblocks pre,.mce-visualblocks section,.mce-visualblocks ul{background-repeat:no-repeat;border:1px dashed #bbb;margin-left:3px;padding-top:10px}.mce-visualblocks p{background-image:url(data:image/gif;base64,R0lGODlhCQAJAJEAAAAAAP///7u7u////yH5BAEAAAMALAAAAAAJAAkAAAIQnG+CqCN/mlyvsRUpThG6AgA7)}.mce-visualblocks h1{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGu1JuxHoAfRNRW3TWXyF2YiRUAOw==)}.mce-visualblocks h2{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8Hybbx4oOuqgTynJd6bGlWg3DkJzoaUAAAOw==)}.mce-visualblocks h3{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIZjI8Hybbx4oOuqgTynJf2Ln2NOHpQpmhAAQA7)}.mce-visualblocks h4{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxInR0zqeAdhtJlXwV1oCll2HaWgAAOw==)}.mce-visualblocks h5{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjane4iq5GlW05GgIkIZUAAAOw==)}.mce-visualblocks h6{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjan04jep1iZ1XRlAo5bVgAAOw==)}.mce-visualblocks div:not([data-mce-bogus]){background-image:url(data:image/gif;base64,R0lGODlhEgAKAIABALu7u////yH5BAEAAAEALAAAAAASAAoAAAIfjI9poI0cgDywrhuxfbrzDEbQM2Ei5aRjmoySW4pAAQA7)}.mce-visualblocks section{background-image:url(data:image/gif;base64,R0lGODlhKAAKAIABALu7u////yH5BAEAAAEALAAAAAAoAAoAAAI5jI+pywcNY3sBWHdNrplytD2ellDeSVbp+GmWqaDqDMepc8t17Y4vBsK5hDyJMcI6KkuYU+jpjLoKADs=)}.mce-visualblocks article{background-image:url(data:image/gif;base64,R0lGODlhKgAKAIABALu7u////yH5BAEAAAEALAAAAAAqAAoAAAI6jI+pywkNY3wG0GBvrsd2tXGYSGnfiF7ikpXemTpOiJScasYoDJJrjsG9gkCJ0ag6KhmaIe3pjDYBBQA7)}.mce-visualblocks blockquote{background-image:url(data:image/gif;base64,R0lGODlhPgAKAIABALu7u////yH5BAEAAAEALAAAAAA+AAoAAAJPjI+py+0Knpz0xQDyuUhvfoGgIX5iSKZYgq5uNL5q69asZ8s5rrf0yZmpNkJZzFesBTu8TOlDVAabUyatguVhWduud3EyiUk45xhTTgMBBQA7)}.mce-visualblocks address{background-image:url(data:image/gif;base64,R0lGODlhLQAKAIABALu7u////yH5BAEAAAEALAAAAAAtAAoAAAI/jI+pywwNozSP1gDyyZcjb3UaRpXkWaXmZW4OqKLhBmLs+K263DkJK7OJeifh7FicKD9A1/IpGdKkyFpNmCkAADs=)}.mce-visualblocks pre{background-image:url(data:image/gif;base64,R0lGODlhFQAKAIABALu7uwAAACH5BAEAAAEALAAAAAAVAAoAAAIjjI+ZoN0cgDwSmnpz1NCueYERhnibZVKLNnbOq8IvKpJtVQAAOw==)}.mce-visualblocks figure{background-image:url(data:image/gif;base64,R0lGODlhJAAKAIAAALu7u////yH5BAEAAAEALAAAAAAkAAoAAAI0jI+py+2fwAHUSFvD3RlvG4HIp4nX5JFSpnZUJ6LlrM52OE7uSWosBHScgkSZj7dDKnWAAgA7)}.mce-visualblocks figcaption{border:1px dashed #bbb}.mce-visualblocks hgroup{background-image:url(data:image/gif;base64,R0lGODlhJwAKAIABALu7uwAAACH5BAEAAAEALAAAAAAnAAoAAAI3jI+pywYNI3uB0gpsRtt5fFnfNZaVSYJil4Wo03Hv6Z62uOCgiXH1kZIIJ8NiIxRrAZNMZAtQAAA7)}.mce-visualblocks aside{background-image:url(data:image/gif;base64,R0lGODlhHgAKAIABAKqqqv///yH5BAEAAAEALAAAAAAeAAoAAAItjI+pG8APjZOTzgtqy7I3f1yehmQcFY4WKZbqByutmW4aHUd6vfcVbgudgpYCADs=)}.mce-visualblocks ul{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIAAALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGuYnqUVSjvw26DzzXiqIDlVwAAOw==)}.mce-visualblocks ol{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybH6HHt0qourxC6CvzXieHyeWQAAOw==)}.mce-visualblocks dl{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybEOnmOvUoWznTqeuEjNSCqeGRUAOw==)}.mce-visualblocks:not([dir=rtl]) address,.mce-visualblocks:not([dir=rtl]) article,.mce-visualblocks:not([dir=rtl]) aside,.mce-visualblocks:not([dir=rtl]) blockquote,.mce-visualblocks:not([dir=rtl]) div:not([data-mce-bogus]),.mce-visualblocks:not([dir=rtl]) dl,.mce-visualblocks:not([dir=rtl]) figcaption,.mce-visualblocks:not([dir=rtl]) figure,.mce-visualblocks:not([dir=rtl]) h1,.mce-visualblocks:not([dir=rtl]) h2,.mce-visualblocks:not([dir=rtl]) h3,.mce-visualblocks:not([dir=rtl]) h4,.mce-visualblocks:not([dir=rtl]) h5,.mce-visualblocks:not([dir=rtl]) h6,.mce-visualblocks:not([dir=rtl]) hgroup,.mce-visualblocks:not([dir=rtl]) ol,.mce-visualblocks:not([dir=rtl]) p,.mce-visualblocks:not([dir=rtl]) pre,.mce-visualblocks:not([dir=rtl]) section,.mce-visualblocks:not([dir=rtl]) ul{margin-left:3px}.mce-visualblocks[dir=rtl] address,.mce-visualblocks[dir=rtl] article,.mce-visualblocks[dir=rtl] aside,.mce-visualblocks[dir=rtl] blockquote,.mce-visualblocks[dir=rtl] div:not([data-mce-bogus]),.mce-visualblocks[dir=rtl] dl,.mce-visualblocks[dir=rtl] figcaption,.mce-visualblocks[dir=rtl] figure,.mce-visualblocks[dir=rtl] h1,.mce-visualblocks[dir=rtl] h2,.mce-visualblocks[dir=rtl] h3,.mce-visualblocks[dir=rtl] h4,.mce-visualblocks[dir=rtl] h5,.mce-visualblocks[dir=rtl] h6,.mce-visualblocks[dir=rtl] hgroup,.mce-visualblocks[dir=rtl] ol,.mce-visualblocks[dir=rtl] p,.mce-visualblocks[dir=rtl] pre,.mce-visualblocks[dir=rtl] section,.mce-visualblocks[dir=rtl] ul{background-position-x:right;margin-right:3px}.mce-nbsp,.mce-shy{background:#aaa}.mce-shy::after{content:'-'}body{font-family:sans-serif}table{border-collapse:collapse}\")\n//# sourceMappingURL=content.js.map\n"
  },
  {
    "path": "public/libs/tinymce/skins/ui/tinymce-5/skin.js",
    "content": "tinymce.Resource.add('ui/tinymce-5/skin.css', \".tox{box-shadow:none;box-sizing:content-box;color:#222f3e;cursor:auto;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:16px;font-style:normal;font-weight:400;line-height:normal;-webkit-tap-highlight-color:transparent;text-decoration:none;text-shadow:none;text-transform:none;vertical-align:initial;white-space:normal}.tox :not(svg):not(rect){box-sizing:inherit;color:inherit;cursor:inherit;direction:inherit;font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;line-height:inherit;-webkit-tap-highlight-color:inherit;text-align:inherit;text-decoration:inherit;text-shadow:inherit;text-transform:inherit;vertical-align:inherit;white-space:inherit}.tox :not(svg):not(rect){background:0 0;border:0;box-shadow:none;float:none;height:auto;margin:0;max-width:none;outline:0;padding:0;position:static;width:auto}.tox:not([dir=rtl]){direction:ltr;text-align:left}.tox[dir=rtl]{direction:rtl;text-align:right}.tox-tinymce{border:1px solid #ccc;border-radius:0;box-shadow:none;box-sizing:border-box;display:flex;flex-direction:column;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;overflow:hidden;position:relative;visibility:inherit!important}.tox.tox-tinymce-inline{border:none;box-shadow:none;overflow:initial}.tox.tox-tinymce-inline .tox-editor-container{overflow:initial}.tox.tox-tinymce-inline .tox-editor-header{background-color:#fff;border:1px solid #ccc;border-radius:0;box-shadow:none;overflow:hidden}.tox-tinymce-aux{font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;z-index:1300}.tox-tinymce :focus,.tox-tinymce-aux :focus{outline:0}button::-moz-focus-inner{border:0}.tox[dir=rtl] .tox-icon--flip svg{transform:rotateY(180deg)}.tox .accessibility-issue__header{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description{align-items:stretch;border-radius:3px;display:flex;justify-content:space-between}.tox .accessibility-issue__description>div{padding-bottom:4px}.tox .accessibility-issue__description>div>div{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description>div>div .tox-icon svg{display:block}.tox .accessibility-issue__repair{margin-top:16px}.tox .tox-dialog__body-content .accessibility-issue--info .accessibility-issue__description{background-color:rgba(30,113,170,.1);color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--info .tox-form__group h2{color:#207ab7}.tox .tox-dialog__body-content .accessibility-issue--info .tox-icon svg{fill:#207ab7}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon{background-color:#207ab7;color:#fff}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:hover{background-color:#1c6ca1}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:active{background-color:#185d8c}.tox .tox-dialog__body-content .accessibility-issue--warn .accessibility-issue__description{background-color:rgba(255,165,0,.08);color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-form__group h2{color:#8f5d00}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-icon svg{fill:#8f5d00}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon{background-color:#ffe89d;color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:hover{background-color:#f2d574;color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:active{background-color:#e8c657;color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--error .accessibility-issue__description{background-color:rgba(204,0,0,.1);color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--error .tox-form__group h2{color:#c00}.tox .tox-dialog__body-content .accessibility-issue--error .tox-icon svg{fill:#c00}.tox .tox-dialog__body-content .accessibility-issue--error a.tox-button--naked.tox-button--icon{background-color:#f2bfbf;color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--error a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--error a.tox-button--naked.tox-button--icon:hover{background-color:#e9a4a4;color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--error a.tox-button--naked.tox-button--icon:active{background-color:#ee9494;color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--success .accessibility-issue__description{background-color:rgba(120,171,70,.1);color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--success .accessibility-issue__description>:last-child{display:none}.tox .tox-dialog__body-content .accessibility-issue--success .tox-form__group h2{color:#527530}.tox .tox-dialog__body-content .accessibility-issue--success .tox-icon svg{fill:#527530}.tox .tox-dialog__body-content .accessibility-issue__header .tox-form__group h1,.tox .tox-dialog__body-content .tox-form__group .accessibility-issue__description h2{font-size:14px;margin-top:0}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__header .tox-button{margin-left:4px}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__header>:nth-last-child(2){margin-left:auto}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__description{padding:4px 4px 4px 8px}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__header .tox-button{margin-right:4px}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__header>:nth-last-child(2){margin-right:auto}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__description{padding:4px 8px 4px 4px}.tox .tox-advtemplate .tox-form__grid{flex:1}.tox .tox-advtemplate .tox-form__grid>div:first-child{display:flex;flex-direction:column;width:30%}.tox .tox-advtemplate .tox-form__grid>div:first-child>div:nth-child(2){flex-basis:0;flex-grow:1;overflow:auto}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-advtemplate .tox-form__grid>div:first-child{width:100%}}.tox .tox-advtemplate iframe{border-color:#ccc;border-radius:0;border-style:solid;border-width:1px;margin:0 10px}.tox .tox-anchorbar{display:flex;flex:0 0 auto}.tox .tox-bottom-anchorbar{display:flex;flex:0 0 auto}.tox .tox-bar{display:flex;flex:0 0 auto}.tox .tox-button{background-color:#207ab7;background-image:none;background-position:0 0;background-repeat:repeat;border-color:#207ab7;border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;color:#fff;cursor:pointer;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:14px;font-style:normal;font-weight:700;letter-spacing:normal;line-height:24px;margin:0;outline:0;padding:4px 16px;position:relative;text-align:center;text-decoration:none;text-transform:none;white-space:nowrap}.tox .tox-button::before{border-radius:3px;bottom:-1px;box-shadow:inset 0 0 0 2px #fff,0 0 0 1px #207ab7,0 0 0 3px rgba(32,122,183,.25);content:'';left:-1px;opacity:0;pointer-events:none;position:absolute;right:-1px;top:-1px}.tox .tox-button[disabled]{background-color:#207ab7;background-image:none;border-color:#207ab7;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-button:focus:not(:disabled){background-color:#1c6ca1;background-image:none;border-color:#1c6ca1;box-shadow:none;color:#fff}.tox .tox-button:focus-visible:not(:disabled)::before{opacity:1}.tox .tox-button:hover:not(:disabled){background-color:#1c6ca1;background-image:none;border-color:#1c6ca1;box-shadow:none;color:#fff}.tox .tox-button:active:not(:disabled){background-color:#185d8c;background-image:none;border-color:#185d8c;box-shadow:none;color:#fff}.tox .tox-button.tox-button--enabled{background-color:#185d8c;background-image:none;border-color:#185d8c;box-shadow:none;color:#fff}.tox .tox-button.tox-button--enabled[disabled]{background-color:#185d8c;background-image:none;border-color:#185d8c;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-button.tox-button--enabled:focus:not(:disabled){background-color:#154f76;background-image:none;border-color:#154f76;box-shadow:none;color:#fff}.tox .tox-button.tox-button--enabled:hover:not(:disabled){background-color:#154f76;background-image:none;border-color:#154f76;box-shadow:none;color:#fff}.tox .tox-button.tox-button--enabled:active:not(:disabled){background-color:#114060;background-image:none;border-color:#114060;box-shadow:none;color:#fff}.tox .tox-button--icon-and-text,.tox .tox-button.tox-button--icon-and-text,.tox .tox-button.tox-button--secondary.tox-button--icon-and-text{display:flex;padding:5px 4px}.tox .tox-button--icon-and-text .tox-icon svg,.tox .tox-button.tox-button--icon-and-text .tox-icon svg,.tox .tox-button.tox-button--secondary.tox-button--icon-and-text .tox-icon svg{display:block;fill:currentColor}.tox .tox-button--secondary{background-color:#f0f0f0;background-image:none;background-position:0 0;background-repeat:repeat;border-color:#f0f0f0;border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;color:#222f3e;font-size:14px;font-style:normal;font-weight:700;letter-spacing:normal;outline:0;padding:4px 16px;text-decoration:none;text-transform:none}.tox .tox-button--secondary[disabled]{background-color:#f0f0f0;background-image:none;border-color:#f0f0f0;box-shadow:none;color:rgba(34,47,62,.5)}.tox .tox-button--secondary:focus:not(:disabled){background-color:#e3e3e3;background-image:none;border-color:#e3e3e3;box-shadow:none;color:#222f3e}.tox .tox-button--secondary:hover:not(:disabled){background-color:#e3e3e3;background-image:none;border-color:#e3e3e3;box-shadow:none;color:#222f3e}.tox .tox-button--secondary:active:not(:disabled){background-color:#d6d6d6;background-image:none;border-color:#d6d6d6;box-shadow:none;color:#222f3e}.tox .tox-button--secondary.tox-button--enabled{background-color:#b1ccdf;background-image:none;border-color:#b1ccdf;box-shadow:none;color:#222f3e}.tox .tox-button--secondary.tox-button--enabled[disabled]{background-color:#b1ccdf;background-image:none;border-color:#b1ccdf;box-shadow:none;color:rgba(34,47,62,.5)}.tox .tox-button--secondary.tox-button--enabled:focus:not(:disabled){background-color:#9fc1d7;background-image:none;border-color:#9fc1d7;box-shadow:none;color:#222f3e}.tox .tox-button--secondary.tox-button--enabled:hover:not(:disabled){background-color:#9fc1d7;background-image:none;border-color:#9fc1d7;box-shadow:none;color:#222f3e}.tox .tox-button--secondary.tox-button--enabled:active:not(:disabled){background-color:#8db5d0;background-image:none;border-color:#8db5d0;box-shadow:none;color:#222f3e}.tox .tox-button--icon,.tox .tox-button.tox-button--icon,.tox .tox-button.tox-button--secondary.tox-button--icon{padding:4px}.tox .tox-button--icon .tox-icon svg,.tox .tox-button.tox-button--icon .tox-icon svg,.tox .tox-button.tox-button--secondary.tox-button--icon .tox-icon svg{display:block;fill:currentColor}.tox .tox-button-link{background:0;border:none;box-sizing:border-box;cursor:pointer;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:16px;font-weight:400;line-height:1.3;margin:0;padding:0;white-space:nowrap}.tox .tox-button-link--sm{font-size:14px}.tox .tox-button--naked{background-color:transparent;border-color:transparent;box-shadow:unset;color:#222f3e}.tox .tox-button--naked[disabled]{background-color:#f0f0f0;border-color:#f0f0f0;box-shadow:none;color:rgba(34,47,62,.5)}.tox .tox-button--naked:hover:not(:disabled){background-color:#e3e3e3;border-color:#e3e3e3;box-shadow:none;color:#222f3e}.tox .tox-button--naked:focus:not(:disabled){background-color:#e3e3e3;border-color:#e3e3e3;box-shadow:none;color:#222f3e}.tox .tox-button--naked:active:not(:disabled){background-color:#d6d6d6;border-color:#d6d6d6;box-shadow:none;color:#222f3e}.tox .tox-button--naked .tox-icon svg{fill:currentColor}.tox .tox-button--naked.tox-button--icon:hover:not(:disabled){color:#222f3e}.tox .tox-checkbox{align-items:center;border-radius:3px;cursor:pointer;display:flex;height:36px;min-width:36px}.tox .tox-checkbox__input{height:1px;overflow:hidden;position:absolute;top:auto;width:1px}.tox .tox-checkbox__icons{align-items:center;border-radius:3px;box-shadow:0 0 0 2px transparent;box-sizing:content-box;display:flex;height:24px;justify-content:center;padding:calc(4px - 1px);width:24px}.tox .tox-checkbox__icons .tox-checkbox-icon__unchecked svg{display:block;fill:rgba(34,47,62,.3)}.tox .tox-checkbox__icons .tox-checkbox-icon__indeterminate svg{display:none;fill:#207ab7}.tox .tox-checkbox__icons .tox-checkbox-icon__checked svg{display:none;fill:#207ab7}.tox .tox-checkbox--disabled{color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-checkbox--disabled .tox-checkbox__icons .tox-checkbox-icon__checked svg{fill:rgba(34,47,62,.5)}.tox .tox-checkbox--disabled .tox-checkbox__icons .tox-checkbox-icon__unchecked svg{fill:rgba(34,47,62,.5)}.tox .tox-checkbox--disabled .tox-checkbox__icons .tox-checkbox-icon__indeterminate svg{fill:rgba(34,47,62,.5)}.tox input.tox-checkbox__input:checked+.tox-checkbox__icons .tox-checkbox-icon__unchecked svg{display:none}.tox input.tox-checkbox__input:checked+.tox-checkbox__icons .tox-checkbox-icon__checked svg{display:block}.tox input.tox-checkbox__input:indeterminate+.tox-checkbox__icons .tox-checkbox-icon__unchecked svg{display:none}.tox input.tox-checkbox__input:indeterminate+.tox-checkbox__icons .tox-checkbox-icon__indeterminate svg{display:block}.tox input.tox-checkbox__input:focus+.tox-checkbox__icons{border-radius:3px;box-shadow:inset 0 0 0 1px #207ab7;padding:calc(4px - 1px)}.tox:not([dir=rtl]) .tox-checkbox__label{margin-left:4px}.tox:not([dir=rtl]) .tox-checkbox__input{left:-10000px}.tox:not([dir=rtl]) .tox-bar .tox-checkbox{margin-left:4px}.tox[dir=rtl] .tox-checkbox__label{margin-right:4px}.tox[dir=rtl] .tox-checkbox__input{right:-10000px}.tox[dir=rtl] .tox-bar .tox-checkbox{margin-right:4px}.tox .tox-collection--toolbar .tox-collection__group{display:flex;padding:0}.tox .tox-collection--grid .tox-collection__group{display:flex;flex-wrap:wrap;max-height:208px;overflow-x:hidden;overflow-y:auto;padding:0}.tox .tox-collection--list .tox-collection__group{border-bottom-width:0;border-color:#ccc;border-left-width:0;border-right-width:0;border-style:solid;border-top-width:1px;padding:4px 0}.tox .tox-collection--list .tox-collection__group:first-child{border-top-width:0}.tox .tox-collection__group-heading{background-color:#e6e6e6;color:rgba(34,47,62,.7);cursor:default;font-size:12px;font-style:normal;font-weight:400;margin-bottom:4px;margin-top:-4px;padding:4px 8px;text-transform:none;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.tox .tox-collection__item{align-items:center;border-radius:3px;color:#222f3e;display:flex;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.tox .tox-collection--list .tox-collection__item{padding:4px 8px}.tox .tox-collection--toolbar .tox-collection__item{border-radius:3px;padding:4px}.tox .tox-collection--grid .tox-collection__item{border-radius:3px;padding:4px}.tox .tox-collection--list .tox-collection__item--enabled{background-color:#fff;color:#222f3e}.tox .tox-collection--list .tox-collection__item--active{background-color:#dee0e2}.tox .tox-collection--toolbar .tox-collection__item--enabled{background-color:#c8cbcf;color:#222f3e}.tox .tox-collection--toolbar .tox-collection__item--active{background-color:#dee0e2}.tox .tox-collection--grid .tox-collection__item--enabled{background-color:#c8cbcf;color:#222f3e}.tox .tox-collection--grid .tox-collection__item--active:not(.tox-collection__item--state-disabled){background-color:#dee0e2;color:#222f3e}.tox .tox-collection--list .tox-collection__item--active:not(.tox-collection__item--state-disabled){color:#222f3e}.tox .tox-collection--toolbar .tox-collection__item--active:not(.tox-collection__item--state-disabled){color:#222f3e}.tox .tox-collection__item-checkmark,.tox .tox-collection__item-icon{align-items:center;display:flex;height:24px;justify-content:center;width:24px}.tox .tox-collection__item-checkmark svg,.tox .tox-collection__item-icon svg{fill:currentColor}.tox .tox-collection--toolbar-lg .tox-collection__item-icon{height:48px;width:48px}.tox .tox-collection__item-label{color:currentColor;display:inline-block;flex:1;font-size:14px;font-style:normal;font-weight:400;line-height:24px;max-width:100%;text-transform:none;word-break:break-all}.tox .tox-collection__item-accessory{color:rgba(34,47,62,.7);display:inline-block;font-size:14px;height:24px;line-height:24px;text-transform:none}.tox .tox-collection__item-caret{align-items:center;display:flex;min-height:24px}.tox .tox-collection__item-caret::after{content:'';font-size:0;min-height:inherit}.tox .tox-collection__item-caret svg{fill:#222f3e}.tox .tox-collection__item--state-disabled{background-color:transparent;color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-collection__item--state-disabled .tox-collection__item-caret svg{fill:rgba(34,47,62,.5)}.tox .tox-collection--list .tox-collection__item:not(.tox-collection__item--enabled) .tox-collection__item-checkmark svg{display:none}.tox .tox-collection--list .tox-collection__item:not(.tox-collection__item--enabled) .tox-collection__item-accessory+.tox-collection__item-checkmark{display:none}.tox .tox-collection--horizontal{background-color:#fff;border:1px solid #ccc;border-radius:3px;box-shadow:0 0 2px 0 rgba(34,47,62,.2),0 4px 8px 0 rgba(34,47,62,.15);display:flex;flex:0 0 auto;flex-shrink:0;flex-wrap:nowrap;margin-bottom:0;overflow-x:auto;padding:0}.tox .tox-collection--horizontal .tox-collection__group{align-items:center;display:flex;flex-wrap:nowrap;margin:0;padding:0 4px}.tox .tox-collection--horizontal .tox-collection__item{height:34px;margin:3px 0 2px 0;padding:0 4px}.tox .tox-collection--horizontal .tox-collection__item-label{white-space:nowrap}.tox .tox-collection--horizontal .tox-collection__item-caret{margin-left:4px}.tox .tox-collection__item-container{display:flex}.tox .tox-collection__item-container--row{align-items:center;flex:1 1 auto;flex-direction:row}.tox .tox-collection__item-container--row.tox-collection__item-container--align-left{margin-right:auto}.tox .tox-collection__item-container--row.tox-collection__item-container--align-right{justify-content:flex-end;margin-left:auto}.tox .tox-collection__item-container--row.tox-collection__item-container--valign-top{align-items:flex-start;margin-bottom:auto}.tox .tox-collection__item-container--row.tox-collection__item-container--valign-middle{align-items:center}.tox .tox-collection__item-container--row.tox-collection__item-container--valign-bottom{align-items:flex-end;margin-top:auto}.tox .tox-collection__item-container--column{align-self:center;flex:1 1 auto;flex-direction:column}.tox .tox-collection__item-container--column.tox-collection__item-container--align-left{align-items:flex-start}.tox .tox-collection__item-container--column.tox-collection__item-container--align-right{align-items:flex-end}.tox .tox-collection__item-container--column.tox-collection__item-container--valign-top{align-self:flex-start}.tox .tox-collection__item-container--column.tox-collection__item-container--valign-middle{align-self:center}.tox .tox-collection__item-container--column.tox-collection__item-container--valign-bottom{align-self:flex-end}.tox:not([dir=rtl]) .tox-collection--horizontal .tox-collection__group:not(:last-of-type){border-right:1px solid #ccc}.tox:not([dir=rtl]) .tox-collection--list .tox-collection__item>:not(:first-child){margin-left:8px}.tox:not([dir=rtl]) .tox-collection--list .tox-collection__item>.tox-collection__item-label:first-child{margin-left:4px}.tox:not([dir=rtl]) .tox-collection__item-accessory{margin-left:16px;text-align:right}.tox:not([dir=rtl]) .tox-collection .tox-collection__item-caret{margin-left:16px}.tox[dir=rtl] .tox-collection--horizontal .tox-collection__group:not(:last-of-type){border-left:1px solid #ccc}.tox[dir=rtl] .tox-collection--list .tox-collection__item>:not(:first-child){margin-right:8px}.tox[dir=rtl] .tox-collection--list .tox-collection__item>.tox-collection__item-label:first-child{margin-right:4px}.tox[dir=rtl] .tox-collection__item-accessory{margin-right:16px;text-align:left}.tox[dir=rtl] .tox-collection .tox-collection__item-caret{margin-right:16px;transform:rotateY(180deg)}.tox[dir=rtl] .tox-collection--horizontal .tox-collection__item-caret{margin-right:4px}.tox .tox-color-picker-container{display:flex;flex-direction:row;height:225px;margin:0}.tox .tox-sv-palette{box-sizing:border-box;display:flex;height:100%}.tox .tox-sv-palette-spectrum{height:100%}.tox .tox-sv-palette,.tox .tox-sv-palette-spectrum{width:225px}.tox .tox-sv-palette-thumb{background:0 0;border:1px solid #000;border-radius:50%;box-sizing:content-box;height:12px;position:absolute;width:12px}.tox .tox-sv-palette-inner-thumb{border:1px solid #fff;border-radius:50%;height:10px;position:absolute;width:10px}.tox .tox-hue-slider{box-sizing:border-box;height:100%;width:25px}.tox .tox-hue-slider-spectrum{background:linear-gradient(to bottom,red,#ff0080,#f0f,#8000ff,#00f,#0080ff,#0ff,#00ff80,#0f0,#80ff00,#ff0,#ff8000,red);height:100%;width:100%}.tox .tox-hue-slider,.tox .tox-hue-slider-spectrum{width:20px}.tox .tox-hue-slider-spectrum:focus,.tox .tox-sv-palette-spectrum:focus{outline:#08f solid}.tox .tox-hue-slider-thumb{background:#fff;border:1px solid #000;box-sizing:content-box;height:4px;width:100%}.tox .tox-rgb-form{display:flex;flex-direction:column;justify-content:space-between}.tox .tox-rgb-form div{align-items:center;display:flex;justify-content:space-between;margin-bottom:5px;width:inherit}.tox .tox-rgb-form input{width:6em}.tox .tox-rgb-form input.tox-invalid{border:1px solid red!important}.tox .tox-rgb-form .tox-rgba-preview{border:1px solid #000;flex-grow:2;margin-bottom:0}.tox:not([dir=rtl]) .tox-sv-palette{margin-right:15px}.tox:not([dir=rtl]) .tox-hue-slider{margin-right:15px}.tox:not([dir=rtl]) .tox-hue-slider-thumb{margin-left:-1px}.tox:not([dir=rtl]) .tox-rgb-form label{margin-right:.5em}.tox[dir=rtl] .tox-sv-palette{margin-left:15px}.tox[dir=rtl] .tox-hue-slider{margin-left:15px}.tox[dir=rtl] .tox-hue-slider-thumb{margin-right:-1px}.tox[dir=rtl] .tox-rgb-form label{margin-left:.5em}.tox .tox-toolbar .tox-swatches,.tox .tox-toolbar__overflow .tox-swatches,.tox .tox-toolbar__primary .tox-swatches{margin:2px 0 3px 4px}.tox .tox-collection--list .tox-collection__group .tox-swatches-menu{border:0;margin:-4px 0}.tox .tox-swatches__row{display:flex}.tox .tox-swatch{height:30px;transition:transform .15s,box-shadow .15s;width:30px}.tox .tox-swatch:focus,.tox .tox-swatch:hover{box-shadow:0 0 0 1px rgba(127,127,127,.3) inset;transform:scale(.8)}.tox .tox-swatch--remove{align-items:center;display:flex;justify-content:center}.tox .tox-swatch--remove svg path{stroke:#e74c3c}.tox .tox-swatches__picker-btn{align-items:center;background-color:transparent;border:0;cursor:pointer;display:flex;height:30px;justify-content:center;outline:0;padding:0;width:30px}.tox .tox-swatches__picker-btn svg{fill:#222f3e;height:24px;width:24px}.tox .tox-swatches__picker-btn:hover{background:#dee0e2}.tox div.tox-swatch:not(.tox-swatch--remove) svg{display:none;fill:#222f3e;height:24px;margin:calc((30px - 24px)/ 2) calc((30px - 24px)/ 2);width:24px}.tox div.tox-swatch:not(.tox-swatch--remove) svg path{fill:#fff;paint-order:stroke;stroke:#222f3e;stroke-width:2px}.tox div.tox-swatch:not(.tox-swatch--remove).tox-collection__item--enabled svg{display:block}.tox:not([dir=rtl]) .tox-swatches__picker-btn{margin-left:auto}.tox[dir=rtl] .tox-swatches__picker-btn{margin-right:auto}.tox .tox-comment-thread{background:#fff;position:relative}.tox .tox-comment-thread>:not(:first-child){margin-top:8px}.tox .tox-comment{background:#fff;border:1px solid #ccc;border-radius:3px;box-shadow:0 4px 8px 0 rgba(34,47,62,.1);padding:8px 8px 16px 8px;position:relative}.tox .tox-comment__header{align-items:center;color:#222f3e;display:flex;justify-content:space-between}.tox .tox-comment__date{color:#222f3e;font-size:12px;line-height:18px}.tox .tox-comment__body{color:#222f3e;font-size:14px;font-style:normal;font-weight:400;line-height:1.3;margin-top:8px;position:relative;text-transform:initial}.tox .tox-comment__body textarea{resize:none;white-space:normal;width:100%}.tox .tox-comment__expander{padding-top:8px}.tox .tox-comment__expander p{color:rgba(34,47,62,.7);font-size:14px;font-style:normal}.tox .tox-comment__body p{margin:0}.tox .tox-comment__buttonspacing{padding-top:16px;text-align:center}.tox .tox-comment-thread__overlay::after{background:#fff;bottom:0;content:\\\"\\\";display:flex;left:0;opacity:.9;position:absolute;right:0;top:0;z-index:5}.tox .tox-comment__reply{display:flex;flex-shrink:0;flex-wrap:wrap;justify-content:flex-end;margin-top:8px}.tox .tox-comment__reply>:first-child{margin-bottom:8px;width:100%}.tox .tox-comment__edit{display:flex;flex-wrap:wrap;justify-content:flex-end;margin-top:16px}.tox .tox-comment__gradient::after{background:linear-gradient(rgba(255,255,255,0),#fff);bottom:0;content:\\\"\\\";display:block;height:5em;margin-top:-40px;position:absolute;width:100%}.tox .tox-comment__overlay{background:#fff;bottom:0;display:flex;flex-direction:column;flex-grow:1;left:0;opacity:.9;position:absolute;right:0;text-align:center;top:0;z-index:5}.tox .tox-comment__loading-text{align-items:center;color:#222f3e;display:flex;flex-direction:column;position:relative}.tox .tox-comment__loading-text>div{padding-bottom:16px}.tox .tox-comment__overlaytext{bottom:0;flex-direction:column;font-size:14px;left:0;padding:1em;position:absolute;right:0;top:0;z-index:10}.tox .tox-comment__overlaytext p{background-color:#fff;box-shadow:0 0 8px 8px #fff;color:#222f3e;text-align:center}.tox .tox-comment__overlaytext div:nth-of-type(2){font-size:.8em}.tox .tox-comment__busy-spinner{align-items:center;background-color:#fff;bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0;z-index:20}.tox .tox-comment__scroll{display:flex;flex-direction:column;flex-shrink:1;overflow:auto}.tox .tox-conversations{margin:8px}.tox:not([dir=rtl]) .tox-comment__edit{margin-left:8px}.tox:not([dir=rtl]) .tox-comment__buttonspacing>:last-child,.tox:not([dir=rtl]) .tox-comment__edit>:last-child,.tox:not([dir=rtl]) .tox-comment__reply>:last-child{margin-left:8px}.tox[dir=rtl] .tox-comment__edit{margin-right:8px}.tox[dir=rtl] .tox-comment__buttonspacing>:last-child,.tox[dir=rtl] .tox-comment__edit>:last-child,.tox[dir=rtl] .tox-comment__reply>:last-child{margin-right:8px}.tox .tox-user{align-items:center;display:flex}.tox .tox-user__avatar svg{fill:rgba(34,47,62,.7)}.tox .tox-user__avatar img{border-radius:50%;height:36px;object-fit:cover;vertical-align:middle;width:36px}.tox .tox-user__name{color:#222f3e;font-size:14px;font-style:normal;font-weight:700;line-height:18px;text-transform:none}.tox:not([dir=rtl]) .tox-user__avatar img,.tox:not([dir=rtl]) .tox-user__avatar svg{margin-right:8px}.tox:not([dir=rtl]) .tox-user__avatar+.tox-user__name{margin-left:8px}.tox[dir=rtl] .tox-user__avatar img,.tox[dir=rtl] .tox-user__avatar svg{margin-left:8px}.tox[dir=rtl] .tox-user__avatar+.tox-user__name{margin-right:8px}.tox .tox-dialog-wrap{align-items:center;bottom:0;display:flex;justify-content:center;left:0;position:fixed;right:0;top:0;z-index:1100}.tox .tox-dialog-wrap__backdrop{background-color:rgba(255,255,255,.75);bottom:0;left:0;position:absolute;right:0;top:0;z-index:1}.tox .tox-dialog-wrap__backdrop--opaque{background-color:#fff}.tox .tox-dialog{background-color:#fff;border-color:#ccc;border-radius:3px;border-style:solid;border-width:1px;box-shadow:0 16px 16px -10px rgba(34,47,62,.15),0 0 40px 1px rgba(34,47,62,.15);display:flex;flex-direction:column;max-height:100%;max-width:480px;overflow:hidden;position:relative;width:95vw;z-index:2}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-dialog{align-self:flex-start;margin:8px auto;max-height:calc(100vh - 8px * 2);width:calc(100vw - 16px)}}.tox .tox-dialog-inline{z-index:1100}.tox .tox-dialog__header{align-items:center;background-color:#fff;border-bottom:none;color:#222f3e;display:flex;font-size:16px;justify-content:space-between;padding:8px 16px 0 16px;position:relative}.tox .tox-dialog__header .tox-button{z-index:1}.tox .tox-dialog__draghandle{cursor:grab;height:100%;left:0;position:absolute;top:0;width:100%}.tox .tox-dialog__draghandle:active{cursor:grabbing}.tox .tox-dialog__dismiss{margin-left:auto}.tox .tox-dialog__title{font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:20px;font-style:normal;font-weight:400;line-height:1.3;margin:0;text-transform:none}.tox .tox-dialog__body{color:#222f3e;display:flex;flex:1;font-size:16px;font-style:normal;font-weight:400;line-height:1.3;min-width:0;text-align:left;text-transform:none}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-dialog__body{flex-direction:column}}.tox .tox-dialog__body-nav{align-items:flex-start;display:flex;flex-direction:column;flex-shrink:0;padding:16px 16px}@media only screen and (min-width:768px){.tox .tox-dialog__body-nav{max-width:11em}}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-dialog__body-nav{flex-direction:row;-webkit-overflow-scrolling:touch;overflow-x:auto;padding-bottom:0}}.tox .tox-dialog__body-nav-item{border-bottom:2px solid transparent;color:rgba(34,47,62,.7);display:inline-block;flex-shrink:0;font-size:14px;line-height:1.3;margin-bottom:8px;max-width:13em;text-decoration:none}.tox .tox-dialog__body-nav-item:focus{background-color:rgba(32,122,183,.1)}.tox .tox-dialog__body-nav-item--active{border-bottom:2px solid #207ab7;color:#207ab7}.tox .tox-dialog__body-content{box-sizing:border-box;display:flex;flex:1;flex-direction:column;max-height:min(650px,calc(100vh - 110px));overflow:auto;-webkit-overflow-scrolling:touch;padding:16px 16px}.tox .tox-dialog__body-content>*{margin-bottom:0;margin-top:16px}.tox .tox-dialog__body-content>:first-child{margin-top:0}.tox .tox-dialog__body-content>:last-child{margin-bottom:0}.tox .tox-dialog__body-content>:only-child{margin-bottom:0;margin-top:0}.tox .tox-dialog__body-content a{color:#207ab7;cursor:pointer;text-decoration:underline}.tox .tox-dialog__body-content a:focus,.tox .tox-dialog__body-content a:hover{color:#114060;text-decoration:underline}.tox .tox-dialog__body-content a:focus-visible{border-radius:1px;outline:2px solid #207ab7;outline-offset:2px}.tox .tox-dialog__body-content a:active{color:#092335;text-decoration:underline}.tox .tox-dialog__body-content svg{fill:#222f3e}.tox .tox-dialog__body-content strong{font-weight:700}.tox .tox-dialog__body-content ul{list-style-type:disc}.tox .tox-dialog__body-content dd,.tox .tox-dialog__body-content ol,.tox .tox-dialog__body-content ul{padding-inline-start:2.5rem}.tox .tox-dialog__body-content dl,.tox .tox-dialog__body-content ol,.tox .tox-dialog__body-content ul{margin-bottom:16px}.tox .tox-dialog__body-content dd,.tox .tox-dialog__body-content dl,.tox .tox-dialog__body-content dt,.tox .tox-dialog__body-content ol,.tox .tox-dialog__body-content ul{display:block;margin-inline-end:0;margin-inline-start:0}.tox .tox-dialog__body-content .tox-form__group h1{color:#222f3e;font-size:20px;font-style:normal;font-weight:700;letter-spacing:normal;margin-bottom:16px;margin-top:2rem;text-transform:none}.tox .tox-dialog__body-content .tox-form__group h2{color:#222f3e;font-size:16px;font-style:normal;font-weight:700;letter-spacing:normal;margin-bottom:16px;margin-top:2rem;text-transform:none}.tox .tox-dialog__body-content .tox-form__group p{margin-bottom:16px}.tox .tox-dialog__body-content .tox-form__group h1:first-child,.tox .tox-dialog__body-content .tox-form__group h2:first-child,.tox .tox-dialog__body-content .tox-form__group p:first-child{margin-top:0}.tox .tox-dialog__body-content .tox-form__group h1:last-child,.tox .tox-dialog__body-content .tox-form__group h2:last-child,.tox .tox-dialog__body-content .tox-form__group p:last-child{margin-bottom:0}.tox .tox-dialog__body-content .tox-form__group h1:only-child,.tox .tox-dialog__body-content .tox-form__group h2:only-child,.tox .tox-dialog__body-content .tox-form__group p:only-child{margin-bottom:0;margin-top:0}.tox .tox-dialog__body-content .tox-form__group .tox-label.tox-label--center{text-align:center}.tox .tox-dialog__body-content .tox-form__group .tox-label.tox-label--end{text-align:end}.tox .tox-dialog--width-lg{height:650px;max-width:1200px}.tox .tox-dialog--fullscreen{height:100%;max-width:100%}.tox .tox-dialog--fullscreen .tox-dialog__body-content{max-height:100%}.tox .tox-dialog--width-md{max-width:800px}.tox .tox-dialog--width-md .tox-dialog__body-content{overflow:auto}.tox .tox-dialog__body-content--centered{text-align:center}.tox .tox-dialog__footer{align-items:center;background-color:#fff;border-top:1px solid #ccc;display:flex;justify-content:space-between;padding:8px 16px}.tox .tox-dialog__footer-end,.tox .tox-dialog__footer-start{display:flex}.tox .tox-dialog__busy-spinner{align-items:center;background-color:rgba(255,255,255,.75);bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0;z-index:3}.tox .tox-dialog__table{border-collapse:collapse;width:100%}.tox .tox-dialog__table thead th{font-weight:700;padding-bottom:8px}.tox .tox-dialog__table thead th:first-child{padding-right:8px}.tox .tox-dialog__table tbody tr{border-bottom:1px solid #404040}.tox .tox-dialog__table tbody tr:last-child{border-bottom:none}.tox .tox-dialog__table td{padding-bottom:8px;padding-top:8px}.tox .tox-dialog__table td:first-child{padding-right:8px}.tox .tox-dialog__iframe{min-height:200px}.tox .tox-dialog__iframe.tox-dialog__iframe--opaque{background:#fff}.tox .tox-navobj-bordered{position:relative}.tox .tox-navobj-bordered::before{border:1px solid #ccc;border-radius:3px;content:'';inset:0;opacity:1;pointer-events:none;position:absolute;z-index:1}.tox .tox-navobj-bordered-focus.tox-navobj-bordered::before{border-color:#207ab7;box-shadow:none;outline:2px solid rgba(32,122,183,.25)}.tox .tox-dialog__popups{position:absolute;width:100%;z-index:1100}.tox .tox-dialog__body-iframe{display:flex;flex:1;flex-direction:column}.tox .tox-dialog__body-iframe .tox-navobj{display:flex;flex:1}.tox .tox-dialog__body-iframe .tox-navobj :nth-child(2){flex:1;height:100%}.tox .tox-dialog-dock-fadeout{opacity:0;visibility:hidden}.tox .tox-dialog-dock-fadein{opacity:1;visibility:visible}.tox .tox-dialog-dock-transition{transition:visibility 0s linear .3s,opacity .3s ease}.tox .tox-dialog-dock-transition.tox-dialog-dock-fadein{transition-delay:0s}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox:not([dir=rtl]) .tox-dialog__body-nav{margin-right:0}}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox:not([dir=rtl]) .tox-dialog__body-nav-item:not(:first-child){margin-left:8px}}.tox:not([dir=rtl]) .tox-dialog__footer .tox-dialog__footer-end>*,.tox:not([dir=rtl]) .tox-dialog__footer .tox-dialog__footer-start>*{margin-left:8px}.tox[dir=rtl] .tox-dialog__body{text-align:right}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox[dir=rtl] .tox-dialog__body-nav{margin-left:0}}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox[dir=rtl] .tox-dialog__body-nav-item:not(:first-child){margin-right:8px}}.tox[dir=rtl] .tox-dialog__footer .tox-dialog__footer-end>*,.tox[dir=rtl] .tox-dialog__footer .tox-dialog__footer-start>*{margin-right:8px}body.tox-dialog__disable-scroll{overflow:hidden}.tox .tox-dropzone-container{display:flex;flex:1}.tox .tox-dropzone{align-items:center;background:#fff;border:2px dashed #ccc;box-sizing:border-box;display:flex;flex-direction:column;flex-grow:1;justify-content:center;min-height:100px;padding:10px}.tox .tox-dropzone p{color:rgba(34,47,62,.7);margin:0 0 16px 0}.tox .tox-edit-area{display:flex;flex:1;overflow:hidden;position:relative}.tox .tox-edit-area::before{border:2px solid #2d6adf;border-radius:4px;content:'';inset:0;opacity:0;pointer-events:none;position:absolute;transition:opacity .15s;z-index:1}.tox .tox-edit-area__iframe{background-color:#fff;border:0;box-sizing:border-box;flex:1;height:100%;position:absolute;width:100%}.tox.tox-edit-focus .tox-edit-area::before{opacity:1}.tox.tox-inline-edit-area{border:1px dotted #ccc}.tox .tox-editor-container{display:flex;flex:1 1 auto;flex-direction:column;overflow:hidden}.tox .tox-editor-header{display:grid;grid-template-columns:1fr min-content;z-index:2}.tox:not(.tox-tinymce-inline) .tox-editor-header{background-color:#fff;border-bottom:none;box-shadow:none;padding:4px 0}.tox:not(.tox-tinymce-inline) .tox-editor-header:not(.tox-editor-dock-transition){transition:box-shadow .5s}.tox:not(.tox-tinymce-inline).tox-tinymce--toolbar-bottom .tox-editor-header{border-top:1px solid #ccc;box-shadow:none}.tox:not(.tox-tinymce-inline).tox-tinymce--toolbar-sticky-on .tox-editor-header{background-color:#fff;box-shadow:0 4px 4px -3px rgba(0,0,0,.25);padding:4px 0}.tox:not(.tox-tinymce-inline).tox-tinymce--toolbar-sticky-on.tox-tinymce--toolbar-bottom .tox-editor-header{box-shadow:0 4px 4px -3px rgba(0,0,0,.25)}.tox.tox:not(.tox-tinymce-inline) .tox-editor-header.tox-editor-header--empty{background:0 0;border:none;box-shadow:none;padding:0}.tox-editor-dock-fadeout{opacity:0;visibility:hidden}.tox-editor-dock-fadein{opacity:1;visibility:visible}.tox-editor-dock-transition{transition:visibility 0s linear .25s,opacity .25s ease}.tox-editor-dock-transition.tox-editor-dock-fadein{transition-delay:0s}.tox .tox-control-wrap{flex:1;position:relative}.tox .tox-control-wrap:not(.tox-control-wrap--status-invalid) .tox-control-wrap__status-icon-invalid,.tox .tox-control-wrap:not(.tox-control-wrap--status-unknown) .tox-control-wrap__status-icon-unknown,.tox .tox-control-wrap:not(.tox-control-wrap--status-valid) .tox-control-wrap__status-icon-valid{display:none}.tox .tox-control-wrap svg{display:block}.tox .tox-control-wrap__status-icon-wrap{position:absolute;top:50%;transform:translateY(-50%)}.tox .tox-control-wrap__status-icon-invalid svg{fill:#c00}.tox .tox-control-wrap__status-icon-unknown svg{fill:orange}.tox .tox-control-wrap__status-icon-valid svg{fill:green}.tox:not([dir=rtl]) .tox-control-wrap--status-invalid .tox-textfield,.tox:not([dir=rtl]) .tox-control-wrap--status-unknown .tox-textfield,.tox:not([dir=rtl]) .tox-control-wrap--status-valid .tox-textfield{padding-right:32px}.tox:not([dir=rtl]) .tox-control-wrap__status-icon-wrap{right:4px}.tox[dir=rtl] .tox-control-wrap--status-invalid .tox-textfield,.tox[dir=rtl] .tox-control-wrap--status-unknown .tox-textfield,.tox[dir=rtl] .tox-control-wrap--status-valid .tox-textfield{padding-left:32px}.tox[dir=rtl] .tox-control-wrap__status-icon-wrap{left:4px}.tox .tox-autocompleter{max-width:25em}.tox .tox-autocompleter .tox-menu{box-sizing:border-box;max-width:25em}.tox .tox-autocompleter .tox-autocompleter-highlight{font-weight:700}.tox .tox-color-input{display:flex;position:relative;z-index:1}.tox .tox-color-input .tox-textfield{z-index:-1}.tox .tox-color-input span{border-color:rgba(34,47,62,.2);border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;height:24px;position:absolute;top:6px;width:24px}.tox .tox-color-input span:focus:not([aria-disabled=true]),.tox .tox-color-input span:hover:not([aria-disabled=true]){border-color:#207ab7;cursor:pointer}.tox .tox-color-input span::before{background-image:linear-gradient(45deg,rgba(0,0,0,.25) 25%,transparent 25%),linear-gradient(-45deg,rgba(0,0,0,.25) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,rgba(0,0,0,.25) 75%),linear-gradient(-45deg,transparent 75%,rgba(0,0,0,.25) 75%);background-position:0 0,0 6px,6px -6px,-6px 0;background-size:12px 12px;border:1px solid #fff;border-radius:3px;box-sizing:border-box;content:'';height:24px;left:-1px;position:absolute;top:-1px;width:24px;z-index:-1}.tox .tox-color-input span[aria-disabled=true]{cursor:not-allowed}.tox:not([dir=rtl]) .tox-color-input .tox-textfield{padding-left:36px}.tox:not([dir=rtl]) .tox-color-input span{left:6px}.tox[dir=rtl] .tox-color-input .tox-textfield{padding-right:36px}.tox[dir=rtl] .tox-color-input span{right:6px}.tox .tox-label,.tox .tox-toolbar-label{color:rgba(34,47,62,.7);display:block;font-size:14px;font-style:normal;font-weight:400;line-height:1.3;padding:0 8px 0 0;text-transform:none;white-space:nowrap}.tox .tox-toolbar-label{padding:0 8px}.tox[dir=rtl] .tox-label{padding:0 0 0 8px}.tox .tox-form{display:flex;flex:1;flex-direction:column}.tox .tox-form__group{box-sizing:border-box;margin-bottom:4px}.tox .tox-form-group--maximize{flex:1}.tox .tox-form__group--error{color:#c00}.tox .tox-form__group--collection{display:flex}.tox .tox-form__grid{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between}.tox .tox-form__grid--2col>.tox-form__group{width:calc(50% - (8px / 2))}.tox .tox-form__grid--3col>.tox-form__group{width:calc(100% / 3 - (8px / 2))}.tox .tox-form__grid--4col>.tox-form__group{width:calc(25% - (8px / 2))}.tox .tox-form__controls-h-stack{align-items:center;display:flex}.tox .tox-form__group--inline{align-items:center;display:flex}.tox .tox-form__group--stretched{display:flex;flex:1;flex-direction:column}.tox .tox-form__group--stretched .tox-textarea{flex:1}.tox .tox-form__group--stretched .tox-navobj{display:flex;flex:1}.tox .tox-form__group--stretched .tox-navobj :nth-child(2){flex:1;height:100%}.tox:not([dir=rtl]) .tox-form__controls-h-stack>:not(:first-child){margin-left:4px}.tox[dir=rtl] .tox-form__controls-h-stack>:not(:first-child){margin-right:4px}.tox .tox-lock.tox-locked .tox-lock-icon__unlock,.tox .tox-lock:not(.tox-locked) .tox-lock-icon__lock{display:none}.tox .tox-listboxfield .tox-listbox--select,.tox .tox-textarea,.tox .tox-textarea-wrap .tox-textarea:focus,.tox .tox-textfield,.tox .tox-toolbar-textfield{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#ccc;border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;color:#222f3e;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:16px;line-height:24px;margin:0;min-height:34px;outline:0;padding:5px 4.75px;resize:none;width:100%}.tox .tox-textarea[disabled],.tox .tox-textfield[disabled]{background-color:#f2f2f2;color:rgba(34,47,62,.85);cursor:not-allowed}.tox .tox-custom-editor:focus-within,.tox .tox-listboxfield .tox-listbox--select:focus,.tox .tox-textarea-wrap:focus-within,.tox .tox-textarea:focus,.tox .tox-textfield:focus{background-color:#fff;border-color:#207ab7;box-shadow:none;outline:2px solid rgba(32,122,183,.25)}.tox .tox-toolbar-textfield{border-width:0;margin-bottom:3px;margin-top:2px;max-width:250px}.tox .tox-naked-btn{background-color:transparent;border:0;border-color:transparent;box-shadow:unset;color:#207ab7;cursor:pointer;display:block;margin:0;padding:0}.tox .tox-naked-btn svg{display:block;fill:#222f3e}.tox:not([dir=rtl]) .tox-toolbar-textfield+*{margin-left:4px}.tox[dir=rtl] .tox-toolbar-textfield+*{margin-right:4px}.tox .tox-listboxfield{cursor:pointer;position:relative}.tox .tox-listboxfield .tox-listbox--select[disabled]{background-color:#f2f2f2;color:rgba(34,47,62,.85);cursor:not-allowed}.tox .tox-listbox__select-label{cursor:default;flex:1;margin:0 4px}.tox .tox-listbox__select-chevron{align-items:center;display:flex;justify-content:center;width:16px}.tox .tox-listbox__select-chevron svg{fill:#222f3e}.tox .tox-listboxfield .tox-listbox--select{align-items:center;display:flex}.tox:not([dir=rtl]) .tox-listboxfield svg{right:8px}.tox[dir=rtl] .tox-listboxfield svg{left:8px}.tox .tox-selectfield{cursor:pointer;position:relative}.tox .tox-selectfield select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#ccc;border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;color:#222f3e;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:16px;line-height:24px;margin:0;min-height:34px;outline:0;padding:5px 4.75px;resize:none;width:100%}.tox .tox-selectfield select[disabled]{background-color:#f2f2f2;color:rgba(34,47,62,.85);cursor:not-allowed}.tox .tox-selectfield select::-ms-expand{display:none}.tox .tox-selectfield select:focus{background-color:#fff;border-color:#207ab7;box-shadow:none;outline:2px solid rgba(32,122,183,.25)}.tox .tox-selectfield svg{pointer-events:none;position:absolute;top:50%;transform:translateY(-50%)}.tox:not([dir=rtl]) .tox-selectfield select[size=\\\"0\\\"],.tox:not([dir=rtl]) .tox-selectfield select[size=\\\"1\\\"]{padding-right:24px}.tox:not([dir=rtl]) .tox-selectfield svg{right:8px}.tox[dir=rtl] .tox-selectfield select[size=\\\"0\\\"],.tox[dir=rtl] .tox-selectfield select[size=\\\"1\\\"]{padding-left:24px}.tox[dir=rtl] .tox-selectfield svg{left:8px}.tox .tox-textarea-wrap{border-color:#ccc;border-radius:3px;border-style:solid;border-width:1px;display:flex;flex:1;overflow:hidden}.tox .tox-textarea{-webkit-appearance:textarea;-moz-appearance:textarea;appearance:textarea;white-space:pre-wrap}.tox .tox-textarea-wrap .tox-textarea{border:none}.tox .tox-textarea-wrap .tox-textarea:focus{border:none}.tox-fullscreen{border:0;height:100%;margin:0;overflow:hidden;overscroll-behavior:none;padding:0;touch-action:pinch-zoom;width:100%}.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle{display:none}.tox-shadowhost.tox-fullscreen,.tox.tox-tinymce.tox-fullscreen{left:0;position:fixed;top:0;z-index:1200}.tox.tox-tinymce.tox-fullscreen{background-color:transparent}.tox-fullscreen .tox.tox-tinymce-aux,.tox-fullscreen~.tox.tox-tinymce-aux{z-index:1201}.tox .tox-help__more-link{list-style:none;margin-top:1em}.tox .tox-imagepreview{background-color:#666;height:380px;overflow:hidden;position:relative;width:100%}.tox .tox-imagepreview.tox-imagepreview__loaded{overflow:auto}.tox .tox-imagepreview__container{display:flex;left:100vw;position:absolute;top:100vw}.tox .tox-imagepreview__image{background:url(data:image/gif;base64,R0lGODdhDAAMAIABAMzMzP///ywAAAAADAAMAAACFoQfqYeabNyDMkBQb81Uat85nxguUAEAOw==)}.tox .tox-image-tools .tox-spacer{flex:1}.tox .tox-image-tools .tox-bar{align-items:center;display:flex;height:60px;justify-content:center}.tox .tox-image-tools .tox-imagepreview,.tox .tox-image-tools .tox-imagepreview+.tox-bar{margin-top:8px}.tox .tox-image-tools .tox-croprect-block{background:#000;opacity:.5;position:absolute;zoom:1}.tox .tox-image-tools .tox-croprect-handle{border:2px solid #fff;height:20px;left:0;position:absolute;top:0;width:20px}.tox .tox-image-tools .tox-croprect-handle-move{border:0;cursor:move;position:absolute}.tox .tox-image-tools .tox-croprect-handle-nw{border-width:2px 0 0 2px;cursor:nw-resize;left:100px;margin:-2px 0 0 -2px;top:100px}.tox .tox-image-tools .tox-croprect-handle-ne{border-width:2px 2px 0 0;cursor:ne-resize;left:200px;margin:-2px 0 0 -20px;top:100px}.tox .tox-image-tools .tox-croprect-handle-sw{border-width:0 0 2px 2px;cursor:sw-resize;left:100px;margin:-20px 2px 0 -2px;top:200px}.tox .tox-image-tools .tox-croprect-handle-se{border-width:0 2px 2px 0;cursor:se-resize;left:200px;margin:-20px 0 0 -20px;top:200px}.tox .tox-insert-table-picker{display:flex;flex-wrap:wrap;width:170px}.tox .tox-insert-table-picker>div{border-color:#ccc;border-style:solid;border-width:0 1px 1px 0;box-sizing:border-box;height:17px;width:17px}.tox .tox-collection--list .tox-collection__group .tox-insert-table-picker{margin:0 -4px}.tox .tox-insert-table-picker .tox-insert-table-picker__selected{background-color:rgba(32,122,183,.5);border-color:rgba(32,122,183,.5)}.tox .tox-insert-table-picker__label{color:rgba(34,47,62,.7);display:block;font-size:14px;padding:4px;text-align:center;width:100%}.tox:not([dir=rtl]) .tox-insert-table-picker>div:nth-child(10n){border-right:0}.tox[dir=rtl] .tox-insert-table-picker>div:nth-child(10n+1){border-right:0}.tox .tox-menu{background-color:#fff;border:1px solid #ccc;border-radius:3px;box-shadow:0 4px 8px 0 rgba(34,47,62,.1);display:inline-block;overflow:hidden;vertical-align:top;z-index:1150}.tox .tox-menu.tox-collection.tox-collection--list{padding:0 0}.tox .tox-menu.tox-collection.tox-collection--toolbar{padding:4px}.tox .tox-menu.tox-collection.tox-collection--grid{padding:4px}@media only screen and (min-width:768px){.tox .tox-menu .tox-collection__item-label{overflow-wrap:break-word;word-break:normal}.tox .tox-dialog__popups .tox-menu .tox-collection__item-label{word-break:break-all}}.tox .tox-menu__label blockquote,.tox .tox-menu__label code,.tox .tox-menu__label h1,.tox .tox-menu__label h2,.tox .tox-menu__label h3,.tox .tox-menu__label h4,.tox .tox-menu__label h5,.tox .tox-menu__label h6,.tox .tox-menu__label p{margin:0}.tox .tox-menubar{background:url(\\\"data:image/svg+xml;charset=utf8,%3Csvg height='39px' viewBox='0 0 40 39px' width='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='0' y='38px' width='100' height='1' fill='%23cccccc'/%3E%3C/svg%3E\\\") left 0 top 0 #fff;background-color:#fff;display:flex;flex:0 0 auto;flex-shrink:0;flex-wrap:wrap;grid-column:1/-1;grid-row:1;padding:0 4px 0 4px}.tox .tox-promotion+.tox-menubar{grid-column:1}.tox .tox-promotion{background:url(\\\"data:image/svg+xml;charset=utf8,%3Csvg height='39px' viewBox='0 0 40 39px' width='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='0' y='38px' width='100' height='1' fill='%23cccccc'/%3E%3C/svg%3E\\\") left 0 top 0 #fff;background-color:#fff;grid-column:2;grid-row:1;padding-inline-end:8px;padding-inline-start:4px;padding-top:5px}.tox .tox-promotion-link{align-items:unsafe center;background-color:#e8f1f8;border-radius:5px;color:#086be6;cursor:pointer;display:flex;font-size:14px;height:26.6px;padding:4px 8px;white-space:nowrap}.tox .tox-promotion-link:hover{background-color:#b4d7ff}.tox .tox-promotion-link:focus{background-color:#d9edf7}.tox .tox-mbtn{align-items:center;background:0 0;border:0;border-radius:3px;box-shadow:none;color:#222f3e;display:flex;flex:0 0 auto;font-size:14px;font-style:normal;font-weight:400;height:34px;justify-content:center;margin:2px 0 3px 0;outline:0;overflow:hidden;padding:0 4px;text-transform:none;width:auto}.tox .tox-mbtn[disabled]{background-color:transparent;border:0;box-shadow:none;color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-mbtn:focus:not(:disabled){background:#dee0e2;border:0;box-shadow:none;color:#222f3e}.tox .tox-mbtn--active{background:#c8cbcf;border:0;box-shadow:none;color:#222f3e}.tox .tox-mbtn:hover:not(:disabled):not(.tox-mbtn--active){background:#dee0e2;border:0;box-shadow:none;color:#222f3e}.tox .tox-mbtn__select-label{cursor:default;font-weight:400;margin:0 4px}.tox .tox-mbtn[disabled] .tox-mbtn__select-label{cursor:not-allowed}.tox .tox-mbtn__select-chevron{align-items:center;display:flex;justify-content:center;width:16px;display:none}.tox .tox-notification{border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;display:grid;font-size:14px;font-weight:400;grid-template-columns:minmax(40px,1fr) auto minmax(40px,1fr);margin-top:4px;opacity:0;padding:4px;transition:transform .1s ease-in,opacity 150ms ease-in}.tox .tox-notification p{font-size:14px;font-weight:400}.tox .tox-notification a{cursor:pointer;text-decoration:underline}.tox .tox-notification--in{opacity:1}.tox .tox-notification--success{background-color:#e4eeda;border-color:#d7e6c8;color:#222f3e}.tox .tox-notification--success p{color:#222f3e}.tox .tox-notification--success a{color:#517342}.tox .tox-notification--success svg{fill:#222f3e}.tox .tox-notification--error{background-color:#f5cccc;border-color:#f0b3b3;color:#222f3e}.tox .tox-notification--error p{color:#222f3e}.tox .tox-notification--error a{color:#77181f}.tox .tox-notification--error svg{fill:#222f3e}.tox .tox-notification--warn,.tox .tox-notification--warning{background-color:#fff5cc;border-color:#fff0b3;color:#222f3e}.tox .tox-notification--warn p,.tox .tox-notification--warning p{color:#222f3e}.tox .tox-notification--warn a,.tox .tox-notification--warning a{color:#7a6e25}.tox .tox-notification--warn svg,.tox .tox-notification--warning svg{fill:#222f3e}.tox .tox-notification--info{background-color:#d6e7fb;border-color:#c1dbf9;color:#222f3e}.tox .tox-notification--info p{color:#222f3e}.tox .tox-notification--info a{color:#2a64a6}.tox .tox-notification--info svg{fill:#222f3e}.tox .tox-notification__body{align-self:center;color:#222f3e;font-size:14px;grid-column-end:3;grid-column-start:2;grid-row-end:2;grid-row-start:1;text-align:center;white-space:normal;word-break:break-all;word-break:break-word}.tox .tox-notification__body>*{margin:0}.tox .tox-notification__body>*+*{margin-top:1rem}.tox .tox-notification__icon{align-self:center;grid-column-end:2;grid-column-start:1;grid-row-end:2;grid-row-start:1;justify-self:end}.tox .tox-notification__icon svg{display:block}.tox .tox-notification__dismiss{align-self:start;grid-column-end:4;grid-column-start:3;grid-row-end:2;grid-row-start:1;justify-self:end}.tox .tox-notification .tox-progress-bar{grid-column-end:4;grid-column-start:1;grid-row-end:3;grid-row-start:2;justify-self:center}.tox .tox-pop{display:inline-block;position:relative}.tox .tox-pop--resizing{transition:width .1s ease}.tox .tox-pop--resizing .tox-toolbar,.tox .tox-pop--resizing .tox-toolbar__group{flex-wrap:nowrap}.tox .tox-pop--transition{transition:.15s ease;transition-property:left,right,top,bottom}.tox .tox-pop--transition::after,.tox .tox-pop--transition::before{transition:all .15s,visibility 0s,opacity 75ms ease 75ms}.tox .tox-pop__dialog{background-color:#fff;border:1px solid #ccc;border-radius:3px;box-shadow:0 0 2px 0 rgba(34,47,62,.2),0 4px 8px 0 rgba(34,47,62,.15);min-width:0;overflow:hidden}.tox .tox-pop__dialog>:not(.tox-toolbar){margin:4px 4px 4px 8px}.tox .tox-pop__dialog .tox-toolbar{background-color:transparent;margin-bottom:-1px}.tox .tox-pop::after,.tox .tox-pop::before{border-style:solid;content:'';display:block;height:0;opacity:1;position:absolute;width:0}.tox .tox-pop.tox-pop--inset::after,.tox .tox-pop.tox-pop--inset::before{opacity:0;transition:all 0s .15s,visibility 0s,opacity 75ms ease}.tox .tox-pop.tox-pop--bottom::after,.tox .tox-pop.tox-pop--bottom::before{left:50%;top:100%}.tox .tox-pop.tox-pop--bottom::after{border-color:#fff transparent transparent transparent;border-width:8px;margin-left:-8px;margin-top:-1px}.tox .tox-pop.tox-pop--bottom::before{border-color:#ccc transparent transparent transparent;border-width:9px;margin-left:-9px}.tox .tox-pop.tox-pop--top::after,.tox .tox-pop.tox-pop--top::before{left:50%;top:0;transform:translateY(-100%)}.tox .tox-pop.tox-pop--top::after{border-color:transparent transparent #fff transparent;border-width:8px;margin-left:-8px;margin-top:1px}.tox .tox-pop.tox-pop--top::before{border-color:transparent transparent #ccc transparent;border-width:9px;margin-left:-9px}.tox .tox-pop.tox-pop--left::after,.tox .tox-pop.tox-pop--left::before{left:0;top:calc(50% - 1px);transform:translateY(-50%)}.tox .tox-pop.tox-pop--left::after{border-color:transparent #fff transparent transparent;border-width:8px;margin-left:-15px}.tox .tox-pop.tox-pop--left::before{border-color:transparent #ccc transparent transparent;border-width:10px;margin-left:-19px}.tox .tox-pop.tox-pop--right::after,.tox .tox-pop.tox-pop--right::before{left:100%;top:calc(50% + 1px);transform:translateY(-50%)}.tox .tox-pop.tox-pop--right::after{border-color:transparent transparent transparent #fff;border-width:8px;margin-left:-1px}.tox .tox-pop.tox-pop--right::before{border-color:transparent transparent transparent #ccc;border-width:10px;margin-left:-1px}.tox .tox-pop.tox-pop--align-left::after,.tox .tox-pop.tox-pop--align-left::before{left:20px}.tox .tox-pop.tox-pop--align-right::after,.tox .tox-pop.tox-pop--align-right::before{left:calc(100% - 20px)}.tox .tox-sidebar-wrap{display:flex;flex-direction:row;flex-grow:1;min-height:0}.tox .tox-sidebar{background-color:#fff;display:flex;flex-direction:row;justify-content:flex-end}.tox .tox-sidebar__slider{display:flex;overflow:hidden}.tox .tox-sidebar__pane-container{display:flex}.tox .tox-sidebar__pane{display:flex}.tox .tox-sidebar--sliding-closed{opacity:0}.tox .tox-sidebar--sliding-open{opacity:1}.tox .tox-sidebar--sliding-growing,.tox .tox-sidebar--sliding-shrinking{transition:width .5s ease,opacity .5s ease}.tox .tox-selector{background-color:#4099ff;border-color:#4099ff;border-style:solid;border-width:1px;box-sizing:border-box;display:inline-block;height:10px;position:absolute;width:10px}.tox.tox-platform-touch .tox-selector{height:12px;width:12px}.tox .tox-slider{align-items:center;display:flex;flex:1;height:24px;justify-content:center;position:relative}.tox .tox-slider__rail{background-color:transparent;border:1px solid #ccc;border-radius:3px;height:10px;min-width:120px;width:100%}.tox .tox-slider__handle{background-color:#207ab7;border:2px solid #185d8c;border-radius:3px;box-shadow:none;height:24px;left:50%;position:absolute;top:50%;transform:translateX(-50%) translateY(-50%);width:14px}.tox .tox-form__controls-h-stack>.tox-slider:not(:first-of-type){margin-inline-start:8px}.tox .tox-form__controls-h-stack>.tox-form__group+.tox-slider{margin-inline-start:32px}.tox .tox-form__controls-h-stack>.tox-slider+.tox-form__group{margin-inline-start:32px}.tox .tox-source-code{overflow:auto}.tox .tox-spinner{display:flex}.tox .tox-spinner>div{animation:tam-bouncing-dots 1.5s ease-in-out 0s infinite both;background-color:rgba(34,47,62,.7);border-radius:100%;height:8px;width:8px}.tox .tox-spinner>div:nth-child(1){animation-delay:-.32s}.tox .tox-spinner>div:nth-child(2){animation-delay:-.16s}@keyframes tam-bouncing-dots{0%,100%,80%{transform:scale(0)}40%{transform:scale(1)}}.tox:not([dir=rtl]) .tox-spinner>div:not(:first-child){margin-left:4px}.tox[dir=rtl] .tox-spinner>div:not(:first-child){margin-right:4px}.tox .tox-statusbar{align-items:center;background-color:#fff;border-top:1px solid #ccc;color:rgba(34,47,62,.7);display:flex;flex:0 0 auto;font-size:12px;font-weight:400;height:18px;overflow:hidden;padding:0 8px;position:relative;text-transform:uppercase}.tox .tox-statusbar__path{display:flex;flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tox .tox-statusbar__right-container{display:flex;justify-content:flex-end;white-space:nowrap}.tox .tox-statusbar__help-text{text-align:center}.tox .tox-statusbar__text-container{display:flex;flex:1 1 auto;justify-content:space-between;overflow:hidden}@media only screen and (min-width:768px){.tox .tox-statusbar__text-container.tox-statusbar__text-container-3-cols>.tox-statusbar__help-text,.tox .tox-statusbar__text-container.tox-statusbar__text-container-3-cols>.tox-statusbar__path,.tox .tox-statusbar__text-container.tox-statusbar__text-container-3-cols>.tox-statusbar__right-container{flex:0 0 calc(100% / 3)}}.tox .tox-statusbar__text-container.tox-statusbar__text-container--flex-end{justify-content:flex-end}.tox .tox-statusbar__text-container.tox-statusbar__text-container--flex-start{justify-content:flex-start}.tox .tox-statusbar__text-container.tox-statusbar__text-container--space-around{justify-content:space-around}.tox .tox-statusbar__path>*{display:inline;white-space:nowrap}.tox .tox-statusbar__wordcount{flex:0 0 auto;margin-left:1ch}@media only screen and (max-width:767px){.tox .tox-statusbar__text-container .tox-statusbar__help-text{display:none}.tox .tox-statusbar__text-container .tox-statusbar__help-text:only-child{display:block}}.tox .tox-statusbar a,.tox .tox-statusbar__path-item,.tox .tox-statusbar__wordcount{color:rgba(34,47,62,.7);text-decoration:none}.tox .tox-statusbar a:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar a:hover:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__path-item:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__path-item:hover:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__wordcount:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__wordcount:hover:not(:disabled):not([aria-disabled=true]){color:#222f3e;cursor:pointer}.tox .tox-statusbar__branding svg{fill:rgba(34,47,62,.8);height:1.14em;vertical-align:-.28em;width:3.6em}.tox .tox-statusbar__branding a:focus:not(:disabled):not([aria-disabled=true]) svg,.tox .tox-statusbar__branding a:hover:not(:disabled):not([aria-disabled=true]) svg{fill:#222f3e}.tox .tox-statusbar__resize-handle{align-items:flex-end;align-self:stretch;cursor:nwse-resize;display:flex;flex:0 0 auto;justify-content:flex-end;margin-left:auto;margin-right:-8px;padding-bottom:3px;padding-left:1ch;padding-right:3px}.tox .tox-statusbar__resize-handle svg{display:block;fill:rgba(34,47,62,.5)}.tox .tox-statusbar__resize-handle:focus svg{background-color:#dee0e2;border-radius:1px 1px -4px 1px;box-shadow:0 0 0 2px #dee0e2}.tox:not([dir=rtl]) .tox-statusbar__path>*{margin-right:4px}.tox:not([dir=rtl]) .tox-statusbar__branding{margin-left:2ch}.tox[dir=rtl] .tox-statusbar{flex-direction:row-reverse}.tox[dir=rtl] .tox-statusbar__path>*{margin-left:4px}.tox .tox-throbber{z-index:1299}.tox .tox-throbber__busy-spinner{align-items:center;background-color:rgba(255,255,255,.6);bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0}.tox .tox-tbtn{align-items:center;background:0 0;border:0;border-radius:3px;box-shadow:none;color:#222f3e;display:flex;flex:0 0 auto;font-size:14px;font-style:normal;font-weight:400;height:34px;justify-content:center;margin:3px 0 2px 0;outline:0;overflow:hidden;padding:0;text-transform:none;width:34px}.tox .tox-tbtn svg{display:block;fill:#222f3e}.tox .tox-tbtn.tox-tbtn-more{padding-left:5px;padding-right:5px;width:inherit}.tox .tox-tbtn:focus{background:#dee0e2;border:0;box-shadow:none}.tox .tox-tbtn:hover{background:#dee0e2;border:0;box-shadow:none;color:#222f3e}.tox .tox-tbtn:hover svg{fill:#222f3e}.tox .tox-tbtn:active{background:#c8cbcf;border:0;box-shadow:none;color:#222f3e}.tox .tox-tbtn:active svg{fill:#222f3e}.tox .tox-tbtn--disabled .tox-tbtn--enabled svg{fill:rgba(34,47,62,.5)}.tox .tox-tbtn--disabled,.tox .tox-tbtn--disabled:hover,.tox .tox-tbtn:disabled,.tox .tox-tbtn:disabled:hover{background:0 0;border:0;box-shadow:none;color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-tbtn--disabled svg,.tox .tox-tbtn--disabled:hover svg,.tox .tox-tbtn:disabled svg,.tox .tox-tbtn:disabled:hover svg{fill:rgba(34,47,62,.5)}.tox .tox-tbtn--enabled,.tox .tox-tbtn--enabled:hover{background:#c8cbcf;border:0;box-shadow:none;color:#222f3e}.tox .tox-tbtn--enabled:hover>*,.tox .tox-tbtn--enabled>*{transform:none}.tox .tox-tbtn--enabled svg,.tox .tox-tbtn--enabled:hover svg{fill:#222f3e}.tox .tox-tbtn--enabled.tox-tbtn--disabled svg,.tox .tox-tbtn--enabled:hover.tox-tbtn--disabled svg{fill:rgba(34,47,62,.5)}.tox .tox-tbtn:focus:not(.tox-tbtn--disabled){color:#222f3e}.tox .tox-tbtn:focus:not(.tox-tbtn--disabled) svg{fill:#222f3e}.tox .tox-tbtn:active>*{transform:none}.tox .tox-tbtn--md{height:51px;width:51px}.tox .tox-tbtn--lg{flex-direction:column;height:68px;width:68px}.tox .tox-tbtn--return{align-self:stretch;height:unset;width:16px}.tox .tox-tbtn--labeled{padding:0 4px;width:unset}.tox .tox-tbtn__vlabel{display:block;font-size:10px;font-weight:400;letter-spacing:-.025em;margin-bottom:4px;white-space:nowrap}.tox .tox-number-input{border-radius:3px;display:flex;margin:3px 0 2px 0;padding:0 4px;width:auto}.tox .tox-number-input .tox-input-wrapper{background:0 0;display:flex;pointer-events:none;text-align:center}.tox .tox-number-input .tox-input-wrapper:focus{background:#dee0e2}.tox .tox-number-input input{border-radius:3px;color:#222f3e;font-size:14px;margin:2px 0;pointer-events:all;width:60px}.tox .tox-number-input input:hover{background:#dee0e2;color:#222f3e}.tox .tox-number-input input:focus{background:#fff;color:#222f3e}.tox .tox-number-input input:disabled{background:0 0;border:0;box-shadow:none;color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-number-input button{background:0 0;color:#222f3e;height:34px;text-align:center;width:24px}.tox .tox-number-input button svg{display:block;fill:#222f3e;margin:0 auto;transform:scale(.67)}.tox .tox-number-input button:focus{background:#dee0e2}.tox .tox-number-input button:hover{background:#dee0e2;border:0;box-shadow:none;color:#222f3e}.tox .tox-number-input button:hover svg{fill:#222f3e}.tox .tox-number-input button:active{background:#c8cbcf;border:0;box-shadow:none;color:#222f3e}.tox .tox-number-input button:active svg{fill:#222f3e}.tox .tox-number-input button:disabled{background:0 0;border:0;box-shadow:none;color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-number-input button:disabled svg{fill:rgba(34,47,62,.5)}.tox .tox-number-input button.minus{border-radius:3px 0 0 3px}.tox .tox-number-input button.plus{border-radius:0 3px 3px 0}.tox .tox-number-input:focus:not(:active)>.tox-input-wrapper,.tox .tox-number-input:focus:not(:active)>button{background:#dee0e2}.tox .tox-tbtn--select{margin:3px 0 2px 0;padding:0 4px;width:auto}.tox .tox-tbtn__select-label{cursor:default;font-weight:400;height:initial;margin:0 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tox .tox-tbtn__select-chevron{align-items:center;display:flex;justify-content:center;width:16px}.tox .tox-tbtn__select-chevron svg{fill:rgba(34,47,62,.5)}.tox .tox-tbtn--bespoke{background:0 0}.tox .tox-tbtn--bespoke+.tox-tbtn--bespoke{margin-inline-start:0}.tox .tox-tbtn--bespoke .tox-tbtn__select-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:7em}.tox .tox-tbtn--disabled .tox-tbtn__select-label,.tox .tox-tbtn--select:disabled .tox-tbtn__select-label{cursor:not-allowed}.tox .tox-split-button{border:0;border-radius:3px;box-sizing:border-box;display:flex;margin:3px 0 2px 0;overflow:hidden}.tox .tox-split-button:hover{box-shadow:0 0 0 1px #dee0e2 inset}.tox .tox-split-button:focus{background:#dee0e2;box-shadow:none;color:#222f3e}.tox .tox-split-button>*{border-radius:0}.tox .tox-split-button__chevron{width:16px}.tox .tox-split-button__chevron svg{fill:rgba(34,47,62,.5)}.tox .tox-split-button .tox-tbtn{margin:0}.tox .tox-split-button.tox-tbtn--disabled .tox-tbtn:focus,.tox .tox-split-button.tox-tbtn--disabled .tox-tbtn:hover,.tox .tox-split-button.tox-tbtn--disabled:focus,.tox .tox-split-button.tox-tbtn--disabled:hover{background:0 0;box-shadow:none;color:rgba(34,47,62,.5)}.tox.tox-platform-touch .tox-split-button .tox-tbtn--select{padding:0 0}.tox.tox-platform-touch .tox-split-button .tox-tbtn:not(.tox-tbtn--select):first-child{width:30px}.tox.tox-platform-touch .tox-split-button__chevron{width:20px}.tox .tox-split-button.tox-tbtn--disabled svg #tox-icon-highlight-bg-color__color,.tox .tox-split-button.tox-tbtn--disabled svg #tox-icon-text-color__color{opacity:.6}.tox .tox-toolbar-overlord{background-color:#fff}.tox .tox-toolbar,.tox .tox-toolbar__overflow,.tox .tox-toolbar__primary{background-attachment:local;background-color:#fff;background-image:repeating-linear-gradient(#ccc 0 1px,transparent 1px 39px);background-position:center top 39px;background-repeat:no-repeat;background-size:calc(100% - 4px * 2) calc(100% - 39px);display:flex;flex:0 0 auto;flex-shrink:0;flex-wrap:wrap;padding:0 0;transform:perspective(1px)}.tox .tox-toolbar-overlord>.tox-toolbar,.tox .tox-toolbar-overlord>.tox-toolbar__overflow,.tox .tox-toolbar-overlord>.tox-toolbar__primary{background-position:center top 0;background-size:calc(100% - 4px * 2) calc(100% - 0px)}.tox .tox-toolbar__overflow.tox-toolbar__overflow--closed{height:0;opacity:0;padding-bottom:0;padding-top:0;visibility:hidden}.tox .tox-toolbar__overflow--growing{transition:height .3s ease,opacity .2s linear .1s}.tox .tox-toolbar__overflow--shrinking{transition:opacity .3s ease,height .2s linear .1s,visibility 0s linear .3s}.tox .tox-anchorbar,.tox .tox-toolbar-overlord{grid-column:1/-1}.tox .tox-menubar+.tox-toolbar,.tox .tox-menubar+.tox-toolbar-overlord{border-top:1px solid #ccc;margin-top:-1px;padding-bottom:0;padding-top:0}.tox .tox-toolbar--scrolling{flex-wrap:nowrap;overflow-x:auto}.tox .tox-pop .tox-toolbar{border-width:0}.tox .tox-toolbar--no-divider{background-image:none}.tox .tox-toolbar-overlord .tox-toolbar:not(.tox-toolbar--scrolling):first-child,.tox .tox-toolbar-overlord .tox-toolbar__primary{background-position:center top 39px}.tox .tox-editor-header>.tox-toolbar--scrolling,.tox .tox-toolbar-overlord .tox-toolbar--scrolling:first-child{background-image:none}.tox.tox-tinymce-aux .tox-toolbar__overflow{background-color:#fff;background-position:center top 43px;background-size:calc(100% - 8px * 2) calc(100% - 51px);border:none;border-radius:3px;box-shadow:0 0 2px 0 rgba(34,47,62,.2),0 4px 8px 0 rgba(34,47,62,.15);overscroll-behavior:none;padding:4px 0}.tox-pop .tox-pop__dialog .tox-toolbar{background-position:center top 43px;background-size:calc(100% - 4px * 2) calc(100% - 51px);padding:4px 0}.tox .tox-toolbar__group{align-items:center;display:flex;flex-wrap:wrap;margin:0 0;padding:0 4px 0 4px}.tox .tox-toolbar__group--pull-right{margin-left:auto}.tox .tox-toolbar--scrolling .tox-toolbar__group{flex-shrink:0;flex-wrap:nowrap}.tox:not([dir=rtl]) .tox-toolbar__group:not(:last-of-type){border-right:1px solid #ccc}.tox[dir=rtl] .tox-toolbar__group:not(:last-of-type){border-left:1px solid #ccc}.tox .tox-tooltip{display:inline-block;padding:8px;position:relative}.tox .tox-tooltip__body{background-color:#222f3e;border-radius:3px;box-shadow:0 2px 4px rgba(34,47,62,.3);color:rgba(255,255,255,.75);font-size:14px;font-style:normal;font-weight:400;padding:4px 8px;text-transform:none}.tox .tox-tooltip__arrow{position:absolute}.tox .tox-tooltip--down .tox-tooltip__arrow{border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid #222f3e;bottom:0;left:50%;position:absolute;transform:translateX(-50%)}.tox .tox-tooltip--up .tox-tooltip__arrow{border-bottom:8px solid #222f3e;border-left:8px solid transparent;border-right:8px solid transparent;left:50%;position:absolute;top:0;transform:translateX(-50%)}.tox .tox-tooltip--right .tox-tooltip__arrow{border-bottom:8px solid transparent;border-left:8px solid #222f3e;border-top:8px solid transparent;position:absolute;right:0;top:50%;transform:translateY(-50%)}.tox .tox-tooltip--left .tox-tooltip__arrow{border-bottom:8px solid transparent;border-right:8px solid #222f3e;border-top:8px solid transparent;left:0;position:absolute;top:50%;transform:translateY(-50%)}.tox .tox-tree{display:flex;flex-direction:column}.tox .tox-tree .tox-trbtn{align-items:center;background:0 0;border:0;border-radius:4px;box-shadow:none;color:#222f3e;display:flex;flex:0 0 auto;font-size:14px;font-style:normal;font-weight:400;height:28px;margin-bottom:4px;margin-top:4px;outline:0;overflow:hidden;padding:0;padding-left:8px;text-transform:none}.tox .tox-tree .tox-trbtn .tox-tree__label{cursor:default;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tox .tox-tree .tox-trbtn svg{display:block;fill:#222f3e}.tox .tox-tree .tox-trbtn:focus{background:#dee0e2;border:0;box-shadow:none}.tox .tox-tree .tox-trbtn:hover{background:#dee0e2;border:0;box-shadow:none;color:#222f3e}.tox .tox-tree .tox-trbtn:hover svg{fill:#222f3e}.tox .tox-tree .tox-trbtn:active{background:#b1d0e6;border:0;box-shadow:none;color:#222f3e}.tox .tox-tree .tox-trbtn:active svg{fill:#222f3e}.tox .tox-tree .tox-trbtn--disabled,.tox .tox-tree .tox-trbtn--disabled:hover,.tox .tox-tree .tox-trbtn:disabled,.tox .tox-tree .tox-trbtn:disabled:hover{background:0 0;border:0;box-shadow:none;color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-tree .tox-trbtn--disabled svg,.tox .tox-tree .tox-trbtn--disabled:hover svg,.tox .tox-tree .tox-trbtn:disabled svg,.tox .tox-tree .tox-trbtn:disabled:hover svg{fill:rgba(34,47,62,.5)}.tox .tox-tree .tox-trbtn--enabled,.tox .tox-tree .tox-trbtn--enabled:hover{background:#b1d0e6;border:0;box-shadow:none;color:#222f3e}.tox .tox-tree .tox-trbtn--enabled:hover>*,.tox .tox-tree .tox-trbtn--enabled>*{transform:none}.tox .tox-tree .tox-trbtn--enabled svg,.tox .tox-tree .tox-trbtn--enabled:hover svg{fill:#222f3e}.tox .tox-tree .tox-trbtn:focus:not(.tox-trbtn--disabled){color:#222f3e}.tox .tox-tree .tox-trbtn:focus:not(.tox-trbtn--disabled) svg{fill:#222f3e}.tox .tox-tree .tox-trbtn:active>*{transform:none}.tox .tox-tree .tox-trbtn--return{align-self:stretch;height:unset;width:16px}.tox .tox-tree .tox-trbtn--labeled{padding:0 4px;width:unset}.tox .tox-tree .tox-trbtn__vlabel{display:block;font-size:10px;font-weight:400;letter-spacing:-.025em;margin-bottom:4px;white-space:nowrap}.tox .tox-tree .tox-tree--directory{display:flex;flex-direction:column}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label{font-weight:700}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-mbtn{margin-left:auto}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-mbtn svg{fill:transparent}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-mbtn.tox-mbtn--active svg,.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-mbtn:focus svg{fill:#222f3e}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:focus .tox-mbtn svg,.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:hover .tox-mbtn svg{fill:#222f3e}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:hover:has(.tox-mbtn:hover){background-color:transparent;color:#222f3e}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:hover:has(.tox-mbtn:hover) .tox-chevron svg{fill:#222f3e}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-chevron{margin-right:6px}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:has(+.tox-tree--directory__children--growing) .tox-chevron,.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:has(+.tox-tree--directory__children--shrinking) .tox-chevron{transition:transform .5s ease-in-out}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:has(+.tox-tree--directory__children--growing) .tox-chevron,.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:has(+.tox-tree--directory__children--open) .tox-chevron{transform:rotate(90deg)}.tox .tox-tree .tox-tree--leaf__label{font-weight:400}.tox .tox-tree .tox-tree--leaf__label .tox-mbtn{margin-left:auto}.tox .tox-tree .tox-tree--leaf__label .tox-mbtn svg{fill:transparent}.tox .tox-tree .tox-tree--leaf__label .tox-mbtn.tox-mbtn--active svg,.tox .tox-tree .tox-tree--leaf__label .tox-mbtn:focus svg{fill:#222f3e}.tox .tox-tree .tox-tree--leaf__label:hover .tox-mbtn svg{fill:#222f3e}.tox .tox-tree .tox-tree--leaf__label:hover:has(.tox-mbtn:hover){background-color:transparent;color:#222f3e}.tox .tox-tree .tox-tree--leaf__label:hover:has(.tox-mbtn:hover) .tox-chevron svg{fill:#222f3e}.tox .tox-tree .tox-tree--directory__children{overflow:hidden;padding-left:16px}.tox .tox-tree .tox-tree--directory__children.tox-tree--directory__children--growing,.tox .tox-tree .tox-tree--directory__children.tox-tree--directory__children--shrinking{transition:height .5s ease-in-out}.tox .tox-tree .tox-trbtn.tox-tree--leaf__label{display:flex;justify-content:space-between}.tox .tox-view-wrap,.tox .tox-view-wrap__slot-container{background-color:#fff;display:flex;flex:1;flex-direction:column}.tox .tox-view{display:flex;flex:1 1 auto;flex-direction:column;overflow:hidden}.tox .tox-view__header{align-items:center;display:flex;font-size:16px;justify-content:space-between;padding:8px 8px 0 8px;position:relative}.tox .tox-view--mobile.tox-view__header,.tox .tox-view--mobile.tox-view__toolbar{padding:8px}.tox .tox-view--scrolling{flex-wrap:nowrap;overflow-x:auto}.tox .tox-view__toolbar{display:flex;flex-direction:row;gap:8px;justify-content:space-between;padding:8px 8px 0 8px}.tox .tox-view__toolbar__group{display:flex;flex-direction:row;gap:12px}.tox .tox-view__header-end,.tox .tox-view__header-start{display:flex}.tox .tox-view__pane{height:100%;padding:8px;width:100%}.tox .tox-view__pane_panel{border:1px solid #ccc;border-radius:3px}.tox:not([dir=rtl]) .tox-view__header .tox-view__header-end>*,.tox:not([dir=rtl]) .tox-view__header .tox-view__header-start>*{margin-left:8px}.tox[dir=rtl] .tox-view__header .tox-view__header-end>*,.tox[dir=rtl] .tox-view__header .tox-view__header-start>*{margin-right:8px}.tox .tox-well{border:1px solid #ccc;border-radius:3px;padding:8px;width:100%}.tox .tox-well>:first-child{margin-top:0}.tox .tox-well>:last-child{margin-bottom:0}.tox .tox-well>:only-child{margin:0}.tox .tox-custom-editor{border:1px solid #ccc;border-radius:3px;display:flex;flex:1;overflow:hidden;position:relative}.tox .tox-dialog-loading::before{background-color:rgba(0,0,0,.5);content:\\\"\\\";height:100%;position:absolute;width:100%;z-index:1000}.tox .tox-tab{cursor:pointer}.tox .tox-dialog__content-js{display:flex;flex:1}.tox .tox-dialog__body-content .tox-collection{display:flex;flex:1}.tox:not(.tox-tinymce-inline) .tox-editor-header{background-color:none;padding:0}.tox.tox-tinymce--toolbar-bottom .tox-editor-header,.tox.tox-tinymce-inline .tox-editor-header{margin-bottom:-1px}.tox.tox-tinymce-inline .tox-editor-container{overflow:hidden}.tox:not(.tox-tinymce-inline).tox-tinymce--toolbar-bottom .tox-editor-header{border-top:none;box-shadow:none}.tox.tox.tox-tinymce--toolbar-sticky-on .tox-editor-header{background-color:transparent;box-shadow:0 4px 4px -3px rgba(0,0,0,.25);padding:0}.tox.tox.tox-tinymce--toolbar-sticky-on.tox-tinymce--toolbar-bottom .tox-editor-header{box-shadow:0 4px 4px -3px rgba(0,0,0,.25)}.tox .tox-collection--list .tox-collection__group .tox-insert-table-picker{margin:-4px 0}.tox .tox-menu.tox-collection.tox-collection--list{padding:0}.tox .tox-pop{box-shadow:none}.tox .tox-number-input,.tox .tox-split-button,.tox .tox-tbtn,.tox .tox-tbtn--select{margin:2px 0 3px 0}.tox .tox-toolbar,.tox .tox-toolbar__overflow,.tox .tox-toolbar__primary{background:url(\\\"data:image/svg+xml;charset=utf8,%3Csvg height='39px' viewBox='0 0 40 39px' width='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='0' y='38px' width='100' height='1' fill='%23cccccc'/%3E%3C/svg%3E\\\") left 0 top 0 #fff!important}.tox .tox-menubar+.tox-toolbar-overlord{border-top:none}.tox .tox-menubar+.tox-toolbar,.tox .tox-menubar+.tox-toolbar-overlord .tox-toolbar__primary{border-top:1px solid #ccc;margin-top:-1px}.tox.tox-tinymce-aux .tox-toolbar__overflow{border:1px solid #ccc;padding:0}.tox .tox-pop .tox-pop__dialog .tox-toolbar{padding:0}.tox:not(.tox-tinymce-inline) .tox-editor-header:not(:first-child) .tox-menubar{border-top:1px solid #ccc}.tox:not(.tox-tinymce-inline) .tox-editor-header:not(:first-child) .tox-toolbar-overlord:first-child .tox-toolbar__primary,.tox:not(.tox-tinymce-inline) .tox-editor-header:not(:first-child) .tox-toolbar:first-child{border-top:1px solid #ccc}.tox .tox-toolbar__group{padding:0 4px 0 4px}.tox .tox-collection__item{border-radius:0;cursor:pointer}.tox .tox-statusbar a:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar a:hover:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__path-item:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__path-item:hover:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__wordcount:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__wordcount:hover:not(:disabled):not([aria-disabled=true]){color:rgba(34,47,62,.7);text-decoration:underline}.tox .tox-statusbar__branding svg{vertical-align:-.25em}.tox:not([dir=rtl]) .tox-statusbar__branding{margin-left:1ch}.tox .tox-statusbar__resize-handle{padding-bottom:0;padding-right:0}.tox .tox-button::before{display:none}\")\n//# sourceMappingURL=skin.js.map\n"
  },
  {
    "path": "public/libs/tinymce/skins/ui/tinymce-5/skin.shadowdom.js",
    "content": "tinymce.Resource.add('ui/tinymce-5/skin.shadowdom.css', \"body.tox-dialog__disable-scroll{overflow:hidden}.tox-fullscreen{border:0;height:100%;margin:0;overflow:hidden;overscroll-behavior:none;padding:0;touch-action:pinch-zoom;width:100%}.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle{display:none}.tox-shadowhost.tox-fullscreen,.tox.tox-tinymce.tox-fullscreen{left:0;position:fixed;top:0;z-index:1200}.tox.tox-tinymce.tox-fullscreen{background-color:transparent}.tox-fullscreen .tox.tox-tinymce-aux,.tox-fullscreen~.tox.tox-tinymce-aux{z-index:1201}\")\n//# sourceMappingURL=skin.shadowdom.js.map\n"
  },
  {
    "path": "public/libs/tinymce/skins/ui/tinymce-5-dark/content.inline.js",
    "content": "tinymce.Resource.add('ui/tinymce-5-dark/content.inline.css', \".mce-content-body .mce-item-anchor{background:transparent url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'8'%20height%3D'12'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20d%3D'M0%200L8%200%208%2012%204.09117821%209%200%2012z'%2F%3E%3C%2Fsvg%3E%0A\\\") no-repeat center}.mce-content-body .mce-item-anchor:empty{cursor:default;display:inline-block;height:12px!important;padding:0 2px;-webkit-user-modify:read-only;-moz-user-modify:read-only;-webkit-user-select:all;-moz-user-select:all;user-select:all;width:8px!important}.mce-content-body .mce-item-anchor:not(:empty){background-position-x:2px;display:inline-block;padding-left:12px}.mce-content-body .mce-item-anchor[data-mce-selected]{outline-offset:1px}.tox-comments-visible .tox-comment[contenteditable=false]:not([data-mce-selected]),.tox-comments-visible span.tox-comment img:not([data-mce-selected]),.tox-comments-visible span.tox-comment span.mce-preview-object:not([data-mce-selected]),.tox-comments-visible span.tox-comment>audio:not([data-mce-selected]),.tox-comments-visible span.tox-comment>video:not([data-mce-selected]){outline:3px solid #ffe89d}.tox-comments-visible .tox-comment[contenteditable=false][data-mce-annotation-active=true]:not([data-mce-selected]){outline:3px solid #fed635}.tox-comments-visible span.tox-comment[data-mce-annotation-active=true] img:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true] span.mce-preview-object:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]>audio:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]>video:not([data-mce-selected]){outline:3px solid #fed635}.tox-comments-visible span.tox-comment:not([data-mce-selected]){background-color:#ffe89d;outline:0}.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]:not([data-mce-selected=inline-boundary]){background-color:#fed635}.tox-checklist>li:not(.tox-checklist--hidden){list-style:none;margin:.25em 0}.tox-checklist>li:not(.tox-checklist--hidden)::before{content:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-unchecked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2215%22%20height%3D%2215%22%20x%3D%22.5%22%20y%3D%22.5%22%20fill-rule%3D%22nonzero%22%20stroke%3D%22%234C4C4C%22%20rx%3D%222%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A\\\");cursor:pointer;height:1em;margin-left:-1.5em;margin-top:.125em;position:absolute;width:1em}.tox-checklist li:not(.tox-checklist--hidden).tox-checklist--checked::before{content:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-checked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2216%22%20height%3D%2216%22%20fill%3D%22%234099FF%22%20fill-rule%3D%22nonzero%22%20rx%3D%222%22%2F%3E%3Cpath%20id%3D%22Path%22%20fill%3D%22%23FFF%22%20fill-rule%3D%22nonzero%22%20d%3D%22M11.5703186%2C3.14417309%20C11.8516238%2C2.73724603%2012.4164781%2C2.62829933%2012.83558%2C2.89774797%20C13.260121%2C3.17069355%2013.3759736%2C3.72932262%2013.0909105%2C4.14168582%20L7.7580587%2C11.8560195%20C7.43776896%2C12.3193404%206.76483983%2C12.3852142%206.35607322%2C11.9948725%20L3.02491697%2C8.8138662%20C2.66090143%2C8.46625845%202.65798871%2C7.89594698%203.01850234%2C7.54483354%20C3.373942%2C7.19866177%203.94940006%2C7.19592841%204.30829608%2C7.5386474%20L6.85276923%2C9.9684299%20L11.5703186%2C3.14417309%20Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A\\\")}[dir=rtl] .tox-checklist>li:not(.tox-checklist--hidden)::before{margin-left:0;margin-right:-1.5em}code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;tab-size:4;-webkit-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.mce-content-body{overflow-wrap:break-word;word-wrap:break-word}.mce-content-body .mce-visual-caret{background-color:#000;background-color:currentColor;position:absolute}.mce-content-body .mce-visual-caret-hidden{display:none}.mce-content-body [data-mce-caret]{left:-1000px;margin:0;padding:0;position:absolute;right:auto;top:0}.mce-content-body .mce-offscreen-selection{left:-2000000px;max-width:1000000px;position:absolute}.mce-content-body [contentEditable=false]{cursor:default}.mce-content-body [contentEditable=true]{cursor:text}.tox-cursor-format-painter{cursor:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%0A%20%20%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M15%2C6%20C15%2C5.45%2014.55%2C5%2014%2C5%20L6%2C5%20C5.45%2C5%205%2C5.45%205%2C6%20L5%2C10%20C5%2C10.55%205.45%2C11%206%2C11%20L14%2C11%20C14.55%2C11%2015%2C10.55%2015%2C10%20L15%2C9%20L16%2C9%20L16%2C12%20L9%2C12%20L9%2C19%20C9%2C19.55%209.45%2C20%2010%2C20%20L11%2C20%20C11.55%2C20%2012%2C19.55%2012%2C19%20L12%2C14%20L18%2C14%20L18%2C7%20L15%2C7%20L15%2C6%20Z%22%2F%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M1%2C1%20L8.25%2C1%20C8.66421356%2C1%209%2C1.33578644%209%2C1.75%20L9%2C1.75%20C9%2C2.16421356%208.66421356%2C2.5%208.25%2C2.5%20L2.5%2C2.5%20L2.5%2C8.25%20C2.5%2C8.66421356%202.16421356%2C9%201.75%2C9%20L1.75%2C9%20C1.33578644%2C9%201%2C8.66421356%201%2C8.25%20L1%2C1%20Z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E%0A\\\"),default}div.mce-footnotes hr{margin-inline-end:auto;margin-inline-start:0;width:25%}div.mce-footnotes li>a.mce-footnotes-backlink{text-decoration:none}@media print{sup.mce-footnote a{color:#000;text-decoration:none}div.mce-footnotes{break-inside:avoid;width:100%}div.mce-footnotes li>a.mce-footnotes-backlink{display:none}}.mce-content-body figure.align-left{float:left}.mce-content-body figure.align-right{float:right}.mce-content-body figure.image.align-center{display:table;margin-left:auto;margin-right:auto}.mce-preview-object{border:1px solid gray;display:inline-block;line-height:0;margin:0 2px 0 2px;position:relative}.mce-preview-object .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.mce-preview-object[data-mce-selected=\\\"2\\\"] .mce-shim{display:none}.mce-content-body .mce-mergetag{cursor:default!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mce-content-body .mce-mergetag:hover{background-color:rgba(0,108,231,.1)}.mce-content-body .mce-mergetag-affix{background-color:rgba(0,108,231,.1);color:#006ce7}.mce-object{background:transparent url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M4%203h16a1%201%200%200%201%201%201v16a1%201%200%200%201-1%201H4a1%201%200%200%201-1-1V4a1%201%200%200%201%201-1zm1%202v14h14V5H5zm4.79%202.565l5.64%204.028a.5.5%200%200%201%200%20.814l-5.64%204.028a.5.5%200%200%201-.79-.407V7.972a.5.5%200%200%201%20.79-.407z%22%2F%3E%3C%2Fsvg%3E%0A\\\") no-repeat center;border:1px dashed #aaa}.mce-pagebreak{border:1px dashed #aaa;cursor:default;display:block;height:5px;margin-top:15px;page-break-before:always;width:100%}@media print{.mce-pagebreak{border:0}}.tiny-pageembed .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.tiny-pageembed[data-mce-selected=\\\"2\\\"] .mce-shim{display:none}.tiny-pageembed{display:inline-block;position:relative}.tiny-pageembed--16by9,.tiny-pageembed--1by1,.tiny-pageembed--21by9,.tiny-pageembed--4by3{display:block;overflow:hidden;padding:0;position:relative;width:100%}.tiny-pageembed--21by9{padding-top:42.857143%}.tiny-pageembed--16by9{padding-top:56.25%}.tiny-pageembed--4by3{padding-top:75%}.tiny-pageembed--1by1{padding-top:100%}.tiny-pageembed--16by9 iframe,.tiny-pageembed--1by1 iframe,.tiny-pageembed--21by9 iframe,.tiny-pageembed--4by3 iframe{border:0;height:100%;left:0;position:absolute;top:0;width:100%}.mce-content-body[data-mce-placeholder]{position:relative}.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before{color:rgba(34,47,62,.7);content:attr(data-mce-placeholder);position:absolute}.mce-content-body:not([dir=rtl])[data-mce-placeholder]:not(.mce-visualblocks)::before{left:1px}.mce-content-body[dir=rtl][data-mce-placeholder]:not(.mce-visualblocks)::before{right:1px}.mce-content-body div.mce-resizehandle{background-color:#4099ff;border-color:#4099ff;border-style:solid;border-width:1px;box-sizing:border-box;height:10px;position:absolute;width:10px;z-index:1298}.mce-content-body div.mce-resizehandle:hover{background-color:#4099ff}.mce-content-body div.mce-resizehandle:nth-of-type(1){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(2){cursor:nesw-resize}.mce-content-body div.mce-resizehandle:nth-of-type(3){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(4){cursor:nesw-resize}.mce-content-body .mce-resize-backdrop{z-index:10000}.mce-content-body .mce-clonedresizable{cursor:default;opacity:.5;outline:1px dashed #000;position:absolute;z-index:10001}.mce-content-body .mce-clonedresizable.mce-resizetable-columns td,.mce-content-body .mce-clonedresizable.mce-resizetable-columns th{border:0}.mce-content-body .mce-resize-helper{background:#555;background:rgba(0,0,0,.75);border:1px;border-radius:3px;color:#fff;display:none;font-family:sans-serif;font-size:12px;line-height:14px;margin:5px 10px;padding:5px;position:absolute;white-space:nowrap;z-index:10002}.tox-rtc-user-selection{position:relative}.tox-rtc-user-cursor{bottom:0;cursor:default;position:absolute;top:0;width:2px}.tox-rtc-user-cursor::before{background-color:inherit;border-radius:50%;content:'';display:block;height:8px;position:absolute;right:-3px;top:-3px;width:8px}.tox-rtc-user-cursor:hover::after{background-color:inherit;border-radius:100px;box-sizing:border-box;color:#fff;content:attr(data-user);display:block;font-size:12px;font-weight:700;left:-5px;min-height:8px;min-width:8px;padding:0 12px;position:absolute;top:-11px;white-space:nowrap;z-index:1000}.tox-rtc-user-selection--1 .tox-rtc-user-cursor{background-color:#2dc26b}.tox-rtc-user-selection--2 .tox-rtc-user-cursor{background-color:#e03e2d}.tox-rtc-user-selection--3 .tox-rtc-user-cursor{background-color:#f1c40f}.tox-rtc-user-selection--4 .tox-rtc-user-cursor{background-color:#3598db}.tox-rtc-user-selection--5 .tox-rtc-user-cursor{background-color:#b96ad9}.tox-rtc-user-selection--6 .tox-rtc-user-cursor{background-color:#e67e23}.tox-rtc-user-selection--7 .tox-rtc-user-cursor{background-color:#aaa69d}.tox-rtc-user-selection--8 .tox-rtc-user-cursor{background-color:#f368e0}.tox-rtc-remote-image{background:#eaeaea url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2236%22%20height%3D%2212%22%20viewBox%3D%220%200%2036%2012%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Ccircle%20cx%3D%226%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2218%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.33s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2230%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.66s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%3C%2Fsvg%3E%0A\\\") no-repeat center center;border:1px solid #ccc;min-height:240px;min-width:320px}.mce-match-marker{background:#aaa;color:#fff}.mce-match-marker-selected{background:#39f;color:#fff}.mce-match-marker-selected::-moz-selection{background:#39f;color:#fff}.mce-match-marker-selected::selection{background:#39f;color:#fff}.mce-content-body audio[data-mce-selected],.mce-content-body details[data-mce-selected],.mce-content-body embed[data-mce-selected],.mce-content-body img[data-mce-selected],.mce-content-body object[data-mce-selected],.mce-content-body table[data-mce-selected],.mce-content-body video[data-mce-selected]{outline:3px solid #b4d7ff}.mce-content-body hr[data-mce-selected]{outline:3px solid #b4d7ff;outline-offset:1px}.mce-content-body [contentEditable=false] [contentEditable=true]:focus{outline:3px solid #b4d7ff}.mce-content-body [contentEditable=false] [contentEditable=true]:hover{outline:3px solid #b4d7ff}.mce-content-body [contentEditable=false][data-mce-selected]{cursor:not-allowed;outline:3px solid #b4d7ff}.mce-content-body.mce-content-readonly [contentEditable=true]:focus,.mce-content-body.mce-content-readonly [contentEditable=true]:hover{outline:0}.mce-content-body [data-mce-selected=inline-boundary]{background-color:#b4d7ff}.mce-content-body .mce-edit-focus{outline:3px solid #b4d7ff}.mce-content-body td[data-mce-selected],.mce-content-body th[data-mce-selected]{position:relative}.mce-content-body td[data-mce-selected]::-moz-selection,.mce-content-body th[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body td[data-mce-selected]::selection,.mce-content-body th[data-mce-selected]::selection{background:0 0}.mce-content-body td[data-mce-selected] *,.mce-content-body th[data-mce-selected] *{outline:0;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{background-color:rgba(180,215,255,.7);border:1px solid rgba(180,215,255,.7);bottom:-1px;content:'';left:-1px;mix-blend-mode:multiply;position:absolute;right:-1px;top:-1px}@media screen and (-ms-high-contrast:active),(-ms-high-contrast:none){.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{border-color:rgba(0,84,180,.7)}}.mce-content-body img[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body img[data-mce-selected]::selection{background:0 0}.ephox-snooker-resizer-bar{background-color:#b4d7ff;opacity:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ephox-snooker-resizer-cols{cursor:col-resize}.ephox-snooker-resizer-rows{cursor:row-resize}.ephox-snooker-resizer-bar.ephox-snooker-resizer-bar-dragging{opacity:1}.mce-spellchecker-word{background-image:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%23ff0000'%20fill%3D'none'%20stroke-linecap%3D'round'%20stroke-opacity%3D'.75'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A\\\");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default;height:2rem}.mce-spellchecker-grammar{background-image:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%2300A835'%20fill%3D'none'%20stroke-linecap%3D'round'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A\\\");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc ul>li{list-style-type:none}[data-mce-block]{display:block}.mce-item-table:not([border]),.mce-item-table:not([border]) caption,.mce-item-table:not([border]) td,.mce-item-table:not([border]) th,.mce-item-table[border=\\\"0\\\"],.mce-item-table[border=\\\"0\\\"] caption,.mce-item-table[border=\\\"0\\\"] td,.mce-item-table[border=\\\"0\\\"] th,table[style*=\\\"border-width: 0px\\\"],table[style*=\\\"border-width: 0px\\\"] caption,table[style*=\\\"border-width: 0px\\\"] td,table[style*=\\\"border-width: 0px\\\"] th{border:1px dashed #bbb}.mce-visualblocks address,.mce-visualblocks article,.mce-visualblocks aside,.mce-visualblocks blockquote,.mce-visualblocks div:not([data-mce-bogus]),.mce-visualblocks dl,.mce-visualblocks figcaption,.mce-visualblocks figure,.mce-visualblocks h1,.mce-visualblocks h2,.mce-visualblocks h3,.mce-visualblocks h4,.mce-visualblocks h5,.mce-visualblocks h6,.mce-visualblocks hgroup,.mce-visualblocks ol,.mce-visualblocks p,.mce-visualblocks pre,.mce-visualblocks section,.mce-visualblocks ul{background-repeat:no-repeat;border:1px dashed #bbb;margin-left:3px;padding-top:10px}.mce-visualblocks p{background-image:url(data:image/gif;base64,R0lGODlhCQAJAJEAAAAAAP///7u7u////yH5BAEAAAMALAAAAAAJAAkAAAIQnG+CqCN/mlyvsRUpThG6AgA7)}.mce-visualblocks h1{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGu1JuxHoAfRNRW3TWXyF2YiRUAOw==)}.mce-visualblocks h2{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8Hybbx4oOuqgTynJd6bGlWg3DkJzoaUAAAOw==)}.mce-visualblocks h3{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIZjI8Hybbx4oOuqgTynJf2Ln2NOHpQpmhAAQA7)}.mce-visualblocks h4{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxInR0zqeAdhtJlXwV1oCll2HaWgAAOw==)}.mce-visualblocks h5{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjane4iq5GlW05GgIkIZUAAAOw==)}.mce-visualblocks h6{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjan04jep1iZ1XRlAo5bVgAAOw==)}.mce-visualblocks div:not([data-mce-bogus]){background-image:url(data:image/gif;base64,R0lGODlhEgAKAIABALu7u////yH5BAEAAAEALAAAAAASAAoAAAIfjI9poI0cgDywrhuxfbrzDEbQM2Ei5aRjmoySW4pAAQA7)}.mce-visualblocks section{background-image:url(data:image/gif;base64,R0lGODlhKAAKAIABALu7u////yH5BAEAAAEALAAAAAAoAAoAAAI5jI+pywcNY3sBWHdNrplytD2ellDeSVbp+GmWqaDqDMepc8t17Y4vBsK5hDyJMcI6KkuYU+jpjLoKADs=)}.mce-visualblocks article{background-image:url(data:image/gif;base64,R0lGODlhKgAKAIABALu7u////yH5BAEAAAEALAAAAAAqAAoAAAI6jI+pywkNY3wG0GBvrsd2tXGYSGnfiF7ikpXemTpOiJScasYoDJJrjsG9gkCJ0ag6KhmaIe3pjDYBBQA7)}.mce-visualblocks blockquote{background-image:url(data:image/gif;base64,R0lGODlhPgAKAIABALu7u////yH5BAEAAAEALAAAAAA+AAoAAAJPjI+py+0Knpz0xQDyuUhvfoGgIX5iSKZYgq5uNL5q69asZ8s5rrf0yZmpNkJZzFesBTu8TOlDVAabUyatguVhWduud3EyiUk45xhTTgMBBQA7)}.mce-visualblocks address{background-image:url(data:image/gif;base64,R0lGODlhLQAKAIABALu7u////yH5BAEAAAEALAAAAAAtAAoAAAI/jI+pywwNozSP1gDyyZcjb3UaRpXkWaXmZW4OqKLhBmLs+K263DkJK7OJeifh7FicKD9A1/IpGdKkyFpNmCkAADs=)}.mce-visualblocks pre{background-image:url(data:image/gif;base64,R0lGODlhFQAKAIABALu7uwAAACH5BAEAAAEALAAAAAAVAAoAAAIjjI+ZoN0cgDwSmnpz1NCueYERhnibZVKLNnbOq8IvKpJtVQAAOw==)}.mce-visualblocks figure{background-image:url(data:image/gif;base64,R0lGODlhJAAKAIAAALu7u////yH5BAEAAAEALAAAAAAkAAoAAAI0jI+py+2fwAHUSFvD3RlvG4HIp4nX5JFSpnZUJ6LlrM52OE7uSWosBHScgkSZj7dDKnWAAgA7)}.mce-visualblocks figcaption{border:1px dashed #bbb}.mce-visualblocks hgroup{background-image:url(data:image/gif;base64,R0lGODlhJwAKAIABALu7uwAAACH5BAEAAAEALAAAAAAnAAoAAAI3jI+pywYNI3uB0gpsRtt5fFnfNZaVSYJil4Wo03Hv6Z62uOCgiXH1kZIIJ8NiIxRrAZNMZAtQAAA7)}.mce-visualblocks aside{background-image:url(data:image/gif;base64,R0lGODlhHgAKAIABAKqqqv///yH5BAEAAAEALAAAAAAeAAoAAAItjI+pG8APjZOTzgtqy7I3f1yehmQcFY4WKZbqByutmW4aHUd6vfcVbgudgpYCADs=)}.mce-visualblocks ul{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIAAALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGuYnqUVSjvw26DzzXiqIDlVwAAOw==)}.mce-visualblocks ol{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybH6HHt0qourxC6CvzXieHyeWQAAOw==)}.mce-visualblocks dl{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybEOnmOvUoWznTqeuEjNSCqeGRUAOw==)}.mce-visualblocks:not([dir=rtl]) address,.mce-visualblocks:not([dir=rtl]) article,.mce-visualblocks:not([dir=rtl]) aside,.mce-visualblocks:not([dir=rtl]) blockquote,.mce-visualblocks:not([dir=rtl]) div:not([data-mce-bogus]),.mce-visualblocks:not([dir=rtl]) dl,.mce-visualblocks:not([dir=rtl]) figcaption,.mce-visualblocks:not([dir=rtl]) figure,.mce-visualblocks:not([dir=rtl]) h1,.mce-visualblocks:not([dir=rtl]) h2,.mce-visualblocks:not([dir=rtl]) h3,.mce-visualblocks:not([dir=rtl]) h4,.mce-visualblocks:not([dir=rtl]) h5,.mce-visualblocks:not([dir=rtl]) h6,.mce-visualblocks:not([dir=rtl]) hgroup,.mce-visualblocks:not([dir=rtl]) ol,.mce-visualblocks:not([dir=rtl]) p,.mce-visualblocks:not([dir=rtl]) pre,.mce-visualblocks:not([dir=rtl]) section,.mce-visualblocks:not([dir=rtl]) ul{margin-left:3px}.mce-visualblocks[dir=rtl] address,.mce-visualblocks[dir=rtl] article,.mce-visualblocks[dir=rtl] aside,.mce-visualblocks[dir=rtl] blockquote,.mce-visualblocks[dir=rtl] div:not([data-mce-bogus]),.mce-visualblocks[dir=rtl] dl,.mce-visualblocks[dir=rtl] figcaption,.mce-visualblocks[dir=rtl] figure,.mce-visualblocks[dir=rtl] h1,.mce-visualblocks[dir=rtl] h2,.mce-visualblocks[dir=rtl] h3,.mce-visualblocks[dir=rtl] h4,.mce-visualblocks[dir=rtl] h5,.mce-visualblocks[dir=rtl] h6,.mce-visualblocks[dir=rtl] hgroup,.mce-visualblocks[dir=rtl] ol,.mce-visualblocks[dir=rtl] p,.mce-visualblocks[dir=rtl] pre,.mce-visualblocks[dir=rtl] section,.mce-visualblocks[dir=rtl] ul{background-position-x:right;margin-right:3px}.mce-nbsp,.mce-shy{background:#aaa}.mce-shy::after{content:'-'}\")\n//# sourceMappingURL=content.inline.js.map\n"
  },
  {
    "path": "public/libs/tinymce/skins/ui/tinymce-5-dark/content.js",
    "content": "tinymce.Resource.add('ui/tinymce-5-dark/content.css', \".mce-content-body .mce-item-anchor{background:transparent url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'8'%20height%3D'12'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20d%3D'M0%200L8%200%208%2012%204.09117821%209%200%2012z'%20fill%3D%22%23cccccc%22%2F%3E%3C%2Fsvg%3E%0A\\\") no-repeat center}.mce-content-body .mce-item-anchor:empty{cursor:default;display:inline-block;height:12px!important;padding:0 2px;-webkit-user-modify:read-only;-moz-user-modify:read-only;-webkit-user-select:all;-moz-user-select:all;user-select:all;width:8px!important}.mce-content-body .mce-item-anchor:not(:empty){background-position-x:2px;display:inline-block;padding-left:12px}.mce-content-body .mce-item-anchor[data-mce-selected]{outline-offset:1px}.tox-comments-visible .tox-comment[contenteditable=false]:not([data-mce-selected]),.tox-comments-visible span.tox-comment img:not([data-mce-selected]),.tox-comments-visible span.tox-comment span.mce-preview-object:not([data-mce-selected]),.tox-comments-visible span.tox-comment>audio:not([data-mce-selected]),.tox-comments-visible span.tox-comment>video:not([data-mce-selected]){outline:3px solid #ffe89d}.tox-comments-visible .tox-comment[contenteditable=false][data-mce-annotation-active=true]:not([data-mce-selected]){outline:3px solid #fed635}.tox-comments-visible span.tox-comment[data-mce-annotation-active=true] img:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true] span.mce-preview-object:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]>audio:not([data-mce-selected]),.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]>video:not([data-mce-selected]){outline:3px solid #fed635}.tox-comments-visible span.tox-comment:not([data-mce-selected]){background-color:#ffe89d;outline:0}.tox-comments-visible span.tox-comment[data-mce-annotation-active=true]:not([data-mce-selected=inline-boundary]){background-color:#fed635}.tox-checklist>li:not(.tox-checklist--hidden){list-style:none;margin:.25em 0}.tox-checklist>li:not(.tox-checklist--hidden)::before{content:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-unchecked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2215%22%20height%3D%2215%22%20x%3D%22.5%22%20y%3D%22.5%22%20fill-rule%3D%22nonzero%22%20stroke%3D%22%236d737b%22%20rx%3D%222%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A\\\");cursor:pointer;height:1em;margin-left:-1.5em;margin-top:.125em;position:absolute;width:1em}.tox-checklist li:not(.tox-checklist--hidden).tox-checklist--checked::before{content:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-checked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2216%22%20height%3D%2216%22%20fill%3D%22%234099FF%22%20fill-rule%3D%22nonzero%22%20rx%3D%222%22%2F%3E%3Cpath%20id%3D%22Path%22%20fill%3D%22%23FFF%22%20fill-rule%3D%22nonzero%22%20d%3D%22M11.5703186%2C3.14417309%20C11.8516238%2C2.73724603%2012.4164781%2C2.62829933%2012.83558%2C2.89774797%20C13.260121%2C3.17069355%2013.3759736%2C3.72932262%2013.0909105%2C4.14168582%20L7.7580587%2C11.8560195%20C7.43776896%2C12.3193404%206.76483983%2C12.3852142%206.35607322%2C11.9948725%20L3.02491697%2C8.8138662%20C2.66090143%2C8.46625845%202.65798871%2C7.89594698%203.01850234%2C7.54483354%20C3.373942%2C7.19866177%203.94940006%2C7.19592841%204.30829608%2C7.5386474%20L6.85276923%2C9.9684299%20L11.5703186%2C3.14417309%20Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A\\\")}[dir=rtl] .tox-checklist>li:not(.tox-checklist--hidden)::before{margin-left:0;margin-right:-1.5em}code[class*=language-],pre[class*=language-]{color:#f8f8f2;background:0 0;text-shadow:0 1px rgba(0,0,0,.3);font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;tab-size:4;-webkit-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:#282a36}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#6272a4}.token.punctuation{color:#f8f8f2}.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#ff79c6}.token.boolean,.token.number{color:#bd93f9}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#50fa7b}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#f1fa8c}.token.keyword{color:#8be9fd}.token.important,.token.regex{color:#ffb86c}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.mce-content-body{overflow-wrap:break-word;word-wrap:break-word}.mce-content-body .mce-visual-caret{background-color:#000;background-color:currentColor;position:absolute}.mce-content-body .mce-visual-caret-hidden{display:none}.mce-content-body [data-mce-caret]{left:-1000px;margin:0;padding:0;position:absolute;right:auto;top:0}.mce-content-body .mce-offscreen-selection{left:-2000000px;max-width:1000000px;position:absolute}.mce-content-body [contentEditable=false]{cursor:default}.mce-content-body [contentEditable=true]{cursor:text}.tox-cursor-format-painter{cursor:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%0A%20%20%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M15%2C6%20C15%2C5.45%2014.55%2C5%2014%2C5%20L6%2C5%20C5.45%2C5%205%2C5.45%205%2C6%20L5%2C10%20C5%2C10.55%205.45%2C11%206%2C11%20L14%2C11%20C14.55%2C11%2015%2C10.55%2015%2C10%20L15%2C9%20L16%2C9%20L16%2C12%20L9%2C12%20L9%2C19%20C9%2C19.55%209.45%2C20%2010%2C20%20L11%2C20%20C11.55%2C20%2012%2C19.55%2012%2C19%20L12%2C14%20L18%2C14%20L18%2C7%20L15%2C7%20L15%2C6%20Z%22%2F%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M1%2C1%20L8.25%2C1%20C8.66421356%2C1%209%2C1.33578644%209%2C1.75%20L9%2C1.75%20C9%2C2.16421356%208.66421356%2C2.5%208.25%2C2.5%20L2.5%2C2.5%20L2.5%2C8.25%20C2.5%2C8.66421356%202.16421356%2C9%201.75%2C9%20L1.75%2C9%20C1.33578644%2C9%201%2C8.66421356%201%2C8.25%20L1%2C1%20Z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E%0A\\\"),default}div.mce-footnotes hr{margin-inline-end:auto;margin-inline-start:0;width:25%}div.mce-footnotes li>a.mce-footnotes-backlink{text-decoration:none}@media print{sup.mce-footnote a{color:#000;text-decoration:none}div.mce-footnotes{break-inside:avoid;width:100%}div.mce-footnotes li>a.mce-footnotes-backlink{display:none}}.mce-content-body figure.align-left{float:left}.mce-content-body figure.align-right{float:right}.mce-content-body figure.image.align-center{display:table;margin-left:auto;margin-right:auto}.mce-preview-object{border:1px solid gray;display:inline-block;line-height:0;margin:0 2px 0 2px;position:relative}.mce-preview-object .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.mce-preview-object[data-mce-selected=\\\"2\\\"] .mce-shim{display:none}.mce-content-body .mce-mergetag{cursor:default!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mce-content-body .mce-mergetag:hover{background-color:rgba(0,108,231,.3)}.mce-content-body .mce-mergetag-affix{background-color:rgba(0,108,231,.3);color:#006ce7}.mce-object{background:transparent url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M4%203h16a1%201%200%200%201%201%201v16a1%201%200%200%201-1%201H4a1%201%200%200%201-1-1V4a1%201%200%200%201%201-1zm1%202v14h14V5H5zm4.79%202.565l5.64%204.028a.5.5%200%200%201%200%20.814l-5.64%204.028a.5.5%200%200%201-.79-.407V7.972a.5.5%200%200%201%20.79-.407z%22%20fill%3D%22%23cccccc%22%2F%3E%3C%2Fsvg%3E%0A\\\") no-repeat center;border:1px dashed #aaa}.mce-pagebreak{border:1px dashed #aaa;cursor:default;display:block;height:5px;margin-top:15px;page-break-before:always;width:100%}@media print{.mce-pagebreak{border:0}}.tiny-pageembed .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.tiny-pageembed[data-mce-selected=\\\"2\\\"] .mce-shim{display:none}.tiny-pageembed{display:inline-block;position:relative}.tiny-pageembed--16by9,.tiny-pageembed--1by1,.tiny-pageembed--21by9,.tiny-pageembed--4by3{display:block;overflow:hidden;padding:0;position:relative;width:100%}.tiny-pageembed--21by9{padding-top:42.857143%}.tiny-pageembed--16by9{padding-top:56.25%}.tiny-pageembed--4by3{padding-top:75%}.tiny-pageembed--1by1{padding-top:100%}.tiny-pageembed--16by9 iframe,.tiny-pageembed--1by1 iframe,.tiny-pageembed--21by9 iframe,.tiny-pageembed--4by3 iframe{border:0;height:100%;left:0;position:absolute;top:0;width:100%}.mce-content-body[data-mce-placeholder]{position:relative}.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before{color:rgba(34,47,62,.7);content:attr(data-mce-placeholder);position:absolute}.mce-content-body:not([dir=rtl])[data-mce-placeholder]:not(.mce-visualblocks)::before{left:1px}.mce-content-body[dir=rtl][data-mce-placeholder]:not(.mce-visualblocks)::before{right:1px}.mce-content-body div.mce-resizehandle{background-color:#4099ff;border-color:#4099ff;border-style:solid;border-width:1px;box-sizing:border-box;height:10px;position:absolute;width:10px;z-index:1298}.mce-content-body div.mce-resizehandle:hover{background-color:#4099ff}.mce-content-body div.mce-resizehandle:nth-of-type(1){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(2){cursor:nesw-resize}.mce-content-body div.mce-resizehandle:nth-of-type(3){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(4){cursor:nesw-resize}.mce-content-body .mce-resize-backdrop{z-index:10000}.mce-content-body .mce-clonedresizable{cursor:default;opacity:.5;outline:1px dashed #000;position:absolute;z-index:10001}.mce-content-body .mce-clonedresizable.mce-resizetable-columns td,.mce-content-body .mce-clonedresizable.mce-resizetable-columns th{border:0}.mce-content-body .mce-resize-helper{background:#555;background:rgba(0,0,0,.75);border:1px;border-radius:3px;color:#fff;display:none;font-family:sans-serif;font-size:12px;line-height:14px;margin:5px 10px;padding:5px;position:absolute;white-space:nowrap;z-index:10002}.tox-rtc-user-selection{position:relative}.tox-rtc-user-cursor{bottom:0;cursor:default;position:absolute;top:0;width:2px}.tox-rtc-user-cursor::before{background-color:inherit;border-radius:50%;content:'';display:block;height:8px;position:absolute;right:-3px;top:-3px;width:8px}.tox-rtc-user-cursor:hover::after{background-color:inherit;border-radius:100px;box-sizing:border-box;color:#fff;content:attr(data-user);display:block;font-size:12px;font-weight:700;left:-5px;min-height:8px;min-width:8px;padding:0 12px;position:absolute;top:-11px;white-space:nowrap;z-index:1000}.tox-rtc-user-selection--1 .tox-rtc-user-cursor{background-color:#2dc26b}.tox-rtc-user-selection--2 .tox-rtc-user-cursor{background-color:#e03e2d}.tox-rtc-user-selection--3 .tox-rtc-user-cursor{background-color:#f1c40f}.tox-rtc-user-selection--4 .tox-rtc-user-cursor{background-color:#3598db}.tox-rtc-user-selection--5 .tox-rtc-user-cursor{background-color:#b96ad9}.tox-rtc-user-selection--6 .tox-rtc-user-cursor{background-color:#e67e23}.tox-rtc-user-selection--7 .tox-rtc-user-cursor{background-color:#aaa69d}.tox-rtc-user-selection--8 .tox-rtc-user-cursor{background-color:#f368e0}.tox-rtc-remote-image{background:#eaeaea url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2236%22%20height%3D%2212%22%20viewBox%3D%220%200%2036%2012%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Ccircle%20cx%3D%226%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2218%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.33s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2230%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.66s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%3C%2Fsvg%3E%0A\\\") no-repeat center center;border:1px solid #ccc;min-height:240px;min-width:320px}.mce-match-marker{background:#aaa;color:#fff}.mce-match-marker-selected{background:#39f;color:#fff}.mce-match-marker-selected::-moz-selection{background:#39f;color:#fff}.mce-match-marker-selected::selection{background:#39f;color:#fff}.mce-content-body audio[data-mce-selected],.mce-content-body details[data-mce-selected],.mce-content-body embed[data-mce-selected],.mce-content-body img[data-mce-selected],.mce-content-body object[data-mce-selected],.mce-content-body table[data-mce-selected],.mce-content-body video[data-mce-selected]{outline:3px solid #4099ff}.mce-content-body hr[data-mce-selected]{outline:3px solid #4099ff;outline-offset:1px}.mce-content-body [contentEditable=false] [contentEditable=true]:focus{outline:3px solid #4099ff}.mce-content-body [contentEditable=false] [contentEditable=true]:hover{outline:3px solid #4099ff}.mce-content-body [contentEditable=false][data-mce-selected]{cursor:not-allowed;outline:3px solid #4099ff}.mce-content-body.mce-content-readonly [contentEditable=true]:focus,.mce-content-body.mce-content-readonly [contentEditable=true]:hover{outline:0}.mce-content-body [data-mce-selected=inline-boundary]{background-color:#4099ff}.mce-content-body .mce-edit-focus{outline:3px solid #4099ff}.mce-content-body td[data-mce-selected],.mce-content-body th[data-mce-selected]{position:relative}.mce-content-body td[data-mce-selected]::-moz-selection,.mce-content-body th[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body td[data-mce-selected]::selection,.mce-content-body th[data-mce-selected]::selection{background:0 0}.mce-content-body td[data-mce-selected] *,.mce-content-body th[data-mce-selected] *{outline:0;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{background-color:rgba(180,215,255,.7);border:1px solid transparent;bottom:-1px;content:'';left:-1px;mix-blend-mode:lighten;position:absolute;right:-1px;top:-1px}@media screen and (-ms-high-contrast:active),(-ms-high-contrast:none){.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{border-color:rgba(0,84,180,.7)}}.mce-content-body img[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body img[data-mce-selected]::selection{background:0 0}.ephox-snooker-resizer-bar{background-color:#4099ff;opacity:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ephox-snooker-resizer-cols{cursor:col-resize}.ephox-snooker-resizer-rows{cursor:row-resize}.ephox-snooker-resizer-bar.ephox-snooker-resizer-bar-dragging{opacity:1}.mce-spellchecker-word{background-image:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%23ff0000'%20fill%3D'none'%20stroke-linecap%3D'round'%20stroke-opacity%3D'.75'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A\\\");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default;height:2rem}.mce-spellchecker-grammar{background-image:url(\\\"data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%2300A835'%20fill%3D'none'%20stroke-linecap%3D'round'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A\\\");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc ul>li{list-style-type:none}[data-mce-block]{display:block}.mce-item-table:not([border]),.mce-item-table:not([border]) caption,.mce-item-table:not([border]) td,.mce-item-table:not([border]) th,.mce-item-table[border=\\\"0\\\"],.mce-item-table[border=\\\"0\\\"] caption,.mce-item-table[border=\\\"0\\\"] td,.mce-item-table[border=\\\"0\\\"] th,table[style*=\\\"border-width: 0px\\\"],table[style*=\\\"border-width: 0px\\\"] caption,table[style*=\\\"border-width: 0px\\\"] td,table[style*=\\\"border-width: 0px\\\"] th{border:1px dashed #bbb}.mce-visualblocks address,.mce-visualblocks article,.mce-visualblocks aside,.mce-visualblocks blockquote,.mce-visualblocks div:not([data-mce-bogus]),.mce-visualblocks dl,.mce-visualblocks figcaption,.mce-visualblocks figure,.mce-visualblocks h1,.mce-visualblocks h2,.mce-visualblocks h3,.mce-visualblocks h4,.mce-visualblocks h5,.mce-visualblocks h6,.mce-visualblocks hgroup,.mce-visualblocks ol,.mce-visualblocks p,.mce-visualblocks pre,.mce-visualblocks section,.mce-visualblocks ul{background-repeat:no-repeat;border:1px dashed #bbb;margin-left:3px;padding-top:10px}.mce-visualblocks p{background-image:url(data:image/gif;base64,R0lGODlhCQAJAJEAAAAAAP///7u7u////yH5BAEAAAMALAAAAAAJAAkAAAIQnG+CqCN/mlyvsRUpThG6AgA7)}.mce-visualblocks h1{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGu1JuxHoAfRNRW3TWXyF2YiRUAOw==)}.mce-visualblocks h2{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8Hybbx4oOuqgTynJd6bGlWg3DkJzoaUAAAOw==)}.mce-visualblocks h3{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIZjI8Hybbx4oOuqgTynJf2Ln2NOHpQpmhAAQA7)}.mce-visualblocks h4{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxInR0zqeAdhtJlXwV1oCll2HaWgAAOw==)}.mce-visualblocks h5{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjane4iq5GlW05GgIkIZUAAAOw==)}.mce-visualblocks h6{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjan04jep1iZ1XRlAo5bVgAAOw==)}.mce-visualblocks div:not([data-mce-bogus]){background-image:url(data:image/gif;base64,R0lGODlhEgAKAIABALu7u////yH5BAEAAAEALAAAAAASAAoAAAIfjI9poI0cgDywrhuxfbrzDEbQM2Ei5aRjmoySW4pAAQA7)}.mce-visualblocks section{background-image:url(data:image/gif;base64,R0lGODlhKAAKAIABALu7u////yH5BAEAAAEALAAAAAAoAAoAAAI5jI+pywcNY3sBWHdNrplytD2ellDeSVbp+GmWqaDqDMepc8t17Y4vBsK5hDyJMcI6KkuYU+jpjLoKADs=)}.mce-visualblocks article{background-image:url(data:image/gif;base64,R0lGODlhKgAKAIABALu7u////yH5BAEAAAEALAAAAAAqAAoAAAI6jI+pywkNY3wG0GBvrsd2tXGYSGnfiF7ikpXemTpOiJScasYoDJJrjsG9gkCJ0ag6KhmaIe3pjDYBBQA7)}.mce-visualblocks blockquote{background-image:url(data:image/gif;base64,R0lGODlhPgAKAIABALu7u////yH5BAEAAAEALAAAAAA+AAoAAAJPjI+py+0Knpz0xQDyuUhvfoGgIX5iSKZYgq5uNL5q69asZ8s5rrf0yZmpNkJZzFesBTu8TOlDVAabUyatguVhWduud3EyiUk45xhTTgMBBQA7)}.mce-visualblocks address{background-image:url(data:image/gif;base64,R0lGODlhLQAKAIABALu7u////yH5BAEAAAEALAAAAAAtAAoAAAI/jI+pywwNozSP1gDyyZcjb3UaRpXkWaXmZW4OqKLhBmLs+K263DkJK7OJeifh7FicKD9A1/IpGdKkyFpNmCkAADs=)}.mce-visualblocks pre{background-image:url(data:image/gif;base64,R0lGODlhFQAKAIABALu7uwAAACH5BAEAAAEALAAAAAAVAAoAAAIjjI+ZoN0cgDwSmnpz1NCueYERhnibZVKLNnbOq8IvKpJtVQAAOw==)}.mce-visualblocks figure{background-image:url(data:image/gif;base64,R0lGODlhJAAKAIAAALu7u////yH5BAEAAAEALAAAAAAkAAoAAAI0jI+py+2fwAHUSFvD3RlvG4HIp4nX5JFSpnZUJ6LlrM52OE7uSWosBHScgkSZj7dDKnWAAgA7)}.mce-visualblocks figcaption{border:1px dashed #bbb}.mce-visualblocks hgroup{background-image:url(data:image/gif;base64,R0lGODlhJwAKAIABALu7uwAAACH5BAEAAAEALAAAAAAnAAoAAAI3jI+pywYNI3uB0gpsRtt5fFnfNZaVSYJil4Wo03Hv6Z62uOCgiXH1kZIIJ8NiIxRrAZNMZAtQAAA7)}.mce-visualblocks aside{background-image:url(data:image/gif;base64,R0lGODlhHgAKAIABAKqqqv///yH5BAEAAAEALAAAAAAeAAoAAAItjI+pG8APjZOTzgtqy7I3f1yehmQcFY4WKZbqByutmW4aHUd6vfcVbgudgpYCADs=)}.mce-visualblocks ul{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIAAALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGuYnqUVSjvw26DzzXiqIDlVwAAOw==)}.mce-visualblocks ol{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybH6HHt0qourxC6CvzXieHyeWQAAOw==)}.mce-visualblocks dl{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybEOnmOvUoWznTqeuEjNSCqeGRUAOw==)}.mce-visualblocks:not([dir=rtl]) address,.mce-visualblocks:not([dir=rtl]) article,.mce-visualblocks:not([dir=rtl]) aside,.mce-visualblocks:not([dir=rtl]) blockquote,.mce-visualblocks:not([dir=rtl]) div:not([data-mce-bogus]),.mce-visualblocks:not([dir=rtl]) dl,.mce-visualblocks:not([dir=rtl]) figcaption,.mce-visualblocks:not([dir=rtl]) figure,.mce-visualblocks:not([dir=rtl]) h1,.mce-visualblocks:not([dir=rtl]) h2,.mce-visualblocks:not([dir=rtl]) h3,.mce-visualblocks:not([dir=rtl]) h4,.mce-visualblocks:not([dir=rtl]) h5,.mce-visualblocks:not([dir=rtl]) h6,.mce-visualblocks:not([dir=rtl]) hgroup,.mce-visualblocks:not([dir=rtl]) ol,.mce-visualblocks:not([dir=rtl]) p,.mce-visualblocks:not([dir=rtl]) pre,.mce-visualblocks:not([dir=rtl]) section,.mce-visualblocks:not([dir=rtl]) ul{margin-left:3px}.mce-visualblocks[dir=rtl] address,.mce-visualblocks[dir=rtl] article,.mce-visualblocks[dir=rtl] aside,.mce-visualblocks[dir=rtl] blockquote,.mce-visualblocks[dir=rtl] div:not([data-mce-bogus]),.mce-visualblocks[dir=rtl] dl,.mce-visualblocks[dir=rtl] figcaption,.mce-visualblocks[dir=rtl] figure,.mce-visualblocks[dir=rtl] h1,.mce-visualblocks[dir=rtl] h2,.mce-visualblocks[dir=rtl] h3,.mce-visualblocks[dir=rtl] h4,.mce-visualblocks[dir=rtl] h5,.mce-visualblocks[dir=rtl] h6,.mce-visualblocks[dir=rtl] hgroup,.mce-visualblocks[dir=rtl] ol,.mce-visualblocks[dir=rtl] p,.mce-visualblocks[dir=rtl] pre,.mce-visualblocks[dir=rtl] section,.mce-visualblocks[dir=rtl] ul{background-position-x:right;margin-right:3px}.mce-nbsp,.mce-shy{background:#aaa}.mce-shy::after{content:'-'}body{font-family:sans-serif}table{border-collapse:collapse}\")\n//# sourceMappingURL=content.js.map\n"
  },
  {
    "path": "public/libs/tinymce/skins/ui/tinymce-5-dark/skin.js",
    "content": "tinymce.Resource.add('ui/tinymce-5-dark/skin.css', \".tox{box-shadow:none;box-sizing:content-box;color:#2a3746;cursor:auto;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:16px;font-style:normal;font-weight:400;line-height:normal;-webkit-tap-highlight-color:transparent;text-decoration:none;text-shadow:none;text-transform:none;vertical-align:initial;white-space:normal}.tox :not(svg):not(rect){box-sizing:inherit;color:inherit;cursor:inherit;direction:inherit;font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;line-height:inherit;-webkit-tap-highlight-color:inherit;text-align:inherit;text-decoration:inherit;text-shadow:inherit;text-transform:inherit;vertical-align:inherit;white-space:inherit}.tox :not(svg):not(rect){background:0 0;border:0;box-shadow:none;float:none;height:auto;margin:0;max-width:none;outline:0;padding:0;position:static;width:auto}.tox:not([dir=rtl]){direction:ltr;text-align:left}.tox[dir=rtl]{direction:rtl;text-align:right}.tox-tinymce{border:1px solid #000;border-radius:0;box-shadow:none;box-sizing:border-box;display:flex;flex-direction:column;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;overflow:hidden;position:relative;visibility:inherit!important}.tox.tox-tinymce-inline{border:none;box-shadow:none;overflow:initial}.tox.tox-tinymce-inline .tox-editor-container{overflow:initial}.tox.tox-tinymce-inline .tox-editor-header{background-color:#222f3e;border:1px solid #000;border-radius:0;box-shadow:none;overflow:hidden}.tox-tinymce-aux{font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;z-index:1300}.tox-tinymce :focus,.tox-tinymce-aux :focus{outline:0}button::-moz-focus-inner{border:0}.tox[dir=rtl] .tox-icon--flip svg{transform:rotateY(180deg)}.tox .accessibility-issue__header{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description{align-items:stretch;border-radius:3px;display:flex;justify-content:space-between}.tox .accessibility-issue__description>div{padding-bottom:4px}.tox .accessibility-issue__description>div>div{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description>div>div .tox-icon svg{display:block}.tox .accessibility-issue__repair{margin-top:16px}.tox .tox-dialog__body-content .accessibility-issue--info .accessibility-issue__description{background-color:rgba(30,113,170,.4);color:#fff}.tox .tox-dialog__body-content .accessibility-issue--info .tox-form__group h2{color:#fff}.tox .tox-dialog__body-content .accessibility-issue--info .tox-icon svg{fill:#fff}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon{background-color:#207ab7;color:#fff}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:hover{background-color:#1c6ca1}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:active{background-color:#185d8c}.tox .tox-dialog__body-content .accessibility-issue--warn .accessibility-issue__description{background-color:rgba(255,165,0,.5);color:#fff}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-form__group h2{color:#fff}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-icon svg{fill:#fff}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon{background-color:#ffe89d;color:#2a3746}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:hover{background-color:#f2d574;color:#2a3746}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:active{background-color:#e8c657;color:#2a3746}.tox .tox-dialog__body-content .accessibility-issue--error .accessibility-issue__description{background-color:rgba(204,0,0,.5);color:#fff}.tox .tox-dialog__body-content .accessibility-issue--error .tox-form__group h2{color:#fff}.tox .tox-dialog__body-content .accessibility-issue--error .tox-icon svg{fill:#fff}.tox .tox-dialog__body-content .accessibility-issue--error a.tox-button--naked.tox-button--icon{background-color:#f2bfbf;color:#2a3746}.tox .tox-dialog__body-content .accessibility-issue--error a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--error a.tox-button--naked.tox-button--icon:hover{background-color:#e9a4a4;color:#2a3746}.tox .tox-dialog__body-content .accessibility-issue--error a.tox-button--naked.tox-button--icon:active{background-color:#ee9494;color:#2a3746}.tox .tox-dialog__body-content .accessibility-issue--success .accessibility-issue__description{background-color:rgba(120,171,70,.5);color:#fff}.tox .tox-dialog__body-content .accessibility-issue--success .accessibility-issue__description>:last-child{display:none}.tox .tox-dialog__body-content .accessibility-issue--success .tox-form__group h2{color:#fff}.tox .tox-dialog__body-content .accessibility-issue--success .tox-icon svg{fill:#fff}.tox .tox-dialog__body-content .accessibility-issue__header .tox-form__group h1,.tox .tox-dialog__body-content .tox-form__group .accessibility-issue__description h2{font-size:14px;margin-top:0}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__header .tox-button{margin-left:4px}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__header>:nth-last-child(2){margin-left:auto}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__description{padding:4px 4px 4px 8px}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__header .tox-button{margin-right:4px}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__header>:nth-last-child(2){margin-right:auto}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__description{padding:4px 8px 4px 4px}.tox .tox-advtemplate .tox-form__grid{flex:1}.tox .tox-advtemplate .tox-form__grid>div:first-child{display:flex;flex-direction:column;width:30%}.tox .tox-advtemplate .tox-form__grid>div:first-child>div:nth-child(2){flex-basis:0;flex-grow:1;overflow:auto}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-advtemplate .tox-form__grid>div:first-child{width:100%}}.tox .tox-advtemplate iframe{border-color:#000;border-radius:0;border-style:solid;border-width:1px;margin:0 10px}.tox .tox-anchorbar{display:flex;flex:0 0 auto}.tox .tox-bottom-anchorbar{display:flex;flex:0 0 auto}.tox .tox-bar{display:flex;flex:0 0 auto}.tox .tox-button{background-color:#207ab7;background-image:none;background-position:0 0;background-repeat:repeat;border-color:#207ab7;border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;color:#fff;cursor:pointer;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:14px;font-style:normal;font-weight:700;letter-spacing:normal;line-height:24px;margin:0;outline:0;padding:4px 16px;position:relative;text-align:center;text-decoration:none;text-transform:none;white-space:nowrap}.tox .tox-button::before{border-radius:3px;bottom:-1px;box-shadow:inset 0 0 0 2px #fff,0 0 0 1px #207ab7,0 0 0 3px rgba(32,122,183,.25);content:'';left:-1px;opacity:0;pointer-events:none;position:absolute;right:-1px;top:-1px}.tox .tox-button[disabled]{background-color:#207ab7;background-image:none;border-color:#207ab7;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-button:focus:not(:disabled){background-color:#1c6ca1;background-image:none;border-color:#1c6ca1;box-shadow:none;color:#fff}.tox .tox-button:focus-visible:not(:disabled)::before{opacity:1}.tox .tox-button:hover:not(:disabled){background-color:#1c6ca1;background-image:none;border-color:#1c6ca1;box-shadow:none;color:#fff}.tox .tox-button:active:not(:disabled){background-color:#185d8c;background-image:none;border-color:#185d8c;box-shadow:none;color:#fff}.tox .tox-button.tox-button--enabled{background-color:#185d8c;background-image:none;border-color:#185d8c;box-shadow:none;color:#fff}.tox .tox-button.tox-button--enabled[disabled]{background-color:#185d8c;background-image:none;border-color:#185d8c;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-button.tox-button--enabled:focus:not(:disabled){background-color:#154f76;background-image:none;border-color:#154f76;box-shadow:none;color:#fff}.tox .tox-button.tox-button--enabled:hover:not(:disabled){background-color:#154f76;background-image:none;border-color:#154f76;box-shadow:none;color:#fff}.tox .tox-button.tox-button--enabled:active:not(:disabled){background-color:#114060;background-image:none;border-color:#114060;box-shadow:none;color:#fff}.tox .tox-button--icon-and-text,.tox .tox-button.tox-button--icon-and-text,.tox .tox-button.tox-button--secondary.tox-button--icon-and-text{display:flex;padding:5px 4px}.tox .tox-button--icon-and-text .tox-icon svg,.tox .tox-button.tox-button--icon-and-text .tox-icon svg,.tox .tox-button.tox-button--secondary.tox-button--icon-and-text .tox-icon svg{display:block;fill:currentColor}.tox .tox-button--secondary{background-color:#3d546f;background-image:none;background-position:0 0;background-repeat:repeat;border-color:#3d546f;border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;color:#fff;font-size:14px;font-style:normal;font-weight:700;letter-spacing:normal;outline:0;padding:4px 16px;text-decoration:none;text-transform:none}.tox .tox-button--secondary[disabled]{background-color:#3d546f;background-image:none;border-color:#3d546f;box-shadow:none;color:rgba(255,255,255,.5)}.tox .tox-button--secondary:focus:not(:disabled){background-color:#34485f;background-image:none;border-color:#34485f;box-shadow:none;color:#fff}.tox .tox-button--secondary:hover:not(:disabled){background-color:#34485f;background-image:none;border-color:#34485f;box-shadow:none;color:#fff}.tox .tox-button--secondary:active:not(:disabled){background-color:#2b3b4e;background-image:none;border-color:#2b3b4e;box-shadow:none;color:#fff}.tox .tox-button--secondary.tox-button--enabled{background-color:#346085;background-image:none;border-color:#346085;box-shadow:none;color:#fff}.tox .tox-button--secondary.tox-button--enabled[disabled]{background-color:#346085;background-image:none;border-color:#346085;box-shadow:none;color:rgba(255,255,255,.5)}.tox .tox-button--secondary.tox-button--enabled:focus:not(:disabled){background-color:#2d5373;background-image:none;border-color:#2d5373;box-shadow:none;color:#fff}.tox .tox-button--secondary.tox-button--enabled:hover:not(:disabled){background-color:#2d5373;background-image:none;border-color:#2d5373;box-shadow:none;color:#fff}.tox .tox-button--secondary.tox-button--enabled:active:not(:disabled){background-color:#264560;background-image:none;border-color:#264560;box-shadow:none;color:#fff}.tox .tox-button--icon,.tox .tox-button.tox-button--icon,.tox .tox-button.tox-button--secondary.tox-button--icon{padding:4px}.tox .tox-button--icon .tox-icon svg,.tox .tox-button.tox-button--icon .tox-icon svg,.tox .tox-button.tox-button--secondary.tox-button--icon .tox-icon svg{display:block;fill:currentColor}.tox .tox-button-link{background:0;border:none;box-sizing:border-box;cursor:pointer;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:16px;font-weight:400;line-height:1.3;margin:0;padding:0;white-space:nowrap}.tox .tox-button-link--sm{font-size:14px}.tox .tox-button--naked{background-color:transparent;border-color:transparent;box-shadow:unset;color:#fff}.tox .tox-button--naked[disabled]{background-color:#3d546f;border-color:#3d546f;box-shadow:none;color:rgba(255,255,255,.5)}.tox .tox-button--naked:hover:not(:disabled){background-color:#34485f;border-color:#34485f;box-shadow:none;color:#fff}.tox .tox-button--naked:focus:not(:disabled){background-color:#34485f;border-color:#34485f;box-shadow:none;color:#fff}.tox .tox-button--naked:active:not(:disabled){background-color:#2b3b4e;border-color:#2b3b4e;box-shadow:none;color:#fff}.tox .tox-button--naked .tox-icon svg{fill:currentColor}.tox .tox-button--naked.tox-button--icon:hover:not(:disabled){color:#fff}.tox .tox-checkbox{align-items:center;border-radius:3px;cursor:pointer;display:flex;height:36px;min-width:36px}.tox .tox-checkbox__input{height:1px;overflow:hidden;position:absolute;top:auto;width:1px}.tox .tox-checkbox__icons{align-items:center;border-radius:3px;box-shadow:0 0 0 2px transparent;box-sizing:content-box;display:flex;height:24px;justify-content:center;padding:calc(4px - 1px);width:24px}.tox .tox-checkbox__icons .tox-checkbox-icon__unchecked svg{display:block;fill:rgba(255,255,255,.2)}.tox .tox-checkbox__icons .tox-checkbox-icon__indeterminate svg{display:none;fill:#207ab7}.tox .tox-checkbox__icons .tox-checkbox-icon__checked svg{display:none;fill:#207ab7}.tox .tox-checkbox--disabled{color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-checkbox--disabled .tox-checkbox__icons .tox-checkbox-icon__checked svg{fill:rgba(255,255,255,.5)}.tox .tox-checkbox--disabled .tox-checkbox__icons .tox-checkbox-icon__unchecked svg{fill:rgba(255,255,255,.5)}.tox .tox-checkbox--disabled .tox-checkbox__icons .tox-checkbox-icon__indeterminate svg{fill:rgba(255,255,255,.5)}.tox input.tox-checkbox__input:checked+.tox-checkbox__icons .tox-checkbox-icon__unchecked svg{display:none}.tox input.tox-checkbox__input:checked+.tox-checkbox__icons .tox-checkbox-icon__checked svg{display:block}.tox input.tox-checkbox__input:indeterminate+.tox-checkbox__icons .tox-checkbox-icon__unchecked svg{display:none}.tox input.tox-checkbox__input:indeterminate+.tox-checkbox__icons .tox-checkbox-icon__indeterminate svg{display:block}.tox input.tox-checkbox__input:focus+.tox-checkbox__icons{border-radius:3px;box-shadow:inset 0 0 0 1px #207ab7;padding:calc(4px - 1px)}.tox:not([dir=rtl]) .tox-checkbox__label{margin-left:4px}.tox:not([dir=rtl]) .tox-checkbox__input{left:-10000px}.tox:not([dir=rtl]) .tox-bar .tox-checkbox{margin-left:4px}.tox[dir=rtl] .tox-checkbox__label{margin-right:4px}.tox[dir=rtl] .tox-checkbox__input{right:-10000px}.tox[dir=rtl] .tox-bar .tox-checkbox{margin-right:4px}.tox .tox-collection--toolbar .tox-collection__group{display:flex;padding:0}.tox .tox-collection--grid .tox-collection__group{display:flex;flex-wrap:wrap;max-height:208px;overflow-x:hidden;overflow-y:auto;padding:0}.tox .tox-collection--list .tox-collection__group{border-bottom-width:0;border-color:#1a1a1a;border-left-width:0;border-right-width:0;border-style:solid;border-top-width:1px;padding:4px 0}.tox .tox-collection--list .tox-collection__group:first-child{border-top-width:0}.tox .tox-collection__group-heading{background-color:#333;color:#fff;cursor:default;font-size:12px;font-style:normal;font-weight:400;margin-bottom:4px;margin-top:-4px;padding:4px 8px;text-transform:none;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.tox .tox-collection__item{align-items:center;border-radius:3px;color:#fff;display:flex;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.tox .tox-collection--list .tox-collection__item{padding:4px 8px}.tox .tox-collection--toolbar .tox-collection__item{border-radius:3px;padding:4px}.tox .tox-collection--grid .tox-collection__item{border-radius:3px;padding:4px}.tox .tox-collection--list .tox-collection__item--enabled{background-color:#2b3b4e;color:#fff}.tox .tox-collection--list .tox-collection__item--active{background-color:#4a5562}.tox .tox-collection--toolbar .tox-collection__item--enabled{background-color:#757d87;color:#fff}.tox .tox-collection--toolbar .tox-collection__item--active{background-color:#4a5562}.tox .tox-collection--grid .tox-collection__item--enabled{background-color:#757d87;color:#fff}.tox .tox-collection--grid .tox-collection__item--active:not(.tox-collection__item--state-disabled){background-color:#4a5562;color:#fff}.tox .tox-collection--list .tox-collection__item--active:not(.tox-collection__item--state-disabled){color:#fff}.tox .tox-collection--toolbar .tox-collection__item--active:not(.tox-collection__item--state-disabled){color:#fff}.tox .tox-collection__item-checkmark,.tox .tox-collection__item-icon{align-items:center;display:flex;height:24px;justify-content:center;width:24px}.tox .tox-collection__item-checkmark svg,.tox .tox-collection__item-icon svg{fill:currentColor}.tox .tox-collection--toolbar-lg .tox-collection__item-icon{height:48px;width:48px}.tox .tox-collection__item-label{color:currentColor;display:inline-block;flex:1;font-size:14px;font-style:normal;font-weight:400;line-height:24px;max-width:100%;text-transform:none;word-break:break-all}.tox .tox-collection__item-accessory{color:rgba(255,255,255,.5);display:inline-block;font-size:14px;height:24px;line-height:24px;text-transform:none}.tox .tox-collection__item-caret{align-items:center;display:flex;min-height:24px}.tox .tox-collection__item-caret::after{content:'';font-size:0;min-height:inherit}.tox .tox-collection__item-caret svg{fill:#fff}.tox .tox-collection__item--state-disabled{background-color:transparent;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-collection__item--state-disabled .tox-collection__item-caret svg{fill:rgba(255,255,255,.5)}.tox .tox-collection--list .tox-collection__item:not(.tox-collection__item--enabled) .tox-collection__item-checkmark svg{display:none}.tox .tox-collection--list .tox-collection__item:not(.tox-collection__item--enabled) .tox-collection__item-accessory+.tox-collection__item-checkmark{display:none}.tox .tox-collection--horizontal{background-color:#2b3b4e;border:1px solid #1a1a1a;border-radius:3px;box-shadow:0 0 2px 0 rgba(42,55,70,.2),0 4px 8px 0 rgba(42,55,70,.15);display:flex;flex:0 0 auto;flex-shrink:0;flex-wrap:nowrap;margin-bottom:0;overflow-x:auto;padding:0}.tox .tox-collection--horizontal .tox-collection__group{align-items:center;display:flex;flex-wrap:nowrap;margin:0;padding:0 4px}.tox .tox-collection--horizontal .tox-collection__item{height:34px;margin:3px 0 2px 0;padding:0 4px}.tox .tox-collection--horizontal .tox-collection__item-label{white-space:nowrap}.tox .tox-collection--horizontal .tox-collection__item-caret{margin-left:4px}.tox .tox-collection__item-container{display:flex}.tox .tox-collection__item-container--row{align-items:center;flex:1 1 auto;flex-direction:row}.tox .tox-collection__item-container--row.tox-collection__item-container--align-left{margin-right:auto}.tox .tox-collection__item-container--row.tox-collection__item-container--align-right{justify-content:flex-end;margin-left:auto}.tox .tox-collection__item-container--row.tox-collection__item-container--valign-top{align-items:flex-start;margin-bottom:auto}.tox .tox-collection__item-container--row.tox-collection__item-container--valign-middle{align-items:center}.tox .tox-collection__item-container--row.tox-collection__item-container--valign-bottom{align-items:flex-end;margin-top:auto}.tox .tox-collection__item-container--column{align-self:center;flex:1 1 auto;flex-direction:column}.tox .tox-collection__item-container--column.tox-collection__item-container--align-left{align-items:flex-start}.tox .tox-collection__item-container--column.tox-collection__item-container--align-right{align-items:flex-end}.tox .tox-collection__item-container--column.tox-collection__item-container--valign-top{align-self:flex-start}.tox .tox-collection__item-container--column.tox-collection__item-container--valign-middle{align-self:center}.tox .tox-collection__item-container--column.tox-collection__item-container--valign-bottom{align-self:flex-end}.tox:not([dir=rtl]) .tox-collection--horizontal .tox-collection__group:not(:last-of-type){border-right:1px solid #000}.tox:not([dir=rtl]) .tox-collection--list .tox-collection__item>:not(:first-child){margin-left:8px}.tox:not([dir=rtl]) .tox-collection--list .tox-collection__item>.tox-collection__item-label:first-child{margin-left:4px}.tox:not([dir=rtl]) .tox-collection__item-accessory{margin-left:16px;text-align:right}.tox:not([dir=rtl]) .tox-collection .tox-collection__item-caret{margin-left:16px}.tox[dir=rtl] .tox-collection--horizontal .tox-collection__group:not(:last-of-type){border-left:1px solid #000}.tox[dir=rtl] .tox-collection--list .tox-collection__item>:not(:first-child){margin-right:8px}.tox[dir=rtl] .tox-collection--list .tox-collection__item>.tox-collection__item-label:first-child{margin-right:4px}.tox[dir=rtl] .tox-collection__item-accessory{margin-right:16px;text-align:left}.tox[dir=rtl] .tox-collection .tox-collection__item-caret{margin-right:16px;transform:rotateY(180deg)}.tox[dir=rtl] .tox-collection--horizontal .tox-collection__item-caret{margin-right:4px}.tox .tox-color-picker-container{display:flex;flex-direction:row;height:225px;margin:0}.tox .tox-sv-palette{box-sizing:border-box;display:flex;height:100%}.tox .tox-sv-palette-spectrum{height:100%}.tox .tox-sv-palette,.tox .tox-sv-palette-spectrum{width:225px}.tox .tox-sv-palette-thumb{background:0 0;border:1px solid #000;border-radius:50%;box-sizing:content-box;height:12px;position:absolute;width:12px}.tox .tox-sv-palette-inner-thumb{border:1px solid #fff;border-radius:50%;height:10px;position:absolute;width:10px}.tox .tox-hue-slider{box-sizing:border-box;height:100%;width:25px}.tox .tox-hue-slider-spectrum{background:linear-gradient(to bottom,red,#ff0080,#f0f,#8000ff,#00f,#0080ff,#0ff,#00ff80,#0f0,#80ff00,#ff0,#ff8000,red);height:100%;width:100%}.tox .tox-hue-slider,.tox .tox-hue-slider-spectrum{width:20px}.tox .tox-hue-slider-spectrum:focus,.tox .tox-sv-palette-spectrum:focus{outline:#08f solid}.tox .tox-hue-slider-thumb{background:#fff;border:1px solid #000;box-sizing:content-box;height:4px;width:100%}.tox .tox-rgb-form{display:flex;flex-direction:column;justify-content:space-between}.tox .tox-rgb-form div{align-items:center;display:flex;justify-content:space-between;margin-bottom:5px;width:inherit}.tox .tox-rgb-form input{width:6em}.tox .tox-rgb-form input.tox-invalid{border:1px solid red!important}.tox .tox-rgb-form .tox-rgba-preview{border:1px solid #000;flex-grow:2;margin-bottom:0}.tox:not([dir=rtl]) .tox-sv-palette{margin-right:15px}.tox:not([dir=rtl]) .tox-hue-slider{margin-right:15px}.tox:not([dir=rtl]) .tox-hue-slider-thumb{margin-left:-1px}.tox:not([dir=rtl]) .tox-rgb-form label{margin-right:.5em}.tox[dir=rtl] .tox-sv-palette{margin-left:15px}.tox[dir=rtl] .tox-hue-slider{margin-left:15px}.tox[dir=rtl] .tox-hue-slider-thumb{margin-right:-1px}.tox[dir=rtl] .tox-rgb-form label{margin-left:.5em}.tox .tox-toolbar .tox-swatches,.tox .tox-toolbar__overflow .tox-swatches,.tox .tox-toolbar__primary .tox-swatches{margin:2px 0 3px 4px}.tox .tox-collection--list .tox-collection__group .tox-swatches-menu{border:0;margin:-4px 0}.tox .tox-swatches__row{display:flex}.tox .tox-swatch{height:30px;transition:transform .15s,box-shadow .15s;width:30px}.tox .tox-swatch:focus,.tox .tox-swatch:hover{box-shadow:0 0 0 1px rgba(127,127,127,.3) inset;transform:scale(.8)}.tox .tox-swatch--remove{align-items:center;display:flex;justify-content:center}.tox .tox-swatch--remove svg path{stroke:#e74c3c}.tox .tox-swatches__picker-btn{align-items:center;background-color:transparent;border:0;cursor:pointer;display:flex;height:30px;justify-content:center;outline:0;padding:0;width:30px}.tox .tox-swatches__picker-btn svg{fill:#fff;height:24px;width:24px}.tox .tox-swatches__picker-btn:hover{background:#4a5562}.tox div.tox-swatch:not(.tox-swatch--remove) svg{display:none;fill:#fff;height:24px;margin:calc((30px - 24px)/ 2) calc((30px - 24px)/ 2);width:24px}.tox div.tox-swatch:not(.tox-swatch--remove) svg path{fill:#fff;paint-order:stroke;stroke:#222f3e;stroke-width:2px}.tox div.tox-swatch:not(.tox-swatch--remove).tox-collection__item--enabled svg{display:block}.tox:not([dir=rtl]) .tox-swatches__picker-btn{margin-left:auto}.tox[dir=rtl] .tox-swatches__picker-btn{margin-right:auto}.tox .tox-comment-thread{background:#2b3b4e;position:relative}.tox .tox-comment-thread>:not(:first-child){margin-top:8px}.tox .tox-comment{background:#2b3b4e;border:1px solid #000;border-radius:3px;box-shadow:0 4px 8px 0 rgba(42,55,70,.1);padding:8px 8px 16px 8px;position:relative}.tox .tox-comment__header{align-items:center;color:#fff;display:flex;justify-content:space-between}.tox .tox-comment__date{color:#fff;font-size:12px;line-height:18px}.tox .tox-comment__body{color:#fff;font-size:14px;font-style:normal;font-weight:400;line-height:1.3;margin-top:8px;position:relative;text-transform:initial}.tox .tox-comment__body textarea{resize:none;white-space:normal;width:100%}.tox .tox-comment__expander{padding-top:8px}.tox .tox-comment__expander p{color:rgba(255,255,255,.5);font-size:14px;font-style:normal}.tox .tox-comment__body p{margin:0}.tox .tox-comment__buttonspacing{padding-top:16px;text-align:center}.tox .tox-comment-thread__overlay::after{background:#2b3b4e;bottom:0;content:\\\"\\\";display:flex;left:0;opacity:.9;position:absolute;right:0;top:0;z-index:5}.tox .tox-comment__reply{display:flex;flex-shrink:0;flex-wrap:wrap;justify-content:flex-end;margin-top:8px}.tox .tox-comment__reply>:first-child{margin-bottom:8px;width:100%}.tox .tox-comment__edit{display:flex;flex-wrap:wrap;justify-content:flex-end;margin-top:16px}.tox .tox-comment__gradient::after{background:linear-gradient(rgba(43,59,78,0),#2b3b4e);bottom:0;content:\\\"\\\";display:block;height:5em;margin-top:-40px;position:absolute;width:100%}.tox .tox-comment__overlay{background:#2b3b4e;bottom:0;display:flex;flex-direction:column;flex-grow:1;left:0;opacity:.9;position:absolute;right:0;text-align:center;top:0;z-index:5}.tox .tox-comment__loading-text{align-items:center;color:#fff;display:flex;flex-direction:column;position:relative}.tox .tox-comment__loading-text>div{padding-bottom:16px}.tox .tox-comment__overlaytext{bottom:0;flex-direction:column;font-size:14px;left:0;padding:1em;position:absolute;right:0;top:0;z-index:10}.tox .tox-comment__overlaytext p{background-color:#2b3b4e;box-shadow:0 0 8px 8px #2b3b4e;color:#fff;text-align:center}.tox .tox-comment__overlaytext div:nth-of-type(2){font-size:.8em}.tox .tox-comment__busy-spinner{align-items:center;background-color:#2b3b4e;bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0;z-index:20}.tox .tox-comment__scroll{display:flex;flex-direction:column;flex-shrink:1;overflow:auto}.tox .tox-conversations{margin:8px}.tox:not([dir=rtl]) .tox-comment__edit{margin-left:8px}.tox:not([dir=rtl]) .tox-comment__buttonspacing>:last-child,.tox:not([dir=rtl]) .tox-comment__edit>:last-child,.tox:not([dir=rtl]) .tox-comment__reply>:last-child{margin-left:8px}.tox[dir=rtl] .tox-comment__edit{margin-right:8px}.tox[dir=rtl] .tox-comment__buttonspacing>:last-child,.tox[dir=rtl] .tox-comment__edit>:last-child,.tox[dir=rtl] .tox-comment__reply>:last-child{margin-right:8px}.tox .tox-user{align-items:center;display:flex}.tox .tox-user__avatar svg{fill:rgba(255,255,255,.5)}.tox .tox-user__avatar img{border-radius:50%;height:36px;object-fit:cover;vertical-align:middle;width:36px}.tox .tox-user__name{color:#fff;font-size:14px;font-style:normal;font-weight:700;line-height:18px;text-transform:none}.tox:not([dir=rtl]) .tox-user__avatar img,.tox:not([dir=rtl]) .tox-user__avatar svg{margin-right:8px}.tox:not([dir=rtl]) .tox-user__avatar+.tox-user__name{margin-left:8px}.tox[dir=rtl] .tox-user__avatar img,.tox[dir=rtl] .tox-user__avatar svg{margin-left:8px}.tox[dir=rtl] .tox-user__avatar+.tox-user__name{margin-right:8px}.tox .tox-dialog-wrap{align-items:center;bottom:0;display:flex;justify-content:center;left:0;position:fixed;right:0;top:0;z-index:1100}.tox .tox-dialog-wrap__backdrop{background-color:rgba(34,47,62,.75);bottom:0;left:0;position:absolute;right:0;top:0;z-index:1}.tox .tox-dialog-wrap__backdrop--opaque{background-color:#222f3e}.tox .tox-dialog{background-color:#2b3b4e;border-color:#000;border-radius:3px;border-style:solid;border-width:1px;box-shadow:0 16px 16px -10px rgba(42,55,70,.15),0 0 40px 1px rgba(42,55,70,.15);display:flex;flex-direction:column;max-height:100%;max-width:480px;overflow:hidden;position:relative;width:95vw;z-index:2}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-dialog{align-self:flex-start;margin:8px auto;max-height:calc(100vh - 8px * 2);width:calc(100vw - 16px)}}.tox .tox-dialog-inline{z-index:1100}.tox .tox-dialog__header{align-items:center;background-color:#2b3b4e;border-bottom:none;color:#fff;display:flex;font-size:16px;justify-content:space-between;padding:8px 16px 0 16px;position:relative}.tox .tox-dialog__header .tox-button{z-index:1}.tox .tox-dialog__draghandle{cursor:grab;height:100%;left:0;position:absolute;top:0;width:100%}.tox .tox-dialog__draghandle:active{cursor:grabbing}.tox .tox-dialog__dismiss{margin-left:auto}.tox .tox-dialog__title{font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:20px;font-style:normal;font-weight:400;line-height:1.3;margin:0;text-transform:none}.tox .tox-dialog__body{color:#fff;display:flex;flex:1;font-size:16px;font-style:normal;font-weight:400;line-height:1.3;min-width:0;text-align:left;text-transform:none}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-dialog__body{flex-direction:column}}.tox .tox-dialog__body-nav{align-items:flex-start;display:flex;flex-direction:column;flex-shrink:0;padding:16px 16px}@media only screen and (min-width:768px){.tox .tox-dialog__body-nav{max-width:11em}}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-dialog__body-nav{flex-direction:row;-webkit-overflow-scrolling:touch;overflow-x:auto;padding-bottom:0}}.tox .tox-dialog__body-nav-item{border-bottom:2px solid transparent;color:rgba(255,255,255,.5);display:inline-block;flex-shrink:0;font-size:14px;line-height:1.3;margin-bottom:8px;max-width:13em;text-decoration:none}.tox .tox-dialog__body-nav-item:focus{background-color:rgba(32,122,183,.1)}.tox .tox-dialog__body-nav-item--active{border-bottom:2px solid #207ab7;color:#207ab7}.tox .tox-dialog__body-content{box-sizing:border-box;display:flex;flex:1;flex-direction:column;max-height:min(650px,calc(100vh - 110px));overflow:auto;-webkit-overflow-scrolling:touch;padding:16px 16px}.tox .tox-dialog__body-content>*{margin-bottom:0;margin-top:16px}.tox .tox-dialog__body-content>:first-child{margin-top:0}.tox .tox-dialog__body-content>:last-child{margin-bottom:0}.tox .tox-dialog__body-content>:only-child{margin-bottom:0;margin-top:0}.tox .tox-dialog__body-content a{color:#207ab7;cursor:pointer;text-decoration:underline}.tox .tox-dialog__body-content a:focus,.tox .tox-dialog__body-content a:hover{color:#114060;text-decoration:underline}.tox .tox-dialog__body-content a:focus-visible{border-radius:1px;outline:2px solid #207ab7;outline-offset:2px}.tox .tox-dialog__body-content a:active{color:#092335;text-decoration:underline}.tox .tox-dialog__body-content svg{fill:#fff}.tox .tox-dialog__body-content strong{font-weight:700}.tox .tox-dialog__body-content ul{list-style-type:disc}.tox .tox-dialog__body-content dd,.tox .tox-dialog__body-content ol,.tox .tox-dialog__body-content ul{padding-inline-start:2.5rem}.tox .tox-dialog__body-content dl,.tox .tox-dialog__body-content ol,.tox .tox-dialog__body-content ul{margin-bottom:16px}.tox .tox-dialog__body-content dd,.tox .tox-dialog__body-content dl,.tox .tox-dialog__body-content dt,.tox .tox-dialog__body-content ol,.tox .tox-dialog__body-content ul{display:block;margin-inline-end:0;margin-inline-start:0}.tox .tox-dialog__body-content .tox-form__group h1{color:#fff;font-size:20px;font-style:normal;font-weight:700;letter-spacing:normal;margin-bottom:16px;margin-top:2rem;text-transform:none}.tox .tox-dialog__body-content .tox-form__group h2{color:#fff;font-size:16px;font-style:normal;font-weight:700;letter-spacing:normal;margin-bottom:16px;margin-top:2rem;text-transform:none}.tox .tox-dialog__body-content .tox-form__group p{margin-bottom:16px}.tox .tox-dialog__body-content .tox-form__group h1:first-child,.tox .tox-dialog__body-content .tox-form__group h2:first-child,.tox .tox-dialog__body-content .tox-form__group p:first-child{margin-top:0}.tox .tox-dialog__body-content .tox-form__group h1:last-child,.tox .tox-dialog__body-content .tox-form__group h2:last-child,.tox .tox-dialog__body-content .tox-form__group p:last-child{margin-bottom:0}.tox .tox-dialog__body-content .tox-form__group h1:only-child,.tox .tox-dialog__body-content .tox-form__group h2:only-child,.tox .tox-dialog__body-content .tox-form__group p:only-child{margin-bottom:0;margin-top:0}.tox .tox-dialog__body-content .tox-form__group .tox-label.tox-label--center{text-align:center}.tox .tox-dialog__body-content .tox-form__group .tox-label.tox-label--end{text-align:end}.tox .tox-dialog--width-lg{height:650px;max-width:1200px}.tox .tox-dialog--fullscreen{height:100%;max-width:100%}.tox .tox-dialog--fullscreen .tox-dialog__body-content{max-height:100%}.tox .tox-dialog--width-md{max-width:800px}.tox .tox-dialog--width-md .tox-dialog__body-content{overflow:auto}.tox .tox-dialog__body-content--centered{text-align:center}.tox .tox-dialog__footer{align-items:center;background-color:#2b3b4e;border-top:1px solid #000;display:flex;justify-content:space-between;padding:8px 16px}.tox .tox-dialog__footer-end,.tox .tox-dialog__footer-start{display:flex}.tox .tox-dialog__busy-spinner{align-items:center;background-color:rgba(34,47,62,.75);bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0;z-index:3}.tox .tox-dialog__table{border-collapse:collapse;width:100%}.tox .tox-dialog__table thead th{font-weight:700;padding-bottom:8px}.tox .tox-dialog__table thead th:first-child{padding-right:8px}.tox .tox-dialog__table tbody tr{border-bottom:1px solid #000}.tox .tox-dialog__table tbody tr:last-child{border-bottom:none}.tox .tox-dialog__table td{padding-bottom:8px;padding-top:8px}.tox .tox-dialog__table td:first-child{padding-right:8px}.tox .tox-dialog__iframe{min-height:200px}.tox .tox-dialog__iframe.tox-dialog__iframe--opaque{background:#fff}.tox .tox-navobj-bordered{position:relative}.tox .tox-navobj-bordered::before{border:1px solid #000;border-radius:3px;content:'';inset:0;opacity:1;pointer-events:none;position:absolute;z-index:1}.tox .tox-navobj-bordered-focus.tox-navobj-bordered::before{border-color:#207ab7;box-shadow:none;outline:2px solid rgba(32,122,183,.25)}.tox .tox-dialog__popups{position:absolute;width:100%;z-index:1100}.tox .tox-dialog__body-iframe{display:flex;flex:1;flex-direction:column}.tox .tox-dialog__body-iframe .tox-navobj{display:flex;flex:1}.tox .tox-dialog__body-iframe .tox-navobj :nth-child(2){flex:1;height:100%}.tox .tox-dialog-dock-fadeout{opacity:0;visibility:hidden}.tox .tox-dialog-dock-fadein{opacity:1;visibility:visible}.tox .tox-dialog-dock-transition{transition:visibility 0s linear .3s,opacity .3s ease}.tox .tox-dialog-dock-transition.tox-dialog-dock-fadein{transition-delay:0s}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox:not([dir=rtl]) .tox-dialog__body-nav{margin-right:0}}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox:not([dir=rtl]) .tox-dialog__body-nav-item:not(:first-child){margin-left:8px}}.tox:not([dir=rtl]) .tox-dialog__footer .tox-dialog__footer-end>*,.tox:not([dir=rtl]) .tox-dialog__footer .tox-dialog__footer-start>*{margin-left:8px}.tox[dir=rtl] .tox-dialog__body{text-align:right}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox[dir=rtl] .tox-dialog__body-nav{margin-left:0}}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox[dir=rtl] .tox-dialog__body-nav-item:not(:first-child){margin-right:8px}}.tox[dir=rtl] .tox-dialog__footer .tox-dialog__footer-end>*,.tox[dir=rtl] .tox-dialog__footer .tox-dialog__footer-start>*{margin-right:8px}body.tox-dialog__disable-scroll{overflow:hidden}.tox .tox-dropzone-container{display:flex;flex:1}.tox .tox-dropzone{align-items:center;background:#fff;border:2px dashed #000;box-sizing:border-box;display:flex;flex-direction:column;flex-grow:1;justify-content:center;min-height:100px;padding:10px}.tox .tox-dropzone p{color:rgba(255,255,255,.5);margin:0 0 16px 0}.tox .tox-edit-area{display:flex;flex:1;overflow:hidden;position:relative}.tox .tox-edit-area::before{border:2px solid #2d6adf;border-radius:4px;content:'';inset:0;opacity:0;pointer-events:none;position:absolute;transition:opacity .15s;z-index:1}.tox .tox-edit-area__iframe{background-color:#fff;border:0;box-sizing:border-box;flex:1;height:100%;position:absolute;width:100%}.tox.tox-edit-focus .tox-edit-area::before{opacity:1}.tox.tox-inline-edit-area{border:1px dotted #000}.tox .tox-editor-container{display:flex;flex:1 1 auto;flex-direction:column;overflow:hidden}.tox .tox-editor-header{display:grid;grid-template-columns:1fr min-content;z-index:2}.tox:not(.tox-tinymce-inline) .tox-editor-header{background-color:#222f3e;border-bottom:none;box-shadow:none;padding:4px 0}.tox:not(.tox-tinymce-inline) .tox-editor-header:not(.tox-editor-dock-transition){transition:box-shadow .5s}.tox:not(.tox-tinymce-inline).tox-tinymce--toolbar-bottom .tox-editor-header{border-top:1px solid #000;box-shadow:none}.tox:not(.tox-tinymce-inline).tox-tinymce--toolbar-sticky-on .tox-editor-header{background-color:#222f3e;box-shadow:0 4px 4px -3px rgba(0,0,0,.25);padding:4px 0}.tox:not(.tox-tinymce-inline).tox-tinymce--toolbar-sticky-on.tox-tinymce--toolbar-bottom .tox-editor-header{box-shadow:0 4px 4px -3px rgba(0,0,0,.25)}.tox.tox:not(.tox-tinymce-inline) .tox-editor-header.tox-editor-header--empty{background:0 0;border:none;box-shadow:none;padding:0}.tox-editor-dock-fadeout{opacity:0;visibility:hidden}.tox-editor-dock-fadein{opacity:1;visibility:visible}.tox-editor-dock-transition{transition:visibility 0s linear .25s,opacity .25s ease}.tox-editor-dock-transition.tox-editor-dock-fadein{transition-delay:0s}.tox .tox-control-wrap{flex:1;position:relative}.tox .tox-control-wrap:not(.tox-control-wrap--status-invalid) .tox-control-wrap__status-icon-invalid,.tox .tox-control-wrap:not(.tox-control-wrap--status-unknown) .tox-control-wrap__status-icon-unknown,.tox .tox-control-wrap:not(.tox-control-wrap--status-valid) .tox-control-wrap__status-icon-valid{display:none}.tox .tox-control-wrap svg{display:block}.tox .tox-control-wrap__status-icon-wrap{position:absolute;top:50%;transform:translateY(-50%)}.tox .tox-control-wrap__status-icon-invalid svg{fill:#c00}.tox .tox-control-wrap__status-icon-unknown svg{fill:orange}.tox .tox-control-wrap__status-icon-valid svg{fill:green}.tox:not([dir=rtl]) .tox-control-wrap--status-invalid .tox-textfield,.tox:not([dir=rtl]) .tox-control-wrap--status-unknown .tox-textfield,.tox:not([dir=rtl]) .tox-control-wrap--status-valid .tox-textfield{padding-right:32px}.tox:not([dir=rtl]) .tox-control-wrap__status-icon-wrap{right:4px}.tox[dir=rtl] .tox-control-wrap--status-invalid .tox-textfield,.tox[dir=rtl] .tox-control-wrap--status-unknown .tox-textfield,.tox[dir=rtl] .tox-control-wrap--status-valid .tox-textfield{padding-left:32px}.tox[dir=rtl] .tox-control-wrap__status-icon-wrap{left:4px}.tox .tox-autocompleter{max-width:25em}.tox .tox-autocompleter .tox-menu{box-sizing:border-box;max-width:25em}.tox .tox-autocompleter .tox-autocompleter-highlight{font-weight:700}.tox .tox-color-input{display:flex;position:relative;z-index:1}.tox .tox-color-input .tox-textfield{z-index:-1}.tox .tox-color-input span{border-color:rgba(42,55,70,.2);border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;height:24px;position:absolute;top:6px;width:24px}.tox .tox-color-input span:focus:not([aria-disabled=true]),.tox .tox-color-input span:hover:not([aria-disabled=true]){border-color:#207ab7;cursor:pointer}.tox .tox-color-input span::before{background-image:linear-gradient(45deg,rgba(255,255,255,.25) 25%,transparent 25%),linear-gradient(-45deg,rgba(255,255,255,.25) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,rgba(255,255,255,.25) 75%),linear-gradient(-45deg,transparent 75%,rgba(255,255,255,.25) 75%);background-position:0 0,0 6px,6px -6px,-6px 0;background-size:12px 12px;border:1px solid #2b3b4e;border-radius:3px;box-sizing:border-box;content:'';height:24px;left:-1px;position:absolute;top:-1px;width:24px;z-index:-1}.tox .tox-color-input span[aria-disabled=true]{cursor:not-allowed}.tox:not([dir=rtl]) .tox-color-input .tox-textfield{padding-left:36px}.tox:not([dir=rtl]) .tox-color-input span{left:6px}.tox[dir=rtl] .tox-color-input .tox-textfield{padding-right:36px}.tox[dir=rtl] .tox-color-input span{right:6px}.tox .tox-label,.tox .tox-toolbar-label{color:rgba(255,255,255,.5);display:block;font-size:14px;font-style:normal;font-weight:400;line-height:1.3;padding:0 8px 0 0;text-transform:none;white-space:nowrap}.tox .tox-toolbar-label{padding:0 8px}.tox[dir=rtl] .tox-label{padding:0 0 0 8px}.tox .tox-form{display:flex;flex:1;flex-direction:column}.tox .tox-form__group{box-sizing:border-box;margin-bottom:4px}.tox .tox-form-group--maximize{flex:1}.tox .tox-form__group--error{color:#c00}.tox .tox-form__group--collection{display:flex}.tox .tox-form__grid{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between}.tox .tox-form__grid--2col>.tox-form__group{width:calc(50% - (8px / 2))}.tox .tox-form__grid--3col>.tox-form__group{width:calc(100% / 3 - (8px / 2))}.tox .tox-form__grid--4col>.tox-form__group{width:calc(25% - (8px / 2))}.tox .tox-form__controls-h-stack{align-items:center;display:flex}.tox .tox-form__group--inline{align-items:center;display:flex}.tox .tox-form__group--stretched{display:flex;flex:1;flex-direction:column}.tox .tox-form__group--stretched .tox-textarea{flex:1}.tox .tox-form__group--stretched .tox-navobj{display:flex;flex:1}.tox .tox-form__group--stretched .tox-navobj :nth-child(2){flex:1;height:100%}.tox:not([dir=rtl]) .tox-form__controls-h-stack>:not(:first-child){margin-left:4px}.tox[dir=rtl] .tox-form__controls-h-stack>:not(:first-child){margin-right:4px}.tox .tox-lock.tox-locked .tox-lock-icon__unlock,.tox .tox-lock:not(.tox-locked) .tox-lock-icon__lock{display:none}.tox .tox-listboxfield .tox-listbox--select,.tox .tox-textarea,.tox .tox-textarea-wrap .tox-textarea:focus,.tox .tox-textfield,.tox .tox-toolbar-textfield{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#2b3b4e;border-color:#000;border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;color:#fff;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:16px;line-height:24px;margin:0;min-height:34px;outline:0;padding:5px 4.75px;resize:none;width:100%}.tox .tox-textarea[disabled],.tox .tox-textfield[disabled]{background-color:#222f3e;color:rgba(255,255,255,.85);cursor:not-allowed}.tox .tox-custom-editor:focus-within,.tox .tox-listboxfield .tox-listbox--select:focus,.tox .tox-textarea-wrap:focus-within,.tox .tox-textarea:focus,.tox .tox-textfield:focus{background-color:#2b3b4e;border-color:#207ab7;box-shadow:none;outline:2px solid rgba(32,122,183,.25)}.tox .tox-toolbar-textfield{border-width:0;margin-bottom:3px;margin-top:2px;max-width:250px}.tox .tox-naked-btn{background-color:transparent;border:0;border-color:transparent;box-shadow:unset;color:#207ab7;cursor:pointer;display:block;margin:0;padding:0}.tox .tox-naked-btn svg{display:block;fill:#fff}.tox:not([dir=rtl]) .tox-toolbar-textfield+*{margin-left:4px}.tox[dir=rtl] .tox-toolbar-textfield+*{margin-right:4px}.tox .tox-listboxfield{cursor:pointer;position:relative}.tox .tox-listboxfield .tox-listbox--select[disabled]{background-color:#19232e;color:rgba(255,255,255,.85);cursor:not-allowed}.tox .tox-listbox__select-label{cursor:default;flex:1;margin:0 4px}.tox .tox-listbox__select-chevron{align-items:center;display:flex;justify-content:center;width:16px}.tox .tox-listbox__select-chevron svg{fill:#fff}.tox .tox-listboxfield .tox-listbox--select{align-items:center;display:flex}.tox:not([dir=rtl]) .tox-listboxfield svg{right:8px}.tox[dir=rtl] .tox-listboxfield svg{left:8px}.tox .tox-selectfield{cursor:pointer;position:relative}.tox .tox-selectfield select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#2b3b4e;border-color:#000;border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;color:#fff;font-family:-apple-system,BlinkMacSystemFont,\\\"Segoe UI\\\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\\\"Helvetica Neue\\\",sans-serif;font-size:16px;line-height:24px;margin:0;min-height:34px;outline:0;padding:5px 4.75px;resize:none;width:100%}.tox .tox-selectfield select[disabled]{background-color:#19232e;color:rgba(255,255,255,.85);cursor:not-allowed}.tox .tox-selectfield select::-ms-expand{display:none}.tox .tox-selectfield select:focus{background-color:#2b3b4e;border-color:#207ab7;box-shadow:none;outline:2px solid rgba(32,122,183,.25)}.tox .tox-selectfield svg{pointer-events:none;position:absolute;top:50%;transform:translateY(-50%)}.tox:not([dir=rtl]) .tox-selectfield select[size=\\\"0\\\"],.tox:not([dir=rtl]) .tox-selectfield select[size=\\\"1\\\"]{padding-right:24px}.tox:not([dir=rtl]) .tox-selectfield svg{right:8px}.tox[dir=rtl] .tox-selectfield select[size=\\\"0\\\"],.tox[dir=rtl] .tox-selectfield select[size=\\\"1\\\"]{padding-left:24px}.tox[dir=rtl] .tox-selectfield svg{left:8px}.tox .tox-textarea-wrap{border-color:#000;border-radius:3px;border-style:solid;border-width:1px;display:flex;flex:1;overflow:hidden}.tox .tox-textarea{-webkit-appearance:textarea;-moz-appearance:textarea;appearance:textarea;white-space:pre-wrap}.tox .tox-textarea-wrap .tox-textarea{border:none}.tox .tox-textarea-wrap .tox-textarea:focus{border:none}.tox-fullscreen{border:0;height:100%;margin:0;overflow:hidden;overscroll-behavior:none;padding:0;touch-action:pinch-zoom;width:100%}.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle{display:none}.tox-shadowhost.tox-fullscreen,.tox.tox-tinymce.tox-fullscreen{left:0;position:fixed;top:0;z-index:1200}.tox.tox-tinymce.tox-fullscreen{background-color:transparent}.tox-fullscreen .tox.tox-tinymce-aux,.tox-fullscreen~.tox.tox-tinymce-aux{z-index:1201}.tox .tox-help__more-link{list-style:none;margin-top:1em}.tox .tox-imagepreview{background-color:#666;height:380px;overflow:hidden;position:relative;width:100%}.tox .tox-imagepreview.tox-imagepreview__loaded{overflow:auto}.tox .tox-imagepreview__container{display:flex;left:100vw;position:absolute;top:100vw}.tox .tox-imagepreview__image{background:url(data:image/gif;base64,R0lGODdhDAAMAIABAMzMzP///ywAAAAADAAMAAACFoQfqYeabNyDMkBQb81Uat85nxguUAEAOw==)}.tox .tox-image-tools .tox-spacer{flex:1}.tox .tox-image-tools .tox-bar{align-items:center;display:flex;height:60px;justify-content:center}.tox .tox-image-tools .tox-imagepreview,.tox .tox-image-tools .tox-imagepreview+.tox-bar{margin-top:8px}.tox .tox-image-tools .tox-croprect-block{background:#000;opacity:.5;position:absolute;zoom:1}.tox .tox-image-tools .tox-croprect-handle{border:2px solid #fff;height:20px;left:0;position:absolute;top:0;width:20px}.tox .tox-image-tools .tox-croprect-handle-move{border:0;cursor:move;position:absolute}.tox .tox-image-tools .tox-croprect-handle-nw{border-width:2px 0 0 2px;cursor:nw-resize;left:100px;margin:-2px 0 0 -2px;top:100px}.tox .tox-image-tools .tox-croprect-handle-ne{border-width:2px 2px 0 0;cursor:ne-resize;left:200px;margin:-2px 0 0 -20px;top:100px}.tox .tox-image-tools .tox-croprect-handle-sw{border-width:0 0 2px 2px;cursor:sw-resize;left:100px;margin:-20px 2px 0 -2px;top:200px}.tox .tox-image-tools .tox-croprect-handle-se{border-width:0 2px 2px 0;cursor:se-resize;left:200px;margin:-20px 0 0 -20px;top:200px}.tox .tox-insert-table-picker{display:flex;flex-wrap:wrap;width:170px}.tox .tox-insert-table-picker>div{border-color:#000;border-style:solid;border-width:0 1px 1px 0;box-sizing:border-box;height:17px;width:17px}.tox .tox-collection--list .tox-collection__group .tox-insert-table-picker{margin:0 -4px}.tox .tox-insert-table-picker .tox-insert-table-picker__selected{background-color:rgba(32,122,183,.5);border-color:rgba(32,122,183,.5)}.tox .tox-insert-table-picker__label{color:#fff;display:block;font-size:14px;padding:4px;text-align:center;width:100%}.tox:not([dir=rtl]) .tox-insert-table-picker>div:nth-child(10n){border-right:0}.tox[dir=rtl] .tox-insert-table-picker>div:nth-child(10n+1){border-right:0}.tox .tox-menu{background-color:#2b3b4e;border:1px solid #000;border-radius:3px;box-shadow:0 4px 8px 0 rgba(42,55,70,.1);display:inline-block;overflow:hidden;vertical-align:top;z-index:1150}.tox .tox-menu.tox-collection.tox-collection--list{padding:0 0}.tox .tox-menu.tox-collection.tox-collection--toolbar{padding:4px}.tox .tox-menu.tox-collection.tox-collection--grid{padding:4px}@media only screen and (min-width:768px){.tox .tox-menu .tox-collection__item-label{overflow-wrap:break-word;word-break:normal}.tox .tox-dialog__popups .tox-menu .tox-collection__item-label{word-break:break-all}}.tox .tox-menu__label blockquote,.tox .tox-menu__label code,.tox .tox-menu__label h1,.tox .tox-menu__label h2,.tox .tox-menu__label h3,.tox .tox-menu__label h4,.tox .tox-menu__label h5,.tox .tox-menu__label h6,.tox .tox-menu__label p{margin:0}.tox .tox-menubar{background:url(\\\"data:image/svg+xml;charset=utf8,%3Csvg height='39px' viewBox='0 0 40 39px' width='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='0' y='38px' width='100' height='1' fill='%23000000'/%3E%3C/svg%3E\\\") left 0 top 0 #222f3e;background-color:#222f3e;display:flex;flex:0 0 auto;flex-shrink:0;flex-wrap:wrap;grid-column:1/-1;grid-row:1;padding:0 4px 0 4px}.tox .tox-promotion+.tox-menubar{grid-column:1}.tox .tox-promotion{background:url(\\\"data:image/svg+xml;charset=utf8,%3Csvg height='39px' viewBox='0 0 40 39px' width='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='0' y='38px' width='100' height='1' fill='%23000000'/%3E%3C/svg%3E\\\") left 0 top 0 #222f3e;background-color:#222f3e;grid-column:2;grid-row:1;padding-inline-end:8px;padding-inline-start:4px;padding-top:5px}.tox .tox-promotion-link{align-items:unsafe center;background-color:#e8f1f8;border-radius:5px;color:#086be6;cursor:pointer;display:flex;font-size:14px;height:26.6px;padding:4px 8px;white-space:nowrap}.tox .tox-promotion-link:hover{background-color:#b4d7ff}.tox .tox-promotion-link:focus{background-color:#d9edf7}.tox .tox-mbtn{align-items:center;background:0 0;border:0;border-radius:3px;box-shadow:none;color:#fff;display:flex;flex:0 0 auto;font-size:14px;font-style:normal;font-weight:400;height:34px;justify-content:center;margin:2px 0 3px 0;outline:0;overflow:hidden;padding:0 4px;text-transform:none;width:auto}.tox .tox-mbtn[disabled]{background-color:transparent;border:0;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-mbtn:focus:not(:disabled){background:#4a5562;border:0;box-shadow:none;color:#fff}.tox .tox-mbtn--active{background:#757d87;border:0;box-shadow:none;color:#fff}.tox .tox-mbtn:hover:not(:disabled):not(.tox-mbtn--active){background:#4a5562;border:0;box-shadow:none;color:#fff}.tox .tox-mbtn__select-label{cursor:default;font-weight:400;margin:0 4px}.tox .tox-mbtn[disabled] .tox-mbtn__select-label{cursor:not-allowed}.tox .tox-mbtn__select-chevron{align-items:center;display:flex;justify-content:center;width:16px;display:none}.tox .tox-notification{border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;display:grid;font-size:14px;font-weight:400;grid-template-columns:minmax(40px,1fr) auto minmax(40px,1fr);margin-top:4px;opacity:0;padding:4px;transition:transform .1s ease-in,opacity 150ms ease-in}.tox .tox-notification p{font-size:14px;font-weight:400}.tox .tox-notification a{cursor:pointer;text-decoration:underline}.tox .tox-notification--in{opacity:1}.tox .tox-notification--success{background-color:#334840;border-color:#3c5440;color:#fff}.tox .tox-notification--success p{color:#fff}.tox .tox-notification--success a{color:#b5d199}.tox .tox-notification--success svg{fill:#fff}.tox .tox-notification--error{background-color:#442632;border-color:#55212b;color:#fff}.tox .tox-notification--error p{color:#fff}.tox .tox-notification--error a{color:#e68080}.tox .tox-notification--error svg{fill:#fff}.tox .tox-notification--warn,.tox .tox-notification--warning{background-color:#222f3e;border-color:#000;color:#fff0b3}.tox .tox-notification--warn p,.tox .tox-notification--warning p{color:#fff0b3}.tox .tox-notification--warn a,.tox .tox-notification--warning a{color:#fc0}.tox .tox-notification--warn svg,.tox .tox-notification--warning svg{fill:#fff0b3}.tox .tox-notification--info{background-color:#254161;border-color:#264972;color:#fff}.tox .tox-notification--info p{color:#fff}.tox .tox-notification--info a{color:#83b7f3}.tox .tox-notification--info svg{fill:#fff}.tox .tox-notification__body{align-self:center;color:#fff;font-size:14px;grid-column-end:3;grid-column-start:2;grid-row-end:2;grid-row-start:1;text-align:center;white-space:normal;word-break:break-all;word-break:break-word}.tox .tox-notification__body>*{margin:0}.tox .tox-notification__body>*+*{margin-top:1rem}.tox .tox-notification__icon{align-self:center;grid-column-end:2;grid-column-start:1;grid-row-end:2;grid-row-start:1;justify-self:end}.tox .tox-notification__icon svg{display:block}.tox .tox-notification__dismiss{align-self:start;grid-column-end:4;grid-column-start:3;grid-row-end:2;grid-row-start:1;justify-self:end}.tox .tox-notification .tox-progress-bar{grid-column-end:4;grid-column-start:1;grid-row-end:3;grid-row-start:2;justify-self:center}.tox .tox-pop{display:inline-block;position:relative}.tox .tox-pop--resizing{transition:width .1s ease}.tox .tox-pop--resizing .tox-toolbar,.tox .tox-pop--resizing .tox-toolbar__group{flex-wrap:nowrap}.tox .tox-pop--transition{transition:.15s ease;transition-property:left,right,top,bottom}.tox .tox-pop--transition::after,.tox .tox-pop--transition::before{transition:all .15s,visibility 0s,opacity 75ms ease 75ms}.tox .tox-pop__dialog{background-color:#222f3e;border:1px solid #000;border-radius:3px;box-shadow:0 0 2px 0 rgba(42,55,70,.2),0 4px 8px 0 rgba(42,55,70,.15);min-width:0;overflow:hidden}.tox .tox-pop__dialog>:not(.tox-toolbar){margin:4px 4px 4px 8px}.tox .tox-pop__dialog .tox-toolbar{background-color:transparent;margin-bottom:-1px}.tox .tox-pop::after,.tox .tox-pop::before{border-style:solid;content:'';display:block;height:0;opacity:1;position:absolute;width:0}.tox .tox-pop.tox-pop--inset::after,.tox .tox-pop.tox-pop--inset::before{opacity:0;transition:all 0s .15s,visibility 0s,opacity 75ms ease}.tox .tox-pop.tox-pop--bottom::after,.tox .tox-pop.tox-pop--bottom::before{left:50%;top:100%}.tox .tox-pop.tox-pop--bottom::after{border-color:#222f3e transparent transparent transparent;border-width:8px;margin-left:-8px;margin-top:-1px}.tox .tox-pop.tox-pop--bottom::before{border-color:#000 transparent transparent transparent;border-width:9px;margin-left:-9px}.tox .tox-pop.tox-pop--top::after,.tox .tox-pop.tox-pop--top::before{left:50%;top:0;transform:translateY(-100%)}.tox .tox-pop.tox-pop--top::after{border-color:transparent transparent #222f3e transparent;border-width:8px;margin-left:-8px;margin-top:1px}.tox .tox-pop.tox-pop--top::before{border-color:transparent transparent #000 transparent;border-width:9px;margin-left:-9px}.tox .tox-pop.tox-pop--left::after,.tox .tox-pop.tox-pop--left::before{left:0;top:calc(50% - 1px);transform:translateY(-50%)}.tox .tox-pop.tox-pop--left::after{border-color:transparent #222f3e transparent transparent;border-width:8px;margin-left:-15px}.tox .tox-pop.tox-pop--left::before{border-color:transparent #000 transparent transparent;border-width:10px;margin-left:-19px}.tox .tox-pop.tox-pop--right::after,.tox .tox-pop.tox-pop--right::before{left:100%;top:calc(50% + 1px);transform:translateY(-50%)}.tox .tox-pop.tox-pop--right::after{border-color:transparent transparent transparent #222f3e;border-width:8px;margin-left:-1px}.tox .tox-pop.tox-pop--right::before{border-color:transparent transparent transparent #000;border-width:10px;margin-left:-1px}.tox .tox-pop.tox-pop--align-left::after,.tox .tox-pop.tox-pop--align-left::before{left:20px}.tox .tox-pop.tox-pop--align-right::after,.tox .tox-pop.tox-pop--align-right::before{left:calc(100% - 20px)}.tox .tox-sidebar-wrap{display:flex;flex-direction:row;flex-grow:1;min-height:0}.tox .tox-sidebar{background-color:#222f3e;display:flex;flex-direction:row;justify-content:flex-end}.tox .tox-sidebar__slider{display:flex;overflow:hidden}.tox .tox-sidebar__pane-container{display:flex}.tox .tox-sidebar__pane{display:flex}.tox .tox-sidebar--sliding-closed{opacity:0}.tox .tox-sidebar--sliding-open{opacity:1}.tox .tox-sidebar--sliding-growing,.tox .tox-sidebar--sliding-shrinking{transition:width .5s ease,opacity .5s ease}.tox .tox-selector{background-color:#4099ff;border-color:#4099ff;border-style:solid;border-width:1px;box-sizing:border-box;display:inline-block;height:10px;position:absolute;width:10px}.tox.tox-platform-touch .tox-selector{height:12px;width:12px}.tox .tox-slider{align-items:center;display:flex;flex:1;height:24px;justify-content:center;position:relative}.tox .tox-slider__rail{background-color:transparent;border:1px solid #000;border-radius:3px;height:10px;min-width:120px;width:100%}.tox .tox-slider__handle{background-color:#207ab7;border:2px solid #185d8c;border-radius:3px;box-shadow:none;height:24px;left:50%;position:absolute;top:50%;transform:translateX(-50%) translateY(-50%);width:14px}.tox .tox-form__controls-h-stack>.tox-slider:not(:first-of-type){margin-inline-start:8px}.tox .tox-form__controls-h-stack>.tox-form__group+.tox-slider{margin-inline-start:32px}.tox .tox-form__controls-h-stack>.tox-slider+.tox-form__group{margin-inline-start:32px}.tox .tox-source-code{overflow:auto}.tox .tox-spinner{display:flex}.tox .tox-spinner>div{animation:tam-bouncing-dots 1.5s ease-in-out 0s infinite both;background-color:rgba(255,255,255,.5);border-radius:100%;height:8px;width:8px}.tox .tox-spinner>div:nth-child(1){animation-delay:-.32s}.tox .tox-spinner>div:nth-child(2){animation-delay:-.16s}@keyframes tam-bouncing-dots{0%,100%,80%{transform:scale(0)}40%{transform:scale(1)}}.tox:not([dir=rtl]) .tox-spinner>div:not(:first-child){margin-left:4px}.tox[dir=rtl] .tox-spinner>div:not(:first-child){margin-right:4px}.tox .tox-statusbar{align-items:center;background-color:#222f3e;border-top:1px solid #000;color:#fff;display:flex;flex:0 0 auto;font-size:12px;font-weight:400;height:18px;overflow:hidden;padding:0 8px;position:relative;text-transform:uppercase}.tox .tox-statusbar__path{display:flex;flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tox .tox-statusbar__right-container{display:flex;justify-content:flex-end;white-space:nowrap}.tox .tox-statusbar__help-text{text-align:center}.tox .tox-statusbar__text-container{display:flex;flex:1 1 auto;justify-content:space-between;overflow:hidden}@media only screen and (min-width:768px){.tox .tox-statusbar__text-container.tox-statusbar__text-container-3-cols>.tox-statusbar__help-text,.tox .tox-statusbar__text-container.tox-statusbar__text-container-3-cols>.tox-statusbar__path,.tox .tox-statusbar__text-container.tox-statusbar__text-container-3-cols>.tox-statusbar__right-container{flex:0 0 calc(100% / 3)}}.tox .tox-statusbar__text-container.tox-statusbar__text-container--flex-end{justify-content:flex-end}.tox .tox-statusbar__text-container.tox-statusbar__text-container--flex-start{justify-content:flex-start}.tox .tox-statusbar__text-container.tox-statusbar__text-container--space-around{justify-content:space-around}.tox .tox-statusbar__path>*{display:inline;white-space:nowrap}.tox .tox-statusbar__wordcount{flex:0 0 auto;margin-left:1ch}@media only screen and (max-width:767px){.tox .tox-statusbar__text-container .tox-statusbar__help-text{display:none}.tox .tox-statusbar__text-container .tox-statusbar__help-text:only-child{display:block}}.tox .tox-statusbar a,.tox .tox-statusbar__path-item,.tox .tox-statusbar__wordcount{color:#fff;text-decoration:none}.tox .tox-statusbar a:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar a:hover:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__path-item:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__path-item:hover:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__wordcount:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__wordcount:hover:not(:disabled):not([aria-disabled=true]){color:#fff;cursor:pointer}.tox .tox-statusbar__branding svg{fill:rgba(255,255,255,.8);height:1.14em;vertical-align:-.28em;width:3.6em}.tox .tox-statusbar__branding a:focus:not(:disabled):not([aria-disabled=true]) svg,.tox .tox-statusbar__branding a:hover:not(:disabled):not([aria-disabled=true]) svg{fill:#fff}.tox .tox-statusbar__resize-handle{align-items:flex-end;align-self:stretch;cursor:nwse-resize;display:flex;flex:0 0 auto;justify-content:flex-end;margin-left:auto;margin-right:-8px;padding-bottom:3px;padding-left:1ch;padding-right:3px}.tox .tox-statusbar__resize-handle svg{display:block;fill:rgba(255,255,255,.5)}.tox .tox-statusbar__resize-handle:focus svg{background-color:#4a5562;border-radius:1px 1px -4px 1px;box-shadow:0 0 0 2px #4a5562}.tox:not([dir=rtl]) .tox-statusbar__path>*{margin-right:4px}.tox:not([dir=rtl]) .tox-statusbar__branding{margin-left:2ch}.tox[dir=rtl] .tox-statusbar{flex-direction:row-reverse}.tox[dir=rtl] .tox-statusbar__path>*{margin-left:4px}.tox .tox-throbber{z-index:1299}.tox .tox-throbber__busy-spinner{align-items:center;background-color:rgba(34,47,62,.6);bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0}.tox .tox-tbtn{align-items:center;background:0 0;border:0;border-radius:3px;box-shadow:none;color:#fff;display:flex;flex:0 0 auto;font-size:14px;font-style:normal;font-weight:400;height:34px;justify-content:center;margin:3px 0 2px 0;outline:0;overflow:hidden;padding:0;text-transform:none;width:34px}.tox .tox-tbtn svg{display:block;fill:#fff}.tox .tox-tbtn.tox-tbtn-more{padding-left:5px;padding-right:5px;width:inherit}.tox .tox-tbtn:focus{background:#4a5562;border:0;box-shadow:none}.tox .tox-tbtn:hover{background:#4a5562;border:0;box-shadow:none;color:#fff}.tox .tox-tbtn:hover svg{fill:#fff}.tox .tox-tbtn:active{background:#757d87;border:0;box-shadow:none;color:#fff}.tox .tox-tbtn:active svg{fill:#fff}.tox .tox-tbtn--disabled .tox-tbtn--enabled svg{fill:rgba(255,255,255,.5)}.tox .tox-tbtn--disabled,.tox .tox-tbtn--disabled:hover,.tox .tox-tbtn:disabled,.tox .tox-tbtn:disabled:hover{background:0 0;border:0;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-tbtn--disabled svg,.tox .tox-tbtn--disabled:hover svg,.tox .tox-tbtn:disabled svg,.tox .tox-tbtn:disabled:hover svg{fill:rgba(255,255,255,.5)}.tox .tox-tbtn--enabled,.tox .tox-tbtn--enabled:hover{background:#757d87;border:0;box-shadow:none;color:#fff}.tox .tox-tbtn--enabled:hover>*,.tox .tox-tbtn--enabled>*{transform:none}.tox .tox-tbtn--enabled svg,.tox .tox-tbtn--enabled:hover svg{fill:#fff}.tox .tox-tbtn--enabled.tox-tbtn--disabled svg,.tox .tox-tbtn--enabled:hover.tox-tbtn--disabled svg{fill:rgba(255,255,255,.5)}.tox .tox-tbtn:focus:not(.tox-tbtn--disabled){color:#fff}.tox .tox-tbtn:focus:not(.tox-tbtn--disabled) svg{fill:#fff}.tox .tox-tbtn:active>*{transform:none}.tox .tox-tbtn--md{height:51px;width:51px}.tox .tox-tbtn--lg{flex-direction:column;height:68px;width:68px}.tox .tox-tbtn--return{align-self:stretch;height:unset;width:16px}.tox .tox-tbtn--labeled{padding:0 4px;width:unset}.tox .tox-tbtn__vlabel{display:block;font-size:10px;font-weight:400;letter-spacing:-.025em;margin-bottom:4px;white-space:nowrap}.tox .tox-number-input{border-radius:3px;display:flex;margin:3px 0 2px 0;padding:0 4px;width:auto}.tox .tox-number-input .tox-input-wrapper{background:0 0;display:flex;pointer-events:none;text-align:center}.tox .tox-number-input .tox-input-wrapper:focus{background:#4a5562}.tox .tox-number-input input{border-radius:3px;color:#fff;font-size:14px;margin:2px 0;pointer-events:all;width:60px}.tox .tox-number-input input:hover{background:#4a5562;color:#fff}.tox .tox-number-input input:focus{background:#fff;color:#2a3746}.tox .tox-number-input input:disabled{background:0 0;border:0;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-number-input button{background:0 0;color:#fff;height:34px;text-align:center;width:24px}.tox .tox-number-input button svg{display:block;fill:#fff;margin:0 auto;transform:scale(.67)}.tox .tox-number-input button:focus{background:#4a5562}.tox .tox-number-input button:hover{background:#4a5562;border:0;box-shadow:none;color:#fff}.tox .tox-number-input button:hover svg{fill:#fff}.tox .tox-number-input button:active{background:#757d87;border:0;box-shadow:none;color:#fff}.tox .tox-number-input button:active svg{fill:#fff}.tox .tox-number-input button:disabled{background:0 0;border:0;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-number-input button:disabled svg{fill:rgba(255,255,255,.5)}.tox .tox-number-input button.minus{border-radius:3px 0 0 3px}.tox .tox-number-input button.plus{border-radius:0 3px 3px 0}.tox .tox-number-input:focus:not(:active)>.tox-input-wrapper,.tox .tox-number-input:focus:not(:active)>button{background:#4a5562}.tox .tox-tbtn--select{margin:3px 0 2px 0;padding:0 4px;width:auto}.tox .tox-tbtn__select-label{cursor:default;font-weight:400;height:initial;margin:0 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tox .tox-tbtn__select-chevron{align-items:center;display:flex;justify-content:center;width:16px}.tox .tox-tbtn__select-chevron svg{fill:rgba(255,255,255,.5)}.tox .tox-tbtn--bespoke{background:0 0}.tox .tox-tbtn--bespoke+.tox-tbtn--bespoke{margin-inline-start:0}.tox .tox-tbtn--bespoke .tox-tbtn__select-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:7em}.tox .tox-tbtn--disabled .tox-tbtn__select-label,.tox .tox-tbtn--select:disabled .tox-tbtn__select-label{cursor:not-allowed}.tox .tox-split-button{border:0;border-radius:3px;box-sizing:border-box;display:flex;margin:3px 0 2px 0;overflow:hidden}.tox .tox-split-button:hover{box-shadow:0 0 0 1px #4a5562 inset}.tox .tox-split-button:focus{background:#4a5562;box-shadow:none;color:#fff}.tox .tox-split-button>*{border-radius:0}.tox .tox-split-button__chevron{width:16px}.tox .tox-split-button__chevron svg{fill:rgba(255,255,255,.5)}.tox .tox-split-button .tox-tbtn{margin:0}.tox .tox-split-button.tox-tbtn--disabled .tox-tbtn:focus,.tox .tox-split-button.tox-tbtn--disabled .tox-tbtn:hover,.tox .tox-split-button.tox-tbtn--disabled:focus,.tox .tox-split-button.tox-tbtn--disabled:hover{background:0 0;box-shadow:none;color:rgba(255,255,255,.5)}.tox.tox-platform-touch .tox-split-button .tox-tbtn--select{padding:0 0}.tox.tox-platform-touch .tox-split-button .tox-tbtn:not(.tox-tbtn--select):first-child{width:30px}.tox.tox-platform-touch .tox-split-button__chevron{width:20px}.tox .tox-split-button.tox-tbtn--disabled svg #tox-icon-highlight-bg-color__color,.tox .tox-split-button.tox-tbtn--disabled svg #tox-icon-text-color__color{opacity:.6}.tox .tox-toolbar-overlord{background-color:#222f3e}.tox .tox-toolbar,.tox .tox-toolbar__overflow,.tox .tox-toolbar__primary{background-attachment:local;background-color:#222f3e;background-image:repeating-linear-gradient(#000 0 1px,transparent 1px 39px);background-position:center top 39px;background-repeat:no-repeat;background-size:calc(100% - 4px * 2) calc(100% - 39px);display:flex;flex:0 0 auto;flex-shrink:0;flex-wrap:wrap;padding:0 0;transform:perspective(1px)}.tox .tox-toolbar-overlord>.tox-toolbar,.tox .tox-toolbar-overlord>.tox-toolbar__overflow,.tox .tox-toolbar-overlord>.tox-toolbar__primary{background-position:center top 0;background-size:calc(100% - 4px * 2) calc(100% - 0px)}.tox .tox-toolbar__overflow.tox-toolbar__overflow--closed{height:0;opacity:0;padding-bottom:0;padding-top:0;visibility:hidden}.tox .tox-toolbar__overflow--growing{transition:height .3s ease,opacity .2s linear .1s}.tox .tox-toolbar__overflow--shrinking{transition:opacity .3s ease,height .2s linear .1s,visibility 0s linear .3s}.tox .tox-anchorbar,.tox .tox-toolbar-overlord{grid-column:1/-1}.tox .tox-menubar+.tox-toolbar,.tox .tox-menubar+.tox-toolbar-overlord{border-top:1px solid #000;margin-top:-1px;padding-bottom:0;padding-top:0}.tox .tox-toolbar--scrolling{flex-wrap:nowrap;overflow-x:auto}.tox .tox-pop .tox-toolbar{border-width:0}.tox .tox-toolbar--no-divider{background-image:none}.tox .tox-toolbar-overlord .tox-toolbar:not(.tox-toolbar--scrolling):first-child,.tox .tox-toolbar-overlord .tox-toolbar__primary{background-position:center top 39px}.tox .tox-editor-header>.tox-toolbar--scrolling,.tox .tox-toolbar-overlord .tox-toolbar--scrolling:first-child{background-image:none}.tox.tox-tinymce-aux .tox-toolbar__overflow{background-color:#222f3e;background-position:center top 43px;background-size:calc(100% - 8px * 2) calc(100% - 51px);border:none;border-radius:3px;box-shadow:0 0 2px 0 rgba(42,55,70,.2),0 4px 8px 0 rgba(42,55,70,.15);overscroll-behavior:none;padding:4px 0}.tox-pop .tox-pop__dialog .tox-toolbar{background-position:center top 43px;background-size:calc(100% - 4px * 2) calc(100% - 51px);padding:4px 0}.tox .tox-toolbar__group{align-items:center;display:flex;flex-wrap:wrap;margin:0 0;padding:0 4px 0 4px}.tox .tox-toolbar__group--pull-right{margin-left:auto}.tox .tox-toolbar--scrolling .tox-toolbar__group{flex-shrink:0;flex-wrap:nowrap}.tox:not([dir=rtl]) .tox-toolbar__group:not(:last-of-type){border-right:1px solid #000}.tox[dir=rtl] .tox-toolbar__group:not(:last-of-type){border-left:1px solid #000}.tox .tox-tooltip{display:inline-block;padding:8px;position:relative}.tox .tox-tooltip__body{background-color:#3d546f;border-radius:3px;box-shadow:0 2px 4px rgba(42,55,70,.3);color:rgba(255,255,255,.75);font-size:14px;font-style:normal;font-weight:400;padding:4px 8px;text-transform:none}.tox .tox-tooltip__arrow{position:absolute}.tox .tox-tooltip--down .tox-tooltip__arrow{border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid #3d546f;bottom:0;left:50%;position:absolute;transform:translateX(-50%)}.tox .tox-tooltip--up .tox-tooltip__arrow{border-bottom:8px solid #3d546f;border-left:8px solid transparent;border-right:8px solid transparent;left:50%;position:absolute;top:0;transform:translateX(-50%)}.tox .tox-tooltip--right .tox-tooltip__arrow{border-bottom:8px solid transparent;border-left:8px solid #3d546f;border-top:8px solid transparent;position:absolute;right:0;top:50%;transform:translateY(-50%)}.tox .tox-tooltip--left .tox-tooltip__arrow{border-bottom:8px solid transparent;border-right:8px solid #3d546f;border-top:8px solid transparent;left:0;position:absolute;top:50%;transform:translateY(-50%)}.tox .tox-tree{display:flex;flex-direction:column}.tox .tox-tree .tox-trbtn{align-items:center;background:0 0;border:0;border-radius:4px;box-shadow:none;color:#fff;display:flex;flex:0 0 auto;font-size:14px;font-style:normal;font-weight:400;height:28px;margin-bottom:4px;margin-top:4px;outline:0;overflow:hidden;padding:0;padding-left:8px;text-transform:none}.tox .tox-tree .tox-trbtn .tox-tree__label{cursor:default;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tox .tox-tree .tox-trbtn svg{display:block;fill:#fff}.tox .tox-tree .tox-trbtn:focus{background:#4a5562;border:0;box-shadow:none}.tox .tox-tree .tox-trbtn:hover{background:#4a5562;border:0;box-shadow:none;color:#fff}.tox .tox-tree .tox-trbtn:hover svg{fill:#fff}.tox .tox-tree .tox-trbtn:active{background:#6ea9d0;border:0;box-shadow:none;color:#fff}.tox .tox-tree .tox-trbtn:active svg{fill:#fff}.tox .tox-tree .tox-trbtn--disabled,.tox .tox-tree .tox-trbtn--disabled:hover,.tox .tox-tree .tox-trbtn:disabled,.tox .tox-tree .tox-trbtn:disabled:hover{background:0 0;border:0;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-tree .tox-trbtn--disabled svg,.tox .tox-tree .tox-trbtn--disabled:hover svg,.tox .tox-tree .tox-trbtn:disabled svg,.tox .tox-tree .tox-trbtn:disabled:hover svg{fill:rgba(255,255,255,.5)}.tox .tox-tree .tox-trbtn--enabled,.tox .tox-tree .tox-trbtn--enabled:hover{background:#6ea9d0;border:0;box-shadow:none;color:#fff}.tox .tox-tree .tox-trbtn--enabled:hover>*,.tox .tox-tree .tox-trbtn--enabled>*{transform:none}.tox .tox-tree .tox-trbtn--enabled svg,.tox .tox-tree .tox-trbtn--enabled:hover svg{fill:#fff}.tox .tox-tree .tox-trbtn:focus:not(.tox-trbtn--disabled){color:#fff}.tox .tox-tree .tox-trbtn:focus:not(.tox-trbtn--disabled) svg{fill:#fff}.tox .tox-tree .tox-trbtn:active>*{transform:none}.tox .tox-tree .tox-trbtn--return{align-self:stretch;height:unset;width:16px}.tox .tox-tree .tox-trbtn--labeled{padding:0 4px;width:unset}.tox .tox-tree .tox-trbtn__vlabel{display:block;font-size:10px;font-weight:400;letter-spacing:-.025em;margin-bottom:4px;white-space:nowrap}.tox .tox-tree .tox-tree--directory{display:flex;flex-direction:column}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label{font-weight:700}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-mbtn{margin-left:auto}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-mbtn svg{fill:transparent}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-mbtn.tox-mbtn--active svg,.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-mbtn:focus svg{fill:#fff}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:focus .tox-mbtn svg,.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:hover .tox-mbtn svg{fill:#fff}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:hover:has(.tox-mbtn:hover){background-color:transparent;color:#fff}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:hover:has(.tox-mbtn:hover) .tox-chevron svg{fill:#fff}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label .tox-chevron{margin-right:6px}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:has(+.tox-tree--directory__children--growing) .tox-chevron,.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:has(+.tox-tree--directory__children--shrinking) .tox-chevron{transition:transform .5s ease-in-out}.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:has(+.tox-tree--directory__children--growing) .tox-chevron,.tox .tox-tree .tox-tree--directory .tox-tree--directory__label:has(+.tox-tree--directory__children--open) .tox-chevron{transform:rotate(90deg)}.tox .tox-tree .tox-tree--leaf__label{font-weight:400}.tox .tox-tree .tox-tree--leaf__label .tox-mbtn{margin-left:auto}.tox .tox-tree .tox-tree--leaf__label .tox-mbtn svg{fill:transparent}.tox .tox-tree .tox-tree--leaf__label .tox-mbtn.tox-mbtn--active svg,.tox .tox-tree .tox-tree--leaf__label .tox-mbtn:focus svg{fill:#fff}.tox .tox-tree .tox-tree--leaf__label:hover .tox-mbtn svg{fill:#fff}.tox .tox-tree .tox-tree--leaf__label:hover:has(.tox-mbtn:hover){background-color:transparent;color:#fff}.tox .tox-tree .tox-tree--leaf__label:hover:has(.tox-mbtn:hover) .tox-chevron svg{fill:#fff}.tox .tox-tree .tox-tree--directory__children{overflow:hidden;padding-left:16px}.tox .tox-tree .tox-tree--directory__children.tox-tree--directory__children--growing,.tox .tox-tree .tox-tree--directory__children.tox-tree--directory__children--shrinking{transition:height .5s ease-in-out}.tox .tox-tree .tox-trbtn.tox-tree--leaf__label{display:flex;justify-content:space-between}.tox .tox-view-wrap,.tox .tox-view-wrap__slot-container{background-color:#222f3e;display:flex;flex:1;flex-direction:column}.tox .tox-view{display:flex;flex:1 1 auto;flex-direction:column;overflow:hidden}.tox .tox-view__header{align-items:center;display:flex;font-size:16px;justify-content:space-between;padding:8px 8px 0 8px;position:relative}.tox .tox-view--mobile.tox-view__header,.tox .tox-view--mobile.tox-view__toolbar{padding:8px}.tox .tox-view--scrolling{flex-wrap:nowrap;overflow-x:auto}.tox .tox-view__toolbar{display:flex;flex-direction:row;gap:8px;justify-content:space-between;padding:8px 8px 0 8px}.tox .tox-view__toolbar__group{display:flex;flex-direction:row;gap:12px}.tox .tox-view__header-end,.tox .tox-view__header-start{display:flex}.tox .tox-view__pane{height:100%;padding:8px;width:100%}.tox .tox-view__pane_panel{border:1px solid #000;border-radius:3px}.tox:not([dir=rtl]) .tox-view__header .tox-view__header-end>*,.tox:not([dir=rtl]) .tox-view__header .tox-view__header-start>*{margin-left:8px}.tox[dir=rtl] .tox-view__header .tox-view__header-end>*,.tox[dir=rtl] .tox-view__header .tox-view__header-start>*{margin-right:8px}.tox .tox-well{border:1px solid #000;border-radius:3px;padding:8px;width:100%}.tox .tox-well>:first-child{margin-top:0}.tox .tox-well>:last-child{margin-bottom:0}.tox .tox-well>:only-child{margin:0}.tox .tox-custom-editor{border:1px solid #000;border-radius:3px;display:flex;flex:1;overflow:hidden;position:relative}.tox .tox-dialog-loading::before{background-color:rgba(0,0,0,.5);content:\\\"\\\";height:100%;position:absolute;width:100%;z-index:1000}.tox .tox-tab{cursor:pointer}.tox .tox-dialog__content-js{display:flex;flex:1}.tox .tox-dialog__body-content .tox-collection{display:flex;flex:1}.tox:not(.tox-tinymce-inline) .tox-editor-header{background-color:none;padding:0}.tox.tox-tinymce--toolbar-bottom .tox-editor-header,.tox.tox-tinymce-inline .tox-editor-header{margin-bottom:-1px}.tox.tox-tinymce-inline .tox-editor-container{overflow:hidden}.tox:not(.tox-tinymce-inline).tox-tinymce--toolbar-bottom .tox-editor-header{border-top:none;box-shadow:none}.tox.tox.tox-tinymce--toolbar-sticky-on .tox-editor-header{background-color:transparent;box-shadow:0 4px 4px -3px rgba(0,0,0,.25);padding:0}.tox.tox.tox-tinymce--toolbar-sticky-on.tox-tinymce--toolbar-bottom .tox-editor-header{box-shadow:0 4px 4px -3px rgba(0,0,0,.25)}.tox .tox-collection--list .tox-collection__group .tox-insert-table-picker{margin:-4px 0}.tox .tox-menu.tox-collection.tox-collection--list{padding:0}.tox .tox-pop{box-shadow:none}.tox .tox-number-input,.tox .tox-split-button,.tox .tox-tbtn,.tox .tox-tbtn--select{margin:2px 0 3px 0}.tox .tox-toolbar,.tox .tox-toolbar__overflow,.tox .tox-toolbar__primary{background:url(\\\"data:image/svg+xml;charset=utf8,%3Csvg height='39px' viewBox='0 0 40 39px' width='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='0' y='38px' width='100' height='1' fill='%23000000'/%3E%3C/svg%3E\\\") left 0 top 0 #222f3e!important}.tox .tox-menubar+.tox-toolbar-overlord{border-top:none}.tox .tox-menubar+.tox-toolbar,.tox .tox-menubar+.tox-toolbar-overlord .tox-toolbar__primary{border-top:1px solid #000;margin-top:-1px}.tox.tox-tinymce-aux .tox-toolbar__overflow{border:1px solid #000;padding:0}.tox .tox-pop .tox-pop__dialog .tox-toolbar{padding:0}.tox:not(.tox-tinymce-inline) .tox-editor-header:not(:first-child) .tox-menubar{border-top:1px solid #000}.tox:not(.tox-tinymce-inline) .tox-editor-header:not(:first-child) .tox-toolbar-overlord:first-child .tox-toolbar__primary,.tox:not(.tox-tinymce-inline) .tox-editor-header:not(:first-child) .tox-toolbar:first-child{border-top:1px solid #000}.tox .tox-toolbar__group{padding:0 4px 0 4px}.tox .tox-collection__item{border-radius:0;cursor:pointer}.tox .tox-statusbar a:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar a:hover:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__path-item:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__path-item:hover:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__wordcount:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__wordcount:hover:not(:disabled):not([aria-disabled=true]){color:#fff;text-decoration:underline}.tox .tox-statusbar__branding svg{vertical-align:-.25em}.tox:not([dir=rtl]) .tox-statusbar__branding{margin-left:1ch}.tox .tox-statusbar__resize-handle{padding-bottom:0;padding-right:0}.tox .tox-button::before{display:none}\")\n//# sourceMappingURL=skin.js.map\n"
  },
  {
    "path": "public/libs/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.js",
    "content": "tinymce.Resource.add('ui/tinymce-5-dark/skin.shadowdom.css', \"body.tox-dialog__disable-scroll{overflow:hidden}.tox-fullscreen{border:0;height:100%;margin:0;overflow:hidden;overscroll-behavior:none;padding:0;touch-action:pinch-zoom;width:100%}.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle{display:none}.tox-shadowhost.tox-fullscreen,.tox.tox-tinymce.tox-fullscreen{left:0;position:fixed;top:0;z-index:1200}.tox.tox-tinymce.tox-fullscreen{background-color:transparent}.tox-fullscreen .tox.tox-tinymce-aux,.tox-fullscreen~.tox.tox-tinymce-aux{z-index:1201}\")\n//# sourceMappingURL=skin.shadowdom.js.map\n"
  },
  {
    "path": "public/libs/tinymce/tinymce.d.ts",
    "content": "interface StringPathBookmark {\n    start: string;\n    end?: string;\n    forward?: boolean;\n}\ninterface RangeBookmark {\n    rng: Range;\n    forward?: boolean;\n}\ninterface IdBookmark {\n    id: string;\n    keep?: boolean;\n    forward?: boolean;\n}\ninterface IndexBookmark {\n    name: string;\n    index: number;\n}\ninterface PathBookmark {\n    start: number[];\n    end?: number[];\n    isFakeCaret?: boolean;\n    forward?: boolean;\n}\ntype Bookmark = StringPathBookmark | RangeBookmark | IdBookmark | IndexBookmark | PathBookmark;\ntype NormalizedEvent<E, T = any> = E & {\n    readonly type: string;\n    readonly target: T;\n    readonly isDefaultPrevented: () => boolean;\n    readonly preventDefault: () => void;\n    readonly isPropagationStopped: () => boolean;\n    readonly stopPropagation: () => void;\n    readonly isImmediatePropagationStopped: () => boolean;\n    readonly stopImmediatePropagation: () => void;\n};\ntype MappedEvent<T extends {}, K extends string> = K extends keyof T ? T[K] : any;\ninterface NativeEventMap {\n    'beforepaste': Event;\n    'blur': FocusEvent;\n    'beforeinput': InputEvent;\n    'click': MouseEvent;\n    'compositionend': Event;\n    'compositionstart': Event;\n    'compositionupdate': Event;\n    'contextmenu': PointerEvent;\n    'copy': ClipboardEvent;\n    'cut': ClipboardEvent;\n    'dblclick': MouseEvent;\n    'drag': DragEvent;\n    'dragdrop': DragEvent;\n    'dragend': DragEvent;\n    'draggesture': DragEvent;\n    'dragover': DragEvent;\n    'dragstart': DragEvent;\n    'drop': DragEvent;\n    'focus': FocusEvent;\n    'focusin': FocusEvent;\n    'focusout': FocusEvent;\n    'input': InputEvent;\n    'keydown': KeyboardEvent;\n    'keypress': KeyboardEvent;\n    'keyup': KeyboardEvent;\n    'mousedown': MouseEvent;\n    'mouseenter': MouseEvent;\n    'mouseleave': MouseEvent;\n    'mousemove': MouseEvent;\n    'mouseout': MouseEvent;\n    'mouseover': MouseEvent;\n    'mouseup': MouseEvent;\n    'paste': ClipboardEvent;\n    'selectionchange': Event;\n    'submit': Event;\n    'touchend': TouchEvent;\n    'touchmove': TouchEvent;\n    'touchstart': TouchEvent;\n    'touchcancel': TouchEvent;\n    'wheel': WheelEvent;\n}\ntype EditorEvent<T> = NormalizedEvent<T>;\ninterface EventDispatcherSettings {\n    scope?: any;\n    toggleEvent?: (name: string, state: boolean) => void | boolean;\n    beforeFire?: <T>(args: EditorEvent<T>) => void;\n}\ninterface EventDispatcherConstructor<T extends {}> {\n    readonly prototype: EventDispatcher<T>;\n    new (settings?: EventDispatcherSettings): EventDispatcher<T>;\n    isNative: (name: string) => boolean;\n}\ndeclare class EventDispatcher<T extends {}> {\n    static isNative(name: string): boolean;\n    private readonly settings;\n    private readonly scope;\n    private readonly toggleEvent;\n    private bindings;\n    constructor(settings?: EventDispatcherSettings);\n    fire<K extends string, U extends MappedEvent<T, K>>(name: K, args?: U): EditorEvent<U>;\n    dispatch<K extends string, U extends MappedEvent<T, K>>(name: K, args?: U): EditorEvent<U>;\n    on<K extends string>(name: K, callback: false | ((event: EditorEvent<MappedEvent<T, K>>) => void | boolean), prepend?: boolean, extra?: {}): this;\n    off<K extends string>(name?: K, callback?: (event: EditorEvent<MappedEvent<T, K>>) => void): this;\n    once<K extends string>(name: K, callback: (event: EditorEvent<MappedEvent<T, K>>) => void, prepend?: boolean): this;\n    has(name: string): boolean;\n}\ntype UndoLevelType = 'fragmented' | 'complete';\ninterface BaseUndoLevel {\n    type: UndoLevelType;\n    bookmark: Bookmark | null;\n    beforeBookmark: Bookmark | null;\n}\ninterface FragmentedUndoLevel extends BaseUndoLevel {\n    type: 'fragmented';\n    fragments: string[];\n    content: '';\n}\ninterface CompleteUndoLevel extends BaseUndoLevel {\n    type: 'complete';\n    fragments: null;\n    content: string;\n}\ntype NewUndoLevel = CompleteUndoLevel | FragmentedUndoLevel;\ntype UndoLevel = NewUndoLevel & {\n    bookmark: Bookmark;\n};\ninterface UndoManager {\n    data: UndoLevel[];\n    typing: boolean;\n    add: (level?: Partial<UndoLevel>, event?: EditorEvent<any>) => UndoLevel | null;\n    dispatchChange: () => void;\n    beforeChange: () => void;\n    undo: () => UndoLevel | undefined;\n    redo: () => UndoLevel | undefined;\n    clear: () => void;\n    reset: () => void;\n    hasUndo: () => boolean;\n    hasRedo: () => boolean;\n    transact: (callback: () => void) => UndoLevel | null;\n    ignore: (callback: () => void) => void;\n    extra: (callback1: () => void, callback2: () => void) => void;\n}\ntype SchemaType = 'html4' | 'html5' | 'html5-strict';\ninterface ElementSettings {\n    block_elements?: string;\n    boolean_attributes?: string;\n    move_caret_before_on_enter_elements?: string;\n    non_empty_elements?: string;\n    self_closing_elements?: string;\n    text_block_elements?: string;\n    text_inline_elements?: string;\n    void_elements?: string;\n    whitespace_elements?: string;\n    transparent_elements?: string;\n    wrap_block_elements?: string;\n}\ninterface SchemaSettings extends ElementSettings {\n    custom_elements?: string;\n    extended_valid_elements?: string;\n    invalid_elements?: string;\n    invalid_styles?: string | Record<string, string>;\n    schema?: SchemaType;\n    valid_children?: string;\n    valid_classes?: string | Record<string, string>;\n    valid_elements?: string;\n    valid_styles?: string | Record<string, string>;\n    verify_html?: boolean;\n    padd_empty_block_inline_children?: boolean;\n}\ninterface Attribute {\n    required?: boolean;\n    defaultValue?: string;\n    forcedValue?: string;\n    validValues?: Record<string, {}>;\n}\ninterface DefaultAttribute {\n    name: string;\n    value: string;\n}\ninterface AttributePattern extends Attribute {\n    pattern: RegExp;\n}\ninterface ElementRule {\n    attributes: Record<string, Attribute>;\n    attributesDefault?: DefaultAttribute[];\n    attributesForced?: DefaultAttribute[];\n    attributesOrder: string[];\n    attributePatterns?: AttributePattern[];\n    attributesRequired?: string[];\n    paddEmpty?: boolean;\n    removeEmpty?: boolean;\n    removeEmptyAttrs?: boolean;\n    paddInEmptyBlock?: boolean;\n}\ninterface SchemaElement extends ElementRule {\n    outputName?: string;\n    parentsRequired?: string[];\n    pattern?: RegExp;\n}\ninterface SchemaMap {\n    [name: string]: {};\n}\ninterface SchemaRegExpMap {\n    [name: string]: RegExp;\n}\ninterface Schema {\n    type: SchemaType;\n    children: Record<string, SchemaMap>;\n    elements: Record<string, SchemaElement>;\n    getValidStyles: () => Record<string, string[]> | undefined;\n    getValidClasses: () => Record<string, SchemaMap> | undefined;\n    getBlockElements: () => SchemaMap;\n    getInvalidStyles: () => Record<string, SchemaMap> | undefined;\n    getVoidElements: () => SchemaMap;\n    getTextBlockElements: () => SchemaMap;\n    getTextInlineElements: () => SchemaMap;\n    getBoolAttrs: () => SchemaMap;\n    getElementRule: (name: string) => SchemaElement | undefined;\n    getSelfClosingElements: () => SchemaMap;\n    getNonEmptyElements: () => SchemaMap;\n    getMoveCaretBeforeOnEnterElements: () => SchemaMap;\n    getWhitespaceElements: () => SchemaMap;\n    getTransparentElements: () => SchemaMap;\n    getSpecialElements: () => SchemaRegExpMap;\n    isValidChild: (name: string, child: string) => boolean;\n    isValid: (name: string, attr?: string) => boolean;\n    isBlock: (name: string) => boolean;\n    isInline: (name: string) => boolean;\n    isWrapper: (name: string) => boolean;\n    getCustomElements: () => SchemaMap;\n    addValidElements: (validElements: string) => void;\n    setValidElements: (validElements: string) => void;\n    addCustomElements: (customElements: string) => void;\n    addValidChildren: (validChildren: any) => void;\n}\ntype Attributes$1 = Array<{\n    name: string;\n    value: string;\n}> & {\n    map: Record<string, string>;\n};\ninterface AstNodeConstructor {\n    readonly prototype: AstNode;\n    new (name: string, type: number): AstNode;\n    create(name: string, attrs?: Record<string, string>): AstNode;\n}\ndeclare class AstNode {\n    static create(name: string, attrs?: Record<string, string>): AstNode;\n    name: string;\n    type: number;\n    attributes?: Attributes$1;\n    value?: string;\n    parent?: AstNode | null;\n    firstChild?: AstNode | null;\n    lastChild?: AstNode | null;\n    next?: AstNode | null;\n    prev?: AstNode | null;\n    raw?: boolean;\n    constructor(name: string, type: number);\n    replace(node: AstNode): AstNode;\n    attr(name: string, value: string | null | undefined): AstNode | undefined;\n    attr(name: Record<string, string | null | undefined> | undefined): AstNode | undefined;\n    attr(name: string): string | undefined;\n    clone(): AstNode;\n    wrap(wrapper: AstNode): AstNode;\n    unwrap(): void;\n    remove(): AstNode;\n    append(node: AstNode): AstNode;\n    insert(node: AstNode, refNode: AstNode, before?: boolean): AstNode;\n    getAll(name: string): AstNode[];\n    children(): AstNode[];\n    empty(): AstNode;\n    isEmpty(elements: SchemaMap, whitespace?: SchemaMap, predicate?: (node: AstNode) => boolean): boolean;\n    walk(prev?: boolean): AstNode | null | undefined;\n}\ntype Content = string | AstNode;\ntype ContentFormat = 'raw' | 'text' | 'html' | 'tree';\ninterface GetContentArgs {\n    format: ContentFormat;\n    get: boolean;\n    getInner: boolean;\n    no_events?: boolean;\n    save?: boolean;\n    source_view?: boolean;\n    [key: string]: any;\n}\ninterface SetContentArgs {\n    format: string;\n    set: boolean;\n    content: Content;\n    no_events?: boolean;\n    no_selection?: boolean;\n    paste?: boolean;\n    load?: boolean;\n    initial?: boolean;\n    [key: string]: any;\n}\ninterface GetSelectionContentArgs extends GetContentArgs {\n    selection?: boolean;\n    contextual?: boolean;\n}\ninterface SetSelectionContentArgs extends SetContentArgs {\n    content: string;\n    selection?: boolean;\n}\ninterface BlobInfoData {\n    id?: string;\n    name?: string;\n    filename?: string;\n    blob: Blob;\n    base64: string;\n    blobUri?: string;\n    uri?: string;\n}\ninterface BlobInfo {\n    id: () => string;\n    name: () => string;\n    filename: () => string;\n    blob: () => Blob;\n    base64: () => string;\n    blobUri: () => string;\n    uri: () => string | undefined;\n}\ninterface BlobCache {\n    create: {\n        (o: BlobInfoData): BlobInfo;\n        (id: string, blob: Blob, base64: string, name?: string, filename?: string): BlobInfo;\n    };\n    add: (blobInfo: BlobInfo) => void;\n    get: (id: string) => BlobInfo | undefined;\n    getByUri: (blobUri: string) => BlobInfo | undefined;\n    getByData: (base64: string, type: string) => BlobInfo | undefined;\n    findFirst: (predicate: (blobInfo: BlobInfo) => boolean) => BlobInfo | undefined;\n    removeByUri: (blobUri: string) => void;\n    destroy: () => void;\n}\ninterface BlobInfoImagePair {\n    image: HTMLImageElement;\n    blobInfo: BlobInfo;\n}\ndeclare class NodeChange {\n    private readonly editor;\n    private lastPath;\n    constructor(editor: Editor);\n    nodeChanged(args?: Record<string, any>): void;\n    private isSameElementPath;\n}\ninterface SelectionOverrides {\n    showCaret: (direction: number, node: HTMLElement, before: boolean, scrollIntoView?: boolean) => Range | null;\n    showBlockCaretContainer: (blockCaretContainer: HTMLElement) => void;\n    hideFakeCaret: () => void;\n    destroy: () => void;\n}\ninterface Quirks {\n    refreshContentEditable(): void;\n    isHidden(): boolean;\n}\ntype DecoratorData = Record<string, any>;\ntype Decorator = (uid: string, data: DecoratorData) => {\n    attributes?: {};\n    classes?: string[];\n};\ntype AnnotationListener = (state: boolean, name: string, data?: {\n    uid: string;\n    nodes: any[];\n}) => void;\ntype AnnotationListenerApi = AnnotationListener;\ninterface AnnotatorSettings {\n    decorate: Decorator;\n    persistent?: boolean;\n}\ninterface Annotator {\n    register: (name: string, settings: AnnotatorSettings) => void;\n    annotate: (name: string, data: DecoratorData) => void;\n    annotationChanged: (name: string, f: AnnotationListenerApi) => void;\n    remove: (name: string) => void;\n    removeAll: (name: string) => void;\n    getAll: (name: string) => Record<string, Element[]>;\n}\ninterface GeomRect {\n    readonly x: number;\n    readonly y: number;\n    readonly w: number;\n    readonly h: number;\n}\ninterface Rect {\n    inflate: (rect: GeomRect, w: number, h: number) => GeomRect;\n    relativePosition: (rect: GeomRect, targetRect: GeomRect, rel: string) => GeomRect;\n    findBestRelativePosition: (rect: GeomRect, targetRect: GeomRect, constrainRect: GeomRect, rels: string[]) => string | null;\n    intersect: (rect: GeomRect, cropRect: GeomRect) => GeomRect | null;\n    clamp: (rect: GeomRect, clampRect: GeomRect, fixedSize?: boolean) => GeomRect;\n    create: (x: number, y: number, w: number, h: number) => GeomRect;\n    fromClientRect: (clientRect: DOMRect) => GeomRect;\n}\ninterface NotificationManagerImpl {\n    open: (spec: NotificationSpec, closeCallback: () => void) => NotificationApi;\n    close: <T extends NotificationApi>(notification: T) => void;\n    getArgs: <T extends NotificationApi>(notification: T) => NotificationSpec;\n}\ninterface NotificationSpec {\n    type?: 'info' | 'warning' | 'error' | 'success';\n    text: string;\n    icon?: string;\n    progressBar?: boolean;\n    timeout?: number;\n    closeButton?: boolean;\n}\ninterface NotificationApi {\n    close: () => void;\n    progressBar: {\n        value: (percent: number) => void;\n    };\n    text: (text: string) => void;\n    reposition: () => void;\n    getEl: () => HTMLElement;\n    settings: NotificationSpec;\n}\ninterface NotificationManager {\n    open: (spec: NotificationSpec) => NotificationApi;\n    close: () => void;\n    getNotifications: () => NotificationApi[];\n}\ninterface UploadFailure {\n    message: string;\n    remove?: boolean;\n}\ntype ProgressFn = (percent: number) => void;\ntype UploadHandler = (blobInfo: BlobInfo, progress: ProgressFn) => Promise<string>;\ninterface UploadResult$2 {\n    url: string;\n    blobInfo: BlobInfo;\n    status: boolean;\n    error?: UploadFailure;\n}\ninterface RawPattern {\n    start?: any;\n    end?: any;\n    format?: any;\n    cmd?: any;\n    value?: any;\n    replacement?: any;\n}\ninterface InlineBasePattern {\n    readonly start: string;\n    readonly end: string;\n}\ninterface InlineFormatPattern extends InlineBasePattern {\n    readonly type: 'inline-format';\n    readonly format: string[];\n}\ninterface InlineCmdPattern extends InlineBasePattern {\n    readonly type: 'inline-command';\n    readonly cmd: string;\n    readonly value?: any;\n}\ntype InlinePattern = InlineFormatPattern | InlineCmdPattern;\ninterface BlockBasePattern {\n    readonly start: string;\n}\ninterface BlockFormatPattern extends BlockBasePattern {\n    readonly type: 'block-format';\n    readonly format: string;\n}\ninterface BlockCmdPattern extends BlockBasePattern {\n    readonly type: 'block-command';\n    readonly cmd: string;\n    readonly value?: any;\n}\ntype BlockPattern = BlockFormatPattern | BlockCmdPattern;\ntype Pattern = InlinePattern | BlockPattern;\ninterface DynamicPatternContext {\n    readonly text: string;\n    readonly block: Element;\n}\ntype DynamicPatternsLookup = (ctx: DynamicPatternContext) => Pattern[];\ntype RawDynamicPatternsLookup = (ctx: DynamicPatternContext) => RawPattern[];\ninterface AlertBannerSpec {\n    type: 'alertbanner';\n    level: 'info' | 'warn' | 'error' | 'success';\n    text: string;\n    icon: string;\n    url?: string;\n}\ninterface ButtonSpec {\n    type: 'button';\n    text: string;\n    enabled?: boolean;\n    primary?: boolean;\n    name?: string;\n    icon?: string;\n    borderless?: boolean;\n    buttonType?: 'primary' | 'secondary' | 'toolbar';\n}\ninterface FormComponentSpec {\n    type: string;\n    name: string;\n}\ninterface FormComponentWithLabelSpec extends FormComponentSpec {\n    label?: string;\n}\ninterface CheckboxSpec extends FormComponentSpec {\n    type: 'checkbox';\n    label: string;\n    enabled?: boolean;\n}\ninterface CollectionSpec extends FormComponentWithLabelSpec {\n    type: 'collection';\n}\ninterface CollectionItem {\n    value: string;\n    text: string;\n    icon: string;\n}\ninterface ColorInputSpec extends FormComponentWithLabelSpec {\n    type: 'colorinput';\n    storageKey?: string;\n}\ninterface ColorPickerSpec extends FormComponentWithLabelSpec {\n    type: 'colorpicker';\n}\ninterface CustomEditorInit {\n    setValue: (value: string) => void;\n    getValue: () => string;\n    destroy: () => void;\n}\ntype CustomEditorInitFn = (elm: HTMLElement, settings: any) => Promise<CustomEditorInit>;\ninterface CustomEditorOldSpec extends FormComponentSpec {\n    type: 'customeditor';\n    tag?: string;\n    init: (e: HTMLElement) => Promise<CustomEditorInit>;\n}\ninterface CustomEditorNewSpec extends FormComponentSpec {\n    type: 'customeditor';\n    tag?: string;\n    scriptId: string;\n    scriptUrl: string;\n    settings?: any;\n}\ntype CustomEditorSpec = CustomEditorOldSpec | CustomEditorNewSpec;\ninterface DropZoneSpec extends FormComponentWithLabelSpec {\n    type: 'dropzone';\n}\ninterface GridSpec {\n    type: 'grid';\n    columns: number;\n    items: BodyComponentSpec[];\n}\ninterface HtmlPanelSpec {\n    type: 'htmlpanel';\n    html: string;\n    presets?: 'presentation' | 'document';\n}\ninterface IframeSpec extends FormComponentWithLabelSpec {\n    type: 'iframe';\n    border?: boolean;\n    sandboxed?: boolean;\n    streamContent?: boolean;\n    transparent?: boolean;\n}\ninterface ImagePreviewSpec extends FormComponentSpec {\n    type: 'imagepreview';\n    height?: string;\n}\ninterface InputSpec extends FormComponentWithLabelSpec {\n    type: 'input';\n    inputMode?: string;\n    placeholder?: string;\n    maximized?: boolean;\n    enabled?: boolean;\n}\ntype Alignment = 'start' | 'center' | 'end';\ninterface LabelSpec {\n    type: 'label';\n    label: string;\n    items: BodyComponentSpec[];\n    align?: Alignment;\n}\ninterface ListBoxSingleItemSpec {\n    text: string;\n    value: string;\n}\ninterface ListBoxNestedItemSpec {\n    text: string;\n    items: ListBoxItemSpec[];\n}\ntype ListBoxItemSpec = ListBoxNestedItemSpec | ListBoxSingleItemSpec;\ninterface ListBoxSpec extends FormComponentWithLabelSpec {\n    type: 'listbox';\n    items: ListBoxItemSpec[];\n    disabled?: boolean;\n}\ninterface PanelSpec {\n    type: 'panel';\n    classes?: string[];\n    items: BodyComponentSpec[];\n}\ninterface SelectBoxItemSpec {\n    text: string;\n    value: string;\n}\ninterface SelectBoxSpec extends FormComponentWithLabelSpec {\n    type: 'selectbox';\n    items: SelectBoxItemSpec[];\n    size?: number;\n    enabled?: boolean;\n}\ninterface SizeInputSpec extends FormComponentWithLabelSpec {\n    type: 'sizeinput';\n    constrain?: boolean;\n    enabled?: boolean;\n}\ninterface SliderSpec extends FormComponentSpec {\n    type: 'slider';\n    label: string;\n    min?: number;\n    max?: number;\n}\ninterface TableSpec {\n    type: 'table';\n    header: string[];\n    cells: string[][];\n}\ninterface TextAreaSpec extends FormComponentWithLabelSpec {\n    type: 'textarea';\n    placeholder?: string;\n    maximized?: boolean;\n    enabled?: boolean;\n}\ninterface BaseToolbarButtonSpec<I extends BaseToolbarButtonInstanceApi> {\n    enabled?: boolean;\n    tooltip?: string;\n    icon?: string;\n    text?: string;\n    onSetup?: (api: I) => (api: I) => void;\n}\ninterface BaseToolbarButtonInstanceApi {\n    isEnabled: () => boolean;\n    setEnabled: (state: boolean) => void;\n    setText: (text: string) => void;\n    setIcon: (icon: string) => void;\n}\ninterface ToolbarButtonSpec extends BaseToolbarButtonSpec<ToolbarButtonInstanceApi> {\n    type?: 'button';\n    onAction: (api: ToolbarButtonInstanceApi) => void;\n}\ninterface ToolbarButtonInstanceApi extends BaseToolbarButtonInstanceApi {\n}\ninterface ToolbarGroupSetting {\n    name: string;\n    items: string[];\n}\ntype ToolbarConfig = string | ToolbarGroupSetting[];\ninterface GroupToolbarButtonInstanceApi extends BaseToolbarButtonInstanceApi {\n}\ninterface GroupToolbarButtonSpec extends BaseToolbarButtonSpec<GroupToolbarButtonInstanceApi> {\n    type?: 'grouptoolbarbutton';\n    items?: ToolbarConfig;\n}\ninterface CardImageSpec {\n    type: 'cardimage';\n    src: string;\n    alt?: string;\n    classes?: string[];\n}\ninterface CardTextSpec {\n    type: 'cardtext';\n    text: string;\n    name?: string;\n    classes?: string[];\n}\ntype CardItemSpec = CardContainerSpec | CardImageSpec | CardTextSpec;\ntype CardContainerDirection = 'vertical' | 'horizontal';\ntype CardContainerAlign = 'left' | 'right';\ntype CardContainerValign = 'top' | 'middle' | 'bottom';\ninterface CardContainerSpec {\n    type: 'cardcontainer';\n    items: CardItemSpec[];\n    direction?: CardContainerDirection;\n    align?: CardContainerAlign;\n    valign?: CardContainerValign;\n}\ninterface CommonMenuItemSpec {\n    enabled?: boolean;\n    text?: string;\n    value?: string;\n    meta?: Record<string, any>;\n    shortcut?: string;\n}\ninterface CommonMenuItemInstanceApi {\n    isEnabled: () => boolean;\n    setEnabled: (state: boolean) => void;\n}\ninterface CardMenuItemInstanceApi extends CommonMenuItemInstanceApi {\n}\ninterface CardMenuItemSpec extends Omit<CommonMenuItemSpec, 'text' | 'shortcut'> {\n    type: 'cardmenuitem';\n    label?: string;\n    items: CardItemSpec[];\n    onSetup?: (api: CardMenuItemInstanceApi) => (api: CardMenuItemInstanceApi) => void;\n    onAction?: (api: CardMenuItemInstanceApi) => void;\n}\ninterface ChoiceMenuItemSpec extends CommonMenuItemSpec {\n    type?: 'choiceitem';\n    icon?: string;\n}\ninterface ChoiceMenuItemInstanceApi extends CommonMenuItemInstanceApi {\n    isActive: () => boolean;\n    setActive: (state: boolean) => void;\n}\ninterface ContextMenuItem extends CommonMenuItemSpec {\n    text: string;\n    icon?: string;\n    type?: 'item';\n    onAction: () => void;\n}\ninterface ContextSubMenu extends CommonMenuItemSpec {\n    type: 'submenu';\n    text: string;\n    icon?: string;\n    getSubmenuItems: () => string | Array<ContextMenuContents>;\n}\ntype ContextMenuContents = string | ContextMenuItem | SeparatorMenuItemSpec | ContextSubMenu;\ninterface ContextMenuApi {\n    update: (element: Element) => string | Array<ContextMenuContents>;\n}\ninterface FancyActionArgsMap {\n    'inserttable': {\n        numRows: number;\n        numColumns: number;\n    };\n    'colorswatch': {\n        value: string;\n    };\n}\ninterface BaseFancyMenuItemSpec<T extends keyof FancyActionArgsMap> {\n    type: 'fancymenuitem';\n    fancytype: T;\n    initData?: Record<string, unknown>;\n    onAction?: (data: FancyActionArgsMap[T]) => void;\n}\ninterface InsertTableMenuItemSpec extends BaseFancyMenuItemSpec<'inserttable'> {\n    fancytype: 'inserttable';\n    initData?: {};\n}\ninterface ColorSwatchMenuItemSpec extends BaseFancyMenuItemSpec<'colorswatch'> {\n    fancytype: 'colorswatch';\n    select?: (value: string) => boolean;\n    initData?: {\n        allowCustomColors?: boolean;\n        colors?: ChoiceMenuItemSpec[];\n        storageKey?: string;\n    };\n}\ntype FancyMenuItemSpec = InsertTableMenuItemSpec | ColorSwatchMenuItemSpec;\ninterface MenuItemSpec extends CommonMenuItemSpec {\n    type?: 'menuitem';\n    icon?: string;\n    onSetup?: (api: MenuItemInstanceApi) => (api: MenuItemInstanceApi) => void;\n    onAction?: (api: MenuItemInstanceApi) => void;\n}\ninterface MenuItemInstanceApi extends CommonMenuItemInstanceApi {\n}\ninterface SeparatorMenuItemSpec {\n    type?: 'separator';\n    text?: string;\n}\ninterface ToggleMenuItemSpec extends CommonMenuItemSpec {\n    type?: 'togglemenuitem';\n    icon?: string;\n    active?: boolean;\n    onSetup?: (api: ToggleMenuItemInstanceApi) => void;\n    onAction: (api: ToggleMenuItemInstanceApi) => void;\n}\ninterface ToggleMenuItemInstanceApi extends CommonMenuItemInstanceApi {\n    isActive: () => boolean;\n    setActive: (state: boolean) => void;\n}\ntype NestedMenuItemContents = string | MenuItemSpec | NestedMenuItemSpec | ToggleMenuItemSpec | SeparatorMenuItemSpec | FancyMenuItemSpec;\ninterface NestedMenuItemSpec extends CommonMenuItemSpec {\n    type?: 'nestedmenuitem';\n    icon?: string;\n    getSubmenuItems: () => string | Array<NestedMenuItemContents>;\n    onSetup?: (api: NestedMenuItemInstanceApi) => (api: NestedMenuItemInstanceApi) => void;\n}\ninterface NestedMenuItemInstanceApi extends CommonMenuItemInstanceApi {\n    setTooltip: (tooltip: string) => void;\n    setIconFill: (id: string, value: string) => void;\n}\ntype MenuButtonItemTypes = NestedMenuItemContents;\ntype SuccessCallback$1 = (menu: string | MenuButtonItemTypes[]) => void;\ninterface MenuButtonFetchContext {\n    pattern: string;\n}\ninterface BaseMenuButtonSpec {\n    text?: string;\n    tooltip?: string;\n    icon?: string;\n    search?: boolean | {\n        placeholder?: string;\n    };\n    fetch: (success: SuccessCallback$1, fetchContext: MenuButtonFetchContext, api: BaseMenuButtonInstanceApi) => void;\n    onSetup?: (api: BaseMenuButtonInstanceApi) => (api: BaseMenuButtonInstanceApi) => void;\n}\ninterface BaseMenuButtonInstanceApi {\n    isEnabled: () => boolean;\n    setEnabled: (state: boolean) => void;\n    isActive: () => boolean;\n    setActive: (state: boolean) => void;\n    setText: (text: string) => void;\n    setIcon: (icon: string) => void;\n}\ninterface ToolbarMenuButtonSpec extends BaseMenuButtonSpec {\n    type?: 'menubutton';\n    onSetup?: (api: ToolbarMenuButtonInstanceApi) => (api: ToolbarMenuButtonInstanceApi) => void;\n}\ninterface ToolbarMenuButtonInstanceApi extends BaseMenuButtonInstanceApi {\n}\ntype ToolbarSplitButtonItemTypes = ChoiceMenuItemSpec | SeparatorMenuItemSpec;\ntype SuccessCallback = (menu: ToolbarSplitButtonItemTypes[]) => void;\ntype SelectPredicate = (value: string) => boolean;\ntype PresetTypes = 'color' | 'normal' | 'listpreview';\ntype ColumnTypes$1 = number | 'auto';\ninterface ToolbarSplitButtonSpec {\n    type?: 'splitbutton';\n    tooltip?: string;\n    icon?: string;\n    text?: string;\n    select?: SelectPredicate;\n    presets?: PresetTypes;\n    columns?: ColumnTypes$1;\n    fetch: (success: SuccessCallback) => void;\n    onSetup?: (api: ToolbarSplitButtonInstanceApi) => (api: ToolbarSplitButtonInstanceApi) => void;\n    onAction: (api: ToolbarSplitButtonInstanceApi) => void;\n    onItemAction: (api: ToolbarSplitButtonInstanceApi, value: string) => void;\n}\ninterface ToolbarSplitButtonInstanceApi {\n    isEnabled: () => boolean;\n    setEnabled: (state: boolean) => void;\n    setIconFill: (id: string, value: string) => void;\n    isActive: () => boolean;\n    setActive: (state: boolean) => void;\n    setTooltip: (tooltip: string) => void;\n    setText: (text: string) => void;\n    setIcon: (icon: string) => void;\n}\ninterface BaseToolbarToggleButtonSpec<I extends BaseToolbarButtonInstanceApi> extends BaseToolbarButtonSpec<I> {\n    active?: boolean;\n}\ninterface BaseToolbarToggleButtonInstanceApi extends BaseToolbarButtonInstanceApi {\n    isActive: () => boolean;\n    setActive: (state: boolean) => void;\n}\ninterface ToolbarToggleButtonSpec extends BaseToolbarToggleButtonSpec<ToolbarToggleButtonInstanceApi> {\n    type?: 'togglebutton';\n    onAction: (api: ToolbarToggleButtonInstanceApi) => void;\n}\ninterface ToolbarToggleButtonInstanceApi extends BaseToolbarToggleButtonInstanceApi {\n}\ntype Id = string;\ninterface TreeSpec {\n    type: 'tree';\n    items: TreeItemSpec[];\n    onLeafAction?: (id: Id) => void;\n    defaultExpandedIds?: Id[];\n    onToggleExpand?: (expandedIds: Id[], { expanded, node }: {\n        expanded: boolean;\n        node: Id;\n    }) => void;\n    defaultSelectedId?: Id;\n}\ninterface BaseTreeItemSpec {\n    title: string;\n    id: Id;\n    menu?: ToolbarMenuButtonSpec;\n}\ninterface DirectorySpec extends BaseTreeItemSpec {\n    type: 'directory';\n    children: TreeItemSpec[];\n}\ninterface LeafSpec extends BaseTreeItemSpec {\n    type: 'leaf';\n}\ntype TreeItemSpec = DirectorySpec | LeafSpec;\ninterface UrlInputSpec extends FormComponentWithLabelSpec {\n    type: 'urlinput';\n    filetype?: 'image' | 'media' | 'file';\n    enabled?: boolean;\n    picker_text?: string;\n}\ninterface UrlInputData {\n    value: string;\n    meta: {\n        text?: string;\n    };\n}\ntype BodyComponentSpec = BarSpec | ButtonSpec | CheckboxSpec | TextAreaSpec | InputSpec | ListBoxSpec | SelectBoxSpec | SizeInputSpec | SliderSpec | IframeSpec | HtmlPanelSpec | UrlInputSpec | DropZoneSpec | ColorInputSpec | GridSpec | ColorPickerSpec | ImagePreviewSpec | AlertBannerSpec | CollectionSpec | LabelSpec | TableSpec | TreeSpec | PanelSpec | CustomEditorSpec;\ninterface BarSpec {\n    type: 'bar';\n    items: BodyComponentSpec[];\n}\ninterface DialogToggleMenuItemSpec extends CommonMenuItemSpec {\n    type?: 'togglemenuitem';\n    name: string;\n}\ntype DialogFooterMenuButtonItemSpec = DialogToggleMenuItemSpec;\ninterface BaseDialogFooterButtonSpec {\n    name?: string;\n    align?: 'start' | 'end';\n    primary?: boolean;\n    enabled?: boolean;\n    icon?: string;\n    buttonType?: 'primary' | 'secondary';\n}\ninterface DialogFooterNormalButtonSpec extends BaseDialogFooterButtonSpec {\n    type: 'submit' | 'cancel' | 'custom';\n    text: string;\n}\ninterface DialogFooterMenuButtonSpec extends BaseDialogFooterButtonSpec {\n    type: 'menu';\n    text?: string;\n    tooltip?: string;\n    icon?: string;\n    items: DialogFooterMenuButtonItemSpec[];\n}\ninterface DialogFooterToggleButtonSpec extends BaseDialogFooterButtonSpec {\n    type: 'togglebutton';\n    tooltip?: string;\n    icon?: string;\n    text?: string;\n    active?: boolean;\n}\ntype DialogFooterButtonSpec = DialogFooterNormalButtonSpec | DialogFooterMenuButtonSpec | DialogFooterToggleButtonSpec;\ninterface TabSpec {\n    name?: string;\n    title: string;\n    items: BodyComponentSpec[];\n}\ninterface TabPanelSpec {\n    type: 'tabpanel';\n    tabs: TabSpec[];\n}\ntype DialogDataItem = any;\ntype DialogData = Record<string, DialogDataItem>;\ninterface DialogInstanceApi<T extends DialogData> {\n    getData: () => T;\n    setData: (data: Partial<T>) => void;\n    setEnabled: (name: string, state: boolean) => void;\n    focus: (name: string) => void;\n    showTab: (name: string) => void;\n    redial: (nu: DialogSpec<T>) => void;\n    block: (msg: string) => void;\n    unblock: () => void;\n    toggleFullscreen: () => void;\n    close: () => void;\n}\ninterface DialogActionDetails {\n    name: string;\n    value?: any;\n}\ninterface DialogChangeDetails<T> {\n    name: keyof T;\n}\ninterface DialogTabChangeDetails {\n    newTabName: string;\n    oldTabName: string;\n}\ntype DialogActionHandler<T extends DialogData> = (api: DialogInstanceApi<T>, details: DialogActionDetails) => void;\ntype DialogChangeHandler<T extends DialogData> = (api: DialogInstanceApi<T>, details: DialogChangeDetails<T>) => void;\ntype DialogSubmitHandler<T extends DialogData> = (api: DialogInstanceApi<T>) => void;\ntype DialogCloseHandler = () => void;\ntype DialogCancelHandler<T extends DialogData> = (api: DialogInstanceApi<T>) => void;\ntype DialogTabChangeHandler<T extends DialogData> = (api: DialogInstanceApi<T>, details: DialogTabChangeDetails) => void;\ntype DialogSize = 'normal' | 'medium' | 'large';\ninterface DialogSpec<T extends DialogData> {\n    title: string;\n    size?: DialogSize;\n    body: TabPanelSpec | PanelSpec;\n    buttons?: DialogFooterButtonSpec[];\n    initialData?: Partial<T>;\n    onAction?: DialogActionHandler<T>;\n    onChange?: DialogChangeHandler<T>;\n    onSubmit?: DialogSubmitHandler<T>;\n    onClose?: DialogCloseHandler;\n    onCancel?: DialogCancelHandler<T>;\n    onTabChange?: DialogTabChangeHandler<T>;\n}\ninterface UrlDialogInstanceApi {\n    block: (msg: string) => void;\n    unblock: () => void;\n    close: () => void;\n    sendMessage: (msg: any) => void;\n}\ninterface UrlDialogActionDetails {\n    name: string;\n    value?: any;\n}\ninterface UrlDialogMessage {\n    mceAction: string;\n    [key: string]: any;\n}\ntype UrlDialogActionHandler = (api: UrlDialogInstanceApi, actions: UrlDialogActionDetails) => void;\ntype UrlDialogCloseHandler = () => void;\ntype UrlDialogCancelHandler = (api: UrlDialogInstanceApi) => void;\ntype UrlDialogMessageHandler = (api: UrlDialogInstanceApi, message: UrlDialogMessage) => void;\ninterface UrlDialogFooterButtonSpec extends DialogFooterNormalButtonSpec {\n    type: 'cancel' | 'custom';\n}\ninterface UrlDialogSpec {\n    title: string;\n    url: string;\n    height?: number;\n    width?: number;\n    buttons?: UrlDialogFooterButtonSpec[];\n    onAction?: UrlDialogActionHandler;\n    onClose?: UrlDialogCloseHandler;\n    onCancel?: UrlDialogCancelHandler;\n    onMessage?: UrlDialogMessageHandler;\n}\ntype ColumnTypes = number | 'auto';\ntype SeparatorItemSpec = SeparatorMenuItemSpec;\ninterface AutocompleterItemSpec {\n    type?: 'autocompleteitem';\n    value: string;\n    text?: string;\n    icon?: string;\n    meta?: Record<string, any>;\n}\ntype AutocompleterContents = SeparatorItemSpec | AutocompleterItemSpec | CardMenuItemSpec;\ninterface AutocompleterSpec {\n    type?: 'autocompleter';\n    ch?: string;\n    trigger?: string;\n    minChars?: number;\n    columns?: ColumnTypes;\n    matches?: (rng: Range, text: string, pattern: string) => boolean;\n    fetch: (pattern: string, maxResults: number, fetchOptions: Record<string, any>) => Promise<AutocompleterContents[]>;\n    onAction: (autocompleterApi: AutocompleterInstanceApi, rng: Range, value: string, meta: Record<string, any>) => void;\n    maxResults?: number;\n    highlightOn?: string[];\n}\ninterface AutocompleterInstanceApi {\n    hide: () => void;\n    reload: (fetchOptions: Record<string, any>) => void;\n}\ntype ContextPosition = 'node' | 'selection' | 'line';\ntype ContextScope = 'node' | 'editor';\ninterface ContextBarSpec {\n    predicate?: (elem: Element) => boolean;\n    position?: ContextPosition;\n    scope?: ContextScope;\n}\ninterface ContextFormLaunchButtonApi extends BaseToolbarButtonSpec<BaseToolbarButtonInstanceApi> {\n    type: 'contextformbutton';\n}\ninterface ContextFormLaunchToggleButtonSpec extends BaseToolbarToggleButtonSpec<BaseToolbarToggleButtonInstanceApi> {\n    type: 'contextformtogglebutton';\n}\ninterface ContextFormButtonInstanceApi extends BaseToolbarButtonInstanceApi {\n}\ninterface ContextFormToggleButtonInstanceApi extends BaseToolbarToggleButtonInstanceApi {\n}\ninterface ContextFormButtonSpec extends BaseToolbarButtonSpec<ContextFormButtonInstanceApi> {\n    type?: 'contextformbutton';\n    primary?: boolean;\n    onAction: (formApi: ContextFormInstanceApi, api: ContextFormButtonInstanceApi) => void;\n}\ninterface ContextFormToggleButtonSpec extends BaseToolbarToggleButtonSpec<ContextFormToggleButtonInstanceApi> {\n    type?: 'contextformtogglebutton';\n    onAction: (formApi: ContextFormInstanceApi, buttonApi: ContextFormToggleButtonInstanceApi) => void;\n    primary?: boolean;\n}\ninterface ContextFormInstanceApi {\n    hide: () => void;\n    getValue: () => string;\n}\ninterface ContextFormSpec extends ContextBarSpec {\n    type?: 'contextform';\n    initValue?: () => string;\n    label?: string;\n    launch?: ContextFormLaunchButtonApi | ContextFormLaunchToggleButtonSpec;\n    commands: Array<ContextFormToggleButtonSpec | ContextFormButtonSpec>;\n}\ninterface ContextToolbarSpec extends ContextBarSpec {\n    type?: 'contexttoolbar';\n    items: string;\n}\ntype PublicDialog_d_AlertBannerSpec = AlertBannerSpec;\ntype PublicDialog_d_BarSpec = BarSpec;\ntype PublicDialog_d_BodyComponentSpec = BodyComponentSpec;\ntype PublicDialog_d_ButtonSpec = ButtonSpec;\ntype PublicDialog_d_CheckboxSpec = CheckboxSpec;\ntype PublicDialog_d_CollectionItem = CollectionItem;\ntype PublicDialog_d_CollectionSpec = CollectionSpec;\ntype PublicDialog_d_ColorInputSpec = ColorInputSpec;\ntype PublicDialog_d_ColorPickerSpec = ColorPickerSpec;\ntype PublicDialog_d_CustomEditorSpec = CustomEditorSpec;\ntype PublicDialog_d_CustomEditorInit = CustomEditorInit;\ntype PublicDialog_d_CustomEditorInitFn = CustomEditorInitFn;\ntype PublicDialog_d_DialogData = DialogData;\ntype PublicDialog_d_DialogSize = DialogSize;\ntype PublicDialog_d_DialogSpec<T extends DialogData> = DialogSpec<T>;\ntype PublicDialog_d_DialogInstanceApi<T extends DialogData> = DialogInstanceApi<T>;\ntype PublicDialog_d_DialogFooterButtonSpec = DialogFooterButtonSpec;\ntype PublicDialog_d_DialogActionDetails = DialogActionDetails;\ntype PublicDialog_d_DialogChangeDetails<T> = DialogChangeDetails<T>;\ntype PublicDialog_d_DialogTabChangeDetails = DialogTabChangeDetails;\ntype PublicDialog_d_DropZoneSpec = DropZoneSpec;\ntype PublicDialog_d_GridSpec = GridSpec;\ntype PublicDialog_d_HtmlPanelSpec = HtmlPanelSpec;\ntype PublicDialog_d_IframeSpec = IframeSpec;\ntype PublicDialog_d_ImagePreviewSpec = ImagePreviewSpec;\ntype PublicDialog_d_InputSpec = InputSpec;\ntype PublicDialog_d_LabelSpec = LabelSpec;\ntype PublicDialog_d_ListBoxSpec = ListBoxSpec;\ntype PublicDialog_d_ListBoxItemSpec = ListBoxItemSpec;\ntype PublicDialog_d_ListBoxNestedItemSpec = ListBoxNestedItemSpec;\ntype PublicDialog_d_ListBoxSingleItemSpec = ListBoxSingleItemSpec;\ntype PublicDialog_d_PanelSpec = PanelSpec;\ntype PublicDialog_d_SelectBoxSpec = SelectBoxSpec;\ntype PublicDialog_d_SelectBoxItemSpec = SelectBoxItemSpec;\ntype PublicDialog_d_SizeInputSpec = SizeInputSpec;\ntype PublicDialog_d_SliderSpec = SliderSpec;\ntype PublicDialog_d_TableSpec = TableSpec;\ntype PublicDialog_d_TabSpec = TabSpec;\ntype PublicDialog_d_TabPanelSpec = TabPanelSpec;\ntype PublicDialog_d_TextAreaSpec = TextAreaSpec;\ntype PublicDialog_d_TreeSpec = TreeSpec;\ntype PublicDialog_d_TreeItemSpec = TreeItemSpec;\ntype PublicDialog_d_UrlInputData = UrlInputData;\ntype PublicDialog_d_UrlInputSpec = UrlInputSpec;\ntype PublicDialog_d_UrlDialogSpec = UrlDialogSpec;\ntype PublicDialog_d_UrlDialogFooterButtonSpec = UrlDialogFooterButtonSpec;\ntype PublicDialog_d_UrlDialogInstanceApi = UrlDialogInstanceApi;\ntype PublicDialog_d_UrlDialogActionDetails = UrlDialogActionDetails;\ntype PublicDialog_d_UrlDialogMessage = UrlDialogMessage;\ndeclare namespace PublicDialog_d {\n    export { PublicDialog_d_AlertBannerSpec as AlertBannerSpec, PublicDialog_d_BarSpec as BarSpec, PublicDialog_d_BodyComponentSpec as BodyComponentSpec, PublicDialog_d_ButtonSpec as ButtonSpec, PublicDialog_d_CheckboxSpec as CheckboxSpec, PublicDialog_d_CollectionItem as CollectionItem, PublicDialog_d_CollectionSpec as CollectionSpec, PublicDialog_d_ColorInputSpec as ColorInputSpec, PublicDialog_d_ColorPickerSpec as ColorPickerSpec, PublicDialog_d_CustomEditorSpec as CustomEditorSpec, PublicDialog_d_CustomEditorInit as CustomEditorInit, PublicDialog_d_CustomEditorInitFn as CustomEditorInitFn, PublicDialog_d_DialogData as DialogData, PublicDialog_d_DialogSize as DialogSize, PublicDialog_d_DialogSpec as DialogSpec, PublicDialog_d_DialogInstanceApi as DialogInstanceApi, PublicDialog_d_DialogFooterButtonSpec as DialogFooterButtonSpec, PublicDialog_d_DialogActionDetails as DialogActionDetails, PublicDialog_d_DialogChangeDetails as DialogChangeDetails, PublicDialog_d_DialogTabChangeDetails as DialogTabChangeDetails, PublicDialog_d_DropZoneSpec as DropZoneSpec, PublicDialog_d_GridSpec as GridSpec, PublicDialog_d_HtmlPanelSpec as HtmlPanelSpec, PublicDialog_d_IframeSpec as IframeSpec, PublicDialog_d_ImagePreviewSpec as ImagePreviewSpec, PublicDialog_d_InputSpec as InputSpec, PublicDialog_d_LabelSpec as LabelSpec, PublicDialog_d_ListBoxSpec as ListBoxSpec, PublicDialog_d_ListBoxItemSpec as ListBoxItemSpec, PublicDialog_d_ListBoxNestedItemSpec as ListBoxNestedItemSpec, PublicDialog_d_ListBoxSingleItemSpec as ListBoxSingleItemSpec, PublicDialog_d_PanelSpec as PanelSpec, PublicDialog_d_SelectBoxSpec as SelectBoxSpec, PublicDialog_d_SelectBoxItemSpec as SelectBoxItemSpec, PublicDialog_d_SizeInputSpec as SizeInputSpec, PublicDialog_d_SliderSpec as SliderSpec, PublicDialog_d_TableSpec as TableSpec, PublicDialog_d_TabSpec as TabSpec, PublicDialog_d_TabPanelSpec as TabPanelSpec, PublicDialog_d_TextAreaSpec as TextAreaSpec, PublicDialog_d_TreeSpec as TreeSpec, PublicDialog_d_TreeItemSpec as TreeItemSpec, DirectorySpec as TreeDirectorySpec, LeafSpec as TreeLeafSpec, PublicDialog_d_UrlInputData as UrlInputData, PublicDialog_d_UrlInputSpec as UrlInputSpec, PublicDialog_d_UrlDialogSpec as UrlDialogSpec, PublicDialog_d_UrlDialogFooterButtonSpec as UrlDialogFooterButtonSpec, PublicDialog_d_UrlDialogInstanceApi as UrlDialogInstanceApi, PublicDialog_d_UrlDialogActionDetails as UrlDialogActionDetails, PublicDialog_d_UrlDialogMessage as UrlDialogMessage, };\n}\ntype PublicInlineContent_d_AutocompleterSpec = AutocompleterSpec;\ntype PublicInlineContent_d_AutocompleterItemSpec = AutocompleterItemSpec;\ntype PublicInlineContent_d_AutocompleterContents = AutocompleterContents;\ntype PublicInlineContent_d_AutocompleterInstanceApi = AutocompleterInstanceApi;\ntype PublicInlineContent_d_ContextPosition = ContextPosition;\ntype PublicInlineContent_d_ContextScope = ContextScope;\ntype PublicInlineContent_d_ContextFormSpec = ContextFormSpec;\ntype PublicInlineContent_d_ContextFormInstanceApi = ContextFormInstanceApi;\ntype PublicInlineContent_d_ContextFormButtonSpec = ContextFormButtonSpec;\ntype PublicInlineContent_d_ContextFormButtonInstanceApi = ContextFormButtonInstanceApi;\ntype PublicInlineContent_d_ContextFormToggleButtonSpec = ContextFormToggleButtonSpec;\ntype PublicInlineContent_d_ContextFormToggleButtonInstanceApi = ContextFormToggleButtonInstanceApi;\ntype PublicInlineContent_d_ContextToolbarSpec = ContextToolbarSpec;\ntype PublicInlineContent_d_SeparatorItemSpec = SeparatorItemSpec;\ndeclare namespace PublicInlineContent_d {\n    export { PublicInlineContent_d_AutocompleterSpec as AutocompleterSpec, PublicInlineContent_d_AutocompleterItemSpec as AutocompleterItemSpec, PublicInlineContent_d_AutocompleterContents as AutocompleterContents, PublicInlineContent_d_AutocompleterInstanceApi as AutocompleterInstanceApi, PublicInlineContent_d_ContextPosition as ContextPosition, PublicInlineContent_d_ContextScope as ContextScope, PublicInlineContent_d_ContextFormSpec as ContextFormSpec, PublicInlineContent_d_ContextFormInstanceApi as ContextFormInstanceApi, PublicInlineContent_d_ContextFormButtonSpec as ContextFormButtonSpec, PublicInlineContent_d_ContextFormButtonInstanceApi as ContextFormButtonInstanceApi, PublicInlineContent_d_ContextFormToggleButtonSpec as ContextFormToggleButtonSpec, PublicInlineContent_d_ContextFormToggleButtonInstanceApi as ContextFormToggleButtonInstanceApi, PublicInlineContent_d_ContextToolbarSpec as ContextToolbarSpec, PublicInlineContent_d_SeparatorItemSpec as SeparatorItemSpec, };\n}\ntype PublicMenu_d_MenuItemSpec = MenuItemSpec;\ntype PublicMenu_d_MenuItemInstanceApi = MenuItemInstanceApi;\ntype PublicMenu_d_NestedMenuItemContents = NestedMenuItemContents;\ntype PublicMenu_d_NestedMenuItemSpec = NestedMenuItemSpec;\ntype PublicMenu_d_NestedMenuItemInstanceApi = NestedMenuItemInstanceApi;\ntype PublicMenu_d_FancyMenuItemSpec = FancyMenuItemSpec;\ntype PublicMenu_d_ColorSwatchMenuItemSpec = ColorSwatchMenuItemSpec;\ntype PublicMenu_d_InsertTableMenuItemSpec = InsertTableMenuItemSpec;\ntype PublicMenu_d_ToggleMenuItemSpec = ToggleMenuItemSpec;\ntype PublicMenu_d_ToggleMenuItemInstanceApi = ToggleMenuItemInstanceApi;\ntype PublicMenu_d_ChoiceMenuItemSpec = ChoiceMenuItemSpec;\ntype PublicMenu_d_ChoiceMenuItemInstanceApi = ChoiceMenuItemInstanceApi;\ntype PublicMenu_d_SeparatorMenuItemSpec = SeparatorMenuItemSpec;\ntype PublicMenu_d_ContextMenuApi = ContextMenuApi;\ntype PublicMenu_d_ContextMenuContents = ContextMenuContents;\ntype PublicMenu_d_ContextMenuItem = ContextMenuItem;\ntype PublicMenu_d_ContextSubMenu = ContextSubMenu;\ntype PublicMenu_d_CardMenuItemSpec = CardMenuItemSpec;\ntype PublicMenu_d_CardMenuItemInstanceApi = CardMenuItemInstanceApi;\ntype PublicMenu_d_CardItemSpec = CardItemSpec;\ntype PublicMenu_d_CardContainerSpec = CardContainerSpec;\ntype PublicMenu_d_CardImageSpec = CardImageSpec;\ntype PublicMenu_d_CardTextSpec = CardTextSpec;\ndeclare namespace PublicMenu_d {\n    export { PublicMenu_d_MenuItemSpec as MenuItemSpec, PublicMenu_d_MenuItemInstanceApi as MenuItemInstanceApi, PublicMenu_d_NestedMenuItemContents as NestedMenuItemContents, PublicMenu_d_NestedMenuItemSpec as NestedMenuItemSpec, PublicMenu_d_NestedMenuItemInstanceApi as NestedMenuItemInstanceApi, PublicMenu_d_FancyMenuItemSpec as FancyMenuItemSpec, PublicMenu_d_ColorSwatchMenuItemSpec as ColorSwatchMenuItemSpec, PublicMenu_d_InsertTableMenuItemSpec as InsertTableMenuItemSpec, PublicMenu_d_ToggleMenuItemSpec as ToggleMenuItemSpec, PublicMenu_d_ToggleMenuItemInstanceApi as ToggleMenuItemInstanceApi, PublicMenu_d_ChoiceMenuItemSpec as ChoiceMenuItemSpec, PublicMenu_d_ChoiceMenuItemInstanceApi as ChoiceMenuItemInstanceApi, PublicMenu_d_SeparatorMenuItemSpec as SeparatorMenuItemSpec, PublicMenu_d_ContextMenuApi as ContextMenuApi, PublicMenu_d_ContextMenuContents as ContextMenuContents, PublicMenu_d_ContextMenuItem as ContextMenuItem, PublicMenu_d_ContextSubMenu as ContextSubMenu, PublicMenu_d_CardMenuItemSpec as CardMenuItemSpec, PublicMenu_d_CardMenuItemInstanceApi as CardMenuItemInstanceApi, PublicMenu_d_CardItemSpec as CardItemSpec, PublicMenu_d_CardContainerSpec as CardContainerSpec, PublicMenu_d_CardImageSpec as CardImageSpec, PublicMenu_d_CardTextSpec as CardTextSpec, };\n}\ninterface SidebarInstanceApi {\n    element: () => HTMLElement;\n}\ninterface SidebarSpec {\n    icon?: string;\n    tooltip?: string;\n    onShow?: (api: SidebarInstanceApi) => void;\n    onSetup?: (api: SidebarInstanceApi) => (api: SidebarInstanceApi) => void;\n    onHide?: (api: SidebarInstanceApi) => void;\n}\ntype PublicSidebar_d_SidebarSpec = SidebarSpec;\ntype PublicSidebar_d_SidebarInstanceApi = SidebarInstanceApi;\ndeclare namespace PublicSidebar_d {\n    export { PublicSidebar_d_SidebarSpec as SidebarSpec, PublicSidebar_d_SidebarInstanceApi as SidebarInstanceApi, };\n}\ntype PublicToolbar_d_ToolbarButtonSpec = ToolbarButtonSpec;\ntype PublicToolbar_d_ToolbarButtonInstanceApi = ToolbarButtonInstanceApi;\ntype PublicToolbar_d_ToolbarSplitButtonSpec = ToolbarSplitButtonSpec;\ntype PublicToolbar_d_ToolbarSplitButtonInstanceApi = ToolbarSplitButtonInstanceApi;\ntype PublicToolbar_d_ToolbarMenuButtonSpec = ToolbarMenuButtonSpec;\ntype PublicToolbar_d_ToolbarMenuButtonInstanceApi = ToolbarMenuButtonInstanceApi;\ntype PublicToolbar_d_ToolbarToggleButtonSpec = ToolbarToggleButtonSpec;\ntype PublicToolbar_d_ToolbarToggleButtonInstanceApi = ToolbarToggleButtonInstanceApi;\ntype PublicToolbar_d_GroupToolbarButtonSpec = GroupToolbarButtonSpec;\ntype PublicToolbar_d_GroupToolbarButtonInstanceApi = GroupToolbarButtonInstanceApi;\ndeclare namespace PublicToolbar_d {\n    export { PublicToolbar_d_ToolbarButtonSpec as ToolbarButtonSpec, PublicToolbar_d_ToolbarButtonInstanceApi as ToolbarButtonInstanceApi, PublicToolbar_d_ToolbarSplitButtonSpec as ToolbarSplitButtonSpec, PublicToolbar_d_ToolbarSplitButtonInstanceApi as ToolbarSplitButtonInstanceApi, PublicToolbar_d_ToolbarMenuButtonSpec as ToolbarMenuButtonSpec, PublicToolbar_d_ToolbarMenuButtonInstanceApi as ToolbarMenuButtonInstanceApi, PublicToolbar_d_ToolbarToggleButtonSpec as ToolbarToggleButtonSpec, PublicToolbar_d_ToolbarToggleButtonInstanceApi as ToolbarToggleButtonInstanceApi, PublicToolbar_d_GroupToolbarButtonSpec as GroupToolbarButtonSpec, PublicToolbar_d_GroupToolbarButtonInstanceApi as GroupToolbarButtonInstanceApi, };\n}\ninterface ViewButtonApi {\n    setIcon: (newIcon: string) => void;\n}\ninterface ViewToggleButtonApi extends ViewButtonApi {\n    isActive: () => boolean;\n    setActive: (state: boolean) => void;\n}\ninterface BaseButtonSpec<Api extends ViewButtonApi> {\n    text?: string;\n    icon?: string;\n    tooltip?: string;\n    buttonType?: 'primary' | 'secondary';\n    borderless?: boolean;\n    onAction: (api: Api) => void;\n}\ninterface ViewNormalButtonSpec extends BaseButtonSpec<ViewButtonApi> {\n    text: string;\n    type: 'button';\n}\ninterface ViewToggleButtonSpec extends BaseButtonSpec<ViewToggleButtonApi> {\n    type: 'togglebutton';\n    active?: boolean;\n    onAction: (api: ViewToggleButtonApi) => void;\n}\ninterface ViewButtonsGroupSpec {\n    type: 'group';\n    buttons: Array<ViewNormalButtonSpec | ViewToggleButtonSpec>;\n}\ntype ViewButtonSpec = ViewNormalButtonSpec | ViewToggleButtonSpec | ViewButtonsGroupSpec;\ninterface ViewInstanceApi {\n    getContainer: () => HTMLElement;\n}\ninterface ViewSpec {\n    buttons?: ViewButtonSpec[];\n    onShow: (api: ViewInstanceApi) => void;\n    onHide: (api: ViewInstanceApi) => void;\n}\ntype PublicView_d_ViewSpec = ViewSpec;\ntype PublicView_d_ViewInstanceApi = ViewInstanceApi;\ndeclare namespace PublicView_d {\n    export { PublicView_d_ViewSpec as ViewSpec, PublicView_d_ViewInstanceApi as ViewInstanceApi, };\n}\ninterface Registry$1 {\n    addButton: (name: string, spec: ToolbarButtonSpec) => void;\n    addGroupToolbarButton: (name: string, spec: GroupToolbarButtonSpec) => void;\n    addToggleButton: (name: string, spec: ToolbarToggleButtonSpec) => void;\n    addMenuButton: (name: string, spec: ToolbarMenuButtonSpec) => void;\n    addSplitButton: (name: string, spec: ToolbarSplitButtonSpec) => void;\n    addMenuItem: (name: string, spec: MenuItemSpec) => void;\n    addNestedMenuItem: (name: string, spec: NestedMenuItemSpec) => void;\n    addToggleMenuItem: (name: string, spec: ToggleMenuItemSpec) => void;\n    addContextMenu: (name: string, spec: ContextMenuApi) => void;\n    addContextToolbar: (name: string, spec: ContextToolbarSpec) => void;\n    addContextForm: (name: string, spec: ContextFormSpec) => void;\n    addIcon: (name: string, svgData: string) => void;\n    addAutocompleter: (name: string, spec: AutocompleterSpec) => void;\n    addSidebar: (name: string, spec: SidebarSpec) => void;\n    addView: (name: string, spec: ViewSpec) => void;\n    getAll: () => {\n        buttons: Record<string, ToolbarButtonSpec | GroupToolbarButtonSpec | ToolbarMenuButtonSpec | ToolbarSplitButtonSpec | ToolbarToggleButtonSpec>;\n        menuItems: Record<string, MenuItemSpec | NestedMenuItemSpec | ToggleMenuItemSpec>;\n        popups: Record<string, AutocompleterSpec>;\n        contextMenus: Record<string, ContextMenuApi>;\n        contextToolbars: Record<string, ContextToolbarSpec | ContextFormSpec>;\n        icons: Record<string, string>;\n        sidebars: Record<string, SidebarSpec>;\n        views: Record<string, ViewSpec>;\n    };\n}\ninterface AutocompleteLookupData {\n    readonly matchText: string;\n    readonly items: AutocompleterContents[];\n    readonly columns: ColumnTypes;\n    readonly onAction: (autoApi: AutocompleterInstanceApi, rng: Range, value: string, meta: Record<string, any>) => void;\n    readonly highlightOn: string[];\n}\ninterface AutocompleterEventArgs {\n    readonly lookupData: AutocompleteLookupData[];\n}\ninterface RangeLikeObject {\n    startContainer: Node;\n    startOffset: number;\n    endContainer: Node;\n    endOffset: number;\n}\ntype ApplyFormat = BlockFormat | InlineFormat | SelectorFormat;\ntype RemoveFormat = RemoveBlockFormat | RemoveInlineFormat | RemoveSelectorFormat;\ntype Format = ApplyFormat | RemoveFormat;\ntype Formats = Record<string, Format | Format[]>;\ntype FormatAttrOrStyleValue = string | ((vars?: FormatVars) => string | null);\ntype FormatVars = Record<string, string | null>;\ninterface BaseFormat<T> {\n    ceFalseOverride?: boolean;\n    classes?: string | string[];\n    collapsed?: boolean;\n    exact?: boolean;\n    expand?: boolean;\n    links?: boolean;\n    mixed?: boolean;\n    block_expand?: boolean;\n    onmatch?: (node: Element, fmt: T, itemName: string) => boolean;\n    remove?: 'none' | 'empty' | 'all';\n    remove_similar?: boolean;\n    split?: boolean;\n    deep?: boolean;\n    preserve_attributes?: string[];\n}\ninterface Block {\n    block: string;\n    list_block?: string;\n    wrapper?: boolean;\n}\ninterface Inline {\n    inline: string;\n}\ninterface Selector {\n    selector: string;\n    inherit?: boolean;\n}\ninterface CommonFormat<T> extends BaseFormat<T> {\n    attributes?: Record<string, FormatAttrOrStyleValue>;\n    styles?: Record<string, FormatAttrOrStyleValue>;\n    toggle?: boolean;\n    preview?: string | false;\n    onformat?: (elm: Element, fmt: T, vars?: FormatVars, node?: Node | RangeLikeObject | null) => void;\n    clear_child_styles?: boolean;\n    merge_siblings?: boolean;\n    merge_with_parents?: boolean;\n}\ninterface BlockFormat extends Block, CommonFormat<BlockFormat> {\n}\ninterface InlineFormat extends Inline, CommonFormat<InlineFormat> {\n}\ninterface SelectorFormat extends Selector, CommonFormat<SelectorFormat> {\n}\ninterface CommonRemoveFormat<T> extends BaseFormat<T> {\n    attributes?: string[] | Record<string, FormatAttrOrStyleValue>;\n    styles?: string[] | Record<string, FormatAttrOrStyleValue>;\n}\ninterface RemoveBlockFormat extends Block, CommonRemoveFormat<RemoveBlockFormat> {\n}\ninterface RemoveInlineFormat extends Inline, CommonRemoveFormat<RemoveInlineFormat> {\n}\ninterface RemoveSelectorFormat extends Selector, CommonRemoveFormat<RemoveSelectorFormat> {\n}\ninterface Filter<C extends Function> {\n    name: string;\n    callbacks: C[];\n}\ninterface ParserArgs {\n    getInner?: boolean | number;\n    forced_root_block?: boolean | string;\n    context?: string;\n    isRootContent?: boolean;\n    format?: string;\n    invalid?: boolean;\n    no_events?: boolean;\n    [key: string]: any;\n}\ntype ParserFilterCallback = (nodes: AstNode[], name: string, args: ParserArgs) => void;\ninterface ParserFilter extends Filter<ParserFilterCallback> {\n}\ninterface DomParserSettings {\n    allow_html_data_urls?: boolean;\n    allow_svg_data_urls?: boolean;\n    allow_conditional_comments?: boolean;\n    allow_html_in_named_anchor?: boolean;\n    allow_script_urls?: boolean;\n    allow_unsafe_link_target?: boolean;\n    blob_cache?: BlobCache;\n    convert_fonts_to_spans?: boolean;\n    convert_unsafe_embeds?: boolean;\n    document?: Document;\n    fix_list_elements?: boolean;\n    font_size_legacy_values?: string;\n    forced_root_block?: boolean | string;\n    forced_root_block_attrs?: Record<string, string>;\n    inline_styles?: boolean;\n    pad_empty_with_br?: boolean;\n    preserve_cdata?: boolean;\n    remove_trailing_brs?: boolean;\n    root_name?: string;\n    sandbox_iframes?: boolean;\n    sanitize?: boolean;\n    validate?: boolean;\n}\ninterface DomParser {\n    schema: Schema;\n    addAttributeFilter: (name: string, callback: ParserFilterCallback) => void;\n    getAttributeFilters: () => ParserFilter[];\n    removeAttributeFilter: (name: string, callback?: ParserFilterCallback) => void;\n    addNodeFilter: (name: string, callback: ParserFilterCallback) => void;\n    getNodeFilters: () => ParserFilter[];\n    removeNodeFilter: (name: string, callback?: ParserFilterCallback) => void;\n    parse: (html: string, args?: ParserArgs) => AstNode;\n}\ninterface StyleSheetLoaderSettings {\n    maxLoadTime?: number;\n    contentCssCors?: boolean;\n    referrerPolicy?: ReferrerPolicy;\n}\ninterface StyleSheetLoader {\n    load: (url: string) => Promise<void>;\n    loadRawCss: (key: string, css: string) => void;\n    loadAll: (urls: string[]) => Promise<string[]>;\n    unload: (url: string) => void;\n    unloadRawCss: (key: string) => void;\n    unloadAll: (urls: string[]) => void;\n    _setReferrerPolicy: (referrerPolicy: ReferrerPolicy) => void;\n    _setContentCssCors: (contentCssCors: boolean) => void;\n}\ntype Registry = Registry$1;\ninterface EditorUiApi {\n    show: () => void;\n    hide: () => void;\n    setEnabled: (state: boolean) => void;\n    isEnabled: () => boolean;\n}\ninterface EditorUi extends EditorUiApi {\n    registry: Registry;\n    styleSheetLoader: StyleSheetLoader;\n}\ntype Ui_d_Registry = Registry;\ntype Ui_d_EditorUiApi = EditorUiApi;\ntype Ui_d_EditorUi = EditorUi;\ndeclare namespace Ui_d {\n    export { Ui_d_Registry as Registry, PublicDialog_d as Dialog, PublicInlineContent_d as InlineContent, PublicMenu_d as Menu, PublicView_d as View, PublicSidebar_d as Sidebar, PublicToolbar_d as Toolbar, Ui_d_EditorUiApi as EditorUiApi, Ui_d_EditorUi as EditorUi, };\n}\ninterface WindowParams {\n    readonly inline?: 'cursor' | 'toolbar' | 'bottom';\n    readonly ariaAttrs?: boolean;\n    readonly persistent?: boolean;\n}\ntype InstanceApi<T extends DialogData> = UrlDialogInstanceApi | DialogInstanceApi<T>;\ninterface WindowManagerImpl {\n    open: <T extends DialogData>(config: DialogSpec<T>, params: WindowParams | undefined, closeWindow: (dialog: DialogInstanceApi<T>) => void) => DialogInstanceApi<T>;\n    openUrl: (config: UrlDialogSpec, closeWindow: (dialog: UrlDialogInstanceApi) => void) => UrlDialogInstanceApi;\n    alert: (message: string, callback: () => void) => void;\n    confirm: (message: string, callback: (state: boolean) => void) => void;\n    close: (dialog: InstanceApi<any>) => void;\n}\ninterface WindowManager {\n    open: <T extends DialogData>(config: DialogSpec<T>, params?: WindowParams) => DialogInstanceApi<T>;\n    openUrl: (config: UrlDialogSpec) => UrlDialogInstanceApi;\n    alert: (message: string, callback?: () => void, scope?: any) => void;\n    confirm: (message: string, callback?: (state: boolean) => void, scope?: any) => void;\n    close: () => void;\n}\ninterface ExecCommandEvent {\n    command: string;\n    ui: boolean;\n    value?: any;\n}\ninterface BeforeGetContentEvent extends GetContentArgs {\n    selection?: boolean;\n}\ninterface GetContentEvent extends BeforeGetContentEvent {\n    content: string;\n}\ninterface BeforeSetContentEvent extends SetContentArgs {\n    content: string;\n    selection?: boolean;\n}\ninterface SetContentEvent extends BeforeSetContentEvent {\n    content: string;\n}\ninterface SaveContentEvent extends GetContentEvent {\n    save: boolean;\n}\ninterface NewBlockEvent {\n    newBlock: Element;\n}\ninterface NodeChangeEvent {\n    element: Element;\n    parents: Node[];\n    selectionChange?: boolean;\n    initial?: boolean;\n}\ninterface FormatEvent {\n    format: string;\n    vars?: FormatVars;\n    node?: Node | RangeLikeObject | null;\n}\ninterface ObjectResizeEvent {\n    target: HTMLElement;\n    width: number;\n    height: number;\n    origin: string;\n}\ninterface ObjectSelectedEvent {\n    target: Node;\n    targetClone?: Node;\n}\ninterface ScrollIntoViewEvent {\n    elm: HTMLElement;\n    alignToTop: boolean | undefined;\n}\ninterface SetSelectionRangeEvent {\n    range: Range;\n    forward: boolean | undefined;\n}\ninterface ShowCaretEvent {\n    target: Node;\n    direction: number;\n    before: boolean;\n}\ninterface SwitchModeEvent {\n    mode: string;\n}\ninterface ChangeEvent {\n    level: UndoLevel;\n    lastLevel: UndoLevel | undefined;\n}\ninterface AddUndoEvent extends ChangeEvent {\n    originalEvent: Event | undefined;\n}\ninterface UndoRedoEvent {\n    level: UndoLevel;\n}\ninterface WindowEvent<T extends DialogData> {\n    dialog: InstanceApi<T>;\n}\ninterface ProgressStateEvent {\n    state: boolean;\n    time?: number;\n}\ninterface AfterProgressStateEvent {\n    state: boolean;\n}\ninterface PlaceholderToggleEvent {\n    state: boolean;\n}\ninterface LoadErrorEvent {\n    message: string;\n}\ninterface PreProcessEvent extends ParserArgs {\n    node: Element;\n}\ninterface PostProcessEvent extends ParserArgs {\n    content: string;\n}\ninterface PastePlainTextToggleEvent {\n    state: boolean;\n}\ninterface PastePreProcessEvent {\n    content: string;\n    readonly internal: boolean;\n}\ninterface PastePostProcessEvent {\n    node: HTMLElement;\n    readonly internal: boolean;\n}\ninterface EditableRootStateChangeEvent {\n    state: boolean;\n}\ninterface NewTableRowEvent {\n    node: HTMLTableRowElement;\n}\ninterface NewTableCellEvent {\n    node: HTMLTableCellElement;\n}\ninterface TableEventData {\n    readonly structure: boolean;\n    readonly style: boolean;\n}\ninterface TableModifiedEvent extends TableEventData {\n    readonly table: HTMLTableElement;\n}\ninterface BeforeOpenNotificationEvent {\n    notification: NotificationSpec;\n}\ninterface OpenNotificationEvent {\n    notification: NotificationApi;\n}\ninterface EditorEventMap extends Omit<NativeEventMap, 'blur' | 'focus'> {\n    'activate': {\n        relatedTarget: Editor | null;\n    };\n    'deactivate': {\n        relatedTarget: Editor;\n    };\n    'focus': {\n        blurredEditor: Editor | null;\n    };\n    'blur': {\n        focusedEditor: Editor | null;\n    };\n    'resize': UIEvent;\n    'scroll': UIEvent;\n    'input': InputEvent;\n    'beforeinput': InputEvent;\n    'detach': {};\n    'remove': {};\n    'init': {};\n    'ScrollIntoView': ScrollIntoViewEvent;\n    'AfterScrollIntoView': ScrollIntoViewEvent;\n    'ObjectResized': ObjectResizeEvent;\n    'ObjectResizeStart': ObjectResizeEvent;\n    'SwitchMode': SwitchModeEvent;\n    'ScrollWindow': Event;\n    'ResizeWindow': UIEvent;\n    'SkinLoaded': {};\n    'SkinLoadError': LoadErrorEvent;\n    'PluginLoadError': LoadErrorEvent;\n    'ModelLoadError': LoadErrorEvent;\n    'IconsLoadError': LoadErrorEvent;\n    'ThemeLoadError': LoadErrorEvent;\n    'LanguageLoadError': LoadErrorEvent;\n    'BeforeExecCommand': ExecCommandEvent;\n    'ExecCommand': ExecCommandEvent;\n    'NodeChange': NodeChangeEvent;\n    'FormatApply': FormatEvent;\n    'FormatRemove': FormatEvent;\n    'ShowCaret': ShowCaretEvent;\n    'SelectionChange': {};\n    'ObjectSelected': ObjectSelectedEvent;\n    'BeforeObjectSelected': ObjectSelectedEvent;\n    'GetSelectionRange': {\n        range: Range;\n    };\n    'SetSelectionRange': SetSelectionRangeEvent;\n    'AfterSetSelectionRange': SetSelectionRangeEvent;\n    'BeforeGetContent': BeforeGetContentEvent;\n    'GetContent': GetContentEvent;\n    'BeforeSetContent': BeforeSetContentEvent;\n    'SetContent': SetContentEvent;\n    'SaveContent': SaveContentEvent;\n    'RawSaveContent': SaveContentEvent;\n    'LoadContent': {\n        load: boolean;\n        element: HTMLElement;\n    };\n    'PreviewFormats': {};\n    'AfterPreviewFormats': {};\n    'ScriptsLoaded': {};\n    'PreInit': {};\n    'PostRender': {};\n    'NewBlock': NewBlockEvent;\n    'ClearUndos': {};\n    'TypingUndo': {};\n    'Redo': UndoRedoEvent;\n    'Undo': UndoRedoEvent;\n    'BeforeAddUndo': AddUndoEvent;\n    'AddUndo': AddUndoEvent;\n    'change': ChangeEvent;\n    'CloseWindow': WindowEvent<any>;\n    'OpenWindow': WindowEvent<any>;\n    'ProgressState': ProgressStateEvent;\n    'AfterProgressState': AfterProgressStateEvent;\n    'PlaceholderToggle': PlaceholderToggleEvent;\n    'tap': TouchEvent;\n    'longpress': TouchEvent;\n    'longpresscancel': {};\n    'PreProcess': PreProcessEvent;\n    'PostProcess': PostProcessEvent;\n    'AutocompleterStart': AutocompleterEventArgs;\n    'AutocompleterUpdate': AutocompleterEventArgs;\n    'AutocompleterEnd': {};\n    'PastePlainTextToggle': PastePlainTextToggleEvent;\n    'PastePreProcess': PastePreProcessEvent;\n    'PastePostProcess': PastePostProcessEvent;\n    'TableModified': TableModifiedEvent;\n    'NewRow': NewTableRowEvent;\n    'NewCell': NewTableCellEvent;\n    'SetAttrib': SetAttribEvent;\n    'hide': {};\n    'show': {};\n    'dirty': {};\n    'BeforeOpenNotification': BeforeOpenNotificationEvent;\n    'OpenNotification': OpenNotificationEvent;\n}\ninterface EditorManagerEventMap {\n    'AddEditor': {\n        editor: Editor;\n    };\n    'RemoveEditor': {\n        editor: Editor;\n    };\n    'BeforeUnload': {\n        returnValue: any;\n    };\n}\ntype EventTypes_d_ExecCommandEvent = ExecCommandEvent;\ntype EventTypes_d_BeforeGetContentEvent = BeforeGetContentEvent;\ntype EventTypes_d_GetContentEvent = GetContentEvent;\ntype EventTypes_d_BeforeSetContentEvent = BeforeSetContentEvent;\ntype EventTypes_d_SetContentEvent = SetContentEvent;\ntype EventTypes_d_SaveContentEvent = SaveContentEvent;\ntype EventTypes_d_NewBlockEvent = NewBlockEvent;\ntype EventTypes_d_NodeChangeEvent = NodeChangeEvent;\ntype EventTypes_d_FormatEvent = FormatEvent;\ntype EventTypes_d_ObjectResizeEvent = ObjectResizeEvent;\ntype EventTypes_d_ObjectSelectedEvent = ObjectSelectedEvent;\ntype EventTypes_d_ScrollIntoViewEvent = ScrollIntoViewEvent;\ntype EventTypes_d_SetSelectionRangeEvent = SetSelectionRangeEvent;\ntype EventTypes_d_ShowCaretEvent = ShowCaretEvent;\ntype EventTypes_d_SwitchModeEvent = SwitchModeEvent;\ntype EventTypes_d_ChangeEvent = ChangeEvent;\ntype EventTypes_d_AddUndoEvent = AddUndoEvent;\ntype EventTypes_d_UndoRedoEvent = UndoRedoEvent;\ntype EventTypes_d_WindowEvent<T extends DialogData> = WindowEvent<T>;\ntype EventTypes_d_ProgressStateEvent = ProgressStateEvent;\ntype EventTypes_d_AfterProgressStateEvent = AfterProgressStateEvent;\ntype EventTypes_d_PlaceholderToggleEvent = PlaceholderToggleEvent;\ntype EventTypes_d_LoadErrorEvent = LoadErrorEvent;\ntype EventTypes_d_PreProcessEvent = PreProcessEvent;\ntype EventTypes_d_PostProcessEvent = PostProcessEvent;\ntype EventTypes_d_PastePlainTextToggleEvent = PastePlainTextToggleEvent;\ntype EventTypes_d_PastePreProcessEvent = PastePreProcessEvent;\ntype EventTypes_d_PastePostProcessEvent = PastePostProcessEvent;\ntype EventTypes_d_EditableRootStateChangeEvent = EditableRootStateChangeEvent;\ntype EventTypes_d_NewTableRowEvent = NewTableRowEvent;\ntype EventTypes_d_NewTableCellEvent = NewTableCellEvent;\ntype EventTypes_d_TableEventData = TableEventData;\ntype EventTypes_d_TableModifiedEvent = TableModifiedEvent;\ntype EventTypes_d_BeforeOpenNotificationEvent = BeforeOpenNotificationEvent;\ntype EventTypes_d_OpenNotificationEvent = OpenNotificationEvent;\ntype EventTypes_d_EditorEventMap = EditorEventMap;\ntype EventTypes_d_EditorManagerEventMap = EditorManagerEventMap;\ndeclare namespace EventTypes_d {\n    export { EventTypes_d_ExecCommandEvent as ExecCommandEvent, EventTypes_d_BeforeGetContentEvent as BeforeGetContentEvent, EventTypes_d_GetContentEvent as GetContentEvent, EventTypes_d_BeforeSetContentEvent as BeforeSetContentEvent, EventTypes_d_SetContentEvent as SetContentEvent, EventTypes_d_SaveContentEvent as SaveContentEvent, EventTypes_d_NewBlockEvent as NewBlockEvent, EventTypes_d_NodeChangeEvent as NodeChangeEvent, EventTypes_d_FormatEvent as FormatEvent, EventTypes_d_ObjectResizeEvent as ObjectResizeEvent, EventTypes_d_ObjectSelectedEvent as ObjectSelectedEvent, EventTypes_d_ScrollIntoViewEvent as ScrollIntoViewEvent, EventTypes_d_SetSelectionRangeEvent as SetSelectionRangeEvent, EventTypes_d_ShowCaretEvent as ShowCaretEvent, EventTypes_d_SwitchModeEvent as SwitchModeEvent, EventTypes_d_ChangeEvent as ChangeEvent, EventTypes_d_AddUndoEvent as AddUndoEvent, EventTypes_d_UndoRedoEvent as UndoRedoEvent, EventTypes_d_WindowEvent as WindowEvent, EventTypes_d_ProgressStateEvent as ProgressStateEvent, EventTypes_d_AfterProgressStateEvent as AfterProgressStateEvent, EventTypes_d_PlaceholderToggleEvent as PlaceholderToggleEvent, EventTypes_d_LoadErrorEvent as LoadErrorEvent, EventTypes_d_PreProcessEvent as PreProcessEvent, EventTypes_d_PostProcessEvent as PostProcessEvent, EventTypes_d_PastePlainTextToggleEvent as PastePlainTextToggleEvent, EventTypes_d_PastePreProcessEvent as PastePreProcessEvent, EventTypes_d_PastePostProcessEvent as PastePostProcessEvent, EventTypes_d_EditableRootStateChangeEvent as EditableRootStateChangeEvent, EventTypes_d_NewTableRowEvent as NewTableRowEvent, EventTypes_d_NewTableCellEvent as NewTableCellEvent, EventTypes_d_TableEventData as TableEventData, EventTypes_d_TableModifiedEvent as TableModifiedEvent, EventTypes_d_BeforeOpenNotificationEvent as BeforeOpenNotificationEvent, EventTypes_d_OpenNotificationEvent as OpenNotificationEvent, EventTypes_d_EditorEventMap as EditorEventMap, EventTypes_d_EditorManagerEventMap as EditorManagerEventMap, };\n}\ntype Format_d_Formats = Formats;\ntype Format_d_Format = Format;\ntype Format_d_ApplyFormat = ApplyFormat;\ntype Format_d_BlockFormat = BlockFormat;\ntype Format_d_InlineFormat = InlineFormat;\ntype Format_d_SelectorFormat = SelectorFormat;\ntype Format_d_RemoveFormat = RemoveFormat;\ntype Format_d_RemoveBlockFormat = RemoveBlockFormat;\ntype Format_d_RemoveInlineFormat = RemoveInlineFormat;\ntype Format_d_RemoveSelectorFormat = RemoveSelectorFormat;\ndeclare namespace Format_d {\n    export { Format_d_Formats as Formats, Format_d_Format as Format, Format_d_ApplyFormat as ApplyFormat, Format_d_BlockFormat as BlockFormat, Format_d_InlineFormat as InlineFormat, Format_d_SelectorFormat as SelectorFormat, Format_d_RemoveFormat as RemoveFormat, Format_d_RemoveBlockFormat as RemoveBlockFormat, Format_d_RemoveInlineFormat as RemoveInlineFormat, Format_d_RemoveSelectorFormat as RemoveSelectorFormat, };\n}\ntype StyleFormat = BlockStyleFormat | InlineStyleFormat | SelectorStyleFormat;\ntype AllowedFormat = Separator | FormatReference | StyleFormat | NestedFormatting;\ninterface Separator {\n    title: string;\n}\ninterface FormatReference {\n    title: string;\n    format: string;\n    icon?: string;\n}\ninterface NestedFormatting {\n    title: string;\n    items: Array<FormatReference | StyleFormat>;\n}\ninterface CommonStyleFormat {\n    name?: string;\n    title: string;\n    icon?: string;\n}\ninterface BlockStyleFormat extends BlockFormat, CommonStyleFormat {\n}\ninterface InlineStyleFormat extends InlineFormat, CommonStyleFormat {\n}\ninterface SelectorStyleFormat extends SelectorFormat, CommonStyleFormat {\n}\ntype EntityEncoding = 'named' | 'numeric' | 'raw' | 'named,numeric' | 'named+numeric' | 'numeric,named' | 'numeric+named';\ninterface ContentLanguage {\n    readonly title: string;\n    readonly code: string;\n    readonly customCode?: string;\n}\ntype ThemeInitFunc = (editor: Editor, elm: HTMLElement) => {\n    editorContainer: HTMLElement;\n    iframeContainer: HTMLElement;\n    height?: number;\n    iframeHeight?: number;\n    api?: EditorUiApi;\n};\ntype SetupCallback = (editor: Editor) => void;\ntype FilePickerCallback = (callback: (value: string, meta?: Record<string, any>) => void, value: string, meta: Record<string, any>) => void;\ntype FilePickerValidationStatus = 'valid' | 'unknown' | 'invalid' | 'none';\ntype FilePickerValidationCallback = (info: {\n    type: string;\n    url: string;\n}, callback: (validation: {\n    status: FilePickerValidationStatus;\n    message: string;\n}) => void) => void;\ntype PastePreProcessFn = (editor: Editor, args: PastePreProcessEvent) => void;\ntype PastePostProcessFn = (editor: Editor, args: PastePostProcessEvent) => void;\ntype URLConverter = (url: string, name: string, elm?: string | Element) => string;\ntype URLConverterCallback = (url: string, node: Node | string | undefined, on_save: boolean, name: string) => string;\ninterface ToolbarGroup {\n    name?: string;\n    items: string[];\n}\ntype ToolbarMode = 'floating' | 'sliding' | 'scrolling' | 'wrap';\ntype ToolbarLocation = 'top' | 'bottom' | 'auto';\ntype ForceHexColor = 'always' | 'rgb_only' | 'off';\ninterface BaseEditorOptions {\n    a11y_advanced_options?: boolean;\n    add_form_submit_trigger?: boolean;\n    add_unload_trigger?: boolean;\n    allow_conditional_comments?: boolean;\n    allow_html_data_urls?: boolean;\n    allow_html_in_named_anchor?: boolean;\n    allow_script_urls?: boolean;\n    allow_svg_data_urls?: boolean;\n    allow_unsafe_link_target?: boolean;\n    anchor_bottom?: false | string;\n    anchor_top?: false | string;\n    auto_focus?: string | true;\n    automatic_uploads?: boolean;\n    base_url?: string;\n    block_formats?: string;\n    block_unsupported_drop?: boolean;\n    body_id?: string;\n    body_class?: string;\n    br_in_pre?: boolean;\n    br_newline_selector?: string;\n    browser_spellcheck?: boolean;\n    branding?: boolean;\n    cache_suffix?: string;\n    color_cols?: number;\n    color_cols_foreground?: number;\n    color_cols_background?: number;\n    color_map?: string[];\n    color_map_foreground?: string[];\n    color_map_background?: string[];\n    color_default_foreground?: string;\n    color_default_background?: string;\n    content_css?: boolean | string | string[];\n    content_css_cors?: boolean;\n    content_security_policy?: string;\n    content_style?: string;\n    content_langs?: ContentLanguage[];\n    contextmenu?: string | string[] | false;\n    contextmenu_never_use_native?: boolean;\n    convert_fonts_to_spans?: boolean;\n    convert_unsafe_embeds?: boolean;\n    convert_urls?: boolean;\n    custom_colors?: boolean;\n    custom_elements?: string;\n    custom_ui_selector?: string;\n    custom_undo_redo_levels?: number;\n    default_font_stack?: string[];\n    deprecation_warnings?: boolean;\n    directionality?: 'ltr' | 'rtl';\n    doctype?: string;\n    document_base_url?: string;\n    draggable_modal?: boolean;\n    editable_class?: string;\n    editable_root?: boolean;\n    element_format?: 'xhtml' | 'html';\n    elementpath?: boolean;\n    encoding?: string;\n    end_container_on_empty_block?: boolean | string;\n    entities?: string;\n    entity_encoding?: EntityEncoding;\n    extended_valid_elements?: string;\n    event_root?: string;\n    file_picker_callback?: FilePickerCallback;\n    file_picker_types?: string;\n    file_picker_validator_handler?: FilePickerValidationCallback;\n    fix_list_elements?: boolean;\n    fixed_toolbar_container?: string;\n    fixed_toolbar_container_target?: HTMLElement;\n    font_css?: string | string[];\n    font_family_formats?: string;\n    font_size_classes?: string;\n    font_size_legacy_values?: string;\n    font_size_style_values?: string;\n    font_size_formats?: string;\n    font_size_input_default_unit?: string;\n    force_hex_color?: ForceHexColor;\n    forced_root_block?: string;\n    forced_root_block_attrs?: Record<string, string>;\n    formats?: Formats;\n    format_noneditable_selector?: string;\n    height?: number | string;\n    help_accessibility?: boolean;\n    hidden_input?: boolean;\n    highlight_on_focus?: boolean;\n    icons?: string;\n    icons_url?: string;\n    id?: string;\n    iframe_aria_text?: string;\n    iframe_attrs?: Record<string, string>;\n    images_file_types?: string;\n    images_replace_blob_uris?: boolean;\n    images_reuse_filename?: boolean;\n    images_upload_base_path?: string;\n    images_upload_credentials?: boolean;\n    images_upload_handler?: UploadHandler;\n    images_upload_url?: string;\n    indent?: boolean;\n    indent_after?: string;\n    indent_before?: string;\n    indent_use_margin?: boolean;\n    indentation?: string;\n    init_instance_callback?: SetupCallback;\n    inline?: boolean;\n    inline_boundaries?: boolean;\n    inline_boundaries_selector?: string;\n    inline_styles?: boolean;\n    invalid_elements?: string;\n    invalid_styles?: string | Record<string, string>;\n    keep_styles?: boolean;\n    language?: string;\n    language_load?: boolean;\n    language_url?: string;\n    line_height_formats?: string;\n    max_height?: number;\n    max_width?: number;\n    menu?: Record<string, {\n        title: string;\n        items: string;\n    }>;\n    menubar?: boolean | string;\n    min_height?: number;\n    min_width?: number;\n    model?: string;\n    model_url?: string;\n    newdocument_content?: string;\n    newline_behavior?: 'block' | 'linebreak' | 'invert' | 'default';\n    no_newline_selector?: string;\n    noneditable_class?: string;\n    noneditable_regexp?: RegExp | RegExp[];\n    nowrap?: boolean;\n    object_resizing?: boolean | string;\n    pad_empty_with_br?: boolean;\n    paste_as_text?: boolean;\n    paste_block_drop?: boolean;\n    paste_data_images?: boolean;\n    paste_merge_formats?: boolean;\n    paste_postprocess?: PastePostProcessFn;\n    paste_preprocess?: PastePreProcessFn;\n    paste_remove_styles_if_webkit?: boolean;\n    paste_tab_spaces?: number;\n    paste_webkit_styles?: string;\n    placeholder?: string;\n    preserve_cdata?: boolean;\n    preview_styles?: false | string;\n    promotion?: boolean;\n    protect?: RegExp[];\n    readonly?: boolean;\n    referrer_policy?: ReferrerPolicy;\n    relative_urls?: boolean;\n    remove_script_host?: boolean;\n    remove_trailing_brs?: boolean;\n    removed_menuitems?: string;\n    resize?: boolean | 'both';\n    resize_img_proportional?: boolean;\n    root_name?: string;\n    sandbox_iframes?: boolean;\n    schema?: SchemaType;\n    selector?: string;\n    setup?: SetupCallback;\n    sidebar_show?: string;\n    skin?: boolean | string;\n    skin_url?: string;\n    smart_paste?: boolean;\n    statusbar?: boolean;\n    style_formats?: AllowedFormat[];\n    style_formats_autohide?: boolean;\n    style_formats_merge?: boolean;\n    submit_patch?: boolean;\n    suffix?: string;\n    table_tab_navigation?: boolean;\n    target?: HTMLElement;\n    text_patterns?: RawPattern[] | false;\n    text_patterns_lookup?: RawDynamicPatternsLookup;\n    theme?: string | ThemeInitFunc | false;\n    theme_url?: string;\n    toolbar?: boolean | string | string[] | Array<ToolbarGroup>;\n    toolbar1?: string;\n    toolbar2?: string;\n    toolbar3?: string;\n    toolbar4?: string;\n    toolbar5?: string;\n    toolbar6?: string;\n    toolbar7?: string;\n    toolbar8?: string;\n    toolbar9?: string;\n    toolbar_groups?: Record<string, GroupToolbarButtonSpec>;\n    toolbar_location?: ToolbarLocation;\n    toolbar_mode?: ToolbarMode;\n    toolbar_sticky?: boolean;\n    toolbar_sticky_offset?: number;\n    typeahead_urls?: boolean;\n    ui_mode?: 'combined' | 'split';\n    url_converter?: URLConverter;\n    url_converter_scope?: any;\n    urlconverter_callback?: URLConverterCallback;\n    valid_children?: string;\n    valid_classes?: string | Record<string, string>;\n    valid_elements?: string;\n    valid_styles?: string | Record<string, string>;\n    verify_html?: boolean;\n    visual?: boolean;\n    visual_anchor_class?: string;\n    visual_table_class?: string;\n    width?: number | string;\n    xss_sanitization?: boolean;\n    disable_nodechange?: boolean;\n    forced_plugins?: string | string[];\n    plugin_base_urls?: Record<string, string>;\n    service_message?: string;\n    [key: string]: any;\n}\ninterface RawEditorOptions extends BaseEditorOptions {\n    external_plugins?: Record<string, string>;\n    mobile?: RawEditorOptions;\n    plugins?: string | string[];\n}\ninterface NormalizedEditorOptions extends BaseEditorOptions {\n    external_plugins: Record<string, string>;\n    forced_plugins: string[];\n    plugins: string[];\n}\ninterface EditorOptions extends NormalizedEditorOptions {\n    a11y_advanced_options: boolean;\n    allow_unsafe_link_target: boolean;\n    anchor_bottom: string;\n    anchor_top: string;\n    automatic_uploads: boolean;\n    block_formats: string;\n    body_class: string;\n    body_id: string;\n    br_newline_selector: string;\n    color_map: string[];\n    color_cols: number;\n    color_cols_foreground: number;\n    color_cols_background: number;\n    color_default_background: string;\n    color_default_foreground: string;\n    content_css: string[];\n    contextmenu: string[];\n    convert_unsafe_embeds: boolean;\n    custom_colors: boolean;\n    default_font_stack: string[];\n    document_base_url: string;\n    init_content_sync: boolean;\n    draggable_modal: boolean;\n    editable_class: string;\n    editable_root: boolean;\n    font_css: string[];\n    font_family_formats: string;\n    font_size_classes: string;\n    font_size_formats: string;\n    font_size_input_default_unit: string;\n    font_size_legacy_values: string;\n    font_size_style_values: string;\n    forced_root_block: string;\n    forced_root_block_attrs: Record<string, string>;\n    force_hex_color: ForceHexColor;\n    format_noneditable_selector: string;\n    height: number | string;\n    highlight_on_focus: boolean;\n    iframe_attrs: Record<string, string>;\n    images_file_types: string;\n    images_upload_base_path: string;\n    images_upload_credentials: boolean;\n    images_upload_url: string;\n    indent_use_margin: boolean;\n    indentation: string;\n    inline: boolean;\n    inline_boundaries_selector: string;\n    language: string;\n    language_load: boolean;\n    language_url: string;\n    line_height_formats: string;\n    menu: Record<string, {\n        title: string;\n        items: string;\n    }>;\n    menubar: boolean | string;\n    model: string;\n    newdocument_content: string;\n    no_newline_selector: string;\n    noneditable_class: string;\n    noneditable_regexp: RegExp[];\n    object_resizing: string;\n    pad_empty_with_br: boolean;\n    paste_as_text: boolean;\n    preview_styles: string;\n    promotion: boolean;\n    readonly: boolean;\n    removed_menuitems: string;\n    sandbox_iframes: boolean;\n    toolbar: boolean | string | string[] | Array<ToolbarGroup>;\n    toolbar_groups: Record<string, GroupToolbarButtonSpec>;\n    toolbar_location: ToolbarLocation;\n    toolbar_mode: ToolbarMode;\n    toolbar_persist: boolean;\n    toolbar_sticky: boolean;\n    toolbar_sticky_offset: number;\n    text_patterns: Pattern[];\n    text_patterns_lookup: DynamicPatternsLookup;\n    visual: boolean;\n    visual_anchor_class: string;\n    visual_table_class: string;\n    width: number | string;\n    xss_sanitization: boolean;\n}\ntype StyleMap = Record<string, string | number>;\ninterface StylesSettings {\n    allow_script_urls?: boolean;\n    allow_svg_data_urls?: boolean;\n    url_converter?: URLConverter;\n    url_converter_scope?: any;\n    force_hex_color?: ForceHexColor;\n}\ninterface Styles {\n    parse: (css: string | undefined) => Record<string, string>;\n    serialize: (styles: StyleMap, elementName?: string) => string;\n}\ntype EventUtilsCallback<T> = (event: EventUtilsEvent<T>) => void | boolean;\ntype EventUtilsEvent<T> = NormalizedEvent<T> & {\n    metaKey: boolean;\n};\ninterface Callback$1<T> {\n    func: EventUtilsCallback<T>;\n    scope: any;\n}\ninterface CallbackList<T> extends Array<Callback$1<T>> {\n    fakeName: string | false;\n    capture: boolean;\n    nativeHandler: EventListener;\n}\ninterface EventUtilsConstructor {\n    readonly prototype: EventUtils;\n    new (): EventUtils;\n    Event: EventUtils;\n}\ndeclare class EventUtils {\n    static Event: EventUtils;\n    domLoaded: boolean;\n    events: Record<number, Record<string, CallbackList<any>>>;\n    private readonly expando;\n    private hasFocusIn;\n    private count;\n    constructor();\n    bind<K extends keyof HTMLElementEventMap>(target: any, name: K, callback: EventUtilsCallback<HTMLElementEventMap[K]>, scope?: any): EventUtilsCallback<HTMLElementEventMap[K]>;\n    bind<T = any>(target: any, names: string, callback: EventUtilsCallback<T>, scope?: any): EventUtilsCallback<T>;\n    unbind<K extends keyof HTMLElementEventMap>(target: any, name: K, callback?: EventUtilsCallback<HTMLElementEventMap[K]>): this;\n    unbind<T = any>(target: any, names: string, callback?: EventUtilsCallback<T>): this;\n    unbind(target: any): this;\n    fire(target: any, name: string, args?: {}): this;\n    dispatch(target: any, name: string, args?: {}): this;\n    clean(target: any): this;\n    destroy(): void;\n    cancel<T>(e: EventUtilsEvent<T>): boolean;\n    private executeHandlers;\n}\ninterface SetAttribEvent {\n    attrElm: HTMLElement;\n    attrName: string;\n    attrValue: string | boolean | number | null;\n}\ninterface DOMUtilsSettings {\n    schema: Schema;\n    url_converter: URLConverter;\n    url_converter_scope: any;\n    ownEvents: boolean;\n    keep_values: boolean;\n    update_styles: boolean;\n    root_element: HTMLElement | null;\n    collect: boolean;\n    onSetAttrib: (event: SetAttribEvent) => void;\n    contentCssCors: boolean;\n    referrerPolicy: ReferrerPolicy;\n    force_hex_color: ForceHexColor;\n}\ntype Target = Node | Window;\ntype RunArguments<T extends Node = Node> = string | T | Array<string | T> | null;\ntype BoundEvent = [\n    Target,\n    string,\n    EventUtilsCallback<any>,\n    any\n];\ntype Callback<K extends string> = EventUtilsCallback<MappedEvent<HTMLElementEventMap, K>>;\ntype RunResult<T, R> = T extends Array<any> ? R[] : false | R;\ninterface DOMUtils {\n    doc: Document;\n    settings: Partial<DOMUtilsSettings>;\n    win: Window;\n    files: Record<string, boolean>;\n    stdMode: boolean;\n    boxModel: boolean;\n    styleSheetLoader: StyleSheetLoader;\n    boundEvents: BoundEvent[];\n    styles: Styles;\n    schema: Schema;\n    events: EventUtils;\n    root: Node | null;\n    isBlock: {\n        (node: Node | null): node is HTMLElement;\n        (node: string): boolean;\n    };\n    clone: (node: Node, deep: boolean) => Node;\n    getRoot: () => HTMLElement;\n    getViewPort: (argWin?: Window) => GeomRect;\n    getRect: (elm: string | HTMLElement) => GeomRect;\n    getSize: (elm: string | HTMLElement) => {\n        w: number;\n        h: number;\n    };\n    getParent: {\n        <K extends keyof HTMLElementTagNameMap>(node: string | Node | null, selector: K, root?: Node): HTMLElementTagNameMap[K] | null;\n        <T extends Element>(node: string | Node | null, selector: string | ((node: Node) => node is T), root?: Node): T | null;\n        (node: string | Node | null, selector?: string | ((node: Node) => boolean | void), root?: Node): Node | null;\n    };\n    getParents: {\n        <K extends keyof HTMLElementTagNameMap>(elm: string | HTMLElementTagNameMap[K] | null, selector: K, root?: Node, collect?: boolean): Array<HTMLElementTagNameMap[K]>;\n        <T extends Element>(node: string | Node | null, selector: string | ((node: Node) => node is T), root?: Node, collect?: boolean): T[];\n        (elm: string | Node | null, selector?: string | ((node: Node) => boolean | void), root?: Node, collect?: boolean): Node[];\n    };\n    get: {\n        <T extends Node>(elm: T): T;\n        (elm: string): HTMLElement | null;\n    };\n    getNext: (node: Node | null, selector: string | ((node: Node) => boolean)) => Node | null;\n    getPrev: (node: Node | null, selector: string | ((node: Node) => boolean)) => Node | null;\n    select: {\n        <K extends keyof HTMLElementTagNameMap>(selector: K, scope?: string | Node): Array<HTMLElementTagNameMap[K]>;\n        <T extends HTMLElement = HTMLElement>(selector: string, scope?: string | Node): T[];\n    };\n    is: {\n        <T extends Element>(elm: Node | Node[] | null, selector: string): elm is T;\n        (elm: Node | Node[] | null, selector: string): boolean;\n    };\n    add: (parentElm: RunArguments, name: string | Element, attrs?: Record<string, string | boolean | number | null>, html?: string | Node | null, create?: boolean) => HTMLElement;\n    create: {\n        <K extends keyof HTMLElementTagNameMap>(name: K, attrs?: Record<string, string | boolean | number | null>, html?: string | Node | null): HTMLElementTagNameMap[K];\n        (name: string, attrs?: Record<string, string | boolean | number | null>, html?: string | Node | null): HTMLElement;\n    };\n    createHTML: (name: string, attrs?: Record<string, string | null>, html?: string) => string;\n    createFragment: (html?: string) => DocumentFragment;\n    remove: {\n        <T extends Node>(node: T | T[], keepChildren?: boolean): typeof node extends Array<any> ? T[] : T;\n        <T extends Node>(node: string, keepChildren?: boolean): T | false;\n    };\n    getStyle: {\n        (elm: Element, name: string, computed: true): string;\n        (elm: string | Element | null, name: string, computed?: boolean): string | undefined;\n    };\n    setStyle: (elm: string | Element | Element[], name: string, value: string | number | null) => void;\n    setStyles: (elm: string | Element | Element[], stylesArg: StyleMap) => void;\n    removeAllAttribs: (e: RunArguments<Element>) => void;\n    setAttrib: (elm: RunArguments<Element>, name: string, value: string | boolean | number | null) => void;\n    setAttribs: (elm: RunArguments<Element>, attrs: Record<string, string | boolean | number | null>) => void;\n    getAttrib: (elm: string | Element | null, name: string, defaultVal?: string) => string;\n    getAttribs: (elm: string | Element) => NamedNodeMap | Attr[];\n    getPos: (elm: string | Element, rootElm?: Node) => {\n        x: number;\n        y: number;\n    };\n    parseStyle: (cssText: string) => Record<string, string>;\n    serializeStyle: (stylesArg: StyleMap, name?: string) => string;\n    addStyle: (cssText: string) => void;\n    loadCSS: (url: string) => void;\n    hasClass: (elm: string | Element, cls: string) => boolean;\n    addClass: (elm: RunArguments<Element>, cls: string) => void;\n    removeClass: (elm: RunArguments<Element>, cls: string) => void;\n    toggleClass: (elm: RunArguments<Element>, cls: string, state?: boolean) => void;\n    show: (elm: string | Node | Node[]) => void;\n    hide: (elm: string | Node | Node[]) => void;\n    isHidden: (elm: string | Node) => boolean;\n    uniqueId: (prefix?: string) => string;\n    setHTML: (elm: RunArguments<Element>, html: string) => void;\n    getOuterHTML: (elm: string | Node) => string;\n    setOuterHTML: (elm: string | Node | Node[], html: string) => void;\n    decode: (text: string) => string;\n    encode: (text: string) => string;\n    insertAfter: {\n        <T extends Node>(node: T | T[], reference: string | Node): T;\n        <T extends Node>(node: RunArguments<T>, reference: string | Node): RunResult<typeof node, T>;\n    };\n    replace: {\n        <T extends Node>(newElm: Node, oldElm: T | T[], keepChildren?: boolean): T;\n        <T extends Node>(newElm: Node, oldElm: RunArguments<T>, keepChildren?: boolean): false | T;\n    };\n    rename: {\n        <K extends keyof HTMLElementTagNameMap>(elm: Element, name: K): HTMLElementTagNameMap[K];\n        (elm: Element, name: string): Element;\n    };\n    findCommonAncestor: (a: Node, b: Node) => Node | null;\n    run<R, T extends Node>(this: DOMUtils, elm: T | T[], func: (node: T) => R, scope?: any): typeof elm extends Array<any> ? R[] : R;\n    run<R, T extends Node>(this: DOMUtils, elm: RunArguments<T>, func: (node: T) => R, scope?: any): RunResult<typeof elm, R>;\n    isEmpty: (node: Node, elements?: Record<string, any>, options?: ({\n        includeZwsp?: boolean;\n    })) => boolean;\n    createRng: () => Range;\n    nodeIndex: (node: Node, normalized?: boolean) => number;\n    split: {\n        <T extends Node>(parentElm: Node, splitElm: Node, replacementElm: T): T | undefined;\n        <T extends Node>(parentElm: Node, splitElm: T): T | undefined;\n    };\n    bind: {\n        <K extends string>(target: Target, name: K, func: Callback<K>, scope?: any): Callback<K>;\n        <K extends string>(target: Target[], name: K, func: Callback<K>, scope?: any): Callback<K>[];\n    };\n    unbind: {\n        <K extends string>(target: Target, name?: K, func?: EventUtilsCallback<MappedEvent<HTMLElementEventMap, K>>): EventUtils;\n        <K extends string>(target: Target[], name?: K, func?: EventUtilsCallback<MappedEvent<HTMLElementEventMap, K>>): EventUtils[];\n    };\n    fire: (target: Node | Window, name: string, evt?: {}) => EventUtils;\n    dispatch: (target: Node | Window, name: string, evt?: {}) => EventUtils;\n    getContentEditable: (node: Node) => string | null;\n    getContentEditableParent: (node: Node) => string | null;\n    isEditable: (node: Node | null | undefined) => boolean;\n    destroy: () => void;\n    isChildOf: (node: Node, parent: Node) => boolean;\n    dumpRng: (r: Range) => string;\n}\ninterface ClientRect {\n    left: number;\n    top: number;\n    bottom: number;\n    right: number;\n    width: number;\n    height: number;\n}\ninterface BookmarkManager {\n    getBookmark: (type?: number, normalized?: boolean) => Bookmark;\n    moveToBookmark: (bookmark: Bookmark) => void;\n}\ninterface ControlSelection {\n    isResizable: (elm: Element) => boolean;\n    showResizeRect: (elm: HTMLElement) => void;\n    hideResizeRect: () => void;\n    updateResizeRect: (evt: EditorEvent<any>) => void;\n    destroy: () => void;\n}\ninterface WriterSettings {\n    element_format?: 'xhtml' | 'html';\n    entities?: string;\n    entity_encoding?: EntityEncoding;\n    indent?: boolean;\n    indent_after?: string;\n    indent_before?: string;\n}\ntype Attributes = Array<{\n    name: string;\n    value: string;\n}>;\ninterface Writer {\n    cdata: (text: string) => void;\n    comment: (text: string) => void;\n    doctype: (text: string) => void;\n    end: (name: string) => void;\n    getContent: () => string;\n    pi: (name: string, text?: string) => void;\n    reset: () => void;\n    start: (name: string, attrs?: Attributes | null, empty?: boolean) => void;\n    text: (text: string, raw?: boolean) => void;\n}\ninterface HtmlSerializerSettings extends WriterSettings {\n    inner?: boolean;\n    validate?: boolean;\n}\ninterface HtmlSerializer {\n    serialize: (node: AstNode) => string;\n}\ninterface DomSerializerSettings extends DomParserSettings, WriterSettings, SchemaSettings, HtmlSerializerSettings {\n    remove_trailing_brs?: boolean;\n    url_converter?: URLConverter;\n    url_converter_scope?: {};\n}\ninterface DomSerializerImpl {\n    schema: Schema;\n    addNodeFilter: (name: string, callback: ParserFilterCallback) => void;\n    addAttributeFilter: (name: string, callback: ParserFilterCallback) => void;\n    getNodeFilters: () => ParserFilter[];\n    getAttributeFilters: () => ParserFilter[];\n    removeNodeFilter: (name: string, callback?: ParserFilterCallback) => void;\n    removeAttributeFilter: (name: string, callback?: ParserFilterCallback) => void;\n    serialize: {\n        (node: Element, parserArgs: {\n            format: 'tree';\n        } & ParserArgs): AstNode;\n        (node: Element, parserArgs?: ParserArgs): string;\n    };\n    addRules: (rules: string) => void;\n    setRules: (rules: string) => void;\n    addTempAttr: (name: string) => void;\n    getTempAttrs: () => string[];\n}\ninterface DomSerializer extends DomSerializerImpl {\n}\ninterface EditorSelection {\n    bookmarkManager: BookmarkManager;\n    controlSelection: ControlSelection;\n    dom: DOMUtils;\n    win: Window;\n    serializer: DomSerializer;\n    editor: Editor;\n    collapse: (toStart?: boolean) => void;\n    setCursorLocation: {\n        (node: Node, offset: number): void;\n        (): void;\n    };\n    getContent: {\n        (args: {\n            format: 'tree';\n        } & Partial<GetSelectionContentArgs>): AstNode;\n        (args?: Partial<GetSelectionContentArgs>): string;\n    };\n    setContent: (content: string, args?: Partial<SetSelectionContentArgs>) => void;\n    getBookmark: (type?: number, normalized?: boolean) => Bookmark;\n    moveToBookmark: (bookmark: Bookmark) => void;\n    select: (node: Node, content?: boolean) => Node;\n    isCollapsed: () => boolean;\n    isEditable: () => boolean;\n    isForward: () => boolean;\n    setNode: (elm: Element) => Element;\n    getNode: () => HTMLElement;\n    getSel: () => Selection | null;\n    setRng: (rng: Range, forward?: boolean) => void;\n    getRng: () => Range;\n    getStart: (real?: boolean) => Element;\n    getEnd: (real?: boolean) => Element;\n    getSelectedBlocks: (startElm?: Element, endElm?: Element) => Element[];\n    normalize: () => Range;\n    selectorChanged: (selector: string, callback: (active: boolean, args: {\n        node: Node;\n        selector: String;\n        parents: Node[];\n    }) => void) => EditorSelection;\n    selectorChangedWithUnbind: (selector: string, callback: (active: boolean, args: {\n        node: Node;\n        selector: String;\n        parents: Node[];\n    }) => void) => {\n        unbind: () => void;\n    };\n    getScrollContainer: () => HTMLElement | undefined;\n    scrollIntoView: (elm?: HTMLElement, alignToTop?: boolean) => void;\n    placeCaretAt: (clientX: number, clientY: number) => void;\n    getBoundingClientRect: () => ClientRect | DOMRect;\n    destroy: () => void;\n    expand: (options?: {\n        type: 'word';\n    }) => void;\n}\ntype EditorCommandCallback<S> = (this: S, ui: boolean, value: any) => void;\ntype EditorCommandsCallback = (command: string, ui: boolean, value?: any) => void;\ninterface Commands {\n    state: Record<string, (command: string) => boolean>;\n    exec: Record<string, EditorCommandsCallback>;\n    value: Record<string, (command: string) => string>;\n}\ninterface ExecCommandArgs {\n    skip_focus?: boolean;\n}\ninterface EditorCommandsConstructor {\n    readonly prototype: EditorCommands;\n    new (editor: Editor): EditorCommands;\n}\ndeclare class EditorCommands {\n    private readonly editor;\n    private commands;\n    constructor(editor: Editor);\n    execCommand(command: string, ui?: boolean, value?: any, args?: ExecCommandArgs): boolean;\n    queryCommandState(command: string): boolean;\n    queryCommandValue(command: string): string;\n    addCommands<K extends keyof Commands>(commandList: Commands[K], type: K): void;\n    addCommands(commandList: Record<string, EditorCommandsCallback>): void;\n    addCommand<S>(command: string, callback: EditorCommandCallback<S>, scope: S): void;\n    addCommand(command: string, callback: EditorCommandCallback<Editor>): void;\n    queryCommandSupported(command: string): boolean;\n    addQueryStateHandler<S>(command: string, callback: (this: S) => boolean, scope: S): void;\n    addQueryStateHandler(command: string, callback: (this: Editor) => boolean): void;\n    addQueryValueHandler<S>(command: string, callback: (this: S) => string, scope: S): void;\n    addQueryValueHandler(command: string, callback: (this: Editor) => string): void;\n}\ninterface RawString {\n    raw: string;\n}\ntype Primitive = string | number | boolean | Record<string | number, any> | Function;\ntype TokenisedString = [\n    string,\n    ...Primitive[]\n];\ntype Untranslated = Primitive | TokenisedString | RawString | null | undefined;\ntype TranslatedString = string;\ninterface I18n {\n    getData: () => Record<string, Record<string, string>>;\n    setCode: (newCode: string) => void;\n    getCode: () => string;\n    add: (code: string, items: Record<string, string>) => void;\n    translate: (text: Untranslated) => TranslatedString;\n    isRtl: () => boolean;\n    hasCode: (code: string) => boolean;\n}\ninterface Observable<T extends {}> {\n    fire<K extends string, U extends MappedEvent<T, K>>(name: K, args?: U, bubble?: boolean): EditorEvent<U>;\n    dispatch<K extends string, U extends MappedEvent<T, K>>(name: K, args?: U, bubble?: boolean): EditorEvent<U>;\n    on<K extends string>(name: K, callback: (event: EditorEvent<MappedEvent<T, K>>) => void, prepend?: boolean): EventDispatcher<T>;\n    off<K extends string>(name?: K, callback?: (event: EditorEvent<MappedEvent<T, K>>) => void): EventDispatcher<T>;\n    once<K extends string>(name: K, callback: (event: EditorEvent<MappedEvent<T, K>>) => void): EventDispatcher<T>;\n    hasEventListeners(name: string): boolean;\n}\ninterface URISettings {\n    base_uri?: URI;\n}\ninterface URIConstructor {\n    readonly prototype: URI;\n    new (url: string, settings?: URISettings): URI;\n    getDocumentBaseUrl: (loc: {\n        protocol: string;\n        host?: string;\n        href?: string;\n        pathname?: string;\n    }) => string;\n    parseDataUri: (uri: string) => {\n        type: string;\n        data: string;\n    };\n}\ninterface SafeUriOptions {\n    readonly allow_html_data_urls?: boolean;\n    readonly allow_script_urls?: boolean;\n    readonly allow_svg_data_urls?: boolean;\n}\ndeclare class URI {\n    static parseDataUri(uri: string): {\n        type: string | undefined;\n        data: string;\n    };\n    static isDomSafe(uri: string, context?: string, options?: SafeUriOptions): boolean;\n    static getDocumentBaseUrl(loc: {\n        protocol: string;\n        host?: string;\n        href?: string;\n        pathname?: string;\n    }): string;\n    source: string;\n    protocol: string | undefined;\n    authority: string | undefined;\n    userInfo: string | undefined;\n    user: string | undefined;\n    password: string | undefined;\n    host: string | undefined;\n    port: string | undefined;\n    relative: string | undefined;\n    path: string;\n    directory: string;\n    file: string | undefined;\n    query: string | undefined;\n    anchor: string | undefined;\n    settings: URISettings;\n    constructor(url: string, settings?: URISettings);\n    setPath(path: string): void;\n    toRelative(uri: string): string;\n    toAbsolute(uri: string, noHost?: boolean): string;\n    isSameOrigin(uri: URI): boolean;\n    toRelPath(base: string, path: string): string;\n    toAbsPath(base: string, path: string): string;\n    getURI(noProtoHost?: boolean): string;\n}\ninterface EditorManager extends Observable<EditorManagerEventMap> {\n    defaultOptions: RawEditorOptions;\n    majorVersion: string;\n    minorVersion: string;\n    releaseDate: string;\n    activeEditor: Editor | null;\n    focusedEditor: Editor | null;\n    baseURI: URI;\n    baseURL: string;\n    documentBaseURL: string;\n    i18n: I18n;\n    suffix: string;\n    add(this: EditorManager, editor: Editor): Editor;\n    addI18n: (code: string, item: Record<string, string>) => void;\n    createEditor(this: EditorManager, id: string, options: RawEditorOptions): Editor;\n    execCommand(this: EditorManager, cmd: string, ui: boolean, value: any): boolean;\n    get(this: EditorManager): Editor[];\n    get(this: EditorManager, id: number | string): Editor | null;\n    init(this: EditorManager, options: RawEditorOptions): Promise<Editor[]>;\n    overrideDefaults(this: EditorManager, defaultOptions: Partial<RawEditorOptions>): void;\n    remove(this: EditorManager): void;\n    remove(this: EditorManager, selector: string): void;\n    remove(this: EditorManager, editor: Editor): Editor | null;\n    setActive(this: EditorManager, editor: Editor): void;\n    setup(this: EditorManager): void;\n    translate: (text: Untranslated) => TranslatedString;\n    triggerSave: () => void;\n    _setBaseUrl(this: EditorManager, baseUrl: string): void;\n}\ninterface EditorObservable extends Observable<EditorEventMap> {\n    bindPendingEventDelegates(this: Editor): void;\n    toggleNativeEvent(this: Editor, name: string, state: boolean): void;\n    unbindAllNativeEvents(this: Editor): void;\n}\ninterface ProcessorSuccess<T> {\n    valid: true;\n    value: T;\n}\ninterface ProcessorError {\n    valid: false;\n    message: string;\n}\ntype SimpleProcessor = (value: unknown) => boolean;\ntype Processor<T> = (value: unknown) => ProcessorSuccess<T> | ProcessorError;\ninterface BuiltInOptionTypeMap {\n    'string': string;\n    'number': number;\n    'boolean': boolean;\n    'array': any[];\n    'function': Function;\n    'object': any;\n    'string[]': string[];\n    'object[]': any[];\n    'regexp': RegExp;\n}\ntype BuiltInOptionType = keyof BuiltInOptionTypeMap;\ninterface BaseOptionSpec {\n    immutable?: boolean;\n    deprecated?: boolean;\n    docsUrl?: string;\n}\ninterface BuiltInOptionSpec<K extends BuiltInOptionType> extends BaseOptionSpec {\n    processor: K;\n    default?: BuiltInOptionTypeMap[K];\n}\ninterface SimpleOptionSpec<T> extends BaseOptionSpec {\n    processor: SimpleProcessor;\n    default?: T;\n}\ninterface OptionSpec<T, U> extends BaseOptionSpec {\n    processor: Processor<U>;\n    default?: T;\n}\ninterface Options {\n    register: {\n        <K extends BuiltInOptionType>(name: string, spec: BuiltInOptionSpec<K>): void;\n        <K extends keyof NormalizedEditorOptions>(name: K, spec: OptionSpec<NormalizedEditorOptions[K], EditorOptions[K]> | SimpleOptionSpec<NormalizedEditorOptions[K]>): void;\n        <T, U>(name: string, spec: OptionSpec<T, U>): void;\n        <T>(name: string, spec: SimpleOptionSpec<T>): void;\n    };\n    isRegistered: (name: string) => boolean;\n    get: {\n        <K extends keyof EditorOptions>(name: K): EditorOptions[K];\n        <T>(name: string): T | undefined;\n    };\n    set: <K extends string, T>(name: K, value: K extends keyof NormalizedEditorOptions ? NormalizedEditorOptions[K] : T) => boolean;\n    unset: (name: string) => boolean;\n    isSet: (name: string) => boolean;\n}\ninterface UploadResult$1 {\n    element: HTMLImageElement;\n    status: boolean;\n    blobInfo: BlobInfo;\n    uploadUri: string;\n    removed: boolean;\n}\ninterface EditorUpload {\n    blobCache: BlobCache;\n    addFilter: (filter: (img: HTMLImageElement) => boolean) => void;\n    uploadImages: () => Promise<UploadResult$1[]>;\n    uploadImagesAuto: () => Promise<UploadResult$1[]>;\n    scanForImages: () => Promise<BlobInfoImagePair[]>;\n    destroy: () => void;\n}\ntype FormatChangeCallback = (state: boolean, data: {\n    node: Node;\n    format: string;\n    parents: Element[];\n}) => void;\ninterface FormatRegistry {\n    get: {\n        (name: string): Format[] | undefined;\n        (): Record<string, Format[]>;\n    };\n    has: (name: string) => boolean;\n    register: (name: string | Formats, format?: Format[] | Format) => void;\n    unregister: (name: string) => Formats;\n}\ninterface Formatter extends FormatRegistry {\n    apply: (name: string, vars?: FormatVars, node?: Node | RangeLikeObject | null) => void;\n    remove: (name: string, vars?: FormatVars, node?: Node | Range, similar?: boolean) => void;\n    toggle: (name: string, vars?: FormatVars, node?: Node) => void;\n    match: (name: string, vars?: FormatVars, node?: Node, similar?: boolean) => boolean;\n    closest: (names: string[]) => string | null;\n    matchAll: (names: string[], vars?: FormatVars) => string[];\n    matchNode: (node: Node | null, name: string, vars?: FormatVars, similar?: boolean) => Format | undefined;\n    canApply: (name: string) => boolean;\n    formatChanged: (names: string, callback: FormatChangeCallback, similar?: boolean, vars?: FormatVars) => {\n        unbind: () => void;\n    };\n    getCssText: (format: string | ApplyFormat) => string;\n}\ninterface EditorMode {\n    isReadOnly: () => boolean;\n    set: (mode: string) => void;\n    get: () => string;\n    register: (mode: string, api: EditorModeApi) => void;\n}\ninterface EditorModeApi {\n    activate: () => void;\n    deactivate: () => void;\n    editorReadOnly: boolean;\n}\ninterface Model {\n    readonly table: {\n        readonly getSelectedCells: () => HTMLTableCellElement[];\n        readonly clearSelectedCells: (container: Node) => void;\n    };\n}\ntype ModelManager = AddOnManager<Model>;\ninterface Plugin {\n    getMetadata?: () => {\n        name: string;\n        url: string;\n    };\n    init?: (editor: Editor, url: string) => void;\n    [key: string]: any;\n}\ntype PluginManager = AddOnManager<void | Plugin>;\ninterface ShortcutsConstructor {\n    readonly prototype: Shortcuts;\n    new (editor: Editor): Shortcuts;\n}\ntype CommandFunc = string | [\n    string,\n    boolean,\n    any\n] | (() => void);\ndeclare class Shortcuts {\n    private readonly editor;\n    private readonly shortcuts;\n    private pendingPatterns;\n    constructor(editor: Editor);\n    add(pattern: string, desc: string | null, cmdFunc: CommandFunc, scope?: any): boolean;\n    remove(pattern: string): boolean;\n    private normalizeCommandFunc;\n    private createShortcut;\n    private hasModifier;\n    private isFunctionKey;\n    private matchShortcut;\n    private executeShortcutAction;\n}\ninterface RenderResult {\n    iframeContainer?: HTMLElement;\n    editorContainer: HTMLElement;\n    api?: Partial<EditorUiApi>;\n}\ninterface Theme {\n    ui?: any;\n    inline?: any;\n    execCommand?: (command: string, ui?: boolean, value?: any) => boolean;\n    destroy?: () => void;\n    init?: (editor: Editor, url: string) => void;\n    renderUI?: () => Promise<RenderResult> | RenderResult;\n    getNotificationManagerImpl?: () => NotificationManagerImpl;\n    getWindowManagerImpl?: () => WindowManagerImpl;\n}\ntype ThemeManager = AddOnManager<void | Theme>;\ninterface EditorConstructor {\n    readonly prototype: Editor;\n    new (id: string, options: RawEditorOptions, editorManager: EditorManager): Editor;\n}\ndeclare class Editor implements EditorObservable {\n    documentBaseUrl: string;\n    baseUri: URI;\n    id: string;\n    plugins: Record<string, Plugin>;\n    documentBaseURI: URI;\n    baseURI: URI;\n    contentCSS: string[];\n    contentStyles: string[];\n    ui: EditorUi;\n    mode: EditorMode;\n    options: Options;\n    editorUpload: EditorUpload;\n    shortcuts: Shortcuts;\n    loadedCSS: Record<string, any>;\n    editorCommands: EditorCommands;\n    suffix: string;\n    editorManager: EditorManager;\n    hidden: boolean;\n    inline: boolean;\n    hasVisual: boolean;\n    isNotDirty: boolean;\n    annotator: Annotator;\n    bodyElement: HTMLElement | undefined;\n    bookmark: any;\n    composing: boolean;\n    container: HTMLElement;\n    contentAreaContainer: HTMLElement;\n    contentDocument: Document;\n    contentWindow: Window;\n    delegates: Record<string, EventUtilsCallback<any>> | undefined;\n    destroyed: boolean;\n    dom: DOMUtils;\n    editorContainer: HTMLElement;\n    eventRoot: Element | undefined;\n    formatter: Formatter;\n    formElement: HTMLElement | undefined;\n    formEventDelegate: ((e: Event) => void) | undefined;\n    hasHiddenInput: boolean;\n    iframeElement: HTMLIFrameElement | null;\n    iframeHTML: string | undefined;\n    initialized: boolean;\n    notificationManager: NotificationManager;\n    orgDisplay: string;\n    orgVisibility: string | undefined;\n    parser: DomParser;\n    quirks: Quirks;\n    readonly: boolean;\n    removed: boolean;\n    schema: Schema;\n    selection: EditorSelection;\n    serializer: DomSerializer;\n    startContent: string;\n    targetElm: HTMLElement;\n    theme: Theme;\n    model: Model;\n    undoManager: UndoManager;\n    windowManager: WindowManager;\n    _beforeUnload: (() => void) | undefined;\n    _eventDispatcher: EventDispatcher<NativeEventMap> | undefined;\n    _nodeChangeDispatcher: NodeChange;\n    _pendingNativeEvents: string[];\n    _selectionOverrides: SelectionOverrides;\n    _skinLoaded: boolean;\n    _editableRoot: boolean;\n    bindPendingEventDelegates: EditorObservable['bindPendingEventDelegates'];\n    toggleNativeEvent: EditorObservable['toggleNativeEvent'];\n    unbindAllNativeEvents: EditorObservable['unbindAllNativeEvents'];\n    fire: EditorObservable['fire'];\n    dispatch: EditorObservable['dispatch'];\n    on: EditorObservable['on'];\n    off: EditorObservable['off'];\n    once: EditorObservable['once'];\n    hasEventListeners: EditorObservable['hasEventListeners'];\n    constructor(id: string, options: RawEditorOptions, editorManager: EditorManager);\n    render(): void;\n    focus(skipFocus?: boolean): void;\n    hasFocus(): boolean;\n    translate(text: Untranslated): TranslatedString;\n    getParam<K extends BuiltInOptionType>(name: string, defaultVal: BuiltInOptionTypeMap[K], type: K): BuiltInOptionTypeMap[K];\n    getParam<K extends keyof NormalizedEditorOptions>(name: K, defaultVal?: NormalizedEditorOptions[K], type?: BuiltInOptionType): NormalizedEditorOptions[K];\n    getParam<T>(name: string, defaultVal: T, type?: BuiltInOptionType): T;\n    hasPlugin(name: string, loaded?: boolean): boolean;\n    nodeChanged(args?: any): void;\n    addCommand<S>(name: string, callback: EditorCommandCallback<S>, scope: S): void;\n    addCommand(name: string, callback: EditorCommandCallback<Editor>): void;\n    addQueryStateHandler<S>(name: string, callback: (this: S) => boolean, scope?: S): void;\n    addQueryStateHandler(name: string, callback: (this: Editor) => boolean): void;\n    addQueryValueHandler<S>(name: string, callback: (this: S) => string, scope: S): void;\n    addQueryValueHandler(name: string, callback: (this: Editor) => string): void;\n    addShortcut(pattern: string, desc: string, cmdFunc: string | [\n        string,\n        boolean,\n        any\n    ] | (() => void), scope?: any): void;\n    execCommand(cmd: string, ui?: boolean, value?: any, args?: ExecCommandArgs): boolean;\n    queryCommandState(cmd: string): boolean;\n    queryCommandValue(cmd: string): string;\n    queryCommandSupported(cmd: string): boolean;\n    show(): void;\n    hide(): void;\n    isHidden(): boolean;\n    setProgressState(state: boolean, time?: number): void;\n    load(args?: Partial<SetContentArgs>): string;\n    save(args?: Partial<GetContentArgs>): string;\n    setContent(content: string, args?: Partial<SetContentArgs>): string;\n    setContent(content: AstNode, args?: Partial<SetContentArgs>): AstNode;\n    setContent(content: Content, args?: Partial<SetContentArgs>): Content;\n    getContent(args: {\n        format: 'tree';\n    } & Partial<GetContentArgs>): AstNode;\n    getContent(args?: Partial<GetContentArgs>): string;\n    insertContent(content: string, args?: any): void;\n    resetContent(initialContent?: string): void;\n    isDirty(): boolean;\n    setDirty(state: boolean): void;\n    getContainer(): HTMLElement;\n    getContentAreaContainer(): HTMLElement;\n    getElement(): HTMLElement;\n    getWin(): Window;\n    getDoc(): Document;\n    getBody(): HTMLElement;\n    convertURL(url: string, name: string, elm?: string | Element): string;\n    addVisual(elm?: HTMLElement): void;\n    setEditableRoot(state: boolean): void;\n    hasEditableRoot(): boolean;\n    remove(): void;\n    destroy(automatic?: boolean): void;\n    uploadImages(): Promise<UploadResult$1[]>;\n    _scanForImages(): Promise<BlobInfoImagePair[]>;\n}\ninterface UrlObject {\n    prefix: string;\n    resource: string;\n    suffix: string;\n}\ntype WaitState = 'added' | 'loaded';\ntype AddOnConstructor<T> = (editor: Editor, url: string) => T;\ninterface AddOnManager<T> {\n    items: AddOnConstructor<T>[];\n    urls: Record<string, string>;\n    lookup: Record<string, {\n        instance: AddOnConstructor<T>;\n    }>;\n    get: (name: string) => AddOnConstructor<T> | undefined;\n    requireLangPack: (name: string, languages?: string) => void;\n    add: (id: string, addOn: AddOnConstructor<T>) => AddOnConstructor<T>;\n    remove: (name: string) => void;\n    createUrl: (baseUrl: UrlObject, dep: string | UrlObject) => UrlObject;\n    load: (name: string, addOnUrl: string | UrlObject) => Promise<void>;\n    waitFor: (name: string, state?: WaitState) => Promise<void>;\n}\ninterface RangeUtils {\n    walk: (rng: Range, callback: (nodes: Node[]) => void) => void;\n    split: (rng: Range) => RangeLikeObject;\n    normalize: (rng: Range) => boolean;\n    expand: (rng: Range, options?: {\n        type: 'word';\n    }) => Range;\n}\ninterface ScriptLoaderSettings {\n    referrerPolicy?: ReferrerPolicy;\n}\ninterface ScriptLoaderConstructor {\n    readonly prototype: ScriptLoader;\n    new (): ScriptLoader;\n    ScriptLoader: ScriptLoader;\n}\ndeclare class ScriptLoader {\n    static ScriptLoader: ScriptLoader;\n    private settings;\n    private states;\n    private queue;\n    private scriptLoadedCallbacks;\n    private queueLoadedCallbacks;\n    private loading;\n    constructor(settings?: ScriptLoaderSettings);\n    _setReferrerPolicy(referrerPolicy: ReferrerPolicy): void;\n    loadScript(url: string): Promise<void>;\n    isDone(url: string): boolean;\n    markDone(url: string): void;\n    add(url: string): Promise<void>;\n    load(url: string): Promise<void>;\n    remove(url: string): void;\n    loadQueue(): Promise<void>;\n    loadScripts(scripts: string[]): Promise<void>;\n}\ntype TextProcessCallback = (node: Text, offset: number, text: string) => number;\ninterface Spot {\n    container: Text;\n    offset: number;\n}\ninterface TextSeeker {\n    backwards: (node: Node, offset: number, process: TextProcessCallback, root?: Node) => Spot | null;\n    forwards: (node: Node, offset: number, process: TextProcessCallback, root?: Node) => Spot | null;\n}\ninterface DomTreeWalkerConstructor {\n    readonly prototype: DomTreeWalker;\n    new (startNode: Node, rootNode: Node): DomTreeWalker;\n}\ndeclare class DomTreeWalker {\n    private readonly rootNode;\n    private node;\n    constructor(startNode: Node, rootNode: Node);\n    current(): Node | null | undefined;\n    next(shallow?: boolean): Node | null | undefined;\n    prev(shallow?: boolean): Node | null | undefined;\n    prev2(shallow?: boolean): Node | null | undefined;\n    private findSibling;\n    private findPreviousNode;\n}\ninterface Version {\n    major: number;\n    minor: number;\n}\ninterface Env {\n    transparentSrc: string;\n    documentMode: number;\n    cacheSuffix: any;\n    container: any;\n    canHaveCSP: boolean;\n    windowsPhone: boolean;\n    browser: {\n        current: string | undefined;\n        version: Version;\n        isEdge: () => boolean;\n        isChromium: () => boolean;\n        isIE: () => boolean;\n        isOpera: () => boolean;\n        isFirefox: () => boolean;\n        isSafari: () => boolean;\n    };\n    os: {\n        current: string | undefined;\n        version: Version;\n        isWindows: () => boolean;\n        isiOS: () => boolean;\n        isAndroid: () => boolean;\n        isMacOS: () => boolean;\n        isLinux: () => boolean;\n        isSolaris: () => boolean;\n        isFreeBSD: () => boolean;\n        isChromeOS: () => boolean;\n    };\n    deviceType: {\n        isiPad: () => boolean;\n        isiPhone: () => boolean;\n        isTablet: () => boolean;\n        isPhone: () => boolean;\n        isTouch: () => boolean;\n        isWebView: () => boolean;\n        isDesktop: () => boolean;\n    };\n}\ninterface FakeClipboardItem {\n    readonly items: Record<string, any>;\n    readonly types: ReadonlyArray<string>;\n    readonly getType: <D = any>(type: string) => D | undefined;\n}\ninterface FakeClipboard {\n    readonly FakeClipboardItem: (items: Record<string, any>) => FakeClipboardItem;\n    readonly write: (data: FakeClipboardItem[]) => void;\n    readonly read: () => FakeClipboardItem[] | undefined;\n    readonly clear: () => void;\n}\ninterface FocusManager {\n    isEditorUIElement: (elm: Element) => boolean;\n}\ninterface EntitiesMap {\n    [name: string]: string;\n}\ninterface Entities {\n    encodeRaw: (text: string, attr?: boolean) => string;\n    encodeAllRaw: (text: string) => string;\n    encodeNumeric: (text: string, attr?: boolean) => string;\n    encodeNamed: (text: string, attr?: boolean, entities?: EntitiesMap) => string;\n    getEncodeFunc: (name: string, entities?: string) => (text: string, attr?: boolean) => string;\n    decode: (text: string) => string;\n}\ninterface IconPack {\n    icons: Record<string, string>;\n}\ninterface IconManager {\n    add: (id: string, iconPack: IconPack) => void;\n    get: (id: string) => IconPack;\n    has: (id: string) => boolean;\n}\ninterface Resource {\n    load: <T = any>(id: string, url: string) => Promise<T>;\n    add: (id: string, data: any) => void;\n    has: (id: string) => boolean;\n    get: (id: string) => any;\n    unload: (id: string) => void;\n}\ntype TextPatterns_d_Pattern = Pattern;\ntype TextPatterns_d_RawPattern = RawPattern;\ntype TextPatterns_d_DynamicPatternsLookup = DynamicPatternsLookup;\ntype TextPatterns_d_RawDynamicPatternsLookup = RawDynamicPatternsLookup;\ntype TextPatterns_d_DynamicPatternContext = DynamicPatternContext;\ntype TextPatterns_d_BlockCmdPattern = BlockCmdPattern;\ntype TextPatterns_d_BlockPattern = BlockPattern;\ntype TextPatterns_d_BlockFormatPattern = BlockFormatPattern;\ntype TextPatterns_d_InlineCmdPattern = InlineCmdPattern;\ntype TextPatterns_d_InlinePattern = InlinePattern;\ntype TextPatterns_d_InlineFormatPattern = InlineFormatPattern;\ndeclare namespace TextPatterns_d {\n    export { TextPatterns_d_Pattern as Pattern, TextPatterns_d_RawPattern as RawPattern, TextPatterns_d_DynamicPatternsLookup as DynamicPatternsLookup, TextPatterns_d_RawDynamicPatternsLookup as RawDynamicPatternsLookup, TextPatterns_d_DynamicPatternContext as DynamicPatternContext, TextPatterns_d_BlockCmdPattern as BlockCmdPattern, TextPatterns_d_BlockPattern as BlockPattern, TextPatterns_d_BlockFormatPattern as BlockFormatPattern, TextPatterns_d_InlineCmdPattern as InlineCmdPattern, TextPatterns_d_InlinePattern as InlinePattern, TextPatterns_d_InlineFormatPattern as InlineFormatPattern, };\n}\ninterface Delay {\n    setEditorInterval: (editor: Editor, callback: () => void, time?: number) => number;\n    setEditorTimeout: (editor: Editor, callback: () => void, time?: number) => number;\n}\ntype UploadResult = UploadResult$2;\ninterface ImageUploader {\n    upload: (blobInfos: BlobInfo[], showNotification?: boolean) => Promise<UploadResult[]>;\n}\ntype ArrayCallback$1<T, R> = (this: any, x: T, i: number, xs: ArrayLike<T>) => R;\ntype ObjCallback$1<T, R> = (this: any, value: T, key: string, obj: Record<string, T>) => R;\ntype ArrayCallback<T, R> = ArrayCallback$1<T, R>;\ntype ObjCallback<T, R> = ObjCallback$1<T, R>;\ntype WalkCallback<T> = (this: any, o: T, i: string, n: keyof T | undefined) => boolean | void;\ninterface Tools {\n    is: (obj: any, type?: string) => boolean;\n    isArray: <T>(arr: any) => arr is Array<T>;\n    inArray: <T>(arr: ArrayLike<T>, value: T) => number;\n    grep: {\n        <T>(arr: ArrayLike<T> | null | undefined, pred?: ArrayCallback<T, boolean>): T[];\n        <T>(arr: Record<string, T> | null | undefined, pred?: ObjCallback<T, boolean>): T[];\n    };\n    trim: (str: string | null | undefined) => string;\n    toArray: <T>(obj: ArrayLike<T>) => T[];\n    hasOwn: (obj: any, name: string) => boolean;\n    makeMap: (items: ArrayLike<string> | string | undefined, delim?: string | RegExp, map?: Record<string, {}>) => Record<string, {}>;\n    each: {\n        <T>(arr: ArrayLike<T> | null | undefined, cb: ArrayCallback<T, void | boolean>, scope?: any): boolean;\n        <T>(obj: Record<string, T> | null | undefined, cb: ObjCallback<T, void | boolean>, scope?: any): boolean;\n    };\n    map: {\n        <T, R>(arr: ArrayLike<T> | null | undefined, cb: ArrayCallback<T, R>): R[];\n        <T, R>(obj: Record<string, T> | null | undefined, cb: ObjCallback<T, R>): R[];\n    };\n    extend: (obj: Object, ext: Object, ...objs: Object[]) => any;\n    walk: <T extends Record<string, any>>(obj: T, f: WalkCallback<T>, n?: keyof T, scope?: any) => void;\n    resolve: (path: string, o?: Object) => any;\n    explode: (s: string | string[], d?: string | RegExp) => string[];\n    _addCacheSuffix: (url: string) => string;\n}\ninterface KeyboardLikeEvent {\n    shiftKey: boolean;\n    ctrlKey: boolean;\n    altKey: boolean;\n    metaKey: boolean;\n}\ninterface VK {\n    BACKSPACE: number;\n    DELETE: number;\n    DOWN: number;\n    ENTER: number;\n    ESC: number;\n    LEFT: number;\n    RIGHT: number;\n    SPACEBAR: number;\n    TAB: number;\n    UP: number;\n    PAGE_UP: number;\n    PAGE_DOWN: number;\n    END: number;\n    HOME: number;\n    modifierPressed: (e: KeyboardLikeEvent) => boolean;\n    metaKeyPressed: (e: KeyboardLikeEvent) => boolean;\n}\ninterface DOMUtilsNamespace {\n    (doc: Document, settings: Partial<DOMUtilsSettings>): DOMUtils;\n    DOM: DOMUtils;\n    nodeIndex: (node: Node, normalized?: boolean) => number;\n}\ninterface RangeUtilsNamespace {\n    (dom: DOMUtils): RangeUtils;\n    compareRanges: (rng1: RangeLikeObject, rng2: RangeLikeObject) => boolean;\n    getCaretRangeFromPoint: (clientX: number, clientY: number, doc: Document) => Range;\n    getSelectedNode: (range: Range) => Node;\n    getNode: (container: Node, offset: number) => Node;\n}\ninterface AddOnManagerNamespace {\n    <T>(): AddOnManager<T>;\n    language: string | undefined;\n    languageLoad: boolean;\n    baseURL: string;\n    PluginManager: PluginManager;\n    ThemeManager: ThemeManager;\n    ModelManager: ModelManager;\n}\ninterface BookmarkManagerNamespace {\n    (selection: EditorSelection): BookmarkManager;\n    isBookmarkNode: (node: Node) => boolean;\n}\ninterface TinyMCE extends EditorManager {\n    geom: {\n        Rect: Rect;\n    };\n    util: {\n        Delay: Delay;\n        Tools: Tools;\n        VK: VK;\n        URI: URIConstructor;\n        EventDispatcher: EventDispatcherConstructor<any>;\n        Observable: Observable<any>;\n        I18n: I18n;\n        LocalStorage: Storage;\n        ImageUploader: ImageUploader;\n    };\n    dom: {\n        EventUtils: EventUtilsConstructor;\n        TreeWalker: DomTreeWalkerConstructor;\n        TextSeeker: (dom: DOMUtils, isBlockBoundary?: (node: Node) => boolean) => TextSeeker;\n        DOMUtils: DOMUtilsNamespace;\n        ScriptLoader: ScriptLoaderConstructor;\n        RangeUtils: RangeUtilsNamespace;\n        Serializer: (settings: DomSerializerSettings, editor?: Editor) => DomSerializer;\n        ControlSelection: (selection: EditorSelection, editor: Editor) => ControlSelection;\n        BookmarkManager: BookmarkManagerNamespace;\n        Selection: (dom: DOMUtils, win: Window, serializer: DomSerializer, editor: Editor) => EditorSelection;\n        StyleSheetLoader: (documentOrShadowRoot: Document | ShadowRoot, settings: StyleSheetLoaderSettings) => StyleSheetLoader;\n        Event: EventUtils;\n    };\n    html: {\n        Styles: (settings?: StylesSettings, schema?: Schema) => Styles;\n        Entities: Entities;\n        Node: AstNodeConstructor;\n        Schema: (settings?: SchemaSettings) => Schema;\n        DomParser: (settings?: DomParserSettings, schema?: Schema) => DomParser;\n        Writer: (settings?: WriterSettings) => Writer;\n        Serializer: (settings?: HtmlSerializerSettings, schema?: Schema) => HtmlSerializer;\n    };\n    AddOnManager: AddOnManagerNamespace;\n    Annotator: (editor: Editor) => Annotator;\n    Editor: EditorConstructor;\n    EditorCommands: EditorCommandsConstructor;\n    EditorManager: EditorManager;\n    EditorObservable: EditorObservable;\n    Env: Env;\n    FocusManager: FocusManager;\n    Formatter: (editor: Editor) => Formatter;\n    NotificationManager: (editor: Editor) => NotificationManager;\n    Shortcuts: ShortcutsConstructor;\n    UndoManager: (editor: Editor) => UndoManager;\n    WindowManager: (editor: Editor) => WindowManager;\n    DOM: DOMUtils;\n    ScriptLoader: ScriptLoader;\n    PluginManager: PluginManager;\n    ThemeManager: ThemeManager;\n    ModelManager: ModelManager;\n    IconManager: IconManager;\n    Resource: Resource;\n    FakeClipboard: FakeClipboard;\n    trim: Tools['trim'];\n    isArray: Tools['isArray'];\n    is: Tools['is'];\n    toArray: Tools['toArray'];\n    makeMap: Tools['makeMap'];\n    each: Tools['each'];\n    map: Tools['map'];\n    grep: Tools['grep'];\n    inArray: Tools['inArray'];\n    extend: Tools['extend'];\n    walk: Tools['walk'];\n    resolve: Tools['resolve'];\n    explode: Tools['explode'];\n    _addCacheSuffix: Tools['_addCacheSuffix'];\n}\ndeclare const tinymce: TinyMCE;\nexport { AddOnManager, Annotator, AstNode, Bookmark, BookmarkManager, ControlSelection, DOMUtils, Delay, DomParser, DomParserSettings, DomSerializer, DomSerializerSettings, DomTreeWalker, Editor, EditorCommands, EditorEvent, EditorManager, EditorModeApi, EditorObservable, EditorOptions, EditorSelection, Entities, Env, EventDispatcher, EventUtils, EventTypes_d as Events, FakeClipboard, FocusManager, Format_d as Formats, Formatter, GeomRect, HtmlSerializer, HtmlSerializerSettings, I18n, IconManager, Model, ModelManager, NotificationApi, NotificationManager, NotificationSpec, Observable, Plugin, PluginManager, RangeUtils, RawEditorOptions, Rect, Resource, Schema, SchemaSettings, ScriptLoader, Shortcuts, StyleSheetLoader, Styles, TextPatterns_d as TextPatterns, TextSeeker, Theme, ThemeManager, TinyMCE, Tools, URI, Ui_d as Ui, UndoManager, VK, WindowManager, Writer, WriterSettings, tinymce as default };\n"
  },
  {
    "path": "public/uploads/.gitignore",
    "content": "*\n!.gitignore\n!.htaccess"
  },
  {
    "path": "public/uploads/.htaccess",
    "content": "Options -Indexes"
  },
  {
    "path": "public/web.config",
    "content": "<!--\n    Rewrites requires Microsoft URL Rewrite Module for IIS\n    Download: https://www.iis.net/downloads/microsoft/url-rewrite\n    Debug Help: https://docs.microsoft.com/en-us/iis/extensions/url-rewrite-module/using-failed-request-tracing-to-trace-rewrite-rules\n-->\n<configuration>\n  <system.webServer>\n    <rewrite>\n      <rules>\n        <rule name=\"Imported Rule 1\" stopProcessing=\"true\">\n          <match url=\"^(.*)/$\" ignoreCase=\"false\" />\n          <conditions>\n            <add input=\"{REQUEST_FILENAME}\" matchType=\"IsDirectory\" ignoreCase=\"false\" negate=\"true\" />\n          </conditions>\n          <action type=\"Redirect\" redirectType=\"Permanent\" url=\"/{R:1}\" />\n        </rule>\n        <rule name=\"Imported Rule 2\" stopProcessing=\"true\">\n          <match url=\"^\" ignoreCase=\"false\" />\n          <conditions>\n            <add input=\"{REQUEST_FILENAME}\" matchType=\"IsDirectory\" ignoreCase=\"false\" negate=\"true\" />\n            <add input=\"{REQUEST_FILENAME}\" matchType=\"IsFile\" ignoreCase=\"false\" negate=\"true\" />\n          </conditions>\n          <action type=\"Rewrite\" url=\"index.php\" />\n        </rule>\n      </rules>\n    </rewrite>\n  </system.webServer>\n</configuration>"
  },
  {
    "path": "readme.md",
    "content": "# BookStack\n\n[![GitHub release](https://img.shields.io/github/release/BookStackApp/BookStack.svg)](https://github.com/BookStackApp/BookStack/releases/latest)\n[![license](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/BookStackApp/BookStack/blob/development/LICENSE)\n[![Crowdin](https://badges.crowdin.net/bookstack/localized.svg)](https://crowdin.com/project/bookstack)\n[![Build Status](https://github.com/BookStackApp/BookStack/workflows/test-php/badge.svg)](https://github.com/BookStackApp/BookStack/actions)\n[![Lint Status](https://github.com/BookStackApp/BookStack/workflows/lint-php/badge.svg)](https://github.com/BookStackApp/BookStack/actions)\n[![php-metrics](https://img.shields.io/static/v1?label=Metrics&message=php&color=4F5B93)](https://source.bookstackapp.com/php-stats/index.html)\n<br>\n[![Alternate Source](https://img.shields.io/static/v1?label=Alt+Source&message=Git&color=ef391a&logo=git)](https://source.bookstackapp.com/)\n[![Repo Stats](https://img.shields.io/static/v1?label=GitHub+project&message=stats&color=f27e3f)](https://gh-stats.bookstackapp.com/)\n[![Discord](https://img.shields.io/static/v1?label=Discord&message=chat&color=738adb&logo=discord)](https://www.bookstackapp.com/links/discord)\n[![Mastodon](https://img.shields.io/static/v1?label=Mastodon&message=@bookstack&color=595aff&logo=mastodon)](https://www.bookstackapp.com/links/mastodon)\n<br>\n[![PeerTube](https://img.shields.io/static/v1?label=PeerTube&message=bookstack@foss.video&color=f2690d&logo=peertube)](https://foss.video/c/bookstack)\n[![YouTube](https://img.shields.io/static/v1?label=YouTube&message=bookstackapp&color=ff0000&logo=youtube)](https://www.youtube.com/bookstackapp)\n\nA platform for storing and organising information and documentation. Details for BookStack can be found on the official website at https://www.bookstackapp.com/.\n\n* [Installation Instructions](https://www.bookstackapp.com/docs/admin/installation)\n* [Documentation](https://www.bookstackapp.com/docs)\n* [Demo Instance](https://demo.bookstackapp.com)\n    * [Admin Login](https://demo.bookstackapp.com/login?email=admin@example.com&password=password)\n* [Screenshots](https://www.bookstackapp.com/#screenshots) \n* [BookStack Blog](https://www.bookstackapp.com/blog)\n* [Issue List](https://github.com/BookStackApp/BookStack/issues)\n* [Discord Chat](https://www.bookstackapp.com/links/discord)\n* [Support Options](https://www.bookstackapp.com/support/)\n\n## 📚 Project Definition\n\nBookStack is an opinionated documentation platform that provides a pleasant and simple out-of-the-box experience. New users to an instance should find the experience intuitive and only basic word-processing skills should be required to get involved in creating content on BookStack. The platform should provide advanced power features to those that desire it, but they should not interfere with the core simple user experience.\n\nBookStack is not designed as an extensible platform to be used for purposes that differ to the statement above.\n\nIn regard to development philosophy, BookStack has a relaxed, open & positive approach. We aim to slowly yet continuously evolve the platform while providing a stable & easy upgrade path. \n\nYou can read more about the project and its origins in [our FAQ here](https://www.bookstackapp.com/about/project-faq/).\n\n## 🌟 Project Sponsors\n\nShown below are our bronze, silver and gold project sponsors.\nBig thanks to these companies for supporting the project.\n*Note: The listed services are not tested, vetted nor supported by the official BookStack project in any manner.*\n\n[Project donation details](https://www.bookstackapp.com/donate/) - [GitHub Sponsors Page](https://github.com/sponsors/ssddanbrown) - [Ko-fi Page](https://ko-fi.com/ssddanbrown)\n\n#### Gold Sponsor\n\n<table><tbody><tr>\n<td align=\"center\"><a href=\"https://www.diagrams.net/\" target=\"_blank\">\n    <img width=\"480\" src=\"https://www.bookstackapp.com/images/sponsors/diagramsnet.png\" alt=\"Diagrams.net\">\n</a></td>\n</tr>\n<tr>\n<td align=\"center\"><a href=\"https://www.onyx.app/?utm_source=bookstack\" target=\"_blank\">\n    <img width=\"400\" src=\"https://www.bookstackapp.com/images/sponsors/onyx.png\" alt=\"onyx.app\">\n</a></td>\n</tr>\n</tbody></table>\n\n#### Bronze Sponsors\n\n<table><tbody><tr>\n<td align=\"center\"><a href=\"https://cloudabove.com/hosting\" target=\"_blank\">\n    <img width=\"200\" src=\"https://www.bookstackapp.com/images/sponsors/cloudabove.png\" alt=\"Cloudabove\">\n</a></td>\n<td align=\"center\"><a href=\"https://www.practicali.be\" target=\"_blank\">\n    <img width=\"240\" src=\"https://www.bookstackapp.com/images/sponsors/practicali.png\" alt=\"Practicali\">\n</a></td>\n</tr><tr>\n<td align=\"center\"><a href=\"https://www.stellarhosted.com/bookstack/\" target=\"_blank\">\n    <img width=\"240\" src=\"https://www.bookstackapp.com/images/sponsors/stellarhosted.png\" alt=\"Stellar Hosted\">\n</a></td>\n<td align=\"center\" style=\"text-align: center\"><a href=\"https://nws.netways.de/apps/bookstack/\" target=\"_blank\">\n    <img width=\"240\" src=\"https://www.bookstackapp.com/images/sponsors/netways.png\" alt=\"NETWAYS Web Services\">\n</a></td>\n</tr>\n<tr>\n<td align=\"center\"><a href=\"https://practinet.be/\" target=\"_blank\">\n    <img width=\"240\" src=\"https://www.bookstackapp.com/images/sponsors/practinet.png\" alt=\"Practinet\">\n</a></td>\n<td align=\"center\"><a href=\"https://route4me.com/\" target=\"_blank\">\n    <img width=\"240\" src=\"https://www.bookstackapp.com/images/sponsors/route4me.png\" alt=\"Route4Me - Route Optimizer and Route Planner Software\">\n</a></td>\n</tr>\n<tr>\n<td align=\"center\"><a href=\"https://phamos.eu\" target=\"_blank\">\n    <img width=\"132\" src=\"https://www.bookstackapp.com/images/sponsors/phamos.png\" alt=\"phamos\">\n</a></td>\n<td align=\"center\"><a href=\"https://sitespeak.ai/bookstack\" target=\"_blank\">\n    <img width=\"240\" src=\"https://www.bookstackapp.com/images/sponsors/sitespeak.png\" alt=\"SiteSpeakAI\">\n</a></td>\n</tr>\n<tr>\n<td align=\"center\" colspan=\"2\"><a href=\"https://www.admin-intelligence.de/bookstack/\" target=\"_blank\">\n    <img width=\"210\" src=\"https://www.bookstackapp.com/images/sponsors/admin-intelligence.png\" alt=\"Admin Intelligence\">\n</a></td>\n</tr>\n</tbody></table>\n\n## 🛠️ Development & Testing\n\nPlease see our [development docs](dev/docs/development.md) for full details regarding work on the BookStack source code.\n\nIf you're just looking to customize or extend your own BookStack instance, take a look at our [Hacking BookStack documentation page](https://www.bookstackapp.com/docs/admin/hacking-bookstack/) for details on various options to achieve this without altering the BookStack source code.\n\nDetails about BookStack's versioning scheme and the general release process [can be found here](dev/docs/release-process.md).\n\n## 🌎 Translations\n\nTranslations for text within BookStack are managed through the [BookStack project on Crowdin](https://crowdin.com/project/bookstack). Some strings have colon-prefixed variables such as `:userName`. Leave these values as they are as they will be replaced at run-time.\n\nPlease use [Crowdin](https://crowdin.com/project/bookstack) to contribute translations instead of opening a pull request. The translations within the working codebase can be out-of-date, and merging via code can cause conflicts & sync issues. If for some reason you can't use Crowdin feel free to open an issue to discuss alternative options. \n\nIf you'd like a new language to be added to Crowdin, for you to be able to provide translations for, please [open a new issue here](https://github.com/BookStackApp/BookStack/issues/new?template=language_request.yml).\n\nPlease note, translations in BookStack are provided to the \"Crowdin Global Translation Memory\" which helps BookStack and other projects with finding translations. If you are not happy with contributing to this then providing translations to BookStack, even manually via GitHub, is not advised.\n\n## 🎁 Contributing, Issues & Pull Requests\n\nFeel free to [create issues](https://github.com/BookStackApp/BookStack/issues/new/choose) to request new features or to report bugs & problems. Just please follow the template given when creating the issue.\n\nPull requests are welcome but, unless it's a small tweak, it may be best to open the pull request early or create an issue for your intended change to discuss how it will fit into the project and plan out the merge. Just because a feature request exists, or is tagged, does not mean that feature would be accepted into the core project.\n\nPull requests should be created from the `development` branch since they will be merged back into `development` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases. If you are looking to alter CSS or JavaScript content please edit the source files found in `resources/`. Any CSS or JS files within `public` are built from these source files and therefore should not be edited directly.\n\nThe project's code of conduct [can be found here](https://github.com/BookStackApp/BookStack/blob/development/.github/CODE_OF_CONDUCT.md).\n\n## 🔒 Security\n\nSecurity information for administering a BookStack instance can be found on the [documentation site here](https://www.bookstackapp.com/docs/admin/security/).\n\nIf you'd like to be notified of new potential security concerns you can [sign-up to the BookStack security mailing list](https://updates.bookstackapp.com/signup/bookstack-security-updates).\n\nIf you would like to report a security concern, details of doing so [can be found here](https://github.com/BookStackApp/BookStack/blob/development/.github/SECURITY.md).\n\n## ♿ Accessibility\n\nWe want BookStack to remain accessible to as many people as possible. We aim for at least WCAG 2.1 Level A standards where possible although we do not strictly test this upon each release. If you come across any accessibility issues please feel free to open an issue.\n\n## 🖥️ Website, Docs & Blog\n\nThe website which contains the project docs & blog can be found in the [BookStackApp/website](https://codeberg.org/bookstack/website) repo.\n\n## ⚖️ License\n\nThe BookStack source is provided under the [MIT License](https://github.com/BookStackApp/BookStack/blob/development/LICENSE). \n\nThe libraries used by, and included with, BookStack are provided under their own licenses and copyright.\nThe licenses for many of our core dependencies can be found in the attribution list below, but this is not an exhaustive list of all projects used within BookStack. \n\n## 👪 Attribution\n\nThe great people that have worked to build and improve BookStack can [be seen here](https://github.com/BookStackApp/BookStack/graphs/contributors). The wonderful people that have provided translations, either through GitHub or via Crowdin [can be seen here](https://github.com/BookStackApp/BookStack/blob/development/.github/translators.txt).\n\nBelow are the great open-source projects used to help build BookStack. \nNote: This is not an exhaustive list of all libraries and projects that would be used in an active BookStack instance.\n\n* [Laravel](http://laravel.com/) - _[MIT](https://github.com/laravel/framework/blob/v8.82.0/LICENSE.md)_\n* [TinyMCE](https://www.tinymce.com/) - _[MIT](https://github.com/tinymce/tinymce/blob/develop/LICENSE.TXT)_\n* [Lexical](https://lexical.dev/) - _[MIT](https://github.com/facebook/lexical/blob/main/LICENSE)_\n* [CodeMirror](https://codemirror.net) - _[MIT](https://github.com/codemirror/CodeMirror/blob/master/LICENSE)_\n* [Sortable](https://github.com/SortableJS/Sortable) - _[MIT](https://github.com/SortableJS/Sortable/blob/master/LICENSE)_\n* [Google Material Icons](https://github.com/google/material-design-icons) - _[Apache-2.0](https://github.com/google/material-design-icons/blob/master/LICENSE)_\n* [markdown-it](https://github.com/markdown-it/markdown-it) and [markdown-it-task-lists](https://github.com/revin/markdown-it-task-lists) - _[MIT](https://github.com/markdown-it/markdown-it/blob/master/LICENSE) and [ISC](https://github.com/revin/markdown-it-task-lists/blob/master/LICENSE)_\n* [Dompdf](https://github.com/dompdf/dompdf) - _[LGPL v2.1](https://github.com/dompdf/dompdf/blob/master/LICENSE.LGPL)_\n* [KnpLabs/snappy](https://github.com/KnpLabs/snappy) - _[MIT](https://github.com/KnpLabs/snappy/blob/master/LICENSE)_\n* [WKHTMLtoPDF](http://wkhtmltopdf.org/index.html) - _[LGPL v3.0](https://github.com/wkhtmltopdf/wkhtmltopdf/blob/master/LICENSE)_\n* [diagrams.net](https://github.com/jgraph/drawio) - _[Embedded Version Terms](https://www.diagrams.net/trust/) / [Source Project - Apache-2.0](https://github.com/jgraph/drawio/blob/dev/LICENSE)_\n* [SAML PHP Toolkit](https://github.com/SAML-Toolkits/php-saml) - _[MIT](https://github.com/SAML-Toolkits/php-saml/blob/master/LICENSE)_\n* [League/CommonMark](https://commonmark.thephpleague.com/) - _[BSD-3-Clause](https://github.com/thephpleague/commonmark/blob/2.2/LICENSE)_\n* [League/Flysystem](https://flysystem.thephpleague.com) - _[MIT](https://github.com/thephpleague/flysystem/blob/3.x/LICENSE)_\n* [League/html-to-markdown](https://github.com/thephpleague/html-to-markdown) - _[MIT](https://github.com/thephpleague/html-to-markdown/blob/master/LICENSE)_\n* [League/oauth2-client](https://oauth2-client.thephpleague.com/) - _[MIT](https://github.com/thephpleague/oauth2-client/blob/master/LICENSE)_\n* [pragmarx/google2fa](https://github.com/antonioribeiro/google2fa) - _[MIT](https://github.com/antonioribeiro/google2fa/blob/8.x/LICENSE.md)_\n* [Bacon/BaconQrCode](https://github.com/Bacon/BaconQrCode) - _[BSD-2-Clause](https://github.com/Bacon/BaconQrCode/blob/master/LICENSE)_\n* [phpseclib](https://github.com/phpseclib/phpseclib) - _[MIT](https://github.com/phpseclib/phpseclib/blob/master/LICENSE)_\n* [Clockwork](https://github.com/itsgoingd/clockwork) - _[MIT](https://github.com/itsgoingd/clockwork/blob/master/LICENSE)_\n* [PHPStan](https://phpstan.org/) & [Larastan](https://github.com/nunomaduro/larastan) - _[MIT](https://github.com/phpstan/phpstan/blob/master/LICENSE) and [MIT](https://github.com/nunomaduro/larastan/blob/master/LICENSE.md)_\n* [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) - _[BSD 3-Clause](https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt)_\n* [JakeArchibald/IDB-Keyval](https://github.com/jakearchibald/idb-keyval) - _[Apache-2.0](https://github.com/jakearchibald/idb-keyval/blob/main/LICENCE)_\n* [HTML Purifier](https://github.com/ezyang/htmlpurifier) and [htmlpurifier-html5](https://github.com/xemlock/htmlpurifier-html5) - _[LGPL-2.1](https://github.com/ezyang/htmlpurifier/blob/master/LICENSE) and [MIT](https://github.com/xemlock/htmlpurifier-html5/blob/master/LICENSE)_\n\nFor a detailed breakdown of the JavaScript & PHP projects imported and used via NPM & composer package managers, along with their licenses, please see the [dev/licensing/js-library-licenses.txt](dev/licensing/js-library-licenses.txt) and [dev/licensing/php-library-licenses.txt](dev/licensing/php-library-licenses.txt) files. "
  },
  {
    "path": "resources/js/app.ts",
    "content": "import {EventManager} from './services/events';\nimport {HttpManager} from './services/http';\nimport {Translator} from './services/translations';\nimport * as componentMap from './components/index';\nimport {ComponentStore} from './services/components';\nimport {baseUrl, importVersioned} from \"./services/util\";\n\n// eslint-disable-next-line no-underscore-dangle\nwindow.__DEV__ = false;\n\n// Make common important util functions global\nwindow.baseUrl = baseUrl;\nwindow.importVersioned = importVersioned;\n\n// Setup events, http & translation services\nwindow.$http = new HttpManager();\nwindow.$events = new EventManager();\nwindow.$trans = new Translator();\n\n// Load & initialise components\nwindow.$components = new ComponentStore();\nwindow.$components.register(componentMap);\nwindow.$components.init();\n"
  },
  {
    "path": "resources/js/code/index.mjs",
    "content": "import {EditorView, keymap} from '@codemirror/view';\n\nimport {copyTextToClipboard} from '../services/clipboard.ts';\nimport {viewerExtensions, editorExtensions} from './setups';\nimport {createView} from './views';\nimport {SimpleEditorInterface} from './simple-editor-interface';\n\n/**\n * Add a button to a CodeMirror instance which copies the contents to the clipboard upon click.\n * @param {EditorView} editorView\n */\nfunction addCopyIcon(editorView) {\n    const copyIcon = '<svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z\"/></svg>';\n    const checkIcon = '<svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z\"/></svg>';\n    const copyButton = document.createElement('button');\n    copyButton.setAttribute('type', 'button');\n    copyButton.classList.add('cm-copy-button');\n    copyButton.innerHTML = copyIcon;\n    editorView.dom.appendChild(copyButton);\n\n    const notifyTime = 620;\n    const transitionTime = 60;\n    copyButton.addEventListener('click', () => {\n        copyTextToClipboard(editorView.state.doc.toString());\n        copyButton.classList.add('success');\n\n        setTimeout(() => {\n            copyButton.innerHTML = checkIcon;\n        }, transitionTime / 2);\n\n        setTimeout(() => {\n            copyButton.classList.remove('success');\n        }, notifyTime);\n\n        setTimeout(() => {\n            copyButton.innerHTML = copyIcon;\n        }, notifyTime + (transitionTime / 2));\n    });\n}\n\n/**\n * @param {HTMLElement} codeElem\n * @returns {String}\n */\nfunction getDirectionFromCodeBlock(codeElem) {\n    let dir = '';\n    const innerCodeElem = codeElem.querySelector('code');\n\n    if (innerCodeElem && innerCodeElem.hasAttribute('dir')) {\n        dir = innerCodeElem.getAttribute('dir');\n    } else if (codeElem.hasAttribute('dir')) {\n        dir = codeElem.getAttribute('dir');\n    }\n\n    return dir;\n}\n\n/**\n * Add code highlighting to a single element.\n * @param {HTMLElement} elem\n */\nfunction highlightElem(elem) {\n    const innerCodeElem = elem.querySelector('code[class^=language-]');\n    elem.innerHTML = elem.innerHTML.replace(/<br\\s*\\/?>/gi, '\\n');\n    const content = elem.textContent.trimEnd();\n\n    let langName = '';\n    if (innerCodeElem !== null) {\n        langName = innerCodeElem.className.replace('language-', '');\n    }\n\n    const wrapper = document.createElement('div');\n    elem.parentNode.insertBefore(wrapper, elem);\n\n    const direction = getDirectionFromCodeBlock(elem);\n    if (direction) {\n        wrapper.setAttribute('dir', direction);\n    }\n\n    const ev = createView('content-code-block', {\n        parent: wrapper,\n        doc: content,\n        extensions: viewerExtensions(wrapper),\n    });\n\n    const editor = new SimpleEditorInterface(ev);\n    editor.setMode(langName, content);\n\n    elem.remove();\n    addCopyIcon(ev);\n}\n\n/**\n * Highlight all code blocks within the given parent element\n * @param {HTMLElement} parent\n */\nexport function highlightWithin(parent) {\n    const codeBlocks = parent.querySelectorAll('pre');\n    for (const codeBlock of codeBlocks) {\n        highlightElem(codeBlock);\n    }\n}\n\n/**\n * Highlight pre elements on a page\n */\nexport function highlight() {\n    const codeBlocks = document.querySelectorAll('.page-content pre, .comment-box .content pre');\n    for (const codeBlock of codeBlocks) {\n        highlightElem(codeBlock);\n    }\n}\n\n/**\n * Create a CodeMirror instance for showing inside the WYSIWYG editor.\n * Manages a textarea element to hold code content.\n * @param {HTMLElement} cmContainer\n * @param {ShadowRoot} shadowRoot\n * @param {String} content\n * @param {String} language\n * @returns {SimpleEditorInterface}\n */\nexport function wysiwygView(cmContainer, shadowRoot, content, language) {\n    const ev = createView('content-code-block', {\n        parent: cmContainer,\n        doc: content,\n        extensions: viewerExtensions(cmContainer),\n        root: shadowRoot,\n    });\n\n    const editor = new SimpleEditorInterface(ev);\n    editor.setMode(language, content);\n\n    return editor;\n}\n\n/**\n * Create a CodeMirror instance to show in the WYSIWYG pop-up editor\n * @param {HTMLElement} elem\n * @param {String} modeSuggestion\n * @returns {SimpleEditorInterface}\n */\nexport function popupEditor(elem, modeSuggestion) {\n    const content = elem.textContent;\n    const config = {\n        parent: elem.parentElement,\n        doc: content,\n        extensions: [\n            ...editorExtensions(elem.parentElement),\n        ],\n    };\n\n    // Create editor, hide original input\n    const editor = new SimpleEditorInterface(createView('code-editor', config));\n    editor.setMode(modeSuggestion, content);\n    elem.style.display = 'none';\n\n    return editor;\n}\n\n/**\n * Create an inline editor to replace the given textarea.\n * @param {HTMLTextAreaElement} textArea\n * @param {String} mode\n * @returns {SimpleEditorInterface}\n */\nexport function inlineEditor(textArea, mode) {\n    const content = textArea.value;\n    const config = {\n        parent: textArea.parentElement,\n        doc: content,\n        extensions: [\n            ...editorExtensions(textArea.parentElement),\n            EditorView.updateListener.of(v => {\n                if (v.docChanged) {\n                    textArea.value = v.state.doc.toString();\n                }\n            }),\n        ],\n    };\n\n    // Create editor view, hide original input\n    const ev = createView('code-input', config);\n    const editor = new SimpleEditorInterface(ev);\n    editor.setMode(mode, content);\n    textArea.style.display = 'none';\n\n    return editor;\n}\n\n/**\n * Get a CodeMirror instance to use for the markdown editor.\n * @param {HTMLElement} elem\n * @param {function} onChange\n * @param {object} domEventHandlers\n * @param {Array} keyBindings\n * @returns {EditorView}\n */\nexport function markdownEditor(elem, onChange, domEventHandlers, keyBindings) {\n    const content = elem.textContent;\n    const config = {\n        parent: elem.parentElement,\n        doc: content,\n        extensions: [\n            keymap.of(keyBindings),\n            ...editorExtensions(elem.parentElement),\n            EditorView.updateListener.of(v => {\n                onChange(v);\n            }),\n            EditorView.domEventHandlers(domEventHandlers),\n        ],\n    };\n\n    // Emit a pre-event public event to allow tweaking of the configure before view creation.\n    window.$events.emitPublic(elem, 'editor-markdown-cm6::pre-init', {editorViewConfig: config});\n\n    // Create editor view, hide original input\n    const ev = createView('markdown-editor', config);\n    (new SimpleEditorInterface(ev)).setMode('markdown', '');\n    elem.style.display = 'none';\n\n    return ev;\n}\n"
  },
  {
    "path": "resources/js/code/languages.js",
    "content": "import {StreamLanguage} from '@codemirror/language';\n\nimport {css} from '@codemirror/lang-css';\nimport {json} from '@codemirror/lang-json';\nimport {javascript} from '@codemirror/lang-javascript';\nimport {html} from '@codemirror/lang-html';\nimport {markdown} from '@codemirror/lang-markdown';\nimport {php} from '@codemirror/lang-php';\nimport {twig} from '@ssddanbrown/codemirror-lang-twig';\nimport {xml} from '@codemirror/lang-xml';\n\nconst legacyLoad = async mode => {\n    const modes = await window.importVersioned('legacy-modes');\n    return StreamLanguage.define(modes[mode]);\n};\n\n// Mapping of possible languages or formats from user input to their codemirror modes.\n// Value can be a mode string or a function that will receive the code content & return the mode string.\n// The function option is used in the event the exact mode could be dynamic depending on the code.\nconst modeMap = {\n    bash: () => legacyLoad('shell'),\n    c: () => legacyLoad('c'),\n    css: async () => css(),\n    'c++': () => legacyLoad('cpp'),\n    'c#': () => legacyLoad('csharp'),\n    clj: () => legacyLoad('clojure'),\n    clojure: () => legacyLoad('clojure'),\n    csharp: () => legacyLoad('csharp'),\n    dart: () => legacyLoad('dart'),\n    diff: () => legacyLoad('diff'),\n    for: () => legacyLoad('fortran'),\n    fortran: () => legacyLoad('fortran'),\n    'f#': () => legacyLoad('fSharp'),\n    fsharp: () => legacyLoad('fSharp'),\n    go: () => legacyLoad('go'),\n    groovy: () => legacyLoad('groovy'),\n    haskell: () => legacyLoad('haskell'),\n    hs: () => legacyLoad('haskell'),\n    html: async () => html({selfClosingTags: true}),\n    ini: () => legacyLoad('properties'),\n    java: () => legacyLoad('java'),\n    javascript: async () => javascript(),\n    json: async () => json(),\n    js: async () => javascript(),\n    jl: () => legacyLoad('julia'),\n    julia: () => legacyLoad('julia'),\n    kotlin: () => legacyLoad('kotlin'),\n    latex: () => legacyLoad('stex'),\n    lua: () => legacyLoad('lua'),\n    markdown: async () => markdown(),\n    matlab: () => legacyLoad('octave'),\n    md: async () => markdown(),\n    mdown: async () => markdown(),\n    ml: () => legacyLoad('sml'),\n    mssql: () => legacyLoad('msSQL'),\n    mysql: () => legacyLoad('mySQL'),\n    nginx: () => legacyLoad('nginx'),\n    octave: () => legacyLoad('octave'),\n    pas: () => legacyLoad('pascal'),\n    pascal: () => legacyLoad('pascal'),\n    perl: () => legacyLoad('perl'),\n    pgsql: () => legacyLoad('pgSQL'),\n    php: async code => {\n        const hasTags = code.includes('<?php');\n        return php({plain: !hasTags});\n    },\n    pl: () => legacyLoad('perl'),\n    'pl/sql': () => legacyLoad('plSQL'),\n    postgresql: () => legacyLoad('pgSQL'),\n    powershell: () => legacyLoad('powerShell'),\n    properties: () => legacyLoad('properties'),\n    ocaml: () => legacyLoad('oCaml'),\n    py: () => legacyLoad('python'),\n    python: () => legacyLoad('python'),\n    r: () => legacyLoad('r'),\n    rb: () => legacyLoad('ruby'),\n    rs: () => legacyLoad('rust'),\n    ruby: () => legacyLoad('ruby'),\n    rust: () => legacyLoad('rust'),\n    sas: () => legacyLoad('sas'),\n    scala: () => legacyLoad('scala'),\n    scheme: () => legacyLoad('scheme'),\n    shell: () => legacyLoad('shell'),\n    sh: () => legacyLoad('shell'),\n    smarty: () => legacyLoad('smarty'),\n    stext: () => legacyLoad('stex'),\n    swift: () => legacyLoad('swift'),\n    toml: () => legacyLoad('toml'),\n    ts: async () => javascript({typescript: true}),\n    twig: async () => twig(),\n    typescript: async () => javascript({typescript: true}),\n    sql: () => legacyLoad('standardSQL'),\n    sqlite: () => legacyLoad('sqlite'),\n    vbs: () => legacyLoad('vbScript'),\n    vbscript: () => legacyLoad('vbScript'),\n    'vb.net': () => legacyLoad('vb'),\n    vbnet: () => legacyLoad('vb'),\n    xml: async () => xml(),\n    yaml: () => legacyLoad('yaml'),\n    yml: () => legacyLoad('yaml'),\n};\n\n/**\n * Get the relevant codemirror language extension based upon the given language\n * suggestion and content.\n * @param {String} langSuggestion\n * @param {String} content\n * @returns {Promise<StreamLanguage|LanguageSupport>}\n */\nexport function getLanguageExtension(langSuggestion, content) {\n    const suggestion = langSuggestion.trim().replace(/^\\./g, '').toLowerCase();\n\n    const language = modeMap[suggestion];\n\n    if (typeof language === 'undefined') {\n        return undefined;\n    }\n\n    return language(content);\n}\n"
  },
  {
    "path": "resources/js/code/legacy-modes.mjs",
    "content": "export {\n    c, cpp, csharp, java, kotlin, scala, dart,\n} from '@codemirror/legacy-modes/mode/clike';\nexport {clojure} from '@codemirror/legacy-modes/mode/clojure';\nexport {diff} from '@codemirror/legacy-modes/mode/diff';\nexport {fortran} from '@codemirror/legacy-modes/mode/fortran';\nexport {go} from '@codemirror/legacy-modes/mode/go';\nexport {groovy} from '@codemirror/legacy-modes/mode/groovy';\nexport {haxe} from '@codemirror/legacy-modes/mode/haxe';\nexport {haskell} from '@codemirror/legacy-modes/mode/haskell';\nexport {julia} from '@codemirror/legacy-modes/mode/julia';\nexport {lua} from '@codemirror/legacy-modes/mode/lua';\nexport {oCaml, fSharp, sml} from '@codemirror/legacy-modes/mode/mllike';\nexport {nginx} from '@codemirror/legacy-modes/mode/nginx';\nexport {octave} from '@codemirror/legacy-modes/mode/octave';\nexport {perl} from '@codemirror/legacy-modes/mode/perl';\nexport {pascal} from '@codemirror/legacy-modes/mode/pascal';\nexport {powerShell} from '@codemirror/legacy-modes/mode/powershell';\nexport {properties} from '@codemirror/legacy-modes/mode/properties';\nexport {python} from '@codemirror/legacy-modes/mode/python';\nexport {r} from '@codemirror/legacy-modes/mode/r';\nexport {ruby} from '@codemirror/legacy-modes/mode/ruby';\nexport {rust} from '@codemirror/legacy-modes/mode/rust';\nexport {sas} from '@codemirror/legacy-modes/mode/sas';\nexport {scheme} from '@codemirror/legacy-modes/mode/scheme';\nexport {shell} from '@codemirror/legacy-modes/mode/shell';\nexport {\n    standardSQL, pgSQL, msSQL, mySQL, sqlite, plSQL,\n} from '@codemirror/legacy-modes/mode/sql';\nexport {stex} from '@codemirror/legacy-modes/mode/stex';\nexport {swift} from '@codemirror/legacy-modes/mode/swift';\nexport {toml} from '@codemirror/legacy-modes/mode/toml';\nexport {vb} from '@codemirror/legacy-modes/mode/vb';\nexport {vbScript} from '@codemirror/legacy-modes/mode/vbscript';\nexport {yaml} from '@codemirror/legacy-modes/mode/yaml';\nexport {smarty} from '@ssddanbrown/codemirror-lang-smarty';\n"
  },
  {
    "path": "resources/js/code/setups.js",
    "content": "import {\n    EditorView, keymap, drawSelection, highlightActiveLine, dropCursor,\n    rectangularSelection, lineNumbers, highlightActiveLineGutter,\n} from '@codemirror/view';\nimport {bracketMatching} from '@codemirror/language';\nimport {\n    defaultKeymap, history, historyKeymap, indentWithTab,\n} from '@codemirror/commands';\nimport {Compartment, EditorState} from '@codemirror/state';\nimport {getTheme} from './themes';\n\n/**\n * @param {Element} parentEl\n * @return {(Extension[]|{extension: Extension}|readonly Extension[])[]}\n */\nfunction common(parentEl) {\n    return [\n        getTheme(parentEl),\n        lineNumbers(),\n        drawSelection(),\n        dropCursor(),\n        bracketMatching(),\n        rectangularSelection(),\n    ];\n}\n\n/**\n * @returns {({extension: Extension}|readonly Extension[])[]}\n */\nfunction getDynamicActiveLineHighlighter() {\n    const highlightingCompartment = new Compartment();\n    const domEvents = {\n        focus(event, view) {\n            view.dispatch({\n                effects: highlightingCompartment.reconfigure([\n                    highlightActiveLineGutter(),\n                    highlightActiveLine(),\n                ]),\n            });\n        },\n        blur(event, view) {\n            view.dispatch({\n                effects: highlightingCompartment.reconfigure([]),\n            });\n        },\n    };\n\n    return [\n        highlightingCompartment.of([]),\n        EditorView.domEventHandlers(domEvents),\n    ];\n}\n\n/**\n * @param {Element} parentEl\n * @return {*[]}\n */\nexport function viewerExtensions(parentEl) {\n    return [\n        ...common(parentEl),\n        getDynamicActiveLineHighlighter(),\n        keymap.of([\n            ...defaultKeymap,\n        ]),\n        EditorState.readOnly.of(true),\n    ];\n}\n\n/**\n * @param {Element} parentEl\n * @return {*[]}\n */\nexport function editorExtensions(parentEl) {\n    return [\n        ...common(parentEl),\n        highlightActiveLineGutter(),\n        highlightActiveLine(),\n        history(),\n        keymap.of([\n            ...defaultKeymap,\n            ...historyKeymap,\n            indentWithTab,\n        ]),\n        EditorView.lineWrapping,\n    ];\n}\n"
  },
  {
    "path": "resources/js/code/simple-editor-interface.js",
    "content": "import {updateViewLanguage} from './views';\n\nexport class SimpleEditorInterface {\n\n    /**\n     * @param {EditorView} editorView\n     */\n    constructor(editorView) {\n        this.ev = editorView;\n    }\n\n    /**\n     * Get the contents of an editor instance.\n     * @return {string}\n     */\n    getContent() {\n        return this.ev.state.doc.toString();\n    }\n\n    /**\n     * Set the contents of an editor instance.\n     * @param content\n     */\n    setContent(content) {\n        const {doc} = this.ev.state;\n        this.ev.dispatch({\n            changes: {from: 0, to: doc.length, insert: content},\n        });\n    }\n\n    /**\n     * Return focus to the editor instance.\n     */\n    focus() {\n        this.ev.focus();\n    }\n\n    /**\n     * Set the language mode of the editor instance.\n     * @param {String} mode\n     * @param {String} content\n     */\n    setMode(mode, content = '') {\n        updateViewLanguage(this.ev, mode, content);\n    }\n\n}\n"
  },
  {
    "path": "resources/js/code/themes.js",
    "content": "import {tags} from '@lezer/highlight';\nimport {HighlightStyle, syntaxHighlighting} from '@codemirror/language';\nimport {EditorView} from '@codemirror/view';\nimport {oneDarkHighlightStyle, oneDarkTheme} from '@codemirror/theme-one-dark';\n\nconst defaultLightHighlightStyle = HighlightStyle.define([\n    {\n        tag: tags.meta,\n        color: '#388938',\n    },\n    {\n        tag: tags.link,\n        textDecoration: 'underline',\n    },\n    {\n        tag: tags.heading,\n        textDecoration: 'underline',\n        fontWeight: 'bold',\n    },\n    {\n        tag: tags.emphasis,\n        fontStyle: 'italic',\n    },\n    {\n        tag: tags.strong,\n        fontWeight: 'bold',\n    },\n    {\n        tag: tags.strikethrough,\n        textDecoration: 'line-through',\n    },\n    {\n        tag: tags.keyword,\n        color: '#708',\n    },\n    {\n        tag: [tags.atom, tags.bool, tags.url, tags.contentSeparator, tags.labelName],\n        color: '#219',\n    },\n    {\n        tag: [tags.literal, tags.inserted],\n        color: '#164',\n    },\n    {\n        tag: [tags.string, tags.deleted],\n        color: '#a11',\n    },\n    {\n        tag: [tags.regexp, tags.escape, tags.special(tags.string)],\n        color: '#e40',\n    },\n    {\n        tag: tags.definition(tags.variableName),\n        color: '#00f',\n    },\n    {\n        tag: tags.local(tags.variableName),\n        color: '#30a',\n    },\n    {\n        tag: [tags.typeName, tags.namespace],\n        color: '#085',\n    },\n    {\n        tag: tags.className,\n        color: '#167',\n    },\n    {\n        tag: [tags.special(tags.variableName), tags.macroName],\n        color: '#256',\n    },\n    {\n        tag: tags.definition(tags.propertyName),\n        color: '#00c',\n    },\n    {\n        tag: tags.compareOperator,\n        color: '#708',\n    },\n    {\n        tag: tags.comment,\n        color: '#940',\n    },\n    {\n        tag: tags.invalid,\n        color: '#f00',\n    },\n]);\n\nconst defaultThemeSpec = {\n    '&': {\n        backgroundColor: '#FFF',\n        color: '#000',\n    },\n    '&.cm-focused': {\n        outline: 'none',\n    },\n    '.cm-line': {\n        lineHeight: '1.6',\n    },\n};\n\n/**\n * Get the theme extension to use for editor view instance.\n * @returns {Extension[]}\n */\nexport function getTheme(viewParentEl) {\n    const darkMode = document.documentElement.classList.contains('dark-mode');\n    let viewTheme = darkMode ? oneDarkTheme : EditorView.theme(defaultThemeSpec);\n    let highlightStyle = darkMode ? oneDarkHighlightStyle : defaultLightHighlightStyle;\n\n    const eventData = {\n        darkModeActive: darkMode,\n        registerViewTheme(builder) {\n            const spec = builder();\n            if (spec) {\n                viewTheme = EditorView.theme(spec);\n            }\n        },\n        registerHighlightStyle(builder) {\n            const tagStyles = builder(tags) || [];\n            if (tagStyles.length) {\n                highlightStyle = HighlightStyle.define(tagStyles);\n            }\n        },\n    };\n\n    window.$events.emitPublic(viewParentEl, 'library-cm6::configure-theme', eventData);\n\n    return [viewTheme, syntaxHighlighting(highlightStyle)];\n}\n"
  },
  {
    "path": "resources/js/code/views.js",
    "content": "import {Compartment, EditorState} from '@codemirror/state';\nimport {EditorView} from '@codemirror/view';\nimport {getLanguageExtension} from './languages';\n\nconst viewLangCompartments = new WeakMap();\n\n/**\n * Create a new editor view.\n *\n * @param {String} usageType\n * @param {{parent: Element, doc: String, extensions: Array}} config\n * @returns {EditorView}\n */\nexport function createView(usageType, config) {\n    const langCompartment = new Compartment();\n    config.extensions.push(langCompartment.of([]));\n\n    const commonEventData = {\n        usage: usageType,\n        editorViewConfig: config,\n        libEditorView: EditorView,\n        libEditorState: EditorState,\n        libCompartment: Compartment,\n    };\n\n    // Emit a pre-init public event so the user can tweak the config before instance creation\n    window.$events.emitPublic(config.parent, 'library-cm6::pre-init', commonEventData);\n\n    const ev = new EditorView(config);\n    viewLangCompartments.set(ev, langCompartment);\n\n    // Emit a post-init public event so the user can gain a reference to the EditorView\n    window.$events.emitPublic(config.parent, 'library-cm6::post-init', {editorView: ev, ...commonEventData});\n\n    return ev;\n}\n\n/**\n * Set the language mode of an EditorView.\n *\n * @param {EditorView} ev\n * @param {string} modeSuggestion\n * @param {string} content\n */\nexport async function updateViewLanguage(ev, modeSuggestion, content) {\n    const compartment = viewLangCompartments.get(ev);\n    const language = await getLanguageExtension(modeSuggestion, content);\n\n    ev.dispatch({\n        effects: compartment.reconfigure(language || []),\n    });\n}\n"
  },
  {
    "path": "resources/js/components/add-remove-rows.js",
    "content": "import {onChildEvent} from '../services/dom.ts';\nimport {uniqueId} from '../services/util.ts';\nimport {Component} from './component';\n\n/**\n * AddRemoveRows\n * Allows easy row add/remove controls onto a table.\n * Needs a model row to use when adding a new row.\n */\nexport class AddRemoveRows extends Component {\n\n    setup() {\n        this.modelRow = this.$refs.model;\n        this.addButton = this.$refs.add;\n        this.removeSelector = this.$opts.removeSelector;\n        this.rowSelector = this.$opts.rowSelector;\n        this.setupListeners();\n    }\n\n    setupListeners() {\n        this.addButton.addEventListener('click', this.add.bind(this));\n\n        onChildEvent(this.$el, this.removeSelector, 'click', e => {\n            const row = e.target.closest(this.rowSelector);\n            row.remove();\n        });\n    }\n\n    // For external use\n    add() {\n        const clone = this.modelRow.cloneNode(true);\n        clone.classList.remove('hidden');\n        this.setClonedInputNames(clone);\n        this.modelRow.parentNode.insertBefore(clone, this.modelRow);\n        window.$components.init(clone);\n    }\n\n    /**\n     * Update the HTML names of a clone to be unique if required.\n     * Names can use placeholder values. For exmaple, a model row\n     * may have name=\"tags[randrowid][name]\".\n     * These are the available placeholder values:\n     * - randrowid - An random string ID, applied the same across the row.\n     * @param {HTMLElement} clone\n     */\n    setClonedInputNames(clone) {\n        const rowId = uniqueId();\n        const randRowIdElems = clone.querySelectorAll('[name*=\"randrowid\"]');\n        for (const elem of randRowIdElems) {\n            elem.name = elem.name.split('randrowid').join(rowId);\n        }\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/ajax-delete-row.ts",
    "content": "import {onSelect} from '../services/dom';\nimport {Component} from './component';\n\nexport class AjaxDeleteRow extends Component {\n\n    protected row!: HTMLElement;\n    protected url!: string;\n    protected deleteButtons: HTMLElement[] = [];\n\n    setup() {\n        this.row = this.$el;\n        this.url = this.$opts.url;\n        this.deleteButtons = this.$manyRefs.delete || [];\n\n        onSelect(this.deleteButtons, this.runDelete.bind(this));\n    }\n\n    runDelete() {\n        this.row.style.opacity = '0.7';\n        this.row.style.pointerEvents = 'none';\n\n        window.$http.delete(this.url).then(resp => {\n            if (typeof resp.data === 'object' && resp.data.message) {\n                window.$events.emit('success', resp.data.message);\n            }\n            this.row.remove();\n        }).catch(() => {\n            this.row.style.removeProperty('opacity');\n            this.row.style.removeProperty('pointer-events');\n        });\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/ajax-form.js",
    "content": "import {onEnterPress, onSelect} from '../services/dom.ts';\nimport {Component} from './component';\n\n/**\n * Ajax Form\n * Will handle button clicks or input enter press events and submit\n * the data over ajax. Will always expect a partial HTML view to be returned.\n * Fires an 'ajax-form-success' event when submitted successfully.\n *\n * Will handle a real form if that's what the component is added to\n * otherwise will act as a fake form element.\n */\nexport class AjaxForm extends Component {\n\n    setup() {\n        this.container = this.$el;\n        this.responseContainer = this.container;\n        this.url = this.$opts.url;\n        this.method = this.$opts.method || 'post';\n        this.successMessage = this.$opts.successMessage;\n        this.submitButtons = this.$manyRefs.submit || [];\n\n        if (this.$opts.responseContainer) {\n            this.responseContainer = this.container.closest(this.$opts.responseContainer);\n        }\n\n        this.setupListeners();\n    }\n\n    setupListeners() {\n        if (this.container.tagName === 'FORM') {\n            this.container.addEventListener('submit', this.submitRealForm.bind(this));\n            return;\n        }\n\n        onEnterPress(this.container, event => {\n            this.submitFakeForm();\n            event.preventDefault();\n        });\n\n        this.submitButtons.forEach(button => onSelect(button, this.submitFakeForm.bind(this)));\n    }\n\n    submitFakeForm() {\n        const fd = new FormData();\n        const inputs = this.container.querySelectorAll('[name]');\n        for (const input of inputs) {\n            fd.append(input.getAttribute('name'), input.value);\n        }\n        this.submit(fd);\n    }\n\n    submitRealForm(event) {\n        event.preventDefault();\n        const fd = new FormData(this.container);\n        this.submit(fd);\n    }\n\n    async submit(formData) {\n        this.responseContainer.style.opacity = '0.7';\n        this.responseContainer.style.pointerEvents = 'none';\n\n        try {\n            const resp = await window.$http[this.method.toLowerCase()](this.url, formData);\n            this.$emit('success', {formData});\n            this.responseContainer.innerHTML = resp.data;\n            if (this.successMessage) {\n                window.$events.emit('success', this.successMessage);\n            }\n        } catch (err) {\n            this.responseContainer.innerHTML = err.data;\n        }\n\n        window.$components.init(this.responseContainer);\n        this.responseContainer.style.opacity = null;\n        this.responseContainer.style.pointerEvents = null;\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/api-nav.ts",
    "content": "import {Component} from \"./component\";\n\nexport class ApiNav extends Component {\n    private select!: HTMLSelectElement;\n    private sidebar!: HTMLElement;\n    private body!: HTMLElement;\n\n    setup() {\n        this.select = this.$refs.select as HTMLSelectElement;\n        this.sidebar = this.$refs.sidebar;\n        this.body = this.$el.ownerDocument.documentElement;\n        this.select.addEventListener('change', () => {\n            const section = this.select.value;\n            const sidebarTarget = document.getElementById(`sidebar-header-${section}`);\n            const contentTarget = document.getElementById(`section-${section}`);\n            if (sidebarTarget && contentTarget) {\n\n                const sidebarPos = sidebarTarget.getBoundingClientRect().top - this.sidebar.getBoundingClientRect().top + this.sidebar.scrollTop;\n                this.sidebar.scrollTo({\n                    top: sidebarPos - 120,\n                    behavior: 'smooth',\n                });\n\n                const bodyPos = contentTarget.getBoundingClientRect().top + this.body.scrollTop;\n                this.body.scrollTo({\n                    top: bodyPos - 20,\n                    behavior: 'smooth',\n                });\n            }\n        });\n    }\n}"
  },
  {
    "path": "resources/js/components/attachments-list.js",
    "content": "import {Component} from './component';\n\n/**\n * Attachments List\n * Adds '?open=true' query to file attachment links\n * when ctrl/cmd is pressed down.\n */\nexport class AttachmentsList extends Component {\n\n    setup() {\n        this.container = this.$el;\n        this.fileLinks = this.$manyRefs.linkTypeFile;\n\n        this.setupListeners();\n    }\n\n    setupListeners() {\n        const isExpectedKey = event => event.key === 'Control' || event.key === 'Meta';\n        window.addEventListener('keydown', event => {\n            if (isExpectedKey(event)) {\n                this.addOpenQueryToLinks();\n            }\n        }, {passive: true});\n        window.addEventListener('keyup', event => {\n            if (isExpectedKey(event)) {\n                this.removeOpenQueryFromLinks();\n            }\n        }, {passive: true});\n    }\n\n    addOpenQueryToLinks() {\n        for (const link of this.fileLinks) {\n            if (link.href.split('?')[1] !== 'open=true') {\n                link.href += '?open=true';\n                link.setAttribute('target', '_blank');\n            }\n        }\n    }\n\n    removeOpenQueryFromLinks() {\n        for (const link of this.fileLinks) {\n            link.href = link.href.split('?')[0];\n            link.removeAttribute('target');\n        }\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/attachments.js",
    "content": "import {showLoading} from '../services/dom.ts';\nimport {Component} from './component';\n\nexport class Attachments extends Component {\n\n    setup() {\n        this.container = this.$el;\n        this.pageId = this.$opts.pageId;\n        this.editContainer = this.$refs.editContainer;\n        this.listContainer = this.$refs.listContainer;\n        this.linksContainer = this.$refs.linksContainer;\n        this.listPanel = this.$refs.listPanel;\n        this.attachLinkButton = this.$refs.attachLinkButton;\n\n        this.setupListeners();\n    }\n\n    setupListeners() {\n        const reloadListBound = this.reloadList.bind(this);\n        this.container.addEventListener('dropzone-upload-success', reloadListBound);\n        this.container.addEventListener('ajax-form-success', reloadListBound);\n\n        this.container.addEventListener('sortable-list-sort', event => {\n            this.updateOrder(event.detail.ids);\n        });\n\n        this.container.addEventListener('event-emit-select-edit', event => {\n            this.startEdit(event.detail.id);\n        });\n\n        this.container.addEventListener('event-emit-select-edit-back', () => {\n            this.stopEdit();\n        });\n\n        this.container.addEventListener('event-emit-select-insert', event => {\n            const insertContent = event.target.closest('[data-drag-content]').getAttribute('data-drag-content');\n            const contentTypes = JSON.parse(insertContent);\n            window.$events.emit('editor::insert', {\n                html: contentTypes['text/html'],\n                markdown: contentTypes['text/plain'],\n            });\n        });\n\n        this.attachLinkButton.addEventListener('click', () => {\n            this.showSection('links');\n        });\n    }\n\n    showSection(section) {\n        const sectionMap = {\n            links: this.linksContainer,\n            edit: this.editContainer,\n            list: this.listContainer,\n        };\n\n        for (const [name, elem] of Object.entries(sectionMap)) {\n            elem.toggleAttribute('hidden', name !== section);\n        }\n    }\n\n    reloadList() {\n        this.stopEdit();\n        window.$http.get(`/attachments/get/page/${this.pageId}`).then(resp => {\n            this.listPanel.innerHTML = resp.data;\n            window.$components.init(this.listPanel);\n        });\n    }\n\n    updateOrder(idOrder) {\n        window.$http.put(`/attachments/sort/page/${this.pageId}`, {order: idOrder}).then(resp => {\n            window.$events.emit('success', resp.data.message);\n        });\n    }\n\n    async startEdit(id) {\n        this.showSection('edit');\n\n        showLoading(this.editContainer);\n        const resp = await window.$http.get(`/attachments/edit/${id}`);\n        this.editContainer.innerHTML = resp.data;\n        window.$components.init(this.editContainer);\n    }\n\n    stopEdit() {\n        this.showSection('list');\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/auto-submit.js",
    "content": "import {Component} from './component';\n\nexport class AutoSubmit extends Component {\n\n    setup() {\n        this.form = this.$el;\n\n        this.form.submit();\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/auto-suggest.js",
    "content": "import {escapeHtml} from '../services/util.ts';\nimport {onChildEvent} from '../services/dom.ts';\nimport {Component} from './component';\nimport {KeyboardNavigationHandler} from '../services/keyboard-navigation.ts';\n\nconst ajaxCache = {};\n\n/**\n * AutoSuggest\n */\nexport class AutoSuggest extends Component {\n\n    setup() {\n        this.parent = this.$el.parentElement;\n        this.container = this.$el;\n        this.type = this.$opts.type;\n        this.url = this.$opts.url;\n        this.input = this.$refs.input;\n        this.list = this.$refs.list;\n\n        this.lastPopulated = 0;\n        this.setupListeners();\n    }\n\n    setupListeners() {\n        const navHandler = new KeyboardNavigationHandler(\n            this.list,\n            () => {\n                this.input.focus();\n                setTimeout(() => this.hideSuggestions(), 1);\n            },\n            event => {\n                event.preventDefault();\n                const selectionValue = event.target.textContent;\n                if (selectionValue) {\n                    this.selectSuggestion(selectionValue);\n                }\n            },\n        );\n        navHandler.shareHandlingToEl(this.input);\n\n        onChildEvent(this.list, '.text-item', 'click', (event, el) => {\n            this.selectSuggestion(el.textContent);\n        });\n\n        this.input.addEventListener('input', this.requestSuggestions.bind(this));\n        this.input.addEventListener('focus', this.requestSuggestions.bind(this));\n        this.input.addEventListener('blur', this.hideSuggestionsIfFocusedLost.bind(this));\n        this.input.addEventListener('keydown', event => {\n            if (event.key === 'Tab') {\n                this.hideSuggestions();\n            }\n        });\n    }\n\n    selectSuggestion(value) {\n        this.input.value = value;\n        this.lastPopulated = Date.now();\n        this.input.focus();\n        this.input.dispatchEvent(new Event('input', {bubbles: true}));\n        this.input.dispatchEvent(new Event('change', {bubbles: true}));\n        this.hideSuggestions();\n    }\n\n    async requestSuggestions() {\n        if (Date.now() - this.lastPopulated < 50) {\n            return;\n        }\n\n        const nameFilter = this.getNameFilterIfNeeded();\n        const search = this.input.value.toLowerCase();\n        const suggestions = await this.loadSuggestions(search, nameFilter);\n\n        const toShow = suggestions.filter(val => search === '' || val.toLowerCase().startsWith(search)).slice(0, 10);\n\n        this.displaySuggestions(toShow);\n    }\n\n    getNameFilterIfNeeded() {\n        if (this.type !== 'value') return null;\n        return this.parent.querySelector('input').value;\n    }\n\n    /**\n     * @param {String} search\n     * @param {String|null} nameFilter\n     * @returns {Promise<Object|String|*>}\n     */\n    async loadSuggestions(search, nameFilter = null) {\n        // Truncate search to prevent over numerous lookups\n        search = search.slice(0, 4);\n\n        const params = {search, name: nameFilter};\n        const cacheKey = `${this.url}:${JSON.stringify(params)}`;\n\n        if (ajaxCache[cacheKey]) {\n            return ajaxCache[cacheKey];\n        }\n\n        const resp = await window.$http.get(this.url, params);\n        ajaxCache[cacheKey] = resp.data;\n        return resp.data;\n    }\n\n    /**\n     * @param {String[]} suggestions\n     */\n    displaySuggestions(suggestions) {\n        if (suggestions.length === 0) {\n            this.hideSuggestions();\n            return;\n        }\n\n        // This used to use <button>s but was changed to div elements since Safari would not focus on buttons\n        // on which causes a range of other complexities related to focus handling.\n        this.list.innerHTML = suggestions.map(value => `<li><div tabindex=\"0\" class=\"text-item\">${escapeHtml(value)}</div></li>`).join('');\n        this.list.style.display = 'block';\n        for (const button of this.list.querySelectorAll('.text-item')) {\n            button.addEventListener('blur', this.hideSuggestionsIfFocusedLost.bind(this));\n        }\n    }\n\n    hideSuggestions() {\n        this.list.style.display = 'none';\n    }\n\n    hideSuggestionsIfFocusedLost(event) {\n        if (!this.container.contains(event.relatedTarget)) {\n            this.hideSuggestions();\n        }\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/back-to-top.js",
    "content": "import {Component} from './component';\n\nexport class BackToTop extends Component {\n\n    setup() {\n        this.button = this.$el;\n        this.targetElem = document.getElementById('header');\n        this.showing = false;\n        this.breakPoint = 1200;\n\n        if (document.body.classList.contains('flexbox')) {\n            this.button.style.display = 'none';\n            return;\n        }\n\n        this.button.addEventListener('click', this.scrollToTop.bind(this));\n        window.addEventListener('scroll', this.onPageScroll.bind(this));\n    }\n\n    onPageScroll() {\n        const scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;\n        if (!this.showing && scrollTopPos > this.breakPoint) {\n            this.button.style.display = 'block';\n            this.showing = true;\n            setTimeout(() => {\n                this.button.style.opacity = 0.4;\n            }, 1);\n        } else if (this.showing && scrollTopPos < this.breakPoint) {\n            this.button.style.opacity = 0;\n            this.showing = false;\n            setTimeout(() => {\n                this.button.style.display = 'none';\n            }, 500);\n        }\n    }\n\n    scrollToTop() {\n        const targetTop = this.targetElem.getBoundingClientRect().top;\n        const scrollElem = document.documentElement.scrollTop ? document.documentElement : document.body;\n        const duration = 300;\n        const start = Date.now();\n        const scrollStart = this.targetElem.getBoundingClientRect().top;\n\n        function setPos() {\n            const percentComplete = (1 - ((Date.now() - start) / duration));\n            const target = Math.abs(percentComplete * scrollStart);\n            if (percentComplete > 0) {\n                scrollElem.scrollTop = target;\n                requestAnimationFrame(setPos.bind(this));\n            } else {\n                scrollElem.scrollTop = targetTop;\n            }\n        }\n\n        requestAnimationFrame(setPos.bind(this));\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/book-sort.js",
    "content": "import Sortable, {MultiDrag} from 'sortablejs';\nimport {Component} from './component';\nimport {htmlToDom} from '../services/dom.ts';\n\n// Auto sort control\nconst sortOperations = {\n    name(a, b) {\n        const aName = a.getAttribute('data-name').trim().toLowerCase();\n        const bName = b.getAttribute('data-name').trim().toLowerCase();\n        return aName.localeCompare(bName);\n    },\n    created(a, b) {\n        const aTime = Number(a.getAttribute('data-created'));\n        const bTime = Number(b.getAttribute('data-created'));\n        return bTime - aTime;\n    },\n    updated(a, b) {\n        const aTime = Number(a.getAttribute('data-updated'));\n        const bTime = Number(b.getAttribute('data-updated'));\n        return bTime - aTime;\n    },\n    chaptersFirst(a, b) {\n        const aType = a.getAttribute('data-type');\n        const bType = b.getAttribute('data-type');\n        if (aType === bType) {\n            return 0;\n        }\n        return (aType === 'chapter' ? -1 : 1);\n    },\n    chaptersLast(a, b) {\n        const aType = a.getAttribute('data-type');\n        const bType = b.getAttribute('data-type');\n        if (aType === bType) {\n            return 0;\n        }\n        return (aType === 'chapter' ? 1 : -1);\n    },\n};\n\n/**\n * The available move actions.\n * The active function indicates if the action is possible for the given item.\n * The run function performs the move.\n * @type {{up: {active(Element, ?Element, Element): boolean, run(Element, ?Element, Element)}}}\n */\nconst moveActions = {\n    up: {\n        active(elem, parent) {\n            return !(elem.previousElementSibling === null && !parent);\n        },\n        run(elem, parent) {\n            const newSibling = elem.previousElementSibling || parent;\n            newSibling.insertAdjacentElement('beforebegin', elem);\n        },\n    },\n    down: {\n        active(elem, parent) {\n            return !(elem.nextElementSibling === null && !parent);\n        },\n        run(elem, parent) {\n            const newSibling = elem.nextElementSibling || parent;\n            newSibling.insertAdjacentElement('afterend', elem);\n        },\n    },\n    next_book: {\n        active(elem, parent, book) {\n            return book.nextElementSibling !== null;\n        },\n        run(elem, parent, book) {\n            const newList = book.nextElementSibling.querySelector('ul');\n            newList.prepend(elem);\n        },\n    },\n    prev_book: {\n        active(elem, parent, book) {\n            return book.previousElementSibling !== null;\n        },\n        run(elem, parent, book) {\n            const newList = book.previousElementSibling.querySelector('ul');\n            newList.appendChild(elem);\n        },\n    },\n    next_chapter: {\n        active(elem, parent) {\n            return elem.dataset.type === 'page' && this.getNextChapter(elem, parent);\n        },\n        run(elem, parent) {\n            const nextChapter = this.getNextChapter(elem, parent);\n            nextChapter.querySelector('ul').prepend(elem);\n        },\n        getNextChapter(elem, parent) {\n            const topLevel = (parent || elem);\n            const topItems = Array.from(topLevel.parentElement.children);\n            const index = topItems.indexOf(topLevel);\n            return topItems.slice(index + 1).find(item => item.dataset.type === 'chapter');\n        },\n    },\n    prev_chapter: {\n        active(elem, parent) {\n            return elem.dataset.type === 'page' && this.getPrevChapter(elem, parent);\n        },\n        run(elem, parent) {\n            const prevChapter = this.getPrevChapter(elem, parent);\n            prevChapter.querySelector('ul').append(elem);\n        },\n        getPrevChapter(elem, parent) {\n            const topLevel = (parent || elem);\n            const topItems = Array.from(topLevel.parentElement.children);\n            const index = topItems.indexOf(topLevel);\n            return topItems.slice(0, index).reverse().find(item => item.dataset.type === 'chapter');\n        },\n    },\n    book_end: {\n        active(elem, parent) {\n            return parent || (parent === null && elem.nextElementSibling);\n        },\n        run(elem, parent, book) {\n            book.querySelector('ul').append(elem);\n        },\n    },\n    book_start: {\n        active(elem, parent) {\n            return parent || (parent === null && elem.previousElementSibling);\n        },\n        run(elem, parent, book) {\n            book.querySelector('ul').prepend(elem);\n        },\n    },\n    before_chapter: {\n        active(elem, parent) {\n            return parent;\n        },\n        run(elem, parent) {\n            parent.insertAdjacentElement('beforebegin', elem);\n        },\n    },\n    after_chapter: {\n        active(elem, parent) {\n            return parent;\n        },\n        run(elem, parent) {\n            parent.insertAdjacentElement('afterend', elem);\n        },\n    },\n};\n\nexport class BookSort extends Component {\n\n    setup() {\n        this.container = this.$el;\n        this.sortContainer = this.$refs.sortContainer;\n        this.input = this.$refs.input;\n\n        Sortable.mount(new MultiDrag());\n\n        const initialSortBox = this.container.querySelector('.sort-box');\n        this.setupBookSortable(initialSortBox);\n        this.setupSortPresets();\n        this.setupMoveActions();\n\n        window.$events.listen('entity-select-change', this.bookSelect.bind(this));\n    }\n\n    /**\n     * Set up the handlers for the item-level move buttons.\n     */\n    setupMoveActions() {\n        // Handle move button click\n        this.container.addEventListener('click', event => {\n            if (event.target.matches('[data-move]')) {\n                const action = event.target.getAttribute('data-move');\n                const sortItem = event.target.closest('[data-id]');\n                this.runSortAction(sortItem, action);\n            }\n        });\n\n        this.updateMoveActionStateForAll();\n    }\n\n    /**\n     * Set up the handlers for the preset sort type buttons.\n     */\n    setupSortPresets() {\n        let lastSort = '';\n        let reverse = false;\n        const reversibleTypes = ['name', 'created', 'updated'];\n\n        this.sortContainer.addEventListener('click', event => {\n            const sortButton = event.target.closest('.sort-box-options [data-sort]');\n            if (!sortButton) return;\n\n            event.preventDefault();\n            const sortLists = sortButton.closest('.sort-box').querySelectorAll('ul');\n            const sort = sortButton.getAttribute('data-sort');\n\n            reverse = (lastSort === sort) ? !reverse : false;\n            let sortFunction = sortOperations[sort];\n            if (reverse && reversibleTypes.includes(sort)) {\n                sortFunction = function reverseSortOperation(a, b) {\n                    return 0 - sortOperations[sort](a, b);\n                };\n            }\n\n            for (const list of sortLists) {\n                const directItems = Array.from(list.children).filter(child => child.matches('li'));\n                directItems.sort(sortFunction).forEach(sortedItem => {\n                    list.appendChild(sortedItem);\n                });\n            }\n\n            lastSort = sort;\n            this.updateMapInput();\n        });\n    }\n\n    /**\n     * Handle book selection from the entity selector.\n     * @param {Object} entityInfo\n     */\n    bookSelect(entityInfo) {\n        const alreadyAdded = this.container.querySelector(`[data-type=\"book\"][data-id=\"${entityInfo.id}\"]`) !== null;\n        if (alreadyAdded) return;\n\n        const entitySortItemUrl = `${entityInfo.link}/sort-item`;\n        window.$http.get(entitySortItemUrl).then(resp => {\n            const newBookContainer = htmlToDom(resp.data);\n            this.sortContainer.append(newBookContainer);\n            this.setupBookSortable(newBookContainer);\n            this.updateMoveActionStateForAll();\n\n            const summary = newBookContainer.querySelector('summary');\n            summary.focus();\n        });\n    }\n\n    /**\n     * Set up the given book container element to have sortable items.\n     * @param {Element} bookContainer\n     */\n    setupBookSortable(bookContainer) {\n        const sortElems = Array.from(bookContainer.querySelectorAll('.sort-list, .sortable-page-sublist'));\n\n        const bookGroupConfig = {\n            name: 'book',\n            pull: ['book', 'chapter'],\n            put: ['book', 'chapter'],\n        };\n\n        const chapterGroupConfig = {\n            name: 'chapter',\n            pull: ['book', 'chapter'],\n            put(toList, fromList, draggedElem) {\n                return draggedElem.getAttribute('data-type') === 'page';\n            },\n        };\n\n        for (const sortElem of sortElems) {\n            Sortable.create(sortElem, {\n                group: sortElem.classList.contains('sort-list') ? bookGroupConfig : chapterGroupConfig,\n                animation: 150,\n                fallbackOnBody: true,\n                swapThreshold: 0.65,\n                onSort: () => {\n                    this.ensureNoNestedChapters();\n                    this.updateMapInput();\n                    this.updateMoveActionStateForAll();\n                },\n                dragClass: 'bg-white',\n                ghostClass: 'primary-background-light',\n                multiDrag: true,\n                multiDragKey: 'Control',\n                selectedClass: 'sortable-selected',\n            });\n        }\n    }\n\n    /**\n     * Handle nested chapters by moving them to the parent book.\n     * Needed since sorting with multi-sort only checks group rules based on the active item,\n     * not all in group, therefore need to manually check after a sort.\n     * Must be done before updating the map input.\n     */\n    ensureNoNestedChapters() {\n        const nestedChapters = this.container.querySelectorAll('[data-type=\"chapter\"] [data-type=\"chapter\"]');\n        for (const chapter of nestedChapters) {\n            const parentChapter = chapter.parentElement.closest('[data-type=\"chapter\"]');\n            parentChapter.insertAdjacentElement('afterend', chapter);\n        }\n    }\n\n    /**\n     * Update the input with our sort data.\n     */\n    updateMapInput() {\n        const pageMap = this.buildEntityMap();\n        this.input.value = JSON.stringify(pageMap);\n    }\n\n    /**\n     * Build up a mapping of entities with their ordering and nesting.\n     * @returns {Array}\n     */\n    buildEntityMap() {\n        const entityMap = [];\n        const lists = this.container.querySelectorAll('.sort-list');\n\n        for (const list of lists) {\n            const bookId = list.closest('[data-type=\"book\"]').getAttribute('data-id');\n            const directChildren = Array.from(list.children)\n                .filter(elem => elem.matches('[data-type=\"page\"], [data-type=\"chapter\"]'));\n            for (let i = 0; i < directChildren.length; i++) {\n                this.addBookChildToMap(directChildren[i], i, bookId, entityMap);\n            }\n        }\n\n        return entityMap;\n    }\n\n    /**\n     * Parse a sort item and add it to a data-map array.\n     * Parses sub0items if existing also.\n     * @param {Element} childElem\n     * @param {Number} index\n     * @param {Number} bookId\n     * @param {Array} entityMap\n     */\n    addBookChildToMap(childElem, index, bookId, entityMap) {\n        const type = childElem.getAttribute('data-type');\n        const parentChapter = false;\n        const childId = childElem.getAttribute('data-id');\n\n        entityMap.push({\n            id: childId,\n            sort: index,\n            parentChapter,\n            type,\n            book: bookId,\n        });\n\n        const subPages = childElem.querySelectorAll('[data-type=\"page\"]');\n        for (let i = 0; i < subPages.length; i++) {\n            entityMap.push({\n                id: subPages[i].getAttribute('data-id'),\n                sort: i,\n                parentChapter: childId,\n                type: 'page',\n                book: bookId,\n            });\n        }\n    }\n\n    /**\n     * Run the given sort action up the provided sort item.\n     * @param {Element} item\n     * @param {String} action\n     */\n    runSortAction(item, action) {\n        const parentItem = item.parentElement.closest('li[data-id]');\n        const parentBook = item.parentElement.closest('[data-type=\"book\"]');\n        moveActions[action].run(item, parentItem, parentBook);\n        this.updateMapInput();\n        this.updateMoveActionStateForAll();\n        item.scrollIntoView({behavior: 'smooth', block: 'nearest'});\n        item.focus();\n    }\n\n    /**\n     * Update the state of the available move actions on this item.\n     * @param {Element} item\n     */\n    updateMoveActionState(item) {\n        const parentItem = item.parentElement.closest('li[data-id]');\n        const parentBook = item.parentElement.closest('[data-type=\"book\"]');\n        for (const [action, functions] of Object.entries(moveActions)) {\n            const moveButton = item.querySelector(`[data-move=\"${action}\"]`);\n            moveButton.disabled = !functions.active(item, parentItem, parentBook);\n        }\n    }\n\n    updateMoveActionStateForAll() {\n        const items = this.container.querySelectorAll('[data-type=\"chapter\"],[data-type=\"page\"]');\n        for (const item of items) {\n            this.updateMoveActionState(item);\n        }\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/chapter-contents.js",
    "content": "import {slideUp, slideDown} from '../services/animations.ts';\nimport {Component} from './component';\n\nexport class ChapterContents extends Component {\n\n    setup() {\n        this.list = this.$refs.list;\n        this.toggle = this.$refs.toggle;\n\n        this.isOpen = this.toggle.classList.contains('open');\n        this.toggle.addEventListener('click', this.click.bind(this));\n    }\n\n    open() {\n        this.toggle.classList.add('open');\n        this.toggle.setAttribute('aria-expanded', 'true');\n        slideDown(this.list, 180);\n        this.isOpen = true;\n    }\n\n    close() {\n        this.toggle.classList.remove('open');\n        this.toggle.setAttribute('aria-expanded', 'false');\n        slideUp(this.list, 180);\n        this.isOpen = false;\n    }\n\n    click(event) {\n        event.preventDefault();\n        if (this.isOpen) {\n            this.close();\n        } else {\n            this.open();\n        }\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/code-editor.js",
    "content": "import {onChildEvent, onEnterPress, onSelect} from '../services/dom.ts';\nimport {Component} from './component';\n\nexport class CodeEditor extends Component {\n\n    /**\n     * @type {null|SimpleEditorInterface}\n     */\n    editor = null;\n\n    /**\n     * @type {?Function}\n     */\n    saveCallback = null;\n\n    /**\n     * @type {?Function}\n     */\n    cancelCallback = null;\n\n    history = {};\n\n    historyKey = 'code_history';\n\n    setup() {\n        this.container = this.$refs.container;\n        this.popup = this.$el;\n        this.editorInput = this.$refs.editor;\n        this.languageButtons = this.$manyRefs.languageButton;\n        this.languageOptionsContainer = this.$refs.languageOptionsContainer;\n        this.saveButton = this.$refs.saveButton;\n        this.languageInput = this.$refs.languageInput;\n        this.historyDropDown = this.$refs.historyDropDown;\n        this.historyList = this.$refs.historyList;\n        this.favourites = new Set(this.$opts.favourites.split(','));\n\n        this.setupListeners();\n        this.setupFavourites();\n    }\n\n    setupListeners() {\n        this.container.addEventListener('keydown', event => {\n            if (event.ctrlKey && event.key === 'Enter') {\n                this.save();\n            }\n        });\n\n        onSelect(this.languageButtons, event => {\n            const language = event.target.dataset.lang;\n            this.languageInput.value = language;\n            this.languageInputChange(language);\n        });\n\n        onEnterPress(this.languageInput, () => this.save());\n        this.languageInput.addEventListener('input', () => this.languageInputChange(this.languageInput.value));\n        onSelect(this.saveButton, () => this.save());\n\n        onChildEvent(this.historyList, 'button', 'click', (event, elem) => {\n            event.preventDefault();\n            const historyTime = elem.dataset.time;\n            if (this.editor) {\n                this.editor.setContent(this.history[historyTime]);\n            }\n        });\n    }\n\n    setupFavourites() {\n        for (const button of this.languageButtons) {\n            this.setupFavouritesForButton(button);\n        }\n\n        this.sortLanguageList();\n    }\n\n    /**\n     * @param {HTMLButtonElement} button\n     */\n    setupFavouritesForButton(button) {\n        const language = button.dataset.lang;\n        let isFavorite = this.favourites.has(language);\n        button.setAttribute('data-favourite', isFavorite ? 'true' : 'false');\n\n        onChildEvent(button.parentElement, '.lang-option-favorite-toggle', 'click', () => {\n            isFavorite = !isFavorite;\n\n            if (isFavorite) {\n                this.favourites.add(language);\n            } else {\n                this.favourites.delete(language);\n            }\n\n            button.setAttribute('data-favourite', isFavorite ? 'true' : 'false');\n\n            window.$http.patch('/preferences/update-code-language-favourite', {\n                language,\n                active: isFavorite,\n            });\n\n            this.sortLanguageList();\n            if (isFavorite) {\n                button.scrollIntoView({block: 'center', behavior: 'smooth'});\n            }\n        });\n    }\n\n    sortLanguageList() {\n        const sortedParents = this.languageButtons.sort((a, b) => {\n            const aFav = a.dataset.favourite === 'true';\n            const bFav = b.dataset.favourite === 'true';\n\n            if (aFav && !bFav) {\n                return -1;\n            } if (bFav && !aFav) {\n                return 1;\n            }\n\n            return a.dataset.lang > b.dataset.lang ? 1 : -1;\n        }).map(button => button.parentElement);\n\n        for (const parent of sortedParents) {\n            this.languageOptionsContainer.append(parent);\n        }\n    }\n\n    save() {\n        if (this.saveCallback) {\n            this.saveCallback(this.editor.getContent(), this.languageInput.value);\n        }\n        this.hide();\n    }\n\n    async open(code, language, direction, saveCallback, cancelCallback) {\n        this.languageInput.value = language;\n        this.saveCallback = saveCallback;\n        this.cancelCallback = cancelCallback;\n\n        await this.show();\n        this.languageInputChange(language);\n        this.editor.setContent(code);\n        this.setDirection(direction);\n    }\n\n    async show() {\n        const Code = await window.importVersioned('code');\n        if (!this.editor) {\n            this.editor = Code.popupEditor(this.editorInput, this.languageInput.value);\n        }\n\n        this.loadHistory();\n        this.getPopup().show(() => {\n            this.editor.focus();\n        }, () => {\n            this.addHistory();\n            if (this.cancelCallback) {\n                this.cancelCallback();\n            }\n        });\n    }\n\n    setDirection(direction) {\n        const target = this.editorInput.parentElement;\n        if (direction) {\n            target.setAttribute('dir', direction);\n        } else {\n            target.removeAttribute('dir');\n        }\n    }\n\n    hide() {\n        this.getPopup().hide();\n        this.addHistory();\n    }\n\n    /**\n     * @returns {Popup}\n     */\n    getPopup() {\n        return window.$components.firstOnElement(this.popup, 'popup');\n    }\n\n    async updateEditorMode(language) {\n        this.editor.setMode(language, this.editor.getContent());\n    }\n\n    languageInputChange(language) {\n        this.updateEditorMode(language);\n        const inputLang = language.toLowerCase();\n\n        for (const link of this.languageButtons) {\n            const lang = link.dataset.lang.toLowerCase().trim();\n            const isMatch = inputLang === lang;\n            link.classList.toggle('active', isMatch);\n            if (isMatch) {\n                link.scrollIntoView({block: 'center', behavior: 'smooth'});\n            }\n        }\n    }\n\n    loadHistory() {\n        this.history = JSON.parse(window.sessionStorage.getItem(this.historyKey) || '{}');\n        const historyKeys = Object.keys(this.history).reverse();\n        this.historyDropDown.classList.toggle('hidden', historyKeys.length === 0);\n        this.historyList.innerHTML = historyKeys.map(key => {\n            const localTime = (new Date(parseInt(key, 10))).toLocaleTimeString();\n            return `<li><button type=\"button\" data-time=\"${key}\" class=\"text-item\">${localTime}</button></li>`;\n        }).join('');\n    }\n\n    addHistory() {\n        if (!this.editor) return;\n        const code = this.editor.getContent();\n        if (!code) return;\n\n        // Stop if we'd be storing the same as the last item\n        const lastHistoryKey = Object.keys(this.history).pop();\n        if (this.history[lastHistoryKey] === code) return;\n\n        this.history[String(Date.now())] = code;\n        const historyString = JSON.stringify(this.history);\n        window.sessionStorage.setItem(this.historyKey, historyString);\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/code-highlighter.js",
    "content": "import {Component} from './component';\n\nexport class CodeHighlighter extends Component {\n\n    setup() {\n        const container = this.$el;\n\n        const codeBlocks = container.querySelectorAll('pre');\n        if (codeBlocks.length > 0) {\n            window.importVersioned('code').then(Code => {\n                Code.highlightWithin(container);\n            });\n        }\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/code-textarea.js",
    "content": "/**\n * A simple component to render a code editor within the textarea\n * this exists upon.\n */\nimport {Component} from './component';\n\nexport class CodeTextarea extends Component {\n\n    async setup() {\n        const {mode} = this.$opts;\n        const Code = await window.importVersioned('code');\n        Code.inlineEditor(this.$el, mode);\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/collapsible.js",
    "content": "import {slideDown, slideUp} from '../services/animations.ts';\nimport {Component} from './component';\n\n/**\n * Collapsible\n * Provides some simple logic to allow collapsible sections.\n */\nexport class Collapsible extends Component {\n\n    setup() {\n        this.container = this.$el;\n        this.trigger = this.$refs.trigger;\n        this.content = this.$refs.content;\n\n        if (this.trigger) {\n            this.trigger.addEventListener('click', this.toggle.bind(this));\n            this.openIfContainsError();\n        }\n    }\n\n    open() {\n        this.container.classList.add('open');\n        this.trigger.setAttribute('aria-expanded', 'true');\n        slideDown(this.content, 300);\n    }\n\n    close() {\n        this.container.classList.remove('open');\n        this.trigger.setAttribute('aria-expanded', 'false');\n        slideUp(this.content, 300);\n    }\n\n    toggle() {\n        if (this.container.classList.contains('open')) {\n            this.close();\n        } else {\n            this.open();\n        }\n    }\n\n    openIfContainsError() {\n        const error = this.content.querySelector('.text-neg.text-small');\n        if (error) {\n            this.open();\n        }\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/component.js",
    "content": "export class Component {\n\n    /**\n     * The registered name of the component.\n     * @type {string}\n     */\n    $name = '';\n\n    /**\n     * The element that the component is registered upon.\n     * @type {HTMLElement}\n     */\n    $el = null;\n\n    /**\n     * Mapping of referenced elements within the component.\n     * @type {Object<string, HTMLElement>}\n     */\n    $refs = {};\n\n    /**\n     * Mapping of arrays of referenced elements within the component so multiple\n     * references, sharing the same name, can be fetched.\n     * @type {Object<string, HTMLElement[]>}\n     */\n    $manyRefs = {};\n\n    /**\n     * Options passed into this component.\n     * @type {Object<String, String>}\n     */\n    $opts = {};\n\n    /**\n     * Component-specific setup methods.\n     * Use this to assign local variables and run any initial setup or actions.\n     */\n    setup() {\n        //\n    }\n\n    /**\n     * Emit an event from this component.\n     * Will be bubbled up from the dom element this is registered on, as a custom event\n     * with the name `<elementName>-<eventName>`, with the provided data in the event detail.\n     * @param {String} eventName\n     * @param {Object} data\n     */\n    $emit(eventName, data = {}) {\n        data.from = this;\n        const componentName = this.$name;\n        const event = new CustomEvent(`${componentName}-${eventName}`, {\n            bubbles: true,\n            detail: data,\n        });\n        this.$el.dispatchEvent(event);\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/confirm-dialog.js",
    "content": "import {onSelect} from '../services/dom.ts';\nimport {Component} from './component';\n\n/**\n * Custom equivalent of window.confirm() using our popup component.\n * Is promise based so can be used like so:\n * `const result = await dialog.show()`\n */\nexport class ConfirmDialog extends Component {\n\n    setup() {\n        this.container = this.$el;\n        this.confirmButton = this.$refs.confirm;\n\n        this.res = null;\n\n        onSelect(this.confirmButton, () => {\n            this.sendResult(true);\n            this.getPopup().hide();\n        });\n    }\n\n    show() {\n        this.getPopup().show(null, () => {\n            this.sendResult(false);\n        });\n\n        return new Promise(res => {\n            this.res = res;\n        });\n    }\n\n    /**\n     * @returns {Popup}\n     */\n    getPopup() {\n        return window.$components.firstOnElement(this.container, 'popup');\n    }\n\n    /**\n     * @param {Boolean} result\n     */\n    sendResult(result) {\n        if (this.res) {\n            this.res(result);\n            this.res = null;\n        }\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/custom-checkbox.js",
    "content": "import {Component} from './component';\n\nexport class CustomCheckbox extends Component {\n\n    setup() {\n        this.container = this.$el;\n        this.checkbox = this.container.querySelector('input[type=checkbox]');\n        this.display = this.container.querySelector('[role=\"checkbox\"]');\n\n        this.checkbox.addEventListener('change', this.stateChange.bind(this));\n        this.container.addEventListener('keydown', this.onKeyDown.bind(this));\n    }\n\n    onKeyDown(event) {\n        const isEnterOrSpace = event.key === ' ' || event.key === 'Enter';\n        if (isEnterOrSpace) {\n            event.preventDefault();\n            this.toggle();\n        }\n    }\n\n    toggle() {\n        this.checkbox.checked = !this.checkbox.checked;\n        this.checkbox.dispatchEvent(new Event('change'));\n        this.stateChange();\n    }\n\n    stateChange() {\n        const checked = this.checkbox.checked ? 'true' : 'false';\n        this.display.setAttribute('aria-checked', checked);\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/details-highlighter.js",
    "content": "import {Component} from './component';\n\nexport class DetailsHighlighter extends Component {\n\n    setup() {\n        this.container = this.$el;\n        this.dealtWith = false;\n\n        this.container.addEventListener('toggle', this.onToggle.bind(this));\n    }\n\n    onToggle() {\n        if (this.dealtWith) return;\n\n        if (this.container.querySelector('pre')) {\n            window.importVersioned('code').then(Code => {\n                Code.highlightWithin(this.container);\n            });\n        }\n        this.dealtWith = true;\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/dropdown-search.js",
    "content": "import {debounce} from '../services/util.ts';\nimport {transitionHeight} from '../services/animations.ts';\nimport {Component} from './component';\n\nexport class DropdownSearch extends Component {\n\n    setup() {\n        this.elem = this.$el;\n        this.searchInput = this.$refs.searchInput;\n        this.loadingElem = this.$refs.loading;\n        this.listContainerElem = this.$refs.listContainer;\n\n        this.localSearchSelector = this.$opts.localSearchSelector;\n        this.url = this.$opts.url;\n\n        this.elem.addEventListener('show', this.onShow.bind(this));\n        this.searchInput.addEventListener('input', this.onSearch.bind(this));\n\n        this.runAjaxSearch = debounce(this.runAjaxSearch, 300, false);\n    }\n\n    onShow() {\n        this.loadList();\n    }\n\n    onSearch() {\n        const input = this.searchInput.value.toLowerCase().trim();\n        if (this.localSearchSelector) {\n            this.runLocalSearch(input);\n        } else {\n            this.toggleLoading(true);\n            this.listContainerElem.innerHTML = '';\n            this.runAjaxSearch(input);\n        }\n    }\n\n    runAjaxSearch(searchTerm) {\n        this.loadList(searchTerm);\n    }\n\n    runLocalSearch(searchTerm) {\n        const listItems = this.listContainerElem.querySelectorAll(this.localSearchSelector);\n        for (const listItem of listItems) {\n            const match = !searchTerm || listItem.textContent.toLowerCase().includes(searchTerm);\n            listItem.style.display = match ? 'flex' : 'none';\n            listItem.classList.toggle('hidden', !match);\n        }\n    }\n\n    async loadList(searchTerm = '') {\n        this.listContainerElem.innerHTML = '';\n        this.toggleLoading(true);\n\n        try {\n            const resp = await window.$http.get(this.getAjaxUrl(searchTerm));\n            const animate = transitionHeight(this.listContainerElem, 80);\n            this.listContainerElem.innerHTML = resp.data;\n            animate();\n        } catch (err) {\n            console.error(err);\n        }\n\n        this.toggleLoading(false);\n        if (this.localSearchSelector) {\n            this.onSearch();\n        }\n    }\n\n    getAjaxUrl(searchTerm = null) {\n        if (!searchTerm) {\n            return this.url;\n        }\n\n        const joiner = this.url.includes('?') ? '&' : '?';\n        return `${this.url}${joiner}search=${encodeURIComponent(searchTerm)}`;\n    }\n\n    toggleLoading(show = false) {\n        this.loadingElem.style.display = show ? 'block' : 'none';\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/dropdown.js",
    "content": "import {findClosestScrollContainer, onSelect} from '../services/dom.ts';\nimport {KeyboardNavigationHandler} from '../services/keyboard-navigation.ts';\nimport {Component} from './component';\n\n/**\n * Dropdown\n * Provides some simple logic to create simple dropdown menus.\n */\nexport class Dropdown extends Component {\n\n    setup() {\n        this.container = this.$el;\n        this.menu = this.$refs.menu;\n        this.toggle = this.$refs.toggle;\n        this.moveMenu = this.$opts.moveMenu;\n        this.bubbleEscapes = this.$opts.bubbleEscapes === 'true';\n\n        this.direction = (document.dir === 'rtl') ? 'right' : 'left';\n        this.body = document.body;\n        this.showing = false;\n\n        this.hide = this.hide.bind(this);\n        this.setupListeners();\n    }\n\n    show(event = null) {\n        this.hideAll();\n\n        this.menu.style.display = 'block';\n        this.menu.classList.add('anim', 'menuIn');\n        this.toggle.setAttribute('aria-expanded', 'true');\n\n        const menuOriginalRect = this.menu.getBoundingClientRect();\n        let heightOffset = 0;\n        const toggleHeight = this.toggle.getBoundingClientRect().height;\n        const containerBounds = findClosestScrollContainer(this.menu).getBoundingClientRect();\n        const dropUpwards = menuOriginalRect.bottom > containerBounds.bottom;\n        const containerRect = this.container.getBoundingClientRect();\n\n        // If enabled, Move to body to prevent being trapped within scrollable sections\n        if (this.moveMenu) {\n            this.body.appendChild(this.menu);\n            this.menu.style.position = 'fixed';\n            this.menu.style.width = `${menuOriginalRect.width}px`;\n            this.menu.style.left = `${menuOriginalRect.left}px`;\n            if (dropUpwards) {\n                heightOffset = (window.innerHeight - menuOriginalRect.top - toggleHeight / 2);\n            } else {\n                heightOffset = menuOriginalRect.top;\n            }\n        }\n\n        // Adjust menu to display upwards if near the bottom of the screen\n        if (dropUpwards) {\n            this.menu.style.top = 'initial';\n            this.menu.style.bottom = `${heightOffset}px`;\n            const maxHeight = (window.innerHeight - 40) - (window.innerHeight - containerRect.bottom);\n            this.menu.style.maxHeight = `${Math.floor(maxHeight)}px`;\n        } else {\n            this.menu.style.top = `${heightOffset}px`;\n            this.menu.style.bottom = 'initial';\n            const maxHeight = (window.innerHeight - 40) - containerRect.top;\n            this.menu.style.maxHeight = `${Math.floor(maxHeight)}px`;\n        }\n\n        // Set listener to hide on mouse leave or window click\n        this.menu.addEventListener('mouseleave', this.hide);\n        window.addEventListener('click', clickEvent => {\n            if (!this.menu.contains(clickEvent.target)) {\n                this.hide();\n            }\n        });\n\n        // Focus on first input if existing\n        const input = this.menu.querySelector('input');\n        if (input !== null) input.focus();\n\n        this.showing = true;\n\n        const showEvent = new Event('show');\n        this.container.dispatchEvent(showEvent);\n\n        if (event) {\n            event.stopPropagation();\n        }\n    }\n\n    hideAll() {\n        for (const dropdown of window.$components.get('dropdown')) {\n            dropdown.hide();\n        }\n    }\n\n    hide() {\n        this.menu.style.display = 'none';\n        this.menu.classList.remove('anim', 'menuIn');\n        this.toggle.setAttribute('aria-expanded', 'false');\n        this.menu.style.top = '';\n        this.menu.style.bottom = '';\n        this.menu.style.maxHeight = '';\n\n        if (this.moveMenu) {\n            this.menu.style.position = '';\n            this.menu.style[this.direction] = '';\n            this.menu.style.width = '';\n            this.menu.style.left = '';\n            this.container.appendChild(this.menu);\n        }\n\n        this.showing = false;\n    }\n\n    setupListeners() {\n        const keyboardNavHandler = new KeyboardNavigationHandler(this.container, event => {\n            this.hide();\n            this.toggle.focus();\n            if (!this.bubbleEscapes) {\n                event.stopPropagation();\n            }\n        }, event => {\n            if (event.target.nodeName === 'INPUT') {\n                event.preventDefault();\n                event.stopPropagation();\n            }\n            this.hide();\n        });\n\n        if (this.moveMenu) {\n            keyboardNavHandler.shareHandlingToEl(this.menu);\n        }\n\n        // Hide menu on option click\n        this.container.addEventListener('click', event => {\n            const possibleChildren = Array.from(this.menu.querySelectorAll('a'));\n            if (possibleChildren.includes(event.target)) {\n                this.hide();\n            }\n        });\n\n        onSelect(this.toggle, event => {\n            event.stopPropagation();\n            event.preventDefault();\n            this.show(event);\n            if (event instanceof KeyboardEvent) {\n                keyboardNavHandler.focusNext();\n            }\n        });\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/dropzone.js",
    "content": "import {Component} from './component';\nimport {Clipboard} from '../services/clipboard.ts';\nimport {\n    elem, getLoading, onSelect, removeLoading,\n} from '../services/dom.ts';\n\nexport class Dropzone extends Component {\n\n    setup() {\n        this.container = this.$el;\n        this.statusArea = this.$refs.statusArea;\n        this.dropTarget = this.$refs.dropTarget;\n        this.selectButtons = this.$manyRefs.selectButton || [];\n\n        this.isActive = true;\n\n        this.url = this.$opts.url;\n        this.method = (this.$opts.method || 'post').toUpperCase();\n        this.successMessage = this.$opts.successMessage;\n        this.errorMessage = this.$opts.errorMessage;\n        this.uploadLimitMb = Number(this.$opts.uploadLimit);\n        this.uploadLimitMessage = this.$opts.uploadLimitMessage;\n        this.zoneText = this.$opts.zoneText;\n        this.fileAcceptTypes = this.$opts.fileAccept;\n        this.allowMultiple = this.$opts.allowMultiple === 'true';\n\n        this.setupListeners();\n    }\n\n    /**\n     * Public method to allow external disabling/enabling of this drag+drop dropzone.\n     * @param {Boolean} active\n     */\n    toggleActive(active) {\n        this.isActive = active;\n    }\n\n    setupListeners() {\n        onSelect(this.selectButtons, this.manualSelectHandler.bind(this));\n        this.setupDropTargetHandlers();\n    }\n\n    setupDropTargetHandlers() {\n        let depth = 0;\n\n        const reset = () => {\n            this.hideOverlay();\n            depth = 0;\n        };\n\n        this.dropTarget.addEventListener('dragenter', event => {\n            event.preventDefault();\n            depth += 1;\n\n            if (depth === 1 && this.isActive) {\n                this.showOverlay();\n            }\n        });\n\n        this.dropTarget.addEventListener('dragover', event => {\n            event.preventDefault();\n        });\n\n        this.dropTarget.addEventListener('dragend', reset);\n        this.dropTarget.addEventListener('dragleave', () => {\n            depth -= 1;\n            if (depth === 0) {\n                reset();\n            }\n        });\n        this.dropTarget.addEventListener('drop', event => {\n            event.preventDefault();\n            reset();\n\n            if (!this.isActive) {\n                return;\n            }\n\n            const clipboard = new Clipboard(event.dataTransfer);\n            const files = clipboard.getFiles();\n            for (const file of files) {\n                this.createUploadFromFile(file);\n            }\n        });\n    }\n\n    manualSelectHandler() {\n        const input = elem('input', {\n            type: 'file',\n            style: 'left: -400px; visibility: hidden; position: fixed;',\n            accept: this.fileAcceptTypes,\n            multiple: this.allowMultiple ? '' : null,\n        });\n        this.container.append(input);\n        input.click();\n        input.addEventListener('change', () => {\n            for (const file of input.files) {\n                this.createUploadFromFile(file);\n            }\n            input.remove();\n        });\n    }\n\n    showOverlay() {\n        const overlay = this.dropTarget.querySelector('.dropzone-overlay');\n        if (!overlay) {\n            const zoneElem = elem('div', {class: 'dropzone-overlay'}, [this.zoneText]);\n            this.dropTarget.append(zoneElem);\n        }\n    }\n\n    hideOverlay() {\n        const overlay = this.dropTarget.querySelector('.dropzone-overlay');\n        if (overlay) {\n            overlay.remove();\n        }\n    }\n\n    /**\n     * @param {File} file\n     * @return {Upload}\n     */\n    createUploadFromFile(file) {\n        const {\n            dom, status, progress, dismiss,\n        } = this.createDomForFile(file);\n        this.statusArea.append(dom);\n        const component = this;\n\n        const upload = {\n            file,\n            dom,\n            updateProgress(percentComplete) {\n                progress.textContent = `${percentComplete}%`;\n                progress.style.width = `${percentComplete}%`;\n            },\n            markError(message) {\n                status.setAttribute('data-status', 'error');\n                status.textContent = message;\n                removeLoading(dom);\n                this.updateProgress(100);\n            },\n            markSuccess(message) {\n                status.setAttribute('data-status', 'success');\n                status.textContent = message;\n                removeLoading(dom);\n                setTimeout(dismiss, 2400);\n                component.$emit('upload-success', {\n                    name: file.name,\n                });\n            },\n        };\n\n        // Enforce early upload filesize limit\n        if (file.size > (this.uploadLimitMb * 1000000)) {\n            upload.markError(this.uploadLimitMessage);\n            return upload;\n        }\n\n        this.startXhrForUpload(upload);\n\n        return upload;\n    }\n\n    /**\n     * @param {Upload} upload\n     */\n    startXhrForUpload(upload) {\n        const formData = new FormData();\n        formData.append('file', upload.file, upload.file.name);\n        if (this.method !== 'POST') {\n            formData.append('_method', this.method);\n        }\n        const component = this;\n\n        const req = window.$http.createXMLHttpRequest('POST', this.url, {\n            error() {\n                upload.markError(component.errorMessage);\n            },\n            readystatechange() {\n                if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {\n                    upload.markSuccess(component.successMessage);\n                } else if (this.readyState === XMLHttpRequest.DONE && this.status >= 400) {\n                    upload.markError(window.$http.formatErrorResponseText(this.responseText));\n                }\n            },\n        });\n\n        req.upload.addEventListener('progress', evt => {\n            const percent = Math.min(Math.ceil((evt.loaded / evt.total) * 100), 100);\n            upload.updateProgress(percent);\n        });\n\n        req.setRequestHeader('Accept', 'application/json');\n        req.send(formData);\n    }\n\n    /**\n     * @param {File} file\n     * @return {{image: Element, dom: Element, progress: Element, status: Element, dismiss: function}}\n     */\n    createDomForFile(file) {\n        const image = elem('img', {src: \"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9.224 7.373a.924.924 0 0 0-.92.925l-.006 7.404c0 .509.412.925.921.925h5.557a.928.928 0 0 0 .926-.925v-5.553l-2.777-2.776Zm3.239 3.239V8.067l2.545 2.545z' style='fill:%23000;fill-opacity:.75'/%3E%3C/svg%3E\"});\n        const status = elem('div', {class: 'dropzone-file-item-status'}, []);\n        const progress = elem('div', {class: 'dropzone-file-item-progress'});\n        const imageWrap = elem('div', {class: 'dropzone-file-item-image-wrap'}, [image]);\n\n        const dom = elem('div', {class: 'dropzone-file-item'}, [\n            imageWrap,\n            elem('div', {class: 'dropzone-file-item-text-wrap'}, [\n                elem('div', {class: 'dropzone-file-item-label'}, [file.name]),\n                getLoading(),\n                status,\n            ]),\n            progress,\n        ]);\n\n        if (file.type.startsWith('image/')) {\n            image.src = URL.createObjectURL(file);\n        }\n\n        const dismiss = () => {\n            dom.classList.add('dismiss');\n            dom.addEventListener('animationend', () => {\n                dom.remove();\n            });\n        };\n\n        dom.addEventListener('click', dismiss);\n\n        return {\n            dom, progress, status, dismiss,\n        };\n    }\n\n}\n\n/**\n * @typedef Upload\n * @property {File} file\n * @property {Element} dom\n * @property {function(Number)} updateProgress\n * @property {function(String)} markError\n * @property {function(String)} markSuccess\n */\n"
  },
  {
    "path": "resources/js/components/editor-toolbox.ts",
    "content": "import {Component} from './component';\n\nexport interface EditorToolboxChangeEventData {\n    tab: string;\n    open: boolean;\n}\n\nexport class EditorToolbox extends Component {\n\n    protected container!: HTMLElement;\n    protected buttons!: HTMLButtonElement[];\n    protected contentElements!: HTMLElement[];\n    protected toggleButton!: HTMLElement;\n    protected editorWrapEl!: HTMLElement;\n\n    protected open: boolean = false;\n    protected tab: string = '';\n\n    setup() {\n        // Elements\n        this.container = this.$el;\n        this.buttons = this.$manyRefs.tabButton as HTMLButtonElement[];\n        this.contentElements = this.$manyRefs.tabContent;\n        this.toggleButton = this.$refs.toggle;\n        this.editorWrapEl = this.container.closest('.page-editor') as HTMLElement;\n\n        this.setupListeners();\n\n        // Set the first tab as active on load\n        this.setActiveTab(this.contentElements[0].dataset.tabContent || '');\n    }\n\n    protected setupListeners(): void {\n        // Toolbox toggle button click\n        this.toggleButton.addEventListener('click', () => this.toggle());\n        // Tab button click\n        this.container.addEventListener('click', (event: MouseEvent) => {\n            const button = (event.target as HTMLElement).closest('button');\n            if (button instanceof HTMLButtonElement && this.buttons.includes(button)) {\n                const name = button.dataset.tab || '';\n                this.setActiveTab(name, true);\n            }\n        });\n    }\n\n    protected toggle(): void {\n        this.container.classList.toggle('open');\n        const isOpen = this.container.classList.contains('open');\n        this.toggleButton.setAttribute('aria-expanded', isOpen ? 'true' : 'false');\n        this.editorWrapEl.classList.toggle('toolbox-open', isOpen);\n        this.open = isOpen;\n        this.emitState();\n    }\n\n    protected setActiveTab(tabName: string, openToolbox: boolean = false): void {\n        // Set button visibility\n        for (const button of this.buttons) {\n            button.classList.remove('active');\n            const bName = button.dataset.tab;\n            if (bName === tabName) button.classList.add('active');\n        }\n\n        // Set content visibility\n        for (const contentEl of this.contentElements) {\n            contentEl.style.display = 'none';\n            const cName = contentEl.dataset.tabContent;\n            if (cName === tabName) contentEl.style.display = 'block';\n        }\n\n        if (openToolbox && !this.container.classList.contains('open')) {\n            this.toggle();\n        }\n\n        this.tab = tabName;\n        this.emitState();\n    }\n\n    protected emitState(): void {\n        const data: EditorToolboxChangeEventData = {tab: this.tab, open: this.open};\n        this.$emit('change', data);\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/entity-permissions.js",
    "content": "import {htmlToDom} from '../services/dom.ts';\nimport {Component} from './component';\n\nexport class EntityPermissions extends Component {\n\n    setup() {\n        this.container = this.$el;\n        this.entityType = this.$opts.entityType;\n\n        this.everyoneInheritToggle = this.$refs.everyoneInherit;\n        this.roleSelect = this.$refs.roleSelect;\n        this.roleContainer = this.$refs.roleContainer;\n\n        this.setupListeners();\n    }\n\n    setupListeners() {\n        // \"Everyone Else\" inherit toggle\n        this.everyoneInheritToggle.addEventListener('change', event => {\n            const inherit = event.target.checked;\n            const permissions = document.querySelectorAll('input[name^=\"permissions[0][\"]');\n            for (const permission of permissions) {\n                permission.disabled = inherit;\n                permission.checked = false;\n            }\n        });\n\n        // Remove role row button click\n        this.container.addEventListener('click', event => {\n            const button = event.target.closest('button');\n            if (button && button.dataset.roleId) {\n                this.removeRowOnButtonClick(button);\n            }\n        });\n\n        // Role select change\n        this.roleSelect.addEventListener('change', () => {\n            const roleId = this.roleSelect.value;\n            if (roleId) {\n                this.addRoleRow(roleId);\n            }\n        });\n    }\n\n    async addRoleRow(roleId) {\n        this.roleSelect.disabled = true;\n\n        // Remove option from select\n        const option = this.roleSelect.querySelector(`option[value=\"${roleId}\"]`);\n        if (option) {\n            option.remove();\n        }\n\n        // Get and insert new row\n        const resp = await window.$http.get(`/permissions/form-row/${this.entityType}/${roleId}`);\n        const row = htmlToDom(resp.data);\n        this.roleContainer.append(row);\n\n        this.roleSelect.disabled = false;\n    }\n\n    removeRowOnButtonClick(button) {\n        const row = button.closest('.item-list-row');\n        const {roleId} = button.dataset;\n        const {roleName} = button.dataset;\n\n        const option = document.createElement('option');\n        option.value = roleId;\n        option.textContent = roleName;\n\n        this.roleSelect.append(option);\n        row.remove();\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/entity-search.js",
    "content": "import {onSelect} from '../services/dom.ts';\nimport {Component} from './component';\n\nexport class EntitySearch extends Component {\n\n    setup() {\n        this.entityId = this.$opts.entityId;\n        this.entityType = this.$opts.entityType;\n\n        this.contentView = this.$refs.contentView;\n        this.searchView = this.$refs.searchView;\n        this.searchResults = this.$refs.searchResults;\n        this.searchInput = this.$refs.searchInput;\n        this.searchForm = this.$refs.searchForm;\n        this.clearButton = this.$refs.clearButton;\n        this.loadingBlock = this.$refs.loadingBlock;\n\n        this.setupListeners();\n    }\n\n    setupListeners() {\n        this.searchInput.addEventListener('change', this.runSearch.bind(this));\n        this.searchForm.addEventListener('submit', e => {\n            e.preventDefault();\n            this.runSearch();\n        });\n\n        onSelect(this.clearButton, this.clearSearch.bind(this));\n    }\n\n    runSearch() {\n        const term = this.searchInput.value.trim();\n        if (term.length === 0) {\n            this.clearSearch();\n            return;\n        }\n\n        this.searchView.classList.remove('hidden');\n        this.contentView.classList.add('hidden');\n        this.loadingBlock.classList.remove('hidden');\n\n        const url = window.baseUrl(`/search/${this.entityType}/${this.entityId}`);\n        window.$http.get(url, {term}).then(resp => {\n            this.searchResults.innerHTML = resp.data;\n        }).catch(console.error).then(() => {\n            this.loadingBlock.classList.add('hidden');\n        });\n    }\n\n    clearSearch() {\n        this.searchView.classList.add('hidden');\n        this.contentView.classList.remove('hidden');\n        this.loadingBlock.classList.add('hidden');\n        this.searchInput.value = '';\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/entity-selector-popup.ts",
    "content": "import {Component} from './component';\nimport {EntitySelector, EntitySelectorEntity, EntitySelectorSearchOptions} from \"./entity-selector\";\nimport {Popup} from \"./popup\";\n\nexport type EntitySelectorPopupCallback = (entity: EntitySelectorEntity) => void;\n\nexport class EntitySelectorPopup extends Component {\n\n    protected container!: HTMLElement;\n    protected selectButton!: HTMLElement;\n    protected selectorEl!: HTMLElement;\n\n    protected callback: EntitySelectorPopupCallback|null = null;\n    protected selection: EntitySelectorEntity|null = null;\n\n    setup() {\n        this.container = this.$el;\n        this.selectButton = this.$refs.select;\n        this.selectorEl = this.$refs.selector;\n\n        this.selectButton.addEventListener('click', this.onSelectButtonClick.bind(this));\n        window.$events.listen('entity-select-change', this.onSelectionChange.bind(this));\n        window.$events.listen('entity-select-confirm', this.handleConfirmedSelection.bind(this));\n    }\n\n    /**\n     * Show the selector popup.\n     */\n    show(callback: EntitySelectorPopupCallback, searchOptions: Partial<EntitySelectorSearchOptions> = {}) {\n        this.callback = callback;\n        this.getSelector().configureSearchOptions(searchOptions);\n        this.getPopup().show();\n\n        this.getSelector().focusSearch();\n    }\n\n    hide() {\n        this.getPopup().hide();\n    }\n\n    getPopup(): Popup {\n        return window.$components.firstOnElement(this.container, 'popup') as Popup;\n    }\n\n    getSelector(): EntitySelector {\n        return window.$components.firstOnElement(this.selectorEl, 'entity-selector') as EntitySelector;\n    }\n\n    onSelectButtonClick() {\n        this.handleConfirmedSelection(this.selection);\n    }\n\n    onSelectionChange(entity: EntitySelectorEntity|{}) {\n        this.selection = (entity.hasOwnProperty('id') ? entity : null) as EntitySelectorEntity|null;\n        if (!this.selection) {\n            this.selectButton.setAttribute('disabled', 'true');\n        } else {\n            this.selectButton.removeAttribute('disabled');\n        }\n    }\n\n    handleConfirmedSelection(entity: EntitySelectorEntity|null): void {\n        this.hide();\n        this.getSelector().reset();\n        if (this.callback && entity) this.callback(entity);\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/entity-selector.ts",
    "content": "import {onChildEvent} from '../services/dom';\nimport {Component} from './component';\n\nexport interface EntitySelectorSearchOptions {\n    entityTypes: string;\n    entityPermission: string;\n    searchEndpoint: string;\n    initialValue: string;\n}\n\nexport type EntitySelectorEntity = {\n    id: number,\n    name: string,\n    link: string,\n};\n\nexport class EntitySelector extends Component {\n    protected elem!: HTMLElement;\n    protected input!: HTMLInputElement;\n    protected searchInput!: HTMLInputElement;\n    protected loading!: HTMLElement;\n    protected resultsContainer!: HTMLElement;\n\n    protected searchOptions!: EntitySelectorSearchOptions;\n\n    protected search = '';\n    protected lastClick = 0;\n\n    setup() {\n        this.elem = this.$el;\n\n        this.input = this.$refs.input as HTMLInputElement;\n        this.searchInput = this.$refs.search as HTMLInputElement;\n        this.loading = this.$refs.loading;\n        this.resultsContainer = this.$refs.results;\n\n        this.searchOptions = {\n            entityTypes: this.$opts.entityTypes || 'page,book,chapter',\n            entityPermission: this.$opts.entityPermission || 'view',\n            searchEndpoint: this.$opts.searchEndpoint || '',\n            initialValue: this.searchInput.value || '',\n        };\n\n        this.setupListeners();\n        this.showLoading();\n\n        if (this.searchOptions.searchEndpoint) {\n            this.initialLoad();\n        }\n    }\n\n    configureSearchOptions(options: Partial<EntitySelectorSearchOptions>): void {\n        Object.assign(this.searchOptions, options);\n        this.reset();\n        this.searchInput.value = this.searchOptions.initialValue;\n    }\n\n    setupListeners(): void {\n        this.elem.addEventListener('click', this.onClick.bind(this));\n\n        let lastSearch = 0;\n        this.searchInput.addEventListener('input', () => {\n            lastSearch = Date.now();\n            this.showLoading();\n            setTimeout(() => {\n                if (Date.now() - lastSearch < 199) return;\n                this.searchEntities(this.searchInput.value);\n            }, 200);\n        });\n\n        this.searchInput.addEventListener('keydown', event => {\n            if (event.keyCode === 13) event.preventDefault();\n        });\n\n        // Keyboard navigation\n        onChildEvent(this.$el, '[data-entity-type]', 'keydown', ((event: KeyboardEvent) => {\n            if (event.ctrlKey && event.code === 'Enter') {\n                const form = this.$el.closest('form');\n                if (form) {\n                    form.submit();\n                    event.preventDefault();\n                    return;\n                }\n            }\n\n            if (event.code === 'ArrowDown') {\n                this.focusAdjacent(true);\n            }\n            if (event.code === 'ArrowUp') {\n                this.focusAdjacent(false);\n            }\n        }) as (event: Event) => void);\n\n        this.searchInput.addEventListener('keydown', event => {\n            if (event.code === 'ArrowDown') {\n                this.focusAdjacent(true);\n            }\n        });\n    }\n\n    focusAdjacent(forward = true) {\n        const items: (Element|null)[] = Array.from(this.resultsContainer.querySelectorAll('[data-entity-type]'));\n        const selectedIndex = items.indexOf(document.activeElement);\n        const newItem = items[selectedIndex + (forward ? 1 : -1)] || items[0];\n        if (newItem instanceof HTMLElement) {\n            newItem.focus();\n        }\n    }\n\n    reset() {\n        this.searchInput.value = '';\n        this.showLoading();\n        this.initialLoad();\n    }\n\n    focusSearch() {\n        this.searchInput.focus();\n    }\n\n    showLoading() {\n        this.loading.style.display = 'block';\n        this.resultsContainer.style.display = 'none';\n    }\n\n    hideLoading() {\n        this.loading.style.display = 'none';\n        this.resultsContainer.style.display = 'block';\n    }\n\n    initialLoad() {\n        if (!this.searchOptions.searchEndpoint) {\n            throw new Error('Search endpoint not set for entity-selector load');\n        }\n\n        if (this.searchOptions.initialValue) {\n            this.searchEntities(this.searchOptions.initialValue);\n            return;\n        }\n\n        window.$http.get(this.searchUrl()).then(resp => {\n            this.resultsContainer.innerHTML = resp.data as string;\n            this.hideLoading();\n        });\n    }\n\n    searchUrl() {\n        const query = `types=${encodeURIComponent(this.searchOptions.entityTypes)}&permission=${encodeURIComponent(this.searchOptions.entityPermission)}`;\n        return `${this.searchOptions.searchEndpoint}?${query}`;\n    }\n\n    searchEntities(searchTerm: string) {\n        if (!this.searchOptions.searchEndpoint) {\n            throw new Error('Search endpoint not set for entity-selector load');\n        }\n\n        this.input.value = '';\n        const url = `${this.searchUrl()}&term=${encodeURIComponent(searchTerm)}`;\n        window.$http.get(url).then(resp => {\n            this.resultsContainer.innerHTML = resp.data as string;\n            this.hideLoading();\n        });\n    }\n\n    isDoubleClick() {\n        const now = Date.now();\n        const answer = now - this.lastClick < 300;\n        this.lastClick = now;\n        return answer;\n    }\n\n    onClick(event: MouseEvent) {\n        const listItem = (event.target as HTMLElement).closest('[data-entity-type]');\n        if (listItem instanceof HTMLElement) {\n            event.preventDefault();\n            event.stopPropagation();\n            this.selectItem(listItem);\n        }\n    }\n\n    selectItem(item: HTMLElement): void {\n        const isDblClick = this.isDoubleClick();\n        const type = item.getAttribute('data-entity-type');\n        const id = item.getAttribute('data-entity-id');\n        const isSelected = (!item.classList.contains('selected') || isDblClick);\n\n        this.unselectAll();\n        this.input.value = isSelected ? `${type}:${id}` : '';\n\n        const link = item.getAttribute('href') || '';\n        const name = item.querySelector('.entity-list-item-name')?.textContent || '';\n        const data: EntitySelectorEntity = {id: Number(id), name, link};\n\n        if (isSelected) {\n            item.classList.add('selected');\n        } else {\n            window.$events.emit('entity-select-change');\n        }\n\n        if (!isDblClick && !isSelected) return;\n\n        if (isDblClick) {\n            this.confirmSelection(data);\n        }\n        if (isSelected) {\n            window.$events.emit('entity-select-change', data);\n        }\n    }\n\n    confirmSelection(data: EntitySelectorEntity) {\n        window.$events.emit('entity-select-confirm', data);\n    }\n\n    unselectAll() {\n        const selected = this.elem.querySelectorAll('.selected');\n        for (const selectedElem of selected) {\n            selectedElem.classList.remove('selected', 'primary-background');\n        }\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/event-emit-select.js",
    "content": "import {onSelect} from '../services/dom.ts';\nimport {Component} from './component';\n\n/**\n * EventEmitSelect\n * Component will simply emit an event when selected.\n *\n * Has one required option: \"name\".\n * A name of \"hello\" will emit a component DOM event of\n * \"event-emit-select-name\"\n *\n * All options will be set as the \"detail\" of the event with\n * their values included.\n */\nexport class EventEmitSelect extends Component {\n\n    setup() {\n        this.container = this.$el;\n        this.name = this.$opts.name;\n\n        onSelect(this.$el, () => {\n            this.$emit(this.name, this.$opts);\n        });\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/expand-toggle.js",
    "content": "import {slideUp, slideDown} from '../services/animations.ts';\nimport {Component} from './component';\n\nexport class ExpandToggle extends Component {\n\n    setup() {\n        this.targetSelector = this.$opts.targetSelector;\n        this.isOpen = this.$opts.isOpen === 'true';\n        this.updateEndpoint = this.$opts.updateEndpoint;\n\n        // Listener setup\n        this.$el.addEventListener('click', this.click.bind(this));\n    }\n\n    open(elemToToggle) {\n        slideDown(elemToToggle, 200);\n    }\n\n    close(elemToToggle) {\n        slideUp(elemToToggle, 200);\n    }\n\n    click(event) {\n        event.preventDefault();\n\n        const matchingElems = document.querySelectorAll(this.targetSelector);\n        for (const match of matchingElems) {\n            const action = this.isOpen ? this.close : this.open;\n            action(match);\n        }\n\n        this.isOpen = !this.isOpen;\n        this.updateSystemAjax(this.isOpen);\n    }\n\n    updateSystemAjax(isOpen) {\n        window.$http.patch(this.updateEndpoint, {\n            expand: isOpen ? 'true' : 'false',\n        });\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/global-search.js",
    "content": "import {htmlToDom} from '../services/dom.ts';\nimport {debounce} from '../services/util.ts';\nimport {KeyboardNavigationHandler} from '../services/keyboard-navigation.ts';\nimport {Component} from './component';\n\n/**\n * Global (header) search box handling.\n * Mainly to show live results preview.\n */\nexport class GlobalSearch extends Component {\n\n    setup() {\n        this.container = this.$el;\n        this.input = this.$refs.input;\n        this.suggestions = this.$refs.suggestions;\n        this.suggestionResultsWrap = this.$refs.suggestionResults;\n        this.loadingWrap = this.$refs.loading;\n        this.button = this.$refs.button;\n\n        this.setupListeners();\n    }\n\n    setupListeners() {\n        const updateSuggestionsDebounced = debounce(this.updateSuggestions.bind(this), 200, false);\n\n        // Handle search input changes\n        this.input.addEventListener('input', () => {\n            const {value} = this.input;\n            if (value.length > 0) {\n                this.loadingWrap.style.display = 'block';\n                this.suggestionResultsWrap.style.opacity = '0.5';\n                updateSuggestionsDebounced(value);\n            } else {\n                this.hideSuggestions();\n            }\n        });\n\n        // Allow double click to show auto-click suggestions\n        this.input.addEventListener('dblclick', () => {\n            this.input.setAttribute('autocomplete', 'on');\n            this.button.focus();\n            this.input.focus();\n        });\n\n        new KeyboardNavigationHandler(this.container, () => {\n            this.hideSuggestions();\n        });\n    }\n\n    /**\n     * @param {String} search\n     */\n    async updateSuggestions(search) {\n        const {data: results} = await window.$http.get('/search/suggest', {term: search});\n        if (!this.input.value) {\n            return;\n        }\n\n        const resultDom = htmlToDom(results);\n\n        this.suggestionResultsWrap.innerHTML = '';\n        this.suggestionResultsWrap.style.opacity = '1';\n        this.loadingWrap.style.display = 'none';\n        this.suggestionResultsWrap.append(resultDom);\n        if (!this.container.classList.contains('search-active')) {\n            this.showSuggestions();\n        }\n    }\n\n    showSuggestions() {\n        this.container.classList.add('search-active');\n        window.requestAnimationFrame(() => {\n            this.suggestions.classList.add('search-suggestions-animation');\n        });\n    }\n\n    hideSuggestions() {\n        this.container.classList.remove('search-active');\n        this.suggestions.classList.remove('search-suggestions-animation');\n        this.suggestionResultsWrap.innerHTML = '';\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/header-mobile-toggle.js",
    "content": "import {Component} from './component';\n\nexport class HeaderMobileToggle extends Component {\n\n    setup() {\n        this.elem = this.$el;\n        this.toggleButton = this.$refs.toggle;\n        this.menu = this.$refs.menu;\n\n        this.open = false;\n        this.toggleButton.addEventListener('click', this.onToggle.bind(this));\n        this.onWindowClick = this.onWindowClick.bind(this);\n        this.onKeyDown = this.onKeyDown.bind(this);\n    }\n\n    onToggle(event) {\n        this.open = !this.open;\n        this.menu.classList.toggle('show', this.open);\n        this.toggleButton.setAttribute('aria-expanded', this.open ? 'true' : 'false');\n        if (this.open) {\n            this.elem.addEventListener('keydown', this.onKeyDown);\n            window.addEventListener('click', this.onWindowClick);\n        } else {\n            this.elem.removeEventListener('keydown', this.onKeyDown);\n            window.removeEventListener('click', this.onWindowClick);\n        }\n        event.stopPropagation();\n    }\n\n    onKeyDown(event) {\n        if (event.code === 'Escape') {\n            this.onToggle(event);\n        }\n    }\n\n    onWindowClick(event) {\n        this.onToggle(event);\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/image-manager.js",
    "content": "import {\n    onChildEvent, onSelect, removeLoading, showLoading,\n} from '../services/dom.ts';\nimport {Component} from './component';\n\nexport class ImageManager extends Component {\n\n    setup() {\n        // Options\n        this.uploadedTo = this.$opts.uploadedTo;\n\n        // Element References\n        this.container = this.$el;\n        this.popupEl = this.$refs.popup;\n        this.searchForm = this.$refs.searchForm;\n        this.searchInput = this.$refs.searchInput;\n        this.cancelSearch = this.$refs.cancelSearch;\n        this.listContainer = this.$refs.listContainer;\n        this.filterTabs = this.$manyRefs.filterTabs;\n        this.selectButton = this.$refs.selectButton;\n        this.uploadButton = this.$refs.uploadButton;\n        this.uploadHint = this.$refs.uploadHint;\n        this.formContainer = this.$refs.formContainer;\n        this.formContainerPlaceholder = this.$refs.formContainerPlaceholder;\n        this.dropzoneContainer = this.$refs.dropzoneContainer;\n        this.loadMore = this.$refs.loadMore;\n\n        // Instance data\n        this.type = 'gallery';\n        this.lastSelected = {};\n        this.lastSelectedTime = 0;\n        this.callback = null;\n        this.resetState = () => {\n            this.hasData = false;\n            this.page = 1;\n            this.filter = 'all';\n        };\n        this.resetState();\n\n        this.setupListeners();\n    }\n\n    setupListeners() {\n        // Filter tab click\n        onSelect(this.filterTabs, e => {\n            this.resetAll();\n            this.filter = e.target.dataset.filter;\n            this.setActiveFilterTab(this.filter);\n            this.loadGallery();\n        });\n\n        // Search submit\n        this.searchForm.addEventListener('submit', event => {\n            this.resetListView();\n            this.loadGallery();\n            this.cancelSearch.toggleAttribute('hidden', !this.searchInput.value);\n            event.preventDefault();\n        });\n\n        // Cancel search button\n        onSelect(this.cancelSearch, () => {\n            this.resetListView();\n            this.resetSearchView();\n            this.loadGallery();\n        });\n\n        // Load more button click\n        onChildEvent(this.container, '.load-more button', 'click', this.runLoadMore.bind(this));\n\n        // Select image event\n        this.listContainer.addEventListener('event-emit-select-image', this.onImageSelectEvent.bind(this));\n\n        // Image load error handling\n        this.listContainer.addEventListener('error', event => {\n            event.target.src = window.baseUrl('loading_error.png');\n        }, true);\n\n        // Footer select button click\n        onSelect(this.selectButton, () => {\n            if (this.callback) {\n                this.callback(this.lastSelected);\n            }\n            this.hide();\n        });\n\n        // Delete button click\n        onChildEvent(this.formContainer, '#image-manager-delete', 'click', () => {\n            if (this.lastSelected) {\n                this.loadImageEditForm(this.lastSelected.id, true);\n            }\n        });\n\n        // Rebuild thumbs click\n        onChildEvent(this.formContainer, '#image-manager-rebuild-thumbs', 'click', async (_, button) => {\n            button.disabled = true;\n            if (this.lastSelected) {\n                await this.rebuildThumbnails(this.lastSelected.id);\n            }\n            button.disabled = false;\n        });\n\n        // Edit form submit\n        this.formContainer.addEventListener('ajax-form-success', () => {\n            this.refreshGallery();\n            this.resetEditForm();\n        });\n\n        // Image upload success\n        this.container.addEventListener('dropzone-upload-success', this.refreshGallery.bind(this));\n\n        // Auto load-more on scroll\n        const scrollZone = this.listContainer.parentElement;\n        let scrollEvents = [];\n        scrollZone.addEventListener('wheel', event => {\n            const scrollOffset = Math.ceil(scrollZone.scrollHeight - scrollZone.scrollTop);\n            const bottomedOut = scrollOffset === scrollZone.clientHeight;\n            if (!bottomedOut || event.deltaY < 1) {\n                return;\n            }\n\n            const secondAgo = Date.now() - 1000;\n            scrollEvents.push(Date.now());\n            scrollEvents = scrollEvents.filter(d => d >= secondAgo);\n            if (scrollEvents.length > 5 && this.canLoadMore()) {\n                this.runLoadMore();\n            }\n        });\n    }\n\n    /**\n     * @param {({ thumbs: { display: string; }; url: string; name: string; }) => void} callback\n     * @param {String} type\n     */\n    show(callback, type = 'gallery') {\n        this.resetAll();\n\n        this.callback = callback;\n        this.type = type;\n        this.getPopup().show();\n\n        const hideUploads = type !== 'gallery';\n        this.dropzoneContainer.classList.toggle('hidden', hideUploads);\n        this.uploadButton.classList.toggle('hidden', hideUploads);\n        this.uploadHint.classList.toggle('hidden', hideUploads);\n\n        /** @var {Dropzone} * */\n        const dropzone = window.$components.firstOnElement(this.container, 'dropzone');\n        dropzone.toggleActive(!hideUploads);\n\n        if (!this.hasData) {\n            this.loadGallery();\n            this.hasData = true;\n        }\n    }\n\n    hide() {\n        this.getPopup().hide();\n    }\n\n    /**\n     * @returns {Popup}\n     */\n    getPopup() {\n        return window.$components.firstOnElement(this.popupEl, 'popup');\n    }\n\n    async loadGallery() {\n        const params = {\n            page: this.page,\n            search: this.searchInput.value || null,\n            uploaded_to: this.uploadedTo,\n            filter_type: this.filter === 'all' ? null : this.filter,\n        };\n\n        const {data: html} = await window.$http.get(`images/${this.type}`, params);\n        if (params.page === 1) {\n            this.listContainer.innerHTML = '';\n        }\n        this.addReturnedHtmlElementsToList(html);\n        removeLoading(this.listContainer);\n    }\n\n    addReturnedHtmlElementsToList(html) {\n        const el = document.createElement('div');\n        el.innerHTML = html;\n\n        const loadMore = el.querySelector('.load-more');\n        if (loadMore) {\n            loadMore.remove();\n            this.loadMore.innerHTML = loadMore.innerHTML;\n        }\n        this.loadMore.toggleAttribute('hidden', !loadMore);\n\n        window.$components.init(el);\n        for (const child of [...el.children]) {\n            this.listContainer.appendChild(child);\n        }\n    }\n\n    setActiveFilterTab(filterName) {\n        for (const tab of this.filterTabs) {\n            const selected = tab.dataset.filter === filterName;\n            tab.setAttribute('aria-selected', selected ? 'true' : 'false');\n        }\n    }\n\n    resetAll() {\n        this.resetState();\n        this.resetListView();\n        this.resetSearchView();\n        this.resetEditForm();\n        this.setActiveFilterTab('all');\n        this.selectButton.classList.add('hidden');\n    }\n\n    resetSearchView() {\n        this.searchInput.value = '';\n        this.cancelSearch.toggleAttribute('hidden', true);\n    }\n\n    resetEditForm() {\n        this.formContainer.innerHTML = '';\n        this.formContainerPlaceholder.removeAttribute('hidden');\n    }\n\n    resetListView() {\n        showLoading(this.listContainer);\n        this.page = 1;\n    }\n\n    refreshGallery() {\n        this.resetListView();\n        this.loadGallery();\n    }\n\n    async onImageSelectEvent(event) {\n        let image = JSON.parse(event.detail.data);\n        const isDblClick = ((image && image.id === this.lastSelected.id)\n            && Date.now() - this.lastSelectedTime < 400);\n        const alreadySelected = event.target.classList.contains('selected');\n        [...this.listContainer.querySelectorAll('.selected')].forEach(el => {\n            el.classList.remove('selected');\n        });\n\n        if (!alreadySelected && !isDblClick) {\n            event.target.classList.add('selected');\n            image = await this.loadImageEditForm(image.id);\n        } else if (!isDblClick) {\n            this.resetEditForm();\n        } else if (isDblClick) {\n            image = this.lastSelected;\n        }\n\n        this.selectButton.classList.toggle('hidden', alreadySelected);\n\n        if (isDblClick && this.callback) {\n            this.callback(image);\n            this.hide();\n        }\n\n        this.lastSelected = image;\n        this.lastSelectedTime = Date.now();\n    }\n\n    async loadImageEditForm(imageId, requestDelete = false) {\n        if (!requestDelete) {\n            this.formContainer.innerHTML = '';\n        }\n\n        const params = requestDelete ? {delete: true} : {};\n        const {data: formHtml} = await window.$http.get(`/images/edit/${imageId}`, params);\n        this.formContainer.innerHTML = formHtml;\n        this.formContainerPlaceholder.setAttribute('hidden', '');\n        window.$components.init(this.formContainer);\n\n        const imageDataEl = this.formContainer.querySelector('#image-manager-form-image-data');\n        return JSON.parse(imageDataEl.text);\n    }\n\n    runLoadMore() {\n        showLoading(this.loadMore);\n        this.page += 1;\n        this.loadGallery();\n    }\n\n    canLoadMore() {\n        return this.loadMore.querySelector('button') && !this.loadMore.hasAttribute('hidden');\n    }\n\n    async rebuildThumbnails(imageId) {\n        try {\n            const response = await window.$http.put(`/images/${imageId}/rebuild-thumbnails`);\n            window.$events.success(response.data);\n            this.refreshGallery();\n        } catch (err) {\n            window.$events.showResponseError(err);\n        }\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/image-picker.js",
    "content": "import {Component} from './component';\n\nexport class ImagePicker extends Component {\n\n    setup() {\n        this.imageElem = this.$refs.image;\n        this.imageInput = this.$refs.imageInput;\n        this.resetInput = this.$refs.resetInput;\n        this.removeInput = this.$refs.removeInput;\n        this.resetButton = this.$refs.resetButton;\n        this.removeButton = this.$refs.removeButton || null;\n\n        this.defaultImage = this.$opts.defaultImage;\n\n        this.setupListeners();\n    }\n\n    setupListeners() {\n        this.resetButton.addEventListener('click', this.reset.bind(this));\n\n        if (this.removeButton) {\n            this.removeButton.addEventListener('click', this.removeImage.bind(this));\n        }\n\n        this.imageInput.addEventListener('change', this.fileInputChange.bind(this));\n    }\n\n    fileInputChange() {\n        this.resetInput.setAttribute('disabled', 'disabled');\n        if (this.removeInput) {\n            this.removeInput.setAttribute('disabled', 'disabled');\n        }\n\n        for (const file of this.imageInput.files) {\n            this.imageElem.src = window.URL.createObjectURL(file);\n        }\n        this.imageElem.classList.remove('none');\n    }\n\n    reset() {\n        this.imageInput.value = '';\n        this.imageElem.src = this.defaultImage;\n        this.resetInput.removeAttribute('disabled');\n        if (this.removeInput) {\n            this.removeInput.setAttribute('disabled', 'disabled');\n        }\n        this.imageElem.classList.remove('none');\n    }\n\n    removeImage() {\n        this.imageInput.value = '';\n        this.imageElem.classList.add('none');\n        this.removeInput.removeAttribute('disabled');\n        this.resetInput.setAttribute('disabled', 'disabled');\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/index.ts",
    "content": "export {AddRemoveRows} from './add-remove-rows';\nexport {AjaxDeleteRow} from './ajax-delete-row';\nexport {AjaxForm} from './ajax-form';\nexport {ApiNav} from './api-nav';\nexport {Attachments} from './attachments';\nexport {AttachmentsList} from './attachments-list';\nexport {AutoSuggest} from './auto-suggest';\nexport {AutoSubmit} from './auto-submit';\nexport {BackToTop} from './back-to-top';\nexport {BookSort} from './book-sort';\nexport {ChapterContents} from './chapter-contents';\nexport {CodeEditor} from './code-editor';\nexport {CodeHighlighter} from './code-highlighter';\nexport {CodeTextarea} from './code-textarea';\nexport {Collapsible} from './collapsible';\nexport {ConfirmDialog} from './confirm-dialog';\nexport {CustomCheckbox} from './custom-checkbox';\nexport {DetailsHighlighter} from './details-highlighter';\nexport {Dropdown} from './dropdown';\nexport {DropdownSearch} from './dropdown-search';\nexport {Dropzone} from './dropzone';\nexport {EditorToolbox} from './editor-toolbox';\nexport {EntityPermissions} from './entity-permissions';\nexport {EntitySearch} from './entity-search';\nexport {EntitySelector} from './entity-selector';\nexport {EntitySelectorPopup} from './entity-selector-popup';\nexport {EventEmitSelect} from './event-emit-select';\nexport {ExpandToggle} from './expand-toggle';\nexport {GlobalSearch} from './global-search';\nexport {HeaderMobileToggle} from './header-mobile-toggle';\nexport {ImageManager} from './image-manager';\nexport {ImagePicker} from './image-picker';\nexport {ListSortControl} from './list-sort-control';\nexport {LoadingButton} from './loading-button';\nexport {MarkdownEditor} from './markdown-editor';\nexport {NewUserPassword} from './new-user-password';\nexport {Notification} from './notification';\nexport {OptionalInput} from './optional-input';\nexport {PageComment} from './page-comment';\nexport {PageCommentReference} from './page-comment-reference';\nexport {PageComments} from './page-comments';\nexport {PageDisplay} from './page-display';\nexport {PageEditor} from './page-editor';\nexport {PagePicker} from './page-picker';\nexport {PermissionsTable} from './permissions-table';\nexport {Pointer} from './pointer';\nexport {Popup} from './popup';\nexport {SettingAppColorScheme} from './setting-app-color-scheme';\nexport {SettingColorPicker} from './setting-color-picker';\nexport {SettingHomepageControl} from './setting-homepage-control';\nexport {ShelfSort} from './shelf-sort';\nexport {Shortcuts} from './shortcuts';\nexport {ShortcutInput} from './shortcut-input';\nexport {SortableList} from './sortable-list';\nexport {SortRuleManager} from './sort-rule-manager'\nexport {SubmitOnChange} from './submit-on-change';\nexport {Tabs} from './tabs';\nexport {TagManager} from './tag-manager';\nexport {TemplateManager} from './template-manager';\nexport {ToggleSwitch} from './toggle-switch';\nexport {TriLayout} from './tri-layout';\nexport {UserSelect} from './user-select';\nexport {WebhookEvents} from './webhook-events';\nexport {WysiwygEditor} from './wysiwyg-editor';\nexport {WysiwygEditorTinymce} from './wysiwyg-editor-tinymce';\nexport {WysiwygInput} from './wysiwyg-input';\n"
  },
  {
    "path": "resources/js/components/list-sort-control.js",
    "content": "/**\n * ListSortControl\n * Manages the logic for the control which provides list sorting options.\n */\nimport {Component} from './component';\n\nexport class ListSortControl extends Component {\n\n    setup() {\n        this.elem = this.$el;\n        this.menu = this.$refs.menu;\n\n        this.sortInput = this.$refs.sort;\n        this.orderInput = this.$refs.order;\n        this.form = this.$refs.form;\n\n        this.setupListeners();\n    }\n\n    setupListeners() {\n        this.menu.addEventListener('click', event => {\n            if (event.target.closest('[data-sort-value]') !== null) {\n                this.sortOptionClick(event);\n            }\n        });\n\n        this.elem.addEventListener('click', event => {\n            if (event.target.closest('[data-sort-dir]') !== null) {\n                this.sortDirectionClick(event);\n            }\n        });\n    }\n\n    sortOptionClick(event) {\n        const sortOption = event.target.closest('[data-sort-value]');\n        this.sortInput.value = sortOption.getAttribute('data-sort-value');\n        event.preventDefault();\n        this.form.submit();\n    }\n\n    sortDirectionClick(event) {\n        const currentDir = this.orderInput.value;\n        this.orderInput.value = (currentDir === 'asc') ? 'desc' : 'asc';\n        event.preventDefault();\n        this.form.submit();\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/loading-button.ts",
    "content": "import {Component} from \"./component.js\";\nimport {showLoading} from \"../services/dom\";\nimport {el} from \"../wysiwyg/utils/dom\";\n\n/**\n * Loading button.\n * Shows a loading indicator and disables the button when the button is clicked,\n * or when the form attached to the button is submitted.\n */\nexport class LoadingButton extends Component {\n\n    protected button!: HTMLButtonElement;\n    protected loadingEl: HTMLDivElement|null = null;\n\n    setup() {\n        this.button = this.$el as HTMLButtonElement;\n        const form = this.button.form;\n\n        const action = () => {\n            setTimeout(() => this.showLoadingState(), 10)\n        };\n\n        this.button.addEventListener('click', action);\n        if (form) {\n            form.addEventListener('submit', action);\n        }\n    }\n\n    showLoadingState() {\n        this.button.disabled = true;\n\n        if (!this.loadingEl) {\n            this.loadingEl = el('div', {class: 'inline block'}) as HTMLDivElement;\n            showLoading(this.loadingEl);\n            this.button.after(this.loadingEl);\n        }\n    }\n}"
  },
  {
    "path": "resources/js/components/markdown-editor.js",
    "content": "import {Component} from './component';\n\nexport class MarkdownEditor extends Component {\n\n    setup() {\n        this.elem = this.$el;\n\n        this.pageId = this.$opts.pageId;\n        this.textDirection = this.$opts.textDirection;\n        this.imageUploadErrorText = this.$opts.imageUploadErrorText;\n        this.serverUploadLimitText = this.$opts.serverUploadLimitText;\n\n        this.display = this.$refs.display;\n        this.input = this.$refs.input;\n        this.divider = this.$refs.divider;\n        this.displayWrap = this.$refs.displayWrap;\n\n        const {settingContainer} = this.$refs;\n        const settingInputs = settingContainer.querySelectorAll('input[type=\"checkbox\"]');\n\n        this.editor = null;\n        window.importVersioned('markdown').then(markdown => {\n            return markdown.init({\n                pageId: this.pageId,\n                container: this.elem,\n                displayEl: this.display,\n                inputEl: this.input,\n                drawioUrl: this.getDrawioUrl(),\n                settingInputs: Array.from(settingInputs),\n                text: {\n                    serverUploadLimit: this.serverUploadLimitText,\n                    imageUploadError: this.imageUploadErrorText,\n                },\n            });\n        }).then(editor => {\n            this.editor = editor;\n            this.setupListeners();\n            this.emitEditorEvents();\n            this.scrollToTextIfNeeded();\n            this.editor.actions.updateAndRender();\n        });\n    }\n\n    emitEditorEvents() {\n        window.$events.emitPublic(this.elem, 'editor-markdown::setup', {\n            markdownIt: this.editor.markdown.getRenderer(),\n            displayEl: this.display,\n            cmEditorView: this.editor.cm,\n        });\n    }\n\n    setupListeners() {\n        // Button actions\n        this.elem.addEventListener('click', event => {\n            const button = event.target.closest('button[data-action]');\n            if (button === null) return;\n\n            const action = button.getAttribute('data-action');\n            if (action === 'insertImage') this.editor.actions.showImageInsert();\n            if (action === 'insertLink') this.editor.actions.showLinkSelector();\n            if (action === 'insertDrawing' && (event.ctrlKey || event.metaKey)) {\n                this.editor.actions.showImageManager();\n                return;\n            }\n            if (action === 'insertDrawing') this.editor.actions.startDrawing();\n            if (action === 'fullscreen') this.editor.actions.fullScreen();\n        });\n\n        // Mobile section toggling\n        this.elem.addEventListener('click', event => {\n            const toolbarLabel = event.target.closest('.editor-toolbar-label');\n            if (!toolbarLabel) return;\n\n            const currentActiveSections = this.elem.querySelectorAll('.markdown-editor-wrap');\n            for (const activeElem of currentActiveSections) {\n                activeElem.classList.remove('active');\n            }\n\n            toolbarLabel.closest('.markdown-editor-wrap').classList.add('active');\n        });\n\n        this.handleDividerDrag();\n    }\n\n    handleDividerDrag() {\n        this.divider.addEventListener('pointerdown', () => {\n            const wrapRect = this.elem.getBoundingClientRect();\n            const moveListener = event => {\n                const xRel = event.pageX - wrapRect.left;\n                const xPct = Math.min(Math.max(20, Math.floor((xRel / wrapRect.width) * 100)), 80);\n                this.displayWrap.style.flexBasis = `${100 - xPct}%`;\n                this.editor.settings.set('editorWidth', xPct);\n            };\n            const upListener = () => {\n                window.removeEventListener('pointermove', moveListener);\n                window.removeEventListener('pointerup', upListener);\n                this.display.style.pointerEvents = null;\n                document.body.style.userSelect = null;\n            };\n\n            this.display.style.pointerEvents = 'none';\n            document.body.style.userSelect = 'none';\n            window.addEventListener('pointermove', moveListener);\n            window.addEventListener('pointerup', upListener);\n        });\n        const widthSetting = this.editor.settings.get('editorWidth');\n        if (widthSetting) {\n            this.displayWrap.style.flexBasis = `${100 - widthSetting}%`;\n        }\n    }\n\n    scrollToTextIfNeeded() {\n        const queryParams = (new URL(window.location)).searchParams;\n        const scrollText = queryParams.get('content-text');\n        if (scrollText) {\n            this.editor.actions.scrollToText(scrollText);\n        }\n    }\n\n    /**\n     * Get the URL for the configured drawio instance.\n     * @returns {String}\n     */\n    getDrawioUrl() {\n        const drawioAttrEl = document.querySelector('[drawio-url]');\n        if (!drawioAttrEl) {\n            return '';\n        }\n\n        return drawioAttrEl.getAttribute('drawio-url') || '';\n    }\n\n    /**\n     * Get the content of this editor.\n     * Used by the parent page editor component.\n     * @return {Promise<{html: String, markdown: String}>}\n     */\n    async getContent() {\n        return this.editor.actions.getContent();\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/new-user-password.js",
    "content": "import {Component} from './component';\n\nexport class NewUserPassword extends Component {\n\n    setup() {\n        this.container = this.$el;\n        this.inputContainer = this.$refs.inputContainer;\n        this.inviteOption = this.container.querySelector('input[name=send_invite]');\n\n        if (this.inviteOption) {\n            this.inviteOption.addEventListener('change', this.inviteOptionChange.bind(this));\n            this.inviteOptionChange();\n        }\n    }\n\n    inviteOptionChange() {\n        const inviting = (this.inviteOption.value === 'true');\n        const passwordBoxes = this.container.querySelectorAll('input[type=password]');\n        for (const input of passwordBoxes) {\n            input.disabled = inviting;\n        }\n\n        this.inputContainer.style.display = inviting ? 'none' : 'block';\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/notification.js",
    "content": "import {Component} from './component';\n\nexport class Notification extends Component {\n\n    setup() {\n        this.container = this.$el;\n        this.type = this.$opts.type;\n        this.textElem = this.container.querySelector('span');\n        this.autoHide = this.$opts.autoHide === 'true';\n        this.initialShow = this.$opts.show === 'true';\n        this.container.style.display = 'grid';\n\n        window.$events.listen(this.type, text => {\n            this.show(text);\n        });\n        this.container.addEventListener('click', this.hide.bind(this));\n\n        if (this.initialShow) {\n            setTimeout(() => this.show(this.textElem.textContent), 100);\n        }\n\n        this.hideCleanup = this.hideCleanup.bind(this);\n    }\n\n    show(textToShow = '') {\n        this.container.removeEventListener('transitionend', this.hideCleanup);\n        this.textElem.textContent = textToShow;\n        this.container.style.display = 'grid';\n        setTimeout(() => {\n            this.container.classList.add('showing');\n        }, 1);\n\n        if (this.autoHide) {\n            const words = textToShow.split(' ').length;\n            const timeToShow = Math.max(2000, 1000 + (250 * words));\n            setTimeout(this.hide.bind(this), timeToShow);\n        }\n    }\n\n    hide() {\n        this.container.classList.remove('showing');\n        this.container.addEventListener('transitionend', this.hideCleanup);\n    }\n\n    hideCleanup() {\n        this.container.style.display = 'none';\n        this.container.removeEventListener('transitionend', this.hideCleanup);\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/optional-input.js",
    "content": "import {onSelect} from '../services/dom.ts';\nimport {Component} from './component';\n\nexport class OptionalInput extends Component {\n\n    setup() {\n        this.removeButton = this.$refs.remove;\n        this.showButton = this.$refs.show;\n        this.input = this.$refs.input;\n        this.setupListeners();\n    }\n\n    setupListeners() {\n        onSelect(this.removeButton, () => {\n            this.input.value = '';\n            this.input.classList.add('hidden');\n            this.removeButton.classList.add('hidden');\n            this.showButton.classList.remove('hidden');\n        });\n\n        onSelect(this.showButton, () => {\n            this.input.classList.remove('hidden');\n            this.removeButton.classList.remove('hidden');\n            this.showButton.classList.add('hidden');\n        });\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/page-comment-reference.ts",
    "content": "import {Component} from \"./component\";\nimport {findTargetNodeAndOffset, hashElement} from \"../services/dom\";\nimport {el} from \"../wysiwyg/utils/dom\";\nimport commentIcon from \"@icons/comment.svg\";\nimport closeIcon from \"@icons/close.svg\";\nimport {debounce, scrollAndHighlightElement} from \"../services/util\";\nimport {EditorToolboxChangeEventData} from \"./editor-toolbox\";\nimport {TabsChangeEvent} from \"./tabs\";\n\n/**\n * Track the close function for the current open marker so it can be closed\n * when another is opened so we only show one marker comment thread at one time.\n */\nlet openMarkerClose: Function|null = null;\n\nexport class PageCommentReference extends Component {\n    protected link!: HTMLLinkElement;\n    protected reference!: string;\n    protected markerWrap: HTMLElement|null = null;\n\n    protected viewCommentText!: string;\n    protected jumpToThreadText!: string;\n    protected closeText!: string;\n\n    setup() {\n        this.link = this.$el as HTMLLinkElement;\n        this.reference = this.$opts.reference;\n        this.viewCommentText = this.$opts.viewCommentText;\n        this.jumpToThreadText = this.$opts.jumpToThreadText;\n        this.closeText = this.$opts.closeText;\n\n        // Show within page display area if seen\n        this.showForDisplay();\n\n        // Handle editor view to show on comments toolbox view\n        window.addEventListener('editor-toolbox-change', ((event: CustomEvent<EditorToolboxChangeEventData>) => {\n            const tabName: string = event.detail.tab;\n            const isOpen = event.detail.open;\n            if (tabName === 'comments' && isOpen && this.link.checkVisibility()) {\n                this.showForEditor();\n            } else {\n                this.hideMarker();\n            }\n        }) as EventListener);\n\n        // Handle visibility changes within editor toolbox archived details dropdown\n        window.addEventListener('toggle', event => {\n            if (event.target instanceof HTMLElement && event.target.contains(this.link)) {\n                window.requestAnimationFrame(() => {\n                    if (this.link.checkVisibility()) {\n                        this.showForEditor();\n                    } else {\n                        this.hideMarker();\n                    }\n                });\n            }\n        }, {capture: true});\n\n        // Handle comments tab changes to hide/show markers & indicators\n        window.addEventListener('tabs-change', ((event: CustomEvent<TabsChangeEvent>) => {\n            const sectionId = event.detail.showing;\n            if (!sectionId.startsWith('comment-tab-panel')) {\n                return;\n            }\n\n            const panel = document.getElementById(sectionId);\n            if (panel?.contains(this.link)) {\n                this.showForDisplay();\n            } else {\n                this.hideMarker();\n            }\n        }) as EventListener);\n    }\n\n    public showForDisplay() {\n        const pageContentArea = document.querySelector('.page-content');\n        if (pageContentArea instanceof HTMLElement && this.link.checkVisibility()) {\n            this.updateMarker(pageContentArea);\n        }\n    }\n\n    protected showForEditor() {\n        const contentWrap = document.querySelector('.editor-content-wrap');\n        if (contentWrap instanceof HTMLElement) {\n            this.updateMarker(contentWrap);\n        }\n\n        const onChange = () => {\n            this.hideMarker();\n            setTimeout(() => {\n                window.$events.remove('editor-html-change', onChange);\n            }, 1);\n        };\n\n        window.$events.listen('editor-html-change', onChange);\n    }\n\n    protected updateMarker(contentContainer: HTMLElement) {\n        // Reset link and existing marker\n        this.link.classList.remove('outdated', 'missing');\n        if (this.markerWrap) {\n            this.markerWrap.remove();\n        }\n\n        const [refId, refHash, refRange] = this.reference.split(':');\n        const refEl = document.getElementById(refId);\n        if (!refEl) {\n            this.link.classList.add('outdated', 'missing');\n            return;\n        }\n\n        const actualHash = hashElement(refEl);\n        if (actualHash !== refHash) {\n            this.link.classList.add('outdated');\n        }\n\n        const marker = el('button', {\n            type: 'button',\n            class: 'content-comment-marker',\n            title: this.viewCommentText,\n        });\n        marker.innerHTML = <string>commentIcon;\n        marker.addEventListener('click', event => {\n            this.showCommentAtMarker(marker);\n        });\n\n        this.markerWrap = el('div', {\n            class: 'content-comment-highlight',\n        }, [marker]);\n\n        contentContainer.append(this.markerWrap);\n        this.positionMarker(refEl, refRange);\n\n        this.link.href = `#${refEl.id}`;\n        this.link.addEventListener('click', (event: MouseEvent) => {\n            event.preventDefault();\n            scrollAndHighlightElement(refEl);\n        });\n\n        const debouncedReposition = debounce(() => {\n            this.positionMarker(refEl, refRange);\n        }, 50, false).bind(this);\n        window.addEventListener('resize', debouncedReposition);\n    }\n\n    protected positionMarker(targetEl: HTMLElement, range: string) {\n        if (!this.markerWrap) {\n            return;\n        }\n\n        const markerParent = this.markerWrap.parentElement as HTMLElement;\n        const parentBounds = markerParent.getBoundingClientRect();\n        let targetBounds = targetEl.getBoundingClientRect();\n        const [rangeStart, rangeEnd] = range.split('-');\n        if (rangeStart && rangeEnd) {\n            const range = new Range();\n            const relStart = findTargetNodeAndOffset(targetEl, Number(rangeStart));\n            const relEnd = findTargetNodeAndOffset(targetEl, Number(rangeEnd));\n            if (relStart && relEnd) {\n                range.setStart(relStart.node, relStart.offset);\n                range.setEnd(relEnd.node, relEnd.offset);\n                targetBounds = range.getBoundingClientRect();\n            }\n        }\n\n        const relLeft = targetBounds.left - parentBounds.left;\n        const relTop = (targetBounds.top - parentBounds.top) + markerParent.scrollTop;\n\n        this.markerWrap.style.left = `${relLeft}px`;\n        this.markerWrap.style.top = `${relTop}px`;\n        this.markerWrap.style.width = `${targetBounds.width}px`;\n        this.markerWrap.style.height = `${targetBounds.height}px`;\n    }\n\n    public hideMarker() {\n        // Hide marker and close existing marker windows\n        if (openMarkerClose) {\n            openMarkerClose();\n        }\n        this.markerWrap?.remove();\n        this.markerWrap = null;\n    }\n\n    protected showCommentAtMarker(marker: HTMLElement): void {\n        // Hide marker and close existing marker windows\n        if (openMarkerClose) {\n            openMarkerClose();\n        }\n        marker.hidden = true;\n\n        // Locate relevant comment\n        const commentBox = this.link.closest('.comment-box') as HTMLElement;\n\n        // Build comment window\n        const readClone = (commentBox.closest('.comment-branch') as HTMLElement).cloneNode(true) as HTMLElement;\n        const toRemove = readClone.querySelectorAll('.actions, form');\n        for (const el of toRemove) {\n            el.remove();\n        }\n\n        const close = el('button', {type: 'button', title: this.closeText});\n        close.innerHTML = (closeIcon as string);\n        const jump = el('button', {type: 'button', 'data-action': 'jump'}, [this.jumpToThreadText]);\n\n        const commentWindow = el('div', {\n            class: 'content-comment-window'\n        }, [\n            el('div', {\n                class: 'content-comment-window-actions',\n            }, [jump, close]),\n            el('div', {\n                class: 'content-comment-window-content comment-container-compact comment-container-super-compact',\n            }, [readClone]),\n        ]);\n\n        marker.parentElement?.append(commentWindow);\n\n        // Handle interaction within window\n        const closeAction = () => {\n            commentWindow.remove();\n            marker.hidden = false;\n            window.removeEventListener('click', windowCloseAction);\n            openMarkerClose = null;\n        };\n\n        const windowCloseAction = (event: MouseEvent) => {\n            if (!(marker.parentElement as HTMLElement).contains(event.target as HTMLElement)) {\n                closeAction();\n            }\n        };\n        window.addEventListener('click', windowCloseAction);\n\n        openMarkerClose = closeAction;\n        close.addEventListener('click', closeAction.bind(this));\n        jump.addEventListener('click', () => {\n            closeAction();\n            commentBox.scrollIntoView({behavior: 'smooth'});\n            const highlightTarget = commentBox.querySelector('.header') as HTMLElement;\n            highlightTarget.classList.add('anim-highlight');\n            highlightTarget.addEventListener('animationend', () => highlightTarget.classList.remove('anim-highlight'))\n        });\n\n        // Position window within bounds\n        const commentWindowBounds = commentWindow.getBoundingClientRect();\n        const contentBounds = document.querySelector('.page-content')?.getBoundingClientRect();\n        if (contentBounds && commentWindowBounds.right > contentBounds.right) {\n            const diff = commentWindowBounds.right - contentBounds.right;\n            commentWindow.style.left = `-${diff}px`;\n        }\n    }\n}"
  },
  {
    "path": "resources/js/components/page-comment.ts",
    "content": "import {Component} from './component';\nimport {getLoading, htmlToDom} from '../services/dom';\nimport {PageCommentReference} from \"./page-comment-reference\";\nimport {HttpError} from \"../services/http\";\nimport {createCommentEditorInstance, SimpleWysiwygEditorInterface} from \"../wysiwyg\";\nimport {el} from \"../wysiwyg/utils/dom\";\n\nexport interface PageCommentReplyEventData {\n    id: string; // ID of comment being replied to\n    element: HTMLElement; // Container for comment replied to\n}\n\nexport interface PageCommentArchiveEventData {\n    new_thread_dom: HTMLElement;\n}\n\nexport class PageComment extends Component {\n\n    protected commentId!: string;\n    protected commentLocalId!: string;\n    protected deletedText!: string;\n    protected updatedText!: string;\n    protected archiveText!: string;\n\n    protected wysiwygEditor: SimpleWysiwygEditorInterface|null = null;\n    protected wysiwygTextDirection!: string;\n\n    protected container!: HTMLElement;\n    protected contentContainer!: HTMLElement;\n    protected form!: HTMLFormElement;\n    protected formCancel!: HTMLElement;\n    protected editButton!: HTMLElement;\n    protected deleteButton!: HTMLElement;\n    protected replyButton!: HTMLElement;\n    protected archiveButton!: HTMLElement;\n    protected input!: HTMLInputElement;\n\n    setup() {\n        // Options\n        this.commentId = this.$opts.commentId;\n        this.commentLocalId = this.$opts.commentLocalId;\n        this.deletedText = this.$opts.deletedText;\n        this.updatedText = this.$opts.updatedText;\n        this.archiveText = this.$opts.archiveText;\n\n        // Editor reference and text options\n        this.wysiwygTextDirection = this.$opts.wysiwygTextDirection;\n\n        // Element references\n        this.container = this.$el;\n        this.contentContainer = this.$refs.contentContainer;\n        this.form = this.$refs.form as HTMLFormElement;\n        this.formCancel = this.$refs.formCancel;\n        this.editButton = this.$refs.editButton;\n        this.deleteButton = this.$refs.deleteButton;\n        this.replyButton = this.$refs.replyButton;\n        this.archiveButton = this.$refs.archiveButton;\n        this.input = this.$refs.input as HTMLInputElement;\n\n        this.setupListeners();\n    }\n\n    protected setupListeners(): void {\n        if (this.replyButton) {\n            const data: PageCommentReplyEventData = {\n                id: this.commentLocalId,\n                element: this.container,\n            };\n            this.replyButton.addEventListener('click', () => this.$emit('reply', data));\n        }\n\n        if (this.editButton) {\n            this.editButton.addEventListener('click', this.startEdit.bind(this));\n            this.form.addEventListener('submit', this.update.bind(this));\n            this.formCancel.addEventListener('click', () => this.toggleEditMode(false));\n        }\n\n        if (this.deleteButton) {\n            this.deleteButton.addEventListener('click', this.delete.bind(this));\n        }\n\n        if (this.archiveButton) {\n            this.archiveButton.addEventListener('click', this.archive.bind(this));\n        }\n    }\n\n    protected toggleEditMode(show: boolean) : void {\n        this.contentContainer.toggleAttribute('hidden', show);\n        this.form.toggleAttribute('hidden', !show);\n    }\n\n    protected async startEdit(): Promise<void> {\n        this.toggleEditMode(true);\n\n        if (this.wysiwygEditor) {\n            this.wysiwygEditor.focus();\n            return;\n        }\n\n        type WysiwygModule = typeof import('../wysiwyg');\n        const wysiwygModule = (await window.importVersioned('wysiwyg')) as WysiwygModule;\n        const editorContent = this.input.value;\n        const container = el('div', {class: 'comment-editor-container'});\n        this.input.parentElement?.appendChild(container);\n        this.input.hidden = true;\n\n        this.wysiwygEditor = wysiwygModule.createCommentEditorInstance(container as HTMLElement, editorContent, {\n            darkMode: document.documentElement.classList.contains('dark-mode'),\n            textDirection: this.$opts.textDirection,\n            translations: (window as unknown as Record<string, Object>).editor_translations,\n        });\n\n        this.wysiwygEditor.focus();\n    }\n\n    protected async update(event: Event): Promise<void> {\n        event.preventDefault();\n        const loading = this.showLoading();\n        this.form.toggleAttribute('hidden', true);\n\n        const reqData = {\n            html: await this.wysiwygEditor?.getContentAsHtml() || '',\n        };\n\n        try {\n            const resp = await window.$http.put(`/comment/${this.commentId}`, reqData);\n            const newComment = htmlToDom(resp.data as string);\n            this.container.replaceWith(newComment);\n            window.$events.success(this.updatedText);\n        } catch (err) {\n            console.error(err);\n            if (err instanceof HttpError) {\n                window.$events.showValidationErrors(err);\n            }\n            this.form.toggleAttribute('hidden', false);\n            loading.remove();\n        }\n    }\n\n    protected async delete(): Promise<void> {\n        this.showLoading();\n\n        await window.$http.delete(`/comment/${this.commentId}`);\n        this.$emit('delete');\n\n        const branch = this.container.closest('.comment-branch');\n        if (branch instanceof HTMLElement) {\n            const refs = window.$components.allWithinElement<PageCommentReference>(branch, 'page-comment-reference');\n            for (const ref of refs) {\n                ref.hideMarker();\n            }\n            branch.remove();\n        }\n\n        window.$events.success(this.deletedText);\n    }\n\n    protected async archive(): Promise<void> {\n        this.showLoading();\n        const isArchived = this.archiveButton.dataset.isArchived === 'true';\n        const action = isArchived ? 'unarchive' : 'archive';\n\n        const response = await window.$http.put(`/comment/${this.commentId}/${action}`);\n        window.$events.success(this.archiveText);\n        const eventData: PageCommentArchiveEventData = {new_thread_dom: htmlToDom(response.data as string)};\n        this.$emit(action, eventData);\n\n        const branch = this.container.closest('.comment-branch') as HTMLElement;\n        const references = window.$components.allWithinElement<PageCommentReference>(branch, 'page-comment-reference');\n        for (const reference of references) {\n            reference.hideMarker();\n        }\n        branch.remove();\n    }\n\n    protected showLoading(): HTMLElement {\n        const loading = getLoading();\n        loading.classList.add('px-l');\n        this.container.append(loading);\n        return loading;\n    }\n}\n"
  },
  {
    "path": "resources/js/components/page-comments.ts",
    "content": "import {Component} from './component';\nimport {getLoading, htmlToDom} from '../services/dom';\nimport {Tabs} from \"./tabs\";\nimport {PageCommentReference} from \"./page-comment-reference\";\nimport {scrollAndHighlightElement} from \"../services/util\";\nimport {PageCommentArchiveEventData, PageCommentReplyEventData} from \"./page-comment\";\nimport {el} from \"../wysiwyg/utils/dom\";\nimport {createCommentEditorInstance, SimpleWysiwygEditorInterface} from \"../wysiwyg\";\n\nexport class PageComments extends Component {\n\n    private elem!: HTMLElement;\n    private pageId!: number;\n    private container!: HTMLElement;\n    private commentCountBar!: HTMLElement;\n    private activeTab!: HTMLElement;\n    private archivedTab!: HTMLElement;\n    private addButtonContainer!: HTMLElement;\n    private archiveContainer!: HTMLElement;\n    private activeContainer!: HTMLElement;\n    private replyToRow!: HTMLElement;\n    private referenceRow!: HTMLElement;\n    private formContainer!: HTMLElement;\n    private form!: HTMLFormElement;\n    private formInput!: HTMLInputElement;\n    private formReplyLink!: HTMLAnchorElement;\n    private formReferenceLink!: HTMLAnchorElement;\n    private addCommentButton!: HTMLElement;\n    private hideFormButton!: HTMLElement;\n    private removeReplyToButton!: HTMLElement;\n    private removeReferenceButton!: HTMLElement;\n    private wysiwygTextDirection!: string;\n    private wysiwygEditor: SimpleWysiwygEditorInterface|null = null;\n    private createdText!: string;\n    private countText!: string;\n    private archivedCountText!: string;\n    private parentId: number | null = null;\n    private contentReference: string = '';\n    private formReplyText: string = '';\n\n    setup() {\n        this.elem = this.$el;\n        this.pageId = Number(this.$opts.pageId);\n\n        // Element references\n        this.container = this.$refs.commentContainer;\n        this.commentCountBar = this.$refs.commentCountBar;\n        this.activeTab = this.$refs.activeTab;\n        this.archivedTab = this.$refs.archivedTab;\n        this.addButtonContainer = this.$refs.addButtonContainer;\n        this.archiveContainer = this.$refs.archiveContainer;\n        this.activeContainer = this.$refs.activeContainer;\n        this.replyToRow = this.$refs.replyToRow;\n        this.referenceRow = this.$refs.referenceRow;\n        this.formContainer = this.$refs.formContainer;\n        this.form = this.$refs.form as HTMLFormElement;\n        this.formInput = this.$refs.formInput as HTMLInputElement;\n        this.formReplyLink = this.$refs.formReplyLink as HTMLAnchorElement;\n        this.formReferenceLink = this.$refs.formReferenceLink as HTMLAnchorElement;\n        this.addCommentButton = this.$refs.addCommentButton;\n        this.hideFormButton = this.$refs.hideFormButton;\n        this.removeReplyToButton = this.$refs.removeReplyToButton;\n        this.removeReferenceButton = this.$refs.removeReferenceButton;\n\n        // WYSIWYG options\n        this.wysiwygTextDirection = this.$opts.wysiwygTextDirection;\n\n        // Translations\n        this.createdText = this.$opts.createdText;\n        this.countText = this.$opts.countText;\n        this.archivedCountText = this.$opts.archivedCountText;\n\n        this.formReplyText = this.formReplyLink?.textContent || '';\n\n        this.setupListeners();\n    }\n\n    protected setupListeners(): void {\n        this.elem.addEventListener('page-comment-delete', () => {\n            setTimeout(() => {\n                this.updateCount();\n                this.hideForm();\n            }, 1);\n        });\n\n        this.elem.addEventListener('page-comment-reply', ((event: CustomEvent<PageCommentReplyEventData>) => {\n            this.setReply(event.detail.id, event.detail.element);\n        }) as EventListener);\n\n        this.elem.addEventListener('page-comment-archive', ((event: CustomEvent<PageCommentArchiveEventData>) => {\n            this.archiveContainer.append(event.detail.new_thread_dom);\n            setTimeout(() => this.updateCount(), 1);\n        }) as EventListener);\n\n        this.elem.addEventListener('page-comment-unarchive', ((event: CustomEvent<PageCommentArchiveEventData>) => {\n            this.container.append(event.detail.new_thread_dom);\n            setTimeout(() => this.updateCount(), 1);\n        }) as EventListener);\n\n        if (this.form) {\n            this.removeReplyToButton.addEventListener('click', this.removeReplyTo.bind(this));\n            this.removeReferenceButton.addEventListener('click', () => this.setContentReference(''));\n            this.hideFormButton.addEventListener('click', this.hideForm.bind(this));\n            this.addCommentButton.addEventListener('click', this.showForm.bind(this));\n            this.form.addEventListener('submit', this.saveComment.bind(this));\n        }\n    }\n\n    protected async saveComment(event: SubmitEvent): Promise<void> {\n        event.preventDefault();\n        event.stopPropagation();\n\n        const loading = getLoading();\n        loading.classList.add('px-l');\n        this.form.after(loading);\n        this.form.toggleAttribute('hidden', true);\n\n        const reqData = {\n            html: (await this.wysiwygEditor?.getContentAsHtml()) || '',\n            parent_id: this.parentId || null,\n            content_ref: this.contentReference,\n        };\n\n        window.$http.post(`/comment/${this.pageId}`, reqData).then(resp => {\n            const newElem = htmlToDom(resp.data as string);\n\n            if (reqData.parent_id) {\n                this.formContainer.after(newElem);\n            } else {\n                this.container.append(newElem);\n            }\n\n            const refs = window.$components.allWithinElement<PageCommentReference>(newElem, 'page-comment-reference');\n            for (const ref of refs) {\n                ref.showForDisplay();\n            }\n\n            window.$events.success(this.createdText);\n            this.hideForm();\n            this.updateCount();\n        }).catch(err => {\n            this.form.toggleAttribute('hidden', false);\n            window.$events.showValidationErrors(err);\n        });\n\n        this.form.toggleAttribute('hidden', false);\n        loading.remove();\n    }\n\n    protected updateCount(): void {\n        const activeCount = this.getActiveThreadCount();\n        this.activeTab.textContent = window.$trans.choice(this.countText, activeCount);\n        const archivedCount = this.getArchivedThreadCount();\n        this.archivedTab.textContent = window.$trans.choice(this.archivedCountText, archivedCount);\n    }\n\n    protected resetForm(): void {\n        this.removeEditor();\n        this.formInput.value = '';\n        this.parentId = null;\n        this.replyToRow.toggleAttribute('hidden', true);\n        this.container.append(this.formContainer);\n        this.setContentReference('');\n    }\n\n    protected showForm(): void {\n        this.removeEditor();\n        this.formContainer.toggleAttribute('hidden', false);\n        this.addButtonContainer.toggleAttribute('hidden', true);\n        this.formContainer.scrollIntoView({behavior: 'smooth', block: 'nearest'});\n        this.loadEditor();\n\n        // Ensure the active comments tab is displaying if that's where we're showing the form\n        const tabs = window.$components.firstOnElement(this.elem, 'tabs');\n        if (tabs instanceof Tabs && this.formContainer.closest('#comment-tab-panel-active')) {\n            tabs.show('comment-tab-panel-active');\n        }\n    }\n\n    protected hideForm(): void {\n        this.resetForm();\n        this.formContainer.toggleAttribute('hidden', true);\n        if (this.getActiveThreadCount() > 0) {\n            this.activeContainer.append(this.addButtonContainer);\n        } else {\n            this.commentCountBar.append(this.addButtonContainer);\n        }\n        this.addButtonContainer.toggleAttribute('hidden', false);\n    }\n\n    protected async loadEditor(): Promise<void> {\n        if (this.wysiwygEditor) {\n            this.wysiwygEditor.focus();\n            return;\n        }\n\n        type WysiwygModule = typeof import('../wysiwyg');\n        const wysiwygModule = (await window.importVersioned('wysiwyg')) as WysiwygModule;\n        const container = el('div', {class: 'comment-editor-container'});\n        this.formInput.parentElement?.appendChild(container);\n        this.formInput.hidden = true;\n\n        this.wysiwygEditor = wysiwygModule.createCommentEditorInstance(container as HTMLElement, '<p></p>', {\n            darkMode: document.documentElement.classList.contains('dark-mode'),\n            textDirection: this.wysiwygTextDirection,\n            translations: (window as unknown as Record<string, Object>).editor_translations,\n        });\n\n        this.wysiwygEditor.focus();\n    }\n\n    protected removeEditor(): void {\n        if (this.wysiwygEditor) {\n            this.wysiwygEditor.remove();\n            this.wysiwygEditor = null;\n        }\n    }\n\n    protected getActiveThreadCount(): number {\n        return this.container.querySelectorAll(':scope > .comment-branch:not([hidden])').length;\n    }\n\n    protected getArchivedThreadCount(): number {\n        return this.archiveContainer.querySelectorAll(':scope > .comment-branch').length;\n    }\n\n    protected setReply(commentLocalId: string, commentElement: HTMLElement): void {\n        const targetFormLocation = (commentElement.closest('.comment-branch') as HTMLElement).querySelector('.comment-branch-children') as HTMLElement;\n        targetFormLocation.append(this.formContainer);\n        this.showForm();\n        this.parentId = Number(commentLocalId);\n        this.replyToRow.toggleAttribute('hidden', false);\n        this.formReplyLink.textContent = this.formReplyText.replace('1234', String(this.parentId));\n        this.formReplyLink.href = `#comment${this.parentId}`;\n    }\n\n    protected removeReplyTo(): void {\n        this.parentId = null;\n        this.replyToRow.toggleAttribute('hidden', true);\n        this.container.append(this.formContainer);\n        this.showForm();\n    }\n\n    public startNewComment(contentReference: string): void {\n        this.resetForm();\n        this.showForm();\n        this.setContentReference(contentReference);\n    }\n\n    protected setContentReference(reference: string): void {\n        this.contentReference = reference;\n        this.referenceRow.toggleAttribute('hidden', !Boolean(reference));\n        const [id] = reference.split(':');\n        this.formReferenceLink.href = `#${id}`;\n        this.formReferenceLink.onclick = function(event) {\n            event.preventDefault();\n            const el = document.getElementById(id);\n            if (el) {\n                scrollAndHighlightElement(el);\n            }\n        };\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/page-display.js",
    "content": "import * as DOM from '../services/dom.ts';\nimport {scrollAndHighlightElement} from '../services/util.ts';\nimport {Component} from './component';\n\nfunction toggleAnchorHighlighting(elementId, shouldHighlight) {\n    DOM.forEach(`#page-navigation a[href=\"#${elementId}\"]`, anchor => {\n        anchor.closest('li').classList.toggle('current-heading', shouldHighlight);\n    });\n}\n\nfunction headingVisibilityChange(entries) {\n    for (const entry of entries) {\n        const isVisible = (entry.intersectionRatio === 1);\n        toggleAnchorHighlighting(entry.target.id, isVisible);\n    }\n}\n\nfunction addNavObserver(headings) {\n    // Setup the intersection observer.\n    const intersectOpts = {\n        rootMargin: '0px 0px 0px 0px',\n        threshold: 1.0,\n    };\n    const pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);\n\n    // observe each heading\n    for (const heading of headings) {\n        pageNavObserver.observe(heading);\n    }\n}\n\nexport class PageDisplay extends Component {\n\n    setup() {\n        this.container = this.$el;\n        this.pageId = this.$opts.pageId;\n\n        window.importVersioned('code').then(Code => Code.highlight());\n        this.setupNavHighlighting();\n\n        // Check the hash on load\n        if (window.location.hash) {\n            const text = window.location.hash.replace(/%20/g, ' ').substring(1);\n            this.goToText(text);\n        }\n\n        // Sidebar page nav click event\n        const sidebarPageNav = document.querySelector('.sidebar-page-nav');\n        if (sidebarPageNav) {\n            DOM.onChildEvent(sidebarPageNav, 'a', 'click', (event, child) => {\n                event.preventDefault();\n                window.$components.first('tri-layout').showContent();\n                const contentId = child.getAttribute('href').substr(1);\n                this.goToText(contentId);\n                window.history.pushState(null, null, `#${contentId}`);\n            });\n        }\n    }\n\n    goToText(text) {\n        const idElem = document.getElementById(text);\n\n        DOM.forEach('.page-content [data-highlighted]', elem => {\n            elem.removeAttribute('data-highlighted');\n            elem.style.backgroundColor = null;\n        });\n\n        if (idElem !== null) {\n            scrollAndHighlightElement(idElem);\n        } else {\n            const textElem = DOM.findText('.page-content > div > *', text);\n            if (textElem) {\n                scrollAndHighlightElement(textElem);\n            }\n        }\n    }\n\n    setupNavHighlighting() {\n        const pageNav = document.querySelector('.sidebar-page-nav');\n\n        // fetch all the headings.\n        const headings = document.querySelector('.page-content').querySelectorAll('h1, h2, h3, h4, h5, h6');\n        // if headings are present, add observers.\n        if (headings.length > 0 && pageNav !== null) {\n            addNavObserver(headings);\n        }\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/page-editor.js",
    "content": "import {onSelect} from '../services/dom.ts';\nimport {debounce} from '../services/util.ts';\nimport {Component} from './component';\nimport {utcTimeStampToLocalTime} from '../services/dates.ts';\n\nexport class PageEditor extends Component {\n\n    setup() {\n        // Options\n        this.draftsEnabled = this.$opts.draftsEnabled === 'true';\n        this.editorType = this.$opts.editorType;\n        this.pageId = Number(this.$opts.pageId);\n        this.isNewDraft = this.$opts.pageNewDraft === 'true';\n        this.hasDefaultTitle = this.$opts.hasDefaultTitle || false;\n\n        // Elements\n        this.container = this.$el;\n        this.titleElem = this.$refs.titleContainer.querySelector('input');\n        this.saveDraftButton = this.$refs.saveDraft;\n        this.discardDraftButton = this.$refs.discardDraft;\n        this.discardDraftWrap = this.$refs.discardDraftWrap;\n        this.deleteDraftButton = this.$refs.deleteDraft;\n        this.deleteDraftWrap = this.$refs.deleteDraftWrap;\n        this.draftDisplay = this.$refs.draftDisplay;\n        this.draftDisplayIcon = this.$refs.draftDisplayIcon;\n        this.changelogInput = this.$refs.changelogInput;\n        this.changelogDisplay = this.$refs.changelogDisplay;\n        this.changelogCounter = this.$refs.changelogCounter;\n        this.changeEditorButtons = this.$manyRefs.changeEditor || [];\n        this.switchDialogContainer = this.$refs.switchDialog;\n        this.deleteDraftDialogContainer = this.$refs.deleteDraftDialog;\n\n        // Translations\n        this.draftText = this.$opts.draftText;\n        this.autosaveFailText = this.$opts.autosaveFailText;\n        this.editingPageText = this.$opts.editingPageText;\n        this.draftDiscardedText = this.$opts.draftDiscardedText;\n        this.draftDeleteText = this.$opts.draftDeleteText;\n        this.draftDeleteFailText = this.$opts.draftDeleteFailText;\n        this.setChangelogText = this.$opts.setChangelogText;\n\n        // State data\n        this.autoSave = {\n            interval: null,\n            frequency: 30000,\n            last: 0,\n            pendingChange: false,\n        };\n        this.shownWarningsCache = new Set();\n\n        if (this.pageId !== 0 && this.draftsEnabled) {\n            window.setTimeout(() => {\n                this.startAutoSave();\n            }, 1000);\n        }\n        this.draftDisplay.innerHTML = this.draftText;\n\n        this.setupListeners();\n        this.setInitialFocus();\n    }\n\n    setupListeners() {\n        // Listen to save events from editor\n        window.$events.listen('editor-save-draft', this.saveDraft.bind(this));\n        window.$events.listen('editor-save-page', this.savePage.bind(this));\n\n        // Listen to content changes from the editor\n        const onContentChange = () => {\n            this.autoSave.pendingChange = true;\n        };\n        window.$events.listen('editor-html-change', onContentChange);\n        window.$events.listen('editor-markdown-change', onContentChange);\n\n        // Listen to changes on the title input\n        this.titleElem.addEventListener('input', onContentChange);\n\n        // Changelog controls\n        const updateChangelogDebounced = debounce(this.updateChangelogDisplay.bind(this), 300, false);\n        this.changelogInput.addEventListener('input', () => {\n            const count = this.changelogInput.value.length;\n            this.changelogCounter.innerText = `${count} / 180`;\n            updateChangelogDebounced();\n        });\n\n        // Draft Controls\n        onSelect(this.saveDraftButton, this.saveDraft.bind(this));\n        onSelect(this.discardDraftButton, this.discardDraft.bind(this));\n        onSelect(this.deleteDraftButton, this.deleteDraft.bind(this));\n\n        // Change editor controls\n        onSelect(this.changeEditorButtons, this.changeEditor.bind(this));\n    }\n\n    setInitialFocus() {\n        if (this.hasDefaultTitle) {\n            this.titleElem.select();\n            return;\n        }\n\n        window.setTimeout(() => {\n            window.$events.emit('editor::focus', '');\n        }, 500);\n    }\n\n    startAutoSave() {\n        this.autoSave.interval = window.setInterval(this.runAutoSave.bind(this), this.autoSave.frequency);\n    }\n\n    runAutoSave() {\n        // Stop if manually saved recently to prevent bombarding the server\n        const savedRecently = (Date.now() - this.autoSave.last < (this.autoSave.frequency) / 2);\n        if (savedRecently || !this.autoSave.pendingChange) {\n            return;\n        }\n\n        this.saveDraft();\n    }\n\n    savePage() {\n        this.container.closest('form').requestSubmit();\n    }\n\n    async saveDraft() {\n        const data = {name: this.titleElem.value.trim()};\n\n        const editorContent = await this.getEditorComponent().getContent();\n        Object.assign(data, editorContent);\n\n        let didSave = false;\n        try {\n            const resp = await window.$http.put(`/ajax/page/${this.pageId}/save-draft`, data);\n            if (!this.isNewDraft) {\n                this.discardDraftWrap.toggleAttribute('hidden', false);\n                this.deleteDraftWrap.toggleAttribute('hidden', false);\n            }\n\n            this.draftNotifyChange(`${resp.data.message} ${utcTimeStampToLocalTime(resp.data.timestamp)}`);\n            this.autoSave.last = Date.now();\n            if (resp.data.warning && !this.shownWarningsCache.has(resp.data.warning)) {\n                window.$events.emit('warning', resp.data.warning);\n                this.shownWarningsCache.add(resp.data.warning);\n            }\n\n            didSave = true;\n            this.autoSave.pendingChange = false;\n        } catch {\n            // Save the editor content in LocalStorage as a last resort, just in case.\n            try {\n                const saveKey = `draft-save-fail-${(new Date()).toISOString()}`;\n                window.localStorage.setItem(saveKey, JSON.stringify(data));\n            } catch (lsErr) {\n                console.error(lsErr);\n            }\n\n            window.$events.emit('error', this.autosaveFailText);\n        }\n\n        return didSave;\n    }\n\n    draftNotifyChange(text) {\n        this.draftDisplay.innerText = text;\n        this.draftDisplayIcon.classList.add('visible');\n        window.setTimeout(() => {\n            this.draftDisplayIcon.classList.remove('visible');\n        }, 2000);\n    }\n\n    async discardDraft(notify = true) {\n        let response;\n        try {\n            response = await window.$http.get(`/ajax/page/${this.pageId}`);\n        } catch (e) {\n            console.error(e);\n            return;\n        }\n\n        if (this.autoSave.interval) {\n            window.clearInterval(this.autoSave.interval);\n        }\n\n        this.draftDisplay.innerText = this.editingPageText;\n        this.discardDraftWrap.toggleAttribute('hidden', true);\n        window.$events.emit('editor::replace', {\n            html: response.data.html,\n            markdown: response.data.markdown,\n        });\n\n        this.titleElem.value = response.data.name;\n        window.setTimeout(() => {\n            this.startAutoSave();\n        }, 1000);\n\n        if (notify) {\n            window.$events.success(this.draftDiscardedText);\n        }\n    }\n\n    async deleteDraft() {\n        /** @var {ConfirmDialog} * */\n        const dialog = window.$components.firstOnElement(this.deleteDraftDialogContainer, 'confirm-dialog');\n        const confirmed = await dialog.show();\n        if (!confirmed) {\n            return;\n        }\n\n        try {\n            const discard = this.discardDraft(false);\n            const draftDelete = window.$http.delete(`/page-revisions/user-drafts/${this.pageId}`);\n            await Promise.all([discard, draftDelete]);\n            window.$events.success(this.draftDeleteText);\n            this.deleteDraftWrap.toggleAttribute('hidden', true);\n        } catch (err) {\n            console.error(err);\n            window.$events.error(this.draftDeleteFailText);\n        }\n    }\n\n    updateChangelogDisplay() {\n        let summary = this.changelogInput.value.trim();\n        if (summary.length === 0) {\n            summary = this.setChangelogText;\n        } else if (summary.length > 16) {\n            summary = `${summary.slice(0, 16)}...`;\n        }\n        this.changelogDisplay.innerText = summary;\n    }\n\n    async changeEditor(event) {\n        event.preventDefault();\n\n        const link = event.target.closest('a').href;\n        /** @var {ConfirmDialog} * */\n        const dialog = window.$components.firstOnElement(this.switchDialogContainer, 'confirm-dialog');\n        const [saved, confirmed] = await Promise.all([this.saveDraft(), dialog.show()]);\n\n        if (saved && confirmed) {\n            window.location = link;\n        }\n    }\n\n    /**\n     * @return {MarkdownEditor|WysiwygEditor|WysiwygEditorTinymce}\n     */\n    getEditorComponent() {\n        return window.$components.first('markdown-editor')\n            || window.$components.first('wysiwyg-editor')\n            || window.$components.first('wysiwyg-editor-tinymce');\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/page-picker.js",
    "content": "import {Component} from './component';\n\nfunction toggleElem(elem, show) {\n    elem.toggleAttribute('hidden', !show);\n}\n\nexport class PagePicker extends Component {\n\n    setup() {\n        this.input = this.$refs.input;\n        this.resetButton = this.$refs.resetButton;\n        this.selectButton = this.$refs.selectButton;\n        this.display = this.$refs.display;\n        this.defaultDisplay = this.$refs.defaultDisplay;\n        this.buttonSep = this.$refs.buttonSeperator;\n\n        this.selectorEndpoint = this.$opts.selectorEndpoint;\n\n        this.value = this.input.value;\n        this.setupListeners();\n    }\n\n    setupListeners() {\n        this.selectButton.addEventListener('click', this.showPopup.bind(this));\n        this.display.parentElement.addEventListener('click', this.showPopup.bind(this));\n        this.display.addEventListener('click', e => e.stopPropagation());\n\n        this.resetButton.addEventListener('click', () => {\n            this.setValue('', '');\n        });\n    }\n\n    showPopup() {\n        /** @type {EntitySelectorPopup} * */\n        const selectorPopup = window.$components.first('entity-selector-popup');\n        selectorPopup.show(entity => {\n            this.setValue(entity.id, entity.name);\n        }, {\n            initialValue: '',\n            searchEndpoint: this.selectorEndpoint,\n            entityTypes: 'page',\n            entityPermission: 'view',\n        });\n    }\n\n    setValue(value, name) {\n        this.value = value;\n        this.input.value = value;\n        this.controlView(name);\n    }\n\n    controlView(name) {\n        const hasValue = this.value && this.value !== 0;\n        toggleElem(this.resetButton, hasValue);\n        toggleElem(this.buttonSep, hasValue);\n        toggleElem(this.defaultDisplay, !hasValue);\n        toggleElem(this.display, hasValue);\n        if (hasValue) {\n            const id = this.getAssetIdFromVal();\n            this.display.textContent = `#${id}, ${name}`;\n            this.display.href = window.baseUrl(`/link/${id}`);\n        }\n    }\n\n    getAssetIdFromVal() {\n        return Number(this.value);\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/permissions-table.js",
    "content": "import {Component} from './component';\n\nexport class PermissionsTable extends Component {\n\n    setup() {\n        this.container = this.$el;\n        this.cellSelector = this.$opts.cellSelector || 'td,th';\n        this.rowSelector = this.$opts.rowSelector || 'tr';\n\n        // Handle toggle all event\n        for (const toggleAllElem of (this.$manyRefs.toggleAll || [])) {\n            toggleAllElem.addEventListener('click', this.toggleAllClick.bind(this));\n        }\n\n        // Handle toggle row event\n        for (const toggleRowElem of (this.$manyRefs.toggleRow || [])) {\n            toggleRowElem.addEventListener('click', this.toggleRowClick.bind(this));\n        }\n\n        // Handle toggle column event\n        for (const toggleColElem of (this.$manyRefs.toggleColumn || [])) {\n            toggleColElem.addEventListener('click', this.toggleColumnClick.bind(this));\n        }\n    }\n\n    toggleAllClick(event) {\n        event.preventDefault();\n        this.toggleAllInElement(this.container);\n    }\n\n    toggleRowClick(event) {\n        event.preventDefault();\n        this.toggleAllInElement(event.target.closest(this.rowSelector));\n    }\n\n    toggleColumnClick(event) {\n        event.preventDefault();\n\n        const tableCell = event.target.closest(this.cellSelector);\n        const colIndex = Array.from(tableCell.parentElement.children).indexOf(tableCell);\n        const tableRows = this.container.querySelectorAll(this.rowSelector);\n        const inputsToToggle = [];\n\n        for (const row of tableRows) {\n            const targetCell = row.children[colIndex];\n            if (targetCell) {\n                inputsToToggle.push(...targetCell.querySelectorAll('input[type=checkbox]'));\n            }\n        }\n        this.toggleAllInputs(inputsToToggle);\n    }\n\n    toggleAllInElement(domElem) {\n        const inputsToToggle = domElem.querySelectorAll('input[type=checkbox]');\n        this.toggleAllInputs(inputsToToggle);\n    }\n\n    toggleAllInputs(inputsToToggle) {\n        const currentState = inputsToToggle.length > 0 ? inputsToToggle[0].checked : false;\n        for (const checkbox of inputsToToggle) {\n            checkbox.checked = !currentState;\n            checkbox.dispatchEvent(new Event('change'));\n        }\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/pointer.ts",
    "content": "import * as DOM from '../services/dom';\nimport {Component} from './component';\nimport {copyTextToClipboard} from '../services/clipboard';\nimport {hashElement, normalizeNodeTextOffsetToParent} from \"../services/dom\";\nimport {PageComments} from \"./page-comments\";\n\nexport class Pointer extends Component {\n\n    protected showing: boolean = false;\n    protected isMakingSelection: boolean = false;\n    protected targetElement: HTMLElement|null = null;\n    protected targetSelectionRange: Range|null = null;\n\n    protected pointer!: HTMLElement;\n    protected linkInput!: HTMLInputElement;\n    protected linkButton!: HTMLElement;\n    protected includeInput!: HTMLInputElement;\n    protected includeButton!: HTMLElement;\n    protected sectionModeButton!: HTMLElement;\n    protected commentButton!: HTMLElement;\n    protected modeToggles!: HTMLElement[];\n    protected modeSections!: HTMLElement[];\n    protected pageId!: string;\n\n    setup() {\n        this.pointer = this.$refs.pointer;\n        this.linkInput = this.$refs.linkInput as HTMLInputElement;\n        this.linkButton = this.$refs.linkButton;\n        this.includeInput = this.$refs.includeInput as HTMLInputElement;\n        this.includeButton = this.$refs.includeButton;\n        this.sectionModeButton = this.$refs.sectionModeButton;\n        this.commentButton = this.$refs.commentButton;\n        this.modeToggles = this.$manyRefs.modeToggle;\n        this.modeSections = this.$manyRefs.modeSection;\n        this.pageId = this.$opts.pageId;\n\n        this.setupListeners();\n    }\n\n    setupListeners() {\n        // Copy on copy button click\n        this.includeButton.addEventListener('click', () => copyTextToClipboard(this.includeInput.value));\n        this.linkButton.addEventListener('click', () => copyTextToClipboard(this.linkInput.value));\n\n        // Select all contents on input click\n        DOM.onSelect([this.includeInput, this.linkInput], event => {\n            (event.target as HTMLInputElement).select();\n            event.stopPropagation();\n        });\n\n        // Prevent closing pointer when clicked or focused\n        DOM.onEvents(this.pointer, ['click', 'focus'], event => {\n            event.stopPropagation();\n        });\n\n        // Hide pointer when clicking away\n        DOM.onEvents(document.body, ['click', 'focus'], () => {\n            if (!this.showing || this.isMakingSelection) return;\n            this.hidePointer();\n        });\n\n        // Hide pointer on escape press\n        DOM.onEscapePress(this.pointer, this.hidePointer.bind(this));\n\n        // Show pointer when selecting a single block of tagged content\n        const pageContent = document.querySelector('.page-content');\n        DOM.onEvents(pageContent, ['mouseup', 'keyup'], event => {\n            event.stopPropagation();\n            const targetEl = (event.target as HTMLElement).closest('[id^=\"bkmrk\"]');\n            if (targetEl instanceof HTMLElement && (window.getSelection() || '').toString().length > 0) {\n                const xPos = (event instanceof MouseEvent) ? event.pageX : 0;\n                this.showPointerAtTarget(targetEl, xPos, false);\n            }\n        });\n\n        // Start section selection mode on button press\n        DOM.onSelect(this.sectionModeButton, this.enterSectionSelectMode.bind(this));\n\n        // Toggle between pointer modes\n        DOM.onSelect(this.modeToggles, event => {\n            const targetToggle = (event.target as HTMLElement);\n            for (const section of this.modeSections) {\n                const show = !section.contains(targetToggle);\n                section.toggleAttribute('hidden', !show);\n            }\n\n            const otherToggle = this.modeToggles.find(b => b !== targetToggle);\n            otherToggle && otherToggle.focus();\n        });\n\n        if (this.commentButton) {\n            DOM.onSelect(this.commentButton, this.createCommentAtPointer.bind(this));\n        }\n    }\n\n    hidePointer() {\n        this.pointer.style.removeProperty('display');\n        this.showing = false;\n        this.targetElement = null;\n        this.targetSelectionRange = null;\n    }\n\n    /**\n     * Move and display the pointer at the given element, targeting the given screen x-position if possible.\n     */\n    showPointerAtTarget(element: HTMLElement, xPosition: number, keyboardMode: boolean) {\n        this.targetElement = element;\n        this.targetSelectionRange = window.getSelection()?.getRangeAt(0) || null;\n        this.updateDomForTarget(element);\n\n        this.pointer.style.display = 'block';\n        const targetBounds = element.getBoundingClientRect();\n        const pointerBounds = this.pointer.getBoundingClientRect();\n\n        const xTarget = Math.min(Math.max(xPosition, targetBounds.left), targetBounds.right);\n        const xOffset = xTarget - (pointerBounds.width / 2);\n        const yOffset = (targetBounds.top - pointerBounds.height) - 16;\n\n        this.pointer.style.left = `${xOffset}px`;\n        this.pointer.style.top = `${yOffset}px`;\n\n        this.showing = true;\n        this.isMakingSelection = true;\n\n        setTimeout(() => {\n            this.isMakingSelection = false;\n        }, 100);\n\n        const scrollListener = () => {\n            this.hidePointer();\n            window.removeEventListener('scroll', scrollListener);\n        };\n\n        element.parentElement?.insertBefore(this.pointer, element);\n        if (!keyboardMode) {\n            window.addEventListener('scroll', scrollListener, {passive: true});\n        }\n    }\n\n    /**\n     * Update the pointer inputs/content for the given target element.\n     */\n    updateDomForTarget(element: HTMLElement) {\n        const permaLink = window.baseUrl(`/link/${this.pageId}#${element.id}`);\n        const includeTag = `{{@${this.pageId}#${element.id}}}`;\n\n        this.linkInput.value = permaLink;\n        this.includeInput.value = includeTag;\n\n        // Update anchor if present\n        const editAnchor = this.pointer.querySelector('#pointer-edit');\n        if (editAnchor instanceof HTMLAnchorElement && element) {\n            const {editHref} = editAnchor.dataset;\n            const elementId = element.id;\n\n            // Get the first 50 characters.\n            const queryContent = (element.textContent || '').substring(0, 50);\n            editAnchor.href = `${editHref}?content-id=${elementId}&content-text=${encodeURIComponent(queryContent)}`;\n        }\n    }\n\n    enterSectionSelectMode() {\n        const sections = Array.from(document.querySelectorAll('.page-content [id^=\"bkmrk\"]')) as HTMLElement[];\n        for (const section of sections) {\n            section.setAttribute('tabindex', '0');\n        }\n\n        sections[0].focus();\n\n        DOM.onEnterPress(sections, event => {\n            this.showPointerAtTarget(event.target as HTMLElement, 0, true);\n            this.pointer.focus();\n        });\n    }\n\n    createCommentAtPointer() {\n        if (!this.targetElement) {\n            return;\n        }\n\n        const refId = this.targetElement.id;\n        const hash = hashElement(this.targetElement);\n        let range = '';\n        if (this.targetSelectionRange) {\n            const commonContainer = this.targetSelectionRange.commonAncestorContainer;\n            if (this.targetElement.contains(commonContainer)) {\n                const start = normalizeNodeTextOffsetToParent(\n                    this.targetSelectionRange.startContainer,\n                    this.targetSelectionRange.startOffset,\n                    this.targetElement\n                );\n                const end = normalizeNodeTextOffsetToParent(\n                    this.targetSelectionRange.endContainer,\n                    this.targetSelectionRange.endOffset,\n                    this.targetElement\n                );\n                range = `${start}-${end}`;\n            }\n        }\n\n        const reference = `${refId}:${hash}:${range}`;\n        const pageComments = window.$components.first('page-comments') as PageComments;\n        pageComments.startNewComment(reference);\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/popup.js",
    "content": "import {fadeIn, fadeOut} from '../services/animations.ts';\nimport {onSelect} from '../services/dom.ts';\nimport {Component} from './component';\n\n/**\n * Popup window that will contain other content.\n * This component provides the show/hide functionality\n * with the ability for popup@hide child references to close this.\n */\nexport class Popup extends Component {\n\n    setup() {\n        this.container = this.$el;\n        this.hideButtons = this.$manyRefs.hide || [];\n\n        this.onkeyup = null;\n        this.onHide = null;\n        this.setupListeners();\n    }\n\n    setupListeners() {\n        let lastMouseDownTarget = null;\n        this.container.addEventListener('mousedown', event => {\n            lastMouseDownTarget = event.target;\n        });\n\n        this.container.addEventListener('click', event => {\n            if (event.target === this.container && lastMouseDownTarget === this.container) {\n                this.hide();\n            }\n        });\n\n        onSelect(this.hideButtons, () => this.hide());\n    }\n\n    hide(onComplete = null) {\n        fadeOut(this.container, 120, onComplete);\n        if (this.onkeyup) {\n            window.removeEventListener('keyup', this.onkeyup);\n            this.onkeyup = null;\n        }\n        if (this.onHide) {\n            this.onHide();\n        }\n    }\n\n    show(onComplete = null, onHide = null) {\n        fadeIn(this.container, 120, onComplete);\n\n        this.onkeyup = event => {\n            if (event.key === 'Escape') {\n                this.hide();\n            }\n        };\n        window.addEventListener('keyup', this.onkeyup);\n        this.onHide = onHide;\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/setting-app-color-scheme.js",
    "content": "import {Component} from './component';\n\nexport class SettingAppColorScheme extends Component {\n\n    setup() {\n        this.container = this.$el;\n        this.mode = this.$opts.mode;\n        this.lightContainer = this.$refs.lightContainer;\n        this.darkContainer = this.$refs.darkContainer;\n\n        this.container.addEventListener('tabs-change', event => {\n            const panel = event.detail.showing;\n            const newMode = (panel === 'color-scheme-panel-light') ? 'light' : 'dark';\n            this.handleModeChange(newMode);\n        });\n\n        const onInputChange = event => {\n            this.updateAppColorsFromInputs();\n\n            if (event.target.name.startsWith('setting-app-color')) {\n                this.updateLightForInput(event.target);\n            }\n        };\n        this.container.addEventListener('change', onInputChange);\n        this.container.addEventListener('input', onInputChange);\n    }\n\n    handleModeChange(newMode) {\n        this.mode = newMode;\n        const isDark = (newMode === 'dark');\n\n        document.documentElement.classList.toggle('dark-mode', isDark);\n        this.updateAppColorsFromInputs();\n    }\n\n    updateAppColorsFromInputs() {\n        const inputContainer = this.mode === 'dark' ? this.darkContainer : this.lightContainer;\n        const inputs = inputContainer.querySelectorAll('input[type=\"color\"]');\n        for (const input of inputs) {\n            const splitName = input.name.split('-');\n            const colorPos = splitName.indexOf('color');\n            let cssId = splitName.slice(1, colorPos).join('-');\n            if (cssId === 'app') {\n                cssId = 'primary';\n            }\n\n            const varName = `--color-${cssId}`;\n            document.body.style.setProperty(varName, input.value);\n        }\n    }\n\n    /**\n     * Update the 'light' app color variant for the given input.\n     * @param {HTMLInputElement} input\n     */\n    updateLightForInput(input) {\n        const lightName = input.name.replace('-color', '-color-light');\n        const hexVal = input.value;\n        const rgb = this.hexToRgb(hexVal);\n        const rgbLightVal = `rgba(${[rgb.r, rgb.g, rgb.b, '0.15'].join(',')})`;\n\n        const lightColorInput = this.container.querySelector(`input[name=\"${lightName}\"][type=\"hidden\"]`);\n        lightColorInput.value = rgbLightVal;\n    }\n\n    /**\n     * Covert a hex color code to rgb components.\n     * @attribution https://stackoverflow.com/a/5624139\n     * @param {String} hex\n     * @returns {{r: Number, g: Number, b: Number}}\n     */\n    hexToRgb(hex) {\n        const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n        return {\n            r: result ? parseInt(result[1], 16) : 0,\n            g: result ? parseInt(result[2], 16) : 0,\n            b: result ? parseInt(result[3], 16) : 0,\n        };\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/setting-color-picker.js",
    "content": "import {Component} from './component';\n\nexport class SettingColorPicker extends Component {\n\n    setup() {\n        this.colorInput = this.$refs.input;\n        this.resetButton = this.$refs.resetButton;\n        this.defaultButton = this.$refs.defaultButton;\n        this.currentColor = this.$opts.current;\n        this.defaultColor = this.$opts.default;\n\n        this.resetButton.addEventListener('click', () => this.setValue(this.currentColor));\n        this.defaultButton.addEventListener('click', () => this.setValue(this.defaultColor));\n    }\n\n    setValue(value) {\n        this.colorInput.value = value;\n        this.colorInput.dispatchEvent(new Event('change', {bubbles: true}));\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/setting-homepage-control.js",
    "content": "import {Component} from './component';\n\nexport class SettingHomepageControl extends Component {\n\n    setup() {\n        this.typeControl = this.$refs.typeControl;\n        this.pagePickerContainer = this.$refs.pagePickerContainer;\n\n        this.typeControl.addEventListener('change', this.controlPagePickerVisibility.bind(this));\n        this.controlPagePickerVisibility();\n    }\n\n    controlPagePickerVisibility() {\n        const showPagePicker = this.typeControl.value === 'page';\n        this.pagePickerContainer.style.display = (showPagePicker ? 'block' : 'none');\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/shelf-sort.js",
    "content": "import Sortable from 'sortablejs';\nimport {Component} from './component';\nimport {buildListActions, sortActionClickListener} from '../services/dual-lists.ts';\n\nexport class ShelfSort extends Component {\n\n    setup() {\n        this.elem = this.$el;\n        this.input = this.$refs.input;\n        this.shelfBookList = this.$refs.shelfBookList;\n        this.allBookList = this.$refs.allBookList;\n        this.bookSearchInput = this.$refs.bookSearch;\n        this.sortButtonContainer = this.$refs.sortButtonContainer;\n\n        this.lastSort = null;\n\n        this.initSortable();\n        this.setupListeners();\n    }\n\n    initSortable() {\n        const scrollBoxes = this.elem.querySelectorAll('.scroll-box');\n        for (const scrollBox of scrollBoxes) {\n            new Sortable(scrollBox, {\n                group: 'shelf-books',\n                ghostClass: 'primary-background-light',\n                handle: '.handle',\n                animation: 150,\n                onSort: this.onChange.bind(this),\n            });\n        }\n    }\n\n    setupListeners() {\n        const listActions = buildListActions(this.allBookList, this.shelfBookList);\n        const sortActionListener = sortActionClickListener(listActions, this.onChange.bind(this));\n        this.elem.addEventListener('click', sortActionListener);\n\n        this.bookSearchInput.addEventListener('input', () => {\n            this.filterBooksByName(this.bookSearchInput.value);\n        });\n\n        this.sortButtonContainer.addEventListener('click', event => {\n            const button = event.target.closest('button[data-sort]');\n            if (button) {\n                this.sortShelfBooks(button.dataset.sort);\n            }\n        });\n    }\n\n    /**\n     * @param {String} filterVal\n     */\n    filterBooksByName(filterVal) {\n        // Set height on first search, if not already set, to prevent the distraction\n        // of the list height jumping around\n        if (!this.allBookList.style.height) {\n            this.allBookList.style.height = `${this.allBookList.getBoundingClientRect().height}px`;\n        }\n\n        const books = this.allBookList.children;\n        const lowerFilter = filterVal.trim().toLowerCase();\n\n        for (const bookEl of books) {\n            const show = !filterVal || bookEl.textContent.toLowerCase().includes(lowerFilter);\n            bookEl.style.display = show ? null : 'none';\n        }\n    }\n\n    onChange() {\n        const shelfBookElems = Array.from(this.shelfBookList.querySelectorAll('[data-id]'));\n        this.input.value = shelfBookElems.map(elem => elem.getAttribute('data-id')).join(',');\n    }\n\n    sortShelfBooks(sortProperty) {\n        const books = Array.from(this.shelfBookList.children);\n        const reverse = sortProperty === this.lastSort;\n\n        books.sort((bookA, bookB) => {\n            const aProp = bookA.dataset[sortProperty].toLowerCase();\n            const bProp = bookB.dataset[sortProperty].toLowerCase();\n\n            if (reverse) {\n                return bProp.localeCompare(aProp);\n            }\n\n            return aProp.localeCompare(bProp);\n        });\n\n        for (const book of books) {\n            this.shelfBookList.append(book);\n        }\n\n        this.lastSort = (this.lastSort === sortProperty) ? null : sortProperty;\n        this.onChange();\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/shortcut-input.js",
    "content": "import {Component} from './component';\n\n/**\n * Keys to ignore when recording shortcuts.\n * @type {string[]}\n */\nconst ignoreKeys = ['Control', 'Alt', 'Shift', 'Meta', 'Super', ' ', '+', 'Tab', 'Escape'];\n\nexport class ShortcutInput extends Component {\n\n    setup() {\n        this.input = this.$el;\n\n        this.setupListeners();\n    }\n\n    setupListeners() {\n        this.listenerRecordKey = this.listenerRecordKey.bind(this);\n\n        this.input.addEventListener('focus', () => {\n            this.startListeningForInput();\n        });\n\n        this.input.addEventListener('blur', () => {\n            this.stopListeningForInput();\n        });\n    }\n\n    startListeningForInput() {\n        this.input.addEventListener('keydown', this.listenerRecordKey);\n    }\n\n    /**\n     * @param {KeyboardEvent} event\n     */\n    listenerRecordKey(event) {\n        if (ignoreKeys.includes(event.key)) {\n            return;\n        }\n\n        const keys = [\n            event.ctrlKey ? 'Ctrl' : '',\n            event.metaKey ? 'Cmd' : '',\n            event.key,\n        ];\n\n        this.input.value = keys.filter(s => Boolean(s)).join(' + ');\n    }\n\n    stopListeningForInput() {\n        this.input.removeEventListener('keydown', this.listenerRecordKey);\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/shortcuts.js",
    "content": "import {Component} from './component';\n\nfunction reverseMap(map) {\n    const reversed = {};\n    for (const [key, value] of Object.entries(map)) {\n        reversed[value] = key;\n    }\n    return reversed;\n}\n\nexport class Shortcuts extends Component {\n\n    setup() {\n        this.container = this.$el;\n        this.mapById = JSON.parse(this.$opts.keyMap);\n        this.mapByShortcut = reverseMap(this.mapById);\n\n        this.hintsShowing = false;\n\n        this.hideHints = this.hideHints.bind(this);\n        this.hintAbortController = null;\n\n        this.setupListeners();\n    }\n\n    setupListeners() {\n        window.addEventListener('keydown', event => {\n            if (event.target.closest('input, select, textarea, .cm-editor, .editor-container')) {\n                return;\n            }\n\n            if (event.key === '?') {\n                if (this.hintsShowing) {\n                    this.hideHints();\n                } else {\n                    this.showHints();\n                }\n                return;\n            }\n\n            this.handleShortcutPress(event);\n        });\n    }\n\n    /**\n     * @param {KeyboardEvent} event\n     */\n    handleShortcutPress(event) {\n        const keys = [\n            event.ctrlKey ? 'Ctrl' : '',\n            event.metaKey ? 'Cmd' : '',\n            event.key,\n        ];\n\n        const combo = keys.filter(s => Boolean(s)).join(' + ');\n\n        const shortcutId = this.mapByShortcut[combo];\n        if (shortcutId) {\n            const wasHandled = this.runShortcut(shortcutId);\n            if (wasHandled) {\n                event.preventDefault();\n            }\n        }\n    }\n\n    /**\n     * Run the given shortcut, and return a boolean to indicate if the event\n     * was successfully handled by a shortcut action.\n     * @param {String} id\n     * @return {boolean}\n     */\n    runShortcut(id) {\n        const el = this.container.querySelector(`[data-shortcut=\"${id}\"]`);\n        if (!el) {\n            return false;\n        }\n\n        if (el.matches('input, textarea, select')) {\n            el.focus();\n            return true;\n        }\n\n        if (el.matches('a, button')) {\n            el.click();\n            return true;\n        }\n\n        if (el.matches('div[tabindex]')) {\n            el.click();\n            el.focus();\n            return true;\n        }\n\n        console.error('Shortcut attempted to be ran for element type that does not have handling setup', el);\n\n        return false;\n    }\n\n    showHints() {\n        const wrapper = document.createElement('div');\n        wrapper.classList.add('shortcut-container');\n        this.container.append(wrapper);\n\n        const shortcutEls = this.container.querySelectorAll('[data-shortcut]');\n        const displayedIds = new Set();\n        for (const shortcutEl of shortcutEls) {\n            const id = shortcutEl.getAttribute('data-shortcut');\n            if (displayedIds.has(id)) {\n                continue;\n            }\n\n            const key = this.mapById[id];\n            this.showHintLabel(shortcutEl, key, wrapper);\n            displayedIds.add(id);\n        }\n\n        this.hintAbortController = new AbortController();\n        const signal = this.hintAbortController.signal;\n        window.addEventListener('scroll', this.hideHints, {signal});\n        window.addEventListener('focus', this.hideHints, {signal});\n        window.addEventListener('blur', this.hideHints, {signal});\n        window.addEventListener('click', this.hideHints, {signal});\n\n        this.hintsShowing = true;\n    }\n\n    /**\n     * @param {Element} targetEl\n     * @param {String} key\n     * @param {Element} wrapper\n     */\n    showHintLabel(targetEl, key, wrapper) {\n        const targetBounds = targetEl.getBoundingClientRect();\n\n        const label = document.createElement('div');\n        label.classList.add('shortcut-hint');\n        label.textContent = key;\n\n        const linkage = document.createElement('div');\n        linkage.classList.add('shortcut-linkage');\n        linkage.style.left = `${targetBounds.x}px`;\n        linkage.style.top = `${targetBounds.y}px`;\n        linkage.style.width = `${targetBounds.width}px`;\n        linkage.style.height = `${targetBounds.height}px`;\n\n        wrapper.append(label, linkage);\n\n        const labelBounds = label.getBoundingClientRect();\n\n        label.style.insetInlineStart = `${((targetBounds.x + targetBounds.width) - (labelBounds.width + 6))}px`;\n        label.style.insetBlockStart = `${(targetBounds.y + (targetBounds.height - labelBounds.height) / 2)}px`;\n    }\n\n    hideHints() {\n        const wrapper = this.container.querySelector('.shortcut-container');\n        wrapper.remove();\n        this.hintAbortController?.abort();\n        this.hintsShowing = false;\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/sort-rule-manager.ts",
    "content": "import {Component} from \"./component.js\";\nimport Sortable from \"sortablejs\";\nimport {buildListActions, sortActionClickListener} from \"../services/dual-lists\";\n\n\nexport class SortRuleManager extends Component {\n\n    protected input!: HTMLInputElement;\n    protected configuredList!: HTMLElement;\n    protected availableList!: HTMLElement;\n\n    setup() {\n        this.input = this.$refs.input as HTMLInputElement;\n        this.configuredList = this.$refs.configuredOperationsList;\n        this.availableList = this.$refs.availableOperationsList;\n\n        this.initSortable();\n\n        const listActions = buildListActions(this.availableList, this.configuredList);\n        const sortActionListener = sortActionClickListener(listActions, this.onChange.bind(this));\n        this.$el.addEventListener('click', sortActionListener);\n    }\n\n    initSortable() {\n        const scrollBoxes = [this.configuredList, this.availableList];\n        for (const scrollBox of scrollBoxes) {\n            new Sortable(scrollBox, {\n                group: 'sort-rule-operations',\n                ghostClass: 'primary-background-light',\n                handle: '.handle',\n                animation: 150,\n                onSort: this.onChange.bind(this),\n            });\n        }\n    }\n\n    onChange() {\n        const configuredOpEls = Array.from(this.configuredList.querySelectorAll('[data-id]'));\n        this.input.value = configuredOpEls.map(elem => elem.getAttribute('data-id')).join(',');\n    }\n}"
  },
  {
    "path": "resources/js/components/sortable-list.js",
    "content": "import Sortable from 'sortablejs';\nimport {Component} from './component';\n\n/**\n * SortableList\n *\n * Can have data set on the dragged items by setting a 'data-drag-content' attribute.\n * This attribute must contain JSON where the keys are content types and the values are\n * the data to set on the data-transfer.\n */\nexport class SortableList extends Component {\n\n    setup() {\n        this.container = this.$el;\n        this.handleSelector = this.$opts.handleSelector;\n\n        const sortable = new Sortable(this.container, {\n            handle: this.handleSelector,\n            animation: 150,\n            onSort: () => {\n                this.$emit('sort', {ids: sortable.toArray()});\n            },\n            setData(dataTransferItem, dragEl) {\n                const jsonContent = dragEl.getAttribute('data-drag-content');\n                if (jsonContent) {\n                    const contentByType = JSON.parse(jsonContent);\n                    for (const [type, content] of Object.entries(contentByType)) {\n                        dataTransferItem.setData(type, content);\n                    }\n                }\n            },\n            revertOnSpill: true,\n            dropBubble: true,\n            dragoverBubble: false,\n        });\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/submit-on-change.js",
    "content": "import {Component} from './component';\n\n/**\n * Submit on change\n * Simply submits a parent form when this input is changed.\n */\nexport class SubmitOnChange extends Component {\n\n    setup() {\n        this.filter = this.$opts.filter;\n\n        this.$el.addEventListener('change', event => {\n            if (this.filter && !event.target.matches(this.filter)) {\n                return;\n            }\n\n            const form = this.$el.closest('form');\n            if (form) {\n                form.submit();\n            }\n        });\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/tabs.ts",
    "content": "import {Component} from './component';\n\nexport interface TabsChangeEvent {\n    showing: string;\n}\n\n/**\n * Tabs\n * Uses accessible attributes to drive its functionality.\n * On tab wrapping element:\n * - role=tablist\n * On tabs (Should be a button):\n * - id\n * - role=tab\n * - aria-selected=true/false\n * - aria-controls=<id-of-panel-section>\n * On panels:\n * - id\n * - tabindex=0\n * - role=tabpanel\n * - aria-labelledby=<id-of-tab-for-panel>\n * - hidden (If not shown by default).\n */\nexport class Tabs extends Component {\n\n    protected container!: HTMLElement;\n    protected tabList!: HTMLElement;\n    protected tabs!: HTMLElement[];\n    protected panels!: HTMLElement[];\n\n    protected activeUnder!: number;\n    protected active: null|boolean = null;\n\n    setup() {\n        this.container = this.$el;\n        this.tabList = this.container.querySelector('[role=\"tablist\"]') as HTMLElement;\n        this.tabs = Array.from(this.tabList.querySelectorAll('[role=\"tab\"]'));\n        this.panels = Array.from(this.container.querySelectorAll(':scope > [role=\"tabpanel\"], :scope > * > [role=\"tabpanel\"]'));\n        this.activeUnder = this.$opts.activeUnder ? Number(this.$opts.activeUnder) : 10000;\n\n        this.container.addEventListener('click', event => {\n            const tab = (event.target as HTMLElement).closest('[role=\"tab\"]');\n            if (tab instanceof HTMLElement && this.tabs.includes(tab)) {\n                this.show(tab.getAttribute('aria-controls') || '');\n            }\n        });\n\n        window.addEventListener('resize', this.updateActiveState.bind(this), {\n            passive: true,\n        });\n        this.updateActiveState();\n    }\n\n    public show(sectionId: string): void {\n        for (const panel of this.panels) {\n            panel.toggleAttribute('hidden', panel.id !== sectionId);\n        }\n\n        for (const tab of this.tabs) {\n            const tabSection = tab.getAttribute('aria-controls');\n            const selected = tabSection === sectionId;\n            tab.setAttribute('aria-selected', selected ? 'true' : 'false');\n        }\n\n        const data: TabsChangeEvent = {showing: sectionId};\n        this.$emit('change', data);\n    }\n\n    protected updateActiveState(): void {\n        const active = window.innerWidth < this.activeUnder;\n        if (active === this.active) {\n            return;\n        }\n\n        if (active) {\n            this.activate();\n        } else {\n            this.deactivate();\n        }\n\n        this.active = active;\n    }\n\n    protected activate(): void {\n        const panelToShow = this.panels.find(p => !p.hasAttribute('hidden')) || this.panels[0];\n        this.show(panelToShow.id);\n        this.tabList.toggleAttribute('hidden', false);\n    }\n\n    protected deactivate(): void {\n        for (const panel of this.panels) {\n            panel.removeAttribute('hidden');\n        }\n        for (const tab of this.tabs) {\n            tab.setAttribute('aria-selected', 'false');\n        }\n        this.tabList.toggleAttribute('hidden', true);\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/tag-manager.js",
    "content": "import {Component} from './component';\n\nexport class TagManager extends Component {\n\n    setup() {\n        this.addRemoveComponentEl = this.$refs.addRemove;\n        this.container = this.$el;\n        this.rowSelector = this.$opts.rowSelector;\n\n        this.setupListeners();\n    }\n\n    setupListeners() {\n        this.container.addEventListener('input', event => {\n            /** @var {AddRemoveRows} * */\n            const addRemoveComponent = window.$components.firstOnElement(this.addRemoveComponentEl, 'add-remove-rows');\n            if (!this.hasEmptyRows() && event.target.value) {\n                addRemoveComponent.add();\n            }\n        });\n    }\n\n    hasEmptyRows() {\n        const rows = this.container.querySelectorAll(this.rowSelector);\n        const firstEmpty = [...rows].find(row => [...row.querySelectorAll('input')].filter(input => input.value).length === 0);\n        return firstEmpty !== undefined;\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/template-manager.js",
    "content": "import * as DOM from '../services/dom.ts';\nimport {Component} from './component';\n\nexport class TemplateManager extends Component {\n\n    setup() {\n        this.container = this.$el;\n        this.list = this.$refs.list;\n\n        this.searchInput = this.$refs.searchInput;\n        this.searchButton = this.$refs.searchButton;\n        this.searchCancel = this.$refs.searchCancel;\n\n        this.setupListeners();\n    }\n\n    setupListeners() {\n        // Template insert action buttons\n        DOM.onChildEvent(this.container, '[template-action]', 'click', this.handleTemplateActionClick.bind(this));\n\n        // Template list pagination click\n        DOM.onChildEvent(this.container, '.pagination a', 'click', this.handlePaginationClick.bind(this));\n\n        // Template list item content click\n        DOM.onChildEvent(this.container, '.template-item-content', 'click', this.handleTemplateItemClick.bind(this));\n\n        // Template list item drag start\n        DOM.onChildEvent(this.container, '.template-item', 'dragstart', this.handleTemplateItemDragStart.bind(this));\n\n        // Search box enter press\n        this.searchInput.addEventListener('keypress', event => {\n            if (event.key === 'Enter') {\n                event.preventDefault();\n                this.performSearch();\n            }\n        });\n\n        // Search submit button press\n        this.searchButton.addEventListener('click', () => this.performSearch());\n\n        // Search cancel button press\n        this.searchCancel.addEventListener('click', () => {\n            this.searchInput.value = '';\n            this.performSearch();\n        });\n    }\n\n    handleTemplateItemClick(event, templateItem) {\n        const templateId = templateItem.closest('[template-id]').getAttribute('template-id');\n        this.insertTemplate(templateId, 'replace');\n    }\n\n    handleTemplateItemDragStart(event, templateItem) {\n        const templateId = templateItem.closest('[template-id]').getAttribute('template-id');\n        event.dataTransfer.setData('bookstack/template', templateId);\n        event.dataTransfer.setData('text/plain', templateId);\n    }\n\n    handleTemplateActionClick(event, actionButton) {\n        event.stopPropagation();\n\n        const action = actionButton.getAttribute('template-action');\n        const templateId = actionButton.closest('[template-id]').getAttribute('template-id');\n        this.insertTemplate(templateId, action);\n    }\n\n    async insertTemplate(templateId, action = 'replace') {\n        const resp = await window.$http.get(`/templates/${templateId}`);\n        const eventName = `editor::${action}`;\n        window.$events.emit(eventName, resp.data);\n    }\n\n    async handlePaginationClick(event, paginationLink) {\n        event.preventDefault();\n        const paginationUrl = paginationLink.getAttribute('href');\n        const resp = await window.$http.get(paginationUrl);\n        this.list.innerHTML = resp.data;\n    }\n\n    async performSearch() {\n        const searchTerm = this.searchInput.value;\n        const resp = await window.$http.get('/templates', {\n            search: searchTerm,\n        });\n        this.searchCancel.style.display = searchTerm ? 'block' : 'none';\n        this.list.innerHTML = resp.data;\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/toggle-switch.js",
    "content": "import {Component} from './component';\n\nexport class ToggleSwitch extends Component {\n\n    setup() {\n        this.input = this.$el.querySelector('input[type=hidden]');\n        this.checkbox = this.$el.querySelector('input[type=checkbox]');\n\n        this.checkbox.addEventListener('change', this.stateChange.bind(this));\n    }\n\n    stateChange() {\n        this.input.value = (this.checkbox.checked ? 'true' : 'false');\n\n        // Dispatch change event from hidden input so they can be listened to\n        // like a normal checkbox.\n        const changeEvent = new Event('change');\n        this.input.dispatchEvent(changeEvent);\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/tri-layout.ts",
    "content": "import {Component} from './component';\n\nexport class TriLayout extends Component {\n    private container!: HTMLElement;\n    private tabs!: HTMLElement[];\n    private sidebarScrollContainers!: HTMLElement[];\n\n    private lastLayoutType = 'none';\n    private onDestroy: (()=>void)|null = null;\n    private scrollCache: Record<string, number> = {\n        content: 0,\n        info: 0,\n    };\n    private lastTabShown = 'content';\n\n    setup(): void {\n        this.container = this.$refs.container;\n        this.tabs = this.$manyRefs.tab;\n        this.sidebarScrollContainers = this.$manyRefs.sidebarScrollContainer;\n\n        // Bind any listeners\n        this.mobileTabClick = this.mobileTabClick.bind(this);\n\n        // Watch layout changes\n        this.updateLayout();\n        window.addEventListener('resize', () => {\n            this.updateLayout();\n        }, {passive: true});\n\n        this.setupSidebarScrollHandlers();\n    }\n\n    updateLayout(): void {\n        let newLayout = 'tablet';\n        if (window.innerWidth <= 1000) newLayout = 'mobile';\n        if (window.innerWidth > 1400) newLayout = 'desktop';\n        if (newLayout === this.lastLayoutType) return;\n\n        if (this.onDestroy) {\n            this.onDestroy();\n            this.onDestroy = null;\n        }\n\n        if (newLayout === 'desktop') {\n            this.setupDesktop();\n        } else if (newLayout === 'mobile') {\n            this.setupMobile();\n        }\n\n        this.lastLayoutType = newLayout;\n    }\n\n    setupMobile() {\n        for (const tab of this.tabs) {\n            tab.addEventListener('click', this.mobileTabClick);\n        }\n\n        this.onDestroy = () => {\n            for (const tab of this.tabs) {\n                tab.removeEventListener('click', this.mobileTabClick);\n            }\n        };\n    }\n\n    setupDesktop(): void {\n        //\n    }\n\n    /**\n     * Action to run when the mobile info toggle bar is clicked/tapped\n     */\n    mobileTabClick(event: MouseEvent): void {\n        const tab = (event.target as HTMLElement).dataset.tab || '';\n        this.showTab(tab);\n    }\n\n    /**\n     * Show the content tab.\n     * Used by the page-display component.\n     */\n    showContent(): void {\n        this.showTab('content', false);\n    }\n\n    /**\n     * Show the given tab\n     */\n    showTab(tabName: string, scroll: boolean = true): void {\n        this.scrollCache[this.lastTabShown] = document.documentElement.scrollTop;\n\n        // Set tab status\n        for (const tab of this.tabs) {\n            const isActive = (tab.dataset.tab === tabName);\n            tab.setAttribute('aria-selected', isActive ? 'true' : 'false');\n        }\n\n        // Toggle section\n        const showInfo = (tabName === 'info');\n        this.container.classList.toggle('show-info', showInfo);\n\n        // Set the scroll position from cache\n        if (scroll) {\n            const pageHeader = document.querySelector('header') as HTMLElement;\n            const defaultScrollTop = pageHeader.getBoundingClientRect().bottom;\n            document.documentElement.scrollTop = this.scrollCache[tabName] || defaultScrollTop;\n            setTimeout(() => {\n                document.documentElement.scrollTop = this.scrollCache[tabName] || defaultScrollTop;\n            }, 50);\n        }\n\n        this.lastTabShown = tabName;\n    }\n\n    setupSidebarScrollHandlers(): void {\n        for (const sidebar of this.sidebarScrollContainers) {\n            sidebar.addEventListener('scroll', () => this.handleSidebarScroll(sidebar), {\n                passive: true,\n            });\n            this.handleSidebarScroll(sidebar);\n        }\n\n        window.addEventListener('resize', () => {\n            for (const sidebar of this.sidebarScrollContainers) {\n                this.handleSidebarScroll(sidebar);\n            }\n        });\n    }\n\n    handleSidebarScroll(sidebar: HTMLElement): void {\n        const scrollable = sidebar.clientHeight !== sidebar.scrollHeight;\n        const atTop = sidebar.scrollTop === 0;\n        const atBottom = (sidebar.scrollTop + sidebar.clientHeight) === sidebar.scrollHeight;\n\n        if (sidebar.parentElement) {\n            sidebar.parentElement.classList.toggle('scroll-away-from-top', !atTop && scrollable);\n            sidebar.parentElement.classList.toggle('scroll-away-from-bottom', !atBottom && scrollable);\n        }\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/user-select.js",
    "content": "import {onChildEvent} from '../services/dom.ts';\nimport {Component} from './component';\n\nexport class UserSelect extends Component {\n\n    setup() {\n        this.container = this.$el;\n        this.input = this.$refs.input;\n        this.userInfoContainer = this.$refs.userInfo;\n\n        onChildEvent(this.container, 'a.dropdown-search-item', 'click', this.selectUser.bind(this));\n    }\n\n    selectUser(event, userEl) {\n        event.preventDefault();\n        this.input.value = userEl.getAttribute('data-id');\n        this.userInfoContainer.innerHTML = userEl.innerHTML;\n        this.input.dispatchEvent(new Event('change', {bubbles: true}));\n        this.hide();\n    }\n\n    hide() {\n        /** @var {Dropdown} * */\n        const dropdown = window.$components.firstOnElement(this.container, 'dropdown');\n        dropdown.hide();\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/webhook-events.js",
    "content": "/**\n * Webhook Events\n * Manages dynamic selection control in the webhook form interface.\n */\nimport {Component} from './component';\n\nexport class WebhookEvents extends Component {\n\n    setup() {\n        this.checkboxes = this.$el.querySelectorAll('input[type=\"checkbox\"]');\n        this.allCheckbox = this.$el.querySelector('input[type=\"checkbox\"][value=\"all\"]');\n\n        this.$el.addEventListener('change', event => {\n            if (event.target.checked && event.target === this.allCheckbox) {\n                this.deselectIndividualEvents();\n            } else if (event.target.checked) {\n                this.allCheckbox.checked = false;\n            }\n        });\n    }\n\n    deselectIndividualEvents() {\n        for (const checkbox of this.checkboxes) {\n            if (checkbox !== this.allCheckbox) {\n                checkbox.checked = false;\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/wysiwyg-editor-tinymce.js",
    "content": "import {buildForEditor as buildEditorConfig} from '../wysiwyg-tinymce/config';\nimport {Component} from './component';\n\nexport class WysiwygEditorTinymce extends Component {\n\n    setup() {\n        this.elem = this.$el;\n\n        this.tinyMceConfig = buildEditorConfig({\n            language: this.$opts.language,\n            containerElement: this.elem,\n            darkMode: document.documentElement.classList.contains('dark-mode'),\n            textDirection: this.$opts.textDirection,\n            drawioUrl: this.getDrawIoUrl(),\n            pageId: Number(this.$opts.pageId),\n            translations: {\n                imageUploadErrorText: this.$opts.imageUploadErrorText,\n                serverUploadLimitText: this.$opts.serverUploadLimitText,\n            },\n            translationMap: window.editor_translations,\n        });\n\n        window.$events.emitPublic(this.elem, 'editor-tinymce::pre-init', {config: this.tinyMceConfig});\n        window.tinymce.init(this.tinyMceConfig).then(editors => {\n            this.editor = editors[0];\n        });\n    }\n\n    getDrawIoUrl() {\n        const drawioUrlElem = document.querySelector('[drawio-url]');\n        if (drawioUrlElem) {\n            return drawioUrlElem.getAttribute('drawio-url');\n        }\n        return '';\n    }\n\n    /**\n     * Get the content of this editor.\n     * Used by the parent page editor component.\n     * @return {Promise<{html: String}>}\n     */\n    async getContent() {\n        return {\n            html: this.editor.getContent(),\n        };\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/wysiwyg-editor.js",
    "content": "import {Component} from './component';\n\nexport class WysiwygEditor extends Component {\n\n    setup() {\n        this.elem = this.$el;\n        this.editContainer = this.$refs.editContainer;\n        this.input = this.$refs.input;\n\n        /** @var {SimpleWysiwygEditorInterface|null} */\n        this.editor = null;\n\n        const translations = {\n            ...window.editor_translations,\n            imageUploadErrorText: this.$opts.imageUploadErrorText,\n            serverUploadLimitText: this.$opts.serverUploadLimitText,\n        };\n\n        window.importVersioned('wysiwyg').then(wysiwyg => {\n            const editorContent = this.input.value;\n            this.editor = wysiwyg.createPageEditorInstance(this.editContainer, editorContent, {\n                drawioUrl: this.getDrawIoUrl(),\n                pageId: Number(this.$opts.pageId),\n                darkMode: document.documentElement.classList.contains('dark-mode'),\n                textDirection: this.$opts.textDirection,\n                translations,\n            });\n            window.wysiwyg = this.editor;\n        });\n\n        let handlingFormSubmit = false;\n        this.input.form.addEventListener('submit', event => {\n            if (!this.editor) {\n                return;\n            }\n\n            if (!handlingFormSubmit) {\n                event.preventDefault();\n                handlingFormSubmit = true;\n                this.editor.getContentAsHtml().then(html => {\n                    this.input.value = html;\n                    setTimeout(() => {\n                        this.input.form.requestSubmit();\n                    }, 5);\n                });\n            } else {\n                handlingFormSubmit = false;\n            }\n        });\n    }\n\n    getDrawIoUrl() {\n        const drawioUrlElem = document.querySelector('[drawio-url]');\n        if (drawioUrlElem) {\n            return drawioUrlElem.getAttribute('drawio-url');\n        }\n        return '';\n    }\n\n    /**\n     * Get the content of this editor.\n     * Used by the parent page editor component.\n     * @return {Promise<{html: String}>}\n     */\n    async getContent() {\n        return {\n            html: await this.editor.getContentAsHtml(),\n        };\n    }\n\n}\n"
  },
  {
    "path": "resources/js/components/wysiwyg-input.ts",
    "content": "import {Component} from './component';\nimport {el} from \"../wysiwyg/utils/dom\";\nimport {SimpleWysiwygEditorInterface} from \"../wysiwyg\";\n\nexport class WysiwygInput extends Component {\n    private elem!: HTMLTextAreaElement;\n    private wysiwygEditor!: SimpleWysiwygEditorInterface;\n    private textDirection!: string;\n\n    async setup() {\n        this.elem = this.$el as HTMLTextAreaElement;\n        this.textDirection = this.$opts.textDirection;\n\n        type WysiwygModule = typeof import('../wysiwyg');\n        const wysiwygModule = (await window.importVersioned('wysiwyg')) as WysiwygModule;\n        const container = el('div', {class: 'basic-editor-container'});\n        this.elem.parentElement?.appendChild(container);\n        this.elem.hidden = true;\n\n        this.wysiwygEditor = wysiwygModule.createBasicEditorInstance(container as HTMLElement, this.elem.value, {\n            darkMode: document.documentElement.classList.contains('dark-mode'),\n            textDirection: this.textDirection,\n            translations: (window as unknown as Record<string, Object>).editor_translations,\n        });\n\n        this.wysiwygEditor.onChange(() => {\n            this.wysiwygEditor.getContentAsHtml().then(html => {\n                this.elem.value = html;\n            });\n        });\n    }\n}\n"
  },
  {
    "path": "resources/js/custom.d.ts",
    "content": "declare module '*.svg' {\n    const content: string;\n    export default content;\n}"
  },
  {
    "path": "resources/js/global.d.ts",
    "content": "import {ComponentStore} from \"./services/components\";\nimport {EventManager} from \"./services/events\";\nimport {HttpManager} from \"./services/http\";\nimport {Translator} from \"./services/translations\";\n\ndeclare global {\n    const __DEV__: boolean;\n\n    interface Window {\n        __DEV__: boolean;\n        $components: ComponentStore;\n        $events: EventManager;\n        $trans: Translator;\n        $http: HttpManager;\n        baseUrl: (path: string) => string;\n        importVersioned: (module: string) => Promise<object>;\n    }\n}\n\nexport type CodeModule = (typeof import('./code/index.mjs'));"
  },
  {
    "path": "resources/js/markdown/actions.ts",
    "content": "import * as DrawIO from '../services/drawio';\nimport {MarkdownEditor} from \"./index.mjs\";\nimport {EntitySelectorPopup, ImageManager} from \"../components\";\nimport {MarkdownEditorInputSelection} from \"./inputs/interface\";\n\ninterface ImageManagerImage {\n    id: number;\n    name: string;\n    thumbs: { display: string; };\n    url: string;\n}\n\nexport class Actions {\n\n    protected readonly editor: MarkdownEditor;\n    protected lastContent: { html: string; markdown: string } = {\n        html: '',\n        markdown: '',\n    };\n\n    constructor(editor: MarkdownEditor) {\n        this.editor = editor;\n    }\n\n    updateAndRender() {\n        const content = this.editor.input.getText();\n        this.editor.config.inputEl.value = content;\n\n        const html = this.editor.markdown.render(content);\n        window.$events.emit('editor-html-change', '');\n        window.$events.emit('editor-markdown-change', '');\n        this.lastContent.html = html;\n        this.lastContent.markdown = content;\n        this.editor.display.patchWithHtml(html);\n    }\n\n    getContent() {\n        return this.lastContent;\n    }\n\n    showImageInsert() {\n        const imageManager = window.$components.first('image-manager') as ImageManager;\n\n        imageManager.show((image: ImageManagerImage) => {\n            const imageUrl = image.thumbs?.display || image.url;\n            const selectedText = this.editor.input.getSelectionText();\n            const newText = `[![${selectedText || image.name}](${imageUrl})](${image.url})`;\n            this.#replaceSelection(newText, newText.length);\n        }, 'gallery');\n    }\n\n    insertImage() {\n        const newText = `![${this.editor.input.getSelectionText()}](http://)`;\n        this.#replaceSelection(newText, newText.length - 1);\n    }\n\n    insertLink() {\n        const selectedText = this.editor.input.getSelectionText();\n        const newText = `[${selectedText}]()`;\n        const cursorPosDiff = (selectedText === '') ? -3 : -1;\n        this.#replaceSelection(newText, newText.length + cursorPosDiff);\n    }\n\n    showImageManager() {\n        const selectionRange = this.editor.input.getSelection();\n        const imageManager = window.$components.first('image-manager') as ImageManager;\n        imageManager.show((image: ImageManagerImage) => {\n            this.#insertDrawing(image, selectionRange);\n        }, 'drawio');\n    }\n\n    // Show the popup link selector and insert a link when finished\n    showLinkSelector() {\n        const selectionRange = this.editor.input.getSelection();\n\n        const selector = window.$components.first('entity-selector-popup') as EntitySelectorPopup;\n        const selectionText = this.editor.input.getSelectionText(selectionRange);\n        selector.show(entity => {\n            const selectedText = selectionText || entity.name;\n            const newText = `[${selectedText}](${entity.link})`;\n            this.#replaceSelection(newText, newText.length, selectionRange);\n        }, {\n            initialValue: selectionText,\n            searchEndpoint: '/search/entity-selector',\n            entityTypes: 'page,book,chapter,bookshelf',\n            entityPermission: 'view',\n        });\n    }\n\n    // Show draw.io if enabled and handle save.\n    startDrawing() {\n        const url = this.editor.config.drawioUrl;\n        if (!url) return;\n\n        const selectionRange = this.editor.input.getSelection();\n\n        DrawIO.show(url, () => Promise.resolve(''), async pngData => {\n            const data = {\n                image: pngData,\n                uploaded_to: Number(this.editor.config.pageId),\n            };\n\n            try {\n                const resp = await window.$http.post('/images/drawio', data);\n                this.#insertDrawing(resp.data as ImageManagerImage, selectionRange);\n                DrawIO.close();\n            } catch (err) {\n                this.handleDrawingUploadError(err);\n                throw new Error(`Failed to save image with error: ${err}`);\n            }\n        });\n    }\n\n    #insertDrawing(image: ImageManagerImage, originalSelectionRange: MarkdownEditorInputSelection) {\n        const newText = `<div drawio-diagram=\"${image.id}\"><img src=\"${image.url}\"></div>`;\n        this.#replaceSelection(newText, newText.length, originalSelectionRange);\n    }\n\n    // Show draw.io if enabled and handle save.\n    editDrawing(imgContainer: HTMLElement) {\n        const {drawioUrl} = this.editor.config;\n        if (!drawioUrl) {\n            return;\n        }\n\n        const selectionRange = this.editor.input.getSelection();\n        const drawingId = imgContainer.getAttribute('drawio-diagram') || '';\n        if (!drawingId) {\n            return;\n        }\n\n        DrawIO.show(drawioUrl, () => DrawIO.load(drawingId), async pngData => {\n            const data = {\n                image: pngData,\n                uploaded_to: Number(this.editor.config.pageId),\n            };\n\n            try {\n                const resp = await window.$http.post('/images/drawio', data);\n                const image = resp.data as ImageManagerImage;\n                const newText = `<div drawio-diagram=\"${image.id}\"><img src=\"${image.url}\"></div>`;\n                const newContent = this.editor.input.getText().split('\\n').map(line => {\n                    if (line.indexOf(`drawio-diagram=\"${drawingId}\"`) !== -1) {\n                        return newText;\n                    }\n                    return line;\n                }).join('\\n');\n                this.editor.input.setText(newContent, selectionRange);\n                DrawIO.close();\n            } catch (err) {\n                this.handleDrawingUploadError(err);\n                throw new Error(`Failed to save image with error: ${err}`);\n            }\n        });\n    }\n\n    handleDrawingUploadError(error: any): void {\n        if (error.status === 413) {\n            window.$events.emit('error', this.editor.config.text.serverUploadLimit);\n        } else {\n            window.$events.emit('error', this.editor.config.text.imageUploadError);\n        }\n        console.error(error);\n    }\n\n    // Make the editor full screen\n    fullScreen() {\n        const {container} = this.editor.config;\n        const alreadyFullscreen = container.classList.contains('fullscreen');\n        container.classList.toggle('fullscreen', !alreadyFullscreen);\n        document.body.classList.toggle('markdown-fullscreen', !alreadyFullscreen);\n    }\n\n    // Scroll to a specified text\n    scrollToText(searchText: string): void {\n        if (!searchText) {\n            return;\n        }\n\n        const lineRange = this.editor.input.searchForLineContaining(searchText);\n        if (lineRange) {\n            this.editor.input.setSelection(lineRange, true);\n            this.editor.input.focus();\n        }\n    }\n\n    focus() {\n        this.editor.input.focus();\n    }\n\n    /**\n     * Insert content into the editor.\n     */\n    insertContent(content: string) {\n        this.#replaceSelection(content, content.length);\n    }\n\n    /**\n     * Prepend content to the editor.\n     */\n    prependContent(content: string): void {\n        content = this.#cleanTextForEditor(content);\n        const selectionRange = this.editor.input.getSelection();\n        const selectFrom = selectionRange.from + content.length + 1;\n        this.editor.input.spliceText(0, 0, `${content}\\n`, {from: selectFrom});\n        this.editor.input.focus();\n    }\n\n    /**\n     * Append content to the editor.\n     */\n    appendContent(content: string): void {\n        content = this.#cleanTextForEditor(content);\n        this.editor.input.appendText(content);\n        this.editor.input.focus();\n    }\n\n    /**\n     * Replace the editor's contents\n     */\n    replaceContent(content: string): void {\n        this.editor.input.setText(content);\n    }\n\n    /**\n     * Replace the start of the line\n     * @param {String} newStart\n     */\n    replaceLineStart(newStart: string): void {\n        const selectionRange = this.editor.input.getSelection();\n        const lineRange = this.editor.input.getLineRangeFromPosition(selectionRange.from);\n        const lineContent = this.editor.input.getSelectionText(lineRange);\n        const lineStart = lineContent.split(' ')[0];\n\n        // Remove symbol if already set\n        if (lineStart === newStart) {\n            const newLineContent = lineContent.replace(`${newStart} `, '');\n            const selectFrom = selectionRange.from + (newLineContent.length - lineContent.length);\n            this.editor.input.spliceText(lineRange.from, lineRange.to, newLineContent, {from: selectFrom});\n            return;\n        }\n\n        let newLineContent = lineContent;\n        const alreadySymbol = /^[#>`]/.test(lineStart);\n        if (alreadySymbol) {\n            newLineContent = lineContent.replace(lineStart, newStart).trim();\n        } else if (newStart !== '') {\n            newLineContent = `${newStart} ${lineContent}`;\n        }\n\n        const selectFrom = selectionRange.from + (newLineContent.length - lineContent.length);\n        this.editor.input.spliceText(lineRange.from, lineRange.to, newLineContent, {from: selectFrom});\n    }\n\n    /**\n     * Wrap the selection in the given contents start and end contents.\n     */\n    wrapSelection(start: string, end: string): void {\n        const selectRange = this.editor.input.getSelection();\n        const selectionText = this.editor.input.getSelectionText(selectRange);\n        if (!selectionText) {\n            this.#wrapLine(start, end);\n            return;\n        }\n\n        let newSelectionText: string;\n        let newRange = {from: selectRange.from, to: selectRange.to};\n\n        if (selectionText.startsWith(start) && selectionText.endsWith(end)) {\n            newSelectionText = selectionText.slice(start.length, selectionText.length - end.length);\n            newRange.to = selectRange.to - (start.length + end.length);\n        } else {\n            newSelectionText = `${start}${selectionText}${end}`;\n            newRange.to = selectRange.to + (start.length + end.length);\n        }\n\n        this.editor.input.spliceText(\n            selectRange.from,\n            selectRange.to,\n            newSelectionText,\n            newRange,\n        );\n    }\n\n    replaceLineStartForOrderedList() {\n        const selectionRange = this.editor.input.getSelection();\n        const lineRange = this.editor.input.getLineRangeFromPosition(selectionRange.from);\n        const prevLineRange = this.editor.input.getLineRangeFromPosition(lineRange.from - 1);\n        const prevLineText = this.editor.input.getSelectionText(prevLineRange);\n\n        const listMatch = prevLineText.match(/^(\\s*)(\\d)([).])\\s/) || [];\n\n        const number = (Number(listMatch[2]) || 0) + 1;\n        const whiteSpace = listMatch[1] || '';\n        const listMark = listMatch[3] || '.';\n\n        const prefix = `${whiteSpace}${number}${listMark}`;\n        return this.replaceLineStart(prefix);\n    }\n\n    /**\n     * Cycles through the type of callout block within the selection.\n     * Creates a callout block if none existing, and removes it if cycling past the danger type.\n     */\n    cycleCalloutTypeAtSelection() {\n        const selectionRange = this.editor.input.getSelection();\n        const lineRange = this.editor.input.getLineRangeFromPosition(selectionRange.from);\n        const lineText = this.editor.input.getSelectionText(lineRange);\n\n        const formats = ['info', 'success', 'warning', 'danger'];\n        const joint = formats.join('|');\n        const regex = new RegExp(`class=\"((${joint})\\\\s+callout|callout\\\\s+(${joint}))\"`, 'i');\n        const matches = regex.exec(lineText);\n        const format = (matches ? (matches[2] || matches[3]) : '').toLowerCase();\n\n        if (format === formats[formats.length - 1]) {\n            this.#wrapLine(`<p class=\"callout ${formats[formats.length - 1]}\">`, '</p>');\n        } else if (format === '') {\n            this.#wrapLine('<p class=\"callout info\">', '</p>');\n        } else if (matches) {\n            const newFormatIndex = formats.indexOf(format) + 1;\n            const newFormat = formats[newFormatIndex];\n            const newContent = lineText.replace(matches[0], matches[0].replace(format, newFormat));\n            const lineDiff = newContent.length - lineText.length;\n            const anchor = Math.min(selectionRange.from, selectionRange.to);\n            const head = Math.max(selectionRange.from, selectionRange.to);\n            this.editor.input.spliceText(\n                lineRange.from,\n                lineRange.to,\n                newContent,\n                {from: anchor + lineDiff, to: head + lineDiff}\n            );\n        }\n    }\n\n    syncDisplayPosition(event: Event): void {\n        // Thanks to http://liuhao.im/english/2015/11/10/the-sync-scroll-of-markdown-editor-in-javascript.html\n        const scrollEl = event.target as HTMLElement;\n        const atEnd = Math.abs(scrollEl.scrollHeight - scrollEl.clientHeight - scrollEl.scrollTop) < 1;\n        if (atEnd) {\n            this.editor.display.scrollToIndex(-1);\n            return;\n        }\n\n        const range = this.editor.input.getTextAboveView();\n        const parser = new DOMParser();\n        const doc = parser.parseFromString(this.editor.markdown.render(range), 'text/html');\n        const totalLines = doc.documentElement.querySelectorAll('body > *');\n        this.editor.display.scrollToIndex(totalLines.length);\n    }\n\n    /**\n     * Fetch and insert the template of the given ID.\n     * The page-relative position provided can be used to determine insert location if possible.\n     */\n    async insertTemplate(templateId: string, event: MouseEvent): Promise<void> {\n        const cursorPos = this.editor.input.eventToPosition(event).from;\n        const responseData = (await window.$http.get(`/templates/${templateId}`)).data as {markdown: string, html: string};\n        const content = responseData.markdown || responseData.html;\n        this.editor.input.spliceText(cursorPos, cursorPos, content, {from: cursorPos});\n    }\n\n    /**\n     * Insert multiple images from the clipboard from an event at the provided\n     * screen coordinates (Typically form a paste event).\n     */\n    insertClipboardImages(images: File[], event: MouseEvent): void {\n        const cursorPos = this.editor.input.eventToPosition(event).from;\n        for (const image of images) {\n            this.uploadImage(image, cursorPos);\n        }\n    }\n\n    /**\n     * Handle image upload and add image into Markdown content\n     */\n    async uploadImage(file: File, position: number|null = null): Promise<void> {\n        if (file === null || file.type.indexOf('image') !== 0) return;\n        let ext = 'png';\n\n        if (position === null) {\n            position = this.editor.input.getSelection().from;\n        }\n\n        if (file.name) {\n            const fileNameMatches = file.name.match(/\\.(.+)$/);\n            if (fileNameMatches && fileNameMatches.length > 1) {\n                ext = fileNameMatches[1];\n            }\n        }\n\n        // Insert image into markdown\n        const id = `image-${Math.random().toString(16).slice(2)}`;\n        const placeholderImage = window.baseUrl(`/loading.gif#upload${id}`);\n        const placeHolderText = `![](${placeholderImage})`;\n        this.editor.input.spliceText(position, position, placeHolderText, {from: position});\n\n        const remoteFilename = `image-${Date.now()}.${ext}`;\n        const formData = new FormData();\n        formData.append('file', file, remoteFilename);\n        formData.append('uploaded_to', this.editor.config.pageId);\n\n        try {\n            const image = (await window.$http.post('/images/gallery', formData)).data as ImageManagerImage;\n            const newContent = `[![](${image.thumbs.display})](${image.url})`;\n            this.#findAndReplaceContent(placeHolderText, newContent);\n        } catch (err: any) {\n            window.$events.error(err?.data?.message || this.editor.config.text.imageUploadError);\n            this.#findAndReplaceContent(placeHolderText, '');\n            console.error(err);\n        }\n    }\n\n    /**\n     * Replace the current selection and focus the editor.\n     * Takes an offset for the cursor, after the change, relative to the start of the provided string.\n     * Can be provided a selection range to use instead of the current selection range.\n     */\n    #replaceSelection(newContent: string, offset: number = 0, selection: MarkdownEditorInputSelection|null = null) {\n        selection = selection || this.editor.input.getSelection();\n        const selectFrom = selection.from + offset;\n        this.editor.input.spliceText(selection.from, selection.to, newContent, {from: selectFrom, to: selectFrom});\n        this.editor.input.focus();\n    }\n\n    /**\n     * Cleans the given text to work with the editor.\n     * Standardises line endings to what's expected.\n     */\n    #cleanTextForEditor(text: string): string {\n        return text.replace(/\\r\\n|\\r/g, '\\n');\n    }\n\n    /**\n     * Find and replace the first occurrence of [search] with [replace]\n     */\n    #findAndReplaceContent(search: string, replace: string): void {\n        const newText = this.editor.input.getText().replace(search, replace);\n        this.editor.input.setText(newText);\n    }\n\n    /**\n     * Wrap the line in the given start and end contents.\n     */\n    #wrapLine(start: string, end: string): void {\n        const selectionRange = this.editor.input.getSelection();\n        const lineRange = this.editor.input.getLineRangeFromPosition(selectionRange.from);\n        const lineContent = this.editor.input.getSelectionText(lineRange);\n        let newLineContent: string;\n        let lineOffset: number;\n\n        if (lineContent.startsWith(start) && lineContent.endsWith(end)) {\n            newLineContent = lineContent.slice(start.length, lineContent.length - end.length);\n            lineOffset = -(start.length);\n        } else {\n            newLineContent = `${start}${lineContent}${end}`;\n            lineOffset = start.length;\n        }\n\n        this.editor.input.spliceText(lineRange.from, lineRange.to, newLineContent, {from: selectionRange.from + lineOffset});\n    }\n\n}\n"
  },
  {
    "path": "resources/js/markdown/codemirror.ts",
    "content": "import {EditorView, KeyBinding, ViewUpdate} from \"@codemirror/view\";\nimport {CodeModule} from \"../global\";\nimport {MarkdownEditorEventMap} from \"./dom-handlers\";\nimport {MarkdownEditorShortcutMap} from \"./shortcuts\";\n\n/**\n * Convert editor shortcuts to CodeMirror keybinding format.\n */\nexport function shortcutsToKeyBindings(shortcuts: MarkdownEditorShortcutMap): KeyBinding[] {\n    const keyBindings = [];\n\n    const wrapAction = (action: () => void) => () => {\n        action();\n        return true;\n    };\n\n    for (const [shortcut, action] of Object.entries(shortcuts)) {\n        keyBindings.push({key: shortcut, run: wrapAction(action), preventDefault: true});\n    }\n\n    return keyBindings;\n}\n\n/**\n * Initiate the codemirror instance for the Markdown editor.\n */\nexport async function init(\n    input: HTMLTextAreaElement,\n    shortcuts: MarkdownEditorShortcutMap,\n    domEventHandlers: MarkdownEditorEventMap,\n    onChange: () => void\n): Promise<EditorView> {\n    const Code = await window.importVersioned('code') as CodeModule;\n\n    function onViewUpdate(v: ViewUpdate) {\n        if (v.docChanged) {\n            onChange();\n        }\n    }\n\n    const cm = Code.markdownEditor(\n        input,\n        onViewUpdate,\n        domEventHandlers,\n        shortcutsToKeyBindings(shortcuts),\n    );\n\n    // Add editor view to the window for easy access/debugging.\n    // Not part of official API/Docs\n    // @ts-ignore\n    window.mdEditorView = cm;\n\n    return cm;\n}\n"
  },
  {
    "path": "resources/js/markdown/common-events.ts",
    "content": "import {MarkdownEditor} from \"./index.mjs\";\n\nexport interface HtmlOrMarkdown {\n    html: string;\n    markdown: string;\n}\n\nfunction getContentToInsert({html, markdown}: {html: string, markdown: string}): string {\n    return markdown || html;\n}\n\nexport function listenToCommonEvents(editor: MarkdownEditor): void {\n    window.$events.listen('editor::replace', (eventContent: HtmlOrMarkdown) => {\n        const markdown = getContentToInsert(eventContent);\n        editor.actions.replaceContent(markdown);\n    });\n\n    window.$events.listen('editor::append', (eventContent: HtmlOrMarkdown) => {\n        const markdown = getContentToInsert(eventContent);\n        editor.actions.appendContent(markdown);\n    });\n\n    window.$events.listen('editor::prepend', (eventContent: HtmlOrMarkdown) => {\n        const markdown = getContentToInsert(eventContent);\n        editor.actions.prependContent(markdown);\n    });\n\n    window.$events.listen('editor::insert', (eventContent: HtmlOrMarkdown) => {\n        const markdown = getContentToInsert(eventContent);\n        editor.actions.insertContent(markdown);\n    });\n\n    window.$events.listen('editor::focus', () => {\n        editor.actions.focus();\n    });\n}\n"
  },
  {
    "path": "resources/js/markdown/display.ts",
    "content": "import { patchDomFromHtmlString } from '../services/vdom';\nimport {MarkdownEditor} from \"./index.mjs\";\n\nexport class Display {\n    protected editor: MarkdownEditor;\n    protected container: HTMLIFrameElement;\n    protected doc: Document | null = null;\n    protected lastDisplayClick: number = 0;\n\n    constructor(editor: MarkdownEditor) {\n        this.editor = editor;\n        this.container = editor.config.displayEl;\n\n        if (this.container.contentDocument?.readyState === 'complete') {\n            this.onLoad();\n        } else {\n            this.container.addEventListener('load', this.onLoad.bind(this));\n        }\n\n        this.updateVisibility(Boolean(editor.settings.get('showPreview')));\n        editor.settings.onChange('showPreview', (show) => this.updateVisibility(Boolean(show)));\n    }\n\n    protected updateVisibility(show: boolean): void {\n        const wrap = this.container.closest('.markdown-editor-wrap') as HTMLElement;\n        wrap.style.display = show ? '' : 'none';\n    }\n\n    protected onLoad(): void {\n        this.doc = this.container.contentDocument;\n\n        if (!this.doc) return;\n\n        this.loadStylesIntoDisplay();\n        this.doc.body.className = 'page-content';\n\n        // Prevent markdown display link click redirect\n        this.doc.addEventListener('click', this.onDisplayClick.bind(this));\n    }\n\n    protected onDisplayClick(event: MouseEvent): void {\n        const isDblClick = Date.now() - this.lastDisplayClick < 300;\n\n        const link = (event.target as Element).closest('a');\n        if (link !== null) {\n            event.preventDefault();\n            const href = link.getAttribute('href');\n            if (href) {\n                window.open(href);\n            }\n            return;\n        }\n\n        const drawing = (event.target as Element).closest('[drawio-diagram]') as HTMLElement;\n        if (drawing !== null && isDblClick) {\n            this.editor.actions.editDrawing(drawing);\n            return;\n        }\n\n        this.lastDisplayClick = Date.now();\n    }\n\n    protected loadStylesIntoDisplay(): void {\n        if (!this.doc) return;\n\n        this.doc.documentElement.classList.add('markdown-editor-display');\n\n        // Set display to be dark mode if the parent is\n        if (document.documentElement.classList.contains('dark-mode')) {\n            this.doc.documentElement.style.backgroundColor = '#222';\n            this.doc.documentElement.classList.add('dark-mode');\n        }\n\n        this.doc.head.innerHTML = '';\n        const styles = document.head.querySelectorAll('style,link[rel=stylesheet]');\n        for (const style of styles) {\n            const copy = style.cloneNode(true) as HTMLElement;\n            this.doc.head.appendChild(copy);\n        }\n    }\n\n    /**\n     * Patch the display DOM with the given HTML content.\n     */\n    public patchWithHtml(html: string): void {\n        if (!this.doc) return;\n\n        const { body } = this.doc;\n\n        if (body.children.length === 0) {\n            const wrap = document.createElement('div');\n            this.doc.body.append(wrap);\n        }\n\n        const target = body.children[0] as HTMLElement;\n\n        patchDomFromHtmlString(target, html);\n    }\n\n    /**\n     * Scroll to the given block index within the display content.\n     * Will scroll to the end if the index is -1.\n     */\n    public scrollToIndex(index: number): void {\n        const elems = this.doc?.body?.children[0]?.children;\n        if (!elems || elems.length <= index) return;\n\n        const topElem = (index === -1) ? elems[elems.length - 1] : elems[index];\n        (topElem as Element).scrollIntoView({\n            block: 'start',\n            inline: 'nearest',\n            behavior: 'smooth'\n        });\n    }\n}"
  },
  {
    "path": "resources/js/markdown/dom-handlers.ts",
    "content": "import {Clipboard} from \"../services/clipboard\";\nimport {MarkdownEditor} from \"./index.mjs\";\nimport {debounce} from \"../services/util\";\n\n\nexport type MarkdownEditorEventMap = Record<string, (event: any) => void>;\n\nexport function getMarkdownDomEventHandlers(editor: MarkdownEditor): MarkdownEditorEventMap {\n\n    const onScrollDebounced = debounce(editor.actions.syncDisplayPosition.bind(editor.actions), 100, false);\n    let syncActive = editor.settings.get('scrollSync');\n    editor.settings.onChange('scrollSync', val => {\n        syncActive = val;\n    });\n\n    return {\n        // Handle scroll to sync display view\n        scroll: (event: Event) => syncActive && onScrollDebounced(event),\n        // Handle image & content drag n drop\n        drop: (event: DragEvent) => {\n            if (!event.dataTransfer) {\n                return;\n            }\n\n            const templateId = event.dataTransfer.getData('bookstack/template');\n            if (templateId) {\n                event.preventDefault();\n                editor.actions.insertTemplate(templateId, event);\n            }\n\n            const clipboard = new Clipboard(event.dataTransfer);\n            const clipboardImages = clipboard.getImages();\n            if (clipboardImages.length > 0) {\n                event.stopPropagation();\n                event.preventDefault();\n                editor.actions.insertClipboardImages(clipboardImages, event);\n            }\n        },\n        // Handle dragover event to allow as drop-target in chrome\n        dragover: (event: DragEvent) => {\n            event.preventDefault();\n        },\n        // Handle image paste\n        paste: (event: ClipboardEvent) => {\n            if (!event.clipboardData) {\n                return;\n            }\n\n            const clipboard = new Clipboard(event.clipboardData);\n\n            // Don't handle the event ourselves if no items exist of contains table-looking data\n            if (!clipboard.hasItems() || clipboard.containsTabularData()) {\n                return;\n            }\n\n            const images = clipboard.getImages();\n            for (const image of images) {\n                editor.actions.uploadImage(image);\n            }\n        },\n    };\n}"
  },
  {
    "path": "resources/js/markdown/index.mts",
    "content": "import {Markdown} from './markdown';\nimport {Display} from './display';\nimport {Actions} from './actions';\nimport {Settings} from './settings';\nimport {listenToCommonEvents} from './common-events';\nimport {init as initCodemirror} from './codemirror';\nimport {MarkdownEditorInput} from \"./inputs/interface\";\nimport {CodemirrorInput} from \"./inputs/codemirror\";\nimport {TextareaInput} from \"./inputs/textarea\";\nimport {provideShortcutMap} from \"./shortcuts\";\nimport {getMarkdownDomEventHandlers} from \"./dom-handlers\";\n\nexport interface MarkdownEditorConfig {\n    pageId: string;\n    container: Element;\n    displayEl: HTMLIFrameElement;\n    inputEl: HTMLTextAreaElement;\n    drawioUrl: string;\n    settingInputs: HTMLInputElement[];\n    text: Record<string, string>;\n}\n\nexport interface MarkdownEditor {\n    config: MarkdownEditorConfig;\n    display: Display;\n    markdown: Markdown;\n    actions: Actions;\n    input: MarkdownEditorInput;\n    settings: Settings;\n}\n\n/**\n * Initiate a new Markdown editor instance.\n */\nexport async function init(config: MarkdownEditorConfig): Promise<MarkdownEditor> {\n    const editor: MarkdownEditor = {\n        config,\n        markdown: new Markdown(),\n        settings: new Settings(config.settingInputs),\n    } as MarkdownEditor;\n\n    editor.actions = new Actions(editor);\n    editor.display = new Display(editor);\n\n    const eventHandlers = getMarkdownDomEventHandlers(editor);\n    const shortcuts = provideShortcutMap(editor);\n    const onInputChange = () => editor.actions.updateAndRender();\n\n    const initCodemirrorInput: () => Promise<MarkdownEditorInput> = async () => {\n        const codeMirror = await initCodemirror(config.inputEl, shortcuts, eventHandlers, onInputChange);\n        return new CodemirrorInput(codeMirror);\n    };\n    const initTextAreaInput: () => Promise<MarkdownEditorInput> = async () => {\n        return new TextareaInput(config.inputEl, shortcuts, eventHandlers, onInputChange);\n    };\n\n    const isPlainEditor = Boolean(editor.settings.get('plainEditor'));\n    editor.input = await (isPlainEditor ? initTextAreaInput() : initCodemirrorInput());\n    editor.settings.onChange('plainEditor', async (value) => {\n        const isPlain = Boolean(value);\n        const newInput = await (isPlain ? initTextAreaInput() : initCodemirrorInput());\n        editor.input.teardown();\n        editor.input = newInput;\n    });\n\n    listenToCommonEvents(editor);\n\n    return editor;\n}\n\n\n"
  },
  {
    "path": "resources/js/markdown/inputs/codemirror.ts",
    "content": "import {MarkdownEditorInput, MarkdownEditorInputSelection} from \"./interface\";\nimport {EditorView} from \"@codemirror/view\";\nimport {ChangeSpec, TransactionSpec} from \"@codemirror/state\";\n\n\nexport class CodemirrorInput implements MarkdownEditorInput {\n    protected cm: EditorView;\n\n    constructor(cm: EditorView) {\n        this.cm = cm;\n    }\n\n    teardown(): void {\n        this.cm.destroy();\n    }\n\n    focus(): void {\n        if (!this.cm.hasFocus) {\n            this.cm.focus();\n        }\n    }\n\n    getSelection(): MarkdownEditorInputSelection {\n        return this.cm.state.selection.main;\n    }\n\n    getSelectionText(selection?: MarkdownEditorInputSelection): string {\n        selection = selection || this.getSelection();\n        return this.cm.state.sliceDoc(selection.from, selection.to);\n    }\n\n    setSelection(selection: MarkdownEditorInputSelection, scrollIntoView: boolean = false) {\n        this.cm.dispatch({\n            selection: {anchor: selection.from, head: selection.to},\n            scrollIntoView,\n        });\n    }\n\n    getText(): string {\n        return this.cm.state.doc.toString();\n    }\n\n    getTextAboveView(): string {\n        const blockInfo = this.cm.lineBlockAtHeight(this.cm.scrollDOM.scrollTop);\n        return this.cm.state.sliceDoc(0, blockInfo.from);\n    }\n\n    setText(text: string, selection?: MarkdownEditorInputSelection) {\n        selection = selection || this.getSelection();\n        const newDoc = this.cm.state.toText(text);\n        const newSelectFrom = Math.min(selection.from, newDoc.length);\n        const scrollTop = this.cm.scrollDOM.scrollTop;\n        this.dispatchChange(0, this.cm.state.doc.length, text, newSelectFrom);\n        this.focus();\n        window.requestAnimationFrame(() => {\n            this.cm.scrollDOM.scrollTop = scrollTop;\n        });\n    }\n\n    spliceText(from: number, to: number, newText: string, selection: Partial<MarkdownEditorInputSelection> | null = null) {\n        const end = (selection?.from === selection?.to) ? null : selection?.to;\n        this.dispatchChange(from, to, newText, selection?.from, end)\n    }\n\n    appendText(text: string) {\n        const end = this.cm.state.doc.length;\n        this.dispatchChange(end, end, `\\n${text}`);\n    }\n\n    getLineText(lineIndex: number = -1): string {\n        const index = lineIndex > -1 ? lineIndex : this.getSelection().from;\n        return this.cm.state.doc.lineAt(index).text;\n    }\n\n    eventToPosition(event: MouseEvent): MarkdownEditorInputSelection {\n        const cursorPos = this.cm.posAtCoords({x: event.screenX, y: event.screenY}, false);\n        return {from: cursorPos, to: cursorPos};\n    }\n\n    getLineRangeFromPosition(position: number): MarkdownEditorInputSelection {\n        const line = this.cm.state.doc.lineAt(position);\n        return {from: line.from, to: line.to};\n    }\n\n    searchForLineContaining(text: string): MarkdownEditorInputSelection | null {\n        const docText = this.cm.state.doc;\n        let lineCount = 1;\n        let scrollToLine = -1;\n        for (const line of docText.iterLines()) {\n            if (line.includes(text)) {\n                scrollToLine = lineCount;\n                break;\n            }\n            lineCount += 1;\n        }\n\n        if (scrollToLine === -1) {\n            return null;\n        }\n\n        const line = docText.line(scrollToLine);\n        return {from: line.from, to: line.to};\n    }\n\n    /**\n     * Dispatch changes to the editor.\n     */\n    protected dispatchChange(from: number, to: number|null = null, text: string|null = null, selectFrom: number|null = null, selectTo: number|null = null): void {\n        const change: ChangeSpec = {from};\n        if (to) {\n            change.to = to;\n        }\n        if (text) {\n            change.insert = text;\n        }\n        const tr: TransactionSpec = {changes: change};\n\n        if (selectFrom) {\n            tr.selection = {anchor: selectFrom};\n            if (selectTo) {\n                tr.selection.head = selectTo;\n            }\n        }\n\n        this.cm.dispatch(tr);\n    }\n\n}"
  },
  {
    "path": "resources/js/markdown/inputs/interface.ts",
    "content": "\nexport interface MarkdownEditorInputSelection {\n    from: number;\n    to: number;\n}\n\nexport interface MarkdownEditorInput {\n    /**\n     * Focus on the editor.\n     */\n    focus(): void;\n\n    /**\n     * Get the current selection range.\n     */\n    getSelection(): MarkdownEditorInputSelection;\n\n    /**\n     * Get the text of the given (or current) selection range.\n     */\n    getSelectionText(selection?: MarkdownEditorInputSelection): string;\n\n    /**\n     * Set the selection range of the editor.\n     */\n    setSelection(selection: MarkdownEditorInputSelection, scrollIntoView: boolean): void;\n\n    /**\n     * Get the full text of the input.\n     */\n    getText(): string;\n\n    /**\n     * Get just the text which is above (out) the current view range.\n     * This is used for position estimation.\n     */\n    getTextAboveView(): string;\n\n    /**\n     * Set the full text of the input.\n     * Optionally can provide a selection to restore after setting text.\n     */\n    setText(text: string, selection?: MarkdownEditorInputSelection): void;\n\n    /**\n     * Splice in/out text within the input.\n     * Optionally can provide a selection to restore after setting text.\n     */\n    spliceText(from: number, to: number, newText: string, selection: Partial<MarkdownEditorInputSelection>|null): void;\n\n    /**\n     * Append text to the end of the editor.\n     */\n    appendText(text: string): void;\n\n    /**\n     * Get the text of the given line number otherwise the text\n     * of the current selected line.\n     */\n    getLineText(lineIndex:number): string;\n\n    /**\n     * Get a selection representing the line range from the given position.\n     */\n    getLineRangeFromPosition(position: number): MarkdownEditorInputSelection;\n\n    /**\n     * Convert the given event position to a selection position within the input.\n     */\n    eventToPosition(event: MouseEvent): MarkdownEditorInputSelection;\n\n    /**\n     * Search and return a line range which includes the provided text.\n     */\n    searchForLineContaining(text: string): MarkdownEditorInputSelection|null;\n\n    /**\n     * Tear down the input.\n     */\n    teardown(): void;\n}"
  },
  {
    "path": "resources/js/markdown/inputs/textarea.ts",
    "content": "import {MarkdownEditorInput, MarkdownEditorInputSelection} from \"./interface\";\nimport {MarkdownEditorShortcutMap} from \"../shortcuts\";\nimport {MarkdownEditorEventMap} from \"../dom-handlers\";\nimport {debounce} from \"../../services/util\";\n\ntype UndoStackEntry = {\n    content: string;\n    selection: MarkdownEditorInputSelection;\n}\n\nclass UndoStack {\n    protected onChangeDebounced: (callback: () => UndoStackEntry) => void;\n\n    protected stack: UndoStackEntry[] = [];\n    protected pointer: number = -1;\n    protected lastActionTime: number = 0;\n\n    constructor() {\n        this.onChangeDebounced = debounce(this.onChange, 1000, false);\n    }\n\n    undo(): UndoStackEntry|null {\n        if (this.pointer < 1) {\n            return null;\n        }\n\n        this.lastActionTime = Date.now();\n        this.pointer -= 1;\n        return this.stack[this.pointer];\n    }\n\n    redo(): UndoStackEntry|null {\n        const atEnd = this.pointer === this.stack.length - 1;\n        if (atEnd) {\n            return null;\n        }\n\n        this.lastActionTime = Date.now();\n        this.pointer++;\n        return this.stack[this.pointer];\n    }\n\n    push(getValueCallback: () => UndoStackEntry): void {\n        // Ignore changes made via undo/redo actions\n        if (Date.now() - this.lastActionTime < 100) {\n            return;\n        }\n\n        this.onChangeDebounced(getValueCallback);\n    }\n\n    protected onChange(getValueCallback: () => UndoStackEntry) {\n        // Trim the end of the stack from the pointer since we're branching away\n        if (this.pointer !== this.stack.length - 1) {\n            this.stack = this.stack.slice(0, this.pointer)\n        }\n\n        this.stack.push(getValueCallback());\n\n        // Limit stack size\n        if (this.stack.length > 50) {\n            this.stack = this.stack.slice(this.stack.length - 50);\n        }\n\n        this.pointer = this.stack.length - 1;\n    }\n}\n\nexport class TextareaInput implements MarkdownEditorInput {\n\n    protected input: HTMLTextAreaElement;\n    protected shortcuts: MarkdownEditorShortcutMap;\n    protected events: MarkdownEditorEventMap;\n    protected onChange: () => void;\n    protected eventController = new AbortController();\n    protected undoStack = new UndoStack();\n\n    protected textSizeCache: {x: number; y: number}|null = null;\n\n    constructor(\n        input: HTMLTextAreaElement,\n        shortcuts: MarkdownEditorShortcutMap,\n        events: MarkdownEditorEventMap,\n        onChange: () => void\n    ) {\n        this.input = input;\n        this.shortcuts = shortcuts;\n        this.events = events;\n        this.onChange = onChange;\n\n        this.onKeyDown = this.onKeyDown.bind(this);\n        this.configureLocalShortcuts();\n        this.configureListeners();\n\n        this.input.style.removeProperty(\"display\");\n        this.undoStack.push(() => ({content: this.getText(), selection: this.getSelection()}));\n    }\n\n    teardown() {\n        this.eventController.abort('teardown');\n    }\n\n    configureLocalShortcuts(): void {\n        this.shortcuts['Mod-z'] = () => {\n            const undoEntry = this.undoStack.undo();\n            if (undoEntry) {\n                this.setText(undoEntry.content);\n                this.setSelection(undoEntry.selection, false);\n            }\n        };\n        this.shortcuts['Mod-y'] = () => {\n            const redoContent = this.undoStack.redo();\n            if (redoContent) {\n                this.setText(redoContent.content);\n                this.setSelection(redoContent.selection, false);\n            }\n        }\n    }\n\n    configureListeners(): void {\n        // Keyboard shortcuts\n        this.input.addEventListener('keydown', this.onKeyDown, {signal: this.eventController.signal});\n\n        // Shared event listeners\n        for (const [name, listener] of Object.entries(this.events)) {\n            this.input.addEventListener(name, listener, {signal: this.eventController.signal});\n        }\n\n        // Input change handling\n        this.input.addEventListener('input', () => {\n            this.onChange();\n            this.undoStack.push(() => ({content: this.input.value, selection: this.getSelection()}));\n        }, {signal: this.eventController.signal});\n    }\n\n    onKeyDown(e: KeyboardEvent) {\n        const isApple = navigator.platform.startsWith(\"Mac\") || navigator.platform === \"iPhone\";\n        const key = e.key.length > 1 ? e.key : e.key.toLowerCase();\n        const keyParts = [\n            e.shiftKey ? 'Shift' : null,\n            isApple && e.metaKey ? 'Mod' : null,\n            !isApple && e.ctrlKey ? 'Mod' : null,\n            key,\n        ];\n\n        const keyString = keyParts.filter(Boolean).join('-');\n        if (this.shortcuts[keyString]) {\n            e.preventDefault();\n            this.shortcuts[keyString]();\n        }\n    }\n\n    appendText(text: string): void {\n        this.input.value += `\\n${text}`;\n        this.input.dispatchEvent(new Event('input'));\n    }\n\n    eventToPosition(event: MouseEvent): MarkdownEditorInputSelection {\n        const eventCoords = this.mouseEventToTextRelativeCoords(event);\n        return this.inputPositionToSelection(eventCoords.x, eventCoords.y);\n    }\n\n    focus(): void {\n        this.input.focus();\n    }\n\n    getLineRangeFromPosition(position: number): MarkdownEditorInputSelection {\n        const lines = this.getText().split('\\n');\n        let lineStart = 0;\n        for (let i = 0; i < lines.length; i++) {\n            const line = lines[i];\n            const lineEnd = lineStart + line.length;\n            if (position <= lineEnd) {\n                return {from: lineStart, to: lineEnd};\n            }\n            lineStart = lineEnd + 1;\n        }\n\n        return {from: 0, to: 0};\n    }\n\n    getLineText(lineIndex: number): string {\n        const text = this.getText();\n        const lines = text.split(\"\\n\");\n        return lines[lineIndex] || '';\n    }\n\n    getSelection(): MarkdownEditorInputSelection {\n        return {from: this.input.selectionStart, to: this.input.selectionEnd};\n    }\n\n    getSelectionText(selection?: MarkdownEditorInputSelection): string {\n        const text = this.getText();\n        const range = selection || this.getSelection();\n        return text.slice(range.from, range.to);\n    }\n\n    getText(): string {\n        return this.input.value;\n    }\n\n    getTextAboveView(): string {\n        const scrollTop = this.input.scrollTop;\n        const selection = this.inputPositionToSelection(0, scrollTop);\n        return this.getSelectionText({from: 0, to: selection.to});\n    }\n\n    searchForLineContaining(text: string): MarkdownEditorInputSelection | null {\n        const textPosition = this.getText().indexOf(text);\n        if (textPosition > -1) {\n            return this.getLineRangeFromPosition(textPosition);\n        }\n\n        return null;\n    }\n\n    setSelection(selection: MarkdownEditorInputSelection, scrollIntoView: boolean): void {\n        this.input.selectionStart = selection.from;\n        this.input.selectionEnd = selection.to;\n    }\n\n    setText(text: string, selection?: MarkdownEditorInputSelection): void {\n        this.input.value = text;\n        this.input.dispatchEvent(new Event('input'));\n        if (selection) {\n            this.setSelection(selection, false);\n        }\n    }\n\n    spliceText(from: number, to: number, newText: string, selection: Partial<MarkdownEditorInputSelection> | null): void {\n        const text = this.getText();\n        const updatedText = text.slice(0, from) + newText + text.slice(to);\n        this.setText(updatedText);\n        if (selection && selection.from) {\n            const newSelection = {from: selection.from, to: selection.to || selection.from};\n            this.setSelection(newSelection, false);\n        }\n    }\n\n    protected measureTextSize(): {x: number; y: number} {\n        if (this.textSizeCache) {\n            return this.textSizeCache;\n        }\n\n        const el = document.createElement(\"div\");\n        el.textContent = `a\\nb`;\n        const inputStyles = window.getComputedStyle(this.input)\n        el.style.font = inputStyles.font;\n        el.style.lineHeight = inputStyles.lineHeight;\n        el.style.padding = '0px';\n        el.style.display = 'inline-block';\n        el.style.visibility = 'hidden';\n        el.style.position = 'absolute';\n        el.style.whiteSpace = 'pre';\n        this.input.after(el);\n\n        const bounds = el.getBoundingClientRect();\n        el.remove();\n        this.textSizeCache = {\n            x: bounds.width,\n            y: bounds.height / 2,\n        };\n        return this.textSizeCache;\n    }\n\n    protected measureLineCharCount(textWidth: number): number {\n        const inputStyles = window.getComputedStyle(this.input);\n        const paddingLeft = Number(inputStyles.paddingLeft.replace('px', ''));\n        const paddingRight = Number(inputStyles.paddingRight.replace('px', ''));\n        const width = Number(inputStyles.width.replace('px', ''));\n        const textSpace = width - (paddingLeft + paddingRight);\n\n        return Math.floor(textSpace / textWidth);\n    }\n\n    protected mouseEventToTextRelativeCoords(event: MouseEvent): {x: number; y: number} {\n        const inputBounds = this.input.getBoundingClientRect();\n        const inputStyles = window.getComputedStyle(this.input);\n        const paddingTop = Number(inputStyles.paddingTop.replace('px', ''));\n        const paddingLeft = Number(inputStyles.paddingLeft.replace('px', ''));\n\n        const xPos = Math.max(event.clientX - (inputBounds.left + paddingLeft), 0);\n        const yPos = Math.max((event.clientY - (inputBounds.top + paddingTop)) + this.input.scrollTop, 0);\n\n        return {x: xPos, y: yPos};\n    }\n\n    protected inputPositionToSelection(x: number, y: number): MarkdownEditorInputSelection {\n        const textSize = this.measureTextSize();\n        const lineWidth = this.measureLineCharCount(textSize.x);\n\n        const lines = this.getText().split('\\n');\n\n        let currY = 0;\n        let currPos = 0;\n        for (const line of lines) {\n            let linePos = 0;\n            const wrapCount = Math.max(Math.ceil(line.length / lineWidth), 1);\n            for (let i = 0; i < wrapCount; i++) {\n                currY += textSize.y;\n                if (currY > y) {\n                    const targetX = Math.floor(x / textSize.x);\n                    const maxPos = Math.min(currPos + linePos + targetX, currPos + line.length);\n                    return {from: maxPos, to: maxPos};\n                }\n\n                linePos += lineWidth;\n            }\n\n            currPos += line.length + 1;\n        }\n\n        return this.getSelection();\n    }\n}"
  },
  {
    "path": "resources/js/markdown/markdown.ts",
    "content": "import MarkdownIt from 'markdown-it';\n// @ts-ignore\nimport mdTasksLists from 'markdown-it-task-lists';\n\nexport class Markdown {\n    protected renderer: MarkdownIt;\n\n    constructor() {\n        this.renderer = new MarkdownIt({html: true});\n        this.renderer.use(mdTasksLists, {label: true});\n    }\n\n    /**\n     * Get the front-end render used to convert Markdown to HTML.\n     */\n    getRenderer(): MarkdownIt {\n        return this.renderer;\n    }\n\n    /**\n     * Convert the given Markdown to HTML.\n     */\n    render(markdown: string): string {\n        return this.renderer.render(markdown);\n    }\n\n}\n"
  },
  {
    "path": "resources/js/markdown/settings.ts",
    "content": "type ChangeListener = (value: boolean|number) => void;\n\nexport class Settings {\n    protected changeListeners: Record<string, ChangeListener[]> = {};\n\n    protected settingMap: Record<string, boolean|number> = {\n        scrollSync: true,\n        showPreview: true,\n        editorWidth: 50,\n        plainEditor: false,\n    };\n\n    constructor(settingInputs: HTMLInputElement[]) {\n        this.loadFromLocalStorage();\n        this.applyToInputs(settingInputs);\n        this.listenToInputChanges(settingInputs);\n    }\n\n    protected applyToInputs(inputs: HTMLInputElement[]): void {\n        for (const input of inputs) {\n            const name = input.getAttribute('name')?.replace('md-', '');\n            if (name && name in this.settingMap) {\n                const value = this.settingMap[name];\n                if (typeof value === 'boolean') {\n                    input.checked = value;\n                } else {\n                    input.value = value.toString();\n                }\n            }\n        }\n    }\n\n    protected listenToInputChanges(inputs: HTMLInputElement[]): void {\n        for (const input of inputs) {\n            input.addEventListener('change', () => {\n                const name = input.getAttribute('name')?.replace('md-', '');\n                if (name && name in this.settingMap) {\n                    let value = (input.type === 'checkbox') ? input.checked : Number(input.value);\n                    this.set(name, value);\n                }\n            });\n        }\n    }\n\n    protected loadFromLocalStorage(): void {\n        const lsValString = window.localStorage.getItem('md-editor-settings');\n        if (!lsValString) {\n            return;\n        }\n\n        try {\n            const lsVals = JSON.parse(lsValString);\n            for (const [key, value] of Object.entries(lsVals)) {\n                if (value !== null && value !== undefined && key in this.settingMap) {\n                    this.settingMap[key] = value as boolean|number;\n                }\n            }\n        } catch (error) {\n            console.warn('Failed to parse settings from localStorage:', error);\n        }\n    }\n\n    public set(key: string, value: boolean|number): void {\n        this.settingMap[key] = value;\n        window.localStorage.setItem('md-editor-settings', JSON.stringify(this.settingMap));\n\n        const listeners = this.changeListeners[key] || [];\n        for (const listener of listeners) {\n            listener(value);\n        }\n    }\n\n    public get(key: string): number|boolean|null {\n        return this.settingMap[key] ?? null;\n    }\n\n    public onChange(key: string, callback: ChangeListener): void {\n        const listeners = this.changeListeners[key] || [];\n        listeners.push(callback);\n        this.changeListeners[key] = listeners;\n    }\n}"
  },
  {
    "path": "resources/js/markdown/shortcuts.ts",
    "content": "import {MarkdownEditor} from \"./index.mjs\";\n\nexport type MarkdownEditorShortcutMap = Record<string, () => void>;\n\n/**\n * Provide shortcuts for the editor instance.\n */\nexport function provideShortcutMap(editor: MarkdownEditor): MarkdownEditorShortcutMap {\n    const shortcuts: MarkdownEditorShortcutMap = {};\n\n    // Insert Image shortcut\n    shortcuts['Shift-Mod-i'] = () => editor.actions.insertImage();\n\n    // Save draft\n    shortcuts['Mod-s'] = () => window.$events.emit('editor-save-draft');\n\n    // Save page\n    shortcuts['Mod-Enter'] = () => window.$events.emit('editor-save-page');\n\n    // Show link selector\n    shortcuts['Shift-Mod-k'] = () => editor.actions.showLinkSelector();\n\n    // Insert Link\n    shortcuts['Mod-k'] = () => editor.actions.insertLink();\n\n    // FormatShortcuts\n    shortcuts['Mod-1'] = () => editor.actions.replaceLineStart('##');\n    shortcuts['Mod-2'] = () => editor.actions.replaceLineStart('###');\n    shortcuts['Mod-3'] = () => editor.actions.replaceLineStart('####');\n    shortcuts['Mod-4'] = () => editor.actions.replaceLineStart('#####');\n    shortcuts['Mod-5'] = () => editor.actions.replaceLineStart('');\n    shortcuts['Mod-d'] = () => editor.actions.replaceLineStart('');\n    shortcuts['Mod-6'] = () => editor.actions.replaceLineStart('>');\n    shortcuts['Mod-q'] = () => editor.actions.replaceLineStart('>');\n    shortcuts['Mod-7'] = () => editor.actions.wrapSelection('\\n```\\n', '\\n```');\n    shortcuts['Mod-8'] = () => editor.actions.wrapSelection('`', '`');\n    shortcuts['Shift-Mod-e'] = () => editor.actions.wrapSelection('`', '`');\n    shortcuts['Mod-9'] = () => editor.actions.cycleCalloutTypeAtSelection();\n    shortcuts['Mod-p'] = () => editor.actions.replaceLineStart('-');\n    shortcuts['Mod-o'] = () => editor.actions.replaceLineStartForOrderedList();\n\n    return shortcuts;\n}\n"
  },
  {
    "path": "resources/js/services/__tests__/translations.test.ts",
    "content": "import {Translator} from \"../translations\";\n\n\ndescribe('Translations Service', () => {\n\n    let $trans: Translator;\n\n    beforeEach(() => {\n        $trans = new Translator();\n    });\n\n    describe('choice()', () => {\n\n        test('it pluralises as expected', () => {\n\n            const cases = [\n                {\n                    translation: `cat`, count: 10000,\n                    expected: `cat`,\n                },\n                {\n                    translation: `cat|cats`, count: 1,\n                    expected: `cat`,\n                },\n                {\n                    translation: `cat|cats`, count: 0,\n                    expected: `cats`,\n                },\n                {\n                    translation: `cat|cats`, count: 2,\n                    expected: `cats`,\n                },\n                {\n                    translation: `{0} cat|[1,100] dog|[100,*] turtle`, count: 0,\n                    expected: `cat`,\n                },\n                {\n                    translation: `{0} cat|[1,100] dog|[100,*] turtle`, count: 40,\n                    expected: `dog`,\n                },\n                {\n                    translation: `{0} cat|[1,100] dog|[100,*] turtle`, count: 101,\n                    expected: `turtle`,\n                },\n            ];\n\n            for (const testCase of cases) {\n                const output = $trans.choice(testCase.translation, testCase.count, {});\n                expect(output).toEqual(testCase.expected);\n            }\n        });\n\n        test('it replaces as expected', () => {\n            const caseA = $trans.choice(`{0} cat|[1,100] :count dog|[100,*] turtle`, 4, {count: '5'});\n            expect(caseA).toEqual('5 dog');\n\n            const caseB = $trans.choice(`an :a :b :c dinosaur|many`, 1, {a: 'orange', b: 'angry', c: 'big'});\n            expect(caseB).toEqual('an orange angry big dinosaur');\n        });\n\n        test('it provides count as a replacement by default', () => {\n            const caseA = $trans.choice(`:count cats|:count dogs`, 4);\n            expect(caseA).toEqual('4 dogs');\n        });\n\n        test('not provided replacements are left as-is', () => {\n            const caseA = $trans.choice(`An :a dog`, 5, {});\n            expect(caseA).toEqual('An :a dog');\n        });\n\n    });\n});"
  },
  {
    "path": "resources/js/services/animations.ts",
    "content": "/**\n * Used in the function below to store references of clean-up functions.\n * Used to ensure only one transitionend function exists at any time.\n */\nconst animateStylesCleanupMap: WeakMap<object, any> = new WeakMap();\n\n/**\n * Animate the css styles of an element using FLIP animation techniques.\n * Styles must be an object where the keys are style rule names and the values\n * are an array of two items in the format [initialValue, finalValue]\n */\nfunction animateStyles(\n    element: HTMLElement,\n    styles: Record<string, string[]>,\n    animTime: number = 400,\n    onComplete: Function | null = null\n): void {\n    const styleNames = Object.keys(styles);\n    for (const style of styleNames) {\n        element.style.setProperty(style, styles[style][0]);\n    }\n\n    const cleanup = () => {\n        for (const style of styleNames) {\n            element.style.removeProperty(style);\n        }\n        element.style.removeProperty('transition');\n        element.removeEventListener('transitionend', cleanup);\n        animateStylesCleanupMap.delete(element);\n        if (onComplete) onComplete();\n    };\n\n    setTimeout(() => {\n        element.style.transition = `all ease-in-out ${animTime}ms`;\n        for (const style of styleNames) {\n            element.style.setProperty(style, styles[style][1]);\n        }\n\n        element.addEventListener('transitionend', cleanup);\n        animateStylesCleanupMap.set(element, cleanup);\n    }, 15);\n}\n\n/**\n * Run the active cleanup action for the given element.\n */\nfunction cleanupExistingElementAnimation(element: Element) {\n    if (animateStylesCleanupMap.has(element)) {\n        const oldCleanup = animateStylesCleanupMap.get(element);\n        oldCleanup();\n    }\n}\n\n/**\n * Fade in the given element.\n */\nexport function fadeIn(element: HTMLElement, animTime: number = 400, onComplete: Function | null = null): void {\n    cleanupExistingElementAnimation(element);\n    element.style.display = 'block';\n    animateStyles(element, {\n        'opacity': ['0', '1'],\n    }, animTime, () => {\n        if (onComplete) onComplete();\n    });\n}\n\n/**\n * Fade out the given element.\n */\nexport function fadeOut(element: HTMLElement, animTime: number = 400, onComplete: Function | null = null): void {\n    cleanupExistingElementAnimation(element);\n    animateStyles(element, {\n        'opacity': ['1', '0'],\n    }, animTime, () => {\n        element.style.display = 'none';\n        if (onComplete) onComplete();\n    });\n}\n\n/**\n * Hide the element by sliding the contents upwards.\n */\nexport function slideUp(element: HTMLElement, animTime: number = 400) {\n    cleanupExistingElementAnimation(element);\n    const currentHeight = element.getBoundingClientRect().height;\n    const computedStyles = getComputedStyle(element);\n    const currentPaddingTop = computedStyles.getPropertyValue('padding-top');\n    const currentPaddingBottom = computedStyles.getPropertyValue('padding-bottom');\n    const animStyles = {\n        'max-height': [`${currentHeight}px`, '0px'],\n        'overflow': ['hidden', 'hidden'],\n        'padding-top': [currentPaddingTop, '0px'],\n        'padding-bottom': [currentPaddingBottom, '0px'],\n    };\n\n    animateStyles(element, animStyles, animTime, () => {\n        element.style.display = 'none';\n    });\n}\n\n/**\n * Show the given element by expanding the contents.\n */\nexport function slideDown(element: HTMLElement, animTime: number = 400) {\n    cleanupExistingElementAnimation(element);\n    element.style.display = 'block';\n    const targetHeight = element.getBoundingClientRect().height;\n    const computedStyles = getComputedStyle(element);\n    const targetPaddingTop = computedStyles.getPropertyValue('padding-top');\n    const targetPaddingBottom = computedStyles.getPropertyValue('padding-bottom');\n    const animStyles = {\n        'max-height': ['0px', `${targetHeight}px`],\n        'overflow': ['hidden', 'hidden'],\n        'padding-top': ['0px', targetPaddingTop],\n        'padding-bottom': ['0px', targetPaddingBottom],\n    };\n\n    animateStyles(element, animStyles, animTime);\n}\n\n/**\n * Transition the height of the given element between two states.\n * Call with first state, and you'll receive a function in return.\n * Call the returned function in the second state to animate between those two states.\n * If animating to/from 0-height use the slide-up/slide down as easier alternatives.\n */\nexport function transitionHeight(element: HTMLElement, animTime: number = 400): () => void {\n    const startHeight = element.getBoundingClientRect().height;\n    const initialComputedStyles = getComputedStyle(element);\n    const startPaddingTop = initialComputedStyles.getPropertyValue('padding-top');\n    const startPaddingBottom = initialComputedStyles.getPropertyValue('padding-bottom');\n\n    return () => {\n        cleanupExistingElementAnimation(element);\n        const targetHeight = element.getBoundingClientRect().height;\n        const computedStyles = getComputedStyle(element);\n        const targetPaddingTop = computedStyles.getPropertyValue('padding-top');\n        const targetPaddingBottom = computedStyles.getPropertyValue('padding-bottom');\n        const animStyles = {\n            'height': [`${startHeight}px`, `${targetHeight}px`],\n            'overflow': ['hidden', 'hidden'],\n            'padding-top': [startPaddingTop, targetPaddingTop],\n            'padding-bottom': [startPaddingBottom, targetPaddingBottom],\n        };\n\n        animateStyles(element, animStyles, animTime);\n    };\n}\n"
  },
  {
    "path": "resources/js/services/clipboard.ts",
    "content": "export class Clipboard {\n\n    protected data: DataTransfer;\n\n    constructor(clipboardData: DataTransfer) {\n        this.data = clipboardData;\n    }\n\n    /**\n     * Check if the clipboard has any items.\n     */\n    hasItems(): boolean {\n        return Boolean(this.data) && Boolean(this.data.types) && this.data.types.length > 0;\n    }\n\n    /**\n     * Check if the given event has tabular-looking data in the clipboard.\n     */\n    containsTabularData(): boolean {\n        const rtfData = this.data.getData('text/rtf');\n        return !!rtfData && rtfData.includes('\\\\trowd');\n    }\n\n    /**\n     * Get the images that are in the clipboard data.\n     */\n    getImages(): File[] {\n        return this.getFiles().filter(f => f.type.includes('image'));\n    }\n\n    /**\n     * Get the files included in the clipboard data.\n     */\n    getFiles(): File[] {\n        const {files} = this.data;\n        return [...files];\n    }\n}\n\nexport async function copyTextToClipboard(text: string) {\n    if (window.isSecureContext && navigator.clipboard) {\n        await navigator.clipboard.writeText(text);\n        return;\n    }\n\n    // Backup option where we can't use the navigator.clipboard API\n    const tempInput = document.createElement('textarea');\n    tempInput.setAttribute('style', 'position: absolute; left: -1000px; top: -1000px;');\n    tempInput.value = text;\n    document.body.appendChild(tempInput);\n    tempInput.select();\n    document.execCommand('copy');\n    document.body.removeChild(tempInput);\n}\n"
  },
  {
    "path": "resources/js/services/components.ts",
    "content": "import {kebabToCamel, camelToKebab} from './text';\nimport {Component} from \"../components/component\";\n\n/**\n * Parse out the element references within the given element\n * for the given component name.\n */\nfunction parseRefs(name: string, element: HTMLElement):\n    {refs: Record<string, HTMLElement>, manyRefs: Record<string, HTMLElement[]>} {\n    const refs: Record<string, HTMLElement> = {};\n    const manyRefs: Record<string, HTMLElement[]> = {};\n\n    const prefix = `${name}@`;\n    const selector = `[refs*=\"${prefix}\"]`;\n    const refElems = [...element.querySelectorAll(selector)];\n    if (element.matches(selector)) {\n        refElems.push(element);\n    }\n\n    for (const el of refElems as HTMLElement[]) {\n        const refNames = (el.getAttribute('refs') || '')\n            .split(' ')\n            .filter(str => str.startsWith(prefix))\n            .map(str => str.replace(prefix, ''))\n            .map(kebabToCamel);\n        for (const ref of refNames) {\n            refs[ref] = el;\n            if (typeof manyRefs[ref] === 'undefined') {\n                manyRefs[ref] = [];\n            }\n            manyRefs[ref].push(el);\n        }\n    }\n    return {refs, manyRefs};\n}\n\n/**\n * Parse out the element component options.\n */\nfunction parseOpts(componentName: string, element: HTMLElement): Record<string, string> {\n    const opts: Record<string, string> = {};\n    const prefix = `option:${componentName}:`;\n    for (const {name, value} of element.attributes) {\n        if (name.startsWith(prefix)) {\n            const optName = name.replace(prefix, '');\n            opts[kebabToCamel(optName)] = value || '';\n        }\n    }\n    return opts;\n}\n\nexport class ComponentStore {\n    /**\n     * A mapping of active components keyed by name, with values being arrays of component\n     * instances since there can be multiple components of the same type.\n     */\n    protected components: Record<string, Component[]> = {};\n\n    /**\n     * A mapping of component class models, keyed by name.\n     */\n    protected componentModelMap: Record<string, typeof Component> = {};\n\n    /**\n     * A mapping of active component maps, keyed by the element components are assigned to.\n     */\n    protected elementComponentMap: WeakMap<HTMLElement, Record<string, Component>> = new WeakMap();\n\n    /**\n     * Initialize a component instance on the given dom element.\n     */\n     protected initComponent(name: string, element: HTMLElement): void {\n        const ComponentModel = this.componentModelMap[name];\n        if (ComponentModel === undefined) return;\n\n        // Create our component instance\n        let instance: Component|null = null;\n        try {\n            instance = new ComponentModel();\n            instance.$name = name;\n            instance.$el = element;\n            const allRefs = parseRefs(name, element);\n            instance.$refs = allRefs.refs;\n            instance.$manyRefs = allRefs.manyRefs;\n            instance.$opts = parseOpts(name, element);\n            instance.setup();\n        } catch (e) {\n            console.error('Failed to create component', e, name, element);\n        }\n\n        if (!instance) {\n            return;\n        }\n\n        // Add to global listing\n        if (typeof this.components[name] === 'undefined') {\n            this.components[name] = [];\n        }\n        this.components[name].push(instance);\n\n        // Add to element mapping\n        const elComponents = this.elementComponentMap.get(element) || {};\n        elComponents[name] = instance;\n        this.elementComponentMap.set(element, elComponents);\n    }\n\n    /**\n     * Initialize all components found within the given element.\n     */\n    public init(parentElement: Document|HTMLElement = document) {\n        const componentElems = parentElement.querySelectorAll('[component],[components]');\n\n        for (const el of componentElems) {\n            const componentNames = `${el.getAttribute('component') || ''} ${(el.getAttribute('components'))}`.toLowerCase().split(' ').filter(Boolean);\n            for (const name of componentNames) {\n                this.initComponent(name, el as HTMLElement);\n            }\n        }\n    }\n\n    /**\n     * Register the given component mapping into the component system.\n     * @param {Object<String, ObjectConstructor<Component>>} mapping\n     */\n    public register(mapping: Record<string, typeof Component>) {\n        const keys = Object.keys(mapping);\n        for (const key of keys) {\n            this.componentModelMap[camelToKebab(key)] = mapping[key];\n        }\n    }\n\n    /**\n     * Get the first component of the given name.\n     */\n    public first(name: string): Component|null {\n        return (this.components[name] || [null])[0];\n    }\n\n    /**\n     * Get all the components of the given name.\n     */\n    public get<T extends Component>(name: string): T[] {\n        return (this.components[name] || []) as T[];\n    }\n\n    /**\n     * Get the first component, of the given name, that's assigned to the given element.\n     */\n    public firstOnElement(element: HTMLElement, name: string): Component|null {\n        const elComponents = this.elementComponentMap.get(element) || {};\n        return elComponents[name] || null;\n    }\n\n    public allWithinElement<T extends Component>(element: HTMLElement, name: string): T[] {\n        const components = this.get<T>(name);\n        return components.filter(c => element.contains(c.$el));\n    }\n}\n"
  },
  {
    "path": "resources/js/services/dates.ts",
    "content": "export function getCurrentDay(): string {\n    const date = new Date();\n    const month = date.getMonth() + 1;\n    const day = date.getDate();\n\n    return `${date.getFullYear()}-${(month > 9 ? '' : '0') + month}-${(day > 9 ? '' : '0') + day}`;\n}\n\nexport function utcTimeStampToLocalTime(timestamp: number): string {\n    const date = new Date(timestamp * 1000);\n    const hours = date.getHours();\n    const mins = date.getMinutes();\n    return `${(hours > 9 ? '' : '0') + hours}:${(mins > 9 ? '' : '0') + mins}`;\n}\n"
  },
  {
    "path": "resources/js/services/dom.ts",
    "content": "import {cyrb53} from \"./util\";\n\n/**\n * Check if the given param is a HTMLElement\n */\nexport function isHTMLElement(el: any): el is HTMLElement {\n    return el instanceof HTMLElement;\n}\n\n/**\n * Create a new element with the given attrs and children.\n * Children can be a string for text nodes or other elements.\n */\nexport function elem(tagName: string, attrs: Record<string, string> = {}, children: Element[]|string[] = []): HTMLElement {\n    const el = document.createElement(tagName);\n\n    for (const [key, val] of Object.entries(attrs)) {\n        if (val === null) {\n            el.removeAttribute(key);\n        } else {\n            el.setAttribute(key, val);\n        }\n    }\n\n    for (const child of children) {\n        if (typeof child === 'string') {\n            el.append(document.createTextNode(child));\n        } else {\n            el.append(child);\n        }\n    }\n\n    return el;\n}\n\n/**\n * Run the given callback against each element that matches the given selector.\n */\nexport function forEach(selector: string, callback: (el: Element) => any) {\n    const elements = document.querySelectorAll(selector);\n    for (const element of elements) {\n        callback(element);\n    }\n}\n\n/**\n * Helper to listen to multiple DOM events\n */\nexport function onEvents(listenerElement: Element|null, events: string[], callback: (e: Event) => any): void {\n    if (listenerElement) {\n        for (const eventName of events) {\n            listenerElement.addEventListener(eventName, callback);\n        }\n    }\n}\n\n/**\n * Helper to run an action when an element is selected.\n * A \"select\" is made to be accessible, So can be a click, space-press or enter-press.\n */\nexport function onSelect(elements: HTMLElement|HTMLElement[], callback: (e: Event) => any): void {\n    if (!Array.isArray(elements)) {\n        elements = [elements];\n    }\n\n    for (const listenerElement of elements) {\n        listenerElement.addEventListener('click', callback);\n        listenerElement.addEventListener('keydown', event => {\n            if (event.key === 'Enter' || event.key === ' ') {\n                event.preventDefault();\n                callback(event);\n            }\n        });\n    }\n}\n\n/**\n * Listen to key press on the given element(s).\n */\nfunction onKeyPress(key: string, elements: HTMLElement|HTMLElement[], callback: (e: KeyboardEvent) => any): void {\n    if (!Array.isArray(elements)) {\n        elements = [elements];\n    }\n\n    const listener = (event: KeyboardEvent) => {\n        if (event.key === key) {\n            callback(event);\n        }\n    };\n\n    elements.forEach(e => e.addEventListener('keydown', listener));\n}\n\n/**\n * Listen to enter press on the given element(s).\n */\nexport function onEnterPress(elements: HTMLElement|HTMLElement[], callback: (e: KeyboardEvent) => any): void {\n    onKeyPress('Enter', elements, callback);\n}\n\n/**\n * Listen to escape press on the given element(s).\n */\nexport function onEscapePress(elements: HTMLElement|HTMLElement[], callback: (e: KeyboardEvent) => any): void {\n    onKeyPress('Escape', elements, callback);\n}\n\n/**\n * Set a listener on an element for an event emitted by a child\n * matching the given childSelector param.\n * Used in a similar fashion to jQuery's $('listener').on('eventName', 'childSelector', callback)\n */\nexport function onChildEvent(\n    listenerElement: HTMLElement,\n    childSelector: string,\n    eventName: string,\n    callback: (this: HTMLElement, e: Event, child: HTMLElement) => any\n): void {\n    listenerElement.addEventListener(eventName, (event: Event) => {\n        const matchingChild = (event.target as HTMLElement|null)?.closest(childSelector) as HTMLElement;\n        if (matchingChild) {\n            callback.call(matchingChild, event, matchingChild);\n        }\n    });\n}\n\n/**\n * Look for elements that match the given selector and contain the given text.\n * Is case-insensitive and returns the first result or null if nothing is found.\n */\nexport function findText(selector: string, text: string): Element|null {\n    const elements = document.querySelectorAll(selector);\n    text = text.toLowerCase();\n    for (const element of elements) {\n        if ((element.textContent || '').toLowerCase().includes(text) && isHTMLElement(element)) {\n            return element;\n        }\n    }\n    return null;\n}\n\n/**\n * Show a loading indicator in the given element.\n * This will effectively clear the element.\n */\nexport function showLoading(element: HTMLElement): void {\n    element.innerHTML = '<div class=\"loading-container\"><div></div><div></div><div></div></div>';\n}\n\n/**\n * Get a loading element indicator element.\n */\nexport function getLoading(): HTMLElement {\n    const wrap = document.createElement('div');\n    wrap.classList.add('loading-container');\n    wrap.innerHTML = '<div></div><div></div><div></div>';\n    return wrap;\n}\n\n/**\n * Remove any loading indicators within the given element.\n */\nexport function removeLoading(element: HTMLElement): void {\n    const loadingEls = element.querySelectorAll('.loading-container');\n    for (const el of loadingEls) {\n        el.remove();\n    }\n}\n\n/**\n * Convert the given html data into a live DOM element.\n * Initiates any components defined in the data.\n */\nexport function htmlToDom(html: string): HTMLElement {\n    const wrap = document.createElement('div');\n    wrap.innerHTML = html;\n    window.$components.init(wrap);\n    const firstChild = wrap.children[0];\n    if (!isHTMLElement(firstChild)) {\n        throw new Error('Could not find child HTMLElement when creating DOM element from HTML');\n    }\n\n    return firstChild;\n}\n\n/**\n * For the given node and offset, return an adjusted offset that's relative to the given parent element.\n */\nexport function normalizeNodeTextOffsetToParent(node: Node, offset: number, parentElement: HTMLElement): number {\n    if (!parentElement.contains(node)) {\n        throw new Error('ParentElement must be a prent of element');\n    }\n\n    let normalizedOffset = offset;\n    let currentNode: Node|null = node.nodeType === Node.TEXT_NODE ?\n        node : node.childNodes[offset];\n\n    while (currentNode !== parentElement && currentNode) {\n        if (currentNode.previousSibling) {\n            currentNode = currentNode.previousSibling;\n            normalizedOffset += (currentNode.textContent?.length || 0);\n        } else {\n            currentNode = currentNode.parentNode;\n        }\n    }\n\n    return normalizedOffset;\n}\n\n/**\n * Find the target child node and adjusted offset based on a parent node and text offset.\n * Returns null if offset not found within the given parent node.\n */\nexport function findTargetNodeAndOffset(parentNode: HTMLElement, offset: number): ({node: Node, offset: number}|null) {\n    if (offset === 0) {\n        return { node: parentNode, offset: 0 };\n    }\n\n    let currentOffset = 0;\n    let currentNode = null;\n\n    for (let i = 0; i < parentNode.childNodes.length; i++) {\n        currentNode = parentNode.childNodes[i];\n\n        if (currentNode.nodeType === Node.TEXT_NODE) {\n            // For text nodes, count the length of their content\n            // Returns if within range\n            const textLength = (currentNode.textContent || '').length;\n            if (currentOffset + textLength >= offset) {\n                return {\n                    node: currentNode,\n                    offset: offset - currentOffset\n                };\n            }\n\n            currentOffset += textLength;\n        } else if (currentNode.nodeType === Node.ELEMENT_NODE) {\n            // Otherwise, if an element, track the text length and search within\n            // if in range for the target offset\n            const elementTextLength = (currentNode.textContent || '').length;\n            if (currentOffset + elementTextLength >= offset) {\n                return findTargetNodeAndOffset(currentNode as HTMLElement, offset - currentOffset);\n            }\n\n            currentOffset += elementTextLength;\n        }\n    }\n\n    // Return null if not found within range\n    return null;\n}\n\n/**\n * Create a hash for the given HTML element content.\n */\nexport function hashElement(element: HTMLElement): string {\n    const normalisedElemText = (element.textContent || '').replace(/\\s{2,}/g, '');\n    return cyrb53(normalisedElemText);\n}\n\n/**\n * Find the closest scroll container parent for the given element\n * otherwise will default to the body element.\n */\nexport function findClosestScrollContainer(start: HTMLElement): HTMLElement {\n    let el: HTMLElement|null = start;\n    do {\n        const computed = window.getComputedStyle(el);\n        if (computed.overflowY === 'scroll') {\n            return el;\n        }\n\n        el = el.parentElement;\n    } while (el);\n\n    return document.body;\n}"
  },
  {
    "path": "resources/js/services/drawio.ts",
    "content": "// Docs: https://www.diagrams.net/doc/faq/embed-mode\nimport * as store from './store';\nimport {ConfirmDialog} from \"../components\";\nimport {HttpError} from \"./http\";\n\ntype DrawioExportEventResponse = {\n    action: 'export',\n    format: string,\n    message: string,\n    data: string,\n    xml: string,\n};\n\ntype DrawioSaveEventResponse = {\n    action: 'save',\n    xml: string,\n};\n\nlet iFrame: HTMLIFrameElement|null = null;\nlet lastApprovedOrigin: string;\nlet onInit: () => Promise<string>;\nlet onSave: (data: string) => Promise<any>;\nconst saveBackupKey = 'last-drawing-save';\n\nfunction drawPostMessage(data: Record<any, any>): void {\n    iFrame?.contentWindow?.postMessage(JSON.stringify(data), lastApprovedOrigin);\n}\n\nfunction drawEventExport(message: DrawioExportEventResponse) {\n    store.set(saveBackupKey, message.data);\n    if (onSave) {\n        onSave(message.data).then(() => {\n            store.del(saveBackupKey);\n        });\n    }\n}\n\nfunction drawEventSave(message: DrawioSaveEventResponse) {\n    drawPostMessage({\n        action: 'export', format: 'xmlpng', xml: message.xml, spin: 'Updating drawing',\n    });\n}\n\nfunction drawEventInit() {\n    if (!onInit) return;\n    onInit().then(xml => {\n        drawPostMessage({action: 'load', autosave: 1, xml});\n    });\n}\n\nfunction drawEventConfigure() {\n    const config = {};\n    if (iFrame) {\n        window.$events.emitPublic(iFrame, 'editor-drawio::configure', {config});\n        drawPostMessage({action: 'configure', config});\n    }\n}\n\nfunction drawEventClose() {\n    // eslint-disable-next-line no-use-before-define\n    window.removeEventListener('message', drawReceive);\n    if (iFrame) document.body.removeChild(iFrame);\n}\n\n/**\n * Receive and handle a message event from the draw.io window.\n */\nfunction drawReceive(event: MessageEvent) {\n    if (!event.data || event.data.length < 1) return;\n    if (event.origin !== lastApprovedOrigin) return;\n\n    const message = JSON.parse(event.data);\n    if (message.event === 'init') {\n        drawEventInit();\n    } else if (message.event === 'exit') {\n        drawEventClose();\n    } else if (message.event === 'save') {\n        drawEventSave(message as DrawioSaveEventResponse);\n    } else if (message.event === 'export') {\n        drawEventExport(message as DrawioExportEventResponse);\n    } else if (message.event === 'configure') {\n        drawEventConfigure();\n    }\n}\n\n/**\n * Attempt to prompt and restore unsaved drawing content if existing.\n * @returns {Promise<void>}\n */\nasync function attemptRestoreIfExists() {\n    const backupVal = await store.get(saveBackupKey);\n    const dialogEl = document.getElementById('unsaved-drawing-dialog');\n\n    if (!dialogEl) {\n        console.error('Missing expected unsaved-drawing dialog');\n    }\n\n    if (backupVal && dialogEl) {\n        const dialog = window.$components.firstOnElement(dialogEl, 'confirm-dialog') as ConfirmDialog;\n        const restore = await dialog.show();\n        if (restore) {\n            onInit = async () => backupVal;\n        }\n    }\n}\n\n/**\n * Show the draw.io editor.\n * onSaveCallback must return a promise that resolves on successful save and errors on failure.\n * onInitCallback must return a promise with the xml to load for the editor.\n * Will attempt to provide an option to restore unsaved changes if found to exist.\n * onSaveCallback Is called with the drawing data on save.\n */\nexport async function show(drawioUrl: string, onInitCallback: () => Promise<string>, onSaveCallback: (data: string) => Promise<void>): Promise<void> {\n    onInit = onInitCallback;\n    onSave = onSaveCallback;\n\n    await attemptRestoreIfExists();\n\n    iFrame = document.createElement('iframe');\n    iFrame.setAttribute('frameborder', '0');\n    window.addEventListener('message', drawReceive);\n    iFrame.setAttribute('src', drawioUrl);\n    iFrame.setAttribute('class', 'fullscreen');\n    iFrame.style.backgroundColor = '#FFFFFF';\n    document.body.appendChild(iFrame);\n    lastApprovedOrigin = (new URL(drawioUrl)).origin;\n}\n\nexport async function upload(imageData: string, pageUploadedToId: string): Promise<{id: number, url: string}> {\n    const data = {\n        image: imageData,\n        uploaded_to: pageUploadedToId,\n    };\n    const resp = await window.$http.post(window.baseUrl('/images/drawio'), data);\n    return resp.data as {id: number, url: string};\n}\n\nexport function close() {\n    drawEventClose();\n}\n\n/**\n * Load an existing image, by fetching it as Base64 from the system.\n */\nexport async function load(drawingId: string): Promise<string> {\n    try {\n        const resp = await window.$http.get(window.baseUrl(`/images/drawio/base64/${drawingId}`));\n        const data = resp.data as {content: string};\n        return `data:image/png;base64,${data.content}`;\n    } catch (error) {\n        if (error instanceof HttpError) {\n            window.$events.showResponseError(error);\n        }\n        close();\n        throw error;\n    }\n}\n"
  },
  {
    "path": "resources/js/services/dual-lists.ts",
    "content": "/**\n * Service for helping manage common dual-list scenarios.\n * (Shelf book manager, sort set manager).\n */\n\ntype ListActionsSet = Record<string, ((item: HTMLElement) => void)>;\n\nexport function buildListActions(\n    availableList: HTMLElement,\n    configuredList: HTMLElement,\n): ListActionsSet {\n    return {\n        move_up(item) {\n            const list = item.parentNode as HTMLElement;\n            const index = Array.from(list.children).indexOf(item);\n            const newIndex = Math.max(index - 1, 0);\n            list.insertBefore(item, list.children[newIndex] || null);\n        },\n        move_down(item) {\n            const list = item.parentNode as HTMLElement;\n            const index = Array.from(list.children).indexOf(item);\n            const newIndex = Math.min(index + 2, list.children.length);\n            list.insertBefore(item, list.children[newIndex] || null);\n        },\n        remove(item) {\n            availableList.appendChild(item);\n        },\n        add(item) {\n            configuredList.appendChild(item);\n        },\n    };\n}\n\nexport function sortActionClickListener(actions: ListActionsSet, onChange: () => void) {\n    return (event: MouseEvent) => {\n        const sortItemAction = (event.target as Element).closest('.scroll-box-item button[data-action]') as HTMLElement|null;\n        if (sortItemAction) {\n            const sortItem = sortItemAction.closest('.scroll-box-item') as HTMLElement;\n            const action = sortItemAction.dataset.action;\n            if (!action) {\n                throw new Error('No action defined for clicked button');\n            }\n\n            const actionFunction = actions[action];\n            actionFunction(sortItem);\n\n            onChange();\n        }\n    };\n}\n\n"
  },
  {
    "path": "resources/js/services/events.ts",
    "content": "import {HttpError} from \"./http\";\n\ntype Listener = (data: any) => void;\n\nexport class EventManager {\n    protected listeners: Record<string, Listener[]> = {};\n    protected stack: {name: string, data: {}}[] = [];\n\n    /**\n     * Emit a custom event for any handlers to pick-up.\n     */\n    emit(eventName: string, eventData: {} = {}): void {\n        this.stack.push({name: eventName, data: eventData});\n\n        const listenersToRun = this.listeners[eventName] || [];\n        for (const listener of listenersToRun) {\n            listener(eventData);\n        }\n    }\n\n    /**\n     * Listen to a custom event and run the given callback when that event occurs.\n     */\n    listen<T>(eventName: string, callback: (data: T) => void): void {\n        if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = [];\n        this.listeners[eventName].push(callback);\n    }\n\n    /**\n     * Remove an event listener which is using the given callback for the given event name.\n     */\n    remove(eventName: string, callback: Listener): void {\n        const listeners = this.listeners[eventName] || [];\n        const index = listeners.indexOf(callback);\n        if (index !== -1) {\n            listeners.splice(index, 1);\n        }\n    }\n\n    /**\n     * Emit an event for public use.\n     * Sends the event via the native DOM event handling system.\n     */\n    emitPublic(targetElement: Element, eventName: string, eventData: {}): void {\n        const event = new CustomEvent(eventName, {\n            detail: eventData,\n            bubbles: true,\n        });\n        targetElement.dispatchEvent(event);\n    }\n\n    /**\n     * Emit a success event with the provided message.\n     */\n    success(message: string): void {\n        this.emit('success', message);\n    }\n\n    /**\n     * Emit an error event with the provided message.\n     */\n    error(message: string): void {\n        this.emit('error', message);\n    }\n\n    /**\n     * Notify of standard server-provided validation errors.\n     */\n    showValidationErrors(responseErr: HttpError): void {\n        if (responseErr.status === 422 && responseErr.data) {\n            const message = Object.values(responseErr.data).flat().join('\\n');\n            this.error(message);\n        }\n    }\n\n    /**\n     * Notify standard server-provided error messages.\n     */\n    showResponseError(responseErr: {status?: number, data?: Record<any, any>}|HttpError): void {\n        if (!responseErr.status) return;\n        if (responseErr.status >= 400 && typeof responseErr.data === 'object' && responseErr.data.message) {\n            this.error(responseErr.data.message);\n        }\n    }\n}\n"
  },
  {
    "path": "resources/js/services/http.ts",
    "content": "type ResponseData = Record<any, any>|string;\n\ntype RequestOptions = {\n    params?: Record<string, string>,\n    headers?: Record<string, string>\n};\n\ntype FormattedResponse = {\n    headers: Headers;\n    original: Response;\n    data: ResponseData;\n    redirected: boolean;\n    status: number;\n    statusText: string;\n    url: string;\n};\n\nexport class HttpError extends Error implements FormattedResponse {\n\n    data: ResponseData;\n    headers: Headers;\n    original: Response;\n    redirected: boolean;\n    status: number;\n    statusText: string;\n    url: string;\n\n    constructor(response: Response, content: ResponseData) {\n        super(response.statusText);\n        this.data = content;\n        this.headers = response.headers;\n        this.redirected = response.redirected;\n        this.status = response.status;\n        this.statusText = response.statusText;\n        this.url = response.url;\n        this.original = response;\n    }\n}\n\nexport class HttpManager {\n\n    /**\n     * Get the content from a fetch response.\n     * Checks the content-type header to determine the format.\n     */\n    protected async getResponseContent(response: Response): Promise<ResponseData|null> {\n        if (response.status === 204) {\n            return null;\n        }\n\n        const responseContentType = response.headers.get('Content-Type') || '';\n        const subType = responseContentType.split(';')[0].split('/').pop();\n\n        if (subType === 'javascript' || subType === 'json') {\n            return response.json();\n        }\n\n        return response.text();\n    }\n\n    createXMLHttpRequest(method: string, url: string, events: Record<string, (e: Event) => void> = {}): XMLHttpRequest {\n        const csrfToken = document.querySelector('meta[name=token]')?.getAttribute('content');\n        const req = new XMLHttpRequest();\n\n        for (const [eventName, callback] of Object.entries(events)) {\n            req.addEventListener(eventName, callback.bind(req));\n        }\n\n        req.open(method, url);\n        req.withCredentials = true;\n        req.setRequestHeader('X-CSRF-TOKEN', csrfToken || '');\n\n        return req;\n    }\n\n    /**\n     * Create a new HTTP request, setting the required CSRF information\n     * to communicate with the back-end. Parses & formats the response.\n     */\n    protected async request(url: string, options: RequestOptions & RequestInit = {}): Promise<FormattedResponse> {\n        let requestUrl = url;\n\n        if (!requestUrl.startsWith('http')) {\n            requestUrl = window.baseUrl(requestUrl);\n        }\n\n        if (options.params) {\n            const urlObj = new URL(requestUrl);\n            for (const paramName of Object.keys(options.params)) {\n                const value = options.params[paramName];\n                if (typeof value !== 'undefined' && value !== null) {\n                    urlObj.searchParams.set(paramName, value);\n                }\n            }\n            requestUrl = urlObj.toString();\n        }\n\n        const csrfToken = document.querySelector('meta[name=token]')?.getAttribute('content') || '';\n        const requestOptions: RequestInit = {...options, credentials: 'same-origin'};\n        requestOptions.headers = {\n            ...requestOptions.headers || {},\n            baseURL: window.baseUrl(''),\n            'X-CSRF-TOKEN': csrfToken,\n        };\n\n        const response = await fetch(requestUrl, requestOptions);\n        const content = await this.getResponseContent(response) || '';\n        const returnData: FormattedResponse = {\n            data: content,\n            headers: response.headers,\n            redirected: response.redirected,\n            status: response.status,\n            statusText: response.statusText,\n            url: response.url,\n            original: response,\n        };\n\n        if (!response.ok) {\n            throw new HttpError(response, content);\n        }\n\n        return returnData;\n    }\n\n    /**\n     * Perform a HTTP request to the back-end that includes data in the body.\n     * Parses the body to JSON if an object, setting the correct headers.\n     */\n    protected async dataRequest(method: string, url: string, data: Record<string, any>|null): Promise<FormattedResponse> {\n        const options: RequestInit & RequestOptions = {\n            method,\n            body: data as BodyInit,\n        };\n\n        // Send data as JSON if a plain object\n        if (typeof data === 'object' && !(data instanceof FormData)) {\n            options.headers = {\n                'Content-Type': 'application/json',\n                'X-Requested-With': 'XMLHttpRequest',\n            };\n            options.body = JSON.stringify(data);\n        }\n\n        // Ensure FormData instances are sent over POST\n        // Since Laravel does not read multipart/form-data from other types\n        // of request, hence the addition of the magic _method value.\n        if (data instanceof FormData && method !== 'post') {\n            data.append('_method', method);\n            options.method = 'post';\n        }\n\n        return this.request(url, options);\n    }\n\n    /**\n     * Perform a HTTP GET request.\n     * Can easily pass query parameters as the second parameter.\n     */\n    async get(url: string, params: {} = {}): Promise<FormattedResponse> {\n        return this.request(url, {\n            method: 'GET',\n            params,\n        });\n    }\n\n    /**\n     * Perform a HTTP POST request.\n     */\n    async post(url: string, data: null|Record<string, any> = null): Promise<FormattedResponse> {\n        return this.dataRequest('POST', url, data);\n    }\n\n    /**\n     * Perform a HTTP PUT request.\n     */\n    async put(url: string, data: null|Record<string, any> = null): Promise<FormattedResponse> {\n        return this.dataRequest('PUT', url, data);\n    }\n\n    /**\n     * Perform a HTTP PATCH request.\n     */\n    async patch(url: string, data: null|Record<string, any> = null): Promise<FormattedResponse> {\n        return this.dataRequest('PATCH', url, data);\n    }\n\n    /**\n     * Perform a HTTP DELETE request.\n     */\n    async delete(url: string, data: null|Record<string, any> = null): Promise<FormattedResponse> {\n        return this.dataRequest('DELETE', url, data);\n    }\n\n    /**\n     * Parse the response text for an error response to a user\n     * presentable string. Handles a range of errors responses including\n     * validation responses & server response text.\n     */\n    protected formatErrorResponseText(text: string): string {\n        const data = text.startsWith('{') ? JSON.parse(text) : {message: text};\n        if (!data) {\n            return text;\n        }\n\n        if (data.message || data.error) {\n            return data.message || data.error;\n        }\n\n        const values = Object.values(data);\n        const isValidation = values.every(val => {\n            return Array.isArray(val) && val.every(x => typeof x === 'string');\n        });\n\n        if (isValidation) {\n            return values.flat().join(' ');\n        }\n\n        return text;\n    }\n\n}\n"
  },
  {
    "path": "resources/js/services/keyboard-navigation.ts",
    "content": "import {isHTMLElement} from \"./dom\";\n\ntype OptionalKeyEventHandler = ((e: KeyboardEvent) => any)|null;\n\n/**\n * Handle common keyboard navigation events within a given container.\n */\nexport class KeyboardNavigationHandler {\n\n    protected containers: HTMLElement[];\n    protected onEscape: OptionalKeyEventHandler;\n    protected onEnter: OptionalKeyEventHandler;\n\n    constructor(container: HTMLElement, onEscape: OptionalKeyEventHandler = null, onEnter: OptionalKeyEventHandler = null) {\n        this.containers = [container];\n        this.onEscape = onEscape;\n        this.onEnter = onEnter;\n        container.addEventListener('keydown', this.#keydownHandler.bind(this));\n    }\n\n    /**\n     * Also share the keyboard event handling to the given element.\n     * Only elements within the original container are considered focusable though.\n     */\n    shareHandlingToEl(element: HTMLElement) {\n        this.containers.push(element);\n        element.addEventListener('keydown', this.#keydownHandler.bind(this));\n    }\n\n    /**\n     * Focus on the next focusable element within the current containers.\n     */\n    focusNext() {\n        const focusable = this.#getFocusable();\n        const activeEl = document.activeElement;\n        const currentIndex = isHTMLElement(activeEl) ? focusable.indexOf(activeEl) : -1;\n        let newIndex = currentIndex + 1;\n        if (newIndex >= focusable.length) {\n            newIndex = 0;\n        }\n\n        focusable[newIndex].focus();\n    }\n\n    /**\n     * Focus on the previous existing focusable element within the current containers.\n     */\n    focusPrevious() {\n        const focusable = this.#getFocusable();\n        const activeEl = document.activeElement;\n        const currentIndex = isHTMLElement(activeEl) ? focusable.indexOf(activeEl) : -1;\n        let newIndex = currentIndex - 1;\n        if (newIndex < 0) {\n            newIndex = focusable.length - 1;\n        }\n\n        focusable[newIndex].focus();\n    }\n\n    #keydownHandler(event: KeyboardEvent) {\n        // Ignore certain key events in inputs to allow text editing.\n        if (isHTMLElement(event.target) && event.target.matches('input') && (event.key === 'ArrowRight' || event.key === 'ArrowLeft')) {\n            return;\n        }\n\n        if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {\n            this.focusNext();\n            event.preventDefault();\n        } else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {\n            this.focusPrevious();\n            event.preventDefault();\n        } else if (event.key === 'Escape') {\n            if (this.onEscape) {\n                this.onEscape(event);\n            } else if (isHTMLElement(document.activeElement)) {\n                document.activeElement.blur();\n            }\n        } else if (event.key === 'Enter' && this.onEnter) {\n            this.onEnter(event);\n        }\n    }\n\n    /**\n     * Get an array of focusable elements within the current containers.\n     */\n    #getFocusable(): HTMLElement[] {\n        const focusable: HTMLElement[] = [];\n        const selector = '[tabindex]:not([tabindex=\"-1\"]),[href],button:not([tabindex=\"-1\"],[disabled]),input:not([type=hidden])';\n        for (const container of this.containers) {\n            const toAdd = [...container.querySelectorAll(selector)].filter(e => isHTMLElement(e));\n            focusable.push(...toAdd);\n        }\n\n        return focusable;\n    }\n\n}\n"
  },
  {
    "path": "resources/js/services/store.ts",
    "content": "export {get, set, del} from 'idb-keyval';\n"
  },
  {
    "path": "resources/js/services/text.ts",
    "content": "/**\n * Convert a kebab-case string to camelCase\n */\nexport function kebabToCamel(kebab: string): string {\n    const ucFirst = (word: string) => word.slice(0, 1).toUpperCase() + word.slice(1);\n    const words = kebab.split('-');\n    return words[0] + words.slice(1).map(ucFirst).join('');\n}\n\n/**\n * Convert a camelCase string to a kebab-case string.\n */\nexport function camelToKebab(camelStr: string): string {\n    return camelStr.replace(/[A-Z]/g, (str, offset) => (offset > 0 ? '-' : '') + str.toLowerCase());\n}\n"
  },
  {
    "path": "resources/js/services/translations.ts",
    "content": "/**\n *  Translation Manager\n *  Helps with some of the JavaScript side of translating strings\n *  in a way which fits with Laravel.\n */\nexport class Translator {\n\n    /**\n     * Parse the given translation and find the correct plural option\n     * to use. Similar format at Laravel's 'trans_choice' helper.\n     */\n    choice(translation: string, count: number, replacements: Record<string, string> = {}): string {\n        replacements = Object.assign({}, {count: String(count)}, replacements);\n        const splitText = translation.split('|');\n        const exactCountRegex = /^{([0-9]+)}/;\n        const rangeRegex = /^\\[([0-9]+),([0-9*]+)]/;\n        let result = null;\n\n        for (const t of splitText) {\n            // Parse exact matches\n            const exactMatches = t.match(exactCountRegex);\n            if (exactMatches !== null && Number(exactMatches[1]) === count) {\n                result = t.replace(exactCountRegex, '').trim();\n                break;\n            }\n\n            // Parse range matches\n            const rangeMatches = t.match(rangeRegex);\n            if (rangeMatches !== null) {\n                const rangeStart = Number(rangeMatches[1]);\n                if (rangeStart <= count && (rangeMatches[2] === '*' || Number(rangeMatches[2]) >= count)) {\n                    result = t.replace(rangeRegex, '').trim();\n                    break;\n                }\n            }\n        }\n\n        if (result === null && splitText.length > 1) {\n            result = (count === 1) ? splitText[0] : splitText[1];\n        }\n\n        if (result === null) {\n            result = splitText[0];\n        }\n\n        return this.performReplacements(result, replacements);\n    }\n\n    protected performReplacements(string: string, replacements: Record<string, string>): string {\n        const replaceMatches = string.match(/:(\\S+)/g);\n        if (replaceMatches === null) {\n            return string;\n        }\n\n        let updatedString = string;\n\n        for (const match of replaceMatches) {\n            const key = match.substring(1);\n            if (typeof replacements[key] === 'undefined') {\n                continue;\n            }\n            updatedString = updatedString.replace(match, replacements[key]);\n        }\n\n        return updatedString;\n    }\n\n}\n"
  },
  {
    "path": "resources/js/services/util.ts",
    "content": "/**\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing.\n * @attribution https://davidwalsh.name/javascript-debounce-function\n */\nexport function debounce<T extends (...args: any[]) => any>(func: T, waitMs: number, immediate: boolean): T {\n    let timeout: number|null = null;\n    return function debouncedWrapper(this: any, ...args: any[]) {\n        const context: any = this;\n        const later = function debouncedTimeout() {\n            timeout = null;\n            if (!immediate) func.apply(context, args);\n        };\n        const callNow = immediate && !timeout;\n        if (timeout) {\n            clearTimeout(timeout);\n        }\n        timeout = window.setTimeout(later, waitMs);\n        if (callNow) func.apply(context, args);\n    } as T;\n}\n\nfunction isDetailsElement(element: HTMLElement): element is HTMLDetailsElement {\n    return element.nodeName === 'DETAILS';\n}\n\n/**\n * Scroll-to and highlight an element.\n */\nexport function scrollAndHighlightElement(element: HTMLElement): void {\n    if (!element) return;\n\n    // Open up parent <details> elements if within\n    let parent = element;\n    while (parent.parentElement) {\n        parent = parent.parentElement;\n        if (isDetailsElement(parent) && !parent.open) {\n            parent.open = true;\n        }\n    }\n\n    element.scrollIntoView({behavior: 'smooth'});\n\n    const highlight = getComputedStyle(document.body).getPropertyValue('--color-link');\n    element.style.outline = `2px dashed ${highlight}`;\n    element.style.outlineOffset = '5px';\n    element.style.removeProperty('transition');\n    setTimeout(() => {\n        element.style.transition = 'outline linear 3s';\n        element.style.outline = '2px dashed rgba(0, 0, 0, 0)';\n        const listener = () => {\n            element.removeEventListener('transitionend', listener);\n            element.style.removeProperty('transition');\n            element.style.removeProperty('outline');\n            element.style.removeProperty('outlineOffset');\n        };\n        element.addEventListener('transitionend', listener);\n    }, 1000);\n}\n\n/**\n * Escape any HTML in the given 'unsafe' string.\n * Take from https://stackoverflow.com/a/6234804.\n */\nexport function escapeHtml(unsafe: string): string {\n    return unsafe\n        .replace(/&/g, '&amp;')\n        .replace(/</g, '&lt;')\n        .replace(/>/g, '&gt;')\n        .replace(/\"/g, '&quot;')\n        .replace(/'/g, '&#039;');\n}\n\n/**\n * Generate a random unique ID.\n */\nexport function uniqueId(): string {\n    // eslint-disable-next-line no-bitwise\n    const S4 = () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);\n    return (`${S4() + S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`);\n}\n\n/**\n * Generate a random smaller unique ID.\n */\nexport function uniqueIdSmall(): string {\n    // eslint-disable-next-line no-bitwise\n    const S4 = () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);\n    return S4();\n}\n\n/**\n * Create a promise that resolves after the given time.\n */\nexport function wait(timeMs: number): Promise<any> {\n    return new Promise(res => {\n        setTimeout(res, timeMs);\n    });\n}\n\n/**\n * Generate a full URL from the given relative URL, using a base\n * URL defined in the head of the page.\n */\nexport function baseUrl(path: string): string {\n    let targetPath = path;\n    const baseUrlMeta = document.querySelector('meta[name=\"base-url\"]');\n    if (!baseUrlMeta) {\n        throw new Error('Could not find expected base-url meta tag in document');\n    }\n\n    let basePath = baseUrlMeta.getAttribute('content') || '';\n    if (basePath[basePath.length - 1] === '/') {\n        basePath = basePath.slice(0, basePath.length - 1);\n    }\n\n    if (targetPath[0] === '/') {\n        targetPath = targetPath.slice(1);\n    }\n\n    return `${basePath}/${targetPath}`;\n}\n\n/**\n * Get the current version of BookStack in use.\n * Grabs this from the version query used on app assets.\n */\nfunction getVersion(): string {\n    const styleLink = document.querySelector('link[href*=\"/dist/styles.css?version=\"]');\n    if (!styleLink) {\n        throw new Error('Could not find expected style link in document for version use');\n    }\n\n    const href = (styleLink.getAttribute('href') || '');\n    return href.split('?version=').pop() || '';\n}\n\n/**\n * Perform a module import, Ensuring the import is fetched with the current\n * app version as a cache-breaker.\n */\nexport function importVersioned(moduleName: string): Promise<object> {\n    const importPath = window.baseUrl(`dist/${moduleName}.js?version=${getVersion()}`);\n    return import(importPath);\n}\n\n/*\n    cyrb53 (c) 2018 bryc (github.com/bryc)\n    License: Public domain (or MIT if needed). Attribution appreciated.\n    A fast and simple 53-bit string hash function with decent collision resistance.\n    Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.\n    Taken from: https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js\n*/\nexport function cyrb53(str: string, seed: number = 0): string {\n    let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;\n    for(let i = 0, ch; i < str.length; i++) {\n        ch = str.charCodeAt(i);\n        h1 = Math.imul(h1 ^ ch, 2654435761);\n        h2 = Math.imul(h2 ^ ch, 1597334677);\n    }\n    h1  = Math.imul(h1 ^ (h1 >>> 16), 2246822507);\n    h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);\n    h2  = Math.imul(h2 ^ (h2 >>> 16), 2246822507);\n    h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);\n    return String((4294967296 * (2097151 & h2) + (h1 >>> 0)));\n}"
  },
  {
    "path": "resources/js/services/vdom.ts",
    "content": "import {\n    init,\n    attributesModule,\n    toVNode,\n} from 'snabbdom';\nimport {VNode} from \"snabbdom/build/vnode\";\n\ntype vDomPatcher = (oldVnode: VNode | Element | DocumentFragment, vnode: VNode) => VNode;\n\nlet patcher: vDomPatcher;\n\nfunction getPatcher(): vDomPatcher {\n    if (patcher) return patcher;\n\n    patcher = init([\n        attributesModule,\n    ]);\n\n    return patcher;\n}\n\nexport function patchDomFromHtmlString(domTarget: Element, html: string): void {\n    const contentDom = document.createElement('div');\n    contentDom.innerHTML = html;\n    getPatcher()(toVNode(domTarget), toVNode(contentDom));\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/api/__tests__/api-test-utils.ts",
    "content": "import {createTestContext} from \"lexical/__tests__/utils\";\nimport {EditorApi} from \"../api\";\nimport {EditorUiContext} from \"../../ui/framework/core\";\nimport {LexicalEditor} from \"lexical\";\n\n\n/**\n * Create an instance of the EditorApi and EditorUiContext.\n */\nexport function createEditorApiInstance(): { api: EditorApi; context: EditorUiContext, editor: LexicalEditor} {\n    const context = createTestContext();\n    const api = new EditorApi(context);\n    return {api, context, editor: context.editor};\n}"
  },
  {
    "path": "resources/js/wysiwyg/api/__tests__/content.test.ts",
    "content": "import {createEditorApiInstance} from \"./api-test-utils\";\nimport {$createParagraphNode, $createTextNode, $getRoot, IS_BOLD, LexicalEditor} from \"lexical\";\nimport {expectNodeShapeToMatch} from \"lexical/__tests__/utils\";\n\n\ndescribe('Editor API: Content Module', () => {\n\n    describe('insertHtml()', () => {\n        it('should insert html at selection by default', () => {\n            const {api, editor} = createEditorApiInstance();\n            insertAndSelectSampleBlock(editor);\n\n            api.content.insertHtml('<strong>pp</strong>');\n            editor.commitUpdates();\n\n            expectNodeShapeToMatch(editor, [\n                {type: 'paragraph', children: [\n                    {text: 'He'},\n                    {text: 'pp', format: IS_BOLD},\n                    {text: 'o World'}\n                ]}\n            ]);\n        });\n\n        it('should handle a mix of inline and block elements', () => {\n            const {api, editor} = createEditorApiInstance();\n            insertAndSelectSampleBlock(editor);\n\n            api.content.insertHtml('<p>cat</p><strong>pp</strong><p>dog</p>');\n            editor.commitUpdates();\n\n            expectNodeShapeToMatch(editor, [\n                {type: 'paragraph', children: [{text: 'cat'}]},\n                {type: 'paragraph', children: [\n                        {text: 'He'},\n                        {text: 'pp', format: IS_BOLD},\n                        {text: 'o World'}\n                    ]},\n                {type: 'paragraph', children: [{text: 'dog'}]},\n            ]);\n        });\n\n        it('should throw and error if an invalid position is provided', () => {\n            const {api, editor} = createEditorApiInstance();\n            insertAndSelectSampleBlock(editor);\n\n\n            expect(() => {\n                api.content.insertHtml('happy<p>cat</p>', 'near-the-end');\n            }).toThrow('Invalid position: near-the-end. Valid positions are: start, end, selection');\n        });\n\n        it('should append html if end provided as a position', () => {\n            const {api, editor} = createEditorApiInstance();\n            insertAndSelectSampleBlock(editor);\n\n            api.content.insertHtml('happy<p>cat</p>', 'end');\n            editor.commitUpdates();\n\n            expectNodeShapeToMatch(editor, [\n                {type: 'paragraph', children: [{text: 'Hello World'}]},\n                {type: 'paragraph', children: [{text: 'happy'}]},\n                {type: 'paragraph', children: [{text: 'cat'}]},\n            ]);\n        });\n\n        it('should prepend html if start provided as a position', () => {\n            const {api, editor} = createEditorApiInstance();\n            insertAndSelectSampleBlock(editor);\n\n            api.content.insertHtml('happy<p>cat</p>', 'start');\n            editor.commitUpdates();\n\n            expectNodeShapeToMatch(editor, [\n                {type: 'paragraph', children: [{text: 'happy'}]},\n                {type: 'paragraph', children: [{text: 'cat'}]},\n                {type: 'paragraph', children: [{text: 'Hello World'}]},\n            ]);\n        });\n    });\n\n    function insertAndSelectSampleBlock(editor: LexicalEditor) {\n        editor.updateAndCommit(() => {\n            const p = $createParagraphNode();\n            const text = $createTextNode('Hello World');\n            p.append(text);\n            $getRoot().append(p);\n\n            text.select(2, 4);\n        });\n    }\n\n});"
  },
  {
    "path": "resources/js/wysiwyg/api/__tests__/ui.test.ts",
    "content": "import {createEditorApiInstance} from \"./api-test-utils\";\nimport {EditorApiButton, EditorApiToolbar, EditorApiToolbarSection} from \"../ui\";\nimport {getMainEditorFullToolbar} from \"../../ui/defaults/toolbars\";\nimport {EditorContainerUiElement} from \"../../ui/framework/core\";\nimport {EditorOverflowContainer} from \"../../ui/framework/blocks/overflow-container\";\n\n\ndescribe('Editor API: UI Module', () => {\n\n    describe('createButton()', () => {\n        it('should return a button', () => {\n            const {api} = createEditorApiInstance();\n            const button = api.ui.createButton({label: 'Test', icon: 'test', action: () => ''});\n            expect(button).toBeInstanceOf(EditorApiButton);\n        });\n\n        it('should only need action to be required', () => {\n            const {api} = createEditorApiInstance();\n            const button = api.ui.createButton({action: () => ''});\n            expect(button).toBeInstanceOf(EditorApiButton);\n        });\n\n        it('should pass the label and icon to the button', () => {\n            const {api} = createEditorApiInstance();\n            const button = api.ui.createButton({label: 'TestLabel', icon: '<svg>cat</svg>', action: () => ''});\n            const html = button._getOriginalModel().getDOMElement().outerHTML;\n            expect(html).toContain('TestLabel');\n            expect(html).toContain('<svg>cat</svg>');\n        })\n    });\n\n    describe('EditorApiButton', () => {\n\n        describe('setActive()', () => {\n            it('should update the active state of the button', () => {\n                const {api} = createEditorApiInstance();\n                const button = api.ui.createButton({label: 'Test', icon: 'test', action: () => ''});\n\n                button.setActive(true);\n                expect(button._getOriginalModel().isActive()).toBe(true);\n\n                button.setActive(false);\n                expect(button._getOriginalModel().isActive()).toBe(false);\n            })\n        });\n\n        it('should call the provided action on click', () => {\n            const {api} = createEditorApiInstance();\n            let count = 0;\n            const button = api.ui.createButton({label: 'Test', icon: 'test', action: () => {\n                count++;\n            }});\n\n            const dom = button._getOriginalModel().getDOMElement();\n            dom.click();\n            dom.click();\n            expect(count).toBe(2);\n        });\n\n    });\n\n    describe('getMainToolbar()', () => {\n        it('should return the main editor toolbar', () => {\n            const {api, context} = createEditorApiInstance();\n            context.manager.setToolbar(getMainEditorFullToolbar(context));\n\n            const toolbar = api.ui.getMainToolbar();\n\n            expect(toolbar).toBeInstanceOf(EditorApiToolbar);\n        });\n    });\n\n    describe('EditorApiToolbar', () => {\n        describe('getSections()', () => {\n            it('should return the sections of the toolbar', () => {\n                const {api, context} = createEditorApiInstance();\n                context.manager.setToolbar(testToolbar());\n                const toolbar = api.ui.getMainToolbar();\n\n                const sections = toolbar?.getSections() || [];\n\n                expect(sections.length).toBe(2);\n                expect(sections[0]).toBeInstanceOf(EditorApiToolbarSection);\n            })\n        })\n    })\n\n    describe('EditorApiToolbarSection', () => {\n\n        describe('getLabel()', () => {\n            it('should return the label of the section', () => {\n                const {api, context} = createEditorApiInstance();\n                context.manager.setToolbar(testToolbar());\n                const section = api.ui.getMainToolbar()?.getSections()[0] as EditorApiToolbarSection;\n                expect(section.getLabel()).toBe('section-a');\n             })\n        });\n\n        describe('addButton()', () => {\n            it('should add a button to the section', () => {\n                const {api, context} = createEditorApiInstance();\n                const toolbar = testToolbar();\n                context.manager.setToolbar(toolbar);\n                const section = api.ui.getMainToolbar()?.getSections()[0] as EditorApiToolbarSection;\n\n                const button = api.ui.createButton({label: 'TestButtonText!', action: () => ''});\n                section.addButton(button);\n\n                const toolbarRendered = toolbar.getDOMElement().innerHTML;\n                expect(toolbarRendered).toContain('TestButtonText!');\n            });\n        });\n\n    });\n\n    function testToolbar(): EditorContainerUiElement {\n        return new EditorContainerUiElement([\n            new EditorOverflowContainer('section-a', 1, []),\n            new EditorOverflowContainer('section-b', 1, []),\n        ]);\n    }\n\n});"
  },
  {
    "path": "resources/js/wysiwyg/api/api.ts",
    "content": "import {EditorApiUiModule} from \"./ui\";\nimport {EditorUiContext} from \"../ui/framework/core\";\nimport {EditorApiContentModule} from \"./content\";\n\nexport class EditorApi {\n\n    public ui: EditorApiUiModule;\n    public content: EditorApiContentModule;\n\n    constructor(context: EditorUiContext) {\n        this.ui = new EditorApiUiModule(context);\n        this.content = new EditorApiContentModule(context);\n    }\n}"
  },
  {
    "path": "resources/js/wysiwyg/api/content.ts",
    "content": "import {EditorUiContext} from \"../ui/framework/core\";\nimport {appendHtmlToEditor, insertHtmlIntoEditor, prependHtmlToEditor} from \"../utils/actions\";\n\n\nexport class EditorApiContentModule {\n    readonly #context: EditorUiContext;\n\n    constructor(context: EditorUiContext) {\n        this.#context = context;\n    }\n\n    insertHtml(html: string, position: string = 'selection'): void {\n        const validPositions = ['start', 'end', 'selection'];\n        if (!validPositions.includes(position)) {\n            throw new Error(`Invalid position: ${position}. Valid positions are: ${validPositions.join(', ')}`);\n        }\n\n        if (position === 'start') {\n            prependHtmlToEditor(this.#context.editor, html);\n        } else if (position === 'end') {\n            appendHtmlToEditor(this.#context.editor, html);\n        } else {\n            insertHtmlIntoEditor(this.#context.editor, html);\n        }\n    }\n}"
  },
  {
    "path": "resources/js/wysiwyg/api/ui.ts",
    "content": "import {EditorButton} from \"../ui/framework/buttons\";\nimport {EditorContainerUiElement, EditorUiContext} from \"../ui/framework/core\";\nimport {EditorOverflowContainer} from \"../ui/framework/blocks/overflow-container\";\n\ntype EditorApiButtonOptions = {\n    label?: string;\n    icon?: string;\n    action: () => void;\n};\n\nexport class EditorApiButton {\n    readonly #button: EditorButton;\n    #isActive: boolean = false;\n\n    constructor(options: EditorApiButtonOptions, context: EditorUiContext) {\n        this.#button = new EditorButton({\n            label: options.label || '',\n            icon: options.icon || '',\n            action: () => {\n                options.action();\n            },\n            isActive: () => this.#isActive,\n        });\n        this.#button.setContext(context);\n    }\n\n    setActive(active: boolean = true): void {\n        this.#isActive = active;\n        this.#button.setActiveState(active);\n    }\n\n    _getOriginalModel() {\n        return this.#button;\n    }\n}\n\nexport class EditorApiToolbar {\n    readonly #toolbar: EditorContainerUiElement;\n\n    constructor(toolbar: EditorContainerUiElement) {\n        this.#toolbar = toolbar;\n    }\n\n    getSections(): EditorApiToolbarSection[] {\n        const sections = this.#toolbar.getChildren();\n        return sections.filter(section => {\n            return section instanceof EditorOverflowContainer;\n        }).map(section => new EditorApiToolbarSection(section));\n    }\n}\n\nexport class EditorApiToolbarSection {\n    readonly #section: EditorOverflowContainer;\n\n    constructor(section: EditorOverflowContainer) {\n        this.#section = section;\n    }\n\n    getLabel(): string {\n        return this.#section.getLabel();\n    }\n\n    addButton(button: EditorApiButton, targetIndex: number = -1): void {\n        this.#section.addChild(button._getOriginalModel(), targetIndex);\n        this.#section.rebuildDOM();\n    }\n}\n\n\nexport class EditorApiUiModule {\n    readonly #context: EditorUiContext;\n    \n    constructor(context: EditorUiContext) {\n        this.#context = context;\n    }\n    \n    createButton(options: EditorApiButtonOptions): EditorApiButton {\n        return new EditorApiButton(options, this.#context);\n    }\n\n    getMainToolbar(): EditorApiToolbar|null {\n        const toolbar = this.#context.manager.getToolbar();\n        if (!toolbar) {\n            return null;\n        }\n\n        return new EditorApiToolbar(toolbar);\n    }\n}"
  },
  {
    "path": "resources/js/wysiwyg/index.ts",
    "content": "import {createEditor} from 'lexical';\nimport {createEmptyHistoryState, registerHistory} from '@lexical/history';\nimport {registerRichText} from '@lexical/rich-text';\nimport {mergeRegister} from '@lexical/utils';\nimport {\n    getNodesForBasicEditor,\n    getNodesForCommentEditor,\n    getNodesForPageEditor,\n    registerCommonNodeMutationListeners\n} from './nodes';\nimport {buildEditorUI} from \"./ui\";\nimport {focusEditor, getEditorContentAsHtml, setEditorContentFromHtml} from \"./utils/actions\";\nimport {registerTableResizer} from \"./ui/framework/helpers/table-resizer\";\nimport {EditorUiContext} from \"./ui/framework/core\";\nimport {listen as listenToCommonEvents} from \"./services/common-events\";\nimport {registerDropPasteHandling} from \"./services/drop-paste-handling\";\nimport {registerTaskListHandler} from \"./ui/framework/helpers/task-list-handler\";\nimport {registerTableSelectionHandler} from \"./ui/framework/helpers/table-selection-handler\";\nimport {registerShortcuts} from \"./services/shortcuts\";\nimport {registerNodeResizer} from \"./ui/framework/helpers/node-resizer\";\nimport {registerKeyboardHandling} from \"./services/keyboard-handling\";\nimport {registerAutoLinks} from \"./services/auto-links\";\nimport {contextToolbars, getBasicEditorToolbar, getMainEditorFullToolbar} from \"./ui/defaults/toolbars\";\nimport {modals} from \"./ui/defaults/modals\";\nimport {CodeBlockDecorator} from \"./ui/decorators/CodeBlockDecorator\";\nimport {DiagramDecorator} from \"./ui/decorators/DiagramDecorator\";\nimport {registerMouseHandling} from \"./services/mouse-handling\";\nimport {registerSelectionHandling} from \"./services/selection-handling\";\nimport {EditorApi} from \"./api/api\";\nimport {registerMentions} from \"./services/mentions\";\nimport {MentionDecorator} from \"./ui/decorators/MentionDecorator\";\n\nconst theme = {\n    text: {\n        bold: 'editor-theme-bold',\n        code: 'editor-theme-code',\n        italic: 'editor-theme-italic',\n        strikethrough: 'editor-theme-strikethrough',\n        subscript: 'editor-theme-subscript',\n        superscript: 'editor-theme-superscript',\n        underline: 'editor-theme-underline',\n        underlineStrikethrough: 'editor-theme-underline-strikethrough',\n    }\n};\n\nexport function createPageEditorInstance(container: HTMLElement, htmlContent: string, options: Record<string, any> = {}): SimpleWysiwygEditorInterface {\n    const editor = createEditor({\n        namespace: 'BookStackPageEditor',\n        nodes: getNodesForPageEditor(),\n        onError: console.error,\n        theme: theme,\n    });\n    const context: EditorUiContext = buildEditorUI(container, editor, {\n        ...options,\n        editorClass: 'page-content',\n    });\n    editor.setRootElement(context.editorDOM);\n\n    mergeRegister(\n        registerRichText(editor),\n        registerHistory(editor, createEmptyHistoryState(), 300),\n        registerShortcuts(context),\n        registerKeyboardHandling(context),\n        registerMouseHandling(context),\n        registerSelectionHandling(context),\n        registerTableResizer(editor, context.scrollDOM),\n        registerTableSelectionHandler(editor),\n        registerTaskListHandler(editor, context.editorDOM),\n        registerDropPasteHandling(context),\n        registerNodeResizer(context),\n        registerAutoLinks(editor),\n    );\n\n    // Register toolbars, modals & decorators\n    context.manager.setToolbar(getMainEditorFullToolbar(context));\n    for (const key of Object.keys(contextToolbars)) {\n        context.manager.registerContextToolbar(key, contextToolbars[key]);\n    }\n    for (const key of Object.keys(modals)) {\n        context.manager.registerModal(key, modals[key]);\n    }\n    context.manager.registerDecoratorType('code', CodeBlockDecorator);\n    context.manager.registerDecoratorType('diagram', DiagramDecorator);\n\n    listenToCommonEvents(editor);\n    setEditorContentFromHtml(editor, htmlContent);\n\n    const debugView = document.getElementById('lexical-debug');\n    if (debugView) {\n        debugView.hidden = true;\n        editor.registerUpdateListener(({dirtyElements, dirtyLeaves, editorState, prevEditorState}) => {\n            // Debug logic\n            // console.log('editorState', editorState.toJSON());\n            debugView.textContent = JSON.stringify(editorState.toJSON(), null, 2);\n        });\n    }\n\n    // @ts-ignore\n    window.debugEditorState = () => {\n        return editor.getEditorState().toJSON();\n    };\n\n    registerCommonNodeMutationListeners(context);\n\n    window.$events.emitPublic(container, 'editor-wysiwyg::post-init', {\n        usage: 'page-editor',\n        api: new EditorApi(context),\n    });\n\n    return new SimpleWysiwygEditorInterface(context);\n}\n\nexport function createBasicEditorInstance(container: HTMLElement, htmlContent: string, options: Record<string, any> = {}): SimpleWysiwygEditorInterface {\n    const editor = createEditor({\n        namespace: 'BookStackBasicEditor',\n        nodes: getNodesForBasicEditor(),\n        onError: console.error,\n        theme: theme,\n    });\n    const context: EditorUiContext = buildEditorUI(container, editor, options);\n    editor.setRootElement(context.editorDOM);\n\n    const editorTeardown = mergeRegister(\n        registerRichText(editor),\n        registerHistory(editor, createEmptyHistoryState(), 300),\n        registerShortcuts(context),\n        registerAutoLinks(editor),\n    );\n\n    // Register toolbars, modals & decorators\n    context.manager.setToolbar(getBasicEditorToolbar(context));\n    context.manager.registerContextToolbar('link', contextToolbars.link);\n    context.manager.registerModal('link', modals.link);\n    context.manager.onTeardown(editorTeardown);\n\n    setEditorContentFromHtml(editor, htmlContent);\n\n    window.$events.emitPublic(container, 'editor-wysiwyg::post-init', {\n        usage: 'description-editor',\n        api: new EditorApi(context),\n    });\n\n    return new SimpleWysiwygEditorInterface(context);\n}\n\nexport function createCommentEditorInstance(container: HTMLElement, htmlContent: string, options: Record<string, any> = {}): SimpleWysiwygEditorInterface {\n    const editor = createEditor({\n        namespace: 'BookStackCommentEditor',\n        nodes: getNodesForCommentEditor(),\n        onError: console.error,\n        theme: theme,\n    });\n\n    const context: EditorUiContext = buildEditorUI(container, editor, options);\n    editor.setRootElement(context.editorDOM);\n\n    const editorTeardown = mergeRegister(\n        registerRichText(editor),\n        registerHistory(editor, createEmptyHistoryState(), 300),\n        registerShortcuts(context),\n        registerAutoLinks(editor),\n        registerMentions(context),\n    );\n\n    // Register toolbars, modals & decorators\n    context.manager.setToolbar(getBasicEditorToolbar(context));\n    context.manager.registerContextToolbar('link', contextToolbars.link);\n    context.manager.registerModal('link', modals.link);\n    context.manager.onTeardown(editorTeardown);\n    context.manager.registerDecoratorType('mention', MentionDecorator);\n\n    setEditorContentFromHtml(editor, htmlContent);\n\n    window.$events.emitPublic(container, 'editor-wysiwyg::post-init', {\n        usage: 'comment-editor',\n        api: new EditorApi(context),\n    });\n\n    return new SimpleWysiwygEditorInterface(context);\n}\n\nexport class SimpleWysiwygEditorInterface {\n    protected context: EditorUiContext;\n    protected onChangeListeners: (() => void)[] = [];\n    protected editorListenerTeardown: (() => void)|null = null;\n\n    constructor(context: EditorUiContext) {\n        this.context = context;\n    }\n\n    async getContentAsHtml(): Promise<string> {\n        return await getEditorContentAsHtml(this.context.editor);\n    }\n\n    onChange(listener: () => void) {\n        this.onChangeListeners.push(listener);\n        this.startListeningToChanges();\n    }\n\n    focus(): void {\n        focusEditor(this.context.editor);\n    }\n\n    remove() {\n        this.context.manager.teardown();\n        this.context.containerDOM.remove();\n        if (this.editorListenerTeardown) {\n            this.editorListenerTeardown();\n        }\n    }\n\n    protected startListeningToChanges(): void {\n        if (this.editorListenerTeardown) {\n            return;\n        }\n\n        this.editorListenerTeardown = this.context.editor.registerUpdateListener(() => {\n             for (const listener of this.onChangeListeners) {\n                 listener();\n             }\n        });\n    }\n}"
  },
  {
    "path": "resources/js/wysiwyg/lexical/ORIGINAL-LEXICAL-LICENSE",
    "content": "MIT License\n\nCopyright (c) Meta Platforms, Inc. and affiliates.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/clipboard/clipboard.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html';\nimport {$addNodeStyle, $sliceSelectedTextNodeContent} from '@lexical/selection';\nimport {objectKlassEquals} from '@lexical/utils';\nimport {\n  $cloneWithProperties,\n  $createTabNode,\n  $getEditor,\n  $getRoot,\n  $getSelection,\n  $isElementNode,\n  $isRangeSelection,\n  $isTextNode,\n  $parseSerializedNode,\n  BaseSelection,\n  COMMAND_PRIORITY_CRITICAL,\n  COPY_COMMAND,\n  isSelectionWithinEditor,\n  LexicalEditor,\n  LexicalNode,\n  SELECTION_INSERT_CLIPBOARD_NODES_COMMAND,\n  SerializedElementNode,\n  SerializedTextNode,\n} from 'lexical';\nimport {CAN_USE_DOM} from 'lexical/shared/canUseDOM';\nimport invariant from 'lexical/shared/invariant';\n\nconst getDOMSelection = (targetWindow: Window | null): Selection | null =>\n  CAN_USE_DOM ? (targetWindow || window).getSelection() : null;\n\nexport interface LexicalClipboardData {\n  'text/html'?: string | undefined;\n  'application/x-lexical-editor'?: string | undefined;\n  'text/plain': string;\n}\n\n/**\n * Returns the *currently selected* Lexical content as an HTML string, relying on the\n * logic defined in the exportDOM methods on the LexicalNode classes. Note that\n * this will not return the HTML content of the entire editor (unless all the content is included\n * in the current selection).\n *\n * @param editor - LexicalEditor instance to get HTML content from\n * @param selection - The selection to use (default is $getSelection())\n * @returns a string of HTML content\n */\nexport function $getHtmlContent(\n  editor: LexicalEditor,\n  selection = $getSelection(),\n): string {\n  if (selection == null) {\n    invariant(false, 'Expected valid LexicalSelection');\n  }\n\n  // If we haven't selected anything\n  if (\n    ($isRangeSelection(selection) && selection.isCollapsed()) ||\n    selection.getNodes().length === 0\n  ) {\n    return '';\n  }\n\n  return $generateHtmlFromNodes(editor, selection);\n}\n\n/**\n * Returns the *currently selected* Lexical content as a JSON string, relying on the\n * logic defined in the exportJSON methods on the LexicalNode classes. Note that\n * this will not return the JSON content of the entire editor (unless all the content is included\n * in the current selection).\n *\n * @param editor  - LexicalEditor instance to get the JSON content from\n * @param selection - The selection to use (default is $getSelection())\n * @returns\n */\nexport function $getLexicalContent(\n  editor: LexicalEditor,\n  selection = $getSelection(),\n): null | string {\n  if (selection == null) {\n    invariant(false, 'Expected valid LexicalSelection');\n  }\n\n  // If we haven't selected anything\n  if (\n    ($isRangeSelection(selection) && selection.isCollapsed()) ||\n    selection.getNodes().length === 0\n  ) {\n    return null;\n  }\n\n  return JSON.stringify($generateJSONFromSelectedNodes(editor, selection));\n}\n\n/**\n * Attempts to insert content of the mime-types text/plain or text/uri-list from\n * the provided DataTransfer object into the editor at the provided selection.\n * text/uri-list is only used if text/plain is not also provided.\n *\n * @param dataTransfer an object conforming to the [DataTransfer interface] (https://html.spec.whatwg.org/multipage/dnd.html#the-datatransfer-interface)\n * @param selection the selection to use as the insertion point for the content in the DataTransfer object\n */\nexport function $insertDataTransferForPlainText(\n  dataTransfer: DataTransfer,\n  selection: BaseSelection,\n): void {\n  const text =\n    dataTransfer.getData('text/plain') || dataTransfer.getData('text/uri-list');\n\n  if (text != null) {\n    selection.insertRawText(text);\n  }\n}\n\n/**\n * Attempts to insert content of the mime-types application/x-lexical-editor, text/html,\n * text/plain, or text/uri-list (in descending order of priority) from the provided DataTransfer\n * object into the editor at the provided selection.\n *\n * @param dataTransfer an object conforming to the [DataTransfer interface] (https://html.spec.whatwg.org/multipage/dnd.html#the-datatransfer-interface)\n * @param selection the selection to use as the insertion point for the content in the DataTransfer object\n * @param editor the LexicalEditor the content is being inserted into.\n */\nexport function $insertDataTransferForRichText(\n  dataTransfer: DataTransfer,\n  selection: BaseSelection,\n  editor: LexicalEditor,\n): void {\n  const lexicalString = dataTransfer.getData('application/x-lexical-editor');\n\n  if (lexicalString) {\n    try {\n      const payload = JSON.parse(lexicalString);\n      if (\n        payload.namespace === editor._config.namespace &&\n        Array.isArray(payload.nodes)\n      ) {\n        const nodes = $generateNodesFromSerializedNodes(payload.nodes);\n        return $insertGeneratedNodes(editor, nodes, selection);\n      }\n    } catch {\n      // Fail silently.\n    }\n  }\n\n  const htmlString = dataTransfer.getData('text/html');\n  if (htmlString) {\n    try {\n      const parser = new DOMParser();\n      const dom = parser.parseFromString(htmlString, 'text/html');\n      const nodes = $generateNodesFromDOM(editor, dom);\n      return $insertGeneratedNodes(editor, nodes, selection);\n    } catch {\n      // Fail silently.\n    }\n  }\n\n  // Multi-line plain text in rich text mode pasted as separate paragraphs\n  // instead of single paragraph with linebreaks.\n  // Webkit-specific: Supports read 'text/uri-list' in clipboard.\n  const text =\n    dataTransfer.getData('text/plain') || dataTransfer.getData('text/uri-list');\n  if (text != null) {\n    if ($isRangeSelection(selection)) {\n      const parts = text.split(/(\\r?\\n|\\t)/);\n      if (parts[parts.length - 1] === '') {\n        parts.pop();\n      }\n      for (let i = 0; i < parts.length; i++) {\n        const currentSelection = $getSelection();\n        if ($isRangeSelection(currentSelection)) {\n          const part = parts[i];\n          if (part === '\\n' || part === '\\r\\n') {\n            currentSelection.insertParagraph();\n          } else if (part === '\\t') {\n            currentSelection.insertNodes([$createTabNode()]);\n          } else {\n            currentSelection.insertText(part);\n          }\n        }\n      }\n    } else {\n      selection.insertRawText(text);\n    }\n  }\n}\n\n/**\n * Inserts Lexical nodes into the editor using different strategies depending on\n * some simple selection-based heuristics. If you're looking for a generic way to\n * to insert nodes into the editor at a specific selection point, you probably want\n * {@link lexical.$insertNodes}\n *\n * @param editor LexicalEditor instance to insert the nodes into.\n * @param nodes The nodes to insert.\n * @param selection The selection to insert the nodes into.\n */\nexport function $insertGeneratedNodes(\n  editor: LexicalEditor,\n  nodes: Array<LexicalNode>,\n  selection: BaseSelection,\n): void {\n  if (\n    !editor.dispatchCommand(SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, {\n      nodes,\n      selection,\n    })\n  ) {\n    selection.insertNodes(nodes);\n  }\n  return;\n}\n\nexport interface BaseSerializedNode {\n  children?: Array<BaseSerializedNode>;\n  type: string;\n  version: number;\n}\n\nfunction exportNodeToJSON<T extends LexicalNode>(node: T): BaseSerializedNode {\n  const serializedNode = node.exportJSON();\n  const nodeClass = node.constructor;\n\n  if (serializedNode.type !== nodeClass.getType()) {\n    invariant(\n      false,\n      'LexicalNode: Node %s does not implement .exportJSON().',\n      nodeClass.name,\n    );\n  }\n\n  if ($isElementNode(node)) {\n    const serializedChildren = (serializedNode as SerializedElementNode)\n      .children;\n    if (!Array.isArray(serializedChildren)) {\n      invariant(\n        false,\n        'LexicalNode: Node %s is an element but .exportJSON() does not have a children array.',\n        nodeClass.name,\n      );\n    }\n  }\n\n  return serializedNode;\n}\n\nfunction $appendNodesToJSON(\n  editor: LexicalEditor,\n  selection: BaseSelection | null,\n  currentNode: LexicalNode,\n  targetArray: Array<BaseSerializedNode> = [],\n): boolean {\n  let shouldInclude =\n    selection !== null ? currentNode.isSelected(selection) : true;\n  const shouldExclude =\n    $isElementNode(currentNode) && currentNode.excludeFromCopy('html');\n  let target = currentNode;\n\n  if (selection !== null) {\n    let clone = $cloneWithProperties(currentNode);\n    clone =\n      $isTextNode(clone) && selection !== null\n        ? $sliceSelectedTextNodeContent(selection, clone)\n        : clone;\n    target = clone;\n  }\n  const children = $isElementNode(target) ? target.getChildren() : [];\n\n  const serializedNode = exportNodeToJSON(target);\n\n  // TODO: TextNode calls getTextContent() (NOT node.__text) within its exportJSON method\n  // which uses getLatest() to get the text from the original node with the same key.\n  // This is a deeper issue with the word \"clone\" here, it's still a reference to the\n  // same node as far as the LexicalEditor is concerned since it shares a key.\n  // We need a way to create a clone of a Node in memory with its own key, but\n  // until then this hack will work for the selected text extract use case.\n  if ($isTextNode(target)) {\n    const text = target.__text;\n    // If an uncollapsed selection ends or starts at the end of a line of specialized,\n    // TextNodes, such as code tokens, we will get a 'blank' TextNode here, i.e., one\n    // with text of length 0. We don't want this, it makes a confusing mess. Reset!\n    if (text.length > 0) {\n      (serializedNode as SerializedTextNode).text = text;\n    } else {\n      shouldInclude = false;\n    }\n  }\n\n  for (let i = 0; i < children.length; i++) {\n    const childNode = children[i];\n    const shouldIncludeChild = $appendNodesToJSON(\n      editor,\n      selection,\n      childNode,\n      serializedNode.children,\n    );\n\n    if (\n      !shouldInclude &&\n      $isElementNode(currentNode) &&\n      shouldIncludeChild &&\n      currentNode.extractWithChild(childNode, selection, 'clone')\n    ) {\n      shouldInclude = true;\n    }\n  }\n\n  if (shouldInclude && !shouldExclude) {\n    targetArray.push(serializedNode);\n  } else if (Array.isArray(serializedNode.children)) {\n    for (let i = 0; i < serializedNode.children.length; i++) {\n      const serializedChildNode = serializedNode.children[i];\n      targetArray.push(serializedChildNode);\n    }\n  }\n\n  return shouldInclude;\n}\n\n// TODO why $ function with Editor instance?\n/**\n * Gets the Lexical JSON of the nodes inside the provided Selection.\n *\n * @param editor LexicalEditor to get the JSON content from.\n * @param selection Selection to get the JSON content from.\n * @returns an object with the editor namespace and a list of serializable nodes as JavaScript objects.\n */\nexport function $generateJSONFromSelectedNodes<\n  SerializedNode extends BaseSerializedNode,\n>(\n  editor: LexicalEditor,\n  selection: BaseSelection | null,\n): {\n  namespace: string;\n  nodes: Array<SerializedNode>;\n} {\n  const nodes: Array<SerializedNode> = [];\n  const root = $getRoot();\n  const topLevelChildren = root.getChildren();\n  for (let i = 0; i < topLevelChildren.length; i++) {\n    const topLevelNode = topLevelChildren[i];\n    $appendNodesToJSON(editor, selection, topLevelNode, nodes);\n  }\n  return {\n    namespace: editor._config.namespace,\n    nodes,\n  };\n}\n\n/**\n * This method takes an array of objects conforming to the BaseSeralizedNode interface and returns\n * an Array containing instances of the corresponding LexicalNode classes registered on the editor.\n * Normally, you'd get an Array of BaseSerialized nodes from {@link $generateJSONFromSelectedNodes}\n *\n * @param serializedNodes an Array of objects conforming to the BaseSerializedNode interface.\n * @returns an Array of Lexical Node objects.\n */\nexport function $generateNodesFromSerializedNodes(\n  serializedNodes: Array<BaseSerializedNode>,\n): Array<LexicalNode> {\n  const nodes = [];\n  for (let i = 0; i < serializedNodes.length; i++) {\n    const serializedNode = serializedNodes[i];\n    const node = $parseSerializedNode(serializedNode);\n    if ($isTextNode(node)) {\n      $addNodeStyle(node);\n    }\n    nodes.push(node);\n  }\n  return nodes;\n}\n\nconst EVENT_LATENCY = 50;\nlet clipboardEventTimeout: null | number = null;\n\n// TODO custom selection\n// TODO potentially have a node customizable version for plain text\n/**\n * Copies the content of the current selection to the clipboard in\n * text/plain, text/html, and application/x-lexical-editor (Lexical JSON)\n * formats.\n *\n * @param editor the LexicalEditor instance to copy content from\n * @param event the native browser ClipboardEvent to add the content to.\n * @returns\n */\nexport async function copyToClipboard(\n  editor: LexicalEditor,\n  event: null | ClipboardEvent,\n  data?: LexicalClipboardData,\n): Promise<boolean> {\n  if (clipboardEventTimeout !== null) {\n    // Prevent weird race conditions that can happen when this function is run multiple times\n    // synchronously. In the future, we can do better, we can cancel/override the previously running job.\n    return false;\n  }\n  if (event !== null) {\n    return new Promise((resolve, reject) => {\n      editor.update(() => {\n        resolve($copyToClipboardEvent(editor, event, data));\n      });\n    });\n  }\n\n  const rootElement = editor.getRootElement();\n  const windowDocument =\n    editor._window == null ? window.document : editor._window.document;\n  const domSelection = getDOMSelection(editor._window);\n  if (rootElement === null || domSelection === null) {\n    return false;\n  }\n  const element = windowDocument.createElement('span');\n  element.style.cssText = 'position: fixed; top: -1000px;';\n  element.append(windowDocument.createTextNode('#'));\n  rootElement.append(element);\n  const range = new Range();\n  range.setStart(element, 0);\n  range.setEnd(element, 1);\n  domSelection.removeAllRanges();\n  domSelection.addRange(range);\n  return new Promise((resolve, reject) => {\n    const removeListener = editor.registerCommand(\n      COPY_COMMAND,\n      (secondEvent) => {\n        if (objectKlassEquals(secondEvent, ClipboardEvent)) {\n          removeListener();\n          if (clipboardEventTimeout !== null) {\n            window.clearTimeout(clipboardEventTimeout);\n            clipboardEventTimeout = null;\n          }\n          resolve(\n            $copyToClipboardEvent(editor, secondEvent as ClipboardEvent, data),\n          );\n        }\n        // Block the entire copy flow while we wait for the next ClipboardEvent\n        return true;\n      },\n      COMMAND_PRIORITY_CRITICAL,\n    );\n    // If the above hack execCommand hack works, this timeout code should never fire. Otherwise,\n    // the listener will be quickly freed so that the user can reuse it again\n    clipboardEventTimeout = window.setTimeout(() => {\n      removeListener();\n      clipboardEventTimeout = null;\n      resolve(false);\n    }, EVENT_LATENCY);\n    windowDocument.execCommand('copy');\n    element.remove();\n  });\n}\n\n// TODO shouldn't pass editor (pass namespace directly)\nfunction $copyToClipboardEvent(\n  editor: LexicalEditor,\n  event: ClipboardEvent,\n  data?: LexicalClipboardData,\n): boolean {\n  if (data === undefined) {\n    const domSelection = getDOMSelection(editor._window);\n    if (!domSelection) {\n      return false;\n    }\n    const anchorDOM = domSelection.anchorNode;\n    const focusDOM = domSelection.focusNode;\n    if (\n      anchorDOM !== null &&\n      focusDOM !== null &&\n      !isSelectionWithinEditor(editor, anchorDOM, focusDOM)\n    ) {\n      return false;\n    }\n    const selection = $getSelection();\n    if (selection === null) {\n      return false;\n    }\n    data = $getClipboardDataFromSelection(selection);\n  }\n  event.preventDefault();\n  const clipboardData = event.clipboardData;\n  if (clipboardData === null) {\n    return false;\n  }\n  setLexicalClipboardDataTransfer(clipboardData, data);\n  return true;\n}\n\nconst clipboardDataFunctions = [\n  ['text/html', $getHtmlContent],\n  ['application/x-lexical-editor', $getLexicalContent],\n] as const;\n\n/**\n * Serialize the content of the current selection to strings in\n * text/plain, text/html, and application/x-lexical-editor (Lexical JSON)\n * formats (as available).\n *\n * @param selection the selection to serialize (defaults to $getSelection())\n * @returns LexicalClipboardData\n */\nexport function $getClipboardDataFromSelection(\n  selection: BaseSelection | null = $getSelection(),\n): LexicalClipboardData {\n  const clipboardData: LexicalClipboardData = {\n    'text/plain': selection ? selection.getTextContent() : '',\n  };\n  if (selection) {\n    const editor = $getEditor();\n    for (const [mimeType, $editorFn] of clipboardDataFunctions) {\n      const v = $editorFn(editor, selection);\n      if (v !== null) {\n        clipboardData[mimeType] = v;\n      }\n    }\n  }\n  return clipboardData;\n}\n\n/**\n * Call setData on the given clipboardData for each MIME type present\n * in the given data (from {@link $getClipboardDataFromSelection})\n *\n * @param clipboardData the event.clipboardData to populate from data\n * @param data The lexical data\n */\nexport function setLexicalClipboardDataTransfer(\n  clipboardData: DataTransfer,\n  data: LexicalClipboardData,\n) {\n  for (const k in data) {\n    const v = data[k as keyof LexicalClipboardData];\n    if (v !== undefined) {\n      clipboardData.setData(k, v);\n    }\n  }\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/clipboard/index.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nexport {\n  $generateJSONFromSelectedNodes,\n  $generateNodesFromSerializedNodes,\n  $getClipboardDataFromSelection,\n  $getHtmlContent,\n  $getLexicalContent,\n  $insertDataTransferForPlainText,\n  $insertDataTransferForRichText,\n  $insertGeneratedNodes,\n  copyToClipboard,\n  type LexicalClipboardData,\n  setLexicalClipboardDataTransfer,\n} from './clipboard';\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/LexicalCommands.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {\n  BaseSelection,\n  LexicalCommand,\n  LexicalNode,\n  TextFormatType,\n} from 'lexical';\n\nexport type PasteCommandType = ClipboardEvent | InputEvent | KeyboardEvent;\n\nexport function createCommand<T>(type?: string): LexicalCommand<T> {\n  return __DEV__ ? {type} : {};\n}\n\nexport const SELECTION_CHANGE_COMMAND: LexicalCommand<void> = createCommand(\n  'SELECTION_CHANGE_COMMAND',\n);\nexport const SELECTION_INSERT_CLIPBOARD_NODES_COMMAND: LexicalCommand<{\n  nodes: Array<LexicalNode>;\n  selection: BaseSelection;\n}> = createCommand('SELECTION_INSERT_CLIPBOARD_NODES_COMMAND');\nexport const CLICK_COMMAND: LexicalCommand<MouseEvent> =\n  createCommand('CLICK_COMMAND');\nexport const DELETE_CHARACTER_COMMAND: LexicalCommand<boolean> = createCommand(\n  'DELETE_CHARACTER_COMMAND',\n);\nexport const INSERT_LINE_BREAK_COMMAND: LexicalCommand<boolean> = createCommand(\n  'INSERT_LINE_BREAK_COMMAND',\n);\nexport const INSERT_PARAGRAPH_COMMAND: LexicalCommand<void> = createCommand(\n  'INSERT_PARAGRAPH_COMMAND',\n);\nexport const CONTROLLED_TEXT_INSERTION_COMMAND: LexicalCommand<\n  InputEvent | string\n> = createCommand('CONTROLLED_TEXT_INSERTION_COMMAND');\nexport const PASTE_COMMAND: LexicalCommand<PasteCommandType> =\n  createCommand('PASTE_COMMAND');\nexport const REMOVE_TEXT_COMMAND: LexicalCommand<InputEvent | null> =\n  createCommand('REMOVE_TEXT_COMMAND');\nexport const DELETE_WORD_COMMAND: LexicalCommand<boolean> = createCommand(\n  'DELETE_WORD_COMMAND',\n);\nexport const DELETE_LINE_COMMAND: LexicalCommand<boolean> = createCommand(\n  'DELETE_LINE_COMMAND',\n);\nexport const FORMAT_TEXT_COMMAND: LexicalCommand<TextFormatType> =\n  createCommand('FORMAT_TEXT_COMMAND');\nexport const UNDO_COMMAND: LexicalCommand<void> = createCommand('UNDO_COMMAND');\nexport const REDO_COMMAND: LexicalCommand<void> = createCommand('REDO_COMMAND');\nexport const KEY_DOWN_COMMAND: LexicalCommand<KeyboardEvent> =\n  createCommand('KEYDOWN_COMMAND');\nexport const KEY_ARROW_RIGHT_COMMAND: LexicalCommand<KeyboardEvent> =\n  createCommand('KEY_ARROW_RIGHT_COMMAND');\nexport const MOVE_TO_END: LexicalCommand<KeyboardEvent> =\n  createCommand('MOVE_TO_END');\nexport const KEY_ARROW_LEFT_COMMAND: LexicalCommand<KeyboardEvent> =\n  createCommand('KEY_ARROW_LEFT_COMMAND');\nexport const MOVE_TO_START: LexicalCommand<KeyboardEvent> =\n  createCommand('MOVE_TO_START');\nexport const KEY_ARROW_UP_COMMAND: LexicalCommand<KeyboardEvent> =\n  createCommand('KEY_ARROW_UP_COMMAND');\nexport const KEY_ARROW_DOWN_COMMAND: LexicalCommand<KeyboardEvent> =\n  createCommand('KEY_ARROW_DOWN_COMMAND');\nexport const KEY_ENTER_COMMAND: LexicalCommand<KeyboardEvent | null> =\n  createCommand('KEY_ENTER_COMMAND');\nexport const KEY_SPACE_COMMAND: LexicalCommand<KeyboardEvent> =\n  createCommand('KEY_SPACE_COMMAND');\nexport const KEY_BACKSPACE_COMMAND: LexicalCommand<KeyboardEvent> =\n  createCommand('KEY_BACKSPACE_COMMAND');\nexport const KEY_ESCAPE_COMMAND: LexicalCommand<KeyboardEvent> =\n  createCommand('KEY_ESCAPE_COMMAND');\nexport const KEY_DELETE_COMMAND: LexicalCommand<KeyboardEvent> =\n  createCommand('KEY_DELETE_COMMAND');\nexport const KEY_AT_COMMAND: LexicalCommand<KeyboardEvent> =\n    createCommand('KEY_AT_COMMAND');\nexport const KEY_TAB_COMMAND: LexicalCommand<KeyboardEvent> =\n  createCommand('KEY_TAB_COMMAND');\nexport const INSERT_TAB_COMMAND: LexicalCommand<void> =\n  createCommand('INSERT_TAB_COMMAND');\nexport const INDENT_CONTENT_COMMAND: LexicalCommand<void> = createCommand(\n  'INDENT_CONTENT_COMMAND',\n);\nexport const OUTDENT_CONTENT_COMMAND: LexicalCommand<void> = createCommand(\n  'OUTDENT_CONTENT_COMMAND',\n);\nexport const DROP_COMMAND: LexicalCommand<DragEvent> =\n  createCommand('DROP_COMMAND');\nexport const DRAGSTART_COMMAND: LexicalCommand<DragEvent> =\n  createCommand('DRAGSTART_COMMAND');\nexport const DRAGOVER_COMMAND: LexicalCommand<DragEvent> =\n  createCommand('DRAGOVER_COMMAND');\nexport const DRAGEND_COMMAND: LexicalCommand<DragEvent> =\n  createCommand('DRAGEND_COMMAND');\nexport const COPY_COMMAND: LexicalCommand<\n  ClipboardEvent | KeyboardEvent | null\n> = createCommand('COPY_COMMAND');\nexport const CUT_COMMAND: LexicalCommand<\n  ClipboardEvent | KeyboardEvent | null\n> = createCommand('CUT_COMMAND');\nexport const SELECT_ALL_COMMAND: LexicalCommand<KeyboardEvent> =\n  createCommand('SELECT_ALL_COMMAND');\nexport const CLEAR_EDITOR_COMMAND: LexicalCommand<void> = createCommand(\n  'CLEAR_EDITOR_COMMAND',\n);\nexport const CLEAR_HISTORY_COMMAND: LexicalCommand<void> = createCommand(\n  'CLEAR_HISTORY_COMMAND',\n);\nexport const CAN_REDO_COMMAND: LexicalCommand<boolean> =\n  createCommand('CAN_REDO_COMMAND');\nexport const CAN_UNDO_COMMAND: LexicalCommand<boolean> =\n  createCommand('CAN_UNDO_COMMAND');\nexport const FOCUS_COMMAND: LexicalCommand<FocusEvent> =\n  createCommand('FOCUS_COMMAND');\nexport const BLUR_COMMAND: LexicalCommand<FocusEvent> =\n  createCommand('BLUR_COMMAND');\nexport const KEY_MODIFIER_COMMAND: LexicalCommand<KeyboardEvent> =\n  createCommand('KEY_MODIFIER_COMMAND');\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/LexicalConstants.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {\n  TextDetailType,\n  TextFormatType,\n  TextModeType,\n} from './nodes/LexicalTextNode';\n\nimport {\n  IS_APPLE_WEBKIT,\n  IS_FIREFOX,\n  IS_IOS,\n  IS_SAFARI,\n} from 'lexical/shared/environment';\n\n// DOM\nexport const DOM_ELEMENT_TYPE = 1;\nexport const DOM_TEXT_TYPE = 3;\n\n// Reconciling\nexport const NO_DIRTY_NODES = 0;\nexport const HAS_DIRTY_NODES = 1;\nexport const FULL_RECONCILE = 2;\n\n// Text node modes\nexport const IS_NORMAL = 0;\nexport const IS_TOKEN = 1;\nexport const IS_SEGMENTED = 2;\n// IS_INERT = 3\n\n// Text node formatting\nexport const IS_BOLD = 1;\nexport const IS_ITALIC = 1 << 1;\nexport const IS_STRIKETHROUGH = 1 << 2;\nexport const IS_UNDERLINE = 1 << 3;\nexport const IS_CODE = 1 << 4;\nexport const IS_SUBSCRIPT = 1 << 5;\nexport const IS_SUPERSCRIPT = 1 << 6;\nexport const IS_HIGHLIGHT = 1 << 7;\n\nexport const IS_ALL_FORMATTING =\n  IS_BOLD |\n  IS_ITALIC |\n  IS_STRIKETHROUGH |\n  IS_UNDERLINE |\n  IS_CODE |\n  IS_SUBSCRIPT |\n  IS_SUPERSCRIPT |\n  IS_HIGHLIGHT;\n\n// Text node details\nexport const IS_DIRECTIONLESS = 1;\nexport const IS_UNMERGEABLE = 1 << 1;\n\n// Element node formatting\nexport const IS_ALIGN_LEFT = 1;\nexport const IS_ALIGN_CENTER = 2;\nexport const IS_ALIGN_RIGHT = 3;\nexport const IS_ALIGN_JUSTIFY = 4;\nexport const IS_ALIGN_START = 5;\nexport const IS_ALIGN_END = 6;\n\n// Reconciliation\nexport const NON_BREAKING_SPACE = '\\u00A0';\nconst ZERO_WIDTH_SPACE = '\\u200b';\n\n// For iOS/Safari we use a non breaking space, otherwise the cursor appears\n// overlapping the composed text.\nexport const COMPOSITION_SUFFIX: string =\n  IS_SAFARI || IS_IOS || IS_APPLE_WEBKIT\n    ? NON_BREAKING_SPACE\n    : ZERO_WIDTH_SPACE;\nexport const DOUBLE_LINE_BREAK = '\\n\\n';\n\n// For FF, we need to use a non-breaking space, or it gets composition\n// in a stuck state.\nexport const COMPOSITION_START_CHAR: string = IS_FIREFOX\n  ? NON_BREAKING_SPACE\n  : COMPOSITION_SUFFIX;\nconst RTL = '\\u0591-\\u07FF\\uFB1D-\\uFDFD\\uFE70-\\uFEFC';\nconst LTR =\n  'A-Za-z\\u00C0-\\u00D6\\u00D8-\\u00F6' +\n  '\\u00F8-\\u02B8\\u0300-\\u0590\\u0800-\\u1FFF\\u200E\\u2C00-\\uFB1C' +\n  '\\uFE00-\\uFE6F\\uFEFD-\\uFFFF';\n\n// eslint-disable-next-line no-misleading-character-class\nexport const RTL_REGEX = new RegExp('^[^' + LTR + ']*[' + RTL + ']');\n// eslint-disable-next-line no-misleading-character-class\nexport const LTR_REGEX = new RegExp('^[^' + RTL + ']*[' + LTR + ']');\n\nexport const TEXT_TYPE_TO_FORMAT: Record<TextFormatType | string, number> = {\n  bold: IS_BOLD,\n  code: IS_CODE,\n  highlight: IS_HIGHLIGHT,\n  italic: IS_ITALIC,\n  strikethrough: IS_STRIKETHROUGH,\n  subscript: IS_SUBSCRIPT,\n  superscript: IS_SUPERSCRIPT,\n  underline: IS_UNDERLINE,\n};\n\nexport const DETAIL_TYPE_TO_DETAIL: Record<TextDetailType | string, number> = {\n  directionless: IS_DIRECTIONLESS,\n  unmergeable: IS_UNMERGEABLE,\n};\n\nexport const TEXT_MODE_TO_TYPE: Record<TextModeType, 0 | 1 | 2> = {\n  normal: IS_NORMAL,\n  segmented: IS_SEGMENTED,\n  token: IS_TOKEN,\n};\n\nexport const TEXT_TYPE_TO_MODE: Record<number, TextModeType> = {\n  [IS_NORMAL]: 'normal',\n  [IS_SEGMENTED]: 'segmented',\n  [IS_TOKEN]: 'token',\n};\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/LexicalEditor.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {EditorState, SerializedEditorState} from './LexicalEditorState';\nimport type {\n  DOMConversion,\n  DOMConversionMap,\n  DOMExportOutput,\n  DOMExportOutputMap,\n  NodeKey,\n} from './LexicalNode';\n\nimport invariant from 'lexical/shared/invariant';\n\nimport {$getRoot, $getSelection, TextNode} from '.';\nimport {FULL_RECONCILE, NO_DIRTY_NODES} from './LexicalConstants';\nimport {createEmptyEditorState} from './LexicalEditorState';\nimport {addRootElementEvents, removeRootElementEvents} from './LexicalEvents';\nimport {$flushRootMutations, initMutationObserver} from './LexicalMutations';\nimport {LexicalNode} from './LexicalNode';\nimport {\n  $commitPendingUpdates,\n  internalGetActiveEditor,\n  parseEditorState,\n  triggerListeners,\n  updateEditor,\n} from './LexicalUpdates';\nimport {\n  createUID,\n  dispatchCommand,\n  getCachedClassNameArray,\n  getCachedTypeToNodeMap,\n  getDefaultView,\n  getDOMSelection,\n  markAllNodesAsDirty,\n} from './LexicalUtils';\nimport {ArtificialNode__DO_NOT_USE} from './nodes/ArtificialNode';\nimport {DecoratorNode} from './nodes/LexicalDecoratorNode';\nimport {LineBreakNode} from './nodes/LexicalLineBreakNode';\nimport {ParagraphNode} from './nodes/LexicalParagraphNode';\nimport {RootNode} from './nodes/LexicalRootNode';\nimport {TabNode} from './nodes/LexicalTabNode';\n\nexport type Spread<T1, T2> = Omit<T2, keyof T1> & T1;\n\n// https://github.com/microsoft/TypeScript/issues/3841\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type KlassConstructor<Cls extends GenericConstructor<any>> =\n  GenericConstructor<InstanceType<Cls>> & {[k in keyof Cls]: Cls[k]};\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype GenericConstructor<T> = new (...args: any[]) => T;\n\nexport type Klass<T extends LexicalNode> = InstanceType<\n  T['constructor']\n> extends T\n  ? T['constructor']\n  : GenericConstructor<T> & T['constructor'];\n\nexport type EditorThemeClassName = string;\n\nexport type TextNodeThemeClasses = {\n  base?: EditorThemeClassName;\n  bold?: EditorThemeClassName;\n  code?: EditorThemeClassName;\n  highlight?: EditorThemeClassName;\n  italic?: EditorThemeClassName;\n  strikethrough?: EditorThemeClassName;\n  subscript?: EditorThemeClassName;\n  superscript?: EditorThemeClassName;\n  underline?: EditorThemeClassName;\n  underlineStrikethrough?: EditorThemeClassName;\n  [key: string]: EditorThemeClassName | undefined;\n};\n\nexport type EditorUpdateOptions = {\n  onUpdate?: () => void;\n  skipTransforms?: true;\n  tag?: string;\n  discrete?: true;\n};\n\nexport type EditorSetOptions = {\n  tag?: string;\n};\n\nexport type EditorFocusOptions = {\n  defaultSelection?: 'rootStart' | 'rootEnd';\n};\n\nexport type EditorThemeClasses = {\n  blockCursor?: EditorThemeClassName;\n  characterLimit?: EditorThemeClassName;\n  code?: EditorThemeClassName;\n  codeHighlight?: Record<string, EditorThemeClassName>;\n  hashtag?: EditorThemeClassName;\n  heading?: {\n    h1?: EditorThemeClassName;\n    h2?: EditorThemeClassName;\n    h3?: EditorThemeClassName;\n    h4?: EditorThemeClassName;\n    h5?: EditorThemeClassName;\n    h6?: EditorThemeClassName;\n  };\n  hr?: EditorThemeClassName;\n  image?: EditorThemeClassName;\n  link?: EditorThemeClassName;\n  list?: {\n    ul?: EditorThemeClassName;\n    ulDepth?: Array<EditorThemeClassName>;\n    ol?: EditorThemeClassName;\n    olDepth?: Array<EditorThemeClassName>;\n    checklist?: EditorThemeClassName;\n    listitem?: EditorThemeClassName;\n    listitemChecked?: EditorThemeClassName;\n    listitemUnchecked?: EditorThemeClassName;\n    nested?: {\n      list?: EditorThemeClassName;\n      listitem?: EditorThemeClassName;\n    };\n  };\n  ltr?: EditorThemeClassName;\n  mark?: EditorThemeClassName;\n  markOverlap?: EditorThemeClassName;\n  paragraph?: EditorThemeClassName;\n  quote?: EditorThemeClassName;\n  root?: EditorThemeClassName;\n  rtl?: EditorThemeClassName;\n  table?: EditorThemeClassName;\n  tableAddColumns?: EditorThemeClassName;\n  tableAddRows?: EditorThemeClassName;\n  tableCellActionButton?: EditorThemeClassName;\n  tableCellActionButtonContainer?: EditorThemeClassName;\n  tableCellPrimarySelected?: EditorThemeClassName;\n  tableCellSelected?: EditorThemeClassName;\n  tableCell?: EditorThemeClassName;\n  tableCellEditing?: EditorThemeClassName;\n  tableCellHeader?: EditorThemeClassName;\n  tableCellResizer?: EditorThemeClassName;\n  tableCellSortedIndicator?: EditorThemeClassName;\n  tableResizeRuler?: EditorThemeClassName;\n  tableRow?: EditorThemeClassName;\n  tableSelected?: EditorThemeClassName;\n  text?: TextNodeThemeClasses;\n  embedBlock?: {\n    base?: EditorThemeClassName;\n    focus?: EditorThemeClassName;\n  };\n  indent?: EditorThemeClassName;\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  [key: string]: any;\n};\n\nexport type EditorConfig = {\n  disableEvents?: boolean;\n  namespace: string;\n  theme: EditorThemeClasses;\n};\n\nexport type LexicalNodeReplacement = {\n  replace: Klass<LexicalNode>;\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  with: <T extends {new (...args: any): any}>(\n    node: InstanceType<T>,\n  ) => LexicalNode;\n  withKlass?: Klass<LexicalNode>;\n};\n\nexport type HTMLConfig = {\n  export?: DOMExportOutputMap;\n  import?: DOMConversionMap;\n};\n\nexport type CreateEditorArgs = {\n  disableEvents?: boolean;\n  editorState?: EditorState;\n  namespace?: string;\n  nodes?: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement>;\n  onError?: ErrorHandler;\n  parentEditor?: LexicalEditor;\n  editable?: boolean;\n  theme?: EditorThemeClasses;\n  html?: HTMLConfig;\n};\n\nexport type RegisteredNodes = Map<string, RegisteredNode>;\n\nexport type RegisteredNode = {\n  klass: Klass<LexicalNode>;\n  transforms: Set<Transform<LexicalNode>>;\n  replace: null | ((node: LexicalNode) => LexicalNode);\n  replaceWithKlass: null | Klass<LexicalNode>;\n  exportDOM?: (\n    editor: LexicalEditor,\n    targetNode: LexicalNode,\n  ) => DOMExportOutput;\n};\n\nexport type Transform<T extends LexicalNode> = (node: T) => void;\n\nexport type ErrorHandler = (error: Error) => void;\n\nexport type MutationListeners = Map<MutationListener, Klass<LexicalNode>>;\n\nexport type MutatedNodes = Map<Klass<LexicalNode>, Map<NodeKey, NodeMutation>>;\n\nexport type NodeMutation = 'created' | 'updated' | 'destroyed';\n\nexport interface MutationListenerOptions {\n  /**\n   * Skip the initial call of the listener with pre-existing DOM nodes.\n   *\n   * The default is currently true for backwards compatibility with <= 0.16.1\n   * but this default is expected to change to false in 0.17.0.\n   */\n  skipInitialization?: boolean;\n}\n\nconst DEFAULT_SKIP_INITIALIZATION = true;\n\nexport type UpdateListener = (arg0: {\n  dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>;\n  dirtyLeaves: Set<NodeKey>;\n  editorState: EditorState;\n  normalizedNodes: Set<NodeKey>;\n  prevEditorState: EditorState;\n  tags: Set<string>;\n}) => void;\n\nexport type DecoratorListener<T = never> = (\n  decorator: Record<NodeKey, T>,\n) => void;\n\nexport type RootListener = (\n  rootElement: null | HTMLElement,\n  prevRootElement: null | HTMLElement,\n) => void;\n\nexport type TextContentListener = (text: string) => void;\n\nexport type MutationListener = (\n  nodes: Map<NodeKey, NodeMutation>,\n  payload: {\n    updateTags: Set<string>;\n    dirtyLeaves: Set<string>;\n    prevEditorState: EditorState;\n  },\n) => void;\n\nexport type CommandListener<P> = (payload: P, editor: LexicalEditor) => boolean;\n\nexport type EditableListener = (editable: boolean) => void;\n\nexport type CommandListenerPriority = 0 | 1 | 2 | 3 | 4;\n\nexport const COMMAND_PRIORITY_EDITOR = 0;\nexport const COMMAND_PRIORITY_LOW = 1;\nexport const COMMAND_PRIORITY_NORMAL = 2;\nexport const COMMAND_PRIORITY_HIGH = 3;\nexport const COMMAND_PRIORITY_CRITICAL = 4;\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport type LexicalCommand<TPayload> = {\n  type?: string;\n};\n\n/**\n * Type helper for extracting the payload type from a command.\n *\n * @example\n * ```ts\n * const MY_COMMAND = createCommand<SomeType>();\n *\n * // ...\n *\n * editor.registerCommand(MY_COMMAND, payload => {\n *   // Type of `payload` is inferred here. But lets say we want to extract a function to delegate to\n *   handleMyCommand(editor, payload);\n *   return true;\n * });\n *\n * function handleMyCommand(editor: LexicalEditor, payload: CommandPayloadType<typeof MY_COMMAND>) {\n *   // `payload` is of type `SomeType`, extracted from the command.\n * }\n * ```\n */\nexport type CommandPayloadType<TCommand extends LexicalCommand<unknown>> =\n  TCommand extends LexicalCommand<infer TPayload> ? TPayload : never;\n\ntype Commands = Map<\n  LexicalCommand<unknown>,\n  Array<Set<CommandListener<unknown>>>\n>;\ntype Listeners = {\n  decorator: Set<DecoratorListener>;\n  mutation: MutationListeners;\n  editable: Set<EditableListener>;\n  root: Set<RootListener>;\n  textcontent: Set<TextContentListener>;\n  update: Set<UpdateListener>;\n};\n\nexport type Listener =\n  | DecoratorListener\n  | EditableListener\n  | MutationListener\n  | RootListener\n  | TextContentListener\n  | UpdateListener;\n\nexport type ListenerType =\n  | 'update'\n  | 'root'\n  | 'decorator'\n  | 'textcontent'\n  | 'mutation'\n  | 'editable';\n\nexport type TransformerType = 'text' | 'decorator' | 'element' | 'root';\n\ntype IntentionallyMarkedAsDirtyElement = boolean;\n\ntype DOMConversionCache = Map<\n  string,\n  Array<(node: Node) => DOMConversion | null>\n>;\n\nexport type SerializedEditor = {\n  editorState: SerializedEditorState;\n};\n\nexport function resetEditor(\n  editor: LexicalEditor,\n  prevRootElement: null | HTMLElement,\n  nextRootElement: null | HTMLElement,\n  pendingEditorState: EditorState,\n): void {\n  const keyNodeMap = editor._keyToDOMMap;\n  keyNodeMap.clear();\n  editor._editorState = createEmptyEditorState();\n  editor._pendingEditorState = pendingEditorState;\n  editor._compositionKey = null;\n  editor._dirtyType = NO_DIRTY_NODES;\n  editor._cloneNotNeeded.clear();\n  editor._dirtyLeaves = new Set();\n  editor._dirtyElements.clear();\n  editor._normalizedNodes = new Set();\n  editor._updateTags = new Set();\n  editor._updates = [];\n  editor._blockCursorElement = null;\n\n  const observer = editor._observer;\n\n  if (observer !== null) {\n    observer.disconnect();\n    editor._observer = null;\n  }\n\n  // Remove all the DOM nodes from the root element\n  if (prevRootElement !== null) {\n    prevRootElement.textContent = '';\n  }\n\n  if (nextRootElement !== null) {\n    nextRootElement.textContent = '';\n    keyNodeMap.set('root', nextRootElement);\n  }\n}\n\nfunction initializeConversionCache(\n  nodes: RegisteredNodes,\n  additionalConversions?: DOMConversionMap,\n): DOMConversionCache {\n  const conversionCache = new Map();\n  const handledConversions = new Set();\n  const addConversionsToCache = (map: DOMConversionMap) => {\n    Object.keys(map).forEach((key) => {\n      let currentCache = conversionCache.get(key);\n\n      if (currentCache === undefined) {\n        currentCache = [];\n        conversionCache.set(key, currentCache);\n      }\n\n      currentCache.push(map[key]);\n    });\n  };\n  nodes.forEach((node) => {\n    const importDOM = node.klass.importDOM;\n\n    if (importDOM == null || handledConversions.has(importDOM)) {\n      return;\n    }\n\n    handledConversions.add(importDOM);\n    const map = importDOM.call(node.klass);\n\n    if (map !== null) {\n      addConversionsToCache(map);\n    }\n  });\n  if (additionalConversions) {\n    addConversionsToCache(additionalConversions);\n  }\n  return conversionCache;\n}\n\n/**\n * Creates a new LexicalEditor attached to a single contentEditable (provided in the config). This is\n * the lowest-level initialization API for a LexicalEditor. If you're using React or another framework,\n * consider using the appropriate abstractions, such as LexicalComposer\n * @param editorConfig - the editor configuration.\n * @returns a LexicalEditor instance\n */\nexport function createEditor(editorConfig?: CreateEditorArgs): LexicalEditor {\n  const config = editorConfig || {};\n  const activeEditor = internalGetActiveEditor();\n  const theme = config.theme || {};\n  const parentEditor =\n    editorConfig === undefined ? activeEditor : config.parentEditor || null;\n  const disableEvents = config.disableEvents || false;\n  const editorState = createEmptyEditorState();\n  const namespace =\n    config.namespace ||\n    (parentEditor !== null ? parentEditor._config.namespace : createUID());\n  const initialEditorState = config.editorState;\n  const nodes = [\n    RootNode,\n    TextNode,\n    LineBreakNode,\n    TabNode,\n    ParagraphNode,\n    ArtificialNode__DO_NOT_USE,\n    ...(config.nodes || []),\n  ];\n  const {onError, html} = config;\n  const isEditable = config.editable !== undefined ? config.editable : true;\n  let registeredNodes: Map<string, RegisteredNode>;\n\n  if (editorConfig === undefined && activeEditor !== null) {\n    registeredNodes = activeEditor._nodes;\n  } else {\n    registeredNodes = new Map();\n    for (let i = 0; i < nodes.length; i++) {\n      let klass = nodes[i];\n      let replace: RegisteredNode['replace'] = null;\n      let replaceWithKlass: RegisteredNode['replaceWithKlass'] = null;\n\n      if (typeof klass !== 'function') {\n        const options = klass;\n        klass = options.replace;\n        replace = options.with;\n        replaceWithKlass = options.withKlass || null;\n      }\n      // Ensure custom nodes implement required methods and replaceWithKlass is instance of base klass.\n      if (__DEV__) {\n        // ArtificialNode__DO_NOT_USE can get renamed, so we use the type\n        const nodeType =\n          Object.prototype.hasOwnProperty.call(klass, 'getType') &&\n          klass.getType();\n        const name = klass.name;\n\n        if (replaceWithKlass) {\n          invariant(\n            replaceWithKlass.prototype instanceof klass,\n            \"%s doesn't extend the %s\",\n            replaceWithKlass.name,\n            name,\n          );\n        }\n\n        if (\n          name !== 'RootNode' &&\n          nodeType !== 'root' &&\n          nodeType !== 'artificial'\n        ) {\n          const proto = klass.prototype;\n          ['getType', 'clone'].forEach((method) => {\n            // eslint-disable-next-line no-prototype-builtins\n            if (!klass.hasOwnProperty(method)) {\n              console.warn(`${name} must implement static \"${method}\" method`);\n            }\n          });\n          if (\n            // eslint-disable-next-line no-prototype-builtins\n            !klass.hasOwnProperty('importDOM') &&\n            // eslint-disable-next-line no-prototype-builtins\n            klass.hasOwnProperty('exportDOM')\n          ) {\n            console.warn(\n              `${name} should implement \"importDOM\" if using a custom \"exportDOM\" method to ensure HTML serialization (important for copy & paste) works as expected`,\n            );\n          }\n          if (proto instanceof DecoratorNode) {\n            // eslint-disable-next-line no-prototype-builtins\n            if (!proto.hasOwnProperty('decorate')) {\n              console.warn(\n                `${proto.constructor.name} must implement \"decorate\" method`,\n              );\n            }\n          }\n          if (\n            // eslint-disable-next-line no-prototype-builtins\n            !klass.hasOwnProperty('importJSON')\n          ) {\n            console.warn(\n              `${name} should implement \"importJSON\" method to ensure JSON and default HTML serialization works as expected`,\n            );\n          }\n          if (\n            // eslint-disable-next-line no-prototype-builtins\n            !proto.hasOwnProperty('exportJSON')\n          ) {\n            console.warn(\n              `${name} should implement \"exportJSON\" method to ensure JSON and default HTML serialization works as expected`,\n            );\n          }\n        }\n      }\n      const type = klass.getType();\n      const transform = klass.transform();\n      const transforms = new Set<Transform<LexicalNode>>();\n      if (transform !== null) {\n        transforms.add(transform);\n      }\n      registeredNodes.set(type, {\n        exportDOM: html && html.export ? html.export.get(klass) : undefined,\n        klass,\n        replace,\n        replaceWithKlass,\n        transforms,\n      });\n    }\n  }\n  const editor = new LexicalEditor(\n    editorState,\n    parentEditor,\n    registeredNodes,\n    {\n      disableEvents,\n      namespace,\n      theme,\n    },\n    onError ? onError : console.error,\n    initializeConversionCache(registeredNodes, html ? html.import : undefined),\n    isEditable,\n  );\n\n  if (initialEditorState !== undefined) {\n    editor._pendingEditorState = initialEditorState;\n    editor._dirtyType = FULL_RECONCILE;\n  }\n\n  return editor;\n}\nexport class LexicalEditor {\n  ['constructor']!: KlassConstructor<typeof LexicalEditor>;\n\n  /** The version with build identifiers for this editor (since 0.17.1) */\n  static version: string | undefined;\n\n  /** @internal */\n  _headless: boolean;\n  /** @internal */\n  _parentEditor: null | LexicalEditor;\n  /** @internal */\n  _rootElement: null | HTMLElement;\n  /** @internal */\n  _editorState: EditorState;\n  /** @internal */\n  _pendingEditorState: null | EditorState;\n  /** @internal */\n  _compositionKey: null | NodeKey;\n  /** @internal */\n  _deferred: Array<() => void>;\n  /** @internal */\n  _keyToDOMMap: Map<NodeKey, HTMLElement>;\n  /** @internal */\n  _updates: Array<[() => void, EditorUpdateOptions | undefined]>;\n  /** @internal */\n  _updating: boolean;\n  /** @internal */\n  _listeners: Listeners;\n  /** @internal */\n  _commands: Commands;\n  /** @internal */\n  _nodes: RegisteredNodes;\n  /** @internal */\n  _decorators: Record<NodeKey, unknown>;\n  /** @internal */\n  _pendingDecorators: null | Record<NodeKey, unknown>;\n  /** @internal */\n  _config: EditorConfig;\n  /** @internal */\n  _dirtyType: 0 | 1 | 2;\n  /** @internal */\n  _cloneNotNeeded: Set<NodeKey>;\n  /** @internal */\n  _dirtyLeaves: Set<NodeKey>;\n  /** @internal */\n  _dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>;\n  /** @internal */\n  _normalizedNodes: Set<NodeKey>;\n  /** @internal */\n  _updateTags: Set<string>;\n  /** @internal */\n  _observer: null | MutationObserver;\n  /** @internal */\n  _key: string;\n  /** @internal */\n  _onError: ErrorHandler;\n  /** @internal */\n  _htmlConversions: DOMConversionCache;\n  /** @internal */\n  _window: null | Window;\n  /** @internal */\n  _editable: boolean;\n  /** @internal */\n  _blockCursorElement: null | HTMLDivElement;\n\n  /** @internal */\n  constructor(\n    editorState: EditorState,\n    parentEditor: null | LexicalEditor,\n    nodes: RegisteredNodes,\n    config: EditorConfig,\n    onError: ErrorHandler,\n    htmlConversions: DOMConversionCache,\n    editable: boolean,\n  ) {\n    this._parentEditor = parentEditor;\n    // The root element associated with this editor\n    this._rootElement = null;\n    // The current editor state\n    this._editorState = editorState;\n    // Handling of drafts and updates\n    this._pendingEditorState = null;\n    // Used to help co-ordinate selection and events\n    this._compositionKey = null;\n    this._deferred = [];\n    // Used during reconciliation\n    this._keyToDOMMap = new Map();\n    this._updates = [];\n    this._updating = false;\n    // Listeners\n    this._listeners = {\n      decorator: new Set(),\n      editable: new Set(),\n      mutation: new Map(),\n      root: new Set(),\n      textcontent: new Set(),\n      update: new Set(),\n    };\n    // Commands\n    this._commands = new Map();\n    // Editor configuration for theme/context.\n    this._config = config;\n    // Mapping of types to their nodes\n    this._nodes = nodes;\n    // React node decorators for portals\n    this._decorators = {};\n    this._pendingDecorators = null;\n    // Used to optimize reconciliation\n    this._dirtyType = NO_DIRTY_NODES;\n    this._cloneNotNeeded = new Set();\n    this._dirtyLeaves = new Set();\n    this._dirtyElements = new Map();\n    this._normalizedNodes = new Set();\n    this._updateTags = new Set();\n    // Handling of DOM mutations\n    this._observer = null;\n    // Used for identifying owning editors\n    this._key = createUID();\n\n    this._onError = onError;\n    this._htmlConversions = htmlConversions;\n    this._editable = editable;\n    this._headless = parentEditor !== null && parentEditor._headless;\n    this._window = null;\n    this._blockCursorElement = null;\n  }\n\n  /**\n   *\n   * @returns true if the editor is currently in \"composition\" mode due to receiving input\n   * through an IME, or 3P extension, for example. Returns false otherwise.\n   */\n  isComposing(): boolean {\n    return this._compositionKey != null;\n  }\n  /**\n   * Registers a listener for Editor update event. Will trigger the provided callback\n   * each time the editor goes through an update (via {@link LexicalEditor.update}) until the\n   * teardown function is called.\n   *\n   * @returns a teardown function that can be used to cleanup the listener.\n   */\n  registerUpdateListener(listener: UpdateListener): () => void {\n    const listenerSetOrMap = this._listeners.update;\n    listenerSetOrMap.add(listener);\n    return () => {\n      listenerSetOrMap.delete(listener);\n    };\n  }\n  /**\n   * Registers a listener for for when the editor changes between editable and non-editable states.\n   * Will trigger the provided callback each time the editor transitions between these states until the\n   * teardown function is called.\n   *\n   * @returns a teardown function that can be used to cleanup the listener.\n   */\n  registerEditableListener(listener: EditableListener): () => void {\n    const listenerSetOrMap = this._listeners.editable;\n    listenerSetOrMap.add(listener);\n    return () => {\n      listenerSetOrMap.delete(listener);\n    };\n  }\n  /**\n   * Registers a listener for when the editor's decorator object changes. The decorator object contains\n   * all DecoratorNode keys -> their decorated value. This is primarily used with external UI frameworks.\n   *\n   * Will trigger the provided callback each time the editor transitions between these states until the\n   * teardown function is called.\n   *\n   * @returns a teardown function that can be used to cleanup the listener.\n   */\n  registerDecoratorListener<T>(listener: DecoratorListener<T>): () => void {\n    const listenerSetOrMap = this._listeners.decorator;\n    listenerSetOrMap.add(listener);\n    return () => {\n      listenerSetOrMap.delete(listener);\n    };\n  }\n  /**\n   * Registers a listener for when Lexical commits an update to the DOM and the text content of\n   * the editor changes from the previous state of the editor. If the text content is the\n   * same between updates, no notifications to the listeners will happen.\n   *\n   * Will trigger the provided callback each time the editor transitions between these states until the\n   * teardown function is called.\n   *\n   * @returns a teardown function that can be used to cleanup the listener.\n   */\n  registerTextContentListener(listener: TextContentListener): () => void {\n    const listenerSetOrMap = this._listeners.textcontent;\n    listenerSetOrMap.add(listener);\n    return () => {\n      listenerSetOrMap.delete(listener);\n    };\n  }\n  /**\n   * Registers a listener for when the editor's root DOM element (the content editable\n   * Lexical attaches to) changes. This is primarily used to attach event listeners to the root\n   *  element. The root listener function is executed directly upon registration and then on\n   * any subsequent update.\n   *\n   * Will trigger the provided callback each time the editor transitions between these states until the\n   * teardown function is called.\n   *\n   * @returns a teardown function that can be used to cleanup the listener.\n   */\n  registerRootListener(listener: RootListener): () => void {\n    const listenerSetOrMap = this._listeners.root;\n    listener(this._rootElement, null);\n    listenerSetOrMap.add(listener);\n    return () => {\n      listener(null, this._rootElement);\n      listenerSetOrMap.delete(listener);\n    };\n  }\n  /**\n   * Registers a listener that will trigger anytime the provided command\n   * is dispatched, subject to priority. Listeners that run at a higher priority can \"intercept\"\n   * commands and prevent them from propagating to other handlers by returning true.\n   *\n   * Listeners registered at the same priority level will run deterministically in the order of registration.\n   *\n   * @param command - the command that will trigger the callback.\n   * @param listener - the function that will execute when the command is dispatched.\n   * @param priority - the relative priority of the listener. 0 | 1 | 2 | 3 | 4\n   * @returns a teardown function that can be used to cleanup the listener.\n   */\n  registerCommand<P>(\n    command: LexicalCommand<P>,\n    listener: CommandListener<P>,\n    priority: CommandListenerPriority,\n  ): () => void {\n    if (priority === undefined) {\n      invariant(false, 'Listener for type \"command\" requires a \"priority\".');\n    }\n\n    const commandsMap = this._commands;\n\n    if (!commandsMap.has(command)) {\n      commandsMap.set(command, [\n        new Set(),\n        new Set(),\n        new Set(),\n        new Set(),\n        new Set(),\n      ]);\n    }\n\n    const listenersInPriorityOrder = commandsMap.get(command);\n\n    if (listenersInPriorityOrder === undefined) {\n      invariant(\n        false,\n        'registerCommand: Command %s not found in command map',\n        String(command),\n      );\n    }\n\n    const listeners = listenersInPriorityOrder[priority];\n    listeners.add(listener as CommandListener<unknown>);\n    return () => {\n      listeners.delete(listener as CommandListener<unknown>);\n\n      if (\n        listenersInPriorityOrder.every(\n          (listenersSet) => listenersSet.size === 0,\n        )\n      ) {\n        commandsMap.delete(command);\n      }\n    };\n  }\n\n  /**\n   * Registers a listener that will run when a Lexical node of the provided class is\n   * mutated. The listener will receive a list of nodes along with the type of mutation\n   * that was performed on each: created, destroyed, or updated.\n   *\n   * One common use case for this is to attach DOM event listeners to the underlying DOM nodes as Lexical nodes are created.\n   * {@link LexicalEditor.getElementByKey} can be used for this.\n   *\n   * If any existing nodes are in the DOM, and skipInitialization is not true, the listener\n   * will be called immediately with an updateTag of 'registerMutationListener' where all\n   * nodes have the 'created' NodeMutation. This can be controlled with the skipInitialization option\n   * (default is currently true for backwards compatibility in 0.16.x but will change to false in 0.17.0).\n   *\n   * @param klass - The class of the node that you want to listen to mutations on.\n   * @param listener - The logic you want to run when the node is mutated.\n   * @param options - see {@link MutationListenerOptions}\n   * @returns a teardown function that can be used to cleanup the listener.\n   */\n  registerMutationListener(\n    klass: Klass<LexicalNode>,\n    listener: MutationListener,\n    options?: MutationListenerOptions,\n  ): () => void {\n    const klassToMutate = this.resolveRegisteredNodeAfterReplacements(\n      this.getRegisteredNode(klass),\n    ).klass;\n    const mutations = this._listeners.mutation;\n    mutations.set(listener, klassToMutate);\n    const skipInitialization = options && options.skipInitialization;\n    if (\n      !(skipInitialization === undefined\n        ? DEFAULT_SKIP_INITIALIZATION\n        : skipInitialization)\n    ) {\n      this.initializeMutationListener(listener, klassToMutate);\n    }\n\n    return () => {\n      mutations.delete(listener);\n    };\n  }\n\n  /** @internal */\n  private getRegisteredNode(klass: Klass<LexicalNode>): RegisteredNode {\n    const registeredNode = this._nodes.get(klass.getType());\n\n    if (registeredNode === undefined) {\n      invariant(\n        false,\n        'Node %s has not been registered. Ensure node has been passed to createEditor.',\n        klass.name,\n      );\n    }\n\n    return registeredNode;\n  }\n\n  /** @internal */\n  private resolveRegisteredNodeAfterReplacements(\n    registeredNode: RegisteredNode,\n  ): RegisteredNode {\n    while (registeredNode.replaceWithKlass) {\n      registeredNode = this.getRegisteredNode(registeredNode.replaceWithKlass);\n    }\n    return registeredNode;\n  }\n\n  /** @internal */\n  private initializeMutationListener(\n    listener: MutationListener,\n    klass: Klass<LexicalNode>,\n  ): void {\n    const prevEditorState = this._editorState;\n    const nodeMap = getCachedTypeToNodeMap(prevEditorState).get(\n      klass.getType(),\n    );\n    if (!nodeMap) {\n      return;\n    }\n    const nodeMutationMap = new Map<string, NodeMutation>();\n    for (const k of nodeMap.keys()) {\n      nodeMutationMap.set(k, 'created');\n    }\n    if (nodeMutationMap.size > 0) {\n      listener(nodeMutationMap, {\n        dirtyLeaves: new Set(),\n        prevEditorState,\n        updateTags: new Set(['registerMutationListener']),\n      });\n    }\n  }\n\n  /** @internal */\n  private registerNodeTransformToKlass<T extends LexicalNode>(\n    klass: Klass<T>,\n    listener: Transform<T>,\n  ): RegisteredNode {\n    const registeredNode = this.getRegisteredNode(klass);\n    registeredNode.transforms.add(listener as Transform<LexicalNode>);\n\n    return registeredNode;\n  }\n\n  /**\n   * Registers a listener that will run when a Lexical node of the provided class is\n   * marked dirty during an update. The listener will continue to run as long as the node\n   * is marked dirty. There are no guarantees around the order of transform execution!\n   *\n   * Watch out for infinite loops. See [Node Transforms](https://lexical.dev/docs/concepts/transforms)\n   * @param klass - The class of the node that you want to run transforms on.\n   * @param listener - The logic you want to run when the node is updated.\n   * @returns a teardown function that can be used to cleanup the listener.\n   */\n  registerNodeTransform<T extends LexicalNode>(\n    klass: Klass<T>,\n    listener: Transform<T>,\n  ): () => void {\n    const registeredNode = this.registerNodeTransformToKlass(klass, listener);\n    const registeredNodes = [registeredNode];\n\n    const replaceWithKlass = registeredNode.replaceWithKlass;\n    if (replaceWithKlass != null) {\n      const registeredReplaceWithNode = this.registerNodeTransformToKlass(\n        replaceWithKlass,\n        listener as Transform<LexicalNode>,\n      );\n      registeredNodes.push(registeredReplaceWithNode);\n    }\n\n    markAllNodesAsDirty(this, klass.getType());\n    return () => {\n      registeredNodes.forEach((node) =>\n        node.transforms.delete(listener as Transform<LexicalNode>),\n      );\n    };\n  }\n\n  /**\n   * Used to assert that a certain node is registered, usually by plugins to ensure nodes that they\n   * depend on have been registered.\n   * @returns True if the editor has registered the provided node type, false otherwise.\n   */\n  hasNode<T extends Klass<LexicalNode>>(node: T): boolean {\n    return this._nodes.has(node.getType());\n  }\n\n  /**\n   * Used to assert that certain nodes are registered, usually by plugins to ensure nodes that they\n   * depend on have been registered.\n   * @returns True if the editor has registered all of the provided node types, false otherwise.\n   */\n  hasNodes<T extends Klass<LexicalNode>>(nodes: Array<T>): boolean {\n    return nodes.every(this.hasNode.bind(this));\n  }\n\n  /**\n   * Dispatches a command of the specified type with the specified payload.\n   * This triggers all command listeners (set by {@link LexicalEditor.registerCommand})\n   * for this type, passing them the provided payload.\n   * @param type - the type of command listeners to trigger.\n   * @param payload - the data to pass as an argument to the command listeners.\n   */\n  dispatchCommand<TCommand extends LexicalCommand<unknown>>(\n    type: TCommand,\n    payload: CommandPayloadType<TCommand>,\n  ): boolean {\n    return dispatchCommand(this, type, payload);\n  }\n\n  /**\n   * Gets a map of all decorators in the editor.\n   * @returns A mapping of call decorator keys to their decorated content\n   */\n  getDecorators<T>(): Record<NodeKey, T> {\n    return this._decorators as Record<NodeKey, T>;\n  }\n\n  /**\n   *\n   * @returns the current root element of the editor. If you want to register\n   * an event listener, do it via {@link LexicalEditor.registerRootListener}, since\n   * this reference may not be stable.\n   */\n  getRootElement(): null | HTMLElement {\n    return this._rootElement;\n  }\n\n  /**\n   * Gets the key of the editor\n   * @returns The editor key\n   */\n  getKey(): string {\n    return this._key;\n  }\n\n  /**\n   * Imperatively set the root contenteditable element that Lexical listens\n   * for events on.\n   */\n  setRootElement(nextRootElement: null | HTMLElement): void {\n    const prevRootElement = this._rootElement;\n\n    if (nextRootElement !== prevRootElement) {\n      const classNames = getCachedClassNameArray(this._config.theme, 'root');\n      const pendingEditorState = this._pendingEditorState || this._editorState;\n      this._rootElement = nextRootElement;\n      resetEditor(this, prevRootElement, nextRootElement, pendingEditorState);\n\n      if (prevRootElement !== null) {\n        // TODO: remove this flag once we no longer use UEv2 internally\n        if (!this._config.disableEvents) {\n          removeRootElementEvents(prevRootElement);\n        }\n        if (classNames != null) {\n          prevRootElement.classList.remove(...classNames);\n        }\n      }\n\n      if (nextRootElement !== null) {\n        const windowObj = getDefaultView(nextRootElement);\n        const style = nextRootElement.style;\n        style.userSelect = 'text';\n        style.whiteSpace = 'pre-wrap';\n        style.wordBreak = 'break-word';\n        nextRootElement.setAttribute('data-lexical-editor', 'true');\n        this._window = windowObj;\n        this._dirtyType = FULL_RECONCILE;\n        initMutationObserver(this);\n\n        this._updateTags.add('history-merge');\n\n        $commitPendingUpdates(this);\n\n        // TODO: remove this flag once we no longer use UEv2 internally\n        if (!this._config.disableEvents) {\n          addRootElementEvents(nextRootElement, this);\n        }\n        if (classNames != null) {\n          nextRootElement.classList.add(...classNames);\n        }\n      } else {\n        // If content editable is unmounted we'll reset editor state back to original\n        // (or pending) editor state since there will be no reconciliation\n        this._editorState = pendingEditorState;\n        this._pendingEditorState = null;\n        this._window = null;\n      }\n\n      triggerListeners('root', this, false, nextRootElement, prevRootElement);\n    }\n  }\n\n  /**\n   * Gets the underlying HTMLElement associated with the LexicalNode for the given key.\n   * @returns the HTMLElement rendered by the LexicalNode associated with the key.\n   * @param key - the key of the LexicalNode.\n   */\n  getElementByKey(key: NodeKey): HTMLElement | null {\n    return this._keyToDOMMap.get(key) || null;\n  }\n\n  /**\n   * Gets the active editor state.\n   * @returns The editor state\n   */\n  getEditorState(): EditorState {\n    return this._editorState;\n  }\n\n  /**\n   * Imperatively set the EditorState. Triggers reconciliation like an update.\n   * @param editorState - the state to set the editor\n   * @param options - options for the update.\n   */\n  setEditorState(editorState: EditorState, options?: EditorSetOptions): void {\n    if (editorState.isEmpty()) {\n      invariant(\n        false,\n        \"setEditorState: the editor state is empty. Ensure the editor state's root node never becomes empty.\",\n      );\n    }\n\n    $flushRootMutations(this);\n    const pendingEditorState = this._pendingEditorState;\n    const tags = this._updateTags;\n    const tag = options !== undefined ? options.tag : null;\n\n    if (pendingEditorState !== null && !pendingEditorState.isEmpty()) {\n      if (tag != null) {\n        tags.add(tag);\n      }\n\n      $commitPendingUpdates(this);\n    }\n\n    this._pendingEditorState = editorState;\n    this._dirtyType = FULL_RECONCILE;\n    this._dirtyElements.set('root', false);\n    this._compositionKey = null;\n\n    if (tag != null) {\n      tags.add(tag);\n    }\n\n    $commitPendingUpdates(this);\n  }\n\n  /**\n   * Parses a SerializedEditorState (usually produced by {@link EditorState.toJSON}) and returns\n   * and EditorState object that can be, for example, passed to {@link LexicalEditor.setEditorState}. Typically,\n   * deserialization from JSON stored in a database uses this method.\n   * @param maybeStringifiedEditorState\n   * @param updateFn\n   * @returns\n   */\n  parseEditorState(\n    maybeStringifiedEditorState: string | SerializedEditorState,\n    updateFn?: () => void,\n  ): EditorState {\n    const serializedEditorState =\n      typeof maybeStringifiedEditorState === 'string'\n        ? JSON.parse(maybeStringifiedEditorState)\n        : maybeStringifiedEditorState;\n    return parseEditorState(serializedEditorState, this, updateFn);\n  }\n\n  /**\n   * Executes a read of the editor's state, with the\n   * editor context available (useful for exporting and read-only DOM\n   * operations). Much like update, but prevents any mutation of the\n   * editor's state. Any pending updates will be flushed immediately before\n   * the read.\n   * @param callbackFn - A function that has access to read-only editor state.\n   */\n  read<T>(callbackFn: () => T): T {\n    $commitPendingUpdates(this);\n    return this.getEditorState().read(callbackFn, {editor: this});\n  }\n\n  /**\n   * Executes an update to the editor state. The updateFn callback is the ONLY place\n   * where Lexical editor state can be safely mutated.\n   * @param updateFn - A function that has access to writable editor state.\n   * @param options - A bag of options to control the behavior of the update.\n   * @param options.onUpdate - A function to run once the update is complete.\n   * Useful for synchronizing updates in some cases.\n   * @param options.skipTransforms - Setting this to true will suppress all node\n   * transforms for this update cycle.\n   * @param options.tag - A tag to identify this update, in an update listener, for instance.\n   * Some tags are reserved by the core and control update behavior in different ways.\n   * @param options.discrete - If true, prevents this update from being batched, forcing it to\n   * run synchronously.\n   */\n  update(updateFn: () => void, options?: EditorUpdateOptions): void {\n    updateEditor(this, updateFn, options);\n  }\n\n  /**\n   * Helper to run the update and commitUpdates methods in a single call.\n   */\n  updateAndCommit(updateFn: () => void, options?: EditorUpdateOptions): void {\n    this.update(updateFn, options);\n    this.commitUpdates();\n  }\n\n  /**\n   * Focuses the editor\n   * @param callbackFn - A function to run after the editor is focused.\n   * @param options - A bag of options\n   * @param options.defaultSelection - Where to move selection when the editor is\n   * focused. Can be rootStart, rootEnd, or undefined. Defaults to rootEnd.\n   */\n  focus(callbackFn?: () => void, options: EditorFocusOptions = {}): void {\n    const rootElement = this._rootElement;\n\n    if (rootElement !== null) {\n      // This ensures that iOS does not trigger caps lock upon focus\n      rootElement.setAttribute('autocapitalize', 'off');\n      updateEditor(\n        this,\n        () => {\n          const selection = $getSelection();\n          const root = $getRoot();\n\n          if (selection !== null) {\n            // Marking the selection dirty will force the selection back to it\n            selection.dirty = true;\n          } else if (root.getChildrenSize() !== 0) {\n            if (options.defaultSelection === 'rootStart') {\n              root.selectStart();\n            } else {\n              root.selectEnd();\n            }\n          }\n        },\n        {\n          onUpdate: () => {\n            rootElement.removeAttribute('autocapitalize');\n            if (callbackFn) {\n              callbackFn();\n            }\n          },\n          tag: 'focus',\n        },\n      );\n      // In the case where onUpdate doesn't fire (due to the focus update not\n      // occuring).\n      if (this._pendingEditorState === null) {\n        rootElement.removeAttribute('autocapitalize');\n      }\n    }\n  }\n\n  /**\n   * Commits any currently pending updates scheduled for the editor.\n   */\n  commitUpdates(): void {\n    $commitPendingUpdates(this);\n  }\n\n  /**\n   * Removes focus from the editor.\n   */\n  blur(): void {\n    const rootElement = this._rootElement;\n\n    if (rootElement !== null) {\n      rootElement.blur();\n    }\n\n    const domSelection = getDOMSelection(this._window);\n\n    if (domSelection !== null) {\n      domSelection.removeAllRanges();\n    }\n  }\n  /**\n   * Returns true if the editor is editable, false otherwise.\n   * @returns True if the editor is editable, false otherwise.\n   */\n  isEditable(): boolean {\n    return this._editable;\n  }\n  /**\n   * Sets the editable property of the editor. When false, the\n   * editor will not listen for user events on the underling contenteditable.\n   * @param editable - the value to set the editable mode to.\n   */\n  setEditable(editable: boolean): void {\n    if (this._editable !== editable) {\n      this._editable = editable;\n      triggerListeners('editable', this, true, editable);\n    }\n  }\n  /**\n   * Returns a JSON-serializable javascript object NOT a JSON string.\n   * You still must call JSON.stringify (or something else) to turn the\n   * state into a string you can transfer over the wire and store in a database.\n   *\n   * See {@link LexicalNode.exportJSON}\n   *\n   * @returns A JSON-serializable javascript object\n   */\n  toJSON(): SerializedEditor {\n    return {\n      editorState: this._editorState.toJSON(),\n    };\n  }\n}\n\nLexicalEditor.version = '0.17.1';\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/LexicalEditorState.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {LexicalEditor} from './LexicalEditor';\nimport type {LexicalNode, NodeMap, SerializedLexicalNode} from './LexicalNode';\nimport type {BaseSelection} from './LexicalSelection';\nimport type {SerializedElementNode} from './nodes/LexicalElementNode';\nimport type {SerializedRootNode} from './nodes/LexicalRootNode';\n\nimport invariant from 'lexical/shared/invariant';\n\nimport {readEditorState} from './LexicalUpdates';\nimport {$getRoot} from './LexicalUtils';\nimport {$isElementNode} from './nodes/LexicalElementNode';\nimport {$createRootNode} from './nodes/LexicalRootNode';\n\nexport interface SerializedEditorState<\n  T extends SerializedLexicalNode = SerializedLexicalNode,\n> {\n  root: SerializedRootNode<T>;\n}\n\nexport function editorStateHasDirtySelection(\n  editorState: EditorState,\n  editor: LexicalEditor,\n): boolean {\n  const currentSelection = editor.getEditorState()._selection;\n\n  const pendingSelection = editorState._selection;\n\n  // Check if we need to update because of changes in selection\n  if (pendingSelection !== null) {\n    if (pendingSelection.dirty || !pendingSelection.is(currentSelection)) {\n      return true;\n    }\n  } else if (currentSelection !== null) {\n    return true;\n  }\n\n  return false;\n}\n\nexport function cloneEditorState(current: EditorState): EditorState {\n  return new EditorState(new Map(current._nodeMap));\n}\n\nexport function createEmptyEditorState(): EditorState {\n  return new EditorState(new Map([['root', $createRootNode()]]));\n}\n\nfunction exportNodeToJSON<SerializedNode extends SerializedLexicalNode>(\n  node: LexicalNode,\n): SerializedNode {\n  const serializedNode = node.exportJSON();\n  const nodeClass = node.constructor;\n\n  if (serializedNode.type !== nodeClass.getType()) {\n    invariant(\n      false,\n      'LexicalNode: Node %s does not match the serialized type. Check if .exportJSON() is implemented and it is returning the correct type.',\n      nodeClass.name,\n    );\n  }\n\n  if ($isElementNode(node)) {\n    const serializedChildren = (serializedNode as SerializedElementNode)\n      .children;\n    if (!Array.isArray(serializedChildren)) {\n      invariant(\n        false,\n        'LexicalNode: Node %s is an element but .exportJSON() does not have a children array.',\n        nodeClass.name,\n      );\n    }\n\n    const children = node.getChildren();\n\n    for (let i = 0; i < children.length; i++) {\n      const child = children[i];\n      const serializedChildNode = exportNodeToJSON(child);\n      serializedChildren.push(serializedChildNode);\n    }\n  }\n\n  // @ts-expect-error\n  return serializedNode;\n}\n\nexport interface EditorStateReadOptions {\n  editor?: LexicalEditor | null;\n}\n\nexport class EditorState {\n  _nodeMap: NodeMap;\n  _selection: null | BaseSelection;\n  _flushSync: boolean;\n  _readOnly: boolean;\n\n  constructor(nodeMap: NodeMap, selection?: null | BaseSelection) {\n    this._nodeMap = nodeMap;\n    this._selection = selection || null;\n    this._flushSync = false;\n    this._readOnly = false;\n  }\n\n  isEmpty(): boolean {\n    return this._nodeMap.size === 1 && this._selection === null;\n  }\n\n  read<V>(callbackFn: () => V, options?: EditorStateReadOptions): V {\n    return readEditorState(\n      (options && options.editor) || null,\n      this,\n      callbackFn,\n    );\n  }\n\n  clone(selection?: null | BaseSelection): EditorState {\n    const editorState = new EditorState(\n      this._nodeMap,\n      selection === undefined ? this._selection : selection,\n    );\n    editorState._readOnly = true;\n\n    return editorState;\n  }\n  toJSON(): SerializedEditorState {\n    return readEditorState(null, this, () => ({\n      root: exportNodeToJSON($getRoot()),\n    }));\n  }\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/LexicalEvents.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {LexicalEditor} from './LexicalEditor';\nimport type {NodeKey} from './LexicalNode';\nimport type {ElementNode} from './nodes/LexicalElementNode';\nimport type {TextNode} from './nodes/LexicalTextNode';\n\nimport {\n  CAN_USE_BEFORE_INPUT,\n  IS_ANDROID_CHROME,\n  IS_APPLE_WEBKIT,\n  IS_FIREFOX,\n  IS_IOS,\n  IS_SAFARI,\n} from 'lexical/shared/environment';\nimport invariant from 'lexical/shared/invariant';\n\nimport {\n  $getPreviousSelection,\n  $getRoot,\n  $getSelection,\n  $isElementNode,\n  $isNodeSelection,\n  $isRangeSelection,\n  $isRootNode,\n  $isTextNode,\n  $setCompositionKey,\n  BLUR_COMMAND,\n  CLICK_COMMAND,\n  CONTROLLED_TEXT_INSERTION_COMMAND,\n  COPY_COMMAND,\n  CUT_COMMAND,\n  DELETE_CHARACTER_COMMAND,\n  DELETE_LINE_COMMAND,\n  DELETE_WORD_COMMAND,\n  DRAGEND_COMMAND,\n  DRAGOVER_COMMAND,\n  DRAGSTART_COMMAND,\n  DROP_COMMAND,\n  FOCUS_COMMAND,\n  FORMAT_TEXT_COMMAND,\n  INSERT_LINE_BREAK_COMMAND,\n  INSERT_PARAGRAPH_COMMAND,\n  KEY_ARROW_DOWN_COMMAND,\n  KEY_ARROW_LEFT_COMMAND,\n  KEY_ARROW_RIGHT_COMMAND,\n  KEY_ARROW_UP_COMMAND,\n  KEY_BACKSPACE_COMMAND,\n  KEY_DELETE_COMMAND,\n  KEY_DOWN_COMMAND,\n  KEY_ENTER_COMMAND,\n  KEY_ESCAPE_COMMAND,\n  KEY_SPACE_COMMAND,\n  KEY_TAB_COMMAND,\n  MOVE_TO_END,\n  MOVE_TO_START,\n  ParagraphNode,\n  PASTE_COMMAND,\n  REDO_COMMAND,\n  REMOVE_TEXT_COMMAND,\n  SELECTION_CHANGE_COMMAND,\n  UNDO_COMMAND,\n} from '.';\nimport {KEY_AT_COMMAND, KEY_MODIFIER_COMMAND, SELECT_ALL_COMMAND} from './LexicalCommands';\nimport {\n  COMPOSITION_START_CHAR,\n  DOM_ELEMENT_TYPE,\n  DOM_TEXT_TYPE,\n  DOUBLE_LINE_BREAK,\n  IS_ALL_FORMATTING,\n} from './LexicalConstants';\nimport {\n  $internalCreateRangeSelection,\n  RangeSelection,\n} from './LexicalSelection';\nimport {getActiveEditor, updateEditor} from './LexicalUpdates';\nimport {\n  $flushMutations,\n  $getNodeByKey,\n  $isSelectionCapturedInDecorator,\n  $isTokenOrSegmented,\n  $setSelection,\n  $shouldInsertTextAfterOrBeforeTextNode,\n  $updateSelectedTextFromDOM,\n  $updateTextNodeFromDOMContent,\n  dispatchCommand,\n  doesContainGrapheme,\n  getAnchorTextFromDOM,\n  getDOMSelection,\n  getDOMTextNode,\n  getEditorPropertyFromDOMNode,\n  getEditorsToPropagate,\n  getNearestEditorFromDOMNode,\n  getWindow, isAt,\n  isBackspace,\n  isBold,\n  isCopy,\n  isCut,\n  isDelete,\n  isDeleteBackward,\n  isDeleteForward,\n  isDeleteLineBackward,\n  isDeleteLineForward,\n  isDeleteWordBackward,\n  isDeleteWordForward,\n  isEscape,\n  isFirefoxClipboardEvents,\n  isItalic,\n  isLexicalEditor,\n  isLineBreak,\n  isModifier,\n  isMoveBackward,\n  isMoveDown,\n  isMoveForward,\n  isMoveToEnd,\n  isMoveToStart,\n  isMoveUp,\n  isOpenLineBreak,\n  isParagraph,\n  isRedo,\n  isSelectAll,\n  isSelectionWithinEditor,\n  isSpace,\n  isTab,\n  isUnderline,\n  isUndo,\n} from './LexicalUtils';\n\ntype RootElementRemoveHandles = Array<() => void>;\ntype RootElementEvents = Array<\n  [\n    string,\n    Record<string, unknown> | ((event: Event, editor: LexicalEditor) => void),\n  ]\n>;\nconst PASS_THROUGH_COMMAND = Object.freeze({});\nconst ANDROID_COMPOSITION_LATENCY = 30;\nconst rootElementEvents: RootElementEvents = [\n  ['keydown', onKeyDown],\n  ['pointerdown', onPointerDown],\n  ['compositionstart', onCompositionStart],\n  ['compositionend', onCompositionEnd],\n  ['input', onInput],\n  ['click', onClick],\n  ['cut', PASS_THROUGH_COMMAND],\n  ['copy', PASS_THROUGH_COMMAND],\n  ['dragstart', PASS_THROUGH_COMMAND],\n  ['dragover', PASS_THROUGH_COMMAND],\n  ['dragend', PASS_THROUGH_COMMAND],\n  ['paste', PASS_THROUGH_COMMAND],\n  ['focus', PASS_THROUGH_COMMAND],\n  ['blur', PASS_THROUGH_COMMAND],\n  ['drop', PASS_THROUGH_COMMAND],\n];\n\nif (CAN_USE_BEFORE_INPUT) {\n  rootElementEvents.push([\n    'beforeinput',\n    (event, editor) => onBeforeInput(event as InputEvent, editor),\n  ]);\n}\n\nlet lastKeyDownTimeStamp = 0;\nlet lastKeyCode: null | string = null;\nlet lastBeforeInputInsertTextTimeStamp = 0;\nlet unprocessedBeforeInputData: null | string = null;\nconst rootElementsRegistered = new WeakMap<Document, number>();\nlet isSelectionChangeFromDOMUpdate = false;\nlet isSelectionChangeFromMouseDown = false;\nlet isInsertLineBreak = false;\nlet isFirefoxEndingComposition = false;\nlet collapsedSelectionFormat: [number, string, number, NodeKey, number] = [\n  0,\n  '',\n  0,\n  'root',\n  0,\n];\n\n// This function is used to determine if Lexical should attempt to override\n// the default browser behavior for insertion of text and use its own internal\n// heuristics. This is an extremely important function, and makes much of Lexical\n// work as intended between different browsers and across word, line and character\n// boundary/formats. It also is important for text replacement, node schemas and\n// composition mechanics.\n\nfunction $shouldPreventDefaultAndInsertText(\n  selection: RangeSelection,\n  domTargetRange: null | StaticRange,\n  text: string,\n  timeStamp: number,\n  isBeforeInput: boolean,\n): boolean {\n  const anchor = selection.anchor;\n  const focus = selection.focus;\n  const anchorNode = anchor.getNode();\n  const editor = getActiveEditor();\n  const domSelection = getDOMSelection(editor._window);\n  const domAnchorNode = domSelection !== null ? domSelection.anchorNode : null;\n  const anchorKey = anchor.key;\n  const backingAnchorElement = editor.getElementByKey(anchorKey);\n  const textLength = text.length;\n\n  return (\n    anchorKey !== focus.key ||\n    // If we're working with a non-text node.\n    !$isTextNode(anchorNode) ||\n    // If we are replacing a range with a single character or grapheme, and not composing.\n    (((!isBeforeInput &&\n      (!CAN_USE_BEFORE_INPUT ||\n        // We check to see if there has been\n        // a recent beforeinput event for \"textInput\". If there has been one in the last\n        // 50ms then we proceed as normal. However, if there is not, then this is likely\n        // a dangling `input` event caused by execCommand('insertText').\n        lastBeforeInputInsertTextTimeStamp < timeStamp + 50)) ||\n      (anchorNode.isDirty() && textLength < 2) ||\n      doesContainGrapheme(text)) &&\n      anchor.offset !== focus.offset &&\n      !anchorNode.isComposing()) ||\n    // Any non standard text node.\n    $isTokenOrSegmented(anchorNode) ||\n    // If the text length is more than a single character and we're either\n    // dealing with this in \"beforeinput\" or where the node has already recently\n    // been changed (thus is dirty).\n    (anchorNode.isDirty() && textLength > 1) ||\n    // If the DOM selection element is not the same as the backing node during beforeinput.\n    ((isBeforeInput || !CAN_USE_BEFORE_INPUT) &&\n      backingAnchorElement !== null &&\n      !anchorNode.isComposing() &&\n      domAnchorNode !== getDOMTextNode(backingAnchorElement)) ||\n    // If TargetRange is not the same as the DOM selection; browser trying to edit random parts\n    // of the editor.\n    (domSelection !== null &&\n      domTargetRange !== null &&\n      (!domTargetRange.collapsed ||\n        domTargetRange.startContainer !== domSelection.anchorNode ||\n        domTargetRange.startOffset !== domSelection.anchorOffset)) ||\n    // Check if we're changing from bold to italics, or some other format.\n    anchorNode.getFormat() !== selection.format ||\n    anchorNode.getStyle() !== selection.style ||\n    // One last set of heuristics to check against.\n    $shouldInsertTextAfterOrBeforeTextNode(selection, anchorNode)\n  );\n}\n\nfunction shouldSkipSelectionChange(\n  domNode: null | Node,\n  offset: number,\n): boolean {\n  return (\n    domNode !== null &&\n    domNode.nodeValue !== null &&\n    domNode.nodeType === DOM_TEXT_TYPE &&\n    offset !== 0 &&\n    offset !== domNode.nodeValue.length\n  );\n}\n\nfunction onSelectionChange(\n  domSelection: Selection,\n  editor: LexicalEditor,\n  isActive: boolean,\n): void {\n  const {\n    anchorNode: anchorDOM,\n    anchorOffset,\n    focusNode: focusDOM,\n    focusOffset,\n  } = domSelection;\n  if (isSelectionChangeFromDOMUpdate) {\n    isSelectionChangeFromDOMUpdate = false;\n\n    // If native DOM selection is on a DOM element, then\n    // we should continue as usual, as Lexical's selection\n    // may have normalized to a better child. If the DOM\n    // element is a text node, we can safely apply this\n    // optimization and skip the selection change entirely.\n    // We also need to check if the offset is at the boundary,\n    // because in this case, we might need to normalize to a\n    // sibling instead.\n    if (\n      shouldSkipSelectionChange(anchorDOM, anchorOffset) &&\n      shouldSkipSelectionChange(focusDOM, focusOffset)\n    ) {\n      return;\n    }\n  }\n  updateEditor(editor, () => {\n    // Non-active editor don't need any extra logic for selection, it only needs update\n    // to reconcile selection (set it to null) to ensure that only one editor has non-null selection.\n    if (!isActive) {\n      $setSelection(null);\n      return;\n    }\n\n    if (!isSelectionWithinEditor(editor, anchorDOM, focusDOM)) {\n      return;\n    }\n\n    const selection = $getSelection();\n\n    // Update the selection format\n    if ($isRangeSelection(selection)) {\n      const anchor = selection.anchor;\n      const anchorNode = anchor.getNode();\n\n      if (selection.isCollapsed()) {\n        // Badly interpreted range selection when collapsed - #1482\n        if (\n          domSelection.type === 'Range' &&\n          domSelection.anchorNode === domSelection.focusNode\n        ) {\n          selection.dirty = true;\n        }\n\n        // If we have marked a collapsed selection format, and we're\n        // within the given time range – then attempt to use that format\n        // instead of getting the format from the anchor node.\n        const windowEvent = getWindow(editor).event;\n        const currentTimeStamp = windowEvent\n          ? windowEvent.timeStamp\n          : performance.now();\n        const [lastFormat, lastStyle, lastOffset, lastKey, timeStamp] =\n          collapsedSelectionFormat;\n\n        const root = $getRoot();\n        const isRootTextContentEmpty =\n          editor.isComposing() === false && root.getTextContent() === '';\n\n        if (\n          currentTimeStamp < timeStamp + 200 &&\n          anchor.offset === lastOffset &&\n          anchor.key === lastKey\n        ) {\n          selection.format = lastFormat;\n          selection.style = lastStyle;\n        } else {\n          if (anchor.type === 'text') {\n            invariant(\n              $isTextNode(anchorNode),\n              'Point.getNode() must return TextNode when type is text',\n            );\n            selection.format = anchorNode.getFormat();\n            selection.style = anchorNode.getStyle();\n          } else if (anchor.type === 'element' && !isRootTextContentEmpty) {\n            const lastNode = anchor.getNode();\n            selection.style = '';\n            if (\n              lastNode instanceof ParagraphNode &&\n              lastNode.getChildrenSize() === 0\n            ) {\n              selection.format = lastNode.getTextFormat();\n              selection.style = lastNode.getTextStyle();\n            } else {\n              selection.format = 0;\n            }\n          }\n        }\n      } else {\n        const anchorKey = anchor.key;\n        const focus = selection.focus;\n        const focusKey = focus.key;\n        const nodes = selection.getNodes();\n        const nodesLength = nodes.length;\n        const isBackward = selection.isBackward();\n        const startOffset = isBackward ? focusOffset : anchorOffset;\n        const endOffset = isBackward ? anchorOffset : focusOffset;\n        const startKey = isBackward ? focusKey : anchorKey;\n        const endKey = isBackward ? anchorKey : focusKey;\n        let combinedFormat = IS_ALL_FORMATTING;\n        let hasTextNodes = false;\n        for (let i = 0; i < nodesLength; i++) {\n          const node = nodes[i];\n          const textContentSize = node.getTextContentSize();\n          if (\n            $isTextNode(node) &&\n            textContentSize !== 0 &&\n            // Exclude empty text nodes at boundaries resulting from user's selection\n            !(\n              (i === 0 &&\n                node.__key === startKey &&\n                startOffset === textContentSize) ||\n              (i === nodesLength - 1 &&\n                node.__key === endKey &&\n                endOffset === 0)\n            )\n          ) {\n            // TODO: what about style?\n            hasTextNodes = true;\n            combinedFormat &= node.getFormat();\n            if (combinedFormat === 0) {\n              break;\n            }\n          }\n        }\n\n        selection.format = hasTextNodes ? combinedFormat : 0;\n      }\n    }\n\n    dispatchCommand(editor, SELECTION_CHANGE_COMMAND, undefined);\n  });\n}\n\n// This is a work-around is mainly Chrome specific bug where if you select\n// the contents of an empty block, you cannot easily unselect anything.\n// This results in a tiny selection box that looks buggy/broken. This can\n// also help other browsers when selection might \"appear\" lost, when it\n// really isn't.\nfunction onClick(event: PointerEvent, editor: LexicalEditor): void {\n  updateEditor(editor, () => {\n    const selection = $getSelection();\n    const domSelection = getDOMSelection(editor._window);\n    const lastSelection = $getPreviousSelection();\n\n    if (domSelection) {\n      if ($isRangeSelection(selection)) {\n        const anchor = selection.anchor;\n        const anchorNode = anchor.getNode();\n\n        if (\n          anchor.type === 'element' &&\n          anchor.offset === 0 &&\n          selection.isCollapsed() &&\n          !$isRootNode(anchorNode) &&\n          $getRoot().getChildrenSize() === 1 &&\n          anchorNode.getTopLevelElementOrThrow().isEmpty() &&\n          lastSelection !== null &&\n          selection.is(lastSelection)\n        ) {\n          domSelection.removeAllRanges();\n          selection.dirty = true;\n        } else if (event.detail === 3 && !selection.isCollapsed()) {\n          // Tripple click causing selection to overflow into the nearest element. In that\n          // case visually it looks like a single element content is selected, focus node\n          // is actually at the beginning of the next element (if present) and any manipulations\n          // with selection (formatting) are affecting second element as well\n          const focus = selection.focus;\n          const focusNode = focus.getNode();\n          if (anchorNode !== focusNode) {\n            if ($isElementNode(anchorNode)) {\n              anchorNode.select(0);\n            } else {\n              anchorNode.getParentOrThrow().select(0);\n            }\n          }\n        }\n      } else if (event.pointerType === 'touch') {\n        // This is used to update the selection on touch devices when the user clicks on text after a\n        // node selection. See isSelectionChangeFromMouseDown for the inverse\n        const domAnchorNode = domSelection.anchorNode;\n        if (domAnchorNode !== null) {\n          const nodeType = domAnchorNode.nodeType;\n          // If the user is attempting to click selection back onto text, then\n          // we should attempt create a range selection.\n          // When we click on an empty paragraph node or the end of a paragraph that ends\n          // with an image/poll, the nodeType will be ELEMENT_NODE\n          if (nodeType === DOM_ELEMENT_TYPE || nodeType === DOM_TEXT_TYPE) {\n            const newSelection = $internalCreateRangeSelection(\n              lastSelection,\n              domSelection,\n              editor,\n              event,\n            );\n            $setSelection(newSelection);\n          }\n        }\n      }\n    }\n\n    dispatchCommand(editor, CLICK_COMMAND, event);\n  });\n}\n\nfunction onPointerDown(event: PointerEvent, editor: LexicalEditor) {\n  // TODO implement text drag & drop\n  const target = event.target;\n  const pointerType = event.pointerType;\n  if (target instanceof Node && pointerType !== 'touch') {\n    updateEditor(editor, () => {\n      // Drag & drop should not recompute selection until mouse up; otherwise the initially\n      // selected content is lost.\n      if (!$isSelectionCapturedInDecorator(target)) {\n        isSelectionChangeFromMouseDown = true;\n      }\n    });\n  }\n}\n\nfunction getTargetRange(event: InputEvent): null | StaticRange {\n  if (!event.getTargetRanges) {\n    return null;\n  }\n  const targetRanges = event.getTargetRanges();\n  if (targetRanges.length === 0) {\n    return null;\n  }\n  return targetRanges[0];\n}\n\nfunction $canRemoveText(\n  anchorNode: TextNode | ElementNode,\n  focusNode: TextNode | ElementNode,\n): boolean {\n  return (\n    anchorNode !== focusNode ||\n    $isElementNode(anchorNode) ||\n    $isElementNode(focusNode) ||\n    !anchorNode.isToken() ||\n    !focusNode.isToken()\n  );\n}\n\nfunction isPossiblyAndroidKeyPress(timeStamp: number): boolean {\n  return (\n    lastKeyCode === 'MediaLast' &&\n    timeStamp < lastKeyDownTimeStamp + ANDROID_COMPOSITION_LATENCY\n  );\n}\n\nfunction onBeforeInput(event: InputEvent, editor: LexicalEditor): void {\n  const inputType = event.inputType;\n  const targetRange = getTargetRange(event);\n\n  // We let the browser do its own thing for composition.\n  if (\n    inputType === 'deleteCompositionText' ||\n    // If we're pasting in FF, we shouldn't get this event\n    // as the `paste` event should have triggered, unless the\n    // user has dom.event.clipboardevents.enabled disabled in\n    // about:config. In that case, we need to process the\n    // pasted content in the DOM mutation phase.\n    (IS_FIREFOX && isFirefoxClipboardEvents(editor))\n  ) {\n    return;\n  } else if (inputType === 'insertCompositionText') {\n    return;\n  }\n\n  updateEditor(editor, () => {\n    const selection = $getSelection();\n\n    if (inputType === 'deleteContentBackward') {\n      if (selection === null) {\n        // Use previous selection\n        const prevSelection = $getPreviousSelection();\n\n        if (!$isRangeSelection(prevSelection)) {\n          return;\n        }\n\n        $setSelection(prevSelection.clone());\n      }\n\n      if ($isRangeSelection(selection)) {\n        const isSelectionAnchorSameAsFocus =\n          selection.anchor.key === selection.focus.key;\n\n        if (\n          isPossiblyAndroidKeyPress(event.timeStamp) &&\n          editor.isComposing() &&\n          isSelectionAnchorSameAsFocus\n        ) {\n          $setCompositionKey(null);\n          lastKeyDownTimeStamp = 0;\n          // Fixes an Android bug where selection flickers when backspacing\n          setTimeout(() => {\n            updateEditor(editor, () => {\n              $setCompositionKey(null);\n            });\n          }, ANDROID_COMPOSITION_LATENCY);\n          if ($isRangeSelection(selection)) {\n            const anchorNode = selection.anchor.getNode();\n            anchorNode.markDirty();\n            invariant(\n              $isTextNode(anchorNode),\n              'Anchor node must be a TextNode',\n            );\n            selection.style = anchorNode.getStyle();\n          }\n        } else {\n          $setCompositionKey(null);\n          event.preventDefault();\n          // Chromium Android at the moment seems to ignore the preventDefault\n          // on 'deleteContentBackward' and still deletes the content. Which leads\n          // to multiple deletions. So we let the browser handle the deletion in this case.\n          const selectedNodeText = selection.anchor.getNode().getTextContent();\n          const hasSelectedAllTextInNode =\n            selection.anchor.offset === 0 &&\n            selection.focus.offset === selectedNodeText.length;\n          const shouldLetBrowserHandleDelete =\n            IS_ANDROID_CHROME &&\n            isSelectionAnchorSameAsFocus &&\n            !hasSelectedAllTextInNode;\n          if (!shouldLetBrowserHandleDelete) {\n            dispatchCommand(editor, DELETE_CHARACTER_COMMAND, true);\n          }\n        }\n        return;\n      }\n    }\n\n    if (!$isRangeSelection(selection)) {\n      return;\n    }\n\n    const data = event.data;\n\n    // This represents the case when two beforeinput events are triggered at the same time (without a\n    // full event loop ending at input). This happens with MacOS with the default keyboard settings,\n    // a combination of autocorrection + autocapitalization.\n    // Having Lexical run everything in controlled mode would fix the issue without additional code\n    // but this would kill the massive performance win from the most common typing event.\n    // Alternatively, when this happens we can prematurely update our EditorState based on the DOM\n    // content, a job that would usually be the input event's responsibility.\n    if (unprocessedBeforeInputData !== null) {\n      $updateSelectedTextFromDOM(false, editor, unprocessedBeforeInputData);\n    }\n\n    if (\n      (!selection.dirty || unprocessedBeforeInputData !== null) &&\n      selection.isCollapsed() &&\n      !$isRootNode(selection.anchor.getNode()) &&\n      targetRange !== null\n    ) {\n      selection.applyDOMRange(targetRange);\n    }\n\n    unprocessedBeforeInputData = null;\n\n    const anchor = selection.anchor;\n    const focus = selection.focus;\n    const anchorNode = anchor.getNode();\n    const focusNode = focus.getNode();\n\n    if (inputType === 'insertText' || inputType === 'insertTranspose') {\n      if (data === '\\n') {\n        event.preventDefault();\n        dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, false);\n      } else if (data === DOUBLE_LINE_BREAK) {\n        event.preventDefault();\n        dispatchCommand(editor, INSERT_PARAGRAPH_COMMAND, undefined);\n      } else if (data == null && event.dataTransfer) {\n        // Gets around a Safari text replacement bug.\n        const text = event.dataTransfer.getData('text/plain');\n        event.preventDefault();\n        selection.insertRawText(text);\n      } else if (\n        data != null &&\n        $shouldPreventDefaultAndInsertText(\n          selection,\n          targetRange,\n          data,\n          event.timeStamp,\n          true,\n        )\n      ) {\n        event.preventDefault();\n        dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, data);\n      } else {\n        unprocessedBeforeInputData = data;\n      }\n      lastBeforeInputInsertTextTimeStamp = event.timeStamp;\n      return;\n    }\n\n    // Prevent the browser from carrying out\n    // the input event, so we can control the\n    // output.\n    event.preventDefault();\n\n    switch (inputType) {\n      case 'insertFromYank':\n      case 'insertFromDrop':\n      case 'insertReplacementText': {\n        dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, event);\n        break;\n      }\n\n      case 'insertFromComposition': {\n        // This is the end of composition\n        $setCompositionKey(null);\n        dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, event);\n        break;\n      }\n\n      case 'insertLineBreak': {\n        // Used for Android\n        $setCompositionKey(null);\n        dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, false);\n        break;\n      }\n\n      case 'insertParagraph': {\n        // Used for Android\n        $setCompositionKey(null);\n\n        // Safari does not provide the type \"insertLineBreak\".\n        // So instead, we need to infer it from the keyboard event.\n        // We do not apply this logic to iOS to allow newline auto-capitalization\n        // work without creating linebreaks when pressing Enter\n        if (isInsertLineBreak && !IS_IOS) {\n          isInsertLineBreak = false;\n          dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, false);\n        } else {\n          dispatchCommand(editor, INSERT_PARAGRAPH_COMMAND, undefined);\n        }\n\n        break;\n      }\n\n      case 'insertFromPaste':\n      case 'insertFromPasteAsQuotation': {\n        dispatchCommand(editor, PASTE_COMMAND, event);\n        break;\n      }\n\n      case 'deleteByComposition': {\n        if ($canRemoveText(anchorNode, focusNode)) {\n          dispatchCommand(editor, REMOVE_TEXT_COMMAND, event);\n        }\n\n        break;\n      }\n\n      case 'deleteByDrag':\n      case 'deleteByCut': {\n        dispatchCommand(editor, REMOVE_TEXT_COMMAND, event);\n        break;\n      }\n\n      case 'deleteContent': {\n        dispatchCommand(editor, DELETE_CHARACTER_COMMAND, false);\n        break;\n      }\n\n      case 'deleteWordBackward': {\n        dispatchCommand(editor, DELETE_WORD_COMMAND, true);\n        break;\n      }\n\n      case 'deleteWordForward': {\n        dispatchCommand(editor, DELETE_WORD_COMMAND, false);\n        break;\n      }\n\n      case 'deleteHardLineBackward':\n      case 'deleteSoftLineBackward': {\n        dispatchCommand(editor, DELETE_LINE_COMMAND, true);\n        break;\n      }\n\n      case 'deleteContentForward':\n      case 'deleteHardLineForward':\n      case 'deleteSoftLineForward': {\n        dispatchCommand(editor, DELETE_LINE_COMMAND, false);\n        break;\n      }\n\n      case 'formatStrikeThrough': {\n        dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'strikethrough');\n        break;\n      }\n\n      case 'formatBold': {\n        dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'bold');\n        break;\n      }\n\n      case 'formatItalic': {\n        dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'italic');\n        break;\n      }\n\n      case 'formatUnderline': {\n        dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'underline');\n        break;\n      }\n\n      case 'historyUndo': {\n        dispatchCommand(editor, UNDO_COMMAND, undefined);\n        break;\n      }\n\n      case 'historyRedo': {\n        dispatchCommand(editor, REDO_COMMAND, undefined);\n        break;\n      }\n\n      default:\n      // NO-OP\n    }\n  });\n}\n\nfunction onInput(event: InputEvent, editor: LexicalEditor): void {\n  // We don't want the onInput to bubble, in the case of nested editors.\n  event.stopPropagation();\n  updateEditor(editor, () => {\n    const selection = $getSelection();\n    const data = event.data;\n    const targetRange = getTargetRange(event);\n\n    if (\n      data != null &&\n      $isRangeSelection(selection) &&\n      $shouldPreventDefaultAndInsertText(\n        selection,\n        targetRange,\n        data,\n        event.timeStamp,\n        false,\n      )\n    ) {\n      // Given we're over-riding the default behavior, we will need\n      // to ensure to disable composition before dispatching the\n      // insertText command for when changing the sequence for FF.\n      if (isFirefoxEndingComposition) {\n        $onCompositionEndImpl(editor, data);\n        isFirefoxEndingComposition = false;\n      }\n      const anchor = selection.anchor;\n      const anchorNode = anchor.getNode();\n      const domSelection = getDOMSelection(editor._window);\n      if (domSelection === null) {\n        return;\n      }\n      const isBackward = selection.isBackward();\n      const startOffset = isBackward\n        ? selection.anchor.offset\n        : selection.focus.offset;\n      const endOffset = isBackward\n        ? selection.focus.offset\n        : selection.anchor.offset;\n      // If the content is the same as inserted, then don't dispatch an insertion.\n      // Given onInput doesn't take the current selection (it uses the previous)\n      // we can compare that against what the DOM currently says.\n      if (\n        !CAN_USE_BEFORE_INPUT ||\n        selection.isCollapsed() ||\n        !$isTextNode(anchorNode) ||\n        domSelection.anchorNode === null ||\n        anchorNode.getTextContent().slice(0, startOffset) +\n          data +\n          anchorNode.getTextContent().slice(startOffset + endOffset) !==\n          getAnchorTextFromDOM(domSelection.anchorNode)\n      ) {\n        dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, data);\n      }\n\n      const textLength = data.length;\n\n      // Another hack for FF, as it's possible that the IME is still\n      // open, even though compositionend has already fired (sigh).\n      if (\n        IS_FIREFOX &&\n        textLength > 1 &&\n        event.inputType === 'insertCompositionText' &&\n        !editor.isComposing()\n      ) {\n        selection.anchor.offset -= textLength;\n      }\n\n      // This ensures consistency on Android.\n      if (!IS_SAFARI && !IS_IOS && !IS_APPLE_WEBKIT && editor.isComposing()) {\n        lastKeyDownTimeStamp = 0;\n        $setCompositionKey(null);\n      }\n    } else {\n      const characterData = data !== null ? data : undefined;\n      $updateSelectedTextFromDOM(false, editor, characterData);\n\n      // onInput always fires after onCompositionEnd for FF.\n      if (isFirefoxEndingComposition) {\n        $onCompositionEndImpl(editor, data || undefined);\n        isFirefoxEndingComposition = false;\n      }\n    }\n\n    // Also flush any other mutations that might have occurred\n    // since the change.\n    $flushMutations();\n  });\n  unprocessedBeforeInputData = null;\n}\n\nfunction onCompositionStart(\n  event: CompositionEvent,\n  editor: LexicalEditor,\n): void {\n  updateEditor(editor, () => {\n    const selection = $getSelection();\n\n    if ($isRangeSelection(selection) && !editor.isComposing()) {\n      const anchor = selection.anchor;\n      const node = selection.anchor.getNode();\n      $setCompositionKey(anchor.key);\n\n      if (\n        // If it has been 30ms since the last keydown, then we should\n        // apply the empty space heuristic. We can't do this for Safari,\n        // as the keydown fires after composition start.\n        event.timeStamp < lastKeyDownTimeStamp + ANDROID_COMPOSITION_LATENCY ||\n        // FF has issues around composing multibyte characters, so we also\n        // need to invoke the empty space heuristic below.\n        anchor.type === 'element' ||\n        !selection.isCollapsed() ||\n        ($isTextNode(node) && node.getStyle() !== selection.style)\n      ) {\n        // We insert a zero width character, ready for the composition\n        // to get inserted into the new node we create. If\n        // we don't do this, Safari will fail on us because\n        // there is no text node matching the selection.\n        dispatchCommand(\n          editor,\n          CONTROLLED_TEXT_INSERTION_COMMAND,\n          COMPOSITION_START_CHAR,\n        );\n      }\n    }\n  });\n}\n\nfunction $onCompositionEndImpl(editor: LexicalEditor, data?: string): void {\n  const compositionKey = editor._compositionKey;\n  $setCompositionKey(null);\n\n  // Handle termination of composition.\n  if (compositionKey !== null && data != null) {\n    // Composition can sometimes move to an adjacent DOM node when backspacing.\n    // So check for the empty case.\n    if (data === '') {\n      const node = $getNodeByKey(compositionKey);\n      const textNode = getDOMTextNode(editor.getElementByKey(compositionKey));\n\n      if (\n        textNode !== null &&\n        textNode.nodeValue !== null &&\n        $isTextNode(node)\n      ) {\n        $updateTextNodeFromDOMContent(\n          node,\n          textNode.nodeValue,\n          null,\n          null,\n          true,\n        );\n      }\n\n      return;\n    }\n\n    // Composition can sometimes be that of a new line. In which case, we need to\n    // handle that accordingly.\n    if (data[data.length - 1] === '\\n') {\n      const selection = $getSelection();\n\n      if ($isRangeSelection(selection)) {\n        // If the last character is a line break, we also need to insert\n        // a line break.\n        const focus = selection.focus;\n        selection.anchor.set(focus.key, focus.offset, focus.type);\n        dispatchCommand(editor, KEY_ENTER_COMMAND, null);\n        return;\n      }\n    }\n  }\n\n  $updateSelectedTextFromDOM(true, editor, data);\n}\n\nfunction onCompositionEnd(\n  event: CompositionEvent,\n  editor: LexicalEditor,\n): void {\n  // Firefox fires onCompositionEnd before onInput, but Chrome/Webkit,\n  // fire onInput before onCompositionEnd. To ensure the sequence works\n  // like Chrome/Webkit we use the isFirefoxEndingComposition flag to\n  // defer handling of onCompositionEnd in Firefox till we have processed\n  // the logic in onInput.\n  if (IS_FIREFOX) {\n    isFirefoxEndingComposition = true;\n  } else {\n    updateEditor(editor, () => {\n      $onCompositionEndImpl(editor, event.data);\n    });\n  }\n}\n\nfunction onKeyDown(event: KeyboardEvent, editor: LexicalEditor): void {\n  lastKeyDownTimeStamp = event.timeStamp;\n  lastKeyCode = event.key;\n  if (editor.isComposing()) {\n    return;\n  }\n\n  const {key, shiftKey, ctrlKey, metaKey, altKey} = event;\n\n  if (dispatchCommand(editor, KEY_DOWN_COMMAND, event)) {\n    return;\n  }\n\n  if (key == null) {\n    return;\n  }\n\n  if (isMoveForward(key, ctrlKey, altKey, metaKey)) {\n    dispatchCommand(editor, KEY_ARROW_RIGHT_COMMAND, event);\n  } else if (isMoveToEnd(key, ctrlKey, shiftKey, altKey, metaKey)) {\n    dispatchCommand(editor, MOVE_TO_END, event);\n  } else if (isMoveBackward(key, ctrlKey, altKey, metaKey)) {\n    dispatchCommand(editor, KEY_ARROW_LEFT_COMMAND, event);\n  } else if (isMoveToStart(key, ctrlKey, shiftKey, altKey, metaKey)) {\n    dispatchCommand(editor, MOVE_TO_START, event);\n  } else if (isMoveUp(key, ctrlKey, metaKey)) {\n    dispatchCommand(editor, KEY_ARROW_UP_COMMAND, event);\n  } else if (isMoveDown(key, ctrlKey, metaKey)) {\n    dispatchCommand(editor, KEY_ARROW_DOWN_COMMAND, event);\n  } else if (isLineBreak(key, shiftKey)) {\n    isInsertLineBreak = true;\n    dispatchCommand(editor, KEY_ENTER_COMMAND, event);\n  } else if (isSpace(key)) {\n    dispatchCommand(editor, KEY_SPACE_COMMAND, event);\n  } else if (isOpenLineBreak(key, ctrlKey)) {\n    event.preventDefault();\n    isInsertLineBreak = true;\n    dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, true);\n  } else if (isParagraph(key, shiftKey)) {\n    isInsertLineBreak = false;\n    dispatchCommand(editor, KEY_ENTER_COMMAND, event);\n  } else if (isDeleteBackward(key, altKey, metaKey, ctrlKey)) {\n    if (isBackspace(key)) {\n      dispatchCommand(editor, KEY_BACKSPACE_COMMAND, event);\n    } else {\n      event.preventDefault();\n      dispatchCommand(editor, DELETE_CHARACTER_COMMAND, true);\n    }\n  } else if (isEscape(key)) {\n    dispatchCommand(editor, KEY_ESCAPE_COMMAND, event);\n  } else if (isDeleteForward(key, ctrlKey, shiftKey, altKey, metaKey)) {\n    if (isDelete(key)) {\n      dispatchCommand(editor, KEY_DELETE_COMMAND, event);\n    } else {\n      event.preventDefault();\n      dispatchCommand(editor, DELETE_CHARACTER_COMMAND, false);\n    }\n  } else if (isDeleteWordBackward(key, altKey, ctrlKey)) {\n    event.preventDefault();\n    dispatchCommand(editor, DELETE_WORD_COMMAND, true);\n  } else if (isDeleteWordForward(key, altKey, ctrlKey)) {\n    event.preventDefault();\n    dispatchCommand(editor, DELETE_WORD_COMMAND, false);\n  } else if (isDeleteLineBackward(key, metaKey)) {\n    event.preventDefault();\n    dispatchCommand(editor, DELETE_LINE_COMMAND, true);\n  } else if (isDeleteLineForward(key, metaKey)) {\n    event.preventDefault();\n    dispatchCommand(editor, DELETE_LINE_COMMAND, false);\n  } else if (isAt(key)) {\n    dispatchCommand(editor, KEY_AT_COMMAND, event);\n  } else if (isBold(key, altKey, metaKey, ctrlKey)) {\n    event.preventDefault();\n    dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'bold');\n  } else if (isUnderline(key, altKey, metaKey, ctrlKey)) {\n    event.preventDefault();\n    dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'underline');\n  } else if (isItalic(key, altKey, metaKey, ctrlKey)) {\n    event.preventDefault();\n    dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'italic');\n  } else if (isTab(key, altKey, ctrlKey, metaKey)) {\n    dispatchCommand(editor, KEY_TAB_COMMAND, event);\n  } else if (isUndo(key, shiftKey, metaKey, ctrlKey)) {\n    event.preventDefault();\n    dispatchCommand(editor, UNDO_COMMAND, undefined);\n  } else if (isRedo(key, shiftKey, metaKey, ctrlKey)) {\n    event.preventDefault();\n    dispatchCommand(editor, REDO_COMMAND, undefined);\n  } else {\n    const prevSelection = editor._editorState._selection;\n    if ($isNodeSelection(prevSelection)) {\n      if (isCopy(key, shiftKey, metaKey, ctrlKey)) {\n        event.preventDefault();\n        dispatchCommand(editor, COPY_COMMAND, event);\n      } else if (isCut(key, shiftKey, metaKey, ctrlKey)) {\n        event.preventDefault();\n        dispatchCommand(editor, CUT_COMMAND, event);\n      } else if (isSelectAll(key, metaKey, ctrlKey)) {\n        event.preventDefault();\n        dispatchCommand(editor, SELECT_ALL_COMMAND, event);\n      }\n      // FF does it well (no need to override behavior)\n    } else if (!IS_FIREFOX && isSelectAll(key, metaKey, ctrlKey)) {\n      event.preventDefault();\n      dispatchCommand(editor, SELECT_ALL_COMMAND, event);\n    }\n  }\n\n  if (isModifier(ctrlKey, shiftKey, altKey, metaKey)) {\n    dispatchCommand(editor, KEY_MODIFIER_COMMAND, event);\n  }\n}\n\nfunction getRootElementRemoveHandles(\n  rootElement: HTMLElement,\n): RootElementRemoveHandles {\n  // @ts-expect-error: internal field\n  let eventHandles = rootElement.__lexicalEventHandles;\n\n  if (eventHandles === undefined) {\n    eventHandles = [];\n    // @ts-expect-error: internal field\n    rootElement.__lexicalEventHandles = eventHandles;\n  }\n\n  return eventHandles;\n}\n\n// Mapping root editors to their active nested editors, contains nested editors\n// mapping only, so if root editor is selected map will have no reference to free up memory\nconst activeNestedEditorsMap: Map<string, LexicalEditor> = new Map();\n\nfunction onDocumentSelectionChange(event: Event): void {\n  const target = event.target as null | Element | Document;\n  const targetWindow =\n    target == null\n      ? null\n      : target.nodeType === 9\n      ? (target as Document).defaultView\n      : (target as Element).ownerDocument.defaultView;\n  const domSelection = getDOMSelection(targetWindow);\n  if (domSelection === null) {\n    return;\n  }\n  const nextActiveEditor = getNearestEditorFromDOMNode(domSelection.anchorNode);\n  if (nextActiveEditor === null) {\n    return;\n  }\n\n  if (isSelectionChangeFromMouseDown) {\n    isSelectionChangeFromMouseDown = false;\n    updateEditor(nextActiveEditor, () => {\n      const lastSelection = $getPreviousSelection();\n      const domAnchorNode = domSelection.anchorNode;\n      if (domAnchorNode === null) {\n        return;\n      }\n      const nodeType = domAnchorNode.nodeType;\n      // If the user is attempting to click selection back onto text, then\n      // we should attempt create a range selection.\n      // When we click on an empty paragraph node or the end of a paragraph that ends\n      // with an image/poll, the nodeType will be ELEMENT_NODE\n      if (nodeType !== DOM_ELEMENT_TYPE && nodeType !== DOM_TEXT_TYPE) {\n        return;\n      }\n      const newSelection = $internalCreateRangeSelection(\n        lastSelection,\n        domSelection,\n        nextActiveEditor,\n        event,\n      );\n      $setSelection(newSelection);\n    });\n  }\n\n  // When editor receives selection change event, we're checking if\n  // it has any sibling editors (within same parent editor) that were active\n  // before, and trigger selection change on it to nullify selection.\n  const editors = getEditorsToPropagate(nextActiveEditor);\n  const rootEditor = editors[editors.length - 1];\n  const rootEditorKey = rootEditor._key;\n  const activeNestedEditor = activeNestedEditorsMap.get(rootEditorKey);\n  const prevActiveEditor = activeNestedEditor || rootEditor;\n\n  if (prevActiveEditor !== nextActiveEditor) {\n    onSelectionChange(domSelection, prevActiveEditor, false);\n  }\n\n  onSelectionChange(domSelection, nextActiveEditor, true);\n\n  // If newly selected editor is nested, then add it to the map, clean map otherwise\n  if (nextActiveEditor !== rootEditor) {\n    activeNestedEditorsMap.set(rootEditorKey, nextActiveEditor);\n  } else if (activeNestedEditor) {\n    activeNestedEditorsMap.delete(rootEditorKey);\n  }\n}\n\nfunction stopLexicalPropagation(event: Event): void {\n  // We attach a special property to ensure the same event doesn't re-fire\n  // for parent editors.\n  // @ts-ignore\n  event._lexicalHandled = true;\n}\n\nfunction hasStoppedLexicalPropagation(event: Event): boolean {\n  // @ts-ignore\n  const stopped = event._lexicalHandled === true;\n  return stopped;\n}\n\nexport type EventHandler = (event: Event, editor: LexicalEditor) => void;\n\nexport function addRootElementEvents(\n  rootElement: HTMLElement,\n  editor: LexicalEditor,\n): void {\n  // We only want to have a single global selectionchange event handler, shared\n  // between all editor instances.\n  const doc = rootElement.ownerDocument;\n  const documentRootElementsCount = rootElementsRegistered.get(doc);\n  if (\n    documentRootElementsCount === undefined ||\n    documentRootElementsCount < 1\n  ) {\n    doc.addEventListener('selectionchange', onDocumentSelectionChange);\n  }\n  rootElementsRegistered.set(doc, (documentRootElementsCount || 0) + 1);\n\n  // @ts-expect-error: internal field\n  rootElement.__lexicalEditor = editor;\n  const removeHandles = getRootElementRemoveHandles(rootElement);\n\n  for (let i = 0; i < rootElementEvents.length; i++) {\n    const [eventName, onEvent] = rootElementEvents[i];\n    const eventHandler =\n      typeof onEvent === 'function'\n        ? (event: Event) => {\n            if (hasStoppedLexicalPropagation(event)) {\n              return;\n            }\n            stopLexicalPropagation(event);\n            if (editor.isEditable() || eventName === 'click') {\n              onEvent(event, editor);\n            }\n          }\n        : (event: Event) => {\n            if (hasStoppedLexicalPropagation(event)) {\n              return;\n            }\n            stopLexicalPropagation(event);\n            const isEditable = editor.isEditable();\n            switch (eventName) {\n              case 'cut':\n                return (\n                  isEditable &&\n                  dispatchCommand(editor, CUT_COMMAND, event as ClipboardEvent)\n                );\n\n              case 'copy':\n                return dispatchCommand(\n                  editor,\n                  COPY_COMMAND,\n                  event as ClipboardEvent,\n                );\n\n              case 'paste':\n                return (\n                  isEditable &&\n                  dispatchCommand(\n                    editor,\n                    PASTE_COMMAND,\n                    event as ClipboardEvent,\n                  )\n                );\n\n              case 'dragstart':\n                return (\n                  isEditable &&\n                  dispatchCommand(editor, DRAGSTART_COMMAND, event as DragEvent)\n                );\n\n              case 'dragover':\n                return (\n                  isEditable &&\n                  dispatchCommand(editor, DRAGOVER_COMMAND, event as DragEvent)\n                );\n\n              case 'dragend':\n                return (\n                  isEditable &&\n                  dispatchCommand(editor, DRAGEND_COMMAND, event as DragEvent)\n                );\n\n              case 'focus':\n                return (\n                  isEditable &&\n                  dispatchCommand(editor, FOCUS_COMMAND, event as FocusEvent)\n                );\n\n              case 'blur': {\n                return (\n                  isEditable &&\n                  dispatchCommand(editor, BLUR_COMMAND, event as FocusEvent)\n                );\n              }\n\n              case 'drop':\n                return (\n                  isEditable &&\n                  dispatchCommand(editor, DROP_COMMAND, event as DragEvent)\n                );\n            }\n          };\n    rootElement.addEventListener(eventName, eventHandler);\n    removeHandles.push(() => {\n      rootElement.removeEventListener(eventName, eventHandler);\n    });\n  }\n}\n\nexport function removeRootElementEvents(rootElement: HTMLElement): void {\n  const doc = rootElement.ownerDocument;\n  const documentRootElementsCount = rootElementsRegistered.get(doc);\n  invariant(\n    documentRootElementsCount !== undefined,\n    'Root element not registered',\n  );\n\n  // We only want to have a single global selectionchange event handler, shared\n  // between all editor instances.\n  const newCount = documentRootElementsCount - 1;\n  invariant(newCount >= 0, 'Root element count less than 0');\n  rootElementsRegistered.set(doc, newCount);\n  if (newCount === 0) {\n    doc.removeEventListener('selectionchange', onDocumentSelectionChange);\n  }\n\n  const editor = getEditorPropertyFromDOMNode(rootElement);\n\n  if (isLexicalEditor(editor)) {\n    cleanActiveNestedEditorsMap(editor);\n    // @ts-expect-error: internal field\n    rootElement.__lexicalEditor = null;\n  } else if (editor) {\n    invariant(\n      false,\n      'Attempted to remove event handlers from a node that does not belong to this build of Lexical',\n    );\n  }\n\n  const removeHandles = getRootElementRemoveHandles(rootElement);\n\n  for (let i = 0; i < removeHandles.length; i++) {\n    removeHandles[i]();\n  }\n\n  // @ts-expect-error: internal field\n  rootElement.__lexicalEventHandles = [];\n}\n\nfunction cleanActiveNestedEditorsMap(editor: LexicalEditor) {\n  if (editor._parentEditor !== null) {\n    // For nested editor cleanup map if this editor was marked as active\n    const editors = getEditorsToPropagate(editor);\n    const rootEditor = editors[editors.length - 1];\n    const rootEditorKey = rootEditor._key;\n\n    if (activeNestedEditorsMap.get(rootEditorKey) === editor) {\n      activeNestedEditorsMap.delete(rootEditorKey);\n    }\n  } else {\n    // For top-level editors cleanup map\n    activeNestedEditorsMap.delete(editor._key);\n  }\n}\n\nexport function markSelectionChangeFromDOMUpdate(): void {\n  isSelectionChangeFromDOMUpdate = true;\n}\n\nexport function markCollapsedSelectionFormat(\n  format: number,\n  style: string,\n  offset: number,\n  key: NodeKey,\n  timeStamp: number,\n): void {\n  collapsedSelectionFormat = [format, style, offset, key, timeStamp];\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/LexicalGC.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {ElementNode} from '.';\nimport type {LexicalEditor} from './LexicalEditor';\nimport type {EditorState} from './LexicalEditorState';\nimport type {NodeKey, NodeMap} from './LexicalNode';\n\nimport {$isElementNode} from '.';\nimport {cloneDecorators} from './LexicalUtils';\n\nexport function $garbageCollectDetachedDecorators(\n  editor: LexicalEditor,\n  pendingEditorState: EditorState,\n): void {\n  const currentDecorators = editor._decorators;\n  const pendingDecorators = editor._pendingDecorators;\n  let decorators = pendingDecorators || currentDecorators;\n  const nodeMap = pendingEditorState._nodeMap;\n  let key;\n\n  for (key in decorators) {\n    if (!nodeMap.has(key)) {\n      if (decorators === currentDecorators) {\n        decorators = cloneDecorators(editor);\n      }\n\n      delete decorators[key];\n    }\n  }\n}\n\ntype IntentionallyMarkedAsDirtyElement = boolean;\n\nfunction $garbageCollectDetachedDeepChildNodes(\n  node: ElementNode,\n  parentKey: NodeKey,\n  prevNodeMap: NodeMap,\n  nodeMap: NodeMap,\n  nodeMapDelete: Array<NodeKey>,\n  dirtyNodes: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,\n): void {\n  let child = node.getFirstChild();\n\n  while (child !== null) {\n    const childKey = child.__key;\n    // TODO Revise condition below, redundant? LexicalNode already cleans up children when moving Nodes\n    if (child.__parent === parentKey) {\n      if ($isElementNode(child)) {\n        $garbageCollectDetachedDeepChildNodes(\n          child,\n          childKey,\n          prevNodeMap,\n          nodeMap,\n          nodeMapDelete,\n          dirtyNodes,\n        );\n      }\n\n      // If we have created a node and it was dereferenced, then also\n      // remove it from out dirty nodes Set.\n      if (!prevNodeMap.has(childKey)) {\n        dirtyNodes.delete(childKey);\n      }\n      nodeMapDelete.push(childKey);\n    }\n    child = child.getNextSibling();\n  }\n}\n\nexport function $garbageCollectDetachedNodes(\n  prevEditorState: EditorState,\n  editorState: EditorState,\n  dirtyLeaves: Set<NodeKey>,\n  dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,\n): void {\n  const prevNodeMap = prevEditorState._nodeMap;\n  const nodeMap = editorState._nodeMap;\n  // Store dirtyElements in a queue for later deletion; deleting dirty subtrees too early will\n  // hinder accessing .__next on child nodes\n  const nodeMapDelete: Array<NodeKey> = [];\n\n  for (const [nodeKey] of dirtyElements) {\n    const node = nodeMap.get(nodeKey);\n    if (node !== undefined) {\n      // Garbage collect node and its children if they exist\n      if (!node.isAttached()) {\n        if ($isElementNode(node)) {\n          $garbageCollectDetachedDeepChildNodes(\n            node,\n            nodeKey,\n            prevNodeMap,\n            nodeMap,\n            nodeMapDelete,\n            dirtyElements,\n          );\n        }\n        // If we have created a node and it was dereferenced, then also\n        // remove it from out dirty nodes Set.\n        if (!prevNodeMap.has(nodeKey)) {\n          dirtyElements.delete(nodeKey);\n        }\n        nodeMapDelete.push(nodeKey);\n      }\n    }\n  }\n  for (const nodeKey of nodeMapDelete) {\n    nodeMap.delete(nodeKey);\n  }\n\n  for (const nodeKey of dirtyLeaves) {\n    const node = nodeMap.get(nodeKey);\n    if (node !== undefined && !node.isAttached()) {\n      if (!prevNodeMap.has(nodeKey)) {\n        dirtyLeaves.delete(nodeKey);\n      }\n      nodeMap.delete(nodeKey);\n    }\n  }\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/LexicalMutations.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {TextNode} from '.';\nimport type {LexicalEditor} from './LexicalEditor';\nimport type {BaseSelection} from './LexicalSelection';\n\nimport {IS_FIREFOX} from 'lexical/shared/environment';\n\nimport {\n  $getSelection,\n  $isDecoratorNode,\n  $isElementNode,\n  $isTextNode,\n  $setSelection,\n} from '.';\nimport {DOM_TEXT_TYPE} from './LexicalConstants';\nimport {updateEditor} from './LexicalUpdates';\nimport {\n  $getNearestNodeFromDOMNode,\n  $getNodeFromDOMNode,\n  $updateTextNodeFromDOMContent,\n  getDOMSelection,\n  getWindow,\n  internalGetRoot,\n  isFirefoxClipboardEvents,\n} from './LexicalUtils';\n// The time between a text entry event and the mutation observer firing.\nconst TEXT_MUTATION_VARIANCE = 100;\n\nlet isProcessingMutations = false;\nlet lastTextEntryTimeStamp = 0;\n\nexport function getIsProcessingMutations(): boolean {\n  return isProcessingMutations;\n}\n\nfunction updateTimeStamp(event: Event) {\n  lastTextEntryTimeStamp = event.timeStamp;\n}\n\nfunction initTextEntryListener(editor: LexicalEditor): void {\n  if (lastTextEntryTimeStamp === 0) {\n    getWindow(editor).addEventListener('textInput', updateTimeStamp, true);\n  }\n}\n\nfunction isManagedLineBreak(\n  dom: Node,\n  target: Node,\n  editor: LexicalEditor,\n): boolean {\n  return (\n    // @ts-expect-error: internal field\n    target.__lexicalLineBreak === dom ||\n    // @ts-ignore We intentionally add this to the Node.\n    dom[`__lexicalKey_${editor._key}`] !== undefined\n  );\n}\n\nfunction getLastSelection(editor: LexicalEditor): null | BaseSelection {\n  return editor.getEditorState().read(() => {\n    const selection = $getSelection();\n    return selection !== null ? selection.clone() : null;\n  });\n}\n\nfunction $handleTextMutation(\n  target: Text,\n  node: TextNode,\n  editor: LexicalEditor,\n): void {\n  const domSelection = getDOMSelection(editor._window);\n  let anchorOffset = null;\n  let focusOffset = null;\n\n  if (domSelection !== null && domSelection.anchorNode === target) {\n    anchorOffset = domSelection.anchorOffset;\n    focusOffset = domSelection.focusOffset;\n  }\n\n  const text = target.nodeValue;\n  if (text !== null) {\n    $updateTextNodeFromDOMContent(node, text, anchorOffset, focusOffset, false);\n  }\n}\n\nfunction shouldUpdateTextNodeFromMutation(\n  selection: null | BaseSelection,\n  targetDOM: Node,\n  targetNode: TextNode,\n): boolean {\n  return targetDOM.nodeType === DOM_TEXT_TYPE && targetNode.isAttached();\n}\n\nexport function $flushMutations(\n  editor: LexicalEditor,\n  mutations: Array<MutationRecord>,\n  observer: MutationObserver,\n): void {\n  isProcessingMutations = true;\n  const shouldFlushTextMutations =\n    performance.now() - lastTextEntryTimeStamp > TEXT_MUTATION_VARIANCE;\n\n  try {\n    updateEditor(editor, () => {\n      const selection = $getSelection() || getLastSelection(editor);\n      const badDOMTargets = new Map();\n      const rootElement = editor.getRootElement();\n      // We use the current editor state, as that reflects what is\n      // actually \"on screen\".\n      const currentEditorState = editor._editorState;\n      const blockCursorElement = editor._blockCursorElement;\n      let shouldRevertSelection = false;\n      let possibleTextForFirefoxPaste = '';\n\n      for (let i = 0; i < mutations.length; i++) {\n        const mutation = mutations[i];\n        const type = mutation.type;\n        const targetDOM = mutation.target;\n        let targetNode = $getNearestNodeFromDOMNode(\n          targetDOM,\n          currentEditorState,\n        );\n\n        if (\n          (targetNode === null && targetDOM !== rootElement) ||\n          $isDecoratorNode(targetNode)\n        ) {\n          continue;\n        }\n\n        if (type === 'characterData') {\n          // Text mutations are deferred and passed to mutation listeners to be\n          // processed outside of the Lexical engine.\n          if (\n            shouldFlushTextMutations &&\n            $isTextNode(targetNode) &&\n            shouldUpdateTextNodeFromMutation(selection, targetDOM, targetNode)\n          ) {\n            $handleTextMutation(\n              // nodeType === DOM_TEXT_TYPE is a Text DOM node\n              targetDOM as Text,\n              targetNode,\n              editor,\n            );\n          }\n        } else if (type === 'childList') {\n          shouldRevertSelection = true;\n          // We attempt to \"undo\" any changes that have occurred outside\n          // of Lexical. We want Lexical's editor state to be source of truth.\n          // To the user, these will look like no-ops.\n          const addedDOMs = mutation.addedNodes;\n\n          for (let s = 0; s < addedDOMs.length; s++) {\n            const addedDOM = addedDOMs[s];\n            const node = $getNodeFromDOMNode(addedDOM);\n            const parentDOM = addedDOM.parentNode;\n\n            if (\n              parentDOM != null &&\n              addedDOM !== blockCursorElement &&\n              node === null &&\n              (addedDOM.nodeName !== 'BR' ||\n                !isManagedLineBreak(addedDOM, parentDOM, editor))\n            ) {\n              if (IS_FIREFOX) {\n                const possibleText =\n                  (addedDOM as HTMLElement).innerText || addedDOM.nodeValue;\n\n                if (possibleText) {\n                  possibleTextForFirefoxPaste += possibleText;\n                }\n              }\n\n              parentDOM.removeChild(addedDOM);\n            }\n          }\n\n          const removedDOMs = mutation.removedNodes;\n          const removedDOMsLength = removedDOMs.length;\n\n          if (removedDOMsLength > 0) {\n            let unremovedBRs = 0;\n\n            for (let s = 0; s < removedDOMsLength; s++) {\n              const removedDOM = removedDOMs[s];\n\n              if (\n                (removedDOM.nodeName === 'BR' &&\n                  isManagedLineBreak(removedDOM, targetDOM, editor)) ||\n                blockCursorElement === removedDOM\n              ) {\n                targetDOM.appendChild(removedDOM);\n                unremovedBRs++;\n              }\n            }\n\n            if (removedDOMsLength !== unremovedBRs) {\n              if (targetDOM === rootElement) {\n                targetNode = internalGetRoot(currentEditorState);\n              }\n\n              badDOMTargets.set(targetDOM, targetNode);\n            }\n          }\n        }\n      }\n\n      // Now we process each of the unique target nodes, attempting\n      // to restore their contents back to the source of truth, which\n      // is Lexical's \"current\" editor state. This is basically like\n      // an internal revert on the DOM.\n      if (badDOMTargets.size > 0) {\n        for (const [targetDOM, targetNode] of badDOMTargets) {\n          if ($isElementNode(targetNode)) {\n            const childKeys = targetNode.getChildrenKeys();\n            let currentDOM = targetDOM.firstChild;\n\n            for (let s = 0; s < childKeys.length; s++) {\n              const key = childKeys[s];\n              const correctDOM = editor.getElementByKey(key);\n\n              if (correctDOM === null) {\n                continue;\n              }\n\n              if (currentDOM == null) {\n                targetDOM.appendChild(correctDOM);\n                currentDOM = correctDOM;\n              } else if (currentDOM !== correctDOM) {\n                targetDOM.replaceChild(correctDOM, currentDOM);\n              }\n\n              currentDOM = currentDOM.nextSibling;\n            }\n          } else if ($isTextNode(targetNode)) {\n            targetNode.markDirty();\n          }\n        }\n      }\n\n      // Capture all the mutations made during this function. This\n      // also prevents us having to process them on the next cycle\n      // of onMutation, as these mutations were made by us.\n      const records = observer.takeRecords();\n\n      // Check for any random auto-added <br> elements, and remove them.\n      // These get added by the browser when we undo the above mutations\n      // and this can lead to a broken UI.\n      if (records.length > 0) {\n        for (let i = 0; i < records.length; i++) {\n          const record = records[i];\n          const addedNodes = record.addedNodes;\n          const target = record.target;\n\n          for (let s = 0; s < addedNodes.length; s++) {\n            const addedDOM = addedNodes[s];\n            const parentDOM = addedDOM.parentNode;\n\n            if (\n              parentDOM != null &&\n              addedDOM.nodeName === 'BR' &&\n              !isManagedLineBreak(addedDOM, target, editor)\n            ) {\n              parentDOM.removeChild(addedDOM);\n            }\n          }\n        }\n\n        // Clear any of those removal mutations\n        observer.takeRecords();\n      }\n\n      if (selection !== null) {\n        if (shouldRevertSelection) {\n          selection.dirty = true;\n          $setSelection(selection);\n        }\n\n        if (IS_FIREFOX && isFirefoxClipboardEvents(editor)) {\n          selection.insertRawText(possibleTextForFirefoxPaste);\n        }\n      }\n    });\n  } finally {\n    isProcessingMutations = false;\n  }\n}\n\nexport function $flushRootMutations(editor: LexicalEditor): void {\n  const observer = editor._observer;\n\n  if (observer !== null) {\n    const mutations = observer.takeRecords();\n    $flushMutations(editor, mutations, observer);\n  }\n}\n\nexport function initMutationObserver(editor: LexicalEditor): void {\n  initTextEntryListener(editor);\n  editor._observer = new MutationObserver(\n    (mutations: Array<MutationRecord>, observer: MutationObserver) => {\n      $flushMutations(editor, mutations, observer);\n    },\n  );\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/LexicalNode.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\n/* eslint-disable no-constant-condition */\nimport type {EditorConfig, LexicalEditor} from './LexicalEditor';\nimport type {BaseSelection, RangeSelection} from './LexicalSelection';\nimport type {Klass, KlassConstructor} from 'lexical';\n\nimport invariant from 'lexical/shared/invariant';\n\nimport {\n  $createParagraphNode,\n  $isDecoratorNode,\n  $isElementNode,\n  $isRootNode,\n  $isTextNode,\n  type DecoratorNode,\n  ElementNode,\n} from '.';\nimport {\n  $getSelection,\n  $isNodeSelection,\n  $isRangeSelection,\n  $moveSelectionPointToEnd,\n  $updateElementSelectionOnCreateDeleteNode,\n  moveSelectionPointToSibling,\n} from './LexicalSelection';\nimport {\n  errorOnReadOnly,\n  getActiveEditor,\n  getActiveEditorState,\n} from './LexicalUpdates';\nimport {\n  $cloneWithProperties,\n  $getCompositionKey,\n  $getNodeByKey,\n  $isRootOrShadowRoot,\n  $maybeMoveChildrenSelectionToParent,\n  $setCompositionKey,\n  $setNodeKey,\n  $setSelection,\n  errorOnInsertTextNodeOnRoot,\n  internalMarkNodeAsDirty,\n  removeFromParent,\n} from './LexicalUtils';\n\nexport type NodeMap = Map<NodeKey, LexicalNode>;\n\nexport type SerializedLexicalNode = {\n  type: string;\n  version: number;\n};\n\nexport function $removeNode(\n  nodeToRemove: LexicalNode,\n  restoreSelection: boolean,\n  preserveEmptyParent?: boolean,\n): void {\n  errorOnReadOnly();\n  const key = nodeToRemove.__key;\n  const parent = nodeToRemove.getParent();\n  if (parent === null) {\n    return;\n  }\n  const selection = $maybeMoveChildrenSelectionToParent(nodeToRemove);\n  let selectionMoved = false;\n  if ($isRangeSelection(selection) && restoreSelection) {\n    const anchor = selection.anchor;\n    const focus = selection.focus;\n    if (anchor.key === key) {\n      moveSelectionPointToSibling(\n        anchor,\n        nodeToRemove,\n        parent,\n        nodeToRemove.getPreviousSibling(),\n        nodeToRemove.getNextSibling(),\n      );\n      selectionMoved = true;\n    }\n    if (focus.key === key) {\n      moveSelectionPointToSibling(\n        focus,\n        nodeToRemove,\n        parent,\n        nodeToRemove.getPreviousSibling(),\n        nodeToRemove.getNextSibling(),\n      );\n      selectionMoved = true;\n    }\n  } else if (\n    $isNodeSelection(selection) &&\n    restoreSelection &&\n    nodeToRemove.isSelected()\n  ) {\n    nodeToRemove.selectPrevious();\n  }\n\n  if ($isRangeSelection(selection) && restoreSelection && !selectionMoved) {\n    // Doing this is O(n) so lets avoid it unless we need to do it\n    const index = nodeToRemove.getIndexWithinParent();\n    removeFromParent(nodeToRemove);\n    $updateElementSelectionOnCreateDeleteNode(selection, parent, index, -1);\n  } else {\n    removeFromParent(nodeToRemove);\n  }\n\n  if (\n    !preserveEmptyParent &&\n    !$isRootOrShadowRoot(parent) &&\n    !parent.canBeEmpty() &&\n    parent.isEmpty()\n  ) {\n    $removeNode(parent, restoreSelection);\n  }\n  if (restoreSelection && $isRootNode(parent) && parent.isEmpty()) {\n    parent.selectEnd();\n  }\n}\n\nexport type DOMConversion<T extends HTMLElement = HTMLElement> = {\n  conversion: DOMConversionFn<T>;\n  priority?: 0 | 1 | 2 | 3 | 4;\n};\n\nexport type DOMConversionFn<T extends HTMLElement = HTMLElement> = (\n  element: T,\n) => DOMConversionOutput | null;\n\nexport type DOMChildConversion = (\n  lexicalNode: LexicalNode,\n  parentLexicalNode: LexicalNode | null | undefined,\n) => LexicalNode | null | undefined;\n\nexport type DOMConversionMap<T extends HTMLElement = HTMLElement> = Record<\n  NodeName,\n  (node: T) => DOMConversion<T> | null\n>;\ntype NodeName = string;\n\n/**\n * Output for a DOM conversion.\n * Node can be set to 'ignore' to ignore the conversion and handling of the DOMNode\n * including all its children.\n *\n * You can specify a function to run for each converted child (forChild) or on all\n * the child nodes after the conversion is complete (after).\n * The key difference here is that forChild runs for every deeply nested child node\n * of the current node, whereas after will run only once after the\n * transformation of the node and all its children is complete.\n */\nexport type DOMConversionOutput = {\n  after?: (childLexicalNodes: Array<LexicalNode>) => Array<LexicalNode>;\n  forChild?: DOMChildConversion;\n  node: null | LexicalNode | Array<LexicalNode> | 'ignore';\n};\n\nexport type DOMExportOutputMap = Map<\n  Klass<LexicalNode>,\n  (editor: LexicalEditor, target: LexicalNode) => DOMExportOutput\n>;\n\nexport type DOMExportOutput = {\n  after?: (\n    generatedElement: HTMLElement | Text | null | undefined,\n  ) => HTMLElement | Text | null | undefined;\n  element: HTMLElement | Text | null;\n};\n\nexport type NodeKey = string;\n\nexport class LexicalNode {\n  // Allow us to look up the type including static props\n  declare ['constructor']: KlassConstructor<typeof LexicalNode>;\n  /** @internal */\n  __type: string;\n  /** @internal */\n  //@ts-ignore We set the key in the constructor.\n  __key: string;\n  /** @internal */\n  __parent: null | NodeKey;\n  /** @internal */\n  __prev: null | NodeKey;\n  /** @internal */\n  __next: null | NodeKey;\n\n  // Flow doesn't support abstract classes unfortunately, so we can't _force_\n  // subclasses of Node to implement statics. All subclasses of Node should have\n  // a static getType and clone method though. We define getType and clone here so we can call it\n  // on any  Node, and we throw this error by default since the subclass should provide\n  // their own implementation.\n  /**\n   * Returns the string type of this node. Every node must\n   * implement this and it MUST BE UNIQUE amongst nodes registered\n   * on the editor.\n   *\n   */\n  static getType(): string {\n    invariant(\n      false,\n      'LexicalNode: Node %s does not implement .getType().',\n      this.name,\n    );\n  }\n\n  /**\n   * Clones this node, creating a new node with a different key\n   * and adding it to the EditorState (but not attaching it anywhere!). All nodes must\n   * implement this method.\n   *\n   */\n  static clone(_data: unknown): LexicalNode {\n    invariant(\n      false,\n      'LexicalNode: Node %s does not implement .clone().',\n      this.name,\n    );\n  }\n\n  /**\n   * Perform any state updates on the clone of prevNode that are not already\n   * handled by the constructor call in the static clone method. If you have\n   * state to update in your clone that is not handled directly by the\n   * constructor, it is advisable to override this method but it is required\n   * to include a call to `super.afterCloneFrom(prevNode)` in your\n   * implementation. This is only intended to be called by\n   * {@link $cloneWithProperties} function or via a super call.\n   *\n   * @example\n   * ```ts\n   * class ClassesTextNode extends TextNode {\n   *   // Not shown: static getType, static importJSON, exportJSON, createDOM, updateDOM\n   *   __classes = new Set<string>();\n   *   static clone(node: ClassesTextNode): ClassesTextNode {\n   *     // The inherited TextNode constructor is used here, so\n   *     // classes is not set by this method.\n   *     return new ClassesTextNode(node.__text, node.__key);\n   *   }\n   *   afterCloneFrom(node: this): void {\n   *     // This calls TextNode.afterCloneFrom and LexicalNode.afterCloneFrom\n   *     // for necessary state updates\n   *     super.afterCloneFrom(node);\n   *     this.__addClasses(node.__classes);\n   *   }\n   *   // This method is a private implementation detail, it is not\n   *   // suitable for the public API because it does not call getWritable\n   *   __addClasses(classNames: Iterable<string>): this {\n   *     for (const className of classNames) {\n   *       this.__classes.add(className);\n   *     }\n   *     return this;\n   *   }\n   *   addClass(...classNames: string[]): this {\n   *     return this.getWritable().__addClasses(classNames);\n   *   }\n   *   removeClass(...classNames: string[]): this {\n   *     const node = this.getWritable();\n   *     for (const className of classNames) {\n   *       this.__classes.delete(className);\n   *     }\n   *     return this;\n   *   }\n   *   getClasses(): Set<string> {\n   *     return this.getLatest().__classes;\n   *   }\n   * }\n   * ```\n   *\n   */\n  afterCloneFrom(prevNode: this) {\n    this.__parent = prevNode.__parent;\n    this.__next = prevNode.__next;\n    this.__prev = prevNode.__prev;\n  }\n\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  static importDOM?: () => DOMConversionMap<any> | null;\n\n  constructor(key?: NodeKey) {\n    this.__type = this.constructor.getType();\n    this.__parent = null;\n    this.__prev = null;\n    this.__next = null;\n    $setNodeKey(this, key);\n\n    if (__DEV__) {\n      if (this.__type !== 'root') {\n        errorOnReadOnly();\n        errorOnTypeKlassMismatch(this.__type, this.constructor);\n      }\n    }\n  }\n  // Getters and Traversers\n\n  /**\n   * Returns the string type of this node.\n   */\n  getType(): string {\n    return this.__type;\n  }\n\n  isInline(): boolean {\n    invariant(\n      false,\n      'LexicalNode: Node %s does not implement .isInline().',\n      this.constructor.name,\n    );\n  }\n\n  /**\n   * Returns true if there is a path between this node and the RootNode, false otherwise.\n   * This is a way of determining if the node is \"attached\" EditorState. Unattached nodes\n   * won't be reconciled and will ultimatelt be cleaned up by the Lexical GC.\n   */\n  isAttached(): boolean {\n    let nodeKey: string | null = this.__key;\n    while (nodeKey !== null) {\n      if (nodeKey === 'root') {\n        return true;\n      }\n\n      const node: LexicalNode | null = $getNodeByKey(nodeKey);\n\n      if (node === null) {\n        break;\n      }\n      nodeKey = node.__parent;\n    }\n    return false;\n  }\n\n  /**\n   * Returns true if this node is contained within the provided Selection., false otherwise.\n   * Relies on the algorithms implemented in {@link BaseSelection.getNodes} to determine\n   * what's included.\n   *\n   * @param selection - The selection that we want to determine if the node is in.\n   */\n  isSelected(selection?: null | BaseSelection): boolean {\n    const targetSelection = selection || $getSelection();\n    if (targetSelection == null) {\n      return false;\n    }\n\n    const isSelected = targetSelection\n      .getNodes()\n      .some((n) => n.__key === this.__key);\n\n    if ($isTextNode(this)) {\n      return isSelected;\n    }\n    // For inline images inside of element nodes.\n    // Without this change the image will be selected if the cursor is before or after it.\n    const isElementRangeSelection =\n      $isRangeSelection(targetSelection) &&\n      targetSelection.anchor.type === 'element' &&\n      targetSelection.focus.type === 'element';\n\n    if (isElementRangeSelection) {\n      if (targetSelection.isCollapsed()) {\n        return false;\n      }\n\n      const parentNode = this.getParent();\n      if ($isDecoratorNode(this) && this.isInline() && parentNode) {\n        const firstPoint = targetSelection.isBackward()\n          ? targetSelection.focus\n          : targetSelection.anchor;\n        const firstElement = firstPoint.getNode() as ElementNode;\n        if (\n          firstPoint.offset === firstElement.getChildrenSize() &&\n          firstElement.is(parentNode) &&\n          firstElement.getLastChildOrThrow().is(this)\n        ) {\n          return false;\n        }\n      }\n    }\n    return isSelected;\n  }\n\n    /**\n     * Indicate if this node should be selected directly instead of the default\n     * where the selection would descend to the nearest initial child element.\n     */\n  shouldSelectDirectly(): boolean {\n      return false;\n  }\n\n  /**\n   * Returns this nodes key.\n   */\n  getKey(): NodeKey {\n    // Key is stable between copies\n    return this.__key;\n  }\n\n  /**\n   * Returns the zero-based index of this node within the parent.\n   */\n  getIndexWithinParent(): number {\n    const parent = this.getParent();\n    if (parent === null) {\n      return -1;\n    }\n    let node = parent.getFirstChild();\n    let index = 0;\n    while (node !== null) {\n      if (this.is(node)) {\n        return index;\n      }\n      index++;\n      node = node.getNextSibling();\n    }\n    return -1;\n  }\n\n  /**\n   * Returns the parent of this node, or null if none is found.\n   */\n  getParent<T extends ElementNode>(): T | null {\n    const parent = this.getLatest().__parent;\n    if (parent === null) {\n      return null;\n    }\n    return $getNodeByKey<T>(parent);\n  }\n\n  /**\n   * Returns the parent of this node, or throws if none is found.\n   */\n  getParentOrThrow<T extends ElementNode>(): T {\n    const parent = this.getParent<T>();\n    if (parent === null) {\n      invariant(false, 'Expected node %s to have a parent.', this.__key);\n    }\n    return parent;\n  }\n\n  /**\n   * Returns the highest (in the EditorState tree)\n   * non-root ancestor of this node, or null if none is found. See {@link lexical!$isRootOrShadowRoot}\n   * for more information on which Elements comprise \"roots\".\n   */\n  getTopLevelElement(): ElementNode | DecoratorNode<unknown> | null {\n    let node: ElementNode | this | null = this;\n    while (node !== null) {\n      const parent: ElementNode | null = node.getParent();\n      if ($isRootOrShadowRoot(parent)) {\n        invariant(\n          $isElementNode(node) || (node === this && $isDecoratorNode(node)),\n          'Children of root nodes must be elements or decorators',\n        );\n        return node;\n      }\n      node = parent;\n    }\n    return null;\n  }\n\n  /**\n   * Returns the highest (in the EditorState tree)\n   * non-root ancestor of this node, or throws if none is found. See {@link lexical!$isRootOrShadowRoot}\n   * for more information on which Elements comprise \"roots\".\n   */\n  getTopLevelElementOrThrow(): ElementNode | DecoratorNode<unknown> {\n    const parent = this.getTopLevelElement();\n    if (parent === null) {\n      invariant(\n        false,\n        'Expected node %s to have a top parent element.',\n        this.__key,\n      );\n    }\n    return parent;\n  }\n\n  /**\n   * Returns a list of the every ancestor of this node,\n   * all the way up to the RootNode.\n   *\n   */\n  getParents(): Array<ElementNode> {\n    const parents: Array<ElementNode> = [];\n    let node = this.getParent();\n    while (node !== null) {\n      parents.push(node);\n      node = node.getParent();\n    }\n    return parents;\n  }\n\n  /**\n   * Returns a list of the keys of every ancestor of this node,\n   * all the way up to the RootNode.\n   *\n   */\n  getParentKeys(): Array<NodeKey> {\n    const parents = [];\n    let node = this.getParent();\n    while (node !== null) {\n      parents.push(node.__key);\n      node = node.getParent();\n    }\n    return parents;\n  }\n\n  /**\n   * Returns the \"previous\" siblings - that is, the node that comes\n   * before this one in the same parent.\n   *\n   */\n  getPreviousSibling<T extends LexicalNode>(): T | null {\n    const self = this.getLatest();\n    const prevKey = self.__prev;\n    return prevKey === null ? null : $getNodeByKey<T>(prevKey);\n  }\n\n  /**\n   * Returns the \"previous\" siblings - that is, the nodes that come between\n   * this one and the first child of it's parent, inclusive.\n   *\n   */\n  getPreviousSiblings<T extends LexicalNode>(): Array<T> {\n    const siblings: Array<T> = [];\n    const parent = this.getParent();\n    if (parent === null) {\n      return siblings;\n    }\n    let node: null | T = parent.getFirstChild();\n    while (node !== null) {\n      if (node.is(this)) {\n        break;\n      }\n      siblings.push(node);\n      node = node.getNextSibling();\n    }\n    return siblings;\n  }\n\n  /**\n   * Returns the \"next\" siblings - that is, the node that comes\n   * after this one in the same parent\n   *\n   */\n  getNextSibling<T extends LexicalNode>(): T | null {\n    const self = this.getLatest();\n    const nextKey = self.__next;\n    return nextKey === null ? null : $getNodeByKey<T>(nextKey);\n  }\n\n  /**\n   * Returns all \"next\" siblings - that is, the nodes that come between this\n   * one and the last child of it's parent, inclusive.\n   *\n   */\n  getNextSiblings<T extends LexicalNode>(): Array<T> {\n    const siblings: Array<T> = [];\n    let node: null | T = this.getNextSibling();\n    while (node !== null) {\n      siblings.push(node);\n      node = node.getNextSibling();\n    }\n    return siblings;\n  }\n\n  /**\n   * Returns the closest common ancestor of this node and the provided one or null\n   * if one cannot be found.\n   *\n   * @param node - the other node to find the common ancestor of.\n   */\n  getCommonAncestor<T extends ElementNode = ElementNode>(\n    node: LexicalNode,\n  ): T | null {\n    const a = this.getParents();\n    const b = node.getParents();\n    if ($isElementNode(this)) {\n      a.unshift(this);\n    }\n    if ($isElementNode(node)) {\n      b.unshift(node);\n    }\n    const aLength = a.length;\n    const bLength = b.length;\n    if (aLength === 0 || bLength === 0 || a[aLength - 1] !== b[bLength - 1]) {\n      return null;\n    }\n    const bSet = new Set(b);\n    for (let i = 0; i < aLength; i++) {\n      const ancestor = a[i] as T;\n      if (bSet.has(ancestor)) {\n        return ancestor;\n      }\n    }\n    return null;\n  }\n\n  /**\n   * Returns true if the provided node is the exact same one as this node, from Lexical's perspective.\n   * Always use this instead of referential equality.\n   *\n   * @param object - the node to perform the equality comparison on.\n   */\n  is(object: LexicalNode | null | undefined): boolean {\n    if (object == null) {\n      return false;\n    }\n    return this.__key === object.__key;\n  }\n\n  /**\n   * Returns true if this node logical precedes the target node in the editor state.\n   *\n   * @param targetNode - the node we're testing to see if it's after this one.\n   */\n  isBefore(targetNode: LexicalNode): boolean {\n    if (this === targetNode) {\n      return false;\n    }\n    if (targetNode.isParentOf(this)) {\n      return true;\n    }\n    if (this.isParentOf(targetNode)) {\n      return false;\n    }\n    const commonAncestor = this.getCommonAncestor(targetNode);\n    let indexA = 0;\n    let indexB = 0;\n    let node: this | ElementNode | LexicalNode = this;\n    while (true) {\n      const parent: ElementNode = node.getParentOrThrow();\n      if (parent === commonAncestor) {\n        indexA = node.getIndexWithinParent();\n        break;\n      }\n      node = parent;\n    }\n    node = targetNode;\n    while (true) {\n      const parent: ElementNode = node.getParentOrThrow();\n      if (parent === commonAncestor) {\n        indexB = node.getIndexWithinParent();\n        break;\n      }\n      node = parent;\n    }\n    return indexA < indexB;\n  }\n\n  /**\n   * Returns true if this node is the parent of the target node, false otherwise.\n   *\n   * @param targetNode - the would-be child node.\n   */\n  isParentOf(targetNode: LexicalNode): boolean {\n    const key = this.__key;\n    if (key === targetNode.__key) {\n      return false;\n    }\n    let node: ElementNode | LexicalNode | null = targetNode;\n    while (node !== null) {\n      if (node.__key === key) {\n        return true;\n      }\n      node = node.getParent();\n    }\n    return false;\n  }\n\n  // TO-DO: this function can be simplified a lot\n  /**\n   * Returns a list of nodes that are between this node and\n   * the target node in the EditorState.\n   *\n   * @param targetNode - the node that marks the other end of the range of nodes to be returned.\n   */\n  getNodesBetween(targetNode: LexicalNode): Array<LexicalNode> {\n    const isBefore = this.isBefore(targetNode);\n    const nodes = [];\n    const visited = new Set();\n    let node: LexicalNode | this | null = this;\n    while (true) {\n      if (node === null) {\n        break;\n      }\n      const key = node.__key;\n      if (!visited.has(key)) {\n        visited.add(key);\n        nodes.push(node);\n      }\n      if (node === targetNode) {\n        break;\n      }\n      const child: LexicalNode | null = $isElementNode(node)\n        ? isBefore\n          ? node.getFirstChild()\n          : node.getLastChild()\n        : null;\n      if (child !== null) {\n        node = child;\n        continue;\n      }\n      const nextSibling: LexicalNode | null = isBefore\n        ? node.getNextSibling()\n        : node.getPreviousSibling();\n      if (nextSibling !== null) {\n        node = nextSibling;\n        continue;\n      }\n      const parent: LexicalNode | null = node.getParentOrThrow();\n      if (!visited.has(parent.__key)) {\n        nodes.push(parent);\n      }\n      if (parent === targetNode) {\n        break;\n      }\n      let parentSibling = null;\n      let ancestor: LexicalNode | null = parent;\n      do {\n        if (ancestor === null) {\n          invariant(false, 'getNodesBetween: ancestor is null');\n        }\n        parentSibling = isBefore\n          ? ancestor.getNextSibling()\n          : ancestor.getPreviousSibling();\n        ancestor = ancestor.getParent();\n        if (ancestor !== null) {\n          if (parentSibling === null && !visited.has(ancestor.__key)) {\n            nodes.push(ancestor);\n          }\n        } else {\n          break;\n        }\n      } while (parentSibling === null);\n      node = parentSibling;\n    }\n    if (!isBefore) {\n      nodes.reverse();\n    }\n    return nodes;\n  }\n\n  /**\n   * Returns true if this node has been marked dirty during this update cycle.\n   *\n   */\n  isDirty(): boolean {\n    const editor = getActiveEditor();\n    const dirtyLeaves = editor._dirtyLeaves;\n    return dirtyLeaves !== null && dirtyLeaves.has(this.__key);\n  }\n\n  /**\n   * Returns the latest version of the node from the active EditorState.\n   * This is used to avoid getting values from stale node references.\n   *\n   */\n  getLatest(): this {\n    const latest = $getNodeByKey<this>(this.__key);\n    if (latest === null) {\n      invariant(\n        false,\n        'Lexical node does not exist in active editor state. Avoid using the same node references between nested closures from editorState.read/editor.update.',\n      );\n    }\n    return latest;\n  }\n\n  /**\n   * Returns a mutable version of the node using {@link $cloneWithProperties}\n   * if necessary. Will throw an error if called outside of a Lexical Editor\n   * {@link LexicalEditor.update} callback.\n   *\n   */\n  getWritable(): this {\n    errorOnReadOnly();\n    const editorState = getActiveEditorState();\n    const editor = getActiveEditor();\n    const nodeMap = editorState._nodeMap;\n    const key = this.__key;\n    // Ensure we get the latest node from pending state\n    const latestNode = this.getLatest();\n    const cloneNotNeeded = editor._cloneNotNeeded;\n    const selection = $getSelection();\n    if (selection !== null) {\n      selection.setCachedNodes(null);\n    }\n    if (cloneNotNeeded.has(key)) {\n      // Transforms clear the dirty node set on each iteration to keep track on newly dirty nodes\n      internalMarkNodeAsDirty(latestNode);\n      return latestNode;\n    }\n    const mutableNode = $cloneWithProperties(latestNode);\n    cloneNotNeeded.add(key);\n    internalMarkNodeAsDirty(mutableNode);\n    // Update reference in node map\n    nodeMap.set(key, mutableNode);\n\n    return mutableNode;\n  }\n\n  /**\n   * Returns the text content of the node. Override this for\n   * custom nodes that should have a representation in plain text\n   * format (for copy + paste, for example)\n   *\n   */\n  getTextContent(): string {\n    return '';\n  }\n\n  /**\n   * Returns the length of the string produced by calling getTextContent on this node.\n   *\n   */\n  getTextContentSize(): number {\n    return this.getTextContent().length;\n  }\n\n  // View\n\n  /**\n   * Called during the reconciliation process to determine which nodes\n   * to insert into the DOM for this Lexical Node.\n   *\n   * This method must return exactly one HTMLElement. Nested elements are not supported.\n   *\n   * Do not attempt to update the Lexical EditorState during this phase of the update lifecyle.\n   *\n   * @param _config - allows access to things like the EditorTheme (to apply classes) during reconciliation.\n   * @param _editor - allows access to the editor for context during reconciliation.\n   *\n   * */\n  createDOM(_config: EditorConfig, _editor: LexicalEditor): HTMLElement {\n    invariant(false, 'createDOM: base method not extended');\n  }\n\n  /**\n   * Called when a node changes and should update the DOM\n   * in whatever way is necessary to make it align with any changes that might\n   * have happened during the update.\n   *\n   * Returning \"true\" here will cause lexical to unmount and recreate the DOM node\n   * (by calling createDOM). You would need to do this if the element tag changes,\n   * for instance.\n   *\n   * */\n  updateDOM(\n    _prevNode: unknown,\n    _dom: HTMLElement,\n    _config: EditorConfig,\n  ): boolean {\n    invariant(false, 'updateDOM: base method not extended');\n  }\n\n  /**\n   * Controls how the this node is serialized to HTML. This is important for\n   * copy and paste between Lexical and non-Lexical editors, or Lexical editors with different namespaces,\n   * in which case the primary transfer format is HTML. It's also important if you're serializing\n   * to HTML for any other reason via {@link @lexical/html!$generateHtmlFromNodes}. You could\n   * also use this method to build your own HTML renderer.\n   *\n   * */\n  exportDOM(editor: LexicalEditor): DOMExportOutput {\n    const element = this.createDOM(editor._config, editor);\n    return {element};\n  }\n\n  /**\n   * Controls how the this node is serialized to JSON. This is important for\n   * copy and paste between Lexical editors sharing the same namespace. It's also important\n   * if you're serializing to JSON for persistent storage somewhere.\n   * See [Serialization & Deserialization](https://lexical.dev/docs/concepts/serialization#lexical---html).\n   *\n   * */\n  exportJSON(): SerializedLexicalNode {\n    invariant(false, 'exportJSON: base method not extended');\n  }\n\n  /**\n   * Controls how the this node is deserialized from JSON. This is usually boilerplate,\n   * but provides an abstraction between the node implementation and serialized interface that can\n   * be important if you ever make breaking changes to a node schema (by adding or removing properties).\n   * See [Serialization & Deserialization](https://lexical.dev/docs/concepts/serialization#lexical---html).\n   *\n   * */\n  static importJSON(_serializedNode: SerializedLexicalNode): LexicalNode {\n    invariant(\n      false,\n      'LexicalNode: Node %s does not implement .importJSON().',\n      this.name,\n    );\n  }\n  /**\n   * @experimental\n   *\n   * Registers the returned function as a transform on the node during\n   * Editor initialization. Most such use cases should be addressed via\n   * the {@link LexicalEditor.registerNodeTransform} API.\n   *\n   * Experimental - use at your own risk.\n   */\n  static transform(): ((node: LexicalNode) => void) | null {\n    return null;\n  }\n\n  // Setters and mutators\n\n  /**\n   * Removes this LexicalNode from the EditorState. If the node isn't re-inserted\n   * somewhere, the Lexical garbage collector will eventually clean it up.\n   *\n   * @param preserveEmptyParent - If falsy, the node's parent will be removed if\n   * it's empty after the removal operation. This is the default behavior, subject to\n   * other node heuristics such as {@link ElementNode#canBeEmpty}\n   * */\n  remove(preserveEmptyParent?: boolean): void {\n    $removeNode(this, true, preserveEmptyParent);\n  }\n\n  /**\n   * Replaces this LexicalNode with the provided node, optionally transferring the children\n   * of the replaced node to the replacing node.\n   *\n   * @param replaceWith - The node to replace this one with.\n   * @param includeChildren - Whether or not to transfer the children of this node to the replacing node.\n   * */\n  replace<N extends LexicalNode>(replaceWith: N, includeChildren?: boolean): N {\n    errorOnReadOnly();\n    let selection = $getSelection();\n    if (selection !== null) {\n      selection = selection.clone();\n    }\n    errorOnInsertTextNodeOnRoot(this, replaceWith);\n    const self = this.getLatest();\n    const toReplaceKey = this.__key;\n    const key = replaceWith.__key;\n    const writableReplaceWith = replaceWith.getWritable();\n    const writableParent = this.getParentOrThrow().getWritable();\n    const size = writableParent.__size;\n    removeFromParent(writableReplaceWith);\n    const prevSibling = self.getPreviousSibling();\n    const nextSibling = self.getNextSibling();\n    const prevKey = self.__prev;\n    const nextKey = self.__next;\n    const parentKey = self.__parent;\n    $removeNode(self, false, true);\n\n    if (prevSibling === null) {\n      writableParent.__first = key;\n    } else {\n      const writablePrevSibling = prevSibling.getWritable();\n      writablePrevSibling.__next = key;\n    }\n    writableReplaceWith.__prev = prevKey;\n    if (nextSibling === null) {\n      writableParent.__last = key;\n    } else {\n      const writableNextSibling = nextSibling.getWritable();\n      writableNextSibling.__prev = key;\n    }\n    writableReplaceWith.__next = nextKey;\n    writableReplaceWith.__parent = parentKey;\n    writableParent.__size = size;\n    if (includeChildren) {\n      invariant(\n        $isElementNode(this) && $isElementNode(writableReplaceWith),\n        'includeChildren should only be true for ElementNodes',\n      );\n      this.getChildren().forEach((child: LexicalNode) => {\n        writableReplaceWith.append(child);\n      });\n    }\n    if ($isRangeSelection(selection)) {\n      $setSelection(selection);\n      const anchor = selection.anchor;\n      const focus = selection.focus;\n      if (anchor.key === toReplaceKey) {\n        $moveSelectionPointToEnd(anchor, writableReplaceWith);\n      }\n      if (focus.key === toReplaceKey) {\n        $moveSelectionPointToEnd(focus, writableReplaceWith);\n      }\n    }\n    if ($getCompositionKey() === toReplaceKey) {\n      $setCompositionKey(key);\n    }\n    return writableReplaceWith;\n  }\n\n  /**\n   * Inserts a node after this LexicalNode (as the next sibling).\n   *\n   * @param nodeToInsert - The node to insert after this one.\n   * @param restoreSelection - Whether or not to attempt to resolve the\n   * selection to the appropriate place after the operation is complete.\n   * */\n  insertAfter(nodeToInsert: LexicalNode, restoreSelection = true): LexicalNode {\n    errorOnReadOnly();\n    errorOnInsertTextNodeOnRoot(this, nodeToInsert);\n    const writableSelf = this.getWritable();\n    const writableNodeToInsert = nodeToInsert.getWritable();\n    const oldParent = writableNodeToInsert.getParent();\n    const selection = $getSelection();\n    let elementAnchorSelectionOnNode = false;\n    let elementFocusSelectionOnNode = false;\n    if (oldParent !== null) {\n      // TODO: this is O(n), can we improve?\n      const oldIndex = nodeToInsert.getIndexWithinParent();\n      removeFromParent(writableNodeToInsert);\n      if ($isRangeSelection(selection)) {\n        const oldParentKey = oldParent.__key;\n        const anchor = selection.anchor;\n        const focus = selection.focus;\n        elementAnchorSelectionOnNode =\n          anchor.type === 'element' &&\n          anchor.key === oldParentKey &&\n          anchor.offset === oldIndex + 1;\n        elementFocusSelectionOnNode =\n          focus.type === 'element' &&\n          focus.key === oldParentKey &&\n          focus.offset === oldIndex + 1;\n      }\n    }\n    const nextSibling = this.getNextSibling();\n    const writableParent = this.getParentOrThrow().getWritable();\n    const insertKey = writableNodeToInsert.__key;\n    const nextKey = writableSelf.__next;\n    if (nextSibling === null) {\n      writableParent.__last = insertKey;\n    } else {\n      const writableNextSibling = nextSibling.getWritable();\n      writableNextSibling.__prev = insertKey;\n    }\n    writableParent.__size++;\n    writableSelf.__next = insertKey;\n    writableNodeToInsert.__next = nextKey;\n    writableNodeToInsert.__prev = writableSelf.__key;\n    writableNodeToInsert.__parent = writableSelf.__parent;\n    if (restoreSelection && $isRangeSelection(selection)) {\n      const index = this.getIndexWithinParent();\n      $updateElementSelectionOnCreateDeleteNode(\n        selection,\n        writableParent,\n        index + 1,\n      );\n      const writableParentKey = writableParent.__key;\n      if (elementAnchorSelectionOnNode) {\n        selection.anchor.set(writableParentKey, index + 2, 'element');\n      }\n      if (elementFocusSelectionOnNode) {\n        selection.focus.set(writableParentKey, index + 2, 'element');\n      }\n    }\n    return nodeToInsert;\n  }\n\n  /**\n   * Inserts a node before this LexicalNode (as the previous sibling).\n   *\n   * @param nodeToInsert - The node to insert before this one.\n   * @param restoreSelection - Whether or not to attempt to resolve the\n   * selection to the appropriate place after the operation is complete.\n   * */\n  insertBefore(\n    nodeToInsert: LexicalNode,\n    restoreSelection = true,\n  ): LexicalNode {\n    errorOnReadOnly();\n    errorOnInsertTextNodeOnRoot(this, nodeToInsert);\n    const writableSelf = this.getWritable();\n    const writableNodeToInsert = nodeToInsert.getWritable();\n    const insertKey = writableNodeToInsert.__key;\n    removeFromParent(writableNodeToInsert);\n    const prevSibling = this.getPreviousSibling();\n    const writableParent = this.getParentOrThrow().getWritable();\n    const prevKey = writableSelf.__prev;\n    // TODO: this is O(n), can we improve?\n    const index = this.getIndexWithinParent();\n    if (prevSibling === null) {\n      writableParent.__first = insertKey;\n    } else {\n      const writablePrevSibling = prevSibling.getWritable();\n      writablePrevSibling.__next = insertKey;\n    }\n    writableParent.__size++;\n    writableSelf.__prev = insertKey;\n    writableNodeToInsert.__prev = prevKey;\n    writableNodeToInsert.__next = writableSelf.__key;\n    writableNodeToInsert.__parent = writableSelf.__parent;\n    const selection = $getSelection();\n    if (restoreSelection && $isRangeSelection(selection)) {\n      const parent = this.getParentOrThrow();\n      $updateElementSelectionOnCreateDeleteNode(selection, parent, index);\n    }\n    return nodeToInsert;\n  }\n\n  /**\n   * Whether or not this node has a required parent. Used during copy + paste operations\n   * to normalize nodes that would otherwise be orphaned. For example, ListItemNodes without\n   * a ListNode parent or TextNodes with a ParagraphNode parent.\n   *\n   * */\n  isParentRequired(): boolean {\n    return false;\n  }\n\n  /**\n   * The creation logic for any required parent. Should be implemented if {@link isParentRequired} returns true.\n   *\n   * */\n  createParentElementNode(): ElementNode {\n    return $createParagraphNode();\n  }\n\n  selectStart(): RangeSelection {\n    return this.selectPrevious();\n  }\n\n  selectEnd(): RangeSelection {\n    return this.selectNext(0, 0);\n  }\n\n  /**\n   * Moves selection to the previous sibling of this node, at the specified offsets.\n   *\n   * @param anchorOffset - The anchor offset for selection.\n   * @param focusOffset -  The focus offset for selection\n   * */\n  selectPrevious(anchorOffset?: number, focusOffset?: number): RangeSelection {\n    errorOnReadOnly();\n    const prevSibling = this.getPreviousSibling();\n    const parent = this.getParentOrThrow();\n    if (prevSibling === null) {\n      return parent.select(0, 0);\n    }\n    if ($isElementNode(prevSibling)) {\n      return prevSibling.select();\n    } else if (!$isTextNode(prevSibling)) {\n      const index = prevSibling.getIndexWithinParent() + 1;\n      return parent.select(index, index);\n    }\n    return prevSibling.select(anchorOffset, focusOffset);\n  }\n\n  /**\n   * Moves selection to the next sibling of this node, at the specified offsets.\n   *\n   * @param anchorOffset - The anchor offset for selection.\n   * @param focusOffset -  The focus offset for selection\n   * */\n  selectNext(anchorOffset?: number, focusOffset?: number): RangeSelection {\n    errorOnReadOnly();\n    const nextSibling = this.getNextSibling();\n    const parent = this.getParentOrThrow();\n    if (nextSibling === null) {\n      return parent.select();\n    }\n    if ($isElementNode(nextSibling)) {\n      return nextSibling.select(0, 0);\n    } else if (!$isTextNode(nextSibling)) {\n      const index = nextSibling.getIndexWithinParent();\n      return parent.select(index, index);\n    }\n    return nextSibling.select(anchorOffset, focusOffset);\n  }\n\n  /**\n   * Marks a node dirty, triggering transforms and\n   * forcing it to be reconciled during the update cycle.\n   *\n   * */\n  markDirty(): void {\n    this.getWritable();\n  }\n\n  /**\n   * Insert the DOM of this node into that of the parent.\n   * Allows this node to implement custom DOM attachment logic.\n   * Boolean result indicates if the insertion was handled by the function.\n   * A true return value prevents default insertion logic from taking place.\n   */\n  insertDOMIntoParent(nodeDOM: HTMLElement, parentDOM: HTMLElement): boolean {\n    return false;\n  }\n}\n\nfunction errorOnTypeKlassMismatch(\n  type: string,\n  klass: Klass<LexicalNode>,\n): void {\n  const registeredNode = getActiveEditor()._nodes.get(type);\n  // Common error - split in its own invariant\n  if (registeredNode === undefined) {\n    invariant(\n      false,\n      'Create node: Attempted to create node %s that was not configured to be used on the editor.',\n      klass.name,\n    );\n  }\n  const editorKlass = registeredNode.klass;\n  if (editorKlass !== klass) {\n    invariant(\n      false,\n      'Create node: Type %s in node %s does not match registered node %s with the same type',\n      type,\n      klass.name,\n      editorKlass.name,\n    );\n  }\n}\n\n/**\n * Insert a series of nodes after this LexicalNode (as next siblings)\n *\n * @param firstToInsert - The first node to insert after this one.\n * @param lastToInsert - The last node to insert after this one. Must be a\n * later sibling of FirstNode. If not provided, it will be its last sibling.\n */\nexport function insertRangeAfter(\n  node: LexicalNode,\n  firstToInsert: LexicalNode,\n  lastToInsert?: LexicalNode,\n) {\n  const lastToInsert2 =\n    lastToInsert || firstToInsert.getParentOrThrow().getLastChild()!;\n  let current = firstToInsert;\n  const nodesToInsert = [firstToInsert];\n  while (current !== lastToInsert2) {\n    if (!current.getNextSibling()) {\n      invariant(\n        false,\n        'insertRangeAfter: lastToInsert must be a later sibling of firstToInsert',\n      );\n    }\n    current = current.getNextSibling()!;\n    nodesToInsert.push(current);\n  }\n\n  let currentNode: LexicalNode = node;\n  for (const nodeToInsert of nodesToInsert) {\n    currentNode = currentNode.insertAfter(nodeToInsert);\n  }\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/LexicalNormalization.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {RangeSelection, TextNode} from '.';\nimport type {PointType} from './LexicalSelection';\n\nimport {$isElementNode, $isTextNode} from '.';\nimport {getActiveEditor} from './LexicalUpdates';\n\nfunction $canSimpleTextNodesBeMerged(\n  node1: TextNode,\n  node2: TextNode,\n): boolean {\n  const node1Mode = node1.__mode;\n  const node1Format = node1.__format;\n  const node1Style = node1.__style;\n  const node2Mode = node2.__mode;\n  const node2Format = node2.__format;\n  const node2Style = node2.__style;\n  return (\n    (node1Mode === null || node1Mode === node2Mode) &&\n    (node1Format === null || node1Format === node2Format) &&\n    (node1Style === null || node1Style === node2Style)\n  );\n}\n\nfunction $mergeTextNodes(node1: TextNode, node2: TextNode): TextNode {\n  const writableNode1 = node1.mergeWithSibling(node2);\n\n  const normalizedNodes = getActiveEditor()._normalizedNodes;\n\n  normalizedNodes.add(node1.__key);\n  normalizedNodes.add(node2.__key);\n  return writableNode1;\n}\n\nexport function $normalizeTextNode(textNode: TextNode): void {\n  let node = textNode;\n\n  if (node.__text === '' && node.isSimpleText() && !node.isUnmergeable()) {\n    node.remove();\n    return;\n  }\n\n  // Backward\n  let previousNode;\n\n  while (\n    (previousNode = node.getPreviousSibling()) !== null &&\n    $isTextNode(previousNode) &&\n    previousNode.isSimpleText() &&\n    !previousNode.isUnmergeable()\n  ) {\n    if (previousNode.__text === '') {\n      previousNode.remove();\n    } else if ($canSimpleTextNodesBeMerged(previousNode, node)) {\n      node = $mergeTextNodes(previousNode, node);\n      break;\n    } else {\n      break;\n    }\n  }\n\n  // Forward\n  let nextNode;\n\n  while (\n    (nextNode = node.getNextSibling()) !== null &&\n    $isTextNode(nextNode) &&\n    nextNode.isSimpleText() &&\n    !nextNode.isUnmergeable()\n  ) {\n    if (nextNode.__text === '') {\n      nextNode.remove();\n    } else if ($canSimpleTextNodesBeMerged(node, nextNode)) {\n      node = $mergeTextNodes(node, nextNode);\n      break;\n    } else {\n      break;\n    }\n  }\n}\n\nexport function $normalizeSelection(selection: RangeSelection): RangeSelection {\n  $normalizePoint(selection.anchor);\n  $normalizePoint(selection.focus);\n  return selection;\n}\n\nfunction $normalizePoint(point: PointType): void {\n  while (point.type === 'element') {\n    const node = point.getNode();\n    const offset = point.offset;\n    let nextNode;\n    let nextOffsetAtEnd;\n    if (offset === node.getChildrenSize()) {\n      nextNode = node.getChildAtIndex(offset - 1);\n      nextOffsetAtEnd = true;\n    } else {\n      nextNode = node.getChildAtIndex(offset);\n      nextOffsetAtEnd = false;\n    }\n    if ($isTextNode(nextNode)) {\n      point.set(\n        nextNode.__key,\n        nextOffsetAtEnd ? nextNode.getTextContentSize() : 0,\n        'text',\n      );\n      break;\n    } else if (!$isElementNode(nextNode)) {\n      break;\n    }\n    point.set(\n      nextNode.__key,\n      nextOffsetAtEnd ? nextNode.getChildrenSize() : 0,\n      'element',\n    );\n  }\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/LexicalReconciler.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {\n  EditorConfig,\n  LexicalEditor,\n  MutatedNodes,\n  MutationListeners,\n  RegisteredNodes,\n} from './LexicalEditor';\nimport type {NodeKey, NodeMap} from './LexicalNode';\nimport type {ElementNode} from './nodes/LexicalElementNode';\n\nimport invariant from 'lexical/shared/invariant';\n\nimport {\n  $isDecoratorNode,\n  $isElementNode,\n  $isLineBreakNode,\n  $isParagraphNode,\n  $isRootNode,\n  $isTextNode,\n} from '.';\nimport {\n  DOUBLE_LINE_BREAK,\n  FULL_RECONCILE,\n\n\n\n\n\n\n} from './LexicalConstants';\nimport {EditorState} from './LexicalEditorState';\nimport {\n  $textContentRequiresDoubleLinebreakAtEnd,\n  cloneDecorators,\n  getElementByKeyOrThrow,\n  setMutatedNode,\n} from './LexicalUtils';\n\ntype IntentionallyMarkedAsDirtyElement = boolean;\n\nlet subTreeTextContent = '';\nlet subTreeTextFormat: number | null = null;\nlet subTreeTextStyle: string = '';\nlet editorTextContent = '';\nlet activeEditorConfig: EditorConfig;\nlet activeEditor: LexicalEditor;\nlet activeEditorNodes: RegisteredNodes;\nlet treatAllNodesAsDirty = false;\nlet activeEditorStateReadOnly = false;\nlet activeMutationListeners: MutationListeners;\nlet activeDirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>;\nlet activeDirtyLeaves: Set<NodeKey>;\nlet activePrevNodeMap: NodeMap;\nlet activeNextNodeMap: NodeMap;\nlet activePrevKeyToDOMMap: Map<NodeKey, HTMLElement>;\nlet mutatedNodes: MutatedNodes;\n\nfunction destroyNode(key: NodeKey, parentDOM: null | HTMLElement): void {\n  const node = activePrevNodeMap.get(key);\n\n  if (parentDOM !== null) {\n    const dom = getPrevElementByKeyOrThrow(key);\n    if (dom.parentNode === parentDOM) {\n      parentDOM.removeChild(dom);\n    }\n  }\n\n  // This logic is really important, otherwise we will leak DOM nodes\n  // when their corresponding LexicalNodes are removed from the editor state.\n  if (!activeNextNodeMap.has(key)) {\n    activeEditor._keyToDOMMap.delete(key);\n  }\n\n  if ($isElementNode(node)) {\n    const children = createChildrenArray(node, activePrevNodeMap);\n    destroyChildren(children, 0, children.length - 1, null);\n  }\n\n  if (node !== undefined) {\n    setMutatedNode(\n      mutatedNodes,\n      activeEditorNodes,\n      activeMutationListeners,\n      node,\n      'destroyed',\n    );\n  }\n}\n\nfunction destroyChildren(\n  children: Array<NodeKey>,\n  _startIndex: number,\n  endIndex: number,\n  dom: null | HTMLElement,\n): void {\n  let startIndex = _startIndex;\n\n  for (; startIndex <= endIndex; ++startIndex) {\n    const child = children[startIndex];\n\n    if (child !== undefined) {\n      destroyNode(child, dom);\n    }\n  }\n}\n\nfunction setTextAlign(domStyle: CSSStyleDeclaration, value: string): void {\n  domStyle.setProperty('text-align', value);\n}\n\nfunction $createNode(\n  key: NodeKey,\n  parentDOM: null | HTMLElement,\n  insertDOM: null | Node,\n): HTMLElement {\n  const node = activeNextNodeMap.get(key);\n\n  if (node === undefined) {\n    invariant(false, 'createNode: node does not exist in nodeMap');\n  }\n  const dom = node.createDOM(activeEditorConfig, activeEditor);\n  storeDOMWithKey(key, dom, activeEditor);\n\n  // This helps preserve the text, and stops spell check tools from\n  // merging or break the spans (which happens if they are missing\n  // this attribute).\n  if ($isTextNode(node)) {\n    dom.setAttribute('data-lexical-text', 'true');\n  } else if ($isDecoratorNode(node)) {\n    dom.setAttribute('data-lexical-decorator', 'true');\n  }\n\n  if ($isElementNode(node)) {\n    const childrenSize = node.__size;\n\n    if (childrenSize !== 0) {\n      const endIndex = childrenSize - 1;\n      const children = createChildrenArray(node, activeNextNodeMap);\n      $createChildren(children, node, 0, endIndex, dom, null);\n    }\n\n    if (!node.isInline()) {\n      reconcileElementTerminatingLineBreak(null, node, dom);\n    }\n    if ($textContentRequiresDoubleLinebreakAtEnd(node)) {\n      subTreeTextContent += DOUBLE_LINE_BREAK;\n      editorTextContent += DOUBLE_LINE_BREAK;\n    }\n  } else {\n    const text = node.getTextContent();\n\n    if ($isDecoratorNode(node)) {\n      const decorator = node.decorate(activeEditor, activeEditorConfig);\n\n      if (decorator !== null) {\n        reconcileDecorator(key, decorator);\n      }\n      // Decorators are always non editable\n      dom.contentEditable = 'false';\n    }\n    subTreeTextContent += text;\n    editorTextContent += text;\n  }\n\n  if (parentDOM !== null) {\n\n    const inserted = node?.insertDOMIntoParent(dom, parentDOM);\n\n    if (!inserted) {\n      if (insertDOM != null) {\n        parentDOM.insertBefore(dom, insertDOM);\n      } else {\n        // @ts-expect-error: internal field\n        const possibleLineBreak = parentDOM.__lexicalLineBreak;\n\n        if (possibleLineBreak != null) {\n          parentDOM.insertBefore(dom, possibleLineBreak);\n        } else {\n          parentDOM.appendChild(dom);\n        }\n      }\n    }\n  }\n\n  if (__DEV__) {\n    // Freeze the node in DEV to prevent accidental mutations\n    Object.freeze(node);\n  }\n\n  setMutatedNode(\n    mutatedNodes,\n    activeEditorNodes,\n    activeMutationListeners,\n    node,\n    'created',\n  );\n  return dom;\n}\n\nfunction $createChildren(\n  children: Array<NodeKey>,\n  element: ElementNode,\n  _startIndex: number,\n  endIndex: number,\n  dom: null | HTMLElement,\n  insertDOM: null | HTMLElement,\n): void {\n  const previousSubTreeTextContent = subTreeTextContent;\n  subTreeTextContent = '';\n  let startIndex = _startIndex;\n\n  for (; startIndex <= endIndex; ++startIndex) {\n    $createNode(children[startIndex], dom, insertDOM);\n    const node = activeNextNodeMap.get(children[startIndex]);\n    if (node !== null && $isTextNode(node)) {\n      if (subTreeTextFormat === null) {\n        subTreeTextFormat = node.getFormat();\n      }\n      if (subTreeTextStyle === '') {\n        subTreeTextStyle = node.getStyle();\n      }\n    }\n  }\n  if ($textContentRequiresDoubleLinebreakAtEnd(element)) {\n    subTreeTextContent += DOUBLE_LINE_BREAK;\n  }\n  // @ts-expect-error: internal field\n  dom.__lexicalTextContent = subTreeTextContent;\n  subTreeTextContent = previousSubTreeTextContent + subTreeTextContent;\n}\n\nfunction isLastChildLineBreakOrDecorator(\n  childKey: NodeKey,\n  nodeMap: NodeMap,\n): boolean {\n  const node = nodeMap.get(childKey);\n  return $isLineBreakNode(node) || ($isDecoratorNode(node) && node.isInline());\n}\n\n// If we end an element with a LineBreakNode, then we need to add an additional <br>\nfunction reconcileElementTerminatingLineBreak(\n  prevElement: null | ElementNode,\n  nextElement: ElementNode,\n  dom: HTMLElement,\n): void {\n  const prevLineBreak =\n    prevElement !== null &&\n    (prevElement.__size === 0 ||\n      isLastChildLineBreakOrDecorator(\n        prevElement.__last as NodeKey,\n        activePrevNodeMap,\n      ));\n  const nextLineBreak =\n    nextElement.__size === 0 ||\n    isLastChildLineBreakOrDecorator(\n      nextElement.__last as NodeKey,\n      activeNextNodeMap,\n    );\n\n  if (prevLineBreak) {\n    if (!nextLineBreak) {\n      // @ts-expect-error: internal field\n      const element = dom.__lexicalLineBreak;\n\n      if (element != null) {\n        try {\n          dom.removeChild(element);\n        } catch (error) {\n          if (typeof error === 'object' && error != null) {\n            const msg = `${error.toString()} Parent: ${dom.tagName}, child: ${\n              element.tagName\n            }.`;\n            throw new Error(msg);\n          } else {\n            throw error;\n          }\n        }\n      }\n\n      // @ts-expect-error: internal field\n      dom.__lexicalLineBreak = null;\n    }\n  } else if (nextLineBreak) {\n    const element = document.createElement('br');\n    // @ts-expect-error: internal field\n    dom.__lexicalLineBreak = element;\n    dom.appendChild(element);\n  }\n}\n\nfunction reconcileParagraphFormat(element: ElementNode): void {\n  if (\n    $isParagraphNode(element) &&\n    subTreeTextFormat != null &&\n    !activeEditorStateReadOnly\n  ) {\n    element.setTextStyle(subTreeTextStyle);\n  }\n}\n\nfunction reconcileParagraphStyle(element: ElementNode): void {\n  if (\n    $isParagraphNode(element) &&\n    subTreeTextStyle !== '' &&\n    subTreeTextStyle !== element.__textStyle &&\n    !activeEditorStateReadOnly\n  ) {\n    element.setTextStyle(subTreeTextStyle);\n  }\n}\n\nfunction $reconcileChildrenWithDirection(\n  prevElement: ElementNode,\n  nextElement: ElementNode,\n  dom: HTMLElement,\n): void {\n  subTreeTextFormat = null;\n  subTreeTextStyle = '';\n  $reconcileChildren(prevElement, nextElement, dom);\n  reconcileParagraphFormat(nextElement);\n  reconcileParagraphStyle(nextElement);\n}\n\nfunction createChildrenArray(\n  element: ElementNode,\n  nodeMap: NodeMap,\n): Array<NodeKey> {\n  const children = [];\n  let nodeKey = element.__first;\n  while (nodeKey !== null) {\n    const node = nodeMap.get(nodeKey);\n    if (node === undefined) {\n      invariant(false, 'createChildrenArray: node does not exist in nodeMap');\n    }\n    children.push(nodeKey);\n    nodeKey = node.__next;\n  }\n  return children;\n}\n\nfunction $reconcileChildren(\n  prevElement: ElementNode,\n  nextElement: ElementNode,\n  dom: HTMLElement,\n): void {\n  const previousSubTreeTextContent = subTreeTextContent;\n  const prevChildrenSize = prevElement.__size;\n  const nextChildrenSize = nextElement.__size;\n  subTreeTextContent = '';\n\n  if (prevChildrenSize === 1 && nextChildrenSize === 1) {\n    const prevFirstChildKey = prevElement.__first as NodeKey;\n    const nextFrstChildKey = nextElement.__first as NodeKey;\n    if (prevFirstChildKey === nextFrstChildKey) {\n      $reconcileNode(prevFirstChildKey, dom);\n    } else {\n      const lastDOM = getPrevElementByKeyOrThrow(prevFirstChildKey);\n      const replacementDOM = $createNode(nextFrstChildKey, null, null);\n      try {\n        dom.replaceChild(replacementDOM, lastDOM);\n      } catch (error) {\n        if (typeof error === 'object' && error != null) {\n          const msg = `${error.toString()} Parent: ${\n            dom.tagName\n          }, new child: {tag: ${\n            replacementDOM.tagName\n          } key: ${nextFrstChildKey}}, old child: {tag: ${\n            lastDOM.tagName\n          }, key: ${prevFirstChildKey}}.`;\n          throw new Error(msg);\n        } else {\n          throw error;\n        }\n      }\n      destroyNode(prevFirstChildKey, null);\n    }\n    const nextChildNode = activeNextNodeMap.get(nextFrstChildKey);\n    if ($isTextNode(nextChildNode)) {\n      if (subTreeTextFormat === null) {\n        subTreeTextFormat = nextChildNode.getFormat();\n      }\n      if (subTreeTextStyle === '') {\n        subTreeTextStyle = nextChildNode.getStyle();\n      }\n    }\n  } else {\n    const prevChildren = createChildrenArray(prevElement, activePrevNodeMap);\n    const nextChildren = createChildrenArray(nextElement, activeNextNodeMap);\n\n    if (prevChildrenSize === 0) {\n      if (nextChildrenSize !== 0) {\n        $createChildren(\n          nextChildren,\n          nextElement,\n          0,\n          nextChildrenSize - 1,\n          dom,\n          null,\n        );\n      }\n    } else if (nextChildrenSize === 0) {\n      if (prevChildrenSize !== 0) {\n        // @ts-expect-error: internal field\n        const lexicalLineBreak = dom.__lexicalLineBreak;\n        const canUseFastPath = lexicalLineBreak == null;\n        destroyChildren(\n          prevChildren,\n          0,\n          prevChildrenSize - 1,\n          canUseFastPath ? null : dom,\n        );\n\n        if (canUseFastPath) {\n          // Fast path for removing DOM nodes\n          dom.textContent = '';\n        }\n      }\n    } else {\n      $reconcileNodeChildren(\n        nextElement,\n        prevChildren,\n        nextChildren,\n        prevChildrenSize,\n        nextChildrenSize,\n        dom,\n      );\n    }\n  }\n\n  if ($textContentRequiresDoubleLinebreakAtEnd(nextElement)) {\n    subTreeTextContent += DOUBLE_LINE_BREAK;\n  }\n\n  // @ts-expect-error: internal field\n  dom.__lexicalTextContent = subTreeTextContent;\n  subTreeTextContent = previousSubTreeTextContent + subTreeTextContent;\n}\n\nfunction $reconcileNode(\n  key: NodeKey,\n  parentDOM: HTMLElement | null,\n): HTMLElement {\n  const prevNode = activePrevNodeMap.get(key);\n  let nextNode = activeNextNodeMap.get(key);\n\n  if (prevNode === undefined || nextNode === undefined) {\n    invariant(\n      false,\n      'reconcileNode: prevNode or nextNode does not exist in nodeMap',\n    );\n  }\n\n  const isDirty =\n    treatAllNodesAsDirty ||\n    activeDirtyLeaves.has(key) ||\n    activeDirtyElements.has(key);\n  const dom = getElementByKeyOrThrow(activeEditor, key);\n\n  // If the node key points to the same instance in both states\n  // and isn't dirty, we just update the text content cache\n  // and return the existing DOM Node.\n  if (prevNode === nextNode && !isDirty) {\n    if ($isElementNode(prevNode)) {\n      // @ts-expect-error: internal field\n      const previousSubTreeTextContent = dom.__lexicalTextContent;\n\n      if (previousSubTreeTextContent !== undefined) {\n        subTreeTextContent += previousSubTreeTextContent;\n        editorTextContent += previousSubTreeTextContent;\n      }\n    } else {\n      const text = prevNode.getTextContent();\n\n      editorTextContent += text;\n      subTreeTextContent += text;\n    }\n\n    return dom;\n  }\n  // If the node key doesn't point to the same instance in both maps,\n  // it means it were cloned. If they're also dirty, we mark them as mutated.\n  if (prevNode !== nextNode && isDirty) {\n    setMutatedNode(\n      mutatedNodes,\n      activeEditorNodes,\n      activeMutationListeners,\n      nextNode,\n      'updated',\n    );\n  }\n\n  // Update node. If it returns true, we need to unmount and re-create the node\n  if (nextNode.updateDOM(prevNode, dom, activeEditorConfig)) {\n    const replacementDOM = $createNode(key, null, null);\n\n    if (parentDOM === null) {\n      invariant(false, 'reconcileNode: parentDOM is null');\n    }\n\n    parentDOM.replaceChild(replacementDOM, dom);\n    destroyNode(key, null);\n    return replacementDOM;\n  }\n\n  if ($isElementNode(prevNode) && $isElementNode(nextNode)) {\n    // Reconcile element children\n    if (isDirty) {\n      $reconcileChildrenWithDirection(prevNode, nextNode, dom);\n      if (!$isRootNode(nextNode) && !nextNode.isInline()) {\n        reconcileElementTerminatingLineBreak(prevNode, nextNode, dom);\n      }\n    }\n\n    if ($textContentRequiresDoubleLinebreakAtEnd(nextNode)) {\n      subTreeTextContent += DOUBLE_LINE_BREAK;\n      editorTextContent += DOUBLE_LINE_BREAK;\n    }\n  } else {\n    const text = nextNode.getTextContent();\n\n    if ($isDecoratorNode(nextNode)) {\n      const decorator = nextNode.decorate(activeEditor, activeEditorConfig);\n\n      if (decorator !== null) {\n        reconcileDecorator(key, decorator);\n      }\n    }\n\n    subTreeTextContent += text;\n    editorTextContent += text;\n  }\n\n  if (\n    !activeEditorStateReadOnly &&\n    $isRootNode(nextNode) &&\n    nextNode.__cachedText !== editorTextContent\n  ) {\n    // Cache the latest text content.\n    const nextRootNode = nextNode.getWritable();\n    nextRootNode.__cachedText = editorTextContent;\n    nextNode = nextRootNode;\n  }\n\n  if (__DEV__) {\n    // Freeze the node in DEV to prevent accidental mutations\n    Object.freeze(nextNode);\n  }\n\n  return dom;\n}\n\nfunction reconcileDecorator(key: NodeKey, decorator: unknown): void {\n  let pendingDecorators = activeEditor._pendingDecorators;\n  const currentDecorators = activeEditor._decorators;\n\n  if (pendingDecorators === null) {\n    if (currentDecorators[key] === decorator) {\n      return;\n    }\n\n    pendingDecorators = cloneDecorators(activeEditor);\n  }\n\n  pendingDecorators[key] = decorator;\n}\n\nfunction getFirstChild(element: HTMLElement): Node | null {\n  return element.firstChild;\n}\n\nfunction getNextSibling(element: HTMLElement): Node | null {\n  let nextSibling = element.nextSibling;\n  if (\n    nextSibling !== null &&\n    nextSibling === activeEditor._blockCursorElement\n  ) {\n    nextSibling = nextSibling.nextSibling;\n  }\n  return nextSibling;\n}\n\nfunction $reconcileNodeChildren(\n  nextElement: ElementNode,\n  prevChildren: Array<NodeKey>,\n  nextChildren: Array<NodeKey>,\n  prevChildrenLength: number,\n  nextChildrenLength: number,\n  dom: HTMLElement,\n): void {\n  const prevEndIndex = prevChildrenLength - 1;\n  const nextEndIndex = nextChildrenLength - 1;\n  let prevChildrenSet: Set<NodeKey> | undefined;\n  let nextChildrenSet: Set<NodeKey> | undefined;\n  let siblingDOM: null | Node = getFirstChild(dom);\n  let prevIndex = 0;\n  let nextIndex = 0;\n\n  while (prevIndex <= prevEndIndex && nextIndex <= nextEndIndex) {\n    const prevKey = prevChildren[prevIndex];\n    const nextKey = nextChildren[nextIndex];\n\n    if (prevKey === nextKey) {\n      siblingDOM = getNextSibling($reconcileNode(nextKey, dom));\n      prevIndex++;\n      nextIndex++;\n    } else {\n      if (prevChildrenSet === undefined) {\n        prevChildrenSet = new Set(prevChildren);\n      }\n\n      if (nextChildrenSet === undefined) {\n        nextChildrenSet = new Set(nextChildren);\n      }\n\n      const nextHasPrevKey = nextChildrenSet.has(prevKey);\n      const prevHasNextKey = prevChildrenSet.has(nextKey);\n\n      if (!nextHasPrevKey) {\n        // Remove prev\n        siblingDOM = getNextSibling(getPrevElementByKeyOrThrow(prevKey));\n        destroyNode(prevKey, dom);\n        prevIndex++;\n      } else if (!prevHasNextKey) {\n        // Create next\n        $createNode(nextKey, dom, siblingDOM);\n        nextIndex++;\n      } else {\n        // Move next\n        const childDOM = getElementByKeyOrThrow(activeEditor, nextKey);\n\n        if (childDOM === siblingDOM) {\n          siblingDOM = getNextSibling($reconcileNode(nextKey, dom));\n        } else {\n          if (siblingDOM != null) {\n            dom.insertBefore(childDOM, siblingDOM);\n          } else {\n            dom.appendChild(childDOM);\n          }\n\n          $reconcileNode(nextKey, dom);\n        }\n\n        prevIndex++;\n        nextIndex++;\n      }\n    }\n\n    const node = activeNextNodeMap.get(nextKey);\n    if (node !== null && $isTextNode(node)) {\n      if (subTreeTextFormat === null) {\n        subTreeTextFormat = node.getFormat();\n      }\n      if (subTreeTextStyle === '') {\n        subTreeTextStyle = node.getStyle();\n      }\n    }\n  }\n\n  const appendNewChildren = prevIndex > prevEndIndex;\n  const removeOldChildren = nextIndex > nextEndIndex;\n\n  if (appendNewChildren && !removeOldChildren) {\n    const previousNode = nextChildren[nextEndIndex + 1];\n    const insertDOM =\n      previousNode === undefined\n        ? null\n        : activeEditor.getElementByKey(previousNode);\n    $createChildren(\n      nextChildren,\n      nextElement,\n      nextIndex,\n      nextEndIndex,\n      dom,\n      insertDOM,\n    );\n  } else if (removeOldChildren && !appendNewChildren) {\n    destroyChildren(prevChildren, prevIndex, prevEndIndex, dom);\n  }\n}\n\nexport function $reconcileRoot(\n  prevEditorState: EditorState,\n  nextEditorState: EditorState,\n  editor: LexicalEditor,\n  dirtyType: 0 | 1 | 2,\n  dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,\n  dirtyLeaves: Set<NodeKey>,\n): MutatedNodes {\n  // We cache text content to make retrieval more efficient.\n  // The cache must be rebuilt during reconciliation to account for any changes.\n  subTreeTextContent = '';\n  editorTextContent = '';\n  // Rather than pass around a load of arguments through the stack recursively\n  // we instead set them as bindings within the scope of the module.\n  treatAllNodesAsDirty = dirtyType === FULL_RECONCILE;\n  activeEditor = editor;\n  activeEditorConfig = editor._config;\n  activeEditorNodes = editor._nodes;\n  activeMutationListeners = activeEditor._listeners.mutation;\n  activeDirtyElements = dirtyElements;\n  activeDirtyLeaves = dirtyLeaves;\n  activePrevNodeMap = prevEditorState._nodeMap;\n  activeNextNodeMap = nextEditorState._nodeMap;\n  activeEditorStateReadOnly = nextEditorState._readOnly;\n  activePrevKeyToDOMMap = new Map(editor._keyToDOMMap);\n  // We keep track of mutated nodes so we can trigger mutation\n  // listeners later in the update cycle.\n  const currentMutatedNodes = new Map();\n  mutatedNodes = currentMutatedNodes;\n  $reconcileNode('root', null);\n  // We don't want a bunch of void checks throughout the scope\n  // so instead we make it seem that these values are always set.\n  // We also want to make sure we clear them down, otherwise we\n  // can leak memory.\n  // @ts-ignore\n  activeEditor = undefined;\n  // @ts-ignore\n  activeEditorNodes = undefined;\n  // @ts-ignore\n  activeDirtyElements = undefined;\n  // @ts-ignore\n  activeDirtyLeaves = undefined;\n  // @ts-ignore\n  activePrevNodeMap = undefined;\n  // @ts-ignore\n  activeNextNodeMap = undefined;\n  // @ts-ignore\n  activeEditorConfig = undefined;\n  // @ts-ignore\n  activePrevKeyToDOMMap = undefined;\n  // @ts-ignore\n  mutatedNodes = undefined;\n\n  return currentMutatedNodes;\n}\n\nexport function storeDOMWithKey(\n  key: NodeKey,\n  dom: HTMLElement,\n  editor: LexicalEditor,\n): void {\n  const keyToDOMMap = editor._keyToDOMMap;\n  // @ts-ignore We intentionally add this to the Node.\n  dom['__lexicalKey_' + editor._key] = key;\n  keyToDOMMap.set(key, dom);\n}\n\nfunction getPrevElementByKeyOrThrow(key: NodeKey): HTMLElement {\n  const element = activePrevKeyToDOMMap.get(key);\n\n  if (element === undefined) {\n    invariant(\n      false,\n      'Reconciliation: could not find DOM element for node key %s',\n      key,\n    );\n  }\n\n  return element;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/LexicalSelection.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {LexicalEditor} from './LexicalEditor';\nimport type {EditorState} from './LexicalEditorState';\nimport type {NodeKey} from './LexicalNode';\nimport type {ElementNode} from './nodes/LexicalElementNode';\nimport type {TextFormatType} from './nodes/LexicalTextNode';\n\nimport invariant from 'lexical/shared/invariant';\n\nimport {\n  $createLineBreakNode,\n  $createParagraphNode,\n  $createTextNode, $getNearestNodeFromDOMNode,\n  $isDecoratorNode,\n  $isElementNode,\n  $isLineBreakNode,\n  $isRootNode,\n  $isTextNode,\n  $setSelection,\n  SELECTION_CHANGE_COMMAND,\n  TextNode,\n} from '.';\nimport {DOM_ELEMENT_TYPE, TEXT_TYPE_TO_FORMAT} from './LexicalConstants';\nimport {\n  markCollapsedSelectionFormat,\n  markSelectionChangeFromDOMUpdate,\n} from './LexicalEvents';\nimport {getIsProcessingMutations} from './LexicalMutations';\nimport {insertRangeAfter, LexicalNode} from './LexicalNode';\nimport {\n  getActiveEditor,\n  getActiveEditorState,\n  isCurrentlyReadOnlyMode,\n} from './LexicalUpdates';\nimport {\n  $getAdjacentNode,\n  $getAncestor,\n  $getCompositionKey,\n  $getNearestRootOrShadowRoot,\n  $getNodeByKey,\n  $getNodeFromDOM,\n  $getRoot,\n  $hasAncestor,\n  $isTokenOrSegmented,\n  $setCompositionKey,\n  doesContainGrapheme,\n  getDOMSelection,\n  getDOMTextNode,\n  getElementByKeyOrThrow,\n  getTextNodeOffset,\n  INTERNAL_$isBlock,\n  isSelectionCapturedInDecoratorInput,\n  isSelectionWithinEditor,\n  removeDOMBlockCursorElement,\n  scrollIntoViewIfNeeded,\n  toggleTextFormatType,\n} from './LexicalUtils';\nimport {$createTabNode, $isTabNode} from './nodes/LexicalTabNode';\nimport {$selectSingleNode} from \"../../utils/selection\";\n\nexport type TextPointType = {\n  _selection: BaseSelection;\n  getNode: () => TextNode;\n  is: (point: PointType) => boolean;\n  isBefore: (point: PointType) => boolean;\n  key: NodeKey;\n  offset: number;\n  set: (key: NodeKey, offset: number, type: 'text' | 'element') => void;\n  type: 'text';\n};\n\nexport type ElementPointType = {\n  _selection: BaseSelection;\n  getNode: () => ElementNode;\n  is: (point: PointType) => boolean;\n  isBefore: (point: PointType) => boolean;\n  key: NodeKey;\n  offset: number;\n  set: (key: NodeKey, offset: number, type: 'text' | 'element') => void;\n  type: 'element';\n};\n\nexport type PointType = TextPointType | ElementPointType;\n\nexport class Point {\n  key: NodeKey;\n  offset: number;\n  type: 'text' | 'element';\n  _selection: BaseSelection | null;\n\n  constructor(key: NodeKey, offset: number, type: 'text' | 'element') {\n    this._selection = null;\n    this.key = key;\n    this.offset = offset;\n    this.type = type;\n  }\n\n  is(point: PointType): boolean {\n    return (\n      this.key === point.key &&\n      this.offset === point.offset &&\n      this.type === point.type\n    );\n  }\n\n  isBefore(b: PointType): boolean {\n    let aNode = this.getNode();\n    let bNode = b.getNode();\n    const aOffset = this.offset;\n    const bOffset = b.offset;\n\n    if ($isElementNode(aNode)) {\n      const aNodeDescendant = aNode.getDescendantByIndex<ElementNode>(aOffset);\n      aNode = aNodeDescendant != null ? aNodeDescendant : aNode;\n    }\n    if ($isElementNode(bNode)) {\n      const bNodeDescendant = bNode.getDescendantByIndex<ElementNode>(bOffset);\n      bNode = bNodeDescendant != null ? bNodeDescendant : bNode;\n    }\n    if (aNode === bNode) {\n      return aOffset < bOffset;\n    }\n    return aNode.isBefore(bNode);\n  }\n\n  getNode(): LexicalNode {\n    const key = this.key;\n    const node = $getNodeByKey(key);\n    if (node === null) {\n      invariant(false, 'Point.getNode: node not found');\n    }\n    return node;\n  }\n\n  set(key: NodeKey, offset: number, type: 'text' | 'element'): void {\n    const selection = this._selection;\n    const oldKey = this.key;\n    this.key = key;\n    this.offset = offset;\n    this.type = type;\n    if (!isCurrentlyReadOnlyMode()) {\n      if ($getCompositionKey() === oldKey) {\n        $setCompositionKey(key);\n      }\n      if (selection !== null) {\n        selection.setCachedNodes(null);\n        selection.dirty = true;\n      }\n    }\n  }\n}\n\nexport function $createPoint(\n  key: NodeKey,\n  offset: number,\n  type: 'text' | 'element',\n): PointType {\n  // @ts-expect-error: intentionally cast as we use a class for perf reasons\n  return new Point(key, offset, type);\n}\n\nfunction selectPointOnNode(point: PointType, node: LexicalNode): void {\n  let key = node.__key;\n  let offset = point.offset;\n  let type: 'element' | 'text' = 'element';\n  if ($isTextNode(node)) {\n    type = 'text';\n    const textContentLength = node.getTextContentSize();\n    if (offset > textContentLength) {\n      offset = textContentLength;\n    }\n  } else if (!$isElementNode(node)) {\n    const nextSibling = node.getNextSibling();\n    if ($isTextNode(nextSibling)) {\n      key = nextSibling.__key;\n      offset = 0;\n      type = 'text';\n    } else {\n      const parentNode = node.getParent();\n      if (parentNode) {\n        key = parentNode.__key;\n        offset = node.getIndexWithinParent() + 1;\n      }\n    }\n  }\n  point.set(key, offset, type);\n}\n\nexport function $moveSelectionPointToEnd(\n  point: PointType,\n  node: LexicalNode,\n): void {\n  if ($isElementNode(node)) {\n    const lastNode = node.getLastDescendant();\n    if ($isElementNode(lastNode) || $isTextNode(lastNode)) {\n      selectPointOnNode(point, lastNode);\n    } else {\n      selectPointOnNode(point, node);\n    }\n  } else {\n    selectPointOnNode(point, node);\n  }\n}\n\nfunction $transferStartingElementPointToTextPoint(\n  start: ElementPointType,\n  end: PointType,\n  format: number,\n  style: string,\n): void {\n  const element = start.getNode();\n  const placementNode = element.getChildAtIndex(start.offset);\n  const textNode = $createTextNode();\n  const target = $isRootNode(element)\n    ? $createParagraphNode().append(textNode)\n    : textNode;\n  textNode.setFormat(format);\n  textNode.setStyle(style);\n  if (placementNode === null) {\n    element.append(target);\n  } else {\n    placementNode.insertBefore(target);\n  }\n  // Transfer the element point to a text point.\n  if (start.is(end)) {\n    end.set(textNode.__key, 0, 'text');\n  }\n  start.set(textNode.__key, 0, 'text');\n}\n\nfunction $setPointValues(\n  point: PointType,\n  key: NodeKey,\n  offset: number,\n  type: 'text' | 'element',\n): void {\n  point.key = key;\n  point.offset = offset;\n  point.type = type;\n}\n\nexport interface BaseSelection {\n  _cachedNodes: Array<LexicalNode> | null;\n  dirty: boolean;\n\n  clone(): BaseSelection;\n  extract(): Array<LexicalNode>;\n  getNodes(): Array<LexicalNode>;\n  getTextContent(): string;\n  insertText(text: string): void;\n  insertRawText(text: string): void;\n  is(selection: null | BaseSelection): boolean;\n  insertNodes(nodes: Array<LexicalNode>): void;\n  getStartEndPoints(): null | [PointType, PointType];\n  isCollapsed(): boolean;\n  isBackward(): boolean;\n  getCachedNodes(): LexicalNode[] | null;\n  setCachedNodes(nodes: LexicalNode[] | null): void;\n}\n\nexport class NodeSelection implements BaseSelection {\n  _nodes: Set<NodeKey>;\n  _cachedNodes: Array<LexicalNode> | null;\n  dirty: boolean;\n\n  constructor(objects: Set<NodeKey>) {\n    this._cachedNodes = null;\n    this._nodes = objects;\n    this.dirty = false;\n  }\n\n  getCachedNodes(): LexicalNode[] | null {\n    return this._cachedNodes;\n  }\n\n  setCachedNodes(nodes: LexicalNode[] | null): void {\n    this._cachedNodes = nodes;\n  }\n\n  is(selection: null | BaseSelection): boolean {\n    if (!$isNodeSelection(selection)) {\n      return false;\n    }\n    const a: Set<NodeKey> = this._nodes;\n    const b: Set<NodeKey> = selection._nodes;\n    return a.size === b.size && Array.from(a).every((key) => b.has(key));\n  }\n\n  isCollapsed(): boolean {\n    return false;\n  }\n\n  isBackward(): boolean {\n    return false;\n  }\n\n  getStartEndPoints(): null {\n    return null;\n  }\n\n  add(key: NodeKey): void {\n    this.dirty = true;\n    this._nodes.add(key);\n    this._cachedNodes = null;\n  }\n\n  delete(key: NodeKey): void {\n    this.dirty = true;\n    this._nodes.delete(key);\n    this._cachedNodes = null;\n  }\n\n  clear(): void {\n    this.dirty = true;\n    this._nodes.clear();\n    this._cachedNodes = null;\n  }\n\n  has(key: NodeKey): boolean {\n    return this._nodes.has(key);\n  }\n\n  clone(): NodeSelection {\n    return new NodeSelection(new Set(this._nodes));\n  }\n\n  extract(): Array<LexicalNode> {\n    return this.getNodes();\n  }\n\n  insertRawText(text: string): void {\n    // Do nothing?\n  }\n\n  insertText(): void {\n    // Do nothing?\n  }\n\n  insertNodes(nodes: Array<LexicalNode>) {\n    const selectedNodes = this.getNodes();\n    const selectedNodesLength = selectedNodes.length;\n    const lastSelectedNode = selectedNodes[selectedNodesLength - 1];\n    let selectionAtEnd: RangeSelection;\n    // Insert nodes\n    if ($isTextNode(lastSelectedNode)) {\n      selectionAtEnd = lastSelectedNode.select();\n    } else {\n      const index = lastSelectedNode.getIndexWithinParent() + 1;\n      selectionAtEnd = lastSelectedNode.getParentOrThrow().select(index, index);\n    }\n    selectionAtEnd.insertNodes(nodes);\n    // Remove selected nodes\n    for (let i = 0; i < selectedNodesLength; i++) {\n      selectedNodes[i].remove();\n    }\n  }\n\n  getNodes(): Array<LexicalNode> {\n    const cachedNodes = this._cachedNodes;\n    if (cachedNodes !== null) {\n      return cachedNodes;\n    }\n    const objects = this._nodes;\n    const nodes = [];\n    for (const object of objects) {\n      const node = $getNodeByKey(object);\n      if (node !== null) {\n        nodes.push(node);\n      }\n    }\n    if (!isCurrentlyReadOnlyMode()) {\n      this._cachedNodes = nodes;\n    }\n    return nodes;\n  }\n\n  getTextContent(): string {\n    const nodes = this.getNodes();\n    let textContent = '';\n    for (let i = 0; i < nodes.length; i++) {\n      textContent += nodes[i].getTextContent();\n    }\n    return textContent;\n  }\n}\n\nexport function $isRangeSelection(x: unknown): x is RangeSelection {\n  return x instanceof RangeSelection;\n}\n\nexport class RangeSelection implements BaseSelection {\n  format: number;\n  style: string;\n  anchor: PointType;\n  focus: PointType;\n  _cachedNodes: Array<LexicalNode> | null;\n  dirty: boolean;\n\n  constructor(\n    anchor: PointType,\n    focus: PointType,\n    format: number,\n    style: string,\n  ) {\n    this.anchor = anchor;\n    this.focus = focus;\n    anchor._selection = this;\n    focus._selection = this;\n    this._cachedNodes = null;\n    this.format = format;\n    this.style = style;\n    this.dirty = false;\n  }\n\n  getCachedNodes(): LexicalNode[] | null {\n    return this._cachedNodes;\n  }\n\n  setCachedNodes(nodes: LexicalNode[] | null): void {\n    this._cachedNodes = nodes;\n  }\n\n  /**\n   * Used to check if the provided selections is equal to this one by value,\n   * inluding anchor, focus, format, and style properties.\n   * @param selection - the Selection to compare this one to.\n   * @returns true if the Selections are equal, false otherwise.\n   */\n  is(selection: null | BaseSelection): boolean {\n    if (!$isRangeSelection(selection)) {\n      return false;\n    }\n    return (\n      this.anchor.is(selection.anchor) &&\n      this.focus.is(selection.focus) &&\n      this.format === selection.format &&\n      this.style === selection.style\n    );\n  }\n\n  /**\n   * Returns whether the Selection is \"collapsed\", meaning the anchor and focus are\n   * the same node and have the same offset.\n   *\n   * @returns true if the Selection is collapsed, false otherwise.\n   */\n  isCollapsed(): boolean {\n    return this.anchor.is(this.focus);\n  }\n\n  /**\n   * Gets all the nodes in the Selection. Uses caching to make it generally suitable\n   * for use in hot paths.\n   *\n   * @returns an Array containing all the nodes in the Selection\n   */\n  getNodes(): Array<LexicalNode> {\n    const cachedNodes = this._cachedNodes;\n    if (cachedNodes !== null) {\n      return cachedNodes;\n    }\n    const anchor = this.anchor;\n    const focus = this.focus;\n    const isBefore = anchor.isBefore(focus);\n    const firstPoint = isBefore ? anchor : focus;\n    const lastPoint = isBefore ? focus : anchor;\n    let firstNode = firstPoint.getNode();\n    let lastNode = lastPoint.getNode();\n    const startOffset = firstPoint.offset;\n    const endOffset = lastPoint.offset;\n\n    if ($isElementNode(firstNode) && !firstNode.shouldSelectDirectly()) {\n      const firstNodeDescendant =\n        firstNode.getDescendantByIndex<ElementNode>(startOffset);\n      firstNode = firstNodeDescendant != null ? firstNodeDescendant : firstNode;\n    }\n    if ($isElementNode(lastNode) && !lastNode.shouldSelectDirectly()) {\n      let lastNodeDescendant =\n        lastNode.getDescendantByIndex<ElementNode>(endOffset);\n      // We don't want to over-select, as node selection infers the child before\n      // the last descendant, not including that descendant.\n      if (\n        lastNodeDescendant !== null &&\n        lastNodeDescendant !== firstNode &&\n        lastNode.getChildAtIndex(endOffset) === lastNodeDescendant\n      ) {\n        lastNodeDescendant = lastNodeDescendant.getPreviousSibling();\n      }\n      lastNode = lastNodeDescendant != null ? lastNodeDescendant : lastNode;\n    }\n\n    let nodes: Array<LexicalNode>;\n\n    if (firstNode.is(lastNode)) {\n      if ($isElementNode(firstNode) && firstNode.getChildrenSize() > 0 && !firstNode.shouldSelectDirectly()) {\n        nodes = [];\n      } else {\n        nodes = [firstNode];\n      }\n    } else {\n      nodes = firstNode.getNodesBetween(lastNode);\n    }\n    if (!isCurrentlyReadOnlyMode()) {\n      this._cachedNodes = nodes;\n    }\n    return nodes;\n  }\n\n  /**\n   * Sets this Selection to be of type \"text\" at the provided anchor and focus values.\n   *\n   * @param anchorNode - the anchor node to set on the Selection\n   * @param anchorOffset - the offset to set on the Selection\n   * @param focusNode - the focus node to set on the Selection\n   * @param focusOffset - the focus offset to set on the Selection\n   */\n  setTextNodeRange(\n    anchorNode: TextNode,\n    anchorOffset: number,\n    focusNode: TextNode,\n    focusOffset: number,\n  ): void {\n    $setPointValues(this.anchor, anchorNode.__key, anchorOffset, 'text');\n    $setPointValues(this.focus, focusNode.__key, focusOffset, 'text');\n    this._cachedNodes = null;\n    this.dirty = true;\n  }\n\n  /**\n   * Gets the (plain) text content of all the nodes in the selection.\n   *\n   * @returns a string representing the text content of all the nodes in the Selection\n   */\n  getTextContent(): string {\n    const nodes = this.getNodes();\n    if (nodes.length === 0) {\n      return '';\n    }\n    const firstNode = nodes[0];\n    const lastNode = nodes[nodes.length - 1];\n    const anchor = this.anchor;\n    const focus = this.focus;\n    const isBefore = anchor.isBefore(focus);\n    const [anchorOffset, focusOffset] = $getCharacterOffsets(this);\n    let textContent = '';\n    let prevWasElement = true;\n    for (let i = 0; i < nodes.length; i++) {\n      const node = nodes[i];\n      if ($isElementNode(node) && !node.isInline()) {\n        if (!prevWasElement) {\n          textContent += '\\n';\n        }\n        if (node.isEmpty()) {\n          prevWasElement = false;\n        } else {\n          prevWasElement = true;\n        }\n      } else {\n        prevWasElement = false;\n        if ($isTextNode(node)) {\n          let text = node.getTextContent();\n          if (node === firstNode) {\n            if (node === lastNode) {\n              if (\n                anchor.type !== 'element' ||\n                focus.type !== 'element' ||\n                focus.offset === anchor.offset\n              ) {\n                text =\n                  anchorOffset < focusOffset\n                    ? text.slice(anchorOffset, focusOffset)\n                    : text.slice(focusOffset, anchorOffset);\n              }\n            } else {\n              text = isBefore\n                ? text.slice(anchorOffset)\n                : text.slice(focusOffset);\n            }\n          } else if (node === lastNode) {\n            text = isBefore\n              ? text.slice(0, focusOffset)\n              : text.slice(0, anchorOffset);\n          }\n          textContent += text;\n        } else if (\n          ($isDecoratorNode(node) || $isLineBreakNode(node)) &&\n          (node !== lastNode || !this.isCollapsed())\n        ) {\n          textContent += node.getTextContent();\n        }\n      }\n    }\n    return textContent;\n  }\n\n  /**\n   * Attempts to map a DOM selection range onto this Lexical Selection,\n   * setting the anchor, focus, and type accordingly\n   *\n   * @param range a DOM Selection range conforming to the StaticRange interface.\n   */\n  applyDOMRange(range: StaticRange): void {\n    const editor = getActiveEditor();\n    const currentEditorState = editor.getEditorState();\n    const lastSelection = currentEditorState._selection;\n    const resolvedSelectionPoints = $internalResolveSelectionPoints(\n      range.startContainer,\n      range.startOffset,\n      range.endContainer,\n      range.endOffset,\n      editor,\n      lastSelection,\n    );\n    if (resolvedSelectionPoints === null) {\n      return;\n    }\n    const [anchorPoint, focusPoint] = resolvedSelectionPoints;\n    $setPointValues(\n      this.anchor,\n      anchorPoint.key,\n      anchorPoint.offset,\n      anchorPoint.type,\n    );\n    $setPointValues(\n      this.focus,\n      focusPoint.key,\n      focusPoint.offset,\n      focusPoint.type,\n    );\n    this._cachedNodes = null;\n  }\n\n  /**\n   * Creates a new RangeSelection, copying over all the property values from this one.\n   *\n   * @returns a new RangeSelection with the same property values as this one.\n   */\n  clone(): RangeSelection {\n    const anchor = this.anchor;\n    const focus = this.focus;\n    const selection = new RangeSelection(\n      $createPoint(anchor.key, anchor.offset, anchor.type),\n      $createPoint(focus.key, focus.offset, focus.type),\n      this.format,\n      this.style,\n    );\n    return selection;\n  }\n\n  /**\n   * Toggles the provided format on all the TextNodes in the Selection.\n   *\n   * @param format a string TextFormatType to toggle on the TextNodes in the selection\n   */\n  toggleFormat(format: TextFormatType): void {\n    this.format = toggleTextFormatType(this.format, format, null);\n    this.dirty = true;\n  }\n\n  /**\n   * Sets the value of the style property on the Selection\n   *\n   * @param style - the style to set at the value of the style property.\n   */\n  setStyle(style: string): void {\n    this.style = style;\n    this.dirty = true;\n  }\n\n  /**\n   * Returns whether the provided TextFormatType is present on the Selection. This will be true if any node in the Selection\n   * has the specified format.\n   *\n   * @param type the TextFormatType to check for.\n   * @returns true if the provided format is currently toggled on on the Selection, false otherwise.\n   */\n  hasFormat(type: TextFormatType): boolean {\n    const formatFlag = TEXT_TYPE_TO_FORMAT[type];\n    return (this.format & formatFlag) !== 0;\n  }\n\n  /**\n   * Attempts to insert the provided text into the EditorState at the current Selection.\n   * converts tabs, newlines, and carriage returns into LexicalNodes.\n   *\n   * @param text the text to insert into the Selection\n   */\n  insertRawText(text: string): void {\n    const parts = text.split(/(\\r?\\n|\\t)/);\n    const nodes = [];\n    const length = parts.length;\n    for (let i = 0; i < length; i++) {\n      const part = parts[i];\n      if (part === '\\n' || part === '\\r\\n') {\n        nodes.push($createLineBreakNode());\n      } else if (part === '\\t') {\n        nodes.push($createTabNode());\n      } else {\n        nodes.push($createTextNode(part));\n      }\n    }\n    this.insertNodes(nodes);\n  }\n\n  /**\n   * Attempts to insert the provided text into the EditorState at the current Selection as a new\n   * Lexical TextNode, according to a series of insertion heuristics based on the selection type and position.\n   *\n   * @param text the text to insert into the Selection\n   */\n  insertText(text: string): void {\n    const anchor = this.anchor;\n    const focus = this.focus;\n    const format = this.format;\n    const style = this.style;\n    let firstPoint = anchor;\n    let endPoint = focus;\n    if (!this.isCollapsed() && focus.isBefore(anchor)) {\n      firstPoint = focus;\n      endPoint = anchor;\n    }\n    if (firstPoint.type === 'element') {\n      $transferStartingElementPointToTextPoint(\n        firstPoint,\n        endPoint,\n        format,\n        style,\n      );\n    }\n    const startOffset = firstPoint.offset;\n    let endOffset = endPoint.offset;\n    const selectedNodes = this.getNodes();\n    const selectedNodesLength = selectedNodes.length;\n    let firstNode: TextNode = selectedNodes[0] as TextNode;\n\n    if (!$isTextNode(firstNode)) {\n      invariant(false, 'insertText: first node is not a text node');\n    }\n    const firstNodeText = firstNode.getTextContent();\n    const firstNodeTextLength = firstNodeText.length;\n    const firstNodeParent = firstNode.getParentOrThrow();\n    const lastIndex = selectedNodesLength - 1;\n    let lastNode = selectedNodes[lastIndex];\n\n    if (selectedNodesLength === 1 && endPoint.type === 'element') {\n      endOffset = firstNodeTextLength;\n      endPoint.set(firstPoint.key, endOffset, 'text');\n    }\n\n    if (\n      this.isCollapsed() &&\n      startOffset === firstNodeTextLength &&\n      (firstNode.isSegmented() ||\n        firstNode.isToken() ||\n        !firstNode.canInsertTextAfter() ||\n        (!firstNodeParent.canInsertTextAfter() &&\n          firstNode.getNextSibling() === null))\n    ) {\n      let nextSibling = firstNode.getNextSibling<TextNode>();\n      if (\n        !$isTextNode(nextSibling) ||\n        !nextSibling.canInsertTextBefore() ||\n        $isTokenOrSegmented(nextSibling)\n      ) {\n        nextSibling = $createTextNode();\n        nextSibling.setFormat(format);\n        nextSibling.setStyle(style);\n        if (!firstNodeParent.canInsertTextAfter()) {\n          firstNodeParent.insertAfter(nextSibling);\n        } else {\n          firstNode.insertAfter(nextSibling);\n        }\n      }\n      nextSibling.select(0, 0);\n      firstNode = nextSibling;\n      if (text !== '') {\n        this.insertText(text);\n        return;\n      }\n    } else if (\n      this.isCollapsed() &&\n      startOffset === 0 &&\n      (firstNode.isSegmented() ||\n        firstNode.isToken() ||\n        !firstNode.canInsertTextBefore() ||\n        (!firstNodeParent.canInsertTextBefore() &&\n          firstNode.getPreviousSibling() === null))\n    ) {\n      let prevSibling = firstNode.getPreviousSibling<TextNode>();\n      if (!$isTextNode(prevSibling) || $isTokenOrSegmented(prevSibling)) {\n        prevSibling = $createTextNode();\n        prevSibling.setFormat(format);\n        if (!firstNodeParent.canInsertTextBefore()) {\n          firstNodeParent.insertBefore(prevSibling);\n        } else {\n          firstNode.insertBefore(prevSibling);\n        }\n      }\n      prevSibling.select();\n      firstNode = prevSibling;\n      if (text !== '') {\n        this.insertText(text);\n        return;\n      }\n    } else if (firstNode.isSegmented() && startOffset !== firstNodeTextLength) {\n      const textNode = $createTextNode(firstNode.getTextContent());\n      textNode.setFormat(format);\n      firstNode.replace(textNode);\n      firstNode = textNode;\n    } else if (!this.isCollapsed() && text !== '') {\n      // When the firstNode or lastNode parents are elements that\n      // do not allow text to be inserted before or after, we first\n      // clear the content. Then we normalize selection, then insert\n      // the new content.\n      const lastNodeParent = lastNode.getParent();\n\n      if (\n        !firstNodeParent.canInsertTextBefore() ||\n        !firstNodeParent.canInsertTextAfter() ||\n        ($isElementNode(lastNodeParent) &&\n          (!lastNodeParent.canInsertTextBefore() ||\n            !lastNodeParent.canInsertTextAfter()))\n      ) {\n        this.insertText('');\n        $normalizeSelectionPointsForBoundaries(this.anchor, this.focus, null);\n        this.insertText(text);\n        return;\n      }\n    }\n\n    if (selectedNodesLength === 1) {\n      if (firstNode.isToken()) {\n        const textNode = $createTextNode(text);\n        textNode.select();\n        firstNode.replace(textNode);\n        return;\n      }\n      const firstNodeFormat = firstNode.getFormat();\n      const firstNodeStyle = firstNode.getStyle();\n\n      if (\n        startOffset === endOffset &&\n        (firstNodeFormat !== format || firstNodeStyle !== style)\n      ) {\n        if (firstNode.getTextContent() === '') {\n          firstNode.setFormat(format);\n          firstNode.setStyle(style);\n        } else {\n          const textNode = $createTextNode(text);\n          textNode.setFormat(format);\n          textNode.setStyle(style);\n          textNode.select();\n          if (startOffset === 0) {\n            firstNode.insertBefore(textNode, false);\n          } else {\n            const [targetNode] = firstNode.splitText(startOffset);\n            targetNode.insertAfter(textNode, false);\n          }\n          // When composing, we need to adjust the anchor offset so that\n          // we correctly replace that right range.\n          if (textNode.isComposing() && this.anchor.type === 'text') {\n            this.anchor.offset -= text.length;\n          }\n          return;\n        }\n      } else if ($isTabNode(firstNode)) {\n        // We don't need to check for delCount because there is only the entire selected node case\n        // that can hit here for content size 1 and with canInsertTextBeforeAfter false\n        const textNode = $createTextNode(text);\n        textNode.setFormat(format);\n        textNode.setStyle(style);\n        textNode.select();\n        firstNode.replace(textNode);\n        return;\n      }\n      const delCount = endOffset - startOffset;\n\n      firstNode = firstNode.spliceText(startOffset, delCount, text, true);\n      if (firstNode.getTextContent() === '') {\n        firstNode.remove();\n      } else if (this.anchor.type === 'text') {\n        if (firstNode.isComposing()) {\n          // When composing, we need to adjust the anchor offset so that\n          // we correctly replace that right range.\n          this.anchor.offset -= text.length;\n        } else {\n          this.format = firstNodeFormat;\n          this.style = firstNodeStyle;\n        }\n      }\n    } else {\n      const markedNodeKeysForKeep = new Set([\n        ...firstNode.getParentKeys(),\n        ...lastNode.getParentKeys(),\n      ]);\n\n      // We have to get the parent elements before the next section,\n      // as in that section we might mutate the lastNode.\n      const firstElement = $isElementNode(firstNode)\n        ? firstNode\n        : firstNode.getParentOrThrow();\n      let lastElement = $isElementNode(lastNode)\n        ? lastNode\n        : lastNode.getParentOrThrow();\n      let lastElementChild = lastNode;\n\n      // If the last element is inline, we should instead look at getting\n      // the nodes of its parent, rather than itself. This behavior will\n      // then better match how text node insertions work. We will need to\n      // also update the last element's child accordingly as we do this.\n      if (!firstElement.is(lastElement) && lastElement.isInline()) {\n        // Keep traversing till we have a non-inline element parent.\n        do {\n          lastElementChild = lastElement;\n          lastElement = lastElement.getParentOrThrow();\n        } while (lastElement.isInline());\n      }\n\n      // Handle mutations to the last node.\n      if (\n        (endPoint.type === 'text' &&\n          (endOffset !== 0 || lastNode.getTextContent() === '')) ||\n        (endPoint.type === 'element' &&\n          lastNode.getIndexWithinParent() < endOffset)\n      ) {\n        if (\n          $isTextNode(lastNode) &&\n          !lastNode.isToken() &&\n          endOffset !== lastNode.getTextContentSize()\n        ) {\n          if (lastNode.isSegmented()) {\n            const textNode = $createTextNode(lastNode.getTextContent());\n            lastNode.replace(textNode);\n            lastNode = textNode;\n          }\n          // root node selections only select whole nodes, so no text splice is necessary\n          if (!$isRootNode(endPoint.getNode()) && endPoint.type === 'text') {\n            lastNode = (lastNode as TextNode).spliceText(0, endOffset, '');\n          }\n          markedNodeKeysForKeep.add(lastNode.__key);\n        } else {\n          const lastNodeParent = lastNode.getParentOrThrow();\n          if (\n            !lastNodeParent.canBeEmpty() &&\n            lastNodeParent.getChildrenSize() === 1\n          ) {\n            lastNodeParent.remove();\n          } else {\n            lastNode.remove();\n          }\n        }\n      } else {\n        markedNodeKeysForKeep.add(lastNode.__key);\n      }\n\n      // Either move the remaining nodes of the last parent to after\n      // the first child, or remove them entirely. If the last parent\n      // is the same as the first parent, this logic also works.\n      const lastNodeChildren = lastElement.getChildren();\n      const selectedNodesSet = new Set(selectedNodes);\n      const firstAndLastElementsAreEqual = firstElement.is(lastElement);\n\n      // We choose a target to insert all nodes after. In the case of having\n      // and inline starting parent element with a starting node that has no\n      // siblings, we should insert after the starting parent element, otherwise\n      // we will incorrectly merge into the starting parent element.\n      // TODO: should we keep on traversing parents if we're inside another\n      // nested inline element?\n      const insertionTarget =\n        firstElement.isInline() && firstNode.getNextSibling() === null\n          ? firstElement\n          : firstNode;\n\n      for (let i = lastNodeChildren.length - 1; i >= 0; i--) {\n        const lastNodeChild = lastNodeChildren[i];\n\n        if (\n          lastNodeChild.is(firstNode) ||\n          ($isElementNode(lastNodeChild) && lastNodeChild.isParentOf(firstNode))\n        ) {\n          break;\n        }\n\n        if (lastNodeChild.isAttached()) {\n          if (\n            !selectedNodesSet.has(lastNodeChild) ||\n            lastNodeChild.is(lastElementChild)\n          ) {\n            if (!firstAndLastElementsAreEqual) {\n              insertionTarget.insertAfter(lastNodeChild, false);\n            }\n          } else {\n            lastNodeChild.remove();\n          }\n        }\n      }\n\n      if (!firstAndLastElementsAreEqual) {\n        // Check if we have already moved out all the nodes of the\n        // last parent, and if so, traverse the parent tree and mark\n        // them all as being able to deleted too.\n        let parent: ElementNode | null = lastElement;\n        let lastRemovedParent = null;\n\n        while (parent !== null) {\n          const children = parent.getChildren();\n          const childrenLength = children.length;\n          if (\n            childrenLength === 0 ||\n            children[childrenLength - 1].is(lastRemovedParent)\n          ) {\n            markedNodeKeysForKeep.delete(parent.__key);\n            lastRemovedParent = parent;\n          }\n          parent = parent.getParent();\n        }\n      }\n\n      // Ensure we do splicing after moving of nodes, as splicing\n      // can have side-effects (in the case of hashtags).\n      if (!firstNode.isToken()) {\n        firstNode = firstNode.spliceText(\n          startOffset,\n          firstNodeTextLength - startOffset,\n          text,\n          true,\n        );\n        if (firstNode.getTextContent() === '') {\n          firstNode.remove();\n        } else if (firstNode.isComposing() && this.anchor.type === 'text') {\n          // When composing, we need to adjust the anchor offset so that\n          // we correctly replace that right range.\n          this.anchor.offset -= text.length;\n        }\n      } else if (startOffset === firstNodeTextLength) {\n        firstNode.select();\n      } else {\n        const textNode = $createTextNode(text);\n        textNode.select();\n        firstNode.replace(textNode);\n      }\n\n      // Remove all selected nodes that haven't already been removed.\n      for (let i = 1; i < selectedNodesLength; i++) {\n        const selectedNode = selectedNodes[i];\n        const key = selectedNode.__key;\n        if (!markedNodeKeysForKeep.has(key)) {\n          selectedNode.remove();\n        }\n      }\n    }\n  }\n\n  /**\n   * Removes the text in the Selection, adjusting the EditorState accordingly.\n   */\n  removeText(): void {\n    this.insertText('');\n  }\n\n  /**\n   * Applies the provided format to the TextNodes in the Selection, splitting or\n   * merging nodes as necessary.\n   *\n   * @param formatType the format type to apply to the nodes in the Selection.\n   */\n  formatText(formatType: TextFormatType): void {\n    if (this.isCollapsed()) {\n      this.toggleFormat(formatType);\n      // When changing format, we should stop composition\n      $setCompositionKey(null);\n      return;\n    }\n\n    const selectedNodes = this.getNodes();\n    const selectedTextNodes: Array<TextNode> = [];\n    for (const selectedNode of selectedNodes) {\n      if ($isTextNode(selectedNode)) {\n        selectedTextNodes.push(selectedNode);\n      }\n    }\n\n    const selectedTextNodesLength = selectedTextNodes.length;\n    if (selectedTextNodesLength === 0) {\n      this.toggleFormat(formatType);\n      // When changing format, we should stop composition\n      $setCompositionKey(null);\n      return;\n    }\n\n    const anchor = this.anchor;\n    const focus = this.focus;\n    const isBackward = this.isBackward();\n    const startPoint = isBackward ? focus : anchor;\n    const endPoint = isBackward ? anchor : focus;\n\n    let firstIndex = 0;\n    let firstNode = selectedTextNodes[0];\n    let startOffset = startPoint.type === 'element' ? 0 : startPoint.offset;\n\n    // In case selection started at the end of text node use next text node\n    if (\n      startPoint.type === 'text' &&\n      startOffset === firstNode.getTextContentSize()\n    ) {\n      firstIndex = 1;\n      firstNode = selectedTextNodes[1];\n      startOffset = 0;\n    }\n\n    if (firstNode == null) {\n      return;\n    }\n\n    const firstNextFormat = firstNode.getFormatFlags(formatType, null);\n\n    const lastIndex = selectedTextNodesLength - 1;\n    let lastNode = selectedTextNodes[lastIndex];\n    const endOffset =\n      endPoint.type === 'text'\n        ? endPoint.offset\n        : lastNode.getTextContentSize();\n\n    // Single node selected\n    if (firstNode.is(lastNode)) {\n      // No actual text is selected, so do nothing.\n      if (startOffset === endOffset) {\n        return;\n      }\n      // The entire node is selected or it is token, so just format it\n      if (\n        $isTokenOrSegmented(firstNode) ||\n        (startOffset === 0 && endOffset === firstNode.getTextContentSize())\n      ) {\n        firstNode.setFormat(firstNextFormat);\n      } else {\n        // Node is partially selected, so split it into two nodes\n        // add style the selected one.\n        const splitNodes = firstNode.splitText(startOffset, endOffset);\n        const replacement = startOffset === 0 ? splitNodes[0] : splitNodes[1];\n        replacement.setFormat(firstNextFormat);\n\n        // Update selection only if starts/ends on text node\n        if (startPoint.type === 'text') {\n          startPoint.set(replacement.__key, 0, 'text');\n        }\n        if (endPoint.type === 'text') {\n          endPoint.set(replacement.__key, endOffset - startOffset, 'text');\n        }\n      }\n\n      this.format = firstNextFormat;\n\n      return;\n    }\n    // Multiple nodes selected\n    // The entire first node isn't selected, so split it\n    if (startOffset !== 0 && !$isTokenOrSegmented(firstNode)) {\n      [, firstNode as TextNode] = firstNode.splitText(startOffset);\n      startOffset = 0;\n    }\n    firstNode.setFormat(firstNextFormat);\n\n    const lastNextFormat = lastNode.getFormatFlags(formatType, firstNextFormat);\n    // If the offset is 0, it means no actual characters are selected,\n    // so we skip formatting the last node altogether.\n    if (endOffset > 0) {\n      if (\n        endOffset !== lastNode.getTextContentSize() &&\n        !$isTokenOrSegmented(lastNode)\n      ) {\n        [lastNode as TextNode] = lastNode.splitText(endOffset);\n      }\n      lastNode.setFormat(lastNextFormat);\n    }\n\n    // Process all text nodes in between\n    for (let i = firstIndex + 1; i < lastIndex; i++) {\n      const textNode = selectedTextNodes[i];\n      const nextFormat = textNode.getFormatFlags(formatType, lastNextFormat);\n      textNode.setFormat(nextFormat);\n    }\n\n    // Update selection only if starts/ends on text node\n    if (startPoint.type === 'text') {\n      startPoint.set(firstNode.__key, startOffset, 'text');\n    }\n    if (endPoint.type === 'text') {\n      endPoint.set(lastNode.__key, endOffset, 'text');\n    }\n\n    this.format = firstNextFormat | lastNextFormat;\n  }\n\n  /**\n   * Attempts to \"intelligently\" insert an arbitrary list of Lexical nodes into the EditorState at the\n   * current Selection according to a set of heuristics that determine how surrounding nodes\n   * should be changed, replaced, or moved to accomodate the incoming ones.\n   *\n   * @param nodes - the nodes to insert\n   */\n  insertNodes(nodes: Array<LexicalNode>): void {\n    if (nodes.length === 0) {\n      return;\n    }\n    if (this.anchor.key === 'root') {\n      this.insertParagraph();\n      const selection = $getSelection();\n      invariant(\n        $isRangeSelection(selection),\n        'Expected RangeSelection after insertParagraph',\n      );\n      return selection.insertNodes(nodes);\n    }\n\n    const firstPoint = this.isBackward() ? this.focus : this.anchor;\n    const firstBlock = $getAncestor(firstPoint.getNode(), INTERNAL_$isBlock)!;\n\n    const last = nodes[nodes.length - 1]!;\n\n    // CASE 1: insert inside a code block\n    if ('__language' in firstBlock && $isElementNode(firstBlock)) {\n      if ('__language' in nodes[0]) {\n        this.insertText(nodes[0].getTextContent());\n      } else {\n        const index = $removeTextAndSplitBlock(this);\n        firstBlock.splice(index, 0, nodes);\n        last.selectEnd();\n      }\n      return;\n    }\n\n    // CASE 2: All elements of the array are inline\n    const notInline = (node: LexicalNode) =>\n      ($isElementNode(node) || $isDecoratorNode(node)) && !node.isInline();\n\n    if (!nodes.some(notInline)) {\n      invariant(\n        $isElementNode(firstBlock),\n        \"Expected 'firstBlock' to be an ElementNode\",\n      );\n      const index = $removeTextAndSplitBlock(this);\n      firstBlock.splice(index, 0, nodes);\n      last.selectEnd();\n      return;\n    }\n\n    // CASE 3: At least 1 element of the array is not inline\n    const blocksParent = $wrapInlineNodes(nodes);\n    const nodeToSelect = blocksParent.getLastDescendant()!;\n    const blocks = blocksParent.getChildren();\n    const isMergeable = (node: LexicalNode): node is ElementNode =>\n      $isElementNode(node) &&\n      INTERNAL_$isBlock(node) &&\n      !node.isEmpty() &&\n      $isElementNode(firstBlock) &&\n      (!firstBlock.isEmpty() || firstBlock.canMergeWhenEmpty());\n\n    const shouldInsert = !$isElementNode(firstBlock) || !firstBlock.isEmpty();\n    const insertedParagraph = shouldInsert ? this.insertParagraph() : null;\n    const lastToInsert = blocks[blocks.length - 1];\n    let firstToInsert = blocks[0];\n    if (isMergeable(firstToInsert)) {\n      invariant(\n        $isElementNode(firstBlock),\n        \"Expected 'firstBlock' to be an ElementNode\",\n      );\n      firstBlock.append(...firstToInsert.getChildren());\n      firstToInsert = blocks[1];\n    }\n    if (firstToInsert) {\n      insertRangeAfter(firstBlock, firstToInsert);\n    }\n    const lastInsertedBlock = $getAncestor(nodeToSelect, INTERNAL_$isBlock)!;\n\n    if (\n      insertedParagraph &&\n      $isElementNode(lastInsertedBlock) &&\n      (insertedParagraph.canMergeWhenEmpty() || INTERNAL_$isBlock(lastToInsert))\n    ) {\n      lastInsertedBlock.append(...insertedParagraph.getChildren());\n      insertedParagraph.remove();\n    }\n    if ($isElementNode(firstBlock) && firstBlock.isEmpty()) {\n      firstBlock.remove();\n    }\n\n    nodeToSelect.selectEnd();\n\n    // To understand this take a look at the test \"can wrap post-linebreak nodes into new element\"\n    const lastChild = $isElementNode(firstBlock)\n      ? firstBlock.getLastChild()\n      : null;\n    if ($isLineBreakNode(lastChild) && lastInsertedBlock !== firstBlock) {\n      lastChild.remove();\n    }\n  }\n\n  /**\n   * Inserts a new ParagraphNode into the EditorState at the current Selection\n   *\n   * @returns the newly inserted node.\n   */\n  insertParagraph(): ElementNode | null {\n    if (this.anchor.key === 'root') {\n      const paragraph = $createParagraphNode();\n      $getRoot().splice(this.anchor.offset, 0, [paragraph]);\n      paragraph.select();\n      return paragraph;\n    }\n    const index = $removeTextAndSplitBlock(this);\n    const block = $getAncestor(this.anchor.getNode(), INTERNAL_$isBlock)!;\n    invariant($isElementNode(block), 'Expected ancestor to be an ElementNode');\n    const firstToAppend = block.getChildAtIndex(index);\n    const nodesToInsert = firstToAppend\n      ? [firstToAppend, ...firstToAppend.getNextSiblings()]\n      : [];\n    const newBlock = block.insertNewAfter(this, false) as ElementNode | null;\n    if (newBlock) {\n      newBlock.append(...nodesToInsert);\n      newBlock.selectStart();\n      return newBlock;\n    }\n    // if newBlock is null, it means that block is of type CodeNode.\n    return null;\n  }\n\n  /**\n   * Inserts a logical linebreak, which may be a new LineBreakNode or a new ParagraphNode, into the EditorState at the\n   * current Selection.\n   */\n  insertLineBreak(selectStart?: boolean): void {\n    const lineBreak = $createLineBreakNode();\n    this.insertNodes([lineBreak]);\n    // this is used in MacOS with the command 'ctrl-O' (openLineBreak)\n    if (selectStart) {\n      const parent = lineBreak.getParentOrThrow();\n      const index = lineBreak.getIndexWithinParent();\n      parent.select(index, index);\n    }\n  }\n\n  /**\n   * Extracts the nodes in the Selection, splitting nodes where necessary\n   * to get offset-level precision.\n   *\n   * @returns The nodes in the Selection\n   */\n  extract(): Array<LexicalNode> {\n    const selectedNodes = this.getNodes();\n    const selectedNodesLength = selectedNodes.length;\n    const lastIndex = selectedNodesLength - 1;\n    const anchor = this.anchor;\n    const focus = this.focus;\n    let firstNode = selectedNodes[0];\n    let lastNode = selectedNodes[lastIndex];\n    const [anchorOffset, focusOffset] = $getCharacterOffsets(this);\n\n    if (selectedNodesLength === 0) {\n      return [];\n    } else if (selectedNodesLength === 1) {\n      if ($isTextNode(firstNode) && !this.isCollapsed()) {\n        const startOffset =\n          anchorOffset > focusOffset ? focusOffset : anchorOffset;\n        const endOffset =\n          anchorOffset > focusOffset ? anchorOffset : focusOffset;\n        const splitNodes = firstNode.splitText(startOffset, endOffset);\n        const node = startOffset === 0 ? splitNodes[0] : splitNodes[1];\n        return node != null ? [node] : [];\n      }\n      return [firstNode];\n    }\n    const isBefore = anchor.isBefore(focus);\n\n    if ($isTextNode(firstNode)) {\n      const startOffset = isBefore ? anchorOffset : focusOffset;\n      if (startOffset === firstNode.getTextContentSize()) {\n        selectedNodes.shift();\n      } else if (startOffset !== 0) {\n        [, firstNode] = firstNode.splitText(startOffset);\n        selectedNodes[0] = firstNode;\n      }\n    }\n    if ($isTextNode(lastNode)) {\n      const lastNodeText = lastNode.getTextContent();\n      const lastNodeTextLength = lastNodeText.length;\n      const endOffset = isBefore ? focusOffset : anchorOffset;\n      if (endOffset === 0) {\n        selectedNodes.pop();\n      } else if (endOffset !== lastNodeTextLength) {\n        [lastNode] = lastNode.splitText(endOffset);\n        selectedNodes[lastIndex] = lastNode;\n      }\n    }\n    return selectedNodes;\n  }\n\n  /**\n   * Modifies the Selection according to the parameters and a set of heuristics that account for\n   * various node types. Can be used to safely move or extend selection by one logical \"unit\" without\n   * dealing explicitly with all the possible node types.\n   *\n   * @param alter the type of modification to perform\n   * @param isBackward whether or not selection is backwards\n   * @param granularity the granularity at which to apply the modification\n   */\n  modify(\n    alter: 'move' | 'extend',\n    isBackward: boolean,\n    granularity: 'character' | 'word' | 'lineboundary',\n  ): void {\n    const focus = this.focus;\n    const anchor = this.anchor;\n    const collapse = alter === 'move';\n\n    // Handle the selection movement around decorators.\n    const possibleNode = $getAdjacentNode(focus, isBackward);\n    if ($isDecoratorNode(possibleNode) && !possibleNode.isIsolated()) {\n      // Make it possible to move selection from range selection to\n      // node selection on the node.\n      if (collapse && possibleNode.isKeyboardSelectable()) {\n        const nodeSelection = $createNodeSelection();\n        nodeSelection.add(possibleNode.__key);\n        $setSelection(nodeSelection);\n        return;\n      }\n      const sibling = isBackward\n        ? possibleNode.getPreviousSibling()\n        : possibleNode.getNextSibling();\n\n      if (!$isTextNode(sibling)) {\n        const parent = possibleNode.getParentOrThrow();\n        let offset;\n        let elementKey;\n\n        if ($isElementNode(sibling)) {\n          elementKey = sibling.__key;\n          offset = isBackward ? sibling.getChildrenSize() : 0;\n        } else {\n          offset = possibleNode.getIndexWithinParent();\n          elementKey = parent.__key;\n          if (!isBackward) {\n            offset++;\n          }\n        }\n        focus.set(elementKey, offset, 'element');\n        if (collapse) {\n          anchor.set(elementKey, offset, 'element');\n        }\n        return;\n      } else {\n        const siblingKey = sibling.__key;\n        const offset = isBackward ? sibling.getTextContent().length : 0;\n        focus.set(siblingKey, offset, 'text');\n        if (collapse) {\n          anchor.set(siblingKey, offset, 'text');\n        }\n        return;\n      }\n    }\n    const editor = getActiveEditor();\n    const domSelection = getDOMSelection(editor._window);\n\n    if (!domSelection) {\n      return;\n    }\n    const blockCursorElement = editor._blockCursorElement;\n    const rootElement = editor._rootElement;\n    // Remove the block cursor element if it exists. This will ensure selection\n    // works as intended. If we leave it in the DOM all sorts of strange bugs\n    // occur. :/\n    if (\n      rootElement !== null &&\n      blockCursorElement !== null &&\n      $isElementNode(possibleNode) &&\n      !possibleNode.isInline() &&\n      !possibleNode.canBeEmpty()\n    ) {\n      removeDOMBlockCursorElement(blockCursorElement, editor, rootElement);\n    }\n    // We use the DOM selection.modify API here to \"tell\" us what the selection\n    // will be. We then use it to update the Lexical selection accordingly. This\n    // is much more reliable than waiting for a beforeinput and using the ranges\n    // from getTargetRanges(), and is also better than trying to do it ourselves\n    // using Intl.Segmenter or other workarounds that struggle with word segments\n    // and line segments (especially with word wrapping and non-Roman languages).\n    moveNativeSelection(\n      domSelection,\n      alter,\n      isBackward ? 'backward' : 'forward',\n      granularity,\n    );\n    // Guard against no ranges\n    if (domSelection.rangeCount > 0) {\n      const range = domSelection.getRangeAt(0);\n      // Apply the DOM selection to our Lexical selection.\n      const anchorNode = this.anchor.getNode();\n      const root = $isRootNode(anchorNode)\n        ? anchorNode\n        : $getNearestRootOrShadowRoot(anchorNode);\n      this.applyDOMRange(range);\n      this.dirty = true;\n      if (!collapse) {\n        // Validate selection; make sure that the new extended selection respects shadow roots\n        const nodes = this.getNodes();\n        const validNodes = [];\n        let shrinkSelection = false;\n        for (let i = 0; i < nodes.length; i++) {\n          const nextNode = nodes[i];\n          if ($hasAncestor(nextNode, root)) {\n            validNodes.push(nextNode);\n          } else {\n            shrinkSelection = true;\n          }\n        }\n        if (shrinkSelection && validNodes.length > 0) {\n          // validNodes length check is a safeguard against an invalid selection; as getNodes()\n          // will return an empty array in this case\n          if (isBackward) {\n            const firstValidNode = validNodes[0];\n            if ($isElementNode(firstValidNode)) {\n              firstValidNode.selectStart();\n            } else {\n              firstValidNode.getParentOrThrow().selectStart();\n            }\n          } else {\n            const lastValidNode = validNodes[validNodes.length - 1];\n            if ($isElementNode(lastValidNode)) {\n              lastValidNode.selectEnd();\n            } else {\n              lastValidNode.getParentOrThrow().selectEnd();\n            }\n          }\n        }\n\n        // Because a range works on start and end, we might need to flip\n        // the anchor and focus points to match what the DOM has, not what\n        // the range has specifically.\n        if (\n          domSelection.anchorNode !== range.startContainer ||\n          domSelection.anchorOffset !== range.startOffset\n        ) {\n          $swapPoints(this);\n        }\n      }\n    }\n  }\n  /**\n   * Helper for handling forward character and word deletion that prevents element nodes\n   * like a table, columns layout being destroyed\n   *\n   * @param anchor the anchor\n   * @param anchorNode the anchor node in the selection\n   * @param isBackward whether or not selection is backwards\n   */\n  forwardDeletion(\n    anchor: PointType,\n    anchorNode: TextNode | ElementNode,\n    isBackward: boolean,\n  ): boolean {\n    if (\n      !isBackward &&\n      // Delete forward handle case\n      ((anchor.type === 'element' &&\n        $isElementNode(anchorNode) &&\n        anchor.offset === anchorNode.getChildrenSize()) ||\n        (anchor.type === 'text' &&\n          anchor.offset === anchorNode.getTextContentSize()))\n    ) {\n      const parent = anchorNode.getParent();\n      const nextSibling =\n        anchorNode.getNextSibling() ||\n        (parent === null ? null : parent.getNextSibling());\n\n      if ($isElementNode(nextSibling) && nextSibling.isShadowRoot()) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  /**\n   * Performs one logical character deletion operation on the EditorState based on the current Selection.\n   * Handles different node types.\n   *\n   * @param isBackward whether or not the selection is backwards.\n   */\n  deleteCharacter(isBackward: boolean): void {\n    const wasCollapsed = this.isCollapsed();\n    if (this.isCollapsed()) {\n      const anchor = this.anchor;\n      let anchorNode: TextNode | ElementNode | null = anchor.getNode();\n      if (this.forwardDeletion(anchor, anchorNode, isBackward)) {\n        return;\n      }\n\n      // Handle the deletion around decorators.\n      const focus = this.focus;\n      const possibleNode = $getAdjacentNode(focus, isBackward);\n      if ($isDecoratorNode(possibleNode) && !possibleNode.isIsolated()) {\n        // Make it possible to move selection from range selection to\n        // node selection on the node.\n        if (\n          possibleNode.isKeyboardSelectable() &&\n          $isElementNode(anchorNode) &&\n          anchorNode.getChildrenSize() === 0\n        ) {\n          anchorNode.remove();\n          const nodeSelection = $createNodeSelection();\n          nodeSelection.add(possibleNode.__key);\n          $setSelection(nodeSelection);\n        } else {\n          possibleNode.remove();\n          const editor = getActiveEditor();\n          editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);\n        }\n        return;\n      } else if (\n        !isBackward &&\n        $isElementNode(possibleNode) &&\n        $isElementNode(anchorNode) &&\n        anchorNode.isEmpty()\n      ) {\n        anchorNode.remove();\n        possibleNode.selectStart();\n        return;\n      }\n      this.modify('extend', isBackward, 'character');\n\n      if (!this.isCollapsed()) {\n        const focusNode = focus.type === 'text' ? focus.getNode() : null;\n        anchorNode = anchor.type === 'text' ? anchor.getNode() : null;\n\n        if (focusNode !== null && focusNode.isSegmented()) {\n          const offset = focus.offset;\n          const textContentSize = focusNode.getTextContentSize();\n          if (\n            focusNode.is(anchorNode) ||\n            (isBackward && offset !== textContentSize) ||\n            (!isBackward && offset !== 0)\n          ) {\n            $removeSegment(focusNode, isBackward, offset);\n            return;\n          }\n        } else if (anchorNode !== null && anchorNode.isSegmented()) {\n          const offset = anchor.offset;\n          const textContentSize = anchorNode.getTextContentSize();\n          if (\n            anchorNode.is(focusNode) ||\n            (isBackward && offset !== 0) ||\n            (!isBackward && offset !== textContentSize)\n          ) {\n            $removeSegment(anchorNode, isBackward, offset);\n            return;\n          }\n        }\n        $updateCaretSelectionForUnicodeCharacter(this, isBackward);\n      } else if (isBackward && anchor.offset === 0) {\n        // Special handling around rich text nodes\n        const element =\n          anchor.type === 'element'\n            ? anchor.getNode()\n            : anchor.getNode().getParentOrThrow();\n        if (element.collapseAtStart(this)) {\n          return;\n        }\n      }\n    }\n    this.removeText();\n    if (\n      isBackward &&\n      !wasCollapsed &&\n      this.isCollapsed() &&\n      this.anchor.type === 'element' &&\n      this.anchor.offset === 0\n    ) {\n      const anchorNode = this.anchor.getNode();\n      if (\n        anchorNode.isEmpty() &&\n        $isRootNode(anchorNode.getParent()) &&\n        anchorNode.getIndexWithinParent() === 0\n      ) {\n        anchorNode.collapseAtStart(this);\n      }\n    }\n  }\n\n  /**\n   * Performs one logical line deletion operation on the EditorState based on the current Selection.\n   * Handles different node types.\n   *\n   * @param isBackward whether or not the selection is backwards.\n   */\n  deleteLine(isBackward: boolean): void {\n    if (this.isCollapsed()) {\n      // Since `domSelection.modify('extend', ..., 'lineboundary')` works well for text selections\n      // but doesn't properly handle selections which end on elements, a space character is added\n      // for such selections transforming their anchor's type to 'text'\n      const anchorIsElement = this.anchor.type === 'element';\n      if (anchorIsElement) {\n        this.insertText(' ');\n      }\n\n      this.modify('extend', isBackward, 'lineboundary');\n\n      // If selection is extended to cover text edge then extend it one character more\n      // to delete its parent element. Otherwise text content will be deleted but empty\n      // parent node will remain\n      const endPoint = isBackward ? this.focus : this.anchor;\n      if (endPoint.offset === 0) {\n        this.modify('extend', isBackward, 'character');\n      }\n\n      // Adjusts selection to include an extra character added for element anchors to remove it\n      if (anchorIsElement) {\n        const startPoint = isBackward ? this.anchor : this.focus;\n        startPoint.set(startPoint.key, startPoint.offset + 1, startPoint.type);\n      }\n    }\n    this.removeText();\n  }\n\n  /**\n   * Performs one logical word deletion operation on the EditorState based on the current Selection.\n   * Handles different node types.\n   *\n   * @param isBackward whether or not the selection is backwards.\n   */\n  deleteWord(isBackward: boolean): void {\n    if (this.isCollapsed()) {\n      const anchor = this.anchor;\n      const anchorNode: TextNode | ElementNode | null = anchor.getNode();\n      if (this.forwardDeletion(anchor, anchorNode, isBackward)) {\n        return;\n      }\n      this.modify('extend', isBackward, 'word');\n    }\n    this.removeText();\n  }\n\n  /**\n   * Returns whether the Selection is \"backwards\", meaning the focus\n   * logically precedes the anchor in the EditorState.\n   * @returns true if the Selection is backwards, false otherwise.\n   */\n  isBackward(): boolean {\n    return this.focus.isBefore(this.anchor);\n  }\n\n  getStartEndPoints(): null | [PointType, PointType] {\n    return [this.anchor, this.focus];\n  }\n}\n\nexport function $isNodeSelection(x: unknown): x is NodeSelection {\n  return x instanceof NodeSelection;\n}\n\nfunction getCharacterOffset(point: PointType): number {\n  const offset = point.offset;\n  if (point.type === 'text') {\n    return offset;\n  }\n\n  const parent = point.getNode();\n  return offset === parent.getChildrenSize()\n    ? parent.getTextContent().length\n    : 0;\n}\n\nexport function $getCharacterOffsets(\n  selection: BaseSelection,\n): [number, number] {\n  const anchorAndFocus = selection.getStartEndPoints();\n  if (anchorAndFocus === null) {\n    return [0, 0];\n  }\n  const [anchor, focus] = anchorAndFocus;\n  if (\n    anchor.type === 'element' &&\n    focus.type === 'element' &&\n    anchor.key === focus.key &&\n    anchor.offset === focus.offset\n  ) {\n    return [0, 0];\n  }\n  return [getCharacterOffset(anchor), getCharacterOffset(focus)];\n}\n\nfunction $swapPoints(selection: RangeSelection): void {\n  const focus = selection.focus;\n  const anchor = selection.anchor;\n  const anchorKey = anchor.key;\n  const anchorOffset = anchor.offset;\n  const anchorType = anchor.type;\n\n  $setPointValues(anchor, focus.key, focus.offset, focus.type);\n  $setPointValues(focus, anchorKey, anchorOffset, anchorType);\n  selection._cachedNodes = null;\n}\n\nfunction moveNativeSelection(\n  domSelection: Selection,\n  alter: 'move' | 'extend',\n  direction: 'backward' | 'forward' | 'left' | 'right',\n  granularity: 'character' | 'word' | 'lineboundary',\n): void {\n  // Selection.modify() method applies a change to the current selection or cursor position,\n  // but is still non-standard in some browsers.\n  domSelection.modify(alter, direction, granularity);\n}\n\nfunction $updateCaretSelectionForUnicodeCharacter(\n  selection: RangeSelection,\n  isBackward: boolean,\n): void {\n  const anchor = selection.anchor;\n  const focus = selection.focus;\n  const anchorNode = anchor.getNode();\n  const focusNode = focus.getNode();\n\n  if (\n    anchorNode === focusNode &&\n    anchor.type === 'text' &&\n    focus.type === 'text'\n  ) {\n    // Handling of multibyte characters\n    const anchorOffset = anchor.offset;\n    const focusOffset = focus.offset;\n    const isBefore = anchorOffset < focusOffset;\n    const startOffset = isBefore ? anchorOffset : focusOffset;\n    const endOffset = isBefore ? focusOffset : anchorOffset;\n    const characterOffset = endOffset - 1;\n\n    if (startOffset !== characterOffset) {\n      const text = anchorNode.getTextContent().slice(startOffset, endOffset);\n      if (!doesContainGrapheme(text)) {\n        if (isBackward) {\n          focus.offset = characterOffset;\n        } else {\n          anchor.offset = characterOffset;\n        }\n      }\n    }\n  } else {\n    // TODO Handling of multibyte characters\n  }\n}\n\nfunction $removeSegment(\n  node: TextNode,\n  isBackward: boolean,\n  offset: number,\n): void {\n  const textNode = node;\n  const textContent = textNode.getTextContent();\n  const split = textContent.split(/(?=\\s)/g);\n  const splitLength = split.length;\n  let segmentOffset = 0;\n  let restoreOffset: number | undefined = 0;\n\n  for (let i = 0; i < splitLength; i++) {\n    const text = split[i];\n    const isLast = i === splitLength - 1;\n    restoreOffset = segmentOffset;\n    segmentOffset += text.length;\n\n    if (\n      (isBackward && segmentOffset === offset) ||\n      segmentOffset > offset ||\n      isLast\n    ) {\n      split.splice(i, 1);\n      if (isLast) {\n        restoreOffset = undefined;\n      }\n      break;\n    }\n  }\n  const nextTextContent = split.join('').trim();\n\n  if (nextTextContent === '') {\n    textNode.remove();\n  } else {\n    textNode.setTextContent(nextTextContent);\n    textNode.select(restoreOffset, restoreOffset);\n  }\n}\n\nfunction shouldResolveAncestor(\n  resolvedElement: ElementNode,\n  resolvedOffset: number,\n  lastPoint: null | PointType,\n): boolean {\n  const parent = resolvedElement.getParent();\n  return (\n    lastPoint === null ||\n    parent === null ||\n    !parent.canBeEmpty() ||\n    parent !== lastPoint.getNode()\n  );\n}\n\nfunction $internalResolveSelectionPoint(\n  dom: Node,\n  offset: number,\n  lastPoint: null | PointType,\n  editor: LexicalEditor,\n): null | PointType {\n  let resolvedOffset = offset;\n  let resolvedNode: TextNode | LexicalNode | null;\n  // If we have selection on an element, we will\n  // need to figure out (using the offset) what text\n  // node should be selected.\n\n  if (dom.nodeType === DOM_ELEMENT_TYPE) {\n    // Resolve element to a ElementNode, or TextNode, or null\n    let moveSelectionToEnd = false;\n    // Given we're moving selection to another node, selection is\n    // definitely dirty.\n    // We use the anchor to find which child node to select\n    const childNodes = dom.childNodes;\n    const childNodesLength = childNodes.length;\n    const blockCursorElement = editor._blockCursorElement;\n    // If the anchor is the same as length, then this means we\n    // need to select the very last text node.\n    if (resolvedOffset === childNodesLength) {\n      moveSelectionToEnd = true;\n      resolvedOffset = childNodesLength - 1;\n    }\n    let childDOM = childNodes[resolvedOffset];\n    let hasBlockCursor = false;\n    if (childDOM === blockCursorElement) {\n      childDOM = childNodes[resolvedOffset + 1];\n      hasBlockCursor = true;\n    } else if (blockCursorElement !== null) {\n      const blockCursorElementParent = blockCursorElement.parentNode;\n      if (dom === blockCursorElementParent) {\n        const blockCursorOffset = Array.prototype.indexOf.call(\n          blockCursorElementParent.children,\n          blockCursorElement,\n        );\n        if (offset > blockCursorOffset) {\n          resolvedOffset--;\n        }\n      }\n    }\n    resolvedNode = $getNodeFromDOM(childDOM);\n\n    if ($isTextNode(resolvedNode)) {\n      resolvedOffset = getTextNodeOffset(resolvedNode, moveSelectionToEnd);\n    } else {\n      let resolvedElement = $getNodeFromDOM(dom);\n      // Ensure resolvedElement is actually a element.\n      if (resolvedElement === null) {\n        return null;\n      }\n      if ($isElementNode(resolvedElement)) {\n        resolvedOffset = Math.min(\n          resolvedElement.getChildrenSize(),\n          resolvedOffset,\n        );\n        let child = resolvedElement.getChildAtIndex(resolvedOffset);\n        if (\n          $isElementNode(child) &&\n          shouldResolveAncestor(child, resolvedOffset, lastPoint)\n        ) {\n          const descendant = moveSelectionToEnd\n            ? child.getLastDescendant()\n            : child.getFirstDescendant();\n          if (descendant === null) {\n            resolvedElement = child;\n          } else {\n            child = descendant;\n            resolvedElement = $isElementNode(child)\n              ? child\n              : child.getParentOrThrow();\n          }\n          resolvedOffset = 0;\n        }\n        if ($isTextNode(child)) {\n          resolvedNode = child;\n          resolvedElement = null;\n          resolvedOffset = getTextNodeOffset(child, moveSelectionToEnd);\n        } else if (\n          child !== resolvedElement &&\n          moveSelectionToEnd &&\n          !hasBlockCursor\n        ) {\n          resolvedOffset++;\n        }\n      } else {\n        const index = resolvedElement.getIndexWithinParent();\n        // When selecting decorators, there can be some selection issues when using resolvedOffset,\n        // and instead we should be checking if we're using the offset\n        if (\n          offset === 0 &&\n          $isDecoratorNode(resolvedElement) &&\n          $getNodeFromDOM(dom) === resolvedElement\n        ) {\n          resolvedOffset = index;\n        } else {\n          resolvedOffset = index + 1;\n        }\n        resolvedElement = resolvedElement.getParentOrThrow();\n      }\n      if ($isElementNode(resolvedElement)) {\n        return $createPoint(resolvedElement.__key, resolvedOffset, 'element');\n      }\n    }\n  } else {\n    // TextNode or null\n    resolvedNode = $getNodeFromDOM(dom);\n  }\n  if (!$isTextNode(resolvedNode)) {\n    return null;\n  }\n  return $createPoint(resolvedNode.__key, resolvedOffset, 'text');\n}\n\nfunction resolveSelectionPointOnBoundary(\n  point: TextPointType,\n  isBackward: boolean,\n  isCollapsed: boolean,\n): void {\n  const offset = point.offset;\n  const node = point.getNode();\n\n  if (offset === 0) {\n    const prevSibling = node.getPreviousSibling();\n    const parent = node.getParent();\n\n    if (!isBackward) {\n      if (\n        $isElementNode(prevSibling) &&\n        !isCollapsed &&\n        prevSibling.isInline()\n      ) {\n        point.key = prevSibling.__key;\n        point.offset = prevSibling.getChildrenSize();\n        // @ts-expect-error: intentional\n        point.type = 'element';\n      } else if ($isTextNode(prevSibling)) {\n        point.key = prevSibling.__key;\n        point.offset = prevSibling.getTextContent().length;\n      }\n    } else if (\n      (isCollapsed || !isBackward) &&\n      prevSibling === null &&\n      $isElementNode(parent) &&\n      parent.isInline()\n    ) {\n      const parentSibling = parent.getPreviousSibling();\n      if ($isTextNode(parentSibling)) {\n        point.key = parentSibling.__key;\n        point.offset = parentSibling.getTextContent().length;\n      }\n    }\n  } else if (offset === node.getTextContent().length) {\n    const nextSibling = node.getNextSibling();\n    const parent = node.getParent();\n\n    if (isBackward && $isElementNode(nextSibling) && nextSibling.isInline()) {\n      point.key = nextSibling.__key;\n      point.offset = 0;\n      // @ts-expect-error: intentional\n      point.type = 'element';\n    } else if (\n      (isCollapsed || isBackward) &&\n      nextSibling === null &&\n      $isElementNode(parent) &&\n      parent.isInline() &&\n      !parent.canInsertTextAfter()\n    ) {\n      const parentSibling = parent.getNextSibling();\n      if ($isTextNode(parentSibling)) {\n        point.key = parentSibling.__key;\n        point.offset = 0;\n      }\n    }\n  }\n}\n\nfunction $normalizeSelectionPointsForBoundaries(\n  anchor: PointType,\n  focus: PointType,\n  lastSelection: null | BaseSelection,\n): void {\n  if (anchor.type === 'text' && focus.type === 'text') {\n    const isBackward = anchor.isBefore(focus);\n    const isCollapsed = anchor.is(focus);\n\n    // Attempt to normalize the offset to the previous sibling if we're at the\n    // start of a text node and the sibling is a text node or inline element.\n    resolveSelectionPointOnBoundary(anchor, isBackward, isCollapsed);\n    resolveSelectionPointOnBoundary(focus, !isBackward, isCollapsed);\n\n    if (isCollapsed) {\n      focus.key = anchor.key;\n      focus.offset = anchor.offset;\n      focus.type = anchor.type;\n    }\n    const editor = getActiveEditor();\n\n    if (\n      editor.isComposing() &&\n      editor._compositionKey !== anchor.key &&\n      $isRangeSelection(lastSelection)\n    ) {\n      const lastAnchor = lastSelection.anchor;\n      const lastFocus = lastSelection.focus;\n      $setPointValues(\n        anchor,\n        lastAnchor.key,\n        lastAnchor.offset,\n        lastAnchor.type,\n      );\n      $setPointValues(focus, lastFocus.key, lastFocus.offset, lastFocus.type);\n    }\n  }\n}\n\nfunction $internalResolveSelectionPoints(\n  anchorDOM: null | Node,\n  anchorOffset: number,\n  focusDOM: null | Node,\n  focusOffset: number,\n  editor: LexicalEditor,\n  lastSelection: null | BaseSelection,\n): null | [PointType, PointType] {\n  if (\n    anchorDOM === null ||\n    focusDOM === null ||\n    !isSelectionWithinEditor(editor, anchorDOM, focusDOM)\n  ) {\n    return null;\n  }\n  const resolvedAnchorPoint = $internalResolveSelectionPoint(\n    anchorDOM,\n    anchorOffset,\n    $isRangeSelection(lastSelection) ? lastSelection.anchor : null,\n    editor,\n  );\n  if (resolvedAnchorPoint === null) {\n    return null;\n  }\n  const resolvedFocusPoint = $internalResolveSelectionPoint(\n    focusDOM,\n    focusOffset,\n    $isRangeSelection(lastSelection) ? lastSelection.focus : null,\n    editor,\n  );\n  if (resolvedFocusPoint === null) {\n    return null;\n  }\n  if (\n    resolvedAnchorPoint.type === 'element' &&\n    resolvedFocusPoint.type === 'element'\n  ) {\n    const anchorNode = $getNodeFromDOM(anchorDOM);\n    const focusNode = $getNodeFromDOM(focusDOM);\n    // Ensure if we're selecting the content of a decorator that we\n    // return null for this point, as it's not in the controlled scope\n    // of Lexical.\n    if ($isDecoratorNode(anchorNode) && $isDecoratorNode(focusNode)) {\n      return null;\n    }\n  }\n\n  // Handle normalization of selection when it is at the boundaries.\n  $normalizeSelectionPointsForBoundaries(\n    resolvedAnchorPoint,\n    resolvedFocusPoint,\n    lastSelection,\n  );\n\n  return [resolvedAnchorPoint, resolvedFocusPoint];\n}\n\nexport function $isBlockElementNode(\n  node: LexicalNode | null | undefined,\n): node is ElementNode {\n  return $isElementNode(node) && !node.isInline();\n}\n\n// This is used to make a selection when the existing\n// selection is null, i.e. forcing selection on the editor\n// when it current exists outside the editor.\n\nexport function $internalMakeRangeSelection(\n  anchorKey: NodeKey,\n  anchorOffset: number,\n  focusKey: NodeKey,\n  focusOffset: number,\n  anchorType: 'text' | 'element',\n  focusType: 'text' | 'element',\n): RangeSelection {\n  const editorState = getActiveEditorState();\n  const selection = new RangeSelection(\n    $createPoint(anchorKey, anchorOffset, anchorType),\n    $createPoint(focusKey, focusOffset, focusType),\n    0,\n    '',\n  );\n  selection.dirty = true;\n  editorState._selection = selection;\n  return selection;\n}\n\nexport function $createRangeSelection(): RangeSelection {\n  const anchor = $createPoint('root', 0, 'element');\n  const focus = $createPoint('root', 0, 'element');\n  return new RangeSelection(anchor, focus, 0, '');\n}\n\nexport function $createNodeSelection(): NodeSelection {\n  return new NodeSelection(new Set());\n}\n\nexport function $internalCreateSelection(\n  editor: LexicalEditor,\n): null | BaseSelection {\n  const currentEditorState = editor.getEditorState();\n  const lastSelection = currentEditorState._selection;\n  const domSelection = getDOMSelection(editor._window);\n\n  if ($isRangeSelection(lastSelection) || lastSelection == null) {\n    return $internalCreateRangeSelection(\n      lastSelection,\n      domSelection,\n      editor,\n      null,\n    );\n  }\n  return lastSelection.clone();\n}\n\nexport function $createRangeSelectionFromDom(\n  domSelection: Selection | null,\n  editor: LexicalEditor,\n): null | RangeSelection {\n  return $internalCreateRangeSelection(null, domSelection, editor, null);\n}\n\nexport function $internalCreateRangeSelection(\n  lastSelection: null | BaseSelection,\n  domSelection: Selection | null,\n  editor: LexicalEditor,\n  event: UIEvent | Event | null,\n): null | RangeSelection {\n  const windowObj = editor._window;\n  if (windowObj === null) {\n    return null;\n  }\n  // When we create a selection, we try to use the previous\n  // selection where possible, unless an actual user selection\n  // change has occurred. When we do need to create a new selection\n  // we validate we can have text nodes for both anchor and focus\n  // nodes. If that holds true, we then return that selection\n  // as a mutable object that we use for the editor state for this\n  // update cycle. If a selection gets changed, and requires a\n  // update to native DOM selection, it gets marked as \"dirty\".\n  // If the selection changes, but matches with the existing\n  // DOM selection, then we only need to sync it. Otherwise,\n  // we generally bail out of doing an update to selection during\n  // reconciliation unless there are dirty nodes that need\n  // reconciling.\n\n  const windowEvent = event || windowObj.event;\n  const eventType = windowEvent ? windowEvent.type : undefined;\n  const isSelectionChange = eventType === 'selectionchange';\n  const useDOMSelection =\n    !getIsProcessingMutations() &&\n    (isSelectionChange ||\n      eventType === 'beforeinput' ||\n      eventType === 'compositionstart' ||\n      eventType === 'compositionend' ||\n      (eventType === 'click' &&\n        windowEvent &&\n        (windowEvent as InputEvent).detail === 3) ||\n      eventType === 'drop' ||\n      eventType === undefined);\n  let anchorDOM, focusDOM, anchorOffset, focusOffset;\n\n  if (!$isRangeSelection(lastSelection) || useDOMSelection) {\n    if (domSelection === null) {\n      return null;\n    }\n    anchorDOM = domSelection.anchorNode;\n    focusDOM = domSelection.focusNode;\n    anchorOffset = domSelection.anchorOffset;\n    focusOffset = domSelection.focusOffset;\n    if (\n      isSelectionChange &&\n      $isRangeSelection(lastSelection) &&\n      !isSelectionWithinEditor(editor, anchorDOM, focusDOM)\n    ) {\n      return lastSelection.clone();\n    }\n  } else {\n    return lastSelection.clone();\n  }\n  // Let's resolve the text nodes from the offsets and DOM nodes we have from\n  // native selection.\n  const resolvedSelectionPoints = $internalResolveSelectionPoints(\n    anchorDOM,\n    anchorOffset,\n    focusDOM,\n    focusOffset,\n    editor,\n    lastSelection,\n  );\n  if (resolvedSelectionPoints === null) {\n    return null;\n  }\n  const [resolvedAnchorPoint, resolvedFocusPoint] = resolvedSelectionPoints;\n  return new RangeSelection(\n    resolvedAnchorPoint,\n    resolvedFocusPoint,\n    !$isRangeSelection(lastSelection) ? 0 : lastSelection.format,\n    !$isRangeSelection(lastSelection) ? '' : lastSelection.style,\n  );\n}\n\nexport function $getSelection(): null | BaseSelection {\n  const editorState = getActiveEditorState();\n  return editorState._selection;\n}\n\nexport function $getPreviousSelection(): null | BaseSelection {\n  const editor = getActiveEditor();\n  return editor._editorState._selection;\n}\n\nexport function $updateElementSelectionOnCreateDeleteNode(\n  selection: RangeSelection,\n  parentNode: LexicalNode,\n  nodeOffset: number,\n  times = 1,\n): void {\n  const anchor = selection.anchor;\n  const focus = selection.focus;\n  const anchorNode = anchor.getNode();\n  const focusNode = focus.getNode();\n  if (!parentNode.is(anchorNode) && !parentNode.is(focusNode)) {\n    return;\n  }\n  const parentKey = parentNode.__key;\n  // Single node. We shift selection but never redimension it\n  if (selection.isCollapsed()) {\n    const selectionOffset = anchor.offset;\n    if (\n      (nodeOffset <= selectionOffset && times > 0) ||\n      (nodeOffset < selectionOffset && times < 0)\n    ) {\n      const newSelectionOffset = Math.max(0, selectionOffset + times);\n      anchor.set(parentKey, newSelectionOffset, 'element');\n      focus.set(parentKey, newSelectionOffset, 'element');\n      // The new selection might point to text nodes, try to resolve them\n      $updateSelectionResolveTextNodes(selection);\n    }\n  } else {\n    // Multiple nodes selected. We shift or redimension selection\n    const isBackward = selection.isBackward();\n    const firstPoint = isBackward ? focus : anchor;\n    const firstPointNode = firstPoint.getNode();\n    const lastPoint = isBackward ? anchor : focus;\n    const lastPointNode = lastPoint.getNode();\n    if (parentNode.is(firstPointNode)) {\n      const firstPointOffset = firstPoint.offset;\n      if (\n        (nodeOffset <= firstPointOffset && times > 0) ||\n        (nodeOffset < firstPointOffset && times < 0)\n      ) {\n        firstPoint.set(\n          parentKey,\n          Math.max(0, firstPointOffset + times),\n          'element',\n        );\n      }\n    }\n    if (parentNode.is(lastPointNode)) {\n      const lastPointOffset = lastPoint.offset;\n      if (\n        (nodeOffset <= lastPointOffset && times > 0) ||\n        (nodeOffset < lastPointOffset && times < 0)\n      ) {\n        lastPoint.set(\n          parentKey,\n          Math.max(0, lastPointOffset + times),\n          'element',\n        );\n      }\n    }\n  }\n  // The new selection might point to text nodes, try to resolve them\n  $updateSelectionResolveTextNodes(selection);\n}\n\nfunction $updateSelectionResolveTextNodes(selection: RangeSelection): void {\n  const anchor = selection.anchor;\n  const anchorOffset = anchor.offset;\n  const focus = selection.focus;\n  const focusOffset = focus.offset;\n  const anchorNode = anchor.getNode();\n  const focusNode = focus.getNode();\n  if (selection.isCollapsed()) {\n    if (!$isElementNode(anchorNode)) {\n      return;\n    }\n    const childSize = anchorNode.getChildrenSize();\n    const anchorOffsetAtEnd = anchorOffset >= childSize;\n    const child = anchorOffsetAtEnd\n      ? anchorNode.getChildAtIndex(childSize - 1)\n      : anchorNode.getChildAtIndex(anchorOffset);\n    if ($isTextNode(child)) {\n      let newOffset = 0;\n      if (anchorOffsetAtEnd) {\n        newOffset = child.getTextContentSize();\n      }\n      anchor.set(child.__key, newOffset, 'text');\n      focus.set(child.__key, newOffset, 'text');\n    }\n    return;\n  }\n  if ($isElementNode(anchorNode)) {\n    const childSize = anchorNode.getChildrenSize();\n    const anchorOffsetAtEnd = anchorOffset >= childSize;\n    const child = anchorOffsetAtEnd\n      ? anchorNode.getChildAtIndex(childSize - 1)\n      : anchorNode.getChildAtIndex(anchorOffset);\n    if ($isTextNode(child)) {\n      let newOffset = 0;\n      if (anchorOffsetAtEnd) {\n        newOffset = child.getTextContentSize();\n      }\n      anchor.set(child.__key, newOffset, 'text');\n    }\n  }\n  if ($isElementNode(focusNode)) {\n    const childSize = focusNode.getChildrenSize();\n    const focusOffsetAtEnd = focusOffset >= childSize;\n    const child = focusOffsetAtEnd\n      ? focusNode.getChildAtIndex(childSize - 1)\n      : focusNode.getChildAtIndex(focusOffset);\n    if ($isTextNode(child)) {\n      let newOffset = 0;\n      if (focusOffsetAtEnd) {\n        newOffset = child.getTextContentSize();\n      }\n      focus.set(child.__key, newOffset, 'text');\n    }\n  }\n}\n\nexport function applySelectionTransforms(\n  nextEditorState: EditorState,\n  editor: LexicalEditor,\n): void {\n  const prevEditorState = editor.getEditorState();\n  const prevSelection = prevEditorState._selection;\n  const nextSelection = nextEditorState._selection;\n  if ($isRangeSelection(nextSelection)) {\n    const anchor = nextSelection.anchor;\n    const focus = nextSelection.focus;\n    let anchorNode;\n\n    if (anchor.type === 'text') {\n      anchorNode = anchor.getNode();\n      anchorNode.selectionTransform(prevSelection, nextSelection);\n    }\n    if (focus.type === 'text') {\n      const focusNode = focus.getNode();\n      if (anchorNode !== focusNode) {\n        focusNode.selectionTransform(prevSelection, nextSelection);\n      }\n    }\n  }\n}\n\nexport function moveSelectionPointToSibling(\n  point: PointType,\n  node: LexicalNode,\n  parent: ElementNode,\n  prevSibling: LexicalNode | null,\n  nextSibling: LexicalNode | null,\n): void {\n  let siblingKey = null;\n  let offset = 0;\n  let type: 'text' | 'element' | null = null;\n  if (prevSibling !== null) {\n    siblingKey = prevSibling.__key;\n    if ($isTextNode(prevSibling)) {\n      offset = prevSibling.getTextContentSize();\n      type = 'text';\n    } else if ($isElementNode(prevSibling)) {\n      offset = prevSibling.getChildrenSize();\n      type = 'element';\n    }\n  } else {\n    if (nextSibling !== null) {\n      siblingKey = nextSibling.__key;\n      if ($isTextNode(nextSibling)) {\n        type = 'text';\n      } else if ($isElementNode(nextSibling)) {\n        type = 'element';\n      }\n    }\n  }\n  if (siblingKey !== null && type !== null) {\n    point.set(siblingKey, offset, type);\n  } else {\n    offset = node.getIndexWithinParent();\n    if (offset === -1) {\n      // Move selection to end of parent\n      offset = parent.getChildrenSize();\n    }\n    point.set(parent.__key, offset, 'element');\n  }\n}\n\nexport function adjustPointOffsetForMergedSibling(\n  point: PointType,\n  isBefore: boolean,\n  key: NodeKey,\n  target: TextNode,\n  textLength: number,\n): void {\n  if (point.type === 'text') {\n    point.key = key;\n    if (!isBefore) {\n      point.offset += textLength;\n    }\n  } else if (point.offset > target.getIndexWithinParent()) {\n    point.offset -= 1;\n  }\n}\n\nexport function updateDOMSelection(\n  prevSelection: BaseSelection | null,\n  nextSelection: BaseSelection | null,\n  editor: LexicalEditor,\n  domSelection: Selection,\n  tags: Set<string>,\n  rootElement: HTMLElement,\n  nodeCount: number,\n): void {\n  const anchorDOMNode = domSelection.anchorNode;\n  const focusDOMNode = domSelection.focusNode;\n  const anchorOffset = domSelection.anchorOffset;\n  const focusOffset = domSelection.focusOffset;\n  const activeElement = document.activeElement;\n\n  // TODO: make this not hard-coded, and add another config option\n  // that makes this configurable.\n  if (\n    (tags.has('collaboration') && activeElement !== rootElement) ||\n    (activeElement !== null &&\n      isSelectionCapturedInDecoratorInput(activeElement))\n  ) {\n    return;\n  }\n\n  if (!$isRangeSelection(nextSelection)) {\n\n    // If the DOM selection enters a decorator node update the selection to a single node selection\n    if (activeElement !== null && domSelection.isCollapsed && focusDOMNode instanceof Node) {\n      const node = $getNearestNodeFromDOMNode(focusDOMNode);\n      if ($isDecoratorNode(node)) {\n        domSelection.removeAllRanges();\n        $selectSingleNode(node);\n        return;\n      }\n    }\n\n    // We don't remove selection if the prevSelection is null because\n    // of editor.setRootElement(). If this occurs on init when the\n    // editor is already focused, then this can cause the editor to\n    // lose focus.\n    if (\n      prevSelection !== null &&\n      isSelectionWithinEditor(editor, anchorDOMNode, focusDOMNode)\n    ) {\n      domSelection.removeAllRanges();\n    }\n\n    return;\n  }\n\n  const anchor = nextSelection.anchor;\n  const focus = nextSelection.focus;\n  const anchorKey = anchor.key;\n  const focusKey = focus.key;\n  const anchorDOM = getElementByKeyOrThrow(editor, anchorKey);\n  const focusDOM = getElementByKeyOrThrow(editor, focusKey);\n  const nextAnchorOffset = anchor.offset;\n  const nextFocusOffset = focus.offset;\n  const nextFormat = nextSelection.format;\n  const nextStyle = nextSelection.style;\n  const isCollapsed = nextSelection.isCollapsed();\n  let nextAnchorNode: HTMLElement | Text | null = anchorDOM;\n  let nextFocusNode: HTMLElement | Text | null = focusDOM;\n  let anchorFormatOrStyleChanged = false;\n\n  if (anchor.type === 'text') {\n    nextAnchorNode = getDOMTextNode(anchorDOM);\n    const anchorNode = anchor.getNode();\n    anchorFormatOrStyleChanged =\n      anchorNode.getFormat() !== nextFormat ||\n      anchorNode.getStyle() !== nextStyle;\n  } else if (\n    $isRangeSelection(prevSelection) &&\n    prevSelection.anchor.type === 'text'\n  ) {\n    anchorFormatOrStyleChanged = true;\n  }\n\n  if (focus.type === 'text') {\n    nextFocusNode = getDOMTextNode(focusDOM);\n  }\n\n  // If we can't get an underlying text node for selection, then\n  // we should avoid setting selection to something incorrect.\n  if (nextAnchorNode === null || nextFocusNode === null) {\n    return;\n  }\n\n  if (\n    isCollapsed &&\n    (prevSelection === null ||\n      anchorFormatOrStyleChanged ||\n      ($isRangeSelection(prevSelection) &&\n        (prevSelection.format !== nextFormat ||\n          prevSelection.style !== nextStyle)))\n  ) {\n    markCollapsedSelectionFormat(\n      nextFormat,\n      nextStyle,\n      nextAnchorOffset,\n      anchorKey,\n      performance.now(),\n    );\n  }\n\n  // Diff against the native DOM selection to ensure we don't do\n  // an unnecessary selection update. We also skip this check if\n  // we're moving selection to within an element, as this can\n  // sometimes be problematic around scrolling.\n  if (\n    anchorOffset === nextAnchorOffset &&\n    focusOffset === nextFocusOffset &&\n    anchorDOMNode === nextAnchorNode &&\n    focusDOMNode === nextFocusNode && // Badly interpreted range selection when collapsed - #1482\n    !(domSelection.type === 'Range' && isCollapsed)\n  ) {\n    // If the root element does not have focus, ensure it has focus\n    if (activeElement === null || !rootElement.contains(activeElement)) {\n      rootElement.focus({\n        preventScroll: true,\n      });\n    }\n    if (anchor.type !== 'element') {\n      return;\n    }\n  }\n\n  // Apply the updated selection to the DOM. Note: this will trigger\n  // a \"selectionchange\" event, although it will be asynchronous.\n  try {\n    domSelection.setBaseAndExtent(\n      nextAnchorNode,\n      nextAnchorOffset,\n      nextFocusNode,\n      nextFocusOffset,\n    );\n  } catch (error) {\n    // If we encounter an error, continue. This can sometimes\n    // occur with FF and there's no good reason as to why it\n    // should happen.\n    if (__DEV__) {\n      console.warn(error);\n    }\n  }\n  if (\n    !tags.has('skip-scroll-into-view') &&\n    nextSelection.isCollapsed() &&\n    rootElement !== null &&\n    rootElement === document.activeElement\n  ) {\n    const selectionTarget: null | Range | HTMLElement | Text =\n      nextSelection instanceof RangeSelection &&\n      nextSelection.anchor.type === 'element'\n        ? (nextAnchorNode.childNodes[nextAnchorOffset] as HTMLElement | Text) ||\n          null\n        : domSelection.rangeCount > 0\n        ? domSelection.getRangeAt(0)\n        : null;\n    if (selectionTarget !== null) {\n      let selectionRect: DOMRect;\n      if (selectionTarget instanceof Text) {\n        const range = document.createRange();\n        range.selectNode(selectionTarget);\n        selectionRect = range.getBoundingClientRect();\n      } else if (selectionTarget instanceof Range) {\n          selectionRect = selectionTarget.getBoundingClientRect();\n      } else {\n        selectionRect = selectionTarget.getBoundingClientRect();\n      }\n      scrollIntoViewIfNeeded(editor, selectionRect, rootElement);\n    }\n  }\n\n  markSelectionChangeFromDOMUpdate();\n}\n\nexport function $insertNodes(nodes: Array<LexicalNode>) {\n  let selection = $getSelection() || $getPreviousSelection();\n\n  if (selection === null) {\n    selection = $getRoot().selectEnd();\n  }\n  selection.insertNodes(nodes);\n}\n\nexport function $getTextContent(): string {\n  const selection = $getSelection();\n  if (selection === null) {\n    return '';\n  }\n  return selection.getTextContent();\n}\n\nfunction $removeTextAndSplitBlock(selection: RangeSelection): number {\n  let selection_ = selection;\n  if (!selection.isCollapsed()) {\n    selection_.removeText();\n  }\n  // A new selection can originate as a result of node replacement, in which case is registered via\n  // $setSelection\n  const newSelection = $getSelection();\n  if ($isRangeSelection(newSelection)) {\n    selection_ = newSelection;\n  }\n\n  invariant(\n    $isRangeSelection(selection_),\n    'Unexpected dirty selection to be null',\n  );\n\n  const anchor = selection_.anchor;\n  let node = anchor.getNode();\n  let offset = anchor.offset;\n\n  while (!INTERNAL_$isBlock(node)) {\n    [node, offset] = $splitNodeAtPoint(node, offset);\n  }\n\n  return offset;\n}\n\nfunction $splitNodeAtPoint(\n  node: LexicalNode,\n  offset: number,\n): [parent: ElementNode, offset: number] {\n  const parent = node.getParent();\n  if (!parent) {\n    const paragraph = $createParagraphNode();\n    $getRoot().append(paragraph);\n    paragraph.select();\n    return [$getRoot(), 0];\n  }\n\n  if ($isTextNode(node)) {\n    const split = node.splitText(offset);\n    if (split.length === 0) {\n      return [parent, node.getIndexWithinParent()];\n    }\n    const x = offset === 0 ? 0 : 1;\n    const index = split[0].getIndexWithinParent() + x;\n\n    return [parent, index];\n  }\n\n  if (!$isElementNode(node) || offset === 0) {\n    return [parent, node.getIndexWithinParent()];\n  }\n\n  const firstToAppend = node.getChildAtIndex(offset);\n  if (firstToAppend) {\n    const insertPoint = new RangeSelection(\n      $createPoint(node.__key, offset, 'element'),\n      $createPoint(node.__key, offset, 'element'),\n      0,\n      '',\n    );\n    const newElement = node.insertNewAfter(insertPoint) as ElementNode | null;\n    if (newElement) {\n      newElement.append(firstToAppend, ...firstToAppend.getNextSiblings());\n    }\n  }\n  return [parent, node.getIndexWithinParent() + 1];\n}\n\nfunction $wrapInlineNodes(nodes: LexicalNode[]) {\n  // We temporarily insert the topLevelNodes into an arbitrary ElementNode,\n  // since insertAfter does not work on nodes that have no parent (TO-DO: fix that).\n  const virtualRoot = $createParagraphNode();\n\n  let currentBlock = null;\n  for (let i = 0; i < nodes.length; i++) {\n    const node = nodes[i];\n\n    const isLineBreakNode = $isLineBreakNode(node);\n\n    if (\n      isLineBreakNode ||\n      ($isDecoratorNode(node) && node.isInline()) ||\n      ($isElementNode(node) && node.isInline()) ||\n      $isTextNode(node) ||\n      node.isParentRequired()\n    ) {\n      if (currentBlock === null) {\n        currentBlock = node.createParentElementNode();\n        virtualRoot.append(currentBlock);\n        // In the case of LineBreakNode, we just need to\n        // add an empty ParagraphNode to the topLevelBlocks.\n        if (isLineBreakNode) {\n          continue;\n        }\n      }\n\n      if (currentBlock !== null) {\n        currentBlock.append(node);\n      }\n    } else {\n      virtualRoot.append(node);\n      currentBlock = null;\n    }\n  }\n\n  return virtualRoot;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/LexicalUpdates.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {SerializedEditorState} from './LexicalEditorState';\nimport type {LexicalNode, SerializedLexicalNode} from './LexicalNode';\n\nimport invariant from 'lexical/shared/invariant';\n\nimport {$isElementNode, $isTextNode, SELECTION_CHANGE_COMMAND} from '.';\nimport {FULL_RECONCILE, NO_DIRTY_NODES} from './LexicalConstants';\nimport {\n  CommandPayloadType,\n  EditorUpdateOptions,\n  LexicalCommand,\n  LexicalEditor,\n  Listener,\n  MutatedNodes,\n  RegisteredNodes,\n  resetEditor,\n  Transform,\n} from './LexicalEditor';\nimport {\n  cloneEditorState,\n  createEmptyEditorState,\n  EditorState,\n  editorStateHasDirtySelection,\n} from './LexicalEditorState';\nimport {\n  $garbageCollectDetachedDecorators,\n  $garbageCollectDetachedNodes,\n} from './LexicalGC';\nimport {initMutationObserver} from './LexicalMutations';\nimport {$normalizeTextNode} from './LexicalNormalization';\nimport {$reconcileRoot} from './LexicalReconciler';\nimport {\n  $internalCreateSelection,\n  $isNodeSelection,\n  $isRangeSelection,\n  applySelectionTransforms,\n  updateDOMSelection,\n} from './LexicalSelection';\nimport {\n  $getCompositionKey,\n  getDOMSelection,\n  getEditorPropertyFromDOMNode,\n  getEditorStateTextContent,\n  getEditorsToPropagate,\n  getRegisteredNodeOrThrow,\n  isLexicalEditor,\n  removeDOMBlockCursorElement,\n  scheduleMicroTask,\n  updateDOMBlockCursorElement,\n} from './LexicalUtils';\n\nlet activeEditorState: null | EditorState = null;\nlet activeEditor: null | LexicalEditor = null;\nlet isReadOnlyMode = false;\nlet isAttemptingToRecoverFromReconcilerError = false;\nlet infiniteTransformCount = 0;\n\nconst observerOptions = {\n  characterData: true,\n  childList: true,\n  subtree: true,\n};\n\nexport function isCurrentlyReadOnlyMode(): boolean {\n  return (\n    isReadOnlyMode ||\n    (activeEditorState !== null && activeEditorState._readOnly)\n  );\n}\n\nexport function errorOnReadOnly(): void {\n  if (isReadOnlyMode) {\n    invariant(false, 'Cannot use method in read-only mode.');\n  }\n}\n\nexport function errorOnInfiniteTransforms(): void {\n  if (infiniteTransformCount > 99) {\n    invariant(\n      false,\n      'One or more transforms are endlessly triggering additional transforms. May have encountered infinite recursion caused by transforms that have their preconditions too lose and/or conflict with each other.',\n    );\n  }\n}\n\nexport function getActiveEditorState(): EditorState {\n  if (activeEditorState === null) {\n    invariant(\n      false,\n      'Unable to find an active editor state. ' +\n        'State helpers or node methods can only be used ' +\n        'synchronously during the callback of ' +\n        'editor.update(), editor.read(), or editorState.read().%s',\n      collectBuildInformation(),\n    );\n  }\n\n  return activeEditorState;\n}\n\nexport function getActiveEditor(): LexicalEditor {\n  if (activeEditor === null) {\n    invariant(\n      false,\n      'Unable to find an active editor. ' +\n        'This method can only be used ' +\n        'synchronously during the callback of ' +\n        'editor.update() or editor.read().%s',\n      collectBuildInformation(),\n    );\n  }\n  return activeEditor;\n}\n\nfunction collectBuildInformation(): string {\n  let compatibleEditors = 0;\n  const incompatibleEditors = new Set<string>();\n  const thisVersion = LexicalEditor.version;\n  if (typeof window !== 'undefined') {\n    for (const node of document.querySelectorAll('[contenteditable]')) {\n      const editor = getEditorPropertyFromDOMNode(node);\n      if (isLexicalEditor(editor)) {\n        compatibleEditors++;\n      } else if (editor) {\n        let version = String(\n          (\n            editor.constructor as typeof editor['constructor'] &\n              Record<string, unknown>\n          ).version || '<0.17.1',\n        );\n        if (version === thisVersion) {\n          version +=\n            ' (separately built, likely a bundler configuration issue)';\n        }\n        incompatibleEditors.add(version);\n      }\n    }\n  }\n  let output = ` Detected on the page: ${compatibleEditors} compatible editor(s) with version ${thisVersion}`;\n  if (incompatibleEditors.size) {\n    output += ` and incompatible editors with versions ${Array.from(\n      incompatibleEditors,\n    ).join(', ')}`;\n  }\n  return output;\n}\n\nexport function internalGetActiveEditor(): LexicalEditor | null {\n  return activeEditor;\n}\n\nexport function internalGetActiveEditorState(): EditorState | null {\n  return activeEditorState;\n}\n\nexport function $applyTransforms(\n  editor: LexicalEditor,\n  node: LexicalNode,\n  transformsCache: Map<string, Array<Transform<LexicalNode>>>,\n) {\n  const type = node.__type;\n  const registeredNode = getRegisteredNodeOrThrow(editor, type);\n  let transformsArr = transformsCache.get(type);\n\n  if (transformsArr === undefined) {\n    transformsArr = Array.from(registeredNode.transforms);\n    transformsCache.set(type, transformsArr);\n  }\n\n  const transformsArrLength = transformsArr.length;\n\n  for (let i = 0; i < transformsArrLength; i++) {\n    transformsArr[i](node);\n\n    if (!node.isAttached()) {\n      break;\n    }\n  }\n}\n\nfunction $isNodeValidForTransform(\n  node: LexicalNode,\n  compositionKey: null | string,\n): boolean {\n  return (\n    node !== undefined &&\n    // We don't want to transform nodes being composed\n    node.__key !== compositionKey &&\n    node.isAttached()\n  );\n}\n\nfunction $normalizeAllDirtyTextNodes(\n  editorState: EditorState,\n  editor: LexicalEditor,\n): void {\n  const dirtyLeaves = editor._dirtyLeaves;\n  const nodeMap = editorState._nodeMap;\n\n  for (const nodeKey of dirtyLeaves) {\n    const node = nodeMap.get(nodeKey);\n\n    if (\n      $isTextNode(node) &&\n      node.isAttached() &&\n      node.isSimpleText() &&\n      !node.isUnmergeable()\n    ) {\n      $normalizeTextNode(node);\n    }\n  }\n}\n\n/**\n * Transform heuristic:\n * 1. We transform leaves first. If transforms generate additional dirty nodes we repeat step 1.\n * The reasoning behind this is that marking a leaf as dirty marks all its parent elements as dirty too.\n * 2. We transform elements. If element transforms generate additional dirty nodes we repeat step 1.\n * If element transforms only generate additional dirty elements we only repeat step 2.\n *\n * Note that to keep track of newly dirty nodes and subtrees we leverage the editor._dirtyNodes and\n * editor._subtrees which we reset in every loop.\n */\nfunction $applyAllTransforms(\n  editorState: EditorState,\n  editor: LexicalEditor,\n): void {\n  const dirtyLeaves = editor._dirtyLeaves;\n  const dirtyElements = editor._dirtyElements;\n  const nodeMap = editorState._nodeMap;\n  const compositionKey = $getCompositionKey();\n  const transformsCache = new Map();\n\n  let untransformedDirtyLeaves = dirtyLeaves;\n  let untransformedDirtyLeavesLength = untransformedDirtyLeaves.size;\n  let untransformedDirtyElements = dirtyElements;\n  let untransformedDirtyElementsLength = untransformedDirtyElements.size;\n\n  while (\n    untransformedDirtyLeavesLength > 0 ||\n    untransformedDirtyElementsLength > 0\n  ) {\n    if (untransformedDirtyLeavesLength > 0) {\n      // We leverage editor._dirtyLeaves to track the new dirty leaves after the transforms\n      editor._dirtyLeaves = new Set();\n\n      for (const nodeKey of untransformedDirtyLeaves) {\n        const node = nodeMap.get(nodeKey);\n\n        if (\n          $isTextNode(node) &&\n          node.isAttached() &&\n          node.isSimpleText() &&\n          !node.isUnmergeable()\n        ) {\n          $normalizeTextNode(node);\n        }\n\n        if (\n          node !== undefined &&\n          $isNodeValidForTransform(node, compositionKey)\n        ) {\n          $applyTransforms(editor, node, transformsCache);\n        }\n\n        dirtyLeaves.add(nodeKey);\n      }\n\n      untransformedDirtyLeaves = editor._dirtyLeaves;\n      untransformedDirtyLeavesLength = untransformedDirtyLeaves.size;\n\n      // We want to prioritize node transforms over element transforms\n      if (untransformedDirtyLeavesLength > 0) {\n        infiniteTransformCount++;\n        continue;\n      }\n    }\n\n    // All dirty leaves have been processed. Let's do elements!\n    // We have previously processed dirty leaves, so let's restart the editor leaves Set to track\n    // new ones caused by element transforms\n    editor._dirtyLeaves = new Set();\n    editor._dirtyElements = new Map();\n\n    for (const currentUntransformedDirtyElement of untransformedDirtyElements) {\n      const nodeKey = currentUntransformedDirtyElement[0];\n      const intentionallyMarkedAsDirty = currentUntransformedDirtyElement[1];\n      if (nodeKey !== 'root' && !intentionallyMarkedAsDirty) {\n        continue;\n      }\n\n      const node = nodeMap.get(nodeKey);\n\n      if (\n        node !== undefined &&\n        $isNodeValidForTransform(node, compositionKey)\n      ) {\n        $applyTransforms(editor, node, transformsCache);\n      }\n\n      dirtyElements.set(nodeKey, intentionallyMarkedAsDirty);\n    }\n\n    untransformedDirtyLeaves = editor._dirtyLeaves;\n    untransformedDirtyLeavesLength = untransformedDirtyLeaves.size;\n    untransformedDirtyElements = editor._dirtyElements;\n    untransformedDirtyElementsLength = untransformedDirtyElements.size;\n    infiniteTransformCount++;\n  }\n\n  editor._dirtyLeaves = dirtyLeaves;\n  editor._dirtyElements = dirtyElements;\n}\n\ntype InternalSerializedNode = {\n  children?: Array<InternalSerializedNode>;\n  type: string;\n  version: number;\n};\n\nexport function $parseSerializedNode(\n  serializedNode: SerializedLexicalNode,\n): LexicalNode {\n  const internalSerializedNode: InternalSerializedNode = serializedNode;\n  return $parseSerializedNodeImpl(\n    internalSerializedNode,\n    getActiveEditor()._nodes,\n  );\n}\n\nfunction $parseSerializedNodeImpl<\n  SerializedNode extends InternalSerializedNode,\n>(\n  serializedNode: SerializedNode,\n  registeredNodes: RegisteredNodes,\n): LexicalNode {\n  const type = serializedNode.type;\n  const registeredNode = registeredNodes.get(type);\n\n  if (registeredNode === undefined) {\n    invariant(false, 'parseEditorState: type \"%s\" + not found', type);\n  }\n\n  const nodeClass = registeredNode.klass;\n\n  if (serializedNode.type !== nodeClass.getType()) {\n    invariant(\n      false,\n      'LexicalNode: Node %s does not implement .importJSON().',\n      nodeClass.name,\n    );\n  }\n\n  const node = nodeClass.importJSON(serializedNode);\n  const children = serializedNode.children;\n\n  if ($isElementNode(node) && Array.isArray(children)) {\n    for (let i = 0; i < children.length; i++) {\n      const serializedJSONChildNode = children[i];\n      const childNode = $parseSerializedNodeImpl(\n        serializedJSONChildNode,\n        registeredNodes,\n      );\n      node.append(childNode);\n    }\n  }\n\n  return node;\n}\n\nexport function parseEditorState(\n  serializedEditorState: SerializedEditorState,\n  editor: LexicalEditor,\n  updateFn: void | (() => void),\n): EditorState {\n  const editorState = createEmptyEditorState();\n  const previousActiveEditorState = activeEditorState;\n  const previousReadOnlyMode = isReadOnlyMode;\n  const previousActiveEditor = activeEditor;\n  const previousDirtyElements = editor._dirtyElements;\n  const previousDirtyLeaves = editor._dirtyLeaves;\n  const previousCloneNotNeeded = editor._cloneNotNeeded;\n  const previousDirtyType = editor._dirtyType;\n  editor._dirtyElements = new Map();\n  editor._dirtyLeaves = new Set();\n  editor._cloneNotNeeded = new Set();\n  editor._dirtyType = 0;\n  activeEditorState = editorState;\n  isReadOnlyMode = false;\n  activeEditor = editor;\n\n  try {\n    const registeredNodes = editor._nodes;\n    const serializedNode = serializedEditorState.root;\n    $parseSerializedNodeImpl(serializedNode, registeredNodes);\n    if (updateFn) {\n      updateFn();\n    }\n\n    // Make the editorState immutable\n    editorState._readOnly = true;\n\n    if (__DEV__) {\n      handleDEVOnlyPendingUpdateGuarantees(editorState);\n    }\n  } catch (error) {\n    if (error instanceof Error) {\n      editor._onError(error);\n    }\n  } finally {\n    editor._dirtyElements = previousDirtyElements;\n    editor._dirtyLeaves = previousDirtyLeaves;\n    editor._cloneNotNeeded = previousCloneNotNeeded;\n    editor._dirtyType = previousDirtyType;\n    activeEditorState = previousActiveEditorState;\n    isReadOnlyMode = previousReadOnlyMode;\n    activeEditor = previousActiveEditor;\n  }\n\n  return editorState;\n}\n\n// This technically isn't an update but given we need\n// exposure to the module's active bindings, we have this\n// function here\n\nexport function readEditorState<V>(\n  editor: LexicalEditor | null,\n  editorState: EditorState,\n  callbackFn: () => V,\n): V {\n  const previousActiveEditorState = activeEditorState;\n  const previousReadOnlyMode = isReadOnlyMode;\n  const previousActiveEditor = activeEditor;\n\n  activeEditorState = editorState;\n  isReadOnlyMode = true;\n  activeEditor = editor;\n\n  try {\n    return callbackFn();\n  } finally {\n    activeEditorState = previousActiveEditorState;\n    isReadOnlyMode = previousReadOnlyMode;\n    activeEditor = previousActiveEditor;\n  }\n}\n\nfunction handleDEVOnlyPendingUpdateGuarantees(\n  pendingEditorState: EditorState,\n): void {\n  // Given we can't Object.freeze the nodeMap as it's a Map,\n  // we instead replace its set, clear and delete methods.\n  const nodeMap = pendingEditorState._nodeMap;\n\n  nodeMap.set = () => {\n    throw new Error('Cannot call set() on a frozen Lexical node map');\n  };\n\n  nodeMap.clear = () => {\n    throw new Error('Cannot call clear() on a frozen Lexical node map');\n  };\n\n  nodeMap.delete = () => {\n    throw new Error('Cannot call delete() on a frozen Lexical node map');\n  };\n}\n\nexport function $commitPendingUpdates(\n  editor: LexicalEditor,\n  recoveryEditorState?: EditorState,\n): void {\n  const pendingEditorState = editor._pendingEditorState;\n  const rootElement = editor._rootElement;\n  const shouldSkipDOM = editor._headless || rootElement === null;\n\n  if (pendingEditorState === null) {\n    return;\n  }\n\n  // ======\n  // Reconciliation has started.\n  // ======\n\n  const currentEditorState = editor._editorState;\n  const currentSelection = currentEditorState._selection;\n  const pendingSelection = pendingEditorState._selection;\n  const needsUpdate = editor._dirtyType !== NO_DIRTY_NODES;\n  const previousActiveEditorState = activeEditorState;\n  const previousReadOnlyMode = isReadOnlyMode;\n  const previousActiveEditor = activeEditor;\n  const previouslyUpdating = editor._updating;\n  const observer = editor._observer;\n  let mutatedNodes = null;\n  editor._pendingEditorState = null;\n  editor._editorState = pendingEditorState;\n\n  if (!shouldSkipDOM && needsUpdate && observer !== null) {\n    activeEditor = editor;\n    activeEditorState = pendingEditorState;\n    isReadOnlyMode = false;\n    // We don't want updates to sync block the reconciliation.\n    editor._updating = true;\n    try {\n      const dirtyType = editor._dirtyType;\n      const dirtyElements = editor._dirtyElements;\n      const dirtyLeaves = editor._dirtyLeaves;\n      observer.disconnect();\n\n      mutatedNodes = $reconcileRoot(\n        currentEditorState,\n        pendingEditorState,\n        editor,\n        dirtyType,\n        dirtyElements,\n        dirtyLeaves,\n      );\n    } catch (error) {\n      // Report errors\n      if (error instanceof Error) {\n        editor._onError(error);\n      }\n\n      // Reset editor and restore incoming editor state to the DOM\n      if (!isAttemptingToRecoverFromReconcilerError) {\n        resetEditor(editor, null, rootElement, pendingEditorState);\n        initMutationObserver(editor);\n        editor._dirtyType = FULL_RECONCILE;\n        isAttemptingToRecoverFromReconcilerError = true;\n        $commitPendingUpdates(editor, currentEditorState);\n        isAttemptingToRecoverFromReconcilerError = false;\n      } else {\n        // To avoid a possible situation of infinite loops, lets throw\n        throw error;\n      }\n\n      return;\n    } finally {\n      observer.observe(rootElement as Node, observerOptions);\n      editor._updating = previouslyUpdating;\n      activeEditorState = previousActiveEditorState;\n      isReadOnlyMode = previousReadOnlyMode;\n      activeEditor = previousActiveEditor;\n    }\n  }\n\n  if (!pendingEditorState._readOnly) {\n    pendingEditorState._readOnly = true;\n    if (__DEV__) {\n      handleDEVOnlyPendingUpdateGuarantees(pendingEditorState);\n      if ($isRangeSelection(pendingSelection)) {\n        Object.freeze(pendingSelection.anchor);\n        Object.freeze(pendingSelection.focus);\n      }\n      Object.freeze(pendingSelection);\n    }\n  }\n\n  const dirtyLeaves = editor._dirtyLeaves;\n  const dirtyElements = editor._dirtyElements;\n  const normalizedNodes = editor._normalizedNodes;\n  const tags = editor._updateTags;\n  const deferred = editor._deferred;\n  const nodeCount = pendingEditorState._nodeMap.size;\n\n  if (needsUpdate) {\n    editor._dirtyType = NO_DIRTY_NODES;\n    editor._cloneNotNeeded.clear();\n    editor._dirtyLeaves = new Set();\n    editor._dirtyElements = new Map();\n    editor._normalizedNodes = new Set();\n    editor._updateTags = new Set();\n  }\n  $garbageCollectDetachedDecorators(editor, pendingEditorState);\n\n  // ======\n  // Reconciliation has finished. Now update selection and trigger listeners.\n  // ======\n\n  const domSelection = shouldSkipDOM ? null : getDOMSelection(editor._window);\n\n  // Attempt to update the DOM selection, including focusing of the root element,\n  // and scroll into view if needed.\n  if (\n    editor._editable &&\n    // domSelection will be null in headless\n    domSelection !== null &&\n    (needsUpdate || pendingSelection === null || pendingSelection.dirty)\n  ) {\n    activeEditor = editor;\n    activeEditorState = pendingEditorState;\n    try {\n      if (observer !== null) {\n        observer.disconnect();\n      }\n      if (needsUpdate || pendingSelection === null || pendingSelection.dirty) {\n        const blockCursorElement = editor._blockCursorElement;\n        if (blockCursorElement !== null) {\n          removeDOMBlockCursorElement(\n            blockCursorElement,\n            editor,\n            rootElement as HTMLElement,\n          );\n        }\n        updateDOMSelection(\n          currentSelection,\n          pendingSelection,\n          editor,\n          domSelection,\n          tags,\n          rootElement as HTMLElement,\n          nodeCount,\n        );\n      }\n      updateDOMBlockCursorElement(\n        editor,\n        rootElement as HTMLElement,\n        pendingSelection,\n      );\n      if (observer !== null) {\n        observer.observe(rootElement as Node, observerOptions);\n      }\n    } finally {\n      activeEditor = previousActiveEditor;\n      activeEditorState = previousActiveEditorState;\n    }\n  }\n\n  if (mutatedNodes !== null) {\n    triggerMutationListeners(\n      editor,\n      mutatedNodes,\n      tags,\n      dirtyLeaves,\n      currentEditorState,\n    );\n  }\n  if (\n    !$isRangeSelection(pendingSelection) &&\n    pendingSelection !== null &&\n    (currentSelection === null || !currentSelection.is(pendingSelection))\n  ) {\n    editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);\n  }\n  /**\n   * Capture pendingDecorators after garbage collecting detached decorators\n   */\n  const pendingDecorators = editor._pendingDecorators;\n  if (pendingDecorators !== null) {\n    editor._decorators = pendingDecorators;\n    editor._pendingDecorators = null;\n    triggerListeners('decorator', editor, true, pendingDecorators);\n  }\n\n  // If reconciler fails, we reset whole editor (so current editor state becomes empty)\n  // and attempt to re-render pendingEditorState. If that goes through we trigger\n  // listeners, but instead use recoverEditorState which is current editor state before reset\n  // This specifically important for collab that relies on prevEditorState from update\n  // listener to calculate delta of changed nodes/properties\n  triggerTextContentListeners(\n    editor,\n    recoveryEditorState || currentEditorState,\n    pendingEditorState,\n  );\n  triggerListeners('update', editor, true, {\n    dirtyElements,\n    dirtyLeaves,\n    editorState: pendingEditorState,\n    normalizedNodes,\n    prevEditorState: recoveryEditorState || currentEditorState,\n    tags,\n  });\n  triggerDeferredUpdateCallbacks(editor, deferred);\n  $triggerEnqueuedUpdates(editor);\n}\n\nfunction triggerTextContentListeners(\n  editor: LexicalEditor,\n  currentEditorState: EditorState,\n  pendingEditorState: EditorState,\n): void {\n  const currentTextContent = getEditorStateTextContent(currentEditorState);\n  const latestTextContent = getEditorStateTextContent(pendingEditorState);\n\n  if (currentTextContent !== latestTextContent) {\n    triggerListeners('textcontent', editor, true, latestTextContent);\n  }\n}\n\nfunction triggerMutationListeners(\n  editor: LexicalEditor,\n  mutatedNodes: MutatedNodes,\n  updateTags: Set<string>,\n  dirtyLeaves: Set<string>,\n  prevEditorState: EditorState,\n): void {\n  const listeners = Array.from(editor._listeners.mutation);\n  const listenersLength = listeners.length;\n\n  for (let i = 0; i < listenersLength; i++) {\n    const [listener, klass] = listeners[i];\n    const mutatedNodesByType = mutatedNodes.get(klass);\n    if (mutatedNodesByType !== undefined) {\n      listener(mutatedNodesByType, {\n        dirtyLeaves,\n        prevEditorState,\n        updateTags,\n      });\n    }\n  }\n}\n\nexport function triggerListeners(\n  type: 'update' | 'root' | 'decorator' | 'textcontent' | 'editable',\n  editor: LexicalEditor,\n  isCurrentlyEnqueuingUpdates: boolean,\n  ...payload: unknown[]\n): void {\n  const previouslyUpdating = editor._updating;\n  editor._updating = isCurrentlyEnqueuingUpdates;\n\n  try {\n    const listeners = Array.from<Listener>(editor._listeners[type]);\n    for (let i = 0; i < listeners.length; i++) {\n      // @ts-ignore\n      listeners[i].apply(null, payload);\n    }\n  } finally {\n    editor._updating = previouslyUpdating;\n  }\n}\n\nexport function triggerCommandListeners<\n  TCommand extends LexicalCommand<unknown>,\n>(\n  editor: LexicalEditor,\n  type: TCommand,\n  payload: CommandPayloadType<TCommand>,\n): boolean {\n  if (editor._updating === false || activeEditor !== editor) {\n    let returnVal = false;\n    editor.update(() => {\n      returnVal = triggerCommandListeners(editor, type, payload);\n    });\n    return returnVal;\n  }\n\n  const editors = getEditorsToPropagate(editor);\n\n  for (let i = 4; i >= 0; i--) {\n    for (let e = 0; e < editors.length; e++) {\n      const currentEditor = editors[e];\n      const commandListeners = currentEditor._commands;\n      const listenerInPriorityOrder = commandListeners.get(type);\n\n      if (listenerInPriorityOrder !== undefined) {\n        const listenersSet = listenerInPriorityOrder[i];\n\n        if (listenersSet !== undefined) {\n          const listeners = Array.from(listenersSet);\n          const listenersLength = listeners.length;\n\n          for (let j = 0; j < listenersLength; j++) {\n            if (listeners[j](payload, editor) === true) {\n              return true;\n            }\n          }\n        }\n      }\n    }\n  }\n\n  return false;\n}\n\nfunction $triggerEnqueuedUpdates(editor: LexicalEditor): void {\n  const queuedUpdates = editor._updates;\n\n  if (queuedUpdates.length !== 0) {\n    const queuedUpdate = queuedUpdates.shift();\n    if (queuedUpdate) {\n      const [updateFn, options] = queuedUpdate;\n      $beginUpdate(editor, updateFn, options);\n    }\n  }\n}\n\nfunction triggerDeferredUpdateCallbacks(\n  editor: LexicalEditor,\n  deferred: Array<() => void>,\n): void {\n  editor._deferred = [];\n\n  if (deferred.length !== 0) {\n    const previouslyUpdating = editor._updating;\n    editor._updating = true;\n\n    try {\n      for (let i = 0; i < deferred.length; i++) {\n        deferred[i]();\n      }\n    } finally {\n      editor._updating = previouslyUpdating;\n    }\n  }\n}\n\nfunction processNestedUpdates(\n  editor: LexicalEditor,\n  initialSkipTransforms?: boolean,\n): boolean {\n  const queuedUpdates = editor._updates;\n  let skipTransforms = initialSkipTransforms || false;\n\n  // Updates might grow as we process them, we so we'll need\n  // to handle each update as we go until the updates array is\n  // empty.\n  while (queuedUpdates.length !== 0) {\n    const queuedUpdate = queuedUpdates.shift();\n    if (queuedUpdate) {\n      const [nextUpdateFn, options] = queuedUpdate;\n\n      let onUpdate;\n      let tag;\n\n      if (options !== undefined) {\n        onUpdate = options.onUpdate;\n        tag = options.tag;\n\n        if (options.skipTransforms) {\n          skipTransforms = true;\n        }\n        if (options.discrete) {\n          const pendingEditorState = editor._pendingEditorState;\n          invariant(\n            pendingEditorState !== null,\n            'Unexpected empty pending editor state on discrete nested update',\n          );\n          pendingEditorState._flushSync = true;\n        }\n\n        if (onUpdate) {\n          editor._deferred.push(onUpdate);\n        }\n\n        if (tag) {\n          editor._updateTags.add(tag);\n        }\n      }\n\n      nextUpdateFn();\n    }\n  }\n\n  return skipTransforms;\n}\n\nfunction $beginUpdate(\n  editor: LexicalEditor,\n  updateFn: () => void,\n  options?: EditorUpdateOptions,\n): void {\n  const updateTags = editor._updateTags;\n  let onUpdate;\n  let tag;\n  let skipTransforms = false;\n  let discrete = false;\n\n  if (options !== undefined) {\n    onUpdate = options.onUpdate;\n    tag = options.tag;\n\n    if (tag != null) {\n      updateTags.add(tag);\n    }\n\n    skipTransforms = options.skipTransforms || false;\n    discrete = options.discrete || false;\n  }\n\n  if (onUpdate) {\n    editor._deferred.push(onUpdate);\n  }\n\n  const currentEditorState = editor._editorState;\n  let pendingEditorState = editor._pendingEditorState;\n  let editorStateWasCloned = false;\n\n  if (pendingEditorState === null || pendingEditorState._readOnly) {\n    pendingEditorState = editor._pendingEditorState = cloneEditorState(\n      pendingEditorState || currentEditorState,\n    );\n    editorStateWasCloned = true;\n  }\n  pendingEditorState._flushSync = discrete;\n\n  const previousActiveEditorState = activeEditorState;\n  const previousReadOnlyMode = isReadOnlyMode;\n  const previousActiveEditor = activeEditor;\n  const previouslyUpdating = editor._updating;\n  activeEditorState = pendingEditorState;\n  isReadOnlyMode = false;\n  editor._updating = true;\n  activeEditor = editor;\n\n  try {\n    if (editorStateWasCloned) {\n      if (editor._headless) {\n        if (currentEditorState._selection !== null) {\n          pendingEditorState._selection = currentEditorState._selection.clone();\n        }\n      } else {\n        pendingEditorState._selection = $internalCreateSelection(editor);\n      }\n    }\n\n    const startingCompositionKey = editor._compositionKey;\n    updateFn();\n    skipTransforms = processNestedUpdates(editor, skipTransforms);\n    applySelectionTransforms(pendingEditorState, editor);\n\n    if (editor._dirtyType !== NO_DIRTY_NODES) {\n      if (skipTransforms) {\n        $normalizeAllDirtyTextNodes(pendingEditorState, editor);\n      } else {\n        $applyAllTransforms(pendingEditorState, editor);\n      }\n\n      processNestedUpdates(editor);\n      $garbageCollectDetachedNodes(\n        currentEditorState,\n        pendingEditorState,\n        editor._dirtyLeaves,\n        editor._dirtyElements,\n      );\n    }\n\n    const endingCompositionKey = editor._compositionKey;\n\n    if (startingCompositionKey !== endingCompositionKey) {\n      pendingEditorState._flushSync = true;\n    }\n\n    const pendingSelection = pendingEditorState._selection;\n\n    if ($isRangeSelection(pendingSelection)) {\n      const pendingNodeMap = pendingEditorState._nodeMap;\n      const anchorKey = pendingSelection.anchor.key;\n      const focusKey = pendingSelection.focus.key;\n\n      if (\n        pendingNodeMap.get(anchorKey) === undefined ||\n        pendingNodeMap.get(focusKey) === undefined\n      ) {\n        invariant(\n          false,\n          'updateEditor: selection has been lost because the previously selected nodes have been removed and ' +\n            \"selection wasn't moved to another node. Ensure selection changes after removing/replacing a selected node.\",\n        );\n      }\n    } else if ($isNodeSelection(pendingSelection)) {\n      // TODO: we should also validate node selection?\n      if (pendingSelection._nodes.size === 0) {\n        pendingEditorState._selection = null;\n      }\n    }\n  } catch (error) {\n    // Report errors\n    if (error instanceof Error) {\n      editor._onError(error);\n    }\n\n    // Restore existing editor state to the DOM\n    editor._pendingEditorState = currentEditorState;\n    editor._dirtyType = FULL_RECONCILE;\n\n    editor._cloneNotNeeded.clear();\n\n    editor._dirtyLeaves = new Set();\n\n    editor._dirtyElements.clear();\n\n    $commitPendingUpdates(editor);\n    return;\n  } finally {\n    activeEditorState = previousActiveEditorState;\n    isReadOnlyMode = previousReadOnlyMode;\n    activeEditor = previousActiveEditor;\n    editor._updating = previouslyUpdating;\n    infiniteTransformCount = 0;\n  }\n\n  const shouldUpdate =\n    editor._dirtyType !== NO_DIRTY_NODES ||\n    editorStateHasDirtySelection(pendingEditorState, editor);\n\n  if (shouldUpdate) {\n    if (pendingEditorState._flushSync) {\n      pendingEditorState._flushSync = false;\n      $commitPendingUpdates(editor);\n    } else if (editorStateWasCloned) {\n      scheduleMicroTask(() => {\n        $commitPendingUpdates(editor);\n      });\n    }\n  } else {\n    pendingEditorState._flushSync = false;\n\n    if (editorStateWasCloned) {\n      updateTags.clear();\n      editor._deferred = [];\n      editor._pendingEditorState = null;\n    }\n  }\n}\n\nexport function updateEditor(\n  editor: LexicalEditor,\n  updateFn: () => void,\n  options?: EditorUpdateOptions,\n): void {\n  if (editor._updating) {\n    editor._updates.push([updateFn, options]);\n  } else {\n    $beginUpdate(editor, updateFn, options);\n  }\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/LexicalUtils.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {\n  CommandPayloadType,\n  EditorConfig,\n  EditorThemeClasses,\n  Klass,\n  LexicalCommand,\n  MutatedNodes,\n  MutationListeners,\n  NodeMutation,\n  RegisteredNode,\n  RegisteredNodes,\n  Spread,\n} from './LexicalEditor';\nimport type {EditorState} from './LexicalEditorState';\nimport type {LexicalNode, NodeKey, NodeMap} from './LexicalNode';\nimport type {\n  BaseSelection,\n  PointType,\n  RangeSelection,\n} from './LexicalSelection';\nimport type {RootNode} from './nodes/LexicalRootNode';\nimport type {TextFormatType, TextNode} from './nodes/LexicalTextNode';\n\nimport {CAN_USE_DOM} from 'lexical/shared/canUseDOM';\nimport {IS_APPLE, IS_APPLE_WEBKIT, IS_IOS, IS_SAFARI} from 'lexical/shared/environment';\nimport invariant from 'lexical/shared/invariant';\nimport normalizeClassNames from 'lexical/shared/normalizeClassNames';\n\nimport {\n  $createTextNode,\n  $getPreviousSelection,\n  $getSelection,\n  $isDecoratorNode,\n  $isElementNode,\n  $isLineBreakNode,\n  $isRangeSelection,\n  $isRootNode,\n  $isTextNode,\n  DecoratorNode,\n  ElementNode,\n  LineBreakNode,\n} from '.';\nimport {\n  COMPOSITION_SUFFIX,\n  DOM_TEXT_TYPE,\n  HAS_DIRTY_NODES,\n  LTR_REGEX,\n  RTL_REGEX,\n  TEXT_TYPE_TO_FORMAT,\n} from './LexicalConstants';\nimport {LexicalEditor} from './LexicalEditor';\nimport {$flushRootMutations} from './LexicalMutations';\nimport {$normalizeSelection} from './LexicalNormalization';\nimport {\n  errorOnInfiniteTransforms,\n  errorOnReadOnly,\n  getActiveEditor,\n  getActiveEditorState,\n  internalGetActiveEditorState,\n  isCurrentlyReadOnlyMode,\n  triggerCommandListeners,\n  updateEditor,\n} from './LexicalUpdates';\n\nexport const emptyFunction = () => {\n  return;\n};\n\nlet keyCounter = 1;\n\nexport function resetRandomKey(): void {\n  keyCounter = 1;\n}\n\nexport function generateRandomKey(): string {\n  return '' + keyCounter++;\n}\n\nexport function getRegisteredNodeOrThrow(\n  editor: LexicalEditor,\n  nodeType: string,\n): RegisteredNode {\n  const registeredNode = editor._nodes.get(nodeType);\n  if (registeredNode === undefined) {\n    invariant(false, 'registeredNode: Type %s not found', nodeType);\n  }\n  return registeredNode;\n}\n\nexport const isArray = Array.isArray;\n\nexport const scheduleMicroTask: (fn: () => void) => void =\n  typeof queueMicrotask === 'function'\n    ? queueMicrotask\n    : (fn) => {\n        // No window prefix intended (#1400)\n        Promise.resolve().then(fn);\n      };\n\nexport function $isSelectionCapturedInDecorator(node: Node): boolean {\n  return $isDecoratorNode($getNearestNodeFromDOMNode(node));\n}\n\nexport function isSelectionCapturedInDecoratorInput(anchorDOM: Node): boolean {\n  const activeElement = document.activeElement as HTMLElement;\n\n  if (activeElement === null) {\n    return false;\n  }\n  const nodeName = activeElement.nodeName;\n\n  return (\n    $isDecoratorNode($getNearestNodeFromDOMNode(anchorDOM)) &&\n    (nodeName === 'INPUT' ||\n      nodeName === 'TEXTAREA' ||\n      (activeElement.contentEditable === 'true' &&\n        getEditorPropertyFromDOMNode(activeElement) == null))\n  );\n}\n\nexport function isSelectionWithinEditor(\n  editor: LexicalEditor,\n  anchorDOM: null | Node,\n  focusDOM: null | Node,\n): boolean {\n  const rootElement = editor.getRootElement();\n  try {\n    return (\n      rootElement !== null &&\n      rootElement.contains(anchorDOM) &&\n      rootElement.contains(focusDOM) &&\n      // Ignore if selection is within nested editor\n      anchorDOM !== null &&\n      !isSelectionCapturedInDecoratorInput(anchorDOM as Node) &&\n      getNearestEditorFromDOMNode(anchorDOM) === editor\n    );\n  } catch (error) {\n    return false;\n  }\n}\n\n/**\n * @returns true if the given argument is a LexicalEditor instance from this build of Lexical\n */\nexport function isLexicalEditor(editor: unknown): editor is LexicalEditor {\n  // Check instanceof to prevent issues with multiple embedded Lexical installations\n  return editor instanceof LexicalEditor;\n}\n\nexport function getNearestEditorFromDOMNode(\n  node: Node | null,\n): LexicalEditor | null {\n  let currentNode = node;\n  while (currentNode != null) {\n    const editor = getEditorPropertyFromDOMNode(currentNode);\n    if (isLexicalEditor(editor)) {\n      return editor;\n    }\n    currentNode = getParentElement(currentNode);\n  }\n  return null;\n}\n\n/** @internal */\nexport function getEditorPropertyFromDOMNode(node: Node | null): unknown {\n  // @ts-expect-error: internal field\n  return node ? node.__lexicalEditor : null;\n}\n\nexport function getTextDirection(text: string): 'ltr' | 'rtl' | null {\n  if (RTL_REGEX.test(text)) {\n    return 'rtl';\n  }\n  if (LTR_REGEX.test(text)) {\n    return 'ltr';\n  }\n  return null;\n}\n\nexport function $isTokenOrSegmented(node: TextNode): boolean {\n  return node.isToken() || node.isSegmented();\n}\n\nfunction isDOMNodeLexicalTextNode(node: Node): node is Text {\n  return node.nodeType === DOM_TEXT_TYPE;\n}\n\nexport function getDOMTextNode(element: Node | null): Text | null {\n  let node = element;\n  while (node != null) {\n    if (isDOMNodeLexicalTextNode(node)) {\n      return node;\n    }\n    node = node.firstChild;\n  }\n  return null;\n}\n\nexport function toggleTextFormatType(\n  format: number,\n  type: TextFormatType,\n  alignWithFormat: null | number,\n): number {\n  const activeFormat = TEXT_TYPE_TO_FORMAT[type];\n  if (\n    alignWithFormat !== null &&\n    (format & activeFormat) === (alignWithFormat & activeFormat)\n  ) {\n    return format;\n  }\n  let newFormat = format ^ activeFormat;\n  if (type === 'subscript') {\n    newFormat &= ~TEXT_TYPE_TO_FORMAT.superscript;\n  } else if (type === 'superscript') {\n    newFormat &= ~TEXT_TYPE_TO_FORMAT.subscript;\n  }\n  return newFormat;\n}\n\nexport function $isLeafNode(\n  node: LexicalNode | null | undefined,\n): node is TextNode | LineBreakNode | DecoratorNode<unknown> {\n  return $isTextNode(node) || $isLineBreakNode(node) || $isDecoratorNode(node);\n}\n\nexport function $setNodeKey(\n  node: LexicalNode,\n  existingKey: NodeKey | null | undefined,\n): void {\n  if (existingKey != null) {\n    if (__DEV__) {\n      errorOnNodeKeyConstructorMismatch(node, existingKey);\n    }\n    node.__key = existingKey;\n    return;\n  }\n  errorOnReadOnly();\n  errorOnInfiniteTransforms();\n  const editor = getActiveEditor();\n  const editorState = getActiveEditorState();\n  const key = generateRandomKey();\n  editorState._nodeMap.set(key, node);\n  // TODO Split this function into leaf/element\n  if ($isElementNode(node)) {\n    editor._dirtyElements.set(key, true);\n  } else {\n    editor._dirtyLeaves.add(key);\n  }\n  editor._cloneNotNeeded.add(key);\n  editor._dirtyType = HAS_DIRTY_NODES;\n  node.__key = key;\n}\n\nfunction errorOnNodeKeyConstructorMismatch(\n  node: LexicalNode,\n  existingKey: NodeKey,\n) {\n  const editorState = internalGetActiveEditorState();\n  if (!editorState) {\n    // tests expect to be able to do this kind of clone without an active editor state\n    return;\n  }\n  const existingNode = editorState._nodeMap.get(existingKey);\n  if (existingNode && existingNode.constructor !== node.constructor) {\n    // Lifted condition to if statement because the inverted logic is a bit confusing\n    if (node.constructor.name !== existingNode.constructor.name) {\n      invariant(\n        false,\n        'Lexical node with constructor %s attempted to re-use key from node in active editor state with constructor %s. Keys must not be re-used when the type is changed.',\n        node.constructor.name,\n        existingNode.constructor.name,\n      );\n    } else {\n      invariant(\n        false,\n        'Lexical node with constructor %s attempted to re-use key from node in active editor state with different constructor with the same name (possibly due to invalid Hot Module Replacement). Keys must not be re-used when the type is changed.',\n        node.constructor.name,\n      );\n    }\n  }\n}\n\ntype IntentionallyMarkedAsDirtyElement = boolean;\n\nfunction internalMarkParentElementsAsDirty(\n  parentKey: NodeKey,\n  nodeMap: NodeMap,\n  dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,\n): void {\n  let nextParentKey: string | null = parentKey;\n  while (nextParentKey !== null) {\n    if (dirtyElements.has(nextParentKey)) {\n      return;\n    }\n    const node = nodeMap.get(nextParentKey);\n    if (node === undefined) {\n      break;\n    }\n    dirtyElements.set(nextParentKey, false);\n    nextParentKey = node.__parent;\n  }\n}\n\n// TODO #6031 this function or their callers have to adjust selection (i.e. insertBefore)\nexport function removeFromParent(node: LexicalNode): void {\n  const oldParent = node.getParent();\n  if (oldParent !== null) {\n    const writableNode = node.getWritable();\n    const writableParent = oldParent.getWritable();\n    const prevSibling = node.getPreviousSibling();\n    const nextSibling = node.getNextSibling();\n    // TODO: this function duplicates a bunch of operations, can be simplified.\n    if (prevSibling === null) {\n      if (nextSibling !== null) {\n        const writableNextSibling = nextSibling.getWritable();\n        writableParent.__first = nextSibling.__key;\n        writableNextSibling.__prev = null;\n      } else {\n        writableParent.__first = null;\n      }\n    } else {\n      const writablePrevSibling = prevSibling.getWritable();\n      if (nextSibling !== null) {\n        const writableNextSibling = nextSibling.getWritable();\n        writableNextSibling.__prev = writablePrevSibling.__key;\n        writablePrevSibling.__next = writableNextSibling.__key;\n      } else {\n        writablePrevSibling.__next = null;\n      }\n      writableNode.__prev = null;\n    }\n    if (nextSibling === null) {\n      if (prevSibling !== null) {\n        const writablePrevSibling = prevSibling.getWritable();\n        writableParent.__last = prevSibling.__key;\n        writablePrevSibling.__next = null;\n      } else {\n        writableParent.__last = null;\n      }\n    } else {\n      const writableNextSibling = nextSibling.getWritable();\n      if (prevSibling !== null) {\n        const writablePrevSibling = prevSibling.getWritable();\n        writablePrevSibling.__next = writableNextSibling.__key;\n        writableNextSibling.__prev = writablePrevSibling.__key;\n      } else {\n        writableNextSibling.__prev = null;\n      }\n      writableNode.__next = null;\n    }\n    writableParent.__size--;\n    writableNode.__parent = null;\n  }\n}\n\n// Never use this function directly! It will break\n// the cloning heuristic. Instead use node.getWritable().\nexport function internalMarkNodeAsDirty(node: LexicalNode): void {\n  errorOnInfiniteTransforms();\n  const latest = node.getLatest();\n  const parent = latest.__parent;\n  const editorState = getActiveEditorState();\n  const editor = getActiveEditor();\n  const nodeMap = editorState._nodeMap;\n  const dirtyElements = editor._dirtyElements;\n  if (parent !== null) {\n    internalMarkParentElementsAsDirty(parent, nodeMap, dirtyElements);\n  }\n  const key = latest.__key;\n  editor._dirtyType = HAS_DIRTY_NODES;\n  if ($isElementNode(node)) {\n    dirtyElements.set(key, true);\n  } else {\n    // TODO split internally MarkNodeAsDirty into two dedicated Element/leave functions\n    editor._dirtyLeaves.add(key);\n  }\n}\n\nexport function internalMarkSiblingsAsDirty(node: LexicalNode) {\n  const previousNode = node.getPreviousSibling();\n  const nextNode = node.getNextSibling();\n  if (previousNode !== null) {\n    internalMarkNodeAsDirty(previousNode);\n  }\n  if (nextNode !== null) {\n    internalMarkNodeAsDirty(nextNode);\n  }\n}\n\nexport function $setCompositionKey(compositionKey: null | NodeKey): void {\n  errorOnReadOnly();\n  const editor = getActiveEditor();\n  const previousCompositionKey = editor._compositionKey;\n  if (compositionKey !== previousCompositionKey) {\n    editor._compositionKey = compositionKey;\n    if (previousCompositionKey !== null) {\n      const node = $getNodeByKey(previousCompositionKey);\n      if (node !== null) {\n        node.getWritable();\n      }\n    }\n    if (compositionKey !== null) {\n      const node = $getNodeByKey(compositionKey);\n      if (node !== null) {\n        node.getWritable();\n      }\n    }\n  }\n}\n\nexport function $getCompositionKey(): null | NodeKey {\n  if (isCurrentlyReadOnlyMode()) {\n    return null;\n  }\n  const editor = getActiveEditor();\n  return editor._compositionKey;\n}\n\nexport function $getNodeByKey<T extends LexicalNode>(\n  key: NodeKey,\n  _editorState?: EditorState,\n): T | null {\n  const editorState = _editorState || getActiveEditorState();\n  const node = editorState._nodeMap.get(key) as T;\n  if (node === undefined) {\n    return null;\n  }\n  return node;\n}\n\nexport function $getNodeFromDOMNode(\n  dom: Node,\n  editorState?: EditorState,\n): LexicalNode | null {\n  const editor = getActiveEditor();\n  // @ts-ignore We intentionally add this to the Node.\n  const key = dom[`__lexicalKey_${editor._key}`];\n  if (key !== undefined) {\n    return $getNodeByKey(key, editorState);\n  }\n  return null;\n}\n\nexport function $getNearestNodeFromDOMNode(\n  startingDOM: Node,\n  editorState?: EditorState,\n): LexicalNode | null {\n  let dom: Node | null = startingDOM;\n  while (dom != null) {\n    const node = $getNodeFromDOMNode(dom, editorState);\n    if (node !== null) {\n      return node;\n    }\n    dom = getParentElement(dom);\n  }\n  return null;\n}\n\nexport function cloneDecorators(\n  editor: LexicalEditor,\n): Record<NodeKey, unknown> {\n  const currentDecorators = editor._decorators;\n  const pendingDecorators = Object.assign({}, currentDecorators);\n  editor._pendingDecorators = pendingDecorators;\n  return pendingDecorators;\n}\n\nexport function getEditorStateTextContent(editorState: EditorState): string {\n  return editorState.read(() => $getRoot().getTextContent());\n}\n\nexport function markAllNodesAsDirty(editor: LexicalEditor, type: string): void {\n  // Mark all existing text nodes as dirty\n  updateEditor(\n    editor,\n    () => {\n      const editorState = getActiveEditorState();\n      if (editorState.isEmpty()) {\n        return;\n      }\n      if (type === 'root') {\n        $getRoot().markDirty();\n        return;\n      }\n      const nodeMap = editorState._nodeMap;\n      for (const [, node] of nodeMap) {\n        node.markDirty();\n      }\n    },\n    editor._pendingEditorState === null\n      ? {\n          tag: 'history-merge',\n        }\n      : undefined,\n  );\n}\n\nexport function $getRoot(): RootNode {\n  return internalGetRoot(getActiveEditorState());\n}\n\nexport function internalGetRoot(editorState: EditorState): RootNode {\n  return editorState._nodeMap.get('root') as RootNode;\n}\n\nexport function $setSelection(selection: null | BaseSelection): void {\n  errorOnReadOnly();\n  const editorState = getActiveEditorState();\n  if (selection !== null) {\n    if (__DEV__) {\n      if (Object.isFrozen(selection)) {\n        invariant(\n          false,\n          '$setSelection called on frozen selection object. Ensure selection is cloned before passing in.',\n        );\n      }\n    }\n    selection.dirty = true;\n    selection.setCachedNodes(null);\n  }\n  editorState._selection = selection;\n}\n\nexport function $flushMutations(): void {\n  errorOnReadOnly();\n  const editor = getActiveEditor();\n  $flushRootMutations(editor);\n}\n\nexport function $getNodeFromDOM(dom: Node): null | LexicalNode {\n  const editor = getActiveEditor();\n  const nodeKey = getNodeKeyFromDOM(dom, editor);\n  if (nodeKey === null) {\n    const rootElement = editor.getRootElement();\n    if (dom === rootElement) {\n      return $getNodeByKey('root');\n    }\n    return null;\n  }\n  return $getNodeByKey(nodeKey);\n}\n\nexport function getTextNodeOffset(\n  node: TextNode,\n  moveSelectionToEnd: boolean,\n): number {\n  return moveSelectionToEnd ? node.getTextContentSize() : 0;\n}\n\nfunction getNodeKeyFromDOM(\n  // Note that node here refers to a DOM Node, not an Lexical Node\n  dom: Node,\n  editor: LexicalEditor,\n): NodeKey | null {\n  let node: Node | null = dom;\n  while (node != null) {\n    // @ts-ignore We intentionally add this to the Node.\n    const key: NodeKey = node[`__lexicalKey_${editor._key}`];\n    if (key !== undefined) {\n      return key;\n    }\n    node = getParentElement(node);\n  }\n  return null;\n}\n\nexport function doesContainGrapheme(str: string): boolean {\n  return /[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]/g.test(str);\n}\n\nexport function getEditorsToPropagate(\n  editor: LexicalEditor,\n): Array<LexicalEditor> {\n  const editorsToPropagate = [];\n  let currentEditor: LexicalEditor | null = editor;\n  while (currentEditor !== null) {\n    editorsToPropagate.push(currentEditor);\n    currentEditor = currentEditor._parentEditor;\n  }\n  return editorsToPropagate;\n}\n\nexport function createUID(): string {\n  return Math.random()\n    .toString(36)\n    .replace(/[^a-z]+/g, '')\n    .substr(0, 5);\n}\n\nexport function getAnchorTextFromDOM(anchorNode: Node): null | string {\n  if (anchorNode.nodeType === DOM_TEXT_TYPE) {\n    return anchorNode.nodeValue;\n  }\n  return null;\n}\n\nexport function $updateSelectedTextFromDOM(\n  isCompositionEnd: boolean,\n  editor: LexicalEditor,\n  data?: string,\n): void {\n  // Update the text content with the latest composition text\n  const domSelection = getDOMSelection(editor._window);\n  if (domSelection === null) {\n    return;\n  }\n  const anchorNode = domSelection.anchorNode;\n  let {anchorOffset, focusOffset} = domSelection;\n  if (anchorNode !== null) {\n    let textContent = getAnchorTextFromDOM(anchorNode);\n    const node = $getNearestNodeFromDOMNode(anchorNode);\n    if (textContent !== null && $isTextNode(node)) {\n      // Data is intentionally truthy, as we check for boolean, null and empty string.\n      if (textContent === COMPOSITION_SUFFIX && data) {\n        const offset = data.length;\n        textContent = data;\n        anchorOffset = offset;\n        focusOffset = offset;\n      }\n\n      if (textContent !== null) {\n        $updateTextNodeFromDOMContent(\n          node,\n          textContent,\n          anchorOffset,\n          focusOffset,\n          isCompositionEnd,\n        );\n      }\n    }\n  }\n}\n\nexport function $updateTextNodeFromDOMContent(\n  textNode: TextNode,\n  textContent: string,\n  anchorOffset: null | number,\n  focusOffset: null | number,\n  compositionEnd: boolean,\n): void {\n  let node = textNode;\n\n  if (node.isAttached() && (compositionEnd || !node.isDirty())) {\n    const isComposing = node.isComposing();\n    let normalizedTextContent = textContent;\n\n    if (\n      (isComposing || compositionEnd) &&\n      textContent[textContent.length - 1] === COMPOSITION_SUFFIX\n    ) {\n      normalizedTextContent = textContent.slice(0, -1);\n    }\n    const prevTextContent = node.getTextContent();\n\n    if (compositionEnd || normalizedTextContent !== prevTextContent) {\n      if (normalizedTextContent === '') {\n        $setCompositionKey(null);\n        if (!IS_SAFARI && !IS_IOS && !IS_APPLE_WEBKIT) {\n          // For composition (mainly Android), we have to remove the node on a later update\n          const editor = getActiveEditor();\n          setTimeout(() => {\n            editor.update(() => {\n              if (node.isAttached()) {\n                node.remove();\n              }\n            });\n          }, 20);\n        } else {\n          node.remove();\n        }\n        return;\n      }\n      const parent = node.getParent();\n      const prevSelection = $getPreviousSelection();\n      const prevTextContentSize = node.getTextContentSize();\n      const compositionKey = $getCompositionKey();\n      const nodeKey = node.getKey();\n\n      if (\n        node.isToken() ||\n        (compositionKey !== null &&\n          nodeKey === compositionKey &&\n          !isComposing) ||\n        // Check if character was added at the start or boundaries when not insertable, and we need\n        // to clear this input from occurring as that action wasn't permitted.\n        ($isRangeSelection(prevSelection) &&\n          ((parent !== null &&\n            !parent.canInsertTextBefore() &&\n            prevSelection.anchor.offset === 0) ||\n            (prevSelection.anchor.key === textNode.__key &&\n              prevSelection.anchor.offset === 0 &&\n              !node.canInsertTextBefore() &&\n              !isComposing) ||\n            (prevSelection.focus.key === textNode.__key &&\n              prevSelection.focus.offset === prevTextContentSize &&\n              !node.canInsertTextAfter() &&\n              !isComposing)))\n      ) {\n        node.markDirty();\n        return;\n      }\n      const selection = $getSelection();\n\n      if (\n        !$isRangeSelection(selection) ||\n        anchorOffset === null ||\n        focusOffset === null\n      ) {\n        node.setTextContent(normalizedTextContent);\n        return;\n      }\n      selection.setTextNodeRange(node, anchorOffset, node, focusOffset);\n\n      if (node.isSegmented()) {\n        const originalTextContent = node.getTextContent();\n        const replacement = $createTextNode(originalTextContent);\n        node.replace(replacement);\n        node = replacement;\n      }\n      node.setTextContent(normalizedTextContent);\n    }\n  }\n}\n\nfunction $previousSiblingDoesNotAcceptText(node: TextNode): boolean {\n  const previousSibling = node.getPreviousSibling();\n\n  return (\n    ($isTextNode(previousSibling) ||\n      ($isElementNode(previousSibling) && previousSibling.isInline())) &&\n    !previousSibling.canInsertTextAfter()\n  );\n}\n\n// This function is connected to $shouldPreventDefaultAndInsertText and determines whether the\n// TextNode boundaries are writable or we should use the previous/next sibling instead. For example,\n// in the case of a LinkNode, boundaries are not writable.\nexport function $shouldInsertTextAfterOrBeforeTextNode(\n  selection: RangeSelection,\n  node: TextNode,\n): boolean {\n  if (node.isSegmented()) {\n    return true;\n  }\n  if (!selection.isCollapsed()) {\n    return false;\n  }\n  const offset = selection.anchor.offset;\n  const parent = node.getParentOrThrow();\n  const isToken = node.isToken();\n  if (offset === 0) {\n    return (\n      !node.canInsertTextBefore() ||\n      (!parent.canInsertTextBefore() && !node.isComposing()) ||\n      isToken ||\n      $previousSiblingDoesNotAcceptText(node)\n    );\n  } else if (offset === node.getTextContentSize()) {\n    return (\n      !node.canInsertTextAfter() ||\n      (!parent.canInsertTextAfter() && !node.isComposing()) ||\n      isToken\n    );\n  } else {\n    return false;\n  }\n}\n\nexport function isTab(\n  key: string,\n  altKey: boolean,\n  ctrlKey: boolean,\n  metaKey: boolean,\n): boolean {\n  return key === 'Tab' && !altKey && !ctrlKey && !metaKey;\n}\n\nexport function isBold(\n  key: string,\n  altKey: boolean,\n  metaKey: boolean,\n  ctrlKey: boolean,\n): boolean {\n  return (\n    key.toLowerCase() === 'b' && !altKey && controlOrMeta(metaKey, ctrlKey)\n  );\n}\n\nexport function isItalic(\n  key: string,\n  altKey: boolean,\n  metaKey: boolean,\n  ctrlKey: boolean,\n): boolean {\n  return (\n    key.toLowerCase() === 'i' && !altKey && controlOrMeta(metaKey, ctrlKey)\n  );\n}\n\nexport function isUnderline(\n  key: string,\n  altKey: boolean,\n  metaKey: boolean,\n  ctrlKey: boolean,\n): boolean {\n  return (\n    key.toLowerCase() === 'u' && !altKey && controlOrMeta(metaKey, ctrlKey)\n  );\n}\n\nexport function isParagraph(key: string, shiftKey: boolean): boolean {\n  return isReturn(key) && !shiftKey;\n}\n\nexport function isLineBreak(key: string, shiftKey: boolean): boolean {\n  return isReturn(key) && shiftKey;\n}\n\n// Inserts a new line after the selection\n\nexport function isOpenLineBreak(key: string, ctrlKey: boolean): boolean {\n  // 79 = KeyO\n  return IS_APPLE && ctrlKey && key.toLowerCase() === 'o';\n}\n\nexport function isDeleteWordBackward(\n  key: string,\n  altKey: boolean,\n  ctrlKey: boolean,\n): boolean {\n  return isBackspace(key) && (IS_APPLE ? altKey : ctrlKey);\n}\n\nexport function isDeleteWordForward(\n  key: string,\n  altKey: boolean,\n  ctrlKey: boolean,\n): boolean {\n  return isDelete(key) && (IS_APPLE ? altKey : ctrlKey);\n}\n\nexport function isDeleteLineBackward(key: string, metaKey: boolean): boolean {\n  return IS_APPLE && metaKey && isBackspace(key);\n}\n\nexport function isDeleteLineForward(key: string, metaKey: boolean): boolean {\n  return IS_APPLE && metaKey && isDelete(key);\n}\n\nexport function isDeleteBackward(\n  key: string,\n  altKey: boolean,\n  metaKey: boolean,\n  ctrlKey: boolean,\n): boolean {\n  if (IS_APPLE) {\n    if (altKey || metaKey) {\n      return false;\n    }\n    return isBackspace(key) || (key.toLowerCase() === 'h' && ctrlKey);\n  }\n  if (ctrlKey || altKey || metaKey) {\n    return false;\n  }\n  return isBackspace(key);\n}\n\nexport function isDeleteForward(\n  key: string,\n  ctrlKey: boolean,\n  shiftKey: boolean,\n  altKey: boolean,\n  metaKey: boolean,\n): boolean {\n  if (IS_APPLE) {\n    if (shiftKey || altKey || metaKey) {\n      return false;\n    }\n    return isDelete(key) || (key.toLowerCase() === 'd' && ctrlKey);\n  }\n  if (ctrlKey || altKey || metaKey) {\n    return false;\n  }\n  return isDelete(key);\n}\n\nexport function isUndo(\n  key: string,\n  shiftKey: boolean,\n  metaKey: boolean,\n  ctrlKey: boolean,\n): boolean {\n  return (\n    key.toLowerCase() === 'z' && !shiftKey && controlOrMeta(metaKey, ctrlKey)\n  );\n}\n\nexport function isRedo(\n  key: string,\n  shiftKey: boolean,\n  metaKey: boolean,\n  ctrlKey: boolean,\n): boolean {\n  if (IS_APPLE) {\n    return key.toLowerCase() === 'z' && metaKey && shiftKey;\n  }\n  return (\n    (key.toLowerCase() === 'y' && ctrlKey) ||\n    (key.toLowerCase() === 'z' && ctrlKey && shiftKey)\n  );\n}\n\nexport function isCopy(\n  key: string,\n  shiftKey: boolean,\n  metaKey: boolean,\n  ctrlKey: boolean,\n): boolean {\n  if (shiftKey) {\n    return false;\n  }\n  if (key.toLowerCase() === 'c') {\n    return IS_APPLE ? metaKey : ctrlKey;\n  }\n\n  return false;\n}\n\nexport function isCut(\n  key: string,\n  shiftKey: boolean,\n  metaKey: boolean,\n  ctrlKey: boolean,\n): boolean {\n  if (shiftKey) {\n    return false;\n  }\n  if (key.toLowerCase() === 'x') {\n    return IS_APPLE ? metaKey : ctrlKey;\n  }\n\n  return false;\n}\n\nfunction isArrowLeft(key: string): boolean {\n  return key === 'ArrowLeft';\n}\n\nfunction isArrowRight(key: string): boolean {\n  return key === 'ArrowRight';\n}\n\nfunction isArrowUp(key: string): boolean {\n  return key === 'ArrowUp';\n}\n\nfunction isArrowDown(key: string): boolean {\n  return key === 'ArrowDown';\n}\n\nexport function isMoveBackward(\n  key: string,\n  ctrlKey: boolean,\n  altKey: boolean,\n  metaKey: boolean,\n): boolean {\n  return isArrowLeft(key) && !ctrlKey && !metaKey && !altKey;\n}\n\nexport function isMoveToStart(\n  key: string,\n  ctrlKey: boolean,\n  shiftKey: boolean,\n  altKey: boolean,\n  metaKey: boolean,\n): boolean {\n  return isArrowLeft(key) && !altKey && !shiftKey && (ctrlKey || metaKey);\n}\n\nexport function isMoveForward(\n  key: string,\n  ctrlKey: boolean,\n  altKey: boolean,\n  metaKey: boolean,\n): boolean {\n  return isArrowRight(key) && !ctrlKey && !metaKey && !altKey;\n}\n\nexport function isMoveToEnd(\n  key: string,\n  ctrlKey: boolean,\n  shiftKey: boolean,\n  altKey: boolean,\n  metaKey: boolean,\n): boolean {\n  return isArrowRight(key) && !altKey && !shiftKey && (ctrlKey || metaKey);\n}\n\nexport function isMoveUp(\n  key: string,\n  ctrlKey: boolean,\n  metaKey: boolean,\n): boolean {\n  return isArrowUp(key) && !ctrlKey && !metaKey;\n}\n\nexport function isMoveDown(\n  key: string,\n  ctrlKey: boolean,\n  metaKey: boolean,\n): boolean {\n  return isArrowDown(key) && !ctrlKey && !metaKey;\n}\n\nexport function isModifier(\n  ctrlKey: boolean,\n  shiftKey: boolean,\n  altKey: boolean,\n  metaKey: boolean,\n): boolean {\n  return ctrlKey || shiftKey || altKey || metaKey;\n}\n\nexport function isSpace(key: string): boolean {\n  return key === ' ';\n}\n\nexport function controlOrMeta(metaKey: boolean, ctrlKey: boolean): boolean {\n  if (IS_APPLE) {\n    return metaKey;\n  }\n  return ctrlKey;\n}\n\nexport function isReturn(key: string): boolean {\n  return key === 'Enter';\n}\n\nexport function isBackspace(key: string): boolean {\n  return key === 'Backspace';\n}\n\nexport function isEscape(key: string): boolean {\n  return key === 'Escape';\n}\n\nexport function isDelete(key: string): boolean {\n  return key === 'Delete';\n}\n\nexport function isAt(key: string): boolean {\n  return key === '@';\n}\n\nexport function isSelectAll(\n  key: string,\n  metaKey: boolean,\n  ctrlKey: boolean,\n): boolean {\n  return key.toLowerCase() === 'a' && controlOrMeta(metaKey, ctrlKey);\n}\n\nexport function $selectAll(): void {\n  const root = $getRoot();\n  const selection = root.select(0, root.getChildrenSize());\n  $setSelection($normalizeSelection(selection));\n}\n\nexport function getCachedClassNameArray(\n  classNamesTheme: EditorThemeClasses,\n  classNameThemeType: string,\n): Array<string> {\n  if (classNamesTheme.__lexicalClassNameCache === undefined) {\n    classNamesTheme.__lexicalClassNameCache = {};\n  }\n  const classNamesCache = classNamesTheme.__lexicalClassNameCache;\n  const cachedClassNames = classNamesCache[classNameThemeType];\n  if (cachedClassNames !== undefined) {\n    return cachedClassNames;\n  }\n  const classNames = classNamesTheme[classNameThemeType];\n  // As we're using classList, we need\n  // to handle className tokens that have spaces.\n  // The easiest way to do this to convert the\n  // className tokens to an array that can be\n  // applied to classList.add()/remove().\n  if (typeof classNames === 'string') {\n    const classNamesArr = normalizeClassNames(classNames);\n    classNamesCache[classNameThemeType] = classNamesArr;\n    return classNamesArr;\n  }\n  return classNames;\n}\n\nexport function setMutatedNode(\n  mutatedNodes: MutatedNodes,\n  registeredNodes: RegisteredNodes,\n  mutationListeners: MutationListeners,\n  node: LexicalNode,\n  mutation: NodeMutation,\n) {\n  if (mutationListeners.size === 0) {\n    return;\n  }\n  const nodeType = node.__type;\n  const nodeKey = node.__key;\n  const registeredNode = registeredNodes.get(nodeType);\n  if (registeredNode === undefined) {\n    invariant(false, 'Type %s not in registeredNodes', nodeType);\n  }\n  const klass = registeredNode.klass;\n  let mutatedNodesByType = mutatedNodes.get(klass);\n  if (mutatedNodesByType === undefined) {\n    mutatedNodesByType = new Map();\n    mutatedNodes.set(klass, mutatedNodesByType);\n  }\n  const prevMutation = mutatedNodesByType.get(nodeKey);\n  // If the node has already been \"destroyed\", yet we are\n  // re-making it, then this means a move likely happened.\n  // We should change the mutation to be that of \"updated\"\n  // instead.\n  const isMove = prevMutation === 'destroyed' && mutation === 'created';\n  if (prevMutation === undefined || isMove) {\n    mutatedNodesByType.set(nodeKey, isMove ? 'updated' : mutation);\n  }\n}\n\nexport function $nodesOfType<T extends LexicalNode>(klass: Klass<T>): Array<T> {\n  const klassType = klass.getType();\n  const editorState = getActiveEditorState();\n  if (editorState._readOnly) {\n    const nodes = getCachedTypeToNodeMap(editorState).get(klassType) as\n      | undefined\n      | Map<string, T>;\n    return nodes ? Array.from(nodes.values()) : [];\n  }\n  const nodes = editorState._nodeMap;\n  const nodesOfType: Array<T> = [];\n  for (const [, node] of nodes) {\n    if (\n      node instanceof klass &&\n      node.__type === klassType &&\n      node.isAttached()\n    ) {\n      nodesOfType.push(node as T);\n    }\n  }\n  return nodesOfType;\n}\n\nfunction resolveElement(\n  element: ElementNode,\n  isBackward: boolean,\n  focusOffset: number,\n): LexicalNode | null {\n  const parent = element.getParent();\n  let offset = focusOffset;\n  let block = element;\n  if (parent !== null) {\n    if (isBackward && focusOffset === 0) {\n      offset = block.getIndexWithinParent();\n      block = parent;\n    } else if (!isBackward && focusOffset === block.getChildrenSize()) {\n      offset = block.getIndexWithinParent() + 1;\n      block = parent;\n    }\n  }\n  return block.getChildAtIndex(isBackward ? offset - 1 : offset);\n}\n\nexport function $getAdjacentNode(\n  focus: PointType,\n  isBackward: boolean,\n): null | LexicalNode {\n  const focusOffset = focus.offset;\n  if (focus.type === 'element') {\n    const block = focus.getNode();\n    return resolveElement(block, isBackward, focusOffset);\n  } else {\n    const focusNode = focus.getNode();\n    if (\n      (isBackward && focusOffset === 0) ||\n      (!isBackward && focusOffset === focusNode.getTextContentSize())\n    ) {\n      const possibleNode = isBackward\n        ? focusNode.getPreviousSibling()\n        : focusNode.getNextSibling();\n      if (possibleNode === null) {\n        return resolveElement(\n          focusNode.getParentOrThrow(),\n          isBackward,\n          focusNode.getIndexWithinParent() + (isBackward ? 0 : 1),\n        );\n      }\n      return possibleNode;\n    }\n  }\n  return null;\n}\n\nexport function isFirefoxClipboardEvents(editor: LexicalEditor): boolean {\n  const event = getWindow(editor).event;\n  const inputType = event && (event as InputEvent).inputType;\n  return (\n    inputType === 'insertFromPaste' ||\n    inputType === 'insertFromPasteAsQuotation'\n  );\n}\n\nexport function dispatchCommand<TCommand extends LexicalCommand<unknown>>(\n  editor: LexicalEditor,\n  command: TCommand,\n  payload: CommandPayloadType<TCommand>,\n): boolean {\n  return triggerCommandListeners(editor, command, payload);\n}\n\nexport function $textContentRequiresDoubleLinebreakAtEnd(\n  node: ElementNode,\n): boolean {\n  return !$isRootNode(node) && !node.isLastChild() && !node.isInline();\n}\n\nexport function getElementByKeyOrThrow(\n  editor: LexicalEditor,\n  key: NodeKey,\n): HTMLElement {\n  const element = editor._keyToDOMMap.get(key);\n\n  if (element === undefined) {\n    invariant(\n      false,\n      'Reconciliation: could not find DOM element for node key %s',\n      key,\n    );\n  }\n\n  return element;\n}\n\nexport function getParentElement(node: Node): HTMLElement | null {\n  const parentElement =\n    (node as HTMLSlotElement).assignedSlot || node.parentElement;\n  return parentElement !== null && parentElement.nodeType === 11\n    ? ((parentElement as unknown as ShadowRoot).host as HTMLElement)\n    : parentElement;\n}\n\nexport function scrollIntoViewIfNeeded(\n  editor: LexicalEditor,\n  selectionRect: DOMRect,\n  rootElement: HTMLElement,\n): void {\n  const doc = rootElement.ownerDocument;\n  const defaultView = doc.defaultView;\n\n  if (defaultView === null) {\n    return;\n  }\n  let {top: currentTop, bottom: currentBottom} = selectionRect;\n  let targetTop = 0;\n  let targetBottom = 0;\n  let element: HTMLElement | null = rootElement;\n\n  while (element !== null) {\n    const isBodyElement = element === doc.body;\n    if (isBodyElement) {\n      targetTop = 0;\n      targetBottom = getWindow(editor).innerHeight;\n    } else {\n      const targetRect = element.getBoundingClientRect();\n      targetTop = targetRect.top;\n      targetBottom = targetRect.bottom;\n    }\n    let diff = 0;\n\n    if (currentTop < targetTop) {\n      diff = -(targetTop - currentTop);\n    } else if (currentBottom > targetBottom) {\n      diff = currentBottom - targetBottom;\n    }\n\n    if (diff !== 0) {\n      if (isBodyElement) {\n        // Only handles scrolling of Y axis\n        defaultView.scrollBy(0, diff);\n      } else {\n        const scrollTop = element.scrollTop;\n        element.scrollTop += diff;\n        const yOffset = element.scrollTop - scrollTop;\n        currentTop -= yOffset;\n        currentBottom -= yOffset;\n      }\n    }\n    if (isBodyElement) {\n      break;\n    }\n    element = getParentElement(element);\n  }\n}\n\nexport function $hasUpdateTag(tag: string): boolean {\n  const editor = getActiveEditor();\n  return editor._updateTags.has(tag);\n}\n\nexport function $addUpdateTag(tag: string): void {\n  errorOnReadOnly();\n  const editor = getActiveEditor();\n  editor._updateTags.add(tag);\n}\n\nexport function $maybeMoveChildrenSelectionToParent(\n  parentNode: LexicalNode,\n): BaseSelection | null {\n  const selection = $getSelection();\n  if (!$isRangeSelection(selection) || !$isElementNode(parentNode)) {\n    return selection;\n  }\n  const {anchor, focus} = selection;\n  const anchorNode = anchor.getNode();\n  const focusNode = focus.getNode();\n  if ($hasAncestor(anchorNode, parentNode)) {\n    anchor.set(parentNode.__key, 0, 'element');\n  }\n  if ($hasAncestor(focusNode, parentNode)) {\n    focus.set(parentNode.__key, 0, 'element');\n  }\n  return selection;\n}\n\nexport function $hasAncestor(\n  child: LexicalNode,\n  targetNode: LexicalNode,\n): boolean {\n  let parent = child.getParent();\n  while (parent !== null) {\n    if (parent.is(targetNode)) {\n      return true;\n    }\n    parent = parent.getParent();\n  }\n  return false;\n}\n\nexport function getDefaultView(domElem: HTMLElement): Window | null {\n  const ownerDoc = domElem.ownerDocument;\n  return (ownerDoc && ownerDoc.defaultView) || null;\n}\n\nexport function getWindow(editor: LexicalEditor): Window {\n  const windowObj = editor._window;\n  if (windowObj === null) {\n    invariant(false, 'window object not found');\n  }\n  return windowObj;\n}\n\nexport function $isInlineElementOrDecoratorNode(node: LexicalNode): boolean {\n  return (\n    ($isElementNode(node) && node.isInline()) ||\n    ($isDecoratorNode(node) && node.isInline())\n  );\n}\n\nexport function $getNearestRootOrShadowRoot(\n  node: LexicalNode,\n): RootNode | ElementNode {\n  let parent = node.getParentOrThrow();\n  while (parent !== null) {\n    if ($isRootOrShadowRoot(parent)) {\n      return parent;\n    }\n    parent = parent.getParentOrThrow();\n  }\n  return parent;\n}\n\nconst ShadowRootNodeBrand: unique symbol = Symbol.for(\n  '@lexical/ShadowRootNodeBrand',\n);\ntype ShadowRootNode = Spread<\n  {isShadowRoot(): true; [ShadowRootNodeBrand]: never},\n  ElementNode\n>;\nexport function $isRootOrShadowRoot(\n  node: null | LexicalNode,\n): node is RootNode | ShadowRootNode {\n  return $isRootNode(node) || ($isElementNode(node) && node.isShadowRoot());\n}\n\n/**\n * Returns a shallow clone of node with a new key\n *\n * @param node - The node to be copied.\n * @returns The copy of the node.\n */\nexport function $copyNode<T extends LexicalNode>(node: T): T {\n  const copy = node.constructor.clone(node) as T;\n  $setNodeKey(copy, null);\n  return copy;\n}\n\nexport function $applyNodeReplacement<N extends LexicalNode>(\n  node: LexicalNode,\n): N {\n  const editor = getActiveEditor();\n  const nodeType = node.constructor.getType();\n  const registeredNode = editor._nodes.get(nodeType);\n  if (registeredNode === undefined) {\n    invariant(\n      false,\n      '$initializeNode failed. Ensure node has been registered to the editor. You can do this by passing the node class via the \"nodes\" array in the editor config.',\n    );\n  }\n  const replaceFunc = registeredNode.replace;\n  if (replaceFunc !== null) {\n    const replacementNode = replaceFunc(node) as N;\n    if (!(replacementNode instanceof node.constructor)) {\n      invariant(\n        false,\n        '$initializeNode failed. Ensure replacement node is a subclass of the original node.',\n      );\n    }\n    return replacementNode;\n  }\n  return node as N;\n}\n\nexport function errorOnInsertTextNodeOnRoot(\n  node: LexicalNode,\n  insertNode: LexicalNode,\n): void {\n  const parentNode = node.getParent();\n  if (\n    $isRootNode(parentNode) &&\n    !$isElementNode(insertNode) &&\n    !$isDecoratorNode(insertNode)\n  ) {\n    invariant(\n      false,\n      'Only element or decorator nodes can be inserted in to the root node',\n    );\n  }\n}\n\nexport function $getNodeByKeyOrThrow<N extends LexicalNode>(key: NodeKey): N {\n  const node = $getNodeByKey<N>(key);\n  if (node === null) {\n    invariant(\n      false,\n      \"Expected node with key %s to exist but it's not in the nodeMap.\",\n      key,\n    );\n  }\n  return node;\n}\n\nfunction createBlockCursorElement(editorConfig: EditorConfig): HTMLDivElement {\n  const theme = editorConfig.theme;\n  const element = document.createElement('div');\n  element.contentEditable = 'false';\n  element.setAttribute('data-lexical-cursor', 'true');\n  let blockCursorTheme = theme.blockCursor;\n  if (blockCursorTheme !== undefined) {\n    if (typeof blockCursorTheme === 'string') {\n      const classNamesArr = normalizeClassNames(blockCursorTheme);\n      // @ts-expect-error: intentional\n      blockCursorTheme = theme.blockCursor = classNamesArr;\n    }\n    if (blockCursorTheme !== undefined) {\n      element.classList.add(...blockCursorTheme);\n    }\n  }\n  return element;\n}\n\nfunction needsBlockCursor(node: null | LexicalNode): boolean {\n  return (\n    ($isDecoratorNode(node) || ($isElementNode(node) && !node.canBeEmpty())) &&\n    !node.isInline()\n  );\n}\n\nexport function removeDOMBlockCursorElement(\n  blockCursorElement: HTMLElement,\n  editor: LexicalEditor,\n  rootElement: HTMLElement,\n) {\n  rootElement.style.removeProperty('caret-color');\n  editor._blockCursorElement = null;\n  const parentElement = blockCursorElement.parentElement;\n  if (parentElement !== null) {\n    parentElement.removeChild(blockCursorElement);\n  }\n}\n\nexport function updateDOMBlockCursorElement(\n  editor: LexicalEditor,\n  rootElement: HTMLElement,\n  nextSelection: null | BaseSelection,\n): void {\n  let blockCursorElement = editor._blockCursorElement;\n\n  if (\n    $isRangeSelection(nextSelection) &&\n    nextSelection.isCollapsed() &&\n    nextSelection.anchor.type === 'element' &&\n    rootElement.contains(document.activeElement)\n  ) {\n    const anchor = nextSelection.anchor;\n    const elementNode = anchor.getNode();\n    const offset = anchor.offset;\n    const elementNodeSize = elementNode.getChildrenSize();\n    let isBlockCursor = false;\n    let insertBeforeElement: null | HTMLElement = null;\n\n    if (offset === elementNodeSize) {\n      const child = elementNode.getChildAtIndex(offset - 1);\n      if (needsBlockCursor(child)) {\n        isBlockCursor = true;\n      }\n    } else {\n      const child = elementNode.getChildAtIndex(offset);\n      if (needsBlockCursor(child)) {\n        const sibling = (child as LexicalNode).getPreviousSibling();\n        if (sibling === null || needsBlockCursor(sibling)) {\n          isBlockCursor = true;\n          insertBeforeElement = editor.getElementByKey(\n            (child as LexicalNode).__key,\n          );\n        }\n      }\n    }\n    if (isBlockCursor) {\n      const elementDOM = editor.getElementByKey(\n        elementNode.__key,\n      ) as HTMLElement;\n      if (blockCursorElement === null) {\n        editor._blockCursorElement = blockCursorElement =\n          createBlockCursorElement(editor._config);\n      }\n      rootElement.style.caretColor = 'transparent';\n      if (insertBeforeElement === null) {\n        elementDOM.appendChild(blockCursorElement);\n      } else {\n        elementDOM.insertBefore(blockCursorElement, insertBeforeElement);\n      }\n      return;\n    }\n  }\n  // Remove cursor\n  if (blockCursorElement !== null) {\n    removeDOMBlockCursorElement(blockCursorElement, editor, rootElement);\n  }\n}\n\nexport function getDOMSelection(targetWindow: null | Window): null | Selection {\n  return !CAN_USE_DOM ? null : (targetWindow || window).getSelection();\n}\n\nexport function $splitNode(\n  node: ElementNode,\n  offset: number,\n): [ElementNode | null, ElementNode] {\n  let startNode = node.getChildAtIndex(offset);\n  if (startNode == null) {\n    startNode = node;\n  }\n\n  invariant(\n    !$isRootOrShadowRoot(node),\n    'Can not call $splitNode() on root element',\n  );\n\n  const recurse = <T extends LexicalNode>(\n    currentNode: T,\n  ): [ElementNode, ElementNode, T] => {\n    const parent = currentNode.getParentOrThrow();\n    const isParentRoot = $isRootOrShadowRoot(parent);\n    // The node we start split from (leaf) is moved, but its recursive\n    // parents are copied to create separate tree\n    const nodeToMove =\n      currentNode === startNode && !isParentRoot\n        ? currentNode\n        : $copyNode(currentNode);\n\n    if (isParentRoot) {\n      invariant(\n        $isElementNode(currentNode) && $isElementNode(nodeToMove),\n        'Children of a root must be ElementNode',\n      );\n\n      currentNode.insertAfter(nodeToMove);\n      return [currentNode, nodeToMove, nodeToMove];\n    } else {\n      const [leftTree, rightTree, newParent] = recurse(parent);\n      const nextSiblings = currentNode.getNextSiblings();\n\n      newParent.append(nodeToMove, ...nextSiblings);\n      return [leftTree, rightTree, nodeToMove];\n    }\n  };\n\n  const [leftTree, rightTree] = recurse(startNode);\n\n  return [leftTree, rightTree];\n}\n\nexport function $findMatchingParent(\n  startingNode: LexicalNode,\n  findFn: (node: LexicalNode) => boolean,\n): LexicalNode | null {\n  let curr: ElementNode | LexicalNode | null = startingNode;\n\n  while (curr !== $getRoot() && curr != null) {\n    if (findFn(curr)) {\n      return curr;\n    }\n\n    curr = curr.getParent();\n  }\n\n  return null;\n}\n\n/**\n * @param x - The element being tested\n * @returns Returns true if x is an HTML anchor tag, false otherwise\n */\nexport function isHTMLAnchorElement(x: Node): x is HTMLAnchorElement {\n  return isHTMLElement(x) && x.tagName === 'A';\n}\n\n/**\n * @param x - The element being testing\n * @returns Returns true if x is an HTML element, false otherwise.\n */\nexport function isHTMLElement(x: Node | EventTarget): x is HTMLElement {\n  // @ts-ignore-next-line - strict check on nodeType here should filter out non-Element EventTarget implementors\n  return x.nodeType === 1;\n}\n\n/**\n *\n * @param node - the Dom Node to check\n * @returns if the Dom Node is an inline node\n */\nexport function isInlineDomNode(node: Node) {\n  const inlineNodes = new RegExp(\n    /^(a|abbr|acronym|b|cite|code|del|em|i|ins|kbd|label|output|q|ruby|s|samp|span|strong|sub|sup|time|u|tt|var|#text)$/,\n    'i',\n  );\n  return node.nodeName.match(inlineNodes) !== null;\n}\n\n/**\n *\n * @param node - the Dom Node to check\n * @returns if the Dom Node is a block node\n */\nexport function isBlockDomNode(node: Node) {\n  const blockNodes = new RegExp(\n    /^(address|article|aside|blockquote|canvas|dd|div|dl|dt|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hr|li|main|nav|noscript|ol|p|pre|section|table|td|tfoot|ul|video)$/,\n    'i',\n  );\n  return node.nodeName.match(blockNodes) !== null;\n}\n\n/**\n * This function is for internal use of the library.\n * Please do not use it as it may change in the future.\n */\nexport function INTERNAL_$isBlock(\n  node: LexicalNode,\n): node is ElementNode | DecoratorNode<unknown> {\n  if ($isRootNode(node) || ($isDecoratorNode(node) && !node.isInline())) {\n    return true;\n  }\n  if (!$isElementNode(node) || $isRootOrShadowRoot(node)) {\n    return false;\n  }\n\n  const firstChild = node.getFirstChild();\n  const isLeafElement =\n    firstChild === null ||\n    $isLineBreakNode(firstChild) ||\n    $isTextNode(firstChild) ||\n    firstChild.isInline();\n\n  return !node.isInline() && node.canBeEmpty() !== false && isLeafElement;\n}\n\nexport function $getAncestor<NodeType extends LexicalNode = LexicalNode>(\n  node: LexicalNode,\n  predicate: (ancestor: LexicalNode) => ancestor is NodeType,\n) {\n  let parent = node;\n  while (parent !== null && parent.getParent() !== null && !predicate(parent)) {\n    parent = parent.getParentOrThrow();\n  }\n  return predicate(parent) ? parent : null;\n}\n\n/**\n * Utility function for accessing current active editor instance.\n * @returns Current active editor\n */\nexport function $getEditor(): LexicalEditor {\n  return getActiveEditor();\n}\n\n/** @internal */\nexport type TypeToNodeMap = Map<string, NodeMap>;\n/**\n * @internal\n * Compute a cached Map of node type to nodes for a frozen EditorState\n */\nconst cachedNodeMaps = new WeakMap<EditorState, TypeToNodeMap>();\nconst EMPTY_TYPE_TO_NODE_MAP: TypeToNodeMap = new Map();\nexport function getCachedTypeToNodeMap(\n  editorState: EditorState,\n): TypeToNodeMap {\n  // If this is a new Editor it may have a writable this._editorState\n  // with only a 'root' entry.\n  if (!editorState._readOnly && editorState.isEmpty()) {\n    return EMPTY_TYPE_TO_NODE_MAP;\n  }\n  invariant(\n    editorState._readOnly,\n    'getCachedTypeToNodeMap called with a writable EditorState',\n  );\n  let typeToNodeMap = cachedNodeMaps.get(editorState);\n  if (!typeToNodeMap) {\n    typeToNodeMap = new Map();\n    cachedNodeMaps.set(editorState, typeToNodeMap);\n    for (const [nodeKey, node] of editorState._nodeMap) {\n      const nodeType = node.__type;\n      let nodeMap = typeToNodeMap.get(nodeType);\n      if (!nodeMap) {\n        nodeMap = new Map();\n        typeToNodeMap.set(nodeType, nodeMap);\n      }\n      nodeMap.set(nodeKey, node);\n    }\n  }\n  return typeToNodeMap;\n}\n\n/**\n * Returns a clone of a node using `node.constructor.clone()` followed by\n * `clone.afterCloneFrom(node)`. The resulting clone must have the same key,\n * parent/next/prev pointers, and other properties that are not set by\n * `node.constructor.clone` (format, style, etc.). This is primarily used by\n * {@link LexicalNode.getWritable} to create a writable version of an\n * existing node. The clone is the same logical node as the original node,\n * do not try and use this function to duplicate or copy an existing node.\n *\n * Does not mutate the EditorState.\n * @param node - The node to be cloned.\n * @returns The clone of the node.\n */\nexport function $cloneWithProperties<T extends LexicalNode>(latestNode: T): T {\n  const constructor = latestNode.constructor;\n  const mutableNode = constructor.clone(latestNode) as T;\n  mutableNode.afterCloneFrom(latestNode);\n  if (__DEV__) {\n    invariant(\n      mutableNode.__key === latestNode.__key,\n      \"$cloneWithProperties: %s.clone(node) (with type '%s') did not return a node with the same key, make sure to specify node.__key as the last argument to the constructor\",\n      constructor.name,\n      constructor.getType(),\n    );\n    invariant(\n      mutableNode.__parent === latestNode.__parent &&\n        mutableNode.__next === latestNode.__next &&\n        mutableNode.__prev === latestNode.__prev,\n      \"$cloneWithProperties: %s.clone(node) (with type '%s') overrided afterCloneFrom but did not call super.afterCloneFrom(prevNode)\",\n      constructor.name,\n      constructor.getType(),\n    );\n  }\n  return mutableNode;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/__tests__/unit/HTMLCopyAndPaste.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {$insertDataTransferForRichText} from '@lexical/clipboard';\nimport {\n  $createParagraphNode,\n  $getRoot,\n  $getSelection,\n  $isRangeSelection,\n} from 'lexical';\nimport {\n  DataTransferMock,\n  initializeUnitTest,\n  invariant,\n} from 'lexical/__tests__/utils';\n\ndescribe('HTMLCopyAndPaste tests', () => {\n  initializeUnitTest(\n    (testEnv) => {\n      beforeEach(async () => {\n        const {editor} = testEnv;\n        await editor.update(() => {\n          const root = $getRoot();\n          const paragraph = $createParagraphNode();\n          root.append(paragraph);\n          paragraph.select();\n        });\n      });\n\n      const HTML_COPY_PASTING_TESTS = [\n        {\n          expectedHTML: `<p><span data-lexical-text=\"true\">Hello!</span></p>`,\n          name: 'plain DOM text node',\n          pastedHTML: `Hello!`,\n        },\n        {\n          expectedHTML: `<p><span data-lexical-text=\"true\">Hello!</span></p><p><br></p>`,\n          name: 'a paragraph element',\n          pastedHTML: `<p>Hello!<p>`,\n        },\n        {\n          expectedHTML: `<p><span data-lexical-text=\"true\">123</span></p><p><span data-lexical-text=\"true\">456</span></p>`,\n          name: 'a single div',\n          pastedHTML: `123\n            <div>\n              456\n            </div>`,\n        },\n        {\n          expectedHTML: `<p><span data-lexical-text=\"true\">a b c d e</span></p><p><span data-lexical-text=\"true\">f g h</span></p>`,\n          name: 'multiple nested spans and divs',\n          pastedHTML: `<div>\n            a b\n            <span>\n              c d\n              <span>e</span>\n            </span>\n            <div>\n              f\n              <span>g h</span>\n            </div>\n          </div>`,\n        },\n        {\n          expectedHTML: `<p><span data-lexical-text=\"true\">123</span></p><p><span data-lexical-text=\"true\">456</span></p>`,\n          name: 'nested span in a div',\n          pastedHTML: `<div>\n            <span>\n              123\n              <div>456</div>\n            </span>\n          </div>`,\n        },\n        {\n          expectedHTML: `<p><span data-lexical-text=\"true\">123</span></p><p><span data-lexical-text=\"true\">456</span></p>`,\n          name: 'nested div in a span',\n          pastedHTML: ` <span>123<div>456</div></span>`,\n        },\n        {\n          expectedHTML: `<ul><li class=\"task-list-item\" checked=\"checked\" value=\"1\"><span style=\"color: rgb(0, 0, 0);\" data-lexical-text=\"true\">done</span></li><li class=\"task-list-item\" value=\"2\"><span style=\"color: rgb(0, 0, 0);\" data-lexical-text=\"true\">todo</span></li><li value=\"3\" style=\"list-style: none;\"><ul><li class=\"task-list-item\" checked=\"checked\" value=\"1\"><span style=\"color: rgb(0, 0, 0);\" data-lexical-text=\"true\">done</span></li><li class=\"task-list-item\" value=\"2\"><span style=\"color: rgb(0, 0, 0);\" data-lexical-text=\"true\">todo</span></li></ul></li><li class=\"task-list-item\" value=\"3\"><span style=\"color: rgb(0, 0, 0);\" data-lexical-text=\"true\">todo</span></li></ul>`,\n          name: 'google doc checklist',\n          pastedHTML: `<meta charset='utf-8'><meta charset=\"utf-8\"><b style=\"font-weight:normal;\" id=\"docs-internal-guid-1980f960-7fff-f4df-4ba3-26c6e1508542\"><ul style=\"margin-top:0;margin-bottom:0;padding-inline-start:28px;\"><li role=\"checkbox\" aria-checked=\"true\" style=\"list-style-type:none;font-size:11.5pt;font-family:'Optimistic Text',sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:line-through;-webkit-text-decoration-skip:none;text-decoration-skip-ink:none;vertical-align:baseline;white-space:pre;\" aria-level=\"1\"><img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAABbElEQVR4Ae3bsU4CYRDEcRsxodZE8Q0BbS258l5MwESJNL6HOfrPKdhyxeBcwk5mkn9F98sGIOSuPM/zPM/zPI+xG/SEtuiAWpEOaIOWaDIWziP6RK14OzSjX44ITvTBvqRn1MRaMIHeBIE2TKBBEGhgArWkKmtJBjKQgQxkIANd/Aw0NVC+O7RHvYFynHasN1COE/UGynGiXgOIjxOtdIH4OGJAfBwxID6OGBAfRwiIjyMARMCpCjRF5+72Dzhd5R+rHfpC92NeTlWgLl5PkQg4RYBynBSJgFMGKMNJkQg4lYFeUDuFRMCpBXQOEgGnDtA/kPg4xT7m2y/tCd9zKgOdviTC5RQEIiAFjh4QASlw9IAISIEjCURAWvmf1UDKcQwUSDmOgWLdMcxA7BnIQAYykIEM5EcRvplAW0GgNRNoKQg0ZwJN0E4I5x1dI+pmgSSA84BG2QQt0LrYG/eAXtGccjme53me53me9wPjPWZWjhktAQAAAABJRU5ErkJggg==\" width=\"18.4px\" height=\"18.4px\" alt=\"checked\" aria-roledescription=\"checkbox\" style=\"margin-right:3px;\" /><p style=\"line-height:1.38;margin-top:0pt;margin-bottom:0pt;display:inline-block;vertical-align:top;margin-top:0;\" role=\"presentation\"><span style=\"font-size:11.5pt;font-family:'Optimistic Text',sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:line-through;-webkit-text-decoration-skip:none;text-decoration-skip-ink:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">done</span></p></li><li role=\"checkbox\" aria-checked=\"false\" style=\"list-style-type:none;font-size:11.5pt;font-family:'Optimistic Text',sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;\" aria-level=\"1\"><img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAA1ElEQVR4Ae3bMQ4BURSFYY2xBuwQ7BIkTGxFRj9Oo9RdkXn5TvL3L19u+2ZmZmZmZhVbpH26pFcaJ9IrndMudb/CWadHGiden1bll9MIzqd79SUd0thY20qga4NA50qgoUGgoRJo/NL/V/N+QIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEyFeEZyXQpUGgUyXQrkGgTSVQl/qGcG5pnkq3Sn0jOMv0k3Vpm05pmNjfsGPalFyOmZmZmdkbSS9cKbtzhxMAAAAASUVORK5CYII=\" width=\"18.4px\" height=\"18.4px\" alt=\"unchecked\" aria-roledescription=\"checkbox\" style=\"margin-right:3px;\" /><p style=\"line-height:1.38;margin-top:0pt;margin-bottom:0pt;display:inline-block;vertical-align:top;margin-top:0;\" role=\"presentation\"><span style=\"font-size:11.5pt;font-family:'Optimistic Text',sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">todo</span></p></li><ul style=\"margin-top:0;margin-bottom:0;padding-inline-start:28px;\"><li role=\"checkbox\" aria-checked=\"true\" style=\"list-style-type:none;font-size:11.5pt;font-family:'Optimistic Text',sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:line-through;-webkit-text-decoration-skip:none;text-decoration-skip-ink:none;vertical-align:baseline;white-space:pre;\" aria-level=\"2\"><img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAABbElEQVR4Ae3bsU4CYRDEcRsxodZE8Q0BbS258l5MwESJNL6HOfrPKdhyxeBcwk5mkn9F98sGIOSuPM/zPM/zPI+xG/SEtuiAWpEOaIOWaDIWziP6RK14OzSjX44ITvTBvqRn1MRaMIHeBIE2TKBBEGhgArWkKmtJBjKQgQxkIANd/Aw0NVC+O7RHvYFynHasN1COE/UGynGiXgOIjxOtdIH4OGJAfBwxID6OGBAfRwiIjyMARMCpCjRF5+72Dzhd5R+rHfpC92NeTlWgLl5PkQg4RYBynBSJgFMGKMNJkQg4lYFeUDuFRMCpBXQOEgGnDtA/kPg4xT7m2y/tCd9zKgOdviTC5RQEIiAFjh4QASlw9IAISIEjCURAWvmf1UDKcQwUSDmOgWLdMcxA7BnIQAYykIEM5EcRvplAW0GgNRNoKQg0ZwJN0E4I5x1dI+pmgSSA84BG2QQt0LrYG/eAXtGccjme53me53me9wPjPWZWjhktAQAAAABJRU5ErkJggg==\" width=\"18.4px\" height=\"18.4px\" alt=\"checked\" aria-roledescription=\"checkbox\" style=\"margin-right:3px;\" /><p style=\"line-height:1.38;margin-top:0pt;margin-bottom:0pt;display:inline-block;vertical-align:top;margin-top:0;\" role=\"presentation\"><span style=\"font-size:11.5pt;font-family:'Optimistic Text',sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:line-through;-webkit-text-decoration-skip:none;text-decoration-skip-ink:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">done</span></p></li><li role=\"checkbox\" aria-checked=\"false\" style=\"list-style-type:none;font-size:11.5pt;font-family:'Optimistic Text',sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;\" aria-level=\"2\"><img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAA1ElEQVR4Ae3bMQ4BURSFYY2xBuwQ7BIkTGxFRj9Oo9RdkXn5TvL3L19u+2ZmZmZmZhVbpH26pFcaJ9IrndMudb/CWadHGiden1bll9MIzqd79SUd0thY20qga4NA50qgoUGgoRJo/NL/V/N+QIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEyFeEZyXQpUGgUyXQrkGgTSVQl/qGcG5pnkq3Sn0jOMv0k3Vpm05pmNjfsGPalFyOmZmZmdkbSS9cKbtzhxMAAAAASUVORK5CYII=\" width=\"18.4px\" height=\"18.4px\" alt=\"unchecked\" aria-roledescription=\"checkbox\" style=\"margin-right:3px;\" /><p style=\"line-height:1.38;margin-top:0pt;margin-bottom:0pt;display:inline-block;vertical-align:top;margin-top:0;\" role=\"presentation\"><span style=\"font-size:11.5pt;font-family:'Optimistic Text',sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">todo</span></p></li></ul><li role=\"checkbox\" aria-checked=\"false\" style=\"list-style-type:none;font-size:11.5pt;font-family:'Optimistic Text',sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;\" aria-level=\"1\"><img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAA1ElEQVR4Ae3bMQ4BURSFYY2xBuwQ7BIkTGxFRj9Oo9RdkXn5TvL3L19u+2ZmZmZmZhVbpH26pFcaJ9IrndMudb/CWadHGiden1bll9MIzqd79SUd0thY20qga4NA50qgoUGgoRJo/NL/V/N+QIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEyFeEZyXQpUGgUyXQrkGgTSVQl/qGcG5pnkq3Sn0jOMv0k3Vpm05pmNjfsGPalFyOmZmZmdkbSS9cKbtzhxMAAAAASUVORK5CYII=\" width=\"18.4px\" height=\"18.4px\" alt=\"unchecked\" aria-roledescription=\"checkbox\" style=\"margin-right:3px;\" /><p style=\"line-height:1.38;margin-top:0pt;margin-bottom:0pt;display:inline-block;vertical-align:top;margin-top:0;\" role=\"presentation\"><span style=\"font-size:11.5pt;font-family:'Optimistic Text',sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">todo</span></p></li></ul></b>`,\n        },\n        {\n          expectedHTML: `<p><span data-lexical-text=\"true\">checklist</span></p><ul><li class=\"task-list-item\" checked=\"checked\" value=\"1\"><span data-lexical-text=\"true\">done</span></li><li class=\"task-list-item\" value=\"2\"><span data-lexical-text=\"true\">todo</span></li></ul>`,\n          name: 'github checklist',\n          pastedHTML: `<meta charset='utf-8'><p dir=\"auto\" style=\"box-sizing: border-box; margin-top: 0px !important; margin-bottom: 16px; color: rgb(31, 35, 40); font-family: -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, &quot;Noto Sans&quot;, Helvetica, Arial, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;\">checklist</p><ul class=\"contains-task-list\" style=\"box-sizing: border-box; padding: 0px; margin-top: 0px; margin-bottom: 0px !important; position: relative; color: rgb(31, 35, 40); font-family: -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, &quot;Noto Sans&quot;, Helvetica, Arial, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;\"><li class=\"task-list-item enabled\" style=\"box-sizing: border-box; list-style-type: none; padding: 2px 15px 2px 42px; margin-right: -15px; margin-left: -15px; line-height: 1.5; border: 0px;\"><span class=\"handle\" style=\"box-sizing: border-box; display: block; float: left; width: 20px; padding: 2px 0px 0px 2px; margin-left: -43px; opacity: 0;\"><svg class=\"drag-handle\" aria-hidden=\"true\" width=\"16\" height=\"16\"><path d=\"M10 13a1 1 0 100-2 1 1 0 000 2zm-4 0a1 1 0 100-2 1 1 0 000 2zm1-5a1 1 0 11-2 0 1 1 0 012 0zm3 1a1 1 0 100-2 1 1 0 000 2zm1-5a1 1 0 11-2 0 1 1 0 012 0zM6 5a1 1 0 100-2 1 1 0 000 2z\"></path></svg></span><input type=\"checkbox\" id=\"\" class=\"task-list-item-checkbox\" checked=\"\" style=\"box-sizing: border-box; font: inherit; margin: 0px 0.2em 0.25em -1.4em; overflow: visible; padding: 0px; vertical-align: middle;\"><span></span>done</li><li class=\"task-list-item enabled\" style=\"box-sizing: border-box; list-style-type: none; margin-top: 0px; padding: 2px 15px 2px 42px; margin-right: -15px; margin-left: -15px; line-height: 1.5; border: 0px;\"><span class=\"handle\" style=\"box-sizing: border-box; display: block; float: left; width: 20px; padding: 2px 0px 0px 2px; margin-left: -43px; opacity: 0;\"><svg class=\"drag-handle\" aria-hidden=\"true\" width=\"16\" height=\"16\"><path d=\"M10 13a1 1 0 100-2 1 1 0 000 2zm-4 0a1 1 0 100-2 1 1 0 000 2zm1-5a1 1 0 11-2 0 1 1 0 012 0zm3 1a1 1 0 100-2 1 1 0 000 2zm1-5a1 1 0 11-2 0 1 1 0 012 0zM6 5a1 1 0 100-2 1 1 0 000 2z\"></path></svg></span><input type=\"checkbox\" id=\"\" class=\"task-list-item-checkbox\" style=\"box-sizing: border-box; font: inherit; margin: 0px 0.2em 0.25em -1.4em; overflow: visible; padding: 0px; vertical-align: middle;\"><span></span>todo</li></ul>`,\n        },\n      ];\n\n      HTML_COPY_PASTING_TESTS.forEach((testCase, i) => {\n        test(`HTML copy paste: ${testCase.name}`, async () => {\n          const {editor} = testEnv;\n\n          const dataTransfer = new DataTransferMock();\n          dataTransfer.setData('text/html', testCase.pastedHTML);\n          await editor.update(() => {\n            const selection = $getSelection();\n            invariant(\n              $isRangeSelection(selection),\n              'isRangeSelection(selection)',\n            );\n            $insertDataTransferForRichText(dataTransfer, selection, editor);\n          });\n          expect(testEnv.innerHTML).toBe(testCase.expectedHTML);\n        });\n      });\n    },\n    {\n      namespace: 'test',\n      theme: {\n        text: {\n          bold: 'editor-text-bold',\n          italic: 'editor-text-italic',\n          underline: 'editor-text-underline',\n        },\n      },\n    },\n  );\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/__tests__/unit/LexicalEditor.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html';\nimport {\n  $createTableCellNode,\n  $createTableNode,\n  $createTableRowNode,\n  TableCellNode,\n  TableRowNode,\n} from '@lexical/table';\nimport {\n  $createLineBreakNode,\n  $createNodeSelection,\n  $createParagraphNode,\n  $createRangeSelection,\n  $createTextNode,\n  $getEditor,\n  $getNearestNodeFromDOMNode,\n  $getNodeByKey,\n  $getRoot,\n  $isParagraphNode,\n  $isTextNode,\n  $parseSerializedNode,\n  $setCompositionKey,\n  $setSelection,\n  COMMAND_PRIORITY_EDITOR,\n  COMMAND_PRIORITY_LOW,\n  createCommand,\n  createEditor,\n  EditorState,\n  ElementNode,\n  type Klass,\n  type LexicalEditor,\n  type LexicalNode,\n  type LexicalNodeReplacement,\n  ParagraphNode,\n  RootNode,\n  TextNode,\n} from 'lexical';\n\nimport invariant from 'lexical/shared/invariant';\n\nimport {\n  $createTestElementNode,\n  $createTestInlineElementNode,\n  createTestEditor,\n  createTestHeadlessEditor,\n  TestTextNode,\n} from '../utils';\n\ndescribe('LexicalEditor tests', () => {\n  let container: HTMLElement;\n  function setContainerChild(el: HTMLElement) {\n    container.innerHTML = '';\n    container.append(el);\n  }\n\n  beforeEach(() => {\n    container = document.createElement('div');\n    document.body.appendChild(container);\n  });\n\n  afterEach(() => {\n    document.body.removeChild(container);\n    // @ts-ignore\n    container = null;\n\n    jest.restoreAllMocks();\n  });\n\n  function useLexicalEditor(\n    rootElement: HTMLDivElement,\n    onError?: (error: Error) => void,\n    nodes?: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement>,\n  ) {\n    const editor = createTestEditor({\n      nodes: nodes ?? [],\n      onError: onError || jest.fn(),\n      theme: {\n        text: {\n          bold: 'editor-text-bold',\n          italic: 'editor-text-italic',\n          underline: 'editor-text-underline',\n        },\n      },\n    });\n    editor.setRootElement(rootElement);\n    return editor;\n  }\n\n  let editor: LexicalEditor;\n\n  function init(onError?: (error: Error) => void) {\n    const edContainer = document.createElement('div');\n    edContainer.setAttribute('contenteditable', 'true');\n\n    setContainerChild(edContainer);\n    editor = useLexicalEditor(edContainer, onError);\n  }\n\n  async function update(fn: () => void) {\n    editor.update(fn);\n\n    return Promise.resolve().then();\n  }\n\n  describe('read()', () => {\n    it('Can read the editor state', async () => {\n      init(function onError(err) {\n        throw err;\n      });\n      expect(editor.read(() => $getRoot().getTextContent())).toEqual('');\n      expect(editor.read(() => $getEditor())).toBe(editor);\n      const onUpdate = jest.fn();\n      editor.update(\n        () => {\n          const root = $getRoot();\n          const paragraph = $createParagraphNode();\n          const text = $createTextNode('This works!');\n          root.append(paragraph);\n          paragraph.append(text);\n        },\n        {onUpdate},\n      );\n      expect(onUpdate).toHaveBeenCalledTimes(0);\n      // This read will flush pending updates\n      expect(editor.read(() => $getRoot().getTextContent())).toEqual(\n        'This works!',\n      );\n      expect(onUpdate).toHaveBeenCalledTimes(1);\n      // Check to make sure there is not an unexpected reconciliation\n      await Promise.resolve().then();\n      expect(onUpdate).toHaveBeenCalledTimes(1);\n      editor.read(() => {\n        const rootElement = editor.getRootElement();\n        expect(rootElement).toBeDefined();\n        // The root never works for this call\n        expect($getNearestNodeFromDOMNode(rootElement!)).toBe(null);\n        const paragraphDom = rootElement!.querySelector('p');\n        expect(paragraphDom).toBeDefined();\n        expect(\n          $isParagraphNode($getNearestNodeFromDOMNode(paragraphDom!)),\n        ).toBe(true);\n        expect(\n          $getNearestNodeFromDOMNode(paragraphDom!)!.getTextContent(),\n        ).toBe('This works!');\n        const textDom = paragraphDom!.querySelector('span');\n        expect(textDom).toBeDefined();\n        expect($isTextNode($getNearestNodeFromDOMNode(textDom!))).toBe(true);\n        expect($getNearestNodeFromDOMNode(textDom!)!.getTextContent()).toBe(\n          'This works!',\n        );\n        expect(\n          $getNearestNodeFromDOMNode(textDom!.firstChild!)!.getTextContent(),\n        ).toBe('This works!');\n      });\n      expect(onUpdate).toHaveBeenCalledTimes(1);\n    });\n    it('runs transforms the editor state', async () => {\n      init(function onError(err) {\n        throw err;\n      });\n      expect(editor.read(() => $getRoot().getTextContent())).toEqual('');\n      expect(editor.read(() => $getEditor())).toBe(editor);\n      editor.registerNodeTransform(TextNode, (node) => {\n        if (node.getTextContent() === 'This works!') {\n          node.replace($createTextNode('Transforms work!'));\n        }\n      });\n      const onUpdate = jest.fn();\n      editor.update(\n        () => {\n          const root = $getRoot();\n          const paragraph = $createParagraphNode();\n          const text = $createTextNode('This works!');\n          root.append(paragraph);\n          paragraph.append(text);\n        },\n        {onUpdate},\n      );\n      expect(onUpdate).toHaveBeenCalledTimes(0);\n      // This read will flush pending updates\n      expect(editor.read(() => $getRoot().getTextContent())).toEqual(\n        'Transforms work!',\n      );\n      expect(editor.getRootElement()!.textContent).toEqual('Transforms work!');\n      expect(onUpdate).toHaveBeenCalledTimes(1);\n      // Check to make sure there is not an unexpected reconciliation\n      await Promise.resolve().then();\n      expect(onUpdate).toHaveBeenCalledTimes(1);\n      expect(editor.read(() => $getRoot().getTextContent())).toEqual(\n        'Transforms work!',\n      );\n    });\n    it('can be nested in an update or read', async () => {\n      init(function onError(err) {\n        throw err;\n      });\n      editor.update(() => {\n        const root = $getRoot();\n        const paragraph = $createParagraphNode();\n        const text = $createTextNode('This works!');\n        root.append(paragraph);\n        paragraph.append(text);\n        editor.read(() => {\n          expect($getRoot().getTextContent()).toBe('This works!');\n        });\n        editor.read(() => {\n          // Nesting update in read works, although it is discouraged in the documentation.\n          editor.update(() => {\n            expect($getRoot().getTextContent()).toBe('This works!');\n          });\n        });\n        // Updating after a nested read will fail as it has already been committed\n        expect(() => {\n          root.append(\n            $createParagraphNode().append(\n              $createTextNode('update-read-update'),\n            ),\n          );\n        }).toThrow();\n      });\n      editor.read(() => {\n        editor.read(() => {\n          expect($getRoot().getTextContent()).toBe('This works!');\n        });\n      });\n    });\n  });\n\n  it('Should create an editor with an initial editor state', async () => {\n    const rootElement = document.createElement('div');\n\n    container.appendChild(rootElement);\n\n    const initialEditor = createTestEditor({\n      onError: jest.fn(),\n    });\n\n    initialEditor.update(() => {\n      const root = $getRoot();\n      const paragraph = $createParagraphNode();\n      const text = $createTextNode('This works!');\n      root.append(paragraph);\n      paragraph.append(text);\n    });\n\n    initialEditor.setRootElement(rootElement);\n\n    // Wait for update to complete\n    await Promise.resolve().then();\n\n    expect(container.innerHTML).toBe(\n      '<div style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">This works!</span></p></div>',\n    );\n\n    const initialEditorState = initialEditor.getEditorState();\n    initialEditor.setRootElement(null);\n\n    expect(container.innerHTML).toBe(\n      '<div style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"></div>',\n    );\n\n    editor = createTestEditor({\n      editorState: initialEditorState,\n      onError: jest.fn(),\n    });\n    editor.setRootElement(rootElement);\n\n    expect(editor.getEditorState()).toEqual(initialEditorState);\n    expect(container.innerHTML).toBe(\n      '<div style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">This works!</span></p></div>',\n    );\n  });\n\n  it('Should handle nested updates in the correct sequence', async () => {\n    init();\n    const onUpdate = jest.fn();\n\n    let log: Array<string> = [];\n\n    editor.registerUpdateListener(onUpdate);\n    editor.update(() => {\n      const root = $getRoot();\n      const paragraph = $createParagraphNode();\n      const text = $createTextNode('This works!');\n      root.append(paragraph);\n      paragraph.append(text);\n    });\n\n    editor.update(\n      () => {\n        log.push('A1');\n        // To enforce the update\n        $getRoot().markDirty();\n        editor.update(\n          () => {\n            log.push('B1');\n            editor.update(\n              () => {\n                log.push('C1');\n              },\n              {\n                onUpdate: () => {\n                  log.push('F1');\n                },\n              },\n            );\n          },\n          {\n            onUpdate: () => {\n              log.push('E1');\n            },\n          },\n        );\n      },\n      {\n        onUpdate: () => {\n          log.push('D1');\n        },\n      },\n    );\n\n    // Wait for update to complete\n    await Promise.resolve().then();\n\n    expect(onUpdate).toHaveBeenCalledTimes(1);\n    expect(log).toEqual(['A1', 'B1', 'C1', 'D1', 'E1', 'F1']);\n\n    log = [];\n    editor.update(\n      () => {\n        log.push('A2');\n        // To enforce the update\n        $getRoot().markDirty();\n      },\n      {\n        onUpdate: () => {\n          log.push('B2');\n          editor.update(\n            () => {\n              // force flush sync\n              $setCompositionKey('root');\n              log.push('D2');\n            },\n            {\n              onUpdate: () => {\n                log.push('F2');\n              },\n            },\n          );\n          log.push('C2');\n          editor.update(\n            () => {\n              log.push('E2');\n            },\n            {\n              onUpdate: () => {\n                log.push('G2');\n              },\n            },\n          );\n        },\n      },\n    );\n\n    // Wait for update to complete\n    await Promise.resolve().then();\n\n    expect(log).toEqual(['A2', 'B2', 'C2', 'D2', 'E2', 'F2', 'G2']);\n\n    log = [];\n    editor.registerNodeTransform(TextNode, () => {\n      log.push('TextTransform A3');\n      editor.update(\n        () => {\n          log.push('TextTransform B3');\n        },\n        {\n          onUpdate: () => {\n            log.push('TextTransform C3');\n          },\n        },\n      );\n    });\n\n    // Wait for update to complete\n    await Promise.resolve().then();\n\n    expect(log).toEqual([\n      'TextTransform A3',\n      'TextTransform B3',\n      'TextTransform C3',\n    ]);\n\n    log = [];\n    editor.update(\n      () => {\n        log.push('A3');\n        $getRoot().getLastDescendant()!.markDirty();\n      },\n      {\n        onUpdate: () => {\n          log.push('B3');\n        },\n      },\n    );\n\n    // Wait for update to complete\n    await Promise.resolve().then();\n\n    expect(log).toEqual([\n      'A3',\n      'TextTransform A3',\n      'TextTransform B3',\n      'B3',\n      'TextTransform C3',\n    ]);\n  });\n\n  it('nested update after selection update triggers exactly 1 update', async () => {\n    init();\n    const onUpdate = jest.fn();\n    editor.registerUpdateListener(onUpdate);\n    editor.update(() => {\n      $setSelection($createRangeSelection());\n      editor.update(() => {\n        $getRoot().append(\n          $createParagraphNode().append($createTextNode('Sync update')),\n        );\n      });\n    });\n\n    await Promise.resolve().then();\n\n    const textContent = editor\n      .getEditorState()\n      .read(() => $getRoot().getTextContent());\n    expect(textContent).toBe('Sync update');\n    expect(onUpdate).toHaveBeenCalledTimes(1);\n  });\n\n  it('update does not call onUpdate callback when no dirty nodes', () => {\n    init();\n\n    const fn = jest.fn();\n    editor.update(\n      () => {\n        //\n      },\n      {\n        onUpdate: fn,\n      },\n    );\n    expect(fn).toHaveBeenCalledTimes(0);\n  });\n\n  it('editor.focus() callback is called', async () => {\n    init();\n\n    await editor.update(() => {\n      const root = $getRoot();\n      root.append($createParagraphNode());\n    });\n\n    const fn = jest.fn();\n\n    await editor.focus(fn);\n\n    expect(fn).toHaveBeenCalledTimes(1);\n  });\n\n  it('Synchronously runs three transforms, two of them depend on the other', async () => {\n    init();\n\n    // 2. Add italics\n    const italicsListener = editor.registerNodeTransform(TextNode, (node) => {\n      if (\n        node.getTextContent() === 'foo' &&\n        node.hasFormat('bold') &&\n        !node.hasFormat('italic')\n      ) {\n        node.toggleFormat('italic');\n      }\n    });\n\n    // 1. Add bold\n    const boldListener = editor.registerNodeTransform(TextNode, (node) => {\n      if (node.getTextContent() === 'foo' && !node.hasFormat('bold')) {\n        node.toggleFormat('bold');\n      }\n    });\n\n    // 2. Add underline\n    const underlineListener = editor.registerNodeTransform(TextNode, (node) => {\n      if (\n        node.getTextContent() === 'foo' &&\n        node.hasFormat('bold') &&\n        !node.hasFormat('underline')\n      ) {\n        node.toggleFormat('underline');\n      }\n    });\n\n    await editor.update(() => {\n      const root = $getRoot();\n      const paragraph = $createParagraphNode();\n      root.append(paragraph);\n      paragraph.append($createTextNode('foo'));\n    });\n    italicsListener();\n    boldListener();\n    underlineListener();\n\n    expect(container.innerHTML).toBe(\n      '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><strong class=\"editor-text-bold editor-text-italic editor-text-underline\" data-lexical-text=\"true\">foo</strong></p></div>',\n    );\n  });\n\n  it('Synchronously runs three transforms, two of them depend on the other (2)', async () => {\n    await init();\n\n    // Add transform makes everything dirty the first time (let's not leverage this here)\n    const skipFirst = [true, true, true];\n\n    // 2. (Block transform) Add text\n    const testParagraphListener = editor.registerNodeTransform(\n      ParagraphNode,\n      (paragraph) => {\n        if (skipFirst[0]) {\n          skipFirst[0] = false;\n\n          return;\n        }\n\n        if (paragraph.isEmpty()) {\n          paragraph.append($createTextNode('foo'));\n        }\n      },\n    );\n\n    // 2. (Text transform) Add bold to text\n    const boldListener = editor.registerNodeTransform(TextNode, (node) => {\n      if (node.getTextContent() === 'foo' && !node.hasFormat('bold')) {\n        node.toggleFormat('bold');\n      }\n    });\n\n    // 3. (Block transform) Add italics to bold text\n    const italicsListener = editor.registerNodeTransform(\n      ParagraphNode,\n      (paragraph) => {\n        const child = paragraph.getLastDescendant();\n\n        if (\n          $isTextNode(child) &&\n          child.hasFormat('bold') &&\n          !child.hasFormat('italic')\n        ) {\n          child.toggleFormat('italic');\n        }\n      },\n    );\n\n    await editor.update(() => {\n      const root = $getRoot();\n      const paragraph = $createParagraphNode();\n      root.append(paragraph);\n    });\n\n    await editor.update(() => {\n      const root = $getRoot();\n      const paragraph = root.getFirstChild();\n      paragraph!.markDirty();\n    });\n\n    testParagraphListener();\n    boldListener();\n    italicsListener();\n\n    expect(container.innerHTML).toBe(\n      '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><strong class=\"editor-text-bold editor-text-italic\" data-lexical-text=\"true\">foo</strong></p></div>',\n    );\n  });\n\n  it('Synchronously runs three transforms, two of them depend on previously merged text content', async () => {\n    const hasRun = [false, false, false];\n    init();\n\n    // 1. [Foo] into [<empty>,Fo,o,<empty>,!,<empty>]\n    const fooListener = editor.registerNodeTransform(TextNode, (node) => {\n      if (node.getTextContent() === 'Foo' && !hasRun[0]) {\n        const [before, after] = node.splitText(2);\n\n        before.insertBefore($createTextNode(''));\n        after.insertAfter($createTextNode(''));\n        after.insertAfter($createTextNode('!'));\n        after.insertAfter($createTextNode(''));\n\n        hasRun[0] = true;\n      }\n    });\n\n    // 2. [Foo!] into [<empty>,Fo,o!,<empty>,!,<empty>]\n    const megaFooListener = editor.registerNodeTransform(\n      ParagraphNode,\n      (paragraph) => {\n        const child = paragraph.getFirstChild();\n\n        if (\n          $isTextNode(child) &&\n          child.getTextContent() === 'Foo!' &&\n          !hasRun[1]\n        ) {\n          const [before, after] = child.splitText(2);\n\n          before.insertBefore($createTextNode(''));\n          after.insertAfter($createTextNode(''));\n          after.insertAfter($createTextNode('!'));\n          after.insertAfter($createTextNode(''));\n\n          hasRun[1] = true;\n        }\n      },\n    );\n\n    // 3. [Foo!!] into formatted bold [<empty>,Fo,o!!,<empty>]\n    const boldFooListener = editor.registerNodeTransform(TextNode, (node) => {\n      if (node.getTextContent() === 'Foo!!' && !hasRun[2]) {\n        node.toggleFormat('bold');\n\n        const [before, after] = node.splitText(2);\n        before.insertBefore($createTextNode(''));\n        after.insertAfter($createTextNode(''));\n\n        hasRun[2] = true;\n      }\n    });\n\n    await editor.update(() => {\n      const root = $getRoot();\n      const paragraph = $createParagraphNode();\n\n      root.append(paragraph);\n      paragraph.append($createTextNode('Foo'));\n    });\n\n    fooListener();\n    megaFooListener();\n    boldFooListener();\n\n    expect(container.innerHTML).toBe(\n      '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><strong class=\"editor-text-bold\" data-lexical-text=\"true\">Foo!!</strong></p></div>',\n    );\n  });\n\n  it('text transform runs when node is removed', async () => {\n    init();\n\n    const executeTransform = jest.fn();\n    let hasBeenRemoved = false;\n    const removeListener = editor.registerNodeTransform(TextNode, (node) => {\n      if (hasBeenRemoved) {\n        executeTransform();\n      }\n    });\n\n    await editor.update(() => {\n      const root = $getRoot();\n      const paragraph = $createParagraphNode();\n      root.append(paragraph);\n      paragraph.append(\n        $createTextNode('Foo').toggleUnmergeable(),\n        $createTextNode('Bar').toggleUnmergeable(),\n      );\n    });\n\n    await editor.update(() => {\n      $getRoot().getLastDescendant()!.remove();\n      hasBeenRemoved = true;\n    });\n\n    expect(executeTransform).toHaveBeenCalledTimes(1);\n\n    removeListener();\n  });\n\n  it('transforms only run on nodes that were explicitly marked as dirty', async () => {\n    init();\n\n    let executeParagraphNodeTransform = () => {\n      return;\n    };\n\n    let executeTextNodeTransform = () => {\n      return;\n    };\n\n    const removeParagraphTransform = editor.registerNodeTransform(\n      ParagraphNode,\n      (node) => {\n        executeParagraphNodeTransform();\n      },\n    );\n    const removeTextNodeTransform = editor.registerNodeTransform(\n      TextNode,\n      (node) => {\n        executeTextNodeTransform();\n      },\n    );\n\n    await editor.update(() => {\n      const root = $getRoot();\n      const paragraph = $createParagraphNode();\n      root.append(paragraph);\n      paragraph.append($createTextNode('Foo'));\n    });\n\n    await editor.update(() => {\n      const root = $getRoot();\n      const paragraph = root.getFirstChild() as ParagraphNode;\n      const textNode = paragraph.getFirstChild() as TextNode;\n\n      textNode.getWritable();\n\n      executeParagraphNodeTransform = jest.fn();\n      executeTextNodeTransform = jest.fn();\n    });\n\n    expect(executeParagraphNodeTransform).toHaveBeenCalledTimes(0);\n    expect(executeTextNodeTransform).toHaveBeenCalledTimes(1);\n\n    removeParagraphTransform();\n    removeTextNodeTransform();\n  });\n\n  describe('transforms on siblings', () => {\n    let textNodeKeys: string[];\n    let textTransformCount: number[];\n    let removeTransform: () => void;\n\n    beforeEach(async () => {\n      init();\n\n      textNodeKeys = [];\n      textTransformCount = [];\n\n      await editor.update(() => {\n        const root = $getRoot();\n        const paragraph0 = $createParagraphNode();\n        const paragraph1 = $createParagraphNode();\n        const textNodes: Array<LexicalNode> = [];\n\n        for (let i = 0; i < 6; i++) {\n          const node = $createTextNode(String(i)).toggleUnmergeable();\n          textNodes.push(node);\n          textNodeKeys.push(node.getKey());\n          textTransformCount[i] = 0;\n        }\n\n        root.append(paragraph0, paragraph1);\n        paragraph0.append(...textNodes.slice(0, 3));\n        paragraph1.append(...textNodes.slice(3));\n      });\n\n      removeTransform = editor.registerNodeTransform(TextNode, (node) => {\n        textTransformCount[Number(node.__text)]++;\n      });\n    });\n\n    afterEach(() => {\n      removeTransform();\n    });\n\n    it('on remove', async () => {\n      await editor.update(() => {\n        const textNode1 = $getNodeByKey(textNodeKeys[1])!;\n        textNode1.remove();\n      });\n      expect(textTransformCount).toEqual([2, 1, 2, 1, 1, 1]);\n    });\n\n    it('on replace', async () => {\n      await editor.update(() => {\n        const textNode1 = $getNodeByKey(textNodeKeys[1])!;\n        const textNode4 = $getNodeByKey(textNodeKeys[4])!;\n        textNode4.replace(textNode1);\n      });\n      expect(textTransformCount).toEqual([2, 2, 2, 2, 1, 2]);\n    });\n\n    it('on insertBefore', async () => {\n      await editor.update(() => {\n        const textNode1 = $getNodeByKey(textNodeKeys[1])!;\n        const textNode4 = $getNodeByKey(textNodeKeys[4])!;\n        textNode4.insertBefore(textNode1);\n      });\n      expect(textTransformCount).toEqual([2, 2, 2, 2, 2, 1]);\n    });\n\n    it('on insertAfter', async () => {\n      await editor.update(() => {\n        const textNode1 = $getNodeByKey(textNodeKeys[1])!;\n        const textNode4 = $getNodeByKey(textNodeKeys[4])!;\n        textNode4.insertAfter(textNode1);\n      });\n      expect(textTransformCount).toEqual([2, 2, 2, 1, 2, 2]);\n    });\n\n    it('on splitText', async () => {\n      await editor.update(() => {\n        const textNode1 = $getNodeByKey(textNodeKeys[1]) as TextNode;\n        textNode1.setTextContent('67');\n        textNode1.splitText(1);\n        textTransformCount.push(0, 0);\n      });\n      expect(textTransformCount).toEqual([2, 1, 2, 1, 1, 1, 1, 1]);\n    });\n\n    it('on append', async () => {\n      await editor.update(() => {\n        const paragraph1 = $getRoot().getFirstChild() as ParagraphNode;\n        paragraph1.append($createTextNode('6').toggleUnmergeable());\n        textTransformCount.push(0);\n      });\n      expect(textTransformCount).toEqual([1, 1, 2, 1, 1, 1, 1]);\n    });\n  });\n\n  it('Detects infinite recursivity on transforms', async () => {\n    const errorListener = jest.fn();\n    init(errorListener);\n\n    const boldListener = editor.registerNodeTransform(TextNode, (node) => {\n      node.toggleFormat('bold');\n    });\n\n    expect(errorListener).toHaveBeenCalledTimes(0);\n\n    await editor.update(() => {\n      const root = $getRoot();\n      const paragraph = $createParagraphNode();\n      root.append(paragraph);\n      paragraph.append($createTextNode('foo'));\n    });\n\n    expect(errorListener).toHaveBeenCalledTimes(1);\n    boldListener();\n  });\n\n  it('Should be able to update an editor state without a root element', () => {\n    const element = document.createElement('div');\n    element.setAttribute('contenteditable', 'true');\n    setContainerChild(element);\n\n    editor = createTestEditor();\n\n    editor.update(() => {\n      const root = $getRoot();\n      const paragraph = $createParagraphNode();\n      const text = $createTextNode('This works!');\n      root.append(paragraph);\n      paragraph.append(text);\n    });\n\n    expect(container.innerHTML).toBe('<div contenteditable=\"true\"></div>');\n\n    editor.setRootElement(element);\n\n    expect(container.innerHTML).toBe(\n      '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">This works!</span></p></div>',\n    );\n  });\n\n  it('Should be able to recover from an update error', async () => {\n    const errorListener = jest.fn();\n    init(errorListener);\n    editor.update(() => {\n      const root = $getRoot();\n\n      if (root.getFirstChild() === null) {\n        const paragraph = $createParagraphNode();\n        const text = $createTextNode('This works!');\n        root.append(paragraph);\n        paragraph.append(text);\n      }\n    });\n\n    // Wait for update to complete\n    await Promise.resolve().then();\n\n    expect(container.innerHTML).toBe(\n      '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">This works!</span></p></div>',\n    );\n    expect(errorListener).toHaveBeenCalledTimes(0);\n\n    editor.update(() => {\n      const root = $getRoot();\n      root\n        .getFirstChild<ElementNode>()!\n        .getFirstChild<ElementNode>()!\n        .getFirstChild<TextNode>()!\n        .setTextContent('Foo');\n    });\n\n    expect(errorListener).toHaveBeenCalledTimes(1);\n    expect(container.innerHTML).toBe(\n      '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">This works!</span></p></div>',\n    );\n  });\n\n  it('Should be able to handle a change in root element', async () => {\n    const rootListener = jest.fn();\n    const updateListener = jest.fn();\n\n    let editorInstance = createTestEditor();\n    editorInstance.registerRootListener(rootListener);\n    editorInstance.registerUpdateListener(updateListener);\n\n    let edContainer: HTMLElement = document.createElement('div');\n    edContainer.setAttribute('contenteditable', 'true');\n    setContainerChild(edContainer);\n    editorInstance.setRootElement(edContainer);\n\n    function runUpdate(changeElement: boolean) {\n      editorInstance.update(() => {\n        const root = $getRoot();\n        const firstChild = root.getFirstChild() as ParagraphNode | null;\n        const text = changeElement ? 'Change successful' : 'Not changed';\n\n        if (firstChild === null) {\n          const paragraph = $createParagraphNode();\n          const textNode = $createTextNode(text);\n          paragraph.append(textNode);\n          root.append(paragraph);\n        } else {\n          const textNode = firstChild.getFirstChild() as TextNode;\n          textNode.setTextContent(text);\n        }\n      });\n    }\n\n    setContainerChild(edContainer);\n    editorInstance.setRootElement(edContainer);\n    runUpdate(false);\n    editorInstance.commitUpdates();\n\n    expect(container.innerHTML).toBe(\n      '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">Not changed</span></p></div>',\n    );\n\n    edContainer = document.createElement('span');\n    edContainer.setAttribute('contenteditable', 'true');\n    runUpdate(true);\n    editorInstance.setRootElement(edContainer);\n    setContainerChild(edContainer);\n    editorInstance.commitUpdates();\n\n    expect(rootListener).toHaveBeenCalledTimes(3);\n    expect(updateListener).toHaveBeenCalledTimes(3);\n    expect(container.innerHTML).toBe(\n      '<span contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">Change successful</span></p></span>',\n    );\n  });\n\n  for (const editable of [true, false]) {\n    it(`Retains pendingEditor while rootNode is not set (${\n      editable ? 'editable' : 'non-editable'\n    })`, async () => {\n      const JSON_EDITOR_STATE =\n        '{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"123\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"root\",\"version\":1}}';\n      init();\n      const contentEditable = editor.getRootElement();\n      editor.setEditable(editable);\n      editor.setRootElement(null);\n      const editorState = editor.parseEditorState(JSON_EDITOR_STATE);\n      editor.setEditorState(editorState);\n      editor.update(() => {\n        //\n      });\n      editor.setRootElement(contentEditable);\n      expect(JSON.stringify(editor.getEditorState().toJSON())).toBe(\n        JSON_EDITOR_STATE,\n      );\n    });\n  }\n\n  describe('parseEditorState()', () => {\n    let originalText: TextNode;\n    let parsedParagraph: ParagraphNode;\n    let parsedRoot: RootNode;\n    let parsedText: TextNode;\n    let paragraphKey: string;\n    let textKey: string;\n    let parsedEditorState: EditorState;\n\n    it('exportJSON API - parses parsed JSON', async () => {\n      await update(() => {\n        const paragraph = $createParagraphNode();\n        originalText = $createTextNode('Hello world');\n        originalText.select(6, 11);\n        paragraph.append(originalText);\n        $getRoot().append(paragraph);\n      });\n      const stringifiedEditorState = JSON.stringify(editor.getEditorState());\n      const parsedEditorStateFromObject = editor.parseEditorState(\n        JSON.parse(stringifiedEditorState),\n      );\n      parsedEditorStateFromObject.read(() => {\n        const root = $getRoot();\n        expect(root.getTextContent()).toMatch(/Hello world/);\n      });\n    });\n\n    describe('range selection', () => {\n      beforeEach(async () => {\n        await init();\n\n        await update(() => {\n          const paragraph = $createParagraphNode();\n          originalText = $createTextNode('Hello world');\n          originalText.select(6, 11);\n          paragraph.append(originalText);\n          $getRoot().append(paragraph);\n        });\n        const stringifiedEditorState = JSON.stringify(\n          editor.getEditorState().toJSON(),\n        );\n        parsedEditorState = editor.parseEditorState(stringifiedEditorState);\n        parsedEditorState.read(() => {\n          parsedRoot = $getRoot();\n          parsedParagraph = parsedRoot.getFirstChild() as ParagraphNode;\n          paragraphKey = parsedParagraph.getKey();\n          parsedText = parsedParagraph.getFirstChild() as TextNode;\n          textKey = parsedText.getKey();\n        });\n      });\n\n      it('Parses the nodes of a stringified editor state', async () => {\n        expect(parsedRoot).toEqual({\n          __cachedText: null,\n          __dir: null,\n          __first: paragraphKey,\n          __key: 'root',\n          __last: paragraphKey,\n          __next: null,\n          __parent: null,\n          __prev: null,\n          __size: 1,\n          __style: '',\n          __type: 'root',\n        });\n        expect(parsedParagraph).toEqual({\n          \"__alignment\": \"\",\n          __dir: null,\n          __first: textKey,\n          __id: '',\n          __inset: 0,\n          __key: paragraphKey,\n          __last: textKey,\n          __next: null,\n          __parent: 'root',\n          __prev: null,\n          __size: 1,\n          __style: '',\n          __textFormat: 0,\n          __textStyle: '',\n          __type: 'paragraph',\n        });\n        expect(parsedText).toEqual({\n          __detail: 0,\n          __format: 0,\n          __key: textKey,\n          __mode: 0,\n          __next: null,\n          __parent: paragraphKey,\n          __prev: null,\n          __style: '',\n          __text: 'Hello world',\n          __type: 'text',\n        });\n      });\n\n      it('Parses the text content of the editor state', async () => {\n        expect(parsedEditorState.read(() => $getRoot().__cachedText)).toBe(\n          null,\n        );\n        expect(parsedEditorState.read(() => $getRoot().getTextContent())).toBe(\n          'Hello world',\n        );\n      });\n    });\n\n    describe('node selection', () => {\n      beforeEach(async () => {\n        init();\n\n        await update(() => {\n          const paragraph = $createParagraphNode();\n          originalText = $createTextNode('Hello world');\n          const selection = $createNodeSelection();\n          selection.add(originalText.getKey());\n          $setSelection(selection);\n          paragraph.append(originalText);\n          $getRoot().append(paragraph);\n        });\n        const stringifiedEditorState = JSON.stringify(\n          editor.getEditorState().toJSON(),\n        );\n        parsedEditorState = editor.parseEditorState(stringifiedEditorState);\n        parsedEditorState.read(() => {\n          parsedRoot = $getRoot();\n          parsedParagraph = parsedRoot.getFirstChild() as ParagraphNode;\n          paragraphKey = parsedParagraph.getKey();\n          parsedText = parsedParagraph.getFirstChild() as TextNode;\n          textKey = parsedText.getKey();\n        });\n      });\n\n      it('Parses the nodes of a stringified editor state', async () => {\n        expect(parsedRoot).toEqual({\n          __cachedText: null,\n          __dir: null,\n          __first: paragraphKey,\n          __key: 'root',\n          __last: paragraphKey,\n          __next: null,\n          __parent: null,\n          __prev: null,\n          __size: 1,\n          __style: '',\n          __type: 'root',\n        });\n        expect(parsedParagraph).toEqual({\n          \"__alignment\": \"\",\n          __dir: null,\n          __first: textKey,\n          __id: '',\n          __inset: 0,\n          __key: paragraphKey,\n          __last: textKey,\n          __next: null,\n          __parent: 'root',\n          __prev: null,\n          __size: 1,\n          __style: '',\n          __textFormat: 0,\n          __textStyle: '',\n          __type: 'paragraph',\n        });\n        expect(parsedText).toEqual({\n          __detail: 0,\n          __format: 0,\n          __key: textKey,\n          __mode: 0,\n          __next: null,\n          __parent: paragraphKey,\n          __prev: null,\n          __style: '',\n          __text: 'Hello world',\n          __type: 'text',\n        });\n      });\n\n      it('Parses the text content of the editor state', async () => {\n        expect(parsedEditorState.read(() => $getRoot().__cachedText)).toBe(\n          null,\n        );\n        expect(parsedEditorState.read(() => $getRoot().getTextContent())).toBe(\n          'Hello world',\n        );\n      });\n    });\n  });\n\n  describe('$parseSerializedNode()', () => {\n    it('parses serialized nodes', async () => {\n      const expectedTextContent = 'Hello world\\n\\nHello world';\n      let actualTextContent: string;\n      let root: RootNode;\n      await update(() => {\n        root = $getRoot();\n        root.clear();\n        const paragraph = $createParagraphNode();\n        paragraph.append($createTextNode('Hello world'));\n        root.append(paragraph);\n      });\n      const stringifiedEditorState = JSON.stringify(editor.getEditorState());\n      const parsedEditorStateJson = JSON.parse(stringifiedEditorState);\n      const rootJson = parsedEditorStateJson.root;\n      await update(() => {\n        const children = rootJson.children.map($parseSerializedNode);\n        root = $getRoot();\n        root.append(...children);\n        actualTextContent = root.getTextContent();\n      });\n      expect(actualTextContent!).toEqual(expectedTextContent);\n    });\n  });\n\n  describe('Node children', () => {\n    beforeEach(async () => {\n      init();\n\n      await reset();\n    });\n\n    async function reset() {\n      init();\n\n      await update(() => {\n        const root = $getRoot();\n        const paragraph = $createParagraphNode();\n        root.append(paragraph);\n      });\n    }\n\n    it('moves node to different tree branches', async () => {\n      function $createElementNodeWithText(text: string) {\n        const elementNode = $createTestElementNode();\n        const textNode = $createTextNode(text);\n        elementNode.append(textNode);\n\n        return [elementNode, textNode];\n      }\n\n      let paragraphNodeKey: string;\n      let elementNode1Key: string;\n      let textNode1Key: string;\n      let elementNode2Key: string;\n      let textNode2Key: string;\n\n      await update(() => {\n        const paragraph = $getRoot().getFirstChild() as ParagraphNode;\n        paragraphNodeKey = paragraph.getKey();\n\n        const [elementNode1, textNode1] = $createElementNodeWithText('A');\n        elementNode1Key = elementNode1.getKey();\n        textNode1Key = textNode1.getKey();\n\n        const [elementNode2, textNode2] = $createElementNodeWithText('B');\n        elementNode2Key = elementNode2.getKey();\n        textNode2Key = textNode2.getKey();\n\n        paragraph.append(elementNode1, elementNode2);\n      });\n\n      await update(() => {\n        const elementNode1 = $getNodeByKey(elementNode1Key) as ElementNode;\n        const elementNode2 = $getNodeByKey(elementNode2Key) as TextNode;\n        elementNode1.append(elementNode2);\n      });\n      const keys = [\n        paragraphNodeKey!,\n        elementNode1Key!,\n        textNode1Key!,\n        elementNode2Key!,\n        textNode2Key!,\n      ];\n\n      for (let i = 0; i < keys.length; i++) {\n        expect(editor._editorState._nodeMap.has(keys[i])).toBe(true);\n        expect(editor._keyToDOMMap.has(keys[i])).toBe(true);\n      }\n\n      expect(editor._editorState._nodeMap.size).toBe(keys.length + 1); // + root\n      expect(editor._keyToDOMMap.size).toBe(keys.length + 1); // + root\n      expect(container.innerHTML).toBe(\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><div><span data-lexical-text=\"true\">A</span><div><span data-lexical-text=\"true\">B</span></div></div></p></div>',\n      );\n    });\n\n    it('moves node to different tree branches (inverse)', async () => {\n      function $createElementNodeWithText(text: string) {\n        const elementNode = $createTestElementNode();\n        const textNode = $createTextNode(text);\n        elementNode.append(textNode);\n\n        return elementNode;\n      }\n\n      let elementNode1Key: string;\n      let elementNode2Key: string;\n\n      await update(() => {\n        const paragraph = $getRoot().getFirstChild() as ParagraphNode;\n\n        const elementNode1 = $createElementNodeWithText('A');\n        elementNode1Key = elementNode1.getKey();\n\n        const elementNode2 = $createElementNodeWithText('B');\n        elementNode2Key = elementNode2.getKey();\n\n        paragraph.append(elementNode1, elementNode2);\n      });\n\n      await update(() => {\n        const elementNode1 = $getNodeByKey(elementNode1Key) as TextNode;\n        const elementNode2 = $getNodeByKey(elementNode2Key) as ElementNode;\n        elementNode2.append(elementNode1);\n      });\n\n      expect(container.innerHTML).toBe(\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><div><span data-lexical-text=\"true\">B</span><div><span data-lexical-text=\"true\">A</span></div></div></p></div>',\n      );\n    });\n\n    it('moves node to different tree branches (node appended twice in two different branches)', async () => {\n      function $createElementNodeWithText(text: string) {\n        const elementNode = $createTestElementNode();\n        const textNode = $createTextNode(text);\n        elementNode.append(textNode);\n\n        return elementNode;\n      }\n\n      let elementNode1Key: string;\n      let elementNode2Key: string;\n      let elementNode3Key: string;\n\n      await update(() => {\n        const paragraph = $getRoot().getFirstChild() as ParagraphNode;\n\n        const elementNode1 = $createElementNodeWithText('A');\n        elementNode1Key = elementNode1.getKey();\n\n        const elementNode2 = $createElementNodeWithText('B');\n        elementNode2Key = elementNode2.getKey();\n\n        const elementNode3 = $createElementNodeWithText('C');\n        elementNode3Key = elementNode3.getKey();\n\n        paragraph.append(elementNode1, elementNode2, elementNode3);\n      });\n\n      await update(() => {\n        const elementNode1 = $getNodeByKey(elementNode1Key) as ElementNode;\n        const elementNode2 = $getNodeByKey(elementNode2Key) as ElementNode;\n        const elementNode3 = $getNodeByKey(elementNode3Key) as TextNode;\n        elementNode2.append(elementNode3);\n        elementNode1.append(elementNode3);\n      });\n\n      expect(container.innerHTML).toBe(\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><div><span data-lexical-text=\"true\">A</span><div><span data-lexical-text=\"true\">C</span></div></div><div><span data-lexical-text=\"true\">B</span></div></p></div>',\n      );\n    });\n  });\n\n  it('can subscribe and unsubscribe from commands and the callback is fired', () => {\n    init();\n\n    const commandListener = jest.fn();\n    const command = createCommand('TEST_COMMAND');\n    const payload = 'testPayload';\n    const removeCommandListener = editor.registerCommand(\n      command,\n      commandListener,\n      COMMAND_PRIORITY_EDITOR,\n    );\n    editor.dispatchCommand(command, payload);\n    editor.dispatchCommand(command, payload);\n    editor.dispatchCommand(command, payload);\n\n    expect(commandListener).toHaveBeenCalledTimes(3);\n    expect(commandListener).toHaveBeenCalledWith(payload, editor);\n\n    removeCommandListener();\n\n    editor.dispatchCommand(command, payload);\n    editor.dispatchCommand(command, payload);\n    editor.dispatchCommand(command, payload);\n\n    expect(commandListener).toHaveBeenCalledTimes(3);\n    expect(commandListener).toHaveBeenCalledWith(payload, editor);\n  });\n\n  it('removes the command from the command map when no listener are attached', () => {\n    init();\n\n    const commandListener = jest.fn();\n    const commandListenerTwo = jest.fn();\n    const command = createCommand('TEST_COMMAND');\n    const removeCommandListener = editor.registerCommand(\n      command,\n      commandListener,\n      COMMAND_PRIORITY_EDITOR,\n    );\n    const removeCommandListenerTwo = editor.registerCommand(\n      command,\n      commandListenerTwo,\n      COMMAND_PRIORITY_EDITOR,\n    );\n\n    expect(editor._commands).toEqual(\n      new Map([\n        [\n          command,\n          [\n            new Set([commandListener, commandListenerTwo]),\n            new Set(),\n            new Set(),\n            new Set(),\n            new Set(),\n          ],\n        ],\n      ]),\n    );\n\n    removeCommandListener();\n\n    expect(editor._commands).toEqual(\n      new Map([\n        [\n          command,\n          [\n            new Set([commandListenerTwo]),\n            new Set(),\n            new Set(),\n            new Set(),\n            new Set(),\n          ],\n        ],\n      ]),\n    );\n\n    removeCommandListenerTwo();\n\n    expect(editor._commands).toEqual(new Map());\n  });\n\n  it('can register transforms before updates', async () => {\n    init();\n\n    const emptyTransform = () => {\n      return;\n    };\n\n    const removeTextTransform = editor.registerNodeTransform(\n      TextNode,\n      emptyTransform,\n    );\n    const removeParagraphTransform = editor.registerNodeTransform(\n      ParagraphNode,\n      emptyTransform,\n    );\n\n    await editor.update(() => {\n      const root = $getRoot();\n      const paragraph = $createParagraphNode();\n      root.append(paragraph);\n    });\n\n    removeTextTransform();\n    removeParagraphTransform();\n  });\n\n  it('textcontent listener', async () => {\n    init();\n\n    const fn = jest.fn();\n    editor.update(() => {\n      const root = $getRoot();\n      const paragraph = $createParagraphNode();\n      const textNode = $createTextNode('foo');\n      root.append(paragraph);\n      paragraph.append(textNode);\n    });\n    editor.registerTextContentListener((text) => {\n      fn(text);\n    });\n\n    await editor.update(() => {\n      const root = $getRoot();\n      const child = root.getLastDescendant()!;\n      child.insertAfter($createTextNode('bar'));\n    });\n\n    expect(fn).toHaveBeenCalledTimes(1);\n    expect(fn).toHaveBeenCalledWith('foobar');\n\n    await editor.update(() => {\n      const root = $getRoot();\n      const child = root.getLastDescendant()!;\n      child.insertAfter($createLineBreakNode());\n    });\n\n    expect(fn).toHaveBeenCalledTimes(2);\n    expect(fn).toHaveBeenCalledWith('foobar\\n');\n\n    await editor.update(() => {\n      const root = $getRoot();\n      root.clear();\n      const paragraph = $createParagraphNode();\n      const paragraph2 = $createParagraphNode();\n      root.append(paragraph);\n      paragraph.append($createTextNode('bar'));\n      paragraph2.append($createTextNode('yar'));\n      paragraph.insertAfter(paragraph2);\n    });\n\n    expect(fn).toHaveBeenCalledTimes(3);\n    expect(fn).toHaveBeenCalledWith('bar\\n\\nyar');\n\n    await editor.update(() => {\n      const root = $getRoot();\n      const paragraph = $createParagraphNode();\n      const paragraph2 = $createParagraphNode();\n      root.getLastChild()!.insertAfter(paragraph);\n      paragraph.append($createTextNode('bar2'));\n      paragraph2.append($createTextNode('yar2'));\n      paragraph.insertAfter(paragraph2);\n    });\n\n    expect(fn).toHaveBeenCalledTimes(4);\n    expect(fn).toHaveBeenCalledWith('bar\\n\\nyar\\n\\nbar2\\n\\nyar2');\n  });\n\n  it('mutation listener', async () => {\n    init();\n\n    const paragraphNodeMutations = jest.fn();\n    const textNodeMutations = jest.fn();\n    editor.registerMutationListener(ParagraphNode, paragraphNodeMutations, {\n      skipInitialization: false,\n    });\n    editor.registerMutationListener(TextNode, textNodeMutations, {\n      skipInitialization: false,\n    });\n    const paragraphKeys: string[] = [];\n    const textNodeKeys: string[] = [];\n\n    // No await intentional (batch with next)\n    editor.update(() => {\n      const root = $getRoot();\n      const paragraph = $createParagraphNode();\n      const textNode = $createTextNode('foo');\n      root.append(paragraph);\n      paragraph.append(textNode);\n      paragraphKeys.push(paragraph.getKey());\n      textNodeKeys.push(textNode.getKey());\n    });\n\n    await editor.update(() => {\n      const textNode = $getNodeByKey(textNodeKeys[0]) as TextNode;\n      const textNode2 = $createTextNode('bar').toggleFormat('bold');\n      const textNode3 = $createTextNode('xyz').toggleFormat('italic');\n      textNode.insertAfter(textNode2);\n      textNode2.insertAfter(textNode3);\n      textNodeKeys.push(textNode2.getKey());\n      textNodeKeys.push(textNode3.getKey());\n    });\n\n    await editor.update(() => {\n      $getRoot().clear();\n    });\n\n    await editor.update(() => {\n      const root = $getRoot();\n      const paragraph = $createParagraphNode();\n\n      paragraphKeys.push(paragraph.getKey());\n\n      // Created and deleted in the same update (not attached to node)\n      textNodeKeys.push($createTextNode('zzz').getKey());\n      root.append(paragraph);\n    });\n\n    expect(paragraphNodeMutations.mock.calls.length).toBe(3);\n    expect(textNodeMutations.mock.calls.length).toBe(2);\n\n    const [paragraphMutation1, paragraphMutation2, paragraphMutation3] =\n      paragraphNodeMutations.mock.calls;\n    const [textNodeMutation1, textNodeMutation2] = textNodeMutations.mock.calls;\n\n    expect(paragraphMutation1[0].size).toBe(1);\n    expect(paragraphMutation1[0].get(paragraphKeys[0])).toBe('created');\n    expect(paragraphMutation1[0].size).toBe(1);\n    expect(paragraphMutation2[0].get(paragraphKeys[0])).toBe('destroyed');\n    expect(paragraphMutation3[0].size).toBe(1);\n    expect(paragraphMutation3[0].get(paragraphKeys[1])).toBe('created');\n    expect(textNodeMutation1[0].size).toBe(3);\n    expect(textNodeMutation1[0].get(textNodeKeys[0])).toBe('created');\n    expect(textNodeMutation1[0].get(textNodeKeys[1])).toBe('created');\n    expect(textNodeMutation1[0].get(textNodeKeys[2])).toBe('created');\n    expect(textNodeMutation2[0].size).toBe(3);\n    expect(textNodeMutation2[0].get(textNodeKeys[0])).toBe('destroyed');\n    expect(textNodeMutation2[0].get(textNodeKeys[1])).toBe('destroyed');\n    expect(textNodeMutation2[0].get(textNodeKeys[2])).toBe('destroyed');\n  });\n  it('mutation listener on newly initialized editor', async () => {\n    editor = createEditor();\n    const textNodeMutations = jest.fn();\n    editor.registerMutationListener(TextNode, textNodeMutations, {\n      skipInitialization: false,\n    });\n    expect(textNodeMutations.mock.calls.length).toBe(0);\n  });\n  it('mutation listener with setEditorState', async () => {\n    init();\n\n    await editor.update(() => {\n      $getRoot().append($createParagraphNode());\n    });\n\n    const initialEditorState = editor.getEditorState();\n    const textNodeMutations = jest.fn();\n    editor.registerMutationListener(TextNode, textNodeMutations, {\n      skipInitialization: false,\n    });\n    const textNodeKeys: string[] = [];\n\n    await editor.update(() => {\n      const paragraph = $getRoot().getFirstChild() as ParagraphNode;\n      const textNode1 = $createTextNode('foo');\n      paragraph.append(textNode1);\n      textNodeKeys.push(textNode1.getKey());\n    });\n\n    const fooEditorState = editor.getEditorState();\n\n    await editor.setEditorState(initialEditorState);\n    // This line should have no effect on the mutation listeners\n    const parsedFooEditorState = editor.parseEditorState(\n      JSON.stringify(fooEditorState),\n    );\n\n    await editor.update(() => {\n      const paragraph = $getRoot().getFirstChild() as ParagraphNode;\n      const textNode2 = $createTextNode('bar').toggleFormat('bold');\n      const textNode3 = $createTextNode('xyz').toggleFormat('italic');\n      paragraph.append(textNode2, textNode3);\n      textNodeKeys.push(textNode2.getKey(), textNode3.getKey());\n    });\n\n    await editor.setEditorState(parsedFooEditorState);\n\n    expect(textNodeMutations.mock.calls.length).toBe(4);\n\n    const [\n      textNodeMutation1,\n      textNodeMutation2,\n      textNodeMutation3,\n      textNodeMutation4,\n    ] = textNodeMutations.mock.calls;\n\n    expect(textNodeMutation1[0].size).toBe(1);\n    expect(textNodeMutation1[0].get(textNodeKeys[0])).toBe('created');\n    expect(textNodeMutation2[0].size).toBe(1);\n    expect(textNodeMutation2[0].get(textNodeKeys[0])).toBe('destroyed');\n    expect(textNodeMutation3[0].size).toBe(2);\n    expect(textNodeMutation3[0].get(textNodeKeys[1])).toBe('created');\n    expect(textNodeMutation3[0].get(textNodeKeys[2])).toBe('created');\n    expect(textNodeMutation4[0].size).toBe(3); // +1 newly generated key by parseEditorState\n    expect(textNodeMutation4[0].get(textNodeKeys[1])).toBe('destroyed');\n    expect(textNodeMutation4[0].get(textNodeKeys[2])).toBe('destroyed');\n  });\n\n  it('mutation listener set for original node should work with the replaced node', async () => {\n\n    function TestBase() {\n      const edContainer = document.createElement('div');\n      edContainer.contentEditable = 'true';\n\n      editor = useLexicalEditor(edContainer, undefined, [\n        TestTextNode,\n        {\n          replace: TextNode,\n          with: (node: TextNode) => new TestTextNode(node.getTextContent()),\n          withKlass: TestTextNode,\n        },\n      ]);\n\n      return edContainer;\n    }\n\n    setContainerChild(TestBase());\n\n    const textNodeMutations = jest.fn();\n    const textNodeMutationsB = jest.fn();\n    editor.registerMutationListener(TextNode, textNodeMutations, {\n      skipInitialization: false,\n    });\n    const textNodeKeys: string[] = [];\n\n    // No await intentional (batch with next)\n    editor.update(() => {\n      const root = $getRoot();\n      const paragraph = $createParagraphNode();\n      const textNode = $createTextNode('foo');\n      root.append(paragraph);\n      paragraph.append(textNode);\n      textNodeKeys.push(textNode.getKey());\n    });\n\n    await editor.update(() => {\n      const textNode = $getNodeByKey(textNodeKeys[0]) as TextNode;\n      const textNode2 = $createTextNode('bar').toggleFormat('bold');\n      const textNode3 = $createTextNode('xyz').toggleFormat('italic');\n      textNode.insertAfter(textNode2);\n      textNode2.insertAfter(textNode3);\n      textNodeKeys.push(textNode2.getKey());\n      textNodeKeys.push(textNode3.getKey());\n    });\n\n    editor.registerMutationListener(TextNode, textNodeMutationsB, {\n      skipInitialization: false,\n    });\n\n    await editor.update(() => {\n      $getRoot().clear();\n    });\n\n    await editor.update(() => {\n      const root = $getRoot();\n      const paragraph = $createParagraphNode();\n\n      // Created and deleted in the same update (not attached to node)\n      textNodeKeys.push($createTextNode('zzz').getKey());\n      root.append(paragraph);\n    });\n\n    expect(textNodeMutations.mock.calls.length).toBe(2);\n    expect(textNodeMutationsB.mock.calls.length).toBe(2);\n\n    const [textNodeMutation1, textNodeMutation2] = textNodeMutations.mock.calls;\n\n    expect(textNodeMutation1[0].size).toBe(3);\n    expect(textNodeMutation1[0].get(textNodeKeys[0])).toBe('created');\n    expect(textNodeMutation1[0].get(textNodeKeys[1])).toBe('created');\n    expect(textNodeMutation1[0].get(textNodeKeys[2])).toBe('created');\n    expect([...textNodeMutation1[1].updateTags]).toEqual([]);\n    expect(textNodeMutation2[0].size).toBe(3);\n    expect(textNodeMutation2[0].get(textNodeKeys[0])).toBe('destroyed');\n    expect(textNodeMutation2[0].get(textNodeKeys[1])).toBe('destroyed');\n    expect(textNodeMutation2[0].get(textNodeKeys[2])).toBe('destroyed');\n    expect([...textNodeMutation2[1].updateTags]).toEqual([]);\n\n    const [textNodeMutationB1, textNodeMutationB2] =\n      textNodeMutationsB.mock.calls;\n\n    expect(textNodeMutationB1[0].size).toBe(3);\n    expect(textNodeMutationB1[0].get(textNodeKeys[0])).toBe('created');\n    expect(textNodeMutationB1[0].get(textNodeKeys[1])).toBe('created');\n    expect(textNodeMutationB1[0].get(textNodeKeys[2])).toBe('created');\n    expect([...textNodeMutationB1[1].updateTags]).toEqual([\n      'registerMutationListener',\n    ]);\n    expect(textNodeMutationB2[0].size).toBe(3);\n    expect(textNodeMutationB2[0].get(textNodeKeys[0])).toBe('destroyed');\n    expect(textNodeMutationB2[0].get(textNodeKeys[1])).toBe('destroyed');\n    expect(textNodeMutationB2[0].get(textNodeKeys[2])).toBe('destroyed');\n    expect([...textNodeMutationB2[1].updateTags]).toEqual([]);\n  });\n\n  it('mutation listener should work with the replaced node', async () => {\n\n    function TestBase() {\n      const edContainer = document.createElement('div');\n      edContainer.contentEditable = 'true';\n\n      editor = useLexicalEditor(edContainer, undefined, [\n        TestTextNode,\n        {\n          replace: TextNode,\n          with: (node: TextNode) => new TestTextNode(node.getTextContent()),\n          withKlass: TestTextNode,\n        },\n      ]);\n\n      return edContainer;\n    }\n\n    setContainerChild(TestBase());\n\n    const textNodeMutations = jest.fn();\n    const textNodeMutationsB = jest.fn();\n    editor.registerMutationListener(TestTextNode, textNodeMutations, {\n      skipInitialization: false,\n    });\n    const textNodeKeys: string[] = [];\n\n    await editor.update(() => {\n      const root = $getRoot();\n      const paragraph = $createParagraphNode();\n      const textNode = $createTextNode('foo');\n      root.append(paragraph);\n      paragraph.append(textNode);\n      textNodeKeys.push(textNode.getKey());\n    });\n\n    editor.registerMutationListener(TestTextNode, textNodeMutationsB, {\n      skipInitialization: false,\n    });\n\n    expect(textNodeMutations.mock.calls.length).toBe(1);\n\n    const [textNodeMutation1] = textNodeMutations.mock.calls;\n\n    expect(textNodeMutation1[0].size).toBe(1);\n    expect(textNodeMutation1[0].get(textNodeKeys[0])).toBe('created');\n    expect([...textNodeMutation1[1].updateTags]).toEqual([]);\n\n    const [textNodeMutationB1] = textNodeMutationsB.mock.calls;\n\n    expect(textNodeMutationB1[0].size).toBe(1);\n    expect(textNodeMutationB1[0].get(textNodeKeys[0])).toBe('created');\n    expect([...textNodeMutationB1[1].updateTags]).toEqual([\n      'registerMutationListener',\n    ]);\n  });\n\n  it('mutation listeners does not trigger when other node types are mutated', async () => {\n    init();\n\n    const paragraphNodeMutations = jest.fn();\n    const textNodeMutations = jest.fn();\n    editor.registerMutationListener(ParagraphNode, paragraphNodeMutations, {\n      skipInitialization: false,\n    });\n    editor.registerMutationListener(TextNode, textNodeMutations, {\n      skipInitialization: false,\n    });\n\n    await editor.update(() => {\n      $getRoot().append($createParagraphNode());\n    });\n\n    expect(paragraphNodeMutations.mock.calls.length).toBe(1);\n    expect(textNodeMutations.mock.calls.length).toBe(0);\n  });\n\n  it('mutation listeners with normalization', async () => {\n    init();\n\n    const textNodeMutations = jest.fn();\n    editor.registerMutationListener(TextNode, textNodeMutations, {\n      skipInitialization: false,\n    });\n    const textNodeKeys: string[] = [];\n\n    await editor.update(() => {\n      const root = $getRoot();\n      const paragraph = $createParagraphNode();\n      const textNode1 = $createTextNode('foo');\n      const textNode2 = $createTextNode('bar');\n\n      textNodeKeys.push(textNode1.getKey(), textNode2.getKey());\n      root.append(paragraph);\n      paragraph.append(textNode1, textNode2);\n    });\n\n    await editor.update(() => {\n      const paragraph = $getRoot().getFirstChild() as ParagraphNode;\n      const textNode3 = $createTextNode('xyz').toggleFormat('bold');\n      paragraph.append(textNode3);\n      textNodeKeys.push(textNode3.getKey());\n    });\n\n    await editor.update(() => {\n      const textNode3 = $getNodeByKey(textNodeKeys[2]) as TextNode;\n      textNode3.toggleFormat('bold'); // Normalize with foobar\n    });\n\n    expect(textNodeMutations.mock.calls.length).toBe(3);\n\n    const [textNodeMutation1, textNodeMutation2, textNodeMutation3] =\n      textNodeMutations.mock.calls;\n\n    expect(textNodeMutation1[0].size).toBe(1);\n    expect(textNodeMutation1[0].get(textNodeKeys[0])).toBe('created');\n    expect(textNodeMutation2[0].size).toBe(2);\n    expect(textNodeMutation2[0].get(textNodeKeys[2])).toBe('created');\n    expect(textNodeMutation3[0].size).toBe(2);\n    expect(textNodeMutation3[0].get(textNodeKeys[0])).toBe('updated');\n    expect(textNodeMutation3[0].get(textNodeKeys[2])).toBe('destroyed');\n  });\n\n  it('mutation \"update\" listener', async () => {\n    init();\n\n    const paragraphNodeMutations = jest.fn();\n    const textNodeMutations = jest.fn();\n\n    editor.registerMutationListener(ParagraphNode, paragraphNodeMutations, {\n      skipInitialization: false,\n    });\n    editor.registerMutationListener(TextNode, textNodeMutations, {\n      skipInitialization: false,\n    });\n\n    const paragraphNodeKeys: string[] = [];\n    const textNodeKeys: string[] = [];\n\n    await editor.update(() => {\n      const root = $getRoot();\n      const paragraph = $createParagraphNode();\n      const textNode1 = $createTextNode('foo');\n      textNodeKeys.push(textNode1.getKey());\n      paragraphNodeKeys.push(paragraph.getKey());\n      root.append(paragraph);\n      paragraph.append(textNode1);\n    });\n\n    expect(paragraphNodeMutations.mock.calls.length).toBe(1);\n\n    const [paragraphNodeMutation1] = paragraphNodeMutations.mock.calls;\n    expect(textNodeMutations.mock.calls.length).toBe(1);\n\n    const [textNodeMutation1] = textNodeMutations.mock.calls;\n\n    expect(textNodeMutation1[0].size).toBe(1);\n    expect(paragraphNodeMutation1[0].size).toBe(1);\n\n    // Change first text node's content.\n    await editor.update(() => {\n      const textNode1 = $getNodeByKey(textNodeKeys[0]) as TextNode;\n      textNode1.setTextContent('Test'); // Normalize with foobar\n    });\n\n    // Append text node to paragraph.\n    await editor.update(() => {\n      const paragraphNode1 = $getNodeByKey(\n        paragraphNodeKeys[0],\n      ) as ParagraphNode;\n      const textNode1 = $createTextNode('foo');\n      paragraphNode1.append(textNode1);\n    });\n\n    expect(textNodeMutations.mock.calls.length).toBe(3);\n\n    const textNodeMutation2 = textNodeMutations.mock.calls[1];\n\n    // Show TextNode was updated when text content changed.\n    expect(textNodeMutation2[0].get(textNodeKeys[0])).toBe('updated');\n    expect(paragraphNodeMutations.mock.calls.length).toBe(2);\n\n    const paragraphNodeMutation2 = paragraphNodeMutations.mock.calls[1];\n\n    // Show ParagraphNode was updated when new text node was appended.\n    expect(paragraphNodeMutation2[0].get(paragraphNodeKeys[0])).toBe('updated');\n\n    let tableCellKey: string;\n    let tableRowKey: string;\n\n    const tableCellMutations = jest.fn();\n    const tableRowMutations = jest.fn();\n\n    editor.registerMutationListener(TableCellNode, tableCellMutations, {\n      skipInitialization: false,\n    });\n    editor.registerMutationListener(TableRowNode, tableRowMutations, {\n      skipInitialization: false,\n    });\n    // Create Table\n\n    await editor.update(() => {\n      const root = $getRoot();\n      const tableCell = $createTableCellNode(0);\n      const tableRow = $createTableRowNode();\n      const table = $createTableNode();\n\n      tableRow.append(tableCell);\n      table.append(tableRow);\n      root.append(table);\n\n      tableRowKey = tableRow.getKey();\n      tableCellKey = tableCell.getKey();\n    });\n    // Add New Table Cell To Row\n\n    await editor.update(() => {\n      const tableRow = $getNodeByKey(tableRowKey) as TableRowNode;\n      const tableCell = $createTableCellNode(0);\n      tableRow.append(tableCell);\n    });\n\n    // Update Table Cell\n    await editor.update(() => {\n      const tableCell = $getNodeByKey(tableCellKey) as TableCellNode;\n      tableCell.toggleHeaderStyle(1);\n    });\n\n    expect(tableCellMutations.mock.calls.length).toBe(3);\n    const tableCellMutation3 = tableCellMutations.mock.calls[2];\n\n    // Show table cell is updated when header value changes.\n    expect(tableCellMutation3[0].get(tableCellKey!)).toBe('updated');\n    expect(tableRowMutations.mock.calls.length).toBe(2);\n\n    const tableRowMutation2 = tableRowMutations.mock.calls[1];\n\n    // Show row is updated when a new child is added.\n    expect(tableRowMutation2[0].get(tableRowKey!)).toBe('updated');\n  });\n\n  it('editable listener', () => {\n    init();\n\n    const editableFn = jest.fn();\n    editor.registerEditableListener(editableFn);\n\n    expect(editor.isEditable()).toBe(true);\n\n    editor.setEditable(false);\n\n    expect(editor.isEditable()).toBe(false);\n\n    editor.setEditable(true);\n\n    expect(editableFn.mock.calls).toEqual([[false], [true]]);\n  });\n\n  it('does not add new listeners while triggering existing', async () => {\n    const updateListener = jest.fn();\n    const mutationListener = jest.fn();\n    const nodeTransformListener = jest.fn();\n    const textContentListener = jest.fn();\n    const editableListener = jest.fn();\n    const commandListener = jest.fn();\n    const TEST_COMMAND = createCommand('TEST_COMMAND');\n\n    init();\n\n    editor.registerUpdateListener(() => {\n      updateListener();\n\n      editor.registerUpdateListener(() => {\n        updateListener();\n      });\n    });\n\n    editor.registerMutationListener(\n      TextNode,\n      (map) => {\n        mutationListener();\n        editor.registerMutationListener(\n          TextNode,\n          () => {\n            mutationListener();\n          },\n          {skipInitialization: true},\n        );\n      },\n      {skipInitialization: false},\n    );\n\n    editor.registerNodeTransform(ParagraphNode, () => {\n      nodeTransformListener();\n      editor.registerNodeTransform(ParagraphNode, () => {\n        nodeTransformListener();\n      });\n    });\n\n    editor.registerEditableListener(() => {\n      editableListener();\n      editor.registerEditableListener(() => {\n        editableListener();\n      });\n    });\n\n    editor.registerTextContentListener(() => {\n      textContentListener();\n      editor.registerTextContentListener(() => {\n        textContentListener();\n      });\n    });\n\n    editor.registerCommand(\n      TEST_COMMAND,\n      (): boolean => {\n        commandListener();\n        editor.registerCommand(\n          TEST_COMMAND,\n          commandListener,\n          COMMAND_PRIORITY_LOW,\n        );\n        return false;\n      },\n      COMMAND_PRIORITY_LOW,\n    );\n\n    await update(() => {\n      $getRoot().append(\n        $createParagraphNode().append($createTextNode('Hello world')),\n      );\n    });\n\n    editor.dispatchCommand(TEST_COMMAND, false);\n\n    editor.setEditable(false);\n\n    expect(updateListener).toHaveBeenCalledTimes(1);\n    expect(editableListener).toHaveBeenCalledTimes(1);\n    expect(commandListener).toHaveBeenCalledTimes(1);\n    expect(textContentListener).toHaveBeenCalledTimes(1);\n    expect(nodeTransformListener).toHaveBeenCalledTimes(1);\n    expect(mutationListener).toHaveBeenCalledTimes(1);\n  });\n\n  it('calls mutation listener with initial state', async () => {\n    // TODO add tests for node replacement\n    const mutationListenerA = jest.fn();\n    const mutationListenerB = jest.fn();\n    const mutationListenerC = jest.fn();\n    init();\n\n    editor.registerMutationListener(TextNode, mutationListenerA, {\n      skipInitialization: false,\n    });\n    expect(mutationListenerA).toHaveBeenCalledTimes(0);\n\n    await update(() => {\n      $getRoot().append(\n        $createParagraphNode().append($createTextNode('Hello world')),\n      );\n    });\n\n    function asymmetricMatcher<T>(asymmetricMatch: (x: T) => boolean) {\n      return {asymmetricMatch};\n    }\n\n    expect(mutationListenerA).toHaveBeenCalledTimes(1);\n    expect(mutationListenerA).toHaveBeenLastCalledWith(\n      expect.anything(),\n      expect.objectContaining({\n        updateTags: asymmetricMatcher(\n          (s: Set<string>) => !s.has('registerMutationListener'),\n        ),\n      }),\n    );\n    editor.registerMutationListener(TextNode, mutationListenerB, {\n      skipInitialization: false,\n    });\n    editor.registerMutationListener(TextNode, mutationListenerC, {\n      skipInitialization: true,\n    });\n    expect(mutationListenerA).toHaveBeenCalledTimes(1);\n    expect(mutationListenerB).toHaveBeenCalledTimes(1);\n    expect(mutationListenerB).toHaveBeenLastCalledWith(\n      expect.anything(),\n      expect.objectContaining({\n        updateTags: asymmetricMatcher((s: Set<string>) =>\n          s.has('registerMutationListener'),\n        ),\n      }),\n    );\n    expect(mutationListenerC).toHaveBeenCalledTimes(0);\n    await update(() => {\n      $getRoot().append(\n        $createParagraphNode().append($createTextNode('Another update!')),\n      );\n    });\n    expect(mutationListenerA).toHaveBeenCalledTimes(2);\n    expect(mutationListenerB).toHaveBeenCalledTimes(2);\n    expect(mutationListenerC).toHaveBeenCalledTimes(1);\n    [mutationListenerA, mutationListenerB, mutationListenerC].forEach((fn) => {\n      expect(fn).toHaveBeenLastCalledWith(\n        expect.anything(),\n        expect.objectContaining({\n          updateTags: asymmetricMatcher(\n            (s: Set<string>) => !s.has('registerMutationListener'),\n          ),\n        }),\n      );\n    });\n  });\n\n  it('can use discrete for synchronous updates', () => {\n    init();\n    const onUpdate = jest.fn();\n    editor.registerUpdateListener(onUpdate);\n    editor.update(\n      () => {\n        $getRoot().append(\n          $createParagraphNode().append($createTextNode('Sync update')),\n        );\n      },\n      {\n        discrete: true,\n      },\n    );\n\n    const textContent = editor\n      .getEditorState()\n      .read(() => $getRoot().getTextContent());\n    expect(textContent).toBe('Sync update');\n    expect(onUpdate).toHaveBeenCalledTimes(1);\n  });\n\n  it('can use discrete after a non-discrete update to flush the entire queue', () => {\n    const headless = createTestHeadlessEditor();\n    const onUpdate = jest.fn();\n    headless.registerUpdateListener(onUpdate);\n    headless.update(() => {\n      $getRoot().append(\n        $createParagraphNode().append($createTextNode('Async update')),\n      );\n    });\n    headless.update(\n      () => {\n        $getRoot().append(\n          $createParagraphNode().append($createTextNode('Sync update')),\n        );\n      },\n      {\n        discrete: true,\n      },\n    );\n\n    const textContent = headless\n      .getEditorState()\n      .read(() => $getRoot().getTextContent());\n    expect(textContent).toBe('Async update\\n\\nSync update');\n    expect(onUpdate).toHaveBeenCalledTimes(1);\n  });\n\n  it('can use discrete after a non-discrete setEditorState to flush the entire queue', () => {\n    init();\n    editor.update(\n      () => {\n        $getRoot().append(\n          $createParagraphNode().append($createTextNode('Async update')),\n        );\n      },\n      {\n        discrete: true,\n      },\n    );\n\n    const headless = createTestHeadlessEditor(editor.getEditorState());\n    headless.update(\n      () => {\n        $getRoot().append(\n          $createParagraphNode().append($createTextNode('Sync update')),\n        );\n      },\n      {\n        discrete: true,\n      },\n    );\n    const textContent = headless\n      .getEditorState()\n      .read(() => $getRoot().getTextContent());\n    expect(textContent).toBe('Async update\\n\\nSync update');\n  });\n\n  it('can use discrete in a nested update to flush the entire queue', () => {\n    init();\n    const onUpdate = jest.fn();\n    editor.registerUpdateListener(onUpdate);\n    editor.update(() => {\n      $getRoot().append(\n        $createParagraphNode().append($createTextNode('Async update')),\n      );\n      editor.update(\n        () => {\n          $getRoot().append(\n            $createParagraphNode().append($createTextNode('Sync update')),\n          );\n        },\n        {\n          discrete: true,\n        },\n      );\n    });\n\n    const textContent = editor\n      .getEditorState()\n      .read(() => $getRoot().getTextContent());\n    expect(textContent).toBe('Async update\\n\\nSync update');\n    expect(onUpdate).toHaveBeenCalledTimes(1);\n  });\n\n  it('does not include linebreak into inline elements', async () => {\n    init();\n\n    await editor.update(() => {\n      $getRoot().append(\n        $createParagraphNode().append(\n          $createTextNode('Hello'),\n          $createTestInlineElementNode(),\n        ),\n      );\n    });\n\n    expect(container.firstElementChild?.innerHTML).toBe(\n      '<p><span data-lexical-text=\"true\">Hello</span><a></a></p>',\n    );\n  });\n\n  it('reconciles state without root element', () => {\n    editor = createTestEditor({});\n    const state = editor.parseEditorState(\n      `{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello world\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}`,\n    );\n    editor.setEditorState(state);\n    expect(editor._editorState).toBe(state);\n    expect(editor._pendingEditorState).toBe(null);\n  });\n\n  describe('node replacement', () => {\n    it('should work correctly', async () => {\n      const onError = jest.fn();\n\n      const newEditor = createTestEditor({\n        nodes: [\n          TestTextNode,\n          {\n            replace: TextNode,\n            with: (node: TextNode) => new TestTextNode(node.getTextContent()),\n          },\n        ],\n        onError: onError,\n        theme: {\n          text: {\n            bold: 'editor-text-bold',\n            italic: 'editor-text-italic',\n            underline: 'editor-text-underline',\n          },\n        },\n      });\n\n      newEditor.setRootElement(container);\n\n      await newEditor.update(() => {\n        const root = $getRoot();\n        const paragraph = $createParagraphNode();\n        const text = $createTextNode('123');\n        root.append(paragraph);\n        paragraph.append(text);\n        expect(text instanceof TestTextNode).toBe(true);\n        expect(text.getTextContent()).toBe('123');\n      });\n\n      expect(onError).not.toHaveBeenCalled();\n    });\n\n    it('should fail if node keys are re-used', async () => {\n      const onError = jest.fn();\n\n      const newEditor = createTestEditor({\n        nodes: [\n          TestTextNode,\n          {\n            replace: TextNode,\n            with: (node: TextNode) =>\n              new TestTextNode(node.getTextContent(), node.getKey()),\n          },\n        ],\n        onError: onError,\n        theme: {\n          text: {\n            bold: 'editor-text-bold',\n            italic: 'editor-text-italic',\n            underline: 'editor-text-underline',\n          },\n        },\n      });\n\n      newEditor.setRootElement(container);\n\n      await newEditor.update(() => {\n        // this will throw\n        $createTextNode('123');\n        expect(false).toBe('unreachable');\n      });\n\n      newEditor.commitUpdates();\n\n      expect(onError).toHaveBeenCalledWith(\n        expect.objectContaining({\n          message: expect.stringMatching(/TestTextNode.*re-use key.*TextNode/),\n        }),\n      );\n    });\n\n    it('node transform to the nodes specified by \"replace\" should not be applied to the nodes specified by \"with\" when \"withKlass\" is not specified', async () => {\n      const onError = jest.fn();\n\n      const newEditor = createTestEditor({\n        nodes: [\n          TestTextNode,\n          {\n            replace: TextNode,\n            with: (node: TextNode) => new TestTextNode(node.getTextContent()),\n          },\n        ],\n        onError: onError,\n        theme: {\n          text: {\n            bold: 'editor-text-bold',\n            italic: 'editor-text-italic',\n            underline: 'editor-text-underline',\n          },\n        },\n      });\n\n      newEditor.setRootElement(container);\n\n      const mockTransform = jest.fn();\n      const removeTransform = newEditor.registerNodeTransform(\n        TextNode,\n        mockTransform,\n      );\n\n      await newEditor.update(() => {\n        const root = $getRoot();\n        const paragraph = $createParagraphNode();\n        const text = $createTextNode('123');\n        root.append(paragraph);\n        paragraph.append(text);\n        expect(text instanceof TestTextNode).toBe(true);\n        expect(text.getTextContent()).toBe('123');\n      });\n\n      await newEditor.getEditorState().read(() => {\n        expect(mockTransform).toHaveBeenCalledTimes(0);\n      });\n\n      expect(onError).not.toHaveBeenCalled();\n      removeTransform();\n    });\n\n    it('node transform to the nodes specified by \"replace\" should be applied also to the nodes specified by \"with\" when \"withKlass\" is specified', async () => {\n      const onError = jest.fn();\n\n      const newEditor = createTestEditor({\n        nodes: [\n          TestTextNode,\n          {\n            replace: TextNode,\n            with: (node: TextNode) => new TestTextNode(node.getTextContent()),\n            withKlass: TestTextNode,\n          },\n        ],\n        onError: onError,\n        theme: {\n          text: {\n            bold: 'editor-text-bold',\n            italic: 'editor-text-italic',\n            underline: 'editor-text-underline',\n          },\n        },\n      });\n\n      newEditor.setRootElement(container);\n\n      const mockTransform = jest.fn();\n      const removeTransform = newEditor.registerNodeTransform(\n        TextNode,\n        mockTransform,\n      );\n\n      await newEditor.update(() => {\n        const root = $getRoot();\n        const paragraph = $createParagraphNode();\n        const text = $createTextNode('123');\n        root.append(paragraph);\n        paragraph.append(text);\n        expect(text instanceof TestTextNode).toBe(true);\n        expect(text.getTextContent()).toBe('123');\n      });\n\n      await newEditor.getEditorState().read(() => {\n        expect(mockTransform).toHaveBeenCalledTimes(1);\n      });\n\n      expect(onError).not.toHaveBeenCalled();\n      removeTransform();\n    });\n  });\n\n  it('recovers from reconciler failure and trigger proper prev editor state', async () => {\n    const updateListener = jest.fn();\n    const textListener = jest.fn();\n    const onError = jest.fn();\n    const updateError = new Error('Failed updateDOM');\n\n    init(onError);\n\n    editor.registerUpdateListener(updateListener);\n    editor.registerTextContentListener(textListener);\n\n    await update(() => {\n      $getRoot().append(\n        $createParagraphNode().append($createTextNode('Hello')),\n      );\n    });\n\n    // Cause reconciler error in update dom, so that it attempts to fallback by\n    // reseting editor and rerendering whole content\n    jest.spyOn(ParagraphNode.prototype, 'updateDOM').mockImplementation(() => {\n      throw updateError;\n    });\n\n    const editorState = editor.getEditorState();\n\n    editor.registerUpdateListener(updateListener);\n\n    await update(() => {\n      $getRoot().append(\n        $createParagraphNode().append($createTextNode('world')),\n      );\n    });\n\n    expect(onError).toHaveBeenCalledWith(updateError);\n    expect(textListener).toHaveBeenCalledWith('Hello\\n\\nworld');\n    expect(updateListener.mock.lastCall[0].prevEditorState).toBe(editorState);\n  });\n\n  it('should call importDOM methods only once', async () => {\n    jest.spyOn(ParagraphNode, 'importDOM');\n\n    class CustomParagraphNode extends ParagraphNode {\n      static getType() {\n        return 'custom-paragraph';\n      }\n\n      static clone(node: CustomParagraphNode) {\n        return new CustomParagraphNode(node.__key);\n      }\n\n      static importJSON() {\n        return new CustomParagraphNode();\n      }\n\n      exportJSON() {\n        return {...super.exportJSON(), type: 'custom-paragraph'};\n      }\n    }\n\n    createTestEditor({nodes: [CustomParagraphNode]});\n\n    expect(ParagraphNode.importDOM).toHaveBeenCalledTimes(1);\n  });\n\n  it('root element count is always positive', () => {\n    const newEditor1 = createTestEditor();\n    const newEditor2 = createTestEditor();\n\n    const container1 = document.createElement('div');\n    const container2 = document.createElement('div');\n\n    newEditor1.setRootElement(container1);\n    newEditor1.setRootElement(null);\n\n    newEditor1.setRootElement(container1);\n    newEditor2.setRootElement(container2);\n    newEditor1.setRootElement(null);\n    newEditor2.setRootElement(null);\n  });\n\n  describe('html config', () => {\n    it('should override export output function', async () => {\n      const onError = jest.fn();\n\n      const newEditor = createTestEditor({\n        html: {\n          export: new Map([\n            [\n              TextNode,\n              (_, target) => {\n                invariant($isTextNode(target));\n\n                return {\n                  element: target.hasFormat('bold')\n                    ? document.createElement('bor')\n                    : document.createElement('foo'),\n                };\n              },\n            ],\n          ]),\n        },\n        onError: onError,\n      });\n\n      newEditor.setRootElement(container);\n\n      newEditor.update(() => {\n        const root = $getRoot();\n        const paragraph = $createParagraphNode();\n        const text = $createTextNode();\n        root.append(paragraph);\n        paragraph.append(text);\n\n        const selection = $createNodeSelection();\n        selection.add(text.getKey());\n\n        const htmlFoo = $generateHtmlFromNodes(newEditor, selection);\n        expect(htmlFoo).toBe('<foo></foo>');\n\n        text.toggleFormat('bold');\n\n        const htmlBold = $generateHtmlFromNodes(newEditor, selection);\n        expect(htmlBold).toBe('<bor></bor>');\n      });\n\n      expect(onError).not.toHaveBeenCalled();\n    });\n\n    it('should override import conversion function', async () => {\n      const onError = jest.fn();\n\n      const newEditor = createTestEditor({\n        html: {\n          import: {\n            figure: () => ({\n              conversion: () => ({node: $createTextNode('yolo')}),\n              priority: 4,\n            }),\n          },\n        },\n        onError: onError,\n      });\n\n      newEditor.setRootElement(container);\n\n      newEditor.update(() => {\n        const html = '<figure></figure>';\n\n        const parser = new DOMParser();\n        const dom = parser.parseFromString(html, 'text/html');\n        const node = $generateNodesFromDOM(newEditor, dom)[0];\n\n        expect(node).toEqual({\n          __detail: 0,\n          __format: 0,\n          __key: node.getKey(),\n          __mode: 0,\n          __next: null,\n          __parent: null,\n          __prev: null,\n          __style: '',\n          __text: 'yolo',\n          __type: 'text',\n        });\n      });\n\n      expect(onError).not.toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/__tests__/unit/LexicalEditorState.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {\n  $createParagraphNode,\n  $createTextNode,\n  $getEditor,\n  $getRoot,\n  ParagraphNode,\n  TextNode,\n} from 'lexical';\n\nimport {EditorState} from '../../LexicalEditorState';\nimport {$createRootNode, RootNode} from '../../nodes/LexicalRootNode';\nimport {initializeUnitTest} from '../utils';\n\ndescribe('LexicalEditorState tests', () => {\n  initializeUnitTest((testEnv) => {\n    test('constructor', async () => {\n      const root = $createRootNode();\n      const nodeMap = new Map([['root', root]]);\n\n      const editorState = new EditorState(nodeMap);\n      expect(editorState._nodeMap).toBe(nodeMap);\n      expect(editorState._selection).toBe(null);\n    });\n\n    test('read()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const paragraph = $createParagraphNode();\n        const text = $createTextNode('foo');\n        paragraph.append(text);\n        $getRoot().append(paragraph);\n      });\n\n      let root!: RootNode;\n      let paragraph!: ParagraphNode;\n      let text!: TextNode;\n\n      editor.getEditorState().read(() => {\n        root = $getRoot();\n        paragraph = root.getFirstChild()!;\n        text = paragraph.getFirstChild()!;\n      });\n\n      expect(root).toEqual({\n        __cachedText: 'foo',\n        __dir: null,\n        __first: '1',\n        __key: 'root',\n        __last: '1',\n        __next: null,\n        __parent: null,\n        __prev: null,\n        __size: 1,\n        __style: '',\n        __type: 'root',\n      });\n      expect(paragraph).toEqual({\n        \"__alignment\": \"\",\n        __dir: null,\n        __first: '2',\n        __id: '',\n        __inset: 0,\n        __key: '1',\n        __last: '2',\n        __next: null,\n        __parent: 'root',\n        __prev: null,\n        __size: 1,\n        __style: '',\n        __textFormat: 0,\n        __textStyle: '',\n        __type: 'paragraph',\n      });\n      expect(text).toEqual({\n        __detail: 0,\n        __format: 0,\n        __key: '2',\n        __mode: 0,\n        __next: null,\n        __parent: '1',\n        __prev: null,\n        __style: '',\n        __text: 'foo',\n        __type: 'text',\n      });\n      expect(() => editor.getEditorState().read(() => $getEditor())).toThrow(\n        /Unable to find an active editor/,\n      );\n      expect(\n        editor.getEditorState().read(() => $getEditor(), {editor: editor}),\n      ).toBe(editor);\n    });\n\n    test('toJSON()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const paragraph = $createParagraphNode();\n        const text = $createTextNode('Hello world');\n        text.select(6, 11);\n        paragraph.append(text);\n        $getRoot().append(paragraph);\n      });\n\n      expect(JSON.stringify(editor.getEditorState().toJSON())).toEqual(\n        `{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello world\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"root\",\"version\":1}}`,\n      );\n    });\n\n    test('ensure garbage collection works as expected', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const paragraph = $createParagraphNode();\n        const text = $createTextNode('foo');\n        paragraph.append(text);\n        $getRoot().append(paragraph);\n      });\n      // Remove the first node, which should cause a GC for everything\n\n      await editor.update(() => {\n        $getRoot().getFirstChild()!.remove();\n      });\n\n      expect(editor.getEditorState()._nodeMap).toEqual(\n        new Map([\n          [\n            'root',\n            {\n              __cachedText: '',\n              __dir: null,\n              __first: null,\n              __key: 'root',\n              __last: null,\n              __next: null,\n              __parent: null,\n              __prev: null,\n              __size: 0,\n              __style: '',\n              __type: 'root',\n            },\n          ],\n        ]),\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/__tests__/unit/LexicalNode.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {\n  $createRangeSelection,\n  $getRoot,\n  $getSelection,\n  $isDecoratorNode,\n  $isElementNode,\n  $isRangeSelection,\n  $setSelection,\n  createEditor,\n  DecoratorNode,\n  ElementNode,\n  LexicalEditor,\n  NodeKey,\n  ParagraphNode,\n  RangeSelection,\n  SerializedTextNode,\n  TextNode,\n} from 'lexical';\n\nimport {LexicalNode} from '../../LexicalNode';\nimport {$createParagraphNode} from '../../nodes/LexicalParagraphNode';\nimport {$createTextNode} from '../../nodes/LexicalTextNode';\nimport {\n  $createTestInlineElementNode,\n  initializeUnitTest,\n  TestElementNode,\n  TestInlineElementNode,\n} from '../utils';\n\nclass TestNode extends LexicalNode {\n  static getType(): string {\n    return 'test';\n  }\n\n  static clone(node: TestNode) {\n    return new TestNode(node.__key);\n  }\n\n  createDOM() {\n    return document.createElement('div');\n  }\n\n  static importJSON() {\n    return new TestNode();\n  }\n\n  exportJSON() {\n    return {type: 'test', version: 1};\n  }\n}\n\nclass InlineDecoratorNode extends DecoratorNode<string> {\n  static getType(): string {\n    return 'inline-decorator';\n  }\n\n  static clone(): InlineDecoratorNode {\n    return new InlineDecoratorNode();\n  }\n\n  static importJSON() {\n    return new InlineDecoratorNode();\n  }\n\n  exportJSON() {\n    return {type: 'inline-decorator', version: 1};\n  }\n\n  createDOM(): HTMLElement {\n    return document.createElement('span');\n  }\n\n  isInline(): true {\n    return true;\n  }\n\n  isParentRequired(): true {\n    return true;\n  }\n\n  decorate() {\n    return 'inline-decorator';\n  }\n}\n\n// This is a hack to bypass the node type validation on LexicalNode. We never want to create\n// an LexicalNode directly but we're testing the base functionality in this module.\nLexicalNode.getType = function () {\n  return 'node';\n};\n\ndescribe('LexicalNode tests', () => {\n  initializeUnitTest(\n    (testEnv) => {\n      let paragraphNode: ParagraphNode;\n      let textNode: TextNode;\n\n      beforeEach(async () => {\n        const {editor} = testEnv;\n\n        await editor.update(() => {\n          const rootNode = $getRoot();\n          paragraphNode = new ParagraphNode();\n          textNode = new TextNode('foo');\n          paragraphNode.append(textNode);\n          rootNode.append(paragraphNode);\n        });\n      });\n\n      test('LexicalNode.constructor', async () => {\n        const {editor} = testEnv;\n\n        await editor.update(() => {\n          const node = new LexicalNode('__custom_key__');\n          expect(node.__type).toBe('node');\n          expect(node.__key).toBe('__custom_key__');\n          expect(node.__parent).toBe(null);\n        });\n\n        await editor.getEditorState().read(() => {\n          expect(() => new LexicalNode()).toThrow();\n          expect(() => new LexicalNode('__custom_key__')).toThrow();\n        });\n      });\n\n      test('LexicalNode.constructor: type change detected', async () => {\n        const {editor} = testEnv;\n\n        await editor.update(() => {\n          const validNode = new TextNode(textNode.__text, textNode.__key);\n          expect(textNode.getLatest()).toBe(textNode);\n          expect(validNode.getLatest()).toBe(textNode);\n          expect(() => new TestNode(textNode.__key)).toThrow(\n            /TestNode.*re-use key.*TextNode/,\n          );\n        });\n      });\n\n      test('LexicalNode.clone()', async () => {\n        const {editor} = testEnv;\n\n        await editor.update(() => {\n          const node = new LexicalNode('__custom_key__');\n\n          expect(() => LexicalNode.clone(node)).toThrow();\n        });\n      });\n      test('LexicalNode.afterCloneFrom()', () => {\n        class VersionedTextNode extends TextNode {\n          // ['constructor']!: KlassConstructor<typeof VersionedTextNode>;\n          __version = 0;\n          static getType(): 'vtext' {\n            return 'vtext';\n          }\n          static clone(node: VersionedTextNode): VersionedTextNode {\n            return new VersionedTextNode(node.__text, node.__key);\n          }\n          static importJSON(node: SerializedTextNode): VersionedTextNode {\n            throw new Error('Not implemented');\n          }\n          exportJSON(): SerializedTextNode {\n            throw new Error('Not implemented');\n          }\n          afterCloneFrom(node: this): void {\n            super.afterCloneFrom(node);\n            this.__version = node.__version + 1;\n          }\n        }\n        const editor = createEditor({\n          nodes: [VersionedTextNode],\n          onError(err) {\n            throw err;\n          },\n        });\n        let versionedTextNode: VersionedTextNode;\n\n        editor.update(\n          () => {\n            versionedTextNode = new VersionedTextNode('test');\n            $getRoot().append($createParagraphNode().append(versionedTextNode));\n            expect(versionedTextNode.__version).toEqual(0);\n          },\n          {discrete: true},\n        );\n        editor.update(\n          () => {\n            expect(versionedTextNode.getLatest().__version).toEqual(0);\n            expect(\n              versionedTextNode.setTextContent('update').setMode('token')\n                .__version,\n            ).toEqual(1);\n          },\n          {discrete: true},\n        );\n        editor.update(\n          () => {\n            let latest = versionedTextNode.getLatest();\n            expect(versionedTextNode.__version).toEqual(0);\n            expect(versionedTextNode.__mode).toEqual(0);\n            expect(versionedTextNode.getMode()).toEqual('token');\n            expect(latest.__version).toEqual(1);\n            expect(latest.__mode).toEqual(1);\n            latest = latest.setTextContent('another update');\n            expect(latest.__version).toEqual(2);\n            expect(latest.getWritable().__version).toEqual(2);\n            expect(\n              versionedTextNode.getLatest().getWritable().__version,\n            ).toEqual(2);\n            expect(versionedTextNode.getLatest().__version).toEqual(2);\n            expect(versionedTextNode.__mode).toEqual(0);\n            expect(versionedTextNode.getLatest().__mode).toEqual(1);\n            expect(versionedTextNode.getMode()).toEqual('token');\n          },\n          {discrete: true},\n        );\n      });\n\n      test('LexicalNode.getType()', async () => {\n        const {editor} = testEnv;\n\n        await editor.update(() => {\n          const node = new LexicalNode('__custom_key__');\n          expect(node.getType()).toEqual(node.__type);\n        });\n      });\n\n      test('LexicalNode.isAttached()', async () => {\n        const {editor} = testEnv;\n        let node: LexicalNode;\n\n        await editor.update(() => {\n          node = new LexicalNode('__custom_key__');\n        });\n\n        await editor.getEditorState().read(() => {\n          expect(node.isAttached()).toBe(false);\n          expect(textNode.isAttached()).toBe(true);\n          expect(paragraphNode.isAttached()).toBe(true);\n        });\n\n        expect(() => textNode.isAttached()).toThrow();\n      });\n\n      test('LexicalNode.isSelected()', async () => {\n        const {editor} = testEnv;\n        let node: LexicalNode;\n\n        await editor.update(() => {\n          node = new LexicalNode('__custom_key__');\n        });\n\n        await editor.getEditorState().read(() => {\n          expect(node.isSelected()).toBe(false);\n          expect(textNode.isSelected()).toBe(false);\n          expect(paragraphNode.isSelected()).toBe(false);\n        });\n\n        await editor.update(() => {\n          textNode.select(0, 0);\n        });\n\n        await editor.getEditorState().read(() => {\n          expect(textNode.isSelected()).toBe(true);\n        });\n\n        expect(() => textNode.isSelected()).toThrow();\n      });\n\n      test('LexicalNode.isSelected(): selected text node', async () => {\n        const {editor} = testEnv;\n\n        await editor.getEditorState().read(() => {\n          expect(paragraphNode.isSelected()).toBe(false);\n          expect(textNode.isSelected()).toBe(false);\n        });\n\n        await editor.update(() => {\n          textNode.select(0, 0);\n        });\n\n        await editor.getEditorState().read(() => {\n          expect(textNode.isSelected()).toBe(true);\n          expect(paragraphNode.isSelected()).toBe(false);\n        });\n      });\n\n      test('LexicalNode.isSelected(): selected block node range', async () => {\n        const {editor} = testEnv;\n        let newParagraphNode: ParagraphNode;\n        let newTextNode: TextNode;\n\n        await editor.update(() => {\n          expect(paragraphNode.isSelected()).toBe(false);\n          expect(textNode.isSelected()).toBe(false);\n          newParagraphNode = new ParagraphNode();\n          newTextNode = new TextNode('bar');\n          newParagraphNode.append(newTextNode);\n          paragraphNode.insertAfter(newParagraphNode);\n          expect(newParagraphNode.isSelected()).toBe(false);\n          expect(newTextNode.isSelected()).toBe(false);\n        });\n\n        await editor.update(() => {\n          textNode.select(0, 0);\n          const selection = $getSelection();\n\n          expect(selection).not.toBe(null);\n\n          if (!$isRangeSelection(selection)) {\n            return;\n          }\n\n          selection.anchor.type = 'text';\n          selection.anchor.offset = 1;\n          selection.anchor.key = textNode.getKey();\n          selection.focus.type = 'text';\n          selection.focus.offset = 1;\n          selection.focus.key = newTextNode.getKey();\n        });\n\n        await Promise.resolve().then();\n\n        await editor.update(() => {\n          const selection = $getSelection();\n\n          if (!$isRangeSelection(selection)) {\n            return;\n          }\n\n          expect(selection.anchor.key).toBe(textNode.getKey());\n          expect(selection.focus.key).toBe(newTextNode.getKey());\n          expect(paragraphNode.isSelected()).toBe(true);\n          expect(textNode.isSelected()).toBe(true);\n          expect(newParagraphNode.isSelected()).toBe(true);\n          expect(newTextNode.isSelected()).toBe(true);\n        });\n      });\n\n      test('LexicalNode.isSelected(): with custom range selection', async () => {\n        const {editor} = testEnv;\n        let newParagraphNode: ParagraphNode;\n        let newTextNode: TextNode;\n\n        await editor.update(() => {\n          expect(paragraphNode.isSelected()).toBe(false);\n          expect(textNode.isSelected()).toBe(false);\n          newParagraphNode = new ParagraphNode();\n          newTextNode = new TextNode('bar');\n          newParagraphNode.append(newTextNode);\n          paragraphNode.insertAfter(newParagraphNode);\n          expect(newParagraphNode.isSelected()).toBe(false);\n          expect(newTextNode.isSelected()).toBe(false);\n        });\n\n        await editor.update(() => {\n          const rangeSelection = $createRangeSelection();\n\n          rangeSelection.anchor.type = 'text';\n          rangeSelection.anchor.offset = 1;\n          rangeSelection.anchor.key = textNode.getKey();\n          rangeSelection.focus.type = 'text';\n          rangeSelection.focus.offset = 1;\n          rangeSelection.focus.key = newTextNode.getKey();\n\n          expect(paragraphNode.isSelected(rangeSelection)).toBe(true);\n          expect(textNode.isSelected(rangeSelection)).toBe(true);\n          expect(newParagraphNode.isSelected(rangeSelection)).toBe(true);\n          expect(newTextNode.isSelected(rangeSelection)).toBe(true);\n        });\n\n        await Promise.resolve().then();\n      });\n\n      describe('LexicalNode.isSelected(): with inline decorator node', () => {\n        let editor: LexicalEditor;\n        let paragraphNode1: ParagraphNode;\n        let paragraphNode2: ParagraphNode;\n        let paragraphNode3: ParagraphNode;\n        let inlineDecoratorNode: InlineDecoratorNode;\n        let names: Record<NodeKey, string>;\n        beforeEach(() => {\n          editor = testEnv.editor;\n          editor.update(() => {\n            inlineDecoratorNode = new InlineDecoratorNode();\n            paragraphNode1 = $createParagraphNode();\n            paragraphNode2 = $createParagraphNode().append(inlineDecoratorNode);\n            paragraphNode3 = $createParagraphNode();\n            names = {\n              [inlineDecoratorNode.getKey()]: 'd',\n              [paragraphNode1.getKey()]: 'p1',\n              [paragraphNode2.getKey()]: 'p2',\n              [paragraphNode3.getKey()]: 'p3',\n            };\n            $getRoot()\n              .clear()\n              .append(paragraphNode1, paragraphNode2, paragraphNode3);\n          });\n        });\n        const cases: {\n          label: string;\n          isSelected: boolean;\n          update: () => void;\n        }[] = [\n          {\n            isSelected: true,\n            label: 'whole editor',\n            update() {\n              $getRoot().select(0);\n            },\n          },\n          {\n            isSelected: true,\n            label: 'containing paragraph',\n            update() {\n              paragraphNode2.select(0);\n            },\n          },\n          {\n            isSelected: true,\n            label: 'before and containing',\n            update() {\n              paragraphNode2\n                .select(0)\n                .anchor.set(paragraphNode1.getKey(), 0, 'element');\n            },\n          },\n          {\n            isSelected: true,\n            label: 'containing and after',\n            update() {\n              paragraphNode2\n                .select(0)\n                .focus.set(paragraphNode3.getKey(), 0, 'element');\n            },\n          },\n          {\n            isSelected: true,\n            label: 'before and after',\n            update() {\n              paragraphNode1\n                .select(0)\n                .focus.set(paragraphNode3.getKey(), 0, 'element');\n            },\n          },\n          {\n            isSelected: false,\n            label: 'collapsed before',\n            update() {\n              paragraphNode2.select(0, 0);\n            },\n          },\n          {\n            isSelected: false,\n            label: 'in another element',\n            update() {\n              paragraphNode1.select(0);\n            },\n          },\n          {\n            isSelected: false,\n            label: 'before',\n            update() {\n              paragraphNode1\n                .select(0)\n                .focus.set(paragraphNode2.getKey(), 0, 'element');\n            },\n          },\n          {\n            isSelected: false,\n            label: 'collapsed after',\n            update() {\n              paragraphNode2.selectEnd();\n            },\n          },\n          {\n            isSelected: false,\n            label: 'after',\n            update() {\n              paragraphNode3\n                .select(0)\n                .anchor.set(\n                  paragraphNode2.getKey(),\n                  paragraphNode2.getChildrenSize(),\n                  'element',\n                );\n            },\n          },\n        ];\n        for (const {label, isSelected, update} of cases) {\n          test(`${isSelected ? 'is' : \"isn't\"} selected ${label}`, () => {\n            editor.update(update);\n            const $verify = () => {\n              const selection = $getSelection() as RangeSelection;\n              expect($isRangeSelection(selection)).toBe(true);\n              const dbg = [selection.anchor, selection.focus]\n                .map(\n                  (point) =>\n                    `(${names[point.key] || point.key}:${point.offset})`,\n                )\n                .join(' ');\n              const nodes = `[${selection\n                .getNodes()\n                .map((k) => names[k.__key] || k.__key)\n                .join(',')}]`;\n              expect([dbg, nodes, inlineDecoratorNode.isSelected()]).toEqual([\n                dbg,\n                nodes,\n                isSelected,\n              ]);\n            };\n            editor.read($verify);\n            editor.update(() => {\n              const selection = $getSelection();\n              if ($isRangeSelection(selection)) {\n                const backwards = $createRangeSelection();\n                backwards.anchor.set(\n                  selection.focus.key,\n                  selection.focus.offset,\n                  selection.focus.type,\n                );\n                backwards.focus.set(\n                  selection.anchor.key,\n                  selection.anchor.offset,\n                  selection.anchor.type,\n                );\n                $setSelection(backwards);\n              }\n              expect($isRangeSelection(selection)).toBe(true);\n            });\n            editor.read($verify);\n          });\n        }\n      });\n\n      test('LexicalNode.getKey()', async () => {\n        expect(textNode.getKey()).toEqual(textNode.__key);\n      });\n\n      test('LexicalNode.getParent()', async () => {\n        const {editor} = testEnv;\n\n        await editor.update(() => {\n          const node = new LexicalNode();\n          expect(node.getParent()).toBe(null);\n        });\n\n        await editor.getEditorState().read(() => {\n          const rootNode = $getRoot();\n          expect(textNode.getParent()).toBe(paragraphNode);\n          expect(paragraphNode.getParent()).toBe(rootNode);\n        });\n        expect(() => textNode.getParent()).toThrow();\n      });\n\n      test('LexicalNode.getParentOrThrow()', async () => {\n        const {editor} = testEnv;\n\n        await editor.update(() => {\n          const node = new LexicalNode();\n          expect(() => node.getParentOrThrow()).toThrow();\n        });\n\n        await editor.getEditorState().read(() => {\n          const rootNode = $getRoot();\n          expect(textNode.getParent()).toBe(paragraphNode);\n          expect(paragraphNode.getParent()).toBe(rootNode);\n        });\n        expect(() => textNode.getParentOrThrow()).toThrow();\n      });\n\n      test('LexicalNode.getTopLevelElement()', async () => {\n        const {editor} = testEnv;\n\n        await editor.update(() => {\n          const node = new LexicalNode();\n          expect(node.getTopLevelElement()).toBe(null);\n        });\n\n        await editor.getEditorState().read(() => {\n          expect(textNode.getTopLevelElement()).toBe(paragraphNode);\n          expect(paragraphNode.getTopLevelElement()).toBe(paragraphNode);\n        });\n        expect(() => textNode.getTopLevelElement()).toThrow();\n        await editor.update(() => {\n          const node = new InlineDecoratorNode();\n          expect(node.getTopLevelElement()).toBe(null);\n          $getRoot().append(node);\n          expect(node.getTopLevelElement()).toBe(node);\n        });\n        editor.getEditorState().read(() => {\n          const elementNodes: ElementNode[] = [];\n          const decoratorNodes: DecoratorNode<unknown>[] = [];\n          for (const child of $getRoot().getChildren()) {\n            expect(child.getTopLevelElement()).toBe(child);\n            if ($isElementNode(child)) {\n              elementNodes.push(child);\n            } else if ($isDecoratorNode(child)) {\n              decoratorNodes.push(child);\n            } else {\n              throw new Error(\n                'Expecting all children to be ElementNode or DecoratorNode',\n              );\n            }\n          }\n          expect(decoratorNodes).toHaveLength(1);\n          expect(elementNodes).toHaveLength(1);\n        });\n      });\n\n      test('LexicalNode.getTopLevelElementOrThrow()', async () => {\n        const {editor} = testEnv;\n\n        await editor.update(() => {\n          const node = new LexicalNode();\n          expect(() => node.getTopLevelElementOrThrow()).toThrow();\n        });\n\n        await editor.getEditorState().read(() => {\n          expect(textNode.getTopLevelElementOrThrow()).toBe(paragraphNode);\n          expect(paragraphNode.getTopLevelElementOrThrow()).toBe(paragraphNode);\n        });\n        expect(() => textNode.getTopLevelElementOrThrow()).toThrow();\n        await editor.update(() => {\n          const node = new InlineDecoratorNode();\n          expect(() => node.getTopLevelElementOrThrow()).toThrow();\n          $getRoot().append(node);\n          expect(node.getTopLevelElementOrThrow()).toBe(node);\n        });\n      });\n\n      test('LexicalNode.getParents()', async () => {\n        const {editor} = testEnv;\n\n        await editor.update(() => {\n          const node = new LexicalNode();\n          expect(node.getParents()).toEqual([]);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span></p></div>',\n        );\n\n        await editor.getEditorState().read(() => {\n          const rootNode = $getRoot();\n          expect(textNode.getParents()).toEqual([paragraphNode, rootNode]);\n          expect(paragraphNode.getParents()).toEqual([rootNode]);\n        });\n        expect(() => textNode.getParents()).toThrow();\n      });\n\n      test('LexicalNode.getPreviousSibling()', async () => {\n        const {editor} = testEnv;\n        let barTextNode: TextNode;\n\n        await editor.update(() => {\n          barTextNode = new TextNode('bar');\n          barTextNode.toggleUnmergeable();\n          paragraphNode.append(barTextNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span><span data-lexical-text=\"true\">bar</span></p></div>',\n        );\n\n        await editor.getEditorState().read(() => {\n          expect(barTextNode.getPreviousSibling()).toEqual({\n            ...textNode,\n            __next: '3',\n          });\n          expect(textNode.getPreviousSibling()).toEqual(null);\n        });\n        expect(() => textNode.getPreviousSibling()).toThrow();\n      });\n\n      test('LexicalNode.getPreviousSiblings()', async () => {\n        const {editor} = testEnv;\n        let barTextNode: TextNode;\n        let bazTextNode: TextNode;\n\n        await editor.update(() => {\n          barTextNode = new TextNode('bar');\n          barTextNode.toggleUnmergeable();\n          bazTextNode = new TextNode('baz');\n          bazTextNode.toggleUnmergeable();\n          paragraphNode.append(barTextNode, bazTextNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span><span data-lexical-text=\"true\">bar</span><span data-lexical-text=\"true\">baz</span></p></div>',\n        );\n\n        await editor.getEditorState().read(() => {\n          expect(bazTextNode.getPreviousSiblings()).toEqual([\n            {\n              ...textNode,\n              __next: '3',\n            },\n            {\n              ...barTextNode,\n              __prev: '2',\n            },\n          ]);\n          expect(barTextNode.getPreviousSiblings()).toEqual([\n            {\n              ...textNode,\n              __next: '3',\n            },\n          ]);\n          expect(textNode.getPreviousSiblings()).toEqual([]);\n        });\n        expect(() => textNode.getPreviousSiblings()).toThrow();\n      });\n\n      test('LexicalNode.getNextSibling()', async () => {\n        const {editor} = testEnv;\n        let barTextNode: TextNode;\n\n        await editor.update(() => {\n          barTextNode = new TextNode('bar');\n          barTextNode.toggleUnmergeable();\n          paragraphNode.append(barTextNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span><span data-lexical-text=\"true\">bar</span></p></div>',\n        );\n\n        await editor.getEditorState().read(() => {\n          expect(barTextNode.getNextSibling()).toEqual(null);\n          expect(textNode.getNextSibling()).toEqual(barTextNode);\n        });\n        expect(() => textNode.getNextSibling()).toThrow();\n      });\n\n      test('LexicalNode.getNextSiblings()', async () => {\n        const {editor} = testEnv;\n        let barTextNode: TextNode;\n        let bazTextNode: TextNode;\n\n        await editor.update(() => {\n          barTextNode = new TextNode('bar');\n          barTextNode.toggleUnmergeable();\n          bazTextNode = new TextNode('baz');\n          bazTextNode.toggleUnmergeable();\n          paragraphNode.append(barTextNode, bazTextNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span><span data-lexical-text=\"true\">bar</span><span data-lexical-text=\"true\">baz</span></p></div>',\n        );\n\n        await editor.getEditorState().read(() => {\n          expect(bazTextNode.getNextSiblings()).toEqual([]);\n          expect(barTextNode.getNextSiblings()).toEqual([bazTextNode]);\n          expect(textNode.getNextSiblings()).toEqual([\n            barTextNode,\n            bazTextNode,\n          ]);\n        });\n        expect(() => textNode.getNextSiblings()).toThrow();\n      });\n\n      test('LexicalNode.getCommonAncestor()', async () => {\n        const {editor} = testEnv;\n        let quxTextNode: TextNode;\n        let barParagraphNode: ParagraphNode;\n        let barTextNode: TextNode;\n        let bazParagraphNode: ParagraphNode;\n        let bazTextNode: TextNode;\n\n        await editor.update(() => {\n          const rootNode = $getRoot();\n          barParagraphNode = new ParagraphNode();\n          barTextNode = new TextNode('bar');\n          barTextNode.toggleUnmergeable();\n          bazParagraphNode = new ParagraphNode();\n          bazTextNode = new TextNode('baz');\n          bazTextNode.toggleUnmergeable();\n          quxTextNode = new TextNode('qux');\n          quxTextNode.toggleUnmergeable();\n          paragraphNode.append(quxTextNode);\n          expect(barTextNode.getCommonAncestor(bazTextNode)).toBe(null);\n          barParagraphNode.append(barTextNode);\n          bazParagraphNode.append(bazTextNode);\n          expect(barTextNode.getCommonAncestor(bazTextNode)).toBe(null);\n          rootNode.append(barParagraphNode, bazParagraphNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span><span data-lexical-text=\"true\">qux</span></p><p><span data-lexical-text=\"true\">bar</span></p><p><span data-lexical-text=\"true\">baz</span></p></div>',\n        );\n\n        await editor.getEditorState().read(() => {\n          const rootNode = $getRoot();\n          expect(textNode.getCommonAncestor(rootNode)).toBe(rootNode);\n          expect(quxTextNode.getCommonAncestor(rootNode)).toBe(rootNode);\n          expect(barTextNode.getCommonAncestor(rootNode)).toBe(rootNode);\n          expect(bazTextNode.getCommonAncestor(rootNode)).toBe(rootNode);\n          expect(textNode.getCommonAncestor(quxTextNode)).toBe(\n            paragraphNode.getLatest(),\n          );\n          expect(barTextNode.getCommonAncestor(bazTextNode)).toBe(rootNode);\n          expect(barTextNode.getCommonAncestor(bazTextNode)).toBe(rootNode);\n        });\n\n        expect(() => textNode.getCommonAncestor(barTextNode)).toThrow();\n      });\n\n      test('LexicalNode.isBefore()', async () => {\n        const {editor} = testEnv;\n        let barTextNode: TextNode;\n        let bazTextNode: TextNode;\n\n        await editor.update(() => {\n          barTextNode = new TextNode('bar');\n          barTextNode.toggleUnmergeable();\n          bazTextNode = new TextNode('baz');\n          bazTextNode.toggleUnmergeable();\n          paragraphNode.append(barTextNode, bazTextNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span><span data-lexical-text=\"true\">bar</span><span data-lexical-text=\"true\">baz</span></p></div>',\n        );\n\n        await editor.getEditorState().read(() => {\n          expect(textNode.isBefore(textNode)).toBe(false);\n          expect(textNode.isBefore(barTextNode)).toBe(true);\n          expect(textNode.isBefore(bazTextNode)).toBe(true);\n          expect(barTextNode.isBefore(bazTextNode)).toBe(true);\n          expect(bazTextNode.isBefore(barTextNode)).toBe(false);\n          expect(bazTextNode.isBefore(textNode)).toBe(false);\n        });\n        expect(() => textNode.isBefore(barTextNode)).toThrow();\n      });\n\n      test('LexicalNode.isParentOf()', async () => {\n        const {editor} = testEnv;\n\n        await editor.getEditorState().read(() => {\n          const rootNode = $getRoot();\n          expect(rootNode.isParentOf(textNode)).toBe(true);\n          expect(rootNode.isParentOf(paragraphNode)).toBe(true);\n          expect(paragraphNode.isParentOf(textNode)).toBe(true);\n          expect(paragraphNode.isParentOf(rootNode)).toBe(false);\n          expect(textNode.isParentOf(paragraphNode)).toBe(false);\n          expect(textNode.isParentOf(rootNode)).toBe(false);\n        });\n        expect(() => paragraphNode.isParentOf(textNode)).toThrow();\n      });\n\n      test('LexicalNode.getNodesBetween()', async () => {\n        const {editor} = testEnv;\n        let barTextNode: TextNode;\n        let bazTextNode: TextNode;\n        let newParagraphNode: ParagraphNode;\n        let quxTextNode: TextNode;\n\n        await editor.update(() => {\n          const rootNode = $getRoot();\n          barTextNode = new TextNode('bar');\n          barTextNode.toggleUnmergeable();\n          bazTextNode = new TextNode('baz');\n          bazTextNode.toggleUnmergeable();\n          newParagraphNode = new ParagraphNode();\n          quxTextNode = new TextNode('qux');\n          quxTextNode.toggleUnmergeable();\n          rootNode.append(newParagraphNode);\n          paragraphNode.append(barTextNode, bazTextNode);\n          newParagraphNode.append(quxTextNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span><span data-lexical-text=\"true\">bar</span><span data-lexical-text=\"true\">baz</span></p><p><span data-lexical-text=\"true\">qux</span></p></div>',\n        );\n\n        await editor.getEditorState().read(() => {\n          expect(textNode.getNodesBetween(textNode)).toEqual([textNode]);\n          expect(textNode.getNodesBetween(barTextNode)).toEqual([\n            textNode,\n            barTextNode,\n          ]);\n          expect(textNode.getNodesBetween(bazTextNode)).toEqual([\n            textNode,\n            barTextNode,\n            bazTextNode,\n          ]);\n          expect(textNode.getNodesBetween(quxTextNode)).toEqual([\n            textNode,\n            barTextNode,\n            bazTextNode,\n            paragraphNode.getLatest(),\n            newParagraphNode,\n            quxTextNode,\n          ]);\n        });\n        expect(() => textNode.getNodesBetween(bazTextNode)).toThrow();\n      });\n\n      test('LexicalNode.isToken()', async () => {\n        const {editor} = testEnv;\n        let tokenTextNode: TextNode;\n\n        await editor.update(() => {\n          tokenTextNode = new TextNode('token').setMode('token');\n          paragraphNode.append(tokenTextNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span><span data-lexical-text=\"true\">token</span></p></div>',\n        );\n\n        await editor.getEditorState().read(() => {\n          expect(textNode.isToken()).toBe(false);\n          expect(tokenTextNode.isToken()).toBe(true);\n        });\n        expect(() => textNode.isToken()).toThrow();\n      });\n\n      test('LexicalNode.isSegmented()', async () => {\n        const {editor} = testEnv;\n        let segmentedTextNode: TextNode;\n\n        await editor.update(() => {\n          segmentedTextNode = new TextNode('segmented').setMode('segmented');\n          paragraphNode.append(segmentedTextNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span><span data-lexical-text=\"true\">segmented</span></p></div>',\n        );\n\n        await editor.getEditorState().read(() => {\n          expect(textNode.isSegmented()).toBe(false);\n          expect(segmentedTextNode.isSegmented()).toBe(true);\n        });\n        expect(() => textNode.isSegmented()).toThrow();\n      });\n\n      test('LexicalNode.isDirectionless()', async () => {\n        const {editor} = testEnv;\n        let directionlessTextNode: TextNode;\n\n        await editor.update(() => {\n          directionlessTextNode = new TextNode(\n            'directionless',\n          ).toggleDirectionless();\n          directionlessTextNode.toggleUnmergeable();\n          paragraphNode.append(directionlessTextNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span><span data-lexical-text=\"true\">directionless</span></p></div>',\n        );\n\n        await editor.getEditorState().read(() => {\n          expect(textNode.isDirectionless()).toBe(false);\n          expect(directionlessTextNode.isDirectionless()).toBe(true);\n        });\n        expect(() => directionlessTextNode.isDirectionless()).toThrow();\n      });\n\n      test('LexicalNode.getLatest()', async () => {\n        const {editor} = testEnv;\n\n        await editor.getEditorState().read(() => {\n          expect(textNode.getLatest()).toBe(textNode);\n        });\n        expect(() => textNode.getLatest()).toThrow();\n      });\n\n      test('LexicalNode.getLatest(): garbage collected node', async () => {\n        const {editor} = testEnv;\n        let node: LexicalNode;\n        let text: TextNode;\n        let block: TestElementNode;\n\n        await editor.update(() => {\n          node = new LexicalNode();\n          node.getLatest();\n          text = new TextNode('');\n          text.getLatest();\n          block = new TestElementNode();\n          block.getLatest();\n        });\n\n        await editor.update(() => {\n          expect(() => node.getLatest()).toThrow();\n          expect(() => text.getLatest()).toThrow();\n          expect(() => block.getLatest()).toThrow();\n        });\n      });\n\n      test('LexicalNode.getTextContent()', async () => {\n        const {editor} = testEnv;\n\n        await editor.update(() => {\n          const node = new LexicalNode();\n          expect(node.getTextContent()).toBe('');\n        });\n\n        await editor.getEditorState().read(() => {\n          expect(textNode.getTextContent()).toBe('foo');\n        });\n        expect(() => textNode.getTextContent()).toThrow();\n      });\n\n      test('LexicalNode.getTextContentSize()', async () => {\n        const {editor} = testEnv;\n\n        await editor.getEditorState().read(() => {\n          expect(textNode.getTextContentSize()).toBe('foo'.length);\n        });\n        expect(() => textNode.getTextContentSize()).toThrow();\n      });\n\n      test('LexicalNode.createDOM()', async () => {\n        const {editor} = testEnv;\n\n        editor.update(() => {\n          const node = new LexicalNode();\n          expect(() =>\n            node.createDOM(\n              {\n                namespace: '',\n                theme: {},\n              },\n              editor,\n            ),\n          ).toThrow();\n        });\n      });\n\n      test('LexicalNode.updateDOM()', async () => {\n        const {editor} = testEnv;\n\n        await editor.update(() => {\n          const node = new LexicalNode();\n          // @ts-expect-error\n          expect(() => node.updateDOM()).toThrow();\n        });\n      });\n\n      test('LexicalNode.remove()', async () => {\n        const {editor} = testEnv;\n\n        await editor.getEditorState().read(() => {\n          expect(() => textNode.remove()).toThrow();\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span></p></div>',\n        );\n\n        await editor.update(() => {\n          const node = new LexicalNode();\n          node.remove();\n          expect(node.getParent()).toBe(null);\n          textNode.remove();\n          expect(textNode.getParent()).toBe(null);\n          expect(editor._dirtyLeaves.has(textNode.getKey()));\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><br></p></div>',\n        );\n        expect(() => textNode.remove()).toThrow();\n      });\n\n      test('LexicalNode.replace()', async () => {\n        const {editor} = testEnv;\n\n        await editor.getEditorState().read(() => {\n          // @ts-expect-error\n          expect(() => textNode.replace()).toThrow();\n        });\n        expect(() => textNode.remove()).toThrow();\n      });\n\n      test('LexicalNode.replace(): from another parent', async () => {\n        const {editor} = testEnv;\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span></p></div>',\n        );\n        let barTextNode: TextNode;\n\n        await editor.update(() => {\n          const rootNode = $getRoot();\n          const barParagraphNode = new ParagraphNode();\n          barTextNode = new TextNode('bar');\n          barParagraphNode.append(barTextNode);\n          rootNode.append(barParagraphNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span></p><p><span data-lexical-text=\"true\">bar</span></p></div>',\n        );\n\n        await editor.update(() => {\n          textNode.replace(barTextNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">bar</span></p><p><br></p></div>',\n        );\n      });\n\n      test('LexicalNode.replace(): text', async () => {\n        const {editor} = testEnv;\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span></p></div>',\n        );\n\n        await editor.update(() => {\n          const barTextNode = new TextNode('bar');\n          textNode.replace(barTextNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">bar</span></p></div>',\n        );\n      });\n\n      test('LexicalNode.replace(): token', async () => {\n        const {editor} = testEnv;\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span></p></div>',\n        );\n\n        await editor.update(() => {\n          const barTextNode = new TextNode('bar').setMode('token');\n          textNode.replace(barTextNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">bar</span></p></div>',\n        );\n      });\n\n      test('LexicalNode.replace(): segmented', async () => {\n        const {editor} = testEnv;\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span></p></div>',\n        );\n\n        await editor.update(() => {\n          const barTextNode = new TextNode('bar').setMode('segmented');\n          textNode.replace(barTextNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">bar</span></p></div>',\n        );\n      });\n\n      test('LexicalNode.replace(): directionless', async () => {\n        const {editor} = testEnv;\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span></p></div>',\n        );\n\n        await editor.update(() => {\n          const barTextNode = new TextNode(`bar`).toggleDirectionless();\n          textNode.replace(barTextNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">bar</span></p></div>',\n        );\n        // TODO: add text direction validations\n      });\n\n      test('LexicalNode.replace() within canBeEmpty: false', async () => {\n        const {editor} = testEnv;\n\n        jest\n          .spyOn(TestInlineElementNode.prototype, 'canBeEmpty')\n          .mockReturnValue(false);\n\n        await editor.update(() => {\n          textNode = $createTextNode('Hello');\n\n          $getRoot()\n            .clear()\n            .append(\n              $createParagraphNode().append(\n                $createTestInlineElementNode().append(textNode),\n              ),\n            );\n\n          textNode.replace($createTextNode('world'));\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><a><span data-lexical-text=\"true\">world</span></a></p></div>',\n        );\n      });\n\n      test('LexicalNode.insertAfter()', async () => {\n        const {editor} = testEnv;\n\n        await editor.getEditorState().read(() => {\n          // @ts-expect-error\n          expect(() => textNode.insertAfter()).toThrow();\n        });\n        // @ts-expect-error\n        expect(() => textNode.insertAfter()).toThrow();\n      });\n\n      test('LexicalNode.insertAfter(): text', async () => {\n        const {editor} = testEnv;\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span></p></div>',\n        );\n\n        await editor.update(() => {\n          const barTextNode = new TextNode('bar');\n          textNode.insertAfter(barTextNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foobar</span></p></div>',\n        );\n      });\n\n      test('LexicalNode.insertAfter(): token', async () => {\n        const {editor} = testEnv;\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span></p></div>',\n        );\n\n        await editor.update(() => {\n          const barTextNode = new TextNode('bar').setMode('token');\n          textNode.insertAfter(barTextNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span><span data-lexical-text=\"true\">bar</span></p></div>',\n        );\n      });\n\n      test('LexicalNode.insertAfter(): segmented', async () => {\n        const {editor} = testEnv;\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span></p></div>',\n        );\n\n        await editor.update(() => {\n          const barTextNode = new TextNode('bar').setMode('token');\n          textNode.insertAfter(barTextNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span><span data-lexical-text=\"true\">bar</span></p></div>',\n        );\n      });\n\n      test('LexicalNode.insertAfter(): directionless', async () => {\n        const {editor} = testEnv;\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span></p></div>',\n        );\n\n        await editor.update(() => {\n          const barTextNode = new TextNode(`bar`).toggleDirectionless();\n          textNode.insertAfter(barTextNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foobar</span></p></div>',\n        );\n        // TODO: add text direction validations\n      });\n\n      test('LexicalNode.insertAfter() move blocks around', async () => {\n        const {editor} = testEnv;\n        let block1: ParagraphNode,\n          block2: ParagraphNode,\n          block3: ParagraphNode,\n          text1: TextNode,\n          text2: TextNode,\n          text3: TextNode;\n\n        await editor.update(() => {\n          const root = $getRoot();\n          root.clear();\n          block1 = new ParagraphNode();\n          block2 = new ParagraphNode();\n          block3 = new ParagraphNode();\n          text1 = new TextNode('A');\n          text2 = new TextNode('B');\n          text3 = new TextNode('C');\n          block1.append(text1);\n          block2.append(text2);\n          block3.append(text3);\n          root.append(block1, block2, block3);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">A</span></p><p><span data-lexical-text=\"true\">B</span></p><p><span data-lexical-text=\"true\">C</span></p></div>',\n        );\n\n        await editor.update(() => {\n          text1.insertAfter(block2);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">A</span><p><span data-lexical-text=\"true\">B</span></p></p><p><span data-lexical-text=\"true\">C</span></p></div>',\n        );\n      });\n\n      test('LexicalNode.insertAfter() move blocks around #2', async () => {\n        const {editor} = testEnv;\n        let block1: ParagraphNode,\n          block2: ParagraphNode,\n          block3: ParagraphNode,\n          text1: TextNode,\n          text2: TextNode,\n          text3: TextNode;\n\n        await editor.update(() => {\n          const root = $getRoot();\n          root.clear();\n          block1 = new ParagraphNode();\n          block2 = new ParagraphNode();\n          block3 = new ParagraphNode();\n          text1 = new TextNode('A');\n          text1.toggleUnmergeable();\n          text2 = new TextNode('B');\n          text2.toggleUnmergeable();\n          text3 = new TextNode('C');\n          text3.toggleUnmergeable();\n          block1.append(text1);\n          block2.append(text2);\n          block3.append(text3);\n          root.append(block1);\n          root.append(block2);\n          root.append(block3);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">A</span></p><p><span data-lexical-text=\"true\">B</span></p><p><span data-lexical-text=\"true\">C</span></p></div>',\n        );\n\n        await editor.update(() => {\n          text3.insertAfter(text1);\n          text3.insertAfter(text2);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><br></p><p><br></p><p><span data-lexical-text=\"true\">C</span><span data-lexical-text=\"true\">B</span><span data-lexical-text=\"true\">A</span></p></div>',\n        );\n      });\n\n      test('LexicalNode.insertBefore()', async () => {\n        const {editor} = testEnv;\n\n        await editor.getEditorState().read(() => {\n          // @ts-expect-error\n          expect(() => textNode.insertBefore()).toThrow();\n        });\n        // @ts-expect-error\n        expect(() => textNode.insertBefore()).toThrow();\n      });\n\n      test('LexicalNode.insertBefore(): from another parent', async () => {\n        const {editor} = testEnv;\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span></p></div>',\n        );\n        let barTextNode;\n\n        await editor.update(() => {\n          const rootNode = $getRoot();\n          const barParagraphNode = new ParagraphNode();\n          barTextNode = new TextNode('bar');\n          barParagraphNode.append(barTextNode);\n          rootNode.append(barParagraphNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span></p><p><span data-lexical-text=\"true\">bar</span></p></div>',\n        );\n      });\n\n      test('LexicalNode.insertBefore(): text', async () => {\n        const {editor} = testEnv;\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span></p></div>',\n        );\n\n        await editor.update(() => {\n          const barTextNode = new TextNode('bar');\n          textNode.insertBefore(barTextNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">barfoo</span></p></div>',\n        );\n      });\n\n      test('LexicalNode.insertBefore(): token', async () => {\n        const {editor} = testEnv;\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span></p></div>',\n        );\n\n        await editor.update(() => {\n          const barTextNode = new TextNode('bar').setMode('token');\n          textNode.insertBefore(barTextNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">bar</span><span data-lexical-text=\"true\">foo</span></p></div>',\n        );\n      });\n\n      test('LexicalNode.insertBefore(): segmented', async () => {\n        const {editor} = testEnv;\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span></p></div>',\n        );\n\n        await editor.update(() => {\n          const barTextNode = new TextNode('bar').setMode('segmented');\n          textNode.insertBefore(barTextNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">bar</span><span data-lexical-text=\"true\">foo</span></p></div>',\n        );\n      });\n\n      test('LexicalNode.insertBefore(): directionless', async () => {\n        const {editor} = testEnv;\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">foo</span></p></div>',\n        );\n\n        await editor.update(() => {\n          const barTextNode = new TextNode(`bar`).toggleDirectionless();\n          textNode.insertBefore(barTextNode);\n        });\n\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">barfoo</span></p></div>',\n        );\n      });\n\n      test('LexicalNode.selectNext()', async () => {\n        const {editor} = testEnv;\n\n        await editor.update(() => {\n          const barTextNode = new TextNode('bar');\n          textNode.insertAfter(barTextNode);\n\n          expect(barTextNode.isSelected()).not.toBe(true);\n\n          textNode.selectNext();\n\n          expect(barTextNode.isSelected()).toBe(true);\n          // TODO: additional validation of anchorOffset and focusOffset\n        });\n      });\n\n      test('LexicalNode.selectNext(): no next sibling', async () => {\n        const {editor} = testEnv;\n\n        await editor.update(() => {\n          const selection = textNode.selectNext();\n          expect(selection.anchor.getNode()).toBe(paragraphNode);\n          expect(selection.anchor.offset).toBe(1);\n        });\n      });\n\n      test('LexicalNode.selectNext(): non-text node', async () => {\n        const {editor} = testEnv;\n\n        await editor.update(() => {\n          const barNode = new TestNode();\n          textNode.insertAfter(barNode);\n          const selection = textNode.selectNext();\n\n          expect(selection.anchor.getNode()).toBe(textNode.getParent());\n          expect(selection.anchor.offset).toBe(1);\n        });\n      });\n    },\n    {\n      namespace: '',\n      nodes: [LexicalNode, TestNode, InlineDecoratorNode],\n      theme: {},\n    },\n  );\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/__tests__/unit/LexicalNormalization.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {\n  $createParagraphNode,\n  $createTextNode,\n  $getRoot,\n  RangeSelection,\n} from 'lexical';\n\nimport {$normalizeSelection} from '../../LexicalNormalization';\nimport {\n  $createTestDecoratorNode,\n  $createTestElementNode,\n  initializeUnitTest,\n} from '../utils';\n\ndescribe('LexicalNormalization tests', () => {\n  initializeUnitTest((testEnv) => {\n    describe('$normalizeSelection', () => {\n      for (const reversed of [false, true]) {\n        const getAnchor = (x: RangeSelection) =>\n          reversed ? x.focus : x.anchor;\n        const getFocus = (x: RangeSelection) => (reversed ? x.anchor : x.focus);\n        const reversedStr = reversed ? ' (reversed)' : '';\n\n        test(`paragraph to text nodes${reversedStr}`, async () => {\n          const {editor} = testEnv;\n          editor.update(() => {\n            const root = $getRoot();\n            const paragraph = $createParagraphNode();\n            const text1 = $createTextNode('a');\n            const text2 = $createTextNode('b');\n            paragraph.append(text1, text2);\n            root.append(paragraph);\n\n            const selection = paragraph.select();\n            getAnchor(selection).set(paragraph.__key, 0, 'element');\n            getFocus(selection).set(paragraph.__key, 2, 'element');\n\n            const normalizedSelection = $normalizeSelection(selection);\n            expect(getAnchor(normalizedSelection).type).toBe('text');\n            expect(getAnchor(normalizedSelection).getNode().__key).toBe(\n              text1.__key,\n            );\n            expect(getAnchor(normalizedSelection).offset).toBe(0);\n            expect(getFocus(normalizedSelection).type).toBe('text');\n            expect(getFocus(normalizedSelection).getNode().__key).toBe(\n              text2.__key,\n            );\n            expect(getFocus(normalizedSelection).offset).toBe(1);\n          });\n        });\n\n        test(`paragraph to text node + element${reversedStr}`, async () => {\n          const {editor} = testEnv;\n          editor.update(() => {\n            const root = $getRoot();\n            const paragraph = $createParagraphNode();\n            const text1 = $createTextNode('a');\n            const elementNode = $createTestElementNode();\n            paragraph.append(text1, elementNode);\n            root.append(paragraph);\n\n            const selection = paragraph.select();\n            getAnchor(selection).set(paragraph.__key, 0, 'element');\n            getFocus(selection).set(paragraph.__key, 2, 'element');\n\n            const normalizedSelection = $normalizeSelection(selection);\n            expect(getAnchor(normalizedSelection).type).toBe('text');\n            expect(getAnchor(normalizedSelection).getNode().__key).toBe(\n              text1.__key,\n            );\n            expect(getAnchor(normalizedSelection).offset).toBe(0);\n            expect(getFocus(normalizedSelection).type).toBe('element');\n            expect(getFocus(normalizedSelection).getNode().__key).toBe(\n              elementNode.__key,\n            );\n            expect(getFocus(normalizedSelection).offset).toBe(0);\n          });\n        });\n\n        test(`paragraph to text node + decorator${reversedStr}`, async () => {\n          const {editor} = testEnv;\n          editor.update(() => {\n            const root = $getRoot();\n            const paragraph = $createParagraphNode();\n            const text1 = $createTextNode('a');\n            const decoratorNode = $createTestDecoratorNode();\n            paragraph.append(text1, decoratorNode);\n            root.append(paragraph);\n\n            const selection = paragraph.select();\n            getAnchor(selection).set(paragraph.__key, 0, 'element');\n            getFocus(selection).set(paragraph.__key, 2, 'element');\n\n            const normalizedSelection = $normalizeSelection(selection);\n            expect(getAnchor(normalizedSelection).type).toBe('text');\n            expect(getAnchor(normalizedSelection).getNode().__key).toBe(\n              text1.__key,\n            );\n            expect(getAnchor(normalizedSelection).offset).toBe(0);\n            expect(getFocus(normalizedSelection).type).toBe('element');\n            expect(getFocus(normalizedSelection).getNode().__key).toBe(\n              paragraph.__key,\n            );\n            expect(getFocus(normalizedSelection).offset).toBe(2);\n          });\n        });\n\n        test(`text + text node${reversedStr}`, async () => {\n          const {editor} = testEnv;\n          editor.update(() => {\n            const root = $getRoot();\n            const paragraph = $createParagraphNode();\n            const text1 = $createTextNode('a');\n            const text2 = $createTextNode('b');\n            paragraph.append(text1, text2);\n            root.append(paragraph);\n\n            const selection = paragraph.select();\n            getAnchor(selection).set(text1.__key, 0, 'text');\n            getFocus(selection).set(text2.__key, 1, 'text');\n\n            const normalizedSelection = $normalizeSelection(selection);\n            expect(getAnchor(normalizedSelection).type).toBe('text');\n            expect(getAnchor(normalizedSelection).getNode().__key).toBe(\n              text1.__key,\n            );\n            expect(getAnchor(normalizedSelection).offset).toBe(0);\n            expect(getFocus(normalizedSelection).type).toBe('text');\n            expect(getFocus(normalizedSelection).getNode().__key).toBe(\n              text2.__key,\n            );\n            expect(getFocus(normalizedSelection).offset).toBe(1);\n          });\n        });\n\n        test(`paragraph to test element to text + text${reversedStr}`, async () => {\n          const {editor} = testEnv;\n          editor.update(() => {\n            const root = $getRoot();\n            const paragraph = $createParagraphNode();\n            const elementNode = $createTestElementNode();\n            const text1 = $createTextNode('a');\n            const text2 = $createTextNode('b');\n            elementNode.append(text1, text2);\n            paragraph.append(elementNode);\n            root.append(paragraph);\n\n            const selection = paragraph.select();\n            getAnchor(selection).set(text1.__key, 0, 'text');\n            getFocus(selection).set(text2.__key, 1, 'text');\n\n            const normalizedSelection = $normalizeSelection(selection);\n            expect(getAnchor(normalizedSelection).type).toBe('text');\n            expect(getAnchor(normalizedSelection).getNode().__key).toBe(\n              text1.__key,\n            );\n            expect(getAnchor(normalizedSelection).offset).toBe(0);\n            expect(getFocus(normalizedSelection).type).toBe('text');\n            expect(getFocus(normalizedSelection).getNode().__key).toBe(\n              text2.__key,\n            );\n            expect(getFocus(normalizedSelection).offset).toBe(1);\n          });\n        });\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/__tests__/unit/LexicalSelection.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {$createLinkNode, $isLinkNode} from '@lexical/link';\nimport {\n  $createParagraphNode,\n  $createTextNode,\n  $getRoot,\n  $isParagraphNode,\n  $isTextNode,\n  LexicalEditor,\n  RangeSelection,\n} from 'lexical';\n\nimport {initializeUnitTest, invariant} from '../utils';\n\ndescribe('LexicalSelection tests', () => {\n  initializeUnitTest((testEnv) => {\n    describe('Inserting text either side of inline elements', () => {\n      const setup = async (\n        mode: 'start-of-paragraph' | 'mid-paragraph' | 'end-of-paragraph',\n      ) => {\n        const {container, editor} = testEnv;\n\n        if (!container) {\n          throw new Error('Expected container to be truthy');\n        }\n\n        await editor.update(() => {\n          const root = $getRoot();\n          if (root.getFirstChild() !== null) {\n            throw new Error('Expected root to be childless');\n          }\n\n          const paragraph = $createParagraphNode();\n          if (mode === 'start-of-paragraph') {\n            paragraph.append(\n              $createLinkNode('https://', {}).append($createTextNode('a')),\n              $createTextNode('b'),\n            );\n          } else if (mode === 'mid-paragraph') {\n            paragraph.append(\n              $createTextNode('a'),\n              $createLinkNode('https://', {}).append($createTextNode('b')),\n              $createTextNode('c'),\n            );\n          } else {\n            paragraph.append(\n              $createTextNode('a'),\n              $createLinkNode('https://', {}).append($createTextNode('b')),\n            );\n          }\n\n          root.append(paragraph);\n        });\n\n        const expectation =\n          mode === 'start-of-paragraph'\n            ? '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><a href=\"https://\"><span data-lexical-text=\"true\">a</span></a><span data-lexical-text=\"true\">b</span></p></div>'\n            : mode === 'mid-paragraph'\n            ? '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">a</span><a href=\"https://\"><span data-lexical-text=\"true\">b</span></a><span data-lexical-text=\"true\">c</span></p></div>'\n            : '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">a</span><a href=\"https://\"><span data-lexical-text=\"true\">b</span></a></p></div>';\n\n        expect(container.innerHTML).toBe(expectation);\n\n        return {container, editor};\n      };\n\n      const $insertTextOrNodes = (\n        selection: RangeSelection,\n        method: 'insertText' | 'insertNodes',\n      ) => {\n        if (method === 'insertText') {\n          // Insert text (mirroring what LexicalClipboard does when pasting\n          // inline plain text)\n          selection.insertText('x');\n        } else {\n          // Insert a paragraph bearing a single text node (mirroring what\n          // LexicalClipboard does when pasting inline rich text)\n          selection.insertNodes([\n            $createParagraphNode().append($createTextNode('x')),\n          ]);\n        }\n      };\n\n      describe('Inserting text before inline elements', () => {\n        describe('Start-of-paragraph inline elements', () => {\n          const insertText = async ({\n            container,\n            editor,\n            method,\n          }: {\n            container: HTMLDivElement;\n            editor: LexicalEditor;\n            method: 'insertText' | 'insertNodes';\n          }) => {\n            await editor.update(() => {\n              const paragraph = $getRoot().getFirstChildOrThrow();\n              invariant($isParagraphNode(paragraph));\n              const linkNode = paragraph.getFirstChildOrThrow();\n              invariant($isLinkNode(linkNode));\n\n              // Place the cursor at the start of the link node\n              // For review: is there a way to select \"outside\" of the link\n              // node?\n              const selection = linkNode.select(0, 0);\n              $insertTextOrNodes(selection, method);\n            });\n\n            expect(container.innerHTML).toBe(\n              '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">x</span><a href=\"https://\"><span data-lexical-text=\"true\">a</span></a><span data-lexical-text=\"true\">b</span></p></div>',\n            );\n          };\n\n          test('Can insert text before a start-of-paragraph inline element, using insertText', async () => {\n            const {container, editor} = await setup('start-of-paragraph');\n\n            await insertText({container, editor, method: 'insertText'});\n          });\n\n          // TODO: https://github.com/facebook/lexical/issues/4295\n          // test('Can insert text before a start-of-paragraph inline element, using insertNodes', async () => {\n          //   const {container, editor} = await setup('start-of-paragraph');\n\n          //   await insertText({container, editor, method: 'insertNodes'});\n          // });\n        });\n\n        describe('Mid-paragraph inline elements', () => {\n          const insertText = async ({\n            container,\n            editor,\n            method,\n          }: {\n            container: HTMLDivElement;\n            editor: LexicalEditor;\n            method: 'insertText' | 'insertNodes';\n          }) => {\n            await editor.update(() => {\n              const paragraph = $getRoot().getFirstChildOrThrow();\n              invariant($isParagraphNode(paragraph));\n              const textNode = paragraph.getFirstChildOrThrow();\n              invariant($isTextNode(textNode));\n\n              // Place the cursor between the link and the first text node by\n              // selecting the end of the text node\n              const selection = textNode.select(1, 1);\n              $insertTextOrNodes(selection, method);\n            });\n\n            expect(container.innerHTML).toBe(\n              '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">ax</span><a href=\"https://\"><span data-lexical-text=\"true\">b</span></a><span data-lexical-text=\"true\">c</span></p></div>',\n            );\n          };\n\n          test('Can insert text before a mid-paragraph inline element, using insertText', async () => {\n            const {container, editor} = await setup('mid-paragraph');\n\n            await insertText({container, editor, method: 'insertText'});\n          });\n\n          test('Can insert text before a mid-paragraph inline element, using insertNodes', async () => {\n            const {container, editor} = await setup('mid-paragraph');\n\n            await insertText({container, editor, method: 'insertNodes'});\n          });\n        });\n\n        describe('End-of-paragraph inline elements', () => {\n          const insertText = async ({\n            container,\n            editor,\n            method,\n          }: {\n            container: HTMLDivElement;\n            editor: LexicalEditor;\n            method: 'insertText' | 'insertNodes';\n          }) => {\n            await editor.update(() => {\n              const paragraph = $getRoot().getFirstChildOrThrow();\n              invariant($isParagraphNode(paragraph));\n              const textNode = paragraph.getFirstChildOrThrow();\n              invariant($isTextNode(textNode));\n\n              // Place the cursor before the link element by selecting the end\n              // of the text node\n              const selection = textNode.select(1, 1);\n              $insertTextOrNodes(selection, method);\n            });\n\n            expect(container.innerHTML).toBe(\n              '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">ax</span><a href=\"https://\"><span data-lexical-text=\"true\">b</span></a></p></div>',\n            );\n          };\n\n          test('Can insert text before an end-of-paragraph inline element, using insertText', async () => {\n            const {container, editor} = await setup('end-of-paragraph');\n\n            await insertText({container, editor, method: 'insertText'});\n          });\n\n          test('Can insert text before an end-of-paragraph inline element, using insertNodes', async () => {\n            const {container, editor} = await setup('end-of-paragraph');\n\n            await insertText({container, editor, method: 'insertNodes'});\n          });\n        });\n      });\n\n      describe('Inserting text after inline elements', () => {\n        describe('Start-of-paragraph inline elements', () => {\n          const insertText = async ({\n            container,\n            editor,\n            method,\n          }: {\n            container: HTMLDivElement;\n            editor: LexicalEditor;\n            method: 'insertText' | 'insertNodes';\n          }) => {\n            await editor.update(() => {\n              const paragraph = $getRoot().getFirstChildOrThrow();\n              invariant($isParagraphNode(paragraph));\n              const textNode = paragraph.getLastChildOrThrow();\n              invariant($isTextNode(textNode));\n\n              // Place the cursor between the link and the last text node by\n              // selecting the start of the text node\n              const selection = textNode.select(0, 0);\n              $insertTextOrNodes(selection, method);\n            });\n\n            expect(container.innerHTML).toBe(\n              '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><a href=\"https://\"><span data-lexical-text=\"true\">a</span></a><span data-lexical-text=\"true\">xb</span></p></div>',\n            );\n          };\n\n          test('Can insert text after a start-of-paragraph inline element, using insertText', async () => {\n            const {container, editor} = await setup('start-of-paragraph');\n\n            await insertText({container, editor, method: 'insertText'});\n          });\n\n          // TODO: https://github.com/facebook/lexical/issues/4295\n          // test('Can insert text after a start-of-paragraph inline element, using insertNodes', async () => {\n          //   const {container, editor} = await setup('start-of-paragraph');\n\n          //   await insertText({container, editor, method: 'insertNodes'});\n          // });\n        });\n\n        describe('Mid-paragraph inline elements', () => {\n          const insertText = async ({\n            container,\n            editor,\n            method,\n          }: {\n            container: HTMLDivElement;\n            editor: LexicalEditor;\n            method: 'insertText' | 'insertNodes';\n          }) => {\n            await editor.update(() => {\n              const paragraph = $getRoot().getFirstChildOrThrow();\n              invariant($isParagraphNode(paragraph));\n              const textNode = paragraph.getLastChildOrThrow();\n              invariant($isTextNode(textNode));\n\n              // Place the cursor between the link and the last text node by\n              // selecting the start of the text node\n              const selection = textNode.select(0, 0);\n              $insertTextOrNodes(selection, method);\n            });\n\n            expect(container.innerHTML).toBe(\n              '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">a</span><a href=\"https://\"><span data-lexical-text=\"true\">b</span></a><span data-lexical-text=\"true\">xc</span></p></div>',\n            );\n          };\n\n          test('Can insert text after a mid-paragraph inline element, using insertText', async () => {\n            const {container, editor} = await setup('mid-paragraph');\n\n            await insertText({container, editor, method: 'insertText'});\n          });\n\n          // TODO: https://github.com/facebook/lexical/issues/4295\n          // test('Can insert text after a mid-paragraph inline element, using insertNodes', async () => {\n          //   const {container, editor} = await setup('mid-paragraph');\n\n          //   await insertText({container, editor, method: 'insertNodes'});\n          // });\n        });\n\n        describe('End-of-paragraph inline elements', () => {\n          const insertText = async ({\n            container,\n            editor,\n            method,\n          }: {\n            container: HTMLDivElement;\n            editor: LexicalEditor;\n            method: 'insertText' | 'insertNodes';\n          }) => {\n            await editor.update(() => {\n              const paragraph = $getRoot().getFirstChildOrThrow();\n              invariant($isParagraphNode(paragraph));\n              const linkNode = paragraph.getLastChildOrThrow();\n              invariant($isLinkNode(linkNode));\n\n              // Place the cursor at the end of the link element\n              // For review: not sure if there's a better way to select\n              // \"outside\" of the link element.\n              const selection = linkNode.select(1, 1);\n              $insertTextOrNodes(selection, method);\n            });\n\n            expect(container.innerHTML).toBe(\n              '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><span data-lexical-text=\"true\">a</span><a href=\"https://\"><span data-lexical-text=\"true\">b</span></a><span data-lexical-text=\"true\">x</span></p></div>',\n            );\n          };\n\n          test('Can insert text after an end-of-paragraph inline element, using insertText', async () => {\n            const {container, editor} = await setup('end-of-paragraph');\n\n            await insertText({container, editor, method: 'insertText'});\n          });\n\n          // TODO: https://github.com/facebook/lexical/issues/4295\n          // test('Can insert text after an end-of-paragraph inline element, using insertNodes', async () => {\n          //   const {container, editor} = await setup('end-of-paragraph');\n\n          //   await insertText({container, editor, method: 'insertNodes'});\n          // });\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/__tests__/unit/LexicalSerialization.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {$createLinkNode} from '@lexical/link';\nimport {$createListItemNode, $createListNode} from '@lexical/list';\nimport {$createTableNodeWithDimensions} from '@lexical/table';\nimport {$createParagraphNode, $createTextNode, $getRoot} from 'lexical';\n\nimport {initializeUnitTest} from '../utils';\nimport {$createHeadingNode} from \"@lexical/rich-text/LexicalHeadingNode\";\nimport {$createQuoteNode} from \"@lexical/rich-text/LexicalQuoteNode\";\n\nfunction $createEditorContent() {\n  const root = $getRoot();\n  if (root.getFirstChild() === null) {\n    const heading = $createHeadingNode('h1');\n    heading.append($createTextNode('Welcome to the playground'));\n    root.append(heading);\n    const quote = $createQuoteNode();\n    quote.append(\n      $createTextNode(\n        `In case you were wondering what the black box at the bottom is – it's the debug view, showing the current state of the editor. ` +\n          `You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting.`,\n      ),\n    );\n    root.append(quote);\n    const paragraph = $createParagraphNode();\n    paragraph.append(\n      $createTextNode('The playground is a demo environment built with '),\n      $createTextNode('@lexical/react').toggleFormat('code'),\n      $createTextNode('.'),\n      $createTextNode(' Try typing in '),\n      $createTextNode('some text').toggleFormat('bold'),\n      $createTextNode(' with '),\n      $createTextNode('different').toggleFormat('italic'),\n      $createTextNode(' formats.'),\n    );\n    root.append(paragraph);\n    const paragraph2 = $createParagraphNode();\n    paragraph2.append(\n      $createTextNode(\n        'Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!',\n      ),\n    );\n    root.append(paragraph2);\n    const paragraph3 = $createParagraphNode();\n    paragraph3.append(\n      $createTextNode(`If you'd like to find out more about Lexical, you can:`),\n    );\n    root.append(paragraph3);\n    const list = $createListNode('bullet');\n    list.append(\n      $createListItemNode().append(\n        $createTextNode(`Visit the `),\n        $createLinkNode('https://lexical.dev/').append(\n          $createTextNode('Lexical website'),\n        ),\n        $createTextNode(` for documentation and more information.`),\n      ),\n      $createListItemNode().append(\n        $createTextNode(`Check out the code on our `),\n        $createLinkNode('https://github.com/facebook/lexical').append(\n          $createTextNode('GitHub repository'),\n        ),\n        $createTextNode(`.`),\n      ),\n      $createListItemNode().append(\n        $createTextNode(`Playground code can be found `),\n        $createLinkNode(\n          'https://github.com/facebook/lexical/tree/main/packages/lexical-playground',\n        ).append($createTextNode('here')),\n        $createTextNode(`.`),\n      ),\n      $createListItemNode().append(\n        $createTextNode(`Join our `),\n        $createLinkNode('https://discord.com/invite/KmG4wQnnD9').append(\n          $createTextNode('Discord Server'),\n        ),\n        $createTextNode(` and chat with the team.`),\n      ),\n    );\n    root.append(list);\n    const paragraph4 = $createParagraphNode();\n    paragraph4.append(\n      $createTextNode(\n        `Lastly, we're constantly adding cool new features to this playground. So make sure you check back here when you next get a chance :).`,\n      ),\n    );\n    root.append(paragraph4);\n    const table = $createTableNodeWithDimensions(5, 5, true);\n    root.append(table);\n  }\n}\n\ndescribe('LexicalSerialization tests', () => {\n  initializeUnitTest((testEnv) => {\n    test('serializes and deserializes from JSON', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        $createEditorContent();\n      });\n\n      const stringifiedEditorState = JSON.stringify(editor.getEditorState());\n      const expectedStringifiedEditorState = `{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Welcome to the playground\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"heading\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"tag\":\"h1\"},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"In case you were wondering what the black box at the bottom is – it's the debug view, showing the current state of the editor. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting.\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"quote\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"The playground is a demo environment built with \",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":16,\"mode\":\"normal\",\"style\":\"\",\"text\":\"@lexical/react\",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\". Try typing in \",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":1,\"mode\":\"normal\",\"style\":\"\",\"text\":\"some text\",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\" with \",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":2,\"mode\":\"normal\",\"style\":\"\",\"text\":\"different\",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\" formats.\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"If you'd like to find out more about Lexical, you can:\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"},{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Visit the \",\"type\":\"text\",\"version\":1},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Lexical website\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"link\",\"version\":1,\"rel\":null,\"target\":null,\"title\":null,\"url\":\"https://lexical.dev/\"},{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\" for documentation and more information.\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"listitem\",\"version\":1,\"value\":1},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Check out the code on our \",\"type\":\"text\",\"version\":1},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"GitHub repository\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"link\",\"version\":1,\"rel\":null,\"target\":null,\"title\":null,\"url\":\"https://github.com/facebook/lexical\"},{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\".\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"listitem\",\"version\":1,\"value\":2},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Playground code can be found \",\"type\":\"text\",\"version\":1},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"here\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"link\",\"version\":1,\"rel\":null,\"target\":null,\"title\":null,\"url\":\"https://github.com/facebook/lexical/tree/main/packages/lexical-playground\"},{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\".\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"listitem\",\"version\":1,\"value\":3},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Join our \",\"type\":\"text\",\"version\":1},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Discord Server\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"link\",\"version\":1,\"rel\":null,\"target\":null,\"title\":null,\"url\":\"https://discord.com/invite/KmG4wQnnD9\"},{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\" and chat with the team.\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"listitem\",\"version\":1,\"value\":4}],\"direction\":null,\"type\":\"list\",\"version\":1,\"listType\":\"bullet\",\"start\":1,\"tag\":\"ul\",\"id\":\"\"},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Lastly, we're constantly adding cool new features to this playground. So make sure you check back here when you next get a chance :).\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"},{\"children\":[{\"children\":[{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":3,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":1,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":1,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":1,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":1,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"}],\"direction\":null,\"type\":\"tablerow\",\"version\":1,\"styles\":{},\"height\":0},{\"children\":[{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":2,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"}],\"direction\":null,\"type\":\"tablerow\",\"version\":1,\"styles\":{},\"height\":0},{\"children\":[{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":2,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"}],\"direction\":null,\"type\":\"tablerow\",\"version\":1,\"styles\":{},\"height\":0},{\"children\":[{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":2,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"}],\"direction\":null,\"type\":\"tablerow\",\"version\":1,\"styles\":{},\"height\":0},{\"children\":[{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":2,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"}],\"direction\":null,\"type\":\"tablerow\",\"version\":1,\"styles\":{},\"height\":0}],\"direction\":null,\"type\":\"table\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"colWidths\":[],\"styles\":{}}],\"direction\":null,\"type\":\"root\",\"version\":1}}`;\n\n      expect(stringifiedEditorState).toBe(expectedStringifiedEditorState);\n\n      const editorState = editor.parseEditorState(stringifiedEditorState);\n\n      const otherStringifiedEditorState = JSON.stringify(editorState);\n\n      expect(otherStringifiedEditorState).toBe(\n        `{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Welcome to the playground\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"heading\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"tag\":\"h1\"},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"In case you were wondering what the black box at the bottom is – it's the debug view, showing the current state of the editor. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting.\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"quote\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"The playground is a demo environment built with \",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":16,\"mode\":\"normal\",\"style\":\"\",\"text\":\"@lexical/react\",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\". Try typing in \",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":1,\"mode\":\"normal\",\"style\":\"\",\"text\":\"some text\",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\" with \",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":2,\"mode\":\"normal\",\"style\":\"\",\"text\":\"different\",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\" formats.\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"If you'd like to find out more about Lexical, you can:\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"},{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Visit the \",\"type\":\"text\",\"version\":1},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Lexical website\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"link\",\"version\":1,\"rel\":null,\"target\":null,\"title\":null,\"url\":\"https://lexical.dev/\"},{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\" for documentation and more information.\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"listitem\",\"version\":1,\"value\":1},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Check out the code on our \",\"type\":\"text\",\"version\":1},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"GitHub repository\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"link\",\"version\":1,\"rel\":null,\"target\":null,\"title\":null,\"url\":\"https://github.com/facebook/lexical\"},{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\".\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"listitem\",\"version\":1,\"value\":2},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Playground code can be found \",\"type\":\"text\",\"version\":1},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"here\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"link\",\"version\":1,\"rel\":null,\"target\":null,\"title\":null,\"url\":\"https://github.com/facebook/lexical/tree/main/packages/lexical-playground\"},{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\".\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"listitem\",\"version\":1,\"value\":3},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Join our \",\"type\":\"text\",\"version\":1},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Discord Server\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"link\",\"version\":1,\"rel\":null,\"target\":null,\"title\":null,\"url\":\"https://discord.com/invite/KmG4wQnnD9\"},{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\" and chat with the team.\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"listitem\",\"version\":1,\"value\":4}],\"direction\":null,\"type\":\"list\",\"version\":1,\"listType\":\"bullet\",\"start\":1,\"tag\":\"ul\",\"id\":\"\"},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Lastly, we're constantly adding cool new features to this playground. So make sure you check back here when you next get a chance :).\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"},{\"children\":[{\"children\":[{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":3,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":1,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":1,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":1,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":1,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"}],\"direction\":null,\"type\":\"tablerow\",\"version\":1,\"styles\":{},\"height\":0},{\"children\":[{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":2,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"}],\"direction\":null,\"type\":\"tablerow\",\"version\":1,\"styles\":{},\"height\":0},{\"children\":[{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":2,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"}],\"direction\":null,\"type\":\"tablerow\",\"version\":1,\"styles\":{},\"height\":0},{\"children\":[{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":2,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"}],\"direction\":null,\"type\":\"tablerow\",\"version\":1,\"styles\":{},\"height\":0},{\"children\":[{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":2,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[],\"direction\":null,\"type\":\"paragraph\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":0,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"}],\"direction\":null,\"type\":\"tablerow\",\"version\":1,\"styles\":{},\"height\":0}],\"direction\":null,\"type\":\"table\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"colWidths\":[],\"styles\":{}}],\"direction\":null,\"type\":\"root\",\"version\":1}}`,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/__tests__/unit/LexicalUtils.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {\n  $getNodeByKey,\n  $getRoot,\n  $isTokenOrSegmented,\n  $nodesOfType,\n  emptyFunction,\n  generateRandomKey,\n  getCachedTypeToNodeMap,\n  getTextDirection,\n  isArray,\n  isSelectionWithinEditor,\n  resetRandomKey,\n  scheduleMicroTask,\n} from '../../LexicalUtils';\nimport {\n  $createParagraphNode,\n  ParagraphNode,\n} from '../../nodes/LexicalParagraphNode';\nimport {$createTextNode, TextNode} from '../../nodes/LexicalTextNode';\nimport {initializeUnitTest} from '../utils';\n\ndescribe('LexicalUtils tests', () => {\n  initializeUnitTest((testEnv) => {\n    test('scheduleMicroTask(): native', async () => {\n      jest.resetModules();\n\n      let flag = false;\n\n      scheduleMicroTask(() => {\n        flag = true;\n      });\n\n      expect(flag).toBe(false);\n\n      await null;\n\n      expect(flag).toBe(true);\n    });\n\n    test('scheduleMicroTask(): promise', async () => {\n      jest.resetModules();\n      const nativeQueueMicrotask = window.queueMicrotask;\n      const fn = jest.fn();\n      try {\n        // @ts-ignore\n        window.queueMicrotask = undefined;\n        scheduleMicroTask(fn);\n      } finally {\n        // Reset it before yielding control\n        window.queueMicrotask = nativeQueueMicrotask;\n      }\n\n      expect(fn).toHaveBeenCalledTimes(0);\n\n      await null;\n\n      expect(fn).toHaveBeenCalledTimes(1);\n    });\n\n    test('emptyFunction()', () => {\n      expect(emptyFunction).toBeInstanceOf(Function);\n      expect(emptyFunction.length).toBe(0);\n      expect(emptyFunction()).toBe(undefined);\n    });\n\n    test('resetRandomKey()', () => {\n      resetRandomKey();\n      const key1 = generateRandomKey();\n      resetRandomKey();\n      const key2 = generateRandomKey();\n      expect(typeof key1).toBe('string');\n      expect(typeof key2).toBe('string');\n      expect(key1).not.toBe('');\n      expect(key2).not.toBe('');\n      expect(key1).toEqual(key2);\n    });\n\n    test('generateRandomKey()', () => {\n      const key1 = generateRandomKey();\n      const key2 = generateRandomKey();\n      expect(typeof key1).toBe('string');\n      expect(typeof key2).toBe('string');\n      expect(key1).not.toBe('');\n      expect(key2).not.toBe('');\n      expect(key1).not.toEqual(key2);\n    });\n\n    test('isArray()', () => {\n      expect(isArray).toBeInstanceOf(Function);\n      expect(isArray).toBe(Array.isArray);\n    });\n\n    test('isSelectionWithinEditor()', async () => {\n      const {editor} = testEnv;\n      let textNode: TextNode;\n\n      await editor.update(() => {\n        const root = $getRoot();\n        const paragraph = $createParagraphNode();\n        textNode = $createTextNode('foo');\n        paragraph.append(textNode);\n        root.append(paragraph);\n      });\n\n      await editor.update(() => {\n        const domSelection = window.getSelection()!;\n\n        expect(\n          isSelectionWithinEditor(\n            editor,\n            domSelection.anchorNode,\n            domSelection.focusNode,\n          ),\n        ).toBe(false);\n\n        textNode.select(0, 0);\n      });\n\n      await editor.update(() => {\n        const domSelection = window.getSelection()!;\n\n        expect(\n          isSelectionWithinEditor(\n            editor,\n            domSelection.anchorNode,\n            domSelection.focusNode,\n          ),\n        ).toBe(true);\n      });\n    });\n\n    test('getTextDirection()', () => {\n      expect(getTextDirection('')).toBe(null);\n      expect(getTextDirection(' ')).toBe(null);\n      expect(getTextDirection('0')).toBe(null);\n      expect(getTextDirection('A')).toBe('ltr');\n      expect(getTextDirection('Z')).toBe('ltr');\n      expect(getTextDirection('a')).toBe('ltr');\n      expect(getTextDirection('z')).toBe('ltr');\n      expect(getTextDirection('\\u00C0')).toBe('ltr');\n      expect(getTextDirection('\\u00D6')).toBe('ltr');\n      expect(getTextDirection('\\u00D8')).toBe('ltr');\n      expect(getTextDirection('\\u00F6')).toBe('ltr');\n      expect(getTextDirection('\\u00F8')).toBe('ltr');\n      expect(getTextDirection('\\u02B8')).toBe('ltr');\n      expect(getTextDirection('\\u0300')).toBe('ltr');\n      expect(getTextDirection('\\u0590')).toBe('ltr');\n      expect(getTextDirection('\\u0800')).toBe('ltr');\n      expect(getTextDirection('\\u1FFF')).toBe('ltr');\n      expect(getTextDirection('\\u200E')).toBe('ltr');\n      expect(getTextDirection('\\u2C00')).toBe('ltr');\n      expect(getTextDirection('\\uFB1C')).toBe('ltr');\n      expect(getTextDirection('\\uFE00')).toBe('ltr');\n      expect(getTextDirection('\\uFE6F')).toBe('ltr');\n      expect(getTextDirection('\\uFEFD')).toBe('ltr');\n      expect(getTextDirection('\\uFFFF')).toBe('ltr');\n      expect(getTextDirection(`\\u0591`)).toBe('rtl');\n      expect(getTextDirection(`\\u07FF`)).toBe('rtl');\n      expect(getTextDirection(`\\uFB1D`)).toBe('rtl');\n      expect(getTextDirection(`\\uFDFD`)).toBe('rtl');\n      expect(getTextDirection(`\\uFE70`)).toBe('rtl');\n      expect(getTextDirection(`\\uFEFC`)).toBe('rtl');\n    });\n\n    test('isTokenOrSegmented()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const node = $createTextNode('foo');\n        expect($isTokenOrSegmented(node)).toBe(false);\n\n        const tokenNode = $createTextNode().setMode('token');\n        expect($isTokenOrSegmented(tokenNode)).toBe(true);\n\n        const segmentedNode = $createTextNode('foo').setMode('segmented');\n        expect($isTokenOrSegmented(segmentedNode)).toBe(true);\n      });\n    });\n\n    test('$getNodeByKey', async () => {\n      const {editor} = testEnv;\n      let paragraphNode: ParagraphNode;\n      let textNode: TextNode;\n\n      await editor.update(() => {\n        const rootNode = $getRoot();\n        paragraphNode = new ParagraphNode();\n        textNode = new TextNode('foo');\n        paragraphNode.append(textNode);\n        rootNode.append(paragraphNode);\n      });\n\n      await editor.getEditorState().read(() => {\n        expect($getNodeByKey('1')).toBe(paragraphNode);\n        expect($getNodeByKey('2')).toBe(textNode);\n        expect($getNodeByKey('3')).toBe(null);\n      });\n\n      // @ts-expect-error\n      expect(() => $getNodeByKey()).toThrow();\n    });\n\n    test('$nodesOfType', async () => {\n      const {editor} = testEnv;\n      const paragraphKeys: string[] = [];\n\n      const $paragraphKeys = () =>\n        $nodesOfType(ParagraphNode).map((node) => node.getKey());\n\n      await editor.update(() => {\n        const root = $getRoot();\n        const paragraph1 = $createParagraphNode();\n        const paragraph2 = $createParagraphNode();\n        $createParagraphNode();\n        root.append(paragraph1, paragraph2);\n        paragraphKeys.push(paragraph1.getKey(), paragraph2.getKey());\n        const currentParagraphKeys = $paragraphKeys();\n        expect(currentParagraphKeys).toHaveLength(paragraphKeys.length);\n        expect(currentParagraphKeys).toEqual(\n          expect.arrayContaining(paragraphKeys),\n        );\n      });\n      editor.getEditorState().read(() => {\n        const currentParagraphKeys = $paragraphKeys();\n        expect(currentParagraphKeys).toHaveLength(paragraphKeys.length);\n        expect(currentParagraphKeys).toEqual(\n          expect.arrayContaining(paragraphKeys),\n        );\n      });\n    });\n\n    test('getCachedTypeToNodeMap', async () => {\n      const {editor} = testEnv;\n      const paragraphKeys: string[] = [];\n\n      const initialTypeToNodeMap = getCachedTypeToNodeMap(\n        editor.getEditorState(),\n      );\n      expect(getCachedTypeToNodeMap(editor.getEditorState())).toBe(\n        initialTypeToNodeMap,\n      );\n      expect([...initialTypeToNodeMap.keys()]).toEqual(['root']);\n      expect(initialTypeToNodeMap.get('root')).toMatchObject({size: 1});\n\n      editor.update(\n        () => {\n          const root = $getRoot();\n          const paragraph1 = $createParagraphNode().append(\n            $createTextNode('a'),\n          );\n          const paragraph2 = $createParagraphNode().append(\n            $createTextNode('b'),\n          );\n          // these will be garbage collected and not in the readonly map\n          $createParagraphNode().append($createTextNode('c'));\n          root.append(paragraph1, paragraph2);\n          paragraphKeys.push(paragraph1.getKey(), paragraph2.getKey());\n        },\n        {discrete: true},\n      );\n\n      const typeToNodeMap = getCachedTypeToNodeMap(editor.getEditorState());\n      // verify that the initial cache was not used\n      expect(typeToNodeMap).not.toBe(initialTypeToNodeMap);\n      // verify that the cache is used for subsequent calls\n      expect(getCachedTypeToNodeMap(editor.getEditorState())).toBe(\n        typeToNodeMap,\n      );\n      expect(typeToNodeMap.size).toEqual(3);\n      expect([...typeToNodeMap.keys()]).toEqual(\n        expect.arrayContaining(['root', 'paragraph', 'text']),\n      );\n      const paragraphMap = typeToNodeMap.get('paragraph')!;\n      expect(paragraphMap.size).toEqual(paragraphKeys.length);\n      expect([...paragraphMap.keys()]).toEqual(\n        expect.arrayContaining(paragraphKeys),\n      );\n      const textMap = typeToNodeMap.get('text')!;\n      expect(textMap.size).toEqual(2);\n      expect(\n        [...textMap.values()].map((node) => (node as TextNode).__text),\n      ).toEqual(expect.arrayContaining(['a', 'b']));\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {createHeadlessEditor} from '@lexical/headless';\nimport {AutoLinkNode, LinkNode} from '@lexical/link';\nimport {ListItemNode, ListNode} from '@lexical/list';\n\nimport {TableCellNode, TableNode, TableRowNode} from '@lexical/table';\n\nimport {\n  $getSelection,\n  $isRangeSelection,\n  createEditor,\n  DecoratorNode,\n  EditorState,\n  EditorThemeClasses,\n  ElementNode,\n  Klass,\n  LexicalEditor,\n  LexicalNode,\n  RangeSelection,\n  SerializedElementNode,\n  SerializedLexicalNode,\n  SerializedTextNode,\n  TextNode,\n} from 'lexical';\n\nimport {CreateEditorArgs, HTMLConfig, LexicalNodeReplacement,} from '../../LexicalEditor';\nimport {resetRandomKey} from '../../LexicalUtils';\nimport {HeadingNode} from \"@lexical/rich-text/LexicalHeadingNode\";\nimport {QuoteNode} from \"@lexical/rich-text/LexicalQuoteNode\";\nimport {DetailsNode} from \"@lexical/rich-text/LexicalDetailsNode\";\nimport {EditorUiContext} from \"../../../../ui/framework/core\";\nimport {EditorUIManager} from \"../../../../ui/framework/manager\";\nimport {ImageNode} from \"@lexical/rich-text/LexicalImageNode\";\nimport {MediaNode} from \"@lexical/rich-text/LexicalMediaNode\";\n\ntype TestEnv = {\n  readonly container: HTMLDivElement;\n  readonly editor: LexicalEditor;\n  readonly outerHTML: string;\n  readonly innerHTML: string;\n};\n\n/**\n * @deprecated - Consider using `createTestContext` instead within the test case.\n */\nexport function initializeUnitTest(\n  runTests: (testEnv: TestEnv) => void,\n  editorConfig: CreateEditorArgs = {namespace: 'test', theme: {}},\n) {\n  const testEnv = {\n    _container: null as HTMLDivElement | null,\n    _editor: null as LexicalEditor | null,\n    get container() {\n      if (!this._container) {\n        throw new Error('testEnv.container not initialized.');\n      }\n      return this._container;\n    },\n    set container(container) {\n      this._container = container;\n    },\n    get editor() {\n      if (!this._editor) {\n        throw new Error('testEnv.editor not initialized.');\n      }\n      return this._editor;\n    },\n    set editor(editor) {\n      this._editor = editor;\n    },\n    get innerHTML() {\n      return (this.container.firstChild as HTMLElement).innerHTML;\n    },\n    get outerHTML() {\n      return this.container.innerHTML;\n    },\n    reset() {\n      this._container = null;\n      this._editor = null;\n    },\n  };\n\n  beforeEach(async () => {\n    resetRandomKey();\n\n    testEnv.container = document.createElement('div');\n    document.body.appendChild(testEnv.container);\n\n    const editorEl = document.createElement('div');\n    editorEl.setAttribute('contenteditable', 'true');\n    testEnv.container.append(editorEl);\n\n    const lexicalEditor = createTestEditor(editorConfig);\n    lexicalEditor.setRootElement(editorEl);\n    testEnv.editor = lexicalEditor;\n  });\n\n  afterEach(() => {\n    document.body.removeChild(testEnv.container);\n    testEnv.reset();\n  });\n\n  runTests(testEnv);\n}\n\nexport function initializeClipboard() {\n  Object.defineProperty(window, 'DragEvent', {\n    value: class DragEvent {},\n  });\n  Object.defineProperty(window, 'ClipboardEvent', {\n    value: class ClipboardEvent {},\n  });\n}\n\nexport type SerializedTestElementNode = SerializedElementNode;\n\nexport class TestElementNode extends ElementNode {\n  static getType(): string {\n    return 'test_block';\n  }\n\n  static clone(node: TestElementNode) {\n    return new TestElementNode(node.__key);\n  }\n\n  static importJSON(\n    serializedNode: SerializedTestElementNode,\n  ): TestInlineElementNode {\n    const node = $createTestInlineElementNode();\n    node.setDirection(serializedNode.direction);\n    return node;\n  }\n\n  exportJSON(): SerializedTestElementNode {\n    return {\n      ...super.exportJSON(),\n      type: 'test_block',\n      version: 1,\n    };\n  }\n\n  createDOM() {\n    return document.createElement('div');\n  }\n\n  updateDOM() {\n    return false;\n  }\n}\n\nexport function $createTestElementNode(): TestElementNode {\n  return new TestElementNode();\n}\n\ntype SerializedTestTextNode = SerializedTextNode;\n\nexport class TestTextNode extends TextNode {\n  static getType() {\n    return 'test_text';\n  }\n\n  static clone(node: TestTextNode): TestTextNode {\n    return new TestTextNode(node.__text, node.__key);\n  }\n\n  static importJSON(serializedNode: SerializedTestTextNode): TestTextNode {\n    return new TestTextNode(serializedNode.text);\n  }\n\n  exportJSON(): SerializedTestTextNode {\n    return {\n      ...super.exportJSON(),\n      type: 'test_text',\n      version: 1,\n    };\n  }\n}\n\nexport type SerializedTestInlineElementNode = SerializedElementNode;\n\nexport class TestInlineElementNode extends ElementNode {\n  static getType(): string {\n    return 'test_inline_block';\n  }\n\n  static clone(node: TestInlineElementNode) {\n    return new TestInlineElementNode(node.__key);\n  }\n\n  static importJSON(\n    serializedNode: SerializedTestInlineElementNode,\n  ): TestInlineElementNode {\n    const node = $createTestInlineElementNode();\n    node.setDirection(serializedNode.direction);\n    return node;\n  }\n\n  exportJSON(): SerializedTestInlineElementNode {\n    return {\n      ...super.exportJSON(),\n      type: 'test_inline_block',\n      version: 1,\n    };\n  }\n\n  createDOM() {\n    return document.createElement('a');\n  }\n\n  updateDOM() {\n    return false;\n  }\n\n  isInline() {\n    return true;\n  }\n}\n\nexport function $createTestInlineElementNode(): TestInlineElementNode {\n  return new TestInlineElementNode();\n}\n\nexport type SerializedTestShadowRootNode = SerializedElementNode;\n\nexport class TestShadowRootNode extends ElementNode {\n  static getType(): string {\n    return 'test_shadow_root';\n  }\n\n  static clone(node: TestShadowRootNode) {\n    return new TestElementNode(node.__key);\n  }\n\n  static importJSON(\n    serializedNode: SerializedTestShadowRootNode,\n  ): TestShadowRootNode {\n    const node = $createTestShadowRootNode();\n    node.setDirection(serializedNode.direction);\n    return node;\n  }\n\n  exportJSON(): SerializedTestShadowRootNode {\n    return {\n      ...super.exportJSON(),\n      type: 'test_block',\n      version: 1,\n    };\n  }\n\n  createDOM() {\n    return document.createElement('div');\n  }\n\n  updateDOM() {\n    return false;\n  }\n\n  isShadowRoot() {\n    return true;\n  }\n}\n\nexport function $createTestShadowRootNode(): TestShadowRootNode {\n  return new TestShadowRootNode();\n}\n\nexport type SerializedTestSegmentedNode = SerializedTextNode;\n\nexport class TestSegmentedNode extends TextNode {\n  static getType(): string {\n    return 'test_segmented';\n  }\n\n  static clone(node: TestSegmentedNode): TestSegmentedNode {\n    return new TestSegmentedNode(node.__text, node.__key);\n  }\n\n  static importJSON(\n    serializedNode: SerializedTestSegmentedNode,\n  ): TestSegmentedNode {\n    const node = $createTestSegmentedNode(serializedNode.text);\n    node.setFormat(serializedNode.format);\n    node.setDetail(serializedNode.detail);\n    node.setMode(serializedNode.mode);\n    node.setStyle(serializedNode.style);\n    return node;\n  }\n\n  exportJSON(): SerializedTestSegmentedNode {\n    return {\n      ...super.exportJSON(),\n      type: 'test_segmented',\n      version: 1,\n    };\n  }\n}\n\nexport function $createTestSegmentedNode(text: string): TestSegmentedNode {\n  return new TestSegmentedNode(text).setMode('segmented');\n}\n\nexport type SerializedTestExcludeFromCopyElementNode = SerializedElementNode;\n\nexport class TestExcludeFromCopyElementNode extends ElementNode {\n  static getType(): string {\n    return 'test_exclude_from_copy_block';\n  }\n\n  static clone(node: TestExcludeFromCopyElementNode) {\n    return new TestExcludeFromCopyElementNode(node.__key);\n  }\n\n  static importJSON(\n    serializedNode: SerializedTestExcludeFromCopyElementNode,\n  ): TestExcludeFromCopyElementNode {\n    const node = $createTestExcludeFromCopyElementNode();\n    node.setDirection(serializedNode.direction);\n    return node;\n  }\n\n  exportJSON(): SerializedTestExcludeFromCopyElementNode {\n    return {\n      ...super.exportJSON(),\n      type: 'test_exclude_from_copy_block',\n      version: 1,\n    };\n  }\n\n  createDOM() {\n    return document.createElement('div');\n  }\n\n  updateDOM() {\n    return false;\n  }\n\n  excludeFromCopy() {\n    return true;\n  }\n}\n\nexport function $createTestExcludeFromCopyElementNode(): TestExcludeFromCopyElementNode {\n  return new TestExcludeFromCopyElementNode();\n}\n\nexport type SerializedTestDecoratorNode = SerializedLexicalNode;\n\nexport class TestDecoratorNode extends DecoratorNode<HTMLElement> {\n  static getType(): string {\n    return 'test_decorator';\n  }\n\n  static clone(node: TestDecoratorNode) {\n    return new TestDecoratorNode(node.__key);\n  }\n\n  static importJSON(\n    serializedNode: SerializedTestDecoratorNode,\n  ): TestDecoratorNode {\n    return $createTestDecoratorNode();\n  }\n\n  exportJSON(): SerializedTestDecoratorNode {\n    return {\n      ...super.exportJSON(),\n      type: 'test_decorator',\n      version: 1,\n    };\n  }\n\n  static importDOM() {\n    return {\n      'test-decorator': (domNode: HTMLElement) => {\n        return {\n          conversion: () => ({node: $createTestDecoratorNode()}),\n        };\n      },\n    };\n  }\n\n  exportDOM() {\n    return {\n      element: document.createElement('test-decorator'),\n    };\n  }\n\n  getTextContent() {\n    return 'Hello world';\n  }\n\n  createDOM() {\n    return document.createElement('span');\n  }\n\n  updateDOM() {\n    return false;\n  }\n\n  decorate() {\n    const decorator = document.createElement('span');\n    decorator.textContent = 'Hello world';\n    return decorator;\n  }\n}\n\nexport function $createTestDecoratorNode(): TestDecoratorNode {\n  return new TestDecoratorNode();\n}\n\nconst DEFAULT_NODES: NonNullable<ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement>> = [\n  HeadingNode,\n  ListNode,\n  ListItemNode,\n  QuoteNode,\n  TableNode,\n  TableCellNode,\n  TableRowNode,\n  AutoLinkNode,\n  LinkNode,\n  DetailsNode,\n  TestElementNode,\n  TestSegmentedNode,\n  TestExcludeFromCopyElementNode,\n  TestDecoratorNode,\n  TestInlineElementNode,\n  TestShadowRootNode,\n  TestTextNode,\n];\n\nexport function createTestEditor(\n  config: {\n    namespace?: string;\n    editorState?: EditorState;\n    theme?: EditorThemeClasses;\n    parentEditor?: LexicalEditor;\n    nodes?: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement>;\n    onError?: (error: Error) => void;\n    disableEvents?: boolean;\n    readOnly?: boolean;\n    html?: HTMLConfig;\n  } = {},\n): LexicalEditor {\n  const customNodes = config.nodes || [];\n  const editor = createEditor({\n    namespace: config.namespace,\n    onError: (e) => {\n      throw e;\n    },\n    ...config,\n    nodes: DEFAULT_NODES.concat(customNodes),\n  });\n\n  return editor;\n}\n\nexport function createTestHeadlessEditor(\n  editorState?: EditorState,\n): LexicalEditor {\n  return createHeadlessEditor({\n    editorState,\n    onError: (error) => {\n      throw error;\n    },\n  });\n}\n\nexport function createTestContext(): EditorUiContext {\n\n  const container = document.createElement('div');\n  document.body.appendChild(container);\n\n  const scrollWrap = document.createElement('div');\n  const editorDOM = document.createElement('div');\n  editorDOM.setAttribute('contenteditable', 'true');\n\n  scrollWrap.append(editorDOM);\n  container.append(scrollWrap);\n\n  const editor = createTestEditor({\n    namespace: 'testing',\n    theme: {},\n    nodes: [\n        ImageNode,\n        MediaNode,\n    ]\n  });\n\n  editor.setRootElement(editorDOM);\n\n  const context = {\n    containerDOM: container,\n    editor: editor,\n    editorDOM: editorDOM,\n    error(text: string | Error): void {\n    },\n    manager: new EditorUIManager(),\n    options: {},\n    scrollDOM: scrollWrap,\n    translate(text: string): string {\n      return text;\n    }\n  };\n\n  context.manager.setContext(context);\n\n  return context;\n}\n\nexport function destroyFromContext(context: EditorUiContext) {\n  context.containerDOM.remove();\n}\n\nexport function $assertRangeSelection(selection: unknown): RangeSelection {\n  if (!$isRangeSelection(selection)) {\n    throw new Error(`Expected RangeSelection, got ${selection}`);\n  }\n  return selection;\n}\n\nexport function invariant(cond?: boolean, message?: string): asserts cond {\n  if (cond) {\n    return;\n  }\n  throw new Error(`Invariant: ${message}`);\n}\n\nexport class ClipboardDataMock {\n  getData: jest.Mock<string, [string]>;\n  setData: jest.Mock<void, [string, string]>;\n\n  constructor() {\n    this.getData = jest.fn();\n    this.setData = jest.fn();\n  }\n}\n\nexport class DataTransferMock implements DataTransfer {\n  _data: Map<string, string> = new Map();\n  get dropEffect(): DataTransfer['dropEffect'] {\n    throw new Error('Getter not implemented.');\n  }\n  get effectAllowed(): DataTransfer['effectAllowed'] {\n    throw new Error('Getter not implemented.');\n  }\n  get files(): FileList {\n    throw new Error('Getter not implemented.');\n  }\n  get items(): DataTransferItemList {\n    throw new Error('Getter not implemented.');\n  }\n  get types(): ReadonlyArray<string> {\n    return Array.from(this._data.keys());\n  }\n  clearData(dataType?: string): void {\n    //\n  }\n  getData(dataType: string): string {\n    return this._data.get(dataType) || '';\n  }\n  setData(dataType: string, data: string): void {\n    this._data.set(dataType, data);\n  }\n  setDragImage(image: Element, x: number, y: number): void {\n    //\n  }\n}\n\nexport class EventMock implements Event {\n  get bubbles(): boolean {\n    throw new Error('Getter not implemented.');\n  }\n  get cancelBubble(): boolean {\n    throw new Error('Gettter not implemented.');\n  }\n  get cancelable(): boolean {\n    throw new Error('Gettter not implemented.');\n  }\n  get composed(): boolean {\n    throw new Error('Gettter not implemented.');\n  }\n  get currentTarget(): EventTarget | null {\n    throw new Error('Gettter not implemented.');\n  }\n  get defaultPrevented(): boolean {\n    throw new Error('Gettter not implemented.');\n  }\n  get eventPhase(): number {\n    throw new Error('Gettter not implemented.');\n  }\n  get isTrusted(): boolean {\n    throw new Error('Gettter not implemented.');\n  }\n  get returnValue(): boolean {\n    throw new Error('Gettter not implemented.');\n  }\n  get srcElement(): EventTarget | null {\n    throw new Error('Gettter not implemented.');\n  }\n  get target(): EventTarget | null {\n    throw new Error('Gettter not implemented.');\n  }\n  get timeStamp(): number {\n    throw new Error('Gettter not implemented.');\n  }\n  get type(): string {\n    throw new Error('Gettter not implemented.');\n  }\n  composedPath(): EventTarget[] {\n    throw new Error('Method not implemented.');\n  }\n  initEvent(\n    type: string,\n    bubbles?: boolean | undefined,\n    cancelable?: boolean | undefined,\n  ): void {\n    throw new Error('Method not implemented.');\n  }\n  stopImmediatePropagation(): void {\n    return;\n  }\n  stopPropagation(): void {\n    return;\n  }\n  NONE = 0 as const;\n  CAPTURING_PHASE = 1 as const;\n  AT_TARGET = 2 as const;\n  BUBBLING_PHASE = 3 as const;\n  preventDefault() {\n    return;\n  }\n}\n\nexport class KeyboardEventMock extends EventMock implements KeyboardEvent {\n  altKey = false;\n  get charCode(): number {\n    throw new Error('Getter not implemented.');\n  }\n  get code(): string {\n    throw new Error('Getter not implemented.');\n  }\n  ctrlKey = false;\n  get isComposing(): boolean {\n    throw new Error('Getter not implemented.');\n  }\n  get key(): string {\n    throw new Error('Getter not implemented.');\n  }\n  get keyCode(): number {\n    throw new Error('Getter not implemented.');\n  }\n  get location(): number {\n    throw new Error('Getter not implemented.');\n  }\n  metaKey = false;\n  get repeat(): boolean {\n    throw new Error('Getter not implemented.');\n  }\n  shiftKey = false;\n  constructor(type: void | string) {\n    super();\n  }\n  getModifierState(keyArg: string): boolean {\n    throw new Error('Method not implemented.');\n  }\n  initKeyboardEvent(\n    typeArg: string,\n    bubblesArg?: boolean | undefined,\n    cancelableArg?: boolean | undefined,\n    viewArg?: Window | null | undefined,\n    keyArg?: string | undefined,\n    locationArg?: number | undefined,\n    ctrlKey?: boolean | undefined,\n    altKey?: boolean | undefined,\n    shiftKey?: boolean | undefined,\n    metaKey?: boolean | undefined,\n  ): void {\n    throw new Error('Method not implemented.');\n  }\n  DOM_KEY_LOCATION_STANDARD = 0 as const;\n  DOM_KEY_LOCATION_LEFT = 1 as const;\n  DOM_KEY_LOCATION_RIGHT = 2 as const;\n  DOM_KEY_LOCATION_NUMPAD = 3 as const;\n  get detail(): number {\n    throw new Error('Getter not implemented.');\n  }\n  get view(): Window | null {\n    throw new Error('Getter not implemented.');\n  }\n  get which(): number {\n    throw new Error('Getter not implemented.');\n  }\n  initUIEvent(\n    typeArg: string,\n    bubblesArg?: boolean | undefined,\n    cancelableArg?: boolean | undefined,\n    viewArg?: Window | null | undefined,\n    detailArg?: number | undefined,\n  ): void {\n    throw new Error('Method not implemented.');\n  }\n}\n\nexport function tabKeyboardEvent() {\n  return new KeyboardEventMock('keydown');\n}\n\nexport function shiftTabKeyboardEvent() {\n  const keyboardEvent = new KeyboardEventMock('keydown');\n  keyboardEvent.shiftKey = true;\n  return keyboardEvent;\n}\n\nexport function generatePermutations<T>(\n  values: T[],\n  maxLength = values.length,\n): T[][] {\n  if (maxLength > values.length) {\n    throw new Error('maxLength over values.length');\n  }\n  const result: T[][] = [];\n  const current: T[] = [];\n  const seen = new Set();\n  (function permutationsImpl() {\n    if (current.length > maxLength) {\n      return;\n    }\n    result.push(current.slice());\n    for (let i = 0; i < values.length; i++) {\n      const key = values[i];\n      if (seen.has(key)) {\n        continue;\n      }\n      seen.add(key);\n      current.push(key);\n      permutationsImpl();\n      seen.delete(key);\n      current.pop();\n    }\n  })();\n  return result;\n}\n\n// This tag function is just used to trigger prettier auto-formatting.\n// (https://prettier.io/blog/2020/08/24/2.1.0.html#api)\nexport function html(\n  partials: TemplateStringsArray,\n  ...params: string[]\n): string {\n  let output = '';\n  for (let i = 0; i < partials.length; i++) {\n    output += partials[i];\n    if (i < partials.length - 1) {\n      output += params[i];\n    }\n  }\n  return output;\n}\n\nexport function expectHtmlToBeEqual(expected: string, actual: string): void {\n  expect(formatHtml(expected)).toBe(formatHtml(actual));\n}\n\ntype nodeTextShape = {\n  text: string;\n  format?: number;\n};\n\ntype nodeShape = {\n  type: string;\n  children?: (nodeShape|nodeTextShape)[];\n}\n\nexport function getNodeShape(node: SerializedLexicalNode): nodeShape|nodeTextShape {\n  // @ts-ignore\n  const children: SerializedLexicalNode[] = (node.children || []);\n\n  const shape: nodeShape = {\n    type: node.type,\n  };\n\n  if (shape.type === 'text') {\n    // @ts-ignore\n    const shape: nodeTextShape =  {text: node.text}\n    // @ts-ignore\n    if (node && node.format) {\n      // @ts-ignore\n      shape.format = node.format;\n    }\n    return shape;\n  }\n\n  if (children.length > 0) {\n    shape.children = children.map(c => getNodeShape(c));\n  }\n\n  return shape;\n}\n\nexport function expectNodeShapeToMatch(editor: LexicalEditor, expected: nodeShape[]) {\n  const json = editor.getEditorState().toJSON();\n  const shape = getNodeShape(json.root) as nodeShape;\n  expect(shape.children).toMatchObject(expected);\n}\n\n/**\n * Expect a given prop within the JSON editor state structure to be the given value.\n * Uses dot notation for the provided `propPath`. Example:\n * 0.5.cat => First child, Sixth child, cat property\n */\nexport function expectEditorStateJSONPropToEqual(editor: LexicalEditor, propPath: string, expected: any) {\n  let currentItem: any = editor.getEditorState().toJSON().root;\n  let currentPath = [];\n  const pathParts = propPath.split('.');\n\n  for (const part of pathParts) {\n    currentPath.push(part);\n    const childAccess = Number.isInteger(Number(part)) && Array.isArray(currentItem.children);\n    const target = childAccess ? currentItem.children : currentItem;\n\n    if (typeof target[part] === 'undefined') {\n      throw new Error(`Could not resolve editor state at path ${currentPath.join('.')}`)\n    }\n    currentItem = target[part];\n  }\n\n  expect(currentItem).toBe(expected);\n}\n\nfunction formatHtml(s: string): string {\n  return s.replace(/>\\s+</g, '><').replace(/\\s*\\n\\s*/g, ' ').trim();\n}\n\nexport function dispatchKeydownEventForNode(node: LexicalNode, editor: LexicalEditor, key: string) {\n  const nodeDomEl = editor.getElementByKey(node.getKey());\n  const event = new KeyboardEvent('keydown', {\n    bubbles: true,\n    cancelable: true,\n    key,\n  });\n  nodeDomEl?.dispatchEvent(event);\n  editor.commitUpdates();\n}\n\nexport function dispatchKeydownEventForSelectedNode(editor: LexicalEditor, key: string) {\n  editor.getEditorState().read((): void => {\n    const node = $getSelection()?.getNodes()[0] || null;\n    if (node) {\n      dispatchKeydownEventForNode(node, editor, key);\n    }\n  });\n}\n\nexport function dispatchEditorMouseClick(editor: LexicalEditor, clientX: number, clientY: number) {\n  const dom = editor.getRootElement();\n  if (!dom) {\n    return;\n  }\n\n  const event = new MouseEvent('click', {\n    clientX: clientX,\n    clientY: clientY,\n    bubbles: true,\n    cancelable: true,\n  });\n  dom?.dispatchEvent(event);\n  editor.commitUpdates();\n}\n\nexport function patchRange() {\n    const RangePrototype = Object.getPrototypeOf(document.createRange());\n    RangePrototype.getBoundingClientRect = function (): DOMRect {\n        const rect = {\n            bottom: 0,\n            height: 0,\n            left: 0,\n            right: 0,\n            top: 0,\n            width: 0,\n            x: 0,\n            y: 0,\n        };\n        return {\n            ...rect,\n            toJSON() {\n                return rect;\n            },\n        };\n    };\n}"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/index.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nexport type {PasteCommandType} from './LexicalCommands';\nexport type {\n  CommandListener,\n  CommandListenerPriority,\n  CommandPayloadType,\n  CreateEditorArgs,\n  EditableListener,\n  EditorConfig,\n  EditorSetOptions,\n  EditorThemeClasses,\n  EditorThemeClassName,\n  EditorUpdateOptions,\n  HTMLConfig,\n  Klass,\n  KlassConstructor,\n  LexicalCommand,\n  LexicalEditor,\n  LexicalNodeReplacement,\n  MutationListener,\n  NodeMutation,\n  SerializedEditor,\n  Spread,\n  Transform,\n} from './LexicalEditor';\nexport type {\n  EditorState,\n  EditorStateReadOptions,\n  SerializedEditorState,\n} from './LexicalEditorState';\nexport type {\n  DOMChildConversion,\n  DOMConversion,\n  DOMConversionFn,\n  DOMConversionMap,\n  DOMConversionOutput,\n  DOMExportOutput,\n  LexicalNode,\n  NodeKey,\n  NodeMap,\n  SerializedLexicalNode,\n} from './LexicalNode';\nexport type {\n  BaseSelection,\n  NodeSelection,\n  Point,\n  PointType,\n  RangeSelection,\n} from './LexicalSelection';\nexport type {\n  SerializedElementNode,\n} from './nodes/LexicalElementNode';\nexport type {SerializedRootNode} from './nodes/LexicalRootNode';\nexport type {\n  SerializedTextNode,\n  TextFormatType,\n  TextModeType,\n} from './nodes/LexicalTextNode';\n\n// TODO Move this somewhere else and/or recheck if we still need this\nexport {\n  BLUR_COMMAND,\n  CAN_REDO_COMMAND,\n  CAN_UNDO_COMMAND,\n  CLEAR_EDITOR_COMMAND,\n  CLEAR_HISTORY_COMMAND,\n  CLICK_COMMAND,\n  CONTROLLED_TEXT_INSERTION_COMMAND,\n  COPY_COMMAND,\n  createCommand,\n  CUT_COMMAND,\n  DELETE_CHARACTER_COMMAND,\n  DELETE_LINE_COMMAND,\n  DELETE_WORD_COMMAND,\n  DRAGEND_COMMAND,\n  DRAGOVER_COMMAND,\n  DRAGSTART_COMMAND,\n  DROP_COMMAND,\n  FOCUS_COMMAND,\n  FORMAT_TEXT_COMMAND,\n  INDENT_CONTENT_COMMAND,\n  INSERT_LINE_BREAK_COMMAND,\n  INSERT_PARAGRAPH_COMMAND,\n  INSERT_TAB_COMMAND,\n  KEY_ARROW_DOWN_COMMAND,\n  KEY_ARROW_LEFT_COMMAND,\n  KEY_ARROW_RIGHT_COMMAND,\n  KEY_ARROW_UP_COMMAND,\n  KEY_BACKSPACE_COMMAND,\n  KEY_DELETE_COMMAND,\n  KEY_DOWN_COMMAND,\n  KEY_ENTER_COMMAND,\n  KEY_ESCAPE_COMMAND,\n  KEY_MODIFIER_COMMAND,\n  KEY_SPACE_COMMAND,\n  KEY_TAB_COMMAND,\n  MOVE_TO_END,\n  MOVE_TO_START,\n  OUTDENT_CONTENT_COMMAND,\n  PASTE_COMMAND,\n  REDO_COMMAND,\n  REMOVE_TEXT_COMMAND,\n  SELECT_ALL_COMMAND,\n  SELECTION_CHANGE_COMMAND,\n  SELECTION_INSERT_CLIPBOARD_NODES_COMMAND,\n  UNDO_COMMAND,\n} from './LexicalCommands';\nexport {\n  IS_ALL_FORMATTING,\n  IS_BOLD,\n  IS_CODE,\n  IS_HIGHLIGHT,\n  IS_ITALIC,\n  IS_STRIKETHROUGH,\n  IS_SUBSCRIPT,\n  IS_SUPERSCRIPT,\n  IS_UNDERLINE,\n  TEXT_TYPE_TO_FORMAT,\n} from './LexicalConstants';\nexport {\n  COMMAND_PRIORITY_CRITICAL,\n  COMMAND_PRIORITY_EDITOR,\n  COMMAND_PRIORITY_HIGH,\n  COMMAND_PRIORITY_LOW,\n  COMMAND_PRIORITY_NORMAL,\n  createEditor,\n} from './LexicalEditor';\nexport type {EventHandler} from './LexicalEvents';\nexport {$normalizeSelection as $normalizeSelection__EXPERIMENTAL} from './LexicalNormalization';\nexport {\n  $createNodeSelection,\n  $createPoint,\n  $createRangeSelection,\n  $createRangeSelectionFromDom,\n  $getCharacterOffsets,\n  $getPreviousSelection,\n  $getSelection,\n  $getTextContent,\n  $insertNodes,\n  $isBlockElementNode,\n  $isNodeSelection,\n  $isRangeSelection,\n} from './LexicalSelection';\nexport {$parseSerializedNode, isCurrentlyReadOnlyMode} from './LexicalUpdates';\nexport {\n  $addUpdateTag,\n  $applyNodeReplacement,\n  $cloneWithProperties,\n  $copyNode,\n  $getAdjacentNode,\n  $getEditor,\n  $getNearestNodeFromDOMNode,\n  $getNearestRootOrShadowRoot,\n  $getNodeByKey,\n  $getNodeByKeyOrThrow,\n  $getRoot,\n  $hasAncestor,\n  $hasUpdateTag,\n  $isInlineElementOrDecoratorNode,\n  $isLeafNode,\n  $isRootOrShadowRoot,\n  $isTokenOrSegmented,\n  $nodesOfType,\n  $selectAll,\n  $setCompositionKey,\n  $setSelection,\n  $splitNode,\n  getEditorPropertyFromDOMNode,\n  getNearestEditorFromDOMNode,\n  isBlockDomNode,\n  isHTMLAnchorElement,\n  isHTMLElement,\n  isInlineDomNode,\n  isLexicalEditor,\n  isSelectionCapturedInDecoratorInput,\n  isSelectionWithinEditor,\n  resetRandomKey,\n} from './LexicalUtils';\nexport {ArtificialNode__DO_NOT_USE} from './nodes/ArtificialNode';\nexport {$isDecoratorNode, DecoratorNode} from './nodes/LexicalDecoratorNode';\nexport {$isElementNode, ElementNode} from './nodes/LexicalElementNode';\nexport type {SerializedLineBreakNode} from './nodes/LexicalLineBreakNode';\nexport {\n  $createLineBreakNode,\n  $isLineBreakNode,\n  LineBreakNode,\n} from './nodes/LexicalLineBreakNode';\nexport type {SerializedParagraphNode} from './nodes/LexicalParagraphNode';\nexport {\n  $createParagraphNode,\n  $isParagraphNode,\n  ParagraphNode,\n} from './nodes/LexicalParagraphNode';\nexport {$isRootNode, RootNode} from './nodes/LexicalRootNode';\nexport type {SerializedTabNode} from './nodes/LexicalTabNode';\nexport {$createTabNode, $isTabNode, TabNode} from './nodes/LexicalTabNode';\nexport {$createTextNode, $isTextNode, TextNode} from './nodes/LexicalTextNode';\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/nodes/ArtificialNode.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\nimport type {EditorConfig} from 'lexical';\n\nimport {ElementNode} from './LexicalElementNode';\n\n// TODO: Cleanup ArtificialNode__DO_NOT_USE #5966\nexport class ArtificialNode__DO_NOT_USE extends ElementNode {\n  static getType(): string {\n    return 'artificial';\n  }\n\n  createDOM(config: EditorConfig): HTMLElement {\n    // this isnt supposed to be used and is not used anywhere but defining it to appease the API\n    const dom = document.createElement('div');\n    return dom;\n  }\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/nodes/CommonBlockNode.ts",
    "content": "import {ElementNode, type SerializedElementNode} from \"./LexicalElementNode\";\nimport {CommonBlockAlignment, CommonBlockInterface} from \"./common\";\nimport {Spread} from \"lexical\";\n\n\nexport type SerializedCommonBlockNode = Spread<{\n    id: string;\n    alignment: CommonBlockAlignment;\n    inset: number;\n}, SerializedElementNode>\n\nexport class CommonBlockNode extends ElementNode implements CommonBlockInterface {\n    __id: string = '';\n    __alignment: CommonBlockAlignment = '';\n    __inset: number = 0;\n\n    setId(id: string) {\n        const self = this.getWritable();\n        self.__id = id;\n    }\n\n    getId(): string {\n        const self = this.getLatest();\n        return self.__id;\n    }\n\n    setAlignment(alignment: CommonBlockAlignment) {\n        const self = this.getWritable();\n        self.__alignment = alignment;\n    }\n\n    getAlignment(): CommonBlockAlignment {\n        const self = this.getLatest();\n        return self.__alignment;\n    }\n\n    setInset(size: number) {\n        const self = this.getWritable();\n        self.__inset = size;\n    }\n\n    getInset(): number {\n        const self = this.getLatest();\n        return self.__inset;\n    }\n\n    exportJSON(): SerializedCommonBlockNode {\n        return {\n            ...super.exportJSON(),\n            id: this.__id,\n            alignment: this.__alignment,\n            inset: this.__inset,\n        };\n    }\n}\n\nexport function copyCommonBlockProperties(from: CommonBlockNode, to: CommonBlockNode): void {\n    // to.__id = from.__id;\n    to.__alignment = from.__alignment;\n    to.__inset = from.__inset;\n}"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/nodes/LexicalDecoratorNode.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {KlassConstructor, LexicalEditor} from '../LexicalEditor';\nimport type {NodeKey} from '../LexicalNode';\nimport type {ElementNode} from './LexicalElementNode';\n\nimport {EditorConfig} from 'lexical';\nimport invariant from 'lexical/shared/invariant';\n\nimport {LexicalNode} from '../LexicalNode';\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport interface DecoratorNode<T> {\n  getTopLevelElement(): ElementNode | this | null;\n  getTopLevelElementOrThrow(): ElementNode | this;\n}\n\n/** @noInheritDoc */\n// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging\nexport class DecoratorNode<T> extends LexicalNode {\n  declare ['constructor']: KlassConstructor<typeof DecoratorNode<T>>;\n  constructor(key?: NodeKey) {\n    super(key);\n  }\n\n  /**\n   * The returned value is added to the LexicalEditor._decorators\n   */\n  decorate(editor: LexicalEditor, config: EditorConfig): T {\n    invariant(false, 'decorate: base method not extended');\n  }\n\n  isIsolated(): boolean {\n    return false;\n  }\n\n  isInline(): boolean {\n    return true;\n  }\n\n  isKeyboardSelectable(): boolean {\n    return true;\n  }\n}\n\nexport function $isDecoratorNode<T>(\n  node: LexicalNode | null | undefined,\n): node is DecoratorNode<T> {\n  return node instanceof DecoratorNode;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {NodeKey, SerializedLexicalNode} from '../LexicalNode';\nimport type {\n  BaseSelection,\n  PointType,\n  RangeSelection,\n} from '../LexicalSelection';\nimport type {KlassConstructor, Spread} from 'lexical';\n\nimport invariant from 'lexical/shared/invariant';\n\nimport {$isTextNode, TextNode} from '../index';\nimport {\n  DOUBLE_LINE_BREAK,\n\n\n} from '../LexicalConstants';\nimport {LexicalNode} from '../LexicalNode';\nimport {\n  $getSelection,\n  $internalMakeRangeSelection,\n  $isRangeSelection,\n  moveSelectionPointToSibling,\n} from '../LexicalSelection';\nimport {errorOnReadOnly, getActiveEditor} from '../LexicalUpdates';\nimport {\n  $getNodeByKey,\n  $isRootOrShadowRoot,\n  removeFromParent,\n} from '../LexicalUtils';\n\nexport type SerializedElementNode<\n  T extends SerializedLexicalNode = SerializedLexicalNode,\n> = Spread<\n  {\n    children: Array<T>;\n    direction: 'ltr' | 'rtl' | null;\n  },\n  SerializedLexicalNode\n>;\n\n// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging\nexport interface ElementNode {\n  getTopLevelElement(): ElementNode | null;\n  getTopLevelElementOrThrow(): ElementNode;\n}\n\n/** @noInheritDoc */\n// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging\nexport class ElementNode extends LexicalNode {\n  declare ['constructor']: KlassConstructor<typeof ElementNode>;\n  /** @internal */\n  __first: null | NodeKey;\n  /** @internal */\n  __last: null | NodeKey;\n  /** @internal */\n  __size: number;\n  /** @internal */\n  __style: string;\n  /** @internal */\n  __dir: 'ltr' | 'rtl' | null;\n\n  constructor(key?: NodeKey) {\n    super(key);\n    this.__first = null;\n    this.__last = null;\n    this.__size = 0;\n    this.__style = '';\n    this.__dir = null;\n  }\n\n  afterCloneFrom(prevNode: this) {\n    super.afterCloneFrom(prevNode);\n    this.__first = prevNode.__first;\n    this.__last = prevNode.__last;\n    this.__size = prevNode.__size;\n    this.__style = prevNode.__style;\n    this.__dir = prevNode.__dir;\n  }\n\n  getStyle(): string {\n    const self = this.getLatest();\n    return self.__style;\n  }\n  getChildren<T extends LexicalNode>(): Array<T> {\n    const children: Array<T> = [];\n    let child: T | null = this.getFirstChild();\n    while (child !== null) {\n      children.push(child);\n      child = child.getNextSibling();\n    }\n    return children;\n  }\n  getChildrenKeys(): Array<NodeKey> {\n    const children: Array<NodeKey> = [];\n    let child: LexicalNode | null = this.getFirstChild();\n    while (child !== null) {\n      children.push(child.__key);\n      child = child.getNextSibling();\n    }\n    return children;\n  }\n  getChildrenSize(): number {\n    const self = this.getLatest();\n    return self.__size;\n  }\n  isEmpty(): boolean {\n    return this.getChildrenSize() === 0;\n  }\n  isDirty(): boolean {\n    const editor = getActiveEditor();\n    const dirtyElements = editor._dirtyElements;\n    return dirtyElements !== null && dirtyElements.has(this.__key);\n  }\n  isLastChild(): boolean {\n    const self = this.getLatest();\n    const parentLastChild = this.getParentOrThrow().getLastChild();\n    return parentLastChild !== null && parentLastChild.is(self);\n  }\n  getAllTextNodes(): Array<TextNode> {\n    const textNodes = [];\n    let child: LexicalNode | null = this.getFirstChild();\n    while (child !== null) {\n      if ($isTextNode(child)) {\n        textNodes.push(child);\n      }\n      if ($isElementNode(child)) {\n        const subChildrenNodes = child.getAllTextNodes();\n        textNodes.push(...subChildrenNodes);\n      }\n      child = child.getNextSibling();\n    }\n    return textNodes;\n  }\n  getFirstDescendant<T extends LexicalNode>(): null | T {\n    let node = this.getFirstChild<T>();\n    while ($isElementNode(node)) {\n      const child = node.getFirstChild<T>();\n      if (child === null) {\n        break;\n      }\n      node = child;\n    }\n    return node;\n  }\n    getFirstSelectableDescendant<T extends LexicalNode>(): null | T {\n      if (this.shouldSelectDirectly()) {\n          return null;\n      }\n        let node = this.getFirstChild<T>();\n        while ($isElementNode(node) && !node.shouldSelectDirectly()) {\n            const child = node.getFirstChild<T>();\n            if (child === null) {\n                break;\n            }\n            node = child;\n        }\n        return node;\n    }\n  getLastDescendant<T extends LexicalNode>(): null | T {\n    let node = this.getLastChild<T>();\n    while ($isElementNode(node)) {\n      const child = node.getLastChild<T>();\n      if (child === null) {\n        break;\n      }\n      node = child;\n    }\n    return node;\n  }\n    getLastSelectableDescendant<T extends LexicalNode>(): null | T {\n      if (this.shouldSelectDirectly()) {\n          return null;\n      }\n        let node = this.getLastChild<T>();\n        while ($isElementNode(node) && !node.shouldSelectDirectly()) {\n            const child = node.getLastChild<T>();\n            if (child === null) {\n                break;\n            }\n            node = child;\n        }\n        return node;\n    }\n  getDescendantByIndex<T extends LexicalNode>(index: number): null | T {\n    const children = this.getChildren<T>();\n    const childrenLength = children.length;\n    // For non-empty element nodes, we resolve its descendant\n    // (either a leaf node or the bottom-most element)\n    if (index >= childrenLength) {\n      const resolvedNode = children[childrenLength - 1];\n      return (\n        ($isElementNode(resolvedNode) && resolvedNode.getLastDescendant()) ||\n        resolvedNode ||\n        null\n      );\n    }\n    const resolvedNode = children[index];\n    return (\n      ($isElementNode(resolvedNode) && resolvedNode.getFirstDescendant()) ||\n      resolvedNode ||\n      null\n    );\n  }\n  getFirstChild<T extends LexicalNode>(): null | T {\n    const self = this.getLatest();\n    const firstKey = self.__first;\n    return firstKey === null ? null : $getNodeByKey<T>(firstKey);\n  }\n  getFirstChildOrThrow<T extends LexicalNode>(): T {\n    const firstChild = this.getFirstChild<T>();\n    if (firstChild === null) {\n      invariant(false, 'Expected node %s to have a first child.', this.__key);\n    }\n    return firstChild;\n  }\n  getLastChild<T extends LexicalNode>(): null | T {\n    const self = this.getLatest();\n    const lastKey = self.__last;\n    return lastKey === null ? null : $getNodeByKey<T>(lastKey);\n  }\n  getLastChildOrThrow<T extends LexicalNode>(): T {\n    const lastChild = this.getLastChild<T>();\n    if (lastChild === null) {\n      invariant(false, 'Expected node %s to have a last child.', this.__key);\n    }\n    return lastChild;\n  }\n  getChildAtIndex<T extends LexicalNode>(index: number): null | T {\n    const size = this.getChildrenSize();\n    let node: null | T;\n    let i;\n    if (index < size / 2) {\n      node = this.getFirstChild<T>();\n      i = 0;\n      while (node !== null && i <= index) {\n        if (i === index) {\n          return node;\n        }\n        node = node.getNextSibling();\n        i++;\n      }\n      return null;\n    }\n    node = this.getLastChild<T>();\n    i = size - 1;\n    while (node !== null && i >= index) {\n      if (i === index) {\n        return node;\n      }\n      node = node.getPreviousSibling();\n      i--;\n    }\n    return null;\n  }\n  getTextContent(): string {\n    let textContent = '';\n    const children = this.getChildren();\n    const childrenLength = children.length;\n    for (let i = 0; i < childrenLength; i++) {\n      const child = children[i];\n      textContent += child.getTextContent();\n      if (\n        $isElementNode(child) &&\n        i !== childrenLength - 1 &&\n        !child.isInline()\n      ) {\n        textContent += DOUBLE_LINE_BREAK;\n      }\n    }\n    return textContent;\n  }\n  getTextContentSize(): number {\n    let textContentSize = 0;\n    const children = this.getChildren();\n    const childrenLength = children.length;\n    for (let i = 0; i < childrenLength; i++) {\n      const child = children[i];\n      textContentSize += child.getTextContentSize();\n      if (\n        $isElementNode(child) &&\n        i !== childrenLength - 1 &&\n        !child.isInline()\n      ) {\n        textContentSize += DOUBLE_LINE_BREAK.length;\n      }\n    }\n    return textContentSize;\n  }\n  getDirection(): 'ltr' | 'rtl' | null {\n    const self = this.getLatest();\n    return self.__dir;\n  }\n\n  // Mutators\n\n  select(_anchorOffset?: number, _focusOffset?: number): RangeSelection {\n    errorOnReadOnly();\n    const selection = $getSelection();\n    let anchorOffset = _anchorOffset;\n    let focusOffset = _focusOffset;\n    const childrenCount = this.getChildrenSize();\n    if (!this.canBeEmpty() && !this.shouldSelectDirectly()) {\n      if (_anchorOffset === 0 && _focusOffset === 0) {\n        const firstChild = this.getFirstChild();\n        if ($isTextNode(firstChild) || $isElementNode(firstChild)) {\n          return firstChild.select(0, 0);\n        }\n      } else if (\n        (_anchorOffset === undefined || _anchorOffset === childrenCount) &&\n        (_focusOffset === undefined || _focusOffset === childrenCount)\n      ) {\n        const lastChild = this.getLastChild();\n        if ($isTextNode(lastChild) || $isElementNode(lastChild)) {\n          return lastChild.select();\n        }\n      }\n    }\n    if (anchorOffset === undefined) {\n      anchorOffset = childrenCount;\n    }\n    if (focusOffset === undefined) {\n      focusOffset = childrenCount;\n    }\n    const key = this.__key;\n    if (!$isRangeSelection(selection)) {\n      return $internalMakeRangeSelection(\n        key,\n        anchorOffset,\n        key,\n        focusOffset,\n        'element',\n        'element',\n      );\n    } else {\n      selection.anchor.set(key, anchorOffset, 'element');\n      selection.focus.set(key, focusOffset, 'element');\n      selection.dirty = true;\n    }\n    return selection;\n  }\n  selectStart(): RangeSelection {\n    const firstNode = this.getFirstSelectableDescendant();\n    return firstNode ? firstNode.selectStart() : this.select();\n  }\n  selectEnd(): RangeSelection {\n    const lastNode = this.getLastSelectableDescendant();\n    return lastNode ? lastNode.selectEnd() : this.select();\n  }\n  clear(): this {\n    const writableSelf = this.getWritable();\n    const children = this.getChildren();\n    children.forEach((child) => child.remove());\n    return writableSelf;\n  }\n  append(...nodesToAppend: LexicalNode[]): this {\n    return this.splice(this.getChildrenSize(), 0, nodesToAppend);\n  }\n  setDirection(direction: 'ltr' | 'rtl' | null): this {\n    const self = this.getWritable();\n    self.__dir = direction;\n    return self;\n  }\n  setStyle(style: string): this {\n    const self = this.getWritable();\n    self.__style = style || '';\n    return this;\n  }\n  splice(\n    start: number,\n    deleteCount: number,\n    nodesToInsert: Array<LexicalNode>,\n  ): this {\n    const nodesToInsertLength = nodesToInsert.length;\n    const oldSize = this.getChildrenSize();\n    const writableSelf = this.getWritable();\n    const writableSelfKey = writableSelf.__key;\n    const nodesToInsertKeys = [];\n    const nodesToRemoveKeys = [];\n    const nodeAfterRange = this.getChildAtIndex(start + deleteCount);\n    let nodeBeforeRange = null;\n    let newSize = oldSize - deleteCount + nodesToInsertLength;\n\n    if (start !== 0) {\n      if (start === oldSize) {\n        nodeBeforeRange = this.getLastChild();\n      } else {\n        const node = this.getChildAtIndex(start);\n        if (node !== null) {\n          nodeBeforeRange = node.getPreviousSibling();\n        }\n      }\n    }\n\n    if (deleteCount > 0) {\n      let nodeToDelete =\n        nodeBeforeRange === null\n          ? this.getFirstChild()\n          : nodeBeforeRange.getNextSibling();\n      for (let i = 0; i < deleteCount; i++) {\n        if (nodeToDelete === null) {\n          invariant(false, 'splice: sibling not found');\n        }\n        const nextSibling = nodeToDelete.getNextSibling();\n        const nodeKeyToDelete = nodeToDelete.__key;\n        const writableNodeToDelete = nodeToDelete.getWritable();\n        removeFromParent(writableNodeToDelete);\n        nodesToRemoveKeys.push(nodeKeyToDelete);\n        nodeToDelete = nextSibling;\n      }\n    }\n\n    let prevNode = nodeBeforeRange;\n    for (let i = 0; i < nodesToInsertLength; i++) {\n      const nodeToInsert = nodesToInsert[i];\n      if (prevNode !== null && nodeToInsert.is(prevNode)) {\n        nodeBeforeRange = prevNode = prevNode.getPreviousSibling();\n      }\n      const writableNodeToInsert = nodeToInsert.getWritable();\n      if (writableNodeToInsert.__parent === writableSelfKey) {\n        newSize--;\n      }\n      removeFromParent(writableNodeToInsert);\n      const nodeKeyToInsert = nodeToInsert.__key;\n      if (prevNode === null) {\n        writableSelf.__first = nodeKeyToInsert;\n        writableNodeToInsert.__prev = null;\n      } else {\n        const writablePrevNode = prevNode.getWritable();\n        writablePrevNode.__next = nodeKeyToInsert;\n        writableNodeToInsert.__prev = writablePrevNode.__key;\n      }\n      if (nodeToInsert.__key === writableSelfKey) {\n        invariant(false, 'append: attempting to append self');\n      }\n      // Set child parent to self\n      writableNodeToInsert.__parent = writableSelfKey;\n      nodesToInsertKeys.push(nodeKeyToInsert);\n      prevNode = nodeToInsert;\n    }\n\n    if (start + deleteCount === oldSize) {\n      if (prevNode !== null) {\n        const writablePrevNode = prevNode.getWritable();\n        writablePrevNode.__next = null;\n        writableSelf.__last = prevNode.__key;\n      }\n    } else if (nodeAfterRange !== null) {\n      const writableNodeAfterRange = nodeAfterRange.getWritable();\n      if (prevNode !== null) {\n        const writablePrevNode = prevNode.getWritable();\n        writableNodeAfterRange.__prev = prevNode.__key;\n        writablePrevNode.__next = nodeAfterRange.__key;\n      } else {\n        writableNodeAfterRange.__prev = null;\n      }\n    }\n\n    writableSelf.__size = newSize;\n\n    // In case of deletion we need to adjust selection, unlink removed nodes\n    // and clean up node itself if it becomes empty. None of these needed\n    // for insertion-only cases\n    if (nodesToRemoveKeys.length) {\n      // Adjusting selection, in case node that was anchor/focus will be deleted\n      const selection = $getSelection();\n      if ($isRangeSelection(selection)) {\n        const nodesToRemoveKeySet = new Set(nodesToRemoveKeys);\n        const nodesToInsertKeySet = new Set(nodesToInsertKeys);\n\n        const {anchor, focus} = selection;\n        if (isPointRemoved(anchor, nodesToRemoveKeySet, nodesToInsertKeySet)) {\n          moveSelectionPointToSibling(\n            anchor,\n            anchor.getNode(),\n            this,\n            nodeBeforeRange,\n            nodeAfterRange,\n          );\n        }\n        if (isPointRemoved(focus, nodesToRemoveKeySet, nodesToInsertKeySet)) {\n          moveSelectionPointToSibling(\n            focus,\n            focus.getNode(),\n            this,\n            nodeBeforeRange,\n            nodeAfterRange,\n          );\n        }\n        // Cleanup if node can't be empty\n        if (newSize === 0 && !this.canBeEmpty() && !$isRootOrShadowRoot(this)) {\n          this.remove();\n        }\n      }\n    }\n\n    return writableSelf;\n  }\n  // JSON serialization\n  exportJSON(): SerializedElementNode {\n    return {\n      children: [],\n      direction: this.getDirection(),\n      type: 'element',\n      version: 1,\n    };\n  }\n  // These are intended to be extends for specific element heuristics.\n  insertNewAfter(\n    selection: RangeSelection,\n    restoreSelection?: boolean,\n  ): null | LexicalNode {\n    return null;\n  }\n  canIndent(): boolean {\n    return true;\n  }\n  /*\n   * This method controls the behavior of a the node during backwards\n   * deletion (i.e., backspace) when selection is at the beginning of\n   * the node (offset 0)\n   */\n  collapseAtStart(selection: RangeSelection): boolean {\n    return false;\n  }\n  excludeFromCopy(destination?: 'clone' | 'html'): boolean {\n    return false;\n  }\n  /** @deprecated @internal */\n  canReplaceWith(replacement: LexicalNode): boolean {\n    return true;\n  }\n  /** @deprecated @internal */\n  canInsertAfter(node: LexicalNode): boolean {\n    return true;\n  }\n  canBeEmpty(): boolean {\n    return true;\n  }\n  canInsertTextBefore(): boolean {\n    return true;\n  }\n  canInsertTextAfter(): boolean {\n    return true;\n  }\n  isInline(): boolean {\n    return false;\n  }\n  // A shadow root is a Node that behaves like RootNode. The shadow root (and RootNode) mark the\n  // end of the hiercharchy, most implementations should treat it as there's nothing (upwards)\n  // beyond this point. For example, node.getTopLevelElement(), when performed inside a TableCellNode\n  // will return the immediate first child underneath TableCellNode instead of RootNode.\n  isShadowRoot(): boolean {\n    return false;\n  }\n  /** @deprecated @internal */\n  canMergeWith(node: ElementNode): boolean {\n    return false;\n  }\n  extractWithChild(\n    child: LexicalNode,\n    selection: BaseSelection | null,\n    destination: 'clone' | 'html',\n  ): boolean {\n    return false;\n  }\n\n  /**\n   * Determines whether this node, when empty, can merge with a first block\n   * of nodes being inserted.\n   *\n   * This method is specifically called in {@link RangeSelection.insertNodes}\n   * to determine merging behavior during nodes insertion.\n   *\n   * @example\n   * // In a ListItemNode or QuoteNode implementation:\n   * canMergeWhenEmpty(): true {\n   *  return true;\n   * }\n   */\n  canMergeWhenEmpty(): boolean {\n    return false;\n  }\n}\n\nexport function $isElementNode(\n  node: LexicalNode | null | undefined,\n): node is ElementNode {\n  return node instanceof ElementNode;\n}\n\nfunction isPointRemoved(\n  point: PointType,\n  nodesToRemoveKeySet: Set<NodeKey>,\n  nodesToInsertKeySet: Set<NodeKey>,\n): boolean {\n  let node: ElementNode | TextNode | null = point.getNode();\n  while (node) {\n    const nodeKey = node.__key;\n    if (nodesToRemoveKeySet.has(nodeKey) && !nodesToInsertKeySet.has(nodeKey)) {\n      return true;\n    }\n    node = node.getParent();\n  }\n  return false;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/nodes/LexicalLineBreakNode.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {KlassConstructor} from '../LexicalEditor';\nimport type {\n  DOMConversionMap,\n  DOMConversionOutput,\n  NodeKey,\n  SerializedLexicalNode,\n} from '../LexicalNode';\n\nimport {DOM_TEXT_TYPE} from '../LexicalConstants';\nimport {LexicalNode} from '../LexicalNode';\nimport {$applyNodeReplacement, isBlockDomNode} from '../LexicalUtils';\n\nexport type SerializedLineBreakNode = SerializedLexicalNode;\n\n/** @noInheritDoc */\nexport class LineBreakNode extends LexicalNode {\n  declare ['constructor']: KlassConstructor<typeof LineBreakNode>;\n  static getType(): string {\n    return 'linebreak';\n  }\n\n  static clone(node: LineBreakNode): LineBreakNode {\n    return new LineBreakNode(node.__key);\n  }\n\n  constructor(key?: NodeKey) {\n    super(key);\n  }\n\n  getTextContent(): '\\n' {\n    return '\\n';\n  }\n\n  createDOM(): HTMLElement {\n    return document.createElement('br');\n  }\n\n  updateDOM(): false {\n    return false;\n  }\n\n  static importDOM(): DOMConversionMap | null {\n    return {\n      br: (node: Node) => {\n        if (isOnlyChildInBlockNode(node) || isLastChildInBlockNode(node)) {\n          return null;\n        }\n        return {\n          conversion: $convertLineBreakElement,\n          priority: 0,\n        };\n      },\n    };\n  }\n\n  static importJSON(\n    serializedLineBreakNode: SerializedLineBreakNode,\n  ): LineBreakNode {\n    return $createLineBreakNode();\n  }\n\n  exportJSON(): SerializedLexicalNode {\n    return {\n      type: 'linebreak',\n      version: 1,\n    };\n  }\n}\n\nfunction $convertLineBreakElement(node: Node): DOMConversionOutput {\n  return {node: $createLineBreakNode()};\n}\n\nexport function $createLineBreakNode(): LineBreakNode {\n  return $applyNodeReplacement(new LineBreakNode());\n}\n\nexport function $isLineBreakNode(\n  node: LexicalNode | null | undefined,\n): node is LineBreakNode {\n  return node instanceof LineBreakNode;\n}\n\nfunction isOnlyChildInBlockNode(node: Node): boolean {\n  const parentElement = node.parentElement;\n  if (parentElement !== null && isBlockDomNode(parentElement)) {\n    const firstChild = parentElement.firstChild!;\n    if (\n      firstChild === node ||\n      (firstChild.nextSibling === node && isWhitespaceDomTextNode(firstChild))\n    ) {\n      const lastChild = parentElement.lastChild!;\n      if (\n        lastChild === node ||\n        (lastChild.previousSibling === node &&\n          isWhitespaceDomTextNode(lastChild))\n      ) {\n        return true;\n      }\n    }\n  }\n  return false;\n}\n\nfunction isLastChildInBlockNode(node: Node): boolean {\n  const parentElement = node.parentElement;\n  if (parentElement !== null && isBlockDomNode(parentElement)) {\n    // check if node is first child, because only childs dont count\n    const firstChild = parentElement.firstChild!;\n    if (\n      firstChild === node ||\n      (firstChild.nextSibling === node && isWhitespaceDomTextNode(firstChild))\n    ) {\n      return false;\n    }\n\n    // check if its last child\n    const lastChild = parentElement.lastChild!;\n    if (\n      lastChild === node ||\n      (lastChild.previousSibling === node && isWhitespaceDomTextNode(lastChild))\n    ) {\n      return true;\n    }\n  }\n  return false;\n}\n\nfunction isWhitespaceDomTextNode(node: Node): boolean {\n  return (\n    node.nodeType === DOM_TEXT_TYPE &&\n    /^( |\\t|\\r?\\n)+$/.test(node.textContent || '')\n  );\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/nodes/LexicalParagraphNode.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {\n  EditorConfig,\n  KlassConstructor,\n  LexicalEditor,\n  Spread,\n} from '../LexicalEditor';\nimport type {\n  DOMConversionMap,\n  DOMConversionOutput,\n  DOMExportOutput,\n  LexicalNode,\n  NodeKey,\n} from '../LexicalNode';\nimport {RangeSelection, TEXT_TYPE_TO_FORMAT, TextFormatType} from 'lexical';\n\nimport {\n  $applyNodeReplacement,\n  getCachedClassNameArray,\n  isHTMLElement,\n} from '../LexicalUtils';\nimport {$isTextNode} from './LexicalTextNode';\nimport {\n  commonPropertiesDifferent, deserializeCommonBlockNode,\n  setCommonBlockPropsFromElement,\n  updateElementWithCommonBlockProps\n} from \"./common\";\nimport {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} from \"lexical/nodes/CommonBlockNode\";\n\nexport type SerializedParagraphNode = Spread<\n  {\n    textFormat: number;\n    textStyle: string;\n  },\n  SerializedCommonBlockNode\n>;\n\n/** @noInheritDoc */\nexport class ParagraphNode extends CommonBlockNode {\n  declare ['constructor']: KlassConstructor<typeof ParagraphNode>;\n  /** @internal */\n  __textFormat: number;\n  __textStyle: string;\n\n  constructor(key?: NodeKey) {\n    super(key);\n    this.__textFormat = 0;\n    this.__textStyle = '';\n  }\n\n  static getType(): string {\n    return 'paragraph';\n  }\n\n  getTextFormat(): number {\n    const self = this.getLatest();\n    return self.__textFormat;\n  }\n\n  setTextFormat(type: number): this {\n    const self = this.getWritable();\n    self.__textFormat = type;\n    return self;\n  }\n\n  hasTextFormat(type: TextFormatType): boolean {\n    const formatFlag = TEXT_TYPE_TO_FORMAT[type];\n    return (this.getTextFormat() & formatFlag) !== 0;\n  }\n\n  getTextStyle(): string {\n    const self = this.getLatest();\n    return self.__textStyle;\n  }\n\n  setTextStyle(style: string): this {\n    const self = this.getWritable();\n    self.__textStyle = style;\n    return self;\n  }\n\n  static clone(node: ParagraphNode): ParagraphNode {\n    return new ParagraphNode(node.__key);\n  }\n\n  afterCloneFrom(prevNode: this) {\n    super.afterCloneFrom(prevNode);\n    this.__textFormat = prevNode.__textFormat;\n    this.__textStyle = prevNode.__textStyle;\n    copyCommonBlockProperties(prevNode, this);\n  }\n\n  // View\n\n  createDOM(config: EditorConfig): HTMLElement {\n    const dom = document.createElement('p');\n    const classNames = getCachedClassNameArray(config.theme, 'paragraph');\n    if (classNames !== undefined) {\n      const domClassList = dom.classList;\n      domClassList.add(...classNames);\n    }\n\n    updateElementWithCommonBlockProps(dom, this);\n\n    return dom;\n  }\n  updateDOM(\n    prevNode: ParagraphNode,\n    dom: HTMLElement,\n    config: EditorConfig,\n  ): boolean {\n    return commonPropertiesDifferent(prevNode, this);\n  }\n\n  static importDOM(): DOMConversionMap | null {\n    return {\n      p: (node: Node) => ({\n        conversion: $convertParagraphElement,\n        priority: 0,\n      }),\n    };\n  }\n\n  exportDOM(editor: LexicalEditor): DOMExportOutput {\n    const {element} = super.exportDOM(editor);\n\n    if (element && isHTMLElement(element)) {\n      if (this.isEmpty()) {\n        element.append(document.createElement('br'));\n      }\n    }\n\n    return {\n      element,\n    };\n  }\n\n  static importJSON(serializedNode: SerializedParagraphNode): ParagraphNode {\n    const node = $createParagraphNode();\n    deserializeCommonBlockNode(serializedNode, node);\n    node.setTextFormat(serializedNode.textFormat);\n    return node;\n  }\n\n  exportJSON(): SerializedParagraphNode {\n    return {\n      ...super.exportJSON(),\n      textFormat: this.getTextFormat(),\n      textStyle: this.getTextStyle(),\n      type: 'paragraph',\n      version: 1,\n    };\n  }\n\n  // Mutation\n\n  insertNewAfter(\n    rangeSelection: RangeSelection,\n    restoreSelection: boolean,\n  ): ParagraphNode {\n    const newElement = $createParagraphNode();\n    newElement.setTextFormat(rangeSelection.format);\n    newElement.setTextStyle(rangeSelection.style);\n    const direction = this.getDirection();\n    newElement.setDirection(direction);\n    newElement.setStyle(this.getTextStyle());\n    this.insertAfter(newElement, restoreSelection);\n    return newElement;\n  }\n\n  collapseAtStart(): boolean {\n    const children = this.getChildren();\n    // If we have an empty (trimmed) first paragraph and try and remove it,\n    // delete the paragraph as long as we have another sibling to go to\n    if (\n      children.length === 0 ||\n      ($isTextNode(children[0]) && children[0].getTextContent().trim() === '')\n    ) {\n      const nextSibling = this.getNextSibling();\n      if (nextSibling !== null) {\n        this.selectNext();\n        this.remove();\n        return true;\n      }\n      const prevSibling = this.getPreviousSibling();\n      if (prevSibling !== null) {\n        this.selectPrevious();\n        this.remove();\n        return true;\n      }\n    }\n    return false;\n  }\n}\n\nfunction $convertParagraphElement(element: HTMLElement): DOMConversionOutput {\n  const node = $createParagraphNode();\n  setCommonBlockPropsFromElement(element, node);\n  return {node};\n}\n\nexport function $createParagraphNode(): ParagraphNode {\n  return $applyNodeReplacement(new ParagraphNode());\n}\n\nexport function $isParagraphNode(\n  node: LexicalNode | null | undefined,\n): node is ParagraphNode {\n  return node instanceof ParagraphNode;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/nodes/LexicalRootNode.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {LexicalNode, SerializedLexicalNode} from '../LexicalNode';\nimport type {SerializedElementNode} from './LexicalElementNode';\n\nimport invariant from 'lexical/shared/invariant';\n\nimport {NO_DIRTY_NODES} from '../LexicalConstants';\nimport {getActiveEditor, isCurrentlyReadOnlyMode} from '../LexicalUpdates';\nimport {$getRoot} from '../LexicalUtils';\nimport {$isDecoratorNode} from './LexicalDecoratorNode';\nimport {$isElementNode, ElementNode} from './LexicalElementNode';\n\nexport type SerializedRootNode<\n  T extends SerializedLexicalNode = SerializedLexicalNode,\n> = SerializedElementNode<T>;\n\n/** @noInheritDoc */\nexport class RootNode extends ElementNode {\n  /** @internal */\n  __cachedText: null | string;\n\n  static getType(): string {\n    return 'root';\n  }\n\n  static clone(): RootNode {\n    return new RootNode();\n  }\n\n  constructor() {\n    super('root');\n    this.__cachedText = null;\n  }\n\n  getTopLevelElementOrThrow(): never {\n    invariant(\n      false,\n      'getTopLevelElementOrThrow: root nodes are not top level elements',\n    );\n  }\n\n  getTextContent(): string {\n    const cachedText = this.__cachedText;\n    if (\n      isCurrentlyReadOnlyMode() ||\n      getActiveEditor()._dirtyType === NO_DIRTY_NODES\n    ) {\n      if (cachedText !== null) {\n        return cachedText;\n      }\n    }\n    return super.getTextContent();\n  }\n\n  remove(): never {\n    invariant(false, 'remove: cannot be called on root nodes');\n  }\n\n  replace<N = LexicalNode>(node: N): never {\n    invariant(false, 'replace: cannot be called on root nodes');\n  }\n\n  insertBefore(nodeToInsert: LexicalNode): LexicalNode {\n    invariant(false, 'insertBefore: cannot be called on root nodes');\n  }\n\n  insertAfter(nodeToInsert: LexicalNode): LexicalNode {\n    invariant(false, 'insertAfter: cannot be called on root nodes');\n  }\n\n  // View\n\n  updateDOM(prevNode: RootNode, dom: HTMLElement): false {\n    return false;\n  }\n\n  // Mutate\n\n  append(...nodesToAppend: LexicalNode[]): this {\n    for (let i = 0; i < nodesToAppend.length; i++) {\n      const node = nodesToAppend[i];\n      if (!$isElementNode(node) && !$isDecoratorNode(node)) {\n        invariant(\n          false,\n          'rootNode.append: Only element or decorator nodes can be appended to the root node',\n        );\n      }\n    }\n    return super.append(...nodesToAppend);\n  }\n\n  static importJSON(serializedNode: SerializedRootNode): RootNode {\n    // We don't create a root, and instead use the existing root.\n    const node = $getRoot();\n    node.setDirection(serializedNode.direction);\n    return node;\n  }\n\n  exportJSON(): SerializedRootNode {\n    return {\n      children: [],\n      direction: this.getDirection(),\n      type: 'root',\n      version: 1,\n    };\n  }\n\n  collapseAtStart(): true {\n    return true;\n  }\n}\n\nexport function $createRootNode(): RootNode {\n  return new RootNode();\n}\n\nexport function $isRootNode(\n  node: RootNode | LexicalNode | null | undefined,\n): node is RootNode {\n  return node instanceof RootNode;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/nodes/LexicalTabNode.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {DOMConversionMap, NodeKey} from '../LexicalNode';\n\nimport invariant from 'lexical/shared/invariant';\n\nimport {IS_UNMERGEABLE} from '../LexicalConstants';\nimport {LexicalNode} from '../LexicalNode';\nimport {$applyNodeReplacement} from '../LexicalUtils';\nimport {\n  SerializedTextNode,\n  TextDetailType,\n  TextModeType,\n  TextNode,\n} from './LexicalTextNode';\n\nexport type SerializedTabNode = SerializedTextNode;\n\n/** @noInheritDoc */\nexport class TabNode extends TextNode {\n  static getType(): string {\n    return 'tab';\n  }\n\n  static clone(node: TabNode): TabNode {\n    return new TabNode(node.__key);\n  }\n\n  afterCloneFrom(prevNode: this): void {\n    super.afterCloneFrom(prevNode);\n    // TabNode __text can be either '\\t' or ''. insertText will remove the empty Node\n    this.__text = prevNode.__text;\n  }\n\n  constructor(key?: NodeKey) {\n    super('\\t', key);\n    this.__detail = IS_UNMERGEABLE;\n  }\n\n  static importDOM(): DOMConversionMap | null {\n    return null;\n  }\n\n  static importJSON(serializedTabNode: SerializedTabNode): TabNode {\n    const node = $createTabNode();\n    node.setFormat(serializedTabNode.format);\n    node.setStyle(serializedTabNode.style);\n    return node;\n  }\n\n  exportJSON(): SerializedTabNode {\n    return {\n      ...super.exportJSON(),\n      type: 'tab',\n      version: 1,\n    };\n  }\n\n  setTextContent(_text: string): this {\n    invariant(false, 'TabNode does not support setTextContent');\n  }\n\n  setDetail(_detail: TextDetailType | number): this {\n    invariant(false, 'TabNode does not support setDetail');\n  }\n\n  setMode(_type: TextModeType): this {\n    invariant(false, 'TabNode does not support setMode');\n  }\n\n  canInsertTextBefore(): boolean {\n    return false;\n  }\n\n  canInsertTextAfter(): boolean {\n    return false;\n  }\n}\n\nexport function $createTabNode(): TabNode {\n  return $applyNodeReplacement(new TabNode());\n}\n\nexport function $isTabNode(\n  node: LexicalNode | null | undefined,\n): node is TabNode {\n  return node instanceof TabNode;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {\n  EditorConfig,\n  KlassConstructor,\n  LexicalEditor,\n  Spread,\n  TextNodeThemeClasses,\n} from '../LexicalEditor';\nimport type {\n  DOMConversionMap,\n  DOMConversionOutput,\n  DOMExportOutput,\n  NodeKey,\n  SerializedLexicalNode,\n} from '../LexicalNode';\nimport type {BaseSelection, RangeSelection} from '../LexicalSelection';\nimport type {ElementNode} from './LexicalElementNode';\n\nimport {IS_FIREFOX} from 'lexical/shared/environment';\nimport invariant from 'lexical/shared/invariant';\n\nimport {\n  COMPOSITION_SUFFIX,\n  DETAIL_TYPE_TO_DETAIL,\n  DOM_ELEMENT_TYPE,\n  DOM_TEXT_TYPE,\n  IS_BOLD,\n  IS_CODE,\n  IS_DIRECTIONLESS,\n  IS_HIGHLIGHT,\n  IS_ITALIC,\n  IS_SEGMENTED,\n  IS_STRIKETHROUGH,\n  IS_SUBSCRIPT,\n  IS_SUPERSCRIPT,\n  IS_TOKEN,\n  IS_UNDERLINE,\n  IS_UNMERGEABLE,\n  TEXT_MODE_TO_TYPE,\n  TEXT_TYPE_TO_FORMAT,\n  TEXT_TYPE_TO_MODE,\n} from '../LexicalConstants';\nimport {LexicalNode} from '../LexicalNode';\nimport {\n  $getSelection,\n  $internalMakeRangeSelection,\n  $isRangeSelection,\n  $updateElementSelectionOnCreateDeleteNode,\n  adjustPointOffsetForMergedSibling,\n} from '../LexicalSelection';\nimport {errorOnReadOnly} from '../LexicalUpdates';\nimport {\n  $applyNodeReplacement,\n  $getCompositionKey,\n  $setCompositionKey,\n  getCachedClassNameArray,\n  internalMarkSiblingsAsDirty,\n  isHTMLElement,\n  isInlineDomNode,\n  toggleTextFormatType,\n} from '../LexicalUtils';\nimport {$createLineBreakNode} from './LexicalLineBreakNode';\nimport {$createTabNode} from './LexicalTabNode';\n\nexport type SerializedTextNode = Spread<\n  {\n    detail: number;\n    format: number;\n    mode: TextModeType;\n    style: string;\n    text: string;\n  },\n  SerializedLexicalNode\n>;\n\nexport type TextDetailType = 'directionless' | 'unmergable';\n\nexport type TextFormatType =\n  | 'bold'\n  | 'underline'\n  | 'strikethrough'\n  | 'italic'\n  | 'highlight'\n  | 'code'\n  | 'subscript'\n  | 'superscript';\n\nexport type TextModeType = 'normal' | 'token' | 'segmented';\n\nexport type TextMark = {end: null | number; id: string; start: null | number};\n\nexport type TextMarks = Array<TextMark>;\n\nfunction getElementOuterTag(node: TextNode, format: number): string | null {\n  if (format & IS_CODE) {\n    return 'code';\n  }\n  if (format & IS_HIGHLIGHT) {\n    return 'mark';\n  }\n  if (format & IS_SUBSCRIPT) {\n    return 'sub';\n  }\n  if (format & IS_SUPERSCRIPT) {\n    return 'sup';\n  }\n  return null;\n}\n\nfunction getElementInnerTag(node: TextNode, format: number): string {\n  if (format & IS_BOLD) {\n    return 'strong';\n  }\n  if (format & IS_ITALIC) {\n    return 'em';\n  }\n  return 'span';\n}\n\nfunction setTextThemeClassNames(\n  tag: string,\n  prevFormat: number,\n  nextFormat: number,\n  dom: HTMLElement,\n  textClassNames: TextNodeThemeClasses,\n): void {\n  const domClassList = dom.classList;\n  // Firstly we handle the base theme.\n  let classNames = getCachedClassNameArray(textClassNames, 'base');\n  if (classNames !== undefined) {\n    domClassList.add(...classNames);\n  }\n  // Secondly we handle the special case: underline + strikethrough.\n  // We have to do this as we need a way to compose the fact that\n  // the same CSS property will need to be used: text-decoration.\n  // In an ideal world we shouldn't have to do this, but there's no\n  // easy workaround for many atomic CSS systems today.\n  classNames = getCachedClassNameArray(\n    textClassNames,\n    'underlineStrikethrough',\n  );\n  let hasUnderlineStrikethrough = false;\n  const prevUnderlineStrikethrough =\n    prevFormat & IS_UNDERLINE && prevFormat & IS_STRIKETHROUGH;\n  const nextUnderlineStrikethrough =\n    nextFormat & IS_UNDERLINE && nextFormat & IS_STRIKETHROUGH;\n\n  if (classNames !== undefined) {\n    if (nextUnderlineStrikethrough) {\n      hasUnderlineStrikethrough = true;\n      if (!prevUnderlineStrikethrough) {\n        domClassList.add(...classNames);\n      }\n    } else if (prevUnderlineStrikethrough) {\n      domClassList.remove(...classNames);\n    }\n  }\n\n  for (const key in TEXT_TYPE_TO_FORMAT) {\n    const format = key;\n    const flag = TEXT_TYPE_TO_FORMAT[format];\n    classNames = getCachedClassNameArray(textClassNames, key);\n    if (classNames !== undefined) {\n      if (nextFormat & flag) {\n        if (\n          hasUnderlineStrikethrough &&\n          (key === 'underline' || key === 'strikethrough')\n        ) {\n          if (prevFormat & flag) {\n            domClassList.remove(...classNames);\n          }\n          continue;\n        }\n        if (\n          (prevFormat & flag) === 0 ||\n          (prevUnderlineStrikethrough && key === 'underline') ||\n          key === 'strikethrough'\n        ) {\n          domClassList.add(...classNames);\n        }\n      } else if (prevFormat & flag) {\n        domClassList.remove(...classNames);\n      }\n    }\n  }\n}\n\nfunction diffComposedText(a: string, b: string): [number, number, string] {\n  const aLength = a.length;\n  const bLength = b.length;\n  let left = 0;\n  let right = 0;\n\n  while (left < aLength && left < bLength && a[left] === b[left]) {\n    left++;\n  }\n  while (\n    right + left < aLength &&\n    right + left < bLength &&\n    a[aLength - right - 1] === b[bLength - right - 1]\n  ) {\n    right++;\n  }\n\n  return [left, aLength - left - right, b.slice(left, bLength - right)];\n}\n\nfunction setTextContent(\n  nextText: string,\n  dom: HTMLElement,\n  node: TextNode,\n): void {\n  const firstChild = dom.firstChild;\n  const isComposing = node.isComposing();\n  // Always add a suffix if we're composing a node\n  const suffix = isComposing ? COMPOSITION_SUFFIX : '';\n  const text: string = nextText + suffix;\n\n  if (firstChild == null) {\n    dom.textContent = text;\n  } else {\n    const nodeValue = firstChild.nodeValue;\n    if (nodeValue !== text) {\n      if (isComposing || IS_FIREFOX) {\n        // We also use the diff composed text for general text in FF to avoid\n        // the spellcheck red line from flickering.\n        const [index, remove, insert] = diffComposedText(\n          nodeValue as string,\n          text,\n        );\n        if (remove !== 0) {\n          // @ts-expect-error\n          firstChild.deleteData(index, remove);\n        }\n        // @ts-expect-error\n        firstChild.insertData(index, insert);\n      } else {\n        firstChild.nodeValue = text;\n      }\n    }\n  }\n}\n\nfunction createTextInnerDOM(\n  innerDOM: HTMLElement,\n  node: TextNode,\n  innerTag: string,\n  format: number,\n  text: string,\n  config: EditorConfig,\n): void {\n  setTextContent(text, innerDOM, node);\n  const theme = config.theme;\n  // Apply theme class names\n  const textClassNames = theme.text;\n\n  if (textClassNames !== undefined) {\n    setTextThemeClassNames(innerTag, 0, format, innerDOM, textClassNames);\n  }\n}\n\nfunction wrapElementWith(\n  element: HTMLElement | Text,\n  tag: string,\n): HTMLElement {\n  const el = document.createElement(tag);\n  el.appendChild(element);\n  return el;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging\nexport interface TextNode {\n  getTopLevelElement(): ElementNode | null;\n  getTopLevelElementOrThrow(): ElementNode;\n}\n\n/** @noInheritDoc */\n// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging\nexport class TextNode extends LexicalNode {\n  declare ['constructor']: KlassConstructor<typeof TextNode>;\n  __text: string;\n  /** @internal */\n  __format: number;\n  /** @internal */\n  __style: string;\n  /** @internal */\n  __mode: 0 | 1 | 2 | 3;\n  /** @internal */\n  __detail: number;\n\n  static getType(): string {\n    return 'text';\n  }\n\n  static clone(node: TextNode): TextNode {\n    return new TextNode(node.__text, node.__key);\n  }\n\n  afterCloneFrom(prevNode: this): void {\n    super.afterCloneFrom(prevNode);\n    this.__format = prevNode.__format;\n    this.__style = prevNode.__style;\n    this.__mode = prevNode.__mode;\n    this.__detail = prevNode.__detail;\n  }\n\n  constructor(text: string, key?: NodeKey) {\n    super(key);\n    this.__text = text;\n    this.__format = 0;\n    this.__style = '';\n    this.__mode = 0;\n    this.__detail = 0;\n  }\n\n  /**\n   * Returns a 32-bit integer that represents the TextFormatTypes currently applied to the\n   * TextNode. You probably don't want to use this method directly - consider using TextNode.hasFormat instead.\n   *\n   * @returns a number representing the format of the text node.\n   */\n  getFormat(): number {\n    const self = this.getLatest();\n    return self.__format;\n  }\n\n  /**\n   * Returns a 32-bit integer that represents the TextDetailTypes currently applied to the\n   * TextNode. You probably don't want to use this method directly - consider using TextNode.isDirectionless\n   * or TextNode.isUnmergeable instead.\n   *\n   * @returns a number representing the detail of the text node.\n   */\n  getDetail(): number {\n    const self = this.getLatest();\n    return self.__detail;\n  }\n\n  /**\n   * Returns the mode (TextModeType) of the TextNode, which may be \"normal\", \"token\", or \"segmented\"\n   *\n   * @returns TextModeType.\n   */\n  getMode(): TextModeType {\n    const self = this.getLatest();\n    return TEXT_TYPE_TO_MODE[self.__mode];\n  }\n\n  /**\n   * Returns the styles currently applied to the node. This is analogous to CSSText in the DOM.\n   *\n   * @returns CSSText-like string of styles applied to the underlying DOM node.\n   */\n  getStyle(): string {\n    const self = this.getLatest();\n    return self.__style;\n  }\n\n  /**\n   * Returns whether or not the node is in \"token\" mode. TextNodes in token mode can be navigated through character-by-character\n   * with a RangeSelection, but are deleted as a single entity (not invdividually by character).\n   *\n   * @returns true if the node is in token mode, false otherwise.\n   */\n  isToken(): boolean {\n    const self = this.getLatest();\n    return self.__mode === IS_TOKEN;\n  }\n\n  /**\n   *\n   * @returns true if Lexical detects that an IME or other 3rd-party script is attempting to\n   * mutate the TextNode, false otherwise.\n   */\n  isComposing(): boolean {\n    return this.__key === $getCompositionKey();\n  }\n\n  /**\n   * Returns whether or not the node is in \"segemented\" mode. TextNodes in segemented mode can be navigated through character-by-character\n   * with a RangeSelection, but are deleted in space-delimited \"segments\".\n   *\n   * @returns true if the node is in segmented mode, false otherwise.\n   */\n  isSegmented(): boolean {\n    const self = this.getLatest();\n    return self.__mode === IS_SEGMENTED;\n  }\n  /**\n   * Returns whether or not the node is \"directionless\". Directionless nodes don't respect changes between RTL and LTR modes.\n   *\n   * @returns true if the node is directionless, false otherwise.\n   */\n  isDirectionless(): boolean {\n    const self = this.getLatest();\n    return (self.__detail & IS_DIRECTIONLESS) !== 0;\n  }\n  /**\n   * Returns whether or not the node is unmergeable. In some scenarios, Lexical tries to merge\n   * adjacent TextNodes into a single TextNode. If a TextNode is unmergeable, this won't happen.\n   *\n   * @returns true if the node is unmergeable, false otherwise.\n   */\n  isUnmergeable(): boolean {\n    const self = this.getLatest();\n    return (self.__detail & IS_UNMERGEABLE) !== 0;\n  }\n\n  /**\n   * Returns whether or not the node has the provided format applied. Use this with the human-readable TextFormatType\n   * string values to get the format of a TextNode.\n   *\n   * @param type - the TextFormatType to check for.\n   *\n   * @returns true if the node has the provided format, false otherwise.\n   */\n  hasFormat(type: TextFormatType): boolean {\n    const formatFlag = TEXT_TYPE_TO_FORMAT[type];\n    return (this.getFormat() & formatFlag) !== 0;\n  }\n\n  /**\n   * Returns whether or not the node is simple text. Simple text is defined as a TextNode that has the string type \"text\"\n   * (i.e., not a subclass) and has no mode applied to it (i.e., not segmented or token).\n   *\n   * @returns true if the node is simple text, false otherwise.\n   */\n  isSimpleText(): boolean {\n    return this.__type === 'text' && this.__mode === 0;\n  }\n\n  /**\n   * Returns the text content of the node as a string.\n   *\n   * @returns a string representing the text content of the node.\n   */\n  getTextContent(): string {\n    const self = this.getLatest();\n    return self.__text;\n  }\n\n  /**\n   * Returns the format flags applied to the node as a 32-bit integer.\n   *\n   * @returns a number representing the TextFormatTypes applied to the node.\n   */\n  getFormatFlags(type: TextFormatType, alignWithFormat: null | number): number {\n    const self = this.getLatest();\n    const format = self.__format;\n    return toggleTextFormatType(format, type, alignWithFormat);\n  }\n\n  /**\n   *\n   * @returns true if the text node supports font styling, false otherwise.\n   */\n  canHaveFormat(): boolean {\n    return true;\n  }\n\n  // View\n\n  createDOM(config: EditorConfig, editor?: LexicalEditor): HTMLElement {\n    const format = this.__format;\n    const outerTag = getElementOuterTag(this, format);\n    const innerTag = getElementInnerTag(this, format);\n    const tag = outerTag === null ? innerTag : outerTag;\n    const dom = document.createElement(tag);\n    let innerDOM = dom;\n    if (this.hasFormat('code')) {\n      dom.setAttribute('spellcheck', 'false');\n    }\n    if (outerTag !== null) {\n      innerDOM = document.createElement(innerTag);\n      dom.appendChild(innerDOM);\n    }\n    const text = this.__text;\n    createTextInnerDOM(innerDOM, this, innerTag, format, text, config);\n    const style = this.__style;\n    if (style !== '') {\n      dom.style.cssText = style;\n    }\n    return dom;\n  }\n\n  updateDOM(\n    prevNode: TextNode,\n    dom: HTMLElement,\n    config: EditorConfig,\n  ): boolean {\n    const nextText = this.__text;\n    const prevFormat = prevNode.__format;\n    const nextFormat = this.__format;\n    const prevOuterTag = getElementOuterTag(this, prevFormat);\n    const nextOuterTag = getElementOuterTag(this, nextFormat);\n    const prevInnerTag = getElementInnerTag(this, prevFormat);\n    const nextInnerTag = getElementInnerTag(this, nextFormat);\n    const prevTag = prevOuterTag === null ? prevInnerTag : prevOuterTag;\n    const nextTag = nextOuterTag === null ? nextInnerTag : nextOuterTag;\n\n    if (prevTag !== nextTag) {\n      return true;\n    }\n    if (prevOuterTag === nextOuterTag && prevInnerTag !== nextInnerTag) {\n      // should always be an element\n      const prevInnerDOM: HTMLElement = dom.firstChild as HTMLElement;\n      if (prevInnerDOM == null) {\n        invariant(false, 'updateDOM: prevInnerDOM is null or undefined');\n      }\n      const nextInnerDOM = document.createElement(nextInnerTag);\n      createTextInnerDOM(\n        nextInnerDOM,\n        this,\n        nextInnerTag,\n        nextFormat,\n        nextText,\n        config,\n      );\n      dom.replaceChild(nextInnerDOM, prevInnerDOM);\n      return false;\n    }\n    let innerDOM = dom;\n    if (nextOuterTag !== null) {\n      if (prevOuterTag !== null) {\n        innerDOM = dom.firstChild as HTMLElement;\n        if (innerDOM == null) {\n          invariant(false, 'updateDOM: innerDOM is null or undefined');\n        }\n      }\n    }\n    setTextContent(nextText, innerDOM, this);\n    const theme = config.theme;\n    // Apply theme class names\n    const textClassNames = theme.text;\n\n    if (textClassNames !== undefined && prevFormat !== nextFormat) {\n      setTextThemeClassNames(\n        nextInnerTag,\n        prevFormat,\n        nextFormat,\n        innerDOM,\n        textClassNames,\n      );\n    }\n    const prevStyle = prevNode.__style;\n    const nextStyle = this.__style;\n    if (prevStyle !== nextStyle) {\n      dom.style.cssText = nextStyle;\n    }\n    return false;\n  }\n\n  static importDOM(): DOMConversionMap | null {\n    return {\n      '#text': () => ({\n        conversion: $convertTextDOMNode,\n        priority: 0,\n      }),\n      b: () => ({\n        conversion: convertBringAttentionToElement,\n        priority: 0,\n      }),\n      code: () => ({\n        conversion: convertTextFormatElement,\n        priority: 0,\n      }),\n      em: () => ({\n        conversion: convertTextFormatElement,\n        priority: 0,\n      }),\n      i: () => ({\n        conversion: convertTextFormatElement,\n        priority: 0,\n      }),\n      s: () => ({\n        conversion: convertTextFormatElement,\n        priority: 0,\n      }),\n      span: () => ({\n        conversion: convertSpanElement,\n        priority: 0,\n      }),\n      strong: () => ({\n        conversion: convertTextFormatElement,\n        priority: 0,\n      }),\n      sub: () => ({\n        conversion: convertTextFormatElement,\n        priority: 0,\n      }),\n      sup: () => ({\n        conversion: convertTextFormatElement,\n        priority: 0,\n      }),\n      u: () => ({\n        conversion: convertTextFormatElement,\n        priority: 0,\n      }),\n    };\n  }\n\n  static importJSON(serializedNode: SerializedTextNode): TextNode {\n    const node = $createTextNode(serializedNode.text);\n    node.setFormat(serializedNode.format);\n    node.setDetail(serializedNode.detail);\n    node.setMode(serializedNode.mode);\n    node.setStyle(serializedNode.style);\n    return node;\n  }\n\n  // This improves Lexical's basic text output in copy+paste plus\n  // for headless mode where people might use Lexical to generate\n  // HTML content and not have the ability to use CSS classes.\n  exportDOM(editor: LexicalEditor): DOMExportOutput {\n    let {element} = super.exportDOM(editor);\n    const originalElementName = (element?.nodeName || '').toLowerCase()\n    invariant(\n      element !== null && isHTMLElement(element),\n      'Expected TextNode createDOM to always return a HTMLElement',\n    );\n\n    // Wrap up to retain space if head/tail whitespace exists\n    const text = this.getTextContent();\n    if (/^\\s|\\s$/.test(text)) {\n      element.style.whiteSpace = 'pre-wrap';\n    }\n\n    // Strip editor theme classes\n    for (const className of Array.from(element.classList.values())) {\n      if (className.startsWith('editor-theme-')) {\n        element.classList.remove(className);\n      }\n    }\n    if (element.classList.length === 0) {\n      element.removeAttribute('class');\n    }\n\n    // Remove placeholder tag if redundant\n    if (element.nodeName === 'SPAN' && !element.getAttribute('style')) {\n      element = document.createTextNode(text);\n    }\n\n    // This is the only way to properly add support for most clients,\n    // even if it's semantically incorrect to have to resort to using\n    // <b>, <u>, <s>, <i> elements.\n    if (this.hasFormat('bold') && originalElementName !== 'strong') {\n      element = wrapElementWith(element, 'strong');\n    }\n    if (this.hasFormat('italic')) {\n      element = wrapElementWith(element, 'em');\n    }\n    if (this.hasFormat('strikethrough')) {\n      element = wrapElementWith(element, 's');\n    }\n    if (this.hasFormat('underline')) {\n      element = wrapElementWith(element, 'u');\n    }\n\n    return {\n      element,\n    };\n  }\n\n  exportJSON(): SerializedTextNode {\n    return {\n      detail: this.getDetail(),\n      format: this.getFormat(),\n      mode: this.getMode(),\n      style: this.getStyle(),\n      text: this.getTextContent(),\n      type: 'text',\n      version: 1,\n    };\n  }\n\n  // Mutators\n  selectionTransform(\n    prevSelection: null | BaseSelection,\n    nextSelection: RangeSelection,\n  ): void {\n    return;\n  }\n\n  /**\n   * Sets the node format to the provided TextFormatType or 32-bit integer. Note that the TextFormatType\n   * version of the argument can only specify one format and doing so will remove all other formats that\n   * may be applied to the node. For toggling behavior, consider using {@link TextNode.toggleFormat}\n   *\n   * @param format - TextFormatType or 32-bit integer representing the node format.\n   *\n   * @returns this TextNode.\n   * // TODO 0.12 This should just be a `string`.\n   */\n  setFormat(format: TextFormatType | number): this {\n    const self = this.getWritable();\n    self.__format =\n      typeof format === 'string' ? TEXT_TYPE_TO_FORMAT[format] : format;\n    return self;\n  }\n\n  /**\n   * Sets the node detail to the provided TextDetailType or 32-bit integer. Note that the TextDetailType\n   * version of the argument can only specify one detail value and doing so will remove all other detail values that\n   * may be applied to the node. For toggling behavior, consider using {@link TextNode.toggleDirectionless}\n   * or {@link TextNode.toggleUnmergeable}\n   *\n   * @param detail - TextDetailType or 32-bit integer representing the node detail.\n   *\n   * @returns this TextNode.\n   * // TODO 0.12 This should just be a `string`.\n   */\n  setDetail(detail: TextDetailType | number): this {\n    const self = this.getWritable();\n    self.__detail =\n      typeof detail === 'string' ? DETAIL_TYPE_TO_DETAIL[detail] : detail;\n    return self;\n  }\n\n  /**\n   * Sets the node style to the provided CSSText-like string. Set this property as you\n   * would an HTMLElement style attribute to apply inline styles to the underlying DOM Element.\n   *\n   * @param style - CSSText to be applied to the underlying HTMLElement.\n   *\n   * @returns this TextNode.\n   */\n  setStyle(style: string): this {\n    const self = this.getWritable();\n    self.__style = style;\n    return self;\n  }\n\n  /**\n   * Applies the provided format to this TextNode if it's not present. Removes it if it's present.\n   * The subscript and superscript formats are mutually exclusive.\n   * Prefer using this method to turn specific formats on and off.\n   *\n   * @param type - TextFormatType to toggle.\n   *\n   * @returns this TextNode.\n   */\n  toggleFormat(type: TextFormatType): this {\n    const format = this.getFormat();\n    const newFormat = toggleTextFormatType(format, type, null);\n    return this.setFormat(newFormat);\n  }\n\n  /**\n   * Toggles the directionless detail value of the node. Prefer using this method over setDetail.\n   *\n   * @returns this TextNode.\n   */\n  toggleDirectionless(): this {\n    const self = this.getWritable();\n    self.__detail ^= IS_DIRECTIONLESS;\n    return self;\n  }\n\n  /**\n   * Toggles the unmergeable detail value of the node. Prefer using this method over setDetail.\n   *\n   * @returns this TextNode.\n   */\n  toggleUnmergeable(): this {\n    const self = this.getWritable();\n    self.__detail ^= IS_UNMERGEABLE;\n    return self;\n  }\n\n  /**\n   * Sets the mode of the node.\n   *\n   * @returns this TextNode.\n   */\n  setMode(type: TextModeType): this {\n    const mode = TEXT_MODE_TO_TYPE[type];\n    if (this.__mode === mode) {\n      return this;\n    }\n    const self = this.getWritable();\n    self.__mode = mode;\n    return self;\n  }\n\n  /**\n   * Sets the text content of the node.\n   *\n   * @param text - the string to set as the text value of the node.\n   *\n   * @returns this TextNode.\n   */\n  setTextContent(text: string): this {\n    if (this.__text === text) {\n      return this;\n    }\n    const self = this.getWritable();\n    self.__text = text;\n    return self;\n  }\n\n  /**\n   * Sets the current Lexical selection to be a RangeSelection with anchor and focus on this TextNode at the provided offsets.\n   *\n   * @param _anchorOffset - the offset at which the Selection anchor will be placed.\n   * @param _focusOffset - the offset at which the Selection focus will be placed.\n   *\n   * @returns the new RangeSelection.\n   */\n  select(_anchorOffset?: number, _focusOffset?: number): RangeSelection {\n    errorOnReadOnly();\n    let anchorOffset = _anchorOffset;\n    let focusOffset = _focusOffset;\n    const selection = $getSelection();\n    const text = this.getTextContent();\n    const key = this.__key;\n    if (typeof text === 'string') {\n      const lastOffset = text.length;\n      if (anchorOffset === undefined) {\n        anchorOffset = lastOffset;\n      }\n      if (focusOffset === undefined) {\n        focusOffset = lastOffset;\n      }\n    } else {\n      anchorOffset = 0;\n      focusOffset = 0;\n    }\n    if (!$isRangeSelection(selection)) {\n      return $internalMakeRangeSelection(\n        key,\n        anchorOffset,\n        key,\n        focusOffset,\n        'text',\n        'text',\n      );\n    } else {\n      const compositionKey = $getCompositionKey();\n      if (\n        compositionKey === selection.anchor.key ||\n        compositionKey === selection.focus.key\n      ) {\n        $setCompositionKey(key);\n      }\n      selection.setTextNodeRange(this, anchorOffset, this, focusOffset);\n    }\n    return selection;\n  }\n\n  selectStart(): RangeSelection {\n    return this.select(0, 0);\n  }\n\n  selectEnd(): RangeSelection {\n    const size = this.getTextContentSize();\n    return this.select(size, size);\n  }\n\n  /**\n   * Inserts the provided text into this TextNode at the provided offset, deleting the number of characters\n   * specified. Can optionally calculate a new selection after the operation is complete.\n   *\n   * @param offset - the offset at which the splice operation should begin.\n   * @param delCount - the number of characters to delete, starting from the offset.\n   * @param newText - the text to insert into the TextNode at the offset.\n   * @param moveSelection - optional, whether or not to move selection to the end of the inserted substring.\n   *\n   * @returns this TextNode.\n   */\n  spliceText(\n    offset: number,\n    delCount: number,\n    newText: string,\n    moveSelection?: boolean,\n  ): TextNode {\n    const writableSelf = this.getWritable();\n    const text = writableSelf.__text;\n    const handledTextLength = newText.length;\n    let index = offset;\n    if (index < 0) {\n      index = handledTextLength + index;\n      if (index < 0) {\n        index = 0;\n      }\n    }\n    const selection = $getSelection();\n    if (moveSelection && $isRangeSelection(selection)) {\n      const newOffset = offset + handledTextLength;\n      selection.setTextNodeRange(\n        writableSelf,\n        newOffset,\n        writableSelf,\n        newOffset,\n      );\n    }\n\n    const updatedText =\n      text.slice(0, index) + newText + text.slice(index + delCount);\n\n    writableSelf.__text = updatedText;\n    return writableSelf;\n  }\n\n  /**\n   * This method is meant to be overriden by TextNode subclasses to control the behavior of those nodes\n   * when a user event would cause text to be inserted before them in the editor. If true, Lexical will attempt\n   * to insert text into this node. If false, it will insert the text in a new sibling node.\n   *\n   * @returns true if text can be inserted before the node, false otherwise.\n   */\n  canInsertTextBefore(): boolean {\n    return true;\n  }\n\n  /**\n   * This method is meant to be overriden by TextNode subclasses to control the behavior of those nodes\n   * when a user event would cause text to be inserted after them in the editor. If true, Lexical will attempt\n   * to insert text into this node. If false, it will insert the text in a new sibling node.\n   *\n   * @returns true if text can be inserted after the node, false otherwise.\n   */\n  canInsertTextAfter(): boolean {\n    return true;\n  }\n\n  /**\n   * Splits this TextNode at the provided character offsets, forming new TextNodes from the substrings\n   * formed by the split, and inserting those new TextNodes into the editor, replacing the one that was split.\n   *\n   * @param splitOffsets - rest param of the text content character offsets at which this node should be split.\n   *\n   * @returns an Array containing the newly-created TextNodes.\n   */\n  splitText(...splitOffsets: Array<number>): Array<TextNode> {\n    errorOnReadOnly();\n    const self = this.getLatest();\n    const textContent = self.getTextContent();\n    const key = self.__key;\n    const compositionKey = $getCompositionKey();\n    const offsetsSet = new Set(splitOffsets);\n    const parts = [];\n    const textLength = textContent.length;\n    let string = '';\n    for (let i = 0; i < textLength; i++) {\n      if (string !== '' && offsetsSet.has(i)) {\n        parts.push(string);\n        string = '';\n      }\n      string += textContent[i];\n    }\n    if (string !== '') {\n      parts.push(string);\n    }\n    const partsLength = parts.length;\n    if (partsLength === 0) {\n      return [];\n    } else if (parts[0] === textContent) {\n      return [self];\n    }\n    const firstPart = parts[0];\n    const parent = self.getParent();\n    let writableNode;\n    const format = self.getFormat();\n    const style = self.getStyle();\n    const detail = self.__detail;\n    let hasReplacedSelf = false;\n\n    if (self.isSegmented()) {\n      // Create a new TextNode\n      writableNode = $createTextNode(firstPart);\n      writableNode.__format = format;\n      writableNode.__style = style;\n      writableNode.__detail = detail;\n      hasReplacedSelf = true;\n    } else {\n      // For the first part, update the existing node\n      writableNode = self.getWritable();\n      writableNode.__text = firstPart;\n    }\n\n    // Handle selection\n    const selection = $getSelection();\n\n    // Then handle all other parts\n    const splitNodes: TextNode[] = [writableNode];\n    let textSize = firstPart.length;\n\n    for (let i = 1; i < partsLength; i++) {\n      const part = parts[i];\n      const partSize = part.length;\n      const sibling = $createTextNode(part).getWritable();\n      sibling.__format = format;\n      sibling.__style = style;\n      sibling.__detail = detail;\n      const siblingKey = sibling.__key;\n      const nextTextSize = textSize + partSize;\n\n      if ($isRangeSelection(selection)) {\n        const anchor = selection.anchor;\n        const focus = selection.focus;\n\n        if (\n          anchor.key === key &&\n          anchor.type === 'text' &&\n          anchor.offset > textSize &&\n          anchor.offset <= nextTextSize\n        ) {\n          anchor.key = siblingKey;\n          anchor.offset -= textSize;\n          selection.dirty = true;\n        }\n        if (\n          focus.key === key &&\n          focus.type === 'text' &&\n          focus.offset > textSize &&\n          focus.offset <= nextTextSize\n        ) {\n          focus.key = siblingKey;\n          focus.offset -= textSize;\n          selection.dirty = true;\n        }\n      }\n      if (compositionKey === key) {\n        $setCompositionKey(siblingKey);\n      }\n      textSize = nextTextSize;\n      splitNodes.push(sibling);\n    }\n\n    // Insert the nodes into the parent's children\n    if (parent !== null) {\n      internalMarkSiblingsAsDirty(this);\n      const writableParent = parent.getWritable();\n      const insertionIndex = this.getIndexWithinParent();\n      if (hasReplacedSelf) {\n        writableParent.splice(insertionIndex, 0, splitNodes);\n        this.remove();\n      } else {\n        writableParent.splice(insertionIndex, 1, splitNodes);\n      }\n\n      if ($isRangeSelection(selection)) {\n        $updateElementSelectionOnCreateDeleteNode(\n          selection,\n          parent,\n          insertionIndex,\n          partsLength - 1,\n        );\n      }\n    }\n\n    return splitNodes;\n  }\n\n  /**\n   * Merges the target TextNode into this TextNode, removing the target node.\n   *\n   * @param target - the TextNode to merge into this one.\n   *\n   * @returns this TextNode.\n   */\n  mergeWithSibling(target: TextNode): TextNode {\n    const isBefore = target === this.getPreviousSibling();\n    if (!isBefore && target !== this.getNextSibling()) {\n      invariant(\n        false,\n        'mergeWithSibling: sibling must be a previous or next sibling',\n      );\n    }\n    const key = this.__key;\n    const targetKey = target.__key;\n    const text = this.__text;\n    const textLength = text.length;\n    const compositionKey = $getCompositionKey();\n\n    if (compositionKey === targetKey) {\n      $setCompositionKey(key);\n    }\n    const selection = $getSelection();\n    if ($isRangeSelection(selection)) {\n      const anchor = selection.anchor;\n      const focus = selection.focus;\n      if (anchor !== null && anchor.key === targetKey) {\n        adjustPointOffsetForMergedSibling(\n          anchor,\n          isBefore,\n          key,\n          target,\n          textLength,\n        );\n        selection.dirty = true;\n      }\n      if (focus !== null && focus.key === targetKey) {\n        adjustPointOffsetForMergedSibling(\n          focus,\n          isBefore,\n          key,\n          target,\n          textLength,\n        );\n        selection.dirty = true;\n      }\n    }\n    const targetText = target.__text;\n    const newText = isBefore ? targetText + text : text + targetText;\n    this.setTextContent(newText);\n    const writableSelf = this.getWritable();\n    target.remove();\n    return writableSelf;\n  }\n\n  /**\n   * This method is meant to be overriden by TextNode subclasses to control the behavior of those nodes\n   * when used with the registerLexicalTextEntity function. If you're using registerLexicalTextEntity, the\n   * node class that you create and replace matched text with should return true from this method.\n   *\n   * @returns true if the node is to be treated as a \"text entity\", false otherwise.\n   */\n  isTextEntity(): boolean {\n    return false;\n  }\n}\n\nfunction convertSpanElement(domNode: HTMLSpanElement): DOMConversionOutput {\n  // domNode is a <span> since we matched it by nodeName\n  const span = domNode;\n  const style = span.style;\n\n  return {\n    forChild: applyTextFormatFromStyle(style),\n    node: null,\n  };\n}\n\nfunction convertBringAttentionToElement(\n  domNode: HTMLElement,\n): DOMConversionOutput {\n  // domNode is a <b> since we matched it by nodeName\n  const b = domNode;\n  // Google Docs wraps all copied HTML in a <b> with font-weight normal\n  const hasNormalFontWeight = b.style.fontWeight === 'normal';\n\n  return {\n    forChild: applyTextFormatFromStyle(\n      b.style,\n      hasNormalFontWeight ? undefined : 'bold',\n    ),\n    node: null,\n  };\n}\n\nconst preParentCache = new WeakMap<Node, null | Node>();\n\nfunction isNodePre(node: Node): boolean {\n  return (\n    node.nodeName === 'PRE' ||\n    (node.nodeType === DOM_ELEMENT_TYPE &&\n      (node as HTMLElement).style !== undefined &&\n      (node as HTMLElement).style.whiteSpace !== undefined &&\n      (node as HTMLElement).style.whiteSpace.startsWith('pre'))\n  );\n}\n\nexport function findParentPreDOMNode(node: Node) {\n  let cached;\n  let parent = node.parentNode;\n  const visited = [node];\n  while (\n    parent !== null &&\n    (cached = preParentCache.get(parent)) === undefined &&\n    !isNodePre(parent)\n  ) {\n    visited.push(parent);\n    parent = parent.parentNode;\n  }\n  const resultNode = cached === undefined ? parent : cached;\n  for (let i = 0; i < visited.length; i++) {\n    preParentCache.set(visited[i], resultNode);\n  }\n  return resultNode;\n}\n\nfunction $convertTextDOMNode(domNode: Node): DOMConversionOutput {\n  const domNode_ = domNode as Text;\n  const parentDom = domNode.parentElement;\n  invariant(\n    parentDom !== null,\n    'Expected parentElement of Text not to be null',\n  );\n  let textContent = domNode_.textContent || '';\n  // No collapse and preserve segment break for pre, pre-wrap and pre-line\n  if (findParentPreDOMNode(domNode_) !== null) {\n    const parts = textContent.split(/(\\r?\\n|\\t)/);\n    const nodes: Array<LexicalNode> = [];\n    const length = parts.length;\n    for (let i = 0; i < length; i++) {\n      const part = parts[i];\n      if (part === '\\n' || part === '\\r\\n') {\n        nodes.push($createLineBreakNode());\n      } else if (part === '\\t') {\n        nodes.push($createTabNode());\n      } else if (part !== '') {\n        nodes.push($createTextNode(part));\n      }\n    }\n    return {node: nodes};\n  }\n  textContent = textContent.replace(/\\r/g, '').replace(/[ \\t\\n]+/g, ' ');\n  if (textContent === '') {\n    return {node: null};\n  }\n  if (textContent[0] === ' ') {\n    // Traverse backward while in the same line. If content contains new line or tab -> pontential\n    // delete, other elements can borrow from this one. Deletion depends on whether it's also the\n    // last space (see next condition: textContent[textContent.length - 1] === ' '))\n    let previousText: null | Text = domNode_;\n    let isStartOfLine = true;\n    while (\n      previousText !== null &&\n      (previousText = findTextInLine(previousText, false)) !== null\n    ) {\n      const previousTextContent = previousText.textContent || '';\n      if (previousTextContent.length > 0) {\n        if (/[ \\t\\n]$/.test(previousTextContent)) {\n          textContent = textContent.slice(1);\n        }\n        isStartOfLine = false;\n        break;\n      }\n    }\n    if (isStartOfLine) {\n      textContent = textContent.slice(1);\n    }\n  }\n  if (textContent[textContent.length - 1] === ' ') {\n    // Traverse forward while in the same line, preserve if next inline will require a space\n    let nextText: null | Text = domNode_;\n    let isEndOfLine = true;\n    while (\n      nextText !== null &&\n      (nextText = findTextInLine(nextText, true)) !== null\n    ) {\n      const nextTextContent = (nextText.textContent || '').replace(\n        /^( |\\t|\\r?\\n)+/,\n        '',\n      );\n      if (nextTextContent.length > 0) {\n        isEndOfLine = false;\n        break;\n      }\n    }\n    if (isEndOfLine) {\n      textContent = textContent.slice(0, textContent.length - 1);\n    }\n  }\n  if (textContent === '') {\n    return {node: null};\n  }\n  return {node: $createTextNode(textContent)};\n}\n\nfunction findTextInLine(text: Text, forward: boolean): null | Text {\n  let node: Node = text;\n  // eslint-disable-next-line no-constant-condition\n  while (true) {\n    let sibling: null | Node;\n    while (\n      (sibling = forward ? node.nextSibling : node.previousSibling) === null\n    ) {\n      const parentElement = node.parentElement;\n      if (parentElement === null) {\n        return null;\n      }\n      node = parentElement;\n    }\n    node = sibling;\n    if (node.nodeType === DOM_ELEMENT_TYPE) {\n      const display = (node as HTMLElement).style.display;\n      if (\n        (display === '' && !isInlineDomNode(node)) ||\n        (display !== '' && !display.startsWith('inline'))\n      ) {\n        return null;\n      }\n    }\n    let descendant: null | Node = node;\n    while ((descendant = forward ? node.firstChild : node.lastChild) !== null) {\n      node = descendant;\n    }\n    if (node.nodeType === DOM_TEXT_TYPE) {\n      return node as Text;\n    } else if (node.nodeName === 'BR') {\n      return null;\n    }\n  }\n}\n\nconst nodeNameToTextFormat: Record<string, TextFormatType> = {\n  code: 'code',\n  em: 'italic',\n  i: 'italic',\n  s: 'strikethrough',\n  strong: 'bold',\n  sub: 'subscript',\n  sup: 'superscript',\n  u: 'underline',\n};\n\nfunction convertTextFormatElement(domNode: HTMLElement): DOMConversionOutput {\n  const format = nodeNameToTextFormat[domNode.nodeName.toLowerCase()];\n\n  if (format === 'code' && domNode.closest('pre')) {\n    return {node: null};\n  }\n\n  if (format === undefined) {\n    return {node: null};\n  }\n  return {\n    forChild: applyTextFormatFromStyle(domNode.style, format),\n    node: null,\n  };\n}\n\nexport function $createTextNode(text = ''): TextNode {\n  return $applyNodeReplacement(new TextNode(text));\n}\n\nexport function $isTextNode(\n  node: LexicalNode | null | undefined,\n): node is TextNode {\n  return node instanceof TextNode;\n}\n\nfunction applyTextFormatFromStyle(\n  style: CSSStyleDeclaration,\n  shouldApply?: TextFormatType,\n) {\n  const fontWeight = style.fontWeight;\n  const textDecoration = style.textDecoration.split(' ');\n  // Google Docs uses span tags + font-weight for bold text\n  const hasBoldFontWeight = fontWeight === '700' || fontWeight === 'bold';\n  // Google Docs uses span tags + text-decoration: line-through for strikethrough text\n  const hasLinethroughTextDecoration = textDecoration.includes('line-through');\n  // Google Docs uses span tags + font-style for italic text\n  const hasItalicFontStyle = style.fontStyle === 'italic';\n  // Google Docs uses span tags + text-decoration: underline for underline text\n  const hasUnderlineTextDecoration = textDecoration.includes('underline');\n  // Google Docs uses span tags + vertical-align to specify subscript and superscript\n  const verticalAlign = style.verticalAlign;\n\n  // Styles to copy to node\n  const color = style.color;\n  const backgroundColor = style.backgroundColor;\n\n  return (lexicalNode: LexicalNode) => {\n    if (!$isTextNode(lexicalNode)) {\n      return lexicalNode;\n    }\n    if (hasBoldFontWeight && !lexicalNode.hasFormat('bold')) {\n      lexicalNode.toggleFormat('bold');\n    }\n    if (\n      hasLinethroughTextDecoration &&\n      !lexicalNode.hasFormat('strikethrough')\n    ) {\n      lexicalNode.toggleFormat('strikethrough');\n    }\n    if (hasItalicFontStyle && !lexicalNode.hasFormat('italic')) {\n      lexicalNode.toggleFormat('italic');\n    }\n    if (hasUnderlineTextDecoration && !lexicalNode.hasFormat('underline')) {\n      lexicalNode.toggleFormat('underline');\n    }\n    if (verticalAlign === 'sub' && !lexicalNode.hasFormat('subscript')) {\n      lexicalNode.toggleFormat('subscript');\n    }\n    if (verticalAlign === 'super' && !lexicalNode.hasFormat('superscript')) {\n      lexicalNode.toggleFormat('superscript');\n    }\n\n    // Apply styles\n    let style = lexicalNode.getStyle();\n    if (color) {\n      style += `color: ${color};`;\n    }\n    if (backgroundColor && backgroundColor !== 'transparent') {\n      style += `background-color: ${backgroundColor};`;\n    }\n    if (style) {\n      lexicalNode.setStyle(style);\n    }\n\n    if (shouldApply && !lexicalNode.hasFormat(shouldApply)) {\n      lexicalNode.toggleFormat(shouldApply);\n    }\n\n    return lexicalNode;\n  };\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalElementNode.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {\n  $createTextNode,\n  $getRoot,\n  $getSelection,\n  $isRangeSelection,\n  ElementNode,\n  LexicalEditor,\n  LexicalNode,\n  TextNode,\n} from 'lexical';\n\nimport {\n    $createTestElementNode,\n    createTestEditor, patchRange,\n} from '../../../__tests__/utils';\n\npatchRange();\n\ndescribe('LexicalElementNode tests', () => {\n  let container: HTMLElement;\n\n  beforeEach(async () => {\n    container = document.createElement('div');\n    document.body.appendChild(container);\n\n    await init();\n  });\n\n  afterEach(() => {\n    document.body.removeChild(container);\n    // @ts-ignore\n    container = null;\n  });\n\n  async function update(fn: () => void) {\n    editor.update(fn);\n    editor.commitUpdates();\n    return Promise.resolve().then();\n  }\n\n  let editor: LexicalEditor;\n\n  async function init() {\n    const root = document.createElement('div');\n    root.setAttribute('contenteditable', 'true');\n    container.innerHTML = '';\n    container.appendChild(root);\n\n    editor = createTestEditor();\n    editor.setRootElement(root);\n    root.focus();\n\n    // Insert initial block\n    await update(() => {\n      const block = $createTestElementNode();\n      const text = $createTextNode('Foo');\n      const text2 = $createTextNode('Bar');\n      // Prevent text nodes from combining.\n      text2.setMode('segmented');\n      const text3 = $createTextNode('Baz');\n      block.append(text, text2, text3);\n      $getRoot().append(block);\n        // Some operations require a selection to exist, hence\n        // we make a selection in the setup code.\n        text.select(0, 0);\n    });\n  }\n\n  describe('exportJSON()', () => {\n    test('should return and object conforming to the expected schema', async () => {\n      await update(() => {\n        const node = $createTestElementNode();\n\n        // If you broke this test, you changed the public interface of a\n        // serialized Lexical Core Node. Please ensure the correct adapter\n        // logic is in place in the corresponding importJSON  method\n        // to accomodate these changes.\n\n        expect(node.exportJSON()).toStrictEqual({\n          children: [],\n          direction: null,\n          type: 'test_block',\n          version: 1,\n        });\n      });\n    });\n  });\n\n  describe('getChildren()', () => {\n    test('no children', async () => {\n      await update(() => {\n        const block = $createTestElementNode();\n        const children = block.getChildren();\n        expect(children).toHaveLength(0);\n        expect(children).toEqual([]);\n      });\n    });\n\n    test('some children', async () => {\n      await update(() => {\n        const children = $getRoot().getFirstChild<ElementNode>()!.getChildren();\n        expect(children).toHaveLength(3);\n      });\n    });\n  });\n\n  describe('getAllTextNodes()', () => {\n    test('basic', async () => {\n      await update(() => {\n        const textNodes = $getRoot()\n          .getFirstChild<ElementNode>()!\n          .getAllTextNodes();\n        expect(textNodes).toHaveLength(3);\n      });\n    });\n\n    test('nested', async () => {\n      await update(() => {\n        const block = $createTestElementNode();\n        const innerBlock = $createTestElementNode();\n        const text = $createTextNode('Foo');\n        text.select(0, 0);\n        const text2 = $createTextNode('Bar');\n        const text3 = $createTextNode('Baz');\n        const text4 = $createTextNode('Qux');\n        block.append(text, innerBlock, text4);\n        innerBlock.append(text2, text3);\n        const children = block.getAllTextNodes();\n\n        expect(children).toHaveLength(4);\n        expect(children).toEqual([text, text2, text3, text4]);\n\n        const innerInnerBlock = $createTestElementNode();\n        const text5 = $createTextNode('More');\n        const text6 = $createTextNode('Stuff');\n        innerInnerBlock.append(text5, text6);\n        innerBlock.append(innerInnerBlock);\n        const children2 = block.getAllTextNodes();\n\n        expect(children2).toHaveLength(6);\n        expect(children2).toEqual([text, text2, text3, text5, text6, text4]);\n\n        $getRoot().append(block);\n      });\n    });\n  });\n\n  describe('getFirstChild()', () => {\n    test('basic', async () => {\n      await update(() => {\n        expect(\n          $getRoot()\n            .getFirstChild<ElementNode>()!\n            .getFirstChild()!\n            .getTextContent(),\n        ).toBe('Foo');\n      });\n    });\n\n    test('empty', async () => {\n      await update(() => {\n        const block = $createTestElementNode();\n        expect(block.getFirstChild()).toBe(null);\n      });\n    });\n  });\n\n  describe('getLastChild()', () => {\n    test('basic', async () => {\n      await update(() => {\n        expect(\n          $getRoot()\n            .getFirstChild<ElementNode>()!\n            .getLastChild()!\n            .getTextContent(),\n        ).toBe('Baz');\n      });\n    });\n\n    test('empty', async () => {\n      await update(() => {\n        const block = $createTestElementNode();\n        expect(block.getLastChild()).toBe(null);\n      });\n    });\n  });\n\n  describe('getTextContent()', () => {\n    test('basic', async () => {\n      await update(() => {\n        expect($getRoot().getFirstChild()!.getTextContent()).toBe('FooBarBaz');\n      });\n    });\n\n    test('empty', async () => {\n      await update(() => {\n        const block = $createTestElementNode();\n        expect(block.getTextContent()).toBe('');\n      });\n    });\n\n    test('nested', async () => {\n      await update(() => {\n        const block = $createTestElementNode();\n        const innerBlock = $createTestElementNode();\n        const text = $createTextNode('Foo');\n        text.select(0, 0);\n        const text2 = $createTextNode('Bar');\n        const text3 = $createTextNode('Baz');\n        text3.setMode('token');\n        const text4 = $createTextNode('Qux');\n        block.append(text, innerBlock, text4);\n        innerBlock.append(text2, text3);\n\n        expect(block.getTextContent()).toEqual('FooBarBaz\\n\\nQux');\n\n        const innerInnerBlock = $createTestElementNode();\n        const text5 = $createTextNode('More');\n        text5.setMode('token');\n        const text6 = $createTextNode('Stuff');\n        innerInnerBlock.append(text5, text6);\n        innerBlock.append(innerInnerBlock);\n\n        expect(block.getTextContent()).toEqual('FooBarBazMoreStuff\\n\\nQux');\n\n        $getRoot().append(block);\n      });\n    });\n  });\n\n  describe('getTextContentSize()', () => {\n    test('basic', async () => {\n      await update(() => {\n        expect($getRoot().getFirstChild()!.getTextContentSize()).toBe(\n          $getRoot().getFirstChild()!.getTextContent().length,\n        );\n      });\n    });\n\n    test('child node getTextContentSize() can be overridden and is then reflected when calling the same method on parent node', async () => {\n      await update(() => {\n        const block = $createTestElementNode();\n        const text = $createTextNode('Foo');\n        text.getTextContentSize = () => 1;\n        block.append(text);\n\n        expect(block.getTextContentSize()).toBe(1);\n      });\n    });\n  });\n\n  describe('splice', () => {\n    let block: ElementNode;\n\n    beforeEach(async () => {\n      await update(() => {\n        block = $getRoot().getFirstChildOrThrow();\n      });\n    });\n\n    const BASE_INSERTIONS: Array<{\n      deleteCount: number;\n      deleteOnly: boolean | null | undefined;\n      expectedText: string;\n      name: string;\n      start: number;\n    }> = [\n      // Do nothing\n      {\n        deleteCount: 0,\n        deleteOnly: true,\n        expectedText: 'FooBarBaz',\n        name: 'Do nothing',\n        start: 0,\n      },\n      // Insert\n      {\n        deleteCount: 0,\n        deleteOnly: false,\n        expectedText: 'QuxQuuzFooBarBaz',\n        name: 'Insert in the beginning',\n        start: 0,\n      },\n      {\n        deleteCount: 0,\n        deleteOnly: false,\n        expectedText: 'FooQuxQuuzBarBaz',\n        name: 'Insert in the middle',\n        start: 1,\n      },\n      {\n        deleteCount: 0,\n        deleteOnly: false,\n        expectedText: 'FooBarBazQuxQuuz',\n        name: 'Insert in the end',\n        start: 3,\n      },\n      // Delete\n      {\n        deleteCount: 1,\n        deleteOnly: true,\n        expectedText: 'BarBaz',\n        name: 'Delete in the beginning',\n        start: 0,\n      },\n      {\n        deleteCount: 1,\n        deleteOnly: true,\n        expectedText: 'FooBaz',\n        name: 'Delete in the middle',\n        start: 1,\n      },\n      {\n        deleteCount: 1,\n        deleteOnly: true,\n        expectedText: 'FooBar',\n        name: 'Delete in the end',\n        start: 2,\n      },\n      {\n        deleteCount: 3,\n        deleteOnly: true,\n        expectedText: '',\n        name: 'Delete all',\n        start: 0,\n      },\n      // Replace\n      {\n        deleteCount: 1,\n        deleteOnly: false,\n        expectedText: 'QuxQuuzBarBaz',\n        name: 'Replace in the beginning',\n        start: 0,\n      },\n      {\n        deleteCount: 1,\n        deleteOnly: false,\n        expectedText: 'FooQuxQuuzBaz',\n        name: 'Replace in the middle',\n        start: 1,\n      },\n      {\n        deleteCount: 1,\n        deleteOnly: false,\n        expectedText: 'FooBarQuxQuuz',\n        name: 'Replace in the end',\n        start: 2,\n      },\n      {\n        deleteCount: 3,\n        deleteOnly: false,\n        expectedText: 'QuxQuuz',\n        name: 'Replace all',\n        start: 0,\n      },\n    ];\n\n    BASE_INSERTIONS.forEach((testCase) => {\n      it(`Plain text: ${testCase.name}`, async () => {\n        await update(() => {\n          block.splice(\n            testCase.start,\n            testCase.deleteCount,\n            testCase.deleteOnly\n              ? []\n              : [$createTextNode('Qux'), $createTextNode('Quuz')],\n          );\n\n          expect(block.getTextContent()).toEqual(testCase.expectedText);\n        });\n      });\n    });\n\n    let nodes: Record<string, LexicalNode> = {};\n\n    const NESTED_ELEMENTS_TESTS: Array<{\n      deleteCount: number;\n      deleteOnly?: boolean;\n      expectedSelection: () => {\n        anchor: {\n          key: string;\n          offset: number;\n          type: string;\n        };\n        focus: {\n          key: string;\n          offset: number;\n          type: string;\n        };\n      };\n      expectedText: string;\n      name: string;\n      start: number;\n    }> = [\n      {\n        deleteCount: 0,\n        deleteOnly: true,\n        expectedSelection: () => {\n          return {\n            anchor: {\n              key: nodes.nestedText1.__key,\n              offset: 1,\n              type: 'text',\n            },\n            focus: {\n              key: nodes.nestedText1.__key,\n              offset: 1,\n              type: 'text',\n            },\n          };\n        },\n        expectedText: 'FooWiz\\n\\nFuz\\n\\nBar',\n        name: 'Do nothing',\n        start: 1,\n      },\n      {\n        deleteCount: 1,\n        deleteOnly: true,\n        expectedSelection: () => {\n          return {\n            anchor: {\n              key: nodes.text1.__key,\n              offset: 3,\n              type: 'text',\n            },\n            focus: {\n              key: nodes.text1.__key,\n              offset: 3,\n              type: 'text',\n            },\n          };\n        },\n        expectedText: 'FooFuz\\n\\nBar',\n        name: 'Delete selected element (selection moves to the previous)',\n        start: 1,\n      },\n      {\n        deleteCount: 1,\n        expectedSelection: () => {\n          return {\n            anchor: {\n              key: nodes.text1.__key,\n              offset: 3,\n              type: 'text',\n            },\n            focus: {\n              key: nodes.text1.__key,\n              offset: 3,\n              type: 'text',\n            },\n          };\n        },\n        expectedText: 'FooQuxQuuzFuz\\n\\nBar',\n        name: 'Replace selected element (selection moves to the previous)',\n        start: 1,\n      },\n      {\n        deleteCount: 2,\n        deleteOnly: true,\n        expectedSelection: () => {\n          return {\n            anchor: {\n              key: nodes.nestedText2.__key,\n              offset: 0,\n              type: 'text',\n            },\n            focus: {\n              key: nodes.nestedText2.__key,\n              offset: 0,\n              type: 'text',\n            },\n          };\n        },\n        expectedText: 'Fuz\\n\\nBar',\n        name: 'Delete selected with previous element (selection moves to the next)',\n        start: 0,\n      },\n      {\n        deleteCount: 4,\n        deleteOnly: true,\n        expectedSelection: () => {\n          return {\n            anchor: {\n              key: block.__key,\n              offset: 0,\n              type: 'element',\n            },\n            focus: {\n              key: block.__key,\n              offset: 0,\n              type: 'element',\n            },\n          };\n        },\n        expectedText: '',\n        name: 'Delete selected with all siblings (selection moves up to the element)',\n        start: 0,\n      },\n    ];\n\n    NESTED_ELEMENTS_TESTS.forEach((testCase) => {\n      it(`Nested elements: ${testCase.name}`, async () => {\n        await update(() => {\n          const text1 = $createTextNode('Foo');\n          const text2 = $createTextNode('Bar');\n\n          const nestedBlock1 = $createTestElementNode();\n          const nestedText1 = $createTextNode('Wiz');\n          nestedBlock1.append(nestedText1);\n\n          const nestedBlock2 = $createTestElementNode();\n          const nestedText2 = $createTextNode('Fuz');\n          nestedBlock2.append(nestedText2);\n\n          block.clear();\n          block.append(text1, nestedBlock1, nestedBlock2, text2);\n          nestedText1.select(1, 1);\n\n          expect(block.getTextContent()).toEqual('FooWiz\\n\\nFuz\\n\\nBar');\n\n          nodes = {\n            nestedBlock1,\n            nestedBlock2,\n            nestedText1,\n            nestedText2,\n            text1,\n            text2,\n          };\n        });\n\n        await update(() => {\n          block.splice(\n            testCase.start,\n            testCase.deleteCount,\n            testCase.deleteOnly\n              ? []\n              : [$createTextNode('Qux'), $createTextNode('Quuz')],\n          );\n        });\n\n        await update(() => {\n          expect(block.getTextContent()).toEqual(testCase.expectedText);\n\n          const selection = $getSelection();\n          const expectedSelection = testCase.expectedSelection();\n\n          if (!$isRangeSelection(selection)) {\n            return;\n          }\n\n          expect({\n            key: selection.anchor.key,\n            offset: selection.anchor.offset,\n            type: selection.anchor.type,\n          }).toEqual(expectedSelection.anchor);\n          expect({\n            key: selection.focus.key,\n            offset: selection.focus.offset,\n            type: selection.focus.type,\n          }).toEqual(expectedSelection.focus);\n        });\n      });\n    });\n\n    it('Running transforms for inserted nodes, their previous siblings and new siblings', async () => {\n      const transforms = new Set();\n      const expectedTransforms: string[] = [];\n\n      const removeTransform = editor.registerNodeTransform(TextNode, (node) => {\n        transforms.add(node.__key);\n      });\n\n      await update(() => {\n        const anotherBlock = $createTestElementNode();\n        const text1 = $createTextNode('1');\n        // Prevent text nodes from combining\n        const text2 = $createTextNode('2');\n        text2.setMode('segmented');\n        const text3 = $createTextNode('3');\n        anotherBlock.append(text1, text2, text3);\n        $getRoot().append(anotherBlock);\n\n        // Expect inserted node, its old siblings and new siblings to receive\n        // transformer calls\n        expectedTransforms.push(\n          text1.__key,\n          text2.__key,\n          text3.__key,\n          block.getChildAtIndex(0)!.__key,\n          block.getChildAtIndex(1)!.__key,\n        );\n      });\n\n      await update(() => {\n        block.splice(1, 0, [\n          $getRoot().getLastChild<ElementNode>()!.getChildAtIndex(1)!,\n        ]);\n      });\n\n      removeTransform();\n\n      await update(() => {\n        expect(block.getTextContent()).toEqual('Foo2BarBaz');\n        expectedTransforms.forEach((key) => {\n          expect(transforms).toContain(key);\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalGC.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {\n  $createParagraphNode,\n  $createTextNode,\n  $getNodeByKey,\n  $getRoot,\n  $isElementNode,\n} from 'lexical';\n\nimport {\n  $createTestElementNode,\n  generatePermutations,\n  initializeUnitTest,\n  invariant,\n} from '../../../__tests__/utils';\n\ndescribe('LexicalGC tests', () => {\n  initializeUnitTest((testEnv) => {\n    test('RootNode.clear() with a child and subchild', async () => {\n      const {editor} = testEnv;\n      await editor.update(() => {\n        $getRoot().append(\n          $createParagraphNode().append($createTextNode('foo')),\n        );\n      });\n      expect(editor.getEditorState()._nodeMap.size).toBe(3);\n      await editor.update(() => {\n        $getRoot().clear();\n      });\n      expect(editor.getEditorState()._nodeMap.size).toBe(1);\n    });\n\n    test('RootNode.clear() with a child and three subchildren', async () => {\n      const {editor} = testEnv;\n      await editor.update(() => {\n        const text1 = $createTextNode('foo');\n        const text2 = $createTextNode('bar').toggleUnmergeable();\n        const text3 = $createTextNode('zzz').toggleUnmergeable();\n        const paragraph = $createParagraphNode();\n        paragraph.append(text1, text2, text3);\n        $getRoot().append(paragraph);\n      });\n      expect(editor.getEditorState()._nodeMap.size).toBe(5);\n      await editor.update(() => {\n        $getRoot().clear();\n      });\n      expect(editor.getEditorState()._nodeMap.size).toBe(1);\n    });\n\n    for (let i = 0; i < 3; i++) {\n      test(`RootNode.clear() with a child and three subchildren, subchild ${i} removed first`, async () => {\n        const {editor} = testEnv;\n        await editor.update(() => {\n          const text1 = $createTextNode('foo'); // 1\n          const text2 = $createTextNode('bar').toggleUnmergeable(); // 2\n          const text3 = $createTextNode('zzz').toggleUnmergeable(); // 3\n          const paragraph = $createParagraphNode(); // 4\n          paragraph.append(text1, text2, text3);\n          $getRoot().append(paragraph);\n        });\n        expect(editor.getEditorState()._nodeMap.size).toBe(5);\n        await editor.update(() => {\n          const root = $getRoot();\n          const firstChild = root.getFirstChild();\n          invariant($isElementNode(firstChild));\n          const subchild = firstChild.getChildAtIndex(i)!;\n          expect(subchild.getTextContent()).toBe(['foo', 'bar', 'zzz'][i]);\n          subchild.remove();\n          root.clear();\n        });\n        expect(editor.getEditorState()._nodeMap.size).toEqual(1);\n      });\n    }\n\n    const permutations2 = generatePermutations<string>(\n      ['1', '2', '3', '4', '5', '6'],\n      2,\n    );\n    for (let i = 0; i < permutations2.length; i++) {\n      const removeKeys = permutations2[i];\n      /**\n       *          R\n       *          P\n       *     T   TE    T\n       *        T  T\n       */\n      test(`RootNode.clear() with a complex tree, nodes ${removeKeys.toString()} removed first`, async () => {\n        const {editor} = testEnv;\n        await editor.update(() => {\n          const testElement = $createTestElementNode(); // 1\n          const testElementText1 = $createTextNode('te1').toggleUnmergeable(); // 2\n          const testElementText2 = $createTextNode('te2').toggleUnmergeable(); // 3\n          const text1 = $createTextNode('a').toggleUnmergeable(); // 4\n          const text2 = $createTextNode('b').toggleUnmergeable(); // 5\n          const paragraph = $createParagraphNode(); // 6\n          testElement.append(testElementText1, testElementText2);\n          paragraph.append(text1, testElement, text2);\n          $getRoot().append(paragraph);\n        });\n        expect(editor.getEditorState()._nodeMap.size).toBe(7);\n        await editor.update(() => {\n          for (const key of removeKeys) {\n            const node = $getNodeByKey(String(key))!;\n            node.remove();\n          }\n          $getRoot().clear();\n        });\n        expect(editor.getEditorState()._nodeMap.size).toEqual(1);\n      });\n    }\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalLineBreakNode.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {$createLineBreakNode, $isLineBreakNode} from 'lexical';\n\nimport {initializeUnitTest} from '../../../__tests__/utils';\n\ndescribe('LexicalLineBreakNode tests', () => {\n  initializeUnitTest((testEnv) => {\n    test('LineBreakNode.constructor', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const lineBreakNode = $createLineBreakNode();\n\n        expect(lineBreakNode.getType()).toEqual('linebreak');\n        expect(lineBreakNode.getTextContent()).toEqual('\\n');\n      });\n    });\n\n    test('LineBreakNode.exportJSON() should return and object conforming to the expected schema', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const node = $createLineBreakNode();\n\n        // If you broke this test, you changed the public interface of a\n        // serialized Lexical Core Node. Please ensure the correct adapter\n        // logic is in place in the corresponding importJSON  method\n        // to accomodate these changes.\n        expect(node.exportJSON()).toStrictEqual({\n          type: 'linebreak',\n          version: 1,\n        });\n      });\n    });\n\n    test('LineBreakNode.createDOM()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const lineBreakNode = $createLineBreakNode();\n        const element = lineBreakNode.createDOM();\n\n        expect(element.outerHTML).toBe('<br>');\n      });\n    });\n\n    test('LineBreakNode.updateDOM()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const lineBreakNode = $createLineBreakNode();\n\n        expect(lineBreakNode.updateDOM()).toBe(false);\n      });\n    });\n\n    test('LineBreakNode.$isLineBreakNode()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const lineBreakNode = $createLineBreakNode();\n\n        expect($isLineBreakNode(lineBreakNode)).toBe(true);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalParagraphNode.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {\n  $createParagraphNode,\n  $getRoot,\n  $isParagraphNode,\n  ParagraphNode,\n  RangeSelection,\n} from 'lexical';\n\nimport {initializeUnitTest} from '../../../__tests__/utils';\n\nconst editorConfig = Object.freeze({\n  namespace: '',\n  theme: {\n    paragraph: 'my-paragraph-class',\n  },\n});\n\ndescribe('LexicalParagraphNode tests', () => {\n  initializeUnitTest((testEnv) => {\n    test('ParagraphNode.constructor', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const paragraphNode = new ParagraphNode();\n\n        expect(paragraphNode.getType()).toBe('paragraph');\n        expect(paragraphNode.getTextContent()).toBe('');\n      });\n      expect(() => new ParagraphNode()).toThrow();\n    });\n\n    test('ParagraphNode.exportJSON() should return and object conforming to the expected schema', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const node = $createParagraphNode();\n\n        // If you broke this test, you changed the public interface of a\n        // serialized Lexical Core Node. Please ensure the correct adapter\n        // logic is in place in the corresponding importJSON  method\n        // to accomodate these changes.\n        expect(node.exportJSON()).toStrictEqual({\n          alignment: '',\n          children: [],\n          direction: null,\n          id: '',\n          inset: 0,\n          textFormat: 0,\n          textStyle: '',\n          type: 'paragraph',\n          version: 1,\n        });\n      });\n    });\n\n    test('ParagraphNode.createDOM()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const paragraphNode = new ParagraphNode();\n\n        expect(paragraphNode.createDOM(editorConfig).outerHTML).toBe(\n          '<p class=\"my-paragraph-class\"></p>',\n        );\n        expect(\n          paragraphNode.createDOM({\n            namespace: '',\n            theme: {},\n          }).outerHTML,\n        ).toBe('<p></p>');\n      });\n    });\n\n    test('ParagraphNode.updateDOM()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const paragraphNode = new ParagraphNode();\n        const domElement = paragraphNode.createDOM(editorConfig);\n\n        expect(domElement.outerHTML).toBe('<p class=\"my-paragraph-class\"></p>');\n\n        const newParagraphNode = new ParagraphNode();\n        const result = newParagraphNode.updateDOM(\n          paragraphNode,\n          domElement,\n          editorConfig,\n        );\n\n        expect(result).toBe(false);\n        expect(domElement.outerHTML).toBe('<p class=\"my-paragraph-class\"></p>');\n      });\n    });\n\n    test('ParagraphNode.insertNewAfter()', async () => {\n      const {editor} = testEnv;\n      let paragraphNode: ParagraphNode;\n\n      await editor.update(() => {\n        const root = $getRoot();\n        paragraphNode = new ParagraphNode();\n        root.append(paragraphNode);\n      });\n\n      expect(testEnv.outerHTML).toBe(\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><br></p></div>',\n      );\n\n      await editor.update(() => {\n        const selection = paragraphNode.select();\n        const result = paragraphNode.insertNewAfter(\n          selection as RangeSelection,\n          false,\n        );\n        expect(result).toBeInstanceOf(ParagraphNode);\n        expect(result.getDirection()).toEqual(paragraphNode.getDirection());\n        expect(testEnv.outerHTML).toBe(\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p><br></p></div>',\n        );\n      });\n    });\n\n    test('id is supported', async () => {\n      const {editor} = testEnv;\n      let paragraphNode: ParagraphNode;\n\n      await editor.update(() => {\n        paragraphNode = new ParagraphNode();\n        paragraphNode.setId('testid')\n        $getRoot().append(paragraphNode);\n      });\n\n      expect(testEnv.innerHTML).toBe(\n          '<p id=\"testid\"><br></p>',\n      );\n    });\n\n    test('$createParagraphNode()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const paragraphNode = new ParagraphNode();\n        const createdParagraphNode = $createParagraphNode();\n\n        expect(paragraphNode.__type).toEqual(createdParagraphNode.__type);\n        expect(paragraphNode.__parent).toEqual(createdParagraphNode.__parent);\n        expect(paragraphNode.__key).not.toEqual(createdParagraphNode.__key);\n      });\n    });\n\n    test('$isParagraphNode()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const paragraphNode = new ParagraphNode();\n\n        expect($isParagraphNode(paragraphNode)).toBe(true);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalRootNode.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {\n  $createParagraphNode,\n  $createTextNode,\n  $getRoot,\n  $getSelection,\n  $isRangeSelection,\n  $isRootNode,\n  ElementNode,\n  RootNode,\n  TextNode,\n} from 'lexical';\n\nimport {\n  $createTestDecoratorNode,\n  $createTestElementNode,\n  $createTestInlineElementNode,\n  initializeUnitTest,\n} from '../../../__tests__/utils';\nimport {$createRootNode} from '../../LexicalRootNode';\n\ndescribe('LexicalRootNode tests', () => {\n  initializeUnitTest((testEnv) => {\n    let rootNode: RootNode;\n\n    function expectRootTextContentToBe(text: string): void {\n      const {editor} = testEnv;\n      editor.getEditorState().read(() => {\n        const root = $getRoot();\n\n        expect(root.__cachedText).toBe(text);\n\n        // Copy root to remove __cachedText because it's frozen\n        const rootCopy = Object.assign({}, root);\n        rootCopy.__cachedText = null;\n        Object.setPrototypeOf(rootCopy, Object.getPrototypeOf(root));\n\n        expect(rootCopy.getTextContent()).toBe(text);\n      });\n    }\n\n    beforeEach(async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        rootNode = $createRootNode();\n      });\n    });\n\n    test('RootNode.constructor', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        expect(rootNode).toStrictEqual($createRootNode());\n        expect(rootNode.getType()).toBe('root');\n        expect(rootNode.getTextContent()).toBe('');\n      });\n    });\n\n    test('RootNode.exportJSON() should return and object conforming to the expected schema', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const node = $createRootNode();\n\n        // If you broke this test, you changed the public interface of a\n        // serialized Lexical Core Node. Please ensure the correct adapter\n        // logic is in place in the corresponding importJSON method\n        // to accomodate these changes.\n        expect(node.exportJSON()).toStrictEqual({\n          children: [],\n          direction: null,\n          type: 'root',\n          version: 1,\n        });\n      });\n    });\n\n    test('RootNode.clone()', async () => {\n      const rootNodeClone = (rootNode.constructor as typeof RootNode).clone();\n\n      expect(rootNodeClone).not.toBe(rootNode);\n      expect(rootNodeClone).toStrictEqual(rootNode);\n    });\n\n    test('RootNode.createDOM()', async () => {\n      // @ts-expect-error\n      expect(() => rootNode.createDOM()).toThrow();\n    });\n\n    test('RootNode.updateDOM()', async () => {\n      // @ts-expect-error\n      expect(rootNode.updateDOM()).toBe(false);\n    });\n\n    test('RootNode.isAttached()', async () => {\n      expect(rootNode.isAttached()).toBe(true);\n    });\n\n    test('RootNode.isRootNode()', () => {\n      expect($isRootNode(rootNode)).toBe(true);\n    });\n\n    test('Cached getTextContent with decorators', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const root = $getRoot();\n        const paragraph = $createParagraphNode();\n        root.append(paragraph);\n        paragraph.append($createTestDecoratorNode());\n      });\n\n      expect(\n        editor.getEditorState().read(() => {\n          return $getRoot().getTextContent();\n        }),\n      ).toBe('Hello world');\n    });\n\n    test('RootNode.clear() to handle selection update', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const root = $getRoot();\n        const paragraph = $createParagraphNode();\n        root.append(paragraph);\n        const text = $createTextNode('Hello');\n        paragraph.append(text);\n        text.select();\n      });\n\n      await editor.update(() => {\n        const root = $getRoot();\n        root.clear();\n      });\n\n      await editor.update(() => {\n        const root = $getRoot();\n        const selection = $getSelection();\n\n        if (!$isRangeSelection(selection)) {\n          return;\n        }\n\n        expect(selection.anchor.getNode()).toBe(root);\n        expect(selection.focus.getNode()).toBe(root);\n      });\n    });\n\n    test('RootNode is selected when its only child removed', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const root = $getRoot();\n        const paragraph = $createParagraphNode();\n        root.append(paragraph);\n        const text = $createTextNode('Hello');\n        paragraph.append(text);\n        text.select();\n      });\n\n      await editor.update(() => {\n        const root = $getRoot();\n        root.getFirstChild()!.remove();\n      });\n\n      await editor.update(() => {\n        const root = $getRoot();\n        const selection = $getSelection();\n\n        if (!$isRangeSelection(selection)) {\n          return;\n        }\n\n        expect(selection.anchor.getNode()).toBe(root);\n        expect(selection.focus.getNode()).toBe(root);\n      });\n    });\n\n    test('RootNode __cachedText', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        $getRoot().append($createParagraphNode());\n      });\n\n      expectRootTextContentToBe('');\n\n      await editor.update(() => {\n        const firstParagraph = $getRoot().getFirstChild<ElementNode>()!;\n\n        firstParagraph.append($createTextNode('first line'));\n      });\n\n      expectRootTextContentToBe('first line');\n\n      await editor.update(() => {\n        $getRoot().append($createParagraphNode());\n      });\n\n      expectRootTextContentToBe('first line\\n\\n');\n\n      await editor.update(() => {\n        const secondParagraph = $getRoot().getLastChild<ElementNode>()!;\n\n        secondParagraph.append($createTextNode('second line'));\n      });\n\n      expectRootTextContentToBe('first line\\n\\nsecond line');\n\n      await editor.update(() => {\n        $getRoot().append($createParagraphNode());\n      });\n\n      expectRootTextContentToBe('first line\\n\\nsecond line\\n\\n');\n\n      await editor.update(() => {\n        const thirdParagraph = $getRoot().getLastChild<ElementNode>()!;\n        thirdParagraph.append($createTextNode('third line'));\n      });\n\n      expectRootTextContentToBe('first line\\n\\nsecond line\\n\\nthird line');\n\n      await editor.update(() => {\n        const secondParagraph = $getRoot().getChildAtIndex<ElementNode>(1)!;\n        const secondParagraphText = secondParagraph.getFirstChild<TextNode>()!;\n        secondParagraphText.setTextContent('second line!');\n      });\n\n      expectRootTextContentToBe('first line\\n\\nsecond line!\\n\\nthird line');\n    });\n\n    test('RootNode __cachedText (empty paragraph)', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        $getRoot().append($createParagraphNode(), $createParagraphNode());\n      });\n\n      expectRootTextContentToBe('\\n\\n');\n    });\n\n    test('RootNode __cachedText (inlines)', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const paragraph = $createParagraphNode();\n        $getRoot().append(paragraph);\n        paragraph.append(\n          $createTextNode('a'),\n          $createTestElementNode(),\n          $createTextNode('b'),\n          $createTestInlineElementNode(),\n          $createTextNode('c'),\n        );\n      });\n\n      expectRootTextContentToBe('a\\n\\nbc');\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalTabNode.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {\n  $insertDataTransferForPlainText,\n  $insertDataTransferForRichText,\n} from '@lexical/clipboard';\nimport {\n  $createParagraphNode,\n  $createTabNode,\n  $getRoot,\n  $getSelection,\n  $insertNodes,\n  $isRangeSelection,\n\n} from 'lexical';\n\nimport {\n  DataTransferMock,\n  initializeUnitTest,\n  invariant,\n} from '../../../__tests__/utils';\n\ndescribe('LexicalTabNode tests', () => {\n  initializeUnitTest((testEnv) => {\n    beforeEach(async () => {\n      const {editor} = testEnv;\n      await editor.update(() => {\n        const root = $getRoot();\n        const paragraph = $createParagraphNode();\n        root.append(paragraph);\n        paragraph.select();\n      });\n    });\n\n    test('can paste plain text with tabs and newlines in plain text', async () => {\n      const {editor} = testEnv;\n      const dataTransfer = new DataTransferMock();\n      dataTransfer.setData('text/plain', 'hello\\tworld\\nhello\\tworld');\n      await editor.update(() => {\n        const selection = $getSelection();\n        invariant($isRangeSelection(selection), 'isRangeSelection(selection)');\n        $insertDataTransferForPlainText(dataTransfer, selection);\n      });\n      expect(testEnv.innerHTML).toBe(\n        '<p><span data-lexical-text=\"true\">hello</span><span data-lexical-text=\"true\">\\t</span><span data-lexical-text=\"true\">world</span><br><span data-lexical-text=\"true\">hello</span><span data-lexical-text=\"true\">\\t</span><span data-lexical-text=\"true\">world</span></p>',\n      );\n    });\n\n    test('can paste plain text with tabs and newlines in rich text', async () => {\n      const {editor} = testEnv;\n      const dataTransfer = new DataTransferMock();\n      dataTransfer.setData('text/plain', 'hello\\tworld\\nhello\\tworld');\n      await editor.update(() => {\n        const selection = $getSelection();\n        invariant($isRangeSelection(selection), 'isRangeSelection(selection)');\n        $insertDataTransferForRichText(dataTransfer, selection, editor);\n      });\n      expect(testEnv.innerHTML).toBe(\n        '<p><span data-lexical-text=\"true\">hello</span><span data-lexical-text=\"true\">\\t</span><span data-lexical-text=\"true\">world</span></p><p><span data-lexical-text=\"true\">hello</span><span data-lexical-text=\"true\">\\t</span><span data-lexical-text=\"true\">world</span></p>',\n      );\n    });\n\n    // TODO fixme\n    // test('can paste HTML with tabs and new lines #4429', async () => {\n    //       const {editor} = testEnv;\n    //       const dataTransfer = new DataTransferMock();\n    //       // https://codepen.io/zurfyx/pen/bGmrzMR\n    //       dataTransfer.setData(\n    //         'text/html',\n    //         `<meta charset='utf-8'><span style=\"color: rgb(0, 0, 0); font-family: Times; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;\">hello\tworld\n    // hello\tworld</span>`,\n    //       );\n    //       await editor.update(() => {\n    //         const selection = $getSelection();\n    //         invariant($isRangeSelection(selection), 'isRangeSelection(selection)');\n    //         $insertDataTransferForRichText(dataTransfer, selection, editor);\n    //       });\n    //       expect(testEnv.innerHTML).toBe(\n    //         '<p><span data-lexical-text=\"true\">hello</span><span data-lexical-text=\"true\">\\t</span><span data-lexical-text=\"true\">world</span><br><span data-lexical-text=\"true\">hello</span><span data-lexical-text=\"true\">\\t</span><span data-lexical-text=\"true\">world</span></p>',\n    //       );\n    // });\n\n    test('can paste HTML with tabs and new lines (2)', async () => {\n      const {editor} = testEnv;\n      const dataTransfer = new DataTransferMock();\n      // GDoc 2-liner hello\\tworld (like previous test)\n      dataTransfer.setData(\n        'text/html',\n        `<meta charset='utf-8'><meta charset=\"utf-8\"><b style=\"font-weight:normal;\" id=\"docs-internal-guid-123\"><p style=\"line-height:1.38;margin-left: 36pt;margin-top:0pt;margin-bottom:0pt;\"><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">Hello</span><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\"><span class=\"Apple-tab-span\" style=\"white-space:pre;\">\t</span></span><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">world</span></p><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">Hello</span><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\"><span class=\"Apple-tab-span\" style=\"white-space:pre;\">\t</span></span><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">world</span></b>`,\n      );\n      await editor.update(() => {\n        const selection = $getSelection();\n        invariant($isRangeSelection(selection), 'isRangeSelection(selection)');\n        $insertDataTransferForRichText(dataTransfer, selection, editor);\n      });\n      expect(testEnv.innerHTML).toBe(\n        '<p><span style=\"color: rgb(0, 0, 0);\" data-lexical-text=\"true\">Hello</span><span data-lexical-text=\"true\">\\t</span><span style=\"color: rgb(0, 0, 0);\" data-lexical-text=\"true\">world</span></p><p><span style=\"color: rgb(0, 0, 0);\" data-lexical-text=\"true\">Hello</span><span data-lexical-text=\"true\">\\t</span><span style=\"color: rgb(0, 0, 0);\" data-lexical-text=\"true\">world</span></p>',\n      );\n    });\n\n    test('can type between two (leaf nodes) canInsertBeforeAfter false', async () => {\n      const {editor} = testEnv;\n      await editor.update(() => {\n        const tab1 = $createTabNode();\n        const tab2 = $createTabNode();\n        $insertNodes([tab1, tab2]);\n        tab1.select(1, 1);\n        $getSelection()!.insertText('f');\n      });\n      expect(testEnv.innerHTML).toBe(\n        '<p><span data-lexical-text=\"true\">\\t</span><span data-lexical-text=\"true\">f</span><span data-lexical-text=\"true\">\\t</span></p>',\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalTextNode.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {\n  $createParagraphNode,\n  $createTextNode, $getEditor,\n  $getNodeByKey,\n  $getRoot,\n  $getSelection,\n  $isNodeSelection,\n  $isRangeSelection,\n  ElementNode,\n  LexicalEditor,\n  ParagraphNode,\n  TextFormatType,\n  TextModeType,\n  TextNode,\n} from 'lexical';\n\nimport {\n  $createTestSegmentedNode,\n  createTestEditor,\n} from '../../../__tests__/utils';\nimport {\n  IS_BOLD,\n  IS_CODE,\n  IS_HIGHLIGHT,\n  IS_ITALIC,\n  IS_STRIKETHROUGH,\n  IS_SUBSCRIPT,\n  IS_SUPERSCRIPT,\n  IS_UNDERLINE,\n} from '../../../LexicalConstants';\nimport {\n  $getCompositionKey,\n  $setCompositionKey,\n  getEditorStateTextContent,\n} from '../../../LexicalUtils';\nimport {$generateHtmlFromNodes} from \"@lexical/html\";\n\nconst editorConfig = Object.freeze({\n  namespace: '',\n  theme: {\n    text: {\n      bold: 'my-bold-class',\n      code: 'my-code-class',\n      highlight: 'my-highlight-class',\n      italic: 'my-italic-class',\n      strikethrough: 'my-strikethrough-class',\n      underline: 'my-underline-class',\n      underlineStrikethrough: 'my-underline-strikethrough-class',\n    },\n  },\n});\n\ndescribe('LexicalTextNode tests', () => {\n  let container: HTMLElement;\n\n  beforeEach(async () => {\n    container = document.createElement('div');\n    document.body.appendChild(container);\n\n    await init();\n  });\n  afterEach(() => {\n    document.body.removeChild(container);\n    // @ts-ignore\n    container = null;\n  });\n\n  async function update(fn: () => void) {\n    editor.update(fn);\n    editor.commitUpdates();\n    return Promise.resolve().then();\n  }\n\n  let editor: LexicalEditor;\n\n  async function init() {\n    const root = document.createElement('div');\n    root.setAttribute('contenteditable', 'true');\n    container.innerHTML = '';\n    container.appendChild(root);\n\n    editor = createTestEditor();\n    editor.setRootElement(root);\n\n    // Insert initial block\n    await update(() => {\n      const paragraph = $createParagraphNode();\n      const text = $createTextNode();\n      text.toggleUnmergeable();\n      paragraph.append(text);\n      $getRoot().append(paragraph);\n    });\n  }\n\n  describe('exportJSON()', () => {\n    test('should return and object conforming to the expected schema', async () => {\n      await update(() => {\n        const node = $createTextNode();\n\n        // If you broke this test, you changed the public interface of a\n        // serialized Lexical Core Node. Please ensure the correct adapter\n        // logic is in place in the corresponding importJSON  method\n        // to accomodate these changes.\n\n        expect(node.exportJSON()).toStrictEqual({\n          detail: 0,\n          format: 0,\n          mode: 'normal',\n          style: '',\n          text: '',\n          type: 'text',\n          version: 1,\n        });\n      });\n    });\n  });\n\n  describe('root.getTextContent()', () => {\n    test('writable nodes', async () => {\n      let nodeKey: string;\n\n      await update(() => {\n        const textNode = $createTextNode('Text');\n        nodeKey = textNode.getKey();\n\n        expect(textNode.getTextContent()).toBe('Text');\n        expect(textNode.__text).toBe('Text');\n\n        $getRoot().getFirstChild<ElementNode>()!.append(textNode);\n      });\n\n      expect(\n        editor.getEditorState().read(() => {\n          const root = $getRoot();\n          return root.__cachedText;\n        }),\n      );\n      expect(getEditorStateTextContent(editor.getEditorState())).toBe('Text');\n\n      // Make sure that the editor content is still set after further reconciliations\n      await update(() => {\n        $getNodeByKey(nodeKey)!.markDirty();\n      });\n      expect(getEditorStateTextContent(editor.getEditorState())).toBe('Text');\n    });\n\n    test('prepend node', async () => {\n      await update(() => {\n        const textNode = $createTextNode('World').toggleUnmergeable();\n        $getRoot().getFirstChild<ElementNode>()!.append(textNode);\n      });\n\n      await update(() => {\n        const textNode = $createTextNode('Hello ').toggleUnmergeable();\n        const previousTextNode = $getRoot()\n          .getFirstChild<ElementNode>()!\n          .getFirstChild()!;\n        previousTextNode.insertBefore(textNode);\n      });\n\n      expect(getEditorStateTextContent(editor.getEditorState())).toBe(\n        'Hello World',\n      );\n    });\n  });\n\n  describe('setTextContent()', () => {\n    test('writable nodes', async () => {\n      await update(() => {\n        const textNode = $createTextNode('My new text node');\n        textNode.setTextContent('My newer text node');\n\n        expect(textNode.getTextContent()).toBe('My newer text node');\n      });\n    });\n  });\n\n  describe.each([\n    ['bold', IS_BOLD],\n    ['italic', IS_ITALIC],\n    ['strikethrough', IS_STRIKETHROUGH],\n    ['underline', IS_UNDERLINE],\n    ['code', IS_CODE],\n    ['subscript', IS_SUBSCRIPT],\n    ['superscript', IS_SUPERSCRIPT],\n    ['highlight', IS_HIGHLIGHT],\n  ] as const)('%s flag', (formatFlag: TextFormatType, stateFormat: number) => {\n    const flagPredicate = (node: TextNode) => node.hasFormat(formatFlag);\n    const flagToggle = (node: TextNode) => node.toggleFormat(formatFlag);\n\n    test(`getFormatFlags(${formatFlag})`, async () => {\n      await update(() => {\n        const root = $getRoot();\n        const paragraphNode = root.getFirstChild<ParagraphNode>()!;\n        const textNode = paragraphNode.getFirstChild<TextNode>()!;\n        const newFormat = textNode.getFormatFlags(formatFlag, null);\n\n        expect(newFormat).toBe(stateFormat);\n\n        textNode.setFormat(newFormat);\n        const newFormat2 = textNode.getFormatFlags(formatFlag, null);\n\n        expect(newFormat2).toBe(0);\n      });\n    });\n\n    test(`predicate for ${formatFlag}`, async () => {\n      await update(() => {\n        const root = $getRoot();\n        const paragraphNode = root.getFirstChild<ParagraphNode>()!;\n        const textNode = paragraphNode.getFirstChild<TextNode>()!;\n\n        textNode.setFormat(stateFormat);\n\n        expect(flagPredicate(textNode)).toBe(true);\n      });\n    });\n\n    test(`toggling for ${formatFlag}`, async () => {\n      // Toggle method hasn't been implemented for this flag.\n      if (flagToggle === null) {\n        return;\n      }\n\n      await update(() => {\n        const root = $getRoot();\n        const paragraphNode = root.getFirstChild<ParagraphNode>()!;\n        const textNode = paragraphNode.getFirstChild<TextNode>()!;\n\n        expect(flagPredicate(textNode)).toBe(false);\n\n        flagToggle(textNode);\n\n        expect(flagPredicate(textNode)).toBe(true);\n\n        flagToggle(textNode);\n\n        expect(flagPredicate(textNode)).toBe(false);\n      });\n    });\n  });\n\n  test('setting subscript clears superscript', async () => {\n    await update(() => {\n      const paragraphNode = $createParagraphNode();\n      const textNode = $createTextNode('Hello World');\n      paragraphNode.append(textNode);\n      $getRoot().append(paragraphNode);\n      textNode.toggleFormat('superscript');\n      textNode.toggleFormat('subscript');\n      expect(textNode.hasFormat('subscript')).toBe(true);\n      expect(textNode.hasFormat('superscript')).toBe(false);\n    });\n  });\n\n  test('setting superscript clears subscript', async () => {\n    await update(() => {\n      const paragraphNode = $createParagraphNode();\n      const textNode = $createTextNode('Hello World');\n      paragraphNode.append(textNode);\n      $getRoot().append(paragraphNode);\n      textNode.toggleFormat('subscript');\n      textNode.toggleFormat('superscript');\n      expect(textNode.hasFormat('superscript')).toBe(true);\n      expect(textNode.hasFormat('subscript')).toBe(false);\n    });\n  });\n\n  test('clearing subscript does not set superscript', async () => {\n    await update(() => {\n      const paragraphNode = $createParagraphNode();\n      const textNode = $createTextNode('Hello World');\n      paragraphNode.append(textNode);\n      $getRoot().append(paragraphNode);\n      textNode.toggleFormat('subscript');\n      textNode.toggleFormat('subscript');\n      expect(textNode.hasFormat('subscript')).toBe(false);\n      expect(textNode.hasFormat('superscript')).toBe(false);\n    });\n  });\n\n  test('clearing superscript does not set subscript', async () => {\n    await update(() => {\n      const paragraphNode = $createParagraphNode();\n      const textNode = $createTextNode('Hello World');\n      paragraphNode.append(textNode);\n      $getRoot().append(paragraphNode);\n      textNode.toggleFormat('superscript');\n      textNode.toggleFormat('superscript');\n      expect(textNode.hasFormat('superscript')).toBe(false);\n      expect(textNode.hasFormat('subscript')).toBe(false);\n    });\n  });\n\n  test('selectPrevious()', async () => {\n    await update(() => {\n      const paragraphNode = $createParagraphNode();\n      const textNode = $createTextNode('Hello World');\n      const textNode2 = $createTextNode('Goodbye Earth');\n      paragraphNode.append(textNode, textNode2);\n      $getRoot().append(paragraphNode);\n\n      let selection = textNode2.selectPrevious();\n\n      expect(selection.anchor.getNode()).toBe(textNode);\n      expect(selection.anchor.offset).toBe(11);\n      expect(selection.focus.getNode()).toBe(textNode);\n      expect(selection.focus.offset).toBe(11);\n\n      selection = textNode.selectPrevious();\n\n      expect(selection.anchor.getNode()).toBe(paragraphNode);\n      expect(selection.anchor.offset).toBe(0);\n    });\n  });\n\n  test('selectNext()', async () => {\n    await update(() => {\n      const paragraphNode = $createParagraphNode();\n      const textNode = $createTextNode('Hello World');\n      const textNode2 = $createTextNode('Goodbye Earth');\n      paragraphNode.append(textNode, textNode2);\n      $getRoot().append(paragraphNode);\n      let selection = textNode.selectNext(1, 3);\n\n      if ($isNodeSelection(selection)) {\n        return;\n      }\n\n      expect(selection.anchor.getNode()).toBe(textNode2);\n      expect(selection.anchor.offset).toBe(1);\n      expect(selection.focus.getNode()).toBe(textNode2);\n      expect(selection.focus.offset).toBe(3);\n\n      selection = textNode2.selectNext();\n\n      expect(selection.anchor.getNode()).toBe(paragraphNode);\n      expect(selection.anchor.offset).toBe(2);\n    });\n  });\n\n  describe('select()', () => {\n    test.each([\n      [\n        [2, 4],\n        [2, 4],\n      ],\n      [\n        [4, 2],\n        [4, 2],\n      ],\n      [\n        [undefined, 2],\n        [11, 2],\n      ],\n      [\n        [2, undefined],\n        [2, 11],\n      ],\n      [\n        [undefined, undefined],\n        [11, 11],\n      ],\n    ])(\n      'select(...%p)',\n      async (\n        [anchorOffset, focusOffset],\n        [expectedAnchorOffset, expectedFocusOffset],\n      ) => {\n        await update(() => {\n          const paragraphNode = $createParagraphNode();\n          const textNode = $createTextNode('Hello World');\n          paragraphNode.append(textNode);\n          $getRoot().append(paragraphNode);\n\n          const selection = textNode.select(anchorOffset, focusOffset);\n\n          expect(selection.focus.getNode()).toBe(textNode);\n          expect(selection.anchor.offset).toBe(expectedAnchorOffset);\n          expect(selection.focus.getNode()).toBe(textNode);\n          expect(selection.focus.offset).toBe(expectedFocusOffset);\n        });\n      },\n    );\n  });\n\n  describe('splitText()', () => {\n    test('convert segmented node into plain text', async () => {\n      await update(() => {\n        const segmentedNode = $createTestSegmentedNode('Hello World');\n        const paragraphNode = $createParagraphNode();\n        paragraphNode.append(segmentedNode);\n\n        const [middle, next] = segmentedNode.splitText(5);\n\n        const children = paragraphNode.getAllTextNodes();\n        expect(paragraphNode.getTextContent()).toBe('Hello World');\n        expect(children[0].isSimpleText()).toBe(true);\n        expect(children[0].getTextContent()).toBe('Hello');\n        expect(middle).toBe(children[0]);\n        expect(next).toBe(children[1]);\n      });\n    });\n    test.each([\n      ['a', [], ['a']],\n      ['a', [1], ['a']],\n      ['a', [5], ['a']],\n      ['Hello World', [], ['Hello World']],\n      ['Hello World', [3], ['Hel', 'lo World']],\n      ['Hello World', [3, 3], ['Hel', 'lo World']],\n      ['Hello World', [3, 7], ['Hel', 'lo W', 'orld']],\n      ['Hello World', [7, 3], ['Hel', 'lo W', 'orld']],\n      ['Hello World', [3, 7, 99], ['Hel', 'lo W', 'orld']],\n    ])(\n      '\"%s\" splitText(...%p)',\n      async (initialString, splitOffsets, splitStrings) => {\n        await update(() => {\n          const paragraphNode = $createParagraphNode();\n          const textNode = $createTextNode(initialString);\n          paragraphNode.append(textNode);\n\n          const splitNodes = textNode.splitText(...splitOffsets);\n\n          expect(paragraphNode.getChildren()).toHaveLength(splitStrings.length);\n          expect(splitNodes.map((node) => node.getTextContent())).toEqual(\n            splitStrings,\n          );\n        });\n      },\n    );\n\n    test('splitText moves composition key to last node', async () => {\n      await update(() => {\n        const paragraphNode = $createParagraphNode();\n        const textNode = $createTextNode('12345');\n        paragraphNode.append(textNode);\n        $setCompositionKey(textNode.getKey());\n\n        const [, splitNode2] = textNode.splitText(1);\n        expect($getCompositionKey()).toBe(splitNode2.getKey());\n      });\n    });\n\n    test.each([\n      [\n        'Hello',\n        [4],\n        [3, 3],\n        {\n          anchorNodeIndex: 0,\n          anchorOffset: 3,\n          focusNodeIndex: 0,\n          focusOffset: 3,\n        },\n      ],\n      [\n        'Hello',\n        [4],\n        [5, 5],\n        {\n          anchorNodeIndex: 1,\n          anchorOffset: 1,\n          focusNodeIndex: 1,\n          focusOffset: 1,\n        },\n      ],\n      [\n        'Hello World',\n        [4],\n        [2, 7],\n        {\n          anchorNodeIndex: 0,\n          anchorOffset: 2,\n          focusNodeIndex: 1,\n          focusOffset: 3,\n        },\n      ],\n      [\n        'Hello World',\n        [4],\n        [2, 4],\n        {\n          anchorNodeIndex: 0,\n          anchorOffset: 2,\n          focusNodeIndex: 0,\n          focusOffset: 4,\n        },\n      ],\n      [\n        'Hello World',\n        [4],\n        [7, 2],\n        {\n          anchorNodeIndex: 1,\n          anchorOffset: 3,\n          focusNodeIndex: 0,\n          focusOffset: 2,\n        },\n      ],\n      [\n        'Hello World',\n        [4, 6],\n        [2, 9],\n        {\n          anchorNodeIndex: 0,\n          anchorOffset: 2,\n          focusNodeIndex: 2,\n          focusOffset: 3,\n        },\n      ],\n      [\n        'Hello World',\n        [4, 6],\n        [9, 2],\n        {\n          anchorNodeIndex: 2,\n          anchorOffset: 3,\n          focusNodeIndex: 0,\n          focusOffset: 2,\n        },\n      ],\n      [\n        'Hello World',\n        [4, 6],\n        [9, 9],\n        {\n          anchorNodeIndex: 2,\n          anchorOffset: 3,\n          focusNodeIndex: 2,\n          focusOffset: 3,\n        },\n      ],\n    ])(\n      '\"%s\" splitText(...%p) with select(...%p)',\n      async (\n        initialString,\n        splitOffsets,\n        selectionOffsets,\n        {anchorNodeIndex, anchorOffset, focusNodeIndex, focusOffset},\n      ) => {\n        await update(() => {\n          const paragraphNode = $createParagraphNode();\n          const textNode = $createTextNode(initialString);\n          paragraphNode.append(textNode);\n          $getRoot().append(paragraphNode);\n\n          const selection = textNode.select(...selectionOffsets);\n          const childrenNodes = textNode.splitText(...splitOffsets);\n\n          expect(selection.anchor.getNode()).toBe(\n            childrenNodes[anchorNodeIndex],\n          );\n          expect(selection.anchor.offset).toBe(anchorOffset);\n          expect(selection.focus.getNode()).toBe(childrenNodes[focusNodeIndex]);\n          expect(selection.focus.offset).toBe(focusOffset);\n        });\n      },\n    );\n\n    test('with detached parent', async () => {\n      await update(() => {\n        const textNode = $createTextNode('foo');\n        const splits = textNode.splitText(1, 2);\n        expect(splits.map((split) => split.getTextContent())).toEqual([\n          'f',\n          'o',\n          'o',\n        ]);\n      });\n    });\n  });\n\n  describe('createDOM()', () => {\n    test.each([\n      ['no formatting', 0, 'My text node', '<span>My text node</span>'],\n      [\n        'bold',\n        IS_BOLD,\n        'My text node',\n        '<strong class=\"my-bold-class\">My text node</strong>',\n      ],\n      ['bold + empty', IS_BOLD, '', `<strong class=\"my-bold-class\"></strong>`],\n      [\n        'underline',\n        IS_UNDERLINE,\n        'My text node',\n        '<span class=\"my-underline-class\">My text node</span>',\n      ],\n      [\n        'strikethrough',\n        IS_STRIKETHROUGH,\n        'My text node',\n        '<span class=\"my-strikethrough-class\">My text node</span>',\n      ],\n      [\n        'highlight',\n        IS_HIGHLIGHT,\n        'My text node',\n        '<mark><span class=\"my-highlight-class\">My text node</span></mark>',\n      ],\n      [\n        'italic',\n        IS_ITALIC,\n        'My text node',\n        '<em class=\"my-italic-class\">My text node</em>',\n      ],\n      [\n        'code',\n        IS_CODE,\n        'My text node',\n        '<code spellcheck=\"false\"><span class=\"my-code-class\">My text node</span></code>',\n      ],\n      [\n        'underline + strikethrough',\n        IS_UNDERLINE | IS_STRIKETHROUGH,\n        'My text node',\n        '<span class=\"my-underline-strikethrough-class\">' +\n          'My text node</span>',\n      ],\n      [\n        'code + italic',\n        IS_CODE | IS_ITALIC,\n        'My text node',\n        '<code spellcheck=\"false\"><em class=\"my-code-class my-italic-class\">My text node</em></code>',\n      ],\n      [\n        'code + underline + strikethrough',\n        IS_CODE | IS_UNDERLINE | IS_STRIKETHROUGH,\n        'My text node',\n        '<code spellcheck=\"false\"><span class=\"my-underline-strikethrough-class my-code-class\">' +\n          'My text node</span></code>',\n      ],\n      [\n        'highlight + italic',\n        IS_HIGHLIGHT | IS_ITALIC,\n        'My text node',\n        '<mark><em class=\"my-highlight-class my-italic-class\">My text node</em></mark>',\n      ],\n      [\n        'code + underline + strikethrough + bold + italic',\n        IS_CODE | IS_UNDERLINE | IS_STRIKETHROUGH | IS_BOLD | IS_ITALIC,\n        'My text node',\n        '<code spellcheck=\"false\"><strong class=\"my-underline-strikethrough-class my-bold-class my-code-class my-italic-class\">My text node</strong></code>',\n      ],\n      [\n        'code + underline + strikethrough + bold + italic + highlight',\n        IS_CODE |\n          IS_UNDERLINE |\n          IS_STRIKETHROUGH |\n          IS_BOLD |\n          IS_ITALIC |\n          IS_HIGHLIGHT,\n        'My text node',\n        '<code spellcheck=\"false\"><strong class=\"my-underline-strikethrough-class my-bold-class my-code-class my-highlight-class my-italic-class\">My text node</strong></code>',\n      ],\n    ])('%s text format type', async (_type, format, contents, expectedHTML) => {\n      await update(() => {\n        const textNode = $createTextNode(contents);\n        textNode.setFormat(format);\n        const element = textNode.createDOM(editorConfig);\n\n        expect(element.outerHTML).toBe(expectedHTML);\n      });\n    });\n\n    describe('has parent node', () => {\n      test.each([\n        ['no formatting', 0, 'My text node', '<span>My text node</span>'],\n        ['no formatting + empty string', 0, '', `<span></span>`],\n      ])(\n        '%s text format type',\n        async (_type, format, contents, expectedHTML) => {\n          await update(() => {\n            const paragraphNode = $createParagraphNode();\n            const textNode = $createTextNode(contents);\n            textNode.setFormat(format);\n            paragraphNode.append(textNode);\n            const element = textNode.createDOM(editorConfig);\n\n            expect(element.outerHTML).toBe(expectedHTML);\n          });\n        },\n      );\n    });\n  });\n\n  describe('updateDOM()', () => {\n    test.each([\n      [\n        'different tags',\n        {\n          format: IS_BOLD,\n          mode: 'normal',\n          text: 'My text node',\n        },\n        {\n          format: IS_ITALIC,\n          mode: 'normal',\n          text: 'My text node',\n        },\n        {\n          expectedHTML: null,\n          result: true,\n        },\n      ],\n      [\n        'no change in tags',\n        {\n          format: IS_BOLD,\n          mode: 'normal',\n          text: 'My text node',\n        },\n        {\n          format: IS_BOLD,\n          mode: 'normal',\n          text: 'My text node',\n        },\n        {\n          expectedHTML: '<strong class=\"my-bold-class\">My text node</strong>',\n          result: false,\n        },\n      ],\n      [\n        'change in text',\n        {\n          format: IS_BOLD,\n          mode: 'normal',\n          text: 'My text node',\n        },\n        {\n          format: IS_BOLD,\n          mode: 'normal',\n          text: 'My new text node',\n        },\n        {\n          expectedHTML:\n            '<strong class=\"my-bold-class\">My new text node</strong>',\n          result: false,\n        },\n      ],\n      [\n        'removing code block',\n        {\n          format: IS_CODE | IS_BOLD,\n          mode: 'normal',\n          text: 'My text node',\n        },\n        {\n          format: IS_BOLD,\n          mode: 'normal',\n          text: 'My new text node',\n        },\n        {\n          expectedHTML: null,\n          result: true,\n        },\n      ],\n    ])(\n      '%s',\n      async (\n        _desc,\n        {text: prevText, mode: prevMode, format: prevFormat},\n        {text: nextText, mode: nextMode, format: nextFormat},\n        {result, expectedHTML},\n      ) => {\n        await update(() => {\n          const prevTextNode = $createTextNode(prevText);\n          prevTextNode.setMode(prevMode as TextModeType);\n          prevTextNode.setFormat(prevFormat);\n          const element = prevTextNode.createDOM(editorConfig);\n          const textNode = $createTextNode(nextText);\n          textNode.setMode(nextMode as TextModeType);\n          textNode.setFormat(nextFormat);\n\n          expect(textNode.updateDOM(prevTextNode, element, editorConfig)).toBe(\n            result,\n          );\n          // Only need to bother about DOM element contents if updateDOM()\n          // returns false.\n          if (!result) {\n            expect(element.outerHTML).toBe(expectedHTML);\n          }\n        });\n      },\n    );\n  });\n\n  describe('exportDOM()', () => {\n\n    test('simple text exports as a text node', async () => {\n      await update(() => {\n        const paragraph = $getRoot().getFirstChild<ElementNode>()!;\n        const textNode = $createTextNode('hello');\n        paragraph.append(textNode);\n\n        const html = $generateHtmlFromNodes($getEditor(), null);\n        expect(html).toBe('<p>hello</p>');\n      });\n    });\n\n    test('simple text wrapped in span if leading or ending spacing', async () => {\n\n      const textByExpectedHtml = {\n        'hello ': '<p><span style=\"white-space: pre-wrap;\">hello </span></p>',\n        ' hello': '<p><span style=\"white-space: pre-wrap;\"> hello</span></p>',\n        ' hello ': '<p><span style=\"white-space: pre-wrap;\"> hello </span></p>',\n      }\n\n      await update(() => {\n        const paragraph = $getRoot().getFirstChild<ElementNode>()!;\n        for (const [text, expectedHtml] of Object.entries(textByExpectedHtml)) {\n          paragraph.getChildren().forEach(c => c.remove(true));\n          const textNode = $createTextNode(text);\n          paragraph.append(textNode);\n\n          const html = $generateHtmlFromNodes($getEditor(), null);\n          expect(html).toBe(expectedHtml);\n        }\n      });\n    });\n\n    test('text with formats exports using format elements instead of classes', async () => {\n      await update(() => {\n        const paragraph = $getRoot().getFirstChild<ElementNode>()!;\n        const textNode = $createTextNode('hello');\n        textNode.toggleFormat('bold');\n        textNode.toggleFormat('subscript');\n        textNode.toggleFormat('italic');\n        textNode.toggleFormat('underline');\n        textNode.toggleFormat('code');\n        paragraph.append(textNode);\n\n        const html = $generateHtmlFromNodes($getEditor(), null);\n        expect(html).toBe('<p><u><em><strong><code spellcheck=\"false\"><strong>hello</strong></code></strong></em></u></p>');\n      });\n    });\n\n  });\n\n  test('mergeWithSibling', async () => {\n    await update(() => {\n      const paragraph = $getRoot().getFirstChild<ElementNode>()!;\n      const textNode1 = $createTextNode('1');\n      const textNode2 = $createTextNode('2');\n      const textNode3 = $createTextNode('3');\n      paragraph.append(textNode1, textNode2, textNode3);\n      textNode2.select();\n\n      const selection = $getSelection();\n      textNode2.mergeWithSibling(textNode1);\n\n      if (!$isRangeSelection(selection)) {\n        return;\n      }\n\n      expect(selection.anchor.getNode()).toBe(textNode2);\n      expect(selection.anchor.offset).toBe(1);\n      expect(selection.focus.offset).toBe(1);\n\n      textNode2.mergeWithSibling(textNode3);\n\n      expect(selection.anchor.getNode()).toBe(textNode2);\n      expect(selection.anchor.offset).toBe(1);\n      expect(selection.focus.offset).toBe(1);\n    });\n\n    expect(getEditorStateTextContent(editor.getEditorState())).toBe('123');\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/nodes/common.ts",
    "content": "import {sizeToPixels} from \"../../../utils/dom\";\nimport {SerializedCommonBlockNode} from \"lexical/nodes/CommonBlockNode\";\n\nexport type CommonBlockAlignment = 'left' | 'right' | 'center' | 'justify' | '';\nconst validAlignments: CommonBlockAlignment[] = ['left', 'right', 'center', 'justify'];\n\ntype EditorNodeDirection = 'ltr' | 'rtl' | null;\n\nexport interface NodeHasAlignment {\n    readonly __alignment: CommonBlockAlignment;\n    setAlignment(alignment: CommonBlockAlignment): void;\n    getAlignment(): CommonBlockAlignment;\n}\n\nexport interface NodeHasId {\n    readonly __id: string;\n    setId(id: string): void;\n    getId(): string;\n}\n\nexport interface NodeHasInset {\n    readonly __inset: number;\n    setInset(inset: number): void;\n    getInset(): number;\n}\n\nexport interface NodeHasDirection {\n    readonly __dir: EditorNodeDirection;\n    setDirection(direction: EditorNodeDirection): void;\n    getDirection(): EditorNodeDirection;\n}\n\nexport interface CommonBlockInterface extends NodeHasId, NodeHasAlignment, NodeHasInset, NodeHasDirection {}\n\nexport function extractAlignmentFromElement(element: HTMLElement): CommonBlockAlignment {\n    const textAlignStyle: string = element.style.textAlign || '';\n    if (validAlignments.includes(textAlignStyle as CommonBlockAlignment)) {\n        return textAlignStyle as CommonBlockAlignment;\n    }\n\n    if (element.classList.contains('align-left')) {\n        return 'left';\n    } else if (element.classList.contains('align-right')) {\n        return 'right'\n    } else if (element.classList.contains('align-center')) {\n        return 'center'\n    } else if (element.classList.contains('align-justify')) {\n        return 'justify'\n    }\n\n    return '';\n}\n\nexport function extractInsetFromElement(element: HTMLElement): number {\n    const elemPadding: string = element.style.paddingLeft || '0';\n    return sizeToPixels(elemPadding);\n}\n\nexport function extractDirectionFromElement(element: HTMLElement): EditorNodeDirection {\n    const elemDir = (element.dir || '').toLowerCase();\n    if (elemDir === 'rtl' || elemDir === 'ltr') {\n        return elemDir;\n    }\n\n    return null;\n}\n\nexport function setCommonBlockPropsFromElement(element: HTMLElement, node: CommonBlockInterface): void {\n    if (element.id) {\n        node.setId(element.id);\n    }\n\n    node.setAlignment(extractAlignmentFromElement(element));\n    node.setInset(extractInsetFromElement(element));\n    node.setDirection(extractDirectionFromElement(element));\n}\n\nexport function commonPropertiesDifferent(nodeA: CommonBlockInterface, nodeB: CommonBlockInterface): boolean {\n    return nodeA.__id !== nodeB.__id ||\n        nodeA.__alignment !== nodeB.__alignment ||\n        nodeA.__inset !== nodeB.__inset ||\n        nodeA.__dir !== nodeB.__dir;\n}\n\nexport function applyCommonPropertyChanges(prevNode: CommonBlockInterface, currentNode: CommonBlockInterface, element: HTMLElement): void {\n    if (prevNode.__id !== currentNode.__id) {\n        element.setAttribute('id', currentNode.__id);\n    }\n\n    if (prevNode.__alignment !== currentNode.__alignment) {\n        for (const alignment of validAlignments) {\n            element.classList.remove('align-' + alignment);\n        }\n\n        if (currentNode.__alignment) {\n            element.classList.add('align-' + currentNode.__alignment);\n        }\n    }\n\n    if (prevNode.__inset !== currentNode.__inset) {\n        if (currentNode.__inset) {\n            element.style.paddingLeft = `${currentNode.__inset}px`;\n        } else {\n            element.style.removeProperty('paddingLeft');\n        }\n    }\n\n    if (prevNode.__dir !== currentNode.__dir) {\n        if (currentNode.__dir) {\n            element.dir = currentNode.__dir;\n        } else {\n            element.removeAttribute('dir');\n        }\n    }\n}\n\nexport function updateElementWithCommonBlockProps(element: HTMLElement, node: CommonBlockInterface): void {\n    if (node.__id) {\n        element.setAttribute('id', node.__id);\n    }\n\n    if (node.__alignment) {\n        element.classList.add('align-' + node.__alignment);\n    }\n\n    if (node.__inset) {\n        element.style.paddingLeft = `${node.__inset}px`;\n    }\n\n    if (node.__dir) {\n        element.dir = node.__dir;\n    }\n}\n\nexport function deserializeCommonBlockNode(serializedNode: SerializedCommonBlockNode, node: CommonBlockInterface): void {\n    node.setId(serializedNode.id);\n    node.setAlignment(serializedNode.alignment);\n    node.setInset(serializedNode.inset);\n    node.setDirection(serializedNode.direction);\n}\n\nexport interface NodeHasSize {\n    setHeight(height: number): void;\n    setWidth(width: number): void;\n    getHeight(): number;\n    getWidth(): number;\n}"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/shared/__mocks__/invariant.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\n// invariant(condition, message) will refine types based on \"condition\", and\n// if \"condition\" is false will throw an error. This function is special-cased\n// in flow itself, so we can't name it anything else.\nexport default function invariant(\n  cond?: boolean,\n  message?: string,\n  ...args: string[]\n): asserts cond {\n  if (cond) {\n    return;\n  }\n\n  throw new Error(\n    args.reduce((msg, arg) => msg.replace('%s', String(arg)), message || ''),\n  );\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/shared/canUseDOM.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nexport const CAN_USE_DOM: boolean =\n  typeof window !== 'undefined' &&\n  typeof window.document !== 'undefined' &&\n  typeof window.document.createElement !== 'undefined';\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/shared/caretFromPoint.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nexport default function caretFromPoint(\n  x: number,\n  y: number,\n): null | {\n  offset: number;\n  node: Node;\n} {\n  if (typeof document.caretRangeFromPoint !== 'undefined') {\n    const range = document.caretRangeFromPoint(x, y);\n    if (range === null) {\n      return null;\n    }\n    return {\n      node: range.startContainer,\n      offset: range.startOffset,\n    };\n    // @ts-ignore\n  } else if (document.caretPositionFromPoint !== 'undefined') {\n    // @ts-ignore FF - no types\n    const range = document.caretPositionFromPoint(x, y);\n    if (range === null) {\n      return null;\n    }\n    return {\n      node: range.offsetNode,\n      offset: range.offset,\n    };\n  } else {\n    // Gracefully handle IE\n    return null;\n  }\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/shared/environment.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {CAN_USE_DOM} from 'lexical/shared/canUseDOM';\n\ndeclare global {\n  interface Document {\n    documentMode?: unknown;\n  }\n\n  interface Window {\n    MSStream?: unknown;\n  }\n}\n\nconst documentMode =\n  CAN_USE_DOM && 'documentMode' in document ? document.documentMode : null;\n\nexport const IS_APPLE: boolean =\n  CAN_USE_DOM && /Mac|iPod|iPhone|iPad/.test(navigator.platform);\n\nexport const IS_FIREFOX: boolean =\n  CAN_USE_DOM && /^(?!.*Seamonkey)(?=.*Firefox).*/i.test(navigator.userAgent);\n\nexport const CAN_USE_BEFORE_INPUT: boolean =\n  CAN_USE_DOM && 'InputEvent' in window && !documentMode\n    ? 'getTargetRanges' in new window.InputEvent('input')\n    : false;\n\nexport const IS_SAFARI: boolean =\n  CAN_USE_DOM && /Version\\/[\\d.]+.*Safari/.test(navigator.userAgent);\n\nexport const IS_IOS: boolean =\n  CAN_USE_DOM &&\n  /iPad|iPhone|iPod/.test(navigator.userAgent) &&\n  !window.MSStream;\n\nexport const IS_ANDROID: boolean =\n  CAN_USE_DOM && /Android/.test(navigator.userAgent);\n\n// Keep these in case we need to use them in the future.\n// export const IS_WINDOWS: boolean = CAN_USE_DOM && /Win/.test(navigator.platform);\nexport const IS_CHROME: boolean =\n  CAN_USE_DOM && /^(?=.*Chrome).*/i.test(navigator.userAgent);\n// export const canUseTextInputEvent: boolean = CAN_USE_DOM && 'TextEvent' in window && !documentMode;\n\nexport const IS_ANDROID_CHROME: boolean =\n  CAN_USE_DOM && IS_ANDROID && IS_CHROME;\n\nexport const IS_APPLE_WEBKIT =\n  CAN_USE_DOM && /AppleWebKit\\/[\\d.]+/.test(navigator.userAgent) && !IS_CHROME;\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/shared/invariant.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\n// invariant(condition, message) will refine types based on \"condition\", and\n// if \"condition\" is false will throw an error. This function is special-cased\n// in flow itself, so we can't name it anything else.\nexport default function invariant(\n  cond?: boolean,\n  message?: string,\n  ...args: string[]\n): asserts cond {\n  if (cond) {\n    return;\n  }\n\n  for (const arg of args) {\n    message = (message || '').replace('%s', arg);\n  }\n\n  throw new Error(message);\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/shared/normalizeClassNames.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nexport default function normalizeClassNames(\n  ...classNames: Array<typeof undefined | boolean | null | string>\n): Array<string> {\n  const rval = [];\n  for (const className of classNames) {\n    if (className && typeof className === 'string') {\n      for (const [s] of className.matchAll(/\\S+/g)) {\n        rval.push(s);\n      }\n    }\n  }\n  return rval;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/shared/simpleDiffWithCursor.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nexport default function simpleDiffWithCursor(\n  a: string,\n  b: string,\n  cursor: number,\n): {index: number; insert: string; remove: number} {\n  const aLength = a.length;\n  const bLength = b.length;\n  let left = 0; // number of same characters counting from left\n  let right = 0; // number of same characters counting from right\n  // Iterate left to the right until we find a changed character\n  // First iteration considers the current cursor position\n  while (\n    left < aLength &&\n    left < bLength &&\n    a[left] === b[left] &&\n    left < cursor\n  ) {\n    left++;\n  }\n  // Iterate right to the left until we find a changed character\n  while (\n    right + left < aLength &&\n    right + left < bLength &&\n    a[aLength - right - 1] === b[bLength - right - 1]\n  ) {\n    right++;\n  }\n  // Try to iterate left further to the right without caring about the current cursor position\n  while (\n    right + left < aLength &&\n    right + left < bLength &&\n    a[left] === b[left]\n  ) {\n    left++;\n  }\n  return {\n    index: left,\n    insert: b.slice(left, bLength - right),\n    remove: aLength - left - right,\n  };\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/core/shared/warnOnlyOnce.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nexport default function warnOnlyOnce(message: string) {\n  if (!__DEV__) {\n    return;\n  }\n  let run = false;\n  return () => {\n    if (!run) {\n      console.warn(message);\n    }\n    run = true;\n  };\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/headless/__tests__/unit/LexicalHeadlessEditor.test.ts",
    "content": "/**\n * @jest-environment node\n */\n\n// Jest environment should be at the very top of the file. overriding environment for this test\n// to ensure that headless editor works within node environment\n// https://jestjs.io/docs/configuration#testenvironment-string\n\n/* eslint-disable header/header */\n\n/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {EditorState, LexicalEditor, RangeSelection} from 'lexical';\n\nimport {$generateHtmlFromNodes} from '@lexical/html';\nimport {JSDOM} from 'jsdom';\nimport {\n  $createParagraphNode,\n  $createTextNode,\n  $getRoot,\n  $getSelection,\n  COMMAND_PRIORITY_NORMAL,\n  CONTROLLED_TEXT_INSERTION_COMMAND,\n  ParagraphNode,\n} from 'lexical';\n\nimport {createHeadlessEditor} from '../..';\n\ndescribe('LexicalHeadlessEditor', () => {\n  let editor: LexicalEditor;\n\n  async function update(updateFn: () => void) {\n    editor.update(updateFn);\n    await Promise.resolve();\n  }\n\n  function assertEditorState(\n    editorState: EditorState,\n    nodes: Record<string, unknown>[],\n  ) {\n    const nodesFromState = Array.from(editorState._nodeMap.values());\n    expect(nodesFromState).toEqual(\n      nodes.map((node) => expect.objectContaining(node)),\n    );\n  }\n\n  beforeEach(() => {\n    editor = createHeadlessEditor({\n      namespace: '',\n      onError: (error) => {\n        throw error;\n      },\n    });\n  });\n\n  it('should be headless environment', async () => {\n    expect(typeof window === 'undefined').toBe(true);\n    expect(typeof document === 'undefined').toBe(true);\n  });\n\n  it('can update editor', async () => {\n    await update(() => {\n      $getRoot().append(\n        $createParagraphNode().append(\n          $createTextNode('Hello').toggleFormat('bold'),\n          $createTextNode('world'),\n        ),\n      );\n    });\n\n    assertEditorState(editor.getEditorState(), [\n      {\n        __key: 'root',\n      },\n      {\n        __type: 'paragraph',\n      },\n      {\n        __format: 1,\n        __text: 'Hello',\n        __type: 'text',\n      },\n      {\n        __format: 0,\n        __text: 'world',\n        __type: 'text',\n      },\n    ]);\n  });\n\n  it('can set editor state from json', async () => {\n    editor.setEditorState(\n      editor.parseEditorState(\n        '{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":1,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello\",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"world\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"paragraph\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}',\n      ),\n    );\n\n    assertEditorState(editor.getEditorState(), [\n      {\n        __key: 'root',\n      },\n      {\n        __type: 'paragraph',\n      },\n      {\n        __format: 1,\n        __text: 'Hello',\n        __type: 'text',\n      },\n      {\n        __format: 0,\n        __text: 'world',\n        __type: 'text',\n      },\n    ]);\n  });\n\n  it('can register listeners', async () => {\n    const onUpdate = jest.fn();\n    const onCommand = jest.fn();\n    const onTransform = jest.fn();\n    const onTextContent = jest.fn();\n\n    editor.registerUpdateListener(onUpdate);\n    editor.registerCommand(\n      CONTROLLED_TEXT_INSERTION_COMMAND,\n      onCommand,\n      COMMAND_PRIORITY_NORMAL,\n    );\n    editor.registerNodeTransform(ParagraphNode, onTransform);\n    editor.registerTextContentListener(onTextContent);\n\n    await update(() => {\n      $getRoot().append(\n        $createParagraphNode().append(\n          $createTextNode('Hello').toggleFormat('bold'),\n          $createTextNode('world'),\n        ),\n      );\n      editor.dispatchCommand(CONTROLLED_TEXT_INSERTION_COMMAND, 'foo');\n    });\n\n    expect(onUpdate).toHaveBeenCalled();\n    expect(onCommand).toHaveBeenCalledWith('foo', expect.anything());\n    expect(onTransform).toHaveBeenCalledWith(\n      expect.objectContaining({__type: 'paragraph'}),\n    );\n    expect(onTextContent).toHaveBeenCalledWith('Helloworld');\n  });\n\n  it('can preserve selection for pending editor state (within update loop)', async () => {\n    await update(() => {\n      const textNode = $createTextNode('Hello world');\n      $getRoot().append($createParagraphNode().append(textNode));\n      textNode.select(1, 2);\n    });\n\n    await update(() => {\n      const selection = $getSelection() as RangeSelection;\n      expect(selection.anchor).toEqual(\n        expect.objectContaining({offset: 1, type: 'text'}),\n      );\n      expect(selection.focus).toEqual(\n        expect.objectContaining({offset: 2, type: 'text'}),\n      );\n    });\n  });\n\n  function setupDom() {\n    const jsdom = new JSDOM();\n\n    const _window = global.window;\n    const _document = global.document;\n\n    // @ts-expect-error\n    global.window = jsdom.window;\n    global.document = jsdom.window.document;\n\n    return () => {\n      global.window = _window;\n      global.document = _document;\n    };\n  }\n\n  it('can generate html from the nodes when dom is set', async () => {\n    editor.setEditorState(\n      // \"hello world\"\n      editor.parseEditorState(\n        `{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"hello world\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"paragraph\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}`,\n      ),\n    );\n\n    const cleanup = setupDom();\n\n    const html = editor\n      .getEditorState()\n      .read(() => $generateHtmlFromNodes(editor, null));\n\n    cleanup();\n\n    expect(html).toBe(\n      '<p dir=\"ltr\">hello world</p>',\n    );\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/headless/index.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {CreateEditorArgs, LexicalEditor} from 'lexical';\n\nimport {createEditor} from 'lexical';\n\n/**\n * Generates a headless editor that allows lexical to be used without the need for a DOM, eg in Node.js.\n * Throws an error when unsupported methods are used.\n * @param editorConfig - The optional lexical editor configuration.\n * @returns - The configured headless editor.\n */\nexport function createHeadlessEditor(\n  editorConfig?: CreateEditorArgs,\n): LexicalEditor {\n  const editor = createEditor(editorConfig);\n  editor._headless = true;\n\n  const unsupportedMethods = [\n    'registerDecoratorListener',\n    'registerRootListener',\n    'registerMutationListener',\n    'getRootElement',\n    'setRootElement',\n    'getElementByKey',\n    'focus',\n    'blur',\n  ] as const;\n\n  unsupportedMethods.forEach((method: typeof unsupportedMethods[number]) => {\n    editor[method] = () => {\n      throw new Error(`${method} is not supported in headless mode`);\n    };\n  });\n\n  return editor;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/history/index.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {EditorState, LexicalEditor, LexicalNode, NodeKey} from 'lexical';\n\nimport {mergeRegister} from '@lexical/utils';\nimport {\n  $isRangeSelection,\n  $isRootNode,\n  $isTextNode,\n  CAN_REDO_COMMAND,\n  CAN_UNDO_COMMAND,\n  CLEAR_EDITOR_COMMAND,\n  CLEAR_HISTORY_COMMAND,\n  COMMAND_PRIORITY_EDITOR,\n  REDO_COMMAND,\n  UNDO_COMMAND,\n} from 'lexical';\n\ntype MergeAction = 0 | 1 | 2;\nconst HISTORY_MERGE = 0;\nconst HISTORY_PUSH = 1;\nconst DISCARD_HISTORY_CANDIDATE = 2;\n\ntype ChangeType = 0 | 1 | 2 | 3 | 4;\nconst OTHER = 0;\nconst COMPOSING_CHARACTER = 1;\nconst INSERT_CHARACTER_AFTER_SELECTION = 2;\nconst DELETE_CHARACTER_BEFORE_SELECTION = 3;\nconst DELETE_CHARACTER_AFTER_SELECTION = 4;\n\nexport type HistoryStateEntry = {\n  editor: LexicalEditor;\n  editorState: EditorState;\n};\nexport type HistoryState = {\n  current: null | HistoryStateEntry;\n  redoStack: Array<HistoryStateEntry>;\n  undoStack: Array<HistoryStateEntry>;\n};\n\ntype IntentionallyMarkedAsDirtyElement = boolean;\n\nfunction getDirtyNodes(\n  editorState: EditorState,\n  dirtyLeaves: Set<NodeKey>,\n  dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,\n): Array<LexicalNode> {\n  const nodeMap = editorState._nodeMap;\n  const nodes = [];\n\n  for (const dirtyLeafKey of dirtyLeaves) {\n    const dirtyLeaf = nodeMap.get(dirtyLeafKey);\n\n    if (dirtyLeaf !== undefined) {\n      nodes.push(dirtyLeaf);\n    }\n  }\n\n  for (const [dirtyElementKey, intentionallyMarkedAsDirty] of dirtyElements) {\n    if (!intentionallyMarkedAsDirty) {\n      continue;\n    }\n\n    const dirtyElement = nodeMap.get(dirtyElementKey);\n\n    if (dirtyElement !== undefined && !$isRootNode(dirtyElement)) {\n      nodes.push(dirtyElement);\n    }\n  }\n\n  return nodes;\n}\n\nfunction getChangeType(\n  prevEditorState: null | EditorState,\n  nextEditorState: EditorState,\n  dirtyLeavesSet: Set<NodeKey>,\n  dirtyElementsSet: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,\n  isComposing: boolean,\n): ChangeType {\n  if (\n    prevEditorState === null ||\n    (dirtyLeavesSet.size === 0 && dirtyElementsSet.size === 0 && !isComposing)\n  ) {\n    return OTHER;\n  }\n\n  const nextSelection = nextEditorState._selection;\n  const prevSelection = prevEditorState._selection;\n\n  if (isComposing) {\n    return COMPOSING_CHARACTER;\n  }\n\n  if (\n    !$isRangeSelection(nextSelection) ||\n    !$isRangeSelection(prevSelection) ||\n    !prevSelection.isCollapsed() ||\n    !nextSelection.isCollapsed()\n  ) {\n    return OTHER;\n  }\n\n  const dirtyNodes = getDirtyNodes(\n    nextEditorState,\n    dirtyLeavesSet,\n    dirtyElementsSet,\n  );\n\n  if (dirtyNodes.length === 0) {\n    return OTHER;\n  }\n\n  // Catching the case when inserting new text node into an element (e.g. first char in paragraph/list),\n  // or after existing node.\n  if (dirtyNodes.length > 1) {\n    const nextNodeMap = nextEditorState._nodeMap;\n    const nextAnchorNode = nextNodeMap.get(nextSelection.anchor.key);\n    const prevAnchorNode = nextNodeMap.get(prevSelection.anchor.key);\n\n    if (\n      nextAnchorNode &&\n      prevAnchorNode &&\n      !prevEditorState._nodeMap.has(nextAnchorNode.__key) &&\n      $isTextNode(nextAnchorNode) &&\n      nextAnchorNode.__text.length === 1 &&\n      nextSelection.anchor.offset === 1\n    ) {\n      return INSERT_CHARACTER_AFTER_SELECTION;\n    }\n\n    return OTHER;\n  }\n\n  const nextDirtyNode = dirtyNodes[0];\n\n  const prevDirtyNode = prevEditorState._nodeMap.get(nextDirtyNode.__key);\n\n  if (\n    !$isTextNode(prevDirtyNode) ||\n    !$isTextNode(nextDirtyNode) ||\n    prevDirtyNode.__mode !== nextDirtyNode.__mode\n  ) {\n    return OTHER;\n  }\n\n  const prevText = prevDirtyNode.__text;\n  const nextText = nextDirtyNode.__text;\n\n  if (prevText === nextText) {\n    return OTHER;\n  }\n\n  const nextAnchor = nextSelection.anchor;\n  const prevAnchor = prevSelection.anchor;\n\n  if (nextAnchor.key !== prevAnchor.key || nextAnchor.type !== 'text') {\n    return OTHER;\n  }\n\n  const nextAnchorOffset = nextAnchor.offset;\n  const prevAnchorOffset = prevAnchor.offset;\n  const textDiff = nextText.length - prevText.length;\n\n  if (textDiff === 1 && prevAnchorOffset === nextAnchorOffset - 1) {\n    return INSERT_CHARACTER_AFTER_SELECTION;\n  }\n\n  if (textDiff === -1 && prevAnchorOffset === nextAnchorOffset + 1) {\n    return DELETE_CHARACTER_BEFORE_SELECTION;\n  }\n\n  if (textDiff === -1 && prevAnchorOffset === nextAnchorOffset) {\n    return DELETE_CHARACTER_AFTER_SELECTION;\n  }\n\n  return OTHER;\n}\n\nfunction isTextNodeUnchanged(\n  key: NodeKey,\n  prevEditorState: EditorState,\n  nextEditorState: EditorState,\n): boolean {\n  const prevNode = prevEditorState._nodeMap.get(key);\n  const nextNode = nextEditorState._nodeMap.get(key);\n\n  const prevSelection = prevEditorState._selection;\n  const nextSelection = nextEditorState._selection;\n  const isDeletingLine =\n    $isRangeSelection(prevSelection) &&\n    $isRangeSelection(nextSelection) &&\n    prevSelection.anchor.type === 'element' &&\n    prevSelection.focus.type === 'element' &&\n    nextSelection.anchor.type === 'text' &&\n    nextSelection.focus.type === 'text';\n\n  if (\n    !isDeletingLine &&\n    $isTextNode(prevNode) &&\n    $isTextNode(nextNode) &&\n    prevNode.__parent === nextNode.__parent\n  ) {\n    // This has the assumption that object key order won't change if the\n    // content did not change, which should normally be safe given\n    // the manner in which nodes and exportJSON are typically implemented.\n    return (\n      JSON.stringify(prevEditorState.read(() => prevNode.exportJSON())) ===\n      JSON.stringify(nextEditorState.read(() => nextNode.exportJSON()))\n    );\n  }\n  return false;\n}\n\nfunction createMergeActionGetter(\n  editor: LexicalEditor,\n  delay: number,\n): (\n  prevEditorState: null | EditorState,\n  nextEditorState: EditorState,\n  currentHistoryEntry: null | HistoryStateEntry,\n  dirtyLeaves: Set<NodeKey>,\n  dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,\n  tags: Set<string>,\n) => MergeAction {\n  let prevChangeTime = Date.now();\n  let prevChangeType = OTHER;\n\n  return (\n    prevEditorState,\n    nextEditorState,\n    currentHistoryEntry,\n    dirtyLeaves,\n    dirtyElements,\n    tags,\n  ) => {\n    const changeTime = Date.now();\n\n    // If applying changes from history stack there's no need\n    // to run history logic again, as history entries already calculated\n    if (tags.has('historic')) {\n      prevChangeType = OTHER;\n      prevChangeTime = changeTime;\n      return DISCARD_HISTORY_CANDIDATE;\n    }\n\n    const changeType = getChangeType(\n      prevEditorState,\n      nextEditorState,\n      dirtyLeaves,\n      dirtyElements,\n      editor.isComposing(),\n    );\n\n    const mergeAction = (() => {\n      const isSameEditor =\n        currentHistoryEntry === null || currentHistoryEntry.editor === editor;\n      const shouldPushHistory = tags.has('history-push');\n      const shouldMergeHistory =\n        !shouldPushHistory && isSameEditor && tags.has('history-merge');\n\n      if (shouldMergeHistory) {\n        return HISTORY_MERGE;\n      }\n\n      if (prevEditorState === null) {\n        return HISTORY_PUSH;\n      }\n\n      const selection = nextEditorState._selection;\n      const hasDirtyNodes = dirtyLeaves.size > 0 || dirtyElements.size > 0;\n\n      if (!hasDirtyNodes) {\n        if (selection !== null) {\n          return HISTORY_MERGE;\n        }\n\n        return DISCARD_HISTORY_CANDIDATE;\n      }\n\n      if (\n        shouldPushHistory === false &&\n        changeType !== OTHER &&\n        changeType === prevChangeType &&\n        changeTime < prevChangeTime + delay &&\n        isSameEditor\n      ) {\n        return HISTORY_MERGE;\n      }\n\n      // A single node might have been marked as dirty, but not have changed\n      // due to some node transform reverting the change.\n      if (dirtyLeaves.size === 1) {\n        const dirtyLeafKey = Array.from(dirtyLeaves)[0];\n        if (\n          isTextNodeUnchanged(dirtyLeafKey, prevEditorState, nextEditorState)\n        ) {\n          return HISTORY_MERGE;\n        }\n      }\n\n      return HISTORY_PUSH;\n    })();\n\n    prevChangeTime = changeTime;\n    prevChangeType = changeType;\n\n    return mergeAction;\n  };\n}\n\nfunction redo(editor: LexicalEditor, historyState: HistoryState): void {\n  const redoStack = historyState.redoStack;\n  const undoStack = historyState.undoStack;\n\n  if (redoStack.length !== 0) {\n    const current = historyState.current;\n\n    if (current !== null) {\n      undoStack.push(current);\n      editor.dispatchCommand(CAN_UNDO_COMMAND, true);\n    }\n\n    const historyStateEntry = redoStack.pop();\n\n    if (redoStack.length === 0) {\n      editor.dispatchCommand(CAN_REDO_COMMAND, false);\n    }\n\n    historyState.current = historyStateEntry || null;\n\n    if (historyStateEntry) {\n      historyStateEntry.editor.setEditorState(historyStateEntry.editorState, {\n        tag: 'historic',\n      });\n    }\n  }\n}\n\nfunction undo(editor: LexicalEditor, historyState: HistoryState): void {\n  const redoStack = historyState.redoStack;\n  const undoStack = historyState.undoStack;\n  const undoStackLength = undoStack.length;\n\n  if (undoStackLength !== 0) {\n    const current = historyState.current;\n    const historyStateEntry = undoStack.pop();\n\n    if (current !== null) {\n      redoStack.push(current);\n      editor.dispatchCommand(CAN_REDO_COMMAND, true);\n    }\n\n    if (undoStack.length === 0) {\n      editor.dispatchCommand(CAN_UNDO_COMMAND, false);\n    }\n\n    historyState.current = historyStateEntry || null;\n\n    if (historyStateEntry) {\n      historyStateEntry.editor.setEditorState(historyStateEntry.editorState, {\n        tag: 'historic',\n      });\n    }\n  }\n}\n\nfunction clearHistory(historyState: HistoryState) {\n  historyState.undoStack = [];\n  historyState.redoStack = [];\n  historyState.current = null;\n}\n\n/**\n * Registers necessary listeners to manage undo/redo history stack and related editor commands.\n * It returns `unregister` callback that cleans up all listeners and should be called on editor unmount.\n * @param editor - The lexical editor.\n * @param historyState - The history state, containing the current state and the undo/redo stack.\n * @param delay - The time (in milliseconds) the editor should delay generating a new history stack,\n * instead of merging the current changes with the current stack.\n * @returns The listeners cleanup callback function.\n */\nexport function registerHistory(\n  editor: LexicalEditor,\n  historyState: HistoryState,\n  delay: number,\n): () => void {\n  const getMergeAction = createMergeActionGetter(editor, delay);\n\n  const applyChange = ({\n    editorState,\n    prevEditorState,\n    dirtyLeaves,\n    dirtyElements,\n    tags,\n  }: {\n    editorState: EditorState;\n    prevEditorState: EditorState;\n    dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>;\n    dirtyLeaves: Set<NodeKey>;\n    tags: Set<string>;\n  }): void => {\n    const current = historyState.current;\n    const redoStack = historyState.redoStack;\n    const undoStack = historyState.undoStack;\n    const currentEditorState = current === null ? null : current.editorState;\n\n    if (current !== null && editorState === currentEditorState) {\n      return;\n    }\n\n    const mergeAction = getMergeAction(\n      prevEditorState,\n      editorState,\n      current,\n      dirtyLeaves,\n      dirtyElements,\n      tags,\n    );\n\n    if (mergeAction === HISTORY_PUSH) {\n      if (redoStack.length !== 0) {\n        historyState.redoStack = [];\n        editor.dispatchCommand(CAN_REDO_COMMAND, false);\n      }\n\n      if (current !== null) {\n        undoStack.push({\n          ...current,\n        });\n        editor.dispatchCommand(CAN_UNDO_COMMAND, true);\n      }\n    } else if (mergeAction === DISCARD_HISTORY_CANDIDATE) {\n      return;\n    }\n\n    // Else we merge\n    historyState.current = {\n      editor,\n      editorState,\n    };\n  };\n\n  const unregister = mergeRegister(\n    editor.registerCommand(\n      UNDO_COMMAND,\n      () => {\n        undo(editor, historyState);\n        return true;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand(\n      REDO_COMMAND,\n      () => {\n        redo(editor, historyState);\n        return true;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand(\n      CLEAR_EDITOR_COMMAND,\n      () => {\n        clearHistory(historyState);\n        return false;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand(\n      CLEAR_HISTORY_COMMAND,\n      () => {\n        clearHistory(historyState);\n        editor.dispatchCommand(CAN_REDO_COMMAND, false);\n        editor.dispatchCommand(CAN_UNDO_COMMAND, false);\n        return true;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerUpdateListener(applyChange),\n  );\n\n  return unregister;\n}\n\n/**\n * Creates an empty history state.\n * @returns - The empty history state, as an object.\n */\nexport function createEmptyHistoryState(): HistoryState {\n  return {\n    current: null,\n    redoStack: [],\n    undoStack: [],\n  };\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/html/__tests__/unit/LexicalHtml.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\n//@ts-ignore-next-line\nimport type {RangeSelection} from 'lexical';\n\nimport {createHeadlessEditor} from '@lexical/headless';\nimport {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html';\nimport {LinkNode} from '@lexical/link';\nimport {ListItemNode, ListNode} from '@lexical/list';\nimport {\n  $createParagraphNode,\n  $createRangeSelection,\n  $createTextNode,\n  $getRoot,\n} from 'lexical';\nimport {HeadingNode} from \"@lexical/rich-text/LexicalHeadingNode\";\nimport {QuoteNode} from \"@lexical/rich-text/LexicalQuoteNode\";\n\ndescribe('HTML', () => {\n  type Input = Array<{\n    name: string;\n    html: string;\n    initializeEditorState: () => void;\n  }>;\n\n  const HTML_SERIALIZE: Input = [\n    {\n      html: '<p><br></p>',\n      initializeEditorState: () => {\n        $getRoot().append($createParagraphNode());\n      },\n      name: 'Empty editor state',\n    },\n  ];\n  for (const {name, html, initializeEditorState} of HTML_SERIALIZE) {\n    test(`[Lexical -> HTML]: ${name}`, () => {\n      const editor = createHeadlessEditor({\n        nodes: [\n          HeadingNode,\n          ListNode,\n          ListItemNode,\n          QuoteNode,\n          LinkNode,\n        ],\n      });\n\n      editor.update(initializeEditorState, {\n        discrete: true,\n      });\n\n      expect(\n        editor.getEditorState().read(() => $generateHtmlFromNodes(editor)),\n      ).toBe(html);\n    });\n  }\n\n  test(`[Lexical -> HTML]: Use provided selection`, () => {\n    const editor = createHeadlessEditor({\n      nodes: [\n        HeadingNode,\n        ListNode,\n        ListItemNode,\n        QuoteNode,\n        LinkNode,\n      ],\n    });\n\n    let selection: RangeSelection | null = null;\n\n    editor.update(\n      () => {\n        const root = $getRoot();\n        const p1 = $createParagraphNode();\n        const text1 = $createTextNode('Hello');\n        p1.append(text1);\n        const p2 = $createParagraphNode();\n        const text2 = $createTextNode('World');\n        p2.append(text2);\n        root.append(p1).append(p2);\n        // Root\n        // - ParagraphNode\n        // -- TextNode \"Hello\"\n        // - ParagraphNode\n        // -- TextNode \"World\"\n        p1.select(0, text1.getTextContentSize());\n        selection = $createRangeSelection();\n        selection.setTextNodeRange(text2, 0, text2, text2.getTextContentSize());\n      },\n      {\n        discrete: true,\n      },\n    );\n\n    let html = '';\n\n    editor.update(() => {\n      html = $generateHtmlFromNodes(editor, selection);\n    });\n\n    expect(html).toBe('World');\n  });\n\n  test(`[Lexical -> HTML]: Default selection (undefined) should serialize entire editor state`, () => {\n    const editor = createHeadlessEditor({\n      nodes: [\n        HeadingNode,\n        ListNode,\n        ListItemNode,\n        QuoteNode,\n        LinkNode,\n      ],\n    });\n\n    editor.update(\n      () => {\n        const root = $getRoot();\n        const p1 = $createParagraphNode();\n        const text1 = $createTextNode('Hello');\n        p1.append(text1);\n        const p2 = $createParagraphNode();\n        const text2 = $createTextNode('World');\n        p2.append(text2);\n        root.append(p1).append(p2);\n        // Root\n        // - ParagraphNode\n        // -- TextNode \"Hello\"\n        // - ParagraphNode\n        // -- TextNode \"World\"\n        p1.select(0, text1.getTextContentSize());\n      },\n      {\n        discrete: true,\n      },\n    );\n\n    let html = '';\n\n    editor.update(() => {\n      html = $generateHtmlFromNodes(editor);\n    });\n\n    expect(html).toBe(\n      '<p>Hello</p>\\n<p>World</p>',\n    );\n  });\n\n  test(`If alignment is set on the paragraph, don't overwrite from parent empty format`, () => {\n    const editor = createHeadlessEditor();\n    const parser = new DOMParser();\n    const rightAlignedParagraphInDiv =\n      '<div><p style=\"text-align: center;\">Hello world!</p></div>';\n\n    editor.update(\n      () => {\n        const root = $getRoot();\n        const dom = parser.parseFromString(\n          rightAlignedParagraphInDiv,\n          'text/html',\n        );\n        const nodes = $generateNodesFromDOM(editor, dom);\n        root.append(...nodes);\n      },\n      {discrete: true},\n    );\n\n    let html = '';\n\n    editor.update(() => {\n      html = $generateHtmlFromNodes(editor);\n    });\n\n    expect(html).toBe(\n      '<p class=\"align-center\">Hello world!</p>',\n    );\n  });\n\n  test(`If alignment is set on the paragraph, it should take precedence over its parent block alignment`, () => {\n    const editor = createHeadlessEditor();\n    const parser = new DOMParser();\n    const rightAlignedParagraphInDiv =\n      '<div style=\"text-align: right;\"><p style=\"text-align: center;\">Hello world!</p></div>';\n\n    editor.update(\n      () => {\n        const root = $getRoot();\n        const dom = parser.parseFromString(\n          rightAlignedParagraphInDiv,\n          'text/html',\n        );\n        const nodes = $generateNodesFromDOM(editor, dom);\n        root.append(...nodes);\n      },\n      {discrete: true},\n    );\n\n    let html = '';\n\n    editor.update(() => {\n      html = $generateHtmlFromNodes(editor);\n    });\n\n    expect(html).toBe(\n      '<p class=\"align-center\">Hello world!</p>',\n    );\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/html/index.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {\n  BaseSelection,\n  DOMChildConversion,\n  DOMConversion,\n  DOMConversionFn,\n  LexicalEditor,\n  LexicalNode,\n} from 'lexical';\n\nimport {$sliceSelectedTextNodeContent} from '@lexical/selection';\nimport {isBlockDomNode, isHTMLElement} from '@lexical/utils';\nimport {\n  $cloneWithProperties,\n  $createLineBreakNode,\n  $createParagraphNode,\n  $getRoot,\n  $isBlockElementNode,\n  $isElementNode,\n  $isRootOrShadowRoot,\n  $isTextNode,\n  ArtificialNode__DO_NOT_USE,\n  ElementNode,\n  isInlineDomNode,\n} from 'lexical';\n\n/**\n * How you parse your html string to get a document is left up to you. In the browser you can use the native\n * DOMParser API to generate a document (see clipboard.ts), but to use in a headless environment you can use JSDom\n * or an equivalent library and pass in the document here.\n */\nexport function $generateNodesFromDOM(\n  editor: LexicalEditor,\n  dom: Document,\n): Array<LexicalNode> {\n  const elements = dom.body ? dom.body.childNodes : [];\n  let lexicalNodes: Array<LexicalNode> = [];\n  const allArtificialNodes: Array<ArtificialNode__DO_NOT_USE> = [];\n  for (let i = 0; i < elements.length; i++) {\n    const element = elements[i];\n    if (!IGNORE_TAGS.has(element.nodeName)) {\n      const lexicalNode = $createNodesFromDOM(\n        element,\n        editor,\n        allArtificialNodes,\n        false,\n      );\n      if (lexicalNode !== null) {\n        lexicalNodes = lexicalNodes.concat(lexicalNode);\n      }\n    }\n  }\n\n  $unwrapArtificalNodes(allArtificialNodes);\n\n  return lexicalNodes;\n}\n\nexport function $generateHtmlFromNodes(\n  editor: LexicalEditor,\n  selection?: BaseSelection | null,\n): string {\n  if (\n    typeof document === 'undefined' ||\n    (typeof window === 'undefined' && typeof global.window === 'undefined')\n  ) {\n    throw new Error(\n      'To use $generateHtmlFromNodes in headless mode please initialize a headless browser implementation such as JSDom before calling this function.',\n    );\n  }\n\n  const container = document.createElement('div');\n  const root = $getRoot();\n  const topLevelChildren = root.getChildren();\n\n  for (let i = 0; i < topLevelChildren.length; i++) {\n    const topLevelNode = topLevelChildren[i];\n    $appendNodesToHTML(editor, topLevelNode, container, selection);\n  }\n\n  const nodeCode = [];\n  for (const node of container.childNodes) {\n    if (\"outerHTML\" in node) {\n      nodeCode.push(node.outerHTML)\n    } else {\n      const wrap = document.createElement('div');\n      wrap.appendChild(node.cloneNode(true));\n      nodeCode.push(wrap.innerHTML);\n    }\n  }\n\n  return nodeCode.join('\\n');\n}\n\nfunction $appendNodesToHTML(\n  editor: LexicalEditor,\n  currentNode: LexicalNode,\n  parentElement: HTMLElement | DocumentFragment,\n  selection: BaseSelection | null = null,\n): boolean {\n  let shouldInclude =\n    selection !== null ? currentNode.isSelected(selection) : true;\n  const shouldExclude =\n    $isElementNode(currentNode) && currentNode.excludeFromCopy('html');\n  let target = currentNode;\n\n  if (selection !== null) {\n    let clone = $cloneWithProperties(currentNode);\n    clone =\n      $isTextNode(clone) && selection !== null\n        ? $sliceSelectedTextNodeContent(selection, clone)\n        : clone;\n    target = clone;\n  }\n  const children = $isElementNode(target) ? target.getChildren() : [];\n  const registeredNode = editor._nodes.get(target.getType());\n  let exportOutput;\n\n  // Use HTMLConfig overrides, if available.\n  if (registeredNode && registeredNode.exportDOM !== undefined) {\n    exportOutput = registeredNode.exportDOM(editor, target);\n  } else {\n    exportOutput = target.exportDOM(editor);\n  }\n\n  const {element, after} = exportOutput;\n\n  if (!element) {\n    return false;\n  }\n\n  const fragment = document.createDocumentFragment();\n\n  for (let i = 0; i < children.length; i++) {\n    const childNode = children[i];\n    const shouldIncludeChild = $appendNodesToHTML(\n      editor,\n      childNode,\n      fragment,\n      selection,\n    );\n\n    if (\n      !shouldInclude &&\n      $isElementNode(currentNode) &&\n      shouldIncludeChild &&\n      currentNode.extractWithChild(childNode, selection, 'html')\n    ) {\n      shouldInclude = true;\n    }\n  }\n\n  if (shouldInclude && !shouldExclude) {\n    if (isHTMLElement(element)) {\n      element.append(fragment);\n    }\n    parentElement.append(element);\n\n    if (after) {\n      const newElement = after.call(target, element);\n      if (newElement) {\n        element.replaceWith(newElement);\n      }\n    }\n  } else {\n    parentElement.append(fragment);\n  }\n\n  return shouldInclude;\n}\n\nfunction getConversionFunction(\n  domNode: Node,\n  editor: LexicalEditor,\n): DOMConversionFn | null {\n  const {nodeName} = domNode;\n\n  const cachedConversions = editor._htmlConversions.get(nodeName.toLowerCase());\n\n  let currentConversion: DOMConversion | null = null;\n\n  if (cachedConversions !== undefined) {\n    for (const cachedConversion of cachedConversions) {\n      const domConversion = cachedConversion(domNode);\n      if (\n        domConversion !== null &&\n        (currentConversion === null ||\n          (currentConversion.priority || 0) < (domConversion.priority || 0))\n      ) {\n        currentConversion = domConversion;\n      }\n    }\n  }\n\n  return currentConversion !== null ? currentConversion.conversion : null;\n}\n\nconst IGNORE_TAGS = new Set(['STYLE', 'SCRIPT']);\n\nfunction $createNodesFromDOM(\n  node: Node,\n  editor: LexicalEditor,\n  allArtificialNodes: Array<ArtificialNode__DO_NOT_USE>,\n  hasBlockAncestorLexicalNode: boolean,\n  forChildMap: Map<string, DOMChildConversion> = new Map(),\n  parentLexicalNode?: LexicalNode | null | undefined,\n): Array<LexicalNode> {\n  let lexicalNodes: Array<LexicalNode> = [];\n\n  if (IGNORE_TAGS.has(node.nodeName)) {\n    return lexicalNodes;\n  }\n\n  let currentLexicalNode = null;\n  const transformFunction = getConversionFunction(node, editor);\n  const transformOutput = transformFunction\n    ? transformFunction(node as HTMLElement)\n    : null;\n  let postTransform = null;\n\n  if (transformOutput !== null) {\n    postTransform = transformOutput.after;\n    const transformNodes = transformOutput.node;\n\n    if (transformNodes === 'ignore') {\n      return lexicalNodes;\n    }\n\n    currentLexicalNode = Array.isArray(transformNodes)\n      ? transformNodes[transformNodes.length - 1]\n      : transformNodes;\n\n    if (currentLexicalNode !== null) {\n      for (const [, forChildFunction] of forChildMap) {\n        currentLexicalNode = forChildFunction(\n          currentLexicalNode,\n          parentLexicalNode,\n        );\n\n        if (!currentLexicalNode) {\n          break;\n        }\n      }\n\n      if (currentLexicalNode) {\n        lexicalNodes.push(\n          ...(Array.isArray(transformNodes)\n            ? transformNodes\n            : [currentLexicalNode]),\n        );\n      }\n    }\n\n    if (transformOutput.forChild != null) {\n      forChildMap.set(node.nodeName, transformOutput.forChild);\n    }\n  }\n\n  // If the DOM node doesn't have a transformer, we don't know what\n  // to do with it but we still need to process any childNodes.\n  const children = node.childNodes;\n  let childLexicalNodes = [];\n\n  const hasBlockAncestorLexicalNodeForChildren =\n    currentLexicalNode != null && $isRootOrShadowRoot(currentLexicalNode)\n      ? false\n      : (currentLexicalNode != null &&\n          $isBlockElementNode(currentLexicalNode)) ||\n        hasBlockAncestorLexicalNode;\n\n  for (let i = 0; i < children.length; i++) {\n    childLexicalNodes.push(\n      ...$createNodesFromDOM(\n        children[i],\n        editor,\n        allArtificialNodes,\n        hasBlockAncestorLexicalNodeForChildren,\n        new Map(forChildMap),\n        currentLexicalNode,\n      ),\n    );\n  }\n\n  if (postTransform != null) {\n    childLexicalNodes = postTransform(childLexicalNodes);\n  }\n\n  if (isBlockDomNode(node)) {\n    if (!hasBlockAncestorLexicalNodeForChildren) {\n      childLexicalNodes = wrapContinuousInlines(\n        node,\n        childLexicalNodes,\n        $createParagraphNode,\n      );\n    } else {\n      childLexicalNodes = wrapContinuousInlines(node, childLexicalNodes, () => {\n        const artificialNode = new ArtificialNode__DO_NOT_USE();\n        allArtificialNodes.push(artificialNode);\n        return artificialNode;\n      });\n    }\n  }\n\n  if (currentLexicalNode == null) {\n    if (childLexicalNodes.length > 0) {\n      // If it hasn't been converted to a LexicalNode, we hoist its children\n      // up to the same level as it.\n      lexicalNodes = lexicalNodes.concat(childLexicalNodes);\n    } else {\n      if (isBlockDomNode(node) && isDomNodeBetweenTwoInlineNodes(node)) {\n        // Empty block dom node that hasnt been converted, we replace it with a linebreak if its between inline nodes\n        lexicalNodes = lexicalNodes.concat($createLineBreakNode());\n      }\n    }\n  } else {\n    if ($isElementNode(currentLexicalNode)) {\n      // If the current node is a ElementNode after conversion,\n      // we can append all the children to it.\n      currentLexicalNode.append(...childLexicalNodes);\n    }\n  }\n\n  return lexicalNodes;\n}\n\nfunction wrapContinuousInlines(\n  domNode: Node,\n  nodes: Array<LexicalNode>,\n  createWrapperFn: () => ElementNode,\n): Array<LexicalNode> {\n  const out: Array<LexicalNode> = [];\n  let continuousInlines: Array<LexicalNode> = [];\n  // wrap contiguous inline child nodes in para\n  for (let i = 0; i < nodes.length; i++) {\n    const node = nodes[i];\n    if ($isBlockElementNode(node)) {\n      out.push(node);\n    } else {\n      continuousInlines.push(node);\n      if (\n        i === nodes.length - 1 ||\n        (i < nodes.length - 1 && $isBlockElementNode(nodes[i + 1]))\n      ) {\n        const wrapper = createWrapperFn();\n        wrapper.append(...continuousInlines);\n        out.push(wrapper);\n        continuousInlines = [];\n      }\n    }\n  }\n  return out;\n}\n\nfunction $unwrapArtificalNodes(\n  allArtificialNodes: Array<ArtificialNode__DO_NOT_USE>,\n) {\n  for (const node of allArtificialNodes) {\n    if (node.getNextSibling() instanceof ArtificialNode__DO_NOT_USE) {\n      node.insertAfter($createLineBreakNode());\n    }\n  }\n  // Replace artificial node with it's children\n  for (const node of allArtificialNodes) {\n    const children = node.getChildren();\n    for (const child of children) {\n      node.insertBefore(child);\n    }\n    node.remove();\n  }\n}\n\nfunction isDomNodeBetweenTwoInlineNodes(node: Node): boolean {\n  if (node.nextSibling == null || node.previousSibling == null) {\n    return false;\n  }\n  return (\n    isInlineDomNode(node.nextSibling) && isInlineDomNode(node.previousSibling)\n  );\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/link/LexicalMentionNode.ts",
    "content": "import {\n    DecoratorNode,\n    DOMConversion,\n    DOMConversionMap, DOMConversionOutput,\n    type EditorConfig,\n    LexicalEditor, LexicalNode,\n    SerializedLexicalNode,\n    Spread\n} from \"lexical\";\nimport {EditorDecoratorAdapter} from \"../../ui/framework/decorator\";\n\nexport type SerializedMentionNode = Spread<{\n    user_id: number;\n    user_name: string;\n    user_slug: string;\n}, SerializedLexicalNode>\n\nexport class MentionNode extends DecoratorNode<EditorDecoratorAdapter> {\n    __user_id: number = 0;\n    __user_name: string = '';\n    __user_slug: string = '';\n\n    static getType(): string {\n        return 'mention';\n    }\n    static clone(node: MentionNode): MentionNode {\n        const newNode = new MentionNode(node.__key);\n        newNode.__user_id = node.__user_id;\n        newNode.__user_name = node.__user_name;\n        newNode.__user_slug = node.__user_slug;\n        return newNode;\n    }\n\n    setUserDetails(userId: number, userName: string, userSlug: string): void {\n        const self = this.getWritable();\n        self.__user_id = userId;\n        self.__user_name = userName;\n        self.__user_slug = userSlug;\n    }\n\n    hasUserSet(): boolean {\n        return this.__user_id > 0;\n    }\n\n    isInline(): boolean {\n        return true;\n    }\n\n    isParentRequired(): boolean {\n        return true;\n    }\n\n    decorate(editor: LexicalEditor, config: EditorConfig): EditorDecoratorAdapter {\n        return {\n            type: 'mention',\n            getNode: () => this,\n        };\n    }\n\n    createDOM(_config: EditorConfig, _editor: LexicalEditor) {\n        const element = document.createElement('a');\n        element.setAttribute('target', '_blank');\n        element.setAttribute('href', window.baseUrl('/user/' + this.__user_slug));\n        element.setAttribute('data-mention-user-id', String(this.__user_id));\n        element.setAttribute('title', '@' + this.__user_name);\n        element.textContent = '@' + this.__user_name;\n        return element;\n    }\n\n    updateDOM(prevNode: MentionNode): boolean {\n        return prevNode.__user_id !== this.__user_id;\n    }\n\n    static importDOM(): DOMConversionMap|null {\n        return {\n            a(node: HTMLElement): DOMConversion|null {\n                if (node.hasAttribute('data-mention-user-id')) {\n                    return {\n                        conversion: (element: HTMLElement): DOMConversionOutput|null => {\n                            const node = new MentionNode();\n                            node.setUserDetails(\n                                Number(element.getAttribute('data-mention-user-id') || '0'),\n                                element.innerText.replace(/^@/, ''),\n                                element.getAttribute('href')?.split('/user/')[1] || ''\n                            );\n\n                            return {\n                                node,\n                                after(childNodes): LexicalNode[] {\n                                    return [];\n                                }\n                            };\n                        },\n                        priority: 4,\n                    };\n                }\n                return null;\n            },\n        };\n    }\n\n    exportJSON(): SerializedMentionNode {\n        return {\n            type: 'mention',\n            version: 1,\n            user_id: this.__user_id,\n            user_name: this.__user_name,\n            user_slug: this.__user_slug,\n        };\n    }\n\n    static importJSON(serializedNode: SerializedMentionNode): MentionNode {\n        return $createMentionNode(serializedNode.user_id, serializedNode.user_name, serializedNode.user_slug);\n    }\n}\n\nexport function $createMentionNode(userId: number, userName: string, userSlug: string) {\n    const node = new MentionNode();\n    node.setUserDetails(userId, userName, userSlug);\n    return node;\n}\n\nexport function $isMentionNode(node: LexicalNode | null | undefined): node is MentionNode {\n    return node instanceof MentionNode;\n}"
  },
  {
    "path": "resources/js/wysiwyg/lexical/link/__tests__/unit/LexicalAutoLinkNode.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {\n  $createAutoLinkNode,\n  $isAutoLinkNode,\n  $toggleLink,\n  AutoLinkNode,\n  SerializedAutoLinkNode,\n} from '@lexical/link';\nimport {\n  $getRoot,\n  $selectAll,\n  ParagraphNode,\n  SerializedParagraphNode,\n  TextNode,\n} from 'lexical';\nimport {initializeUnitTest} from 'lexical/__tests__/utils';\n\nconst editorConfig = Object.freeze({\n  namespace: '',\n  theme: {\n    link: 'my-autolink-class',\n    text: {\n      bold: 'my-bold-class',\n      code: 'my-code-class',\n      hashtag: 'my-hashtag-class',\n      italic: 'my-italic-class',\n      strikethrough: 'my-strikethrough-class',\n      underline: 'my-underline-class',\n      underlineStrikethrough: 'my-underline-strikethrough-class',\n    },\n  },\n});\n\ndescribe('LexicalAutoAutoLinkNode tests', () => {\n  initializeUnitTest((testEnv) => {\n    test('AutoAutoLinkNode.constructor', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const actutoLinkNode = new AutoLinkNode('/');\n\n        expect(actutoLinkNode.__type).toBe('autolink');\n        expect(actutoLinkNode.__url).toBe('/');\n        expect(actutoLinkNode.__isUnlinked).toBe(false);\n      });\n\n      expect(() => new AutoLinkNode('')).toThrow();\n    });\n\n    test('AutoAutoLinkNode.constructor with isUnlinked param set to true', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const actutoLinkNode = new AutoLinkNode('/', {\n          isUnlinked: true,\n        });\n\n        expect(actutoLinkNode.__type).toBe('autolink');\n        expect(actutoLinkNode.__url).toBe('/');\n        expect(actutoLinkNode.__isUnlinked).toBe(true);\n      });\n\n      expect(() => new AutoLinkNode('')).toThrow();\n    });\n\n    ///\n\n    test('LineBreakNode.clone()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const autoLinkNode = new AutoLinkNode('/');\n\n        const clone = AutoLinkNode.clone(autoLinkNode);\n\n        expect(clone).not.toBe(autoLinkNode);\n        expect(clone).toStrictEqual(autoLinkNode);\n      });\n    });\n\n    test('AutoLinkNode.getURL()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const autoLinkNode = new AutoLinkNode('https://example.com/foo');\n\n        expect(autoLinkNode.getURL()).toBe('https://example.com/foo');\n      });\n    });\n\n    test('AutoLinkNode.setURL()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const autoLinkNode = new AutoLinkNode('https://example.com/foo');\n\n        expect(autoLinkNode.getURL()).toBe('https://example.com/foo');\n\n        autoLinkNode.setURL('https://example.com/bar');\n\n        expect(autoLinkNode.getURL()).toBe('https://example.com/bar');\n      });\n    });\n\n    test('AutoLinkNode.getTarget()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const autoLinkNode = new AutoLinkNode('https://example.com/foo', {\n          target: '_blank',\n        });\n\n        expect(autoLinkNode.getTarget()).toBe('_blank');\n      });\n    });\n\n    test('AutoLinkNode.setTarget()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const autoLinkNode = new AutoLinkNode('https://example.com/foo', {\n          target: '_blank',\n        });\n\n        expect(autoLinkNode.getTarget()).toBe('_blank');\n\n        autoLinkNode.setTarget('_self');\n\n        expect(autoLinkNode.getTarget()).toBe('_self');\n      });\n    });\n\n    test('AutoLinkNode.getRel()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const autoLinkNode = new AutoLinkNode('https://example.com/foo', {\n          rel: 'noopener noreferrer',\n          target: '_blank',\n        });\n\n        expect(autoLinkNode.getRel()).toBe('noopener noreferrer');\n      });\n    });\n\n    test('AutoLinkNode.setRel()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const autoLinkNode = new AutoLinkNode('https://example.com/foo', {\n          rel: 'noopener',\n          target: '_blank',\n        });\n\n        expect(autoLinkNode.getRel()).toBe('noopener');\n\n        autoLinkNode.setRel('noopener noreferrer');\n\n        expect(autoLinkNode.getRel()).toBe('noopener noreferrer');\n      });\n    });\n\n    test('AutoLinkNode.getTitle()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const autoLinkNode = new AutoLinkNode('https://example.com/foo', {\n          title: 'Hello world',\n        });\n\n        expect(autoLinkNode.getTitle()).toBe('Hello world');\n      });\n    });\n\n    test('AutoLinkNode.setTitle()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const autoLinkNode = new AutoLinkNode('https://example.com/foo', {\n          title: 'Hello world',\n        });\n\n        expect(autoLinkNode.getTitle()).toBe('Hello world');\n\n        autoLinkNode.setTitle('World hello');\n\n        expect(autoLinkNode.getTitle()).toBe('World hello');\n      });\n    });\n\n    test('AutoLinkNode.getIsUnlinked()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const autoLinkNode = new AutoLinkNode('/', {\n          isUnlinked: true,\n        });\n        expect(autoLinkNode.getIsUnlinked()).toBe(true);\n      });\n    });\n\n    test('AutoLinkNode.setIsUnlinked()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const autoLinkNode = new AutoLinkNode('/');\n        expect(autoLinkNode.getIsUnlinked()).toBe(false);\n        autoLinkNode.setIsUnlinked(true);\n        expect(autoLinkNode.getIsUnlinked()).toBe(true);\n      });\n    });\n\n    test('AutoLinkNode.createDOM()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const autoLinkNode = new AutoLinkNode('https://example.com/foo');\n\n        expect(autoLinkNode.createDOM(editorConfig).outerHTML).toBe(\n          '<a href=\"https://example.com/foo\" class=\"my-autolink-class\"></a>',\n        );\n        expect(\n          autoLinkNode.createDOM({\n            namespace: '',\n            theme: {},\n          }).outerHTML,\n        ).toBe('<a href=\"https://example.com/foo\"></a>');\n      });\n    });\n\n    test('AutoLinkNode.createDOM() for unlinked', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const autoLinkNode = new AutoLinkNode('https://example.com/foo', {\n          isUnlinked: true,\n        });\n\n        expect(autoLinkNode.createDOM(editorConfig).outerHTML).toBe(\n          `<span>${autoLinkNode.getTextContent()}</span>`,\n        );\n      });\n    });\n\n    test('AutoLinkNode.createDOM() with target, rel and title', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const autoLinkNode = new AutoLinkNode('https://example.com/foo', {\n          rel: 'noopener noreferrer',\n          target: '_blank',\n          title: 'Hello world',\n        });\n\n        expect(autoLinkNode.createDOM(editorConfig).outerHTML).toBe(\n          '<a href=\"https://example.com/foo\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Hello world\" class=\"my-autolink-class\"></a>',\n        );\n        expect(\n          autoLinkNode.createDOM({\n            namespace: '',\n            theme: {},\n          }).outerHTML,\n        ).toBe(\n          '<a href=\"https://example.com/foo\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Hello world\"></a>',\n        );\n      });\n    });\n\n    test('AutoLinkNode.updateDOM()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const autoLinkNode = new AutoLinkNode('https://example.com/foo');\n\n        const domElement = autoLinkNode.createDOM(editorConfig);\n\n        expect(autoLinkNode.createDOM(editorConfig).outerHTML).toBe(\n          '<a href=\"https://example.com/foo\" class=\"my-autolink-class\"></a>',\n        );\n\n        const newAutoLinkNode = new AutoLinkNode('https://example.com/bar');\n        const result = newAutoLinkNode.updateDOM(\n          autoLinkNode,\n          domElement,\n          editorConfig,\n        );\n\n        expect(result).toBe(false);\n        expect(domElement.outerHTML).toBe(\n          '<a href=\"https://example.com/bar\" class=\"my-autolink-class\"></a>',\n        );\n      });\n    });\n\n    test('AutoLinkNode.updateDOM() with target, rel and title', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const autoLinkNode = new AutoLinkNode('https://example.com/foo', {\n          rel: 'noopener noreferrer',\n          target: '_blank',\n          title: 'Hello world',\n        });\n\n        const domElement = autoLinkNode.createDOM(editorConfig);\n\n        expect(autoLinkNode.createDOM(editorConfig).outerHTML).toBe(\n          '<a href=\"https://example.com/foo\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Hello world\" class=\"my-autolink-class\"></a>',\n        );\n\n        const newAutoLinkNode = new AutoLinkNode('https://example.com/bar', {\n          rel: 'noopener',\n          target: '_self',\n          title: 'World hello',\n        });\n        const result = newAutoLinkNode.updateDOM(\n          autoLinkNode,\n          domElement,\n          editorConfig,\n        );\n\n        expect(result).toBe(false);\n        expect(domElement.outerHTML).toBe(\n          '<a href=\"https://example.com/bar\" target=\"_self\" rel=\"noopener\" title=\"World hello\" class=\"my-autolink-class\"></a>',\n        );\n      });\n    });\n\n    test('AutoLinkNode.updateDOM() with undefined target, undefined rel and undefined title', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const autoLinkNode = new AutoLinkNode('https://example.com/foo', {\n          rel: 'noopener noreferrer',\n          target: '_blank',\n          title: 'Hello world',\n        });\n\n        const domElement = autoLinkNode.createDOM(editorConfig);\n\n        expect(autoLinkNode.createDOM(editorConfig).outerHTML).toBe(\n          '<a href=\"https://example.com/foo\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Hello world\" class=\"my-autolink-class\"></a>',\n        );\n\n        const newNode = new AutoLinkNode('https://example.com/bar');\n        const result = newNode.updateDOM(\n          autoLinkNode,\n          domElement,\n          editorConfig,\n        );\n\n        expect(result).toBe(false);\n        expect(domElement.outerHTML).toBe(\n          '<a href=\"https://example.com/bar\" class=\"my-autolink-class\"></a>',\n        );\n      });\n    });\n\n    test('AutoLinkNode.updateDOM() with isUnlinked \"true\"', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const autoLinkNode = new AutoLinkNode('https://example.com/foo', {\n          isUnlinked: false,\n        });\n\n        const domElement = autoLinkNode.createDOM(editorConfig);\n        expect(domElement.outerHTML).toBe(\n          '<a href=\"https://example.com/foo\" class=\"my-autolink-class\"></a>',\n        );\n\n        const newAutoLinkNode = new AutoLinkNode('https://example.com/bar', {\n          isUnlinked: true,\n        });\n        const newDomElement = newAutoLinkNode.createDOM(editorConfig);\n        expect(newDomElement.outerHTML).toBe(\n          `<span>${newAutoLinkNode.getTextContent()}</span>`,\n        );\n\n        const result = newAutoLinkNode.updateDOM(\n          autoLinkNode,\n          domElement,\n          editorConfig,\n        );\n        expect(result).toBe(true);\n      });\n    });\n\n    test('AutoLinkNode.canInsertTextBefore()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const autoLinkNode = new AutoLinkNode('https://example.com/foo');\n\n        expect(autoLinkNode.canInsertTextBefore()).toBe(false);\n      });\n    });\n\n    test('AutoLinkNode.canInsertTextAfter()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const autoLinkNode = new AutoLinkNode('https://example.com/foo');\n        expect(autoLinkNode.canInsertTextAfter()).toBe(false);\n      });\n    });\n\n    test('$createAutoLinkNode()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const autoLinkNode = new AutoLinkNode('https://example.com/foo');\n        const createdAutoLinkNode = $createAutoLinkNode(\n          'https://example.com/foo',\n        );\n\n        expect(autoLinkNode.__type).toEqual(createdAutoLinkNode.__type);\n        expect(autoLinkNode.__parent).toEqual(createdAutoLinkNode.__parent);\n        expect(autoLinkNode.__url).toEqual(createdAutoLinkNode.__url);\n        expect(autoLinkNode.__isUnlinked).toEqual(\n          createdAutoLinkNode.__isUnlinked,\n        );\n        expect(autoLinkNode.__key).not.toEqual(createdAutoLinkNode.__key);\n      });\n    });\n\n    test('$createAutoLinkNode() with target, rel, isUnlinked and title', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const autoLinkNode = new AutoLinkNode('https://example.com/foo', {\n          rel: 'noopener noreferrer',\n          target: '_blank',\n          title: 'Hello world',\n        });\n\n        const createdAutoLinkNode = $createAutoLinkNode(\n          'https://example.com/foo',\n          {\n            isUnlinked: true,\n            rel: 'noopener noreferrer',\n            target: '_blank',\n            title: 'Hello world',\n          },\n        );\n\n        expect(autoLinkNode.__type).toEqual(createdAutoLinkNode.__type);\n        expect(autoLinkNode.__parent).toEqual(createdAutoLinkNode.__parent);\n        expect(autoLinkNode.__url).toEqual(createdAutoLinkNode.__url);\n        expect(autoLinkNode.__target).toEqual(createdAutoLinkNode.__target);\n        expect(autoLinkNode.__rel).toEqual(createdAutoLinkNode.__rel);\n        expect(autoLinkNode.__title).toEqual(createdAutoLinkNode.__title);\n        expect(autoLinkNode.__key).not.toEqual(createdAutoLinkNode.__key);\n        expect(autoLinkNode.__isUnlinked).not.toEqual(\n          createdAutoLinkNode.__isUnlinked,\n        );\n      });\n    });\n\n    test('$isAutoLinkNode()', async () => {\n      const {editor} = testEnv;\n      await editor.update(() => {\n        const autoLinkNode = new AutoLinkNode('');\n        expect($isAutoLinkNode(autoLinkNode)).toBe(true);\n      });\n    });\n\n    test('$toggleLink applies the title attribute when creating', async () => {\n      const {editor} = testEnv;\n      await editor.update(() => {\n        const p = new ParagraphNode();\n        p.append(new TextNode('Some text'));\n        $getRoot().append(p);\n      });\n\n      await editor.update(() => {\n        $selectAll();\n        $toggleLink('https://lexical.dev/', {title: 'Lexical Website'});\n      });\n\n      const paragraph = editor!.getEditorState().toJSON().root\n        .children[0] as SerializedParagraphNode;\n      const link = paragraph.children[0] as SerializedAutoLinkNode;\n      expect(link.title).toBe('Lexical Website');\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/link/__tests__/unit/LexicalLinkNode.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {\n  $createLinkNode,\n  $isLinkNode,\n  $toggleLink,\n  LinkNode,\n  SerializedLinkNode,\n} from '@lexical/link';\nimport {\n  $getRoot,\n  $selectAll,\n  ParagraphNode,\n  SerializedParagraphNode,\n  TextNode,\n} from 'lexical';\nimport {initializeUnitTest} from 'lexical/__tests__/utils';\n\nconst editorConfig = Object.freeze({\n  namespace: '',\n  theme: {\n    link: 'my-link-class',\n    text: {\n      bold: 'my-bold-class',\n      code: 'my-code-class',\n      hashtag: 'my-hashtag-class',\n      italic: 'my-italic-class',\n      strikethrough: 'my-strikethrough-class',\n      underline: 'my-underline-class',\n      underlineStrikethrough: 'my-underline-strikethrough-class',\n    },\n  },\n});\n\ndescribe('LexicalLinkNode tests', () => {\n  initializeUnitTest((testEnv) => {\n    test('LinkNode.constructor', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const linkNode = new LinkNode('/');\n\n        expect(linkNode.__type).toBe('link');\n        expect(linkNode.__url).toBe('/');\n      });\n\n      expect(() => new LinkNode('')).toThrow();\n    });\n\n    test('LineBreakNode.clone()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const linkNode = new LinkNode('/');\n\n        const linkNodeClone = LinkNode.clone(linkNode);\n\n        expect(linkNodeClone).not.toBe(linkNode);\n        expect(linkNodeClone).toStrictEqual(linkNode);\n      });\n    });\n\n    test('LinkNode.getURL()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const linkNode = new LinkNode('https://example.com/foo');\n\n        expect(linkNode.getURL()).toBe('https://example.com/foo');\n      });\n    });\n\n    test('LinkNode.setURL()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const linkNode = new LinkNode('https://example.com/foo');\n\n        expect(linkNode.getURL()).toBe('https://example.com/foo');\n\n        linkNode.setURL('https://example.com/bar');\n\n        expect(linkNode.getURL()).toBe('https://example.com/bar');\n      });\n    });\n\n    test('LinkNode.getTarget()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const linkNode = new LinkNode('https://example.com/foo', {\n          target: '_blank',\n        });\n\n        expect(linkNode.getTarget()).toBe('_blank');\n      });\n    });\n\n    test('LinkNode.setTarget()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const linkNode = new LinkNode('https://example.com/foo', {\n          target: '_blank',\n        });\n\n        expect(linkNode.getTarget()).toBe('_blank');\n\n        linkNode.setTarget('_self');\n\n        expect(linkNode.getTarget()).toBe('_self');\n      });\n    });\n\n    test('LinkNode.getRel()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const linkNode = new LinkNode('https://example.com/foo', {\n          rel: 'noopener noreferrer',\n          target: '_blank',\n        });\n\n        expect(linkNode.getRel()).toBe('noopener noreferrer');\n      });\n    });\n\n    test('LinkNode.setRel()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const linkNode = new LinkNode('https://example.com/foo', {\n          rel: 'noopener',\n          target: '_blank',\n        });\n\n        expect(linkNode.getRel()).toBe('noopener');\n\n        linkNode.setRel('noopener noreferrer');\n\n        expect(linkNode.getRel()).toBe('noopener noreferrer');\n      });\n    });\n\n    test('LinkNode.getTitle()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const linkNode = new LinkNode('https://example.com/foo', {\n          title: 'Hello world',\n        });\n\n        expect(linkNode.getTitle()).toBe('Hello world');\n      });\n    });\n\n    test('LinkNode.setTitle()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const linkNode = new LinkNode('https://example.com/foo', {\n          title: 'Hello world',\n        });\n\n        expect(linkNode.getTitle()).toBe('Hello world');\n\n        linkNode.setTitle('World hello');\n\n        expect(linkNode.getTitle()).toBe('World hello');\n      });\n    });\n\n    test('LinkNode.createDOM()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const linkNode = new LinkNode('https://example.com/foo');\n\n        expect(linkNode.createDOM(editorConfig).outerHTML).toBe(\n          '<a href=\"https://example.com/foo\" class=\"my-link-class\"></a>',\n        );\n        expect(\n          linkNode.createDOM({\n            namespace: '',\n            theme: {},\n          }).outerHTML,\n        ).toBe('<a href=\"https://example.com/foo\"></a>');\n      });\n    });\n\n    test('LinkNode.createDOM() with target, rel and title', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const linkNode = new LinkNode('https://example.com/foo', {\n          rel: 'noopener noreferrer',\n          target: '_blank',\n          title: 'Hello world',\n        });\n\n        expect(linkNode.createDOM(editorConfig).outerHTML).toBe(\n          '<a href=\"https://example.com/foo\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Hello world\" class=\"my-link-class\"></a>',\n        );\n        expect(\n          linkNode.createDOM({\n            namespace: '',\n            theme: {},\n          }).outerHTML,\n        ).toBe(\n          '<a href=\"https://example.com/foo\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Hello world\"></a>',\n        );\n      });\n    });\n\n    test('LinkNode.updateDOM()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const linkNode = new LinkNode('https://example.com/foo');\n\n        const domElement = linkNode.createDOM(editorConfig);\n\n        expect(linkNode.createDOM(editorConfig).outerHTML).toBe(\n          '<a href=\"https://example.com/foo\" class=\"my-link-class\"></a>',\n        );\n\n        const newLinkNode = new LinkNode('https://example.com/bar');\n        const result = newLinkNode.updateDOM(\n          linkNode,\n          domElement,\n          editorConfig,\n        );\n\n        expect(result).toBe(false);\n        expect(domElement.outerHTML).toBe(\n          '<a href=\"https://example.com/bar\" class=\"my-link-class\"></a>',\n        );\n      });\n    });\n\n    test('LinkNode.updateDOM() with target, rel and title', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const linkNode = new LinkNode('https://example.com/foo', {\n          rel: 'noopener noreferrer',\n          target: '_blank',\n          title: 'Hello world',\n        });\n\n        const domElement = linkNode.createDOM(editorConfig);\n\n        expect(linkNode.createDOM(editorConfig).outerHTML).toBe(\n          '<a href=\"https://example.com/foo\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Hello world\" class=\"my-link-class\"></a>',\n        );\n\n        const newLinkNode = new LinkNode('https://example.com/bar', {\n          rel: 'noopener',\n          target: '_self',\n          title: 'World hello',\n        });\n        const result = newLinkNode.updateDOM(\n          linkNode,\n          domElement,\n          editorConfig,\n        );\n\n        expect(result).toBe(false);\n        expect(domElement.outerHTML).toBe(\n          '<a href=\"https://example.com/bar\" target=\"_self\" rel=\"noopener\" title=\"World hello\" class=\"my-link-class\"></a>',\n        );\n      });\n    });\n\n    test('LinkNode.updateDOM() with undefined target, undefined rel and undefined title', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const linkNode = new LinkNode('https://example.com/foo', {\n          rel: 'noopener noreferrer',\n          target: '_blank',\n          title: 'Hello world',\n        });\n\n        const domElement = linkNode.createDOM(editorConfig);\n\n        expect(linkNode.createDOM(editorConfig).outerHTML).toBe(\n          '<a href=\"https://example.com/foo\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Hello world\" class=\"my-link-class\"></a>',\n        );\n\n        const newLinkNode = new LinkNode('https://example.com/bar');\n        const result = newLinkNode.updateDOM(\n          linkNode,\n          domElement,\n          editorConfig,\n        );\n\n        expect(result).toBe(false);\n        expect(domElement.outerHTML).toBe(\n          '<a href=\"https://example.com/bar\" class=\"my-link-class\"></a>',\n        );\n      });\n    });\n\n    test('LinkNode.canInsertTextBefore()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const linkNode = new LinkNode('https://example.com/foo');\n\n        expect(linkNode.canInsertTextBefore()).toBe(false);\n      });\n    });\n\n    test('LinkNode.canInsertTextAfter()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const linkNode = new LinkNode('https://example.com/foo');\n\n        expect(linkNode.canInsertTextAfter()).toBe(false);\n      });\n    });\n\n    test('$createLinkNode()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const linkNode = new LinkNode('https://example.com/foo');\n\n        const createdLinkNode = $createLinkNode('https://example.com/foo');\n\n        expect(linkNode.__type).toEqual(createdLinkNode.__type);\n        expect(linkNode.__parent).toEqual(createdLinkNode.__parent);\n        expect(linkNode.__url).toEqual(createdLinkNode.__url);\n        expect(linkNode.__key).not.toEqual(createdLinkNode.__key);\n      });\n    });\n\n    test('$createLinkNode() with target, rel and title', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const linkNode = new LinkNode('https://example.com/foo', {\n          rel: 'noopener noreferrer',\n          target: '_blank',\n          title: 'Hello world',\n        });\n\n        const createdLinkNode = $createLinkNode('https://example.com/foo', {\n          rel: 'noopener noreferrer',\n          target: '_blank',\n          title: 'Hello world',\n        });\n\n        expect(linkNode.__type).toEqual(createdLinkNode.__type);\n        expect(linkNode.__parent).toEqual(createdLinkNode.__parent);\n        expect(linkNode.__url).toEqual(createdLinkNode.__url);\n        expect(linkNode.__target).toEqual(createdLinkNode.__target);\n        expect(linkNode.__rel).toEqual(createdLinkNode.__rel);\n        expect(linkNode.__title).toEqual(createdLinkNode.__title);\n        expect(linkNode.__key).not.toEqual(createdLinkNode.__key);\n      });\n    });\n\n    test('$isLinkNode()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const linkNode = new LinkNode('');\n\n        expect($isLinkNode(linkNode)).toBe(true);\n      });\n    });\n\n    test('$toggleLink applies the title attribute when creating', async () => {\n      const {editor} = testEnv;\n      await editor.update(() => {\n        const p = new ParagraphNode();\n        p.append(new TextNode('Some text'));\n        $getRoot().append(p);\n      });\n\n      await editor.update(() => {\n        $selectAll();\n        $toggleLink('https://lexical.dev/', {title: 'Lexical Website'});\n      });\n\n      const paragraph = editor!.getEditorState().toJSON().root\n        .children[0] as SerializedParagraphNode;\n      const link = paragraph.children[0] as SerializedLinkNode;\n      expect(link.title).toBe('Lexical Website');\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/link/index.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {\n  BaseSelection,\n  DOMConversionMap,\n  DOMConversionOutput,\n  EditorConfig,\n  LexicalCommand,\n  LexicalNode,\n  NodeKey,\n  RangeSelection,\n  SerializedElementNode,\n} from 'lexical';\n\nimport {addClassNamesToElement, isHTMLAnchorElement} from '@lexical/utils';\nimport {\n  $applyNodeReplacement,\n  $getSelection,\n  $isElementNode,\n  $isRangeSelection,\n  createCommand,\n  ElementNode,\n  Spread,\n} from 'lexical';\n\nexport type LinkAttributes = {\n  rel?: null | string;\n  target?: null | string;\n  title?: null | string;\n};\n\nexport type AutoLinkAttributes = Partial<\n  Spread<LinkAttributes, {isUnlinked?: boolean}>\n>;\n\nexport type SerializedLinkNode = Spread<\n  {\n    url: string;\n  },\n  Spread<LinkAttributes, SerializedElementNode>\n>;\n\ntype LinkHTMLElementType = HTMLAnchorElement | HTMLSpanElement;\n\n/** @noInheritDoc */\nexport class LinkNode extends ElementNode {\n  /** @internal */\n  __url: string;\n  /** @internal */\n  __target: null | string;\n  /** @internal */\n  __rel: null | string;\n  /** @internal */\n  __title: null | string;\n\n  static getType(): string {\n    return 'link';\n  }\n\n  static clone(node: LinkNode): LinkNode {\n    return new LinkNode(\n      node.__url,\n      {rel: node.__rel, target: node.__target, title: node.__title},\n      node.__key,\n    );\n  }\n\n  constructor(url: string, attributes: LinkAttributes = {}, key?: NodeKey) {\n    super(key);\n    const {target = null, rel = null, title = null} = attributes;\n    this.__url = url;\n    this.__target = target;\n    this.__rel = rel;\n    this.__title = title;\n  }\n\n  createDOM(config: EditorConfig): LinkHTMLElementType {\n    const element = document.createElement('a');\n    element.href = this.__url;\n    if (this.__target !== null) {\n      element.target = this.__target;\n    }\n    if (this.__rel !== null) {\n      element.rel = this.__rel;\n    }\n    if (this.__title !== null) {\n      element.title = this.__title;\n    }\n    addClassNamesToElement(element, config.theme.link);\n    return element;\n  }\n\n  updateDOM(\n    prevNode: LinkNode,\n    anchor: LinkHTMLElementType,\n    config: EditorConfig,\n  ): boolean {\n    if (anchor instanceof HTMLAnchorElement) {\n      const url = this.__url;\n      const target = this.__target;\n      const rel = this.__rel;\n      const title = this.__title;\n      if (url !== prevNode.__url) {\n        anchor.href = url;\n      }\n\n      if (target !== prevNode.__target) {\n        if (target) {\n          anchor.target = target;\n        } else {\n          anchor.removeAttribute('target');\n        }\n      }\n\n      if (rel !== prevNode.__rel) {\n        if (rel) {\n          anchor.rel = rel;\n        } else {\n          anchor.removeAttribute('rel');\n        }\n      }\n\n      if (title !== prevNode.__title) {\n        if (title) {\n          anchor.title = title;\n        } else {\n          anchor.removeAttribute('title');\n        }\n      }\n    }\n    return false;\n  }\n\n  static importDOM(): DOMConversionMap | null {\n    return {\n      a: (node: Node) => ({\n        conversion: $convertAnchorElement,\n        priority: 1,\n      }),\n    };\n  }\n\n  static importJSON(\n    serializedNode: SerializedLinkNode | SerializedAutoLinkNode,\n  ): LinkNode {\n    const node = $createLinkNode(serializedNode.url, {\n      rel: serializedNode.rel,\n      target: serializedNode.target,\n      title: serializedNode.title,\n    });\n    node.setDirection(serializedNode.direction);\n    return node;\n  }\n\n  exportJSON(): SerializedLinkNode | SerializedAutoLinkNode {\n    return {\n      ...super.exportJSON(),\n      rel: this.getRel(),\n      target: this.getTarget(),\n      title: this.getTitle(),\n      type: 'link',\n      url: this.getURL(),\n      version: 1,\n    };\n  }\n\n  getURL(): string {\n    return this.getLatest().__url;\n  }\n\n  setURL(url: string): void {\n    const writable = this.getWritable();\n    writable.__url = url;\n  }\n\n  getTarget(): null | string {\n    return this.getLatest().__target;\n  }\n\n  setTarget(target: null | string): void {\n    const writable = this.getWritable();\n    writable.__target = target;\n  }\n\n  getRel(): null | string {\n    return this.getLatest().__rel;\n  }\n\n  setRel(rel: null | string): void {\n    const writable = this.getWritable();\n    writable.__rel = rel;\n  }\n\n  getTitle(): null | string {\n    return this.getLatest().__title;\n  }\n\n  setTitle(title: null | string): void {\n    const writable = this.getWritable();\n    writable.__title = title;\n  }\n\n  insertNewAfter(\n    _: RangeSelection,\n    restoreSelection = true,\n  ): null | ElementNode {\n    const linkNode = $createLinkNode(this.__url, {\n      rel: this.__rel,\n      target: this.__target,\n      title: this.__title,\n    });\n    this.insertAfter(linkNode, restoreSelection);\n    return linkNode;\n  }\n\n  canInsertTextBefore(): false {\n    return false;\n  }\n\n  canInsertTextAfter(): false {\n    return false;\n  }\n\n  canBeEmpty(): false {\n    return false;\n  }\n\n  isInline(): true {\n    return true;\n  }\n\n  extractWithChild(\n    child: LexicalNode,\n    selection: BaseSelection,\n    destination: 'clone' | 'html',\n  ): boolean {\n    if (!$isRangeSelection(selection)) {\n      return false;\n    }\n\n    const anchorNode = selection.anchor.getNode();\n    const focusNode = selection.focus.getNode();\n\n    return (\n      this.isParentOf(anchorNode) &&\n      this.isParentOf(focusNode) &&\n      selection.getTextContent().length > 0\n    );\n  }\n\n  isEmailURI(): boolean {\n    return this.__url.startsWith('mailto:');\n  }\n\n  isWebSiteURI(): boolean {\n    return (\n      this.__url.startsWith('https://') || this.__url.startsWith('http://')\n    );\n  }\n}\n\nfunction $convertAnchorElement(domNode: Node): DOMConversionOutput {\n  let node = null;\n  if (isHTMLAnchorElement(domNode)) {\n    const content = domNode.textContent;\n    if ((content !== null && content !== '') || domNode.children.length > 0) {\n      node = $createLinkNode(domNode.getAttribute('href') || '', {\n        rel: domNode.getAttribute('rel'),\n        target: domNode.getAttribute('target'),\n        title: domNode.getAttribute('title'),\n      });\n    }\n  }\n  return {node};\n}\n\n/**\n * Takes a URL and creates a LinkNode.\n * @param url - The URL the LinkNode should direct to.\n * @param attributes - Optional HTML a tag attributes \\\\{ target, rel, title \\\\}\n * @returns The LinkNode.\n */\nexport function $createLinkNode(\n  url: string,\n  attributes?: LinkAttributes,\n): LinkNode {\n  return $applyNodeReplacement(new LinkNode(url, attributes));\n}\n\n/**\n * Determines if node is a LinkNode.\n * @param node - The node to be checked.\n * @returns true if node is a LinkNode, false otherwise.\n */\nexport function $isLinkNode(\n  node: LexicalNode | null | undefined,\n): node is LinkNode {\n  return node instanceof LinkNode;\n}\n\nexport type SerializedAutoLinkNode = Spread<\n  {\n    isUnlinked: boolean;\n  },\n  SerializedLinkNode\n>;\n\n// Custom node type to override `canInsertTextAfter` that will\n// allow typing within the link\nexport class AutoLinkNode extends LinkNode {\n  /** @internal */\n  /** Indicates whether the autolink was ever unlinked. **/\n  __isUnlinked: boolean;\n\n  constructor(url: string, attributes: AutoLinkAttributes = {}, key?: NodeKey) {\n    super(url, attributes, key);\n    this.__isUnlinked =\n      attributes.isUnlinked !== undefined && attributes.isUnlinked !== null\n        ? attributes.isUnlinked\n        : false;\n  }\n\n  static getType(): string {\n    return 'autolink';\n  }\n\n  static clone(node: AutoLinkNode): AutoLinkNode {\n    return new AutoLinkNode(\n      node.__url,\n      {\n        isUnlinked: node.__isUnlinked,\n        rel: node.__rel,\n        target: node.__target,\n        title: node.__title,\n      },\n      node.__key,\n    );\n  }\n\n  getIsUnlinked(): boolean {\n    return this.__isUnlinked;\n  }\n\n  setIsUnlinked(value: boolean) {\n    const self = this.getWritable();\n    self.__isUnlinked = value;\n    return self;\n  }\n\n  createDOM(config: EditorConfig): LinkHTMLElementType {\n    if (this.__isUnlinked) {\n      return document.createElement('span');\n    } else {\n      return super.createDOM(config);\n    }\n  }\n\n  updateDOM(\n    prevNode: AutoLinkNode,\n    anchor: LinkHTMLElementType,\n    config: EditorConfig,\n  ): boolean {\n    return (\n      super.updateDOM(prevNode, anchor, config) ||\n      prevNode.__isUnlinked !== this.__isUnlinked\n    );\n  }\n\n  static importJSON(serializedNode: SerializedAutoLinkNode): AutoLinkNode {\n    const node = $createAutoLinkNode(serializedNode.url, {\n      isUnlinked: serializedNode.isUnlinked,\n      rel: serializedNode.rel,\n      target: serializedNode.target,\n      title: serializedNode.title,\n    });\n    node.setDirection(serializedNode.direction);\n    return node;\n  }\n\n  static importDOM(): null {\n    // TODO: Should link node should handle the import over autolink?\n    return null;\n  }\n\n  exportJSON(): SerializedAutoLinkNode {\n    return {\n      ...super.exportJSON(),\n      isUnlinked: this.__isUnlinked,\n      type: 'autolink',\n      version: 1,\n    };\n  }\n\n  insertNewAfter(\n    selection: RangeSelection,\n    restoreSelection = true,\n  ): null | ElementNode {\n    const element = this.getParentOrThrow().insertNewAfter(\n      selection,\n      restoreSelection,\n    );\n    if ($isElementNode(element)) {\n      const linkNode = $createAutoLinkNode(this.__url, {\n        isUnlinked: this.__isUnlinked,\n        rel: this.__rel,\n        target: this.__target,\n        title: this.__title,\n      });\n      element.append(linkNode);\n      return linkNode;\n    }\n    return null;\n  }\n}\n\n/**\n * Takes a URL and creates an AutoLinkNode. AutoLinkNodes are generally automatically generated\n * during typing, which is especially useful when a button to generate a LinkNode is not practical.\n * @param url - The URL the LinkNode should direct to.\n * @param attributes - Optional HTML a tag attributes. \\\\{ target, rel, title \\\\}\n * @returns The LinkNode.\n */\nexport function $createAutoLinkNode(\n  url: string,\n  attributes?: AutoLinkAttributes,\n): AutoLinkNode {\n  return $applyNodeReplacement(new AutoLinkNode(url, attributes));\n}\n\n/**\n * Determines if node is an AutoLinkNode.\n * @param node - The node to be checked.\n * @returns true if node is an AutoLinkNode, false otherwise.\n */\nexport function $isAutoLinkNode(\n  node: LexicalNode | null | undefined,\n): node is AutoLinkNode {\n  return node instanceof AutoLinkNode;\n}\n\nexport const TOGGLE_LINK_COMMAND: LexicalCommand<\n  string | ({url: string} & LinkAttributes) | null\n> = createCommand('TOGGLE_LINK_COMMAND');\n\n/**\n * Generates or updates a LinkNode. It can also delete a LinkNode if the URL is null,\n * but saves any children and brings them up to the parent node.\n * @param url - The URL the link directs to.\n * @param attributes - Optional HTML a tag attributes. \\\\{ target, rel, title \\\\}\n */\nexport function $toggleLink(\n  url: null | string,\n  attributes: LinkAttributes = {},\n): void {\n  const {target, title} = attributes;\n  const rel = attributes.rel === undefined ? 'noreferrer' : attributes.rel;\n  const selection = $getSelection();\n\n  if (!$isRangeSelection(selection)) {\n    return;\n  }\n  const nodes = selection.extract();\n\n  if (url === null) {\n    // Remove LinkNodes\n    nodes.forEach((node) => {\n      const parent = node.getParent();\n\n      if (!$isAutoLinkNode(parent) && $isLinkNode(parent)) {\n        const children = parent.getChildren();\n\n        for (let i = 0; i < children.length; i++) {\n          parent.insertBefore(children[i]);\n        }\n\n        parent.remove();\n      }\n    });\n  } else {\n    // Add or merge LinkNodes\n    if (nodes.length === 1) {\n      const firstNode = nodes[0];\n      // if the first node is a LinkNode or if its\n      // parent is a LinkNode, we update the URL, target and rel.\n      const linkNode = $getAncestor(firstNode, $isLinkNode);\n      if (linkNode !== null) {\n        linkNode.setURL(url);\n        if (target !== undefined) {\n          linkNode.setTarget(target);\n        }\n        if (rel !== null) {\n          linkNode.setRel(rel);\n        }\n        if (title !== undefined) {\n          linkNode.setTitle(title);\n        }\n        return;\n      }\n    }\n\n    let prevParent: ElementNode | LinkNode | null = null;\n    let linkNode: LinkNode | null = null;\n\n    nodes.forEach((node) => {\n      const parent = node.getParent();\n\n      if (\n        parent === linkNode ||\n        parent === null ||\n        ($isElementNode(node) && !node.isInline())\n      ) {\n        return;\n      }\n\n      if ($isLinkNode(parent)) {\n        linkNode = parent;\n        parent.setURL(url);\n        if (target !== undefined) {\n          parent.setTarget(target);\n        }\n        if (rel !== null) {\n          linkNode.setRel(rel);\n        }\n        if (title !== undefined) {\n          linkNode.setTitle(title);\n        }\n        return;\n      }\n\n      if (!parent.is(prevParent)) {\n        prevParent = parent;\n        linkNode = $createLinkNode(url, {rel, target, title});\n\n        if ($isLinkNode(parent)) {\n          if (node.getPreviousSibling() === null) {\n            parent.insertBefore(linkNode);\n          } else {\n            parent.insertAfter(linkNode);\n          }\n        } else {\n          node.insertBefore(linkNode);\n        }\n      }\n\n      if ($isLinkNode(node)) {\n        if (node.is(linkNode)) {\n          return;\n        }\n        if (linkNode !== null) {\n          const children = node.getChildren();\n\n          for (let i = 0; i < children.length; i++) {\n            linkNode.append(children[i]);\n          }\n        }\n\n        node.remove();\n        return;\n      }\n\n      if (linkNode !== null) {\n        linkNode.append(node);\n      }\n    });\n  }\n}\n/** @deprecated renamed to {@link $toggleLink} by @lexical/eslint-plugin rules-of-lexical */\nexport const toggleLink = $toggleLink;\n\nfunction $getAncestor<NodeType extends LexicalNode = LexicalNode>(\n  node: LexicalNode,\n  predicate: (ancestor: LexicalNode) => ancestor is NodeType,\n) {\n  let parent = node;\n  while (parent !== null && parent.getParent() !== null && !predicate(parent)) {\n    parent = parent.getParentOrThrow();\n  }\n  return predicate(parent) ? parent : null;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/list/LexicalListItemNode.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {ListNode, ListType} from './';\nimport type {\n  BaseSelection,\n  DOMConversionMap,\n  DOMConversionOutput,\n  DOMExportOutput,\n  EditorConfig,\n  LexicalNode,\n  NodeKey,\n  ParagraphNode,\n  RangeSelection,\n  SerializedElementNode,\n  Spread,\n} from 'lexical';\n\nimport {\n  $applyNodeReplacement,\n  $createParagraphNode,\n  $isElementNode,\n  $isParagraphNode,\n  $isRangeSelection,\n  ElementNode,\n  LexicalEditor,\n} from 'lexical';\nimport invariant from 'lexical/shared/invariant';\n\nimport {$createListNode, $isListNode} from './';\nimport {mergeLists} from './formatList';\nimport {isNestedListNode} from './utils';\nimport {el} from \"../../utils/dom\";\n\nexport type SerializedListItemNode = Spread<\n  {\n    checked: boolean | undefined;\n    value: number;\n  },\n  SerializedElementNode\n>;\n\n/** @noInheritDoc */\nexport class ListItemNode extends ElementNode {\n  /** @internal */\n  __value: number;\n  /** @internal */\n  __checked?: boolean;\n\n  static getType(): string {\n    return 'listitem';\n  }\n\n  static clone(node: ListItemNode): ListItemNode {\n    return new ListItemNode(node.__value, node.__checked, node.__key);\n  }\n\n  constructor(value?: number, checked?: boolean, key?: NodeKey) {\n    super(key);\n    this.__value = value === undefined ? 1 : value;\n    this.__checked = checked;\n  }\n\n  createDOM(config: EditorConfig): HTMLElement {\n    const element = document.createElement('li');\n    const parent = this.getParent();\n\n    if ($isListNode(parent) && parent.getListType() === 'check') {\n      updateListItemChecked(element, this);\n    }\n\n    element.value = this.__value;\n\n    if ($hasNestedListWithoutLabel(this)) {\n      element.style.listStyle = 'none';\n    }\n\n    return element;\n  }\n\n  updateDOM(\n    prevNode: ListItemNode,\n    dom: HTMLElement,\n    config: EditorConfig,\n  ): boolean {\n    const parent = this.getParent();\n    if ($isListNode(parent) && parent.getListType() === 'check') {\n      updateListItemChecked(dom, this);\n    }\n\n    dom.style.listStyle = $hasNestedListWithoutLabel(this) ? 'none' : '';\n    // @ts-expect-error - this is always HTMLListItemElement\n    dom.value = this.__value;\n\n    return false;\n  }\n\n  static transform(): (node: LexicalNode) => void {\n    return (node: LexicalNode) => {\n      invariant($isListItemNode(node), 'node is not a ListItemNode');\n      if (node.__checked == null) {\n        return;\n      }\n      const parent = node.getParent();\n      if ($isListNode(parent)) {\n        if (parent.getListType() !== 'check' && node.getChecked() != null) {\n          node.setChecked(undefined);\n        }\n      }\n    };\n  }\n\n  static importDOM(): DOMConversionMap | null {\n    return {\n      li: () => ({\n        conversion: $convertListItemElement,\n        priority: 0,\n      }),\n    };\n  }\n\n  static importJSON(serializedNode: SerializedListItemNode): ListItemNode {\n    const node = $createListItemNode();\n    node.setChecked(serializedNode.checked);\n    node.setValue(serializedNode.value);\n    node.setDirection(serializedNode.direction);\n    return node;\n  }\n\n  exportDOM(editor: LexicalEditor): DOMExportOutput {\n    const element = this.createDOM(editor._config);\n\n    if (element.classList.contains('task-list-item')) {\n      const input = el('input', {\n        type: 'checkbox',\n        disabled: 'disabled',\n      });\n      if (element.hasAttribute('checked')) {\n        input.setAttribute('checked', 'checked');\n        element.removeAttribute('checked');\n      }\n\n      element.prepend(input);\n    }\n\n    return {\n      element,\n    };\n  }\n\n  exportJSON(): SerializedListItemNode {\n    return {\n      ...super.exportJSON(),\n      checked: this.getChecked(),\n      type: 'listitem',\n      value: this.getValue(),\n      version: 1,\n    };\n  }\n\n  append(...nodes: LexicalNode[]): this {\n    for (let i = 0; i < nodes.length; i++) {\n      const node = nodes[i];\n\n      if ($isElementNode(node) && this.canMergeWith(node)) {\n        const children = node.getChildren();\n        this.append(...children);\n        node.remove();\n      } else {\n        super.append(node);\n      }\n    }\n\n    return this;\n  }\n\n  replace<N extends LexicalNode>(\n    replaceWithNode: N,\n    includeChildren?: boolean,\n  ): N {\n    if ($isListItemNode(replaceWithNode)) {\n      return super.replace(replaceWithNode);\n    }\n    const list = this.getParentOrThrow();\n    if (!$isListNode(list)) {\n      return replaceWithNode;\n    }\n    if (list.__first === this.getKey()) {\n      list.insertBefore(replaceWithNode);\n    } else if (list.__last === this.getKey()) {\n      list.insertAfter(replaceWithNode);\n    } else {\n      // Split the list\n      const newList = $createListNode(list.getListType());\n      let nextSibling = this.getNextSibling();\n      while (nextSibling) {\n        const nodeToAppend = nextSibling;\n        nextSibling = nextSibling.getNextSibling();\n        newList.append(nodeToAppend);\n      }\n      list.insertAfter(replaceWithNode);\n      replaceWithNode.insertAfter(newList);\n    }\n    if (includeChildren) {\n      invariant(\n        $isElementNode(replaceWithNode),\n        'includeChildren should only be true for ElementNodes',\n      );\n      this.getChildren().forEach((child: LexicalNode) => {\n        replaceWithNode.append(child);\n      });\n    }\n    this.remove();\n    if (list.getChildrenSize() === 0) {\n      list.remove();\n    }\n    return replaceWithNode;\n  }\n\n  insertAfter(node: LexicalNode, restoreSelection = true): LexicalNode {\n    const listNode = this.getParentOrThrow();\n\n    if (!$isListNode(listNode)) {\n      invariant(\n        false,\n        'insertAfter: list node is not parent of list item node',\n      );\n    }\n\n    if ($isListItemNode(node)) {\n      return super.insertAfter(node, restoreSelection);\n    }\n\n    const siblings = this.getNextSiblings();\n\n    // Split the lists and insert the node in between them\n    listNode.insertAfter(node, restoreSelection);\n\n    if (siblings.length !== 0) {\n      const newListNode = $createListNode(listNode.getListType());\n\n      siblings.forEach((sibling) => newListNode.append(sibling));\n\n      node.insertAfter(newListNode, restoreSelection);\n    }\n\n    return node;\n  }\n\n  remove(preserveEmptyParent?: boolean): void {\n    const prevSibling = this.getPreviousSibling();\n    const nextSibling = this.getNextSibling();\n    super.remove(preserveEmptyParent);\n\n    if (\n      prevSibling &&\n      nextSibling &&\n      isNestedListNode(prevSibling) &&\n      isNestedListNode(nextSibling)\n    ) {\n      mergeLists(prevSibling.getFirstChild(), nextSibling.getFirstChild());\n      nextSibling.remove();\n    }\n  }\n\n  insertNewAfter(\n    _: RangeSelection,\n    restoreSelection = true,\n  ): ListItemNode | ParagraphNode | null {\n\n    if (this.getTextContent().trim() === '' && this.isLastChild()) {\n      const list = this.getParentOrThrow<ListNode>();\n      const parentListItem = list.getParent();\n      if ($isListItemNode(parentListItem)) {\n        // Un-nest list item if empty nested item\n        parentListItem.insertAfter(this);\n        this.selectStart();\n        return null;\n      } else {\n        // Insert empty paragraph after list if adding after last empty child\n        const paragraph = $createParagraphNode();\n        list.insertAfter(paragraph, restoreSelection);\n        this.remove();\n        return paragraph;\n      }\n    }\n\n    const newElement = $createListItemNode(\n      this.__checked == null ? undefined : false,\n    );\n\n    this.insertAfter(newElement, restoreSelection);\n\n    return newElement;\n  }\n\n  collapseAtStart(selection: RangeSelection): true {\n    const paragraph = $createParagraphNode();\n    const children = this.getChildren();\n    children.forEach((child) => paragraph.append(child));\n    const listNode = this.getParentOrThrow();\n    const listNodeParent = listNode.getParentOrThrow();\n    const isIndented = $isListItemNode(listNodeParent);\n\n    if (listNode.getChildrenSize() === 1) {\n      if (isIndented) {\n        // if the list node is nested, we just want to remove it,\n        // effectively unindenting it.\n        listNode.remove();\n        listNodeParent.select();\n      } else {\n        listNode.insertBefore(paragraph);\n        listNode.remove();\n        // If we have selection on the list item, we'll need to move it\n        // to the paragraph\n        const anchor = selection.anchor;\n        const focus = selection.focus;\n        const key = paragraph.getKey();\n\n        if (anchor.type === 'element' && anchor.getNode().is(this)) {\n          anchor.set(key, anchor.offset, 'element');\n        }\n\n        if (focus.type === 'element' && focus.getNode().is(this)) {\n          focus.set(key, focus.offset, 'element');\n        }\n      }\n    } else {\n      listNode.insertBefore(paragraph);\n      this.remove();\n    }\n\n    return true;\n  }\n\n  getValue(): number {\n    const self = this.getLatest();\n\n    return self.__value;\n  }\n\n  setValue(value: number): void {\n    const self = this.getWritable();\n    self.__value = value;\n  }\n\n  getChecked(): boolean | undefined {\n    const self = this.getLatest();\n\n    let listType: ListType | undefined;\n\n    const parent = this.getParent();\n    if ($isListNode(parent)) {\n      listType = parent.getListType();\n    }\n\n    return listType === 'check' ? Boolean(self.__checked) : undefined;\n  }\n\n  setChecked(checked?: boolean): void {\n    const self = this.getWritable();\n    self.__checked = checked;\n  }\n\n  toggleChecked(): void {\n    this.setChecked(!this.__checked);\n  }\n\n  /** @deprecated @internal */\n  canInsertAfter(node: LexicalNode): boolean {\n    return $isListItemNode(node);\n  }\n\n  /** @deprecated @internal */\n  canReplaceWith(replacement: LexicalNode): boolean {\n    return $isListItemNode(replacement);\n  }\n\n  canMergeWith(node: LexicalNode): boolean {\n    return $isParagraphNode(node) || $isListItemNode(node);\n  }\n\n  extractWithChild(child: LexicalNode, selection: BaseSelection): boolean {\n    if (!$isRangeSelection(selection)) {\n      return false;\n    }\n\n    const anchorNode = selection.anchor.getNode();\n    const focusNode = selection.focus.getNode();\n\n    return (\n      this.isParentOf(anchorNode) &&\n      this.isParentOf(focusNode) &&\n      this.getTextContent().length === selection.getTextContent().length\n    );\n  }\n\n  isParentRequired(): true {\n    return true;\n  }\n\n  createParentElementNode(): ElementNode {\n    return $createListNode('bullet');\n  }\n\n  canMergeWhenEmpty(): true {\n    return true;\n  }\n}\n\nfunction $hasNestedListWithoutLabel(node: ListItemNode): boolean {\n  const children = node.getChildren();\n  let hasLabel = false;\n  let hasNestedList = false;\n\n  for (const child of children) {\n    if ($isListNode(child)) {\n      hasNestedList = true;\n    } else if (child.getTextContent().trim().length > 0) {\n      hasLabel = true;\n    }\n  }\n\n  return hasNestedList && !hasLabel;\n}\n\nfunction updateListItemChecked(\n  dom: HTMLElement,\n  listItemNode: ListItemNode,\n): void {\n  // Only set task list attrs for leaf list items\n  const shouldBeTaskItem = !$isListNode(listItemNode.getFirstChild());\n  dom.classList.toggle('task-list-item', shouldBeTaskItem);\n  if (listItemNode.__checked) {\n    dom.setAttribute('checked', 'checked');\n  } else {\n    dom.removeAttribute('checked');\n  }\n}\n\nfunction $convertListItemElement(domNode: HTMLElement): DOMConversionOutput {\n  const isGitHubCheckList = domNode.classList.contains('task-list-item');\n  if (isGitHubCheckList) {\n    for (const child of domNode.children) {\n      if (child.tagName === 'INPUT') {\n        return $convertCheckboxInput(child);\n      }\n    }\n  }\n\n  const ariaCheckedAttr = domNode.getAttribute('aria-checked');\n  const checked =\n    ariaCheckedAttr === 'true'\n      ? true\n      : ariaCheckedAttr === 'false'\n      ? false\n      : undefined;\n  return {node: $createListItemNode(checked)};\n}\n\nfunction $convertCheckboxInput(domNode: Element): DOMConversionOutput {\n  const isCheckboxInput = domNode.getAttribute('type') === 'checkbox';\n  if (!isCheckboxInput) {\n    return {node: null};\n  }\n  const checked = domNode.hasAttribute('checked');\n  return {node: $createListItemNode(checked)};\n}\n\n/**\n * Creates a new List Item node, passing true/false will convert it to a checkbox input.\n * @param checked - Is the List Item a checkbox and, if so, is it checked? undefined/null: not a checkbox, true/false is a checkbox and checked/unchecked, respectively.\n * @returns The new List Item.\n */\nexport function $createListItemNode(checked?: boolean): ListItemNode {\n  return $applyNodeReplacement(new ListItemNode(undefined, checked));\n}\n\n/**\n * Checks to see if the node is a ListItemNode.\n * @param node - The node to be checked.\n * @returns true if the node is a ListItemNode, false otherwise.\n */\nexport function $isListItemNode(\n  node: LexicalNode | null | undefined,\n): node is ListItemNode {\n  return node instanceof ListItemNode;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/list/LexicalListNode.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {\n  addClassNamesToElement,\n  isHTMLElement,\n  removeClassNamesFromElement,\n} from '@lexical/utils';\nimport {\n  $applyNodeReplacement,\n  $createTextNode,\n  $isElementNode,\n  DOMConversionMap,\n  DOMConversionOutput,\n  DOMExportOutput,\n  EditorConfig,\n  EditorThemeClasses,\n  ElementNode,\n  LexicalEditor,\n  LexicalNode,\n  NodeKey,\n  SerializedElementNode,\n  Spread,\n} from 'lexical';\nimport invariant from 'lexical/shared/invariant';\nimport normalizeClassNames from 'lexical/shared/normalizeClassNames';\n\nimport {$createListItemNode, $isListItemNode, ListItemNode} from '.';\nimport {\n  mergeNextSiblingListIfSameType,\n  updateChildrenListItemValue,\n} from './formatList';\nimport {$getListDepth, $wrapInListItem} from './utils';\nimport {extractDirectionFromElement} from \"lexical/nodes/common\";\n\nexport type SerializedListNode = Spread<\n  {\n    id: string;\n    listType: ListType;\n    start: number;\n    tag: ListNodeTagType;\n  },\n  SerializedElementNode\n>;\n\nexport type ListType = 'number' | 'bullet' | 'check';\n\nexport type ListNodeTagType = 'ul' | 'ol';\n\n/** @noInheritDoc */\nexport class ListNode extends ElementNode {\n  /** @internal */\n  __tag: ListNodeTagType;\n  /** @internal */\n  __start: number;\n  /** @internal */\n  __listType: ListType;\n  /** @internal */\n  __id: string = '';\n\n  static getType(): string {\n    return 'list';\n  }\n\n  static clone(node: ListNode): ListNode {\n    const newNode = new ListNode(node.__listType, node.__start, node.__key);\n    newNode.__id = node.__id;\n    newNode.__dir = node.__dir;\n    return newNode;\n  }\n\n  constructor(listType: ListType, start: number, key?: NodeKey) {\n    super(key);\n    const _listType = TAG_TO_LIST_TYPE[listType] || listType;\n    this.__listType = _listType;\n    this.__tag = _listType === 'number' ? 'ol' : 'ul';\n    this.__start = start;\n  }\n\n  getTag(): ListNodeTagType {\n    return this.__tag;\n  }\n\n  setId(id: string) {\n    const self = this.getWritable();\n    self.__id = id;\n  }\n\n  getId(): string {\n    const self = this.getLatest();\n    return self.__id;\n  }\n\n  setListType(type: ListType): void {\n    const writable = this.getWritable();\n    writable.__listType = type;\n    writable.__tag = type === 'number' ? 'ol' : 'ul';\n  }\n\n  getListType(): ListType {\n    return this.__listType;\n  }\n\n  getStart(): number {\n    return this.__start;\n  }\n\n  // View\n\n  createDOM(config: EditorConfig, _editor?: LexicalEditor): HTMLElement {\n    const tag = this.__tag;\n    const dom = document.createElement(tag);\n\n    if (this.__start !== 1) {\n      dom.setAttribute('start', String(this.__start));\n    }\n    // @ts-expect-error Internal field.\n    dom.__lexicalListType = this.__listType;\n    $setListThemeClassNames(dom, config.theme, this);\n\n    if (this.__id) {\n      dom.setAttribute('id', this.__id);\n    }\n\n    if (this.__dir) {\n      dom.setAttribute('dir', this.__dir);\n    }\n\n    return dom;\n  }\n\n  updateDOM(\n    prevNode: ListNode,\n    dom: HTMLElement,\n    config: EditorConfig,\n  ): boolean {\n    if (\n        prevNode.__tag !== this.__tag\n        || prevNode.__dir !== this.__dir\n        || prevNode.__id !== this.__id\n    ) {\n      return true;\n    }\n\n    $setListThemeClassNames(dom, config.theme, this);\n\n    return false;\n  }\n\n  static transform(): (node: LexicalNode) => void {\n    return (node: LexicalNode) => {\n      invariant($isListNode(node), 'node is not a ListNode');\n      mergeNextSiblingListIfSameType(node);\n      updateChildrenListItemValue(node);\n    };\n  }\n\n  static importDOM(): DOMConversionMap | null {\n    return {\n      ol: () => ({\n        conversion: $convertListNode,\n        priority: 0,\n      }),\n      ul: () => ({\n        conversion: $convertListNode,\n        priority: 0,\n      }),\n    };\n  }\n\n  static importJSON(serializedNode: SerializedListNode): ListNode {\n    const node = $createListNode(serializedNode.listType, serializedNode.start);\n    node.setId(serializedNode.id);\n    node.setDirection(serializedNode.direction);\n    return node;\n  }\n\n  exportDOM(editor: LexicalEditor): DOMExportOutput {\n    const {element} = super.exportDOM(editor);\n    if (element && isHTMLElement(element)) {\n      if (this.__start !== 1) {\n        element.setAttribute('start', String(this.__start));\n      }\n      if (this.__listType === 'check') {\n        element.setAttribute('__lexicalListType', 'check');\n      }\n    }\n    return {\n      element,\n    };\n  }\n\n  exportJSON(): SerializedListNode {\n    return {\n      ...super.exportJSON(),\n      listType: this.getListType(),\n      start: this.getStart(),\n      tag: this.getTag(),\n      type: 'list',\n      version: 1,\n      id: this.__id,\n    };\n  }\n\n  canBeEmpty(): false {\n    return false;\n  }\n\n  canIndent(): false {\n    return false;\n  }\n\n  append(...nodesToAppend: LexicalNode[]): this {\n    for (let i = 0; i < nodesToAppend.length; i++) {\n      const currentNode = nodesToAppend[i];\n\n      if ($isListItemNode(currentNode)) {\n        super.append(currentNode);\n      } else {\n        const listItemNode = $createListItemNode();\n\n        if ($isListNode(currentNode)) {\n          listItemNode.append(currentNode);\n        } else if ($isElementNode(currentNode)) {\n          const textNode = $createTextNode(currentNode.getTextContent());\n          listItemNode.append(textNode);\n        } else {\n          listItemNode.append(currentNode);\n        }\n        super.append(listItemNode);\n      }\n    }\n    return this;\n  }\n\n  extractWithChild(child: LexicalNode): boolean {\n    return $isListItemNode(child);\n  }\n}\n\nfunction $setListThemeClassNames(\n  dom: HTMLElement,\n  editorThemeClasses: EditorThemeClasses,\n  node: ListNode,\n): void {\n  const classesToAdd = [];\n  const classesToRemove = [];\n  const listTheme = editorThemeClasses.list;\n\n  if (listTheme !== undefined) {\n    const listLevelsClassNames = listTheme[`${node.__tag}Depth`] || [];\n    const listDepth = $getListDepth(node) - 1;\n    const normalizedListDepth = listDepth % listLevelsClassNames.length;\n    const listLevelClassName = listLevelsClassNames[normalizedListDepth];\n    const listClassName = listTheme[node.__tag];\n    let nestedListClassName;\n    const nestedListTheme = listTheme.nested;\n    const checklistClassName = listTheme.checklist;\n\n    if (nestedListTheme !== undefined && nestedListTheme.list) {\n      nestedListClassName = nestedListTheme.list;\n    }\n\n    if (listClassName !== undefined) {\n      classesToAdd.push(listClassName);\n    }\n\n    if (checklistClassName !== undefined && node.__listType === 'check') {\n      classesToAdd.push(checklistClassName);\n    }\n\n    if (listLevelClassName !== undefined) {\n      classesToAdd.push(...normalizeClassNames(listLevelClassName));\n      for (let i = 0; i < listLevelsClassNames.length; i++) {\n        if (i !== normalizedListDepth) {\n          classesToRemove.push(node.__tag + i);\n        }\n      }\n    }\n\n    if (nestedListClassName !== undefined) {\n      const nestedListItemClasses = normalizeClassNames(nestedListClassName);\n\n      if (listDepth > 1) {\n        classesToAdd.push(...nestedListItemClasses);\n      } else {\n        classesToRemove.push(...nestedListItemClasses);\n      }\n    }\n  }\n\n  if (classesToRemove.length > 0) {\n    removeClassNamesFromElement(dom, ...classesToRemove);\n  }\n\n  if (classesToAdd.length > 0) {\n    addClassNamesToElement(dom, ...classesToAdd);\n  }\n}\n\n/*\n * This function is a custom normalization function to allow nested lists within list item elements.\n * Original taken from https://github.com/facebook/lexical/blob/6e10210fd1e113ccfafdc999b1d896733c5c5bea/packages/lexical-list/src/LexicalListNode.ts#L284-L303\n * With modifications made.\n */\nfunction $normalizeChildren(nodes: Array<LexicalNode>): Array<ListItemNode> {\n  const normalizedListItems: Array<ListItemNode> = [];\n\n  for (const node of nodes) {\n    if ($isListItemNode(node)) {\n      normalizedListItems.push(node);\n    } else {\n      normalizedListItems.push($wrapInListItem(node));\n    }\n  }\n\n  return normalizedListItems;\n}\n\nfunction isDomChecklist(domNode: HTMLElement) {\n  if (\n    domNode.getAttribute('__lexicallisttype') === 'check' ||\n    // is github checklist\n    domNode.classList.contains('contains-task-list')\n  ) {\n    return true;\n  }\n  // if children are checklist items, the node is a checklist ul. Applicable for googledoc checklist pasting.\n  for (const child of domNode.childNodes) {\n    if (!isHTMLElement(child)) {\n      continue;\n    }\n\n    if (child.hasAttribute('aria-checked')) {\n      return true;\n    }\n\n    if (child.classList.contains('task-list-item')) {\n      return true;\n    }\n\n    if (child.firstElementChild && child.firstElementChild.matches('input[type=\"checkbox\"]')) {\n      return true;\n    }\n  }\n  return false;\n}\n\nfunction $convertListNode(domNode: HTMLElement): DOMConversionOutput {\n  const nodeName = domNode.nodeName.toLowerCase();\n  let node = null;\n  if (nodeName === 'ol') {\n    // @ts-ignore\n    const start = domNode.start;\n    node = $createListNode('number', start);\n  } else if (nodeName === 'ul') {\n    if (isDomChecklist(domNode)) {\n      node = $createListNode('check');\n    } else {\n      node = $createListNode('bullet');\n    }\n  }\n\n  if (domNode.id && node) {\n    node.setId(domNode.id);\n  }\n\n  if (domNode.dir && node) {\n    node.setDirection(extractDirectionFromElement(domNode));\n  }\n\n  return {\n    after: $normalizeChildren,\n    node,\n  };\n}\n\nconst TAG_TO_LIST_TYPE: Record<string, ListType> = {\n  ol: 'number',\n  ul: 'bullet',\n};\n\n/**\n * Creates a ListNode of listType.\n * @param listType - The type of list to be created. Can be 'number', 'bullet', or 'check'.\n * @param start - Where an ordered list starts its count, start = 1 if left undefined.\n * @returns The new ListNode\n */\nexport function $createListNode(listType: ListType, start = 1): ListNode {\n  return $applyNodeReplacement(new ListNode(listType, start));\n}\n\n/**\n * Checks to see if the node is a ListNode.\n * @param node - The node to be checked.\n * @returns true if the node is a ListNode, false otherwise.\n */\nexport function $isListNode(\n  node: LexicalNode | null | undefined,\n): node is ListNode {\n  return node instanceof ListNode;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/list/__tests__/unit/LexicalListItemNode.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {\n  $createParagraphNode,\n  $createRangeSelection,\n  $getRoot, LexicalEditor,\n  TextNode,\n} from 'lexical';\nimport {\n  createTestContext, destroyFromContext,\n  expectHtmlToBeEqual,\n  html,\n} from 'lexical/__tests__/utils';\n\nimport {\n  $createListItemNode,\n  $isListItemNode,\n  ListItemNode,\n  ListNode,\n} from '../..';\nimport {EditorUiContext} from \"../../../../ui/framework/core\";\nimport {$htmlToBlockNodes} from \"../../../../utils/nodes\";\n\ndescribe('LexicalListItemNode tests', () => {\n\n  let context!: EditorUiContext;\n  let editor!: LexicalEditor;\n\n  beforeEach(() => {\n    context = createTestContext();\n    editor = context.editor;\n  });\n\n  afterEach(() => {\n    destroyFromContext(context);\n  });\n\n  test('ListItemNode.constructor', async () => {\n\n    await editor.update(() => {\n      const listItemNode = new ListItemNode();\n\n      expect(listItemNode.getType()).toBe('listitem');\n\n      expect(listItemNode.getTextContent()).toBe('');\n    });\n\n    expect(() => new ListItemNode()).toThrow();\n  });\n\n  test('ListItemNode.createDOM()', async () => {\n\n    await editor.update(() => {\n      const listItemNode = new ListItemNode();\n\n      expectHtmlToBeEqual(\n          listItemNode.createDOM(editor._config).outerHTML,\n          html`\n            <li value=\"1\"></li>\n          `,\n      );\n\n      expectHtmlToBeEqual(\n          listItemNode.createDOM({\n            namespace: '',\n            theme: {},\n          }).outerHTML,\n          html`\n            <li value=\"1\"></li>\n          `,\n      );\n    });\n  });\n\n  describe('ListItemNode.updateDOM()', () => {\n    test('base', async () => {\n\n      await editor.update(() => {\n        const listItemNode = new ListItemNode();\n\n        const domElement = listItemNode.createDOM(editor._config);\n\n        expectHtmlToBeEqual(\n            domElement.outerHTML,\n            html`\n              <li value=\"1\"></li>\n            `,\n        );\n        const newListItemNode = new ListItemNode();\n\n        const result = newListItemNode.updateDOM(\n            listItemNode,\n            domElement,\n            editor._config,\n        );\n\n        expect(result).toBe(false);\n\n        expectHtmlToBeEqual(\n            domElement.outerHTML,\n            html`\n              <li value=\"1\"></li>\n            `,\n        );\n      });\n    });\n\n    test('nested list', async () => {\n\n      await editor.update(() => {\n        const parentListNode = new ListNode('bullet', 1);\n        const parentlistItemNode = new ListItemNode();\n\n        parentListNode.append(parentlistItemNode);\n        const domElement = parentlistItemNode.createDOM(editor._config);\n\n        expectHtmlToBeEqual(\n            domElement.outerHTML,\n            html`\n              <li value=\"1\"></li>\n            `,\n        );\n        const nestedListNode = new ListNode('bullet', 1);\n        nestedListNode.append(new ListItemNode());\n        parentlistItemNode.append(nestedListNode);\n        const result = parentlistItemNode.updateDOM(\n            parentlistItemNode,\n            domElement,\n            editor._config,\n        );\n\n        expect(result).toBe(false);\n\n        expectHtmlToBeEqual(\n            domElement.outerHTML,\n            html`\n              <li value=\"1\" style=\"list-style: none;\"></li>\n            `,\n        );\n      });\n    });\n  });\n\n  describe('ListItemNode.replace()', () => {\n    let listNode: ListNode;\n    let listItemNode1: ListItemNode;\n    let listItemNode2: ListItemNode;\n    let listItemNode3: ListItemNode;\n\n    beforeEach(async () => {\n\n      await editor.update(() => {\n        const root = $getRoot();\n        listNode = new ListNode('bullet', 1);\n        listItemNode1 = new ListItemNode();\n\n        listItemNode1.append(new TextNode('one'));\n        listItemNode2 = new ListItemNode();\n\n        listItemNode2.append(new TextNode('two'));\n        listItemNode3 = new ListItemNode();\n\n        listItemNode3.append(new TextNode('three'));\n        root.append(listNode);\n        listNode.append(listItemNode1, listItemNode2, listItemNode3);\n      });\n\n      expectHtmlToBeEqual(\n          context.editorDOM.outerHTML,\n          html`\n            <div\n              contenteditable=\"true\"\n              style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\"\n              data-lexical-editor=\"true\">\n              <ul>\n                <li value=\"1\">\n                  <span data-lexical-text=\"true\">one</span>\n                </li>\n                <li value=\"2\">\n                  <span data-lexical-text=\"true\">two</span>\n                </li>\n                <li value=\"3\">\n                  <span data-lexical-text=\"true\">three</span>\n                </li>\n              </ul>\n            </div>\n          `,\n      );\n    });\n\n    test('another list item node', async () => {\n\n      await editor.update(() => {\n        const newListItemNode = new ListItemNode();\n\n        newListItemNode.append(new TextNode('bar'));\n        listItemNode1.replace(newListItemNode);\n      });\n\n      expectHtmlToBeEqual(\n          context.editorDOM.outerHTML,\n          html`\n            <div\n              contenteditable=\"true\"\n              style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\"\n              data-lexical-editor=\"true\">\n              <ul>\n                <li value=\"1\">\n                  <span data-lexical-text=\"true\">bar</span>\n                </li>\n                <li value=\"2\">\n                  <span data-lexical-text=\"true\">two</span>\n                </li>\n                <li value=\"3\">\n                  <span data-lexical-text=\"true\">three</span>\n                </li>\n              </ul>\n            </div>\n          `,\n      );\n    });\n\n    test('first list item with a non list item node', async () => {\n\n      await editor.update(() => {\n        return;\n      });\n\n      expectHtmlToBeEqual(\n          context.editorDOM.outerHTML,\n          html`\n            <div\n              contenteditable=\"true\"\n              style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\"\n              data-lexical-editor=\"true\">\n              <ul>\n                <li value=\"1\">\n                  <span data-lexical-text=\"true\">one</span>\n                </li>\n                <li value=\"2\">\n                  <span data-lexical-text=\"true\">two</span>\n                </li>\n                <li value=\"3\">\n                  <span data-lexical-text=\"true\">three</span>\n                </li>\n              </ul>\n            </div>\n          `,\n      );\n\n      await editor.update(() => {\n        const paragraphNode = $createParagraphNode();\n        listItemNode1.replace(paragraphNode);\n      });\n\n      expectHtmlToBeEqual(\n          context.editorDOM.outerHTML,\n          html`\n            <div\n              contenteditable=\"true\"\n              style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\"\n              data-lexical-editor=\"true\">\n              <p><br></p>\n              <ul>\n                <li value=\"1\">\n                  <span data-lexical-text=\"true\">two</span>\n                </li>\n                <li value=\"2\">\n                  <span data-lexical-text=\"true\">three</span>\n                </li>\n              </ul>\n            </div>\n          `,\n      );\n    });\n\n    test('last list item with a non list item node', async () => {\n\n      await editor.update(() => {\n        const paragraphNode = $createParagraphNode();\n        listItemNode3.replace(paragraphNode);\n      });\n\n      expectHtmlToBeEqual(\n          context.editorDOM.outerHTML,\n          html`\n            <div\n              contenteditable=\"true\"\n              style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\"\n              data-lexical-editor=\"true\">\n              <ul>\n                <li value=\"1\">\n                  <span data-lexical-text=\"true\">one</span>\n                </li>\n                <li value=\"2\">\n                  <span data-lexical-text=\"true\">two</span>\n                </li>\n              </ul>\n              <p><br></p>\n            </div>\n          `,\n      );\n    });\n\n    test('middle list item with a non list item node', async () => {\n\n      await editor.update(() => {\n        const paragraphNode = $createParagraphNode();\n        listItemNode2.replace(paragraphNode);\n      });\n\n      expectHtmlToBeEqual(\n          context.editorDOM.outerHTML,\n          html`\n            <div\n              contenteditable=\"true\"\n              style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\"\n              data-lexical-editor=\"true\">\n              <ul>\n                <li value=\"1\">\n                  <span data-lexical-text=\"true\">one</span>\n                </li>\n              </ul>\n              <p><br></p>\n              <ul>\n                <li value=\"1\">\n                  <span data-lexical-text=\"true\">three</span>\n                </li>\n              </ul>\n            </div>\n          `,\n      );\n    });\n\n    test('the only list item with a non list item node', async () => {\n\n      await editor.update(() => {\n        listItemNode2.remove();\n        listItemNode3.remove();\n      });\n\n      expectHtmlToBeEqual(\n          context.editorDOM.outerHTML,\n          html`\n            <div\n              contenteditable=\"true\"\n              style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\"\n              data-lexical-editor=\"true\">\n              <ul>\n                <li value=\"1\">\n                  <span data-lexical-text=\"true\">one</span>\n                </li>\n              </ul>\n            </div>\n          `,\n      );\n\n      await editor.update(() => {\n        const paragraphNode = $createParagraphNode();\n        listItemNode1.replace(paragraphNode);\n      });\n\n      expectHtmlToBeEqual(\n          context.editorDOM.outerHTML,\n          html`\n            <div\n              contenteditable=\"true\"\n              style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\"\n              data-lexical-editor=\"true\">\n              <p><br></p>\n            </div>\n          `,\n      );\n    });\n  });\n\n  describe('ListItemNode.remove()', () => {\n    // - A\n    // - x\n    // - B\n    test('siblings are not nested', async () => {\n      let x: ListItemNode;\n\n      await editor.update(() => {\n        const root = $getRoot();\n        const parent = new ListNode('bullet', 1);\n\n        const A_listItem = new ListItemNode();\n        A_listItem.append(new TextNode('A'));\n\n        x = new ListItemNode();\n        x.append(new TextNode('x'));\n\n        const B_listItem = new ListItemNode();\n        B_listItem.append(new TextNode('B'));\n\n        parent.append(A_listItem, x, B_listItem);\n        root.append(parent);\n      });\n\n      expectHtmlToBeEqual(\n          context.editorDOM.outerHTML,\n          html`\n            <div\n              contenteditable=\"true\"\n              style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\"\n              data-lexical-editor=\"true\">\n              <ul>\n                <li value=\"1\">\n                  <span data-lexical-text=\"true\">A</span>\n                </li>\n                <li value=\"2\">\n                  <span data-lexical-text=\"true\">x</span>\n                </li>\n                <li value=\"3\">\n                  <span data-lexical-text=\"true\">B</span>\n                </li>\n              </ul>\n            </div>\n          `,\n      );\n\n      await editor.update(() => x.remove());\n\n      expectHtmlToBeEqual(\n          context.editorDOM.outerHTML,\n          html`\n            <div\n              contenteditable=\"true\"\n              style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\"\n              data-lexical-editor=\"true\">\n              <ul>\n                <li value=\"1\">\n                  <span data-lexical-text=\"true\">A</span>\n                </li>\n                <li value=\"2\">\n                  <span data-lexical-text=\"true\">B</span>\n                </li>\n              </ul>\n            </div>\n          `,\n      );\n    });\n\n    //   - A\n    // - x\n    // - B\n    test('the previous sibling is nested', async () => {\n      let x: ListItemNode;\n\n      await editor.update(() => {\n        const root = $getRoot();\n        const parent = new ListNode('bullet', 1);\n\n        const A_listItem = new ListItemNode();\n        const A_nestedList = new ListNode('bullet', 1);\n        const A_nestedListItem = new ListItemNode();\n        A_listItem.append(A_nestedList);\n        A_nestedList.append(A_nestedListItem);\n        A_nestedListItem.append(new TextNode('A'));\n\n        x = new ListItemNode();\n        x.append(new TextNode('x'));\n\n        const B_listItem = new ListItemNode();\n        B_listItem.append(new TextNode('B'));\n\n        parent.append(A_listItem, x, B_listItem);\n        root.append(parent);\n      });\n\n      expectHtmlToBeEqual(\n          context.editorDOM.innerHTML,\n          html`\n            <ul>\n              <li value=\"1\" style=\"list-style: none;\">\n                <ul>\n                  <li value=\"1\">\n                    <span data-lexical-text=\"true\">A</span>\n                  </li>\n                </ul>\n              </li>\n              <li value=\"1\">\n                <span data-lexical-text=\"true\">x</span>\n              </li>\n              <li value=\"2\">\n                <span data-lexical-text=\"true\">B</span>\n              </li>\n            </ul>\n          `,\n      );\n\n      await editor.update(() => x.remove());\n\n      expectHtmlToBeEqual(\n          context.editorDOM.innerHTML,\n          html`\n            <ul>\n              <li value=\"1\" style=\"list-style: none;\">\n                <ul>\n                  <li value=\"1\">\n                    <span data-lexical-text=\"true\">A</span>\n                  </li>\n                </ul>\n              </li>\n              <li value=\"1\">\n                <span data-lexical-text=\"true\">B</span>\n              </li>\n            </ul>\n          `,\n      );\n    });\n\n    // - A\n    // - x\n    //   - B\n    test('the next sibling is nested', async () => {\n      let x: ListItemNode;\n\n      await editor.update(() => {\n        const root = $getRoot();\n        const parent = new ListNode('bullet', 1);\n\n        const A_listItem = new ListItemNode();\n        A_listItem.append(new TextNode('A'));\n\n        x = new ListItemNode();\n        x.append(new TextNode('x'));\n\n        const B_listItem = new ListItemNode();\n        const B_nestedList = new ListNode('bullet', 1);\n        const B_nestedListItem = new ListItemNode();\n        B_listItem.append(B_nestedList);\n        B_nestedList.append(B_nestedListItem);\n        B_nestedListItem.append(new TextNode('B'));\n\n        parent.append(A_listItem, x, B_listItem);\n        root.append(parent);\n      });\n\n      expectHtmlToBeEqual(\n          context.editorDOM.innerHTML,\n          html`\n            <ul>\n              <li value=\"1\">\n                <span data-lexical-text=\"true\">A</span>\n              </li>\n              <li value=\"2\">\n                <span data-lexical-text=\"true\">x</span>\n              </li>\n              <li value=\"3\" style=\"list-style: none;\">\n                <ul>\n                  <li value=\"1\">\n                    <span data-lexical-text=\"true\">B</span>\n                  </li>\n                </ul>\n              </li>\n            </ul>\n          `,\n      );\n\n      await editor.update(() => x.remove());\n\n      expectHtmlToBeEqual(\n          context.editorDOM.innerHTML,\n          html`\n            <ul>\n              <li value=\"1\">\n                <span data-lexical-text=\"true\">A</span>\n              </li>\n              <li value=\"2\" style=\"list-style: none;\">\n                <ul>\n                  <li value=\"1\">\n                    <span data-lexical-text=\"true\">B</span>\n                  </li>\n                </ul>\n              </li>\n            </ul>\n          `,\n      );\n    });\n\n    //   - A\n    // - x\n    //   - B\n    test('both siblings are nested', async () => {\n      let x: ListItemNode;\n\n      await editor.update(() => {\n        const root = $getRoot();\n        const parent = new ListNode('bullet', 1);\n\n        const A_listItem = new ListItemNode();\n        const A_nestedList = new ListNode('bullet', 1);\n        const A_nestedListItem = new ListItemNode();\n        A_listItem.append(A_nestedList);\n        A_nestedList.append(A_nestedListItem);\n        A_nestedListItem.append(new TextNode('A'));\n\n        x = new ListItemNode();\n        x.append(new TextNode('x'));\n\n        const B_listItem = new ListItemNode();\n        const B_nestedList = new ListNode('bullet', 1);\n        const B_nestedListItem = new ListItemNode();\n        B_listItem.append(B_nestedList);\n        B_nestedList.append(B_nestedListItem);\n        B_nestedListItem.append(new TextNode('B'));\n\n        parent.append(A_listItem, x, B_listItem);\n        root.append(parent);\n      });\n\n      expectHtmlToBeEqual(\n          context.editorDOM.innerHTML,\n          html`\n            <ul>\n              <li value=\"1\" style=\"list-style: none;\">\n                <ul>\n                  <li value=\"1\">\n                    <span data-lexical-text=\"true\">A</span>\n                  </li>\n                </ul>\n              </li>\n              <li value=\"1\">\n                <span data-lexical-text=\"true\">x</span>\n              </li>\n              <li value=\"2\" style=\"list-style: none;\">\n                <ul>\n                  <li value=\"1\">\n                    <span data-lexical-text=\"true\">B</span>\n                  </li>\n                </ul>\n              </li>\n            </ul>\n          `,\n      );\n\n      await editor.update(() => x.remove());\n\n      expectHtmlToBeEqual(\n          context.editorDOM.innerHTML,\n          html`\n            <ul>\n              <li value=\"1\" style=\"list-style: none;\">\n                <ul>\n                  <li value=\"1\">\n                    <span data-lexical-text=\"true\">A</span>\n                  </li>\n                  <li value=\"2\">\n                    <span data-lexical-text=\"true\">B</span>\n                  </li>\n                </ul>\n              </li>\n            </ul>\n          `,\n      );\n    });\n\n    //  - A1\n    //     - A2\n    // - x\n    //   - B\n    test('the previous sibling is nested deeper than the next sibling', async () => {\n      let x: ListItemNode;\n\n      await editor.update(() => {\n        const root = $getRoot();\n        const parent = new ListNode('bullet', 1);\n\n        const A_listItem = new ListItemNode();\n        const A_nestedList = new ListNode('bullet', 1);\n        const A_nestedListItem1 = new ListItemNode();\n        const A_nestedListItem2 = new ListItemNode();\n        const A_deeplyNestedList = new ListNode('bullet', 1);\n        const A_deeplyNestedListItem = new ListItemNode();\n        A_listItem.append(A_nestedList);\n        A_nestedList.append(A_nestedListItem1);\n        A_nestedList.append(A_nestedListItem2);\n        A_nestedListItem1.append(new TextNode('A1'));\n        A_nestedListItem2.append(A_deeplyNestedList);\n        A_deeplyNestedList.append(A_deeplyNestedListItem);\n        A_deeplyNestedListItem.append(new TextNode('A2'));\n\n        x = new ListItemNode();\n        x.append(new TextNode('x'));\n\n        const B_listItem = new ListItemNode();\n        const B_nestedList = new ListNode('bullet', 1);\n        const B_nestedlistItem = new ListItemNode();\n        B_listItem.append(B_nestedList);\n        B_nestedList.append(B_nestedlistItem);\n        B_nestedlistItem.append(new TextNode('B'));\n\n        parent.append(A_listItem, x, B_listItem);\n        root.append(parent);\n      });\n\n      expectHtmlToBeEqual(\n          context.editorDOM.innerHTML,\n          html`\n            <ul>\n              <li value=\"1\" style=\"list-style: none;\">\n                <ul>\n                  <li value=\"1\">\n                    <span data-lexical-text=\"true\">A1</span>\n                  </li>\n                  <li value=\"2\" style=\"list-style: none;\">\n                    <ul>\n                      <li value=\"1\">\n                        <span data-lexical-text=\"true\">A2</span>\n                      </li>\n                    </ul>\n                  </li>\n                </ul>\n              </li>\n              <li value=\"1\">\n                <span data-lexical-text=\"true\">x</span>\n              </li>\n              <li value=\"2\" style=\"list-style: none;\">\n                <ul>\n                  <li value=\"1\">\n                    <span data-lexical-text=\"true\">B</span>\n                  </li>\n                </ul>\n              </li>\n            </ul>\n          `,\n      );\n\n      await editor.update(() => x.remove());\n\n      expectHtmlToBeEqual(\n          context.editorDOM.innerHTML,\n          html`\n            <ul>\n              <li value=\"1\" style=\"list-style: none;\">\n                <ul>\n                  <li value=\"1\">\n                    <span data-lexical-text=\"true\">A1</span>\n                  </li>\n                  <li value=\"2\" style=\"list-style: none;\">\n                    <ul>\n                      <li value=\"1\">\n                        <span data-lexical-text=\"true\">A2</span>\n                      </li>\n                    </ul>\n                  </li>\n                  <li value=\"2\">\n                    <span data-lexical-text=\"true\">B</span>\n                  </li>\n                </ul>\n              </li>\n            </ul>\n          `,\n      );\n    });\n\n    //   - A\n    // - x\n    //     - B1\n    //   - B2\n    test('the next sibling is nested deeper than the previous sibling', async () => {\n      let x: ListItemNode;\n\n      await editor.update(() => {\n        const root = $getRoot();\n        const parent = new ListNode('bullet', 1);\n\n        const A_listItem = new ListItemNode();\n        const A_nestedList = new ListNode('bullet', 1);\n        const A_nestedListItem = new ListItemNode();\n        A_listItem.append(A_nestedList);\n        A_nestedList.append(A_nestedListItem);\n        A_nestedListItem.append(new TextNode('A'));\n\n        x = new ListItemNode();\n        x.append(new TextNode('x'));\n\n        const B_listItem = new ListItemNode();\n        const B_nestedList = new ListNode('bullet', 1);\n        const B_nestedListItem1 = new ListItemNode();\n        const B_nestedListItem2 = new ListItemNode();\n        const B_deeplyNestedList = new ListNode('bullet', 1);\n        const B_deeplyNestedListItem = new ListItemNode();\n        B_listItem.append(B_nestedList);\n        B_nestedList.append(B_nestedListItem1);\n        B_nestedList.append(B_nestedListItem2);\n        B_nestedListItem1.append(B_deeplyNestedList);\n        B_nestedListItem2.append(new TextNode('B2'));\n        B_deeplyNestedList.append(B_deeplyNestedListItem);\n        B_deeplyNestedListItem.append(new TextNode('B1'));\n\n        parent.append(A_listItem, x, B_listItem);\n        root.append(parent);\n      });\n\n      expectHtmlToBeEqual(\n          context.editorDOM.innerHTML,\n          html`\n            <ul>\n              <li value=\"1\" style=\"list-style: none;\">\n                <ul>\n                  <li value=\"1\">\n                    <span data-lexical-text=\"true\">A</span>\n                  </li>\n                </ul>\n              </li>\n              <li value=\"1\">\n                <span data-lexical-text=\"true\">x</span>\n              </li>\n              <li value=\"2\" style=\"list-style: none;\">\n                <ul>\n                  <li value=\"1\" style=\"list-style: none;\">\n                    <ul>\n                      <li value=\"1\">\n                        <span data-lexical-text=\"true\">B1</span>\n                      </li>\n                    </ul>\n                  </li>\n                  <li value=\"1\">\n                    <span data-lexical-text=\"true\">B2</span>\n                  </li>\n                </ul>\n              </li>\n            </ul>\n          `,\n      );\n\n      await editor.update(() => x.remove());\n\n      expectHtmlToBeEqual(\n          context.editorDOM.innerHTML,\n          html`\n            <ul>\n              <li value=\"1\" style=\"list-style: none;\">\n                <ul>\n                  <li value=\"1\">\n                    <span data-lexical-text=\"true\">A</span>\n                  </li>\n                  <li value=\"2\" style=\"list-style: none;\">\n                    <ul>\n                      <li value=\"1\">\n                        <span data-lexical-text=\"true\">B1</span>\n                      </li>\n                    </ul>\n                  </li>\n                  <li value=\"2\">\n                    <span data-lexical-text=\"true\">B2</span>\n                  </li>\n                </ul>\n              </li>\n            </ul>\n          `,\n      );\n    });\n\n    //   - A1\n    //     - A2\n    // - x\n    //     - B1\n    //   - B2\n    test('both siblings are deeply nested', async () => {\n      let x: ListItemNode;\n\n      await editor.update(() => {\n        const root = $getRoot();\n        const parent = new ListNode('bullet', 1);\n\n        const A_listItem = new ListItemNode();\n        const A_nestedList = new ListNode('bullet', 1);\n        const A_nestedListItem1 = new ListItemNode();\n        const A_nestedListItem2 = new ListItemNode();\n        const A_deeplyNestedList = new ListNode('bullet', 1);\n        const A_deeplyNestedListItem = new ListItemNode();\n        A_listItem.append(A_nestedList);\n        A_nestedList.append(A_nestedListItem1);\n        A_nestedList.append(A_nestedListItem2);\n        A_nestedListItem1.append(new TextNode('A1'));\n        A_nestedListItem2.append(A_deeplyNestedList);\n        A_deeplyNestedList.append(A_deeplyNestedListItem);\n        A_deeplyNestedListItem.append(new TextNode('A2'));\n\n        x = new ListItemNode();\n        x.append(new TextNode('x'));\n\n        const B_listItem = new ListItemNode();\n        const B_nestedList = new ListNode('bullet', 1);\n        const B_nestedListItem1 = new ListItemNode();\n        const B_nestedListItem2 = new ListItemNode();\n        const B_deeplyNestedList = new ListNode('bullet', 1);\n        const B_deeplyNestedListItem = new ListItemNode();\n        B_listItem.append(B_nestedList);\n        B_nestedList.append(B_nestedListItem1);\n        B_nestedList.append(B_nestedListItem2);\n        B_nestedListItem1.append(B_deeplyNestedList);\n        B_nestedListItem2.append(new TextNode('B2'));\n        B_deeplyNestedList.append(B_deeplyNestedListItem);\n        B_deeplyNestedListItem.append(new TextNode('B1'));\n\n        parent.append(A_listItem, x, B_listItem);\n        root.append(parent);\n      });\n\n      expectHtmlToBeEqual(\n          context.editorDOM.innerHTML,\n          html`\n            <ul>\n              <li value=\"1\" style=\"list-style: none;\">\n                <ul>\n                  <li value=\"1\">\n                    <span data-lexical-text=\"true\">A1</span>\n                  </li>\n                  <li value=\"2\" style=\"list-style: none;\">\n                    <ul>\n                      <li value=\"1\">\n                        <span data-lexical-text=\"true\">A2</span>\n                      </li>\n                    </ul>\n                  </li>\n                </ul>\n              </li>\n              <li value=\"1\">\n                <span data-lexical-text=\"true\">x</span>\n              </li>\n              <li value=\"2\" style=\"list-style: none;\">\n                <ul>\n                  <li value=\"1\" style=\"list-style: none;\">\n                    <ul>\n                      <li value=\"1\">\n                        <span data-lexical-text=\"true\">B1</span>\n                      </li>\n                    </ul>\n                  </li>\n                  <li value=\"1\">\n                    <span data-lexical-text=\"true\">B2</span>\n                  </li>\n                </ul>\n              </li>\n            </ul>\n          `,\n      );\n\n      await editor.update(() => x.remove());\n\n      expectHtmlToBeEqual(\n          context.editorDOM.innerHTML,\n          html`\n            <ul>\n              <li value=\"1\" style=\"list-style: none;\">\n                <ul>\n                  <li value=\"1\">\n                    <span data-lexical-text=\"true\">A1</span>\n                  </li>\n                  <li value=\"2\" style=\"list-style: none;\">\n                    <ul>\n                      <li value=\"1\">\n                        <span data-lexical-text=\"true\">A2</span>\n                      </li>\n                      <li value=\"2\">\n                        <span data-lexical-text=\"true\">B1</span>\n                      </li>\n                    </ul>\n                  </li>\n                  <li value=\"2\">\n                    <span data-lexical-text=\"true\">B2</span>\n                  </li>\n                </ul>\n              </li>\n            </ul>\n          `,\n      );\n    });\n  });\n\n  describe('ListItemNode.insertNewAfter(): non-empty list items', () => {\n    let listNode: ListNode;\n    let listItemNode1: ListItemNode;\n    let listItemNode2: ListItemNode;\n    let listItemNode3: ListItemNode;\n\n    beforeEach(async () => {\n\n      await editor.update(() => {\n        const root = $getRoot();\n        listNode = new ListNode('bullet', 1);\n        listItemNode1 = new ListItemNode();\n\n        listItemNode2 = new ListItemNode();\n\n        listItemNode3 = new ListItemNode();\n\n        root.append(listNode);\n        listNode.append(listItemNode1, listItemNode2, listItemNode3);\n        listItemNode1.append(new TextNode('one'));\n        listItemNode2.append(new TextNode('two'));\n        listItemNode3.append(new TextNode('three'));\n      });\n\n      expectHtmlToBeEqual(\n          context.editorDOM.outerHTML,\n          html`\n            <div\n              contenteditable=\"true\"\n              style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\"\n              data-lexical-editor=\"true\">\n              <ul>\n                <li value=\"1\">\n                  <span data-lexical-text=\"true\">one</span>\n                </li>\n                <li value=\"2\">\n                  <span data-lexical-text=\"true\">two</span>\n                </li>\n                <li value=\"3\">\n                  <span data-lexical-text=\"true\">three</span>\n                </li>\n              </ul>\n            </div>\n          `,\n      );\n    });\n\n    test('first list item', async () => {\n\n      await editor.update(() => {\n        listItemNode1.insertNewAfter($createRangeSelection());\n      });\n\n      expectHtmlToBeEqual(\n          context.editorDOM.outerHTML,\n          html`\n            <div\n              contenteditable=\"true\"\n              style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\"\n              data-lexical-editor=\"true\">\n              <ul>\n                <li value=\"1\">\n                  <span data-lexical-text=\"true\">one</span>\n                </li>\n                <li value=\"2\"><br></li>\n                <li value=\"3\">\n                  <span data-lexical-text=\"true\">two</span>\n                </li>\n                <li value=\"4\">\n                  <span data-lexical-text=\"true\">three</span>\n                </li>\n              </ul>\n            </div>\n          `,\n      );\n    });\n\n    test('last list item', async () => {\n\n      await editor.update(() => {\n        listItemNode3.insertNewAfter($createRangeSelection());\n      });\n\n      expectHtmlToBeEqual(\n          context.editorDOM.outerHTML,\n          html`\n            <div\n              contenteditable=\"true\"\n              style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\"\n              data-lexical-editor=\"true\">\n              <ul>\n                <li value=\"1\">\n                  <span data-lexical-text=\"true\">one</span>\n                </li>\n                <li value=\"2\">\n                  <span data-lexical-text=\"true\">two</span>\n                </li>\n                <li value=\"3\">\n                  <span data-lexical-text=\"true\">three</span>\n                </li>\n                <li value=\"4\"><br></li>\n              </ul>\n            </div>\n          `,\n      );\n    });\n\n    test('middle list item', async () => {\n\n      await editor.update(() => {\n        listItemNode3.insertNewAfter($createRangeSelection());\n      });\n\n      expectHtmlToBeEqual(\n          context.editorDOM.outerHTML,\n          html`\n            <div\n              contenteditable=\"true\"\n              style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\"\n              data-lexical-editor=\"true\">\n              <ul>\n                <li value=\"1\">\n                  <span data-lexical-text=\"true\">one</span>\n                </li>\n                <li value=\"2\">\n                  <span data-lexical-text=\"true\">two</span>\n                </li>\n                <li value=\"3\">\n                  <span data-lexical-text=\"true\">three</span>\n                </li>\n                <li value=\"4\"><br></li>\n              </ul>\n            </div>\n          `,\n      );\n    });\n\n    test('the only list item', async () => {\n      await editor.update(() => {\n        listItemNode2.remove();\n        listItemNode3.remove();\n      });\n\n      expectHtmlToBeEqual(\n          context.editorDOM.outerHTML,\n          html`\n            <div\n              contenteditable=\"true\"\n              style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\"\n              data-lexical-editor=\"true\">\n              <ul>\n                <li value=\"1\">\n                  <span data-lexical-text=\"true\">one</span>\n                </li>\n              </ul>\n            </div>\n          `,\n      );\n\n      await editor.update(() => {\n        listItemNode1.insertNewAfter($createRangeSelection());\n      });\n\n      expectHtmlToBeEqual(\n          context.editorDOM.outerHTML,\n          html`\n            <div\n              contenteditable=\"true\"\n              style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\"\n              data-lexical-editor=\"true\">\n              <ul>\n                <li value=\"1\">\n                  <span data-lexical-text=\"true\">one</span>\n                </li>\n                <li value=\"2\"><br></li>\n              </ul>\n            </div>\n          `,\n      );\n    });\n  });\n\n  describe('ListItemNode.insertNewAfter()', () => {\n    test('new items after empty nested items un-nests the current item instead of creating new', () => {\n      let nestedItem!: ListItemNode;\n      const input = `<ul>\n        <li>\n            Item A\n            <ul><li>Nested item A</li></ul>\n        </li>        \n        <li>Item B</li>        \n        </ul>`;\n\n      editor.updateAndCommit(() => {\n        const root = $getRoot();\n        root.append(...$htmlToBlockNodes(editor, input));\n        const list = root.getFirstChild() as ListNode;\n        const itemA = list.getFirstChild() as ListItemNode;\n        const nestedList = itemA.getLastChild() as ListNode;\n        nestedItem = nestedList.getFirstChild() as ListItemNode;\n        nestedList.selectEnd();\n      });\n\n      editor.updateAndCommit(() => {\n          nestedItem.insertNewAfter($createRangeSelection());\n          const newItem = nestedItem.getNextSibling() as ListItemNode;\n          newItem.insertNewAfter($createRangeSelection());\n      });\n\n      expectHtmlToBeEqual(\n          context.editorDOM.innerHTML,\n          html`<ul>\n            <li value=\"1\">\n              <span data-lexical-text=\"true\">Item A</span>\n              <ul><li value=\"1\"><span data-lexical-text=\"true\">Nested item A</span></li></ul>\n            </li>\n            <li value=\"2\"><br></li>\n            <li value=\"3\"><span data-lexical-text=\"true\">Item B</span></li>\n          </ul>`,\n      );\n    });\n  });\n\n  test('$createListItemNode()', async () => {\n    await editor.update(() => {\n      const listItemNode = new ListItemNode();\n\n      const createdListItemNode = $createListItemNode();\n\n      expect(listItemNode.__type).toEqual(createdListItemNode.__type);\n      expect(listItemNode.__parent).toEqual(createdListItemNode.__parent);\n      expect(listItemNode.__key).not.toEqual(createdListItemNode.__key);\n    });\n  });\n\n  test('$isListItemNode()', async () => {\n    await editor.update(() => {\n      const listItemNode = new ListItemNode();\n\n      expect($isListItemNode(listItemNode)).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/list/__tests__/unit/LexicalListNode.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\nimport {ParagraphNode, TextNode} from 'lexical';\nimport {createTestContext} from 'lexical/__tests__/utils';\n\nimport {\n  $createListItemNode,\n  $createListNode,\n  $isListItemNode,\n  $isListNode,\n  ListItemNode,\n  ListNode,\n} from '../..';\nimport {$htmlToBlockNodes} from \"../../../../utils/nodes\";\n\nconst editorConfig = Object.freeze({\n  namespace: '',\n  theme: {\n    list: {\n      ol: 'my-ol-list-class',\n      olDepth: [\n        'my-ol-list-class-1',\n        'my-ol-list-class-2',\n        'my-ol-list-class-3',\n        'my-ol-list-class-4',\n        'my-ol-list-class-5',\n        'my-ol-list-class-6',\n        'my-ol-list-class-7',\n      ],\n      ul: 'my-ul-list-class',\n      ulDepth: [\n        'my-ul-list-class-1',\n        'my-ul-list-class-2',\n        'my-ul-list-class-3',\n        'my-ul-list-class-4',\n        'my-ul-list-class-5',\n        'my-ul-list-class-6',\n        'my-ul-list-class-7',\n      ],\n    },\n  },\n});\n\ndescribe('LexicalListNode tests', () => {\n  test('ListNode.constructor', async () => {\n    const {editor} = createTestContext();\n\n    await editor.update(() => {\n      const listNode = $createListNode('bullet', 1);\n      expect(listNode.getType()).toBe('list');\n      expect(listNode.getTag()).toBe('ul');\n      expect(listNode.getTextContent()).toBe('');\n    });\n\n    // @ts-expect-error\n    expect(() => $createListNode()).toThrow();\n  });\n\n  test('ListNode.getTag()', async () => {\n    const {editor} = createTestContext();\n\n    await editor.update(() => {\n      const ulListNode = $createListNode('bullet', 1);\n      expect(ulListNode.getTag()).toBe('ul');\n      const olListNode = $createListNode('number', 1);\n      expect(olListNode.getTag()).toBe('ol');\n      const checkListNode = $createListNode('check', 1);\n      expect(checkListNode.getTag()).toBe('ul');\n    });\n  });\n\n  test('ListNode.createDOM()', async () => {\n    const {editor} = createTestContext();\n\n    await editor.update(() => {\n      const listNode = $createListNode('bullet', 1);\n      expect(listNode.createDOM(editorConfig).outerHTML).toBe(\n          '<ul class=\"my-ul-list-class my-ul-list-class-1\"></ul>',\n      );\n      expect(\n          listNode.createDOM({\n            namespace: '',\n            theme: {\n              list: {},\n            },\n          }).outerHTML,\n      ).toBe('<ul></ul>');\n      expect(\n          listNode.createDOM({\n            namespace: '',\n            theme: {},\n          }).outerHTML,\n      ).toBe('<ul></ul>');\n    });\n  });\n\n  test('ListNode.createDOM() correctly applies classes to a nested ListNode', async () => {\n    const {editor} = createTestContext();\n\n    await editor.update(() => {\n      const listNode1 = $createListNode('bullet');\n      const listNode2 = $createListNode('bullet');\n      const listNode3 = $createListNode('bullet');\n      const listNode4 = $createListNode('bullet');\n      const listNode5 = $createListNode('bullet');\n      const listNode6 = $createListNode('bullet');\n      const listNode7 = $createListNode('bullet');\n\n      const listItem1 = $createListItemNode();\n      const listItem2 = $createListItemNode();\n      const listItem3 = $createListItemNode();\n      const listItem4 = $createListItemNode();\n\n      listNode1.append(listItem1);\n      listItem1.append(listNode2);\n      listNode2.append(listItem2);\n      listItem2.append(listNode3);\n      listNode3.append(listItem3);\n      listItem3.append(listNode4);\n      listNode4.append(listItem4);\n      listNode4.append(listNode5);\n      listNode5.append(listNode6);\n      listNode6.append(listNode7);\n\n      expect(listNode1.createDOM(editorConfig).outerHTML).toBe(\n          '<ul class=\"my-ul-list-class my-ul-list-class-1\"></ul>',\n      );\n      expect(\n          listNode1.createDOM({\n            namespace: '',\n            theme: {\n              list: {},\n            },\n          }).outerHTML,\n      ).toBe('<ul></ul>');\n      expect(\n          listNode1.createDOM({\n            namespace: '',\n            theme: {},\n          }).outerHTML,\n      ).toBe('<ul></ul>');\n      expect(listNode2.createDOM(editorConfig).outerHTML).toBe(\n          '<ul class=\"my-ul-list-class my-ul-list-class-2\"></ul>',\n      );\n      expect(listNode3.createDOM(editorConfig).outerHTML).toBe(\n          '<ul class=\"my-ul-list-class my-ul-list-class-3\"></ul>',\n      );\n      expect(listNode4.createDOM(editorConfig).outerHTML).toBe(\n          '<ul class=\"my-ul-list-class my-ul-list-class-4\"></ul>',\n      );\n      expect(listNode5.createDOM(editorConfig).outerHTML).toBe(\n          '<ul class=\"my-ul-list-class my-ul-list-class-5\"></ul>',\n      );\n      expect(listNode6.createDOM(editorConfig).outerHTML).toBe(\n          '<ul class=\"my-ul-list-class my-ul-list-class-6\"></ul>',\n      );\n      expect(listNode7.createDOM(editorConfig).outerHTML).toBe(\n          '<ul class=\"my-ul-list-class my-ul-list-class-7\"></ul>',\n      );\n      expect(\n          listNode5.createDOM({\n            namespace: '',\n            theme: {\n              list: {\n                ...editorConfig.theme.list,\n                ulDepth: [\n                  'my-ul-list-class-1',\n                  'my-ul-list-class-2',\n                  'my-ul-list-class-3',\n                ],\n              },\n            },\n          }).outerHTML,\n      ).toBe('<ul class=\"my-ul-list-class my-ul-list-class-2\"></ul>');\n    });\n  });\n\n  test('ListNode.updateDOM()', async () => {\n    const {editor} = createTestContext();\n\n    await editor.update(() => {\n      const listNode = $createListNode('bullet', 1);\n      const domElement = listNode.createDOM(editorConfig);\n\n      expect(domElement.outerHTML).toBe(\n          '<ul class=\"my-ul-list-class my-ul-list-class-1\"></ul>',\n      );\n\n      const newListNode = $createListNode('number', 1);\n      const result = newListNode.updateDOM(\n          listNode,\n          domElement,\n          editorConfig,\n      );\n\n      expect(result).toBe(true);\n      expect(domElement.outerHTML).toBe(\n          '<ul class=\"my-ul-list-class my-ul-list-class-1\"></ul>',\n      );\n    });\n  });\n\n  test('ListNode.append() should properly transform a ListItemNode', async () => {\n    const {editor} = createTestContext();\n\n    await editor.update(() => {\n      const listNode = new ListNode('bullet', 1);\n      const listItemNode = new ListItemNode();\n      const textNode = new TextNode('Hello');\n\n      listItemNode.append(textNode);\n      const nodesToAppend = [listItemNode];\n\n      expect(listNode.append(...nodesToAppend)).toBe(listNode);\n      expect(listNode.getFirstChild()).toBe(listItemNode);\n      expect(listNode.getFirstChild()?.getTextContent()).toBe('Hello');\n    });\n  });\n\n  test('ListNode.append() should properly transform a ListNode', async () => {\n    const {editor} = createTestContext();\n\n    await editor.update(() => {\n      const listNode = new ListNode('bullet', 1);\n      const nestedListNode = new ListNode('bullet', 1);\n      const listItemNode = new ListItemNode();\n      const textNode = new TextNode('Hello');\n\n      listItemNode.append(textNode);\n      nestedListNode.append(listItemNode);\n\n      const nodesToAppend = [nestedListNode];\n\n      expect(listNode.append(...nodesToAppend)).toBe(listNode);\n      expect($isListItemNode(listNode.getFirstChild())).toBe(true);\n      expect(listNode.getFirstChild<ListItemNode>()!.getFirstChild()).toBe(\n          nestedListNode,\n      );\n    });\n  });\n\n  test('ListNode.append() should properly transform a ParagraphNode', async () => {\n    const {editor} = createTestContext();\n\n    await editor.update(() => {\n      const listNode = new ListNode('bullet', 1);\n      const paragraph = new ParagraphNode();\n      const textNode = new TextNode('Hello');\n      paragraph.append(textNode);\n      const nodesToAppend = [paragraph];\n\n      expect(listNode.append(...nodesToAppend)).toBe(listNode);\n      expect($isListItemNode(listNode.getFirstChild())).toBe(true);\n      expect(listNode.getFirstChild()?.getTextContent()).toBe('Hello');\n    });\n  });\n\n  test('$createListNode()', async () => {\n    const {editor} = createTestContext();\n\n    await editor.update(() => {\n      const listNode = $createListNode('bullet', 1);\n      const createdListNode = $createListNode('bullet');\n\n      expect(listNode.__type).toEqual(createdListNode.__type);\n      expect(listNode.__parent).toEqual(createdListNode.__parent);\n      expect(listNode.__tag).toEqual(createdListNode.__tag);\n      expect(listNode.__key).not.toEqual(createdListNode.__key);\n    });\n  });\n\n  test('$isListNode()', async () => {\n    const {editor} = createTestContext();\n\n    await editor.update(() => {\n      const listNode = $createListNode('bullet', 1);\n\n      expect($isListNode(listNode)).toBe(true);\n    });\n  });\n\n  test('$createListNode() with tag name (backward compatibility)', async () => {\n    const {editor} = createTestContext();\n\n    await editor.update(() => {\n      const numberList = $createListNode('number', 1);\n      const bulletList = $createListNode('bullet', 1);\n      expect(numberList.__listType).toBe('number');\n      expect(bulletList.__listType).toBe('bullet');\n    });\n  });\n\n  test('importDOM handles old editor expected task list format', async () => {\n    const {editor} = createTestContext();\n\n    let list!: ListNode;\n    editor.update(() => {\n      const nodes = $htmlToBlockNodes(editor, `<ul><li class=\"task-list-item\"><input checked=\"\" disabled=\"\" type=\"checkbox\"> A</li></ul>`);\n      list = nodes[0] as ListNode;\n    });\n\n    expect(list).toBeInstanceOf(ListNode);\n    expect(list.getListType()).toBe('check');\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/list/__tests__/unit/utils.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\nimport {$createParagraphNode, $getRoot} from 'lexical';\nimport {initializeUnitTest} from 'lexical/__tests__/utils';\n\nimport {$createListItemNode, $createListNode} from '../..';\nimport {$getListDepth, $getTopListNode, $isLastItemInList} from '../../utils';\n\ndescribe('Lexical List Utils tests', () => {\n  initializeUnitTest((testEnv) => {\n    test('getListDepth should return the 1-based depth of a list with one levels', async () => {\n      const editor = testEnv.editor;\n\n      editor.update(() => {\n        // Root\n        //   |- ListNode\n        const root = $getRoot();\n\n        const topListNode = $createListNode('bullet');\n\n        root.append(topListNode);\n\n        const result = $getListDepth(topListNode);\n\n        expect(result).toEqual(1);\n      });\n    });\n\n    test('getListDepth should return the 1-based depth of a list with two levels', async () => {\n      const editor = testEnv.editor;\n\n      await editor.update(() => {\n        // Root\n        //   |- ListNode\n        //         |- ListItemNode\n        //         |- ListItemNode\n        //         |- ListNode\n        //               |- ListItemNode\n        const root = $getRoot();\n\n        const topListNode = $createListNode('bullet');\n        const secondLevelListNode = $createListNode('bullet');\n\n        const listItem1 = $createListItemNode();\n        const listItem2 = $createListItemNode();\n        const listItem3 = $createListItemNode();\n\n        root.append(topListNode);\n\n        topListNode.append(listItem1);\n        topListNode.append(listItem2);\n        topListNode.append(secondLevelListNode);\n\n        secondLevelListNode.append(listItem3);\n\n        const result = $getListDepth(secondLevelListNode);\n\n        expect(result).toEqual(2);\n      });\n    });\n\n    test('getListDepth should return the 1-based depth of a list with five levels', async () => {\n      const editor = testEnv.editor;\n\n      await editor.update(() => {\n        // Root\n        //   |- ListNode\n        //        |- ListItemNode\n        //             |- ListNode\n        //                  |- ListItemNode\n        //                       |- ListNode\n        //                            |- ListItemNode\n        //                                 |- ListNode\n        //                                     |- ListItemNode\n        //                                          |- ListNode\n        const root = $getRoot();\n\n        const topListNode = $createListNode('bullet');\n        const listNode2 = $createListNode('bullet');\n        const listNode3 = $createListNode('bullet');\n        const listNode4 = $createListNode('bullet');\n        const listNode5 = $createListNode('bullet');\n\n        const listItem1 = $createListItemNode();\n        const listItem2 = $createListItemNode();\n        const listItem3 = $createListItemNode();\n        const listItem4 = $createListItemNode();\n\n        root.append(topListNode);\n\n        topListNode.append(listItem1);\n\n        listItem1.append(listNode2);\n        listNode2.append(listItem2);\n        listItem2.append(listNode3);\n        listNode3.append(listItem3);\n        listItem3.append(listNode4);\n        listNode4.append(listItem4);\n        listItem4.append(listNode5);\n\n        const result = $getListDepth(listNode5);\n\n        expect(result).toEqual(5);\n      });\n    });\n\n    test('getTopListNode should return the top list node when the list is a direct child of the RootNode', async () => {\n      const editor = testEnv.editor;\n\n      await editor.update(() => {\n        // Root\n        //   |- ListNode\n        //         |- ListItemNode\n        //         |- ListItemNode\n        //         |- ListNode\n        //               |- ListItemNode\n        const root = $getRoot();\n\n        const topListNode = $createListNode('bullet');\n        const secondLevelListNode = $createListNode('bullet');\n\n        const listItem1 = $createListItemNode();\n        const listItem2 = $createListItemNode();\n        const listItem3 = $createListItemNode();\n\n        root.append(topListNode);\n\n        topListNode.append(listItem1);\n        topListNode.append(listItem2);\n        topListNode.append(secondLevelListNode);\n        secondLevelListNode.append(listItem3);\n\n        const result = $getTopListNode(listItem3);\n        expect(result.getKey()).toEqual(topListNode.getKey());\n      });\n    });\n\n    test('getTopListNode should return the top list node when the list is not a direct child of the RootNode', async () => {\n      const editor = testEnv.editor;\n\n      await editor.update(() => {\n        // Root\n        // |- ParagraphNode\n        //     |- ListNode\n        //        |- ListItemNode\n        //        |- ListItemNode\n        //           |- ListNode\n        //              |- ListItemNode\n        const root = $getRoot();\n\n        const paragraphNode = $createParagraphNode();\n        const topListNode = $createListNode('bullet');\n        const secondLevelListNode = $createListNode('bullet');\n\n        const listItem1 = $createListItemNode();\n        const listItem2 = $createListItemNode();\n        const listItem3 = $createListItemNode();\n        root.append(paragraphNode);\n        paragraphNode.append(topListNode);\n        topListNode.append(listItem1);\n        topListNode.append(listItem2);\n        topListNode.append(secondLevelListNode);\n        secondLevelListNode.append(listItem3);\n\n        const result = $getTopListNode(listItem3);\n        expect(result.getKey()).toEqual(topListNode.getKey());\n      });\n    });\n\n    test('getTopListNode should return the top list node when the list item is deeply nested.', async () => {\n      const editor = testEnv.editor;\n\n      await editor.update(() => {\n        // Root\n        // |- ParagraphNode\n        //     |- ListNode\n        //        |- ListItemNode\n        //           |- ListNode\n        //              |- ListItemNode\n        //                  |- ListNode\n        //                      |- ListItemNode\n        //        |- ListItemNode\n        const root = $getRoot();\n\n        const paragraphNode = $createParagraphNode();\n        const topListNode = $createListNode('bullet');\n        const secondLevelListNode = $createListNode('bullet');\n        const thirdLevelListNode = $createListNode('bullet');\n\n        const listItem1 = $createListItemNode();\n        const listItem2 = $createListItemNode();\n        const listItem3 = $createListItemNode();\n        const listItem4 = $createListItemNode();\n        root.append(paragraphNode);\n        paragraphNode.append(topListNode);\n        topListNode.append(listItem1);\n        listItem1.append(secondLevelListNode);\n        secondLevelListNode.append(listItem2);\n        listItem2.append(thirdLevelListNode);\n        thirdLevelListNode.append(listItem3);\n        topListNode.append(listItem4);\n\n        const result = $getTopListNode(listItem4);\n        expect(result.getKey()).toEqual(topListNode.getKey());\n      });\n    });\n\n    test('isLastItemInList should return true if the listItem is the last in a nested list.', async () => {\n      const editor = testEnv.editor;\n\n      await editor.update(() => {\n        // Root\n        //   |- ListNode\n        //      |- ListItemNode\n        //         |- ListNode\n        //            |- ListItemNode\n        //                |- ListNode\n        //                    |- ListItemNode\n        const root = $getRoot();\n\n        const topListNode = $createListNode('bullet');\n        const secondLevelListNode = $createListNode('bullet');\n        const thirdLevelListNode = $createListNode('bullet');\n\n        const listItem1 = $createListItemNode();\n        const listItem2 = $createListItemNode();\n        const listItem3 = $createListItemNode();\n\n        root.append(topListNode);\n\n        topListNode.append(listItem1);\n        listItem1.append(secondLevelListNode);\n        secondLevelListNode.append(listItem2);\n        listItem2.append(thirdLevelListNode);\n        thirdLevelListNode.append(listItem3);\n\n        const result = $isLastItemInList(listItem3);\n\n        expect(result).toEqual(true);\n      });\n    });\n\n    test('isLastItemInList should return true if the listItem is the last in a non-nested list.', async () => {\n      const editor = testEnv.editor;\n\n      await editor.update(() => {\n        // Root\n        //   |- ListNode\n        //      |- ListItemNode\n        //      |- ListItemNode\n        const root = $getRoot();\n\n        const topListNode = $createListNode('bullet');\n\n        const listItem1 = $createListItemNode();\n        const listItem2 = $createListItemNode();\n\n        root.append(topListNode);\n\n        topListNode.append(listItem1);\n        topListNode.append(listItem2);\n\n        const result = $isLastItemInList(listItem2);\n\n        expect(result).toEqual(true);\n      });\n    });\n\n    test('isLastItemInList should return false if the listItem is not the last in a nested list.', async () => {\n      const editor = testEnv.editor;\n\n      await editor.update(() => {\n        // Root\n        //   |- ListNode\n        //      |- ListItemNode\n        //         |- ListNode\n        //            |- ListItemNode\n        //                |- ListNode\n        //                    |- ListItemNode\n        const root = $getRoot();\n\n        const topListNode = $createListNode('bullet');\n        const secondLevelListNode = $createListNode('bullet');\n        const thirdLevelListNode = $createListNode('bullet');\n\n        const listItem1 = $createListItemNode();\n        const listItem2 = $createListItemNode();\n        const listItem3 = $createListItemNode();\n\n        root.append(topListNode);\n\n        topListNode.append(listItem1);\n        listItem1.append(secondLevelListNode);\n        secondLevelListNode.append(listItem2);\n        listItem2.append(thirdLevelListNode);\n        thirdLevelListNode.append(listItem3);\n\n        const result = $isLastItemInList(listItem2);\n\n        expect(result).toEqual(false);\n      });\n    });\n\n    test('isLastItemInList should return true if the listItem is not the last in a non-nested list.', async () => {\n      const editor = testEnv.editor;\n\n      await editor.update(() => {\n        // Root\n        //   |- ListNode\n        //      |- ListItemNode\n        //      |- ListItemNode\n        const root = $getRoot();\n\n        const topListNode = $createListNode('bullet');\n\n        const listItem1 = $createListItemNode();\n        const listItem2 = $createListItemNode();\n\n        root.append(topListNode);\n\n        topListNode.append(listItem1);\n        topListNode.append(listItem2);\n\n        const result = $isLastItemInList(listItem1);\n\n        expect(result).toEqual(false);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/list/formatList.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {$getNearestNodeOfType} from '@lexical/utils';\nimport {\n  $createParagraphNode,\n  $getSelection,\n  $isElementNode,\n  $isLeafNode,\n  $isParagraphNode,\n  $isRangeSelection,\n  $isRootOrShadowRoot,\n  ElementNode,\n  LexicalEditor,\n  LexicalNode,\n  NodeKey,\n  ParagraphNode,\n} from 'lexical';\nimport invariant from 'lexical/shared/invariant';\n\nimport {\n  $createListItemNode,\n  $createListNode,\n  $isListItemNode,\n  $isListNode,\n  ListItemNode,\n  ListNode,\n} from './';\nimport {ListType} from './LexicalListNode';\nimport {\n  $getAllListItems,\n  $getTopListNode,\n  $removeHighestEmptyListParent,\n  isNestedListNode,\n} from './utils';\n\nfunction $isSelectingEmptyListItem(\n  anchorNode: ListItemNode | LexicalNode,\n  nodes: Array<LexicalNode>,\n): boolean {\n  return (\n    $isListItemNode(anchorNode) &&\n    (nodes.length === 0 ||\n      (nodes.length === 1 &&\n        anchorNode.is(nodes[0]) &&\n        anchorNode.getChildrenSize() === 0))\n  );\n}\n\n/**\n * Inserts a new ListNode. If the selection's anchor node is an empty ListItemNode and is a child of\n * the root/shadow root, it will replace the ListItemNode with a ListNode and the old ListItemNode.\n * Otherwise it will replace its parent with a new ListNode and re-insert the ListItemNode and any previous children.\n * If the selection's anchor node is not an empty ListItemNode, it will add a new ListNode or merge an existing ListNode,\n * unless the the node is a leaf node, in which case it will attempt to find a ListNode up the branch and replace it with\n * a new ListNode, or create a new ListNode at the nearest root/shadow root.\n * @param editor - The lexical editor.\n * @param listType - The type of list, \"number\" | \"bullet\" | \"check\".\n */\nexport function insertList(editor: LexicalEditor, listType: ListType): void {\n  editor.update(() => {\n    const selection = $getSelection();\n\n    if (selection !== null) {\n      const nodes = selection.getNodes();\n      if ($isRangeSelection(selection)) {\n        const anchorAndFocus = selection.getStartEndPoints();\n        invariant(\n          anchorAndFocus !== null,\n          'insertList: anchor should be defined',\n        );\n        const [anchor] = anchorAndFocus;\n        const anchorNode = anchor.getNode();\n        const anchorNodeParent = anchorNode.getParent();\n\n        if ($isSelectingEmptyListItem(anchorNode, nodes)) {\n          const list = $createListNode(listType);\n\n          if ($isRootOrShadowRoot(anchorNodeParent)) {\n            anchorNode.replace(list);\n            const listItem = $createListItemNode();\n            list.append(listItem);\n          } else if ($isListItemNode(anchorNode)) {\n            const parent = anchorNode.getParentOrThrow();\n            append(list, parent.getChildren());\n            parent.replace(list);\n          }\n\n          return;\n        }\n      }\n\n      const handled = new Set();\n      for (let i = 0; i < nodes.length; i++) {\n        const node = nodes[i];\n\n        if (\n          $isElementNode(node) &&\n          node.isEmpty() &&\n          !$isListItemNode(node) &&\n          !handled.has(node.getKey())\n        ) {\n          $createListOrMerge(node, listType);\n          continue;\n        }\n\n        if ($isLeafNode(node)) {\n          let parent = node.getParent();\n          while (parent != null) {\n            const parentKey = parent.getKey();\n\n            if ($isListNode(parent)) {\n              if (!handled.has(parentKey)) {\n                const newListNode = $createListNode(listType);\n                append(newListNode, parent.getChildren());\n                parent.replace(newListNode);\n                handled.add(parentKey);\n              }\n\n              break;\n            } else {\n              const nextParent = parent.getParent();\n\n              if ($isRootOrShadowRoot(nextParent) && !handled.has(parentKey)) {\n                handled.add(parentKey);\n                $createListOrMerge(parent, listType);\n                break;\n              }\n\n              parent = nextParent;\n            }\n          }\n        }\n      }\n    }\n  });\n}\n\nfunction append(node: ElementNode, nodesToAppend: Array<LexicalNode>) {\n  node.splice(node.getChildrenSize(), 0, nodesToAppend);\n}\n\nfunction $createListOrMerge(node: ElementNode, listType: ListType): ListNode {\n  if ($isListNode(node)) {\n    return node;\n  }\n\n  const previousSibling = node.getPreviousSibling();\n  const nextSibling = node.getNextSibling();\n  const listItem = $createListItemNode();\n  append(listItem, node.getChildren());\n\n  if (\n    $isListNode(previousSibling) &&\n    listType === previousSibling.getListType()\n  ) {\n    previousSibling.append(listItem);\n    node.remove();\n    // if the same type of list is on both sides, merge them.\n\n    if ($isListNode(nextSibling) && listType === nextSibling.getListType()) {\n      append(previousSibling, nextSibling.getChildren());\n      nextSibling.remove();\n    }\n    return previousSibling;\n  } else if (\n    $isListNode(nextSibling) &&\n    listType === nextSibling.getListType()\n  ) {\n    nextSibling.getFirstChildOrThrow().insertBefore(listItem);\n    node.remove();\n    return nextSibling;\n  } else {\n    const list = $createListNode(listType);\n    list.append(listItem);\n    node.replace(list);\n    return list;\n  }\n}\n\n/**\n * A recursive function that goes through each list and their children, including nested lists,\n * appending list2 children after list1 children and updating ListItemNode values.\n * @param list1 - The first list to be merged.\n * @param list2 - The second list to be merged.\n */\nexport function mergeLists(list1: ListNode, list2: ListNode): void {\n  const listItem1 = list1.getLastChild();\n  const listItem2 = list2.getFirstChild();\n\n  if (\n    listItem1 &&\n    listItem2 &&\n    isNestedListNode(listItem1) &&\n    isNestedListNode(listItem2)\n  ) {\n    mergeLists(listItem1.getFirstChild(), listItem2.getFirstChild());\n    listItem2.remove();\n  }\n\n  const toMerge = list2.getChildren();\n  if (toMerge.length > 0) {\n    list1.append(...toMerge);\n  }\n\n  list2.remove();\n}\n\n/**\n * Searches for the nearest ancestral ListNode and removes it. If selection is an empty ListItemNode\n * it will remove the whole list, including the ListItemNode. For each ListItemNode in the ListNode,\n * removeList will also generate new ParagraphNodes in the removed ListNode's place. Any child node\n * inside a ListItemNode will be appended to the new ParagraphNodes.\n * @param editor - The lexical editor.\n */\nexport function removeList(editor: LexicalEditor): void {\n  editor.update(() => {\n    const selection = $getSelection();\n\n    if ($isRangeSelection(selection)) {\n      const listNodes = new Set<ListNode>();\n      const nodes = selection.getNodes();\n      const anchorNode = selection.anchor.getNode();\n\n      if ($isSelectingEmptyListItem(anchorNode, nodes)) {\n        listNodes.add($getTopListNode(anchorNode));\n      } else {\n        for (let i = 0; i < nodes.length; i++) {\n          const node = nodes[i];\n\n          if ($isLeafNode(node)) {\n            const listItemNode = $getNearestNodeOfType(node, ListItemNode);\n\n            if (listItemNode != null) {\n              listNodes.add($getTopListNode(listItemNode));\n            }\n          }\n        }\n      }\n\n      for (const listNode of listNodes) {\n        let insertionPoint: ListNode | ParagraphNode = listNode;\n\n        const listItems = $getAllListItems(listNode);\n\n        for (const listItemNode of listItems) {\n          const paragraph = $createParagraphNode();\n\n          append(paragraph, listItemNode.getChildren());\n\n          insertionPoint.insertAfter(paragraph);\n          insertionPoint = paragraph;\n\n          // When the anchor and focus fall on the textNode\n          // we don't have to change the selection because the textNode will be appended to\n          // the newly generated paragraph.\n          // When selection is in empty nested list item, selection is actually on the listItemNode.\n          // When the corresponding listItemNode is deleted and replaced by the newly generated paragraph\n          // we should manually set the selection's focus and anchor to the newly generated paragraph.\n          if (listItemNode.__key === selection.anchor.key) {\n            selection.anchor.set(paragraph.getKey(), 0, 'element');\n          }\n          if (listItemNode.__key === selection.focus.key) {\n            selection.focus.set(paragraph.getKey(), 0, 'element');\n          }\n\n          listItemNode.remove();\n        }\n        listNode.remove();\n      }\n    }\n  });\n}\n\n/**\n * Takes the value of a child ListItemNode and makes it the value the ListItemNode\n * should be if it isn't already. Also ensures that checked is undefined if the\n * parent does not have a list type of 'check'.\n * @param list - The list whose children are updated.\n */\nexport function updateChildrenListItemValue(list: ListNode): void {\n  const isNotChecklist = list.getListType() !== 'check';\n  let value = list.getStart();\n  for (const child of list.getChildren()) {\n    if ($isListItemNode(child)) {\n      if (child.getValue() !== value) {\n        child.setValue(value);\n      }\n      if (isNotChecklist && child.getLatest().__checked != null) {\n        child.setChecked(undefined);\n      }\n      if (!$isListNode(child.getFirstChild())) {\n        value++;\n      }\n    }\n  }\n}\n\n/**\n * Merge the next sibling list if same type.\n * <ul> will merge with <ul>, but NOT <ul> with <ol>.\n * @param list - The list whose next sibling should be potentially merged\n */\nexport function mergeNextSiblingListIfSameType(list: ListNode): void {\n  const nextSibling = list.getNextSibling();\n  if (\n    $isListNode(nextSibling) &&\n    list.getListType() === nextSibling.getListType()\n  ) {\n    mergeLists(list, nextSibling);\n  }\n}\n\n/**\n * Adds an empty ListNode/ListItemNode chain at listItemNode, so as to\n * create an indent effect. Won't indent ListItemNodes that have a ListNode as\n * a child, but does merge sibling ListItemNodes if one has a nested ListNode.\n * @param listItemNode - The ListItemNode to be indented.\n */\nexport function $handleIndent(listItemNode: ListItemNode): void {\n  // go through each node and decide where to move it.\n  const removed = new Set<NodeKey>();\n\n  if (isNestedListNode(listItemNode) || removed.has(listItemNode.getKey())) {\n    return;\n  }\n\n  const parent = listItemNode.getParent();\n\n  // We can cast both of the below `isNestedListNode` only returns a boolean type instead of a user-defined type guards\n  const nextSibling =\n    listItemNode.getNextSibling<ListItemNode>() as ListItemNode;\n  const previousSibling =\n    listItemNode.getPreviousSibling<ListItemNode>() as ListItemNode;\n  // if there are nested lists on either side, merge them all together.\n\n  if (isNestedListNode(nextSibling) && isNestedListNode(previousSibling)) {\n    const innerList = previousSibling.getFirstChild();\n\n    if ($isListNode(innerList)) {\n      innerList.append(listItemNode);\n      const nextInnerList = nextSibling.getFirstChild();\n\n      if ($isListNode(nextInnerList)) {\n        const children = nextInnerList.getChildren();\n        append(innerList, children);\n        nextSibling.remove();\n        removed.add(nextSibling.getKey());\n      }\n    }\n  } else if (isNestedListNode(nextSibling)) {\n    // if the ListItemNode is next to a nested ListNode, merge them\n    const innerList = nextSibling.getFirstChild();\n\n    if ($isListNode(innerList)) {\n      const firstChild = innerList.getFirstChild();\n\n      if (firstChild !== null) {\n        firstChild.insertBefore(listItemNode);\n      }\n    }\n  } else if (isNestedListNode(previousSibling)) {\n    const innerList = previousSibling.getFirstChild();\n\n    if ($isListNode(innerList)) {\n      innerList.append(listItemNode);\n    }\n  } else {\n    // otherwise, we need to create a new nested ListNode\n\n    if ($isListNode(parent)) {\n      const newListItem = $createListItemNode();\n      const newList = $createListNode(parent.getListType());\n      newListItem.append(newList);\n      newList.append(listItemNode);\n\n      if (previousSibling) {\n        previousSibling.insertAfter(newListItem);\n      } else if (nextSibling) {\n        nextSibling.insertBefore(newListItem);\n      } else {\n        parent.append(newListItem);\n      }\n    }\n  }\n}\n\n/**\n * Removes an indent by removing an empty ListNode/ListItemNode chain. An indented ListItemNode\n * has a great grandparent node of type ListNode, which is where the ListItemNode will reside\n * within as a child.\n * @param listItemNode - The ListItemNode to remove the indent (outdent).\n */\nexport function $handleOutdent(listItemNode: ListItemNode): void {\n  // go through each node and decide where to move it.\n\n  if (isNestedListNode(listItemNode)) {\n    return;\n  }\n  const parentList = listItemNode.getParent();\n  const grandparentListItem = parentList ? parentList.getParent() : undefined;\n  const greatGrandparentList = grandparentListItem\n    ? grandparentListItem.getParent()\n    : undefined;\n  // If it doesn't have these ancestors, it's not indented.\n\n  if (\n    $isListNode(greatGrandparentList) &&\n    $isListItemNode(grandparentListItem) &&\n    $isListNode(parentList)\n  ) {\n    // if it's the first child in it's parent list, insert it into the\n    // great grandparent list before the grandparent\n    const firstChild = parentList ? parentList.getFirstChild() : undefined;\n    const lastChild = parentList ? parentList.getLastChild() : undefined;\n\n    if (listItemNode.is(firstChild)) {\n      grandparentListItem.insertBefore(listItemNode);\n\n      if (parentList.isEmpty()) {\n        grandparentListItem.remove();\n      }\n      // if it's the last child in it's parent list, insert it into the\n      // great grandparent list after the grandparent.\n    } else if (listItemNode.is(lastChild)) {\n      grandparentListItem.insertAfter(listItemNode);\n\n      if (parentList.isEmpty()) {\n        grandparentListItem.remove();\n      }\n    } else {\n      // otherwise, we need to split the siblings into two new nested lists\n      const listType = parentList.getListType();\n      const previousSiblingsListItem = $createListItemNode();\n      const previousSiblingsList = $createListNode(listType);\n      previousSiblingsListItem.append(previousSiblingsList);\n      listItemNode\n        .getPreviousSiblings()\n        .forEach((sibling) => previousSiblingsList.append(sibling));\n      const nextSiblingsListItem = $createListItemNode();\n      const nextSiblingsList = $createListNode(listType);\n      nextSiblingsListItem.append(nextSiblingsList);\n      append(nextSiblingsList, listItemNode.getNextSiblings());\n      // put the sibling nested lists on either side of the grandparent list item in the great grandparent.\n      grandparentListItem.insertBefore(previousSiblingsListItem);\n      grandparentListItem.insertAfter(nextSiblingsListItem);\n      // replace the grandparent list item (now between the siblings) with the outdented list item.\n      grandparentListItem.replace(listItemNode);\n    }\n  }\n}\n\n/**\n * Attempts to insert a ParagraphNode at selection and selects the new node. The selection must contain a ListItemNode\n * or a node that does not already contain text. If its grandparent is the root/shadow root, it will get the ListNode\n * (which should be the parent node) and insert the ParagraphNode as a sibling to the ListNode. If the ListNode is\n * nested in a ListItemNode instead, it will add the ParagraphNode after the grandparent ListItemNode.\n * Throws an invariant if the selection is not a child of a ListNode.\n * @returns true if a ParagraphNode was inserted succesfully, false if there is no selection\n * or the selection does not contain a ListItemNode or the node already holds text.\n */\nexport function $handleListInsertParagraph(): boolean {\n  const selection = $getSelection();\n\n  if (!$isRangeSelection(selection) || !selection.isCollapsed()) {\n    return false;\n  }\n  // Only run this code on empty list items\n  const anchor = selection.anchor.getNode();\n\n  if (!$isListItemNode(anchor) || anchor.getChildrenSize() !== 0) {\n    return false;\n  }\n  const topListNode = $getTopListNode(anchor);\n  const parent = anchor.getParent();\n\n  invariant(\n    $isListNode(parent),\n    'A ListItemNode must have a ListNode for a parent.',\n  );\n\n  const grandparent = parent.getParent();\n\n  let replacementNode;\n\n  if ($isRootOrShadowRoot(grandparent)) {\n    replacementNode = $createParagraphNode();\n    topListNode.insertAfter(replacementNode);\n  } else if ($isListItemNode(grandparent)) {\n    replacementNode = $createListItemNode();\n    grandparent.insertAfter(replacementNode);\n  } else {\n    return false;\n  }\n  replacementNode.select();\n\n  const nextSiblings = anchor.getNextSiblings();\n\n  if (nextSiblings.length > 0) {\n    const newList = $createListNode(parent.getListType());\n\n    if ($isParagraphNode(replacementNode)) {\n      replacementNode.insertAfter(newList);\n    } else {\n      const newListItem = $createListItemNode();\n      newListItem.append(newList);\n      replacementNode.insertAfter(newListItem);\n    }\n    nextSiblings.forEach((sibling) => {\n      sibling.remove();\n      newList.append(sibling);\n    });\n  }\n\n  // Don't leave hanging nested empty lists\n  $removeHighestEmptyListParent(anchor);\n\n  return true;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/list/index.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {SerializedListItemNode} from './LexicalListItemNode';\nimport type {ListType, SerializedListNode} from './LexicalListNode';\nimport type {LexicalCommand} from 'lexical';\n\nimport {createCommand} from 'lexical';\n\nimport {$handleListInsertParagraph, insertList, removeList} from './formatList';\nimport {\n  $createListItemNode,\n  $isListItemNode,\n  ListItemNode,\n} from './LexicalListItemNode';\nimport {$createListNode, $isListNode, ListNode} from './LexicalListNode';\nimport {$getListDepth} from './utils';\n\nexport {\n  $createListItemNode,\n  $createListNode,\n  $getListDepth,\n  $handleListInsertParagraph,\n  $isListItemNode,\n  $isListNode,\n  insertList,\n  ListItemNode,\n  ListNode,\n  ListType,\n  removeList,\n  SerializedListItemNode,\n  SerializedListNode,\n};\n\nexport const INSERT_UNORDERED_LIST_COMMAND: LexicalCommand<void> =\n  createCommand('INSERT_UNORDERED_LIST_COMMAND');\nexport const INSERT_ORDERED_LIST_COMMAND: LexicalCommand<void> = createCommand(\n  'INSERT_ORDERED_LIST_COMMAND',\n);\nexport const INSERT_CHECK_LIST_COMMAND: LexicalCommand<void> = createCommand(\n  'INSERT_CHECK_LIST_COMMAND',\n);\nexport const REMOVE_LIST_COMMAND: LexicalCommand<void> = createCommand(\n  'REMOVE_LIST_COMMAND',\n);\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/list/utils.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {LexicalNode, Spread} from 'lexical';\n\nimport {$findMatchingParent} from '@lexical/utils';\nimport invariant from 'lexical/shared/invariant';\n\nimport {\n  $createListItemNode,\n  $isListItemNode,\n  $isListNode,\n  ListItemNode,\n  ListNode,\n} from './';\n\n/**\n * Checks the depth of listNode from the root node.\n * @param listNode - The ListNode to be checked.\n * @returns The depth of the ListNode.\n */\nexport function $getListDepth(listNode: ListNode): number {\n  let depth = 1;\n  let parent = listNode.getParent();\n\n  while (parent != null) {\n    if ($isListItemNode(parent)) {\n      const parentList = parent.getParent();\n\n      if ($isListNode(parentList)) {\n        depth++;\n        parent = parentList.getParent();\n        continue;\n      }\n      invariant(false, 'A ListItemNode must have a ListNode for a parent.');\n    }\n\n    return depth;\n  }\n\n  return depth;\n}\n\n/**\n * Finds the nearest ancestral ListNode and returns it, throws an invariant if listItem is not a ListItemNode.\n * @param listItem - The node to be checked.\n * @returns The ListNode found.\n */\nexport function $getTopListNode(listItem: LexicalNode): ListNode {\n  let list = listItem.getParent<ListNode>();\n\n  if (!$isListNode(list)) {\n    invariant(false, 'A ListItemNode must have a ListNode for a parent.');\n  }\n\n  let parent: ListNode | null = list;\n\n  while (parent !== null) {\n    parent = parent.getParent();\n\n    if ($isListNode(parent)) {\n      list = parent;\n    }\n  }\n\n  return list;\n}\n\n/**\n * Checks if listItem has no child ListNodes and has no ListItemNode ancestors with siblings.\n * @param listItem - the ListItemNode to be checked.\n * @returns true if listItem has no child ListNode and no ListItemNode ancestors with siblings, false otherwise.\n */\nexport function $isLastItemInList(listItem: ListItemNode): boolean {\n  let isLast = true;\n  const firstChild = listItem.getFirstChild();\n\n  if ($isListNode(firstChild)) {\n    return false;\n  }\n  let parent: ListItemNode | null = listItem;\n\n  while (parent !== null) {\n    if ($isListItemNode(parent)) {\n      if (parent.getNextSiblings().length > 0) {\n        isLast = false;\n      }\n    }\n\n    parent = parent.getParent();\n  }\n\n  return isLast;\n}\n\n/**\n * A recursive Depth-First Search (Postorder Traversal) that finds all of a node's children\n * that are of type ListItemNode and returns them in an array.\n * @param node - The ListNode to start the search.\n * @returns An array containing all nodes of type ListItemNode found.\n */\n// This should probably be $getAllChildrenOfType\nexport function $getAllListItems(node: ListNode): Array<ListItemNode> {\n  let listItemNodes: Array<ListItemNode> = [];\n  const listChildren: Array<ListItemNode> = node\n    .getChildren()\n    .filter($isListItemNode);\n\n  for (let i = 0; i < listChildren.length; i++) {\n    const listItemNode = listChildren[i];\n    const firstChild = listItemNode.getFirstChild();\n\n    if ($isListNode(firstChild)) {\n      listItemNodes = listItemNodes.concat($getAllListItems(firstChild));\n    } else {\n      listItemNodes.push(listItemNode);\n    }\n  }\n\n  return listItemNodes;\n}\n\nconst NestedListNodeBrand: unique symbol = Symbol.for(\n  '@lexical/NestedListNodeBrand',\n);\n\n/**\n * Checks to see if the passed node is a ListItemNode and has a ListNode as a child.\n * @param node - The node to be checked.\n * @returns true if the node is a ListItemNode and has a ListNode child, false otherwise.\n */\nexport function isNestedListNode(\n  node: LexicalNode | null | undefined,\n): node is Spread<\n  {getFirstChild(): ListNode; [NestedListNodeBrand]: never},\n  ListItemNode\n> {\n  return $isListItemNode(node) && $isListNode(node.getFirstChild());\n}\n\n/**\n * Traverses up the tree and returns the first ListItemNode found.\n * @param node - Node to start the search.\n * @returns The first ListItemNode found, or null if none exist.\n */\nexport function $findNearestListItemNode(\n  node: LexicalNode,\n): ListItemNode | null {\n  const matchingParent = $findMatchingParent(node, (parent) =>\n    $isListItemNode(parent),\n  );\n  return matchingParent as ListItemNode | null;\n}\n\n/**\n * Takes a deeply nested ListNode or ListItemNode and traverses up the branch to delete the first\n * ancestral ListNode (which could be the root ListNode) or ListItemNode with siblings, essentially\n * bringing the deeply nested node up the branch once. Would remove sublist if it has siblings.\n * Should not break ListItem -> List -> ListItem chain as empty List/ItemNodes should be removed on .remove().\n * @param sublist - The nested ListNode or ListItemNode to be brought up the branch.\n */\nexport function $removeHighestEmptyListParent(\n  sublist: ListItemNode | ListNode,\n) {\n  // Nodes may be repeatedly indented, to create deeply nested lists that each\n  // contain just one bullet.\n  // Our goal is to remove these (empty) deeply nested lists. The easiest\n  // way to do that is crawl back up the tree until we find a node that has siblings\n  // (e.g. is actually part of the list contents) and delete that, or delete\n  // the root of the list (if no list nodes have siblings.)\n  let emptyListPtr = sublist;\n\n  while (\n    emptyListPtr.getNextSibling() == null &&\n    emptyListPtr.getPreviousSibling() == null\n  ) {\n    const parent = emptyListPtr.getParent<ListItemNode | ListNode>();\n\n    if (\n      parent == null ||\n      !($isListItemNode(emptyListPtr) || $isListNode(emptyListPtr))\n    ) {\n      break;\n    }\n\n    emptyListPtr = parent;\n  }\n\n  emptyListPtr.remove();\n}\n\n/**\n * Wraps a node into a ListItemNode.\n * @param node - The node to be wrapped into a ListItemNode\n * @returns The ListItemNode which the passed node is wrapped in.\n */\nexport function $wrapInListItem(node: LexicalNode): ListItemNode {\n  const listItemWrapper = $createListItemNode();\n  return listItemWrapper.append(node);\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/readme.md",
    "content": "# Lexical Editor Framework\n\nThis is a fork and import of [the Lexical editor](https://lexical.dev/) at the version of v0.17.1 for direct use and modification in BookStack. This was done due to fighting many of the opinionated defaults in Lexical during editor development.\n\nOnly components used, or intended to be used, were copied in at this point.\n\n#### License\n\nThe original work built upon in this directory and below is under the copyright of Meta Platforms, Inc. and affiliates.\nThe original license can be seen in the [ORIGINAL-LEXICAL-LICENSE](./ORIGINAL-LEXICAL-LICENSE) file.\n\nFiles may have since been added or modified with changes being under the license and copyright of the BookStack project as a whole. "
  },
  {
    "path": "resources/js/wysiwyg/lexical/rich-text/LexicalCalloutNode.ts",
    "content": "import {\n    $createParagraphNode,\n    DOMConversion,\n    DOMConversionMap, DOMConversionOutput,\n    ElementNode,\n    LexicalEditor,\n    LexicalNode,\n    ParagraphNode, Spread\n} from 'lexical';\nimport type {EditorConfig} from \"lexical/LexicalEditor\";\nimport type {RangeSelection} from \"lexical/LexicalSelection\";\nimport {\n    CommonBlockAlignment, commonPropertiesDifferent, deserializeCommonBlockNode,\n    setCommonBlockPropsFromElement,\n    updateElementWithCommonBlockProps\n} from \"lexical/nodes/common\";\nimport {SerializedCommonBlockNode} from \"lexical/nodes/CommonBlockNode\";\n\nexport type CalloutCategory = 'info' | 'danger' | 'warning' | 'success';\n\nexport type SerializedCalloutNode = Spread<{\n    category: CalloutCategory;\n}, SerializedCommonBlockNode>\n\nexport class CalloutNode extends ElementNode {\n    __id: string = '';\n    __category: CalloutCategory = 'info';\n    __alignment: CommonBlockAlignment = '';\n    __inset: number = 0;\n\n    static getType() {\n        return 'callout';\n    }\n\n    static clone(node: CalloutNode) {\n        const newNode = new CalloutNode(node.__category, node.__key);\n        newNode.__id = node.__id;\n        newNode.__alignment = node.__alignment;\n        newNode.__inset = node.__inset;\n        return newNode;\n    }\n\n    constructor(category: CalloutCategory, key?: string) {\n        super(key);\n        this.__category = category;\n    }\n\n    setCategory(category: CalloutCategory) {\n        const self = this.getWritable();\n        self.__category = category;\n    }\n\n    getCategory(): CalloutCategory {\n        const self = this.getLatest();\n        return self.__category;\n    }\n\n    setId(id: string) {\n        const self = this.getWritable();\n        self.__id = id;\n    }\n\n    getId(): string {\n        const self = this.getLatest();\n        return self.__id;\n    }\n\n    setAlignment(alignment: CommonBlockAlignment) {\n        const self = this.getWritable();\n        self.__alignment = alignment;\n    }\n\n    getAlignment(): CommonBlockAlignment {\n        const self = this.getLatest();\n        return self.__alignment;\n    }\n\n    setInset(size: number) {\n        const self = this.getWritable();\n        self.__inset = size;\n    }\n\n    getInset(): number {\n        const self = this.getLatest();\n        return self.__inset;\n    }\n\n    createDOM(_config: EditorConfig, _editor: LexicalEditor) {\n        const element = document.createElement('p');\n        element.classList.add('callout', this.__category || '');\n        updateElementWithCommonBlockProps(element, this);\n        return element;\n    }\n\n    updateDOM(prevNode: CalloutNode): boolean {\n        return prevNode.__category !== this.__category ||\n            commonPropertiesDifferent(prevNode, this);\n    }\n\n    insertNewAfter(selection: RangeSelection, restoreSelection?: boolean): CalloutNode|ParagraphNode {\n        const anchorOffset = selection ? selection.anchor.offset : 0;\n        const newElement = anchorOffset === this.getTextContentSize() || !selection\n            ? $createParagraphNode() : $createCalloutNode(this.__category);\n\n        newElement.setDirection(this.getDirection());\n        this.insertAfter(newElement, restoreSelection);\n\n        if (anchorOffset === 0 && !this.isEmpty() && selection) {\n            const paragraph = $createParagraphNode();\n            paragraph.select();\n            this.replace(paragraph, true);\n        }\n\n        return newElement;\n    }\n\n    static importDOM(): DOMConversionMap|null {\n        return {\n            p(node: HTMLElement): DOMConversion|null {\n                if (node.classList.contains('callout')) {\n                    return {\n                        conversion: (element: HTMLElement): DOMConversionOutput|null => {\n                            let category: CalloutCategory = 'info';\n                            const categories: CalloutCategory[] = ['info', 'success', 'warning', 'danger'];\n\n                            for (const c of categories) {\n                                if (element.classList.contains(c)) {\n                                    category = c;\n                                    break;\n                                }\n                            }\n\n                            const node = new CalloutNode(category);\n                            setCommonBlockPropsFromElement(element, node);\n\n                            return {\n                                node,\n                            };\n                        },\n                        priority: 3,\n                    };\n                }\n                return null;\n            },\n        };\n    }\n\n    exportJSON(): SerializedCalloutNode {\n        return {\n            ...super.exportJSON(),\n            type: 'callout',\n            version: 1,\n            category: this.__category,\n            id: this.__id,\n            alignment: this.__alignment,\n            inset: this.__inset,\n        };\n    }\n\n    static importJSON(serializedNode: SerializedCalloutNode): CalloutNode {\n        const node = $createCalloutNode(serializedNode.category);\n        deserializeCommonBlockNode(serializedNode, node);\n        return node;\n    }\n\n}\n\nexport function $createCalloutNode(category: CalloutCategory = 'info') {\n    return new CalloutNode(category);\n}\n\nexport function $isCalloutNode(node: LexicalNode | null | undefined): node is CalloutNode {\n    return node instanceof CalloutNode;\n}\n\nexport function $isCalloutNodeOfCategory(node: LexicalNode | null | undefined, category: CalloutCategory = 'info') {\n    return node instanceof CalloutNode && (node as CalloutNode).getCategory() === category;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/rich-text/LexicalCodeBlockNode.ts",
    "content": "import {\n    DecoratorNode,\n    DOMConversion,\n    DOMConversionMap,\n    DOMConversionOutput, DOMExportOutput,\n    LexicalEditor, LexicalNode,\n    SerializedLexicalNode,\n    Spread\n} from \"lexical\";\nimport type {EditorConfig} from \"lexical/LexicalEditor\";\nimport {EditorDecoratorAdapter} from \"../../ui/framework/decorator\";\nimport {CodeEditor} from \"../../../components\";\nimport {el} from \"../../utils/dom\";\n\nexport type SerializedCodeBlockNode = Spread<{\n    language: string;\n    id: string;\n    code: string;\n}, SerializedLexicalNode>\n\nconst getLanguageFromClassList = (classes: string) => {\n    const langClasses = classes.split(' ').filter(cssClass => cssClass.startsWith('language-'));\n    return (langClasses[0] || '').replace('language-', '');\n};\n\nexport class CodeBlockNode extends DecoratorNode<EditorDecoratorAdapter> {\n    __id: string = '';\n    __language: string = '';\n    __code: string = '';\n\n    static getType(): string {\n        return 'code-block';\n    }\n\n    static clone(node: CodeBlockNode): CodeBlockNode {\n        const newNode = new CodeBlockNode(node.__language, node.__code, node.__key);\n        newNode.__id = node.__id;\n        return newNode;\n    }\n\n    constructor(language: string = '', code: string = '', key?: string) {\n        super(key);\n        this.__language = language;\n        this.__code = code;\n    }\n\n    setLanguage(language: string): void {\n        const self = this.getWritable();\n        self.__language = language;\n    }\n\n    getLanguage(): string {\n        const self = this.getLatest();\n        return self.__language;\n    }\n\n    setCode(code: string): void {\n        const self = this.getWritable();\n        self.__code = code;\n    }\n\n    getCode(): string {\n        const self = this.getLatest();\n        return self.__code;\n    }\n\n    setId(id: string) {\n        const self = this.getWritable();\n        self.__id = id;\n    }\n\n    getId(): string {\n        const self = this.getLatest();\n        return self.__id;\n    }\n\n    decorate(editor: LexicalEditor, config: EditorConfig): EditorDecoratorAdapter {\n        return {\n            type: 'code',\n            getNode: () => this,\n        };\n    }\n\n    isInline(): boolean {\n        return false;\n    }\n\n    isIsolated() {\n        return true;\n    }\n\n    createDOM(_config: EditorConfig, _editor: LexicalEditor) {\n        const codeBlock = el('pre', {\n            id: this.__id || null,\n        }, [\n            el('code', {\n                class: this.__language ? `language-${this.__language}` : null,\n            }, [this.__code]),\n        ]);\n\n        return el('div', {class: 'editor-code-block-wrap'}, [codeBlock]);\n    }\n\n    updateDOM(prevNode: CodeBlockNode, dom: HTMLElement) {\n        const code = dom.querySelector('code');\n        if (!code) return false;\n\n        if (prevNode.__language !== this.__language) {\n            code.className = this.__language ? `language-${this.__language}` : '';\n        }\n\n        if (prevNode.__id !== this.__id) {\n            dom.setAttribute('id', this.__id);\n        }\n\n        if (prevNode.__code !== this.__code) {\n            code.textContent = this.__code;\n        }\n\n        return false;\n    }\n\n    exportDOM(editor: LexicalEditor): DOMExportOutput {\n        const dom = this.createDOM(editor._config, editor);\n        return {\n            element: dom.querySelector('pre') as HTMLElement,\n        };\n    }\n\n    static importDOM(): DOMConversionMap|null {\n        return {\n            pre(node: HTMLElement): DOMConversion|null {\n                return {\n                    conversion: (element: HTMLElement): DOMConversionOutput|null => {\n\n                        const codeEl = element.querySelector('code');\n                        const language = getLanguageFromClassList(element.className)\n                                        || (codeEl && getLanguageFromClassList(codeEl.className))\n                                        || '';\n\n                        const code = codeEl ? (codeEl.textContent || '').trim() : (element.textContent || '').trim();\n                        const node = $createCodeBlockNode(language, code);\n\n                        if (element.id) {\n                            node.setId(element.id);\n                        }\n\n                        return {\n                            node,\n                            after(childNodes): LexicalNode[] {\n                                // Remove any child nodes that may get parsed since we're manually\n                                // controlling the code contents.\n                                return [];\n                            },\n                        };\n                    },\n                    priority: 3,\n                };\n            },\n        };\n    }\n\n    exportJSON(): SerializedCodeBlockNode {\n        return {\n            type: 'code-block',\n            version: 1,\n            id: this.__id,\n            language: this.__language,\n            code: this.__code,\n        };\n    }\n\n    static importJSON(serializedNode: SerializedCodeBlockNode): CodeBlockNode {\n        const node = $createCodeBlockNode(serializedNode.language, serializedNode.code);\n        node.setId(serializedNode.id || '');\n        return node;\n    }\n}\n\nexport function $createCodeBlockNode(language: string = '', code: string = ''): CodeBlockNode {\n    return new CodeBlockNode(language, code);\n}\n\nexport function $isCodeBlockNode(node: LexicalNode | null | undefined) {\n    return node instanceof CodeBlockNode;\n}\n\nexport function $openCodeEditorForNode(editor: LexicalEditor, node: CodeBlockNode): void {\n    const code = node.getCode();\n    const language = node.getLanguage();\n\n    // @ts-ignore\n    const codeEditor = window.$components.first('code-editor') as CodeEditor;\n    // TODO - Handle direction\n    codeEditor.open(code, language, 'ltr', (newCode: string, newLang: string) => {\n        editor.update(() => {\n            node.setCode(newCode);\n            node.setLanguage(newLang);\n        });\n        // TODO - Re-focus\n    }, () => {\n        // TODO - Re-focus\n    });\n}"
  },
  {
    "path": "resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts",
    "content": "import {\n    DOMConversion,\n    DOMConversionMap, DOMConversionOutput,\n    ElementNode,\n    LexicalEditor,\n    LexicalNode,\n    SerializedElementNode, Spread,\n    EditorConfig, DOMExportOutput,\n} from 'lexical';\n\nimport {extractDirectionFromElement} from \"lexical/nodes/common\";\n\nexport type SerializedDetailsNode = Spread<{\n    id: string;\n    summary: string;\n}, SerializedElementNode>\n\nexport class DetailsNode extends ElementNode {\n    __id: string = '';\n    __summary: string = '';\n    __open: boolean = false;\n\n    static getType() {\n        return 'details';\n    }\n\n    setId(id: string) {\n        const self = this.getWritable();\n        self.__id = id;\n    }\n\n    getId(): string {\n        const self = this.getLatest();\n        return self.__id;\n    }\n\n    setSummary(summary: string) {\n        const self = this.getWritable();\n        self.__summary = summary;\n    }\n\n    getSummary(): string {\n        const self = this.getLatest();\n        return self.__summary;\n    }\n\n    setOpen(open: boolean) {\n        const self = this.getWritable();\n        self.__open = open;\n    }\n\n    getOpen(): boolean {\n        const self = this.getLatest();\n        return self.__open;\n    }\n\n    static clone(node: DetailsNode): DetailsNode {\n        const newNode =  new DetailsNode(node.__key);\n        newNode.__id = node.__id;\n        newNode.__dir = node.__dir;\n        newNode.__summary = node.__summary;\n        newNode.__open = node.__open;\n        return newNode;\n    }\n\n    createDOM(_config: EditorConfig, _editor: LexicalEditor) {\n        const el = document.createElement('details');\n        if (this.__id) {\n            el.setAttribute('id', this.__id);\n        }\n\n        if (this.__dir) {\n            el.setAttribute('dir', this.__dir);\n        }\n\n        if (this.__open) {\n            el.setAttribute('open', 'true');\n            el.removeAttribute('contenteditable');\n        } else {\n            el.setAttribute('contenteditable', 'false');\n        }\n\n        const summary = document.createElement('summary');\n        summary.textContent = this.__summary;\n        summary.setAttribute('contenteditable', 'false');\n        summary.addEventListener('click', event => {\n            event.preventDefault();\n            _editor.update(() => {\n                this.select();\n            });\n        });\n\n        el.append(summary);\n\n        return el;\n    }\n\n    updateDOM(prevNode: DetailsNode, dom: HTMLElement) {\n\n        if (prevNode.__open !== this.__open) {\n            dom.toggleAttribute('open', this.__open);\n            if (this.__open) {\n                dom.removeAttribute('contenteditable');\n            } else {\n                dom.setAttribute('contenteditable', 'false');\n            }\n        }\n\n        return prevNode.__id !== this.__id\n        || prevNode.__dir !== this.__dir\n        || prevNode.__summary !== this.__summary;\n    }\n\n    static importDOM(): DOMConversionMap|null {\n        return {\n            details(node: HTMLElement): DOMConversion|null {\n                return {\n                    conversion: (element: HTMLElement): DOMConversionOutput|null => {\n                        const node = new DetailsNode();\n                        if (element.id) {\n                            node.setId(element.id);\n                        }\n\n                        if (element.dir) {\n                            node.setDirection(extractDirectionFromElement(element));\n                        }\n\n                        const summaryElem = Array.from(element.children).find(e => e.nodeName === 'SUMMARY');\n                        node.setSummary(summaryElem?.textContent || '');\n\n                        return {node};\n                    },\n                    priority: 3,\n                };\n            },\n            summary(node: HTMLElement): DOMConversion|null {\n                return {\n                    conversion: (element: HTMLElement): DOMConversionOutput|null => {\n                        return {node: 'ignore'};\n                    },\n                    priority: 3,\n                };\n            },\n        };\n    }\n\n    exportDOM(editor: LexicalEditor): DOMExportOutput {\n        const element = this.createDOM(editor._config, editor);\n        const editable = element.querySelectorAll('[contenteditable]');\n        for (const elem of editable) {\n            elem.removeAttribute('contenteditable');\n        }\n\n        element.removeAttribute('open');\n        element.removeAttribute('contenteditable');\n\n        return {element};\n    }\n\n    exportJSON(): SerializedDetailsNode {\n        return {\n            ...super.exportJSON(),\n            type: 'details',\n            version: 1,\n            id: this.__id,\n            summary: this.__summary,\n        };\n    }\n\n    static importJSON(serializedNode: SerializedDetailsNode): DetailsNode {\n        const node = $createDetailsNode();\n        node.setId(serializedNode.id);\n        node.setDirection(serializedNode.direction);\n        return node;\n    }\n\n    shouldSelectDirectly(): boolean {\n        return true;\n    }\n\n    canBeEmpty(): boolean {\n        return false;\n    }\n\n}\n\nexport function $createDetailsNode() {\n    return new DetailsNode();\n}\n\nexport function $isDetailsNode(node: LexicalNode | null | undefined): node is DetailsNode {\n    return node instanceof DetailsNode;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/rich-text/LexicalDiagramNode.ts",
    "content": "import {\n    DecoratorNode,\n    DOMConversion,\n    DOMConversionMap,\n    DOMConversionOutput,\n    LexicalEditor,\n    SerializedLexicalNode,\n    Spread\n} from \"lexical\";\nimport type {EditorConfig} from \"lexical/LexicalEditor\";\nimport {EditorDecoratorAdapter} from \"../../ui/framework/decorator\";\nimport {el} from \"../../utils/dom\";\n\nexport type SerializedDiagramNode = Spread<{\n    id: string;\n    drawingId: string;\n    drawingUrl: string;\n}, SerializedLexicalNode>\n\nexport class DiagramNode extends DecoratorNode<EditorDecoratorAdapter> {\n    __id: string = '';\n    __drawingId: string = '';\n    __drawingUrl: string = '';\n\n    static getType(): string {\n        return 'diagram';\n    }\n\n    static clone(node: DiagramNode): DiagramNode {\n        const newNode = new DiagramNode(node.__drawingId, node.__drawingUrl);\n        newNode.__id = node.__id;\n        return newNode;\n    }\n\n    constructor(drawingId: string, drawingUrl: string, key?: string) {\n        super(key);\n        this.__drawingId = drawingId;\n        this.__drawingUrl = drawingUrl;\n    }\n\n    setDrawingIdAndUrl(drawingId: string, drawingUrl: string): void {\n        const self = this.getWritable();\n        self.__drawingUrl = drawingUrl;\n        self.__drawingId = drawingId;\n    }\n\n    getDrawingIdAndUrl(): { id: string, url: string } {\n        const self = this.getLatest();\n        return {\n            id: self.__drawingId,\n            url: self.__drawingUrl,\n        };\n    }\n\n    setId(id: string) {\n        const self = this.getWritable();\n        self.__id = id;\n    }\n\n    getId(): string {\n        const self = this.getLatest();\n        return self.__id;\n    }\n\n    decorate(editor: LexicalEditor, config: EditorConfig): EditorDecoratorAdapter {\n        return {\n            type: 'diagram',\n            getNode: () => this,\n        };\n    }\n\n    isInline(): boolean {\n        return false;\n    }\n\n    isIsolated() {\n        return true;\n    }\n\n    createDOM(_config: EditorConfig, _editor: LexicalEditor) {\n        return el('div', {\n            id: this.__id || null,\n            'drawio-diagram': this.__drawingId,\n        }, [\n            el('img', {src: this.__drawingUrl}),\n        ]);\n    }\n\n    updateDOM(prevNode: DiagramNode, dom: HTMLElement) {\n        const img = dom.querySelector('img');\n        if (!img) return false;\n\n        if (prevNode.__id !== this.__id) {\n            dom.setAttribute('id', this.__id);\n        }\n\n        if (prevNode.__drawingUrl !== this.__drawingUrl) {\n            img.setAttribute('src', this.__drawingUrl);\n        }\n\n        if (prevNode.__drawingId !== this.__drawingId) {\n            dom.setAttribute('drawio-diagram', this.__drawingId);\n        }\n\n        return false;\n    }\n\n    static importDOM(): DOMConversionMap | null {\n        return {\n            div(node: HTMLElement): DOMConversion | null {\n\n                if (!node.hasAttribute('drawio-diagram')) {\n                    return null;\n                }\n\n                return {\n                    conversion: (element: HTMLElement): DOMConversionOutput | null => {\n\n                        const img = element.querySelector('img');\n                        const drawingUrl = img?.getAttribute('src') || '';\n                        const drawingId = element.getAttribute('drawio-diagram') || '';\n                        const node = $createDiagramNode(drawingId, drawingUrl);\n\n                        if (element.id) {\n                            node.setId(element.id);\n                        }\n\n                        return { node };\n                    },\n                    priority: 3,\n                };\n            },\n        };\n    }\n\n    exportJSON(): SerializedDiagramNode {\n        return {\n            type: 'diagram',\n            version: 1,\n            id: this.__id,\n            drawingId: this.__drawingId,\n            drawingUrl: this.__drawingUrl,\n        };\n    }\n\n    static importJSON(serializedNode: SerializedDiagramNode): DiagramNode {\n        const node = $createDiagramNode(serializedNode.drawingId, serializedNode.drawingUrl);\n        node.setId(serializedNode.id || '');\n        return node;\n    }\n}\n\nexport function $createDiagramNode(drawingId: string = '', drawingUrl: string = ''): DiagramNode {\n    return new DiagramNode(drawingId, drawingUrl);\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/rich-text/LexicalHeadingNode.ts",
    "content": "import {\n    $applyNodeReplacement,\n    $createParagraphNode,\n    type DOMConversionMap,\n    DOMConversionOutput,\n    type DOMExportOutput,\n    type EditorConfig,\n    isHTMLElement,\n    type LexicalEditor,\n    type LexicalNode,\n    type NodeKey,\n    type ParagraphNode,\n    type RangeSelection,\n    type Spread\n} from \"lexical\";\nimport {addClassNamesToElement} from \"@lexical/utils\";\nimport {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} from \"lexical/nodes/CommonBlockNode\";\nimport {\n    commonPropertiesDifferent, deserializeCommonBlockNode,\n    setCommonBlockPropsFromElement,\n    updateElementWithCommonBlockProps\n} from \"lexical/nodes/common\";\n\nexport type HeadingTagType = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';\n\nexport type SerializedHeadingNode = Spread<\n    {\n        tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';\n    },\n    SerializedCommonBlockNode\n>;\n\n/** @noInheritDoc */\nexport class HeadingNode extends CommonBlockNode {\n    /** @internal */\n    __tag: HeadingTagType;\n\n    static getType(): string {\n        return 'heading';\n    }\n\n    static clone(node: HeadingNode): HeadingNode {\n        const clone = new HeadingNode(node.__tag, node.__key);\n        copyCommonBlockProperties(node, clone);\n        return clone;\n    }\n\n    constructor(tag: HeadingTagType, key?: NodeKey) {\n        super(key);\n        this.__tag = tag;\n    }\n\n    getTag(): HeadingTagType {\n        return this.__tag;\n    }\n\n    // View\n\n    createDOM(config: EditorConfig): HTMLElement {\n        const tag = this.__tag;\n        const element = document.createElement(tag);\n        const theme = config.theme;\n        const classNames = theme.heading;\n        if (classNames !== undefined) {\n            const className = classNames[tag];\n            addClassNamesToElement(element, className);\n        }\n        updateElementWithCommonBlockProps(element, this);\n        return element;\n    }\n\n    updateDOM(prevNode: HeadingNode, dom: HTMLElement): boolean {\n        return commonPropertiesDifferent(prevNode, this);\n    }\n\n    static importDOM(): DOMConversionMap | null {\n        return {\n            h1: (node: Node) => ({\n                conversion: $convertHeadingElement,\n                priority: 0,\n            }),\n            h2: (node: Node) => ({\n                conversion: $convertHeadingElement,\n                priority: 0,\n            }),\n            h3: (node: Node) => ({\n                conversion: $convertHeadingElement,\n                priority: 0,\n            }),\n            h4: (node: Node) => ({\n                conversion: $convertHeadingElement,\n                priority: 0,\n            }),\n            h5: (node: Node) => ({\n                conversion: $convertHeadingElement,\n                priority: 0,\n            }),\n            h6: (node: Node) => ({\n                conversion: $convertHeadingElement,\n                priority: 0,\n            }),\n        };\n    }\n\n    exportDOM(editor: LexicalEditor): DOMExportOutput {\n        const {element} = super.exportDOM(editor);\n\n        if (element && isHTMLElement(element)) {\n            if (this.isEmpty()) {\n                element.append(document.createElement('br'));\n            }\n        }\n\n        return {\n            element,\n        };\n    }\n\n    static importJSON(serializedNode: SerializedHeadingNode): HeadingNode {\n        const node = $createHeadingNode(serializedNode.tag);\n        deserializeCommonBlockNode(serializedNode, node);\n        return node;\n    }\n\n    exportJSON(): SerializedHeadingNode {\n        return {\n            ...super.exportJSON(),\n            tag: this.getTag(),\n            type: 'heading',\n            version: 1,\n        };\n    }\n\n    // Mutation\n    insertNewAfter(\n        selection?: RangeSelection,\n        restoreSelection = true,\n    ): ParagraphNode | HeadingNode {\n        const anchorOffet = selection ? selection.anchor.offset : 0;\n        const lastDesc = this.getLastDescendant();\n        const isAtEnd =\n            !lastDesc ||\n            (selection &&\n                selection.anchor.key === lastDesc.getKey() &&\n                anchorOffet === lastDesc.getTextContentSize());\n        const newElement =\n            isAtEnd || !selection\n                ? $createParagraphNode()\n                : $createHeadingNode(this.getTag());\n        const direction = this.getDirection();\n        newElement.setDirection(direction);\n        this.insertAfter(newElement, restoreSelection);\n        if (anchorOffet === 0 && !this.isEmpty() && selection) {\n            const paragraph = $createParagraphNode();\n            paragraph.select();\n            this.replace(paragraph, true);\n        }\n        return newElement;\n    }\n\n    collapseAtStart(): true {\n        const newElement = !this.isEmpty()\n            ? $createHeadingNode(this.getTag())\n            : $createParagraphNode();\n        const children = this.getChildren();\n        children.forEach((child) => newElement.append(child));\n        this.replace(newElement);\n        return true;\n    }\n\n    extractWithChild(): boolean {\n        return true;\n    }\n}\n\nfunction $convertHeadingElement(element: HTMLElement): DOMConversionOutput {\n    const nodeName = element.nodeName.toLowerCase();\n    let node = null;\n    if (\n        nodeName === 'h1' ||\n        nodeName === 'h2' ||\n        nodeName === 'h3' ||\n        nodeName === 'h4' ||\n        nodeName === 'h5' ||\n        nodeName === 'h6'\n    ) {\n        node = $createHeadingNode(nodeName);\n        setCommonBlockPropsFromElement(element, node);\n    }\n    return {node};\n}\n\nexport function $createHeadingNode(headingTag: HeadingTagType): HeadingNode {\n    return $applyNodeReplacement(new HeadingNode(headingTag));\n}\n\nexport function $isHeadingNode(\n    node: LexicalNode | null | undefined,\n): node is HeadingNode {\n    return node instanceof HeadingNode;\n}"
  },
  {
    "path": "resources/js/wysiwyg/lexical/rich-text/LexicalHorizontalRuleNode.ts",
    "content": "import {\n    DOMConversion,\n    DOMConversionMap, DOMConversionOutput,\n    ElementNode,\n    LexicalEditor,\n    LexicalNode,\n    SerializedElementNode, Spread,\n} from 'lexical';\nimport type {EditorConfig} from \"lexical/LexicalEditor\";\n\nexport type SerializedHorizontalRuleNode = Spread<{\n    id: string;\n}, SerializedElementNode>\n\nexport class HorizontalRuleNode extends ElementNode {\n    __id: string = '';\n\n    static getType() {\n        return 'horizontal-rule';\n    }\n\n    setId(id: string) {\n        const self = this.getWritable();\n        self.__id = id;\n    }\n\n    getId(): string {\n        const self = this.getLatest();\n        return self.__id;\n    }\n\n    static clone(node: HorizontalRuleNode): HorizontalRuleNode {\n        const newNode = new HorizontalRuleNode(node.__key);\n        newNode.__id = node.__id;\n        return newNode;\n    }\n\n    createDOM(_config: EditorConfig, _editor: LexicalEditor): HTMLElement {\n        const el = document.createElement('hr');\n        if (this.__id) {\n            el.setAttribute('id', this.__id);\n        }\n\n        return el;\n    }\n\n    updateDOM(prevNode: HorizontalRuleNode, dom: HTMLElement) {\n        return prevNode.__id !== this.__id;\n    }\n\n    static importDOM(): DOMConversionMap|null {\n        return {\n            hr(node: HTMLElement): DOMConversion|null {\n                return {\n                    conversion: (element: HTMLElement): DOMConversionOutput|null => {\n                        const node = new HorizontalRuleNode();\n                        if (element.id) {\n                            node.setId(element.id);\n                        }\n\n                        return {node};\n                    },\n                    priority: 3,\n                };\n            },\n        };\n    }\n\n    exportJSON(): SerializedHorizontalRuleNode {\n        return {\n            ...super.exportJSON(),\n            type: 'horizontal-rule',\n            version: 1,\n            id: this.__id,\n        };\n    }\n\n    static importJSON(serializedNode: SerializedHorizontalRuleNode): HorizontalRuleNode {\n        const node = $createHorizontalRuleNode();\n        node.setId(serializedNode.id);\n        return node;\n    }\n\n}\n\nexport function $createHorizontalRuleNode(): HorizontalRuleNode {\n    return new HorizontalRuleNode();\n}\n\nexport function $isHorizontalRuleNode(node: LexicalNode | null | undefined): node is HorizontalRuleNode {\n    return node instanceof HorizontalRuleNode;\n}"
  },
  {
    "path": "resources/js/wysiwyg/lexical/rich-text/LexicalImageNode.ts",
    "content": "import {\n    DOMConversion,\n    DOMConversionMap,\n    DOMConversionOutput, ElementNode,\n    LexicalEditor, LexicalNode,\n    Spread\n} from \"lexical\";\nimport type {EditorConfig} from \"lexical/LexicalEditor\";\nimport {CommonBlockAlignment, extractAlignmentFromElement} from \"lexical/nodes/common\";\nimport {$selectSingleNode} from \"../../utils/selection\";\nimport {SerializedElementNode} from \"lexical/nodes/LexicalElementNode\";\n\nexport interface ImageNodeOptions {\n    alt?: string;\n    width?: number;\n    height?: number;\n}\n\nexport type SerializedImageNode = Spread<{\n    src: string;\n    alt: string;\n    width: number;\n    height: number;\n    alignment: CommonBlockAlignment;\n}, SerializedElementNode>\n\nexport class ImageNode extends ElementNode {\n    __src: string = '';\n    __alt: string = '';\n    __width: number = 0;\n    __height: number = 0;\n    __alignment: CommonBlockAlignment = '';\n\n    static getType(): string {\n        return 'image';\n    }\n\n    static clone(node: ImageNode): ImageNode {\n        const newNode = new ImageNode(node.__src, {\n            alt: node.__alt,\n            width: node.__width,\n            height: node.__height,\n        }, node.__key);\n        newNode.__alignment = node.__alignment;\n        return newNode;\n    }\n\n    constructor(src: string, options: ImageNodeOptions, key?: string) {\n        super(key);\n        this.__src = src;\n        if (options.alt) {\n            this.__alt = options.alt;\n        }\n        if (options.width) {\n            this.__width = options.width;\n        }\n        if (options.height) {\n            this.__height = options.height;\n        }\n    }\n\n    setSrc(src: string): void {\n        const self = this.getWritable();\n        self.__src = src;\n    }\n\n    getSrc(): string {\n        const self = this.getLatest();\n        return self.__src;\n    }\n\n    setAltText(altText: string): void {\n        const self = this.getWritable();\n        self.__alt = altText;\n    }\n\n    getAltText(): string {\n        const self = this.getLatest();\n        return self.__alt;\n    }\n\n    setHeight(height: number): void {\n        const self = this.getWritable();\n        self.__height = height;\n    }\n\n    getHeight(): number {\n        const self = this.getLatest();\n        return self.__height;\n    }\n\n    setWidth(width: number): void {\n        const self = this.getWritable();\n        self.__width = width;\n    }\n\n    getWidth(): number {\n        const self = this.getLatest();\n        return self.__width;\n    }\n\n    setAlignment(alignment: CommonBlockAlignment) {\n        const self = this.getWritable();\n        self.__alignment = alignment;\n    }\n\n    getAlignment(): CommonBlockAlignment {\n        const self = this.getLatest();\n        return self.__alignment;\n    }\n\n    isInline(): boolean {\n        return true;\n    }\n\n    createDOM(_config: EditorConfig, _editor: LexicalEditor) {\n        const element = document.createElement('img');\n        element.setAttribute('src', this.__src);\n\n        if (this.__width) {\n            element.setAttribute('width', String(this.__width));\n        }\n        if (this.__height) {\n            element.setAttribute('height', String(this.__height));\n        }\n        if (this.__alt) {\n            element.setAttribute('alt', this.__alt);\n        }\n\n        if (this.__alignment) {\n            element.classList.add('align-' + this.__alignment);\n        }\n\n        element.addEventListener('click', e => {\n            _editor.update(() => {\n                this.select();\n            });\n        });\n\n        return element;\n    }\n\n    updateDOM(prevNode: ImageNode, dom: HTMLElement) {\n        if (prevNode.__src !== this.__src) {\n            dom.setAttribute('src', this.__src);\n        }\n\n        if (prevNode.__width !== this.__width) {\n            if (this.__width) {\n                dom.setAttribute('width', String(this.__width));\n            } else {\n                dom.removeAttribute('width');\n            }\n        }\n\n        if (prevNode.__height !== this.__height) {\n            if (this.__height) {\n                dom.setAttribute('height', String(this.__height));\n            } else {\n                dom.removeAttribute('height');\n            }\n        }\n\n        if (prevNode.__alt !== this.__alt) {\n            if (this.__alt) {\n                dom.setAttribute('alt', String(this.__alt));\n            } else {\n                dom.removeAttribute('alt');\n            }\n        }\n\n        if (prevNode.__alignment !== this.__alignment) {\n            if (prevNode.__alignment) {\n                dom.classList.remove('align-' + prevNode.__alignment);\n            }\n            if (this.__alignment) {\n                dom.classList.add('align-' + this.__alignment);\n            }\n        }\n\n        return false;\n    }\n\n    static importDOM(): DOMConversionMap|null {\n        return {\n            img(node: HTMLElement): DOMConversion|null {\n                return {\n                    conversion: (element: HTMLElement): DOMConversionOutput|null => {\n\n                        const src = element.getAttribute('src') || '';\n                        const options: ImageNodeOptions = {\n                            alt: element.getAttribute('alt') || '',\n                            height: Number.parseInt(element.getAttribute('height') || '0'),\n                            width: Number.parseInt(element.getAttribute('width') || '0'),\n                        }\n\n                        const node = new ImageNode(src, options);\n                        node.setAlignment(extractAlignmentFromElement(element));\n\n                        return { node };\n                    },\n                    priority: 3,\n                };\n            },\n        };\n    }\n\n    exportJSON(): SerializedImageNode {\n        return {\n            ...super.exportJSON(),\n            type: 'image',\n            version: 1,\n            src: this.__src,\n            alt: this.__alt,\n            height: this.__height,\n            width: this.__width,\n            alignment: this.__alignment,\n        };\n    }\n\n    static importJSON(serializedNode: SerializedImageNode): ImageNode {\n        const node = $createImageNode(serializedNode.src, {\n            alt: serializedNode.alt,\n            width: serializedNode.width,\n            height: serializedNode.height,\n        });\n        node.setAlignment(serializedNode.alignment);\n        return node;\n    }\n}\n\nexport function $createImageNode(src: string, options: ImageNodeOptions = {}): ImageNode {\n    return new ImageNode(src, options);\n}\n\nexport function $isImageNode(node: LexicalNode | null | undefined) {\n    return node instanceof ImageNode;\n}"
  },
  {
    "path": "resources/js/wysiwyg/lexical/rich-text/LexicalMediaNode.ts",
    "content": "import {\n    DOMConversion,\n    DOMConversionMap, DOMConversionOutput, DOMExportOutput,\n    ElementNode,\n    LexicalEditor,\n    LexicalNode,\n    Spread\n} from 'lexical';\nimport type {EditorConfig} from \"lexical/LexicalEditor\";\n\nimport {el, setOrRemoveAttribute, sizeToPixels, styleMapToStyleString, styleStringToStyleMap} from \"../../utils/dom\";\nimport {\n    CommonBlockAlignment, deserializeCommonBlockNode,\n    setCommonBlockPropsFromElement,\n    updateElementWithCommonBlockProps\n} from \"lexical/nodes/common\";\nimport {SerializedCommonBlockNode} from \"lexical/nodes/CommonBlockNode\";\n\nexport type MediaNodeTag = 'iframe' | 'embed' | 'object' | 'video' | 'audio';\nexport type MediaNodeSource = {\n    src: string;\n    type: string;\n};\n\nexport type SerializedMediaNode = Spread<{\n    tag: MediaNodeTag;\n    attributes: Record<string, string>;\n    sources: MediaNodeSource[];\n}, SerializedCommonBlockNode>\n\nconst attributeAllowList = [\n    'width', 'height', 'style', 'title', 'name',\n    'src', 'allow', 'allowfullscreen', 'loading', 'sandbox',\n    'type', 'data', 'controls', 'autoplay', 'controlslist', 'loop',\n    'muted', 'playsinline', 'poster', 'preload'\n];\n\nfunction filterAttributes(attributes: Record<string, string>): Record<string, string> {\n    const filtered: Record<string, string> = {};\n    for (const key of Object.keys(attributes)) {\n        if (attributeAllowList.includes(key)) {\n            filtered[key] = attributes[key];\n        }\n    }\n    return filtered;\n}\n\nfunction removeStyleFromAttributes(attributes: Record<string, string>, styleName: string): Record<string, string> {\n    const attrCopy = Object.assign({}, attributes);\n    if (!attributes.style) {\n        return attrCopy;\n    }\n\n    const map = styleStringToStyleMap(attributes.style);\n    map.delete(styleName);\n\n    attrCopy.style = styleMapToStyleString(map);\n    return attrCopy;\n}\n\nfunction domElementToNode(tag: MediaNodeTag, element: HTMLElement): MediaNode {\n    const node = $createMediaNode(tag);\n\n    const attributes: Record<string, string> = {};\n    for (const attribute of element.attributes) {\n        attributes[attribute.name] = attribute.value;\n    }\n    node.setAttributes(attributes);\n\n    const sources: MediaNodeSource[] = [];\n    if (tag === 'video' || tag === 'audio') {\n        for (const child of element.children) {\n            if (child.tagName === 'SOURCE') {\n                const src = child.getAttribute('src');\n                const type = child.getAttribute('type');\n                if (src && type) {\n                    sources.push({ src, type });\n                }\n            }\n        }\n        node.setSources(sources);\n    }\n\n    setCommonBlockPropsFromElement(element, node);\n\n    return node;\n}\n\nexport class MediaNode extends ElementNode {\n    __id: string = '';\n    __alignment: CommonBlockAlignment = '';\n    __tag: MediaNodeTag;\n    __attributes: Record<string, string> = {};\n    __sources: MediaNodeSource[] = [];\n    __inset: number = 0;\n\n    static getType() {\n        return 'media';\n    }\n\n    static clone(node: MediaNode) {\n        const newNode = new MediaNode(node.__tag, node.__key);\n        newNode.__attributes = Object.assign({}, node.__attributes);\n        newNode.__sources = node.__sources.map(s => Object.assign({}, s));\n        newNode.__id = node.__id;\n        newNode.__alignment = node.__alignment;\n        newNode.__inset = node.__inset;\n        return newNode;\n    }\n\n    constructor(tag: MediaNodeTag, key?: string) {\n        super(key);\n        this.__tag = tag;\n    }\n\n    setTag(tag: MediaNodeTag) {\n        const self = this.getWritable();\n        self.__tag = tag;\n    }\n\n    getTag(): MediaNodeTag {\n        const self = this.getLatest();\n        return self.__tag;\n    }\n\n    setAttributes(attributes: Record<string, string>) {\n        const self = this.getWritable();\n        self.__attributes = filterAttributes(attributes);\n    }\n\n    getAttributes(): Record<string, string> {\n        const self = this.getLatest();\n        return Object.assign({}, self.__attributes);\n    }\n\n    setSources(sources: MediaNodeSource[]) {\n        const self = this.getWritable();\n        self.__sources = sources;\n    }\n\n    getSources(): MediaNodeSource[] {\n        const self = this.getLatest();\n        return self.__sources.map(s => Object.assign({}, s))\n    }\n\n    setSrc(src: string): void {\n        const attrs = this.getAttributes();\n        const sources = this.getSources();\n\n        if (this.__tag ==='object') {\n            attrs.data = src;\n        } if (this.__tag === 'video' && sources.length > 0) {\n            sources[0].src = src;\n            delete attrs.src;\n            if (sources.length > 1) {\n                sources.splice(1, sources.length - 1);\n            }\n            this.setSources(sources);\n        } else {\n            attrs.src = src;\n        }\n\n        this.setAttributes(attrs);\n    }\n\n    setWidthAndHeight(width: string, height: string): void {\n        let attrs: Record<string, string> = Object.assign(\n            this.getAttributes(),\n            {width, height},\n        );\n\n        attrs = removeStyleFromAttributes(attrs, 'width');\n        attrs = removeStyleFromAttributes(attrs, 'height');\n        this.setAttributes(attrs);\n    }\n\n    setId(id: string) {\n        const self = this.getWritable();\n        self.__id = id;\n    }\n\n    getId(): string {\n        const self = this.getLatest();\n        return self.__id;\n    }\n\n    setAlignment(alignment: CommonBlockAlignment) {\n        const self = this.getWritable();\n        self.__alignment = alignment;\n    }\n\n    getAlignment(): CommonBlockAlignment {\n        const self = this.getLatest();\n        return self.__alignment;\n    }\n\n    setInset(size: number) {\n        const self = this.getWritable();\n        self.__inset = size;\n    }\n\n    getInset(): number {\n        const self = this.getLatest();\n        return self.__inset;\n    }\n\n    setHeight(height: number): void {\n        if (!height) {\n            return;\n        }\n\n        const attrs = Object.assign(this.getAttributes(), {height});\n        this.setAttributes(removeStyleFromAttributes(attrs, 'height'));\n    }\n\n    getHeight(): number {\n        const self = this.getLatest();\n        return sizeToPixels(self.__attributes.height || '0');\n    }\n\n    setWidth(width: number): void {\n        const existingAttrs = this.getAttributes();\n        const attrs: Record<string, string> = Object.assign(existingAttrs, {width});\n        this.setAttributes(removeStyleFromAttributes(attrs, 'width'));\n    }\n\n    getWidth(): number {\n        const self = this.getLatest();\n        return sizeToPixels(self.__attributes.width || '0');\n    }\n\n    isInline(): boolean {\n        return true;\n    }\n\n    isParentRequired(): boolean {\n        return true;\n    }\n\n    createInnerDOM() {\n        const sources = (this.__tag === 'video' || this.__tag === 'audio') ? this.__sources : [];\n        const sourceEls = sources.map(source => el('source', source));\n        const element = el(this.__tag, this.__attributes, sourceEls);\n        updateElementWithCommonBlockProps(element, this);\n        return element;\n    }\n\n    createDOM(_config: EditorConfig, _editor: LexicalEditor) {\n        const media = this.createInnerDOM();\n        return el('span', {\n            class: media.className + ' editor-media-wrap',\n        }, [media]);\n    }\n\n    updateDOM(prevNode: MediaNode, dom: HTMLElement): boolean {\n        if (prevNode.__tag !== this.__tag) {\n            return true;\n        }\n\n        if (JSON.stringify(prevNode.__sources) !== JSON.stringify(this.__sources)) {\n            return true;\n        }\n\n        if (JSON.stringify(prevNode.__attributes) !== JSON.stringify(this.__attributes)) {\n            return true;\n        }\n\n        const mediaEl = dom.firstElementChild as HTMLElement;\n\n        if (prevNode.__id !== this.__id) {\n            setOrRemoveAttribute(mediaEl, 'id', this.__id);\n        }\n\n        if (prevNode.__alignment !== this.__alignment) {\n            if (prevNode.__alignment) {\n                dom.classList.remove(`align-${prevNode.__alignment}`);\n                mediaEl.classList.remove(`align-${prevNode.__alignment}`);\n            }\n            if (this.__alignment) {\n                dom.classList.add(`align-${this.__alignment}`);\n                mediaEl.classList.add(`align-${this.__alignment}`);\n            }\n        }\n\n        if (prevNode.__inset !== this.__inset) {\n            dom.style.paddingLeft = `${this.__inset}px`;\n        }\n\n        return false;\n    }\n\n    static importDOM(): DOMConversionMap|null {\n\n        const buildConverter = (tag: MediaNodeTag) => {\n            return (node: HTMLElement): DOMConversion|null => {\n                return {\n                    conversion: (element: HTMLElement): DOMConversionOutput|null => {\n                        return {\n                            node: domElementToNode(tag, element),\n                        };\n                    },\n                    priority: 3,\n                };\n            };\n        };\n\n        return {\n            iframe: buildConverter('iframe'),\n            embed: buildConverter('embed'),\n            object: buildConverter('object'),\n            video: buildConverter('video'),\n            audio: buildConverter('audio'),\n        };\n    }\n\n    exportDOM(editor: LexicalEditor): DOMExportOutput {\n        const element = this.createInnerDOM();\n        return { element };\n    }\n\n    exportJSON(): SerializedMediaNode {\n        return {\n            ...super.exportJSON(),\n            type: 'media',\n            version: 1,\n            id: this.__id,\n            alignment: this.__alignment,\n            inset: this.__inset,\n            tag: this.__tag,\n            attributes: this.__attributes,\n            sources: this.__sources,\n        };\n    }\n\n    static importJSON(serializedNode: SerializedMediaNode): MediaNode {\n        const node = $createMediaNode(serializedNode.tag);\n        deserializeCommonBlockNode(serializedNode, node);\n        return node;\n    }\n\n}\n\nexport function $createMediaNode(tag: MediaNodeTag) {\n    return new MediaNode(tag);\n}\n\nexport function $createMediaNodeFromHtml(html: string): MediaNode | null {\n    const parser = new DOMParser();\n    const doc = parser.parseFromString(`<body>${html}</body>`, 'text/html');\n\n    const el = doc.body.children[0];\n    if (!(el instanceof HTMLElement)) {\n        return null;\n    }\n\n    const tag = el.tagName.toLowerCase();\n    const validTypes = ['embed', 'iframe', 'video', 'audio', 'object'];\n    if (!validTypes.includes(tag)) {\n        return null;\n    }\n\n    return domElementToNode(tag as MediaNodeTag, el);\n}\n\ninterface UrlPattern {\n    readonly regex: RegExp;\n    readonly w: number;\n    readonly h: number;\n    readonly url: string;\n}\n\n/**\n * These patterns originate from the tinymce/tinymce project.\n * https://github.com/tinymce/tinymce/blob/release/6.6/modules/tinymce/src/plugins/media/main/ts/core/UrlPatterns.ts\n * License: MIT Copyright (c) 2022 Ephox Corporation DBA Tiny Technologies, Inc.\n * License Link: https://github.com/tinymce/tinymce/blob/584a150679669859a528828e5d2910a083b1d911/LICENSE.TXT\n */\nconst urlPatterns: UrlPattern[] = [\n    {\n        regex: /.*?youtu\\.be\\/([\\w\\-_\\?&=.]+)/i,\n        w: 560, h: 314,\n        url: 'https://www.youtube.com/embed/$1',\n    },\n    {\n        regex: /.*youtube\\.com(.+)v=([^&]+)(&([a-z0-9&=\\-_]+))?.*/i,\n        w: 560, h: 314,\n        url: 'https://www.youtube.com/embed/$2?$4',\n    },\n    {\n        regex: /.*youtube.com\\/embed\\/([a-z0-9\\?&=\\-_]+).*/i,\n        w: 560, h: 314,\n        url: 'https://www.youtube.com/embed/$1',\n    },\n];\n\nconst videoExtensions = ['mp4', 'mpeg', 'm4v', 'm4p', 'mov'];\nconst audioExtensions = ['3gp', 'aac', 'flac', 'mp3', 'm4a', 'ogg', 'wav', 'webm'];\nconst iframeExtensions = ['html', 'htm', 'php', 'asp', 'aspx', ''];\n\nexport function $createMediaNodeFromSrc(src: string): MediaNode {\n\n    for (const pattern of urlPatterns) {\n        const match = src.match(pattern.regex);\n        if (match) {\n            const newSrc = src.replace(pattern.regex, pattern.url);\n            const node = new MediaNode('iframe');\n            node.setSrc(newSrc);\n            node.setHeight(pattern.h);\n            node.setWidth(pattern.w);\n            return node;\n        }\n    }\n\n    let nodeTag: MediaNodeTag = 'iframe';\n    const srcEnd = src.split('?')[0].split('/').pop() || '';\n    const srcEndSplit = srcEnd.split('.');\n    const extension = (srcEndSplit.length > 1 ? srcEndSplit[srcEndSplit.length - 1] : '').toLowerCase();\n    if (videoExtensions.includes(extension)) {\n        nodeTag = 'video';\n    } else if (audioExtensions.includes(extension)) {\n        nodeTag = 'audio';\n    } else if (extension && !iframeExtensions.includes(extension)) {\n        nodeTag = 'embed';\n    }\n\n    const node = new MediaNode(nodeTag);\n    node.setSrc(src);\n    return node;\n}\n\nexport function $isMediaNode(node: LexicalNode | null | undefined): node is MediaNode {\n    return node instanceof MediaNode;\n}\n\nexport function $isMediaNodeOfTag(node: LexicalNode | null | undefined, tag: MediaNodeTag): boolean {\n    return node instanceof MediaNode && (node as MediaNode).getTag() === tag;\n}"
  },
  {
    "path": "resources/js/wysiwyg/lexical/rich-text/LexicalQuoteNode.ts",
    "content": "import {\n    $applyNodeReplacement,\n    $createParagraphNode,\n    type DOMConversionMap,\n    type DOMConversionOutput,\n    type DOMExportOutput,\n    type EditorConfig,\n    isHTMLElement,\n    type LexicalEditor,\n    LexicalNode,\n    type NodeKey,\n    type ParagraphNode,\n    type RangeSelection\n} from \"lexical\";\nimport {addClassNamesToElement} from \"@lexical/utils\";\nimport {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} from \"lexical/nodes/CommonBlockNode\";\nimport {\n    commonPropertiesDifferent, deserializeCommonBlockNode,\n    setCommonBlockPropsFromElement,\n    updateElementWithCommonBlockProps\n} from \"lexical/nodes/common\";\n\nexport type SerializedQuoteNode = SerializedCommonBlockNode;\n\n/** @noInheritDoc */\nexport class QuoteNode extends CommonBlockNode {\n    static getType(): string {\n        return 'quote';\n    }\n\n    static clone(node: QuoteNode): QuoteNode {\n        const clone = new QuoteNode(node.__key);\n        copyCommonBlockProperties(node, clone);\n        return clone;\n    }\n\n    constructor(key?: NodeKey) {\n        super(key);\n    }\n\n    // View\n\n    createDOM(config: EditorConfig): HTMLElement {\n        const element = document.createElement('blockquote');\n        addClassNamesToElement(element, config.theme.quote);\n        updateElementWithCommonBlockProps(element, this);\n        return element;\n    }\n\n    updateDOM(prevNode: QuoteNode, dom: HTMLElement): boolean {\n        return commonPropertiesDifferent(prevNode, this);\n    }\n\n    static importDOM(): DOMConversionMap | null {\n        return {\n            blockquote: (node: Node) => ({\n                conversion: $convertBlockquoteElement,\n                priority: 0,\n            }),\n        };\n    }\n\n    exportDOM(editor: LexicalEditor): DOMExportOutput {\n        const {element} = super.exportDOM(editor);\n\n        if (element && isHTMLElement(element)) {\n            if (this.isEmpty()) {\n                element.append(document.createElement('br'));\n            }\n        }\n\n        return {\n            element,\n        };\n    }\n\n    static importJSON(serializedNode: SerializedQuoteNode): QuoteNode {\n        const node = $createQuoteNode();\n        deserializeCommonBlockNode(serializedNode, node);\n        return node;\n    }\n\n    exportJSON(): SerializedQuoteNode {\n        return {\n            ...super.exportJSON(),\n            type: 'quote',\n        };\n    }\n\n    // Mutation\n\n    insertNewAfter(_: RangeSelection, restoreSelection?: boolean): ParagraphNode {\n        const newBlock = $createParagraphNode();\n        const direction = this.getDirection();\n        newBlock.setDirection(direction);\n        this.insertAfter(newBlock, restoreSelection);\n        return newBlock;\n    }\n\n    collapseAtStart(): true {\n        const paragraph = $createParagraphNode();\n        const children = this.getChildren();\n        children.forEach((child) => paragraph.append(child));\n        this.replace(paragraph);\n        return true;\n    }\n\n    canMergeWhenEmpty(): true {\n        return true;\n    }\n}\n\nexport function $createQuoteNode(): QuoteNode {\n    return $applyNodeReplacement(new QuoteNode());\n}\n\nexport function $isQuoteNode(\n    node: LexicalNode | null | undefined,\n): node is QuoteNode {\n    return node instanceof QuoteNode;\n}\n\nfunction $convertBlockquoteElement(element: HTMLElement): DOMConversionOutput {\n    const node = $createQuoteNode();\n    setCommonBlockPropsFromElement(element, node);\n    return {node};\n}"
  },
  {
    "path": "resources/js/wysiwyg/lexical/rich-text/__tests__/unit/LexicalDetailsNode.test.ts",
    "content": "import {createTestContext} from \"lexical/__tests__/utils\";\nimport {$createDetailsNode} from \"@lexical/rich-text/LexicalDetailsNode\";\n\nconst editorConfig = Object.freeze({\n    namespace: '',\n    theme: {\n    },\n});\n\ndescribe('LexicalDetailsNode tests', () => {\n    test('createDOM()', () => {\n        const {editor} = createTestContext();\n        let html!: string;\n\n        editor.updateAndCommit(() => {\n            const details = $createDetailsNode();\n            html = details.createDOM(editorConfig, editor).outerHTML;\n        });\n\n        expect(html).toBe(`<details contenteditable=\"false\"><summary contenteditable=\"false\"></summary></details>`);\n    });\n\n    test('exportDOM()', () => {\n        const {editor} = createTestContext();\n        let html!: string;\n\n        editor.updateAndCommit(() => {\n            const details = $createDetailsNode();\n            details.setSummary('Hello there<>!')\n            html = (details.exportDOM(editor).element as HTMLElement).outerHTML;\n        });\n\n        expect(html).toBe(`<details><summary>Hello there&lt;&gt;!</summary></details>`);\n    });\n})"
  },
  {
    "path": "resources/js/wysiwyg/lexical/rich-text/__tests__/unit/LexicalHeadingNode.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {\n  $createTextNode,\n  $getRoot,\n  $getSelection,\n  ParagraphNode,\n  RangeSelection,\n} from 'lexical';\nimport {initializeUnitTest} from 'lexical/__tests__/utils';\nimport {$createHeadingNode, $isHeadingNode, HeadingNode} from \"@lexical/rich-text/LexicalHeadingNode\";\n\nconst editorConfig = Object.freeze({\n  namespace: '',\n  theme: {\n    heading: {\n      h1: 'my-h1-class',\n      h2: 'my-h2-class',\n      h3: 'my-h3-class',\n      h4: 'my-h4-class',\n      h5: 'my-h5-class',\n      h6: 'my-h6-class',\n    },\n  },\n});\n\ndescribe('LexicalHeadingNode tests', () => {\n  initializeUnitTest((testEnv) => {\n    test('HeadingNode.constructor', async () => {\n      const {editor} = testEnv;\n      await editor.update(() => {\n        const headingNode = new HeadingNode('h1');\n        expect(headingNode.getType()).toBe('heading');\n        expect(headingNode.getTag()).toBe('h1');\n        expect(headingNode.getTextContent()).toBe('');\n      });\n      expect(() => new HeadingNode('h1')).toThrow();\n    });\n\n    test('HeadingNode.createDOM()', async () => {\n      const {editor} = testEnv;\n      await editor.update(() => {\n        const headingNode = new HeadingNode('h1');\n        expect(headingNode.createDOM(editorConfig).outerHTML).toBe(\n          '<h1 class=\"my-h1-class\"></h1>',\n        );\n        expect(\n          headingNode.createDOM({\n            namespace: '',\n            theme: {\n              heading: {},\n            },\n          }).outerHTML,\n        ).toBe('<h1></h1>');\n        expect(\n          headingNode.createDOM({\n            namespace: '',\n            theme: {},\n          }).outerHTML,\n        ).toBe('<h1></h1>');\n      });\n    });\n\n    test('HeadingNode.updateDOM()', async () => {\n      const {editor} = testEnv;\n      await editor.update(() => {\n        const headingNode = new HeadingNode('h1');\n        const domElement = headingNode.createDOM(editorConfig);\n        expect(domElement.outerHTML).toBe('<h1 class=\"my-h1-class\"></h1>');\n        const newHeadingNode = new HeadingNode('h2');\n        const result = newHeadingNode.updateDOM(headingNode, domElement);\n        expect(result).toBe(false);\n        expect(domElement.outerHTML).toBe('<h1 class=\"my-h1-class\"></h1>');\n      });\n    });\n\n    test('HeadingNode.insertNewAfter() empty', async () => {\n      const {editor} = testEnv;\n      let headingNode: HeadingNode;\n      await editor.update(() => {\n        const root = $getRoot();\n        headingNode = new HeadingNode('h1');\n        root.append(headingNode);\n      });\n      expect(testEnv.outerHTML).toBe(\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><h1><br></h1></div>',\n      );\n      await editor.update(() => {\n        const selection = $getSelection() as RangeSelection;\n        const result = headingNode.insertNewAfter(selection);\n        expect(result).toBeInstanceOf(ParagraphNode);\n        expect(result.getDirection()).toEqual(headingNode.getDirection());\n      });\n      expect(testEnv.outerHTML).toBe(\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><h1><br></h1><p><br></p></div>',\n      );\n    });\n\n    test('HeadingNode.insertNewAfter() middle', async () => {\n      const {editor} = testEnv;\n      let headingNode: HeadingNode;\n      await editor.update(() => {\n        const root = $getRoot();\n        headingNode = new HeadingNode('h1');\n        const headingTextNode = $createTextNode('hello world');\n        root.append(headingNode.append(headingTextNode));\n        headingTextNode.select(5, 5);\n      });\n      expect(testEnv.outerHTML).toBe(\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><h1><span data-lexical-text=\"true\">hello world</span></h1></div>',\n      );\n      await editor.update(() => {\n        const selection = $getSelection() as RangeSelection;\n        const result = headingNode.insertNewAfter(selection);\n        expect(result).toBeInstanceOf(HeadingNode);\n        expect(result.getDirection()).toEqual(headingNode.getDirection());\n      });\n      expect(testEnv.outerHTML).toBe(\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><h1><span data-lexical-text=\"true\">hello world</span></h1><h1><br></h1></div>',\n      );\n    });\n\n    test('HeadingNode.insertNewAfter() end', async () => {\n      const {editor} = testEnv;\n      let headingNode: HeadingNode;\n      await editor.update(() => {\n        const root = $getRoot();\n        headingNode = new HeadingNode('h1');\n        const headingTextNode1 = $createTextNode('hello');\n        const headingTextNode2 = $createTextNode(' world');\n        headingTextNode2.setFormat('bold');\n        root.append(headingNode.append(headingTextNode1, headingTextNode2));\n        headingTextNode2.selectEnd();\n      });\n      expect(testEnv.outerHTML).toBe(\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><h1><span data-lexical-text=\"true\">hello</span><strong data-lexical-text=\"true\"> world</strong></h1></div>',\n      );\n      await editor.update(() => {\n        const selection = $getSelection() as RangeSelection;\n        const result = headingNode.insertNewAfter(selection);\n        expect(result).toBeInstanceOf(ParagraphNode);\n        expect(result.getDirection()).toEqual(headingNode.getDirection());\n      });\n      expect(testEnv.outerHTML).toBe(\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><h1><span data-lexical-text=\"true\">hello</span><strong data-lexical-text=\"true\"> world</strong></h1><p><br></p></div>',\n      );\n    });\n\n    test('$createHeadingNode()', async () => {\n      const {editor} = testEnv;\n      await editor.update(() => {\n        const headingNode = new HeadingNode('h1');\n        const createdHeadingNode = $createHeadingNode('h1');\n        expect(headingNode.__type).toEqual(createdHeadingNode.__type);\n        expect(headingNode.__parent).toEqual(createdHeadingNode.__parent);\n        expect(headingNode.__key).not.toEqual(createdHeadingNode.__key);\n      });\n    });\n\n    test('$isHeadingNode()', async () => {\n      const {editor} = testEnv;\n      await editor.update(() => {\n        const headingNode = new HeadingNode('h1');\n        expect($isHeadingNode(headingNode)).toBe(true);\n      });\n    });\n\n    test('creates a h2 with text and can insert a new paragraph after', async () => {\n      const {editor} = testEnv;\n      let headingNode: HeadingNode;\n      const text = 'hello world';\n      await editor.update(() => {\n        const root = $getRoot();\n        headingNode = new HeadingNode('h2');\n        root.append(headingNode);\n        const textNode = $createTextNode(text);\n        headingNode.append(textNode);\n      });\n      expect(testEnv.outerHTML).toBe(\n        `<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><h2><span data-lexical-text=\"true\">${text}</span></h2></div>`,\n      );\n      await editor.update(() => {\n        const result = headingNode.insertNewAfter();\n        expect(result).toBeInstanceOf(ParagraphNode);\n        expect(result.getDirection()).toEqual(headingNode.getDirection());\n      });\n      expect(testEnv.outerHTML).toBe(\n        `<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><h2><span data-lexical-text=\"true\">${text}</span></h2><p><br></p></div>`,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/rich-text/__tests__/unit/LexicalMediaNode.test.ts",
    "content": "import {createTestContext} from \"lexical/__tests__/utils\";\nimport {$createMediaNode} from \"@lexical/rich-text/LexicalMediaNode\";\n\n\ndescribe('LexicalMediaNode', () => {\n\n    test('setWidth/setHeight/setWidthAndHeight functions remove relevant styles', () => {\n        const {editor} = createTestContext();\n        editor.updateAndCommit(() => {\n            const mediaMode = $createMediaNode('video');\n            const defaultStyles = {style: 'width:20px;height:40px;color:red'};\n\n            mediaMode.setAttributes(defaultStyles);\n            mediaMode.setWidth(60);\n            expect(mediaMode.getWidth()).toBe(60);\n            expect(mediaMode.getAttributes().style).toBe('height:40px;color:red');\n\n            mediaMode.setAttributes(defaultStyles);\n            mediaMode.setHeight(77);\n            expect(mediaMode.getHeight()).toBe(77);\n            expect(mediaMode.getAttributes().style).toBe('width:20px;color:red');\n\n            mediaMode.setAttributes(defaultStyles);\n            mediaMode.setWidthAndHeight('6', '7');\n            expect(mediaMode.getWidth()).toBe(6);\n            expect(mediaMode.getHeight()).toBe(7);\n            expect(mediaMode.getAttributes().style).toBe('color:red');\n        });\n    });\n\n    test('setSrc on video uses sources if existing', () => {\n        const {editor} = createTestContext();\n        editor.updateAndCommit(() => {\n            const mediaMode = $createMediaNode('video');\n            mediaMode.setAttributes({src: 'z'});\n            mediaMode.setSources([{src: 'a', type: 'video'}, {src: 'b', type: 'video'}]);\n\n            mediaMode.setSrc('c');\n\n            expect(mediaMode.getAttributes().src).toBeUndefined();\n            expect(mediaMode.getSources()).toHaveLength(1);\n            expect(mediaMode.getSources()[0].src).toBe('c');\n        });\n    });\n\n});"
  },
  {
    "path": "resources/js/wysiwyg/lexical/rich-text/__tests__/unit/LexicalQuoteNode.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {$createRangeSelection, $getRoot, ParagraphNode} from 'lexical';\nimport {initializeUnitTest} from 'lexical/__tests__/utils';\nimport {$createQuoteNode, QuoteNode} from \"@lexical/rich-text/LexicalQuoteNode\";\n\nconst editorConfig = Object.freeze({\n  namespace: '',\n  theme: {\n    quote: 'my-quote-class',\n  },\n});\n\ndescribe('LexicalQuoteNode tests', () => {\n  initializeUnitTest((testEnv) => {\n    test('QuoteNode.constructor', async () => {\n      const {editor} = testEnv;\n      await editor.update(() => {\n        const quoteNode = $createQuoteNode();\n        expect(quoteNode.getType()).toBe('quote');\n        expect(quoteNode.getTextContent()).toBe('');\n      });\n      expect(() => $createQuoteNode()).toThrow();\n    });\n\n    test('QuoteNode.createDOM()', async () => {\n      const {editor} = testEnv;\n      await editor.update(() => {\n        const quoteNode = $createQuoteNode();\n        expect(quoteNode.createDOM(editorConfig).outerHTML).toBe(\n          '<blockquote class=\"my-quote-class\"></blockquote>',\n        );\n        expect(\n          quoteNode.createDOM({\n            namespace: '',\n            theme: {},\n          }).outerHTML,\n        ).toBe('<blockquote></blockquote>');\n      });\n    });\n\n    test('QuoteNode.updateDOM()', async () => {\n      const {editor} = testEnv;\n      await editor.update(() => {\n        const quoteNode = $createQuoteNode();\n        const domElement = quoteNode.createDOM(editorConfig);\n        expect(domElement.outerHTML).toBe(\n          '<blockquote class=\"my-quote-class\"></blockquote>',\n        );\n        const newQuoteNode = $createQuoteNode();\n        const result = newQuoteNode.updateDOM(quoteNode, domElement);\n        expect(result).toBe(false);\n        expect(domElement.outerHTML).toBe(\n          '<blockquote class=\"my-quote-class\"></blockquote>',\n        );\n      });\n    });\n\n    test('QuoteNode.insertNewAfter()', async () => {\n      const {editor} = testEnv;\n      let quoteNode: QuoteNode;\n      await editor.update(() => {\n        const root = $getRoot();\n        quoteNode = $createQuoteNode();\n        root.append(quoteNode);\n      });\n      expect(testEnv.outerHTML).toBe(\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><blockquote><br></blockquote></div>',\n      );\n      await editor.update(() => {\n        const result = quoteNode.insertNewAfter($createRangeSelection());\n        expect(result).toBeInstanceOf(ParagraphNode);\n        expect(result.getDirection()).toEqual(quoteNode.getDirection());\n      });\n      expect(testEnv.outerHTML).toBe(\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><blockquote><br></blockquote><p><br></p></div>',\n      );\n    });\n\n    test('$createQuoteNode()', async () => {\n      const {editor} = testEnv;\n      await editor.update(() => {\n        const quoteNode = $createQuoteNode();\n        const createdQuoteNode = $createQuoteNode();\n        expect(quoteNode.__type).toEqual(createdQuoteNode.__type);\n        expect(quoteNode.__parent).toEqual(createdQuoteNode.__parent);\n        expect(quoteNode.__key).not.toEqual(createdQuoteNode.__key);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/rich-text/index.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {\n  CommandPayloadType,\n  LexicalCommand,\n  LexicalEditor,\n  PasteCommandType,\n  RangeSelection,\n  TextFormatType,\n} from 'lexical';\nimport {\n  $createRangeSelection,\n  $createTabNode,\n  $getAdjacentNode,\n  $getNearestNodeFromDOMNode,\n  $getRoot,\n  $getSelection,\n  $insertNodes,\n  $isDecoratorNode,\n  $isElementNode,\n  $isNodeSelection,\n  $isRangeSelection,\n  $isTextNode,\n  $normalizeSelection__EXPERIMENTAL,\n  $selectAll,\n  $setSelection,\n  CLICK_COMMAND,\n  COMMAND_PRIORITY_EDITOR,\n  CONTROLLED_TEXT_INSERTION_COMMAND,\n  COPY_COMMAND,\n  createCommand,\n  CUT_COMMAND,\n  DELETE_CHARACTER_COMMAND,\n  DELETE_LINE_COMMAND,\n  DELETE_WORD_COMMAND,\n  DRAGOVER_COMMAND,\n  DRAGSTART_COMMAND,\n  DROP_COMMAND,\n  ElementNode,\n  FORMAT_TEXT_COMMAND,\n  INSERT_LINE_BREAK_COMMAND,\n  INSERT_PARAGRAPH_COMMAND,\n  INSERT_TAB_COMMAND,\n  isSelectionCapturedInDecoratorInput,\n  KEY_ARROW_DOWN_COMMAND,\n  KEY_ARROW_LEFT_COMMAND,\n  KEY_ARROW_RIGHT_COMMAND,\n  KEY_ARROW_UP_COMMAND,\n  KEY_BACKSPACE_COMMAND,\n  KEY_DELETE_COMMAND,\n  KEY_ENTER_COMMAND,\n  KEY_ESCAPE_COMMAND,\n  PASTE_COMMAND,\n  REMOVE_TEXT_COMMAND,\n  SELECT_ALL_COMMAND,\n} from 'lexical';\n\nimport {$insertDataTransferForRichText, copyToClipboard,} from '@lexical/clipboard';\nimport {$moveCharacter, $shouldOverrideDefaultCharacterSelection,} from '@lexical/selection';\nimport {$findMatchingParent, mergeRegister, objectKlassEquals,} from '@lexical/utils';\nimport caretFromPoint from 'lexical/shared/caretFromPoint';\nimport {CAN_USE_BEFORE_INPUT, IS_APPLE_WEBKIT, IS_IOS, IS_SAFARI,} from 'lexical/shared/environment';\n\nexport const DRAG_DROP_PASTE: LexicalCommand<Array<File>> = createCommand(\n  'DRAG_DROP_PASTE_FILE',\n);\n\n\n\nfunction onPasteForRichText(\n  event: CommandPayloadType<typeof PASTE_COMMAND>,\n  editor: LexicalEditor,\n): void {\n  event.preventDefault();\n  editor.update(\n    () => {\n      const selection = $getSelection();\n      const clipboardData =\n        objectKlassEquals(event, InputEvent) ||\n        objectKlassEquals(event, KeyboardEvent)\n          ? null\n          : (event as ClipboardEvent).clipboardData;\n      if (clipboardData != null && selection !== null) {\n        $insertDataTransferForRichText(clipboardData, selection, editor);\n      }\n    },\n    {\n      tag: 'paste',\n    },\n  );\n}\n\nasync function onCutForRichText(\n  event: CommandPayloadType<typeof CUT_COMMAND>,\n  editor: LexicalEditor,\n): Promise<void> {\n  await copyToClipboard(\n    editor,\n    objectKlassEquals(event, ClipboardEvent) ? (event as ClipboardEvent) : null,\n  );\n  editor.update(() => {\n    const selection = $getSelection();\n    if ($isRangeSelection(selection)) {\n      selection.removeText();\n    } else if ($isNodeSelection(selection)) {\n      selection.getNodes().forEach((node) => node.remove());\n    }\n  });\n}\n\n// Clipboard may contain files that we aren't allowed to read. While the event is arguably useless,\n// in certain occasions, we want to know whether it was a file transfer, as opposed to text. We\n// control this with the first boolean flag.\nexport function eventFiles(\n  event: DragEvent | PasteCommandType,\n): [boolean, Array<File>, boolean] {\n  let dataTransfer: null | DataTransfer = null;\n  if (objectKlassEquals(event, DragEvent)) {\n    dataTransfer = (event as DragEvent).dataTransfer;\n  } else if (objectKlassEquals(event, ClipboardEvent)) {\n    dataTransfer = (event as ClipboardEvent).clipboardData;\n  }\n\n  if (dataTransfer === null) {\n    return [false, [], false];\n  }\n\n  const types = dataTransfer.types;\n  const hasFiles = types.includes('Files');\n  const hasContent =\n    types.includes('text/html') || types.includes('text/plain');\n  return [hasFiles, Array.from(dataTransfer.files), hasContent];\n}\n\nfunction $handleIndentAndOutdent(\n  indentOrOutdent: (block: ElementNode) => void,\n): boolean {\n  const selection = $getSelection();\n  if (!$isRangeSelection(selection)) {\n    return false;\n  }\n  const alreadyHandled = new Set();\n  const nodes = selection.getNodes();\n  for (let i = 0; i < nodes.length; i++) {\n    const node = nodes[i];\n    const key = node.getKey();\n    if (alreadyHandled.has(key)) {\n      continue;\n    }\n    const parentBlock = $findMatchingParent(\n      node,\n      (parentNode): parentNode is ElementNode =>\n        $isElementNode(parentNode) && !parentNode.isInline(),\n    );\n    if (parentBlock === null) {\n      continue;\n    }\n    const parentKey = parentBlock.getKey();\n    if (parentBlock.canIndent() && !alreadyHandled.has(parentKey)) {\n      alreadyHandled.add(parentKey);\n      indentOrOutdent(parentBlock);\n    }\n  }\n  return alreadyHandled.size > 0;\n}\n\nfunction $isTargetWithinDecorator(target: HTMLElement): boolean {\n  const node = $getNearestNodeFromDOMNode(target);\n  return $isDecoratorNode(node);\n}\n\nfunction $isSelectionAtEndOfRoot(selection: RangeSelection) {\n  const focus = selection.focus;\n  return focus.key === 'root' && focus.offset === $getRoot().getChildrenSize();\n}\n\nexport function registerRichText(editor: LexicalEditor): () => void {\n  const removeListener = mergeRegister(\n    editor.registerCommand(\n      CLICK_COMMAND,\n      (payload) => {\n        const selection = $getSelection();\n        if ($isNodeSelection(selection)) {\n          selection.clear();\n          return true;\n        }\n        return false;\n      },\n      0,\n    ),\n    editor.registerCommand<boolean>(\n      DELETE_CHARACTER_COMMAND,\n      (isBackward) => {\n        const selection = $getSelection();\n        if (!$isRangeSelection(selection)) {\n          return false;\n        }\n        selection.deleteCharacter(isBackward);\n        return true;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand<boolean>(\n      DELETE_WORD_COMMAND,\n      (isBackward) => {\n        const selection = $getSelection();\n        if (!$isRangeSelection(selection)) {\n          return false;\n        }\n        selection.deleteWord(isBackward);\n        return true;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand<boolean>(\n      DELETE_LINE_COMMAND,\n      (isBackward) => {\n        const selection = $getSelection();\n        if (!$isRangeSelection(selection)) {\n          return false;\n        }\n        selection.deleteLine(isBackward);\n        return true;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand(\n      CONTROLLED_TEXT_INSERTION_COMMAND,\n      (eventOrText) => {\n        const selection = $getSelection();\n\n        if (typeof eventOrText === 'string') {\n          if (selection !== null) {\n            selection.insertText(eventOrText);\n          }\n        } else {\n          if (selection === null) {\n            return false;\n          }\n\n          const dataTransfer = eventOrText.dataTransfer;\n          if (dataTransfer != null) {\n            $insertDataTransferForRichText(dataTransfer, selection, editor);\n          } else if ($isRangeSelection(selection)) {\n            const data = eventOrText.data;\n            if (data) {\n              selection.insertText(data);\n            }\n            return true;\n          }\n        }\n        return true;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand(\n      REMOVE_TEXT_COMMAND,\n      () => {\n        const selection = $getSelection();\n        if (!$isRangeSelection(selection)) {\n          return false;\n        }\n        selection.removeText();\n        return true;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand<TextFormatType>(\n      FORMAT_TEXT_COMMAND,\n      (format) => {\n        const selection = $getSelection();\n        if (!$isRangeSelection(selection)) {\n          return false;\n        }\n        selection.formatText(format);\n        return true;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand<boolean>(\n      INSERT_LINE_BREAK_COMMAND,\n      (selectStart) => {\n        const selection = $getSelection();\n        if (!$isRangeSelection(selection)) {\n          return false;\n        }\n        selection.insertLineBreak(selectStart);\n        return true;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand(\n      INSERT_PARAGRAPH_COMMAND,\n      () => {\n        const selection = $getSelection();\n        if (!$isRangeSelection(selection)) {\n          return false;\n        }\n        selection.insertParagraph();\n        return true;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand(\n      INSERT_TAB_COMMAND,\n      () => {\n        $insertNodes([$createTabNode()]);\n        return true;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand<KeyboardEvent>(\n      KEY_ARROW_UP_COMMAND,\n      (event) => {\n        const selection = $getSelection();\n        if (\n          $isNodeSelection(selection) &&\n          !$isTargetWithinDecorator(event.target as HTMLElement)\n        ) {\n          // If selection is on a node, let's try and move selection\n          // back to being a range selection.\n          const nodes = selection.getNodes();\n          if (nodes.length > 0) {\n            nodes[0].selectPrevious();\n            return true;\n          }\n        } else if ($isRangeSelection(selection)) {\n          const possibleNode = $getAdjacentNode(selection.focus, true);\n          if (\n            !event.shiftKey &&\n            $isDecoratorNode(possibleNode) &&\n            !possibleNode.isIsolated() &&\n            !possibleNode.isInline()\n          ) {\n            possibleNode.selectPrevious();\n            event.preventDefault();\n            return true;\n          }\n        }\n        return false;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand<KeyboardEvent>(\n      KEY_ARROW_DOWN_COMMAND,\n      (event) => {\n        const selection = $getSelection();\n        if ($isNodeSelection(selection)) {\n          // If selection is on a node, let's try and move selection\n          // back to being a range selection.\n          const nodes = selection.getNodes();\n          if (nodes.length > 0) {\n            nodes[0].selectNext(0, 0);\n            return true;\n          }\n        } else if ($isRangeSelection(selection)) {\n          if ($isSelectionAtEndOfRoot(selection)) {\n            event.preventDefault();\n            return true;\n          }\n          const possibleNode = $getAdjacentNode(selection.focus, false);\n          if (\n            !event.shiftKey &&\n            $isDecoratorNode(possibleNode) &&\n            !possibleNode.isIsolated() &&\n            !possibleNode.isInline()\n          ) {\n            possibleNode.selectNext();\n            event.preventDefault();\n            return true;\n          }\n        }\n        return false;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand<KeyboardEvent>(\n      KEY_ARROW_LEFT_COMMAND,\n      (event) => {\n        const selection = $getSelection();\n        if ($isNodeSelection(selection)) {\n          // If selection is on a node, let's try and move selection\n          // back to being a range selection.\n          const nodes = selection.getNodes();\n          if (nodes.length > 0) {\n            event.preventDefault();\n            nodes[0].selectPrevious();\n            return true;\n          }\n        }\n        if (!$isRangeSelection(selection)) {\n          return false;\n        }\n        if ($shouldOverrideDefaultCharacterSelection(selection, true)) {\n          const isHoldingShift = event.shiftKey;\n          event.preventDefault();\n          $moveCharacter(selection, isHoldingShift, true);\n          return true;\n        }\n        return false;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand<KeyboardEvent>(\n      KEY_ARROW_RIGHT_COMMAND,\n      (event) => {\n        const selection = $getSelection();\n        if (\n          $isNodeSelection(selection) &&\n          !$isTargetWithinDecorator(event.target as HTMLElement)\n        ) {\n          // If selection is on a node, let's try and move selection\n          // back to being a range selection.\n          const nodes = selection.getNodes();\n          if (nodes.length > 0) {\n            event.preventDefault();\n            nodes[0].selectNext(0, 0);\n            return true;\n          }\n        }\n        if (!$isRangeSelection(selection)) {\n          return false;\n        }\n        const isHoldingShift = event.shiftKey;\n        if ($shouldOverrideDefaultCharacterSelection(selection, false)) {\n          event.preventDefault();\n          $moveCharacter(selection, isHoldingShift, false);\n          return true;\n        }\n        return false;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand<KeyboardEvent>(\n      KEY_BACKSPACE_COMMAND,\n      (event) => {\n        if ($isTargetWithinDecorator(event.target as HTMLElement)) {\n          return false;\n        }\n        const selection = $getSelection();\n        if (!$isRangeSelection(selection)) {\n          return false;\n        }\n        event.preventDefault();\n\n        return editor.dispatchCommand(DELETE_CHARACTER_COMMAND, true);\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand<KeyboardEvent>(\n      KEY_DELETE_COMMAND,\n      (event) => {\n        if ($isTargetWithinDecorator(event.target as HTMLElement)) {\n          return false;\n        }\n        const selection = $getSelection();\n        if (!$isRangeSelection(selection)) {\n          return false;\n        }\n        event.preventDefault();\n        return editor.dispatchCommand(DELETE_CHARACTER_COMMAND, false);\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand<KeyboardEvent | null>(\n      KEY_ENTER_COMMAND,\n      (event) => {\n        const selection = $getSelection();\n        if (!$isRangeSelection(selection)) {\n          return false;\n        }\n        if (event !== null) {\n          // If we have beforeinput, then we can avoid blocking\n          // the default behavior. This ensures that the iOS can\n          // intercept that we're actually inserting a paragraph,\n          // and autocomplete, autocapitalize etc work as intended.\n          // This can also cause a strange performance issue in\n          // Safari, where there is a noticeable pause due to\n          // preventing the key down of enter.\n          if (\n            (IS_IOS || IS_SAFARI || IS_APPLE_WEBKIT) &&\n            CAN_USE_BEFORE_INPUT\n          ) {\n            return false;\n          }\n          event.preventDefault();\n          if (event.shiftKey) {\n            return editor.dispatchCommand(INSERT_LINE_BREAK_COMMAND, false);\n          }\n        }\n        return editor.dispatchCommand(INSERT_PARAGRAPH_COMMAND, undefined);\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand(\n      KEY_ESCAPE_COMMAND,\n      () => {\n        const selection = $getSelection();\n        if (!$isRangeSelection(selection)) {\n          return false;\n        }\n        editor.blur();\n        return true;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand<DragEvent>(\n      DROP_COMMAND,\n      (event) => {\n        const [, files] = eventFiles(event);\n        if (files.length > 0) {\n          const x = event.clientX;\n          const y = event.clientY;\n          const eventRange = caretFromPoint(x, y);\n          if (eventRange !== null) {\n            const {offset: domOffset, node: domNode} = eventRange;\n            const node = $getNearestNodeFromDOMNode(domNode);\n            if (node !== null) {\n              const selection = $createRangeSelection();\n              if ($isTextNode(node)) {\n                selection.anchor.set(node.getKey(), domOffset, 'text');\n                selection.focus.set(node.getKey(), domOffset, 'text');\n              } else {\n                const parentKey = node.getParentOrThrow().getKey();\n                const offset = node.getIndexWithinParent() + 1;\n                selection.anchor.set(parentKey, offset, 'element');\n                selection.focus.set(parentKey, offset, 'element');\n              }\n              const normalizedSelection =\n                $normalizeSelection__EXPERIMENTAL(selection);\n              $setSelection(normalizedSelection);\n            }\n            editor.dispatchCommand(DRAG_DROP_PASTE, files);\n          }\n          event.preventDefault();\n          return true;\n        }\n\n        const selection = $getSelection();\n        if ($isRangeSelection(selection)) {\n          return true;\n        }\n\n        return false;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand<DragEvent>(\n      DRAGSTART_COMMAND,\n      (event) => {\n        const [isFileTransfer] = eventFiles(event);\n        const selection = $getSelection();\n        if (isFileTransfer && !$isRangeSelection(selection)) {\n          return false;\n        }\n        return true;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand<DragEvent>(\n      DRAGOVER_COMMAND,\n      (event) => {\n        const [isFileTransfer] = eventFiles(event);\n        const selection = $getSelection();\n        if (isFileTransfer && !$isRangeSelection(selection)) {\n          return false;\n        }\n        const x = event.clientX;\n        const y = event.clientY;\n        const eventRange = caretFromPoint(x, y);\n        if (eventRange !== null) {\n          const node = $getNearestNodeFromDOMNode(eventRange.node);\n          if ($isDecoratorNode(node)) {\n            // Show browser caret as the user is dragging the media across the screen. Won't work\n            // for DecoratorNode nor it's relevant.\n            event.preventDefault();\n          }\n        }\n        return true;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand(\n      SELECT_ALL_COMMAND,\n      () => {\n        $selectAll();\n\n        return true;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand(\n      COPY_COMMAND,\n      (event) => {\n        copyToClipboard(\n          editor,\n          objectKlassEquals(event, ClipboardEvent)\n            ? (event as ClipboardEvent)\n            : null,\n        );\n        return true;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand(\n      CUT_COMMAND,\n      (event) => {\n        onCutForRichText(event, editor);\n        return true;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n    editor.registerCommand(\n      PASTE_COMMAND,\n      (event) => {\n        const [, files, hasTextContent] = eventFiles(event);\n        if (files.length > 0 && !hasTextContent) {\n          editor.dispatchCommand(DRAG_DROP_PASTE, files);\n          return true;\n        }\n\n        // if inputs then paste within the input ignore creating a new node on paste event\n        if (isSelectionCapturedInDecoratorInput(event.target as Node)) {\n          return false;\n        }\n\n        const selection = $getSelection();\n        if (selection !== null) {\n          onPasteForRichText(event, editor);\n          return true;\n        }\n\n        return false;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    ),\n  );\n  return removeListener;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/selection/__tests__/unit/LexicalSelection.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {$createLinkNode} from '@lexical/link';\nimport {$createListItemNode, $createListNode} from '@lexical/list';\nimport {registerRichText} from '@lexical/rich-text';\nimport {\n  $addNodeStyle,\n  $getSelectionStyleValueForProperty,\n  $patchStyleText,\n  $setBlocksType,\n} from '@lexical/selection';\nimport {$createTableNodeWithDimensions} from '@lexical/table';\nimport {\n  $createLineBreakNode,\n  $createParagraphNode,\n  $createRangeSelection,\n  $createTextNode,\n  $getRoot,\n  $getSelection, $insertNodes,\n  $isElementNode,\n  $isRangeSelection,\n  $isTextNode,\n  $setSelection,\n  DecoratorNode,\n  ElementNode,\n  LexicalEditor,\n  LexicalNode,\n  ParagraphNode,\n  PointType,\n  type RangeSelection,\n  TextNode,\n} from 'lexical';\nimport {\n    $assertRangeSelection,\n    $createTestDecoratorNode,\n    $createTestElementNode,\n    createTestEditor,\n    initializeClipboard,\n    invariant, patchRange,\n} from 'lexical/__tests__/utils';\n\nimport {\n  $setAnchorPoint,\n  $setFocusPoint,\n  applySelectionInputs,\n  convertToSegmentedNode,\n  convertToTokenNode,\n  deleteBackward,\n  deleteWordBackward,\n  deleteWordForward,\n  formatBold,\n  formatItalic,\n  formatStrikeThrough,\n  formatUnderline,\n  getNodeFromPath,\n  insertParagraph,\n  insertSegmentedNode,\n  insertText,\n  insertTokenNode,\n  moveBackward,\n  moveEnd,\n  moveNativeSelection,\n  pastePlain,\n  printWhitespace,\n  redo,\n  setNativeSelectionWithPaths,\n  undo,\n} from '../utils';\nimport {createEmptyHistoryState, registerHistory} from \"@lexical/history\";\nimport {mergeRegister} from \"@lexical/utils\";\nimport {$createHeadingNode} from \"@lexical/rich-text/LexicalHeadingNode\";\n\ninterface ExpectedSelection {\n  anchorPath: number[];\n  anchorOffset: number;\n  focusPath: number[];\n  focusOffset: number;\n}\n\ninitializeClipboard();\n\njest.mock('lexical/shared/environment', () => {\n  const originalModule = jest.requireActual('lexical/shared/environment');\n\n  return {...originalModule, IS_FIREFOX: true};\n});\n\npatchRange();\n\ndescribe('LexicalSelection tests', () => {\n  let container: HTMLElement;\n  let root: HTMLDivElement;\n  let editor: LexicalEditor | null = null;\n\n  beforeEach(async () => {\n    container = document.createElement('div');\n    document.body.appendChild(container);\n\n    root = document.createElement('div');\n    root.setAttribute('contenteditable', 'true');\n    container.append(root);\n\n    await init();\n  });\n\n  afterEach(async () => {\n    document.body.removeChild(container);\n  });\n\n  async function init() {\n\n    editor = createTestEditor({\n      nodes: [],\n      theme: {\n        code: 'editor-code',\n        heading: {\n          h1: 'editor-heading-h1',\n          h2: 'editor-heading-h2',\n          h3: 'editor-heading-h3',\n          h4: 'editor-heading-h4',\n          h5: 'editor-heading-h5',\n          h6: 'editor-heading-h6',\n        },\n        image: 'editor-image',\n        list: {\n          ol: 'editor-list-ol',\n          ul: 'editor-list-ul',\n        },\n        listitem: 'editor-listitem',\n        paragraph: 'editor-paragraph',\n        quote: 'editor-quote',\n        text: {\n          bold: 'editor-text-bold',\n          code: 'editor-text-code',\n          hashtag: 'editor-text-hashtag',\n          italic: 'editor-text-italic',\n          link: 'editor-text-link',\n          strikethrough: 'editor-text-strikethrough',\n          underline: 'editor-text-underline',\n          underlineStrikethrough: 'editor-text-underlineStrikethrough',\n        },\n      }\n    });\n\n    mergeRegister(\n      registerHistory(editor, createEmptyHistoryState(), 300),\n      registerRichText(editor),\n    );\n\n    editor.setRootElement(root);\n    editor.update(() => {\n        const p = $createParagraphNode();\n        $insertNodes([p]);\n    });\n    editor.commitUpdates();\n    editor.focus();\n\n    // Focus first element\n    setNativeSelectionWithPaths(\n      editor!.getRootElement()!,\n      [0, 0],\n      0,\n      [0, 0],\n      0,\n    );\n  }\n\n  async function update(fn: () => void) {\n    editor!.update(fn);\n    editor!.commitUpdates();\n  }\n\n  test('Expect initial output to be a block with no text.', () => {\n    expect(container!.innerHTML).toBe(\n      '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><br></p></div>',\n    );\n  });\n\n  function assertSelection(\n    rootElement: HTMLElement,\n    expectedSelection: ExpectedSelection,\n  ) {\n    const actualSelection = window.getSelection()!;\n\n    expect(actualSelection.anchorNode).toBe(\n      getNodeFromPath(expectedSelection.anchorPath, rootElement),\n    );\n    expect(actualSelection.anchorOffset).toBe(expectedSelection.anchorOffset);\n    expect(actualSelection.focusNode).toBe(\n      getNodeFromPath(expectedSelection.focusPath, rootElement),\n    );\n    expect(actualSelection.focusOffset).toBe(expectedSelection.focusOffset);\n  }\n\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  const GRAPHEME_SCENARIOS = [\n    {\n      description: 'grapheme cluster',\n      // Hangul grapheme cluster.\n      // https://www.compart.com/en/unicode/U+AC01\n      grapheme: '\\u1100\\u1161\\u11A8',\n    },\n    {\n      description: 'extended grapheme cluster',\n      // Tamil 'ni' grapheme cluster.\n      // http://unicode.org/reports/tr29/#Table_Sample_Grapheme_Clusters\n      grapheme: '\\u0BA8\\u0BBF',\n    },\n    {\n      description: 'tailored grapheme cluster',\n      // Devangari 'kshi' tailored grapheme cluster.\n      // http://unicode.org/reports/tr29/#Table_Sample_Grapheme_Clusters\n      grapheme: '\\u0915\\u094D\\u0937\\u093F',\n    },\n    {\n      description: 'Emoji sequence combined using zero-width joiners',\n      // https://emojipedia.org/family-woman-woman-girl-boy/\n      grapheme:\n        '\\uD83D\\uDC69\\u200D\\uD83D\\uDC69\\u200D\\uD83D\\uDC67\\u200D\\uD83D\\uDC66',\n    },\n    {\n      description: 'Emoji sequence with skin-tone modifier',\n      // https://emojipedia.org/clapping-hands-medium-skin-tone/\n      grapheme: '\\uD83D\\uDC4F\\uD83C\\uDFFD',\n    },\n  ];\n\n  const suite = [\n    {\n      expectedHTML:\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">Hello</span></p></div>',\n      expectedSelection: {\n        anchorOffset: 5,\n        anchorPath: [0, 0, 0],\n        focusOffset: 5,\n        focusPath: [0, 0, 0],\n      },\n      inputs: [\n        insertText('H'),\n        insertText('e'),\n        insertText('l'),\n        insertText('l'),\n        insertText('o'),\n      ],\n      name: 'Simple typing',\n    },\n    {\n      expectedHTML:\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\">' +\n        '<strong class=\"editor-text-bold\" data-lexical-text=\"true\">Hello</strong></p></div>',\n      expectedSelection: {\n        anchorOffset: 5,\n        anchorPath: [0, 0, 0],\n        focusOffset: 5,\n        focusPath: [0, 0, 0],\n      },\n      inputs: [\n        formatBold(),\n        insertText('H'),\n        insertText('e'),\n        insertText('l'),\n        insertText('l'),\n        insertText('o'),\n      ],\n      name: 'Simple typing in bold',\n    },\n    {\n      expectedHTML:\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\">' +\n        '<em class=\"editor-text-italic\" data-lexical-text=\"true\">Hello</em></p></div>',\n      expectedSelection: {\n        anchorOffset: 5,\n        anchorPath: [0, 0, 0],\n        focusOffset: 5,\n        focusPath: [0, 0, 0],\n      },\n      inputs: [\n        formatItalic(),\n        insertText('H'),\n        insertText('e'),\n        insertText('l'),\n        insertText('l'),\n        insertText('o'),\n      ],\n      name: 'Simple typing in italic',\n    },\n    {\n      expectedHTML:\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\">' +\n        '<strong class=\"editor-text-bold editor-text-italic\" data-lexical-text=\"true\">Hello</strong></p></div>',\n      expectedSelection: {\n        anchorOffset: 5,\n        anchorPath: [0, 0, 0],\n        focusOffset: 5,\n        focusPath: [0, 0, 0],\n      },\n      inputs: [\n        formatItalic(),\n        formatBold(),\n        insertText('H'),\n        insertText('e'),\n        insertText('l'),\n        insertText('l'),\n        insertText('o'),\n      ],\n      name: 'Simple typing in italic + bold',\n    },\n    {\n      expectedHTML:\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\">' +\n        '<span class=\"editor-text-underline\" data-lexical-text=\"true\">Hello</span></p></div>',\n      expectedSelection: {\n        anchorOffset: 5,\n        anchorPath: [0, 0, 0],\n        focusOffset: 5,\n        focusPath: [0, 0, 0],\n      },\n      inputs: [\n        formatUnderline(),\n        insertText('H'),\n        insertText('e'),\n        insertText('l'),\n        insertText('l'),\n        insertText('o'),\n      ],\n      name: 'Simple typing in underline',\n    },\n    {\n      expectedHTML:\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\">' +\n        '<span class=\"editor-text-strikethrough\" data-lexical-text=\"true\">Hello</span></p></div>',\n      expectedSelection: {\n        anchorOffset: 5,\n        anchorPath: [0, 0, 0],\n        focusOffset: 5,\n        focusPath: [0, 0, 0],\n      },\n      inputs: [\n        formatStrikeThrough(),\n        insertText('H'),\n        insertText('e'),\n        insertText('l'),\n        insertText('l'),\n        insertText('o'),\n      ],\n      name: 'Simple typing in strikethrough',\n    },\n    {\n      expectedHTML:\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\">' +\n        '<span class=\"editor-text-underlineStrikethrough\" data-lexical-text=\"true\">Hello</span></p></div>',\n      expectedSelection: {\n        anchorOffset: 5,\n        anchorPath: [0, 0, 0],\n        focusOffset: 5,\n        focusPath: [0, 0, 0],\n      },\n      inputs: [\n        formatUnderline(),\n        formatStrikeThrough(),\n        insertText('H'),\n        insertText('e'),\n        insertText('l'),\n        insertText('l'),\n        insertText('o'),\n      ],\n      name: 'Simple typing in underline + strikethrough',\n    },\n    {\n      expectedHTML:\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">1246</span></p></div>',\n      expectedSelection: {\n        anchorOffset: 4,\n        anchorPath: [0, 0, 0],\n        focusOffset: 4,\n        focusPath: [0, 0, 0],\n      },\n      inputs: [\n        insertText('1'),\n        insertText('2'),\n        insertText('3'),\n        deleteBackward(1),\n        insertText('4'),\n        insertText('5'),\n        deleteBackward(1),\n        insertText('6'),\n      ],\n      name: 'Deletion',\n    },\n    {\n      expectedHTML:\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\">' +\n        '<span data-lexical-text=\"true\">Dominic Gannaway</span>' +\n        '</p></div>',\n      expectedSelection: {\n        anchorOffset: 16,\n        anchorPath: [0, 0, 0],\n        focusOffset: 16,\n        focusPath: [0, 0, 0],\n      },\n      inputs: [insertTokenNode('Dominic Gannaway')],\n      name: 'Creation of an token node',\n    },\n    {\n      expectedHTML:\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\">' +\n        '<span data-lexical-text=\"true\">Dominic Gannaway</span>' +\n        '</p></div>',\n      expectedSelection: {\n        anchorOffset: 1,\n        anchorPath: [0],\n        focusOffset: 1,\n        focusPath: [0],\n      },\n      inputs: [\n        insertText('Dominic Gannaway'),\n        moveNativeSelection([0, 0, 0], 0, [0, 0, 0], 16),\n        convertToTokenNode(),\n      ],\n      name: 'Convert text to an token node',\n    },\n    {\n      expectedHTML:\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\">' +\n        '<span data-lexical-text=\"true\">Dominic Gannaway</span>' +\n        '</p></div>',\n      expectedSelection: {\n        anchorOffset: 1,\n        anchorPath: [0],\n        focusOffset: 1,\n        focusPath: [0],\n      },\n      inputs: [insertSegmentedNode('Dominic Gannaway')],\n      name: 'Creation of a segmented node',\n    },\n    {\n      expectedHTML:\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\">' +\n        '<span data-lexical-text=\"true\">Dominic Gannaway</span>' +\n        '</p></div>',\n      expectedSelection: {\n        anchorOffset: 1,\n        anchorPath: [0],\n        focusOffset: 1,\n        focusPath: [0],\n      },\n      inputs: [\n        insertText('Dominic Gannaway'),\n        moveNativeSelection([0, 0, 0], 0, [0, 0, 0], 16),\n        convertToSegmentedNode(),\n      ],\n      name: 'Convert text to a segmented node',\n    },\n    {\n      expectedHTML:\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\">' +\n        '<p class=\"editor-paragraph\"><br></p>' +\n        '<p class=\"editor-paragraph\">' +\n        '<strong class=\"editor-text-bold\" data-lexical-text=\"true\">Hello world</strong>' +\n        '</p>' +\n        '<p class=\"editor-paragraph\"><br></p>' +\n        '</div>',\n      expectedSelection: {\n        anchorOffset: 0,\n        anchorPath: [0],\n        focusOffset: 0,\n        focusPath: [2],\n      },\n      inputs: [\n        insertParagraph(),\n        insertText('Hello world'),\n        insertParagraph(),\n        moveNativeSelection([0], 0, [2], 0),\n        formatBold(),\n      ],\n      name: 'Format selection that starts and ends on element and retain selection',\n    },\n    {\n      expectedHTML:\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\">' +\n        '<p class=\"editor-paragraph\"><br></p>' +\n        '<p class=\"editor-paragraph\">' +\n        '<strong class=\"editor-text-bold\" data-lexical-text=\"true\">Hello</strong>' +\n        '</p>' +\n        '<p class=\"editor-paragraph\">' +\n        '<strong class=\"editor-text-bold\" data-lexical-text=\"true\">world</strong>' +\n        '</p>' +\n        '<p class=\"editor-paragraph\"><br></p>' +\n        '</div>',\n      expectedSelection: {\n        anchorOffset: 0,\n        anchorPath: [0],\n        focusOffset: 0,\n        focusPath: [3],\n      },\n      inputs: [\n        insertParagraph(),\n        insertText('Hello'),\n        insertParagraph(),\n        insertText('world'),\n        insertParagraph(),\n        moveNativeSelection([0], 0, [3], 0),\n        formatBold(),\n      ],\n      name: 'Format multiline text selection that starts and ends on element and retain selection',\n    },\n    {\n      expectedHTML:\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\">' +\n        '<p class=\"editor-paragraph\">' +\n        '<span data-lexical-text=\"true\">He</span>' +\n        '<strong class=\"editor-text-bold\" data-lexical-text=\"true\">llo</strong>' +\n        '</p>' +\n        '<p class=\"editor-paragraph\">' +\n        '<strong class=\"editor-text-bold\" data-lexical-text=\"true\">wo</strong>' +\n        '<span data-lexical-text=\"true\">rld</span>' +\n        '</p>' +\n        '</div>',\n      expectedSelection: {\n        anchorOffset: 0,\n        anchorPath: [0, 1, 0],\n        focusOffset: 2,\n        focusPath: [1, 0, 0],\n      },\n      inputs: [\n        insertText('Hello'),\n        insertParagraph(),\n        insertText('world'),\n        moveNativeSelection([0, 0, 0], 2, [1, 0, 0], 2),\n        formatBold(),\n      ],\n      name: 'Format multiline text selection that starts and ends within text',\n    },\n    {\n      expectedHTML:\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\">' +\n        '<p class=\"editor-paragraph\"><br></p>' +\n        '<p class=\"editor-paragraph\">' +\n        '<span data-lexical-text=\"true\">Hello </span>' +\n        '<strong class=\"editor-text-bold\" data-lexical-text=\"true\">world</strong>' +\n        '</p>' +\n        '<p class=\"editor-paragraph\"><br></p>' +\n        '</div>',\n      expectedSelection: {\n        anchorOffset: 0,\n        anchorPath: [1, 1, 0],\n        focusOffset: 0,\n        focusPath: [2],\n      },\n      inputs: [\n        insertParagraph(),\n        insertText('Hello world'),\n        insertParagraph(),\n        moveNativeSelection([1, 0, 0], 6, [2], 0),\n        formatBold(),\n      ],\n      name: 'Format selection that starts on text and ends on element and retain selection',\n    },\n    {\n      expectedHTML:\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\">' +\n        '<p class=\"editor-paragraph\"><br></p>' +\n        '<p class=\"editor-paragraph\">' +\n        '<strong class=\"editor-text-bold\" data-lexical-text=\"true\">Hello</strong>' +\n        '<span data-lexical-text=\"true\"> world</span>' +\n        '</p>' +\n        '<p class=\"editor-paragraph\"><br></p>' +\n        '</div>',\n      expectedSelection: {\n        anchorOffset: 0,\n        anchorPath: [0],\n        focusOffset: 5,\n        focusPath: [1, 0, 0],\n      },\n      inputs: [\n        insertParagraph(),\n        insertText('Hello world'),\n        insertParagraph(),\n        moveNativeSelection([0], 0, [1, 0, 0], 5),\n        formatBold(),\n      ],\n      name: 'Format selection that starts on element and ends on text and retain selection',\n    },\n\n    {\n      expectedHTML:\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\">' +\n        '<p class=\"editor-paragraph\"><br></p>' +\n        '<p class=\"editor-paragraph\">' +\n        '<strong class=\"editor-text-bold\" data-lexical-text=\"true\">Hello</strong><strong class=\"editor-text-bold\" data-lexical-text=\"true\"> world</strong>' +\n        '</p>' +\n        '<p class=\"editor-paragraph\"><br></p>' +\n        '</div>',\n      expectedSelection: {\n        anchorOffset: 2,\n        anchorPath: [1, 0, 0],\n        focusOffset: 0,\n        focusPath: [2],\n      },\n      inputs: [\n        insertParagraph(),\n        insertTokenNode('Hello'),\n        insertText(' world'),\n        insertParagraph(),\n        moveNativeSelection([1, 0, 0], 2, [2], 0),\n        formatBold(),\n      ],\n      name: 'Format selection that starts on middle of token node should format complete node',\n    },\n\n    {\n      expectedHTML:\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\">' +\n        '<p class=\"editor-paragraph\"><br></p>' +\n        '<p class=\"editor-paragraph\">' +\n        '<strong class=\"editor-text-bold\" data-lexical-text=\"true\">Hello </strong><strong class=\"editor-text-bold\" data-lexical-text=\"true\">world</strong>' +\n        '</p>' +\n        '<p class=\"editor-paragraph\"><br></p>' +\n        '</div>',\n      expectedSelection: {\n        anchorOffset: 0,\n        anchorPath: [0],\n        focusOffset: 2,\n        focusPath: [1, 1, 0],\n      },\n      inputs: [\n        insertParagraph(),\n        insertText('Hello '),\n        insertTokenNode('world'),\n        insertParagraph(),\n        moveNativeSelection([0], 0, [1, 1, 0], 2),\n        formatBold(),\n      ],\n      name: 'Format selection that ends on middle of token node should format complete node',\n    },\n\n    {\n      expectedHTML:\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\">' +\n        '<p class=\"editor-paragraph\"><br></p>' +\n        '<p class=\"editor-paragraph\">' +\n        '<strong class=\"editor-text-bold\" data-lexical-text=\"true\">Hello</strong><span data-lexical-text=\"true\"> world</span>' +\n        '</p>' +\n        '<p class=\"editor-paragraph\"><br></p>' +\n        '</div>',\n      expectedSelection: {\n        anchorOffset: 2,\n        anchorPath: [1, 0, 0],\n        focusOffset: 3,\n        focusPath: [1, 0, 0],\n      },\n      inputs: [\n        insertParagraph(),\n        insertTokenNode('Hello'),\n        insertText(' world'),\n        insertParagraph(),\n        moveNativeSelection([1, 0, 0], 2, [1, 0, 0], 3),\n        formatBold(),\n      ],\n      name: 'Format token node if it is the single one selected',\n    },\n\n    {\n      expectedHTML:\n        '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\">' +\n        '<p class=\"editor-paragraph\"><br></p>' +\n        '<p class=\"editor-paragraph\">' +\n        '<strong class=\"editor-text-bold\" data-lexical-text=\"true\">Hello </strong><strong class=\"editor-text-bold\" data-lexical-text=\"true\">beautiful</strong><strong class=\"editor-text-bold\" data-lexical-text=\"true\"> world</strong>' +\n        '</p>' +\n        '<p class=\"editor-paragraph\"><br></p>' +\n        '</div>',\n      expectedSelection: {\n        anchorOffset: 0,\n        anchorPath: [0],\n        focusOffset: 0,\n        focusPath: [2],\n      },\n      inputs: [\n        insertParagraph(),\n        insertText('Hello '),\n        insertTokenNode('beautiful'),\n        insertText(' world'),\n        insertParagraph(),\n        moveNativeSelection([0], 0, [2], 0),\n        formatBold(),\n      ],\n      name: 'Format selection that contains a token node in the middle should format the token node',\n    },\n\n    ...[\n      {\n        whitespaceCharacter: ' ',\n        whitespaceName: 'space',\n      },\n      {\n        whitespaceCharacter: '\\u00a0',\n        whitespaceName: 'non-breaking space',\n      },\n      {\n        whitespaceCharacter: '\\u2000',\n        whitespaceName: 'en quad',\n      },\n      {\n        whitespaceCharacter: '\\u2001',\n        whitespaceName: 'em quad',\n      },\n      {\n        whitespaceCharacter: '\\u2002',\n        whitespaceName: 'en space',\n      },\n      {\n        whitespaceCharacter: '\\u2003',\n        whitespaceName: 'em space',\n      },\n      {\n        whitespaceCharacter: '\\u2004',\n        whitespaceName: 'three-per-em space',\n      },\n      {\n        whitespaceCharacter: '\\u2005',\n        whitespaceName: 'four-per-em space',\n      },\n      {\n        whitespaceCharacter: '\\u2006',\n        whitespaceName: 'six-per-em space',\n      },\n      {\n        whitespaceCharacter: '\\u2007',\n        whitespaceName: 'figure space',\n      },\n      {\n        whitespaceCharacter: '\\u2008',\n        whitespaceName: 'punctuation space',\n      },\n      {\n        whitespaceCharacter: '\\u2009',\n        whitespaceName: 'thin space',\n      },\n      {\n        whitespaceCharacter: '\\u200A',\n        whitespaceName: 'hair space',\n      },\n    ].flatMap(({whitespaceCharacter, whitespaceName}) => [\n      {\n        expectedHTML: `<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">Hello${printWhitespace(\n          whitespaceCharacter,\n        )}</span></p></div>`,\n        expectedSelection: {\n          anchorOffset: 6,\n          anchorPath: [0, 0, 0],\n          focusOffset: 6,\n          focusPath: [0, 0, 0],\n        },\n        inputs: [\n          insertText(`Hello${whitespaceCharacter}world`),\n          deleteWordBackward(1),\n        ],\n        name: `Type two words separated by a ${whitespaceName}, delete word backward from end`,\n      },\n      {\n        expectedHTML: `<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">${printWhitespace(\n          whitespaceCharacter,\n        )}world</span></p></div>`,\n        expectedSelection: {\n          anchorOffset: 0,\n          anchorPath: [0, 0, 0],\n          focusOffset: 0,\n          focusPath: [0, 0, 0],\n        },\n        inputs: [\n          insertText(`Hello${whitespaceCharacter}world`),\n          moveNativeSelection([0, 0, 0], 0, [0, 0, 0], 0),\n          deleteWordForward(1),\n        ],\n        name: `Type two words separated by a ${whitespaceName}, delete word forward from beginning`,\n      },\n      {\n        expectedHTML:\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">Hello</span></p></div>',\n        expectedSelection: {\n          anchorOffset: 5,\n          anchorPath: [0, 0, 0],\n          focusOffset: 5,\n          focusPath: [0, 0, 0],\n        },\n        inputs: [\n          insertText(`Hello${whitespaceCharacter}world`),\n          moveNativeSelection([0, 0, 0], 5, [0, 0, 0], 5),\n          deleteWordForward(1),\n        ],\n        name: `Type two words separated by a ${whitespaceName}, delete word forward from beginning of preceding whitespace`,\n      },\n      {\n        expectedHTML:\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">world</span></p></div>',\n        expectedSelection: {\n          anchorOffset: 0,\n          anchorPath: [0, 0, 0],\n          focusOffset: 0,\n          focusPath: [0, 0, 0],\n        },\n        inputs: [\n          insertText(`Hello${whitespaceCharacter}world`),\n          moveNativeSelection([0, 0, 0], 6, [0, 0, 0], 6),\n          deleteWordBackward(1),\n        ],\n        name: `Type two words separated by a ${whitespaceName}, delete word backward from end of trailing whitespace`,\n      },\n      {\n        expectedHTML:\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">Hello world</span></p></div>',\n        expectedSelection: {\n          anchorOffset: 11,\n          anchorPath: [0, 0, 0],\n          focusOffset: 11,\n          focusPath: [0, 0, 0],\n        },\n        inputs: [insertText('Hello world'), deleteWordBackward(1), undo(1)],\n        name: `Type a word, delete it and undo the deletion`,\n      },\n      {\n        expectedHTML:\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">Hello </span></p></div>',\n        expectedSelection: {\n          anchorOffset: 6,\n          anchorPath: [0, 0, 0],\n          focusOffset: 6,\n          focusPath: [0, 0, 0],\n        },\n        inputs: [\n          insertText('Hello world'),\n          deleteWordBackward(1),\n          undo(1),\n          redo(1),\n        ],\n        name: `Type a word, delete it and undo the deletion`,\n      },\n      {\n        expectedHTML:\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\">' +\n          '<span data-lexical-text=\"true\">this is weird test</span></p></div>',\n        expectedSelection: {\n          anchorOffset: 0,\n          anchorPath: [0, 0, 0],\n          focusOffset: 0,\n          focusPath: [0, 0, 0],\n        },\n        inputs: [\n          insertText('this is weird test'),\n          moveNativeSelection([0, 0, 0], 14, [0, 0, 0], 14),\n          moveBackward(14),\n        ],\n        name: 'Type a sentence, move the caret to the middle and move with the arrows to the start',\n      },\n      {\n        expectedHTML:\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\">' +\n          '<span data-lexical-text=\"true\">Hello </span>' +\n          '<span data-lexical-text=\"true\">Bob</span>' +\n          '</p></div>',\n        expectedSelection: {\n          anchorOffset: 3,\n          anchorPath: [0, 1, 0],\n          focusOffset: 3,\n          focusPath: [0, 1, 0],\n        },\n        inputs: [\n          insertText('Hello '),\n          insertTokenNode('Bob'),\n          moveBackward(1),\n          moveBackward(1),\n          moveEnd(),\n        ],\n        name: 'Type a text and token text, move the caret to the end of the first text',\n      },\n      {\n        expectedHTML:\n          '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">ABD</span><span data-lexical-text=\"true\">\\t</span><span data-lexical-text=\"true\">EFG</span></p></div>',\n        expectedSelection: {\n          anchorOffset: 3,\n          anchorPath: [0, 0, 0],\n          focusOffset: 3,\n          focusPath: [0, 0, 0],\n        },\n        inputs: [\n          pastePlain('ABD\\tEFG'),\n          moveBackward(5),\n          insertText('C'),\n          moveBackward(1),\n          deleteWordForward(1),\n        ],\n        name: 'Paste text, move selection and delete word forward',\n      },\n    ]),\n  ];\n\n  suite.forEach((testUnit, i) => {\n    const name = testUnit.name || 'Test case';\n\n    test(name + ` (#${i + 1})`, async () => {\n      await applySelectionInputs(testUnit.inputs, update, editor!);\n\n      // Validate HTML matches\n      expect(container.innerHTML).toBe(testUnit.expectedHTML);\n\n      // Validate selection matches\n      const rootElement = editor!.getRootElement()!;\n      const expectedSelection = testUnit.expectedSelection;\n\n      assertSelection(rootElement, expectedSelection);\n    });\n  });\n\n  test('insert text one selected node element selection', async () => {\n    await editor!.update(() => {\n      const root = $getRoot();\n\n      const paragraph = root.getFirstChild<ParagraphNode>()!;\n\n      const elementNode = $createTestElementNode();\n      const text = $createTextNode('foo');\n\n      paragraph.append(elementNode);\n      elementNode.append(text);\n\n      const selection = $createRangeSelection();\n      selection.anchor.set(text.__key, 0, 'text');\n      selection.focus.set(paragraph.__key, 1, 'element');\n\n      selection.insertText('');\n\n      expect(root.getTextContent()).toBe('');\n    });\n  });\n\n  test('getNodes resolves nested block nodes', async () => {\n    await editor!.update(() => {\n      const root = $getRoot();\n\n      const paragraph = root.getFirstChild<ParagraphNode>()!;\n\n      const elementNode = $createTestElementNode();\n      const text = $createTextNode();\n\n      paragraph.append(elementNode);\n      elementNode.append(text);\n\n      const selectedNodes = $getSelection()!.getNodes();\n\n      expect(selectedNodes.length).toBe(1);\n      expect(selectedNodes[0].getKey()).toBe(text.getKey());\n    });\n  });\n\n  describe('Block selection moves when new nodes are inserted', () => {\n    const baseCases: {\n      name: string;\n      anchorOffset: number;\n      focusOffset: number;\n      fn: (\n        paragraph: ElementNode,\n        text: TextNode,\n      ) => {\n        expectedAnchor: LexicalNode;\n        expectedAnchorOffset: number;\n        expectedFocus: LexicalNode;\n        expectedFocusOffset: number;\n      };\n      fnBefore?: (paragraph: ElementNode, text: TextNode) => void;\n      invertSelection?: true;\n      only?: true;\n    }[] = [\n      // Collapsed selection on end; add/remove/replace beginning\n      {\n        anchorOffset: 2,\n        fn: (paragraph, text) => {\n          const newText = $createTextNode('2');\n          text.insertBefore(newText);\n\n          return {\n            expectedAnchor: paragraph,\n            expectedAnchorOffset: 3,\n            expectedFocus: paragraph,\n            expectedFocusOffset: 3,\n          };\n        },\n        focusOffset: 2,\n        name: 'insertBefore - Collapsed selection on end; add beginning',\n      },\n      {\n        anchorOffset: 2,\n        fn: (paragraph, text) => {\n          const newText = $createTextNode('2');\n          text.insertAfter(newText);\n\n          return {\n            expectedAnchor: paragraph,\n            expectedAnchorOffset: 3,\n            expectedFocus: paragraph,\n            expectedFocusOffset: 3,\n          };\n        },\n        focusOffset: 2,\n        name: 'insertAfter - Collapsed selection on end; add beginning',\n      },\n      {\n        anchorOffset: 2,\n        fn: (paragraph, text) => {\n          text.splitText(1);\n\n          return {\n            expectedAnchor: paragraph,\n            expectedAnchorOffset: 3,\n            expectedFocus: paragraph,\n            expectedFocusOffset: 3,\n          };\n        },\n        focusOffset: 2,\n        name: 'splitText - Collapsed selection on end; add beginning',\n      },\n      {\n        anchorOffset: 1,\n        fn: (paragraph, text) => {\n          text.remove();\n\n          return {\n            expectedAnchor: paragraph,\n            expectedAnchorOffset: 0,\n            expectedFocus: paragraph,\n            expectedFocusOffset: 0,\n          };\n        },\n        focusOffset: 1,\n        name: 'remove - Collapsed selection on end; add beginning',\n      },\n      {\n        anchorOffset: 1,\n        fn: (paragraph, text) => {\n          const newText = $createTextNode('replacement');\n          text.replace(newText);\n\n          return {\n            expectedAnchor: paragraph,\n            expectedAnchorOffset: 1,\n            expectedFocus: paragraph,\n            expectedFocusOffset: 1,\n          };\n        },\n        focusOffset: 1,\n        name: 'replace - Collapsed selection on end; replace beginning',\n      },\n      // All selected; add/remove/replace on beginning\n      {\n        anchorOffset: 0,\n        fn: (paragraph, text) => {\n          const newText = $createTextNode('2');\n          text.insertBefore(newText);\n\n          return {\n            expectedAnchor: text,\n            expectedAnchorOffset: 0,\n            expectedFocus: paragraph,\n            expectedFocusOffset: 3,\n          };\n        },\n        focusOffset: 2,\n        name: 'insertBefore - All selected; add on beginning',\n      },\n      {\n        anchorOffset: 0,\n        fn: (paragraph, originalText) => {\n          const [, text] = originalText.splitText(1);\n\n          return {\n            expectedAnchor: text,\n            expectedAnchorOffset: 0,\n            expectedFocus: paragraph,\n            expectedFocusOffset: 3,\n          };\n        },\n        focusOffset: 2,\n        name: 'splitNodes - All selected; add on beginning',\n      },\n      {\n        anchorOffset: 0,\n        fn: (paragraph, text) => {\n          text.remove();\n\n          return {\n            expectedAnchor: paragraph,\n            expectedAnchorOffset: 0,\n            expectedFocus: paragraph,\n            expectedFocusOffset: 0,\n          };\n        },\n        focusOffset: 1,\n        name: 'remove - All selected; remove on beginning',\n      },\n      {\n        anchorOffset: 0,\n        fn: (paragraph, text) => {\n          const newText = $createTextNode('replacement');\n          text.replace(newText);\n\n          return {\n            expectedAnchor: paragraph,\n            expectedAnchorOffset: 0,\n            expectedFocus: paragraph,\n            expectedFocusOffset: 1,\n          };\n        },\n        focusOffset: 1,\n        name: 'replace - All selected; replace on beginning',\n      },\n      // Selection beginning; add/remove/replace on end\n      {\n        anchorOffset: 0,\n        fn: (paragraph, originalText1) => {\n          const originalText2 = originalText1.getPreviousSibling()!;\n          const lastChild = paragraph.getLastChild()!;\n          const newText = $createTextNode('2');\n          lastChild.insertBefore(newText);\n\n          return {\n            expectedAnchor: originalText2,\n            expectedAnchorOffset: 0,\n            expectedFocus: originalText1,\n            expectedFocusOffset: 0,\n          };\n        },\n        fnBefore: (paragraph, originalText1) => {\n          const originalText2 = $createTextNode('bar');\n          originalText1.insertBefore(originalText2);\n        },\n        focusOffset: 1,\n        name: 'insertBefore - Selection beginning; add on end',\n      },\n      {\n        anchorOffset: 0,\n        fn: (paragraph, text) => {\n          const lastChild = paragraph.getLastChild()!;\n          const newText = $createTextNode('2');\n          lastChild.insertAfter(newText);\n\n          return {\n            expectedAnchor: text,\n            expectedAnchorOffset: 0,\n            expectedFocus: paragraph,\n            expectedFocusOffset: 1,\n          };\n        },\n        focusOffset: 1,\n        name: 'insertAfter - Selection beginning; add on end',\n      },\n      {\n        anchorOffset: 0,\n        fn: (paragraph, originalText1) => {\n          const originalText2 = originalText1.getPreviousSibling()!;\n          const [, text] = originalText1.splitText(1);\n\n          return {\n            expectedAnchor: originalText2,\n            expectedAnchorOffset: 0,\n            expectedFocus: text,\n            expectedFocusOffset: 0,\n          };\n        },\n        fnBefore: (paragraph, originalText1) => {\n          const originalText2 = $createTextNode('bar');\n          originalText1.insertBefore(originalText2);\n        },\n        focusOffset: 1,\n        name: 'splitText - Selection beginning; add on end',\n      },\n      {\n        anchorOffset: 0,\n        fn: (paragraph, text) => {\n          const lastChild = paragraph.getLastChild()!;\n          lastChild.remove();\n\n          return {\n            expectedAnchor: text,\n            expectedAnchorOffset: 0,\n            expectedFocus: text,\n            expectedFocusOffset: 3,\n          };\n        },\n        focusOffset: 1,\n        name: 'remove - Selection beginning; remove on end',\n      },\n      {\n        anchorOffset: 0,\n        fn: (paragraph, text) => {\n          const newText = $createTextNode('replacement');\n          const lastChild = paragraph.getLastChild()!;\n          lastChild.replace(newText);\n\n          return {\n            expectedAnchor: paragraph,\n            expectedAnchorOffset: 0,\n            expectedFocus: paragraph,\n            expectedFocusOffset: 1,\n          };\n        },\n        focusOffset: 1,\n        name: 'replace - Selection beginning; replace on end',\n      },\n      // All selected; add/remove/replace in end offset [1, 2] -> [1, N, 2]\n      {\n        anchorOffset: 0,\n        fn: (paragraph, text) => {\n          const lastChild = paragraph.getLastChild()!;\n          const newText = $createTextNode('2');\n          lastChild.insertBefore(newText);\n\n          return {\n            expectedAnchor: text,\n            expectedAnchorOffset: 0,\n            expectedFocus: paragraph,\n            expectedFocusOffset: 2,\n          };\n        },\n        focusOffset: 1,\n        name: 'insertBefore - All selected; add in end offset',\n      },\n      {\n        anchorOffset: 0,\n        fn: (paragraph, text) => {\n          const newText = $createTextNode('2');\n          text.insertAfter(newText);\n\n          return {\n            expectedAnchor: text,\n            expectedAnchorOffset: 0,\n            expectedFocus: paragraph,\n            expectedFocusOffset: 2,\n          };\n        },\n        focusOffset: 1,\n        name: 'insertAfter - All selected; add in end offset',\n      },\n      {\n        anchorOffset: 0,\n        fn: (paragraph, originalText1) => {\n          const originalText2 = originalText1.getPreviousSibling()!;\n          const [, text] = originalText1.splitText(1);\n\n          return {\n            expectedAnchor: originalText2,\n            expectedAnchorOffset: 0,\n            expectedFocus: text,\n            expectedFocusOffset: 0,\n          };\n        },\n        fnBefore: (paragraph, originalText1) => {\n          const originalText2 = $createTextNode('bar');\n          originalText1.insertBefore(originalText2);\n        },\n        focusOffset: 1,\n        name: 'splitText - All selected; add in end offset',\n      },\n      {\n        anchorOffset: 1,\n        fn: (paragraph, originalText1) => {\n          const lastChild = paragraph.getLastChild()!;\n          lastChild.remove();\n\n          return {\n            expectedAnchor: originalText1,\n            expectedAnchorOffset: 0,\n            expectedFocus: originalText1,\n            expectedFocusOffset: 3,\n          };\n        },\n        fnBefore: (paragraph, originalText1) => {\n          const originalText2 = $createTextNode('bar');\n          originalText1.insertBefore(originalText2);\n        },\n        focusOffset: 2,\n        name: 'remove - All selected; remove in end offset',\n      },\n      {\n        anchorOffset: 1,\n        fn: (paragraph, originalText1) => {\n          const newText = $createTextNode('replacement');\n          const lastChild = paragraph.getLastChild()!;\n          lastChild.replace(newText);\n\n          return {\n            expectedAnchor: paragraph,\n            expectedAnchorOffset: 1,\n            expectedFocus: paragraph,\n            expectedFocusOffset: 2,\n          };\n        },\n        fnBefore: (paragraph, originalText1) => {\n          const originalText2 = $createTextNode('bar');\n          originalText1.insertBefore(originalText2);\n        },\n        focusOffset: 2,\n        name: 'replace - All selected; replace in end offset',\n      },\n      // All selected; add/remove/replace in middle [1, 2, 3] -> [1, 2, N, 3]\n      {\n        anchorOffset: 0,\n        fn: (paragraph, originalText1) => {\n          const originalText2 = originalText1.getPreviousSibling()!;\n          const lastChild = paragraph.getLastChild()!;\n          const newText = $createTextNode('2');\n          lastChild.insertBefore(newText);\n\n          return {\n            expectedAnchor: originalText2,\n            expectedAnchorOffset: 0,\n            expectedFocus: paragraph,\n            expectedFocusOffset: 3,\n          };\n        },\n        fnBefore: (paragraph, originalText1) => {\n          const originalText2 = $createTextNode('bar');\n          originalText1.insertBefore(originalText2);\n        },\n        focusOffset: 2,\n        name: 'insertBefore - All selected; add in middle',\n      },\n      {\n        anchorOffset: 0,\n        fn: (paragraph, originalText1) => {\n          const originalText2 = originalText1.getPreviousSibling()!;\n          const newText = $createTextNode('2');\n          originalText1.insertAfter(newText);\n\n          return {\n            expectedAnchor: originalText2,\n            expectedAnchorOffset: 0,\n            expectedFocus: paragraph,\n            expectedFocusOffset: 3,\n          };\n        },\n        fnBefore: (paragraph, originalText1) => {\n          const originalText2 = $createTextNode('bar');\n          originalText1.insertBefore(originalText2);\n        },\n        focusOffset: 2,\n        name: 'insertAfter - All selected; add in middle',\n      },\n      {\n        anchorOffset: 0,\n        fn: (paragraph, originalText1) => {\n          const originalText2 = originalText1.getPreviousSibling()!;\n          originalText1.splitText(1);\n\n          return {\n            expectedAnchor: originalText2,\n            expectedAnchorOffset: 0,\n            expectedFocus: paragraph,\n            expectedFocusOffset: 3,\n          };\n        },\n        fnBefore: (paragraph, originalText1) => {\n          const originalText2 = $createTextNode('bar');\n          originalText1.insertBefore(originalText2);\n        },\n        focusOffset: 2,\n        name: 'splitText - All selected; add in middle',\n      },\n      {\n        anchorOffset: 0,\n        fn: (paragraph, originalText1) => {\n          const originalText2 = originalText1.getPreviousSibling()!;\n          originalText1.remove();\n\n          return {\n            expectedAnchor: originalText2,\n            expectedAnchorOffset: 0,\n            expectedFocus: paragraph,\n            expectedFocusOffset: 1,\n          };\n        },\n        fnBefore: (paragraph, originalText1) => {\n          const originalText2 = $createTextNode('bar');\n          originalText1.insertBefore(originalText2);\n        },\n        focusOffset: 2,\n        name: 'remove - All selected; remove in middle',\n      },\n      {\n        anchorOffset: 0,\n        fn: (paragraph, originalText1) => {\n          const newText = $createTextNode('replacement');\n          originalText1.replace(newText);\n\n          return {\n            expectedAnchor: paragraph,\n            expectedAnchorOffset: 0,\n            expectedFocus: paragraph,\n            expectedFocusOffset: 2,\n          };\n        },\n        fnBefore: (paragraph, originalText1) => {\n          const originalText2 = $createTextNode('bar');\n          originalText1.insertBefore(originalText2);\n        },\n        focusOffset: 2,\n        name: 'replace - All selected; replace in middle',\n      },\n      // Edge cases\n      {\n        anchorOffset: 3,\n        fn: (paragraph, originalText1) => {\n          const originalText2 = paragraph.getLastChild()!;\n          const newText = $createTextNode('new');\n          originalText1.insertBefore(newText);\n\n          return {\n            expectedAnchor: originalText2,\n            expectedAnchorOffset: 'bar'.length,\n            expectedFocus: originalText2,\n            expectedFocusOffset: 'bar'.length,\n          };\n        },\n        fnBefore: (paragraph, originalText1) => {\n          const originalText2 = $createTextNode('bar');\n          paragraph.append(originalText2);\n        },\n        focusOffset: 3,\n        name: \"Selection resolves to the end of text node when it's at the end (1)\",\n      },\n      {\n        anchorOffset: 0,\n        fn: (paragraph, originalText1) => {\n          const originalText2 = paragraph.getLastChild()!;\n          const newText = $createTextNode('new');\n          originalText1.insertBefore(newText);\n\n          return {\n            expectedAnchor: originalText1,\n            expectedAnchorOffset: 0,\n            expectedFocus: originalText2,\n            expectedFocusOffset: 'bar'.length,\n          };\n        },\n        fnBefore: (paragraph, originalText1) => {\n          const originalText2 = $createTextNode('bar');\n          paragraph.append(originalText2);\n        },\n        focusOffset: 3,\n        name: \"Selection resolves to the end of text node when it's at the end (2)\",\n      },\n      {\n        anchorOffset: 1,\n        fn: (paragraph, originalText1) => {\n          originalText1.getNextSibling()!.remove();\n\n          return {\n            expectedAnchor: originalText1,\n            expectedAnchorOffset: 3,\n            expectedFocus: originalText1,\n            expectedFocusOffset: 3,\n          };\n        },\n        focusOffset: 1,\n        name: 'remove - Remove with collapsed selection at offset #4221',\n      },\n      {\n        anchorOffset: 0,\n        fn: (paragraph, originalText1) => {\n          originalText1.getNextSibling()!.remove();\n\n          return {\n            expectedAnchor: originalText1,\n            expectedAnchorOffset: 0,\n            expectedFocus: originalText1,\n            expectedFocusOffset: 3,\n          };\n        },\n        focusOffset: 1,\n        name: 'remove - Remove with non-collapsed selection at offset',\n      },\n    ];\n    baseCases\n      .flatMap((testCase) => {\n        // Test inverse selection\n        const inverse = {\n          ...testCase,\n          anchorOffset: testCase.focusOffset,\n          focusOffset: testCase.anchorOffset,\n          invertSelection: true,\n          name: testCase.name + ' (inverse selection)',\n        };\n        return [testCase, inverse];\n      })\n      .forEach(\n        ({\n          name,\n          fn,\n          fnBefore = () => {\n            return;\n          },\n          anchorOffset,\n          focusOffset,\n          invertSelection,\n          only,\n        }) => {\n          // eslint-disable-next-line no-only-tests/no-only-tests\n          const test_ = only === true ? test.only : test;\n          test_(name, async () => {\n            await editor!.update(() => {\n              const root = $getRoot();\n\n              const paragraph = root.getFirstChild<ParagraphNode>()!;\n              const textNode = $createTextNode('foo');\n              // Note: line break can't be selected by the DOM\n              const linebreak = $createLineBreakNode();\n\n              const selection = $getSelection();\n\n              if (!$isRangeSelection(selection)) {\n                return;\n              }\n\n              const anchor = selection.anchor;\n              const focus = selection.focus;\n\n              paragraph.append(textNode, linebreak);\n\n              fnBefore(paragraph, textNode);\n\n              anchor.set(paragraph.getKey(), anchorOffset, 'element');\n              focus.set(paragraph.getKey(), focusOffset, 'element');\n\n              const {\n                expectedAnchor,\n                expectedAnchorOffset,\n                expectedFocus,\n                expectedFocusOffset,\n              } = fn(paragraph, textNode);\n\n              if (invertSelection !== true) {\n                expect(selection.anchor.key).toBe(expectedAnchor.__key);\n                expect(selection.anchor.offset).toBe(expectedAnchorOffset);\n                expect(selection.focus.key).toBe(expectedFocus.__key);\n                expect(selection.focus.offset).toBe(expectedFocusOffset);\n              } else {\n                expect(selection.anchor.key).toBe(expectedFocus.__key);\n                expect(selection.anchor.offset).toBe(expectedFocusOffset);\n                expect(selection.focus.key).toBe(expectedAnchor.__key);\n                expect(selection.focus.offset).toBe(expectedAnchorOffset);\n              }\n            });\n          });\n        },\n      );\n  });\n\n  describe('Selection correctly resolves to a sibling ElementNode when a node is removed', () => {\n    test('', async () => {\n      await editor!.update(() => {\n        const root = $getRoot();\n\n        const listNode = $createListNode('bullet');\n        const listItemNode = $createListItemNode();\n        const paragraph = $createParagraphNode();\n\n        root.append(listNode);\n\n        listNode.append(listItemNode);\n        listItemNode.select();\n        listNode.insertAfter(paragraph);\n        listItemNode.remove();\n\n        const selection = $getSelection();\n\n        if (!$isRangeSelection(selection)) {\n          return;\n        }\n\n        expect(selection.anchor.getNode().__type).toBe('paragraph');\n        expect(selection.focus.getNode().__type).toBe('paragraph');\n      });\n    });\n  });\n\n  describe('Selection correctly resolves to a sibling ElementNode when a selected node child is removed', () => {\n    test('', async () => {\n      let paragraphNodeKey: string;\n      await editor!.update(() => {\n        const root = $getRoot();\n\n        const paragraphNode = $createParagraphNode();\n        paragraphNodeKey = paragraphNode.__key;\n        const listNode = $createListNode('number');\n        const listItemNode1 = $createListItemNode();\n        const textNode1 = $createTextNode('foo');\n        const listItemNode2 = $createListItemNode();\n        const listNode2 = $createListNode('number');\n        const listItemNode2x1 = $createListItemNode();\n\n        listNode.append(listItemNode1, listItemNode2);\n        listItemNode1.append(textNode1);\n        listItemNode2.append(listNode2);\n        listNode2.append(listItemNode2x1);\n        root.append(paragraphNode, listNode);\n\n        listItemNode2.select();\n\n        listNode.remove();\n      });\n      await editor!.getEditorState().read(() => {\n        const selection = $assertRangeSelection($getSelection());\n        expect(selection.anchor.key).toBe(paragraphNodeKey);\n        expect(selection.focus.key).toBe(paragraphNodeKey);\n      });\n    });\n  });\n\n  describe('Selection correctly resolves to a sibling ElementNode that has multiple children with the correct offset when a node is removed', () => {\n    test('', async () => {\n      await editor!.update(() => {\n        // Arrange\n        // Root\n        //  |- Paragraph\n        //    |- Link\n        //      |- Text\n        //      |- LineBreak\n        //      |- Text\n        //    |- Text\n        const root = $getRoot();\n\n        const paragraph = $createParagraphNode();\n        const link = $createLinkNode('bullet');\n        const textOne = $createTextNode('Hello');\n        const br = $createLineBreakNode();\n        const textTwo = $createTextNode('world');\n        const textThree = $createTextNode(' ');\n\n        root.append(paragraph);\n        link.append(textOne);\n        link.append(br);\n        link.append(textTwo);\n\n        paragraph.append(link);\n        paragraph.append(textThree);\n\n        textThree.select();\n        // Act\n        textThree.remove();\n        // Assert\n        const expectedKey = link.getKey();\n\n        const selection = $getSelection();\n\n        if (!$isRangeSelection(selection)) {\n          return;\n        }\n\n        const {anchor, focus} = selection;\n\n        expect(anchor.getNode().getKey()).toBe(expectedKey);\n        expect(focus.getNode().getKey()).toBe(expectedKey);\n        expect(anchor.offset).toBe(3);\n        expect(focus.offset).toBe(3);\n      });\n    });\n  });\n\n  test('isBackward', async () => {\n    await editor!.update(() => {\n      const root = $getRoot();\n\n      const paragraph = root.getFirstChild<ParagraphNode>()!;\n      const paragraphKey = paragraph.getKey();\n      const textNode = $createTextNode('foo');\n      const textNodeKey = textNode.getKey();\n      // Note: line break can't be selected by the DOM\n      const linebreak = $createLineBreakNode();\n\n      const selection = $getSelection();\n\n      if (!$isRangeSelection(selection)) {\n        return;\n      }\n\n      const anchor = selection.anchor;\n      const focus = selection.focus;\n      paragraph.append(textNode, linebreak);\n      anchor.set(textNodeKey, 0, 'text');\n      focus.set(textNodeKey, 0, 'text');\n\n      expect(selection.isBackward()).toBe(false);\n\n      anchor.set(paragraphKey, 1, 'element');\n      focus.set(paragraphKey, 1, 'element');\n\n      expect(selection.isBackward()).toBe(false);\n\n      anchor.set(paragraphKey, 0, 'element');\n      focus.set(paragraphKey, 1, 'element');\n\n      expect(selection.isBackward()).toBe(false);\n\n      anchor.set(paragraphKey, 1, 'element');\n      focus.set(paragraphKey, 0, 'element');\n\n      expect(selection.isBackward()).toBe(true);\n    });\n  });\n\n  describe('Decorator text content for selection', () => {\n    const baseCases: {\n      name: string;\n      fn: (opts: {\n        textNode1: TextNode;\n        textNode2: TextNode;\n        decorator: DecoratorNode<unknown>;\n        paragraph: ParagraphNode;\n        anchor: PointType;\n        focus: PointType;\n      }) => string;\n      invertSelection?: true;\n    }[] = [\n      {\n        fn: ({textNode1, anchor, focus}) => {\n          anchor.set(textNode1.getKey(), 1, 'text');\n          focus.set(textNode1.getKey(), 1, 'text');\n\n          return '';\n        },\n        name: 'Not included if cursor right before it',\n      },\n      {\n        fn: ({textNode2, anchor, focus}) => {\n          anchor.set(textNode2.getKey(), 0, 'text');\n          focus.set(textNode2.getKey(), 0, 'text');\n\n          return '';\n        },\n        name: 'Not included if cursor right after it',\n      },\n      {\n        fn: ({textNode1, textNode2, decorator, anchor, focus}) => {\n          anchor.set(textNode1.getKey(), 1, 'text');\n          focus.set(textNode2.getKey(), 0, 'text');\n\n          return decorator.getTextContent();\n        },\n        name: 'Included if decorator is selected within text',\n      },\n      {\n        fn: ({textNode1, textNode2, decorator, anchor, focus}) => {\n          anchor.set(textNode1.getKey(), 0, 'text');\n          focus.set(textNode2.getKey(), 0, 'text');\n\n          return textNode1.getTextContent() + decorator.getTextContent();\n        },\n        name: 'Included if decorator is selected with another node before it',\n      },\n      {\n        fn: ({textNode1, textNode2, decorator, anchor, focus}) => {\n          anchor.set(textNode1.getKey(), 1, 'text');\n          focus.set(textNode2.getKey(), 1, 'text');\n\n          return decorator.getTextContent() + textNode2.getTextContent();\n        },\n        name: 'Included if decorator is selected with another node after it',\n      },\n      {\n        fn: ({paragraph, textNode1, textNode2, decorator, anchor, focus}) => {\n          textNode1.remove();\n          textNode2.remove();\n          anchor.set(paragraph.getKey(), 0, 'element');\n          focus.set(paragraph.getKey(), 1, 'element');\n\n          return decorator.getTextContent();\n        },\n        name: 'Included if decorator is selected as the only node',\n      },\n    ];\n    baseCases\n      .flatMap((testCase) => {\n        const inverse = {\n          ...testCase,\n          invertSelection: true,\n          name: testCase.name + ' (inverse selection)',\n        };\n\n        return [testCase, inverse];\n      })\n      .forEach(({name, fn, invertSelection}) => {\n        it(name, async () => {\n          await editor!.update(() => {\n            const root = $getRoot();\n\n            const paragraph = root.getFirstChild<ParagraphNode>()!;\n            const textNode1 = $createTextNode('1');\n            const textNode2 = $createTextNode('2');\n            const decorator = $createTestDecoratorNode();\n\n            paragraph.append(textNode1, decorator, textNode2);\n\n            const selection = $getSelection();\n\n            if (!$isRangeSelection(selection)) {\n              return;\n            }\n\n            const expectedTextContent = fn({\n              anchor: invertSelection ? selection.focus : selection.anchor,\n              decorator,\n              focus: invertSelection ? selection.anchor : selection.focus,\n              paragraph,\n              textNode1,\n              textNode2,\n            });\n\n            expect(selection.getTextContent()).toBe(expectedTextContent);\n          });\n        });\n      });\n  });\n\n  describe('insertParagraph', () => {\n    test('three text nodes at offset 0 on third node', async () => {\n      const testEditor = createTestEditor();\n      const element = document.createElement('div');\n      testEditor.setRootElement(element);\n\n      await testEditor.update(() => {\n        const root = $getRoot();\n\n        const paragraph = $createParagraphNode();\n        const text = $createTextNode('Hello ');\n        const text2 = $createTextNode('awesome');\n\n        text2.toggleFormat('bold');\n\n        const text3 = $createTextNode(' world');\n\n        paragraph.append(text, text2, text3);\n        root.append(paragraph);\n\n        $setAnchorPoint({\n          key: text3.getKey(),\n          offset: 0,\n          type: 'text',\n        });\n\n        $setFocusPoint({\n          key: text3.getKey(),\n          offset: 0,\n          type: 'text',\n        });\n\n        const selection = $getSelection();\n\n        if (!$isRangeSelection(selection)) {\n          return;\n        }\n\n        selection.insertParagraph();\n      });\n\n      expect(element.innerHTML).toBe(\n        '<p><span data-lexical-text=\"true\">Hello </span><strong data-lexical-text=\"true\">awesome</strong></p><p><span data-lexical-text=\"true\"> world</span></p>',\n      );\n    });\n\n    test('four text nodes at offset 0 on third node', async () => {\n      const testEditor = createTestEditor();\n      const element = document.createElement('div');\n      testEditor.setRootElement(element);\n\n      await testEditor.update(() => {\n        const root = $getRoot();\n\n        const paragraph = $createParagraphNode();\n        const text = $createTextNode('Hello ');\n        const text2 = $createTextNode('awesome ');\n\n        text2.toggleFormat('bold');\n\n        const text3 = $createTextNode('beautiful');\n        const text4 = $createTextNode(' world');\n\n        text4.toggleFormat('bold');\n\n        paragraph.append(text, text2, text3, text4);\n        root.append(paragraph);\n\n        $setAnchorPoint({\n          key: text3.getKey(),\n          offset: 0,\n          type: 'text',\n        });\n\n        $setFocusPoint({\n          key: text3.getKey(),\n          offset: 0,\n          type: 'text',\n        });\n\n        const selection = $getSelection();\n\n        if (!$isRangeSelection(selection)) {\n          return;\n        }\n\n        selection.insertParagraph();\n      });\n\n      expect(element.innerHTML).toBe(\n        '<p><span data-lexical-text=\"true\">Hello </span><strong data-lexical-text=\"true\">awesome </strong></p><p><span data-lexical-text=\"true\">beautiful</span><strong data-lexical-text=\"true\"> world</strong></p>',\n      );\n    });\n\n    it('adjust offset for inline elements text formatting', async () => {\n      await init();\n\n      await editor!.update(() => {\n        const root = $getRoot();\n\n        const text1 = $createTextNode('--');\n        const text2 = $createTextNode('abc');\n        const text3 = $createTextNode('--');\n\n        root.append(\n            $createParagraphNode().append(\n                text1,\n                $createLinkNode('https://lexical.dev').append(text2),\n                text3,\n            ),\n        );\n\n        $setAnchorPoint({\n          key: text1.getKey(),\n          offset: 2,\n          type: 'text',\n        });\n\n        $setFocusPoint({\n          key: text3.getKey(),\n          offset: 0,\n          type: 'text',\n        });\n\n        const selection = $getSelection();\n\n        if (!$isRangeSelection(selection)) {\n          return;\n        }\n\n        selection.formatText('bold');\n\n        expect(text2.hasFormat('bold')).toBe(true);\n      });\n    });\n  });\n\n  describe('Node.replace', () => {\n    let text1: TextNode,\n      text2: TextNode,\n      text3: TextNode,\n      paragraph: ParagraphNode,\n      testEditor: LexicalEditor;\n\n    beforeEach(async () => {\n      testEditor = createTestEditor();\n\n      const element = document.createElement('div');\n      testEditor.setRootElement(element);\n\n      await testEditor.update(() => {\n        const root = $getRoot();\n\n        paragraph = $createParagraphNode();\n        text1 = $createTextNode('Hello ');\n        text2 = $createTextNode('awesome');\n\n        text2.toggleFormat('bold');\n\n        text3 = $createTextNode(' world');\n\n        paragraph.append(text1, text2, text3);\n        root.append(paragraph);\n      });\n    });\n    [\n      {\n        fn: () => {\n          text2.select(1, 1);\n          text2.replace($createTestDecoratorNode());\n\n          return {\n            key: text3.__key,\n            offset: 0,\n          };\n        },\n        name: 'moves selection to to next text node if replacing with decorator',\n      },\n      {\n        fn: () => {\n          text3.replace($createTestDecoratorNode());\n          text2.select(1, 1);\n          text2.replace($createTestDecoratorNode());\n\n          return {\n            key: paragraph.__key,\n            offset: 2,\n          };\n        },\n        name: 'moves selection to parent if next sibling is not a text node',\n      },\n    ].forEach((testCase) => {\n      test(testCase.name, async () => {\n        await testEditor.update(() => {\n          const {key, offset} = testCase.fn();\n\n          const selection = $getSelection();\n\n          if (!$isRangeSelection(selection)) {\n            return;\n          }\n\n          expect(selection.anchor.key).toBe(key);\n          expect(selection.anchor.offset).toBe(offset);\n          expect(selection.focus.key).toBe(key);\n          expect(selection.focus.offset).toBe(offset);\n        });\n      });\n    });\n  });\n\n  describe('Testing that $getStyleObjectFromRawCSS handles unformatted css text ', () => {\n    test('', async () => {\n      const testEditor = createTestEditor();\n      const element = document.createElement('div');\n      testEditor.setRootElement(element);\n\n      await testEditor.update(() => {\n        const root = $getRoot();\n        const paragraph = $createParagraphNode();\n        const textNode = $createTextNode('Hello, World!');\n        textNode.setStyle(\n          '   font-family  : Arial  ;  color    :   red   ;top     : 50px',\n        );\n        $addNodeStyle(textNode);\n        paragraph.append(textNode);\n        root.append(paragraph);\n\n        const selection = $createRangeSelection();\n        $setSelection(selection);\n        selection.insertParagraph();\n        $setAnchorPoint({\n          key: textNode.getKey(),\n          offset: 0,\n          type: 'text',\n        });\n\n        $setFocusPoint({\n          key: textNode.getKey(),\n          offset: 10,\n          type: 'text',\n        });\n\n        const cssFontFamilyValue = $getSelectionStyleValueForProperty(\n          selection,\n          'font-family',\n          '',\n        );\n        expect(cssFontFamilyValue).toBe('Arial');\n\n        const cssColorValue = $getSelectionStyleValueForProperty(\n          selection,\n          'color',\n          '',\n        );\n        expect(cssColorValue).toBe('red');\n\n        const cssTopValue = $getSelectionStyleValueForProperty(\n          selection,\n          'top',\n          '',\n        );\n        expect(cssTopValue).toBe('50px');\n      });\n    });\n  });\n\n  describe('Testing that getStyleObjectFromRawCSS handles values with colons', () => {\n    test('', async () => {\n      const testEditor = createTestEditor();\n      const element = document.createElement('div');\n      testEditor.setRootElement(element);\n\n      await testEditor.update(() => {\n        const root = $getRoot();\n        const paragraph = $createParagraphNode();\n        const textNode = $createTextNode('Hello, World!');\n        textNode.setStyle(\n          'font-family: double:prefix:Arial; color: color:white; font-size: 30px',\n        );\n        $addNodeStyle(textNode);\n        paragraph.append(textNode);\n        root.append(paragraph);\n\n        const selection = $createRangeSelection();\n        $setSelection(selection);\n        selection.insertParagraph();\n        $setAnchorPoint({\n          key: textNode.getKey(),\n          offset: 0,\n          type: 'text',\n        });\n\n        $setFocusPoint({\n          key: textNode.getKey(),\n          offset: 10,\n          type: 'text',\n        });\n\n        const cssFontFamilyValue = $getSelectionStyleValueForProperty(\n          selection,\n          'font-family',\n          '',\n        );\n        expect(cssFontFamilyValue).toBe('double:prefix:Arial');\n\n        const cssColorValue = $getSelectionStyleValueForProperty(\n          selection,\n          'color',\n          '',\n        );\n        expect(cssColorValue).toBe('color:white');\n\n        const cssFontSizeValue = $getSelectionStyleValueForProperty(\n          selection,\n          'font-size',\n          '',\n        );\n        expect(cssFontSizeValue).toBe('30px');\n      });\n    });\n  });\n\n  describe('$patchStyle', () => {\n    it('should patch the style with the new style object', async () => {\n      await editor!.update(() => {\n        const root = $getRoot();\n        const paragraph = $createParagraphNode();\n        const textNode = $createTextNode('Hello, World!');\n        textNode.setStyle('font-family: serif; color: red;');\n        $addNodeStyle(textNode);\n        paragraph.append(textNode);\n        root.append(paragraph);\n\n        const selection = $createRangeSelection();\n        $setSelection(selection);\n        selection.insertParagraph();\n        $setAnchorPoint({\n          key: textNode.getKey(),\n          offset: 0,\n          type: 'text',\n        });\n\n        $setFocusPoint({\n          key: textNode.getKey(),\n          offset: 10,\n          type: 'text',\n        });\n\n        const newStyle = {\n          color: 'blue',\n          'font-family': 'Arial',\n        };\n\n        $patchStyleText(selection, newStyle);\n\n        const cssFontFamilyValue = $getSelectionStyleValueForProperty(\n            selection,\n            'font-family',\n            '',\n        );\n        expect(cssFontFamilyValue).toBe('Arial');\n\n        const cssColorValue = $getSelectionStyleValueForProperty(\n            selection,\n            'color',\n            '',\n        );\n        expect(cssColorValue).toBe('blue');\n      });\n    });\n\n    it('should patch the style with property function', async () => {\n      await editor!.update(() => {\n        const currentColor = 'red';\n        const nextColor = 'blue';\n\n        const root = $getRoot();\n        const paragraph = $createParagraphNode();\n        const textNode = $createTextNode('Hello, World!');\n        textNode.setStyle(`color: ${currentColor};`);\n        $addNodeStyle(textNode);\n        paragraph.append(textNode);\n        root.append(paragraph);\n\n        const selection = $createRangeSelection();\n        $setSelection(selection);\n        selection.insertParagraph();\n        $setAnchorPoint({\n          key: textNode.getKey(),\n          offset: 0,\n          type: 'text',\n        });\n\n        $setFocusPoint({\n          key: textNode.getKey(),\n          offset: 10,\n          type: 'text',\n        });\n\n        const newStyle = {\n          color: jest.fn(\n              (current: string | null, target: LexicalNode | RangeSelection) =>\n                  nextColor,\n          ),\n        };\n\n        $patchStyleText(selection, newStyle);\n\n        const cssColorValue = $getSelectionStyleValueForProperty(\n            selection,\n            'color',\n            '',\n        );\n\n        expect(cssColorValue).toBe(nextColor);\n        expect(newStyle.color).toHaveBeenCalledTimes(1);\n\n        const lastCall = newStyle.color.mock.lastCall!;\n        expect(lastCall[0]).toBe(currentColor);\n        // @ts-ignore - It expected to be a LexicalNode\n        expect($isTextNode(lastCall[1])).toBeTruthy();\n      });\n    });\n  });\n\n  describe('$setBlocksType', () => {\n    test('Collapsed selection in text', async () => {\n      const testEditor = createTestEditor();\n      const element = document.createElement('div');\n      testEditor.setRootElement(element);\n\n      await testEditor.update(() => {\n        const root = $getRoot();\n        const paragraph1 = $createParagraphNode();\n        const text1 = $createTextNode('text 1');\n        const paragraph2 = $createParagraphNode();\n        const text2 = $createTextNode('text 2');\n        root.append(paragraph1, paragraph2);\n        paragraph1.append(text1);\n        paragraph2.append(text2);\n\n        const selection = $createRangeSelection();\n        $setSelection(selection);\n        $setAnchorPoint({\n          key: text1.__key,\n          offset: text1.__text.length,\n          type: 'text',\n        });\n        $setFocusPoint({\n          key: text1.__key,\n          offset: text1.__text.length,\n          type: 'text',\n        });\n\n        $setBlocksType(selection, () => {\n          return $createHeadingNode('h1');\n        });\n\n        const rootChildren = root.getChildren();\n        expect(rootChildren[0].__type).toBe('heading');\n        expect(rootChildren[1].__type).toBe('paragraph');\n        expect(rootChildren.length).toBe(2);\n      });\n    });\n\n    test('Collapsed selection in element', async () => {\n      const testEditor = createTestEditor();\n      const element = document.createElement('div');\n      testEditor.setRootElement(element);\n\n      await testEditor.update(() => {\n        const root = $getRoot();\n        const paragraph1 = $createParagraphNode();\n        const paragraph2 = $createParagraphNode();\n        root.append(paragraph1, paragraph2);\n\n        const selection = $createRangeSelection();\n        $setSelection(selection);\n        $setAnchorPoint({\n          key: 'root',\n          offset: 0,\n          type: 'element',\n        });\n        $setFocusPoint({\n          key: 'root',\n          offset: 0,\n          type: 'element',\n        });\n\n        $setBlocksType(selection, () => {\n          return $createHeadingNode('h1');\n        });\n\n        const rootChildren = root.getChildren();\n        expect(rootChildren[0].__type).toBe('heading');\n        expect(rootChildren[1].__type).toBe('paragraph');\n        expect(rootChildren.length).toBe(2);\n      });\n    });\n\n    test('Two elements, same top-element', async () => {\n      const testEditor = createTestEditor();\n      const element = document.createElement('div');\n      testEditor.setRootElement(element);\n\n      await testEditor.update(() => {\n        const root = $getRoot();\n        const paragraph1 = $createParagraphNode();\n        const text1 = $createTextNode('text 1');\n        const paragraph2 = $createParagraphNode();\n        const text2 = $createTextNode('text 2');\n        root.append(paragraph1, paragraph2);\n        paragraph1.append(text1);\n        paragraph2.append(text2);\n\n        const selection = $createRangeSelection();\n        $setSelection(selection);\n        $setAnchorPoint({\n          key: text1.__key,\n          offset: 0,\n          type: 'text',\n        });\n        $setFocusPoint({\n          key: text2.__key,\n          offset: text1.__text.length,\n          type: 'text',\n        });\n\n        $setBlocksType(selection, () => {\n          return $createHeadingNode('h1');\n        });\n\n        const rootChildren = root.getChildren();\n        expect(rootChildren[0].__type).toBe('heading');\n        expect(rootChildren[1].__type).toBe('heading');\n        expect(rootChildren.length).toBe(2);\n      });\n    });\n\n    test('Two empty elements, same top-element', async () => {\n      const testEditor = createTestEditor();\n      const element = document.createElement('div');\n      testEditor.setRootElement(element);\n\n      await testEditor.update(() => {\n        const root = $getRoot();\n        const paragraph1 = $createParagraphNode();\n        const paragraph2 = $createParagraphNode();\n        root.append(paragraph1, paragraph2);\n\n        const selection = $createRangeSelection();\n        $setSelection(selection);\n        $setAnchorPoint({\n          key: paragraph1.__key,\n          offset: 0,\n          type: 'element',\n        });\n        $setFocusPoint({\n          key: paragraph2.__key,\n          offset: 0,\n          type: 'element',\n        });\n\n        $setBlocksType(selection, () => {\n          return $createHeadingNode('h1');\n        });\n\n        const rootChildren = root.getChildren();\n        expect(rootChildren[0].__type).toBe('heading');\n        expect(rootChildren[1].__type).toBe('heading');\n        expect(rootChildren.length).toBe(2);\n        const sel = $getSelection()!;\n        expect(sel.getNodes().length).toBe(2);\n      });\n    });\n\n    test('Two elements, same top-element', async () => {\n      const testEditor = createTestEditor();\n      const element = document.createElement('div');\n      testEditor.setRootElement(element);\n\n      await testEditor.update(() => {\n        const root = $getRoot();\n        const paragraph1 = $createParagraphNode();\n        const text1 = $createTextNode('text 1');\n        const paragraph2 = $createParagraphNode();\n        const text2 = $createTextNode('text 2');\n        root.append(paragraph1, paragraph2);\n        paragraph1.append(text1);\n        paragraph2.append(text2);\n\n        const selection = $createRangeSelection();\n        $setSelection(selection);\n        $setAnchorPoint({\n          key: text1.__key,\n          offset: 0,\n          type: 'text',\n        });\n        $setFocusPoint({\n          key: text2.__key,\n          offset: text1.__text.length,\n          type: 'text',\n        });\n\n        $setBlocksType(selection, () => {\n          return $createHeadingNode('h1');\n        });\n\n        const rootChildren = root.getChildren();\n        expect(rootChildren[0].__type).toBe('heading');\n        expect(rootChildren[1].__type).toBe('heading');\n        expect(rootChildren.length).toBe(2);\n      });\n    });\n\n    test('Collapsed in element inside top-element', async () => {\n      const testEditor = createTestEditor();\n      const element = document.createElement('div');\n      testEditor.setRootElement(element);\n\n      await testEditor.update(() => {\n        const root = $getRoot();\n        const table = $createTableNodeWithDimensions(1, 1);\n        const row = table.getFirstChild();\n        invariant($isElementNode(row));\n        const column = row.getFirstChild();\n        invariant($isElementNode(column));\n        const paragraph = column.getFirstChild();\n        invariant($isElementNode(paragraph));\n        if (paragraph.getFirstChild()) {\n          paragraph.getFirstChild()!.remove();\n        }\n        root.append(table);\n\n        const selection = $createRangeSelection();\n        $setSelection(selection);\n        $setAnchorPoint({\n          key: paragraph.__key,\n          offset: 0,\n          type: 'element',\n        });\n        $setFocusPoint({\n          key: paragraph.__key,\n          offset: 0,\n          type: 'element',\n        });\n\n        const columnChildrenPrev = column.getChildren();\n        expect(columnChildrenPrev[0].__type).toBe('paragraph');\n        $setBlocksType(selection, () => {\n          return $createHeadingNode('h1');\n        });\n\n        const columnChildrenAfter = column.getChildren();\n        expect(columnChildrenAfter[0].__type).toBe('heading');\n        expect(columnChildrenAfter.length).toBe(1);\n      });\n    });\n\n    test('Collapsed in text inside top-element', async () => {\n      const testEditor = createTestEditor();\n      const element = document.createElement('div');\n      testEditor.setRootElement(element);\n\n      await testEditor.update(() => {\n        const root = $getRoot();\n        const table = $createTableNodeWithDimensions(1, 1);\n        const row = table.getFirstChild();\n        invariant($isElementNode(row));\n        const column = row.getFirstChild();\n        invariant($isElementNode(column));\n        const paragraph = column.getFirstChild();\n        invariant($isElementNode(paragraph));\n        const text = $createTextNode('foo');\n        root.append(table);\n        paragraph.append(text);\n\n        const selectionz = $createRangeSelection();\n        $setSelection(selectionz);\n        $setAnchorPoint({\n          key: text.__key,\n          offset: text.__text.length,\n          type: 'text',\n        });\n        $setFocusPoint({\n          key: text.__key,\n          offset: text.__text.length,\n          type: 'text',\n        });\n        const selection = $getSelection() as RangeSelection;\n\n        const columnChildrenPrev = column.getChildren();\n        expect(columnChildrenPrev[0].__type).toBe('paragraph');\n        $setBlocksType(selection, () => {\n          return $createHeadingNode('h1');\n        });\n\n        const columnChildrenAfter = column.getChildren();\n        expect(columnChildrenAfter[0].__type).toBe('heading');\n        expect(columnChildrenAfter.length).toBe(1);\n      });\n    });\n\n    test('Full editor selection with a mix of top-elements', async () => {\n      const testEditor = createTestEditor();\n      const element = document.createElement('div');\n      testEditor.setRootElement(element);\n\n      await testEditor.update(() => {\n        const root = $getRoot();\n\n        const paragraph1 = $createParagraphNode();\n        const paragraph2 = $createParagraphNode();\n        const text1 = $createTextNode();\n        const text2 = $createTextNode();\n        paragraph1.append(text1);\n        paragraph2.append(text2);\n        root.append(paragraph1, paragraph2);\n\n        const table = $createTableNodeWithDimensions(1, 2);\n        const row = table.getFirstChild();\n        invariant($isElementNode(row));\n        const columns = row.getChildren();\n        root.append(table);\n\n        const column1 = columns[0];\n        const paragraph3 = $createParagraphNode();\n        const paragraph4 = $createParagraphNode();\n        const text3 = $createTextNode();\n        const text4 = $createTextNode();\n        paragraph1.append(text3);\n        paragraph2.append(text4);\n        invariant($isElementNode(column1));\n        column1.append(paragraph3, paragraph4);\n\n        const column2 = columns[1];\n        const paragraph5 = $createParagraphNode();\n        const paragraph6 = $createParagraphNode();\n        invariant($isElementNode(column2));\n        column2.append(paragraph5, paragraph6);\n\n        const paragraph7 = $createParagraphNode();\n        root.append(paragraph7);\n\n        const selectionz = $createRangeSelection();\n        $setSelection(selectionz);\n        $setAnchorPoint({\n          key: paragraph1.__key,\n          offset: 0,\n          type: 'element',\n        });\n        $setFocusPoint({\n          key: paragraph7.__key,\n          offset: 0,\n          type: 'element',\n        });\n        const selection = $getSelection() as RangeSelection;\n\n        $setBlocksType(selection, () => {\n          return $createHeadingNode('h1');\n        });\n        expect(JSON.stringify(testEditor._pendingEditorState?.toJSON())).toBe(\n          '{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"\",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"heading\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"tag\":\"h1\"},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"\",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"heading\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"tag\":\"h1\"},{\"children\":[{\"children\":[{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"heading\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"tag\":\"h1\"},{\"children\":[],\"direction\":null,\"type\":\"heading\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"tag\":\"h1\"},{\"children\":[],\"direction\":null,\"type\":\"heading\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"tag\":\"h1\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":3,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"},{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"type\":\"heading\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"tag\":\"h1\"},{\"children\":[],\"direction\":null,\"type\":\"heading\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"tag\":\"h1\"},{\"children\":[],\"direction\":null,\"type\":\"heading\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"tag\":\"h1\"}],\"direction\":null,\"type\":\"tablecell\",\"version\":1,\"backgroundColor\":null,\"colSpan\":1,\"headerState\":1,\"rowSpan\":1,\"styles\":{},\"alignment\":\"\"}],\"direction\":null,\"type\":\"tablerow\",\"version\":1,\"styles\":{},\"height\":0}],\"direction\":null,\"type\":\"table\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"colWidths\":[],\"styles\":{}},{\"children\":[],\"direction\":null,\"type\":\"heading\",\"version\":1,\"id\":\"\",\"alignment\":\"\",\"inset\":0,\"tag\":\"h1\"}],\"direction\":null,\"type\":\"root\",\"version\":1}}',\n        );\n      });\n    });\n\n    test('Paragraph with links to heading with links', async () => {\n      const testEditor = createTestEditor();\n      const element = document.createElement('div');\n      testEditor.setRootElement(element);\n\n      await testEditor.update(() => {\n        const root = $getRoot();\n        const paragraph = $createParagraphNode();\n        const text1 = $createTextNode('Links: ');\n        const text2 = $createTextNode('link1');\n        const text3 = $createTextNode('link2');\n        root.append(\n          paragraph.append(\n            text1,\n            $createLinkNode('https://lexical.dev').append(text2),\n            $createTextNode(' '),\n            $createLinkNode('https://playground.lexical.dev').append(text3),\n          ),\n        );\n\n        const paragraphChildrenKeys = [...paragraph.getChildrenKeys()];\n        const selection = $createRangeSelection();\n        $setSelection(selection);\n        $setAnchorPoint({\n          key: text1.getKey(),\n          offset: 1,\n          type: 'text',\n        });\n        $setFocusPoint({\n          key: text3.getKey(),\n          offset: 1,\n          type: 'text',\n        });\n\n        $setBlocksType(selection, () => {\n          return $createHeadingNode('h1');\n        });\n\n        const rootChildren = root.getChildren();\n        expect(rootChildren.length).toBe(1);\n        invariant($isElementNode(rootChildren[0]));\n        expect(rootChildren[0].getType()).toBe('heading');\n        expect(rootChildren[0].getChildrenKeys()).toEqual(\n          paragraphChildrenKeys,\n        );\n      });\n    });\n\n    test('Nested list', async () => {\n      const testEditor = createTestEditor();\n      const element = document.createElement('div');\n      testEditor.setRootElement(element);\n\n      await testEditor.update(() => {\n        const root = $getRoot();\n        const ul1 = $createListNode('bullet');\n        const text1 = $createTextNode('1');\n        const li1 = $createListItemNode().append(text1);\n        const li1_wrapper = $createListItemNode();\n        const ul2 = $createListNode('bullet');\n        const text1_1 = $createTextNode('1.1');\n        const li1_1 = $createListItemNode().append(text1_1);\n        ul1.append(li1, li1_wrapper);\n        li1_wrapper.append(ul2);\n        ul2.append(li1_1);\n        root.append(ul1);\n\n        const selection = $createRangeSelection();\n        $setSelection(selection);\n        $setAnchorPoint({\n          key: text1.getKey(),\n          offset: 1,\n          type: 'text',\n        });\n        $setFocusPoint({\n          key: text1_1.getKey(),\n          offset: 1,\n          type: 'text',\n        });\n\n        $setBlocksType(selection, () => {\n          return $createHeadingNode('h1');\n        });\n      });\n      expect(element.innerHTML).toStrictEqual(\n        `<h1><span data-lexical-text=\"true\">1</span></h1><ul><li value=\"1\"><h1><span data-lexical-text=\"true\">1.1</span></h1></li></ul>`,\n      );\n    });\n\n    test('Nested list with listItem twice indented from his father', async () => {\n      const testEditor = createTestEditor();\n      const element = document.createElement('div');\n      testEditor.setRootElement(element);\n\n      await testEditor.update(() => {\n        const root = $getRoot();\n        const ul1 = $createListNode('bullet');\n        const li1_wrapper = $createListItemNode();\n        const ul2 = $createListNode('bullet');\n        const text1_1 = $createTextNode('1.1');\n        const li1_1 = $createListItemNode().append(text1_1);\n        ul1.append(li1_wrapper);\n        li1_wrapper.append(ul2);\n        ul2.append(li1_1);\n        root.append(ul1);\n\n        const selection = $createRangeSelection();\n        $setSelection(selection);\n        $setAnchorPoint({\n          key: text1_1.getKey(),\n          offset: 1,\n          type: 'text',\n        });\n        $setFocusPoint({\n          key: text1_1.getKey(),\n          offset: 1,\n          type: 'text',\n        });\n\n        $setBlocksType(selection, () => {\n          return $createHeadingNode('h1');\n        });\n      });\n      expect(element.innerHTML).toStrictEqual(\n        `<ul><li value=\"1\"><h1><span data-lexical-text=\"true\">1.1</span></h1></li></ul>`,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/selection/__tests__/unit/LexicalSelectionHelpers.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {$createLinkNode} from '@lexical/link';\nimport {\n  $getSelectionStyleValueForProperty,\n  $patchStyleText,\n} from '@lexical/selection';\nimport {\n  $createLineBreakNode,\n  $createParagraphNode,\n  $createRangeSelection,\n  $createTextNode,\n  $getNodeByKey,\n  $getRoot,\n  $getSelection,\n  $insertNodes,\n  $isElementNode,\n  $isParagraphNode,\n  $isRangeSelection,\n  $setSelection,\n  ElementNode,\n  LexicalEditor,\n  LexicalNode,\n  ParagraphNode,\n  RangeSelection,\n  TextModeType,\n  TextNode,\n} from 'lexical';\nimport {\n  $createTestDecoratorNode,\n  $createTestElementNode,\n  $createTestShadowRootNode,\n  createTestEditor,\n  createTestHeadlessEditor,\n  invariant,\n  TestDecoratorNode,\n} from 'lexical/__tests__/utils';\n\nimport {$setAnchorPoint, $setFocusPoint} from '../utils';\nimport {$createHeadingNode, $isHeadingNode} from \"@lexical/rich-text/LexicalHeadingNode\";\n\nRange.prototype.getBoundingClientRect = function (): DOMRect {\n  const rect = {\n    bottom: 0,\n    height: 0,\n    left: 0,\n    right: 0,\n    top: 0,\n    width: 0,\n    x: 0,\n    y: 0,\n  };\n  return {\n    ...rect,\n    toJSON() {\n      return rect;\n    },\n  };\n};\n\nfunction $createParagraphWithNodes(\n  editor: LexicalEditor,\n  nodes: {text: string; key: string; mergeable?: boolean}[],\n) {\n  const paragraph = $createParagraphNode();\n  const nodeMap = editor._pendingEditorState!._nodeMap;\n\n  for (let i = 0; i < nodes.length; i++) {\n    const {text, key, mergeable} = nodes[i];\n    const textNode = new TextNode(text, key);\n    nodeMap.set(key, textNode);\n\n    if (!mergeable) {\n      textNode.toggleUnmergeable();\n    }\n\n    paragraph.append(textNode);\n  }\n\n  return paragraph;\n}\n\ndescribe('LexicalSelectionHelpers tests', () => {\n  describe('Collapsed', () => {\n    test('Can handle a text point', () => {\n      const setupTestCase = (\n        cb: (selection: RangeSelection, node: ElementNode) => void,\n      ) => {\n        const editor = createTestEditor();\n\n        editor.update(() => {\n          const root = $getRoot();\n\n          const element = $createParagraphWithNodes(editor, [\n            {\n              key: 'a',\n              mergeable: false,\n              text: 'a',\n            },\n            {\n              key: 'b',\n              mergeable: false,\n              text: 'b',\n            },\n            {\n              key: 'c',\n              mergeable: false,\n              text: 'c',\n            },\n          ]);\n\n          root.append(element);\n\n          $setAnchorPoint({\n            key: 'a',\n            offset: 0,\n            type: 'text',\n          });\n\n          $setFocusPoint({\n            key: 'a',\n            offset: 0,\n            type: 'text',\n          });\n          const selection = $getSelection();\n          cb(selection as RangeSelection, element);\n        });\n      };\n\n      // getNodes\n      setupTestCase((selection, state) => {\n        expect(selection.getNodes()).toEqual([$getNodeByKey('a')]);\n      });\n\n      // getTextContent\n      setupTestCase((selection) => {\n        expect(selection.getTextContent()).toEqual('');\n      });\n\n      // insertText\n      setupTestCase((selection, state) => {\n        selection.insertText('Test');\n\n        expect($getNodeByKey('a')!.getTextContent()).toBe('Testa');\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: 'a',\n            offset: 4,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: 'a',\n            offset: 4,\n            type: 'text',\n          }),\n        );\n      });\n\n      // insertNodes\n      setupTestCase((selection, element) => {\n        selection.insertNodes([$createTextNode('foo')]);\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: element.getFirstChild()!.getKey(),\n            offset: 3,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: element.getFirstChild()!.getKey(),\n            offset: 3,\n            type: 'text',\n          }),\n        );\n      });\n\n      // insertParagraph\n      setupTestCase((selection) => {\n        selection.insertParagraph();\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: 'a',\n            offset: 0,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: 'a',\n            offset: 0,\n            type: 'text',\n          }),\n        );\n      });\n\n      // insertLineBreak\n      setupTestCase((selection, element) => {\n        selection.insertLineBreak(true);\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: element.getKey(),\n            offset: 0,\n            type: 'element',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: element.getKey(),\n            offset: 0,\n            type: 'element',\n          }),\n        );\n      });\n\n      // Format text\n      setupTestCase((selection, element) => {\n        selection.formatText('bold');\n        selection.insertText('Test');\n\n        expect(element.getFirstChild()!.getTextContent()).toBe('Test');\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: element.getFirstChild()!.getKey(),\n            offset: 4,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: element.getFirstChild()!.getKey(),\n            offset: 4,\n            type: 'text',\n          }),\n        );\n\n        expect(\n          element.getFirstChild()!.getNextSibling()!.getTextContent(),\n        ).toBe('a');\n      });\n\n      // Extract selection\n      setupTestCase((selection, state) => {\n        expect(selection.extract()).toEqual([$getNodeByKey('a')]);\n      });\n    });\n\n    test('Has correct text point after removal after merge', async () => {\n      const editor = createTestEditor();\n\n      const domElement = document.createElement('div');\n      let element;\n\n      editor.setRootElement(domElement);\n\n      editor.update(() => {\n        const root = $getRoot();\n\n        element = $createParagraphWithNodes(editor, [\n          {\n            key: 'a',\n            mergeable: true,\n            text: 'a',\n          },\n          {\n            key: 'bb',\n            mergeable: true,\n            text: 'bb',\n          },\n          {\n            key: 'empty',\n            mergeable: true,\n            text: '',\n          },\n          {\n            key: 'cc',\n            mergeable: true,\n            text: 'cc',\n          },\n          {\n            key: 'd',\n            mergeable: true,\n            text: 'd',\n          },\n        ]);\n\n        root.append(element);\n\n        $setAnchorPoint({\n          key: 'bb',\n          offset: 1,\n          type: 'text',\n        });\n\n        $setFocusPoint({\n          key: 'cc',\n          offset: 1,\n          type: 'text',\n        });\n      });\n\n      await Promise.resolve().then();\n\n      editor.getEditorState().read(() => {\n        const selection = $getSelection();\n\n        if (!$isRangeSelection(selection)) {\n          return;\n        }\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: 'a',\n            offset: 2,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: 'a',\n            offset: 4,\n            type: 'text',\n          }),\n        );\n      });\n    });\n\n    test('Has correct text point after removal after merge (2)', async () => {\n      const editor = createTestEditor();\n\n      const domElement = document.createElement('div');\n      let element;\n\n      editor.setRootElement(domElement);\n\n      editor.update(() => {\n        const root = $getRoot();\n\n        element = $createParagraphWithNodes(editor, [\n          {\n            key: 'a',\n            mergeable: true,\n            text: 'a',\n          },\n          {\n            key: 'empty',\n            mergeable: true,\n            text: '',\n          },\n          {\n            key: 'b',\n            mergeable: true,\n            text: 'b',\n          },\n          {\n            key: 'c',\n            mergeable: true,\n            text: 'c',\n          },\n        ]);\n\n        root.append(element);\n\n        $setAnchorPoint({\n          key: 'a',\n          offset: 0,\n          type: 'text',\n        });\n\n        $setFocusPoint({\n          key: 'c',\n          offset: 1,\n          type: 'text',\n        });\n      });\n\n      await Promise.resolve().then();\n\n      editor.getEditorState().read(() => {\n        const selection = $getSelection();\n\n        if (!$isRangeSelection(selection)) {\n          return;\n        }\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: 'a',\n            offset: 0,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: 'a',\n            offset: 3,\n            type: 'text',\n          }),\n        );\n      });\n    });\n\n    test('Has correct text point adjust to element point after removal of a single empty text node', async () => {\n      const editor = createTestEditor();\n\n      const domElement = document.createElement('div');\n      let element: ParagraphNode;\n\n      editor.setRootElement(domElement);\n\n      editor.update(() => {\n        const root = $getRoot();\n\n        element = $createParagraphWithNodes(editor, [\n          {\n            key: 'a',\n            mergeable: true,\n            text: '',\n          },\n        ]);\n\n        root.append(element);\n\n        $setAnchorPoint({\n          key: 'a',\n          offset: 0,\n          type: 'text',\n        });\n\n        $setFocusPoint({\n          key: 'a',\n          offset: 0,\n          type: 'text',\n        });\n      });\n\n      await Promise.resolve().then();\n\n      editor.getEditorState().read(() => {\n        const selection = $getSelection();\n\n        if (!$isRangeSelection(selection)) {\n          return;\n        }\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: element.getKey(),\n            offset: 0,\n            type: 'element',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: element.getKey(),\n            offset: 0,\n            type: 'element',\n          }),\n        );\n      });\n    });\n\n    test('Has correct element point after removal of an empty text node in a group #1', async () => {\n      const editor = createTestEditor();\n\n      const domElement = document.createElement('div');\n      let element;\n\n      editor.setRootElement(domElement);\n\n      editor.update(() => {\n        const root = $getRoot();\n\n        element = $createParagraphWithNodes(editor, [\n          {\n            key: 'a',\n            mergeable: true,\n            text: '',\n          },\n          {\n            key: 'b',\n            mergeable: false,\n            text: 'b',\n          },\n        ]);\n\n        root.append(element);\n\n        $setAnchorPoint({\n          key: element.getKey(),\n          offset: 2,\n          type: 'element',\n        });\n\n        $setFocusPoint({\n          key: element.getKey(),\n          offset: 2,\n          type: 'element',\n        });\n      });\n\n      await Promise.resolve().then();\n\n      editor.getEditorState().read(() => {\n        const selection = $getSelection();\n\n        if (!$isRangeSelection(selection)) {\n          return;\n        }\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: 'b',\n            offset: 1,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: 'b',\n            offset: 1,\n            type: 'text',\n          }),\n        );\n      });\n    });\n\n    test('Has correct element point after removal of an empty text node in a group #2', async () => {\n      const editor = createTestEditor();\n\n      const domElement = document.createElement('div');\n      let element;\n\n      editor.setRootElement(domElement);\n\n      editor.update(() => {\n        const root = $getRoot();\n\n        element = $createParagraphWithNodes(editor, [\n          {\n            key: 'a',\n            mergeable: true,\n            text: '',\n          },\n          {\n            key: 'b',\n            mergeable: false,\n            text: 'b',\n          },\n          {\n            key: 'c',\n            mergeable: true,\n            text: 'c',\n          },\n          {\n            key: 'd',\n            mergeable: true,\n            text: 'd',\n          },\n        ]);\n\n        root.append(element);\n\n        $setAnchorPoint({\n          key: element.getKey(),\n          offset: 4,\n          type: 'element',\n        });\n\n        $setFocusPoint({\n          key: element.getKey(),\n          offset: 4,\n          type: 'element',\n        });\n      });\n\n      await Promise.resolve().then();\n\n      editor.getEditorState().read(() => {\n        const selection = $getSelection();\n\n        if (!$isRangeSelection(selection)) {\n          return;\n        }\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: 'c',\n            offset: 2,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: 'c',\n            offset: 2,\n            type: 'text',\n          }),\n        );\n      });\n    });\n\n    test('Has correct text point after removal of an empty text node in a group #3', async () => {\n      const editor = createTestEditor();\n\n      const domElement = document.createElement('div');\n      let element;\n\n      editor.setRootElement(domElement);\n\n      editor.update(() => {\n        const root = $getRoot();\n\n        element = $createParagraphWithNodes(editor, [\n          {\n            key: 'a',\n            mergeable: true,\n            text: '',\n          },\n          {\n            key: 'b',\n            mergeable: false,\n            text: 'b',\n          },\n          {\n            key: 'c',\n            mergeable: true,\n            text: 'c',\n          },\n          {\n            key: 'd',\n            mergeable: true,\n            text: 'd',\n          },\n        ]);\n\n        root.append(element);\n\n        $setAnchorPoint({\n          key: 'd',\n          offset: 1,\n          type: 'text',\n        });\n\n        $setFocusPoint({\n          key: 'd',\n          offset: 1,\n          type: 'text',\n        });\n      });\n\n      await Promise.resolve().then();\n\n      editor.getEditorState().read(() => {\n        const selection = $getSelection();\n\n        if (!$isRangeSelection(selection)) {\n          return;\n        }\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: 'c',\n            offset: 2,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: 'c',\n            offset: 2,\n            type: 'text',\n          }),\n        );\n      });\n    });\n\n    test('Can handle an element point on empty element', () => {\n      const setupTestCase = (\n        cb: (selection: RangeSelection, el: ElementNode) => void,\n      ) => {\n        const editor = createTestEditor();\n\n        editor.update(() => {\n          const root = $getRoot();\n\n          const element = $createParagraphWithNodes(editor, []);\n\n          root.append(element);\n\n          $setAnchorPoint({\n            key: element.getKey(),\n            offset: 0,\n            type: 'element',\n          });\n\n          $setFocusPoint({\n            key: element.getKey(),\n            offset: 0,\n            type: 'element',\n          });\n          const selection = $getSelection();\n          cb(selection as RangeSelection, element);\n        });\n      };\n\n      // getNodes\n      setupTestCase((selection, element) => {\n        expect(selection.getNodes()).toEqual([element]);\n      });\n\n      // getTextContent\n      setupTestCase((selection) => {\n        expect(selection.getTextContent()).toEqual('');\n      });\n\n      // insertText\n      setupTestCase((selection, element) => {\n        selection.insertText('Test');\n        const firstChild = element.getFirstChild()!;\n\n        expect(firstChild.getTextContent()).toBe('Test');\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: firstChild.getKey(),\n            offset: 4,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: firstChild.getKey(),\n            offset: 4,\n            type: 'text',\n          }),\n        );\n      });\n\n      // insertParagraph\n      setupTestCase((selection, element) => {\n        selection.insertParagraph();\n        const nextElement = element.getNextSibling()!;\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: nextElement.getKey(),\n            offset: 0,\n            type: 'element',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: nextElement.getKey(),\n            offset: 0,\n            type: 'element',\n          }),\n        );\n      });\n\n      // insertLineBreak\n      setupTestCase((selection, element) => {\n        selection.insertLineBreak(true);\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: element.getKey(),\n            offset: 0,\n            type: 'element',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: element.getKey(),\n            offset: 0,\n            type: 'element',\n          }),\n        );\n      });\n\n      // Format text\n      setupTestCase((selection, element) => {\n        selection.formatText('bold');\n        selection.insertText('Test');\n        const firstChild = element.getFirstChild()!;\n\n        expect(firstChild.getTextContent()).toBe('Test');\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: firstChild.getKey(),\n            offset: 4,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: firstChild.getKey(),\n            offset: 4,\n            type: 'text',\n          }),\n        );\n      });\n\n      // Extract selection\n      setupTestCase((selection, element) => {\n        expect(selection.extract()).toEqual([element]);\n      });\n    });\n\n    test('Can handle a start element point', () => {\n      const setupTestCase = (\n        cb: (selection: RangeSelection, el: ElementNode) => void,\n      ) => {\n        const editor = createTestEditor();\n\n        editor.update(() => {\n          const root = $getRoot();\n\n          const element = $createParagraphWithNodes(editor, [\n            {\n              key: 'a',\n              mergeable: false,\n              text: 'a',\n            },\n            {\n              key: 'b',\n              mergeable: false,\n              text: 'b',\n            },\n            {\n              key: 'c',\n              mergeable: false,\n              text: 'c',\n            },\n          ]);\n\n          root.append(element);\n\n          $setAnchorPoint({\n            key: element.getKey(),\n            offset: 0,\n            type: 'element',\n          });\n\n          $setFocusPoint({\n            key: element.getKey(),\n            offset: 0,\n            type: 'element',\n          });\n          const selection = $getSelection();\n          cb(selection as RangeSelection, element);\n        });\n      };\n\n      // getNodes\n      setupTestCase((selection, state) => {\n        expect(selection.getNodes()).toEqual([$getNodeByKey('a')]);\n      });\n\n      // getTextContent\n      setupTestCase((selection) => {\n        expect(selection.getTextContent()).toEqual('');\n      });\n\n      // insertText\n      setupTestCase((selection, element) => {\n        selection.insertText('Test');\n        const firstChild = element.getFirstChild()!;\n\n        expect(firstChild.getTextContent()).toBe('Test');\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: firstChild.getKey(),\n            offset: 4,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: firstChild.getKey(),\n            offset: 4,\n            type: 'text',\n          }),\n        );\n      });\n\n      // insertParagraph\n      setupTestCase((selection, element) => {\n        selection.insertParagraph();\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: 'a',\n            offset: 0,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: 'a',\n            offset: 0,\n            type: 'text',\n          }),\n        );\n      });\n\n      // insertLineBreak\n      setupTestCase((selection, element) => {\n        selection.insertLineBreak(true);\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: element.getKey(),\n            offset: 0,\n            type: 'element',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: element.getKey(),\n            offset: 0,\n            type: 'element',\n          }),\n        );\n      });\n\n      // Format text\n      setupTestCase((selection, element) => {\n        selection.formatText('bold');\n        selection.insertText('Test');\n\n        const firstChild = element.getFirstChild()!;\n\n        expect(firstChild.getTextContent()).toBe('Test');\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: firstChild.getKey(),\n            offset: 4,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: firstChild.getKey(),\n            offset: 4,\n            type: 'text',\n          }),\n        );\n      });\n\n      // Extract selection\n      setupTestCase((selection, element) => {\n        expect(selection.extract()).toEqual([$getNodeByKey('a')]);\n      });\n    });\n\n    test('Can handle an end element point', () => {\n      const setupTestCase = (\n        cb: (selection: RangeSelection, el: ElementNode) => void,\n      ) => {\n        const editor = createTestEditor();\n\n        editor.update(() => {\n          const root = $getRoot();\n\n          const element = $createParagraphWithNodes(editor, [\n            {\n              key: 'a',\n              mergeable: false,\n              text: 'a',\n            },\n            {\n              key: 'b',\n              mergeable: false,\n              text: 'b',\n            },\n            {\n              key: 'c',\n              mergeable: false,\n              text: 'c',\n            },\n          ]);\n\n          root.append(element);\n\n          $setAnchorPoint({\n            key: element.getKey(),\n            offset: 3,\n            type: 'element',\n          });\n\n          $setFocusPoint({\n            key: element.getKey(),\n            offset: 3,\n            type: 'element',\n          });\n          const selection = $getSelection();\n          cb(selection as RangeSelection, element);\n        });\n      };\n\n      // getNodes\n      setupTestCase((selection, state) => {\n        expect(selection.getNodes()).toEqual([$getNodeByKey('c')]);\n      });\n\n      // getTextContent\n      setupTestCase((selection) => {\n        expect(selection.getTextContent()).toEqual('');\n      });\n\n      // insertText\n      setupTestCase((selection, element) => {\n        selection.insertText('Test');\n        const lastChild = element.getLastChild()!;\n\n        expect(lastChild.getTextContent()).toBe('Test');\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: lastChild.getKey(),\n            offset: 4,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: lastChild.getKey(),\n            offset: 4,\n            type: 'text',\n          }),\n        );\n      });\n\n      // insertParagraph\n      setupTestCase((selection, element) => {\n        selection.insertParagraph();\n        const nextSibling = element.getNextSibling()!;\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: nextSibling.getKey(),\n            offset: 0,\n            type: 'element',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: nextSibling.getKey(),\n            offset: 0,\n            type: 'element',\n          }),\n        );\n      });\n\n      // insertLineBreak\n      setupTestCase((selection, element) => {\n        selection.insertLineBreak();\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: element.getKey(),\n            offset: 4,\n            type: 'element',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: element.getKey(),\n            offset: 4,\n            type: 'element',\n          }),\n        );\n      });\n\n      // Format text\n      setupTestCase((selection, element) => {\n        selection.formatText('bold');\n        selection.insertText('Test');\n        const lastChild = element.getLastChild()!;\n\n        expect(lastChild.getTextContent()).toBe('Test');\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: lastChild.getKey(),\n            offset: 4,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: lastChild.getKey(),\n            offset: 4,\n            type: 'text',\n          }),\n        );\n      });\n\n      // Extract selection\n      setupTestCase((selection, element) => {\n        expect(selection.extract()).toEqual([$getNodeByKey('c')]);\n      });\n    });\n\n    test('Has correct element point after merge from middle', async () => {\n      const editor = createTestEditor();\n\n      const domElement = document.createElement('div');\n      let element;\n\n      editor.setRootElement(domElement);\n\n      editor.update(() => {\n        const root = $getRoot();\n\n        element = $createParagraphWithNodes(editor, [\n          {\n            key: 'a',\n            mergeable: true,\n            text: 'a',\n          },\n          {\n            key: 'b',\n            mergeable: true,\n            text: 'b',\n          },\n          {\n            key: 'c',\n            mergeable: true,\n            text: 'c',\n          },\n        ]);\n\n        root.append(element);\n\n        $setAnchorPoint({\n          key: element.getKey(),\n          offset: 2,\n          type: 'element',\n        });\n\n        $setFocusPoint({\n          key: element.getKey(),\n          offset: 2,\n          type: 'element',\n        });\n      });\n\n      await Promise.resolve().then();\n\n      editor.getEditorState().read(() => {\n        const selection = $getSelection();\n\n        if (!$isRangeSelection(selection)) {\n          return;\n        }\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: 'a',\n            offset: 2,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: 'a',\n            offset: 2,\n            type: 'text',\n          }),\n        );\n      });\n    });\n\n    test('Has correct element point after merge from end', async () => {\n      const editor = createTestEditor();\n\n      const domElement = document.createElement('div');\n      let element;\n\n      editor.setRootElement(domElement);\n\n      editor.update(() => {\n        const root = $getRoot();\n\n        element = $createParagraphWithNodes(editor, [\n          {\n            key: 'a',\n            mergeable: true,\n            text: 'a',\n          },\n          {\n            key: 'b',\n            mergeable: true,\n            text: 'b',\n          },\n          {\n            key: 'c',\n            mergeable: true,\n            text: 'c',\n          },\n        ]);\n\n        root.append(element);\n\n        $setAnchorPoint({\n          key: element.getKey(),\n          offset: 3,\n          type: 'element',\n        });\n\n        $setFocusPoint({\n          key: element.getKey(),\n          offset: 3,\n          type: 'element',\n        });\n      });\n\n      await Promise.resolve().then();\n\n      editor.getEditorState().read(() => {\n        const selection = $getSelection();\n\n        if (!$isRangeSelection(selection)) {\n          return;\n        }\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: 'a',\n            offset: 3,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: 'a',\n            offset: 3,\n            type: 'text',\n          }),\n        );\n      });\n    });\n  });\n\n  describe('Simple range', () => {\n    test('Can handle multiple text points', () => {\n      const setupTestCase = (\n        cb: (selection: RangeSelection, el: ElementNode) => void,\n      ) => {\n        const editor = createTestEditor();\n\n        editor.update(() => {\n          const root = $getRoot();\n\n          const element = $createParagraphWithNodes(editor, [\n            {\n              key: 'a',\n              mergeable: false,\n              text: 'a',\n            },\n            {\n              key: 'b',\n              mergeable: false,\n              text: 'b',\n            },\n            {\n              key: 'c',\n              mergeable: false,\n              text: 'c',\n            },\n          ]);\n\n          root.append(element);\n\n          $setAnchorPoint({\n            key: 'a',\n            offset: 0,\n            type: 'text',\n          });\n\n          $setFocusPoint({\n            key: 'b',\n            offset: 0,\n            type: 'text',\n          });\n          const selection = $getSelection();\n          if (!$isRangeSelection(selection)) {\n            return;\n          }\n          cb(selection, element);\n        });\n      };\n\n      // getNodes\n      setupTestCase((selection, state) => {\n        expect(selection.getNodes()).toEqual([\n          $getNodeByKey('a'),\n          $getNodeByKey('b'),\n        ]);\n      });\n\n      // getTextContent\n      setupTestCase((selection) => {\n        expect(selection.getTextContent()).toEqual('a');\n      });\n\n      // insertText\n      setupTestCase((selection, state) => {\n        selection.insertText('Test');\n\n        expect($getNodeByKey('a')!.getTextContent()).toBe('Test');\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: 'a',\n            offset: 4,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: 'a',\n            offset: 4,\n            type: 'text',\n          }),\n        );\n      });\n\n      // insertNodes\n      setupTestCase((selection, element) => {\n        selection.insertNodes([$createTextNode('foo')]);\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: element.getFirstChild()!.getKey(),\n            offset: 3,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: element.getFirstChild()!.getKey(),\n            offset: 3,\n            type: 'text',\n          }),\n        );\n      });\n\n      // insertParagraph\n      setupTestCase((selection) => {\n        selection.insertParagraph();\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: 'b',\n            offset: 0,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: 'b',\n            offset: 0,\n            type: 'text',\n          }),\n        );\n      });\n\n      // insertLineBreak\n      setupTestCase((selection, element) => {\n        selection.insertLineBreak(true);\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: element.getKey(),\n            offset: 0,\n            type: 'element',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: element.getKey(),\n            offset: 0,\n            type: 'element',\n          }),\n        );\n      });\n\n      // Format text\n      setupTestCase((selection, element) => {\n        selection.formatText('bold');\n        selection.insertText('Test');\n\n        expect(element.getFirstChild()!.getTextContent()).toBe('Test');\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: element.getFirstChild()!.getKey(),\n            offset: 4,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: element.getFirstChild()!.getKey(),\n            offset: 4,\n            type: 'text',\n          }),\n        );\n      });\n\n      // Extract selection\n      setupTestCase((selection, state) => {\n        expect(selection.extract()).toEqual([{...$getNodeByKey('a')}]);\n      });\n    });\n\n    test('Can handle multiple element points', () => {\n      const setupTestCase = (\n        cb: (selection: RangeSelection, el: ElementNode) => void,\n      ) => {\n        const editor = createTestEditor();\n\n        editor.update(() => {\n          const root = $getRoot();\n\n          const element = $createParagraphWithNodes(editor, [\n            {\n              key: 'a',\n              mergeable: false,\n              text: 'a',\n            },\n            {\n              key: 'b',\n              mergeable: false,\n              text: 'b',\n            },\n            {\n              key: 'c',\n              mergeable: false,\n              text: 'c',\n            },\n          ]);\n\n          root.append(element);\n\n          $setAnchorPoint({\n            key: element.getKey(),\n            offset: 0,\n            type: 'element',\n          });\n\n          $setFocusPoint({\n            key: element.getKey(),\n            offset: 1,\n            type: 'element',\n          });\n          const selection = $getSelection();\n          if (!$isRangeSelection(selection)) {\n            return;\n          }\n          cb(selection, element);\n        });\n      };\n\n      // getNodes\n      setupTestCase((selection) => {\n        expect(selection.getNodes()).toEqual([$getNodeByKey('a')]);\n      });\n\n      // getTextContent\n      setupTestCase((selection) => {\n        expect(selection.getTextContent()).toEqual('a');\n      });\n\n      // insertText\n      setupTestCase((selection, element) => {\n        selection.insertText('Test');\n        const firstChild = element.getFirstChild()!;\n\n        expect(firstChild.getTextContent()).toBe('Test');\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: firstChild.getKey(),\n            offset: 4,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: firstChild.getKey(),\n            offset: 4,\n            type: 'text',\n          }),\n        );\n      });\n\n      // insertParagraph\n      setupTestCase((selection, element) => {\n        selection.insertParagraph();\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: 'b',\n            offset: 0,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: 'b',\n            offset: 0,\n            type: 'text',\n          }),\n        );\n      });\n\n      // insertLineBreak\n      setupTestCase((selection, element) => {\n        selection.insertLineBreak(true);\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: element.getKey(),\n            offset: 0,\n            type: 'element',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: element.getKey(),\n            offset: 0,\n            type: 'element',\n          }),\n        );\n      });\n\n      // Format text\n      setupTestCase((selection, element) => {\n        selection.formatText('bold');\n        selection.insertText('Test');\n        const firstChild = element.getFirstChild()!;\n\n        expect(firstChild.getTextContent()).toBe('Test');\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: firstChild.getKey(),\n            offset: 4,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: firstChild.getKey(),\n            offset: 4,\n            type: 'text',\n          }),\n        );\n      });\n\n      // Extract selection\n      setupTestCase((selection, element) => {\n        const firstChild = element.getFirstChild();\n\n        expect(selection.extract()).toEqual([firstChild]);\n      });\n    });\n\n    test('Can handle a mix of text and element points', () => {\n      const setupTestCase = (\n        cb: (selection: RangeSelection, el: ElementNode) => void,\n      ) => {\n        const editor = createTestEditor();\n\n        editor.update(() => {\n          const root = $getRoot();\n\n          const element = $createParagraphWithNodes(editor, [\n            {\n              key: 'a',\n              mergeable: false,\n              text: 'a',\n            },\n            {\n              key: 'b',\n              mergeable: false,\n              text: 'b',\n            },\n            {\n              key: 'c',\n              mergeable: false,\n              text: 'c',\n            },\n          ]);\n\n          root.append(element);\n\n          $setAnchorPoint({\n            key: element.getKey(),\n            offset: 0,\n            type: 'element',\n          });\n\n          $setFocusPoint({\n            key: 'c',\n            offset: 1,\n            type: 'text',\n          });\n          const selection = $getSelection();\n          if (!$isRangeSelection(selection)) {\n            return;\n          }\n          cb(selection, element);\n        });\n      };\n\n      // isBefore\n      setupTestCase((selection, state) => {\n        expect(selection.anchor.isBefore(selection.focus)).toEqual(true);\n      });\n\n      // getNodes\n      setupTestCase((selection, state) => {\n        expect(selection.getNodes()).toEqual([\n          $getNodeByKey('a'),\n          $getNodeByKey('b'),\n          $getNodeByKey('c'),\n        ]);\n      });\n\n      // getTextContent\n      setupTestCase((selection) => {\n        expect(selection.getTextContent()).toEqual('abc');\n      });\n\n      // insertText\n      setupTestCase((selection, element) => {\n        selection.insertText('Test');\n        const firstChild = element.getFirstChild()!;\n\n        expect(firstChild.getTextContent()).toBe('Test');\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: firstChild.getKey(),\n            offset: 4,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: firstChild.getKey(),\n            offset: 4,\n            type: 'text',\n          }),\n        );\n      });\n\n      // insertParagraph\n      setupTestCase((selection, element) => {\n        selection.insertParagraph();\n        const nextElement = element.getNextSibling()!;\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: nextElement.getKey(),\n            offset: 0,\n            type: 'element',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: nextElement.getKey(),\n            offset: 0,\n            type: 'element',\n          }),\n        );\n      });\n\n      // insertLineBreak\n      setupTestCase((selection, element) => {\n        selection.insertLineBreak(true);\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: element.getKey(),\n            offset: 0,\n            type: 'element',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: element.getKey(),\n            offset: 0,\n            type: 'element',\n          }),\n        );\n      });\n\n      // Format text\n      setupTestCase((selection, element) => {\n        selection.formatText('bold');\n        selection.insertText('Test');\n        const firstChild = element.getFirstChild()!;\n\n        expect(firstChild.getTextContent()).toBe('Test');\n\n        expect(selection.anchor).toEqual(\n          expect.objectContaining({\n            key: firstChild.getKey(),\n            offset: 4,\n            type: 'text',\n          }),\n        );\n\n        expect(selection.focus).toEqual(\n          expect.objectContaining({\n            key: firstChild.getKey(),\n            offset: 4,\n            type: 'text',\n          }),\n        );\n      });\n\n      // Extract selection\n      setupTestCase((selection, element) => {\n        expect(selection.extract()).toEqual([\n          $getNodeByKey('a'),\n          $getNodeByKey('b'),\n          $getNodeByKey('c'),\n        ]);\n      });\n    });\n  });\n\n  describe('can insert non-element nodes correctly', () => {\n    describe('with an empty paragraph node selected', () => {\n      test('a single text node', async () => {\n        const editor = createTestEditor();\n\n        const element = document.createElement('div');\n\n        editor.setRootElement(element);\n\n        await editor.update(() => {\n          const root = $getRoot();\n\n          const paragraph = $createParagraphNode();\n          root.append(paragraph);\n\n          $setAnchorPoint({\n            key: paragraph.getKey(),\n            offset: 0,\n            type: 'element',\n          });\n\n          $setFocusPoint({\n            key: paragraph.getKey(),\n            offset: 0,\n            type: 'element',\n          });\n\n          const selection = $getSelection();\n\n          if (!$isRangeSelection(selection)) {\n            return;\n          }\n\n          selection.insertNodes([$createTextNode('foo')]);\n        });\n\n        expect(element.innerHTML).toBe(\n          '<p><span data-lexical-text=\"true\">foo</span></p>',\n        );\n      });\n\n      test('two text nodes', async () => {\n        const editor = createTestEditor();\n\n        const element = document.createElement('div');\n\n        editor.setRootElement(element);\n\n        await editor.update(() => {\n          const root = $getRoot();\n\n          const paragraph = $createParagraphNode();\n          root.append(paragraph);\n\n          $setAnchorPoint({\n            key: paragraph.getKey(),\n            offset: 0,\n            type: 'element',\n          });\n\n          $setFocusPoint({\n            key: paragraph.getKey(),\n            offset: 0,\n            type: 'element',\n          });\n          const selection = $getSelection();\n\n          if (!$isRangeSelection(selection)) {\n            return;\n          }\n\n          selection.insertNodes([\n            $createTextNode('foo'),\n            $createTextNode('bar'),\n          ]);\n        });\n\n        expect(element.innerHTML).toBe(\n          '<p><span data-lexical-text=\"true\">foobar</span></p>',\n        );\n      });\n\n      test('link insertion without parent element', async () => {\n        const editor = createTestEditor();\n\n        const element = document.createElement('div');\n\n        editor.setRootElement(element);\n\n        await editor.update(() => {\n          const root = $getRoot();\n\n          const paragraph = $createParagraphNode();\n          root.append(paragraph);\n\n          $setAnchorPoint({\n            key: paragraph.getKey(),\n            offset: 0,\n            type: 'element',\n          });\n\n          $setFocusPoint({\n            key: paragraph.getKey(),\n            offset: 0,\n            type: 'element',\n          });\n          const link = $createLinkNode('https://');\n          link.append($createTextNode('ello worl'));\n\n          const selection = $getSelection();\n\n          if (!$isRangeSelection(selection)) {\n            return;\n          }\n\n          selection.insertNodes([\n            $createTextNode('h'),\n            link,\n            $createTextNode('d'),\n          ]);\n        });\n\n        expect(element.innerHTML).toBe(\n          '<p><span data-lexical-text=\"true\">h</span><a href=\"https://\"><span data-lexical-text=\"true\">ello worl</span></a><span data-lexical-text=\"true\">d</span></p>',\n        );\n      });\n\n      test('a single heading node with a child text node', async () => {\n        const editor = createTestEditor();\n\n        const element = document.createElement('div');\n\n        editor.setRootElement(element);\n\n        await editor.update(() => {\n          const root = $getRoot();\n\n          const paragraph = $createParagraphNode();\n          root.append(paragraph);\n\n          $setAnchorPoint({\n            key: paragraph.getKey(),\n            offset: 0,\n            type: 'element',\n          });\n\n          $setFocusPoint({\n            key: paragraph.getKey(),\n            offset: 0,\n            type: 'element',\n          });\n\n          const heading = $createHeadingNode('h1');\n          const child = $createTextNode('foo');\n\n          heading.append(child);\n\n          const selection = $getSelection();\n\n          if (!$isRangeSelection(selection)) {\n            return;\n          }\n          selection.insertNodes([heading]);\n        });\n\n        expect(element.innerHTML).toBe(\n          '<h1><span data-lexical-text=\"true\">foo</span></h1>',\n        );\n      });\n    });\n\n    describe('with a paragraph node selected on some existing text', () => {\n      test('a single text node', async () => {\n        const editor = createTestEditor();\n\n        const element = document.createElement('div');\n\n        editor.setRootElement(element);\n\n        await editor.update(() => {\n          const root = $getRoot();\n\n          const paragraph = $createParagraphNode();\n          const text = $createTextNode('Existing text...');\n\n          paragraph.append(text);\n          root.append(paragraph);\n\n          $setAnchorPoint({\n            key: text.getKey(),\n            offset: 16,\n            type: 'text',\n          });\n\n          $setFocusPoint({\n            key: text.getKey(),\n            offset: 16,\n            type: 'text',\n          });\n\n          const selection = $getSelection();\n\n          if (!$isRangeSelection(selection)) {\n            return;\n          }\n          selection.insertNodes([$createTextNode('foo')]);\n        });\n\n        expect(element.innerHTML).toBe(\n          '<p><span data-lexical-text=\"true\">Existing text...foo</span></p>',\n        );\n      });\n\n      test('two text nodes', async () => {\n        const editor = createTestEditor();\n\n        const element = document.createElement('div');\n\n        editor.setRootElement(element);\n\n        await editor.update(() => {\n          const root = $getRoot();\n\n          const paragraph = $createParagraphNode();\n          const text = $createTextNode('Existing text...');\n\n          paragraph.append(text);\n          root.append(paragraph);\n\n          $setAnchorPoint({\n            key: text.getKey(),\n            offset: 16,\n            type: 'text',\n          });\n\n          $setFocusPoint({\n            key: text.getKey(),\n            offset: 16,\n            type: 'text',\n          });\n\n          const selection = $getSelection();\n\n          if (!$isRangeSelection(selection)) {\n            return;\n          }\n\n          selection.insertNodes([\n            $createTextNode('foo'),\n            $createTextNode('bar'),\n          ]);\n        });\n\n        expect(element.innerHTML).toBe(\n          '<p><span data-lexical-text=\"true\">Existing text...foobar</span></p>',\n        );\n      });\n\n      test('a single heading node with a child text node', async () => {\n        const editor = createTestEditor();\n\n        const element = document.createElement('div');\n\n        editor.setRootElement(element);\n\n        await editor.update(() => {\n          const root = $getRoot();\n\n          const paragraph = $createParagraphNode();\n          const text = $createTextNode('Existing text...');\n\n          paragraph.append(text);\n          root.append(paragraph);\n\n          $setAnchorPoint({\n            key: text.getKey(),\n            offset: 16,\n            type: 'text',\n          });\n\n          $setFocusPoint({\n            key: text.getKey(),\n            offset: 16,\n            type: 'text',\n          });\n\n          const heading = $createHeadingNode('h1');\n          const child = $createTextNode('foo');\n\n          heading.append(child);\n\n          const selection = $getSelection();\n\n          if (!$isRangeSelection(selection)) {\n            return;\n          }\n\n          selection.insertNodes([heading]);\n        });\n\n        expect(element.innerHTML).toBe(\n          '<p><span data-lexical-text=\"true\">Existing text...foo</span></p>',\n        );\n      });\n\n      test('a paragraph with a child text and a child italic text and a child text', async () => {\n        const editor = createTestEditor();\n\n        const element = document.createElement('div');\n\n        editor.setRootElement(element);\n\n        await editor.update(() => {\n          const root = $getRoot();\n\n          const paragraph = $createParagraphNode();\n          const text = $createTextNode('AE');\n\n          paragraph.append(text);\n          root.append(paragraph);\n\n          $setAnchorPoint({\n            key: text.getKey(),\n            offset: 1,\n            type: 'text',\n          });\n\n          $setFocusPoint({\n            key: text.getKey(),\n            offset: 1,\n            type: 'text',\n          });\n\n          const insertedParagraph = $createParagraphNode();\n          const insertedTextB = $createTextNode('B');\n          const insertedTextC = $createTextNode('C');\n          const insertedTextD = $createTextNode('D');\n\n          insertedTextC.toggleFormat('italic');\n\n          insertedParagraph.append(insertedTextB, insertedTextC, insertedTextD);\n\n          const selection = $getSelection();\n\n          if (!$isRangeSelection(selection)) {\n            return;\n          }\n\n          selection.insertNodes([insertedParagraph]);\n\n          expect(selection.anchor).toEqual(\n            expect.objectContaining({\n              key: paragraph\n                .getChildAtIndex(paragraph.getChildrenSize() - 2)!\n                .getKey(),\n              offset: 1,\n              type: 'text',\n            }),\n          );\n\n          expect(selection.focus).toEqual(\n            expect.objectContaining({\n              key: paragraph\n                .getChildAtIndex(paragraph.getChildrenSize() - 2)!\n                .getKey(),\n              offset: 1,\n              type: 'text',\n            }),\n          );\n        });\n\n        expect(element.innerHTML).toBe(\n          '<p><span data-lexical-text=\"true\">AB</span><em data-lexical-text=\"true\">C</em><span data-lexical-text=\"true\">DE</span></p>',\n        );\n      });\n    });\n\n    describe('with a fully-selected text node', () => {\n      test('a single text node', async () => {\n        const editor = createTestEditor();\n\n        const element = document.createElement('div');\n\n        editor.setRootElement(element);\n\n        await editor.update(() => {\n          const root = $getRoot();\n\n          const paragraph = $createParagraphNode();\n          root.append(paragraph);\n\n          const text = $createTextNode('Existing text...');\n          paragraph.append(text);\n\n          $setAnchorPoint({\n            key: text.getKey(),\n            offset: 0,\n            type: 'text',\n          });\n\n          $setFocusPoint({\n            key: text.getKey(),\n            offset: 'Existing text...'.length,\n            type: 'text',\n          });\n\n          const selection = $getSelection();\n\n          if (!$isRangeSelection(selection)) {\n            return;\n          }\n          selection.insertNodes([$createTextNode('foo')]);\n        });\n\n        expect(element.innerHTML).toBe(\n          '<p><span data-lexical-text=\"true\">foo</span></p>',\n        );\n      });\n    });\n\n    describe('with a fully-selected text node followed by an inline element', () => {\n      test('a single text node', async () => {\n        const editor = createTestEditor();\n\n        const element = document.createElement('div');\n\n        editor.setRootElement(element);\n\n        await editor.update(() => {\n          const root = $getRoot();\n\n          const paragraph = $createParagraphNode();\n          root.append(paragraph);\n\n          const text = $createTextNode('Existing text...');\n          paragraph.append(text);\n\n          const link = $createLinkNode('https://');\n          link.append($createTextNode('link'));\n          paragraph.append(link);\n\n          $setAnchorPoint({\n            key: text.getKey(),\n            offset: 0,\n            type: 'text',\n          });\n\n          $setFocusPoint({\n            key: text.getKey(),\n            offset: 'Existing text...'.length,\n            type: 'text',\n          });\n\n          const selection = $getSelection();\n\n          if (!$isRangeSelection(selection)) {\n            return;\n          }\n          selection.insertNodes([$createTextNode('foo')]);\n        });\n\n        expect(element.innerHTML).toBe(\n          '<p><span data-lexical-text=\"true\">foo</span><a href=\"https://\"><span data-lexical-text=\"true\">link</span></a></p>',\n        );\n      });\n    });\n\n    describe('with a fully-selected text node preceded by an inline element', () => {\n      test('a single text node', async () => {\n        const editor = createTestEditor();\n\n        const element = document.createElement('div');\n\n        editor.setRootElement(element);\n\n        await editor.update(() => {\n          const root = $getRoot();\n\n          const paragraph = $createParagraphNode();\n          root.append(paragraph);\n\n          const link = $createLinkNode('https://');\n          link.append($createTextNode('link'));\n          paragraph.append(link);\n\n          const text = $createTextNode('Existing text...');\n          paragraph.append(text);\n\n          $setAnchorPoint({\n            key: text.getKey(),\n            offset: 0,\n            type: 'text',\n          });\n\n          $setFocusPoint({\n            key: text.getKey(),\n            offset: 'Existing text...'.length,\n            type: 'text',\n          });\n\n          const selection = $getSelection();\n\n          if (!$isRangeSelection(selection)) {\n            return;\n          }\n          selection.insertNodes([$createTextNode('foo')]);\n        });\n\n        expect(element.innerHTML).toBe(\n          '<p><a href=\"https://\"><span data-lexical-text=\"true\">link</span></a><span data-lexical-text=\"true\">foo</span></p>',\n        );\n      });\n    });\n\n    test.skip('can insert a linebreak node before an inline element node', async () => {\n      const editor = createTestEditor();\n      const element = document.createElement('div');\n      editor.setRootElement(element);\n\n      await editor.update(() => {\n        const root = $getRoot();\n        const paragraph = $createParagraphNode();\n        root.append(paragraph);\n        const link = $createLinkNode('https://lexical.dev/');\n        paragraph.append(link);\n        const text = $createTextNode('Lexical');\n        link.append(text);\n        text.select(0, 0);\n\n        $insertNodes([$createLineBreakNode()]);\n      });\n\n      // TODO #5109 ElementNode should have a way to control when other nodes can be inserted inside\n      expect(element.innerHTML).toBe(\n        '<p><a href=\"https://lexical.dev/\"><br><span data-lexical-text=\"true\">Lexical</span></a></p>',\n      );\n    });\n  });\n\n  describe('can insert block element nodes correctly', () => {\n    describe('with a fully-selected text node', () => {\n      test('a paragraph node', async () => {\n        const editor = createTestEditor();\n\n        const element = document.createElement('div');\n\n        editor.setRootElement(element);\n\n        await editor.update(() => {\n          const root = $getRoot();\n\n          const paragraph = $createParagraphNode();\n          root.append(paragraph);\n\n          const text = $createTextNode('Existing text...');\n          paragraph.append(text);\n\n          $setAnchorPoint({\n            key: text.getKey(),\n            offset: 0,\n            type: 'text',\n          });\n\n          $setFocusPoint({\n            key: text.getKey(),\n            offset: 'Existing text...'.length,\n            type: 'text',\n          });\n\n          const paragraphToInsert = $createParagraphNode();\n          paragraphToInsert.append($createTextNode('foo'));\n\n          const selection = $getSelection();\n\n          if (!$isRangeSelection(selection)) {\n            return;\n          }\n          selection.insertNodes([paragraphToInsert]);\n        });\n\n        expect(element.innerHTML).toBe(\n          '<p><span data-lexical-text=\"true\">foo</span></p>',\n        );\n      });\n    });\n\n    describe('with a fully-selected text node followed by an inline element', () => {\n      test('a paragraph node', async () => {\n        const editor = createTestEditor();\n\n        const element = document.createElement('div');\n\n        editor.setRootElement(element);\n\n        await editor.update(() => {\n          const root = $getRoot();\n\n          const paragraph = $createParagraphNode();\n          root.append(paragraph);\n\n          const text = $createTextNode('Existing text...');\n          paragraph.append(text);\n\n          const link = $createLinkNode('https://');\n          link.append($createTextNode('link'));\n          paragraph.append(link);\n\n          $setAnchorPoint({\n            key: text.getKey(),\n            offset: 0,\n            type: 'text',\n          });\n\n          $setFocusPoint({\n            key: text.getKey(),\n            offset: 'Existing text...'.length,\n            type: 'text',\n          });\n\n          const paragraphToInsert = $createParagraphNode();\n          paragraphToInsert.append($createTextNode('foo'));\n\n          const selection = $getSelection();\n\n          if (!$isRangeSelection(selection)) {\n            return;\n          }\n          selection.insertNodes([paragraphToInsert]);\n        });\n\n        expect(element.innerHTML).toBe(\n          '<p><span data-lexical-text=\"true\">foo</span><a href=\"https://\"><span data-lexical-text=\"true\">link</span></a></p>',\n        );\n      });\n    });\n\n    describe('with a fully-selected text node preceded by an inline element', () => {\n      test('a paragraph node', async () => {\n        const editor = createTestEditor();\n\n        const element = document.createElement('div');\n\n        editor.setRootElement(element);\n\n        await editor.update(() => {\n          const root = $getRoot();\n\n          const paragraph = $createParagraphNode();\n          root.append(paragraph);\n\n          const link = $createLinkNode('https://');\n          link.append($createTextNode('link'));\n          paragraph.append(link);\n\n          const text = $createTextNode('Existing text...');\n          paragraph.append(text);\n\n          $setAnchorPoint({\n            key: text.getKey(),\n            offset: 0,\n            type: 'text',\n          });\n\n          $setFocusPoint({\n            key: text.getKey(),\n            offset: 'Existing text...'.length,\n            type: 'text',\n          });\n\n          const paragraphToInsert = $createParagraphNode();\n          paragraphToInsert.append($createTextNode('foo'));\n\n          const selection = $getSelection();\n\n          if (!$isRangeSelection(selection)) {\n            return;\n          }\n          selection.insertNodes([paragraphToInsert]);\n        });\n\n        expect(element.innerHTML).toBe(\n          '<p><a href=\"https://\"><span data-lexical-text=\"true\">link</span></a><span data-lexical-text=\"true\">foo</span></p>',\n        );\n      });\n    });\n\n    test('Can insert link into empty paragraph', async () => {\n      const editor = createTestEditor();\n      const element = document.createElement('div');\n      editor.setRootElement(element);\n\n      await editor.update(() => {\n        const root = $getRoot();\n        const paragraph = $createParagraphNode();\n        root.append(paragraph);\n        const linkNode = $createLinkNode('https://lexical.dev');\n        const linkTextNode = $createTextNode('Lexical');\n        linkNode.append(linkTextNode);\n        $insertNodes([linkNode]);\n      });\n      expect(element.innerHTML).toBe(\n        '<p><a href=\"https://lexical.dev\"><span data-lexical-text=\"true\">Lexical</span></a></p>',\n      );\n    });\n\n    test('Can insert link into empty paragraph (2)', async () => {\n      const editor = createTestEditor();\n      const element = document.createElement('div');\n      editor.setRootElement(element);\n\n      await editor.update(() => {\n        const root = $getRoot();\n        const paragraph = $createParagraphNode();\n        root.append(paragraph);\n        const linkNode = $createLinkNode('https://lexical.dev');\n        const linkTextNode = $createTextNode('Lexical');\n        linkNode.append(linkTextNode);\n        const textNode2 = $createTextNode('...');\n        $insertNodes([linkNode, textNode2]);\n      });\n      expect(element.innerHTML).toBe(\n        '<p><a href=\"https://lexical.dev\"><span data-lexical-text=\"true\">Lexical</span></a><span data-lexical-text=\"true\">...</span></p>',\n      );\n    });\n\n    test('Can insert an ElementNode after ShadowRoot', async () => {\n      const editor = createTestEditor();\n      const element = document.createElement('div');\n      editor.setRootElement(element);\n\n      await editor.update(() => {\n        const root = $getRoot();\n        const paragraph = $createParagraphNode();\n        root.append(paragraph);\n        paragraph.selectStart();\n        const element1 = $createTestShadowRootNode();\n        const element2 = $createTestElementNode();\n        $insertNodes([element1, element2]);\n      });\n      expect([\n        '<div><br></div><div><br></div>',\n        '<div><br></div><p><br></p>',\n      ]).toContain(element.innerHTML);\n    });\n  });\n});\n\ndescribe('extract', () => {\n  test('Should return the selected node when collapsed on a TextNode', async () => {\n    const editor = createTestEditor();\n\n    const element = document.createElement('div');\n\n    editor.setRootElement(element);\n\n    await editor.update(() => {\n      const root = $getRoot();\n\n      const paragraph = $createParagraphNode();\n      const text = $createTextNode('Existing text...');\n\n      paragraph.append(text);\n      root.append(paragraph);\n\n      $setAnchorPoint({\n        key: text.getKey(),\n        offset: 16,\n        type: 'text',\n      });\n\n      $setFocusPoint({\n        key: text.getKey(),\n        offset: 16,\n        type: 'text',\n      });\n\n      const selection = $getSelection();\n      expect($isRangeSelection(selection)).toBeTruthy();\n\n      expect(selection!.extract()).toEqual([text]);\n    });\n  });\n});\n\ndescribe('insertNodes', () => {\n  afterEach(() => {\n    jest.clearAllMocks();\n  });\n\n  it('can insert element next to top level decorator node', async () => {\n    const editor = createTestEditor();\n    const element = document.createElement('div');\n    editor.setRootElement(element);\n\n    jest.spyOn(TestDecoratorNode.prototype, 'isInline').mockReturnValue(false);\n\n    await editor.update(() => {\n      $getRoot().append(\n        $createParagraphNode(),\n        $createTestDecoratorNode(),\n        $createParagraphNode().append($createTextNode('Text after')),\n      );\n    });\n\n    await editor.update(() => {\n      const selectionNode = $getRoot().getFirstChild();\n      invariant($isElementNode(selectionNode));\n      const selection = selectionNode.select();\n      selection.insertNodes([\n        $createParagraphNode().append($createTextNode('Text before')),\n      ]);\n    });\n\n    expect(element.innerHTML).toBe(\n      '<p><span data-lexical-text=\"true\">Text before</span></p>' +\n        '<span data-lexical-decorator=\"true\" contenteditable=\"false\"></span>' +\n        '<p><span data-lexical-text=\"true\">Text after</span></p>',\n    );\n  });\n\n  it('can insert when previous selection was null', async () => {\n    const editor = createTestHeadlessEditor();\n    await editor.update(() => {\n      const selection = $createRangeSelection();\n      selection.anchor.set('root', 0, 'element');\n      selection.focus.set('root', 0, 'element');\n\n      selection.insertNodes([\n        $createParagraphNode().append($createTextNode('Text')),\n      ]);\n\n      expect($getRoot().getTextContent()).toBe('Text');\n\n      $setSelection(null);\n    });\n    await editor.update(() => {\n      const selection = $createRangeSelection();\n      const text = $getRoot().getLastDescendant()!;\n      selection.anchor.set(text.getKey(), 0, 'text');\n      selection.focus.set(text.getKey(), 0, 'text');\n\n      selection.insertNodes([\n        $createParagraphNode().append($createTextNode('Before ')),\n      ]);\n\n      expect($getRoot().getTextContent()).toBe('Before Text');\n    });\n  });\n\n  it('can insert when before empty text node', async () => {\n    const editor = createTestEditor();\n    const element = document.createElement('div');\n    editor.setRootElement(element);\n\n    await editor.update(() => {\n      // Empty text node to test empty text split\n      const emptyTextNode = $createTextNode('');\n      $getRoot().append(\n        $createParagraphNode().append(emptyTextNode, $createTextNode('text')),\n      );\n      emptyTextNode.select(0, 0);\n      const selection = $getSelection()!;\n      expect($isRangeSelection(selection)).toBeTruthy();\n      selection.insertNodes([$createTextNode('foo')]);\n\n      expect($getRoot().getTextContent()).toBe('footext');\n    });\n  });\n\n  it('last node is LineBreakNode', async () => {\n    const editor = createTestEditor();\n    const element = document.createElement('div');\n    editor.setRootElement(element);\n\n    await editor.update(() => {\n      // Empty text node to test empty text split\n      const paragraph = $createParagraphNode();\n      $getRoot().append(paragraph);\n      const selection = paragraph.select();\n      expect($isRangeSelection(selection)).toBeTruthy();\n\n      const newHeading = $createHeadingNode('h1').append(\n        $createTextNode('heading'),\n      );\n      selection.insertNodes([newHeading, $createLineBreakNode()]);\n    });\n    editor.getEditorState().read(() => {\n      expect(element.innerHTML).toBe(\n        '<h1><span data-lexical-text=\"true\">heading</span></h1><p><br></p>',\n      );\n      const selectedNode = ($getSelection() as RangeSelection).anchor.getNode();\n      expect($isParagraphNode(selectedNode)).toBeTruthy();\n      expect($isHeadingNode(selectedNode.getPreviousSibling())).toBeTruthy();\n    });\n  });\n});\n\ndescribe('$patchStyleText', () => {\n  test('can patch a selection anchored to the end of a TextNode before an inline element', async () => {\n    const editor = createTestEditor();\n    const element = document.createElement('div');\n    editor.setRootElement(element);\n\n    await editor.update(() => {\n      const root = $getRoot();\n\n      const paragraph = $createParagraphWithNodes(editor, [\n        {\n          key: 'a',\n          mergeable: false,\n          text: 'a',\n        },\n        {\n          key: 'b',\n          mergeable: false,\n          text: 'b',\n        },\n      ]);\n\n      root.append(paragraph);\n\n      const link = $createLinkNode('https://');\n      link.append($createTextNode('link'));\n\n      const a = $getNodeByKey('a')!;\n      a.insertAfter(link);\n\n      $setAnchorPoint({\n        key: 'a',\n        offset: 1,\n        type: 'text',\n      });\n      $setFocusPoint({\n        key: 'b',\n        offset: 1,\n        type: 'text',\n      });\n\n      const selection = $getSelection();\n      if (!$isRangeSelection(selection)) {\n        return;\n      }\n      $patchStyleText(selection, {'text-emphasis': 'filled'});\n    });\n\n    expect(element.innerHTML).toBe(\n      '<p><span data-lexical-text=\"true\">a</span>' +\n        '<a href=\"https://\">' +\n        '<span style=\"text-emphasis: filled;\" data-lexical-text=\"true\">link</span>' +\n        '</a>' +\n        '<span style=\"text-emphasis: filled;\" data-lexical-text=\"true\">b</span></p>',\n    );\n  });\n\n  test('can patch a selection anchored to the end of a TextNode at the end of a paragraph', async () => {\n    const editor = createTestEditor();\n    const element = document.createElement('div');\n    editor.setRootElement(element);\n\n    await editor.update(() => {\n      const root = $getRoot();\n\n      const paragraph1 = $createParagraphWithNodes(editor, [\n        {\n          key: 'a',\n          mergeable: false,\n          text: 'a',\n        },\n      ]);\n      const paragraph2 = $createParagraphWithNodes(editor, [\n        {\n          key: 'b',\n          mergeable: false,\n          text: 'b',\n        },\n      ]);\n\n      root.append(paragraph1);\n      root.append(paragraph2);\n\n      $setAnchorPoint({\n        key: 'a',\n        offset: 1,\n        type: 'text',\n      });\n      $setFocusPoint({\n        key: 'b',\n        offset: 1,\n        type: 'text',\n      });\n\n      const selection = $getSelection();\n      if (!$isRangeSelection(selection)) {\n        return;\n      }\n      $patchStyleText(selection, {'text-emphasis': 'filled'});\n    });\n\n    expect(element.innerHTML).toBe(\n      '<p><span data-lexical-text=\"true\">a</span></p>' +\n        '<p><span style=\"text-emphasis: filled;\" data-lexical-text=\"true\">b</span></p>',\n    );\n  });\n\n  test('can patch a selection that ends on an element', async () => {\n    const editor = createTestEditor();\n    const element = document.createElement('div');\n    editor.setRootElement(element);\n\n    await editor.update(() => {\n      const root = $getRoot();\n\n      const paragraph = $createParagraphWithNodes(editor, [\n        {\n          key: 'a',\n          mergeable: false,\n          text: 'a',\n        },\n      ]);\n\n      root.append(paragraph);\n\n      const link = $createLinkNode('https://');\n      link.append($createTextNode('link'));\n\n      const a = $getNodeByKey('a')!;\n      a.insertAfter(link);\n\n      $setAnchorPoint({\n        key: 'a',\n        offset: 0,\n        type: 'text',\n      });\n      // Select to end of the link _element_\n      $setFocusPoint({\n        key: link.getKey(),\n        offset: 1,\n        type: 'element',\n      });\n\n      const selection = $getSelection();\n      if (!$isRangeSelection(selection)) {\n        return;\n      }\n      $patchStyleText(selection, {'text-emphasis': 'filled'});\n    });\n\n    expect(element.innerHTML).toBe(\n      '<p>' +\n        '<span style=\"text-emphasis: filled;\" data-lexical-text=\"true\">a</span>' +\n        '<a href=\"https://\">' +\n        '<span style=\"text-emphasis: filled;\" data-lexical-text=\"true\">link</span>' +\n        '</a>' +\n        '</p>',\n    );\n  });\n\n  test('can patch a reversed selection that ends on an element', async () => {\n    const editor = createTestEditor();\n    const element = document.createElement('div');\n    editor.setRootElement(element);\n\n    await editor.update(() => {\n      const root = $getRoot();\n\n      const paragraph = $createParagraphWithNodes(editor, [\n        {\n          key: 'a',\n          mergeable: false,\n          text: 'a',\n        },\n      ]);\n\n      root.append(paragraph);\n\n      const link = $createLinkNode('https://');\n      link.append($createTextNode('link'));\n\n      const a = $getNodeByKey('a')!;\n      a.insertAfter(link);\n\n      // Select from the end of the link _element_\n      $setAnchorPoint({\n        key: link.getKey(),\n        offset: 1,\n        type: 'element',\n      });\n      $setFocusPoint({\n        key: 'a',\n        offset: 0,\n        type: 'text',\n      });\n\n      const selection = $getSelection();\n      if (!$isRangeSelection(selection)) {\n        return;\n      }\n      $patchStyleText(selection, {'text-emphasis': 'filled'});\n    });\n\n    expect(element.innerHTML).toBe(\n      '<p>' +\n        '<span style=\"text-emphasis: filled;\" data-lexical-text=\"true\">a</span>' +\n        '<a href=\"https://\">' +\n        '<span style=\"text-emphasis: filled;\" data-lexical-text=\"true\">link</span>' +\n        '</a>' +\n        '</p>',\n    );\n  });\n\n  test('can patch a selection that starts and ends on an element', async () => {\n    const editor = createTestEditor();\n    const element = document.createElement('div');\n    editor.setRootElement(element);\n\n    await editor.update(() => {\n      const root = $getRoot();\n\n      const paragraph = $createParagraphNode();\n      root.append(paragraph);\n\n      const link = $createLinkNode('https://');\n      link.append($createTextNode('link'));\n      paragraph.append(link);\n\n      $setAnchorPoint({\n        key: link.getKey(),\n        offset: 0,\n        type: 'element',\n      });\n      $setFocusPoint({\n        key: link.getKey(),\n        offset: 1,\n        type: 'element',\n      });\n\n      const selection = $getSelection();\n      if (!$isRangeSelection(selection)) {\n        return;\n      }\n      $patchStyleText(selection, {'text-emphasis': 'filled'});\n    });\n\n    expect(element.innerHTML).toBe(\n      '<p>' +\n        '<a href=\"https://\">' +\n        '<span style=\"text-emphasis: filled;\" data-lexical-text=\"true\">link</span>' +\n        '</a>' +\n        '</p>',\n    );\n  });\n\n  test('can clear a style', async () => {\n    const editor = createTestEditor();\n    const element = document.createElement('div');\n    editor.setRootElement(element);\n\n    await editor.update(() => {\n      const root = $getRoot();\n\n      const paragraph = $createParagraphNode();\n      root.append(paragraph);\n\n      const text = $createTextNode('text');\n      paragraph.append(text);\n\n      $setAnchorPoint({\n        key: text.getKey(),\n        offset: 0,\n        type: 'text',\n      });\n      $setFocusPoint({\n        key: text.getKey(),\n        offset: text.getTextContentSize(),\n        type: 'text',\n      });\n\n      const selection = $getSelection();\n      if (!$isRangeSelection(selection)) {\n        return;\n      }\n      $patchStyleText(selection, {'text-emphasis': 'filled'});\n      $patchStyleText(selection, {'text-emphasis': null});\n    });\n\n    expect(element.innerHTML).toBe(\n      '<p><span data-lexical-text=\"true\">text</span></p>',\n    );\n  });\n\n  test('can toggle a style on a collapsed selection', async () => {\n    const editor = createTestEditor();\n    const element = document.createElement('div');\n    editor.setRootElement(element);\n\n    await editor.update(() => {\n      const root = $getRoot();\n\n      const paragraph = $createParagraphNode();\n      root.append(paragraph);\n\n      const text = $createTextNode('text');\n      paragraph.append(text);\n\n      $setAnchorPoint({\n        key: text.getKey(),\n        offset: 0,\n        type: 'text',\n      });\n      $setFocusPoint({\n        key: text.getKey(),\n        offset: 0,\n        type: 'text',\n      });\n\n      const selection = $getSelection();\n      if (!$isRangeSelection(selection)) {\n        return;\n      }\n      $patchStyleText(selection, {'text-emphasis': 'filled'});\n\n      expect(\n        $getSelectionStyleValueForProperty(selection, 'text-emphasis', ''),\n      ).toEqual('filled');\n\n      $patchStyleText(selection, {'text-emphasis': null});\n\n      expect(\n        $getSelectionStyleValueForProperty(selection, 'text-emphasis', ''),\n      ).toEqual('');\n\n      $patchStyleText(selection, {'text-emphasis': 'filled'});\n\n      expect(\n        $getSelectionStyleValueForProperty(selection, 'text-emphasis', ''),\n      ).toEqual('filled');\n    });\n  });\n\n  test('updates cached styles when setting on a collapsed selection', async () => {\n    const editor = createTestEditor();\n    const element = document.createElement('div');\n    editor.setRootElement(element);\n\n    await editor.update(() => {\n      const root = $getRoot();\n\n      const paragraph = $createParagraphNode();\n      root.append(paragraph);\n\n      const text = $createTextNode('text');\n      paragraph.append(text);\n\n      $setAnchorPoint({\n        key: text.getKey(),\n        offset: 0,\n        type: 'text',\n      });\n      $setFocusPoint({\n        key: text.getKey(),\n        offset: 0,\n        type: 'text',\n      });\n\n      // First fetch the initial style -- this will cause the CSS cache to be\n      // populated with an empty string pointing to an empty style object.\n      const selection = $getSelection();\n      if (!$isRangeSelection(selection)) {\n        return;\n      }\n      $getSelectionStyleValueForProperty(selection, 'color', '');\n\n      // Now when we set the style, we should _not_ touch the previously created\n      // empty style object, but create a new one instead.\n      $patchStyleText(selection, {color: 'red'});\n\n      // We can check that result by clearing the style and re-querying it.\n      ($getSelection() as RangeSelection).setStyle('');\n\n      const color = $getSelectionStyleValueForProperty(\n        $getSelection() as RangeSelection,\n        'color',\n        '',\n      );\n      expect(color).toEqual('');\n    });\n  });\n\n  test.each<TextModeType>(['token', 'segmented'])(\n    'can update style of text node that is in %s mode',\n    async (mode) => {\n      const editor = createTestEditor();\n\n      const element = document.createElement('div');\n      editor.setRootElement(element);\n\n      await editor.update(() => {\n        const root = $getRoot();\n\n        const paragraph = $createParagraphNode();\n        root.append(paragraph);\n\n        const text = $createTextNode('first').setFormat('bold');\n        paragraph.append(text);\n\n        const textInMode = $createTextNode('second').setMode(mode);\n        paragraph.append(textInMode);\n\n        $setAnchorPoint({\n          key: text.getKey(),\n          offset: 'fir'.length,\n          type: 'text',\n        });\n\n        $setFocusPoint({\n          key: textInMode.getKey(),\n          offset: 'sec'.length,\n          type: 'text',\n        });\n\n        const selection = $getSelection();\n        $patchStyleText(selection!, {'font-size': '15px'});\n      });\n\n      expect(element.innerHTML).toBe(\n        '<p>' +\n          '<strong data-lexical-text=\"true\">fir</strong>' +\n          '<strong style=\"font-size: 15px;\" data-lexical-text=\"true\">st</strong>' +\n          '<span style=\"font-size: 15px;\" data-lexical-text=\"true\">second</span>' +\n          '</p>',\n      );\n    },\n  );\n\n  test('preserve backward selection when changing style of 2 different text nodes', async () => {\n    const editor = createTestEditor();\n\n    const element = document.createElement('div');\n\n    editor.setRootElement(element);\n\n    editor.update(() => {\n      const root = $getRoot();\n\n      const paragraph = $createParagraphNode();\n      root.append(paragraph);\n\n      const firstText = $createTextNode('first ').setFormat('bold');\n      paragraph.append(firstText);\n\n      const secondText = $createTextNode('second').setFormat('italic');\n      paragraph.append(secondText);\n\n      $setAnchorPoint({\n        key: secondText.getKey(),\n        offset: 'sec'.length,\n        type: 'text',\n      });\n\n      $setFocusPoint({\n        key: firstText.getKey(),\n        offset: 'fir'.length,\n        type: 'text',\n      });\n\n      const selection = $getSelection();\n\n      $patchStyleText(selection!, {'font-size': '11px'});\n\n      const [newAnchor, newFocus] = selection!.getStartEndPoints()!;\n\n      const newAnchorNode: LexicalNode = newAnchor.getNode();\n      expect(newAnchorNode.getTextContent()).toBe('sec');\n      expect(newAnchor.offset).toBe('sec'.length);\n\n      const newFocusNode: LexicalNode = newFocus.getNode();\n      expect(newFocusNode.getTextContent()).toBe('st ');\n      expect(newFocus.offset).toBe(0);\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/selection/__tests__/utils/index.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {\n  $createTextNode,\n  $getSelection,\n  $isNodeSelection,\n  $isRangeSelection,\n  $isTextNode,\n  LexicalEditor,\n  PointType,\n} from 'lexical';\n\nObject.defineProperty(HTMLElement.prototype, 'contentEditable', {\n  get() {\n    return this.getAttribute('contenteditable');\n  },\n\n  set(value) {\n    this.setAttribute('contenteditable', value);\n  },\n});\n\ntype Segment = {\n  index: number;\n  isWordLike: boolean;\n  segment: string;\n};\n\nif (!Selection.prototype.modify) {\n  const wordBreakPolyfillRegex =\n    /[\\s.,\\\\/#!$%^&*;:{}=\\-`~()\\uD800-\\uDBFF\\uDC00-\\uDFFF\\u3000-\\u303F]/u;\n\n  const pushSegment = function (\n    segments: Array<Segment>,\n    index: number,\n    str: string,\n    isWordLike: boolean,\n  ): void {\n    segments.push({\n      index: index - str.length,\n      isWordLike,\n      segment: str,\n    });\n  };\n\n  const getWordsFromString = function (string: string): Array<Segment> {\n    const segments: Segment[] = [];\n    let wordString = '';\n    let nonWordString = '';\n    let i;\n\n    for (i = 0; i < string.length; i++) {\n      const char = string[i];\n\n      if (wordBreakPolyfillRegex.test(char)) {\n        if (wordString !== '') {\n          pushSegment(segments, i, wordString, true);\n          wordString = '';\n        }\n\n        nonWordString += char;\n      } else {\n        if (nonWordString !== '') {\n          pushSegment(segments, i, nonWordString, false);\n          nonWordString = '';\n        }\n\n        wordString += char;\n      }\n    }\n\n    if (wordString !== '') {\n      pushSegment(segments, i, wordString, true);\n    }\n\n    if (nonWordString !== '') {\n      pushSegment(segments, i, nonWordString, false);\n    }\n\n    return segments;\n  };\n\n  Selection.prototype.modify = function (alter, direction, granularity) {\n    // This is not a thorough implementation, it was more to get tests working\n    // given the refactor to use this selection method.\n    const symbol = Object.getOwnPropertySymbols(this)[0];\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const impl = (this as any)[symbol];\n    const focus = impl._focus;\n    const anchor = impl._anchor;\n\n    if (granularity === 'character') {\n      let anchorNode = anchor.node;\n      let anchorOffset = anchor.offset;\n      let _$isTextNode = false;\n\n      if (anchorNode.nodeType === 3) {\n        _$isTextNode = true;\n        anchorNode = anchorNode.parentElement;\n      } else if (anchorNode.nodeName === 'BR') {\n        const parentNode = anchorNode.parentElement;\n        const childNodes = Array.from(parentNode.childNodes);\n        anchorOffset = childNodes.indexOf(anchorNode);\n        anchorNode = parentNode;\n      }\n\n      if (direction === 'backward') {\n        if (anchorOffset === 0) {\n          let prevSibling = anchorNode.previousSibling;\n\n          if (prevSibling === null) {\n            prevSibling = anchorNode.parentElement.previousSibling.lastChild;\n          }\n\n          if (prevSibling.nodeName === 'P') {\n            prevSibling = prevSibling.firstChild;\n          }\n\n          if (prevSibling.nodeName === 'BR') {\n            anchor.node = prevSibling;\n            anchor.offset = 0;\n          } else {\n            anchor.node = prevSibling.firstChild;\n            anchor.offset = anchor.node.nodeValue.length - 1;\n          }\n        } else if (!_$isTextNode) {\n          anchor.node = anchorNode.childNodes[anchorOffset - 1];\n          anchor.offset = anchor.node.nodeValue.length - 1;\n        } else {\n          anchor.offset--;\n        }\n      } else {\n        if (\n          (_$isTextNode && anchorOffset === anchorNode.textContent.length) ||\n          (!_$isTextNode &&\n            (anchorNode.childNodes.length === anchorOffset ||\n              (anchorNode.childNodes.length === 1 &&\n                anchorNode.firstChild.nodeName === 'BR')))\n        ) {\n          let nextSibling = anchorNode.nextSibling;\n\n          if (nextSibling === null) {\n            nextSibling = anchorNode.parentElement.nextSibling.lastChild;\n          }\n\n          if (nextSibling.nodeName === 'P') {\n            nextSibling = nextSibling.lastChild;\n          }\n\n          if (nextSibling.nodeName === 'BR') {\n            anchor.node = nextSibling;\n            anchor.offset = 0;\n          } else {\n            anchor.node = nextSibling.firstChild;\n            anchor.offset = 0;\n          }\n        } else {\n          anchor.offset++;\n        }\n      }\n    } else if (granularity === 'word') {\n      const anchorNode = this.anchorNode!;\n      const targetTextContent =\n        direction === 'backward'\n          ? anchorNode.textContent!.slice(0, this.anchorOffset)\n          : anchorNode.textContent!.slice(this.anchorOffset);\n      const segments = getWordsFromString(targetTextContent);\n      const segmentsLength = segments.length;\n      let index = anchor.offset;\n      let foundWordNode = false;\n\n      if (direction === 'backward') {\n        for (let i = segmentsLength - 1; i >= 0; i--) {\n          const segment = segments[i];\n          const nextIndex = segment.index;\n\n          if (segment.isWordLike) {\n            index = nextIndex;\n            foundWordNode = true;\n          } else if (foundWordNode) {\n            break;\n          } else {\n            index = nextIndex;\n          }\n        }\n      } else {\n        for (let i = 0; i < segmentsLength; i++) {\n          const segment = segments[i];\n          const nextIndex = segment.index + segment.segment.length;\n\n          if (segment.isWordLike) {\n            index = nextIndex;\n            foundWordNode = true;\n          } else if (foundWordNode) {\n            break;\n          } else {\n            index = nextIndex;\n          }\n        }\n      }\n\n      if (direction === 'forward') {\n        index += anchor.offset;\n      }\n\n      anchor.offset = index;\n    }\n\n    if (alter === 'move') {\n      focus.offset = anchor.offset;\n      focus.node = anchor.node;\n    }\n  };\n}\n\nexport function printWhitespace(whitespaceCharacter: string) {\n  return whitespaceCharacter.charCodeAt(0) === 160\n    ? '&nbsp;'\n    : whitespaceCharacter;\n}\n\nexport function insertText(text: string) {\n  return {\n    text,\n    type: 'insert_text',\n  };\n}\n\nexport function insertTokenNode(text: string) {\n  return {\n    text,\n    type: 'insert_token_node',\n  };\n}\n\nexport function insertSegmentedNode(text: string) {\n  return {\n    text,\n    type: 'insert_segmented_node',\n  };\n}\n\nexport function convertToTokenNode() {\n  return {\n    text: null,\n    type: 'convert_to_token_node',\n  };\n}\n\nexport function convertToSegmentedNode() {\n  return {\n    text: null,\n    type: 'convert_to_segmented_node',\n  };\n}\n\nexport function insertParagraph() {\n  return {\n    type: 'insert_paragraph',\n  };\n}\n\nexport function deleteWordBackward(n: number | null | undefined) {\n  return {\n    text: null,\n    times: n,\n    type: 'delete_word_backward',\n  };\n}\n\nexport function deleteWordForward(n: number | null | undefined) {\n  return {\n    text: null,\n    times: n,\n    type: 'delete_word_forward',\n  };\n}\n\nexport function moveBackward(n: number | null | undefined) {\n  return {\n    text: null,\n    times: n,\n    type: 'move_backward',\n  };\n}\n\nexport function moveForward(n: number | null | undefined) {\n  return {\n    text: null,\n    times: n,\n    type: 'move_forward',\n  };\n}\n\nexport function moveEnd() {\n  return {\n    type: 'move_end',\n  };\n}\n\nexport function deleteBackward(n: number | null | undefined) {\n  return {\n    text: null,\n    times: n,\n    type: 'delete_backward',\n  };\n}\n\nexport function deleteForward(n: number | null | undefined) {\n  return {\n    text: null,\n    times: n,\n    type: 'delete_forward',\n  };\n}\n\nexport function formatBold() {\n  return {\n    format: 'bold',\n    type: 'format_text',\n  };\n}\n\nexport function formatItalic() {\n  return {\n    format: 'italic',\n    type: 'format_text',\n  };\n}\n\nexport function formatStrikeThrough() {\n  return {\n    format: 'strikethrough',\n    type: 'format_text',\n  };\n}\n\nexport function formatUnderline() {\n  return {\n    format: 'underline',\n    type: 'format_text',\n  };\n}\n\nexport function redo(n: number | null | undefined) {\n  return {\n    text: null,\n    times: n,\n    type: 'redo',\n  };\n}\n\nexport function undo(n: number | null | undefined) {\n  return {\n    text: null,\n    times: n,\n    type: 'undo',\n  };\n}\n\nexport function pastePlain(text: string) {\n  return {\n    text: text,\n    type: 'paste_plain',\n  };\n}\n\nexport function pasteLexical(text: string) {\n  return {\n    text: text,\n    type: 'paste_lexical',\n  };\n}\n\nexport function pasteHTML(text: string) {\n  return {\n    text: text,\n    type: 'paste_html',\n  };\n}\n\nexport function moveNativeSelection(\n  anchorPath: number[],\n  anchorOffset: number,\n  focusPath: number[],\n  focusOffset: number,\n) {\n  return {\n    anchorOffset,\n    anchorPath,\n    focusOffset,\n    focusPath,\n    type: 'move_native_selection',\n  };\n}\n\nexport function getNodeFromPath(path: number[], rootElement: Node) {\n  let node = rootElement;\n\n  for (let i = 0; i < path.length; i++) {\n    node = node.childNodes[path[i]];\n  }\n\n  return node;\n}\n\nexport function setNativeSelection(\n  anchorNode: Node,\n  anchorOffset: number,\n  focusNode: Node,\n  focusOffset: number,\n) {\n  const domSelection = window.getSelection()!;\n  const range = document.createRange();\n  range.setStart(anchorNode, anchorOffset);\n  range.setEnd(focusNode, focusOffset);\n  domSelection.removeAllRanges();\n  domSelection.addRange(range);\n}\n\nexport function setNativeSelectionWithPaths(\n  rootElement: Node,\n  anchorPath: number[],\n  anchorOffset: number,\n  focusPath: number[],\n  focusOffset: number,\n) {\n  const anchorNode = getNodeFromPath(anchorPath, rootElement);\n  const focusNode = getNodeFromPath(focusPath, rootElement);\n  setNativeSelection(anchorNode, anchorOffset, focusNode, focusOffset);\n}\n\nfunction getLastTextNode(startingNode: Node) {\n  let node = startingNode;\n\n  mainLoop: while (node !== null) {\n    if (node !== startingNode && node.nodeType === 3) {\n      return node;\n    }\n\n    const child = node.lastChild;\n\n    if (child !== null) {\n      node = child;\n      continue;\n    }\n\n    const previousSibling = node.previousSibling;\n\n    if (previousSibling !== null) {\n      node = previousSibling;\n      continue;\n    }\n\n    let parent = node.parentNode;\n\n    while (parent !== null) {\n      const parentSibling = parent.previousSibling;\n\n      if (parentSibling !== null) {\n        node = parentSibling;\n        continue mainLoop;\n      }\n\n      parent = parent.parentNode;\n    }\n  }\n\n  return null;\n}\n\nfunction getNextTextNode(startingNode: Node) {\n  let node = startingNode;\n\n  mainLoop: while (node !== null) {\n    if (node !== startingNode && node.nodeType === 3) {\n      return node;\n    }\n\n    const child = node.firstChild;\n\n    if (child !== null) {\n      node = child;\n      continue;\n    }\n\n    const nextSibling = node.nextSibling;\n\n    if (nextSibling !== null) {\n      node = nextSibling;\n      continue;\n    }\n\n    let parent = node.parentNode;\n\n    while (parent !== null) {\n      const parentSibling = parent.nextSibling;\n\n      if (parentSibling !== null) {\n        node = parentSibling;\n        continue mainLoop;\n      }\n\n      parent = parent.parentNode;\n    }\n  }\n\n  return null;\n}\n\nfunction moveNativeSelectionBackward() {\n  const domSelection = window.getSelection()!;\n  let anchorNode = domSelection.anchorNode!;\n  let anchorOffset = domSelection.anchorOffset!;\n\n  if (domSelection.isCollapsed) {\n    const target = (\n      anchorNode.nodeType === 1 ? anchorNode : anchorNode.parentNode\n    )!;\n    const keyDownEvent = new KeyboardEvent('keydown', {\n      bubbles: true,\n      cancelable: true,\n      key: 'ArrowLeft',\n      keyCode: 37,\n    });\n    target.dispatchEvent(keyDownEvent);\n\n    if (!keyDownEvent.defaultPrevented) {\n      if (anchorNode.nodeType === 3) {\n        if (anchorOffset === 0) {\n          const lastTextNode = getLastTextNode(anchorNode);\n\n          if (lastTextNode === null) {\n            throw new Error('moveNativeSelectionBackward: TODO');\n          } else {\n            const textLength = lastTextNode.nodeValue!.length;\n            setNativeSelection(\n              lastTextNode,\n              textLength,\n              lastTextNode,\n              textLength,\n            );\n          }\n        } else {\n          setNativeSelection(\n            anchorNode,\n            anchorOffset - 1,\n            anchorNode,\n            anchorOffset - 1,\n          );\n        }\n      } else if (anchorNode.nodeType === 1) {\n        if (anchorNode.nodeName === 'BR') {\n          const parentNode = anchorNode.parentNode!;\n          const childNodes = Array.from(parentNode.childNodes);\n          anchorOffset = childNodes.indexOf(anchorNode as ChildNode);\n          anchorNode = parentNode;\n        } else {\n          anchorOffset--;\n        }\n\n        setNativeSelection(anchorNode, anchorOffset, anchorNode, anchorOffset);\n      } else {\n        throw new Error('moveNativeSelectionBackward: TODO');\n      }\n    }\n\n    const keyUpEvent = new KeyboardEvent('keyup', {\n      bubbles: true,\n      cancelable: true,\n      key: 'ArrowLeft',\n      keyCode: 37,\n    });\n    target.dispatchEvent(keyUpEvent);\n  } else {\n    throw new Error('moveNativeSelectionBackward: TODO');\n  }\n}\n\nfunction moveNativeSelectionForward() {\n  const domSelection = window.getSelection()!;\n  const anchorNode = domSelection.anchorNode!;\n  const anchorOffset = domSelection.anchorOffset!;\n\n  if (domSelection.isCollapsed) {\n    const target = (\n      anchorNode.nodeType === 1 ? anchorNode : anchorNode.parentNode\n    )!;\n    const keyDownEvent = new KeyboardEvent('keydown', {\n      bubbles: true,\n      cancelable: true,\n      key: 'ArrowRight',\n      keyCode: 39,\n    });\n    target.dispatchEvent(keyDownEvent);\n\n    if (!keyDownEvent.defaultPrevented) {\n      if (anchorNode.nodeType === 3) {\n        const text = anchorNode.nodeValue!;\n\n        if (text.length === anchorOffset) {\n          const nextTextNode = getNextTextNode(anchorNode);\n\n          if (nextTextNode === null) {\n            throw new Error('moveNativeSelectionForward: TODO');\n          } else {\n            setNativeSelection(nextTextNode, 0, nextTextNode, 0);\n          }\n        } else {\n          setNativeSelection(\n            anchorNode,\n            anchorOffset + 1,\n            anchorNode,\n            anchorOffset + 1,\n          );\n        }\n      } else {\n        throw new Error('moveNativeSelectionForward: TODO');\n      }\n    }\n\n    const keyUpEvent = new KeyboardEvent('keyup', {\n      bubbles: true,\n      cancelable: true,\n      key: 'ArrowRight',\n      keyCode: 39,\n    });\n    target.dispatchEvent(keyUpEvent);\n  } else {\n    throw new Error('moveNativeSelectionForward: TODO');\n  }\n}\n\nexport async function applySelectionInputs(\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  inputs: Record<string, any>[],\n  update: (fn: () => void) => Promise<void>,\n  editor: LexicalEditor,\n) {\n  const rootElement = editor.getRootElement()!;\n  // Set initial focus as if we're in the editor\n  rootElement.focus();\n\n  for (let i = 0; i < inputs.length; i++) {\n    const input = inputs[i];\n    const times = input?.times ?? 1;\n\n    for (let j = 0; j < times; j++) {\n      await update(() => {\n        const selection = $getSelection()!;\n\n        switch (input.type) {\n          case 'insert_text': {\n            selection.insertText(input.text);\n            break;\n          }\n\n          case 'insert_paragraph': {\n            if ($isRangeSelection(selection)) {\n              selection.insertParagraph();\n            }\n            break;\n          }\n\n          case 'move_backward': {\n            moveNativeSelectionBackward();\n            break;\n          }\n\n          case 'move_forward': {\n            moveNativeSelectionForward();\n            break;\n          }\n\n          case 'move_end': {\n            if ($isRangeSelection(selection)) {\n              const anchorNode = selection.anchor.getNode();\n              if ($isTextNode(anchorNode)) {\n                anchorNode.select();\n              }\n            }\n            break;\n          }\n\n          case 'delete_backward': {\n            if ($isRangeSelection(selection)) {\n              selection.deleteCharacter(true);\n            }\n            break;\n          }\n\n          case 'delete_forward': {\n            if ($isRangeSelection(selection)) {\n              selection.deleteCharacter(false);\n            }\n            break;\n          }\n\n          case 'delete_word_backward': {\n            if ($isRangeSelection(selection)) {\n              selection.deleteWord(true);\n            }\n            break;\n          }\n\n          case 'delete_word_forward': {\n            if ($isRangeSelection(selection)) {\n              selection.deleteWord(false);\n            }\n            break;\n          }\n\n          case 'format_text': {\n            if ($isRangeSelection(selection)) {\n              selection.formatText(input.format);\n            }\n            break;\n          }\n\n          case 'move_native_selection': {\n            setNativeSelectionWithPaths(\n              rootElement,\n              input.anchorPath,\n              input.anchorOffset,\n              input.focusPath,\n              input.focusOffset,\n            );\n            break;\n          }\n\n          case 'insert_token_node': {\n            const text = $createTextNode(input.text);\n            text.setMode('token');\n            if ($isRangeSelection(selection)) {\n              selection.insertNodes([text]);\n            }\n            break;\n          }\n\n          case 'insert_segmented_node': {\n            const text = $createTextNode(input.text);\n            text.setMode('segmented');\n            if ($isRangeSelection(selection)) {\n              selection.insertNodes([text]);\n            }\n            text.selectNext();\n            break;\n          }\n\n          case 'convert_to_token_node': {\n            const text = $createTextNode(selection.getTextContent());\n            text.setMode('token');\n            if ($isRangeSelection(selection)) {\n              selection.insertNodes([text]);\n            }\n            text.selectNext();\n            break;\n          }\n\n          case 'convert_to_segmented_node': {\n            const text = $createTextNode(selection.getTextContent());\n            text.setMode('segmented');\n            if ($isRangeSelection(selection)) {\n              selection.insertNodes([text]);\n            }\n            text.selectNext();\n            break;\n          }\n\n          case 'undo': {\n            rootElement.dispatchEvent(\n              new KeyboardEvent('keydown', {\n                bubbles: true,\n                cancelable: true,\n                ctrlKey: true,\n                key: 'z',\n                keyCode: 90,\n              }),\n            );\n            break;\n          }\n\n          case 'redo': {\n            rootElement.dispatchEvent(\n              new KeyboardEvent('keydown', {\n                bubbles: true,\n                cancelable: true,\n                ctrlKey: true,\n                key: 'z',\n                keyCode: 90,\n                shiftKey: true,\n              }),\n            );\n            break;\n          }\n\n          case 'paste_plain': {\n            rootElement.dispatchEvent(\n              Object.assign(\n                new Event('paste', {\n                  bubbles: true,\n                  cancelable: true,\n                }),\n                {\n                  clipboardData: {\n                    getData: (type: string) => {\n                      if (type === 'text/plain') {\n                        return input.text;\n                      }\n\n                      return '';\n                    },\n                  },\n                },\n              ),\n            );\n            break;\n          }\n\n          case 'paste_lexical': {\n            rootElement.dispatchEvent(\n              Object.assign(\n                new Event('paste', {\n                  bubbles: true,\n                  cancelable: true,\n                }),\n                {\n                  clipboardData: {\n                    getData: (type: string) => {\n                      if (type === 'application/x-lexical-editor') {\n                        return input.text;\n                      }\n\n                      return '';\n                    },\n                  },\n                },\n              ),\n            );\n            break;\n          }\n\n          case 'paste_html': {\n            rootElement.dispatchEvent(\n              Object.assign(\n                new Event('paste', {\n                  bubbles: true,\n                  cancelable: true,\n                }),\n                {\n                  clipboardData: {\n                    getData: (type: string) => {\n                      if (type === 'text/html') {\n                        return input.text;\n                      }\n\n                      return '';\n                    },\n                  },\n                },\n              ),\n            );\n            break;\n          }\n        }\n      });\n    }\n  }\n}\n\nexport function $setAnchorPoint(\n  point: Pick<PointType, 'type' | 'offset' | 'key'>,\n) {\n  const selection = $getSelection();\n\n  if (!$isRangeSelection(selection)) {\n    const dummyTextNode = $createTextNode();\n    dummyTextNode.select();\n    return $setAnchorPoint(point);\n  }\n\n  if ($isNodeSelection(selection)) {\n    return;\n  }\n\n  const anchor = selection.anchor;\n  anchor.type = point.type;\n  anchor.offset = point.offset;\n  anchor.key = point.key;\n}\n\nexport function $setFocusPoint(\n  point: Pick<PointType, 'type' | 'offset' | 'key'>,\n) {\n  const selection = $getSelection();\n\n  if (!$isRangeSelection(selection)) {\n    const dummyTextNode = $createTextNode();\n    dummyTextNode.select();\n    return $setFocusPoint(point);\n  }\n\n  if ($isNodeSelection(selection)) {\n    return;\n  }\n\n  const focus = selection.focus;\n  focus.type = point.type;\n  focus.offset = point.offset;\n  focus.key = point.key;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/selection/constants.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\nexport const CSS_TO_STYLES: Map<string, Record<string, string>> = new Map();\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/selection/index.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {\n  $addNodeStyle,\n  $isAtNodeEnd,\n  $patchStyleText,\n  $sliceSelectedTextNodeContent,\n  $trimTextContentFromAnchor,\n} from './lexical-node';\nimport {\n  $getSelectionStyleValueForProperty,\n  $isParentElementRTL,\n  $moveCaretSelection,\n  $moveCharacter,\n  $selectAll,\n  $setBlocksType,\n  $shouldOverrideDefaultCharacterSelection,\n  $wrapNodes,\n} from './range-selection';\nimport {\n  createDOMRange,\n  createRectsFromDOMRange,\n  getStyleObjectFromCSS,\n} from './utils';\n\nexport {\n  /** @deprecated moved to the lexical package */ $cloneWithProperties,\n} from 'lexical';\nexport {\n  $addNodeStyle,\n  $isAtNodeEnd,\n  $patchStyleText,\n  $sliceSelectedTextNodeContent,\n  $trimTextContentFromAnchor,\n};\n/** @deprecated renamed to {@link $trimTextContentFromAnchor} by @lexical/eslint-plugin rules-of-lexical */\nexport const trimTextContentFromAnchor = $trimTextContentFromAnchor;\n\nexport {\n  $getSelectionStyleValueForProperty,\n  $isParentElementRTL,\n  $moveCaretSelection,\n  $moveCharacter,\n  $selectAll,\n  $setBlocksType,\n  $shouldOverrideDefaultCharacterSelection,\n  $wrapNodes,\n};\n\nexport {createDOMRange, createRectsFromDOMRange, getStyleObjectFromCSS};\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/selection/lexical-node.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\nimport {\n  $createTextNode,\n  $getCharacterOffsets,\n  $getNodeByKey,\n  $getPreviousSelection,\n  $isElementNode,\n  $isRangeSelection,\n  $isRootNode,\n  $isTextNode,\n  $isTokenOrSegmented,\n  BaseSelection,\n  LexicalEditor,\n  LexicalNode,\n  Point,\n  RangeSelection,\n  TextNode,\n} from 'lexical';\nimport invariant from 'lexical/shared/invariant';\n\nimport {CSS_TO_STYLES} from './constants';\nimport {\n  getCSSFromStyleObject,\n  getStyleObjectFromCSS,\n  getStyleObjectFromRawCSS,\n} from './utils';\n\n/**\n * Generally used to append text content to HTML and JSON. Grabs the text content and \"slices\"\n * it to be generated into the new TextNode.\n * @param selection - The selection containing the node whose TextNode is to be edited.\n * @param textNode - The TextNode to be edited.\n * @returns The updated TextNode.\n */\nexport function $sliceSelectedTextNodeContent(\n  selection: BaseSelection,\n  textNode: TextNode,\n): LexicalNode {\n  const anchorAndFocus = selection.getStartEndPoints();\n  if (\n    textNode.isSelected(selection) &&\n    !textNode.isSegmented() &&\n    !textNode.isToken() &&\n    anchorAndFocus !== null\n  ) {\n    const [anchor, focus] = anchorAndFocus;\n    const isBackward = selection.isBackward();\n    const anchorNode = anchor.getNode();\n    const focusNode = focus.getNode();\n    const isAnchor = textNode.is(anchorNode);\n    const isFocus = textNode.is(focusNode);\n\n    if (isAnchor || isFocus) {\n      const [anchorOffset, focusOffset] = $getCharacterOffsets(selection);\n      const isSame = anchorNode.is(focusNode);\n      const isFirst = textNode.is(isBackward ? focusNode : anchorNode);\n      const isLast = textNode.is(isBackward ? anchorNode : focusNode);\n      let startOffset = 0;\n      let endOffset = undefined;\n\n      if (isSame) {\n        startOffset = anchorOffset > focusOffset ? focusOffset : anchorOffset;\n        endOffset = anchorOffset > focusOffset ? anchorOffset : focusOffset;\n      } else if (isFirst) {\n        const offset = isBackward ? focusOffset : anchorOffset;\n        startOffset = offset;\n        endOffset = undefined;\n      } else if (isLast) {\n        const offset = isBackward ? anchorOffset : focusOffset;\n        startOffset = 0;\n        endOffset = offset;\n      }\n\n      textNode.__text = textNode.__text.slice(startOffset, endOffset);\n      return textNode;\n    }\n  }\n  return textNode;\n}\n\n/**\n * Determines if the current selection is at the end of the node.\n * @param point - The point of the selection to test.\n * @returns true if the provided point offset is in the last possible position, false otherwise.\n */\nexport function $isAtNodeEnd(point: Point): boolean {\n  if (point.type === 'text') {\n    return point.offset === point.getNode().getTextContentSize();\n  }\n  const node = point.getNode();\n  invariant(\n    $isElementNode(node),\n    'isAtNodeEnd: node must be a TextNode or ElementNode',\n  );\n\n  return point.offset === node.getChildrenSize();\n}\n\n/**\n * Trims text from a node in order to shorten it, eg. to enforce a text's max length. If it deletes text\n * that is an ancestor of the anchor then it will leave 2 indents, otherwise, if no text content exists, it deletes\n * the TextNode. It will move the focus to either the end of any left over text or beginning of a new TextNode.\n * @param editor - The lexical editor.\n * @param anchor - The anchor of the current selection, where the selection should be pointing.\n * @param delCount - The amount of characters to delete. Useful as a dynamic variable eg. textContentSize - maxLength;\n */\nexport function $trimTextContentFromAnchor(\n  editor: LexicalEditor,\n  anchor: Point,\n  delCount: number,\n): void {\n  // Work from the current selection anchor point\n  let currentNode: LexicalNode | null = anchor.getNode();\n  let remaining: number = delCount;\n\n  if ($isElementNode(currentNode)) {\n    const descendantNode = currentNode.getDescendantByIndex(anchor.offset);\n    if (descendantNode !== null) {\n      currentNode = descendantNode;\n    }\n  }\n\n  while (remaining > 0 && currentNode !== null) {\n    if ($isElementNode(currentNode)) {\n      const lastDescendant: null | LexicalNode =\n        currentNode.getLastDescendant<LexicalNode>();\n      if (lastDescendant !== null) {\n        currentNode = lastDescendant;\n      }\n    }\n    let nextNode: LexicalNode | null = currentNode.getPreviousSibling();\n    let additionalElementWhitespace = 0;\n    if (nextNode === null) {\n      let parent: LexicalNode | null = currentNode.getParentOrThrow();\n      let parentSibling: LexicalNode | null = parent.getPreviousSibling();\n\n      while (parentSibling === null) {\n        parent = parent.getParent();\n        if (parent === null) {\n          nextNode = null;\n          break;\n        }\n        parentSibling = parent.getPreviousSibling();\n      }\n      if (parent !== null) {\n        additionalElementWhitespace = parent.isInline() ? 0 : 2;\n        nextNode = parentSibling;\n      }\n    }\n    let text = currentNode.getTextContent();\n    // If the text is empty, we need to consider adding in two line breaks to match\n    // the content if we were to get it from its parent.\n    if (text === '' && $isElementNode(currentNode) && !currentNode.isInline()) {\n      // TODO: should this be handled in core?\n      text = '\\n\\n';\n    }\n    const currentNodeSize = text.length;\n\n    if (!$isTextNode(currentNode) || remaining >= currentNodeSize) {\n      const parent = currentNode.getParent();\n      currentNode.remove();\n      if (\n        parent != null &&\n        parent.getChildrenSize() === 0 &&\n        !$isRootNode(parent)\n      ) {\n        parent.remove();\n      }\n      remaining -= currentNodeSize + additionalElementWhitespace;\n      currentNode = nextNode;\n    } else {\n      const key = currentNode.getKey();\n      // See if we can just revert it to what was in the last editor state\n      const prevTextContent: string | null = editor\n        .getEditorState()\n        .read(() => {\n          const prevNode = $getNodeByKey(key);\n          if ($isTextNode(prevNode) && prevNode.isSimpleText()) {\n            return prevNode.getTextContent();\n          }\n          return null;\n        });\n      const offset = currentNodeSize - remaining;\n      const slicedText = text.slice(0, offset);\n      if (prevTextContent !== null && prevTextContent !== text) {\n        const prevSelection = $getPreviousSelection();\n        let target = currentNode;\n        if (!currentNode.isSimpleText()) {\n          const textNode = $createTextNode(prevTextContent);\n          currentNode.replace(textNode);\n          target = textNode;\n        } else {\n          currentNode.setTextContent(prevTextContent);\n        }\n        if ($isRangeSelection(prevSelection) && prevSelection.isCollapsed()) {\n          const prevOffset = prevSelection.anchor.offset;\n          target.select(prevOffset, prevOffset);\n        }\n      } else if (currentNode.isSimpleText()) {\n        // Split text\n        const isSelected = anchor.key === key;\n        let anchorOffset = anchor.offset;\n        // Move offset to end if it's less than the remaining number, otherwise\n        // we'll have a negative splitStart.\n        if (anchorOffset < remaining) {\n          anchorOffset = currentNodeSize;\n        }\n        const splitStart = isSelected ? anchorOffset - remaining : 0;\n        const splitEnd = isSelected ? anchorOffset : offset;\n        if (isSelected && splitStart === 0) {\n          const [excessNode] = currentNode.splitText(splitStart, splitEnd);\n          excessNode.remove();\n        } else {\n          const [, excessNode] = currentNode.splitText(splitStart, splitEnd);\n          excessNode.remove();\n        }\n      } else {\n        const textNode = $createTextNode(slicedText);\n        currentNode.replace(textNode);\n      }\n      remaining = 0;\n    }\n  }\n}\n\n/**\n * Gets the TextNode's style object and adds the styles to the CSS.\n * @param node - The TextNode to add styles to.\n */\nexport function $addNodeStyle(node: TextNode): void {\n  const CSSText = node.getStyle();\n  const styles = getStyleObjectFromRawCSS(CSSText);\n  CSS_TO_STYLES.set(CSSText, styles);\n}\n\nfunction $patchStyle(\n  target: TextNode | RangeSelection,\n  patch: Record<\n    string,\n    | string\n    | null\n    | ((currentStyleValue: string | null, _target: typeof target) => string)\n  >,\n): void {\n  const prevStyles = getStyleObjectFromCSS(\n    'getStyle' in target ? target.getStyle() : target.style,\n  );\n  const newStyles = Object.entries(patch).reduce<Record<string, string>>(\n    (styles, [key, value]) => {\n      if (typeof value === 'function') {\n        styles[key] = value(prevStyles[key], target);\n      } else if (value === null) {\n        delete styles[key];\n      } else {\n        styles[key] = value;\n      }\n      return styles;\n    },\n    {...prevStyles},\n  );\n  const newCSSText = getCSSFromStyleObject(newStyles);\n  target.setStyle(newCSSText);\n  CSS_TO_STYLES.set(newCSSText, newStyles);\n}\n\n/**\n * Applies the provided styles to the TextNodes in the provided Selection.\n * Will update partially selected TextNodes by splitting the TextNode and applying\n * the styles to the appropriate one.\n * @param selection - The selected node(s) to update.\n * @param patch - The patch to apply, which can include multiple styles. \\\\{CSSProperty: value\\\\} . Can also accept a function that returns the new property value.\n */\nexport function $patchStyleText(\n  selection: BaseSelection,\n  patch: Record<\n    string,\n    | string\n    | null\n    | ((\n        currentStyleValue: string | null,\n        target: TextNode | RangeSelection,\n      ) => string)\n  >,\n): void {\n  const selectedNodes = selection.getNodes();\n  const selectedNodesLength = selectedNodes.length;\n  const anchorAndFocus = selection.getStartEndPoints();\n  if (anchorAndFocus === null) {\n    return;\n  }\n  const [anchor, focus] = anchorAndFocus;\n\n  const lastIndex = selectedNodesLength - 1;\n  let firstNode = selectedNodes[0];\n  let lastNode = selectedNodes[lastIndex];\n\n  if (selection.isCollapsed() && $isRangeSelection(selection)) {\n    $patchStyle(selection, patch);\n    return;\n  }\n\n  const firstNodeText = firstNode.getTextContent();\n  const firstNodeTextLength = firstNodeText.length;\n  const focusOffset = focus.offset;\n  let anchorOffset = anchor.offset;\n  const isBefore = anchor.isBefore(focus);\n  let startOffset = isBefore ? anchorOffset : focusOffset;\n  let endOffset = isBefore ? focusOffset : anchorOffset;\n  const startType = isBefore ? anchor.type : focus.type;\n  const endType = isBefore ? focus.type : anchor.type;\n  const endKey = isBefore ? focus.key : anchor.key;\n\n  // This is the case where the user only selected the very end of the\n  // first node so we don't want to include it in the formatting change.\n  if ($isTextNode(firstNode) && startOffset === firstNodeTextLength) {\n    const nextSibling = firstNode.getNextSibling();\n\n    if ($isTextNode(nextSibling)) {\n      // we basically make the second node the firstNode, changing offsets accordingly\n      anchorOffset = 0;\n      startOffset = 0;\n      firstNode = nextSibling;\n    }\n  }\n\n  // This is the case where we only selected a single node\n  if (selectedNodes.length === 1) {\n    if ($isTextNode(firstNode) && firstNode.canHaveFormat()) {\n      startOffset =\n        startType === 'element'\n          ? 0\n          : anchorOffset > focusOffset\n          ? focusOffset\n          : anchorOffset;\n      endOffset =\n        endType === 'element'\n          ? firstNodeTextLength\n          : anchorOffset > focusOffset\n          ? anchorOffset\n          : focusOffset;\n\n      // No actual text is selected, so do nothing.\n      if (startOffset === endOffset) {\n        return;\n      }\n\n      // The entire node is selected or a token/segment, so just format it\n      if (\n        $isTokenOrSegmented(firstNode) ||\n        (startOffset === 0 && endOffset === firstNodeTextLength)\n      ) {\n        $patchStyle(firstNode, patch);\n        firstNode.select(startOffset, endOffset);\n      } else {\n        // The node is partially selected, so split it into two nodes\n        // and style the selected one.\n        const splitNodes = firstNode.splitText(startOffset, endOffset);\n        const replacement = startOffset === 0 ? splitNodes[0] : splitNodes[1];\n        $patchStyle(replacement, patch);\n        replacement.select(0, endOffset - startOffset);\n      }\n    } // multiple nodes selected.\n  } else {\n    if (\n      $isTextNode(firstNode) &&\n      startOffset < firstNode.getTextContentSize() &&\n      firstNode.canHaveFormat()\n    ) {\n      if (startOffset !== 0 && !$isTokenOrSegmented(firstNode)) {\n        // the entire first node isn't selected and it isn't a token or segmented, so split it\n        firstNode = firstNode.splitText(startOffset)[1];\n        startOffset = 0;\n        if (isBefore) {\n          anchor.set(firstNode.getKey(), startOffset, 'text');\n        } else {\n          focus.set(firstNode.getKey(), startOffset, 'text');\n        }\n      }\n\n      $patchStyle(firstNode as TextNode, patch);\n    }\n\n    if ($isTextNode(lastNode) && lastNode.canHaveFormat()) {\n      const lastNodeText = lastNode.getTextContent();\n      const lastNodeTextLength = lastNodeText.length;\n\n      // The last node might not actually be the end node\n      //\n      // If not, assume the last node is fully-selected unless the end offset is\n      // zero.\n      if (lastNode.__key !== endKey && endOffset !== 0) {\n        endOffset = lastNodeTextLength;\n      }\n\n      // if the entire last node isn't selected and it isn't a token or segmented, split it\n      if (endOffset !== lastNodeTextLength && !$isTokenOrSegmented(lastNode)) {\n        [lastNode] = lastNode.splitText(endOffset);\n      }\n\n      if (endOffset !== 0 || endType === 'element') {\n        $patchStyle(lastNode as TextNode, patch);\n      }\n    }\n\n    // style all the text nodes in between\n    for (let i = 1; i < lastIndex; i++) {\n      const selectedNode = selectedNodes[i];\n      const selectedNodeKey = selectedNode.getKey();\n\n      if (\n        $isTextNode(selectedNode) &&\n        selectedNode.canHaveFormat() &&\n        selectedNodeKey !== firstNode.getKey() &&\n        selectedNodeKey !== lastNode.getKey() &&\n        !selectedNode.isToken()\n      ) {\n        $patchStyle(selectedNode, patch);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/selection/range-selection.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {\n  BaseSelection,\n  ElementNode,\n  LexicalNode,\n  NodeKey,\n  Point,\n  RangeSelection,\n  TextNode,\n} from 'lexical';\n\nimport {TableSelection} from '@lexical/table';\nimport {\n  $getAdjacentNode,\n  $getPreviousSelection,\n  $getRoot,\n  $hasAncestor,\n  $isDecoratorNode,\n  $isElementNode,\n  $isLeafNode,\n  $isLineBreakNode,\n  $isRangeSelection,\n  $isRootNode,\n  $isRootOrShadowRoot,\n  $isTextNode,\n  $setSelection,\n} from 'lexical';\nimport invariant from 'lexical/shared/invariant';\n\nimport {getStyleObjectFromCSS} from './utils';\n\n/**\n * Converts all nodes in the selection that are of one block type to another.\n * @param selection - The selected blocks to be converted.\n * @param createElement - The function that creates the node. eg. $createParagraphNode.\n */\nexport function $setBlocksType(\n  selection: BaseSelection | null,\n  createElement: () => ElementNode,\n): void {\n  if (selection === null) {\n    return;\n  }\n  const anchorAndFocus = selection.getStartEndPoints();\n  const anchor = anchorAndFocus ? anchorAndFocus[0] : null;\n\n  if (anchor !== null && anchor.key === 'root') {\n    const element = createElement();\n    const root = $getRoot();\n    const firstChild = root.getFirstChild();\n\n    if (firstChild) {\n      firstChild.replace(element, true);\n    } else {\n      root.append(element);\n    }\n\n    return;\n  }\n\n  const nodes = selection.getNodes();\n  const firstSelectedBlock =\n    anchor !== null ? $getAncestor(anchor.getNode(), INTERNAL_$isBlock) : false;\n  if (firstSelectedBlock && nodes.indexOf(firstSelectedBlock) === -1) {\n    nodes.push(firstSelectedBlock);\n  }\n\n  for (let i = 0; i < nodes.length; i++) {\n    const node = nodes[i];\n\n    if (!INTERNAL_$isBlock(node)) {\n      continue;\n    }\n    invariant($isElementNode(node), 'Expected block node to be an ElementNode');\n\n    const targetElement = createElement();\n    node.replace(targetElement, true);\n  }\n}\n\nfunction isPointAttached(point: Point): boolean {\n  return point.getNode().isAttached();\n}\n\nfunction $removeParentEmptyElements(startingNode: ElementNode): void {\n  let node: ElementNode | null = startingNode;\n\n  while (node !== null && !$isRootOrShadowRoot(node)) {\n    const latest = node.getLatest();\n    const parentNode: ElementNode | null = node.getParent<ElementNode>();\n\n    if (latest.getChildrenSize() === 0) {\n      node.remove(true);\n    }\n\n    node = parentNode;\n  }\n}\n\n/**\n * @deprecated\n * Wraps all nodes in the selection into another node of the type returned by createElement.\n * @param selection - The selection of nodes to be wrapped.\n * @param createElement - A function that creates the wrapping ElementNode. eg. $createParagraphNode.\n * @param wrappingElement - An element to append the wrapped selection and its children to.\n */\nexport function $wrapNodes(\n  selection: BaseSelection,\n  createElement: () => ElementNode,\n  wrappingElement: null | ElementNode = null,\n): void {\n  const anchorAndFocus = selection.getStartEndPoints();\n  const anchor = anchorAndFocus ? anchorAndFocus[0] : null;\n  const nodes = selection.getNodes();\n  const nodesLength = nodes.length;\n\n  if (\n    anchor !== null &&\n    (nodesLength === 0 ||\n      (nodesLength === 1 &&\n        anchor.type === 'element' &&\n        anchor.getNode().getChildrenSize() === 0))\n  ) {\n    const target =\n      anchor.type === 'text'\n        ? anchor.getNode().getParentOrThrow()\n        : anchor.getNode();\n    const children = target.getChildren();\n    let element = createElement();\n    children.forEach((child) => element.append(child));\n\n    if (wrappingElement) {\n      element = wrappingElement.append(element);\n    }\n\n    target.replace(element);\n\n    return;\n  }\n\n  let topLevelNode = null;\n  let descendants: LexicalNode[] = [];\n  for (let i = 0; i < nodesLength; i++) {\n    const node = nodes[i];\n    // Determine whether wrapping has to be broken down into multiple chunks. This can happen if the\n    // user selected multiple Root-like nodes that have to be treated separately as if they are\n    // their own branch. I.e. you don't want to wrap a whole table, but rather the contents of each\n    // of each of the cell nodes.\n    if ($isRootOrShadowRoot(node)) {\n      $wrapNodesImpl(\n        selection,\n        descendants,\n        descendants.length,\n        createElement,\n        wrappingElement,\n      );\n      descendants = [];\n      topLevelNode = node;\n    } else if (\n      topLevelNode === null ||\n      (topLevelNode !== null && $hasAncestor(node, topLevelNode))\n    ) {\n      descendants.push(node);\n    } else {\n      $wrapNodesImpl(\n        selection,\n        descendants,\n        descendants.length,\n        createElement,\n        wrappingElement,\n      );\n      descendants = [node];\n    }\n  }\n  $wrapNodesImpl(\n    selection,\n    descendants,\n    descendants.length,\n    createElement,\n    wrappingElement,\n  );\n}\n\n/**\n * Wraps each node into a new ElementNode.\n * @param selection - The selection of nodes to wrap.\n * @param nodes - An array of nodes, generally the descendants of the selection.\n * @param nodesLength - The length of nodes.\n * @param createElement - A function that creates the wrapping ElementNode. eg. $createParagraphNode.\n * @param wrappingElement - An element to wrap all the nodes into.\n * @returns\n */\nexport function $wrapNodesImpl(\n  selection: BaseSelection,\n  nodes: LexicalNode[],\n  nodesLength: number,\n  createElement: () => ElementNode,\n  wrappingElement: null | ElementNode = null,\n): void {\n  if (nodes.length === 0) {\n    return;\n  }\n\n  const firstNode = nodes[0];\n  const elementMapping: Map<NodeKey, ElementNode> = new Map();\n  const elements = [];\n  // The below logic is to find the right target for us to\n  // either insertAfter/insertBefore/append the corresponding\n  // elements to. This is made more complicated due to nested\n  // structures.\n  let target = $isElementNode(firstNode)\n    ? firstNode\n    : firstNode.getParentOrThrow();\n\n  if (target.isInline()) {\n    target = target.getParentOrThrow();\n  }\n\n  let targetIsPrevSibling = false;\n  while (target !== null) {\n    const prevSibling = target.getPreviousSibling<ElementNode>();\n\n    if (prevSibling !== null) {\n      target = prevSibling;\n      targetIsPrevSibling = true;\n      break;\n    }\n\n    target = target.getParentOrThrow();\n\n    if ($isRootOrShadowRoot(target)) {\n      break;\n    }\n  }\n\n  const emptyElements = new Set();\n\n  // Find any top level empty elements\n  for (let i = 0; i < nodesLength; i++) {\n    const node = nodes[i];\n\n    if ($isElementNode(node) && node.getChildrenSize() === 0) {\n      emptyElements.add(node.getKey());\n    }\n  }\n\n  const movedNodes: Set<NodeKey> = new Set();\n\n  // Move out all leaf nodes into our elements array.\n  // If we find a top level empty element, also move make\n  // an element for that.\n  for (let i = 0; i < nodesLength; i++) {\n    const node = nodes[i];\n    let parent = node.getParent();\n\n    if (parent !== null && parent.isInline()) {\n      parent = parent.getParent();\n    }\n\n    if (\n      parent !== null &&\n      $isLeafNode(node) &&\n      !movedNodes.has(node.getKey())\n    ) {\n      const parentKey = parent.getKey();\n\n      if (elementMapping.get(parentKey) === undefined) {\n        const targetElement = createElement();\n        elements.push(targetElement);\n        elementMapping.set(parentKey, targetElement);\n        // Move node and its siblings to the new\n        // element.\n        parent.getChildren().forEach((child) => {\n          targetElement.append(child);\n          movedNodes.add(child.getKey());\n          if ($isElementNode(child)) {\n            // Skip nested leaf nodes if the parent has already been moved\n            child.getChildrenKeys().forEach((key) => movedNodes.add(key));\n          }\n        });\n        $removeParentEmptyElements(parent);\n      }\n    } else if (emptyElements.has(node.getKey())) {\n      invariant(\n        $isElementNode(node),\n        'Expected node in emptyElements to be an ElementNode',\n      );\n      const targetElement = createElement();\n      elements.push(targetElement);\n      node.remove(true);\n    }\n  }\n\n  if (wrappingElement !== null) {\n    for (let i = 0; i < elements.length; i++) {\n      const element = elements[i];\n      wrappingElement.append(element);\n    }\n  }\n  let lastElement = null;\n\n  // If our target is Root-like, let's see if we can re-adjust\n  // so that the target is the first child instead.\n  if ($isRootOrShadowRoot(target)) {\n    if (targetIsPrevSibling) {\n      if (wrappingElement !== null) {\n        target.insertAfter(wrappingElement);\n      } else {\n        for (let i = elements.length - 1; i >= 0; i--) {\n          const element = elements[i];\n          target.insertAfter(element);\n        }\n      }\n    } else {\n      const firstChild = target.getFirstChild();\n\n      if ($isElementNode(firstChild)) {\n        target = firstChild;\n      }\n\n      if (firstChild === null) {\n        if (wrappingElement) {\n          target.append(wrappingElement);\n        } else {\n          for (let i = 0; i < elements.length; i++) {\n            const element = elements[i];\n            target.append(element);\n            lastElement = element;\n          }\n        }\n      } else {\n        if (wrappingElement !== null) {\n          firstChild.insertBefore(wrappingElement);\n        } else {\n          for (let i = 0; i < elements.length; i++) {\n            const element = elements[i];\n            firstChild.insertBefore(element);\n            lastElement = element;\n          }\n        }\n      }\n    }\n  } else {\n    if (wrappingElement) {\n      target.insertAfter(wrappingElement);\n    } else {\n      for (let i = elements.length - 1; i >= 0; i--) {\n        const element = elements[i];\n        target.insertAfter(element);\n        lastElement = element;\n      }\n    }\n  }\n\n  const prevSelection = $getPreviousSelection();\n\n  if (\n    $isRangeSelection(prevSelection) &&\n    isPointAttached(prevSelection.anchor) &&\n    isPointAttached(prevSelection.focus)\n  ) {\n    $setSelection(prevSelection.clone());\n  } else if (lastElement !== null) {\n    lastElement.selectEnd();\n  } else {\n    selection.dirty = true;\n  }\n}\n\n/**\n * Determines if the default character selection should be overridden. Used with DecoratorNodes\n * @param selection - The selection whose default character selection may need to be overridden.\n * @param isBackward - Is the selection backwards (the focus comes before the anchor)?\n * @returns true if it should be overridden, false if not.\n */\nexport function $shouldOverrideDefaultCharacterSelection(\n  selection: RangeSelection,\n  isBackward: boolean,\n): boolean {\n  const possibleNode = $getAdjacentNode(selection.focus, isBackward);\n\n  return (\n    ($isDecoratorNode(possibleNode) && !possibleNode.isIsolated()) ||\n    ($isElementNode(possibleNode) &&\n      !possibleNode.isInline() &&\n      !possibleNode.canBeEmpty())\n  );\n}\n\n/**\n * Moves the selection according to the arguments.\n * @param selection - The selected text or nodes.\n * @param isHoldingShift - Is the shift key being held down during the operation.\n * @param isBackward - Is the selection selected backwards (the focus comes before the anchor)?\n * @param granularity - The distance to adjust the current selection.\n */\nexport function $moveCaretSelection(\n  selection: RangeSelection,\n  isHoldingShift: boolean,\n  isBackward: boolean,\n  granularity: 'character' | 'word' | 'lineboundary',\n): void {\n  selection.modify(isHoldingShift ? 'extend' : 'move', isBackward, granularity);\n}\n\n/**\n * Tests a parent element for right to left direction.\n * @param selection - The selection whose parent is to be tested.\n * @returns true if the selections' parent element has a direction of 'rtl' (right to left), false otherwise.\n */\nexport function $isParentElementRTL(selection: RangeSelection): boolean {\n  const anchorNode = selection.anchor.getNode();\n  const parent = $isRootNode(anchorNode)\n    ? anchorNode\n    : anchorNode.getParentOrThrow();\n\n  return parent.getDirection() === 'rtl';\n}\n\n/**\n * Moves selection by character according to arguments.\n * @param selection - The selection of the characters to move.\n * @param isHoldingShift - Is the shift key being held down during the operation.\n * @param isBackward - Is the selection backward (the focus comes before the anchor)?\n */\nexport function $moveCharacter(\n  selection: RangeSelection,\n  isHoldingShift: boolean,\n  isBackward: boolean,\n): void {\n  const isRTL = $isParentElementRTL(selection);\n  $moveCaretSelection(\n    selection,\n    isHoldingShift,\n    isBackward ? !isRTL : isRTL,\n    'character',\n  );\n}\n\n/**\n * Expands the current Selection to cover all of the content in the editor.\n * @param selection - The current selection.\n */\nexport function $selectAll(selection: RangeSelection): void {\n  const anchor = selection.anchor;\n  const focus = selection.focus;\n  const anchorNode = anchor.getNode();\n  const topParent = anchorNode.getTopLevelElementOrThrow();\n  const root = topParent.getParentOrThrow();\n  let firstNode = root.getFirstDescendant();\n  let lastNode = root.getLastDescendant();\n  let firstType: 'element' | 'text' = 'element';\n  let lastType: 'element' | 'text' = 'element';\n  let lastOffset = 0;\n\n  if ($isTextNode(firstNode)) {\n    firstType = 'text';\n  } else if (!$isElementNode(firstNode) && firstNode !== null) {\n    firstNode = firstNode.getParentOrThrow();\n  }\n\n  if ($isTextNode(lastNode)) {\n    lastType = 'text';\n    lastOffset = lastNode.getTextContentSize();\n  } else if (!$isElementNode(lastNode) && lastNode !== null) {\n    lastNode = lastNode.getParentOrThrow();\n  }\n\n  if (firstNode && lastNode) {\n    anchor.set(firstNode.getKey(), 0, firstType);\n    focus.set(lastNode.getKey(), lastOffset, lastType);\n  }\n}\n\n/**\n * Returns the current value of a CSS property for Nodes, if set. If not set, it returns the defaultValue.\n * @param node - The node whose style value to get.\n * @param styleProperty - The CSS style property.\n * @param defaultValue - The default value for the property.\n * @returns The value of the property for node.\n */\nfunction $getNodeStyleValueForProperty(\n  node: TextNode,\n  styleProperty: string,\n  defaultValue: string,\n): string {\n  const css = node.getStyle();\n  const styleObject = getStyleObjectFromCSS(css);\n\n  if (styleObject !== null) {\n    return styleObject[styleProperty] || defaultValue;\n  }\n\n  return defaultValue;\n}\n\n/**\n * Returns the current value of a CSS property for TextNodes in the Selection, if set. If not set, it returns the defaultValue.\n * If all TextNodes do not have the same value, it returns an empty string.\n * @param selection - The selection of TextNodes whose value to find.\n * @param styleProperty - The CSS style property.\n * @param defaultValue - The default value for the property, defaults to an empty string.\n * @returns The value of the property for the selected TextNodes.\n */\nexport function $getSelectionStyleValueForProperty(\n  selection: RangeSelection | TableSelection,\n  styleProperty: string,\n  defaultValue = '',\n): string {\n  let styleValue: string | null = null;\n  const nodes = selection.getNodes();\n  const anchor = selection.anchor;\n  const focus = selection.focus;\n  const isBackward = selection.isBackward();\n  const endOffset = isBackward ? focus.offset : anchor.offset;\n  const endNode = isBackward ? focus.getNode() : anchor.getNode();\n\n  if (\n    $isRangeSelection(selection) &&\n    selection.isCollapsed() &&\n    selection.style !== ''\n  ) {\n    const css = selection.style;\n    const styleObject = getStyleObjectFromCSS(css);\n\n    if (styleObject !== null && styleProperty in styleObject) {\n      return styleObject[styleProperty];\n    }\n  }\n\n  for (let i = 0; i < nodes.length; i++) {\n    const node = nodes[i];\n\n    // if no actual characters in the end node are selected, we don't\n    // include it in the selection for purposes of determining style\n    // value\n    if (i !== 0 && endOffset === 0 && node.is(endNode)) {\n      continue;\n    }\n\n    if ($isTextNode(node)) {\n      const nodeStyleValue = $getNodeStyleValueForProperty(\n        node,\n        styleProperty,\n        defaultValue,\n      );\n\n      if (styleValue === null) {\n        styleValue = nodeStyleValue;\n      } else if (styleValue !== nodeStyleValue) {\n        // multiple text nodes are in the selection and they don't all\n        // have the same style.\n        styleValue = '';\n        break;\n      }\n    }\n  }\n\n  return styleValue === null ? defaultValue : styleValue;\n}\n\n/**\n * This function is for internal use of the library.\n * Please do not use it as it may change in the future.\n */\nexport function INTERNAL_$isBlock(node: LexicalNode): node is ElementNode {\n  if ($isDecoratorNode(node)) {\n    return false;\n  }\n  if (!$isElementNode(node) || $isRootOrShadowRoot(node)) {\n    return false;\n  }\n\n  const firstChild = node.getFirstChild();\n  const isLeafElement =\n    firstChild === null ||\n    $isLineBreakNode(firstChild) ||\n    $isTextNode(firstChild) ||\n    firstChild.isInline();\n\n  return !node.isInline() && node.canBeEmpty() !== false && isLeafElement;\n}\n\nexport function $getAncestor<NodeType extends LexicalNode = LexicalNode>(\n  node: LexicalNode,\n  predicate: (ancestor: LexicalNode) => ancestor is NodeType,\n) {\n  let parent = node;\n  while (parent !== null && parent.getParent() !== null && !predicate(parent)) {\n    parent = parent.getParentOrThrow();\n  }\n  return predicate(parent) ? parent : null;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/selection/utils.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\nimport type {LexicalEditor, LexicalNode} from 'lexical';\n\nimport {$isTextNode} from 'lexical';\n\nimport {CSS_TO_STYLES} from './constants';\n\nfunction getDOMTextNode(element: Node | null): Text | null {\n  let node = element;\n\n  while (node != null) {\n    if (node.nodeType === Node.TEXT_NODE) {\n      return node as Text;\n    }\n\n    node = node.firstChild;\n  }\n\n  return null;\n}\n\nfunction getDOMIndexWithinParent(node: ChildNode): [ParentNode, number] {\n  const parent = node.parentNode;\n\n  if (parent == null) {\n    throw new Error('Should never happen');\n  }\n\n  return [parent, Array.from(parent.childNodes).indexOf(node)];\n}\n\n/**\n * Creates a selection range for the DOM.\n * @param editor - The lexical editor.\n * @param anchorNode - The anchor node of a selection.\n * @param _anchorOffset - The amount of space offset from the anchor to the focus.\n * @param focusNode - The current focus.\n * @param _focusOffset - The amount of space offset from the focus to the anchor.\n * @returns The range of selection for the DOM that was created.\n */\nexport function createDOMRange(\n  editor: LexicalEditor,\n  anchorNode: LexicalNode,\n  _anchorOffset: number,\n  focusNode: LexicalNode,\n  _focusOffset: number,\n): Range | null {\n  const anchorKey = anchorNode.getKey();\n  const focusKey = focusNode.getKey();\n  const range = document.createRange();\n  let anchorDOM: Node | Text | null = editor.getElementByKey(anchorKey);\n  let focusDOM: Node | Text | null = editor.getElementByKey(focusKey);\n  let anchorOffset = _anchorOffset;\n  let focusOffset = _focusOffset;\n\n  if ($isTextNode(anchorNode)) {\n    anchorDOM = getDOMTextNode(anchorDOM);\n  }\n\n  if ($isTextNode(focusNode)) {\n    focusDOM = getDOMTextNode(focusDOM);\n  }\n\n  if (\n    anchorNode === undefined ||\n    focusNode === undefined ||\n    anchorDOM === null ||\n    focusDOM === null\n  ) {\n    return null;\n  }\n\n  if (anchorDOM.nodeName === 'BR') {\n    [anchorDOM, anchorOffset] = getDOMIndexWithinParent(anchorDOM as ChildNode);\n  }\n\n  if (focusDOM.nodeName === 'BR') {\n    [focusDOM, focusOffset] = getDOMIndexWithinParent(focusDOM as ChildNode);\n  }\n\n  const firstChild = anchorDOM.firstChild;\n\n  if (\n    anchorDOM === focusDOM &&\n    firstChild != null &&\n    firstChild.nodeName === 'BR' &&\n    anchorOffset === 0 &&\n    focusOffset === 0\n  ) {\n    focusOffset = 1;\n  }\n\n  try {\n    range.setStart(anchorDOM, anchorOffset);\n    range.setEnd(focusDOM, focusOffset);\n  } catch (e) {\n    return null;\n  }\n\n  if (\n    range.collapsed &&\n    (anchorOffset !== focusOffset || anchorKey !== focusKey)\n  ) {\n    // Range is backwards, we need to reverse it\n    range.setStart(focusDOM, focusOffset);\n    range.setEnd(anchorDOM, anchorOffset);\n  }\n\n  return range;\n}\n\n/**\n * Creates DOMRects, generally used to help the editor find a specific location on the screen.\n * @param editor - The lexical editor\n * @param range - A fragment of a document that can contain nodes and parts of text nodes.\n * @returns The selectionRects as an array.\n */\nexport function createRectsFromDOMRange(\n  editor: LexicalEditor,\n  range: Range,\n): Array<ClientRect> {\n  const rootElement = editor.getRootElement();\n\n  if (rootElement === null) {\n    return [];\n  }\n  const rootRect = rootElement.getBoundingClientRect();\n  const computedStyle = getComputedStyle(rootElement);\n  const rootPadding =\n    parseFloat(computedStyle.paddingLeft) +\n    parseFloat(computedStyle.paddingRight);\n  const selectionRects = Array.from(range.getClientRects());\n  let selectionRectsLength = selectionRects.length;\n  //sort rects from top left to bottom right.\n  selectionRects.sort((a, b) => {\n    const top = a.top - b.top;\n    // Some rects match position closely, but not perfectly,\n    // so we give a 3px tolerance.\n    if (Math.abs(top) <= 3) {\n      return a.left - b.left;\n    }\n    return top;\n  });\n  let prevRect;\n  for (let i = 0; i < selectionRectsLength; i++) {\n    const selectionRect = selectionRects[i];\n    // Exclude rects that overlap preceding Rects in the sorted list.\n    const isOverlappingRect =\n      prevRect &&\n      prevRect.top <= selectionRect.top &&\n      prevRect.top + prevRect.height > selectionRect.top &&\n      prevRect.left + prevRect.width > selectionRect.left;\n    // Exclude selections that span the entire element\n    const selectionSpansElement =\n      selectionRect.width + rootPadding === rootRect.width;\n    if (isOverlappingRect || selectionSpansElement) {\n      selectionRects.splice(i--, 1);\n      selectionRectsLength--;\n      continue;\n    }\n    prevRect = selectionRect;\n  }\n  return selectionRects;\n}\n\n/**\n * Creates an object containing all the styles and their values provided in the CSS string.\n * @param css - The CSS string of styles and their values.\n * @returns The styleObject containing all the styles and their values.\n */\nexport function getStyleObjectFromRawCSS(css: string): Record<string, string> {\n  const styleObject: Record<string, string> = {};\n  const styles = css.split(';');\n\n  for (const style of styles) {\n    if (style !== '') {\n      const [key, value] = style.split(/:([^]+)/); // split on first colon\n      if (key && value) {\n        styleObject[key.trim()] = value.trim();\n      }\n    }\n  }\n\n  return styleObject;\n}\n\n/**\n * Given a CSS string, returns an object from the style cache.\n * @param css - The CSS property as a string.\n * @returns The value of the given CSS property.\n */\nexport function getStyleObjectFromCSS(css: string): Record<string, string> {\n  let value = CSS_TO_STYLES.get(css);\n  if (value === undefined) {\n    value = getStyleObjectFromRawCSS(css);\n    CSS_TO_STYLES.set(css, value);\n  }\n\n  if (__DEV__) {\n    // Freeze the value in DEV to prevent accidental mutations\n    Object.freeze(value);\n  }\n\n  return value;\n}\n\n/**\n * Gets the CSS styles from the style object.\n * @param styles - The style object containing the styles to get.\n * @returns A string containing the CSS styles and their values.\n */\nexport function getCSSFromStyleObject(styles: Record<string, string>): string {\n  let css = '';\n\n  for (const style in styles) {\n    if (style) {\n      css += `${style}: ${styles[style]};`;\n    }\n  }\n\n  return css;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/table/LexicalCaptionNode.ts",
    "content": "import {\n    $createTextNode,\n    DOMConversionMap,\n    DOMExportOutput,\n    EditorConfig,\n    ElementNode,\n    LexicalEditor,\n    LexicalNode,\n    SerializedElementNode\n} from \"lexical\";\nimport {TableNode} from \"@lexical/table/LexicalTableNode\";\n\n\nexport class CaptionNode extends ElementNode {\n    static getType(): string {\n        return 'caption';\n    }\n\n    static clone(node: CaptionNode): CaptionNode {\n        return new CaptionNode(node.__key);\n    }\n\n    createDOM(_config: EditorConfig, _editor: LexicalEditor): HTMLElement {\n        return document.createElement('caption');\n    }\n\n    updateDOM(_prevNode: unknown, _dom: HTMLElement, _config: EditorConfig): boolean {\n        return false;\n    }\n\n    isParentRequired(): true {\n        return true;\n    }\n\n    canBeEmpty(): boolean {\n        return false;\n    }\n\n    exportJSON(): SerializedElementNode {\n        return {\n            ...super.exportJSON(),\n            type: 'caption',\n            version: 1,\n        };\n    }\n\n    insertDOMIntoParent(nodeDOM: HTMLElement, parentDOM: HTMLElement): boolean {\n        parentDOM.insertBefore(nodeDOM, parentDOM.firstChild);\n        return true;\n    }\n\n    static importJSON(serializedNode: SerializedElementNode): CaptionNode {\n        return $createCaptionNode();\n    }\n\n    static importDOM(): DOMConversionMap | null {\n        return {\n            caption: (node: Node) => ({\n                conversion(domNode: Node) {\n                    return {\n                        node: $createCaptionNode(),\n                    }\n                },\n                priority: 0,\n            }),\n        };\n    }\n}\n\nexport function $createCaptionNode(): CaptionNode {\n    return new CaptionNode();\n}\n\nexport function $isCaptionNode(node: LexicalNode | null | undefined): node is CaptionNode {\n    return node instanceof CaptionNode;\n}\n\nexport function $tableHasCaption(table: TableNode): boolean {\n    for (const child of table.getChildren()) {\n        if ($isCaptionNode(child)) {\n            return true;\n        }\n    }\n    return false;\n}\n\nexport function $addCaptionToTable(table: TableNode, text: string = ''): void {\n    const caption = $createCaptionNode();\n    const textNode = $createTextNode(text || ' ');\n    caption.append(textNode);\n    table.append(caption);\n}"
  },
  {
    "path": "resources/js/wysiwyg/lexical/table/LexicalTableCellNode.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {\n  DOMConversionMap,\n  DOMConversionOutput,\n  DOMExportOutput,\n  EditorConfig,\n  LexicalEditor,\n  LexicalNode,\n  NodeKey,\n  SerializedElementNode,\n  Spread,\n} from 'lexical';\n\nimport {addClassNamesToElement} from '@lexical/utils';\nimport {\n  $applyNodeReplacement,\n  $createParagraphNode,\n  $isElementNode,\n  $isLineBreakNode,\n  $isTextNode,\n  ElementNode,\n} from 'lexical';\n\nimport {extractStyleMapFromElement, StyleMap} from \"../../utils/dom\";\nimport {CommonBlockAlignment, extractAlignmentFromElement} from \"lexical/nodes/common\";\n\nexport const TableCellHeaderStates = {\n  BOTH: 3,\n  COLUMN: 2,\n  NO_STATUS: 0,\n  ROW: 1,\n};\n\nexport type TableCellHeaderState =\n  typeof TableCellHeaderStates[keyof typeof TableCellHeaderStates];\n\nexport type SerializedTableCellNode = Spread<\n  {\n    colSpan?: number;\n    rowSpan?: number;\n    headerState: TableCellHeaderState;\n    width?: number;\n    backgroundColor?: null | string;\n    styles: Record<string, string>;\n    alignment: CommonBlockAlignment;\n  },\n  SerializedElementNode\n>;\n\n/** @noInheritDoc */\nexport class TableCellNode extends ElementNode {\n  /** @internal */\n  __colSpan: number;\n  /** @internal */\n  __rowSpan: number;\n  /** @internal */\n  __headerState: TableCellHeaderState;\n  /** @internal */\n  __width?: number;\n  /** @internal */\n  __backgroundColor: null | string;\n  /** @internal */\n  __styles: StyleMap = new Map;\n  /** @internal */\n  __alignment: CommonBlockAlignment = '';\n\n  static getType(): string {\n    return 'tablecell';\n  }\n\n  static clone(node: TableCellNode): TableCellNode {\n    const cellNode = new TableCellNode(\n      node.__headerState,\n      node.__colSpan,\n      node.__width,\n      node.__key,\n    );\n    cellNode.__rowSpan = node.__rowSpan;\n    cellNode.__backgroundColor = node.__backgroundColor;\n    cellNode.__styles = new Map(node.__styles);\n    cellNode.__alignment = node.__alignment;\n    return cellNode;\n  }\n\n  static importDOM(): DOMConversionMap | null {\n    return {\n      td: (node: Node) => ({\n        conversion: $convertTableCellNodeElement,\n        priority: 0,\n      }),\n      th: (node: Node) => ({\n        conversion: $convertTableCellNodeElement,\n        priority: 0,\n      }),\n    };\n  }\n\n  static importJSON(serializedNode: SerializedTableCellNode): TableCellNode {\n    const node = $createTableCellNode(\n        serializedNode.headerState,\n        serializedNode.colSpan,\n        serializedNode.width,\n    );\n\n    if (serializedNode.rowSpan) {\n        node.setRowSpan(serializedNode.rowSpan);\n    }\n\n    node.setStyles(new Map(Object.entries(serializedNode.styles)));\n    node.setAlignment(serializedNode.alignment);\n\n    return node;\n  }\n\n  constructor(\n    headerState = TableCellHeaderStates.NO_STATUS,\n    colSpan = 1,\n    width?: number,\n    key?: NodeKey,\n  ) {\n    super(key);\n    this.__colSpan = colSpan;\n    this.__rowSpan = 1;\n    this.__headerState = headerState;\n    this.__width = width;\n    this.__backgroundColor = null;\n  }\n\n  createDOM(config: EditorConfig): HTMLElement {\n    const element = document.createElement(\n      this.getTag(),\n    ) as HTMLTableCellElement;\n\n    if (this.__width) {\n      element.style.width = `${this.__width}px`;\n    }\n    if (this.__colSpan > 1) {\n      element.colSpan = this.__colSpan;\n    }\n    if (this.__rowSpan > 1) {\n      element.rowSpan = this.__rowSpan;\n    }\n    if (this.__backgroundColor !== null) {\n      element.style.backgroundColor = this.__backgroundColor;\n    }\n\n    addClassNamesToElement(\n      element,\n      config.theme.tableCell,\n      this.hasHeader() && config.theme.tableCellHeader,\n    );\n\n    for (const [name, value] of this.__styles.entries()) {\n      element.style.setProperty(name, value);\n    }\n\n    if (this.__alignment) {\n      element.classList.add('align-' + this.__alignment);\n    }\n\n    return element;\n  }\n\n  exportDOM(editor: LexicalEditor): DOMExportOutput {\n    const {element} = super.exportDOM(editor);\n    return {\n      element,\n    };\n  }\n\n  exportJSON(): SerializedTableCellNode {\n    return {\n      ...super.exportJSON(),\n      backgroundColor: this.getBackgroundColor(),\n      colSpan: this.__colSpan,\n      headerState: this.__headerState,\n      rowSpan: this.__rowSpan,\n      type: 'tablecell',\n      width: this.getWidth(),\n      styles: Object.fromEntries(this.__styles),\n      alignment: this.__alignment,\n    };\n  }\n\n  getColSpan(): number {\n    return this.__colSpan;\n  }\n\n  setColSpan(colSpan: number): this {\n    this.getWritable().__colSpan = colSpan;\n    return this;\n  }\n\n  getRowSpan(): number {\n    return this.__rowSpan;\n  }\n\n  setRowSpan(rowSpan: number): this {\n    this.getWritable().__rowSpan = rowSpan;\n    return this;\n  }\n\n  getTag(): string {\n    return this.hasHeader() ? 'th' : 'td';\n  }\n\n  setHeaderStyles(headerState: TableCellHeaderState): TableCellHeaderState {\n    const self = this.getWritable();\n    self.__headerState = headerState;\n    return this.__headerState;\n  }\n\n  getHeaderStyles(): TableCellHeaderState {\n    return this.getLatest().__headerState;\n  }\n\n  setWidth(width: number): number | null | undefined {\n    const self = this.getWritable();\n    self.__width = width;\n    return this.__width;\n  }\n\n  getWidth(): number | undefined {\n    return this.getLatest().__width;\n  }\n\n  clearWidth(): void {\n    const self = this.getWritable();\n    self.__width = undefined;\n  }\n\n  getStyles(): StyleMap {\n    const self = this.getLatest();\n    return new Map(self.__styles);\n  }\n\n  setStyles(styles: StyleMap): void {\n    const self = this.getWritable();\n    self.__styles = new Map(styles);\n  }\n\n  setAlignment(alignment: CommonBlockAlignment) {\n    const self = this.getWritable();\n    self.__alignment = alignment;\n  }\n\n  getAlignment(): CommonBlockAlignment {\n    const self = this.getLatest();\n    return self.__alignment;\n  }\n\n  updateTag(tag: string): void {\n    const isHeader = tag.toLowerCase() === 'th';\n    const state = isHeader ? TableCellHeaderStates.ROW : TableCellHeaderStates.NO_STATUS;\n    const self = this.getWritable();\n    self.__headerState = state;\n  }\n\n  getBackgroundColor(): null | string {\n    return this.getLatest().__backgroundColor;\n  }\n\n  setBackgroundColor(newBackgroundColor: null | string): void {\n    this.getWritable().__backgroundColor = newBackgroundColor;\n  }\n\n  toggleHeaderStyle(headerStateToToggle: TableCellHeaderState): TableCellNode {\n    const self = this.getWritable();\n\n    if ((self.__headerState & headerStateToToggle) === headerStateToToggle) {\n      self.__headerState -= headerStateToToggle;\n    } else {\n      self.__headerState += headerStateToToggle;\n    }\n\n    return self;\n  }\n\n  hasHeaderState(headerState: TableCellHeaderState): boolean {\n    return (this.getHeaderStyles() & headerState) === headerState;\n  }\n\n  hasHeader(): boolean {\n    return this.getLatest().__headerState !== TableCellHeaderStates.NO_STATUS;\n  }\n\n  updateDOM(prevNode: TableCellNode): boolean {\n    return (\n      prevNode.__headerState !== this.__headerState ||\n      prevNode.__width !== this.__width ||\n      prevNode.__colSpan !== this.__colSpan ||\n      prevNode.__rowSpan !== this.__rowSpan ||\n      prevNode.__backgroundColor !== this.__backgroundColor ||\n      prevNode.__styles !== this.__styles ||\n      prevNode.__alignment !== this.__alignment\n    );\n  }\n\n  isShadowRoot(): boolean {\n    return true;\n  }\n\n  collapseAtStart(): true {\n    return true;\n  }\n\n  canBeEmpty(): false {\n    return false;\n  }\n\n  canIndent(): false {\n    return false;\n  }\n}\n\nexport function $convertTableCellNodeElement(\n    domNode: Node,\n): DOMConversionOutput {\n  const domNode_ = domNode as HTMLTableCellElement;\n  const nodeName = domNode.nodeName.toLowerCase();\n\n  let width: number | undefined = undefined;\n\n\n  const PIXEL_VALUE_REG_EXP = /^(\\d+(?:\\.\\d+)?)px$/;\n  if (PIXEL_VALUE_REG_EXP.test(domNode_.style.width)) {\n    width = parseFloat(domNode_.style.width);\n  }\n\n  const tableCellNode = $createTableCellNode(\n      nodeName === 'th'\n          ? TableCellHeaderStates.ROW\n          : TableCellHeaderStates.NO_STATUS,\n      domNode_.colSpan,\n      width,\n  );\n\n  tableCellNode.__rowSpan = domNode_.rowSpan;\n\n  const style = domNode_.style;\n  const textDecoration = style.textDecoration.split(' ');\n  const hasBoldFontWeight =\n      style.fontWeight === '700' || style.fontWeight === 'bold';\n  const hasLinethroughTextDecoration = textDecoration.includes('line-through');\n  const hasItalicFontStyle = style.fontStyle === 'italic';\n  const hasUnderlineTextDecoration = textDecoration.includes('underline');\n\n  if (domNode instanceof HTMLElement) {\n    const styleMap = extractStyleMapFromElement(domNode);\n    styleMap.delete('background-color');\n    tableCellNode.setStyles(styleMap);\n    tableCellNode.setAlignment(extractAlignmentFromElement(domNode));\n  }\n\n  const background = style.backgroundColor || null;\n  if (background) {\n    tableCellNode.setBackgroundColor(background);\n  }\n\n  return {\n    after: (childLexicalNodes) => {\n      if (childLexicalNodes.length === 0) {\n        childLexicalNodes.push($createParagraphNode());\n      }\n      return childLexicalNodes;\n    },\n    forChild: (lexicalNode, parentLexicalNode) => {\n      if ($isTableCellNode(parentLexicalNode) && !$isElementNode(lexicalNode)) {\n        const paragraphNode = $createParagraphNode();\n        if (\n            $isLineBreakNode(lexicalNode) &&\n            lexicalNode.getTextContent() === '\\n'\n        ) {\n          return null;\n        }\n        if ($isTextNode(lexicalNode)) {\n          if (hasBoldFontWeight) {\n            lexicalNode.toggleFormat('bold');\n          }\n          if (hasLinethroughTextDecoration) {\n            lexicalNode.toggleFormat('strikethrough');\n          }\n          if (hasItalicFontStyle) {\n            lexicalNode.toggleFormat('italic');\n          }\n          if (hasUnderlineTextDecoration) {\n            lexicalNode.toggleFormat('underline');\n          }\n        }\n        paragraphNode.append(lexicalNode);\n        return paragraphNode;\n      }\n\n      return lexicalNode;\n    },\n    node: tableCellNode,\n  };\n}\n\nexport function $createTableCellNode(\n  headerState: TableCellHeaderState = TableCellHeaderStates.NO_STATUS,\n  colSpan = 1,\n  width?: number,\n): TableCellNode {\n  return $applyNodeReplacement(new TableCellNode(headerState, colSpan, width));\n}\n\nexport function $isTableCellNode(\n  node: LexicalNode | null | undefined,\n): node is TableCellNode {\n  return node instanceof TableCellNode;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/table/LexicalTableCommands.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {LexicalCommand} from 'lexical';\n\nimport {createCommand} from 'lexical';\n\nexport type InsertTableCommandPayloadHeaders =\n  | Readonly<{\n      rows: boolean;\n      columns: boolean;\n    }>\n  | boolean;\n\nexport type InsertTableCommandPayload = Readonly<{\n  columns: string;\n  rows: string;\n  includeHeaders?: InsertTableCommandPayloadHeaders;\n}>;\n\nexport const INSERT_TABLE_COMMAND: LexicalCommand<InsertTableCommandPayload> =\n  createCommand('INSERT_TABLE_COMMAND');\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/table/LexicalTableNode.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {TableCellNode} from './LexicalTableCellNode';\nimport {\n  DOMConversionMap,\n  DOMConversionOutput,\n  DOMExportOutput,\n  EditorConfig,\n  LexicalEditor,\n  LexicalNode,\n  NodeKey,\n  Spread,\n} from 'lexical';\n\nimport {addClassNamesToElement, isHTMLElement} from '@lexical/utils';\nimport {\n  $applyNodeReplacement,\n  $getNearestNodeFromDOMNode,\n\n} from 'lexical';\n\nimport {$isTableCellNode} from './LexicalTableCellNode';\nimport {TableDOMCell, TableDOMTable} from './LexicalTableObserver';\nimport {getTable} from './LexicalTableSelectionHelpers';\nimport {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} from \"lexical/nodes/CommonBlockNode\";\nimport {\n  applyCommonPropertyChanges,\n  commonPropertiesDifferent, deserializeCommonBlockNode,\n  setCommonBlockPropsFromElement,\n  updateElementWithCommonBlockProps\n} from \"lexical/nodes/common\";\nimport {el, extractStyleMapFromElement, StyleMap} from \"../../utils/dom\";\nimport {buildColgroupFromTableWidths, getTableColumnWidths} from \"../../utils/tables\";\n\nexport type SerializedTableNode = Spread<{\n  colWidths: string[];\n  styles: Record<string, string>,\n}, SerializedCommonBlockNode>\n\n/** @noInheritDoc */\nexport class TableNode extends CommonBlockNode {\n  __colWidths: string[] = [];\n  __styles: StyleMap = new Map;\n\n  static getType(): string {\n    return 'table';\n  }\n\n  static clone(node: TableNode): TableNode {\n    const newNode = new TableNode(node.__key);\n    copyCommonBlockProperties(node, newNode);\n    newNode.__colWidths = [...node.__colWidths];\n    newNode.__styles = new Map(node.__styles);\n    return newNode;\n  }\n\n  static importDOM(): DOMConversionMap | null {\n    return {\n      table: (_node: Node) => ({\n        conversion: $convertTableElement,\n        priority: 1,\n      }),\n    };\n  }\n\n  static importJSON(_serializedNode: SerializedTableNode): TableNode {\n    const node = $createTableNode();\n    deserializeCommonBlockNode(_serializedNode, node);\n    node.setColWidths(_serializedNode.colWidths);\n    node.setStyles(new Map(Object.entries(_serializedNode.styles)));\n    return node;\n  }\n\n  constructor(key?: NodeKey) {\n    super(key);\n  }\n\n  exportJSON(): SerializedTableNode {\n    return {\n      ...super.exportJSON(),\n      type: 'table',\n      version: 1,\n      colWidths: this.__colWidths,\n      styles: Object.fromEntries(this.__styles),\n    };\n  }\n\n  createDOM(config: EditorConfig, editor?: LexicalEditor): HTMLElement {\n    const tableElement = document.createElement('table');\n\n    addClassNamesToElement(tableElement, config.theme.table);\n\n    updateElementWithCommonBlockProps(tableElement, this);\n\n    const colWidths = this.getColWidths();\n    const colgroup = buildColgroupFromTableWidths(colWidths);\n    if (colgroup) {\n      tableElement.append(colgroup);\n    }\n\n    for (const [name, value] of this.__styles.entries()) {\n      tableElement.style.setProperty(name, value);\n    }\n\n    return tableElement;\n  }\n\n  updateDOM(_prevNode: TableNode, dom: HTMLElement): boolean {\n    applyCommonPropertyChanges(_prevNode, this, dom);\n\n    if (this.__colWidths.join(':') !== _prevNode.__colWidths.join(':')) {\n      const existingColGroup = Array.from(dom.children).find(child => child.nodeName === 'COLGROUP');\n      const newColGroup = buildColgroupFromTableWidths(this.__colWidths);\n      if (existingColGroup) {\n        existingColGroup.remove();\n      }\n\n      if (newColGroup) {\n        dom.prepend(newColGroup);\n      }\n    }\n\n    if (Array.from(this.__styles.values()).join(':') !== Array.from(_prevNode.__styles.values()).join(':')) {\n      dom.style.cssText = '';\n      for (const [name, value] of this.__styles.entries()) {\n        dom.style.setProperty(name, value);\n      }\n    }\n\n    return false;\n  }\n\n  exportDOM(editor: LexicalEditor): DOMExportOutput {\n    return {\n      ...super.exportDOM(editor),\n      after: (tableElement) => {\n        if (!tableElement) {\n          return;\n        }\n\n        const newElement = tableElement.cloneNode() as ParentNode;\n        const tBody = document.createElement('tbody');\n\n        if (isHTMLElement(tableElement)) {\n          for (const child of Array.from(tableElement.children)) {\n            if (child.nodeName === 'TR') {\n              tBody.append(child);\n            } else if (child.nodeName === 'CAPTION') {\n              newElement.insertBefore(child, newElement.firstChild);\n            } else {\n              newElement.append(child);\n            }\n          }\n        }\n\n        newElement.append(tBody);\n\n        return newElement as HTMLElement;\n      },\n    };\n  }\n\n  canBeEmpty(): false {\n    return false;\n  }\n\n  isShadowRoot(): boolean {\n    return true;\n  }\n\n  setColWidths(widths: string[]) {\n    const self = this.getWritable();\n    self.__colWidths = widths;\n  }\n\n  getColWidths(): string[] {\n    const self = this.getLatest();\n    return [...self.__colWidths];\n  }\n\n  getStyles(): StyleMap {\n    const self = this.getLatest();\n    return new Map(self.__styles);\n  }\n\n  setStyles(styles: StyleMap): void {\n    const self = this.getWritable();\n    self.__styles = new Map(styles);\n  }\n\n  getCordsFromCellNode(\n    tableCellNode: TableCellNode,\n    table: TableDOMTable,\n  ): {x: number; y: number} {\n    const {rows, domRows} = table;\n\n    for (let y = 0; y < rows; y++) {\n      const row = domRows[y];\n\n      if (row == null) {\n        continue;\n      }\n\n      const x = row.findIndex((cell) => {\n        if (!cell) {\n          return;\n        }\n        const {elem} = cell;\n        const cellNode = $getNearestNodeFromDOMNode(elem);\n        return cellNode === tableCellNode;\n      });\n\n      if (x !== -1) {\n        return {x, y};\n      }\n    }\n\n    throw new Error('Cell not found in table.');\n  }\n\n  getDOMCellFromCords(\n    x: number,\n    y: number,\n    table: TableDOMTable,\n  ): null | TableDOMCell {\n    const {domRows} = table;\n\n    const row = domRows[y];\n\n    if (row == null) {\n      return null;\n    }\n\n    const index = x < row.length ? x : row.length - 1;\n\n    const cell = row[index];\n\n    if (cell == null) {\n      return null;\n    }\n\n    return cell;\n  }\n\n  getDOMCellFromCordsOrThrow(\n    x: number,\n    y: number,\n    table: TableDOMTable,\n  ): TableDOMCell {\n    const cell = this.getDOMCellFromCords(x, y, table);\n\n    if (!cell) {\n      throw new Error('Cell not found at cords.');\n    }\n\n    return cell;\n  }\n\n  getCellNodeFromCords(\n    x: number,\n    y: number,\n    table: TableDOMTable,\n  ): null | TableCellNode {\n    const cell = this.getDOMCellFromCords(x, y, table);\n\n    if (cell == null) {\n      return null;\n    }\n\n    const node = $getNearestNodeFromDOMNode(cell.elem);\n\n    if ($isTableCellNode(node)) {\n      return node;\n    }\n\n    return null;\n  }\n\n  getCellNodeFromCordsOrThrow(\n    x: number,\n    y: number,\n    table: TableDOMTable,\n  ): TableCellNode {\n    const node = this.getCellNodeFromCords(x, y, table);\n\n    if (!node) {\n      throw new Error('Node at cords not TableCellNode.');\n    }\n\n    return node;\n  }\n\n  canSelectBefore(): true {\n    return true;\n  }\n\n  canIndent(): false {\n    return false;\n  }\n}\n\nexport function $getElementForTableNode(\n  editor: LexicalEditor,\n  tableNode: TableNode,\n): TableDOMTable {\n  const tableElement = editor.getElementByKey(tableNode.getKey());\n\n  if (tableElement == null) {\n    throw new Error('Table Element Not Found');\n  }\n\n  return getTable(tableElement);\n}\n\nexport function $convertTableElement(element: HTMLElement): DOMConversionOutput {\n  const node = $createTableNode();\n  setCommonBlockPropsFromElement(element, node);\n\n  const colWidths = getTableColumnWidths(element as HTMLTableElement);\n  node.setColWidths(colWidths);\n  node.setStyles(extractStyleMapFromElement(element));\n\n  return {node};\n}\n\nexport function $createTableNode(): TableNode {\n  return $applyNodeReplacement(new TableNode());\n}\n\nexport function $isTableNode(\n  node: LexicalNode | null | undefined,\n): node is TableNode {\n  return node instanceof TableNode;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/table/LexicalTableObserver.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {LexicalEditor, NodeKey, TextFormatType} from 'lexical';\n\nimport {\n  addClassNamesToElement,\n  removeClassNamesFromElement,\n} from '@lexical/utils';\nimport {\n  $createParagraphNode,\n  $createRangeSelection,\n  $createTextNode,\n  $getNearestNodeFromDOMNode,\n  $getNodeByKey,\n  $getRoot,\n  $getSelection,\n  $isElementNode,\n  $setSelection,\n  SELECTION_CHANGE_COMMAND,\n} from 'lexical';\nimport invariant from 'lexical/shared/invariant';\n\nimport {$isTableCellNode} from './LexicalTableCellNode';\nimport {$isTableNode} from './LexicalTableNode';\nimport {\n  $createTableSelection,\n  $isTableSelection,\n  type TableSelection,\n} from './LexicalTableSelection';\nimport {\n  $findTableNode,\n  $updateDOMForSelection,\n  getDOMSelection,\n  getTable,\n} from './LexicalTableSelectionHelpers';\n\nexport type TableDOMCell = {\n  elem: HTMLElement;\n  highlighted: boolean;\n  hasBackgroundColor: boolean;\n  x: number;\n  y: number;\n};\n\nexport type TableDOMRows = Array<Array<TableDOMCell | undefined> | undefined>;\n\nexport type TableDOMTable = {\n  domRows: TableDOMRows;\n  columns: number;\n  rows: number;\n};\n\nexport class TableObserver {\n  focusX: number;\n  focusY: number;\n  listenersToRemove: Set<() => void>;\n  table: TableDOMTable;\n  isHighlightingCells: boolean;\n  anchorX: number;\n  anchorY: number;\n  tableNodeKey: NodeKey;\n  anchorCell: TableDOMCell | null;\n  focusCell: TableDOMCell | null;\n  anchorCellNodeKey: NodeKey | null;\n  focusCellNodeKey: NodeKey | null;\n  editor: LexicalEditor;\n  tableSelection: TableSelection | null;\n  hasHijackedSelectionStyles: boolean;\n  isSelecting: boolean;\n\n  constructor(editor: LexicalEditor, tableNodeKey: string) {\n    this.isHighlightingCells = false;\n    this.anchorX = -1;\n    this.anchorY = -1;\n    this.focusX = -1;\n    this.focusY = -1;\n    this.listenersToRemove = new Set();\n    this.tableNodeKey = tableNodeKey;\n    this.editor = editor;\n    this.table = {\n      columns: 0,\n      domRows: [],\n      rows: 0,\n    };\n    this.tableSelection = null;\n    this.anchorCellNodeKey = null;\n    this.focusCellNodeKey = null;\n    this.anchorCell = null;\n    this.focusCell = null;\n    this.hasHijackedSelectionStyles = false;\n    this.trackTable();\n    this.isSelecting = false;\n  }\n\n  getTable(): TableDOMTable {\n    return this.table;\n  }\n\n  removeListeners() {\n    Array.from(this.listenersToRemove).forEach((removeListener) =>\n      removeListener(),\n    );\n  }\n\n  trackTable() {\n    const observer = new MutationObserver((records) => {\n      this.editor.update(() => {\n        let gridNeedsRedraw = false;\n\n        for (let i = 0; i < records.length; i++) {\n          const record = records[i];\n          const target = record.target;\n          const nodeName = target.nodeName;\n\n          if (\n            nodeName === 'TABLE' ||\n            nodeName === 'TBODY' ||\n            nodeName === 'THEAD' ||\n            nodeName === 'TR'\n          ) {\n            gridNeedsRedraw = true;\n            break;\n          }\n        }\n\n        if (!gridNeedsRedraw) {\n          return;\n        }\n\n        const tableElement = this.editor.getElementByKey(this.tableNodeKey);\n\n        if (!tableElement) {\n          throw new Error('Expected to find TableElement in DOM');\n        }\n\n        this.table = getTable(tableElement);\n      });\n    });\n    this.editor.update(() => {\n      const tableElement = this.editor.getElementByKey(this.tableNodeKey);\n\n      if (!tableElement) {\n        throw new Error('Expected to find TableElement in DOM');\n      }\n\n      this.table = getTable(tableElement);\n      observer.observe(tableElement, {\n        attributes: true,\n        childList: true,\n        subtree: true,\n      });\n    });\n  }\n\n  clearHighlight() {\n    const editor = this.editor;\n    this.isHighlightingCells = false;\n    this.anchorX = -1;\n    this.anchorY = -1;\n    this.focusX = -1;\n    this.focusY = -1;\n    this.tableSelection = null;\n    this.anchorCellNodeKey = null;\n    this.focusCellNodeKey = null;\n    this.anchorCell = null;\n    this.focusCell = null;\n    this.hasHijackedSelectionStyles = false;\n\n    this.enableHighlightStyle();\n\n    editor.update(() => {\n      const tableNode = $getNodeByKey(this.tableNodeKey);\n\n      if (!$isTableNode(tableNode)) {\n        throw new Error('Expected TableNode.');\n      }\n\n      const tableElement = editor.getElementByKey(this.tableNodeKey);\n\n      if (!tableElement) {\n        throw new Error('Expected to find TableElement in DOM');\n      }\n\n      const grid = getTable(tableElement);\n      $updateDOMForSelection(editor, grid, null);\n      $setSelection(null);\n      editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);\n    });\n  }\n\n  enableHighlightStyle() {\n    const editor = this.editor;\n    editor.update(() => {\n      const tableElement = editor.getElementByKey(this.tableNodeKey);\n\n      if (!tableElement) {\n        throw new Error('Expected to find TableElement in DOM');\n      }\n\n      removeClassNamesFromElement(\n        tableElement,\n        editor._config.theme.tableSelection,\n      );\n      tableElement.classList.remove('disable-selection');\n      this.hasHijackedSelectionStyles = false;\n    });\n  }\n\n  disableHighlightStyle() {\n    const editor = this.editor;\n    editor.update(() => {\n      const tableElement = editor.getElementByKey(this.tableNodeKey);\n\n      if (!tableElement) {\n        throw new Error('Expected to find TableElement in DOM');\n      }\n\n      addClassNamesToElement(tableElement, editor._config.theme.tableSelection);\n      this.hasHijackedSelectionStyles = true;\n    });\n  }\n\n  updateTableTableSelection(selection: TableSelection | null): void {\n    if (selection !== null && selection.tableKey === this.tableNodeKey) {\n      const editor = this.editor;\n      this.tableSelection = selection;\n      this.isHighlightingCells = true;\n      this.disableHighlightStyle();\n      $updateDOMForSelection(editor, this.table, this.tableSelection);\n    } else if (selection == null) {\n      this.clearHighlight();\n    } else {\n      this.tableNodeKey = selection.tableKey;\n      this.updateTableTableSelection(selection);\n    }\n  }\n\n  setFocusCellForSelection(cell: TableDOMCell, ignoreStart = false) {\n    const editor = this.editor;\n    editor.update(() => {\n      const tableNode = $getNodeByKey(this.tableNodeKey);\n\n      if (!$isTableNode(tableNode)) {\n        throw new Error('Expected TableNode.');\n      }\n\n      const tableElement = editor.getElementByKey(this.tableNodeKey);\n\n      if (!tableElement) {\n        throw new Error('Expected to find TableElement in DOM');\n      }\n\n      const cellX = cell.x;\n      const cellY = cell.y;\n      this.focusCell = cell;\n\n      if (this.anchorCell !== null) {\n        const domSelection = getDOMSelection(editor._window);\n        // Collapse the selection\n        if (domSelection) {\n          domSelection.setBaseAndExtent(\n            this.anchorCell.elem,\n            0,\n            this.focusCell.elem,\n            0,\n          );\n        }\n      }\n\n      if (\n        !this.isHighlightingCells &&\n        (this.anchorX !== cellX || this.anchorY !== cellY || ignoreStart)\n      ) {\n        this.isHighlightingCells = true;\n        this.disableHighlightStyle();\n      } else if (cellX === this.focusX && cellY === this.focusY) {\n        return;\n      }\n\n      this.focusX = cellX;\n      this.focusY = cellY;\n\n      if (this.isHighlightingCells) {\n        const focusTableCellNode = $getNearestNodeFromDOMNode(cell.elem);\n\n        if (\n          this.tableSelection != null &&\n          this.anchorCellNodeKey != null &&\n          $isTableCellNode(focusTableCellNode) &&\n          tableNode.is($findTableNode(focusTableCellNode))\n        ) {\n          const focusNodeKey = focusTableCellNode.getKey();\n\n          this.tableSelection =\n            this.tableSelection.clone() || $createTableSelection();\n\n          this.focusCellNodeKey = focusNodeKey;\n          this.tableSelection.set(\n            this.tableNodeKey,\n            this.anchorCellNodeKey,\n            this.focusCellNodeKey,\n          );\n\n          $setSelection(this.tableSelection);\n\n          editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);\n\n          $updateDOMForSelection(editor, this.table, this.tableSelection);\n        }\n      }\n    });\n  }\n\n  setAnchorCellForSelection(cell: TableDOMCell) {\n    this.isHighlightingCells = false;\n    this.anchorCell = cell;\n    this.anchorX = cell.x;\n    this.anchorY = cell.y;\n\n    this.editor.update(() => {\n      const anchorTableCellNode = $getNearestNodeFromDOMNode(cell.elem);\n\n      if ($isTableCellNode(anchorTableCellNode)) {\n        const anchorNodeKey = anchorTableCellNode.getKey();\n        this.tableSelection =\n          this.tableSelection != null\n            ? this.tableSelection.clone()\n            : $createTableSelection();\n        this.anchorCellNodeKey = anchorNodeKey;\n      }\n    });\n  }\n\n  formatCells(type: TextFormatType) {\n    this.editor.update(() => {\n      const selection = $getSelection();\n\n      if (!$isTableSelection(selection)) {\n        invariant(false, 'Expected grid selection');\n      }\n\n      const formatSelection = $createRangeSelection();\n\n      const anchor = formatSelection.anchor;\n      const focus = formatSelection.focus;\n\n      selection.getNodes().forEach((cellNode) => {\n        if ($isTableCellNode(cellNode) && cellNode.getTextContentSize() !== 0) {\n          anchor.set(cellNode.getKey(), 0, 'element');\n          focus.set(cellNode.getKey(), cellNode.getChildrenSize(), 'element');\n          formatSelection.formatText(type);\n        }\n      });\n\n      $setSelection(selection);\n\n      this.editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);\n    });\n  }\n\n  clearText() {\n    const editor = this.editor;\n    editor.update(() => {\n      const tableNode = $getNodeByKey(this.tableNodeKey);\n\n      if (!$isTableNode(tableNode)) {\n        throw new Error('Expected TableNode.');\n      }\n\n      const selection = $getSelection();\n\n      if (!$isTableSelection(selection)) {\n        invariant(false, 'Expected grid selection');\n      }\n\n      const selectedNodes = selection.getNodes().filter($isTableCellNode);\n\n      if (selectedNodes.length === this.table.columns * this.table.rows) {\n        tableNode.selectPrevious();\n        // Delete entire table\n        tableNode.remove();\n        const rootNode = $getRoot();\n        rootNode.selectStart();\n        return;\n      }\n\n      selectedNodes.forEach((cellNode) => {\n        if ($isElementNode(cellNode)) {\n          const paragraphNode = $createParagraphNode();\n          const textNode = $createTextNode();\n          paragraphNode.append(textNode);\n          cellNode.append(paragraphNode);\n          cellNode.getChildren().forEach((child) => {\n            if (child !== paragraphNode) {\n              child.remove();\n            }\n          });\n        }\n      });\n\n      $updateDOMForSelection(editor, this.table, null);\n\n      $setSelection(null);\n\n      editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);\n    });\n  }\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/table/LexicalTableRowNode.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {Spread} from 'lexical';\n\nimport {addClassNamesToElement} from '@lexical/utils';\nimport {\n  $applyNodeReplacement,\n  DOMConversionMap,\n  DOMConversionOutput,\n  EditorConfig,\n  ElementNode,\n  LexicalNode,\n  NodeKey,\n  SerializedElementNode,\n} from 'lexical';\n\nimport {extractStyleMapFromElement, sizeToPixels, StyleMap} from \"../../utils/dom\";\n\nexport type SerializedTableRowNode = Spread<\n  {\n    styles: Record<string, string>,\n    height?: number,\n  },\n  SerializedElementNode\n>;\n\n/** @noInheritDoc */\nexport class TableRowNode extends ElementNode {\n  /** @internal */\n  __height?: number;\n  /** @internal */\n  __styles: StyleMap = new Map();\n\n  static getType(): string {\n    return 'tablerow';\n  }\n\n  static clone(node: TableRowNode): TableRowNode {\n    const newNode = new TableRowNode(node.__key);\n    newNode.__styles = new Map(node.__styles);\n    return newNode;\n  }\n\n  static importDOM(): DOMConversionMap | null {\n    return {\n      tr: (node: Node) => ({\n        conversion: $convertTableRowElement,\n        priority: 0,\n      }),\n    };\n  }\n\n  static importJSON(serializedNode: SerializedTableRowNode): TableRowNode {\n    const node = $createTableRowNode();\n\n    node.setStyles(new Map(Object.entries(serializedNode.styles)));\n\n    return node;\n  }\n\n  constructor(key?: NodeKey) {\n    super(key);\n  }\n\n  exportJSON(): SerializedTableRowNode {\n    return {\n      ...super.exportJSON(),\n      type: 'tablerow',\n      version: 1,\n      styles: Object.fromEntries(this.__styles),\n      height: this.__height || 0,\n    };\n  }\n\n  createDOM(config: EditorConfig): HTMLElement {\n    const element = document.createElement('tr');\n\n    if (this.__height) {\n      element.style.height = `${this.__height}px`;\n    }\n\n    for (const [name, value] of this.__styles.entries()) {\n      element.style.setProperty(name, value);\n    }\n\n    addClassNamesToElement(element, config.theme.tableRow);\n\n    return element;\n  }\n\n  isShadowRoot(): boolean {\n    return true;\n  }\n\n  getStyles(): StyleMap {\n    const self = this.getLatest();\n    return new Map(self.__styles);\n  }\n\n  setStyles(styles: StyleMap): void {\n    const self = this.getWritable();\n    self.__styles = new Map(styles);\n  }\n\n  setHeight(height: number): number | null | undefined {\n    const self = this.getWritable();\n    self.__height = height;\n    return this.__height;\n  }\n\n  getHeight(): number | undefined {\n    return this.getLatest().__height;\n  }\n\n  updateDOM(prevNode: TableRowNode): boolean {\n    return prevNode.__height !== this.__height\n        || prevNode.__styles !== this.__styles;\n  }\n\n  canBeEmpty(): false {\n    return false;\n  }\n\n  canIndent(): false {\n    return false;\n  }\n}\n\nexport function $convertTableRowElement(domNode: Node): DOMConversionOutput {\n  const rowNode = $createTableRowNode();\n  const domNode_ = domNode as HTMLElement;\n\n  const height = sizeToPixels(domNode_.style.height);\n  rowNode.setHeight(height);\n\n  if (domNode instanceof HTMLElement) {\n    rowNode.setStyles(extractStyleMapFromElement(domNode));\n  }\n\n  return {node: rowNode};\n}\n\nexport function $createTableRowNode(): TableRowNode {\n  return $applyNodeReplacement(new TableRowNode());\n}\n\nexport function $isTableRowNode(\n  node: LexicalNode | null | undefined,\n): node is TableRowNode {\n  return node instanceof TableRowNode;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/table/LexicalTableSelection.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {$findMatchingParent} from '@lexical/utils';\nimport {\n  $createPoint,\n  $getNodeByKey,\n  $isElementNode,\n  $normalizeSelection__EXPERIMENTAL,\n  BaseSelection,\n  isCurrentlyReadOnlyMode,\n  LexicalNode,\n  NodeKey,\n  PointType,\n} from 'lexical';\nimport invariant from 'lexical/shared/invariant';\n\nimport {$isTableCellNode, TableCellNode} from './LexicalTableCellNode';\nimport {$isTableNode} from './LexicalTableNode';\nimport {$isTableRowNode} from './LexicalTableRowNode';\nimport {$computeTableMap, $getTableCellNodeRect} from './LexicalTableUtils';\n\nexport type TableSelectionShape = {\n  fromX: number;\n  fromY: number;\n  toX: number;\n  toY: number;\n};\n\nexport type TableMapValueType = {\n  cell: TableCellNode;\n  startRow: number;\n  startColumn: number;\n};\nexport type TableMapType = Array<Array<TableMapValueType>>;\n\nexport class TableSelection implements BaseSelection {\n  tableKey: NodeKey;\n  anchor: PointType;\n  focus: PointType;\n  _cachedNodes: Array<LexicalNode> | null;\n  dirty: boolean;\n\n  constructor(tableKey: NodeKey, anchor: PointType, focus: PointType) {\n    this.anchor = anchor;\n    this.focus = focus;\n    anchor._selection = this;\n    focus._selection = this;\n    this._cachedNodes = null;\n    this.dirty = false;\n    this.tableKey = tableKey;\n  }\n\n  getStartEndPoints(): [PointType, PointType] {\n    return [this.anchor, this.focus];\n  }\n\n  /**\n   * Returns whether the Selection is \"backwards\", meaning the focus\n   * logically precedes the anchor in the EditorState.\n   * @returns true if the Selection is backwards, false otherwise.\n   */\n  isBackward(): boolean {\n    return this.focus.isBefore(this.anchor);\n  }\n\n  getCachedNodes(): LexicalNode[] | null {\n    return this._cachedNodes;\n  }\n\n  setCachedNodes(nodes: LexicalNode[] | null): void {\n    this._cachedNodes = nodes;\n  }\n\n  is(selection: null | BaseSelection): boolean {\n    if (!$isTableSelection(selection)) {\n      return false;\n    }\n    return (\n      this.tableKey === selection.tableKey &&\n      this.anchor.is(selection.anchor) &&\n      this.focus.is(selection.focus)\n    );\n  }\n\n  set(tableKey: NodeKey, anchorCellKey: NodeKey, focusCellKey: NodeKey): void {\n    this.dirty = true;\n    this.tableKey = tableKey;\n    this.anchor.key = anchorCellKey;\n    this.focus.key = focusCellKey;\n    this._cachedNodes = null;\n  }\n\n  clone(): TableSelection {\n    return new TableSelection(this.tableKey, this.anchor, this.focus);\n  }\n\n  isCollapsed(): boolean {\n    return false;\n  }\n\n  extract(): Array<LexicalNode> {\n    return this.getNodes();\n  }\n\n  insertRawText(text: string): void {\n    // Do nothing?\n  }\n\n  insertText(): void {\n    // Do nothing?\n  }\n\n  insertNodes(nodes: Array<LexicalNode>) {\n    const focusNode = this.focus.getNode();\n    invariant(\n      $isElementNode(focusNode),\n      'Expected TableSelection focus to be an ElementNode',\n    );\n    const selection = $normalizeSelection__EXPERIMENTAL(\n      focusNode.select(0, focusNode.getChildrenSize()),\n    );\n    selection.insertNodes(nodes);\n  }\n\n  // TODO Deprecate this method. It's confusing when used with colspan|rowspan\n  getShape(): TableSelectionShape {\n    const anchorCellNode = $getNodeByKey(this.anchor.key);\n    invariant(\n      $isTableCellNode(anchorCellNode),\n      'Expected TableSelection anchor to be (or a child of) TableCellNode',\n    );\n    const anchorCellNodeRect = $getTableCellNodeRect(anchorCellNode);\n    invariant(\n      anchorCellNodeRect !== null,\n      'getCellRect: expected to find AnchorNode',\n    );\n\n    const focusCellNode = $getNodeByKey(this.focus.key);\n    invariant(\n      $isTableCellNode(focusCellNode),\n      'Expected TableSelection focus to be (or a child of) TableCellNode',\n    );\n    const focusCellNodeRect = $getTableCellNodeRect(focusCellNode);\n    invariant(\n      focusCellNodeRect !== null,\n      'getCellRect: expected to find focusCellNode',\n    );\n\n    const startX = Math.min(\n      anchorCellNodeRect.columnIndex,\n      focusCellNodeRect.columnIndex,\n    );\n    const stopX = Math.max(\n      anchorCellNodeRect.columnIndex,\n      focusCellNodeRect.columnIndex,\n    );\n\n    const startY = Math.min(\n      anchorCellNodeRect.rowIndex,\n      focusCellNodeRect.rowIndex,\n    );\n    const stopY = Math.max(\n      anchorCellNodeRect.rowIndex,\n      focusCellNodeRect.rowIndex,\n    );\n\n    return {\n      fromX: Math.min(startX, stopX),\n      fromY: Math.min(startY, stopY),\n      toX: Math.max(startX, stopX),\n      toY: Math.max(startY, stopY),\n    };\n  }\n\n  getNodes(): Array<LexicalNode> {\n    const cachedNodes = this._cachedNodes;\n    if (cachedNodes !== null) {\n      return cachedNodes;\n    }\n\n    const anchorNode = this.anchor.getNode();\n    const focusNode = this.focus.getNode();\n    const anchorCell = $findMatchingParent(anchorNode, $isTableCellNode);\n    // todo replace with triplet\n    const focusCell = $findMatchingParent(focusNode, $isTableCellNode);\n    invariant(\n      $isTableCellNode(anchorCell),\n      'Expected TableSelection anchor to be (or a child of) TableCellNode',\n    );\n    invariant(\n      $isTableCellNode(focusCell),\n      'Expected TableSelection focus to be (or a child of) TableCellNode',\n    );\n    const anchorRow = anchorCell.getParent();\n    invariant(\n      $isTableRowNode(anchorRow),\n      'Expected anchorCell to have a parent TableRowNode',\n    );\n    const tableNode = anchorRow.getParent();\n    invariant(\n      $isTableNode(tableNode),\n      'Expected tableNode to have a parent TableNode',\n    );\n\n    const focusCellGrid = focusCell.getParents()[1];\n    if (focusCellGrid !== tableNode) {\n      if (!tableNode.isParentOf(focusCell)) {\n        // focus is on higher Grid level than anchor\n        const gridParent = tableNode.getParent();\n        invariant(gridParent != null, 'Expected gridParent to have a parent');\n        this.set(this.tableKey, gridParent.getKey(), focusCell.getKey());\n      } else {\n        // anchor is on higher Grid level than focus\n        const focusCellParent = focusCellGrid.getParent();\n        invariant(\n          focusCellParent != null,\n          'Expected focusCellParent to have a parent',\n        );\n        this.set(this.tableKey, focusCell.getKey(), focusCellParent.getKey());\n      }\n      return this.getNodes();\n    }\n\n    // TODO Mapping the whole Grid every time not efficient. We need to compute the entire state only\n    // once (on load) and iterate on it as updates occur. However, to do this we need to have the\n    // ability to store a state. Killing TableSelection and moving the logic to the plugin would make\n    // this possible.\n    const [map, cellAMap, cellBMap] = $computeTableMap(\n      tableNode,\n      anchorCell,\n      focusCell,\n    );\n\n    let minColumn = Math.min(cellAMap.startColumn, cellBMap.startColumn);\n    let minRow = Math.min(cellAMap.startRow, cellBMap.startRow);\n    let maxColumn = Math.max(\n      cellAMap.startColumn + cellAMap.cell.__colSpan - 1,\n      cellBMap.startColumn + cellBMap.cell.__colSpan - 1,\n    );\n    let maxRow = Math.max(\n      cellAMap.startRow + cellAMap.cell.__rowSpan - 1,\n      cellBMap.startRow + cellBMap.cell.__rowSpan - 1,\n    );\n    let exploredMinColumn = minColumn;\n    let exploredMinRow = minRow;\n    let exploredMaxColumn = minColumn;\n    let exploredMaxRow = minRow;\n    function expandBoundary(mapValue: TableMapValueType): void {\n      const {\n        cell,\n        startColumn: cellStartColumn,\n        startRow: cellStartRow,\n      } = mapValue;\n      minColumn = Math.min(minColumn, cellStartColumn);\n      minRow = Math.min(minRow, cellStartRow);\n      maxColumn = Math.max(maxColumn, cellStartColumn + cell.__colSpan - 1);\n      maxRow = Math.max(maxRow, cellStartRow + cell.__rowSpan - 1);\n    }\n    while (\n      minColumn < exploredMinColumn ||\n      minRow < exploredMinRow ||\n      maxColumn > exploredMaxColumn ||\n      maxRow > exploredMaxRow\n    ) {\n      if (minColumn < exploredMinColumn) {\n        // Expand on the left\n        const rowDiff = exploredMaxRow - exploredMinRow;\n        const previousColumn = exploredMinColumn - 1;\n        for (let i = 0; i <= rowDiff; i++) {\n          expandBoundary(map[exploredMinRow + i][previousColumn]);\n        }\n        exploredMinColumn = previousColumn;\n      }\n      if (minRow < exploredMinRow) {\n        // Expand on top\n        const columnDiff = exploredMaxColumn - exploredMinColumn;\n        const previousRow = exploredMinRow - 1;\n        for (let i = 0; i <= columnDiff; i++) {\n          expandBoundary(map[previousRow][exploredMinColumn + i]);\n        }\n        exploredMinRow = previousRow;\n      }\n      if (maxColumn > exploredMaxColumn) {\n        // Expand on the right\n        const rowDiff = exploredMaxRow - exploredMinRow;\n        const nextColumn = exploredMaxColumn + 1;\n        for (let i = 0; i <= rowDiff; i++) {\n          expandBoundary(map[exploredMinRow + i][nextColumn]);\n        }\n        exploredMaxColumn = nextColumn;\n      }\n      if (maxRow > exploredMaxRow) {\n        // Expand on the bottom\n        const columnDiff = exploredMaxColumn - exploredMinColumn;\n        const nextRow = exploredMaxRow + 1;\n        for (let i = 0; i <= columnDiff; i++) {\n          expandBoundary(map[nextRow][exploredMinColumn + i]);\n        }\n        exploredMaxRow = nextRow;\n      }\n    }\n\n    const nodes: Array<LexicalNode> = [tableNode];\n    let lastRow = null;\n    for (let i = minRow; i <= maxRow; i++) {\n      for (let j = minColumn; j <= maxColumn; j++) {\n        const {cell} = map[i][j];\n        const currentRow = cell.getParent();\n        invariant(\n          $isTableRowNode(currentRow),\n          'Expected TableCellNode parent to be a TableRowNode',\n        );\n        if (currentRow !== lastRow) {\n          nodes.push(currentRow);\n        }\n        nodes.push(cell, ...$getChildrenRecursively(cell));\n        lastRow = currentRow;\n      }\n    }\n\n    if (!isCurrentlyReadOnlyMode()) {\n      this._cachedNodes = nodes;\n    }\n    return nodes;\n  }\n\n  getTextContent(): string {\n    const nodes = this.getNodes().filter((node) => $isTableCellNode(node));\n    let textContent = '';\n    for (let i = 0; i < nodes.length; i++) {\n      const node = nodes[i];\n      const row = node.__parent;\n      const nextRow = (nodes[i + 1] || {}).__parent;\n      textContent += node.getTextContent() + (nextRow !== row ? '\\n' : '\\t');\n    }\n    return textContent;\n  }\n}\n\nexport function $isTableSelection(x: unknown): x is TableSelection {\n  return x instanceof TableSelection;\n}\n\nexport function $createTableSelection(): TableSelection {\n  const anchor = $createPoint('root', 0, 'element');\n  const focus = $createPoint('root', 0, 'element');\n  return new TableSelection('root', anchor, focus);\n}\n\nexport function $getChildrenRecursively(node: LexicalNode): Array<LexicalNode> {\n  const nodes = [];\n  const stack = [node];\n  while (stack.length > 0) {\n    const currentNode = stack.pop();\n    invariant(\n      currentNode !== undefined,\n      \"Stack.length > 0; can't be undefined\",\n    );\n    if ($isElementNode(currentNode)) {\n      stack.unshift(...currentNode.getChildren());\n    }\n    if (currentNode !== node) {\n      nodes.push(currentNode);\n    }\n  }\n  return nodes;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/table/LexicalTableSelectionHelpers.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {TableCellNode} from './LexicalTableCellNode';\nimport type {TableNode} from './LexicalTableNode';\nimport type {TableDOMCell, TableDOMRows} from './LexicalTableObserver';\nimport type {\n  TableMapType,\n  TableMapValueType,\n  TableSelection,\n} from './LexicalTableSelection';\nimport type {\n  BaseSelection,\n  LexicalCommand,\n  LexicalEditor,\n  LexicalNode,\n  RangeSelection,\n  TextFormatType,\n} from 'lexical';\n\nimport {\n  $getClipboardDataFromSelection,\n  copyToClipboard,\n} from '@lexical/clipboard';\nimport {$findMatchingParent, objectKlassEquals} from '@lexical/utils';\nimport {\n  $createParagraphNode,\n  $createRangeSelectionFromDom,\n  $createTextNode,\n  $getNearestNodeFromDOMNode,\n  $getPreviousSelection,\n  $getSelection,\n  $isDecoratorNode,\n  $isElementNode,\n  $isRangeSelection,\n  $isRootOrShadowRoot,\n  $isTextNode,\n  $setSelection,\n  COMMAND_PRIORITY_CRITICAL,\n  COMMAND_PRIORITY_HIGH,\n  CONTROLLED_TEXT_INSERTION_COMMAND,\n  CUT_COMMAND,\n  DELETE_CHARACTER_COMMAND,\n  DELETE_LINE_COMMAND,\n  DELETE_WORD_COMMAND,\n  FOCUS_COMMAND,\n  FORMAT_TEXT_COMMAND,\n  INSERT_PARAGRAPH_COMMAND,\n  KEY_ARROW_DOWN_COMMAND,\n  KEY_ARROW_LEFT_COMMAND,\n  KEY_ARROW_RIGHT_COMMAND,\n  KEY_ARROW_UP_COMMAND,\n  KEY_BACKSPACE_COMMAND,\n  KEY_DELETE_COMMAND,\n  KEY_ESCAPE_COMMAND,\n  KEY_TAB_COMMAND,\n  SELECTION_CHANGE_COMMAND,\n  SELECTION_INSERT_CLIPBOARD_NODES_COMMAND,\n} from 'lexical';\nimport {CAN_USE_DOM} from 'lexical/shared/canUseDOM';\nimport invariant from 'lexical/shared/invariant';\n\nimport {$isTableCellNode} from './LexicalTableCellNode';\nimport {$isTableNode} from './LexicalTableNode';\nimport {TableDOMTable, TableObserver} from './LexicalTableObserver';\nimport {$isTableRowNode} from './LexicalTableRowNode';\nimport {$isTableSelection} from './LexicalTableSelection';\nimport {$computeTableMap, $getNodeTriplet} from './LexicalTableUtils';\nimport {$selectOrCreateAdjacent} from \"../../utils/nodes\";\n\nconst LEXICAL_ELEMENT_KEY = '__lexicalTableSelection';\n\nexport const getDOMSelection = (\n  targetWindow: Window | null,\n): Selection | null =>\n  CAN_USE_DOM ? (targetWindow || window).getSelection() : null;\n\nconst isMouseDownOnEvent = (event: MouseEvent) => {\n  return (event.buttons & 1) === 1;\n};\n\nexport function applyTableHandlers(\n  tableNode: TableNode,\n  tableElement: HTMLTableElementWithWithTableSelectionState,\n  editor: LexicalEditor,\n  hasTabHandler: boolean,\n): TableObserver {\n  const rootElement = editor.getRootElement();\n\n  if (rootElement === null) {\n    throw new Error('No root element.');\n  }\n\n  const tableObserver = new TableObserver(editor, tableNode.getKey());\n  const editorWindow = editor._window || window;\n\n  attachTableObserverToTableElement(tableElement, tableObserver);\n\n  const createMouseHandlers = () => {\n    const onMouseUp = () => {\n      tableObserver.isSelecting = false;\n      editorWindow.removeEventListener('mouseup', onMouseUp);\n      editorWindow.removeEventListener('mousemove', onMouseMove);\n    };\n\n    const onMouseMove = (moveEvent: MouseEvent) => {\n      // delaying mousemove handler to allow selectionchange handler from LexicalEvents.ts to be executed first\n      setTimeout(() => {\n        if (!isMouseDownOnEvent(moveEvent) && tableObserver.isSelecting) {\n          tableObserver.isSelecting = false;\n          editorWindow.removeEventListener('mouseup', onMouseUp);\n          editorWindow.removeEventListener('mousemove', onMouseMove);\n          return;\n        }\n        const focusCell = getDOMCellFromTarget(moveEvent.target as Node);\n        if (\n          focusCell !== null &&\n          (tableObserver.anchorX !== focusCell.x ||\n            tableObserver.anchorY !== focusCell.y)\n        ) {\n          moveEvent.preventDefault();\n          tableObserver.setFocusCellForSelection(focusCell);\n        }\n      }, 0);\n    };\n    return {onMouseMove: onMouseMove, onMouseUp: onMouseUp};\n  };\n\n  tableElement.addEventListener('mousedown', (event: MouseEvent) => {\n    setTimeout(() => {\n      if (event.button !== 0) {\n        return;\n      }\n\n      if (!editorWindow) {\n        return;\n      }\n\n      const anchorCell = getDOMCellFromTarget(event.target as Node);\n      if (anchorCell !== null) {\n        stopEvent(event);\n        tableObserver.setAnchorCellForSelection(anchorCell);\n      }\n\n      const {onMouseUp, onMouseMove} = createMouseHandlers();\n      tableObserver.isSelecting = true;\n      editorWindow.addEventListener('mouseup', onMouseUp);\n      editorWindow.addEventListener('mousemove', onMouseMove);\n    }, 0);\n  });\n\n  // Clear selection when clicking outside of dom.\n  const mouseDownCallback = (event: MouseEvent) => {\n    if (event.button !== 0) {\n      return;\n    }\n\n    editor.update(() => {\n      const selection = $getSelection();\n      const target = event.target as Node;\n      if (\n        $isTableSelection(selection) &&\n        selection.tableKey === tableObserver.tableNodeKey &&\n        rootElement.contains(target)\n      ) {\n        tableObserver.clearHighlight();\n      }\n    });\n  };\n\n  editorWindow.addEventListener('mousedown', mouseDownCallback);\n\n  tableObserver.listenersToRemove.add(() =>\n    editorWindow.removeEventListener('mousedown', mouseDownCallback),\n  );\n\n  tableObserver.listenersToRemove.add(\n    editor.registerCommand<KeyboardEvent>(\n      KEY_ARROW_DOWN_COMMAND,\n      (event) =>\n        $handleArrowKey(editor, event, 'down', tableNode, tableObserver),\n      COMMAND_PRIORITY_HIGH,\n    ),\n  );\n\n  tableObserver.listenersToRemove.add(\n    editor.registerCommand<KeyboardEvent>(\n      KEY_ARROW_UP_COMMAND,\n      (event) => $handleArrowKey(editor, event, 'up', tableNode, tableObserver),\n      COMMAND_PRIORITY_HIGH,\n    ),\n  );\n\n  tableObserver.listenersToRemove.add(\n    editor.registerCommand<KeyboardEvent>(\n      KEY_ARROW_LEFT_COMMAND,\n      (event) =>\n        $handleArrowKey(editor, event, 'backward', tableNode, tableObserver),\n      COMMAND_PRIORITY_HIGH,\n    ),\n  );\n\n  tableObserver.listenersToRemove.add(\n    editor.registerCommand<KeyboardEvent>(\n      KEY_ARROW_RIGHT_COMMAND,\n      (event) =>\n        $handleArrowKey(editor, event, 'forward', tableNode, tableObserver),\n      COMMAND_PRIORITY_HIGH,\n    ),\n  );\n\n  tableObserver.listenersToRemove.add(\n    editor.registerCommand<KeyboardEvent>(\n      KEY_ESCAPE_COMMAND,\n      (event) => {\n        const selection = $getSelection();\n        if ($isTableSelection(selection)) {\n          const focusCellNode = $findMatchingParent(\n            selection.focus.getNode(),\n            $isTableCellNode,\n          );\n          if ($isTableCellNode(focusCellNode)) {\n            stopEvent(event);\n            focusCellNode.selectEnd();\n            return true;\n          }\n        }\n\n        return false;\n      },\n      COMMAND_PRIORITY_HIGH,\n    ),\n  );\n\n  const deleteTextHandler = (command: LexicalCommand<boolean>) => () => {\n    const selection = $getSelection();\n\n    if (!$isSelectionInTable(selection, tableNode)) {\n      return false;\n    }\n\n    if ($isTableSelection(selection)) {\n      tableObserver.clearText();\n\n      return true;\n    } else if ($isRangeSelection(selection)) {\n      const tableCellNode = $findMatchingParent(\n        selection.anchor.getNode(),\n        (n) => $isTableCellNode(n),\n      );\n\n      if (!$isTableCellNode(tableCellNode)) {\n        return false;\n      }\n\n      const anchorNode = selection.anchor.getNode();\n      const focusNode = selection.focus.getNode();\n      const isAnchorInside = tableNode.isParentOf(anchorNode);\n      const isFocusInside = tableNode.isParentOf(focusNode);\n\n      const selectionContainsPartialTable =\n        (isAnchorInside && !isFocusInside) ||\n        (isFocusInside && !isAnchorInside);\n\n      if (selectionContainsPartialTable) {\n        tableObserver.clearText();\n        return true;\n      }\n\n      const nearestElementNode = $findMatchingParent(\n        selection.anchor.getNode(),\n        (n) => $isElementNode(n),\n      );\n\n      const topLevelCellElementNode =\n        nearestElementNode &&\n        $findMatchingParent(\n          nearestElementNode,\n          (n) => $isElementNode(n) && $isTableCellNode(n.getParent()),\n        );\n\n      if (\n        !$isElementNode(topLevelCellElementNode) ||\n        !$isElementNode(nearestElementNode)\n      ) {\n        return false;\n      }\n\n      if (\n        command === DELETE_LINE_COMMAND &&\n        topLevelCellElementNode.getPreviousSibling() === null\n      ) {\n        // TODO: Fix Delete Line in Table Cells.\n        return true;\n      }\n    }\n\n    return false;\n  };\n\n  [DELETE_WORD_COMMAND, DELETE_LINE_COMMAND, DELETE_CHARACTER_COMMAND].forEach(\n    (command) => {\n      tableObserver.listenersToRemove.add(\n        editor.registerCommand(\n          command,\n          deleteTextHandler(command),\n          COMMAND_PRIORITY_CRITICAL,\n        ),\n      );\n    },\n  );\n\n  const $deleteCellHandler = (\n    event: KeyboardEvent | ClipboardEvent | null,\n  ): boolean => {\n    const selection = $getSelection();\n\n    if (!$isSelectionInTable(selection, tableNode)) {\n      const nodes = selection ? selection.getNodes() : null;\n      if (nodes) {\n        const table = nodes.find(\n          (node) =>\n            $isTableNode(node) && node.getKey() === tableObserver.tableNodeKey,\n        );\n        if ($isTableNode(table)) {\n          const parentNode = table.getParent();\n          if (!parentNode) {\n            return false;\n          }\n          table.remove();\n        }\n      }\n      return false;\n    }\n\n    if ($isTableSelection(selection)) {\n      if (event) {\n        event.preventDefault();\n        event.stopPropagation();\n      }\n      tableObserver.clearText();\n\n      return true;\n    } else if ($isRangeSelection(selection)) {\n      const tableCellNode = $findMatchingParent(\n        selection.anchor.getNode(),\n        (n) => $isTableCellNode(n),\n      );\n\n      if (!$isTableCellNode(tableCellNode)) {\n        return false;\n      }\n    }\n\n    return false;\n  };\n\n  tableObserver.listenersToRemove.add(\n    editor.registerCommand<KeyboardEvent>(\n      KEY_BACKSPACE_COMMAND,\n      $deleteCellHandler,\n      COMMAND_PRIORITY_CRITICAL,\n    ),\n  );\n\n  tableObserver.listenersToRemove.add(\n    editor.registerCommand<KeyboardEvent>(\n      KEY_DELETE_COMMAND,\n      $deleteCellHandler,\n      COMMAND_PRIORITY_CRITICAL,\n    ),\n  );\n\n  tableObserver.listenersToRemove.add(\n    editor.registerCommand<KeyboardEvent | ClipboardEvent | null>(\n      CUT_COMMAND,\n      (event) => {\n        const selection = $getSelection();\n        if (selection) {\n          if (!($isTableSelection(selection) || $isRangeSelection(selection))) {\n            return false;\n          }\n          // Copying to the clipboard is async so we must capture the data\n          // before we delete it\n          void copyToClipboard(\n            editor,\n            objectKlassEquals(event, ClipboardEvent)\n              ? (event as ClipboardEvent)\n              : null,\n            $getClipboardDataFromSelection(selection),\n          );\n          const intercepted = $deleteCellHandler(event);\n          if ($isRangeSelection(selection)) {\n            selection.removeText();\n          }\n          return intercepted;\n        }\n        return false;\n      },\n      COMMAND_PRIORITY_CRITICAL,\n    ),\n  );\n\n  tableObserver.listenersToRemove.add(\n    editor.registerCommand<TextFormatType>(\n      FORMAT_TEXT_COMMAND,\n      (payload) => {\n        const selection = $getSelection();\n\n        if (!$isSelectionInTable(selection, tableNode)) {\n          return false;\n        }\n\n        if ($isTableSelection(selection)) {\n          tableObserver.formatCells(payload);\n\n          return true;\n        } else if ($isRangeSelection(selection)) {\n          const tableCellNode = $findMatchingParent(\n            selection.anchor.getNode(),\n            (n) => $isTableCellNode(n),\n          );\n\n          if (!$isTableCellNode(tableCellNode)) {\n            return false;\n          }\n        }\n\n        return false;\n      },\n      COMMAND_PRIORITY_CRITICAL,\n    ),\n  );\n\n  tableObserver.listenersToRemove.add(\n    editor.registerCommand(\n      CONTROLLED_TEXT_INSERTION_COMMAND,\n      (payload) => {\n        const selection = $getSelection();\n\n        if (!$isSelectionInTable(selection, tableNode)) {\n          return false;\n        }\n\n        if ($isTableSelection(selection)) {\n          tableObserver.clearHighlight();\n\n          return false;\n        } else if ($isRangeSelection(selection)) {\n          const tableCellNode = $findMatchingParent(\n            selection.anchor.getNode(),\n            (n) => $isTableCellNode(n),\n          );\n\n          if (!$isTableCellNode(tableCellNode)) {\n            return false;\n          }\n\n          if (typeof payload === 'string') {\n            const edgePosition = $getTableEdgeCursorPosition(\n              editor,\n              selection,\n              tableNode,\n            );\n            if (edgePosition) {\n              $insertParagraphAtTableEdge(edgePosition, tableNode, [\n                $createTextNode(payload),\n              ]);\n              return true;\n            }\n          }\n        }\n\n        return false;\n      },\n      COMMAND_PRIORITY_CRITICAL,\n    ),\n  );\n\n  if (hasTabHandler) {\n    tableObserver.listenersToRemove.add(\n      editor.registerCommand<KeyboardEvent>(\n        KEY_TAB_COMMAND,\n        (event) => {\n          const selection = $getSelection();\n          if (\n            !$isRangeSelection(selection) ||\n            !selection.isCollapsed() ||\n            !$isSelectionInTable(selection, tableNode)\n          ) {\n            return false;\n          }\n\n          const tableCellNode = $findCellNode(selection.anchor.getNode());\n          if (tableCellNode === null) {\n            return false;\n          }\n\n          stopEvent(event);\n\n          const currentCords = tableNode.getCordsFromCellNode(\n            tableCellNode,\n            tableObserver.table,\n          );\n\n          selectTableNodeInDirection(\n            tableObserver,\n            tableNode,\n            currentCords.x,\n            currentCords.y,\n            !event.shiftKey ? 'forward' : 'backward',\n          );\n\n          return true;\n        },\n        COMMAND_PRIORITY_CRITICAL,\n      ),\n    );\n  }\n\n  tableObserver.listenersToRemove.add(\n    editor.registerCommand(\n      FOCUS_COMMAND,\n      (payload) => {\n        return tableNode.isSelected();\n      },\n      COMMAND_PRIORITY_HIGH,\n    ),\n  );\n\n  function getObserverCellFromCellNode(\n    tableCellNode: TableCellNode,\n  ): TableDOMCell {\n    const currentCords = tableNode.getCordsFromCellNode(\n      tableCellNode,\n      tableObserver.table,\n    );\n    return tableNode.getDOMCellFromCordsOrThrow(\n      currentCords.x,\n      currentCords.y,\n      tableObserver.table,\n    );\n  }\n\n  tableObserver.listenersToRemove.add(\n    editor.registerCommand(\n      SELECTION_INSERT_CLIPBOARD_NODES_COMMAND,\n      (selectionPayload) => {\n        const {nodes, selection} = selectionPayload;\n        const anchorAndFocus = selection.getStartEndPoints();\n        const isTableSelection = $isTableSelection(selection);\n        const isRangeSelection = $isRangeSelection(selection);\n        const isSelectionInsideOfGrid =\n          (isRangeSelection &&\n            $findMatchingParent(selection.anchor.getNode(), (n) =>\n              $isTableCellNode(n),\n            ) !== null &&\n            $findMatchingParent(selection.focus.getNode(), (n) =>\n              $isTableCellNode(n),\n            ) !== null) ||\n          isTableSelection;\n\n        if (\n          nodes.length !== 1 ||\n          !$isTableNode(nodes[0]) ||\n          !isSelectionInsideOfGrid ||\n          anchorAndFocus === null\n        ) {\n          return false;\n        }\n        const [anchor] = anchorAndFocus;\n\n        const newGrid = nodes[0];\n        const newGridRows = newGrid.getChildren();\n        const newColumnCount = newGrid\n          .getFirstChildOrThrow<TableNode>()\n          .getChildrenSize();\n        const newRowCount = newGrid.getChildrenSize();\n        const gridCellNode = $findMatchingParent(anchor.getNode(), (n) =>\n          $isTableCellNode(n),\n        );\n        const gridRowNode =\n          gridCellNode &&\n          $findMatchingParent(gridCellNode, (n) => $isTableRowNode(n));\n        const gridNode =\n          gridRowNode &&\n          $findMatchingParent(gridRowNode, (n) => $isTableNode(n));\n\n        if (\n          !$isTableCellNode(gridCellNode) ||\n          !$isTableRowNode(gridRowNode) ||\n          !$isTableNode(gridNode)\n        ) {\n          return false;\n        }\n\n        const startY = gridRowNode.getIndexWithinParent();\n        const stopY = Math.min(\n          gridNode.getChildrenSize() - 1,\n          startY + newRowCount - 1,\n        );\n        const startX = gridCellNode.getIndexWithinParent();\n        const stopX = Math.min(\n          gridRowNode.getChildrenSize() - 1,\n          startX + newColumnCount - 1,\n        );\n        const fromX = Math.min(startX, stopX);\n        const fromY = Math.min(startY, stopY);\n        const toX = Math.max(startX, stopX);\n        const toY = Math.max(startY, stopY);\n        const gridRowNodes = gridNode.getChildren();\n        let newRowIdx = 0;\n\n        for (let r = fromY; r <= toY; r++) {\n          const currentGridRowNode = gridRowNodes[r];\n\n          if (!$isTableRowNode(currentGridRowNode)) {\n            return false;\n          }\n\n          const newGridRowNode = newGridRows[newRowIdx];\n\n          if (!$isTableRowNode(newGridRowNode)) {\n            return false;\n          }\n\n          const gridCellNodes = currentGridRowNode.getChildren();\n          const newGridCellNodes = newGridRowNode.getChildren();\n          let newColumnIdx = 0;\n\n          for (let c = fromX; c <= toX; c++) {\n            const currentGridCellNode = gridCellNodes[c];\n\n            if (!$isTableCellNode(currentGridCellNode)) {\n              return false;\n            }\n\n            const newGridCellNode = newGridCellNodes[newColumnIdx];\n\n            if (!$isTableCellNode(newGridCellNode)) {\n              return false;\n            }\n\n            const originalChildren = currentGridCellNode.getChildren();\n            newGridCellNode.getChildren().forEach((child) => {\n              if ($isTextNode(child)) {\n                const paragraphNode = $createParagraphNode();\n                paragraphNode.append(child);\n                currentGridCellNode.append(child);\n              } else {\n                currentGridCellNode.append(child);\n              }\n            });\n            originalChildren.forEach((n) => n.remove());\n            newColumnIdx++;\n          }\n\n          newRowIdx++;\n        }\n        return true;\n      },\n      COMMAND_PRIORITY_CRITICAL,\n    ),\n  );\n\n  tableObserver.listenersToRemove.add(\n    editor.registerCommand(\n      SELECTION_CHANGE_COMMAND,\n      () => {\n        const selection = $getSelection();\n        const prevSelection = $getPreviousSelection();\n\n        if ($isRangeSelection(selection)) {\n          const {anchor, focus} = selection;\n          const anchorNode = anchor.getNode();\n          const focusNode = focus.getNode();\n          // Using explicit comparison with table node to ensure it's not a nested table\n          // as in that case we'll leave selection resolving to that table\n          const anchorCellNode = $findCellNode(anchorNode);\n          const focusCellNode = $findCellNode(focusNode);\n          const isAnchorInside = !!(\n            anchorCellNode && tableNode.is($findTableNode(anchorCellNode))\n          );\n          const isFocusInside = !!(\n            focusCellNode && tableNode.is($findTableNode(focusCellNode))\n          );\n          const isPartialyWithinTable = isAnchorInside !== isFocusInside;\n          const isWithinTable = isAnchorInside && isFocusInside;\n          const isBackward = selection.isBackward();\n\n          if (isPartialyWithinTable) {\n            const newSelection = selection.clone();\n            if (isFocusInside) {\n              const [tableMap] = $computeTableMap(\n                tableNode,\n                focusCellNode,\n                focusCellNode,\n              );\n              const firstCell = tableMap[0][0].cell;\n              const lastCell = tableMap[tableMap.length - 1].at(-1)!.cell;\n              newSelection.focus.set(\n                isBackward ? firstCell.getKey() : lastCell.getKey(),\n                isBackward\n                  ? firstCell.getChildrenSize()\n                  : lastCell.getChildrenSize(),\n                'element',\n              );\n            }\n            $setSelection(newSelection);\n            $addHighlightStyleToTable(editor, tableObserver);\n          } else if (isWithinTable) {\n            // Handle case when selection spans across multiple cells but still\n            // has range selection, then we convert it into grid selection\n            if (!anchorCellNode.is(focusCellNode)) {\n              tableObserver.setAnchorCellForSelection(\n                getObserverCellFromCellNode(anchorCellNode),\n              );\n              tableObserver.setFocusCellForSelection(\n                getObserverCellFromCellNode(focusCellNode),\n                true,\n              );\n              if (!tableObserver.isSelecting) {\n                setTimeout(() => {\n                  const {onMouseUp, onMouseMove} = createMouseHandlers();\n                  tableObserver.isSelecting = true;\n                  editorWindow.addEventListener('mouseup', onMouseUp);\n                  editorWindow.addEventListener('mousemove', onMouseMove);\n                }, 0);\n              }\n            }\n          }\n        } else if (\n          selection &&\n          $isTableSelection(selection) &&\n          selection.is(prevSelection) &&\n          selection.tableKey === tableNode.getKey()\n        ) {\n          // if selection goes outside of the table we need to change it to Range selection\n          const domSelection = getDOMSelection(editor._window);\n          if (\n            domSelection &&\n            domSelection.anchorNode &&\n            domSelection.focusNode\n          ) {\n            const focusNode = $getNearestNodeFromDOMNode(\n              domSelection.focusNode,\n            );\n            const isFocusOutside =\n              focusNode && !tableNode.is($findTableNode(focusNode));\n\n            const anchorNode = $getNearestNodeFromDOMNode(\n              domSelection.anchorNode,\n            );\n            const isAnchorInside =\n              anchorNode && tableNode.is($findTableNode(anchorNode));\n\n            if (\n              isFocusOutside &&\n              isAnchorInside &&\n              domSelection.rangeCount > 0\n            ) {\n              const newSelection = $createRangeSelectionFromDom(\n                domSelection,\n                editor,\n              );\n              if (newSelection) {\n                newSelection.anchor.set(\n                  tableNode.getKey(),\n                  selection.isBackward() ? tableNode.getChildrenSize() : 0,\n                  'element',\n                );\n                domSelection.removeAllRanges();\n                $setSelection(newSelection);\n              }\n            }\n          }\n        }\n\n        if (\n          selection &&\n          !selection.is(prevSelection) &&\n          ($isTableSelection(selection) || $isTableSelection(prevSelection)) &&\n          tableObserver.tableSelection &&\n          !tableObserver.tableSelection.is(prevSelection)\n        ) {\n          if (\n            $isTableSelection(selection) &&\n            selection.tableKey === tableObserver.tableNodeKey\n          ) {\n            tableObserver.updateTableTableSelection(selection);\n          } else if (\n            !$isTableSelection(selection) &&\n            $isTableSelection(prevSelection) &&\n            prevSelection.tableKey === tableObserver.tableNodeKey\n          ) {\n            tableObserver.updateTableTableSelection(null);\n          }\n          return false;\n        }\n\n        if (\n          tableObserver.hasHijackedSelectionStyles &&\n          !tableNode.isSelected()\n        ) {\n          $removeHighlightStyleToTable(editor, tableObserver);\n        } else if (\n          !tableObserver.hasHijackedSelectionStyles &&\n          tableNode.isSelected()\n        ) {\n          $addHighlightStyleToTable(editor, tableObserver);\n        }\n\n        return false;\n      },\n      COMMAND_PRIORITY_CRITICAL,\n    ),\n  );\n\n  tableObserver.listenersToRemove.add(\n    editor.registerCommand(\n      INSERT_PARAGRAPH_COMMAND,\n      () => {\n        const selection = $getSelection();\n        if (\n          !$isRangeSelection(selection) ||\n          !selection.isCollapsed() ||\n          !$isSelectionInTable(selection, tableNode)\n        ) {\n          return false;\n        }\n        const edgePosition = $getTableEdgeCursorPosition(\n          editor,\n          selection,\n          tableNode,\n        );\n        if (edgePosition) {\n          $insertParagraphAtTableEdge(edgePosition, tableNode);\n          return true;\n        }\n        return false;\n      },\n      COMMAND_PRIORITY_CRITICAL,\n    ),\n  );\n\n  return tableObserver;\n}\n\nexport type HTMLTableElementWithWithTableSelectionState = HTMLTableElement &\n  Record<typeof LEXICAL_ELEMENT_KEY, TableObserver>;\n\nexport function attachTableObserverToTableElement(\n  tableElement: HTMLTableElementWithWithTableSelectionState,\n  tableObserver: TableObserver,\n) {\n  tableElement[LEXICAL_ELEMENT_KEY] = tableObserver;\n}\n\nexport function getTableObserverFromTableElement(\n  tableElement: HTMLTableElementWithWithTableSelectionState,\n): TableObserver | null {\n  return tableElement[LEXICAL_ELEMENT_KEY];\n}\n\nexport function getDOMCellFromTarget(node: Node): TableDOMCell | null {\n  let currentNode: ParentNode | Node | null = node;\n\n  while (currentNode != null) {\n    const nodeName = currentNode.nodeName;\n\n    if (nodeName === 'TD' || nodeName === 'TH') {\n      // @ts-expect-error: internal field\n      const cell = currentNode._cell;\n\n      if (cell === undefined) {\n        return null;\n      }\n\n      return cell;\n    }\n\n    currentNode = currentNode.parentNode;\n  }\n\n  return null;\n}\n\nexport function doesTargetContainText(node: Node): boolean {\n  const currentNode: ParentNode | Node | null = node;\n\n  if (currentNode !== null) {\n    const nodeName = currentNode.nodeName;\n\n    if (nodeName === 'SPAN') {\n      return true;\n    }\n  }\n  return false;\n}\n\nexport function getTable(tableElement: HTMLElement): TableDOMTable {\n  const domRows: TableDOMRows = [];\n  const grid = {\n    columns: 0,\n    domRows,\n    rows: 0,\n  };\n  let currentNode = tableElement.firstChild;\n  let x = 0;\n  let y = 0;\n  domRows.length = 0;\n\n  while (currentNode != null) {\n    const nodeName = currentNode.nodeName;\n\n    if (nodeName === 'COLGROUP' || nodeName === 'CAPTION') {\n      currentNode = currentNode.nextSibling;\n      continue;\n    }\n\n    if (nodeName === 'TD' || nodeName === 'TH') {\n      const elem = currentNode as HTMLElement;\n      const cell = {\n        elem,\n        hasBackgroundColor: elem.style.backgroundColor !== '',\n        highlighted: false,\n        x,\n        y,\n      };\n\n      // @ts-expect-error: internal field\n      currentNode._cell = cell;\n\n      let row = domRows[y];\n      if (row === undefined) {\n        row = domRows[y] = [];\n      }\n\n      row[x] = cell;\n    } else {\n      const child = currentNode.firstChild;\n\n      if (child != null) {\n        currentNode = child;\n        continue;\n      }\n    }\n\n    const sibling = currentNode.nextSibling;\n\n    if (sibling != null) {\n      x++;\n      currentNode = sibling;\n      continue;\n    }\n\n    const parent = currentNode.parentNode;\n\n    if (parent != null) {\n      const parentSibling = parent.nextSibling;\n\n      if (parentSibling == null) {\n        break;\n      }\n\n      y++;\n      x = 0;\n      currentNode = parentSibling;\n    }\n  }\n\n  grid.columns = x + 1;\n  grid.rows = y + 1;\n\n  return grid;\n}\n\nexport function $updateDOMForSelection(\n  editor: LexicalEditor,\n  table: TableDOMTable,\n  selection: TableSelection | RangeSelection | null,\n) {\n  const selectedCellNodes = new Set(selection ? selection.getNodes() : []);\n  $forEachTableCell(table, (cell, lexicalNode) => {\n    const elem = cell.elem;\n\n    if (selectedCellNodes.has(lexicalNode)) {\n      cell.highlighted = true;\n      $addHighlightToDOM(editor, cell);\n    } else {\n      cell.highlighted = false;\n      $removeHighlightFromDOM(editor, cell);\n      if (!elem.getAttribute('style')) {\n        elem.removeAttribute('style');\n      }\n    }\n  });\n}\n\nexport function $forEachTableCell(\n  grid: TableDOMTable,\n  cb: (\n    cell: TableDOMCell,\n    lexicalNode: LexicalNode,\n    cords: {\n      x: number;\n      y: number;\n    },\n  ) => void,\n) {\n  const {domRows} = grid;\n\n  for (let y = 0; y < domRows.length; y++) {\n    const row = domRows[y];\n    if (!row) {\n      continue;\n    }\n\n    for (let x = 0; x < row.length; x++) {\n      const cell = row[x];\n      if (!cell) {\n        continue;\n      }\n      const lexicalNode = $getNearestNodeFromDOMNode(cell.elem);\n\n      if (lexicalNode !== null) {\n        cb(cell, lexicalNode, {\n          x,\n          y,\n        });\n      }\n    }\n  }\n}\n\nexport function $addHighlightStyleToTable(\n  editor: LexicalEditor,\n  tableSelection: TableObserver,\n) {\n  tableSelection.disableHighlightStyle();\n  $forEachTableCell(tableSelection.table, (cell) => {\n    cell.highlighted = true;\n    $addHighlightToDOM(editor, cell);\n  });\n}\n\nexport function $removeHighlightStyleToTable(\n  editor: LexicalEditor,\n  tableObserver: TableObserver,\n) {\n  tableObserver.enableHighlightStyle();\n  $forEachTableCell(tableObserver.table, (cell) => {\n    const elem = cell.elem;\n    cell.highlighted = false;\n    $removeHighlightFromDOM(editor, cell);\n\n    if (!elem.getAttribute('style')) {\n      elem.removeAttribute('style');\n    }\n  });\n}\n\ntype Direction = 'backward' | 'forward' | 'up' | 'down';\n\nconst selectTableNodeInDirection = (\n  tableObserver: TableObserver,\n  tableNode: TableNode,\n  x: number,\n  y: number,\n  direction: Direction,\n): boolean => {\n  const isForward = direction === 'forward';\n\n  switch (direction) {\n    case 'backward':\n    case 'forward':\n      if (x !== (isForward ? tableObserver.table.columns - 1 : 0)) {\n        selectTableCellNode(\n          tableNode.getCellNodeFromCordsOrThrow(\n            x + (isForward ? 1 : -1),\n            y,\n            tableObserver.table,\n          ),\n          isForward,\n        );\n      } else {\n        if (y !== (isForward ? tableObserver.table.rows - 1 : 0)) {\n          selectTableCellNode(\n            tableNode.getCellNodeFromCordsOrThrow(\n              isForward ? 0 : tableObserver.table.columns - 1,\n              y + (isForward ? 1 : -1),\n              tableObserver.table,\n            ),\n            isForward,\n          );\n        } else if (!isForward) {\n          tableNode.selectPrevious();\n        } else {\n          tableNode.selectNext();\n        }\n      }\n\n      return true;\n\n    case 'up':\n      if (y !== 0) {\n        selectTableCellNode(\n          tableNode.getCellNodeFromCordsOrThrow(x, y - 1, tableObserver.table),\n          false,\n        );\n      } else {\n        $selectOrCreateAdjacent(tableNode, false);\n      }\n\n      return true;\n\n    case 'down':\n      if (y !== tableObserver.table.rows - 1) {\n        selectTableCellNode(\n          tableNode.getCellNodeFromCordsOrThrow(x, y + 1, tableObserver.table),\n          true,\n        );\n      } else {\n        $selectOrCreateAdjacent(tableNode, true);\n      }\n\n      return true;\n    default:\n      return false;\n  }\n};\n\nconst adjustFocusNodeInDirection = (\n  tableObserver: TableObserver,\n  tableNode: TableNode,\n  x: number,\n  y: number,\n  direction: Direction,\n): boolean => {\n  const isForward = direction === 'forward';\n\n  switch (direction) {\n    case 'backward':\n    case 'forward':\n      if (x !== (isForward ? tableObserver.table.columns - 1 : 0)) {\n        tableObserver.setFocusCellForSelection(\n          tableNode.getDOMCellFromCordsOrThrow(\n            x + (isForward ? 1 : -1),\n            y,\n            tableObserver.table,\n          ),\n        );\n      }\n\n      return true;\n    case 'up':\n      if (y !== 0) {\n        tableObserver.setFocusCellForSelection(\n          tableNode.getDOMCellFromCordsOrThrow(x, y - 1, tableObserver.table),\n        );\n\n        return true;\n      } else {\n        return false;\n      }\n    case 'down':\n      if (y !== tableObserver.table.rows - 1) {\n        tableObserver.setFocusCellForSelection(\n          tableNode.getDOMCellFromCordsOrThrow(x, y + 1, tableObserver.table),\n        );\n\n        return true;\n      } else {\n        return false;\n      }\n    default:\n      return false;\n  }\n};\n\nfunction $isSelectionInTable(\n  selection: null | BaseSelection,\n  tableNode: TableNode,\n): boolean {\n  if ($isRangeSelection(selection) || $isTableSelection(selection)) {\n    const isAnchorInside = tableNode.isParentOf(selection.anchor.getNode());\n    const isFocusInside = tableNode.isParentOf(selection.focus.getNode());\n\n    return isAnchorInside && isFocusInside;\n  }\n\n  return false;\n}\n\nfunction selectTableCellNode(tableCell: TableCellNode, fromStart: boolean) {\n  if (fromStart) {\n    tableCell.selectStart();\n  } else {\n    tableCell.selectEnd();\n  }\n}\n\nconst BROWSER_BLUE_RGB = '172,206,247';\nfunction $addHighlightToDOM(editor: LexicalEditor, cell: TableDOMCell): void {\n  const element = cell.elem;\n  const node = $getNearestNodeFromDOMNode(element);\n  invariant(\n    $isTableCellNode(node),\n    'Expected to find LexicalNode from Table Cell DOMNode',\n  );\n  const backgroundColor = node.getBackgroundColor();\n  if (backgroundColor === null) {\n    element.style.setProperty('background-color', `rgb(${BROWSER_BLUE_RGB})`);\n  } else {\n    element.style.setProperty(\n      'background-image',\n      `linear-gradient(to right, rgba(${BROWSER_BLUE_RGB},0.85), rgba(${BROWSER_BLUE_RGB},0.85))`,\n    );\n  }\n  element.style.setProperty('caret-color', 'transparent');\n}\n\nfunction $removeHighlightFromDOM(\n  editor: LexicalEditor,\n  cell: TableDOMCell,\n): void {\n  const element = cell.elem;\n  const node = $getNearestNodeFromDOMNode(element);\n  invariant(\n    $isTableCellNode(node),\n    'Expected to find LexicalNode from Table Cell DOMNode',\n  );\n  const backgroundColor = node.getBackgroundColor();\n  if (backgroundColor === null) {\n    element.style.removeProperty('background-color');\n  }\n  element.style.removeProperty('background-image');\n  element.style.removeProperty('caret-color');\n}\n\nexport function $findCellNode(node: LexicalNode): null | TableCellNode {\n  const cellNode = $findMatchingParent(node, $isTableCellNode);\n  return $isTableCellNode(cellNode) ? cellNode : null;\n}\n\nexport function $findTableNode(node: LexicalNode): null | TableNode {\n  const tableNode = $findMatchingParent(node, $isTableNode);\n  return $isTableNode(tableNode) ? tableNode : null;\n}\n\nfunction $handleArrowKey(\n  editor: LexicalEditor,\n  event: KeyboardEvent,\n  direction: Direction,\n  tableNode: TableNode,\n  tableObserver: TableObserver,\n): boolean {\n  if (\n    (direction === 'up' || direction === 'down') &&\n    isTypeaheadMenuInView(editor)\n  ) {\n    return false;\n  }\n\n  const selection = $getSelection();\n\n  if (!$isSelectionInTable(selection, tableNode)) {\n    if ($isRangeSelection(selection)) {\n      if (selection.isCollapsed() && direction === 'backward') {\n        const anchorType = selection.anchor.type;\n        const anchorOffset = selection.anchor.offset;\n        if (\n          anchorType !== 'element' &&\n          !(anchorType === 'text' && anchorOffset === 0)\n        ) {\n          return false;\n        }\n        const anchorNode = selection.anchor.getNode();\n        if (!anchorNode) {\n          return false;\n        }\n        const parentNode = $findMatchingParent(\n          anchorNode,\n          (n) => $isElementNode(n) && !n.isInline(),\n        );\n        if (!parentNode) {\n          return false;\n        }\n        const siblingNode = parentNode.getPreviousSibling();\n        if (!siblingNode || !$isTableNode(siblingNode)) {\n          return false;\n        }\n        stopEvent(event);\n        siblingNode.selectEnd();\n        return true;\n      } else if (\n        event.shiftKey &&\n        (direction === 'up' || direction === 'down')\n      ) {\n        const focusNode = selection.focus.getNode();\n        if ($isRootOrShadowRoot(focusNode)) {\n          const selectedNode = selection.getNodes()[0];\n          if (selectedNode) {\n            const tableCellNode = $findMatchingParent(\n              selectedNode,\n              $isTableCellNode,\n            );\n            if (tableCellNode && tableNode.isParentOf(tableCellNode)) {\n              const firstDescendant = tableNode.getFirstDescendant();\n              const lastDescendant = tableNode.getLastDescendant();\n              if (!firstDescendant || !lastDescendant) {\n                return false;\n              }\n              const [firstCellNode] = $getNodeTriplet(firstDescendant);\n              const [lastCellNode] = $getNodeTriplet(lastDescendant);\n              const firstCellCoords = tableNode.getCordsFromCellNode(\n                firstCellNode,\n                tableObserver.table,\n              );\n              const lastCellCoords = tableNode.getCordsFromCellNode(\n                lastCellNode,\n                tableObserver.table,\n              );\n              const firstCellDOM = tableNode.getDOMCellFromCordsOrThrow(\n                firstCellCoords.x,\n                firstCellCoords.y,\n                tableObserver.table,\n              );\n              const lastCellDOM = tableNode.getDOMCellFromCordsOrThrow(\n                lastCellCoords.x,\n                lastCellCoords.y,\n                tableObserver.table,\n              );\n              tableObserver.setAnchorCellForSelection(firstCellDOM);\n              tableObserver.setFocusCellForSelection(lastCellDOM, true);\n              return true;\n            }\n          }\n          return false;\n        } else {\n          const focusParentNode = $findMatchingParent(\n            focusNode,\n            (n) => $isElementNode(n) && !n.isInline(),\n          );\n          if (!focusParentNode) {\n            return false;\n          }\n          const sibling =\n            direction === 'down'\n              ? focusParentNode.getNextSibling()\n              : focusParentNode.getPreviousSibling();\n          if (\n            $isTableNode(sibling) &&\n            tableObserver.tableNodeKey === sibling.getKey()\n          ) {\n            const firstDescendant = sibling.getFirstDescendant();\n            const lastDescendant = sibling.getLastDescendant();\n            if (!firstDescendant || !lastDescendant) {\n              return false;\n            }\n            const [firstCellNode] = $getNodeTriplet(firstDescendant);\n            const [lastCellNode] = $getNodeTriplet(lastDescendant);\n            const newSelection = selection.clone();\n            newSelection.focus.set(\n              (direction === 'up' ? firstCellNode : lastCellNode).getKey(),\n              direction === 'up' ? 0 : lastCellNode.getChildrenSize(),\n              'element',\n            );\n            $setSelection(newSelection);\n            return true;\n          }\n        }\n      }\n    }\n    return false;\n  }\n\n  if ($isRangeSelection(selection) && selection.isCollapsed()) {\n    const {anchor, focus} = selection;\n    const anchorCellNode = $findMatchingParent(\n      anchor.getNode(),\n      $isTableCellNode,\n    );\n    const focusCellNode = $findMatchingParent(\n      focus.getNode(),\n      $isTableCellNode,\n    );\n    if (\n      !$isTableCellNode(anchorCellNode) ||\n      !anchorCellNode.is(focusCellNode)\n    ) {\n      return false;\n    }\n    const anchorCellTable = $findTableNode(anchorCellNode);\n    if (anchorCellTable !== tableNode && anchorCellTable != null) {\n      const anchorCellTableElement = editor.getElementByKey(\n        anchorCellTable.getKey(),\n      );\n      if (anchorCellTableElement != null) {\n        tableObserver.table = getTable(anchorCellTableElement);\n        return $handleArrowKey(\n          editor,\n          event,\n          direction,\n          anchorCellTable,\n          tableObserver,\n        );\n      }\n    }\n\n    if (direction === 'backward' || direction === 'forward') {\n      const anchorType = anchor.type;\n      const anchorOffset = anchor.offset;\n      const anchorNode = anchor.getNode();\n      if (!anchorNode) {\n        return false;\n      }\n\n      const selectedNodes = selection.getNodes();\n      if (selectedNodes.length === 1 && $isDecoratorNode(selectedNodes[0])) {\n        return false;\n      }\n\n      if (\n        isExitingTableAnchor(anchorType, anchorOffset, anchorNode, direction)\n      ) {\n        return $handleTableExit(event, anchorNode, tableNode, direction);\n      }\n\n      return false;\n    }\n\n    const anchorCellDom = editor.getElementByKey(anchorCellNode.__key);\n    const anchorDOM = editor.getElementByKey(anchor.key);\n    if (anchorDOM == null || anchorCellDom == null) {\n      return false;\n    }\n\n    let edgeSelectionRect;\n    if (anchor.type === 'element') {\n      edgeSelectionRect = anchorDOM.getBoundingClientRect();\n    } else {\n      const domSelection = window.getSelection();\n      if (domSelection === null || domSelection.rangeCount === 0) {\n        return false;\n      }\n\n      const range = domSelection.getRangeAt(0);\n      edgeSelectionRect = range.getBoundingClientRect();\n    }\n\n    const edgeChild =\n      direction === 'up'\n        ? anchorCellNode.getFirstChild()\n        : anchorCellNode.getLastChild();\n    if (edgeChild == null) {\n      return false;\n    }\n\n    const edgeChildDOM = editor.getElementByKey(edgeChild.__key);\n\n    if (edgeChildDOM == null) {\n      return false;\n    }\n\n    const edgeRect = edgeChildDOM.getBoundingClientRect();\n    const isExiting =\n      direction === 'up'\n        ? edgeRect.top > edgeSelectionRect.top - edgeSelectionRect.height\n        : edgeSelectionRect.bottom + edgeSelectionRect.height > edgeRect.bottom;\n\n    if (isExiting) {\n      stopEvent(event);\n\n      const cords = tableNode.getCordsFromCellNode(\n        anchorCellNode,\n        tableObserver.table,\n      );\n\n      if (event.shiftKey) {\n        const cell = tableNode.getDOMCellFromCordsOrThrow(\n          cords.x,\n          cords.y,\n          tableObserver.table,\n        );\n        tableObserver.setAnchorCellForSelection(cell);\n        tableObserver.setFocusCellForSelection(cell, true);\n      } else {\n        return selectTableNodeInDirection(\n          tableObserver,\n          tableNode,\n          cords.x,\n          cords.y,\n          direction,\n        );\n      }\n\n      return true;\n    }\n  } else if ($isTableSelection(selection)) {\n    const {anchor, focus} = selection;\n    const anchorCellNode = $findMatchingParent(\n      anchor.getNode(),\n      $isTableCellNode,\n    );\n    const focusCellNode = $findMatchingParent(\n      focus.getNode(),\n      $isTableCellNode,\n    );\n\n    const [tableNodeFromSelection] = selection.getNodes();\n    const tableElement = editor.getElementByKey(\n      tableNodeFromSelection.getKey(),\n    );\n    if (\n      !$isTableCellNode(anchorCellNode) ||\n      !$isTableCellNode(focusCellNode) ||\n      !$isTableNode(tableNodeFromSelection) ||\n      tableElement == null\n    ) {\n      return false;\n    }\n    tableObserver.updateTableTableSelection(selection);\n\n    const grid = getTable(tableElement);\n    const cordsAnchor = tableNode.getCordsFromCellNode(anchorCellNode, grid);\n    const anchorCell = tableNode.getDOMCellFromCordsOrThrow(\n      cordsAnchor.x,\n      cordsAnchor.y,\n      grid,\n    );\n    tableObserver.setAnchorCellForSelection(anchorCell);\n\n    stopEvent(event);\n\n    if (event.shiftKey) {\n      const cords = tableNode.getCordsFromCellNode(focusCellNode, grid);\n      return adjustFocusNodeInDirection(\n        tableObserver,\n        tableNodeFromSelection,\n        cords.x,\n        cords.y,\n        direction,\n      );\n    } else {\n      focusCellNode.selectEnd();\n    }\n\n    return true;\n  }\n\n  return false;\n}\n\nfunction stopEvent(event: Event) {\n  event.preventDefault();\n  event.stopImmediatePropagation();\n  event.stopPropagation();\n}\n\nfunction isTypeaheadMenuInView(editor: LexicalEditor) {\n  // There is no inbuilt way to check if the component picker is in view\n  // but we can check if the root DOM element has the aria-controls attribute \"typeahead-menu\".\n  const root = editor.getRootElement();\n  if (!root) {\n    return false;\n  }\n  return (\n    root.hasAttribute('aria-controls') &&\n    root.getAttribute('aria-controls') === 'typeahead-menu'\n  );\n}\n\nfunction isExitingTableAnchor(\n  type: string,\n  offset: number,\n  anchorNode: LexicalNode,\n  direction: 'backward' | 'forward',\n) {\n  return (\n    isExitingTableElementAnchor(type, anchorNode, direction) ||\n    $isExitingTableTextAnchor(type, offset, anchorNode, direction)\n  );\n}\n\nfunction isExitingTableElementAnchor(\n  type: string,\n  anchorNode: LexicalNode,\n  direction: 'backward' | 'forward',\n) {\n  return (\n    type === 'element' &&\n    (direction === 'backward'\n      ? anchorNode.getPreviousSibling() === null\n      : anchorNode.getNextSibling() === null)\n  );\n}\n\nfunction $isExitingTableTextAnchor(\n  type: string,\n  offset: number,\n  anchorNode: LexicalNode,\n  direction: 'backward' | 'forward',\n) {\n  const parentNode = $findMatchingParent(\n    anchorNode,\n    (n) => $isElementNode(n) && !n.isInline(),\n  );\n  if (!parentNode) {\n    return false;\n  }\n  const hasValidOffset =\n    direction === 'backward'\n      ? offset === 0\n      : offset === anchorNode.getTextContentSize();\n  return (\n    type === 'text' &&\n    hasValidOffset &&\n    (direction === 'backward'\n      ? parentNode.getPreviousSibling() === null\n      : parentNode.getNextSibling() === null)\n  );\n}\n\nfunction $handleTableExit(\n  event: KeyboardEvent,\n  anchorNode: LexicalNode,\n  tableNode: TableNode,\n  direction: 'backward' | 'forward',\n) {\n  const anchorCellNode = $findMatchingParent(anchorNode, $isTableCellNode);\n  if (!$isTableCellNode(anchorCellNode)) {\n    return false;\n  }\n  const [tableMap, cellValue] = $computeTableMap(\n    tableNode,\n    anchorCellNode,\n    anchorCellNode,\n  );\n  if (!isExitingCell(tableMap, cellValue, direction)) {\n    return false;\n  }\n\n  const toNode = $getExitingToNode(anchorNode, direction, tableNode);\n  if (!toNode || $isTableNode(toNode)) {\n    return false;\n  }\n\n  stopEvent(event);\n  if (direction === 'backward') {\n    toNode.selectEnd();\n  } else {\n    toNode.selectStart();\n  }\n  return true;\n}\n\nfunction isExitingCell(\n  tableMap: TableMapType,\n  cellValue: TableMapValueType,\n  direction: 'backward' | 'forward',\n) {\n  const firstCell = tableMap[0][0];\n  const lastCell = tableMap[tableMap.length - 1][tableMap[0].length - 1];\n  const {startColumn, startRow} = cellValue;\n  return direction === 'backward'\n    ? startColumn === firstCell.startColumn && startRow === firstCell.startRow\n    : startColumn === lastCell.startColumn && startRow === lastCell.startRow;\n}\n\nfunction $getExitingToNode(\n  anchorNode: LexicalNode,\n  direction: 'backward' | 'forward',\n  tableNode: TableNode,\n) {\n  const parentNode = $findMatchingParent(\n    anchorNode,\n    (n) => $isElementNode(n) && !n.isInline(),\n  );\n  if (!parentNode) {\n    return undefined;\n  }\n  const anchorSibling =\n    direction === 'backward'\n      ? parentNode.getPreviousSibling()\n      : parentNode.getNextSibling();\n  return anchorSibling && $isTableNode(anchorSibling)\n    ? anchorSibling\n    : direction === 'backward'\n    ? tableNode.getPreviousSibling()\n    : tableNode.getNextSibling();\n}\n\nfunction $insertParagraphAtTableEdge(\n  edgePosition: 'first' | 'last',\n  tableNode: TableNode,\n  children?: LexicalNode[],\n) {\n  const paragraphNode = $createParagraphNode();\n  if (edgePosition === 'first') {\n    tableNode.insertBefore(paragraphNode);\n  } else {\n    tableNode.insertAfter(paragraphNode);\n  }\n  paragraphNode.append(...(children || []));\n  paragraphNode.selectEnd();\n}\n\nfunction $getTableEdgeCursorPosition(\n  editor: LexicalEditor,\n  selection: RangeSelection,\n  tableNode: TableNode,\n) {\n  const tableNodeParent = tableNode.getParent();\n  if (!tableNodeParent) {\n    return undefined;\n  }\n\n  const tableNodeParentDOM = editor.getElementByKey(tableNodeParent.getKey());\n  if (!tableNodeParentDOM) {\n    return undefined;\n  }\n\n  // TODO: Add support for nested tables\n  const domSelection = window.getSelection();\n  if (!domSelection || domSelection.anchorNode !== tableNodeParentDOM) {\n    return undefined;\n  }\n\n  const anchorCellNode = $findMatchingParent(selection.anchor.getNode(), (n) =>\n    $isTableCellNode(n),\n  ) as TableCellNode | null;\n  if (!anchorCellNode) {\n    return undefined;\n  }\n\n  const parentTable = $findMatchingParent(anchorCellNode, (n) =>\n    $isTableNode(n),\n  );\n  if (!$isTableNode(parentTable) || !parentTable.is(tableNode)) {\n    return undefined;\n  }\n\n  const [tableMap, cellValue] = $computeTableMap(\n    tableNode,\n    anchorCellNode,\n    anchorCellNode,\n  );\n  const firstCell = tableMap[0][0];\n  const lastCell = tableMap[tableMap.length - 1][tableMap[0].length - 1];\n  const {startRow, startColumn} = cellValue;\n\n  const isAtFirstCell =\n    startRow === firstCell.startRow && startColumn === firstCell.startColumn;\n  const isAtLastCell =\n    startRow === lastCell.startRow && startColumn === lastCell.startColumn;\n\n  if (isAtFirstCell) {\n    return 'first';\n  } else if (isAtLastCell) {\n    return 'last';\n  } else {\n    return undefined;\n  }\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/table/LexicalTableUtils.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {TableMapType, TableMapValueType} from './LexicalTableSelection';\nimport type {ElementNode, PointType} from 'lexical';\n\nimport {$findMatchingParent} from '@lexical/utils';\nimport {\n  $createParagraphNode,\n  $createTextNode,\n  $getSelection,\n  $isRangeSelection,\n  LexicalNode,\n} from 'lexical';\nimport invariant from 'lexical/shared/invariant';\n\nimport {InsertTableCommandPayloadHeaders} from '.';\nimport {\n  $createTableCellNode,\n  $isTableCellNode,\n  TableCellHeaderState,\n  TableCellHeaderStates,\n  TableCellNode,\n} from './LexicalTableCellNode';\nimport {$createTableNode, $isTableNode, TableNode} from './LexicalTableNode';\nimport {TableDOMTable} from './LexicalTableObserver';\nimport {\n  $createTableRowNode,\n  $isTableRowNode,\n  TableRowNode,\n} from './LexicalTableRowNode';\nimport {$isTableSelection} from './LexicalTableSelection';\nimport {$isCaptionNode} from \"@lexical/table/LexicalCaptionNode\";\n\nexport function $createTableNodeWithDimensions(\n  rowCount: number,\n  columnCount: number,\n  includeHeaders: InsertTableCommandPayloadHeaders = true,\n): TableNode {\n  const tableNode = $createTableNode();\n\n  for (let iRow = 0; iRow < rowCount; iRow++) {\n    const tableRowNode = $createTableRowNode();\n\n    for (let iColumn = 0; iColumn < columnCount; iColumn++) {\n      let headerState = TableCellHeaderStates.NO_STATUS;\n\n      if (typeof includeHeaders === 'object') {\n        if (iRow === 0 && includeHeaders.rows) {\n          headerState |= TableCellHeaderStates.ROW;\n        }\n        if (iColumn === 0 && includeHeaders.columns) {\n          headerState |= TableCellHeaderStates.COLUMN;\n        }\n      } else if (includeHeaders) {\n        if (iRow === 0) {\n          headerState |= TableCellHeaderStates.ROW;\n        }\n        if (iColumn === 0) {\n          headerState |= TableCellHeaderStates.COLUMN;\n        }\n      }\n\n      const tableCellNode = $createTableCellNode(headerState);\n      const paragraphNode = $createParagraphNode();\n      paragraphNode.append($createTextNode());\n      tableCellNode.append(paragraphNode);\n      tableRowNode.append(tableCellNode);\n    }\n\n    tableNode.append(tableRowNode);\n  }\n\n  return tableNode;\n}\n\nexport function $getTableCellNodeFromLexicalNode(\n  startingNode: LexicalNode,\n): TableCellNode | null {\n  const node = $findMatchingParent(startingNode, (n) => $isTableCellNode(n));\n\n  if ($isTableCellNode(node)) {\n    return node;\n  }\n\n  return null;\n}\n\nexport function $getTableRowNodeFromTableCellNodeOrThrow(\n  startingNode: LexicalNode,\n): TableRowNode {\n  const node = $findMatchingParent(startingNode, (n) => $isTableRowNode(n));\n\n  if ($isTableRowNode(node)) {\n    return node;\n  }\n\n  throw new Error('Expected table cell to be inside of table row.');\n}\n\nexport function $getTableNodeFromLexicalNodeOrThrow(\n  startingNode: LexicalNode,\n): TableNode {\n  const node = $findMatchingParent(startingNode, (n) => $isTableNode(n));\n\n  if ($isTableNode(node)) {\n    return node;\n  }\n\n  throw new Error('Expected table cell to be inside of table.');\n}\n\nexport function $getTableRowIndexFromTableCellNode(\n  tableCellNode: TableCellNode,\n): number {\n  const tableRowNode = $getTableRowNodeFromTableCellNodeOrThrow(tableCellNode);\n  const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableRowNode);\n  return tableNode.getChildren().findIndex((n) => n.is(tableRowNode));\n}\n\nexport function $getTableColumnIndexFromTableCellNode(\n  tableCellNode: TableCellNode,\n): number {\n  const tableRowNode = $getTableRowNodeFromTableCellNodeOrThrow(tableCellNode);\n  return tableRowNode.getChildren().findIndex((n) => n.is(tableCellNode));\n}\n\nexport type TableCellSiblings = {\n  above: TableCellNode | null | undefined;\n  below: TableCellNode | null | undefined;\n  left: TableCellNode | null | undefined;\n  right: TableCellNode | null | undefined;\n};\n\nexport function $getTableCellSiblingsFromTableCellNode(\n  tableCellNode: TableCellNode,\n  table: TableDOMTable,\n): TableCellSiblings {\n  const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);\n  const {x, y} = tableNode.getCordsFromCellNode(tableCellNode, table);\n  return {\n    above: tableNode.getCellNodeFromCords(x, y - 1, table),\n    below: tableNode.getCellNodeFromCords(x, y + 1, table),\n    left: tableNode.getCellNodeFromCords(x - 1, y, table),\n    right: tableNode.getCellNodeFromCords(x + 1, y, table),\n  };\n}\n\nexport function $removeTableRowAtIndex(\n  tableNode: TableNode,\n  indexToDelete: number,\n): TableNode {\n  const tableRows = tableNode.getChildren();\n\n  if (indexToDelete >= tableRows.length || indexToDelete < 0) {\n    throw new Error('Expected table cell to be inside of table row.');\n  }\n\n  const targetRowNode = tableRows[indexToDelete];\n  targetRowNode.remove();\n  return tableNode;\n}\n\nexport function $insertTableRow(\n  tableNode: TableNode,\n  targetIndex: number,\n  shouldInsertAfter = true,\n  rowCount: number,\n  table: TableDOMTable,\n): TableNode {\n  const tableRows = tableNode.getChildren();\n\n  if (targetIndex >= tableRows.length || targetIndex < 0) {\n    throw new Error('Table row target index out of range');\n  }\n\n  const targetRowNode = tableRows[targetIndex];\n\n  if ($isTableRowNode(targetRowNode)) {\n    for (let r = 0; r < rowCount; r++) {\n      const tableRowCells = targetRowNode.getChildren<TableCellNode>();\n      const tableColumnCount = tableRowCells.length;\n      const newTableRowNode = $createTableRowNode();\n\n      for (let c = 0; c < tableColumnCount; c++) {\n        const tableCellFromTargetRow = tableRowCells[c];\n\n        invariant(\n          $isTableCellNode(tableCellFromTargetRow),\n          'Expected table cell',\n        );\n\n        const {above, below} = $getTableCellSiblingsFromTableCellNode(\n          tableCellFromTargetRow,\n          table,\n        );\n\n        let headerState = TableCellHeaderStates.NO_STATUS;\n        const width =\n          (above && above.getWidth()) ||\n          (below && below.getWidth()) ||\n          undefined;\n\n        if (\n          (above && above.hasHeaderState(TableCellHeaderStates.COLUMN)) ||\n          (below && below.hasHeaderState(TableCellHeaderStates.COLUMN))\n        ) {\n          headerState |= TableCellHeaderStates.COLUMN;\n        }\n\n        const tableCellNode = $createTableCellNode(headerState, 1, width);\n\n        tableCellNode.append($createParagraphNode());\n\n        newTableRowNode.append(tableCellNode);\n      }\n\n      if (shouldInsertAfter) {\n        targetRowNode.insertAfter(newTableRowNode);\n      } else {\n        targetRowNode.insertBefore(newTableRowNode);\n      }\n    }\n  } else {\n    throw new Error('Row before insertion index does not exist.');\n  }\n\n  return tableNode;\n}\n\nconst getHeaderState = (\n  currentState: TableCellHeaderState,\n  possibleState: TableCellHeaderState,\n): TableCellHeaderState => {\n  if (\n    currentState === TableCellHeaderStates.BOTH ||\n    currentState === possibleState\n  ) {\n    return possibleState;\n  }\n  return TableCellHeaderStates.NO_STATUS;\n};\n\nexport function $insertTableRow__EXPERIMENTAL(insertAfter = true): void {\n  const selection = $getSelection();\n  invariant(\n    $isRangeSelection(selection) || $isTableSelection(selection),\n    'Expected a RangeSelection or TableSelection',\n  );\n  const focus = selection.focus.getNode();\n  const [focusCell, , grid] = $getNodeTriplet(focus);\n  const [gridMap, focusCellMap] = $computeTableMap(grid, focusCell, focusCell);\n  const columnCount = gridMap[0].length;\n  const {startRow: focusStartRow} = focusCellMap;\n  if (insertAfter) {\n    const focusEndRow = focusStartRow + focusCell.__rowSpan - 1;\n    const focusEndRowMap = gridMap[focusEndRow];\n    const newRow = $createTableRowNode();\n    for (let i = 0; i < columnCount; i++) {\n      const {cell, startRow} = focusEndRowMap[i];\n      if (startRow + cell.__rowSpan - 1 <= focusEndRow) {\n        const currentCell = focusEndRowMap[i].cell as TableCellNode;\n        const currentCellHeaderState = currentCell.__headerState;\n\n        const headerState = getHeaderState(\n          currentCellHeaderState,\n          TableCellHeaderStates.COLUMN,\n        );\n\n        newRow.append(\n          $createTableCellNode(headerState).append($createParagraphNode()),\n        );\n      } else {\n        cell.setRowSpan(cell.__rowSpan + 1);\n      }\n    }\n    const focusEndRowNode = grid.getChildAtIndex(focusEndRow);\n    invariant(\n      $isTableRowNode(focusEndRowNode),\n      'focusEndRow is not a TableRowNode',\n    );\n    focusEndRowNode.insertAfter(newRow);\n  } else {\n    const focusStartRowMap = gridMap[focusStartRow];\n    const newRow = $createTableRowNode();\n    for (let i = 0; i < columnCount; i++) {\n      const {cell, startRow} = focusStartRowMap[i];\n      if (startRow === focusStartRow) {\n        const currentCell = focusStartRowMap[i].cell as TableCellNode;\n        const currentCellHeaderState = currentCell.__headerState;\n\n        const headerState = getHeaderState(\n          currentCellHeaderState,\n          TableCellHeaderStates.COLUMN,\n        );\n\n        newRow.append(\n          $createTableCellNode(headerState).append($createParagraphNode()),\n        );\n      } else {\n        cell.setRowSpan(cell.__rowSpan + 1);\n      }\n    }\n    const focusStartRowNode = grid.getChildAtIndex(focusStartRow);\n    invariant(\n      $isTableRowNode(focusStartRowNode),\n      'focusEndRow is not a TableRowNode',\n    );\n    focusStartRowNode.insertBefore(newRow);\n  }\n}\n\nexport function $insertTableColumn(\n  tableNode: TableNode,\n  targetIndex: number,\n  shouldInsertAfter = true,\n  columnCount: number,\n  table: TableDOMTable,\n): TableNode {\n  const tableRows = tableNode.getChildren();\n\n  const tableCellsToBeInserted = [];\n  for (let r = 0; r < tableRows.length; r++) {\n    const currentTableRowNode = tableRows[r];\n\n    if ($isTableRowNode(currentTableRowNode)) {\n      for (let c = 0; c < columnCount; c++) {\n        const tableRowChildren = currentTableRowNode.getChildren();\n        if (targetIndex >= tableRowChildren.length || targetIndex < 0) {\n          throw new Error('Table column target index out of range');\n        }\n\n        const targetCell = tableRowChildren[targetIndex];\n\n        invariant($isTableCellNode(targetCell), 'Expected table cell');\n\n        const {left, right} = $getTableCellSiblingsFromTableCellNode(\n          targetCell,\n          table,\n        );\n\n        let headerState = TableCellHeaderStates.NO_STATUS;\n\n        if (\n          (left && left.hasHeaderState(TableCellHeaderStates.ROW)) ||\n          (right && right.hasHeaderState(TableCellHeaderStates.ROW))\n        ) {\n          headerState |= TableCellHeaderStates.ROW;\n        }\n\n        const newTableCell = $createTableCellNode(headerState);\n\n        newTableCell.append($createParagraphNode());\n        tableCellsToBeInserted.push({\n          newTableCell,\n          targetCell,\n        });\n      }\n    }\n  }\n  tableCellsToBeInserted.forEach(({newTableCell, targetCell}) => {\n    if (shouldInsertAfter) {\n      targetCell.insertAfter(newTableCell);\n    } else {\n      targetCell.insertBefore(newTableCell);\n    }\n  });\n\n  return tableNode;\n}\n\nexport function $insertTableColumn__EXPERIMENTAL(insertAfter = true): void {\n  const selection = $getSelection();\n  invariant(\n    $isRangeSelection(selection) || $isTableSelection(selection),\n    'Expected a RangeSelection or TableSelection',\n  );\n  const anchor = selection.anchor.getNode();\n  const focus = selection.focus.getNode();\n  const [anchorCell] = $getNodeTriplet(anchor);\n  const [focusCell, , grid] = $getNodeTriplet(focus);\n  const [gridMap, focusCellMap, anchorCellMap] = $computeTableMap(\n    grid,\n    focusCell,\n    anchorCell,\n  );\n  const rowCount = gridMap.length;\n  const startColumn = insertAfter\n    ? Math.max(focusCellMap.startColumn, anchorCellMap.startColumn)\n    : Math.min(focusCellMap.startColumn, anchorCellMap.startColumn);\n  const insertAfterColumn = insertAfter\n    ? startColumn + focusCell.__colSpan - 1\n    : startColumn - 1;\n  const gridFirstChild = grid.getFirstChild();\n  invariant(\n    $isTableRowNode(gridFirstChild),\n    'Expected firstTable child to be a row',\n  );\n  let firstInsertedCell: null | TableCellNode = null;\n  function $createTableCellNodeForInsertTableColumn(\n    headerState: TableCellHeaderState = TableCellHeaderStates.NO_STATUS,\n  ) {\n    const cell = $createTableCellNode(headerState).append(\n      $createParagraphNode(),\n    );\n    if (firstInsertedCell === null) {\n      firstInsertedCell = cell;\n    }\n    return cell;\n  }\n  let loopRow: TableRowNode = gridFirstChild;\n  rowLoop: for (let i = 0; i < rowCount; i++) {\n    if (i !== 0) {\n      const currentRow = loopRow.getNextSibling();\n      invariant(\n        $isTableRowNode(currentRow),\n        'Expected row nextSibling to be a row',\n      );\n      loopRow = currentRow;\n    }\n    const rowMap = gridMap[i];\n\n    const currentCellHeaderState = (\n      rowMap[insertAfterColumn < 0 ? 0 : insertAfterColumn]\n        .cell as TableCellNode\n    ).__headerState;\n\n    const headerState = getHeaderState(\n      currentCellHeaderState,\n      TableCellHeaderStates.ROW,\n    );\n\n    if (insertAfterColumn < 0) {\n      $insertFirst(\n        loopRow,\n        $createTableCellNodeForInsertTableColumn(headerState),\n      );\n      continue;\n    }\n    const {\n      cell: currentCell,\n      startColumn: currentStartColumn,\n      startRow: currentStartRow,\n    } = rowMap[insertAfterColumn];\n    if (currentStartColumn + currentCell.__colSpan - 1 <= insertAfterColumn) {\n      let insertAfterCell: TableCellNode = currentCell;\n      let insertAfterCellRowStart = currentStartRow;\n      let prevCellIndex = insertAfterColumn;\n      while (insertAfterCellRowStart !== i && insertAfterCell.__rowSpan > 1) {\n        prevCellIndex -= currentCell.__colSpan;\n        if (prevCellIndex >= 0) {\n          const {cell: cell_, startRow: startRow_} = rowMap[prevCellIndex];\n          insertAfterCell = cell_;\n          insertAfterCellRowStart = startRow_;\n        } else {\n          loopRow.append($createTableCellNodeForInsertTableColumn(headerState));\n          continue rowLoop;\n        }\n      }\n      insertAfterCell.insertAfter(\n        $createTableCellNodeForInsertTableColumn(headerState),\n      );\n    } else {\n      currentCell.setColSpan(currentCell.__colSpan + 1);\n    }\n  }\n  if (firstInsertedCell !== null) {\n    $moveSelectionToCell(firstInsertedCell);\n  }\n}\n\nexport function $deleteTableColumn(\n  tableNode: TableNode,\n  targetIndex: number,\n): TableNode {\n  const tableRows = tableNode.getChildren();\n\n  for (let i = 0; i < tableRows.length; i++) {\n    const currentTableRowNode = tableRows[i];\n\n    if ($isTableRowNode(currentTableRowNode)) {\n      const tableRowChildren = currentTableRowNode.getChildren();\n\n      if (targetIndex >= tableRowChildren.length || targetIndex < 0) {\n        throw new Error('Table column target index out of range');\n      }\n\n      tableRowChildren[targetIndex].remove();\n    }\n  }\n\n  return tableNode;\n}\n\nexport function $deleteTableRow__EXPERIMENTAL(): void {\n  const selection = $getSelection();\n  invariant(\n    $isRangeSelection(selection) || $isTableSelection(selection),\n    'Expected a RangeSelection or TableSelection',\n  );\n  const anchor = selection.anchor.getNode();\n  const focus = selection.focus.getNode();\n  const [anchorCell, , grid] = $getNodeTriplet(anchor);\n  const [focusCell] = $getNodeTriplet(focus);\n  const [gridMap, anchorCellMap, focusCellMap] = $computeTableMap(\n    grid,\n    anchorCell,\n    focusCell,\n  );\n  const {startRow: anchorStartRow} = anchorCellMap;\n  const {startRow: focusStartRow} = focusCellMap;\n  const focusEndRow = focusStartRow + focusCell.__rowSpan - 1;\n  if (gridMap.length === focusEndRow - anchorStartRow + 1) {\n    // Empty grid\n    grid.remove();\n    return;\n  }\n  const columnCount = gridMap[0].length;\n  const nextRow = gridMap[focusEndRow + 1];\n  const nextRowNode: null | TableRowNode = grid.getChildAtIndex(\n    focusEndRow + 1,\n  );\n  for (let row = focusEndRow; row >= anchorStartRow; row--) {\n    for (let column = columnCount - 1; column >= 0; column--) {\n      const {\n        cell,\n        startRow: cellStartRow,\n        startColumn: cellStartColumn,\n      } = gridMap[row][column];\n      if (cellStartColumn !== column) {\n        // Don't repeat work for the same Cell\n        continue;\n      }\n      // Rows overflowing top have to be trimmed\n      if (row === anchorStartRow && cellStartRow < anchorStartRow) {\n        cell.setRowSpan(cell.__rowSpan - (cellStartRow - anchorStartRow));\n      }\n      // Rows overflowing bottom have to be trimmed and moved to the next row\n      if (\n        cellStartRow >= anchorStartRow &&\n        cellStartRow + cell.__rowSpan - 1 > focusEndRow\n      ) {\n        cell.setRowSpan(cell.__rowSpan - (focusEndRow - cellStartRow + 1));\n        invariant(nextRowNode !== null, 'Expected nextRowNode not to be null');\n        if (column === 0) {\n          $insertFirst(nextRowNode, cell);\n        } else {\n          const {cell: previousCell} = nextRow[column - 1];\n          previousCell.insertAfter(cell);\n        }\n      }\n    }\n    const rowNode = grid.getChildAtIndex(row);\n    invariant(\n      $isTableRowNode(rowNode),\n      'Expected GridNode childAtIndex(%s) to be RowNode',\n      String(row),\n    );\n    rowNode.remove();\n  }\n  if (nextRow !== undefined) {\n    const {cell} = nextRow[0];\n    $moveSelectionToCell(cell);\n  } else {\n    const previousRow = gridMap[anchorStartRow - 1];\n    const {cell} = previousRow[0];\n    $moveSelectionToCell(cell);\n  }\n}\n\nexport function $deleteTableColumn__EXPERIMENTAL(): void {\n  const selection = $getSelection();\n  invariant(\n    $isRangeSelection(selection) || $isTableSelection(selection),\n    'Expected a RangeSelection or TableSelection',\n  );\n  const anchor = selection.anchor.getNode();\n  const focus = selection.focus.getNode();\n  const [anchorCell, , grid] = $getNodeTriplet(anchor);\n  const [focusCell] = $getNodeTriplet(focus);\n  const [gridMap, anchorCellMap, focusCellMap] = $computeTableMap(\n    grid,\n    anchorCell,\n    focusCell,\n  );\n  const {startColumn: anchorStartColumn} = anchorCellMap;\n  const {startRow: focusStartRow, startColumn: focusStartColumn} = focusCellMap;\n  const startColumn = Math.min(anchorStartColumn, focusStartColumn);\n  const endColumn = Math.max(\n    anchorStartColumn + anchorCell.__colSpan - 1,\n    focusStartColumn + focusCell.__colSpan - 1,\n  );\n  const selectedColumnCount = endColumn - startColumn + 1;\n  const columnCount = gridMap[0].length;\n  if (columnCount === endColumn - startColumn + 1) {\n    // Empty grid\n    grid.selectPrevious();\n    grid.remove();\n    return;\n  }\n  const rowCount = gridMap.length;\n  for (let row = 0; row < rowCount; row++) {\n    for (let column = startColumn; column <= endColumn; column++) {\n      const {cell, startColumn: cellStartColumn} = gridMap[row][column];\n      if (cellStartColumn < startColumn) {\n        if (column === startColumn) {\n          const overflowLeft = startColumn - cellStartColumn;\n          // Overflowing left\n          cell.setColSpan(\n            cell.__colSpan -\n              // Possible overflow right too\n              Math.min(selectedColumnCount, cell.__colSpan - overflowLeft),\n          );\n        }\n      } else if (cellStartColumn + cell.__colSpan - 1 > endColumn) {\n        if (column === endColumn) {\n          // Overflowing right\n          const inSelectedArea = endColumn - cellStartColumn + 1;\n          cell.setColSpan(cell.__colSpan - inSelectedArea);\n        }\n      } else {\n        cell.remove();\n      }\n    }\n  }\n  const focusRowMap = gridMap[focusStartRow];\n  const nextColumn =\n    anchorStartColumn > focusStartColumn\n      ? focusRowMap[anchorStartColumn + anchorCell.__colSpan]\n      : focusRowMap[focusStartColumn + focusCell.__colSpan];\n  if (nextColumn !== undefined) {\n    const {cell} = nextColumn;\n    $moveSelectionToCell(cell);\n  } else {\n    const previousRow =\n      focusStartColumn < anchorStartColumn\n        ? focusRowMap[focusStartColumn - 1]\n        : focusRowMap[anchorStartColumn - 1];\n    const {cell} = previousRow;\n    $moveSelectionToCell(cell);\n  }\n}\n\nfunction $moveSelectionToCell(cell: TableCellNode): void {\n  const firstDescendant = cell.getFirstDescendant();\n  if (firstDescendant == null) {\n    cell.selectStart();\n  } else {\n    firstDescendant.getParentOrThrow().selectStart();\n  }\n}\n\nfunction $insertFirst(parent: ElementNode, node: LexicalNode): void {\n  const firstChild = parent.getFirstChild();\n  if (firstChild !== null) {\n    firstChild.insertBefore(node);\n  } else {\n    parent.append(node);\n  }\n}\n\nexport function $unmergeCell(): void {\n  const selection = $getSelection();\n  invariant(\n    $isRangeSelection(selection) || $isTableSelection(selection),\n    'Expected a RangeSelection or TableSelection',\n  );\n  const anchor = selection.anchor.getNode();\n  const [cell, row, grid] = $getNodeTriplet(anchor);\n  const colSpan = cell.__colSpan;\n  const rowSpan = cell.__rowSpan;\n  if (colSpan > 1) {\n    for (let i = 1; i < colSpan; i++) {\n      cell.insertAfter(\n        $createTableCellNode(TableCellHeaderStates.NO_STATUS).append(\n          $createParagraphNode(),\n        ),\n      );\n    }\n    cell.setColSpan(1);\n  }\n  if (rowSpan > 1) {\n    const [map, cellMap] = $computeTableMap(grid, cell, cell);\n    const {startColumn, startRow} = cellMap;\n    let currentRowNode;\n    for (let i = 1; i < rowSpan; i++) {\n      const currentRow = startRow + i;\n      const currentRowMap = map[currentRow];\n      currentRowNode = (currentRowNode || row).getNextSibling();\n      invariant(\n        $isTableRowNode(currentRowNode),\n        'Expected row next sibling to be a row',\n      );\n      let insertAfterCell: null | TableCellNode = null;\n      for (let column = 0; column < startColumn; column++) {\n        const currentCellMap = currentRowMap[column];\n        const currentCell = currentCellMap.cell;\n        if (currentCellMap.startRow === currentRow) {\n          insertAfterCell = currentCell;\n        }\n        if (currentCell.__colSpan > 1) {\n          column += currentCell.__colSpan - 1;\n        }\n      }\n      if (insertAfterCell === null) {\n        for (let j = 0; j < colSpan; j++) {\n          $insertFirst(\n            currentRowNode,\n            $createTableCellNode(TableCellHeaderStates.NO_STATUS).append(\n              $createParagraphNode(),\n            ),\n          );\n        }\n      } else {\n        for (let j = 0; j < colSpan; j++) {\n          insertAfterCell.insertAfter(\n            $createTableCellNode(TableCellHeaderStates.NO_STATUS).append(\n              $createParagraphNode(),\n            ),\n          );\n        }\n      }\n    }\n    cell.setRowSpan(1);\n  }\n}\n\nexport function $computeTableMap(\n  grid: TableNode,\n  cellA: TableCellNode,\n  cellB: TableCellNode,\n): [TableMapType, TableMapValueType, TableMapValueType] {\n  const [tableMap, cellAValue, cellBValue] = $computeTableMapSkipCellCheck(\n    grid,\n    cellA,\n    cellB,\n  );\n  invariant(cellAValue !== null, 'Anchor not found in Grid');\n  invariant(cellBValue !== null, 'Focus not found in Grid');\n  return [tableMap, cellAValue, cellBValue];\n}\n\nexport function $computeTableMapSkipCellCheck(\n  grid: TableNode,\n  cellA: null | TableCellNode,\n  cellB: null | TableCellNode,\n): [TableMapType, TableMapValueType | null, TableMapValueType | null] {\n  const tableMap: TableMapType = [];\n  let cellAValue: null | TableMapValueType = null;\n  let cellBValue: null | TableMapValueType = null;\n  function write(startRow: number, startColumn: number, cell: TableCellNode) {\n    const value = {\n      cell,\n      startColumn,\n      startRow,\n    };\n    const rowSpan = cell.__rowSpan;\n    const colSpan = cell.__colSpan;\n    for (let i = 0; i < rowSpan; i++) {\n      if (tableMap[startRow + i] === undefined) {\n        tableMap[startRow + i] = [];\n      }\n      for (let j = 0; j < colSpan; j++) {\n        tableMap[startRow + i][startColumn + j] = value;\n      }\n    }\n    if (cellA !== null && cellA.is(cell)) {\n      cellAValue = value;\n    }\n    if (cellB !== null && cellB.is(cell)) {\n      cellBValue = value;\n    }\n  }\n  function isEmpty(row: number, column: number) {\n    return tableMap[row] === undefined || tableMap[row][column] === undefined;\n  }\n\n  const gridChildren = grid.getChildren().filter(node => !$isCaptionNode(node));\n  for (let i = 0; i < gridChildren.length; i++) {\n    const row = gridChildren[i];\n    invariant(\n      $isTableRowNode(row),\n      'Expected GridNode children to be TableRowNode',\n    );\n    const rowChildren = row.getChildren();\n    let j = 0;\n    for (const cell of rowChildren) {\n      invariant(\n        $isTableCellNode(cell),\n        'Expected TableRowNode children to be TableCellNode',\n      );\n      while (!isEmpty(i, j)) {\n        j++;\n      }\n      write(i, j, cell);\n      j += cell.__colSpan;\n    }\n  }\n  return [tableMap, cellAValue, cellBValue];\n}\n\nexport function $getNodeTriplet(\n  source: PointType | LexicalNode | TableCellNode,\n): [TableCellNode, TableRowNode, TableNode] {\n  let cell: TableCellNode;\n  if (source instanceof TableCellNode) {\n    cell = source;\n  } else if ('__type' in source) {\n    const cell_ = $findMatchingParent(source, $isTableCellNode);\n    invariant(\n      $isTableCellNode(cell_),\n      'Expected to find a parent TableCellNode',\n    );\n    cell = cell_;\n  } else {\n    const cell_ = $findMatchingParent(source.getNode(), $isTableCellNode);\n    invariant(\n      $isTableCellNode(cell_),\n      'Expected to find a parent TableCellNode',\n    );\n    cell = cell_;\n  }\n  const row = cell.getParent();\n  invariant(\n    $isTableRowNode(row),\n    'Expected TableCellNode to have a parent TableRowNode',\n  );\n  const grid = row.getParent();\n  invariant(\n    $isTableNode(grid),\n    'Expected TableRowNode to have a parent GridNode',\n  );\n  return [cell, row, grid];\n}\n\nexport function $getTableCellNodeRect(tableCellNode: TableCellNode): {\n  rowIndex: number;\n  columnIndex: number;\n  rowSpan: number;\n  colSpan: number;\n} | null {\n  const [cellNode, , gridNode] = $getNodeTriplet(tableCellNode);\n  const rows = gridNode.getChildren<TableRowNode>();\n  const rowCount = rows.length;\n  const columnCount = rows[0].getChildren().length;\n\n  // Create a matrix of the same size as the table to track the position of each cell\n  const cellMatrix = new Array(rowCount);\n  for (let i = 0; i < rowCount; i++) {\n    cellMatrix[i] = new Array(columnCount);\n  }\n\n  for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {\n    const row = rows[rowIndex];\n    const cells = row.getChildren<TableCellNode>();\n    let columnIndex = 0;\n\n    for (let cellIndex = 0; cellIndex < cells.length; cellIndex++) {\n      // Find the next available position in the matrix, skip the position of merged cells\n      while (cellMatrix[rowIndex][columnIndex]) {\n        columnIndex++;\n      }\n\n      const cell = cells[cellIndex];\n      const rowSpan = cell.__rowSpan || 1;\n      const colSpan = cell.__colSpan || 1;\n\n      // Put the cell into the corresponding position in the matrix\n      for (let i = 0; i < rowSpan; i++) {\n        for (let j = 0; j < colSpan; j++) {\n          cellMatrix[rowIndex + i][columnIndex + j] = cell;\n        }\n      }\n\n      // Return to the original index, row span and column span of the cell.\n      if (cellNode === cell) {\n        return {\n          colSpan,\n          columnIndex,\n          rowIndex,\n          rowSpan,\n        };\n      }\n\n      columnIndex += colSpan;\n    }\n  }\n\n  return null;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/table/__tests__/unit/LexicalTableCellNode.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {$createTableCellNode, TableCellHeaderStates} from '@lexical/table';\nimport {initializeUnitTest} from 'lexical/__tests__/utils';\n\nconst editorConfig = Object.freeze({\n  namespace: '',\n  theme: {\n    tableCell: 'test-table-cell-class',\n  },\n});\n\ndescribe('LexicalTableCellNode tests', () => {\n  initializeUnitTest((testEnv) => {\n    test('TableCellNode.constructor', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const cellNode = $createTableCellNode(TableCellHeaderStates.NO_STATUS);\n\n        expect(cellNode).not.toBe(null);\n      });\n\n      expect(() =>\n        $createTableCellNode(TableCellHeaderStates.NO_STATUS),\n      ).toThrow();\n    });\n\n    test('TableCellNode.createDOM()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const cellNode = $createTableCellNode(TableCellHeaderStates.NO_STATUS);\n        expect(cellNode.createDOM(editorConfig).outerHTML).toBe(\n          `<td class=\"${editorConfig.theme.tableCell}\"></td>`,\n        );\n\n        const headerCellNode = $createTableCellNode(TableCellHeaderStates.ROW);\n        expect(headerCellNode.createDOM(editorConfig).outerHTML).toBe(\n          `<th class=\"${editorConfig.theme.tableCell}\"></th>`,\n        );\n\n        const colSpan = 2;\n        const cellWithRowSpanNode = $createTableCellNode(\n          TableCellHeaderStates.NO_STATUS,\n          colSpan,\n        );\n        expect(cellWithRowSpanNode.createDOM(editorConfig).outerHTML).toBe(\n          `<td colspan=\"${colSpan}\" class=\"${editorConfig.theme.tableCell}\"></td>`,\n        );\n\n        const cellWidth = 200;\n        const cellWithCustomWidthNode = $createTableCellNode(\n          TableCellHeaderStates.NO_STATUS,\n          undefined,\n          cellWidth,\n        );\n        expect(cellWithCustomWidthNode.createDOM(editorConfig).outerHTML).toBe(\n          `<td style=\"width: ${cellWidth}px;\" class=\"${editorConfig.theme.tableCell}\"></td>`,\n        );\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/table/__tests__/unit/LexicalTableNode.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {$insertDataTransferForRichText} from '@lexical/clipboard';\nimport {\n  $createTableNode,\n} from '@lexical/table';\nimport {\n  $createParagraphNode,\n  $getRoot,\n  $getSelection,\n  $isRangeSelection,\n} from 'lexical';\nimport {\n  DataTransferMock,\n  initializeUnitTest,\n  invariant,\n} from 'lexical/__tests__/utils';\n\nexport class ClipboardDataMock {\n  getData: jest.Mock<string, [string]>;\n  setData: jest.Mock<void, [string, string]>;\n\n  constructor() {\n    this.getData = jest.fn();\n    this.setData = jest.fn();\n  }\n}\n\nexport class ClipboardEventMock extends Event {\n  clipboardData: ClipboardDataMock;\n\n  constructor(type: string, options?: EventInit) {\n    super(type, options);\n    this.clipboardData = new ClipboardDataMock();\n  }\n}\n\nglobal.document.execCommand = function execCommandMock(\n  commandId: string,\n  showUI?: boolean,\n  value?: string,\n): boolean {\n  return true;\n};\nObject.defineProperty(window, 'ClipboardEvent', {\n  value: new ClipboardEventMock('cut'),\n});\n\nconst editorConfig = Object.freeze({\n  namespace: '',\n  theme: {\n    table: 'test-table-class',\n  },\n});\n\ndescribe('LexicalTableNode tests', () => {\n  initializeUnitTest(\n    (testEnv) => {\n      beforeEach(async () => {\n        const {editor} = testEnv;\n        await editor.update(() => {\n          const root = $getRoot();\n          const paragraph = $createParagraphNode();\n          root.append(paragraph);\n          paragraph.select();\n        });\n      });\n\n      test('TableNode.constructor', async () => {\n        const {editor} = testEnv;\n\n        await editor.update(() => {\n          const tableNode = $createTableNode();\n\n          expect(tableNode).not.toBe(null);\n        });\n\n        expect(() => $createTableNode()).toThrow();\n      });\n\n      test('TableNode.createDOM()', async () => {\n        const {editor} = testEnv;\n\n        await editor.update(() => {\n          const tableNode = $createTableNode();\n\n          expect(tableNode.createDOM(editorConfig).outerHTML).toBe(\n            `<table class=\"${editorConfig.theme.table}\"></table>`,\n          );\n        });\n      });\n\n      test('Copy table from an external source', async () => {\n        const {editor} = testEnv;\n\n        const dataTransfer = new DataTransferMock();\n        dataTransfer.setData(\n          'text/html',\n          '<html><body><meta charset=\"utf-8\"><b style=\"font-weight:normal;\" id=\"docs-internal-guid-16a69100-7fff-6cb9-b829-cb1def16a58d\"><div style=\"margin-left:0pt;\" align=\"left\"><table style=\"border:none;border-collapse:collapse;table-layout:fixed;width:468pt\"><colgroup><col /><col /></colgroup><tbody><tr style=\"height:22.015pt\"><td style=\"border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;\"><p style=\"line-height:1.2;margin-top:0pt;margin-bottom:0pt;\"><span style=\"font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">Hello there</span></p></td><td style=\"border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;\"><p style=\"line-height:1.2;margin-top:0pt;margin-bottom:0pt;\"><span style=\"font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">General Kenobi!</span></p></td></tr><tr style=\"height:22.015pt\"><td style=\"border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;\"><p style=\"line-height:1.2;margin-top:0pt;margin-bottom:0pt;\"><span style=\"font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">Lexical is nice</span></p></td><td style=\"border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;\"><br /></td></tr></tbody></table></div></b><!--EndFragment--></body></html>',\n        );\n        await editor.update(() => {\n          const selection = $getSelection();\n          invariant(\n            $isRangeSelection(selection),\n            'isRangeSelection(selection)',\n          );\n          $insertDataTransferForRichText(dataTransfer, selection, editor);\n        });\n        // Make sure paragraph is inserted inside empty cells\n        expect(testEnv.innerHTML).toBe(\n          `<table style=\"border-collapse: collapse; table-layout: fixed; width: 468pt;\"><colgroup><col><col></colgroup><tr style=\"height: 22.015pt;\"><td style=\"border-left: 1pt solid rgb(0, 0, 0); border-right: 1pt solid rgb(0, 0, 0); border-bottom: 1pt solid rgb(0, 0, 0); border-top: 1pt solid rgb(0, 0, 0); vertical-align: top; padding: 5pt 5pt 5pt 5pt; overflow: hidden; overflow-wrap: break-word;\"><p><span style=\"color: rgb(0, 0, 0);\" data-lexical-text=\"true\">Hello there</span></p></td><td style=\"border-left: 1pt solid rgb(0, 0, 0); border-right: 1pt solid rgb(0, 0, 0); border-bottom: 1pt solid rgb(0, 0, 0); border-top: 1pt solid rgb(0, 0, 0); vertical-align: top; padding: 5pt 5pt 5pt 5pt; overflow: hidden; overflow-wrap: break-word;\"><p><span style=\"color: rgb(0, 0, 0);\" data-lexical-text=\"true\">General Kenobi!</span></p></td></tr><tr style=\"height: 22.015pt;\"><td style=\"border-left: 1pt solid rgb(0, 0, 0); border-right: 1pt solid rgb(0, 0, 0); border-bottom: 1pt solid rgb(0, 0, 0); border-top: 1pt solid rgb(0, 0, 0); vertical-align: top; padding: 5pt 5pt 5pt 5pt; overflow: hidden; overflow-wrap: break-word;\"><p><span style=\"color: rgb(0, 0, 0);\" data-lexical-text=\"true\">Lexical is nice</span></p></td><td style=\"border-left: 1pt solid rgb(0, 0, 0); border-right: 1pt solid rgb(0, 0, 0); border-bottom: 1pt solid rgb(0, 0, 0); border-top: 1pt solid rgb(0, 0, 0); vertical-align: top; padding: 5pt 5pt 5pt 5pt; overflow: hidden; overflow-wrap: break-word;\"><p><br></p></td></tr></table>`,\n        );\n      });\n\n      test('Copy table from an external source like gdoc with formatting', async () => {\n        const {editor} = testEnv;\n\n        const dataTransfer = new DataTransferMock();\n        dataTransfer.setData(\n          'text/html',\n          '<google-sheets-html-origin><style type=\"text/css\"><!--td {border: 1px solid #cccccc;}br {mso-data-placement:same-cell;}--></style><table xmlns=\"http://www.w3.org/1999/xhtml\" cellspacing=\"0\" cellpadding=\"0\" border=\"1\" style=\"table-layout:fixed;font-size:10pt;font-family:Arial;width:0px;border-collapse:collapse;border:none\" data-sheets-root=\"1\"><colgroup><col width=\"100\"/><col width=\"189\"/><col width=\"171\"/></colgroup><tbody><tr style=\"height:21px;\"><td style=\"overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;font-weight:bold;\" data-sheets-value=\"{&quot;1&quot;:2,&quot;2&quot;:&quot;Surface&quot;}\">Surface</td><td style=\"overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;font-style:italic;\" data-sheets-value=\"{&quot;1&quot;:2,&quot;2&quot;:&quot;MWP_WORK_LS_COMPOSER&quot;}\">MWP_WORK_LS_COMPOSER</td><td style=\"overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-decoration:underline;text-align:right;\" data-sheets-value=\"{&quot;1&quot;:3,&quot;3&quot;:77349}\">77349</td></tr><tr style=\"height:21px;\"><td style=\"overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;\" data-sheets-value=\"{&quot;1&quot;:2,&quot;2&quot;:&quot;Lexical&quot;}\">Lexical</td><td style=\"overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-decoration:line-through;\" data-sheets-value=\"{&quot;1&quot;:2,&quot;2&quot;:&quot;XDS_RICH_TEXT_AREA&quot;}\">XDS_RICH_TEXT_AREA</td><td style=\"overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;\" data-sheets-value=\"{&quot;1&quot;:2,&quot;2&quot;:&quot;sdvd sdfvsfs&quot;}\" data-sheets-textstyleruns=\"{&quot;1&quot;:0}{&quot;1&quot;:5,&quot;2&quot;:{&quot;5&quot;:1}}\"><span style=\"font-size:10pt;font-family:Arial;font-style:normal;\">sdvd </span><span style=\"font-size:10pt;font-family:Arial;font-weight:bold;font-style:normal;\">sdfvsfs</span></td></tr></tbody></table>',\n        );\n        await editor.update(() => {\n          const selection = $getSelection();\n          invariant(\n            $isRangeSelection(selection),\n            'isRangeSelection(selection)',\n          );\n          $insertDataTransferForRichText(dataTransfer, selection, editor);\n        });\n        expect(testEnv.innerHTML).toBe(\n          `<table style=\"table-layout: fixed; font-size: 10pt; font-family: Arial; width: 0px; border-collapse: collapse;\"><colgroup><col style=\"width: 100px;\"><col style=\"width: 189px;\"><col style=\"width: 171px;\"></colgroup><tr style=\"height: 21px;\"><td style=\"overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom; font-weight: bold;\"><p><strong data-lexical-text=\"true\">Surface</strong></p></td><td style=\"overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom; font-style: italic;\"><p><em data-lexical-text=\"true\">MWP_WORK_LS_COMPOSER</em></p></td><td style=\"overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom; text-decoration: underline; text-align: right;\" class=\"align-right\"><p><span data-lexical-text=\"true\">77349</span></p></td></tr><tr style=\"height: 21px;\"><td style=\"overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom;\"><p><span data-lexical-text=\"true\">Lexical</span></p></td><td style=\"overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom; text-decoration: line-through;\"><p><span data-lexical-text=\"true\">XDS_RICH_TEXT_AREA</span></p></td><td style=\"overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom;\"><p><span data-lexical-text=\"true\">sdvd </span><strong data-lexical-text=\"true\">sdfvsfs</strong></p></td></tr></table>`,\n        );\n      });\n    },\n    undefined,\n  );\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/table/__tests__/unit/LexicalTableRowNode.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {$createTableRowNode} from '@lexical/table';\nimport {initializeUnitTest} from 'lexical/__tests__/utils';\n\nconst editorConfig = Object.freeze({\n  namespace: '',\n  theme: {\n    tableRow: 'test-table-row-class',\n  },\n});\n\ndescribe('LexicalTableRowNode tests', () => {\n  initializeUnitTest((testEnv) => {\n    test('TableRowNode.constructor', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const rowNode = $createTableRowNode();\n\n        expect(rowNode).not.toBe(null);\n      });\n\n      expect(() => $createTableRowNode()).toThrow();\n    });\n\n    test('TableRowNode.createDOM()', async () => {\n      const {editor} = testEnv;\n\n      await editor.update(() => {\n        const rowNode = $createTableRowNode();\n        expect(rowNode.createDOM(editorConfig).outerHTML).toBe(\n          `<tr class=\"${editorConfig.theme.tableRow}\"></tr>`,\n        );\n\n        const rowWithCustomHeightNode = $createTableRowNode();\n        expect(rowWithCustomHeightNode.createDOM(editorConfig).outerHTML).toBe(\n          `<tr class=\"${editorConfig.theme.tableRow}\"></tr>`,\n        );\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/table/__tests__/unit/LexicalTableSelection.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {$createTableSelection} from '@lexical/table';\nimport {\n  $createParagraphNode,\n  $createTextNode,\n  $getRoot,\n  $setSelection,\n  EditorState,\n  type LexicalEditor,\n  ParagraphNode,\n  RootNode,\n  TextNode,\n} from 'lexical';\nimport {createTestEditor} from 'lexical/__tests__/utils';\n\ndescribe('table selection', () => {\n  let originalText: TextNode;\n  let parsedParagraph: ParagraphNode;\n  let parsedRoot: RootNode;\n  let parsedText: TextNode;\n  let paragraphKey: string;\n  let textKey: string;\n  let parsedEditorState: EditorState;\n  let root: HTMLDivElement;\n  let container: HTMLDivElement | null = null;\n  let editor: LexicalEditor | null = null;\n\n  beforeEach(() => {\n    container = document.createElement('div');\n    root = document.createElement('div');\n    root.setAttribute('contenteditable', 'true');\n    document.body.appendChild(container);\n  });\n\n  afterEach(() => {\n    container?.remove();\n  });\n\n  function init(onError?: () => void) {\n    editor = createTestEditor({\n      nodes: [],\n      onError: onError || jest.fn(),\n      theme: {\n        text: {\n          bold: 'editor-text-bold',\n          italic: 'editor-text-italic',\n          underline: 'editor-text-underline',\n        },\n      },\n    })\n\n    editor.setRootElement(root);\n  }\n\n  async function update(fn: () => void) {\n    editor!.update(fn);\n\n    return Promise.resolve().then();\n  }\n\n  beforeEach(async () => {\n    init();\n\n    await update(() => {\n      const paragraph = $createParagraphNode();\n      originalText = $createTextNode('Hello world');\n      const selection = $createTableSelection();\n      selection.set(\n        originalText.getKey(),\n        originalText.getKey(),\n        originalText.getKey(),\n      );\n      $setSelection(selection);\n      paragraph.append(originalText);\n      $getRoot().append(paragraph);\n    });\n\n    const stringifiedEditorState = JSON.stringify(\n      editor!.getEditorState().toJSON(),\n    );\n\n    parsedEditorState = editor!.parseEditorState(stringifiedEditorState);\n    parsedEditorState.read(() => {\n      parsedRoot = $getRoot();\n      parsedParagraph = parsedRoot.getFirstChild()!;\n      paragraphKey = parsedParagraph.getKey();\n      parsedText = parsedParagraph.getFirstChild()!;\n      textKey = parsedText.getKey();\n    });\n  });\n\n  it('Parses the nodes of a stringified editor state', async () => {\n    expect(parsedRoot).toEqual({\n      __cachedText: null,\n      __dir: null,\n      __first: paragraphKey,\n      __key: 'root',\n      __last: paragraphKey,\n      __next: null,\n      __parent: null,\n      __prev: null,\n      __size: 1,\n      __style: '',\n      __type: 'root',\n    });\n    expect(parsedParagraph).toEqual({\n      __alignment: \"\",\n      __dir: null,\n      __first: textKey,\n      __id: '',\n      __inset: 0,\n      __key: paragraphKey,\n      __last: textKey,\n      __next: null,\n      __parent: 'root',\n      __prev: null,\n      __size: 1,\n      __style: '',\n      __textFormat: 0,\n      __textStyle: '',\n      __type: 'paragraph',\n    });\n    expect(parsedText).toEqual({\n      __detail: 0,\n      __format: 0,\n      __key: textKey,\n      __mode: 0,\n      __next: null,\n      __parent: paragraphKey,\n      __prev: null,\n      __style: '',\n      __text: 'Hello world',\n      __type: 'text',\n    });\n  });\n\n  it('Parses the text content of the editor state', async () => {\n    expect(parsedEditorState.read(() => $getRoot().__cachedText)).toBe(null);\n    expect(parsedEditorState.read(() => $getRoot().getTextContent())).toBe(\n      'Hello world',\n    );\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/table/constants.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nexport const PIXEL_VALUE_REG_EXP = /^(\\d+(?:\\.\\d+)?)px$/;\n\n// .PlaygroundEditorTheme__tableCell width value from\n// packages/lexical-playground/src/themes/PlaygroundEditorTheme.css\nexport const COLUMN_WIDTH = 75;\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/table/index.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nexport type {SerializedTableCellNode} from './LexicalTableCellNode';\nexport {\n  $createTableCellNode,\n  $isTableCellNode,\n  TableCellHeaderStates,\n  TableCellNode,\n} from './LexicalTableCellNode';\nexport type {\n  InsertTableCommandPayload,\n  InsertTableCommandPayloadHeaders,\n} from './LexicalTableCommands';\nexport {INSERT_TABLE_COMMAND} from './LexicalTableCommands';\nexport type {SerializedTableNode} from './LexicalTableNode';\nexport {\n  $createTableNode,\n  $getElementForTableNode,\n  $isTableNode,\n  TableNode,\n} from './LexicalTableNode';\nexport type {TableDOMCell} from './LexicalTableObserver';\nexport {TableObserver} from './LexicalTableObserver';\nexport type {SerializedTableRowNode} from './LexicalTableRowNode';\nexport {\n  $createTableRowNode,\n  $isTableRowNode,\n  TableRowNode,\n} from './LexicalTableRowNode';\nexport type {\n  TableMapType,\n  TableMapValueType,\n  TableSelection,\n  TableSelectionShape,\n} from './LexicalTableSelection';\nexport {\n  $createTableSelection,\n  $isTableSelection,\n} from './LexicalTableSelection';\nexport type {HTMLTableElementWithWithTableSelectionState} from './LexicalTableSelectionHelpers';\nexport {\n  $findCellNode,\n  $findTableNode,\n  applyTableHandlers,\n  getDOMCellFromTarget,\n  getTableObserverFromTableElement,\n} from './LexicalTableSelectionHelpers';\nexport {\n  $computeTableMap,\n  $computeTableMapSkipCellCheck,\n  $createTableNodeWithDimensions,\n  $deleteTableColumn,\n  $deleteTableColumn__EXPERIMENTAL,\n  $deleteTableRow__EXPERIMENTAL,\n  $getNodeTriplet,\n  $getTableCellNodeFromLexicalNode,\n  $getTableCellNodeRect,\n  $getTableColumnIndexFromTableCellNode,\n  $getTableNodeFromLexicalNodeOrThrow,\n  $getTableRowIndexFromTableCellNode,\n  $getTableRowNodeFromTableCellNodeOrThrow,\n  $insertTableColumn,\n  $insertTableColumn__EXPERIMENTAL,\n  $insertTableRow,\n  $insertTableRow__EXPERIMENTAL,\n  $removeTableRowAtIndex,\n  $unmergeCell,\n} from './LexicalTableUtils';\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/utils/__tests__/unit/LexicalElementHelpers.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {\n  addClassNamesToElement,\n  removeClassNamesFromElement,\n} from '@lexical/utils';\n\ndescribe('LexicalElementHelpers tests', () => {\n  describe('addClassNamesToElement() and removeClassNamesFromElement()', () => {\n    test('basic', async () => {\n      const element = document.createElement('div');\n      addClassNamesToElement(element, 'test-class');\n\n      expect(element.className).toEqual('test-class');\n\n      removeClassNamesFromElement(element, 'test-class');\n\n      expect(element.className).toEqual('');\n    });\n\n    test('empty', async () => {\n      const element = document.createElement('div');\n      addClassNamesToElement(\n        element,\n        null,\n        undefined,\n        false,\n        true,\n        '',\n        ' ',\n        '  \\t\\n',\n      );\n\n      expect(element.className).toEqual('');\n    });\n\n    test('multiple', async () => {\n      const element = document.createElement('div');\n      addClassNamesToElement(element, 'a', 'b', 'c');\n\n      expect(element.className).toEqual('a b c');\n\n      removeClassNamesFromElement(element, 'a', 'b', 'c');\n\n      expect(element.className).toEqual('');\n    });\n\n    test('space separated', async () => {\n      const element = document.createElement('div');\n      addClassNamesToElement(element, 'a b c');\n\n      expect(element.className).toEqual('a b c');\n\n      removeClassNamesFromElement(element, 'a b c');\n\n      expect(element.className).toEqual('');\n    });\n  });\n\n  test('multiple spaces', async () => {\n    const classNames = ' a  b   c \\t\\n  ';\n    const element = document.createElement('div');\n    addClassNamesToElement(element, classNames);\n\n    expect(element.className).toEqual('a b c');\n\n    removeClassNamesFromElement(element, classNames);\n\n    expect(element.className).toEqual('');\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/utils/__tests__/unit/LexicalEventHelpers.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\nimport {AutoLinkNode, LinkNode} from '@lexical/link';\nimport {ListItemNode, ListNode} from '@lexical/list';\nimport {registerRichText} from '@lexical/rich-text';\nimport {\n  applySelectionInputs,\n  pasteHTML,\n} from '@lexical/selection/__tests__/utils';\nimport {TableCellNode, TableNode, TableRowNode} from '@lexical/table';\nimport {$createParagraphNode, $insertNodes, LexicalEditor} from 'lexical';\nimport {createTestEditor, initializeClipboard} from 'lexical/__tests__/utils';\nimport {HeadingNode} from \"@lexical/rich-text/LexicalHeadingNode\";\nimport {QuoteNode} from \"@lexical/rich-text/LexicalQuoteNode\";\n\njest.mock('lexical/shared/environment', () => {\n  const originalModule = jest.requireActual('lexical/shared/environment');\n  return {...originalModule, IS_FIREFOX: true};\n});\n\nRange.prototype.getBoundingClientRect = function (): DOMRect {\n  const rect = {\n    bottom: 0,\n    height: 0,\n    left: 0,\n    right: 0,\n    top: 0,\n    width: 0,\n    x: 0,\n    y: 0,\n  };\n  return {\n    ...rect,\n    toJSON() {\n      return rect;\n    },\n  };\n};\n\ninitializeClipboard();\n\nRange.prototype.getBoundingClientRect = function (): DOMRect {\n  const rect = {\n    bottom: 0,\n    height: 0,\n    left: 0,\n    right: 0,\n    top: 0,\n    width: 0,\n    x: 0,\n    y: 0,\n  };\n  return {\n    ...rect,\n    toJSON() {\n      return rect;\n    },\n  };\n};\n\ndescribe('LexicalEventHelpers', () => {\n  let container: HTMLDivElement | null = null;\n\n  beforeEach(async () => {\n    container = document.createElement('div');\n    document.body.appendChild(container);\n    await init();\n  });\n\n  afterEach(() => {\n    document.body.removeChild(container!);\n    container = null;\n  });\n\n  let editor: LexicalEditor | null = null;\n\n  async function init() {\n\n    const config = {\n      nodes: [\n        LinkNode,\n        HeadingNode,\n        ListNode,\n        ListItemNode,\n        QuoteNode,\n        TableNode,\n        TableCellNode,\n        TableRowNode,\n        AutoLinkNode,\n      ],\n      theme: {\n        code: 'editor-code',\n        heading: {\n          h1: 'editor-heading-h1',\n          h2: 'editor-heading-h2',\n          h3: 'editor-heading-h3',\n          h4: 'editor-heading-h4',\n          h5: 'editor-heading-h5',\n          h6: 'editor-heading-h6',\n        },\n        image: 'editor-image',\n        list: {\n          listitem: 'editor-listitem',\n          olDepth: ['editor-list-ol'],\n          ulDepth: ['editor-list-ul'],\n        },\n        paragraph: 'editor-paragraph',\n        placeholder: 'editor-placeholder',\n        quote: 'editor-quote',\n        text: {\n          bold: 'editor-text-bold',\n          code: 'editor-text-code',\n          hashtag: 'editor-text-hashtag',\n          italic: 'editor-text-italic',\n          link: 'editor-text-link',\n          strikethrough: 'editor-text-strikethrough',\n          underline: 'editor-text-underline',\n          underlineStrikethrough: 'editor-text-underlineStrikethrough',\n        },\n      },\n    };\n\n    editor = createTestEditor(config);\n    registerRichText(editor);\n\n    const root = document.createElement('div');\n    root.setAttribute('contenteditable', 'true');\n    container?.append(root);\n\n    editor.setRootElement(root);\n\n    editor.update(() => {\n      $insertNodes([$createParagraphNode()])\n    });\n    editor.commitUpdates();\n  }\n\n  async function update(fn: () => void) {\n    await editor!.update(fn);\n    editor?.commitUpdates();\n\n    return Promise.resolve().then();\n  }\n\n  test('Expect initial output to be a block with no text', () => {\n    expect(container!.innerHTML).toBe(\n      '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><br></p></div>',\n    );\n  });\n\n  describe('onPasteForRichText', () => {\n    describe('baseline', () => {\n      const suite = [\n        {\n          expectedHTML:\n            '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><h1 class=\"editor-heading-h1\"><span data-lexical-text=\"true\">Hello</span></h1></div>',\n          inputs: [pasteHTML(`<meta charset='utf-8'><h1>Hello</h1>`)],\n          name: 'should produce the correct editor state from a pasted HTML h1 element',\n        },\n        {\n          expectedHTML:\n            '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><h2 class=\"editor-heading-h2\"><span data-lexical-text=\"true\">From</span></h2></div>',\n          inputs: [pasteHTML(`<meta charset='utf-8'><h2>From</h2>`)],\n          name: 'should produce the correct editor state from a pasted HTML h2 element',\n        },\n        {\n          expectedHTML:\n            '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><h3 class=\"editor-heading-h3\"><span data-lexical-text=\"true\">The</span></h3></div>',\n          inputs: [pasteHTML(`<meta charset='utf-8'><h3>The</h3>`)],\n          name: 'should produce the correct editor state from a pasted HTML h3 element',\n        },\n        {\n          expectedHTML:\n            '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><ul class=\"editor-list-ul\"><li value=\"1\"><span data-lexical-text=\"true\">Other side</span></li><li value=\"2\"><span data-lexical-text=\"true\">I must have called</span></li></ul></div>',\n          inputs: [\n            pasteHTML(\n              `<meta charset='utf-8'><ul><li>Other side</li><li>I must have called</li></ul>`,\n            ),\n          ],\n          name: 'should produce the correct editor state from a pasted HTML ul element',\n        },\n        {\n          expectedHTML:\n            '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><ol class=\"editor-list-ol\"><li value=\"1\"><span data-lexical-text=\"true\">To tell you</span></li><li value=\"2\"><span data-lexical-text=\"true\">I’m sorry</span></li></ol></div>',\n          inputs: [\n            pasteHTML(\n              `<meta charset='utf-8'><ol><li>To tell you</li><li>I’m sorry</li></ol>`,\n            ),\n          ],\n          name: 'should produce the correct editor state from pasted HTML ol element',\n        },\n        {\n          expectedHTML:\n            '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">A thousand times</span></p></div>',\n          inputs: [pasteHTML(`<meta charset='utf-8'>A thousand times`)],\n          name: 'should produce the correct editor state from pasted DOM Text Node',\n        },\n        {\n          expectedHTML:\n            '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><strong class=\"editor-text-bold\" data-lexical-text=\"true\">Bold</strong></p></div>',\n          inputs: [pasteHTML(`<meta charset='utf-8'><b>Bold</b>`)],\n          name: 'should produce the correct editor state from a pasted HTML b element',\n        },\n        {\n          expectedHTML:\n            '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><em class=\"editor-text-italic\" data-lexical-text=\"true\">Italic</em></p></div>',\n          inputs: [pasteHTML(`<meta charset='utf-8'><i>Italic</i>`)],\n          name: 'should produce the correct editor state from a pasted HTML i element',\n        },\n        {\n          expectedHTML:\n            '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><em class=\"editor-text-italic\" data-lexical-text=\"true\">Italic</em></p></div>',\n          inputs: [pasteHTML(`<meta charset='utf-8'><em>Italic</em>`)],\n          name: 'should produce the correct editor state from a pasted HTML em element',\n        },\n        {\n          expectedHTML:\n            '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><span class=\"editor-text-underline\" data-lexical-text=\"true\">Underline</span></p></div>',\n          inputs: [pasteHTML(`<meta charset='utf-8'><u>Underline</u>`)],\n          name: 'should produce the correct editor state from a pasted HTML u element',\n        },\n        {\n          expectedHTML:\n            '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><h1 class=\"editor-heading-h1\"><span data-lexical-text=\"true\">Lyrics to Hello by Adele</span></h1><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">A thousand times</span></p></div>',\n          inputs: [\n            pasteHTML(\n              `<meta charset='utf-8'><h1>Lyrics to Hello by Adele</h1>A thousand times`,\n            ),\n          ],\n          name: 'should produce the correct editor state from pasted heading node followed by a DOM Text Node',\n        },\n        {\n          expectedHTML:\n            '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><a href=\"https://facebook.com\"><span data-lexical-text=\"true\">Facebook</span></a></p></div>',\n          inputs: [\n            pasteHTML(\n              `<meta charset='utf-8'><a href=\"https://facebook.com\">Facebook</a>`,\n            ),\n          ],\n          name: 'should produce the correct editor state from a pasted HTML anchor element',\n        },\n        {\n          expectedHTML:\n            '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">Welcome to</span><a href=\"https://facebook.com\"><span data-lexical-text=\"true\">Facebook!</span></a></p></div>',\n          inputs: [\n            pasteHTML(\n              `<meta charset='utf-8'>Welcome to<a href=\"https://facebook.com\">Facebook!</a>`,\n            ),\n          ],\n          name: 'should produce the correct editor state from a pasted combination of an HTML text node followed by an anchor node',\n        },\n        {\n          expectedHTML:\n            '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">Welcome to</span><a href=\"https://facebook.com\"><span data-lexical-text=\"true\">Facebook!</span></a><span data-lexical-text=\"true\">We hope you like it here.</span></p></div>',\n          inputs: [\n            pasteHTML(\n              `<meta charset='utf-8'>Welcome to<a href=\"https://facebook.com\">Facebook!</a>We hope you like it here.`,\n            ),\n          ],\n          name: 'should produce the correct editor state from a pasted combination of HTML anchor elements and text nodes',\n        },\n        {\n          expectedHTML:\n            '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><ul class=\"editor-list-ul\"><li value=\"1\"><span data-lexical-text=\"true\">Hello</span></li><li value=\"2\"><span data-lexical-text=\"true\">from the other</span></li><li value=\"3\"><span data-lexical-text=\"true\">side</span></li></ul></div>',\n          inputs: [\n            pasteHTML(\n              `<meta charset='utf-8'><doesnotexist><ul><li>Hello</li><li>from the other</li><li>side</li></ul></doesnotexist>`,\n            ),\n          ],\n          name: 'should ignore DOM node types that do not have transformers, but still process their children.',\n        },\n        {\n          expectedHTML:\n            '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><ul class=\"editor-list-ul\"><li value=\"1\"><span data-lexical-text=\"true\">Hello</span></li><li value=\"2\"><span data-lexical-text=\"true\">from the other</span></li><li value=\"3\"><span data-lexical-text=\"true\">side</span></li></ul></div>',\n          inputs: [\n            pasteHTML(\n              `<meta charset='utf-8'><doesnotexist><doesnotexist><ul><li>Hello</li><li>from the other</li><li>side</li></ul></doesnotexist></doesnotexist>`,\n            ),\n          ],\n          name: 'should ignore multiple levels of DOM node types that do not have transformers, but still process their children.',\n        },\n        {\n          expectedHTML:\n            '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">Welcome to</span><a href=\"https://facebook.com\"><strong class=\"editor-text-bold\" data-lexical-text=\"true\">Facebook!</strong></a><span data-lexical-text=\"true\">We hope you like it here.</span></p></div>',\n          inputs: [\n            pasteHTML(\n              `<meta charset='utf-8'>Welcome to<b><a href=\"https://facebook.com\">Facebook!</a></b>We hope you like it here.`,\n            ),\n          ],\n          name: 'should preserve formatting from HTML tags on deeply nested text nodes.',\n        },\n        {\n          expectedHTML:\n            '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">Welcome to</span><a href=\"https://facebook.com\"><strong class=\"editor-text-bold\" data-lexical-text=\"true\">Facebook!</strong></a><strong class=\"editor-text-bold\" data-lexical-text=\"true\">We hope you like it here.</strong></p></div>',\n          inputs: [\n            pasteHTML(\n              `<meta charset='utf-8'>Welcome to<b><a href=\"https://facebook.com\">Facebook!</a>We hope you like it here.</b>`,\n            ),\n          ],\n          name: 'should preserve formatting from HTML tags on deeply nested and top level text nodes.',\n        },\n        {\n          expectedHTML:\n            '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">Welcome to</span><a href=\"https://facebook.com\"><strong class=\"editor-text-bold editor-text-italic\" data-lexical-text=\"true\">Facebook!</strong></a><strong class=\"editor-text-bold editor-text-italic\" data-lexical-text=\"true\">We hope you like it here.</strong></p></div>',\n          inputs: [\n            pasteHTML(\n              `<meta charset='utf-8'>Welcome to<b><i><a href=\"https://facebook.com\">Facebook!</a>We hope you like it here.</i></b>`,\n            ),\n          ],\n          name: 'should preserve multiple types of formatting on deeply nested text nodes and top level text nodes',\n        },\n      ];\n\n      suite.forEach((testUnit, i) => {\n        const name = testUnit.name || 'Test case';\n\n        test(name + ` (#${i + 1})`, async () => {\n          await applySelectionInputs(testUnit.inputs, update, editor!);\n\n          // Validate HTML matches\n          expect(container!.innerHTML).toBe(testUnit.expectedHTML);\n        });\n      });\n    });\n\n    describe('Google Docs', () => {\n      const suite = [\n        {\n          expectedHTML:\n            '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><span style=\"color: rgb(0, 0, 0);\" data-lexical-text=\"true\">Get schwifty!</span></p></div>',\n          inputs: [\n            pasteHTML(\n              `<b style=\"font-weight:normal;\" id=\"docs-internal-guid-2c706577-7fff-f54a-fe65-12f480020fac\"><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">Get schwifty!</span></b>`,\n            ),\n          ],\n          name: 'should produce the correct editor state from Normal text',\n        },\n        {\n          expectedHTML:\n            '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><strong class=\"editor-text-bold\" style=\"color: rgb(0, 0, 0);\" data-lexical-text=\"true\">Get schwifty!</strong></p></div>',\n          inputs: [\n            pasteHTML(\n              `<b style=\"font-weight:normal;\" id=\"docs-internal-guid-9db03964-7fff-c26c-8b1e-9484fb3b54a4\"><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">Get schwifty!</span></b>`,\n            ),\n          ],\n          name: 'should produce the correct editor state from bold text',\n        },\n        {\n          expectedHTML:\n            '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><em class=\"editor-text-italic\" style=\"color: rgb(0, 0, 0);\" data-lexical-text=\"true\">Get schwifty!</em></p></div>',\n          inputs: [\n            pasteHTML(\n              `<b style=\"font-weight:normal;\" id=\"docs-internal-guid-9db03964-7fff-c26c-8b1e-9484fb3b54a4\"><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:italic;font-variant:normal;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">Get schwifty!</span></b>`,\n            ),\n          ],\n          name: 'should produce the correct editor state from italic text',\n        },\n        {\n          expectedHTML:\n            '<div contenteditable=\"true\" style=\"user-select: text; white-space: pre-wrap; word-break: break-word;\" data-lexical-editor=\"true\"><p class=\"editor-paragraph\"><span class=\"editor-text-strikethrough\" style=\"color: rgb(0, 0, 0);\" data-lexical-text=\"true\">Get schwifty!</span></p></div>',\n          inputs: [\n            pasteHTML(\n              `<b style=\"font-weight:normal;\" id=\"docs-internal-guid-9db03964-7fff-c26c-8b1e-9484fb3b54a4\"><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:line-through;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">Get schwifty!</span></b>`,\n            ),\n          ],\n          name: 'should produce the correct editor state from strikethrough text',\n        },\n      ];\n\n      suite.forEach((testUnit, i) => {\n        const name = testUnit.name || 'Test case';\n\n        test(name + ` (#${i + 1})`, async () => {\n          await applySelectionInputs(testUnit.inputs, update, editor!);\n\n          // Validate HTML matches\n          expect(container!.innerHTML).toBe(testUnit.expectedHTML);\n        });\n      });\n    });\n\n    describe('W3 spacing', () => {\n      const suite = [\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><span data-lexical-text=\"true\">hello world</span></p>',\n          inputs: [pasteHTML('<span>hello world</span>')],\n          name: 'inline hello world',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><span data-lexical-text=\"true\">hello world</span></p>',\n          inputs: [pasteHTML('<span>    hello  </span>world  ')],\n          name: 'inline hello world (2)',\n        },\n        {\n          // MS Office got it right\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><span data-lexical-text=\"true\"> hello world</span></p>',\n          inputs: [\n            pasteHTML(' <span style=\"white-space: pre\"> hello </span> world  '),\n          ],\n          name: 'pre + inline (inline collapses with pre)',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><span data-lexical-text=\"true\">  a b</span><span data-lexical-text=\"true\">\\t</span><span data-lexical-text=\"true\">c  </span></p>',\n          inputs: [pasteHTML('<p style=\"white-space: pre\">  a b\\tc  </p>')],\n          name: 'white-space: pre (1) (no touchy)',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><span data-lexical-text=\"true\">a b c</span></p>',\n          inputs: [pasteHTML('<p>\\ta\\tb  <span>c\\t</span>\\t</p>')],\n          name: 'tabs are collapsed',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><span data-lexical-text=\"true\">hello world</span></p>',\n          inputs: [\n            pasteHTML(`\n              <div>\n                hello\n                world\n              </div>\n            `),\n          ],\n          name: 'remove beginning + end spaces on the block',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><strong class=\"editor-text-bold\" data-lexical-text=\"true\">hello world</strong></p>',\n          inputs: [\n            pasteHTML(`\n              <div>\n                <strong>\n                  hello\n                  world\n                </strong>\n              </div>\n          `),\n          ],\n          name: 'remove beginning + end spaces on the block (2)',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><span data-lexical-text=\"true\">a </span><strong class=\"editor-text-bold\" data-lexical-text=\"true\">b</strong><span data-lexical-text=\"true\"> c</span></p>',\n          inputs: [\n            pasteHTML(`\n              <div>\n                a\n                <strong>b</strong>\n                c\n              </div>\n          `),\n          ],\n          name: 'remove beginning + end spaces on the block + anonymous inlines collapsible rules',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><strong class=\"editor-text-bold\" data-lexical-text=\"true\">a </strong><span data-lexical-text=\"true\">b</span></p>',\n          inputs: [pasteHTML('<div><strong>a </strong>b</div>')],\n          name: 'collapsibles and neighbors (1)',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><span data-lexical-text=\"true\">a</span><strong class=\"editor-text-bold\" data-lexical-text=\"true\"> b</strong></p>',\n          inputs: [pasteHTML('<div>a<strong> b</strong></div>')],\n          name: 'collapsibles and neighbors (2)',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><strong class=\"editor-text-bold\" data-lexical-text=\"true\">a </strong><span data-lexical-text=\"true\">b</span></p>',\n          inputs: [pasteHTML('<div><strong>a </strong><span></span>b</div>')],\n          name: 'collapsibles and neighbors (3)',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><span data-lexical-text=\"true\">a</span><strong class=\"editor-text-bold\" data-lexical-text=\"true\"> b</strong></p>',\n          inputs: [pasteHTML('<div>a<span></span><strong> b</strong></div>')],\n          name: 'collapsibles and neighbors (4)',\n        },\n        {\n          expectedHTML: '<p class=\"editor-paragraph\"><br></p>',\n          inputs: [\n            pasteHTML(`\n              <p>\n              </p>\n          `),\n          ],\n          name: 'empty block',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><span data-lexical-text=\"true\">a</span></p>',\n          inputs: [pasteHTML('<span> </span><span>a</span>')],\n          name: 'redundant inline at start',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><span data-lexical-text=\"true\">a</span></p>',\n          inputs: [pasteHTML('<span>a</span><span> </span>')],\n          name: 'redundant inline at end',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><span data-lexical-text=\"true\">a</span></p><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">b</span></p>',\n          inputs: [\n            pasteHTML(`\n            <div>\n              <p>\n                a\n              </p>\n              <p>\n                b\n              </p>\n            </div>\n            `),\n          ],\n          name: 'collapsible spaces with nested structures',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><strong class=\"editor-text-bold\" data-lexical-text=\"true\">a b</strong></p>',\n          inputs: [\n            pasteHTML(`\n            <div>\n              <strong>\n                a\n              </strong>\n              <strong>\n                b\n              </strong>\n            </div>\n            `),\n          ],\n          name: 'collapsible spaces with nested structures (3)',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><span data-lexical-text=\"true\">a</span><br><span data-lexical-text=\"true\">b</span></p>',\n          inputs: [\n            pasteHTML(`\n            <p>\n            a\n            <br>\n            b\n            </p>\n            `),\n          ],\n          name: 'forced line break should remain',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><span data-lexical-text=\"true\">a</span><br><span data-lexical-text=\"true\">b</span></p>',\n          inputs: [\n            pasteHTML(`\n            <p>\n            a\n            \\t<br>\\t\n            b\n            </p>\n            `),\n          ],\n          name: 'forced line break with tabs',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><span data-lexical-text=\"true\">paragraph1</span></p><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">paragraph2</span></p>',\n          inputs: [\n            pasteHTML(\n              '\\n<p class=\"p1\">paragraph1</p>\\n<p class=\"p1\">paragraph2</p>\\n',\n            ),\n          ],\n          name: 'two Apple Notes paragraphs',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><span data-lexical-text=\"true\">line 1</span><br><span data-lexical-text=\"true\">line 2</span></p><p class=\"editor-paragraph\"><br></p><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">paragraph 1</span></p><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">paragraph 2</span></p>',\n          inputs: [\n            pasteHTML(\n              '\\n<p class=\"p1\">line 1<br>\\nline 2</p>\\n<p class=\"p2\"><br></p>\\n<p class=\"p1\">paragraph 1</p>\\n<p class=\"p1\">paragraph 2</p>\\n',\n            ),\n          ],\n          name: 'two Apple Notes lines + two paragraphs separated by an empty paragraph',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><span data-lexical-text=\"true\">line 1</span><br><span data-lexical-text=\"true\">line 2</span></p><p class=\"editor-paragraph\"><br></p><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">paragraph 1</span></p><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">paragraph 2</span></p>',\n          inputs: [\n            pasteHTML(\n              '\\n<p class=\"p1\">line 1<br>\\nline 2</p>\\n<p class=\"p2\">\\n<br>\\n</p>\\n<p class=\"p1\">paragraph 1</p>\\n<p class=\"p1\">paragraph 2</p>\\n',\n            ),\n          ],\n          name: 'two lines + two paragraphs separated by an empty paragraph (2)',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><span data-lexical-text=\"true\">line 1</span><br><span data-lexical-text=\"true\">line 2</span></p>',\n          inputs: [\n            pasteHTML(\n              '<p class=\"p1\"><span>line 1</span><span><br></span><span>line 2</span></p>',\n            ),\n          ],\n          name: 'two lines and br in spans',\n        },\n        {\n          expectedHTML:\n            '<ol class=\"editor-list-ol\"><li value=\"1\"><span data-lexical-text=\"true\">1</span><br><span data-lexical-text=\"true\">2</span></li><li value=\"2\"><br></li><li value=\"3\"><span data-lexical-text=\"true\">3</span></li></ol>',\n          inputs: [\n            pasteHTML('<ol><li>1<div></div>2</li><li></li><li>3</li></ol>'),\n          ],\n          name: 'empty block node in li behaves like a line break',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><span data-lexical-text=\"true\">1</span><br><span data-lexical-text=\"true\">2</span></p>',\n          inputs: [pasteHTML('<div>1<div></div>2</div>')],\n          name: 'empty block node in div behaves like a line break',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><span data-lexical-text=\"true\">12</span></p>',\n          inputs: [pasteHTML('<div>1<text></text>2</div>')],\n          name: 'empty inline node does not behave like a line break',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><span data-lexical-text=\"true\">1</span></p><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">2</span></p>',\n          inputs: [pasteHTML('<div><div>1</div><div></div><div>2</div></div>')],\n          name: 'empty block node between non inline siblings does not behave like a line break',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><span data-lexical-text=\"true\">a</span></p><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">b b</span></p><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">c</span></p><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">z</span></p><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">d e</span></p><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">fg</span></p>',\n          inputs: [\n            pasteHTML(\n              `<div>a<div>b b<div>c<div><div></div>z</div></div>d e</div>fg</div>`,\n            ),\n          ],\n          name: 'nested divs',\n        },\n        {\n          expectedHTML:\n            '<ol class=\"editor-list-ol\"><li value=\"1\"><span data-lexical-text=\"true\">1</span></li><li value=\"2\"><br></li><li value=\"3\"><span data-lexical-text=\"true\">3</span></li></ol>',\n          inputs: [pasteHTML('<ol><li>1</li><li><br /></li><li>3</li></ol>')],\n          name: 'only br in a li',\n        },\n        {\n          expectedHTML:\n            '<p class=\"editor-paragraph\"><span data-lexical-text=\"true\">1</span></p><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">2</span></p><p class=\"editor-paragraph\"><span data-lexical-text=\"true\">3</span></p>',\n          inputs: [pasteHTML('1<p>2<br /></p>3')],\n          name: 'last br in a block node is ignored',\n        },\n      ];\n\n      suite.forEach((testUnit, i) => {\n        const name = testUnit.name || 'Test case';\n\n        // eslint-disable-next-line no-only-tests/no-only-tests, dot-notation\n        const test_ = 'only' in testUnit && testUnit['only'] ? test.only : test;\n        test_(name + ` (#${i + 1})`, async () => {\n          await applySelectionInputs(testUnit.inputs, update, editor!);\n\n          // Validate HTML matches\n          expect((container!.firstChild as HTMLElement).innerHTML).toBe(\n            testUnit.expectedHTML,\n          );\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/utils/__tests__/unit/LexicalNodeHelpers.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {\n  $createParagraphNode,\n  $createTextNode,\n  $getNodeByKey,\n  $getRoot,\n  $isElementNode,\n  LexicalEditor,\n  NodeKey,\n} from 'lexical';\nimport {\n  $createTestElementNode,\n  initializeUnitTest,\n  invariant,\n} from 'lexical/__tests__/utils';\n\nimport {$dfs} from '../..';\n\ndescribe('LexicalNodeHelpers tests', () => {\n  initializeUnitTest((testEnv) => {\n    /**\n     *               R\n     *        P1            P2\n     *     B1     B2     T4 T5 B3\n     *     T1   T2 T3          T6\n     *\n     *  DFS: R, P1, B1, T1, B2, T2, T3, P2, T4, T5, B3, T6\n     */\n    test('DFS node order', async () => {\n      const editor: LexicalEditor = testEnv.editor;\n\n      let expectedKeys: Array<{\n        depth: number;\n        node: NodeKey;\n      }> = [];\n\n      await editor.update(() => {\n        const root = $getRoot();\n\n        const paragraph1 = $createParagraphNode();\n        const paragraph2 = $createParagraphNode();\n\n        const block1 = $createTestElementNode();\n        const block2 = $createTestElementNode();\n        const block3 = $createTestElementNode();\n\n        const text1 = $createTextNode('text1');\n        const text2 = $createTextNode('text2');\n        const text3 = $createTextNode('text3');\n        const text4 = $createTextNode('text4');\n        const text5 = $createTextNode('text5');\n        const text6 = $createTextNode('text6');\n\n        root.append(paragraph1, paragraph2);\n        paragraph1.append(block1, block2);\n        paragraph2.append(text4, text5);\n\n        text5.toggleFormat('bold'); // Prevent from merging with text 4\n\n        paragraph2.append(block3);\n        block1.append(text1);\n        block2.append(text2, text3);\n\n        text3.toggleFormat('bold'); // Prevent from merging with text2\n\n        block3.append(text6);\n\n        expectedKeys = [\n          {\n            depth: 0,\n            node: root.getKey(),\n          },\n          {\n            depth: 1,\n            node: paragraph1.getKey(),\n          },\n          {\n            depth: 2,\n            node: block1.getKey(),\n          },\n          {\n            depth: 3,\n            node: text1.getKey(),\n          },\n          {\n            depth: 2,\n            node: block2.getKey(),\n          },\n          {\n            depth: 3,\n            node: text2.getKey(),\n          },\n          {\n            depth: 3,\n            node: text3.getKey(),\n          },\n          {\n            depth: 1,\n            node: paragraph2.getKey(),\n          },\n          {\n            depth: 2,\n            node: text4.getKey(),\n          },\n          {\n            depth: 2,\n            node: text5.getKey(),\n          },\n          {\n            depth: 2,\n            node: block3.getKey(),\n          },\n          {\n            depth: 3,\n            node: text6.getKey(),\n          },\n        ];\n      });\n\n      editor.getEditorState().read(() => {\n        const expectedNodes = expectedKeys.map(({depth, node: nodeKey}) => ({\n          depth,\n          node: $getNodeByKey(nodeKey)!.getLatest(),\n        }));\n\n        const first = expectedNodes[0];\n        const second = expectedNodes[1];\n        const last = expectedNodes[expectedNodes.length - 1];\n        const secondToLast = expectedNodes[expectedNodes.length - 2];\n\n        expect($dfs(first.node, last.node)).toEqual(expectedNodes);\n        expect($dfs(second.node, secondToLast.node)).toEqual(\n          expectedNodes.slice(1, expectedNodes.length - 1),\n        );\n        expect($dfs()).toEqual(expectedNodes);\n        expect($dfs($getRoot())).toEqual(expectedNodes);\n      });\n    });\n\n    test('DFS triggers getLatest()', async () => {\n      const editor: LexicalEditor = testEnv.editor;\n\n      let rootKey: string;\n      let paragraphKey: string;\n      let block1Key: string;\n      let block2Key: string;\n\n      await editor.update(() => {\n        const root = $getRoot();\n\n        const paragraph = $createParagraphNode();\n        const block1 = $createTestElementNode();\n        const block2 = $createTestElementNode();\n\n        rootKey = root.getKey();\n        paragraphKey = paragraph.getKey();\n        block1Key = block1.getKey();\n        block2Key = block2.getKey();\n\n        root.append(paragraph);\n        paragraph.append(block1, block2);\n      });\n\n      await editor.update(() => {\n        const root = $getNodeByKey(rootKey);\n        const paragraph = $getNodeByKey(paragraphKey);\n        const block1 = $getNodeByKey(block1Key);\n        const block2 = $getNodeByKey(block2Key);\n\n        const block3 = $createTestElementNode();\n        invariant($isElementNode(block1));\n\n        block1.append(block3);\n\n        expect($dfs(root!)).toEqual([\n          {\n            depth: 0,\n            node: root!.getLatest(),\n          },\n          {\n            depth: 1,\n            node: paragraph!.getLatest(),\n          },\n          {\n            depth: 2,\n            node: block1.getLatest(),\n          },\n          {\n            depth: 3,\n            node: block3.getLatest(),\n          },\n          {\n            depth: 2,\n            node: block2!.getLatest(),\n          },\n        ]);\n      });\n    });\n\n    test('DFS of empty ParagraphNode returns only itself', async () => {\n      const editor: LexicalEditor = testEnv.editor;\n\n      let paragraphKey: string;\n\n      await editor.update(() => {\n        const root = $getRoot();\n\n        const paragraph = $createParagraphNode();\n        const paragraph2 = $createParagraphNode();\n        const text = $createTextNode('test');\n\n        paragraphKey = paragraph.getKey();\n\n        paragraph2.append(text);\n        root.append(paragraph, paragraph2);\n      });\n      await editor.update(() => {\n        const paragraph = $getNodeByKey(paragraphKey)!;\n\n        expect($dfs(paragraph ?? undefined)).toEqual([\n          {\n            depth: 1,\n            node: paragraph?.getLatest(),\n          },\n        ]);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/utils/__tests__/unit/LexicalRootHelpers.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {$createParagraphNode, $createTextNode, $getRoot} from 'lexical';\nimport {initializeUnitTest} from 'lexical/__tests__/utils';\n\nexport function $rootTextContent(): string {\n  const root = $getRoot();\n\n  return root.getTextContent();\n}\n\nexport function $isRootTextContentEmpty(\n    isEditorComposing: boolean,\n    trim = true,\n): boolean {\n  if (isEditorComposing) {\n    return false;\n  }\n\n  let text = $rootTextContent();\n\n  if (trim) {\n    text = text.trim();\n  }\n\n  return text === '';\n}\n\nexport function $isRootTextContentEmptyCurry(\n    isEditorComposing: boolean,\n    trim?: boolean,\n): () => boolean {\n  return () => $isRootTextContentEmpty(isEditorComposing, trim);\n}\n\ndescribe('LexicalRootHelpers tests', () => {\n  initializeUnitTest((testEnv) => {\n    it('textContent', async () => {\n      const editor = testEnv.editor;\n\n      expect(editor.getEditorState().read($rootTextContent)).toBe('');\n\n      await editor.update(() => {\n        const root = $getRoot();\n        const paragraph = $createParagraphNode();\n        const text = $createTextNode('foo');\n        root.append(paragraph);\n        paragraph.append(text);\n\n        expect($rootTextContent()).toBe('foo');\n      });\n\n      expect(editor.getEditorState().read($rootTextContent)).toBe('foo');\n    });\n\n    it('isBlank', async () => {\n      const editor = testEnv.editor;\n\n      expect(\n        editor\n          .getEditorState()\n          .read($isRootTextContentEmptyCurry(editor.isComposing())),\n      ).toBe(true);\n\n      await editor.update(() => {\n        const root = $getRoot();\n        const paragraph = $createParagraphNode();\n        const text = $createTextNode('foo');\n        root.append(paragraph);\n        paragraph.append(text);\n\n        expect($isRootTextContentEmpty(editor.isComposing())).toBe(false);\n      });\n\n      expect(\n        editor\n          .getEditorState()\n          .read($isRootTextContentEmptyCurry(editor.isComposing())),\n      ).toBe(false);\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/utils/__tests__/unit/LexicalUtilsKlassEqual.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {objectKlassEquals} from '@lexical/utils';\nimport {initializeUnitTest} from 'lexical/__tests__/utils';\n\nclass MyEvent extends Event {}\n\nclass MyEvent2 extends Event {}\n\nlet MyEventShadow: typeof Event = MyEvent;\n\n{\n  // eslint-disable-next-line no-shadow\n  class MyEvent extends Event {}\n  MyEventShadow = MyEvent;\n}\n\ndescribe('LexicalUtilsKlassEqual tests', () => {\n  initializeUnitTest((testEnv) => {\n    it('objectKlassEquals', async () => {\n      const eventInstance = new MyEvent('');\n      expect(eventInstance instanceof MyEvent).toBeTruthy();\n      expect(objectKlassEquals(eventInstance, MyEvent)).toBeTruthy();\n      expect(eventInstance instanceof MyEvent2).toBeFalsy();\n      expect(objectKlassEquals(eventInstance, MyEvent2)).toBeFalsy();\n      expect(eventInstance instanceof MyEventShadow).toBeFalsy();\n      expect(objectKlassEquals(eventInstance, MyEventShadow)).toBeTruthy();\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/utils/__tests__/unit/LexicalUtilsSplitNode.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {ElementNode, LexicalEditor} from 'lexical';\n\nimport {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html';\nimport {$getRoot, $isElementNode} from 'lexical';\nimport {createTestEditor} from 'lexical/__tests__/utils';\n\nimport {$splitNode} from '../../index';\n\ndescribe('LexicalUtils#splitNode', () => {\n  let editor: LexicalEditor;\n\n  const update = async (updateFn: () => void) => {\n    editor.update(updateFn);\n    await Promise.resolve();\n  };\n\n  beforeEach(async () => {\n    editor = createTestEditor();\n    editor._headless = true;\n  });\n\n  const testCases: Array<{\n    _: string;\n    expectedHtml: string;\n    initialHtml: string;\n    splitPath: Array<number>;\n    splitOffset: number;\n    only?: boolean;\n  }> = [\n    {\n      _: 'split paragraph in between two text nodes',\n      expectedHtml:\n        '<p>Hello</p>\\n<p>world</p>',\n      initialHtml: '<p><span>Hello</span><span>world</span></p>',\n      splitOffset: 1,\n      splitPath: [0],\n    },\n    {\n      _: 'split paragraph before the first text node',\n      expectedHtml:\n        '<p><br></p>\\n<p>Helloworld</p>',\n      initialHtml: '<p><span>Hello</span><span>world</span></p>',\n      splitOffset: 0,\n      splitPath: [0],\n    },\n    {\n      _: 'split paragraph after the last text node',\n      expectedHtml:\n        '<p>Helloworld</p>\\n<p><br></p>',\n      initialHtml: '<p><span>Hello</span><span>world</span></p>',\n      splitOffset: 2, // Any offset that is higher than children size\n      splitPath: [0],\n    },\n    {\n      _: 'split list items between two text nodes',\n      expectedHtml:\n        '<ul><li>Hello</li></ul>\\n' +\n        '<ul><li>world</li></ul>',\n      initialHtml: '<ul><li><span>Hello</span><span>world</span></li></ul>',\n      splitOffset: 1, // Any offset that is higher than children size\n      splitPath: [0, 0],\n    },\n    {\n      _: 'split list items before the first text node',\n      expectedHtml:\n        '<ul><li></li></ul>\\n' +\n        '<ul><li>Helloworld</li></ul>',\n      initialHtml: '<ul><li><span>Hello</span><span>world</span></li></ul>',\n      splitOffset: 0, // Any offset that is higher than children size\n      splitPath: [0, 0],\n    },\n    {\n      _: 'split nested list items',\n      expectedHtml:\n        '<ul>' +\n        '<li>Before</li>' +\n        '<li style=\"list-style: none;\"><ul><li>Hello</li></ul></li>' +\n        '</ul>\\n' +\n        '<ul>' +\n        '<li style=\"list-style: none;\"><ul><li>world</li></ul></li>' +\n        '<li>After</li>' +\n        '</ul>',\n      initialHtml:\n        '<ul>' +\n        '<li><span>Before</span></li>' +\n        '<ul><li><span>Hello</span><span>world</span></li></ul>' +\n        '<li><span>After</span></li>' +\n        '</ul>',\n      splitOffset: 1, // Any offset that is higher than children size\n      splitPath: [0, 1, 0, 0],\n    },\n  ];\n\n  for (const testCase of testCases) {\n    it(testCase._, async () => {\n      await update(() => {\n        // Running init, update, assert in the same update loop\n        // to skip text nodes normalization (then separate text\n        // nodes will still be separate and represented by its own\n        // spans in html output) and make assertions more precise\n        const parser = new DOMParser();\n        const dom = parser.parseFromString(testCase.initialHtml, 'text/html');\n        const nodesToInsert = $generateNodesFromDOM(editor, dom);\n        $getRoot()\n          .clear()\n          .append(...nodesToInsert);\n\n        let nodeToSplit: ElementNode = $getRoot();\n        for (const index of testCase.splitPath) {\n          nodeToSplit = nodeToSplit.getChildAtIndex(index)!;\n          if (!$isElementNode(nodeToSplit)) {\n            throw new Error('Expected node to be element');\n          }\n        }\n\n        $splitNode(nodeToSplit, testCase.splitOffset);\n\n        // Cleaning up list value attributes as it's not really needed in this test\n        // and it clutters expected output\n        const actualHtml = $generateHtmlFromNodes(editor).replace(\n          /\\svalue=\"\\d{1,}\"/g,\n          '',\n        );\n        expect(actualHtml).toEqual(testCase.expectedHtml);\n      });\n    });\n  }\n\n  it('throws when splitting root', async () => {\n    await update(() => {\n      expect(() => $splitNode($getRoot(), 0)).toThrow();\n    });\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/utils/__tests__/unit/LexlcaiUtilsInsertNodeToNearestRoot.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {LexicalEditor, LexicalNode} from 'lexical';\n\nimport {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html';\nimport {\n  $createRangeSelection,\n  $getRoot,\n  $isElementNode,\n  $setSelection,\n} from 'lexical';\nimport {\n  $createTestDecoratorNode,\n  createTestEditor,\n} from 'lexical/__tests__/utils';\n\nimport {$insertNodeToNearestRoot} from '../..';\n\ndescribe('LexicalUtils#insertNodeToNearestRoot', () => {\n  let editor: LexicalEditor;\n\n  const update = async (updateFn: () => void) => {\n    editor.update(updateFn);\n    await Promise.resolve();\n  };\n\n  beforeEach(async () => {\n    editor = createTestEditor();\n    editor._headless = true;\n  });\n\n  const testCases: Array<{\n    _: string;\n    expectedHtml: string;\n    initialHtml: string;\n    selectionPath: Array<number>;\n    selectionOffset: number;\n    only?: boolean;\n  }> = [\n    {\n      _: 'insert into paragraph in between two text nodes',\n      expectedHtml:\n        '<p>Hello</p>\\n<test-decorator></test-decorator>\\n<p>world</p>',\n      initialHtml: '<p><span>Helloworld</span></p>',\n      selectionOffset: 5, // Selection on text node after \"Hello\" world\n      selectionPath: [0, 0],\n    },\n    {\n      _: 'insert into nested list items',\n      expectedHtml:\n        '<ul>' +\n        '<li>Before</li>' +\n        '<li style=\"list-style: none;\"><ul><li>Hello</li></ul></li>' +\n        '</ul>\\n' +\n        '<test-decorator></test-decorator>\\n' +\n        '<ul>' +\n        '<li style=\"list-style: none;\"><ul><li>world</li></ul></li>' +\n        '<li>After</li>' +\n        '</ul>',\n      initialHtml:\n        '<ul>' +\n        '<li><span>Before</span></li>' +\n        '<ul><li><span>Helloworld</span></li></ul>' +\n        '<li><span>After</span></li>' +\n        '</ul>',\n      selectionOffset: 5, // Selection on text node after \"Hello\" world\n      selectionPath: [0, 1, 0, 0, 0],\n    },\n    {\n      _: 'insert into empty paragraph',\n      expectedHtml: '<p><br></p>\\n<test-decorator></test-decorator>\\n<p><br></p>',\n      initialHtml: '<p></p>',\n      selectionOffset: 0, // Selection on text node after \"Hello\" world\n      selectionPath: [0],\n    },\n    {\n      _: 'insert in the end of paragraph',\n      expectedHtml:\n        '<p>Hello world</p>\\n' +\n        '<test-decorator></test-decorator>\\n' +\n        '<p><br></p>',\n      initialHtml: '<p>Hello world</p>',\n      selectionOffset: 12, // Selection on text node after \"Hello\" world\n      selectionPath: [0, 0],\n    },\n    {\n      _: 'insert in the beginning of paragraph',\n      expectedHtml:\n        '<p><br></p>\\n' +\n        '<test-decorator></test-decorator>\\n' +\n        '<p>Hello world</p>',\n      initialHtml: '<p>Hello world</p>',\n      selectionOffset: 0, // Selection on text node after \"Hello\" world\n      selectionPath: [0, 0],\n    },\n    {\n      _: 'insert with selection on root start',\n      expectedHtml:\n        '<test-decorator></test-decorator>\\n' +\n        '<test-decorator></test-decorator>\\n' +\n        '<p>Before</p>\\n' +\n        '<p>After</p>',\n      initialHtml:\n        '<test-decorator></test-decorator>' +\n        '<p><span>Before</span></p>' +\n        '<p><span>After</span></p>',\n      selectionOffset: 0,\n      selectionPath: [],\n    },\n    {\n      _: 'insert with selection on root child',\n      expectedHtml:\n        '<p>Before</p>\\n' +\n        '<test-decorator></test-decorator>\\n' +\n        '<p>After</p>',\n      initialHtml: '<p>Before</p><p>After</p>',\n      selectionOffset: 1,\n      selectionPath: [],\n    },\n    {\n      _: 'insert with selection on root end',\n      expectedHtml:\n        '<p>Before</p>\\n' +\n        '<test-decorator></test-decorator>',\n      initialHtml: '<p>Before</p>',\n      selectionOffset: 1,\n      selectionPath: [],\n    },\n  ];\n\n  for (const testCase of testCases) {\n    it(testCase._, async () => {\n      await update(() => {\n        // Running init, update, assert in the same update loop\n        // to skip text nodes normalization (then separate text\n        // nodes will still be separate and represented by its own\n        // spans in html output) and make assertions more precise\n        const parser = new DOMParser();\n        const dom = parser.parseFromString(testCase.initialHtml, 'text/html');\n        const nodesToInsert = $generateNodesFromDOM(editor, dom);\n        $getRoot()\n          .clear()\n          .append(...nodesToInsert);\n\n        let selectionNode: LexicalNode = $getRoot();\n        for (const index of testCase.selectionPath) {\n          if (!$isElementNode(selectionNode)) {\n            throw new Error(\n              'Expected node to be element (to traverse the tree)',\n            );\n          }\n          selectionNode = selectionNode.getChildAtIndex(index)!;\n        }\n\n        // Calling selectionNode.select() would \"normalize\" selection and move it\n        // to text node (if available), while for the purpose of the test we'd want\n        // to use whatever was passed (e.g. keep selection on root node)\n        const selection = $createRangeSelection();\n        const type = $isElementNode(selectionNode) ? 'element' : 'text';\n        selection.anchor.key = selection.focus.key = selectionNode.getKey();\n        selection.anchor.offset = selection.focus.offset =\n          testCase.selectionOffset;\n        selection.anchor.type = selection.focus.type = type;\n        $setSelection(selection);\n\n        $insertNodeToNearestRoot($createTestDecoratorNode());\n\n        // Cleaning up list value attributes as it's not really needed in this test\n        // and it clutters expected output\n        const actualHtml = $generateHtmlFromNodes(editor).replace(\n          /\\svalue=\"\\d{1,}\"/g,\n          '',\n        );\n        expect(actualHtml).toEqual(testCase.expectedHtml);\n      });\n    });\n  }\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/utils/__tests__/unit/mergeRegister.test.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\nimport {mergeRegister} from '@lexical/utils';\n\ndescribe('mergeRegister', () => {\n  it('calls all of the clean-up functions', () => {\n    const cleanup = jest.fn();\n    mergeRegister(cleanup, cleanup)();\n    expect(cleanup).toHaveBeenCalledTimes(2);\n  });\n  it('calls the clean-up functions in reverse order', () => {\n    const cleanup = jest.fn();\n    mergeRegister(cleanup.bind(null, 1), cleanup.bind(null, 2))();\n    expect(cleanup.mock.calls.map(([v]) => v)).toEqual([2, 1]);\n  });\n});\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/utils/index.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {\n  $cloneWithProperties,\n  $createParagraphNode,\n  $getPreviousSelection,\n  $getRoot,\n  $getSelection,\n  $isElementNode,\n  $isRangeSelection,\n  $isRootOrShadowRoot,\n  $isTextNode,\n  $setSelection,\n  $splitNode,\n  EditorState,\n  ElementNode,\n  Klass,\n  LexicalEditor,\n  LexicalNode,\n} from 'lexical';\n// This underscore postfixing is used as a hotfix so we do not\n// export shared types from this module #5918\nimport {CAN_USE_DOM as CAN_USE_DOM_} from 'lexical/shared/canUseDOM';\nimport {\n  CAN_USE_BEFORE_INPUT as CAN_USE_BEFORE_INPUT_,\n  IS_ANDROID as IS_ANDROID_,\n  IS_ANDROID_CHROME as IS_ANDROID_CHROME_,\n  IS_APPLE as IS_APPLE_,\n  IS_APPLE_WEBKIT as IS_APPLE_WEBKIT_,\n  IS_CHROME as IS_CHROME_,\n  IS_FIREFOX as IS_FIREFOX_,\n  IS_IOS as IS_IOS_,\n  IS_SAFARI as IS_SAFARI_,\n} from 'lexical/shared/environment';\nimport invariant from 'lexical/shared/invariant';\nimport normalizeClassNames from 'lexical/shared/normalizeClassNames';\n\nexport {default as markSelection} from './markSelection';\nexport {default as mergeRegister} from './mergeRegister';\nexport {default as positionNodeOnRange} from './positionNodeOnRange';\nexport {\n  $splitNode,\n  isBlockDomNode,\n  isHTMLAnchorElement,\n  isHTMLElement,\n  isInlineDomNode,\n} from 'lexical';\n// Hotfix to export these with inlined types #5918\nexport const CAN_USE_BEFORE_INPUT: boolean = CAN_USE_BEFORE_INPUT_;\nexport const CAN_USE_DOM: boolean = CAN_USE_DOM_;\nexport const IS_ANDROID: boolean = IS_ANDROID_;\nexport const IS_ANDROID_CHROME: boolean = IS_ANDROID_CHROME_;\nexport const IS_APPLE: boolean = IS_APPLE_;\nexport const IS_APPLE_WEBKIT: boolean = IS_APPLE_WEBKIT_;\nexport const IS_CHROME: boolean = IS_CHROME_;\nexport const IS_FIREFOX: boolean = IS_FIREFOX_;\nexport const IS_IOS: boolean = IS_IOS_;\nexport const IS_SAFARI: boolean = IS_SAFARI_;\n\nexport type DFSNode = Readonly<{\n  depth: number;\n  node: LexicalNode;\n}>;\n\n/**\n * Takes an HTML element and adds the classNames passed within an array,\n * ignoring any non-string types. A space can be used to add multiple classes\n * eg. addClassNamesToElement(element, ['element-inner active', true, null])\n * will add both 'element-inner' and 'active' as classes to that element.\n * @param element - The element in which the classes are added\n * @param classNames - An array defining the class names to add to the element\n */\nexport function addClassNamesToElement(\n  element: HTMLElement,\n  ...classNames: Array<typeof undefined | boolean | null | string>\n): void {\n  const classesToAdd = normalizeClassNames(...classNames);\n  if (classesToAdd.length > 0) {\n    element.classList.add(...classesToAdd);\n  }\n}\n\n/**\n * Takes an HTML element and removes the classNames passed within an array,\n * ignoring any non-string types. A space can be used to remove multiple classes\n * eg. removeClassNamesFromElement(element, ['active small', true, null])\n * will remove both the 'active' and 'small' classes from that element.\n * @param element - The element in which the classes are removed\n * @param classNames - An array defining the class names to remove from the element\n */\nexport function removeClassNamesFromElement(\n  element: HTMLElement,\n  ...classNames: Array<typeof undefined | boolean | null | string>\n): void {\n  const classesToRemove = normalizeClassNames(...classNames);\n  if (classesToRemove.length > 0) {\n    element.classList.remove(...classesToRemove);\n  }\n}\n\n/**\n * Returns true if the file type matches the types passed within the acceptableMimeTypes array, false otherwise.\n * The types passed must be strings and are CASE-SENSITIVE.\n * eg. if file is of type 'text' and acceptableMimeTypes = ['TEXT', 'IMAGE'] the function will return false.\n * @param file - The file you want to type check.\n * @param acceptableMimeTypes - An array of strings of types which the file is checked against.\n * @returns true if the file is an acceptable mime type, false otherwise.\n */\nexport function isMimeType(\n  file: File,\n  acceptableMimeTypes: Array<string>,\n): boolean {\n  for (const acceptableType of acceptableMimeTypes) {\n    if (file.type.startsWith(acceptableType)) {\n      return true;\n    }\n  }\n  return false;\n}\n\n/**\n * Lexical File Reader with:\n *  1. MIME type support\n *  2. batched results (HistoryPlugin compatibility)\n *  3. Order aware (respects the order when multiple Files are passed)\n *\n * const filesResult = await mediaFileReader(files, ['image/']);\n * filesResult.forEach(file => editor.dispatchCommand('INSERT_IMAGE', \\\\{\n *   src: file.result,\n * \\\\}));\n */\nexport function mediaFileReader(\n  files: Array<File>,\n  acceptableMimeTypes: Array<string>,\n): Promise<Array<{file: File; result: string}>> {\n  const filesIterator = files[Symbol.iterator]();\n  return new Promise((resolve, reject) => {\n    const processed: Array<{file: File; result: string}> = [];\n    const handleNextFile = () => {\n      const {done, value: file} = filesIterator.next();\n      if (done) {\n        return resolve(processed);\n      }\n      const fileReader = new FileReader();\n      fileReader.addEventListener('error', reject);\n      fileReader.addEventListener('load', () => {\n        const result = fileReader.result;\n        if (typeof result === 'string') {\n          processed.push({file, result});\n        }\n        handleNextFile();\n      });\n      if (isMimeType(file, acceptableMimeTypes)) {\n        fileReader.readAsDataURL(file);\n      } else {\n        handleNextFile();\n      }\n    };\n    handleNextFile();\n  });\n}\n\n/**\n * \"Depth-First Search\" starts at the root/top node of a tree and goes as far as it can down a branch end\n * before backtracking and finding a new path. Consider solving a maze by hugging either wall, moving down a\n * branch until you hit a dead-end (leaf) and backtracking to find the nearest branching path and repeat.\n * It will then return all the nodes found in the search in an array of objects.\n * @param startingNode - The node to start the search, if ommitted, it will start at the root node.\n * @param endingNode - The node to end the search, if ommitted, it will find all descendants of the startingNode.\n * @returns An array of objects of all the nodes found by the search, including their depth into the tree.\n * \\\\{depth: number, node: LexicalNode\\\\} It will always return at least 1 node (the ending node) so long as it exists\n */\nexport function $dfs(\n  startingNode?: LexicalNode,\n  endingNode?: LexicalNode,\n): Array<DFSNode> {\n  const nodes = [];\n  const start = (startingNode || $getRoot()).getLatest();\n  const end =\n    endingNode ||\n    ($isElementNode(start) ? start.getLastDescendant() || start : start);\n  let node: LexicalNode | null = start;\n  let depth = $getDepth(node);\n\n  while (node !== null && !node.is(end)) {\n    nodes.push({depth, node});\n\n    if ($isElementNode(node) && node.getChildrenSize() > 0) {\n      node = node.getFirstChild();\n      depth++;\n    } else {\n      // Find immediate sibling or nearest parent sibling\n      let sibling = null;\n\n      while (sibling === null && node !== null) {\n        sibling = node.getNextSibling();\n\n        if (sibling === null) {\n          node = node.getParent();\n          depth--;\n        } else {\n          node = sibling;\n        }\n      }\n    }\n  }\n\n  if (node !== null && node.is(end)) {\n    nodes.push({depth, node});\n  }\n\n  return nodes;\n}\n\nfunction $getDepth(node: LexicalNode): number {\n  let innerNode: LexicalNode | null = node;\n  let depth = 0;\n\n  while ((innerNode = innerNode.getParent()) !== null) {\n    depth++;\n  }\n\n  return depth;\n}\n\n/**\n * Performs a right-to-left preorder tree traversal.\n * From the starting node it goes to the rightmost child, than backtracks to paret and finds new rightmost path.\n * It will return the next node in traversal sequence after the startingNode.\n * The traversal is similar to $dfs functions above, but the nodes are visited right-to-left, not left-to-right.\n * @param startingNode - The node to start the search.\n * @returns The next node in pre-order right to left traversal sequence or `null`, if the node does not exist\n */\nexport function $getNextRightPreorderNode(\n  startingNode: LexicalNode,\n): LexicalNode | null {\n  let node: LexicalNode | null = startingNode;\n\n  if ($isElementNode(node) && node.getChildrenSize() > 0) {\n    node = node.getLastChild();\n  } else {\n    let sibling = null;\n\n    while (sibling === null && node !== null) {\n      sibling = node.getPreviousSibling();\n\n      if (sibling === null) {\n        node = node.getParent();\n      } else {\n        node = sibling;\n      }\n    }\n  }\n  return node;\n}\n\n/**\n * Takes a node and traverses up its ancestors (toward the root node)\n * in order to find a specific type of node.\n * @param node - the node to begin searching.\n * @param klass - an instance of the type of node to look for.\n * @returns the node of type klass that was passed, or null if none exist.\n */\nexport function $getNearestNodeOfType<T extends ElementNode>(\n  node: LexicalNode,\n  klass: Klass<T>,\n): T | null {\n  let parent: ElementNode | LexicalNode | null = node;\n\n  while (parent != null) {\n    if (parent instanceof klass) {\n      return parent as T;\n    }\n\n    parent = parent.getParent();\n  }\n\n  return null;\n}\n\n/**\n * Returns the element node of the nearest ancestor, otherwise throws an error.\n * @param startNode - The starting node of the search\n * @returns The ancestor node found\n */\nexport function $getNearestBlockElementAncestorOrThrow(\n  startNode: LexicalNode,\n): ElementNode {\n  const blockNode = $findMatchingParent(\n    startNode,\n    (node) => $isElementNode(node) && !node.isInline(),\n  );\n  if (!$isElementNode(blockNode)) {\n    invariant(\n      false,\n      'Expected node %s to have closest block element node.',\n      startNode.__key,\n    );\n  }\n  return blockNode;\n}\n\nexport type DOMNodeToLexicalConversion = (element: Node) => LexicalNode;\n\nexport type DOMNodeToLexicalConversionMap = Record<\n  string,\n  DOMNodeToLexicalConversion\n>;\n\n/**\n * Starts with a node and moves up the tree (toward the root node) to find a matching node based on\n * the search parameters of the findFn. (Consider JavaScripts' .find() function where a testing function must be\n * passed as an argument. eg. if( (node) => node.__type === 'div') ) return true; otherwise return false\n * @param startingNode - The node where the search starts.\n * @param findFn - A testing function that returns true if the current node satisfies the testing parameters.\n * @returns A parent node that matches the findFn parameters, or null if one wasn't found.\n */\nexport const $findMatchingParent: {\n  <T extends LexicalNode>(\n    startingNode: LexicalNode,\n    findFn: (node: LexicalNode) => node is T,\n  ): T | null;\n  (\n    startingNode: LexicalNode,\n    findFn: (node: LexicalNode) => boolean,\n  ): LexicalNode | null;\n} = (\n  startingNode: LexicalNode,\n  findFn: (node: LexicalNode) => boolean,\n): LexicalNode | null => {\n  let curr: ElementNode | LexicalNode | null = startingNode;\n\n  while (curr !== $getRoot() && curr != null) {\n    if (findFn(curr)) {\n      return curr;\n    }\n\n    curr = curr.getParent();\n  }\n\n  return null;\n};\n\n/**\n * Attempts to resolve nested element nodes of the same type into a single node of that type.\n * It is generally used for marks/commenting\n * @param editor - The lexical editor\n * @param targetNode - The target for the nested element to be extracted from.\n * @param cloneNode - See {@link $createMarkNode}\n * @param handleOverlap - Handles any overlap between the node to extract and the targetNode\n * @returns The lexical editor\n */\nexport function registerNestedElementResolver<N extends ElementNode>(\n  editor: LexicalEditor,\n  targetNode: Klass<N>,\n  cloneNode: (from: N) => N,\n  handleOverlap: (from: N, to: N) => void,\n): () => void {\n  const $isTargetNode = (node: LexicalNode | null | undefined): node is N => {\n    return node instanceof targetNode;\n  };\n\n  const $findMatch = (node: N): {child: ElementNode; parent: N} | null => {\n    // First validate we don't have any children that are of the target,\n    // as we need to handle them first.\n    const children = node.getChildren();\n\n    for (let i = 0; i < children.length; i++) {\n      const child = children[i];\n\n      if ($isTargetNode(child)) {\n        return null;\n      }\n    }\n\n    let parentNode: N | null = node;\n    let childNode = node;\n\n    while (parentNode !== null) {\n      childNode = parentNode;\n      parentNode = parentNode.getParent();\n\n      if ($isTargetNode(parentNode)) {\n        return {child: childNode, parent: parentNode};\n      }\n    }\n\n    return null;\n  };\n\n  const $elementNodeTransform = (node: N) => {\n    const match = $findMatch(node);\n\n    if (match !== null) {\n      const {child, parent} = match;\n\n      // Simple path, we can move child out and siblings into a new parent.\n\n      if (child.is(node)) {\n        handleOverlap(parent, node);\n        const nextSiblings = child.getNextSiblings();\n        const nextSiblingsLength = nextSiblings.length;\n        parent.insertAfter(child);\n\n        if (nextSiblingsLength !== 0) {\n          const newParent = cloneNode(parent);\n          child.insertAfter(newParent);\n\n          for (let i = 0; i < nextSiblingsLength; i++) {\n            newParent.append(nextSiblings[i]);\n          }\n        }\n\n        if (!parent.canBeEmpty() && parent.getChildrenSize() === 0) {\n          parent.remove();\n        }\n      } else {\n        // Complex path, we have a deep node that isn't a child of the\n        // target parent.\n        // TODO: implement this functionality\n      }\n    }\n  };\n\n  return editor.registerNodeTransform(targetNode, $elementNodeTransform);\n}\n\n/**\n * Clones the editor and marks it as dirty to be reconciled. If there was a selection,\n * it would be set back to its previous state, or null otherwise.\n * @param editor - The lexical editor\n * @param editorState - The editor's state\n */\nexport function $restoreEditorState(\n  editor: LexicalEditor,\n  editorState: EditorState,\n): void {\n  const FULL_RECONCILE = 2;\n  const nodeMap = new Map();\n  const activeEditorState = editor._pendingEditorState;\n\n  for (const [key, node] of editorState._nodeMap) {\n    nodeMap.set(key, $cloneWithProperties(node));\n  }\n\n  if (activeEditorState) {\n    activeEditorState._nodeMap = nodeMap;\n  }\n\n  editor._dirtyType = FULL_RECONCILE;\n  const selection = editorState._selection;\n  $setSelection(selection === null ? null : selection.clone());\n}\n\n/**\n * If the selected insertion area is the root/shadow root node (see {@link lexical!$isRootOrShadowRoot}),\n * the node will be appended there, otherwise, it will be inserted before the insertion area.\n * If there is no selection where the node is to be inserted, it will be appended after any current nodes\n * within the tree, as a child of the root node. A paragraph node will then be added after the inserted node and selected.\n * @param node - The node to be inserted\n * @returns The node after its insertion\n */\nexport function $insertNodeToNearestRoot<T extends LexicalNode>(node: T): T {\n  const selection = $getSelection() || $getPreviousSelection();\n\n  if ($isRangeSelection(selection)) {\n    const {focus} = selection;\n    const focusNode = focus.getNode();\n    const focusOffset = focus.offset;\n\n    if ($isRootOrShadowRoot(focusNode)) {\n      const focusChild = focusNode.getChildAtIndex(focusOffset);\n      if (focusChild == null) {\n        focusNode.append(node);\n      } else {\n        focusChild.insertBefore(node);\n      }\n      node.selectNext();\n    } else {\n      let splitNode: ElementNode;\n      let splitOffset: number;\n      if ($isTextNode(focusNode)) {\n        splitNode = focusNode.getParentOrThrow();\n        splitOffset = focusNode.getIndexWithinParent();\n        if (focusOffset > 0) {\n          splitOffset += 1;\n          focusNode.splitText(focusOffset);\n        }\n      } else {\n        splitNode = focusNode;\n        splitOffset = focusOffset;\n      }\n      const [, rightTree] = $splitNode(splitNode, splitOffset);\n      rightTree.insertBefore(node);\n      rightTree.selectStart();\n    }\n  } else {\n    if (selection != null) {\n      const nodes = selection.getNodes();\n      nodes[nodes.length - 1].getTopLevelElementOrThrow().insertAfter(node);\n    } else {\n      const root = $getRoot();\n      root.append(node);\n    }\n    const paragraphNode = $createParagraphNode();\n    node.insertAfter(paragraphNode);\n    paragraphNode.select();\n  }\n  return node.getLatest();\n}\n\n/**\n * Wraps the node into another node created from a createElementNode function, eg. $createParagraphNode\n * @param node - Node to be wrapped.\n * @param createElementNode - Creates a new lexical element to wrap the to-be-wrapped node and returns it.\n * @returns A new lexical element with the previous node appended within (as a child, including its children).\n */\nexport function $wrapNodeInElement(\n  node: LexicalNode,\n  createElementNode: () => ElementNode,\n): ElementNode {\n  const elementNode = createElementNode();\n  node.replace(elementNode);\n  elementNode.append(node);\n  return elementNode;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype ObjectKlass<T> = new (...args: any[]) => T;\n\n/**\n * @param object = The instance of the type\n * @param objectClass = The class of the type\n * @returns Whether the object is has the same Klass of the objectClass, ignoring the difference across window (e.g. different iframs)\n */\nexport function objectKlassEquals<T>(\n  object: unknown,\n  objectClass: ObjectKlass<T>,\n): boolean {\n  return object !== null\n    ? Object.getPrototypeOf(object).constructor.name === objectClass.name\n    : false;\n}\n\n/**\n * Filter the nodes\n * @param nodes Array of nodes that needs to be filtered\n * @param filterFn A filter function that returns node if the current node satisfies the condition otherwise null\n * @returns Array of filtered nodes\n */\n\nexport function $filter<T>(\n  nodes: Array<LexicalNode>,\n  filterFn: (node: LexicalNode) => null | T,\n): Array<T> {\n  const result: T[] = [];\n  for (let i = 0; i < nodes.length; i++) {\n    const node = filterFn(nodes[i]);\n    if (node !== null) {\n      result.push(node);\n    }\n  }\n  return result;\n}\n/**\n * Appends the node before the first child of the parent node\n * @param parent A parent node\n * @param node Node that needs to be appended\n */\nexport function $insertFirst(parent: ElementNode, node: LexicalNode): void {\n  const firstChild = parent.getFirstChild();\n  if (firstChild !== null) {\n    firstChild.insertBefore(node);\n  } else {\n    parent.append(node);\n  }\n}\n\n/**\n * Calculates the zoom level of an element as a result of using\n * css zoom property.\n * @param element\n */\nexport function calculateZoomLevel(element: Element | null): number {\n  if (IS_FIREFOX) {\n    return 1;\n  }\n  let zoom = 1;\n  while (element) {\n    zoom *= Number(window.getComputedStyle(element).getPropertyValue('zoom'));\n    element = element.parentElement;\n  }\n  return zoom;\n}\n\n/**\n * Checks if the editor is a nested editor created by LexicalNestedComposer\n */\nexport function $isEditorIsNestedEditor(editor: LexicalEditor): boolean {\n  return editor._parentEditor !== null;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/utils/markSelection.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {\n  $getSelection,\n  $isRangeSelection,\n  type EditorState,\n  ElementNode,\n  type LexicalEditor,\n  TextNode,\n} from 'lexical';\nimport invariant from 'lexical/shared/invariant';\n\nimport mergeRegister from './mergeRegister';\nimport positionNodeOnRange from './positionNodeOnRange';\nimport px from './px';\n\nexport default function markSelection(\n  editor: LexicalEditor,\n  onReposition?: (node: Array<HTMLElement>) => void,\n): () => void {\n  let previousAnchorNode: null | TextNode | ElementNode = null;\n  let previousAnchorOffset: null | number = null;\n  let previousFocusNode: null | TextNode | ElementNode = null;\n  let previousFocusOffset: null | number = null;\n  let removeRangeListener: () => void = () => {};\n  function compute(editorState: EditorState) {\n    editorState.read(() => {\n      const selection = $getSelection();\n      if (!$isRangeSelection(selection)) {\n        // TODO\n        previousAnchorNode = null;\n        previousAnchorOffset = null;\n        previousFocusNode = null;\n        previousFocusOffset = null;\n        removeRangeListener();\n        removeRangeListener = () => {};\n        return;\n      }\n      const {anchor, focus} = selection;\n      const currentAnchorNode = anchor.getNode();\n      const currentAnchorNodeKey = currentAnchorNode.getKey();\n      const currentAnchorOffset = anchor.offset;\n      const currentFocusNode = focus.getNode();\n      const currentFocusNodeKey = currentFocusNode.getKey();\n      const currentFocusOffset = focus.offset;\n      const currentAnchorNodeDOM = editor.getElementByKey(currentAnchorNodeKey);\n      const currentFocusNodeDOM = editor.getElementByKey(currentFocusNodeKey);\n      const differentAnchorDOM =\n        previousAnchorNode === null ||\n        currentAnchorNodeDOM === null ||\n        currentAnchorOffset !== previousAnchorOffset ||\n        currentAnchorNodeKey !== previousAnchorNode.getKey() ||\n        (currentAnchorNode !== previousAnchorNode &&\n          (!(previousAnchorNode instanceof TextNode) ||\n            currentAnchorNode.updateDOM(\n              previousAnchorNode,\n              currentAnchorNodeDOM,\n              editor._config,\n            )));\n      const differentFocusDOM =\n        previousFocusNode === null ||\n        currentFocusNodeDOM === null ||\n        currentFocusOffset !== previousFocusOffset ||\n        currentFocusNodeKey !== previousFocusNode.getKey() ||\n        (currentFocusNode !== previousFocusNode &&\n          (!(previousFocusNode instanceof TextNode) ||\n            currentFocusNode.updateDOM(\n              previousFocusNode,\n              currentFocusNodeDOM,\n              editor._config,\n            )));\n      if (differentAnchorDOM || differentFocusDOM) {\n        const anchorHTMLElement = editor.getElementByKey(\n          anchor.getNode().getKey(),\n        );\n        const focusHTMLElement = editor.getElementByKey(\n          focus.getNode().getKey(),\n        );\n        // TODO handle selection beyond the common TextNode\n        if (\n          anchorHTMLElement !== null &&\n          focusHTMLElement !== null &&\n          anchorHTMLElement.tagName === 'SPAN' &&\n          focusHTMLElement.tagName === 'SPAN'\n        ) {\n          const range = document.createRange();\n          let firstHTMLElement;\n          let firstOffset;\n          let lastHTMLElement;\n          let lastOffset;\n          if (focus.isBefore(anchor)) {\n            firstHTMLElement = focusHTMLElement;\n            firstOffset = focus.offset;\n            lastHTMLElement = anchorHTMLElement;\n            lastOffset = anchor.offset;\n          } else {\n            firstHTMLElement = anchorHTMLElement;\n            firstOffset = anchor.offset;\n            lastHTMLElement = focusHTMLElement;\n            lastOffset = focus.offset;\n          }\n          const firstTextNode = firstHTMLElement.firstChild;\n          invariant(\n            firstTextNode !== null,\n            'Expected text node to be first child of span',\n          );\n          const lastTextNode = lastHTMLElement.firstChild;\n          invariant(\n            lastTextNode !== null,\n            'Expected text node to be first child of span',\n          );\n          range.setStart(firstTextNode, firstOffset);\n          range.setEnd(lastTextNode, lastOffset);\n          removeRangeListener();\n          removeRangeListener = positionNodeOnRange(\n            editor,\n            range,\n            (domNodes) => {\n              for (const domNode of domNodes) {\n                const domNodeStyle = domNode.style;\n                if (domNodeStyle.background !== 'Highlight') {\n                  domNodeStyle.background = 'Highlight';\n                }\n                if (domNodeStyle.color !== 'HighlightText') {\n                  domNodeStyle.color = 'HighlightText';\n                }\n                if (domNodeStyle.zIndex !== '-1') {\n                  domNodeStyle.zIndex = '-1';\n                }\n                if (domNodeStyle.pointerEvents !== 'none') {\n                  domNodeStyle.pointerEvents = 'none';\n                }\n                if (domNodeStyle.marginTop !== px(-1.5)) {\n                  domNodeStyle.marginTop = px(-1.5);\n                }\n                if (domNodeStyle.paddingTop !== px(4)) {\n                  domNodeStyle.paddingTop = px(4);\n                }\n                if (domNodeStyle.paddingBottom !== px(0)) {\n                  domNodeStyle.paddingBottom = px(0);\n                }\n              }\n              if (onReposition !== undefined) {\n                onReposition(domNodes);\n              }\n            },\n          );\n        }\n      }\n      previousAnchorNode = currentAnchorNode;\n      previousAnchorOffset = currentAnchorOffset;\n      previousFocusNode = currentFocusNode;\n      previousFocusOffset = currentFocusOffset;\n    });\n  }\n  compute(editor.getEditorState());\n  return mergeRegister(\n    editor.registerUpdateListener(({editorState}) => compute(editorState)),\n    removeRangeListener,\n    () => {\n      removeRangeListener();\n    },\n  );\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/utils/mergeRegister.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\ntype Func = () => void;\n\n/**\n * Returns a function that will execute all functions passed when called. It is generally used\n * to register multiple lexical listeners and then tear them down with a single function call, such\n * as React's useEffect hook.\n * @example\n * ```ts\n * useEffect(() => {\n *   return mergeRegister(\n *     editor.registerCommand(...registerCommand1 logic),\n *     editor.registerCommand(...registerCommand2 logic),\n *     editor.registerCommand(...registerCommand3 logic)\n *   )\n * }, [editor])\n * ```\n * In this case, useEffect is returning the function returned by mergeRegister as a cleanup\n * function to be executed after either the useEffect runs again (due to one of its dependencies\n * updating) or the component it resides in unmounts.\n * Note the functions don't neccesarily need to be in an array as all arguments\n * are considered to be the func argument and spread from there.\n * The order of cleanup is the reverse of the argument order. Generally it is\n * expected that the first \"acquire\" will be \"released\" last (LIFO order),\n * because a later step may have some dependency on an earlier one.\n * @param func - An array of cleanup functions meant to be executed by the returned function.\n * @returns the function which executes all the passed cleanup functions.\n */\nexport default function mergeRegister(...func: Array<Func>): () => void {\n  return () => {\n    for (let i = func.length - 1; i >= 0; i--) {\n      func[i]();\n    }\n    // Clean up the references and make future calls a no-op\n    func.length = 0;\n  };\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/utils/positionNodeOnRange.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {LexicalEditor} from 'lexical';\n\nimport {createRectsFromDOMRange} from '@lexical/selection';\nimport invariant from 'lexical/shared/invariant';\n\nimport px from './px';\n\nconst mutationObserverConfig = {\n  attributes: true,\n  characterData: true,\n  childList: true,\n  subtree: true,\n};\n\nexport default function positionNodeOnRange(\n  editor: LexicalEditor,\n  range: Range,\n  onReposition: (node: Array<HTMLElement>) => void,\n): () => void {\n  let rootDOMNode: null | HTMLElement = null;\n  let parentDOMNode: null | HTMLElement = null;\n  let observer: null | MutationObserver = null;\n  let lastNodes: Array<HTMLElement> = [];\n  const wrapperNode = document.createElement('div');\n\n  function position(): void {\n    invariant(rootDOMNode !== null, 'Unexpected null rootDOMNode');\n    invariant(parentDOMNode !== null, 'Unexpected null parentDOMNode');\n    const {left: rootLeft, top: rootTop} = rootDOMNode.getBoundingClientRect();\n    const parentDOMNode_ = parentDOMNode;\n    const rects = createRectsFromDOMRange(editor, range);\n    if (!wrapperNode.isConnected) {\n      parentDOMNode_.append(wrapperNode);\n    }\n    let hasRepositioned = false;\n    for (let i = 0; i < rects.length; i++) {\n      const rect = rects[i];\n      // Try to reuse the previously created Node when possible, no need to\n      // remove/create on the most common case reposition case\n      const rectNode = lastNodes[i] || document.createElement('div');\n      const rectNodeStyle = rectNode.style;\n      if (rectNodeStyle.position !== 'absolute') {\n        rectNodeStyle.position = 'absolute';\n        hasRepositioned = true;\n      }\n      const left = px(rect.left - rootLeft);\n      if (rectNodeStyle.left !== left) {\n        rectNodeStyle.left = left;\n        hasRepositioned = true;\n      }\n      const top = px(rect.top - rootTop);\n      if (rectNodeStyle.top !== top) {\n        rectNode.style.top = top;\n        hasRepositioned = true;\n      }\n      const width = px(rect.width);\n      if (rectNodeStyle.width !== width) {\n        rectNode.style.width = width;\n        hasRepositioned = true;\n      }\n      const height = px(rect.height);\n      if (rectNodeStyle.height !== height) {\n        rectNode.style.height = height;\n        hasRepositioned = true;\n      }\n      if (rectNode.parentNode !== wrapperNode) {\n        wrapperNode.append(rectNode);\n        hasRepositioned = true;\n      }\n      lastNodes[i] = rectNode;\n    }\n    while (lastNodes.length > rects.length) {\n      lastNodes.pop();\n    }\n    if (hasRepositioned) {\n      onReposition(lastNodes);\n    }\n  }\n\n  function stop(): void {\n    parentDOMNode = null;\n    rootDOMNode = null;\n    if (observer !== null) {\n      observer.disconnect();\n    }\n    observer = null;\n    wrapperNode.remove();\n    for (const node of lastNodes) {\n      node.remove();\n    }\n    lastNodes = [];\n  }\n\n  function restart(): void {\n    const currentRootDOMNode = editor.getRootElement();\n    if (currentRootDOMNode === null) {\n      return stop();\n    }\n    const currentParentDOMNode = currentRootDOMNode.parentElement;\n    if (!(currentParentDOMNode instanceof HTMLElement)) {\n      return stop();\n    }\n    stop();\n    rootDOMNode = currentRootDOMNode;\n    parentDOMNode = currentParentDOMNode;\n    observer = new MutationObserver((mutations) => {\n      const nextRootDOMNode = editor.getRootElement();\n      const nextParentDOMNode =\n        nextRootDOMNode && nextRootDOMNode.parentElement;\n      if (\n        nextRootDOMNode !== rootDOMNode ||\n        nextParentDOMNode !== parentDOMNode\n      ) {\n        return restart();\n      }\n      for (const mutation of mutations) {\n        if (!wrapperNode.contains(mutation.target)) {\n          // TODO throttle\n          return position();\n        }\n      }\n    });\n    observer.observe(currentParentDOMNode, mutationObserverConfig);\n    position();\n  }\n\n  const removeRootListener = editor.registerRootListener(restart);\n\n  return () => {\n    removeRootListener();\n    stop();\n  };\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/utils/px.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nexport default function px(value: number) {\n  return `${value}px`;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/yjs/Bindings.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {CollabDecoratorNode} from './CollabDecoratorNode';\nimport type {CollabElementNode} from './CollabElementNode';\nimport type {CollabLineBreakNode} from './CollabLineBreakNode';\nimport type {CollabTextNode} from './CollabTextNode';\nimport type {Cursor} from './SyncCursors';\nimport type {LexicalEditor, NodeKey} from 'lexical';\nimport type {Doc} from 'yjs';\n\nimport {Klass, LexicalNode} from 'lexical';\nimport invariant from 'lexical/shared/invariant';\nimport {XmlText} from 'yjs';\n\nimport {Provider} from '.';\nimport {$createCollabElementNode} from './CollabElementNode';\n\nexport type ClientID = number;\nexport type Binding = {\n  clientID: number;\n  collabNodeMap: Map<\n    NodeKey,\n    | CollabElementNode\n    | CollabTextNode\n    | CollabDecoratorNode\n    | CollabLineBreakNode\n  >;\n  cursors: Map<ClientID, Cursor>;\n  cursorsContainer: null | HTMLElement;\n  doc: Doc;\n  docMap: Map<string, Doc>;\n  editor: LexicalEditor;\n  id: string;\n  nodeProperties: Map<string, Array<string>>;\n  root: CollabElementNode;\n  excludedProperties: ExcludedProperties;\n};\nexport type ExcludedProperties = Map<Klass<LexicalNode>, Set<string>>;\n\nexport function createBinding(\n  editor: LexicalEditor,\n  provider: Provider,\n  id: string,\n  doc: Doc | null | undefined,\n  docMap: Map<string, Doc>,\n  excludedProperties?: ExcludedProperties,\n): Binding {\n  invariant(\n    doc !== undefined && doc !== null,\n    'createBinding: doc is null or undefined',\n  );\n  const rootXmlText = doc.get('root', XmlText) as XmlText;\n  const root: CollabElementNode = $createCollabElementNode(\n    rootXmlText,\n    null,\n    'root',\n  );\n  root._key = 'root';\n  return {\n    clientID: doc.clientID,\n    collabNodeMap: new Map(),\n    cursors: new Map(),\n    cursorsContainer: null,\n    doc,\n    docMap,\n    editor,\n    excludedProperties: excludedProperties || new Map(),\n    id,\n    nodeProperties: new Map(),\n    root,\n  };\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/yjs/CollabDecoratorNode.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {Binding} from '.';\nimport type {CollabElementNode} from './CollabElementNode';\nimport type {DecoratorNode, NodeKey, NodeMap} from 'lexical';\nimport type {XmlElement} from 'yjs';\n\nimport {$getNodeByKey, $isDecoratorNode} from 'lexical';\nimport invariant from 'lexical/shared/invariant';\n\nimport {syncPropertiesFromLexical, syncPropertiesFromYjs} from './Utils';\n\nexport class CollabDecoratorNode {\n  _xmlElem: XmlElement;\n  _key: NodeKey;\n  _parent: CollabElementNode;\n  _type: string;\n\n  constructor(xmlElem: XmlElement, parent: CollabElementNode, type: string) {\n    this._key = '';\n    this._xmlElem = xmlElem;\n    this._parent = parent;\n    this._type = type;\n  }\n\n  getPrevNode(nodeMap: null | NodeMap): null | DecoratorNode<unknown> {\n    if (nodeMap === null) {\n      return null;\n    }\n\n    const node = nodeMap.get(this._key);\n    return $isDecoratorNode(node) ? node : null;\n  }\n\n  getNode(): null | DecoratorNode<unknown> {\n    const node = $getNodeByKey(this._key);\n    return $isDecoratorNode(node) ? node : null;\n  }\n\n  getSharedType(): XmlElement {\n    return this._xmlElem;\n  }\n\n  getType(): string {\n    return this._type;\n  }\n\n  getKey(): NodeKey {\n    return this._key;\n  }\n\n  getSize(): number {\n    return 1;\n  }\n\n  getOffset(): number {\n    const collabElementNode = this._parent;\n    return collabElementNode.getChildOffset(this);\n  }\n\n  syncPropertiesFromLexical(\n    binding: Binding,\n    nextLexicalNode: DecoratorNode<unknown>,\n    prevNodeMap: null | NodeMap,\n  ): void {\n    const prevLexicalNode = this.getPrevNode(prevNodeMap);\n    const xmlElem = this._xmlElem;\n\n    syncPropertiesFromLexical(\n      binding,\n      xmlElem,\n      prevLexicalNode,\n      nextLexicalNode,\n    );\n  }\n\n  syncPropertiesFromYjs(\n    binding: Binding,\n    keysChanged: null | Set<string>,\n  ): void {\n    const lexicalNode = this.getNode();\n    invariant(\n      lexicalNode !== null,\n      'syncPropertiesFromYjs: could not find decorator node',\n    );\n    const xmlElem = this._xmlElem;\n    syncPropertiesFromYjs(binding, xmlElem, lexicalNode, keysChanged);\n  }\n\n  destroy(binding: Binding): void {\n    const collabNodeMap = binding.collabNodeMap;\n    collabNodeMap.delete(this._key);\n  }\n}\n\nexport function $createCollabDecoratorNode(\n  xmlElem: XmlElement,\n  parent: CollabElementNode,\n  type: string,\n): CollabDecoratorNode {\n  const collabNode = new CollabDecoratorNode(xmlElem, parent, type);\n  xmlElem._collabNode = collabNode;\n  return collabNode;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/yjs/CollabElementNode.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {Binding} from '.';\nimport type {ElementNode, NodeKey, NodeMap} from 'lexical';\nimport type {AbstractType, Map as YMap, XmlElement, XmlText} from 'yjs';\n\nimport {$createChildrenArray} from '@lexical/offset';\nimport {\n  $getNodeByKey,\n  $isDecoratorNode,\n  $isElementNode,\n  $isTextNode,\n} from 'lexical';\nimport invariant from 'lexical/shared/invariant';\n\nimport {CollabDecoratorNode} from './CollabDecoratorNode';\nimport {CollabLineBreakNode} from './CollabLineBreakNode';\nimport {CollabTextNode} from './CollabTextNode';\nimport {\n  $createCollabNodeFromLexicalNode,\n  $getNodeByKeyOrThrow,\n  $getOrInitCollabNodeFromSharedType,\n  createLexicalNodeFromCollabNode,\n  getPositionFromElementAndOffset,\n  removeFromParent,\n  spliceString,\n  syncPropertiesFromLexical,\n  syncPropertiesFromYjs,\n} from './Utils';\n\ntype IntentionallyMarkedAsDirtyElement = boolean;\n\nexport class CollabElementNode {\n  _key: NodeKey;\n  _children: Array<\n    | CollabElementNode\n    | CollabTextNode\n    | CollabDecoratorNode\n    | CollabLineBreakNode\n  >;\n  _xmlText: XmlText;\n  _type: string;\n  _parent: null | CollabElementNode;\n\n  constructor(\n    xmlText: XmlText,\n    parent: null | CollabElementNode,\n    type: string,\n  ) {\n    this._key = '';\n    this._children = [];\n    this._xmlText = xmlText;\n    this._type = type;\n    this._parent = parent;\n  }\n\n  getPrevNode(nodeMap: null | NodeMap): null | ElementNode {\n    if (nodeMap === null) {\n      return null;\n    }\n\n    const node = nodeMap.get(this._key);\n    return $isElementNode(node) ? node : null;\n  }\n\n  getNode(): null | ElementNode {\n    const node = $getNodeByKey(this._key);\n    return $isElementNode(node) ? node : null;\n  }\n\n  getSharedType(): XmlText {\n    return this._xmlText;\n  }\n\n  getType(): string {\n    return this._type;\n  }\n\n  getKey(): NodeKey {\n    return this._key;\n  }\n\n  isEmpty(): boolean {\n    return this._children.length === 0;\n  }\n\n  getSize(): number {\n    return 1;\n  }\n\n  getOffset(): number {\n    const collabElementNode = this._parent;\n    invariant(\n      collabElementNode !== null,\n      'getOffset: could not find collab element node',\n    );\n\n    return collabElementNode.getChildOffset(this);\n  }\n\n  syncPropertiesFromYjs(\n    binding: Binding,\n    keysChanged: null | Set<string>,\n  ): void {\n    const lexicalNode = this.getNode();\n    invariant(\n      lexicalNode !== null,\n      'syncPropertiesFromYjs: could not find element node',\n    );\n    syncPropertiesFromYjs(binding, this._xmlText, lexicalNode, keysChanged);\n  }\n\n  applyChildrenYjsDelta(\n    binding: Binding,\n    deltas: Array<{\n      insert?: string | object | AbstractType<unknown>;\n      delete?: number;\n      retain?: number;\n      attributes?: {\n        [x: string]: unknown;\n      };\n    }>,\n  ): void {\n    const children = this._children;\n    let currIndex = 0;\n\n    for (let i = 0; i < deltas.length; i++) {\n      const delta = deltas[i];\n      const insertDelta = delta.insert;\n      const deleteDelta = delta.delete;\n\n      if (delta.retain != null) {\n        currIndex += delta.retain;\n      } else if (typeof deleteDelta === 'number') {\n        let deletionSize = deleteDelta;\n\n        while (deletionSize > 0) {\n          const {node, nodeIndex, offset, length} =\n            getPositionFromElementAndOffset(this, currIndex, false);\n\n          if (\n            node instanceof CollabElementNode ||\n            node instanceof CollabLineBreakNode ||\n            node instanceof CollabDecoratorNode\n          ) {\n            children.splice(nodeIndex, 1);\n            deletionSize -= 1;\n          } else if (node instanceof CollabTextNode) {\n            const delCount = Math.min(deletionSize, length);\n            const prevCollabNode =\n              nodeIndex !== 0 ? children[nodeIndex - 1] : null;\n            const nodeSize = node.getSize();\n\n            if (\n              offset === 0 &&\n              delCount === 1 &&\n              nodeIndex > 0 &&\n              prevCollabNode instanceof CollabTextNode &&\n              length === nodeSize &&\n              // If the node has no keys, it's been deleted\n              Array.from(node._map.keys()).length === 0\n            ) {\n              // Merge the text node with previous.\n              prevCollabNode._text += node._text;\n              children.splice(nodeIndex, 1);\n            } else if (offset === 0 && delCount === nodeSize) {\n              // The entire thing needs removing\n              children.splice(nodeIndex, 1);\n            } else {\n              node._text = spliceString(node._text, offset, delCount, '');\n            }\n\n            deletionSize -= delCount;\n          } else {\n            // Can occur due to the deletion from the dangling text heuristic below.\n            break;\n          }\n        }\n      } else if (insertDelta != null) {\n        if (typeof insertDelta === 'string') {\n          const {node, offset} = getPositionFromElementAndOffset(\n            this,\n            currIndex,\n            true,\n          );\n\n          if (node instanceof CollabTextNode) {\n            node._text = spliceString(node._text, offset, 0, insertDelta);\n          } else {\n            // TODO: maybe we can improve this by keeping around a redundant\n            // text node map, rather than removing all the text nodes, so there\n            // never can be dangling text.\n\n            // We have a conflict where there was likely a CollabTextNode and\n            // an Lexical TextNode too, but they were removed in a merge. So\n            // let's just ignore the text and trigger a removal for it from our\n            // shared type.\n            this._xmlText.delete(offset, insertDelta.length);\n          }\n\n          currIndex += insertDelta.length;\n        } else {\n          const sharedType = insertDelta;\n          const {nodeIndex} = getPositionFromElementAndOffset(\n            this,\n            currIndex,\n            false,\n          );\n          const collabNode = $getOrInitCollabNodeFromSharedType(\n            binding,\n            sharedType as XmlText | YMap<unknown> | XmlElement,\n            this,\n          );\n          children.splice(nodeIndex, 0, collabNode);\n          currIndex += 1;\n        }\n      } else {\n        throw new Error('Unexpected delta format');\n      }\n    }\n  }\n\n  syncChildrenFromYjs(binding: Binding): void {\n    // Now diff the children of the collab node with that of our existing Lexical node.\n    const lexicalNode = this.getNode();\n    invariant(\n      lexicalNode !== null,\n      'syncChildrenFromYjs: could not find element node',\n    );\n\n    const key = lexicalNode.__key;\n    const prevLexicalChildrenKeys = $createChildrenArray(lexicalNode, null);\n    const nextLexicalChildrenKeys: Array<NodeKey> = [];\n    const lexicalChildrenKeysLength = prevLexicalChildrenKeys.length;\n    const collabChildren = this._children;\n    const collabChildrenLength = collabChildren.length;\n    const collabNodeMap = binding.collabNodeMap;\n    const visitedKeys = new Set();\n    let collabKeys;\n    let writableLexicalNode;\n    let prevIndex = 0;\n    let prevChildNode = null;\n\n    if (collabChildrenLength !== lexicalChildrenKeysLength) {\n      writableLexicalNode = lexicalNode.getWritable();\n    }\n\n    for (let i = 0; i < collabChildrenLength; i++) {\n      const lexicalChildKey = prevLexicalChildrenKeys[prevIndex];\n      const childCollabNode = collabChildren[i];\n      const collabLexicalChildNode = childCollabNode.getNode();\n      const collabKey = childCollabNode._key;\n\n      if (collabLexicalChildNode !== null && lexicalChildKey === collabKey) {\n        const childNeedsUpdating = $isTextNode(collabLexicalChildNode);\n        // Update\n        visitedKeys.add(lexicalChildKey);\n\n        if (childNeedsUpdating) {\n          childCollabNode._key = lexicalChildKey;\n\n          if (childCollabNode instanceof CollabElementNode) {\n            const xmlText = childCollabNode._xmlText;\n            childCollabNode.syncPropertiesFromYjs(binding, null);\n            childCollabNode.applyChildrenYjsDelta(binding, xmlText.toDelta());\n            childCollabNode.syncChildrenFromYjs(binding);\n          } else if (childCollabNode instanceof CollabTextNode) {\n            childCollabNode.syncPropertiesAndTextFromYjs(binding, null);\n          } else if (childCollabNode instanceof CollabDecoratorNode) {\n            childCollabNode.syncPropertiesFromYjs(binding, null);\n          } else if (!(childCollabNode instanceof CollabLineBreakNode)) {\n            invariant(\n              false,\n              'syncChildrenFromYjs: expected text, element, decorator, or linebreak collab node',\n            );\n          }\n        }\n\n        nextLexicalChildrenKeys[i] = lexicalChildKey;\n        prevChildNode = collabLexicalChildNode;\n        prevIndex++;\n      } else {\n        if (collabKeys === undefined) {\n          collabKeys = new Set();\n\n          for (let s = 0; s < collabChildrenLength; s++) {\n            const child = collabChildren[s];\n            const childKey = child._key;\n\n            if (childKey !== '') {\n              collabKeys.add(childKey);\n            }\n          }\n        }\n\n        if (\n          collabLexicalChildNode !== null &&\n          lexicalChildKey !== undefined &&\n          !collabKeys.has(lexicalChildKey)\n        ) {\n          const nodeToRemove = $getNodeByKeyOrThrow(lexicalChildKey);\n          removeFromParent(nodeToRemove);\n          i--;\n          prevIndex++;\n          continue;\n        }\n\n        writableLexicalNode = lexicalNode.getWritable();\n        // Create/Replace\n        const lexicalChildNode = createLexicalNodeFromCollabNode(\n          binding,\n          childCollabNode,\n          key,\n        );\n        const childKey = lexicalChildNode.__key;\n        collabNodeMap.set(childKey, childCollabNode);\n        nextLexicalChildrenKeys[i] = childKey;\n        if (prevChildNode === null) {\n          const nextSibling = writableLexicalNode.getFirstChild();\n          writableLexicalNode.__first = childKey;\n          if (nextSibling !== null) {\n            const writableNextSibling = nextSibling.getWritable();\n            writableNextSibling.__prev = childKey;\n            lexicalChildNode.__next = writableNextSibling.__key;\n          }\n        } else {\n          const writablePrevChildNode = prevChildNode.getWritable();\n          const nextSibling = prevChildNode.getNextSibling();\n          writablePrevChildNode.__next = childKey;\n          lexicalChildNode.__prev = prevChildNode.__key;\n          if (nextSibling !== null) {\n            const writableNextSibling = nextSibling.getWritable();\n            writableNextSibling.__prev = childKey;\n            lexicalChildNode.__next = writableNextSibling.__key;\n          }\n        }\n        if (i === collabChildrenLength - 1) {\n          writableLexicalNode.__last = childKey;\n        }\n        writableLexicalNode.__size++;\n        prevChildNode = lexicalChildNode;\n      }\n    }\n\n    for (let i = 0; i < lexicalChildrenKeysLength; i++) {\n      const lexicalChildKey = prevLexicalChildrenKeys[i];\n\n      if (!visitedKeys.has(lexicalChildKey)) {\n        // Remove\n        const lexicalChildNode = $getNodeByKeyOrThrow(lexicalChildKey);\n        const collabNode = binding.collabNodeMap.get(lexicalChildKey);\n\n        if (collabNode !== undefined) {\n          collabNode.destroy(binding);\n        }\n        removeFromParent(lexicalChildNode);\n      }\n    }\n  }\n\n  syncPropertiesFromLexical(\n    binding: Binding,\n    nextLexicalNode: ElementNode,\n    prevNodeMap: null | NodeMap,\n  ): void {\n    syncPropertiesFromLexical(\n      binding,\n      this._xmlText,\n      this.getPrevNode(prevNodeMap),\n      nextLexicalNode,\n    );\n  }\n\n  _syncChildFromLexical(\n    binding: Binding,\n    index: number,\n    key: NodeKey,\n    prevNodeMap: null | NodeMap,\n    dirtyElements: null | Map<NodeKey, IntentionallyMarkedAsDirtyElement>,\n    dirtyLeaves: null | Set<NodeKey>,\n  ): void {\n    const childCollabNode = this._children[index];\n    // Update\n    const nextChildNode = $getNodeByKeyOrThrow(key);\n\n    if (\n      childCollabNode instanceof CollabElementNode &&\n      $isElementNode(nextChildNode)\n    ) {\n      childCollabNode.syncPropertiesFromLexical(\n        binding,\n        nextChildNode,\n        prevNodeMap,\n      );\n      childCollabNode.syncChildrenFromLexical(\n        binding,\n        nextChildNode,\n        prevNodeMap,\n        dirtyElements,\n        dirtyLeaves,\n      );\n    } else if (\n      childCollabNode instanceof CollabTextNode &&\n      $isTextNode(nextChildNode)\n    ) {\n      childCollabNode.syncPropertiesAndTextFromLexical(\n        binding,\n        nextChildNode,\n        prevNodeMap,\n      );\n    } else if (\n      childCollabNode instanceof CollabDecoratorNode &&\n      $isDecoratorNode(nextChildNode)\n    ) {\n      childCollabNode.syncPropertiesFromLexical(\n        binding,\n        nextChildNode,\n        prevNodeMap,\n      );\n    }\n  }\n\n  syncChildrenFromLexical(\n    binding: Binding,\n    nextLexicalNode: ElementNode,\n    prevNodeMap: null | NodeMap,\n    dirtyElements: null | Map<NodeKey, IntentionallyMarkedAsDirtyElement>,\n    dirtyLeaves: null | Set<NodeKey>,\n  ): void {\n    const prevLexicalNode = this.getPrevNode(prevNodeMap);\n    const prevChildren =\n      prevLexicalNode === null\n        ? []\n        : $createChildrenArray(prevLexicalNode, prevNodeMap);\n    const nextChildren = $createChildrenArray(nextLexicalNode, null);\n    const prevEndIndex = prevChildren.length - 1;\n    const nextEndIndex = nextChildren.length - 1;\n    const collabNodeMap = binding.collabNodeMap;\n    let prevChildrenSet: Set<NodeKey> | undefined;\n    let nextChildrenSet: Set<NodeKey> | undefined;\n    let prevIndex = 0;\n    let nextIndex = 0;\n\n    while (prevIndex <= prevEndIndex && nextIndex <= nextEndIndex) {\n      const prevKey = prevChildren[prevIndex];\n      const nextKey = nextChildren[nextIndex];\n\n      if (prevKey === nextKey) {\n        // Nove move, create or remove\n        this._syncChildFromLexical(\n          binding,\n          nextIndex,\n          nextKey,\n          prevNodeMap,\n          dirtyElements,\n          dirtyLeaves,\n        );\n\n        prevIndex++;\n        nextIndex++;\n      } else {\n        if (prevChildrenSet === undefined) {\n          prevChildrenSet = new Set(prevChildren);\n        }\n\n        if (nextChildrenSet === undefined) {\n          nextChildrenSet = new Set(nextChildren);\n        }\n\n        const nextHasPrevKey = nextChildrenSet.has(prevKey);\n        const prevHasNextKey = prevChildrenSet.has(nextKey);\n\n        if (!nextHasPrevKey) {\n          // Remove\n          this.splice(binding, nextIndex, 1);\n          prevIndex++;\n        } else {\n          // Create or replace\n          const nextChildNode = $getNodeByKeyOrThrow(nextKey);\n          const collabNode = $createCollabNodeFromLexicalNode(\n            binding,\n            nextChildNode,\n            this,\n          );\n          collabNodeMap.set(nextKey, collabNode);\n\n          if (prevHasNextKey) {\n            this.splice(binding, nextIndex, 1, collabNode);\n            prevIndex++;\n            nextIndex++;\n          } else {\n            this.splice(binding, nextIndex, 0, collabNode);\n            nextIndex++;\n          }\n        }\n      }\n    }\n\n    const appendNewChildren = prevIndex > prevEndIndex;\n    const removeOldChildren = nextIndex > nextEndIndex;\n\n    if (appendNewChildren && !removeOldChildren) {\n      for (; nextIndex <= nextEndIndex; ++nextIndex) {\n        const key = nextChildren[nextIndex];\n        const nextChildNode = $getNodeByKeyOrThrow(key);\n        const collabNode = $createCollabNodeFromLexicalNode(\n          binding,\n          nextChildNode,\n          this,\n        );\n        this.append(collabNode);\n        collabNodeMap.set(key, collabNode);\n      }\n    } else if (removeOldChildren && !appendNewChildren) {\n      for (let i = this._children.length - 1; i >= nextIndex; i--) {\n        this.splice(binding, i, 1);\n      }\n    }\n  }\n\n  append(\n    collabNode:\n      | CollabElementNode\n      | CollabDecoratorNode\n      | CollabTextNode\n      | CollabLineBreakNode,\n  ): void {\n    const xmlText = this._xmlText;\n    const children = this._children;\n    const lastChild = children[children.length - 1];\n    const offset =\n      lastChild !== undefined ? lastChild.getOffset() + lastChild.getSize() : 0;\n\n    if (collabNode instanceof CollabElementNode) {\n      xmlText.insertEmbed(offset, collabNode._xmlText);\n    } else if (collabNode instanceof CollabTextNode) {\n      const map = collabNode._map;\n\n      if (map.parent === null) {\n        xmlText.insertEmbed(offset, map);\n      }\n\n      xmlText.insert(offset + 1, collabNode._text);\n    } else if (collabNode instanceof CollabLineBreakNode) {\n      xmlText.insertEmbed(offset, collabNode._map);\n    } else if (collabNode instanceof CollabDecoratorNode) {\n      xmlText.insertEmbed(offset, collabNode._xmlElem);\n    }\n\n    this._children.push(collabNode);\n  }\n\n  splice(\n    binding: Binding,\n    index: number,\n    delCount: number,\n    collabNode?:\n      | CollabElementNode\n      | CollabDecoratorNode\n      | CollabTextNode\n      | CollabLineBreakNode,\n  ): void {\n    const children = this._children;\n    const child = children[index];\n\n    if (child === undefined) {\n      invariant(\n        collabNode !== undefined,\n        'splice: could not find collab element node',\n      );\n      this.append(collabNode);\n      return;\n    }\n\n    const offset = child.getOffset();\n    invariant(offset !== -1, 'splice: expected offset to be greater than zero');\n\n    const xmlText = this._xmlText;\n\n    if (delCount !== 0) {\n      // What if we delete many nodes, don't we need to get all their\n      // sizes?\n      xmlText.delete(offset, child.getSize());\n    }\n\n    if (collabNode instanceof CollabElementNode) {\n      xmlText.insertEmbed(offset, collabNode._xmlText);\n    } else if (collabNode instanceof CollabTextNode) {\n      const map = collabNode._map;\n\n      if (map.parent === null) {\n        xmlText.insertEmbed(offset, map);\n      }\n\n      xmlText.insert(offset + 1, collabNode._text);\n    } else if (collabNode instanceof CollabLineBreakNode) {\n      xmlText.insertEmbed(offset, collabNode._map);\n    } else if (collabNode instanceof CollabDecoratorNode) {\n      xmlText.insertEmbed(offset, collabNode._xmlElem);\n    }\n\n    if (delCount !== 0) {\n      const childrenToDelete = children.slice(index, index + delCount);\n\n      for (let i = 0; i < childrenToDelete.length; i++) {\n        childrenToDelete[i].destroy(binding);\n      }\n    }\n\n    if (collabNode !== undefined) {\n      children.splice(index, delCount, collabNode);\n    } else {\n      children.splice(index, delCount);\n    }\n  }\n\n  getChildOffset(\n    collabNode:\n      | CollabElementNode\n      | CollabTextNode\n      | CollabDecoratorNode\n      | CollabLineBreakNode,\n  ): number {\n    let offset = 0;\n    const children = this._children;\n\n    for (let i = 0; i < children.length; i++) {\n      const child = children[i];\n\n      if (child === collabNode) {\n        return offset;\n      }\n\n      offset += child.getSize();\n    }\n\n    return -1;\n  }\n\n  destroy(binding: Binding): void {\n    const collabNodeMap = binding.collabNodeMap;\n    const children = this._children;\n\n    for (let i = 0; i < children.length; i++) {\n      children[i].destroy(binding);\n    }\n\n    collabNodeMap.delete(this._key);\n  }\n}\n\nexport function $createCollabElementNode(\n  xmlText: XmlText,\n  parent: null | CollabElementNode,\n  type: string,\n): CollabElementNode {\n  const collabNode = new CollabElementNode(xmlText, parent, type);\n  xmlText._collabNode = collabNode;\n  return collabNode;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/yjs/CollabLineBreakNode.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {Binding} from '.';\nimport type {CollabElementNode} from './CollabElementNode';\nimport type {LineBreakNode, NodeKey} from 'lexical';\nimport type {Map as YMap} from 'yjs';\n\nimport {$getNodeByKey, $isLineBreakNode} from 'lexical';\n\nexport class CollabLineBreakNode {\n  _map: YMap<unknown>;\n  _key: NodeKey;\n  _parent: CollabElementNode;\n  _type: 'linebreak';\n\n  constructor(map: YMap<unknown>, parent: CollabElementNode) {\n    this._key = '';\n    this._map = map;\n    this._parent = parent;\n    this._type = 'linebreak';\n  }\n\n  getNode(): null | LineBreakNode {\n    const node = $getNodeByKey(this._key);\n    return $isLineBreakNode(node) ? node : null;\n  }\n\n  getKey(): NodeKey {\n    return this._key;\n  }\n\n  getSharedType(): YMap<unknown> {\n    return this._map;\n  }\n\n  getType(): string {\n    return this._type;\n  }\n\n  getSize(): number {\n    return 1;\n  }\n\n  getOffset(): number {\n    const collabElementNode = this._parent;\n    return collabElementNode.getChildOffset(this);\n  }\n\n  destroy(binding: Binding): void {\n    const collabNodeMap = binding.collabNodeMap;\n    collabNodeMap.delete(this._key);\n  }\n}\n\nexport function $createCollabLineBreakNode(\n  map: YMap<unknown>,\n  parent: CollabElementNode,\n): CollabLineBreakNode {\n  const collabNode = new CollabLineBreakNode(map, parent);\n  map._collabNode = collabNode;\n  return collabNode;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/yjs/CollabTextNode.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {Binding} from '.';\nimport type {CollabElementNode} from './CollabElementNode';\nimport type {NodeKey, NodeMap, TextNode} from 'lexical';\nimport type {Map as YMap} from 'yjs';\n\nimport {\n  $getNodeByKey,\n  $getSelection,\n  $isRangeSelection,\n  $isTextNode,\n} from 'lexical';\nimport invariant from 'lexical/shared/invariant';\nimport simpleDiffWithCursor from 'lexical/shared/simpleDiffWithCursor';\n\nimport {syncPropertiesFromLexical, syncPropertiesFromYjs} from './Utils';\n\nfunction $diffTextContentAndApplyDelta(\n  collabNode: CollabTextNode,\n  key: NodeKey,\n  prevText: string,\n  nextText: string,\n): void {\n  const selection = $getSelection();\n  let cursorOffset = nextText.length;\n\n  if ($isRangeSelection(selection) && selection.isCollapsed()) {\n    const anchor = selection.anchor;\n\n    if (anchor.key === key) {\n      cursorOffset = anchor.offset;\n    }\n  }\n\n  const diff = simpleDiffWithCursor(prevText, nextText, cursorOffset);\n  collabNode.spliceText(diff.index, diff.remove, diff.insert);\n}\n\nexport class CollabTextNode {\n  _map: YMap<unknown>;\n  _key: NodeKey;\n  _parent: CollabElementNode;\n  _text: string;\n  _type: string;\n  _normalized: boolean;\n\n  constructor(\n    map: YMap<unknown>,\n    text: string,\n    parent: CollabElementNode,\n    type: string,\n  ) {\n    this._key = '';\n    this._map = map;\n    this._parent = parent;\n    this._text = text;\n    this._type = type;\n    this._normalized = false;\n  }\n\n  getPrevNode(nodeMap: null | NodeMap): null | TextNode {\n    if (nodeMap === null) {\n      return null;\n    }\n\n    const node = nodeMap.get(this._key);\n    return $isTextNode(node) ? node : null;\n  }\n\n  getNode(): null | TextNode {\n    const node = $getNodeByKey(this._key);\n    return $isTextNode(node) ? node : null;\n  }\n\n  getSharedType(): YMap<unknown> {\n    return this._map;\n  }\n\n  getType(): string {\n    return this._type;\n  }\n\n  getKey(): NodeKey {\n    return this._key;\n  }\n\n  getSize(): number {\n    return this._text.length + (this._normalized ? 0 : 1);\n  }\n\n  getOffset(): number {\n    const collabElementNode = this._parent;\n    return collabElementNode.getChildOffset(this);\n  }\n\n  spliceText(index: number, delCount: number, newText: string): void {\n    const collabElementNode = this._parent;\n    const xmlText = collabElementNode._xmlText;\n    const offset = this.getOffset() + 1 + index;\n\n    if (delCount !== 0) {\n      xmlText.delete(offset, delCount);\n    }\n\n    if (newText !== '') {\n      xmlText.insert(offset, newText);\n    }\n  }\n\n  syncPropertiesAndTextFromLexical(\n    binding: Binding,\n    nextLexicalNode: TextNode,\n    prevNodeMap: null | NodeMap,\n  ): void {\n    const prevLexicalNode = this.getPrevNode(prevNodeMap);\n    const nextText = nextLexicalNode.__text;\n\n    syncPropertiesFromLexical(\n      binding,\n      this._map,\n      prevLexicalNode,\n      nextLexicalNode,\n    );\n\n    if (prevLexicalNode !== null) {\n      const prevText = prevLexicalNode.__text;\n\n      if (prevText !== nextText) {\n        const key = nextLexicalNode.__key;\n        $diffTextContentAndApplyDelta(this, key, prevText, nextText);\n        this._text = nextText;\n      }\n    }\n  }\n\n  syncPropertiesAndTextFromYjs(\n    binding: Binding,\n    keysChanged: null | Set<string>,\n  ): void {\n    const lexicalNode = this.getNode();\n    invariant(\n      lexicalNode !== null,\n      'syncPropertiesAndTextFromYjs: could not find decorator node',\n    );\n\n    syncPropertiesFromYjs(binding, this._map, lexicalNode, keysChanged);\n\n    const collabText = this._text;\n\n    if (lexicalNode.__text !== collabText) {\n      const writable = lexicalNode.getWritable();\n      writable.__text = collabText;\n    }\n  }\n\n  destroy(binding: Binding): void {\n    const collabNodeMap = binding.collabNodeMap;\n    collabNodeMap.delete(this._key);\n  }\n}\n\nexport function $createCollabTextNode(\n  map: YMap<unknown>,\n  text: string,\n  parent: CollabElementNode,\n  type: string,\n): CollabTextNode {\n  const collabNode = new CollabTextNode(map, text, parent, type);\n  map._collabNode = collabNode;\n  return collabNode;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/yjs/SyncCursors.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {Binding} from './Bindings';\nimport type {BaseSelection, NodeKey, NodeMap, Point} from 'lexical';\nimport type {AbsolutePosition, RelativePosition} from 'yjs';\n\nimport {createDOMRange, createRectsFromDOMRange} from '@lexical/selection';\nimport {\n  $getNodeByKey,\n  $getSelection,\n  $isElementNode,\n  $isLineBreakNode,\n  $isRangeSelection,\n  $isTextNode,\n} from 'lexical';\nimport invariant from 'lexical/shared/invariant';\nimport {\n  compareRelativePositions,\n  createAbsolutePositionFromRelativePosition,\n  createRelativePositionFromTypeIndex,\n} from 'yjs';\n\nimport {Provider} from '.';\nimport {CollabDecoratorNode} from './CollabDecoratorNode';\nimport {CollabElementNode} from './CollabElementNode';\nimport {CollabLineBreakNode} from './CollabLineBreakNode';\nimport {CollabTextNode} from './CollabTextNode';\nimport {getPositionFromElementAndOffset} from './Utils';\n\nexport type CursorSelection = {\n  anchor: {\n    key: NodeKey;\n    offset: number;\n  };\n  caret: HTMLElement;\n  color: string;\n  focus: {\n    key: NodeKey;\n    offset: number;\n  };\n  name: HTMLSpanElement;\n  selections: Array<HTMLElement>;\n};\nexport type Cursor = {\n  color: string;\n  name: string;\n  selection: null | CursorSelection;\n};\n\nfunction createRelativePosition(\n  point: Point,\n  binding: Binding,\n): null | RelativePosition {\n  const collabNodeMap = binding.collabNodeMap;\n  const collabNode = collabNodeMap.get(point.key);\n\n  if (collabNode === undefined) {\n    return null;\n  }\n\n  let offset = point.offset;\n  let sharedType = collabNode.getSharedType();\n\n  if (collabNode instanceof CollabTextNode) {\n    sharedType = collabNode._parent._xmlText;\n    const currentOffset = collabNode.getOffset();\n\n    if (currentOffset === -1) {\n      return null;\n    }\n\n    offset = currentOffset + 1 + offset;\n  } else if (\n    collabNode instanceof CollabElementNode &&\n    point.type === 'element'\n  ) {\n    const parent = point.getNode();\n    invariant($isElementNode(parent), 'Element point must be an element node');\n    let accumulatedOffset = 0;\n    let i = 0;\n    let node = parent.getFirstChild();\n    while (node !== null && i++ < offset) {\n      if ($isTextNode(node)) {\n        accumulatedOffset += node.getTextContentSize() + 1;\n      } else {\n        accumulatedOffset++;\n      }\n      node = node.getNextSibling();\n    }\n    offset = accumulatedOffset;\n  }\n\n  return createRelativePositionFromTypeIndex(sharedType, offset);\n}\n\nfunction createAbsolutePosition(\n  relativePosition: RelativePosition,\n  binding: Binding,\n): AbsolutePosition | null {\n  return createAbsolutePositionFromRelativePosition(\n    relativePosition,\n    binding.doc,\n  );\n}\n\nfunction shouldUpdatePosition(\n  currentPos: RelativePosition | null | undefined,\n  pos: RelativePosition | null | undefined,\n): boolean {\n  if (currentPos == null) {\n    if (pos != null) {\n      return true;\n    }\n  } else if (pos == null || !compareRelativePositions(currentPos, pos)) {\n    return true;\n  }\n\n  return false;\n}\n\nfunction createCursor(name: string, color: string): Cursor {\n  return {\n    color: color,\n    name: name,\n    selection: null,\n  };\n}\n\nfunction destroySelection(binding: Binding, selection: CursorSelection) {\n  const cursorsContainer = binding.cursorsContainer;\n\n  if (cursorsContainer !== null) {\n    const selections = selection.selections;\n    const selectionsLength = selections.length;\n\n    for (let i = 0; i < selectionsLength; i++) {\n      cursorsContainer.removeChild(selections[i]);\n    }\n  }\n}\n\nfunction destroyCursor(binding: Binding, cursor: Cursor) {\n  const selection = cursor.selection;\n\n  if (selection !== null) {\n    destroySelection(binding, selection);\n  }\n}\n\nfunction createCursorSelection(\n  cursor: Cursor,\n  anchorKey: NodeKey,\n  anchorOffset: number,\n  focusKey: NodeKey,\n  focusOffset: number,\n): CursorSelection {\n  const color = cursor.color;\n  const caret = document.createElement('span');\n  caret.style.cssText = `position:absolute;top:0;bottom:0;right:-1px;width:1px;background-color:${color};z-index:10;`;\n  const name = document.createElement('span');\n  name.textContent = cursor.name;\n  name.style.cssText = `position:absolute;left:-2px;top:-16px;background-color:${color};color:#fff;line-height:12px;font-size:12px;padding:2px;font-family:Arial;font-weight:bold;white-space:nowrap;`;\n  caret.appendChild(name);\n  return {\n    anchor: {\n      key: anchorKey,\n      offset: anchorOffset,\n    },\n    caret,\n    color,\n    focus: {\n      key: focusKey,\n      offset: focusOffset,\n    },\n    name,\n    selections: [],\n  };\n}\n\nfunction updateCursor(\n  binding: Binding,\n  cursor: Cursor,\n  nextSelection: null | CursorSelection,\n  nodeMap: NodeMap,\n): void {\n  const editor = binding.editor;\n  const rootElement = editor.getRootElement();\n  const cursorsContainer = binding.cursorsContainer;\n\n  if (cursorsContainer === null || rootElement === null) {\n    return;\n  }\n\n  const cursorsContainerOffsetParent = cursorsContainer.offsetParent;\n  if (cursorsContainerOffsetParent === null) {\n    return;\n  }\n\n  const containerRect = cursorsContainerOffsetParent.getBoundingClientRect();\n  const prevSelection = cursor.selection;\n\n  if (nextSelection === null) {\n    if (prevSelection === null) {\n      return;\n    } else {\n      cursor.selection = null;\n      destroySelection(binding, prevSelection);\n      return;\n    }\n  } else {\n    cursor.selection = nextSelection;\n  }\n\n  const caret = nextSelection.caret;\n  const color = nextSelection.color;\n  const selections = nextSelection.selections;\n  const anchor = nextSelection.anchor;\n  const focus = nextSelection.focus;\n  const anchorKey = anchor.key;\n  const focusKey = focus.key;\n  const anchorNode = nodeMap.get(anchorKey);\n  const focusNode = nodeMap.get(focusKey);\n\n  if (anchorNode == null || focusNode == null) {\n    return;\n  }\n  let selectionRects: Array<DOMRect>;\n\n  // In the case of a collapsed selection on a linebreak, we need\n  // to improvise as the browser will return nothing here as <br>\n  // apparantly take up no visual space :/\n  // This won't work in all cases, but it's better than just showing\n  // nothing all the time.\n  if (anchorNode === focusNode && $isLineBreakNode(anchorNode)) {\n    const brRect = (\n      editor.getElementByKey(anchorKey) as HTMLElement\n    ).getBoundingClientRect();\n    selectionRects = [brRect];\n  } else {\n    const range = createDOMRange(\n      editor,\n      anchorNode,\n      anchor.offset,\n      focusNode,\n      focus.offset,\n    );\n\n    if (range === null) {\n      return;\n    }\n    selectionRects = createRectsFromDOMRange(editor, range);\n  }\n\n  const selectionsLength = selections.length;\n  const selectionRectsLength = selectionRects.length;\n\n  for (let i = 0; i < selectionRectsLength; i++) {\n    const selectionRect = selectionRects[i];\n    let selection = selections[i];\n\n    if (selection === undefined) {\n      selection = document.createElement('span');\n      selections[i] = selection;\n      const selectionBg = document.createElement('span');\n      selection.appendChild(selectionBg);\n      cursorsContainer.appendChild(selection);\n    }\n\n    const top = selectionRect.top - containerRect.top;\n    const left = selectionRect.left - containerRect.left;\n    const style = `position:absolute;top:${top}px;left:${left}px;height:${selectionRect.height}px;width:${selectionRect.width}px;pointer-events:none;z-index:5;`;\n    selection.style.cssText = style;\n\n    (\n      selection.firstChild as HTMLSpanElement\n    ).style.cssText = `${style}left:0;top:0;background-color:${color};opacity:0.3;`;\n\n    if (i === selectionRectsLength - 1) {\n      if (caret.parentNode !== selection) {\n        selection.appendChild(caret);\n      }\n    }\n  }\n\n  for (let i = selectionsLength - 1; i >= selectionRectsLength; i--) {\n    const selection = selections[i];\n    cursorsContainer.removeChild(selection);\n    selections.pop();\n  }\n}\n\nexport function $syncLocalCursorPosition(\n  binding: Binding,\n  provider: Provider,\n): void {\n  const awareness = provider.awareness;\n  const localState = awareness.getLocalState();\n\n  if (localState === null) {\n    return;\n  }\n\n  const anchorPos = localState.anchorPos;\n  const focusPos = localState.focusPos;\n\n  if (anchorPos !== null && focusPos !== null) {\n    const anchorAbsPos = createAbsolutePosition(anchorPos, binding);\n    const focusAbsPos = createAbsolutePosition(focusPos, binding);\n\n    if (anchorAbsPos !== null && focusAbsPos !== null) {\n      const [anchorCollabNode, anchorOffset] = getCollabNodeAndOffset(\n        anchorAbsPos.type,\n        anchorAbsPos.index,\n      );\n      const [focusCollabNode, focusOffset] = getCollabNodeAndOffset(\n        focusAbsPos.type,\n        focusAbsPos.index,\n      );\n\n      if (anchorCollabNode !== null && focusCollabNode !== null) {\n        const anchorKey = anchorCollabNode.getKey();\n        const focusKey = focusCollabNode.getKey();\n\n        const selection = $getSelection();\n\n        if (!$isRangeSelection(selection)) {\n          return;\n        }\n        const anchor = selection.anchor;\n        const focus = selection.focus;\n\n        $setPoint(anchor, anchorKey, anchorOffset);\n        $setPoint(focus, focusKey, focusOffset);\n      }\n    }\n  }\n}\n\nfunction $setPoint(point: Point, key: NodeKey, offset: number): void {\n  if (point.key !== key || point.offset !== offset) {\n    let anchorNode = $getNodeByKey(key);\n    if (\n      anchorNode !== null &&\n      !$isElementNode(anchorNode) &&\n      !$isTextNode(anchorNode)\n    ) {\n      const parent = anchorNode.getParentOrThrow();\n      key = parent.getKey();\n      offset = anchorNode.getIndexWithinParent();\n      anchorNode = parent;\n    }\n    point.set(key, offset, $isElementNode(anchorNode) ? 'element' : 'text');\n  }\n}\n\nfunction getCollabNodeAndOffset(\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  sharedType: any,\n  offset: number,\n): [\n  (\n    | null\n    | CollabDecoratorNode\n    | CollabElementNode\n    | CollabTextNode\n    | CollabLineBreakNode\n  ),\n  number,\n] {\n  const collabNode = sharedType._collabNode;\n\n  if (collabNode === undefined) {\n    return [null, 0];\n  }\n\n  if (collabNode instanceof CollabElementNode) {\n    const {node, offset: collabNodeOffset} = getPositionFromElementAndOffset(\n      collabNode,\n      offset,\n      true,\n    );\n\n    if (node === null) {\n      return [collabNode, 0];\n    } else {\n      return [node, collabNodeOffset];\n    }\n  }\n\n  return [null, 0];\n}\n\nexport function syncCursorPositions(\n  binding: Binding,\n  provider: Provider,\n): void {\n  const awarenessStates = Array.from(provider.awareness.getStates());\n  const localClientID = binding.clientID;\n  const cursors = binding.cursors;\n  const editor = binding.editor;\n  const nodeMap = editor._editorState._nodeMap;\n  const visitedClientIDs = new Set();\n\n  for (let i = 0; i < awarenessStates.length; i++) {\n    const awarenessState = awarenessStates[i];\n    const [clientID, awareness] = awarenessState;\n\n    if (clientID !== localClientID) {\n      visitedClientIDs.add(clientID);\n      const {anchorPos, focusPos, name, color, focusing} = awareness;\n      let selection = null;\n\n      let cursor = cursors.get(clientID);\n\n      if (cursor === undefined) {\n        cursor = createCursor(name, color);\n        cursors.set(clientID, cursor);\n      }\n\n      if (anchorPos !== null && focusPos !== null && focusing) {\n        const anchorAbsPos = createAbsolutePosition(anchorPos, binding);\n        const focusAbsPos = createAbsolutePosition(focusPos, binding);\n\n        if (anchorAbsPos !== null && focusAbsPos !== null) {\n          const [anchorCollabNode, anchorOffset] = getCollabNodeAndOffset(\n            anchorAbsPos.type,\n            anchorAbsPos.index,\n          );\n          const [focusCollabNode, focusOffset] = getCollabNodeAndOffset(\n            focusAbsPos.type,\n            focusAbsPos.index,\n          );\n\n          if (anchorCollabNode !== null && focusCollabNode !== null) {\n            const anchorKey = anchorCollabNode.getKey();\n            const focusKey = focusCollabNode.getKey();\n            selection = cursor.selection;\n\n            if (selection === null) {\n              selection = createCursorSelection(\n                cursor,\n                anchorKey,\n                anchorOffset,\n                focusKey,\n                focusOffset,\n              );\n            } else {\n              const anchor = selection.anchor;\n              const focus = selection.focus;\n              anchor.key = anchorKey;\n              anchor.offset = anchorOffset;\n              focus.key = focusKey;\n              focus.offset = focusOffset;\n            }\n          }\n        }\n      }\n\n      updateCursor(binding, cursor, selection, nodeMap);\n    }\n  }\n\n  const allClientIDs = Array.from(cursors.keys());\n\n  for (let i = 0; i < allClientIDs.length; i++) {\n    const clientID = allClientIDs[i];\n\n    if (!visitedClientIDs.has(clientID)) {\n      const cursor = cursors.get(clientID);\n\n      if (cursor !== undefined) {\n        destroyCursor(binding, cursor);\n        cursors.delete(clientID);\n      }\n    }\n  }\n}\n\nexport function syncLexicalSelectionToYjs(\n  binding: Binding,\n  provider: Provider,\n  prevSelection: null | BaseSelection,\n  nextSelection: null | BaseSelection,\n): void {\n  const awareness = provider.awareness;\n  const localState = awareness.getLocalState();\n\n  if (localState === null) {\n    return;\n  }\n\n  const {\n    anchorPos: currentAnchorPos,\n    focusPos: currentFocusPos,\n    name,\n    color,\n    focusing,\n    awarenessData,\n  } = localState;\n  let anchorPos = null;\n  let focusPos = null;\n\n  if (\n    nextSelection === null ||\n    (currentAnchorPos !== null && !nextSelection.is(prevSelection))\n  ) {\n    if (prevSelection === null) {\n      return;\n    }\n  }\n\n  if ($isRangeSelection(nextSelection)) {\n    anchorPos = createRelativePosition(nextSelection.anchor, binding);\n    focusPos = createRelativePosition(nextSelection.focus, binding);\n  }\n\n  if (\n    shouldUpdatePosition(currentAnchorPos, anchorPos) ||\n    shouldUpdatePosition(currentFocusPos, focusPos)\n  ) {\n    awareness.setLocalState({\n      anchorPos,\n      awarenessData,\n      color,\n      focusPos,\n      focusing,\n      name,\n    });\n  }\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/yjs/SyncEditorStates.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {EditorState, NodeKey} from 'lexical';\n\nimport {\n  $createParagraphNode,\n  $getNodeByKey,\n  $getRoot,\n  $getSelection,\n  $isRangeSelection,\n  $isTextNode,\n} from 'lexical';\nimport invariant from 'lexical/shared/invariant';\nimport {Text as YText, YEvent, YMapEvent, YTextEvent, YXmlEvent} from 'yjs';\n\nimport {Binding, Provider} from '.';\nimport {CollabDecoratorNode} from './CollabDecoratorNode';\nimport {CollabElementNode} from './CollabElementNode';\nimport {CollabTextNode} from './CollabTextNode';\nimport {\n  $syncLocalCursorPosition,\n  syncCursorPositions,\n  syncLexicalSelectionToYjs,\n} from './SyncCursors';\nimport {\n  $getOrInitCollabNodeFromSharedType,\n  $moveSelectionToPreviousNode,\n  doesSelectionNeedRecovering,\n  syncWithTransaction,\n} from './Utils';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction $syncEvent(binding: Binding, event: any): void {\n  const {target} = event;\n  const collabNode = $getOrInitCollabNodeFromSharedType(binding, target);\n\n  if (collabNode instanceof CollabElementNode && event instanceof YTextEvent) {\n    // @ts-expect-error We need to access the private property of the class\n    const {keysChanged, childListChanged, delta} = event;\n\n    // Update\n    if (keysChanged.size > 0) {\n      collabNode.syncPropertiesFromYjs(binding, keysChanged);\n    }\n\n    if (childListChanged) {\n      collabNode.applyChildrenYjsDelta(binding, delta);\n      collabNode.syncChildrenFromYjs(binding);\n    }\n  } else if (\n    collabNode instanceof CollabTextNode &&\n    event instanceof YMapEvent\n  ) {\n    const {keysChanged} = event;\n\n    // Update\n    if (keysChanged.size > 0) {\n      collabNode.syncPropertiesAndTextFromYjs(binding, keysChanged);\n    }\n  } else if (\n    collabNode instanceof CollabDecoratorNode &&\n    event instanceof YXmlEvent\n  ) {\n    const {attributesChanged} = event;\n\n    // Update\n    if (attributesChanged.size > 0) {\n      collabNode.syncPropertiesFromYjs(binding, attributesChanged);\n    }\n  } else {\n    invariant(false, 'Expected text, element, or decorator event');\n  }\n}\n\nexport function syncYjsChangesToLexical(\n  binding: Binding,\n  provider: Provider,\n  events: Array<YEvent<YText>>,\n  isFromUndoManger: boolean,\n): void {\n  const editor = binding.editor;\n  const currentEditorState = editor._editorState;\n\n  // This line precompute the delta before editor update. The reason is\n  // delta is computed when it is accessed. Note that this can only be\n  // safely computed during the event call. If it is accessed after event\n  // call it might result in unexpected behavior.\n  // https://github.com/yjs/yjs/blob/00ef472d68545cb260abd35c2de4b3b78719c9e4/src/utils/YEvent.js#L132\n  events.forEach((event) => event.delta);\n\n  editor.update(\n    () => {\n      for (let i = 0; i < events.length; i++) {\n        const event = events[i];\n        $syncEvent(binding, event);\n      }\n\n      const selection = $getSelection();\n\n      if ($isRangeSelection(selection)) {\n        if (doesSelectionNeedRecovering(selection)) {\n          const prevSelection = currentEditorState._selection;\n\n          if ($isRangeSelection(prevSelection)) {\n            $syncLocalCursorPosition(binding, provider);\n            if (doesSelectionNeedRecovering(selection)) {\n              // If the selected node is deleted, move the selection to the previous or parent node.\n              const anchorNodeKey = selection.anchor.key;\n              $moveSelectionToPreviousNode(anchorNodeKey, currentEditorState);\n            }\n          }\n\n          syncLexicalSelectionToYjs(\n            binding,\n            provider,\n            prevSelection,\n            $getSelection(),\n          );\n        } else {\n          $syncLocalCursorPosition(binding, provider);\n        }\n      }\n    },\n    {\n      onUpdate: () => {\n        syncCursorPositions(binding, provider);\n        // If there was a collision on the top level paragraph\n        // we need to re-add a paragraph. To ensure this insertion properly syncs with other clients,\n        // it must be placed outside of the update block above that has tags 'collaboration' or 'historic'.\n        editor.update(() => {\n          if ($getRoot().getChildrenSize() === 0) {\n            $getRoot().append($createParagraphNode());\n          }\n        });\n      },\n      skipTransforms: true,\n      tag: isFromUndoManger ? 'historic' : 'collaboration',\n    },\n  );\n}\n\nfunction $handleNormalizationMergeConflicts(\n  binding: Binding,\n  normalizedNodes: Set<NodeKey>,\n): void {\n  // We handle the merge operations here\n  const normalizedNodesKeys = Array.from(normalizedNodes);\n  const collabNodeMap = binding.collabNodeMap;\n  const mergedNodes = [];\n\n  for (let i = 0; i < normalizedNodesKeys.length; i++) {\n    const nodeKey = normalizedNodesKeys[i];\n    const lexicalNode = $getNodeByKey(nodeKey);\n    const collabNode = collabNodeMap.get(nodeKey);\n\n    if (collabNode instanceof CollabTextNode) {\n      if ($isTextNode(lexicalNode)) {\n        // We mutate the text collab nodes after removing\n        // all the dead nodes first, otherwise offsets break.\n        mergedNodes.push([collabNode, lexicalNode.__text]);\n      } else {\n        const offset = collabNode.getOffset();\n\n        if (offset === -1) {\n          continue;\n        }\n\n        const parent = collabNode._parent;\n        collabNode._normalized = true;\n\n        parent._xmlText.delete(offset, 1);\n\n        collabNodeMap.delete(nodeKey);\n        const parentChildren = parent._children;\n        const index = parentChildren.indexOf(collabNode);\n        parentChildren.splice(index, 1);\n      }\n    }\n  }\n\n  for (let i = 0; i < mergedNodes.length; i++) {\n    const [collabNode, text] = mergedNodes[i];\n    if (collabNode instanceof CollabTextNode && typeof text === 'string') {\n      collabNode._text = text;\n    }\n  }\n}\n\ntype IntentionallyMarkedAsDirtyElement = boolean;\n\nexport function syncLexicalUpdateToYjs(\n  binding: Binding,\n  provider: Provider,\n  prevEditorState: EditorState,\n  currEditorState: EditorState,\n  dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,\n  dirtyLeaves: Set<NodeKey>,\n  normalizedNodes: Set<NodeKey>,\n  tags: Set<string>,\n): void {\n  syncWithTransaction(binding, () => {\n    currEditorState.read(() => {\n      // We check if the update has come from a origin where the origin\n      // was the collaboration binding previously. This can help us\n      // prevent unnecessarily re-diffing and possible re-applying\n      // the same change editor state again. For example, if a user\n      // types a character and we get it, we don't want to then insert\n      // the same character again. The exception to this heuristic is\n      // when we need to handle normalization merge conflicts.\n      if (tags.has('collaboration') || tags.has('historic')) {\n        if (normalizedNodes.size > 0) {\n          $handleNormalizationMergeConflicts(binding, normalizedNodes);\n        }\n\n        return;\n      }\n\n      if (dirtyElements.has('root')) {\n        const prevNodeMap = prevEditorState._nodeMap;\n        const nextLexicalRoot = $getRoot();\n        const collabRoot = binding.root;\n        collabRoot.syncPropertiesFromLexical(\n          binding,\n          nextLexicalRoot,\n          prevNodeMap,\n        );\n        collabRoot.syncChildrenFromLexical(\n          binding,\n          nextLexicalRoot,\n          prevNodeMap,\n          dirtyElements,\n          dirtyLeaves,\n        );\n      }\n\n      const selection = $getSelection();\n      const prevSelection = prevEditorState._selection;\n      syncLexicalSelectionToYjs(binding, provider, prevSelection, selection);\n    });\n  });\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/yjs/Utils.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {Binding, YjsNode} from '.';\nimport type {\n  DecoratorNode,\n  EditorState,\n  ElementNode,\n  LexicalNode,\n  RangeSelection,\n  TextNode,\n} from 'lexical';\n\nimport {\n  $getNodeByKey,\n  $getRoot,\n  $isDecoratorNode,\n  $isElementNode,\n  $isLineBreakNode,\n  $isRootNode,\n  $isTextNode,\n  createEditor,\n  NodeKey,\n} from 'lexical';\nimport invariant from 'lexical/shared/invariant';\nimport {Doc, Map as YMap, XmlElement, XmlText} from 'yjs';\n\nimport {\n  $createCollabDecoratorNode,\n  CollabDecoratorNode,\n} from './CollabDecoratorNode';\nimport {$createCollabElementNode, CollabElementNode} from './CollabElementNode';\nimport {\n  $createCollabLineBreakNode,\n  CollabLineBreakNode,\n} from './CollabLineBreakNode';\nimport {$createCollabTextNode, CollabTextNode} from './CollabTextNode';\n\nconst baseExcludedProperties = new Set<string>([\n  '__key',\n  '__parent',\n  '__next',\n  '__prev',\n]);\nconst elementExcludedProperties = new Set<string>([\n  '__first',\n  '__last',\n  '__size',\n]);\nconst rootExcludedProperties = new Set<string>(['__cachedText']);\nconst textExcludedProperties = new Set<string>(['__text']);\n\nfunction isExcludedProperty(\n  name: string,\n  node: LexicalNode,\n  binding: Binding,\n): boolean {\n  if (baseExcludedProperties.has(name)) {\n    return true;\n  }\n\n  if ($isTextNode(node)) {\n    if (textExcludedProperties.has(name)) {\n      return true;\n    }\n  } else if ($isElementNode(node)) {\n    if (\n      elementExcludedProperties.has(name) ||\n      ($isRootNode(node) && rootExcludedProperties.has(name))\n    ) {\n      return true;\n    }\n  }\n\n  const nodeKlass = node.constructor;\n  const excludedProperties = binding.excludedProperties.get(nodeKlass);\n  return excludedProperties != null && excludedProperties.has(name);\n}\n\nexport function getIndexOfYjsNode(\n  yjsParentNode: YjsNode,\n  yjsNode: YjsNode,\n): number {\n  let node = yjsParentNode.firstChild;\n  let i = -1;\n\n  if (node === null) {\n    return -1;\n  }\n\n  do {\n    i++;\n\n    if (node === yjsNode) {\n      return i;\n    }\n\n    // @ts-expect-error Sibling exists but type is not available from YJS.\n    node = node.nextSibling;\n\n    if (node === null) {\n      return -1;\n    }\n  } while (node !== null);\n\n  return i;\n}\n\nexport function $getNodeByKeyOrThrow(key: NodeKey): LexicalNode {\n  const node = $getNodeByKey(key);\n  invariant(node !== null, 'could not find node by key');\n  return node;\n}\n\nexport function $createCollabNodeFromLexicalNode(\n  binding: Binding,\n  lexicalNode: LexicalNode,\n  parent: CollabElementNode,\n):\n  | CollabElementNode\n  | CollabTextNode\n  | CollabLineBreakNode\n  | CollabDecoratorNode {\n  const nodeType = lexicalNode.__type;\n  let collabNode;\n\n  if ($isElementNode(lexicalNode)) {\n    const xmlText = new XmlText();\n    collabNode = $createCollabElementNode(xmlText, parent, nodeType);\n    collabNode.syncPropertiesFromLexical(binding, lexicalNode, null);\n    collabNode.syncChildrenFromLexical(binding, lexicalNode, null, null, null);\n  } else if ($isTextNode(lexicalNode)) {\n    // TODO create a token text node for token, segmented nodes.\n    const map = new YMap();\n    collabNode = $createCollabTextNode(\n      map,\n      lexicalNode.__text,\n      parent,\n      nodeType,\n    );\n    collabNode.syncPropertiesAndTextFromLexical(binding, lexicalNode, null);\n  } else if ($isLineBreakNode(lexicalNode)) {\n    const map = new YMap();\n    map.set('__type', 'linebreak');\n    collabNode = $createCollabLineBreakNode(map, parent);\n  } else if ($isDecoratorNode(lexicalNode)) {\n    const xmlElem = new XmlElement();\n    collabNode = $createCollabDecoratorNode(xmlElem, parent, nodeType);\n    collabNode.syncPropertiesFromLexical(binding, lexicalNode, null);\n  } else {\n    invariant(false, 'Expected text, element, decorator, or linebreak node');\n  }\n\n  collabNode._key = lexicalNode.__key;\n  return collabNode;\n}\n\nfunction getNodeTypeFromSharedType(\n  sharedType: XmlText | YMap<unknown> | XmlElement,\n): string {\n  const type =\n    sharedType instanceof YMap\n      ? sharedType.get('__type')\n      : sharedType.getAttribute('__type');\n  invariant(type != null, 'Expected shared type to include type attribute');\n  return type;\n}\n\nexport function $getOrInitCollabNodeFromSharedType(\n  binding: Binding,\n  sharedType: XmlText | YMap<unknown> | XmlElement,\n  parent?: CollabElementNode,\n):\n  | CollabElementNode\n  | CollabTextNode\n  | CollabLineBreakNode\n  | CollabDecoratorNode {\n  const collabNode = sharedType._collabNode;\n\n  if (collabNode === undefined) {\n    const registeredNodes = binding.editor._nodes;\n    const type = getNodeTypeFromSharedType(sharedType);\n    const nodeInfo = registeredNodes.get(type);\n    invariant(nodeInfo !== undefined, 'Node %s is not registered', type);\n\n    const sharedParent = sharedType.parent;\n    const targetParent =\n      parent === undefined && sharedParent !== null\n        ? $getOrInitCollabNodeFromSharedType(\n            binding,\n            sharedParent as XmlText | YMap<unknown> | XmlElement,\n          )\n        : parent || null;\n\n    invariant(\n      targetParent instanceof CollabElementNode,\n      'Expected parent to be a collab element node',\n    );\n\n    if (sharedType instanceof XmlText) {\n      return $createCollabElementNode(sharedType, targetParent, type);\n    } else if (sharedType instanceof YMap) {\n      if (type === 'linebreak') {\n        return $createCollabLineBreakNode(sharedType, targetParent);\n      }\n      return $createCollabTextNode(sharedType, '', targetParent, type);\n    } else if (sharedType instanceof XmlElement) {\n      return $createCollabDecoratorNode(sharedType, targetParent, type);\n    }\n  }\n\n  return collabNode;\n}\n\nexport function createLexicalNodeFromCollabNode(\n  binding: Binding,\n  collabNode:\n    | CollabElementNode\n    | CollabTextNode\n    | CollabDecoratorNode\n    | CollabLineBreakNode,\n  parentKey: NodeKey,\n): LexicalNode {\n  const type = collabNode.getType();\n  const registeredNodes = binding.editor._nodes;\n  const nodeInfo = registeredNodes.get(type);\n  invariant(nodeInfo !== undefined, 'Node %s is not registered', type);\n  const lexicalNode:\n    | DecoratorNode<unknown>\n    | TextNode\n    | ElementNode\n    | LexicalNode = new nodeInfo.klass();\n  lexicalNode.__parent = parentKey;\n  collabNode._key = lexicalNode.__key;\n\n  if (collabNode instanceof CollabElementNode) {\n    const xmlText = collabNode._xmlText;\n    collabNode.syncPropertiesFromYjs(binding, null);\n    collabNode.applyChildrenYjsDelta(binding, xmlText.toDelta());\n    collabNode.syncChildrenFromYjs(binding);\n  } else if (collabNode instanceof CollabTextNode) {\n    collabNode.syncPropertiesAndTextFromYjs(binding, null);\n  } else if (collabNode instanceof CollabDecoratorNode) {\n    collabNode.syncPropertiesFromYjs(binding, null);\n  }\n\n  binding.collabNodeMap.set(lexicalNode.__key, collabNode);\n  return lexicalNode;\n}\n\nexport function syncPropertiesFromYjs(\n  binding: Binding,\n  sharedType: XmlText | YMap<unknown> | XmlElement,\n  lexicalNode: LexicalNode,\n  keysChanged: null | Set<string>,\n): void {\n  const properties =\n    keysChanged === null\n      ? sharedType instanceof YMap\n        ? Array.from(sharedType.keys())\n        : Object.keys(sharedType.getAttributes())\n      : Array.from(keysChanged);\n  let writableNode;\n\n  for (let i = 0; i < properties.length; i++) {\n    const property = properties[i];\n    if (isExcludedProperty(property, lexicalNode, binding)) {\n      continue;\n    }\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const prevValue = (lexicalNode as any)[property];\n    let nextValue =\n      sharedType instanceof YMap\n        ? sharedType.get(property)\n        : sharedType.getAttribute(property);\n\n    if (prevValue !== nextValue) {\n      if (nextValue instanceof Doc) {\n        const yjsDocMap = binding.docMap;\n\n        if (prevValue instanceof Doc) {\n          yjsDocMap.delete(prevValue.guid);\n        }\n\n        const nestedEditor = createEditor();\n        const key = nextValue.guid;\n        nestedEditor._key = key;\n        yjsDocMap.set(key, nextValue);\n\n        nextValue = nestedEditor;\n      }\n\n      if (writableNode === undefined) {\n        writableNode = lexicalNode.getWritable();\n      }\n\n      writableNode[property as keyof typeof writableNode] = nextValue;\n    }\n  }\n}\n\nexport function syncPropertiesFromLexical(\n  binding: Binding,\n  sharedType: XmlText | YMap<unknown> | XmlElement,\n  prevLexicalNode: null | LexicalNode,\n  nextLexicalNode: LexicalNode,\n): void {\n  const type = nextLexicalNode.__type;\n  const nodeProperties = binding.nodeProperties;\n  let properties = nodeProperties.get(type);\n  if (properties === undefined) {\n    properties = Object.keys(nextLexicalNode).filter((property) => {\n      return !isExcludedProperty(property, nextLexicalNode, binding);\n    });\n    nodeProperties.set(type, properties);\n  }\n\n  const EditorClass = binding.editor.constructor;\n\n  for (let i = 0; i < properties.length; i++) {\n    const property = properties[i];\n    const prevValue =\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      prevLexicalNode === null ? undefined : (prevLexicalNode as any)[property];\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    let nextValue = (nextLexicalNode as any)[property];\n\n    if (prevValue !== nextValue) {\n      if (nextValue instanceof EditorClass) {\n        const yjsDocMap = binding.docMap;\n        let prevDoc;\n\n        if (prevValue instanceof EditorClass) {\n          const prevKey = prevValue._key;\n          prevDoc = yjsDocMap.get(prevKey);\n          yjsDocMap.delete(prevKey);\n        }\n\n        // If we already have a document, use it.\n        const doc = prevDoc || new Doc();\n        const key = doc.guid;\n        nextValue._key = key;\n        yjsDocMap.set(key, doc);\n        nextValue = doc;\n        // Mark the node dirty as we've assigned a new key to it\n        binding.editor.update(() => {\n          nextLexicalNode.markDirty();\n        });\n      }\n\n      if (sharedType instanceof YMap) {\n        sharedType.set(property, nextValue);\n      } else {\n        sharedType.setAttribute(property, nextValue);\n      }\n    }\n  }\n}\n\nexport function spliceString(\n  str: string,\n  index: number,\n  delCount: number,\n  newText: string,\n): string {\n  return str.slice(0, index) + newText + str.slice(index + delCount);\n}\n\nexport function getPositionFromElementAndOffset(\n  node: CollabElementNode,\n  offset: number,\n  boundaryIsEdge: boolean,\n): {\n  length: number;\n  node:\n    | CollabElementNode\n    | CollabTextNode\n    | CollabDecoratorNode\n    | CollabLineBreakNode\n    | null;\n  nodeIndex: number;\n  offset: number;\n} {\n  let index = 0;\n  let i = 0;\n  const children = node._children;\n  const childrenLength = children.length;\n\n  for (; i < childrenLength; i++) {\n    const child = children[i];\n    const childOffset = index;\n    const size = child.getSize();\n    index += size;\n    const exceedsBoundary = boundaryIsEdge ? index >= offset : index > offset;\n\n    if (exceedsBoundary && child instanceof CollabTextNode) {\n      let textOffset = offset - childOffset - 1;\n\n      if (textOffset < 0) {\n        textOffset = 0;\n      }\n\n      const diffLength = index - offset;\n      return {\n        length: diffLength,\n        node: child,\n        nodeIndex: i,\n        offset: textOffset,\n      };\n    }\n\n    if (index > offset) {\n      return {\n        length: 0,\n        node: child,\n        nodeIndex: i,\n        offset: childOffset,\n      };\n    } else if (i === childrenLength - 1) {\n      return {\n        length: 0,\n        node: null,\n        nodeIndex: i + 1,\n        offset: childOffset + 1,\n      };\n    }\n  }\n\n  return {\n    length: 0,\n    node: null,\n    nodeIndex: 0,\n    offset: 0,\n  };\n}\n\nexport function doesSelectionNeedRecovering(\n  selection: RangeSelection,\n): boolean {\n  const anchor = selection.anchor;\n  const focus = selection.focus;\n  let recoveryNeeded = false;\n\n  try {\n    const anchorNode = anchor.getNode();\n    const focusNode = focus.getNode();\n\n    if (\n      // We might have removed a node that no longer exists\n      !anchorNode.isAttached() ||\n      !focusNode.isAttached() ||\n      // If we've split a node, then the offset might not be right\n      ($isTextNode(anchorNode) &&\n        anchor.offset > anchorNode.getTextContentSize()) ||\n      ($isTextNode(focusNode) && focus.offset > focusNode.getTextContentSize())\n    ) {\n      recoveryNeeded = true;\n    }\n  } catch (e) {\n    // Sometimes checking nor a node via getNode might trigger\n    // an error, so we need recovery then too.\n    recoveryNeeded = true;\n  }\n\n  return recoveryNeeded;\n}\n\nexport function syncWithTransaction(binding: Binding, fn: () => void): void {\n  binding.doc.transact(fn, binding);\n}\n\nexport function removeFromParent(node: LexicalNode): void {\n  const oldParent = node.getParent();\n  if (oldParent !== null) {\n    const writableNode = node.getWritable();\n    const writableParent = oldParent.getWritable();\n    const prevSibling = node.getPreviousSibling();\n    const nextSibling = node.getNextSibling();\n    // TODO: this function duplicates a bunch of operations, can be simplified.\n    if (prevSibling === null) {\n      if (nextSibling !== null) {\n        const writableNextSibling = nextSibling.getWritable();\n        writableParent.__first = nextSibling.__key;\n        writableNextSibling.__prev = null;\n      } else {\n        writableParent.__first = null;\n      }\n    } else {\n      const writablePrevSibling = prevSibling.getWritable();\n      if (nextSibling !== null) {\n        const writableNextSibling = nextSibling.getWritable();\n        writableNextSibling.__prev = writablePrevSibling.__key;\n        writablePrevSibling.__next = writableNextSibling.__key;\n      } else {\n        writablePrevSibling.__next = null;\n      }\n      writableNode.__prev = null;\n    }\n    if (nextSibling === null) {\n      if (prevSibling !== null) {\n        const writablePrevSibling = prevSibling.getWritable();\n        writableParent.__last = prevSibling.__key;\n        writablePrevSibling.__next = null;\n      } else {\n        writableParent.__last = null;\n      }\n    } else {\n      const writableNextSibling = nextSibling.getWritable();\n      if (prevSibling !== null) {\n        const writablePrevSibling = prevSibling.getWritable();\n        writablePrevSibling.__next = writableNextSibling.__key;\n        writableNextSibling.__prev = writablePrevSibling.__key;\n      } else {\n        writableNextSibling.__prev = null;\n      }\n      writableNode.__next = null;\n    }\n    writableParent.__size--;\n    writableNode.__parent = null;\n  }\n}\n\nexport function $moveSelectionToPreviousNode(\n  anchorNodeKey: string,\n  currentEditorState: EditorState,\n) {\n  const anchorNode = currentEditorState._nodeMap.get(anchorNodeKey);\n  if (!anchorNode) {\n    $getRoot().selectStart();\n    return;\n  }\n  // Get previous node\n  const prevNodeKey = anchorNode.__prev;\n  let prevNode: ElementNode | null = null;\n  if (prevNodeKey) {\n    prevNode = $getNodeByKey(prevNodeKey);\n  }\n\n  // If previous node not found, get parent node\n  if (prevNode === null && anchorNode.__parent !== null) {\n    prevNode = $getNodeByKey(anchorNode.__parent);\n  }\n  if (prevNode === null) {\n    $getRoot().selectStart();\n    return;\n  }\n\n  if (prevNode !== null && prevNode.isAttached()) {\n    prevNode.selectEnd();\n    return;\n  } else {\n    // If the found node is also deleted, select the next one\n    $moveSelectionToPreviousNode(prevNode.__key, currentEditorState);\n  }\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/yjs/index.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type {Binding} from './Bindings';\nimport type {LexicalCommand} from 'lexical';\nimport type {Doc, RelativePosition, UndoManager, XmlText} from 'yjs';\n\nimport {createCommand} from 'lexical';\nimport {UndoManager as YjsUndoManager} from 'yjs';\n\nexport type UserState = {\n  anchorPos: null | RelativePosition;\n  color: string;\n  focusing: boolean;\n  focusPos: null | RelativePosition;\n  name: string;\n  awarenessData: object;\n};\nexport const CONNECTED_COMMAND: LexicalCommand<boolean> =\n  createCommand('CONNECTED_COMMAND');\nexport const TOGGLE_CONNECT_COMMAND: LexicalCommand<boolean> = createCommand(\n  'TOGGLE_CONNECT_COMMAND',\n);\nexport type ProviderAwareness = {\n  getLocalState: () => UserState | null;\n  getStates: () => Map<number, UserState>;\n  off: (type: 'update', cb: () => void) => void;\n  on: (type: 'update', cb: () => void) => void;\n  setLocalState: (arg0: UserState) => void;\n};\ndeclare interface Provider {\n  awareness: ProviderAwareness;\n  connect(): void | Promise<void>;\n  disconnect(): void;\n  off(type: 'sync', cb: (isSynced: boolean) => void): void;\n  off(type: 'update', cb: (arg0: unknown) => void): void;\n  off(type: 'status', cb: (arg0: {status: string}) => void): void;\n  off(type: 'reload', cb: (doc: Doc) => void): void;\n  on(type: 'sync', cb: (isSynced: boolean) => void): void;\n  on(type: 'status', cb: (arg0: {status: string}) => void): void;\n  on(type: 'update', cb: (arg0: unknown) => void): void;\n  on(type: 'reload', cb: (doc: Doc) => void): void;\n}\nexport type Operation = {\n  attributes: {\n    __type: string;\n  };\n  insert: string | Record<string, unknown>;\n};\nexport type Delta = Array<Operation>;\nexport type YjsNode = Record<string, unknown>;\nexport type YjsEvent = Record<string, unknown>;\nexport type {Provider};\nexport type {Binding, ClientID, ExcludedProperties} from './Bindings';\nexport {createBinding} from './Bindings';\n\nexport function createUndoManager(\n  binding: Binding,\n  root: XmlText,\n): UndoManager {\n  return new YjsUndoManager(root, {\n    trackedOrigins: new Set([binding, null]),\n  });\n}\n\nexport function initLocalState(\n  provider: Provider,\n  name: string,\n  color: string,\n  focusing: boolean,\n  awarenessData: object,\n): void {\n  provider.awareness.setLocalState({\n    anchorPos: null,\n    awarenessData,\n    color,\n    focusPos: null,\n    focusing: focusing,\n    name,\n  });\n}\n\nexport function setLocalStateFocus(\n  provider: Provider,\n  name: string,\n  color: string,\n  focusing: boolean,\n  awarenessData: object,\n): void {\n  const {awareness} = provider;\n  let localState = awareness.getLocalState();\n\n  if (localState === null) {\n    localState = {\n      anchorPos: null,\n      awarenessData,\n      color,\n      focusPos: null,\n      focusing: focusing,\n      name,\n    };\n  }\n\n  localState.focusing = focusing;\n  awareness.setLocalState(localState);\n}\nexport {syncCursorPositions} from './SyncCursors';\nexport {\n  syncLexicalUpdateToYjs,\n  syncYjsChangesToLexical,\n} from './SyncEditorStates';\n"
  },
  {
    "path": "resources/js/wysiwyg/lexical/yjs/types.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {CollabDecoratorNode} from './src/CollabDecoratorNode';\nimport {CollabElementNode} from './src/CollabElementNode';\nimport {CollabLineBreakNode} from './src/CollabLineBreakNode';\nimport {CollabTextNode} from './src/CollabTextNode';\n\ndeclare module 'yjs' {\n  interface XmlElement {\n    _collabNode: CollabDecoratorNode;\n  }\n\n  interface XmlText {\n    _collabNode: CollabElementNode;\n  }\n\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  interface Map<MapType> {\n    _collabNode: CollabLineBreakNode | CollabTextNode;\n  }\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/nodes.ts",
    "content": "import {CalloutNode} from '@lexical/rich-text/LexicalCalloutNode';\nimport {\n    ElementNode,\n    KlassConstructor,\n    LexicalNode,\n    LexicalNodeReplacement, NodeMutation,\n    ParagraphNode\n} from \"lexical\";\nimport {LinkNode} from \"@lexical/link\";\nimport {ImageNode} from \"@lexical/rich-text/LexicalImageNode\";\nimport {DetailsNode} from \"@lexical/rich-text/LexicalDetailsNode\";\nimport {ListItemNode, ListNode} from \"@lexical/list\";\nimport {TableCellNode, TableNode, TableRowNode} from \"@lexical/table\";\nimport {HorizontalRuleNode} from \"@lexical/rich-text/LexicalHorizontalRuleNode\";\nimport {CodeBlockNode} from \"@lexical/rich-text/LexicalCodeBlockNode\";\nimport {DiagramNode} from \"@lexical/rich-text/LexicalDiagramNode\";\nimport {EditorUiContext} from \"./ui/framework/core\";\nimport {MediaNode} from \"@lexical/rich-text/LexicalMediaNode\";\nimport {HeadingNode} from \"@lexical/rich-text/LexicalHeadingNode\";\nimport {QuoteNode} from \"@lexical/rich-text/LexicalQuoteNode\";\nimport {CaptionNode} from \"@lexical/table/LexicalCaptionNode\";\nimport {MentionNode} from \"@lexical/link/LexicalMentionNode\";\n\nexport function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> | LexicalNodeReplacement)[] {\n    return [\n        CalloutNode,\n        HeadingNode,\n        QuoteNode,\n        ListNode,\n        ListItemNode,\n        TableNode,\n        TableRowNode,\n        TableCellNode,\n        CaptionNode,\n        ImageNode, // TODO - Alignment\n        HorizontalRuleNode,\n        DetailsNode,\n        CodeBlockNode,\n        DiagramNode,\n        MediaNode, // TODO - Alignment\n        ParagraphNode,\n        LinkNode,\n    ];\n}\n\nexport function getNodesForBasicEditor(): (KlassConstructor<typeof LexicalNode> | LexicalNodeReplacement)[] {\n    return [\n        ListNode,\n        ListItemNode,\n        ParagraphNode,\n        LinkNode,\n    ];\n}\n\nexport function getNodesForCommentEditor(): (KlassConstructor<typeof LexicalNode> | LexicalNodeReplacement)[] {\n    return [\n        ...getNodesForBasicEditor(),\n        MentionNode,\n    ];\n}\n\nexport function registerCommonNodeMutationListeners(context: EditorUiContext): void {\n    const decorated = [ImageNode, CodeBlockNode, DiagramNode];\n\n    const decorationDestroyListener = (mutations: Map<string, NodeMutation>): void => {\n        for (let [nodeKey, mutation] of mutations) {\n            if (mutation === \"destroyed\") {\n                const decorator = context.manager.getDecoratorByNodeKey(nodeKey);\n                if (decorator) {\n                    decorator.teardown();\n                }\n            }\n        }\n    };\n\n    for (let decoratedNode of decorated) {\n        // Have to pass a unique function here since they are stored by lexical keyed on listener function.\n        context.editor.registerMutationListener(decoratedNode, (mutations) => decorationDestroyListener(mutations));\n    }\n}\n\nexport type LexicalNodeMatcher = (node: LexicalNode|null|undefined) => boolean;\nexport type LexicalElementNodeCreator = () => ElementNode;"
  },
  {
    "path": "resources/js/wysiwyg/services/__tests__/auto-links.test.ts",
    "content": "import {\n    createTestContext,\n    dispatchKeydownEventForNode, expectEditorStateJSONPropToEqual,\n    expectNodeShapeToMatch\n} from \"lexical/__tests__/utils\";\nimport {\n    $getRoot,\n    ParagraphNode,\n    TextNode\n} from \"lexical\";\nimport {registerAutoLinks} from \"../auto-links\";\n\ndescribe('Auto-link service tests', () => {\n    test('space after link in text', async () => {\n        const {editor} = createTestContext();\n        registerAutoLinks(editor);\n        let pNode!: ParagraphNode;\n\n        editor.updateAndCommit(() => {\n            pNode = new ParagraphNode();\n            const text = new TextNode('Some https://example.com?test=true text');\n            pNode.append(text);\n            $getRoot().append(pNode);\n\n            text.select(34, 34);\n        });\n\n        dispatchKeydownEventForNode(pNode, editor, ' ');\n\n        expectEditorStateJSONPropToEqual(editor, '0.1.url', 'https://example.com?test=true');\n        expectEditorStateJSONPropToEqual(editor, '0.1.0.text', 'https://example.com?test=true');\n    });\n\n    test('space after link at end of line', async () => {\n        const {editor} = createTestContext();\n        registerAutoLinks(editor);\n        let pNode!: ParagraphNode;\n\n        editor.updateAndCommit(() => {\n            pNode = new ParagraphNode();\n            const text = new TextNode('Some https://example.com?test=true');\n            pNode.append(text);\n            $getRoot().append(pNode);\n\n            text.selectEnd();\n        });\n\n        dispatchKeydownEventForNode(pNode, editor, ' ');\n\n        expectNodeShapeToMatch(editor, [{type: 'paragraph', children: [\n                {text: 'Some '},\n                {type: 'link', children: [{text: 'https://example.com?test=true'}]}\n            ]}]);\n        expectEditorStateJSONPropToEqual(editor, '0.1.url', 'https://example.com?test=true');\n    });\n\n    test('enter after link in text', async () => {\n        const {editor} = createTestContext();\n        registerAutoLinks(editor);\n        let pNode!: ParagraphNode;\n\n        editor.updateAndCommit(() => {\n            pNode = new ParagraphNode();\n            const text = new TextNode('Some https://example.com?test=true text');\n            pNode.append(text);\n            $getRoot().append(pNode);\n\n            text.select(34, 34);\n        });\n\n        dispatchKeydownEventForNode(pNode, editor, 'Enter');\n\n        expectEditorStateJSONPropToEqual(editor, '0.1.url', 'https://example.com?test=true');\n        expectEditorStateJSONPropToEqual(editor, '0.1.0.text', 'https://example.com?test=true');\n    });\n});"
  },
  {
    "path": "resources/js/wysiwyg/services/__tests__/keyboard-handling.test.ts",
    "content": "import {\n    createTestContext, destroyFromContext,\n    dispatchKeydownEventForNode,\n    dispatchKeydownEventForSelectedNode, expectNodeShapeToMatch,\n} from \"lexical/__tests__/utils\";\nimport {\n    $createParagraphNode, $createTextNode,\n    $getRoot, $getSelection, LexicalEditor, LexicalNode,\n    ParagraphNode, TextNode,\n} from \"lexical\";\nimport {$createDetailsNode, DetailsNode} from \"@lexical/rich-text/LexicalDetailsNode\";\nimport {registerKeyboardHandling} from \"../keyboard-handling\";\nimport {registerRichText} from \"@lexical/rich-text\";\nimport {EditorUiContext} from \"../../ui/framework/core\";\nimport {$createListItemNode, $createListNode, ListItemNode, ListNode} from \"@lexical/list\";\nimport {$createImageNode, ImageNode} from \"@lexical/rich-text/LexicalImageNode\";\n\ndescribe('Keyboard-handling service tests', () => {\n\n    let context!: EditorUiContext;\n    let editor!: LexicalEditor;\n\n    beforeEach(() => {\n        context = createTestContext();\n        editor = context.editor;\n        registerRichText(editor);\n        registerKeyboardHandling(context);\n    });\n\n    afterEach(() => {\n        destroyFromContext(context);\n    });\n\n    test('Details: down key on last lines creates new sibling node', () => {\n        let lastRootChild!: LexicalNode|null;\n        let detailsPara!: ParagraphNode;\n\n        editor.updateAndCommit(() => {\n            const root = $getRoot()\n            const details = $createDetailsNode();\n            detailsPara = $createParagraphNode();\n            details.append(detailsPara);\n            $getRoot().append(details);\n            detailsPara.select();\n\n            lastRootChild = root.getLastChild();\n        });\n\n        expect(lastRootChild).toBeInstanceOf(DetailsNode);\n\n        dispatchKeydownEventForNode(detailsPara, editor, 'ArrowDown');\n\n        editor.getEditorState().read(() => {\n            lastRootChild = $getRoot().getLastChild();\n        });\n\n        expect(lastRootChild).toBeInstanceOf(ParagraphNode);\n    });\n\n    test('Details: enter on last empty block creates new sibling node', () => {\n        registerRichText(editor);\n\n        let lastRootChild!: LexicalNode|null;\n        let detailsPara!: ParagraphNode;\n\n        editor.updateAndCommit(() => {\n            const root = $getRoot()\n            const details = $createDetailsNode();\n            const text = $createTextNode('Hello!');\n            detailsPara = $createParagraphNode();\n            detailsPara.append(text);\n            details.append(detailsPara);\n            $getRoot().append(details);\n            text.selectEnd();\n\n            lastRootChild = root.getLastChild();\n        });\n\n        expect(lastRootChild).toBeInstanceOf(DetailsNode);\n\n        dispatchKeydownEventForNode(detailsPara, editor, 'Enter');\n        dispatchKeydownEventForSelectedNode(editor, 'Enter');\n\n        let detailsChildren!: LexicalNode[];\n        let lastDetailsText!: string;\n\n        editor.getEditorState().read(() => {\n            detailsChildren = (lastRootChild as DetailsNode).getChildren();\n            lastRootChild = $getRoot().getLastChild();\n            lastDetailsText = detailsChildren[0].getTextContent();\n        });\n\n        expect(lastRootChild).toBeInstanceOf(ParagraphNode);\n        expect(detailsChildren).toHaveLength(1);\n        expect(lastDetailsText).toBe('Hello!');\n    });\n\n    test('Lists: tab on empty list item insets item', () => {\n\n        let list!: ListNode;\n        let listItemB!: ListItemNode;\n\n        editor.updateAndCommit(() => {\n            const root = $getRoot();\n            list = $createListNode('bullet');\n            const listItemA = $createListItemNode();\n            listItemA.append($createTextNode('Hello!'));\n            listItemB = $createListItemNode();\n            list.append(listItemA, listItemB);\n            root.append(list);\n            listItemB.selectStart();\n        });\n\n        dispatchKeydownEventForNode(listItemB, editor, 'Tab');\n\n        editor.getEditorState().read(() => {\n            const list = $getRoot().getChildren()[0] as ListNode;\n            const listChild = list.getChildren()[0] as ListItemNode;\n            const children = listChild.getChildren();\n            expect(children).toHaveLength(2);\n            expect(children[0]).toBeInstanceOf(TextNode);\n            expect(children[0].getTextContent()).toBe('Hello!');\n            expect(children[1]).toBeInstanceOf(ListNode);\n\n            const innerList = children[1] as ListNode;\n            const selectedNode = $getSelection()?.getNodes()[0];\n            expect(selectedNode).toBeInstanceOf(ListItemNode);\n            expect(selectedNode?.getKey()).toBe(innerList.getChildren()[0].getKey());\n        });\n    });\n\n    test('Images: up on selected image creates new paragraph if none above', () => {\n        let image!: ImageNode;\n        editor.updateAndCommit(() => {\n            const root = $getRoot();\n            const imageWrap = $createParagraphNode();\n            image = $createImageNode('https://example.com/cat.png');\n            imageWrap.append(image);\n            root.append(imageWrap);\n            image.select();\n        });\n\n        expectNodeShapeToMatch(editor, [{\n            type: 'paragraph',\n            children: [\n                {type: 'image'}\n            ],\n        }]);\n\n        dispatchKeydownEventForNode(image, editor, 'ArrowUp');\n\n        expectNodeShapeToMatch(editor, [{\n            type: 'paragraph',\n        }, {\n            type: 'paragraph',\n            children: [\n                {type: 'image'}\n            ],\n        }]);\n    });\n});"
  },
  {
    "path": "resources/js/wysiwyg/services/__tests__/mouse-handling.test.ts",
    "content": "import {\n    createTestContext, destroyFromContext, dispatchEditorMouseClick,\n} from \"lexical/__tests__/utils\";\nimport {\n    $getRoot, LexicalEditor, LexicalNode,\n    ParagraphNode,\n} from \"lexical\";\nimport {registerRichText} from \"@lexical/rich-text\";\nimport {EditorUiContext} from \"../../ui/framework/core\";\nimport {registerMouseHandling} from \"../mouse-handling\";\nimport {$createTableNode, TableNode} from \"@lexical/table\";\n\ndescribe('Mouse-handling service tests', () => {\n\n    let context!: EditorUiContext;\n    let editor!: LexicalEditor;\n\n    beforeEach(() => {\n        context = createTestContext();\n        editor = context.editor;\n        registerRichText(editor);\n        registerMouseHandling(context);\n    });\n\n    afterEach(() => {\n        destroyFromContext(context);\n    });\n\n    test('Click below last table inserts new empty paragraph', () => {\n        let tableNode!: TableNode;\n        let lastRootChild!: LexicalNode|null;\n\n        editor.updateAndCommit(() => {\n            tableNode = $createTableNode();\n            $getRoot().append(tableNode);\n            lastRootChild = $getRoot().getLastChild();\n        });\n\n        expect(lastRootChild).toBeInstanceOf(TableNode);\n\n        const tableDOM = editor.getElementByKey(tableNode.getKey());\n        const rect = tableDOM?.getBoundingClientRect();\n        dispatchEditorMouseClick(editor, 0, (rect?.bottom || 0) + 1)\n\n        editor.getEditorState().read(() => {\n            lastRootChild = $getRoot().getLastChild();\n        });\n\n        expect(lastRootChild).toBeInstanceOf(ParagraphNode);\n    });\n});"
  },
  {
    "path": "resources/js/wysiwyg/services/auto-links.ts",
    "content": "import {\n    $getSelection, BaseSelection,\n    COMMAND_PRIORITY_NORMAL,\n    KEY_ENTER_COMMAND,\n    KEY_SPACE_COMMAND,\n    LexicalEditor,\n    TextNode\n} from \"lexical\";\nimport {$getTextNodeFromSelection} from \"../utils/selection\";\nimport {$createLinkNode, LinkNode} from \"@lexical/link\";\n\n\nfunction isLinkText(text: string): boolean {\n    const lower = text.toLowerCase();\n    if (!lower.startsWith('http')) {\n        return false;\n    }\n\n    const linkRegex = /(http|https):\\/\\/(\\S+)\\.\\S+$/;\n    return linkRegex.test(text);\n}\n\n\nfunction handlePotentialLinkEvent(node: TextNode, selection: BaseSelection, editor: LexicalEditor) {\n    const selectionRange = selection.getStartEndPoints();\n    if (!selectionRange) {\n        return;\n    }\n\n    const cursorPoint = selectionRange[0].offset;\n    const nodeText = node.getTextContent();\n    const rTrimText = nodeText.slice(0, cursorPoint);\n    const priorSpaceIndex = rTrimText.lastIndexOf(' ');\n    const startIndex = priorSpaceIndex + 1;\n    const textSegment = nodeText.slice(startIndex, cursorPoint);\n\n    if (!isLinkText(textSegment)) {\n        return;\n    }\n\n    editor.update(() => {\n        const linkNode: LinkNode = $createLinkNode(textSegment);\n        linkNode.append(new TextNode(textSegment));\n\n        const splits = node.splitText(startIndex, cursorPoint);\n        const targetIndex = startIndex > 0 ? 1 : 0;\n        const targetText = splits[targetIndex];\n        if (targetText) {\n            targetText.replace(linkNode);\n        }\n    });\n}\n\n\nexport function registerAutoLinks(editor: LexicalEditor): () => void {\n\n    const handler = (payload: KeyboardEvent): boolean => {\n        const selection = $getSelection();\n        const textNode = $getTextNodeFromSelection(selection);\n        if (textNode && selection) {\n            handlePotentialLinkEvent(textNode, selection, editor);\n        }\n\n        return false;\n    };\n\n    const unregisterSpace = editor.registerCommand(KEY_SPACE_COMMAND, handler, COMMAND_PRIORITY_NORMAL);\n    const unregisterEnter = editor.registerCommand(KEY_ENTER_COMMAND, handler, COMMAND_PRIORITY_NORMAL);\n\n    return (): void => {\n        unregisterSpace();\n        unregisterEnter();\n    };\n}"
  },
  {
    "path": "resources/js/wysiwyg/services/common-events.ts",
    "content": "import {LexicalEditor} from \"lexical\";\nimport {\n    appendHtmlToEditor,\n    focusEditor,\n    insertHtmlIntoEditor,\n    prependHtmlToEditor,\n    setEditorContentFromHtml\n} from \"../utils/actions\";\n\ntype EditorEventContent = {\n    html: string;\n    markdown: string;\n};\n\nfunction getContentToInsert(eventContent: EditorEventContent): string {\n    return eventContent.html || '';\n}\n\nexport function listen(editor: LexicalEditor): void {\n    window.$events.listen<EditorEventContent>('editor::replace', eventContent => {\n        const html = getContentToInsert(eventContent);\n        setEditorContentFromHtml(editor, html);\n    });\n\n    window.$events.listen<EditorEventContent>('editor::append', eventContent => {\n        const html = getContentToInsert(eventContent);\n        appendHtmlToEditor(editor, html);\n    });\n\n    window.$events.listen<EditorEventContent>('editor::prepend', eventContent => {\n        const html = getContentToInsert(eventContent);\n        prependHtmlToEditor(editor, html);\n    });\n\n    window.$events.listen<EditorEventContent>('editor::insert', eventContent => {\n        const html = getContentToInsert(eventContent);\n        insertHtmlIntoEditor(editor, html);\n    });\n\n    window.$events.listen<EditorEventContent>('editor::focus', () => {\n        focusEditor(editor);\n    });\n\n    let changeFromLoading = true;\n    editor.registerUpdateListener(({dirtyElements, dirtyLeaves, editorState, prevEditorState}) => {\n        // Emit change event to component system (for draft detection) on actual user content change\n        if (dirtyElements.size > 0 || dirtyLeaves.size > 0) {\n            if (changeFromLoading) {\n                changeFromLoading = false;\n            } else {\n                window.$events.emit('editor-html-change', '');\n            }\n        }\n    });\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/services/drop-paste-handling.ts",
    "content": "import {\n    $createParagraphNode,\n    $insertNodes,\n    $isDecoratorNode, COMMAND_PRIORITY_HIGH, DROP_COMMAND,\n    LexicalEditor,\n    LexicalNode, PASTE_COMMAND\n} from \"lexical\";\nimport {$insertNewBlockNodesAtSelection, $selectSingleNode} from \"../utils/selection\";\nimport {$getNearestBlockNodeForCoords, $htmlToBlockNodes} from \"../utils/nodes\";\nimport {Clipboard} from \"../../services/clipboard\";\nimport {$createImageNode} from \"@lexical/rich-text/LexicalImageNode\";\nimport {$createLinkNode} from \"@lexical/link\";\nimport {EditorImageData, uploadImageFile} from \"../utils/images\";\nimport {EditorUiContext} from \"../ui/framework/core\";\n\nfunction $getNodeFromMouseEvent(event: MouseEvent, editor: LexicalEditor): LexicalNode|null {\n    const x = event.clientX;\n    const y = event.clientY;\n    const dom = document.elementFromPoint(x, y);\n    if (!dom) {\n        return null;\n    }\n\n    return $getNearestBlockNodeForCoords(editor, event.clientX, event.clientY);\n}\n\nfunction $insertNodesAtEvent(nodes: LexicalNode[], event: DragEvent, editor: LexicalEditor) {\n    const positionNode = $getNodeFromMouseEvent(event, editor);\n\n    if (positionNode) {\n        $selectSingleNode(positionNode);\n    }\n\n    $insertNewBlockNodesAtSelection(nodes, true);\n\n    if (!$isDecoratorNode(positionNode) || !positionNode?.getTextContent()) {\n        positionNode?.remove();\n    }\n}\n\nasync function insertTemplateToEditor(editor: LexicalEditor, templateId: string, event: DragEvent) {\n    const resp = await window.$http.get(`/templates/${templateId}`);\n    const data = (resp.data || {html: ''}) as {html: string}\n    const html: string = data.html || '';\n\n    editor.update(() => {\n        const newNodes = $htmlToBlockNodes(editor, html);\n        $insertNodesAtEvent(newNodes, event, editor);\n    });\n}\n\nfunction handleMediaInsert(data: DataTransfer, context: EditorUiContext): boolean {\n    const clipboard = new Clipboard(data);\n    let handled = false;\n\n    // Don't handle the event ourselves if no items exist of contains table-looking data\n    if (!clipboard.hasItems() || clipboard.containsTabularData()) {\n        return handled;\n    }\n\n    const images = clipboard.getImages();\n    if (images.length > 0) {\n        handled = true;\n    }\n\n    context.editor.update(async () => {\n        for (const imageFile of images) {\n            const loadingImage = window.baseUrl('/loading.gif');\n            const loadingNode = $createImageNode(loadingImage);\n            const imageWrap = $createParagraphNode();\n            imageWrap.append(loadingNode);\n            $insertNodes([imageWrap]);\n\n            try {\n                const respData: EditorImageData = await uploadImageFile(imageFile, context.options.pageId);\n                const safeName = respData.name.replace(/\"/g, '');\n                context.editor.update(() => {\n                    const finalImage = $createImageNode(respData.thumbs?.display || '', {\n                        alt: safeName,\n                    });\n                    const imageLink = $createLinkNode(respData.url, {target: '_blank'});\n                    imageLink.append(finalImage);\n                    loadingNode.replace(imageLink);\n                });\n            } catch (err: any) {\n                context.editor.update(() => {\n                    loadingNode.remove(false);\n                });\n                window.$events.error(err?.data?.message || context.options.translations.imageUploadErrorText);\n                console.error(err);\n            }\n        }\n    });\n\n    return handled;\n}\n\nfunction handleImageLinkInsert(data: DataTransfer, context: EditorUiContext): boolean {\n    const regex = /https?:\\/\\/([^?#]*?)\\.(png|jpeg|jpg|gif|webp|bmp|avif)/i\n    const text = data.getData('text/plain');\n    if (text && regex.test(text)) {\n        context.editor.update(() => {\n            const image = $createImageNode(text);\n            $insertNodes([image]);\n            image.select();\n        });\n        return true;\n    }\n\n    return false;\n}\n\nfunction createDropListener(context: EditorUiContext): (event: DragEvent) => boolean {\n    const editor = context.editor;\n    return (event: DragEvent): boolean => {\n        // Template handling\n        const templateId = event.dataTransfer?.getData('bookstack/template') || '';\n        if (templateId) {\n            insertTemplateToEditor(editor, templateId, event);\n            event.preventDefault();\n            event.stopPropagation();\n            return true;\n        }\n\n        // HTML contents drop\n        const html = event.dataTransfer?.getData('text/html') || '';\n        if (html) {\n            editor.update(() => {\n                const newNodes = $htmlToBlockNodes(editor, html);\n                $insertNodesAtEvent(newNodes, event, editor);\n            });\n            event.preventDefault();\n            event.stopPropagation();\n            return true;\n        }\n\n        if (event.dataTransfer) {\n            const handled = handleMediaInsert(event.dataTransfer, context);\n            if (handled) {\n                event.preventDefault();\n                event.stopPropagation();\n                return true;\n            }\n        }\n\n        return false;\n    };\n}\n\nfunction createPasteListener(context: EditorUiContext): (event: ClipboardEvent) => boolean {\n    return (event: ClipboardEvent) => {\n        if (!event.clipboardData) {\n            return false;\n        }\n\n        const handled =\n            handleImageLinkInsert(event.clipboardData, context) ||\n            handleMediaInsert(event.clipboardData, context);\n\n        if (handled) {\n            event.preventDefault();\n        }\n\n        return handled;\n    };\n}\n\nexport function registerDropPasteHandling(context: EditorUiContext): () => void {\n    const dropListener = createDropListener(context);\n    const pasteListener = createPasteListener(context);\n\n    const unregisterDrop = context.editor.registerCommand(DROP_COMMAND, dropListener, COMMAND_PRIORITY_HIGH);\n    const unregisterPaste = context.editor.registerCommand(PASTE_COMMAND, pasteListener, COMMAND_PRIORITY_HIGH);\n    context.scrollDOM.addEventListener('drop', dropListener);\n\n    return () => {\n        unregisterDrop();\n        unregisterPaste();\n        context.scrollDOM.removeEventListener('drop', dropListener);\n    };\n}"
  },
  {
    "path": "resources/js/wysiwyg/services/keyboard-handling.ts",
    "content": "import {EditorUiContext} from \"../ui/framework/core\";\nimport {\n    $createParagraphNode,\n    $getSelection,\n    $isDecoratorNode,\n    COMMAND_PRIORITY_LOW, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND,\n    KEY_BACKSPACE_COMMAND,\n    KEY_DELETE_COMMAND,\n    KEY_ENTER_COMMAND, KEY_TAB_COMMAND,\n    LexicalEditor,\n    LexicalNode\n} from \"lexical\";\nimport {$isImageNode} from \"@lexical/rich-text/LexicalImageNode\";\nimport {$isMediaNode} from \"@lexical/rich-text/LexicalMediaNode\";\nimport {getLastSelection} from \"../utils/selection\";\nimport {$getNearestNodeBlockParent, $getParentOfType, $selectOrCreateAdjacent} from \"../utils/nodes\";\nimport {$setInsetForSelection} from \"../utils/lists\";\nimport {$isListItemNode} from \"@lexical/list\";\nimport {$isDetailsNode, DetailsNode} from \"@lexical/rich-text/LexicalDetailsNode\";\nimport {$isDiagramNode} from \"../utils/diagrams\";\nimport {$unwrapDetailsNode} from \"../utils/details\";\n\nfunction isSingleSelectedNode(nodes: LexicalNode[]): boolean {\n    if (nodes.length === 1) {\n        const node = nodes[0];\n        if ($isDecoratorNode(node) || $isImageNode(node) || $isMediaNode(node) || $isDiagramNode(node)) {\n            return true;\n        }\n    }\n\n    return false;\n}\n\n/**\n * Delete the current node in the selection if the selection contains a single\n * selected node (like image, media etc...).\n */\nfunction deleteSingleSelectedNode(editor: LexicalEditor) {\n    const selectionNodes = getLastSelection(editor)?.getNodes() || [];\n    if (isSingleSelectedNode(selectionNodes)) {\n        editor.update(() => {\n            selectionNodes[0].remove();\n        });\n    }\n}\n\n/**\n * Insert a new empty node before/after the selection if the selection contains a single\n * selected node (like image, media etc...).\n */\nfunction insertAdjacentToSingleSelectedNode(editor: LexicalEditor, event: KeyboardEvent|null): boolean {\n    const selectionNodes = getLastSelection(editor)?.getNodes() || [];\n    if (isSingleSelectedNode(selectionNodes)) {\n        const node = selectionNodes[0];\n        const nearestBlock = $getNearestNodeBlockParent(node) || node;\n        const insertBefore = event?.shiftKey === true;\n        if (nearestBlock) {\n            requestAnimationFrame(() => {\n                editor.update(() => {\n                    const newParagraph = $createParagraphNode();\n                    if (insertBefore) {\n                        nearestBlock.insertBefore(newParagraph);\n                    } else {\n                        nearestBlock.insertAfter(newParagraph);\n                    }\n                    newParagraph.select();\n                });\n            });\n            event?.preventDefault();\n            return true;\n        }\n    }\n\n    return false;\n}\n\nfunction focusAdjacentOrInsertForSingleSelectNode(editor: LexicalEditor, event: KeyboardEvent|null, after: boolean = true): boolean {\n    const selectionNodes = getLastSelection(editor)?.getNodes() || [];\n    if (!isSingleSelectedNode(selectionNodes)) {\n        return false;\n    }\n\n    event?.preventDefault();\n    const node = selectionNodes[0];\n    editor.update(() => {\n        $selectOrCreateAdjacent(node, after);\n    });\n\n    return true;\n}\n\n/**\n * Insert a new node after a details node, if inside a details node that's\n * the last element, and if the cursor is at the last block within the details node.\n */\nfunction insertAfterDetails(editor: LexicalEditor, event: KeyboardEvent|null): boolean {\n    const scenario = getDetailsScenario(editor);\n    if (scenario === null || scenario.detailsSibling) {\n        return false;\n    }\n\n    editor.update(() => {\n        const newParagraph = $createParagraphNode();\n        scenario.parentDetails.insertAfter(newParagraph);\n        newParagraph.select();\n    });\n    event?.preventDefault();\n\n    return true;\n}\n\n/**\n * If within a details block, move after it, creating a new node if required, if we're on\n * the last empty block element within the details node.\n */\nfunction moveAfterDetailsOnEmptyLine(editor: LexicalEditor, event: KeyboardEvent|null): boolean {\n    const scenario = getDetailsScenario(editor);\n    if (scenario === null) {\n        return false;\n    }\n\n    if (scenario.parentBlock.getTextContent() !== '') {\n        return false;\n    }\n\n    event?.preventDefault()\n\n    const nextSibling = scenario.parentDetails.getNextSibling();\n    editor.update(() => {\n        if (nextSibling) {\n            nextSibling.selectStart();\n        } else {\n            const newParagraph = $createParagraphNode();\n            scenario.parentDetails.insertAfter(newParagraph);\n            newParagraph.select();\n        }\n        scenario.parentBlock.remove();\n    });\n\n    return true;\n}\n\n/**\n * Get the common nodes used for a details node scenario, relative to current selection.\n * Returns null if not found, or if the parent block is not the last in the parent details node.\n */\nfunction getDetailsScenario(editor: LexicalEditor): {\n    parentDetails: DetailsNode;\n    parentBlock: LexicalNode;\n    detailsSibling: LexicalNode | null\n} | null {\n    const selection = getLastSelection(editor);\n    const firstNode = selection?.getNodes()[0];\n    if (!firstNode) {\n        return null;\n    }\n\n    const block = $getNearestNodeBlockParent(firstNode);\n    const details = $getParentOfType(firstNode, $isDetailsNode);\n    if (!$isDetailsNode(details) || block === null) {\n        return null;\n    }\n\n    if (block.getKey() !== details.getLastChild()?.getKey()) {\n        return null;\n    }\n\n    const nextSibling = details.getNextSibling();\n    return {\n        parentDetails: details,\n        parentBlock: block,\n        detailsSibling: nextSibling,\n    }\n}\n\nfunction unwrapDetailsNode(context: EditorUiContext, event: KeyboardEvent): boolean {\n    const selection = $getSelection();\n    const nodes = selection?.getNodes() || [];\n\n    if (nodes.length !== 1) {\n        return false;\n    }\n\n    const selectedNearestBlock = $getNearestNodeBlockParent(nodes[0]);\n    if (!selectedNearestBlock) {\n        return false;\n    }\n\n    const selectedParentBlock = selectedNearestBlock.getParent();\n    const selectRange = selection?.getStartEndPoints();\n\n    if (selectRange && $isDetailsNode(selectedParentBlock) && selectRange[0].offset === 0 && selectedNearestBlock.getIndexWithinParent() === 0) {\n        event.preventDefault();\n        context.editor.update(() => {\n            $unwrapDetailsNode(selectedParentBlock);\n            selectedNearestBlock.selectStart();\n            context.manager.triggerLayoutUpdate();\n        });\n        return true;\n    }\n\n    return false;\n}\n\nfunction $isSingleListItem(nodes: LexicalNode[]): boolean {\n    if (nodes.length !== 1) {\n        return false;\n    }\n\n    const node = nodes[0];\n    return $isListItemNode(node) || $isListItemNode(node.getParent());\n}\n\n/**\n * Inset the nodes within selection when a range of nodes is selected\n * or if a list node is selected.\n */\nfunction handleInsetOnTab(editor: LexicalEditor, event: KeyboardEvent|null): boolean {\n    const change = event?.shiftKey ? -40 : 40;\n    const selection = $getSelection();\n    const nodes = selection?.getNodes() || [];\n    if (nodes.length > 1 || $isSingleListItem(nodes)) {\n        editor.update(() => {\n            $setInsetForSelection(editor, change);\n        });\n        event?.preventDefault();\n        return true;\n    }\n\n    return false;\n}\n\nexport function registerKeyboardHandling(context: EditorUiContext): () => void {\n    const unregisterBackspace = context.editor.registerCommand(KEY_BACKSPACE_COMMAND, (event): boolean => {\n        deleteSingleSelectedNode(context.editor);\n        return unwrapDetailsNode(context, event);\n    }, COMMAND_PRIORITY_LOW);\n\n    const unregisterDelete = context.editor.registerCommand(KEY_DELETE_COMMAND, (): boolean => {\n        deleteSingleSelectedNode(context.editor);\n        return false;\n    }, COMMAND_PRIORITY_LOW);\n\n    const unregisterEnter = context.editor.registerCommand(KEY_ENTER_COMMAND, (event): boolean => {\n        return insertAdjacentToSingleSelectedNode(context.editor, event)\n            || moveAfterDetailsOnEmptyLine(context.editor, event);\n    }, COMMAND_PRIORITY_LOW);\n\n    const unregisterTab = context.editor.registerCommand(KEY_TAB_COMMAND, (event): boolean => {\n        return handleInsetOnTab(context.editor, event);\n    }, COMMAND_PRIORITY_LOW);\n\n    const unregisterUp = context.editor.registerCommand(KEY_ARROW_UP_COMMAND, (event): boolean => {\n        return focusAdjacentOrInsertForSingleSelectNode(context.editor, event, false);\n    }, COMMAND_PRIORITY_LOW);\n\n    const unregisterDown = context.editor.registerCommand(KEY_ARROW_DOWN_COMMAND, (event): boolean => {\n        return insertAfterDetails(context.editor, event)\n            || focusAdjacentOrInsertForSingleSelectNode(context.editor, event, true)\n    }, COMMAND_PRIORITY_LOW);\n\n    return () => {\n        unregisterBackspace();\n        unregisterDelete();\n        unregisterEnter();\n        unregisterTab();\n        unregisterUp();\n        unregisterDown();\n    };\n}"
  },
  {
    "path": "resources/js/wysiwyg/services/mentions.ts",
    "content": "import {\n    $getSelection, $isRangeSelection,\n    COMMAND_PRIORITY_NORMAL, KEY_ENTER_COMMAND, RangeSelection, TextNode\n} from \"lexical\";\nimport {KEY_AT_COMMAND} from \"lexical/LexicalCommands\";\nimport {$createMentionNode, $isMentionNode, MentionNode} from \"@lexical/link/LexicalMentionNode\";\nimport {EditorUiContext} from \"../ui/framework/core\";\nimport {MentionDecorator} from \"../ui/decorators/MentionDecorator\";\nimport {$selectSingleNode} from \"../utils/selection\";\n\n\nfunction enterUserSelectMode(context: EditorUiContext, selection: RangeSelection) {\n    const textNode = selection.getNodes()[0] as TextNode;\n    const selectionPos = selection.getStartEndPoints();\n    if (!selectionPos) {\n        return;\n    }\n\n    const offset = selectionPos[0].offset;\n\n    // Ignore if the @ sign is not after a space or the start of the line\n    const atStart = offset === 0;\n    const afterSpace = textNode.getTextContent().charAt(offset - 1) === ' ';\n    if (!atStart && !afterSpace) {\n        return;\n    }\n\n    const split = textNode.splitText(offset);\n    const priorTextNode = split[0];\n    const afterTextNode = split[atStart ? 0 : 1];\n\n    const mention = $createMentionNode(0, '', '');\n    priorTextNode.insertAfter(mention);\n    afterTextNode.spliceText(0, 1, '', false);\n    $selectSingleNode(mention);\n\n    requestAnimationFrame(() => {\n        const mentionDecorator = context.manager.getDecoratorByNodeKey(mention.getKey());\n        if (mentionDecorator instanceof MentionDecorator) {\n            mentionDecorator.showSelection()\n        }\n    });\n}\n\nfunction selectMention(context: EditorUiContext, event: KeyboardEvent): boolean {\n    const selected = $getSelection()?.getNodes() || [];\n    if (selected.length === 1 && $isMentionNode(selected[0])) {\n        const mention = selected[0] as MentionNode;\n        const decorator = context.manager.getDecoratorByNodeKey(mention.getKey()) as MentionDecorator;\n        decorator.showSelection();\n        event.preventDefault();\n        event.stopPropagation();\n        return true;\n    }\n\n    return false;\n}\n\nexport function registerMentions(context: EditorUiContext): () => void {\n    const editor = context.editor;\n\n    const unregisterCommand = editor.registerCommand(KEY_AT_COMMAND, function (event: KeyboardEvent): boolean {\n        const selection = $getSelection();\n        if ($isRangeSelection(selection) && selection.isCollapsed()) {\n            window.setTimeout(() => {\n                editor.update(() => {\n                    enterUserSelectMode(context, selection);\n                });\n            }, 1);\n        }\n        return false;\n    }, COMMAND_PRIORITY_NORMAL);\n\n    const unregisterEnter = editor.registerCommand(KEY_ENTER_COMMAND, function (event: KeyboardEvent): boolean {\n        return selectMention(context, event);\n    }, COMMAND_PRIORITY_NORMAL);\n\n    return (): void => {\n        unregisterCommand();\n        unregisterEnter();\n    };\n}"
  },
  {
    "path": "resources/js/wysiwyg/services/mouse-handling.ts",
    "content": "import {EditorUiContext} from \"../ui/framework/core\";\nimport {\n    $createParagraphNode, $getNearestNodeFromDOMNode, $getRoot,\n    $isDecoratorNode, CLICK_COMMAND,\n    COMMAND_PRIORITY_LOW, ElementNode,\n    LexicalNode\n} from \"lexical\";\nimport {$isImageNode} from \"@lexical/rich-text/LexicalImageNode\";\nimport {$isMediaNode} from \"@lexical/rich-text/LexicalMediaNode\";\nimport {$isDiagramNode} from \"../utils/diagrams\";\nimport {$isTableNode} from \"@lexical/table\";\nimport {$isDetailsNode} from \"@lexical/rich-text/LexicalDetailsNode\";\n\nfunction isHardToEscapeNode(node: LexicalNode): boolean {\n    return $isDecoratorNode(node)\n        || $isImageNode(node)\n        || $isMediaNode(node)\n        || $isDiagramNode(node)\n        || $isTableNode(node)\n        || $isDetailsNode(node);\n}\n\nfunction $getContextNode(event: MouseEvent): ElementNode {\n    if (event.target instanceof HTMLElement) {\n        const nearestDetails = event.target.closest('details');\n        if (nearestDetails) {\n            const detailsNode = $getNearestNodeFromDOMNode(nearestDetails);\n            if ($isDetailsNode(detailsNode)) {\n                return detailsNode;\n            }\n        }\n    }\n    return $getRoot();\n}\n\nfunction insertBelowLastNode(context: EditorUiContext, event: MouseEvent): boolean {\n    const contextNode = $getContextNode(event);\n    const lastNode = contextNode.getLastChild();\n    if (!lastNode || !isHardToEscapeNode(lastNode)) {\n        return false;\n    }\n\n    const lastNodeDom = context.editor.getElementByKey(lastNode.getKey());\n    if (!lastNodeDom) {\n        return false;\n    }\n\n    const nodeBounds = lastNodeDom.getBoundingClientRect();\n    const isClickBelow = event.clientY > nodeBounds.bottom;\n    if (isClickBelow) {\n        context.editor.update(() => {\n            const newNode = $createParagraphNode();\n            contextNode.append(newNode);\n            newNode.select();\n        });\n        return true;\n    }\n\n    return false;\n}\n\nexport function registerMouseHandling(context: EditorUiContext): () => void {\n    const unregisterClick = context.editor.registerCommand(CLICK_COMMAND, (event): boolean => {\n        insertBelowLastNode(context, event);\n        return false;\n    }, COMMAND_PRIORITY_LOW);\n\n\n    return () => {\n        unregisterClick();\n    };\n}"
  },
  {
    "path": "resources/js/wysiwyg/services/selection-handling.ts",
    "content": "import {EditorUiContext} from \"../ui/framework/core\";\nimport {\n    $getSelection,\n    COMMAND_PRIORITY_LOW,\n    SELECTION_CHANGE_COMMAND\n} from \"lexical\";\nimport {$isDetailsNode} from \"@lexical/rich-text/LexicalDetailsNode\";\n\n\nconst trackedDomNodes = new Set<HTMLElement>();\n\n/**\n * Set a selection indicator on nodes which require it.\n * @param context\n */\nfunction setSelectionIndicator(context: EditorUiContext): boolean {\n\n    for (const domNode of trackedDomNodes) {\n        domNode.classList.remove('selected');\n        trackedDomNodes.delete(domNode);\n    }\n\n    const selection = $getSelection();\n    const nodes = selection?.getNodes() || [];\n\n    if (nodes.length === 1) {\n        if ($isDetailsNode(nodes[0])) {\n            const domEl = context.editor.getElementByKey(nodes[0].getKey());\n            if (domEl) {\n                domEl.classList.add('selected');\n                trackedDomNodes.add(domEl);\n            }\n        }\n    }\n\n    return false;\n}\n\nexport function registerSelectionHandling(context: EditorUiContext): () => void {\n    const unregisterSelectionChange = context.editor.registerCommand(SELECTION_CHANGE_COMMAND, (): boolean => {\n        setSelectionIndicator(context);\n        return false;\n    }, COMMAND_PRIORITY_LOW);\n\n\n    return () => {\n        unregisterSelectionChange();\n    };\n}"
  },
  {
    "path": "resources/js/wysiwyg/services/shortcuts.ts",
    "content": "import {$getSelection, COMMAND_PRIORITY_HIGH, FORMAT_TEXT_COMMAND, KEY_ENTER_COMMAND, LexicalEditor} from \"lexical\";\nimport {\n    cycleSelectionCalloutFormats,\n    formatCodeBlock, insertOrUpdateLink,\n    toggleSelectionAsBlockquote,\n    toggleSelectionAsHeading, toggleSelectionAsList,\n    toggleSelectionAsParagraph\n} from \"../utils/formats\";\nimport {EditorUiContext} from \"../ui/framework/core\";\nimport {$getNodeFromSelection} from \"../utils/selection\";\nimport {$isLinkNode, LinkNode} from \"@lexical/link\";\nimport {$showLinkForm} from \"../ui/defaults/forms/objects\";\nimport {showLinkSelector} from \"../utils/links\";\nimport {HeadingTagType} from \"@lexical/rich-text/LexicalHeadingNode\";\n\nfunction headerHandler(context: EditorUiContext, tag: HeadingTagType): boolean {\n    toggleSelectionAsHeading(context.editor, tag);\n    context.manager.triggerFutureStateRefresh();\n    return true;\n}\n\nfunction wrapFormatAction(formatAction: (editor: LexicalEditor) => any): ShortcutAction {\n    return (editor: LexicalEditor, context: EditorUiContext) => {\n        formatAction(editor);\n        context.manager.triggerFutureStateRefresh();\n        return true;\n    };\n}\n\nfunction toggleInlineCode(editor: LexicalEditor): boolean {\n    editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');\n    return true;\n}\n\ntype ShortcutAction = (editor: LexicalEditor, context: EditorUiContext) => boolean;\n\n/**\n * List of action functions by their shortcut combo.\n * We use \"meta\" as an abstraction for ctrl/cmd depending on platform.\n */\nconst actionsByKeys: Record<string, ShortcutAction> = {\n    'meta+s': () => {\n        window.$events.emit('editor-save-draft');\n        return true;\n    },\n    'meta+enter': () => {\n        window.$events.emit('editor-save-page');\n        return true;\n    },\n    'meta+1': (editor, context) => headerHandler(context, 'h2'),\n    'meta+2': (editor, context) => headerHandler(context, 'h3'),\n    'meta+3': (editor, context) => headerHandler(context, 'h4'),\n    'meta+4': (editor, context) => headerHandler(context, 'h5'),\n    'meta+5': wrapFormatAction(toggleSelectionAsParagraph),\n    'meta+d': wrapFormatAction(toggleSelectionAsParagraph),\n    'meta+6': wrapFormatAction(toggleSelectionAsBlockquote),\n    'meta+q': wrapFormatAction(toggleSelectionAsBlockquote),\n    'meta+7': wrapFormatAction(formatCodeBlock),\n    'meta+e': wrapFormatAction(formatCodeBlock),\n    'meta+8': toggleInlineCode,\n    'meta+shift+e': toggleInlineCode,\n    'meta+9': wrapFormatAction(cycleSelectionCalloutFormats),\n\n    'meta+o': wrapFormatAction((e) => toggleSelectionAsList(e, 'number')),\n    'meta+p': wrapFormatAction((e) => toggleSelectionAsList(e, 'bullet')),\n    'meta+k': (editor, context) => {\n        editor.getEditorState().read(() => {\n            const selectedLink = $getNodeFromSelection($getSelection(), $isLinkNode) as LinkNode | null;\n            $showLinkForm(selectedLink, context);\n        });\n        return true;\n    },\n    'meta+shift+k': (editor, context) => {\n        editor.getEditorState().read(() => {\n            const selection = $getSelection();\n            const selectionText = selection?.getTextContent() || '';\n            showLinkSelector(entity => {\n                insertOrUpdateLink(editor, {\n                    text: entity.name,\n                    title: entity.link,\n                    target: '',\n                    url: entity.link,\n                });\n            }, selectionText);\n        });\n        return true;\n    },\n};\n\nfunction createKeyDownListener(context: EditorUiContext): (e: KeyboardEvent) => void {\n    return (event: KeyboardEvent) => {\n        const combo = keyboardEventToKeyComboString(event);\n        // console.log(`pressed: ${combo}`);\n        if (actionsByKeys[combo]) {\n            const handled = actionsByKeys[combo](context.editor, context);\n            if (handled) {\n                event.stopPropagation();\n                event.preventDefault();\n            }\n        }\n    };\n}\n\nfunction keyboardEventToKeyComboString(event: KeyboardEvent): string {\n    const metaKeyPressed = isMac() ? event.metaKey : event.ctrlKey;\n\n    const parts = [\n        metaKeyPressed ? 'meta' : '',\n        event.shiftKey ? 'shift' : '',\n        event.key,\n    ];\n\n    return parts.filter(Boolean).join('+').toLowerCase();\n}\n\nfunction isMac(): boolean {\n    return window.navigator.userAgent.includes('Mac OS X');\n}\n\nfunction overrideDefaultCommands(editor: LexicalEditor) {\n    // Prevent default ctrl+enter command\n    editor.registerCommand(KEY_ENTER_COMMAND, (event) => {\n        if (isMac()) {\n            return event?.metaKey || false;\n        }\n        return event?.ctrlKey || false;\n    }, COMMAND_PRIORITY_HIGH);\n}\n\nexport function registerShortcuts(context: EditorUiContext) {\n    const listener = createKeyDownListener(context);\n    overrideDefaultCommands(context.editor);\n\n    return context.editor.registerRootListener((rootElement: null | HTMLElement, prevRootElement: null | HTMLElement) => {\n        // add the listener to the current root element\n        rootElement?.addEventListener('keydown', listener);\n        // remove the listener from the old root element\n        prevRootElement?.removeEventListener('keydown', listener);\n    });\n}"
  },
  {
    "path": "resources/js/wysiwyg/testing.md",
    "content": "# Testing Guidance\n\nThis is testing guidance specific for this Lexical-based WYSIWYG editor.\nThere is a lot of pre-existing test code carried over form the fork of lexical, but since there we've added a range of helpers and altered how testing can be done to make things a bit simpler and aligned with how we run tests.\n\nThis document is an attempt to document the new best options for added tests with an aim for standardisation on these approaches going forward.\n\n## Utils Location\n\nMost core test utils can be found in the file at path: resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts\n\n## Test Example\n\nThis is an example of a typical test using the common modern utilities to help perform actions or assertions. Comments are for this example only, and are not expected in actual test files.\n\n```ts\nimport {\n    createTestContext,\n    dispatchKeydownEventForNode, \n    expectEditorStateJSONPropToEqual,\n    expectNodeShapeToMatch\n} from \"lexical/__tests__/utils\";\nimport {\n    $getRoot,\n    ParagraphNode,\n    TextNode\n} from \"lexical\";\n\ndescribe('A specific service or file or function', () => {\n    test('it does thing', async () => {\n        // Create the editor context and get an editor reference\n        const {editor} = createTestContext();\n\n        // Run an action within the editor.\n        let pNode: ParagraphNode;\n        editor.updateAndCommit(() => {\n            pNode = new ParagraphNode();\n            const text = new TextNode('Hello!');\n            pNode.append(text);\n            $getRoot().append(pNode);\n        });\n\n        // Dispatch key events via the DOM\n        dispatchKeydownEventForNode(pNode!, editor, ' ');\n\n        // Check the shape (and text) of the resulting state\n        expectNodeShapeToMatch(editor, [{type: 'paragraph', children: [\n                {text: 'Hello!'},\n            ]}]);\n\n        // Check specific props in the resulting JSON state\n        expectEditorStateJSONPropToEqual(editor, '0.0.text', 'Hello!');\n    });\n});\n```"
  },
  {
    "path": "resources/js/wysiwyg/ui/decorators/CodeBlockDecorator.ts",
    "content": "import {EditorDecorator} from \"../framework/decorator\";\nimport {EditorUiContext} from \"../framework/core\";\nimport {$openCodeEditorForNode, CodeBlockNode} from \"@lexical/rich-text/LexicalCodeBlockNode\";\nimport {BaseSelection} from \"lexical\";\nimport {$selectionContainsNode, $selectSingleNode} from \"../../utils/selection\";\n\n\nexport class CodeBlockDecorator extends EditorDecorator {\n\n    protected completedSetup: boolean = false;\n    protected latestCode: string = '';\n    protected latestLanguage: string = '';\n\n    // @ts-ignore\n    protected editor: any = null;\n\n    setup(element: HTMLElement) {\n        const codeNode = this.getNode() as CodeBlockNode;\n        const preEl = element.querySelector('pre');\n        if (!preEl) {\n            return;\n        }\n\n        if (preEl) {\n            preEl.hidden = true;\n        }\n\n        this.latestCode = codeNode.__code;\n        this.latestLanguage = codeNode.__language;\n        const lines = this.latestCode.split('\\n').length;\n        const height = (lines * 19.2) + 18 + 24;\n        element.style.height = `${height}px`;\n\n        const startTime = Date.now();\n\n        element.addEventListener('click', event => {\n            requestAnimationFrame(() => {\n                this.context.editor.update(() => {\n                    $selectSingleNode(this.getNode());\n                });\n            });\n        });\n\n        element.addEventListener('dblclick', event => {\n            this.context.editor.getEditorState().read(() => {\n                $openCodeEditorForNode(this.context.editor, (this.getNode() as CodeBlockNode));\n            });\n        });\n\n        const selectionChange = (selection: BaseSelection|null): void => {\n            element.classList.toggle('selected', $selectionContainsNode(selection, codeNode));\n        };\n        this.context.manager.onSelectionChange(selectionChange);\n        this.onDestroy(() => {\n            this.context.manager.offSelectionChange(selectionChange);\n        });\n\n        // @ts-ignore\n        const renderEditor = (Code) => {\n            this.editor = Code.wysiwygView(element, document, this.latestCode, this.latestLanguage);\n            setTimeout(() => {\n                element.style.height = '';\n            }, 12);\n        };\n\n        // @ts-ignore\n        window.importVersioned('code').then((Code) => {\n            const timeout = (Date.now() - startTime < 20) ? 20 : 0;\n            setTimeout(() => renderEditor(Code), timeout);\n        });\n\n        this.completedSetup = true;\n    }\n\n    update() {\n        const codeNode = this.getNode() as CodeBlockNode;\n        const code = codeNode.getCode();\n        const language = codeNode.getLanguage();\n\n        if (this.latestCode === code && this.latestLanguage === language) {\n            return;\n        }\n        this.latestLanguage = language;\n        this.latestCode = code;\n\n        if (this.editor) {\n            this.editor.setContent(code);\n            this.editor.setMode(language, code);\n        }\n    }\n\n    render(element: HTMLElement): void {\n        if (this.completedSetup) {\n            this.update();\n        } else {\n            this.setup(element);\n        }\n    }\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/decorators/DiagramDecorator.ts",
    "content": "import {EditorDecorator} from \"../framework/decorator\";\nimport {EditorUiContext} from \"../framework/core\";\nimport {BaseSelection, CLICK_COMMAND, COMMAND_PRIORITY_NORMAL} from \"lexical\";\nimport {DiagramNode} from \"@lexical/rich-text/LexicalDiagramNode\";\nimport {$selectionContainsNode, $selectSingleNode} from \"../../utils/selection\";\nimport {$openDrawingEditorForNode} from \"../../utils/diagrams\";\n\n\nexport class DiagramDecorator extends EditorDecorator {\n    protected completedSetup: boolean = false;\n\n    setup(element: HTMLElement) {\n        const diagramNode = this.getNode();\n        element.classList.add('editor-diagram');\n\n        this.context.editor.registerCommand(CLICK_COMMAND, (event: MouseEvent): boolean => {\n            if (!element.contains(event.target as HTMLElement)) {\n                return false;\n            }\n\n            this.context.editor.update(() => {\n                $selectSingleNode(this.getNode());\n            });\n            return true;\n        }, COMMAND_PRIORITY_NORMAL);\n\n        element.addEventListener('dblclick', event => {\n            this.context.editor.getEditorState().read(() => {\n                $openDrawingEditorForNode(this.context, (this.getNode() as DiagramNode));\n            });\n        });\n\n        const selectionChange = (selection: BaseSelection|null): void => {\n            element.classList.toggle('selected', $selectionContainsNode(selection, diagramNode));\n        };\n        this.context.manager.onSelectionChange(selectionChange);\n        this.onDestroy(() => {\n            this.context.manager.offSelectionChange(selectionChange);\n        });\n\n        this.completedSetup = true;\n    }\n\n    update() {\n        //\n    }\n\n    render(element: HTMLElement): void {\n        if (this.completedSetup) {\n            this.update();\n        } else {\n            this.setup(element);\n        }\n    }\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/decorators/MentionDecorator.ts",
    "content": "import {EditorDecorator} from \"../framework/decorator\";\nimport {EditorUiContext} from \"../framework/core\";\nimport {el, htmlToDom} from \"../../utils/dom\";\nimport {showLoading} from \"../../../services/dom\";\nimport {MentionNode} from \"@lexical/link/LexicalMentionNode\";\nimport {debounce} from \"../../../services/util\";\nimport {$createTextNode} from \"lexical\";\nimport {KeyboardNavigationHandler} from \"../../../services/keyboard-navigation\";\n\nimport searchIcon from \"@icons/search.svg\";\n\nfunction userClickHandler(onSelect: (id: number, name: string, slug: string)=>void): (event: PointerEvent) => void {\n    return (event: PointerEvent) => {\n        const userItem = (event.target as HTMLElement).closest('a[data-id]') as HTMLAnchorElement | null;\n        if (!userItem) {\n            return;\n        }\n\n        const id = Number(userItem.dataset.id || '0');\n        const name = userItem.dataset.name || '';\n        const slug = userItem.dataset.slug || '';\n\n        onSelect(id, name, slug);\n        event.preventDefault();\n    };\n}\n\nfunction handleUserSelectCancel(context: EditorUiContext, selectList: HTMLElement, controller: AbortController, onCancel: () => void): void {\n    selectList.addEventListener('keydown', (event) => {\n        if (event.key === 'Escape') {\n            onCancel();\n        }\n    }, {signal: controller.signal});\n\n    const input = selectList.querySelector('input') as HTMLInputElement;\n    input.addEventListener('keydown', (event) => {\n        if (event.key === 'Backspace' && input.value === '') {\n            onCancel();\n            event.preventDefault();\n            event.stopPropagation();\n        }\n    }, {signal: controller.signal});\n\n    context.editorDOM.addEventListener('click', (event) => {\n        onCancel()\n    }, {signal: controller.signal});\n    context.editorDOM.addEventListener('keydown', (event) => {\n        onCancel();\n    }, {signal: controller.signal});\n}\n\nfunction handleUserListLoading(selectList: HTMLElement) {\n    const cache = new Map<string, string>();\n\n    const updateUserList = async (searchTerm: string) => {\n        // Empty list\n        for (const child of [...selectList.children]) {\n            child.remove();\n        }\n\n        // Fetch new content\n        let responseHtml = '';\n        if (cache.has(searchTerm)) {\n            responseHtml = cache.get(searchTerm) || '';\n        } else {\n            const loadingWrap = el('div', {class: 'flex-container-row items-center dropdown-search-item'});\n            showLoading(loadingWrap);\n            selectList.appendChild(loadingWrap);\n\n            const resp = await window.$http.get(`/search/users/mention?search=${searchTerm}`);\n            responseHtml = resp.data as string;\n            cache.set(searchTerm, responseHtml);\n            loadingWrap.remove();\n        }\n\n        const doc = htmlToDom(responseHtml);\n        const toInsert = [...doc.body.children];\n        for (const listEl of toInsert) {\n            const adopted = window.document.adoptNode(listEl) as HTMLElement;\n            selectList.appendChild(adopted);\n        }\n    };\n\n    // Initial load\n    updateUserList('');\n\n    const input = selectList.parentElement?.querySelector('input') as HTMLInputElement;\n    const updateUserListDebounced = debounce(updateUserList, 200, false);\n    input.addEventListener('input', () => {\n        const searchTerm = input.value;\n        updateUserListDebounced(searchTerm);\n    });\n}\n\nfunction buildAndShowUserSelectorAtElement(context: EditorUiContext, mentionDOM: HTMLElement): HTMLElement {\n    const searchInput = el('input', {type: 'text'});\n    const list = el('div', {class: 'dropdown-search-list'});\n    const iconWrap = el('div');\n    iconWrap.innerHTML = searchIcon;\n    const icon = iconWrap.children[0] as HTMLElement;\n    icon.classList.add('svg-icon');\n    const userSelect = el('div', {class: 'dropdown-search-dropdown compact card'}, [\n        el('div', {class: 'dropdown-search-search'}, [icon, searchInput]),\n        list,\n    ]);\n\n    context.containerDOM.appendChild(userSelect);\n\n    userSelect.style.display = 'block';\n    userSelect.style.top = '0';\n    userSelect.style.left = '0';\n    const mentionPos = mentionDOM.getBoundingClientRect();\n    const userSelectPos = userSelect.getBoundingClientRect();\n    userSelect.style.top = `${mentionPos.bottom - userSelectPos.top + 3}px`;\n    userSelect.style.left = `${mentionPos.left - userSelectPos.left}px`;\n\n    searchInput.focus();\n\n    return userSelect;\n}\n\nexport class MentionDecorator extends EditorDecorator {\n    protected abortController: AbortController | null = null;\n    protected dropdownContainer: HTMLElement | null = null;\n    protected mentionElement: HTMLElement | null = null;\n\n    setup(element: HTMLElement) {\n        this.mentionElement = element;\n\n        element.addEventListener('click', (event: PointerEvent) => {\n            this.showSelection();\n            event.preventDefault();\n            event.stopPropagation();\n        });\n    }\n\n    showSelection() {\n        if (!this.mentionElement || this.dropdownContainer) {\n            return;\n        }\n\n        this.hideSelection();\n        this.abortController = new AbortController();\n\n        this.dropdownContainer = buildAndShowUserSelectorAtElement(this.context, this.mentionElement);\n        handleUserListLoading(this.dropdownContainer.querySelector('.dropdown-search-list') as HTMLElement);\n\n        this.dropdownContainer.addEventListener('click', userClickHandler((id, name, slug) => {\n            this.context.editor.update(() => {\n                const mentionNode = this.getNode() as MentionNode;\n                this.hideSelection();\n                mentionNode.setUserDetails(id, name, slug);\n                mentionNode.selectNext();\n            });\n        }), {signal: this.abortController.signal});\n\n        handleUserSelectCancel(this.context, this.dropdownContainer, this.abortController, () => {\n            if ((this.getNode() as MentionNode).hasUserSet()) {\n                this.hideSelection()\n            } else {\n                this.revertMention();\n            }\n        });\n\n        new KeyboardNavigationHandler(this.dropdownContainer);\n    }\n\n    hideSelection() {\n        this.abortController?.abort();\n        this.dropdownContainer?.remove();\n        this.abortController = null;\n        this.dropdownContainer = null;\n        this.context.manager.focus();\n    }\n\n    revertMention() {\n        this.hideSelection();\n        this.context.editor.update(() => {\n            const text = $createTextNode('@');\n            const before = this.getNode().getPreviousSibling();\n            this.getNode().replace(text);\n            requestAnimationFrame(() => {\n                this.context.editor.update(() => {\n                    if (text.isAttached()) {\n                        text.selectEnd();\n                    } else if (before?.isAttached()) {\n                        before?.selectEnd();\n                    }\n                });\n            });\n        });\n    }\n\n    render(element: HTMLElement): void {\n        this.setup(element);\n    }\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/defaults/buttons/alignments.ts",
    "content": "import {$isElementNode, BaseSelection} from \"lexical\";\nimport {EditorButtonDefinition} from \"../../framework/buttons\";\nimport alignLeftIcon from \"@icons/editor/align-left.svg\";\nimport {EditorUiContext} from \"../../framework/core\";\nimport alignCenterIcon from \"@icons/editor/align-center.svg\";\nimport alignRightIcon from \"@icons/editor/align-right.svg\";\nimport alignJustifyIcon from \"@icons/editor/align-justify.svg\";\nimport ltrIcon from \"@icons/editor/direction-ltr.svg\";\nimport rtlIcon from \"@icons/editor/direction-rtl.svg\";\nimport {\n    $getBlockElementNodesInSelection,\n    $selectionContainsAlignment, $selectionContainsDirection, $selectSingleNode, getLastSelection\n} from \"../../../utils/selection\";\nimport {CommonBlockAlignment} from \"lexical/nodes/common\";\nimport {nodeHasAlignment} from \"../../../utils/nodes\";\n\n\nfunction setAlignmentForSelection(context: EditorUiContext, alignment: CommonBlockAlignment): void {\n    const selection = getLastSelection(context.editor);\n    const selectionNodes = selection?.getNodes() || [];\n\n    // Handle inline node selection alignment\n    if (selectionNodes.length === 1 && $isElementNode(selectionNodes[0]) && selectionNodes[0].isInline() && nodeHasAlignment(selectionNodes[0])) {\n        selectionNodes[0].setAlignment(alignment);\n        $selectSingleNode(selectionNodes[0]);\n        context.manager.triggerFutureStateRefresh();\n        return;\n    }\n\n    // Handle normal block/range alignment\n    const elements = $getBlockElementNodesInSelection(selection);\n    const alignmentNodes = elements.filter(n => nodeHasAlignment(n));\n    const allAlreadyAligned = alignmentNodes.every(n => n.getAlignment() === alignment);\n    const newAlignment = allAlreadyAligned ? '' : alignment;\n    for (const node of alignmentNodes) {\n        node.setAlignment(newAlignment);\n    }\n\n    context.manager.triggerFutureStateRefresh();\n}\n\nfunction setDirectionForSelection(context: EditorUiContext, direction: 'ltr' | 'rtl'): void {\n    const selection = getLastSelection(context.editor);\n\n    const elements = $getBlockElementNodesInSelection(selection);\n    for (const node of elements) {\n        node.setDirection(direction);\n    }\n\n    context.manager.triggerFutureStateRefresh();\n}\n\nexport const alignLeft: EditorButtonDefinition = {\n    label: 'Align left',\n    icon: alignLeftIcon,\n    action(context: EditorUiContext) {\n        context.editor.update(() => setAlignmentForSelection(context, 'left'));\n    },\n    isActive(selection: BaseSelection|null) {\n        return $selectionContainsAlignment(selection, 'left');\n    }\n};\n\nexport const alignCenter: EditorButtonDefinition = {\n    label: 'Align center',\n    icon: alignCenterIcon,\n    action(context: EditorUiContext) {\n        context.editor.update(() => setAlignmentForSelection(context, 'center'));\n    },\n    isActive(selection: BaseSelection|null) {\n        return $selectionContainsAlignment(selection, 'center');\n    }\n};\n\nexport const alignRight: EditorButtonDefinition = {\n    label: 'Align right',\n    icon: alignRightIcon,\n    action(context: EditorUiContext) {\n        context.editor.update(() => setAlignmentForSelection(context, 'right'));\n    },\n    isActive(selection: BaseSelection|null) {\n        return $selectionContainsAlignment(selection, 'right');\n    }\n};\n\nexport const alignJustify: EditorButtonDefinition = {\n    label: 'Justify',\n    icon: alignJustifyIcon,\n    action(context: EditorUiContext) {\n        context.editor.update(() => setAlignmentForSelection(context, 'justify'));\n    },\n    isActive(selection: BaseSelection|null) {\n        return $selectionContainsAlignment(selection, 'justify');\n    }\n};\n\nexport const directionLTR: EditorButtonDefinition = {\n    label: 'Left to right',\n    icon: ltrIcon,\n    action(context: EditorUiContext) {\n        context.editor.update(() => setDirectionForSelection(context, 'ltr'));\n    },\n    isActive(selection: BaseSelection|null) {\n        return $selectionContainsDirection(selection, 'ltr');\n    }\n};\n\nexport const directionRTL: EditorButtonDefinition = {\n    label: 'Right to left',\n    icon: rtlIcon,\n    action(context: EditorUiContext) {\n        context.editor.update(() => setDirectionForSelection(context, 'rtl'));\n    },\n    isActive(selection: BaseSelection|null) {\n        return $selectionContainsDirection(selection, 'rtl');\n    }\n};"
  },
  {
    "path": "resources/js/wysiwyg/ui/defaults/buttons/block-formats.ts",
    "content": "import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from \"@lexical/rich-text/LexicalCalloutNode\";\nimport {EditorButtonDefinition} from \"../../framework/buttons\";\nimport {EditorUiContext} from \"../../framework/core\";\nimport {$isParagraphNode, BaseSelection, LexicalNode} from \"lexical\";\nimport {$selectionContainsNodeType, $toggleSelectionBlockNodeType} from \"../../../utils/selection\";\nimport {\n    toggleSelectionAsBlockquote,\n    toggleSelectionAsHeading,\n    toggleSelectionAsParagraph\n} from \"../../../utils/formats\";\nimport {$isHeadingNode, HeadingNode, HeadingTagType} from \"@lexical/rich-text/LexicalHeadingNode\";\nimport {$isQuoteNode} from \"@lexical/rich-text/LexicalQuoteNode\";\n\nfunction buildCalloutButton(category: CalloutCategory, name: string): EditorButtonDefinition {\n    return {\n        label: name,\n        action(context: EditorUiContext) {\n            context.editor.update(() => {\n                $toggleSelectionBlockNodeType(\n                    (node) => $isCalloutNodeOfCategory(node, category),\n                    () => $createCalloutNode(category),\n                )\n            });\n        },\n        isActive(selection: BaseSelection|null): boolean {\n            return $selectionContainsNodeType(selection, (node) => $isCalloutNodeOfCategory(node, category));\n        }\n    };\n}\n\nexport const infoCallout: EditorButtonDefinition = buildCalloutButton('info', 'Info');\nexport const dangerCallout: EditorButtonDefinition = buildCalloutButton('danger', 'Danger');\nexport const warningCallout: EditorButtonDefinition = buildCalloutButton('warning', 'Warning');\nexport const successCallout: EditorButtonDefinition = buildCalloutButton('success', 'Success');\n\nconst isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => {\n    return $isHeadingNode(node) && (node as HeadingNode).getTag() === tag;\n};\n\nfunction buildHeaderButton(tag: HeadingTagType, name: string): EditorButtonDefinition {\n    return {\n        label: name,\n        action(context: EditorUiContext) {\n            toggleSelectionAsHeading(context.editor, tag);\n        },\n        isActive(selection: BaseSelection|null): boolean {\n            return $selectionContainsNodeType(selection, (node) => isHeaderNodeOfTag(node, tag));\n        }\n    };\n}\n\nexport const h2: EditorButtonDefinition = buildHeaderButton('h2', 'Large Header');\nexport const h3: EditorButtonDefinition = buildHeaderButton('h3', 'Medium Header');\nexport const h4: EditorButtonDefinition = buildHeaderButton('h4', 'Small Header');\nexport const h5: EditorButtonDefinition = buildHeaderButton('h5', 'Tiny Header');\n\nexport const blockquote: EditorButtonDefinition = {\n    label: 'Blockquote',\n    action(context: EditorUiContext) {\n        toggleSelectionAsBlockquote(context.editor);\n    },\n    isActive(selection: BaseSelection|null): boolean {\n        return $selectionContainsNodeType(selection, $isQuoteNode);\n    }\n};\n\nexport const paragraph: EditorButtonDefinition = {\n    label: 'Paragraph',\n    action(context: EditorUiContext) {\n        toggleSelectionAsParagraph(context.editor);\n    },\n    isActive(selection: BaseSelection|null): boolean {\n        return $selectionContainsNodeType(selection, $isParagraphNode);\n    }\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/defaults/buttons/controls.ts",
    "content": "import {EditorButton, EditorButtonDefinition} from \"../../framework/buttons\";\nimport undoIcon from \"@icons/editor/undo.svg\";\nimport {EditorUiContext} from \"../../framework/core\";\nimport {\n    BaseSelection,\n    CAN_REDO_COMMAND,\n    CAN_UNDO_COMMAND,\n    COMMAND_PRIORITY_LOW,\n    REDO_COMMAND,\n    UNDO_COMMAND\n} from \"lexical\";\nimport redoIcon from \"@icons/editor/redo.svg\";\nimport sourceIcon from \"@icons/editor/source-view.svg\";\nimport fullscreenIcon from \"@icons/editor/fullscreen.svg\";\nimport aboutIcon from \"@icons/editor/about.svg\";\nimport {getEditorContentAsHtml} from \"../../../utils/actions\";\n\nexport const undo: EditorButtonDefinition = {\n    label: 'Undo',\n    icon: undoIcon,\n    action(context: EditorUiContext) {\n        context.editor.dispatchCommand(UNDO_COMMAND, undefined);\n        context.manager.triggerFutureStateRefresh();\n    },\n    isActive(selection: BaseSelection|null): boolean {\n        return false;\n    },\n    setup(context: EditorUiContext, button: EditorButton) {\n        button.toggleDisabled(true);\n\n        context.editor.registerCommand(CAN_UNDO_COMMAND, (payload: boolean): boolean => {\n            button.toggleDisabled(!payload)\n            return false;\n        }, COMMAND_PRIORITY_LOW);\n    }\n}\n\nexport const redo: EditorButtonDefinition = {\n    label: 'Redo',\n    icon: redoIcon,\n    action(context: EditorUiContext) {\n        context.editor.dispatchCommand(REDO_COMMAND, undefined);\n        context.manager.triggerFutureStateRefresh();\n    },\n    isActive(selection: BaseSelection|null): boolean {\n        return false;\n    },\n    setup(context: EditorUiContext, button: EditorButton) {\n        button.toggleDisabled(true);\n\n        context.editor.registerCommand(CAN_REDO_COMMAND, (payload: boolean): boolean => {\n            button.toggleDisabled(!payload)\n            return false;\n        }, COMMAND_PRIORITY_LOW);\n    }\n}\n\n\nexport const source: EditorButtonDefinition = {\n    label: 'Source code',\n    icon: sourceIcon,\n    async action(context: EditorUiContext) {\n        const modal = context.manager.createModal('source');\n        const source = await getEditorContentAsHtml(context.editor);\n        modal.show({source});\n    },\n    isActive() {\n        return false;\n    }\n};\n\nexport const fullscreen: EditorButtonDefinition = {\n    label: 'Fullscreen',\n    icon: fullscreenIcon,\n    async action(context: EditorUiContext, button: EditorButton) {\n        const isFullScreen = context.containerDOM.classList.contains('fullscreen');\n        context.containerDOM.classList.toggle('fullscreen', !isFullScreen);\n        (context.containerDOM.closest('body') as HTMLElement).classList.toggle('editor-is-fullscreen', !isFullScreen);\n        button.setActiveState(!isFullScreen);\n    },\n    isActive(selection, context: EditorUiContext) {\n        return context.containerDOM.classList.contains('fullscreen');\n    }\n};\n\nexport const about: EditorButtonDefinition = {\n    label: 'About the editor',\n    icon: aboutIcon,\n    async action(context: EditorUiContext, button: EditorButton) {\n        const modal = context.manager.createModal('about');\n        modal.show({});\n    },\n    isActive(selection, context: EditorUiContext) {\n        return false;\n    }\n};"
  },
  {
    "path": "resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts",
    "content": "import {$getSelection, $isTextNode, BaseSelection, FORMAT_TEXT_COMMAND, TextFormatType} from \"lexical\";\nimport {EditorBasicButtonDefinition, EditorButtonDefinition} from \"../../framework/buttons\";\nimport {EditorUiContext} from \"../../framework/core\";\nimport boldIcon from \"@icons/editor/bold.svg\";\nimport italicIcon from \"@icons/editor/italic.svg\";\nimport underlinedIcon from \"@icons/editor/underlined.svg\";\nimport textColorIcon from \"@icons/editor/text-color.svg\";\nimport highlightIcon from \"@icons/editor/highlighter.svg\";\nimport strikethroughIcon from \"@icons/editor/strikethrough.svg\";\nimport superscriptIcon from \"@icons/editor/superscript.svg\";\nimport subscriptIcon from \"@icons/editor/subscript.svg\";\nimport codeIcon from \"@icons/editor/code.svg\";\nimport formatClearIcon from \"@icons/editor/format-clear.svg\";\nimport {$selectionContainsTextFormat} from \"../../../utils/selection\";\nimport {$patchStyleText} from \"@lexical/selection\";\n\nfunction buildFormatButton(label: string, format: TextFormatType, icon: string): EditorButtonDefinition {\n    return {\n        label: label,\n        icon,\n        action(context: EditorUiContext) {\n            context.editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);\n        },\n        isActive(selection: BaseSelection|null): boolean {\n            return $selectionContainsTextFormat(selection, format);\n        }\n    };\n}\n\nexport const bold: EditorButtonDefinition = buildFormatButton('Bold', 'bold', boldIcon);\nexport const italic: EditorButtonDefinition = buildFormatButton('Italic', 'italic', italicIcon);\nexport const underline: EditorButtonDefinition = buildFormatButton('Underline', 'underline', underlinedIcon);\nexport const textColor: EditorBasicButtonDefinition = {label: 'Text color', icon: textColorIcon};\nexport const highlightColor: EditorBasicButtonDefinition = {label: 'Highlight color', icon: highlightIcon};\n\nfunction colorAction(context: EditorUiContext, property: string, color: string): void {\n    context.editor.update(() => {\n        const selection = $getSelection();\n        if (selection) {\n            $patchStyleText(selection, {[property]: color || null});\n        }\n    });\n}\n\nexport const textColorAction = (color: string, context: EditorUiContext) => colorAction(context, 'color', color);\nexport const highlightColorAction = (color: string, context: EditorUiContext) => colorAction(context, 'background-color', color);\n\nexport const strikethrough: EditorButtonDefinition = buildFormatButton('Strikethrough', 'strikethrough', strikethroughIcon);\nexport const superscript: EditorButtonDefinition = buildFormatButton('Superscript', 'superscript', superscriptIcon);\nexport const subscript: EditorButtonDefinition = buildFormatButton('Subscript', 'subscript', subscriptIcon);\nexport const code: EditorButtonDefinition = buildFormatButton('Inline code', 'code', codeIcon);\nexport const clearFormating: EditorButtonDefinition = {\n    label: 'Clear formatting',\n    icon: formatClearIcon,\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            const selection = $getSelection();\n            for (const node of selection?.getNodes() || []) {\n                if ($isTextNode(node)) {\n                    node.setFormat(0);\n                    node.setStyle('');\n                }\n            }\n        });\n    },\n    isActive() {\n        return false;\n    }\n};"
  },
  {
    "path": "resources/js/wysiwyg/ui/defaults/buttons/lists.ts",
    "content": "import {$isListNode, ListNode, ListType} from \"@lexical/list\";\nimport {EditorButtonDefinition} from \"../../framework/buttons\";\nimport {EditorUiContext} from \"../../framework/core\";\nimport {\n    BaseSelection,\n    LexicalNode,\n} from \"lexical\";\nimport listBulletIcon from \"@icons/editor/list-bullet.svg\";\nimport listNumberedIcon from \"@icons/editor/list-numbered.svg\";\nimport listCheckIcon from \"@icons/editor/list-check.svg\";\nimport indentIncreaseIcon from \"@icons/editor/indent-increase.svg\";\nimport indentDecreaseIcon from \"@icons/editor/indent-decrease.svg\";\nimport {\n    $selectionContainsNodeType,\n} from \"../../../utils/selection\";\nimport {toggleSelectionAsList} from \"../../../utils/formats\";\nimport {$setInsetForSelection} from \"../../../utils/lists\";\n\n\nfunction buildListButton(label: string, type: ListType, icon: string): EditorButtonDefinition {\n    return {\n        label,\n        icon,\n        action(context: EditorUiContext) {\n            toggleSelectionAsList(context.editor, type);\n        },\n        isActive(selection: BaseSelection|null): boolean {\n            return $selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => {\n                return $isListNode(node) && (node as ListNode).getListType() === type;\n            });\n        }\n    };\n}\n\nexport const bulletList: EditorButtonDefinition = buildListButton('Bullet list', 'bullet', listBulletIcon);\nexport const numberList: EditorButtonDefinition = buildListButton('Numbered list', 'number', listNumberedIcon);\nexport const taskList: EditorButtonDefinition = buildListButton('Task list', 'check', listCheckIcon);\n\nexport const indentIncrease: EditorButtonDefinition = {\n    label: 'Increase indent',\n    icon: indentIncreaseIcon,\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            $setInsetForSelection(context.editor, 40);\n        });\n    },\n    isActive() {\n        return false;\n    }\n};\n\nexport const indentDecrease: EditorButtonDefinition = {\n    label: 'Decrease indent',\n    icon: indentDecreaseIcon,\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            $setInsetForSelection(context.editor, -40);\n        });\n    },\n    isActive() {\n        return false;\n    }\n};"
  },
  {
    "path": "resources/js/wysiwyg/ui/defaults/buttons/objects.ts",
    "content": "import {EditorButtonDefinition} from \"../../framework/buttons\";\nimport linkIcon from \"@icons/editor/link.svg\";\nimport {EditorUiContext} from \"../../framework/core\";\nimport {\n    $getRoot,\n    $getSelection, $insertNodes,\n    BaseSelection,\n    ElementNode\n} from \"lexical\";\nimport {$isLinkNode, LinkNode} from \"@lexical/link\";\nimport unlinkIcon from \"@icons/editor/unlink.svg\";\nimport imageIcon from \"@icons/editor/image.svg\";\nimport {$isImageNode, ImageNode} from \"@lexical/rich-text/LexicalImageNode\";\nimport horizontalRuleIcon from \"@icons/editor/horizontal-rule.svg\";\nimport {$createHorizontalRuleNode, $isHorizontalRuleNode} from \"@lexical/rich-text/LexicalHorizontalRuleNode\";\nimport codeBlockIcon from \"@icons/editor/code-block.svg\";\nimport {$isCodeBlockNode} from \"@lexical/rich-text/LexicalCodeBlockNode\";\nimport editIcon from \"@icons/edit.svg\";\nimport diagramIcon from \"@icons/editor/diagram.svg\";\nimport {$createDiagramNode, DiagramNode} from \"@lexical/rich-text/LexicalDiagramNode\";\nimport detailsIcon from \"@icons/editor/details.svg\";\nimport detailsToggleIcon from \"@icons/editor/details-toggle.svg\";\nimport tableDeleteIcon from \"@icons/editor/table-delete.svg\";\nimport tagIcon from \"@icons/tag.svg\";\nimport mediaIcon from \"@icons/editor/media.svg\";\nimport {$createDetailsNode, $isDetailsNode} from \"@lexical/rich-text/LexicalDetailsNode\";\nimport {$isMediaNode, MediaNode} from \"@lexical/rich-text/LexicalMediaNode\";\nimport {\n    $getNodeFromSelection,\n    $insertNewBlockNodeAtSelection,\n    $selectionContainsNodeType, getLastSelection\n} from \"../../../utils/selection\";\nimport {$isDiagramNode, $openDrawingEditorForNode, showDiagramManagerForInsert} from \"../../../utils/diagrams\";\nimport {$createLinkedImageNodeFromImageData, showImageManager} from \"../../../utils/images\";\nimport {$showDetailsForm, $showImageForm, $showLinkForm, $showMediaForm} from \"../forms/objects\";\nimport {formatCodeBlock} from \"../../../utils/formats\";\nimport {$unwrapDetailsNode} from \"../../../utils/details\";\n\nexport const link: EditorButtonDefinition = {\n    label: 'Insert/edit link',\n    icon: linkIcon,\n    action(context: EditorUiContext) {\n        context.editor.getEditorState().read(() => {\n            const selectedLink = $getNodeFromSelection($getSelection(), $isLinkNode) as LinkNode | null;\n            $showLinkForm(selectedLink, context);\n        });\n    },\n    isActive(selection: BaseSelection | null): boolean {\n        return $selectionContainsNodeType(selection, $isLinkNode);\n    }\n};\n\nexport const unlink: EditorButtonDefinition = {\n    label: 'Remove link',\n    icon: unlinkIcon,\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            const selection = getLastSelection(context.editor);\n            const selectedLink = $getNodeFromSelection(selection, $isLinkNode) as LinkNode | null;\n\n            if (selectedLink) {\n                const contents = selectedLink.getChildren().reverse();\n                for (const child of contents) {\n                    selectedLink.insertAfter(child);\n                }\n                selectedLink.remove();\n\n                contents[contents.length - 1].selectStart();\n\n                context.manager.triggerFutureStateRefresh();\n            }\n        });\n    },\n    isActive(selection: BaseSelection | null): boolean {\n        return false;\n    }\n};\n\n\nexport const image: EditorButtonDefinition = {\n    label: 'Insert/Edit Image',\n    icon: imageIcon,\n    action(context: EditorUiContext) {\n        context.editor.getEditorState().read(() => {\n            const selection = getLastSelection(context.editor);\n            const selectedImage = $getNodeFromSelection(selection, $isImageNode) as ImageNode | null;\n            if (selectedImage) {\n                $showImageForm(selectedImage, context);\n                return;\n            }\n\n            showImageManager((image) => {\n                context.editor.update(() => {\n                    const link = $createLinkedImageNodeFromImageData(image);\n                    $insertNodes([link]);\n                    link.select();\n                });\n            })\n        });\n    },\n    isActive(selection: BaseSelection | null): boolean {\n        return $selectionContainsNodeType(selection, $isImageNode);\n    }\n};\n\nexport const horizontalRule: EditorButtonDefinition = {\n    label: 'Insert horizontal line',\n    icon: horizontalRuleIcon,\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            $insertNewBlockNodeAtSelection($createHorizontalRuleNode(), false);\n        });\n    },\n    isActive(selection: BaseSelection | null): boolean {\n        return $selectionContainsNodeType(selection, $isHorizontalRuleNode);\n    }\n};\n\nexport const codeBlock: EditorButtonDefinition = {\n    label: 'Insert code block',\n    icon: codeBlockIcon,\n    action(context: EditorUiContext) {\n        formatCodeBlock(context.editor);\n    },\n    isActive(selection: BaseSelection | null): boolean {\n        return $selectionContainsNodeType(selection, $isCodeBlockNode);\n    }\n};\n\nexport const editCodeBlock: EditorButtonDefinition = Object.assign({}, codeBlock, {\n    label: 'Edit code block',\n    icon: editIcon,\n});\n\nexport const diagram: EditorButtonDefinition = {\n    label: 'Insert/edit drawing',\n    icon: diagramIcon,\n    action(context: EditorUiContext) {\n        context.editor.getEditorState().read(() => {\n            const selection = getLastSelection(context.editor);\n            const diagramNode = $getNodeFromSelection(selection, $isDiagramNode) as (DiagramNode | null);\n            if (diagramNode === null) {\n                context.editor.update(() => {\n                    const diagram = $createDiagramNode();\n                    $insertNewBlockNodeAtSelection(diagram, true);\n                    $openDrawingEditorForNode(context, diagram);\n                    diagram.selectStart();\n                });\n            } else {\n                $openDrawingEditorForNode(context, diagramNode);\n            }\n        });\n    },\n    isActive(selection: BaseSelection | null): boolean {\n        return $selectionContainsNodeType(selection, $isDiagramNode);\n    }\n};\n\nexport const diagramManager: EditorButtonDefinition = {\n    label: 'Drawing manager',\n    action(context: EditorUiContext) {\n        showDiagramManagerForInsert(context);\n    },\n    isActive(): boolean {\n        return false;\n    }\n};\n\nexport const media: EditorButtonDefinition = {\n    label: 'Insert/edit media',\n    icon: mediaIcon,\n    action(context: EditorUiContext) {\n        context.editor.getEditorState().read(() => {\n            const selection = $getSelection();\n            const selectedNode = $getNodeFromSelection(selection, $isMediaNode) as MediaNode | null;\n\n            $showMediaForm(selectedNode, context);\n        });\n    },\n    isActive(selection: BaseSelection | null): boolean {\n        return $selectionContainsNodeType(selection, $isMediaNode);\n    }\n};\n\nexport const details: EditorButtonDefinition = {\n    label: 'Insert collapsible block',\n    icon: detailsIcon,\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            const selection = $getSelection();\n            const detailsNode = $createDetailsNode();\n            const selectionNodes = selection?.getNodes() || [];\n            const topLevels = selectionNodes.map(n => n.getTopLevelElement())\n                .filter(n => n !== null) as ElementNode[];\n            const uniqueTopLevels = [...new Set(topLevels)];\n\n            detailsNode.setOpen(true);\n\n            if (uniqueTopLevels.length > 0) {\n                uniqueTopLevels[0].insertAfter(detailsNode);\n            } else {\n                $getRoot().append(detailsNode);\n            }\n\n            for (const node of uniqueTopLevels) {\n                detailsNode.append(node);\n            }\n        });\n    },\n    isActive(selection: BaseSelection | null): boolean {\n        return $selectionContainsNodeType(selection, $isDetailsNode);\n    }\n}\n\nexport const detailsEditLabel: EditorButtonDefinition = {\n    label: 'Edit label',\n    icon: tagIcon,\n    action(context: EditorUiContext) {\n        context.editor.getEditorState().read(() => {\n            const details = $getNodeFromSelection($getSelection(), $isDetailsNode);\n            if ($isDetailsNode(details)) {\n                $showDetailsForm(details, context);\n            }\n        })\n    },\n    isActive(selection: BaseSelection | null): boolean {\n        return false;\n    }\n}\n\nexport const detailsToggle: EditorButtonDefinition = {\n    label: 'Toggle open/closed',\n    icon: detailsToggleIcon,\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            const details = $getNodeFromSelection($getSelection(), $isDetailsNode);\n            if ($isDetailsNode(details)) {\n                details.setOpen(!details.getOpen());\n                context.manager.triggerLayoutUpdate();\n            }\n        })\n    },\n    isActive(selection: BaseSelection | null): boolean {\n        return false;\n    }\n}\n\nexport const detailsUnwrap: EditorButtonDefinition = {\n    label: 'Unwrap',\n    icon: tableDeleteIcon,\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            const details = $getNodeFromSelection($getSelection(), $isDetailsNode);\n            if ($isDetailsNode(details)) {\n                $unwrapDetailsNode(details);\n                context.manager.triggerLayoutUpdate();\n            }\n        })\n    },\n    isActive(selection: BaseSelection | null): boolean {\n        return false;\n    }\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/defaults/buttons/tables.ts",
    "content": "import {EditorBasicButtonDefinition, EditorButtonDefinition} from \"../../framework/buttons\";\nimport tableIcon from \"@icons/editor/table.svg\";\nimport deleteIcon from \"@icons/editor/table-delete.svg\";\nimport deleteColumnIcon from \"@icons/editor/table-delete-column.svg\";\nimport deleteRowIcon from \"@icons/editor/table-delete-row.svg\";\nimport insertColumnAfterIcon from \"@icons/editor/table-insert-column-after.svg\";\nimport insertColumnBeforeIcon from \"@icons/editor/table-insert-column-before.svg\";\nimport insertRowAboveIcon from \"@icons/editor/table-insert-row-above.svg\";\nimport insertRowBelowIcon from \"@icons/editor/table-insert-row-below.svg\";\nimport {EditorUiContext} from \"../../framework/core\";\nimport {$getSelection, BaseSelection} from \"lexical\";\nimport {\n    $deleteTableColumn__EXPERIMENTAL,\n    $deleteTableRow__EXPERIMENTAL,\n    $insertTableColumn__EXPERIMENTAL,\n    $insertTableRow__EXPERIMENTAL, $isTableCellNode,\n    $isTableNode, $isTableRowNode, $isTableSelection, $unmergeCell, TableCellNode,\n} from \"@lexical/table\";\nimport {$getNodeFromSelection, $selectionContainsNodeType} from \"../../../utils/selection\";\nimport {$getParentOfType} from \"../../../utils/nodes\";\nimport {$showCellPropertiesForm, $showRowPropertiesForm, $showTablePropertiesForm} from \"../forms/tables\";\nimport {\n    $clearTableFormatting,\n    $clearTableSizes, $getTableFromSelection,\n    $getTableRowsFromSelection,\n    $mergeTableCellsInSelection\n} from \"../../../utils/tables\";\nimport {\n    $copySelectedColumnsToClipboard,\n    $copySelectedRowsToClipboard,\n    $cutSelectedColumnsToClipboard,\n    $cutSelectedRowsToClipboard,\n    $pasteClipboardRowsBefore,\n    $pasteClipboardRowsAfter,\n    isColumnClipboardEmpty,\n    isRowClipboardEmpty,\n    $pasteClipboardColumnsBefore, $pasteClipboardColumnsAfter\n} from \"../../../utils/table-copy-paste\";\n\nconst neverActive = (): boolean => false;\nconst cellNotSelected = (selection: BaseSelection|null) => !$selectionContainsNodeType(selection, $isTableCellNode);\n\nexport const table: EditorBasicButtonDefinition = {\n    label: 'Table',\n    icon: tableIcon,\n};\n\nexport const tableProperties: EditorButtonDefinition = {\n    label: 'Table properties',\n    icon: tableIcon,\n    action(context: EditorUiContext) {\n        context.editor.getEditorState().read(() => {\n            const table = $getTableFromSelection($getSelection());\n            if ($isTableNode(table)) {\n                $showTablePropertiesForm(table, context);\n            }\n        });\n    },\n    isActive: neverActive,\n    isDisabled: cellNotSelected,\n};\n\nexport const clearTableFormatting: EditorButtonDefinition = {\n    label: 'Clear table formatting',\n    format: 'long',\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);\n            if (!$isTableCellNode(cell)) {\n                return;\n            }\n\n            const table = $getParentOfType(cell, $isTableNode);\n            if ($isTableNode(table)) {\n                $clearTableFormatting(table);\n            }\n        });\n    },\n    isActive: neverActive,\n    isDisabled: cellNotSelected,\n};\n\nexport const resizeTableToContents: EditorButtonDefinition = {\n    label: 'Resize to contents',\n    format: 'long',\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);\n            if (!$isTableCellNode(cell)) {\n                return;\n            }\n\n            const table = $getParentOfType(cell, $isTableNode);\n            if ($isTableNode(table)) {\n                $clearTableSizes(table);\n            }\n        });\n    },\n    isActive: neverActive,\n    isDisabled: cellNotSelected,\n};\n\nexport const deleteTable: EditorButtonDefinition = {\n    label: 'Delete table',\n    icon: deleteIcon,\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            const table = $getNodeFromSelection($getSelection(), $isTableNode);\n            if (table) {\n                table.remove();\n            }\n        });\n    },\n    isActive() {\n        return false;\n    }\n};\n\nexport const deleteTableMenuAction: EditorButtonDefinition = {\n    ...deleteTable,\n    format: 'long',\n    isDisabled(selection) {\n        return !$selectionContainsNodeType(selection, $isTableNode);\n    },\n};\n\nexport const insertRowAbove: EditorButtonDefinition = {\n    label: 'Insert row before',\n    icon: insertRowAboveIcon,\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            $insertTableRow__EXPERIMENTAL(false);\n        });\n    },\n    isActive: neverActive,\n    isDisabled: cellNotSelected,\n};\n\nexport const insertRowBelow: EditorButtonDefinition = {\n    label: 'Insert row after',\n    icon: insertRowBelowIcon,\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            $insertTableRow__EXPERIMENTAL(true);\n        });\n    },\n    isActive: neverActive,\n    isDisabled: cellNotSelected,\n};\n\nexport const deleteRow: EditorButtonDefinition = {\n    label: 'Delete row',\n    icon: deleteRowIcon,\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            $deleteTableRow__EXPERIMENTAL();\n        });\n    },\n    isActive: neverActive,\n    isDisabled: cellNotSelected,\n};\n\nexport const rowProperties: EditorButtonDefinition = {\n    label: 'Row properties',\n    format: 'long',\n    action(context: EditorUiContext) {\n        context.editor.getEditorState().read(() => {\n            const rows = $getTableRowsFromSelection($getSelection());\n            if ($isTableRowNode(rows[0])) {\n                $showRowPropertiesForm(rows[0], context);\n            }\n        });\n    },\n    isActive: neverActive,\n    isDisabled: cellNotSelected,\n};\n\nexport const cutRow: EditorButtonDefinition = {\n    label: 'Cut row',\n    format: 'long',\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            try {\n                $cutSelectedRowsToClipboard();\n            } catch (e: any) {\n                context.error(e);\n            }\n        });\n    },\n    isActive: neverActive,\n    isDisabled: cellNotSelected,\n};\n\nexport const copyRow: EditorButtonDefinition = {\n    label: 'Copy row',\n    format: 'long',\n    action(context: EditorUiContext) {\n        context.editor.getEditorState().read(() => {\n            try {\n                $copySelectedRowsToClipboard();\n            } catch (e: any) {\n                context.error(e);\n            }\n        });\n    },\n    isActive: neverActive,\n    isDisabled: cellNotSelected,\n};\n\nexport const pasteRowBefore: EditorButtonDefinition = {\n    label: 'Paste row before',\n    format: 'long',\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            try {\n                $pasteClipboardRowsBefore(context.editor);\n            } catch (e: any) {\n                context.error(e);\n            }\n        });\n    },\n    isActive: neverActive,\n    isDisabled: (selection) => cellNotSelected(selection) || isRowClipboardEmpty(),\n};\n\nexport const pasteRowAfter: EditorButtonDefinition = {\n    label: 'Paste row after',\n    format: 'long',\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            try {\n                $pasteClipboardRowsAfter(context.editor);\n            } catch (e: any) {\n                context.error(e);\n            }\n        });\n    },\n    isActive: neverActive,\n    isDisabled: (selection) => cellNotSelected(selection) || isRowClipboardEmpty(),\n};\n\nexport const cutColumn: EditorButtonDefinition = {\n    label: 'Cut column',\n    format: 'long',\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            try {\n                $cutSelectedColumnsToClipboard();\n            } catch (e: any) {\n                context.error(e);\n            }\n        });\n    },\n    isActive: neverActive,\n    isDisabled: cellNotSelected,\n};\n\nexport const copyColumn: EditorButtonDefinition = {\n    label: 'Copy column',\n    format: 'long',\n    action(context: EditorUiContext) {\n        context.editor.getEditorState().read(() => {\n            try {\n                $copySelectedColumnsToClipboard();\n            } catch (e: any) {\n                context.error(e);\n            }\n        });\n    },\n    isActive: neverActive,\n    isDisabled: cellNotSelected,\n};\n\nexport const pasteColumnBefore: EditorButtonDefinition = {\n    label: 'Paste column before',\n    format: 'long',\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            try {\n                $pasteClipboardColumnsBefore(context.editor);\n            } catch (e: any) {\n                context.error(e);\n            }\n        });\n    },\n    isActive: neverActive,\n    isDisabled: (selection) => cellNotSelected(selection) || isColumnClipboardEmpty(),\n};\n\nexport const pasteColumnAfter: EditorButtonDefinition = {\n    label: 'Paste column after',\n    format: 'long',\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            try {\n                $pasteClipboardColumnsAfter(context.editor);\n            } catch (e: any) {\n                context.error(e);\n            }\n        });\n    },\n    isActive: neverActive,\n    isDisabled: (selection) => cellNotSelected(selection) || isColumnClipboardEmpty(),\n};\n\nexport const insertColumnBefore: EditorButtonDefinition = {\n    label: 'Insert column before',\n    icon: insertColumnBeforeIcon,\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            $insertTableColumn__EXPERIMENTAL(false);\n        });\n    },\n    isActive() {\n        return false;\n    }\n};\n\nexport const insertColumnAfter: EditorButtonDefinition = {\n    label: 'Insert column after',\n    icon: insertColumnAfterIcon,\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            $insertTableColumn__EXPERIMENTAL(true);\n        });\n    },\n    isActive() {\n        return false;\n    }\n};\n\nexport const deleteColumn: EditorButtonDefinition = {\n    label: 'Delete column',\n    icon: deleteColumnIcon,\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            $deleteTableColumn__EXPERIMENTAL();\n        });\n    },\n    isActive() {\n        return false;\n    }\n};\n\nexport const cellProperties: EditorButtonDefinition = {\n    label: 'Cell properties',\n    format: 'long',\n    action(context: EditorUiContext) {\n        context.editor.getEditorState().read(() => {\n            const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);\n            if ($isTableCellNode(cell)) {\n                $showCellPropertiesForm(cell, context);\n            }\n        });\n    },\n    isActive: neverActive,\n    isDisabled: cellNotSelected,\n};\n\nexport const mergeCells: EditorButtonDefinition = {\n    label: 'Merge cells',\n    format: 'long',\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            const selection = $getSelection();\n            if ($isTableSelection(selection)) {\n                $mergeTableCellsInSelection(selection);\n            }\n        });\n    },\n    isActive: neverActive,\n    isDisabled(selection) {\n        return !$isTableSelection(selection);\n    }\n};\n\nexport const splitCell: EditorButtonDefinition = {\n    label: 'Split cell',\n    format: 'long',\n    action(context: EditorUiContext) {\n        context.editor.update(() => {\n            $unmergeCell();\n        });\n    },\n    isActive: neverActive,\n    isDisabled(selection) {\n        const cell = $getNodeFromSelection(selection, $isTableCellNode) as TableCellNode|null;\n        if (cell) {\n            const merged = cell.getRowSpan() > 1 || cell.getColSpan() > 1;\n            return !merged;\n        }\n\n        return true;\n    }\n};"
  },
  {
    "path": "resources/js/wysiwyg/ui/defaults/forms/controls.ts",
    "content": "import {EditorFormDefinition} from \"../../framework/forms\";\nimport {EditorUiContext, EditorUiElement} from \"../../framework/core\";\nimport {setEditorContentFromHtml} from \"../../../utils/actions\";\nimport {ExternalContent} from \"../../framework/blocks/external-content\";\n\nexport const source: EditorFormDefinition = {\n    submitText: 'Save',\n    async action(formData, context: EditorUiContext) {\n        setEditorContentFromHtml(context.editor, formData.get('source')?.toString() || '');\n        return true;\n    },\n    fields: [\n        {\n            label: 'Source',\n            name: 'source',\n            type: 'textarea',\n        },\n    ],\n};\n\nexport const about: EditorFormDefinition = {\n    submitText: 'Close',\n    async action() {\n        return true;\n    },\n    fields: [\n        {\n            build(): EditorUiElement {\n                return new ExternalContent('/help/wysiwyg');\n            }\n        }\n    ],\n};"
  },
  {
    "path": "resources/js/wysiwyg/ui/defaults/forms/objects.ts",
    "content": "import {\n    EditorFormDefinition,\n    EditorFormField,\n    EditorFormTabs,\n    EditorSelectFormFieldDefinition\n} from \"../../framework/forms\";\nimport {EditorUiContext} from \"../../framework/core\";\nimport {$createNodeSelection, $getSelection, $insertNodes, $setSelection} from \"lexical\";\nimport {$isImageNode, ImageNode} from \"@lexical/rich-text/LexicalImageNode\";\nimport {LinkNode} from \"@lexical/link\";\nimport {$createMediaNodeFromHtml, $createMediaNodeFromSrc, $isMediaNode, MediaNode} from \"@lexical/rich-text/LexicalMediaNode\";\nimport {$getNodeFromSelection, getLastSelection} from \"../../../utils/selection\";\nimport {EditorFormModal} from \"../../framework/modals\";\nimport {EditorActionField} from \"../../framework/blocks/action-field\";\nimport {EditorButton} from \"../../framework/buttons\";\nimport {showImageManager} from \"../../../utils/images\";\nimport searchImageIcon from \"@icons/editor/image-search.svg\";\nimport searchIcon from \"@icons/search.svg\";\nimport {showLinkSelector} from \"../../../utils/links\";\nimport {LinkField} from \"../../framework/blocks/link-field\";\nimport {insertOrUpdateLink} from \"../../../utils/formats\";\nimport {$isDetailsNode, DetailsNode} from \"@lexical/rich-text/LexicalDetailsNode\";\n\nexport function $showImageForm(image: ImageNode, context: EditorUiContext) {\n    const imageModal: EditorFormModal = context.manager.createModal('image');\n    const height = image.getHeight();\n    const width = image.getWidth();\n\n    const formData = {\n        src: image.getSrc(),\n        alt: image.getAltText(),\n        height: height === 0 ? '' : String(height),\n        width: width === 0 ? '' : String(width),\n    };\n\n    imageModal.show(formData);\n}\n\nexport const image: EditorFormDefinition = {\n    submitText: 'Apply',\n    async action(formData, context: EditorUiContext) {\n        context.editor.update(() => {\n            const selection = getLastSelection(context.editor);\n            const selectedImage = $getNodeFromSelection(selection, $isImageNode);\n            if ($isImageNode(selectedImage)) {\n                selectedImage.setSrc(formData.get('src')?.toString() || '');\n                selectedImage.setAltText(formData.get('alt')?.toString() || '');\n\n                selectedImage.setWidth(Number(formData.get('width')?.toString() || '0'));\n                selectedImage.setHeight(Number(formData.get('height')?.toString() || '0'));\n            }\n        });\n        return true;\n    },\n    fields: [\n        {\n            build() {\n                return new EditorActionField(\n                    new EditorFormField({\n                        label: 'Source',\n                        name: 'src',\n                        type: 'text',\n                    }),\n                    new EditorButton({\n                        label: 'Browse files',\n                        icon: searchImageIcon,\n                        action(context: EditorUiContext) {\n                            showImageManager((image) => {\n                                 const modal =  context.manager.getActiveModal('image');\n                                 if (modal) {\n                                     modal.getForm().setValues({\n                                         src: image.thumbs?.display || image.url,\n                                         alt: image.name,\n                                     });\n                                 }\n                            });\n                        }\n                    }),\n                );\n            },\n        },\n        {\n            label: 'Alternative description',\n            name: 'alt',\n            type: 'text',\n        },\n        {\n            label: 'Width',\n            name: 'width',\n            type: 'text',\n        },\n        {\n            label: 'Height',\n            name: 'height',\n            type: 'text',\n        },\n    ],\n};\n\nexport function $showLinkForm(link: LinkNode|null, context: EditorUiContext) {\n    const linkModal = context.manager.createModal('link');\n\n    if (link) {\n        const formDefaults: Record<string, string> = {\n            url: link.getURL(),\n            text: link.getTextContent(),\n            title: link.getTitle() || '',\n            target: link.getTarget() || '',\n        }\n\n        context.editor.update(() => {\n            const selection = $createNodeSelection();\n            selection.add(link.getKey());\n            $setSelection(selection);\n        });\n\n        linkModal.show(formDefaults);\n    } else {\n        context.editor.getEditorState().read(() => {\n            const selection = $getSelection();\n            const text = selection?.getTextContent() || '';\n            const formDefaults = {text};\n            linkModal.show(formDefaults);\n        });\n    }\n}\n\nexport const link: EditorFormDefinition = {\n    submitText: 'Apply',\n    async action(formData, context: EditorUiContext) {\n        insertOrUpdateLink(context.editor, {\n            url: formData.get('url')?.toString() || '',\n            title: formData.get('title')?.toString() || '',\n            target: formData.get('target')?.toString() || '',\n            text: formData.get('text')?.toString() || '',\n        });\n        return true;\n    },\n    fields: [\n        {\n            build() {\n                return new EditorActionField(\n                    new LinkField(new EditorFormField({\n                        label: 'URL',\n                        name: 'url',\n                        type: 'text',\n                    })),\n                    new EditorButton({\n                        label: 'Browse links',\n                        icon: searchIcon,\n                        action(context: EditorUiContext) {\n                            showLinkSelector(entity => {\n                                const modal =  context.manager.getActiveModal('link');\n                                if (modal) {\n                                    modal.getForm().setValues({\n                                        url: entity.link,\n                                        text: entity.name,\n                                        title: entity.name,\n                                    });\n                                }\n                            });\n                        }\n                    }),\n                );\n            },\n        },\n        {\n            label: 'Text to display',\n            name: 'text',\n            type: 'text',\n        },\n        {\n            label: 'Title',\n            name: 'title',\n            type: 'text',\n        },\n        {\n            label: 'Open link in...',\n            name: 'target',\n            type: 'select',\n            valuesByLabel: {\n                'Current window': '',\n                'New window': '_blank',\n            }\n        } as EditorSelectFormFieldDefinition,\n    ],\n};\n\nexport function $showMediaForm(media: MediaNode|null, context: EditorUiContext): void {\n    const mediaModal = context.manager.createModal('media');\n\n    let formDefaults = {};\n    if (media) {\n        const nodeAttrs = media.getAttributes();\n        const nodeDOM = media.exportDOM(context.editor).element;\n        const nodeHtml = (nodeDOM instanceof HTMLElement) ? nodeDOM.outerHTML : '';\n\n        formDefaults = {\n            src: nodeAttrs.src || nodeAttrs.data || media.getSources()[0]?.src || '',\n            width: nodeAttrs.width,\n            height: nodeAttrs.height,\n            embed: nodeHtml,\n\n            // This is used so we can check for edits against the embed field on submit\n            embed_check: nodeHtml,\n        }\n    }\n\n    mediaModal.show(formDefaults);\n}\n\nexport const media: EditorFormDefinition = {\n    submitText: 'Save',\n    async action(formData, context: EditorUiContext) {\n        const selectedNode: MediaNode|null = await (new Promise((res, rej) => {\n            context.editor.getEditorState().read(() => {\n                const node = $getNodeFromSelection($getSelection(), $isMediaNode);\n                res(node as MediaNode|null);\n            });\n        }));\n\n        const embedCode = (formData.get('embed') || '').toString().trim();\n        const embedCheck = (formData.get('embed_check') || '').toString().trim();\n        if (embedCode && embedCode !== embedCheck) {\n            context.editor.update(() => {\n                const node = $createMediaNodeFromHtml(embedCode);\n                if (selectedNode && node) {\n                    selectedNode.replace(node)\n                } else if (node) {\n                    $insertNodes([node]);\n                }\n            });\n\n            return true;\n        }\n\n        context.editor.update(() => {\n            const src = (formData.get('src') || '').toString().trim();\n            const height = (formData.get('height') || '').toString().trim();\n            const width = (formData.get('width') || '').toString().trim();\n\n            // Update existing\n            if (selectedNode) {\n                selectedNode.setSrc(src);\n                selectedNode.setWidthAndHeight(width, height);\n                context.manager.triggerFutureStateRefresh();\n                return;\n            }\n\n            // Insert new\n            const node = $createMediaNodeFromSrc(src);\n            if (width || height) {\n                node.setWidthAndHeight(width, height);\n            }\n            $insertNodes([node]);\n        });\n\n        return true;\n    },\n    fields: [\n        {\n            build() {\n                return new EditorFormTabs([\n                    {\n                        label: 'General',\n                        contents: [\n                            {\n                                label: 'Source',\n                                name: 'src',\n                                type: 'text',\n                            },\n                            {\n                                label: 'Width',\n                                name: 'width',\n                                type: 'text',\n                            },\n                            {\n                                label: 'Height',\n                                name: 'height',\n                                type: 'text',\n                            },\n                        ],\n                    },\n                    {\n                        label: 'Embed',\n                        contents: [\n                            {\n                                label: 'Paste your embed code below:',\n                                name: 'embed',\n                                type: 'textarea',\n                            },\n                            {\n                                label: '',\n                                name: 'embed_check',\n                                type: 'hidden',\n                            },\n                        ],\n                    }\n                ])\n            }\n        },\n    ],\n};\n\nexport function $showDetailsForm(details: DetailsNode|null, context: EditorUiContext) {\n    const linkModal = context.manager.createModal('details');\n    if (!details) {\n        return;\n    }\n\n    linkModal.show({\n        summary: details.getSummary()\n    });\n}\n\nexport const details: EditorFormDefinition = {\n    submitText: 'Save',\n    async action(formData, context: EditorUiContext) {\n        context.editor.update(() => {\n            const node = $getNodeFromSelection($getSelection(), $isDetailsNode);\n            const summary = (formData.get('summary') || '').toString().trim();\n            if ($isDetailsNode(node)) {\n                node.setSummary(summary);\n            }\n        });\n\n        return true;\n    },\n    fields: [\n        {\n            label: 'Toggle label',\n            name: 'summary',\n            type: 'text',\n        },\n    ],\n};"
  },
  {
    "path": "resources/js/wysiwyg/ui/defaults/forms/tables.ts",
    "content": "import {\n    EditorFormDefinition,\n    EditorFormFieldDefinition, EditorFormFields,\n    EditorFormTabs,\n    EditorSelectFormFieldDefinition\n} from \"../../framework/forms\";\nimport {EditorUiContext} from \"../../framework/core\";\nimport {EditorFormModal} from \"../../framework/modals\";\nimport {$getSelection} from \"lexical\";\nimport {\n    $forEachTableCell, $getCellPaddingForTable,\n    $getTableCellColumnWidth,\n    $getTableCellsFromSelection, $getTableFromSelection,\n    $getTableRowsFromSelection,\n    $setTableCellColumnWidth\n} from \"../../../utils/tables\";\nimport {formatSizeValue} from \"../../../utils/dom\";\nimport {TableCellNode, TableNode, TableRowNode} from \"@lexical/table\";\nimport {CommonBlockAlignment} from \"lexical/nodes/common\";\nimport {colorFieldBuilder} from \"../../framework/blocks/color-field\";\nimport {$addCaptionToTable, $isCaptionNode, $tableHasCaption} from \"@lexical/table/LexicalCaptionNode\";\n\nconst borderStyleInput: EditorSelectFormFieldDefinition = {\n    label: 'Border style',\n    name: 'border_style',\n    type: 'select',\n    valuesByLabel: {\n        'Select...': '',\n        \"Solid\": 'solid',\n        \"Dotted\": 'dotted',\n        \"Dashed\": 'dashed',\n        \"Double\": 'double',\n        \"Groove\": 'groove',\n        \"Ridge\": 'ridge',\n        \"Inset\": 'inset',\n        \"Outset\": 'outset',\n        \"None\": 'none',\n        \"Hidden\": 'hidden',\n    }\n};\n\nconst borderColorInput: EditorFormFieldDefinition = {\n    label: 'Border color',\n    name: 'border_color',\n    type: 'text',\n};\n\nconst backgroundColorInput: EditorFormFieldDefinition = {\n    label: 'Background color',\n    name: 'background_color',\n    type: 'text',\n};\n\nconst alignmentInput: EditorSelectFormFieldDefinition = {\n    label: 'Alignment',\n    name: 'align',\n    type: 'select',\n    valuesByLabel: {\n        'None': '',\n        'Left': 'left',\n        'Center': 'center',\n        'Right': 'right',\n    }\n};\n\nexport function $showCellPropertiesForm(cell: TableCellNode, context: EditorUiContext): EditorFormModal {\n    const styles = cell.getStyles();\n    const modalForm = context.manager.createModal('cell_properties');\n    modalForm.show({\n        width: $getTableCellColumnWidth(context.editor, cell),\n        height: styles.get('height') || '',\n        type: cell.getTag(),\n        h_align: cell.getAlignment(),\n        v_align: styles.get('vertical-align') || '',\n        border_width: styles.get('border-width') || '',\n        border_style: styles.get('border-style') || '',\n        border_color: styles.get('border-color') || '',\n        background_color: cell.getBackgroundColor() || styles.get('background-color') || '',\n    });\n    return modalForm;\n}\n\nexport const cellProperties: EditorFormDefinition = {\n    submitText: 'Save',\n    async action(formData, context: EditorUiContext) {\n        context.editor.update(() => {\n            const cells = $getTableCellsFromSelection($getSelection());\n            for (const cell of cells) {\n                const width = formData.get('width')?.toString() || '';\n\n                $setTableCellColumnWidth(cell, width);\n                cell.updateTag(formData.get('type')?.toString() || '');\n                cell.setAlignment((formData.get('h_align')?.toString() || '') as CommonBlockAlignment);\n                cell.setBackgroundColor(formData.get('background_color')?.toString() || '');\n\n                const styles = cell.getStyles();\n                styles.set('height', formatSizeValue(formData.get('height')?.toString() || ''));\n                styles.set('vertical-align', formData.get('v_align')?.toString() || '');\n                styles.set('border-width', formatSizeValue(formData.get('border_width')?.toString() || ''));\n                styles.set('border-style', formData.get('border_style')?.toString() || '');\n                styles.set('border-color', formData.get('border_color')?.toString() || '');\n\n                cell.setStyles(styles);\n            }\n        });\n\n        return true;\n    },\n    fields: [\n        {\n            build() {\n                const generalFields: EditorFormFieldDefinition[] = [\n                    {\n                        label: 'Width', // Colgroup width\n                        name: 'width',\n                        type: 'text',\n                    },\n                    {\n                        label: 'Height', // inline-style: height\n                        name: 'height',\n                        type: 'text',\n                    },\n                    {\n                        label: 'Cell type', // element\n                        name: 'type',\n                        type: 'select',\n                        valuesByLabel: {\n                            'Cell': 'td',\n                            'Header cell': 'th',\n                        }\n                    } as EditorSelectFormFieldDefinition,\n                    {\n                        ...alignmentInput, // class: 'align-right/left/center'\n                        label: 'Horizontal align',\n                        name: 'h_align',\n                    },\n                    {\n                        label: 'Vertical align', // inline-style: vertical-align\n                        name: 'v_align',\n                        type: 'select',\n                        valuesByLabel: {\n                            'None': '',\n                            'Top': 'top',\n                            'Middle': 'middle',\n                            'Bottom': 'bottom',\n                        }\n                    } as EditorSelectFormFieldDefinition,\n                ];\n\n                const advancedFields: EditorFormFields = [\n                    {\n                        label: 'Border width', // inline-style: border-width\n                        name: 'border_width',\n                        type: 'text',\n                    },\n                    borderStyleInput, // inline-style: border-style\n                    colorFieldBuilder(borderColorInput),\n                    colorFieldBuilder(backgroundColorInput),\n                ];\n\n                return new EditorFormTabs([\n                    {\n                        label: 'General',\n                        contents: generalFields,\n                    },\n                    {\n                        label: 'Advanced',\n                        contents: advancedFields,\n                    }\n                ])\n            }\n        },\n    ],\n};\n\nexport function $showRowPropertiesForm(row: TableRowNode, context: EditorUiContext): EditorFormModal {\n    const styles = row.getStyles();\n    const modalForm = context.manager.createModal('row_properties');\n    modalForm.show({\n        height: styles.get('height') || '',\n        border_style: styles.get('border-style') || '',\n        border_color: styles.get('border-color') || '',\n        background_color: styles.get('background-color') || '',\n    });\n    return modalForm;\n}\n\nexport const rowProperties: EditorFormDefinition = {\n    submitText: 'Save',\n    async action(formData, context: EditorUiContext) {\n        context.editor.update(() => {\n            const rows = $getTableRowsFromSelection($getSelection());\n            for (const row of rows) {\n                const styles = row.getStyles();\n                styles.set('height', formatSizeValue(formData.get('height')?.toString() || ''));\n                styles.set('border-style', formData.get('border_style')?.toString() || '');\n                styles.set('border-color', formData.get('border_color')?.toString() || '');\n                styles.set('background-color', formData.get('background_color')?.toString() || '');\n                row.setStyles(styles);\n            }\n        });\n        return true;\n    },\n    fields: [\n        // Removed fields:\n        // Removed 'Row Type' as we don't currently support thead/tfoot elements\n        //  TinyMCE would move rows up/down into these parents when set\n        // Removed 'Alignment' since this was broken in our editor (applied alignment class to whole parent table)\n        {\n            label: 'Height', // style on tr: height\n            name: 'height',\n            type: 'text',\n        },\n        borderStyleInput, // style on tr: height\n        colorFieldBuilder(borderColorInput),\n        colorFieldBuilder(backgroundColorInput),\n    ],\n};\n\nexport function $showTablePropertiesForm(table: TableNode, context: EditorUiContext): EditorFormModal {\n    const styles = table.getStyles();\n    const modalForm = context.manager.createModal('table_properties');\n\n    modalForm.show({\n        width: styles.get('width') || '',\n        height: styles.get('height') || '',\n        cell_spacing: styles.get('cell-spacing') || '',\n        cell_padding: $getCellPaddingForTable(table),\n        border_width: styles.get('border-width') || '',\n        border_style: styles.get('border-style') || '',\n        border_color: styles.get('border-color') || '',\n        background_color: styles.get('background-color') || '',\n        caption: $tableHasCaption(table) ? 'true' : '',\n        align: table.getAlignment(),\n    });\n    return modalForm;\n}\n\nexport const tableProperties: EditorFormDefinition = {\n    submitText: 'Save',\n    async action(formData, context: EditorUiContext) {\n        context.editor.update(() => {\n            const table = $getTableFromSelection($getSelection());\n            if (!table) {\n                return;\n            }\n\n            const styles = table.getStyles();\n            styles.set('width', formatSizeValue(formData.get('width')?.toString() || ''));\n            styles.set('height', formatSizeValue(formData.get('height')?.toString() || ''));\n            styles.set('cell-spacing', formatSizeValue(formData.get('cell_spacing')?.toString() || ''));\n            styles.set('border-width', formatSizeValue(formData.get('border_width')?.toString() || ''));\n            styles.set('border-style', formData.get('border_style')?.toString() || '');\n            styles.set('border-color', formData.get('border_color')?.toString() || '');\n            styles.set('background-color', formData.get('background_color')?.toString() || '');\n            table.setStyles(styles);\n\n            table.setAlignment(formData.get('align') as CommonBlockAlignment);\n\n            const cellPadding = (formData.get('cell_padding')?.toString() || '');\n            if (cellPadding) {\n                const cellPaddingFormatted = formatSizeValue(cellPadding);\n                $forEachTableCell(table, (cell: TableCellNode) => {\n                    const styles = cell.getStyles();\n                    styles.set('padding', cellPaddingFormatted);\n                    cell.setStyles(styles);\n                });\n            }\n\n            const showCaption = Boolean(formData.get('caption')?.toString() || '');\n            const hasCaption = $tableHasCaption(table);\n            if (showCaption && !hasCaption) {\n                $addCaptionToTable(table, context.translate('Caption'));\n            } else if (!showCaption && hasCaption) {\n                for (const child of table.getChildren()) {\n                    if ($isCaptionNode(child)) {\n                        child.remove();\n                    }\n                }\n            }\n        });\n        return true;\n    },\n    fields: [\n        {\n            build() {\n                const generalFields: EditorFormFieldDefinition[] = [\n                    {\n                        label: 'Width', // Style - width\n                        name: 'width',\n                        type: 'text',\n                    },\n                    {\n                        label: 'Height', // Style - height\n                        name: 'height',\n                        type: 'text',\n                    },\n                    {\n                        label: 'Cell spacing', // Style - border-spacing\n                        name: 'cell_spacing',\n                        type: 'text',\n                    },\n                    {\n                        label: 'Cell padding', // Style - padding on child cells?\n                        name: 'cell_padding',\n                        type: 'text',\n                    },\n                    {\n                        label: 'Border width', // Style - border-width\n                        name: 'border_width',\n                        type: 'text',\n                    },\n                    {\n                        label: 'Show caption', // Caption element\n                        name: 'caption',\n                        type: 'checkbox',\n                    },\n                    alignmentInput, // alignment class\n                ];\n\n                const advancedFields: EditorFormFields = [\n                    borderStyleInput,\n                    colorFieldBuilder(borderColorInput),\n                    colorFieldBuilder(backgroundColorInput),\n                ];\n\n                return new EditorFormTabs([\n                    {\n                        label: 'General',\n                        contents: generalFields,\n                    },\n                    {\n                        label: 'Advanced',\n                        contents: advancedFields,\n                    }\n                ])\n            }\n        },\n    ],\n};"
  },
  {
    "path": "resources/js/wysiwyg/ui/defaults/modals.ts",
    "content": "import {EditorFormModalDefinition} from \"../framework/modals\";\nimport {details, image, link, media} from \"./forms/objects\";\nimport {about, source} from \"./forms/controls\";\nimport {cellProperties, rowProperties, tableProperties} from \"./forms/tables\";\n\nexport const modals: Record<string, EditorFormModalDefinition> = {\n    link: {\n        title: 'Insert/Edit Link',\n        form: link,\n    },\n    image: {\n        title: 'Insert/Edit Image',\n        form: image,\n    },\n    media: {\n        title: 'Insert/Edit Media',\n        form: media,\n    },\n    source: {\n        title: 'Source code',\n        form: source,\n    },\n    cell_properties: {\n        title: 'Cell Properties',\n        form: cellProperties,\n    },\n    row_properties: {\n        title: 'Row Properties',\n        form: rowProperties,\n    },\n    table_properties: {\n        title: 'Table Properties',\n        form: tableProperties,\n    },\n    details: {\n        title: 'Edit collapsible block',\n        form: details,\n    },\n    about: {\n        title: 'About the WYSIWYG Editor',\n        form: about,\n    }\n};"
  },
  {
    "path": "resources/js/wysiwyg/ui/defaults/toolbars.ts",
    "content": "import {EditorButton} from \"../framework/buttons\";\nimport {EditorContainerUiElement, EditorSimpleClassContainer, EditorUiContext, EditorUiElement} from \"../framework/core\";\nimport {EditorFormatMenu} from \"../framework/blocks/format-menu\";\nimport {FormatPreviewButton} from \"../framework/blocks/format-preview-button\";\nimport {EditorDropdownButton} from \"../framework/blocks/dropdown-button\";\nimport {EditorColorPicker} from \"../framework/blocks/color-picker\";\nimport {EditorTableCreator} from \"../framework/blocks/table-creator\";\nimport {EditorColorButton} from \"../framework/blocks/color-button\";\nimport {EditorOverflowContainer} from \"../framework/blocks/overflow-container\";\nimport {\n    cellProperties, clearTableFormatting,\n    copyColumn,\n    copyRow,\n    cutColumn,\n    cutRow,\n    deleteColumn,\n    deleteRow,\n    deleteTable,\n    deleteTableMenuAction,\n    insertColumnAfter,\n    insertColumnBefore,\n    insertRowAbove,\n    insertRowBelow,\n    mergeCells,\n    pasteColumnAfter,\n    pasteColumnBefore,\n    pasteRowAfter,\n    pasteRowBefore, resizeTableToContents,\n    rowProperties,\n    splitCell,\n    table, tableProperties\n} from \"./buttons/tables\";\nimport {about, fullscreen, redo, source, undo} from \"./buttons/controls\";\nimport {\n    blockquote, dangerCallout,\n    h2,\n    h3,\n    h4,\n    h5,\n    infoCallout,\n    paragraph,\n    successCallout,\n    warningCallout\n} from \"./buttons/block-formats\";\nimport {\n    bold, clearFormating, code,\n    highlightColor, highlightColorAction,\n    italic,\n    strikethrough, subscript,\n    superscript,\n    textColor, textColorAction,\n    underline\n} from \"./buttons/inline-formats\";\nimport {\n    alignCenter,\n    alignJustify,\n    alignLeft,\n    alignRight,\n    directionLTR,\n    directionRTL\n} from \"./buttons/alignments\";\nimport {\n    bulletList,\n    indentDecrease,\n    indentIncrease,\n    numberList,\n    taskList\n} from \"./buttons/lists\";\nimport {\n    codeBlock,\n    details, detailsEditLabel, detailsToggle, detailsUnwrap,\n    diagram, diagramManager,\n    editCodeBlock,\n    horizontalRule,\n    image,\n    link, media,\n    unlink\n} from \"./buttons/objects\";\nimport {el} from \"../../utils/dom\";\nimport {EditorButtonWithMenu} from \"../framework/blocks/button-with-menu\";\nimport {EditorSeparator} from \"../framework/blocks/separator\";\nimport {EditorContextToolbarDefinition} from \"../framework/toolbars\";\n\nexport function getMainEditorFullToolbar(context: EditorUiContext): EditorContainerUiElement {\n\n    const inRtlMode = context.manager.getDefaultDirection() === 'rtl';\n\n    return new EditorSimpleClassContainer('editor-toolbar-main', [\n\n        // History state\n        new EditorOverflowContainer('history', 2, [\n            new EditorButton(undo),\n            new EditorButton(redo),\n        ]),\n\n        // Block formats\n        new EditorFormatMenu([\n            new FormatPreviewButton(el('h2'), h2),\n            new FormatPreviewButton(el('h3'), h3),\n            new FormatPreviewButton(el('h4'), h4),\n            new FormatPreviewButton(el('h5'), h5),\n            new FormatPreviewButton(el('blockquote'), blockquote),\n            new FormatPreviewButton(el('p'), paragraph),\n            new EditorDropdownButton({button: {label: 'Callouts', format: 'long'}, showOnHover: true, direction: 'vertical'}, [\n                new FormatPreviewButton(el('p', {class: 'callout info'}), infoCallout),\n                new FormatPreviewButton(el('p', {class: 'callout success'}), successCallout),\n                new FormatPreviewButton(el('p', {class: 'callout warning'}), warningCallout),\n                new FormatPreviewButton(el('p', {class: 'callout danger'}), dangerCallout),\n            ]),\n        ]),\n\n        // Inline formats\n        new EditorOverflowContainer('inline_formats', 6, [\n            new EditorButton(bold),\n            new EditorButton(italic),\n            new EditorButton(underline),\n            new EditorDropdownButton({ button: new EditorColorButton(textColor, 'color') }, [\n                new EditorColorPicker(textColorAction),\n            ]),\n            new EditorDropdownButton({button: new EditorColorButton(highlightColor, 'background-color')}, [\n                new EditorColorPicker(highlightColorAction),\n            ]),\n            new EditorButton(strikethrough),\n            new EditorButton(superscript),\n            new EditorButton(subscript),\n            new EditorButton(code),\n            new EditorButton(clearFormating),\n        ]),\n\n        // Alignment\n        new EditorOverflowContainer('alignment', 6, [\n            new EditorButton(alignLeft),\n            new EditorButton(alignCenter),\n            new EditorButton(alignRight),\n            new EditorButton(alignJustify),\n            inRtlMode ? new EditorButton(directionLTR) : null,\n            inRtlMode ? new EditorButton(directionRTL) : null,\n        ].filter(x => x !== null)),\n\n        // Lists\n        new EditorOverflowContainer('lists', 3, [\n            new EditorButton(bulletList),\n            new EditorButton(numberList),\n            new EditorButton(taskList),\n            new EditorButton(indentDecrease),\n            new EditorButton(indentIncrease),\n        ]),\n\n        // Insert types\n        new EditorOverflowContainer('inserts', 4, [\n            new EditorButton(link),\n\n            new EditorDropdownButton({button: table, direction: 'vertical', showAside: false}, [\n                new EditorDropdownButton({button: {label: 'Insert', format: 'long'}, showOnHover: true, showAside: true}, [\n                    new EditorTableCreator(),\n                ]),\n                new EditorSeparator(),\n                new EditorDropdownButton({button: {label: 'Cell', format: 'long'}, direction: 'vertical', showOnHover: true}, [\n                    new EditorButton(cellProperties),\n                    new EditorButton(mergeCells),\n                    new EditorButton(splitCell),\n                ]),\n                new EditorDropdownButton({button: {label: 'Row', format: 'long'}, direction: 'vertical', showOnHover: true}, [\n                    new EditorButton({...insertRowAbove, format: 'long'}),\n                    new EditorButton({...insertRowBelow, format: 'long'}),\n                    new EditorButton({...deleteRow, format: 'long'}),\n                    new EditorButton(rowProperties),\n                    new EditorSeparator(),\n                    new EditorButton(cutRow),\n                    new EditorButton(copyRow),\n                    new EditorButton(pasteRowBefore),\n                    new EditorButton(pasteRowAfter),\n                ]),\n                new EditorDropdownButton({button: {label: 'Column', format: 'long'}, direction: 'vertical', showOnHover: true}, [\n                    new EditorButton({...insertColumnBefore, format: 'long'}),\n                    new EditorButton({...insertColumnAfter, format: 'long'}),\n                    new EditorButton({...deleteColumn, format: 'long'}),\n                    new EditorSeparator(),\n                    new EditorButton(cutColumn),\n                    new EditorButton(copyColumn),\n                    new EditorButton(pasteColumnBefore),\n                    new EditorButton(pasteColumnAfter),\n                ]),\n                new EditorSeparator(),\n                new EditorButton({...tableProperties, format: 'long'}),\n                new EditorButton(clearTableFormatting),\n                new EditorButton(resizeTableToContents),\n                new EditorButton(deleteTableMenuAction),\n            ]),\n\n            new EditorButton(image),\n            new EditorButton(horizontalRule),\n            new EditorButton(codeBlock),\n            new EditorButtonWithMenu(\n                new EditorButton(diagram),\n                [new EditorButton(diagramManager)],\n            ),\n            new EditorButton(media),\n            new EditorButton(details),\n        ]),\n\n        // Meta elements\n        new EditorOverflowContainer('meta', 3, [\n            new EditorButton(source),\n            new EditorButton(about),\n            new EditorButton(fullscreen),\n\n            // Test\n            // new EditorButton({\n            //     label: 'Test button',\n            //     action(context: EditorUiContext) {\n            //         context.editor.update(() => {\n            //             // Do stuff\n            //         });\n            //     },\n            //     isActive() {\n            //         return false;\n            //     }\n            // })\n        ]),\n    ]);\n}\n\nexport function getBasicEditorToolbar(context: EditorUiContext): EditorContainerUiElement {\n    return new EditorSimpleClassContainer('editor-toolbar-main', [\n        new EditorOverflowContainer('formats', 7, [\n            new EditorButton(bold),\n            new EditorButton(italic),\n            new EditorButton(link),\n            new EditorButton(bulletList),\n            new EditorButton(numberList),\n        ])\n    ]);\n}\n\nexport const contextToolbars: Record<string, EditorContextToolbarDefinition> = {\n    image: {\n        selector: 'img:not([drawio-diagram] img)',\n        content: () => [new EditorButton(image)],\n    },\n    media: {\n        selector: '.editor-media-wrap',\n        content: () => [new EditorButton(media)],\n    },\n    link: {\n        selector: 'a:not([data-mention-user-id])',\n        content() {\n            return [\n                new EditorButton(link),\n                new EditorButton(unlink),\n            ]\n        },\n        displayTargetLocator(originalTarget: HTMLElement): HTMLElement {\n            const image = originalTarget.querySelector('img');\n            return image || originalTarget;\n        }\n    },\n    code: {\n        selector: '.editor-code-block-wrap',\n        content: () => [new EditorButton(editCodeBlock)],\n    },\n    table: {\n        selector: 'td,th',\n        content() {\n            return [\n                new EditorOverflowContainer('table', 2, [\n                    new EditorButton(tableProperties),\n                    new EditorButton(deleteTable),\n                ]),\n                new EditorOverflowContainer('table_row',3, [\n                    new EditorButton(insertRowAbove),\n                    new EditorButton(insertRowBelow),\n                    new EditorButton(deleteRow),\n                ]),\n                new EditorOverflowContainer('table_column', 3, [\n                    new EditorButton(insertColumnBefore),\n                    new EditorButton(insertColumnAfter),\n                    new EditorButton(deleteColumn),\n                ]),\n            ];\n        },\n        displayTargetLocator(originalTarget: HTMLElement): HTMLElement {\n            return originalTarget.closest('table') as HTMLTableElement;\n        }\n    },\n    details: {\n        selector: 'details',\n        content() {\n            return [\n                new EditorButton(detailsEditLabel),\n                new EditorButton(detailsToggle),\n                new EditorButton(detailsUnwrap),\n            ]\n        },\n    },\n};"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/blocks/action-field.ts",
    "content": "import {EditorContainerUiElement, EditorUiElement} from \"../core\";\nimport {el} from \"../../../utils/dom\";\nimport {EditorButton} from \"../buttons\";\n\n\nexport class EditorActionField extends EditorContainerUiElement {\n    protected input: EditorUiElement;\n    protected action: EditorButton;\n\n    constructor(input: EditorUiElement, action: EditorButton) {\n        super([input, action]);\n\n        this.input = input;\n        this.action = action;\n    }\n\n    buildDOM(): HTMLElement {\n        return el('div', {\n            class: 'editor-action-input-container',\n        }, [\n            this.input.getDOMElement(),\n            this.action.getDOMElement(),\n        ]);\n    }\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/blocks/button-with-menu.ts",
    "content": "import {EditorContainerUiElement, EditorUiElement} from \"../core\";\nimport {el} from \"../../../utils/dom\";\nimport {EditorButton} from \"../buttons\";\nimport {EditorDropdownButton} from \"./dropdown-button\";\nimport caretDownIcon from \"@icons/caret-down-large.svg\";\n\nexport class EditorButtonWithMenu extends EditorContainerUiElement {\n    protected button: EditorButton;\n    protected dropdownButton: EditorDropdownButton;\n\n    constructor(button: EditorButton, menuItems: EditorUiElement[]) {\n        super([button]);\n\n        this.button = button;\n        this.dropdownButton = new EditorDropdownButton({\n            button: {label: 'Menu', icon: caretDownIcon},\n            showOnHover: false,\n            direction: 'vertical',\n            showAside: false,\n        }, menuItems);\n        this.addChildren(this.dropdownButton);\n    }\n\n    buildDOM(): HTMLElement {\n        return el('div', {\n            class: 'editor-button-with-menu-container',\n        }, [\n            this.button.getDOMElement(),\n            this.dropdownButton.getDOMElement()\n        ]);\n    }\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/blocks/color-button.ts",
    "content": "import {EditorBasicButtonDefinition, EditorButton} from \"../buttons\";\nimport {EditorUiStateUpdate} from \"../core\";\nimport {$isRangeSelection} from \"lexical\";\nimport {$getSelectionStyleValueForProperty} from \"@lexical/selection\";\n\nexport class EditorColorButton extends EditorButton {\n    protected style: string;\n\n    constructor(definition: EditorBasicButtonDefinition, style: string) {\n        super(definition);\n\n        this.style = style;\n    }\n\n    getColorBar(): HTMLElement {\n        const colorBar = this.getDOMElement().querySelector('svg .editor-icon-color-bar');\n\n        if (!colorBar) {\n            throw new Error(`Could not find expected color bar in the icon for this ${this.definition.label} button`);\n        }\n\n        return (colorBar as HTMLElement);\n    }\n\n    updateState(state: EditorUiStateUpdate): void {\n        super.updateState(state);\n\n        if ($isRangeSelection(state.selection)) {\n            const value = $getSelectionStyleValueForProperty(state.selection, this.style);\n            const colorBar = this.getColorBar();\n            colorBar.setAttribute('fill', value);\n        }\n    }\n\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/blocks/color-field.ts",
    "content": "import {EditorContainerUiElement, EditorUiBuilderDefinition, EditorUiContext} from \"../core\";\nimport {EditorFormField, EditorFormFieldDefinition} from \"../forms\";\nimport {EditorColorPicker} from \"./color-picker\";\nimport {EditorDropdownButton} from \"./dropdown-button\";\n\nimport colorDisplayIcon from \"@icons/editor/color-display.svg\"\n\nexport class EditorColorField extends EditorContainerUiElement {\n    protected input: EditorFormField;\n    protected pickerButton: EditorDropdownButton;\n\n    constructor(input: EditorFormField) {\n        super([]);\n\n        this.input = input;\n\n        this.pickerButton = new EditorDropdownButton({\n            button: { icon: colorDisplayIcon, label: 'Select color'}\n        }, [\n            new EditorColorPicker(this.onColorSelect.bind(this))\n        ]);\n        this.addChildren(this.pickerButton, this.input);\n    }\n\n    protected buildDOM(): HTMLElement {\n        const dom = this.input.getDOMElement();\n        dom.append(this.pickerButton.getDOMElement());\n        dom.classList.add('editor-color-field-container');\n\n        const field = dom.querySelector('input') as HTMLInputElement;\n        field.addEventListener('change', () => {\n            this.setIconColor(field.value);\n        });\n\n        return dom;\n    }\n\n    onColorSelect(color: string, context: EditorUiContext): void {\n        this.input.setValue(color);\n    }\n\n    setIconColor(color: string) {\n        const icon = this.getDOMElement().querySelector('svg .editor-icon-color-display');\n        if (icon) {\n            icon.setAttribute('fill', color || 'url(#pattern2)');\n        }\n    }\n}\n\nexport function colorFieldBuilder(field: EditorFormFieldDefinition): EditorUiBuilderDefinition {\n    return {\n        build() {\n            return new EditorColorField(new EditorFormField(field));\n        }\n    }\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/blocks/color-picker.ts",
    "content": "import {EditorUiContext, EditorUiElement} from \"../core\";\nimport {el} from \"../../../utils/dom\";\n\nimport removeIcon from \"@icons/editor/color-clear.svg\";\nimport selectIcon from \"@icons/editor/color-select.svg\";\nimport {uniqueIdSmall} from \"../../../../services/util\";\n\nconst colorChoices = [\n    '#000000',\n    '#ffffff',\n\n    '#BFEDD2',\n    '#FBEEB8',\n    '#F8CAC6',\n    '#ECCAFA',\n    '#C2E0F4',\n\n    '#2DC26B',\n    '#F1C40F',\n    '#E03E2D',\n    '#B96AD9',\n    '#3598DB',\n\n    '#169179',\n    '#E67E23',\n    '#BA372A',\n    '#843FA1',\n    '#236FA1',\n\n    '#ECF0F1',\n    '#CED4D9',\n    '#95A5A6',\n    '#7E8C8D',\n    '#34495E',\n];\n\nconst storageKey = 'bs-lexical-custom-colors';\n\nexport type EditorColorPickerCallback = (color: string, context: EditorUiContext) => void;\n\nexport class EditorColorPicker extends EditorUiElement {\n\n    protected callback: EditorColorPickerCallback;\n\n    constructor(callback: EditorColorPickerCallback) {\n        super();\n        this.callback = callback;\n    }\n\n    buildDOM(): HTMLElement {\n        const id = uniqueIdSmall();\n\n        const allChoices = [...colorChoices, ...this.getCustomColorChoices()];\n        const colorOptions = allChoices.map(choice => {\n            return el('div', {\n                class: 'editor-color-select-option',\n                style: `background-color: ${choice}`,\n                'data-color': choice,\n                'aria-label': choice,\n            });\n        });\n\n        const removeButton = el('div', {\n            class: 'editor-color-select-option',\n            'data-color': '',\n            title: this.getContext().translate('Remove color'),\n        }, []);\n        removeButton.innerHTML = removeIcon;\n        colorOptions.push(removeButton);\n\n        const selectButton = el('label', {\n            class: 'editor-color-select-option',\n            for: `color-select-${id}`,\n            'data-color': '',\n            title: this.getContext().translate('Custom color'),\n        }, []);\n        selectButton.innerHTML = selectIcon;\n        colorOptions.push(selectButton);\n\n        const input = el('input', {type: 'color', hidden: 'true', id: `color-select-${id}`}) as HTMLInputElement;\n        colorOptions.push(input);\n        input.addEventListener('change', e => {\n            if (input.value) {\n                this.storeCustomColorChoice(input.value);\n                this.setColor(input.value);\n                this.rebuildDOM();\n            }\n        });\n\n        const colorRows = [];\n        for (let i = 0; i < colorOptions.length; i+=5) {\n            const options = colorOptions.slice(i, i + 5);\n            colorRows.push(el('div', {\n                class: 'editor-color-select-row',\n            }, options));\n        }\n\n        const wrapper = el('div', {\n            class: 'editor-color-select',\n        }, colorRows);\n\n        wrapper.addEventListener('click', this.onClick.bind(this));\n\n        return wrapper;\n    }\n\n    storeCustomColorChoice(color: string) {\n        if (colorChoices.includes(color)) {\n            return;\n        }\n\n        const customColors: string[] = this.getCustomColorChoices();\n        if (customColors.includes(color)) {\n            return;\n        }\n\n        customColors.push(color);\n        window.localStorage.setItem(storageKey, JSON.stringify(customColors));\n    }\n\n    getCustomColorChoices(): string[] {\n        return JSON.parse(window.localStorage.getItem(storageKey) || '[]');\n    }\n\n    onClick(event: MouseEvent) {\n        const colorEl = (event.target as HTMLElement).closest('[data-color]') as HTMLElement;\n        if (!colorEl) return;\n\n        const color = colorEl.dataset.color as string;\n        this.setColor(color);\n    }\n\n    setColor(color: string) {\n        this.callback(color, this.getContext());\n    }\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/blocks/dropdown-button.ts",
    "content": "import {EditorContainerUiElement, EditorUiElement} from \"../core\";\nimport {EditorBasicButtonDefinition, EditorButton} from \"../buttons\";\nimport {el} from \"../../../utils/dom\";\nimport {EditorMenuButton} from \"./menu-button\";\n\nexport type EditorDropdownButtonOptions = {\n    showOnHover?: boolean;\n    direction?: 'vertical'|'horizontal';\n    showAside?: boolean;\n    hideOnAction?: boolean;\n    button: EditorBasicButtonDefinition|EditorButton;\n};\n\nconst defaultOptions: EditorDropdownButtonOptions = {\n    showOnHover: false,\n    direction: 'horizontal',\n    showAside: undefined,\n    hideOnAction: true,\n    button: {label: 'Menu'},\n}\n\nexport class EditorDropdownButton extends EditorContainerUiElement {\n    protected button: EditorButton;\n    protected childItems: EditorUiElement[];\n    protected open: boolean = false;\n    protected options: EditorDropdownButtonOptions;\n\n    constructor(options: EditorDropdownButtonOptions, children: EditorUiElement[]) {\n        super(children);\n        this.childItems = children;\n        this.options = Object.assign({}, defaultOptions, options);\n\n        if (options.button instanceof EditorButton) {\n            this.button = options.button;\n        } else {\n            const type = options.button.format === 'long' ? EditorMenuButton : EditorButton;\n            this.button = new type({\n                ...options.button,\n                action() {\n                    return false;\n                },\n                isActive: () => {\n                    return this.open;\n                },\n            });\n        }\n\n        this.addChildren(this.button);\n    }\n\n    insertItems(...items: EditorUiElement[]) {\n        this.addChildren(...items);\n        this.childItems.push(...items);\n    }\n\n    protected buildDOM(): HTMLElement {\n        const button = this.button.getDOMElement();\n\n        const childElements: HTMLElement[] = this.childItems.map(child => child.getDOMElement());\n        const menu = el('div', {\n            class: `editor-dropdown-menu editor-dropdown-menu-${this.options.direction}`,\n            hidden: 'true',\n        }, childElements);\n\n        const wrapper = el('div', {\n            class: 'editor-dropdown-menu-container',\n        }, [button, menu]);\n\n        this.getContext().manager.dropdowns.handle({toggle: button, menu : menu,\n            showOnHover: this.options.showOnHover,\n            showAside: typeof this.options.showAside === 'boolean' ? this.options.showAside : (this.options.direction === 'vertical'),\n            onOpen : () => {\n            this.open = true;\n            this.getContext().manager.triggerStateUpdateForElement(this.button);\n        }, onClose : () => {\n            this.open = false;\n            this.getContext().manager.triggerStateUpdateForElement(this.button);\n        }});\n\n        if (this.options.hideOnAction) {\n            this.onEvent('button-action', () => {\n                this.getContext().manager.dropdowns.closeAll();\n            }, wrapper);\n        }\n\n        return wrapper;\n    }\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/blocks/external-content.ts",
    "content": "import {EditorUiElement} from \"../core\";\nimport {el} from \"../../../utils/dom\";\n\nexport class ExternalContent extends EditorUiElement {\n\n    /**\n     * The URL for HTML to be loaded from.\n     */\n    protected url: string = '';\n\n    constructor(url: string) {\n        super();\n        this.url = url;\n    }\n\n    buildDOM(): HTMLElement {\n        const wrapper = el('div', {\n            class: 'editor-external-content',\n        });\n\n        window.$http.get(this.url).then(resp => {\n            if (typeof resp.data === 'string') {\n                wrapper.innerHTML = resp.data;\n            }\n        });\n\n        return wrapper;\n    }\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/blocks/format-menu.ts",
    "content": "import {EditorUiStateUpdate, EditorContainerUiElement} from \"../core\";\nimport {EditorButton} from \"../buttons\";\nimport {el} from \"../../../utils/dom\";\n\nexport class EditorFormatMenu extends EditorContainerUiElement {\n    buildDOM(): HTMLElement {\n        const childElements: HTMLElement[] = this.getChildren().map(child => child.getDOMElement());\n        const menu = el('div', {\n            class: 'editor-format-menu-dropdown editor-dropdown-menu editor-dropdown-menu-vertical',\n            hidden: 'true',\n        }, childElements);\n\n        const toggle = el('button', {\n            class: 'editor-format-menu-toggle editor-button',\n            type: 'button',\n        }, [this.trans('Formats')]);\n\n        const wrapper = el('div', {\n            class: 'editor-format-menu editor-dropdown-menu-container',\n        }, [toggle, menu]);\n\n        this.getContext().manager.dropdowns.handle({toggle : toggle, menu : menu});\n\n        this.onEvent('button-action', () => {\n            this.getContext().manager.dropdowns.closeAll();\n        }, wrapper);\n\n        return wrapper;\n    }\n\n    updateState(state: EditorUiStateUpdate) {\n        super.updateState(state);\n\n        for (const child of this.children) {\n            if (child instanceof EditorButton && child.isActive()) {\n                this.updateToggleLabel(child.getLabel());\n                return;\n            }\n\n            if (child instanceof EditorContainerUiElement) {\n                for (const grandchild of child.getChildren()) {\n                    if (grandchild instanceof EditorButton && grandchild.isActive()) {\n                        this.updateToggleLabel(grandchild.getLabel());\n                        return;\n                    }\n                }\n            }\n        }\n\n        this.updateToggleLabel(this.trans('Formats'));\n    }\n\n    protected updateToggleLabel(text: string): void {\n        const button = this.getDOMElement().querySelector('button');\n        if (button) {\n            button.innerText = text;\n        }\n    }\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/blocks/format-preview-button.ts",
    "content": "import {EditorButton, EditorButtonDefinition} from \"../buttons\";\nimport {el} from \"../../../utils/dom\";\n\nexport class FormatPreviewButton extends EditorButton {\n    protected previewSampleElement: HTMLElement;\n\n    constructor(previewSampleElement: HTMLElement,definition: EditorButtonDefinition) {\n        super(definition);\n        this.previewSampleElement = previewSampleElement;\n    }\n\n    protected buildDOM(): HTMLButtonElement {\n        const button = super.buildDOM();\n        button.innerHTML = '';\n\n        const preview = el('span', {\n            class: 'editor-button-format-preview'\n        }, [this.getLabel()]);\n\n        const stylesToApply = this.getStylesFromPreview();\n        for (const style of Object.keys(stylesToApply)) {\n            preview.style.setProperty(style, stylesToApply[style]);\n        }\n\n        button.append(preview);\n        return button;\n    }\n\n    protected getStylesFromPreview(): Record<string, string> {\n        const wrap = el('div', {style: 'display: none', hidden: 'true', class: 'page-content'});\n        const sampleClone = this.previewSampleElement.cloneNode() as HTMLElement;\n        sampleClone.textContent = this.getLabel();\n        wrap.append(sampleClone);\n        document.body.append(wrap);\n\n        const propertiesToFetch = ['color', 'font-size', 'background-color', 'border-inline-start'];\n        const propertiesToReturn: Record<string, string> = {};\n\n        const computed = window.getComputedStyle(sampleClone);\n        for (const property of propertiesToFetch) {\n            propertiesToReturn[property] = computed.getPropertyValue(property);\n        }\n        wrap.remove();\n\n        return propertiesToReturn;\n    }\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/blocks/link-field.ts",
    "content": "import {EditorContainerUiElement} from \"../core\";\nimport {el} from \"../../../utils/dom\";\nimport {EditorFormField} from \"../forms\";\nimport {$getAllNodesOfType} from \"../../../utils/nodes\";\nimport {uniqueIdSmall} from \"../../../../services/util\";\nimport {$isHeadingNode, HeadingNode} from \"@lexical/rich-text/LexicalHeadingNode\";\n\nexport class LinkField extends EditorContainerUiElement {\n    protected input: EditorFormField;\n    protected headerMap = new Map<string, HeadingNode>();\n\n    constructor(input: EditorFormField) {\n        super([input]);\n\n        this.input = input;\n    }\n\n    buildDOM(): HTMLElement {\n        const listId = 'editor-form-datalist-' + this.input.getName() + '-' + Date.now();\n        const inputOuterDOM = this.input.getDOMElement();\n        const inputFieldDOM = inputOuterDOM.querySelector('input');\n        inputFieldDOM?.setAttribute('list', listId);\n        inputFieldDOM?.setAttribute('autocomplete', 'off');\n        const datalist = el('datalist', {id: listId});\n\n        const container = el('div', {\n            class: 'editor-link-field-container',\n        }, [inputOuterDOM, datalist]);\n\n        inputFieldDOM?.addEventListener('focusin', () => {\n            this.updateDataList(datalist);\n        });\n\n        inputFieldDOM?.addEventListener('input', () => {\n            const value = inputFieldDOM.value;\n            const header = this.headerMap.get(value);\n            if (header) {\n                this.updateFormFromHeader(header);\n            }\n        });\n\n        return container;\n    }\n\n    updateFormFromHeader(header: HeadingNode) {\n        this.getHeaderIdAndText(header).then(({id, text}) => {\n            const modal =  this.getContext().manager.getActiveModal('link');\n            if (modal) {\n                modal.getForm().setValues({\n                    url: `#${id}`,\n                    text: text,\n                    title: text,\n                });\n            }\n        });\n    }\n\n    getHeaderIdAndText(header: HeadingNode): Promise<{id: string, text: string}> {\n        return new Promise((res) => {\n            this.getContext().editor.update(() => {\n                let id = header.getId();\n                if (!id) {\n                    id = 'header-' + uniqueIdSmall();\n                    header.setId(id);\n                }\n\n                const text = header.getTextContent();\n                res({id, text});\n            });\n        });\n    }\n\n    updateDataList(listEl: HTMLElement) {\n        this.getContext().editor.getEditorState().read(() => {\n            const headers = $getAllNodesOfType($isHeadingNode) as HeadingNode[];\n\n            this.headerMap.clear();\n            const listEls: HTMLElement[] = [];\n\n            for (const header of headers) {\n                const key = 'header-' + header.getKey();\n                this.headerMap.set(key, header);\n                listEls.push(el('option', {\n                    value: key,\n                    label: header.getTextContent().substring(0, 54),\n                }));\n            }\n\n            listEl.innerHTML = '';\n            listEl.append(...listEls);\n        });\n    }\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/blocks/menu-button.ts",
    "content": "import {EditorButton} from \"../buttons\";\nimport {el} from \"../../../utils/dom\";\nimport arrowIcon from \"@icons/chevron-right.svg\"\n\nexport class EditorMenuButton extends EditorButton {\n    protected buildDOM(): HTMLButtonElement {\n        const dom = super.buildDOM();\n\n        const icon = el('div', {class: 'editor-menu-button-icon'});\n        icon.innerHTML = arrowIcon;\n        dom.append(icon);\n\n        return dom;\n    }\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/blocks/overflow-container.ts",
    "content": "import {EditorContainerUiElement, EditorUiElement} from \"../core\";\nimport {EditorDropdownButton} from \"./dropdown-button\";\nimport moreHorizontal from \"@icons/editor/more-horizontal.svg\"\nimport {el} from \"../../../utils/dom\";\n\n\nexport class EditorOverflowContainer extends EditorContainerUiElement {\n\n    protected size: number;\n    protected overflowButton: EditorDropdownButton;\n    protected content: EditorUiElement[];\n    protected label: string;\n\n    constructor(label: string, size: number, children: EditorUiElement[]) {\n        super(children);\n        this.label = label;\n        this.size = size;\n        this.content = children;\n        this.overflowButton = new EditorDropdownButton({\n            button: {\n                label: 'More',\n                icon: moreHorizontal,\n            },\n            hideOnAction: false,\n        }, []);\n        this.addChildren(this.overflowButton);\n    }\n\n    addChild(child: EditorUiElement, targetIndex: number = -1): void {\n        this.content.splice(targetIndex, 0, child);\n        this.addChildren(child);\n    }\n\n    protected buildDOM(): HTMLElement {\n        const slicePosition = this.content.length > this.size ? this.size - 1 : this.size;\n        const visibleChildren = this.content.slice(0, slicePosition);\n        const invisibleChildren = this.content.slice(slicePosition);\n\n        const visibleElements = visibleChildren.map(child => child.getDOMElement());\n        if (invisibleChildren.length > 0) {\n            this.removeChildren(...invisibleChildren);\n            this.overflowButton.insertItems(...invisibleChildren);\n            visibleElements.push(this.overflowButton.getDOMElement());\n        }\n\n        return el('div', {\n            class: 'editor-overflow-container',\n        }, visibleElements);\n    }\n\n    getLabel(): string {\n        return this.label;\n    }\n\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/blocks/separator.ts",
    "content": "import {EditorUiElement} from \"../core\";\nimport {el} from \"../../../utils/dom\";\n\nexport class EditorSeparator extends EditorUiElement {\n    buildDOM(): HTMLElement {\n        return el('div', {\n            class: 'editor-separator',\n        });\n    }\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/blocks/table-creator.ts",
    "content": "import {EditorUiElement} from \"../core\";\nimport {$createTableNodeWithDimensions} from \"@lexical/table\";\nimport {$insertNewBlockNodeAtSelection} from \"../../../utils/selection\";\nimport {el} from \"../../../utils/dom\";\n\n\nexport class EditorTableCreator extends EditorUiElement {\n\n    buildDOM(): HTMLElement {\n        const size = 10;\n        const rows: HTMLElement[] = [];\n        const cells: HTMLElement[] = [];\n\n        for (let row = 1; row < size + 1; row++) {\n            const rowCells = [];\n            for (let column = 1; column < size + 1; column++) {\n                const cell = el('div', {\n                    class: 'editor-table-creator-cell',\n                    'data-rows': String(row),\n                    'data-columns': String(column),\n                });\n                rowCells.push(cell);\n                cells.push(cell);\n            }\n            rows.push(el('div', {\n                class: 'editor-table-creator-row'\n            }, rowCells));\n        }\n\n        const display = el('div', {class: 'editor-table-creator-display'}, ['0 x 0']);\n        const grid = el('div', {class: 'editor-table-creator-grid'}, rows);\n        grid.addEventListener('mousemove', event => {\n            const cell = (event.target as HTMLElement).closest('.editor-table-creator-cell') as HTMLElement|null;\n            if (cell) {\n                const row = Number(cell.dataset.rows || 0);\n                const column = Number(cell.dataset.columns || 0);\n                this.updateGridSelection(row, column, cells, display)\n            }\n        });\n\n        grid.addEventListener('click', event => {\n            const cell = (event.target as HTMLElement).closest('.editor-table-creator-cell');\n            if (cell) {\n                this.onCellClick(cell as HTMLElement);\n            }\n        });\n\n        grid.addEventListener('mouseleave', event => {\n             this.updateGridSelection(0, 0, cells, display);\n        });\n\n        return el('div', {\n            class: 'editor-table-creator',\n        }, [\n            grid,\n            display,\n        ]);\n    }\n\n    updateGridSelection(rows: number, columns: number, cells: HTMLElement[], display: HTMLElement) {\n        for (const cell of cells) {\n            const active = Number(cell.dataset.rows) <= rows && Number(cell.dataset.columns) <= columns;\n            cell.classList.toggle('active', active);\n        }\n\n        display.textContent = `${rows} x ${columns}`;\n    }\n\n    onCellClick(cell: HTMLElement) {\n        const rows = Number(cell.dataset.rows || 0);\n        const columns = Number(cell.dataset.columns || 0);\n        if (rows < 1 || columns < 1) {\n            return;\n        }\n\n        const targetColWidth = Math.min(Math.round(840 / columns), 240);\n        const colWidths = Array(columns).fill(targetColWidth + 'px');\n\n        this.getContext().editor.update(() => {\n            const table = $createTableNodeWithDimensions(rows, columns, false);\n            table.setColWidths(colWidths);\n            $insertNewBlockNodeAtSelection(table);\n        });\n    }\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/buttons.ts",
    "content": "import {BaseSelection} from \"lexical\";\nimport {EditorUiContext, EditorUiElement, EditorUiStateUpdate} from \"./core\";\n\nimport {el} from \"../../utils/dom\";\n\nexport interface EditorBasicButtonDefinition {\n    label: string;\n    icon?: string|undefined;\n    format?: 'small' | 'long';\n}\n\nexport interface EditorButtonDefinition extends EditorBasicButtonDefinition {\n    /**\n     * The action to perform when the button is used.\n     * This can return false to indicate that the completion of the action should\n     * NOT be communicated to parent UI elements, which is what occurs by default.\n     */\n    action: (context: EditorUiContext, button: EditorButton) => void|false|Promise<void|boolean>;\n    isActive: (selection: BaseSelection|null, context: EditorUiContext) => boolean;\n    isDisabled?: (selection: BaseSelection|null, context: EditorUiContext) => boolean;\n    setup?: (context: EditorUiContext, button: EditorButton) => void;\n}\n\nexport class EditorButton extends EditorUiElement {\n    protected definition: EditorButtonDefinition;\n    protected active: boolean = false;\n    protected completedSetup: boolean = false;\n    protected disabled: boolean = false;\n\n    constructor(definition: EditorButtonDefinition|EditorBasicButtonDefinition) {\n        super();\n\n        if ((definition as EditorButtonDefinition).action !== undefined) {\n            this.definition = definition as EditorButtonDefinition;\n        } else {\n            this.definition = {\n                ...definition,\n                action() {\n                    return false;\n                },\n                isActive: () => {\n                    return false;\n                }\n            };\n        }\n    }\n\n    setContext(context: EditorUiContext) {\n        super.setContext(context);\n\n        if (this.definition.setup && !this.completedSetup) {\n            this.definition.setup(context, this);\n            this.completedSetup = true;\n        }\n    }\n\n    protected buildDOM(): HTMLButtonElement {\n        const label = this.getLabel();\n        const format = this.definition.format || 'small';\n        const children: (string|HTMLElement)[] = [];\n\n        if (this.definition.icon || format === 'long') {\n            const icon = el('div', {class: 'editor-button-icon'});\n            icon.innerHTML = this.definition.icon || '';\n            children.push(icon);\n        }\n\n        if (!this.definition.icon ||format === 'long') {\n            const text = el('div', {class: 'editor-button-text'}, [label]);\n            children.push(text);\n        }\n\n        const button = el('button', {\n            type: 'button',\n            class: `editor-button editor-button-${format}`,\n            title: this.definition.icon ? label : null,\n            disabled: this.disabled ? 'true' : null,\n        }, children) as HTMLButtonElement;\n\n        button.addEventListener('click', this.onClick.bind(this));\n\n        return button;\n    }\n\n    protected onClick() {\n        const result = this.definition.action(this.getContext(), this);\n        if (result instanceof Promise) {\n            result.then(result => {\n                if (result === false) {\n                    this.emitEvent('button-action');\n                }\n            });\n        } else if (result !== false) {\n            this.emitEvent('button-action');\n        }\n    }\n\n    protected updateActiveState(selection: BaseSelection|null) {\n        const isActive = this.definition.isActive(selection, this.getContext());\n        this.setActiveState(isActive);\n    }\n\n    protected updateDisabledState(selection: BaseSelection|null) {\n        if (this.definition.isDisabled) {\n            const isDisabled = this.definition.isDisabled(selection, this.getContext());\n            this.toggleDisabled(isDisabled);\n        }\n    }\n\n    setActiveState(active: boolean) {\n        this.active = active;\n        this.dom?.classList.toggle('editor-button-active', this.active);\n    }\n\n    updateState(state: EditorUiStateUpdate): void {\n        this.updateActiveState(state.selection);\n        this.updateDisabledState(state.selection);\n    }\n\n    isActive(): boolean {\n        return this.active;\n    }\n\n    getLabel(): string {\n        return this.trans(this.definition.label);\n    }\n\n    toggleDisabled(disabled: boolean) {\n        this.disabled = disabled;\n        if (disabled) {\n            this.dom?.setAttribute('disabled', 'true');\n        } else {\n            this.dom?.removeAttribute('disabled');\n        }\n    }\n}\n"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/core.ts",
    "content": "import {BaseSelection, LexicalEditor} from \"lexical\";\nimport {EditorUIManager} from \"./manager\";\n\nimport {el} from \"../../utils/dom\";\n\nexport type EditorUiStateUpdate = {\n    editor: LexicalEditor;\n    selection: BaseSelection|null;\n};\n\nexport type EditorUiContext = {\n    editor: LexicalEditor; // Lexical editor instance\n    editorDOM: HTMLElement; // DOM element the editor is bound to\n    containerDOM: HTMLElement; // DOM element which contains all editor elements\n    scrollDOM: HTMLElement; // DOM element which is the main content scroll container\n    translate: (text: string) => string; // Translate function\n    error: (text: string|Error) => void; // Error reporting function\n    manager: EditorUIManager; // UI Manager instance for this editor\n    options: Record<string, any>; // General user options which may be used by sub elements\n};\n\nexport interface EditorUiBuilderDefinition {\n    build: () => EditorUiElement;\n}\n\nexport function isUiBuilderDefinition(object: any): object is EditorUiBuilderDefinition {\n    return 'build' in object;\n}\n\nexport abstract class EditorUiElement {\n    protected dom: HTMLElement|null = null;\n    private context: EditorUiContext|null = null;\n    private abortController: AbortController = new AbortController();\n\n    protected abstract buildDOM(): HTMLElement;\n\n    setContext(context: EditorUiContext): void {\n        this.context = context;\n    }\n\n    getContext(): EditorUiContext {\n        if (this.context === null) {\n            throw new Error('Attempted to use EditorUIContext before it has been set');\n        }\n\n        return this.context;\n    }\n\n    getDOMElement(): HTMLElement {\n        if (!this.dom) {\n            this.dom = this.buildDOM();\n        }\n\n        return this.dom;\n    }\n\n    rebuildDOM(): HTMLElement {\n        const newDOM = this.buildDOM();\n        this.dom?.replaceWith(newDOM);\n        this.dom = newDOM;\n        return this.dom;\n    }\n\n    trans(text: string) {\n        return this.getContext().translate(text);\n    }\n\n    updateState(state: EditorUiStateUpdate): void {\n        return;\n    }\n\n    emitEvent(name: string, data: object = {}): void {\n        if (this.dom) {\n            this.dom.dispatchEvent(new CustomEvent('editor::' + name, {detail: data, bubbles: true}));\n        }\n    }\n\n    onEvent(name: string, callback: (data: object) => any, listenTarget: HTMLElement|null = null): void {\n        const target = listenTarget || this.dom;\n        if (target) {\n            target.addEventListener('editor::' + name, ((event: CustomEvent) => {\n                callback(event.detail);\n            }) as EventListener, { signal: this.abortController.signal });\n        }\n    }\n\n    teardown(): void {\n        if (this.dom && this.dom.isConnected) {\n            this.dom.remove();\n        }\n        this.abortController.abort('teardown');\n    }\n}\n\nexport class EditorContainerUiElement extends EditorUiElement {\n    protected children : EditorUiElement[] = [];\n\n    constructor(children: EditorUiElement[]) {\n        super();\n        this.children.push(...children);\n    }\n\n    protected buildDOM(): HTMLElement {\n        return el('div', {}, this.getChildren().map(child => child.getDOMElement()));\n    }\n\n    getChildren(): EditorUiElement[] {\n        return this.children;\n    }\n\n    protected addChildren(...children: EditorUiElement[]): void {\n        this.children.push(...children);\n    }\n\n    protected removeChildren(...children: EditorUiElement[]): void {\n        for (const child of children) {\n            this.removeChild(child);\n        }\n    }\n\n    protected removeChild(child: EditorUiElement) {\n        const index = this.children.indexOf(child);\n        if (index !== -1) {\n            this.children.splice(index, 1);\n        }\n    }\n\n    updateState(state: EditorUiStateUpdate): void {\n        for (const child of this.children) {\n            child.updateState(state);\n        }\n    }\n\n    setContext(context: EditorUiContext) {\n        super.setContext(context);\n        for (const child of this.getChildren()) {\n            child.setContext(context);\n        }\n    }\n\n    teardown() {\n        for (const child of this.children) {\n            child.teardown();\n        }\n        super.teardown();\n    }\n}\n\nexport class EditorSimpleClassContainer extends EditorContainerUiElement {\n    protected className;\n\n    constructor(className: string, children: EditorUiElement[]) {\n        super(children);\n        this.className = className;\n    }\n\n    protected buildDOM(): HTMLElement {\n        return el('div', {\n            class: this.className,\n        }, this.getChildren().map(child => child.getDOMElement()));\n    }\n}\n\n"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/decorator.ts",
    "content": "import {EditorUiContext} from \"./core\";\nimport {LexicalNode} from \"lexical\";\n\nexport interface EditorDecoratorAdapter {\n    type: string;\n    getNode(): LexicalNode;\n}\n\nexport abstract class EditorDecorator {\n\n    protected node: LexicalNode | null = null;\n    protected context: EditorUiContext;\n\n    private onDestroyCallbacks: (() => void)[] = [];\n\n    constructor(context: EditorUiContext) {\n        this.context = context;\n    }\n\n    protected getNode(): LexicalNode {\n        if (!this.node) {\n            throw new Error('Attempted to get use node without it being set');\n        }\n\n        return this.node;\n    }\n\n    setNode(node: LexicalNode) {\n        this.node = node;\n    }\n\n    /**\n     * Register a callback to be ran on destroy of this decorator's node.\n     */\n    protected onDestroy(callback: () => void) {\n        this.onDestroyCallbacks.push(callback);\n    }\n\n    /**\n     * Render the decorator.\n     * Can run on both creation and update for a node decorator.\n     * If an element is returned, this will be appended to the element\n     * that is being decorated.\n     */\n    abstract render(decorated: HTMLElement): HTMLElement|void;\n\n    /**\n     * Destroy this decorator. Used for tear-down operations upon destruction\n     * of the underlying node this decorator is attached to.\n     */\n    teardown(): void {\n        for (const callback of this.onDestroyCallbacks) {\n            callback();\n        }\n    }\n\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/forms.ts",
    "content": "import {\n    EditorUiContext,\n    EditorUiElement,\n    EditorContainerUiElement,\n    EditorUiBuilderDefinition,\n    isUiBuilderDefinition\n} from \"./core\";\nimport {uniqueId} from \"../../../services/util\";\nimport {el} from \"../../utils/dom\";\n\nexport interface EditorFormFieldDefinition {\n    label: string;\n    name: string;\n    type: 'text' | 'select' | 'textarea' | 'checkbox' | 'hidden';\n}\n\nexport interface EditorSelectFormFieldDefinition extends EditorFormFieldDefinition {\n    type: 'select',\n    valuesByLabel: Record<string, string>\n}\n\nexport type EditorFormFields = (EditorFormFieldDefinition|EditorUiBuilderDefinition)[];\n\ninterface EditorFormTabDefinition {\n    label: string;\n    contents: EditorFormFields;\n}\n\nexport interface EditorFormDefinition {\n    submitText: string;\n    action: (formData: FormData, context: EditorUiContext) => Promise<boolean>;\n    fields: EditorFormFields;\n}\n\nexport class EditorFormField extends EditorUiElement {\n    protected definition: EditorFormFieldDefinition;\n\n    constructor(definition: EditorFormFieldDefinition) {\n        super();\n        this.definition = definition;\n    }\n\n    setValue(value: string) {\n        const input = this.getDOMElement().querySelector('input,select,textarea') as HTMLInputElement;\n        if (this.definition.type === 'checkbox') {\n            input.checked = Boolean(value);\n        } else {\n            input.value = value;\n        }\n        input.dispatchEvent(new Event('change'));\n    }\n\n    getName(): string {\n        return this.definition.name;\n    }\n\n    protected buildDOM(): HTMLElement {\n        const id = `editor-form-field-${this.definition.name}-${Date.now()}`;\n        let input: HTMLElement;\n\n        if (this.definition.type === 'select') {\n            const options = (this.definition as EditorSelectFormFieldDefinition).valuesByLabel\n            const labels = Object.keys(options);\n            const optionElems = labels.map(label => el('option', {value: options[label]}, [this.trans(label)]));\n            input = el('select', {id, name: this.definition.name, class: 'editor-form-field-input'}, optionElems);\n        } else if (this.definition.type === 'textarea') {\n            input = el('textarea', {id, name: this.definition.name, class: 'editor-form-field-input'});\n        } else if (this.definition.type === 'checkbox') {\n            input = el('input', {id, name: this.definition.name, type: 'checkbox', class: 'editor-form-field-input-checkbox', value: 'true'});\n        } else if (this.definition.type === 'hidden') {\n            input = el('input', {id, name: this.definition.name, type: 'hidden'});\n            return el('div', {hidden: 'true'}, [input]);\n        } else {\n            input = el('input', {id, name: this.definition.name, class: 'editor-form-field-input'});\n        }\n\n        return el('div', {class: 'editor-form-field-wrapper'}, [\n            el('label', {class: 'editor-form-field-label', for: id}, [this.trans(this.definition.label)]),\n            input,\n        ]);\n    }\n}\n\nexport class EditorForm extends EditorContainerUiElement {\n    protected definition: EditorFormDefinition;\n    protected onCancel: null|(() => void) = null;\n    protected onSuccessfulSubmit: null|(() => void) = null;\n\n    constructor(definition: EditorFormDefinition) {\n        let children: (EditorFormField|EditorUiElement)[] = definition.fields.map(fieldDefinition => {\n            if (isUiBuilderDefinition(fieldDefinition)) {\n                return fieldDefinition.build();\n            }\n            return new EditorFormField(fieldDefinition)\n        });\n\n        super(children);\n        this.definition = definition;\n    }\n\n    focusOnFirst() {\n        const focusable = this.getDOMElement().querySelector('input,select,textarea');\n        if (focusable) {\n            (focusable as HTMLElement).focus();\n        }\n    }\n\n    setValues(values: Record<string, string>) {\n        for (const name of Object.keys(values)) {\n            const field = this.getFieldByName(name);\n            if (field) {\n                field.setValue(values[name]);\n            }\n        }\n    }\n\n    setOnCancel(callback: () => void) {\n        this.onCancel = callback;\n    }\n\n    setOnSuccessfulSubmit(callback: () => void) {\n        this.onSuccessfulSubmit = callback;\n    }\n\n    protected getFieldByName(name: string): EditorFormField|null {\n\n        const search = (children: EditorUiElement[]): EditorFormField|null => {\n            for (const child of children) {\n                if (child instanceof EditorFormField && child.getName() === name) {\n                    return child;\n                } else if (child instanceof EditorContainerUiElement) {\n                    const matchingChild = search(child.getChildren());\n                    if (matchingChild) {\n                        return matchingChild;\n                    }\n                }\n            }\n\n            return null;\n        };\n\n        return search(this.getChildren());\n    }\n\n    protected buildDOM(): HTMLElement {\n        const cancelButton = el('button', {type: 'button', class: 'editor-form-action-secondary'}, [this.trans('Cancel')]);\n        const form = el('form', {}, [\n            ...this.children.map(child => child.getDOMElement()),\n            el('div', {class: 'editor-form-actions'}, [\n                cancelButton,\n                el('button', {type: 'submit', class: 'editor-form-action-primary'}, [this.trans(this.definition.submitText)]),\n            ])\n        ]);\n\n        form.addEventListener('submit', async (event) => {\n            event.preventDefault();\n            const formData = new FormData(form as HTMLFormElement);\n            const result = await this.definition.action(formData, this.getContext());\n            if (result && this.onSuccessfulSubmit) {\n                this.onSuccessfulSubmit();\n            }\n        });\n\n        cancelButton.addEventListener('click', (event) => {\n            if (this.onCancel) {\n                this.onCancel();\n            }\n        });\n\n        return form;\n    }\n}\n\nexport class EditorFormTab extends EditorContainerUiElement {\n\n    protected definition: EditorFormTabDefinition;\n    protected fields: EditorUiElement[];\n    protected id: string;\n\n    constructor(definition: EditorFormTabDefinition) {\n        const fields = definition.contents.map(fieldDef => {\n            if (isUiBuilderDefinition(fieldDef)) {\n                return fieldDef.build();\n            }\n            return new EditorFormField(fieldDef)\n        });\n\n        super(fields);\n\n        this.definition = definition;\n        this.fields = fields;\n        this.id = uniqueId();\n    }\n\n    public getLabel(): string {\n        return this.getContext().translate(this.definition.label);\n    }\n\n    public getId(): string {\n        return this.id;\n    }\n\n    protected buildDOM(): HTMLElement {\n        return el(\n            'div',\n            {\n                class: 'editor-form-tab-content',\n                role: 'tabpanel',\n                id: `editor-tabpanel-${this.id}`,\n                'aria-labelledby': `editor-tab-${this.id}`,\n            },\n            this.fields.map(f => f.getDOMElement())\n        );\n    }\n}\nexport class EditorFormTabs extends EditorContainerUiElement {\n\n    protected definitions: EditorFormTabDefinition[] = [];\n    protected tabs: EditorFormTab[] = [];\n\n    constructor(definitions: EditorFormTabDefinition[]) {\n        const tabs: EditorFormTab[] = definitions.map(d => new EditorFormTab(d));\n        super(tabs);\n\n        this.definitions = definitions;\n        this.tabs = tabs;\n    }\n\n    protected buildDOM(): HTMLElement {\n        const controls: HTMLElement[] = [];\n        const contents: HTMLElement[] = [];\n\n        const selectTab = (tabIndex: number) => {\n            for (let i = 0; i < controls.length; i++) {\n                controls[i].setAttribute('aria-selected', (i === tabIndex) ? 'true' : 'false');\n            }\n            for (let i = 0; i < contents.length; i++) {\n                contents[i].hidden = !(i === tabIndex);\n            }\n        };\n\n        for (const tab of this.tabs) {\n            const button = el('button', {\n                class: 'editor-form-tab-control',\n                type: 'button',\n                role: 'tab',\n                id: `editor-tab-${tab.getId()}`,\n                'aria-controls': `editor-tabpanel-${tab.getId()}`\n            }, [tab.getLabel()]);\n            contents.push(tab.getDOMElement());\n            controls.push(button);\n\n            button.addEventListener('click', event => {\n                selectTab(controls.indexOf(button));\n            });\n        }\n\n        selectTab(0);\n\n        return el('div', {class: 'editor-form-tab-container'}, [\n            el('div', {class: 'editor-form-tab-controls'}, controls),\n            el('div', {class: 'editor-form-tab-contents'}, contents),\n        ]);\n    }\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts",
    "content": "interface HandleDropdownParams {\n    toggle: HTMLElement;\n    menu: HTMLElement;\n    showOnHover?: boolean,\n    onOpen?: Function | undefined;\n    onClose?: Function | undefined;\n    showAside?: boolean;\n}\n\nfunction positionMenu(menu: HTMLElement, toggle: HTMLElement, showAside: boolean) {\n    const toggleRect = toggle.getBoundingClientRect();\n    const menuBounds = menu.getBoundingClientRect();\n\n    menu.style.position = 'fixed';\n\n    if (showAside) {\n        let targetLeft = toggleRect.right;\n        const isRightOOB = toggleRect.right + menuBounds.width > window.innerWidth;\n        if (isRightOOB) {\n            targetLeft = Math.max(toggleRect.left - menuBounds.width, 0);\n        }\n\n        menu.style.top = toggleRect.top + 'px';\n        menu.style.left = targetLeft + 'px';\n    } else {\n        const isRightOOB = toggleRect.left + menuBounds.width > window.innerWidth;\n        let targetLeft = toggleRect.left;\n        if (isRightOOB) {\n            targetLeft = Math.max(toggleRect.right - menuBounds.width, 0);\n        }\n\n        menu.style.top = toggleRect.bottom + 'px';\n        menu.style.left = targetLeft + 'px';\n    }\n}\n\nexport class DropDownManager {\n\n    protected dropdownOptions: WeakMap<HTMLElement, HandleDropdownParams> = new WeakMap();\n    protected openDropdowns: Set<HTMLElement> = new Set();\n\n    constructor() {\n        this.onMenuMouseOver = this.onMenuMouseOver.bind(this);\n        this.onWindowClick = this.onWindowClick.bind(this);\n\n        window.addEventListener('click', this.onWindowClick);\n    }\n\n    teardown(): void {\n        window.removeEventListener('click', this.onWindowClick);\n    }\n\n    protected onWindowClick(event: MouseEvent): void {\n        const target = event.target as HTMLElement;\n        this.closeAllNotContainingElement(target);\n    }\n\n    protected closeAllNotContainingElement(element: HTMLElement): void {\n        for (const menu of this.openDropdowns) {\n            if (!menu.parentElement?.contains(element)) {\n                this.closeDropdown(menu);\n            }\n        }\n    }\n\n    protected onMenuMouseOver(event: MouseEvent): void {\n        const target = event.target as HTMLElement;\n        this.closeAllNotContainingElement(target);\n    }\n\n    /**\n     * Close all open dropdowns.\n     */\n    public closeAll(): void {\n        for (const menu of this.openDropdowns) {\n            this.closeDropdown(menu);\n        }\n    }\n\n    protected closeDropdown(menu: HTMLElement): void {\n        menu.hidden = true;\n        menu.style.removeProperty('position');\n        menu.style.removeProperty('left');\n        menu.style.removeProperty('top');\n\n        this.openDropdowns.delete(menu);\n        menu.removeEventListener('mouseover', this.onMenuMouseOver);\n\n        const onClose = this.getOptions(menu).onClose;\n        if (onClose) {\n            onClose();\n        }\n    }\n\n    protected openDropdown(menu: HTMLElement): void {\n        const {toggle, showAside, onOpen} = this.getOptions(menu);\n        menu.hidden = false\n        positionMenu(menu, toggle, Boolean(showAside));\n\n        this.openDropdowns.add(menu);\n        menu.addEventListener('mouseover', this.onMenuMouseOver);\n\n        if (onOpen) {\n            onOpen();\n        }\n    }\n\n    protected getOptions(menu: HTMLElement): HandleDropdownParams {\n        const options = this.dropdownOptions.get(menu);\n        if (!options) {\n            throw new Error(`Can't find options for dropdown menu`);\n        }\n\n        return options;\n    }\n\n    /**\n     * Add handling for a new dropdown.\n     */\n     public handle(options: HandleDropdownParams) {\n        const {menu, toggle, showOnHover} = options;\n\n        // Register dropdown\n        this.dropdownOptions.set(menu, options);\n\n        // Configure default events\n        const toggleShowing = (event: MouseEvent) => {\n            menu.hasAttribute('hidden') ? this.openDropdown(menu) : this.closeDropdown(menu);\n        };\n        toggle.addEventListener('click', toggleShowing);\n        if (showOnHover) {\n            toggle.addEventListener('mouseenter', () => {\n                this.openDropdown(menu);\n            });\n        }\n    }\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/helpers/mouse-drag-tracker.ts",
    "content": "\nexport type MouseDragTrackerDistance = {\n    x: number;\n    y: number;\n}\n\nexport type MouseDragTrackerOptions = {\n    down?: (event: MouseEvent, element: HTMLElement) => any;\n    move?: (event: MouseEvent, element: HTMLElement, distance: MouseDragTrackerDistance) => any;\n    up?: (event: MouseEvent, element: HTMLElement, distance: MouseDragTrackerDistance) => any;\n}\n\nexport class MouseDragTracker {\n    protected container: HTMLElement;\n    protected dragTargetSelector: string;\n    protected options: MouseDragTrackerOptions;\n\n    protected startX: number = 0;\n    protected startY: number = 0;\n    protected target: HTMLElement|null = null;\n\n    constructor(container: HTMLElement, dragTargetSelector: string, options: MouseDragTrackerOptions) {\n        this.container = container;\n        this.dragTargetSelector = dragTargetSelector;\n        this.options = options;\n\n        this.onMouseDown = this.onMouseDown.bind(this);\n        this.onMouseMove = this.onMouseMove.bind(this);\n        this.onMouseUp = this.onMouseUp.bind(this);\n        this.container.addEventListener('mousedown', this.onMouseDown);\n    }\n\n    teardown() {\n        this.container.removeEventListener('mousedown', this.onMouseDown);\n        this.container.removeEventListener('mouseup', this.onMouseUp);\n        this.container.removeEventListener('mousemove', this.onMouseMove);\n    }\n\n    protected onMouseDown(event: MouseEvent) {\n        this.target = (event.target as HTMLElement).closest(this.dragTargetSelector);\n        if (!this.target) {\n            return;\n        }\n\n        this.startX = event.screenX;\n        this.startY = event.screenY;\n\n        window.addEventListener('mousemove', this.onMouseMove);\n        window.addEventListener('mouseup', this.onMouseUp);\n        if (this.options.down) {\n            this.options.down(event, this.target);\n        }\n    }\n\n    protected onMouseMove(event: MouseEvent) {\n        if (this.options.move && this.target) {\n            this.options.move(event, this.target, {\n                x: event.screenX - this.startX,\n                y: event.screenY - this.startY,\n            });\n        }\n    }\n\n    protected onMouseUp(event: MouseEvent) {\n        window.removeEventListener('mousemove', this.onMouseMove);\n        window.removeEventListener('mouseup', this.onMouseUp);\n\n        if (this.options.up && this.target) {\n            this.options.up(event, this.target, {\n                x: event.screenX - this.startX,\n                y: event.screenY - this.startY,\n            });\n        }\n    }\n\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/helpers/node-resizer.ts",
    "content": "import {BaseSelection, LexicalNode,} from \"lexical\";\nimport {MouseDragTracker, MouseDragTrackerDistance} from \"./mouse-drag-tracker\";\nimport {el} from \"../../../utils/dom\";\nimport {$isImageNode} from \"@lexical/rich-text/LexicalImageNode\";\nimport {EditorUiContext} from \"../core\";\nimport {NodeHasSize} from \"lexical/nodes/common\";\nimport {$isMediaNode} from \"@lexical/rich-text/LexicalMediaNode\";\n\nfunction isNodeWithSize(node: LexicalNode): node is NodeHasSize&LexicalNode {\n    return $isImageNode(node) || $isMediaNode(node);\n}\n\nclass NodeResizer {\n    protected context: EditorUiContext;\n    protected resizerDOM: HTMLElement|null = null;\n    protected targetNode: LexicalNode|null = null;\n    protected scrollContainer: HTMLElement;\n\n    protected mouseTracker: MouseDragTracker|null = null;\n    protected activeSelection: string = '';\n    protected loadAbortController = new AbortController();\n\n    constructor(context: EditorUiContext) {\n        this.context = context;\n        this.scrollContainer = context.scrollDOM;\n\n        this.onSelectionChange = this.onSelectionChange.bind(this);\n        this.onTargetDOMLoad = this.onTargetDOMLoad.bind(this);\n\n        context.manager.onSelectionChange(this.onSelectionChange);\n    }\n\n    onSelectionChange(selection: BaseSelection|null) {\n        const nodes = selection?.getNodes() || [];\n        if (this.activeSelection) {\n            this.hide();\n        }\n\n        if (nodes.length === 1 && isNodeWithSize(nodes[0])) {\n            const node = nodes[0];\n            let nodeDOM = this.getTargetDOM(node)\n\n            if (nodeDOM) {\n                this.showForNode(node, nodeDOM);\n            }\n        }\n    }\n\n    protected getTargetDOM(targetNode: LexicalNode|null): HTMLElement|null {\n        if (targetNode == null) {\n            return null;\n        }\n\n        let nodeDOM =  this.context.editor.getElementByKey(targetNode.__key)\n        if (nodeDOM && nodeDOM.nodeName === 'SPAN') {\n            nodeDOM = nodeDOM.firstElementChild as HTMLElement;\n        }\n        return nodeDOM;\n    }\n\n    protected onTargetDOMLoad(): void {\n        this.updateResizerPosition();\n    }\n\n    teardown() {\n        this.context.manager.offSelectionChange(this.onSelectionChange);\n        this.hide();\n    }\n\n    protected showForNode(node: NodeHasSize&LexicalNode, targetDOM: HTMLElement) {\n        this.resizerDOM = this.buildDOM();\n        this.targetNode = node;\n\n        let ghost = el('span', {class: 'editor-node-resizer-ghost'});\n        if ($isImageNode(node)) {\n            ghost = el('img', {src: targetDOM.getAttribute('src'), class: 'editor-node-resizer-ghost'});\n        }\n        this.resizerDOM.append(ghost);\n\n        this.context.scrollDOM.append(this.resizerDOM);\n        this.updateResizerPosition();\n\n        this.mouseTracker = this.setupTracker(this.resizerDOM, node, targetDOM);\n        this.activeSelection = node.getKey();\n\n        if (targetDOM.matches('img, embed, iframe, object')) {\n            this.loadAbortController = new AbortController();\n            targetDOM.addEventListener('load', this.onTargetDOMLoad, { signal: this.loadAbortController.signal });\n        }\n    }\n\n    protected updateResizerPosition() {\n        const targetDOM = this.getTargetDOM(this.targetNode);\n        if (!this.resizerDOM || !targetDOM) {\n            return;\n        }\n\n        const scrollAreaRect = this.scrollContainer.getBoundingClientRect();\n        const nodeRect = targetDOM.getBoundingClientRect();\n        const top = nodeRect.top - (scrollAreaRect.top - this.scrollContainer.scrollTop);\n        const left = nodeRect.left - scrollAreaRect.left;\n\n        this.resizerDOM.style.top = `${top}px`;\n        this.resizerDOM.style.left = `${left}px`;\n        this.resizerDOM.style.width = nodeRect.width + 'px';\n        this.resizerDOM.style.height = nodeRect.height + 'px';\n    }\n\n    protected updateDOMSize(width: number, height: number): void {\n        if (!this.resizerDOM) {\n            return;\n        }\n\n        this.resizerDOM.style.width = width + 'px';\n        this.resizerDOM.style.height = height + 'px';\n    }\n\n    protected hide() {\n        this.mouseTracker?.teardown();\n        this.resizerDOM?.remove();\n        this.targetNode = null;\n        this.activeSelection = '';\n        this.loadAbortController.abort();\n    }\n\n    protected buildDOM() {\n        const handleClasses = ['nw', 'ne', 'se', 'sw'];\n        const handleElems = handleClasses.map(c => {\n            return el('div', {class: `editor-node-resizer-handle ${c}`});\n        });\n\n        return el('div', {\n            class: 'editor-node-resizer',\n        }, handleElems);\n    }\n\n    setupTracker(container: HTMLElement, node: NodeHasSize&LexicalNode, nodeDOM: HTMLElement): MouseDragTracker {\n        let startingWidth: number = 0;\n        let startingHeight: number = 0;\n        let startingRatio: number = 0;\n        let hasHeight = false;\n        let _this = this;\n        let flipXChange: boolean = false;\n        let flipYChange: boolean = false;\n\n        const calculateSize = (distance: MouseDragTrackerDistance): {width: number, height: number} => {\n            let xChange = distance.x;\n            if (flipXChange) {\n                xChange = 0 - xChange;\n            }\n            let yChange = distance.y;\n            if (flipYChange) {\n                yChange = 0 - yChange;\n            }\n\n            const balancedChange = Math.sqrt(Math.pow(Math.abs(xChange), 2) + Math.pow(Math.abs(yChange), 2));\n            const increase = xChange + yChange > 0;\n            const directedChange = increase ? balancedChange : 0-balancedChange;\n            const newWidth = Math.max(5, Math.round(startingWidth + directedChange));\n            const newHeight = Math.round(newWidth * startingRatio);\n\n            return {width: newWidth, height: newHeight};\n        };\n\n        return new MouseDragTracker(container, '.editor-node-resizer-handle', {\n            down(event: MouseEvent, handle: HTMLElement) {\n                _this.resizerDOM?.classList.add('active');\n                _this.context.editor.getEditorState().read(() => {\n                    const domRect = nodeDOM.getBoundingClientRect();\n                    startingWidth = node.getWidth() || domRect.width;\n                    startingHeight = node.getHeight() || domRect.height;\n                    if (node.getHeight()) {\n                        hasHeight = true;\n                    }\n                    startingRatio = startingHeight / startingWidth;\n                });\n\n                flipXChange = handle.classList.contains('nw') || handle.classList.contains('sw');\n                flipYChange = handle.classList.contains('nw') || handle.classList.contains('ne');\n            },\n            move(event: MouseEvent, handle: HTMLElement, distance: MouseDragTrackerDistance) {\n                const size = calculateSize(distance);\n                _this.updateDOMSize(size.width, size.height);\n            },\n            up(event: MouseEvent, handle: HTMLElement, distance: MouseDragTrackerDistance) {\n                const size = calculateSize(distance);\n                _this.context.editor.update(() => {\n                    node.setWidth(size.width);\n                    node.setHeight(hasHeight ? size.height : 0);\n                }, {\n                    onUpdate: () => {\n                        requestAnimationFrame(() => {\n                            _this.context.manager.triggerLayoutUpdate();\n                            _this.updateResizerPosition();\n                        });\n                    }\n                });\n                _this.resizerDOM?.classList.remove('active');\n            }\n        });\n    }\n}\n\n\nexport function registerNodeResizer(context: EditorUiContext): (() => void) {\n    const resizer = new NodeResizer(context);\n\n    return () => {\n        resizer.teardown();\n    };\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/helpers/table-resizer.ts",
    "content": "import {$getNearestNodeFromDOMNode, LexicalEditor} from \"lexical\";\nimport {MouseDragTracker, MouseDragTrackerDistance} from \"./mouse-drag-tracker\";\nimport {TableNode, TableRowNode} from \"@lexical/table\";\nimport {el} from \"../../../utils/dom\";\nimport {$getTableColumnWidth, $setTableColumnWidth} from \"../../../utils/tables\";\n\ntype MarkerDomRecord = {x: HTMLElement, y: HTMLElement};\n\nclass TableResizer {\n    protected editor: LexicalEditor;\n    protected editScrollContainer: HTMLElement;\n    protected markerDom: MarkerDomRecord|null = null;\n    protected mouseTracker: MouseDragTracker|null = null;\n    protected dragging: boolean = false;\n    protected targetCell: HTMLElement|null = null;\n    protected xMarkerAtStart : boolean = false;\n    protected yMarkerAtStart : boolean = false;\n    protected activeInTable: boolean = false;\n\n    constructor(editor: LexicalEditor, editScrollContainer: HTMLElement) {\n        this.editor = editor;\n        this.editScrollContainer = editScrollContainer;\n\n        this.setupListeners();\n    }\n\n    teardown() {\n        this.editScrollContainer.removeEventListener('mousemove', this.onCellMouseMove);\n        window.removeEventListener('scroll', this.onScrollOrResize, {capture: true});\n        window.removeEventListener('resize', this.onScrollOrResize);\n        if (this.mouseTracker) {\n            this.mouseTracker.teardown();\n        }\n    }\n\n    protected setupListeners() {\n        this.onTableMouseOver = this.onTableMouseOver.bind(this);\n        this.onCellMouseMove = this.onCellMouseMove.bind(this);\n        this.onScrollOrResize = this.onScrollOrResize.bind(this);\n        this.editScrollContainer.addEventListener('mouseover', this.onTableMouseOver, { passive: true });\n        window.addEventListener('scroll', this.onScrollOrResize, {capture: true, passive: true});\n        window.addEventListener('resize', this.onScrollOrResize, {passive: true});\n    }\n\n    protected onScrollOrResize(): void {\n        this.updateCurrentMarkerTargetPosition();\n    }\n\n    protected onTableMouseOver(event: MouseEvent): void {\n        if (this.dragging) {\n            return;\n        }\n\n        const table = (event.target as HTMLElement).closest('table') as HTMLElement|null;\n\n        if (table && !this.activeInTable) {\n            this.editScrollContainer.addEventListener('mousemove', this.onCellMouseMove, { passive: true });\n            this.onCellMouseMove(event);\n            this.activeInTable = true;\n        } else if (!table && this.activeInTable) {\n            this.editScrollContainer.removeEventListener('mousemove', this.onCellMouseMove);\n            this.hideMarkers();\n            this.activeInTable = false;\n        }\n    }\n\n    protected onCellMouseMove(event: MouseEvent) {\n        const cell = (event.target as HTMLElement).closest('td,th') as HTMLElement|null;\n        if (!cell || this.dragging) {\n            return;\n        }\n\n        const rect = cell.getBoundingClientRect();\n        const midX = rect.left + (rect.width / 2);\n        const midY = rect.top + (rect.height / 2);\n\n        this.targetCell = cell;\n        this.xMarkerAtStart = event.clientX <= midX;\n        this.yMarkerAtStart = event.clientY <= midY;\n\n        const xMarkerPos = this.xMarkerAtStart ? rect.left : rect.right;\n        const yMarkerPos = this.yMarkerAtStart ? rect.top : rect.bottom;\n        this.updateMarkersTo(cell, xMarkerPos, yMarkerPos);\n    }\n\n    protected updateMarkersTo(cell: HTMLElement, xPos: number, yPos: number) {\n        const markers: MarkerDomRecord = this.getMarkers();\n        const table = cell.closest('table') as HTMLElement;\n        const caption: HTMLTableCaptionElement|null = table.querySelector('caption');\n        const tableRect = table.getBoundingClientRect();\n        const editBounds = this.editScrollContainer.getBoundingClientRect();\n\n        let tableTop = tableRect.top;\n        if (caption) {\n            tableTop = caption.getBoundingClientRect().bottom;\n        }\n\n        const maxTop = Math.max(tableTop, editBounds.top);\n        const maxBottom = Math.min(tableRect.bottom, editBounds.bottom);\n        const maxHeight = maxBottom - maxTop;\n        markers.x.style.left = xPos + 'px';\n        markers.x.style.top = maxTop + 'px';\n        markers.x.style.height = maxHeight + 'px';\n\n        markers.y.style.top = yPos + 'px';\n        markers.y.style.left = tableRect.left + 'px';\n        markers.y.style.width = tableRect.width + 'px';\n\n        // Hide markers when out of bounds\n        markers.y.hidden = yPos < editBounds.top || yPos > editBounds.bottom;\n        markers.x.hidden = tableRect.top > editBounds.bottom || tableRect.bottom < editBounds.top;\n    }\n\n    protected hideMarkers(): void {\n        if (this.markerDom) {\n            this.markerDom.x.hidden = true;\n            this.markerDom.y.hidden = true;\n        }\n    }\n\n    protected updateCurrentMarkerTargetPosition(): void {\n        if (!this.targetCell) {\n            return;\n        }\n\n        const rect = this.targetCell.getBoundingClientRect();\n        const xMarkerPos = this.xMarkerAtStart ? rect.left : rect.right;\n        const yMarkerPos = this.yMarkerAtStart ? rect.top : rect.bottom;\n        this.updateMarkersTo(this.targetCell, xMarkerPos, yMarkerPos);\n    }\n\n    protected getMarkers(): MarkerDomRecord {\n        if (!this.markerDom) {\n            this.markerDom = {\n                x: el('div', {class: 'editor-table-marker editor-table-marker-column'}),\n                y: el('div', {class: 'editor-table-marker editor-table-marker-row'}),\n            }\n            const wrapper = el('div', {\n                class: 'editor-table-marker-wrap',\n            }, [this.markerDom.x, this.markerDom.y]);\n            this.editScrollContainer.after(wrapper);\n            this.watchMarkerMouseDrags(wrapper);\n        }\n\n        return this.markerDom;\n    }\n\n    protected watchMarkerMouseDrags(wrapper: HTMLElement) {\n        const _this = this;\n        let markerStart: number = 0;\n        let markerProp: 'left' | 'top' = 'left';\n\n        this.mouseTracker = new MouseDragTracker(wrapper, '.editor-table-marker', {\n            down(event: MouseEvent, marker: HTMLElement) {\n                marker.classList.add('active');\n                _this.dragging = true;\n\n                markerProp = marker.classList.contains('editor-table-marker-column') ? 'left' : 'top';\n                markerStart = Number(marker.style[markerProp].replace('px', ''));\n            },\n            move(event: MouseEvent, marker: HTMLElement, distance: MouseDragTrackerDistance) {\n                  marker.style[markerProp] = (markerStart + distance[markerProp === 'left' ? 'x' : 'y']) + 'px';\n            },\n            up(event: MouseEvent, marker: HTMLElement, distance: MouseDragTrackerDistance) {\n                marker.classList.remove('active');\n                marker.style.left = '0';\n                marker.style.top = '0';\n\n                _this.dragging = false;\n                const parentTable = _this.targetCell?.closest('table');\n\n                if (markerProp === 'left' && _this.targetCell && parentTable) {\n                    let cellIndex = _this.getTargetCellColumnIndex();\n                    let change = distance.x;\n                    if (_this.xMarkerAtStart && cellIndex > 0) {\n                        cellIndex -= 1;\n                    } else if  (_this.xMarkerAtStart && cellIndex === 0) {\n                        change = -change;\n                    }\n\n                    _this.editor.update(() => {\n                        const table = $getNearestNodeFromDOMNode(parentTable);\n                        if (table instanceof TableNode) {\n                            const originalWidth = $getTableColumnWidth(_this.editor, table, cellIndex);\n                            const newWidth = Math.max(originalWidth + change, 10);\n                            $setTableColumnWidth(table, cellIndex, newWidth);\n                        }\n                    });\n                }\n\n                if (markerProp === 'top' && _this.targetCell) {\n                    const cellElement = _this.targetCell;\n\n                    _this.editor.update(() => {\n                        const cellNode = $getNearestNodeFromDOMNode(cellElement);\n                        const rowNode = cellNode?.getParent();\n                        let rowIndex = rowNode?.getIndexWithinParent() || 0;\n\n                        let change = distance.y;\n                        if (_this.yMarkerAtStart && rowIndex > 0) {\n                            rowIndex -= 1;\n                        } else if  (_this.yMarkerAtStart && rowIndex === 0) {\n                            change = -change;\n                        }\n\n                        const targetRow = rowNode?.getParent()?.getChildren()[rowIndex];\n                        if (targetRow instanceof TableRowNode) {\n                            const height  = targetRow.getHeight() || 0;\n                            const newHeight = Math.max(height + change, 10);\n                            targetRow.setHeight(newHeight);\n                        }\n                    });\n                }\n            }\n        });\n    }\n\n    protected getTargetCellColumnIndex(): number {\n        const cell = this.targetCell;\n        if (cell === null) {\n            return -1;\n        }\n\n        let index = 0;\n        const row = cell.parentElement;\n        for (const rowCell of row?.children || []) {\n            let size = Number(rowCell.getAttribute('colspan'));\n            if (Number.isNaN(size) || size < 1) {\n                size = 1;\n            }\n\n            index += size;\n\n            if (rowCell === cell) {\n                return index - 1;\n            }\n        }\n\n        return -1;\n    }\n}\n\n\nexport function registerTableResizer(editor: LexicalEditor, editScrollContainer: HTMLElement): (() => void) {\n    const resizer = new TableResizer(editor, editScrollContainer);\n\n    return () => {\n        resizer.teardown();\n    };\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/helpers/table-selection-handler.ts",
    "content": "import {$getNodeByKey, LexicalEditor} from \"lexical\";\nimport {NodeKey} from \"lexical/LexicalNode\";\nimport {\n    $isTableNode,\n    applyTableHandlers,\n    HTMLTableElementWithWithTableSelectionState,\n    TableNode,\n    TableObserver\n} from \"@lexical/table\";\n\n// File adapted from logic in:\n// https://github.com/facebook/lexical/blob/f373759a7849f473d34960a6bf4e34b2a011e762/packages/lexical-react/src/LexicalTablePlugin.ts#L49\n// Copyright (c) Meta Platforms, Inc. and affiliates.\n// License: MIT\n\nclass TableSelectionHandler {\n\n    protected editor: LexicalEditor\n    protected tableSelections = new Map<NodeKey, TableObserver>();\n    protected unregisterMutationListener = () => {};\n\n    constructor(editor: LexicalEditor) {\n        this.editor = editor;\n        this.init();\n    }\n\n    protected init() {\n        this.unregisterMutationListener = this.editor.registerMutationListener(TableNode, (mutations) => {\n            for (const [nodeKey, mutation] of mutations) {\n                if (mutation === 'created') {\n                    this.editor.getEditorState().read(() => {\n                        const tableNode = $getNodeByKey<TableNode>(nodeKey);\n                        if ($isTableNode(tableNode)) {\n                            this.initializeTableNode(tableNode);\n                        }\n                    });\n                } else if (mutation === 'destroyed') {\n                    const tableSelection = this.tableSelections.get(nodeKey);\n\n                    if (tableSelection !== undefined) {\n                        tableSelection.removeListeners();\n                        this.tableSelections.delete(nodeKey);\n                    }\n                }\n            }\n        });\n    }\n\n    protected initializeTableNode(tableNode: TableNode) {\n        const nodeKey = tableNode.getKey();\n        const tableElement = this.editor.getElementByKey(\n            nodeKey,\n        ) as HTMLTableElementWithWithTableSelectionState;\n        if (tableElement && !this.tableSelections.has(nodeKey)) {\n            const tableSelection = applyTableHandlers(\n                tableNode,\n                tableElement,\n                this.editor,\n                true,\n            );\n            this.tableSelections.set(nodeKey, tableSelection);\n        }\n    };\n\n    teardown() {\n        this.unregisterMutationListener();\n        for (const [, tableSelection] of this.tableSelections) {\n            tableSelection.removeListeners();\n        }\n    }\n}\n\nexport function registerTableSelectionHandler(editor: LexicalEditor): (() => void) {\n    const resizer = new TableSelectionHandler(editor);\n\n    return () => {\n        resizer.teardown();\n    };\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/helpers/task-list-handler.ts",
    "content": "import {$getNearestNodeFromDOMNode, LexicalEditor} from \"lexical\";\nimport {$isListItemNode} from \"@lexical/list\";\n\nclass TaskListHandler {\n    protected editorContainer: HTMLElement;\n    protected editor: LexicalEditor;\n\n    constructor(editor: LexicalEditor, editorContainer: HTMLElement) {\n        this.editor = editor;\n        this.editorContainer = editorContainer;\n        this.setupListeners();\n    }\n\n    protected setupListeners() {\n        this.handleClick = this.handleClick.bind(this);\n        this.editorContainer.addEventListener('click', this.handleClick);\n    }\n\n    handleClick(event: MouseEvent) {\n        const target = event.target;\n        if (target instanceof HTMLElement && target.nodeName === 'LI' && target.classList.contains('task-list-item')) {\n            this.handleTaskListItemClick(target, event);\n            event.preventDefault();\n        }\n    }\n\n    handleTaskListItemClick(listItem: HTMLElement, event: MouseEvent) {\n        const bounds = listItem.getBoundingClientRect();\n        const withinBounds = event.clientX <= bounds.right\n            && event.clientX >= bounds.left\n            && event.clientY >= bounds.top\n            && event.clientY <= bounds.bottom;\n\n        // Outside task list item bounds means we're probably clicking the pseudo-element\n        if (withinBounds) {\n            return;\n        }\n\n        this.editor.update(() => {\n            const node = $getNearestNodeFromDOMNode(listItem);\n            if ($isListItemNode(node)) {\n                node.setChecked(!node.getChecked());\n            }\n        });\n    }\n\n    teardown() {\n        this.editorContainer.removeEventListener('click', this.handleClick);\n    }\n}\n\n\nexport function registerTaskListHandler(editor: LexicalEditor, editorContainer: HTMLElement): (() => void) {\n    const handler = new TaskListHandler(editor, editorContainer);\n\n    return () => {\n        handler.teardown();\n    };\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/manager.ts",
    "content": "import {EditorFormModal, EditorFormModalDefinition} from \"./modals\";\nimport {EditorContainerUiElement, EditorUiContext, EditorUiElement, EditorUiStateUpdate} from \"./core\";\nimport {EditorDecorator, EditorDecoratorAdapter} from \"./decorator\";\nimport {$getSelection, BaseSelection, LexicalEditor} from \"lexical\";\nimport {DecoratorListener} from \"lexical/LexicalEditor\";\nimport type {NodeKey} from \"lexical/LexicalNode\";\nimport {EditorContextToolbar, EditorContextToolbarDefinition} from \"./toolbars\";\nimport {getLastSelection, setLastSelection} from \"../../utils/selection\";\nimport {DropDownManager} from \"./helpers/dropdowns\";\n\nexport type SelectionChangeHandler = (selection: BaseSelection|null) => void;\n\nexport class EditorUIManager {\n\n    public dropdowns: DropDownManager = new DropDownManager();\n\n    protected modalDefinitionsByKey: Record<string, EditorFormModalDefinition> = {};\n    protected activeModalsByKey: Record<string, EditorFormModal> = {};\n    protected decoratorConstructorsByType: Record<string, typeof EditorDecorator> = {};\n    protected decoratorInstancesByNodeKey: Record<string, EditorDecorator> = {};\n    protected context: EditorUiContext|null = null;\n    protected toolbar: EditorContainerUiElement|null = null;\n    protected contextToolbarDefinitionsByKey: Record<string, EditorContextToolbarDefinition> = {};\n    protected activeContextToolbars: EditorContextToolbar[] = [];\n    protected selectionChangeHandlers: Set<SelectionChangeHandler> = new Set();\n    protected domEventAbortController = new AbortController();\n    protected teardownCallbacks: (()=>void)[] = [];\n\n    setContext(context: EditorUiContext) {\n        this.context = context;\n        this.setupEventListeners();\n        this.setupEditor(context.editor);\n    }\n\n    getContext(): EditorUiContext {\n        if (this.context === null) {\n            throw new Error(`Context attempted to be used without being set`);\n        }\n\n        return this.context;\n    }\n\n    triggerStateUpdateForElement(element: EditorUiElement) {\n        element.updateState({\n            selection: null,\n            editor: this.getContext().editor\n        });\n    }\n\n    registerModal(key: string, modalDefinition: EditorFormModalDefinition) {\n        this.modalDefinitionsByKey[key] = modalDefinition;\n    }\n\n    createModal(key: string): EditorFormModal {\n        const modalDefinition = this.modalDefinitionsByKey[key];\n        if (!modalDefinition) {\n            throw new Error(`Attempted to show modal of key [${key}] but no modal registered for that key`);\n        }\n\n        const modal = new EditorFormModal(modalDefinition, key);\n        modal.setContext(this.getContext());\n\n        return modal;\n    }\n\n    setModalActive(key: string, modal: EditorFormModal): void {\n        this.activeModalsByKey[key] = modal;\n    }\n\n    setModalInactive(key: string): void {\n        delete this.activeModalsByKey[key];\n    }\n\n    getActiveModal(key: string): EditorFormModal|null {\n        return this.activeModalsByKey[key];\n    }\n\n    registerDecoratorType(type: string, decorator: typeof EditorDecorator) {\n        this.decoratorConstructorsByType[type] = decorator;\n    }\n\n    protected getDecorator(decoratorType: string, nodeKey: string): EditorDecorator {\n        if (this.decoratorInstancesByNodeKey[nodeKey]) {\n            return this.decoratorInstancesByNodeKey[nodeKey];\n        }\n\n        const decoratorClass = this.decoratorConstructorsByType[decoratorType];\n        if (!decoratorClass) {\n            throw new Error(`Attempted to use decorator of type [${decoratorType}] but not decorator registered for that type`);\n        }\n\n        // @ts-ignore\n        const decorator = new decoratorClass(this.getContext());\n        this.decoratorInstancesByNodeKey[nodeKey] = decorator;\n        return decorator;\n    }\n\n    getDecoratorByNodeKey(nodeKey: string): EditorDecorator|null {\n        return this.decoratorInstancesByNodeKey[nodeKey] || null;\n    }\n\n    setToolbar(toolbar: EditorContainerUiElement) {\n        if (this.toolbar) {\n            this.toolbar.teardown();\n        }\n\n        this.toolbar = toolbar;\n        toolbar.setContext(this.getContext());\n        this.getContext().containerDOM.prepend(toolbar.getDOMElement());\n    }\n\n    getToolbar(): EditorContainerUiElement|null {\n        return this.toolbar;\n    }\n\n    registerContextToolbar(key: string, definition: EditorContextToolbarDefinition) {\n        this.contextToolbarDefinitionsByKey[key] = definition;\n    }\n\n    triggerStateUpdate(update: EditorUiStateUpdate): void {\n        setLastSelection(update.editor, update.selection);\n        this.toolbar?.updateState(update);\n        this.updateContextToolbars(update);\n        for (const toolbar of this.activeContextToolbars) {\n            toolbar.updateState(update);\n        }\n        this.triggerSelectionChange(update.selection);\n    }\n\n    triggerStateRefresh(): void {\n        const editor = this.getContext().editor;\n        const update = {\n            editor,\n            selection: getLastSelection(editor),\n        };\n\n        this.triggerStateUpdate(update);\n        this.updateContextToolbars(update);\n    }\n\n    triggerFutureStateRefresh(): void {\n        requestAnimationFrame(() => {\n            this.getContext().editor.getEditorState().read(() => {\n                this.triggerStateRefresh();\n            });\n        });\n    }\n\n    protected triggerSelectionChange(selection: BaseSelection|null): void {\n        if (!selection) {\n            return;\n        }\n\n        for (const handler of this.selectionChangeHandlers) {\n            handler(selection);\n        }\n    }\n\n    onSelectionChange(handler: SelectionChangeHandler): void {\n        this.selectionChangeHandlers.add(handler);\n    }\n\n    offSelectionChange(handler: SelectionChangeHandler): void {\n        this.selectionChangeHandlers.delete(handler);\n    }\n\n    triggerLayoutUpdate(): void {\n        window.requestAnimationFrame(() => {\n            for (const toolbar of this.activeContextToolbars) {\n                toolbar.updatePosition();\n            }\n        });\n    }\n\n    getDefaultDirection(): 'rtl' | 'ltr' {\n        return this.getContext().options.textDirection === 'rtl' ? 'rtl' : 'ltr';\n    }\n\n    onTeardown(callback: () => void): void {\n        this.teardownCallbacks.push(callback);\n    }\n\n    teardown(): void {\n        this.domEventAbortController.abort('teardown');\n\n        for (const [_, modal] of Object.entries(this.activeModalsByKey)) {\n            modal.teardown();\n        }\n\n        for (const [_, decorator] of Object.entries(this.decoratorInstancesByNodeKey)) {\n            decorator.teardown();\n        }\n\n        if (this.toolbar) {\n            this.toolbar.teardown();\n        }\n\n        for (const toolbar of this.activeContextToolbars) {\n            toolbar.teardown();\n        }\n\n        this.dropdowns.teardown();\n\n        for (const callback of this.teardownCallbacks) {\n            callback();\n        }\n    }\n\n    /**\n     * Set the UI focus to the editor.\n     */\n    focus(): void {\n        this.getContext().editorDOM.focus();\n        this.getContext().editor.focus();\n    }\n\n    protected updateContextToolbars(update: EditorUiStateUpdate): void {\n        for (let i = this.activeContextToolbars.length - 1; i >= 0; i--) {\n            const toolbar = this.activeContextToolbars[i];\n            toolbar.teardown();\n            this.activeContextToolbars.splice(i, 1);\n        }\n\n        const node = (update.selection?.getNodes() || [])[0] || null;\n        if (!node) {\n            return;\n        }\n\n        const element = update.editor.getElementByKey(node.getKey());\n        if (!element) {\n            return;\n        }\n\n        const toolbarKeys = Object.keys(this.contextToolbarDefinitionsByKey);\n        const contentByTarget = new Map<HTMLElement, EditorUiElement[]>();\n        for (const key of toolbarKeys) {\n            const definition = this.contextToolbarDefinitionsByKey[key];\n            const matchingElem = ((element.closest(definition.selector)) || (element.querySelector(definition.selector))) as HTMLElement|null;\n            if (matchingElem) {\n                const targetEl = definition.displayTargetLocator ? definition.displayTargetLocator(matchingElem) : matchingElem;\n                if (!contentByTarget.has(targetEl)) {\n                    contentByTarget.set(targetEl, [])\n                }\n                // @ts-ignore\n                contentByTarget.get(targetEl).push(...definition.content());\n            }\n        }\n\n        for (const [target, contents] of contentByTarget) {\n            const toolbar = new EditorContextToolbar(target, contents);\n            toolbar.setContext(this.getContext());\n            this.activeContextToolbars.push(toolbar);\n\n            this.getContext().containerDOM.append(toolbar.getDOMElement());\n            toolbar.updatePosition();\n        }\n    }\n\n    protected setupEditor(editor: LexicalEditor) {\n        // Register our DOM decorate listener with the editor\n        const domDecorateListener: DecoratorListener<EditorDecoratorAdapter> = (decorators: Record<NodeKey, EditorDecoratorAdapter>) => {\n            editor.getEditorState().read(() => {\n                const keys = Object.keys(decorators);\n                for (const key of keys) {\n                    const decoratedEl = editor.getElementByKey(key);\n                    if (!decoratedEl) {\n                        continue;\n                    }\n\n                    const adapter = decorators[key];\n                    const decorator = this.getDecorator(adapter.type, key);\n                    decorator.setNode(adapter.getNode());\n                    const decoratorEl = decorator.render(decoratedEl);\n                    if (decoratorEl) {\n                        decoratedEl.append(decoratorEl);\n                    }\n                }\n            });\n        }\n        editor.registerDecoratorListener(domDecorateListener);\n\n        // Watch for changes to update local state\n        editor.registerUpdateListener(({editorState, prevEditorState}) => {\n            // Watch for selection changes to update the UI on change\n            // Used to be done via SELECTION_CHANGE_COMMAND but this would not always emit\n            // for all selection changes, so this proved more reliable.\n            const selectionChange = !(prevEditorState._selection?.is(editorState._selection) || false);\n            if (selectionChange) {\n                editor.update(() => {\n                    const selection = $getSelection();\n                    // console.log('manager::selection', selection);\n                    this.triggerStateUpdate({\n                        editor, selection,\n                    });\n                });\n            }\n        });\n    }\n\n    protected setupEventListeners() {\n        const layoutUpdate = this.triggerLayoutUpdate.bind(this);\n        window.addEventListener('scroll', layoutUpdate, {capture: true, passive: true, signal: this.domEventAbortController.signal});\n        window.addEventListener('resize', layoutUpdate, {passive: true, signal: this.domEventAbortController.signal});\n    }\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/modals.ts",
    "content": "import {EditorForm, EditorFormDefinition} from \"./forms\";\nimport {EditorContainerUiElement} from \"./core\";\nimport closeIcon from \"@icons/close.svg\";\nimport {el} from \"../../utils/dom\";\n\nexport interface EditorModalDefinition {\n    title: string;\n}\n\nexport interface EditorFormModalDefinition extends EditorModalDefinition {\n    form: EditorFormDefinition;\n}\n\nexport class EditorFormModal extends EditorContainerUiElement {\n    protected definition: EditorFormModalDefinition;\n    protected key: string;\n    protected originalFocus: Element|null = null;\n\n    constructor(definition: EditorFormModalDefinition, key: string) {\n        super([new EditorForm(definition.form)]);\n        this.definition = definition;\n        this.key = key;\n    }\n\n    show(defaultValues: Record<string, string>) {\n        this.originalFocus = document.activeElement as Element;\n        const dom = this.getDOMElement();\n        document.body.append(dom);\n\n        const form = this.getForm();\n        form.setValues(defaultValues);\n        form.setOnCancel(this.hide.bind(this));\n        form.setOnSuccessfulSubmit(this.hide.bind(this));\n\n        this.getContext().manager.setModalActive(this.key, this);\n        form.focusOnFirst();\n    }\n\n    hide() {\n        this.getContext().manager.setModalInactive(this.key);\n        this.teardown();\n        if (this.originalFocus instanceof HTMLElement && this.originalFocus.isConnected) {\n            this.originalFocus.focus();\n        }\n    }\n\n    getForm(): EditorForm {\n        return this.children[0] as EditorForm;\n    }\n\n    protected buildDOM(): HTMLElement {\n        const closeButton = el('button', {\n            class: 'editor-modal-close',\n            type: 'button',\n            title: this.trans('Close'),\n        });\n        closeButton.innerHTML = closeIcon;\n        closeButton.addEventListener('click', this.hide.bind(this));\n\n        const modal = el('div', {class: 'editor-modal editor-form-modal'}, [\n            el('div', {class: 'editor-modal-header'}, [\n                el('div', {class: 'editor-modal-title'}, [this.trans(this.definition.title)]),\n                closeButton,\n            ]),\n            el('div', {class: 'editor-modal-body'}, [\n                this.getForm().getDOMElement(),\n            ]),\n        ]);\n\n        const wrapper = el('div', {class: 'editor-modal-wrapper'}, [modal]);\n\n        wrapper.addEventListener('click', event => {\n            if (event.target && !modal.contains(event.target as HTMLElement)) {\n                this.hide();\n            }\n        });\n\n        wrapper.addEventListener('keydown', event => {\n            if (event.key === 'Escape') {\n                this.hide();\n            }\n        });\n\n        return wrapper;\n    }\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/framework/toolbars.ts",
    "content": "import {EditorContainerUiElement, EditorUiElement} from \"./core\";\n\nimport {el} from \"../../utils/dom\";\n\nexport type EditorContextToolbarDefinition = {\n    selector: string;\n    content: () => EditorUiElement[],\n    displayTargetLocator?: (originalTarget: HTMLElement) => HTMLElement;\n};\n\nexport class EditorContextToolbar extends EditorContainerUiElement {\n\n    protected target: HTMLElement;\n\n    constructor(target: HTMLElement, children: EditorUiElement[]) {\n        super(children);\n        this.target = target;\n    }\n\n    protected buildDOM(): HTMLElement {\n        return el('div', {\n            class: 'editor-context-toolbar',\n        }, this.getChildren().map(child => child.getDOMElement()));\n    }\n\n    updatePosition() {\n        const editorBounds = this.getContext().scrollDOM.getBoundingClientRect();\n        const targetBounds = this.target.getBoundingClientRect();\n        const dom = this.getDOMElement();\n        const domBounds = dom.getBoundingClientRect();\n\n        const showing = targetBounds.bottom > editorBounds.top\n            && targetBounds.top < editorBounds.bottom;\n\n        dom.hidden = !showing;\n\n        if (!this.target.isConnected) {\n            // If our target is no longer in the DOM, tell the manager an update is needed.\n            this.getContext().manager.triggerFutureStateRefresh();\n            return;\n        } else if (!showing) {\n            return;\n        }\n\n        const showAbove: boolean = targetBounds.bottom + 6 + domBounds.height > editorBounds.bottom;\n        dom.classList.toggle('is-above', showAbove);\n\n        const targetMid = targetBounds.left + (targetBounds.width / 2);\n        const targetLeft = targetMid - (domBounds.width / 2);\n        if (showAbove) {\n            dom.style.top = (targetBounds.top - 6 - domBounds.height) + 'px';\n        } else {\n            dom.style.top = (targetBounds.bottom + 6) + 'px';\n        }\n        dom.style.left = targetLeft + 'px';\n    }\n\n    insert(children: EditorUiElement[]) {\n        this.addChildren(...children);\n        const dom = this.getDOMElement();\n        dom.append(...children.map(child => child.getDOMElement()));\n    }\n}"
  },
  {
    "path": "resources/js/wysiwyg/ui/index.ts",
    "content": "import {LexicalEditor} from \"lexical\";\nimport {EditorUIManager} from \"./framework/manager\";\nimport {EditorUiContext} from \"./framework/core\";\nimport {el} from \"../utils/dom\";\n\nexport function buildEditorUI(containerDOM: HTMLElement, editor: LexicalEditor, options: Record<string, any>): EditorUiContext {\n    const editorDOM = el('div', {\n        contenteditable: 'true',\n        class: `editor-content-area ${options.editorClass || ''}`,\n    });\n    const scrollDOM = el('div', {\n        class: 'editor-content-wrap',\n    }, [editorDOM]);\n\n    containerDOM.append(scrollDOM);\n    containerDOM.classList.add('editor-container');\n    containerDOM.setAttribute('dir', options.textDirection);\n    if (options.darkMode) {\n        containerDOM.classList.add('editor-dark');\n    }\n\n    const manager = new EditorUIManager();\n    const context: EditorUiContext = {\n        editor,\n        containerDOM: containerDOM,\n        editorDOM: editorDOM,\n        scrollDOM: scrollDOM,\n        manager,\n        translate(text: string): string {\n            const translations = options.translations;\n            return translations[text] || text;\n        },\n        error(error: string|Error): void {\n            const message = error instanceof Error ? error.message : error;\n            window.$events.error(message); // TODO - Translate\n        },\n        options,\n    };\n    manager.setContext(context);\n\n    return context;\n}"
  },
  {
    "path": "resources/js/wysiwyg/utils/__tests__/lists.test.ts",
    "content": "import {\n    createTestContext, destroyFromContext,\n    dispatchKeydownEventForNode, expectNodeShapeToMatch,\n} from \"lexical/__tests__/utils\";\nimport {\n    $createParagraphNode, $getRoot, LexicalEditor, LexicalNode,\n    ParagraphNode,\n} from \"lexical\";\nimport {$createDetailsNode, DetailsNode} from \"@lexical/rich-text/LexicalDetailsNode\";\nimport {EditorUiContext} from \"../../ui/framework/core\";\nimport {$htmlToBlockNodes} from \"../nodes\";\nimport {ListItemNode, ListNode} from \"@lexical/list\";\nimport {$nestListItem, $unnestListItem} from \"../lists\";\n\ndescribe('List Utils', () => {\n\n    let context!: EditorUiContext;\n    let editor!: LexicalEditor;\n\n    beforeEach(() => {\n        context = createTestContext();\n        editor = context.editor;\n    });\n\n    afterEach(() => {\n        destroyFromContext(context);\n    });\n\n    describe('$nestListItem', () => {\n        test('nesting handles child items to leave at the same level', () => {\n            const input = `<ul>\n    <li>Inner A</li>\n    <li>Inner B <ul>\n            <li>Inner C</li>\n    </ul></li>\n</ul>`;\n            let list!: ListNode;\n\n            editor.updateAndCommit(() => {\n                $getRoot().append(...$htmlToBlockNodes(editor, input));\n                list = $getRoot().getFirstChild() as ListNode;\n            });\n\n            editor.updateAndCommit(() => {\n                $nestListItem(list.getChildren()[1] as ListItemNode);\n            });\n\n            expectNodeShapeToMatch(editor, [\n                {\n                    type: 'list',\n                    children: [\n                        {\n                            type: 'listitem',\n                            children: [\n                                {text: 'Inner A'},\n                                {\n                                    type: 'list',\n                                    children: [\n                                        {type: 'listitem', children: [{text: 'Inner B'}]},\n                                        {type: 'listitem', children: [{text: 'Inner C'}]},\n                                    ]\n                                }\n                            ]\n                        },\n                    ]\n                }\n            ]);\n        });\n    });\n\n    describe('$unnestListItem', () => {\n        test('middle in nested list converts to new parent item at same place', () => {\n            const input = `<ul>\n<li>Nested list:<ul>\n    <li>Inner A</li>\n    <li>Inner B</li>\n    <li>Inner C</li>\n</ul></li>\n</ul>`;\n            let innerList!: ListNode;\n\n            editor.updateAndCommit(() => {\n                $getRoot().append(...$htmlToBlockNodes(editor, input));\n                innerList = (($getRoot().getFirstChild() as ListNode).getFirstChild() as ListItemNode).getLastChild() as ListNode;\n            });\n\n            editor.updateAndCommit(() => {\n                $unnestListItem(innerList.getChildren()[1] as ListItemNode);\n            });\n\n            expectNodeShapeToMatch(editor, [\n                {\n                    type: 'list',\n                    children: [\n                        {\n                            type: 'listitem',\n                            children: [\n                                {text: 'Nested list:'},\n                                {\n                                    type: 'list',\n                                    children: [\n                                        {type: 'listitem', children: [{text: 'Inner A'}]},\n                                    ],\n                                }\n                            ],\n                        },\n                        {\n                            type: 'listitem',\n                            children: [\n                                {text: 'Inner B'},\n                                {\n                                    type: 'list',\n                                    children: [\n                                        {type: 'listitem', children: [{text: 'Inner C'}]},\n                                    ],\n                                }\n                            ],\n                        }\n                    ]\n                }\n            ]);\n        });\n    });\n});"
  },
  {
    "path": "resources/js/wysiwyg/utils/actions.ts",
    "content": "import {$getRoot, $getSelection, $insertNodes, $isBlockElementNode, LexicalEditor} from \"lexical\";\nimport {$generateHtmlFromNodes} from \"@lexical/html\";\nimport {$getNearestNodeBlockParent, $htmlToBlockNodes, $htmlToNodes} from \"./nodes\";\n\nexport function setEditorContentFromHtml(editor: LexicalEditor, html: string) {\n    editor.update(() => {\n        // Empty existing\n        const root = $getRoot();\n        for (const child of root.getChildren()) {\n            child.remove(true);\n        }\n\n        const nodes = $htmlToBlockNodes(editor, html);\n        root.append(...nodes);\n    });\n}\n\nexport function appendHtmlToEditor(editor: LexicalEditor, html: string) {\n    editor.update(() => {\n        const root = $getRoot();\n        const nodes = $htmlToBlockNodes(editor, html);\n        root.append(...nodes);\n    });\n}\n\nexport function prependHtmlToEditor(editor: LexicalEditor, html: string) {\n    editor.update(() => {\n        const root = $getRoot();\n        const nodes = $htmlToBlockNodes(editor, html);\n        let reference = root.getChildren()[0];\n        for (let i = nodes.length - 1; i >= 0; i--) {\n            if (reference) {\n                reference.insertBefore(nodes[i]);\n            } else {\n                root.append(nodes[i])\n            }\n            reference = nodes[i];\n        }\n    });\n}\n\nexport function insertHtmlIntoEditor(editor: LexicalEditor, html: string) {\n    editor.update(() => {\n        const selection = $getSelection();\n        const nodes = $htmlToNodes(editor, html);\n\n        let reference = selection?.getNodes()[0];\n        let replacedReference = false;\n        let parentBlock = reference ? $getNearestNodeBlockParent(reference) : null;\n\n        for (let i = nodes.length - 1; i >= 0; i--) {\n            const toInsert = nodes[i];\n            if ($isBlockElementNode(toInsert) && parentBlock) {\n                // Insert at a block level, before or after the referenced block\n                // depending on if the reference has been replaced.\n                if (replacedReference) {\n                    parentBlock.insertBefore(toInsert);\n                } else {\n                    parentBlock.insertAfter(toInsert);\n                }\n            } else if ($isBlockElementNode(toInsert)) {\n                // Otherwise append blocks to the root\n                $getRoot().append(toInsert);\n            } else if (!replacedReference) {\n                // First inline node, replacing existing selection\n                $insertNodes([toInsert]);\n                reference = toInsert;\n                parentBlock = $getNearestNodeBlockParent(reference);\n                replacedReference = true;\n            } else {\n                // For other inline nodes, insert before the reference node\n                reference?.insertBefore(toInsert)\n            }\n        }\n    });\n}\n\nexport function getEditorContentAsHtml(editor: LexicalEditor): Promise<string> {\n    return new Promise((resolve, reject) => {\n        editor.getEditorState().read(() => {\n            const html = $generateHtmlFromNodes(editor);\n            resolve(html);\n        });\n    });\n}\n\nexport function focusEditor(editor: LexicalEditor): void {\n    editor.focus(() => {}, {defaultSelection: \"rootStart\"});\n}"
  },
  {
    "path": "resources/js/wysiwyg/utils/details.ts",
    "content": "import {DetailsNode} from \"@lexical/rich-text/LexicalDetailsNode\";\n\nexport function $unwrapDetailsNode(node: DetailsNode) {\n    const children = node.getChildren();\n    for (const child of children) {\n        node.insertBefore(child);\n    }\n    node.remove();\n}"
  },
  {
    "path": "resources/js/wysiwyg/utils/diagrams.ts",
    "content": "import {$insertNodes, LexicalEditor, LexicalNode} from \"lexical\";\nimport {HttpError} from \"../../services/http\";\nimport {EditorUiContext} from \"../ui/framework/core\";\nimport * as DrawIO from \"../../services/drawio\";\nimport {$createDiagramNode, DiagramNode} from \"@lexical/rich-text/LexicalDiagramNode\";\nimport {ImageManager} from \"../../components\";\nimport {EditorImageData} from \"./images\";\nimport {$getNodeFromSelection, getLastSelection} from \"./selection\";\n\nexport function $isDiagramNode(node: LexicalNode | null | undefined): node is DiagramNode {\n    return node instanceof DiagramNode;\n}\n\nfunction handleUploadError(error: HttpError, context: EditorUiContext): void {\n    if (error.status === 413) {\n        window.$events.emit('error', context.options.translations.serverUploadLimitText || '');\n    } else {\n        window.$events.emit('error', context.options.translations.imageUploadErrorText || '');\n    }\n    console.error(error);\n}\n\nasync function loadDiagramIdFromNode(editor: LexicalEditor, node: DiagramNode): Promise<string> {\n    const drawingId = await new Promise<string>((res, rej) => {\n        editor.getEditorState().read(() => {\n            const {id: drawingId} = node.getDrawingIdAndUrl();\n            res(drawingId);\n        });\n    });\n\n    return drawingId || '';\n}\n\nasync function updateDrawingNodeFromData(context: EditorUiContext, node: DiagramNode, pngData: string, isNew: boolean): Promise<void> {\n    DrawIO.close();\n\n    if (isNew) {\n        const loadingImage: string = window.baseUrl('/loading.gif');\n        context.editor.update(() => {\n            node.setDrawingIdAndUrl('', loadingImage);\n        });\n    }\n\n    try {\n        const img = await DrawIO.upload(pngData, context.options.pageId);\n        context.editor.update(() => {\n            node.setDrawingIdAndUrl(String(img.id), img.url);\n        });\n    } catch (err) {\n        if (err instanceof HttpError) {\n            handleUploadError(err, context);\n        }\n\n        if (isNew) {\n            context.editor.update(() => {\n                node.remove();\n            });\n        }\n\n        throw new Error(`Failed to save image with error: ${err}`);\n    }\n}\n\nexport function $openDrawingEditorForNode(context: EditorUiContext, node: DiagramNode): void {\n    let isNew = false;\n    DrawIO.show(context.options.drawioUrl, async () => {\n        const drawingId = await loadDiagramIdFromNode(context.editor, node);\n        isNew = !drawingId;\n        return isNew ? '' : DrawIO.load(drawingId);\n    }, async (pngData: string) => {\n        return updateDrawingNodeFromData(context, node, pngData, isNew);\n    });\n}\n\nexport function showDiagramManager(callback: (image: EditorImageData) => any) {\n    const imageManager: ImageManager = window.$components.first('image-manager') as ImageManager;\n    imageManager.show((image: EditorImageData) => {\n        callback(image);\n    }, 'drawio');\n}\n\nexport function showDiagramManagerForInsert(context: EditorUiContext) {\n    const selection = getLastSelection(context.editor);\n    showDiagramManager((image: EditorImageData) => {\n        context.editor.update(() => {\n            const diagramNode = $createDiagramNode(image.id, image.url);\n            const selectedDiagram = $getNodeFromSelection(selection, $isDiagramNode);\n            if ($isDiagramNode(selectedDiagram)) {\n                selectedDiagram.replace(diagramNode);\n            } else {\n                $insertNodes([diagramNode]);\n            }\n        });\n    });\n}"
  },
  {
    "path": "resources/js/wysiwyg/utils/dom.ts",
    "content": "export function el(tag: string, attrs: Record<string, string | null> = {}, children: (string | HTMLElement)[] = []): HTMLElement {\n    const el = document.createElement(tag);\n    const attrKeys = Object.keys(attrs);\n    for (const attr of attrKeys) {\n        if (attrs[attr] !== null) {\n            el.setAttribute(attr, attrs[attr] as string);\n        }\n    }\n\n    for (const child of children) {\n        if (typeof child === 'string') {\n            el.append(document.createTextNode(child));\n        } else {\n            el.append(child);\n        }\n    }\n\n    return el;\n}\n\nexport function htmlToDom(html: string): Document {\n    const parser = new DOMParser();\n    return parser.parseFromString(html, 'text/html');\n}\n\nexport function formatSizeValue(size: number | string, defaultSuffix: string = 'px'): string {\n    if (typeof size === 'number' || /^-?\\d+$/.test(size)) {\n        return `${size}${defaultSuffix}`;\n    }\n\n    return size;\n}\n\nexport function sizeToPixels(size: string): number {\n    if (/^-?\\d+$/.test(size)) {\n        return Number(size);\n    }\n\n    if (/^-?\\d+\\.\\d+$/.test(size)) {\n        return Math.round(Number(size));\n    }\n\n    if (/^-?\\d+px\\s*$/.test(size)) {\n        return Number(size.trim().replace('px', ''));\n    }\n\n    return 0;\n}\n\nexport type StyleMap = Map<string, string>;\n\n/**\n * Creates a map from an element's styles.\n * Uses direct attribute value string handling since attempting to iterate\n * over .style will expand out any shorthand properties (like 'padding')\n * rather than being representative of the actual properties set.\n */\nexport function extractStyleMapFromElement(element: HTMLElement): StyleMap {\n    const styleText= element.getAttribute('style') || '';\n    return styleStringToStyleMap(styleText);\n}\n\n/**\n * Convert string-formatted styles into a StyleMap.\n */\nexport function styleStringToStyleMap(styleText: string): StyleMap {\n    const map: StyleMap = new Map();\n\n    const rules = styleText.split(';');\n    for (const rule of rules) {\n        const [name, value] = rule.split(':');\n        if (!name || !value) {\n            continue;\n        }\n\n        map.set(name.trim().toLowerCase(), value.trim());\n    }\n\n    return map;\n}\n\n/**\n * Convert a StyleMap into inline string style text.\n */\nexport function styleMapToStyleString(map: StyleMap): string {\n    const parts = [];\n    for (const [style, value] of map.entries()) {\n        parts.push(`${style}:${value}`);\n    }\n    return parts.join(';');\n}\n\nexport function setOrRemoveAttribute(element: HTMLElement, name: string, value: string|null|undefined) {\n    if (value) {\n        element.setAttribute(name, value);\n    } else {\n        element.removeAttribute(name);\n    }\n}"
  },
  {
    "path": "resources/js/wysiwyg/utils/formats.ts",
    "content": "import {\n    $createParagraphNode,\n    $createTextNode,\n    $getSelection,\n    $insertNodes,\n    $isParagraphNode,\n    LexicalEditor,\n    LexicalNode\n} from \"lexical\";\nimport {\n    $getBlockElementNodesInSelection,\n    $getNodeFromSelection,\n    $insertNewBlockNodeAtSelection, $selectionContainsNodeType, $selectSingleNode,\n    $toggleSelectionBlockNodeType,\n    getLastSelection\n} from \"./selection\";\nimport {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from \"@lexical/rich-text/LexicalCodeBlockNode\";\nimport {$createCalloutNode, $isCalloutNode, CalloutCategory} from \"@lexical/rich-text/LexicalCalloutNode\";\nimport {$isListNode, insertList, ListNode, ListType, removeList} from \"@lexical/list\";\nimport {$createLinkNode, $isLinkNode} from \"@lexical/link\";\nimport {$createHeadingNode, $isHeadingNode, HeadingTagType} from \"@lexical/rich-text/LexicalHeadingNode\";\nimport {$createQuoteNode, $isQuoteNode} from \"@lexical/rich-text/LexicalQuoteNode\";\n\nconst $isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => {\n    return $isHeadingNode(node) && node.getTag() === tag;\n};\n\nexport function toggleSelectionAsHeading(editor: LexicalEditor, tag: HeadingTagType) {\n    editor.update(() => {\n        $toggleSelectionBlockNodeType(\n            (node) => $isHeaderNodeOfTag(node, tag),\n            () => $createHeadingNode(tag),\n        )\n    });\n}\n\nexport function toggleSelectionAsParagraph(editor: LexicalEditor) {\n    editor.update(() => {\n        $toggleSelectionBlockNodeType($isParagraphNode, $createParagraphNode);\n    });\n}\n\nexport function toggleSelectionAsBlockquote(editor: LexicalEditor) {\n    editor.update(() => {\n        $toggleSelectionBlockNodeType($isQuoteNode, $createQuoteNode);\n    });\n}\n\nexport function toggleSelectionAsList(editor: LexicalEditor, type: ListType) {\n    editor.getEditorState().read(() => {\n        const selection = $getSelection();\n        const listSelected = $selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => {\n            return $isListNode(node) && (node as ListNode).getListType() === type;\n        });\n\n        if (listSelected) {\n            removeList(editor);\n        } else {\n            insertList(editor, type);\n        }\n    });\n}\n\nexport function formatCodeBlock(editor: LexicalEditor) {\n    editor.getEditorState().read(() => {\n        const selection = $getSelection();\n        const lastSelection = getLastSelection(editor);\n        const codeBlock = $getNodeFromSelection(lastSelection, $isCodeBlockNode) as (CodeBlockNode | null);\n        if (codeBlock === null) {\n            editor.update(() => {\n                const codeBlock = $createCodeBlockNode();\n                codeBlock.setCode(selection?.getTextContent() || '');\n\n                const selectionNodes = $getBlockElementNodesInSelection(selection);\n                const firstSelectionNode = selectionNodes[0];\n                const extraNodes = selectionNodes.slice(1);\n                if (firstSelectionNode) {\n                    firstSelectionNode.replace(codeBlock);\n                    extraNodes.forEach(n => n.remove());\n                } else {\n                    $insertNewBlockNodeAtSelection(codeBlock, true);\n                }\n\n                $openCodeEditorForNode(editor, codeBlock);\n                $selectSingleNode(codeBlock);\n            });\n        } else {\n            $openCodeEditorForNode(editor, codeBlock);\n        }\n    });\n}\n\nexport function cycleSelectionCalloutFormats(editor: LexicalEditor) {\n    editor.update(() => {\n        const selection = $getSelection();\n        const blocks = $getBlockElementNodesInSelection(selection);\n\n        let created = false;\n        for (const block of blocks) {\n            if (!$isCalloutNode(block)) {\n                block.replace($createCalloutNode('info'), true);\n                created = true;\n            }\n        }\n\n        if (created) {\n            return;\n        }\n\n        const types: CalloutCategory[] = ['info', 'warning', 'danger', 'success'];\n        for (const block of blocks) {\n            if ($isCalloutNode(block)) {\n                const type = block.getCategory();\n                const typeIndex = types.indexOf(type);\n                const newIndex = (typeIndex + 1) % types.length;\n                const newType = types[newIndex];\n                block.setCategory(newType);\n            }\n        }\n    });\n}\n\nexport function insertOrUpdateLink(editor: LexicalEditor, linkDetails: {text: string, title: string, target: string, url: string}) {\n    editor.update(() => {\n        const selection = $getSelection();\n        let link = $getNodeFromSelection(selection, $isLinkNode);\n        if ($isLinkNode(link)) {\n            link.setURL(linkDetails.url);\n            link.setTarget(linkDetails.target);\n            link.setTitle(linkDetails.title);\n        } else {\n            link = $createLinkNode(linkDetails.url, {\n                title: linkDetails.title,\n                target: linkDetails.target,\n            });\n\n            $insertNodes([link]);\n        }\n\n        if ($isLinkNode(link)) {\n            for (const child of link.getChildren()) {\n                child.remove(true);\n            }\n            link.append($createTextNode(linkDetails.text));\n        }\n    });\n}"
  },
  {
    "path": "resources/js/wysiwyg/utils/images.ts",
    "content": "import {ImageManager} from \"../../components\";\nimport {$createImageNode} from \"@lexical/rich-text/LexicalImageNode\";\nimport {$createLinkNode, LinkNode} from \"@lexical/link\";\n\nexport type EditorImageData = {\n    id: string;\n    url: string;\n    thumbs?: {display: string};\n    name: string;\n};\n\nexport function showImageManager(callback: (image: EditorImageData) => any) {\n    const imageManager: ImageManager = window.$components.first('image-manager') as ImageManager;\n    imageManager.show((image: EditorImageData) => {\n        callback(image);\n    }, 'gallery');\n}\n\nexport function $createLinkedImageNodeFromImageData(image: EditorImageData): LinkNode {\n    const url = image.thumbs?.display || image.url;\n    const linkNode = $createLinkNode(url, {target: '_blank'});\n    const imageNode = $createImageNode(url, {\n        alt: image.name\n    });\n    linkNode.append(imageNode);\n    return linkNode;\n}\n\n/**\n * Upload an image file to the server\n */\nexport async function uploadImageFile(file: File, pageId: string): Promise<EditorImageData> {\n    if (file === null || file.type.indexOf('image') !== 0) {\n        throw new Error('Not an image file');\n    }\n\n    const remoteFilename = file.name || `image-${Date.now()}.png`;\n    const formData = new FormData();\n    formData.append('file', file, remoteFilename);\n    formData.append('uploaded_to', pageId);\n\n    const resp = await window.$http.post('/images/gallery', formData);\n    return resp.data as EditorImageData;\n}"
  },
  {
    "path": "resources/js/wysiwyg/utils/links.ts",
    "content": "import {EntitySelectorPopup} from \"../../components\";\n\ntype EditorEntityData = {\n    link: string;\n    name: string;\n};\n\nexport function showLinkSelector(callback: (entity: EditorEntityData) => any, selectionText?: string) {\n    const selector: EntitySelectorPopup = window.$components.first('entity-selector-popup') as EntitySelectorPopup;\n    selector.show((entity: EditorEntityData) => callback(entity), {\n        initialValue: selectionText || '',\n        searchEndpoint: '/search/entity-selector',\n        entityTypes: 'page,book,chapter,bookshelf',\n        entityPermission: 'view',\n    });\n}"
  },
  {
    "path": "resources/js/wysiwyg/utils/lists.ts",
    "content": "import {$createTextNode, $getSelection, BaseSelection, LexicalEditor, TextNode} from \"lexical\";\nimport {$getBlockElementNodesInSelection, $selectNodes, $toggleSelection} from \"./selection\";\nimport {$sortNodes, nodeHasInset} from \"./nodes\";\nimport {$createListItemNode, $createListNode, $isListItemNode, $isListNode, ListItemNode} from \"@lexical/list\";\n\n\nexport function $nestListItem(node: ListItemNode): ListItemNode {\n    const list = node.getParent();\n    if (!$isListNode(list)) {\n        return node;\n    }\n\n    const nodeChildList = node.getChildren().filter(n => $isListNode(n))[0] || null;\n    const nodeChildItems = nodeChildList?.getChildren() || [];\n\n    const listItems = list.getChildren() as ListItemNode[];\n    const nodeIndex = listItems.findIndex((n) => n.getKey() === node.getKey());\n    const isFirst = nodeIndex === 0;\n\n    const newListItem = $createListItemNode();\n    const newList = $createListNode(list.getListType());\n    newList.append(newListItem);\n    newListItem.append(...node.getChildren());\n\n    if (isFirst) {\n        node.append(newList);\n    } else  {\n        const prevListItem = listItems[nodeIndex - 1];\n        prevListItem.append(newList);\n        node.remove();\n    }\n\n    if (nodeChildList) {\n        for (const child of nodeChildItems) {\n            newListItem.insertAfter(child);\n        }\n        nodeChildList.remove();\n    }\n\n    return newListItem;\n}\n\nexport function $unnestListItem(node: ListItemNode): ListItemNode {\n    const list = node.getParent();\n    const parentListItem = list?.getParent();\n    const outerList = parentListItem?.getParent();\n    if (!$isListNode(list) || !$isListNode(outerList) || !$isListItemNode(parentListItem)) {\n        return node;\n    }\n\n    const laterSiblings = node.getNextSiblings();\n    parentListItem.insertAfter(node);\n    if (list.getChildren().length === 0) {\n        list.remove();\n    }\n\n    if (laterSiblings.length > 0) {\n        const childList = $createListNode(list.getListType());\n        childList.append(...laterSiblings);\n        node.append(childList);\n    }\n\n    if (list.getChildrenSize() === 0) {\n        list.remove();\n    }\n\n    if (parentListItem.getChildren().length === 0) {\n        parentListItem.remove();\n    }\n\n    return node;\n}\n\nfunction getListItemsForSelection(selection: BaseSelection|null): (ListItemNode|null)[] {\n    const nodes = selection?.getNodes() || [];\n    let [start, end] = selection?.getStartEndPoints() || [null, null];\n\n    // Ensure we ignore parent list items of the top-most list item since,\n    // although technically part of the selection, from a user point of\n    // view the selection does not spread to encompass this outer element.\n    const itemsToIgnore: Set<string> = new Set();\n    if (selection && start) {\n        if (selection.isBackward() && end) {\n            [end, start] = [start, end];\n        }\n\n        const startParents = start.getNode().getParents();\n        let foundList = false;\n        for (const parent of startParents) {\n            if ($isListItemNode(parent)) {\n                if (foundList) {\n                    itemsToIgnore.add(parent.getKey());\n                } else {\n                    foundList = true;\n                }\n            }\n        }\n    }\n\n    const listItemNodes = [];\n    outer: for (const node of nodes) {\n        if ($isListItemNode(node)) {\n            if (!itemsToIgnore.has(node.getKey())) {\n                listItemNodes.push(node);\n            }\n            continue;\n        }\n\n        const parents = node.getParents();\n        for (const parent of parents) {\n            if ($isListItemNode(parent)) {\n                if (!itemsToIgnore.has(parent.getKey())) {\n                    listItemNodes.push(parent);\n                }\n                continue outer;\n            }\n        }\n\n        listItemNodes.push(null);\n    }\n\n    return listItemNodes;\n}\n\nfunction $reduceDedupeListItems(listItems: (ListItemNode|null)[]): ListItemNode[] {\n    const listItemMap: Record<string, ListItemNode> = {};\n\n    for (const item of listItems) {\n        if (item === null) {\n            continue;\n        }\n\n        const key = item.getKey();\n        if (typeof listItemMap[key] === 'undefined') {\n            listItemMap[key] = item;\n        }\n    }\n\n    const items = Object.values(listItemMap);\n    return $sortNodes(items) as ListItemNode[];\n}\n\nexport function $setInsetForSelection(editor: LexicalEditor, change: number): void {\n    const selection = $getSelection();\n    const selectionBounds = selection?.getStartEndPoints();\n    const listItemsInSelection = getListItemsForSelection(selection);\n    const isListSelection = listItemsInSelection.length > 0 && !listItemsInSelection.includes(null);\n\n    if (isListSelection) {\n        const alteredListItems = [];\n        const listItems = $reduceDedupeListItems(listItemsInSelection);\n        if (change > 0) {\n            for (const listItem of listItems) {\n                alteredListItems.push($nestListItem(listItem));\n            }\n        } else if (change < 0) {\n            for (const listItem of [...listItems].reverse()) {\n                alteredListItems.push($unnestListItem(listItem));\n            }\n            alteredListItems.reverse();\n        }\n\n        if (alteredListItems.length === 1 && selectionBounds) {\n            // Retain selection range if moving just one item\n            const listItem = alteredListItems[0] as ListItemNode;\n            let child = listItem.getChildren()[0] as TextNode;\n            if (!child) {\n                child = $createTextNode('');\n                listItem.append(child);\n            }\n            child.select(selectionBounds[0].offset, selectionBounds[1].offset);\n        } else {\n            $selectNodes(alteredListItems);\n        }\n\n        return;\n    }\n\n    const elements = $getBlockElementNodesInSelection(selection);\n    for (const node of elements) {\n        if (nodeHasInset(node)) {\n            const currentInset = node.getInset();\n            const newInset = Math.min(Math.max(currentInset + change, 0), 500);\n            node.setInset(newInset)\n        }\n    }\n\n    $toggleSelection(editor);\n}"
  },
  {
    "path": "resources/js/wysiwyg/utils/node-clipboard.ts",
    "content": "import {$isElementNode, LexicalEditor, LexicalNode, SerializedLexicalNode} from \"lexical\";\n\ntype SerializedLexicalNodeWithChildren = {\n    node: SerializedLexicalNode,\n    children: SerializedLexicalNodeWithChildren[],\n};\n\nfunction serializeNodeRecursive(node: LexicalNode): SerializedLexicalNodeWithChildren {\n    const childNodes = $isElementNode(node) ? node.getChildren() : [];\n    return {\n        node: node.exportJSON(),\n        children: childNodes.map(n => serializeNodeRecursive(n)),\n    };\n}\n\nfunction unserializeNodeRecursive(editor: LexicalEditor, {node, children}: SerializedLexicalNodeWithChildren): LexicalNode|null {\n    const instance = editor._nodes.get(node.type)?.klass.importJSON(node);\n    if (!instance) {\n        return null;\n    }\n\n    const childNodes = children.map(child => unserializeNodeRecursive(editor, child));\n    for (const child of childNodes) {\n        if (child && $isElementNode(instance)) {\n            instance.append(child);\n        }\n    }\n\n    return instance;\n}\n\nexport class NodeClipboard<T extends LexicalNode> {\n    protected store: SerializedLexicalNodeWithChildren[] = [];\n\n    set(...nodes: LexicalNode[]): void {\n        this.store.splice(0, this.store.length);\n        for (const node of nodes) {\n            this.store.push(serializeNodeRecursive(node));\n        }\n    }\n\n    get(editor: LexicalEditor): T[] {\n        return this.store.map(json => unserializeNodeRecursive(editor, json)).filter((node) => {\n            return node !== null;\n        }) as T[];\n    }\n\n    size(): number {\n        return this.store.length;\n    }\n}"
  },
  {
    "path": "resources/js/wysiwyg/utils/nodes.ts",
    "content": "import {\n    $createParagraphNode,\n    $getRoot,\n    $isDecoratorNode,\n    $isElementNode, $isRootNode,\n    $isTextNode,\n    ElementNode,\n    LexicalEditor,\n    LexicalNode, RangeSelection\n} from \"lexical\";\nimport {LexicalNodeMatcher} from \"../nodes\";\nimport {$generateNodesFromDOM} from \"@lexical/html\";\nimport {htmlToDom} from \"./dom\";\nimport {NodeHasAlignment, NodeHasInset} from \"lexical/nodes/common\";\nimport {$findMatchingParent} from \"@lexical/utils\";\n\nfunction wrapTextNodes(nodes: LexicalNode[]): LexicalNode[] {\n    return nodes.map(node => {\n        if ($isTextNode(node)) {\n            const paragraph = $createParagraphNode();\n            paragraph.append(node);\n            return paragraph;\n        }\n        return node;\n    });\n}\n\nexport function $htmlToNodes(editor: LexicalEditor, html: string): LexicalNode[] {\n    const dom = htmlToDom(html);\n    return $generateNodesFromDOM(editor, dom);\n}\n\nexport function $htmlToBlockNodes(editor: LexicalEditor, html: string): LexicalNode[] {\n    return wrapTextNodes($htmlToNodes(editor, html));\n}\n\nexport function $getParentOfType(node: LexicalNode, matcher: LexicalNodeMatcher): LexicalNode | null {\n    for (const parent of node.getParents()) {\n        if (matcher(parent)) {\n            return parent;\n        }\n    }\n\n    return null;\n}\n\nexport function $getAllNodesOfType(matcher: LexicalNodeMatcher, root?: ElementNode): LexicalNode[] {\n    if (!root) {\n        root = $getRoot();\n    }\n\n    const matches = [];\n\n    for (const child of root.getChildren()) {\n        if (matcher(child)) {\n            matches.push(child);\n        }\n\n        if ($isElementNode(child)) {\n            matches.push(...$getAllNodesOfType(matcher, child));\n        }\n    }\n\n    return matches;\n}\n\n/**\n * Get the nearest root/block level node for the given position.\n */\nexport function $getNearestBlockNodeForCoords(editor: LexicalEditor, x: number, y: number): LexicalNode | null {\n    // TODO - Take into account x for floated blocks?\n    const rootNodes = $getRoot().getChildren();\n    for (const node of rootNodes) {\n        const nodeDom = editor.getElementByKey(node.__key);\n        if (!nodeDom) {\n            continue;\n        }\n\n        const bounds = nodeDom.getBoundingClientRect();\n        if (y <= bounds.bottom) {\n            return node;\n        }\n    }\n\n    return null;\n}\n\nexport function $getNearestNodeBlockParent(node: LexicalNode): LexicalNode|null {\n    const isBlockNode = (node: LexicalNode): boolean => {\n        return ($isElementNode(node) || $isDecoratorNode(node)) && !node.isInline() && !$isRootNode(node);\n    };\n\n    if (isBlockNode(node)) {\n        return node;\n    }\n\n    return $findMatchingParent(node, isBlockNode);\n}\n\nexport function $sortNodes(nodes: LexicalNode[]): LexicalNode[] {\n    const idChain: string[] = [];\n    const addIds = (n: ElementNode) => {\n        for (const child of n.getChildren()) {\n            idChain.push(child.getKey())\n            if ($isElementNode(child)) {\n                addIds(child)\n            }\n        }\n    };\n\n    const root = $getRoot();\n    addIds(root);\n\n    const sorted = Array.from(nodes);\n    sorted.sort((a, b) => {\n        const aIndex = idChain.indexOf(a.getKey());\n        const bIndex = idChain.indexOf(b.getKey());\n        return aIndex - bIndex;\n    });\n\n    return sorted;\n}\n\nexport function $selectOrCreateAdjacent(node: LexicalNode, after: boolean): RangeSelection {\n    const nearestBlock = $getNearestNodeBlockParent(node) || node;\n    let target = after ? nearestBlock.getNextSibling() : nearestBlock.getPreviousSibling()\n\n    if (!target) {\n        target = $createParagraphNode();\n        if (after) {\n            nearestBlock.insertAfter(target)\n        } else {\n            nearestBlock.insertBefore(target);\n        }\n    }\n\n    return after ? target.selectStart() : target.selectEnd();\n}\n\nexport function nodeHasAlignment(node: object): node is NodeHasAlignment {\n    return '__alignment' in node;\n}\n\nexport function nodeHasInset(node: object): node is NodeHasInset {\n    return '__inset' in node;\n}"
  },
  {
    "path": "resources/js/wysiwyg/utils/selection.ts",
    "content": "import {\n    $createNodeSelection,\n    $createParagraphNode, $createRangeSelection,\n    $getRoot,\n    $getSelection, $isBlockElementNode, $isDecoratorNode,\n    $isElementNode, $isParagraphNode,\n    $isTextNode,\n    $setSelection,\n    BaseSelection, DecoratorNode,\n    ElementNode, LexicalEditor,\n    LexicalNode,\n    TextFormatType, TextNode\n} from \"lexical\";\nimport {$getNearestBlockElementAncestorOrThrow} from \"@lexical/utils\";\nimport {LexicalElementNodeCreator, LexicalNodeMatcher} from \"../nodes\";\nimport {$setBlocksType} from \"@lexical/selection\";\n\nimport {$getNearestNodeBlockParent, $getParentOfType, nodeHasAlignment} from \"./nodes\";\nimport {CommonBlockAlignment} from \"lexical/nodes/common\";\n\nconst lastSelectionByEditor = new WeakMap<LexicalEditor, BaseSelection|null>;\n\nexport function getLastSelection(editor: LexicalEditor): BaseSelection|null {\n    return lastSelectionByEditor.get(editor) || null;\n}\n\nexport function setLastSelection(editor: LexicalEditor, selection: BaseSelection|null): void {\n    lastSelectionByEditor.set(editor, selection);\n}\n\nexport function $selectionContainsNodeType(selection: BaseSelection | null, matcher: LexicalNodeMatcher): boolean {\n    return $getNodeFromSelection(selection, matcher) !== null;\n}\n\nexport function $getNodeFromSelection(selection: BaseSelection | null, matcher: LexicalNodeMatcher): LexicalNode | null {\n    if (!selection) {\n        return null;\n    }\n\n    for (const node of selection.getNodes()) {\n        if (matcher(node)) {\n            return node;\n        }\n\n        const matchedParent = $getParentOfType(node, matcher);\n        if (matchedParent) {\n            return matchedParent;\n        }\n    }\n\n    return null;\n}\n\nexport function $getTextNodeFromSelection(selection: BaseSelection | null): TextNode|null {\n    return $getNodeFromSelection(selection, $isTextNode) as TextNode|null;\n}\n\nexport function $selectionContainsTextFormat(selection: BaseSelection | null, format: TextFormatType): boolean {\n    if (!selection) {\n        return false;\n    }\n\n    // Check text nodes\n    const nodes = selection.getNodes();\n    for (const node of nodes) {\n        if ($isTextNode(node) && node.hasFormat(format)) {\n            return true;\n        }\n    }\n\n    // If we're in an empty paragraph, check the paragraph format\n    if (nodes.length === 1 && $isParagraphNode(nodes[0]) && nodes[0].hasTextFormat(format)) {\n        return true;\n    }\n\n    return false;\n}\n\nexport function $toggleSelectionBlockNodeType(matcher: LexicalNodeMatcher, creator: LexicalElementNodeCreator) {\n    const selection = $getSelection();\n    const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null;\n    if (selection && matcher(blockElement)) {\n        $setBlocksType(selection, $createParagraphNode);\n    } else {\n        $setBlocksType(selection, creator);\n    }\n}\n\nexport function $insertNewBlockNodeAtSelection(node: LexicalNode, insertAfter: boolean = true) {\n    $insertNewBlockNodesAtSelection([node], insertAfter);\n}\n\nexport function $insertNewBlockNodesAtSelection(nodes: LexicalNode[], insertAfter: boolean = true) {\n    const selectionNodes = $getSelection()?.getNodes() || [];\n    const blockElement = selectionNodes.length > 0 ? $getNearestNodeBlockParent(selectionNodes[0]) : null;\n\n    if (blockElement) {\n        if (insertAfter) {\n            for (let i = nodes.length - 1; i >= 0; i--) {\n                blockElement.insertAfter(nodes[i]);\n            }\n        } else {\n            for (const node of nodes) {\n                blockElement.insertBefore(node);\n            }\n        }\n    } else {\n        $getRoot().append(...nodes);\n    }\n}\n\nexport function $selectSingleNode(node: LexicalNode) {\n    const nodeSelection = $createNodeSelection();\n    nodeSelection.add(node.getKey());\n    $setSelection(nodeSelection);\n}\n\nfunction getFirstTextNodeInNodes(nodes: LexicalNode[]): TextNode|null {\n    for (const node of nodes) {\n        if ($isTextNode(node)) {\n            return node;\n        }\n\n        if ($isElementNode(node)) {\n            const children = node.getChildren();\n            const textNode = getFirstTextNodeInNodes(children);\n            if (textNode !== null) {\n                return textNode;\n            }\n        }\n    }\n\n    return null;\n}\n\nfunction getLastTextNodeInNodes(nodes: LexicalNode[]): TextNode|null {\n    const revNodes = [...nodes].reverse();\n    for (const node of revNodes) {\n        if ($isTextNode(node)) {\n            return node;\n        }\n\n        if ($isElementNode(node)) {\n            const children = [...node.getChildren()].reverse();\n            const textNode = getLastTextNodeInNodes(children);\n            if (textNode !== null) {\n                return textNode;\n            }\n        }\n    }\n\n    return null;\n}\n\nexport function $selectNodes(nodes: LexicalNode[]) {\n    if (nodes.length === 0) {\n        return;\n    }\n\n    const selection = $createRangeSelection();\n    const firstText = getFirstTextNodeInNodes(nodes);\n    const lastText = getLastTextNodeInNodes(nodes);\n    if (firstText && lastText) {\n        selection.setTextNodeRange(firstText, 0, lastText, lastText.getTextContentSize() || 0)\n        $setSelection(selection);\n    }\n}\n\nexport function $toggleSelection(editor: LexicalEditor) {\n    const lastSelection = getLastSelection(editor);\n\n    if (lastSelection) {\n        window.requestAnimationFrame(() => {\n            editor.update(() => {\n                $setSelection(lastSelection.clone());\n            })\n        });\n    }\n}\n\nexport function $selectionContainsNode(selection: BaseSelection | null, node: LexicalNode): boolean {\n    if (!selection) {\n        return false;\n    }\n\n    const key = node.getKey();\n    for (const node of selection.getNodes()) {\n        if (node.getKey() === key) {\n            return true;\n        }\n    }\n\n    return false;\n}\n\nexport function $selectionContainsAlignment(selection: BaseSelection | null, alignment: CommonBlockAlignment): boolean {\n\n    const nodes = [\n        ...(selection?.getNodes() || []),\n        ...$getBlockElementNodesInSelection(selection)\n    ];\n    for (const node of nodes) {\n        if (nodeHasAlignment(node) && node.getAlignment() === alignment) {\n            return true;\n        }\n    }\n\n    return false;\n}\n\nexport function $selectionContainsDirection(selection: BaseSelection | null, direction: 'rtl'|'ltr'): boolean {\n\n    const nodes = [\n        ...(selection?.getNodes() || []),\n        ...$getBlockElementNodesInSelection(selection)\n    ];\n\n    for (const node of nodes) {\n        if ($isBlockElementNode(node) && node.getDirection() === direction) {\n            return true;\n        }\n    }\n\n    return false;\n}\n\nexport function $getBlockElementNodesInSelection(selection: BaseSelection | null): ElementNode[] {\n    if (!selection) {\n        return [];\n    }\n\n    const blockNodes: Map<string, ElementNode> = new Map();\n    for (const node of selection.getNodes()) {\n        const blockElement = $getNearestNodeBlockParent(node);\n        if ($isElementNode(blockElement)) {\n            blockNodes.set(blockElement.getKey(), blockElement);\n        }\n    }\n\n    return Array.from(blockNodes.values());\n}\n\nexport function $getDecoratorNodesInSelection(selection: BaseSelection | null): DecoratorNode<any>[] {\n    if (!selection) {\n        return [];\n    }\n\n    return selection.getNodes().filter(node => $isDecoratorNode(node));\n}"
  },
  {
    "path": "resources/js/wysiwyg/utils/table-copy-paste.ts",
    "content": "import {NodeClipboard} from \"./node-clipboard\";\nimport {$getTableCellsFromSelection, $getTableFromSelection, $getTableRowsFromSelection} from \"./tables\";\nimport {$getSelection, BaseSelection, LexicalEditor} from \"lexical\";\nimport {TableMap} from \"./table-map\";\nimport {\n    $createTableCellNode,\n    $isTableCellNode,\n    $isTableSelection,\n    TableCellNode,\n    TableNode,\n    TableRowNode\n} from \"@lexical/table\";\nimport {$getNodeFromSelection} from \"./selection\";\n\nconst rowClipboard: NodeClipboard<TableRowNode> = new NodeClipboard<TableRowNode>();\n\nexport function isRowClipboardEmpty(): boolean {\n    return rowClipboard.size() === 0;\n}\n\nexport function validateRowsToCopy(rows: TableRowNode[]): void {\n    let commonRowSize: number|null = null;\n\n    for (const row of rows) {\n        const cells = row.getChildren().filter(n => $isTableCellNode(n));\n        let rowSize = 0;\n        for (const cell of cells) {\n            rowSize += cell.getColSpan() || 1;\n            if (cell.getRowSpan() > 1) {\n                throw Error('Cannot copy rows with merged cells');\n            }\n        }\n\n        if (commonRowSize === null) {\n            commonRowSize = rowSize;\n        } else if (commonRowSize !== rowSize) {\n            throw Error('Cannot copy rows with inconsistent sizes');\n        }\n    }\n}\n\nexport function validateRowsToPaste(rows: TableRowNode[], targetTable: TableNode): void {\n    const tableColCount = (new TableMap(targetTable)).columnCount;\n    for (const row of rows) {\n        const cells = row.getChildren().filter(n => $isTableCellNode(n));\n        let rowSize = 0;\n        for (const cell of cells) {\n            rowSize += cell.getColSpan() || 1;\n        }\n\n        if (rowSize > tableColCount) {\n            throw Error('Cannot paste rows that are wider than target table');\n        }\n\n        while (rowSize < tableColCount) {\n            row.append($createTableCellNode());\n            rowSize++;\n        }\n    }\n}\n\nexport function $cutSelectedRowsToClipboard(): void {\n    const rows = $getTableRowsFromSelection($getSelection());\n    validateRowsToCopy(rows);\n    rowClipboard.set(...rows);\n    for (const row of rows) {\n        row.remove();\n    }\n}\n\nexport function $copySelectedRowsToClipboard(): void {\n    const rows = $getTableRowsFromSelection($getSelection());\n    validateRowsToCopy(rows);\n    rowClipboard.set(...rows);\n}\n\nexport function $pasteClipboardRowsBefore(editor: LexicalEditor): void {\n    const selection = $getSelection();\n    const rows = $getTableRowsFromSelection(selection);\n    const table = $getTableFromSelection(selection);\n    const lastRow = rows[rows.length - 1];\n    if (lastRow && table) {\n        const clipboardRows = rowClipboard.get(editor);\n        validateRowsToPaste(clipboardRows, table);\n        for (const row of clipboardRows) {\n            lastRow.insertBefore(row);\n        }\n    }\n}\n\nexport function $pasteClipboardRowsAfter(editor: LexicalEditor): void {\n    const selection = $getSelection();\n    const rows = $getTableRowsFromSelection(selection);\n    const table = $getTableFromSelection(selection);\n    const lastRow = rows[rows.length - 1];\n    if (lastRow && table) {\n        const clipboardRows = rowClipboard.get(editor).reverse();\n        validateRowsToPaste(clipboardRows, table);\n        for (const row of clipboardRows) {\n            lastRow.insertAfter(row);\n        }\n    }\n}\n\nconst columnClipboard: NodeClipboard<TableCellNode>[] = [];\n\nfunction setColumnClipboard(columns: TableCellNode[][]): void {\n    const newClipboards = columns.map(cells => {\n        const clipboard = new NodeClipboard<TableCellNode>();\n        clipboard.set(...cells);\n        return clipboard;\n    });\n\n    columnClipboard.splice(0, columnClipboard.length, ...newClipboards);\n}\n\ntype TableRange = {from: number, to: number};\n\nexport function isColumnClipboardEmpty(): boolean {\n    return columnClipboard.length === 0;\n}\n\nfunction $getSelectionColumnRange(selection: BaseSelection|null): TableRange|null {\n    if ($isTableSelection(selection)) {\n        const shape = selection.getShape()\n        return {from: shape.fromX, to: shape.toX};\n    }\n\n    const cell = $getNodeFromSelection(selection, $isTableCellNode);\n    const table = $getTableFromSelection(selection);\n    if (!$isTableCellNode(cell) || !table) {\n        return null;\n    }\n\n    const map = new TableMap(table);\n    const range = map.getRangeForCell(cell);\n    if (!range) {\n        return null;\n    }\n\n    return {from: range.fromX, to: range.toX};\n}\n\nfunction $getTableColumnCellsFromSelection(range: TableRange, table: TableNode): TableCellNode[][] {\n    const map = new TableMap(table);\n    const columns = [];\n    for (let x = range.from; x <= range.to; x++) {\n        const cells = map.getCellsInColumn(x);\n        columns.push(cells);\n    }\n\n    return columns;\n}\n\nfunction validateColumnsToCopy(columns: TableCellNode[][]): void {\n    let commonColSize: number|null = null;\n\n    for (const cells of columns) {\n        let colSize = 0;\n        for (const cell of cells) {\n            colSize += cell.getRowSpan() || 1;\n            if (cell.getColSpan() > 1) {\n                throw Error('Cannot copy columns with merged cells');\n            }\n        }\n\n        if (commonColSize === null) {\n            commonColSize = colSize;\n        } else if (commonColSize !== colSize) {\n            throw Error('Cannot copy columns with inconsistent sizes');\n        }\n    }\n}\n\nexport function $cutSelectedColumnsToClipboard(): void {\n    const selection = $getSelection();\n    const range = $getSelectionColumnRange(selection);\n    const table = $getTableFromSelection(selection);\n    if (!range || !table) {\n        return;\n    }\n\n    const colWidths = table.getColWidths();\n    const columns = $getTableColumnCellsFromSelection(range, table);\n    validateColumnsToCopy(columns);\n    setColumnClipboard(columns);\n    for (const cells of columns) {\n        for (const cell of cells) {\n            cell.remove();\n        }\n    }\n\n    const newWidths = [...colWidths].splice(range.from, (range.to - range.from) + 1);\n    table.setColWidths(newWidths);\n}\n\nexport function $copySelectedColumnsToClipboard(): void {\n    const selection = $getSelection();\n    const range = $getSelectionColumnRange(selection);\n    const table = $getTableFromSelection(selection);\n    if (!range || !table) {\n        return;\n    }\n\n    const columns = $getTableColumnCellsFromSelection(range, table);\n    validateColumnsToCopy(columns);\n    setColumnClipboard(columns);\n}\n\nfunction validateColumnsToPaste(columns: TableCellNode[][], targetTable: TableNode) {\n    const tableRowCount = (new TableMap(targetTable)).rowCount;\n    for (const cells of columns) {\n        let colSize = 0;\n        for (const cell of cells) {\n            colSize += cell.getRowSpan() || 1;\n        }\n\n        if (colSize > tableRowCount) {\n            throw Error('Cannot paste columns that are taller than target table');\n        }\n\n        while (colSize < tableRowCount) {\n            cells.push($createTableCellNode());\n            colSize++;\n        }\n    }\n}\n\nfunction $pasteClipboardColumns(editor: LexicalEditor, isBefore: boolean): void {\n    const selection = $getSelection();\n    const table = $getTableFromSelection(selection);\n    const cells = $getTableCellsFromSelection(selection);\n    const referenceCell = cells[isBefore ? 0 : cells.length - 1];\n    if (!table || !referenceCell) {\n        return;\n    }\n\n    const clipboardCols = columnClipboard.map(cb => cb.get(editor));\n    if (!isBefore) {\n        clipboardCols.reverse();\n    }\n\n    validateColumnsToPaste(clipboardCols, table);\n    const map = new TableMap(table);\n    const cellRange = map.getRangeForCell(referenceCell);\n    if (!cellRange) {\n        return;\n    }\n\n    const colIndex = isBefore ? cellRange.fromX : cellRange.toX;\n    const colWidths = table.getColWidths();\n\n    for (let y = 0; y < map.rowCount; y++) {\n        const relCell = map.getCellAtPosition(colIndex, y);\n        for (const cells of clipboardCols) {\n            const newCell = cells[y];\n            if (isBefore) {\n                relCell.insertBefore(newCell);\n            } else {\n                relCell.insertAfter(newCell);\n            }\n        }\n    }\n\n    const refWidth = colWidths[colIndex];\n    const addedWidths = clipboardCols.map(_ => refWidth);\n    colWidths.splice(isBefore ? colIndex : colIndex + 1, 0, ...addedWidths);\n}\n\nexport function $pasteClipboardColumnsBefore(editor: LexicalEditor): void {\n    $pasteClipboardColumns(editor, true);\n}\n\nexport function $pasteClipboardColumnsAfter(editor: LexicalEditor): void {\n    $pasteClipboardColumns(editor, false);\n}"
  },
  {
    "path": "resources/js/wysiwyg/utils/table-map.ts",
    "content": "import {$isTableCellNode, $isTableRowNode, TableCellNode, TableNode} from \"@lexical/table\";\n\nexport type CellRange = {\n    fromX: number;\n    fromY: number;\n    toX: number;\n    toY: number;\n}\n\nexport class TableMap {\n\n    rowCount: number = 0;\n    columnCount: number = 0;\n\n    // Represents an array (rows*columns in length) of cell nodes from top-left to\n    // bottom right. Cells may repeat where merged and covering multiple spaces.\n    cells: TableCellNode[] = [];\n\n    constructor(table: TableNode) {\n        this.buildCellMap(table);\n    }\n\n    protected buildCellMap(table: TableNode) {\n        const rowsAndCells: TableCellNode[][] = [];\n        const setCell = (x: number, y: number, cell: TableCellNode) => {\n            if (typeof rowsAndCells[y] === 'undefined') {\n                rowsAndCells[y] = [];\n            }\n\n            rowsAndCells[y][x] = cell;\n        };\n        const cellFilled = (x: number, y: number): boolean => !!(rowsAndCells[y] && rowsAndCells[y][x]);\n\n        const rowNodes = table.getChildren().filter(r => $isTableRowNode(r));\n        for (let rowIndex = 0; rowIndex < rowNodes.length; rowIndex++) {\n            const rowNode = rowNodes[rowIndex];\n            const cellNodes = rowNode.getChildren().filter(c => $isTableCellNode(c));\n            let targetColIndex: number = 0;\n            for (let cellIndex = 0; cellIndex < cellNodes.length; cellIndex++) {\n                const cellNode = cellNodes[cellIndex];\n                const colspan = cellNode.getColSpan() || 1;\n                const rowSpan = cellNode.getRowSpan() || 1;\n                for (let x = targetColIndex; x < targetColIndex + colspan; x++) {\n                    for (let y = rowIndex; y < rowIndex + rowSpan; y++) {\n                        while (cellFilled(x, y)) {\n                            targetColIndex += 1;\n                            x += 1;\n                        }\n\n                        setCell(x, y, cellNode);\n                    }\n                }\n                targetColIndex += colspan;\n            }\n        }\n\n        this.rowCount = rowsAndCells.length;\n        this.columnCount = Math.max(...rowsAndCells.map(r => r.length));\n\n        const cells = [];\n        let lastCell: TableCellNode = rowsAndCells[0][0];\n        for (let y = 0; y < this.rowCount; y++) {\n            for (let x = 0; x < this.columnCount; x++) {\n                if (!rowsAndCells[y] || !rowsAndCells[y][x]) {\n                    cells.push(lastCell);\n                } else {\n                    cells.push(rowsAndCells[y][x]);\n                    lastCell = rowsAndCells[y][x];\n                }\n            }\n        }\n\n        this.cells = cells;\n    }\n\n    public getCellAtPosition(x: number, y: number): TableCellNode {\n        const position = (y * this.columnCount) + x;\n        if (position >= this.cells.length) {\n            throw new Error(`TableMap Error: Attempted to get cell ${position+1} of ${this.cells.length}`);\n        }\n\n        return this.cells[position];\n    }\n\n    public getCellsInRange(range: CellRange): TableCellNode[] {\n        const minX = Math.max(Math.min(range.fromX, range.toX), 0);\n        const maxX = Math.min(Math.max(range.fromX, range.toX), this.columnCount - 1);\n        const minY = Math.max(Math.min(range.fromY, range.toY), 0);\n        const maxY = Math.min(Math.max(range.fromY, range.toY), this.rowCount - 1);\n\n        const cells = new Set<TableCellNode>();\n\n        for (let y = minY; y <= maxY; y++) {\n            for (let x = minX; x <= maxX; x++) {\n                cells.add(this.getCellAtPosition(x, y));\n            }\n        }\n\n        return [...cells.values()];\n    }\n\n    public getCellsInColumn(columnIndex: number): TableCellNode[] {\n        return this.getCellsInRange({\n            fromX: columnIndex,\n            toX: columnIndex,\n            fromY: 0,\n            toY: this.rowCount - 1,\n        });\n    }\n\n    public getRangeForCell(cell: TableCellNode): CellRange|null {\n        let range: CellRange|null = null;\n        const cellKey = cell.getKey();\n\n        for (let y = 0; y < this.rowCount; y++) {\n            for (let x = 0; x < this.columnCount; x++) {\n                const index = (y * this.columnCount) + x;\n                const lCell = this.cells[index];\n                if (lCell.getKey() === cellKey) {\n                    if (range === null) {\n                        range = {fromX: x, toX: x, fromY: y, toY: y};\n                    } else {\n                        range.fromX = Math.min(range.fromX, x);\n                        range.toX = Math.max(range.toX, x);\n                        range.fromY = Math.min(range.fromY, y);\n                        range.toY = Math.max(range.toY, y);\n                    }\n                }\n            }\n        }\n\n        return range;\n    }\n}"
  },
  {
    "path": "resources/js/wysiwyg/utils/tables.ts",
    "content": "import {BaseSelection, LexicalEditor} from \"lexical\";\nimport {\n    $isTableCellNode,\n    $isTableNode,\n    $isTableRowNode,\n    $isTableSelection, TableCellNode, TableNode,\n    TableRowNode,\n    TableSelection,\n} from \"@lexical/table\";\nimport {$getParentOfType} from \"./nodes\";\nimport {$getNodeFromSelection} from \"./selection\";\nimport {el, formatSizeValue} from \"./dom\";\nimport {TableMap} from \"./table-map\";\n\nfunction $getTableFromCell(cell: TableCellNode): TableNode|null {\n    return $getParentOfType(cell, $isTableNode) as TableNode|null;\n}\n\nexport function getTableColumnWidths(table: HTMLTableElement): string[] {\n    const maxColRow = getMaxColRowFromTable(table);\n\n    const colGroup = table.querySelector('colgroup');\n    let widths: string[] = [];\n    if (colGroup && (colGroup.childElementCount === maxColRow?.childElementCount || !maxColRow)) {\n        widths = extractWidthsFromRow(colGroup);\n    }\n    if (widths.filter(Boolean).length === 0 && maxColRow) {\n        widths = extractWidthsFromRow(maxColRow);\n    }\n\n    return widths;\n}\n\nfunction getMaxColRowFromTable(table: HTMLTableElement): HTMLTableRowElement | null {\n    const rows = table.querySelectorAll('tr');\n    let maxColCount: number = 0;\n    let maxColRow: HTMLTableRowElement | null = null;\n\n    for (const row of rows) {\n        if (row.childElementCount > maxColCount) {\n            maxColRow = row;\n            maxColCount = row.childElementCount;\n        }\n    }\n\n    return maxColRow;\n}\n\nfunction extractWidthsFromRow(row: HTMLTableRowElement | HTMLTableColElement) {\n    return [...row.children].map(child => extractWidthFromElement(child as HTMLElement))\n}\n\nfunction extractWidthFromElement(element: HTMLElement): string {\n    let width = element.style.width || element.getAttribute('width');\n    if (width && !Number.isNaN(Number(width))) {\n        width = width + 'px';\n    }\n\n    return width || '';\n}\n\nexport function $setTableColumnWidth(node: TableNode, columnIndex: number, width: number|string): void {\n    const rows = node.getChildren() as TableRowNode[];\n    let maxCols = 0;\n    for (const row of rows) {\n        const cellCount = row.getChildren().length;\n        if (cellCount > maxCols) {\n            maxCols = cellCount;\n        }\n    }\n\n    let colWidths = node.getColWidths();\n    if (colWidths.length === 0 || colWidths.length < maxCols) {\n        colWidths = Array(maxCols).fill('');\n    }\n\n    if (columnIndex + 1 > colWidths.length) {\n        console.error(`Attempted to set table column width for column [${columnIndex}] but only ${colWidths.length} columns found`);\n    }\n\n    colWidths[columnIndex] = formatSizeValue(width);\n    node.setColWidths(colWidths);\n}\n\nexport function $getTableColumnWidth(editor: LexicalEditor, node: TableNode, columnIndex: number): number {\n    const colWidths = node.getColWidths();\n    if (colWidths.length > columnIndex && colWidths[columnIndex].endsWith('px')) {\n        return Number(colWidths[columnIndex].replace('px', ''));\n    }\n\n    // Otherwise, get from table element\n    const table = editor.getElementByKey(node.__key) as HTMLTableElement | null;\n    if (table) {\n        const maxColRow = getMaxColRowFromTable(table);\n        if (maxColRow && maxColRow.children.length > columnIndex) {\n            const cell = maxColRow.children[columnIndex];\n            return cell.clientWidth;\n        }\n    }\n\n    return 0;\n}\n\nfunction $getCellColumnIndex(node: TableCellNode): number {\n    const row = node.getParent();\n    if (!$isTableRowNode(row)) {\n        return -1;\n    }\n\n    let index = 0;\n    const cells = row.getChildren<TableCellNode>();\n    for (const cell of cells) {\n        let colSpan = cell.getColSpan() || 1;\n        index += colSpan;\n        if (cell.getKey() === node.getKey()) {\n            break;\n        }\n    }\n\n    return index - 1;\n}\n\nexport function $setTableCellColumnWidth(cell: TableCellNode, width: string): void {\n    const table = $getTableFromCell(cell)\n    const index = $getCellColumnIndex(cell);\n\n    if (table && index >= 0) {\n        $setTableColumnWidth(table, index, width);\n    }\n}\n\nexport function $getTableCellColumnWidth(editor: LexicalEditor, cell: TableCellNode): string {\n    const table = $getTableFromCell(cell)\n    const index = $getCellColumnIndex(cell);\n    if (!table) {\n        return '';\n    }\n\n    const widths = table.getColWidths();\n    return (widths.length > index) ? widths[index] : '';\n}\n\nexport function buildColgroupFromTableWidths(colWidths: string[]): HTMLElement|null {\n    if (colWidths.length === 0) {\n        return null\n    }\n\n    const colgroup = el('colgroup');\n    for (const width of colWidths) {\n        const col = el('col');\n        if (width) {\n            col.style.width = width;\n        }\n        colgroup.append(col);\n    }\n\n    return colgroup;\n}\n\nexport function $getTableCellsFromSelection(selection: BaseSelection|null): TableCellNode[]  {\n    if ($isTableSelection(selection)) {\n        const nodes = selection.getNodes();\n        return nodes.filter(n => $isTableCellNode(n));\n    }\n\n    const cell = $getNodeFromSelection(selection, $isTableCellNode) as TableCellNode;\n    return cell ? [cell] : [];\n}\n\nexport function $mergeTableCellsInSelection(selection: TableSelection): void {\n    const selectionShape = selection.getShape();\n    const cells = $getTableCellsFromSelection(selection);\n    if (cells.length === 0) {\n        return;\n    }\n\n    const table = $getTableFromCell(cells[0]);\n    if (!table) {\n        return;\n    }\n\n    const tableMap = new TableMap(table);\n    const headCell = tableMap.getCellAtPosition(selectionShape.toX, selectionShape.toY);\n    if (!headCell) {\n        return;\n    }\n\n    // We have to adjust the shape since it won't take into account spans for the head corner position.\n    const fixedToX = selectionShape.toX + ((headCell.getColSpan() || 1) - 1);\n    const fixedToY = selectionShape.toY + ((headCell.getRowSpan() || 1) - 1);\n\n    const mergeCells = tableMap.getCellsInRange({\n        fromX: selectionShape.fromX,\n        fromY: selectionShape.fromY,\n        toX: fixedToX,\n        toY: fixedToY,\n    });\n\n    if (mergeCells.length === 0) {\n        return;\n    }\n\n    const firstCell = mergeCells[0];\n    const newWidth = Math.abs(selectionShape.fromX - fixedToX) + 1;\n    const newHeight = Math.abs(selectionShape.fromY - fixedToY) + 1;\n\n    for (let i = 1; i < mergeCells.length; i++) {\n        const mergeCell = mergeCells[i];\n        firstCell.append(...mergeCell.getChildren());\n        mergeCell.remove();\n    }\n\n    firstCell.setColSpan(newWidth);\n    firstCell.setRowSpan(newHeight);\n}\n\nexport function $getTableRowsFromSelection(selection: BaseSelection|null): TableRowNode[] {\n    const cells = $getTableCellsFromSelection(selection);\n    const rowsByKey: Record<string, TableRowNode> = {};\n    for (const cell of cells) {\n        const row = cell.getParent();\n        if ($isTableRowNode(row)) {\n            rowsByKey[row.getKey()] = row;\n        }\n    }\n\n    return Object.values(rowsByKey);\n}\n\nexport function $getTableFromSelection(selection: BaseSelection|null): TableNode|null {\n    const cells = $getTableCellsFromSelection(selection);\n    if (cells.length === 0) {\n        return null;\n    }\n\n    const table = $getParentOfType(cells[0], $isTableNode);\n    if ($isTableNode(table)) {\n        return table;\n    }\n\n    return null;\n}\n\nexport function $clearTableSizes(table: TableNode): void {\n    table.setColWidths([]);\n\n    // TODO - Extra form things once table properties and extra things\n    //   are supported\n\n    for (const row of table.getChildren()) {\n        if (!$isTableRowNode(row)) {\n            continue;\n        }\n\n        const rowStyles = row.getStyles();\n        rowStyles.delete('height');\n        rowStyles.delete('width');\n        row.setStyles(rowStyles);\n\n        const cells = row.getChildren().filter(c => $isTableCellNode(c));\n        for (const cell of cells) {\n            const cellStyles = cell.getStyles();\n            cellStyles.delete('height');\n            cellStyles.delete('width');\n            cell.setStyles(cellStyles);\n            cell.clearWidth();\n        }\n    }\n}\n\nexport function $clearTableFormatting(table: TableNode): void {\n    table.setColWidths([]);\n    table.setStyles(new Map);\n\n    for (const row of table.getChildren()) {\n        if (!$isTableRowNode(row)) {\n            continue;\n        }\n\n        row.setStyles(new Map);\n\n        const cells = row.getChildren().filter(c => $isTableCellNode(c));\n        for (const cell of cells) {\n            cell.setStyles(new Map);\n            cell.setBackgroundColor(null);\n            cell.clearWidth();\n        }\n    }\n}\n\n/**\n * Perform the given callback for each cell in the given table.\n * Returning false from the callback stops the function early.\n */\nexport function $forEachTableCell(table: TableNode, callback: (c: TableCellNode) => void|false): void {\n    outer: for (const row of table.getChildren()) {\n        if (!$isTableRowNode(row)) {\n            continue;\n        }\n        const cells = row.getChildren();\n        for (const cell of cells) {\n            if (!$isTableCellNode(cell)) {\n                return;\n            }\n            const result = callback(cell);\n            if (result === false) {\n                break outer;\n            }\n        }\n    }\n}\n\nexport function $getCellPaddingForTable(table: TableNode): string {\n    let padding: string|null = null;\n\n    $forEachTableCell(table, (cell: TableCellNode) => {\n        const cellPadding = cell.getStyles().get('padding') || ''\n        if (padding === null) {\n            padding = cellPadding;\n        }\n\n        if (cellPadding !== padding) {\n            padding = null;\n            return false;\n        }\n    });\n\n    return padding || '';\n}\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "resources/js/wysiwyg-tinymce/common-events.js",
    "content": "/**\n * @param {Editor} editor\n */\nexport function listen(editor) {\n    // Replace editor content\n    window.$events.listen('editor::replace', ({html}) => {\n        editor.setContent(html);\n    });\n\n    // Append editor content\n    window.$events.listen('editor::append', ({html}) => {\n        const content = editor.getContent() + html;\n        editor.setContent(content);\n    });\n\n    // Prepend editor content\n    window.$events.listen('editor::prepend', ({html}) => {\n        const content = html + editor.getContent();\n        editor.setContent(content);\n    });\n\n    // Insert editor content at the current location\n    window.$events.listen('editor::insert', ({html}) => {\n        editor.insertContent(html);\n    });\n\n    // Focus on the editor\n    window.$events.listen('editor::focus', () => {\n        if (editor.initialized) {\n            editor.focus();\n        }\n    });\n}\n"
  },
  {
    "path": "resources/js/wysiwyg-tinymce/config.js",
    "content": "import {register as registerShortcuts} from './shortcuts';\nimport {listen as listenForCommonEvents} from './common-events';\nimport {scrollToQueryString} from './scrolling';\nimport {listenForDragAndPaste} from './drop-paste-handling';\nimport {getPrimaryToolbar, registerAdditionalToolbars} from './toolbars';\nimport {registerCustomIcons} from './icons';\nimport {setupFilters} from './filters';\n\nimport {getPlugin as getCodeeditorPlugin} from './plugin-codeeditor';\nimport {getPlugin as getDrawioPlugin} from './plugin-drawio';\nimport {getPlugin as getCustomhrPlugin} from './plugins-customhr';\nimport {getPlugin as getImagemanagerPlugin} from './plugins-imagemanager';\nimport {getPlugin as getAboutPlugin} from './plugins-about';\nimport {getPlugin as getDetailsPlugin} from './plugins-details';\nimport {getPlugin as getTableAdditionsPlugin} from './plugins-table-additions';\nimport {getPlugin as getTasklistPlugin} from './plugins-tasklist';\nimport {\n    handleTableCellRangeEvents,\n    handleEmbedAlignmentChanges,\n    handleTextDirectionCleaning,\n} from './fixes';\n\nconst styleFormats = [\n    {title: 'Large Header', format: 'h2', preview: 'color: blue;'},\n    {title: 'Medium Header', format: 'h3'},\n    {title: 'Small Header', format: 'h4'},\n    {title: 'Tiny Header', format: 'h5'},\n    {\n        title: 'Paragraph', format: 'p', exact: true, classes: '',\n    },\n    {title: 'Blockquote', format: 'blockquote'},\n    {\n        title: 'Callouts',\n        items: [\n            {title: 'Information', format: 'calloutinfo'},\n            {title: 'Success', format: 'calloutsuccess'},\n            {title: 'Warning', format: 'calloutwarning'},\n            {title: 'Danger', format: 'calloutdanger'},\n        ],\n    },\n];\n\nconst formats = {\n    alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img,iframe,video', classes: 'align-left'},\n    aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img,iframe,video', classes: 'align-center'},\n    alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img,iframe,video', classes: 'align-right'},\n    calloutsuccess: {block: 'p', exact: true, attributes: {class: 'callout success'}},\n    calloutinfo: {block: 'p', exact: true, attributes: {class: 'callout info'}},\n    calloutwarning: {block: 'p', exact: true, attributes: {class: 'callout warning'}},\n    calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}},\n};\n\nconst colorMap = [\n    '#BFEDD2', '',\n    '#FBEEB8', '',\n    '#F8CAC6', '',\n    '#ECCAFA', '',\n    '#C2E0F4', '',\n\n    '#2DC26B', '',\n    '#F1C40F', '',\n    '#E03E2D', '',\n    '#B96AD9', '',\n    '#3598DB', '',\n\n    '#169179', '',\n    '#E67E23', '',\n    '#BA372A', '',\n    '#843FA1', '',\n    '#236FA1', '',\n\n    '#ECF0F1', '',\n    '#CED4D9', '',\n    '#95A5A6', '',\n    '#7E8C8D', '',\n    '#34495E', '',\n\n    '#000000', '',\n    '#ffffff', '',\n];\n\nfunction filePickerCallback(callback, value, meta) {\n    // field_name, url, type, win\n    if (meta.filetype === 'file') {\n        /** @type {EntitySelectorPopup} * */\n        const selector = window.$components.first('entity-selector-popup');\n        const selectionText = this.selection.getContent({format: 'text'}).trim();\n        selector.show(entity => {\n            callback(entity.link, {\n                text: entity.name,\n                title: entity.name,\n            });\n        }, {\n            initialValue: selectionText,\n            searchEndpoint: '/search/entity-selector',\n            entityTypes: 'page,book,chapter,bookshelf',\n            entityPermission: 'view',\n        });\n    }\n\n    if (meta.filetype === 'image') {\n        // Show image manager\n        /** @type {ImageManager} * */\n        const imageManager = window.$components.first('image-manager');\n        imageManager.show(image => {\n            callback(image.url, {alt: image.name});\n        }, 'gallery');\n    }\n}\n\n/**\n * @param {WysiwygConfigOptions} options\n * @return {string[]}\n */\nfunction gatherPlugins(options) {\n    const plugins = [\n        'image',\n        'table',\n        'link',\n        'autolink',\n        'fullscreen',\n        'code',\n        'customhr',\n        'autosave',\n        'lists',\n        'codeeditor',\n        'media',\n        'imagemanager',\n        'about',\n        'details',\n        'tasklist',\n        'tableadditions',\n        options.textDirection === 'rtl' ? 'directionality' : '',\n    ];\n\n    window.tinymce.PluginManager.add('codeeditor', getCodeeditorPlugin());\n    window.tinymce.PluginManager.add('customhr', getCustomhrPlugin());\n    window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin());\n    window.tinymce.PluginManager.add('about', getAboutPlugin());\n    window.tinymce.PluginManager.add('details', getDetailsPlugin());\n    window.tinymce.PluginManager.add('tasklist', getTasklistPlugin());\n    window.tinymce.PluginManager.add('tableadditions', getTableAdditionsPlugin());\n\n    if (options.drawioUrl) {\n        window.tinymce.PluginManager.add('drawio', getDrawioPlugin(options));\n        plugins.push('drawio');\n    }\n\n    return plugins.filter(plugin => Boolean(plugin));\n}\n\n/**\n * Fetch custom HTML head content nodes from the outer page head\n * and add them to the given editor document.\n * @param {Document} editorDoc\n */\nfunction addCustomHeadContent(editorDoc) {\n    const headContentLines = document.head.innerHTML.split('\\n');\n    const startLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- Start: custom user content -->');\n    const endLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- End: custom user content -->');\n    if (startLineIndex === -1 || endLineIndex === -1) {\n        return;\n    }\n\n    const customHeadHtml = headContentLines.slice(startLineIndex + 1, endLineIndex).join('\\n');\n    const el = editorDoc.createElement('div');\n    el.innerHTML = customHeadHtml;\n\n    editorDoc.head.append(...el.children);\n}\n\n/**\n * @param {WysiwygConfigOptions} options\n * @return {function(Editor)}\n */\nfunction getSetupCallback(options) {\n    return function setupCallback(editor) {\n        function editorChange() {\n            if (options.darkMode) {\n                editor.contentDocument.documentElement.classList.add('dark-mode');\n            }\n            window.$events.emit('editor-html-change', '');\n        }\n\n        editor.on('ExecCommand change input NodeChange ObjectResized', editorChange);\n        listenForCommonEvents(editor);\n        listenForDragAndPaste(editor, options);\n\n        editor.on('init', () => {\n            editorChange();\n            scrollToQueryString(editor);\n            window.editor = editor;\n            registerShortcuts(editor);\n        });\n\n        editor.on('PreInit', () => {\n            setupFilters(editor);\n        });\n\n        handleEmbedAlignmentChanges(editor);\n        handleTableCellRangeEvents(editor);\n        handleTextDirectionCleaning(editor);\n\n        // Custom handler hook\n        window.$events.emitPublic(options.containerElement, 'editor-tinymce::setup', {editor});\n\n        // Inline code format button\n        editor.ui.registry.addButton('inlinecode', {\n            tooltip: 'Inline code',\n            icon: 'sourcecode',\n            onAction() {\n                editor.execCommand('mceToggleFormat', false, 'code');\n            },\n        });\n    };\n}\n\n/**\n * @param {WysiwygConfigOptions} options\n */\nfunction getContentStyle(options) {\n    return `\nhtml, body, html.dark-mode {\n    background: ${options.darkMode ? '#222' : '#fff'};\n} \nbody {\n    padding-left: 15px !important;\n    padding-right: 15px !important; \n    height: initial !important;\n    margin:0!important; \n    margin-left: auto! important;\n    margin-right: auto !important;\n    overflow-y: hidden !important;\n}`.trim().replace('\\n', '');\n}\n\n/**\n * @param {WysiwygConfigOptions} options\n * @return {Object}\n */\nexport function buildForEditor(options) {\n    // Set language\n    window.tinymce.addI18n(options.language, options.translationMap);\n\n    // BookStack Version\n    const version = document.querySelector('script[src*=\"/dist/app.js\"]').getAttribute('src').split('?version=')[1];\n\n    // Return config object\n    return {\n        width: '100%',\n        height: '100%',\n        selector: '#html-editor',\n        cache_suffix: `?version=${version}`,\n        content_css: [\n            window.baseUrl('/dist/styles.css'),\n        ],\n        branding: false,\n        skin: options.darkMode ? 'tinymce-5-dark' : 'tinymce-5',\n        body_class: 'page-content',\n        browser_spellcheck: true,\n        relative_urls: false,\n        language: options.language,\n        directionality: options.textDirection,\n        remove_script_host: false,\n        document_base_url: window.baseUrl('/'),\n        end_container_on_empty_block: true,\n        remove_trailing_brs: false,\n        statusbar: false,\n        menubar: false,\n        paste_data_images: false,\n        extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram],details[*],summary[*],div[*],li[class|checked|style]',\n        automatic_uploads: false,\n        custom_elements: 'doc-root,code-block',\n        valid_children: [\n            '-div[p|h1|h2|h3|h4|h5|h6|blockquote|code-block]',\n            '+div[pre|img]',\n            '-doc-root[doc-root|#text]',\n            '-li[details]',\n            '+code-block[pre]',\n            '+doc-root[p|h1|h2|h3|h4|h5|h6|blockquote|code-block|div|hr]',\n        ].join(','),\n        plugins: gatherPlugins(options),\n        contextmenu: false,\n        toolbar: getPrimaryToolbar(options),\n        content_style: getContentStyle(options),\n        style_formats: styleFormats,\n        style_formats_merge: false,\n        media_alt_source: false,\n        media_poster: false,\n        formats,\n        table_style_by_css: true,\n        table_use_colgroups: true,\n        file_picker_types: 'file image',\n        color_map: colorMap,\n        file_picker_callback: filePickerCallback,\n        paste_preprocess(plugin, args) {\n            const {content} = args;\n            if (content.indexOf('<img src=\"file://') !== -1) {\n                args.content = '';\n            }\n        },\n        init_instance_callback(editor) {\n            addCustomHeadContent(editor.getDoc());\n        },\n        setup(editor) {\n            registerCustomIcons(editor);\n            registerAdditionalToolbars(editor);\n            getSetupCallback(options)(editor);\n        },\n    };\n}\n\n/**\n * @typedef {Object} WysiwygConfigOptions\n * @property {Element} containerElement\n * @property {string} language\n * @property {boolean} darkMode\n * @property {string} textDirection\n * @property {string} drawioUrl\n * @property {int} pageId\n * @property {Object} translations\n * @property {Object} translationMap\n */\n"
  },
  {
    "path": "resources/js/wysiwyg-tinymce/drop-paste-handling.js",
    "content": "import {Clipboard} from '../services/clipboard.ts';\n\nlet wrap;\nlet draggedContentEditable;\n\nfunction hasTextContent(node) {\n    return node && !!(node.textContent || node.innerText);\n}\n\n/**\n * Upload an image file to the server\n * @param {File} file\n * @param {int} pageId\n */\nasync function uploadImageFile(file, pageId) {\n    if (file === null || file.type.indexOf('image') !== 0) {\n        throw new Error('Not an image file');\n    }\n\n    const remoteFilename = file.name || `image-${Date.now()}.png`;\n    const formData = new FormData();\n    formData.append('file', file, remoteFilename);\n    formData.append('uploaded_to', pageId);\n\n    const resp = await window.$http.post(window.baseUrl('/images/gallery'), formData);\n    return resp.data;\n}\n\n/**\n * Handle pasting images from clipboard.\n * @param {Editor} editor\n * @param {WysiwygConfigOptions} options\n * @param {ClipboardEvent|DragEvent} event\n */\nfunction paste(editor, options, event) {\n    const clipboard = new Clipboard(event.clipboardData || event.dataTransfer);\n\n    // Don't handle the event ourselves if no items exist of contains table-looking data\n    if (!clipboard.hasItems() || clipboard.containsTabularData()) {\n        return;\n    }\n\n    const images = clipboard.getImages();\n    for (const imageFile of images) {\n        const id = `image-${Math.random().toString(16).slice(2)}`;\n        const loadingImage = window.baseUrl('/loading.gif');\n        event.preventDefault();\n\n        setTimeout(() => {\n            editor.insertContent(`<p><img src=\"${loadingImage}\" id=\"${id}\"></p>`);\n\n            uploadImageFile(imageFile, options.pageId).then(resp => {\n                const safeName = resp.name.replace(/\"/g, '');\n                const newImageHtml = `<img src=\"${resp.thumbs.display}\" alt=\"${safeName}\" />`;\n\n                const newEl = editor.dom.create('a', {\n                    target: '_blank',\n                    href: resp.url,\n                }, newImageHtml);\n\n                editor.dom.replace(newEl, id);\n            }).catch(err => {\n                editor.dom.remove(id);\n                window.$events.error(err?.data?.message || options.translations.imageUploadErrorText);\n                console.error(err);\n            });\n        }, 10);\n    }\n}\n\n/**\n * @param {Editor} editor\n */\nfunction dragStart(editor) {\n    const node = editor.selection.getNode();\n\n    if (node.nodeName === 'IMG') {\n        wrap = editor.dom.getParent(node, '.mceTemp');\n\n        if (!wrap && node.parentNode.nodeName === 'A' && !hasTextContent(node.parentNode)) {\n            wrap = node.parentNode;\n        }\n    }\n\n    // Track dragged contenteditable blocks\n    if (node.hasAttribute('contenteditable') && node.getAttribute('contenteditable') === 'false') {\n        draggedContentEditable = node;\n    }\n}\n\n/**\n * @param {Editor} editor\n * @param {WysiwygConfigOptions} options\n * @param {DragEvent} event\n */\nfunction drop(editor, options, event) {\n    const {dom} = editor;\n    const rng = window.tinymce.dom.RangeUtils.getCaretRangeFromPoint(\n        event.clientX,\n        event.clientY,\n        editor.getDoc(),\n    );\n\n    // Template insertion\n    const templateId = event.dataTransfer && event.dataTransfer.getData('bookstack/template');\n    if (templateId) {\n        event.preventDefault();\n        window.$http.get(`/templates/${templateId}`).then(resp => {\n            editor.selection.setRng(rng);\n            editor.undoManager.transact(() => {\n                editor.execCommand('mceInsertContent', false, resp.data.html);\n            });\n        });\n    }\n\n    // Don't allow anything to be dropped in a captioned image.\n    if (dom.getParent(rng.startContainer, '.mceTemp')) {\n        event.preventDefault();\n    } else if (wrap) {\n        event.preventDefault();\n\n        editor.undoManager.transact(() => {\n            editor.selection.setRng(rng);\n            editor.selection.setNode(wrap);\n            dom.remove(wrap);\n        });\n    }\n\n    // Handle contenteditable section drop\n    if (!event.isDefaultPrevented() && draggedContentEditable) {\n        event.preventDefault();\n        editor.undoManager.transact(() => {\n            const selectedNode = editor.selection.getNode();\n            const range = editor.selection.getRng();\n            const selectedNodeRoot = selectedNode.closest('body > *');\n            if (range.startOffset > (range.startContainer.length / 2)) {\n                selectedNodeRoot.after(draggedContentEditable);\n            } else {\n                selectedNodeRoot.before(draggedContentEditable);\n            }\n        });\n    }\n\n    // Handle image insert\n    if (!event.isDefaultPrevented()) {\n        paste(editor, options, event);\n    }\n\n    wrap = null;\n}\n\n/**\n * @param {Editor} editor\n * @param {DragEvent} event\n */\nfunction dragOver(editor, event) {\n    // This custom handling essentially emulates the default TinyMCE behaviour while allowing us\n    // to specifically call preventDefault on the event to allow the drop of custom elements.\n    event.preventDefault();\n    editor.focus();\n    const rangeUtils = window.tinymce.dom.RangeUtils;\n    const range = rangeUtils.getCaretRangeFromPoint(event.clientX ?? 0, event.clientY ?? 0, editor.getDoc());\n    editor.selection.setRng(range);\n}\n\n/**\n * @param {Editor} editor\n * @param {WysiwygConfigOptions} options\n */\nexport function listenForDragAndPaste(editor, options) {\n    editor.on('dragover', event => dragOver(editor, event));\n    editor.on('dragstart', () => dragStart(editor));\n    editor.on('drop', event => drop(editor, options, event));\n    editor.on('paste', event => paste(editor, options, event));\n}\n"
  },
  {
    "path": "resources/js/wysiwyg-tinymce/filters.js",
    "content": "/**\n * Setup a serializer filter for <br> tags to ensure they're not rendered\n * within code blocks and that we use newlines there instead.\n * @param {Editor} editor\n */\nfunction setupBrFilter(editor) {\n    editor.serializer.addNodeFilter('br', nodes => {\n        for (const node of nodes) {\n            if (node.parent && node.parent.name === 'code') {\n                const newline = window.tinymce.html.Node.create('#text');\n                newline.value = '\\n';\n                node.replace(newline);\n            }\n        }\n    });\n}\n\n/**\n * Remove accidentally added pointer elements that are within the content.\n * These could have accidentally been added via getting caught in range\n * selection within page content.\n * @param {Editor} editor\n */\nfunction setupPointerFilter(editor) {\n    editor.parser.addNodeFilter('div', nodes => {\n        for (const node of nodes) {\n            const id = node.attr('id') || '';\n            const nodeClass = node.attr('class') || '';\n            if (id === 'pointer' || nodeClass.includes('pointer')) {\n                node.remove();\n            }\n        }\n    });\n}\n\n/**\n * Setup global default filters for the given editor instance.\n * @param {Editor} editor\n */\nexport function setupFilters(editor) {\n    setupBrFilter(editor);\n    setupPointerFilter(editor);\n}\n"
  },
  {
    "path": "resources/js/wysiwyg-tinymce/fixes.js",
    "content": "/**\n * Handle alignment for embed (iframe/video) content.\n * TinyMCE built-in handling doesn't work well for these when classes are used for\n * alignment, since the editor wraps these elements in a non-editable preview span\n * which looses tracking and setting of alignment options.\n * Here we manually manage these properties and formatting events, by effectively\n * syncing the alignment classes to the parent preview span.\n * @param {Editor} editor\n */\nexport function handleEmbedAlignmentChanges(editor) {\n    function updateClassesForPreview(previewElem) {\n        const mediaTarget = previewElem.querySelector('iframe, video');\n        if (!mediaTarget) {\n            return;\n        }\n\n        const alignmentClasses = [...mediaTarget.classList.values()].filter(c => c.startsWith('align-'));\n        const previewAlignClasses = [...previewElem.classList.values()].filter(c => c.startsWith('align-'));\n        previewElem.classList.remove(...previewAlignClasses);\n        previewElem.classList.add(...alignmentClasses);\n    }\n\n    editor.on('SetContent', () => {\n        const previewElems = editor.dom.select('span.mce-preview-object');\n        for (const previewElem of previewElems) {\n            updateClassesForPreview(previewElem);\n        }\n    });\n\n    editor.on('FormatApply', event => {\n        const isAlignment = event.format.startsWith('align');\n        const isElement = event.node instanceof editor.dom.doc.defaultView.HTMLElement;\n        if (!isElement || !isAlignment || !event.node.matches('.mce-preview-object')) {\n            return;\n        }\n\n        const realTarget = event.node.querySelector('iframe, video');\n        if (realTarget) {\n            const className = (editor.formatter.get(event.format)[0]?.classes || [])[0];\n            const toAdd = !realTarget.classList.contains(className);\n\n            const wrapperClasses = (event.node.getAttribute('data-mce-p-class') || '').split(' ');\n            const wrapperClassesFiltered = wrapperClasses.filter(c => !c.startsWith('align-'));\n            if (toAdd) {\n                wrapperClassesFiltered.push(className);\n            }\n\n            const classesToApply = wrapperClassesFiltered.join(' ');\n            event.node.setAttribute('data-mce-p-class', classesToApply);\n\n            realTarget.setAttribute('class', classesToApply);\n            editor.formatter.apply(event.format, {}, realTarget);\n            updateClassesForPreview(event.node);\n        }\n    });\n}\n\n/**\n * Cleans up and removes text-alignment specific properties on all child elements.\n * @param {HTMLElement} element\n */\nfunction cleanChildAlignment(element) {\n    const alignedChildren = element.querySelectorAll('[align],[style*=\"text-align\"],.align-center,.align-left,.align-right');\n    for (const child of alignedChildren) {\n        child.removeAttribute('align');\n        child.style.textAlign = null;\n        child.classList.remove('align-center', 'align-right', 'align-left');\n    }\n}\n\n/**\n * Cleans up the direction property for an element.\n * Removes all inline direction control from child elements.\n * Removes non \"dir\" attribute direction control from provided element.\n * @param {HTMLElement} element\n */\nfunction cleanElementDirection(element) {\n    const directionChildren = element.querySelectorAll('[dir],[style*=\"direction\"]');\n    for (const child of directionChildren) {\n        child.removeAttribute('dir');\n        child.style.direction = null;\n    }\n\n    cleanChildAlignment(element);\n    element.style.direction = null;\n    element.style.textAlign = null;\n    element.removeAttribute('align');\n}\n\n/**\n * @typedef {Function} TableCellHandler\n * @param {HTMLTableCellElement} cell\n */\n\n/**\n * This tracks table cell range selection, so we can apply custom handling where\n * required to actions applied to such selections.\n * The events used don't seem to be advertised by TinyMCE.\n * Found at https://github.com/tinymce/tinymce/blob/6.8.3/modules/tinymce/src/models/dom/main/ts/table/api/Events.ts\n * @param {Editor} editor\n */\nexport function handleTableCellRangeEvents(editor) {\n    /** @var {HTMLTableCellElement[]} * */\n    let selectedCells = [];\n\n    editor.on('TableSelectionChange', event => {\n        selectedCells = (event.cells || []).map(cell => cell.dom);\n    });\n    editor.on('TableSelectionClear', () => {\n        selectedCells = [];\n    });\n\n    /**\n     * @type {Object<String, TableCellHandler>}\n     */\n    const actionByCommand = {\n        // TinyMCE does not seem to do a great job on clearing styles in complex\n        // scenarios (like copied word content) when a range of table cells\n        // are selected. Here we watch for clear formatting events, so some manual\n        // cleanup can be performed.\n        RemoveFormat: cell => {\n            const attrsToRemove = ['class', 'style', 'width', 'height', 'align'];\n            for (const attr of attrsToRemove) {\n                cell.removeAttribute(attr);\n            }\n        },\n\n        // TinyMCE does not apply direction events to table cell range selections\n        // so here we hastily patch in that ability by setting the direction ourselves\n        // when a direction event is fired.\n        mceDirectionLTR: cell => {\n            cell.setAttribute('dir', 'ltr');\n            cleanElementDirection(cell);\n        },\n        mceDirectionRTL: cell => {\n            cell.setAttribute('dir', 'rtl');\n            cleanElementDirection(cell);\n        },\n\n        // The \"align\" attribute can exist on table elements so this clears\n        // the attribute, and also clears common child alignment properties,\n        // when a text direction action is made for a table cell range.\n        JustifyLeft: cell => {\n            cell.removeAttribute('align');\n            cleanChildAlignment(cell);\n        },\n    };\n\n    // Copy justify left action to other alignment actions\n    actionByCommand.JustifyRight = actionByCommand.JustifyLeft;\n    actionByCommand.JustifyCenter = actionByCommand.JustifyLeft;\n    actionByCommand.JustifyFull = actionByCommand.JustifyLeft;\n\n    editor.on('ExecCommand', event => {\n        const action = actionByCommand[event.command];\n        if (action) {\n            for (const cell of selectedCells) {\n                action(cell);\n            }\n        }\n    });\n}\n\n/**\n * Direction control might not work if there are other unexpected direction-handling styles\n * or attributes involved nearby. This watches for direction change events to clean\n * up direction controls, removing non-dir-attr direction controls, while removing\n * directions from child elements that may be involved.\n * @param {Editor} editor\n */\nexport function handleTextDirectionCleaning(editor) {\n    editor.on('ExecCommand', event => {\n        const command = event.command;\n        if (command !== 'mceDirectionLTR' && command !== 'mceDirectionRTL') {\n            return;\n        }\n\n        const blocks = editor.selection.getSelectedBlocks();\n        for (const block of blocks) {\n            cleanElementDirection(block);\n        }\n    });\n}\n"
  },
  {
    "path": "resources/js/wysiwyg-tinymce/icons.js",
    "content": "const icons = {\n    'table-delete-column': '<svg width=\"24\" height=\"24\"><path d=\"M21 19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14c1.1 0 2 .9 2 2zm-2 0V5h-4v2.2h-2V5h-2v2.2H9V5H5v14h4v-2.1h2V19h2v-2.1h2V19Z\"/><path d=\"M14.829 10.585 13.415 12l1.414 1.414c.943.943-.472 2.357-1.414 1.414L12 13.414l-1.414 1.414c-.944.944-2.358-.47-1.414-1.414L10.586 12l-1.414-1.415c-.943-.942.471-2.357 1.414-1.414L12 10.585l1.344-1.343c1.111-1.112 2.2.627 1.485 1.343z\" style=\"fill-rule:nonzero\"/></svg>',\n    'table-delete-row': '<svg width=\"24\" height=\"24\"><path d=\"M5 21a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14c0 1.1-.9 2-2 2zm0-2h14v-4h-2.2v-2H19v-2h-2.2V9H19V5H5v4h2.1v2H5v2h2.1v2H5Z\"/><path d=\"M13.415 14.829 12 13.415l-1.414 1.414c-.943.943-2.357-.472-1.414-1.414L10.586 12l-1.414-1.414c-.944-.944.47-2.358 1.414-1.414L12 10.586l1.415-1.414c.942-.943 2.357.471 1.414 1.414L13.415 12l1.343 1.344c1.112 1.111-.627 2.2-1.343 1.485z\" style=\"fill-rule:nonzero\"/></svg>',\n    'table-insert-column-after': '<svg width=\"24\" height=\"24\"><path d=\"M16 5h-5v14h5c1.235 0 1.234 2 0 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11c1.229 0 1.236 2 0 2zm-7 6V5H5v6zm0 8v-6H5v6zm11.076-6h-2v2c0 1.333-2 1.333-2 0v-2h-2c-1.335 0-1.335-2 0-2h2V9c0-1.333 2-1.333 2 0v2h1.9c1.572 0 1.113 2 .1 2z\"/></svg>',\n    'table-insert-column-before': '<svg width=\"24\" height=\"24\"><path d=\"M8 19h5V5H8C6.764 5 6.766 3 8 3h11a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H8c-1.229 0-1.236-2 0-2zm7-6v6h4v-6zm0-8v6h4V5ZM3.924 11h2V9c0-1.333 2-1.333 2 0v2h2c1.335 0 1.335 2 0 2h-2v2c0 1.333-2 1.333-2 0v-2h-1.9c-1.572 0-1.113-2-.1-2z\"/></svg>',\n    'table-insert-row-above': '<svg width=\"24\" height=\"24\"><path d=\"M5 8v5h14V8c0-1.235 2-1.234 2 0v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8C3 6.77 5 6.764 5 8zm6 7H5v4h6zm8 0h-6v4h6zM13 3.924v2h2c1.333 0 1.333 2 0 2h-2v2c0 1.335-2 1.335-2 0v-2H9c-1.333 0-1.333-2 0-2h2v-1.9c0-1.572 2-1.113 2-.1z\"/></svg>',\n    'table-insert-row-after': '<svg width=\"24\" height=\"24\"><path d=\"M19 16v-5H5v5c0 1.235-2 1.234-2 0V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v11c0 1.229-2 1.236-2 0zm-6-7h6V5h-6zM5 9h6V5H5Zm6 11.076v-2H9c-1.333 0-1.333-2 0-2h2v-2c0-1.335 2-1.335 2 0v2h2c1.333 0 1.333 2 0 2h-2v1.9c0 1.572-2 1.113-2 .1z\"/></svg>',\n    table: '<svg width=\"24\" height=\"24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5c0-1.1.9-2 2-2ZM5 14v5h6v-5zm14 0h-6v5h6zm0-7h-6v5h6zM5 12h6V7H5Z\"/></svg>',\n    'table-delete-table': '<svg width=\"24\" height=\"24\"><path d=\"M5 21a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14c0 1.1-.9 2-2 2zm0-2h14V5H5v14z\"/><path d=\"m13.711 15.423-1.71-1.712-1.712 1.712c-1.14 1.14-2.852-.57-1.71-1.712l1.71-1.71-1.71-1.712c-1.143-1.142.568-2.853 1.71-1.71L12 10.288l1.711-1.71c1.141-1.142 2.852.57 1.712 1.71L13.71 12l1.626 1.626c1.345 1.345-.76 2.663-1.626 1.797z\" style=\"fill-rule:nonzero;stroke-width:1.20992\"/></svg>',\n};\n\n/**\n * @param {Editor} editor\n */\nexport function registerCustomIcons(editor) {\n    for (const [name, svg] of Object.entries(icons)) {\n        editor.ui.registry.addIcon(name, svg);\n    }\n}\n"
  },
  {
    "path": "resources/js/wysiwyg-tinymce/plugin-codeeditor.js",
    "content": "function elemIsCodeBlock(elem) {\n    return elem.tagName.toLowerCase() === 'code-block';\n}\n\n/**\n * @param {Editor} editor\n * @param {String} code\n * @param {String} language\n * @param {String} direction\n * @param {function(string, string)} callback (Receives (code: string,language: string)\n */\nfunction showPopup(editor, code, language, direction, callback) {\n    /** @var {CodeEditor} codeEditor * */\n    const codeEditor = window.$components.first('code-editor');\n    const bookMark = editor.selection.getBookmark();\n    codeEditor.open(code, language, direction, (newCode, newLang) => {\n        callback(newCode, newLang);\n        editor.focus();\n        editor.selection.moveToBookmark(bookMark);\n    }, () => {\n        editor.focus();\n        editor.selection.moveToBookmark(bookMark);\n    });\n}\n\n/**\n * @param {Editor} editor\n * @param {CodeBlockElement} codeBlock\n */\nfunction showPopupForCodeBlock(editor, codeBlock) {\n    const direction = codeBlock.getAttribute('dir') || '';\n    showPopup(editor, codeBlock.getContent(), codeBlock.getLanguage(), direction, (newCode, newLang) => {\n        codeBlock.setContent(newCode, newLang);\n    });\n}\n\n/**\n * Define our custom code-block HTML element that we use.\n * Needs to be delayed since it needs to be defined within the context of the\n * child editor window and document, hence its definition within a callback.\n * @param {Editor} editor\n */\nfunction defineCodeBlockCustomElement(editor) {\n    const doc = editor.getDoc();\n    const win = doc.defaultView;\n\n    class CodeBlockElement extends win.HTMLElement {\n\n        /**\n         * @type {?SimpleEditorInterface}\n         */\n        editor = null;\n\n        constructor() {\n            super();\n            this.attachShadow({mode: 'open'});\n\n            const stylesToCopy = document.head.querySelectorAll('link[rel=\"stylesheet\"]:not([media=\"print\"]),style');\n            const copiedStyles = Array.from(stylesToCopy).map(styleEl => styleEl.cloneNode(true));\n\n            const cmContainer = document.createElement('div');\n            cmContainer.style.pointerEvents = 'none';\n            cmContainer.contentEditable = 'false';\n            cmContainer.classList.add('CodeMirrorContainer');\n            cmContainer.classList.toggle('dark-mode', document.documentElement.classList.contains('dark-mode'));\n\n            this.shadowRoot.append(...copiedStyles, cmContainer);\n        }\n\n        getLanguage() {\n            const getLanguageFromClassList = classes => {\n                const langClasses = classes.split(' ').filter(cssClass => cssClass.startsWith('language-'));\n                return (langClasses[0] || '').replace('language-', '');\n            };\n\n            const code = this.querySelector('code');\n            const pre = this.querySelector('pre');\n            return getLanguageFromClassList(pre.className) || (code && getLanguageFromClassList(code.className)) || '';\n        }\n\n        setContent(content, language) {\n            if (this.editor) {\n                this.editor.setContent(content);\n                this.editor.setMode(language, content);\n            }\n\n            let pre = this.querySelector('pre');\n            if (!pre) {\n                pre = doc.createElement('pre');\n                this.append(pre);\n            }\n            pre.innerHTML = '';\n\n            const code = doc.createElement('code');\n            pre.append(code);\n            code.innerText = content;\n            code.className = `language-${language}`;\n        }\n\n        getContent() {\n            const code = this.querySelector('code') || this.querySelector('pre');\n            const tempEl = document.createElement('pre');\n            tempEl.innerHTML = code.innerHTML.replace(/\\ufeff/g, '');\n\n            const brs = tempEl.querySelectorAll('br');\n            for (const br of brs) {\n                br.replaceWith('\\n');\n            }\n\n            return tempEl.textContent;\n        }\n\n        connectedCallback() {\n            const connectedTime = Date.now();\n            if (this.editor) {\n                return;\n            }\n\n            this.cleanChildContent();\n            const content = this.getContent();\n            const lines = content.split('\\n').length;\n            const height = (lines * 19.2) + 18 + 24;\n            this.style.height = `${height}px`;\n\n            const container = this.shadowRoot.querySelector('.CodeMirrorContainer');\n            const renderEditor = Code => {\n                this.editor = Code.wysiwygView(container, this.shadowRoot, content, this.getLanguage());\n                setTimeout(() => {\n                    this.style.height = null;\n                }, 12);\n            };\n\n            window.importVersioned('code').then(Code => {\n                const timeout = (Date.now() - connectedTime < 20) ? 20 : 0;\n                setTimeout(() => renderEditor(Code), timeout);\n            });\n        }\n\n        cleanChildContent() {\n            const pre = this.querySelector('pre');\n            if (!pre) return;\n\n            for (const preChild of pre.childNodes) {\n                if (preChild.nodeName === '#text' && preChild.textContent === '﻿') {\n                    preChild.remove();\n                }\n            }\n        }\n\n    }\n\n    win.customElements.define('code-block', CodeBlockElement);\n}\n\n/**\n * @param {Editor} editor\n */\nfunction register(editor) {\n    editor.ui.registry.addIcon('codeblock', '<svg width=\"24\" height=\"24\"><path d=\"M4 3h16c.6 0 1 .4 1 1v16c0 .6-.4 1-1 1H4a1 1 0 0 1-1-1V4c0-.6.4-1 1-1Zm1 2v14h14V5Z\"/><path d=\"M11.103 15.423c.277.277.277.738 0 .922a.692.692 0 0 1-1.106 0l-4.057-3.78a.738.738 0 0 1 0-1.107l4.057-3.872c.276-.277.83-.277 1.106 0a.724.724 0 0 1 0 1.014L7.6 12.012ZM12.897 8.577c-.245-.312-.2-.675.08-.955.28-.281.727-.27 1.027.033l4.057 3.78a.738.738 0 0 1 0 1.107l-4.057 3.872c-.277.277-.83.277-1.107 0a.724.724 0 0 1 0-1.014l3.504-3.412z\"/></svg>');\n\n    editor.ui.registry.addButton('codeeditor', {\n        tooltip: 'Insert code block',\n        icon: 'codeblock',\n        onAction() {\n            editor.execCommand('codeeditor');\n        },\n    });\n\n    editor.ui.registry.addButton('editcodeeditor', {\n        tooltip: 'Edit code block',\n        icon: 'edit-block',\n        onAction() {\n            editor.execCommand('codeeditor');\n        },\n    });\n\n    editor.addCommand('codeeditor', () => {\n        const selectedNode = editor.selection.getNode();\n        const doc = selectedNode.ownerDocument;\n        if (elemIsCodeBlock(selectedNode)) {\n            showPopupForCodeBlock(editor, selectedNode);\n        } else {\n            const textContent = editor.selection.getContent({format: 'text'});\n            const direction = document.dir === 'rtl' ? 'ltr' : '';\n            showPopup(editor, textContent, '', direction, (newCode, newLang) => {\n                const pre = doc.createElement('pre');\n                const code = doc.createElement('code');\n                code.classList.add(`language-${newLang}`);\n                code.innerText = newCode;\n                if (direction) {\n                    pre.setAttribute('dir', direction);\n                }\n\n                pre.append(code);\n                editor.insertContent(pre.outerHTML);\n            });\n        }\n    });\n\n    editor.on('dblclick', () => {\n        const selectedNode = editor.selection.getNode();\n        if (elemIsCodeBlock(selectedNode)) {\n            showPopupForCodeBlock(editor, selectedNode);\n        }\n    });\n\n    editor.on('PreInit', () => {\n        editor.parser.addNodeFilter('pre', elms => {\n            for (const el of elms) {\n                const wrapper = window.tinymce.html.Node.create('code-block', {\n                    contenteditable: 'false',\n                });\n\n                const childCodeBlock = el.children().filter(child => child.name === 'code')[0] || null;\n                const direction = el.attr('dir') || (childCodeBlock && childCodeBlock.attr('dir')) || '';\n                if (direction) {\n                    wrapper.attr('dir', direction);\n                }\n\n                const spans = el.getAll('span');\n                for (const span of spans) {\n                    span.unwrap();\n                }\n                el.attr('style', null);\n                el.wrap(wrapper);\n            }\n        });\n\n        editor.parser.addNodeFilter('code-block', elms => {\n            for (const el of elms) {\n                el.attr('contenteditable', 'false');\n            }\n        });\n\n        editor.serializer.addNodeFilter('code-block', elms => {\n            for (const el of elms) {\n                const direction = el.attr('dir');\n                if (direction && el.firstChild) {\n                    el.firstChild.attr('dir', direction);\n                } else if (el.firstChild) {\n                    el.firstChild.attr('dir', null);\n                }\n\n                el.unwrap();\n            }\n        });\n    });\n\n    editor.ui.registry.addContextToolbar('codeeditor', {\n        predicate(node) {\n            return node.nodeName.toLowerCase() === 'code-block';\n        },\n        items: 'editcodeeditor',\n        position: 'node',\n        scope: 'node',\n    });\n\n    editor.on('PreInit', () => {\n        defineCodeBlockCustomElement(editor);\n    });\n}\n\n/**\n * @return {register}\n */\nexport function getPlugin() {\n    return register;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg-tinymce/plugin-drawio.js",
    "content": "import * as DrawIO from '../services/drawio.ts';\nimport {wait} from '../services/util.ts';\n\nlet pageEditor = null;\nlet currentNode = null;\n\n/**\n * @type {WysiwygConfigOptions}\n */\nlet options = {};\n\nfunction isDrawing(node) {\n    return node.hasAttribute('drawio-diagram');\n}\n\nfunction showDrawingManager(mceEditor, selectedNode = null) {\n    pageEditor = mceEditor;\n    currentNode = selectedNode;\n\n    /** @type {ImageManager} * */\n    const imageManager = window.$components.first('image-manager');\n    imageManager.show(image => {\n        if (selectedNode) {\n            const imgElem = selectedNode.querySelector('img');\n            pageEditor.undoManager.transact(() => {\n                pageEditor.dom.setAttrib(imgElem, 'src', image.url);\n                pageEditor.dom.setAttrib(selectedNode, 'drawio-diagram', image.id);\n            });\n        } else {\n            const imgHTML = `<div drawio-diagram=\"${image.id}\" contenteditable=\"false\"><img src=\"${image.url}\"></div>`;\n            pageEditor.insertContent(imgHTML);\n        }\n    }, 'drawio');\n}\n\nasync function updateContent(pngData) {\n    const loadingImage = window.baseUrl('/loading.gif');\n\n    const handleUploadError = error => {\n        if (error.status === 413) {\n            window.$events.emit('error', options.translations.serverUploadLimitText);\n        } else {\n            window.$events.emit('error', options.translations.imageUploadErrorText);\n        }\n        console.error(error);\n    };\n\n    // Handle updating an existing image\n    if (currentNode) {\n        DrawIO.close();\n        const imgElem = currentNode.querySelector('img');\n        try {\n            const img = await DrawIO.upload(pngData, options.pageId);\n            pageEditor.undoManager.transact(() => {\n                pageEditor.dom.setAttrib(imgElem, 'src', img.url);\n                pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', img.id);\n            });\n        } catch (err) {\n            handleUploadError(err);\n            throw new Error(`Failed to save image with error: ${err}`, {cause: err});\n        }\n        return;\n    }\n\n    await wait(5);\n\n    const id = `drawing-${Math.random().toString(16).slice(2)}`;\n    const wrapId = `drawing-wrap-${Math.random().toString(16).slice(2)}`;\n    pageEditor.insertContent(`<div drawio-diagram contenteditable=\"false\" id=\"${wrapId}\"><img src=\"${loadingImage}\" id=\"${id}\"></div>`);\n    DrawIO.close();\n\n    try {\n        const img = await DrawIO.upload(pngData, options.pageId);\n        pageEditor.undoManager.transact(() => {\n            pageEditor.dom.setAttrib(id, 'src', img.url);\n            pageEditor.dom.setAttrib(wrapId, 'drawio-diagram', img.id);\n        });\n    } catch (err) {\n        pageEditor.dom.remove(wrapId);\n        handleUploadError(err);\n        throw new Error(`Failed to save image with error: ${err}`, {cause: err});\n    }\n}\n\nfunction drawingInit() {\n    if (!currentNode) {\n        return Promise.resolve('');\n    }\n\n    const drawingId = currentNode.getAttribute('drawio-diagram');\n    return DrawIO.load(drawingId);\n}\n\nfunction showDrawingEditor(mceEditor, selectedNode = null) {\n    pageEditor = mceEditor;\n    currentNode = selectedNode;\n    DrawIO.show(options.drawioUrl, drawingInit, updateContent);\n}\n\n/**\n * @param {Editor} editor\n */\nfunction register(editor) {\n    editor.addCommand('drawio', () => {\n        const selectedNode = editor.selection.getNode();\n        showDrawingEditor(editor, isDrawing(selectedNode) ? selectedNode : null);\n    });\n\n    editor.ui.registry.addIcon('diagram', `<svg width=\"24\" height=\"24\" fill=\"${options.darkMode ? '#BBB' : '#000000'}\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M20.716 7.639V2.845h-4.794v1.598h-7.99V2.845H3.138v4.794h1.598v7.99H3.138v4.794h4.794v-1.598h7.99v1.598h4.794v-4.794h-1.598v-7.99zM4.736 4.443h1.598V6.04H4.736zm1.598 14.382H4.736v-1.598h1.598zm9.588-1.598h-7.99v-1.598H6.334v-7.99h1.598V6.04h7.99v1.598h1.598v7.99h-1.598zm3.196 1.598H17.52v-1.598h1.598zM17.52 6.04V4.443h1.598V6.04zm-4.21 7.19h-2.79l-.582 1.599H8.643l2.717-7.191h1.119l2.724 7.19h-1.302zm-2.43-1.006h2.086l-1.039-3.06z\"/></svg>`);\n\n    editor.ui.registry.addSplitButton('drawio', {\n        tooltip: 'Insert/edit drawing',\n        icon: 'diagram',\n        onAction() {\n            editor.execCommand('drawio');\n            // Hack to de-focus the tinymce editor toolbar\n            window.document.body.dispatchEvent(new Event('mousedown', {bubbles: true}));\n        },\n        fetch(callback) {\n            callback([\n                {\n                    type: 'choiceitem',\n                    text: 'Drawing manager',\n                    value: 'drawing-manager',\n                },\n            ]);\n        },\n        onItemAction(api, value) {\n            if (value === 'drawing-manager') {\n                const selectedNode = editor.selection.getNode();\n                showDrawingManager(editor, isDrawing(selectedNode) ? selectedNode : null);\n            }\n        },\n    });\n\n    editor.on('dblclick', () => {\n        const selectedNode = editor.selection.getNode();\n        if (!isDrawing(selectedNode)) return;\n        showDrawingEditor(editor, selectedNode);\n    });\n\n    editor.on('SetContent', () => {\n        const drawings = editor.dom.select('body > div[drawio-diagram]');\n        if (!drawings.length) return;\n\n        editor.undoManager.transact(() => {\n            for (const drawing of drawings) {\n                drawing.setAttribute('contenteditable', 'false');\n            }\n        });\n    });\n}\n\n/**\n *\n * @param {WysiwygConfigOptions} providedOptions\n * @return {function(Editor, string)}\n */\nexport function getPlugin(providedOptions) {\n    options = providedOptions;\n    return register;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg-tinymce/plugins-about.js",
    "content": "/**\n * @param {Editor} editor\n */\nfunction register(editor) {\n    const aboutDialog = {\n        title: 'About the WYSIWYG Editor',\n        url: window.baseUrl('/help/tinymce'),\n    };\n\n    editor.ui.registry.addButton('about', {\n        icon: 'help',\n        tooltip: 'About the editor',\n        onAction() {\n            window.tinymce.activeEditor.windowManager.openUrl(aboutDialog);\n        },\n    });\n}\n\n/**\n * @return {register}\n */\nexport function getPlugin() {\n    return register;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg-tinymce/plugins-customhr.js",
    "content": "/**\n * @param {Editor} editor\n */\nfunction register(editor) {\n    editor.addCommand('InsertHorizontalRule', () => {\n        const hrElem = document.createElement('hr');\n        const cNode = editor.selection.getNode();\n        const {parentNode} = cNode;\n        parentNode.insertBefore(hrElem, cNode);\n    });\n\n    editor.ui.registry.addButton('customhr', {\n        icon: 'horizontal-rule',\n        tooltip: 'Insert horizontal line',\n        onAction() {\n            editor.execCommand('InsertHorizontalRule');\n        },\n    });\n}\n\n/**\n * @return {register}\n */\nexport function getPlugin() {\n    return register;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg-tinymce/plugins-details.js",
    "content": "import {blockElementTypes} from './util';\n\n/**\n * @param {Editor} editor\n */\nfunction getSelectedDetailsBlock(editor) {\n    return editor.selection.getNode().closest('details');\n}\n\nfunction setSummary(editor, summaryContent) {\n    const details = getSelectedDetailsBlock(editor);\n    if (!details) return;\n\n    editor.undoManager.transact(() => {\n        let summary = details.querySelector('summary');\n        if (!summary) {\n            summary = document.createElement('summary');\n            details.prepend(summary);\n        }\n        summary.textContent = summaryContent;\n    });\n}\n\n/**\n * @param {Editor} editor\n */\nfunction detailsDialog(editor) {\n    return {\n        title: 'Edit collapsible block',\n        body: {\n            type: 'panel',\n            items: [\n                {\n                    type: 'input',\n                    name: 'summary',\n                    label: 'Toggle label',\n                },\n            ],\n        },\n        buttons: [\n            {\n                type: 'cancel',\n                text: 'Cancel',\n            },\n            {\n                type: 'submit',\n                text: 'Save',\n                primary: true,\n            },\n        ],\n        onSubmit(api) {\n            const {summary} = api.getData();\n            setSummary(editor, summary);\n            api.close();\n        },\n    };\n}\n\n/**\n * @param {Element} element\n */\nfunction getSummaryTextFromDetails(element) {\n    const summary = element.querySelector('summary');\n    if (!summary) {\n        return '';\n    }\n    return summary.textContent;\n}\n\n/**\n * @param {Editor} editor\n */\nfunction showDetailLabelEditWindow(editor) {\n    const details = getSelectedDetailsBlock(editor);\n    const dialog = editor.windowManager.open(detailsDialog(editor));\n    dialog.setData({summary: getSummaryTextFromDetails(details)});\n}\n\n/**\n * @param {Editor} editor\n */\nfunction unwrapDetailsInSelection(editor) {\n    const details = editor.selection.getNode().closest('details');\n    const selectionBm = editor.selection.getBookmark();\n\n    if (details) {\n        const elements = details.querySelectorAll('details > *:not(summary, doc-root), doc-root > *');\n\n        editor.undoManager.transact(() => {\n            for (const element of elements) {\n                details.parentNode.insertBefore(element, details);\n            }\n            details.remove();\n        });\n    }\n\n    editor.focus();\n    editor.selection.moveToBookmark(selectionBm);\n}\n\n/**\n * @param {tinymce.html.Node} detailsEl\n */\nfunction unwrapDetailsEditable(detailsEl) {\n    detailsEl.attr('contenteditable', null);\n    let madeUnwrap = false;\n    for (const child of detailsEl.children()) {\n        if (child.name === 'doc-root') {\n            child.unwrap();\n            madeUnwrap = true;\n        }\n    }\n\n    if (madeUnwrap) {\n        unwrapDetailsEditable(detailsEl);\n    }\n}\n\n/**\n * @param {tinymce.html.Node} detailsEl\n */\nfunction ensureDetailsWrappedInEditable(detailsEl) {\n    unwrapDetailsEditable(detailsEl);\n\n    detailsEl.attr('contenteditable', 'false');\n    const rootWrap = window.tinymce.html.Node.create('doc-root', {contenteditable: 'true'});\n    let previousBlockWrap = null;\n\n    for (const child of detailsEl.children()) {\n        if (child.name === 'summary') continue;\n        const isBlock = blockElementTypes.includes(child.name);\n\n        if (!isBlock) {\n            if (!previousBlockWrap) {\n                previousBlockWrap = window.tinymce.html.Node.create('p');\n                rootWrap.append(previousBlockWrap);\n            }\n            previousBlockWrap.append(child);\n        } else {\n            rootWrap.append(child);\n            previousBlockWrap = null;\n        }\n    }\n\n    detailsEl.append(rootWrap);\n}\n\n/**\n * @param {Editor} editor\n */\nfunction setupElementFilters(editor) {\n    editor.parser.addNodeFilter('details', elms => {\n        for (const el of elms) {\n            ensureDetailsWrappedInEditable(el);\n        }\n    });\n\n    editor.serializer.addNodeFilter('details', elms => {\n        for (const el of elms) {\n            unwrapDetailsEditable(el);\n            el.attr('open', null);\n        }\n    });\n\n    editor.serializer.addNodeFilter('doc-root', elms => {\n        for (const el of elms) {\n            el.unwrap();\n        }\n    });\n}\n\n/**\n * @param {Editor} editor\n */\nfunction register(editor) {\n    editor.ui.registry.addIcon('details', '<svg width=\"24\" height=\"24\"><path d=\"M8.2 9a.5.5 0 0 0-.4.8l4 5.6a.5.5 0 0 0 .8 0l4-5.6a.5.5 0 0 0-.4-.8ZM20.122 18.151h-16c-.964 0-.934 2.7 0 2.7h16c1.139 0 1.173-2.7 0-2.7zM20.122 3.042h-16c-.964 0-.934 2.7 0 2.7h16c1.139 0 1.173-2.7 0-2.7z\"/></svg>');\n    editor.ui.registry.addIcon('togglefold', '<svg height=\"24\"  width=\"24\"><path d=\"M8.12 19.3c.39.39 1.02.39 1.41 0L12 16.83l2.47 2.47c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41l-3.17-3.17c-.39-.39-1.02-.39-1.41 0l-3.17 3.17c-.4.38-.4 1.02-.01 1.41zm7.76-14.6c-.39-.39-1.02-.39-1.41 0L12 7.17 9.53 4.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.03 0 1.42l3.17 3.17c.39.39 1.02.39 1.41 0l3.17-3.17c.4-.39.4-1.03.01-1.42z\"/></svg>');\n    editor.ui.registry.addIcon('togglelabel', '<svg height=\"18\" width=\"18\" viewBox=\"0 0 24 24\"><path d=\"M21.41,11.41l-8.83-8.83C12.21,2.21,11.7,2,11.17,2H4C2.9,2,2,2.9,2,4v7.17c0,0.53,0.21,1.04,0.59,1.41l8.83,8.83 c0.78,0.78,2.05,0.78,2.83,0l7.17-7.17C22.2,13.46,22.2,12.2,21.41,11.41z M6.5,8C5.67,8,5,7.33,5,6.5S5.67,5,6.5,5S8,5.67,8,6.5 S7.33,8,6.5,8z\"/></svg>');\n\n    editor.ui.registry.addButton('details', {\n        icon: 'details',\n        tooltip: 'Insert collapsible block',\n        onAction() {\n            editor.execCommand('InsertDetailsBlock');\n        },\n    });\n\n    editor.ui.registry.addButton('removedetails', {\n        icon: 'table-delete-table',\n        tooltip: 'Unwrap',\n        onAction() {\n            unwrapDetailsInSelection(editor);\n        },\n    });\n\n    editor.ui.registry.addButton('editdetials', {\n        icon: 'togglelabel',\n        tooltip: 'Edit label',\n        onAction() {\n            showDetailLabelEditWindow(editor);\n        },\n    });\n\n    editor.on('dblclick', event => {\n        if (!getSelectedDetailsBlock(editor) || event.target.closest('doc-root')) return;\n        showDetailLabelEditWindow(editor);\n    });\n\n    editor.ui.registry.addButton('toggledetails', {\n        icon: 'togglefold',\n        tooltip: 'Toggle open/closed',\n        onAction() {\n            const details = getSelectedDetailsBlock(editor);\n            details.toggleAttribute('open');\n            editor.focus();\n        },\n    });\n\n    editor.addCommand('InsertDetailsBlock', () => {\n        let content = editor.selection.getContent({format: 'html'});\n        const details = document.createElement('details');\n        const summary = document.createElement('summary');\n        const id = `details-${Date.now()}`;\n        details.setAttribute('data-id', id);\n        details.appendChild(summary);\n\n        if (!content) {\n            content = '<p><br></p>';\n        }\n\n        details.innerHTML += content;\n        editor.insertContent(details.outerHTML);\n        editor.focus();\n\n        const domDetails = editor.dom.select(`[data-id=\"${id}\"]`)[0] || null;\n        if (domDetails) {\n            const firstChild = domDetails.querySelector('doc-root > *');\n            if (firstChild) {\n                firstChild.focus();\n            }\n            domDetails.removeAttribute('data-id');\n        }\n    });\n\n    editor.ui.registry.addContextToolbar('details', {\n        predicate(node) {\n            return node.nodeName.toLowerCase() === 'details';\n        },\n        items: 'editdetials toggledetails removedetails',\n        position: 'node',\n        scope: 'node',\n    });\n\n    editor.on('PreInit', () => {\n        setupElementFilters(editor);\n    });\n}\n\n/**\n * @return {register}\n */\nexport function getPlugin() {\n    return register;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg-tinymce/plugins-imagemanager.js",
    "content": "/**\n * @param {Editor} editor\n */\nfunction register(editor) {\n    // Custom Image picker button\n    editor.ui.registry.addButton('imagemanager-insert', {\n        title: 'Insert image',\n        icon: 'image',\n        tooltip: 'Insert image',\n        onAction() {\n            /** @type {ImageManager} * */\n            const imageManager = window.$components.first('image-manager');\n            imageManager.show(image => {\n                const imageUrl = image.thumbs?.display || image.url;\n                let html = `<a href=\"${image.url}\" target=\"_blank\">`;\n                html += `<img src=\"${imageUrl}\" alt=\"${image.name}\">`;\n                html += '</a>';\n                editor.execCommand('mceInsertContent', false, html);\n            }, 'gallery');\n        },\n    });\n}\n\n/**\n * @return {register}\n */\nexport function getPlugin() {\n    return register;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg-tinymce/plugins-stub.js",
    "content": "/**\n * @param {Editor} editor\n * @param {String} url\n */\nfunction register(editor, url) {\n\n}\n\n/**\n * @param {WysiwygConfigOptions} options\n * @return {register}\n */\nexport function getPlugin(options) {\n    return register;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg-tinymce/plugins-table-additions.js",
    "content": "/**\n * @param {Editor} editor\n */\nfunction register(editor) {\n    editor.ui.registry.addIcon('tableclearformatting', '<svg xmlns=\"http://www.w3.org/2000/svg\" xml:space=\"preserve\" viewBox=\"0 0 24 24\"><path d=\"M15.53088 4.64727v-.82364c0-.453-.37063-.82363-.82363-.82363H4.82363C4.37063 3 4 3.37064 4 3.82363v3.29454c0 .453.37064.82364.82363.82364h9.88362c.453 0 .82363-.37064.82363-.82364v-.82363h.82364v3.29454H8.11817v7.4127c0 .453.37064.82364.82364.82364h1.64727c.453 0 .82363-.37064.82363-.82364v-5.76544h6.58907V4.64727Z\"/><path d=\"m18.42672 19.51563-1.54687-1.54688-1.54688 1.54688c-.26751.2675-.70124.2675-.96875 0-.26751-.26752-.26751-.70124 0-.96876L15.9111 17l-1.54688-1.54688c-.26751-.2675-.26751-.70123 0-.96875.26751-.2675.70124-.2675.96875 0l1.54688 1.54688 1.54687-1.54688c.26751-.2675.70124-.2675.96875 0 .26751.26752.26751.70124 0 .96875L17.8486 17l1.54687 1.54688c.26751.2675.26751.70123 0 .96874-.26751.26752-.70124.26752-.96875 0z\"/></svg>');\n\n    const tableFirstRowContextSpec = {\n        items: ' | tablerowheader',\n        predicate(elem) {\n            const isTable = elem.nodeName.toLowerCase() === 'table';\n            const selectionNode = editor.selection.getNode();\n            const parentTable = selectionNode.closest('table');\n            if (!isTable || !parentTable) {\n                return false;\n            }\n\n            const firstRow = parentTable.querySelector('tr');\n            return firstRow.contains(selectionNode);\n        },\n        position: 'node',\n        scope: 'node',\n    };\n    editor.ui.registry.addContextToolbar('customtabletoolbarfirstrow', tableFirstRowContextSpec);\n\n    editor.addCommand('tableclearformatting', () => {\n        const table = editor.dom.getParent(editor.selection.getStart(), 'table');\n        if (!table) {\n            return;\n        }\n\n        const attrsToRemove = ['class', 'style', 'width', 'height'];\n        const styled = [table, ...table.querySelectorAll(attrsToRemove.map(a => `[${a}]`).join(','))];\n        for (const elem of styled) {\n            for (const attr of attrsToRemove) {\n                elem.removeAttribute(attr);\n            }\n        }\n    });\n\n    editor.addCommand('tableclearsizes', () => {\n        const table = editor.dom.getParent(editor.selection.getStart(), 'table');\n        if (!table) {\n            return;\n        }\n\n        const targets = [table, ...table.querySelectorAll('tr,td,th,tbody,thead,tfoot,th>*,td>*')];\n        for (const elem of targets) {\n            elem.removeAttribute('width');\n            elem.removeAttribute('height');\n            elem.style.height = null;\n            elem.style.width = null;\n        }\n    });\n\n    const onPreInit = () => {\n        const exitingButtons = editor.ui.registry.getAll().buttons;\n\n        editor.ui.registry.addMenuButton('customtable', {\n            ...exitingButtons.table,\n            fetch: callback => callback('inserttable | cell row column | advtablesort | tableprops tableclearformatting tableclearsizes deletetable'),\n        });\n\n        editor.ui.registry.addMenuItem('tableclearformatting', {\n            icon: 'tableclearformatting',\n            text: 'Clear table formatting',\n            onSetup: exitingButtons.tableprops.onSetup,\n            onAction() {\n                editor.execCommand('tableclearformatting');\n            },\n        });\n\n        editor.ui.registry.addMenuItem('tableclearsizes', {\n            icon: 'resize',\n            text: 'Resize to contents',\n            onSetup: exitingButtons.tableprops.onSetup,\n            onAction() {\n                editor.execCommand('tableclearsizes');\n            },\n        });\n\n        editor.off('PreInit', onPreInit);\n    };\n\n    editor.on('PreInit', onPreInit);\n}\n\n/**\n * @return {register}\n */\nexport function getPlugin() {\n    return register;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg-tinymce/plugins-tasklist.js",
    "content": "/**\n * @param {Element} element\n * @return {boolean}\n */\nfunction elementWithinTaskList(element) {\n    const listEl = element.closest('li');\n    return listEl && listEl.parentNode.nodeName === 'UL' && listEl.classList.contains('task-list-item');\n}\n\n/**\n * @param {MouseEvent} event\n * @param {Element} clickedEl\n * @param {Editor} editor\n */\nfunction handleTaskListItemClick(event, clickedEl, editor) {\n    const bounds = clickedEl.getBoundingClientRect();\n    const withinBounds = event.clientX <= bounds.right\n        && event.clientX >= bounds.left\n        && event.clientY >= bounds.top\n        && event.clientY <= bounds.bottom;\n\n    // Outside of the task list item bounds mean we're probably clicking the pseudo-element.\n    if (!withinBounds) {\n        editor.undoManager.transact(() => {\n            if (clickedEl.hasAttribute('checked')) {\n                clickedEl.removeAttribute('checked');\n            } else {\n                clickedEl.setAttribute('checked', 'checked');\n            }\n        });\n    }\n}\n\n/**\n * @param {AstNode} node\n */\nfunction parseTaskListNode(node) {\n    // Force task list item class\n    node.attr('class', 'task-list-item');\n\n    // Copy checkbox status and remove checkbox within editor\n    for (const child of node.children()) {\n        if (child.name === 'input') {\n            if (child.attr('checked') === 'checked') {\n                node.attr('checked', 'checked');\n            }\n            child.remove();\n        }\n    }\n}\n\n/**\n * @param {AstNode} node\n */\nfunction serializeTaskListNode(node) {\n    // Get checked status and clean it from list node\n    const isChecked = node.attr('checked') === 'checked';\n    node.attr('checked', null);\n\n    const inputAttrs = {type: 'checkbox', disabled: 'disabled'};\n    if (isChecked) {\n        inputAttrs.checked = 'checked';\n    }\n\n    // Create & insert checkbox input element\n    const checkbox = window.tinymce.html.Node.create('input', inputAttrs);\n    checkbox.shortEnded = true;\n\n    if (node.firstChild) {\n        node.insert(checkbox, node.firstChild, true);\n    } else {\n        node.append(checkbox);\n    }\n}\n\n/**\n * @param {Editor} editor\n */\nfunction register(editor) {\n    // Tasklist UI buttons\n    editor.ui.registry.addIcon('tasklist', '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M22,8c0-0.55-0.45-1-1-1h-7c-0.55,0-1,0.45-1,1s0.45,1,1,1h7C21.55,9,22,8.55,22,8z M13,16c0,0.55,0.45,1,1,1h7 c0.55,0,1-0.45,1-1c0-0.55-0.45-1-1-1h-7C13.45,15,13,15.45,13,16z M10.47,4.63c0.39,0.39,0.39,1.02,0,1.41l-4.23,4.25 c-0.39,0.39-1.02,0.39-1.42,0L2.7,8.16c-0.39-0.39-0.39-1.02,0-1.41c0.39-0.39,1.02-0.39,1.41,0l1.42,1.42l3.54-3.54 C9.45,4.25,10.09,4.25,10.47,4.63z M10.48,12.64c0.39,0.39,0.39,1.02,0,1.41l-4.23,4.25c-0.39,0.39-1.02,0.39-1.42,0L2.7,16.16 c-0.39-0.39-0.39-1.02,0-1.41s1.02-0.39,1.41,0l1.42,1.42l3.54-3.54C9.45,12.25,10.09,12.25,10.48,12.64L10.48,12.64z\"/></svg>');\n    editor.ui.registry.addToggleButton('tasklist', {\n        tooltip: 'Task list',\n        icon: 'tasklist',\n        active: false,\n        onAction(api) {\n            if (api.isActive()) {\n                editor.execCommand('RemoveList');\n            } else {\n                editor.execCommand('InsertUnorderedList', null, {\n                    'list-item-attributes': {\n                        class: 'task-list-item',\n                    },\n                    'list-style-type': 'tasklist',\n                });\n            }\n        },\n        onSetup(api) {\n            editor.on('NodeChange', event => {\n                const parentListEl = event.parents.find(el => el.nodeName === 'LI');\n                const inList = parentListEl && parentListEl.classList.contains('task-list-item');\n                api.setActive(Boolean(inList));\n            });\n        },\n    });\n\n    // Tweak existing bullet list button active state to not be active\n    // when we're in a task list.\n    const existingBullListButton = editor.ui.registry.getAll().buttons.bullist;\n    existingBullListButton.onSetup = function customBullListOnSetup(api) {\n        editor.on('NodeChange', event => {\n            const parentList = event.parents.find(el => el.nodeName === 'LI');\n            const inTaskList = parentList && parentList.classList.contains('task-list-item');\n            const inUlList = parentList && parentList.parentNode.nodeName === 'UL';\n            api.setActive(Boolean(inUlList && !inTaskList));\n        });\n    };\n    existingBullListButton.onAction = function customBullListOnAction() {\n        // Cheeky hack to prevent list toggle action treating tasklists as normal\n        // unordered lists which would unwrap the list on toggle from tasklist to bullet list.\n        // Instead we quickly jump through an ordered list first if we're within a tasklist.\n        if (elementWithinTaskList(editor.selection.getNode())) {\n            editor.execCommand('InsertOrderedList', null, {\n                'list-item-attributes': {class: null},\n            });\n        }\n\n        editor.execCommand('InsertUnorderedList', null, {\n            'list-item-attributes': {class: null},\n        });\n    };\n    // Tweak existing number list to not allow classes on child items\n    const existingNumListButton = editor.ui.registry.getAll().buttons.numlist;\n    existingNumListButton.onAction = function customNumListButtonOnAction() {\n        editor.execCommand('InsertOrderedList', null, {\n            'list-item-attributes': {class: null},\n        });\n    };\n\n    // Setup filters on pre-init\n    editor.on('PreInit', () => {\n        editor.parser.addNodeFilter('li', nodes => {\n            for (const node of nodes) {\n                if (node.attributes.map.class === 'task-list-item') {\n                    parseTaskListNode(node);\n                }\n            }\n        });\n        editor.serializer.addNodeFilter('li', nodes => {\n            for (const node of nodes) {\n                if (node.attributes.map.class === 'task-list-item') {\n                    serializeTaskListNode(node);\n                }\n            }\n        });\n    });\n\n    // Handle checkbox click in editor\n    editor.on('click', event => {\n        const clickedEl = event.target;\n        if (clickedEl.nodeName === 'LI' && clickedEl.classList.contains('task-list-item')) {\n            handleTaskListItemClick(event, clickedEl, editor);\n            event.preventDefault();\n        }\n    });\n}\n\n/**\n * @return {register}\n */\nexport function getPlugin() {\n    return register;\n}\n"
  },
  {
    "path": "resources/js/wysiwyg-tinymce/scrolling.js",
    "content": "/**\n * @param {Editor} editor\n * @param {String} scrollId\n */\nfunction scrollToText(editor, scrollId) {\n    const element = editor.dom.get(encodeURIComponent(scrollId).replace(/!/g, '%21'));\n    if (!element) {\n        return;\n    }\n\n    // scroll the element into the view and put the cursor at the end.\n    element.scrollIntoView();\n    editor.selection.select(element, true);\n    editor.selection.collapse(false);\n    editor.focus();\n}\n\n/**\n * Scroll to a section dictated by the current URL query string, if present.\n * Used when directly editing a specific section of the page.\n * @param {Editor} editor\n */\nexport function scrollToQueryString(editor) {\n    const queryParams = (new URL(window.location)).searchParams;\n    const scrollId = queryParams.get('content-id');\n    if (scrollId) {\n        scrollToText(editor, scrollId);\n    }\n}\n"
  },
  {
    "path": "resources/js/wysiwyg-tinymce/shortcuts.js",
    "content": "/**\n * @param {Editor} editor\n */\nexport function register(editor) {\n    // Headers\n    for (let i = 1; i < 5; i++) {\n        editor.shortcuts.add(`meta+${i}`, '', ['FormatBlock', false, `h${i + 1}`]);\n    }\n\n    // Other block shortcuts\n    editor.shortcuts.add('meta+5', '', ['FormatBlock', false, 'p']);\n    editor.shortcuts.add('meta+d', '', ['FormatBlock', false, 'p']);\n    editor.shortcuts.add('meta+6', '', ['FormatBlock', false, 'blockquote']);\n    editor.shortcuts.add('meta+q', '', ['FormatBlock', false, 'blockquote']);\n    editor.shortcuts.add('meta+7', '', ['codeeditor', false, 'pre']);\n    editor.shortcuts.add('meta+e', '', ['codeeditor', false, 'pre']);\n    editor.shortcuts.add('meta+8', '', ['FormatBlock', false, 'code']);\n    editor.shortcuts.add('meta+shift+E', '', ['FormatBlock', false, 'code']);\n    editor.shortcuts.add('meta+o', '', 'InsertOrderedList');\n    editor.shortcuts.add('meta+p', '', 'InsertUnorderedList');\n\n    // Save draft shortcut\n    editor.shortcuts.add('meta+S', '', () => {\n        window.$events.emit('editor-save-draft');\n    });\n\n    // Save page shortcut\n    editor.shortcuts.add('meta+13', '', () => {\n        window.$events.emit('editor-save-page');\n    });\n\n    // Loop through callout styles\n    editor.shortcuts.add('meta+9', '', () => {\n        const selectedNode = editor.selection.getNode();\n        const callout = selectedNode ? selectedNode.closest('.callout') : null;\n\n        const formats = ['info', 'success', 'warning', 'danger'];\n        const currentFormatIndex = formats.findIndex(format => {\n            return callout && callout.classList.contains(format);\n        });\n        const newFormatIndex = (currentFormatIndex + 1) % formats.length;\n        const newFormat = formats[newFormatIndex];\n\n        editor.formatter.apply(`callout${newFormat}`);\n    });\n\n    // Link selector shortcut\n    editor.shortcuts.add('meta+shift+K', '', () => {\n        /** @var {EntitySelectorPopup} * */\n        const selectorPopup = window.$components.first('entity-selector-popup');\n        const selectionText = editor.selection.getContent({format: 'text'}).trim();\n        selectorPopup.show(entity => {\n            if (editor.selection.isCollapsed()) {\n                editor.insertContent(editor.dom.createHTML('a', {href: entity.link}, editor.dom.encode(entity.name)));\n            } else {\n                editor.formatter.apply('link', {href: entity.link});\n            }\n\n            editor.selection.collapse(false);\n            editor.focus();\n        }, {\n            initialValue: selectionText,\n            searchEndpoint: '/search/entity-selector',\n            entityTypes: 'page,book,chapter,bookshelf',\n            entityPermission: 'view',\n        });\n    });\n}\n"
  },
  {
    "path": "resources/js/wysiwyg-tinymce/toolbars.js",
    "content": "/**\n * @param {WysiwygConfigOptions} options\n * @return {String}\n */\nexport function getPrimaryToolbar(options) {\n    const textDirPlugins = options.textDirection === 'rtl' ? 'ltr rtl' : '';\n\n    const toolbar = [\n        'undo redo',\n        'styles',\n        'bold italic underline forecolor backcolor formatoverflow',\n        'alignleft aligncenter alignright alignjustify',\n        'bullist numlist listoverflow',\n        textDirPlugins,\n        'link customtable imagemanager-insert insertoverflow',\n        'code about fullscreen',\n    ];\n\n    return toolbar.filter(row => Boolean(row)).join(' | ');\n}\n\n/**\n * @param {Editor} editor\n */\nfunction registerPrimaryToolbarGroups(editor) {\n    editor.ui.registry.addGroupToolbarButton('formatoverflow', {\n        icon: 'more-drawer',\n        tooltip: 'More',\n        items: 'strikethrough superscript subscript inlinecode removeformat',\n    });\n    editor.ui.registry.addGroupToolbarButton('listoverflow', {\n        icon: 'more-drawer',\n        tooltip: 'More',\n        items: 'tasklist outdent indent',\n    });\n    editor.ui.registry.addGroupToolbarButton('insertoverflow', {\n        icon: 'more-drawer',\n        tooltip: 'More',\n        items: 'customhr codeeditor drawio media details',\n    });\n}\n\n/**\n * @param {Editor} editor\n */\nfunction registerLinkContextToolbar(editor) {\n    editor.ui.registry.addContextToolbar('linkcontexttoolbar', {\n        predicate(node) {\n            return node.closest('a') !== null;\n        },\n        position: 'node',\n        scope: 'node',\n        items: 'link unlink openlink',\n    });\n}\n\n/**\n * @param {Editor} editor\n */\nfunction registerImageContextToolbar(editor) {\n    editor.ui.registry.addContextToolbar('imagecontexttoolbar', {\n        predicate(node) {\n            return node.closest('img') !== null && !node.hasAttribute('data-mce-object');\n        },\n        position: 'node',\n        scope: 'node',\n        items: 'image',\n    });\n}\n\n/**\n * @param {Editor} editor\n */\nfunction registerObjectContextToolbar(editor) {\n    editor.ui.registry.addContextToolbar('objectcontexttoolbar', {\n        predicate(node) {\n            return node.closest('img') !== null && node.hasAttribute('data-mce-object');\n        },\n        position: 'node',\n        scope: 'node',\n        items: 'media',\n    });\n}\n\n/**\n * @param {Editor} editor\n */\nexport function registerAdditionalToolbars(editor) {\n    registerPrimaryToolbarGroups(editor);\n    registerLinkContextToolbar(editor);\n    registerImageContextToolbar(editor);\n    registerObjectContextToolbar(editor);\n}\n"
  },
  {
    "path": "resources/js/wysiwyg-tinymce/util.js",
    "content": "export const blockElementTypes = [\n    'p',\n    'h1',\n    'h2',\n    'h3',\n    'h4',\n    'h5',\n    'h6',\n    'div',\n    'blockquote',\n    'pre',\n    'code-block',\n    'details',\n    'ul',\n    'ol',\n    'table',\n    'hr',\n];\n"
  },
  {
    "path": "resources/sass/_animations.scss",
    "content": "\n.anim.fadeIn {\n  opacity: 0;\n  animation-name: fadeIn;\n  animation-duration: 120ms;\n  animation-timing-function: ease-in-out;\n  animation-fill-mode: forwards;\n}\n\n@keyframes fadeIn {\n  0% {\n    opacity: 0;\n  }\n  100% {\n    opacity: 1;\n  }\n}\n\n.search-suggestions-animation{\n  animation-name: searchSuggestions;\n  animation-duration: 120ms;\n  animation-fill-mode: forwards;\n  animation-timing-function: cubic-bezier(.62, .28, .23, .99);\n}\n\n@keyframes searchSuggestions {\n  0% {\n    opacity: .5;\n    transform: scale(0.9);\n  }\n  100% {\n    opacity: 1;\n    transform: scale(1);\n  }\n}\n\n@keyframes loadingBob {\n  0% {\n    transform: translate3d(0, 0, 0);\n  }\n  30% {\n    transform: translate3d(0, 0, 0);\n  }\n  50% {\n    transform: translate3d(0, -10px, 0);\n  }\n  70% {\n    transform: translate3d(0, 0, 0);\n  }\n  100% {\n    transform: translate3d(0, 0, 0);\n  }\n}\n\n@keyframes pointer {\n  0% {\n      transform: translate3d(0, 20px, 0) scale3d(0, 0, 0);\n  }\n  100% {\n      transform: translate3d(0, 0, 0) scale3d(1, 1, 1);\n  }\n}\n\n.anim.pointer {\n  transform-origin: 50% 100%;\n  animation-name: pointer;\n  animation-duration: 180ms;\n  animation-delay: 0s;\n  animation-timing-function: cubic-bezier(.62, .28, .23, .99);\n}\n\n@keyframes highlight {\n  0% {\n    background-color: var(--color-primary-light);\n  }\n  33% {\n    background-color: transparent;\n  }\n  66% {\n    background-color: var(--color-primary-light);\n  }\n  100% {\n    background-color: transparent;\n  }\n}\n\n.anim-highlight {\n  animation-name: highlight;\n  animation-duration: 2s;\n  animation-delay: 0s;\n  animation-timing-function: linear;\n}"
  },
  {
    "path": "resources/sass/_blocks.scss",
    "content": "@use \"mixins\";\n@use \"vars\";\n\n/**\n * Card-style blocks\n */\n\n.card {\n  @include mixins.lightDark(background-color, #FFF, #222);\n  box-shadow: vars.$bs-card;\n  border-radius: 3px;\n  break-inside: avoid;\n  .body, p.empty-text {\n    padding-block: vars.$m;\n  }\n  a, p {\n    word-wrap: break-word;\n    word-break: break-word;\n  }\n}\n\n.card-title {\n  padding: vars.$m vars.$m vars.$xs;\n  margin: 0;\n  font-size: vars.$fs-m;\n  color: #222;\n  fill: #222;\n  font-weight: 400;\n}\n.card-title a {\n  line-height: 1;\n}\n.card-footer-link, button.card-footer-link  {\n  display: block;\n  padding: vars.$s vars.$m;\n  line-height: 1;\n  border-top: 1px solid;\n  width: 100%;\n  text-align: left;\n  @include mixins.lightDark(border-color, #DDD, #555);\n  border-radius: 0 0 3px 3px;\n  font-size: 0.9em;\n  margin-top: vars.$xs;\n  &:hover {\n    text-decoration: none;\n    @include mixins.lightDark(background-color, #f2f2f2, #2d2d2d);\n  }\n  &:focus {\n    @include mixins.lightDark(background-color, #eee, #222);\n    outline: 1px dotted #666;\n    outline-offset: -2px;\n  }\n}\n\n.card.border-card {\n  border: 1px solid;\n  @include mixins.lightDark(border-color, #ddd, #000);\n}\n\n.card.drag-card {\n  border: 1px solid #DDD;\n  @include mixins.lightDark(border-color, #ddd, #000);\n  @include mixins.lightDark(background-color, #fff, #333);\n  border-radius: 4px;\n  display: flex;\n  padding: 0 0 0 (vars.$s + 28px);\n  margin: vars.$s 0;\n  position: relative;\n  .drag-card-action {\n    cursor: pointer;\n  }\n  .handle, .drag-card-action {\n    display: flex;\n    align-items: center;\n    text-align: center;\n    justify-content: center;\n    width: 28px;\n    flex-grow: 0;\n    padding: 0 vars.$xs;\n    &:hover {\n      @include mixins.lightDark(background-color, #eee, #2d2d2d);\n    }\n    .svg-icon {\n      margin-inline-end: 0px;\n    }\n  }\n  .outline input {\n    margin: vars.$s 0;\n    width: 100%;\n  }\n  .outline {\n    position: relative;\n  }\n  .handle {\n    @include mixins.lightDark(background-color, #eee, #2d2d2d);\n    left: 0;\n    position: absolute;\n    top: 0;\n    bottom: 0;\n  }\n  > div {\n    padding: 0 vars.$s;\n    max-width: 80%;\n    flex: 1;\n  }\n}\n\n.grid-card {\n  display: flex;\n  flex-direction: column;\n  border: 1px solid #ddd;\n  @include mixins.lightDark(border-color, #ddd, #000);\n  margin-bottom: vars.$l;\n  border-radius: 4px;\n  overflow: hidden;\n  min-width: 100px;\n  color: vars.$text-dark;\n  transition: border-color ease-in-out 120ms, box-shadow ease-in-out 120ms;\n  &:hover {\n    color: vars.$text-dark;\n    text-decoration: none;\n    @include mixins.lightDark(box-shadow, vars.$bs-card, vars.$bs-card-dark);\n  }\n  h2 {\n    width: 100%;\n    font-size: 1.5em;\n    margin: 0 0 10px;\n  }\n  p {\n    font-size: .7rem;\n    margin: 0;\n    line-height: 1.6em;\n  }\n  .grid-card-content {\n    flex: 1;\n    border-top: 0;\n    border-bottom-width: 2px;\n  }\n  .grid-card-content, .grid-card-footer {\n    padding: vars.$l;\n  }\n  .grid-card-content + .grid-card-footer {\n    padding-top: 0;\n  }\n}\n\n.book-grid-item .grid-card-footer {\n  p.small {\n    font-size: .8em;\n    margin: 0;\n  }\n}\n\n.content-wrap.card {\n  padding: vars.$m vars.$xxl;\n  margin-inline-start: auto;\n  margin-inline-end: auto;\n  margin-bottom: vars.$l;\n  overflow: initial;\n  min-height: 60vh;\n  border-radius: 8px;\n  &.auto-height {\n    min-height: 0;\n  }\n  &.fill-width {\n    width: 100%;\n  }\n}\n@include mixins.smaller-than(vars.$bp-xxl) {\n  .content-wrap.card {\n    padding: vars.$m vars.$xl;\n  }\n}\n@include mixins.smaller-than(vars.$bp-m) {\n  .content-wrap.card {\n    padding: vars.$m vars.$l;\n  }\n}\n@include mixins.smaller-than(vars.$bp-s) {\n  .content-wrap.card {\n    padding: vars.$m vars.$m;\n  }\n}\n\n.sub-card {\n  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);\n  border: 1.5px solid;\n  @include mixins.lightDark(border-color, #E2E2E2, #444);\n  border-radius: 4px;\n}\n\n.outline-hover {\n  border: 1px solid transparent !important;\n  &:hover {\n    border: 1px solid rgba(0, 0, 0, 0.1) !important;\n  }\n}\n\n.fade-in-when-active {\n  @include mixins.lightDark(opacity, 0.6, 0.7);\n  transition: opacity ease-in-out 120ms;\n  &:hover, &:focus-within {\n    opacity: 1 !important;\n  }\n  @media (prefers-contrast: more) {\n    opacity: 1 !important;\n  }\n}\n\n/**\n * Tags\n */\n.tag-item {\n  display: inline-flex;\n  margin-bottom: vars.$xs;\n  margin-inline-end: vars.$xs;\n  border-radius: 4px;\n  border: 1px solid;\n  overflow: hidden;\n  font-size: 0.85em;\n  @include mixins.lightDark(border-color, #CCC, #666);\n  a, span, a:hover, a:active {\n    padding: 4px 8px;\n    @include mixins.lightDark(color, rgba(0, 0, 0, 0.7), rgba(255, 255, 255, 0.8));\n    transition: background-color ease-in-out 80ms;\n    text-decoration: none;\n  }\n  a:hover {\n    @include mixins.lightDark(background-color, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.3));\n  }\n  svg {\n    @include mixins.lightDark(fill, rgba(0, 0, 0, 0.5), rgba(255, 255, 255, 0.5));\n  }\n  .tag-value {\n    border-inline-start: 1px solid;\n    @include mixins.lightDark(border-color, #DDD, #666);\n    @include mixins.lightDark(background-color, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.2))\n  }\n}\n\n.tag-name.highlight, .tag-value.highlight {\n  font-weight: bold;\n}\n\n.tag-list div:last-child .tag-item {\n  margin-bottom: 0;\n}\n\n.item-list-row .tag-item {\n  margin-bottom: 0;\n}\n\n/**\n * API Docs\n */\n.api-method {\n  font-size: 0.75rem;\n  background-color: #888;\n  padding: vars.$xs;\n  line-height: 1.3;\n  opacity: 0.7;\n  vertical-align: top;\n  border-radius: 3px;\n  color: #FFF;\n  display: inline-block;\n  min-width: 60px;\n  text-align: center;\n  font-weight: bold;\n  &[data-method=\"GET\"] { background-color: #077b70 }\n  &[data-method=\"POST\"] { background-color: #cf4d03 }\n  &[data-method=\"PUT\"] { background-color: #0288D1 }\n  &[data-method=\"DELETE\"] { background-color: #ab0f0e }\n}\n\n.sticky-sidebar {\n  position: sticky;\n  top: 0;\n  padding-left: 2px;\n  max-height: calc(100vh);\n  overflow-y: auto;\n  .sticky-sidebar-header {\n    position: sticky;\n    top: 0;\n    background: #F2F2F2;\n    background: linear-gradient(180deg,rgba(242, 242, 242, 1) 66%, rgba(242, 242, 242, 0) 100%);\n    z-index: 4;\n  }\n}\n.dark-mode .sticky-sidebar-header {\n  background: #111;\n  background: linear-gradient(180deg,rgba(17, 17, 17, 1) 66%, rgba(17, 17, 17, 0) 100%);\n}\n"
  },
  {
    "path": "resources/sass/_buttons.scss",
    "content": "@use \"mixins\";\n@use \"vars\";\n\nbutton {\n  background-color: transparent;\n  border: 0;\n  font-size: 100%;\n}\n\n.button  {\n  text-decoration: none;\n  font-size: 0.85rem;\n  line-height: 1.4em;\n  padding: vars.$xs*1.3 vars.$m;\n  margin-top: vars.$xs;\n  margin-bottom: vars.$xs;\n  display: inline-block;\n  font-weight: 400;\n  outline: 0;\n  border-radius: 4px;\n  cursor: pointer;\n  transition: background-color ease-in-out 120ms,\n    filter ease-in-out 120ms,\n    box-shadow ease-in-out 120ms;\n  box-shadow: none;\n  background-color: var(--color-primary);\n  color: #FFF;\n  border: 1px solid var(--color-primary);\n  vertical-align: top;\n  &:hover, &:focus, &:active {\n    background-color: var(--color-primary);\n    text-decoration: none;\n    color: #FFFFFF;\n  }\n  &:hover {\n    @include mixins.lightDark(box-shadow, vars.$bs-light, vars.$bs-dark);\n    filter: brightness(110%);\n  }\n  &:focus {\n    outline: 1px dotted currentColor;\n    outline-offset: -(vars.$xs);\n    box-shadow: none;\n    filter: brightness(90%);\n  }\n  &:active {\n    outline: 0;\n  }\n}\n\n.button.outline {\n  background-color: transparent;\n  @include mixins.lightDark(color, #666, #AAA);\n  fill: currentColor;\n  border: 1px solid;\n  @include mixins.lightDark(border-color, #CCC, #666);\n  &:hover, &:focus, &:active {\n    @include mixins.lightDark(color, #444, #BBB);\n    border: 1px solid #CCC;\n    box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1);\n    background-color: #F2F2F2;\n    @include mixins.lightDark(background-color, #f8f8f8, #444);\n    filter: none;\n  }\n  &:active {\n    border-color: #BBB;\n    background-color: #DDD;\n    color: #666;\n    box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.1);\n  }\n}\n\n.button + .button {\n  margin-inline-start: vars.$s;\n}\n\n.button.small {\n  font-size: 0.75rem;\n  padding: vars.$xs*1.2 vars.$s;\n}\n\n.text-button {\n  cursor: pointer;\n  background-color: transparent;\n  padding: 0;\n  margin: 0;\n  border: none;\n  user-select: none;\n  font-size: 0.75rem;\n  line-height: 1.4em;\n  color: var(--color-link);\n  &:active {\n    outline: 0;\n  }\n  &:hover {\n    text-decoration: none;\n  }\n  &:hover, &:focus {\n    color: var(--color-link);\n    fill: var(--color-link);\n  }\n}\n.text-button.hover-underline:hover {\n  text-decoration: underline;\n}\n\n.button.block {\n  width: 100%;\n  text-align: start;\n  display: block;\n}\n\n.button.icon, .icon-button, .text-button.icon {\n  .svg-icon {\n    margin-inline-end: 0;\n  }\n}\n\n.icon-button {\n  text-align: center;\n  border: 1px solid transparent;\n}\n.icon-button:hover {\n  background-color: rgba(0, 0, 0, 0.05);\n  border-radius: 4px;\n  @include mixins.lightDark(border-color, #DDD, #444);\n  cursor: pointer;\n}\n\n.button.svg {\n  display: flex;\n  align-items: center;\n  padding: vars.$s vars.$m;\n  padding-bottom: (vars.$s - 2px);\n  width: 100%;\n  svg {\n    display: inline-block;\n    width: 24px;\n    height: 24px;\n    bottom: auto;\n    margin-inline-end: vars.$m;\n  }\n}\n\n.button[disabled] {\n  background-color: #BBB;\n  cursor: default;\n  border-color: #CCC;\n  &:hover {\n    background-color: #BBB;\n    cursor: default;\n    box-shadow: none;\n  }\n}"
  },
  {
    "path": "resources/sass/_codemirror.scss",
    "content": "@use \"mixins\";\n@use \"vars\";\n\n/**\n * Custom CodeMirror BookStack overrides\n */\n\n.cm-editor {\n  font-size: 12px;\n  border: 1px solid #ddd;\n  line-height: 1.4;\n  margin-bottom: vars.$l;\n}\n\n.page-content .cm-editor,\n.CodeMirrorContainer .cm-editor {\n  border-radius: 4px;\n}\n\n.cm-editor .cm-line {\n  line-height: 1.6;\n}\n\n.cm-editor .cm-line, .cm-editor .cm-gutter {\n  font-family: var(--font-code);\n}\n\n// Manual dark-mode definition so that it applies to code blocks within the shadow\n// dom which are used within the WYSIWYG editor, as the .dark-mode on the parent\n// <html> node are not applies so instead we have the class on the parent element.\n.dark-mode .cm-editor {\n  border-color: #444;\n}\n\n/**\n * Custom Copy Button\n */\n.cm-copy-button {\n  position: absolute;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  top: -1px;\n  inset-inline-end: -1px;\n  background-color: #EEE;\n  border: 1px solid #DDD;\n  border-start-end-radius: 4px;\n  @include mixins.lightDark(background-color, #eee, #333);\n  @include mixins.lightDark(border-color, #ddd, #444);\n  @include mixins.lightDark(color, #444, #888);\n  line-height: 0;\n  cursor: pointer;\n  z-index: 5;\n  user-select: none;\n  opacity: 0;\n  pointer-events: none;\n  width: 32px;\n  height: 32px;\n  transition: background-color linear 60ms, color linear 60ms;\n  svg {\n    fill: currentColor;\n  }\n  &.success {\n    background: var(--color-positive);\n    color: #FFF;\n  }\n  &:focus {\n    outline: 0 !important;\n  }\n}\n.cm-editor:hover .cm-copy-button  {\n  user-select: all;\n  opacity: .6;\n  pointer-events: all;\n}"
  },
  {
    "path": "resources/sass/_colors.scss",
    "content": "@use \"mixins\";\n\n/**\n * Background colors\n */\n\n.primary-background {\n  background-color: var(--color-primary) !important;\n}\n.primary-background-light {\n  background-color: var(--color-primary-light);\n  @include mixins.whenDark {\n    background: #000;\n    .text-link {\n      color: #AAA !important;\n    }\n  }\n}\n.link-background {\n  background-color: var(--color-link) !important;\n}\n\n/*\n * Status text colors\n */\n.text-pos, .text-pos:hover, .text-pos-hover:hover {\n  color: var(--color-positive) !important;\n  fill: var(--color-positive) !important;\n}\n\n.text-warn, .text-warn:hover, .text-warn-hover:hover {\n  color: var(--color-warning) !important;\n  fill: var(--color-warning) !important;\n}\n\n.text-neg, .text-neg:hover, .text-neg-hover:hover  {\n  color: var(--color-negative) !important;\n  fill: var(--color-negative) !important;\n}\n\n/*\n * Style text colors\n */\n.text-primary, .text-primary:hover, .text-primary-hover:hover  {\n  color: var(--color-primary) !important;\n  fill: var(--color-primary) !important;\n}\n\n.text-link, .text-link:hover, .text-link-hover:hover  {\n  color: var(--color-link) !important;\n  fill: var(--color-link) !important;\n}\n\n.text-muted {\n  @include mixins.lightDark(color, #575757, #888888, true);\n  fill: currentColor !important;\n}\n\n.text-dark {\n  @include mixins.lightDark(color, #222, #ccc, true);\n  fill: currentColor !important;\n}\n\n.text-white {\n  color: #fff;\n  fill: currentColor !important;\n}\n\n/*\n * Entity text colors\n */\n.text-bookshelf, .text-bookshelf:hover {\n  color: var(--color-bookshelf);\n  fill: var(--color-bookshelf);\n}\n.text-book, .text-book:hover {\n  color: var(--color-book);\n  fill: var(--color-book);\n}\n.text-page, .text-page:hover {\n  color: var(--color-page);\n  fill: var(--color-page);\n}\n.text-page.draft, .text-page.draft:hover {\n  color: var(--color-page-draft);\n  fill: var(--color-page-draft);\n}\n.text-chapter, .text-chapter:hover {\n  color: var(--color-chapter);\n  fill: var(--color-chapter);\n}\n\n/*\n * Standard & Entity background colors\n */\n.bg-white {\n  background-color: #FFFFFF;\n}\n.bg-book {\n  background-color: var(--color-book);\n}\n.bg-chapter {\n  background-color: var(--color-chapter);\n}\n.bg-bookshelf {\n  background-color: var(--color-bookshelf);\n}\n"
  },
  {
    "path": "resources/sass/_components.scss",
    "content": "@use \"sass:math\";\n\n@use \"mixins\";\n@use \"vars\";\n\n\n// System wide notifications\n.notification {\n  position: fixed;\n  top: 0;\n  right: 0;\n  margin: vars.$xl;\n  padding: vars.$m vars.$l;\n  background-color: #FFF;\n  @include mixins.lightDark(background-color, #fff, #444);\n  border-radius: 4px;\n  border-inline-start: 6px solid currentColor;\n  box-shadow: vars.$bs-large;\n  z-index: 999999;\n  cursor: pointer;\n  max-width: 360px;\n  transition: transform ease-in-out 280ms;\n  transform: translateX(580px);\n  display: grid;\n  grid-template-columns: 42px 1fr 12px;\n  color: #444;\n  font-weight: 700;\n  span, svg {\n    vertical-align: middle;\n    justify-self: center;\n    align-self: center;\n  }\n  svg {\n    width: 2.8rem;\n    height: 2.8rem;\n    padding-inline-end: vars.$s;\n    fill: currentColor;\n  }\n  .dismiss {\n    margin-top: -8px;\n    svg {\n      height: 1.0rem;\n      @include mixins.lightDark(color, #444, #888);\n    }\n  }\n  span {\n    vertical-align: middle;\n    line-height: 1.3;\n    @include mixins.whenDark {\n      color: #BBB;\n    }\n  }\n  &.pos {\n    color: var(--color-positive);\n  }\n  &.neg {\n    color: var(--color-negative);\n  }\n  &.warning {\n    color: var(--color-warning);\n  }\n  &.showing {\n    transform: translateX(0);\n  }\n  &.showing:hover {\n    transform: translate3d(0, -2px, 0);\n  }\n}\n\n.chapter-contents-toggle {\n  cursor: pointer;\n  margin: 0;\n  transition: all ease-in-out 180ms;\n  user-select: none;\n  svg[data-icon=\"caret-right\"] {\n    margin-inline-end: 0;\n    font-size: 1rem;\n    transition: all ease-in-out 180ms;\n    transform: rotate(0deg);\n    transform-origin: 50% 50%;\n  }\n  &.open svg[data-icon=\"caret-right\"] {\n    transform: rotate(90deg);\n  }\n  svg[data-icon=\"caret-right\"] + * {\n    margin-inline-start: vars.$xxs;\n  }\n}\n\n[overlay], .popup-background {\n  @include mixins.lightDark(background-color, rgba(0, 0, 0, 0.333), rgba(0, 0, 0, 0.6));\n  position: fixed;\n  z-index: 95536;\n  width: 100%;\n  height: 100%;\n  min-width: 100%;\n  min-height: 100%;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  align-items: center;\n  justify-content: center;\n  display: none;\n}\n\n.popup-body-wrap {\n  display: flex;\n}\n\n.popup-body {\n  @include mixins.lightDark(background-color, #fff, #333);\n  max-height: 90%;\n  max-width: 1200px;\n  width: 90%;\n  height: auto;\n  margin: 2% auto;\n  border-radius: 4px;\n  box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.3);\n  overflow: hidden;\n  z-index: 999;\n  display: flex;\n  flex-direction: column;\n  position: relative;\n  &.small {\n    margin: 2% auto;\n    width: 800px;\n    max-width: 90%;\n  }\n  &.very-small {\n    margin: 2% auto;\n    width: 600px;\n    max-width: 90%;\n  }\n  &:before {\n    display: flex;\n    align-self: flex-start;\n  }\n  .popup-content {\n    overflow-y: auto;\n  }\n  &:focus {\n    outline: 0;\n  }\n}\n\n.popup-header button, .popup-footer button {\n  margin: 0;\n  border-radius: 0;\n  box-shadow: none;\n  color: #FFF;\n  padding: vars.$xs vars.$m;\n  cursor: pointer;\n}\n\n.popup-header button:not(.popup-header-close) {\n  font-size: 0.8rem;\n}\n\n.popup-header button:hover {\n    background-color: rgba(255, 255, 255, 0.1);\n}\n\n.popup-footer {\n  justify-content: end;\n  background-color: var(--color-primary-light);\n  min-height: 41px;\n  button {\n    padding: 10px vars.$m;\n  }\n}\n\n.popup-header-close {\n  border: 0;\n  color: #FFF;\n  font-size: 16px;\n  cursor: pointer;\n  svg {\n    margin-right: 0;\n  }\n}\n\n.popup-header, .popup-footer {\n  display: flex;\n  position: relative;\n  height: 40px;\n  flex: 0;\n  .popup-title {\n    color: #FFF;\n    margin-inline-end: auto;\n    padding: 8px vars.$m;\n  }\n  &.flex-container-row {\n    display: flex !important;\n  }\n}\nbody.flexbox-support #entity-selector-wrap .popup-body .form-group {\n  height: 444px;\n  min-height: 444px;\n}\n#entity-selector-wrap .popup-body .form-group {\n  margin: 0;\n}\n.popup-body .entity-selector-container {\n  flex: 1;\n}\n\n.dropzone-overlay {\n  position: absolute;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  font-size: 1.333rem;\n  width: 98%;\n  height: 98%;\n  left: 1%;\n  top: 1%;\n  border-radius: 4px;\n  border: 1px dashed var(--color-primary);\n  font-style: italic;\n  box-sizing: content-box;\n  background-clip: padding-box;\n  background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3E%3Cpath fill='%23a9a9a9' fill-opacity='0.52' d='M1 3h1v1H1V3zm2-2h1v1H3V1z'%3E%3C/path%3E%3C/svg%3E\");\n  background-color: var(--color-primary);\n  color: #FFF;\n  opacity: .8;\n  z-index: 9;\n  pointer-events: none;\n  animation: dzAnimIn 240ms ease-in-out;\n}\n\n.dropzone-landing-area {\n  background-color: var(--color-primary-light);\n  padding: vars.$m vars.$l;\n  width: 100%;\n  border: 1px dashed var(--color-primary);\n  color: var(--color-primary);\n  border-radius: 4px;\n}\n\n@keyframes dzAnimIn {\n  0% {\n    opacity: 0;\n    transform: scale(.7);\n  }\n  60% {\n    transform: scale(1.1);\n  }\n  100% {\n    transform: scale(1);\n    opacity: .8;\n  }\n}\n\n@keyframes dzFileItemIn {\n  0% {\n    opacity: .5;\n    transform: translateY(28px);\n  }\n  100% {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n@keyframes dzFileItemOut {\n  0% {\n    opacity: 1;\n    transform: translateY(0);\n  }\n  100% {\n    opacity: .5;\n    transform: translateY(28px);\n  }\n}\n\n.dropzone-file-item {\n  width: 260px;\n  height: 80px;\n  position: relative;\n  display: flex;\n  margin: 1rem;\n  flex-direction: row;\n  @include mixins.lightDark(background, #FFF, #444);\n  box-shadow: vars.$bs-large;\n  border-radius: 4px;\n  overflow: hidden;\n  padding-bottom: 3px;\n  animation: dzFileItemIn ease-in-out 240ms;\n  transition: transform ease-in-out 120ms, box-shadow ease-in-out 120ms;\n  cursor: pointer;\n  &:hover {\n    transform: translateY(-3px);\n    box-shadow: 0 3px 8px 1px rgba(22, 22, 22, 0.2);\n  }\n}\n.dropzone-file-item.dismiss {\n  animation: dzFileItemOut ease-in-out 240ms;\n}\n.dropzone-file-item .loading-container {\n  text-align: start !important;\n  margin: 0;\n}\n.dropzone-file-item-image-wrap {\n  width: 80px;\n  position: relative;\n  background-color: var(--color-primary-light);\n  img {\n    object-fit: cover;\n    width: 100%;\n    height: 100%;\n    opacity: .8;\n  }\n}\n.dropzone-file-item-text-wrap {\n  flex: 1;\n  display: block;\n  padding: 1rem;\n  overflow: auto;\n}\n.dropzone-file-item-progress {\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  font-size: 0;\n  height: 3px;\n  background-color: var(--color-primary);\n  transition: width ease-in-out 240ms;\n}\n.dropzone-file-item-label {\n  line-height: 1.2;\n  margin-bottom: .2rem;\n}\n.dropzone-file-item-label,\n.dropzone-file-item-status {\n  align-items: center;\n  font-size: .8rem;\n  font-weight: 700;\n}\n.dropzone-file-item-status[data-status] {\n  display: flex;\n  font-size: .6rem;\n  font-weight: 500;\n  line-height: 1.2;\n}\n.dropzone-file-item-status[data-status=\"success\"] {\n  color: var(--color-positive);\n}\n.dropzone-file-item-status[data-status=\"error\"] {\n  color: var(--color-negative);\n}\n.dropzone-file-item-status[data-status] + .dropzone-file-item-label {\n  display: none;\n}\n\n.image-manager-body {\n  min-height: 70vh;\n}\n.image-manager-filter-bar {\n  position: sticky;\n  top: 0;\n  z-index: 5;\n  @include mixins.lightDark(background-color, rgba(255, 255, 255, 0.85), rgba(80, 80, 80, 0.85));\n}\n.image-manager-filter-bar-bg {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 100%;\n  height: 100%;\n  opacity: .15;\n  z-index: -1;\n}\n\n.image-manager-filters {\n  box-shadow: vars.$bs-med;\n  border-radius: 4px;\n  overflow: hidden;\n  border-bottom: 0 !important;\n  @include mixins.whenDark {\n    border: 1px solid #000 !important;\n  }\n  button {\n    line-height: 0;\n    @include mixins.lightDark(background-color, #FFF, #333);\n  }\n  svg {\n    margin: 0;\n  }\n}\n\n.image-manager-list {\n  padding: 3px;\n  display: grid;\n  grid-template-columns: repeat( auto-fill, minmax(max(140px, 17%), 1fr) );\n  gap: 3px;\n  z-index: 3;\n  > div {\n    aspect-ratio: 1;\n  }\n}\n\n.image-manager-list .image {\n  display: block;\n  position: relative;\n  border-radius: 0;\n  margin: 0;\n  width: 100%;\n  text-align: start;\n  padding: 0;\n  cursor: pointer;\n  aspect-ratio: 1;\n  @include mixins.lightDark(border-color, #ddd, #000);\n  transition: all linear 80ms;\n  overflow: hidden;\n  &.selected {\n    background-color: var(--color-primary-light);\n    outline: currentColor 3px solid;\n    border-radius: 3px;\n    transform: scale3d(0.95, 0.95, 0.95);\n  }\n  img {\n    width: 100%;\n    max-width: 100%;\n    display: block;\n    object-fit: cover;\n    height: auto;\n  }\n  .image-meta {\n    opacity: 0;\n    position: absolute;\n    width: 100%;\n    bottom: 0;\n    left: 0;\n    color: #EEE;\n    background-color: rgba(0, 0, 0, 0.7);\n    font-size: 10px;\n    padding: 3px 4px;\n    pointer-events: none;\n    transition: opacity ease-in-out 80ms;\n    span {\n      display: block;\n    }\n  }\n  &.selected .image-meta,\n  &:hover .image-meta,\n  &:focus .image-meta {\n    opacity: 1;\n  }\n  @include mixins.smaller-than(vars.$bp-m) {\n    .image-meta {\n      display: none;\n    }\n  }\n}\n\n.image-manager .load-more {\n  text-align: center;\n  padding: vars.$s vars.$m;\n  clear: both;\n  .loading-container {\n    margin: 0;\n  }\n}\n\n.image-manager .loading-container {\n  text-align: center;\n}\n\n.image-manager-list .image-manager-list-warning {\n  grid-column: 1 / -1;\n  aspect-ratio: auto;\n}\n\n.image-manager-warning {\n  @include mixins.lightDark(background, #FFF, #333);\n  color: var(--color-warning);\n  font-weight: bold;\n  border-inline: 3px solid var(--color-warning);\n}\n\n.image-manager-sidebar {\n  width: 300px;\n  margin: 0 auto;\n  overflow-y: auto;\n  overflow-x: hidden;\n  border-inline-start: 1px solid #DDD;\n  @include mixins.lightDark(border-color, #ddd, #000);\n  .inner {\n    min-height: auto;\n    padding: vars.$m;\n  }\n  .image-manager-viewer img {\n    max-width: 100%;\n    max-height: 180px;\n    display: block;\n    margin: 0 auto vars.$m auto;\n    box-shadow: 0 1px 21px 1px rgba(76, 76, 76, 0.3);\n  }\n  .image-manager-viewer {\n    height: 196px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    a {\n      display: inline-block;\n    }\n  }\n}\n@include mixins.smaller-than(vars.$bp-m) {\n  .image-manager-sidebar {\n    border-inline-start: 0;\n  }\n}\n\n.image-manager-content {\n  display: flex;\n  flex-direction: column;\n  flex: 1;\n  overflow-y: scroll;\n  .container {\n    width: 100%;\n  }\n  .full-tab {\n    text-align: center;\n  }\n}\n\n.tab-container.bordered [role=\"tablist\"] button[role=\"tab\"] {\n  border-inline-end: 1px solid #DDD;\n  @include mixins.lightDark(border-inline-end-color, #DDD, #000);\n  &:last-child {\n    border-inline-end: none;\n  }\n}\n\n.tab-container [role=\"tablist\"] {\n  display: flex;\n  align-items: end;\n  justify-items: start;\n  text-align: start;\n  border-bottom: 1px solid #DDD;\n  @include mixins.lightDark(border-color, #ddd, #444);\n  margin-bottom: vars.$m;\n}\n\n.tab-container [role=\"tablist\"] button[role=\"tab\"] {\n  display: inline-block;\n  padding: vars.$s;\n  @include mixins.lightDark(color, rgba(0, 0, 0, .5), rgba(255, 255, 255, .5));\n  cursor: pointer;\n  border-bottom: 2px solid transparent;\n  margin-bottom: -1px;\n  &[aria-selected=\"true\"] {\n    color: var(--color-link) !important;\n    border-bottom-color: var(--color-link) !important;\n    outline: 0 !important;\n  }\n  &:hover, &:focus {\n    @include mixins.lightDark(color, rgba(0, 0, 0, .8), rgba(255, 255, 255, .8));\n    @include mixins.lightDark(border-bottom-color,  rgba(0, 0, 0, .2), rgba(255, 255, 255, .2));\n  }\n  &:focus {\n    outline: 1px dotted var(--color-primary);\n    outline-offset: -2px;\n  }\n}\n.tab-container [role=\"tablist\"].controls-card {\n  margin-bottom: 0;\n  border-bottom: 0;\n  padding: 0 vars.$xs;\n}\n.tab-container [role=\"tabpanel\"].no-outline:focus {\n  outline: none;\n}\n\n.image-picker .none {\n  display: none;\n}\n\n.code-editor .CodeMirror {\n  height: auto;\n  min-height: 50vh;\n  border-bottom: 0;\n}\n\n.code-editor .lang-options {\n  overflow-y: scroll;\n  flex-basis: 200px;\n  flex-grow: 1;\n}\n\n.code-editor .lang-options button {\n  display: block;\n  padding: vars.$xs vars.$m;\n  border-bottom: 1px solid;\n  @include mixins.lightDark(color, #333, #AAA);\n  @include mixins.lightDark(border-bottom-color, #EEE, #000);\n  cursor: pointer;\n  width: 100%;\n  text-align: left;\n  font-family: var(--font-code);\n  font-size: 0.7rem;\n  padding-left: 24px + vars.$xs;\n  &:hover, &.active {\n    background-color: var(--color-primary-light);\n    color: var(--color-primary);\n  }\n}\n\n.code-editor button.lang-option-favorite-toggle {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 28px;\n  font-size: 1rem;\n  border: 0;\n  line-height: 1;\n  padding: 2px;\n  z-index: 2;\n  height: 100%;\n  text-align: center;\n  color: var(--color-primary);\n  svg {\n    margin: 0;\n  }\n}\n\n.code-editor button[data-favourite=\"true\"] ~ .action-favourite,\n.code-editor button[data-favourite=\"false\"] ~ .action-unfavourite {\n  display: none;\n}\n\n.code-editor .action-favourite {\n  opacity: 0.5;\n}\n.code-editor button:hover ~ .action-favourite {\n  opacity: 1;\n}\n\n.code-editor label {\n  background-color: var(--color-primary-light);\n  width: 100%;\n  color: var(--color-primary);\n  padding: vars.$xxs vars.$s;\n  margin-bottom: 0;\n}\n\n.code-editor-language-list {\n  position: relative;\n  width: 160px;\n  z-index: 2;\n  align-items: stretch;\n}\n\n.code-editor-language-list input {\n  border-radius: 0;\n  border: 0;\n  border-bottom: 1px solid #DDD;\n  padding: vars.$xs vars.$s;\n  height: auto;\n}\n\n.code-editor-main {\n  flex: 1;\n  min-width: 0;\n  .cm-editor {\n    margin-bottom: 0;\n    z-index: 1;\n    max-width: 100%;\n    width: 100%;\n  }\n}\n\n.code-editor-body-wrap {\n  height: 80vh;\n}\n\n@include mixins.smaller-than(vars.$bp-s) {\n  .code-editor .lang-options {\n    display: none;\n  }\n  .code-editor-body-wrap {\n    flex-direction: column;\n  }\n  .code-editor-language-list, .code-editor-language-list input {\n    width: 100%;\n  }\n}\n\n.comments-container {\n  padding-inline: vars.$xl;\n  @include mixins.smaller-than(vars.$bp-m) {\n    padding-inline: vars.$xs;\n  }\n}\n.comment-box {\n  border-radius: 4px;\n  border: 1px solid #DDD;\n  @include mixins.lightDark(border-color, #ddd, #000);\n  @include mixins.lightDark(background-color, #FFF, #222);\n  .content {\n    font-size: 0.666em;\n    padding: vars.$xs vars.$s;\n    p, ul, ol {\n      font-size: vars.$fs-m;\n      margin: .5em 0;\n    }\n  }\n  .actions {\n    opacity: 0;\n    transition: opacity ease-in-out 120ms;\n  }\n  &:hover .actions, &:focus-within .actions {\n    opacity: 1;\n  }\n  .actions button:focus {\n    outline: 1px dotted var(--color-primary);\n  }\n  @include mixins.smaller-than(vars.$bp-m) {\n    .actions {\n      opacity: 1;\n    }\n  }\n}\n\n.comment-box .header {\n  border-bottom: 1px solid #DDD;\n  padding: vars.$xs vars.$s;\n  @include mixins.lightDark(border-color, #DDD, #000);\n  a {\n    color: inherit;\n  }\n  .text-muted {\n    color: #999;\n  }\n  .meta a, .meta span {\n    white-space: nowrap;\n  }\n  .right-meta .text-muted {\n    opacity: .8;\n  }\n}\n\n.comment-thread-indicator {\n  border-inline-start: 3px dotted #DDD;\n  @include mixins.lightDark(border-color, #DDD, #444);\n  margin-inline-start: vars.$xs;\n  width: vars.$l;\n  height: calc(100% - #{vars.$m});\n}\n\n.comment-reference-indicator-wrap a {\n  float: left;\n  margin-top: vars.$xs;\n  font-size: 12px;\n  display: inline-block;\n  font-weight: bold;\n  position: relative;\n  border-radius: 4px;\n  overflow: hidden;\n  padding: 2px 6px 2px 0;\n  margin-inline-end: vars.$xs;\n  color: var(--color-link);\n  span {\n    display: none;\n  }\n  &.outdated span {\n    display: inline;\n  }\n  &.outdated.missing {\n    color: var(--color-warning);\n    pointer-events: none;\n  }\n  svg {\n    width: 24px;\n    margin-inline-end: 0;\n  }\n  &:after {\n    background-color: currentColor;\n    content: '';\n    width: 100%;\n    height: 100%;\n    position: absolute;\n    left: 0;\n    top: 0;\n    opacity: 0.15;\n  }\n  &[href=\"#\"] {\n    color: #444;\n    pointer-events: none;\n  }\n}\n\n.comment-branch .comment-box {\n  margin-bottom: vars.$m;\n}\n\n.comment-branch .comment-branch .comment-branch .comment-branch .comment-thread-indicator {\n  display: none;\n}\n\n.comment-reply {\n  display: none;\n  margin: 0 !important;\n  margin-bottom: -(vars.$xxs) !important;\n}\n\n.comment-branch .comment-branch .comment-branch .comment-branch .comment-reply {\n  display: block;\n}\n\n.comment-container .empty-state {\n  display: none;\n}\n.comment-container:not(:has([component=\"page-comment\"])) .empty-state {\n  display: block;\n}\n\n.comment-container-compact .comment-box {\n  margin-bottom: vars.$xs;\n  .meta {\n    font-size: 0.8rem;\n  }\n  .header {\n    padding: vars.$xs;\n  }\n  .right-meta {\n    display: none;\n  }\n  .content {\n    padding: vars.$xs vars.$s;\n  }\n}\n.comment-container-compact .comment-thread-indicator {\n  width: vars.$m;\n}\n\n.comment-container-super-compact .comment-box {\n  .meta {\n    font-size: 12px;\n  }\n  .avatar {\n    width: 22px;\n    height: 22px;\n    margin-inline-end: 2px !important;\n  }\n  .content {\n    padding: vars.$xxs vars.$s;\n    line-height: 1.2;\n  }\n  .content p {\n    font-size: 12px;\n  }\n}\n\n.comment-container-super-compact .comment-thread-indicator {\n  width: (vars.$xs + 3px);\n  margin-inline-start: 3px;\n}\n\n#tag-manager .drag-card {\n  max-width: 500px;\n}\n\n.template-item {\n  cursor: pointer;\n  position: relative;\n  &:hover, .template-item-actions button:hover {\n    background-color: #F2F2F2;\n  }\n  .template-item-actions {\n    position: absolute;\n    top: 0;\n    inset-inline-end: 0;\n    width: 50px;\n    height: 100%;\n    display: flex;\n    flex-direction: column;\n    border-inline-start: 1px solid;\n    @include mixins.lightDark(border-color, #ddd, #000);\n  }\n  .template-item-actions button {\n    cursor: pointer;\n    flex: 1;\n    @include mixins.lightDark(background-color, #FFF, #222);\n    border: 0;\n    border-top: 1px solid;\n    @include mixins.lightDark(border-color, #DDD, #000);\n  }\n  .template-item-actions button svg {\n    margin: 0;\n  }\n  .template-item-actions button:first-child {\n    border-top: 0;\n  }\n}\n\n\n.dropdown-search {\n  position: relative;\n}\n.dropdown-search-toggle-breadcrumb {\n  border: 1px solid transparent;\n  border-radius: 4px;\n  line-height: normal;\n  padding: vars.$xs;\n  opacity: 0.6;\n  cursor: pointer;\n  &:hover {\n    opacity: 1;\n    @include mixins.lightDark(border-color, #DDD, #444);\n  }\n  .svg-icon {\n    margin-inline-end: 0;\n  }\n}\n.dropdown-search-toggle-select {\n  display: flex;\n  gap: vars.$s;\n  line-height: normal;\n  .svg-icon {\n    height: 26px;\n    width: 26px;\n    margin: 0;\n  }\n  .avatar {\n    height: 22px;\n    width: 22px;\n  }\n  .avatar + span {\n    max-width: 100%;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n  .dropdown-search-toggle-caret {\n    font-size: 1.15rem;\n  }\n}\n.dropdown-search-toggle-select-label {\n  min-width: 0;\n  white-space: nowrap;\n}\n.dropdown-search-toggle-select-caret {\n  line-height: 0;\n  margin-left: auto;\n  margin-top: -2px;\n  display: flex;\n  align-items: center;\n}\n\n.dropdown-search-dropdown {\n  box-shadow: vars.$bs-med;\n  overflow: hidden;\n  min-height: 100px;\n  width: 240px;\n  display: none;\n  position: absolute;\n  z-index: 80;\n  right: 0;\n  top: 0;\n  margin-top: vars.$m;\n  @include mixins.rtl {\n    right: auto;\n    left: -(vars.$m);\n  }\n  .dropdown-search-search .svg-icon {\n    position: absolute;\n    left: vars.$s;\n    @include mixins.rtl {\n      right: vars.$s;\n      left: auto;\n    }\n    top: 11px;\n    fill: #888;\n    pointer-events: none;\n  }\n  .dropdown-search-list {\n    max-height: 400px;\n    overflow-y: scroll;\n    text-align: start;\n  }\n  .dropdown-search-item {\n    padding: vars.$s vars.$m;\n    font-size: 0.8rem;\n    &:hover,&:focus {\n      background-color: #F2F2F2;\n      @include mixins.lightDark(background-color, #F2F2F2, #444);\n      text-decoration: none;\n    }\n  }\n  input, input:focus {\n    padding-inline-start: vars.$xl;\n    border-radius: 0;\n    border: 0;\n    border-bottom: 1px solid #DDD;\n  }\n  input:focus {\n    outline: 0;\n  }\n  .svg-icon {\n    font-size: vars.$fs-m;\n  }\n  &.compact {\n    .dropdown-search-list {\n      max-height: 320px;\n    }\n    .dropdown-search-item {\n      padding: vars.$xs vars.$s;\n    }\n    .avatar {\n      width: 22px;\n      height: 22px;\n    }\n  }\n}\n\n@include mixins.smaller-than(vars.$bp-l) {\n  .dropdown-search-dropdown {\n    inset-inline: vars.$m auto;\n  }\n  .dropdown-search-dropdown .dropdown-search-list {\n    max-height: 240px;\n  }\n}\n\n.item-list {\n  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);\n}\n.item-list-row {\n  border: 1.5px solid;\n  @include mixins.lightDark(border-color, #E2E2E2, #444);\n  border-bottom-width: 0;\n  label {\n    padding-bottom: 0;\n  }\n  &:hover {\n    @include mixins.lightDark(background-color, #F6F6F6, #333);\n  }\n}\n.item-list-row:first-child {\n  border-radius: 4px 4px 0 0;\n}\n.item-list-row:last-child {\n  border-radius: 0 0 4px 4px;\n  border-bottom-width: 1.5px;\n}\n.item-list-row:first-child:last-child {\n  border-radius: 4px;\n}\n.item-list-row-toggle-all {\n  visibility: hidden;\n}\n.item-list-row:hover .item-list-row-toggle-all {\n  visibility: visible;\n}\n\n.status-indicator-active, .status-indicator-inactive {\n  width: 8px;\n  height: 8px;\n  border-radius: 50%;\n  display: inline-block;\n}\n.status-indicator-active {\n  background-color: var(--color-positive);\n}\n.status-indicator-inactive {\n  background-color: var(--color-negative);\n}\n\n.shortcut-container {\n  background-color: rgba(0, 0, 0, 0.25);\n  pointer-events: none;\n  position: fixed;\n  left: 0;\n  top: 0;\n  width: 100%;\n  height: 100%;\n  z-index: 99;\n}\n.shortcut-linkage {\n  position: fixed;\n  box-shadow: 0 0 4px 0 #FFF;\n  border-radius: 3px;\n}\n.shortcut-hint {\n  position: fixed;\n  padding: vars.$xxs vars.$xxs;\n  font-size: .85rem;\n  font-weight: 700;\n  line-height: 1;\n  background-color: #eee;\n  border-radius: 3px;\n  border: 1px solid #b4b4b4;\n  box-shadow: 0 1px 1px rgba(0, 0, 0, .2), 0 2px 0 0 rgba(255, 255, 255, .7) inset;\n  color: #333;\n}\n\n// Back to top link\n$btt-size: 40px;\n.back-to-top {\n  background-color: var(--color-primary);\n  position: fixed;\n  bottom: vars.$m;\n  right: vars.$l;\n  padding: 5px 7px;\n  cursor: pointer;\n  color: #FFF;\n  fill: #FFF;\n  svg {\n    width: math.div($btt-size, 1.5);\n    height: math.div($btt-size, 1.5);\n    margin-inline-end: 4px;\n  }\n  width: $btt-size;\n  height: $btt-size;\n  border-radius: $btt-size;\n  transition: all ease-in-out 180ms;\n  opacity: 0;\n  z-index: 999;\n  overflow: hidden;\n  &:hover {\n    width: $btt-size*3.4;\n    opacity: 1 !important;\n  }\n  .inner {\n    width: $btt-size*3.4;\n  }\n  span {\n    position: relative;\n    vertical-align: top;\n    line-height: 2;\n  }\n}\n\n// Sortable scroll boxes\n.scroll-box {\n  list-style: none;\n  padding: 0;\n  margin: 0;\n  max-height: 280px;\n  overflow-y: scroll;\n  border: 1px solid;\n  @include mixins.lightDark(border-color, #DDD, #000);\n  border-radius: 3px;\n  min-height: 20px;\n  @include mixins.lightDark(background-color, #EEE, #000);\n}\n.scroll-box-item {\n  border-bottom: 1px solid;\n  border-top: 1px solid;\n  @include mixins.lightDark(border-color, #DDD, #000);\n  margin-top: -1px;\n  @include mixins.lightDark(background-color, #FFF, #222);\n  display: flex;\n  align-items: flex-start;\n  padding: 1px;\n  &:last-child {\n    border-bottom: 0;\n  }\n  &:hover {\n    cursor: pointer;\n    @include mixins.lightDark(background-color, #f8f8f8, #333);\n  }\n  &.items-center {\n    align-items: center;\n  }\n  .handle {\n    color: #AAA;\n    cursor: grab;\n  }\n  button {\n    opacity: .6;\n    line-height: 1;\n  }\n  .handle svg {\n    margin: 0;\n  }\n  > * {\n    padding: vars.$xs vars.$m;\n  }\n  .handle + * {\n    padding-left: 0;\n  }\n  &:hover .handle {\n    @include mixins.lightDark(color, #444, #FFF);\n  }\n  &:hover button {\n    opacity: 1;\n  }\n  a:hover {\n    text-decoration: none;\n  }\n}\n\ninput.scroll-box-search, .scroll-box-header-item {\n  font-size: 0.8rem;\n  border: 1px solid;\n  @include mixins.lightDark(border-color, #DDD, #000);\n  @include mixins.lightDark(background-color, #FFF, #222);\n  margin-bottom: -1px;\n  border-radius: 3px 3px 0 0;\n  width: 100%;\n  max-width: 100%;\n  height: auto;\n  line-height: 1.4;\n  color: #666;\n}\n\n.scroll-box-search + .scroll-box,\n.scroll-box-header-item + .scroll-box {\n  border-radius: 0 0 3px 3px;\n}\n\n.scroll-box.configured-option-list [data-action=\"add\"] {\n  display: none;\n}\n.scroll-box.available-option-list [data-action=\"remove\"],\n.scroll-box.available-option-list [data-action=\"move_up\"],\n.scroll-box.available-option-list [data-action=\"move_down\"],\n{\n  display: none;\n}\n\n.scroll-box > li.empty-state {\n  display: none;\n}\n.scroll-box > li.empty-state:last-child {\n  display: list-item;\n}\n\ndetails.section-expander summary {\n  border-top: 1px solid #DDD;\n  @include mixins.lightDark(border-color, #DDD, #000);\n  font-weight: bold;\n  font-size: 12px;\n  color: #888;\n  cursor: pointer;\n  padding-block: vars.$xs;\n}\ndetails.section-expander:open summary {\n  margin-bottom: vars.$s;\n}\ndetails.section-expander {\n  border-bottom: 1px solid #DDD;\n  @include mixins.lightDark(border-color, #DDD, #000);\n}"
  },
  {
    "path": "resources/sass/_content.scss",
    "content": "@use \"mixins\";\n@use \"vars\";\n\n/**\n * Page Content\n * Styles specific to blocks used within page content.\n */\n\n.page-content {\n  width: 100%;\n  max-width: 840px;\n  margin: 0 auto;\n  overflow-wrap: break-word;\n  position: relative;\n  .align-left {\n    text-align: left;\n  }\n  img.align-left, table.align-left, iframe.align-left, video.align-left {\n    float: left !important;\n    margin: vars.$xs vars.$m vars.$m 0;\n  }\n  .align-right {\n    text-align: right !important;\n  }\n  img.align-right, table.align-right, iframe.align-right, video.align-right {\n    float: right !important;\n    margin: vars.$xs 0 vars.$xs vars.$s;\n  }\n  .align-center {\n    text-align: center;\n  }\n  img.align-center, video.align-center, iframe.align-center {\n    display: block;\n  }\n  img.align-center, table.align-center, iframe.align-center, video.align-center {\n    margin-left: auto;\n    margin-right: auto;\n  }\n  .align-justify {\n    text-align: justify;\n  }\n  h1, h2, h3, h4, h5, h6, pre {\n    clear: left;\n  }\n  hr {\n    clear: both;\n    margin: vars.$m 0;\n  }\n  table {\n    hyphens: auto;\n    table-layout: fixed;\n    max-width: 100%;\n    height: auto !important;\n  }\n\n  // diffs\n  ins,\n  del {\n    text-decoration: none;\n  }\n  ins {\n    background: #dbffdb;\n  }\n  del {\n    background: #FFECEC;\n  }\n\n  details {\n    border: 1px solid;\n    @include mixins.lightDark(border-color, #DDD, #555);\n    margin-bottom: 1em;\n    padding: vars.$s;\n  }\n  details > summary {\n    margin-top: -(vars.$s);\n    margin-left: -(vars.$s);\n    margin-right: -(vars.$s);\n    margin-bottom: -(vars.$s);\n    font-weight: bold;\n    @include mixins.lightDark(background-color, #EEE, #333);\n    padding: vars.$xs vars.$s;\n  }\n  details[open] > summary {\n    margin-bottom: vars.$s;\n    border-bottom: 1px solid;\n    @include mixins.lightDark(border-color, #DDD, #555);\n  }\n  details > summary + * {\n    margin-top: .2em;\n  }\n  details:after {\n    content: '';\n    display: block;\n    clear: both;\n  }\n\n  li > input[type=\"checkbox\"] {\n    vertical-align: top;\n    margin-top: 0.3em;\n  }\n\n  p:empty {\n    min-height: 1.6em;\n  }\n\n  &.page-revision {\n    pre code {\n      white-space: pre-wrap;\n    }\n  }\n\n  .cm-editor {\n    margin-bottom: 1.375em;\n  }\n\n  video, iframe {\n    max-width: 100%;\n  }\n\n  a {\n    text-decoration: underline;\n  }\n}\n\n// This is seperated out so we can target it out-of-editor by default\n// and use advanced (:not) syntax, not supported by things like PDF gen,\n// to target in-editor scenarios to handle edge-case of TinyMCE using an\n// image for data placeholders where we'd want height attributes to take effect.\nbody .page-content img,\n.page-content img:not([data-mce-object]) {\n  max-width: 100%;\n  height:auto;\n}\n\n/**\n * Callouts\n * Some styles duplicated for supporting logical units (eg. inline-end) while\n * providing fallbacks to non-logical rules, so RTL is natively supported where possible.\n */\n.callout {\n  border-left: 3px solid #BBB;\n  border-inline-start: 3px solid #BBB;\n  border-inline-end: none;\n  background-color: #EEE;\n  padding: vars.$s;\n  padding-left: vars.$xl;\n  padding-inline-start: vars.$xl;\n  padding-inline-end: vars.$s;\n  display: block;\n  position: relative;\n  overflow: auto;\n  &:before {\n    background-image: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiMwMTUzODAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bTEgMTVoLTJ2LTZoMnY2em0wLThoLTJWN2gydjJ6Ii8+PC9zdmc+');\n    background-repeat: no-repeat;\n    content: '';\n    width: 1.2em;\n    height: 1.2em;\n    left: vars.$xs + 2px;\n    inset-inline-start: vars.$xs + 2px;\n    inset-inline-end: unset;\n    top: 50%;\n    margin-top: -9px;\n    display: inline-block;\n    position: absolute;\n    line-height: 1;\n    opacity: 0.8;\n  }\n  &.success {\n    @include mixins.lightDark(border-color, vars.$positive, vars.$positive-dark);\n    @include mixins.lightDark(background-color, #eafdeb, #122913);\n    @include mixins.lightDark(color, #063409, vars.$positive-dark);\n  }\n  &.success:before {\n    background-image: url(\"data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiMzNzZjMzkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bS0yIDE1bC01LTUgMS40MS0xLjQxTDEwIDE0LjE3bDcuNTktNy41OUwxOSA4bC05IDl6Ii8+PC9zdmc+\");\n  }\n  &.danger {\n    @include mixins.lightDark(border-color, vars.$negative, vars.$negative-dark);\n    @include mixins.lightDark(background-color, #fcdbdb, #250505);\n    @include mixins.lightDark(color, #4d0706, vars.$negative-dark);\n  }\n  &.danger:before {\n    background-image: url(\"data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiNiOTE4MTgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0xNS43MyAzSDguMjdMMyA4LjI3djcuNDZMOC4yNyAyMWg3LjQ2TDIxIDE1LjczVjguMjdMMTUuNzMgM3pNMTIgMTcuM2MtLjcyIDAtMS4zLS41OC0xLjMtMS4zIDAtLjcyLjU4LTEuMyAxLjMtMS4zLjcyIDAgMS4zLjU4IDEuMyAxLjMgMCAuNzItLjU4IDEuMy0xLjMgMS4zem0xLTQuM2gtMlY3aDJ2NnoiLz4gICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjwvc3ZnPg==\");\n  }\n  &.info {\n    @include mixins.lightDark(border-color, vars.$info, vars.$info-dark);\n    @include mixins.lightDark(background-color, #d3efff, #001825);\n    @include mixins.lightDark(color, #01466c, vars.$info-dark);\n  }\n  &.warning {\n    @include mixins.lightDark(border-color, vars.$warning, vars.$warning-dark);\n    @include mixins.lightDark(background-color, #fee3d3, #30170a);\n    @include mixins.lightDark(color, #6a2802, vars.$warning-dark);\n  }\n  &.warning:before {\n    background-image: url(\"data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiNiNjUzMWMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEgMjFoMjJMMTIgMiAxIDIxem0xMi0zaC0ydi0yaDJ2MnptMC00aC0ydi00aDJ2NHoiLz48L3N2Zz4=\");\n  }\n  a {\n    color: inherit;\n    text-decoration: underline;\n  }\n}\n\n/**\n * Mention Links\n */\n\na[data-mention-user-id] {\n  display: inline-block;\n  position: relative;\n  color: var(--color-link);\n  padding: 0.1em 0.4em;\n  display: -webkit-inline-box;\n  -webkit-box-orient: vertical;\n  -webkit-line-clamp: 1;\n  max-width: 180px;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  font-size: 0.92em;\n  margin-inline: 0.2em;\n  vertical-align: middle;\n  border-radius: 3px;\n  border: 1px solid transparent;\n  &:hover {\n    text-decoration: none;\n    border-color: currentColor;\n  }\n  &:after {\n    content: '';\n    background-color: currentColor;\n    opacity: 0.2;\n    position: absolute;\n    top: 0;\n    right: 0;\n    width: 100%;\n    height: 100%;\n    display: block;\n  }\n}"
  },
  {
    "path": "resources/sass/_editor.scss",
    "content": "@use \"mixins\";\n@use \"vars\";\n\n// Common variables\n:root {\n  --editor-color-primary: #206ea7;\n}\n\n// Main UI elements\n.editor-container {\n  @include mixins.lightDark(background-color, #FFF, #222);\n  position: relative;\n  &.fullscreen {\n    z-index: 500;\n  }\n}\n\n.editor-toolbar-main {\n  display: flex;\n  flex-wrap: wrap;\n  justify-content: center;\n  border-top: 1px solid #DDD;\n  border-bottom: 1px solid #DDD;\n  @include mixins.lightDark(border-color, #DDD, #000);\n}\n\n@include mixins.smaller-than(vars.$bp-xl) {\n  .editor-toolbar-main {\n    overflow-x: scroll;\n    flex-wrap: nowrap;\n    justify-content: start;\n  }\n}\n\nbody.editor-is-fullscreen {\n  overflow: hidden;\n  .edit-area {\n    z-index: 20;\n  }\n}\n.editor-content-area {\n  min-height: 100%;\n  padding-block: 1rem;\n  &:focus {\n    outline: 0;\n  }\n}\n.editor-content-wrap {\n  position: relative;\n  overflow-y: scroll;\n  padding-inline: vars.$s;\n  flex: 1;\n}\n\n// Variation specific styles\n.comment-editor-container,\n.basic-editor-container {\n  border-left: 1px solid #DDD;\n  border-right: 1px solid #DDD;\n  border-bottom: 1px solid #DDD;\n  border-radius: 3px;\n  @include mixins.lightDark(border-color, #DDD, #000);\n\n  .editor-toolbar-main {\n    border-radius: 3px 3px 0 0;\n    justify-content: end;\n  }\n}\n\n.basic-editor-container .editor-content-area {\n  padding-bottom: 0;\n}\n\n// Buttons\n.editor-button {\n  font-size: 12px;\n  padding: 4px;\n  color: #444;\n  @include mixins.lightDark(color, #444, #999);\n  border-radius: 4px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  margin: 2px;\n}\n.editor-button:hover {\n  background-color: #EEE;\n  @include mixins.lightDark(background-color, #EEE, #333);\n  cursor: pointer;\n  color: #000;\n}\n.editor-button[disabled] {\n  pointer-events: none;\n  cursor: not-allowed;\n  opacity: .6;\n}\n.editor-button-active, .editor-button-active:hover {\n  @include mixins.lightDark(background-color, #ceebff, #444);\n  color: #000;\n}\n.editor-button-long {\n  display: flex !important;\n  flex-direction: row;\n  align-items: center;\n  justify-content: start;\n  gap: .5rem;\n}\n.editor-button-text {\n  font-weight: 400;\n  @include mixins.lightDark(color, #000, #AAA);\n  font-size: 14px;\n  flex: 1;\n  padding-inline-end: 4px;\n}\n.editor-button-format-preview {\n  padding: 4px 6px;\n  display: block;\n}\n.editor-button-long .editor-button-icon {\n  width: 24px;\n  height: 24px;\n}\n.editor-button-icon svg {\n  width: 24px;\n  height: 24px;\n  color: inherit;\n  fill: currentColor;\n  display: block;\n}\n.editor-menu-button-icon {\n  width: 24px;\n  height: 24px;\n  svg {\n    fill: #888;\n  }\n}\n.editor-container[dir=\"rtl\"] .editor-menu-button-icon {\n  rotate: 180deg;\n}\n.editor-button-with-menu-container {\n  display: flex;\n  flex-direction: row;\n  gap: 0;\n  align-items: stretch;\n  border-radius: 4px;\n  .editor-dropdown-menu-container {\n    display: flex;\n  }\n  .editor-dropdown-menu-container > .editor-dropdown-menu {\n    top: 100%;\n  }\n  .editor-dropdown-menu-container > .editor-button {\n    padding-inline: 4px;\n    margin-inline-start: -3px;\n    svg {\n      width: 12px;\n      height: 12px;\n    }\n  }\n  &:hover {\n    outline: 1px solid;\n    @include mixins.lightDark(outline-color, #DDD, #111);\n    outline-offset: -3px;\n  }\n}\n\n// Containers\n.editor-dropdown-menu-container {\n    position: relative;\n}\n.editor-dropdown-menu {\n  position: absolute;\n  border: 1px solid;\n  @include mixins.lightDark(background-color, #FFF, #292929);\n  @include mixins.lightDark(border-color, #FFF, #333);\n  @include mixins.lightDark(box-shadow, 0 0 6px 0 rgba(0, 0, 0, 0.15), 0 1px 4px 0 rgba(0, 0, 0, 0.4));\n  z-index: 99;\n  display: flex;\n  flex-direction: row;\n  border-radius: 3px;\n}\n.editor-dropdown-menu-vertical {\n  display: flex;\n  flex-direction: column;\n  align-items: stretch;\n  min-width: 160px;\n}\n.editor-dropdown-menu-vertical .editor-button {\n  border-bottom: 0;\n  text-align: start;\n  display: block;\n  width: 100%;\n}\n.editor-dropdown-menu-vertical > .editor-dropdown-menu-container .editor-dropdown-menu {\n  inset-inline-start: 100%;\n  top: 0;\n}\n\n.editor-separator {\n  display: block;\n  height: 1px;\n  opacity: .8;\n  @include mixins.lightDark(background-color, #DDD, #000);\n}\n\n.editor-format-menu-toggle {\n  width: 130px;\n  height: 32px;\n  font-size: 13px;\n  overflow: hidden;\n  padding-inline: 12px;\n  justify-content: start;\n  background-image: url('data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"%23666\" d=\"M7.41 8L12 12.58 16.59 8 18 9.41l-6 6-6-6z\"/></svg>');\n  background-repeat: no-repeat;\n  background-position: 98% 50%;\n  background-size: 28px;\n}\n.editor-container[dir=\"rtl\"] .editor-format-menu-toggle {\n  background-position: 2% 50%;\n}\n.editor-format-menu .editor-dropdown-menu {\n  min-width: 300px;\n  .editor-dropdown-menu {\n    min-width: 220px;\n  }\n  .editor-button-icon {\n    display: none;\n  }\n}\n.editor-format-menu .editor-dropdown-menu .editor-dropdown-menu-container > .editor-button {\n  padding: 8px 10px;\n}\n\n.editor-overflow-container {\n  display: flex;\n  border-inline: 1px solid #DDD;\n  padding-inline: 4px;\n  @include mixins.lightDark(border-color, #DDD, #000);\n  &:first-child {\n    border-inline-start: none;\n  }\n  &:last-child {\n    border-inline-end: none;\n  }\n  + .editor-overflow-container {\n    border-inline-start: none;\n  }\n}\n\n.editor-context-toolbar {\n  position: fixed;\n  border: 1px solid #DDD;\n  @include mixins.lightDark(background-color, #FFF, #222);\n  @include mixins.lightDark(border-color, #DDD, #333);\n  @include mixins.lightDark(box-shadow, 0 2px 4px 0 rgba(0, 0, 0, 0.12), 0 1px 4px 0 rgba(0, 0, 0, 0.4));\n  padding: .2rem;\n  border-radius: 4px;\n  display: flex;\n  flex-direction: row;\n  &:before {\n    content: '';\n    z-index: -1;\n    display: block;\n    width: 8px;\n    height: 8px;\n    position: absolute;\n    @include mixins.lightDark(background-color, #FFF, #222);\n    border-top: 1px solid #DDD;\n    border-left: 1px solid #DDD;\n    @include mixins.lightDark(border-color, #DDD, #333);\n    transform: rotate(45deg);\n    left: 50%;\n    margin-left: -4px;\n    top: -5px;\n  }\n  &.is-above:before {\n    top: calc(100% - 5px);\n    transform: rotate(225deg);\n  }\n}\n\n// Modals\n.editor-modal-wrapper {\n  position: fixed;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  z-index: 999;\n  background-color: rgba(0, 0, 0, 0.5);\n  width: 100%;\n  height: 100%;\n}\n.editor-modal {\n  @include mixins.lightDark(background-color, #FFF, #222);\n  border-radius: 4px;\n  overflow: hidden;\n  box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.3);\n  margin: vars.$xs;\n  max-height: 100%;\n  overflow-y: auto;\n}\n.editor-modal-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: stretch;\n  background-color: var(--color-primary);\n  color: #FFF;\n}\n.editor-modal-title {\n  padding: 8px vars.$m;\n}\n.editor-modal-close {\n  color: #FFF;\n  padding: 8px vars.$m;\n  align-items: center;\n  justify-content: center;\n  cursor: pointer;\n  &:hover {\n  background-color: rgba(255, 255, 255, 0.1);\n  }\n  svg {\n    width: 1rem;\n    height: 1rem;\n    fill: currentColor;\n    display: block;\n  }\n}\n.editor-modal-body {\n  padding: vars.$m;\n}\n\n// Specific UI elements\n.editor-color-select-row {\n  display: flex;\n}\n.editor-color-select-option {\n  width: 28px;\n  height: 28px;\n  cursor: pointer;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n.editor-color-select-option:hover {\n  border-radius: 3px;\n  box-sizing: border-box;\n  z-index: 3;\n  box-shadow: 0 0 4px 1px rgba(0, 0, 0, 0.25);\n}\n.editor-color-select-option[data-color=\"\"] svg {\n  width: 20px;\n  height: 20px;\n  fill: #888;\n}\n.editor-table-creator-row {\n  display: flex;\n}\n.editor-table-creator-cell {\n  border: 1px solid;\n  @include mixins.lightDark(border-color, #DDD, #000);\n  width: 15px;\n  height: 15px;\n  cursor: pointer;\n  &.active {\n    background-color: var(--editor-color-primary);\n  }\n}\n.editor-table-creator-display {\n  text-align: center;\n  padding: 0.2em;\n}\n.editor-external-content {\n  min-width: 500px;\n  min-height: 500px;\n  h4:first-child {\n    margin-top: 0;\n  }\n}\n\n// In-editor elements\n.editor-image-wrap {\n  position: relative;\n  display: inline-flex;\n}\n.editor-node-resizer {\n  position: absolute;\n  left: 0;\n  right: auto;\n  display: inline-block;\n  outline: 2px dashed var(--editor-color-primary);\n  direction: ltr;\n  pointer-events: none;\n}\n.editor-node-resizer-handle {\n  pointer-events: auto;\n  position: absolute;\n  display: block;\n  width: 10px;\n  height: 10px;\n  border: 2px solid var(--editor-color-primary);\n  z-index: 3;\n  @include mixins.lightDark(background-color, #FFF, #000);\n  user-select: none;\n  &.nw {\n    inset-inline-start: -5px;\n    inset-block-start: -5px;\n    cursor: nw-resize;\n  }\n  &.ne {\n    inset-inline-end: -5px;\n    inset-block-start: -5px;\n    cursor: ne-resize;\n  }\n  &.se {\n    inset-inline-end: -5px;\n    inset-block-end: -5px;\n    cursor: se-resize;\n  }\n  &.sw {\n    inset-inline-start: -5px;\n    inset-block-end: -5px;\n    cursor: sw-resize;\n  }\n}\n.editor-node-resizer-ghost {\n  opacity: 0.5;\n  display: none;\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 100%;\n  height: 100%;\n  z-index: 2;\n  pointer-events: none;\n  background-color: var(--editor-color-primary);\n}\n.editor-node-resizer.active .editor-node-resizer-ghost {\n  display: block;\n}\n.editor-content-area details[contenteditable=\"false\"],\n.editor-content-area summary[contenteditable=\"false\"] {\n  user-select: none;\n}\n.editor-content-area details[contenteditable=\"false\"] > details * {\n  pointer-events: none;\n}\n.editor-content-area details summary {\n  caret-color: transparent;\n}\n.editor-content-area details.selected {\n  outline: 1px dashed var(--editor-color-primary);\n  outline-offset: 1px;\n}\n.editor-content-area  [drawio-diagram] {\n  cursor: pointer;\n}\n\n.editor-table-marker {\n  position: fixed;\n  background-color: var(--editor-color-primary);\n  z-index: 3;\n  user-select: none;\n  opacity: 0;\n  &:hover, &.active {\n    opacity: 0.4;\n  }\n}\n.editor-table-marker-column {\n  width: 4px;\n  cursor: col-resize;\n}\n.editor-table-marker-row {\n  height: 4px;\n  cursor: row-resize;\n}\n\n.editor-code-block-wrap {\n  user-select: none;\n  > * {\n    pointer-events: none;\n  }\n  &.selected .cm-editor {\n    border: 1px dashed var(--editor-color-primary);\n  }\n}\n.editor-diagram.selected {\n  outline: 2px dashed var(--editor-color-primary);\n}\n\n.editor-media-wrap {\n  display: inline-block;\n  cursor: not-allowed;\n  iframe, video {\n    pointer-events: none;\n  }\n  &.align-left {\n    float: left;\n  }\n  &.align-right {\n    float: right;\n  }\n  &.align-center {\n    display: block;\n    margin-inline: auto;\n  }\n}\n\n/**\n * Fake task list checkboxes\n */\n.editor-content-area .task-list-item {\n  margin-left: 0;\n  position: relative;\n}\n.editor-content-area .task-list-item > input[type=\"checkbox\"] {\n  display: none;\n}\n.editor-content-area .task-list-item:before {\n  content: '';\n  display: inline-block;\n  border: 2px solid #CCC;\n  width: 12px;\n  height: 12px;\n  border-radius: 2px;\n  margin-right: 8px;\n  vertical-align: text-top;\n  cursor: pointer;\n  position: absolute;\n  left: -24px;\n  top: 4px;\n}\n.editor-content-area .task-list-item[checked]:before {\n  background-color: #CCC;\n  background-image: url('data:image/svg+xml;utf8,<svg fill=\"%23FFFFFF\" version=\"1.1\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"m8.4856 20.274-6.736-6.736 2.9287-2.7823 3.8073 3.8073 10.836-10.836 2.9287 2.9287z\" stroke-width=\"1.4644\"/></svg>');\n  background-position: 50% 50%;\n  background-size: 100% 100%;\n}\n\n/**\n * Form elements\n */\n$inputWidth: 260px;\n\n.editor-form-field-wrapper {\n  margin-bottom: .5rem;\n}\n.editor-form-field-input {\n  display: block;\n  width: $inputWidth;\n  min-width: 100px;\n  max-width: 100%;\n  border: 1px solid;\n  @include mixins.lightDark(border-color, #DDD, #000);\n  padding: .5rem;\n  border-radius: 4px;\n  @include mixins.lightDark(color, #444, #BBB);\n}\n\n@include mixins.smaller-than(vars.$bp-xs) {\n  .editor-form-field-input {\n    min-width: 160px;\n  }\n}\n\ntextarea.editor-form-field-input {\n  font-family: var(--font-code);\n  width: 350px;\n  height: 250px;\n  font-size: 12px;\n}\n.editor-form-field-label {\n  color: #444;\n  font-weight: 700;\n  font-size: 12px;\n}\n.editor-form-actions {\n  display: flex;\n  justify-content: end;\n  gap: vars.$s;\n  margin-top: vars.$m;\n}\n.editor-form-actions > button {\n  display: block;\n  font-size: 0.85rem;\n  line-height: 1.4em;\n  padding: vars.$xs*1.3 vars.$m;\n  font-weight: 400;\n  border-radius: 4px;\n  cursor: pointer;\n  box-shadow: none;\n  &:focus {\n    outline: 1px dotted currentColor;\n    outline-offset: -(vars.$xs);\n    box-shadow: none;\n    filter: brightness(90%);\n  }\n}\n.editor-form-action-primary {\n  background-color: var(--color-primary);\n  color: #FFF;\n  border: 1px solid var(--color-primary);\n  &:hover {\n    @include mixins.lightDark(box-shadow, vars.$bs-light, vars.$bs-dark);\n    filter: brightness(110%);\n  }\n}\n.editor-form-action-secondary {\n  border: 1px solid;\n  @include mixins.lightDark(border-color, #CCC, #666);\n  @include mixins.lightDark(color, #666, #AAA);\n  &:hover, &:focus, &:active {\n    @include mixins.lightDark(color, #444, #BBB);\n    border: 1px solid #CCC;\n    box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1);\n    background-color: #F2F2F2;\n    @include mixins.lightDark(background-color, #f8f8f8, #444);\n    filter: none;\n  }\n  &:active {\n    border-color: #BBB;\n    background-color: #DDD;\n    color: #666;\n    box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.1);\n  }\n}\n.editor-form-tab-container {\n  display: flex;\n  flex-direction: row;\n  gap: 2rem;\n}\n.editor-form-tab-controls {\n  display: flex;\n  flex-direction: column;\n  align-items: stretch;\n  gap: .25rem;\n}\n\n@include mixins.smaller-than(vars.$bp-m) {\n  .editor-form-tab-container {\n    flex-direction: column;\n    gap: .5rem;\n  }\n  .editor-form-tab-controls {\n    flex-direction: row;\n  }\n}\n\n.editor-form-tab-control {\n  font-weight: bold;\n  font-size: 14px;\n  @include mixins.lightDark(color, #444, #666);\n  border-bottom: 2px solid transparent;\n  position: relative;\n  cursor: pointer;\n  padding: .25rem .5rem;\n  text-align: start;\n  &[aria-selected=\"true\"] {\n    border-color: var(--editor-color-primary);\n    color: var(--editor-color-primary) !important;\n  }\n  &[aria-selected=\"true\"]:after, &:hover:after {\n    background-color: var(--editor-color-primary);\n    opacity: .15;\n    content: '';\n    display: block;\n    position: absolute;\n    left: 0;\n    top: 0;\n    width: 100%;\n    height: 100%;\n  }\n}\n.editor-form-tab-contents {\n  width: $inputWidth;\n  max-width: 100%;\n}\n.editor-action-input-container {\n  display: flex;\n  flex-direction: row;\n  align-items: end;\n  justify-content: space-between;\n  gap: .1rem;\n  .editor-button {\n    margin-bottom: 12px;\n  }\n  input {\n    width: $inputWidth - 40px;\n  }\n}\n.editor-color-field-container {\n  position: relative;\n  input {\n    padding-left: 36px;\n  }\n  .editor-dropdown-menu-container {\n    position: absolute;\n    bottom: 0;\n  }\n}\n\n// Specific field styles\ntextarea.editor-form-field-input[name=\"source\"] {\n  width: 1000px;\n  height: 600px;\n  max-height: 60vh;\n  max-width: 80vw;\n}\n\n// Editor theme styles\n.editor-theme-bold {\n  font-weight: bold;\n}\n.editor-theme-italic {\n  font-style: italic;\n}\n.editor-theme-strikethrough {\n  text-decoration-line: line-through;\n}\n.editor-theme-underline {\n  text-decoration-line: underline;\n}\n.editor-theme-underline-strikethrough {\n  text-decoration: underline line-through;\n}"
  },
  {
    "path": "resources/sass/_footer.scss",
    "content": "/**\n * Includes the footer links.\n */\n\n footer {\n    flex-shrink: 0;\n    padding: 1rem 1rem 2rem 1rem;\n    text-align: center;\n  }\n  \n  footer a {\n    margin: 0 .5em;\n  }\n  \n  body.flexbox footer {\n    display: none;\n  }"
  },
  {
    "path": "resources/sass/_forms.scss",
    "content": "@use \"sass:math\";\n\n@use \"mixins\";\n@use \"vars\";\n\n\n.input-base {\n  border-radius: 3px;\n  border: 1px solid #D4D4D4;\n  @include mixins.lightDark(background-color, #fff, #333);\n  @include mixins.lightDark(border-color, #d4d4d4, #111);\n  @include mixins.lightDark(color, #666, #AAA);\n  display: inline-block;\n  font-size: vars.$fs-m;\n  padding: vars.$xs*1.8;\n  height: 40px;\n  width: 250px;\n  max-width: 100%;\n\n  &.neg, &.invalid {\n    border: 1px solid var(--color-negative);\n  }\n  &.pos, &.valid {\n    border: 1px solid var(--color-positive);\n  }\n  &.disabled, &[disabled] {\n    background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAMUlEQVQIW2NkwAGuXbv2nxGbHEhCS0uLEUMSJgHShCKJLIEiiS4Bl8QmAZbEJQGSBAC62BuJ+tt7zgAAAABJRU5ErkJggg==);\n  }\n  &[readonly] {\n    background-color: #f8f8f8;\n  }\n  &:focus {\n    border-color: var(--color-primary);\n    outline: 1px solid var(--color-primary);\n  }\n}\n\n.input-fill-width {\n  width: 100% !important;\n}\n\n.fake-input {\n  @extend .input-base;\n  overflow: auto;\n}\n\n#html-editor {\n  display: none;\n}\n\n#markdown-editor {\n  position: relative;\n  z-index: 5;\n  #markdown-editor-input {\n    font-style: normal;\n    font-weight: 400;\n    padding: vars.$xs vars.$m;\n    @include mixins.lightDark(color, #444, #aaa);\n    @include mixins.lightDark(background-color, #fff, #222);\n    border-radius: 0;\n    height: 100%;\n    font-size: 14px;\n    line-height: 1.2;\n    max-height: 100%;\n    flex: 1;\n    border: 0;\n    width: 100%;\n    margin: 0;\n    &:focus {\n      outline: 0;\n    }\n  }\n  &.fullscreen {\n    position: fixed;\n    top: 0;\n    left: 0;\n    height: 100%;\n    z-index: 2;\n  }\n}\n\n.markdown-editor-wrap {\n  border-top: 1px solid #DDD;\n  border-bottom: 1px solid #DDD;\n  @include mixins.lightDark(border-color, #ddd, #000);\n  position: relative;\n  flex: 1;\n  min-width: 0;\n}\n.markdown-editor-wrap + .markdown-editor-wrap {\n  flex-basis: 50%;\n  flex-shrink: 0;\n  flex-grow: 0;\n}\n\n.markdown-editor-wrap .cm-editor {\n  flex: 1;\n  max-width: 100%;\n  border: 0;\n  margin: 0;\n}\n\n.markdown-panel-divider {\n  width: 2px;\n  @include mixins.lightDark(background-color, #ddd, #000);\n  cursor: col-resize;\n}\n\n@include mixins.smaller-than(vars.$bp-m) {\n  #markdown-editor {\n    flex-direction: column;\n  }\n  #markdown-editor .markdown-editor-wrap {\n    width: 100%;\n    max-width: 100%;\n    flex-grow: 1;\n    flex-basis: auto !important;\n    min-height: 0;\n  }\n  .editor-toolbar-label {\n    float: none !important;\n    @include mixins.lightDark(border-color, #DDD, #555);\n    display: block;\n  }\n  .markdown-editor-wrap:not(.active) .editor-toolbar + div,\n  .markdown-editor-wrap:not(.active) .editor-toolbar .buttons,\n  .markdown-editor-wrap:not(.active) .markdown-display {\n    display: none;\n  }\n  #markdown-editor .markdown-editor-wrap:not(.active) {\n    flex-grow: 0;\n    flex: none;\n  }\n}\n\n.markdown-editor-display {\n  background-color: #fff;\n  body {\n    display: block;\n    background-color: #fff;\n    padding-inline-start: 12px;\n    padding-inline-end: 12px;\n    max-width: 864px;\n  }\n  [drawio-diagram] {\n    cursor: pointer;\n  }\n  [drawio-diagram]:hover {\n    outline: 2px solid var(--color-primary);\n  }\n}\n\nhtml.markdown-editor-display.dark-mode {\n  background-color: #222;\n  body {\n    background-color: #222;\n  }\n}\n\n.editor-toolbar {\n  height: 32px;\n  width: 100%;\n  font-size: 11px;\n  line-height: 1.6;\n  border-bottom: 1px solid #CCC;\n  @include mixins.lightDark(background-color, #FFF, #333);\n  @include mixins.lightDark(border-color, #CCC, #000);\n  flex: none;\n  @include mixins.whenDark {\n    button {\n      color: #AAA;\n    }\n  }\n}\n\n.editor-toolbar .buttons {\n  font-size: vars.$fs-m;\n  .dropdown-menu {\n    padding: 0;\n  }\n  .toggle-switch {\n    margin: vars.$s 0;\n  }\n}\n\n.editor-toolbar .buttons button {\n  font-size: .9rem;\n  width: 2rem;\n  text-align: center;\n  border-left: 1px solid;\n  @include mixins.lightDark(border-color, #DDD, #555);\n  svg {\n    margin-inline-end: 0;\n  }\n  &:hover {\n    @include mixins.lightDark(background-color, #DDD, #222);\n  }\n}\n\n\nlabel {\n  @include mixins.lightDark(color, #666, #ddd);\n  display: block;\n  line-height: 1.4em;\n  font-size: 0.94em;\n  font-weight: 400;\n  padding-bottom: 2px;\n  margin-bottom: 0.2em;\n  &.inline {\n    display: inline-block;\n  }\n}\n\nlabel.radio, label.checkbox {\n  font-weight: 400;\n  user-select: none;\n  input[type=\"radio\"], input[type=\"checkbox\"] {\n    margin-inline-end: vars.$xs;\n  }\n}\n\nlabel.inline.checkbox {\n  margin-inline-end: vars.$m;\n}\n\nlabel + p.small {\n  margin-bottom: 0.8em;\n}\n\ntable.form-table {\n  max-width: 100%;\n  td {\n    overflow: hidden;\n    padding: math.div(vars.$xxs, 2) 0;\n  }\n}\n\ninput[type=\"text\"], input[type=\"number\"], input[type=\"email\"], input[type=\"date\"], input[type=\"search\"], input[type=\"url\"],\ninput[type=\"color\"], input[type=\"password\"], select, textarea {\n  @extend .input-base;\n}\n\nselect {\n  -webkit-appearance: none;\n  -moz-appearance: none;\n  appearance: none;\n  background: url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='%23666666'><polygon points='0,0 100,0 50,50'/></svg>\");\n  background-size: 10px 12px;\n  background-position: calc(100% - 20px) 64%;\n  background-repeat: no-repeat;\n\n  @include mixins.rtl {\n    background-position: 20px 70%;\n  }\n}\n\ninput[type=date] {\n  width: 190px;\n}\n\ninput[type=color] {\n  height: 60px;\n  &.small {\n    height: 42px;\n    width: 60px;\n    padding: 2px;\n  }\n}\n\n.toggle-switch {\n  user-select: none;\n  display: inline-grid;\n  grid-template-columns: (16px + vars.$s) 1fr;\n  align-items: center;\n  margin: vars.$m 0;\n  .custom-checkbox {\n    width: 16px;\n    height: 16px;\n    border-radius: 2px;\n    display: inline-block;\n    border: 2px solid currentColor;\n    overflow: hidden;\n    fill: currentColor;\n    .svg-icon {\n      width: 100%;\n      height: 100%;\n      margin: 0;\n      bottom: auto;\n      top: -1.5px;\n      left: 0;\n      transition: transform ease-in-out 120ms;\n      transform: scale(0);\n      transform-origin: center center;\n    }\n  }\n  input[type=checkbox] {\n    display: none;\n  }\n  input[type=checkbox]:checked + .custom-checkbox .svg-icon {\n    transform: scale(1);\n  }\n  .custom-checkbox:hover {\n    background-color: rgba(0, 0, 0, 0.05);\n    opacity: 0.8;\n  }\n  input[type=checkbox][disabled] ~ * {\n    opacity: 0.8;\n    cursor: not-allowed;\n  }\n  input[type=checkbox][disabled] ~ .custom-checkbox {\n    border-color: #999;\n    color: #999 !important;\n    background: #f2f2f2;\n  }\n}\n.toggle-switch-list {\n  .toggle-switch {\n    margin: vars.$xs 0;\n  }\n  &.compact .toggle-switch {\n    margin: 1px 0;\n  }\n}\n\n.form-group {\n  margin-bottom: vars.$s;\n}\n\n.setting-list > div {\n  border-bottom: 1px solid #DDD;\n  padding: vars.$xl 0;\n  &:last-child {\n    border-bottom: none;\n  }\n}\n.setting-list-label {\n  @include mixins.lightDark(color, #222, #DDD);\n  color: #222;\n  font-size: 1rem;\n}\n.setting-list-label + p.small {\n  margin-bottom: 0;\n}\n.setting-list-label + .grid {\n  margin-top: vars.$m;\n}\n\n.setting-list .grid, .stretch-inputs {\n  input[type=text], input[type=email], input[type=password], select {\n    width: 100%;\n  }\n}\n\n.small-inputs input {\n  width: 150px;\n}\n\n.simple-code-input {\n  background-color: #F8F8F8;\n  font-family: monospace;\n  font-size: 12px;\n  min-height: 100px;\n  display: block;\n  width: 100%;\n}\n\n.form-group {\n  div.text-pos, div.text-neg, p.text-post, p.text-neg {\n    padding: vars.$xs 0;\n  }\n}\n\n.form-group.collapsible {\n  padding: 0 vars.$m;\n  border: 1px solid;\n  @include mixins.lightDark(border-color, #DDD, #000);\n  border-radius: 4px;\n  .collapse-title {\n    margin-inline-start: -(vars.$m);\n    margin-inline-end: -(vars.$m);\n    padding: vars.$s vars.$m;\n    display: block;\n    width: calc(100% + 32px);\n    text-align: start;\n  }\n  .collapse-title, .collapse-title label {\n    cursor: pointer;\n  }\n  .collapse-title label {\n    padding-bottom: 0;\n    margin-bottom: 0;\n    color: inherit;\n  }\n  .collapse-title label:before {\n    display: inline-block;\n    content: '▸';\n    margin-inline-end: vars.$m;\n    transition: all ease-in-out 400ms;\n    transform: rotate(0);\n  }\n  .collapse-content {\n    display: none;\n    padding-bottom: vars.$m;\n  }\n  &.open .collapse-title label:before {\n    transform: rotate(90deg);\n  }\n}\n\n.form-group.ambrosia-container, .form-group.ambrosia-container * {\n    position:absolute !important;\n    height:1px !important;\n    width:1px !important;\n    margin:-1px !important;\n    padding:0 !important;\n    background:transparent !important;\n    color:transparent !important;\n    border:none !important;\n    overflow: hidden !important;\n    clip: rect(0,0,0,0) !important;\n    white-space: nowrap !important;\n}\n\n.title-input input[type=\"text\"] {\n  display: block;\n  width: 100%;\n  padding: vars.$s;\n  margin-top: 0;\n  font-size: 2em;\n  height: auto;\n}\n\n.description-input textarea {\n  display: block;\n  width: 100%;\n  padding: vars.$s;\n  font-size: vars.$fs-m;\n  color: #666;\n  height: auto;\n}\n\n.description-input > .tox-tinymce {\n  border: 1px solid #DDD !important;\n  @include mixins.lightDark(border-color, #DDD !important, #000 !important);\n  border-radius: 3px;\n  .tox-toolbar__primary {\n    justify-content: end;\n  }\n}\n\n.search-box {\n  max-width: 100%;\n  position: relative;\n  button[tabindex=\"-1\"] {\n    background-color: transparent;\n    border: none;\n    @include mixins.lightDark(color, #666, #AAA);\n    padding: 0;\n    cursor: pointer;\n    position: absolute;\n    inset-inline-start: 8px;\n    top: 10px;\n  }\n  input {\n    display: block;\n    padding: vars.$xs * 1.5;\n    padding-inline-start: vars.$l + 4px;\n    width: 300px;\n    max-width: 100%;\n    height: auto;\n  }\n  &.flexible input {\n    width: 100%;\n  }\n  button.search-box-cancel {\n    left: auto;\n    right: 0;\n  }\n}\n\n.contained-search-box {\n  display: flex;\n  height: 38px;\n  z-index: -1;\n  &.floating {\n    box-shadow: vars.$bs-med;\n    border-radius: 4px;\n    overflow: hidden;\n    @include mixins.whenDark {\n      border: 1px solid #000;\n    }\n  }\n  input, button {\n    height: 100%;\n    border-radius: 0;\n    border: 1px solid #ddd;\n    @include mixins.lightDark(border-color, #ddd, #000);\n    margin-inline-start: -1px;\n    &:last-child {\n      border-inline-end: 0;\n    }\n  }\n  input {\n    border: 0;\n    flex: 5;\n    padding: vars.$xs vars.$s;\n    &:focus, &:active {\n      outline: 1px dotted var(--color-primary);\n      outline-offset: -2px;\n      border: 0;\n    }\n  }\n  button {\n    border: 0;\n    width: 48px;\n    border-inline-start: 1px solid #DDD;\n    background-color: #FFF;\n    @include mixins.lightDark(background-color, #FFF, #333);\n    @include mixins.lightDark(color, #444, #AAA);\n  }\n  button:focus {\n    outline: 1px dotted var(--color-primary);\n    outline-offset: -2px;\n  }\n  svg {\n    margin: 0;\n  }\n  @include mixins.smaller-than(vars.$bp-s) {\n    width: 180px;\n  }\n}\n\n.outline > input {\n  border: 0;\n  border-bottom: 2px solid #DDD;\n  border-radius: 0;\n  &:focus, &:active {\n    border: 0;\n    border-bottom: 2px solid #AAA;\n    outline: 0;\n  }\n}\n\n\n.image-picker img {\n  background-color: #BBB;\n  max-width: 100%;\n}\n\n.custom-file-input {\n  overflow: hidden;\n  padding: 0;\n  position: absolute;\n  white-space: nowrap;\n  width: 1px;\n  height: 1px;\n  border: 0;\n  clip: rect(0, 0, 0, 0);\n}\n.custom-file-input:focus + label {\n  border-color: var(--color-primary);\n  outline: 1px solid var(--color-primary);\n}\n\n.custom-simple-file-input {\n  max-width: 100%;\n  border: 1px solid;\n  @include mixins.lightDark(border-color, #DDD, #666);\n  border-radius: 4px;\n  padding: vars.$s vars.$m;\n}\n.custom-simple-file-input::file-selector-button {\n  background-color: transparent;\n  text-decoration: none;\n  font-size: 0.8rem;\n  line-height: 1.4em;\n  padding: vars.$xs vars.$s;\n  border: 1px solid;\n  font-weight: 400;\n  outline: 0;\n  border-radius: 4px;\n  cursor: pointer;\n  margin-right: vars.$m;\n  @include mixins.lightDark(color, #666, #AAA);\n  @include mixins.lightDark(border-color, #CCC, #666);\n  &:hover, &:focus, &:active {\n    @include mixins.lightDark(color, #444, #BBB);\n    border: 1px solid #CCC;\n    box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1);\n    background-color: #F2F2F2;\n    @include mixins.lightDark(background-color, #f8f8f8, #444);\n    filter: none;\n  }\n  &:active {\n    border-color: #BBB;\n    background-color: #DDD;\n    color: #666;\n    box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.1);\n  }\n}\n\ninput.shortcut-input {\n  width: auto;\n  max-width: 120px;\n  height: auto;\n}\n"
  },
  {
    "path": "resources/sass/_header.scss",
    "content": "@use \"mixins\";\n@use \"vars\";\n\n/**\n * Includes the main navigation header and the faded toolbar.\n */\n\nheader.grid {\n  grid-template-columns: minmax(max-content, 2fr) 1fr minmax(max-content, 2fr);\n}\n\n@include mixins.smaller-than(vars.$bp-l) {\n  header.grid {\n    grid-template-columns: 1fr;\n    grid-row-gap: 0;\n  }\n}\n\nheader {\n  position: relative;\n  display: block;\n  z-index: 11;\n  top: 0;\n  color: rgb(250, 250, 250);\n  border-bottom: 1px solid #DDD;\n  box-shadow: vars.$bs-card;\n  @include mixins.lightDark(border-bottom-color, #DDD, #000);\n  .header-links {\n    display: flex;\n    align-items: center;\n    justify-content: end;\n  }\n  .links {\n    display: inline-block;\n    vertical-align: top;\n  }\n  .links a {\n    display: inline-block;\n    padding: 10px vars.$m;\n    color: #FFF;\n    border-radius: 3px;\n  }\n  .links a:hover {\n    text-decoration: none;\n    background-color: rgba(255, 255, 255, .15);\n  }\n  .dropdown-container {\n    padding-inline-start: vars.$m;\n    padding-inline-end: 0;\n  }\n  .avatar, .user-name {\n    display: inline-block;\n  }\n  .avatar {\n    width: 30px;\n    height: 30px;\n  }\n  .user-name {\n    vertical-align: top;\n    position: relative;\n    display: inline-flex;\n    align-items: center;\n    cursor: pointer;\n    padding: vars.$s;\n    margin: 0 (-(vars.$s));\n    border-radius: 3px;\n    gap: vars.$xs;\n    color: #FFF;\n    > span {\n      padding-inline-start: vars.$xs;\n      display: inline-block;\n      line-height: 1;\n    }\n    > svg {\n      font-size: 18px;\n      margin-top: -2px;\n      margin-inline-end: 0;\n    }\n    &:hover {\n      background-color: rgba(255, 255, 255, 0.15);\n    }\n    @include mixins.between(vars.$bp-l, vars.$bp-xl) {\n      padding-inline-start: vars.$xs;\n      .name {\n        display: none;\n      }\n    }\n  }\n}\n\n.header *, .primary-background * {\n  outline-color: #FFF;\n}\n\n\nheader .search-box {\n  display: inline-block;\n  input {\n    background-color: rgba(0, 0, 0, 0.2);\n    border: 1px solid rgba(255, 255, 255, 0.2);\n    border-radius: 40px;\n    color: #EEE;\n    z-index: 2;\n    height: auto;\n    padding: vars.$xs*1.5;\n    padding-inline-start: 40px;\n    &:focus {\n      outline: none;\n      border: 1px solid rgba(255, 255, 255, 0.4);\n    }\n  }\n  input::placeholder {\n    color: #FFF;\n    opacity: 0.6;\n  }\n  @include mixins.between(vars.$bp-l, vars.$bp-xl) {\n    max-width: 200px;\n  }\n  &:focus-within #header-search-box-button {\n    opacity: 1;\n  }\n}\n#header-search-box-button {\n  z-index: 1;\n  inset-inline-start: 16px;\n  top: 10px;\n  color: #FFF;\n  opacity: 0.6;\n  @include mixins.lightDark(color, rgba(255, 255, 255, 0.8), #AAA);\n  svg {\n    margin-inline-end: 0;\n  }\n}\n\n.global-search-suggestions {\n  display: none;\n  position: absolute;\n  top: -(vars.$s);\n  left: 0;\n  right: 0;\n  z-index: -1;\n  margin-left: -(vars.$xxl);\n  margin-right: -(vars.$xxl);\n  padding-top: 56px;\n  border-radius: 3px;\n  box-shadow: vars.$bs-hover;\n  transform-origin: top center;\n  opacity: .5;\n  transform: scale(0.9);\n  .entity-item-snippet p  {\n    display: none;\n  }\n  .entity-item-snippet {\n    font-size: .8rem;\n  }\n  .entity-list-item-name {\n    font-size: .9rem;\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n    -webkit-line-clamp: 2;\n    overflow: hidden;\n  }\n  .global-search-loading {\n    position: absolute;\n    width: 100%;\n  }\n}\nheader .search-box.search-active:focus-within {\n  .global-search-suggestions {\n    display: block;\n  }\n  input {\n    @include mixins.lightDark(background-color, #EEE, #333);\n    @include mixins.lightDark(border-color, #DDD, #111);\n  }\n  #header-search-box-button, input {\n    @include mixins.lightDark(color, #444, #AAA);\n  }\n}\n\n.logo {\n  display: inline-flex;\n  padding: (vars.$s - 6px) vars.$s;\n  margin: 6px (-(vars.$s));\n  gap: vars.$s;\n  align-items: center;\n  border-radius: 4px;\n  &:hover {\n    color: #FFF;\n    text-decoration: none;\n    background-color: rgba(255, 255, 255, .15);\n  }\n}\n\n.logo-text {\n  font-size: 1.8em;\n  color: #fff;\n  font-weight: 400;\n  line-height: 1;\n}\n.logo-image {\n  height: 43px;\n}\n\n.mobile-menu-toggle {\n  color: #FFF;\n  fill: #FFF;\n  font-size: 2em;\n  border: 2px solid rgba(255, 255, 255, 0.8);\n  border-radius: 4px;\n  padding: 0 vars.$xs;\n  line-height: 1;\n  cursor: pointer;\n  user-select: none;\n  svg {\n    margin: 0;\n    bottom: -2px;\n  }\n}\n\n\n@include mixins.smaller-than(vars.$bp-l) {\n  header .header-links {\n    @include mixins.lightDark(background-color, #fff, #333);\n    display: none;\n    z-index: 10;\n    inset-inline-end: vars.$m;\n    border-radius: 4px;\n    overflow: hidden;\n    position: absolute;\n    box-shadow: vars.$bs-hover;\n    margin-top: vars.$m;\n    padding: vars.$xs 0;\n    &.show {\n      display: block;\n    }\n  }\n  header .links a, header .dropdown-container ul li a, header .dropdown-container ul li button {\n    text-align: start;\n    display: grid;\n    align-items: center;\n    padding: 8px vars.$m;\n    gap: vars.$m;\n    color: vars.$text-dark;\n    grid-template-columns: 16px auto;\n    line-height: 1.4;\n    @include mixins.lightDark(color, vars.$text-dark, #eee);\n    svg {\n      margin-inline-end: vars.$s;\n      width: 16px;\n    }\n    &:hover {\n      background-color: var(--color-primary-light);\n      color: var(--color-primary);\n      text-decoration: none;\n    }\n    &:focus {\n      @include mixins.lightDark(background-color, #eee, #333);\n      outline-color: var(--color-primary);\n      color: var(--color-primary);\n    }\n  }\n  header .dropdown-container {\n    display: block;\n    padding-inline-start: 0;\n  }\n  header .links {\n    display: block;\n  }\n  header .dropdown-container ul {\n    display: block !important;\n    position: relative;\n    background-color: transparent;\n    border: 0;\n    padding: 0;\n    margin: 0;\n    box-shadow: none;\n  }\n}\n\n.tri-layout-mobile-tabs {\n  position: sticky;\n  top: 0;\n  z-index: 5;\n  background-color: #FFF;\n  border-bottom: 1px solid #DDD;\n  @include mixins.lightDark(border-bottom-color, #DDD, #333);\n  box-shadow: vars.$bs-card;\n}\n.tri-layout-mobile-tab {\n  text-align: center;\n  border-bottom: 3px solid #BBB;\n  cursor: pointer;\n  margin: 0;\n  @include mixins.lightDark(background-color, #FFF, #222);\n  @include mixins.lightDark(border-bottom-color, #BBB, #333);\n  &:first-child {\n    border-inline-end: 1px solid #DDD;\n    @include mixins.lightDark(border-inline-end-color, #DDD, #000);\n  }\n  &[aria-selected=\"true\"] {\n    border-bottom-color: currentColor !important;\n  }\n}\n\n.breadcrumbs {\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  justify-content: flex-start;\n  flex-wrap: wrap;\n  opacity: 0.7;\n  .icon-list-item {\n    width: auto;\n    padding-top: vars.$xs;\n    padding-bottom: vars.$xs;\n  }\n  .separator {\n    display: inline-block;\n    fill: #aaa;\n    font-size: 1.6em;\n    line-height: 0.8;\n    margin: -2px 0 0;\n  }\n  &:hover, &:focus-within {\n    opacity: 1;\n  }\n  @media (prefers-contrast: more) {\n    opacity: 1;\n  }\n}\n\n@include mixins.smaller-than(vars.$bp-l) {\n  .breadcrumbs .icon-list-item {\n    padding: vars.$xs;\n    > span + span {\n      display: none;\n    }\n    > span:first-child {\n      margin-inline-end: 0;\n    }\n  }\n}\n\n.faded {\n  a, button, span, span > div {\n    color: #666;\n    fill: #666;\n  }\n  .text-button {\n    opacity: 0.5;\n    transition: all ease-in-out 120ms;\n    &:hover {\n      opacity: 1;\n      text-decoration: none;\n    }\n  }\n}\n\n.faded span.faded-text {\n  display: inline-block;\n  padding: vars.$s;\n}"
  },
  {
    "path": "resources/sass/_html.scss",
    "content": "@use \"mixins\";\n@use \"vars\";\n\n* {\n  box-sizing: border-box;\n  outline-color: var(--color-primary);\n  outline-width: 1px;\n}\n\n*:focus {\n  outline-style: dotted;\n}\n\nhtml {\n  height: 100%;\n  overflow-y: scroll;\n  background-color: #F2F2F2;\n  &.flexbox {\n    overflow-y: hidden;\n  }\n  &.dark-mode {\n    background-color: #111;\n  }\n}\n\nbody {\n  font-size: vars.$fs-m;\n  line-height: 1.6;\n  @include mixins.lightDark(color, #444, #AAA);\n  -webkit-font-smoothing: antialiased;\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n}\n"
  },
  {
    "path": "resources/sass/_layout.scss",
    "content": "@use \"mixins\";\n@use \"vars\";\n\n\n/**\n * Generic content container\n */\n.container {\n  max-width: vars.$bp-xxl;\n  margin-inline-start: auto;\n  margin-inline-end: auto;\n  padding-inline-start: vars.$m;\n  padding-inline-end: vars.$m;\n  &.medium {\n    max-width: 1100px;\n  }\n  &.small {\n    max-width: 840px;\n  }\n  &.very-small {\n    max-width: 480px;\n  }\n}\n\n/**\n * Core grid layout system\n */\n.grid {\n  display: grid;\n  grid-column-gap: vars.$l;\n  grid-row-gap: vars.$l;\n  > * {\n    min-width: 0;\n  }\n  &.half {\n    grid-template-columns: 1fr 1fr;\n  }\n  &.third {\n    grid-template-columns: 1fr 1fr 1fr;\n  }\n  &.left-focus {\n    grid-template-columns: 2fr 1fr;\n  }\n  &.right-focus {\n    grid-template-columns: 1fr 3fr;\n  }\n  &.gap-y-xs {\n    grid-row-gap: vars.$xs;\n  }\n  &.gap-xl {\n    grid-column-gap: vars.$xl;\n    grid-row-gap: vars.$xl;\n  }\n  &.gap-xxl {\n    grid-column-gap: vars.$xxl;\n    grid-row-gap: vars.$xxl;\n  }\n  &.v-center {\n    align-items: center;\n  }\n  &.v-end {\n    align-items: end;\n  }\n  &.no-gap {\n    grid-row-gap: 0;\n    grid-column-gap: 0;\n  }\n  &.no-row-gap {\n    grid-row-gap: 0;\n  }\n}\n\n@include mixins.smaller-than(vars.$bp-m) {\n  .grid.third:not(.no-break) {\n    grid-template-columns: 1fr 1fr;\n  }\n  .grid.half:not(.no-break), .grid.left-focus:not(.no-break), .grid.right-focus:not(.no-break) {\n    grid-template-columns: 1fr;\n  }\n  .grid.half.collapse-xs {\n    grid-template-columns: 1fr 1fr;\n  }\n  .grid.gap-xl {\n    grid-column-gap: vars.$m;\n    grid-row-gap: vars.$m;\n  }\n  .grid.right-focus.reverse-collapse > *:nth-child(2) {\n    order: 0;\n  }\n  .grid.right-focus.reverse-collapse > *:nth-child(1) {\n    order: 1;\n  }\n}\n\n@include mixins.smaller-than(vars.$bp-s) {\n  .grid.third:not(.no-break) {\n    grid-template-columns: 1fr;\n  }\n}\n\n@include mixins.smaller-than(vars.$bp-xs) {\n  .grid.half.collapse-xs {\n    grid-template-columns: 1fr;\n  }\n}\n\n#content {\n  flex: 1 0 auto;\n}\n\n/**\n * Flexbox layout system\n */\nbody.flexbox {\n  display: flex;\n  flex-direction: column;\n  align-items: stretch;\n  height: 100%;\n  min-height: 100%;\n  max-height: 100%;\n  overflow: hidden;\n  #content {\n    flex: 1;\n    display: flex;\n    min-height: 0;\n  }\n}\n\n.flex-fill {\n  display: flex;\n  align-items: stretch;\n  min-height: 0;\n  max-width: 100%;\n  position: relative;\n}\n\n.flex-container-row {\n  display: flex;\n  flex-direction: row;\n  &.v-center {\n    align-items: center;\n  }\n}\n\n.flex-container-column {\n  display: flex;\n  flex-direction: column;\n}\n\n.flex-container-row.inline, .flex-container-column.inline {\n  display: inline-flex !important;\n}\n\n.flex-container-column.wrap, .flex-container-row.wrap {\n  flex-wrap: wrap;\n}\n\n.flex {\n  min-height: 0;\n  flex: 1;\n  max-width: 100%;\n  &.fit-content {\n    flex-basis: auto;\n    flex-grow: 0;\n  }\n  &.fill-area {\n    flex-grow: 1;\n    flex-shrink: 0;\n    min-width: fit-content;\n  }\n}\n\n.flex-2 {\n  min-height: 0;\n  flex: 2;\n  max-width: 100%;\n}\n\n.flex-3 {\n  min-height: 0;\n  flex: 3;\n  max-width: 100%;\n}\n\n.flex-none {\n  flex: none;\n}\n\n.justify-flex-start {\n  justify-content: flex-start;\n}\n.justify-flex-end {\n  justify-content: flex-end;\n}\n.justify-center {\n  justify-content: center;\n}\n.justify-space-between {\n  justify-content: space-between;\n}\n.items-center {\n  align-items: center;\n}\n.items-stretch {\n  align-items: stretch;\n}\n\n/**\n * Min width utilities\n */\n.min-width-xxxxs {\n  min-width: 60px;\n}\n.min-width-xxxs {\n  min-width: 80px;\n}\n.min-width-xxs {\n  min-width: 100px;\n}\n.min-width-xs {\n  min-width: 120px;\n}\n.min-width-s {\n  min-width: 160px;\n}\n.min-width-m {\n  min-width: 200px;\n}\n.min-width-l {\n  min-width: 240px;\n}\n.min-width-xl {\n  min-width: 280px;\n}\n.min-width-xxl {\n  min-width: 320px;\n}\n\n/**\n * Display and float utilities\n */\n.block {\n  display: block !important;\n  position: relative;\n}\n\n.inline {\n  display: inline !important;\n}\n\n.block.inline {\n  display: inline-block !important;\n}\n\n.relative {\n  position: relative;\n}\n\n.fixed {\n  position: fixed;\n  z-index: 20;\n  &.top-right {\n    top: 0;\n    right: 0;\n  }\n}\n\n.hidden {\n  display: none !important;\n}\n\n.overflow-hidden {\n  overflow: hidden;\n}\n\n.height-fill {\n  height: 100%;\n}\n\n.height-auto {\n  height: auto !important;\n}\n\n.float {\n  float: left;\n  &.right {\n    float: right;\n  }\n}\n\n.sticky-top-m {\n  position: sticky;\n  top: vars.$m;\n}\n\n/**\n * Visibility\n */\n@each $sizeLetter, $size in vars.$screen-sizes {\n  @include mixins.smaller-than($size) {\n    .hide-under-#{$sizeLetter} {\n      display: none !important;\n    }\n  }\n  @include mixins.larger-than($size) {\n    .hide-over-#{$sizeLetter} {\n      display: none !important;\n    }\n  }\n}\n\n[hidden] {\n  display: none !important;\n}\n\n.screen-reader-only {\n  position: absolute;\n  inset-inline-start: -10000px;\n  top: auto;\n  width: 1px;\n  height: 1px;\n  overflow: hidden;\n}\n\n/**\n * Border radiuses\n */\n.rounded {\n  border-radius: 4px;\n}\n\n/**\n * Inline content columns\n */\n.dual-column-content {\n  columns: 2;\n}\n\n@include mixins.smaller-than(vars.$bp-m) {\n  .dual-column-content {\n    columns: 1;\n  }\n}\n\n\n/**\n * Fixes\n */\n.clearfix::before,\n.clearfix::after {\n  content: \" \";\n  display: table;\n}\n.clearfix::after {\n  clear: both;\n}\n\n/**\n * View Layouts\n */\n.tri-layout-container {\n  display: grid;\n  margin-inline-start: vars.$xl;\n  margin-inline-end: vars.$xl;\n  grid-template-columns: 1fr 4fr 1fr;\n  grid-template-areas: \"a b c\";\n  grid-column-gap: vars.$xl;\n  position: relative;\n}\n.tri-layout-sides {\n  grid-column-start: a;\n  grid-column-end: c;\n  grid-row: 1;\n  min-width: 0;\n  z-index: 4;\n}\n.tri-layout-sides-content {\n  display: grid;\n  grid-template-areas: \"a b c\";\n  grid-template-columns: 1fr 4fr 1fr;\n  height: 100%;\n}\n.tri-layout-middle {\n  grid-area: b;\n  padding-top: vars.$m;\n  min-width: 0;\n  z-index: 5;\n}\n.tri-layout-right {\n  grid-area: c;\n  min-width: 0;\n  position: relative;\n}\n.tri-layout-left {\n  grid-area: a;\n  min-width: 0;\n  position: relative;\n}\n\n@include mixins.larger-than(vars.$bp-xxl) {\n  .tri-layout-left-contents, .tri-layout-right-contents {\n    padding: vars.$xl vars.$m;\n    position: sticky;\n    top: 0;\n    max-height: 100vh;\n    min-height: 50vh;\n    overflow-y: scroll;\n    overflow-x: hidden;\n    height: 100%;\n    scrollbar-width: none;\n    -ms-overflow-style: none;\n    &::-webkit-scrollbar {\n      display: none;\n    }\n  }\n  .tri-layout-middle-contents {\n    max-width: 940px;\n    margin: 0 auto;\n  }\n}\n@include mixins.between(vars.$bp-xxl, vars.$bp-xxxl) {\n  .tri-layout-sides-content, .tri-layout-container {\n    grid-template-columns: 1fr calc(940px + (2 * vars.$m)) 1fr;\n  }\n  .tri-layout-container {\n    grid-column-gap: vars.$s;\n    margin-inline-start: vars.$m;\n    margin-inline-end: vars.$m;\n  }\n}\n@include mixins.smaller-than(vars.$bp-xxl) {\n  .tri-layout-container {\n    grid-template-areas:  \"a b b\";\n    grid-template-columns: 1fr 3fr;\n    grid-template-rows: min-content min-content 1fr;\n    margin-inline-start: (vars.$m + vars.$xxs);\n    margin-inline-end: (vars.$m + vars.$xxs);\n  }\n  .tri-layout-sides {\n    grid-column-start: a;\n    grid-column-end: a;\n  }\n  .tri-layout-sides-content {\n    display: block;\n  }\n}\n@include mixins.between(vars.$bp-l, vars.$bp-xxl) {\n  .tri-layout-sides-content {\n    position: sticky;\n    top: 0;\n    max-height: 100vh;\n    min-height: 50vh;\n    overflow-y: scroll;\n    overflow-x: hidden;\n    height: 100%;\n    scrollbar-width: none;\n    -ms-overflow-style: none;\n    padding-inline: vars.$m;\n    margin-inline: -(vars.$m);\n    &::-webkit-scrollbar {\n      display: none;\n    }\n  }\n}\n@include mixins.larger-than(vars.$bp-l) {\n  .tri-layout-mobile-tabs {\n    display: none;\n  }\n  .tri-layout-left-contents > *, .tri-layout-right-contents > * {\n    @include mixins.lightDark(opacity, 0.6, 0.75);\n    transition: opacity ease-in-out 120ms;\n    &:hover, &:focus-within {\n      opacity: 1 !important;\n    }\n    @media (prefers-contrast: more) {\n      opacity: 1 !important;\n    }\n  }\n}\n@include mixins.smaller-than(vars.$bp-l) {\n  .tri-layout-container {\n    grid-template-areas:  none;\n    grid-template-columns: 1fr;\n    grid-column-gap: 0;\n    padding-inline-end: vars.$xs;\n    padding-inline-start: vars.$xs;\n    .tri-layout-sides {\n      padding-inline-start: vars.$m;\n      padding-inline-end: vars.$m;\n      grid-column: 1/1;\n    }\n    .tri-layout-left > *, .tri-layout-right > * {\n      display: none;\n      pointer-events: none;\n    }\n    .tri-layout-left, .tri-layout-right {\n      padding-top: 0 !important;\n    }\n    .tri-layout-middle {\n      grid-area: none;\n      grid-row: 3;\n      grid-column: 1/1;\n      z-index: 1;\n      overflow: hidden;\n      transition: transform ease-in-out 240ms;\n    }\n    .tri-layout-left {\n      grid-row: 2;\n    }\n    &.show-info {\n      overflow: hidden;\n      .tri-layout-middle {\n        display: none;\n      }\n      .tri-layout-right  > *, .tri-layout-left > * {\n        display: block;\n        pointer-events: auto;\n      }\n    }\n  }\n}\n\n@include mixins.smaller-than(vars.$bp-m) {\n  .tri-layout-container {\n    margin-inline-start: 0;\n    margin-inline-end: 0;\n  }\n}\n\n/**\n * Scroll Indicators\n */\n.scroll-away-from-top:before,\n.scroll-away-from-bottom:after {\n  content: '';\n  display: block;\n  position: absolute;\n  @include mixins.lightDark(color, #F2F2F2, #111);\n  left: 0;\n  top: 0;\n  width: 100%;\n  height: 50px;\n  background: linear-gradient(to bottom, currentColor, transparent);\n  z-index: 2;\n}\n.scroll-away-from-bottom:after {\n  top: auto;\n  bottom: 0;\n  background: linear-gradient(to top, currentColor, transparent);\n}"
  },
  {
    "path": "resources/sass/_lists.scss",
    "content": "@use \"mixins\";\n@use \"vars\";\n\n\n.book-contents .entity-list-item {\n  .icon {\n    width: 4px;\n    border-radius: 1px;\n    justify-self: stretch;\n    align-self: stretch;\n    height: auto;\n    margin-inline-end: vars.$xs;\n  }\n  .icon:after {\n    opacity: 0.5;\n  }\n  .icon svg {\n    display: none;\n  }\n  p {\n    margin-bottom: 0;\n  }\n  .inner-page {\n    padding-top: 0;\n    padding-bottom: 0;\n  }\n}\n\n.entity-list-item + .chapter-expansion {\n  display: flex;\n  padding: 0 vars.$m vars.$m vars.$m;\n  align-items: center;\n  border: 0;\n  width: 100%;\n  position: relative;\n  > .icon {\n    width: 4px;\n    height: auto;\n    border-radius: 0 0 1px 1px;\n    align-self: stretch;\n    flex-shrink: 0;\n    &:before {\n      position: absolute;\n      top: 0;\n      left: 0;\n      width: 100%;\n      height: 1px;\n      background-color: currentColor;\n      content: '';\n      opacity: 0.5;\n    }\n    &:after {\n      opacity: 0.5;\n    }\n  }\n  .icon svg {\n    display: none;\n  }\n  > .content {\n    flex: 1;\n  }\n  .chapter-contents-toggle {\n    border-radius: 0 4px 4px 0;\n    padding: vars.$xs (vars.$m + vars.$xxs);\n    width: 100%;\n    text-align: start;\n  }\n  .chapter-contents-toggle:hover {\n    background-color: rgba(0, 0, 0, 0.06);\n  }\n}\n\n.entity-list-item.has-children {\n  padding-bottom: 0;\n  > .icon {\n    border-radius: 4px 4px 0 0;\n  }\n}\n\n.inset-list {\n  display: none;\n  .entity-list-item-name {\n    font-size: 1rem;\n  }\n  .entity-list-item-children {\n    padding-top: 0;\n    padding-bottom: 0;\n  }\n}\n\n.sidebar-page-nav {\n  $nav-indent: vars.$m;\n  list-style: none;\n  @include mixins.margin(vars.$s, 0, vars.$m, vars.$xs);\n  position: relative;\n  &:after {\n    content: '';\n    display: block;\n    position: absolute;\n    left: 0;\n    @include mixins.rtl {\n      left: auto;\n      right: 0;\n    }\n    @include mixins.lightDark(background-color, rgba(0, 0, 0, 0.2), rgba(255, 255, 255, 0.2));\n    width: 2px;\n    top: 5px;\n    bottom: 5px;\n    z-index: 0;\n  }\n  li {\n    margin-bottom: 4px;\n    font-size: 0.95em;\n    position: relative;\n  }\n  .h1 {\n    padding-inline-start: $nav-indent;\n  }\n  .h2 {\n    padding-inline-start: $nav-indent * 1.5;\n  }\n  .h3 {\n    padding-inline-start: $nav-indent * 2;\n  }\n  .h4 {\n    padding-inline-start: $nav-indent * 2.5;\n  }\n  .h5 {\n    padding-inline-start: $nav-indent*3;\n  }\n  .h6 {\n    padding-inline-start: $nav-indent*3.5;\n  }\n  .current-heading {\n    font-weight: bold;\n  }\n  li:not(.current-heading) .sidebar-page-nav-bullet {\n    @include mixins.lightDark(background-color, #BBB, #666, true);\n  }\n  .sidebar-page-nav-bullet {\n    width: 6px;\n    height: 6px;\n    position: absolute;\n    left: -2px;\n    top: 30%;\n    border-radius: 50%;\n    box-shadow: 0 0 0 6px #F2F2F2;\n    @include mixins.lightDark(box-shadow, 0 0 0 6px #F2F2F2, 0 0 0 6px #111);\n    z-index: 1;\n    @include mixins.rtl {\n      left: auto;\n      right: -2px;\n    }\n  }\n}\n\n// Sidebar list\n.book-tree .sidebar-page-list  {\n  list-style: none;\n  @include mixins.margin(vars.$xs, -(vars.$s), 0, -(vars.$s));\n  padding-inline-start: 0;\n  padding-inline-end: 0;\n\n  ul {\n    list-style: none;\n    padding-inline-start: 1rem;\n    padding-inline-end: 0;\n  }\n\n  .entity-list-item {\n    padding-top: 2px;\n    padding-bottom: 2px;\n    background-clip: content-box;\n    border-radius: 0 3px 3px 0;\n    padding-inline-end: 0;\n    .content {\n      width: 100%;\n      padding-top: vars.$xs;\n      padding-bottom: vars.$xs;\n      max-width: calc(100% - 20px);\n    }\n  }\n  .entity-list-item.selected {\n    @include mixins.lightDark(background-color, rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.06));\n  }\n  .entity-list-item.no-hover {\n    margin-top: -(vars.$xs);\n    padding-inline-end: 0;\n  }\n  .entity-list-item-name {\n    font-size: 1em;\n    margin: 0;\n    margin-inline-end: vars.$m;\n  }\n  .chapter-child-menu {\n    font-size: .8rem;\n    margin-top: -.2rem;\n    margin-inline-start: -1rem;\n  }\n  .chapter-contents-toggle {\n    display: block;\n    width: 100%;\n    text-align: start;\n    padding: vars.$xxs vars.$s (vars.$xxs * 2) vars.$s;\n    border-radius: 0 3px 3px 0;\n    line-height: 1;\n    margin-top: -(vars.$xxs);\n    margin-bottom: -(vars.$xxs);\n    &:hover {\n      @include mixins.lightDark(background-color, rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.06));\n    }\n  }\n  .entity-list-item .icon {\n    z-index: 2;\n    width: 4px;\n    height: auto;\n    align-self: stretch;\n    flex-shrink: 0;\n    border-radius: 1px;\n    opacity: 0.8;\n  }\n  .entity-list-item .icon:after {\n    opacity: 1;\n  }\n  .entity-list-item .icon svg {\n    display: none;\n  }\n}\n\n.chapter-child-menu ul.sub-menu {\n  display: none;\n  padding-inline-start: 0;\n  position: relative;\n  margin-bottom: 0;\n}\n\n// Sortable Lists\n.sortable-page-list, .sortable-page-sublist {\n  list-style: none;\n}\n.sort-box {\n  margin-bottom: vars.$m;\n  padding: vars.$m vars.$xl;\n  position: relative;\n  summary:focus {\n    outline: 1px dashed var(--color-primary);\n    outline-offset: 5px;\n  }\n  &::before {\n    pointer-events: none;\n    content: '';\n    border-radius: 4px;\n    opacity: 0.5;\n    border: 2px solid var(--color-book);\n    display: block;\n    top: 0;\n    bottom: 0;\n    left: 0;\n    right: 0;\n    position: absolute;\n  }\n}\n.sort-box-options {\n  display: flex;\n  flex-wrap: wrap;\n  justify-content: space-between;\n}\n.sort-box-options .button {\n  margin-inline-start: 0;\n}\n.sortable-page-list {\n  margin-inline-start: 0;\n  padding: 0;\n  .entity-list-item > span:first-child {\n    align-self: flex-start;\n  }\n  .sortable-selected, .sortable-selected:hover {\n    outline: 1px dotted var(--color-primary);\n    background-color: var(--color-primary-light) !important;\n  }\n  .entity-list-item > div {\n    display: block;\n    flex: 1;\n  }\n  > ul {\n    margin-inline-start: 0;\n  }\n  .sortable-page-sublist {\n    margin-bottom: vars.$m;\n    margin-top: 0;\n    padding-inline-start: vars.$m;\n  }\n  li {\n    @include mixins.lightDark(background-color, #FFF, #222);\n    border: 1px solid;\n    @include mixins.lightDark(border-color, #DDD, #666);\n    margin-top: -1px;\n    min-height: 38px;\n  }\n  li.text-page, li.text-chapter {\n    border-inline-start: 2px solid currentColor;\n  }\n  li:first-child {\n    margin-top: vars.$xs;\n  }\n}\n.sortable-page-list li.placeholder {\n  position: relative;\n}\n.sortable-page-list li.placeholder:before {\n  position: absolute;\n}\n.sort-box summary {\n  list-style: none;\n  font-size: .9rem;\n  cursor: pointer;\n}\n.sort-box summary::-webkit-details-marker {\n  display: none;\n}\ndetails.sort-box summary .caret-container svg {\n  transition: transform ease-in-out 120ms;\n}\ndetails.sort-box[open] summary .caret-container svg {\n  transform: rotate(90deg);\n}\n.sort-box-actions .icon-button {\n  opacity: .6;\n}\n.sort-box .flex-container-row:hover .sort-box-actions .icon-button,\n.sort-box .flex-container-row:focus-within .sort-box-actions .icon-button {\n  opacity: 1;\n}\n.sort-box-actions .icon-button[disabled] {\n  visibility: hidden;\n}\n.sort-box-actions .dropdown-menu button[disabled] {\n  display: none;\n}\n.sort-list-handle {\n  cursor: grab;\n}\n\n.activity-list-item {\n  padding: vars.$s 0;\n  display: grid;\n  grid-template-columns: min-content 1fr;\n  grid-column-gap: vars.$m;\n  font-size: 0.9em;\n}\n.card .activity-list-item {\n  padding-block: vars.$s;\n}\n\n.user-list-item {\n  display: inline-grid;\n  padding: vars.$s;\n  grid-template-columns: min-content 1fr;\n  grid-column-gap: vars.$m;\n  font-size: 0.9em;\n  align-items: center;\n  > div:first-child {\n    line-height: 0;\n  }\n}\n\nul.pagination {\n  display: inline-flex;\n  flex-wrap: wrap;\n  list-style: none;\n  margin: vars.$m 0;\n  padding-inline: 1px;\n  li:first-child {\n    a, span {\n      border-radius: 3px 0 0 3px;\n    }\n  }\n  li:last-child {\n    a, span {\n      border-radius: 0 3px 3px 0;\n    }\n  }\n  a, span {\n    display: block;\n    padding: vars.$xxs vars.$s;\n    border: 1px solid #CCC;\n    margin-inline-start: -1px;\n    margin-block-end: -1px;\n    user-select: none;\n    @include mixins.lightDark(color, #555, #eee);\n    @include mixins.lightDark(border-color, #ccc, #666);\n  }\n  li.disabled {\n    cursor: not-allowed;\n  }\n  li.active span {\n    @include mixins.lightDark(color, #111, #eee);\n    @include mixins.lightDark(background-color, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.5));\n  }\n}\n\n.compact ul.pagination {\n  margin: 0;\n}\n\n.entity-list, .icon-list {\n  margin: 0 (-(vars.$m));\n  h4 {\n    margin: 0;\n  }\n  hr {\n    margin: 0;\n  }\n  .text-small.text-muted {\n    color: #AAA;\n    font-size: 0.75em;\n    margin-top: vars.$xs;\n  }\n  .text-muted p.text-muted {\n    margin-top: 0;\n  }\n  .page.draft .text-page {\n    color: var(--color-page-draft);\n    fill: var(--color-page-draft);\n  }\n  > .dropdown-container {\n    display: block;\n  }\n}\n\n.icon-list hr {\n  margin: vars.$s vars.$m;\n  max-width: 140px;\n  opacity: 0.25;\n  height: 1.1px;\n}\n\n.icon-list hr + hr, .icon-list hr:first-child, .icon-list hr:last-child {\n  display: none;\n}\n\n.entity-list-item, .icon-list-item {\n  padding: vars.$s vars.$m;\n  display: flex;\n  align-items: center;\n  gap: vars.$m;\n  background-color: transparent;\n  border: 0;\n  width: 100%;\n  position: relative;\n  word-break: break-word;\n  h4 a {\n    color: #666;\n  }\n  > span:first-child {\n    flex-basis: 1.88em;\n    flex: none;\n  }\n  > span:last-child {\n    flex: 1;\n    text-align: start;\n  }\n  > .content {\n    min-width: 0;\n  }\n  &:not(.no-hover) {\n    cursor: pointer;\n  }\n  &:not(.no-hover):hover {\n    @include mixins.lightDark(background-color, rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.06));\n    text-decoration: none;\n    border-radius: 4px;\n  }\n  &.outline-hover:hover {\n    background-color: transparent;\n  }\n  &:focus {\n    @include mixins.lightDark(background-color, #eee, #222);\n    outline: 1px dotted #666;\n    outline-offset: -2px;\n  }\n}\n\n.entity-list-item.disabled {\n  pointer-events: none;\n  cursor: not-allowed;\n  opacity: 0.8;\n  user-select: none;\n  background: var(--bg-disabled);\n}\n\n.entity-list-item-path-sep {\n  display: inline-block;\n  vertical-align: top;\n  position: relative;\n  top: 1px;\n  svg {\n    margin-inline-end: 0;\n  }\n}\n\n.split-icon-list-item {\n  display: flex;\n  align-items: center;\n  gap: vars.$m;\n  background-color: transparent;\n  border: 0;\n  width: 100%;\n  position: relative;\n  word-break: break-word;\n  border-radius: 4px;\n  > a {\n    padding: vars.$s vars.$m;\n    display: flex;\n    align-items: center;\n    gap: vars.$m;\n    flex: 1;\n  }\n  > a:hover {\n    text-decoration: none;\n  }\n  .icon {\n    flex-basis: 1.88em;\n    flex: none;\n  }\n  &:hover {\n    @include mixins.lightDark(background-color, rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.06));\n  }\n}\n\n.icon-list-item-dropdown {\n  margin-inline-start: auto;\n  align-self: stretch;\n  display: flex;\n  align-items: stretch;\n  border-inline-start: 1px solid rgba(0, 0, 0, .1);\n  visibility: hidden;\n}\n.split-icon-list-item:hover .icon-list-item-dropdown,\n.split-icon-list-item:focus-within .icon-list-item-dropdown {\n  visibility: visible;\n}\n.icon-list-item-dropdown-toggle {\n  padding: vars.$xs;\n  display: flex;\n  align-items: center;\n  cursor: pointer;\n  @include mixins.lightDark(color, #888, #999);\n  svg {\n    margin: 0;\n  }\n  &:hover {\n    @include mixins.lightDark(background-color, rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.06));\n  }\n}\n\n.card .entity-list-item:not(.no-hover, .book-contents .entity-list-item):hover {\n  @include mixins.lightDark(background-color, #F2F2F2, #2d2d2d);\n  border-radius: 0;\n}\n.card .entity-list-item .entity-list-item:hover {\n  background-color: #EEEEEE;\n}\n\n.entity-list-item-children {\n  padding: vars.$m vars.$l;\n  > div {\n    overflow: hidden;\n    padding: 0 0 vars.$xs 0;\n  }\n  .entity-chip {\n    text-overflow: ellipsis;\n    height: 2.5em;\n    overflow: hidden;\n    text-align: start;\n    display: block;\n    white-space: nowrap;\n  }\n  > .entity-list > .entity-list-item:last-child {\n    margin-bottom: -(vars.$xs);\n  }\n}\n\n.entity-list-item-image {\n  align-self: stretch;\n  width: 140px;\n  flex: none;\n  background-size: cover;\n  background-position: 50% 50%;\n  border-radius: 3px;\n  position: relative;\n  margin-inline-end: vars.$l;\n\n  &.entity-list-item-image-wide {\n    width: 220px;\n  }\n\n  .svg-icon {\n    @include mixins.lightDark(color, #fff, rgba(255, 255, 255, 0.6));\n    font-size: 1.66rem;\n    margin-inline-end: 0;\n    position: absolute;\n    bottom: vars.$xs;\n    left: vars.$xs;\n  }\n\n  @include mixins.smaller-than(vars.$bp-m) {\n    width: 80px;\n  }\n}\n\n.chapter > .entity-list-item-image {\n  width: 60px;\n}\n\n.entity-list.compact {\n  font-size: 0.6 * vars.$fs-m;\n  h4, a {\n    line-height: 1.2;\n  }\n  .entity-item-snippet {\n    display: none;\n  }\n  .entity-list-item p {\n    font-size: vars.$fs-m * 0.8;\n    padding-top: vars.$xs;\n  }\n  .entity-list-item p:empty {\n    padding-top: 0;\n  }\n  p {\n    margin: 0;\n  }\n  > p.empty-text {\n    display: block;\n    font-size: vars.$fs-m;\n  }\n  hr {\n    margin: 0;\n  }\n  @include mixins.smaller-than(vars.$bp-m) {\n    h4 {\n      font-size: 1.666em;\n    }\n  }\n}\n\n.entity-item-tags {\n  font-size: .75rem;\n  opacity: 1;\n  .primary-background-light {\n    background: transparent;\n  }\n  .tag-name {\n    background-color: rgba(0, 0, 0, 0.05);\n  }\n}\n\n.dropdown-container {\n  display: inline-block;\n  vertical-align: top;\n  position: relative;\n}\n\n.dropdown-menu {\n  display: none;\n  position: absolute;\n  z-index: 999;\n  top: 0;\n  list-style: none;\n  inset-inline-end: 0;\n  margin: vars.$m 0;\n  @include mixins.lightDark(background-color, #fff, #333);\n  box-shadow: 0 1px 6px 0 rgba(0, 0, 0, 0.18);\n  border-radius: 3px;\n  min-width: 180px;\n  padding: vars.$xs 0;\n  @include mixins.lightDark(color, #555, #eee);\n  fill: currentColor;\n  text-align: start !important;\n  max-height: 500px;\n  overflow-y: auto;\n  &.anchor-left {\n    inset-inline-end: auto;\n    inset-inline-start: 0;\n  }\n  &.wide {\n    min-width: 220px;\n  }\n  &.xl-limited {\n    width: 280px;\n    max-width: 100%;\n  }\n  .text-muted {\n    color: #999;\n    fill: #999;\n  }\n  li.active a {\n    font-weight: 600;\n  }\n  button {\n    width: 100%;\n    text-align: start;\n  }\n  li.border-bottom {\n    border-bottom: 1px solid #DDD;\n  }\n  li hr {\n    margin: vars.$xs 0;\n  }\n  .icon-item, .text-item, .label-item {\n    padding: 8px vars.$m;\n    @include mixins.lightDark(color, #555, #eee);\n    fill: currentColor;\n    white-space: nowrap;\n    line-height: 1.4;\n    cursor: pointer;\n    &.break-text {\n      white-space: normal;\n      word-wrap: break-word;\n      overflow-wrap: break-word;\n    }\n    &:hover, &:focus {\n      text-decoration: none;\n      background-color: var(--color-primary-light);\n      color: var(--color-primary);\n    }\n    &:focus {\n      outline: 1px solid var(--color-primary);\n      outline-offset: -2px;\n    }\n    svg {\n      margin-inline-end: vars.$s;\n      display: inline-block;\n      width: 16px;\n    }\n  }\n  .text-item {\n    display: block;\n  }\n  .label-item {\n    display: grid;\n    align-items: center;\n    grid-template-columns: auto min-content;\n    gap: vars.$m;\n  }\n  .label-item > *:nth-child(2) {\n    opacity: 0.7;\n    &:hover {\n      opacity: 1;\n    }\n  }\n  .icon-item {\n    display: grid;\n    align-items: start;\n    grid-template-columns: 16px auto;\n    gap: vars.$m;\n    svg {\n      margin-inline-end: 0;\n      margin-block-start: 1px;\n    }\n  }\n}\n\n// Shift in sidebar dropdown menus to prevent shadows\n// being cut by scrollable container.\n.tri-layout-right .dropdown-menu,\n.tri-layout-left .dropdown-menu {\n  inset-inline-end: vars.$xs;\n}\n\n// Books grid view\n.featured-image-container {\n  position: relative;\n  overflow: hidden;\n  min-height: 140px;\n  background-size: cover;\n  background-position: 50% 50%;\n  transition: opacity ease-in-out 240ms;\n  a {\n    display: block;\n  }\n  img {\n    display: block;\n    width: 100%;\n    max-width: 100%;\n    height: auto;\n  }\n}\n.featured-image-container-wrap {\n  position: relative;\n  .svg-icon {\n    @include mixins.lightDark(color, #fff, rgba(255, 255, 255, 0.6));\n    font-size: 2rem;\n    margin-inline-end: 0;\n    position: absolute;\n    bottom: 10px;\n    left: 6px;\n  }\n}\n.grid-card:hover .featured-image-container {\n  opacity: .5;\n}\n\n.action-link-list {\n  //padding: $-s 0;\n}\n.action-link {\n  background: transparent;\n  border: none;\n  color: currentColor;\n  padding: vars.$m 0;\n}\n\n.active-link-list {\n  a {\n    display: inline-block;\n    padding: vars.$s;\n  }\n  a:not(.active) {\n    @include mixins.lightDark(color, #444, #888);\n  }\n  a:hover {\n    @include mixins.lightDark(background-color, rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05));\n    border-radius: 4px;\n    text-decoration: none;\n  }\n  &.in-sidebar {\n    a {\n      display: block;\n      margin-bottom: vars.$xs;\n    }\n    a.active {\n      border-radius: 4px;\n      @include mixins.lightDark(background-color, rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05));\n    }\n  }\n}\n\n.entity-meta-item {\n  display: flex;\n  line-height: 1.2;\n  margin: 0.6em 0;\n  align-content: start;\n  gap: vars.$s;\n  a {\n    line-height: 1.2;\n  }\n  svg {\n    flex-shrink: 0;\n    width: 1em;\n    margin: 0;\n  }\n}\n"
  },
  {
    "path": "resources/sass/_mixins.scss",
    "content": "// Responsive breakpoint control\n@mixin smaller-than($size) {\n    @media screen and (max-width: $size) { @content; }\n}\n@mixin larger-than($size) {\n    @media screen and (min-width: ($size + 1)) { @content; }\n}\n@mixin between($min, $max) {\n  @media screen and (min-width: ($min + 1)) and (max-width: $max) { @content; }\n}\n\n// Padding shorthand using logical operators to better support RTL.\n@mixin padding($t, $r, $b, $l) {\n  padding-block-start: $t;\n  padding-block-end: $b;\n  padding-inline-start: $l;\n  padding-inline-end: $r;\n}\n\n// Margin shorthand using logical operators to better support RTL.\n@mixin margin($t, $r, $b, $l) {\n  margin-block-start: $t;\n  margin-block-end: $b;\n  margin-inline-start: $l;\n  margin-inline-end: $r;\n}\n\n// Create a RTL specific style block.\n// Mostly used as a patch until browser support improves for logical properties.\n@mixin rtl() {\n  html[dir=rtl] & {\n    @content;\n  }\n}\n\n// Define a property for both light and dark mode\n@mixin lightDark($prop, $light, $dark, $important: false) {\n  @if($important) {\n    #{$prop}: $light !important;\n  } @else {\n    #{$prop}: $light;\n  }\n  html.dark-mode & {\n    @if($important) {\n      #{$prop}: $dark !important;\n    } @else {\n      #{$prop}: $dark;\n    }\n  }\n}\n\n@mixin whenDark {\n    html.dark-mode & {\n      @content;\n    }\n}"
  },
  {
    "path": "resources/sass/_opacity.scss",
    "content": "\n.opacity-10 {\n  opacity: 0.1;\n}\n.opacity-20 {\n  opacity: 0.2;\n}\n.opacity-30 {\n  opacity: 0.3;\n}\n.opacity-40 {\n  opacity: 0.4;\n}\n.opacity-50 {\n  opacity: 0.5;\n}\n.opacity-60 {\n  opacity: 0.6;\n}\n.opacity-70 {\n  opacity: 0.7;\n}\n.opacity-80 {\n  opacity: 0.8;\n}\n.opacity-90 {\n  opacity: 0.9;\n}"
  },
  {
    "path": "resources/sass/_pages.scss",
    "content": "@use \"mixins\";\n@use \"vars\";\n\n.page-editor {\n  display: flex;\n  flex-direction: column;\n  align-items: stretch;\n\n  .edit-area {\n    flex: 1;\n    flex-direction: column;\n    z-index: 10;\n    border-radius: 0 0 8px 8px;\n  }\n\n  .mce-tinymce {\n\tbox-shadow: none;\n  }\n\n  .mce-top-part::before {\n    box-shadow: none;\n  }\n}\n\n.page-editor-page-area {\n  width: 100%;\n  border-radius: 8px;\n  box-shadow: vars.$bs-card;\n  min-width: 300px;\n  @include mixins.lightDark(background-color, #FFF, #333)\n}\n\n.page-edit-toolbar {\n  width: 100%;\n  margin: 0 auto;\n  display: grid;\n  grid-template-columns: minmax(max-content, 2fr) 1.5fr minmax(max-content, 2fr);\n  align-items: center;\n}\n\n@include mixins.larger-than(vars.$bp-xxl) {\n  .page-editor-wysiwyg2024 .page-edit-toolbar,\n  .page-editor-wysiwyg2024 .page-editor-page-area,\n  .page-editor-wysiwyg .page-edit-toolbar,\n  .page-editor-wysiwyg .page-editor-page-area {\n    max-width: 1140px;\n  }\n\n  .page-editor-wysiwyg .floating-toolbox,\n  .page-editor-wysiwyg2024 .floating-toolbox {\n    position: absolute;\n  }\n}\n\n@include mixins.smaller-than(vars.$bp-m) {\n  .page-edit-toolbar {\n    display: flex;\n    flex-direction: row;\n    justify-content: space-between;\n  }\n}\n\n.title-input.page-title {\n  font-size: 0.8em;\n  .input {\n    border: 0;\n    margin-bottom: -1px;\n  }\n  input[type=\"text\"] {\n    max-width: 840px;\n    margin: 0 auto;\n    border: none;\n    height: auto;\n    display: block;\n    width: 100%;\n    font-size: 20px;\n    border-radius: 8px;\n  }\n  input[type=\"text\"]:focus {\n    position: relative;\n    outline-offset: -1px;\n    outline: 1px dashed var(--color-primary);\n    box-shadow: vars.$bs-card;\n    z-index: 50;\n  }\n}\n\n.page-editor-markdown .title-input.page-title input[type=\"text\"] {\n  max-width: 100%;\n}\n\nbody.tox-fullscreen .page-editor .edit-area,\nbody.markdown-fullscreen .page-editor .edit-area {\n  z-index: 12;\n}\n\nbody.tox-fullscreen, body.markdown-fullscreen {\n  .page-editor, .flex-fill {\n    overflow: visible;\n  }\n}\n\n@include mixins.smaller-than(vars.$bp-s) {\n  .page-edit-toolbar {\n    overflow-x: scroll;\n    overflow-y: visible;\n  }\n  .page-edit-toolbar {\n    white-space: nowrap;\n    > div {\n      display: inline-block;\n    }\n  }\n}\n\n.page-save-mobile-button {\n  position: fixed;\n  z-index: 30;\n  border-radius: 50%;\n  width: 52px;\n  height: 52px;\n  font-size: 26px;\n  inset-inline-end: vars.$xs;\n  bottom: vars.$s;\n  box-shadow: vars.$bs-hover;\n  background-color: currentColor;\n  text-align: center;\n  svg {\n    fill: #FFF;\n    margin-inline-end: 0;\n  }\n}\n\n.draft-notification {\n  pointer-events: none;\n  transform: scale(0);\n  transition: transform ease-in-out 120ms;\n  transform-origin: 50% 50%;\n  &.visible {\n    transform: scale(1);\n  }\n}\n\n.page-style.editor {\n  padding: 0 !important;\n}\n\n// Page content pointers\n.pointer-container {\n  position: fixed;\n  display: none;\n  left: 0;\n  z-index: 10;\n}\n.pointer {\n  border: 1px solid #CCC;\n  @include mixins.lightDark(border-color, #ccc, #000);\n  border-radius: 4px;\n  box-shadow: 0 0 12px 1px rgba(0, 0, 0, 0.1);\n  @include mixins.lightDark(background-color, #fff, #333);\n  width: 328px;\n\n  &:before {\n    position: absolute;\n    left: 50%;\n    bottom: -9px;\n    width: 16px;\n    height: 16px;\n    margin-inline-start: -8px;\n    content: '';\n    display: block;\n    transform: rotate(45deg);\n    transform-origin: 50% 50%;\n    border-right: 1px solid #CCC;\n    border-bottom: 1px solid #CCC;\n    z-index: 56;\n    @include mixins.lightDark(background-color, #fff, #333);\n    @include mixins.lightDark(border-color, #ccc, #000);\n  }\n  input, button, a {\n    position: relative;\n    height: 28px;\n    font-size: 12px;\n    vertical-align: top;\n    padding: 5px 16px;\n  }\n  input {\n    background-color: #FFF;\n    border: 1px solid #DDD;\n    @include mixins.lightDark(border-color, #ddd, #000);\n    color: #666;\n    width: auto;\n    flex: 1;\n    z-index: 58;\n    padding: 5px;\n    border-radius: 0;\n  }\n  .text-button {\n    @include mixins.lightDark(color, #444, #AAA);\n  }\n  .input-group .button {\n    line-height: 1;\n    margin-inline-start: -1px;\n    margin-block: 0;\n    box-shadow: none;\n    border-radius: 0;\n  }\n  a.button {\n    margin: 0;\n  }\n  .svg-icon {\n    width: 1.2em;\n    height: 1.2em;\n  }\n  .button {\n    @include mixins.lightDark(border-color, #ddd, #000);\n  }\n}\n\n// Page inline comments\n.content-comment-highlight {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 0;\n  height: 0;\n  user-select: none;\n  pointer-events: none;\n  &:after {\n    content: '';\n    position: absolute;\n    left: 0;\n    top: 0;\n    width: 100%;\n    height: 100%;\n    background-color: var(--color-primary);\n    opacity: 0.25;\n  }\n}\n.content-comment-window {\n  font-size: vars.$fs-m;\n  line-height: 1.4;\n  position: absolute;\n  top: calc(100% + 3px);\n  left: 0;\n  z-index: 92;\n  pointer-events: all;\n  min-width: min(340px, 80vw);\n  @include mixins.lightDark(background-color, #FFF, #222);\n  box-shadow: vars.$bs-hover;\n  border-radius: 4px;\n  overflow: hidden;\n}\n.content-comment-window-actions {\n  background-color: var(--color-primary);\n  color: #FFF;\n  display: flex;\n  align-items: center;\n  justify-content: end;\n  gap: vars.$xs;\n  button {\n    color: #FFF;\n    font-size: 12px;\n    padding: vars.$xs;\n    line-height: 1;\n    cursor: pointer;\n  }\n  button[data-action=\"jump\"] {\n    text-decoration: underline;\n  }\n  svg {\n    fill: currentColor;\n    width: 12px;\n  }\n}\n.content-comment-window-content {\n  padding: vars.$xs vars.$s vars.$xs vars.$xs;\n  max-height: 200px;\n  overflow-y: scroll;\n}\n.content-comment-window-content .comment-reference-indicator-wrap {\n  display: none;\n}\n.content-comment-marker {\n  position: absolute;\n  right: -16px;\n  top: -16px;\n  pointer-events: all;\n  width: min(1.5em, 32px);\n  height: min(1.5em, 32px);\n  border-radius: min(calc(1.5em / 2), 32px);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  background-color: var(--color-primary);\n  box-shadow: vars.$bs-hover;\n  color: #FFF;\n  cursor: pointer;\n  z-index: 90;\n  transform: scale(1);\n  transition: transform ease-in-out 120ms;\n  svg {\n    fill: #FFF;\n    width: 80%;\n  }\n}\n.page-content [id^=\"bkmrk-\"]:hover .content-comment-marker {\n  transform: scale(1.15);\n}\n\n// Page editor sidebar toolbox\n.floating-toolbox {\n  @include mixins.lightDark(background-color, #FFF, #222);\n  overflow: hidden;\n  align-items: stretch;\n  flex-direction: row;\n  display: flex;\n  max-height: 100%;\n  border-radius: 8px;\n  box-shadow: vars.$bs-card;\n  margin-bottom: auto;\n  margin-inline-start: vars.$l;\n  position: relative;\n  &.open {\n    position: relative;\n    right: 0;\n    max-width: 480px;\n    margin-bottom: 0;\n  }\n  &:not(.open) .toolbox-tab-content {\n    display: none !important;\n  }\n  .toolbox-toggle svg {\n    transition: transform ease-in-out 180ms;\n  }\n  .toolbox-toggle {\n    transition: background-color ease-in-out 180ms;\n  }\n  &.open .toolbox-toggle {\n    background-color: rgba(255, 0, 0, 0.20);\n  }\n  &.open .toolbox-toggle svg {\n    transform: rotate(180deg);\n  }\n  > div {\n    flex: 1;\n    position: relative;\n  }\n  .tabs {\n    border-inline-end: 1px solid #DDD;\n    @include mixins.lightDark(border-inline-end-color, #DDD, #000);\n    width: 40px;\n    flex: 0 1 auto;\n    margin-inline-end: -1px;\n  }\n  .tabs-inner {\n    @include mixins.lightDark(background-color, #FFFFFF, #222);\n  }\n  .tabs svg {\n    padding: 0;\n    margin: 0;\n  }\n  .tabs-inner > button {\n    @include mixins.lightDark(color, rgba(0, 0, 0, 0.7), rgba(255, 255, 255, 0.5));\n    display: block;\n    cursor: pointer;\n    padding: 10px vars.$xs;\n    font-size: 18px;\n    line-height: 1.6;\n  }\n  .tabs-inner > button:hover,  &.open .tabs-inner > button.active {\n    color: var(--color-link) !important;\n    position: relative;\n    &:after {\n      content: '';\n      display: block;\n      position: absolute;\n      left: 0;\n      width: 100%;\n      top: 0;\n      height: 100%;\n      background-color: currentColor;\n      opacity: .075;\n    }\n  }\n  &.open .tabs-inner > button.active {\n    border-inline-end: 1px solid var(--color-link);\n    margin-inline-end: -1px;\n  }\n  h4 {\n    font-size: 24px;\n    margin: vars.$m 0 0 0;\n    padding: 0 vars.$l vars.$s vars.$l;\n  }\n  .tags input {\n    max-width: 100%;\n    width: 100%;\n    min-width: 50px;\n  }\n  .tags td, .inline-start-table > div > div > div {\n    padding-inline-end: vars.$s;\n    padding-top: vars.$s;\n    position: relative;\n  }\n  .handle {\n    user-select: none;\n    cursor: move;\n    fill: #999;\n  }\n  form {\n    display: flex;\n    flex: 1;\n    flex-direction: column;\n    overflow-y: scroll;\n  }\n  table td, table th {\n    overflow: visible;\n  }\n}\n\n@include mixins.smaller-than(vars.$bp-xxl) {\n  .floating-toolbox {\n    margin-inline-start: vars.$s;\n  }\n}\n\n@include mixins.smaller-than(vars.$bp-s) {\n  .page-editor-page-area-wrap {\n    margin: 4px !important;\n  }\n  .floating-toolbox {\n    margin-inline-start: 4px;\n  }\n  .floating-toolbox .tabs {\n    width: 32px;\n  }\n  .floating-toolbox .tabs-inner > button {\n    font-size: 12px;\n  }\n  .page-edit-toolbar {\n    padding-block: 0 !important;\n  }\n  .page-editor.toolbox-open .page-editor-page-area {\n    display: none;\n  }\n}\n\n.toolbox-tab-content {\n  display: none;\n  overflow-y: auto;\n  padding-bottom: 45px;\n}\n\n.suggestion-box {\n  top: auto;\n  margin: -4px 0 0;\n  right: auto;\n  left: 0;\n  padding: 0;\n  li {\n    display: block;\n    border-bottom: 1px solid #DDD;\n    &:last-child {\n      border-bottom: 0;\n    }\n  }\n}\n\n.comments-container h5 {\n  color: #888;\n  font-weight: normal;\n  margin-top: 0.5em;\n}\n\n.comment-editor .CodeMirror, .comment-editor .CodeMirror-scroll {\n  min-height: 175px;\n}\n\n/* FIXME - Ugly hack to modify the media plugin for TinyMCE */\n.mce-floatpanel[aria-label=\"Insert/edit media\"] {\n  .mce-open {\n    display: none;\n  }\n}\n\n.entity-list-item > span:first-child,\n.icon-list-item > span:first-child,\n.split-icon-list-item > a > .icon,\n.chapter-expansion > .icon {\n  font-size: 0.8rem;\n  width: 1.88em;\n  height: 1.88em;\n  flex-shrink: 0;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  text-align: center;\n  border-radius: 1em;\n  position: relative;\n  overflow: hidden;\n  svg {\n    margin: 0;\n    bottom: 0;\n  }\n  &:after {\n    content: '';\n    position: absolute;\n    background-color: currentColor;\n    opacity: 0.2;\n    left: 0;\n    top: 0;\n    width: 100%;\n    height: 100%;\n  }\n}\n\n.entity-chip {\n  display: inline-block;\n  align-items: center;\n  justify-content: center;\n  text-align: center;\n  font-size: 0.9em;\n  border-radius: 3px;\n  position: relative;\n  overflow: hidden;\n  padding: vars.$xs vars.$s;\n  fill: currentColor;\n  opacity: 0.85;\n  transition: opacity ease-in-out 120ms;\n  &:after {\n    content: '';\n    position: absolute;\n    background-color: currentColor;\n    opacity: 0.15;\n    left: 0;\n    top: 0;\n    width: 100%;\n    height: 100%;\n  }\n  &:hover {\n    text-decoration: none;\n    opacity: 1;\n  }\n  @media (prefers-contrast: more) {\n    opacity: 1;\n  }\n}\n"
  },
  {
    "path": "resources/sass/_print.scss",
    "content": "html, body {\n  font-size: 12px;\n  background-color: #FFF;\n}\n\n.page-content {\n  margin: 0 auto;\n}\n\n.print-hidden {\n  display: none !important;\n}\n\n.tri-layout-container {\n  grid-template-columns: 1fr;\n  grid-template-areas: \"b\";\n  margin-inline-start: 0;\n  margin-inline-end: 0;\n  display: block;\n}\n\n.card {\n  box-shadow: none;\n}\n\n.content-wrap.card {\n  padding-inline-start: 0;\n  padding-inline-end: 0;\n}"
  },
  {
    "path": "resources/sass/_reset.scss",
    "content": "/* http://meyerweb.com/eric/tools/css/reset/\n   v2.0 | 20110126\n   License: none (public domain)\n*/\n\nhtml, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video {\n  margin: 0;\n  padding: 0;\n  border: 0;\n  font-size: 100%;\n  font: inherit;\n  vertical-align: baseline; }\n\n/* HTML5 display-role reset for older browsers */\n\narticle, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section {\n  display: block; }\n\nbody {\n  line-height: 1; }\n\nol, ul {\n  list-style: none; }\n\nblockquote, q {\n  quotes: none; }\n\nblockquote {\n  &:before, &:after {\n    content: '';\n    content: none; } }\n\nq {\n  &:before, &:after {\n    content: '';\n    content: none; } }\n\ntable {\n  border-collapse: collapse;\n  border-spacing: 0; }\n"
  },
  {
    "path": "resources/sass/_spacing.scss",
    "content": "@use \"vars\";\n\n// Here we generate spacing utility classes for our sizes for all box sides and axis.\n// These will output to classes like .px-m (Padding on x-axis, medium size) or .mr-l (Margin right, large size)\n\n@mixin spacing($prop, $propLetter) {\n  @each $sizeLetter, $size in vars.$spacing {\n    .#{$propLetter}-#{$sizeLetter} {\n      #{$prop}: $size !important;\n    }\n    .#{$propLetter}x-#{$sizeLetter} {\n      #{$prop}-inline-start: $size !important;\n      #{$prop}-inline-end: $size !important;\n    }\n    .#{$propLetter}y-#{$sizeLetter} {\n      #{$prop}-top: $size !important;\n      #{$prop}-bottom: $size !important;\n    }\n    .#{$propLetter}t-#{$sizeLetter} {\n      #{$prop}-top: $size !important;\n    }\n    .#{$propLetter}r-#{$sizeLetter} {\n      #{$prop}-inline-end: $size !important;\n    }\n    .#{$propLetter}b-#{$sizeLetter} {\n      #{$prop}-bottom: $size !important;\n    }\n    .#{$propLetter}l-#{$sizeLetter} {\n      #{$prop}-inline-start: $size !important;\n    }\n  }\n}\n@include spacing('margin', 'm');\n@include spacing('padding', 'p');\n\n@each $sizeLetter, $size in vars.$spacing {\n  .gap-#{$sizeLetter} {\n    gap: $size !important;\n  }\n  .gap-x-#{$sizeLetter} {\n    column-gap: $size !important;\n  }\n  .gap-y-#{$sizeLetter} {\n    row-gap: $size !important;\n  }\n}\n"
  },
  {
    "path": "resources/sass/_tables.scss",
    "content": "@use \"mixins\";\n@use \"vars\";\n\ntable {\n  min-width: 100px;\n  max-width: 100%;\n  thead {\n    @include mixins.lightDark(background-color, #f8f8f8, #333);\n    font-weight: 500;\n  }\n  td, th {\n    min-width: 10px;\n    padding: 6px 8px;\n    border: 1px solid #DDD;\n    overflow: auto;\n    line-height: 1.2;\n\tword-break: break-word;\n    vertical-align: top; // Workaround for: https://bugzilla.mozilla.org/show_bug.cgi?id=569645\n  }\n  td p, th p {\n    margin: 0;\n  }\n}\n\ntable.table {\n  width: 100%;\n  tr td, tr th {\n    border-bottom: 1px solid rgba(0, 0, 0, 0.05);\n  }\n  th, td {\n    text-align: start;\n    border: none;\n    padding: vars.$s vars.$s;\n    vertical-align: middle;\n    margin: 0;\n    overflow: visible;\n  }\n  th {\n    font-weight: bold;\n  }\n  tr:hover {\n    @include mixins.lightDark(background-color, #F2F2F2, #333);\n  }\n  .text-right {\n    text-align: end;\n  }\n  .text-center {\n    text-align: center;\n  }\n  td.actions {\n    overflow: visible;\n  }\n  a {\n    display: inline-block;\n  }\n  &.expand-to-padding {\n    margin-left: -(vars.$s);\n    margin-right: -(vars.$s);\n    width: calc(100% + (2*#{vars.$s}));\n    max-width: calc(100% + (2*#{vars.$s}));\n  }\n}\n\ntable.no-style {\n  td {\n    border: 0;\n    padding: 0;\n  }\n}\n\ntable.list-table {\n  margin: 0 (-(vars.$xs));\n  td {\n    border: 0;\n    vertical-align: middle;\n    padding: vars.$xs;\n  }\n}"
  },
  {
    "path": "resources/sass/_text.scss",
    "content": "@use \"mixins\";\n@use \"vars\";\n\n/**\n * Fonts\n */\n\nbody, button, input, select, label, textarea {\n  font-family: var(--font-body);\n}\npre, #markdown-editor-input, .text-mono, .code-base {\n  font-family: var(--font-code);\n}\n\n/*\n * Header Styles\n */\n\nh1 {\n  font-size: 3.425em;\n  line-height: 1.22222222em;\n  margin-top: 0.48888889em;\n  margin-bottom: 0.48888889em;\n}\nh2 {\n  font-size: 2.8275em;\n  line-height: 1.294117647em;\n  margin-top: 0.8627451em;\n  margin-bottom: 0.43137255em;\n}\nh3 {\n  font-size: 2.333em;\n  line-height: 1.221428572em;\n  margin-top: 0.78571429em;\n  margin-bottom: 0.43137255em;\n}\nh4 {\n  font-size: 1.666em;\n  line-height: 1.375em;\n  margin-top: 0.78571429em;\n  margin-bottom: 0.43137255em;\n}\n\nh1, h2, h3, h4, h5, h6 {\n  font-weight: 400;\n  position: relative;\n  display: block;\n  font-family: var(--font-heading, var(--font-body));\n  @include mixins.lightDark(color, #222, #BBB);\n}\n\nh5 {\n  font-size: 1.4em;\n}\n\nh5, h6 {\n  line-height: 1.2em;\n  margin-top: 0.78571429em;\n  margin-bottom: 0.66em;\n}\n\n@include mixins.smaller-than(vars.$bp-s) {\n  h1 {\n    font-size: 2.8275em;\n  }\n  h2 {\n    font-size: 2.333em;\n  }\n  h3 {\n    font-size: 1.666em;\n  }\n  h4 {\n    font-size: 1.333em;\n  }\n  h5 {\n    font-size: 1.161616em;\n  }\n}\n\n.list-heading {\n  font-size: 2rem;\n}\n\nh2.list-heading {\n  font-size: 1.333rem;\n}\n\n/*\n * Link styling\n */\na {\n  color: var(--color-link);\n  fill: currentColor;\n  cursor: pointer;\n  text-decoration: none;\n  transition: filter ease-in-out 80ms;\n  line-height: 1.6;\n  &:hover {\n    text-decoration: underline;\n  }\n  &.icon {\n    display: inline-block;\n  }\n  svg {\n    position: relative;\n    display: inline-block;\n  }\n  &:focus img:only-child {\n    outline: 2px dashed var(--color-link);\n    outline-offset: 2px;\n  }\n}\n\na.no-link-style {\n  color: inherit;\n  &:hover {\n    text-decoration: none;\n  }\n}\n\n.blended-links a {\n  color: inherit;\n  svg {\n    fill: currentColor;\n  }\n}\n\n/*\n * Other HTML Text Elements\n */\np, ul, ol, pre, table, blockquote {\n  margin-top: 0.3em;\n  margin-bottom: 1.375em;\n}\n\nhr {\n  border: 0;\n  height: 1px;\n  @include mixins.lightDark(background, #eaeaea, #555);\n  margin-bottom: vars.$l;\n  &.faded {\n    background-image: linear-gradient(to right, #FFF, #e3e0e0 20%, #e3e0e0 80%, #FFF);\n  }\n  &.darker {\n    @include mixins.lightDark(background, #DDD, #666);\n  }\n  &.margin-top, &.even {\n    margin-top: vars.$l;\n  }\n}\n\nstrong, b, .bold, .strong {\n  font-weight: bold;\n  > strong, > b, > .bold, > .strong {\n    font-weight: bolder;\n  }\n}\n\nem, i, .italic {\n  font-style: italic;\n}\n\nsmall, p.small, span.small, .text-small {\n  font-size: 0.75rem;\n}\n\nsup, .superscript {\n  vertical-align: super;\n  font-size: 0.8em;\n}\n\nsub, .subscript {\n  vertical-align: sub;\n  font-size: 0.8em;\n}\n\npre {\n  font-size: 12px;\n  border: 1px solid #DDD;\n  @include mixins.lightDark(background-color, #FFF, #2B2B2B);\n  @include mixins.lightDark(border-color, #DDD, #111);\n  border-radius: 4px;\n  padding-inline-start: 26px;\n  position: relative;\n  padding-top: 3px;\n  padding-bottom: 3px;\n  &:before {\n    content: '';\n    display: block;\n    position: absolute;\n    top: 0;\n    width: 22.4px;\n    inset-inline-start: 0;\n    height: 100%;\n    @include mixins.lightDark(background-color, #f5f5f5, #313335);\n    @include mixins.lightDark(border-inline-end, 1px solid #DDD, none);\n  }\n}\n\n@media print {\n  pre {\n    padding-left: 12px;\n  }\n  pre:before {\n    display: none;\n  }\n}\n\nblockquote {\n  display: block;\n  position: relative;\n  border-left: 4px solid transparent;\n  border-left-color: var(--color-primary);\n  @include mixins.lightDark(background-color, #f8f8f8, #333);\n  padding: vars.$s vars.$m vars.$s vars.$xl;\n  overflow: auto;\n  &:before {\n    content: \"\\201C\";\n    font-size: 2em;\n    font-weight: bold;\n    position: absolute;\n    top: vars.$s;\n    left: vars.$s;\n    color: #777;\n  }\n}\n\n.text-mono {\n  font-family: var(--font-code);\n}\n\n.text-uppercase {\n  text-transform: uppercase;\n}\n\n.text-capitals {\n  text-transform: capitalize;\n}\n\n.code-base {\n  font-size: 0.84em;\n  border: 1px solid #DDD;\n  border-radius: 3px;\n  @include mixins.lightDark(background-color, #f8f8f8, #2b2b2b);\n  @include mixins.lightDark(border-color, #DDD, #444);\n}\n\ncode {\n  @extend .code-base;\n  display: inline;\n  padding: 1px 3px;\n  white-space:pre-wrap;\n  line-height: 1.2em;\n}\n\nspan.code {\n  @extend .code-base;\n  padding: 1px vars.$xs;\n}\n\npre code {\n  background-color: transparent;\n  border: 0;\n  font-size: 1em;\n  display: block;\n  line-height: 1.6;\n}\n\nspan.highlight {\n  font-weight: bold;\n  padding: 2px 4px;\n}\n\n/*\n * Lists\n */\nul, ol {\n  padding-left: vars.$m * 2.0;\n  padding-right: vars.$m * 2.0;\n  display: flow-root;\n  p {\n    margin: 0;\n  }\n}\nul {\n  list-style: disc;\n  ul {\n    list-style: circle;\n  }\n  label {\n    margin: 0;\n  }\n}\n\nol {\n  list-style: decimal;\n}\n\nli > ol, li > ul {\n  margin-top: 0;\n  margin-bottom: 0;\n  margin-block-end: 0;\n  margin-block-start: 0;\n  padding-block-end: 0;\n  padding-block-start: 0;\n  padding-left: vars.$m * 1.2;\n  padding-right: vars.$m * 1.2;\n}\n\n/**\n * Checkbox lists\n * Some styles duplicated for supporting logical units (eg. inline-end) while\n * providing fallbacks to non-logical rules, so RTL is natively supported where possible.\n */\nli.checkbox-item, li.task-list-item {\n  display: list-item;\n  list-style: none;\n  margin-left: -(vars.$m * 1.2);\n  margin-inline-start: -(vars.$m * 1.2);\n  margin-inline-end: 0;\n  input[type=\"checkbox\"] {\n    margin-right: vars.$xs;\n    margin-inline-end: vars.$xs;\n    margin-inline-start: 0;\n  }\n  li.checkbox-item, li.task-list-item {\n    margin-left: vars.$xs;\n    margin-inline-start: vars.$xs;\n    margin-inline-end: 0;\n  }\n}\n\n/*\n * Generic text styling classes\n */\n.underlined {\n  text-decoration: underline;\n}\n\n.text-center {\n  text-align: center;\n}\n.text-left {\n  text-align: start;\n}\n.text-right {\n  text-align: end;\n}\n\n@each $sizeLetter, $size in vars.$screen-sizes {\n  @include mixins.larger-than($size) {\n    .text-#{$sizeLetter}-center {\n      text-align: center;\n    }\n    .text-#{$sizeLetter}-left {\n      text-align: start;\n    }\n    .text-#{$sizeLetter}-right {\n      text-align: end;\n    }\n  }\n}\n\n.text-bigger {\n  font-size: 1.1em;\n}\n\n.text-large {\n  font-size: 1.6666em;\n}\n\n.no-color {\n  color: inherit;\n}\n\n.break-text {\n  white-space: normal;\n  word-wrap: break-word;\n  overflow-wrap: break-word;\n}\n\n.text-limit-lines-1 {\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n.text-limit-lines-2 {\n  // -webkit use here is actually standardised cross-browser:\n  // https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-line-clamp\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-line-clamp: 2;\n  overflow: hidden;\n}\n\n/**\n * Grouping\n */\n.header-group {\n  margin: vars.$m 0;\n  h1, h2, h3, h4, h5, h6 {\n    margin: 0;\n  }\n}\n\nspan.sep {\n  color: #BBB;\n  padding: 0 vars.$xs;\n}\n\n.list > * {\n  display: block;\n}\n\n/**\n  * Icons\n  */\n.svg-icon {\n  width: 1em;\n  height: 1em;\n  display: inline-block;\n  position: relative;\n  bottom: -0.105em;\n  margin-inline-end: vars.$xs;\n  pointer-events: none;\n  fill: currentColor;\n}\n"
  },
  {
    "path": "resources/sass/_tinymce.scss",
    "content": "@use \"mixins\";\n@use \"vars\";\n\n\n// Custom full screen mode\n.tox.tox-fullscreen {\n  position: fixed;\n  top: 0;\n  height: 100%;\n  width: 100%;\n  max-width: 100%;\n  z-index: 100;\n}\n\n// Editor wrapper edits\n.tox.tox-tinymce {\n  border-inline: 0;\n  border-bottom: 0;\n}\n\n// In editor body overrides\n.page-content.mce-content-body {\n  padding-block-start: 1rem;\n  padding-block-end: 1rem;\n  outline: 0;\n  display: block;\n  max-width: 870px;\n}\n\n.wysiwyg-input.mce-content-body {\n  padding-block-start: 1rem;\n  padding-block-end: 1rem;\n  outline: 0;\n  display: block;\n}\n\n.wysiwyg-input.mce-content-body:before {\n  padding: 1rem;\n  top: 4px;\n  font-style: italic;\n  @include mixins.lightDark(color, rgba(34,47,62,.5), rgba(155,155,155,.5))\n}\n\n// Default styles for our custom root nodes\n.page-content.mce-content-body doc-root {\n  display: block;\n}\n.page-content.mce-content-body code-block {\n  display: block;\n}\n\n// Pad out bottom of editor\nbody.page-content.mce-content-body  {\n  padding-bottom: 5rem;\n}\n\n// Remove svg background line in toolbar items\n.tox .tox-pop__dialog .tox-toolbar {\n  background: transparent !important;\n}\n\n// Center toolbar items\n.tox-toolbar__primary {\n  justify-content: center;\n}\n\n// Prevent scroll jumps on codemirror clicks\n.page-content.mce-content-body code-block > * {\n  pointer-events: none;\n}\n.page-content.mce-content-body code-block pre {\n  display: none;\n}\n\n// Details/summary editor usability\n.page-content.mce-content-body details summary {\n  pointer-events: none;\n}\n.page-content.mce-content-body details doc-root {\n  padding: vars.$s;\n  margin-left: (2px - vars.$s);\n  margin-right: (2px - vars.$s);\n  margin-bottom: (2px - vars.$s);\n  margin-top: (2px - vars.$s);\n  overflow: hidden;\n}\n\n// Allow alignment to be reflected in media embed wrappers\n.page-content.mce-content-body .mce-preview-object.align-right {\n  float: right !important;\n  margin: vars.$xs 0 vars.$xs vars.$s;\n}\n\n.page-content.mce-content-body .mce-preview-object.align-left {\n  float: left !important;\n  margin: vars.$xs vars.$m vars.$m 0;\n}\n\n.page-content.mce-content-body .mce-preview-object.align-center {\n  display: block;\n  margin-left: auto;\n  margin-right: auto;\n}\n\n.page-content.mce-content-body .mce-preview-object iframe,\n.page-content.mce-content-body .mce-preview-object video {\n  display: block;\n  margin: 0 !important;\n  float: none !important;\n}\n\n.page-content.mce-content-body td[data-mce-selected]::after,\n.page-content.mce-content-body th[data-mce-selected]::after {\n  top: 1px;\n  left: 1px;\n  bottom: 1px;\n  right: 1px;\n  outline: 1px dashed #1a85ff;\n  outline-offset: 0;\n}\n\n/**\n * Dark Mode Overrides\n */\n.dark-mode .tox .tox-toolbar__primary,\n.dark-mode .tox .tox-menu,\n.dark-mode .tox .tox-dialog__header,\n.dark-mode .tox .tox-dialog,\n.dark-mode .tox .tox-dialog__footer,\n.dark-mode .tox .tox-pop__dialog,\n.dark-mode .tox.tox-tinymce-aux .tox-toolbar__overflow {\n    background-color: #333 !important;\n}\n.dark-mode .tox .tox-tbtn svg,\n.dark-mode .tox .tox-tbtn,\n.dark-mode .tox .tox-collection--list .tox-collection__item--active:not(.tox-collection__item--state-disabled)\n{\n  color: #dbdbdb;\n  fill: #dbdbdb;\n}\n\n\n\n/**\n * Format Menu Hacks\n */\n.tox .tox-tbtn--bespoke .tox-tbtn__select-label {\n  width: 6em !important;\n}\n.tox-menu .tox-collection__item blockquote::before {\n  content: none;\n}\n.tox-menu .tox-collection__item blockquote {\n  border-left: 4px solid var(--color-primary) !important;\n  padding: 4px 6px !important;\n}\n.tox-menu .tox-collection__item blockquote {\n  border-left: 4px solid var(--color-primary) !important;\n  padding: 4px 6px !important;\n}\n.tox-menu .tox-collection__item p[style*=\"background-color\"] {\n  padding: 4px 6px !important;\n  border-left: 3px solid currentColor !important;\n}\n.tox-menu .tox-collection__item[title^=\"<\"] > div > div {\n  font-family: var(--font-code) !important;\n  border: 1px solid #DDD !important;\n  background-color: #EEE !important;\n  padding: 4px 6px !important;\n}\n.tox-menu .tox-collection__item-label {\n  line-height: normal !important;\n}\n\n/**\n * Fake task list checkboxes\n */\n.page-content.mce-content-body .task-list-item {\n  margin-inline-start: 0;\n  position: relative;\n}\n.page-content.mce-content-body .task-list-item > input[type=\"checkbox\"] {\n  display: none;\n}\n.page-content.mce-content-body .task-list-item:before {\n  content: '';\n  display: inline-block;\n  border: 2px solid #CCC;\n  width: 12px;\n  height: 12px;\n  border-radius: 2px;\n  margin-inline-end: 8px;\n  vertical-align: text-top;\n  cursor: pointer;\n  position: absolute;\n  inset-inline-start: -24px;\n  top: 4px;\n}\n\n.page-content.mce-content-body .task-list-item[checked]:before {\n  background-color: #CCC;\n  background-image: url('data:image/svg+xml;utf8,<svg fill=\"%23FFFFFF\" version=\"1.1\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"m8.4856 20.274-6.736-6.736 2.9287-2.7823 3.8073 3.8073 10.836-10.836 2.9287 2.9287z\" stroke-width=\"1.4644\"/></svg>');\n  background-position: 50% 50%;\n  background-size: 100% 100%;\n}\n\n/**\n * Ensure cursor indicates that drawings are clickable\n */\n.page-content.mce-content-body [drawio-diagram] {\n  cursor: pointer;\n}\n"
  },
  {
    "path": "resources/sass/_vars.scss",
    "content": "// Variables\n// A range of SASS and plain CSS variables used in BookStack\n////////////////////////////////////////////////////////////\n\n// This is simply placed here at the top to prevent parsing/rendering issues\n// where built CSS files may have a starting BOM mark which can break the first css rule\n// when used inline, so this is here as a sacrifice in such scenarios instead of an important rule.\n// Related: https://github.com/sass/dart-sass/issues/472\n.dummy-style {color: red;}\n\n// Screen breakpoints\n$bp-xxxl: 1700px;\n$bp-xxl: 1400px;\n$bp-xl: 1100px;\n$bp-l: 1000px;\n$bp-m: 880px;\n$bp-s: 600px;\n$bp-xs: 400px;\n$bp-xxs: 360px;\n\n// List of screen sizes\n$screen-sizes: (('xxs', $bp-xxs), ('xs', $bp-xs), ('s', $bp-s), ('m', $bp-m), ('l', $bp-l), ('xl', $bp-xl));\n\n// Spacing (Margins+Padding)\n$xxxl: 64px;\n$xxl: 48px;\n$xl: 32px;\n$l: 24px;\n$m: 16px;\n$s: 12px;\n$xs: 6px;\n$xxs: 3px;\n\n// List of our spacing sizes\n$spacing: (('none', 0), ('xxs', $xxs), ('xs', $xs), ('s', $s), ('m', $m), ('l', $l), ('xl', $xl), ('xxl', $xxl), ('auto', auto));\n\n// Fonts\n$font-body: -apple-system, BlinkMacSystemFont,\n\"Segoe UI\", \"Oxygen\", \"Ubuntu\", \"Roboto\", \"Cantarell\",\n\"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\",\nsans-serif;\n$font-mono: \"Lucida Console\", \"DejaVu Sans Mono\", \"Ubuntu Mono\", Monaco, monospace;\n$fs-m: 14px;\n$fs-s: 12px;\n\n// Colours\n$positive: #0f7d15;\n$negative: #ab0f0e;\n$info: #0288D1;\n$warning: #cf4d03;\n$positive-dark: #4aa850;\n$negative-dark: #e85c5b;\n$info-dark: #0288D1;\n$warning-dark: #de8a5a;\n\n// Text colours\n$text-dark: #444;\n\n// Shadows\n$bs-light: 0 0 4px 1px #CCC;\n$bs-dark: 0 0 4px 1px rgba(0, 0, 0, 0.5);\n$bs-med: 0 1px 3px 1px rgba(76, 76, 76, 0.26);\n$bs-large: 0 1px 6px 1px rgba(22, 22, 22, 0.2);\n$bs-card: 0 1px 6px -1px rgba(0, 0, 0, 0.1);\n$bs-card-dark: 0 1px 6px -1px rgba(0, 0, 0, 0.5);\n$bs-hover: 0 2px 2px 1px rgba(0,0,0,.13);\n\n// CSS root variables\n:root {\n  --font-body: #{$font-body};\n  --font-code: #{$font-mono};\n\n  --color-primary: #206ea7;\n  --color-primary-light: rgba(32,110,167,0.15);\n  --color-link: #206ea7;\n\n  --color-page: #206ea7;\n  --color-page-draft: #7e50b1;\n  --color-chapter: #af4d0d;\n  --color-book: #077b70;\n  --color-bookshelf: #a94747;\n\n  --color-positive: #{$positive};\n  --color-negative: #{$negative};\n  --color-info: #{$info};\n  --color-warning: #{$warning};\n\n  --bg-disabled: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='100%25' width='100%25'%3E%3Cdefs%3E%3Cpattern id='doodad' width='19' height='19' viewBox='0 0 40 40' patternUnits='userSpaceOnUse' patternTransform='rotate(143)'%3E%3Crect width='100%25' height='100%25' fill='rgba(42, 67, 101,0)'/%3E%3Cpath d='M-10 30h60v20h-60zM-10-10h60v20h-60' fill='rgba(26, 32, 44,0)'/%3E%3Cpath d='M-10 10h60v20h-60zM-10-30h60v20h-60z' fill='rgba(0, 0, 0,0.05)'/%3E%3C/pattern%3E%3C/defs%3E%3Crect fill='url(%23doodad)' height='200%25' width='200%25'/%3E%3C/svg%3E\");\n}\n\n:root.dark-mode {\n  --bg-disabled: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='100%25' width='100%25'%3E%3Cdefs%3E%3Cpattern id='doodad' width='19' height='19' viewBox='0 0 40 40' patternUnits='userSpaceOnUse' patternTransform='rotate(143)'%3E%3Crect width='100%25' height='100%25' fill='rgba(42, 67, 101,0)'/%3E%3Cpath d='M-10 30h60v20h-60zM-10-10h60v20h-60' fill='rgba(26, 32, 44,0)'/%3E%3Cpath d='M-10 10h60v20h-60zM-10-30h60v20h-60z' fill='rgba(255, 255, 255,0.05)'/%3E%3C/pattern%3E%3C/defs%3E%3Crect fill='url(%23doodad)' height='200%25' width='200%25'/%3E%3C/svg%3E\");\n  color-scheme: only dark;\n\n  --color-positive: #4aa850;\n  --color-negative: #e85c5b;\n  --color-warning: #de8a5a;\n}\n:root:not(.dark-mode) {\n  color-scheme: only light;\n}"
  },
  {
    "path": "resources/sass/export-styles.scss",
    "content": "@use \"sass:math\";\n\n@use \"vars\";\n@use \"mixins\";\n@use \"html\";\n@use \"text\";\n@use \"tables\";\n@use \"content\";\n\nhtml, body {\n  background-color: #FFF;\n}\n\nbody {\n  font-family: 'DejaVu Sans', -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Oxygen\", \"Ubuntu\", \"Roboto\", \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  margin: 0;\n  padding: 0;\n  display: block;\n}\n\ntable {\n  border-spacing: 0;\n  border-collapse: collapse;\n}\n\n.page-content {\n  overflow: hidden;\n}\n\n// Prevent code block overflow on export\npre {\n  padding-left: 12px;\n}\npre:before {\n  display: none;\n}\npre code {\n  white-space: pre-wrap;\n}\n\n.page-break {\n  page-break-after: always;\n}\n@media screen {\n  .page-break {\n    border-top: 1px solid #DDD;\n  }\n}\n\nul.contents ul li {\n  list-style: circle;\n}\n\n.chapter-hint {\n  color: #888;\n  margin-top: 32px;\n}\n.chapter-hint + h1 {\n  margin-top: 0;\n}\n\n// PDF specific overrides\nbody.export-format-pdf {\n  font-size: 14px;\n  line-height: 1.2;\n\n  h1, h2, h3, h4, h5, h6 {\n    line-height: 1.2;\n  }\n\n  table {\n    max-width: 800px !important;\n    font-size: 0.8em;\n    width: 100% !important;\n  }\n\n  table td {\n    width: auto !important;\n  }\n\n  .page-content .float {\n    float: none !important;\n  }\n\n  .page-content img.align-left, .page-content img.align-right  {\n    float: none !important;\n    clear: both;\n    display: block;\n  }\n\n}\n\n// DOMPDF pdf export specific overrides\nbody.export-format-pdf.export-engine-dompdf {\n  // Fix for full width linked image sizes on DOMPDF\n  .page-content a > img {\n    max-width: 700px;\n  }\n  // Undoes the above for table images to prevent visually worse scenario, Awaiting next DOMPDF release for patch\n  .page-content td a > img {\n    max-width: 100%;\n  }\n}"
  },
  {
    "path": "resources/sass/styles.scss",
    "content": "@use \"sass:meta\";\n\n@use \"reset\";\n@use \"vars\";\n@use \"mixins\";\n@use \"spacing\";\n@use \"opacity\";\n@use \"html\";\n@use \"text\";\n@use \"colors\";\n@use \"layout\";\n@use \"blocks\";\n@use \"buttons\";\n@use \"tables\";\n@use \"forms\";\n@use \"animations\";\n@use \"tinymce\";\n@use \"editor\";\n@use \"codemirror\";\n@use \"components\";\n@use \"header\";\n@use \"footer\";\n@use \"lists\";\n@use \"pages\";\n@use \"content\";\n\n@media print {\n  @include meta.load-css(\"print\");\n}\n\n// Jquery Sortable Styles\n.dragged {\n  position: absolute;\n  opacity: 0.5;\n  z-index: 2000;\n}\nbody.dragging, body.dragging * {\n  cursor: move !important;\n}\n\n// User Avatar Images\n.avatar {\n  border-radius: 100%;\n  @include mixins.lightDark(background-color, #eee, #000);\n  width: 30px;\n  height: 30px;\n  &.med {\n    width: 40px;\n    height: 40px;\n  }\n  &.large {\n    width: 80px;\n    height: 80px;\n  }\n  &.huge {\n    width: 120px;\n    height: 120px;\n  }\n  &.square {\n    border-radius: 3px;\n  }\n  &[src$=\"user_avatar.png\"] {\n    @include mixins.whenDark {\n      filter: invert(1);\n    }\n  }\n}\n\n// Loading icon\n$loadingSize: 10px;\n.loading-container {\n  position: relative;\n  display: block;\n  margin: vars.$xl auto;\n  > div {\n    width: $loadingSize;\n    height: $loadingSize;\n    border-radius: $loadingSize;\n    display: inline-block;\n    vertical-align: top;\n    transform: translate3d(-10px, 0, 0);\n    margin-top: vars.$xs;\n    animation-name: loadingBob;\n    animation-duration: 1.4s;\n    animation-iteration-count: infinite;\n    animation-timing-function: cubic-bezier(.62, .28, .23, .99);\n    margin-inline-end: 4px;\n    background-color: var(--color-page);\n    animation-delay: -300ms;\n  }\n  > div:first-child {\n      left: -($loadingSize+vars.$xs);\n      background-color: var(--color-book);\n      animation-delay: -600ms;\n  }\n  > div:last-of-type {\n    left: $loadingSize+vars.$xs;\n    background-color: var(--color-chapter);\n    animation-delay: 0ms;\n  }\n  > span {\n    margin-inline-start: vars.$s;\n    font-style: italic;\n    color: #888;\n    vertical-align: top;\n  }\n}\n\n.inline.block .loading-container {\n  margin: vars.$xs vars.$s;\n}\n\n.skip-to-content-link {\n  position: fixed;\n  top: -52px;\n  left: 0;\n  background-color: #FFF;\n  z-index: 15;\n  border-radius: 0 4px 4px 0;\n  display: block;\n  box-shadow: vars.$bs-dark;\n  font-weight: bold;\n  &:focus {\n    top: vars.$xl;\n    outline-offset: -10px;\n    outline: 2px dotted var(--color-link);\n  }\n}\n\n.entity-selector {\n  border: 1px solid #DDD;\n  @include mixins.lightDark(border-color, #ddd, #111);\n  border-radius: 3px;\n  overflow: hidden;\n  font-size: 0.8em;\n  input[type=\"text\"] {\n    width: 100%;\n    display: block;\n    border-radius: 0;\n    border: 0;\n    border-bottom: 1px solid #DDD;\n    font-size: 16px;\n    padding: vars.$s vars.$m;\n  }\n  input[type=\"text\"]:focus {\n    outline: 1px solid var(--color-primary);\n    border-radius: 3px 3px 0 0;\n    outline-offset: -1px;\n  }\n  .entity-list {\n    overflow-y: scroll;\n    height: 400px;\n    @include mixins.lightDark(background-color, #eee, #222);\n    margin-inline-end: 0;\n    margin-inline-start: 0;\n  }\n  .entity-list-item {\n    @include mixins.lightDark(background-color, #fff, #222);\n  }\n  .entity-list-item p {\n    margin-bottom: 0;\n  }\n  .entity-list-item:focus {\n    outline: 2px dotted var(--color-primary);\n    outline-offset: -4px;\n  }\n  .entity-list-item.selected {\n    @include mixins.lightDark(background-color, rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05));\n  }\n  .loading {\n    height: 400px;\n    padding-top: vars.$l;\n  }\n  &.compact {\n    font-size: 10px;\n    .entity-item-snippet {\n      display: none;\n    }\n    h4 {\n      font-size: 14px;\n    }\n  }\n  &.small {\n    .entity-list-item {\n      padding: vars.$xs vars.$m;\n    }\n    .entity-list, .loading {\n      height: 300px;\n    }\n    input[type=\"text\"] {\n      font-size: 13px;\n      padding: vars.$xs vars.$m;\n      height: auto;\n    }\n  }\n}\n\n.fullscreen {\n  border:0;\n  position:fixed;\n  top:0;\n  left:0;\n  right:0;\n  bottom:0;\n  width:100%;\n  height:100%;\n  z-index: 150;\n}\n\n@include mixins.between(vars.$bp-s, vars.$bp-m) {\n  #home-default > .grid.third {\n    display: block;\n    columns: 2;\n    column-gap: vars.$l !important;\n  }\n}\n\n.list-sort-container {\n  display: inline-block;\n  form {\n    display: inline-block;\n  }\n  .list-sort {\n    display: inline-grid;\n    margin-inline-start: vars.$s;\n    grid-template-columns: minmax(120px, max-content) 40px;\n    font-size: 0.9rem;\n    border: 2px solid #DDD;\n    @include mixins.lightDark(border-color, #ddd, #444);\n    border-radius: 4px;\n  }\n  .list-sort-label {\n    font-weight: bold;\n    display: inline-block;\n  }\n  .list-sort-label, .list-sort-toggle {\n    @include mixins.lightDark(color, #555, #888);\n  }\n  .list-sort-type {\n    text-align: start;\n  }\n  .list-sort-toggle, .list-sort-dir {\n    padding: (vars.$xs + 2) vars.$s;\n    cursor: pointer;\n  }\n  .list-sort-dir {\n    border-inline-start: 2px solid #DDD;\n    color: #888;\n    @include mixins.lightDark(border-color, #ddd, #444);\n    .svg-icon {\n      transition: transform ease-in-out 120ms;\n    }\n    &:hover .svg-icon {\n      transform: rotate(180deg);\n    }\n  }\n  .list-sort-toggle {\n    display: block;\n    width: 100%;\n    text-align: start;\n  }\n}\n\n.import-item {\n  border-inline-start: 2px solid currentColor;\n  padding-inline-start: vars.$xs;\n}"
  },
  {
    "path": "resources/views/api-docs/index.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div component=\"api-nav\" class=\"container\">\n\n        <div class=\"grid right-focus reverse-collapse\">\n            <div>\n\n                <div refs=\"api-nav@sidebar\" class=\"sticky-sidebar\">\n\n                    <div class=\"sticky-sidebar-header py-xl\">\n                        <select refs=\"api-nav@select\" name=\"navigation\" id=\"navigation\">\n                            <option value=\"getting-started\" selected>Jump To Section</option>\n                            <option value=\"getting-started\">Getting Started</option>\n                            @foreach($docs as $model => $endpoints)\n                                <option value=\"{{ str_replace(' ', '-', $model) }}\">{{ ucfirst($model) }}</option>\n                                @if($model === 'docs' || $model === 'shelves')\n                                    <hr>\n                                @endif\n                            @endforeach\n                        </select>\n                    </div>\n\n                    <div class=\"mb-xl\">\n                        <p id=\"sidebar-header-getting-started\" class=\"text-uppercase text-muted mb-xm\"><strong>Getting Started</strong></p>\n                        <div class=\"text-mono\">\n                            <div class=\"mb-xs\"><a href=\"#authentication\">Authentication</a></div>\n                            <div class=\"mb-xs\"><a href=\"#request-format\">Request Format</a></div>\n                            <div class=\"mb-xs\"><a href=\"#listing-endpoints\">Listing Endpoints</a></div>\n                            <div class=\"mb-xs\"><a href=\"#error-handling\">Error Handling</a></div>\n                            <div class=\"mb-xs\"><a href=\"#rate-limits\">Rate Limits</a></div>\n                            <div class=\"mb-xs\"><a href=\"#content-security\">Content Security</a></div>\n                        </div>\n                    </div>\n\n                    @foreach($docs as $model => $endpoints)\n                        <div class=\"mb-xl\">\n                            <p id=\"sidebar-header-{{ str_replace(' ', '-', $model) }}\" class=\"text-uppercase text-muted mb-xm\"><strong>{{ $model }}</strong></p>\n\n                            @foreach($endpoints as $endpoint)\n                                <div class=\"mb-xs\">\n                                    <a href=\"#{{ $endpoint['name'] }}\" class=\"text-mono inline block mr-s\">\n                                        <span class=\"api-method\" data-method=\"{{ $endpoint['method'] }}\">{{ $endpoint['method'] }}</span>\n                                    </a>\n                                    <a href=\"#{{ $endpoint['name'] }}\" class=\"text-mono\">\n                                        {{ $endpoint['controller_method_kebab'] }}\n                                    </a>\n                                </div>\n                            @endforeach\n                        </div>\n                    @endforeach\n                </div>\n            </div>\n\n            <div class=\"pt-xl\" style=\"overflow: auto;\">\n\n                <section id=\"section-getting-started\" component=\"code-highlighter\" class=\"card content-wrap auto-height\">\n                    @include('api-docs.parts.getting-started')\n                </section>\n\n                @foreach($docs as $model => $endpoints)\n                    <section id=\"section-{{ str_replace(' ', '-', $model) }}\" class=\"card content-wrap auto-height\">\n                        <h1 class=\"list-heading text-capitals\">{{ $model }}</h1>\n                        @if($endpoints[0]['model_description'])\n                            <p>{{ $endpoints[0]['model_description'] }}</p>\n                        @endif\n                        @foreach($endpoints as $endpoint)\n                            @include('api-docs.parts.endpoint', ['endpoint' => $endpoint, 'loop' => $loop])\n                        @endforeach\n                    </section>\n                @endforeach\n            </div>\n\n        </div>\n\n\n    </div>\n@stop"
  },
  {
    "path": "resources/views/api-docs/parts/endpoint.blade.php",
    "content": "<div class=\"flex-container-row items-center gap-m\">\n    <span class=\"api-method text-mono\" data-method=\"{{ $endpoint['method'] }}\">{{ $endpoint['method'] }}</span>\n    <h5 id=\"{{ $endpoint['name'] }}\" class=\"text-mono pb-xs\">\n        @if($endpoint['controller_method_kebab'] === 'list')\n            <a style=\"color: inherit;\" target=\"_blank\" rel=\"noopener\" href=\"{{ url($endpoint['uri']) }}\">{{ url($endpoint['uri']) }}</a>\n        @else\n            <span>{{ url($endpoint['uri']) }}</span>\n        @endif\n    </h5>\n    <h6 class=\"text-uppercase text-muted text-mono ml-auto\">{{ $endpoint['controller_method_kebab'] }}</h6>\n</div>\n\n<div class=\"mb-m\">\n    @foreach(explode(\"\\n\", $endpoint['description'] ?? '') as $descriptionBlock)\n        <p class=\"mb-xxs\">{{ $descriptionBlock }}</p>\n    @endforeach\n</div>\n\n@if($endpoint['body_params'] ?? false)\n    <details class=\"mb-m\">\n        <summary class=\"text-muted\">{{ $endpoint['method'] === 'GET' ? 'Query' : 'Body'  }} Parameters</summary>\n        <table class=\"table\">\n            <tr>\n                <th>Param Name</th>\n                <th>Value Rules</th>\n            </tr>\n            @foreach($endpoint['body_params'] as $paramName => $rules)\n                <tr>\n                    <td>{{ $paramName }}</td>\n                    <td>\n                        @foreach($rules as $rule)\n                            <code class=\"mr-xs\">{{ $rule }}</code>\n                        @endforeach\n                    </td>\n                </tr>\n            @endforeach\n        </table>\n    </details>\n@endif\n\n@if($endpoint['example_request'] ?? false)\n    <details component=\"details-highlighter\" class=\"mb-m\">\n        <summary class=\"text-muted\">Example Request</summary>\n        <pre><code class=\"language-json\">{{ $endpoint['example_request'] }}</code></pre>\n    </details>\n@endif\n\n@if($endpoint['example_response'] ?? false)\n    <details component=\"details-highlighter\" class=\"mb-m\">\n        <summary class=\"text-muted\">Example Response</summary>\n        <pre><code class=\"language-json\">{{ $endpoint['example_response'] }}</code></pre>\n    </details>\n@endif\n\n@if(!$loop->last)\n    <hr>\n@endif"
  },
  {
    "path": "resources/views/api-docs/parts/getting-started.blade.php",
    "content": "<h1 class=\"list-heading text-capitals mb-l\">Getting Started</h1>\n\n<p class=\"mb-none\">\n    This documentation covers use of the REST API. <br>\n    Examples of API usage, in a variety of programming languages, can be found in the <a href=\"https://codeberg.org/bookstack/api-scripts\" target=\"_blank\" rel=\"noopener noreferrer\">BookStack api-scripts repo on GitHub</a>.\n\n    <br> <br>\n    Some alternative options for extension and customization can be found below:\n</p>\n\n<ul>\n    <li>\n        <a href=\"{{ url('/settings/webhooks') }}\" target=\"_blank\" rel=\"noopener noreferrer\">Webhooks</a> -\n        HTTP POST calls upon events occurring in BookStack.\n    </li>\n    <li>\n        <a href=\"https://github.com/BookStackApp/BookStack/blob/development/dev/docs/visual-theme-system.md\" target=\"_blank\" rel=\"noopener noreferrer\">Visual Theme System</a> -\n        Methods to override views, translations and icons within BookStack.\n    </li>\n    <li>\n        <a href=\"https://github.com/BookStackApp/BookStack/blob/development/dev/docs/logical-theme-system.md\" target=\"_blank\" rel=\"noopener noreferrer\">Logical Theme System</a> -\n        Methods to extend back-end functionality within BookStack.\n    </li>\n</ul>\n\n<hr>\n\n<h5 id=\"authentication\" class=\"text-mono mb-m\">Authentication</h5>\n<p>\n    To access the API a user has to have the <em>\"Access System API\"</em> permission enabled on one of their assigned roles.\n    Permissions to content accessed via the API is limited by the roles & permissions assigned to the user that's used to access the API.\n</p>\n<p>Authentication to use the API is primarily done using API Tokens. Once the <em>\"Access System API\"</em> permission has been assigned to a user, a \"API Tokens\" section should be visible when editing their user profile. Choose \"Create Token\" and enter an appropriate name and expiry date, relevant for your API usage then press \"Save\". A \"Token ID\" and \"Token Secret\" will be immediately displayed. These values should be used as a header in API HTTP requests in the following format:</p>\n<pre><code class=\"language-css\">Authorization: Token &lt;token_id&gt;:&lt;token_secret&gt;</code></pre>\n<p>Here's an example of an authorized cURL request to list books in the system:</p>\n<pre><code class=\"language-shell\">curl --request GET \\\n  --url https://example.com/api/books \\\n  --header 'Authorization: Token C6mdvEQTGnebsmVn3sFNeeuelGEBjyQp:NOvD3VlzuSVuBPNaf1xWHmy7nIRlaj22'</code></pre>\n<p>If already logged into the system within the browser, via a user account with permission to access the API, the system will also accept an existing session meaning you can browse API endpoints directly in the browser or use the browser devtools to play with the API.</p>\n\n<hr>\n\n<h5 id=\"request-format\" class=\"text-mono mb-m\">Request Format</h5>\n\n<p>\n    For endpoints in this documentation that accept data a \"Body Parameters\" table will be available to show the parameters that are accepted in the request.\n    Any rules for the values of such parameters, such as the data-type or if they're required, will be shown alongside the parameter name.\n</p>\n\n<p>\n    The API can accept request data in the following <code>Content-Type</code> formats:\n</p>\n\n<ul>\n    <li>application/json</li>\n    <li>application/x-www-form-urlencoded*</li>\n    <li>multipart/form-data*</li>\n</ul>\n\n<p>\n    <em>\n        * Form requests currently only work for POST requests due to how PHP handles request data.\n        If you need to use these formats for PUT or DELETE requests you can work around this limitation by\n        using a POST request and providing a \"_method\" parameter with the value equal to\n        <code>PUT</code> or <code>DELETE</code>.\n    </em>\n</p>\n\n<p>\n    <em>\n        * Form requests can accept boolean (<code>true</code>/<code>false</code>) values via a <code>1</code> or <code>0</code>.\n    </em>\n</p>\n\n<p>\n    Regardless of format chosen, ensure you set a <code>Content-Type</code> header on requests so that the system can correctly parse your request data.\n    The API is primarily designed to be interfaced using JSON, since responses are always in JSON format, hence examples in this documentation will be shown as JSON.\n    Some endpoints, such as those that receive file data, may require the use of <code>multipart/form-data</code>. This will be mentioned within the description for such endpoints.\n</p>\n\n<p>\n    Some data may be expected in a more complex nested structure such as a nested object or array.\n    These can be sent in non-JSON request formats using square brackets to denote index keys or property names.\n    Below is an example of a JSON request body data and it's equivalent x-www-form-urlencoded representation.\n</p>\n\n<p><strong>JSON</strong></p>\n\n<pre><code class=\"language-json\">{\n  \"name\": \"My new item\",\n  \"locked\": true,\n  \"books\": [105, 263],\n  \"tags\": [{\"name\": \"Tag Name\", \"value\": \"Tag Value\"}],\n}</code></pre>\n\n<p><strong>x-www-form-urlencoded</strong></p>\n\n<pre><code class=\"language-text\">name=My%20new%20item&locked=1&books%5B0%5D=105&books%5B1%5D=263&tags%5B0%5D%5Bname%5D=Tag%20Name&tags%5B0%5D%5Bvalue%5D=Tag%20Value</code></pre>\n\n<p><strong>x-www-form-urlencoded (Decoded for readability)</strong></p>\n\n<pre><code class=\"language-text\">name=My new item\nlocked=1\nbooks[0]=105\nbooks[1]=263\ntags[0][name]=Tag Name\ntags[0][value]=Tag Value</code></pre>\n\n<hr>\n\n<h5 id=\"listing-endpoints\" class=\"text-mono mb-m\">Listing Endpoints</h5>\n<p>Some endpoints will return a list of data models. These endpoints will return an array of the model data under a <code>data</code> property along with a numeric <code>total</code> property to indicate the total number of records found for the query within the system. Here's an example of a listing response:</p>\n<pre><code class=\"language-json\">{\n  \"data\": [\n    {\n      \"id\": 1,\n      \"name\": \"BookStack User Guide\",\n      \"slug\": \"bookstack-user-guide\",\n      \"description\": \"This is a general guide on using BookStack on a day-to-day basis.\",\n      \"created_at\": \"2019-05-05 21:48:46\",\n      \"updated_at\": \"2019-12-11 20:57:31\",\n      \"created_by\": 1,\n      \"updated_by\": 1,\n      \"image_id\": 3\n    }\n  ],\n  \"total\": 16\n}</code></pre>\n<p>\n    There are a number of standard URL parameters that can be supplied to manipulate and page through the results returned from a listing endpoint:\n</p>\n<table class=\"table\">\n    <tr>\n        <th width=\"110\">Parameter</th>\n        <th>Details</th>\n        <th width=\"30%\">Examples</th>\n    </tr>\n    <tr>\n        <td>count</td>\n        <td>\n            Specify how many records will be returned in the response. <br>\n            (Default: {{ config('api.default_item_count') }}, Max: {{ config('api.max_item_count') }})\n        </td>\n        <td>Limit the count to 50<br><code>?count=50</code></td>\n    </tr>\n    <tr>\n        <td>offset</td>\n        <td>\n            Specify how many records to skip over in the response. <br>\n            (Default: 0)\n        </td>\n        <td>Skip over the first 100 records<br><code>?offset=100</code></td>\n    </tr>\n    <tr>\n        <td>sort</td>\n        <td>\n            Specify what field is used to sort the data and the direction of the sort (Ascending or Descending).<br>\n            Value is the name of a field, A <code>+</code> or <code>-</code> prefix dictates ordering. <br>\n            Direction defaults to ascending. <br>\n            Can use most fields shown in the response.\n        </td>\n        <td>\n            Sort by name ascending<br><code>?sort=+name</code> <br> <br>\n            Sort by \"Created At\" date descending<br><code>?sort=-created_at</code>\n        </td>\n    </tr>\n    <tr>\n        <td>filter[&lt;field&gt;]</td>\n        <td>\n            Specify a filter to be applied to the query. Can use most fields shown in the response. <br>\n            By default a filter will apply a \"where equals\" query but the below operations are available using the format filter[&lt;field&gt;:&lt;operation&gt;] <br>\n            <table>\n                <tr>\n                    <td>eq</td>\n                    <td>Where <code>&lt;field&gt;</code> equals the filter value.</td>\n                </tr>\n                <tr>\n                    <td>ne</td>\n                    <td>Where <code>&lt;field&gt;</code> does not equal the filter value.</td>\n                </tr>\n                <tr>\n                    <td>gt</td>\n                    <td>Where <code>&lt;field&gt;</code> is greater than the filter value.</td>\n                </tr>\n                <tr>\n                    <td>lt</td>\n                    <td>Where <code>&lt;field&gt;</code> is less than the filter value.</td>\n                </tr>\n                <tr>\n                    <td>gte</td>\n                    <td>Where <code>&lt;field&gt;</code> is greater than or equal to the filter value.</td>\n                </tr>\n                <tr>\n                    <td>lte</td>\n                    <td>Where <code>&lt;field&gt;</code> is less than or equal to the filter value.</td>\n                </tr>\n                <tr>\n                    <td>like</td>\n                    <td>\n                        Where <code>&lt;field&gt;</code> is \"like\" the filter value. <br>\n                        <code>%</code> symbols can be used as wildcards.\n                    </td>\n                </tr>\n            </table>\n        </td>\n        <td>\n            Filter where id is 5: <br><code>?filter[id]=5</code><br><br>\n            Filter where id is not 5: <br><code>?filter[id:ne]=5</code><br><br>\n            Filter where name contains \"cat\": <br><code>?filter[name:like]=%cat%</code><br><br>\n            Filter where created after 2020-01-01: <br><code>?filter[created_at:gt]=2020-01-01</code>\n        </td>\n    </tr>\n</table>\n\n<hr>\n\n<h5 id=\"error-handling\" class=\"text-mono mb-m\">Error Handling</h5>\n<p>\n    Successful responses will return a 200 or 204 HTTP response code. Errors will return a 4xx or a 5xx HTTP response code depending on the type of error. Errors follow a standard format as shown below. The message provided may be translated depending on the configured language of the system in addition to the API users' language preference. The code provided in the JSON response will match the HTTP response code.\n</p>\n\n<pre><code class=\"language-json\">{\n\t\"error\": {\n\t\t\"code\": 401,\n\t\t\"message\": \"No authorization token found on the request\"\n\t}\n}\n</code></pre>\n\n<hr>\n\n<h5 id=\"rate-limits\" class=\"text-mono mb-m\">Rate Limits</h5>\n<p>\n    The API has built-in per-user rate-limiting to prevent potential abuse using the API.\n    By default, this is set to 180 requests per minute but this can be changed by an administrator\n    by setting an \"API_REQUESTS_PER_MIN\" .env option like so:\n</p>\n\n<pre><code class=\"language-bash\"># The number of API requests that can be made per minute by a single user.\nAPI_REQUESTS_PER_MIN=180</code></pre>\n\n<p>\n    When the limit is reached you will receive a 429 \"Too Many Attempts.\" error response.\n    It's generally good practice to limit requests made from your API client, where possible, to avoid\n    affecting normal use of the system caused by over-consuming system resources.\n    Keep in mind there may be other rate-limiting factors such as web-server & firewall controls.\n</p>\n\n<hr>\n\n<h5 id=\"content-security\" class=\"text-mono mb-m\">Content Security</h5>\n<p>\n    Many of the available endpoints will return content that has been provided by user input.\n    Some of this content may be provided in a certain data-format (Such as HTML or Markdown for page content).\n    Such content is not guaranteed to be safe so keep security in mind when dealing with such user-input.\n    In some cases, the system will apply some filtering to content in an attempt to prevent certain vulnerabilities, but\n    this is not assured to be a bullet-proof defence.\n</p>\n<p>\n    Within its own interfaces, unless disabled, the system makes use of Content Security Policy (CSP) rules to heavily negate\n    cross-site scripting vulnerabilities from user content. If displaying user content externally, it's advised you\n    also use defences such as CSP or the disabling of JavaScript completely.\n</p>"
  },
  {
    "path": "resources/views/attachments/list.blade.php",
    "content": "<div component=\"attachments-list\">\n    @foreach($attachments as $attachment)\n        <div class=\"attachment icon-list\">\n            <div class=\"split-icon-list-item attachment-{{ $attachment->external ? 'link' : 'file' }}\">\n                <a href=\"{{ $attachment->getUrl() }}\"\n                   refs=\"attachments-list@link-type-{{ $attachment->external ? 'link' : 'file' }}\"\n                   @if($attachment->external) target=\"_blank\" @endif>\n                    <div class=\"icon\">@icon($attachment->external ? 'export' : 'file')</div>\n                    <div class=\"label\">{{ $attachment->name }}</div>\n                </a>\n                @if(!$attachment->external)\n                    <div component=\"dropdown\" class=\"icon-list-item-dropdown\">\n                        <button refs=\"dropdown@toggle\" type=\"button\" class=\"icon-list-item-dropdown-toggle\">@icon('caret-down')</button>\n                        <ul refs=\"dropdown@menu\" class=\"dropdown-menu\" role=\"menu\">\n                            <a href=\"{{ $attachment->getUrl(false) }}\" class=\"icon-item\">\n                                @icon('download')\n                                <div>{{ trans('common.download') }}</div>\n                            </a>\n                            <a href=\"{{ $attachment->getUrl(true) }}\" target=\"_blank\" class=\"icon-item\">\n                                @icon('export')\n                                <div>{{ trans('common.open_in_tab') }}</div>\n                            </a>\n                        </ul>\n                    </div>\n                @endif\n            </div>\n        </div>\n    @endforeach\n</div>"
  },
  {
    "path": "resources/views/attachments/manager-edit-form.blade.php",
    "content": "<div component=\"ajax-form\"\n     option:ajax-form:url=\"/attachments/{{ $attachment->id }}\"\n     option:ajax-form:method=\"put\"\n     option:ajax-form:response-container=\"#edit-form-container\"\n     option:ajax-form:success-message=\"{{ trans('entities.attachments_updated_success') }}\">\n    <h5>{{ trans('entities.attachments_edit_file') }}</h5>\n\n    <div class=\"form-group\">\n        <label for=\"attachment_edit_name\">{{ trans('entities.attachments_edit_file_name') }}</label>\n        <input type=\"text\" id=\"attachment_edit_name\"\n               name=\"attachment_edit_name\"\n               value=\"{{ $attachment_edit_name ?? $attachment->name ?? '' }}\"\n               placeholder=\"{{ trans('entities.attachments_edit_file_name') }}\">\n        @if($errors->has('attachment_edit_name'))\n            <div class=\"text-neg text-small\">{{ $errors->first('attachment_edit_name') }}</div>\n        @endif\n    </div>\n\n    <div component=\"tabs\" class=\"tab-container\">\n        <div class=\"nav-tabs\" role=\"tablist\">\n            <button id=\"attachment-edit-file-tab\"\n                    type=\"button\"\n                    aria-controls=\"attachment-edit-file-panel\"\n                    aria-selected=\"{{ $attachment->external ? 'false' : 'true' }}\"\n                    role=\"tab\">{{ trans('entities.attachments_upload') }}</button>\n            <button id=\"attachment-edit-link-tab\"\n                    type=\"button\"\n                    aria-controls=\"attachment-edit-link-panel\"\n                    aria-selected=\"{{ $attachment->external ? 'true' : 'false' }}\"\n                    role=\"tab\">{{ trans('entities.attachments_set_link') }}</button>\n        </div>\n        <div id=\"attachment-edit-file-panel\"\n             @if($attachment->external) hidden @endif\n             tabindex=\"0\"\n             role=\"tabpanel\"\n             aria-labelledby=\"attachment-edit-file-tab\"\n             class=\"mb-m\">\n            @include('form.simple-dropzone', [\n                'placeholder' => trans('entities.attachments_edit_drop_upload'),\n                'url' =>  url('/attachments/upload/' . $attachment->id),\n                'successMessage' => trans('entities.attachments_file_updated'),\n            ])\n        </div>\n        <div id=\"attachment-edit-link-panel\"\n             @if(!$attachment->external) hidden @endif\n             tabindex=\"0\"\n             role=\"tabpanel\"\n             aria-labelledby=\"attachment-edit-link-tab\">\n            <div class=\"form-group\">\n                <label for=\"attachment_edit_url\">{{ trans('entities.attachments_link_url') }}</label>\n                <input type=\"text\" id=\"attachment_edit_url\"\n                       name=\"attachment_edit_url\"\n                       value=\"{{ $attachment_edit_url ?? ($attachment->external ? $attachment->path : '')  }}\"\n                       placeholder=\"{{ trans('entities.attachment_link') }}\">\n                @if($errors->has('attachment_edit_url'))\n                    <div class=\"text-neg text-small\">{{ $errors->first('attachment_edit_url') }}</div>\n                @endif\n            </div>\n        </div>\n    </div>\n\n    <button component=\"event-emit-select\"\n            option:event-emit-select:name=\"edit-back\"\n            type=\"button\"\n            class=\"button outline\">{{ trans('common.back') }}</button>\n    <button refs=\"ajax-form@submit\" type=\"button\" class=\"button\">{{ trans('common.save') }}</button>\n</div>"
  },
  {
    "path": "resources/views/attachments/manager-link-form.blade.php",
    "content": "{{--\n@pageId\n--}}\n<div component=\"ajax-form\"\n     option:ajax-form:url=\"/attachments/link\"\n     option:ajax-form:method=\"post\"\n     option:ajax-form:response-container=\"#link-form-container\"\n     option:ajax-form:success-message=\"{{ trans('entities.attachments_link_attached') }}\">\n    <input type=\"hidden\" name=\"attachment_link_uploaded_to\" value=\"{{ $pageId }}\">\n    <p class=\"text-muted small\">{{ trans('entities.attachments_explain_link') }}</p>\n    <div class=\"form-group\">\n        <label for=\"attachment_link_name\">{{ trans('entities.attachments_link_name') }}</label>\n        <input name=\"attachment_link_name\" id=\"attachment_link_name\" type=\"text\" placeholder=\"{{ trans('entities.attachments_link_name') }}\" value=\"{{ $attachment_link_name ?? '' }}\">\n        @if($errors->has('attachment_link_name'))\n            <div class=\"text-neg text-small\">{{ $errors->first('attachment_link_name') }}</div>\n        @endif\n    </div>\n    <div class=\"form-group\">\n        <label for=\"attachment_link_url\">{{ trans('entities.attachments_link_url') }}</label>\n        <input name=\"attachment_link_url\" id=\"attachment_link_url\" type=\"text\" placeholder=\"{{ trans('entities.attachments_link_url_hint') }}\" value=\"{{ $attachment_link_url ?? '' }}\">\n        @if($errors->has('attachment_link_url'))\n            <div class=\"text-neg text-small\">{{ $errors->first('attachment_link_url') }}</div>\n        @endif\n    </div>\n    <button component=\"event-emit-select\"\n            option:event-emit-select:name=\"edit-back\"\n            type=\"button\" class=\"button outline\">{{ trans('common.cancel') }}</button>\n    <button refs=\"ajax-form@submit\"\n            type=\"button\"\n            class=\"button\">{{ trans('entities.attach') }}</button>\n</div>"
  },
  {
    "path": "resources/views/attachments/manager-list.blade.php",
    "content": "<div component=\"sortable-list\"\n     option:sortable-list:handle-selector=\".handle, a\">\n    @foreach($attachments as $attachment)\n        <div component=\"ajax-delete-row\"\n             option:ajax-delete-row:url=\"{{ url('/attachments/' . $attachment->id) }}\"\n             data-id=\"{{ $attachment->id }}\"\n             data-drag-content=\"{{ json_encode($attachment->editorContent()) }}\"\n             class=\"card drag-card\">\n            <div class=\"handle\">@icon('grip')</div>\n            <div class=\"py-s\">\n                <a href=\"{{ $attachment->getUrl() }}\" target=\"_blank\" rel=\"noopener\">{{ $attachment->name }}</a>\n            </div>\n            <div class=\"flex-fill justify-flex-end\">\n                <button component=\"event-emit-select\"\n                        option:event-emit-select:name=\"insert\"\n                        type=\"button\"\n                        title=\"{{ trans('entities.attachments_insert_link') }}\"\n                        class=\"drag-card-action text-center text-link\">@icon('link')</button>\n                @if(userCan(\\BookStack\\Permissions\\Permission::AttachmentUpdate, $attachment))\n                    <button component=\"event-emit-select\"\n                            option:event-emit-select:name=\"edit\"\n                            option:event-emit-select:id=\"{{ $attachment->id }}\"\n                            type=\"button\"\n                            title=\"{{ trans('common.edit') }}\"\n                            class=\"drag-card-action text-center text-link\">@icon('edit')</button>\n                @endif\n                @if(userCan(\\BookStack\\Permissions\\Permission::AttachmentDelete, $attachment))\n                    <div component=\"dropdown\" class=\"flex-fill relative\">\n                        <button refs=\"dropdown@toggle\"\n                                type=\"button\"\n                                title=\"{{ trans('common.delete') }}\"\n                                class=\"drag-card-action text-center text-neg\">@icon('close')</button>\n                        <div refs=\"dropdown@menu\" class=\"dropdown-menu\">\n                            <p class=\"text-neg small px-m mb-xs\">{{ trans('entities.attachments_delete') }}</p>\n                            <button refs=\"ajax-delete-row@delete\" type=\"button\" class=\"text-link small delete text-item\">{{ trans('common.confirm') }}</button>\n                        </div>\n                    </div>\n                @endif\n            </div>\n        </div>\n    @endforeach\n    @if (count($attachments) === 0)\n        <p class=\"small text-muted\">\n            {{ trans('entities.attachments_no_files') }}\n        </p>\n    @endif\n</div>"
  },
  {
    "path": "resources/views/attachments/manager.blade.php",
    "content": "<div style=\"display: block;\"\n     refs=\"editor-toolbox@tab-content\"\n     data-tab-content=\"files\"\n     component=\"attachments\"\n     option:attachments:page-id=\"{{ $page->id ?? 0 }}\"\n     class=\"toolbox-tab-content\">\n\n    <h4>{{ trans('entities.attachments') }}</h4>\n    <div component=\"dropzone\"\n         option:dropzone:url=\"{{ url('/attachments/upload?uploaded_to=' . $page->id) }}\"\n         option:dropzone:success-message=\"{{ trans('entities.attachments_file_uploaded') }}\"\n         option:dropzone:error-message=\"{{ trans('errors.attachment_upload_error') }}\"\n         option:dropzone:upload-limit=\"{{ config('app.upload_limit') }}\"\n         option:dropzone:upload-limit-message=\"{{ trans('errors.server_upload_limit') }}\"\n         option:dropzone:zone-text=\"{{ trans('entities.attachments_dropzone') }}\"\n         option:dropzone:file-accept=\"*\"\n         option:dropzone:allow-multiple=\"true\"\n         class=\"px-l files\">\n\n        <div refs=\"attachments@list-container dropzone@drop-target\" class=\"relative\">\n            <p class=\"text-muted small\">{{ trans('entities.attachments_explain') }} <span\n                        class=\"text-warn\">{{ trans('entities.attachments_explain_instant_save') }}</span></p>\n\n            <hr class=\"mb-s\">\n\n            <div class=\"flex-container-row\">\n                <button refs=\"dropzone@select-button\" type=\"button\" class=\"button outline small\">{{ trans('entities.attachments_upload') }}</button>\n                <button refs=\"attachments@attach-link-button\" type=\"button\" class=\"button outline small\">{{ trans('entities.attachments_link') }}</button>\n            </div>\n            <div>\n                <p class=\"text-muted text-small\">{{ trans('entities.attachments_upload_drop') }}</p>\n            </div>\n            <div refs=\"dropzone@status-area\" class=\"fixed top-right px-m py-m\"></div>\n\n            <hr>\n\n            <div refs=\"attachments@list-panel\">\n                @include('attachments.manager-list', ['attachments' => $page->attachments->all()])\n            </div>\n\n        </div>\n    </div>\n\n    <div id=\"link-form-container\" refs=\"attachments@links-container\" hidden class=\"px-l\">\n        @include('attachments.manager-link-form', ['pageId' => $page->id])\n    </div>\n\n    <div id=\"edit-form-container\" refs=\"attachments@edit-container\" hidden class=\"px-l\"></div>\n\n</div>"
  },
  {
    "path": "resources/views/auth/invite-set-password.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('content')\n\n    <div class=\"container very-small mt-xl\">\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ trans('auth.user_invite_page_welcome', ['appName' => setting('app-name')]) }}</h1>\n            <p>{{ trans('auth.user_invite_page_text', ['appName' => setting('app-name')]) }}</p>\n\n            <form action=\"{{ url('/register/invite/' . $token) }}\" method=\"POST\" class=\"stretch-inputs\">\n                {!! csrf_field() !!}\n\n                <div class=\"form-group\">\n                    <label for=\"password\">{{ trans('auth.password') }}</label>\n                    @include('form.password', ['name' => 'password', 'placeholder' => trans('auth.password_hint')])\n                </div>\n\n                <div class=\"text-right\">\n                    <button class=\"button\">{{ trans('auth.user_invite_page_confirm_button') }}</button>\n                </div>\n\n            </form>\n\n        </div>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/auth/login-initiate.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('content')\n\n    <div class=\"container very-small\">\n\n        <div class=\"my-l\">&nbsp;</div>\n\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ trans('auth.auto_init_starting') }}</h1>\n\n            <div style=\"display:none\">\n                @include('auth.parts.login-form-' . $authMethod)\n            </div>\n\n            <div class=\"grid half left-focus\">\n                <div>\n                    <p class=\"text-small\">{{ trans('auth.auto_init_starting_desc') }}</p>\n                    <p>\n                        <button type=\"submit\" form=\"login-form\" class=\"p-none text-button hover-underline\">\n                            {{ trans('auth.auto_init_start_link') }}\n                        </button>\n                    </p>\n                </div>\n                <div class=\"text-center\">\n                    @include('common.loading-icon')\n                </div>\n            </div>\n\n            <script nonce=\"{{ $cspNonce }}\">\n                window.addEventListener('load', () => document.forms['login-form'].submit());\n            </script>\n\n        </div>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/auth/login.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('content')\n\n    <div class=\"container very-small\">\n\n        <div class=\"my-l\">&nbsp;</div>\n\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ Str::title(trans('auth.log_in')) }}</h1>\n\n            @include('auth.parts.login-message')\n\n            @include('auth.parts.login-form-' . $authMethod)\n\n            @if(count($socialDrivers) > 0)\n                <hr class=\"my-l\">\n                @foreach($socialDrivers as $driver => $name)\n                    <div>\n                        <a id=\"social-login-{{$driver}}\" class=\"button outline svg\" href=\"{{ url(\"/login/service/\" . $driver) }}\">\n                            @icon('auth/' . $driver)\n                            <span>{{ trans('auth.log_in_with', ['socialDriver' => $name]) }}</span>\n                        </a>\n                    </div>\n                @endforeach\n            @endif\n\n            @if(setting('registration-enabled') && config('auth.method') === 'standard')\n                <div class=\"text-center pb-s\">\n                    <hr class=\"my-l\">\n                    <a href=\"{{ url('/register') }}\">{{ trans('auth.dont_have_account') }}</a>\n                </div>\n            @endif\n        </div>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/auth/parts/login-form-ldap.blade.php",
    "content": "<form action=\"{{ url('/login') }}\" method=\"POST\" id=\"login-form\" class=\"mt-l\">\n    {!! csrf_field() !!}\n\n    <div class=\"stretch-inputs\">\n        <div class=\"form-group\">\n            <label for=\"username\">{{ trans('auth.username') }}</label>\n            @include('form.text', ['name' => 'username', 'autofocus' => true])\n        </div>\n\n        @if(session('request-email', false) === true)\n            <div class=\"form-group\">\n                <label for=\"email\">{{ trans('auth.email') }}</label>\n                @include('form.text', ['name' => 'email'])\n                <span class=\"text-neg\">{{ trans('auth.ldap_email_hint') }}</span>\n            </div>\n        @endif\n\n        <div class=\"form-group\">\n            <label for=\"password\">{{ trans('auth.password') }}</label>\n            @include('form.password', ['name' => 'password'])\n        </div>\n\n        <div class=\"form-group text-right pt-s\">\n            <button class=\"button\">{{ Str::title(trans('auth.log_in')) }}</button>\n        </div>\n    </div>\n\n</form>"
  },
  {
    "path": "resources/views/auth/parts/login-form-oidc.blade.php",
    "content": "<form action=\"{{ url('/oidc/login') }}\" method=\"POST\" id=\"login-form\" class=\"mt-l\">\n    {!! csrf_field() !!}\n\n    <div>\n        <button id=\"oidc-login\" class=\"button outline svg\">\n            @icon('oidc')\n            <span>{{ trans('auth.log_in_with', ['socialDriver' => config('oidc.name')]) }}</span>\n        </button>\n    </div>\n\n</form>\n"
  },
  {
    "path": "resources/views/auth/parts/login-form-saml2.blade.php",
    "content": "<form action=\"{{ url('/saml2/login') }}\" method=\"POST\" id=\"login-form\" class=\"mt-l\">\n    {!! csrf_field() !!}\n\n    <div>\n        <button id=\"saml-login\" class=\"button outline svg\">\n            @icon('saml2')\n            <span>{{ trans('auth.log_in_with', ['socialDriver' => config('saml2.name')]) }}</span>\n        </button>\n    </div>\n\n</form>"
  },
  {
    "path": "resources/views/auth/parts/login-form-standard.blade.php",
    "content": "<form action=\"{{ url('/login') }}\" method=\"POST\" id=\"login-form\" class=\"mt-l\">\n    {!! csrf_field() !!}\n\n    <div class=\"stretch-inputs\">\n        <div class=\"form-group\">\n            <label for=\"email\">{{ trans('auth.email') }}</label>\n            @include('form.text', ['name' => 'email', 'autofocus' => true])\n        </div>\n\n        <div class=\"form-group\">\n            <label for=\"password\">{{ trans('auth.password') }}</label>\n            @include('form.password', ['name' => 'password'])\n            <div class=\"small mt-s\">\n                <a href=\"{{ url('/password/email') }}\">{{ trans('auth.forgot_password') }}</a>\n            </div>\n        </div>\n    </div>\n\n    <div class=\"grid half collapse-xs gap-xl v-center\">\n        <div class=\"text-left ml-xxs\">\n            @include('form.custom-checkbox', [\n                'name' => 'remember',\n                'checked' => false,\n                'value' => 'on',\n                'label' => trans('auth.remember_me'),\n            ])\n        </div>\n\n        <div class=\"text-right\">\n            <button class=\"button\">{{ Str::title(trans('auth.log_in')) }}</button>\n        </div>\n    </div>\n\n</form>\n\n\n"
  },
  {
    "path": "resources/views/auth/parts/login-message.blade.php",
    "content": "{{-- This is a placeholder template file provided as a --}}\n{{-- convenience to users of the visual theme system. --}}"
  },
  {
    "path": "resources/views/auth/parts/register-message.blade.php",
    "content": "{{-- This is a placeholder template file provided as a --}}\n{{-- convenience to users of the visual theme system. --}}"
  },
  {
    "path": "resources/views/auth/passwords/email.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('content')\n    <div class=\"container very-small mt-xl\">\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ trans('auth.reset_password') }}</h1>\n\n            <p class=\"text-muted small\">{{ trans('auth.reset_password_send_instructions') }}</p>\n\n            <form action=\"{{ url(\"/password/email\") }}\" method=\"POST\" class=\"stretch-inputs\">\n                {!! csrf_field() !!}\n\n                <div class=\"form-group\">\n                    <label for=\"email\">{{ trans('auth.email') }}</label>\n                    @include('form.text', ['name' => 'email'])\n                </div>\n\n                <div class=\"from-group text-right mt-m\">\n                    <button class=\"button\">{{ trans('auth.reset_password_send_button') }}</button>\n                </div>\n            </form>\n\n        </div>\n    </div>\n@stop"
  },
  {
    "path": "resources/views/auth/passwords/reset.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('content')\n\n    <div class=\"container very-small mt-xl\">\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ trans('auth.reset_password') }}</h1>\n\n            <form action=\"{{ url(\"/password/reset\") }}\" method=\"POST\" class=\"stretch-inputs\">\n                {!! csrf_field() !!}\n                <input type=\"hidden\" name=\"token\" value=\"{{ $token }}\">\n\n                <div class=\"form-group\">\n                    <label for=\"email\">{{ trans('auth.email') }}</label>\n                    @include('form.text', ['name' => 'email'])\n                </div>\n\n                <div class=\"form-group\">\n                    <label for=\"password\">{{ trans('auth.password') }}</label>\n                    @include('form.password', ['name' => 'password'])\n                </div>\n\n                <div class=\"form-group\">\n                    <label for=\"password_confirmation\">{{ trans('auth.password_confirm') }}</label>\n                    @include('form.password', ['name' => 'password_confirmation'])\n                </div>\n\n                <div class=\"from-group text-right mt-m\">\n                    <button class=\"button\">{{ trans('auth.reset_password') }}</button>\n                </div>\n            </form>\n\n        </div>\n    </div>\n\n@stop"
  },
  {
    "path": "resources/views/auth/register-confirm-accept.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('content')\n\n    <div class=\"container very-small mt-xl\">\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ trans('auth.email_confirm_thanks') }}</h1>\n\n            <p class=\"mb-none\">{{ trans('auth.email_confirm_thanks_desc') }}</p>\n\n            <div class=\"flex-container-row items-center wrap\">\n                <div class=\"flex min-width-s\">\n                    @include('common.loading-icon')\n                </div>\n                <div class=\"flex min-width-s text-s-right\">\n                    <form component=\"auto-submit\" action=\"{{ url('/register/confirm/accept') }}\" method=\"post\">\n                        {{ csrf_field() }}\n                        <input type=\"hidden\" name=\"token\" value=\"{{ $token }}\">\n                        <button class=\"text-button\">{{ trans('common.continue') }}</button>\n                    </form>\n                </div>\n            </div>\n\n        </div>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/auth/register-confirm-awaiting.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('content')\n\n    <div class=\"container very-small mt-xl\">\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ trans('auth.email_not_confirmed') }}</h1>\n\n            <p>{{ trans('auth.email_not_confirmed_text') }}<br>\n                {{ trans('auth.email_not_confirmed_click_link') }}\n            </p>\n            <p>\n                {{ trans('auth.email_not_confirmed_resend') }}\n            </p>\n\n            <form action=\"{{ url(\"/register/confirm/resend\") }}\" method=\"POST\" class=\"stretch-inputs\">\n                {{ csrf_field() }}\n                <div class=\"form-group text-right mt-m\">\n                    <button type=\"submit\" class=\"button\">{{ trans('auth.email_not_confirmed_resend_button') }}</button>\n                </div>\n            </form>\n\n        </div>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/auth/register-confirm.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('content')\n\n    <div class=\"container very-small mt-xl\">\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ trans('auth.register_thanks') }}</h1>\n            <p>{{ trans('auth.register_confirm', ['appName' => setting('app-name')]) }}</p>\n        </div>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/auth/register.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('content')\n    <div class=\"container very-small\">\n\n        <div class=\"my-l\">&nbsp;</div>\n\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ Str::title(trans('auth.sign_up')) }}</h1>\n\n            @include('auth.parts.register-message')\n\n            <form action=\"{{ url(\"/register\") }}\" method=\"POST\" class=\"mt-l stretch-inputs\">\n                {!! csrf_field() !!}\n\n                {{-- Simple honeypot field --}}\n                <div class=\"form-group ambrosia-container\" aria-hidden=\"true\">\n                    <label for=\"username\">{{ trans('auth.name') }}</label>\n                    @include('form.text', ['name' => 'username'])\n                </div>\n\n                <div class=\"form-group\">\n                    <label for=\"name\">{{ trans('auth.name') }}</label>\n                    @include('form.text', ['name' => 'name'])\n                </div>\n\n                <div class=\"form-group\">\n                    <label for=\"email\">{{ trans('auth.email') }}</label>\n                    @include('form.text', ['name' => 'email'])\n                </div>\n\n                <div class=\"form-group\">\n                    <label for=\"password\">{{ trans('auth.password') }}</label>\n                    @include('form.password', ['name' => 'password', 'placeholder' => trans('auth.password_hint')])\n                </div>\n\n                <div class=\"grid half collapse-xs gap-xl v-center mt-m\">\n                    <div class=\"text-small\">\n                        <a href=\"{{ url('/login') }}\">{{ trans('auth.already_have_account') }}</a>\n                    </div>\n                    <div class=\"from-group text-right\">\n                        <button class=\"button\">{{ trans('auth.create_account') }}</button>\n                    </div>\n                </div>\n\n            </form>\n\n            @if(count($socialDrivers) > 0)\n                <hr class=\"my-l\">\n                @foreach($socialDrivers as $driver => $name)\n                    <div>\n                        <a id=\"social-register-{{$driver}}\" class=\"button outline svg\" href=\"{{ url(\"/register/service/\" . $driver) }}\">\n                            @icon('auth/' . $driver)\n                            <span>{{ trans('auth.sign_up_with', ['socialDriver' => $name]) }}</span>\n                        </a>\n                    </div>\n                @endforeach\n            @endif\n\n        </div>\n    </div>\n@stop\n"
  },
  {
    "path": "resources/views/books/copy.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                $book,\n                $book->getUrl('/copy') => [\n                    'text' => trans('entities.books_copy'),\n                    'icon' => 'copy',\n                ]\n            ]])\n        </div>\n\n        <div class=\"card content-wrap auto-height\">\n\n            <h1 class=\"list-heading\">{{ trans('entities.books_copy') }}</h1>\n\n            <form action=\"{{ $book->getUrl('/copy') }}\" method=\"POST\">\n                {!! csrf_field() !!}\n\n                <div class=\"form-group title-input\">\n                    <label for=\"name\">{{ trans('common.name') }}</label>\n                    @include('form.text', ['name' => 'name'])\n                </div>\n\n                @include('entities.copy-considerations')\n\n                <div class=\"form-group text-right\">\n                    <a href=\"{{ $book->getUrl() }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                    <button type=\"submit\" class=\"button\">{{ trans('entities.books_copy') }}</button>\n                </div>\n            </form>\n\n        </div>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/books/create.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n    <div class=\"container small\">\n        <div class=\"my-s\">\n            @if (isset($bookshelf))\n                @include('entities.breadcrumbs', ['crumbs' => [\n                    $bookshelf,\n                    $bookshelf->getUrl('/create-book') => [\n                        'text' => trans('entities.books_create'),\n                        'icon' => 'add'\n                    ]\n                ]])\n            @else\n                @include('entities.breadcrumbs', ['crumbs' => [\n                    '/books' => [\n                        'text' => trans('entities.books'),\n                        'icon' => 'book'\n                    ],\n                    '/create-book' => [\n                        'text' => trans('entities.books_create'),\n                        'icon' => 'add'\n                    ]\n                ]])\n            @endif\n        </div>\n\n        <main class=\"content-wrap card\">\n            <h1 class=\"list-heading\">{{ trans('entities.books_create') }}</h1>\n            <form action=\"{{ $bookshelf?->getUrl('/create-book') ?? url('/books') }}\" method=\"POST\" enctype=\"multipart/form-data\">\n                @include('books.parts.form', [\n                    'returnLocation' => $bookshelf?->getUrl() ?? url('/books')\n                ])\n            </form>\n        </main>\n    </div>\n\n@stop"
  },
  {
    "path": "resources/views/books/delete.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                $book,\n                $book->getUrl('/delete') => [\n                    'text' => trans('entities.books_delete'),\n                    'icon' => 'delete',\n                ]\n            ]])\n        </div>\n\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ trans('entities.books_delete') }}</h1>\n            <p>{{ trans('entities.books_delete_explain', ['bookName' => $book->name]) }}</p>\n            <p class=\"text-neg\"><strong>{{ trans('entities.books_delete_confirmation') }}</strong></p>\n\n            <form action=\"{{$book->getUrl()}}\" method=\"POST\" class=\"text-right\">\n                {!! csrf_field() !!}\n                <input type=\"hidden\" name=\"_method\" value=\"DELETE\">\n                <a href=\"{{$book->getUrl()}}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                <button type=\"submit\" class=\"button\">{{ trans('common.confirm') }}</button>\n            </form>\n        </div>\n\n    </div>\n\n@stop"
  },
  {
    "path": "resources/views/books/edit.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                $book,\n                $book->getUrl('/edit') => [\n                    'text' => trans('entities.books_edit'),\n                    'icon' => 'edit',\n                ]\n            ]])\n        </div>\n\n        <main class=\"content-wrap card auto-height\">\n            <h1 class=\"list-heading\">{{ trans('entities.books_edit') }}</h1>\n            <form action=\"{{ $book->getUrl() }}\" method=\"POST\" enctype=\"multipart/form-data\">\n                <input type=\"hidden\" name=\"_method\" value=\"PUT\">\n                @include('books.parts.form', [\n                    'model' => $book,\n                    'returnLocation' => $book->getUrl()\n                ])\n            </form>\n        </main>\n\n\n        @if(userCan(\\BookStack\\Permissions\\Permission::BookDelete, $book) && userCan(\\BookStack\\Permissions\\Permission::BookCreateAll) && userCan(\\BookStack\\Permissions\\Permission::BookshelfCreateAll))\n            @include('books.parts.convert-to-shelf', ['book' => $book])\n        @endif\n    </div>\n@stop"
  },
  {
    "path": "resources/views/books/index.blade.php",
    "content": "@extends('layouts.tri')\n\n@section('body')\n    @include('books.parts.list', ['books' => $books, 'view' => $view, 'listOptions' => $listOptions])\n@stop\n\n@section('left')\n    @include('books.parts.index-sidebar-section-recents', ['recents' => $recents])\n    @include('books.parts.index-sidebar-section-popular', ['popular' => $popular])\n    @include('books.parts.index-sidebar-section-new', ['new' => $new])\n@stop\n\n@section('right')\n    @include('books.parts.index-sidebar-section-actions', ['view' => $view])\n@stop\n"
  },
  {
    "path": "resources/views/books/parts/convert-to-shelf.blade.php",
    "content": "<div class=\"content-wrap card auto-height\">\n    <h2 class=\"list-heading\">{{ trans('entities.convert_to_shelf') }}</h2>\n    <p>\n        {{ trans('entities.convert_to_shelf_contents_desc') }}\n        <br><br>\n        {{ trans('entities.convert_to_shelf_permissions_desc') }}\n    </p>\n    <div class=\"text-right\">\n        <div component=\"dropdown\" class=\"dropdown-container\">\n            <button refs=\"dropdown@toggle\" class=\"button outline\" aria-haspopup=\"true\" aria-expanded=\"false\">{{ trans('entities.convert_book') }}</button>\n            <ul refs=\"dropdown@menu\" class=\"dropdown-menu\" role=\"menu\">\n                <li class=\"px-m py-s text-small text-muted\">\n                    {{ trans('entities.convert_book_confirm') }}\n                    <br>\n                    {{ trans('entities.convert_undo_warning') }}\n                </li>\n                <li>\n                    <form action=\"{{ $book->getUrl('/convert-to-shelf') }}\" method=\"POST\">\n                        {!! csrf_field() !!}\n                        <button type=\"submit\" class=\"text-link text-item\">{{ trans('common.confirm') }}</button>\n                    </form>\n                </li>\n            </ul>\n        </div>\n    </div>\n</div>"
  },
  {
    "path": "resources/views/books/parts/form.blade.php",
    "content": "{{ csrf_field() }}\n<div class=\"form-group title-input\">\n    <label for=\"name\">{{ trans('common.name') }}</label>\n    @include('form.text', ['name' => 'name', 'autofocus' => true])\n</div>\n\n<div class=\"form-group description-input\">\n    <label for=\"description_html\">{{ trans('common.description') }}</label>\n    @include('form.description-html-input')\n</div>\n\n<div class=\"form-group collapsible\" component=\"collapsible\" id=\"logo-control\">\n    <button refs=\"collapsible@trigger\" type=\"button\" class=\"collapse-title text-link\" aria-expanded=\"false\">\n        <label>{{ trans('common.cover_image') }}</label>\n    </button>\n    <div refs=\"collapsible@content\" class=\"collapse-content\">\n        <p class=\"small\">{{ trans('common.cover_image_description') }}</p>\n\n        @include('form.image-picker', [\n            'defaultImage' => url('/book_default_cover.png'),\n            'currentImage' => (($model ?? null)?->coverInfo()->getUrl(440, 250, null) ?? url('/book_default_cover.png')),\n            'name' => 'image',\n            'imageClass' => 'cover'\n        ])\n    </div>\n</div>\n\n<div class=\"form-group collapsible\" component=\"collapsible\" id=\"tags-control\">\n    <button refs=\"collapsible@trigger\" type=\"button\" class=\"collapse-title text-link\" aria-expanded=\"false\">\n        <label for=\"tag-manager\">{{ trans('entities.book_tags') }}</label>\n    </button>\n    <div refs=\"collapsible@content\" class=\"collapse-content\">\n        @include('entities.tag-manager', ['entity' => $book ?? null])\n    </div>\n</div>\n\n<div class=\"form-group collapsible\" component=\"collapsible\" id=\"template-control\">\n    <button refs=\"collapsible@trigger\" type=\"button\" class=\"collapse-title text-link\" aria-expanded=\"false\">\n        <label for=\"template-manager\">{{ trans('entities.default_template') }}</label>\n    </button>\n    <div refs=\"collapsible@content\" class=\"collapse-content\">\n        @include('entities.template-selector', ['entity' => $book ?? null])\n    </div>\n</div>\n\n<div class=\"form-group text-right\">\n    <a href=\"{{ $returnLocation }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n    <button type=\"submit\" class=\"button\">{{ trans('entities.books_save') }}</button>\n</div>\n\n@include('entities.selector-popup')\n@include('form.editor-translations')"
  },
  {
    "path": "resources/views/books/parts/index-sidebar-section-actions.blade.php",
    "content": "<div id=\"actions\" class=\"actions mb-xl\">\n    <h5>{{ trans('common.actions') }}</h5>\n    <div class=\"icon-list text-link\">\n        @if(userCan(\\BookStack\\Permissions\\Permission::BookCreateAll))\n            <a href=\"{{ url(\"/create-book\") }}\" data-shortcut=\"new\" class=\"icon-list-item\">\n                <span>@icon('add')</span>\n                <span>{{ trans('entities.books_create') }}</span>\n            </a>\n        @endif\n\n        @include('entities.view-toggle', ['view' => $view, 'type' => 'books'])\n\n        <a href=\"{{ url('/tags') }}\" class=\"icon-list-item\">\n            <span>@icon('tag')</span>\n            <span>{{ trans('entities.tags_view_tags') }}</span>\n        </a>\n\n        @if(userCan(\\BookStack\\Permissions\\Permission::ContentImport))\n            <a href=\"{{ url('/import') }}\" class=\"icon-list-item\">\n                <span>@icon('upload')</span>\n                <span>{{ trans('entities.import') }}</span>\n            </a>\n        @endif\n    </div>\n</div>"
  },
  {
    "path": "resources/views/books/parts/index-sidebar-section-new.blade.php",
    "content": "<div id=\"new\" class=\"mb-xl\">\n    <h5>{{ trans('entities.books_new') }}</h5>\n    @if(count($new) > 0)\n        @include('entities.list', ['entities' => $new, 'style' => 'compact'])\n    @else\n        <p class=\"text-muted pb-l mb-none\">{{ trans('entities.books_new_empty') }}</p>\n    @endif\n</div>"
  },
  {
    "path": "resources/views/books/parts/index-sidebar-section-popular.blade.php",
    "content": "<div id=\"popular\" class=\"mb-xl\">\n    <h5>{{ trans('entities.books_popular') }}</h5>\n    @if(count($popular) > 0)\n        @include('entities.list', ['entities' => $popular, 'style' => 'compact'])\n    @else\n        <p class=\"text-muted pb-l mb-none\">{{ trans('entities.books_popular_empty') }}</p>\n    @endif\n</div>"
  },
  {
    "path": "resources/views/books/parts/index-sidebar-section-recents.blade.php",
    "content": "@if($recents)\n    <div id=\"recents\" class=\"mb-xl\">\n        <h5>{{ trans('entities.recently_viewed') }}</h5>\n        @include('entities.list', ['entities' => $recents, 'style' => 'compact'])\n    </div>\n@endif"
  },
  {
    "path": "resources/views/books/parts/list-item.blade.php",
    "content": "@php\n    /**\n     * @var \\BookStack\\Entities\\Models\\Book $book\n     */\n@endphp\n<a href=\"{{ $book->getUrl() }}\" class=\"book entity-list-item\" data-entity-type=\"book\" data-entity-id=\"{{$book->id}}\">\n    <div class=\"entity-list-item-image bg-book\" style=\"background-image: url('{{ $book->coverInfo()->getUrl() }}')\">\n        @icon('book')\n    </div>\n    <div class=\"content\">\n        <h4 class=\"entity-list-item-name break-text\">{{ $book->name }}</h4>\n        <div class=\"entity-item-snippet\">\n            <p class=\"text-muted break-text mb-s text-limit-lines-1\">{{ $book->descriptionInfo()->getPlain() }}</p>\n        </div>\n    </div>\n</a>"
  },
  {
    "path": "resources/views/books/parts/list.blade.php",
    "content": "<main class=\"content-wrap mt-m card\">\n    <div class=\"grid half v-center no-row-gap\">\n        <h1 class=\"list-heading\">{{ trans('entities.books') }}</h1>\n        <div class=\"text-m-right my-m\">\n            @include('common.sort', $listOptions->getSortControlData())\n        </div>\n    </div>\n    @if(count($books) > 0)\n        @if($view === 'list')\n            <div class=\"entity-list\">\n                @foreach($books as $book)\n                    @include('books.parts.list-item', ['book' => $book])\n                @endforeach\n            </div>\n        @else\n            <div class=\"grid third\">\n                @foreach($books as $key => $book)\n                    @include('entities.grid-item', ['entity' => $book])\n                @endforeach\n            </div>\n        @endif\n        <div>\n            {!! $books->render() !!}\n        </div>\n    @else\n        <p class=\"text-muted\">{{ trans('entities.books_empty') }}</p>\n        @if(userCan(\\BookStack\\Permissions\\Permission::BookCreateAll))\n            <div class=\"icon-list block inline\">\n                <a href=\"{{ url(\"/create-book\") }}\"\n                   class=\"icon-list-item text-book\">\n                    <span>@icon('add')</span>\n                    <span>{{ trans('entities.create_now') }}</span>\n                </a>\n            </div>\n        @endif\n    @endif\n</main>"
  },
  {
    "path": "resources/views/books/parts/show-sidebar-section-actions.blade.php",
    "content": "<div class=\"actions mb-xl\">\n    <h5>{{ trans('common.actions') }}</h5>\n    <div class=\"icon-list text-link\">\n\n        @if(userCan(\\BookStack\\Permissions\\Permission::PageCreate, $book))\n            <a href=\"{{ $book->getUrl('/create-page') }}\" data-shortcut=\"new\" class=\"icon-list-item\">\n                <span>@icon('add')</span>\n                <span>{{ trans('entities.pages_new') }}</span>\n            </a>\n        @endif\n        @if(userCan(\\BookStack\\Permissions\\Permission::ChapterCreate, $book))\n            <a href=\"{{ $book->getUrl('/create-chapter') }}\" data-shortcut=\"new\" class=\"icon-list-item\">\n                <span>@icon('add')</span>\n                <span>{{ trans('entities.chapters_new') }}</span>\n            </a>\n        @endif\n\n        <hr class=\"primary-background\">\n\n        @if(userCan(\\BookStack\\Permissions\\Permission::BookUpdate, $book))\n            <a href=\"{{ $book->getUrl('/edit') }}\" data-shortcut=\"edit\" class=\"icon-list-item\">\n                <span>@icon('edit')</span>\n                <span>{{ trans('common.edit') }}</span>\n            </a>\n            <a href=\"{{ $book->getUrl('/sort') }}\" data-shortcut=\"sort\" class=\"icon-list-item\">\n                <span>@icon('sort')</span>\n                <span>{{ trans('common.sort') }}</span>\n            </a>\n        @endif\n        @if(userCan(\\BookStack\\Permissions\\Permission::BookCreateAll))\n            <a href=\"{{ $book->getUrl('/copy') }}\" data-shortcut=\"copy\" class=\"icon-list-item\">\n                <span>@icon('copy')</span>\n                <span>{{ trans('common.copy') }}</span>\n            </a>\n        @endif\n        @if(userCan(\\BookStack\\Permissions\\Permission::RestrictionsManage, $book))\n            <a href=\"{{ $book->getUrl('/permissions') }}\" data-shortcut=\"permissions\" class=\"icon-list-item\">\n                <span>@icon('lock')</span>\n                <span>{{ trans('entities.permissions') }}</span>\n            </a>\n        @endif\n        @if(userCan(\\BookStack\\Permissions\\Permission::BookDelete, $book))\n            <a href=\"{{ $book->getUrl('/delete') }}\" data-shortcut=\"delete\" class=\"icon-list-item\">\n                <span>@icon('delete')</span>\n                <span>{{ trans('common.delete') }}</span>\n            </a>\n        @endif\n\n        <hr class=\"primary-background\">\n\n        @if($watchOptions->canWatch() && !$watchOptions->isWatching())\n            @include('entities.watch-action', ['entity' => $book])\n        @endif\n        @if(!user()->isGuest())\n            @include('entities.favourite-action', ['entity' => $book])\n        @endif\n        @if(userCan(\\BookStack\\Permissions\\Permission::ContentExport))\n            @include('entities.export-menu', ['entity' => $book])\n        @endif\n    </div>\n</div>"
  },
  {
    "path": "resources/views/books/parts/show-sidebar-section-activity.blade.php",
    "content": "@if(count($activity) > 0)\n    <div id=\"recent-activity\" class=\"mb-xl\">\n        <h5>{{ trans('entities.recent_activity') }}</h5>\n        @include('common.activity-list', ['activity' => $activity])\n    </div>\n@endif"
  },
  {
    "path": "resources/views/books/parts/show-sidebar-section-details.blade.php",
    "content": "<div class=\"mb-xl\">\n    <h5>{{ trans('common.details') }}</h5>\n    <div class=\"blended-links\">\n        @include('entities.meta', ['entity' => $book, 'watchOptions' => $watchOptions])\n        @if($book->hasPermissions())\n            <div class=\"active-restriction\">\n                @if(userCan(\\BookStack\\Permissions\\Permission::RestrictionsManage, $book))\n                    <a href=\"{{ $book->getUrl('/permissions') }}\" class=\"entity-meta-item\">\n                        @icon('lock')\n                        <div>{{ trans('entities.books_permissions_active') }}</div>\n                    </a>\n                @else\n                    <div class=\"entity-meta-item\">\n                        @icon('lock')\n                        <div>{{ trans('entities.books_permissions_active') }}</div>\n                    </div>\n                @endif\n            </div>\n        @endif\n    </div>\n</div>"
  },
  {
    "path": "resources/views/books/parts/show-sidebar-section-shelves.blade.php",
    "content": "@if(count($bookParentShelves) > 0)\n    <div class=\"actions mb-xl\">\n        <h5>{{ trans('entities.shelves') }}</h5>\n        @include('entities.list', ['entities' => $bookParentShelves, 'style' => 'compact'])\n    </div>\n@endif"
  },
  {
    "path": "resources/views/books/parts/show-sidebar-section-tags.blade.php",
    "content": "@if($book->tags->count() > 0)\n    <div class=\"mb-xl\">\n        @include('entities.tag-list', ['entity' => $book])\n    </div>\n@endif"
  },
  {
    "path": "resources/views/books/parts/sort-box-actions.blade.php",
    "content": "<div class=\"sort-box-actions flex-container-row items-center px-s gap-xxs\">\n    <button type=\"button\" data-move=\"up\" class=\"icon-button p-xs text-bigger\"\n            title=\"{{ trans('entities.books_sort_move_up') }}\">@icon('chevron-up')</button>\n    <button type=\"button\" data-move=\"down\" class=\"icon-button p-xs text-bigger\"\n            title=\"{{ trans('entities.books_sort_move_down') }}\">@icon('chevron-down')</button>\n    <div class=\"dropdown-container\" component=\"dropdown\">\n        <button refs=\"dropdown@toggle\"\n                type=\"button\"\n                title=\"{{ trans('common.more') }}\"\n                class=\"icon-button p-xs text-bigger\"\n                aria-haspopup=\"true\"\n                aria-expanded=\"false\">\n            @icon('more')\n        </button>\n        <div refs=\"dropdown@menu\" class=\"dropdown-menu\" role=\"menu\">\n            <button type=\"button\" class=\"text-item\" data-move=\"prev_book\">{{ trans('entities.books_sort_move_prev_book') }}</button>\n            <button type=\"button\" class=\"text-item\" data-move=\"next_book\">{{ trans('entities.books_sort_move_next_book') }}</button>\n            <button type=\"button\" class=\"text-item\" data-move=\"prev_chapter\">{{ trans('entities.books_sort_move_prev_chapter') }}</button>\n            <button type=\"button\" class=\"text-item\" data-move=\"next_chapter\">{{ trans('entities.books_sort_move_next_chapter') }}</button>\n            <button type=\"button\" class=\"text-item\" data-move=\"book_start\">{{ trans('entities.books_sort_move_book_start') }}</button>\n            <button type=\"button\" class=\"text-item\" data-move=\"book_end\">{{ trans('entities.books_sort_move_book_end') }}</button>\n            <button type=\"button\" class=\"text-item\" data-move=\"before_chapter\">{{ trans('entities.books_sort_move_before_chapter') }}</button>\n            <button type=\"button\" class=\"text-item\" data-move=\"after_chapter\">{{ trans('entities.books_sort_move_after_chapter') }}</button>\n        </div>\n    </div>\n</div>"
  },
  {
    "path": "resources/views/books/parts/sort-box.blade.php",
    "content": "<details class=\"sort-box\" data-type=\"book\" data-id=\"{{ $book->id }}\" open>\n    <summary>\n        <h5 class=\"flex-container-row items-center justify-flex-start gap-xs\">\n            <div class=\"text-book text-bigger caret-container\">\n                @icon('caret-right')\n            </div>\n            <div class=\"entity-list-item no-hover py-s text-book px-none\">\n                <span>@icon('book')</span>\n                <span>{{ $book->name }}</span>\n            </div>\n            <div class=\"flex-container-row items-center text-book\">\n                @if($book->sortRule)\n                    <span title=\"{{ trans('entities.books_sort_auto_sort_active', ['sortName' => $book->sortRule->name]) }}\">@icon('auto-sort')</span>\n                @endif\n            </div>\n        </h5>\n    </summary>\n    <div class=\"sort-box-options pb-sm\">\n        <button type=\"button\" data-sort=\"name\"\n                class=\"button outline small\">{{ trans('entities.books_sort_name') }}</button>\n        <button type=\"button\" data-sort=\"created\"\n                class=\"button outline small\">{{ trans('entities.books_sort_created') }}</button>\n        <button type=\"button\" data-sort=\"updated\"\n                class=\"button outline small\">{{ trans('entities.books_sort_updated') }}</button>\n        <button type=\"button\" data-sort=\"chaptersFirst\"\n                class=\"button outline small\">{{ trans('entities.books_sort_chapters_first') }}</button>\n        <button type=\"button\" data-sort=\"chaptersLast\"\n                class=\"button outline small\">{{ trans('entities.books_sort_chapters_last') }}</button>\n    </div>\n    <ul class=\"sortable-page-list sort-list\">\n\n        @foreach($bookChildren as $bookChild)\n            <li class=\"text-{{ $bookChild->getType() }}\"\n                data-id=\"{{$bookChild->id}}\"\n                data-type=\"{{ $bookChild->getType() }}\"\n                data-name=\"{{ $bookChild->name }}\"\n                data-created=\"{{ $bookChild->created_at->timestamp }}\"\n                data-updated=\"{{ $bookChild->updated_at->timestamp }}\"\n                tabindex=\"-1\">\n                <div class=\"flex-container-row items-center\">\n                    <div class=\"text-muted sort-list-handle px-s py-m\">@icon('grip')</div>\n                    <div class=\"entity-list-item px-none no-hover\">\n                        <span>@icon($bookChild->getType()) </span>\n                        <div>\n                            {{ $bookChild->name }}\n                            <div>\n\n                            </div>\n                        </div>\n                    </div>\n                    @include('books.parts.sort-box-actions')\n                </div>\n                @if($bookChild->isA('chapter'))\n                    <ul class=\"sortable-page-sublist\">\n                        @foreach($bookChild->visible_pages as $page)\n                            <li class=\"text-page flex-container-row items-center\"\n                                data-id=\"{{$page->id}}\" data-type=\"page\"\n                                data-name=\"{{ $page->name }}\" data-created=\"{{ $page->created_at->timestamp }}\"\n                                data-updated=\"{{ $page->updated_at->timestamp }}\"\n                                tabindex=\"-1\">\n                                <div class=\"text-muted sort-list-handle px-s py-m\">@icon('grip')</div>\n                                <div class=\"entity-list-item px-none no-hover\">\n                                    <span>@icon('page')</span>\n                                    <span>{{ $page->name }}</span>\n                                </div>\n                                @include('books.parts.sort-box-actions')\n                            </li>\n                        @endforeach\n                    </ul>\n                @endif\n            </li>\n        @endforeach\n\n    </ul>\n</details>"
  },
  {
    "path": "resources/views/books/permissions.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                $book,\n                $book->getUrl('/permissions') => [\n                    'text' => trans('entities.books_permissions'),\n                    'icon' => 'lock',\n                ]\n            ]])\n        </div>\n\n        <main class=\"card content-wrap auto-height\">\n            @include('form.entity-permissions', ['model' => $book, 'title' => trans('entities.books_permissions')])\n        </main>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/books/references.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                $book,\n                $book->getUrl('/references') => [\n                    'text' => trans('entities.references'),\n                    'icon' => 'reference',\n                ]\n            ]])\n        </div>\n\n        @include('entities.references', ['references' => $references])\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/books/show.blade.php",
    "content": "@extends('layouts.tri')\n\n@section('container-attrs')\n    component=\"entity-search\"\n    option:entity-search:entity-id=\"{{ $book->id }}\"\n    option:entity-search:entity-type=\"book\"\n@stop\n\n@push('social-meta')\n    <meta property=\"og:description\" content=\"{{ Str::limit($book->description, 100, '...') }}\">\n    @if($book->coverInfo()->exists())\n        <meta property=\"og:image\" content=\"{{ $book->coverInfo()->getUrl() }}\">\n    @endif\n@endpush\n\n@include('entities.body-tag-classes', ['entity' => $book])\n\n@section('body')\n\n    <div class=\"mb-s print-hidden\">\n        @include('entities.breadcrumbs', ['crumbs' => [\n            $book,\n        ]])\n    </div>\n\n    <main class=\"content-wrap card\">\n        <h1 class=\"break-text\">{{$book->name}}</h1>\n        <div refs=\"entity-search@contentView\" class=\"book-content\">\n            <div class=\"text-muted break-text\">{!! $book->descriptionInfo()->getHtml() !!}</div>\n            @if(count($bookChildren) > 0)\n                <div class=\"entity-list book-contents\">\n                    @foreach($bookChildren as $childElement)\n                        @if($childElement->isA('chapter'))\n                            @include('chapters.parts.list-item', ['chapter' => $childElement])\n                        @else\n                            @include('pages.parts.list-item', ['page' => $childElement])\n                        @endif\n                    @endforeach\n                </div>\n            @else\n                <div class=\"mt-xl\">\n                    <hr>\n                    <p class=\"text-muted italic mb-m mt-xl\">{{ trans('entities.books_empty_contents') }}</p>\n\n                    <div class=\"icon-list block inline\">\n                        @if(userCan(\\BookStack\\Permissions\\Permission::PageCreate, $book))\n                            <a href=\"{{ $book->getUrl('/create-page') }}\" class=\"icon-list-item text-page\">\n                                <span class=\"icon\">@icon('page')</span>\n                                <span>{{ trans('entities.books_empty_create_page') }}</span>\n                            </a>\n                        @endif\n                        @if(userCan(\\BookStack\\Permissions\\Permission::ChapterCreate, $book))\n                            <a href=\"{{ $book->getUrl('/create-chapter') }}\" class=\"icon-list-item text-chapter\">\n                                <span class=\"icon\">@icon('chapter')</span>\n                                <span>{{ trans('entities.books_empty_add_chapter') }}</span>\n                            </a>\n                        @endif\n                    </div>\n\n                </div>\n            @endif\n        </div>\n\n        @include('entities.search-results')\n    </main>\n\n@stop\n\n@section('right')\n    @include('books.parts.show-sidebar-section-details', ['book' => $book, 'watchOptions' => $watchOptions])\n    @include('books.parts.show-sidebar-section-actions', ['book' => $book, 'watchOptions' => $watchOptions])\n@stop\n\n@section('left')\n    @include('entities.search-form', ['label' => trans('entities.books_search_this')])\n    @include('books.parts.show-sidebar-section-tags', ['book' => $book])\n    @include('books.parts.show-sidebar-section-shelves', ['bookParentShelves' => $bookParentShelves])\n    @include('books.parts.show-sidebar-section-activity', ['activity' => $activity])\n@stop\n\n"
  },
  {
    "path": "resources/views/books/sort.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                $book,\n                $book->getUrl('/sort') => [\n                    'text' => trans('entities.books_sort'),\n                    'icon' => 'sort',\n                ]\n            ]])\n        </div>\n\n        <div class=\"grid left-focus gap-xl\">\n            <div>\n                <div component=\"book-sort\" class=\"card content-wrap auto-height\">\n                    <h1 class=\"list-heading\">{{ trans('entities.books_sort') }}</h1>\n\n                    <div class=\"flex-container-row gap-m wrap mb-m\">\n                        <p class=\"text-muted flex min-width-s mb-none\">{{ trans('entities.books_sort_desc') }}</p>\n                        <div class=\"min-width-s\">\n                            @php\n                                $autoSortVal = intval(old('auto-sort') ?? $book->sort_rule_id ?? 0);\n                            @endphp\n                            <label for=\"auto-sort\">{{ trans('entities.books_sort_auto_sort') }}</label>\n                            <select id=\"auto-sort\"\n                                    name=\"auto-sort\"\n                                    form=\"sort-form\"\n                                    class=\"{{ $errors->has('auto-sort') ? 'neg' : '' }}\">\n                                <option value=\"0\" @if($autoSortVal === 0) selected @endif>-- {{ trans('common.none') }}\n                                    --\n                                </option>\n                                @foreach(\\BookStack\\Sorting\\SortRule::allByName() as $rule)\n                                    <option value=\"{{$rule->id}}\"\n                                            @if($autoSortVal === $rule->id) selected @endif\n                                    >\n                                        {{ $rule->name }}\n                                    </option>\n                                @endforeach\n                            </select>\n                        </div>\n                    </div>\n\n                    <div refs=\"book-sort@sortContainer\">\n                        @include('books.parts.sort-box', ['book' => $book, 'bookChildren' => $bookChildren])\n                    </div>\n\n                    <form id=\"sort-form\" action=\"{{ $book->getUrl('/sort') }}\" method=\"POST\">\n                        {{ csrf_field() }}\n                        <input type=\"hidden\" name=\"_method\" value=\"PUT\">\n                        <input refs=\"book-sort@input\" type=\"hidden\" name=\"sort-tree\">\n                        <div class=\"list text-right\">\n                            <a href=\"{{ $book->getUrl() }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                            <button class=\"button\" type=\"submit\">{{ trans('entities.books_sort_save') }}</button>\n                        </div>\n                    </form>\n                </div>\n            </div>\n\n            <div>\n                <main class=\"card content-wrap auto-height sticky-top-m\">\n                    <h2 class=\"list-heading\">{{ trans('entities.books_sort_show_other') }}</h2>\n                    <p class=\"text-muted\">{{ trans('entities.books_sort_show_other_desc') }}</p>\n\n                    @include('entities.selector', ['name' => 'books_list', 'selectorSize' => 'compact', 'entityTypes' => 'book', 'entityPermission' => 'update'])\n\n                </main>\n            </div>\n        </div>\n\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/chapters/copy.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                $chapter->book,\n                $chapter,\n                $chapter->getUrl('/copy') => [\n                    'text' => trans('entities.chapters_copy'),\n                    'icon' => 'copy',\n                ]\n            ]])\n        </div>\n\n        <div class=\"card content-wrap auto-height\">\n\n            <h1 class=\"list-heading\">{{ trans('entities.chapters_copy') }}</h1>\n\n            <form action=\"{{ $chapter->getUrl('/copy') }}\" method=\"POST\">\n                {!! csrf_field() !!}\n\n                <div class=\"form-group title-input\">\n                    <label for=\"name\">{{ trans('common.name') }}</label>\n                    @include('form.text', ['name' => 'name'])\n                </div>\n\n                <div class=\"form-group\" collapsible>\n                    <button type=\"button\" class=\"collapse-title text-link\" collapsible-trigger aria-expanded=\"false\">\n                        <label for=\"entity_selection\">{{ trans('entities.pages_copy_desination') }}</label>\n                    </button>\n                    <div class=\"collapse-content\" collapsible-content>\n                        @include('entities.selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book', 'entityPermission' => 'chapter-create'])\n                    </div>\n                </div>\n\n                @include('entities.copy-considerations')\n\n                <div class=\"form-group text-right\">\n                    <a href=\"{{ $chapter->getUrl() }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                    <button type=\"submit\" class=\"button\">{{ trans('entities.chapters_copy') }}</button>\n                </div>\n            </form>\n\n        </div>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/chapters/create.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n    <div class=\"container small\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                $book,\n                $book->getUrl('create-chapter') => [\n                    'text' => trans('entities.chapters_create'),\n                    'icon' => 'add',\n                ]\n            ]])\n        </div>\n\n        <main class=\"content-wrap card\">\n            <h1 class=\"list-heading\">{{ trans('entities.chapters_create') }}</h1>\n            <form action=\"{{ $book->getUrl('/create-chapter') }}\" method=\"POST\">\n                @include('chapters.parts.form')\n            </form>\n        </main>\n\n    </div>\n@stop"
  },
  {
    "path": "resources/views/chapters/delete.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                $chapter->book,\n                $chapter,\n                $chapter->getUrl('/delete') => [\n                    'text' => trans('entities.chapters_delete'),\n                    'icon' => 'delete',\n                ]\n            ]])\n        </div>\n\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ trans('entities.chapters_delete') }}</h1>\n            <p>{{ trans('entities.chapters_delete_explain', ['chapterName' => $chapter->name]) }}</p>\n            <p class=\"text-neg\"><strong>{{ trans('entities.chapters_delete_confirm') }}</strong></p>\n\n            <form action=\"{{ $chapter->getUrl() }}\" method=\"POST\">\n\n                {!! csrf_field() !!}\n                <input type=\"hidden\" name=\"_method\" value=\"DELETE\">\n\n                <div class=\"text-right\">\n                    <a href=\"{{ $chapter->getUrl() }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                    <button type=\"submit\" class=\"button\">{{ trans('common.confirm') }}</button>\n                </div>\n            </form>\n        </div>\n    </div>\n\n@stop"
  },
  {
    "path": "resources/views/chapters/edit.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                $book,\n                $chapter,\n                $chapter->getUrl('/edit') => [\n                    'text' => trans('entities.chapters_edit'),\n                    'icon' => 'edit'\n                ]\n            ]])\n        </div>\n\n        <main class=\"content-wrap card auto-height\">\n            <h1 class=\"list-heading\">{{ trans('entities.chapters_edit') }}</h1>\n            <form action=\"{{  $chapter->getUrl() }}\" method=\"POST\">\n                <input type=\"hidden\" name=\"_method\" value=\"PUT\">\n                @include('chapters.parts.form', ['model' => $chapter])\n            </form>\n        </main>\n\n        @if(userCan(\\BookStack\\Permissions\\Permission::ChapterDelete, $chapter) && userCan(\\BookStack\\Permissions\\Permission::BookCreateAll))\n            @include('chapters.parts.convert-to-book')\n        @endif\n\n    </div>\n\n@stop"
  },
  {
    "path": "resources/views/chapters/move.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                $chapter->book,\n                $chapter,\n                $chapter->getUrl('/move') => [\n                    'text' => trans('entities.chapters_move'),\n                    'icon' => 'folder',\n                ]\n            ]])\n        </div>\n\n        <main class=\"card content-wrap\">\n            <h1 class=\"list-heading\">{{ trans('entities.chapters_move') }}</h1>\n\n            <form action=\"{{ $chapter->getUrl('/move') }}\" method=\"POST\">\n\n                {!! csrf_field() !!}\n                <input type=\"hidden\" name=\"_method\" value=\"PUT\">\n\n                @include('entities.selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book', 'entityPermission' => 'chapter-create'])\n\n                <div class=\"form-group text-right\">\n                    <a href=\"{{ $chapter->getUrl() }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                    <button type=\"submit\" class=\"button\">{{ trans('entities.chapters_move') }}</button>\n                </div>\n            </form>\n\n        </main>\n\n\n\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/chapters/parts/child-menu.blade.php",
    "content": "<div component=\"chapter-contents\" class=\"chapter-child-menu\">\n    <button type=\"button\"\n            refs=\"chapter-contents@toggle\"\n            aria-expanded=\"{{ $isOpen ? 'true' : 'false' }}\"\n            class=\"text-muted chapter-contents-toggle @if($isOpen) open @endif\">\n        @icon('caret-right') @icon('page') <span>{{ trans_choice('entities.x_pages', $bookChild->visible_pages->count()) }}</span>\n    </button>\n    <ul refs=\"chapter-contents@list\"\n        class=\"chapter-contents-list sub-menu inset-list @if($isOpen) open @endif\" @if($isOpen)\n        style=\"display: block;\" @endif\n        role=\"menu\">\n        @foreach($bookChild->visible_pages as $childPage)\n            <li class=\"list-item-page {{ $childPage->isA('page') && $childPage->draft ? 'draft' : '' }}\" role=\"presentation\">\n                @include('entities.list-item-basic', ['entity' => $childPage, 'classes' => $current->matches($childPage)? 'selected' : '' ])\n            </li>\n        @endforeach\n    </ul>\n</div>"
  },
  {
    "path": "resources/views/chapters/parts/convert-to-book.blade.php",
    "content": "<div class=\"content-wrap card auto-height\">\n    <h2 class=\"list-heading\">{{ trans('entities.convert_to_book') }}</h2>\n    <div class=\"grid half left-focus no-row-gap\">\n        <p>\n            {{ trans('entities.convert_to_book_desc') }}\n        </p>\n        <div class=\"text-m-right\">\n            <div component=\"dropdown\" class=\"dropdown-container\">\n                <button refs=\"dropdown@toggle\" class=\"button outline\" aria-haspopup=\"true\" aria-expanded=\"false\">\n                    {{ trans('entities.convert_chapter') }}\n                </button>\n                <ul refs=\"dropdown@menu\" class=\"dropdown-menu\" role=\"menu\">\n                    <li class=\"px-m py-s text-small text-muted\">\n                        {{ trans('entities.convert_chapter_confirm') }}\n                        <br>\n                        {{ trans('entities.convert_undo_warning') }}\n                    </li>\n                    <li>\n                        <form action=\"{{ $chapter->getUrl('/convert-to-book') }}\" method=\"POST\">\n                            {!! csrf_field() !!}\n                            <button type=\"submit\" class=\"text-link text-item\">{{ trans('common.confirm') }}</button>\n                        </form>\n                    </li>\n                </ul>\n            </div>\n        </div>\n    </div>\n</div>"
  },
  {
    "path": "resources/views/chapters/parts/form.blade.php",
    "content": "{{ csrf_field() }}\n<div class=\"form-group title-input\">\n    <label for=\"name\">{{ trans('common.name') }}</label>\n    @include('form.text', ['name' => 'name', 'autofocus' => true])\n</div>\n\n<div class=\"form-group description-input\">\n    <label for=\"description_html\">{{ trans('common.description') }}</label>\n    @include('form.description-html-input')\n</div>\n\n<div class=\"form-group collapsible\" component=\"collapsible\" id=\"logo-control\">\n    <button refs=\"collapsible@trigger\" type=\"button\" class=\"collapse-title text-link\" aria-expanded=\"false\">\n        <label for=\"tags\">{{ trans('entities.chapter_tags') }}</label>\n    </button>\n    <div refs=\"collapsible@content\" class=\"collapse-content\">\n        @include('entities.tag-manager', ['entity' => $chapter ?? null])\n    </div>\n</div>\n\n<div class=\"form-group collapsible\" component=\"collapsible\" id=\"template-control\">\n    <button refs=\"collapsible@trigger\" type=\"button\" class=\"collapse-title text-link\" aria-expanded=\"false\">\n        <label for=\"template-manager\">{{ trans('entities.default_template') }}</label>\n    </button>\n    <div refs=\"collapsible@content\" class=\"collapse-content\">\n        @include('entities.template-selector', ['entity' => $chapter ?? null])\n    </div>\n</div>\n\n<div class=\"form-group text-right\">\n    <a href=\"{{ isset($chapter) ? $chapter->getUrl() : $book->getUrl() }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n    <button type=\"submit\" class=\"button\">{{ trans('entities.chapters_save') }}</button>\n</div>\n\n@include('entities.selector-popup')\n@include('form.editor-translations')"
  },
  {
    "path": "resources/views/chapters/parts/list-item.blade.php",
    "content": "{{--This view display child pages in a list if pre-loaded onto a 'visible_pages' property,--}}\n{{--To ensure that the pages have been loaded efficiently with permissions taken into account.--}}\n<a href=\"{{ $chapter->getUrl() }}\" class=\"chapter entity-list-item @if($chapter->visible_pages->count() > 0) has-children @endif\" data-entity-type=\"chapter\" data-entity-id=\"{{$chapter->id}}\">\n    <span class=\"icon text-chapter\">@icon('chapter')</span>\n    <div class=\"content\">\n        <h4 class=\"entity-list-item-name break-text\">{{ $chapter->name }}</h4>\n        <div class=\"entity-item-snippet\">\n            <p class=\"text-muted break-text\">{{ $chapter->getExcerpt() }}</p>\n        </div>\n    </div>\n</a>\n@if ($chapter->visible_pages->count() > 0)\n    <div class=\"chapter chapter-expansion\">\n        <span class=\"icon text-chapter\">@icon('page')</span>\n        <div component=\"chapter-contents\" class=\"content\">\n            <button type=\"button\"\n                    refs=\"chapter-contents@toggle\"\n                    aria-expanded=\"false\"\n                    class=\"text-muted chapter-contents-toggle\">@icon('caret-right') <span>{{ trans_choice('entities.x_pages', $chapter->visible_pages->count()) }}</span></button>\n            <div refs=\"chapter-contents@list\" class=\"inset-list chapter-contents-list\">\n                <div class=\"entity-list-item-children\">\n                    @include('entities.list', ['entities' => $chapter->visible_pages])\n                </div>\n            </div>\n        </div>\n    </div>\n@endif"
  },
  {
    "path": "resources/views/chapters/parts/show-sidebar-section-actions.blade.php",
    "content": "<div class=\"actions mb-xl\">\n    <h5>{{ trans('common.actions') }}</h5>\n    <div class=\"icon-list text-link\">\n\n        @if(userCan(\\BookStack\\Permissions\\Permission::PageCreate, $chapter))\n            <a href=\"{{ $chapter->getUrl('/create-page') }}\" data-shortcut=\"new\" class=\"icon-list-item\">\n                <span>@icon('add')</span>\n                <span>{{ trans('entities.pages_new') }}</span>\n            </a>\n        @endif\n\n        <hr class=\"primary-background\"/>\n\n        @if(userCan(\\BookStack\\Permissions\\Permission::ChapterUpdate, $chapter))\n            <a href=\"{{ $chapter->getUrl('/edit') }}\" data-shortcut=\"edit\" class=\"icon-list-item\">\n                <span>@icon('edit')</span>\n                <span>{{ trans('common.edit') }}</span>\n            </a>\n        @endif\n        @if(userCanOnAny(\\BookStack\\Permissions\\Permission::Create, \\BookStack\\Entities\\Models\\Book::class) || userCan(\\BookStack\\Permissions\\Permission::ChapterCreateAll) || userCan(\\BookStack\\Permissions\\Permission::ChapterCreateOwn))\n            <a href=\"{{ $chapter->getUrl('/copy') }}\" data-shortcut=\"copy\" class=\"icon-list-item\">\n                <span>@icon('copy')</span>\n                <span>{{ trans('common.copy') }}</span>\n            </a>\n        @endif\n        @if(userCan(\\BookStack\\Permissions\\Permission::ChapterUpdate, $chapter) && userCan(\\BookStack\\Permissions\\Permission::ChapterDelete, $chapter))\n            <a href=\"{{ $chapter->getUrl('/move') }}\" data-shortcut=\"move\" class=\"icon-list-item\">\n                <span>@icon('folder')</span>\n                <span>{{ trans('common.move') }}</span>\n            </a>\n        @endif\n        @if(userCan(\\BookStack\\Permissions\\Permission::RestrictionsManage, $chapter))\n            <a href=\"{{ $chapter->getUrl('/permissions') }}\" data-shortcut=\"permissions\" class=\"icon-list-item\">\n                <span>@icon('lock')</span>\n                <span>{{ trans('entities.permissions') }}</span>\n            </a>\n        @endif\n        @if(userCan(\\BookStack\\Permissions\\Permission::ChapterDelete, $chapter))\n            <a href=\"{{ $chapter->getUrl('/delete') }}\" data-shortcut=\"delete\" class=\"icon-list-item\">\n                <span>@icon('delete')</span>\n                <span>{{ trans('common.delete') }}</span>\n            </a>\n        @endif\n\n        @if($chapter->book && userCan(\\BookStack\\Permissions\\Permission::BookUpdate, $chapter->book))\n            <hr class=\"primary-background\"/>\n            <a href=\"{{ $chapter->book->getUrl('/sort') }}\" data-shortcut=\"sort\" class=\"icon-list-item\">\n                <span>@icon('sort')</span>\n                <span>{{ trans('entities.chapter_sort_book') }}</span>\n            </a>\n        @endif\n\n        <hr class=\"primary-background\"/>\n\n        @if($watchOptions->canWatch() && !$watchOptions->isWatching())\n            @include('entities.watch-action', ['entity' => $chapter])\n        @endif\n        @if(!user()->isGuest())\n            @include('entities.favourite-action', ['entity' => $chapter])\n        @endif\n        @if(userCan(\\BookStack\\Permissions\\Permission::ContentExport))\n            @include('entities.export-menu', ['entity' => $chapter])\n        @endif\n    </div>\n</div>"
  },
  {
    "path": "resources/views/chapters/parts/show-sidebar-section-details.blade.php",
    "content": "<div class=\"mb-xl\">\n    <h5>{{ trans('common.details') }}</h5>\n    <div class=\"blended-links\">\n        @include('entities.meta', ['entity' => $chapter, 'watchOptions' => $watchOptions])\n\n        @if($book->hasPermissions())\n            <div class=\"active-restriction\">\n                @if(userCan(\\BookStack\\Permissions\\Permission::RestrictionsManage, $book))\n                    <a href=\"{{ $book->getUrl('/permissions') }}\" class=\"entity-meta-item\">\n                        @icon('lock')\n                        <div>{{ trans('entities.books_permissions_active') }}</div>\n                    </a>\n                @else\n                    <div class=\"entity-meta-item\">\n                        @icon('lock')\n                        <div>{{ trans('entities.books_permissions_active') }}</div>\n                    </div>\n                @endif\n            </div>\n        @endif\n\n        @if($chapter->hasPermissions())\n            <div class=\"active-restriction\">\n                @if(userCan(\\BookStack\\Permissions\\Permission::RestrictionsManage, $chapter))\n                    <a href=\"{{ $chapter->getUrl('/permissions') }}\" class=\"entity-meta-item\">\n                        @icon('lock')\n                        <div>{{ trans('entities.chapters_permissions_active') }}</div>\n                    </a>\n                @else\n                    <div class=\"entity-meta-item\">\n                        @icon('lock')\n                        <div>{{ trans('entities.chapters_permissions_active') }}</div>\n                    </div>\n                @endif\n            </div>\n        @endif\n    </div>\n</div>"
  },
  {
    "path": "resources/views/chapters/parts/show-sidebar-section-tags.blade.php",
    "content": "@if($chapter->tags->count() > 0)\n    <div class=\"mb-xl\">\n        @include('entities.tag-list', ['entity' => $chapter])\n    </div>\n@endif"
  },
  {
    "path": "resources/views/chapters/permissions.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                $chapter->book,\n                $chapter,\n                $chapter->getUrl('/permissions') => [\n                    'text' => trans('entities.chapters_permissions'),\n                    'icon' => 'lock',\n                ]\n            ]])\n        </div>\n\n        <main class=\"card content-wrap auto-height\">\n            @include('form.entity-permissions', ['model' => $chapter, 'title' => trans('entities.chapters_permissions')])\n        </main>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/chapters/references.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                $chapter->book,\n                $chapter,\n                $chapter->getUrl('/references') => [\n                    'text' => trans('entities.references'),\n                    'icon' => 'reference',\n                ]\n            ]])\n        </div>\n\n        @include('entities.references', ['references' => $references])\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/chapters/show.blade.php",
    "content": "@extends('layouts.tri')\n\n@section('container-attrs')\n    component=\"entity-search\"\n    option:entity-search:entity-id=\"{{ $chapter->id }}\"\n    option:entity-search:entity-type=\"chapter\"\n@stop\n\n@push('social-meta')\n    <meta property=\"og:description\" content=\"{{ Str::limit($chapter->description, 100, '...') }}\">\n@endpush\n\n@include('entities.body-tag-classes', ['entity' => $chapter])\n\n@section('body')\n\n    <div class=\"mb-m print-hidden\">\n        @include('entities.breadcrumbs', ['crumbs' => [\n            $chapter->book,\n            $chapter,\n        ]])\n    </div>\n\n    <main class=\"content-wrap card\">\n        <h1 class=\"break-text\">{{ $chapter->name }}</h1>\n        <div refs=\"entity-search@contentView\" class=\"chapter-content\">\n            <div class=\"text-muted break-text\">{!! $chapter->descriptionInfo()->getHtml() !!}</div>\n            @if(count($pages) > 0)\n                <div class=\"entity-list book-contents\">\n                    @foreach($pages as $page)\n                        @include('pages.parts.list-item', ['page' => $page])\n                    @endforeach\n                </div>\n            @else\n                <div class=\"mt-xl\">\n                    <hr>\n                    <p class=\"text-muted italic mb-m mt-xl\">{{ trans('entities.chapters_empty') }}</p>\n\n                    <div class=\"icon-list block inline\">\n                        @if(userCan(\\BookStack\\Permissions\\Permission::PageCreate, $chapter))\n                            <a href=\"{{ $chapter->getUrl('/create-page') }}\" class=\"icon-list-item text-page\">\n                                <span class=\"icon\">@icon('page')</span>\n                                <span>{{ trans('entities.books_empty_create_page') }}</span>\n                            </a>\n                        @endif\n                        @if(userCan(\\BookStack\\Permissions\\Permission::BookUpdate, $book))\n                            <a href=\"{{ $book->getUrl('/sort') }}\" class=\"icon-list-item text-book\">\n                                <span class=\"icon\">@icon('book')</span>\n                                <span>{{ trans('entities.books_empty_sort_current_book') }}</span>\n                            </a>\n                        @endif\n                    </div>\n\n                </div>\n            @endif\n        </div>\n\n        @include('entities.search-results')\n    </main>\n\n    @include('entities.sibling-navigation', ['next' => $next, 'previous' => $previous])\n\n@stop\n\n@section('right')\n    @include('chapters.parts.show-sidebar-section-details', ['chapter' => $chapter, 'book' => $book, 'watchOptions' => $watchOptions])\n    @include('chapters.parts.show-sidebar-section-actions', ['chapter' => $chapter, 'watchOptions' => $watchOptions])\n@stop\n\n@section('left')\n    @include('entities.search-form', ['label' => trans('entities.chapters_search_this')])\n    @include('chapters.parts.show-sidebar-section-tags', ['chapter' => $chapter])\n    @include('entities.book-tree', ['book' => $book, 'sidebarTree' => $sidebarTree])\n@stop\n\n\n"
  },
  {
    "path": "resources/views/comments/comment-branch.blade.php",
    "content": "{{--\n$branch CommentTreeNode\n--}}\n<div class=\"comment-branch\">\n    <div>\n        @include('comments.comment', ['comment' => $branch->comment])\n    </div>\n    <div class=\"flex-container-row\">\n        <div class=\"comment-thread-indicator-parent\">\n            <div class=\"comment-thread-indicator\"></div>\n        </div>\n        <div class=\"comment-branch-children flex\">\n            @foreach($branch->children as $childBranch)\n                @include('comments.comment-branch', ['branch' => $childBranch])\n            @endforeach\n        </div>\n    </div>\n</div>"
  },
  {
    "path": "resources/views/comments/comment.blade.php",
    "content": "@php\n    $commentHtml = $comment->safeHtml();\n@endphp\n<div component=\"{{ $readOnly ? '' : 'page-comment' }}\"\n     option:page-comment:comment-id=\"{{ $comment->id }}\"\n     option:page-comment:comment-local-id=\"{{ $comment->local_id }}\"\n     option:page-comment:updated-text=\"{{ trans('entities.comment_updated_success') }}\"\n     option:page-comment:deleted-text=\"{{ trans('entities.comment_deleted_success') }}\"\n     option:page-comment:archive-text=\"{{ $comment->archived ? trans('entities.comment_unarchive_success') : trans('entities.comment_archive_success') }}\"\n     option:page-comment:wysiwyg-text-direction=\"{{ $locale->htmlDirection() }}\"\n     id=\"comment{{$comment->local_id}}\"\n     class=\"comment-box\">\n    <div class=\"header\">\n        <div class=\"flex-container-row wrap items-center gap-x-xs\">\n            @if ($comment->createdBy)\n                <div>\n                    <img width=\"50\" src=\"{{ $comment->createdBy->getAvatar(50) }}\" class=\"avatar block mr-xs\"\n                         alt=\"{{ $comment->createdBy->name }}\">\n                </div>\n            @endif\n            <div class=\"meta text-muted flex-container-row wrap items-center flex text-small\">\n                @if ($comment->createdBy)\n                    <a href=\"{{ $comment->createdBy->getProfileUrl() }}\">{{ $comment->createdBy->getShortName(16) }}</a>\n                @else\n                    {{ trans('common.deleted_user') }}\n                @endif\n                <span title=\"{{ $dates->absolute($comment->created_at) }}\">&nbsp;{{ trans('entities.comment_created', ['createDiff' => $dates->relative($comment->created_at) ]) }}</span>\n                @if($comment->isUpdated())\n                    <span class=\"mx-xs\">&bull;</span>\n                    <span title=\"{{ trans('entities.comment_updated', ['updateDiff' => $dates->absolute($comment->updated_at), 'username' => $comment->updatedBy->name ?? trans('common.deleted_user')]) }}\">\n                 {{ trans('entities.comment_updated_indicator') }}\n                    </span>\n                @endif\n            </div>\n            <div class=\"right-meta flex-container-row justify-flex-end items-center px-s\">\n                @if(!$readOnly && (userCan(\\BookStack\\Permissions\\Permission::CommentCreateAll) || userCan(\\BookStack\\Permissions\\Permission::CommentUpdate, $comment) || userCan(\\BookStack\\Permissions\\Permission::CommentDelete, $comment)))\n                    <div class=\"actions mr-s\">\n                        @if(userCan(\\BookStack\\Permissions\\Permission::CommentCreateAll))\n                            <button refs=\"page-comment@reply-button\" type=\"button\"\n                                    class=\"text-button text-muted hover-underline text-small p-xs\">@icon('reply') {{ trans('common.reply') }}</button>\n                        @endif\n                        @if(!$comment->parent_id && (userCan(\\BookStack\\Permissions\\Permission::CommentUpdate, $comment) || userCan(\\BookStack\\Permissions\\Permission::CommentDelete, $comment)))\n                            <button refs=\"page-comment@archive-button\"\n                                    type=\"button\"\n                                    data-is-archived=\"{{ $comment->archived ? 'true' : 'false' }}\"\n                                    class=\"text-button text-muted hover-underline text-small p-xs\">@icon('archive') {{ trans('common.' . ($comment->archived ? 'unarchive' : 'archive')) }}</button>\n                        @endif\n                        @if(userCan(\\BookStack\\Permissions\\Permission::CommentUpdate, $comment))\n                            <button refs=\"page-comment@edit-button\" type=\"button\"\n                                    class=\"text-button text-muted hover-underline text-small p-xs\">@icon('edit') {{ trans('common.edit') }}</button>\n                        @endif\n                        @if(userCan(\\BookStack\\Permissions\\Permission::CommentDelete, $comment))\n                            <div component=\"dropdown\" class=\"dropdown-container\">\n                                <button type=\"button\" refs=\"dropdown@toggle\" aria-haspopup=\"true\" aria-expanded=\"false\"\n                                        class=\"text-button text-muted hover-underline text-small p-xs\">@icon('delete') {{ trans('common.delete') }}</button>\n                                <ul refs=\"dropdown@menu\" class=\"dropdown-menu\" role=\"menu\">\n                                    <li class=\"px-m text-small text-muted pb-s\">{{trans('entities.comment_delete_confirm')}}</li>\n                                    <li>\n                                        <button refs=\"page-comment@delete-button\" type=\"button\"\n                                                class=\"text-button text-neg icon-item\">\n                                            @icon('delete')\n                                            <div>{{ trans('common.delete') }}</div>\n                                        </button>\n                                    </li>\n                                </ul>\n                            </div>\n                        @endif\n                        <span class=\"text-muted\">\n                        &nbsp;&bull;&nbsp;\n                    </span>\n                    </div>\n                @endif\n                <div>\n                    <a class=\"bold text-muted text-small\"\n                       href=\"#comment{{$comment->local_id}}\">#{{$comment->local_id}}</a>\n                </div>\n            </div>\n        </div>\n\n    </div>\n\n    <div refs=\"page-comment@content-container\" class=\"content\">\n        @if ($comment->parent_id)\n            <p class=\"comment-reply\">\n                <a class=\"text-muted text-small\"\n                   href=\"#comment{{ $comment->parent_id }}\">@icon('reply'){{ trans('entities.comment_in_reply_to', ['commentId' => '#' . $comment->parent_id]) }}</a>\n            </p>\n        @endif\n        @if($comment->content_ref)\n            <div class=\"comment-reference-indicator-wrap\">\n                <a component=\"page-comment-reference\"\n                   option:page-comment-reference:reference=\"{{ $comment->content_ref }}\"\n                   option:page-comment-reference:view-comment-text=\"{{ trans('entities.comment_view') }}\"\n                   option:page-comment-reference:jump-to-thread-text=\"{{ trans('entities.comment_jump_to_thread') }}\"\n                   option:page-comment-reference:close-text=\"{{ trans('common.close') }}\"\n                   href=\"#\">@icon('bookmark'){{ trans('entities.comment_reference') }}\n                    <span>{{ trans('entities.comment_reference_outdated') }}</span></a>\n            </div>\n        @endif\n        {!! $commentHtml  !!}\n    </div>\n\n    @if(!$readOnly && userCan(\\BookStack\\Permissions\\Permission::CommentUpdate, $comment))\n        <form novalidate refs=\"page-comment@form\" hidden class=\"content pt-s px-s block\">\n            <div class=\"form-group description-input\">\n                <textarea refs=\"page-comment@input\" name=\"html\" rows=\"3\"\n                          placeholder=\"{{ trans('entities.comment_placeholder') }}\">{{ $commentHtml }}</textarea>\n            </div>\n            <div class=\"form-group text-right\">\n                <button type=\"button\" class=\"button outline\"\n                        refs=\"page-comment@form-cancel\">{{ trans('common.cancel') }}</button>\n                <button type=\"submit\" class=\"button\">{{ trans('entities.comment_save') }}</button>\n            </div>\n        </form>\n    @endif\n\n</div>"
  },
  {
    "path": "resources/views/comments/comments.blade.php",
    "content": "<section components=\"page-comments tabs\"\n         option:page-comments:page-id=\"{{ $page->id }}\"\n         option:page-comments:created-text=\"{{ trans('entities.comment_created_success') }}\"\n         option:page-comments:count-text=\"{{ trans('entities.comment_thread_count') }}\"\n         option:page-comments:archived-count-text=\"{{ trans('entities.comment_archived_count') }}\"\n         option:page-comments:wysiwyg-text-direction=\"{{ $locale->htmlDirection() }}\"\n         class=\"comments-list tab-container\"\n         aria-label=\"{{ trans('entities.comments') }}\">\n\n    <div refs=\"page-comments@comment-count-bar\" class=\"flex-container-row items-center\">\n        <div role=\"tablist\" class=\"flex\">\n            <button type=\"button\"\n                    role=\"tab\"\n                    id=\"comment-tab-active\"\n                    aria-controls=\"comment-tab-panel-active\"\n                    refs=\"page-comments@active-tab\"\n                    aria-selected=\"true\">{{ trans_choice('entities.comment_thread_count', $commentTree->activeThreadCount()) }}</button>\n            <button type=\"button\"\n                    role=\"tab\"\n                    id=\"comment-tab-archived\"\n                    aria-controls=\"comment-tab-panel-archived\"\n                    refs=\"page-comments@archived-tab\"\n                    aria-selected=\"false\">{{ trans_choice('entities.comment_archived_count', count($commentTree->getArchived())) }}</button>\n        </div>\n        @if ($commentTree->empty() && userCan(\\BookStack\\Permissions\\Permission::CommentCreateAll))\n            <div refs=\"page-comments@add-button-container\" class=\"ml-m flex-container-row\" >\n                <button type=\"button\"\n                        refs=\"page-comments@add-comment-button\"\n                        class=\"button outline mb-m ml-auto\">{{ trans('entities.comment_add') }}</button>\n            </div>\n        @endif\n    </div>\n\n    <div id=\"comment-tab-panel-active\"\n         refs=\"page-comments@active-container\"\n         tabindex=\"0\"\n         role=\"tabpanel\"\n         aria-labelledby=\"comment-tab-active\"\n         class=\"comment-container no-outline\">\n        <div refs=\"page-comments@comment-container\">\n            @foreach($commentTree->getActive() as $branch)\n                @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => false])\n            @endforeach\n        </div>\n\n        <p class=\"text-center text-muted italic empty-state\">{{ trans('entities.comment_none') }}</p>\n\n        @if(userCan(\\BookStack\\Permissions\\Permission::CommentCreateAll))\n            @include('comments.create')\n            @if (!$commentTree->empty())\n                <div refs=\"page-comments@addButtonContainer\" class=\"ml-m flex-container-row\">\n                    <button type=\"button\"\n                            refs=\"page-comments@add-comment-button\"\n                            class=\"button outline mb-m ml-auto\">{{ trans('entities.comment_add') }}</button>\n                </div>\n            @endif\n        @endif\n    </div>\n\n    <div refs=\"page-comments@archive-container\"\n         id=\"comment-tab-panel-archived\"\n         tabindex=\"0\"\n         role=\"tabpanel\"\n         aria-labelledby=\"comment-tab-archived\"\n         hidden=\"hidden\"\n         class=\"comment-container no-outline\">\n        @foreach($commentTree->getArchived() as $branch)\n            @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => false])\n        @endforeach\n            <p class=\"text-center text-muted italic empty-state\">{{ trans('entities.comment_none') }}</p>\n    </div>\n\n    @if(userCan(\\BookStack\\Permissions\\Permission::CommentCreateAll) || $commentTree->canUpdateAny())\n        @push('body-end')\n            @include('form.editor-translations')\n            @include('entities.selector-popup')\n        @endpush\n    @endif\n\n</section>"
  },
  {
    "path": "resources/views/comments/create.blade.php",
    "content": "<div refs=\"page-comments@form-container\" hidden class=\"comment-branch mb-m\">\n    <div class=\"comment-box\">\n\n        <div class=\"header p-s\">{{ trans('entities.comment_new') }}</div>\n        <div refs=\"page-comments@reply-to-row\" hidden class=\"primary-background-light text-muted px-s py-xs\">\n            <div class=\"grid left-focus v-center\">\n                <div>\n                    <a refs=\"page-comments@form-reply-link\" href=\"#\">{{ trans('entities.comment_in_reply_to', ['commentId' => '1234']) }}</a>\n                </div>\n                <div class=\"text-right\">\n                    <button refs=\"page-comments@remove-reply-to-button\" class=\"text-button\">{{ trans('common.remove') }}</button>\n                </div>\n            </div>\n        </div>\n        <div refs=\"page-comments@reference-row\" hidden class=\"primary-background-light text-muted px-s py-xs\">\n            <div class=\"grid left-focus v-center\">\n                <div>\n                    <a refs=\"page-comments@formReferenceLink\" href=\"#\">{{ trans('entities.comment_reference') }}</a>\n                </div>\n                <div class=\"text-right\">\n                    <button refs=\"page-comments@remove-reference-button\" class=\"text-button\">{{ trans('common.remove') }}</button>\n                </div>\n            </div>\n        </div>\n\n        <div class=\"content px-s pt-s\">\n            <form refs=\"page-comments@form\" novalidate>\n                <div class=\"form-group description-input\">\n                <textarea refs=\"page-comments@form-input\" name=\"html\"\n                          rows=\"3\"\n                          placeholder=\"{{ trans('entities.comment_placeholder') }}\"></textarea>\n                </div>\n                <div class=\"form-group text-right\">\n                    <button type=\"button\" class=\"button outline\"\n                            refs=\"page-comments@hide-form-button\">{{ trans('common.cancel') }}</button>\n                    <button type=\"submit\" class=\"button\">{{ trans('entities.comment_save') }}</button>\n                </div>\n            </form>\n        </div>\n\n    </div>\n</div>"
  },
  {
    "path": "resources/views/common/activity-item.blade.php",
    "content": "\n{{--Requires an Activity item with the name $activity passed in--}}\n\n<div>\n    @if($activity->user)\n    <img class=\"avatar\" src=\"{{ $activity->user->getAvatar(30) }}\" alt=\"{{ $activity->user->name }}\">\n    @endif\n</div>\n\n<div>\n    @if($activity->user)\n        <a href=\"{{ $activity->user->getProfileUrl() }}\">{{ $activity->user->name }}</a>\n    @else\n        {{ trans('common.deleted_user') }}\n    @endif\n\n    {{ $activity->getText() }}\n\n    @if($activity->loggable && is_null($activity->loggable->deleted_at))\n        <a href=\"{{ $activity->loggable->getUrl() }}\">{{ $activity->loggable->name }}</a>\n    @endif\n\n    @if($activity->loggable && !is_null($activity->loggable->deleted_at))\n        \"{{ $activity->loggable->name }}\"\n    @endif\n\n    <br>\n\n    <span class=\"text-muted\" title=\"{{ $dates->absolute($activity->created_at) }}\"><small>@icon('time'){{ $dates->relative($activity->created_at) }}</small></span>\n</div>\n"
  },
  {
    "path": "resources/views/common/activity-list.blade.php",
    "content": "\n@if(count($activity) > 0)\n    <div class=\"activity-list\">\n        @foreach($activity as $activityItem)\n            <div class=\"activity-list-item\">\n                @include('common.activity-item', ['activity' => $activityItem])\n            </div>\n        @endforeach\n    </div>\n@else\n    <p class=\"text-muted empty-text mb-none pb-l\">{{ trans('common.no_activity') }}</p>\n@endif"
  },
  {
    "path": "resources/views/common/confirm-dialog.blade.php",
    "content": "<div components=\"popup confirm-dialog\"\n     @if($id ?? false) id=\"{{ $id }}\" @endif\n     refs=\"confirm-dialog@popup {{ $ref ?? false }}\"\n     class=\"popup-background\">\n    <div class=\"popup-body very-small\" tabindex=\"-1\">\n\n        <div class=\"popup-header primary-background\">\n            <div class=\"popup-title\">{{ $title }}</div>\n            <button refs=\"popup@hide\" type=\"button\" class=\"popup-header-close\">@icon('close')</button>\n        </div>\n\n        <div class=\"px-m py-m\">\n            {{ $slot }}\n\n            <div class=\"text-right\">\n                <button type=\"button\" class=\"button outline\" refs=\"popup@hide\">{{ trans('common.cancel') }}</button>\n                <button type=\"button\" class=\"button\" refs=\"confirm-dialog@confirm\">{{ trans('common.continue') }}</button>\n            </div>\n        </div>\n\n    </div>\n</div>"
  },
  {
    "path": "resources/views/common/dark-mode-toggle.blade.php",
    "content": "<form action=\"{{ url('/preferences/toggle-dark-mode') }}\" method=\"post\">\n    {{ csrf_field() }}\n    {{ method_field('patch') }}\n    <input type=\"hidden\" name=\"_return\" value=\"{{ url()->current() }}\">\n    @if(setting()->getForCurrentUser('dark-mode-enabled'))\n        <button class=\"{{ $classes ?? '' }}\" role=\"{{ $butonRole ?? '' }}\"><span>@icon('light-mode')</span><span>{{ trans('common.light_mode') }}</span></button>\n    @else\n        <button class=\"{{ $classes ?? '' }}\" role=\"{{ $butonRole ?? '' }}\"><span>@icon('dark-mode')</span><span>{{ trans('common.dark_mode') }}</span></button>\n    @endif\n</form>"
  },
  {
    "path": "resources/views/common/detailed-listing-paginated.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n    <div class=\"container small pt-xl\">\n        <main class=\"card content-wrap\">\n            <h1 class=\"list-heading\">{{ $title }}</h1>\n\n            <div class=\"book-contents\">\n                @include('entities.list', ['entities' => $entities, 'style' => 'detailed'])\n            </div>\n\n            <div class=\"text-center\">\n                {!! $entities->links() !!}\n            </div>\n        </main>\n    </div>\n@stop"
  },
  {
    "path": "resources/views/common/detailed-listing-with-more.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n    <div class=\"container small pt-xl\">\n        <main class=\"card content-wrap\">\n            <h1 class=\"list-heading\">{{ $title }}</h1>\n\n            <div class=\"book-contents\">\n                @include('entities.list', ['entities' => $entities, 'style' => 'detailed'])\n            </div>\n\n            <div class=\"text-right\">\n                @if($hasMoreLink)\n                    <a href=\"{{ $hasMoreLink }}\" class=\"button outline\">{{ trans('common.more') }}</a>\n                @endif\n            </div>\n        </main>\n    </div>\n@stop"
  },
  {
    "path": "resources/views/common/loading-icon.blade.php",
    "content": "<div class=\"loading-container\">\n    <div></div>\n    <div></div>\n    <div></div>\n    @if(isset($text))\n        <span>{{$text}}</span>\n    @endif\n</div>"
  },
  {
    "path": "resources/views/common/sort.blade.php",
    "content": "<?php\n    $selectedSort = (isset($sort) && array_key_exists($sort, $options)) ? $sort : array_keys($options)[0];\n    $order = (isset($order) && in_array($order, ['asc', 'desc'])) ? $order : 'asc';\n?>\n<div component=\"list-sort-control\" class=\"list-sort-container\">\n    <div class=\"list-sort-label\">{{ trans('common.sort') }}</div>\n    <form refs=\"list-sort-control@form\"\n          @if($useQuery ?? false)\n              action=\"{{ url()->current() }}\"\n              method=\"get\"\n          @else\n              action=\"{{ url(\"/preferences/change-sort/{$type}\") }}\"\n              method=\"post\"\n          @endif\n    >\n        <input type=\"hidden\" name=\"_return\" value=\"{{ url()->current() }}\">\n\n        @if($useQuery ?? false)\n            @foreach(array_filter(request()->except(['sort', 'order'])) as $key => $value)\n                <input type=\"hidden\" name=\"{{ $key }}\" value=\"{{ $value }}\">\n            @endforeach\n        @else\n            {!! method_field('PATCH') !!}\n            {!! csrf_field() !!}\n        @endif\n\n        <input refs=\"list-sort-control@sort\" type=\"hidden\" value=\"{{ $selectedSort }}\" name=\"sort\">\n        <input refs=\"list-sort-control@order\" type=\"hidden\" value=\"{{ $order }}\" name=\"order\">\n\n        <div class=\"list-sort\">\n            <div component=\"dropdown\" class=\"list-sort-type dropdown-container\">\n                <button refs=\"dropdown@toggle\"\n                        aria-haspopup=\"true\"\n                        aria-expanded=\"false\"\n                        aria-label=\"{{ trans('common.sort_options') }}\"\n                        class=\"list-sort-toggle\">{{ $options[$selectedSort] }}</button>\n                <ul refs=\"dropdown@menu list-sort-control@menu\" class=\"dropdown-menu\" role=\"menu\">\n                    @foreach($options as $key => $label)\n                        <li @if($key === $selectedSort) class=\"active\" @endif><a href=\"#\" data-sort-value=\"{{$key}}\" role=\"menuitem\" class=\"text-item\">{{ $label }}</a></li>\n                    @endforeach\n                </ul>\n            </div>\n            <button class=\"list-sort-dir\" type=\"button\" data-sort-dir\n                    aria-label=\"{{ trans('common.sort_direction_toggle') }} - {{ $order === 'asc' ? trans('common.sort_ascending') : trans('common.sort_descending') }}\" tabindex=\"0\">\n                @icon($order === 'desc' ? 'sort-up' : 'sort-down')\n            </button>\n        </div>\n    </form>\n</div>"
  },
  {
    "path": "resources/views/common/status-indicator.blade.php",
    "content": "<span title=\"{{ trans('common.status_' . ($status ? 'active' : 'inactive')) }}\"\n      class=\"status-indicator-{{ $status ? 'active' : 'inactive' }}\"\n></span>"
  },
  {
    "path": "resources/views/entities/body-tag-classes.blade.php",
    "content": "@push('body-class', e((new \\BookStack\\Activity\\Tools\\TagClassGenerator($entity))->generateAsString() . ' '))"
  },
  {
    "path": "resources/views/entities/book-tree.blade.php",
    "content": "<nav id=\"book-tree\"\n     class=\"book-tree mb-xl\"\n     aria-label=\"{{ trans('entities.books_navigation') }}\">\n\n    <h5>{{ trans('entities.books_navigation') }}</h5>\n\n    <ul class=\"sidebar-page-list mt-xs menu entity-list\">\n        @if (userCan(\\BookStack\\Permissions\\Permission::BookView, $book))\n            <li class=\"list-item-book book\">\n                @include('entities.list-item-basic', ['entity' => $book, 'classes' => ($current->matches($book)? 'selected' : '')])\n            </li>\n        @endif\n\n        @foreach($sidebarTree as $bookChild)\n            <li class=\"list-item-{{ $bookChild->getType() }} {{ $bookChild->getType() }} {{ $bookChild->isA('page') && $bookChild->draft ? 'draft' : '' }}\">\n                @include('entities.list-item-basic', ['entity' => $bookChild, 'classes' => $current->matches($bookChild)? 'selected' : ''])\n\n                @if($bookChild->isA('chapter') && count($bookChild->visible_pages) > 0)\n                    <div class=\"entity-list-item no-hover\">\n                        <span role=\"presentation\" class=\"icon text-chapter\"></span>\n                        <div class=\"content\">\n                            @include('chapters.parts.child-menu', [\n                                'chapter' => $bookChild,\n                                'current' => $current,\n                                'isOpen'  => $bookChild->matchesOrContains($current)\n                            ])\n                        </div>\n                    </div>\n\n                @endif\n\n            </li>\n        @endforeach\n    </ul>\n</nav>"
  },
  {
    "path": "resources/views/entities/breadcrumb-listing.blade.php",
    "content": "<div components=\"dropdown dropdown-search\"\n     option:dropdown-search:url=\"/search/entity/siblings?entity_type={{$entity->getType()}}&entity_id={{ $entity->id }}\"\n     option:dropdown-search:local-search-selector=\".entity-list-item\"\n     class=\"dropdown-search\">\n    <button class=\"dropdown-search-toggle-breadcrumb\"\n            refs=\"dropdown@toggle\"\n            aria-haspopup=\"true\"\n            aria-expanded=\"false\"\n            title=\"{{ trans('entities.breadcrumb_siblings_for_' . $entity->getType()) }}\">\n        <div role=\"presentation\" class=\"separator\">@icon('chevron-right')</div>\n    </button>\n    <div refs=\"dropdown@menu\" class=\"dropdown-search-dropdown card\">\n        <div class=\"dropdown-search-search\">\n            @icon('search')\n            <input refs=\"dropdown-search@searchInput\"\n                   aria-label=\"{{ trans('common.search') }}\"\n                   autocomplete=\"off\"\n                   placeholder=\"{{ trans('common.search') }}\"\n                   type=\"text\">\n        </div>\n        <div refs=\"dropdown-search@loading\">\n            @include('common.loading-icon')\n        </div>\n        <div refs=\"dropdown-search@listContainer\" class=\"dropdown-search-list px-m\" tabindex=\"-1\" role=\"list\"></div>\n    </div>\n</div>"
  },
  {
    "path": "resources/views/entities/breadcrumbs.blade.php",
    "content": "<nav class=\"breadcrumbs text-center\" aria-label=\"{{ trans('common.breadcrumb') }}\">\n    <?php $breadcrumbCount = 0; ?>\n\n    {{-- Show top level books item --}}\n    @if (count($crumbs) > 0 && ($crumbs[0] ?? null) instanceof  \\BookStack\\Entities\\Models\\Book)\n        <a href=\"{{  url('/books')  }}\" class=\"text-book icon-list-item outline-hover\">\n            <span>@icon('books')</span>\n            <span>{{ trans('entities.books') }}</span>\n        </a>\n        <?php $breadcrumbCount++; ?>\n    @endif\n\n    {{-- Show top level shelves item --}}\n    @if (count($crumbs) > 0 && ($crumbs[0] ?? null) instanceof  \\BookStack\\Entities\\Models\\Bookshelf)\n        <a href=\"{{  url('/shelves')  }}\" class=\"text-bookshelf icon-list-item outline-hover\">\n            <span>@icon('bookshelf')</span>\n            <span>{{ trans('entities.shelves') }}</span>\n        </a>\n        <?php $breadcrumbCount++; ?>\n    @endif\n\n    @foreach($crumbs as $key => $crumb)\n        <?php $isEntity = ($crumb instanceof \\BookStack\\Entities\\Models\\Entity); ?>\n\n        @if (is_null($crumb))\n            <?php continue; ?>\n        @endif\n        @if ($breadcrumbCount !== 0 && !$isEntity)\n            <div class=\"separator\">@icon('chevron-right')</div>\n        @endif\n\n        @if (is_string($crumb))\n            <a href=\"{{  url($key)  }}\">\n                {{ $crumb }}\n            </a>\n        @elseif (is_array($crumb))\n            <a href=\"{{  url($key)  }}\" class=\"icon-list-item outline-hover\">\n                <span>@icon($crumb['icon'])</span>\n                <span>{{ $crumb['text'] }}</span>\n            </a>\n        @elseif($isEntity && userCan(\\BookStack\\Permissions\\Permission::View, $crumb))\n            @if($breadcrumbCount > 0)\n                @include('entities.breadcrumb-listing', ['entity' => $crumb])\n            @endif\n            <a href=\"{{ $crumb->getUrl() }}\" class=\"text-{{$crumb->getType()}} icon-list-item outline-hover\">\n                <span>@icon($crumb->getType())</span>\n                <span>\n                    {{ $crumb->getShortName() }}\n                </span>\n            </a>\n        @endif\n        <?php $breadcrumbCount++; ?>\n    @endforeach\n</nav>"
  },
  {
    "path": "resources/views/entities/copy-considerations.blade.php",
    "content": "<p class=\"text-warn mb-none mt-l\">\n    @icon('warning') <strong>{{ trans('entities.copy_consider') }}</strong>\n</p>\n\n<div class=\"grid half no-gap no-row-gap text-warn mb-m\">\n    <ul class=\"pr-s mb-none\">\n        <li>{{ trans('entities.copy_consider_permissions') }}</li>\n        <li>{{ trans('entities.copy_consider_owner') }}</li>\n        <li>{{ trans('entities.copy_consider_images') }}</li>\n    </ul>\n    <ul class=\"pr-s mb-none\">\n        <li>{{ trans('entities.copy_consider_attachments') }}</li>\n        <li>{{ trans('entities.copy_consider_access') }}</li>\n    </ul>\n</div>"
  },
  {
    "path": "resources/views/entities/export-menu.blade.php",
    "content": "<div component=\"dropdown\"\n     class=\"dropdown-container\"\n     id=\"export-menu\">\n\n    <button refs=\"dropdown@toggle\"\n         class=\"icon-list-item text-link\"\n         aria-haspopup=\"true\"\n         aria-expanded=\"false\"\n         aria-label=\"{{ trans('entities.export') }}\"\n         data-shortcut=\"export\">\n        <span>@icon('export')</span>\n        <span>{{ trans('entities.export') }}</span>\n    </button>\n\n    <ul refs=\"dropdown@menu\" class=\"wide dropdown-menu\" role=\"menu\">\n        <li><a href=\"{{ $entity->getUrl('/export/html') }}\" target=\"_blank\" role=\"menuitem\" class=\"label-item\"><span>{{ trans('entities.export_html') }}</span><span>.html</span></a></li>\n        <li><a href=\"{{ $entity->getUrl('/export/pdf') }}\" target=\"_blank\" role=\"menuitem\" class=\"label-item\"><span>{{ trans('entities.export_pdf') }}</span><span>.pdf</span></a></li>\n        <li><a href=\"{{ $entity->getUrl('/export/plaintext') }}\" target=\"_blank\" role=\"menuitem\" class=\"label-item\"><span>{{ trans('entities.export_text') }}</span><span>.txt</span></a></li>\n        <li><a href=\"{{ $entity->getUrl('/export/markdown') }}\" target=\"_blank\" role=\"menuitem\" class=\"label-item\"><span>{{ trans('entities.export_md') }}</span><span>.md</span></a></li>\n        <li><a href=\"{{ $entity->getUrl('/export/zip') }}\" target=\"_blank\" role=\"menuitem\" class=\"label-item\"><span>{{ trans('entities.export_zip') }}</span><span>.zip</span></a></li>\n    </ul>\n\n</div>\n"
  },
  {
    "path": "resources/views/entities/favourite-action.blade.php",
    "content": "@php\n $isFavourite = $entity->isFavourite();\n@endphp\n<form action=\"{{ url('/favourites/' . ($isFavourite ? 'remove' : 'add')) }}\" method=\"POST\">\n    {{ csrf_field() }}\n    <input type=\"hidden\" name=\"type\" value=\"{{ $entity->getMorphClass() }}\">\n    <input type=\"hidden\" name=\"id\" value=\"{{ $entity->id }}\">\n    <button type=\"submit\" data-shortcut=\"favourite\" class=\"icon-list-item text-link\">\n        <span>@icon($isFavourite ? 'star' : 'star-outline')</span>\n        <span>{{ $isFavourite ? trans('common.unfavourite') : trans('common.favourite') }}</span>\n    </button>\n</form>"
  },
  {
    "path": "resources/views/entities/grid-item.blade.php",
    "content": "<a href=\"{{ $entity->getUrl() }}\" class=\"grid-card\"\n   data-entity-type=\"{{ $entity->getType() }}\" data-entity-id=\"{{ $entity->id }}\">\n    <div class=\"bg-{{ $entity->getType() }} featured-image-container-wrap\">\n        <div class=\"featured-image-container\" @if($entity->coverInfo()->exists()) style=\"background-image: url('{{ $entity->coverInfo()->getUrl() }}')\"@endif>\n        </div>\n        @icon($entity->getType())\n    </div>\n    <div class=\"grid-card-content\">\n        <h2 class=\"text-limit-lines-2\">{{ $entity->name }}</h2>\n        <p class=\"text-muted\">{{ $entity->getExcerpt(130) }}</p>\n    </div>\n    <div class=\"grid-card-footer text-muted \">\n        <p>@icon('star')<span title=\"{{ $dates->absolute($entity->created_at) }}\">{{ trans('entities.meta_created', ['timeLength' => $dates->relative($entity->created_at)]) }}</span></p>\n        <p>@icon('edit')<span title=\"{{ $dates->absolute($entity->updated_at) }}\">{{ trans('entities.meta_updated', ['timeLength' => $dates->relative($entity->updated_at)]) }}</span></p>\n    </div>\n</a>"
  },
  {
    "path": "resources/views/entities/icon-link.blade.php",
    "content": "<a href=\"{{ $entity->getUrl() }}\" class=\"flex-container-row items-center\">\n    <span role=\"presentation\"\n          class=\"icon flex-none text-{{$entity->getType()}}\">@icon($entity->getType())</span>\n    <div class=\"flex text-{{ $entity->getType() }}\">\n        {{ $entity->name }}\n    </div>\n</a>"
  },
  {
    "path": "resources/views/entities/list-basic.blade.php",
    "content": "<div class=\"entity-list {{ $style ?? '' }}\">\n    @if(count($entities) > 0)\n        @foreach($entities as $index => $entity)\n            @include('entities.list-item-basic', ['entity' => $entity])\n        @endforeach\n    @else\n        <p class=\"text-muted empty-text\">\n            {{ $emptyText ?? trans('common.no_items') }}\n        </p>\n    @endif\n</div>"
  },
  {
    "path": "resources/views/entities/list-item-basic.blade.php",
    "content": "<?php $type = $entity->getType(); ?>\n<a href=\"{{ $entity->getUrl() }}\"\n   class=\"{{$type}} {{$type === 'page' && $entity->draft ? 'draft' : ''}} {{$classes ?? ''}} entity-list-item\"\n   data-entity-type=\"{{$type}}\"\n   data-entity-id=\"{{$entity->id}}\">\n    <span role=\"presentation\" class=\"icon text-{{$type}}\">@icon($type)</span>\n    <div class=\"content\">\n            <h4 class=\"entity-list-item-name break-text\">{{ $entity->preview_name ?? $entity->name }}</h4>\n            {{ $slot ?? '' }}\n    </div>\n</a>"
  },
  {
    "path": "resources/views/entities/list-item.blade.php",
    "content": "@component('entities.list-item-basic', ['entity' => $entity, 'classes' => (($locked ?? false) ? 'disabled ' : '') . ($classes ?? '') ])\n\n<div class=\"entity-item-snippet\">\n\n    @if($locked ?? false)\n        <div class=\"text-warn my-xxs bold\">\n            @icon('lock'){{ trans('entities.entity_select_lack_permission') }}\n        </div>\n    @endif\n\n    @if($showPath ?? false)\n        @if($entity->relationLoaded('book') && $entity->book)\n            <span class=\"text-book\">{{ $entity->book->getShortName(42) }}</span>\n            @if($entity->relationLoaded('chapter') && $entity->chapter)\n                <span class=\"text-muted entity-list-item-path-sep\">@icon('chevron-right')</span> <span class=\"text-chapter\">{{ $entity->chapter->getShortName(42) }}</span>\n            @endif\n        @endif\n    @endif\n\n    <p class=\"text-muted break-text\">{{ $entity->preview_content ?? $entity->getExcerpt() }}</p>\n</div>\n\n@if(($showTags ?? false) && $entity->tags->count() > 0)\n    <div class=\"entity-item-tags mt-xs\">\n        @include('entities.tag-list', ['entity' => $entity, 'linked' => false ])\n    </div>\n@endif\n\n@if(($showUpdatedBy ?? false) && $entity->relationLoaded('updatedBy') && $entity->updatedBy)\n    <small title=\"{{ $dates->absolute($entity->updated_at) }}\">\n        {!! trans('entities.meta_updated_name', [\n            'timeLength' => $dates->relative($entity->updated_at),\n            'user' => e($entity->updatedBy->name)\n        ]) !!}\n    </small>\n@endif\n\n@endcomponent"
  },
  {
    "path": "resources/views/entities/list.blade.php",
    "content": "@if(count($entities) > 0)\n    <div class=\"entity-list {{ $style ?? '' }}\">\n        @foreach($entities as $index => $entity)\n            @include('entities.list-item', ['entity' => $entity, 'showPath' => $showPath ?? false, 'showTags' => $showTags ?? false])\n        @endforeach\n    </div>\n@else\n    <p class=\"text-muted empty-text pb-l mb-none\">\n        {{ $emptyText ?? trans('common.no_items') }}\n    </p>\n@endif"
  },
  {
    "path": "resources/views/entities/meta.blade.php",
    "content": "<div class=\"entity-meta\">\n    @if($entity->isA('revision'))\n        <div class=\"entity-meta-item\">\n            @icon('history')\n            <div>\n                {{ trans('entities.pages_revision') }}\n                {{ trans('entities.pages_revisions_number') }}{{ $entity->revision_number == 0 ? '' : $entity->revision_number }}\n            </div>\n        </div>\n    @endif\n\n    @if ($entity->isA('page'))\n        <a href=\"{{ $entity->getUrl('/revisions') }}\" class=\"entity-meta-item\">\n            @icon('history'){{ trans('entities.meta_revision', ['revisionCount' => $entity->revision_count]) }}\n        </a>\n    @endif\n\n    @if ($entity->ownedBy && $entity->owned_by !== $entity->created_by)\n        <div class=\"entity-meta-item\">\n            @icon('user')\n            <div>\n                {!! trans('entities.meta_owned_name', [\n                    'user' => \"<a href='{$entity->ownedBy->getProfileUrl()}'>\".e($entity->ownedBy->name). \"</a>\"\n                ]) !!}\n            </div>\n        </div>\n    @endif\n\n    @if ($entity->createdBy)\n        <div class=\"entity-meta-item\">\n            @icon('star')\n            <div>\n                {!! trans('entities.meta_created_name', [\n                    'timeLength' => '<span title=\"'. $dates->absolute($entity->created_at) .'\">'. $dates->relative($entity->created_at) . '</span>',\n                    'user' => \"<a href='{$entity->createdBy->getProfileUrl()}'>\".e($entity->createdBy->name). \"</a>\"\n                ]) !!}\n            </div>\n        </div>\n    @else\n        <div class=\"entity-meta-item\">\n            @icon('star')\n            <span title=\"{{ $dates->absolute($entity->created_at) }}\">{{ trans('entities.meta_created', ['timeLength' => $dates->relative($entity->created_at)]) }}</span>\n        </div>\n    @endif\n\n    @if ($entity->updatedBy)\n        <div class=\"entity-meta-item\">\n            @icon('edit')\n            <div>\n                {!! trans('entities.meta_updated_name', [\n                    'timeLength' => '<span title=\"' . $dates->absolute($entity->updated_at) .'\">' . $dates->relative($entity->updated_at) .'</span>',\n                    'user' => \"<a href='{$entity->updatedBy->getProfileUrl()}'>\".e($entity->updatedBy->name). \"</a>\"\n                ]) !!}\n            </div>\n        </div>\n    @elseif (!$entity->isA('revision'))\n        <div class=\"entity-meta-item\">\n            @icon('edit')\n            <span title=\"{{ $dates->absolute($entity->updated_at) }}\">{{ trans('entities.meta_updated', ['timeLength' => $dates->relative($entity->updated_at)]) }}</span>\n        </div>\n    @endif\n\n    @if($referenceCount ?? 0)\n        <a href=\"{{ $entity->getUrl('/references') }}\" class=\"entity-meta-item\">\n            @icon('reference')\n            <div>\n                {{ trans_choice('entities.meta_reference_count', $referenceCount, ['count' => $referenceCount]) }}\n            </div>\n        </a>\n    @endif\n\n    @if($watchOptions?->canWatch())\n        @if($watchOptions->isWatching())\n            @include('entities.watch-controls', [\n                'entity' => $entity,\n                'watchLevel' => $watchOptions->getWatchLevel(),\n                'label' => trans('entities.watch_detail_' . $watchOptions->getWatchLevel()),\n                'ignoring' => $watchOptions->getWatchLevel() === 'ignore',\n            ])\n        @elseif($watchedParent = $watchOptions->getWatchedParent())\n            @include('entities.watch-controls', [\n                'entity' => $entity,\n                'watchLevel' => $watchOptions->getWatchLevel(),\n                'label' => trans('entities.watch_detail_parent_' . $watchedParent->type . ($watchedParent->ignoring() ? '_ignore' : '')),\n                'ignoring' => $watchedParent->ignoring(),\n            ])\n        @endif\n    @endif\n</div>"
  },
  {
    "path": "resources/views/entities/references.blade.php",
    "content": "<main class=\"card content-wrap\">\n    <h1 class=\"list-heading\">{{ trans('entities.references') }}</h1>\n    <p>{{ trans('entities.references_to_desc') }}</p>\n\n    @if(count($references) > 0)\n        <div class=\"book-contents\">\n            @include('entities.list', ['entities' => $references->pluck('from'), 'showPath' => true])\n        </div>\n    @else\n        <p class=\"text-muted italic\">{{ trans('entities.references_none') }}</p>\n    @endif\n\n</main>"
  },
  {
    "path": "resources/views/entities/search-form.blade.php",
    "content": "{{--\n@label - Placeholder/aria-label text\n--}}\n<div class=\"mb-xl\">\n    <form refs=\"entity-search@searchForm\" class=\"search-box flexible\" role=\"search\">\n        <input refs=\"entity-search@searchInput\" type=\"text\"\n               aria-label=\"{{ $label }}\" name=\"term\" placeholder=\"{{ $label }}\">\n        <button tabindex=\"-1\" type=\"submit\" aria-label=\"{{ trans('common.search') }}\">@icon('search')</button>\n    </form>\n</div>"
  },
  {
    "path": "resources/views/entities/search-results.blade.php",
    "content": "<div refs=\"entity-search@searchView\" class=\"search-results hidden\">\n    <div class=\"grid half v-center\">\n        <h3 class=\"text-muted px-none\">\n            {{ trans('entities.search_results') }}\n        </h3>\n        <div class=\"text-right\">\n            <a refs=\"entity-search@clearButton\" class=\"button outline\">{{ trans('entities.search_clear') }}</a>\n        </div>\n    </div>\n\n    <div refs=\"entity-search@loadingBlock\">\n        @include('common.loading-icon')\n    </div>\n    <div class=\"book-contents\" refs=\"entity-search@searchResults\"></div>\n</div>"
  },
  {
    "path": "resources/views/entities/selector-popup.blade.php",
    "content": "<div id=\"entity-selector-wrap\">\n    <div components=\"popup entity-selector-popup\" class=\"popup-background\">\n        <div class=\"popup-body small\" tabindex=\"-1\">\n            <div class=\"popup-header primary-background\">\n                <div class=\"popup-title\">{{ trans('entities.entity_select') }}</div>\n                <button refs=\"popup@hide\" type=\"button\" class=\"popup-header-close\">@icon('close')</button>\n            </div>\n            @include('entities.selector', ['name' => 'entity-selector', 'selectorEndpoint' => ''])\n            <div class=\"popup-footer\">\n                <button refs=\"entity-selector-popup@select\" type=\"button\" disabled class=\"button\">{{ trans('common.select') }}</button>\n            </div>\n        </div>\n    </div>\n</div>"
  },
  {
    "path": "resources/views/entities/selector.blade.php",
    "content": "{{--\n$name - string\n$autofocus - boolean, optional\n$entityTypes - string, optional\n$entityPermission - string, optional\n$selectorEndpoint - string, optional\n$selectorSize - string, optional (compact)\n--}}\n<div class=\"form-group entity-selector-container\">\n    <div component=\"entity-selector\"\n         refs=\"entity-selector-popup@selector\"\n         class=\"entity-selector {{$selectorSize ?? ''}}\"\n         option:entity-selector:entity-types=\"{{ $entityTypes ?? 'book,chapter,page' }}\"\n         option:entity-selector:entity-permission=\"{{ $entityPermission ?? 'view' }}\"\n         option:entity-selector:search-endpoint=\"{{ $selectorEndpoint ?? '/search/entity-selector' }}\">\n        <input refs=\"entity-selector@input\" type=\"hidden\" name=\"{{$name}}\" value=\"\">\n        <input refs=\"entity-selector@search\" type=\"text\" placeholder=\"{{ trans('common.search') }}\" @if($autofocus ?? false) autofocus @endif>\n        <div class=\"text-center loading\" refs=\"entity-selector@loading\">@include('common.loading-icon')</div>\n        <div refs=\"entity-selector@results\"></div>\n    </div>\n</div>"
  },
  {
    "path": "resources/views/entities/sibling-navigation.blade.php",
    "content": "<div id=\"sibling-navigation\" class=\"grid half collapse-xs items-center mb-m px-m no-row-gap fade-in-when-active print-hidden\">\n    <div>\n        @if($previous)\n            <a href=\"{{  $previous->getUrl()  }}\" data-shortcut=\"previous\" class=\"outline-hover no-link-style block rounded\">\n                <div class=\"px-m pt-xs text-muted\">{{ trans('common.previous') }}</div>\n                <div class=\"inline-block\">\n                    <div class=\"icon-list-item no-hover\">\n                        <span class=\"text-{{ $previous->getType() }} \">@icon($previous->getType())</span>\n                        <span>{{ $previous->getShortName(48) }}</span>\n                    </div>\n                </div>\n            </a>\n        @endif\n    </div>\n    <div>\n        @if($next)\n            <a href=\"{{  $next->getUrl()  }}\" data-shortcut=\"next\" class=\"outline-hover no-link-style block rounded text-xs-right\">\n                <div class=\"px-m pt-xs text-muted text-xs-right\">{{ trans('common.next') }}</div>\n                <div class=\"inline block\">\n                    <div class=\"icon-list-item no-hover\">\n                        <span class=\"text-{{ $next->getType() }} \">@icon($next->getType())</span>\n                        <span>{{ $next->getShortName(48) }}</span>\n                    </div>\n                </div>\n            </a>\n        @endif\n    </div>\n</div>"
  },
  {
    "path": "resources/views/entities/tag-list.blade.php",
    "content": "@foreach($entity->tags as $tag)\n    @include('entities.tag', ['tag' => $tag])\n@endforeach"
  },
  {
    "path": "resources/views/entities/tag-manager-list.blade.php",
    "content": "@foreach(array_merge($tags, [null, null]) as $index => $tag)\n    <div class=\"card drag-card {{ $loop->last ? 'hidden' : '' }}\" @if($loop->last) refs=\"add-remove-rows@model\" @endif>\n        <div class=\"handle\">@icon('grip')</div>\n        @foreach(['name', 'value'] as $type)\n            <div component=\"auto-suggest\"\n                 option:auto-suggest:url=\"{{ url('/ajax/tags/suggest/' . $type . 's') }}\"\n                 option:auto-suggest:type=\"{{ $type }}\"\n                 class=\"outline\">\n                <input value=\"{{ $tag->$type ?? '' }}\"\n                       placeholder=\"{{ trans('entities.tag_' . $type) }}\"\n                       aria-label=\"{{ trans('entities.tag_' . $type) }}\"\n                       name=\"tags[{{ $loop->parent->last ? 'randrowid' : $index }}][{{ $type }}]\"\n                       type=\"text\"\n                       refs=\"auto-suggest@input\"\n                       autocomplete=\"off\"/>\n                <ul refs=\"auto-suggest@list\" class=\"suggestion-box dropdown-menu\"></ul>\n            </div>\n        @endforeach\n        <button type=\"button\"\n                aria-label=\"{{ trans('entities.tags_remove') }}\"\n                class=\"text-center drag-card-action text-neg\">\n            @icon('close')\n        </button>\n    </div>\n@endforeach"
  },
  {
    "path": "resources/views/entities/tag-manager.blade.php",
    "content": "<div components=\"tag-manager add-remove-rows\"\n     option:add-remove-rows:row-selector=\".card\"\n     option:add-remove-rows:remove-selector=\"button.text-neg\"\n     option:tag-manager:row-selector=\".card:not(.hidden)\"\n     refs=\"tag-manager@add-remove\"\n     class=\"tags\">\n\n    <p class=\"text-muted small\">\n        {!! nl2br(e(trans('entities.tags_explain'))) !!} <br>\n        <a href=\"{{ url('/tags') }}\" target=\"_blank\">{{ trans('entities.tags_view_existing_tags') }}</a>.\n    </p>\n\n    <div component=\"sortable-list\"\n         option:sortable-list:handle-selector=\".handle\">\n        @include('entities.tag-manager-list', ['tags' => $entity ? $entity->tags->all() : []])\n    </div>\n\n    <button refs=\"add-remove-rows@add\" type=\"button\" class=\"text-button\">{{ trans('entities.tags_add') }}</button>\n</div>"
  },
  {
    "path": "resources/views/entities/tag.blade.php",
    "content": "<div class=\"tag-item primary-background-light\" data-name=\"{{ $tag->name }}\" data-value=\"{{ $tag->value }}\">\n    @if($linked ?? true)\n        <div class=\"tag-name {{ $tag->highlight_name ? 'highlight' : '' }}\"><a href=\"{{ $tag->nameUrl() }}\">@icon('tag'){{ $tag->name }}</a></div>\n        @if($tag->value) <div class=\"tag-value {{ $tag->highlight_value ? 'highlight' : '' }}\"><a href=\"{{ $tag->valueUrl() }}\">{{$tag->value}}</a></div> @endif\n    @else\n        <div class=\"tag-name {{ $tag->highlight_name ? 'highlight' : '' }}\"><span>@icon('tag'){{ $tag->name }}</span></div>\n        @if($tag->value) <div class=\"tag-value {{ $tag->highlight_value ? 'highlight' : '' }}\"><span>{{$tag->value}}</span></div> @endif\n    @endif\n</div>"
  },
  {
    "path": "resources/views/entities/template-selector.blade.php",
    "content": "<div class=\"flex-container-row gap-l justify-space-between pb-xs wrap\">\r\n    <p class=\"text-muted small my-none min-width-xs flex\">\r\n        {{ trans('entities.default_template_explain') }}\r\n    </p>\r\n\r\n    <div class=\"min-width-m\">\r\n        @include('form.page-picker', [\r\n            'name' => 'default_template_id',\r\n            'placeholder' => trans('entities.default_template_select'),\r\n            'value' => $entity->default_template_id ?? null,\r\n            'selectorEndpoint' => '/search/entity-selector-templates',\r\n        ])\r\n    </div>\r\n</div>"
  },
  {
    "path": "resources/views/entities/view-toggle.blade.php",
    "content": "<div>\n    <form action=\"{{ url(\"/preferences/change-view/\" . $type) }}\" method=\"POST\" class=\"inline\">\n        {{ csrf_field() }}\n        {{ method_field('patch') }}\n        <input type=\"hidden\" name=\"_return\" value=\"{{ url()->current() }}\">\n\n        @if ($view === 'list')\n            <button type=\"submit\" name=\"view\" value=\"grid\" class=\"icon-list-item text-link\">\n                <span class=\"icon\">@icon('grid')</span>\n                <span>{{ trans('common.grid_view') }}</span>\n            </button>\n        @else\n            <button type=\"submit\" name=\"view\" value=\"list\" class=\"icon-list-item text-link\">\n                <span class=\"icon\">@icon('list')</span>\n                <span>{{ trans('common.list_view') }}</span>\n            </button>\n        @endif\n    </form>\n</div>"
  },
  {
    "path": "resources/views/entities/watch-action.blade.php",
    "content": "<form action=\"{{ url('/watching/update') }}\" method=\"POST\">\n    {{ csrf_field() }}\n    {{ method_field('PUT') }}\n    <input type=\"hidden\" name=\"type\" value=\"{{ $entity->getMorphClass() }}\">\n    <input type=\"hidden\" name=\"id\" value=\"{{ $entity->id }}\">\n    <button type=\"submit\"\n            name=\"level\"\n            value=\"updates\"\n            class=\"icon-list-item text-link\">\n        <span>@icon('watch')</span>\n        <span>{{ trans('entities.watch') }}</span>\n    </button>\n</form>"
  },
  {
    "path": "resources/views/entities/watch-controls.blade.php",
    "content": "<div component=\"dropdown\"\n     class=\"dropdown-container block my-xxs\">\n    <a refs=\"dropdown@toggle\"\n       aria-haspopup=\"menu\"\n       aria-expanded=\"false\"\n       role=\"button\"\n       href=\"#\"\n       class=\"entity-meta-item my-none\">\n        @icon(($ignoring ? 'watch-ignore' : 'watch'))\n        <span>{{ $label }}</span>\n    </a>\n    <form action=\"{{ url('/watching/update') }}\" method=\"POST\">\n        {{ method_field('PUT') }}\n        {{ csrf_field() }}\n        <input type=\"hidden\" name=\"type\" value=\"{{ $entity->getMorphClass() }}\">\n        <input type=\"hidden\" name=\"id\" value=\"{{ $entity->id }}\">\n\n        <ul refs=\"dropdown@menu\" class=\"dropdown-menu xl-limited anchor-left pb-none\" role=\"menu\">\n            @foreach(\\BookStack\\Activity\\WatchLevels::allSuitedFor($entity) as $option => $value)\n                <li>\n                    <button name=\"level\" value=\"{{ $option }}\" class=\"icon-item\" role=\"menuitem\">\n                        @if($watchLevel === $option)\n                            <span class=\"text-pos pt-m\"\n                                  title=\"{{ trans('common.status_active') }}\">@icon('check-circle')</span>\n                        @else\n                            <span title=\"{{ trans('common.status_inactive') }}\"></span>\n                        @endif\n                        <div class=\"break-text\">\n                            <div class=\"mb-xxs\"><strong>{{ trans('entities.watch_title_' . $option) }}</strong></div>\n                            <div class=\"text-muted text-small\">\n                                @if(trans()->has('entities.watch_desc_' . $option . '_' . $entity->getMorphClass()))\n                                    {{ trans('entities.watch_desc_' . $option . '_' . $entity->getMorphClass()) }}\n                                @else\n                                    {{ trans('entities.watch_desc_' . $option) }}\n                                @endif\n                            </div>\n                        </div>\n                    </button>\n                </li>\n                <li role=\"presentation\">\n                    <hr class=\"my-none\">\n                </li>\n            @endforeach\n            <li>\n                <a href=\"{{ url('/my-account/notifications') }}\"\n                   role=\"menuitem\"\n                   target=\"_blank\"\n                   class=\"text-item text-muted text-small break-text\">{{ trans('entities.watch_change_default') }}</a>\n            </li>\n        </ul>\n    </form>\n</div>"
  },
  {
    "path": "resources/views/errors/404.blade.php",
    "content": "@extends('layouts.simple')\n@inject('popular', \\BookStack\\Entities\\Queries\\QueryPopular::class)\n@section('content')\n    <div class=\"container mt-l\">\n\n        <div class=\"card mb-xl px-l pb-l pt-l\">\n            <div class=\"grid half v-center\">\n                <div>\n                    @include('errors.parts.not-found-text', [\n                        'title' => $message ?? trans('errors.404_page_not_found'),\n                        'subtitle' => $subtitle ?? trans('errors.sorry_page_not_found'),\n                        'details' => $details ?? trans('errors.sorry_page_not_found_permission_warning'),\n                    ])\n                </div>\n                <div class=\"text-right\">\n                    @if(user()->isGuest())\n                        <a href=\"{{ url('/login') }}\" class=\"button outline\">{{ trans('auth.log_in') }}</a>\n                    @endif\n                    <a href=\"{{ url('/') }}\" class=\"button outline\">{{ trans('errors.return_home') }}</a>\n                </div>\n            </div>\n\n        </div>\n\n        @if (setting('app-public') || !user()->isGuest())\n            <div class=\"grid third gap-xxl\">\n                <div>\n                    <div class=\"card mb-xl\">\n                        <h3 class=\"card-title\">{{ trans('entities.pages_popular') }}</h3>\n                        <div class=\"px-m\">\n                            @include('entities.list', ['entities' => $popular->run(10, 0, ['page']), 'style' => 'compact'])\n                        </div>\n                    </div>\n                </div>\n                <div>\n                    <div class=\"card mb-xl\">\n                        <h3 class=\"card-title\">{{ trans('entities.books_popular') }}</h3>\n                        <div class=\"px-m\">\n                            @include('entities.list', ['entities' => $popular->run(10, 0, ['book']), 'style' => 'compact'])\n                        </div>\n                    </div>\n                </div>\n                <div>\n                    <div class=\"card mb-xl\">\n                        <h3 class=\"card-title\">{{ trans('entities.chapters_popular') }}</h3>\n                        <div class=\"px-m\">\n                            @include('entities.list', ['entities' => $popular->run(10, 0, ['chapter']), 'style' => 'compact'])\n                        </div>\n                    </div>\n                </div>\n            </div>\n        @endif\n    </div>\n\n@stop"
  },
  {
    "path": "resources/views/errors/500.blade.php",
    "content": "@extends('layouts.base')\n\n@section('content')\n\n    <div class=\"container small py-xl\">\n\n        <main class=\"card content-wrap auto-height\">\n            <div id=\"main-content\" class=\"body\">\n                <h3>{{ trans('errors.error_occurred') }}</h3>\n                <h5 class=\"mb-m\">{{ $message ?? 'An unknown error occurred' }}</h5>\n                <p><a href=\"{{ url('/') }}\" class=\"button outline\">{{ trans('errors.return_home') }}</a></p>\n            </div>\n        </main>\n\n    </div>\n\n@stop"
  },
  {
    "path": "resources/views/errors/503.blade.php",
    "content": "@extends('layouts.plain')\n\n@section('content')\n    <div id=\"content\" class=\"block\">\n        <div class=\"container small mt-xl\">\n            <div class=\"card content-wrap auto-height\">\n                <h1 class=\"list-heading\">{{ trans('errors.app_down', ['appName' => setting('app-name')]) }}</h1>\n                <p>{{ trans('errors.back_soon') }}</p>\n            </div>\n        </div>\n    </div>\n@endsection"
  },
  {
    "path": "resources/views/errors/debug.blade.php",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\"\n          content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\">\n    <title>Error: {{ $error }}</title>\n\n    <style>\n        html, body {\n            background-color: #F2F2F2;\n            font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Oxygen\", \"Ubuntu\", \"Roboto\", \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n        }\n\n        html {\n            padding: 0;\n        }\n\n        body {\n            margin: 0;\n            border-top: 6px solid #206ea7;\n        }\n\n        h1 {\n            margin-top: 0;\n        }\n\n        h2 {\n            color: #666;\n            font-size: 1rem;\n            margin-bottom: 0;\n        }\n\n        .container {\n            max-width: 800px;\n            margin: 1rem auto;\n        }\n\n        .panel {\n            background-color: #FFF;\n            border-radius: 3px;\n            box-shadow: 0 1px 6px -1px rgba(0, 0, 0, 0.1);\n            padding: 1rem 2rem;\n            margin: 2rem 1rem;\n        }\n\n        .panel-title {\n            font-weight: bold;\n            font-size: 1rem;\n            color: #FFF;\n            margin-top: 0;\n            margin-bottom: 0;\n            background-color: #206ea7;\n            padding: 0.25rem .5rem;\n            display: inline-block;\n            border-radius: 3px;\n        }\n\n        pre {\n            overflow-x: scroll;\n            background-color: #EEE;\n            border: 1px solid #DDD;\n            padding: .25rem;\n            border-radius: 3px;\n        }\n\n        a {\n            color: #206ea7;\n            text-decoration: none;\n        }\n\n        a:hover, a:focus {\n            text-decoration: underline;\n            color: #105282;\n        }\n\n        ul {\n            margin-left: 0;\n            padding-left: 1rem;\n        }\n\n        li {\n            margin-bottom: .4rem;\n        }\n\n        .notice {\n            margin-top: 2rem;\n            padding: 0 2rem;\n            font-weight: bold;\n            color: #666;\n        }\n    </style>\n</head>\n<body>\n    <div class=\"container\">\n\n        <p class=\"notice\">\n            WARNING: Application is in debug mode. This mode has the potential to leak confidential\n            information and therefore should not be used in production or publicly\n            accessible environments.\n        </p>\n\n        <div class=\"panel\">\n            <h4 class=\"panel-title\">Error</h4>\n            <h2>{{ $errorClass }}</h2>\n            <h1>{{ $error }}</h1>\n        </div>\n\n        <div class=\"panel\">\n            <h4 class=\"panel-title\">Help Resources</h4>\n            <ul>\n                <li>\n                    <a href=\"https://www.bookstackapp.com/docs/admin/debugging/\" target=\"_blank\">Review BookStack debugging documentation &raquo;</a>\n                </li>\n                <li>\n                    <a href=\"https://github.com/BookStackApp/BookStack/releases\" target=\"_blank\">Ensure your instance is up-to-date &raquo;</a>\n                </li>\n                <li>\n                    <a href=\"https://github.com/BookStackApp/BookStack/issues?q=is%3Aissue+{{ urlencode($error) }}\" target=\"_blank\">Search for the issue on GitHub &raquo;</a>\n                </li>\n                <li>\n                    <a href=\"https://discord.gg/ztkBqR2\" target=\"_blank\">Ask for help via Discord &raquo;</a>\n                </li>\n                <li>\n                    <a href=\"https://duckduckgo.com/?q={{urlencode(\"BookStack {$error}\")}}\" target=\"_blank\">Search the error message &raquo;</a>\n                </li>\n            </ul>\n        </div>\n\n        <div class=\"panel\">\n            <h4 class=\"panel-title\">Environment</h4>\n            <ul>\n                @foreach($environment as $label => $text)\n                <li><strong>{{ $label }}:</strong> {{ $text }}</li>\n                @endforeach\n            </ul>\n        </div>\n\n        <div class=\"panel\">\n            <h4 class=\"panel-title\">Stack Trace</h4>\n            <pre>{{ $trace }}</pre>\n        </div>\n\n    </div>\n</body>\n</html>"
  },
  {
    "path": "resources/views/errors/parts/not-found-text.blade.php",
    "content": "{{--The below text may be dynamic based upon language and scenario.--}}\n{{--It's safer to add new text sections here rather than altering existing ones.--}}\n<h1 class=\"list-heading\">{{ $title }}</h1>\n<h5>{{ $subtitle }}</h5>\n<p>{{ $details }}</p>"
  },
  {
    "path": "resources/views/exports/book.blade.php",
    "content": "@extends('layouts.export')\n\n@section('title', $book->name)\n\n@section('content')\n\n    <h1 style=\"font-size: 4.8em\">{{$book->name}}</h1>\n    <div>{!! $book->descriptionInfo()->getHtml() !!}</div>\n\n    @include('exports.parts.book-contents-menu', ['children' => $bookChildren])\n\n    @foreach($bookChildren as $bookChild)\n        @if($bookChild->isA('chapter'))\n            @include('exports.parts.chapter-item', ['chapter' => $bookChild])\n        @else\n            @include('exports.parts.page-item', ['page' => $bookChild, 'chapter' => null])\n        @endif\n    @endforeach\n\n@endsection"
  },
  {
    "path": "resources/views/exports/chapter.blade.php",
    "content": "@extends('layouts.export')\n\n@section('title', $chapter->name)\n\n@section('content')\n\n    <h1 style=\"font-size: 4.8em\">{{$chapter->name}}</h1>\n    <div>{!! $chapter->descriptionInfo()->getHtml() !!}</div>\n\n    @include('exports.parts.chapter-contents-menu', ['pages' => $pages])\n\n    @foreach($pages as $page)\n        @include('exports.parts.page-item', ['page' => $page, 'chapter' => null])\n    @endforeach\n\n@endsection"
  },
  {
    "path": "resources/views/exports/import-show.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n    <div class=\"container small\">\n\n        <main class=\"card content-wrap auto-height mt-xxl\">\n            <h1 class=\"list-heading\">{{ trans('entities.import_continue') }}</h1>\n            <p class=\"text-muted\">{{ trans('entities.import_continue_desc') }}</p>\n\n            @if(session()->has('import_errors'))\n                <div class=\"mb-m\">\n                    <label class=\"setting-list-label mb-xs text-neg\">@icon('warning') {{ trans('entities.import_errors') }}</label>\n                    <p class=\"mb-xs small\">{{ trans('entities.import_errors_desc') }}</p>\n                    @foreach(session()->get('import_errors') ?? [] as $error)\n                        <p class=\"mb-none text-neg\">{{ $error }}</p>\n                    @endforeach\n                    <hr class=\"mt-m\">\n                </div>\n            @endif\n\n            <div class=\"mb-m\">\n                <label class=\"setting-list-label mb-m\">{{ trans('entities.import_details') }}</label>\n                <div class=\"flex-container-row justify-space-between wrap\">\n                    <div>\n                        @include('exports.parts.import-item', ['type' => $import->type, 'model' => $data])\n                    </div>\n                    <div class=\"text-right text-muted\">\n                        <div>{{ trans('entities.import_size', ['size' => $import->getSizeString()]) }}</div>\n                        <div><span title=\"{{ $dates->absolute($import->created_at) }}\">{{ trans('entities.import_uploaded_at', ['relativeTime' => $dates->relative($import->created_at)]) }}</span></div>\n                        @if($import->createdBy)\n                            <div>\n                                {{ trans('entities.import_uploaded_by') }}\n                                <a href=\"{{ $import->createdBy->getProfileUrl() }}\">{{ $import->createdBy->name }}</a>\n                            </div>\n                        @endif\n                    </div>\n                </div>\n            </div>\n\n            <form id=\"import-run-form\"\n                  action=\"{{ $import->getUrl() }}\"\n                  method=\"POST\">\n                {{ csrf_field() }}\n\n                @if($import->type === 'page' || $import->type === 'chapter')\n                    <hr>\n                    <label class=\"setting-list-label\">{{ trans('entities.import_location') }}</label>\n                    <p class=\"small mb-s\">{{ trans('entities.import_location_desc') }}</p>\n                    @if($errors->has('parent'))\n                        <div class=\"mb-s\">\n                            @include('form.errors', ['name' => 'parent'])\n                        </div>\n                    @endif\n                    @include('entities.selector', [\n                        'name' => 'parent',\n                        'entityTypes' => $import->type === 'page' ? 'chapter,book' : 'book',\n                        'entityPermission' => \"{$import->type}-create\",\n                        'selectorSize' => 'compact small',\n                    ])\n                @endif\n\n                <div class=\"flex-container-row items-center justify-flex-end\">\n                    <a href=\"{{ url('/import') }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                    <div component=\"dropdown\" class=\"inline block mx-s\">\n                        <button refs=\"dropdown@toggle\"\n                                type=\"button\"\n                                title=\"{{ trans('common.delete') }}\"\n                                class=\"button outline\">{{ trans('common.delete') }}</button>\n                        <div refs=\"dropdown@menu\" class=\"dropdown-menu\">\n                            <p class=\"text-neg bold small px-m mb-xs\">{{ trans('entities.import_delete_confirm') }}</p>\n                            <p class=\"small px-m mb-xs\">{{ trans('entities.import_delete_desc') }}</p>\n                            <button type=\"submit\" form=\"import-delete-form\" class=\"text-link small text-item\">{{ trans('common.confirm') }}</button>\n                        </div>\n                    </div>\n                    <button component=\"loading-button\" type=\"submit\" class=\"button\">{{ trans('entities.import_run') }}</button>\n                </div>\n            </form>\n        </main>\n    </div>\n\n    <form id=\"import-delete-form\"\n          action=\"{{ $import->getUrl() }}\"\n          method=\"post\">\n        {{ method_field('DELETE') }}\n        {{ csrf_field() }}\n    </form>\n\n@stop\n"
  },
  {
    "path": "resources/views/exports/import.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        <main class=\"card content-wrap auto-height mt-xxl\">\n            <h1 class=\"list-heading\">{{ trans('entities.import') }}</h1>\n            <form action=\"{{ url('/import') }}\" enctype=\"multipart/form-data\" method=\"POST\">\n                {{ csrf_field() }}\n                <div class=\"flex-container-row justify-space-between wrap gap-x-xl gap-y-s\">\n                    <p class=\"flex min-width-l text-muted mb-s\">{{ trans('entities.import_desc') }}</p>\n                    <div class=\"flex-none min-width-l flex-container-row justify-flex-end\">\n                        <div class=\"mb-m\">\n                            <label for=\"file\">{{ trans('entities.import_zip_select') }}</label>\n                            <input type=\"file\"\n                                   accept=\".zip,application/zip,application/x-zip-compressed\"\n                                   name=\"file\"\n                                   id=\"file\"\n                                   class=\"custom-simple-file-input\">\n                            @include('form.errors', ['name' => 'file'])\n                        </div>\n                    </div>\n                </div>\n\n                @if(count($zipErrors) > 0)\n                    <p class=\"mb-xs\"><strong class=\"text-neg\">{{ trans('entities.import_zip_validation_errors') }}</strong></p>\n                    <ul class=\"mb-m\">\n                        @foreach($zipErrors as $key => $error)\n                            <li><strong class=\"text-neg\">[{{ $key }}]</strong>: {{ $error }}</li>\n                        @endforeach\n                    </ul>\n                @endif\n\n                <div class=\"text-right\">\n                    <a href=\"{{ url('/books') }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                    <button type=\"submit\" class=\"button\">{{ trans('entities.import_validate') }}</button>\n                </div>\n            </form>\n        </main>\n\n        <main class=\"card content-wrap auto-height mt-xxl\">\n            <h2 class=\"list-heading\">{{ trans('entities.import_pending') }}</h2>\n            @if(count($imports) === 0)\n                <p>{{ trans('entities.import_pending_none') }}</p>\n            @else\n                <div class=\"item-list my-m\">\n                    @foreach($imports as $import)\n                        @include('exports.parts.import', ['import' => $import])\n                    @endforeach\n                </div>\n            @endif\n        </main>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/exports/page.blade.php",
    "content": "@extends('layouts.export')\n\n@section('title', $page->name)\n\n@section('content')\n    @include('pages.parts.page-display')\n\n    <hr>\n\n    <div class=\"text-muted text-small\">\n        @include('exports.parts.meta', ['entity' => $page])\n    </div>\n@endsection"
  },
  {
    "path": "resources/views/exports/parts/book-contents-menu.blade.php",
    "content": "@if(count($children) > 0)\n    <ul class=\"contents\">\n        @foreach($children as $bookChild)\n            <li><a href=\"#{{$bookChild->getType()}}-{{$bookChild->id}}\">{{ $bookChild->name }}</a></li>\n            @if($bookChild->isA('chapter') && count($bookChild->visible_pages) > 0)\n                @include('exports.parts.chapter-contents-menu', ['pages' => $bookChild->visible_pages])\n            @endif\n        @endforeach\n    </ul>\n@endif"
  },
  {
    "path": "resources/views/exports/parts/chapter-contents-menu.blade.php",
    "content": "@if (count($pages) > 0)\n        <ul class=\"contents\">\n            @foreach($pages as $page)\n                <li><a href=\"#page-{{$page->id}}\">{{ $page->name }}</a></li>\n            @endforeach\n        </ul>\n@endif"
  },
  {
    "path": "resources/views/exports/parts/chapter-item.blade.php",
    "content": "<div class=\"page-break\"></div>\n<h1 id=\"chapter-{{$chapter->id}}\">{{ $chapter->name }}</h1>\n\n<div>{!! $chapter->descriptionInfo()->getHtml() !!}</div>\n\n@if(count($chapter->visible_pages) > 0)\n    @foreach($chapter->visible_pages as $page)\n        @include('exports.parts.page-item', ['page' => $page, 'chapter' => $chapter])\n    @endforeach\n@endif"
  },
  {
    "path": "resources/views/exports/parts/custom-head.blade.php",
    "content": "@inject('headContent', 'BookStack\\Theming\\CustomHtmlHeadContentProvider')\n\n@if(setting('app-custom-head'))\n<!-- Custom user content -->\n{!! $headContent->forExport() !!}\n<!-- End custom user content -->\n@endif"
  },
  {
    "path": "resources/views/exports/parts/import-item.blade.php",
    "content": "{{--\n$type - string\n$model - object\n--}}\n<div class=\"import-item text-{{ $type }} mb-xs\">\n    <p class=\"mb-none\">@icon($type){{ $model->name }}</p>\n    <div class=\"ml-s\">\n        <div class=\"text-muted\">\n            @if($model->attachments ?? [])\n                <span>@icon('attach'){{ count($model->attachments) }}</span>\n            @endif\n            @if($model->images ?? [])\n                <span>@icon('image'){{ count($model->images) }}</span>\n            @endif\n            @if($model->tags ?? [])\n                <span>@icon('tag'){{ count($model->tags) }}</span>\n            @endif\n        </div>\n        @if(method_exists($model, 'children'))\n            @foreach($model->children() as $child)\n                @include('exports.parts.import-item', [\n                    'type' => ($child instanceof \\BookStack\\Exports\\ZipExports\\Models\\ZipExportPage) ? 'page' : 'chapter',\n                    'model' => $child\n                ])\n            @endforeach\n        @endif\n    </div>\n</div>"
  },
  {
    "path": "resources/views/exports/parts/import.blade.php",
    "content": "<div class=\"item-list-row flex-container-row items-center justify-space-between wrap\">\n    <div class=\"px-m py-s\">\n        <a href=\"{{ $import->getUrl() }}\"\n           class=\"text-{{ $import->type }}\">@icon($import->type) {{ $import->name }}</a>\n    </div>\n    <div class=\"px-m py-s flex-container-row gap-m items-center\">\n        <div class=\"bold opacity-80 text-muted\">{{ $import->getSizeString() }}</div>\n        <div class=\"bold opacity-80 text-muted min-width-xs text-right\" title=\"{{ $dates->absolute($import->created_at) }}\">@icon('time'){{ $dates->relative($import->created_at) }}</div>\n    </div>\n</div>"
  },
  {
    "path": "resources/views/exports/parts/meta.blade.php",
    "content": "<div class=\"entity-meta\">\n    @if ($entity->isA('page'))\n        @icon('history'){{ trans('entities.meta_revision', ['revisionCount' => $entity->revision_count]) }} <br>\n    @endif\n\n    @icon('star'){!! trans('entities.meta_created' . ($entity->createdBy ? '_name' : ''), [\n        'timeLength' => $dates->absolute($entity->created_at),\n        'user' => e($entity->createdBy->name ?? ''),\n        ]) !!}\n    <br>\n\n    @icon('edit'){!! trans('entities.meta_updated' . ($entity->updatedBy ? '_name' : ''), [\n            'timeLength' => $dates->absolute($entity->updated_at),\n            'user' => e($entity->updatedBy->name ?? '')\n        ]) !!}\n</div>"
  },
  {
    "path": "resources/views/exports/parts/page-item.blade.php",
    "content": "<div class=\"page-break\"></div>\n\n@if (isset($chapter))\n    <div class=\"chapter-hint\">{{$chapter->name}}</div>\n@endif\n\n<h1 id=\"page-{{$page->id}}\">{{ $page->name }}</h1>\n{!! $page->html !!}"
  },
  {
    "path": "resources/views/exports/parts/styles.blade.php",
    "content": "{{-- Fetch in our standard export styles --}}\n<style>\n    @if (!app()->runningUnitTests())\n        {!! file_get_contents(public_path('/dist/export-styles.css')) !!}\n    @endif\n</style>\n\n{{-- Apply any additional styles that can't be applied via our standard SCSS export styles --}}\n@if ($format === 'pdf')\n    <style>\n        /* Patches for CSS variable colors within PDF exports */\n        a {\n            color: {{ setting('app-link') }};\n        }\n\n        blockquote {\n            border-left-color: {{ setting('app-color') }};\n        }\n    </style>\n@endif"
  },
  {
    "path": "resources/views/form/checkbox.blade.php",
    "content": "{{--\n$name\n$label\n$errors?\n$model?\n--}}\n@include('form.custom-checkbox', [\n    'name' => $name,\n    'label' => $label,\n    'value' => 'true',\n    'checked' => old($name) || (!old() && isset($model) && $model->$name)\n])\n\n@if($errors->has($name))\n    <div class=\"text-neg text-small\">{{ $errors->first($name) }}</div>\n@endif"
  },
  {
    "path": "resources/views/form/custom-checkbox.blade.php",
    "content": "{{--\n$name\n$value\n$checked\n$label\n--}}\n<label component=\"custom-checkbox\" class=\"toggle-switch @if($errors->has($name)) text-neg @endif\">\n    <input type=\"checkbox\" name=\"{{$name}}\" value=\"{{ $value }}\" @if($checked) checked=\"checked\" @endif @if($disabled ?? false) disabled=\"disabled\" @endif>\n    <span tabindex=\"0\" role=\"checkbox\"\n          aria-checked=\"{{ $checked ? 'true' : 'false' }}\"\n          class=\"custom-checkbox text-primary\">@icon('check')</span>\n    <span class=\"label\">{{$label}}</span>\n</label>"
  },
  {
    "path": "resources/views/form/date.blade.php",
    "content": "<input type=\"date\" id=\"{{ $name }}\" name=\"{{ $name }}\"\n       @if($errors->has($name)) class=\"text-neg\" @endif\n       placeholder=\"{{ $placeholder ?? 'YYYY-MM-DD' }}\"\n       @if($autofocus ?? false) autofocus @endif\n       @if($disabled ?? false) disabled=\"disabled\" @endif\n       @if(isset($model) || old($name)) value=\"{{ old($name) ?? $model->$name->format('Y-m-d') ?? ''}}\" @endif>\n@if($errors->has($name))\n    <div class=\"text-neg text-small\">{{ $errors->first($name) }}</div>\n@endif\n"
  },
  {
    "path": "resources/views/form/description-html-input.blade.php",
    "content": "<textarea component=\"wysiwyg-input\"\n          option:wysiwyg-input:text-direction=\"{{ $locale->htmlDirection() }}\"\n          id=\"description_html\" name=\"description_html\" rows=\"5\"\n          @if($errors->has('description_html')) class=\"text-neg\" @endif>@if(isset($model) || old('description_html')){{ old('description_html') ?? $model->descriptionInfo()->getHtml() }}@else{{ '<p></p>' }}@endif</textarea>\n@if($errors->has('description_html'))\n    <div class=\"text-neg text-small\">{{ $errors->first('description_html') }}</div>\n@endif"
  },
  {
    "path": "resources/views/form/editor-translations.blade.php",
    "content": "@php\n    $en = trans('editor', [], 'en');\n    $lang = trans('editor');\n    $mergedText = [];\n    foreach ($en as $key => $value) {\n      $mergedText[$value] = $lang[$key] ?? $value;\n    }\n@endphp\n<script nonce=\"{{ $cspNonce }}\">\n    window.editor_translations = @json($mergedText);\n</script>"
  },
  {
    "path": "resources/views/form/entity-permissions-row.blade.php",
    "content": "{{--\n$role - The Role to display this row for.\n$entityType - String identifier for type of entity having permissions applied.\n$permission - The entity permission containing the permissions.\n$inheriting - Boolean if the current row should be marked as inheriting default permissions. Used for \"Everyone Else\" role.\n--}}\n\n<div component=\"permissions-table\" class=\"item-list-row flex-container-row justify-space-between wrap\">\n    <div class=\"gap-x-m flex-container-row items-center px-l py-m flex\">\n        <div class=\"text-large\" title=\"{{ $role->id === 0 ? trans('entities.permissions_role_everyone_else') : trans('common.role') }}\">\n            @icon($role->id === 0 ? 'groups' : 'role')\n        </div>\n        <span>\n            <strong>{{ $role->display_name }}</strong> <br>\n            <small class=\"text-muted\">{{ $role->description }}</small>\n        </span>\n        @if($role->id !== 0)\n            <button type=\"button\"\n                class=\"ml-auto flex-none text-small text-link text-button hover-underline item-list-row-toggle-all hide-under-s\"\n                refs=\"permissions-table@toggle-all\"\n                ><strong>{{ trans('common.toggle_all') }}</strong></button>\n        @endif\n    </div>\n    @if($role->id === 0)\n        <div class=\"px-l flex-container-row items-center\" refs=\"entity-permissions@everyone-inherit\">\n            @include('form.custom-checkbox', [\n                'name' => 'entity-permissions-inherit',\n                'label' => trans('entities.permissions_inherit_defaults'),\n                'value' => 'true',\n                'checked' => $inheriting\n            ])\n        </div>\n    @endif\n    <div class=\"flex-container-row justify-space-between gap-x-xl wrap items-center\">\n        <input type=\"hidden\" name=\"permissions[{{ $role->id }}][active]\"\n               @if($inheriting) disabled=\"disabled\" @endif\n               value=\"true\">\n        <div class=\"px-l\">\n            @include('form.custom-checkbox', [\n                'name' =>  'permissions[' . $role->id . '][view]',\n                'label' => trans('common.view'),\n                'value' => 'true',\n                'checked' => $permission->view,\n                'disabled' => $inheriting\n            ])\n        </div>\n        @if($entityType !== 'page')\n            <div class=\"px-l\">\n                @include('form.custom-checkbox', [\n                    'name' =>  'permissions[' . $role->id . '][create]',\n                    'label' => trans('common.create') . ($entityType === 'bookshelf' ? ' *'  : ''),\n                    'value' => 'true',\n                    'checked' => $permission->create,\n                    'disabled' => $inheriting\n                ])\n            </div>\n        @endif\n        <div class=\"px-l\">\n            @include('form.custom-checkbox', [\n                'name' =>  'permissions[' . $role->id . '][update]',\n                'label' => trans('common.update'),\n                'value' => 'true',\n                'checked' => $permission->update,\n                'disabled' => $inheriting\n            ])\n        </div>\n        <div class=\"px-l\">\n            @include('form.custom-checkbox', [\n                'name' =>  'permissions[' . $role->id . '][delete]',\n                'label' => trans('common.delete'),\n                'value' => 'true',\n                'checked' => $permission->delete,\n                'disabled' => $inheriting\n            ])\n        </div>\n    </div>\n    @if($role->id !== 0)\n        <div class=\"flex-container-row items-center px-m py-s\">\n            <button type=\"button\"\n                    class=\"text-neg p-m icon-button\"\n                    data-role-id=\"{{ $role->id }}\"\n                    data-role-name=\"{{ $role->display_name }}\"\n                    title=\"{{ trans('common.remove') }}\">\n                @icon('close') <span class=\"hide-over-m ml-xs\">{{ trans('common.remove') }}</span>\n            </button>\n        </div>\n    @endif\n</div>"
  },
  {
    "path": "resources/views/form/entity-permissions.blade.php",
    "content": "<?php\n/** @var \\BookStack\\Permissions\\PermissionFormData $data */\n?>\n<form component=\"entity-permissions\"\n      option:entity-permissions:entity-type=\"{{ $model->getType() }}\"\n      action=\"{{ $model->getUrl('/permissions') }}\"\n      method=\"POST\">\n    {!! csrf_field() !!}\n    <input type=\"hidden\" name=\"_method\" value=\"PUT\">\n\n    <div class=\"grid half left-focus v-end gap-m wrap\">\n        <div>\n            <h1 class=\"list-heading\">{{ $title }}</h1>\n            <p class=\"text-muted mb-s\">\n                {{ trans('entities.permissions_desc') }}\n\n                @if($model instanceof \\BookStack\\Entities\\Models\\Book)\n                    <br> {{ trans('entities.permissions_book_cascade') }}\n                @elseif($model instanceof \\BookStack\\Entities\\Models\\Chapter)\n                    <br> {{ trans('entities.permissions_chapter_cascade') }}\n                @endif\n            </p>\n\n            @if($model instanceof \\BookStack\\Entities\\Models\\Bookshelf)\n                <p class=\"text-warn\">{{ trans('entities.shelves_permissions_cascade_warning') }}</p>\n            @endif\n        </div>\n        <div class=\"flex-container-row justify-flex-end\">\n            <div class=\"form-group mb-m\">\n                <label for=\"owner\">{{ trans('entities.permissions_owner') }}</label>\n                @include('form.user-select', ['user' => $model->ownedBy, 'name' => 'owned_by'])\n            </div>\n        </div>\n    </div>\n\n    <hr>\n\n    <div refs=\"entity-permissions@role-container\" class=\"item-list mt-m mb-m\">\n        @foreach($data->permissionsWithRoles() as $permission)\n            @include('form.entity-permissions-row', [\n                'permission' => $permission,\n                'role' => $permission->role,\n                'entityType' => $model->getType(),\n                'inheriting' => false,\n            ])\n        @endforeach\n    </div>\n\n    <div class=\"flex-container-row justify-flex-end mb-xl\">\n        <div class=\"flex-container-row items-center gap-m\">\n            <label for=\"role_select\" class=\"m-none p-none\"><span\n                        class=\"bold\">{{ trans('entities.permissions_role_override') }}</span></label>\n            <select name=\"role_select\" id=\"role_select\" refs=\"entity-permissions@role-select\">\n                <option value=\"\">{{ trans('common.select') }}</option>\n                @foreach($data->rolesNotAssigned() as $role)\n                    <option value=\"{{ $role->id }}\">{{ $role->display_name }}</option>\n                @endforeach\n            </select>\n        </div>\n    </div>\n\n    <div class=\"item-list mt-m mb-xl\">\n        @include('form.entity-permissions-row', [\n                'role' => $data->everyoneElseRole(),\n                'permission' => $data->everyoneElseEntityPermission(),\n                'entityType' => $model->getType(),\n                'inheriting' => !$model->permissions()->where('role_id', '=', 0)->exists(),\n            ])\n    </div>\n\n    <hr class=\"mb-m\">\n\n    <div class=\"flex-container-row justify-space-between gap-m wrap\">\n        <div class=\"flex min-width-m\">\n            @if($model instanceof \\BookStack\\Entities\\Models\\Bookshelf)\n                <p class=\"small text-muted mb-none\">\n                    * {{ trans('entities.shelves_permissions_create') }}\n                </p>\n            @endif\n        </div>\n        <div class=\"text-right\">\n            <a href=\"{{ $model->getUrl() }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n            <button type=\"submit\" class=\"button\">{{ trans('entities.permissions_save') }}</button>\n        </div>\n    </div>\n</form>"
  },
  {
    "path": "resources/views/form/errors.blade.php",
    "content": "{{--\n$name - string\n--}}\n@if($errors->has($name))\n    <div class=\"text-neg text-small\">{{ $errors->first($name) }}</div>\n@endif"
  },
  {
    "path": "resources/views/form/image-picker.blade.php",
    "content": "<div component=\"image-picker\"\n     option:image-picker:default-image=\"{{ $defaultImage }}\"\n     class=\"image-picker @if($errors->has($name)) has-error @endif\">\n\n    <div class=\"grid half\">\n        <div class=\"text-center\">\n            <img refs=\"image-picker@image\"\n                @if($currentImage && $currentImage !== 'none') src=\"{{$currentImage}}\" @else src=\"{{$defaultImage}}\" @endif\n                class=\"{{$imageClass}} @if($currentImage=== 'none') none @endif\" alt=\"{{ trans('components.image_preview') }}\">\n        </div>\n        <div class=\"text-center\">\n            <input refs=\"image-picker@image-input\" type=\"file\" class=\"custom-file-input\" accept=\"image/*\" name=\"{{ $name }}\" id=\"{{ $name }}\">\n            <label for=\"{{ $name }}\" class=\"button outline\">{{ trans('components.image_select_image') }}</label>\n            <input refs=\"image-picker@reset-input\" type=\"hidden\" name=\"{{ $name }}_reset\" value=\"true\" disabled=\"disabled\">\n            @if(isset($removeName))\n                <input refs=\"image-picker@remove-input\" type=\"hidden\" name=\"{{ $removeName }}\" value=\"{{ $removeValue }}\" disabled=\"disabled\">\n            @endif\n\n            <br>\n            <button refs=\"image-picker@reset-button\" class=\"text-button text-muted\" type=\"button\">{{ trans('common.reset') }}</button>\n\n            @if(isset($removeName))\n                <span class=\"sep\">|</span>\n                <button refs=\"image-picker@remove-button\" class=\"text-button text-muted\" type=\"button\">{{ trans('common.remove') }}</button>\n            @endif\n        </div>\n    </div>\n\n    @if($errors->has($name))\n        <div class=\"text-neg text-small\">{{ $errors->first($name) }}</div>\n    @endif\n\n</div>"
  },
  {
    "path": "resources/views/form/number.blade.php",
    "content": "<input type=\"number\" id=\"{{ $name }}\" name=\"{{ $name }}\"\n       @if($errors->has($name)) class=\"text-neg\" @endif\n       @if(isset($placeholder)) placeholder=\"{{$placeholder}}\" @endif\n       @if($autofocus ?? false) autofocus @endif\n       @if($disabled ?? false) disabled=\"disabled\" @endif\n       @if($readonly ?? false) readonly=\"readonly\" @endif\n       @if($min ?? false) min=\"{{ $min }}\" @endif\n       @if($max ?? false) max=\"{{ $max }}\" @endif\n       @if($step ?? false) step=\"{{ $step }}\" @endif\n       @if(isset($model) || old($name) || isset($value)) value=\"{{ old($name) ?? $model->$name ?? $value }}\" @endif>\n@if($errors->has($name))\n    <div class=\"text-neg text-small\">{{ $errors->first($name) }}</div>\n@endif\n"
  },
  {
    "path": "resources/views/form/page-picker.blade.php",
    "content": "\n{{--Depends on entity selector popup--}}\n<div component=\"page-picker\"\n     option:page-picker:selector-endpoint=\"{{ $selectorEndpoint }}\">\n    <div class=\"input-base overflow-hidden height-auto\">\n        <span @if($value) hidden @endif refs=\"page-picker@default-display\" class=\"text-muted italic\">{{ $placeholder }}</span>\n        <a @if(!$value) hidden @endif href=\"{{ url('/link/' . $value) }}\" target=\"_blank\" rel=\"noopener\" class=\"text-page\" refs=\"page-picker@display\">#{{$value}}, {{$value ? \\BookStack\\Entities\\Models\\Page::query()->visible()->find($value)->name ?? '' : '' }}</a>\n    </div>\n    <br>\n    <input refs=\"page-picker@input\" type=\"hidden\" value=\"{{$value}}\" name=\"{{$name}}\" id=\"{{$name}}\">\n    <button @if(!$value) hidden @endif type=\"button\" refs=\"page-picker@reset-button\" class=\"text-button\">{{ trans('common.reset') }}</button>\n    <span refs=\"page-picker@button-seperator\" @if(!$value) hidden @endif class=\"sep\">|</span>\n    <button type=\"button\" refs=\"page-picker@select-button\" class=\"text-button\">{{ trans('common.select') }}</button>\n</div>"
  },
  {
    "path": "resources/views/form/password.blade.php",
    "content": "<input type=\"password\" id=\"{{ $name }}\" name=\"{{ $name }}\"\n       @if($errors->has($name)) class=\"text-neg\" @endif\n       @if(isset($placeholder)) placeholder=\"{{$placeholder}}\" @endif\n       @if(isset($autocomplete)) autocomplete=\"{{$autocomplete}}\" @endif\n       @if(old($name)) value=\"{{ old($name)}}\" @endif>\n@if($errors->has($name))\n    <div class=\"text-neg text-small\">{{ $errors->first($name) }}</div>\n@endif\n"
  },
  {
    "path": "resources/views/form/request-query-inputs.blade.php",
    "content": "{{--\n$params - The query paramters to convert to inputs.\n--}}\n@foreach(array_intersect_key(request()->query(), array_flip($params)) as $name => $value)\n    @if ($value)\n    <input type=\"hidden\" name=\"{{ $name }}\" value=\"{{ $value }}\">\n    @endif\n@endforeach"
  },
  {
    "path": "resources/views/form/role-checkboxes.blade.php",
    "content": "\n<div class=\"toggle-switch-list dual-column-content\">\n    <input type=\"hidden\" name=\"{{ $name }}[0]\" value=\"0\">\n    @foreach($roles as $role)\n        <div>\n            @include('form.custom-checkbox', [\n                'name' => $name . '[' . strval($role->id) . ']',\n                'label' => $role->display_name,\n                'value' => $role->id,\n                'checked' => old($name . '.' . strval($role->id)) || (!old('name') && isset($model) && $model->hasRole($role->id))\n            ])\n        </div>\n    @endforeach\n</div>\n\n@if($errors->has($name))\n    <div class=\"text-neg text-small\">{{ $errors->first($name) }}</div>\n@endif"
  },
  {
    "path": "resources/views/form/role-select.blade.php",
    "content": "\n<select id=\"{{ $name }}\" name=\"{{ $name }}\">\n    @foreach($options as $option)\n        <option value=\"{{$option->id}}\"\n                @if($errors->has($name)) class=\"text-neg\" @endif\n                @if(isset($model) || old($name)) @if(old($name) && old($name) === $option->id) selected @elseif(isset($model) && $model->role->id === $option->id) selected @endif @endif\n                >\n            {{ $option->display_name }}\n        </option>\n    @endforeach\n</select>\n\n@if($errors->has($name))\n    <div class=\"text-neg text-small\">{{ $errors->first($name) }}</div>\n@endif"
  },
  {
    "path": "resources/views/form/simple-dropzone.blade.php",
    "content": "{{--\n@url - URL to upload to.\n@placeholder - Placeholder text\n@successMessage\n--}}\n<div component=\"dropzone\"\n     option:dropzone:url=\"{{ $url }}\"\n     option:dropzone:success-message=\"{{ $successMessage }}\"\n     option:dropzone:error-message=\"{{ trans('errors.attachment_upload_error') }}\"\n     option:dropzone:upload-limit=\"{{ config('app.upload_limit') }}\"\n     option:dropzone:upload-limit-message=\"{{ trans('errors.server_upload_limit') }}\"\n     option:dropzone:zone-text=\"{{ trans('entities.attachments_dropzone') }}\"\n     option:dropzone:file-accept=\"*\"\n     class=\"relative\">\n    <div refs=\"dropzone@status-area\"></div>\n    <button type=\"button\"\n            refs=\"dropzone@select-button dropzone@drop-target\"\n            class=\"dropzone-landing-area text-center\">\n        {{ $placeholder }}\n    </button>\n</div>"
  },
  {
    "path": "resources/views/form/text.blade.php",
    "content": "<input type=\"text\" id=\"{{ $name }}\" name=\"{{ $name }}\"\n       @if($errors->has($name)) class=\"text-neg\" @endif\n       @if(isset($placeholder)) placeholder=\"{{$placeholder}}\" @endif\n       @if($autofocus ?? false) autofocus @endif\n       @if($disabled ?? false) disabled=\"disabled\" @endif\n       @if($readonly ?? false) readonly=\"readonly\" @endif\n       @if(isset($model) || old($name)) value=\"{{ old($name) ? old($name) : $model->$name}}\" @endif>\n@if($errors->has($name))\n    <div class=\"text-neg text-small\">{{ $errors->first($name) }}</div>\n@endif\n"
  },
  {
    "path": "resources/views/form/textarea.blade.php",
    "content": "<textarea id=\"{{ $name }}\" name=\"{{ $name }}\" rows=\"5\"\n          @if($errors->has($name)) class=\"text-neg\" @endif>@if(isset($model) || old($name)){{ old($name) ? old($name) : $model->$name}}@endif</textarea>\n@if($errors->has($name))\n    <div class=\"text-neg text-small\">{{ $errors->first($name) }}</div>\n@endif"
  },
  {
    "path": "resources/views/form/toggle-switch.blade.php",
    "content": "<label components=\"custom-checkbox toggle-switch\" class=\"toggle-switch\">\n    <input type=\"hidden\" name=\"{{$name}}\" value=\"{{$value?'true':'false'}}\"/>\n    <input type=\"checkbox\" @if($value) checked=\"checked\" @endif>\n    <span tabindex=\"0\" role=\"checkbox\"\n          aria-checked=\"{{ $value ? 'true' : 'false' }}\"\n          class=\"custom-checkbox text-primary\">@icon('check')</span>\n    <span class=\"label\">{{ $label }}</span>\n</label>"
  },
  {
    "path": "resources/views/form/user-mention-list.blade.php",
    "content": "@if($users->isEmpty())\n    <div class=\"flex-container-row items-center dropdown-search-item dropdown-search-item text-muted mt-m\">\n        <span>{{ trans('common.no_items') }}</span>\n    </div>\n@endif\n@foreach($users as $user)\n<a href=\"{{ $user->getProfileUrl() }}\" class=\"flex-container-row items-center dropdown-search-item\"\n   data-id=\"{{ $user->id }}\"\n   data-name=\"{{ $user->name }}\"\n   data-slug=\"{{ $user->slug }}\">\n    <img class=\"avatar mr-m\" src=\"{{ $user->getAvatar(30) }}\" alt=\"{{ $user->name }}\">\n    <span>{{ $user->name }}</span>\n</a>\n@endforeach"
  },
  {
    "path": "resources/views/form/user-select-list.blade.php",
    "content": "<a href=\"#\" class=\"flex-container-row items-center dropdown-search-item\" data-id=\"\">\n    <span>{{ trans('settings.users_none_selected') }}</span>\n</a>\n@foreach($users as $user)\n    <a href=\"#\" class=\"flex-container-row items-center dropdown-search-item\" data-id=\"{{ $user->id }}\">\n        <img class=\"avatar mr-m\" src=\"{{ $user->getAvatar(30) }}\" alt=\"{{ $user->name }}\">\n        <span>{{ $user->name }}</span>\n    </a>\n@endforeach"
  },
  {
    "path": "resources/views/form/user-select.blade.php",
    "content": "<div class=\"dropdown-search\" components=\"dropdown dropdown-search user-select\"\n     option:dropdown-search:url=\"/search/users/select\"\n>\n    <input refs=\"user-select@input\" type=\"hidden\" name=\"{{ $name }}\" value=\"{{ $user->id ?? '' }}\">\n    <div refs=\"dropdown@toggle\"\n         class=\"dropdown-search-toggle-select  input-base\"\n         aria-haspopup=\"true\" aria-expanded=\"false\" tabindex=\"0\">\n        <div refs=\"user-select@user-info\" class=\"dropdown-search-toggle-select-label flex-container-row items-center\">\n            @if($user)\n                <img class=\"avatar small mr-m\" src=\"{{ $user->getAvatar(30) }}\" width=\"30\" height=\"30\" alt=\"{{ $user->name }}\">\n                <span>{{ $user->name }}</span>\n            @else\n                <span>{{ trans('settings.users_none_selected') }}</span>\n            @endif\n        </div>\n        <span class=\"dropdown-search-toggle-select-caret\">\n            @icon('caret-down')\n        </span>\n    </div>\n    <div refs=\"dropdown@menu\" class=\"dropdown-search-dropdown card\" role=\"menu\">\n        <div class=\"dropdown-search-search\">\n            @icon('search')\n            <input refs=\"dropdown-search@searchInput\"\n                   aria-label=\"{{ trans('common.search') }}\"\n                   autocomplete=\"off\"\n                   placeholder=\"{{ trans('common.search') }}\"\n                   type=\"text\">\n        </div>\n        <div refs=\"dropdown-search@loading\" class=\"text-center\">\n            @include('common.loading-icon')\n        </div>\n        <div refs=\"dropdown-search@listContainer\" class=\"dropdown-search-list\"></div>\n    </div>\n</div>"
  },
  {
    "path": "resources/views/help/licenses.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        <div class=\"my-l\">&nbsp;</div>\n\n        <div class=\"card content-wrap auto-height\">\n\n            <h1 class=\"list-heading\">{{ trans('settings.licenses') }}</h1>\n            <p>{{ trans('settings.licenses_desc') }}</p>\n\n            <ul>\n                <li><a href=\"#bookstack-license\">{{ trans('settings.licenses_bookstack') }}</a></li>\n                <li><a href=\"#php-lib-licenses\">{{ trans('settings.licenses_php') }}</a></li>\n                <li><a href=\"#js-lib-licenses\">{{ trans('settings.licenses_js') }}</a></li>\n                <li><a href=\"#other-licenses\">{{ trans('settings.licenses_other') }}</a></li>\n            </ul>\n        </div>\n\n        <div id=\"bookstack-license\" class=\"card content-wrap auto-height\">\n            <h3 class=\"list-heading\">{{ trans('settings.licenses_bookstack') }}</h3>\n            <div style=\"white-space: pre-wrap;\" class=\"mb-m\">{{ $license }}</div>\n            <p>BookStack® is a UK registered trade mark of Daniel Brown. </p>\n        </div>\n\n        <div id=\"php-lib-licenses\" class=\"card content-wrap auto-height\">\n            <h3 class=\"list-heading\">{{ trans('settings.licenses_php') }}</h3>\n            <div style=\"white-space: pre-wrap;\">{{ $phpLibData }}</div>\n        </div>\n\n        <div id=\"js-lib-licenses\" class=\"card content-wrap auto-height\">\n            <h3 class=\"list-heading\">{{ trans('settings.licenses_js') }}</h3>\n            <div style=\"white-space: pre-wrap;\">{{ $jsLibData }}</div>\n        </div>\n\n        <div id=\"other-licenses\" class=\"card content-wrap auto-height\">\n            <h3 class=\"list-heading\">{{ trans('settings.licenses_other') }}</h3>\n            <div style=\"white-space: pre-line;\">BookStack makes heavy use of PHP:\n                License: PHP License, version 3.01\n                License File: https://www.php.net/license/3_01.txt\n                Copyright: Copyright (c) 1999 - 2019 The PHP Group. All rights reserved.\n                Link: https://www.php.net/\n                -----------\n                BookStack uses Icons from Google Material Icons:\n                License: Apache License Version 2.0\n                License File: https://github.com/google/material-design-icons/blob/master/LICENSE\n                Copyright: Copyright 2020 Google LLC\n                Link: https://github.com/google/material-design-icons\n                -----------\n                BookStack is distributed with TinyMCE:\n                License: MIT\n                License File: https://github.com/tinymce/tinymce/blob/release/6.7/LICENSE.TXT\n                Copyright: Copyright (c) 2022 Ephox Corporation DBA Tiny Technologies, Inc.\n                Link: https://github.com/tinymce/tinymce\n                -----------\n                BookStack's newer WYSIWYG editor is based upon lexical code:\n                License: MIT\n                License File: https://github.com/facebook/lexical/blob/v0.17.1/LICENSE\n                Copyright: Copyright (c) Meta Platforms, Inc. and affiliates.\n                Link: https://github.com/facebook/lexical\n            </div>\n        </div>\n    </div>\n\n@endsection"
  },
  {
    "path": "resources/views/help/tinymce.blade.php",
    "content": "@extends('layouts.plain')\n@section('document-class', 'bg-white ' .  (setting()->getForCurrentUser('dark-mode-enabled') ? 'dark-mode ' : ''))\n\n@section('content')\n    <div class=\"p-m\">\n\n        <h4 class=\"mt-s\">{{ trans('editor.editor_license') }}</h4>\n        <p>\n            {!! trans('editor.editor_tiny_license', ['tinyLink' => '<a href=\"https://www.tiny.cloud/\" target=\"_blank\" rel=\"noopener noreferrer\">TinyMCE</a>']) !!}\n            <br>\n            <a href=\"{{ url('/libs/tinymce/license.txt') }}\" target=\"_blank\">{{ trans('editor.editor_tiny_license_link') }}</a>\n        </p>\n\n        <h4>{{ trans('editor.shortcuts') }}</h4>\n\n        <p>{{ trans('editor.shortcuts_intro') }}</p>\n        <table>\n            <thead>\n            <tr>\n                <th>{{ trans('editor.shortcut') }} {{ trans('editor.windows_linux') }}</th>\n                <th>{{ trans('editor.shortcut') }} {{ trans('editor.mac') }}</th>\n                <th>{{ trans('editor.description') }}</th>\n            </tr>\n            </thead>\n            <tbody>\n            <tr>\n                <td><code>Ctrl</code>+<code>S</code></td>\n                <td><code>Cmd</code>+<code>S</code></td>\n                <td>{{ trans('entities.pages_edit_save_draft') }}</td>\n            </tr>\n            <tr>\n                <td><code>Ctrl</code>+<code>Enter</code></td>\n                <td><code>Cmd</code>+<code>Enter</code></td>\n                <td>{{ trans('editor.save_continue') }}</td>\n            </tr>\n            <tr>\n                <td><code>Ctrl</code>+<code>B</code></td>\n                <td><code>Cmd</code>+<code>B</code></td>\n                <td>{{ trans('editor.bold') }}</td>\n            </tr>\n            <tr>\n                <td><code>Ctrl</code>+<code>I</code></td>\n                <td><code>Cmd</code>+<code>I</code></td>\n                <td>{{ trans('editor.italic') }}</td>\n            </tr>\n            <tr>\n                <td>\n                    <code>Ctrl</code>+<code>1</code><br>\n                    <code>Ctrl</code>+<code>2</code><br>\n                    <code>Ctrl</code>+<code>3</code><br>\n                    <code>Ctrl</code>+<code>4</code>\n                </td>\n                <td>\n                    <code>Cmd</code>+<code>1</code><br>\n                    <code>Cmd</code>+<code>2</code><br>\n                    <code>Cmd</code>+<code>3</code><br>\n                    <code>Cmd</code>+<code>4</code>\n                </td>\n                <td>\n                    {{ trans('editor.header_large') }} <br>\n                    {{ trans('editor.header_medium') }} <br>\n                    {{ trans('editor.header_small') }} <br>\n                    {{ trans('editor.header_tiny') }}\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    <code>Ctrl</code>+<code>5</code><br>\n                    <code>Ctrl</code>+<code>D</code>\n                </td>\n                <td>\n                    <code>Cmd</code>+<code>5</code><br>\n                    <code>Cmd</code>+<code>D</code>\n                </td>\n                <td>{{ trans('editor.paragraph') }}</td>\n            </tr>\n            <tr>\n                <td>\n                    <code>Ctrl</code>+<code>6</code><br>\n                    <code>Ctrl</code>+<code>Q</code>\n                </td>\n                <td>\n                    <code>Cmd</code>+<code>6</code><br>\n                    <code>Cmd</code>+<code>Q</code>\n                </td>\n                <td>{{ trans('editor.blockquote') }}</td>\n            </tr>\n            <tr>\n                <td>\n                    <code>Ctrl</code>+<code>7</code><br>\n                    <code>Ctrl</code>+<code>E</code>\n                </td>\n                <td>\n                    <code>Cmd</code>+<code>7</code><br>\n                    <code>Cmd</code>+<code>E</code>\n                </td>\n                <td>{{ trans('editor.insert_code_block') }}</td>\n            </tr>\n            <tr>\n                <td>\n                    <code>Ctrl</code>+<code>8</code><br>\n                    <code>Ctrl</code>+<code>Shift</code>+<code>E</code>\n                </td>\n                <td>\n                    <code>Cmd</code>+<code>8</code><br>\n                    <code>Cmd</code>+<code>Shift</code>+<code>E</code>\n                </td>\n                <td>{{ trans('editor.inline_code') }}</td>\n            </tr>\n            <tr>\n                <td><code>Ctrl</code>+<code>9</code></td>\n                <td><code>Cmd</code>+<code>9</code></td>\n                <td>\n                    {{ trans('editor.callouts') }} <br>\n                    <small>{{ trans('editor.callouts_cycle') }}</small>\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    <code>Ctrl</code>+<code>O</code> <br>\n                    <code>Ctrl</code>+<code>P</code>\n                </td>\n                <td>\n                    <code>Cmd</code>+<code>O</code> <br>\n                    <code>Cmd</code>+<code>P</code>\n                </td>\n                <td>\n                    {{ trans('editor.list_numbered') }} <br>\n                    {{ trans('editor.list_bullet') }}\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    <code>Ctrl</code>+<code>Shift</code>+<code>K</code>\n                </td>\n                <td>\n                    <code>Cmd</code>+<code>Shift</code>+<code>K</code>\n                </td>\n                <td>{{ trans('editor.link_selector') }}</td>\n            </tr>\n            </tbody>\n        </table>\n\n    </div>\n@endsection\n\n"
  },
  {
    "path": "resources/views/help/wysiwyg.blade.php",
    "content": "<h4>{{ trans('editor.shortcuts') }}</h4>\n\n<p>{{ trans('editor.shortcuts_intro') }}</p>\n<table>\n    <thead>\n    <tr>\n        <th>{{ trans('editor.shortcut') }} {{ trans('editor.windows_linux') }}</th>\n        <th>{{ trans('editor.shortcut') }} {{ trans('editor.mac') }}</th>\n        <th>{{ trans('editor.description') }}</th>\n    </tr>\n    </thead>\n    <tbody>\n    <tr>\n        <td><code>Ctrl</code>+<code>S</code></td>\n        <td><code>Cmd</code>+<code>S</code></td>\n        <td>{{ trans('entities.pages_edit_save_draft') }}</td>\n    </tr>\n    <tr>\n        <td><code>Ctrl</code>+<code>Enter</code></td>\n        <td><code>Cmd</code>+<code>Enter</code></td>\n        <td>{{ trans('editor.save_continue') }}</td>\n    </tr>\n    <tr>\n        <td><code>Ctrl</code>+<code>B</code></td>\n        <td><code>Cmd</code>+<code>B</code></td>\n        <td>{{ trans('editor.bold') }}</td>\n    </tr>\n    <tr>\n        <td><code>Ctrl</code>+<code>I</code></td>\n        <td><code>Cmd</code>+<code>I</code></td>\n        <td>{{ trans('editor.italic') }}</td>\n    </tr>\n    <tr>\n        <td>\n            <code>Ctrl</code>+<code>1</code><br>\n            <code>Ctrl</code>+<code>2</code><br>\n            <code>Ctrl</code>+<code>3</code><br>\n            <code>Ctrl</code>+<code>4</code>\n        </td>\n        <td>\n            <code>Cmd</code>+<code>1</code><br>\n            <code>Cmd</code>+<code>2</code><br>\n            <code>Cmd</code>+<code>3</code><br>\n            <code>Cmd</code>+<code>4</code>\n        </td>\n        <td>\n            {{ trans('editor.header_large') }} <br>\n            {{ trans('editor.header_medium') }} <br>\n            {{ trans('editor.header_small') }} <br>\n            {{ trans('editor.header_tiny') }}\n        </td>\n    </tr>\n    <tr>\n        <td>\n            <code>Ctrl</code>+<code>5</code><br>\n            <code>Ctrl</code>+<code>D</code>\n        </td>\n        <td>\n            <code>Cmd</code>+<code>5</code><br>\n            <code>Cmd</code>+<code>D</code>\n        </td>\n        <td>{{ trans('editor.paragraph') }}</td>\n    </tr>\n    <tr>\n        <td>\n            <code>Ctrl</code>+<code>6</code><br>\n            <code>Ctrl</code>+<code>Q</code>\n        </td>\n        <td>\n            <code>Cmd</code>+<code>6</code><br>\n            <code>Cmd</code>+<code>Q</code>\n        </td>\n        <td>{{ trans('editor.blockquote') }}</td>\n    </tr>\n    <tr>\n        <td>\n            <code>Ctrl</code>+<code>7</code><br>\n            <code>Ctrl</code>+<code>E</code>\n        </td>\n        <td>\n            <code>Cmd</code>+<code>7</code><br>\n            <code>Cmd</code>+<code>E</code>\n        </td>\n        <td>{{ trans('editor.insert_code_block') }}</td>\n    </tr>\n    <tr>\n        <td>\n            <code>Ctrl</code>+<code>8</code><br>\n            <code>Ctrl</code>+<code>Shift</code>+<code>E</code>\n        </td>\n        <td>\n            <code>Cmd</code>+<code>8</code><br>\n            <code>Cmd</code>+<code>Shift</code>+<code>E</code>\n        </td>\n        <td>{{ trans('editor.inline_code') }}</td>\n    </tr>\n    <tr>\n        <td><code>Ctrl</code>+<code>9</code></td>\n        <td><code>Cmd</code>+<code>9</code></td>\n        <td>\n            {{ trans('editor.callouts') }} <br>\n            <small>{{ trans('editor.callouts_cycle') }}</small>\n        </td>\n    </tr>\n    <tr>\n        <td>\n            <code>Ctrl</code>+<code>O</code> <br>\n            <code>Ctrl</code>+<code>P</code>\n        </td>\n        <td>\n            <code>Cmd</code>+<code>O</code> <br>\n            <code>Cmd</code>+<code>P</code>\n        </td>\n        <td>\n            {{ trans('editor.list_numbered') }} <br>\n            {{ trans('editor.list_bullet') }}\n        </td>\n    </tr>\n    <tr>\n        <td>\n            <code>Ctrl</code>+<code>Shift</code>+<code>K</code>\n        </td>\n        <td>\n            <code>Cmd</code>+<code>Shift</code>+<code>K</code>\n        </td>\n        <td>{{ trans('editor.link_selector') }}</td>\n    </tr>\n    </tbody>\n</table>\n\n<h4 class=\"mt-s\">{{ trans('editor.editor_license') }}</h4>\n<p>\n    {!! trans('editor.editor_lexical_license', ['lexicalLink' => '<a href=\"https://lexical.dev/\" target=\"_blank\" rel=\"noopener noreferrer\">Lexical</a>']) !!}\n    <br>\n    <em class=\"text-muted\">Copyright (c) Meta Platforms, Inc. and affiliates.</em>\n    <br>\n    <a href=\"{{ url('/licenses') }}\" target=\"_blank\">{{ trans('editor.editor_lexical_license_link') }}</a>\n</p>"
  },
  {
    "path": "resources/views/home/books.blade.php",
    "content": "@extends('layouts.tri')\n\n@section('body')\n    @include('books.parts.list', ['books' => $books, 'view' => $view])\n@stop\n\n@section('left')\n    @include('home.parts.sidebar')\n@stop\n\n@section('right')\n    <div class=\"actions mb-xl\">\n        <h5>{{ trans('common.actions') }}</h5>\n        <div class=\"icon-list text-link\">\n            @if(userCan(\\BookStack\\Permissions\\Permission::BookCreateAll))\n                <a href=\"{{ url(\"/create-book\") }}\" class=\"icon-list-item\">\n                    <span>@icon('add')</span>\n                    <span>{{ trans('entities.books_create') }}</span>\n                </a>\n            @endif\n            @include('entities.view-toggle', ['view' => $view, 'type' => 'books'])\n            <a href=\"{{ url('/tags') }}\" class=\"icon-list-item\">\n                <span>@icon('tag')</span>\n                <span>{{ trans('entities.tags_view_tags') }}</span>\n            </a>\n            @include('home.parts.expand-toggle', ['classes' => 'text-link', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])\n            @include('common.dark-mode-toggle', ['classes' => 'icon-list-item text-link'])\n        </div>\n    </div>\n@stop\n"
  },
  {
    "path": "resources/views/home/default.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container px-xl py-s flex-container-row gap-l wrap justify-space-between\">\n        <div class=\"icon-list inline block\">\n            @include('home.parts.expand-toggle', ['classes' => 'text-muted text-link', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])\n        </div>\n        <div>\n            <div class=\"icon-list inline block\">\n                @include('common.dark-mode-toggle', ['classes' => 'text-muted icon-list-item text-link'])\n            </div>\n        </div>\n    </div>\n\n    <div class=\"container\" id=\"home-default\">\n        <div class=\"grid third gap-x-xxl no-row-gap\">\n            <div>\n                @if(count($draftPages) > 0)\n                    <div id=\"recent-drafts\" class=\"card mb-xl\">\n                        <h3 class=\"card-title\">{{ trans('entities.my_recent_drafts') }}</h3>\n                        <div class=\"px-m\">\n                            @include('entities.list', ['entities' => $draftPages, 'style' => 'compact'])\n                        </div>\n                    </div>\n                @endif\n\n                <div id=\"{{ auth()->check() ? 'recently-viewed' : 'recent-books' }}\" class=\"card mb-xl\">\n                    <h3 class=\"card-title\">{{ trans('entities.' . (auth()->check() ? 'my_recently_viewed' : 'books_recent')) }}</h3>\n                    <div class=\"px-m\">\n                        @include('entities.list', [\n                        'entities' => $recents,\n                        'style' => 'compact',\n                        'emptyText' => auth()->check() ? trans('entities.no_pages_viewed') : trans('entities.books_empty')\n                        ])\n                    </div>\n                </div>\n            </div>\n\n            <div>\n                @if(count($favourites) > 0)\n                    <div id=\"top-favourites\" class=\"card mb-xl\">\n                        <h3 class=\"card-title\">{{ trans('entities.my_most_viewed_favourites') }}</h3>\n                        <div class=\"px-m\">\n                            @include('entities.list', [\n                            'entities' => $favourites,\n                            'style' => 'compact',\n                            ])\n                        </div>\n                        <a href=\"{{ url('/favourites')  }}\" class=\"card-footer-link\">{{ trans('common.view_all') }}</a>\n                    </div>\n                @endif\n\n                <div id=\"recent-pages\" class=\"card mb-xl\">\n                    <h3 class=\"card-title\">{{ trans('entities.recently_updated_pages') }}</h3>\n                    <div id=\"recently-updated-pages\" class=\"px-m\">\n                        @include('entities.list', [\n                        'entities' => $recentlyUpdatedPages,\n                        'style' => 'compact',\n                        'emptyText' => trans('entities.no_pages_recently_updated'),\n                        ])\n                    </div>\n                    @if(count($recentlyUpdatedPages) > 0)\n                        <a href=\"{{ url(\"/pages/recently-updated\") }}\" class=\"card-footer-link\">{{ trans('common.view_all') }}</a>\n                    @endif\n                </div>\n            </div>\n\n            <div>\n                <div id=\"recent-activity\" class=\"card mb-xl\">\n                    <h3 class=\"card-title\">{{ trans('entities.recent_activity') }}</h3>\n                    <div class=\"px-m\">\n                        @include('common.activity-list', ['activity' => $activity])\n                    </div>\n                </div>\n            </div>\n\n        </div>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/home/parts/expand-toggle.blade.php",
    "content": "{{--\n$target - CSS selector of items to expand\n$key - Unique key for checking existing stored state.\n--}}\n<?php $isOpen = setting()->getForCurrentUser('section_expansion#'. $key); ?>\n<button component=\"expand-toggle\"\n        option:expand-toggle:target-selector=\"{{ $target }}\"\n        option:expand-toggle:update-endpoint=\"{{ url('/preferences/change-expansion/' . $key) }}\"\n        option:expand-toggle:is-open=\"{{ $isOpen ? 'true' : 'false' }}\"\n        type=\"button\"\n        class=\"icon-list-item {{ $classes ?? '' }}\">\n    <span>@icon('expand-text')</span>\n    <span>{{ trans('common.toggle_details') }}</span>\n</button>\n@if($isOpen)\n    @push('head')\n        <style>\n            {{ $target }} {display: block;}\n        </style>\n    @endpush\n@endif"
  },
  {
    "path": "resources/views/home/parts/sidebar.blade.php",
    "content": "@if(count($draftPages) > 0)\n    <div id=\"recent-drafts\" class=\"mb-xl\">\n        <h5>{{ trans('entities.my_recent_drafts') }}</h5>\n        @include('entities.list', ['entities' => $draftPages, 'style' => 'compact'])\n    </div>\n@endif\n\n@if(count($favourites) > 0)\n    <div id=\"top-favourites\" class=\"mb-xl\">\n        <h5>{{ trans('entities.my_most_viewed_favourites') }}</h5>\n        @include('entities.list', [\n            'entities' => $favourites,\n            'style' => 'compact',\n        ])\n        <a href=\"{{ url('/favourites')  }}\" class=\"text-muted block py-xs\">{{ trans('common.view_all') }}</a>\n    </div>\n@endif\n\n<div class=\"mb-xl\">\n    <h5>{{ trans('entities.' . (auth()->check() ? 'my_recently_viewed' : 'books_recent')) }}</h5>\n    @include('entities.list', [\n        'entities' => $recents,\n        'style' => 'compact',\n        'emptyText' => auth()->check() ? trans('entities.no_pages_viewed') : trans('entities.books_empty')\n        ])\n</div>\n\n<div class=\"mb-xl\">\n    <h5>{{ trans('entities.recently_updated_pages') }}</h5>\n    <div id=\"recently-updated-pages\">\n        @include('entities.list', [\n        'entities' => $recentlyUpdatedPages,\n        'style' => 'compact',\n        'emptyText' => trans('entities.no_pages_recently_updated')\n        ])\n    </div>\n    <a href=\"{{ url('/pages/recently-updated')  }}\" class=\"text-muted block py-xs\">{{ trans('common.view_all') }}</a>\n</div>\n\n<div id=\"recent-activity\" class=\"mb-xl\">\n    <h5>{{ trans('entities.recent_activity') }}</h5>\n    @include('common.activity-list', ['activity' => $activity])\n</div>"
  },
  {
    "path": "resources/views/home/shelves.blade.php",
    "content": "@extends('layouts.tri')\n\n@section('body')\n    @include('shelves.parts.list', ['shelves' => $shelves, 'view' => $view])\n@stop\n\n@section('left')\n    @include('home.parts.sidebar')\n@stop\n\n@section('right')\n    <div class=\"actions mb-xl\">\n        <h5>{{ trans('common.actions') }}</h5>\n        <div class=\"icon-list text-link\">\n            @if(userCan(\\BookStack\\Permissions\\Permission::BookshelfCreateAll))\n                <a href=\"{{ url(\"/create-shelf\") }}\" class=\"icon-list-item\">\n                    <span>@icon('add')</span>\n                    <span>{{ trans('entities.shelves_new_action') }}</span>\n                </a>\n            @endif\n            @include('entities.view-toggle', ['view' => $view, 'type' => 'bookshelves'])\n            <a href=\"{{ url('/tags') }}\" class=\"icon-list-item\">\n                <span>@icon('tag')</span>\n                <span>{{ trans('entities.tags_view_tags') }}</span>\n            </a>\n            @include('home.parts.expand-toggle', ['classes' => 'text-link', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])\n            @include('common.dark-mode-toggle', ['classes' => 'icon-list-item text-link'])\n        </div>\n    </div>\n@stop\n"
  },
  {
    "path": "resources/views/home/specific-page.blade.php",
    "content": "@extends('layouts.tri')\n\n@section('body')\n    <div class=\"mt-m\">\n        <main class=\"content-wrap card\">\n            <div component=\"page-display\"\n                 option:page-display:page-id=\"{{ $customHomepage->id }}\"\n                 class=\"page-content\">\n                @include('pages.parts.page-display', ['page' => $customHomepage])\n            </div>\n        </main>\n    </div>\n@stop\n\n@section('left')\n    @include('home.parts.sidebar')\n@stop\n\n@section('right')\n    <div class=\"actions mb-xl\">\n        <h5>{{ trans('common.actions') }}</h5>\n        <div class=\"icon-list text-link\">\n            @include('home.parts.expand-toggle', ['classes' => 'text-link', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])\n            @include('common.dark-mode-toggle', ['classes' => 'icon-list-item text-link'])\n        </div>\n    </div>\n@stop"
  },
  {
    "path": "resources/views/layouts/base.blade.php",
    "content": "<!DOCTYPE html>\n<html lang=\"{{ isset($locale) ? $locale->htmlLang() : config('app.default_locale') }}\"\n      dir=\"{{ isset($locale) ? $locale->htmlDirection() : 'auto' }}\"\n      class=\"{{ setting()->getForCurrentUser('dark-mode-enabled') ? 'dark-mode ' : '' }}\">\n<head>\n    <title>{{ isset($pageTitle) ? $pageTitle . ' | ' : '' }}{{ setting('app-name') }}</title>\n\n    <!-- Meta -->\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width\">\n    <meta name=\"token\" content=\"{{ csrf_token() }}\">\n    <meta name=\"base-url\" content=\"{{ url('/') }}\">\n    <meta name=\"theme-color\" content=\"{{(setting()->getForCurrentUser('dark-mode-enabled') ? setting('app-color-dark') : setting('app-color'))}}\"/>\n\n    <!-- Social Cards Meta -->\n    <meta property=\"og:title\" content=\"{{ isset($pageTitle) ? $pageTitle . ' | ' : '' }}{{ setting('app-name') }}\">\n    <meta property=\"og:url\" content=\"{{ url()->current() }}\">\n    @stack('social-meta')\n\n    <!-- Styles -->\n    <link rel=\"stylesheet\" href=\"{{ versioned_asset('dist/styles.css') }}\">\n\n    <!-- Icons -->\n    <link rel=\"icon\" type=\"image/png\" sizes=\"256x256\" href=\"{{ setting('app-icon') ?: url('/icon.png') }}\">\n    <link rel=\"icon\" type=\"image/png\" sizes=\"180x180\" href=\"{{ setting('app-icon-180') ?: url('/icon-180.png') }}\">\n    <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"{{ setting('app-icon-180') ?: url('/icon-180.png') }}\">\n    <link rel=\"icon\" type=\"image/png\" sizes=\"128x128\" href=\"{{ setting('app-icon-128') ?: url('/icon-128.png') }}\">\n    <link rel=\"icon\" type=\"image/png\" sizes=\"64x64\" href=\"{{ setting('app-icon-64') ?: url('/icon-64.png') }}\">\n    <link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"{{ setting('app-icon-32') ?: url('/icon-32.png') }}\">\n\n    <!-- PWA -->\n    <link rel=\"manifest\" href=\"{{ url('/manifest.json') }}\" crossorigin=\"use-credentials\">\n    <meta name=\"mobile-web-app-capable\" content=\"yes\">\n\n    <!-- OpenSearch -->\n    <link rel=\"search\" type=\"application/opensearchdescription+xml\" title=\"{{ setting('app-name') }}\" href=\"{{ url('/opensearch.xml') }}\">\n\n    <!-- Custom Styles & Head Content -->\n    @include('layouts.parts.custom-styles')\n    @include('layouts.parts.custom-head')\n\n    @stack('head')\n\n    <!-- Translations for JS -->\n    @stack('translations')\n</head>\n<body\n    @if(setting()->getForCurrentUser('ui-shortcuts-enabled', false))\n        component=\"shortcuts\"\n        option:shortcuts:key-map=\"{{ \\BookStack\\Settings\\UserShortcutMap::fromUserPreferences()->toJson() }}\"\n    @endif\n      class=\"@stack('body-class')\">\n\n    @include('layouts.parts.base-body-start')\n    @include('layouts.parts.skip-to-content')\n    @include('layouts.parts.notifications')\n    @include('layouts.parts.header')\n\n    <div id=\"content\" components=\"@yield('content-components')\" class=\"block\">\n        @yield('content')\n    </div>\n\n    @include('layouts.parts.footer')\n\n    <div component=\"back-to-top\" class=\"back-to-top print-hidden\">\n        <div class=\"inner\">\n            @icon('chevron-up') <span>{{ trans('common.back_to_top') }}</span>\n        </div>\n    </div>\n\n    @if($cspNonce ?? false)\n        <script src=\"{{ versioned_asset('dist/app.js') }}\" type=\"module\" nonce=\"{{ $cspNonce }}\"></script>\n    @endif\n    @stack('body-end')\n\n    @include('layouts.parts.base-body-end')\n</body>\n</html>\n"
  },
  {
    "path": "resources/views/layouts/export.blade.php",
    "content": "<!doctype html>\n<html lang=\"{{ $locale->htmlLang() }}\">\n<head>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n    <title>@yield('title')</title>\n\n    @if($cspContent ?? false)\n        <meta http-equiv=\"Content-Security-Policy\" content=\"{{ $cspContent }}\">\n    @endif\n\n    @include('exports.parts.styles', ['format' => $format, 'engine' => $engine ?? ''])\n    @include('exports.parts.custom-head')\n</head>\n<body class=\"export export-format-{{ $format }} export-engine-{{ $engine ?? 'none' }}\">\n@include('layouts.parts.export-body-start')\n<div class=\"page-content\" dir=\"auto\">\n    @yield('content')\n</div>\n@include('layouts.parts.export-body-end')\n</body>\n</html>"
  },
  {
    "path": "resources/views/layouts/parts/base-body-end.blade.php",
    "content": "{{-- This is a placeholder template file provided as a --}}\n{{-- convenience to users of the visual theme system. --}}"
  },
  {
    "path": "resources/views/layouts/parts/base-body-start.blade.php",
    "content": "{{-- This is a placeholder template file provided as a --}}\n{{-- convenience to users of the visual theme system. --}}"
  },
  {
    "path": "resources/views/layouts/parts/custom-head.blade.php",
    "content": "@inject('headContent', 'BookStack\\Theming\\CustomHtmlHeadContentProvider')\n\n@if(!request()->routeIs('settings.category'))\n<!-- Start: custom user content -->\n{!! $headContent->forWeb() !!}\n<!-- End: custom user content -->\n@endif"
  },
  {
    "path": "resources/views/layouts/parts/custom-styles.blade.php",
    "content": "<style>\n    :root {\n        --color-primary: {{ setting('app-color') }};\n        --color-primary-light: {{ setting('app-color-light') }};\n        --color-link: {{ setting('link-color') }};\n        --color-bookshelf: {{ setting('bookshelf-color') }};\n        --color-book: {{ setting('book-color') }};\n        --color-chapter: {{ setting('chapter-color') }};\n        --color-page: {{ setting('page-color') }};\n        --color-page-draft: {{ setting('page-draft-color') }};\n    }\n    :root.dark-mode {\n        --color-primary: {{ setting('app-color-dark') }};\n        --color-primary-light: {{ setting('app-color-light-dark') }};\n        --color-link: {{ setting('link-color-dark') }};\n        --color-bookshelf: {{ setting('bookshelf-color-dark') }};\n        --color-book: {{ setting('book-color-dark') }};\n        --color-chapter: {{ setting('chapter-color-dark') }};\n        --color-page: {{ setting('page-color-dark') }};\n        --color-page-draft: {{ setting('page-draft-color-dark') }};\n    }\n</style>\n"
  },
  {
    "path": "resources/views/layouts/parts/export-body-end.blade.php",
    "content": "{{-- This is a placeholder template file provided as a --}}\n{{-- convenience to users of the visual theme system. --}}"
  },
  {
    "path": "resources/views/layouts/parts/export-body-start.blade.php",
    "content": "{{-- This is a placeholder template file provided as a --}}\n{{-- convenience to users of the visual theme system. --}}"
  },
  {
    "path": "resources/views/layouts/parts/footer.blade.php",
    "content": "@if(count(setting('app-footer-links', [])) > 0)\n<footer class=\"print-hidden\">\n    @foreach(setting('app-footer-links', []) as $link)\n        <a href=\"{{ $link['url'] }}\" target=\"_blank\" rel=\"noopener\">{{ strpos($link['label'], 'trans::') === 0 ? trans(str_replace('trans::', '', $link['label'])) : $link['label'] }}</a>\n    @endforeach\n</footer>\n@endif"
  },
  {
    "path": "resources/views/layouts/parts/header-links-start.blade.php",
    "content": "{{-- This is a placeholder template file provided as a --}}\n{{-- convenience to users of the visual theme system. --}}"
  },
  {
    "path": "resources/views/layouts/parts/header-links.blade.php",
    "content": "@include('layouts.parts.header-links-start')\n\n@if (user()->hasAppAccess())\n    <a class=\"hide-over-l\" href=\"{{ url('/search') }}\">@icon('search'){{ trans('common.search') }}</a>\n    @if(userCanOnAny(\\BookStack\\Permissions\\Permission::View, \\BookStack\\Entities\\Models\\Bookshelf::class) || userCan(\\BookStack\\Permissions\\Permission::BookshelfViewAll) || userCan(\\BookStack\\Permissions\\Permission::BookshelfViewOwn))\n        <a href=\"{{ url('/shelves') }}\"\n           data-shortcut=\"shelves_view\">@icon('bookshelf'){{ trans('entities.shelves') }}</a>\n    @endif\n    <a href=\"{{ url('/books') }}\" data-shortcut=\"books_view\">@icon('books'){{ trans('entities.books') }}</a>\n    @if(!user()->isGuest() && userCan(\\BookStack\\Permissions\\Permission::SettingsManage))\n        <a href=\"{{ url('/settings') }}\"\n           data-shortcut=\"settings_view\">@icon('settings'){{ trans('settings.settings') }}</a>\n    @endif\n    @if(!user()->isGuest() && userCan(\\BookStack\\Permissions\\Permission::UsersManage) && !userCan(\\BookStack\\Permissions\\Permission::SettingsManage))\n        <a href=\"{{ url('/settings/users') }}\"\n           data-shortcut=\"settings_view\">@icon('users'){{ trans('settings.users') }}</a>\n    @endif\n@endif\n\n@if(user()->isGuest())\n    @if(setting('registration-enabled') && config('auth.method') === 'standard')\n        <a href=\"{{ url('/register') }}\">@icon('new-user'){{ trans('auth.sign_up') }}</a>\n    @endif\n    <a href=\"{{ url('/login')  }}\">@icon('login'){{ trans('auth.log_in') }}</a>\n@endif"
  },
  {
    "path": "resources/views/layouts/parts/header-logo.blade.php",
    "content": "<a href=\"{{ url('/') }}\" data-shortcut=\"home_view\" class=\"logo\">\n    @if(setting('app-logo', '') !== 'none')\n        <img class=\"logo-image\" src=\"{{ setting('app-logo', '') === '' ? url('/logo.png') : url(setting('app-logo', '')) }}\" alt=\"Logo\">\n    @endif\n    @if (setting('app-name-header'))\n        <span class=\"logo-text\">{{ setting('app-name') }}</span>\n    @endif\n</a>"
  },
  {
    "path": "resources/views/layouts/parts/header-search.blade.php",
    "content": "<form component=\"global-search\" action=\"{{ url('/search') }}\" method=\"GET\" class=\"search-box\" role=\"search\" tabindex=\"0\">\n    <button id=\"header-search-box-button\"\n            refs=\"global-search@button\"\n            type=\"submit\"\n            aria-label=\"{{ trans('common.search') }}\"\n            tabindex=\"-1\">@icon('search')</button>\n    <input id=\"header-search-box-input\"\n           refs=\"global-search@input\"\n           type=\"text\"\n           name=\"term\"\n           data-shortcut=\"global_search\"\n           autocomplete=\"off\"\n           aria-label=\"{{ trans('common.search') }}\" placeholder=\"{{ trans('common.search') }}\"\n           value=\"{{ $searchTerm ?? '' }}\">\n    <div refs=\"global-search@suggestions\" class=\"global-search-suggestions card\">\n        <div refs=\"global-search@loading\" class=\"text-center px-m global-search-loading\">@include('common.loading-icon')</div>\n        <div refs=\"global-search@suggestion-results\" class=\"px-m\"></div>\n        <button class=\"text-button card-footer-link\" type=\"submit\">{{ trans('common.view_all') }}</button>\n    </div>\n</form>"
  },
  {
    "path": "resources/views/layouts/parts/header-user-menu.blade.php",
    "content": "<div class=\"dropdown-container\" component=\"dropdown\" option:dropdown:bubble-escapes=\"true\">\n    <button class=\"user-name py-s hide-under-l\" refs=\"dropdown@toggle\"\n          aria-haspopup=\"menu\"\n          aria-expanded=\"false\"\n          aria-label=\"{{ trans('common.profile_menu') }}\">\n        <img class=\"avatar\" src=\"{{$user->getAvatar(30)}}\" alt=\"{{ $user->name }}\">\n        <span class=\"name\">{{ $user->getShortName(9) }}</span> @icon('caret-down')\n    </button>\n    <ul refs=\"dropdown@menu\" class=\"dropdown-menu\" role=\"menu\" aria-label=\"{{ trans('common.profile_menu') }}\">\n        <li>\n            <a href=\"{{ url('/favourites') }}\"\n               role=\"menuitem\"\n               data-shortcut=\"favourites_view\"\n               class=\"icon-item\">\n                @icon('star')\n                <div>{{ trans('entities.my_favourites') }}</div>\n            </a>\n        </li>\n        <li>\n            <a href=\"{{ $user->getProfileUrl() }}\"\n               role=\"menuitem\"\n               data-shortcut=\"profile_view\"\n               class=\"icon-item\">\n                @icon('user')\n                <div>{{ trans('common.view_profile') }}</div>\n            </a>\n        </li>\n        <li>\n            <a href=\"{{ url('/my-account') }}\"\n               role=\"menuitem\"\n               class=\"icon-item\">\n                @icon('user-preferences')\n                <div>{{ trans('preferences.my_account') }}</div>\n            </a>\n        </li>\n        <li role=\"presentation\"><hr></li>\n        <li>\n            @include('common.dark-mode-toggle', ['classes' => 'icon-item', 'buttonRole' => 'menuitem'])\n        </li>\n        <li role=\"presentation\"><hr></li>\n        <li>\n            @php\n                $logoutPath = match (config('auth.method')) {\n                    'saml2' => '/saml2/logout',\n                    'oidc' => '/oidc/logout',\n                    default => '/logout',\n                }\n            @endphp\n            <form action=\"{{ url($logoutPath) }}\" method=\"post\">\n                {{ csrf_field() }}\n                <button class=\"icon-item\" role=\"menuitem\" data-shortcut=\"logout\">\n                    @icon('logout')\n                    <div>{{ trans('auth.logout') }}</div>\n                </button>\n            </form>\n        </li>\n    </ul>\n</div>"
  },
  {
    "path": "resources/views/layouts/parts/header.blade.php",
    "content": "<header id=\"header\" component=\"header-mobile-toggle\" class=\"primary-background px-xl grid print-hidden\">\n    <div class=\"flex-container-row justify-space-between gap-s items-center\">\n        @include('layouts.parts.header-logo')\n        <div class=\"hide-over-l py-s\">\n            <button type=\"button\"\n                    refs=\"header-mobile-toggle@toggle\"\n                    title=\"{{ trans('common.header_menu_expand') }}\"\n                    aria-expanded=\"false\"\n                    class=\"mobile-menu-toggle\">@icon('more')</button>\n        </div>\n    </div>\n\n    <div class=\"flex-container-column items-center justify-center hide-under-l\">\n    @if(user()->hasAppAccess())\n        @include('layouts.parts.header-search')\n    @endif\n    </div>\n\n    <nav refs=\"header-mobile-toggle@menu\" class=\"header-links\">\n        <div class=\"links text-center\">\n            @include('layouts.parts.header-links')\n        </div>\n        @if(!user()->isGuest())\n            @include('layouts.parts.header-user-menu', ['user' => user()])\n        @endif\n    </nav>\n</header>\n"
  },
  {
    "path": "resources/views/layouts/parts/notifications.blade.php",
    "content": "<div component=\"notification\"\n     option:notification:type=\"success\"\n     option:notification:auto-hide=\"true\"\n     option:notification:show=\"{{ session()->has('success') ? 'true' : 'false' }}\"\n     style=\"display: none;\"\n     class=\"notification pos\"\n     role=\"alert\">\n    @icon('check-circle') <span>@if(session()->has('success')){!! nl2br(htmlentities(session()->get('success'))) !!}@endif</span><div class=\"dismiss\">@icon('close')</div>\n</div>\n\n<div component=\"notification\"\n     option:notification:type=\"warning\"\n     option:notification:auto-hide=\"false\"\n     option:notification:show=\"{{ session()->has('warning') ? 'true' : 'false' }}\"\n     style=\"display: none;\"\n     class=\"notification warning\"\n     role=\"alert\">\n    @icon('info') <span>@if(session()->has('warning')){!! nl2br(htmlentities(session()->get('warning'))) !!}@endif</span><div class=\"dismiss\">@icon('close')</div>\n</div>\n\n<div component=\"notification\"\n     option:notification:type=\"error\"\n     option:notification:auto-hide=\"false\"\n     option:notification:show=\"{{ session()->has('error') ? 'true' : 'false' }}\"\n     style=\"display: none;\"\n     class=\"notification neg\"\n     role=\"alert\">\n    @icon('danger') <span>@if(session()->has('error')){!! nl2br(htmlentities(session()->get('error'))) !!}@endif</span><div class=\"dismiss\">@icon('close')</div>\n</div>"
  },
  {
    "path": "resources/views/layouts/parts/skip-to-content.blade.php",
    "content": "<a class=\"px-m py-s skip-to-content-link print-hidden\" href=\"#main-content\">{{ trans('common.skip_to_main_content') }}</a>"
  },
  {
    "path": "resources/views/layouts/plain.blade.php",
    "content": "<!DOCTYPE html>\n<html lang=\"{{ isset($locale) ? $locale->htmlLang() : config('app.default_locale') }}\"\n      dir=\"{{ isset($locale) ? $locale->htmlDirection() : 'auto' }}\"\n      class=\"@yield('document-class')\">\n<head>\n    <title>{{ isset($pageTitle) ? $pageTitle . ' | ' : '' }}{{ setting('app-name') }}</title>\n\n    <!-- Meta -->\n    <meta name=\"viewport\" content=\"width=device-width\">\n    <meta charset=\"utf-8\">\n\n    <!-- Styles -->\n    <link rel=\"stylesheet\" href=\"{{ versioned_asset('dist/styles.css') }}\">\n\n    <!-- Custom Styles & Head Content -->\n    @include('layouts.parts.custom-styles')\n    @include('layouts.parts.custom-head')\n</head>\n<body>\n    @yield('content')\n</body>\n</html>\n"
  },
  {
    "path": "resources/views/layouts/simple.blade.php",
    "content": "@extends('layouts.base')\n\n@section('content')\n\n    <div class=\"flex-fill flex\">\n        <div class=\"content flex\">\n            <div id=\"main-content\" class=\"scroll-body\">\n                @yield('body')\n            </div>\n        </div>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/layouts/tri.blade.php",
    "content": "@extends('layouts.base')\n\n@push('body-class', 'tri-layout ')\n@section('content-components', 'tri-layout')\n\n@section('content')\n\n    <div class=\"tri-layout-mobile-tabs print-hidden\">\n        <div class=\"grid half no-break no-gap\">\n            <button type=\"button\"\n                    refs=\"tri-layout@tab\"\n                    data-tab=\"info\"\n                    aria-label=\"{{ trans('common.tab_info_label') }}\"\n                    class=\"tri-layout-mobile-tab px-m py-m text-link\">\n                {{ trans('common.tab_info') }}\n            </button>\n            <button type=\"button\"\n                    refs=\"tri-layout@tab\"\n                    data-tab=\"content\"\n                    aria-label=\"{{ trans('common.tab_content_label') }}\"\n                    aria-selected=\"true\"\n                    class=\"tri-layout-mobile-tab px-m py-m text-link active\">\n                {{ trans('common.tab_content') }}\n            </button>\n        </div>\n    </div>\n\n    <div refs=\"tri-layout@container\" class=\"tri-layout-container\" @yield('container-attrs') >\n\n        <div class=\"tri-layout-sides print-hidden\">\n            <div refs=\"tri-layout@sidebar-scroll-container\" class=\"tri-layout-sides-content\">\n                <div class=\"tri-layout-right print-hidden\">\n                    <aside refs=\"tri-layout@sidebar-scroll-container\" class=\"tri-layout-right-contents\">\n                        @yield('right')\n                    </aside>\n                </div>\n\n                <div class=\"tri-layout-left print-hidden\" id=\"sidebar\">\n                    <aside refs=\"tri-layout@sidebar-scroll-container\" class=\"tri-layout-left-contents\">\n                        @yield('left')\n                    </aside>\n                </div>\n            </div>\n        </div>\n\n        <div class=\"@yield('body-wrap-classes') tri-layout-middle\">\n            <div id=\"main-content\" class=\"tri-layout-middle-contents\">\n                @yield('body')\n            </div>\n        </div>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/mfa/backup-codes-generate.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container very-small py-xl\">\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ trans('auth.mfa_gen_backup_codes_title') }}</h1>\n            <p>{{ trans('auth.mfa_gen_backup_codes_desc') }}</p>\n\n            <div class=\"text-center mb-xs\">\n                <div class=\"text-bigger code-base p-m\" style=\"column-count: 2\">\n                    @foreach($codes as $code)\n                        {{ $code }} <br>\n                    @endforeach\n                </div>\n            </div>\n\n            <p class=\"text-right\">\n                <a href=\"{{ $downloadUrl }}\" download=\"backup-codes.txt\" class=\"button outline small\">{{ trans('auth.mfa_gen_backup_codes_download') }}</a>\n            </p>\n\n            <p class=\"callout warning\">\n                {{ trans('auth.mfa_gen_backup_codes_usage_warning') }}\n            </p>\n\n            <form action=\"{{ url('/mfa/backup_codes/confirm') }}\" method=\"POST\">\n                {{ csrf_field() }}\n                <div class=\"mt-s text-right\">\n                    <a href=\"{{ url('/mfa/setup') }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                    <button class=\"button\">{{ trans('auth.mfa_gen_confirm_and_enable') }}</button>\n                </div>\n            </form>\n        </div>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/mfa/parts/setup-method-row.blade.php",
    "content": "<div class=\"grid half gap-xl\">\n    <div>\n        <div class=\"setting-list-label\">{{ trans('auth.mfa_option_' . $method . '_title') }}</div>\n        <p class=\"small\">\n            {{ trans('auth.mfa_option_' . $method . '_desc') }}\n        </p>\n    </div>\n    <div class=\"pt-m\">\n        @if($userMethods->has($method))\n            <div class=\"text-pos\">\n                @icon('check-circle')\n                {{ trans('auth.mfa_setup_configured') }}\n            </div>\n            <a href=\"{{ url('/mfa/' . $method . '/generate') }}\" class=\"button outline small\">{{ trans('auth.mfa_setup_reconfigure') }}</a>\n            <div component=\"dropdown\" class=\"inline relative\">\n                <button type=\"button\" refs=\"dropdown@toggle\" class=\"button outline small\">{{ trans('common.remove') }}</button>\n                <div refs=\"dropdown@menu\" class=\"dropdown-menu\">\n                    <p class=\"text-neg small px-m mb-xs\">{{ trans('auth.mfa_setup_remove_confirmation') }}</p>\n                    <form action=\"{{ url('/mfa/' . $method . '/remove') }}\" method=\"post\">\n                        {{ csrf_field() }}\n                        {{ method_field('delete') }}\n                        <button class=\"text-link small text-item\">{{ trans('common.confirm') }}</button>\n                    </form>\n                </div>\n            </div>\n        @else\n            <a href=\"{{ url('/mfa/' . $method . '/generate') }}\" class=\"button outline\">{{ trans('auth.mfa_setup_action') }}</a>\n        @endif\n    </div>\n</div>"
  },
  {
    "path": "resources/views/mfa/parts/verify-backup_codes.blade.php",
    "content": "<div class=\"setting-list-label\">{{ trans('auth.mfa_verify_backup_code') }}</div>\n\n<p class=\"small mb-m\">{{ trans('auth.mfa_verify_backup_code_desc') }}</p>\n\n<form action=\"{{ url('/mfa/backup_codes/verify') }}\" method=\"post\" autocomplete=\"off\">\n    {{ csrf_field() }}\n    <input type=\"text\"\n           name=\"code\"\n           autocomplete=\"one-time-code\"\n           placeholder=\"{{ trans('auth.mfa_verify_backup_code_enter_here') }}\"\n           class=\"input-fill-width {{ $errors->has('code') ? 'neg' : '' }}\">\n    @if($errors->has('code'))\n        <div class=\"text-neg text-small px-xs\">{{ $errors->first('code') }}</div>\n    @endif\n    <div class=\"mt-s text-right\">\n        <button class=\"button\">{{ trans('common.confirm') }}</button>\n    </div>\n</form>"
  },
  {
    "path": "resources/views/mfa/parts/verify-totp.blade.php",
    "content": "<div class=\"setting-list-label\">{{ trans('auth.mfa_option_totp_title') }}</div>\n\n<p class=\"small mb-m\">{{ trans('auth.mfa_verify_totp_desc') }}</p>\n\n<form action=\"{{ url('/mfa/totp/verify') }}\" method=\"post\" autocomplete=\"off\">\n    {{ csrf_field() }}\n    <input type=\"text\"\n           name=\"code\"\n           autocomplete=\"one-time-code\"\n           autofocus\n           placeholder=\"{{ trans('auth.mfa_gen_totp_provide_code_here') }}\"\n           class=\"input-fill-width {{ $errors->has('code') ? 'neg' : '' }}\">\n    @if($errors->has('code'))\n        <div class=\"text-neg text-small px-xs\">{{ $errors->first('code') }}</div>\n    @endif\n    <div class=\"mt-s text-right\">\n        <button class=\"button\">{{ trans('common.confirm') }}</button>\n    </div>\n</form>\n"
  },
  {
    "path": "resources/views/mfa/setup.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n    <div class=\"container small py-xl\">\n\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ trans('auth.mfa_setup') }}</h1>\n            <p class=\"mb-none\"> {{ trans('auth.mfa_setup_desc') }}</p>\n\n            <div class=\"setting-list\">\n                @foreach(['totp', 'backup_codes'] as $method)\n                    @include('mfa.parts.setup-method-row', ['method' => $method])\n                @endforeach\n            </div>\n\n        </div>\n    </div>\n@stop\n"
  },
  {
    "path": "resources/views/mfa/totp-generate.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container very-small py-xl\">\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ trans('auth.mfa_gen_totp_title') }}</h1>\n            <p>{{ trans('auth.mfa_gen_totp_desc') }}</p>\n            <p>{{ trans('auth.mfa_gen_totp_scan') }}</p>\n\n            <div class=\"text-center\">\n                <div class=\"block inline\">\n                    {!! $svg !!}\n                </div>\n                <div class=\"code-base small text-muted px-s py-xs my-xs\" style=\"overflow-x: scroll; white-space: nowrap;\">\n                    {{ $url }}\n                </div>\n            </div>\n\n            <h2 class=\"list-heading\">{{ trans('auth.mfa_gen_totp_verify_setup') }}</h2>\n            <p id=\"totp-verify-input-details\" class=\"mb-s\">{{ trans('auth.mfa_gen_totp_verify_setup_desc') }}</p>\n            <form action=\"{{ url('/mfa/totp/confirm') }}\" method=\"POST\">\n                {{ csrf_field() }}\n                <input type=\"text\"\n                       name=\"code\"\n                       aria-labelledby=\"totp-verify-input-details\"\n                       placeholder=\"{{ trans('auth.mfa_gen_totp_provide_code_here') }}\"\n                       class=\"input-fill-width {{ $errors->has('code') ? 'neg' : '' }}\">\n                @if($errors->has('code'))\n                    <div class=\"text-neg text-small px-xs\">{{ $errors->first('code') }}</div>\n                @endif\n                <div class=\"mt-s text-right\">\n                    <a href=\"{{ url('/mfa/setup') }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                    <button class=\"button\">{{ trans('auth.mfa_gen_confirm_and_enable') }}</button>\n                </div>\n            </form>\n        </div>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/mfa/verify.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n    <div class=\"container very-small py-xl\">\n\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ trans('auth.mfa_verify_access') }}</h1>\n            <p class=\"mb-none\">{{ trans('auth.mfa_verify_access_desc') }}</p>\n\n            @if(!$method)\n                <hr class=\"my-l\">\n                <h5>{{ trans('auth.mfa_verify_no_methods') }}</h5>\n                <p class=\"small\">{{ trans('auth.mfa_verify_no_methods_desc') }}</p>\n                <div>\n                    <a href=\"{{ url('/mfa/setup') }}\" class=\"button outline\">{{ trans('common.configure') }}</a>\n                </div>\n            @endif\n\n            @if($method)\n                <hr class=\"my-l\">\n                @include('mfa.parts.verify-' . $method)\n            @endif\n\n            @if(count($otherMethods) > 0)\n                <hr class=\"my-l\">\n                @foreach($otherMethods as $otherMethod)\n                    <div class=\"text-center\">\n                        <a href=\"{{ url(\"/mfa/verify?method={$otherMethod}\") }}\">{{ trans('auth.mfa_verify_use_' . $otherMethod) }}</a>\n                    </div>\n                @endforeach\n            @endif\n\n        </div>\n    </div>\n@stop\n"
  },
  {
    "path": "resources/views/misc/opensearch.blade.php",
    "content": "@php echo '<?xml version=\"1.0\" encoding=\"UTF-8\"?>' . \"\\n\"; @endphp\n<OpenSearchDescription xmlns=\"http://a9.com/-/spec/opensearch/1.1/\">\n  <ShortName>{{ mb_strimwidth(setting('app-name'), 0, 16) }}</ShortName>\n  <Description>{{ trans('common.opensearch_description', ['appName' => setting('app-name')]) }}</Description>\n  <Image width=\"256\" height=\"256\" type=\"image/png\">{{ setting('app-icon') ?: url('/icon.png') }}</Image>\n  <Image width=\"180\" height=\"180\" type=\"image/png\">{{ setting('app-icon-180') ?: url('/icon-180.png') }}</Image>\n  <Image width=\"128\" height=\"128\" type=\"image/png\">{{ setting('app-icon-128') ?: url('/icon-128.png') }}</Image>\n  <Image width=\"64\" height=\"64\" type=\"image/png\">{{ setting('app-icon-64') ?: url('/icon-64.png') }}</Image>\n  <Image width=\"32\" height=\"32\" type=\"image/png\">{{ setting('app-icon-32') ?: url('/icon-32.png') }}</Image>\n  <Url type=\"text/html\" rel=\"results\" template=\"{{ url('/search') }}?term={searchTerms}\"/>\n  <Url type=\"application/opensearchdescription+xml\" rel=\"self\" template=\"{{ url('/opensearch.xml') }}\"/>\n</OpenSearchDescription>\n"
  },
  {
    "path": "resources/views/misc/robots.blade.php",
    "content": "User-agent: *\n@if($allowRobots)\nDisallow:\n@else\nDisallow: /\n@endif"
  },
  {
    "path": "resources/views/pages/copy.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                $page->book,\n                $page->chapter,\n                $page,\n                $page->getUrl('/copy') => [\n                    'text' => trans('entities.pages_copy'),\n                    'icon' => 'copy',\n                ]\n            ]])\n        </div>\n\n        <div class=\"card content-wrap auto-height\">\n\n            <h1 class=\"list-heading\">{{ trans('entities.pages_copy') }}</h1>\n\n            <form action=\"{{ $page->getUrl('/copy') }}\" method=\"POST\">\n                {!! csrf_field() !!}\n\n                <div class=\"form-group title-input\">\n                    <label for=\"name\">{{ trans('common.name') }}</label>\n                    @include('form.text', ['name' => 'name'])\n                </div>\n\n                <div class=\"form-group\" collapsible>\n                    <button type=\"button\" class=\"collapse-title text-link\" collapsible-trigger aria-expanded=\"false\">\n                        <label for=\"entity_selection\">{{ trans('entities.pages_copy_desination') }}</label>\n                    </button>\n                    <div class=\"collapse-content\" collapsible-content>\n                        @include('entities.selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter', 'entityPermission' => 'page-create'])\n                    </div>\n                </div>\n\n                @include('entities.copy-considerations')\n\n                <div class=\"form-group text-right\">\n                    <a href=\"{{ $page->getUrl() }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                    <button type=\"submit\" class=\"button\">{{ trans('entities.pages_copy') }}</button>\n                </div>\n            </form>\n\n        </div>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/pages/delete.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                $page->book,\n                $page->chapter,\n                $page,\n                $page->getUrl('/delete') => [\n                    'text' => trans('entities.pages_delete'),\n                    'icon' => 'delete',\n                ]\n            ]])\n        </div>\n\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ $page->draft ? trans('entities.pages_delete_draft') : trans('entities.pages_delete') }}</h1>\n\n            @if($usedAsTemplate)\n                <p class=\"text-warn\">{{ trans('entities.pages_delete_warning_template') }}</p>\n            @endif\n\n            <div class=\"grid half v-center\">\n                <div>\n                    <p class=\"text-neg\">\n                        <strong>\n                            {{ $page->draft ? trans('entities.pages_delete_draft_confirm'): trans('entities.pages_delete_confirm') }}\n                        </strong>\n                    </p>\n                </div>\n                <div>\n                    <form action=\"{{ $page->getUrl() }}\" method=\"POST\">\n                        {!! csrf_field() !!}\n                        <input type=\"hidden\" name=\"_method\" value=\"DELETE\">\n                        <div class=\"form-group text-right\">\n                            <a href=\"{{ $page->getUrl() }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                            <button type=\"submit\" class=\"button\">{{ trans('common.confirm') }}</button>\n                        </div>\n                    </form>\n                </div>\n            </div>\n        </div>\n    </div>\n\n@stop"
  },
  {
    "path": "resources/views/pages/edit.blade.php",
    "content": "@extends('layouts.base')\n\n@push('body-class', 'flexbox ')\n\n@section('content')\n\n    <div id=\"main-content\" class=\"flex-fill flex height-fill\">\n        <form action=\"{{ $page->getUrl() }}\" autocomplete=\"off\" data-page-id=\"{{ $page->id }}\" method=\"POST\" class=\"flex flex-fill\">\n            {{ csrf_field() }}\n\n            @if(!$isDraft) {{ method_field('PUT') }} @endif\n            @include('pages.parts.form', ['model' => $page])\n        </form>\n    </div>\n    \n    @include('pages.parts.image-manager', ['uploaded_to' => $page->id])\n    @include('pages.parts.code-editor')\n    @include('entities.selector-popup')\n@stop"
  },
  {
    "path": "resources/views/pages/guest-create.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                ($parent->isA('chapter') ? $parent->book : null),\n                $parent,\n                $parent->getUrl('/create-page') => [\n                    'text' => trans('entities.pages_new'),\n                    'icon' => 'add',\n                ]\n            ]])\n        </div>\n\n        <main class=\"card content-wrap\">\n            <h1 class=\"list-heading\">{{ trans('entities.pages_new') }}</h1>\n            <form action=\"{{  $parent->getUrl('/create-guest-page') }}\" method=\"POST\">\n                {!! csrf_field() !!}\n\n                <div class=\"form-group title-input\">\n                    <label for=\"name\">{{ trans('entities.pages_name') }}</label>\n                    @include('form.text', ['name' => 'name', 'autofocus' => true])\n                </div>\n\n                <div class=\"form-group text-right\">\n                    <a href=\"{{ $parent->getUrl() }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                    <button type=\"submit\" class=\"button\">{{ trans('common.continue') }}</button>\n                </div>\n\n            </form>\n        </main>\n    </div>\n\n@stop"
  },
  {
    "path": "resources/views/pages/move.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                $page->book,\n                $page->chapter,\n                $page,\n                $page->getUrl('/move') => [\n                    'text' => trans('entities.pages_move'),\n                    'icon' => 'folder',\n                ]\n            ]])\n        </div>\n\n        <main class=\"card content-wrap\">\n            <h1 class=\"list-heading\">{{ trans('entities.pages_move') }}</h1>\n\n            <form action=\"{{ $page->getUrl('/move') }}\" method=\"POST\">\n                {!! csrf_field() !!}\n                <input type=\"hidden\" name=\"_method\" value=\"PUT\">\n\n                @include('entities.selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter', 'entityPermission' => 'page-create', 'autofocus' => true])\n\n                <div class=\"form-group text-right\">\n                    <a href=\"{{ $page->getUrl() }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                    <button type=\"submit\" class=\"button\">{{ trans('entities.pages_move') }}</button>\n                </div>\n            </form>\n\n        </main>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/pages/parts/code-editor.blade.php",
    "content": "<div>\n    <div components=\"popup code-editor\"\n         option:code-editor:favourites=\"{{ setting()->getForCurrentUser('code-language-favourites', '') }}\"\n         class=\"popup-background code-editor\">\n        <div refs=\"code-editor@container\" class=\"popup-body\" tabindex=\"-1\">\n\n            <div class=\"popup-header flex-container-row primary-background\">\n                <div class=\"popup-title\">{{ trans('components.code_editor') }}</div>\n                <div component=\"dropdown\" refs=\"code-editor@historyDropDown\" class=\"flex-container-row\">\n                    <button refs=\"dropdown@toggle\">\n                        <span>@icon('history')</span>\n                        <span>{{ trans('components.code_session_history') }}</span>\n                    </button>\n                    <ul refs=\"dropdown@menu code-editor@historyList\" class=\"dropdown-menu\"></ul>\n                </div>\n                <button class=\"popup-header-close\" refs=\"popup@hide\" title=\"{{ trans('common.close') }}\">@icon('close')</button>\n            </div>\n\n            <div class=\"code-editor-body-wrap flex-container-row flex-fill\">\n                <div class=\"code-editor-language-list flex-container-column flex-fill\">\n                    <label for=\"code-editor-language\">{{ trans('components.code_language') }}</label>\n                    <input refs=\"code-editor@languageInput\" id=\"code-editor-language\" type=\"text\">\n                    <div refs=\"code-editor@language-options-container\" class=\"lang-options\">\n                        @php\n                            $languages = [\n                                'Bash', 'CSS', 'C', 'C++', 'C#', 'Clojure', 'Dart', 'Diff', 'Fortran', 'F#', 'Go', 'Groovy', 'Haskell', 'HTML', 'INI',\n                                'Java', 'JavaScript', 'JSON', 'Julia', 'Kotlin', 'LaTeX', 'Lua', 'MarkDown', 'MATLAB', 'MSSQL', 'MySQL',\n                                'Nginx', 'OCaml', 'Octave', 'Pascal', 'Perl', 'PHP', 'PL/SQL', 'PostgreSQL', 'Powershell', 'Python',\n                                'R', 'Ruby', 'Rust', 'SAS', 'Scala', 'Scheme', 'Shell', 'Smarty', 'SQL', 'SQLite', 'Swift',\n                                'Twig', 'TypeScript', 'VBScript', 'VB.NET', 'XML', 'YAML',\n                            ];\n                        @endphp\n\n                        @foreach($languages as $language)\n                            <div class=\"relative\">\n                                <button type=\"button\" refs=\"code-editor@language-button\" data-favourite=\"false\" data-lang=\"{{ strtolower($language) }}\">{{ $language }}</button>\n                                <button class=\"lang-option-favorite-toggle action-favourite\" title=\"{{ trans('common.favourite') }}\">@icon('star-outline')</button>\n                                <button class=\"lang-option-favorite-toggle action-unfavourite\" title=\"{{ trans('common.unfavourite') }}\">@icon('star')</button>\n                            </div>\n                        @endforeach\n                    </div>\n                </div>\n\n                <div class=\"code-editor-main flex-fill\">\n                    <textarea refs=\"code-editor@editor\"></textarea>\n                </div>\n\n            </div>\n\n            <div class=\"popup-footer\">\n                <button refs=\"code-editor@saveButton\" type=\"button\" class=\"button\">{{ trans('components.code_save') }}</button>\n            </div>\n\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/pages/parts/editor-toolbar.blade.php",
    "content": "<div class=\"toolbar page-edit-toolbar py-xs\">\n\n    <div>\n        <div class=\"inline block\">\n            <a href=\"{{ $isDraft ? $page->getParent()->getUrl() : $page->getUrl() }}\"\n               class=\"icon-list-item text-link\"><span>@icon('back')</span><span class=\"hide-under-l\">{{ trans('common.back') }}</span></a>\n        </div>\n    </div>\n\n    <div class=\"text-center\">\n        <div component=\"dropdown\"\n             option:dropdown:move-menu=\"true\"\n             class=\"dropdown-container  draft-display text {{ $draftsEnabled ? '' : 'hidden' }}\">\n            <div class=\"flex-container-row items-center justify-center\">\n                <button type=\"button\"\n                        refs=\"dropdown@toggle\"\n                        aria-haspopup=\"menu\"\n                        aria-expanded=\"false\"\n                        title=\"{{ trans('entities.pages_edit_draft_options') }}\"\n                        class=\"text-link icon-list-item\">\n                    <span>@icon('time')</span>\n                    <span><span refs=\"page-editor@draftDisplay\" class=\"faded-text\"></span>&nbsp; @icon('more')</span>\n                </button>\n                @icon('check-circle', ['class' => 'text-pos draft-notification svg-icon', 'refs' => 'page-editor@draftDisplayIcon'])\n            </div>\n            <ul refs=\"dropdown@menu\" class=\"dropdown-menu\" role=\"menu\">\n                <li>\n                    <button refs=\"page-editor@saveDraft\" type=\"button\" role=\"menuitem\" class=\"text-pos icon-item\">\n                        @icon('save')\n                        <div>{{ trans('entities.pages_edit_save_draft') }}</div>\n                    </button>\n                </li>\n                @if($isDraft)\n                    <li>\n                        <a href=\"{{ $model->getUrl('/delete') }}\" role=\"menuitem\" class=\"text-neg icon-item\">\n                            @icon('delete')\n                            {{ trans('entities.pages_edit_delete_draft') }}\n                        </a>\n                    </li>\n                @endif\n                <li refs=\"page-editor@discard-draft-wrap\" {{ $isDraftRevision ? '' : 'hidden' }}>\n                    <button refs=\"page-editor@discard-draft\" type=\"button\" role=\"menuitem\" class=\"text-warn icon-item\">\n                        @icon('cancel')\n                        <div>{{ trans('entities.pages_edit_discard_draft') }}</div>\n                    </button>\n                </li>\n                <li refs=\"page-editor@delete-draft-wrap\" {{ $isDraftRevision ? '' : 'hidden' }}>\n                    <button refs=\"page-editor@delete-draft\" type=\"button\" role=\"menuitem\" class=\"text-neg icon-item\">\n                        @icon('delete')\n                        <div>{{ trans('entities.pages_edit_delete_draft') }}</div>\n                    </button>\n                </li>\n                @if(userCan(\\BookStack\\Permissions\\Permission::EditorChange))\n                    <li role=\"presentation\">\n                        <hr>\n                    </li>\n                    <li>\n                        @if($editor !== \\BookStack\\Entities\\Tools\\PageEditorType::Markdown)\n                            <a href=\"{{ $model->getUrl($isDraft ? '' : '/edit') }}?editor=markdown-clean\" refs=\"page-editor@changeEditor\" role=\"menuitem\" class=\"icon-item\">\n                                @icon('swap-horizontal')\n                                <div>\n                                    {{ trans('entities.pages_edit_switch_to_markdown') }}\n                                    <br>\n                                    <small>{{ trans('entities.pages_edit_switch_to_markdown_clean') }}</small>\n                                </div>\n                            </a>\n                            <a href=\"{{ $model->getUrl($isDraft ? '' : '/edit') }}?editor=markdown-stable\" refs=\"page-editor@changeEditor\" role=\"menuitem\" class=\"icon-item\">\n                                @icon('swap-horizontal')\n                                <div>\n                                    {{ trans('entities.pages_edit_switch_to_markdown') }}\n                                    <br>\n                                    <small>{{ trans('entities.pages_edit_switch_to_markdown_stable') }}</small>\n                                </div>\n                            </a>\n                        @endif\n                        @if($editor !== \\BookStack\\Entities\\Tools\\PageEditorType::WysiwygTinymce)\n                            <a href=\"{{ $model->getUrl($isDraft ? '' : '/edit') }}?editor=wysiwyg\" refs=\"page-editor@changeEditor\" role=\"menuitem\" class=\"icon-item\">\n                                @icon('swap-horizontal')\n                                <div>{{ trans('entities.pages_edit_switch_to_wysiwyg') }}</div>\n                            </a>\n                        @endif\n                        @if($editor !== \\BookStack\\Entities\\Tools\\PageEditorType::WysiwygLexical)\n                            <a href=\"{{ $model->getUrl($isDraft ? '' : '/edit') }}?editor=wysiwyg2024\" refs=\"page-editor@changeEditor\" role=\"menuitem\" class=\"icon-item\">\n                                @icon('swap-horizontal')\n                                <div>\n                                    {{ trans('entities.pages_edit_switch_to_new_wysiwyg') }}\n                                    <br>\n                                    <small>{{ trans('entities.pages_edit_switch_to_new_wysiwyg_desc') }}</small>\n                                </div>\n                            </a>\n                        @endif\n                    </li>\n                @endif\n            </ul>\n        </div>\n    </div>\n\n    <div class=\"flex-container-row justify-flex-end gap-x-m items-center\">\n        <div component=\"dropdown\"\n             option:dropdown:move-menu=\"true\"\n             class=\"dropdown-container\">\n            <button refs=\"dropdown@toggle\" type=\"button\" aria-haspopup=\"true\" aria-expanded=\"false\" class=\"icon-list-item text-link\">\n                <span>@icon('edit')</span>\n                <span refs=\"page-editor@changelogDisplay\">{{ trans('entities.pages_edit_set_changelog') }}</span>\n            </button>\n            <ul refs=\"dropdown@menu\" class=\"wide dropdown-menu\">\n                <li class=\"px-m py-s\">\n                    <p class=\"text-muted pb-s\">{{ trans('entities.pages_edit_enter_changelog_desc') }}</p>\n                    <textarea\n                        refs=\"page-editor@changelogInput\"\n                        name=\"summary\"\n                        id=\"summary-input\"\n                        rows=\"2\"\n                        maxlength=\"180\"\n                        title=\"{{ trans('entities.pages_edit_enter_changelog') }}\"\n                        placeholder=\"{{ trans('entities.pages_edit_enter_changelog') }}\"\n                    ></textarea>\n                    <small refs=\"page-editor@changelogCounter\" class=\"text-muted mt-xs\">0 / 180</small>\n                </li>\n            </ul>\n            <span>{{-- Prevents button jumping on menu show --}}</span>\n        </div>\n\n        <div class=\"inline block\">\n            <button type=\"submit\" id=\"save-button\"\n                    class=\"icon-list-item hide-under-m text-pos fill-width\">\n                <span>@icon('save')</span>\n                <span>{{ trans('entities.pages_save') }}</span>\n            </button>\n        </div>\n    </div>\n</div>"
  },
  {
    "path": "resources/views/pages/parts/editor-toolbox.blade.php",
    "content": "<div component=\"editor-toolbox\" class=\"floating-toolbox\">\n\n    <div class=\"tabs flex-container-column justify-flex-start\">\n        <div class=\"tabs-inner flex-container-column justify-center\">\n            <button type=\"button\" refs=\"editor-toolbox@toggle\" title=\"{{ trans('entities.toggle_sidebar') }}\" aria-expanded=\"false\" class=\"toolbox-toggle\">@icon('caret-left-circle')</button>\n            <button type=\"button\" refs=\"editor-toolbox@tab-button\" data-tab=\"tags\" title=\"{{ trans('entities.page_tags') }}\" class=\"active\">@icon('tag')</button>\n            @if(userCan(\\BookStack\\Permissions\\Permission::AttachmentCreateAll))\n                <button type=\"button\" refs=\"editor-toolbox@tab-button\" data-tab=\"files\" title=\"{{ trans('entities.attachments') }}\">@icon('attach')</button>\n            @endif\n            <button type=\"button\" refs=\"editor-toolbox@tab-button\" data-tab=\"templates\" title=\"{{ trans('entities.templates') }}\">@icon('template')</button>\n            @if($comments->enabled())\n                <button type=\"button\" refs=\"editor-toolbox@tab-button\" data-tab=\"comments\" title=\"{{ trans('entities.comments') }}\">@icon('comment')</button>\n            @endif\n        </div>\n    </div>\n\n    <div refs=\"editor-toolbox@tab-content\" data-tab-content=\"tags\" class=\"toolbox-tab-content\">\n        <h4>{{ trans('entities.page_tags') }}</h4>\n        <div class=\"px-l\">\n            @include('entities.tag-manager', ['entity' => $page])\n        </div>\n    </div>\n\n    @if(userCan(\\BookStack\\Permissions\\Permission::AttachmentCreateAll))\n        @include('attachments.manager', ['page' => $page])\n    @endif\n\n    <div refs=\"editor-toolbox@tab-content\" data-tab-content=\"templates\" class=\"toolbox-tab-content\">\n        <h4>{{ trans('entities.templates') }}</h4>\n\n        <div class=\"px-l\">\n            @include('pages.parts.template-manager', ['page' => $page, 'templates' => $templates])\n        </div>\n    </div>\n\n    @if($comments->enabled())\n        @include('pages.parts.toolbox-comments')\n    @endif\n\n</div>\n"
  },
  {
    "path": "resources/views/pages/parts/form.blade.php",
    "content": "<div component=\"page-editor\" class=\"page-editor page-editor-{{ $editor }} flex-fill flex\"\n     option:page-editor:drafts-enabled=\"{{ $draftsEnabled ? 'true' : 'false' }}\"\n     @if(config('services.drawio'))\n        drawio-url=\"{{ is_string(config('services.drawio')) ? config('services.drawio') : 'https://embed.diagrams.net/?embed=1&proto=json&spin=1&configure=1' }}\"\n     @endif\n     @if($model->name === trans('entities.pages_initial_name'))\n        option:page-editor:has-default-title=\"true\"\n     @endif\n     option:page-editor:editor-type=\"{{ $editor }}\"\n     option:page-editor:page-id=\"{{ $model->id ?? '0' }}\"\n     option:page-editor:page-new-draft=\"{{ $isDraft ? 'true' : 'false' }}\"\n     option:page-editor:draft-text=\"{{ ($isDraft || $isDraftRevision) ? trans('entities.pages_editing_draft') : trans('entities.pages_editing_page') }}\"\n     option:page-editor:autosave-fail-text=\"{{ trans('errors.page_draft_autosave_fail') }}\"\n     option:page-editor:editing-page-text=\"{{ trans('entities.pages_editing_page') }}\"\n     option:page-editor:draft-discarded-text=\"{{ trans('entities.pages_draft_discarded') }}\"\n     option:page-editor:draft-delete-text=\"{{ trans('entities.pages_draft_deleted') }}\"\n     option:page-editor:draft-delete-fail-text=\"{{ trans('errors.page_draft_delete_fail') }}\"\n     option:page-editor:set-changelog-text=\"{{ trans('entities.pages_edit_set_changelog') }}\">\n\n    {{--Header Toolbar--}}\n    @include('pages.parts.editor-toolbar', ['model' => $model, 'editor' => $editor, 'isDraft' => $isDraft, 'draftsEnabled' => $draftsEnabled])\n\n    <div class=\"flex flex-fill mx-s mb-s justify-center page-editor-page-area-wrap\">\n        <div class=\"page-editor-page-area flex-container-column flex flex-fill\">\n            {{--Title input--}}\n            <div class=\"title-input page-title clearfix\">\n                <div refs=\"page-editor@titleContainer\" class=\"input\">\n                    @include('form.text', ['name' => 'name', 'model' => $model, 'placeholder' => trans('entities.pages_title')])\n                </div>\n            </div>\n\n            <div class=\"flex-fill flex\">\n                {{--Editors--}}\n                <div class=\"edit-area flex-fill flex\">\n                    <input type=\"hidden\" name=\"editor\" value=\"{{ $editor->value }}\">\n\n                    @if($editor === \\BookStack\\Entities\\Tools\\PageEditorType::WysiwygLexical)\n                        @include('pages.parts.wysiwyg-editor', ['model' => $model])\n                    @endif\n\n                    {{--WYSIWYG Editor (TinyMCE - Deprecated)--}}\n                    @if($editor === \\BookStack\\Entities\\Tools\\PageEditorType::WysiwygTinymce)\n                        @include('pages.parts.wysiwyg-editor-tinymce', ['model' => $model])\n                    @endif\n\n                    {{--Markdown Editor--}}\n                    @if($editor === \\BookStack\\Entities\\Tools\\PageEditorType::Markdown)\n                        @include('pages.parts.markdown-editor', ['model' => $model])\n                    @endif\n\n                </div>\n\n            </div>\n        </div>\n\n        <div class=\"relative flex-fill\">\n            @include('pages.parts.editor-toolbox')\n        </div>\n    </div>\n\n    {{--Mobile Save Button--}}\n    <button type=\"submit\"\n            id=\"save-button-mobile\"\n            title=\"{{ trans('entities.pages_save') }}\"\n            class=\"text-link text-button hide-over-m page-save-mobile-button\">@icon('save')</button>\n\n    {{--Editor Change Dialog--}}\n    @component('common.confirm-dialog', ['title' => trans('entities.pages_editor_switch_title'), 'ref' => 'page-editor@switch-dialog'])\n        <p>\n            {{ trans('entities.pages_editor_switch_are_you_sure') }}\n            <br>\n            {{ trans('entities.pages_editor_switch_consider_following') }}\n        </p>\n\n        <ul>\n            <li>{{ trans('entities.pages_editor_switch_consideration_a') }}</li>\n            <li>{{ trans('entities.pages_editor_switch_consideration_b') }}</li>\n            <li>{{ trans('entities.pages_editor_switch_consideration_c') }}</li>\n        </ul>\n    @endcomponent\n\n    {{--Delete Draft Dialog--}}\n    @component('common.confirm-dialog', ['title' => trans('entities.pages_edit_delete_draft'), 'ref' => 'page-editor@delete-draft-dialog'])\n        <p>\n            {{ trans('entities.pages_edit_delete_draft_confirm') }}\n        </p>\n    @endcomponent\n\n    {{--Saved Drawing--}}\n    @component('common.confirm-dialog', ['title' => trans('entities.pages_drawing_unsaved'), 'id' => 'unsaved-drawing-dialog'])\n        <p>\n            {{ trans('entities.pages_drawing_unsaved_confirm') }}\n        </p>\n    @endcomponent\n</div>"
  },
  {
    "path": "resources/views/pages/parts/image-manager-form.blade.php",
    "content": "<div component=\"dropzone\"\n     option:dropzone:url=\"{{ url(\"/images/{$image->id}/file\") }}\"\n     option:dropzone:method=\"PUT\"\n     option:dropzone:success-message=\"{{ trans('components.image_update_success') }}\"\n     option:dropzone:upload-limit=\"{{ config('app.upload_limit') }}\"\n     option:dropzone:upload-limit-message=\"{{ trans('errors.server_upload_limit') }}\"\n     option:dropzone:zone-text=\"{{ trans('entities.attachments_dropzone') }}\"\n     option:dropzone:file-accept=\"image/*\"\n     class=\"image-manager-details\">\n\n    @if($warning ?? '')\n        <div class=\"image-manager-warning px-m py-xs flex-container-row gap-xs items-center mb-l\">\n            <div>@icon('warning')</div>\n            <div class=\"flex\">{{ $warning }}</div>\n        </div>\n    @endif\n\n    <div refs=\"dropzone@status-area dropzone@drop-target\"></div>\n\n    <script id=\"image-manager-form-image-data\" type=\"application/json\">@json($image)</script>\n\n    <form component=\"ajax-form\"\n          option:ajax-form:success-message=\"{{ trans('components.image_update_success') }}\"\n          option:ajax-form:method=\"put\"\n          option:ajax-form:response-container=\".image-manager-details\"\n          option:ajax-form:url=\"{{ url('images/' . $image->id) }}\">\n\n        <div class=\"image-manager-viewer\">\n            <a href=\"{{ $image->url }}\" target=\"_blank\" rel=\"noopener\" class=\"block\">\n                <img src=\"{{ $image->thumbs['display'] ?? $image->url }}\"\n                     alt=\"{{ $image->name }}\"\n                     class=\"anim fadeIn\"\n                     title=\"{{ $image->name }}\">\n            </a>\n        </div>\n        <div class=\"form-group stretch-inputs\">\n            <label for=\"name\">{{ trans('components.image_image_name') }}</label>\n            <input id=\"name\" class=\"input-base\" type=\"text\" name=\"name\" value=\"{{ $image->name }}\">\n        </div>\n        <div class=\"flex-container-row justify-space-between gap-m\">\n            @if(userCan(\\BookStack\\Permissions\\Permission::ImageDelete, $image) || userCan(\\BookStack\\Permissions\\Permission::ImageUpdate, $image))\n                <div component=\"dropdown\"\n                     class=\"dropdown-container\">\n                    <button refs=\"dropdown@toggle\" type=\"button\" class=\"button icon outline\">@icon('more')</button>\n                    <div refs=\"dropdown@menu\" class=\"dropdown-menu anchor-left\">\n                        @if(userCan(\\BookStack\\Permissions\\Permission::ImageDelete, $image))\n                            <button type=\"button\"\n                                    id=\"image-manager-delete\"\n                                    class=\"text-item\">{{ trans('common.delete') }}</button>\n                        @endif\n                        @if(userCan(\\BookStack\\Permissions\\Permission::ImageUpdate, $image))\n                            <button type=\"button\"\n                                    id=\"image-manager-replace\"\n                                    refs=\"dropzone@select-button\"\n                                    class=\"text-item\">{{ trans('components.image_replace') }}</button>\n                            <button type=\"button\"\n                                    id=\"image-manager-rebuild-thumbs\"\n                                    class=\"text-item\">{{ trans('components.image_rebuild_thumbs') }}</button>\n                        @endif\n                    </div>\n                </div>\n            @endif\n                <button type=\"submit\"\n                        class=\"button icon outline\">{{ trans('common.save') }}</button>\n        </div>\n    </form>\n\n    @if(!is_null($dependantPages))\n        <hr>\n        @if(count($dependantPages) > 0)\n            <p class=\"text-neg mb-xs mt-m\">{{ trans('components.image_delete_used') }}</p>\n            <ul class=\"text-neg\">\n                @foreach($dependantPages as $page)\n                    <li>\n                        <a href=\"{{ $page->url }}\"\n                           target=\"_blank\"\n                           rel=\"noopener\"\n                           class=\"text-neg\">{{ $page->name }}</a>\n                    </li>\n                @endforeach\n            </ul>\n        @endif\n        <p class=\"text-neg mb-xs\">{{ trans('components.image_delete_confirm_text') }}</p>\n        <form component=\"ajax-form\"\n              option:ajax-form:success-message=\"{{ trans('components.image_delete_success') }}\"\n              option:ajax-form:method=\"delete\"\n              option:ajax-form:response-container=\".image-manager-details\"\n              option:ajax-form:url=\"{{ url('images/' . $image->id) }}\">\n            <button type=\"submit\" class=\"button neg\">\n                {{ trans('common.delete_confirm') }}\n            </button>\n        </form>\n    @endif\n\n    <div class=\"text-muted text-small\">\n        <hr class=\"my-m\">\n        <div title=\"{{ $dates->absolute($image->created_at) }}\">\n            @icon('star') {{ trans('components.image_uploaded', ['uploadedDate' => $dates->relative($image->created_at)]) }}\n        </div>\n        @if($image->created_at->valueOf() !== $image->updated_at->valueOf())\n            <div title=\"{{ $dates->absolute($image->updated_at) }}\">\n                @icon('edit') {{ trans('components.image_updated', ['updateDate' => $dates->relative($image->updated_at)]) }}\n            </div>\n        @endif\n        @if($image->createdBy)\n            <div>@icon('user') {{ trans('components.image_uploaded_by', ['userName' => $image->createdBy->name]) }}</div>\n        @endif\n        @if(($page = $image->getPage()) && userCan(\\BookStack\\Permissions\\Permission::PageView, $page))\n            <div>\n                @icon('page')\n                {!! trans('components.image_uploaded_to', [\n                    'pageLink' => '<a class=\"text-page\" href=\"' . e($page->getUrl()) . '\" target=\"_blank\">' . e($page->name) . '</a>'\n                ]) !!}\n            </div>\n        @endif\n    </div>\n\n</div>"
  },
  {
    "path": "resources/views/pages/parts/image-manager-list.blade.php",
    "content": "@if($warning ?? '')\n    <div class=\"image-manager-list-warning image-manager-warning px-m py-xs flex-container-row gap-xs items-center\">\n        <div>@icon('warning')</div>\n        <div class=\"flex\">{{ $warning }}</div>\n    </div>\n@endif\n@foreach($images as $index => $image)\n<div>\n    <button component=\"event-emit-select\"\n         option:event-emit-select:name=\"image\"\n         option:event-emit-select:data=\"{{ json_encode($image) }}\"\n         class=\"image anim fadeIn text-link\"\n         style=\"animation-delay: {{ min($index * 10, 260) . 'ms' }};\">\n        <img src=\"{{ $image->thumbs['gallery'] ?? '' }}\"\n             alt=\"{{ $image->name }}\"\n             role=\"none\"\n             width=\"150\"\n             height=\"150\"\n             loading=\"lazy\">\n        <div class=\"image-meta\">\n            <span class=\"name\">{{ $image->name }}</span>\n            <span class=\"date\">{{ trans('components.image_uploaded', ['uploadedDate' => $image->created_at->format('Y-m-d')]) }}</span>\n        </div>\n    </button>\n</div>\n@endforeach\n@if(count($images) === 0)\n    <p class=\"m-m text-bigger italic text-muted\">{{ trans('common.no_items') }}</p>\n@endif\n@if($hasMore)\n    <div class=\"load-more\">\n        <button type=\"button\" class=\"button small outline\">{{ trans('components.image_load_more') }}</button>\n    </div>\n@endif"
  },
  {
    "path": "resources/views/pages/parts/image-manager.blade.php",
    "content": "<div components=\"image-manager dropzone\"\n     option:dropzone:url=\"{{ url('/images/gallery?' . http_build_query(['uploaded_to' => $uploaded_to ?? 0])) }}\"\n     option:dropzone:success-message=\"{{ trans('components.image_upload_success') }}\"\n     option:dropzone:error-message=\"{{ trans('errors.image_upload_error') }}\"\n     option:dropzone:upload-limit=\"{{ config('app.upload_limit') }}\"\n     option:dropzone:upload-limit-message=\"{{ trans('errors.server_upload_limit') }}\"\n     option:dropzone:zone-text=\"{{ trans('components.image_dropzone_drop') }}\"\n     option:dropzone:file-accept=\"image/*\"\n     option:dropzone:allow-multiple=\"true\"\n     option:image-manager:uploaded-to=\"{{ $uploaded_to ?? 0 }}\"\n     class=\"image-manager\">\n\n    <div component=\"popup\"\n         refs=\"image-manager@popup\"\n         class=\"popup-background\">\n        <div class=\"popup-body\" tabindex=\"-1\">\n\n            <div class=\"popup-header primary-background\">\n                <div class=\"popup-title\">{{ trans('components.image_select') }}</div>\n                <button refs=\"dropzone@selectButton image-manager@uploadButton\" type=\"button\">\n                    <span>@icon('upload')</span>\n                    <span>{{ trans('components.image_upload') }}</span>\n                </button>\n                <button refs=\"popup@hide\"\n                        type=\"button\"\n                        title=\"{{ trans('common.close') }}\"\n                        class=\"popup-header-close\">@icon('close')</button>\n            </div>\n\n            <div component=\"tabs\"\n                 option:tabs:active-under=\"880\"\n                 refs=\"dropzone@drop-target\"\n                 class=\"flex-container-column image-manager-body\">\n                <div class=\"tab-container\">\n                    <div role=\"tablist\" class=\"hide-over-m mb-none\">\n                        <button id=\"image-manager-list-tab\"\n                                aria-selected=\"true\"\n                                aria-controls=\"image-manager-list\"\n                                role=\"tab\">{{ trans('components.image_list') }}</button>\n                        <button id=\"image-manager-info-tab\"\n                                aria-selected=\"true\"\n                                aria-controls=\"image-manager-info\"\n                                role=\"tab\">{{ trans('components.image_details') }}</button>\n                    </div>\n                </div>\n                <div class=\"flex-container-row flex-fill flex\">\n                    <div id=\"image-manager-list\"\n                         tabindex=\"0\"\n                         role=\"tabpanel\"\n                         aria-labelledby=\"image-manager-list-tab\"\n                         class=\"image-manager-content\">\n                        <div class=\"image-manager-filter-bar flex-container-row wrap justify-space-between\">\n                            <div class=\"primary-background image-manager-filter-bar-bg\"></div>\n                            <div>\n                                <form refs=\"image-manager@searchForm\" role=\"search\" class=\"contained-search-box floating mx-m my-s\">\n                                    <input refs=\"image-manager@searchInput\"\n                                           placeholder=\"{{ trans('components.image_search_hint') }}\"\n                                           type=\"search\">\n                                    <button refs=\"image-manager@cancelSearch\"\n                                            title=\"{{ trans('common.search_clear') }}\"\n                                            type=\"button\"\n                                            hidden=\"hidden\"\n                                            class=\"cancel\">@icon('close')</button>\n                                    <button type=\"submit\"\n                                            title=\"{{ trans('common.search') }}\">@icon('search')</button>\n                                </form>\n                            </div>\n                            <div class=\"tab-container bordered mx-m my-s\">\n                                <div role=\"tablist\" class=\"image-manager-filters flex-container-row mb-none\">\n                                    <button refs=\"image-manager@filterTabs\"\n                                            data-filter=\"all\"\n                                            role=\"tab\"\n                                            aria-selected=\"true\"\n                                            type=\"button\"\n                                            title=\"{{ trans('components.image_all_title') }}\">@icon('images')</button>\n                                    <button refs=\"image-manager@filterTabs\"\n                                            data-filter=\"book\"\n                                            role=\"tab\"\n                                            aria-selected=\"false\"\n                                            type=\"button\"\n                                            title=\"{{ trans('components.image_book_title') }}\">@icon('book', ['class' => 'svg-icon'])</button>\n                                    <button refs=\"image-manager@filterTabs\"\n                                            data-filter=\"page\"\n                                            role=\"tab\"\n                                            aria-selected=\"false\"\n                                            type=\"button\"\n                                            title=\"{{ trans('components.image_page_title') }}\">@icon('page', ['class' => 'svg-icon'])</button>\n                                </div>\n                            </div>\n                        </div>\n                        <div refs=\"image-manager@listContainer\" class=\"image-manager-list\"></div>\n                        <div refs=\"image-manager@loadMore\" class=\"load-more\" hidden>\n                            <button type=\"button\" class=\"button small outline\">Load More</button>\n                        </div>\n                    </div>\n\n                    <div id=\"image-manager-info\"\n                         tabindex=\"0\"\n                         role=\"tabpanel\"\n                         aria-labelledby=\"image-manager-info-tab\"\n                         class=\"image-manager-sidebar flex-container-column\">\n\n                        <div refs=\"image-manager@dropzoneContainer\">\n                            <div refs=\"dropzone@status-area\"></div>\n                        </div>\n\n                        <div refs=\"image-manager@form-container-placeholder\" class=\"p-m text-small text-muted\">\n                            <p>{{ trans('components.image_intro') }}</p>\n                            <p refs=\"image-manager@upload-hint\">{{ trans('components.image_intro_upload') }}</p>\n                        </div>\n\n                        <div refs=\"image-manager@formContainer\" class=\"inner flex\">\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"popup-footer\">\n                <button refs=\"image-manager@selectButton\" type=\"button\" class=\"hidden button\">\n                    {{ trans('components.image_select_image') }}\n                </button>\n            </div>\n\n        </div>\n    </div>\n</div>"
  },
  {
    "path": "resources/views/pages/parts/list-item.blade.php",
    "content": "@component('entities.list-item-basic', ['entity' => $page])\n    <div class=\"entity-item-snippet\">\n        <p class=\"text-muted break-text\">{{ $page->getExcerpt() }}</p>\n    </div>\n@endcomponent"
  },
  {
    "path": "resources/views/pages/parts/markdown-editor.blade.php",
    "content": "<div id=\"markdown-editor\" component=\"markdown-editor\"\n     option:markdown-editor:page-id=\"{{ $model->id ?? 0 }}\"\n     option:markdown-editor:text-direction=\"{{ $locale->htmlDirection() }}\"\n     option:markdown-editor:image-upload-error-text=\"{{ trans('errors.image_upload_error') }}\"\n     option:markdown-editor:server-upload-limit-text=\"{{ trans('errors.server_upload_limit') }}\"\n     class=\"flex-fill flex code-fill\">\n\n    <div class=\"markdown-editor-wrap active flex-container-column\">\n        <div class=\"editor-toolbar flex-container-row items-stretch justify-space-between\">\n            <div class=\"editor-toolbar-label text-mono bold px-m py-xs flex-container-row items-center flex\">\n                <span>{{ trans('entities.pages_md_editor') }}</span>\n            </div>\n            <div component=\"dropdown\" class=\"buttons flex-container-row items-stretch\">\n                @if(config('services.drawio'))\n                    <button class=\"text-button\" type=\"button\" data-action=\"insertDrawing\" title=\"{{ trans('entities.pages_md_insert_drawing') }}\">@icon('drawing')</button>\n                @endif\n                <button class=\"text-button\" type=\"button\" data-action=\"insertImage\" title=\"{{ trans('entities.pages_md_insert_image') }}\">@icon('image')</button>\n                <button class=\"text-button\" type=\"button\" data-action=\"insertLink\" title=\"{{ trans('entities.pages_md_insert_link') }}\">@icon('link')</button>\n                <button class=\"text-button\" type=\"button\" data-action=\"fullscreen\" title=\"{{ trans('common.fullscreen') }}\">@icon('fullscreen')</button>\n                <button refs=\"dropdown@toggle\" class=\"text-button\" type=\"button\" title=\"{{ trans('common.more') }}\">@icon('more')</button>\n                <div refs=\"dropdown@menu markdown-editor@setting-container\" class=\"dropdown-menu\" role=\"menu\">\n                    <div class=\"px-m\">\n                        @include('form.custom-checkbox', ['name' => 'md-showPreview', 'label' => trans('entities.pages_md_show_preview'), 'value' => true, 'checked' => true])\n                    </div>\n                    <hr class=\"m-none\">\n                    <div class=\"px-m\">\n                        @include('form.custom-checkbox', ['name' => 'md-scrollSync', 'label' => trans('entities.pages_md_sync_scroll'), 'value' => true, 'checked' => true])\n                    </div>\n                    <hr class=\"m-none\">\n                    <div class=\"px-m\">\n                        @include('form.custom-checkbox', ['name' => 'md-plainEditor', 'label' => trans('entities.pages_md_plain_editor'), 'value' => true, 'checked' => false])\n                    </div>\n                </div>\n            </div>\n        </div>\n\n        <div class=\"flex flex-fill\" dir=\"ltr\">\n            <textarea id=\"markdown-editor-input\"\n                      refs=\"markdown-editor@input\"\n                      @if($errors->has('markdown')) class=\"text-neg\" @endif\n                      name=\"markdown\"\n                      rows=\"5\">@if(isset($model) || old('markdown')){{ old('markdown') ?? ($model->markdown === '' ? $model->html : $model->markdown) }}@endif</textarea>\n        </div>\n\n    </div>\n\n    <div refs=\"markdown-editor@display-wrap\" class=\"markdown-editor-wrap flex-container-row items-stretch\" style=\"display: none\">\n        <div refs=\"markdown-editor@divider\" class=\"markdown-panel-divider flex-fill\"></div>\n        <div class=\"flex-container-column flex flex-fill\">\n            <div class=\"editor-toolbar\">\n                <div class=\"editor-toolbar-label text-mono bold px-m py-xs\">{{ trans('entities.pages_md_preview') }}</div>\n            </div>\n            <iframe src=\"about:blank\"\n                    refs=\"markdown-editor@display\"\n                    class=\"markdown-display flex flex-fill\"\n                    sandbox=\"allow-same-origin\"></iframe>\n        </div>\n    </div>\n</div>\n\n\n\n@if($errors->has('markdown'))\n    <div class=\"text-neg text-small\">{{ $errors->first('markdown') }}</div>\n@endif"
  },
  {
    "path": "resources/views/pages/parts/page-display.blade.php",
    "content": "<div dir=\"auto\">\n\n    <h1 class=\"break-text\" id=\"bkmrk-page-title\">{{$page->name}}</h1>\n\n    <div style=\"clear:left;\"></div>\n\n    @if (isset($diff) && $diff)\n        {!! $diff !!}\n    @else\n        {!! isset($page->renderedHTML) ? $page->renderedHTML : $page->html !!}\n    @endif\n</div>"
  },
  {
    "path": "resources/views/pages/parts/pointer.blade.php",
    "content": "\n<div component=\"pointer\"\n      option:pointer:page-id=\"{{ $page->id }}\">\n    <div id=\"pointer\"\n         refs=\"pointer@pointer\"\n         tabindex=\"-1\"\n         aria-label=\"{{ trans('entities.pages_pointer_label') }}\"\n         class=\"pointer-container\">\n        <div class=\"pointer flex-container-row items-center justify-space-between gap-xs p-xs anim\" >\n            <div refs=\"pointer@mode-section\" class=\"flex flex-container-row items-center gap-xs\">\n                <button refs=\"pointer@mode-toggle\"\n                        title=\"{{ trans('entities.pages_pointer_toggle_link') }}\"\n                        class=\"text-button icon px-xs\">@icon('link')</button>\n                <div class=\"input-group flex flex-container-row items-center\">\n                    <input refs=\"pointer@link-input\" aria-label=\"{{ trans('entities.pages_pointer_permalink') }}\" readonly=\"readonly\" type=\"text\" id=\"pointer-url\" placeholder=\"url\">\n                    <button refs=\"pointer@link-button\" class=\"button outline icon px-xs\" type=\"button\" title=\"{{ trans('entities.pages_copy_link') }}\">@icon('copy')</button>\n                </div>\n            </div>\n            <div refs=\"pointer@mode-section\" hidden class=\"flex flex-container-row items-center gap-xs\">\n                <button refs=\"pointer@mode-toggle\"\n                        title=\"{{ trans('entities.pages_pointer_toggle_include') }}\"\n                        class=\"text-button icon px-xs\">@icon('include')</button>\n                <div class=\"input-group flex flex-container-row items-center\">\n                    <input refs=\"pointer@include-input\" aria-label=\"{{ trans('entities.pages_pointer_include_tag') }}\" readonly=\"readonly\" type=\"text\" id=\"pointer-include\" placeholder=\"include\">\n                    <button refs=\"pointer@include-button\" class=\"button outline icon px-xs\" type=\"button\" title=\"{{ trans('entities.pages_copy_link') }}\">@icon('copy')</button>\n                </div>\n            </div>\n            <div>\n                @if(userCan(\\BookStack\\Permissions\\Permission::PageUpdate, $page))\n                    <a href=\"{{ $page->getUrl('/edit') }}\" id=\"pointer-edit\" data-edit-href=\"{{ $page->getUrl('/edit') }}\"\n                       class=\"button primary outline icon heading-edit-icon px-xs\" title=\"{{ trans('entities.pages_edit_content_link')}}\">@icon('edit')</a>\n                @endif\n                @if($commentTree->enabled() && userCan(\\BookStack\\Permissions\\Permission::CommentCreateAll))\n                    <button type=\"button\"\n                            refs=\"pointer@comment-button\"\n                            class=\"button primary outline icon px-xs m-none\" title=\"{{ trans('entities.comment_add')}}\">@icon('comment')</button>\n                @endif\n            </div>\n        </div>\n    </div>\n\n    <button refs=\"pointer@section-mode-button\" class=\"screen-reader-only\">{{ trans('entities.pages_pointer_enter_mode') }}</button>\n</div>\n"
  },
  {
    "path": "resources/views/pages/parts/revisions-index-row.blade.php",
    "content": "<div class=\"item-list-row flex-container-row items-center wrap\">\n    <div class=\"flex fit-content min-width-xxxxs px-m py-xs\">\n        <span class=\"hide-over-l\">{{ trans('entities.pages_revisions_number') }}</span>\n        {{ $revision->revision_number == 0 ? '' : $revision->revision_number }}\n    </div>\n    <div class=\"flex-2 px-m py-xs min-width-s\">\n        {{ $revision->name }}\n        <br>\n        <small class=\"text-muted\">(<strong class=\"hide-over-l\">{{ trans('entities.pages_revisions_editor') }}: </strong>{{ $revision->is_markdown ? 'Markdown' : 'WYSIWYG' }})</small>\n    </div>\n    <div class=\"flex-3 px-m py-xs min-width-l\">\n        <div class=\"flex-container-row items-center gap-s\">\n            @if($revision->createdBy)\n                <img class=\"avatar flex-none\" height=\"30\" width=\"30\" src=\"{{ $revision->createdBy->getAvatar(30) }}\" alt=\"{{ $revision->createdBy->name }}\">\n            @endif\n            <div>\n                @if($revision->createdBy) {{ $revision->createdBy->name }} @else {{ trans('common.deleted_user') }} @endif\n                <br>\n                <div class=\"text-muted\">\n                    <small>{{ $dates->absolute($revision->created_at) }}</small>\n                    <small>({{ $dates->relative($revision->created_at) }})</small>\n                </div>\n            </div>\n        </div>\n    </div>\n    <div class=\"flex-2 px-m py-xs min-width-m text-small\">\n        {{ $revision->summary }}\n    </div>\n    <div class=\"flex-2 px-m py-xs actions text-small text-l-right min-width-l\">\n        @if(!$oldest)\n            <a href=\"{{ $revision->getUrl('changes') }}\" target=\"_blank\" rel=\"noopener\">{{ trans('entities.pages_revisions_changes') }}</a>\n            <span class=\"text-muted opacity-70\">&nbsp;|&nbsp;</span>\n        @endif\n\n\n        @if ($current)\n            <a target=\"_blank\" rel=\"noopener\" href=\"{{ $revision->page->getUrl() }}\"><i>{{ trans('entities.pages_revisions_current') }}</i></a>\n        @else\n            <a href=\"{{ $revision->getUrl() }}\" target=\"_blank\" rel=\"noopener\">{{ trans('entities.pages_revisions_preview') }}</a>\n\n            @if(userCan(\\BookStack\\Permissions\\Permission::PageUpdate, $revision->page))\n                <span class=\"text-muted opacity-70\">&nbsp;|&nbsp;</span>\n                <div component=\"dropdown\" class=\"dropdown-container\">\n                    <a refs=\"dropdown@toggle\" href=\"#\" aria-haspopup=\"true\" aria-expanded=\"false\">{{ trans('entities.pages_revisions_restore') }}</a>\n                    <ul refs=\"dropdown@menu\" class=\"dropdown-menu\" role=\"menu\">\n                        <li class=\"px-m py-s\"><small class=\"text-muted\">{{trans('entities.revision_restore_confirm')}}</small></li>\n                        <li>\n                            <form action=\"{{ $revision->getUrl('/restore') }}\" method=\"POST\">\n                                {!! csrf_field() !!}\n                                <input type=\"hidden\" name=\"_method\" value=\"PUT\">\n                                <button type=\"submit\" class=\"text-link icon-item\">\n                                    @icon('history')\n                                    <div>{{ trans('entities.pages_revisions_restore') }}</div>\n                                </button>\n                            </form>\n                        </li>\n                    </ul>\n                </div>\n            @endif\n\n            @if(userCan(\\BookStack\\Permissions\\Permission::PageDelete, $revision->page))\n                <span class=\"text-muted opacity-70\">&nbsp;|&nbsp;</span>\n                <div component=\"dropdown\" class=\"dropdown-container\">\n                    <a refs=\"dropdown@toggle\" href=\"#\" aria-haspopup=\"true\" aria-expanded=\"false\">{{ trans('common.delete') }}</a>\n                    <ul refs=\"dropdown@menu\" class=\"dropdown-menu\" role=\"menu\">\n                        <li class=\"px-m py-s\"><small class=\"text-muted\">{{trans('entities.revision_delete_confirm')}}</small></li>\n                        <li>\n                            <form action=\"{{ $revision->getUrl('/delete/') }}\" method=\"POST\">\n                                {!! csrf_field() !!}\n                                <input type=\"hidden\" name=\"_method\" value=\"DELETE\">\n                                <button type=\"submit\" class=\"text-neg icon-item\">\n                                    @icon('delete')\n                                    <div>{{ trans('common.delete') }}</div>\n                                </button>\n                            </form>\n                        </li>\n                    </ul>\n                </div>\n            @endif\n        @endif\n    </div>\n</div>"
  },
  {
    "path": "resources/views/pages/parts/show-sidebar-section-actions.blade.php",
    "content": "<div id=\"actions\" class=\"actions mb-xl\">\n    <h5>{{ trans('common.actions') }}</h5>\n\n    <div class=\"icon-list text-link\">\n\n        {{--User Actions--}}\n        @if(userCan(\\BookStack\\Permissions\\Permission::PageUpdate, $page))\n            <a href=\"{{ $page->getUrl('/edit') }}\" data-shortcut=\"edit\" class=\"icon-list-item\">\n                <span>@icon('edit')</span>\n                <span>{{ trans('common.edit') }}</span>\n            </a>\n        @endif\n        @if(userCan(\\BookStack\\Permissions\\Permission::PageCreateAll) || userCan(\\BookStack\\Permissions\\Permission::PageCreateOwn) || userCanOnAny(\\BookStack\\Permissions\\Permission::Create, \\BookStack\\Entities\\Models\\Book::class) || userCanOnAny(\\BookStack\\Permissions\\Permission::Create, \\BookStack\\Entities\\Models\\Chapter::class))\n            <a href=\"{{ $page->getUrl('/copy') }}\" data-shortcut=\"copy\" class=\"icon-list-item\">\n                <span>@icon('copy')</span>\n                <span>{{ trans('common.copy') }}</span>\n            </a>\n        @endif\n        @if(userCan(\\BookStack\\Permissions\\Permission::PageUpdate, $page))\n            @if(userCan(\\BookStack\\Permissions\\Permission::PageDelete, $page))\n                <a href=\"{{ $page->getUrl('/move') }}\" data-shortcut=\"move\" class=\"icon-list-item\">\n                    <span>@icon('folder')</span>\n                    <span>{{ trans('common.move') }}</span>\n                </a>\n            @endif\n        @endif\n        <a href=\"{{ $page->getUrl('/revisions') }}\" data-shortcut=\"revisions\" class=\"icon-list-item\">\n            <span>@icon('history')</span>\n            <span>{{ trans('entities.revisions') }}</span>\n        </a>\n        @if(userCan(\\BookStack\\Permissions\\Permission::RestrictionsManage, $page))\n            <a href=\"{{ $page->getUrl('/permissions') }}\" data-shortcut=\"permissions\" class=\"icon-list-item\">\n                <span>@icon('lock')</span>\n                <span>{{ trans('entities.permissions') }}</span>\n            </a>\n        @endif\n        @if(userCan(\\BookStack\\Permissions\\Permission::PageDelete, $page))\n            <a href=\"{{ $page->getUrl('/delete') }}\" data-shortcut=\"delete\" class=\"icon-list-item\">\n                <span>@icon('delete')</span>\n                <span>{{ trans('common.delete') }}</span>\n            </a>\n        @endif\n\n        <hr class=\"primary-background\"/>\n\n        @if($watchOptions->canWatch() && !$watchOptions->isWatching())\n            @include('entities.watch-action', ['entity' => $page])\n        @endif\n        @if(!user()->isGuest())\n            @include('entities.favourite-action', ['entity' => $page])\n        @endif\n        @if(userCan(\\BookStack\\Permissions\\Permission::ContentExport))\n            @include('entities.export-menu', ['entity' => $page])\n        @endif\n    </div>\n\n</div>"
  },
  {
    "path": "resources/views/pages/parts/show-sidebar-section-attachments.blade.php",
    "content": "@if($page->attachments->count() > 0)\n    <div id=\"page-attachments\" class=\"mb-l\">\n        <h5>{{ trans('entities.pages_attachments') }}</h5>\n        <div class=\"body\">\n            @include('attachments.list', ['attachments' => $page->attachments])\n        </div>\n    </div>\n@endif"
  },
  {
    "path": "resources/views/pages/parts/show-sidebar-section-details.blade.php",
    "content": "<div id=\"page-details\" class=\"entity-details mb-xl\">\n    <h5>{{ trans('common.details') }}</h5>\n    <div class=\"blended-links\">\n        @include('entities.meta', ['entity' => $page, 'watchOptions' => $watchOptions])\n\n        @if($book->hasPermissions())\n            <div class=\"active-restriction\">\n                @if(userCan(\\BookStack\\Permissions\\Permission::RestrictionsManage, $book))\n                    <a href=\"{{ $book->getUrl('/permissions') }}\" class=\"entity-meta-item\">\n                        @icon('lock')\n                        <div>{{ trans('entities.books_permissions_active') }}</div>\n                    </a>\n                @else\n                    <div class=\"entity-meta-item\">\n                        @icon('lock')\n                        <div>{{ trans('entities.books_permissions_active') }}</div>\n                    </div>\n                @endif\n            </div>\n        @endif\n\n        @if($page->chapter && $page->chapter->hasPermissions())\n            <div class=\"active-restriction\">\n                @if(userCan(\\BookStack\\Permissions\\Permission::RestrictionsManage, $page->chapter))\n                    <a href=\"{{ $page->chapter->getUrl('/permissions') }}\" class=\"entity-meta-item\">\n                        @icon('lock')\n                        <div>{{ trans('entities.chapters_permissions_active') }}</div>\n                    </a>\n                @else\n                    <div class=\"entity-meta-item\">\n                        @icon('lock')\n                        <div>{{ trans('entities.chapters_permissions_active') }}</div>\n                    </div>\n                @endif\n            </div>\n        @endif\n\n        @if($page->hasPermissions())\n            <div class=\"active-restriction\">\n                @if(userCan(\\BookStack\\Permissions\\Permission::RestrictionsManage, $page))\n                    <a href=\"{{ $page->getUrl('/permissions') }}\" class=\"entity-meta-item\">\n                        @icon('lock')\n                        <div>{{ trans('entities.pages_permissions_active') }}</div>\n                    </a>\n                @else\n                    <div class=\"entity-meta-item\">\n                        @icon('lock')\n                        <div>{{ trans('entities.pages_permissions_active') }}</div>\n                    </div>\n                @endif\n            </div>\n        @endif\n\n        @if($page->template)\n            <div class=\"entity-meta-item\">\n                @icon('template')\n                <div>{{ trans('entities.pages_is_template') }}</div>\n            </div>\n        @endif\n    </div>\n</div>"
  },
  {
    "path": "resources/views/pages/parts/show-sidebar-section-page-nav.blade.php",
    "content": "@if(isset($pageNav) && count($pageNav))\n    <nav id=\"page-navigation\" class=\"mb-xl\" aria-label=\"{{ trans('entities.pages_navigation') }}\">\n        <h5>{{ trans('entities.pages_navigation') }}</h5>\n        <div class=\"body\">\n            <div class=\"sidebar-page-nav menu\">\n                @foreach($pageNav as $navItem)\n                    <li class=\"page-nav-item h{{ $navItem['level'] }}\">\n                        <a href=\"{{ $navItem['link'] }}\" class=\"text-limit-lines-1 block\">{{ $navItem['text'] }}</a>\n                        <div class=\"link-background sidebar-page-nav-bullet\"></div>\n                    </li>\n                @endforeach\n            </div>\n        </div>\n    </nav>\n@endif"
  },
  {
    "path": "resources/views/pages/parts/show-sidebar-section-tags.blade.php",
    "content": "@if($page->tags->count() > 0)\n    <section>\n        @include('entities.tag-list', ['entity' => $page])\n    </section>\n@endif"
  },
  {
    "path": "resources/views/pages/parts/template-manager-list.blade.php",
    "content": "{{ $templates->links() }}\n\n@foreach($templates as $template)\n    <div class=\"card template-item border-card p-m mb-m\" tabindex=\"0\"\n         aria-label=\"{{ trans('entities.templates_replace_content') }} - {{ $template->name }}\"\n         draggable=\"true\" template-id=\"{{ $template->id }}\">\n        <div class=\"template-item-content\" title=\"{{ trans('entities.templates_replace_content') }}\">\n            <div>{{ $template->name }}</div>\n            <div class=\"text-muted\" title=\"{{ $dates->absolute($template->updated_at) }}\">{{ trans('entities.meta_updated', ['timeLength' => $dates->relative($template->updated_at)]) }}</div>\n        </div>\n        <div class=\"template-item-actions\">\n            <button type=\"button\"\n                    title=\"{{ trans('entities.templates_prepend_content') }}\"\n                    aria-label=\"{{ trans('entities.templates_prepend_content') }} - {{ $template->name }}\"\n                    template-action=\"prepend\">@icon('chevron-up')</button>\n            <button type=\"button\"\n                    title=\"{{ trans('entities.templates_append_content') }}\"\n                    aria-label=\"{{ trans('entities.templates_append_content') }} -- {{ $template->name }}\"\n                    template-action=\"append\">@icon('chevron-down')</button>\n        </div>\n    </div>\n@endforeach\n\n{{ $templates->links() }}"
  },
  {
    "path": "resources/views/pages/parts/template-manager.blade.php",
    "content": "<div component=\"template-manager\">\n    @if(userCan(\\BookStack\\Permissions\\Permission::TemplatesManage))\n        <p class=\"text-muted small mb-none\">\n            {{ trans('entities.templates_explain_set_as_template') }}\n        </p>\n        @include('form.toggle-switch', [\n               'name' => 'template',\n               'value' => old('template', $page->template ? 'true' : 'false') === 'true',\n               'label' => trans('entities.templates_set_as_template')\n        ])\n        <hr>\n    @endif\n\n    <div class=\"search-box flexible mb-m\" style=\"display: {{ count($templates) > 0 ? 'block' : 'none' }}\">\n        <input refs=\"template-manager@searchInput\" type=\"text\" name=\"template-search\" placeholder=\"{{ trans('common.search') }}\">\n        <button refs=\"template-manager@searchButton\" tabindex=\"-1\" type=\"button\">@icon('search')</button>\n        <button refs=\"template-manager@searchCancel\" class=\"search-box-cancel text-neg\" tabindex=\"-1\" type=\"button\" style=\"display: none\">@icon('close')</button>\n    </div>\n\n    <div refs=\"template-manager@list\">\n        @include('pages.parts.template-manager-list', ['templates' => $templates])\n    </div>\n</div>"
  },
  {
    "path": "resources/views/pages/parts/toolbox-comments.blade.php",
    "content": "{{--\n$comments - CommentTree\n--}}\n<div refs=\"editor-toolbox@tab-content\" data-tab-content=\"comments\" class=\"toolbox-tab-content\">\n    <h4>{{ trans('entities.comments') }}</h4>\n\n    <div class=\"comment-container-compact px-l\">\n        <p class=\"text-muted small mb-m\">\n            {{ trans('entities.comment_editor_explain') }}\n        </p>\n        @foreach($comments->getActive() as $branch)\n            @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => true])\n        @endforeach\n        @if($comments->empty())\n            <p class=\"italic text-muted\">{{ trans('entities.comment_none') }}</p>\n        @endif\n        @if($comments->archivedThreadCount() > 0)\n            <details class=\"section-expander mt-s\">\n                <summary>{{ trans('entities.comment_archived_threads') }}</summary>\n                @foreach($comments->getArchived() as $branch)\n                    @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => true])\n                @endforeach\n            </details>\n        @endif\n    </div>\n</div>"
  },
  {
    "path": "resources/views/pages/parts/wysiwyg-editor-tinymce.blade.php",
    "content": "@push('head')\n    <script src=\"{{ versioned_asset('libs/tinymce/tinymce.min.js') }}\" nonce=\"{{ $cspNonce }}\"></script>\n@endpush\n\n<div component=\"wysiwyg-editor-tinymce\"\n     option:wysiwyg-editor-tinymce:language=\"{{ $locale->htmlLang() }}\"\n     option:wysiwyg-editor-tinymce:page-id=\"{{ $model->id ?? 0 }}\"\n     option:wysiwyg-editor-tinymce:text-direction=\"{{ $locale->htmlDirection() }}\"\n     option:wysiwyg-editor-tinymce:image-upload-error-text=\"{{ trans('errors.image_upload_error') }}\"\n     option:wysiwyg-editor-tinymce:server-upload-limit-text=\"{{ trans('errors.server_upload_limit') }}\"\n     class=\"flex-fill flex\">\n\n    <textarea id=\"html-editor\"  name=\"html\" rows=\"5\"\n          @if($errors->has('html')) class=\"text-neg\" @endif>@if(isset($model) || old('html')){{ old('html') ? old('html') : $model->html }}@endif</textarea>\n</div>\n\n@if($errors->has('html'))\n    <div class=\"text-neg text-small\">{{ $errors->first('html') }}</div>\n@endif\n\n@include('form.editor-translations')"
  },
  {
    "path": "resources/views/pages/parts/wysiwyg-editor.blade.php",
    "content": "<div component=\"wysiwyg-editor\"\n     option:wysiwyg-editor:language=\"{{ $locale->htmlLang() }}\"\n     option:wysiwyg-editor:page-id=\"{{ $model->id ?? 0 }}\"\n     option:wysiwyg-editor:text-direction=\"{{ $locale->htmlDirection() }}\"\n     option:wysiwyg-editor:image-upload-error-text=\"{{ trans('errors.image_upload_error') }}\"\n     option:wysiwyg-editor:server-upload-limit-text=\"{{ trans('errors.server_upload_limit') }}\"\n     class=\"flex-container-column flex-fill flex\">\n\n    <div class=\"editor-container flex-container-column flex-fill flex\" refs=\"wysiwyg-editor@edit-container\">\n    </div>\n\n{{--    <div id=\"lexical-debug\" style=\"white-space: pre-wrap; font-size: 12px; height: 200px; overflow-y: scroll; background-color: #000; padding: 1rem; border-radius: 4px; color: #FFF;\"></div>--}}\n\n    <textarea refs=\"wysiwyg-editor@input\" id=\"html-editor\" hidden=\"hidden\"  name=\"html\" rows=\"5\">{{ old('html') ?? $model->html ?? '' }}</textarea>\n</div>\n\n@if($errors->has('html'))\n    <div class=\"text-neg text-small\">{{ $errors->first('html') }}</div>\n@endif\n\n@include('form.editor-translations')"
  },
  {
    "path": "resources/views/pages/permissions.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                $page->book,\n                $page->chapter,\n                $page,\n                $page->getUrl('/permissions') => [\n                    'text' => trans('entities.pages_permissions'),\n                    'icon' => 'lock',\n                ]\n            ]])\n        </div>\n\n        <main class=\"card content-wrap auto-height\">\n            @include('form.entity-permissions', ['model' => $page, 'title' => trans('entities.pages_permissions')])\n        </main>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/pages/references.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                $page->book,\n                $page->chapter,\n                $page,\n                $page->getUrl('/references') => [\n                    'text' => trans('entities.references'),\n                    'icon' => 'reference',\n                ]\n            ]])\n        </div>\n\n        @include('entities.references', ['references' => $references])\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/pages/revision.blade.php",
    "content": "@extends('layouts.tri')\n\n@section('left')\n    <div id=\"revision-details\" class=\"entity-details mb-xl\">\n        <h5>{{ trans('common.details') }}</h5>\n        <div class=\"body text-small text-muted\">\n            @include('entities.meta', ['entity' => $revision, 'watchOptions' => null])\n        </div>\n    </div>\n@stop\n\n@section('body')\n\n    <div class=\"mb-m print-hidden\">\n        @include('entities.breadcrumbs', ['crumbs' => [\n            $page->$book,\n            $page->chapter,\n            $page,\n            $page->getUrl('/revisions') => [\n                'text' => trans('entities.pages_revisions'),\n                'icon' => 'history',\n            ],\n            $revision->getUrl('/changes') => $diff ? trans('entities.pages_revisions_numbered_changes', ['id' => $revision->id]) : null,\n            $revision->getUrl() => !$diff ? trans('entities.pages_revisions_numbered', ['id' => $revision->id]) : null,\n        ]])\n    </div>\n\n    <main class=\"card content-wrap\">\n        <div class=\"page-content page-revision\">\n            @include('pages.parts.page-display')\n        </div>\n    </main>\n\n@stop"
  },
  {
    "path": "resources/views/pages/revisions.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n    <div class=\"container\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                $page->book,\n                $page->chapter,\n                $page,\n                $page->getUrl('/revisions') => [\n                    'text' => trans('entities.pages_revisions'),\n                    'icon' => 'history',\n                ]\n            ]])\n        </div>\n\n        <main class=\"card content-wrap\">\n            <h1 class=\"list-heading\">{{ trans('entities.pages_revisions') }}</h1>\n\n            <p class=\"text-muted\">{{ trans('entities.pages_revisions_desc') }}</p>\n\n            <div class=\"flex-container-row my-m items-center justify-space-between wrap gap-x-m gap-y-s\">\n                {{ $revisions->links() }}\n                <div>\n                    @include('common.sort', $listOptions->getSortControlData())\n                </div>\n            </div>\n\n            @if(count($revisions) > 0)\n                <div class=\"item-list\">\n                    <div class=\"item-list-row flex-container-row items-center strong hide-under-l\">\n                        <div class=\"flex fit-content min-width-xxxxs px-m py-xs\">{{ trans('entities.pages_revisions_number') }}</div>\n                        <div class=\"flex-2 px-m py-xs\">{{ trans('entities.pages_name') }} / {{ trans('entities.pages_revisions_editor') }}</div>\n                        <div class=\"flex-3 px-m py-xs\">{{ trans('entities.pages_revisions_created_by') }} / {{ trans('entities.pages_revisions_date') }}</div>\n                        <div class=\"flex-2 px-m py-xs\">{{ trans('entities.pages_revisions_changelog') }}</div>\n                        <div class=\"flex-2 px-m py-xs text-right\">{{ trans('common.actions') }}</div>\n                    </div>\n                    @foreach($revisions as $index => $revision)\n                        @include('pages.parts.revisions-index-row', [\n                                'revision' => $revision,\n                                'current' => $page->revision_count === $revision->revision_number,\n                                'oldest' => $oldestRevisionId === $revision->id,\n                            ])\n                    @endforeach\n                </div>\n            @else\n                <p>{{ trans('entities.pages_revisions_none') }}</p>\n            @endif\n\n            <div class=\"my-m\">\n                {{ $revisions->links() }}\n            </div>\n        </main>\n\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/pages/show.blade.php",
    "content": "@extends('layouts.tri')\n\n@push('social-meta')\n    <meta property=\"og:description\" content=\"{{ Str::limit($page->text, 100, '...') }}\">\n@endpush\n\n@include('entities.body-tag-classes', ['entity' => $page])\n\n@section('body')\n\n    <div class=\"mb-m print-hidden\">\n        @include('entities.breadcrumbs', ['crumbs' => [\n            $page->book,\n            $page->hasChapter() ? $page->chapter : null,\n            $page,\n        ]])\n    </div>\n\n    <main class=\"content-wrap card\">\n        <div component=\"page-display\"\n             option:page-display:page-id=\"{{ $page->id }}\"\n             class=\"page-content clearfix\">\n            @include('pages.parts.page-display')\n        </div>\n        @include('pages.parts.pointer', ['page' => $page, 'commentTree' => $commentTree])\n    </main>\n\n    @include('entities.sibling-navigation', ['next' => $next, 'previous' => $previous])\n\n    @if ($commentTree->enabled())\n        <div class=\"comments-container mb-l print-hidden\">\n            @include('comments.comments', ['commentTree' => $commentTree, 'page' => $page])\n            <div class=\"clearfix\"></div>\n        </div>\n    @endif\n@stop\n\n@section('left')\n    @include('pages.parts.show-sidebar-section-tags', ['page' => $page])\n    @include('pages.parts.show-sidebar-section-attachments', ['page' => $page])\n    @include('pages.parts.show-sidebar-section-page-nav', ['pageNav' => $pageNav])\n    @include('entities.book-tree', ['book' => $book, 'sidebarTree' => $sidebarTree])\n@stop\n\n@section('right')\n    @include('pages.parts.show-sidebar-section-details', ['page' => $page, 'watchOptions' => $watchOptions, 'book' => $book])\n    @include('pages.parts.show-sidebar-section-actions', ['page' => $page, 'watchOptions' => $watchOptions])\n@stop\n"
  },
  {
    "path": "resources/views/readme.md",
    "content": "# BookStack Views\n\nAll views within this folder are [Laravel blade](https://laravel.com/docs/6.x/blade) views.\n\n### Overriding\n\nViews can be overridden on a per-file basis via the visual theme system.\nMore information on this can be found within the `dev/docs/visual-theme-system.md`\nfile within this project.\n\n### Convention\n\nViews are broken down into rough domain areas. These aren't too strict although many of the folders\nhere will often match up to a HTTP controller. \n\nWithin each folder views will be structured like so:\n\n```txt\n- folder/\n    - page-a.blade.php\n    - page-b.blade.php\n    - parts/\n        - partial-a.blade.php\n        - partial-b.blade.php\n    - subdomain/\n        - subdomain-page-a.blade.php\n        - subdomain-page-b.blade.php\n        - parts/\n            - subdomain-partial-a.blade.php\n            - subdomain-partial-b.blade.php\n```\n\nIf a folder contains no pages at all (For example: `attachments`, `form`) and only partials, then \nthe partials can be within the top-level folder instead of pages to prevent unneeded nesting.\n\nIf a partial depends on another partial within the same directory, the naming of the child partials should be an extension of the parent.\nFor example:\n\n```txt\n- tag-manager.blade.php\n- tag-manager-list.blade.php\n- tag-manager-input.blade.php\n```"
  },
  {
    "path": "resources/views/search/all.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n    <div class=\"container mt-xl\" id=\"search-system\">\n\n        <div class=\"grid right-focus reverse-collapse gap-xl\">\n            <div>\n                <div>\n                    <h5>{{ trans('entities.search_advanced') }}</h5>\n\n                    @php\n                        $filterMap = $options->filters->nonNegated()->toValueMap();\n                    @endphp\n                    <form method=\"get\" action=\"{{ url('/search') }}\">\n                        <h6>{{ trans('entities.search_terms') }}</h6>\n                        <input type=\"text\" name=\"search\" value=\"{{ implode(' ', $options->searches->toValueArray()) }}\">\n\n                        <h6>{{ trans('entities.search_content_type') }}</h6>\n                        <div class=\"form-group\">\n\n                            <?php\n                            $types = explode('|', $filterMap['type'] ?? '');\n                            $hasTypes = $types[0] !== '';\n                            ?>\n                            @include('search.parts.type-filter', ['checked' => !$hasTypes || in_array('page', $types), 'entity' => 'page', 'transKey' => 'page'])\n                            @include('search.parts.type-filter', ['checked' => !$hasTypes || in_array('chapter', $types), 'entity' => 'chapter', 'transKey' => 'chapter'])\n                            <br>\n                            @include('search.parts.type-filter', ['checked' => !$hasTypes || in_array('book', $types), 'entity' => 'book', 'transKey' => 'book'])\n                            @include('search.parts.type-filter', ['checked' => !$hasTypes || in_array('bookshelf', $types), 'entity' => 'bookshelf', 'transKey' => 'shelf'])\n                        </div>\n\n                        <h6>{{ trans('entities.search_exact_matches') }}</h6>\n                        @include('search.parts.term-list', ['type' => 'exact', 'currentList' => $options->exacts->nonNegated()->toValueArray()])\n\n                        <h6>{{ trans('entities.search_tags') }}</h6>\n                        @include('search.parts.term-list', ['type' => 'tags', 'currentList' => $options->tags->nonNegated()->toValueArray()])\n\n                        @if(!user()->isGuest())\n                            <h6>{{ trans('entities.search_options') }}</h6>\n\n                            @component('search.parts.boolean-filter', ['filters' => $filterMap, 'name' => 'viewed_by_me', 'value' => null])\n                                {{ trans('entities.search_viewed_by_me') }}\n                            @endcomponent\n                            @component('search.parts.boolean-filter', ['filters' => $filterMap, 'name' => 'not_viewed_by_me', 'value' => null])\n                                {{ trans('entities.search_not_viewed_by_me') }}\n                            @endcomponent\n                            @component('search.parts.boolean-filter', ['filters' => $filterMap, 'name' => 'is_restricted', 'value' => null])\n                                {{ trans('entities.search_permissions_set') }}\n                            @endcomponent\n                            @component('search.parts.boolean-filter', ['filters' => $filterMap, 'name' => 'created_by', 'value' => 'me'])\n                                {{ trans('entities.search_created_by_me') }}\n                            @endcomponent\n                            @component('search.parts.boolean-filter', ['filters' => $filterMap, 'name' => 'updated_by', 'value' => 'me'])\n                                {{ trans('entities.search_updated_by_me') }}\n                            @endcomponent\n                            @component('search.parts.boolean-filter', ['filters' => $filterMap, 'name' => 'owned_by', 'value' => 'me'])\n                                {{ trans('entities.search_owned_by_me') }}\n                            @endcomponent\n                        @endif\n\n                        <h6>{{ trans('entities.search_date_options') }}</h6>\n                        @include('search.parts.date-filter', ['name' => 'updated_after', 'filters' => $filterMap])\n                        @include('search.parts.date-filter', ['name' => 'updated_before', 'filters' => $filterMap])\n                        @include('search.parts.date-filter', ['name' => 'created_after', 'filters' => $filterMap])\n                        @include('search.parts.date-filter', ['name' => 'created_before', 'filters' => $filterMap])\n\n                        <input type=\"hidden\" name=\"extras\" value=\"{{ $options->getAdditionalOptionsString() }}\">\n                        <button type=\"submit\" class=\"button\">{{ trans('entities.search_update') }}</button>\n                    </form>\n\n                </div>\n            </div>\n            <div>\n                <div class=\"card content-wrap\">\n                    <h1 class=\"list-heading\">{{ trans('entities.search_results') }}</h1>\n\n                    <form action=\"{{ url('/search') }}\" method=\"GET\" class=\"search-box flexible hide-over-l\">\n                        <input value=\"{{$searchTerm}}\" type=\"text\" name=\"term\"\n                               placeholder=\"{{ trans('common.search') }}\">\n                        <button type=\"submit\"\n                                aria-label=\"{{ trans('common.search') }}\"\n                                tabindex=\"-1\">@icon('search')</button>\n                    </form>\n\n                    <h6 class=\"text-muted\">{{ trans_choice('entities.search_total_results_found', $totalResults, ['count' => $totalResults]) }}</h6>\n                    <div class=\"book-contents\">\n                        @include('entities.list', ['entities' => $entities, 'showPath' => true, 'showTags' => true])\n                    </div>\n\n                    {{ $paginator->render() }}\n                </div>\n            </div>\n        </div>\n\n    </div>\n@stop\n"
  },
  {
    "path": "resources/views/search/parts/boolean-filter.blade.php",
    "content": "{{--\n$filters - Array of search filter values\n$name - Name of filter to limit use.\n$value - Value of filter to use\n--}}\n<label class=\"checkbox\">\n    <input type=\"checkbox\"\n           name=\"filters[{{ $name }}]\"\n           @if (isset($filters[$name]) && (!$value || ($value && $value === $filters[$name]))) checked=\"checked\" @endif\n           value=\"{{ $value ?: 'true' }}\">\n    {{ $slot }}\n</label>"
  },
  {
    "path": "resources/views/search/parts/date-filter.blade.php",
    "content": "{{--\n@filters - Active search filters\n@name - Name of filter\n--}}\n<table class=\"no-style form-table mb-xs\">\n    <tr>\n        <td width=\"200\">{{ trans('entities.search_' . $name) }}</td>\n        <td width=\"80\"></td>\n    </tr>\n    <tr component=\"optional-input\">\n        <td>\n            <button type=\"button\" refs=\"optional-input@show\"\n                    class=\"text-button {{ ($filters[$name] ?? false) ? 'hidden' : '' }}\">{{ trans('entities.search_set_date') }}</button>\n            <input class=\"tag-input {{ ($filters[$name] ?? false) ? '' : 'hidden' }}\"\n                   refs=\"optional-input@input\"\n                   value=\"{{ $filters[$name] ?? '' }}\"\n                   type=\"date\"\n                   name=\"filters[{{ $name }}]\"\n                   pattern=\"[0-9]{4}-[0-9]{2}-[0-9]{2}\">\n        </td>\n        <td>\n            <button type=\"button\"\n                    refs=\"optional-input@remove\"\n                    class=\"text-neg text-button {{ ($filters[$name] ?? false) ? '' : 'hidden' }}\">\n                @icon('close')\n            </button>\n        </td>\n    </tr>\n</table>"
  },
  {
    "path": "resources/views/search/parts/entity-selector-list.blade.php",
    "content": "<div class=\"entity-list\">\n    @if(count($entities) > 0)\n        @foreach($entities as $index => $entity)\n\n            @include('entities.list-item', [\n            'entity' => $entity,\n            'showPath' => true,\n            'locked' => $permission !== 'view' && !userCan($permission, $entity)\n            ])\n        \n            @if($index !== count($entities) - 1)\n                <hr>\n            @endif\n\n        @endforeach\n    @else\n        <p class=\"text-muted text-large p-xl\">\n            {{ trans('common.no_items') }}\n        </p>\n    @endif\n</div>"
  },
  {
    "path": "resources/views/search/parts/entity-suggestion-list.blade.php",
    "content": "<div class=\"entity-list\">\n    @if(count($entities) > 0)\n        @foreach($entities as $index => $entity)\n\n            @include('entities.list-item', [\n                'entity' => $entity,\n                'showPath' => true,\n                'locked' => false,\n            ])\n        \n            @if($index !== count($entities) - 1)\n                <hr>\n            @endif\n\n        @endforeach\n    @else\n        <div class=\"text-muted px-m py-m\">\n            {{ trans('common.no_items') }}\n        </div>\n    @endif\n</div>"
  },
  {
    "path": "resources/views/search/parts/term-list.blade.php",
    "content": "{{--\n@type - Type of term (exact, tag)\n@currentList\n--}}\n<div component=\"add-remove-rows\"\n       option:add-remove-rows:remove-selector=\"button.text-neg\"\n       option:add-remove-rows:row-selector=\".flex-container-row\"\n        class=\"flex-container-column gap-xs\">\n    @foreach(array_merge($currentList, ['']) as $term)\n        <div @if(empty($term)) refs=\"add-remove-rows@model\" @endif\n            class=\"{{ $term ? '' : 'hidden' }} flex-container-row items-center gap-x-xs\">\n            <div>\n                <input class=\"exact-input outline\" type=\"text\" name=\"{{$type}}[]\" value=\"{{ $term }}\">\n            </div>\n            <div>\n                <button type=\"button\" class=\"text-neg text-button icon-button p-xs\">@icon('close')</button>\n            </div>\n        </div>\n    @endforeach\n    <div class=\"flex py-xs\">\n        <button refs=\"add-remove-rows@add\" type=\"button\" class=\"text-button\">\n            @icon('add-circle'){{ trans('common.add') }}\n        </button>\n    </div>\n</div>"
  },
  {
    "path": "resources/views/search/parts/type-filter.blade.php",
    "content": "{{--\n@checked - If the option should be pre-checked\n@entity - Entity Name\n@transKey - Translation Key\n--}}\n<label class=\"inline checkbox text-{{$entity}}\">\n    <input type=\"checkbox\" name=\"types[]\"\n           @if($checked) checked @endif\n           value=\"{{$entity}}\">{{ trans('entities.' . $transKey) }}\n</label>"
  },
  {
    "path": "resources/views/settings/audit.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n    <div class=\"container\">\n\n        @include('settings.parts.navbar', ['selected' => 'audit'])\n\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ trans('settings.audit') }}</h1>\n            <p class=\"text-muted\">{{ trans('settings.audit_desc') }}</p>\n\n            <form action=\"{{ url('/settings/audit') }}\" method=\"get\"\n                  class=\"flex-container-row wrap justify-flex-start gap-x-m gap-y-xs\">\n\n                @foreach(request()->only(['order', 'sort']) as $key => $val)\n                    <input type=\"hidden\" name=\"{{ $key }}\" value=\"{{ $val }}\">\n                @endforeach\n\n                <div component=\"dropdown\" class=\"list-sort-type dropdown-container relative\">\n                    <label for=\"\">{{ trans('settings.audit_event_filter') }}</label>\n                    <button refs=\"dropdown@toggle\"\n                            type=\"button\"\n                            aria-haspopup=\"true\"\n                            aria-expanded=\"false\"\n                            aria-label=\"{{ trans('common.sort_options') }}\"\n                            class=\"input-base text-left\">{{ $filters['event'] ?: trans('settings.audit_event_filter_no_filter') }}</button>\n                    <ul refs=\"dropdown@menu\" class=\"dropdown-menu\">\n                        <li @if($filters['event'] === '') class=\"active\" @endif><a\n                                    href=\"{{ $filterSortUrl->withOverrideData(['event' => ''])->build() }}\"\n                                    class=\"text-item\">{{ trans('settings.audit_event_filter_no_filter') }}</a></li>\n                        @foreach($activityTypes as $type)\n                            <li @if($type === $filters['event']) class=\"active\" @endif><a\n                                        href=\"{{ $filterSortUrl->withOverrideData(['event' => $type])->build() }}\"\n                                        class=\"text-item\">{{ $type }}</a></li>\n                        @endforeach\n                    </ul>\n                </div>\n\n                @if(!empty($filters['event']))\n                    <input type=\"hidden\" name=\"event\" value=\"{{ $filters['event'] }}\">\n                @endif\n\n                @foreach(['date_from', 'date_to'] as $filterKey)\n                    <div class=>\n                        <label for=\"audit_filter_{{ $filterKey }}\">{{ trans('settings.audit_' . $filterKey) }}</label>\n                        <input id=\"audit_filter_{{ $filterKey }}\"\n                               component=\"submit-on-change\"\n                               type=\"date\"\n                               name=\"{{ $filterKey }}\"\n                               value=\"{{ $filters[$filterKey] ?? '' }}\">\n                    </div>\n                @endforeach\n\n                <div class=\"form-group\"\n                     component=\"submit-on-change\"\n                     option:submit-on-change:filter='[name=\"user\"]'>\n                    <label for=\"owner\">{{ trans('settings.audit_table_user') }}</label>\n                    @include('form.user-select', ['user' => $filters['user'] ? \\BookStack\\Users\\Models\\User::query()->find($filters['user']) : null, 'name' => 'user'])\n                </div>\n\n\n                <div class=\"form-group\">\n                    <label for=\"ip\">{{ trans('settings.audit_table_ip') }}</label>\n                    @include('form.text', ['name' => 'ip', 'model' => (object) $filters])\n                    <input type=\"submit\" style=\"display: none\">\n                </div>\n            </form>\n\n            <hr class=\"mt-m mb-s\">\n\n            <div class=\"flex-container-row justify-space-between items-center wrap\">\n                <div class=\"flex-2 min-width-xl\">{{ $activities->links() }}</div>\n                <div class=\"flex-none min-width-m py-m\">\n                    @include('common.sort', array_merge($listOptions->getSortControlData(), ['useQuery' => true]))\n                </div>\n            </div>\n\n            <div class=\"item-list\">\n                <div class=\"item-list-row flex-container-row items-center bold hide-under-m\">\n                    <div class=\"flex-2 px-m py-xs flex-container-row items-center\">{{ trans('settings.audit_table_user') }}</div>\n                    <div class=\"flex-2 px-m py-xs\">{{ trans('settings.audit_table_event') }}</div>\n                    <div class=\"flex-3 px-m py-xs\">{{ trans('settings.audit_table_related') }}</div>\n                    <div class=\"flex-container-row flex-3\">\n                        <div class=\"flex px-m py-xs\">{{ trans('settings.audit_table_ip') }}</div>\n                        <div class=\"flex-2 px-m py-xs text-right\">{{ trans('settings.audit_table_date') }}</div>\n                    </div>\n                </div>\n                @foreach($activities as $activity)\n                    <div class=\"item-list-row flex-container-row items-center wrap py-xxs\">\n                        <div class=\"flex-2 px-m py-xxs flex-container-row items-center min-width-m\">\n                            @if($activity->user && $activity->user->created_at <= $activity->created_at)\n                                @include('settings.parts.table-user', ['user' => $activity->user])\n                            @else\n                                [ID: {{ $activity->user_id }}] {{ trans('common.deleted_user') }}\n                            @endif\n                        </div>\n                        <div class=\"flex-2 px-m py-xxs min-width-m\"><strong\n                                    class=\"mr-xs hide-over-m\">{{ trans('settings.audit_table_event') }}\n                                :</strong> {{ $activity->type }}</div>\n                        <div class=\"flex-3 px-m py-xxs min-width-l\">\n                            @if($activity->loggable instanceof \\BookStack\\Entities\\Models\\Entity)\n                                @include('entities.icon-link', ['entity' => $activity->loggable])\n                            @elseif($activity->detail && $activity->isForEntity())\n                                <div>\n                                    {{ trans('settings.audit_deleted_item') }} <br>\n                                    {{ trans('settings.audit_deleted_item_name', ['name' => $activity->detail]) }}\n                                </div>\n                            @elseif($activity->detail)\n                                <div>{{ $activity->detail }}</div>\n                            @endif\n                        </div>\n                        <div class=\"flex-container-row flex-3 min-width-m\">\n                            <div class=\"flex-2 px-m py-xxs min-width-xs break-text\"><strong\n                                        class=\"mr-xs hide-over-m\">{{ trans('settings.audit_table_ip') }}\n                                    :<br></strong> {{ $activity->ip }}</div>\n                            <div class=\"flex-3 px-m py-xxs text-m-right min-width-xs\"><strong\n                                        class=\"mr-xs hide-over-m\">{{ trans('settings.audit_table_date') }}\n                                    :<br></strong> {{ $activity->created_at }}</div>\n                        </div>\n                    </div>\n                @endforeach\n            </div>\n\n            <div class=\"py-m\">\n                {{ $activities->links() }}\n            </div>\n        </div>\n\n    </div>\n@stop\n"
  },
  {
    "path": "resources/views/settings/categories/customization.blade.php",
    "content": "@extends('settings.layout')\n\n@section('card')\n    <h1 id=\"customization\" class=\"list-heading\">{{ trans('settings.app_customization') }}</h1>\n    <form action=\"{{ url(\"/settings/customization\") }}\" method=\"POST\" enctype=\"multipart/form-data\">\n        {{ csrf_field() }}\n        <input type=\"hidden\" name=\"section\" value=\"customization\">\n\n        <div class=\"setting-list\">\n\n            <div class=\"grid half gap-xl\">\n                <div>\n                    <label for=\"setting-app-name\" class=\"setting-list-label\">{{ trans('settings.app_name') }}</label>\n                    <p class=\"small\">{{ trans('settings.app_name_desc') }}</p>\n                </div>\n                <div class=\"pt-xs\">\n                    <input type=\"text\" value=\"{{ setting('app-name', 'BookStack') }}\" name=\"setting-app-name\" id=\"setting-app-name\">\n                    @include('form.toggle-switch', [\n                        'name' => 'setting-app-name-header',\n                        'value' => setting('app-name-header'),\n                        'label' => trans('settings.app_name_header'),\n                    ])\n                </div>\n            </div>\n\n            <div class=\"grid half gap-xl items-center\">\n                <div>\n                    <label class=\"setting-list-label\" for=\"setting-app-editor\">{{ trans('settings.app_default_editor') }}</label>\n                    <p class=\"small\">{{ trans('settings.app_default_editor_desc') }}</p>\n                </div>\n                <div>\n                    <select name=\"setting-app-editor\" id=\"setting-app-editor\">\n                        <option @if(setting('app-editor') === 'wysiwyg') selected @endif value=\"wysiwyg\">WYSIWYG</option>\n                        <option @if(setting('app-editor') === 'markdown') selected @endif value=\"markdown\">Markdown</option>\n                        <option @if(setting('app-editor') === 'wysiwyg2024') selected @endif value=\"wysiwyg2024\">New WYSIWYG (beta testing)</option>\n                    </select>\n                </div>\n            </div>\n\n            <div class=\"grid half gap-xl\">\n                <div>\n                    <label class=\"setting-list-label\">{{ trans('settings.app_logo') }}</label>\n                    <p class=\"small\">{!! trans('settings.app_logo_desc') !!}</p>\n                </div>\n                <div class=\"pt-xs\">\n                    @include('form.image-picker', [\n                             'removeName' => 'setting-app-logo',\n                             'removeValue' => 'none',\n                             'defaultImage' => url('/logo.png'),\n                             'currentImage' => setting('app-logo'),\n                             'name' => 'app_logo',\n                             'imageClass' => 'logo-image',\n                         ])\n                </div>\n            </div>\n\n            <div class=\"grid half gap-xl\">\n                <div>\n                    <label class=\"setting-list-label\">{{ trans('settings.app_icon') }}</label>\n                    <p class=\"small\">{{ trans('settings.app_icon_desc') }}</p>\n                </div>\n                <div class=\"pt-xs\">\n                    @include('form.image-picker', [\n                             'removeValue' => 'none',\n                             'defaultImage' => url('/icon.png'),\n                             'currentImage' => setting('app-icon'),\n                             'name' => 'app_icon',\n                             'imageClass' => 'logo-image',\n                         ])\n                </div>\n            </div>\n\n            <!-- App Color Scheme -->\n            @php\n                $darkMode = boolval(setting()->getForCurrentUser('dark-mode-enabled'));\n            @endphp\n            <div component=\"setting-app-color-scheme\"\n                 option:setting-app-color-scheme:mode=\"{{ $darkMode ? 'dark' : 'light' }}\"\n                 class=\"pb-l\">\n                <div class=\"mb-l\">\n                    <label class=\"setting-list-label\">{{ trans('settings.color_scheme') }}</label>\n                    <p class=\"small\">{{ trans('settings.color_scheme_desc') }}</p>\n                </div>\n\n                <div component=\"tabs\" class=\"tab-container\">\n                    <div role=\"tablist\" class=\"controls-card\">\n                        <button type=\"button\"\n                                role=\"tab\"\n                                id=\"color-scheme-tab-light\"\n                                aria-selected=\"{{ $darkMode ? 'false' : 'true' }}\"\n                                aria-controls=\"color-scheme-panel-light\">@icon('light-mode'){{ trans('common.light_mode') }}</button>\n                        <button type=\"button\"\n                                role=\"tab\"\n                                id=\"color-scheme-tab-dark\"\n                                aria-selected=\"{{ $darkMode ? 'true' : 'false' }}\"\n                                aria-controls=\"color-scheme-panel-dark\">@icon('dark-mode'){{ trans('common.dark_mode') }}</button>\n                    </div>\n                    <div class=\"sub-card\">\n                        <div id=\"color-scheme-panel-light\"\n                             refs=\"setting-app-color-scheme@lightContainer\"\n                             tabindex=\"0\"\n                             role=\"tabpanel\"\n                             aria-labelledby=\"color-scheme-tab-light\"\n                             @if($darkMode) hidden @endif\n                             class=\"p-m\">\n                            @include('settings.parts.setting-color-scheme', ['mode' => 'light'])\n                        </div>\n                        <div id=\"color-scheme-panel-dark\"\n                             refs=\"setting-app-color-scheme@darkContainer\"\n                             tabindex=\"0\"\n                             role=\"tabpanel\"\n                             aria-labelledby=\"color-scheme-tab-light\"\n                             @if(!$darkMode) hidden @endif\n                             class=\"p-m\">\n                            @include('settings.parts.setting-color-scheme', ['mode' => 'dark'])\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <div component=\"setting-homepage-control\" id=\"homepage-control\" class=\"grid half gap-xl items-center\">\n                <div>\n                    <label for=\"setting-app-homepage-type\" class=\"setting-list-label\">{{ trans('settings.app_homepage') }}</label>\n                    <p class=\"small\">{{ trans('settings.app_homepage_desc') }}</p>\n                </div>\n                <div>\n                    <select refs=\"setting-homepage-control@type-control\"\n                            name=\"setting-app-homepage-type\"\n                            id=\"setting-app-homepage-type\">\n                        <option @if(setting('app-homepage-type') === 'default') selected @endif value=\"default\">{{ trans('common.default') }}</option>\n                        <option @if(setting('app-homepage-type') === 'books') selected @endif value=\"books\">{{ trans('entities.books') }}</option>\n                        <option @if(setting('app-homepage-type') === 'bookshelves') selected @endif value=\"bookshelves\">{{ trans('entities.shelves') }}</option>\n                        <option @if(setting('app-homepage-type') === 'page') selected @endif value=\"page\">{{ trans('entities.pages_specific') }}</option>\n                    </select>\n\n                    <div refs=\"setting-homepage-control@page-picker-container\" style=\"display: none;\" class=\"mt-m\">\n                        @include('form.page-picker', [\n                            'name' => 'setting-app-homepage',\n                            'placeholder' => trans('settings.app_homepage_select'),\n                            'value' => setting('app-homepage'),\n                            'selectorEndpoint' => '/search/entity-selector',\n                        ])\n                    </div>\n                </div>\n            </div>\n\n            <div>\n                <label for=\"setting-app-privacy-link\" class=\"setting-list-label\">{{ trans('settings.app_footer_links') }}</label>\n                <p class=\"small mb-m\">{{ trans('settings.app_footer_links_desc') }}</p>\n                @include('settings.parts.footer-links', ['name' => 'setting-app-footer-links', 'value' => setting('app-footer-links', [])])\n            </div>\n\n\n            <div>\n                <label for=\"setting-app-custom-head\" class=\"setting-list-label\">{{ trans('settings.app_custom_html') }}</label>\n                <p class=\"small\">{{ trans('settings.app_custom_html_desc') }}</p>\n                <div class=\"mt-m\">\n                    <textarea component=\"code-textarea\"\n                              option:code-textarea:mode=\"html\"\n                              name=\"setting-app-custom-head\"\n                              id=\"setting-app-custom-head\"\n                              class=\"simple-code-input\">{{ setting('app-custom-head', '') }}</textarea>\n                </div>\n                <p class=\"small text-right\">{{ trans('settings.app_custom_html_disabled_notice') }}</p>\n            </div>\n\n\n        </div>\n\n        <div class=\"form-group text-right\">\n            <button type=\"submit\" class=\"button\">{{ trans('settings.settings_save') }}</button>\n        </div>\n    </form>\n@endsection\n\n@section('after-content')\n    @include('entities.selector-popup')\n@endsection\n"
  },
  {
    "path": "resources/views/settings/categories/features.blade.php",
    "content": "@extends('settings.layout')\n\n@section('card')\n    <h1 id=\"features\" class=\"list-heading\">{{ trans('settings.app_features_security') }}</h1>\n    <form action=\"{{ url(\"/settings/features\") }}\" method=\"POST\">\n        {!! csrf_field() !!}\n        <input type=\"hidden\" name=\"section\" value=\"features\">\n\n        <div class=\"setting-list\">\n\n\n            <div class=\"grid half gap-xl\">\n                <div>\n                    <label for=\"setting-app-public\" class=\"setting-list-label\">{{ trans('settings.app_public_access') }}</label>\n                    <p class=\"small\">{!! trans('settings.app_public_access_desc') !!}</p>\n                    @if(userCan(\\BookStack\\Permissions\\Permission::UsersManage))\n                        <p class=\"small mb-none\">\n                            <a href=\"{{ url($guestUser->getEditUrl()) }}\">{!! trans('settings.app_public_access_desc_guest') !!}</a>\n                        </p>\n                    @endif\n                </div>\n                <div>\n                    @include('form.toggle-switch', [\n                        'name' => 'setting-app-public',\n                        'value' => setting('app-public'),\n                        'label' => trans('settings.app_public_access_toggle'),\n                    ])\n                </div>\n            </div>\n\n            <div class=\"grid half gap-xl\">\n                <div>\n                    <label class=\"setting-list-label\">{{ trans('settings.app_secure_images') }}</label>\n                    <p class=\"small\">{{ trans('settings.app_secure_images_desc') }}</p>\n                </div>\n                <div>\n                    @include('form.toggle-switch', [\n                        'name' => 'setting-app-secure-images',\n                        'value' => setting('app-secure-images'),\n                        'label' => trans('settings.app_secure_images_toggle'),\n                    ])\n                </div>\n            </div>\n\n            <div class=\"grid half gap-xl\">\n                <div>\n                    <label class=\"setting-list-label\">{{ trans('settings.app_disable_comments') }}</label>\n                    <p class=\"small\">{!! trans('settings.app_disable_comments_desc') !!}</p>\n                </div>\n                <div>\n                    @include('form.toggle-switch', [\n                        'name' => 'setting-app-disable-comments',\n                        'value' => setting('app-disable-comments'),\n                        'label' => trans('settings.app_disable_comments_toggle'),\n                    ])\n                </div>\n            </div>\n\n\n        </div>\n\n        <div class=\"form-group text-right\">\n            <button type=\"submit\" class=\"button\">{{ trans('settings.settings_save') }}</button>\n        </div>\n    </form>\n@endsection"
  },
  {
    "path": "resources/views/settings/categories/registration.blade.php",
    "content": "@extends('settings.layout')\n\n@section('card')\n    <h1 id=\"registration\" class=\"list-heading\">{{ trans('settings.reg_settings') }}</h1>\n    <form action=\"{{ url(\"/settings/registration\") }}\" method=\"POST\">\n        {!! csrf_field() !!}\n        <input type=\"hidden\" name=\"section\" value=\"registration\">\n\n        <div class=\"setting-list\">\n            <div class=\"grid half gap-xl\">\n                <div>\n                    <label class=\"setting-list-label\">{{ trans('settings.reg_enable') }}</label>\n                    <p class=\"small\">{!! trans('settings.reg_enable_desc') !!}</p>\n                </div>\n                <div>\n                    @include('form.toggle-switch', [\n                        'name' => 'setting-registration-enabled',\n                        'value' => setting('registration-enabled'),\n                        'label' => trans('settings.reg_enable_toggle')\n                    ])\n\n                    @if(in_array(config('auth.method'), ['ldap', 'saml2', 'oidc']))\n                        <div class=\"text-warn text-small mb-l\">{{ trans('settings.reg_enable_external_warning') }}</div>\n                    @endif\n\n                    <label for=\"setting-registration-role\">{{ trans('settings.reg_default_role') }}</label>\n                    <select id=\"setting-registration-role\" name=\"setting-registration-role\"\n                            @if($errors->has('setting-registration-role')) class=\"neg\" @endif>\n                        <option value=\"0\" @if(intval(setting('registration-role', '0')) === 0) selected @endif>\n                            -- {{ trans('common.none') }} --\n                        </option>\n                        @foreach(\\BookStack\\Users\\Models\\Role::all() as $role)\n                            <option value=\"{{$role->id}}\"\n                                    data-system-role-name=\"{{ $role->system_name ?? '' }}\"\n                                    @if(intval(setting('registration-role', '0')) === $role->id) selected @endif\n                            >\n                                {{ $role->display_name }}\n                            </option>\n                        @endforeach\n                    </select>\n                </div>\n            </div>\n\n            <div class=\"grid half gap-xl\">\n                <div>\n                    <label for=\"setting-registration-restrict\"\n                           class=\"setting-list-label\">{{ trans('settings.reg_confirm_restrict_domain') }}</label>\n                    <p class=\"small\">{!! trans('settings.reg_confirm_restrict_domain_desc') !!}</p>\n                </div>\n                <div class=\"pt-xs\">\n                    <input type=\"text\" id=\"setting-registration-restrict\" name=\"setting-registration-restrict\"\n                           placeholder=\"{{ trans('settings.reg_confirm_restrict_domain_placeholder') }}\"\n                           value=\"{{ setting('registration-restrict', '') }}\">\n                </div>\n            </div>\n\n            <div class=\"grid half gap-xl\">\n                <div>\n                    <label class=\"setting-list-label\">{{ trans('settings.reg_email_confirmation') }}</label>\n                    <p class=\"small\">{{ trans('settings.reg_confirm_email_desc') }}</p>\n                </div>\n                <div>\n                    @include('form.toggle-switch', [\n                        'name' => 'setting-registration-confirmation',\n                        'value' => setting('registration-confirmation'),\n                        'label' => trans('settings.reg_email_confirmation_toggle')\n                    ])\n                </div>\n            </div>\n\n        </div>\n\n        <div class=\"form-group text-right\">\n            <button type=\"submit\" class=\"button\">{{ trans('settings.settings_save') }}</button>\n        </div>\n    </form>\n@endsection"
  },
  {
    "path": "resources/views/settings/categories/sorting.blade.php",
    "content": "@extends('settings.layout')\n\n@php\n    $sortRules = \\BookStack\\Sorting\\SortRule::allByName();\n@endphp\n\n@section('card')\n    <h1 id=\"sorting\" class=\"list-heading\">{{ trans('settings.sorting') }}</h1>\n    <form action=\"{{ url(\"/settings/sorting\") }}\" method=\"POST\">\n        {{ csrf_field() }}\n        <input type=\"hidden\" name=\"section\" value=\"sorting\">\n\n        <div class=\"setting-list\">\n            <div>\n                <div class=\"mb-m\">\n                    <label class=\"setting-list-label\">{{ trans('settings.sorting_page_limits') }}</label>\n                    <p class=\"small\">{{ trans('settings.sorting_page_limits_desc') }}</p>\n                </div>\n                <div class=\"flex-container-row wrap gap-m small-inputs\">\n                    @php\n                        $labelByKey = ['shelves' => trans('entities.shelves'), 'books' => trans('entities.books'), 'search' => trans('entities.search_results')];\n                    @endphp\n                    @foreach($labelByKey as $key => $label)\n                        <div>\n                            <label for=\"setting-lists-page-count-{{ $key }}\">{{ $label }}</label>\n                            @include('form.number', [\n                                'name' => 'setting-lists-page-count-' . $key,\n                                'value' => setting()->getInteger('lists-page-count-' . $key, 18, 1, 1000),\n                                'min' => 1,\n                                'step' => 1,\n                            ])\n                        </div>\n                    @endforeach\n                </div>\n            </div>\n\n            <div class=\"grid half gap-xl items-center\">\n                <div>\n                    <label for=\"setting-sorting-book-default\"\n                           class=\"setting-list-label\">{{ trans('settings.sorting_book_default') }}</label>\n                    <p class=\"small\">{{ trans('settings.sorting_book_default_desc') }}</p>\n                </div>\n                <div>\n                    <select id=\"setting-sorting-book-default\" name=\"setting-sorting-book-default\"\n                            @if($errors->has('setting-sorting-book-default')) class=\"neg\" @endif>\n                        <option value=\"0\" @if(intval(setting('sorting-book-default', '0')) === 0) selected @endif>\n                            -- {{ trans('common.none') }} --\n                        </option>\n                        @foreach($sortRules as $set)\n                            <option value=\"{{$set->id}}\"\n                                    @if(intval(setting('sorting-book-default', '0')) === $set->id) selected @endif\n                            >\n                                {{ $set->name }}\n                            </option>\n                        @endforeach\n                    </select>\n                </div>\n            </div>\n\n        </div>\n\n        <div class=\"form-group text-right\">\n            <button type=\"submit\" class=\"button\">{{ trans('settings.settings_save') }}</button>\n        </div>\n    </form>\n@endsection\n\n@section('after-card')\n    <div class=\"card content-wrap auto-height\">\n        <div class=\"flex-container-row items-center gap-m\">\n            <div class=\"flex\">\n                <h2 class=\"list-heading\">{{ trans('settings.sorting_rules') }}</h2>\n                <p class=\"text-muted\">{{ trans('settings.sorting_rules_desc') }}</p>\n            </div>\n            <div>\n                <a href=\"{{ url('/settings/sorting/rules/new') }}\"\n                   class=\"button outline\">{{ trans('settings.sort_rule_create') }}</a>\n            </div>\n        </div>\n\n        @if(empty($sortRules))\n            <p class=\"italic text-muted\">{{ trans('common.no_items') }}</p>\n        @else\n            <div class=\"item-list\">\n                @foreach($sortRules as $rule)\n                    @include('settings.sort-rules.parts.sort-rule-list-item', ['rule' => $rule])\n                @endforeach\n            </div>\n        @endif\n    </div>\n@endsection"
  },
  {
    "path": "resources/views/settings/layout.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n    <div class=\"container medium\">\n\n        @include('settings.parts.navbar', ['selected' => 'settings'])\n\n        <div class=\"grid gap-xxl right-focus\">\n\n            <div>\n                <h5>{{ trans('settings.categories') }}</h5>\n                <nav class=\"active-link-list in-sidebar\">\n                    <a href=\"{{ url('/settings/features') }}\" class=\"{{ $category === 'features' ? 'active' : '' }}\">@icon('star') {{ trans('settings.app_features_security') }}</a>\n                    <a href=\"{{ url('/settings/customization') }}\" class=\"{{ $category === 'customization' ? 'active' : '' }}\">@icon('palette') {{ trans('settings.app_customization') }}</a>\n                    <a href=\"{{ url('/settings/registration') }}\" class=\"{{ $category === 'registration' ? 'active' : '' }}\">@icon('security') {{ trans('settings.reg_settings') }}</a>\n                    <a href=\"{{ url('/settings/sorting') }}\" class=\"{{ $category === 'sorting' ? 'active' : '' }}\">@icon('sort') {{ trans('settings.sorting') }}</a>\n                </nav>\n\n                <h5 class=\"mt-xl\">{{ trans('settings.system_version') }}</h5>\n                <div class=\"py-xs\">\n                    <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/BookStackApp/BookStack/releases\">\n                        BookStack @if(!str_starts_with($version, 'v')) version @endif {{ $version }}\n                    </a>\n                    <br>\n                    <a target=\"_blank\" href=\"{{ url('/licenses') }}\" class=\"text-muted\">{{ trans('settings.license_details') }}</a>\n                </div>\n            </div>\n\n            <div>\n                <div class=\"card content-wrap auto-height\">\n                    @yield('card')\n                </div>\n                @yield('after-card')\n            </div>\n\n        </div>\n\n    </div>\n\n    @yield('after-content')\n@stop\n"
  },
  {
    "path": "resources/views/settings/maintenance.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n<div class=\"container small\">\n\n    @include('settings.parts.navbar', ['selected' => 'maintenance'])\n\n    <div class=\"card content-wrap auto-height pb-xl\">\n        <h2 class=\"list-heading\">{{ trans('settings.recycle_bin') }}</h2>\n        <div class=\"grid half gap-xl\">\n            <div>\n                <p class=\"small text-muted\">{{ trans('settings.maint_recycle_bin_desc') }}</p>\n            </div>\n            <div>\n                <div class=\"grid half no-gap mb-m\">\n                    <p class=\"mb-xs text-bookshelf\">@icon('bookshelf'){{ trans('entities.shelves') }}: {{ $recycleStats['bookshelf'] }}</p>\n                    <p class=\"mb-xs text-book\">@icon('book'){{ trans('entities.books') }}: {{ $recycleStats['book'] }}</p>\n                    <p class=\"mb-xs text-chapter\">@icon('chapter'){{ trans('entities.chapters') }}: {{ $recycleStats['chapter'] }}</p>\n                    <p class=\"mb-xs text-page\">@icon('page'){{ trans('entities.pages') }}: {{ $recycleStats['page'] }}</p>\n                </div>\n                <a href=\"{{ url('/settings/recycle-bin') }}\" class=\"button outline\">{{ trans('settings.maint_recycle_bin_open') }}</a>\n            </div>\n        </div>\n    </div>\n\n    <div id=\"image-cleanup\" class=\"card content-wrap auto-height\">\n        <h2 class=\"list-heading\">{{ trans('settings.maint_image_cleanup') }}</h2>\n        <div class=\"grid left-focus gap-xl\">\n            <div>\n                <p class=\"small text-muted\">{{ trans('settings.maint_image_cleanup_desc') }}</p>\n                <p class=\"small text-muted italic\">{{ trans('settings.maint_timeout_command_note') }}</p>\n            </div>\n            <div>\n                <form method=\"POST\" action=\"{{ url('/settings/maintenance/cleanup-images') }}\">\n                    {!! csrf_field()  !!}\n                    <input type=\"hidden\" name=\"_method\" value=\"DELETE\">\n                    <div class=\"mb-s\">\n                        @if(session()->has('cleanup-images-warning'))\n                            <p class=\"text-neg\">\n                                {{ session()->get('cleanup-images-warning') }}\n                            </p>\n                            <input type=\"hidden\" name=\"ignore_revisions\" value=\"{{ session()->getOldInput('ignore_revisions', 'false') }}\">\n                            <input type=\"hidden\" name=\"confirm\" value=\"true\">\n                        @else\n                            <label class=\"flex-container-row\">\n                                <div class=\"mr-s\"><input type=\"checkbox\" name=\"ignore_revisions\" value=\"true\"></div>\n                                <div>{{ trans('settings.maint_delete_images_only_in_revisions') }}</div>\n                            </label>\n                        @endif\n                    </div>\n                    <button class=\"button outline\">{{ trans('settings.maint_image_cleanup_run') }}</button>\n                </form>\n            </div>\n        </div>\n    </div>\n\n    <div id=\"send-test-email\" class=\"card content-wrap auto-height\">\n        <h2 class=\"list-heading\">{{ trans('settings.maint_send_test_email') }}</h2>\n        <div class=\"grid left-focus gap-xl\">\n            <div>\n                <p class=\"small text-muted\">{{ trans('settings.maint_send_test_email_desc') }}</p>\n            </div>\n            <div>\n                <form method=\"POST\" action=\"{{ url('/settings/maintenance/send-test-email') }}\">\n                    {!! csrf_field()  !!}\n                    <button class=\"button outline\">{{ trans('settings.maint_send_test_email_run') }}</button>\n                </form>\n            </div>\n        </div>\n    </div>\n\n    <div id=\"regenerate-references\" class=\"card content-wrap auto-height\">\n        <h2 class=\"list-heading\">{{ trans('settings.maint_regen_references') }}</h2>\n        <div class=\"grid left-focus gap-xl\">\n            <div>\n                <p class=\"small text-muted\">{{ trans('settings.maint_regen_references_desc') }}</p>\n                <p class=\"small text-muted italic\">{{ trans('settings.maint_timeout_command_note') }}</p>\n            </div>\n            <div>\n                <form method=\"POST\" action=\"{{ url('/settings/maintenance/regenerate-references') }}\">\n                    {!! csrf_field()  !!}\n                    <button class=\"button outline\">{{ trans('settings.maint_regen_references') }}</button>\n                </form>\n            </div>\n        </div>\n    </div>\n\n</div>\n@stop\n"
  },
  {
    "path": "resources/views/settings/parts/footer-links.blade.php",
    "content": "{{--\n$value - Setting value\n$name - Setting input name\n--}}\n<div components=\"add-remove-rows\"\n     option:add-remove-rows:row-selector=\".card\"\n     option:add-remove-rows:remove-selector=\"button.text-neg\">\n\n    <div component=\"sortable-list\"\n         option:sortable-list:handle-selector=\".handle\">\n        @foreach(array_merge($value, [['label' => '', 'url' => '']]) as $index => $link)\n            <div class=\"card drag-card {{ $loop->last ? 'hidden' : '' }}\" @if($loop->last) refs=\"add-remove-rows@model\" @endif>\n                <div class=\"handle\">@icon('grip')</div>\n                @foreach(['label', 'url'] as $prop)\n                    <div class=\"outline\">\n                        <input value=\"{{ $link[$prop] ?? '' }}\"\n                               placeholder=\"{{ trans('settings.app_footer_links_' . $prop) }}\"\n                               aria-label=\"{{ trans('settings.app_footer_links_' . $prop) }}\"\n                               name=\"{{ $name }}[{{ $loop->parent->last ? 'randrowid' : $index }}][{{$prop}}]\"\n                               type=\"text\"\n                               autocomplete=\"off\"/>\n                    </div>\n                @endforeach\n                <button type=\"button\"\n                        aria-label=\"{{ trans('common.remove') }}\"\n                        class=\"text-center drag-card-action text-neg\">\n                    @icon('close')\n                </button>\n            </div>\n        @endforeach\n    </div>\n\n    <button refs=\"add-remove-rows@add\" type=\"button\" class=\"text-button\">{{ trans('settings.app_footer_links_add') }}</button>\n</div>"
  },
  {
    "path": "resources/views/settings/parts/navbar.blade.php",
    "content": "\n<nav class=\"active-link-list py-m flex-container-row justify-center wrap\">\n    @if(userCan(\\BookStack\\Permissions\\Permission::SettingsManage))\n        <a href=\"{{ url('/settings') }}\" @if($selected == 'settings') class=\"active\" @endif>@icon('settings'){{ trans('settings.settings') }}</a>\n        <a href=\"{{ url('/settings/maintenance') }}\" @if($selected == 'maintenance') class=\"active\" @endif>@icon('spanner'){{ trans('settings.maint') }}</a>\n    @endif\n    @if(userCan(\\BookStack\\Permissions\\Permission::SettingsManage) && userCan(\\BookStack\\Permissions\\Permission::UsersManage))\n        <a href=\"{{ url('/settings/audit') }}\" @if($selected == 'audit') class=\"active\" @endif>@icon('open-book'){{ trans('settings.audit') }}</a>\n    @endif\n    @if(userCan(\\BookStack\\Permissions\\Permission::UsersManage))\n        <a href=\"{{ url('/settings/users') }}\" @if($selected == 'users') class=\"active\" @endif>@icon('users'){{ trans('settings.users') }}</a>\n    @endif\n    @if(userCan(\\BookStack\\Permissions\\Permission::UserRolesManage))\n        <a href=\"{{ url('/settings/roles') }}\" @if($selected == 'roles') class=\"active\" @endif>@icon('lock-open'){{ trans('settings.roles') }}</a>\n    @endif\n    @if(userCan(\\BookStack\\Permissions\\Permission::SettingsManage))\n        <a href=\"{{ url('/settings/webhooks') }}\" @if($selected == 'webhooks') class=\"active\" @endif>@icon('webhooks'){{ trans('settings.webhooks') }}</a>\n    @endif\n</nav>"
  },
  {
    "path": "resources/views/settings/parts/setting-color-picker.blade.php",
    "content": "{{--\n    @type - Name of color setting\n--}}\n@php\n    $keyAppends = ($mode === 'light' ? '' : '-' . $mode);\n@endphp\n<div component=\"setting-color-picker\"\n     option:setting-color-picker:default=\"{{ config('setting-defaults.'. $type .'-color' . $keyAppends) }}\"\n     option:setting-color-picker:current=\"{{ setting($type .'-color' . $keyAppends) }}\"\n     class=\"grid no-break half mb-l\">\n    <div>\n        <label for=\"setting-{{ $type }}-color{{ $keyAppends }}\" class=\"text-dark\">{{ trans('settings.'. str_replace('-', '_', $type) .'_color') }}</label>\n        <button refs=\"setting-color-picker@default-button\" type=\"button\" class=\"text-button text-muted\">{{ trans('common.default') }}</button>\n        <span class=\"sep\">|</span>\n        <button refs=\"setting-color-picker@reset-button\" type=\"button\" class=\"text-button text-muted\">{{ trans('common.reset') }}</button>\n    </div>\n    <div>\n        <input type=\"color\"\n               refs=\"setting-color-picker@input\"\n               value=\"{{ setting($type . '-color' . $keyAppends) }}\"\n               name=\"setting-{{ $type }}-color{{ $keyAppends }}\"\n               id=\"setting-{{ $type }}-color{{ $keyAppends }}\"\n               placeholder=\"{{ config('setting-defaults.'. $type .'-color' . $keyAppends) }}\"\n               class=\"small\">\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/settings/parts/setting-color-scheme.blade.php",
    "content": "{{--\n    @mode - 'light' or 'dark'.\n--}}\n<p class=\"small\">{{ trans('settings.ui_colors_desc') }}</p>\n<div class=\"grid half pt-m\">\n    <div>\n        @include('settings.parts.setting-color-picker', ['type' => 'app', 'mode' => $mode])\n    </div>\n    <div>\n        @include('settings.parts.setting-color-picker', ['type' => 'link', 'mode' => $mode])\n    </div>\n</div>\n<hr>\n<p class=\"small\">{!! trans('settings.content_colors_desc') !!}</p>\n<div class=\"grid half pt-m\">\n    <div>\n        @include('settings.parts.setting-color-picker', ['type' => 'bookshelf', 'mode' => $mode])\n        @include('settings.parts.setting-color-picker', ['type' => 'book', 'mode' => $mode])\n        @include('settings.parts.setting-color-picker', ['type' => 'chapter', 'mode' => $mode])\n    </div>\n    <div>\n        @include('settings.parts.setting-color-picker', ['type' => 'page', 'mode' => $mode])\n        @include('settings.parts.setting-color-picker', ['type' => 'page-draft', 'mode' => $mode])\n    </div>\n</div>\n\n<input type=\"hidden\"\n       value=\"{{ setting('app-color-light' . ($mode === 'dark' ? '-dark' : '')) }}\"\n       name=\"setting-app-color-light{{ $mode === 'dark' ? '-dark' : '' }}\">"
  },
  {
    "path": "resources/views/settings/parts/table-user.blade.php",
    "content": "{{--\n$user - User to display.\n--}}\n<a href=\"{{ $user->getEditUrl() }}\" class=\"flex-container-row inline gap-s items-center\">\n    <div class=\"flex-none\"><img width=\"40\" height=\"40\" class=\"avatar block\" src=\"{{ $user->getAvatar(40)}}\" alt=\"{{ $user->name }}\"></div>\n    <div class=\"flex\">{{ $user->name }}</div>\n</a>"
  },
  {
    "path": "resources/views/settings/recycle-bin/destroy.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n    <div class=\"container small\">\n\n        @include('settings.parts.navbar', ['selected' => 'maintenance'])\n\n        <div class=\"card content-wrap auto-height\">\n            <h2 class=\"list-heading\">{{ trans('settings.recycle_bin_permanently_delete') }}</h2>\n            <p class=\"text-muted\">{{ trans('settings.recycle_bin_destroy_confirm') }}</p>\n            <form action=\"{{ url('/settings/recycle-bin/' . $deletion->id) }}\" method=\"post\">\n                {!! method_field('DELETE') !!}\n                {!! csrf_field() !!}\n                <a href=\"{{ url('/settings/recycle-bin') }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                <button type=\"submit\" class=\"button\">{{ trans('common.delete_confirm') }}</button>\n            </form>\n\n            @if($deletion->deletable instanceof \\BookStack\\Entities\\Models\\Entity)\n                <hr class=\"mt-m\">\n                <h5>{{ trans('settings.recycle_bin_destroy_list') }}</h5>\n                @include('settings.recycle-bin.parts.deletable-entity-list', ['entity' => $deletion->deletable])\n            @endif\n\n        </div>\n\n    </div>\n@stop\n"
  },
  {
    "path": "resources/views/settings/recycle-bin/index.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n    <div class=\"container\">\n\n        @include('settings.parts.navbar', ['selected' => 'maintenance'])\n\n        <div class=\"card content-wrap auto-height\">\n            <h2 class=\"list-heading\">{{ trans('settings.recycle_bin') }}</h2>\n\n            <div class=\"flex-container-row items-center gap-x-l gap-y-m wrap\">\n                <div class=\"flex-2 min-width-l\">\n                    <p class=\"text-muted mb-none\">{{ trans('settings.recycle_bin_desc') }}</p>\n                </div>\n                <div class=\"flex text-m-right min-width-m\">\n                    <div component=\"dropdown\" class=\"dropdown-container\">\n                        <button refs=\"dropdown@toggle\"\n                                type=\"button\"\n                                class=\"button outline\">{{ trans('settings.recycle_bin_empty') }} </button>\n                        <div refs=\"dropdown@menu\" class=\"dropdown-menu\">\n                            <p class=\"text-neg small px-m mb-xs\">{{ trans('settings.recycle_bin_empty_confirm') }}</p>\n\n                            <form action=\"{{ url('/settings/recycle-bin/empty') }}\" method=\"POST\">\n                                {!! csrf_field() !!}\n                                <button type=\"submit\" class=\"text-link small delete text-item\">{{ trans('common.confirm') }}</button>\n                            </form>\n                        </div>\n                    </div>\n\n                </div>\n            </div>\n\n            <hr class=\"mt-l mb-s\">\n\n            <div class=\"py-m\">\n                {!! $deletions->links() !!}\n            </div>\n\n            <div class=\"item-list\">\n                <div class=\"item-list-row flex-container-row items-center px-s bold hide-under-l\">\n                    <div class=\"flex-2 px-m py-xs\">{{ trans('settings.audit_deleted_item') }}</div>\n                    <div class=\"flex-2 px-m py-xs\">{{ trans('settings.recycle_bin_deleted_parent') }}</div>\n                    <div class=\"flex-2 px-m py-xs\">{{ trans('settings.recycle_bin_deleted_by') }}</div>\n                    <div class=\"flex px-m py-xs\">{{ trans('settings.recycle_bin_deleted_at') }}</div>\n                    <div class=\"flex px-m py-xs text-right\"></div>\n                </div>\n                @if(count($deletions) === 0)\n                    <div class=\"item-list-row px-l py-m\">\n                        <p class=\"text-muted mb-none\"><em>{{ trans('settings.recycle_bin_contents_empty') }}</em></p>\n                    </div>\n                @endif\n                @foreach($deletions as $deletion)\n                    @include('settings.recycle-bin.parts.recycle-bin-list-item', ['deletion' => $deletion])\n                @endforeach\n            </div>\n\n            <div class=\"py-m\">\n                {!! $deletions->links() !!}\n            </div>\n\n        </div>\n\n    </div>\n@stop\n"
  },
  {
    "path": "resources/views/settings/recycle-bin/parts/deletable-entity-list.blade.php",
    "content": "@include('settings.recycle-bin.parts.entity-display-item', ['entity' => $entity])\n@if($entity->isA('book'))\n    @foreach($entity->chapters()->withTrashed()->get() as $chapter)\n        @include('settings.recycle-bin.parts.entity-display-item', ['entity' => $chapter])\n    @endforeach\n@endif\n@if($entity->isA('book') || $entity->isA('chapter'))\n    @foreach($entity->pages()->withTrashed()->get() as $page)\n        @include('settings.recycle-bin.parts.entity-display-item', ['entity' => $page])\n    @endforeach\n@endif"
  },
  {
    "path": "resources/views/settings/recycle-bin/parts/entity-display-item.blade.php",
    "content": "<?php $type = $entity->getType(); ?>\n<div class=\"{{$type}} {{$type === 'page' && $entity->draft ? 'draft' : ''}} {{$classes ?? ''}} entity-list-item no-hover\">\n    <span role=\"presentation\" class=\"icon text-{{$type}} {{$type === 'page' && $entity->draft ? 'draft' : ''}}\">@icon($type)</span>\n    <div class=\"content\">\n        <div class=\"entity-list-item-name break-text\">{{ $entity->name }}</div>\n    </div>\n</div>"
  },
  {
    "path": "resources/views/settings/recycle-bin/parts/recycle-bin-list-item.blade.php",
    "content": "<div class=\"item-list-row flex-container-row items-center px-s wrap\">\n    <div class=\"flex-2 px-m py-xs min-width-xl\">\n        <div class=\"flex-container-row items-center py-xs\">\n            <span role=\"presentation\" class=\"flex-none icon text-{{$deletion->deletable->getType()}}\">@icon($deletion->deletable->getType())</span>\n            <div class=\"text-{{ $deletion->deletable->getType() }}\">\n                {{ $deletion->deletable->name }}\n            </div>\n        </div>\n        @if($deletion->deletable instanceof \\BookStack\\Entities\\Models\\Book)\n            <div class=\"pl-l block inline\">\n                <div class=\"text-chapter\">\n                    @icon('chapter') {{ trans_choice('entities.x_chapters', $deletion->deletable->chapters()->withTrashed()->count()) }}\n                </div>\n            </div>\n        @endif\n        @if($deletion->deletable instanceof \\BookStack\\Entities\\Models\\Book || $deletion->deletable instanceof \\BookStack\\Entities\\Models\\Chapter)\n            <div class=\"pl-l block inline\">\n                <div class=\"text-page\">\n                    @icon('page') {{ trans_choice('entities.x_pages', $deletion->deletable->pages()->withTrashed()->count()) }}\n                </div>\n            </div>\n        @endif\n    </div>\n    <div class=\"flex-2 px-m py-xs min-width-m\">\n        @if($deletion->deletable->getParent())\n            <strong class=\"hide-over-l\">{{ trans('settings.recycle_bin_deleted_parent') }}:<br></strong>\n            <div class=\"flex-container-row items-center\">\n                <span role=\"presentation\" class=\"flex-none icon text-{{$deletion->deletable->getParent()->getType()}}\">@icon($deletion->deletable->getParent()->getType())</span>\n                <div class=\"text-{{ $deletion->deletable->getParent()->getType() }}\">\n                    {{ $deletion->deletable->getParent()->name }}\n                </div>\n            </div>\n        @endif\n    </div>\n    <div class=\"flex-2 px-m py-xs flex-container-row items-center min-width-m\">\n        <div>\n            <strong class=\"hide-over-l\">{{ trans('settings.recycle_bin_deleted_by') }}:<br></strong>\n            @if($deletion->deleter)\n                @include('settings.parts.table-user', ['user' => $deletion->deleter, 'user_id' => $deletion->deleted_by])\n            @else\n                {{ trans('common.deleted_user') }}\n            @endif\n        </div>\n    </div>\n    <div class=\"flex px-m py-xs min-width-s\"><strong class=\"hide-over-l\">{{ trans('settings.recycle_bin_deleted_at') }}:<br></strong>{{ $deletion->created_at }}</div>\n    <div class=\"flex px-m py-xs text-m-right min-width-s\">\n        <div component=\"dropdown\" class=\"dropdown-container\">\n            <button type=\"button\"\n                    refs=\"dropdown@toggle\"\n                    aria-haspopup=\"menu\"\n                    aria-expanded=\"false\"\n                    class=\"button outline\">{{ trans('common.actions') }}</button>\n            <ul refs=\"dropdown@menu\" class=\"dropdown-menu\" role=\"menu\">\n                <li><a class=\"text-item\" href=\"{{ $deletion->getUrl('/restore') }}\" role=\"menuitem\">{{ trans('settings.recycle_bin_restore') }}</a></li>\n                <li><a class=\"text-item\" href=\"{{ $deletion->getUrl('/destroy') }}\" role=\"menuitem\">{{ trans('settings.recycle_bin_permanently_delete') }}</a></li>\n            </ul>\n        </div>\n    </div>\n</div>"
  },
  {
    "path": "resources/views/settings/recycle-bin/restore.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n    <div class=\"container small\">\n\n        @include('settings.parts.navbar', ['selected' => 'maintenance'])\n\n        <div class=\"card content-wrap auto-height\">\n            <h2 class=\"list-heading\">{{ trans('settings.recycle_bin_restore') }}</h2>\n            <p class=\"text-muted\">{{ trans('settings.recycle_bin_restore_confirm') }}</p>\n            <form action=\"{{ $deletion->getUrl('/restore') }}\" method=\"post\">\n                {!! csrf_field() !!}\n                <a href=\"{{ url('/settings/recycle-bin') }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                <button type=\"submit\" class=\"button\">{{ trans('settings.recycle_bin_restore') }}</button>\n            </form>\n\n            @if($deletion->deletable instanceof \\BookStack\\Entities\\Models\\Entity)\n                <hr class=\"mt-m\">\n                <h5>{{ trans('settings.recycle_bin_restore_list') }}</h5>\n                <div class=\"flex-container-row mb-s items-center\">\n                    @if($deletion->deletable->getParent() && $deletion->deletable->getParent()->trashed())\n                        <div class=\"text-neg flex\">{{ trans('settings.recycle_bin_restore_deleted_parent') }}</div>\n                    @endif\n                    @if($parentDeletion)\n                        <div class=\"flex fit-content ml-m\">\n                            <a class=\"button outline\" href=\"{{ $parentDeletion->getUrl('/restore') }}\">{{ trans('settings.recycle_bin_restore_parent') }}</a>\n                        </div>\n                    @endif\n                </div>\n\n                @include('settings.recycle-bin.parts.deletable-entity-list', ['entity' => $deletion->deletable])\n            @endif\n\n        </div>\n\n    </div>\n@stop\n"
  },
  {
    "path": "resources/views/settings/roles/create.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        @include('settings.parts.navbar', ['selected' => 'roles'])\n\n        <div class=\"card content-wrap\">\n            <h1 class=\"list-heading\">{{ trans('settings.role_create') }}</h1>\n\n            <form action=\"{{ url(\"/settings/roles/new\") }}\" method=\"POST\">\n                {{ csrf_field() }}\n\n                @include('settings.roles.parts.form', ['role' => $role ?? null])\n\n                <div class=\"form-group text-right\">\n                    <a href=\"{{ url(\"/settings/roles\") }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                    <button type=\"submit\" class=\"button\">{{ trans('settings.role_save') }}</button>\n                </div>\n            </form>\n\n        </div>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/settings/roles/delete.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n    <div class=\"container small\">\n\n        @include('settings.parts.navbar', ['selected' => 'roles'])\n\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\"> {{ trans('settings.role_delete') }}</h1>\n\n            <p>{{ trans('settings.role_delete_confirm', ['roleName' => $role->display_name]) }}</p>\n\n            <form action=\"{{ url(\"/settings/roles/delete/{$role->id}\") }}\" method=\"POST\">\n                {!! csrf_field() !!}\n                <input type=\"hidden\" name=\"_method\" value=\"DELETE\">\n\n                @if($role->users->count() > 0)\n                    <div class=\"form-group\">\n                        <p>{{ trans('settings.role_delete_users_assigned', ['userCount' => $role->users->count()]) }}</p>\n                        @include('form.role-select', ['options' => $roles, 'name' => 'migrate_role_id'])\n                    </div>\n                @endif\n\n                <div class=\"grid half v-center\">\n                    <div>\n                        <p class=\"text-neg\">\n                            <strong>{{ trans('settings.role_delete_sure') }}</strong>\n                        </p>\n                    </div>\n                    <div>\n                        <div class=\"form-group text-right\">\n                            <a href=\"{{ url(\"/settings/roles/{$role->id}\") }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                            <button type=\"submit\" class=\"button\">{{ trans('common.confirm') }}</button>\n                        </div>\n                    </div>\n                </div>\n\n\n            </form>\n        </div>\n\n    </div>\n@stop\n"
  },
  {
    "path": "resources/views/settings/roles/edit.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n        @include('settings.parts.navbar', ['selected' => 'roles'])\n\n        <div class=\"card content-wrap\">\n            <h1 class=\"list-heading\">{{ trans('settings.role_edit') }}</h1>\n\n            <form action=\"{{ url(\"/settings/roles/{$role->id}\") }}\" method=\"POST\">\n                {{ csrf_field() }}\n                {{ method_field('PUT') }}\n\n                @include('settings.roles.parts.form', ['role' => $role])\n\n                <div class=\"form-group text-right\">\n                    <a href=\"{{ url(\"/settings/roles\") }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                    <a href=\"{{ url(\"/settings/roles/new?copy_from={$role->id}\") }}\" class=\"button outline\">{{ trans('common.copy') }}</a>\n                    <a href=\"{{ url(\"/settings/roles/delete/{$role->id}\") }}\" class=\"button outline\">{{ trans('settings.role_delete') }}</a>\n                    <button type=\"submit\" class=\"button\">{{ trans('settings.role_save') }}</button>\n                </div>\n            </form>\n\n        </div>\n\n\n        <div class=\"card content-wrap auto-height\">\n            <h2 class=\"list-heading\">{{ trans('settings.role_users') }}</h2>\n            @if(count($role->users ?? []) > 0)\n                <div class=\"grid third\">\n                    @foreach($role->users as $user)\n                        <div class=\"user-list-item\">\n                            <div>\n                                <img class=\"avatar small\" src=\"{{ $user->getAvatar(40) }}\" alt=\"{{ $user->name }}\">\n                            </div>\n                            <div>\n                                @if(userCan(\\BookStack\\Permissions\\Permission::UsersManage) || user()->id == $user->id)\n                                    <a href=\"{{ url(\"/settings/users/{$user->id}\") }}\">\n                                        @endif\n                                        {{ $user->name }}\n                                        @if(userCan(\\BookStack\\Permissions\\Permission::UsersManage) || user()->id == $user->id)\n                                    </a>\n                                @endif\n                            </div>\n                        </div>\n                    @endforeach\n                </div>\n            @else\n                <p class=\"text-muted\">\n                    {{ trans('settings.role_users_none') }}\n                </p>\n            @endif\n        </div>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/settings/roles/index.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        @include('settings.parts.navbar', ['selected' => 'roles'])\n\n        <div class=\"card content-wrap auto-height\">\n\n            <div class=\"grid half v-center\">\n                <h1 class=\"list-heading\">{{ trans('settings.role_user_roles') }}</h1>\n\n                <div class=\"text-right\">\n                    <a href=\"{{ url(\"/settings/roles/new\") }}\" class=\"button outline my-none\">{{ trans('settings.role_create') }}</a>\n                </div>\n            </div>\n\n            <p class=\"text-muted\">{{ trans('settings.roles_index_desc') }}</p>\n\n            <div class=\"flex-container-row items-center justify-space-between gap-m mt-m mb-l wrap\">\n                <div>\n                    <div class=\"block inline mr-xs\">\n                        <form method=\"get\" action=\"{{ url(\"/settings/roles\") }}\">\n                            <input type=\"text\"\n                                   name=\"search\"\n                                   title=\"{{ trans('common.search') }}\"\n                                   placeholder=\"{{ trans('common.search') }}\"\n                                   value=\"{{ $listOptions->getSearch() }}\">\n                        </form>\n                    </div>\n                </div>\n                <div class=\"justify-flex-end\">\n                    @include('common.sort', $listOptions->getSortControlData())\n                </div>\n            </div>\n\n            <div class=\"item-list\">\n                @foreach($roles as $role)\n                    @include('settings.roles.parts.roles-list-item', ['role' => $role])\n                @endforeach\n            </div>\n\n            <div class=\"mb-m\">\n                {{ $roles->links() }}\n            </div>\n\n        </div>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/settings/roles/parts/asset-permissions-row.blade.php",
    "content": "<div class=\"item-list-row flex-container-row items-center wrap\">\n    <div class=\"flex py-s px-m min-width-s\">\n        <strong>{{ $title }}</strong> <br>\n        <a href=\"#\" refs=\"permissions-table@toggle-row\" class=\"text-small text-link\">{{ trans('common.toggle_all') }}</a>\n    </div>\n    <div class=\"flex py-s px-m min-width-xxs\">\n        <small class=\"hide-over-m bold\">{{ trans('common.create') }}<br></small>\n        @if($permissionPrefix === 'page' || $permissionPrefix === 'chapter')\n            @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-create-own', 'label' => trans('settings.role_own')])\n            <br>\n        @endif\n        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-create-all', 'label' => trans('settings.role_all')])\n    </div>\n    <div class=\"flex py-s px-m min-width-xxs\">\n        <small class=\"hide-over-m bold\">{{ trans('common.view') }}<br></small>\n        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-view-own', 'label' => trans('settings.role_own')])\n        <br>\n        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-view-all', 'label' => trans('settings.role_all')])\n    </div>\n    <div class=\"flex py-s px-m min-width-xxs\">\n        <small class=\"hide-over-m bold\">{{ trans('common.edit') }}<br></small>\n        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-update-own', 'label' => trans('settings.role_own')])\n        <br>\n        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-update-all', 'label' => trans('settings.role_all')])\n    </div>\n    <div class=\"flex py-s px-m min-width-xxs\">\n        <small class=\"hide-over-m bold\">{{ trans('common.delete') }}<br></small>\n        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-delete-own', 'label' => trans('settings.role_own')])\n        <br>\n        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-delete-all', 'label' => trans('settings.role_all')])\n    </div>\n</div>"
  },
  {
    "path": "resources/views/settings/roles/parts/checkbox.blade.php",
    "content": "\n@include('form.custom-checkbox', [\n       'name' => 'permissions[' . $permission . ']',\n       'value' => 'true',\n       'checked' => old('permissions'.$permission, false)|| (!old('display_name', false) && (isset($role) && $role->hasPermission($permission))),\n       'label' => $label\n])"
  },
  {
    "path": "resources/views/settings/roles/parts/form.blade.php",
    "content": "<div class=\"setting-list\">\n\n    <div class=\"grid half\">\n        <div>\n            <label class=\"setting-list-label\">{{ trans('settings.role_details') }}</label>\n        </div>\n        <div>\n            <div class=\"form-group\">\n                <label for=\"display_name\">{{ trans('settings.role_name') }}</label>\n                @include('form.text', ['name' => 'display_name', 'model' => $role])\n            </div>\n            <div class=\"form-group\">\n                <label for=\"description\">{{ trans('settings.role_desc') }}</label>\n                @include('form.text', ['name' => 'description', 'model' => $role])\n            </div>\n            <div class=\"form-group\">\n                @include('form.checkbox', ['name' => 'mfa_enforced', 'label' => trans('settings.role_mfa_enforced'), 'model' => $role ])\n            </div>\n\n            @if(in_array(config('auth.method'), ['ldap', 'saml2', 'oidc']))\n                <div class=\"form-group\">\n                    <label for=\"name\">{{ trans('settings.role_external_auth_id') }}</label>\n                    @include('form.text', ['name' => 'external_auth_id', 'model' => $role])\n                </div>\n            @endif\n        </div>\n    </div>\n\n    <div component=\"permissions-table\">\n        <label class=\"setting-list-label\">{{ trans('settings.role_system') }}</label>\n        <a href=\"#\" refs=\"permissions-table@toggle-all\" class=\"text-small text-link\">{{ trans('common.toggle_all') }}</a>\n\n        <div class=\"toggle-switch-list grid half my-m\">\n            <div>\n                <div>@include('settings.roles.parts.checkbox', ['permission' => 'restrictions-manage-all', 'label' => trans('settings.role_manage_entity_permissions')]) <sup>1</sup></div>\n                <div>@include('settings.roles.parts.checkbox', ['permission' => 'restrictions-manage-own', 'label' => trans('settings.role_manage_own_entity_permissions')]) <sup>1</sup></div>\n                <div>@include('settings.roles.parts.checkbox', ['permission' => 'templates-manage', 'label' => trans('settings.role_manage_page_templates')])</div>\n                <div>@include('settings.roles.parts.checkbox', ['permission' => 'access-api', 'label' => trans('settings.role_access_api')])</div>\n                <div>@include('settings.roles.parts.checkbox', ['permission' => 'content-export', 'label' => trans('settings.role_export_content')])</div>\n                <div>@include('settings.roles.parts.checkbox', ['permission' => 'content-import', 'label' => trans('settings.role_import_content')])</div>\n                <div>@include('settings.roles.parts.checkbox', ['permission' => 'editor-change', 'label' => trans('settings.role_editor_change')])</div>\n                <div>@include('settings.roles.parts.checkbox', ['permission' => 'receive-notifications', 'label' => trans('settings.role_notifications')])</div>\n            </div>\n            <div>\n                <div>@include('settings.roles.parts.checkbox', ['permission' => 'settings-manage', 'label' => trans('settings.role_manage_settings')])</div>\n                <div>@include('settings.roles.parts.checkbox', ['permission' => 'users-manage', 'label' => trans('settings.role_manage_users')])</div>\n                <div>@include('settings.roles.parts.checkbox', ['permission' => 'user-roles-manage', 'label' => trans('settings.role_manage_roles')])</div>\n                <p class=\"text-warn text-small mt-s mb-none\">{{ trans('settings.roles_system_warning') }}</p>\n            </div>\n        </div>\n\n        <p class=\"mb-none text-small text-muted\">\n            <sup>1</sup> {{ trans('settings.role_permission_note_users_and_roles') }}\n        </p>\n    </div>\n\n    <div>\n        <label class=\"setting-list-label\">{{ trans('settings.role_asset') }}</label>\n        <p>{{ trans('settings.role_asset_desc') }}</p>\n\n        @if (isset($role) && $role->system_name === 'admin')\n            <p class=\"text-warn\">{{ trans('settings.role_asset_admins') }}</p>\n        @endif\n\n        <div component=\"permissions-table\"\n             option:permissions-table:cell-selector=\".item-list-row > div\"\n             option:permissions-table:row-selector=\".item-list-row\"\n             class=\"item-list toggle-switch-list\">\n            <div class=\"item-list-row flex-container-row items-center hide-under-m bold\">\n                <div class=\"flex py-s px-m min-width-s\">\n                    <a href=\"#\" refs=\"permissions-table@toggle-all\" class=\"text-small text-link\">{{ trans('common.toggle_all') }}</a>\n                </div>\n                <div refs=\"permissions-table@toggle-column\" class=\"flex py-s px-m min-width-xxs\">{{ trans('common.create') }}</div>\n                <div refs=\"permissions-table@toggle-column\" class=\"flex py-s px-m min-width-xxs\">{{ trans('common.view') }}</div>\n                <div refs=\"permissions-table@toggle-column\" class=\"flex py-s px-m min-width-xxs\">{{ trans('common.edit') }}</div>\n                <div refs=\"permissions-table@toggle-column\" class=\"flex py-s px-m min-width-xxs\">{{ trans('common.delete') }}</div>\n            </div>\n            @include('settings.roles.parts.asset-permissions-row', ['title' => trans('entities.shelves'), 'permissionPrefix' => 'bookshelf'])\n            @include('settings.roles.parts.asset-permissions-row', ['title' => trans('entities.books'), 'permissionPrefix' => 'book'])\n            @include('settings.roles.parts.asset-permissions-row', ['title' => trans('entities.chapters'), 'permissionPrefix' => 'chapter'])\n            @include('settings.roles.parts.asset-permissions-row', ['title' => trans('entities.pages'), 'permissionPrefix' => 'page'])\n            @include('settings.roles.parts.related-asset-permissions-row', ['title' => trans('entities.images'), 'permissionPrefix' => 'image'])\n            @include('settings.roles.parts.related-asset-permissions-row', ['title' => trans('entities.attachments'), 'permissionPrefix' => 'attachment'])\n            @include('settings.roles.parts.related-asset-permissions-row', ['title' => trans('entities.comments'), 'permissionPrefix' => 'comment'])\n        </div>\n\n        <div>\n            <p class=\"text-muted text-small p-m\">\n                <sup>1</sup> {{ trans('settings.role_asset_image_view_note') }}\n                <br>\n                <sup>2</sup> {{ trans('settings.role_asset_users_note') }}\n            </p>\n        </div>\n    </div>\n</div>"
  },
  {
    "path": "resources/views/settings/roles/parts/related-asset-permissions-row.blade.php",
    "content": "<div class=\"item-list-row flex-container-row items-center wrap\">\n    <div class=\"flex py-s px-m min-width-s\">\n        <strong>{{ $title }}</strong> <br>\n        <a href=\"#\" refs=\"permissions-table@toggle-row\" class=\"text-small text-link\">{{ trans('common.toggle_all') }}</a>\n    </div>\n    <div class=\"flex py-s px-m min-width-xxs\">\n        <small class=\"hide-over-m bold\">{{ trans('common.create') }}<br></small>\n        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-create-all', 'label' => ''])\n        @if($permissionPrefix === 'comment')<sup class=\"text-muted\">2</sup>@endif\n    </div>\n    <div class=\"flex py-s px-m min-width-xxs\">\n        <small class=\"hide-over-m bold\">{{ trans('common.view') }}<br></small>\n        <small class=\"faded\">{{ trans('settings.role_controlled_by_asset') }}@if($permissionPrefix === 'image')<sup class=\"text-muted\">1</sup>@endif</small>\n    </div>\n    <div class=\"flex py-s px-m min-width-xxs\">\n        <small class=\"hide-over-m bold\">{{ trans('common.edit') }}<br></small>\n        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-update-own', 'label' => trans('settings.role_own')])\n        @if($permissionPrefix === 'comment')<sup class=\"text-muted\">2</sup>@endif\n        <br>\n        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-update-all', 'label' => trans('settings.role_all')])\n        @if($permissionPrefix === 'comment')<sup class=\"text-muted\">2</sup>@endif\n    </div>\n    <div class=\"flex py-s px-m min-width-xxs\">\n        <small class=\"hide-over-m bold\">{{ trans('common.delete') }}<br></small>\n        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-delete-own', 'label' => trans('settings.role_own')])\n        <br>\n        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-delete-all', 'label' => trans('settings.role_all')])\n    </div>\n</div>"
  },
  {
    "path": "resources/views/settings/roles/parts/roles-list-item.blade.php",
    "content": "<div class=\"item-list-row flex-container-row py-xs items-center\">\n    <div class=\"py-xs px-m flex-2\">\n        <a href=\"{{ url(\"/settings/roles/{$role->id}\") }}\">{{ $role->display_name }}</a><br>\n        @if($role->mfa_enforced)\n            <small title=\"{{ trans('settings.role_mfa_enforced') }}\">@icon('lock') </small>\n        @endif\n        <small>{{ $role->description }}</small>\n    </div>\n    <div class=\"text-right flex py-xs px-m text-muted\">\n        {{ trans_choice('settings.roles_x_users_assigned', $role->users_count, ['count' => $role->users_count]) }}\n        <br>\n        {{ trans_choice('settings.roles_x_permissions_provided', $role->permissions_count, ['count' => $role->permissions_count]) }}\n    </div>\n</div>"
  },
  {
    "path": "resources/views/settings/sort-rules/create.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        @include('settings.parts.navbar', ['selected' => 'settings'])\n\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ trans('settings.sort_rule_create') }}</h1>\n\n            <form action=\"{{ url(\"/settings/sorting/rules\") }}\" method=\"POST\">\n                {{ csrf_field() }}\n                @include('settings.sort-rules.parts.form', ['model' => null])\n\n                <div class=\"form-group text-right\">\n                    <a href=\"{{ url(\"/settings/sorting\") }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                    <button type=\"submit\" class=\"button\">{{ trans('common.save') }}</button>\n                </div>\n            </form>\n        </div>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/settings/sort-rules/edit.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        @include('settings.parts.navbar', ['selected' => 'settings'])\n\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ trans('settings.sort_rule_edit') }}</h1>\n\n            <form action=\"{{ $rule->getUrl() }}\" method=\"POST\">\n                {{ method_field('PUT') }}\n                {{ csrf_field() }}\n\n                @include('settings.sort-rules.parts.form', ['model' => $rule])\n\n                <div class=\"form-group text-right\">\n                    <a href=\"{{ url(\"/settings/sorting\") }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                    <button type=\"submit\" class=\"button\">{{ trans('common.save') }}</button>\n                </div>\n            </form>\n        </div>\n\n        <div id=\"delete\" class=\"card content-wrap auto-height\">\n            <div class=\"flex-container-row items-center gap-l\">\n                <div class=\"mb-m\">\n                    <h2 class=\"list-heading\">{{ trans('settings.sort_rule_delete') }}</h2>\n                    <p class=\"text-muted mb-xs\">{{ trans('settings.sort_rule_delete_desc') }}</p>\n                    @if($errors->has('delete'))\n                        @foreach($errors->get('delete') as $error)\n                            <p class=\"text-neg mb-xs\">{{ $error }}</p>\n                        @endforeach\n                    @endif\n                </div>\n                <div class=\"flex\">\n                    <form action=\"{{ $rule->getUrl() }}\" method=\"POST\">\n                        {{ method_field('DELETE') }}\n                        {{ csrf_field() }}\n\n                        @if($errors->has('delete'))\n                            <input type=\"hidden\" name=\"confirm\" value=\"true\">\n                        @endif\n\n                        <div class=\"text-right\">\n                            <button type=\"submit\" class=\"button outline\">{{ trans('common.delete') }}</button>\n                        </div>\n                    </form>\n                </div>\n            </div>\n        </div>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/settings/sort-rules/parts/form.blade.php",
    "content": "<div class=\"setting-list\">\n    <div class=\"grid half\">\n        <div>\n            <label class=\"setting-list-label\">{{ trans('settings.sort_rule_details') }}</label>\n            <p class=\"text-muted text-small\">{{ trans('settings.sort_rule_details_desc') }}</p>\n        </div>\n        <div>\n            <div class=\"form-group\">\n                <label for=\"name\">{{ trans('common.name') }}</label>\n                @include('form.text', ['name' => 'name'])\n            </div>\n        </div>\n    </div>\n\n    <div component=\"sort-rule-manager\">\n        <label class=\"setting-list-label\">{{ trans('settings.sort_rule_operations') }}</label>\n        <p class=\"text-muted text-small\">{{ trans('settings.sort_rule_operations_desc') }}</p>\n        @include('form.errors', ['name' => 'sequence'])\n\n        <input refs=\"sort-rule-manager@input\" type=\"hidden\" name=\"sequence\"\n               value=\"{{ old('sequence') ?? $model?->sequence ?? '' }}\">\n\n        @php\n            $configuredOps = old('sequence') ? \\BookStack\\Sorting\\SortRuleOperation::fromSequence(old('sequence')) : ($model?->getOperations() ?? []);\n        @endphp\n\n        <div class=\"grid half\">\n            <div class=\"form-group\">\n                <label for=\"books\"\n                       id=\"sort-rule-configured-operations\">{{ trans('settings.sort_rule_configured_operations') }}</label>\n                <ul refs=\"sort-rule-manager@configured-operations-list\"\n                    aria-labelledby=\"sort-rule-configured-operations\"\n                    class=\"scroll-box configured-option-list\">\n                    <li class=\"text-muted empty-state px-m py-s italic text-small\">{{ trans('settings.sort_rule_configured_operations_empty') }}</li>\n\n                    @foreach($configuredOps as $operation)\n                        @include('settings.sort-rules.parts.operation', ['operation' => $operation])\n                    @endforeach\n                </ul>\n            </div>\n\n            <div class=\"form-group\">\n                <label for=\"books\"\n                       id=\"sort-rule-available-operations\">{{ trans('settings.sort_rule_available_operations') }}</label>\n                <ul refs=\"sort-rule-manager@available-operations-list\"\n                    aria-labelledby=\"sort-rule-available-operations\"\n                    class=\"scroll-box available-option-list\">\n                    <li class=\"text-muted empty-state px-m py-s italic text-small\">{{ trans('settings.sort_rule_available_operations_empty') }}</li>\n                    @foreach(\\BookStack\\Sorting\\SortRuleOperation::allExcluding($configuredOps) as $operation)\n                        @include('settings.sort-rules.parts.operation', ['operation' => $operation])\n                    @endforeach\n                </ul>\n            </div>\n        </div>\n    </div>\n</div>"
  },
  {
    "path": "resources/views/settings/sort-rules/parts/operation.blade.php",
    "content": "<li data-id=\"{{ $operation->value }}\"\n    class=\"scroll-box-item items-center\">\n    <div class=\"handle px-s\">@icon('grip')</div>\n    <div class=\"text-small\">{{ $operation->getLabel() }}</div>\n    <div class=\"buttons flex-container-row items-center ml-auto px-xxs py-xxs\">\n        <button type=\"button\" data-action=\"move_up\" class=\"icon-button p-xxs\"\n                title=\"{{ trans('entities.books_sort_move_up') }}\">@icon('chevron-up')</button>\n        <button type=\"button\" data-action=\"move_down\" class=\"icon-button p-xxs\"\n                title=\"{{ trans('entities.books_sort_move_down') }}\">@icon('chevron-down')</button>\n        <button type=\"button\" data-action=\"remove\" class=\"icon-button p-xxs\"\n                title=\"{{ trans('common.remove') }}\">@icon('remove')</button>\n        <button type=\"button\" data-action=\"add\" class=\"icon-button p-xxs\"\n                title=\"{{ trans('common.add') }}\">@icon('add-small')</button>\n    </div>\n</li>"
  },
  {
    "path": "resources/views/settings/sort-rules/parts/sort-rule-list-item.blade.php",
    "content": "<div class=\"item-list-row flex-container-row py-xs px-m gap-m items-center\">\n    <div class=\"py-xs flex\">\n        <a href=\"{{ $rule->getUrl() }}\">{{ $rule->name }}</a>\n    </div>\n    <div class=\"px-m text-small text-muted ml-auto\">\n        {{ implode(', ', array_map(fn ($op) => $op->getLabel(), $rule->getOperations())) }}\n    </div>\n    <div>\n        <span title=\"{{ trans_choice('settings.sort_rule_assigned_to_x_books', $rule->books_count ?? 0) }}\"\n              class=\"flex fill-area min-width-xxs bold text-right text-book\"><span class=\"opacity-60\">@icon('book')</span>{{ $rule->books_count ?? 0 }}</span>\n    </div>\n</div>"
  },
  {
    "path": "resources/views/settings/webhooks/create.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        @include('settings.parts.navbar', ['selected' => 'webhooks'])\n\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ trans('settings.webhooks_create') }}</h1>\n\n            <form action=\"{{ url(\"/settings/webhooks/create\") }}\" method=\"POST\">\n                {!! csrf_field() !!}\n                @include('settings.webhooks.parts.form', ['title' => trans('settings.webhooks_create')])\n\n                <div class=\"form-group text-right\">\n                    <a href=\"{{ url(\"/settings/webhooks\") }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                    <button type=\"submit\" class=\"button\">{{ trans('settings.webhooks_save') }}</button>\n                </div>\n            </form>\n        </div>\n\n        @include('settings.webhooks.parts.format-example')\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/settings/webhooks/delete.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n    <div class=\"container small\">\n\n        @include('settings.parts.navbar', ['selected' => 'webhooks'])\n\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\"> {{ trans('settings.webhooks_delete') }}</h1>\n\n            <p>{{ trans('settings.webhooks_delete_warning', ['webhookName' => $webhook->name]) }}</p>\n\n\n            <form action=\"{{ $webhook->getUrl() }}\" method=\"POST\">\n                {!! csrf_field() !!}\n                {!! method_field('DELETE') !!}\n\n                <div class=\"grid half v-center\">\n                    <div>\n                        <p class=\"text-neg\">\n                            <strong>{{ trans('settings.webhooks_delete_confirm') }}</strong>\n                        </p>\n                    </div>\n                    <div>\n                        <div class=\"form-group text-right\">\n                            <a href=\"{{ $webhook->getUrl() }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                            <button type=\"submit\" class=\"button\">{{ trans('common.confirm') }}</button>\n                        </div>\n                    </div>\n                </div>\n\n\n            </form>\n        </div>\n\n    </div>\n@stop\n"
  },
  {
    "path": "resources/views/settings/webhooks/edit.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n        @include('settings.parts.navbar', ['selected' => 'webhooks'])\n\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ trans('settings.webhooks_edit') }}</h1>\n\n\n            <div class=\"setting-list\">\n            <div class=\"grid half\">\n                <div>\n                    <label class=\"setting-list-label\">{{ trans('settings.webhooks_status') }}</label>\n                    <p class=\"mb-none\">\n                        @if($webhook->last_called_at)\n                            <span title=\"{{ $dates->absolute($webhook->last_called_at) }}\">{{ trans('settings.webhooks_last_called') }} {{  $dates->relative($webhook->last_called_at) }}</span>\n                        @else\n                            <span>{{ trans('settings.webhooks_last_called') }} {{ trans('common.never') }}</span>\n                        @endif\n                        <br>\n                        @if($webhook->last_errored_at)\n                            <span title=\"{{ $dates->absolute($webhook->last_errored_at) }}\">{{ trans('settings.webhooks_last_errored') }} {{  $dates->relative($webhook->last_errored_at) }}</span>\n                        @else\n                            <span>{{ trans('settings.webhooks_last_errored') }} {{ trans('common.never') }}</span>\n                        @endif\n                    </p>\n                </div>\n                <div class=\"text-muted\">\n                    <br>\n                    @if($webhook->last_error)\n                        {{ trans('settings.webhooks_last_error_message') }} <br>\n                        <span class=\"text-warn text-small\">{{ $webhook->last_error }}</span>\n                    @endif\n                </div>\n            </div>\n            </div>\n\n\n            <hr>\n\n            <form action=\"{{ $webhook->getUrl() }}\" method=\"POST\">\n                {!! csrf_field() !!}\n                {!! method_field('PUT') !!}\n                @include('settings.webhooks.parts.form', ['model' => $webhook, 'title' => trans('settings.webhooks_edit')])\n\n                <div class=\"form-group text-right\">\n                    <a href=\"{{ url(\"/settings/webhooks\") }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                    <a href=\"{{ $webhook->getUrl('/delete') }}\" class=\"button outline\">{{ trans('settings.webhooks_delete') }}</a>\n                    <button type=\"submit\" class=\"button\">{{ trans('settings.webhooks_save') }}</button>\n                </div>\n\n            </form>\n        </div>\n\n        @include('settings.webhooks.parts.format-example')\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/settings/webhooks/index.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        @include('settings.parts.navbar', ['selected' => 'webhooks'])\n\n        <div class=\"card content-wrap auto-height\">\n\n            <div class=\"flex-container-row items-center justify-space-between wrap\">\n                <h1 class=\"list-heading\">{{ trans('settings.webhooks') }}</h1>\n\n                <div>\n                    <a href=\"{{ url(\"/settings/webhooks/create\") }}\"\n                       class=\"button outline\">{{ trans('settings.webhooks_create') }}</a>\n                </div>\n            </div>\n\n            <p class=\"text-muted\">{{ trans('settings.webhooks_index_desc') }}</p>\n\n            <div class=\"flex-container-row items-center justify-space-between gap-m mt-m mb-l wrap\">\n                <div>\n                    <div class=\"block inline mr-xs\">\n                        <form method=\"get\" action=\"{{ url(\"/settings/webhooks\") }}\">\n                            <input type=\"text\"\n                                   name=\"search\"\n                                   placeholder=\"{{ trans('common.search') }}\"\n                                   value=\"{{ $listOptions->getSearch() }}\">\n                        </form>\n                    </div>\n                </div>\n                <div class=\"justify-flex-end\">\n                    @include('common.sort', $listOptions->getSortControlData())\n                </div>\n            </div>\n\n            @if(count($webhooks) > 0)\n                <div class=\"item-list\">\n                    @foreach($webhooks as $webhook)\n                        @include('settings.webhooks.parts.webhooks-list-item', ['webhook' => $webhook])\n                    @endforeach\n                </div>\n            @else\n                <p class=\"text-muted empty-text px-none\">\n                    {{ trans('settings.webhooks_none_created') }}\n                </p>\n            @endif\n\n            <div class=\"my-m\">\n                {{ $webhooks->links() }}\n            </div>\n\n        </div>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/settings/webhooks/parts/form.blade.php",
    "content": "<div class=\"setting-list\">\n\n    <div class=\"grid half\">\n        <div>\n            <label class=\"setting-list-label\">{{ trans('settings.webhooks_details') }}</label>\n            <p class=\"small\">{{ trans('settings.webhooks_details_desc') }}</p>\n            <div>\n                @include('form.toggle-switch', [\n                    'name' => 'active',\n                    'value' => old('active') ?? $model->active ?? true,\n                    'label' => trans('settings.webhooks_active'),\n                ])\n                @include('form.errors', ['name' => 'active'])\n            </div>\n        </div>\n        <div>\n            <div class=\"form-group\">\n                <label for=\"name\">{{ trans('settings.webhooks_name') }}</label>\n                @include('form.text', ['name' => 'name'])\n            </div>\n            <div class=\"form-group\">\n                <label for=\"endpoint\">{{ trans('settings.webhooks_endpoint') }}</label>\n                @include('form.text', ['name' => 'endpoint'])\n            </div>\n            <div class=\"form-group\">\n                <label for=\"endpoint\">{{ trans('settings.webhooks_timeout') }}</label>\n                @include('form.number', ['name' => 'timeout', 'min' => 1, 'max' => 600])\n            </div>\n        </div>\n    </div>\n\n    <div component=\"webhook-events\">\n        <label class=\"setting-list-label\">{{ trans('settings.webhooks_events') }}</label>\n        @include('form.errors', ['name' => 'events'])\n\n        <p class=\"small\">{{ trans('settings.webhooks_events_desc') }}</p>\n        <p class=\"text-warn small\">{{ trans('settings.webhooks_events_warning') }}</p>\n\n        <div class=\"toggle-switch-list\">\n            @include('form.custom-checkbox', [\n                'name' => 'events[]',\n                'value' => 'all',\n                'label' => trans('settings.webhooks_events_all'),\n                'checked' => old('events') ? in_array('all', old('events')) : (isset($webhook) ? $webhook->tracksEvent('all') : false),\n            ])\n        </div>\n\n        <hr class=\"my-s\">\n\n        <div class=\"dual-column-content toggle-switch-list\">\n            @foreach(\\BookStack\\Activity\\ActivityType::all() as $activityType)\n                <div>\n                    @include('form.custom-checkbox', [\n                       'name' => 'events[]',\n                       'value' => $activityType,\n                       'label' => $activityType,\n                       'checked' => old('events') ? in_array($activityType, old('events')) : (isset($webhook) ? $webhook->tracksEvent($activityType) : false),\n                   ])\n                </div>\n            @endforeach\n        </div>\n    </div>\n\n</div>"
  },
  {
    "path": "resources/views/settings/webhooks/parts/format-example.blade.php",
    "content": "<div component=\"code-highlighter\" class=\"card content-wrap auto-height\">\n    <h2 class=\"list-heading\">{{ trans('settings.webhooks_format_example') }}</h2>\n    <p>{{ trans('settings.webhooks_format_example_desc') }}</p>\n    <pre><code class=\"language-json\">{\n    \"event\": \"page_update\",\n    \"text\": \"Benny updated page \\\"My wonderful updated page\\\"\",\n    \"triggered_at\": \"2021-12-11T22:25:10.000000Z\",\n    \"triggered_by\": {\n        \"id\": 1,\n        \"name\": \"Benny\",\n        \"slug\": \"benny\"\n    },\n    \"triggered_by_profile_url\": \"https://bookstack.local/user/benny\",\n    \"webhook_id\": 2,\n    \"webhook_name\": \"My page update webhook\",\n    \"url\": \"https://bookstack.local/books/my-awesome-book/page/my-wonderful-updated-page\",\n    \"related_item\": {\n        \"id\": 2432,\n        \"book_id\": 13,\n        \"chapter_id\": 554,\n        \"name\": \"My wonderful updated page\",\n        \"slug\": \"my-wonderful-updated-page\",\n        \"priority\": 2,\n        \"created_at\": \"2021-12-11T21:53:24.000000Z\",\n        \"updated_at\": \"2021-12-11T22:25:10.000000Z\",\n        \"created_by\": {\n            \"id\": 1,\n            \"name\": \"Benny\",\n            \"slug\": \"benny\"\n        },\n        \"updated_by\": {\n            \"id\": 1,\n            \"name\": \"Benny\",\n            \"slug\": \"benny\"\n        },\n        \"draft\": false,\n        \"revision_count\": 9,\n        \"template\": false,\n        \"owned_by\": {\n            \"id\": 1,\n            \"name\": \"Benny\",\n            \"slug\": \"benny\"\n        },\n       \"current_revision\": {\n            \"id\": 597,\n            \"page_id\": 2598,\n            \"name\": \"My wonderful updated page\",\n            \"created_by\": 1,\n            \"created_at\": \"2021-12-11T21:53:24.000000Z\",\n            \"updated_at\": \"2021-12-11T21:53:24.000000Z\",\n            \"slug\": \"my-wonderful-updated-page\",\n            \"book_slug\": \"my-awesome-book\",\n            \"type\": \"version\",\n            \"summary\": \"Updated the title and fixed some spelling\",\n            \"revision_number\": 2\n        }\n    }\n}</code></pre>\n</div>"
  },
  {
    "path": "resources/views/settings/webhooks/parts/webhooks-list-item.blade.php",
    "content": "<div class=\"item-list-row py-s\">\n    <div class=\"flex-container-row\">\n        <div class=\"flex-2 px-m flex-container-row items-center gap-xs\">\n            @include('common.status-indicator', ['status' => $webhook->active])\n            <div>&nbsp;<a href=\"{{ $webhook->getUrl() }}\">{{ $webhook->name }}</a></div>\n        </div>\n        <div class=\"flex px-m text-right text-muted\">\n            @if($webhook->tracksEvent('all'))\n                {{ trans('settings.webhooks_events_all') }}\n            @else\n                {{ trans_choice('settings.webhooks_x_trigger_events', $webhook->tracked_events_count, ['count' =>  $webhook->tracked_events_count]) }}\n            @endif\n        </div>\n    </div>\n    <div class=\"px-m text-muted italic text-limit-lines-1\">\n        <small>{{ $webhook->endpoint }}</small>\n    </div>\n</div>"
  },
  {
    "path": "resources/views/shelves/create.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                '/shelves' => [\n                    'text' => trans('entities.shelves'),\n                    'icon' => 'bookshelf',\n                ],\n                '/create-shelf' => [\n                    'text' => trans('entities.shelves_create'),\n                    'icon' => 'add',\n                ]\n            ]])\n        </div>\n\n        <main class=\"card content-wrap\">\n            <h1 class=\"list-heading\">{{ trans('entities.shelves_create') }}</h1>\n            <form action=\"{{ url(\"/shelves\") }}\" method=\"POST\" enctype=\"multipart/form-data\">\n                @include('shelves.parts.form', ['shelf' => null, 'books' => $books])\n            </form>\n        </main>\n\n    </div>\n\n@stop"
  },
  {
    "path": "resources/views/shelves/delete.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                $shelf,\n                $shelf->getUrl('/delete') => [\n                    'text' => trans('entities.shelves_delete'),\n                    'icon' => 'delete',\n                ]\n            ]])\n        </div>\n\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ trans('entities.shelves_delete') }}</h1>\n            <p>{{ trans('entities.shelves_delete_explain', ['name' => $shelf->name]) }}</p>\n\n            <div class=\"grid half\">\n                <p class=\"text-neg\">\n                    <strong>{{ trans('entities.shelves_delete_confirmation') }}</strong>\n                </p>\n\n                <form action=\"{{ $shelf->getUrl() }}\" method=\"POST\" class=\"text-right\">\n                    {!! csrf_field() !!}\n                    <input type=\"hidden\" name=\"_method\" value=\"DELETE\">\n\n                    <a href=\"{{ $shelf->getUrl() }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                    <button type=\"submit\" class=\"button\">{{ trans('common.confirm') }}</button>\n                </form>\n            </div>\n\n\n        </div>\n    </div>\n\n@stop"
  },
  {
    "path": "resources/views/shelves/edit.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                $shelf,\n                $shelf->getUrl('/edit') => [\n                    'text' => trans('entities.shelves_edit'),\n                    'icon' => 'edit',\n                ]\n            ]])\n        </div>\n\n        <main class=\"card content-wrap\">\n            <h1 class=\"list-heading\">{{ trans('entities.shelves_edit') }}</h1>\n            <form action=\"{{ $shelf->getUrl() }}\" method=\"POST\" enctype=\"multipart/form-data\">\n                <input type=\"hidden\" name=\"_method\" value=\"PUT\">\n                @include('shelves.parts.form', ['model' => $shelf])\n            </form>\n        </main>\n    </div>\n\n@stop"
  },
  {
    "path": "resources/views/shelves/index.blade.php",
    "content": "@extends('layouts.tri')\n\n@section('body')\n    @include('shelves.parts.list', ['shelves' => $shelves, 'view' => $view, 'listOptions' => $listOptions])\n@stop\n\n@section('right')\n    @include('shelves.parts.index-sidebar-section-actions', ['view' => $view])\n@stop\n\n@section('left')\n    @include('shelves.parts.index-sidebar-section-recents', ['recents' => $recents])\n    @include('shelves.parts.index-sidebar-section-popular', ['popular' => $popular])\n    @include('shelves.parts.index-sidebar-section-new', ['new' => $new])\n@stop"
  },
  {
    "path": "resources/views/shelves/parts/form.blade.php",
    "content": "{{ csrf_field() }}\n<div class=\"form-group title-input\">\n    <label for=\"name\">{{ trans('common.name') }}</label>\n    @include('form.text', ['name' => 'name', 'autofocus' => true])\n</div>\n\n<div class=\"form-group description-input\">\n    <label for=\"description_html\">{{ trans('common.description') }}</label>\n    @include('form.description-html-input')\n</div>\n\n<div component=\"shelf-sort\" class=\"grid half gap-xl\">\n    <div class=\"form-group\">\n        <label for=\"books\" id=\"shelf-sort-books-label\">{{ trans('entities.shelves_books') }}</label>\n        <input refs=\"shelf-sort@input\" type=\"hidden\" name=\"books\"\n               value=\"{{ isset($shelf) ? $shelf->visibleBooks->implode('id', ',') : '' }}\">\n        <div class=\"scroll-box-header-item flex-container-row items-center py-xs\">\n            <span class=\"px-m py-xs\">{{ trans('entities.shelves_drag_books') }}</span>\n            <div class=\"dropdown-container ml-auto\" component=\"dropdown\">\n                <button refs=\"dropdown@toggle\"\n                        type=\"button\"\n                        title=\"{{ trans('common.more') }}\"\n                        class=\"icon-button px-xs py-xxs mx-xs text-bigger\"\n                        aria-haspopup=\"true\"\n                        aria-expanded=\"false\">\n                    @icon('more')\n                </button>\n                <div refs=\"dropdown@menu shelf-sort@sort-button-container\" class=\"dropdown-menu\" role=\"menu\">\n                    <button type=\"button\" class=\"text-item\"\n                            data-sort=\"name\">{{ trans('entities.books_sort_name') }}</button>\n                    <button type=\"button\" class=\"text-item\"\n                            data-sort=\"created\">{{ trans('entities.books_sort_created') }}</button>\n                    <button type=\"button\" class=\"text-item\"\n                            data-sort=\"updated\">{{ trans('entities.books_sort_updated') }}</button>\n                </div>\n            </div>\n        </div>\n        <ul refs=\"shelf-sort@shelf-book-list\"\n            aria-labelledby=\"shelf-sort-books-label\"\n            class=\"scroll-box configured-option-list\">\n            @foreach (($shelf->visibleBooks ?? []) as $book)\n                @include('shelves.parts.shelf-sort-book-item', ['book' => $book])\n            @endforeach\n        </ul>\n    </div>\n    <div class=\"form-group\">\n        <label for=\"books\" id=\"shelf-sort-all-books-label\">{{ trans('entities.shelves_add_books') }}</label>\n        <input type=\"text\" refs=\"shelf-sort@book-search\" class=\"scroll-box-search\"\n               placeholder=\"{{ trans('common.search') }}\">\n        <ul refs=\"shelf-sort@all-book-list\"\n            aria-labelledby=\"shelf-sort-all-books-label\"\n            class=\"scroll-box available-option-list\">\n            @foreach ($books as $book)\n                @include('shelves.parts.shelf-sort-book-item', ['book' => $book])\n            @endforeach\n        </ul>\n    </div>\n</div>\n\n\n<div class=\"form-group collapsible\" component=\"collapsible\" id=\"logo-control\">\n    <button refs=\"collapsible@trigger\" type=\"button\" class=\"collapse-title text-link\" aria-expanded=\"false\">\n        <label>{{ trans('common.cover_image') }}</label>\n    </button>\n    <div refs=\"collapsible@content\" class=\"collapse-content\">\n        <p class=\"small\">{{ trans('common.cover_image_description') }}</p>\n\n        @include('form.image-picker', [\n            'defaultImage' => url('/book_default_cover.png'),\n            'currentImage' => (($shelf ?? null)?->coverInfo()->getUrl(440, 250, null) ?? url('/book_default_cover.png')),\n            'name' => 'image',\n            'imageClass' => 'cover'\n        ])\n    </div>\n</div>\n\n<div class=\"form-group collapsible\" component=\"collapsible\" id=\"tags-control\">\n    <button refs=\"collapsible@trigger\" type=\"button\" class=\"collapse-title text-link\" aria-expanded=\"false\">\n        <label for=\"tag-manager\">{{ trans('entities.shelf_tags') }}</label>\n    </button>\n    <div refs=\"collapsible@content\" class=\"collapse-content\">\n        @include('entities.tag-manager', ['entity' => $shelf ?? null])\n    </div>\n</div>\n\n<div class=\"form-group text-right\">\n    <a href=\"{{ isset($shelf) ? $shelf->getUrl() : url('/shelves') }}\"\n       class=\"button outline\">{{ trans('common.cancel') }}</a>\n    <button type=\"submit\" class=\"button\">{{ trans('entities.shelves_save') }}</button>\n</div>\n\n@include('entities.selector-popup')\n@include('form.editor-translations')"
  },
  {
    "path": "resources/views/shelves/parts/index-sidebar-section-actions.blade.php",
    "content": "<div id=\"actions\" class=\"actions mb-xl\">\n    <h5>{{ trans('common.actions') }}</h5>\n    <div class=\"icon-list text-link\">\n        @if(userCan(\\BookStack\\Permissions\\Permission::BookshelfCreateAll))\n            <a href=\"{{ url(\"/create-shelf\") }}\" data-shortcut=\"new\" class=\"icon-list-item\">\n                <span>@icon('add')</span>\n                <span>{{ trans('entities.shelves_new_action') }}</span>\n            </a>\n        @endif\n\n        @include('entities.view-toggle', ['view' => $view, 'type' => 'bookshelves'])\n\n        <a href=\"{{ url('/tags') }}\" class=\"icon-list-item\">\n            <span>@icon('tag')</span>\n            <span>{{ trans('entities.tags_view_tags') }}</span>\n        </a>\n    </div>\n</div>"
  },
  {
    "path": "resources/views/shelves/parts/index-sidebar-section-new.blade.php",
    "content": "<div id=\"new\" class=\"mb-xl\">\n    <h5>{{ trans('entities.shelves_new') }}</h5>\n    @if(count($new) > 0)\n        @include('entities.list', ['entities' => $new, 'style' => 'compact'])\n    @else\n        <p class=\"text-muted pb-l mb-none\">{{ trans('entities.shelves_new_empty') }}</p>\n    @endif\n</div>"
  },
  {
    "path": "resources/views/shelves/parts/index-sidebar-section-popular.blade.php",
    "content": "<div id=\"popular\" class=\"mb-xl\">\n    <h5>{{ trans('entities.shelves_popular') }}</h5>\n    @if(count($popular) > 0)\n        @include('entities.list', ['entities' => $popular, 'style' => 'compact'])\n    @else\n        <p class=\"text-muted pb-l mb-none\">{{ trans('entities.shelves_popular_empty') }}</p>\n    @endif\n</div>"
  },
  {
    "path": "resources/views/shelves/parts/index-sidebar-section-recents.blade.php",
    "content": "@if($recents)\n    <div id=\"recents\" class=\"mb-xl\">\n        <h5>{{ trans('entities.recently_viewed') }}</h5>\n        @include('entities.list', ['entities' => $recents, 'style' => 'compact'])\n    </div>\n@endif"
  },
  {
    "path": "resources/views/shelves/parts/list-item.blade.php",
    "content": "<a href=\"{{ $shelf->getUrl() }}\" class=\"shelf entity-list-item\" data-entity-type=\"bookshelf\" data-entity-id=\"{{$shelf->id}}\">\n    <div class=\"entity-list-item-image bg-bookshelf @if($shelf->coverInfo()->exists()) has-image @endif\" style=\"background-image: url('{{ $shelf->coverInfo()->getUrl() }}')\">\n        @icon('bookshelf')\n    </div>\n    <div class=\"content py-xs\">\n        <h4 class=\"entity-list-item-name break-text\">{{ $shelf->name }}</h4>\n        <div class=\"entity-item-snippet\">\n            <p class=\"text-muted break-text mb-none\">{{ $shelf->getExcerpt() }}</p>\n        </div>\n    </div>\n</a>\n<div class=\"entity-shelf-books grid third gap-y-xs entity-list-item-children\">\n    @foreach($shelf->visibleBooks as $book)\n        <div>\n            <a href=\"{{ $book->getUrl('?shelf=' . $shelf->id) }}\" class=\"entity-chip text-book\">\n                @icon('book')\n                {{ $book->name }}\n            </a>\n        </div>\n    @endforeach\n</div>"
  },
  {
    "path": "resources/views/shelves/parts/list.blade.php",
    "content": "<main class=\"content-wrap mt-m card\">\n\n    <div class=\"grid half v-center\">\n        <h1 class=\"list-heading\">{{ trans('entities.shelves') }}</h1>\n        <div class=\"text-right\">\n            @include('common.sort', $listOptions->getSortControlData())\n        </div>\n    </div>\n\n    @if(count($shelves) > 0)\n        @if($view === 'list')\n            <div class=\"entity-list\">\n                @foreach($shelves as $index => $shelf)\n                    @if ($index !== 0)\n                        <hr class=\"my-m\">\n                    @endif\n                    @include('shelves.parts.list-item', ['shelf' => $shelf])\n                @endforeach\n            </div>\n        @else\n            <div class=\"grid third\">\n                @foreach($shelves as $key => $shelf)\n                    @include('entities.grid-item', ['entity' => $shelf])\n                @endforeach\n            </div>\n        @endif\n        <div>\n            {!! $shelves->render() !!}\n        </div>\n    @else\n        <p class=\"text-muted\">{{ trans('entities.shelves_empty') }}</p>\n        @if(userCan(\\BookStack\\Permissions\\Permission::BookshelfCreateAll))\n            <div class=\"icon-list block inline\">\n                <a href=\"{{ url(\"/create-shelf\") }}\"\n                   class=\"icon-list-item text-bookshelf\">\n                    <span>@icon('add')</span>\n                    <span>{{ trans('entities.create_now') }}</span>\n                </a>\n            </div>\n        @endif\n    @endif\n\n</main>\n"
  },
  {
    "path": "resources/views/shelves/parts/shelf-sort-book-item.blade.php",
    "content": "<li data-id=\"{{ $book->id }}\"\n     data-name=\"{{ $book->name }}\"\n     data-created=\"{{ $book->created_at->timestamp }}\"\n     data-updated=\"{{ $book->updated_at->timestamp }}\"\n     class=\"scroll-box-item\">\n    <div class=\"handle px-s\">@icon('grip')</div>\n    <div class=\"text-book\">@icon('book'){{ $book->name }}</div>\n    <div class=\"buttons flex-container-row items-center ml-auto px-xxs py-xs\">\n        <button type=\"button\" data-action=\"move_up\" class=\"icon-button p-xxs\"\n                title=\"{{ trans('entities.books_sort_move_up') }}\">@icon('chevron-up')</button>\n        <button type=\"button\" data-action=\"move_down\" class=\"icon-button p-xxs\"\n                title=\"{{ trans('entities.books_sort_move_down') }}\">@icon('chevron-down')</button>\n        <button type=\"button\" data-action=\"remove\" class=\"icon-button p-xxs\"\n                title=\"{{ trans('common.remove') }}\">@icon('remove')</button>\n        <button type=\"button\" data-action=\"add\" class=\"icon-button p-xxs\"\n                title=\"{{ trans('common.add') }}\">@icon('add-small')</button>\n    </div>\n</li>"
  },
  {
    "path": "resources/views/shelves/parts/show-sidebar-section-actions.blade.php",
    "content": "<div id=\"actions\" class=\"actions mb-xl\">\n    <h5>{{ trans('common.actions') }}</h5>\n    <div class=\"icon-list text-link\">\n\n        @if(userCan(\\BookStack\\Permissions\\Permission::BookCreateAll) && userCan(\\BookStack\\Permissions\\Permission::BookshelfUpdate, $shelf))\n            <a href=\"{{ $shelf->getUrl('/create-book') }}\" data-shortcut=\"new\" class=\"icon-list-item\">\n                <span class=\"icon\">@icon('add')</span>\n                <span>{{ trans('entities.books_new_action') }}</span>\n            </a>\n        @endif\n\n        @include('entities.view-toggle', ['view' => $view, 'type' => 'bookshelf'])\n\n        <hr class=\"primary-background\">\n\n        @if(userCan(\\BookStack\\Permissions\\Permission::BookshelfUpdate, $shelf))\n            <a href=\"{{ $shelf->getUrl('/edit') }}\" data-shortcut=\"edit\" class=\"icon-list-item\">\n                <span>@icon('edit')</span>\n                <span>{{ trans('common.edit') }}</span>\n            </a>\n        @endif\n\n        @if(userCan(\\BookStack\\Permissions\\Permission::RestrictionsManage, $shelf))\n            <a href=\"{{ $shelf->getUrl('/permissions') }}\" data-shortcut=\"permissions\" class=\"icon-list-item\">\n                <span>@icon('lock')</span>\n                <span>{{ trans('entities.permissions') }}</span>\n            </a>\n        @endif\n\n        @if(userCan(\\BookStack\\Permissions\\Permission::BookshelfDelete, $shelf))\n            <a href=\"{{ $shelf->getUrl('/delete') }}\" data-shortcut=\"delete\" class=\"icon-list-item\">\n                <span>@icon('delete')</span>\n                <span>{{ trans('common.delete') }}</span>\n            </a>\n        @endif\n\n        @if(!user()->isGuest())\n            <hr class=\"primary-background\">\n            @include('entities.favourite-action', ['entity' => $shelf])\n        @endif\n\n    </div>\n</div>"
  },
  {
    "path": "resources/views/shelves/parts/show-sidebar-section-activity.blade.php",
    "content": "@if(count($activity) > 0)\n    <div id=\"recent-activity\" class=\"mb-xl\">\n        <h5>{{ trans('entities.recent_activity') }}</h5>\n        @include('common.activity-list', ['activity' => $activity])\n    </div>\n@endif"
  },
  {
    "path": "resources/views/shelves/parts/show-sidebar-section-details.blade.php",
    "content": "<div id=\"details\" class=\"mb-xl\">\n    <h5>{{ trans('common.details') }}</h5>\n    <div class=\"blended-links\">\n        @include('entities.meta', ['entity' => $shelf, 'watchOptions' => null])\n        @if($shelf->hasPermissions())\n            <div class=\"active-restriction\">\n                @if(userCan(\\BookStack\\Permissions\\Permission::RestrictionsManage, $shelf))\n                    <a href=\"{{ $shelf->getUrl('/permissions') }}\" class=\"entity-meta-item\">\n                        @icon('lock')\n                        <div>{{ trans('entities.shelves_permissions_active') }}</div>\n                    </a>\n                @else\n                    <div class=\"entity-meta-item\">\n                        @icon('lock')\n                        <div>{{ trans('entities.shelves_permissions_active') }}</div>\n                    </div>\n                @endif\n            </div>\n        @endif\n    </div>\n</div>"
  },
  {
    "path": "resources/views/shelves/parts/show-sidebar-section-tags.blade.php",
    "content": "@if($shelf->tags->count() > 0)\n    <div id=\"tags\" class=\"mb-xl\">\n        @include('entities.tag-list', ['entity' => $shelf])\n    </div>\n@endif"
  },
  {
    "path": "resources/views/shelves/permissions.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                $shelf,\n                $shelf->getUrl('/permissions') => [\n                    'text' => trans('entities.shelves_permissions'),\n                    'icon' => 'lock',\n                ]\n            ]])\n        </div>\n\n        <div class=\"card content-wrap auto-height\">\n            @include('form.entity-permissions', ['model' => $shelf, 'title' => trans('entities.shelves_permissions')])\n        </div>\n\n        <div class=\"card content-wrap auto-height flex-container-row items-center gap-x-xl wrap\">\n            <div class=\"flex\">\n                <h2 class=\"list-heading\">{{ trans('entities.shelves_copy_permissions_to_books') }}</h2>\n                <p>{{ trans('entities.shelves_copy_permissions_explain') }}</p>\n            </div>\n            <form action=\"{{ $shelf->getUrl('/copy-permissions') }}\" method=\"post\" class=\"flex text-right\">\n                {{ csrf_field() }}\n                <button class=\"button\">{{ trans('entities.shelves_copy_permissions') }}</button>\n            </form>\n        </div>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/shelves/references.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        <div class=\"my-s\">\n            @include('entities.breadcrumbs', ['crumbs' => [\n                $shelf,\n                $shelf->getUrl('/references') => [\n                    'text' => trans('entities.references'),\n                    'icon' => 'reference',\n                ]\n            ]])\n        </div>\n\n        @include('entities.references', ['references' => $references])\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/shelves/show.blade.php",
    "content": "@extends('layouts.tri')\n\n@push('social-meta')\n    <meta property=\"og:description\" content=\"{{ Str::limit($shelf->description, 100, '...') }}\">\n    @if($shelf->coverInfo()->exists())\n        <meta property=\"og:image\" content=\"{{ $shelf->coverInfo()->getUrl() }}\">\n    @endif\n@endpush\n\n@include('entities.body-tag-classes', ['entity' => $shelf])\n\n@section('body')\n\n    <div class=\"mb-s print-hidden\">\n        @include('entities.breadcrumbs', ['crumbs' => [\n            $shelf,\n        ]])\n    </div>\n\n    <main class=\"card content-wrap\">\n\n        <div class=\"flex-container-row wrap v-center\">\n            <h1 class=\"flex fit-content break-text\">{{ $shelf->name }}</h1>\n            <div class=\"flex\"></div>\n            <div class=\"flex fit-content text-m-right my-m ml-m\">\n                @include('common.sort', $listOptions->getSortControlData())\n            </div>\n        </div>\n\n        <div class=\"book-content\">\n            <div class=\"text-muted break-text\">{!! $shelf->descriptionInfo()->getHtml() !!}</div>\n            @if(count($sortedVisibleShelfBooks) > 0)\n                @if($view === 'list')\n                    <div class=\"entity-list\">\n                        @foreach($sortedVisibleShelfBooks as $book)\n                            @include('books.parts.list-item', ['book' => $book])\n                        @endforeach\n                    </div>\n                @else\n                    <div class=\"grid third\">\n                        @foreach($sortedVisibleShelfBooks as $book)\n                            @include('entities.grid-item', ['entity' => $book])\n                        @endforeach\n                    </div>\n                @endif\n            @else\n                <div class=\"mt-xl\">\n                    <hr>\n                    <p class=\"text-muted italic mt-xl mb-m\">{{ trans('entities.shelves_empty_contents') }}</p>\n                    <div class=\"icon-list inline block\">\n                        @if(userCan(\\BookStack\\Permissions\\Permission::BookCreateAll) && userCan(\\BookStack\\Permissions\\Permission::BookshelfUpdate, $shelf))\n                            <a href=\"{{ $shelf->getUrl('/create-book') }}\" class=\"icon-list-item text-book\">\n                                <span class=\"icon\">@icon('add')</span>\n                                <span>{{ trans('entities.books_create') }}</span>\n                            </a>\n                        @endif\n                        @if(userCan(\\BookStack\\Permissions\\Permission::BookshelfUpdate, $shelf))\n                            <a href=\"{{ $shelf->getUrl('/edit') }}\" class=\"icon-list-item text-bookshelf\">\n                                <span class=\"icon\">@icon('edit')</span>\n                                <span>{{ trans('entities.shelves_edit_and_assign') }}</span>\n                            </a>\n                        @endif\n                    </div>\n                </div>\n            @endif\n        </div>\n    </main>\n\n@stop\n\n@section('left')\n    @include('shelves.parts.show-sidebar-section-tags', ['shelf' => $shelf])\n    @include('shelves.parts.show-sidebar-section-details', ['shelf' => $shelf])\n    @include('shelves.parts.show-sidebar-section-activity', ['activity' => $activity])\n@stop\n\n@section('right')\n    @include('shelves.parts.show-sidebar-section-actions', ['shelf' => $shelf, 'view' => $view])\n@stop\n\n\n\n\n"
  },
  {
    "path": "resources/views/tags/index.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n    <div class=\"container small\">\n\n        <main class=\"card content-wrap mt-xxl\">\n\n            <h1 class=\"list-heading\">{{ trans('entities.tags') }}</h1>\n\n            <p class=\"text-muted\">{{ trans('entities.tags_index_desc') }}</p>\n\n            <div class=\"flex-container-row wrap justify-space-between items-center mb-s gap-m\">\n                <div class=\"block inline mr-xs\">\n                    <form method=\"get\" action=\"{{ url(\"/tags\") }}\">\n                        @include('form.request-query-inputs', ['params' => ['name']])\n                        <input type=\"text\"\n                               name=\"search\"\n                               placeholder=\"{{ trans('common.search') }}\"\n                               value=\"{{ $listOptions->getSearch() }}\">\n                    </form>\n                </div>\n                <div class=\"block inline\">\n                    @include('common.sort', $listOptions->getSortControlData())\n                </div>\n            </div>\n\n            @if($nameFilter)\n                <div class=\"my-m\">\n                    <strong class=\"mr-xs\">{{ trans('common.filter_active') }}</strong>\n                    @include('entities.tag', ['tag' => new \\BookStack\\Activity\\Models\\Tag(['name' => $nameFilter])])\n                    <form method=\"get\" action=\"{{ url(\"/tags\") }}\" class=\"inline block\">\n                        @include('form.request-query-inputs', ['params' => ['search']])\n                        <button class=\"text-button text-warn\">@icon('close'){{ trans('common.filter_clear') }}</button>\n                    </form>\n                </div>\n            @endif\n\n            @if(count($tags) > 0)\n                <div class=\"item-list mt-m\">\n                    @foreach($tags as $tag)\n                        @include('tags.parts.tags-list-item', ['tag' => $tag, 'nameFilter' => $nameFilter])\n                    @endforeach\n                </div>\n\n                <div class=\"my-m\">\n                    {{ $tags->links() }}\n                </div>\n            @else\n                <p class=\"text-muted italic my-xl\">\n                    {{ trans('common.no_items') }}.\n                    <br>\n                    {{ trans('entities.tags_list_empty_hint') }}\n                </p>\n            @endif\n        </main>\n\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/tags/parts/tags-list-item.blade.php",
    "content": "<div class=\"item-list-row flex-container-row items-center wrap\">\n    <div class=\"{{ isset($nameFilter) && $tag->value ? 'flex-2' : 'flex' }} py-s px-m min-width-m\">\n        <span class=\"text-bigger mr-xl\">@include('entities.tag', ['tag' => $tag])</span>\n    </div>\n    <div class=\"flex-2 flex-container-row justify-center items-center gap-m py-s px-m min-width-l wrap\">\n        <a href=\"{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() }}\"\n           title=\"{{ trans('entities.tags_usages') }}\"\n           class=\"flex fill-area min-width-xxs bold text-right text-muted\"><span class=\"opacity-60\">@icon('leaderboard')</span>{{ $tag->usages }}</a>\n        <a href=\"{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() . '+{type:page}' }}\"\n           title=\"{{ trans('entities.tags_assigned_pages') }}\"\n           class=\"flex fill-area min-width-xxs bold text-right text-page\"><span class=\"opacity-60\">@icon('page')</span>{{ $tag->page_count }}</a>\n        <a href=\"{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() . '+{type:chapter}' }}\"\n           title=\"{{ trans('entities.tags_assigned_chapters') }}\"\n           class=\"flex fill-area min-width-xxs bold text-right text-chapter\"><span class=\"opacity-60\">@icon('chapter')</span>{{ $tag->chapter_count }}</a>\n        <a href=\"{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() . '+{type:book}' }}\"\n           title=\"{{ trans('entities.tags_assigned_books') }}\"\n           class=\"flex fill-area min-width-xxs bold text-right text-book\"><span class=\"opacity-60\">@icon('book')</span>{{ $tag->book_count }}</a>\n        <a href=\"{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() . '+{type:bookshelf}' }}\"\n           title=\"{{ trans('entities.tags_assigned_shelves') }}\"\n           class=\"flex fill-area min-width-xxs bold text-right text-bookshelf\"><span class=\"opacity-60\">@icon('bookshelf')</span>{{ $tag->shelf_count }}</a>\n    </div>\n    @if($tag->values ?? false)\n        <div class=\"flex text-s-right text-muted py-s px-m min-width-s\">\n            <a href=\"{{ url('/tags?name=' . urlencode($tag->name)) }}\">{{ trans('entities.tags_x_unique_values', ['count' => $tag->values]) }}</a>\n        </div>\n    @elseif(empty($nameFilter))\n        <div class=\"flex text-s-right text-muted py-s px-m min-width-s\">\n            <a href=\"{{ url('/tags?name=' . urlencode($tag->name)) }}\">{{ trans('entities.tags_all_values') }}</a>\n        </div>\n    @endif\n</div>"
  },
  {
    "path": "resources/views/users/account/auth.blade.php",
    "content": "@extends('users.account.layout')\n\n@section('main')\n\n    @if($authMethod === 'standard')\n        <section class=\"card content-wrap auto-height\">\n            <form action=\"{{ url('/my-account/auth/password') }}\" method=\"post\">\n                {{ method_field('put') }}\n                {{ csrf_field() }}\n\n                <h2 class=\"list-heading\">{{ trans('preferences.auth_change_password') }}</h2>\n\n                <p class=\"text-muted text-small\">\n                    {{ trans('preferences.auth_change_password_desc') }}\n                </p>\n\n                <div class=\"grid half mt-m gap-xl wrap stretch-inputs mb-m\">\n                    <div>\n                        <label for=\"password\">{{ trans('auth.password') }}</label>\n                        @include('form.password', ['name' => 'password', 'autocomplete' => 'new-password'])\n                    </div>\n                    <div>\n                        <label for=\"password-confirm\">{{ trans('auth.password_confirm') }}</label>\n                        @include('form.password', ['name' => 'password-confirm'])\n                    </div>\n                </div>\n\n                <div class=\"form-group text-right\">\n                    <button class=\"button\">{{ trans('common.update') }}</button>\n                </div>\n\n            </form>\n        </section>\n    @endif\n\n    <section class=\"card content-wrap auto-height items-center flex-container-row gap-m gap-x-l wrap justify-space-between\">\n        <div class=\"flex-min-width-m\">\n            <h2 class=\"list-heading\">{{ trans('settings.users_mfa') }}</h2>\n            <p class=\"text-muted text-small\">{{ trans('settings.users_mfa_desc') }}</p>\n            <p class=\"text-muted\">\n                @if ($mfaMethods->count() > 0)\n                    <span class=\"text-pos\">@icon('check-circle')</span>\n                @else\n                    <span class=\"text-neg\">@icon('cancel')</span>\n                @endif\n                {{ trans_choice('settings.users_mfa_x_methods', $mfaMethods->count()) }}\n            </p>\n        </div>\n        <div class=\"text-right\">\n            <a href=\"{{ url('/mfa/setup')  }}\"\n               class=\"button outline\">{{ trans('common.manage') }}</a>\n        </div>\n    </section>\n\n    @if(count($activeSocialDrivers) > 0)\n        <section id=\"social-accounts\" class=\"card content-wrap auto-height\">\n            <h2 class=\"list-heading\">{{ trans('settings.users_social_accounts') }}</h2>\n            <p class=\"text-muted text-small\">{{ trans('settings.users_social_accounts_info') }}</p>\n            <div class=\"container\">\n                <div class=\"grid third\">\n                    @foreach($activeSocialDrivers as $driver => $enabled)\n                        <div class=\"text-center mb-m\">\n                            <div role=\"presentation\">@icon('auth/'. $driver, ['style' => 'width: 56px;height: 56px;'])</div>\n                            <div>\n                                @if(user()->hasSocialAccount($driver))\n                                    <form action=\"{{ url(\"/login/service/{$driver}/detach\") }}\" method=\"POST\">\n                                        {{ csrf_field() }}\n                                        <button aria-label=\"{{ trans('settings.users_social_disconnect') }} - {{ $driver }}\"\n                                                class=\"button small outline\">{{ trans('settings.users_social_disconnect') }}</button>\n                                    </form>\n                                @else\n                                    <a href=\"{{ url(\"/login/service/{$driver}\") }}\"\n                                       aria-label=\"{{ trans('settings.users_social_connect') }} - {{ $driver }}\"\n                                       class=\"button small outline\">{{ trans('settings.users_social_connect') }}</a>\n                                @endif\n                            </div>\n                        </div>\n                    @endforeach\n                </div>\n            </div>\n        </section>\n    @endif\n\n    @if(userCan(\\BookStack\\Permissions\\Permission::AccessApi))\n        @include('users.api-tokens.parts.list', ['user' => user(), 'context' => 'my-account'])\n    @endif\n@stop\n"
  },
  {
    "path": "resources/views/users/account/delete.blade.php",
    "content": "@extends('users.account.layout')\n\n@section('main')\n\n    <div class=\"card content-wrap auto-height\">\n        <form action=\"{{ url(\"/my-account\") }}\" method=\"POST\">\n            {{ csrf_field() }}\n            {{ method_field('delete') }}\n\n\n            <h1 class=\"list-heading\">{{ trans('preferences.delete_my_account') }}</h1>\n\n            <p>{{ trans('preferences.delete_my_account_desc') }}</p>\n\n            @if(userCan(\\BookStack\\Permissions\\Permission::UsersManage))\n                <hr class=\"my-l\">\n\n                <div class=\"grid half gap-xl v-center\">\n                    <div>\n                        <label class=\"setting-list-label\">{{ trans('settings.users_migrate_ownership') }}</label>\n                        <p class=\"small\">{{ trans('settings.users_migrate_ownership_desc') }}</p>\n                    </div>\n                    <div>\n                        @include('form.user-select', ['name' => 'new_owner_id', 'user' => null])\n                    </div>\n                </div>\n            @endif\n\n            <hr class=\"my-l\">\n\n            <div class=\"grid half\">\n                <p class=\"text-neg\"><strong>{{ trans('preferences.delete_my_account_warning') }}</strong></p>\n                <div class=\"text-right\">\n                    <a href=\"{{ url(\"/my-account/profile\") }}\"\n                       class=\"button outline\">{{ trans('common.cancel') }}</a>\n                    <button type=\"submit\" class=\"button\">{{ trans('common.confirm') }}</button>\n                </div>\n            </div>\n\n        </form>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/users/account/layout.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n    <div class=\"container medium\">\n\n        <div class=\"grid gap-xxl right-focus my-xl\">\n\n            <div>\n                <div class=\"sticky-top-m\">\n                    <h5>{{ trans('preferences.my_account') }}</h5>\n                    <nav class=\"active-link-list in-sidebar\">\n                        <a href=\"{{ url('/my-account/profile') }}\" class=\"{{ $category === 'profile' ? 'active' : '' }}\">@icon('user') {{ trans('preferences.profile') }}</a>\n                        <a href=\"{{ url('/my-account/auth') }}\" class=\"{{ $category === 'auth' ? 'active' : '' }}\">@icon('security') {{ trans('preferences.auth') }}</a>\n                        <a href=\"{{ url('/my-account/shortcuts') }}\" class=\"{{ $category === 'shortcuts' ? 'active' : '' }}\">@icon('shortcuts') {{ trans('preferences.shortcuts_interface') }}</a>\n                        @if(userCan(\\BookStack\\Permissions\\Permission::ReceiveNotifications))\n                            <a href=\"{{ url('/my-account/notifications') }}\" class=\"{{ $category === 'notifications' ? 'active' : '' }}\">@icon('notifications') {{ trans('preferences.notifications') }}</a>\n                        @endif\n                    </nav>\n                </div>\n            </div>\n\n            <div>\n                @yield('main')\n            </div>\n\n        </div>\n\n    </div>\n@stop"
  },
  {
    "path": "resources/views/users/account/notifications.blade.php",
    "content": "@extends('users.account.layout')\n\n@section('main')\n    <section class=\"card content-wrap auto-height\">\n        <form action=\"{{ url('/my-account/notifications') }}\" method=\"post\">\n            {{ method_field('put') }}\n            {{ csrf_field() }}\n\n            <h1 class=\"list-heading\">{{ trans('preferences.notifications') }}</h1>\n            <p class=\"text-small text-muted\">{{ trans('preferences.notifications_desc') }}</p>\n\n            <div class=\"flex-container-row wrap justify-space-between pb-m\">\n                <div class=\"toggle-switch-list min-width-l\">\n                    <div>\n                        @include('form.toggle-switch', [\n                            'name' => 'preferences[own-page-changes]',\n                            'value' => $preferences->notifyOnOwnPageChanges(),\n                            'label' => trans('preferences.notifications_opt_own_page_changes'),\n                        ])\n                    </div>\n                    @if (!setting('app-disable-comments'))\n                        <div>\n                            @include('form.toggle-switch', [\n                                'name' => 'preferences[own-page-comments]',\n                                'value' => $preferences->notifyOnOwnPageComments(),\n                                'label' => trans('preferences.notifications_opt_own_page_comments'),\n                            ])\n                        </div>\n                        <div>\n                            @include('form.toggle-switch', [\n                                'name' => 'preferences[comment-replies]',\n                                'value' => $preferences->notifyOnCommentReplies(),\n                                'label' => trans('preferences.notifications_opt_comment_replies'),\n                            ])\n                        </div>\n                        <div>\n                            @include('form.toggle-switch', [\n                                'name' => 'preferences[comment-mentions]',\n                                'value' => $preferences->notifyOnCommentMentions(),\n                                'label' => trans('preferences.notifications_opt_comment_mentions'),\n                            ])\n                        </div>\n                    @endif\n                </div>\n\n                <div class=\"mt-auto\">\n                    <button class=\"button\">{{ trans('preferences.notifications_save') }}</button>\n                </div>\n            </div>\n\n        </form>\n    </section>\n\n    <section class=\"card content-wrap auto-height\">\n        <h2 class=\"list-heading\">{{ trans('preferences.notifications_watched') }}</h2>\n        <p class=\"text-small text-muted\">{{ trans('preferences.notifications_watched_desc') }}</p>\n\n        @if($watches->isEmpty())\n            <p class=\"text-muted italic\">{{ trans('common.no_items') }}</p>\n        @else\n            <div class=\"item-list\">\n                @foreach($watches as $watch)\n                    <div class=\"flex-container-row justify-space-between item-list-row items-center wrap px-m py-s\">\n                        <div class=\"py-xs px-s min-width-m\">\n                            @include('entities.icon-link', ['entity' => $watch->watchable])\n                        </div>\n                        <div class=\"py-xs min-width-m text-m-right px-m\">\n                            @icon('watch' . ($watch->ignoring() ? '-ignore' : ''))\n                            {{ trans('entities.watch_title_' . $watch->getLevelName()) }}\n                        </div>\n                    </div>\n                @endforeach\n            </div>\n        @endif\n\n        <div class=\"my-m\">{{ $watches->links() }}</div>\n    </section>\n@stop\n"
  },
  {
    "path": "resources/views/users/account/parts/shortcut-control.blade.php",
    "content": "<div class=\"flex-container-row justify-space-between items-center gap-m item-list-row\">\n    <label for=\"shortcut-{{ $id }}\" class=\"bold flex px-m py-xs\">{{ $label }}</label>\n    <div class=\"px-m py-xs\">\n        <input type=\"text\"\n               component=\"shortcut-input\"\n               class=\"small flex-none shortcut-input px-s py-xs\"\n               id=\"shortcut-{{ $id }}\"\n               name=\"shortcut[{{ $id }}]\"\n               readonly\n               value=\"{{ $shortcuts->getShortcut($id) }}\">\n    </div>\n</div>"
  },
  {
    "path": "resources/views/users/account/profile.blade.php",
    "content": "@extends('users.account.layout')\n\n@section('main')\n\n    <section class=\"card content-wrap auto-height\">\n        <form action=\"{{ url('/my-account/profile') }}\" method=\"post\" enctype=\"multipart/form-data\">\n            {{ method_field('put') }}\n            {{ csrf_field() }}\n\n            <div class=\"flex-container-row gap-l items-center wrap justify-space-between\">\n                <h1 class=\"list-heading\">{{ trans('preferences.profile') }}</h1>\n                <div>\n                    <a href=\"{{ user()->getProfileUrl() }}\" class=\"button outline\">{{ trans('preferences.profile_view_public') }}</a>\n                </div>\n            </div>\n\n            <p class=\"text-muted text-small mb-none\">{{ trans('preferences.profile_desc') }}</p>\n\n            <div class=\"setting-list\">\n\n                <div class=\"flex-container-row gap-l items-center wrap\">\n                    <div class=\"flex\">\n                        <label class=\"setting-list-label\" for=\"name\">{{ trans('auth.name') }}</label>\n                        <p class=\"text-small mb-none\">{{ trans('preferences.profile_name_desc') }}</p>\n                    </div>\n                    <div class=\"flex stretch-inputs\">\n                        @include('form.text', ['name' => 'name'])\n                    </div>\n                </div>\n\n                <div>\n                    <div class=\"flex-container-row gap-l items-center wrap\">\n                        <div class=\"flex\">\n                            <label class=\"setting-list-label\" for=\"email\">{{ trans('auth.email') }}</label>\n                            <p class=\"text-small mb-none\">{{ trans('preferences.profile_email_desc') }}</p>\n                        </div>\n                        <div class=\"flex stretch-inputs\">\n                            @include('form.text', ['name' => 'email', 'disabled' => !userCan(\\BookStack\\Permissions\\Permission::UsersManage)])\n                        </div>\n                    </div>\n                    @if(!userCan(\\BookStack\\Permissions\\Permission::UsersManage))\n                        <p class=\"text-small text-muted\">{{ trans('preferences.profile_email_no_permission') }}</p>\n                    @endif\n                </div>\n\n                <div class=\"grid half gap-xl\">\n                    <div>\n                        <label for=\"user-avatar\"\n                               class=\"setting-list-label\">{{ trans('settings.users_avatar') }}</label>\n                        <p class=\"text-small\">{{ trans('preferences.profile_avatar_desc') }}</p>\n                    </div>\n                    <div>\n                        @include('form.image-picker', [\n                            'resizeHeight' => '512',\n                            'resizeWidth' => '512',\n                            'showRemove' => false,\n                            'defaultImage' => url('/user_avatar.png'),\n                            'currentImage' => user()->getAvatar(80),\n                            'currentId' => user()->image_id,\n                            'name' => 'profile_image',\n                            'imageClass' => 'avatar large'\n                        ])\n                    </div>\n                </div>\n\n                @include('users.parts.language-option-row', ['value' => old('language') ?? user()->getLocale()->appLocale()])\n\n            </div>\n\n            <div class=\"form-group text-right\">\n                <a href=\"{{ url('/my-account/delete') }}\" class=\"button outline\">{{ trans('preferences.delete_account') }}</a>\n                <button class=\"button\">{{ trans('common.save') }}</button>\n            </div>\n\n        </form>\n    </section>\n\n    @if(userCan(\\BookStack\\Permissions\\Permission::UsersManage))\n        <section class=\"card content-wrap auto-height\">\n            <div class=\"flex-container-row gap-l items-center wrap\">\n                <div class=\"flex\">\n                    <h2 class=\"list-heading\">{{ trans('preferences.profile_admin_options') }}</h2>\n                    <p class=\"text-small\">{{ trans('preferences.profile_admin_options_desc') }}</p>\n                </div>\n                <div class=\"text-m-right\">\n                    <a class=\"button outline\" href=\"{{ user()->getEditUrl() }}\">{{ trans('common.open') }}</a>\n                </div>\n            </div>\n        </section>\n    @endif\n@stop\n"
  },
  {
    "path": "resources/views/users/account/shortcuts.blade.php",
    "content": "@extends('users.account.layout')\n\n@section('main')\n    <section class=\"card content-wrap\">\n        <form action=\"{{ url('/my-account/shortcuts') }}\" method=\"post\">\n            {{ method_field('put') }}\n            {{ csrf_field() }}\n\n            <h1 class=\"list-heading\">{{ trans('preferences.shortcuts_interface') }}</h1>\n\n            <div class=\"flex-container-row items-center gap-m wrap mb-m\">\n                <p class=\"flex mb-none min-width-m text-small text-muted\">\n                    {{ trans('preferences.shortcuts_toggle_desc') }}\n                    {{ trans('preferences.shortcuts_customize_desc') }}\n                </p>\n                <div class=\"flex min-width-m text-m-center\">\n                    @include('form.toggle-switch', [\n                        'name' => 'enabled',\n                        'value' => $enabled,\n                        'label' => trans('preferences.shortcuts_toggle_label'),\n                    ])\n                </div>\n            </div>\n\n            <hr>\n\n            <h2 class=\"list-heading mb-m\">{{ trans('preferences.shortcuts_section_navigation') }}</h2>\n            <div class=\"flex-container-row wrap gap-m mb-xl\">\n                <div class=\"flex min-width-l item-list\">\n                    @include('users.account.parts.shortcut-control', ['label' => trans('common.homepage'), 'id' => 'home_view'])\n                    @include('users.account.parts.shortcut-control', ['label' => trans('entities.shelves'), 'id' => 'shelves_view'])\n                    @include('users.account.parts.shortcut-control', ['label' => trans('entities.books'), 'id' => 'books_view'])\n                    @include('users.account.parts.shortcut-control', ['label' => trans('settings.settings'), 'id' => 'settings_view'])\n                    @include('users.account.parts.shortcut-control', ['label' => trans('entities.my_favourites'), 'id' => 'favourites_view'])\n                </div>\n                <div class=\"flex min-width-l item-list\">\n                    @include('users.account.parts.shortcut-control', ['label' => trans('common.view_profile'), 'id' => 'profile_view'])\n                    @include('users.account.parts.shortcut-control', ['label' => trans('auth.logout'), 'id' => 'logout'])\n                    @include('users.account.parts.shortcut-control', ['label' => trans('common.global_search'), 'id' => 'global_search'])\n                    @include('users.account.parts.shortcut-control', ['label' => trans('common.next'), 'id' => 'next'])\n                    @include('users.account.parts.shortcut-control', ['label' => trans('common.previous'), 'id' => 'previous'])\n                </div>\n            </div>\n\n            <h2 class=\"list-heading mb-m\">{{ trans('preferences.shortcuts_section_actions') }}</h2>\n            <div class=\"flex-container-row wrap gap-m mb-xl\">\n                <div class=\"flex min-width-l item-list\">\n                    @include('users.account.parts.shortcut-control', ['label' => trans('common.new'), 'id' => 'new'])\n                    @include('users.account.parts.shortcut-control', ['label' => trans('common.edit'), 'id' => 'edit'])\n                    @include('users.account.parts.shortcut-control', ['label' => trans('common.copy'), 'id' => 'copy'])\n                    @include('users.account.parts.shortcut-control', ['label' => trans('common.delete'), 'id' => 'delete'])\n                    @include('users.account.parts.shortcut-control', ['label' => trans('common.favourite'), 'id' => 'favourite'])\n                </div>\n                <div class=\"flex min-width-l item-list\">\n                    @include('users.account.parts.shortcut-control', ['label' => trans('entities.export'), 'id' => 'export'])\n                    @include('users.account.parts.shortcut-control', ['label' => trans('common.sort'), 'id' => 'sort'])\n                    @include('users.account.parts.shortcut-control', ['label' => trans('entities.permissions'), 'id' => 'permissions'])\n                    @include('users.account.parts.shortcut-control', ['label' => trans('common.move'), 'id' => 'move'])\n                    @include('users.account.parts.shortcut-control', ['label' => trans('entities.revisions'), 'id' => 'revisions'])\n                </div>\n            </div>\n\n            <p class=\"text-small text-muted\">{{ trans('preferences.shortcuts_overlay_desc') }}</p>\n\n            <div class=\"form-group text-right\">\n                <button class=\"button\">{{ trans('preferences.shortcuts_save') }}</button>\n            </div>\n\n        </form>\n    </section>\n@stop\n"
  },
  {
    "path": "resources/views/users/api-tokens/create.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small pt-xl\">\n\n        <main class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ trans('settings.user_api_token_create') }}</h1>\n\n            <form action=\"{{ url('/api-tokens/' . $user->id . '/create') }}\" method=\"post\">\n                {{ csrf_field() }}\n\n                <div class=\"setting-list\">\n                    @include('users.api-tokens.parts.form')\n\n                    <div>\n                        <p class=\"text-warn italic\">\n                            {{ trans('settings.user_api_token_create_secret_message') }}\n                        </p>\n                    </div>\n                </div>\n\n                <div class=\"form-group text-right\">\n                    <a href=\"{{ $back }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                    <button class=\"button\" type=\"submit\">{{ trans('common.save') }}</button>\n                </div>\n\n            </form>\n\n        </main>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/users/api-tokens/delete.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n    <div class=\"container small pt-xl\">\n\n        <div class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ trans('settings.user_api_token_delete') }}</h1>\n\n            <p>{{ trans('settings.user_api_token_delete_warning', ['tokenName' => $token->name]) }}</p>\n\n            <div class=\"grid half\">\n                <p class=\"text-neg\"><strong>{{ trans('settings.user_api_token_delete_confirm') }}</strong></p>\n                <div>\n                    <form action=\"{{ $token->getUrl() }}\" method=\"POST\" class=\"text-right\">\n                        {{ csrf_field() }}\n                        {{ method_field('delete') }}\n\n                        <a href=\"{{ $token->getUrl() }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                        <button type=\"submit\" class=\"button\">{{ trans('common.confirm') }}</button>\n                    </form>\n                </div>\n            </div>\n\n        </div>\n    </div>\n@stop\n"
  },
  {
    "path": "resources/views/users/api-tokens/edit.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small pt-xl\">\n\n        <main class=\"card content-wrap auto-height\">\n            <h1 class=\"list-heading\">{{ trans('settings.user_api_token') }}</h1>\n\n            <form action=\"{{ $token->getUrl() }}\" method=\"post\">\n                {{ method_field('put') }}\n                {{ csrf_field() }}\n\n                <div class=\"setting-list\">\n\n                    <div class=\"grid half gap-xl v-center\">\n                        <div>\n                            <label class=\"setting-list-label\">{{ trans('settings.user_api_token_id') }}</label>\n                            <p class=\"small\">{{ trans('settings.user_api_token_id_desc') }}</p>\n                        </div>\n                        <div>\n                            @include('form.text', ['name' => 'token_id', 'readonly' => true])\n                        </div>\n                    </div>\n\n\n                    @if( $secret )\n                        <div class=\"grid half gap-xl v-center\">\n                            <div>\n                                <label class=\"setting-list-label\">{{ trans('settings.user_api_token_secret') }}</label>\n                                <p class=\"small text-warn\">{{ trans('settings.user_api_token_secret_desc') }}</p>\n                            </div>\n                            <div>\n                                <input type=\"text\" readonly=\"readonly\" value=\"{{ $secret }}\">\n                            </div>\n                        </div>\n                    @endif\n\n                    @include('users.api-tokens.parts.form', ['model' => $token])\n                </div>\n\n                <div class=\"grid half gap-xl v-center\">\n\n                    <div class=\"text-muted text-small\">\n                        <span title=\"{{ $dates->absolute($token->created_at) }}\">\n                            {{ trans('settings.user_api_token_created', ['timeAgo' => $dates->relative($token->created_at)]) }}\n                        </span>\n                        <br>\n                        <span title=\"{{ $dates->absolute($token->updated_at) }}\">\n                            {{ trans('settings.user_api_token_updated', ['timeAgo' => $dates->relative($token->created_at)]) }}\n                        </span>\n                    </div>\n\n                    <div class=\"form-group text-right\">\n                        <a href=\"{{  $back }}\" class=\"button outline\">{{ trans('common.back') }}</a>\n                        <a href=\"{{  $token->getUrl('/delete') }}\" class=\"button outline\">{{ trans('settings.user_api_token_delete') }}</a>\n                        <button class=\"button\" type=\"submit\">{{ trans('common.save') }}</button>\n                    </div>\n                </div>\n\n            </form>\n\n        </main>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/users/api-tokens/parts/form.blade.php",
    "content": "\n\n<div class=\"grid half gap-xl v-center\">\n    <div>\n        <label class=\"setting-list-label\">{{ trans('settings.user_api_token_name') }}</label>\n        <p class=\"small\">{{ trans('settings.user_api_token_name_desc') }}</p>\n    </div>\n    <div>\n        @include('form.text', ['name' => 'name'])\n    </div>\n</div>\n\n<div class=\"grid half gap-xl v-center\">\n    <div>\n        <label class=\"setting-list-label\">{{ trans('settings.user_api_token_expiry') }}</label>\n        <p class=\"small\">{{ trans('settings.user_api_token_expiry_desc') }}</p>\n    </div>\n    <div class=\"text-right\">\n        @include('form.date', ['name' => 'expires_at'])\n    </div>\n</div>"
  },
  {
    "path": "resources/views/users/api-tokens/parts/list.blade.php",
    "content": "<section class=\"card content-wrap auto-height\" id=\"api_tokens\">\n    <div class=\"flex-container-row wrap justify-space-between items-center mb-s\">\n        <h2 class=\"list-heading\">{{ trans('settings.users_api_tokens') }}</h2>\n        <div class=\"text-right pt-xs\">\n            @if(userCan(\\BookStack\\Permissions\\Permission::AccessApi))\n                <a href=\"{{ url('/api/docs') }}\" class=\"button outline\">{{ trans('settings.users_api_tokens_docs') }}</a>\n                <a href=\"{{ url('/api-tokens/' . $user->id . '/create?context=' . $context) }}\" class=\"button outline\">{{ trans('settings.users_api_tokens_create') }}</a>\n            @endif\n        </div>\n    </div>\n    <p class=\"text-small text-muted\">{{ trans('settings.users_api_tokens_desc') }}</p>\n    @if (count($user->apiTokens) > 0)\n        <div class=\"item-list my-m\">\n            @foreach($user->apiTokens as $token)\n                <div class=\"item-list-row flex-container-row items-center wrap py-xs gap-x-m\">\n                    <div class=\"flex px-m py-xs min-width-m\">\n                        <a href=\"{{ $token->getUrl(\"?context={$context}\") }}\">{{ $token->name }}</a> <br>\n                        <span class=\"small text-muted italic\">{{ $token->token_id }}</span>\n                    </div>\n                    <div class=\"flex flex-container-row items-center min-width-m\">\n                        <div class=\"flex px-m py-xs text-muted\">\n                            <strong class=\"text-small\">{{ trans('settings.users_api_tokens_expires') }}</strong> <br>\n                            {{ $token->expires_at->format('Y-m-d') ?? '' }}\n                        </div>\n                        <div class=\"flex px-m py-xs text-right\">\n                            <a class=\"button outline small\" href=\"{{ $token->getUrl(\"?context={$context}\") }}\">{{ trans('common.edit') }}</a>\n                        </div>\n                    </div>\n                </div>\n            @endforeach\n        </div>\n    @else\n        <p class=\"text-muted italic py-m\">{{ trans('settings.users_api_tokens_none') }}</p>\n    @endif\n</section>"
  },
  {
    "path": "resources/views/users/create.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container small\">\n\n        @include('settings.parts.navbar', ['selected' => 'users'])\n\n        <main class=\"card content-wrap\">\n            <h1 class=\"list-heading\">{{ trans('settings.users_add_new') }}</h1>\n\n            <form action=\"{{ url(\"/settings/users/create\") }}\" method=\"post\">\n                {!! csrf_field() !!}\n\n                <div class=\"setting-list\">\n                    @include('users.parts.form')\n                    @include('users.parts.language-option-row', ['value' => old('language') ?? config('app.default_locale')])\n                </div>\n\n                <div class=\"form-group text-right\">\n                    <a href=\"{{  url(userCan(\\BookStack\\Permissions\\Permission::UsersManage) ? \"/settings/users\" : \"/\") }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                    <button class=\"button\" type=\"submit\">{{ trans('common.save') }}</button>\n                </div>\n\n            </form>\n\n        </main>\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/users/delete.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n    <div class=\"container small\">\n\n        @include('settings.parts.navbar', ['selected' => 'users'])\n\n        <form action=\"{{ url(\"/settings/users/{$user->id}\") }}\" method=\"POST\">\n            {{ csrf_field() }}\n            {{ method_field('delete') }}\n\n            <div class=\"card content-wrap auto-height\">\n                <h1 class=\"list-heading\">{{ trans('settings.users_delete') }}</h1>\n\n                <p>{{ trans('settings.users_delete_warning', ['userName' => $user->name]) }}</p>\n\n                <hr class=\"my-l\">\n\n                <div class=\"grid half gap-xl v-center\">\n                    <div>\n                        <label class=\"setting-list-label\">{{ trans('settings.users_migrate_ownership') }}</label>\n                        <p class=\"small\">{{ trans('settings.users_migrate_ownership_desc') }}</p>\n                    </div>\n                    <div>\n                        @include('form.user-select', ['name' => 'new_owner_id', 'user' => null])\n                    </div>\n                </div>\n\n                <hr class=\"my-l\">\n\n                <div class=\"grid half\">\n                    <p class=\"text-neg\"><strong>{{ trans('settings.users_delete_confirm') }}</strong></p>\n                    <div class=\"text-right\">\n                        <a href=\"{{ url(\"/settings/users/{$user->id}\") }}\" class=\"button outline\">{{ trans('common.cancel') }}</a>\n                        <button type=\"submit\" class=\"button\">{{ trans('common.confirm') }}</button>\n                    </div>\n                </div>\n\n            </div>\n        </form>\n    </div>\n@stop\n"
  },
  {
    "path": "resources/views/users/edit.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n    <div class=\"container small\">\n\n        @include('settings.parts.navbar', ['selected' => 'users'])\n\n        <section class=\"card content-wrap\">\n            <h1 class=\"list-heading\">{{ $user->id === user()->id ? trans('settings.users_edit_profile') : trans('settings.users_edit') }}</h1>\n            <form action=\"{{ url(\"/settings/users/{$user->id}\") }}\" method=\"post\" enctype=\"multipart/form-data\">\n                {!! csrf_field() !!}\n                <input type=\"hidden\" name=\"_method\" value=\"PUT\">\n\n                <div class=\"setting-list\">\n                    @include('users.parts.form', ['model' => $user, 'authMethod' => $authMethod])\n\n                    <div class=\"grid half gap-xl\">\n                        <div>\n                            <label for=\"user-avatar\"\n                                   class=\"setting-list-label\">{{ trans('settings.users_avatar') }}</label>\n                            <p class=\"small\">{{ trans('settings.users_avatar_desc') }}</p>\n                        </div>\n                        <div>\n                            @include('form.image-picker', [\n                                'resizeHeight' => '512',\n                                'resizeWidth' => '512',\n                                'showRemove' => false,\n                                'defaultImage' => url('/user_avatar.png'),\n                                'currentImage' => $user->getAvatar(80),\n                                'currentId' => $user->image_id,\n                                'name' => 'profile_image',\n                                'imageClass' => 'avatar large'\n                            ])\n                        </div>\n                    </div>\n\n                    @if(!$user->isGuest())\n                        @include('users.parts.language-option-row', ['value' => old('language') ?? $user->getLocale()->appLocale()])\n                    @endif\n                </div>\n\n                <div class=\"text-right\">\n                    <a href=\"{{  url(\"/settings/users\") }}\"\n                       class=\"button outline\">{{ trans('common.cancel') }}</a>\n                    @if($authMethod !== 'system')\n                        <a href=\"{{ url(\"/settings/users/{$user->id}/delete\") }}\"\n                           class=\"button outline\">{{ trans('settings.users_delete') }}</a>\n                    @endif\n                    <button class=\"button\" type=\"submit\">{{ trans('common.save') }}</button>\n                </div>\n            </form>\n        </section>\n\n        <section class=\"card content-wrap auto-height\">\n            <h2 class=\"list-heading\">{{ trans('settings.users_mfa') }}</h2>\n            <p class=\"text-small\">{{ trans('settings.users_mfa_desc') }}</p>\n            <div class=\"grid half gap-xl v-center pb-s\">\n                <div>\n                    @if ($mfaMethods->count() > 0)\n                        <span class=\"text-pos\">@icon('check-circle')</span>\n                    @else\n                        <span class=\"text-neg\">@icon('cancel')</span>\n                    @endif\n                    {{ trans_choice('settings.users_mfa_x_methods', $mfaMethods->count()) }}\n                </div>\n                <div class=\"text-m-right\">\n                    @if($user->id === user()->id)\n                        <a href=\"{{ url('/mfa/setup')  }}\"\n                           class=\"button outline\">{{ trans('settings.users_mfa_configure') }}</a>\n                    @endif\n                </div>\n            </div>\n\n        </section>\n\n        @if(count($activeSocialDrivers) > 0)\n            <section class=\"card content-wrap auto-height\">\n                <div class=\"flex-container-row items-center justify-space-between wrap\">\n                    <h2 class=\"list-heading\">{{ trans('settings.users_social_accounts') }}</h2>\n                    <div>\n                        @if(user()->id === $user->id)\n                            <a class=\"button outline\" href=\"{{ url('/my-account/auth#social-accounts') }}\">{{ trans('common.manage') }}</a>\n                        @endif\n                    </div>\n                </div>\n                <p class=\"text-muted text-small\">{{ trans('settings.users_social_accounts_desc') }}</p>\n                <div class=\"container\">\n                    <div class=\"grid third\">\n                        @foreach($activeSocialDrivers as $driver => $driverName)\n                            <div class=\"text-center mb-m\">\n                                <div role=\"presentation\">@icon('auth/'. $driver, ['style' => 'width: 56px;height: 56px;'])</div>\n                                <p class=\"my-none bold\">{{ $driverName }}</p>\n                                @if($user->hasSocialAccount($driver))\n                                    <p class=\"text-pos bold text-small my-none\">{{ trans('settings.users_social_status_connected') }}</p>\n                                @else\n                                    <p class=\"text-neg bold text-small my-none\">{{ trans('settings.users_social_status_disconnected') }}</p>\n                                @endif\n                            </div>\n                        @endforeach\n                    </div>\n                </div>\n            </section>\n        @endif\n\n        @include('users.api-tokens.parts.list', ['user' => $user, 'context' => 'settings'])\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/users/index.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n    <div class=\"container small\">\n\n        @include('settings.parts.navbar', ['selected' => 'users'])\n\n        <main class=\"card content-wrap\">\n\n            <div class=\"flex-container-row wrap justify-space-between items-center\">\n                <h1 class=\"list-heading\">{{ trans('settings.users') }}</h1>\n                <div>\n                    <a href=\"{{ url(\"/settings/users/create\") }}\" class=\"outline button my-none\">{{ trans('settings.users_add_new') }}</a>\n                </div>\n            </div>\n\n            <p class=\"text-muted\">{{ trans('settings.users_index_desc') }}</p>\n\n            <div class=\"flex-container-row items-center justify-space-between gap-m mt-m mb-l wrap\">\n                <div>\n                    <div class=\"block inline mr-xs\">\n                        <form method=\"get\" action=\"{{ url(\"/settings/users\") }}\">\n                            <input type=\"text\"\n                                   name=\"search\"\n                                   title=\"{{ trans('settings.users_search') }}\"\n                                   placeholder=\"{{ trans('settings.users_search') }}\"\n                                   value=\"{{ $listOptions->getSearch() }}\">\n                        </form>\n                    </div>\n                </div>\n                <div class=\"justify-flex-end\">\n                    @include('common.sort', $listOptions->getSortControlData())\n                </div>\n            </div>\n\n            <div class=\"item-list\">\n                @foreach($users as $user)\n                    @include('users.parts.users-list-item', ['user' => $user])\n                @endforeach\n            </div>\n\n            <div>\n                {{ $users->links() }}\n            </div>\n        </main>\n\n    </div>\n\n@stop\n"
  },
  {
    "path": "resources/views/users/parts/form.blade.php",
    "content": "\n@if($authMethod === 'system' && $user->system_name == 'public')\n    <p class=\"mb-none text-warn\">{{ trans('settings.users_system_public') }}</p>\n@endif\n\n<div class=\"pt-m\">\n    <label class=\"setting-list-label\">{{ trans('settings.users_details') }}</label>\n    @if($authMethod === 'standard')\n        <p class=\"small\">{{ trans('settings.users_details_desc') }}</p>\n    @endif\n    @if($authMethod === 'ldap' || $authMethod === 'system')\n        <p class=\"small\">{{ trans('settings.users_details_desc_no_email') }}</p>\n    @endif\n    <div class=\"grid half mt-m gap-xl mb-l\">\n        <div>\n            <label for=\"name\">{{ trans('auth.name') }}</label>\n            @include('form.text', ['name' => 'name'])\n        </div>\n        <div>\n            @if($authMethod !== 'ldap' || userCan(\\BookStack\\Permissions\\Permission::UsersManage))\n                <label for=\"email\">{{ trans('auth.email') }}</label>\n                @include('form.text', ['name' => 'email', 'disabled' => !userCan(\\BookStack\\Permissions\\Permission::UsersManage)])\n            @endif\n        </div>\n    </div>\n    <div>\n        <div class=\"form-group collapsible mb-none\" component=\"collapsible\" id=\"external-auth-field\">\n            <button refs=\"collapsible@trigger\" type=\"button\" class=\"collapse-title text-link\" aria-expanded=\"false\">\n                <label for=\"external-auth\">{{ trans('settings.users_external_auth_id') }}</label>\n            </button>\n            <div refs=\"collapsible@content\" class=\"collapse-content stretch-inputs\">\n                <p class=\"small\">{{ trans('settings.users_external_auth_id_desc') }}</p>\n                @include('form.text', ['name' => 'external_auth_id'])\n            </div>\n        </div>\n    </div>\n</div>\n\n<div>\n    <label for=\"role\" class=\"setting-list-label\">{{ trans('settings.users_role') }}</label>\n    <p class=\"small\">{{ trans('settings.users_role_desc') }}</p>\n    <div class=\"mt-m\">\n        @include('form.role-checkboxes', ['name' => 'roles', 'roles' => $roles])\n    </div>\n</div>\n\n@if($authMethod === 'standard')\n    <div component=\"new-user-password\">\n        <label class=\"setting-list-label\">{{ trans('settings.users_password') }}</label>\n\n        @if(!isset($model))\n            <p class=\"small\">\n                {{ trans('settings.users_send_invite_text') }}\n            </p>\n\n            @include('form.toggle-switch', [\n                'name' => 'send_invite',\n                'value' => old('send_invite', 'true') === 'true',\n                'label' => trans('settings.users_send_invite_option')\n            ])\n        @endif\n\n        <div refs=\"new-user-password@input-container\" @if(!isset($model)) style=\"display: none;\" @endif>\n            <p class=\"small mb-none\">{{ trans('settings.users_password_desc') }}</p>\n            @if(isset($model))\n                <p class=\"small\">\n                    {{ trans('settings.users_password_warning') }}\n                </p>\n            @endif\n            <div class=\"grid half mt-m gap-xl\">\n                <div>\n                    <label for=\"password\">{{ trans('auth.password') }}</label>\n                    @include('form.password', ['name' => 'password', 'autocomplete' => 'new-password'])\n                </div>\n                <div>\n                    <label for=\"password-confirm\">{{ trans('auth.password_confirm') }}</label>\n                    @include('form.password', ['name' => 'password-confirm'])\n                </div>\n            </div>\n        </div>\n\n    </div>\n@endif\n"
  },
  {
    "path": "resources/views/users/parts/language-option-row.blade.php",
    "content": "{{--\n$value - Currently selected lanuage value\n--}}\n<div class=\"grid half gap-xl v-center\">\n    <div>\n        <label for=\"user-language\" class=\"setting-list-label\">{{ trans('settings.users_preferred_language') }}</label>\n        <p class=\"small\">\n            {{ trans('settings.users_preferred_language_desc') }}\n        </p>\n    </div>\n    <div>\n        <select name=\"language\" id=\"user-language\">\n            @foreach(trans('settings.language_select') as $lang => $label)\n                <option @if($value === $lang) selected @endif value=\"{{ $lang }}\">{{ $label }}</option>\n            @endforeach\n        </select>\n    </div>\n</div>"
  },
  {
    "path": "resources/views/users/parts/users-list-item.blade.php",
    "content": "<div class=\"flex-container-row item-list-row items-center wrap py-xs\">\n    <div class=\"px-m py-xs flex-container-row items-center flex-2 gap-l min-width-m\">\n        <img class=\"avatar med\" width=\"40\" height=\"40\" src=\"{{ $user->getAvatar(40)}}\" alt=\"{{ $user->name }}\">\n        <a href=\"{{ url(\"/settings/users/{$user->id}\") }}\">\n            {{ $user->name }}\n            <br>\n            <span class=\"text-muted\">{{ $user->email }}</span>\n            @if($user->mfa_values_count > 0)\n                <span title=\"MFA Configured\" class=\"text-pos\">@icon('lock')</span>\n            @endif\n        </a>\n    </div>\n    <div class=\"flex-container-row items-center flex-3 min-width-m\">\n        <div class=\"px-m py-xs flex\">\n            @foreach($user->roles as $index => $role)\n                <small><a href=\"{{ url(\"/settings/roles/{$role->id}\") }}\">{{$role->display_name}}</a>@if($index !== count($user->roles) -1),@endif</small>\n            @endforeach\n        </div>\n        <div class=\"px-m py-xs flex text-right text-muted\">\n            @if($user->last_activity_at)\n                <small>{{ trans('settings.users_latest_activity') }}</small>\n                <br>\n                <small title=\"{{ $dates->absolute($user->last_activity_at) }}\">{{ $dates->relative($user->last_activity_at) }}</small>\n            @endif\n        </div>\n    </div>\n</div>"
  },
  {
    "path": "resources/views/users/profile.blade.php",
    "content": "@extends('layouts.simple')\n\n@section('body')\n\n    <div class=\"container medium pt-xl\">\n\n        <div class=\"grid right-focus reverse-collapse\">\n\n            <div>\n                <section id=\"recent-user-activity\" class=\"mb-xl\">\n                    <h5>{{ trans('entities.recent_activity') }}</h5>\n                    @include('common.activity-list', ['activity' => $activity])\n                </section>\n            </div>\n\n            <div>\n                <section class=\"card content-wrap auto-height\">\n                    <div class=\"grid half v-center\">\n                        <div>\n                            <div class=\"mr-m float left\">\n                                <img class=\"avatar square huge\" src=\"{{ $user->getAvatar(120) }}\" alt=\"{{ $user->name }}\">\n                            </div>\n                            <div>\n                                <h4 class=\"mt-md\">{{ $user->name }}</h4>\n                                <p class=\"text-muted\">\n                                    {{ trans('entities.profile_user_for_x', ['time' => $dates->relative($user->created_at, false)]) }}\n                                </p>\n                            </div>\n                        </div>\n                        <div id=\"content-counts\">\n                            <div class=\"text-muted\">{{ trans('entities.profile_created_content') }}</div>\n                            <div class=\"grid half v-center no-row-gap\">\n                                <div class=\"icon-list\">\n                                    <a href=\"#recent-pages\" class=\"text-page icon-list-item\">\n                                        <span>@icon('page')</span>\n                                        <span>{{ trans_choice('entities.x_pages', $assetCounts['pages']) }}</span>\n                                    </a>\n                                    <a href=\"#recent-chapters\" class=\"text-chapter icon-list-item\">\n                                        <span>@icon('chapter')</span>\n                                        <span>{{ trans_choice('entities.x_chapters', $assetCounts['chapters']) }}</span>\n                                    </a>\n                                </div>\n                                <div class=\"icon-list\">\n                                    <a href=\"#recent-books\" class=\"text-book icon-list-item\">\n                                        <span>@icon('book')</span>\n                                        <span>{{ trans_choice('entities.x_books', $assetCounts['books']) }}</span>\n                                    </a>\n                                    <a href=\"#recent-shelves\" class=\"text-bookshelf icon-list-item\">\n                                        <span>@icon('bookshelf')</span>\n                                        <span>{{ trans_choice('entities.x_shelves', $assetCounts['shelves']) }}</span>\n                                    </a>\n                                </div>\n                            </div>\n\n                        </div>\n                    </div>\n                </section>\n\n                <section class=\"card content-wrap auto-height book-contents\">\n                    <h2 id=\"recent-pages\" class=\"list-heading\">\n                        {{ trans('entities.recently_created_pages') }}\n                        @if (count($recentlyCreated['pages']) > 0)\n                            <a href=\"{{ url('/search?term=' . urlencode('{created_by:'.$user->slug.'} {type:page}') ) }}\" class=\"text-small ml-s\">{{ trans('common.view_all') }}</a>\n                        @endif\n                    </h2>\n                    @if (count($recentlyCreated['pages']) > 0)\n                        @include('entities.list', ['entities' => $recentlyCreated['pages'], 'showPath' => true])\n                    @else\n                        <p class=\"text-muted\">{{ trans('entities.profile_not_created_pages', ['userName' => $user->name]) }}</p>\n                    @endif\n                </section>\n\n                <section class=\"card content-wrap auto-height book-contents\">\n                    <h2 id=\"recent-chapters\" class=\"list-heading\">\n                        {{ trans('entities.recently_created_chapters') }}\n                        @if (count($recentlyCreated['chapters']) > 0)\n                            <a href=\"{{ url('/search?term=' . urlencode('{created_by:'.$user->slug.'} {type:chapter}') ) }}\" class=\"text-small ml-s\">{{ trans('common.view_all') }}</a>\n                        @endif\n                    </h2>\n                    @if (count($recentlyCreated['chapters']) > 0)\n                        @include('entities.list', ['entities' => $recentlyCreated['chapters'], 'showPath' => true])\n                    @else\n                        <p class=\"text-muted\">{{ trans('entities.profile_not_created_chapters', ['userName' => $user->name]) }}</p>\n                    @endif\n                </section>\n\n                <section class=\"card content-wrap auto-height book-contents\">\n                    <h2 id=\"recent-books\" class=\"list-heading\">\n                        {{ trans('entities.recently_created_books') }}\n                        @if (count($recentlyCreated['books']) > 0)\n                            <a href=\"{{ url('/search?term=' . urlencode('{created_by:'.$user->slug.'} {type:book}') ) }}\" class=\"text-small ml-s\">{{ trans('common.view_all') }}</a>\n                        @endif\n                    </h2>\n                    @if (count($recentlyCreated['books']) > 0)\n                        @include('entities.list', ['entities' => $recentlyCreated['books'], 'showPath' => true])\n                    @else\n                        <p class=\"text-muted\">{{ trans('entities.profile_not_created_books', ['userName' => $user->name]) }}</p>\n                    @endif\n                </section>\n\n                <section class=\"card content-wrap auto-height book-contents\">\n                    <h2 id=\"recent-shelves\" class=\"list-heading\">\n                        {{ trans('entities.recently_created_shelves') }}\n                        @if (count($recentlyCreated['shelves']) > 0)\n                            <a href=\"{{ url('/search?term=' . urlencode('{created_by:'.$user->slug.'} {type:bookshelf}') ) }}\" class=\"text-small ml-s\">{{ trans('common.view_all') }}</a>\n                        @endif\n                    </h2>\n                    @if (count($recentlyCreated['shelves']) > 0)\n                        @include('entities.list', ['entities' => $recentlyCreated['shelves'], 'showPath' => true])\n                    @else\n                        <p class=\"text-muted\">{{ trans('entities.profile_not_created_shelves', ['userName' => $user->name]) }}</p>\n                    @endif\n                </section>\n            </div>\n\n        </div>\n\n\n    </div>\n@stop"
  },
  {
    "path": "resources/views/vendor/notifications/email-plain.blade.php",
    "content": "<?php\n\nif (! empty($greeting)) {\n    echo $greeting, \"\\n\\n\";\n}\n\nif (! empty($introLines)) {\n    echo implode(\"\\n\", $introLines), \"\\n\\n\";\n}\n\nif (isset($actionText)) {\n    echo \"{$actionText}: {$actionUrl}\", \"\\n\\n\";\n}\n\nif (! empty($outroLines)) {\n    echo implode(\"\\n\", $outroLines), \"\\n\\n\";\n}\n\necho \"\\n\";\necho setting('app-name'), \"\\n\";\n"
  },
  {
    "path": "resources/views/vendor/notifications/email.blade.php",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html lang=\"{{ $locale->htmlLang() }}\">\n<head>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n\n    <style type=\"text/css\" rel=\"stylesheet\" media=\"all\">\n        /* Media Queries */\n        @media only screen and (max-width: 500px) {\n            .button {\n                width: 100% !important;\n            }\n        }\n\n        @media only screen and (max-width: 600px) {\n            .button {\n                width: 100% !important;\n            }\n            .mobile {\n                max-width: 100%;\n                display: block;\n                width: 100%;\n            }\n        }\n    </style>\n</head>\n\n<?php\n\n$style = [\n    /* Layout ------------------------------ */\n\n    'body' => 'margin: 0; padding: 0; width: 100%; background-color: #F2F4F6;color:#444444;',\n    'email-wrapper' => 'width: 100%; margin: 0; padding: 0; background-color: #F2F4F6;',\n\n    /* Masthead ----------------------- */\n\n    'email-masthead' => 'padding: 25px 0; text-align: center;',\n    'email-masthead_name' => 'font-size: 24px; font-weight: 400; color: #2F3133; text-decoration: none; text-shadow: 0 1px 0 white;',\n\n    'email-body' => 'width: 100%; margin: 0; padding: 0; border-top: 4px solid '.setting('app-color').'; border-bottom: 1px solid #EDEFF2; background-color: #FFF;',\n    'email-body_inner' => 'width: auto; max-width: 100%; margin: 0 auto; padding: 0;',\n    'email-body_cell' => 'padding: 35px;',\n\n    'email-footer' => 'width: auto; max-width: 570px; margin: 0 auto; padding: 0; text-align: center;',\n    'email-footer_cell' => 'color: #AEAEAE; padding: 35px; text-align: center;',\n\n    /* Body ------------------------------ */\n\n    'body_action' => 'width: 100%; margin: 30px auto; padding: 0; text-align: center;',\n    'body_sub' => 'margin-top: 25px; padding-top: 25px; border-top: 1px solid #EDEFF2;',\n\n    /* Type ------------------------------ */\n\n    'anchor' => 'color: '.setting('app-color').';overflow-wrap: break-word;word-wrap: break-word;word-break: break-all;word-break:break-word;',\n    'header-1' => 'margin-top: 0; color: #2F3133; font-size: 19px; font-weight: bold; text-align: left;',\n    'paragraph' => 'margin-top: 0; color: #444444; font-size: 16px; line-height: 1.5em;',\n    'paragraph-sub' => 'margin-top: 0; color: #444444; font-size: 12px; line-height: 1.5em;',\n    'paragraph-center' => 'text-align: center;',\n\n    /* Buttons ------------------------------ */\n\n    'button' => 'display: block; display: inline-block; width: 200px; min-height: 20px; padding: 10px;\n                 background-color: #3869D4; border-radius: 3px; color: #ffffff; font-size: 15px; line-height: 25px;\n                 text-align: center; text-decoration: none; -webkit-text-size-adjust: none;',\n\n    'button--green' => 'background-color: #22BC66;',\n    'button--red' => 'background-color: #dc4d2f;',\n    'button--blue' => 'background-color: '.setting('app-color').';',\n];\n?>\n\n<?php $fontFamily = 'font-family: Arial, \\'Helvetica Neue\\', Helvetica, sans-serif;'; ?>\n\n<body style=\"{{ $style['body'] }}\">\n    <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n        <tr>\n            <td align=\"center\" class=\"mobile\">\n                <table width=\"600\" style=\"max-width: 100%; padding: 12px;text-align: left;\" cellpadding=\"0\" cellspacing=\"0\" class=\"mobile\">\n                    <tr>\n                        <td style=\"{{ $style['email-wrapper'] }}\" align=\"center\">\n                            <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n                                <!-- Logo -->\n                                <tr>\n                                    <td style=\"{{ $style['email-masthead'] }}\">\n                                        <a style=\"{{ $fontFamily }} {{ $style['email-masthead_name'] }}\" href=\"{{ url('/') }}\" target=\"_blank\">\n                                            {{ setting('app-name') }}\n                                        </a>\n                                    </td>\n                                </tr>\n\n                                <!-- Email Body -->\n                                <tr>\n                                    <td style=\"{{ $style['email-body'] }}\" width=\"100%\">\n                                        <table style=\"{{ $style['email-body_inner'] }}\" align=\"center\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n                                            <tr>\n                                                <td style=\"{{ $fontFamily }} {{ $style['email-body_cell'] }}\">\n\n                                                    <!-- Greeting -->\n                                                    @if (!empty($greeting) || $level == 'error')\n                                                    <h1 style=\"{{ $style['header-1'] }}\">\n                                                        @if (! empty($greeting))\n                                                            {{ $greeting }}\n                                                        @else\n                                                            @if ($level == 'error')\n                                                                Whoops!\n                                                            @endif\n                                                        @endif\n                                                    </h1>\n                                                    @endif\n\n                                                    <!-- Intro -->\n                                                    @foreach ($introLines as $line)\n                                                        <p style=\"{{ $style['paragraph'] }}\">\n                                                            {{ $line }}\n                                                        </p>\n                                                    @endforeach\n\n                                                    <!-- Action Button -->\n                                                    @if (isset($actionText))\n                                                        <table style=\"{{ $style['body_action'] }}\" align=\"center\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n                                                            <tr>\n                                                                <td align=\"center\">\n                                                                    <?php\n                                                                    switch ($level) {\n                                                                        case 'success':\n                                                                            $actionColor = 'button--green';\n                                                                            break;\n                                                                        case 'error':\n                                                                            $actionColor = 'button--red';\n                                                                            break;\n                                                                        default:\n                                                                            $actionColor = 'button--blue';\n                                                                    }\n                                                                    ?>\n\n                                                                    <a href=\"{{ $actionUrl }}\"\n                                                                       style=\"{{ $fontFamily }} {{ $style['button'] }} {{ $style[$actionColor] }}\"\n                                                                       class=\"button\"\n                                                                       target=\"_blank\">\n                                                                        {{ $actionText }}\n                                                                    </a>\n                                                                </td>\n                                                            </tr>\n                                                        </table>\n                                                    @endif\n\n                                                    <!-- Outro -->\n                                                    @foreach ($outroLines as $line)\n                                                        <p style=\"{{ $style['paragraph-sub'] }}\">\n                                                            {{ $line }}\n                                                        </p>\n                                                    @endforeach\n\n\n                                                    <!-- Sub Copy -->\n                                                    @if (isset($actionText))\n                                                        <table style=\"{{ $style['body_sub'] }}\">\n                                                            <tr>\n                                                                <td style=\"{{ $fontFamily }}\">\n                                                                    <p style=\"{{ $style['paragraph-sub'] }}\">\n                                                                        {{ $locale->trans('common.email_action_help', ['actionText' => $actionText]) }}\n                                                                    </p>\n\n                                                                    <p style=\"{{ $style['paragraph-sub'] }}\">\n                                                                        <a style=\"{{ $style['anchor'] }}\" href=\"{{ $actionUrl }}\" target=\"_blank\">\n                                                                            {{ $actionUrl }}\n                                                                        </a>\n                                                                    </p>\n                                                                </td>\n                                                            </tr>\n                                                        </table>\n                                                    @endif\n\n                                                </td>\n                                            </tr>\n                                        </table>\n                                    </td>\n                                </tr>\n\n                                <!-- Footer -->\n                                <tr>\n                                    <td>\n                                        <table style=\"{{ $style['email-footer'] }}\" align=\"center\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n                                            <tr>\n                                                <td style=\"{{ $fontFamily }} {{ $style['email-footer_cell'] }}\">\n                                                    <p style=\"{{ $style['paragraph-sub'] }}\">\n                                                        &copy; {{ date('Y') }}\n                                                        <a style=\"{{ $style['anchor'] }}\" href=\"{{ url('/') }}\" target=\"_blank\">{{ setting('app-name') }}</a>.\n                                                        {{ $locale->trans('common.email_rights') }}\n                                                    </p>\n                                                </td>\n                                            </tr>\n                                        </table>\n                                    </td>\n                                </tr>\n                            </table>\n                        </td>\n                    </tr>\n                </table>\n            </td>\n        </tr>\n    </table>\n</body>\n</html>\n"
  },
  {
    "path": "resources/views/vendor/pagination/default.blade.php",
    "content": "@if ($paginator->hasPages())\n    <ul class=\"pagination\">\n        {{-- Previous Page Link --}}\n        @if ($paginator->onFirstPage())\n            <li class=\"disabled\"><span>&laquo;</span></li>\n        @else\n            <li><a href=\"{{ $paginator->previousPageUrl() }}\" rel=\"prev\">&laquo;</a></li>\n        @endif\n\n        {{-- Pagination Elements --}}\n        @foreach ($elements as $element)\n            {{-- \"Three Dots\" Separator --}}\n            @if (is_string($element))\n                <li class=\"disabled\"><span>{{ $element }}</span></li>\n            @endif\n\n            {{-- Array Of Links --}}\n            @if (is_array($element))\n                @foreach ($element as $page => $url)\n                    @if ($page == $paginator->currentPage())\n                        <li class=\"active primary-background\"><span>{{ $page }}</span></li>\n                    @else\n                        <li><a href=\"{{ $url }}\">{{ $page }}</a></li>\n                    @endif\n                @endforeach\n            @endif\n        @endforeach\n\n        {{-- Next Page Link --}}\n        @if ($paginator->hasMorePages())\n            <li><a href=\"{{ $paginator->nextPageUrl() }}\" rel=\"next\">&raquo;</a></li>\n        @else\n            <li class=\"disabled\"><span>&raquo;</span></li>\n        @endif\n    </ul>\n@endif\n"
  },
  {
    "path": "routes/api.php",
    "content": "<?php\n\n/**\n * Routes for the BookStack API.\n * Routes have a URI prefix of /api/.\n * Controllers all end with \"ApiController\"\n */\n\nuse BookStack\\Activity\\Controllers as ActivityControllers;\nuse BookStack\\Api\\ApiDocsController;\nuse BookStack\\App\\SystemApiController;\nuse BookStack\\Entities\\Controllers as EntityControllers;\nuse BookStack\\Exports\\Controllers as ExportControllers;\nuse BookStack\\Permissions\\ContentPermissionApiController;\nuse BookStack\\Search\\SearchApiController;\nuse BookStack\\Uploads\\Controllers\\AttachmentApiController;\nuse BookStack\\Uploads\\Controllers\\ImageGalleryApiController;\nuse BookStack\\Users\\Controllers\\RoleApiController;\nuse BookStack\\Users\\Controllers\\UserApiController;\nuse Illuminate\\Support\\Facades\\Route;\n\n// Main Entity Routes\n\nRoute::get('pages', [EntityControllers\\PageApiController::class, 'list']);\nRoute::post('pages', [EntityControllers\\PageApiController::class, 'create']);\nRoute::get('pages/{id}', [EntityControllers\\PageApiController::class, 'read']);\nRoute::put('pages/{id}', [EntityControllers\\PageApiController::class, 'update']);\nRoute::delete('pages/{id}', [EntityControllers\\PageApiController::class, 'delete']);\nRoute::get('pages/{id}/export/html', [ExportControllers\\PageExportApiController::class, 'exportHtml']);\nRoute::get('pages/{id}/export/pdf', [ExportControllers\\PageExportApiController::class, 'exportPdf']);\nRoute::get('pages/{id}/export/plaintext', [ExportControllers\\PageExportApiController::class, 'exportPlainText']);\nRoute::get('pages/{id}/export/markdown', [ExportControllers\\PageExportApiController::class, 'exportMarkdown']);\nRoute::get('pages/{id}/export/zip', [ExportControllers\\PageExportApiController::class, 'exportZip']);\n\nRoute::get('chapters', [EntityControllers\\ChapterApiController::class, 'list']);\nRoute::post('chapters', [EntityControllers\\ChapterApiController::class, 'create']);\nRoute::get('chapters/{id}', [EntityControllers\\ChapterApiController::class, 'read']);\nRoute::put('chapters/{id}', [EntityControllers\\ChapterApiController::class, 'update']);\nRoute::delete('chapters/{id}', [EntityControllers\\ChapterApiController::class, 'delete']);\nRoute::get('chapters/{id}/export/html', [ExportControllers\\ChapterExportApiController::class, 'exportHtml']);\nRoute::get('chapters/{id}/export/pdf', [ExportControllers\\ChapterExportApiController::class, 'exportPdf']);\nRoute::get('chapters/{id}/export/plaintext', [ExportControllers\\ChapterExportApiController::class, 'exportPlainText']);\nRoute::get('chapters/{id}/export/markdown', [ExportControllers\\ChapterExportApiController::class, 'exportMarkdown']);\nRoute::get('chapters/{id}/export/zip', [ExportControllers\\ChapterExportApiController::class, 'exportZip']);\n\nRoute::get('books', [EntityControllers\\BookApiController::class, 'list']);\nRoute::post('books', [EntityControllers\\BookApiController::class, 'create']);\nRoute::get('books/{id}', [EntityControllers\\BookApiController::class, 'read']);\nRoute::put('books/{id}', [EntityControllers\\BookApiController::class, 'update']);\nRoute::delete('books/{id}', [EntityControllers\\BookApiController::class, 'delete']);\nRoute::get('books/{id}/export/html', [ExportControllers\\BookExportApiController::class, 'exportHtml']);\nRoute::get('books/{id}/export/pdf', [ExportControllers\\BookExportApiController::class, 'exportPdf']);\nRoute::get('books/{id}/export/plaintext', [ExportControllers\\BookExportApiController::class, 'exportPlainText']);\nRoute::get('books/{id}/export/markdown', [ExportControllers\\BookExportApiController::class, 'exportMarkdown']);\nRoute::get('books/{id}/export/zip', [ExportControllers\\BookExportApiController::class, 'exportZip']);\n\nRoute::get('shelves', [EntityControllers\\BookshelfApiController::class, 'list']);\nRoute::post('shelves', [EntityControllers\\BookshelfApiController::class, 'create']);\nRoute::get('shelves/{id}', [EntityControllers\\BookshelfApiController::class, 'read']);\nRoute::put('shelves/{id}', [EntityControllers\\BookshelfApiController::class, 'update']);\nRoute::delete('shelves/{id}', [EntityControllers\\BookshelfApiController::class, 'delete']);\n\n// Additional Model Routes, in alphabetical order\n\nRoute::get('attachments', [AttachmentApiController::class, 'list']);\nRoute::post('attachments', [AttachmentApiController::class, 'create']);\nRoute::get('attachments/{id}', [AttachmentApiController::class, 'read']);\nRoute::put('attachments/{id}', [AttachmentApiController::class, 'update']);\nRoute::delete('attachments/{id}', [AttachmentApiController::class, 'delete']);\n\nRoute::get('audit-log', [ActivityControllers\\AuditLogApiController::class, 'list']);\n\nRoute::get('comments', [ActivityControllers\\CommentApiController::class, 'list']);\nRoute::post('comments', [ActivityControllers\\CommentApiController::class, 'create']);\nRoute::get('comments/{id}', [ActivityControllers\\CommentApiController::class, 'read']);\nRoute::put('comments/{id}', [ActivityControllers\\CommentApiController::class, 'update']);\nRoute::delete('comments/{id}', [ActivityControllers\\CommentApiController::class, 'delete']);\n\nRoute::get('content-permissions/{contentType}/{contentId}', [ContentPermissionApiController::class, 'read']);\nRoute::put('content-permissions/{contentType}/{contentId}', [ContentPermissionApiController::class, 'update']);\n\nRoute::get('docs.json', [ApiDocsController::class, 'json']);\n\nRoute::get('image-gallery', [ImageGalleryApiController::class, 'list']);\nRoute::post('image-gallery', [ImageGalleryApiController::class, 'create']);\nRoute::get('image-gallery/url/data', [ImageGalleryApiController::class, 'readDataForUrl']);\nRoute::get('image-gallery/{id}', [ImageGalleryApiController::class, 'read']);\nRoute::get('image-gallery/{id}/data', [ImageGalleryApiController::class, 'readData']);\nRoute::put('image-gallery/{id}', [ImageGalleryApiController::class, 'update']);\nRoute::delete('image-gallery/{id}', [ImageGalleryApiController::class, 'delete']);\n\nRoute::get('imports', [ExportControllers\\ImportApiController::class, 'list']);\nRoute::post('imports', [ExportControllers\\ImportApiController::class, 'create']);\nRoute::get('imports/{id}', [ExportControllers\\ImportApiController::class, 'read']);\nRoute::post('imports/{id}', [ExportControllers\\ImportApiController::class, 'run']);\nRoute::delete('imports/{id}', [ExportControllers\\ImportApiController::class, 'delete']);\n\nRoute::get('recycle-bin', [EntityControllers\\RecycleBinApiController::class, 'list']);\nRoute::put('recycle-bin/{deletionId}', [EntityControllers\\RecycleBinApiController::class, 'restore']);\nRoute::delete('recycle-bin/{deletionId}', [EntityControllers\\RecycleBinApiController::class, 'destroy']);\n\nRoute::get('roles', [RoleApiController::class, 'list']);\nRoute::post('roles', [RoleApiController::class, 'create']);\nRoute::get('roles/{id}', [RoleApiController::class, 'read']);\nRoute::put('roles/{id}', [RoleApiController::class, 'update']);\nRoute::delete('roles/{id}', [RoleApiController::class, 'delete']);\n\nRoute::get('search', [SearchApiController::class, 'all']);\n\nRoute::get('system', [SystemApiController::class, 'read']);\n\nRoute::get('users', [UserApiController::class, 'list']);\nRoute::post('users', [UserApiController::class, 'create']);\nRoute::get('users/{id}', [UserApiController::class, 'read']);\nRoute::put('users/{id}', [UserApiController::class, 'update']);\nRoute::delete('users/{id}', [UserApiController::class, 'delete']);\n"
  },
  {
    "path": "routes/web.php",
    "content": "<?php\n\nuse BookStack\\Access\\Controllers as AccessControllers;\nuse BookStack\\Activity\\Controllers as ActivityControllers;\nuse BookStack\\Api\\ApiDocsController;\nuse BookStack\\Api\\UserApiTokenController;\nuse BookStack\\App\\HomeController;\nuse BookStack\\App\\MetaController;\nuse BookStack\\Entities\\Controllers as EntityControllers;\nuse BookStack\\Exports\\Controllers as ExportControllers;\nuse BookStack\\Http\\Middleware\\VerifyCsrfToken;\nuse BookStack\\Permissions\\PermissionsController;\nuse BookStack\\References\\ReferenceController;\nuse BookStack\\Search\\SearchController;\nuse BookStack\\Settings as SettingControllers;\nuse BookStack\\Sorting as SortingControllers;\nuse BookStack\\Theming\\ThemeController;\nuse BookStack\\Uploads\\Controllers as UploadControllers;\nuse BookStack\\Users\\Controllers as UserControllers;\nuse Illuminate\\Session\\Middleware\\StartSession;\nuse Illuminate\\Support\\Facades\\Route;\nuse Illuminate\\View\\Middleware\\ShareErrorsFromSession;\n\n// Status & Meta routes\nRoute::get('/status', [SettingControllers\\StatusController::class, 'show']);\nRoute::get('/robots.txt', [MetaController::class, 'robots']);\nRoute::get('/favicon.ico', [MetaController::class, 'favicon']);\nRoute::get('/manifest.json', [MetaController::class, 'pwaManifest']);\nRoute::get('/licenses', [MetaController::class, 'licenses']);\nRoute::get('/opensearch.xml', [MetaController::class, 'opensearch']);\n\n// Authenticated routes...\nRoute::middleware('auth')->group(function () {\n\n    // Secure images routing\n    Route::get('/uploads/images/{path}', [UploadControllers\\ImageController::class, 'showImage'])\n        ->where('path', '.*$');\n\n    // API docs routes\n    Route::get('/api', [ApiDocsController::class, 'redirect']);\n    Route::get('/api/docs', [ApiDocsController::class, 'display']);\n\n    Route::get('/pages/recently-updated', [EntityControllers\\PageController::class, 'showRecentlyUpdated']);\n\n    // Shelves\n    Route::get('/create-shelf', [EntityControllers\\BookshelfController::class, 'create']);\n    Route::get('/shelves/', [EntityControllers\\BookshelfController::class, 'index']);\n    Route::post('/shelves/', [EntityControllers\\BookshelfController::class, 'store']);\n    Route::get('/shelves/{slug}/edit', [EntityControllers\\BookshelfController::class, 'edit']);\n    Route::get('/shelves/{slug}/delete', [EntityControllers\\BookshelfController::class, 'showDelete']);\n    Route::get('/shelves/{slug}', [EntityControllers\\BookshelfController::class, 'show']);\n    Route::put('/shelves/{slug}', [EntityControllers\\BookshelfController::class, 'update']);\n    Route::delete('/shelves/{slug}', [EntityControllers\\BookshelfController::class, 'destroy']);\n    Route::get('/shelves/{slug}/permissions', [PermissionsController::class, 'showForShelf']);\n    Route::put('/shelves/{slug}/permissions', [PermissionsController::class, 'updateForShelf']);\n    Route::post('/shelves/{slug}/copy-permissions', [PermissionsController::class, 'copyShelfPermissionsToBooks']);\n    Route::get('/shelves/{slug}/references', [ReferenceController::class, 'shelf']);\n\n    // Book Creation\n    Route::get('/shelves/{shelfSlug}/create-book', [EntityControllers\\BookController::class, 'create']);\n    Route::post('/shelves/{shelfSlug}/create-book', [EntityControllers\\BookController::class, 'store']);\n    Route::get('/create-book', [EntityControllers\\BookController::class, 'create']);\n\n    // Books\n    Route::get('/books/', [EntityControllers\\BookController::class, 'index']);\n    Route::post('/books/', [EntityControllers\\BookController::class, 'store']);\n    Route::get('/books/{slug}/edit', [EntityControllers\\BookController::class, 'edit']);\n    Route::put('/books/{slug}', [EntityControllers\\BookController::class, 'update']);\n    Route::delete('/books/{id}', [EntityControllers\\BookController::class, 'destroy']);\n    Route::get('/books/{slug}/sort-item', [SortingControllers\\BookSortController::class, 'showItem']);\n    Route::get('/books/{slug}', [EntityControllers\\BookController::class, 'show']);\n    Route::get('/books/{bookSlug}/permissions', [PermissionsController::class, 'showForBook']);\n    Route::put('/books/{bookSlug}/permissions', [PermissionsController::class, 'updateForBook']);\n    Route::get('/books/{slug}/delete', [EntityControllers\\BookController::class, 'showDelete']);\n    Route::get('/books/{bookSlug}/copy', [EntityControllers\\BookController::class, 'showCopy']);\n    Route::post('/books/{bookSlug}/copy', [EntityControllers\\BookController::class, 'copy']);\n    Route::post('/books/{bookSlug}/convert-to-shelf', [EntityControllers\\BookController::class, 'convertToShelf']);\n    Route::get('/books/{bookSlug}/sort', [SortingControllers\\BookSortController::class, 'show']);\n    Route::put('/books/{bookSlug}/sort', [SortingControllers\\BookSortController::class, 'update']);\n    Route::get('/books/{slug}/references', [ReferenceController::class, 'book']);\n    Route::get('/books/{bookSlug}/export/html', [ExportControllers\\BookExportController::class, 'html']);\n    Route::get('/books/{bookSlug}/export/pdf', [ExportControllers\\BookExportController::class, 'pdf']);\n    Route::get('/books/{bookSlug}/export/markdown', [ExportControllers\\BookExportController::class, 'markdown']);\n    Route::get('/books/{bookSlug}/export/zip', [ExportControllers\\BookExportController::class, 'zip']);\n    Route::get('/books/{bookSlug}/export/plaintext', [ExportControllers\\BookExportController::class, 'plainText']);\n\n    // Pages\n    Route::get('/books/{bookSlug}/create-page', [EntityControllers\\PageController::class, 'create']);\n    Route::post('/books/{bookSlug}/create-guest-page', [EntityControllers\\PageController::class, 'createAsGuest']);\n    Route::get('/books/{bookSlug}/draft/{pageId}', [EntityControllers\\PageController::class, 'editDraft']);\n    Route::post('/books/{bookSlug}/draft/{pageId}', [EntityControllers\\PageController::class, 'store']);\n    Route::get('/books/{bookSlug}/page/{pageSlug}', [EntityControllers\\PageController::class, 'show']);\n    Route::get('/books/{bookSlug}/page/{pageSlug}/export/pdf', [ExportControllers\\PageExportController::class, 'pdf']);\n    Route::get('/books/{bookSlug}/page/{pageSlug}/export/html', [ExportControllers\\PageExportController::class, 'html']);\n    Route::get('/books/{bookSlug}/page/{pageSlug}/export/markdown', [ExportControllers\\PageExportController::class, 'markdown']);\n    Route::get('/books/{bookSlug}/page/{pageSlug}/export/plaintext', [ExportControllers\\PageExportController::class, 'plainText']);\n    Route::get('/books/{bookSlug}/page/{pageSlug}/export/zip', [ExportControllers\\PageExportController::class, 'zip']);\n    Route::get('/books/{bookSlug}/page/{pageSlug}/edit', [EntityControllers\\PageController::class, 'edit']);\n    Route::get('/books/{bookSlug}/page/{pageSlug}/move', [EntityControllers\\PageController::class, 'showMove']);\n    Route::put('/books/{bookSlug}/page/{pageSlug}/move', [EntityControllers\\PageController::class, 'move']);\n    Route::get('/books/{bookSlug}/page/{pageSlug}/copy', [EntityControllers\\PageController::class, 'showCopy']);\n    Route::post('/books/{bookSlug}/page/{pageSlug}/copy', [EntityControllers\\PageController::class, 'copy']);\n    Route::get('/books/{bookSlug}/page/{pageSlug}/delete', [EntityControllers\\PageController::class, 'showDelete']);\n    Route::get('/books/{bookSlug}/draft/{pageId}/delete', [EntityControllers\\PageController::class, 'showDeleteDraft']);\n    Route::get('/books/{bookSlug}/page/{pageSlug}/permissions', [PermissionsController::class, 'showForPage']);\n    Route::put('/books/{bookSlug}/page/{pageSlug}/permissions', [PermissionsController::class, 'updateForPage']);\n    Route::get('/books/{bookSlug}/page/{pageSlug}/references', [ReferenceController::class, 'page']);\n    Route::put('/books/{bookSlug}/page/{pageSlug}', [EntityControllers\\PageController::class, 'update']);\n    Route::delete('/books/{bookSlug}/page/{pageSlug}', [EntityControllers\\PageController::class, 'destroy']);\n    Route::delete('/books/{bookSlug}/draft/{pageId}', [EntityControllers\\PageController::class, 'destroyDraft']);\n\n    // Revisions\n    Route::get('/books/{bookSlug}/page/{pageSlug}/revisions', [EntityControllers\\PageRevisionController::class, 'index']);\n    Route::get('/books/{bookSlug}/page/{pageSlug}/revisions/{revId}', [EntityControllers\\PageRevisionController::class, 'show']);\n    Route::get('/books/{bookSlug}/page/{pageSlug}/revisions/{revId}/changes', [EntityControllers\\PageRevisionController::class, 'changes']);\n    Route::put('/books/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', [EntityControllers\\PageRevisionController::class, 'restore']);\n    Route::delete('/books/{bookSlug}/page/{pageSlug}/revisions/{revId}/delete', [EntityControllers\\PageRevisionController::class, 'destroy']);\n    Route::delete('/page-revisions/user-drafts/{pageId}', [EntityControllers\\PageRevisionController::class, 'destroyUserDraft']);\n\n    // Chapters\n    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/create-page', [EntityControllers\\PageController::class, 'create']);\n    Route::post('/books/{bookSlug}/chapter/{chapterSlug}/create-guest-page', [EntityControllers\\PageController::class, 'createAsGuest']);\n    Route::get('/books/{bookSlug}/create-chapter', [EntityControllers\\ChapterController::class, 'create']);\n    Route::post('/books/{bookSlug}/create-chapter', [EntityControllers\\ChapterController::class, 'store']);\n    Route::get('/books/{bookSlug}/chapter/{chapterSlug}', [EntityControllers\\ChapterController::class, 'show']);\n    Route::put('/books/{bookSlug}/chapter/{chapterSlug}', [EntityControllers\\ChapterController::class, 'update']);\n    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/move', [EntityControllers\\ChapterController::class, 'showMove']);\n    Route::put('/books/{bookSlug}/chapter/{chapterSlug}/move', [EntityControllers\\ChapterController::class, 'move']);\n    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/copy', [EntityControllers\\ChapterController::class, 'showCopy']);\n    Route::post('/books/{bookSlug}/chapter/{chapterSlug}/copy', [EntityControllers\\ChapterController::class, 'copy']);\n    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/edit', [EntityControllers\\ChapterController::class, 'edit']);\n    Route::post('/books/{bookSlug}/chapter/{chapterSlug}/convert-to-book', [EntityControllers\\ChapterController::class, 'convertToBook']);\n    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/permissions', [PermissionsController::class, 'showForChapter']);\n    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/pdf', [ExportControllers\\ChapterExportController::class, 'pdf']);\n    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/html', [ExportControllers\\ChapterExportController::class, 'html']);\n    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/markdown', [ExportControllers\\ChapterExportController::class, 'markdown']);\n    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/plaintext', [ExportControllers\\ChapterExportController::class, 'plainText']);\n    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/zip', [ExportControllers\\ChapterExportController::class, 'zip']);\n    Route::put('/books/{bookSlug}/chapter/{chapterSlug}/permissions', [PermissionsController::class, 'updateForChapter']);\n    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/references', [ReferenceController::class, 'chapter']);\n    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/delete', [EntityControllers\\ChapterController::class, 'showDelete']);\n    Route::delete('/books/{bookSlug}/chapter/{chapterSlug}', [EntityControllers\\ChapterController::class, 'destroy']);\n\n    // User Profile routes\n    Route::get('/user/{slug}', [UserControllers\\UserProfileController::class, 'show']);\n\n    // Image routes\n    Route::get('/images/gallery', [UploadControllers\\GalleryImageController::class, 'list']);\n    Route::post('/images/gallery', [UploadControllers\\GalleryImageController::class, 'create']);\n    Route::get('/images/drawio', [UploadControllers\\DrawioImageController::class, 'list']);\n    Route::get('/images/drawio/base64/{id}', [UploadControllers\\DrawioImageController::class, 'getAsBase64']);\n    Route::post('/images/drawio', [UploadControllers\\DrawioImageController::class, 'create']);\n    Route::get('/images/edit/{id}', [UploadControllers\\ImageController::class, 'edit']);\n    Route::put('/images/{id}/file', [UploadControllers\\ImageController::class, 'updateFile']);\n    Route::put('/images/{id}/rebuild-thumbnails', [UploadControllers\\ImageController::class, 'rebuildThumbnails']);\n    Route::put('/images/{id}', [UploadControllers\\ImageController::class, 'update']);\n    Route::delete('/images/{id}', [UploadControllers\\ImageController::class, 'destroy']);\n\n    // Attachments routes\n    Route::get('/attachments/{id}', [UploadControllers\\AttachmentController::class, 'get']);\n    Route::post('/attachments/upload', [UploadControllers\\AttachmentController::class, 'upload']);\n    Route::post('/attachments/upload/{id}', [UploadControllers\\AttachmentController::class, 'uploadUpdate']);\n    Route::post('/attachments/link', [UploadControllers\\AttachmentController::class, 'attachLink']);\n    Route::put('/attachments/{id}', [UploadControllers\\AttachmentController::class, 'update']);\n    Route::get('/attachments/edit/{id}', [UploadControllers\\AttachmentController::class, 'getUpdateForm']);\n    Route::get('/attachments/get/page/{pageId}', [UploadControllers\\AttachmentController::class, 'listForPage']);\n    Route::put('/attachments/sort/page/{pageId}', [UploadControllers\\AttachmentController::class, 'sortForPage']);\n    Route::delete('/attachments/{id}', [UploadControllers\\AttachmentController::class, 'delete']);\n\n    // AJAX routes\n    Route::put('/ajax/page/{id}/save-draft', [EntityControllers\\PageController::class, 'saveDraft']);\n    Route::get('/ajax/page/{id}', [EntityControllers\\PageController::class, 'getPageAjax']);\n    Route::delete('/ajax/page/{id}', [EntityControllers\\PageController::class, 'ajaxDestroy']);\n\n    // Tag routes\n    Route::get('/tags', [ActivityControllers\\TagController::class, 'index']);\n    Route::get('/ajax/tags/suggest/names', [ActivityControllers\\TagController::class, 'getNameSuggestions']);\n    Route::get('/ajax/tags/suggest/values', [ActivityControllers\\TagController::class, 'getValueSuggestions']);\n\n    // Comments\n    Route::post('/comment/{pageId}', [ActivityControllers\\CommentController::class, 'savePageComment']);\n    Route::put('/comment/{id}/archive', [ActivityControllers\\CommentController::class, 'archive']);\n    Route::put('/comment/{id}/unarchive', [ActivityControllers\\CommentController::class, 'unarchive']);\n    Route::put('/comment/{id}', [ActivityControllers\\CommentController::class, 'update']);\n    Route::delete('/comment/{id}', [ActivityControllers\\CommentController::class, 'destroy']);\n\n    // Links\n    Route::get('/link/{id}', [EntityControllers\\PageController::class, 'redirectFromLink']);\n\n    // Search\n    Route::get('/search', [SearchController::class, 'search']);\n    Route::get('/search/book/{bookId}', [SearchController::class, 'searchBook']);\n    Route::get('/search/chapter/{bookId}', [SearchController::class, 'searchChapter']);\n    Route::get('/search/entity/siblings', [SearchController::class, 'searchSiblings']);\n    Route::get('/search/entity-selector', [SearchController::class, 'searchForSelector']);\n    Route::get('/search/entity-selector-templates', [SearchController::class, 'templatesForSelector']);\n    Route::get('/search/suggest', [SearchController::class, 'searchSuggestions']);\n\n    // User Search\n    Route::get('/search/users/select', [UserControllers\\UserSearchController::class, 'forSelect']);\n    Route::get('/search/users/mention', [UserControllers\\UserSearchController::class, 'forMentions']);\n\n    // Template System\n    Route::get('/templates', [EntityControllers\\PageTemplateController::class, 'list']);\n    Route::get('/templates/{templateId}', [EntityControllers\\PageTemplateController::class, 'get']);\n\n    // Favourites\n    Route::get('/favourites', [ActivityControllers\\FavouriteController::class, 'index']);\n    Route::post('/favourites/add', [ActivityControllers\\FavouriteController::class, 'add']);\n    Route::post('/favourites/remove', [ActivityControllers\\FavouriteController::class, 'remove']);\n\n    // Watching\n    Route::put('/watching/update', [ActivityControllers\\WatchController::class, 'update']);\n\n    // Importing\n    Route::get('/import', [ExportControllers\\ImportController::class, 'start']);\n    Route::post('/import', [ExportControllers\\ImportController::class, 'upload']);\n    Route::get('/import/{id}', [ExportControllers\\ImportController::class, 'show']);\n    Route::post('/import/{id}', [ExportControllers\\ImportController::class, 'run']);\n    Route::delete('/import/{id}', [ExportControllers\\ImportController::class, 'delete']);\n\n    // Other Pages\n    Route::get('/', [HomeController::class, 'index']);\n    Route::get('/home', [HomeController::class, 'index']);\n\n    // Permissions\n    Route::get('/permissions/form-row/{entityType}/{roleId}', [PermissionsController::class, 'formRowForRole']);\n\n    // Maintenance\n    Route::get('/settings/maintenance', [SettingControllers\\MaintenanceController::class, 'index']);\n    Route::delete('/settings/maintenance/cleanup-images', [SettingControllers\\MaintenanceController::class, 'cleanupImages']);\n    Route::post('/settings/maintenance/send-test-email', [SettingControllers\\MaintenanceController::class, 'sendTestEmail']);\n    Route::post('/settings/maintenance/regenerate-references', [SettingControllers\\MaintenanceController::class, 'regenerateReferences']);\n\n    // Recycle Bin\n    Route::get('/settings/recycle-bin', [EntityControllers\\RecycleBinController::class, 'index']);\n    Route::post('/settings/recycle-bin/empty', [EntityControllers\\RecycleBinController::class, 'empty']);\n    Route::get('/settings/recycle-bin/{id}/destroy', [EntityControllers\\RecycleBinController::class, 'showDestroy']);\n    Route::delete('/settings/recycle-bin/{id}', [EntityControllers\\RecycleBinController::class, 'destroy']);\n    Route::get('/settings/recycle-bin/{id}/restore', [EntityControllers\\RecycleBinController::class, 'showRestore']);\n    Route::post('/settings/recycle-bin/{id}/restore', [EntityControllers\\RecycleBinController::class, 'restore']);\n\n    // Audit Log\n    Route::get('/settings/audit', [ActivityControllers\\AuditLogController::class, 'index']);\n\n    // Users\n    Route::get('/settings/users', [UserControllers\\UserController::class, 'index']);\n    Route::get('/settings/users/create', [UserControllers\\UserController::class, 'create']);\n    Route::get('/settings/users/{id}/delete', [UserControllers\\UserController::class, 'delete']);\n    Route::post('/settings/users/create', [UserControllers\\UserController::class, 'store']);\n    Route::get('/settings/users/{id}', [UserControllers\\UserController::class, 'edit']);\n    Route::put('/settings/users/{id}', [UserControllers\\UserController::class, 'update']);\n    Route::delete('/settings/users/{id}', [UserControllers\\UserController::class, 'destroy']);\n\n    // User Account\n    Route::get('/my-account', [UserControllers\\UserAccountController::class, 'redirect']);\n    Route::get('/my-account/profile', [UserControllers\\UserAccountController::class, 'showProfile']);\n    Route::put('/my-account/profile', [UserControllers\\UserAccountController::class, 'updateProfile']);\n    Route::get('/my-account/shortcuts', [UserControllers\\UserAccountController::class, 'showShortcuts']);\n    Route::put('/my-account/shortcuts', [UserControllers\\UserAccountController::class, 'updateShortcuts']);\n    Route::get('/my-account/notifications', [UserControllers\\UserAccountController::class, 'showNotifications']);\n    Route::put('/my-account/notifications', [UserControllers\\UserAccountController::class, 'updateNotifications']);\n    Route::get('/my-account/auth', [UserControllers\\UserAccountController::class, 'showAuth']);\n    Route::put('/my-account/auth/password', [UserControllers\\UserAccountController::class, 'updatePassword']);\n    Route::get('/my-account/delete', [UserControllers\\UserAccountController::class, 'delete']);\n    Route::delete('/my-account', [UserControllers\\UserAccountController::class, 'destroy']);\n\n    // User Preference Endpoints\n    Route::patch('/preferences/change-view/{type}', [UserControllers\\UserPreferencesController::class, 'changeView']);\n    Route::patch('/preferences/change-sort/{type}', [UserControllers\\UserPreferencesController::class, 'changeSort']);\n    Route::patch('/preferences/change-expansion/{type}', [UserControllers\\UserPreferencesController::class, 'changeExpansion']);\n    Route::patch('/preferences/toggle-dark-mode', [UserControllers\\UserPreferencesController::class, 'toggleDarkMode']);\n    Route::patch('/preferences/update-code-language-favourite', [UserControllers\\UserPreferencesController::class, 'updateCodeLanguageFavourite']);\n\n    // User API Tokens\n    Route::get('/api-tokens/{userId}/create', [UserApiTokenController::class, 'create']);\n    Route::post('/api-tokens/{userId}/create', [UserApiTokenController::class, 'store']);\n    Route::get('/api-tokens/{userId}/{tokenId}', [UserApiTokenController::class, 'edit']);\n    Route::put('/api-tokens/{userId}/{tokenId}', [UserApiTokenController::class, 'update']);\n    Route::get('/api-tokens/{userId}/{tokenId}/delete', [UserApiTokenController::class, 'delete']);\n    Route::delete('/api-tokens/{userId}/{tokenId}', [UserApiTokenController::class, 'destroy']);\n\n    // Roles\n    Route::get('/settings/roles', [UserControllers\\RoleController::class, 'index']);\n    Route::get('/settings/roles/new', [UserControllers\\RoleController::class, 'create']);\n    Route::post('/settings/roles/new', [UserControllers\\RoleController::class, 'store']);\n    Route::get('/settings/roles/delete/{id}', [UserControllers\\RoleController::class, 'showDelete']);\n    Route::delete('/settings/roles/delete/{id}', [UserControllers\\RoleController::class, 'delete']);\n    Route::get('/settings/roles/{id}', [UserControllers\\RoleController::class, 'edit']);\n    Route::put('/settings/roles/{id}', [UserControllers\\RoleController::class, 'update']);\n\n    // Webhooks\n    Route::get('/settings/webhooks', [ActivityControllers\\WebhookController::class, 'index']);\n    Route::get('/settings/webhooks/create', [ActivityControllers\\WebhookController::class, 'create']);\n    Route::post('/settings/webhooks/create', [ActivityControllers\\WebhookController::class, 'store']);\n    Route::get('/settings/webhooks/{id}', [ActivityControllers\\WebhookController::class, 'edit']);\n    Route::put('/settings/webhooks/{id}', [ActivityControllers\\WebhookController::class, 'update']);\n    Route::get('/settings/webhooks/{id}/delete', [ActivityControllers\\WebhookController::class, 'delete']);\n    Route::delete('/settings/webhooks/{id}', [ActivityControllers\\WebhookController::class, 'destroy']);\n\n    // Sort Rules\n    Route::get('/settings/sorting/rules/new', [SortingControllers\\SortRuleController::class, 'create']);\n    Route::post('/settings/sorting/rules', [SortingControllers\\SortRuleController::class, 'store']);\n    Route::get('/settings/sorting/rules/{id}', [SortingControllers\\SortRuleController::class, 'edit']);\n    Route::put('/settings/sorting/rules/{id}', [SortingControllers\\SortRuleController::class, 'update']);\n    Route::delete('/settings/sorting/rules/{id}', [SortingControllers\\SortRuleController::class, 'destroy']);\n\n    // Settings\n    Route::get('/settings', [SettingControllers\\SettingController::class, 'index'])->name('settings');\n    Route::get('/settings/{category}', [SettingControllers\\SettingController::class, 'category'])->name('settings.category');\n    Route::post('/settings/{category}', [SettingControllers\\SettingController::class, 'update']);\n});\n\n// MFA routes\nRoute::middleware('mfa-setup')->group(function () {\n    Route::get('/mfa/setup', [AccessControllers\\MfaController::class, 'setup']);\n    Route::get('/mfa/totp/generate', [AccessControllers\\MfaTotpController::class, 'generate']);\n    Route::post('/mfa/totp/confirm', [AccessControllers\\MfaTotpController::class, 'confirm']);\n    Route::get('/mfa/backup_codes/generate', [AccessControllers\\MfaBackupCodesController::class, 'generate']);\n    Route::post('/mfa/backup_codes/confirm', [AccessControllers\\MfaBackupCodesController::class, 'confirm']);\n});\nRoute::middleware('guest')->group(function () {\n    Route::get('/mfa/verify', [AccessControllers\\MfaController::class, 'verify']);\n    Route::post('/mfa/totp/verify', [AccessControllers\\MfaTotpController::class, 'verify']);\n    Route::post('/mfa/backup_codes/verify', [AccessControllers\\MfaBackupCodesController::class, 'verify']);\n});\nRoute::delete('/mfa/{method}/remove', [AccessControllers\\MfaController::class, 'remove'])->middleware('auth');\n\n// Social auth routes\nRoute::get('/login/service/{socialDriver}', [AccessControllers\\SocialController::class, 'login']);\nRoute::get('/login/service/{socialDriver}/callback', [AccessControllers\\SocialController::class, 'callback']);\nRoute::post('/login/service/{socialDriver}/detach', [AccessControllers\\SocialController::class, 'detach'])->middleware('auth');\nRoute::get('/register/service/{socialDriver}', [AccessControllers\\SocialController::class, 'register']);\n\n// Login/Logout routes\nRoute::get('/login', [AccessControllers\\LoginController::class, 'getLogin']);\nRoute::post('/login', [AccessControllers\\LoginController::class, 'login']);\nRoute::post('/logout', [AccessControllers\\LoginController::class, 'logout']);\nRoute::get('/register', [AccessControllers\\RegisterController::class, 'getRegister']);\nRoute::get('/register/confirm', [AccessControllers\\ConfirmEmailController::class, 'show']);\nRoute::get('/register/confirm/awaiting', [AccessControllers\\ConfirmEmailController::class, 'showAwaiting']);\nRoute::post('/register/confirm/resend', [AccessControllers\\ConfirmEmailController::class, 'resend']);\nRoute::get('/register/confirm/{token}', [AccessControllers\\ConfirmEmailController::class, 'showAcceptForm']);\nRoute::post('/register/confirm/accept', [AccessControllers\\ConfirmEmailController::class, 'confirm'])->middleware('throttle:public');\nRoute::post('/register', [AccessControllers\\RegisterController::class, 'postRegister'])->middleware('throttle:public');\n\n// SAML routes\nRoute::post('/saml2/login', [AccessControllers\\Saml2Controller::class, 'login']);\nRoute::post('/saml2/logout', [AccessControllers\\Saml2Controller::class, 'logout']);\nRoute::get('/saml2/metadata', [AccessControllers\\Saml2Controller::class, 'metadata']);\nRoute::get('/saml2/sls', [AccessControllers\\Saml2Controller::class, 'sls']);\nRoute::post('/saml2/acs', [AccessControllers\\Saml2Controller::class, 'startAcs'])->withoutMiddleware([\n    StartSession::class,\n    ShareErrorsFromSession::class,\n    VerifyCsrfToken::class,\n]);\nRoute::get('/saml2/acs', [AccessControllers\\Saml2Controller::class, 'processAcs']);\n\n// OIDC routes\nRoute::post('/oidc/login', [AccessControllers\\OidcController::class, 'login']);\nRoute::get('/oidc/callback', [AccessControllers\\OidcController::class, 'callback']);\nRoute::post('/oidc/logout', [AccessControllers\\OidcController::class, 'logout']);\n\n// User invitation routes\nRoute::get('/register/invite/{token}', [AccessControllers\\UserInviteController::class, 'showSetPassword'])->middleware('throttle:public');\nRoute::post('/register/invite/{token}', [AccessControllers\\UserInviteController::class, 'setPassword'])->middleware('throttle:public');\n\n// Password reset link request routes\nRoute::get('/password/email', [AccessControllers\\ForgotPasswordController::class, 'showLinkRequestForm']);\nRoute::post('/password/email', [AccessControllers\\ForgotPasswordController::class, 'sendResetLinkEmail'])->middleware('throttle:public');\n\n// Password reset routes\nRoute::get('/password/reset/{token}', [AccessControllers\\ResetPasswordController::class, 'showResetForm']);\nRoute::post('/password/reset', [AccessControllers\\ResetPasswordController::class, 'reset'])->middleware('throttle:public');\n\n// Help & Info routes\nRoute::view('/help/tinymce', 'help.tinymce');\nRoute::view('/help/wysiwyg', 'help.wysiwyg');\n\n// Theme Routes\nRoute::get('/theme/{theme}/{path}', [ThemeController::class, 'publicFile'])\n    ->where('path', '.*$');\n\nRoute::fallback([MetaController::class, 'notFound'])->name('fallback');\n"
  },
  {
    "path": "storage/app/.gitignore",
    "content": "*\n!.gitignore"
  },
  {
    "path": "storage/backups/.gitignore",
    "content": "*\n!.gitignore\n"
  },
  {
    "path": "storage/clockwork/.gitignore",
    "content": "*.json\n*.json.gz\nindex\n"
  },
  {
    "path": "storage/fonts/.gitignore",
    "content": "*\n!.gitignore"
  },
  {
    "path": "storage/framework/.gitignore",
    "content": "compiled.php\nconfig.php\ndown\nevents.scanned.php\nmaintenance.php\nroutes.php\nroutes.scanned.php\nschedule-*\nservices.json\npurifier/\n"
  },
  {
    "path": "storage/framework/cache/.gitignore",
    "content": "*\n!data/\n!.gitignore"
  },
  {
    "path": "storage/framework/sessions/.gitignore",
    "content": "*\n!.gitignore\n"
  },
  {
    "path": "storage/framework/views/.gitignore",
    "content": "*\n!.gitignore\n"
  },
  {
    "path": "storage/logs/.gitignore",
    "content": "*\n!.gitignore\n"
  },
  {
    "path": "storage/uploads/files/.gitignore",
    "content": "*\n!.gitignore\n"
  },
  {
    "path": "storage/uploads/images/.gitignore",
    "content": "*\n!.gitignore\n"
  },
  {
    "path": "tests/Activity/AuditLogApiTest.php",
    "content": "<?php\n\nnamespace Tests\\Activity;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Facades\\Activity;\nuse Tests\\Api\\TestsApi;\nuse Tests\\TestCase;\n\nclass AuditLogApiTest extends TestCase\n{\n    use TestsApi;\n\n    public function test_user_and_settings_manage_permissions_needed()\n    {\n        $editor = $this->users->editor();\n\n        $assertPermissionErrorOnCall = function () use ($editor) {\n            $resp = $this->actingAsForApi($editor)->getJson('/api/audit-log');\n            $resp->assertStatus(403);\n            $resp->assertJson($this->permissionErrorResponse());\n        };\n\n        $assertPermissionErrorOnCall();\n        $this->permissions->grantUserRolePermissions($editor, ['users-manage']);\n        $assertPermissionErrorOnCall();\n        $this->permissions->removeUserRolePermissions($editor, ['users-manage']);\n        $this->permissions->grantUserRolePermissions($editor, ['settings-manage']);\n        $assertPermissionErrorOnCall();\n\n        $this->permissions->grantUserRolePermissions($editor, ['settings-manage', 'users-manage']);\n        $resp = $this->actingAsForApi($editor)->getJson('/api/audit-log');\n        $resp->assertOk();\n    }\n\n    public function test_index_endpoint_returns_expected_data()\n    {\n        $page = $this->entities->page();\n        $admin = $this->users->admin();\n        $this->actingAsForApi($admin);\n        Activity::add(ActivityType::PAGE_UPDATE, $page);\n\n        $resp = $this->get(\"/api/audit-log?filter[loggable_id]={$page->id}\");\n        $resp->assertJson(['data' => [\n            [\n                'type' => 'page_update',\n                'detail' => \"({$page->id}) {$page->name}\",\n                'user_id' => $admin->id,\n                'loggable_id' => $page->id,\n                'loggable_type' => 'page',\n                'ip' => '127.0.0.1',\n                'user' => [\n                    'id' => $admin->id,\n                    'name' => $admin->name,\n                    'slug' => $admin->slug,\n                ],\n            ]\n        ]]);\n    }\n}\n"
  },
  {
    "path": "tests/Activity/AuditLogTest.php",
    "content": "<?php\n\nnamespace Tests\\Activity;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Activity\\Models\\Activity;\nuse BookStack\\Activity\\Tools\\ActivityLogger;\nuse BookStack\\Entities\\Repos\\PageRepo;\nuse BookStack\\Entities\\Tools\\TrashCan;\nuse BookStack\\Users\\UserRepo;\nuse Carbon\\Carbon;\nuse Tests\\TestCase;\n\nclass AuditLogTest extends TestCase\n{\n    protected ActivityLogger $activityService;\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n        $this->activityService = app(ActivityLogger::class);\n    }\n\n    public function test_only_accessible_with_right_permissions()\n    {\n        $viewer = $this->users->viewer();\n        $this->actingAs($viewer);\n\n        $resp = $this->get('/settings/audit');\n        $this->assertPermissionError($resp);\n\n        $this->permissions->grantUserRolePermissions($viewer, ['settings-manage']);\n        $resp = $this->get('/settings/audit');\n        $this->assertPermissionError($resp);\n\n        $this->permissions->grantUserRolePermissions($viewer, ['users-manage']);\n        $resp = $this->get('/settings/audit');\n        $resp->assertStatus(200);\n        $resp->assertSeeText('Audit Log');\n    }\n\n    public function test_shows_activity()\n    {\n        $admin = $this->users->admin();\n        $this->actingAs($admin);\n        $page = $this->entities->page();\n        $this->activityService->add(ActivityType::PAGE_CREATE, $page);\n        $activity = Activity::query()->orderBy('id', 'desc')->first();\n\n        $resp = $this->get('settings/audit');\n        $resp->assertSeeText($page->name);\n        $resp->assertSeeText('page_create');\n        $resp->assertSeeText($activity->created_at->toDateTimeString());\n        $this->withHtml($resp)->assertElementContains('a[href*=\"users/' . $admin->id . '\"]', $admin->name);\n    }\n\n    public function test_shows_name_for_deleted_items()\n    {\n        $this->actingAs($this->users->admin());\n        $page = $this->entities->page();\n        $pageName = $page->name;\n        $this->activityService->add(ActivityType::PAGE_CREATE, $page);\n\n        app(PageRepo::class)->destroy($page);\n        app(TrashCan::class)->empty();\n\n        $resp = $this->get('settings/audit');\n        $resp->assertSeeText('Deleted Item');\n        $resp->assertSeeText('Name: ' . $pageName);\n    }\n\n    public function test_shows_activity_for_deleted_users()\n    {\n        $viewer = $this->users->viewer();\n        $this->actingAs($viewer);\n        $page = $this->entities->page();\n        $this->activityService->add(ActivityType::PAGE_CREATE, $page);\n\n        $this->actingAs($this->users->admin());\n        app(UserRepo::class)->destroy($viewer);\n\n        $resp = $this->get('settings/audit');\n        $resp->assertSeeText(\"[ID: {$viewer->id}] Deleted User\");\n    }\n\n    public function test_deleted_user_shows_if_user_created_date_is_later_than_activity()\n    {\n        $viewer = $this->users->viewer();\n        $this->actingAs($viewer);\n        $page = $this->entities->page();\n        $this->activityService->add(ActivityType::PAGE_CREATE, $page);\n        $viewer->created_at = Carbon::now()->addDay();\n        $viewer->save();\n\n        $this->actingAs($this->users->admin());\n\n        $resp = $this->get('settings/audit');\n        $resp->assertSeeText(\"[ID: {$viewer->id}] Deleted User\");\n        $resp->assertDontSee($viewer->name);\n    }\n\n    public function test_filters_by_key()\n    {\n        $this->actingAs($this->users->admin());\n        $page = $this->entities->page();\n        $this->activityService->add(ActivityType::PAGE_CREATE, $page);\n\n        $resp = $this->get('settings/audit');\n        $resp->assertSeeText($page->name);\n\n        $resp = $this->get('settings/audit?event=page_delete');\n        $resp->assertDontSeeText($page->name);\n    }\n\n    public function test_date_filters()\n    {\n        $this->actingAs($this->users->admin());\n        $page = $this->entities->page();\n        $this->activityService->add(ActivityType::PAGE_CREATE, $page);\n\n        $yesterday = (Carbon::now()->subDay()->format('Y-m-d'));\n        $tomorrow = (Carbon::now()->addDay()->format('Y-m-d'));\n\n        $resp = $this->get('settings/audit?date_from=' . $yesterday);\n        $resp->assertSeeText($page->name);\n\n        $resp = $this->get('settings/audit?date_from=' . $tomorrow);\n        $resp->assertDontSeeText($page->name);\n\n        $resp = $this->get('settings/audit?date_to=' . $tomorrow);\n        $resp->assertSeeText($page->name);\n\n        $resp = $this->get('settings/audit?date_to=' . $yesterday);\n        $resp->assertDontSeeText($page->name);\n    }\n\n    public function test_user_filter()\n    {\n        $admin = $this->users->admin();\n        $editor = $this->users->editor();\n        $this->actingAs($admin);\n        $page = $this->entities->page();\n        $this->activityService->add(ActivityType::PAGE_CREATE, $page);\n\n        $this->actingAs($editor);\n        $chapter = $this->entities->chapter();\n        $this->activityService->add(ActivityType::CHAPTER_UPDATE, $chapter);\n\n        $resp = $this->actingAs($admin)->get('settings/audit?user=' . $admin->id);\n        $resp->assertSeeText($page->name);\n        $resp->assertDontSeeText($chapter->name);\n\n        $resp = $this->actingAs($admin)->get('settings/audit?user=' . $editor->id);\n        $resp->assertSeeText($chapter->name);\n        $resp->assertDontSeeText($page->name);\n    }\n\n    public function test_ip_address_logged_and_visible()\n    {\n        config()->set('app.proxies', '*');\n        $editor = $this->users->editor();\n        $page = $this->entities->page();\n\n        $this->actingAs($editor)->put($page->getUrl(), [\n            'name' => 'Updated page',\n            'html' => '<p>Updated content</p>',\n        ], [\n            'X-Forwarded-For' => '192.123.45.1',\n        ])->assertRedirect($page->refresh()->getUrl());\n\n        $this->assertDatabaseHas('activities', [\n            'type'      => ActivityType::PAGE_UPDATE,\n            'ip'        => '192.123.45.1',\n            'user_id'   => $editor->id,\n            'loggable_id' => $page->id,\n        ]);\n\n        $resp = $this->asAdmin()->get('/settings/audit');\n        $resp->assertSee('192.123.45.1');\n    }\n\n    public function test_ip_address_is_searchable()\n    {\n        config()->set('app.proxies', '*');\n        $editor = $this->users->editor();\n        $page = $this->entities->page();\n\n        $this->actingAs($editor)->put($page->getUrl(), [\n            'name' => 'Updated page',\n            'html' => '<p>Updated content</p>',\n        ], [\n            'X-Forwarded-For' => '192.123.45.1',\n        ])->assertRedirect($page->refresh()->getUrl());\n\n        $this->actingAs($editor)->put($page->getUrl(), [\n            'name' => 'Updated page',\n            'html' => '<p>Updated content</p>',\n        ], [\n            'X-Forwarded-For' => '192.122.45.1',\n        ])->assertRedirect($page->refresh()->getUrl());\n\n        $resp = $this->asAdmin()->get('/settings/audit?&ip=192.123');\n        $resp->assertSee('192.123.45.1');\n        $resp->assertDontSee('192.122.45.1');\n    }\n\n    public function test_ip_address_not_logged_in_demo_mode()\n    {\n        config()->set('app.proxies', '*');\n        config()->set('app.env', 'demo');\n        $editor = $this->users->editor();\n        $page = $this->entities->page();\n\n        $this->actingAs($editor)->put($page->getUrl(), [\n            'name' => 'Updated page',\n            'html' => '<p>Updated content</p>',\n        ], [\n            'X-Forwarded-For' => '192.123.45.1',\n            'REMOTE_ADDR'     => '192.123.45.2',\n        ])->assertRedirect($page->refresh()->getUrl());\n\n        $this->assertDatabaseHas('activities', [\n            'type'      => ActivityType::PAGE_UPDATE,\n            'ip'        => '127.0.0.1',\n            'user_id'   => $editor->id,\n            'loggable_id' => $page->id,\n        ]);\n    }\n\n    public function test_ip_address_respects_precision_setting()\n    {\n        config()->set('app.proxies', '*');\n        config()->set('app.ip_address_precision', 2);\n        $editor = $this->users->editor();\n        $page = $this->entities->page();\n\n        $this->actingAs($editor)->put($page->getUrl(), [\n            'name' => 'Updated page',\n            'html' => '<p>Updated content</p>',\n        ], [\n            'X-Forwarded-For' => '192.123.45.1',\n        ])->assertRedirect($page->refresh()->getUrl());\n\n        $this->assertDatabaseHas('activities', [\n            'type'      => ActivityType::PAGE_UPDATE,\n            'ip'        => '192.123.x.x',\n            'user_id'   => $editor->id,\n            'loggable_id' => $page->id,\n        ]);\n    }\n}\n"
  },
  {
    "path": "tests/Activity/CommentDisplayTest.php",
    "content": "<?php\n\nnamespace Tests\\Activity;\n\nuse BookStack\\Activity\\Models\\Comment;\nuse Tests\\TestCase;\n\nclass CommentDisplayTest extends TestCase\n{\n    public function test_reply_comments_are_nested()\n    {\n        $this->asAdmin();\n        $page = $this->entities->page();\n\n        $this->postJson(\"/comment/$page->id\", ['html' => '<p>My new comment</p>']);\n        $this->postJson(\"/comment/$page->id\", ['html' => '<p>My new comment</p>']);\n\n        $respHtml = $this->withHtml($this->get($page->getUrl()));\n        $respHtml->assertElementCount('.comment-branch', 3);\n        $respHtml->assertElementNotExists('.comment-branch .comment-branch');\n\n        $comment = $page->comments()->first();\n        $resp = $this->postJson(\"/comment/$page->id\", [\n            'html' => '<p>My nested comment</p>', 'parent_id' => $comment->local_id\n        ]);\n        $resp->assertStatus(200);\n\n        $respHtml = $this->withHtml($this->get($page->getUrl()));\n        $respHtml->assertElementCount('.comment-branch', 4);\n        $respHtml->assertElementContains('.comment-branch .comment-branch', 'My nested comment');\n    }\n\n    public function test_comments_are_visible_in_the_page_editor()\n    {\n        $page = $this->entities->page();\n\n        $this->asAdmin()->postJson(\"/comment/$page->id\", ['html' => '<p>My great comment to see in the editor</p>']);\n\n        $respHtml = $this->withHtml($this->get($page->getUrl('/edit')));\n        $respHtml->assertElementContains('.comment-box .content', 'My great comment to see in the editor');\n    }\n\n    public function test_comment_creator_name_truncated()\n    {\n        [$longNamedUser] = $this->users->newUserWithRole(['name' => 'Wolfeschlegelsteinhausenbergerdorff'], ['comment-create-all', 'page-view-all']);\n        $page = $this->entities->page();\n\n        $comment = Comment::factory()->make();\n        $this->actingAs($longNamedUser)->postJson(\"/comment/$page->id\", $comment->getAttributes());\n\n        $pageResp = $this->asAdmin()->get($page->getUrl());\n        $pageResp->assertSee('Wolfeschlegels…');\n    }\n\n    public function test_comment_editor_js_loaded_with_create_or_edit_permissions()\n    {\n        $editor = $this->users->editor();\n        $page = $this->entities->page();\n\n        $resp = $this->actingAs($editor)->get($page->getUrl());\n        $resp->assertSee('window.editor_translations', false);\n        $resp->assertSee('component=\"entity-selector\"', false);\n\n        $this->permissions->removeUserRolePermissions($editor, ['comment-create-all']);\n        $this->permissions->grantUserRolePermissions($editor, ['comment-update-own']);\n\n        $resp = $this->actingAs($editor)->get($page->getUrl());\n        $resp->assertDontSee('window.editor_translations', false);\n        $resp->assertDontSee('component=\"entity-selector\"', false);\n\n        Comment::factory()->create([\n            'created_by'  => $editor->id,\n            'commentable_type' => 'page',\n            'commentable_id'   => $page->id,\n        ]);\n\n        $resp = $this->actingAs($editor)->get($page->getUrl());\n        $resp->assertSee('window.editor_translations', false);\n        $resp->assertSee('component=\"entity-selector\"', false);\n    }\n\n    public function test_comment_displays_relative_times()\n    {\n        $page = $this->entities->page();\n        $comment = Comment::factory()->create(['commentable_id' => $page->id, 'commentable_type' => $page->getMorphClass()]);\n        $comment->created_at = now()->subWeek();\n        $comment->updated_at = now()->subDay();\n        $comment->save();\n\n        $pageResp = $this->asAdmin()->get($page->getUrl());\n        $html = $this->withHtml($pageResp);\n\n        // Create date shows relative time as text to user\n        $html->assertElementContains('.comment-box', 'commented 1 week ago');\n        // Updated indicator has full time as title\n        $html->assertElementContains('.comment-box span[title^=\"Updated ' . $comment->updated_at->format('Y-m-d') .  '\"]', 'Updated');\n    }\n\n    public function test_comment_displays_reference_if_set()\n    {\n        $page = $this->entities->page();\n        $comment = Comment::factory()->make([\n            'content_ref' => 'bkmrk-a:abc:4-1',\n            'local_id'   =>  10,\n        ]);\n        $page->comments()->save($comment);\n\n        $html = $this->withHtml($this->asEditor()->get($page->getUrl()));\n        $html->assertElementExists('#comment10 .comment-reference-indicator-wrap a');\n    }\n\n    public function test_archived_comments_are_shown_in_their_own_container()\n    {\n        $page = $this->entities->page();\n        $comment = Comment::factory()->make(['local_id' => 44]);\n        $page->comments()->save($comment);\n\n        $html = $this->withHtml($this->asEditor()->get($page->getUrl()));\n        $html->assertElementExists('#comment-tab-panel-active #comment44');\n        $html->assertElementNotExists('#comment-tab-panel-archived .comment-box');\n\n        $comment->archived = true;\n        $comment->save();\n\n        $html = $this->withHtml($this->asEditor()->get($page->getUrl()));\n        $html->assertElementExists('#comment-tab-panel-archived #comment44.comment-box');\n        $html->assertElementNotExists('#comment-tab-panel-active #comment44');\n    }\n}\n"
  },
  {
    "path": "tests/Activity/CommentMentionTest.php",
    "content": "<?php\n\nnamespace Tests\\Activity;\n\nuse BookStack\\Activity\\Notifications\\Messages\\CommentMentionNotification;\nuse BookStack\\Permissions\\Permission;\nuse Illuminate\\Support\\Facades\\Notification;\nuse Tests\\TestCase;\n\nclass CommentMentionTest extends TestCase\n{\n    public function test_mentions_are_notified()\n    {\n        $userToMention = $this->users->viewer();\n        $this->permissions->grantUserRolePermissions($userToMention, [Permission::ReceiveNotifications]);\n        $editor = $this->users->editor();\n        $page = $this->entities->pageWithinChapter();\n        $notifications = Notification::fake();\n\n        $this->actingAs($editor)->post(\"/comment/{$page->id}\", [\n            'html' => '<p>Hello <a data-mention-user-id=\"' . $userToMention->id . '\">@user</a></p>'\n        ])->assertOk();\n\n        $notifications->assertSentTo($userToMention, function (CommentMentionNotification $notification) use ($userToMention, $editor, $page) {\n            $mail = $notification->toMail($userToMention);\n            $mailContent = html_entity_decode(strip_tags($mail->render()), ENT_QUOTES);\n            $subjectPrefix = 'You have been mentioned in a comment on page: ' . mb_substr($page->name, 0, 20);\n            return str_starts_with($mail->subject, $subjectPrefix)\n                && str_contains($mailContent, 'View Comment')\n                && str_contains($mailContent, 'Page Name: ' . $page->name)\n                && str_contains($mailContent, 'Page Path: ' . $page->book->getShortName(24) . ' > ' . $page->chapter->getShortName(24))\n                && str_contains($mailContent, 'Commenter: ' . $editor->name)\n                && str_contains($mailContent, 'Comment: Hello @user');\n        });\n    }\n\n    public function test_mentions_are_not_notified_if_mentioned_by_same_user()\n    {\n        $editor = $this->users->editor();\n        $this->permissions->grantUserRolePermissions($editor, [Permission::ReceiveNotifications]);\n        $page = $this->entities->page();\n        $notifications = Notification::fake();\n\n        $this->actingAs($editor)->post(\"/comment/{$page->id}\", [\n            'html' => '<p>Hello <a data-mention-user-id=\"' . $editor->id . '\"></a></p>'\n        ])->assertOk();\n\n        $notifications->assertNothingSent();\n    }\n\n    public function test_mentions_are_logged_to_the_database_even_if_not_notified()\n    {\n        $editor = $this->users->editor();\n        $otherUser = $this->users->viewer();\n        $this->permissions->grantUserRolePermissions($editor, [Permission::ReceiveNotifications]);\n        $page = $this->entities->page();\n        $notifications = Notification::fake();\n\n        $this->actingAs($editor)->post(\"/comment/{$page->id}\", [\n            'html' => '<p>Hello <a data-mention-user-id=\"' . $editor->id . '\"></a> and <a data-mention-user-id=\"' . $otherUser->id . '\"></a></p>'\n        ])->assertOk();\n\n        $notifications->assertNothingSent();\n\n        $comment = $page->comments()->latest()->first();\n        $this->assertDatabaseHas('mention_history', [\n            'mentionable_id' => $comment->id,\n            'mentionable_type' => 'comment',\n            'from_user_id' => $editor->id,\n            'to_user_id' => $otherUser->id,\n        ]);\n        $this->assertDatabaseHas('mention_history', [\n            'mentionable_id' => $comment->id,\n            'mentionable_type' => 'comment',\n            'from_user_id' => $editor->id,\n            'to_user_id' => $editor->id,\n        ]);\n    }\n\n    public function test_comment_updates_will_send_notifications_only_if_mention_is_new()\n    {\n        $userToMention = $this->users->viewer();\n        $this->permissions->grantUserRolePermissions($userToMention, [Permission::ReceiveNotifications]);\n        $editor = $this->users->editor();\n        $this->permissions->grantUserRolePermissions($editor, [Permission::CommentUpdateOwn]);\n        $page = $this->entities->page();\n        $notifications = Notification::fake();\n\n        $this->actingAs($editor)->post(\"/comment/{$page->id}\", [\n            'html' => '<p>Hello there</p>'\n        ])->assertOk();\n        $comment = $page->comments()->latest()->first();\n\n        $notifications->assertNothingSent();\n\n        $this->put(\"/comment/{$comment->id}\", [\n            'html' => '<p>Hello <a data-mention-user-id=\"' . $userToMention->id . '\"></a></p>'\n        ])->assertOk();\n\n        $notifications->assertSentTo($userToMention, CommentMentionNotification::class);\n        $notifications->assertCount(1);\n\n        $this->put(\"/comment/{$comment->id}\", [\n            'html' => '<p>Hello again<a data-mention-user-id=\"' . $userToMention->id . '\"></a></p>'\n        ])->assertOk();\n\n        $notifications->assertCount(1);\n    }\n\n    public function test_notification_limited_to_those_with_view_permissions()\n    {\n        $userA = $this->users->newUser();\n        $userB = $this->users->newUser();\n        $this->permissions->grantUserRolePermissions($userA, [Permission::ReceiveNotifications]);\n        $this->permissions->grantUserRolePermissions($userB, [Permission::ReceiveNotifications]);\n        $notifications = Notification::fake();\n        $page = $this->entities->page();\n\n        $this->permissions->disableEntityInheritedPermissions($page);\n        $this->permissions->addEntityPermission($page, ['view'], $userA->roles()->first());\n\n        $this->asAdmin()->post(\"/comment/{$page->id}\", [\n            'html' => '<p>Hello <a data-mention-user-id=\"' . $userA->id . '\"></a> and <a data-mention-user-id=\"' . $userB->id . '\"></a></p>'\n        ])->assertOk();\n\n        $notifications->assertCount(1);\n        $notifications->assertSentTo($userA, CommentMentionNotification::class);\n    }\n}\n"
  },
  {
    "path": "tests/Activity/CommentSettingTest.php",
    "content": "<?php\n\nnamespace Tests\\Activity;\n\nuse Tests\\TestCase;\n\nclass CommentSettingTest extends TestCase\n{\n    public function test_comment_disable()\n    {\n        $page = $this->entities->page();\n        $this->setSettings(['app-disable-comments' => 'true']);\n        $this->asAdmin();\n\n        $resp = $this->asAdmin()->get($page->getUrl());\n        $this->withHtml($resp)->assertElementNotExists('.comments-list');\n    }\n\n    public function test_comment_enable()\n    {\n        $page = $this->entities->page();\n        $this->setSettings(['app-disable-comments' => 'false']);\n        $this->asAdmin();\n\n        $resp = $this->asAdmin()->get($page->getUrl());\n        $this->withHtml($resp)->assertElementExists('.comments-list');\n    }\n}\n"
  },
  {
    "path": "tests/Activity/CommentStoreTest.php",
    "content": "<?php\n\nnamespace Tests\\Activity;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Activity\\Models\\Comment;\nuse Tests\\TestCase;\n\nclass CommentStoreTest extends TestCase\n{\n    public function test_add_comment()\n    {\n        $this->asAdmin();\n        $page = $this->entities->page();\n\n        Comment::factory()->create(['commentable_id' => $page->id, 'commentable_type' => 'page', 'local_id' => 2]);\n        $comment = Comment::factory()->make(['parent_id' => 2]);\n        $resp = $this->postJson(\"/comment/$page->id\", $comment->getAttributes());\n\n        $resp->assertStatus(200);\n        $resp->assertSee($comment->html, false);\n\n        $pageResp = $this->get($page->getUrl());\n        $pageResp->assertSee($comment->html, false);\n\n        $this->assertDatabaseHas('comments', [\n            'local_id'    => 3,\n            'commentable_id'   => $page->id,\n            'commentable_type' => 'page',\n            'parent_id'   => 2,\n        ]);\n\n        $this->assertActivityExists(ActivityType::COMMENT_CREATE);\n    }\n    public function test_add_comment_stores_content_reference_only_if_format_valid()\n    {\n        $validityByRefs = [\n            'bkmrk-my-title:4589284922:4-3' => true,\n            'bkmrk-my-title:4589284922:' => true,\n            'bkmrk-my-title:4589284922:abc' => false,\n            'my-title:4589284922:' => false,\n            'bkmrk-my-title-4589284922:' => false,\n        ];\n\n        $page = $this->entities->page();\n\n        foreach ($validityByRefs as $ref => $valid) {\n            $this->asAdmin()->postJson(\"/comment/$page->id\", [\n                'html' => '<p>My comment</p>',\n                'parent_id' => null,\n                'content_ref' => $ref,\n            ]);\n\n            if ($valid) {\n                $this->assertDatabaseHas('comments', ['commentable_id' => $page->id, 'content_ref' => $ref]);\n            } else {\n                $this->assertDatabaseMissing('comments', ['commentable_id' => $page->id, 'content_ref' => $ref]);\n            }\n        }\n    }\n\n    public function test_comment_edit()\n    {\n        $this->asAdmin();\n        $page = $this->entities->page();\n\n        $comment = Comment::factory()->make();\n        $this->postJson(\"/comment/$page->id\", $comment->getAttributes());\n\n        $comment = $page->comments()->first();\n        $newHtml = '<p>updated text content</p>';\n        $resp = $this->putJson(\"/comment/$comment->id\", [\n            'html' => $newHtml,\n        ]);\n\n        $resp->assertStatus(200);\n        $resp->assertSee($newHtml, false);\n        $resp->assertDontSee($comment->html, false);\n\n        $this->assertDatabaseHas('comments', [\n            'html'      => $newHtml,\n            'commentable_id' => $page->id,\n        ]);\n\n        $this->assertActivityExists(ActivityType::COMMENT_UPDATE);\n    }\n\n    public function test_comment_delete()\n    {\n        $this->asAdmin();\n        $page = $this->entities->page();\n\n        $comment = Comment::factory()->make();\n        $this->postJson(\"/comment/$page->id\", $comment->getAttributes());\n\n        $comment = $page->comments()->first();\n\n        $resp = $this->delete(\"/comment/$comment->id\");\n        $resp->assertStatus(200);\n\n        $this->assertDatabaseMissing('comments', [\n            'id' => $comment->id,\n        ]);\n\n        $this->assertActivityExists(ActivityType::COMMENT_DELETE);\n    }\n\n    public function test_comment_archive_and_unarchive()\n    {\n        $this->asAdmin();\n        $page = $this->entities->page();\n\n        $comment = Comment::factory()->make();\n        $page->comments()->save($comment);\n        $comment->refresh();\n\n        $this->put(\"/comment/$comment->id/archive\");\n\n        $this->assertDatabaseHas('comments', [\n            'id' => $comment->id,\n            'archived' => true,\n        ]);\n\n        $this->assertActivityExists(ActivityType::COMMENT_UPDATE);\n\n        $this->put(\"/comment/$comment->id/unarchive\");\n\n        $this->assertDatabaseHas('comments', [\n            'id' => $comment->id,\n            'archived' => false,\n        ]);\n\n        $this->assertActivityExists(ActivityType::COMMENT_UPDATE);\n    }\n\n    public function test_archive_endpoints_require_delete_or_edit_permissions()\n    {\n        $viewer = $this->users->viewer();\n        $page = $this->entities->page();\n\n        $comment = Comment::factory()->make();\n        $page->comments()->save($comment);\n        $comment->refresh();\n\n        $endpoints = [\"/comment/$comment->id/archive\", \"/comment/$comment->id/unarchive\"];\n\n        foreach ($endpoints as $endpoint) {\n            $resp = $this->actingAs($viewer)->put($endpoint);\n            $this->assertPermissionError($resp);\n        }\n\n        $this->permissions->grantUserRolePermissions($viewer, ['comment-delete-all']);\n\n        foreach ($endpoints as $endpoint) {\n            $resp = $this->actingAs($viewer)->put($endpoint);\n            $resp->assertOk();\n        }\n\n        $this->permissions->removeUserRolePermissions($viewer, ['comment-delete-all']);\n        $this->permissions->grantUserRolePermissions($viewer, ['comment-update-all']);\n\n        foreach ($endpoints as $endpoint) {\n            $resp = $this->actingAs($viewer)->put($endpoint);\n            $resp->assertOk();\n        }\n    }\n\n    public function test_non_top_level_comments_cant_be_archived_or_unarchived()\n    {\n        $this->asAdmin();\n        $page = $this->entities->page();\n\n        $comment = Comment::factory()->make();\n        $page->comments()->save($comment);\n        $subComment = Comment::factory()->make(['parent_id' => $comment->id]);\n        $page->comments()->save($subComment);\n        $subComment->refresh();\n\n        $resp = $this->putJson(\"/comment/$subComment->id/archive\");\n        $resp->assertStatus(400);\n\n        $this->assertDatabaseHas('comments', [\n            'id' => $subComment->id,\n            'archived' => false,\n        ]);\n\n        $resp = $this->putJson(\"/comment/$subComment->id/unarchive\");\n        $resp->assertStatus(400);\n    }\n\n    public function test_scripts_cannot_be_injected_via_comment_html()\n    {\n        $page = $this->entities->page();\n\n        $script = '<script>const a = \"script\";</script><script>const b = \"sneakyscript\";</script><p onclick=\"1\">My lovely comment</p>';\n        $this->asAdmin()->postJson(\"/comment/$page->id\", [\n            'html' => $script,\n        ]);\n\n        $pageView = $this->get($page->getUrl());\n        $pageView->assertDontSee($script, false);\n        $pageView->assertDontSee('sneakyscript', false);\n        $pageView->assertSee('<p>My lovely comment</p>', false);\n\n        $comment = $page->comments()->first();\n        $this->putJson(\"/comment/$comment->id\", [\n            'html' => $script . '<p>updated</p>',\n        ]);\n\n        $pageView = $this->get($page->getUrl());\n        $pageView->assertDontSee($script, false);\n        $pageView->assertDontSee('sneakyscript', false);\n        $pageView->assertSee('<p>My lovely comment</p><p>updated</p>');\n    }\n\n    public function test_scripts_are_removed_even_if_already_in_db()\n    {\n        $page = $this->entities->page();\n        Comment::factory()->create([\n            'html' => '<script>superbadscript</script><script>superbadscript</script><p onclick=\"superbadonclick\">scriptincommentest</p>',\n            'commentable_type' => 'page', 'commentable_id' => $page\n        ]);\n\n        $resp = $this->asAdmin()->get($page->getUrl());\n        $resp->assertSee('scriptincommentest', false);\n        $resp->assertDontSee('superbadscript', false);\n        $resp->assertDontSee('superbadonclick', false);\n    }\n\n    public function test_comment_html_is_limited()\n    {\n        $page = $this->entities->page();\n        $input = '<h1>Test</h1><p id=\"abc\" href=\"beans\">Content<a href=\"#cat\" data-a=\"b\">a</a><section>Hello</section><section>there</section></p>';\n        $expected = '<p>Content<a href=\"#cat\">a</a></p>';\n\n        $resp = $this->asAdmin()->post(\"/comment/{$page->id}\", ['html' => $input]);\n        $resp->assertOk();\n        $this->assertDatabaseHas('comments', [\n           'commentable_type' => 'page',\n           'commentable_id' => $page->id,\n           'html' => $expected,\n        ]);\n\n        $comment = $page->comments()->first();\n        $resp = $this->put(\"/comment/{$comment->id}\", ['html' => $input]);\n        $resp->assertOk();\n        $this->assertDatabaseHas('comments', [\n            'id'   => $comment->id,\n            'html' => $expected,\n        ]);\n    }\n\n    public function test_comment_html_spans_are_cleaned()\n    {\n        $page = $this->entities->page();\n        $input = '<p><span class=\"beans\">Hello</span> do you have <span style=\"white-space: discard;\">biscuits</span>?</p>';\n        $expected = '<p><span>Hello</span> do you have <span>biscuits</span>?</p>';\n\n        $resp = $this->asAdmin()->post(\"/comment/{$page->id}\", ['html' => $input]);\n        $resp->assertOk();\n        $this->assertDatabaseHas('comments', [\n            'commentable_type' => 'page',\n            'commentable_id' => $page->id,\n            'html' => $expected,\n        ]);\n\n        $comment = $page->comments()->first();\n        $resp = $this->put(\"/comment/{$comment->id}\", ['html' => $input]);\n        $resp->assertOk();\n        $this->assertDatabaseHas('comments', [\n            'id'   => $comment->id,\n            'html' => $expected,\n        ]);\n    }\n}\n"
  },
  {
    "path": "tests/Activity/CommentsApiTest.php",
    "content": "<?php\n\nnamespace Tests\\Activity;\n\nuse BookStack\\Activity\\Models\\Comment;\nuse BookStack\\Permissions\\Permission;\nuse Tests\\Api\\TestsApi;\nuse Tests\\TestCase;\n\nclass CommentsApiTest extends TestCase\n{\n    use TestsApi;\n\n    public function test_endpoint_permission_controls()\n    {\n        $user = $this->users->editor();\n        $this->permissions->grantUserRolePermissions($user, [Permission::CommentDeleteAll, Permission::CommentUpdateAll]);\n\n        $page = $this->entities->page();\n        $comment = Comment::factory()->make();\n        $page->comments()->save($comment);\n        $this->actingAsForApi($user);\n\n        $actions = [\n            ['GET', '/api/comments'],\n            ['GET', \"/api/comments/{$comment->id}\"],\n            ['POST', \"/api/comments\"],\n            ['PUT', \"/api/comments/{$comment->id}\"],\n            ['DELETE', \"/api/comments/{$comment->id}\"],\n        ];\n\n        foreach ($actions as [$method, $endpoint]) {\n            $resp = $this->call($method, $endpoint);\n            $this->assertNotPermissionError($resp);\n        }\n\n        $comment = Comment::factory()->make();\n        $page->comments()->save($comment);\n        $this->getJson(\"/api/comments\")->assertSee(['id' => $comment->id]);\n\n        $this->permissions->removeUserRolePermissions($user, [\n            Permission::CommentDeleteAll, Permission::CommentDeleteOwn,\n            Permission::CommentUpdateAll, Permission::CommentUpdateOwn,\n            Permission::CommentCreateAll\n        ]);\n\n        $this->assertPermissionError($this->json('delete', \"/api/comments/{$comment->id}\"));\n        $this->assertPermissionError($this->json('put', \"/api/comments/{$comment->id}\"));\n        $this->assertPermissionError($this->json('post', \"/api/comments\"));\n        $this->assertNotPermissionError($this->json('get', \"/api/comments/{$comment->id}\"));\n\n        $this->permissions->disableEntityInheritedPermissions($page);\n        $this->json('get', \"/api/comments/{$comment->id}\")->assertStatus(404);\n        $this->getJson(\"/api/comments\")->assertDontSee(['id' => $comment->id]);\n    }\n\n    public function test_index()\n    {\n        $page = $this->entities->page();\n        Comment::query()->delete();\n\n        $comments = Comment::factory()->count(10)->make();\n        $page->comments()->saveMany($comments);\n\n        $firstComment = $comments->first();\n        $resp = $this->actingAsApiEditor()->getJson('/api/comments');\n        $resp->assertJson([\n            'data' => [\n                [\n                    'id' => $firstComment->id,\n                    'commentable_id' => $page->id,\n                    'commentable_type' => 'page',\n                    'parent_id' => null,\n                    'local_id' => $firstComment->local_id,\n                ],\n            ],\n        ]);\n        $resp->assertJsonCount(10, 'data');\n        $resp->assertJson(['total' => 10]);\n\n        $filtered = $this->getJson(\"/api/comments?filter[id]={$firstComment->id}\");\n        $filtered->assertJsonCount(1, 'data');\n        $filtered->assertJson(['total' => 1]);\n    }\n\n    public function test_create()\n    {\n        $page = $this->entities->page();\n\n        $resp = $this->actingAsApiEditor()->postJson('/api/comments', [\n            'page_id' => $page->id,\n            'html' => '<p>My wonderful comment</p>',\n            'content_ref' => 'test-content-ref',\n        ]);\n        $resp->assertOk();\n        $id = $resp->json('id');\n\n        $this->assertDatabaseHas('comments', [\n            'id' => $id,\n            'commentable_id' => $page->id,\n            'commentable_type' => 'page',\n            'html' => '<p>My wonderful comment</p>',\n        ]);\n\n        $comment = Comment::query()->findOrFail($id);\n        $this->assertIsInt($comment->local_id);\n\n        $reply = $this->actingAsApiEditor()->postJson('/api/comments', [\n            'page_id' => $page->id,\n            'html' => '<p>My wonderful reply</p>',\n            'content_ref' => 'test-content-ref',\n            'reply_to' => $comment->local_id,\n        ]);\n        $reply->assertOk();\n\n        $this->assertDatabaseHas('comments', [\n            'id' => $reply->json('id'),\n            'commentable_id' => $page->id,\n            'commentable_type' => 'page',\n            'html' => '<p>My wonderful reply</p>',\n            'parent_id' => $comment->local_id,\n        ]);\n    }\n\n    public function test_read()\n    {\n        $page = $this->entities->page();\n        $user = $this->users->viewer();\n        $comment = Comment::factory()->make([\n            'html' => '<p>A lovely comment <script>hello</script></p>',\n            'created_by' => $user->id,\n            'updated_by' => $user->id,\n        ]);\n        $page->comments()->save($comment);\n        $comment->refresh();\n        $reply = Comment::factory()->make([\n            'parent_id' => $comment->local_id,\n            'html' => '<p>A lovely<script>angry</script>reply</p>',\n        ]);\n        $page->comments()->save($reply);\n\n        $resp = $this->actingAsApiEditor()->getJson(\"/api/comments/{$comment->id}\");\n        $resp->assertJson([\n            'id' => $comment->id,\n            'commentable_id' => $page->id,\n            'commentable_type' => 'page',\n            'html' => '<p>A lovely comment </p>',\n            'archived' => false,\n            'created_by' => [\n                'id' => $user->id,\n                'name' => $user->name,\n            ],\n            'updated_by' => [\n                'id' => $user->id,\n                'name' => $user->name,\n            ],\n            'replies' => [\n                [\n                    'id' => $reply->id,\n                    'html' => '<p>A lovelyreply</p>'\n                ]\n            ]\n        ]);\n    }\n\n    public function test_update()\n    {\n        $page = $this->entities->page();\n        $user = $this->users->editor();\n        $this->permissions->grantUserRolePermissions($user, [Permission::CommentUpdateAll]);\n        $comment = Comment::factory()->make([\n            'html' => '<p>A lovely comment</p>',\n            'created_by' => $this->users->viewer()->id,\n            'updated_by' => $this->users->viewer()->id,\n            'parent_id' => null,\n        ]);\n        $page->comments()->save($comment);\n\n        $this->actingAsForApi($user)->putJson(\"/api/comments/{$comment->id}\", [\n           'html' => '<p>A lovely updated comment</p>',\n        ])->assertOk();\n\n        $this->assertDatabaseHas('comments', [\n            'id' => $comment->id,\n            'html' => '<p>A lovely updated comment</p>',\n            'archived' => 0,\n        ]);\n\n        $this->putJson(\"/api/comments/{$comment->id}\", [\n            'archived' => true,\n        ]);\n\n        $this->assertDatabaseHas('comments', [\n            'id' => $comment->id,\n            'html' => '<p>A lovely updated comment</p>',\n            'archived' => 1,\n        ]);\n\n        $this->putJson(\"/api/comments/{$comment->id}\", [\n            'archived' => false,\n            'html' => '<p>A lovely updated again comment</p>',\n        ]);\n\n        $this->assertDatabaseHas('comments', [\n            'id' => $comment->id,\n            'html' => '<p>A lovely updated again comment</p>',\n            'archived' => 0,\n        ]);\n    }\n\n    public function test_update_cannot_archive_replies()\n    {\n        $page = $this->entities->page();\n        $user = $this->users->editor();\n        $this->permissions->grantUserRolePermissions($user, [Permission::CommentUpdateAll]);\n        $comment = Comment::factory()->make([\n            'html' => '<p>A lovely comment</p>',\n            'created_by' => $this->users->viewer()->id,\n            'updated_by' => $this->users->viewer()->id,\n            'parent_id' => 90,\n        ]);\n        $page->comments()->save($comment);\n\n        $resp = $this->actingAsForApi($user)->putJson(\"/api/comments/{$comment->id}\", [\n            'archived' => true,\n        ]);\n\n        $this->assertEquals($this->errorResponse('Only top-level comments can be archived.', 400), $resp->json());\n        $this->assertDatabaseHas('comments', [\n            'id' => $comment->id,\n            'archived' => 0,\n        ]);\n    }\n\n    public function test_destroy()\n    {\n        $page = $this->entities->page();\n        $user = $this->users->editor();\n        $this->permissions->grantUserRolePermissions($user, [Permission::CommentDeleteAll]);\n        $comment = Comment::factory()->make([\n            'html' => '<p>A lovely comment</p>',\n        ]);\n        $page->comments()->save($comment);\n\n        $this->actingAsForApi($user)->deleteJson(\"/api/comments/{$comment->id}\")->assertStatus(204);\n        $this->assertDatabaseMissing('comments', [\n            'id' => $comment->id,\n        ]);\n    }\n}\n"
  },
  {
    "path": "tests/Activity/MentionParserTest.php",
    "content": "<?php\n\nnamespace Tests\\Activity;\n\nuse BookStack\\Activity\\Tools\\MentionParser;\nuse Tests\\TestCase;\n\nclass MentionParserTest extends TestCase\n{\n    public function test_it_extracts_mentions()\n    {\n        $parser = new MentionParser();\n\n        // Test basic mention extraction\n        $html = '<p>Hello <a href=\"/user/5\" data-mention-user-id=\"5\">@User</a></p>';\n        $result = $parser->parseUserIdsFromHtml($html);\n        $this->assertEquals([5], $result);\n\n        // Test multiple mentions\n        $html = '<p><a data-mention-user-id=\"1\">@Alice</a> and <a data-mention-user-id=\"2\">@Bob</a></p>';\n        $result = $parser->parseUserIdsFromHtml($html);\n        $this->assertEquals([1, 2], $result);\n\n        // Test filtering out invalid IDs (zero and negative)\n        $html = '<p><a data-mention-user-id=\"0\">@Invalid</a> <a data-mention-user-id=\"-5\">@Negative</a> <a data-mention-user-id=\"3\">@Valid</a></p>';\n        $result = $parser->parseUserIdsFromHtml($html);\n        $this->assertEquals([3], $result);\n\n        // Test non-mention links are ignored\n        $html = '<p><a href=\"/page/1\">Normal Link</a> <a data-mention-user-id=\"7\">@User</a></p>';\n        $result = $parser->parseUserIdsFromHtml($html);\n        $this->assertEquals([7], $result);\n\n        // Test empty HTML\n        $result = $parser->parseUserIdsFromHtml('');\n        $this->assertEquals([], $result);\n\n        // Test duplicate user IDs\n        $html = '<p><a data-mention-user-id=\"4\">@User</a> mentioned <a data-mention-user-id=\"4\">@User</a> again</p>';\n        $result = $parser->parseUserIdsFromHtml($html);\n        $this->assertEquals([4], $result);\n    }\n}\n"
  },
  {
    "path": "tests/Activity/WatchTest.php",
    "content": "<?php\n\nnamespace Tests\\Activity;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Activity\\Models\\Comment;\nuse BookStack\\Activity\\Notifications\\Messages\\BaseActivityNotification;\nuse BookStack\\Activity\\Notifications\\Messages\\CommentCreationNotification;\nuse BookStack\\Activity\\Notifications\\Messages\\PageCreationNotification;\nuse BookStack\\Activity\\Notifications\\Messages\\PageUpdateNotification;\nuse BookStack\\Activity\\Tools\\ActivityLogger;\nuse BookStack\\Activity\\Tools\\UserEntityWatchOptions;\nuse BookStack\\Activity\\WatchLevels;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Settings\\UserNotificationPreferences;\nuse Illuminate\\Contracts\\Notifications\\Dispatcher;\nuse Illuminate\\Support\\Facades\\Mail;\nuse Illuminate\\Support\\Facades\\Notification;\nuse Tests\\TestCase;\n\nclass WatchTest extends TestCase\n{\n    public function test_watch_action_exists_on_entity_unless_active()\n    {\n        $editor = $this->users->editor();\n        $this->actingAs($editor);\n\n        $entities = [$this->entities->book(), $this->entities->chapter(), $this->entities->page()];\n        /** @var Entity $entity */\n        foreach ($entities as $entity) {\n            $resp = $this->get($entity->getUrl());\n            $this->withHtml($resp)->assertElementContains('form[action$=\"/watching/update\"] button.icon-list-item', 'Watch');\n\n            $watchOptions = new UserEntityWatchOptions($editor, $entity);\n            $watchOptions->updateLevelByValue(WatchLevels::COMMENTS);\n\n            $resp = $this->get($entity->getUrl());\n            $this->withHtml($resp)->assertElementNotExists('form[action$=\"/watching/update\"] button.icon-list-item');\n        }\n    }\n\n    public function test_watch_action_only_shows_with_permission()\n    {\n        $viewer = $this->users->viewer();\n        $this->actingAs($viewer);\n\n        $entities = [$this->entities->book(), $this->entities->chapter(), $this->entities->page()];\n        /** @var Entity $entity */\n        foreach ($entities as $entity) {\n            $resp = $this->get($entity->getUrl());\n            $this->withHtml($resp)->assertElementNotExists('form[action$=\"/watching/update\"] button.icon-list-item');\n        }\n\n        $this->permissions->grantUserRolePermissions($viewer, ['receive-notifications']);\n\n        /** @var Entity $entity */\n        foreach ($entities as $entity) {\n            $resp = $this->get($entity->getUrl());\n            $this->withHtml($resp)->assertElementExists('form[action$=\"/watching/update\"] button.icon-list-item');\n        }\n    }\n\n    public function test_watch_update()\n    {\n        $editor = $this->users->editor();\n        $book = $this->entities->book();\n\n        $resp = $this->actingAs($editor)->put('/watching/update', [\n            'type' => $book->getMorphClass(),\n            'id' => $book->id,\n            'level' => 'comments'\n        ]);\n\n        $resp->assertRedirect($book->getUrl());\n        $this->assertSessionHas('success');\n        $this->assertDatabaseHas('watches', [\n            'watchable_id' => $book->id,\n            'watchable_type' => $book->getMorphClass(),\n            'user_id' => $editor->id,\n            'level' => WatchLevels::COMMENTS,\n        ]);\n\n        $resp = $this->put('/watching/update', [\n            'type' => $book->getMorphClass(),\n            'id' => $book->id,\n            'level' => 'default'\n        ]);\n        $resp->assertRedirect($book->getUrl());\n        $this->assertDatabaseMissing('watches', [\n            'watchable_id' => $book->id,\n            'watchable_type' => $book->getMorphClass(),\n            'user_id' => $editor->id,\n        ]);\n    }\n\n    public function test_watch_update_fails_for_guest()\n    {\n        $this->setSettings(['app-public' => 'true']);\n        $guest = $this->users->guest();\n        $this->permissions->grantUserRolePermissions($guest, ['receive-notifications']);\n        $book = $this->entities->book();\n\n        $resp = $this->put('/watching/update', [\n            'type' => $book->getMorphClass(),\n            'id' => $book->id,\n            'level' => 'comments'\n        ]);\n\n        $this->assertPermissionError($resp);\n        $guest->unsetRelations();\n    }\n\n    public function test_watch_detail_display_reflects_state()\n    {\n        $editor = $this->users->editor();\n        $book = $this->entities->bookHasChaptersAndPages();\n        $chapter = $book->chapters()->first();\n        $page = $chapter->pages()->first();\n\n        (new UserEntityWatchOptions($editor, $book))->updateLevelByValue(WatchLevels::UPDATES);\n\n        $this->actingAs($editor)->get($book->getUrl())->assertSee('Watching new pages and updates');\n        $this->get($chapter->getUrl())->assertSee('Watching via parent book');\n        $this->get($page->getUrl())->assertSee('Watching via parent book');\n\n        (new UserEntityWatchOptions($editor, $chapter))->updateLevelByValue(WatchLevels::COMMENTS);\n        $this->get($chapter->getUrl())->assertSee('Watching new pages, updates & comments');\n        $this->get($page->getUrl())->assertSee('Watching via parent chapter');\n\n        (new UserEntityWatchOptions($editor, $page))->updateLevelByValue(WatchLevels::UPDATES);\n        $this->get($page->getUrl())->assertSee('Watching new pages and updates');\n    }\n\n    public function test_watch_detail_ignore_indicator_cascades()\n    {\n        $editor = $this->users->editor();\n        $book = $this->entities->bookHasChaptersAndPages();\n        (new UserEntityWatchOptions($editor, $book))->updateLevelByValue(WatchLevels::IGNORE);\n\n        $this->actingAs($editor)->get($book->getUrl())->assertSee('Ignoring notifications');\n        $this->get($book->chapters()->first()->getUrl())->assertSee('Ignoring via parent book');\n        $this->get($book->pages()->first()->getUrl())->assertSee('Ignoring via parent book');\n    }\n\n    public function test_watch_option_menu_shows_current_active_state()\n    {\n        $editor = $this->users->editor();\n        $book = $this->entities->book();\n        $options = new UserEntityWatchOptions($editor, $book);\n\n        $respHtml = $this->withHtml($this->actingAs($editor)->get($book->getUrl()));\n        $respHtml->assertElementNotExists('form[action$=\"/watching/update\"] svg[data-icon=\"check-circle\"]');\n\n        $options->updateLevelByValue(WatchLevels::COMMENTS);\n        $respHtml = $this->withHtml($this->actingAs($editor)->get($book->getUrl()));\n        $respHtml->assertElementExists('form[action$=\"/watching/update\"] button[value=\"comments\"] svg[data-icon=\"check-circle\"]');\n\n        $options->updateLevelByValue(WatchLevels::IGNORE);\n        $respHtml = $this->withHtml($this->actingAs($editor)->get($book->getUrl()));\n        $respHtml->assertElementExists('form[action$=\"/watching/update\"] button[value=\"ignore\"] svg[data-icon=\"check-circle\"]');\n    }\n\n    public function test_watch_option_menu_limits_options_for_pages()\n    {\n        $editor = $this->users->editor();\n        $book = $this->entities->bookHasChaptersAndPages();\n        (new UserEntityWatchOptions($editor, $book))->updateLevelByValue(WatchLevels::IGNORE);\n\n        $respHtml = $this->withHtml($this->actingAs($editor)->get($book->getUrl()));\n        $respHtml->assertElementExists('form[action$=\"/watching/update\"] button[name=\"level\"][value=\"new\"]');\n\n        $respHtml = $this->withHtml($this->get($book->pages()->first()->getUrl()));\n        $respHtml->assertElementExists('form[action$=\"/watching/update\"] button[name=\"level\"][value=\"updates\"]');\n        $respHtml->assertElementNotExists('form[action$=\"/watching/update\"] button[name=\"level\"][value=\"new\"]');\n    }\n\n    public function test_notify_own_page_changes()\n    {\n        $editor = $this->users->editor();\n        $entities = $this->entities->createChainBelongingToUser($editor);\n        $prefs = new UserNotificationPreferences($editor);\n        $prefs->updateFromSettingsArray(['own-page-changes' => 'true']);\n\n        $notifications = Notification::fake();\n\n        $this->asAdmin();\n        $this->entities->updatePage($entities['page'], ['name' => 'My updated page', 'html' => 'Hello']);\n        $notifications->assertSentTo($editor, PageUpdateNotification::class);\n    }\n\n    public function test_notify_own_page_comments()\n    {\n        $editor = $this->users->editor();\n        $entities = $this->entities->createChainBelongingToUser($editor);\n        $prefs = new UserNotificationPreferences($editor);\n        $prefs->updateFromSettingsArray(['own-page-comments' => 'true']);\n\n        $notifications = Notification::fake();\n\n        $this->asAdmin()->post(\"/comment/{$entities['page']->id}\", [\n            'html' => '<p>My new comment</p>'\n        ]);\n        $notifications->assertSentTo($editor, CommentCreationNotification::class);\n    }\n\n    public function test_notify_comment_replies()\n    {\n        $editor = $this->users->editor();\n        $entities = $this->entities->createChainBelongingToUser($editor);\n        $prefs = new UserNotificationPreferences($editor);\n        $prefs->updateFromSettingsArray(['comment-replies' => 'true']);\n\n        // Create some existing comments to pad IDs to help potentially error\n        // on mis-identification of parent via ids used.\n        Comment::factory()->count(5)\n            ->for($entities['page'], 'entity')\n            ->create(['created_by' => $this->users->admin()->id]);\n\n        $notifications = Notification::fake();\n\n        $this->actingAs($editor)->post(\"/comment/{$entities['page']->id}\", [\n            'html' => '<p>My new comment</p>'\n        ]);\n        $comment = $entities['page']->comments()->orderBy('id', 'desc')->first();\n\n        $this->asAdmin()->post(\"/comment/{$entities['page']->id}\", [\n            'html' => '<p>My new comment response</p>',\n            'parent_id' => $comment->local_id,\n        ]);\n        $notifications->assertSentTo($editor, CommentCreationNotification::class);\n    }\n\n    public function test_notify_watch_parent_book_ignore()\n    {\n        $editor = $this->users->editor();\n        $entities = $this->entities->createChainBelongingToUser($editor);\n        $watches = new UserEntityWatchOptions($editor, $entities['book']);\n        $prefs = new UserNotificationPreferences($editor);\n        $watches->updateLevelByValue(WatchLevels::IGNORE);\n        $prefs->updateFromSettingsArray(['own-page-changes' => 'true', 'own-page-comments' => true]);\n\n        $notifications = Notification::fake();\n\n        $this->asAdmin()->post(\"/comment/{$entities['page']->id}\", [\n            'text' => 'My new comment response',\n        ]);\n        $this->entities->updatePage($entities['page'], ['name' => 'My updated page', 'html' => 'Hello']);\n        $notifications->assertNothingSent();\n    }\n\n    public function test_notify_watch_parent_book_comments()\n    {\n        $notifications = Notification::fake();\n        $editor = $this->users->editor();\n        $admin = $this->users->admin();\n        $entities = $this->entities->createChainBelongingToUser($editor);\n        $watches = new UserEntityWatchOptions($editor, $entities['book']);\n        $watches->updateLevelByValue(WatchLevels::COMMENTS);\n\n        // Comment post\n        $this->actingAs($admin)->post(\"/comment/{$entities['page']->id}\", [\n            'html' => '<p>My new comment response</p>',\n        ]);\n\n        $notifications->assertSentTo($editor, function (CommentCreationNotification $notification) use ($editor, $admin, $entities) {\n            $mail = $notification->toMail($editor);\n            $mailContent = html_entity_decode(strip_tags($mail->render()), ENT_QUOTES);\n            return $mail->subject === 'New comment on page: ' . $entities['page']->getShortName()\n                && str_contains($mailContent, 'View Comment')\n                && str_contains($mailContent, 'Page Name: ' . $entities['page']->name)\n                && str_contains($mailContent, 'Page Path: ' . $entities['book']->getShortName(24) . ' > ' . $entities['chapter']->getShortName(24))\n                && str_contains($mailContent, 'Commenter: ' . $admin->name)\n                && str_contains($mailContent, 'Comment: My new comment response');\n        });\n    }\n\n    public function test_notify_watch_parent_book_updates()\n    {\n        $notifications = Notification::fake();\n        $editor = $this->users->editor();\n        $admin = $this->users->admin();\n        $entities = $this->entities->createChainBelongingToUser($editor);\n        $watches = new UserEntityWatchOptions($editor, $entities['book']);\n        $watches->updateLevelByValue(WatchLevels::UPDATES);\n\n        $this->actingAs($admin);\n        $this->entities->updatePage($entities['page'], ['name' => 'Updated page', 'html' => 'new page content']);\n\n        $notifications->assertSentTo($editor, function (PageUpdateNotification $notification) use ($editor, $admin, $entities) {\n            $mail = $notification->toMail($editor);\n            $mailContent = html_entity_decode(strip_tags($mail->render()), ENT_QUOTES);\n            return $mail->subject === 'Updated page: Updated page'\n                && str_contains($mailContent, 'View Page')\n                && str_contains($mailContent, 'Page Name: Updated page')\n                && str_contains($mailContent, 'Page Path: ' . $entities['book']->getShortName(24) . ' > ' . $entities['chapter']->getShortName(24))\n                && str_contains($mailContent, 'Updated By: ' . $admin->name)\n                && str_contains($mailContent, 'you won\\'t be sent notifications for further edits to this page by the same editor');\n        });\n\n        // Test debounce\n        $notifications = Notification::fake();\n        $this->entities->updatePage($entities['page'], ['name' => 'Updated page', 'html' => 'new page content']);\n        $notifications->assertNothingSentTo($editor);\n    }\n\n    public function test_notify_watch_parent_book_new()\n    {\n        $notifications = Notification::fake();\n        $editor = $this->users->editor();\n        $admin = $this->users->admin();\n        $entities = $this->entities->createChainBelongingToUser($editor);\n        $watches = new UserEntityWatchOptions($editor, $entities['book']);\n        $watches->updateLevelByValue(WatchLevels::NEW);\n\n        $this->actingAs($admin)->get($entities['chapter']->getUrl('/create-page'));\n        $page = $entities['chapter']->pages()->where('draft', '=', true)->first();\n        $this->post($page->getUrl(), ['name' => 'My new page', 'html' => 'My new page content']);\n\n        $notifications->assertSentTo($editor, function (PageCreationNotification $notification) use ($editor, $admin, $entities) {\n            $mail = $notification->toMail($editor);\n            $mailContent = html_entity_decode(strip_tags($mail->render()), ENT_QUOTES);\n            return $mail->subject === 'New page: My new page'\n                && str_contains($mailContent, 'View Page')\n                && str_contains($mailContent, 'Page Name: My new page')\n                && str_contains($mailContent, 'Page Path: ' . $entities['book']->getShortName(24) . ' > ' . $entities['chapter']->getShortName(24))\n                && str_contains($mailContent, 'Created By: ' . $admin->name);\n        });\n    }\n\n    public function test_notify_watch_page_ignore_when_no_page_owner()\n    {\n        $editor = $this->users->editor();\n        $entities = $this->entities->createChainBelongingToUser($editor);\n        $entities['page']->owned_by = null;\n        $entities['page']->save();\n\n        $watches = new UserEntityWatchOptions($editor, $entities['page']);\n        $watches->updateLevelByValue(WatchLevels::IGNORE);\n\n        $notifications = Notification::fake();\n        $this->asAdmin();\n\n        $this->entities->updatePage($entities['page'], ['name' => 'My updated page', 'html' => 'Hello']);\n\n        $notifications->assertNothingSent();\n    }\n\n    public function test_notifications_sent_in_right_language()\n    {\n        $editor = $this->users->editor();\n        $admin = $this->users->admin();\n        setting()->putUser($editor, 'language', 'de');\n        $entities = $this->entities->createChainBelongingToUser($editor);\n        $watches = new UserEntityWatchOptions($editor, $entities['book']);\n        $watches->updateLevelByValue(WatchLevels::COMMENTS);\n\n        $activities = [\n            ActivityType::PAGE_CREATE => $entities['page'],\n            ActivityType::PAGE_UPDATE => $entities['page'],\n            ActivityType::COMMENT_CREATE => Comment::factory()->make([\n                'commentable_id' => $entities['page']->id,\n                'commentable_type' => $entities['page']->getMorphClass(),\n            ]),\n        ];\n\n        $notifications = Notification::fake();\n        $logger = app()->make(ActivityLogger::class);\n        $this->actingAs($admin);\n\n        foreach ($activities as $activityType => $detail) {\n            $logger->add($activityType, $detail);\n        }\n\n        $sent = $notifications->sentNotifications()[get_class($editor)][$editor->id];\n        $this->assertCount(3, $sent);\n\n        foreach ($sent as $notificationInfo) {\n            $notification = $notificationInfo[0]['notification'];\n            $this->assertInstanceOf(BaseActivityNotification::class, $notification);\n            $mail = $notification->toMail($editor);\n            $mailContent = html_entity_decode(strip_tags($mail->render()), ENT_QUOTES);\n            $this->assertStringContainsString('Name der Seite:', $mailContent);\n            $this->assertStringContainsString('Diese Benachrichtigung wurde', $mailContent);\n            $this->assertStringContainsString('Sollte es beim Anklicken der Schaltfläche', $mailContent);\n        }\n    }\n\n    public function test_failed_notifications_dont_block_and_log_errors()\n    {\n        $logger = $this->withTestLogger();\n        $editor = $this->users->editor();\n        $admin = $this->users->admin();\n        $page = $this->entities->page();\n        $book = $page->book;\n        $activityLogger = app()->make(ActivityLogger::class);\n\n        $watches = new UserEntityWatchOptions($editor, $book);\n        $watches->updateLevelByValue(WatchLevels::UPDATES);\n\n        $mockDispatcher = $this->mock(Dispatcher::class);\n        $mockDispatcher->shouldReceive('send')->once()\n            ->andThrow(\\Exception::class, 'Failed to connect to mail server');\n\n        $this->actingAs($admin);\n\n        $activityLogger->add(ActivityType::PAGE_UPDATE, $page);\n\n        $this->assertTrue($logger->hasErrorThatContains(\"Failed to send email notification to user [id:{$editor->id}] with error: Failed to connect to mail server\"));\n    }\n\n    public function test_notifications_not_sent_if_lacking_view_permission_for_related_item()\n    {\n        $notifications = Notification::fake();\n        $editor = $this->users->editor();\n        $page = $this->entities->page();\n\n        $watches = new UserEntityWatchOptions($editor, $page);\n        $watches->updateLevelByValue(WatchLevels::COMMENTS);\n        $this->permissions->disableEntityInheritedPermissions($page);\n\n        $this->asAdmin()->post(\"/comment/{$page->id}\", [\n            'html' => '<p>My new comment response</p>',\n        ])->assertOk();\n\n        $notifications->assertNothingSentTo($editor);\n    }\n\n    public function test_watches_deleted_on_user_delete()\n    {\n        $editor = $this->users->editor();\n        $page = $this->entities->page();\n\n        $watches = new UserEntityWatchOptions($editor, $page);\n        $watches->updateLevelByValue(WatchLevels::COMMENTS);\n        $this->assertDatabaseHas('watches', ['user_id' => $editor->id]);\n\n        $this->asAdmin()->delete($editor->getEditUrl());\n\n        $this->assertDatabaseMissing('watches', ['user_id' => $editor->id]);\n    }\n\n    public function test_watches_deleted_on_item_delete()\n    {\n        $editor = $this->users->editor();\n        $page = $this->entities->page();\n\n        $watches = new UserEntityWatchOptions($editor, $page);\n        $watches->updateLevelByValue(WatchLevels::COMMENTS);\n        $this->assertDatabaseHas('watches', ['watchable_type' => 'page', 'watchable_id' => $page->id]);\n\n        $this->entities->destroy($page);\n\n        $this->assertDatabaseMissing('watches', ['watchable_type' => 'page', 'watchable_id' => $page->id]);\n    }\n\n    public function test_page_path_in_notifications_limited_by_permissions()\n    {\n        $chapter = $this->entities->chapterHasPages();\n        $page = $chapter->pages()->first();\n        $book = $chapter->book;\n        $notification = new PageCreationNotification($page, $this->users->editor());\n\n        $viewer = $this->users->viewer();\n        $viewerRole = $viewer->roles()->first();\n\n        $content = html_entity_decode(strip_tags($notification->toMail($viewer)->render()), ENT_QUOTES);\n        $this->assertStringContainsString('Page Path: ' . $book->getShortName(24) . ' > ' . $chapter->getShortName(24), $content);\n\n        $this->permissions->setEntityPermissions($page, ['view'], [$viewerRole]);\n        $this->permissions->setEntityPermissions($chapter, [], [$viewerRole]);\n\n        $content = html_entity_decode(strip_tags($notification->toMail($viewer)->render()), ENT_QUOTES);\n        $this->assertStringContainsString('Page Path: ' . $book->getShortName(24), $content);\n        $this->assertStringNotContainsString(' > ' . $chapter->getShortName(24), $content);\n\n        $this->permissions->setEntityPermissions($book, [], [$viewerRole]);\n\n        $content = html_entity_decode(strip_tags($notification->toMail($viewer)->render()), ENT_QUOTES);\n        $this->assertStringNotContainsString('Page Path:', $content);\n        $this->assertStringNotContainsString($book->getShortName(24), $content);\n        $this->assertStringNotContainsString($chapter->getShortName(24), $content);\n    }\n}\n"
  },
  {
    "path": "tests/Activity/WebhookCallTest.php",
    "content": "<?php\n\nnamespace Tests\\Activity;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Activity\\DispatchWebhookJob;\nuse BookStack\\Activity\\Models\\Webhook;\nuse BookStack\\Activity\\Tools\\ActivityLogger;\nuse BookStack\\Api\\ApiToken;\nuse BookStack\\Users\\Models\\User;\nuse GuzzleHttp\\Exception\\ConnectException;\nuse GuzzleHttp\\Psr7\\Response;\nuse Illuminate\\Support\\Facades\\Bus;\nuse Tests\\TestCase;\n\nclass WebhookCallTest extends TestCase\n{\n    public function test_webhook_listening_to_all_called_on_event()\n    {\n        $this->newWebhook([], ['all']);\n        Bus::fake();\n        $this->runEvent(ActivityType::ROLE_CREATE);\n        Bus::assertDispatched(DispatchWebhookJob::class);\n    }\n\n    public function test_webhook_listening_to_specific_event_called_on_event()\n    {\n        $this->newWebhook([], [ActivityType::ROLE_UPDATE]);\n        Bus::fake();\n        $this->runEvent(ActivityType::ROLE_UPDATE);\n        Bus::assertDispatched(DispatchWebhookJob::class);\n    }\n\n    public function test_webhook_listening_to_specific_event_not_called_on_other_event()\n    {\n        $this->newWebhook([], [ActivityType::ROLE_UPDATE]);\n        Bus::fake();\n        $this->runEvent(ActivityType::ROLE_CREATE);\n        Bus::assertNotDispatched(DispatchWebhookJob::class);\n    }\n\n    public function test_inactive_webhook_not_called_on_event()\n    {\n        $this->newWebhook(['active' => false], ['all']);\n        Bus::fake();\n        $this->runEvent(ActivityType::ROLE_CREATE);\n        Bus::assertNotDispatched(DispatchWebhookJob::class);\n    }\n\n    public function test_webhook_runs_for_delete_actions()\n    {\n        // This test must not fake the queue/bus since this covers an issue\n        // around handling and serialization of items now deleted from the database.\n        $webhook = $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']);\n        $this->mockHttpClient([new Response(500)]);\n\n        $user = $this->users->newUser();\n        $resp = $this->asAdmin()->delete($user->getEditUrl());\n        $resp->assertRedirect('/settings/users');\n\n        /** @var ApiToken $apiToken */\n        $editor = $this->users->editor();\n        $apiToken = ApiToken::factory()->create(['user_id' => $editor]);\n        $this->delete($apiToken->getUrl())->assertRedirect();\n\n        $webhook->refresh();\n        $this->assertEquals('Response status from endpoint was 500', $webhook->last_error);\n    }\n\n    public function test_failed_webhook_call_logs_error()\n    {\n        $logger = $this->withTestLogger();\n        $this->mockHttpClient([new Response(500)]);\n        $webhook = $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']);\n        $this->assertNull($webhook->last_errored_at);\n\n        $this->runEvent(ActivityType::ROLE_CREATE);\n\n        $this->assertTrue($logger->hasError('Webhook call to endpoint https://wh.example.com failed with status 500'));\n\n        $webhook->refresh();\n        $this->assertEquals('Response status from endpoint was 500', $webhook->last_error);\n        $this->assertNotNull($webhook->last_errored_at);\n    }\n\n    public function test_webhook_call_exception_is_caught_and_logged()\n    {\n        $this->mockHttpClient([new ConnectException('Failed to perform request', new \\GuzzleHttp\\Psr7\\Request('GET', ''))]);\n\n        $logger = $this->withTestLogger();\n        $webhook = $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']);\n        $this->assertNull($webhook->last_errored_at);\n\n        $this->runEvent(ActivityType::ROLE_CREATE);\n\n        $this->assertTrue($logger->hasError('Webhook call to endpoint https://wh.example.com failed with error \"Failed to perform request\"'));\n\n        $webhook->refresh();\n        $this->assertEquals('Failed to perform request', $webhook->last_error);\n        $this->assertNotNull($webhook->last_errored_at);\n    }\n\n    public function test_webhook_uses_ssr_hosts_option_if_set()\n    {\n        config()->set('app.ssr_hosts', 'https://*.example.com');\n        $responses = $this->mockHttpClient();\n\n        $webhook = $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.co.uk'], ['all']);\n        $this->runEvent(ActivityType::ROLE_CREATE);\n        $this->assertEquals(0, $responses->requestCount());\n\n        $webhook->refresh();\n        $this->assertEquals('The URL does not match the configured allowed SSR hosts', $webhook->last_error);\n        $this->assertNotNull($webhook->last_errored_at);\n    }\n\n    public function test_webhook_call_data_format()\n    {\n        $responses = $this->mockHttpClient([new Response(200, [], '')]);\n        $webhook = $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']);\n        $page = $this->entities->page();\n        $editor = $this->users->editor();\n\n        $this->runEvent(ActivityType::PAGE_UPDATE, $page, $editor);\n\n        $request = $responses->latestRequest();\n        $reqData = json_decode($request->getBody(), true);\n        $this->assertEquals('page_update', $reqData['event']);\n        $this->assertEquals(($editor->name . ' updated page \"' . $page->name . '\"'), $reqData['text']);\n        $this->assertIsString($reqData['triggered_at']);\n        $this->assertEquals($editor->name, $reqData['triggered_by']['name']);\n        $this->assertEquals($editor->getProfileUrl(), $reqData['triggered_by_profile_url']);\n        $this->assertEquals($webhook->id, $reqData['webhook_id']);\n        $this->assertEquals($webhook->name, $reqData['webhook_name']);\n        $this->assertEquals($page->getUrl(), $reqData['url']);\n        $this->assertEquals($page->name, $reqData['related_item']['name']);\n    }\n\n    protected function runEvent(string $event, $detail = '', ?User $user = null)\n    {\n        if (is_null($user)) {\n            $user = $this->users->editor();\n        }\n\n        $this->actingAs($user);\n\n        $activityLogger = $this->app->make(ActivityLogger::class);\n        $activityLogger->add($event, $detail);\n    }\n\n    protected function newWebhook(array $attrs, array $events): Webhook\n    {\n        /** @var Webhook $webhook */\n        $webhook = Webhook::factory()->create($attrs);\n\n        foreach ($events as $event) {\n            $webhook->trackedEvents()->create(['event' => $event]);\n        }\n\n        return $webhook;\n    }\n}\n"
  },
  {
    "path": "tests/Activity/WebhookFormatTesting.php",
    "content": "<?php\n\nnamespace Tests\\Activity;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Activity\\Models\\Webhook;\nuse BookStack\\Activity\\Tools\\WebhookFormatter;\nuse Illuminate\\Support\\Arr;\nuse Tests\\TestCase;\n\nclass WebhookFormatTesting extends TestCase\n{\n    public function test_entity_events_show_related_user_info()\n    {\n        $events = [\n            ActivityType::BOOK_UPDATE    => $this->entities->book(),\n            ActivityType::CHAPTER_CREATE => $this->entities->chapter(),\n            ActivityType::PAGE_MOVE      => $this->entities->page(),\n        ];\n\n        foreach ($events as $event => $entity) {\n            $data = $this->getWebhookData($event, $entity);\n\n            $this->assertEquals($entity->createdBy->name, Arr::get($data, 'related_item.created_by.name'));\n            $this->assertEquals($entity->updatedBy->id, Arr::get($data, 'related_item.updated_by.id'));\n            $this->assertEquals($entity->ownedBy->slug, Arr::get($data, 'related_item.owned_by.slug'));\n        }\n    }\n\n    public function test_page_create_and_update_events_show_revision_info()\n    {\n        $page = $this->entities->page();\n        $this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']);\n\n        $data = $this->getWebhookData(ActivityType::PAGE_UPDATE, $page);\n        $this->assertEquals($page->currentRevision->id, Arr::get($data, 'related_item.current_revision.id'));\n        $this->assertEquals($page->currentRevision->type, Arr::get($data, 'related_item.current_revision.type'));\n        $this->assertEquals('Update a', Arr::get($data, 'related_item.current_revision.summary'));\n    }\n\n    protected function getWebhookData(string $event, $detail): array\n    {\n        $webhook = Webhook::factory()->make();\n        $user = $this->users->editor();\n        $formatter = WebhookFormatter::getDefault($event, $webhook, $detail, $user, time());\n\n        return $formatter->format();\n    }\n}\n"
  },
  {
    "path": "tests/Activity/WebhookManagementTest.php",
    "content": "<?php\n\nnamespace Tests\\Activity;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Activity\\Models\\Webhook;\nuse Tests\\TestCase;\n\nclass WebhookManagementTest extends TestCase\n{\n    public function test_index_view()\n    {\n        $webhook = $this->newWebhook([\n            'name'     => 'My awesome webhook',\n            'endpoint' => 'https://example.com/donkey/webhook',\n        ], ['all']);\n\n        $resp = $this->asAdmin()->get('/settings/webhooks');\n        $resp->assertOk();\n        $this->withHtml($resp)->assertElementContains('a[href$=\"/settings/webhooks/create\"]', 'Create New Webhook');\n        $this->withHtml($resp)->assertElementContains('a[href=\"' . $webhook->getUrl() . '\"]', $webhook->name);\n        $resp->assertSee($webhook->endpoint);\n        $resp->assertSee('All system events');\n        $resp->assertSee('Active');\n    }\n\n    public function test_create_view()\n    {\n        $resp = $this->asAdmin()->get('/settings/webhooks/create');\n        $resp->assertOk();\n        $resp->assertSee('Create New Webhook');\n        $this->withHtml($resp)->assertElementContains('form[action$=\"/settings/webhooks/create\"] button', 'Save Webhook');\n    }\n\n    public function test_store()\n    {\n        $resp = $this->asAdmin()->post('/settings/webhooks/create', [\n            'name'     => 'My first webhook',\n            'endpoint' => 'https://example.com/webhook',\n            'events'   => ['all'],\n            'active'   => 'true',\n            'timeout'  => 4,\n        ]);\n\n        $resp->assertRedirect('/settings/webhooks');\n        $this->assertActivityExists(ActivityType::WEBHOOK_CREATE);\n\n        $resp = $this->followRedirects($resp);\n        $resp->assertSee('Webhook successfully created');\n\n        $this->assertDatabaseHas('webhooks', [\n            'name'     => 'My first webhook',\n            'endpoint' => 'https://example.com/webhook',\n            'active'   => true,\n            'timeout'  => 4,\n        ]);\n\n        /** @var Webhook $webhook */\n        $webhook = Webhook::query()->where('name', '=', 'My first webhook')->first();\n        $this->assertDatabaseHas('webhook_tracked_events', [\n            'webhook_id' => $webhook->id,\n            'event'      => 'all',\n        ]);\n    }\n\n    public function test_edit_view()\n    {\n        $webhook = $this->newWebhook();\n\n        $resp = $this->asAdmin()->get('/settings/webhooks/' . $webhook->id);\n        $resp->assertOk();\n        $resp->assertSee('Edit Webhook');\n        $this->withHtml($resp)->assertElementContains('form[action=\"' . $webhook->getUrl() . '\"] button', 'Save Webhook');\n        $this->withHtml($resp)->assertElementContains('a[href=\"' . $webhook->getUrl('/delete') . '\"]', 'Delete Webhook');\n        $this->withHtml($resp)->assertElementExists('input[type=\"checkbox\"][value=\"all\"][name=\"events[]\"]');\n    }\n\n    public function test_update()\n    {\n        $webhook = $this->newWebhook();\n\n        $resp = $this->asAdmin()->put('/settings/webhooks/' . $webhook->id, [\n            'name'     => 'My updated webhook',\n            'endpoint' => 'https://example.com/updated-webhook',\n            'events'   => [ActivityType::PAGE_CREATE, ActivityType::PAGE_UPDATE],\n            'active'   => 'true',\n            'timeout'  => 5,\n        ]);\n        $resp->assertRedirect('/settings/webhooks');\n\n        $resp = $this->followRedirects($resp);\n        $resp->assertSee('Webhook successfully updated');\n\n        $this->assertDatabaseHas('webhooks', [\n            'id'       => $webhook->id,\n            'name'     => 'My updated webhook',\n            'endpoint' => 'https://example.com/updated-webhook',\n            'active'   => true,\n            'timeout'  => 5,\n        ]);\n\n        $trackedEvents = $webhook->trackedEvents()->get();\n        $this->assertCount(2, $trackedEvents);\n        $this->assertEquals(['page_create', 'page_update'], $trackedEvents->pluck('event')->values()->all());\n\n        $this->assertActivityExists(ActivityType::WEBHOOK_UPDATE);\n    }\n\n    public function test_delete_view()\n    {\n        $webhook = $this->newWebhook(['name' => 'Webhook to delete']);\n\n        $resp = $this->asAdmin()->get('/settings/webhooks/' . $webhook->id . '/delete');\n        $resp->assertOk();\n        $resp->assertSee('Delete Webhook');\n        $resp->assertSee('This will fully delete this webhook, with the name \\'Webhook to delete\\', from the system.');\n        $this->withHtml($resp)->assertElementContains('form[action$=\"/settings/webhooks/' . $webhook->id . '\"]', 'Delete');\n    }\n\n    public function test_destroy()\n    {\n        $webhook = $this->newWebhook();\n\n        $resp = $this->asAdmin()->delete('/settings/webhooks/' . $webhook->id);\n        $resp->assertRedirect('/settings/webhooks');\n\n        $resp = $this->followRedirects($resp);\n        $resp->assertSee('Webhook successfully deleted');\n\n        $this->assertDatabaseMissing('webhooks', ['id' => $webhook->id]);\n        $this->assertDatabaseMissing('webhook_tracked_events', ['webhook_id' => $webhook->id]);\n\n        $this->assertActivityExists(ActivityType::WEBHOOK_DELETE);\n    }\n\n    public function test_settings_manage_permission_required_for_webhook_routes()\n    {\n        $editor = $this->users->editor();\n        $this->actingAs($editor);\n\n        $routes = [\n            ['GET', '/settings/webhooks'],\n            ['GET', '/settings/webhooks/create'],\n            ['POST', '/settings/webhooks/create'],\n            ['GET', '/settings/webhooks/1'],\n            ['PUT', '/settings/webhooks/1'],\n            ['DELETE', '/settings/webhooks/1'],\n            ['GET', '/settings/webhooks/1/delete'],\n        ];\n\n        foreach ($routes as [$method, $endpoint]) {\n            $resp = $this->call($method, $endpoint);\n            $this->assertPermissionError($resp);\n        }\n\n        $this->permissions->grantUserRolePermissions($editor, ['settings-manage']);\n\n        foreach ($routes as [$method, $endpoint]) {\n            $resp = $this->call($method, $endpoint);\n            $this->assertNotPermissionError($resp);\n        }\n    }\n\n    protected function newWebhook(array $attrs = [], array $events = ['all']): Webhook\n    {\n        /** @var Webhook $webhook */\n        $webhook = Webhook::factory()->create($attrs);\n\n        foreach ($events as $event) {\n            $webhook->trackedEvents()->create(['event' => $event]);\n        }\n\n        return $webhook;\n    }\n}\n"
  },
  {
    "path": "tests/Api/ApiAuthTest.php",
    "content": "<?php\n\nnamespace Tests\\Api;\n\nuse BookStack\\Permissions\\Models\\RolePermission;\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Models\\User;\nuse Carbon\\Carbon;\nuse Tests\\TestCase;\n\nclass ApiAuthTest extends TestCase\n{\n    use TestsApi;\n\n    protected string $endpoint = '/api/books';\n\n    public function test_requests_succeed_with_default_auth()\n    {\n        $viewer = $this->users->viewer();\n        $this->permissions->grantUserRolePermissions($viewer, ['access-api']);\n\n        $resp = $this->get($this->endpoint);\n        $resp->assertStatus(401);\n\n        $this->actingAs($viewer, 'standard');\n\n        $this->startSession();\n        $resp = $this->withCredentials()->get($this->endpoint);\n        $resp->assertStatus(200);\n    }\n\n    public function test_no_token_throws_error()\n    {\n        $resp = $this->get($this->endpoint);\n        $resp->assertStatus(401);\n        $resp->assertJson($this->errorResponse('No authorization token found on the request', 401));\n    }\n\n    public function test_bad_token_format_throws_error()\n    {\n        $resp = $this->get($this->endpoint, ['Authorization' => 'Token abc123']);\n        $resp->assertStatus(401);\n        $resp->assertJson($this->errorResponse('An authorization token was found on the request but the format appeared incorrect', 401));\n    }\n\n    public function test_token_with_non_existing_id_throws_error()\n    {\n        $resp = $this->get($this->endpoint, ['Authorization' => 'Token abc:123']);\n        $resp->assertStatus(401);\n        $resp->assertJson($this->errorResponse('No matching API token was found for the provided authorization token', 401));\n    }\n\n    public function test_token_with_bad_secret_value_throws_error()\n    {\n        $resp = $this->get($this->endpoint, ['Authorization' => \"Token {$this->apiTokenId}:123\"]);\n        $resp->assertStatus(401);\n        $resp->assertJson($this->errorResponse('The secret provided for the given used API token is incorrect', 401));\n    }\n\n    public function test_api_access_permission_required_to_access_api()\n    {\n        $resp = $this->get($this->endpoint, $this->apiAuthHeader());\n        $resp->assertStatus(200);\n        auth()->logout();\n\n        $accessApiPermission = RolePermission::getByName('access-api');\n        $editorRole = $this->users->editor()->roles()->first();\n        $editorRole->detachPermission($accessApiPermission);\n\n        $resp = $this->get($this->endpoint, $this->apiAuthHeader());\n        $resp->assertStatus(403);\n        $resp->assertJson($this->errorResponse('The owner of the used API token does not have permission to make API calls', 403));\n    }\n\n    public function test_api_access_permission_required_to_access_api_with_session_auth()\n    {\n        $editor = $this->users->editor();\n        $this->actingAs($editor, 'standard');\n        $this->startSession();\n\n        $resp = $this->get($this->endpoint);\n        $resp->assertStatus(200);\n        auth('standard')->logout();\n\n        $accessApiPermission = RolePermission::getByName('access-api');\n        $editorRole = $this->users->editor()->roles()->first();\n        $editorRole->detachPermission($accessApiPermission);\n\n        $editor = User::query()->where('id', '=', $editor->id)->first();\n\n        $this->actingAs($editor, 'standard');\n        $resp = $this->get($this->endpoint);\n        $resp->assertStatus(403);\n        $resp->assertJson($this->errorResponse('The owner of the used API token does not have permission to make API calls', 403));\n    }\n\n    public function test_access_prevented_for_guest_users_with_api_permission_while_public_access_disabled()\n    {\n        $this->disableCookieEncryption();\n        $publicRole = Role::getSystemRole('public');\n        $accessApiPermission = RolePermission::getByName('access-api');\n        $publicRole->attachPermission($accessApiPermission);\n\n        $this->withCookie('bookstack_session', 'abc123');\n\n        // Test API access when not public\n        setting()->put('app-public', false);\n        $resp = $this->get($this->endpoint);\n        $resp->assertStatus(403);\n\n        // Test API access when public\n        setting()->put('app-public', true);\n        $resp = $this->get($this->endpoint);\n        $resp->assertStatus(200);\n    }\n\n    public function test_only_get_requests_are_supported_with_session_auth()\n    {\n        $user = $this->users->admin();\n        $this->actingAs($user, 'standard');\n        $this->startSession();\n\n        $uriByMethods = [\n            'POST' => '/books',\n            'PUT' => '/books/1',\n            'DELETE' => '/books/1',\n            'HEAD' => '/books',\n        ];\n\n        foreach ($uriByMethods as $method => $uri) {\n            $resp = $this->withCredentials()->json($method, \"/api{$uri}\");\n            $resp->assertStatus(403);\n            if ($method !== 'HEAD') {\n                $resp->assertJson($this->errorResponse('Only GET requests are allowed when using the API with cookie-based authentication', 403));\n            }\n        }\n    }\n\n    public function test_token_expiry_checked()\n    {\n        $editor = $this->users->editor();\n        $token = $editor->apiTokens()->first();\n\n        $resp = $this->get($this->endpoint, $this->apiAuthHeader());\n        $resp->assertStatus(200);\n        auth()->logout();\n\n        $token->expires_at = Carbon::now()->subDay()->format('Y-m-d');\n        $token->save();\n\n        $resp = $this->get($this->endpoint, $this->apiAuthHeader());\n        $resp->assertJson($this->errorResponse('The authorization token used has expired', 403));\n    }\n\n    public function test_email_confirmation_checked_using_api_auth()\n    {\n        $editor = $this->users->editor();\n        $editor->email_confirmed = false;\n        $editor->save();\n\n        // Set settings and get user instance\n        $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']);\n\n        $resp = $this->get($this->endpoint, $this->apiAuthHeader());\n        $resp->assertStatus(401);\n        $resp->assertJson($this->errorResponse('The email address for the account in use needs to be confirmed', 401));\n    }\n\n    public function test_rate_limit_headers_active_on_requests()\n    {\n        $resp = $this->actingAsApiEditor()->get($this->endpoint);\n        $resp->assertHeader('x-ratelimit-limit', 180);\n        $resp->assertHeader('x-ratelimit-remaining', 179);\n        $resp = $this->actingAsApiEditor()->get($this->endpoint);\n        $resp->assertHeader('x-ratelimit-remaining', 178);\n    }\n\n    public function test_rate_limit_hit_gives_json_error()\n    {\n        config()->set(['api.requests_per_minute' => 1]);\n        $resp = $this->actingAsApiEditor()->get($this->endpoint);\n        $resp->assertStatus(200);\n\n        $resp = $this->actingAsApiEditor()->get($this->endpoint);\n        $resp->assertStatus(429);\n        $resp->assertHeader('x-ratelimit-remaining', 0);\n        $resp->assertHeader('retry-after');\n        $resp->assertJson([\n            'error' => [\n                'code' => 429,\n            ],\n        ]);\n    }\n}\n"
  },
  {
    "path": "tests/Api/ApiConfigTest.php",
    "content": "<?php\n\nnamespace Tests\\Api;\n\nuse Tests\\TestCase;\n\nclass ApiConfigTest extends TestCase\n{\n    use TestsApi;\n\n    protected $endpoint = '/api/books';\n\n    public function test_default_item_count_reflected_in_listing_requests()\n    {\n        $this->actingAsApiEditor();\n\n        config()->set(['api.default_item_count' => 5]);\n        $resp = $this->get($this->endpoint);\n        $resp->assertJsonCount(5, 'data');\n\n        config()->set(['api.default_item_count' => 1]);\n        $resp = $this->get($this->endpoint);\n        $resp->assertJsonCount(1, 'data');\n    }\n\n    public function test_default_item_count_does_not_limit_count_param()\n    {\n        $this->actingAsApiEditor();\n        config()->set(['api.default_item_count' => 1]);\n        $resp = $this->get($this->endpoint . '?count=5');\n        $resp->assertJsonCount(5, 'data');\n    }\n\n    public function test_max_item_count_limits_listing_requests()\n    {\n        $this->actingAsApiEditor();\n\n        config()->set(['api.max_item_count' => 2]);\n        $resp = $this->get($this->endpoint);\n        $resp->assertJsonCount(2, 'data');\n\n        $resp = $this->get($this->endpoint . '?count=5');\n        $resp->assertJsonCount(2, 'data');\n    }\n\n    public function test_requests_per_min_alters_rate_limit()\n    {\n        $resp = $this->actingAsApiEditor()->get($this->endpoint);\n        $resp->assertHeader('x-ratelimit-limit', 180);\n\n        config()->set(['api.requests_per_minute' => 10]);\n\n        $resp = $this->actingAsApiEditor()->get($this->endpoint);\n        $resp->assertHeader('x-ratelimit-limit', 10);\n    }\n}\n"
  },
  {
    "path": "tests/Api/ApiDocsTest.php",
    "content": "<?php\n\nnamespace Tests\\Api;\n\nuse Tests\\TestCase;\n\nclass ApiDocsTest extends TestCase\n{\n    use TestsApi;\n\n    protected string $endpoint = '/api/docs';\n\n    public function test_api_endpoint_redirects_to_docs()\n    {\n        $resp = $this->actingAsApiEditor()->get('/api');\n        $resp->assertRedirect('api/docs');\n    }\n\n    public function test_docs_page_returns_view_with_docs_content()\n    {\n        $resp = $this->actingAsApiEditor()->get($this->endpoint);\n        $resp->assertStatus(200);\n        $resp->assertSee(url('/api/docs.json'));\n        $resp->assertSee('Show a JSON view of the API docs data.');\n        $resp->assertHeader('Content-Type', 'text/html; charset=utf-8');\n    }\n\n    public function test_docs_json_endpoint_returns_json()\n    {\n        $resp = $this->actingAsApiEditor()->get($this->endpoint . '.json');\n        $resp->assertStatus(200);\n        $resp->assertHeader('Content-Type', 'application/json');\n        $resp->assertJson([\n            'docs' => [[\n                'name' => 'docs-display',\n                'uri'  => 'api/docs',\n            ]],\n        ]);\n    }\n}\n"
  },
  {
    "path": "tests/Api/ApiListingTest.php",
    "content": "<?php\n\nnamespace Tests\\Api;\n\nuse BookStack\\Entities\\Models\\Book;\nuse Tests\\TestCase;\n\nclass ApiListingTest extends TestCase\n{\n    use TestsApi;\n\n    protected $endpoint = '/api/books';\n\n    public function test_count_parameter_limits_responses()\n    {\n        $this->actingAsApiEditor();\n        $bookCount = min(Book::visible()->count(), 100);\n\n        $resp = $this->get($this->endpoint);\n        $resp->assertJsonCount($bookCount, 'data');\n\n        $resp = $this->get($this->endpoint . '?count=1');\n        $resp->assertJsonCount(1, 'data');\n    }\n\n    public function test_offset_parameter()\n    {\n        $this->actingAsApiEditor();\n        $books = Book::visible()->orderBy('id')->take(3)->get();\n\n        $resp = $this->get($this->endpoint . '?count=1');\n        $resp->assertJsonMissing(['name' => $books[1]->name]);\n\n        $resp = $this->get($this->endpoint . '?count=1&offset=1000');\n        $resp->assertJsonCount(0, 'data');\n    }\n\n    public function test_sort_parameter()\n    {\n        $this->actingAsApiEditor();\n\n        $sortChecks = [\n            '-id'   => Book::visible()->orderBy('id', 'desc')->first(),\n            '+name' => Book::visible()->orderBy('name', 'asc')->first(),\n            'name'  => Book::visible()->orderBy('name', 'asc')->first(),\n            '-name' => Book::visible()->orderBy('name', 'desc')->first(),\n        ];\n\n        foreach ($sortChecks as $sortOption => $result) {\n            $resp = $this->get($this->endpoint . '?count=1&sort=' . $sortOption);\n            $resp->assertJson(['data' => [\n                [\n                    'id'   => $result->id,\n                    'name' => $result->name,\n                ],\n            ]]);\n        }\n    }\n\n    public function test_filter_parameter()\n    {\n        $this->actingAsApiEditor();\n        $book = Book::visible()->first();\n        $nameSubstr = substr($book->name, 0, 4);\n        $encodedNameSubstr = rawurlencode($nameSubstr);\n\n        $filterChecks = [\n            // Test different types of filter\n            \"filter[id]={$book->id}\"                  => 1,\n            \"filter[id:ne]={$book->id}\"               => Book::visible()->where('id', '!=', $book->id)->count(),\n            \"filter[id:gt]={$book->id}\"               => Book::visible()->where('id', '>', $book->id)->count(),\n            \"filter[id:gte]={$book->id}\"              => Book::visible()->where('id', '>=', $book->id)->count(),\n            \"filter[id:lt]={$book->id}\"               => Book::visible()->where('id', '<', $book->id)->count(),\n            \"filter[name:like]={$encodedNameSubstr}%\" => Book::visible()->where('name', 'like', $nameSubstr . '%')->count(),\n\n            // Test mulitple filters 'and' together\n            \"filter[id]={$book->id}&filter[name]=random_non_existing_string\" => 0,\n        ];\n\n        foreach ($filterChecks as $filterOption => $resultCount) {\n            $resp = $this->get($this->endpoint . '?count=1&' . $filterOption);\n            $resp->assertJson(['total' => $resultCount]);\n        }\n    }\n\n    public function test_total_on_results_shows_correctly()\n    {\n        $this->actingAsApiEditor();\n        $bookCount = Book::query()->count();\n        $resp = $this->get($this->endpoint . '?count=1');\n        $resp->assertJson(['total' => $bookCount]);\n    }\n\n    public function test_total_on_results_shows_correctly_when_offset_provided()\n    {\n        $this->actingAsApiEditor();\n        $bookCount = Book::query()->count();\n        $resp = $this->get($this->endpoint . '?count=1&offset=1');\n        $resp->assertJson(['total' => $bookCount]);\n    }\n}\n"
  },
  {
    "path": "tests/Api/AttachmentsApiTest.php",
    "content": "<?php\n\nnamespace Tests\\Api;\n\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Uploads\\Attachment;\nuse Illuminate\\Http\\UploadedFile;\nuse Illuminate\\Testing\\AssertableJsonString;\nuse Tests\\TestCase;\n\nclass AttachmentsApiTest extends TestCase\n{\n    use TestsApi;\n\n    protected string $baseEndpoint = '/api/attachments';\n\n    public function test_index_endpoint_returns_expected_book()\n    {\n        $this->actingAsApiEditor();\n        $page = $this->entities->page();\n        $attachment = $this->createAttachmentForPage($page, [\n            'name'     => 'My test attachment',\n            'external' => true,\n        ]);\n\n        $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');\n        $resp->assertJson(['data' => [\n            [\n                'id'          => $attachment->id,\n                'name'        => 'My test attachment',\n                'uploaded_to' => $page->id,\n                'external'    => true,\n            ],\n        ]]);\n    }\n\n    public function test_attachments_listing_based_upon_page_visibility()\n    {\n        $this->actingAsApiEditor();\n        $page = $this->entities->page();\n        $attachment = $this->createAttachmentForPage($page, [\n            'name'     => 'My test attachment',\n            'external' => true,\n        ]);\n\n        $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');\n        $resp->assertJson(['data' => [\n            [\n                'id' => $attachment->id,\n            ],\n        ]]);\n\n        $this->permissions->setEntityPermissions($page, [], []);\n\n        $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');\n        $resp->assertJsonMissing(['data' => [\n            [\n                'id' => $attachment->id,\n            ],\n        ]]);\n    }\n\n    public function test_create_endpoint_for_link_attachment()\n    {\n        $this->actingAsApiAdmin();\n        $page = $this->entities->page();\n\n        $details = [\n            'name'        => 'My attachment',\n            'uploaded_to' => $page->id,\n            'link'        => 'https://cats.example.com',\n        ];\n\n        $resp = $this->postJson($this->baseEndpoint, $details);\n        $resp->assertStatus(200);\n        /** @var Attachment $newItem */\n        $newItem = Attachment::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();\n        $resp->assertJson(['id' => $newItem->id, 'external' => true, 'name' => $details['name'], 'uploaded_to' => $page->id]);\n    }\n\n    public function test_create_endpoint_for_upload_attachment()\n    {\n        $this->actingAsApiAdmin();\n        $page = $this->entities->page();\n        $file = $this->getTestFile('textfile.txt');\n\n        $details = [\n            'name'        => 'My attachment',\n            'uploaded_to' => $page->id,\n        ];\n\n        $resp = $this->call('POST', $this->baseEndpoint, $details, [], ['file' => $file]);\n        $resp->assertStatus(200);\n        /** @var Attachment $newItem */\n        $newItem = Attachment::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();\n        $resp->assertJson(['id' => $newItem->id, 'external' => false, 'extension' => 'txt', 'name' => $details['name'], 'uploaded_to' => $page->id]);\n        $this->assertTrue(file_exists(storage_path($newItem->path)));\n        unlink(storage_path($newItem->path));\n    }\n\n    public function test_upload_limit_restricts_attachment_uploads()\n    {\n        $this->actingAsApiAdmin();\n        $page = $this->entities->page();\n\n        config()->set('app.upload_limit', 1);\n\n        $file = tmpfile();\n        $filePath = stream_get_meta_data($file)['uri'];\n        fwrite($file, str_repeat('a', 1200000));\n        $file = new UploadedFile($filePath, 'test.txt', 'text/plain', null, true);\n\n        $details = [\n            'name'        => 'My attachment',\n            'uploaded_to' => $page->id,\n        ];\n        $resp = $this->call('POST', $this->baseEndpoint, $details, [], ['file' => $file]);\n        $resp->assertStatus(422);\n        $resp->assertJson($this->validationResponse([\n            'file' => ['The file may not be greater than 1000 kilobytes.'],\n        ]));\n    }\n\n    public function test_name_needed_to_create()\n    {\n        $this->actingAsApiAdmin();\n        $page = $this->entities->page();\n\n        $details = [\n            'uploaded_to' => $page->id,\n            'link'        => 'https://example.com',\n        ];\n\n        $resp = $this->postJson($this->baseEndpoint, $details);\n        $resp->assertStatus(422);\n        $resp->assertJson($this->validationResponse(['name' => ['The name field is required.']]));\n    }\n\n    public function test_link_or_file_needed_to_create()\n    {\n        $this->actingAsApiAdmin();\n        $page = $this->entities->page();\n\n        $details = [\n            'name'        => 'my attachment',\n            'uploaded_to' => $page->id,\n        ];\n\n        $resp = $this->postJson($this->baseEndpoint, $details);\n        $resp->assertStatus(422);\n        $resp->assertJson($this->validationResponse([\n            'file' => ['The file field is required when link is not present.'],\n            'link' => ['The link field is required when file is not present.'],\n        ]));\n    }\n\n    public function test_message_shown_if_file_is_not_a_valid_file()\n    {\n        $this->actingAsApiAdmin();\n        $page = $this->entities->page();\n\n        $details = [\n            'name'        => 'my attachment',\n            'uploaded_to' => $page->id,\n            'file'        => 'cat',\n        ];\n\n        $resp = $this->postJson($this->baseEndpoint, $details);\n        $resp->assertStatus(422);\n        $resp->assertJson($this->validationResponse(['file' => ['The file must be provided as a valid file.']]));\n    }\n\n    public function test_read_endpoint_for_link_attachment()\n    {\n        $this->actingAsApiAdmin();\n        $page = $this->entities->page();\n\n        $attachment = $this->createAttachmentForPage($page, [\n            'name'  => 'my attachment',\n            'path'  => 'https://example.com',\n            'order' => 1,\n        ]);\n\n        $resp = $this->getJson(\"{$this->baseEndpoint}/{$attachment->id}\");\n\n        $resp->assertStatus(200);\n        $resp->assertJson([\n            'id'          => $attachment->id,\n            'content'     => 'https://example.com',\n            'external'    => true,\n            'uploaded_to' => $page->id,\n            'order'       => 1,\n            'created_by'  => [\n                'name' => $attachment->createdBy->name,\n            ],\n            'updated_by' => [\n                'name' => $attachment->createdBy->name,\n            ],\n            'links' => [\n                'html'     => \"<a target=\\\"_blank\\\" href=\\\"http://localhost/attachments/{$attachment->id}\\\">my attachment</a>\",\n                'markdown' => \"[my attachment](http://localhost/attachments/{$attachment->id})\",\n            ],\n        ]);\n    }\n\n    public function test_read_endpoint_for_file_attachment()\n    {\n        $this->actingAsApiAdmin();\n        $page = $this->entities->page();\n        $file = $this->getTestFile('textfile.txt');\n\n        $details = [\n            'name'        => 'My file attachment',\n            'uploaded_to' => $page->id,\n        ];\n        $this->call('POST', $this->baseEndpoint, $details, [], ['file' => $file]);\n        /** @var Attachment $attachment */\n        $attachment = Attachment::query()->orderByDesc('id')->where('name', '=', $details['name'])->firstOrFail();\n\n        $resp = $this->getJson(\"{$this->baseEndpoint}/{$attachment->id}\");\n        $resp->assertStatus(200);\n        $resp->assertHeader('Content-Type', 'application/json');\n\n        $json = new AssertableJsonString($resp->streamedContent());\n        $json->assertSubset([\n            'id'          => $attachment->id,\n            'content'     => base64_encode(file_get_contents(storage_path($attachment->path))),\n            'external'    => false,\n            'uploaded_to' => $page->id,\n            'order'       => 1,\n            'created_by'  => [\n                'name' => $attachment->createdBy->name,\n            ],\n            'updated_by' => [\n                'name' => $attachment->updatedBy->name,\n            ],\n            'links' => [\n                'html'     => \"<a target=\\\"_blank\\\" href=\\\"http://localhost/attachments/{$attachment->id}\\\">My file attachment</a>\",\n                'markdown' => \"[My file attachment](http://localhost/attachments/{$attachment->id})\",\n            ],\n        ]);\n\n        unlink(storage_path($attachment->path));\n    }\n\n    public function test_attachment_not_visible_on_other_users_draft()\n    {\n        $this->actingAsApiAdmin();\n        $editor = $this->users->editor();\n\n        $page = $this->entities->page();\n        $page->draft = true;\n        $page->owned_by = $editor->id;\n        $page->save();\n        $this->permissions->regenerateForEntity($page);\n\n        $attachment = $this->createAttachmentForPage($page, [\n            'name'  => 'my attachment',\n            'path'  => 'https://example.com',\n            'order' => 1,\n        ]);\n\n        $resp = $this->getJson(\"{$this->baseEndpoint}/{$attachment->id}\");\n\n        $resp->assertStatus(404);\n    }\n\n    public function test_update_endpoint()\n    {\n        $this->actingAsApiAdmin();\n        $page = $this->entities->page();\n        $attachment = $this->createAttachmentForPage($page);\n\n        $details = [\n            'name' => 'My updated API attachment',\n        ];\n\n        $resp = $this->putJson(\"{$this->baseEndpoint}/{$attachment->id}\", $details);\n        $attachment->refresh();\n\n        $resp->assertStatus(200);\n        $resp->assertJson(['id' => $attachment->id, 'name' => 'My updated API attachment']);\n    }\n\n    public function test_update_link_attachment_to_file()\n    {\n        $this->actingAsApiAdmin();\n        $page = $this->entities->page();\n        $attachment = $this->createAttachmentForPage($page);\n        $file = $this->getTestFile('textfile.txt');\n\n        $resp = $this->call('PUT', \"{$this->baseEndpoint}/{$attachment->id}\", ['name' => 'My updated file'], [], ['file' => $file]);\n        $resp->assertStatus(200);\n\n        $attachment->refresh();\n        $this->assertFalse($attachment->external);\n        $this->assertEquals('txt', $attachment->extension);\n        $this->assertStringStartsWith('uploads/files/', $attachment->path);\n        $this->assertFileExists(storage_path($attachment->path));\n\n        unlink(storage_path($attachment->path));\n    }\n\n    public function test_update_file_attachment_to_link()\n    {\n        $this->actingAsApiAdmin();\n        $page = $this->entities->page();\n        $attachment = $this->createAttachmentForPage($page);\n\n        $resp = $this->putJson(\"{$this->baseEndpoint}/{$attachment->id}\", [\n            'link' => 'https://example.com/donkey',\n        ]);\n\n        $resp->assertStatus(200);\n        $this->assertDatabaseHas('attachments', [\n            'id' => $attachment->id,\n            'path' => 'https://example.com/donkey',\n        ]);\n    }\n\n    public function test_update_does_not_require_name()\n    {\n        $this->actingAsApiAdmin();\n        $page = $this->entities->page();\n        $file = $this->getTestFile('textfile.txt');\n        $this->call('POST', $this->baseEndpoint, ['name' => 'My file attachment', 'uploaded_to' => $page->id], [], ['file' => $file]);\n        /** @var Attachment $attachment */\n        $attachment = Attachment::query()->where('name', '=', 'My file attachment')->firstOrFail();\n\n        $filePath = storage_path($attachment->path);\n        $this->assertFileExists($filePath);\n\n        $details = [\n            'name' => 'My updated API attachment',\n            'link' => 'https://cats.example.com',\n        ];\n\n        $resp = $this->putJson(\"{$this->baseEndpoint}/{$attachment->id}\", $details);\n        $resp->assertStatus(200);\n        $attachment->refresh();\n\n        $this->assertFileDoesNotExist($filePath);\n        $this->assertTrue($attachment->external);\n        $this->assertEquals('https://cats.example.com', $attachment->path);\n        $this->assertEquals('', $attachment->extension);\n    }\n\n    public function test_delete_endpoint()\n    {\n        $this->actingAsApiAdmin();\n        $page = $this->entities->page();\n        $attachment = $this->createAttachmentForPage($page);\n\n        $resp = $this->deleteJson(\"{$this->baseEndpoint}/{$attachment->id}\");\n\n        $resp->assertStatus(204);\n        $this->assertDatabaseMissing('attachments', ['id' => $attachment->id]);\n    }\n\n    protected function createAttachmentForPage(Page $page, $attributes = []): Attachment\n    {\n        $admin = $this->users->admin();\n        /** @var Attachment $attachment */\n        $attachment = $page->attachments()->forceCreate(array_merge([\n            'uploaded_to' => $page->id,\n            'name'        => 'test attachment',\n            'external'    => true,\n            'order'       => 1,\n            'created_by'  => $admin->id,\n            'updated_by'  => $admin->id,\n            'path'        => 'https://attachment.example.com',\n        ], $attributes));\n\n        return $attachment;\n    }\n\n    /**\n     * Get a test file that can be uploaded.\n     */\n    protected function getTestFile(string $fileName): UploadedFile\n    {\n        return new UploadedFile(base_path('tests/test-data/test-file.txt'), $fileName, 'text/plain', null, true);\n    }\n}\n"
  },
  {
    "path": "tests/Api/BooksApiTest.php",
    "content": "<?php\n\nnamespace Tests\\Api;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Repos\\BaseRepo;\nuse Carbon\\Carbon;\nuse Tests\\TestCase;\n\nclass BooksApiTest extends TestCase\n{\n    use TestsApi;\n\n    protected string $baseEndpoint = '/api/books';\n\n    public function test_index_endpoint_returns_expected_book()\n    {\n        $this->actingAsApiEditor();\n        $firstBook = Book::query()->orderBy('id', 'asc')->first();\n\n        $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');\n        $resp->assertJson(['data' => [\n            [\n                'id'   => $firstBook->id,\n                'name' => $firstBook->name,\n                'slug' => $firstBook->slug,\n                'owned_by' => $firstBook->owned_by,\n                'created_by' => $firstBook->created_by,\n                'updated_by' => $firstBook->updated_by,\n                'cover' => null,\n            ],\n        ]]);\n    }\n\n    public function test_index_endpoint_includes_cover_if_set()\n    {\n        $this->actingAsApiEditor();\n        $book = $this->entities->book();\n\n        $baseRepo = $this->app->make(BaseRepo::class);\n        $image = $this->files->uploadedImage('book_cover');\n        $baseRepo->updateCoverImage($book, $image);\n\n        $resp = $this->getJson($this->baseEndpoint . '?filter[id]=' . $book->id);\n        $resp->assertJson(['data' => [\n            [\n                'id'   => $book->id,\n                'cover' => [\n                    'id' => $book->coverInfo()->getImage()->id,\n                    'url' => $book->coverInfo()->getImage()->url,\n                ],\n            ],\n        ]]);\n    }\n\n    public function test_create_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $templatePage = $this->entities->templatePage();\n        $details = [\n            'name'                => 'My API book',\n            'description'         => 'A book created via the API',\n            'default_template_id' => $templatePage->id,\n        ];\n\n        $resp = $this->postJson($this->baseEndpoint, $details);\n        $resp->assertStatus(200);\n\n        $newItem = Book::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();\n        $resp->assertJson(array_merge($details, [\n            'id' => $newItem->id,\n            'slug' => $newItem->slug,\n            'description_html' => '<p>A book created via the API</p>',\n        ]));\n        $this->assertActivityExists('book_create', $newItem);\n    }\n\n    public function test_create_endpoint_with_html()\n    {\n        $this->actingAsApiEditor();\n        $details = [\n            'name'             => 'My API book',\n            'description_html' => '<p>A book <em>created</em> <strong>via</strong> the API</p>',\n        ];\n\n        $resp = $this->postJson($this->baseEndpoint, $details);\n        $resp->assertStatus(200);\n\n        $newItem = Book::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();\n        $expectedDetails = array_merge($details, [\n            'id'          => $newItem->id,\n            'description' => 'A book created via the API',\n        ]);\n\n        $resp->assertJson($expectedDetails);\n        $this->assertDatabaseHasEntityData('book', $expectedDetails);\n    }\n\n    public function test_book_name_needed_to_create()\n    {\n        $this->actingAsApiEditor();\n        $details = [\n            'description' => 'A book created via the API',\n        ];\n\n        $resp = $this->postJson($this->baseEndpoint, $details);\n        $resp->assertStatus(422);\n        $resp->assertJson([\n            'error' => [\n                'message'    => 'The given data was invalid.',\n                'validation' => [\n                    'name' => ['The name field is required.'],\n                ],\n                'code'       => 422,\n            ],\n        ]);\n    }\n\n    public function test_read_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $book = $this->entities->book();\n\n        $resp = $this->getJson($this->baseEndpoint . \"/{$book->id}\");\n\n        $resp->assertStatus(200);\n        $resp->assertJson([\n            'id'         => $book->id,\n            'slug'       => $book->slug,\n            'created_by' => [\n                'name' => $book->createdBy->name,\n            ],\n            'updated_by' => [\n                'name' => $book->createdBy->name,\n            ],\n            'owned_by' => [\n                'name' => $book->ownedBy->name,\n            ],\n            'default_template_id' => null,\n        ]);\n    }\n\n    public function test_read_endpoint_includes_chapter_and_page_contents()\n    {\n        $this->actingAsApiEditor();\n        $book = $this->entities->bookHasChaptersAndPages();\n        $chapter = $book->chapters()->first();\n        $chapterPage = $chapter->pages()->first();\n\n        $resp = $this->getJson($this->baseEndpoint . \"/{$book->id}\");\n\n        $directChildCount = $book->directPages()->count() + $book->chapters()->count();\n        $resp->assertStatus(200);\n        $resp->assertJsonCount($directChildCount, 'contents');\n\n        $contents = $resp->json('contents');\n        $respChapter = array_values(array_filter($contents, fn ($item) =>  ($item['id'] === $chapter->id && $item['type'] === 'chapter')))[0];\n        $this->assertArrayMapIncludes([\n            'id' => $chapter->id,\n            'type' => 'chapter',\n            'name' => $chapter->name,\n            'slug' => $chapter->slug,\n        ], $respChapter);\n\n        $respPage = array_values(array_filter($respChapter['pages'], fn ($item) =>  ($item['id'] === $chapterPage->id)))[0];\n\n        $this->assertArrayMapIncludes([\n            'id' => $chapterPage->id,\n            'name' => $chapterPage->name,\n            'slug' => $chapterPage->slug,\n        ], $respPage);\n    }\n\n    public function test_read_endpoint_contents_nested_pages_has_permissions_applied()\n    {\n        $this->actingAsApiEditor();\n\n        $book = $this->entities->bookHasChaptersAndPages();\n        $chapter = $book->chapters()->first();\n        $chapterPage = $chapter->pages()->first();\n        $customName = 'MyNonVisiblePageWithinAChapter';\n        $chapterPage->name = $customName;\n        $chapterPage->save();\n\n        $this->permissions->disableEntityInheritedPermissions($chapterPage);\n\n        $resp = $this->getJson($this->baseEndpoint . \"/{$book->id}\");\n        $resp->assertJsonMissing(['name' => $customName]);\n    }\n\n    public function test_read_endpoint_lists_visible_shelves_the_book_is_assigned_to()\n    {\n        $this->actingAsApiEditor();\n        $shelf = $this->entities->shelf();\n        $otherShelf = $this->entities->shelf();\n        $book = $this->entities->book();\n        $book->shelves()->detach();\n\n        $book->shelves()->attach($shelf);\n        $book->shelves()->attach($otherShelf);\n\n        $this->assertEquals(2, $book->shelves()->count());\n\n        $this->permissions->disableEntityInheritedPermissions($otherShelf);\n\n        $resp = $this->getJson(\"{$this->baseEndpoint}/{$book->id}\");\n        $resp->assertOk();\n        $resp->assertJsonCount(1, 'shelves');\n        $resp->assertJson([\n            'shelves' => [\n                [\n                    'id' => $shelf->id,\n                    'name' => $shelf->name,\n                    'slug' => $shelf->slug,\n                ]\n            ]\n        ]);\n        $resp->assertJsonMissingPath('shelves.0.description');\n        $resp->assertJsonMissingPath('shelves.0.pivot');\n    }\n\n    public function test_update_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $book = $this->entities->book();\n        $templatePage = $this->entities->templatePage();\n        $details = [\n            'name'        => 'My updated API book',\n            'description' => 'A book updated via the API',\n            'default_template_id' => $templatePage->id,\n        ];\n\n        $resp = $this->putJson($this->baseEndpoint . \"/{$book->id}\", $details);\n        $book->refresh();\n\n        $resp->assertStatus(200);\n        $resp->assertJson(array_merge($details, [\n            'id' => $book->id,\n            'slug' => $book->slug,\n            'description_html' => '<p>A book updated via the API</p>',\n        ]));\n        $this->assertActivityExists('book_update', $book);\n    }\n\n    public function test_update_endpoint_with_html()\n    {\n        $this->actingAsApiEditor();\n        $book = $this->entities->book();\n        $details = [\n            'name'             => 'My updated API book',\n            'description_html' => '<p>A book <strong>updated</strong> via the API</p>',\n        ];\n\n        $resp = $this->putJson($this->baseEndpoint . \"/{$book->id}\", $details);\n        $resp->assertStatus(200);\n\n        $this->assertDatabaseHasEntityData('book', array_merge($details, ['id' => $book->id, 'description' => 'A book updated via the API']));\n    }\n\n    public function test_update_increments_updated_date_if_only_tags_are_sent()\n    {\n        $this->actingAsApiEditor();\n        $book = $this->entities->book();\n        Book::query()->where('id', '=', $book->id)->update(['updated_at' => Carbon::now()->subWeek()]);\n\n        $details = [\n            'tags' => [['name' => 'Category', 'value' => 'Testing']],\n        ];\n\n        $this->putJson($this->baseEndpoint . \"/{$book->id}\", $details);\n        $book->refresh();\n        $this->assertGreaterThan(Carbon::now()->subDay()->unix(), $book->updated_at->unix());\n    }\n\n    public function test_update_cover_image_control()\n    {\n        $this->actingAsApiEditor();\n        /** @var Book $book */\n        $book = $this->entities->book();\n        $this->assertNull($book->coverInfo()->getImage());\n        $file = $this->files->uploadedImage('image.png');\n\n        // Ensure cover image can be set via API\n        $resp = $this->call('PUT', $this->baseEndpoint . \"/{$book->id}\", [\n            'name'  => 'My updated API book with image',\n        ], [], ['image' => $file]);\n        $book->refresh();\n\n        $resp->assertStatus(200);\n        $this->assertNotNull($book->coverInfo()->getImage());\n\n        // Ensure further updates without image do not clear cover image\n        $resp = $this->put($this->baseEndpoint . \"/{$book->id}\", [\n            'name' => 'My updated book again',\n        ]);\n        $book->refresh();\n\n        $resp->assertStatus(200);\n        $this->assertNotNull($book->coverInfo()->getImage());\n\n        // Ensure update with null image property clears image\n        $resp = $this->put($this->baseEndpoint . \"/{$book->id}\", [\n            'image' => null,\n        ]);\n        $book->refresh();\n\n        $resp->assertStatus(200);\n        $this->assertNull($book->coverInfo()->getImage());\n    }\n\n    public function test_delete_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $book = $this->entities->book();\n        $resp = $this->deleteJson($this->baseEndpoint . \"/{$book->id}\");\n\n        $resp->assertStatus(204);\n        $this->assertActivityExists('book_delete');\n    }\n}\n"
  },
  {
    "path": "tests/Api/ChaptersApiTest.php",
    "content": "<?php\n\nnamespace Tests\\Api;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\DB;\nuse Tests\\TestCase;\n\nclass ChaptersApiTest extends TestCase\n{\n    use TestsApi;\n\n    protected string $baseEndpoint = '/api/chapters';\n\n    public function test_index_endpoint_returns_expected_chapter()\n    {\n        $this->actingAsApiEditor();\n        $firstChapter = Chapter::query()->orderBy('id', 'asc')->first();\n\n        $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');\n        $resp->assertJson(['data' => [\n            [\n                'id'        => $firstChapter->id,\n                'name'      => $firstChapter->name,\n                'slug'      => $firstChapter->slug,\n                'book_id'   => $firstChapter->book->id,\n                'priority'  => $firstChapter->priority,\n                'book_slug' => $firstChapter->book->slug,\n                'owned_by'   => $firstChapter->owned_by,\n                'created_by' => $firstChapter->created_by,\n                'updated_by' => $firstChapter->updated_by,\n            ],\n        ]]);\n    }\n\n    public function test_create_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $book = $this->entities->book();\n        $templatePage = $this->entities->templatePage();\n        $details = [\n            'name'        => 'My API chapter',\n            'description' => 'A chapter created via the API',\n            'book_id'     => $book->id,\n            'tags'        => [\n                [\n                    'name'  => 'tagname',\n                    'value' => 'tagvalue',\n                ],\n            ],\n            'priority' => 15,\n            'default_template_id' => $templatePage->id,\n        ];\n\n        $resp = $this->postJson($this->baseEndpoint, $details);\n        $resp->assertStatus(200);\n        $newItem = Chapter::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();\n        $resp->assertJson(array_merge($details, [\n            'id' => $newItem->id,\n            'slug' => $newItem->slug,\n            'description_html' => '<p>A chapter created via the API</p>',\n        ]));\n        $this->assertDatabaseHas('tags', [\n            'entity_id'   => $newItem->id,\n            'entity_type' => $newItem->getMorphClass(),\n            'name'        => 'tagname',\n            'value'       => 'tagvalue',\n        ]);\n        $resp->assertJsonMissing(['pages' => []]);\n        $this->assertActivityExists('chapter_create', $newItem);\n    }\n\n    public function test_create_endpoint_with_html()\n    {\n        $this->actingAsApiEditor();\n        $book = $this->entities->book();\n        $details = [\n            'name'             => 'My API chapter',\n            'description_html' => '<p>A chapter <strong>created</strong> via the API</p>',\n            'book_id'          => $book->id,\n        ];\n\n        $resp = $this->postJson($this->baseEndpoint, $details);\n        $resp->assertStatus(200);\n        $newItem = Chapter::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();\n\n        $expectedDetails = array_merge($details, [\n            'id'          => $newItem->id,\n            'description' => 'A chapter created via the API',\n        ]);\n        $resp->assertJson($expectedDetails);\n        $this->assertDatabaseHasEntityData('chapter', $expectedDetails);\n    }\n\n    public function test_chapter_name_needed_to_create()\n    {\n        $this->actingAsApiEditor();\n        $book = $this->entities->book();\n        $details = [\n            'book_id'     => $book->id,\n            'description' => 'A chapter created via the API',\n        ];\n\n        $resp = $this->postJson($this->baseEndpoint, $details);\n        $resp->assertStatus(422);\n        $resp->assertJson($this->validationResponse([\n            'name' => ['The name field is required.'],\n        ]));\n    }\n\n    public function test_chapter_book_id_needed_to_create()\n    {\n        $this->actingAsApiEditor();\n        $details = [\n            'name'        => 'My api chapter',\n            'description' => 'A chapter created via the API',\n        ];\n\n        $resp = $this->postJson($this->baseEndpoint, $details);\n        $resp->assertStatus(422);\n        $resp->assertJson($this->validationResponse([\n            'book_id' => ['The book id field is required.'],\n        ]));\n    }\n\n    public function test_read_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $chapter = $this->entities->chapter();\n        $page = $chapter->pages()->first();\n\n        $resp = $this->getJson($this->baseEndpoint . \"/{$chapter->id}\");\n        $resp->assertStatus(200);\n        $resp->assertJson([\n            'id'         => $chapter->id,\n            'slug'       => $chapter->slug,\n            'book_slug'  => $chapter->book->slug,\n            'created_by' => [\n                'name' => $chapter->createdBy->name,\n            ],\n            'book_id'    => $chapter->book_id,\n            'updated_by' => [\n                'name' => $chapter->createdBy->name,\n            ],\n            'owned_by' => [\n                'name' => $chapter->ownedBy->name,\n            ],\n            'pages' => [\n                [\n                    'id'   => $page->id,\n                    'slug' => $page->slug,\n                    'name' => $page->name,\n                    'owned_by' => $page->owned_by,\n                    'created_by' => $page->created_by,\n                    'updated_by' => $page->updated_by,\n                    'book_id' => $page->book->id,\n                    'chapter_id' => $chapter->id,\n                    'priority' => $page->priority,\n                    'book_slug' => $chapter->book->slug,\n                    'draft' => $page->draft,\n                    'template' => $page->template,\n                    'editor' => $page->editor,\n                ],\n            ],\n            'default_template_id' => null,\n        ]);\n        $resp->assertJsonMissingPath('book');\n        $resp->assertJsonCount($chapter->pages()->count(), 'pages');\n    }\n\n    public function test_update_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $chapter = $this->entities->chapter();\n        $templatePage = $this->entities->templatePage();\n        $details = [\n            'name'        => 'My updated API chapter',\n            'description' => 'A chapter updated via the API',\n            'tags'        => [\n                [\n                    'name'  => 'freshtag',\n                    'value' => 'freshtagval',\n                ],\n            ],\n            'priority'    => 15,\n            'default_template_id' => $templatePage->id,\n        ];\n\n        $resp = $this->putJson($this->baseEndpoint . \"/{$chapter->id}\", $details);\n        $chapter->refresh();\n\n        $resp->assertStatus(200);\n        $resp->assertJson(array_merge($details, [\n            'id' => $chapter->id,\n            'slug' => $chapter->slug,\n            'book_id' => $chapter->book_id,\n            'description_html' => '<p>A chapter updated via the API</p>',\n        ]));\n        $this->assertActivityExists('chapter_update', $chapter);\n    }\n\n    public function test_update_endpoint_with_html()\n    {\n        $this->actingAsApiEditor();\n        $chapter = $this->entities->chapter();\n        $details = [\n            'name'             => 'My updated API chapter',\n            'description_html' => '<p>A chapter <em>updated</em> via the API</p>',\n        ];\n\n        $resp = $this->putJson($this->baseEndpoint . \"/{$chapter->id}\", $details);\n        $resp->assertStatus(200);\n\n        $this->assertDatabaseHasEntityData('chapter', array_merge($details, [\n            'id' => $chapter->id, 'description' => 'A chapter updated via the API'\n        ]));\n    }\n\n    public function test_update_increments_updated_date_if_only_tags_are_sent()\n    {\n        $this->actingAsApiEditor();\n        $chapter = $this->entities->chapter();\n        $chapter->newQuery()->where('id', '=', $chapter->id)->update(['updated_at' => Carbon::now()->subWeek()]);\n\n        $details = [\n            'tags' => [['name' => 'Category', 'value' => 'Testing']],\n        ];\n\n        $this->putJson($this->baseEndpoint . \"/{$chapter->id}\", $details);\n        $chapter->refresh();\n        $this->assertGreaterThan(Carbon::now()->subDay()->unix(), $chapter->updated_at->unix());\n    }\n\n    public function test_update_with_book_id_moves_chapter()\n    {\n        $this->actingAsApiEditor();\n        $chapter = $this->entities->chapterHasPages();\n        $page = $chapter->pages()->first();\n        $newBook = Book::query()->where('id', '!=', $chapter->book_id)->first();\n\n        $resp = $this->putJson($this->baseEndpoint . \"/{$chapter->id}\", ['book_id' => $newBook->id]);\n        $resp->assertOk();\n        $chapter->refresh();\n\n        $this->assertDatabaseHasEntityData('chapter', ['id' => $chapter->id, 'book_id' => $newBook->id]);\n        $this->assertDatabaseHasEntityData('page', ['id' => $page->id, 'book_id' => $newBook->id, 'chapter_id' => $chapter->id]);\n    }\n\n    public function test_update_with_new_book_id_requires_delete_permission()\n    {\n        $editor = $this->users->editor();\n        $this->permissions->removeUserRolePermissions($editor, ['chapter-delete-all', 'chapter-delete-own']);\n        $this->actingAsForApi($editor);\n        $chapter = $this->entities->chapterHasPages();\n        $newBook = Book::query()->where('id', '!=', $chapter->book_id)->first();\n\n        $resp = $this->putJson($this->baseEndpoint . \"/{$chapter->id}\", ['book_id' => $newBook->id]);\n        $this->assertPermissionError($resp);\n    }\n\n    public function test_delete_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $chapter = $this->entities->chapter();\n        $resp = $this->deleteJson($this->baseEndpoint . \"/{$chapter->id}\");\n\n        $resp->assertStatus(204);\n        $this->assertActivityExists('chapter_delete');\n    }\n}\n"
  },
  {
    "path": "tests/Api/ContentPermissionsApiTest.php",
    "content": "<?php\n\nnamespace Tests\\Api;\n\nuse Tests\\TestCase;\n\nclass ContentPermissionsApiTest extends TestCase\n{\n    use TestsApi;\n\n    protected string $baseEndpoint = '/api/content-permissions';\n\n    public function test_user_roles_manage_permission_needed_for_all_endpoints()\n    {\n        $page = $this->entities->page();\n        $endpointMap = [\n            ['get', \"/api/content-permissions/page/{$page->id}\"],\n            ['put', \"/api/content-permissions/page/{$page->id}\"],\n        ];\n        $editor = $this->users->editor();\n\n        $this->actingAs($editor, 'api');\n        foreach ($endpointMap as [$method, $uri]) {\n            $resp = $this->json($method, $uri);\n            $resp->assertStatus(403);\n            $resp->assertJson($this->permissionErrorResponse());\n        }\n\n        $this->permissions->grantUserRolePermissions($editor, ['restrictions-manage-all']);\n\n        foreach ($endpointMap as [$method, $uri]) {\n            $resp = $this->json($method, $uri);\n            $this->assertNotEquals(403, $resp->getStatusCode());\n        }\n    }\n\n    public function test_read_endpoint_shows_expected_detail()\n    {\n        $page = $this->entities->page();\n        $owner = $this->users->newUser();\n        $role = $this->users->createRole();\n        $this->permissions->addEntityPermission($page, ['view', 'delete'], $role);\n        $this->permissions->changeEntityOwner($page, $owner);\n        $this->permissions->setFallbackPermissions($page, ['update', 'create']);\n\n        $this->actingAsApiAdmin();\n        $resp = $this->getJson($this->baseEndpoint . \"/page/{$page->id}\");\n\n        $resp->assertOk();\n        $resp->assertExactJson([\n            'owner' => [\n                'id' => $owner->id, 'name' => $owner->name, 'slug' => $owner->slug,\n            ],\n            'role_permissions' => [\n                [\n                    'role_id' => $role->id,\n                    'view' => true,\n                    'create' => false,\n                    'update' => false,\n                    'delete' => true,\n                    'role' => [\n                        'id' => $role->id,\n                        'display_name' => $role->display_name,\n                    ]\n                ]\n            ],\n            'fallback_permissions' => [\n                'inheriting' => false,\n                'view' => false,\n                'create' => true,\n                'update' => true,\n                'delete' => false,\n            ],\n        ]);\n    }\n\n    public function test_read_endpoint_shows_expected_detail_when_items_are_empty()\n    {\n        $page = $this->entities->page();\n        $page->permissions()->delete();\n        $page->owned_by = null;\n        $page->save();\n\n        $this->actingAsApiAdmin();\n        $resp = $this->getJson($this->baseEndpoint . \"/page/{$page->id}\");\n\n        $resp->assertOk();\n        $resp->assertExactJson([\n            'owner' => null,\n            'role_permissions' => [],\n            'fallback_permissions' => [\n                'inheriting' => true,\n                'view' => null,\n                'create' => null,\n                'update' => null,\n                'delete' => null,\n            ],\n        ]);\n    }\n\n    public function test_update_endpoint_can_change_owner()\n    {\n        $page = $this->entities->page();\n        $newOwner = $this->users->newUser();\n\n        $this->actingAsApiAdmin();\n        $resp = $this->putJson($this->baseEndpoint . \"/page/{$page->id}\", [\n            'owner_id' => $newOwner->id,\n        ]);\n\n        $resp->assertOk();\n        $resp->assertExactJson([\n            'owner' => ['id' => $newOwner->id, 'name' => $newOwner->name, 'slug' => $newOwner->slug],\n            'role_permissions' => [],\n            'fallback_permissions' => [\n                'inheriting' => true,\n                'view' => null,\n                'create' => null,\n                'update' => null,\n                'delete' => null,\n            ],\n        ]);\n    }\n\n    public function test_update_can_set_role_permissions()\n    {\n        $page = $this->entities->page();\n        $page->owned_by = null;\n        $page->save();\n        $newRoleA = $this->users->createRole();\n        $newRoleB = $this->users->createRole();\n\n        $this->actingAsApiAdmin();\n        $resp = $this->putJson($this->baseEndpoint . \"/page/{$page->id}\", [\n            'role_permissions' => [\n                ['role_id' => $newRoleA->id, 'view' => true, 'create' => false, 'update' => false, 'delete' => false],\n                ['role_id' => $newRoleB->id, 'view' => true, 'create' => false, 'update' => true, 'delete' => true],\n            ],\n        ]);\n\n        $resp->assertOk();\n        $resp->assertExactJson([\n            'owner' => null,\n            'role_permissions' => [\n                [\n                    'role_id' => $newRoleA->id,\n                    'view' => true,\n                    'create' => false,\n                    'update' => false,\n                    'delete' => false,\n                    'role' => [\n                        'id' => $newRoleA->id,\n                        'display_name' => $newRoleA->display_name,\n                    ]\n                ],\n                [\n                    'role_id' => $newRoleB->id,\n                    'view' => true,\n                    'create' => false,\n                    'update' => true,\n                    'delete' => true,\n                    'role' => [\n                        'id' => $newRoleB->id,\n                        'display_name' => $newRoleB->display_name,\n                    ]\n                ]\n            ],\n            'fallback_permissions' => [\n                'inheriting' => true,\n                'view' => null,\n                'create' => null,\n                'update' => null,\n                'delete' => null,\n            ],\n        ]);\n    }\n\n    public function test_update_can_set_fallback_permissions()\n    {\n        $page = $this->entities->page();\n        $page->owned_by = null;\n        $page->save();\n\n        $this->actingAsApiAdmin();\n        $resp = $this->putJson($this->baseEndpoint . \"/page/{$page->id}\", [\n            'fallback_permissions' => [\n                'inheriting' => false,\n                'view' => true,\n                'create' => true,\n                'update' => true,\n                'delete' => false,\n            ],\n        ]);\n\n        $resp->assertOk();\n        $resp->assertExactJson([\n            'owner' => null,\n            'role_permissions' => [],\n            'fallback_permissions' => [\n                'inheriting' => false,\n                'view' => true,\n                'create' => true,\n                'update' => true,\n                'delete' => false,\n            ],\n        ]);\n    }\n\n    public function test_update_can_clear_roles_permissions()\n    {\n        $page = $this->entities->page();\n        $this->permissions->addEntityPermission($page, ['view'], $this->users->createRole());\n        $page->owned_by = null;\n        $page->save();\n\n        $this->actingAsApiAdmin();\n        $resp = $this->putJson($this->baseEndpoint . \"/page/{$page->id}\", [\n            'role_permissions' => [],\n        ]);\n\n        $resp->assertOk();\n        $resp->assertExactJson([\n            'owner' => null,\n            'role_permissions' => [],\n            'fallback_permissions' => [\n                'inheriting' => true,\n                'view' => null,\n                'create' => null,\n                'update' => null,\n                'delete' => null,\n            ],\n        ]);\n    }\n\n    public function test_update_can_clear_fallback_permissions()\n    {\n        $page = $this->entities->page();\n        $this->permissions->setFallbackPermissions($page, ['view', 'update']);\n        $page->owned_by = null;\n        $page->save();\n\n        $this->actingAsApiAdmin();\n        $resp = $this->putJson($this->baseEndpoint . \"/page/{$page->id}\", [\n            'fallback_permissions' => [\n                'inheriting' => true,\n            ],\n        ]);\n\n        $resp->assertOk();\n        $resp->assertExactJson([\n            'owner' => null,\n            'role_permissions' => [],\n            'fallback_permissions' => [\n                'inheriting' => true,\n                'view' => null,\n                'create' => null,\n                'update' => null,\n                'delete' => null,\n            ],\n        ]);\n    }\n\n    public function test_update_can_both_provide_owner_and_fallback_permissions()\n    {\n        $user = $this->users->viewer();\n        $page = $this->entities->page();\n        $page->owned_by = null;\n        $page->save();\n\n        $this->actingAsApiAdmin();\n        $resp = $this->putJson($this->baseEndpoint . \"/page/{$page->id}\", [\n            \"owner_id\" => $user->id,\n            'fallback_permissions' => [\n                'inheriting' => false,\n                'view' => false,\n                'create' => false,\n                'update' => false,\n                'delete' => false,\n            ],\n        ]);\n\n        $resp->assertOk();\n        $this->assertDatabaseHasEntityData('page', ['id' => $page->id, 'owned_by' => $user->id]);\n        $this->assertDatabaseHas('entity_permissions', [\n            'entity_id' => $page->id,\n            'entity_type' => 'page',\n            'role_id' => 0,\n            'view' => false,\n            'create' => false,\n            'update' => false,\n            'delete' => false,\n        ]);\n    }\n}\n"
  },
  {
    "path": "tests/Api/ExportsApiTest.php",
    "content": "<?php\n\nnamespace Tests\\Api;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse Tests\\Exports\\ZipTestHelper;\nuse Tests\\TestCase;\n\nclass ExportsApiTest extends TestCase\n{\n    use TestsApi;\n\n    public function test_book_html_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $book = $this->entities->book();\n\n        $resp = $this->get(\"/api/books/{$book->id}/export/html\");\n        $resp->assertStatus(200);\n        $resp->assertSee($book->name);\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $book->slug . '.html');\n    }\n\n    public function test_book_plain_text_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $book = $this->entities->book();\n\n        $resp = $this->get(\"/api/books/{$book->id}/export/plaintext\");\n        $resp->assertStatus(200);\n        $resp->assertSee($book->name);\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $book->slug . '.txt');\n    }\n\n    public function test_book_pdf_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $book = $this->entities->book();\n\n        $resp = $this->get(\"/api/books/{$book->id}/export/pdf\");\n        $resp->assertStatus(200);\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $book->slug . '.pdf');\n    }\n\n    public function test_book_markdown_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $book = Book::visible()->has('pages')->has('chapters')->first();\n\n        $resp = $this->get(\"/api/books/{$book->id}/export/markdown\");\n        $resp->assertStatus(200);\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $book->slug . '.md');\n        $resp->assertSee('# ' . $book->name);\n        $resp->assertSee('# ' . $book->pages()->first()->name);\n        $resp->assertSee('# ' . $book->chapters()->first()->name);\n    }\n\n    public function test_book_zip_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $book = Book::visible()->has('pages')->has('chapters')->first();\n\n        $resp = $this->get(\"/api/books/{$book->id}/export/zip\");\n        $resp->assertStatus(200);\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $book->slug . '.zip');\n\n        $zip = ZipTestHelper::extractFromZipResponse($resp);\n        $this->assertArrayHasKey('book', $zip->data);\n    }\n\n    public function test_chapter_html_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $chapter = $this->entities->chapter();\n\n        $resp = $this->get(\"/api/chapters/{$chapter->id}/export/html\");\n        $resp->assertStatus(200);\n        $resp->assertSee($chapter->name);\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $chapter->slug . '.html');\n    }\n\n    public function test_chapter_plain_text_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $chapter = $this->entities->chapter();\n\n        $resp = $this->get(\"/api/chapters/{$chapter->id}/export/plaintext\");\n        $resp->assertStatus(200);\n        $resp->assertSee($chapter->name);\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $chapter->slug . '.txt');\n    }\n\n    public function test_chapter_pdf_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $chapter = $this->entities->chapter();\n\n        $resp = $this->get(\"/api/chapters/{$chapter->id}/export/pdf\");\n        $resp->assertStatus(200);\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $chapter->slug . '.pdf');\n    }\n\n    public function test_chapter_markdown_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $chapter = Chapter::visible()->has('pages')->first();\n\n        $resp = $this->get(\"/api/chapters/{$chapter->id}/export/markdown\");\n        $resp->assertStatus(200);\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $chapter->slug . '.md');\n        $resp->assertSee('# ' . $chapter->name);\n        $resp->assertSee('# ' . $chapter->pages()->first()->name);\n    }\n\n    public function test_chapter_zip_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $chapter = Chapter::visible()->has('pages')->first();\n\n        $resp = $this->get(\"/api/chapters/{$chapter->id}/export/zip\");\n        $resp->assertStatus(200);\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $chapter->slug . '.zip');\n\n        $zip = ZipTestHelper::extractFromZipResponse($resp);\n        $this->assertArrayHasKey('chapter', $zip->data);\n    }\n\n    public function test_page_html_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $page = $this->entities->page();\n\n        $resp = $this->get(\"/api/pages/{$page->id}/export/html\");\n        $resp->assertStatus(200);\n        $resp->assertSee($page->name);\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $page->slug . '.html');\n    }\n\n    public function test_page_plain_text_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $page = $this->entities->page();\n\n        $resp = $this->get(\"/api/pages/{$page->id}/export/plaintext\");\n        $resp->assertStatus(200);\n        $resp->assertSee($page->name);\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $page->slug . '.txt');\n    }\n\n    public function test_page_pdf_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $page = $this->entities->page();\n\n        $resp = $this->get(\"/api/pages/{$page->id}/export/pdf\");\n        $resp->assertStatus(200);\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $page->slug . '.pdf');\n    }\n\n    public function test_page_markdown_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $page = $this->entities->page();\n\n        $resp = $this->get(\"/api/pages/{$page->id}/export/markdown\");\n        $resp->assertStatus(200);\n        $resp->assertSee('# ' . $page->name);\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $page->slug . '.md');\n    }\n\n    public function test_page_zip_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $page = $this->entities->page();\n\n        $resp = $this->get(\"/api/pages/{$page->id}/export/zip\");\n        $resp->assertStatus(200);\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $page->slug . '.zip');\n\n        $zip = ZipTestHelper::extractFromZipResponse($resp);\n        $this->assertArrayHasKey('page', $zip->data);\n    }\n\n    public function test_cant_export_when_not_have_permission()\n    {\n        $types = ['html', 'plaintext', 'pdf', 'markdown', 'zip'];\n        $this->actingAsApiEditor();\n        $this->permissions->removeUserRolePermissions($this->users->editor(), ['content-export']);\n\n        $book = $this->entities->book();\n        foreach ($types as $type) {\n            $resp = $this->get(\"/api/books/{$book->id}/export/{$type}\");\n            $this->assertPermissionError($resp);\n        }\n\n        $chapter = Chapter::visible()->has('pages')->first();\n        foreach ($types as $type) {\n            $resp = $this->get(\"/api/chapters/{$chapter->id}/export/{$type}\");\n            $this->assertPermissionError($resp);\n        }\n\n        $page = $this->entities->page();\n        foreach ($types as $type) {\n            $resp = $this->get(\"/api/pages/{$page->id}/export/{$type}\");\n            $this->assertPermissionError($resp);\n        }\n    }\n}\n"
  },
  {
    "path": "tests/Api/ImageGalleryApiTest.php",
    "content": "<?php\n\nnamespace Tests\\Api;\n\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Uploads\\Image;\nuse Tests\\TestCase;\n\nclass ImageGalleryApiTest extends TestCase\n{\n    use TestsApi;\n\n    protected string $baseEndpoint = '/api/image-gallery';\n\n    public function test_index_endpoint_returns_expected_image_and_count()\n    {\n        $this->actingAsApiAdmin();\n        $imagePage = $this->entities->page();\n        $data = $this->files->uploadGalleryImageToPage($this, $imagePage);\n        $image = Image::findOrFail($data['response']->id);\n\n        $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');\n        $resp->assertJson(['data' => [\n            [\n                'id' => $image->id,\n                'name' => $image->name,\n                'url' => $image->url,\n                'path' => $image->path,\n                'type' => 'gallery',\n                'uploaded_to' => $imagePage->id,\n                'created_by' => $this->users->admin()->id,\n                'updated_by' => $this->users->admin()->id,\n            ],\n        ]]);\n\n        $resp->assertJson(['total' => Image::query()->count()]);\n    }\n\n    public function test_index_endpoint_doesnt_show_images_for_those_uploaded_to_non_visible_pages()\n    {\n        $this->actingAsApiEditor();\n        $imagePage = $this->entities->page();\n        $data = $this->files->uploadGalleryImageToPage($this, $imagePage);\n        $image = Image::findOrFail($data['response']->id);\n\n        $resp = $this->getJson($this->baseEndpoint . '?filter[id]=' . $image->id);\n        $resp->assertJsonCount(1, 'data');\n        $resp->assertJson(['total' => 1]);\n\n        $this->permissions->disableEntityInheritedPermissions($imagePage);\n\n        $resp = $this->getJson($this->baseEndpoint . '?filter[id]=' . $image->id);\n        $resp->assertJsonCount(0, 'data');\n        $resp->assertJson(['total' => 0]);\n    }\n\n    public function test_index_endpoint_doesnt_show_other_image_types()\n    {\n        $this->actingAsApiEditor();\n        $imagePage = $this->entities->page();\n        $data = $this->files->uploadGalleryImageToPage($this, $imagePage);\n        $image = Image::findOrFail($data['response']->id);\n\n        $typesByCountExpectation = [\n            'cover_book' => 0,\n            'drawio' => 1,\n            'gallery' => 1,\n            'user' => 0,\n            'system' => 0,\n        ];\n\n        foreach ($typesByCountExpectation as $type => $count) {\n            $image->type = $type;\n            $image->save();\n\n            $resp = $this->getJson($this->baseEndpoint . '?filter[id]=' . $image->id);\n            $resp->assertJsonCount($count, 'data');\n            $resp->assertJson(['total' => $count]);\n        }\n    }\n\n    public function test_create_endpoint()\n    {\n        $this->actingAsApiAdmin();\n\n        $imagePage = $this->entities->page();\n        $resp = $this->call('POST', $this->baseEndpoint, [\n            'type' => 'gallery',\n            'uploaded_to' => $imagePage->id,\n            'name' => 'My awesome image!',\n        ], [], [\n            'image' => $this->files->uploadedImage('my-cool-image.png'),\n        ]);\n\n        $resp->assertStatus(200);\n\n        $image = Image::query()->where('uploaded_to', '=', $imagePage->id)->first();\n        $expectedUser = [\n            'id' => $this->users->admin()->id,\n            'name' => $this->users->admin()->name,\n            'slug' => $this->users->admin()->slug,\n        ];\n        $resp->assertJson([\n            'id' => $image->id,\n            'name' => 'My awesome image!',\n            'url' => $image->url,\n            'path' => $image->path,\n            'type' => 'gallery',\n            'uploaded_to' => $imagePage->id,\n            'created_by' => $expectedUser,\n            'updated_by' => $expectedUser,\n        ]);\n    }\n\n    public function test_create_endpoint_requires_image_create_permissions()\n    {\n        $user = $this->users->editor();\n        $this->actingAsForApi($user);\n        $this->permissions->removeUserRolePermissions($user, ['image-create-all']);\n\n        $makeRequest = function () {\n            return $this->call('POST', $this->baseEndpoint, []);\n        };\n\n        $resp = $makeRequest();\n        $resp->assertStatus(403);\n\n        $this->permissions->grantUserRolePermissions($user, ['image-create-all']);\n\n        $resp = $makeRequest();\n        $resp->assertStatus(422);\n    }\n\n    public function test_create_fails_if_uploaded_to_not_visible_or_not_exists()\n    {\n        $this->actingAsApiEditor();\n\n        $makeRequest = function (int $uploadedTo) {\n            return $this->call('POST', $this->baseEndpoint, [\n                'type' => 'gallery',\n                'uploaded_to' => $uploadedTo,\n                'name' => 'My awesome image!',\n            ], [], [\n                'image' => $this->files->uploadedImage('my-cool-image.png'),\n            ]);\n        };\n\n        $page = $this->entities->page();\n        $this->permissions->disableEntityInheritedPermissions($page);\n        $resp = $makeRequest($page->id);\n        $resp->assertStatus(404);\n\n        $resp = $makeRequest(Page::query()->max('id') + 55);\n        $resp->assertStatus(404);\n    }\n\n    public function test_create_has_restricted_types()\n    {\n        $this->actingAsApiEditor();\n\n        $typesByStatusExpectation = [\n            'cover_book' => 422,\n            'drawio' => 200,\n            'gallery' => 200,\n            'user' => 422,\n            'system' => 422,\n        ];\n\n        $makeRequest = function (string $type) {\n            return $this->call('POST', $this->baseEndpoint, [\n                'type' => $type,\n                'uploaded_to' => $this->entities->page()->id,\n                'name' => 'My awesome image!',\n            ], [], [\n                'image' => $this->files->uploadedImage('my-cool-image.png'),\n            ]);\n        };\n\n        foreach ($typesByStatusExpectation as $type => $status) {\n            $resp = $makeRequest($type);\n            $resp->assertStatus($status);\n        }\n    }\n\n    public function test_create_will_use_file_name_if_no_name_provided_in_request()\n    {\n        $this->actingAsApiEditor();\n\n        $imagePage = $this->entities->page();\n        $resp = $this->call('POST', $this->baseEndpoint, [\n            'type' => 'gallery',\n            'uploaded_to' => $imagePage->id,\n        ], [], [\n            'image' => $this->files->uploadedImage('my-cool-image.png'),\n        ]);\n        $resp->assertStatus(200);\n\n        $this->assertDatabaseHas('images', [\n            'type' => 'gallery',\n            'uploaded_to' => $imagePage->id,\n            'name' => 'my-cool-image.png',\n        ]);\n    }\n\n    public function test_read_endpoint()\n    {\n        $this->actingAsApiAdmin();\n        $imagePage = $this->entities->page();\n        $data = $this->files->uploadGalleryImageToPage($this, $imagePage);\n        $image = Image::findOrFail($data['response']->id);\n\n        $resp = $this->getJson($this->baseEndpoint . \"/{$image->id}\");\n        $resp->assertStatus(200);\n\n        $expectedUser = [\n            'id' => $this->users->admin()->id,\n            'name' => $this->users->admin()->name,\n            'slug' => $this->users->admin()->slug,\n        ];\n\n        $displayUrl = $image->getThumb(1680, null, true);\n        $resp->assertJson([\n            'id' => $image->id,\n            'name' => $image->name,\n            'url' => $image->url,\n            'path' => $image->path,\n            'type' => 'gallery',\n            'uploaded_to' => $imagePage->id,\n            'created_by' => $expectedUser,\n            'updated_by' => $expectedUser,\n            'content' => [\n                'html' => \"<a href=\\\"{$image->url}\\\" target=\\\"_blank\\\"><img src=\\\"{$displayUrl}\\\" alt=\\\"{$image->name}\\\"></a>\",\n                'markdown' => \"![{$image->name}]({$displayUrl})\",\n            ],\n            'created_at' => $image->created_at->toISOString(),\n            'updated_at' => $image->updated_at->toISOString(),\n        ]);\n        $this->assertStringStartsWith('http://', $resp->json('thumbs.gallery'));\n        $this->assertStringStartsWith('http://', $resp->json('thumbs.display'));\n    }\n\n    public function test_read_endpoint_provides_different_content_for_drawings()\n    {\n        $this->actingAsApiAdmin();\n        $imagePage = $this->entities->page();\n        $data = $this->files->uploadGalleryImageToPage($this, $imagePage);\n        $image = Image::findOrFail($data['response']->id);\n\n        $image->type = 'drawio';\n        $image->save();\n\n        $resp = $this->getJson($this->baseEndpoint . \"/{$image->id}\");\n        $resp->assertStatus(200);\n\n        $drawing = \"<div drawio-diagram=\\\"{$image->id}\\\"><img src=\\\"{$image->url}\\\"></div>\";\n        $resp->assertJson([\n            'id' => $image->id,\n            'content' => [\n                'html' => $drawing,\n                'markdown' => $drawing,\n            ],\n        ]);\n    }\n\n    public function test_read_endpoint_does_not_show_if_no_permissions_for_related_page()\n    {\n        $this->actingAsApiEditor();\n        $imagePage = $this->entities->page();\n        $data = $this->files->uploadGalleryImageToPage($this, $imagePage);\n        $image = Image::findOrFail($data['response']->id);\n\n        $this->permissions->disableEntityInheritedPermissions($imagePage);\n\n        $resp = $this->getJson($this->baseEndpoint . \"/{$image->id}\");\n        $resp->assertStatus(404);\n    }\n\n    public function test_read_data_endpoint()\n    {\n        $this->actingAsApiAdmin();\n        $imagePage = $this->entities->page();\n        $data = $this->files->uploadGalleryImageToPage($this, $imagePage, 'test-image.png');\n        $image = Image::findOrFail($data['response']->id);\n\n        $resp = $this->get(\"{$this->baseEndpoint}/{$image->id}/data\");\n        $resp->assertStatus(200);\n        $resp->assertHeader('Content-Type', 'image/png');\n\n        $respData = $resp->streamedContent();\n        $this->assertEquals(file_get_contents($this->files->testFilePath('test-image.png')), $respData);\n    }\n\n    public function test_read_data_endpoint_permission_controlled()\n    {\n        $this->actingAsApiEditor();\n        $imagePage = $this->entities->page();\n        $data = $this->files->uploadGalleryImageToPage($this, $imagePage, 'test-image.png');\n        $image = Image::findOrFail($data['response']->id);\n\n        $this->get(\"{$this->baseEndpoint}/{$image->id}/data\")->assertOk();\n\n        $this->permissions->disableEntityInheritedPermissions($imagePage);\n\n        $resp = $this->get(\"{$this->baseEndpoint}/{$image->id}/data\");\n        $resp->assertStatus(404);\n    }\n\n    public function test_read_url_data_endpoint()\n    {\n        $this->actingAsApiAdmin();\n        $imagePage = $this->entities->page();\n        $data = $this->files->uploadGalleryImageToPage($this, $imagePage, 'test-image.png');\n\n        $url = url($data['response']->path);\n        $resp = $this->get(\"{$this->baseEndpoint}/url/data?url=\" . urlencode($url));\n        $resp->assertStatus(200);\n        $resp->assertHeader('Content-Type', 'image/png');\n\n        $respData = $resp->streamedContent();\n        $this->assertEquals(file_get_contents($this->files->testFilePath('test-image.png')), $respData);\n    }\n\n    public function test_read_url_data_endpoint_permission_controlled_when_local_secure_restricted_storage_is_used()\n    {\n        config()->set('filesystems.images', 'local_secure_restricted');\n\n        $this->actingAsApiEditor();\n        $imagePage = $this->entities->page();\n        $data = $this->files->uploadGalleryImageToPage($this, $imagePage, 'test-image.png');\n\n        $url = url($data['response']->path);\n        $resp = $this->get(\"{$this->baseEndpoint}/url/data?url=\" . urlencode($url));\n        $resp->assertStatus(200);\n\n        $this->permissions->disableEntityInheritedPermissions($imagePage);\n\n        $resp = $this->get(\"{$this->baseEndpoint}/url/data?url=\" . urlencode($url));\n        $resp->assertStatus(404);\n    }\n\n    public function test_update_endpoint()\n    {\n        $this->actingAsApiAdmin();\n        $imagePage = $this->entities->page();\n        $data = $this->files->uploadGalleryImageToPage($this, $imagePage);\n        $image = Image::findOrFail($data['response']->id);\n\n        $resp = $this->putJson($this->baseEndpoint . \"/{$image->id}\", [\n            'name' => 'My updated image name!',\n        ]);\n\n        $resp->assertStatus(200);\n        $resp->assertJson([\n            'id' => $image->id,\n            'name' => 'My updated image name!',\n        ]);\n        $this->assertDatabaseHas('images', [\n            'id' => $image->id,\n            'name' => 'My updated image name!',\n        ]);\n    }\n\n    public function test_update_existing_image_file()\n    {\n        $this->actingAsApiAdmin();\n        $imagePage = $this->entities->page();\n        $data = $this->files->uploadGalleryImageToPage($this, $imagePage);\n        $image = Image::findOrFail($data['response']->id);\n\n        $this->assertFileEquals($this->files->testFilePath('test-image.png'), public_path($data['path']));\n\n        $resp = $this->call('PUT', $this->baseEndpoint . \"/{$image->id}\", [], [], [\n            'image' => $this->files->uploadedImage('my-cool-image.png', 'compressed.png'),\n        ]);\n\n        $resp->assertStatus(200);\n        $this->assertFileEquals($this->files->testFilePath('compressed.png'), public_path($data['path']));\n    }\n\n    public function test_update_endpoint_requires_image_update_permission()\n    {\n        $user = $this->users->editor();\n        $this->actingAsForApi($user);\n        $imagePage = $this->entities->page();\n        $this->permissions->removeUserRolePermissions($user, ['image-update-all', 'image-update-own']);\n        $data = $this->files->uploadGalleryImageToPage($this, $imagePage);\n        $image = Image::findOrFail($data['response']->id);\n\n        $resp = $this->putJson($this->baseEndpoint . \"/{$image->id}\", ['name' => 'My new name']);\n        $resp->assertStatus(403);\n        $resp->assertJson($this->permissionErrorResponse());\n\n        $this->permissions->grantUserRolePermissions($user, ['image-update-all']);\n        $resp = $this->putJson($this->baseEndpoint . \"/{$image->id}\", ['name' => 'My new name']);\n        $resp->assertStatus(200);\n    }\n\n    public function test_delete_endpoint()\n    {\n        $this->actingAsApiAdmin();\n        $imagePage = $this->entities->page();\n        $data = $this->files->uploadGalleryImageToPage($this, $imagePage);\n        $image = Image::findOrFail($data['response']->id);\n        $this->assertDatabaseHas('images', ['id' => $image->id]);\n\n        $resp = $this->deleteJson($this->baseEndpoint . \"/{$image->id}\");\n\n        $resp->assertStatus(204);\n        $this->assertDatabaseMissing('images', ['id' => $image->id]);\n    }\n\n    public function test_delete_endpoint_requires_image_delete_permission()\n    {\n        $user = $this->users->editor();\n        $this->actingAsForApi($user);\n        $imagePage = $this->entities->page();\n        $this->permissions->removeUserRolePermissions($user, ['image-delete-all', 'image-delete-own']);\n        $data = $this->files->uploadGalleryImageToPage($this, $imagePage);\n        $image = Image::findOrFail($data['response']->id);\n\n        $resp = $this->deleteJson($this->baseEndpoint . \"/{$image->id}\");\n        $resp->assertStatus(403);\n        $resp->assertJson($this->permissionErrorResponse());\n\n        $this->permissions->grantUserRolePermissions($user, ['image-delete-all']);\n        $resp = $this->deleteJson($this->baseEndpoint . \"/{$image->id}\");\n        $resp->assertStatus(204);\n    }\n}\n"
  },
  {
    "path": "tests/Api/ImportsApiTest.php",
    "content": "<?php\n\nnamespace Tests\\Api;\n\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Exports\\Import;\nuse Tests\\Exports\\ZipTestHelper;\nuse Tests\\TestCase;\n\nclass ImportsApiTest extends TestCase\n{\n    use TestsApi;\n\n    protected string $baseEndpoint = '/api/imports';\n\n    public function test_create_and_run(): void\n    {\n        $book = $this->entities->book();\n        $zip = ZipTestHelper::zipUploadFromData([\n            'page' => [\n                'name' => 'My API import page',\n                'tags' => [\n                    [\n                        'name' => 'My api tag',\n                        'value' => 'api test value'\n                    ]\n                ],\n            ],\n        ]);\n\n        $resp = $this->actingAsApiAdmin()->call('POST', $this->baseEndpoint, [], [], ['file' => $zip]);\n        $resp->assertStatus(200);\n\n        $importId = $resp->json('id');\n        $import = Import::query()->findOrFail($importId);\n        $this->assertEquals('page', $import->type);\n\n        $resp = $this->post($this->baseEndpoint . \"/{$import->id}\", [\n            'parent_type' => 'book',\n            'parent_id' => $book->id,\n        ]);\n        $resp->assertJson([\n            'name' => 'My API import page',\n            'book_id' => $book->id,\n        ]);\n        $resp->assertJsonMissingPath('book');\n\n        $page = Page::query()->where('name', '=', 'My API import page')->first();\n        $this->assertEquals('My api tag', $page->tags()->first()->name);\n    }\n\n    public function test_create_validation_error(): void\n    {\n        $zip = ZipTestHelper::zipUploadFromData([\n            'page' => [\n                'tags' => [\n                    [\n                        'name' => 'My api tag',\n                        'value' => 'api test value'\n                    ]\n                ],\n            ],\n        ]);\n\n        $resp = $this->actingAsApiAdmin()->call('POST', $this->baseEndpoint, [], [], ['file' => $zip]);\n        $resp->assertStatus(422);\n        $message = $resp->json('message');\n\n        $this->assertStringContainsString('ZIP upload failed with the following validation errors:', $message);\n        $this->assertStringContainsString('[page.name] The name field is required.', $message);\n    }\n\n    public function test_list(): void\n    {\n        $imports = Import::factory()->count(10)->create();\n\n        $resp = $this->actingAsApiAdmin()->get($this->baseEndpoint);\n        $resp->assertJsonCount(10, 'data');\n        $resp->assertJsonPath('total', 10);\n\n        $firstImport = $imports->first();\n        $resp = $this->actingAsApiAdmin()->get($this->baseEndpoint . '?filter[id]=' . $firstImport->id);\n        $resp->assertJsonCount(1, 'data');\n        $resp->assertJsonPath('data.0.id', $firstImport->id);\n        $resp->assertJsonPath('data.0.name', $firstImport->name);\n        $resp->assertJsonPath('data.0.size', $firstImport->size);\n        $resp->assertJsonPath('data.0.type', $firstImport->type);\n    }\n\n    public function test_list_visibility_limited(): void\n    {\n        $user = $this->users->editor();\n        $admin = $this->users->admin();\n        $userImport = Import::factory()->create(['name' => 'MySuperUserImport', 'created_by' => $user->id]);\n        $adminImport = Import::factory()->create(['name' => 'MySuperAdminImport', 'created_by' => $admin->id]);\n        $this->permissions->grantUserRolePermissions($user, ['content-import']);\n\n        $resp = $this->actingAsForApi($user)->get($this->baseEndpoint);\n        $resp->assertJsonCount(1, 'data');\n        $resp->assertJsonPath('data.0.name', 'MySuperUserImport');\n\n        $this->permissions->grantUserRolePermissions($user, ['settings-manage']);\n\n        $resp = $this->actingAsForApi($user)->get($this->baseEndpoint);\n        $resp->assertJsonCount(2, 'data');\n        $resp->assertJsonPath('data.1.name', 'MySuperAdminImport');\n    }\n\n    public function test_read(): void\n    {\n        $zip = ZipTestHelper::zipUploadFromData([\n            'book' => [\n                'name' => 'My API import book',\n                'pages' => [\n                    [\n                        'name' => 'My import page',\n                        'tags' => [\n                            [\n                                'name' => 'My api tag',\n                                'value' => 'api test value'\n                            ]\n                        ]\n                    ]\n                ],\n            ],\n        ]);\n\n        $resp = $this->actingAsApiAdmin()->call('POST', $this->baseEndpoint, [], [], ['file' => $zip]);\n        $resp->assertStatus(200);\n\n        $resp = $this->get($this->baseEndpoint . \"/{$resp->json('id')}\");\n        $resp->assertStatus(200);\n\n        $resp->assertJsonPath('details.name', 'My API import book');\n        $resp->assertJsonPath('details.pages.0.name', 'My import page');\n        $resp->assertJsonPath('details.pages.0.tags.0.name', 'My api tag');\n        $resp->assertJsonMissingPath('metadata');\n    }\n\n    public function test_delete(): void\n    {\n        $import = Import::factory()->create();\n\n        $resp = $this->actingAsApiAdmin()->delete($this->baseEndpoint . \"/{$import->id}\");\n        $resp->assertStatus(204);\n    }\n\n    public function test_content_import_permissions_needed(): void\n    {\n        $user = $this->users->viewer();\n        $this->permissions->grantUserRolePermissions($user, ['access-api']);\n        $this->actingAsForApi($user);\n        $requests = [\n             ['GET', $this->baseEndpoint],\n             ['POST', $this->baseEndpoint],\n             ['GET', $this->baseEndpoint . \"/1\"],\n             ['POST', $this->baseEndpoint . \"/1\"],\n             ['DELETE', $this->baseEndpoint . \"/1\"],\n        ];\n\n        foreach ($requests as $request) {\n            [$method, $endpoint] = $request;\n            $resp = $this->json($method, $endpoint);\n            $resp->assertStatus(403);\n        }\n\n        $this->permissions->grantUserRolePermissions($user, ['content-import']);\n\n        foreach ($requests as $request) {\n            [$method, $endpoint] = $request;\n            $resp = $this->call($method, $endpoint);\n            $this->assertNotEquals(403, $resp->status(), \"A {$method} request to {$endpoint} returned 403\");\n        }\n    }\n}\n"
  },
  {
    "path": "tests/Api/PagesApiTest.php",
    "content": "<?php\n\nnamespace Tests\\Api;\n\nuse BookStack\\Activity\\Models\\Comment;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Page;\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\DB;\nuse Tests\\TestCase;\n\nclass PagesApiTest extends TestCase\n{\n    use TestsApi;\n\n    protected string $baseEndpoint = '/api/pages';\n\n    public function test_index_endpoint_returns_expected_page()\n    {\n        $this->actingAsApiEditor();\n        $firstPage = Page::query()->orderBy('id', 'asc')->first();\n\n        $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');\n        $resp->assertJson(['data' => [\n            [\n                'id'       => $firstPage->id,\n                'name'     => $firstPage->name,\n                'slug'     => $firstPage->slug,\n                'book_id'  => $firstPage->book->id,\n                'priority' => $firstPage->priority,\n                'owned_by'   => $firstPage->owned_by,\n                'created_by' => $firstPage->created_by,\n                'updated_by' => $firstPage->updated_by,\n                'revision_count' => $firstPage->revision_count,\n            ],\n        ]]);\n    }\n\n    public function test_create_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $book = $this->entities->book();\n        $details = [\n            'name'    => 'My API page',\n            'book_id' => $book->id,\n            'html'    => '<p>My new page content</p>',\n            'tags'    => [\n                [\n                    'name'  => 'tagname',\n                    'value' => 'tagvalue',\n                ],\n            ],\n            'priority' => 15,\n        ];\n\n        $resp = $this->postJson($this->baseEndpoint, $details);\n        unset($details['html']);\n        $resp->assertStatus(200);\n        $newItem = Page::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();\n        $resp->assertJson(array_merge($details, ['id' => $newItem->id, 'slug' => $newItem->slug]));\n        $this->assertDatabaseHas('tags', [\n            'entity_id'   => $newItem->id,\n            'entity_type' => $newItem->getMorphClass(),\n            'name'        => 'tagname',\n            'value'       => 'tagvalue',\n        ]);\n        $resp->assertSeeText('My new page content');\n        $resp->assertJsonMissing(['book' => []]);\n        $this->assertActivityExists('page_create', $newItem);\n    }\n\n    public function test_page_name_needed_to_create()\n    {\n        $this->actingAsApiEditor();\n        $book = $this->entities->book();\n        $details = [\n            'book_id' => $book->id,\n            'html'    => '<p>A page created via the API</p>',\n        ];\n\n        $resp = $this->postJson($this->baseEndpoint, $details);\n        $resp->assertStatus(422);\n        $resp->assertJson($this->validationResponse([\n            'name' => ['The name field is required.'],\n        ]));\n    }\n\n    public function test_book_id_or_chapter_id_needed_to_create()\n    {\n        $this->actingAsApiEditor();\n        $details = [\n            'name' => 'My api page',\n            'html' => '<p>A page created via the API</p>',\n        ];\n\n        $resp = $this->postJson($this->baseEndpoint, $details);\n        $resp->assertStatus(422);\n        $resp->assertJson($this->validationResponse([\n            'book_id'    => ['The book id field is required when chapter id is not present.'],\n            'chapter_id' => ['The chapter id field is required when book id is not present.'],\n        ]));\n\n        $chapter = $this->entities->chapter();\n        $resp = $this->postJson($this->baseEndpoint, array_merge($details, ['chapter_id' => $chapter->id]));\n        $resp->assertStatus(200);\n\n        $book = $this->entities->book();\n        $resp = $this->postJson($this->baseEndpoint, array_merge($details, ['book_id' => $book->id]));\n        $resp->assertStatus(200);\n    }\n\n    public function test_markdown_can_be_provided_for_create()\n    {\n        $this->actingAsApiEditor();\n        $book = $this->entities->book();\n        $details = [\n            'book_id'  => $book->id,\n            'name'     => 'My api page',\n            'markdown' => \"# A new API page \\n[link](https://example.com)\",\n        ];\n\n        $resp = $this->postJson($this->baseEndpoint, $details);\n        $resp->assertJson(['markdown' => $details['markdown']]);\n\n        $respHtml = $resp->json('html');\n        $this->assertStringContainsString('new API page</h1>', $respHtml);\n        $this->assertStringContainsString('link</a>', $respHtml);\n        $this->assertStringContainsString('href=\"https://example.com\"', $respHtml);\n    }\n\n    public function test_read_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $page = $this->entities->page();\n\n        $resp = $this->getJson($this->baseEndpoint . \"/{$page->id}\");\n        $resp->assertStatus(200);\n        $resp->assertJson([\n            'id'         => $page->id,\n            'slug'       => $page->slug,\n            'created_by' => [\n                'name' => $page->createdBy->name,\n            ],\n            'book_id'    => $page->book_id,\n            'updated_by' => [\n                'name' => $page->createdBy->name,\n            ],\n            'owned_by' => [\n                'name' => $page->ownedBy->name,\n            ],\n        ]);\n    }\n\n    public function test_read_endpoint_provides_rendered_html()\n    {\n        $this->actingAsApiEditor();\n        $page = $this->entities->page();\n        $page->html = \"<p>testing</p><script>alert('danger')</script><h1>Hello</h1>\";\n        $page->save();\n\n        $resp = $this->getJson($this->baseEndpoint . \"/{$page->id}\");\n        $html = $resp->json('html');\n        $this->assertStringNotContainsString('script', $html);\n        $this->assertStringContainsString('Hello', $html);\n        $this->assertStringContainsString('testing', $html);\n    }\n\n    public function test_read_endpoint_provides_raw_html()\n    {\n        $html = \"<p>testing</p><script>alert('danger')</script><h1>Hello</h1>\";\n\n        $this->actingAsApiEditor();\n        $page = $this->entities->page();\n        $page->html = $html;\n        $page->save();\n\n        $resp = $this->getJson($this->baseEndpoint . \"/{$page->id}\");\n        $this->assertEquals($html, $resp->json('raw_html'));\n        $this->assertNotEquals($html, $resp->json('html'));\n    }\n\n    public function test_read_endpoint_returns_not_found()\n    {\n        $this->actingAsApiEditor();\n        // get an id that is not used\n        $id = Page::orderBy('id', 'desc')->first()->id + 1;\n        $this->assertNull(Page::find($id));\n\n        $resp = $this->getJson($this->baseEndpoint . \"/$id\");\n\n        $resp->assertNotFound();\n        $this->assertNull($resp->json('id'));\n        $resp->assertJsonIsObject('error');\n        $resp->assertJsonStructure([\n            'error' => [\n                'code',\n                'message',\n            ],\n        ]);\n        $this->assertSame(404, $resp->json('error')['code']);\n    }\n\n    public function test_read_endpoint_includes_page_comments_tree_structure()\n    {\n        $this->actingAsApiEditor();\n        $page = $this->entities->page();\n        $relation = ['commentable_type' => 'page', 'commentable_id' => $page->id];\n        $active = Comment::factory()->create([...$relation, 'html' => '<p>My active<script>cat</script> comment</p>']);\n        Comment::factory()->count(5)->create([...$relation, 'parent_id' => $active->local_id]);\n        $archived = Comment::factory()->create([...$relation, 'archived' => true]);\n        Comment::factory()->count(2)->create([...$relation, 'parent_id' => $archived->local_id]);\n\n        $resp = $this->getJson(\"{$this->baseEndpoint}/{$page->id}\");\n        $resp->assertOk();\n\n        $resp->assertJsonCount(1, 'comments.active');\n        $resp->assertJsonCount(1, 'comments.archived');\n        $resp->assertJsonCount(5, 'comments.active.0.children');\n        $resp->assertJsonCount(2, 'comments.archived.0.children');\n\n        $resp->assertJsonFragment([\n            'id' => $active->id,\n            'local_id' => $active->local_id,\n            'html' => '<p>My active comment</p>',\n        ]);\n    }\n\n    public function test_update_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $page = $this->entities->page();\n        $details = [\n            'name' => 'My updated API page',\n            'html' => '<p>A page created via the API</p>',\n            'tags' => [\n                [\n                    'name'  => 'freshtag',\n                    'value' => 'freshtagval',\n                ],\n            ],\n            'priority' => 15,\n        ];\n\n        $resp = $this->putJson($this->baseEndpoint . \"/{$page->id}\", $details);\n        $page->refresh();\n\n        $resp->assertStatus(200);\n        unset($details['html']);\n        $resp->assertJson(array_merge($details, [\n            'id' => $page->id, 'slug' => $page->slug, 'book_id' => $page->book_id,\n        ]));\n        $this->assertActivityExists('page_update', $page);\n    }\n\n    public function test_providing_new_chapter_id_on_update_will_move_page()\n    {\n        $this->actingAsApiEditor();\n        $page = $this->entities->page();\n        $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first();\n        $details = [\n            'name'       => 'My updated API page',\n            'chapter_id' => $chapter->id,\n            'html'       => '<p>A page created via the API</p>',\n        ];\n\n        $resp = $this->putJson($this->baseEndpoint . \"/{$page->id}\", $details);\n        $resp->assertStatus(200);\n        $resp->assertJson([\n            'chapter_id' => $chapter->id,\n            'book_id'    => $chapter->book_id,\n        ]);\n    }\n\n    public function test_providing_move_via_update_requires_page_create_permission_on_new_parent()\n    {\n        $this->actingAsApiEditor();\n        $page = $this->entities->page();\n        $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first();\n        $this->permissions->setEntityPermissions($chapter, ['view'], [$this->users->editor()->roles()->first()]);\n        $details = [\n            'name'       => 'My updated API page',\n            'chapter_id' => $chapter->id,\n            'html'       => '<p>A page created via the API</p>',\n        ];\n\n        $resp = $this->putJson($this->baseEndpoint . \"/{$page->id}\", $details);\n        $resp->assertStatus(403);\n    }\n\n    public function test_update_endpoint_does_not_wipe_content_if_no_html_or_md_provided()\n    {\n        $this->actingAsApiEditor();\n        $page = $this->entities->page();\n        $originalContent = $page->html;\n        $details = [\n            'name' => 'My updated API page',\n            'tags' => [\n                [\n                    'name'  => 'freshtag',\n                    'value' => 'freshtagval',\n                ],\n            ],\n        ];\n\n        $this->putJson($this->baseEndpoint . \"/{$page->id}\", $details);\n        $page->refresh();\n\n        $this->assertEquals($originalContent, $page->html);\n    }\n\n    public function test_update_increments_updated_date_if_only_tags_are_sent()\n    {\n        $this->actingAsApiEditor();\n        $page = $this->entities->page();\n        $page->newQuery()->where('id', '=', $page->id)->update(['updated_at' => Carbon::now()->subWeek()]);\n\n        $details = [\n            'tags' => [['name' => 'Category', 'value' => 'Testing']],\n        ];\n\n        $resp = $this->putJson($this->baseEndpoint . \"/{$page->id}\", $details);\n        $resp->assertOk();\n\n        $page->refresh();\n        $this->assertGreaterThan(Carbon::now()->subDay()->unix(), $page->updated_at->unix());\n    }\n\n    public function test_delete_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $page = $this->entities->page();\n        $resp = $this->deleteJson($this->baseEndpoint . \"/{$page->id}\");\n\n        $resp->assertStatus(204);\n        $this->assertActivityExists('page_delete', $page);\n    }\n}\n"
  },
  {
    "path": "tests/Api/RecycleBinApiTest.php",
    "content": "<?php\n\nnamespace Tests\\Api;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Deletion;\nuse Illuminate\\Support\\Collection;\nuse Tests\\TestCase;\n\nclass RecycleBinApiTest extends TestCase\n{\n    use TestsApi;\n\n    protected string $baseEndpoint = '/api/recycle-bin';\n\n    protected array $endpointMap = [\n        ['get', '/api/recycle-bin'],\n        ['put', '/api/recycle-bin/1'],\n        ['delete', '/api/recycle-bin/1'],\n    ];\n\n    public function test_settings_manage_permission_needed_for_all_endpoints()\n    {\n        $editor = $this->users->editor();\n        $this->permissions->grantUserRolePermissions($editor, ['settings-manage']);\n        $this->actingAsForApi($editor);\n\n        foreach ($this->endpointMap as [$method, $uri]) {\n            $resp = $this->json($method, $uri);\n            $resp->assertStatus(403);\n            $resp->assertJson($this->permissionErrorResponse());\n        }\n    }\n\n    public function test_restrictions_manage_all_permission_needed_for_all_endpoints()\n    {\n        $editor = $this->users->editor();\n        $this->permissions->grantUserRolePermissions($editor, ['restrictions-manage-all']);\n        $this->actingAsForApi($editor);\n\n        foreach ($this->endpointMap as [$method, $uri]) {\n            $resp = $this->json($method, $uri);\n            $resp->assertStatus(403);\n            $resp->assertJson($this->permissionErrorResponse());\n        }\n    }\n\n    public function test_index_endpoint_returns_expected_page()\n    {\n        $admin = $this->users->admin();\n\n        $page = $this->entities->page();\n        $book = $this->entities->book();\n        $this->actingAs($admin)->delete($page->getUrl());\n        $this->delete($book->getUrl());\n        $this->actingAsForApi($admin);\n\n        $deletions = Deletion::query()->orderBy('id')->get();\n\n        $resp = $this->getJson($this->baseEndpoint);\n\n        $expectedData = $deletions\n            ->zip([$page, $book])\n            ->map(function (Collection $data) use ($admin) {\n                return [\n                    'id'                => $data[0]->id,\n                    'deleted_by'        => $admin->id,\n                    'created_at'        => $data[0]->created_at->toJson(),\n                    'updated_at'        => $data[0]->updated_at->toJson(),\n                    'deletable_type'    => $data[1]->getMorphClass(),\n                    'deletable_id'      => $data[1]->id,\n                    'deletable'         => [\n                        'name' => $data[1]->name,\n                    ],\n                ];\n            });\n\n        $resp->assertJson([\n            'data'  => $expectedData->values()->all(),\n            'total' => 2,\n        ]);\n    }\n\n    public function test_index_endpoint_returns_children_count()\n    {\n        $admin = $this->users->admin();\n\n        $book = Book::query()->whereHas('pages')->whereHas('chapters')->withCount(['pages', 'chapters'])->first();\n        $this->actingAs($admin)->delete($book->getUrl());\n\n        $deletion = Deletion::query()->orderBy('id')->first();\n\n        $resp = $this->actingAsForApi($admin)->getJson($this->baseEndpoint);\n\n        $expectedData = [\n            [\n                'id'             => $deletion->id,\n                'deletable'      => [\n                    'pages_count'    => $book->pages_count,\n                    'chapters_count' => $book->chapters_count,\n                ],\n            ],\n        ];\n\n        $resp->assertJson([\n            'data'  => $expectedData,\n            'total' => 1,\n        ]);\n    }\n\n    public function test_index_endpoint_returns_parent()\n    {\n        $admin = $this->users->admin();\n        $page = $this->entities->pageWithinChapter();\n\n        $this->actingAs($admin)->delete($page->getUrl());\n        $deletion = Deletion::query()->orderBy('id')->first();\n\n        $this->actingAsForApi($admin);\n        $resp = $this->getJson($this->baseEndpoint);\n\n        $expectedData = [\n            [\n                'id'             => $deletion->id,\n                'deletable'      => [\n                    'parent' => [\n                        'id'   => $page->chapter->id,\n                        'name' => $page->chapter->name,\n                        'type' => 'chapter',\n                    ],\n                ],\n            ],\n        ];\n\n        $resp->assertJson([\n            'data'  => $expectedData,\n            'total' => 1,\n        ]);\n    }\n\n    public function test_restore_endpoint()\n    {\n        $page = $this->entities->page();\n        $this->asAdmin()->delete($page->getUrl());\n        $page->refresh();\n        $this->actingAsApiAdmin();\n\n        $deletion = Deletion::query()->orderBy('id')->first();\n\n        $this->assertDatabaseHasEntityData('page', [\n            'id'            => $page->id,\n            'deleted_at'    => $page->deleted_at,\n        ]);\n\n        $resp = $this->putJson($this->baseEndpoint . '/' . $deletion->id);\n        $resp->assertJson([\n            'restore_count' => 1,\n        ]);\n\n        $this->assertDatabaseHasEntityData('page', [\n            'id'            => $page->id,\n            'deleted_at'    => null,\n        ]);\n    }\n\n    public function test_destroy_endpoint()\n    {\n        $page = $this->entities->page();\n        $this->asAdmin()->delete($page->getUrl());\n        $page->refresh();\n        $this->actingAsApiAdmin();\n\n        $deletion = Deletion::query()->orderBy('id')->first();\n\n        $this->assertDatabaseHasEntityData('page', [\n            'id'            => $page->id,\n            'deleted_at'    => $page->deleted_at,\n        ]);\n\n        $resp = $this->deleteJson($this->baseEndpoint . '/' . $deletion->id);\n        $resp->assertJson([\n            'delete_count' => 1,\n        ]);\n\n        $this->assertDatabaseMissing('entities', ['id' => $page->id, 'type' => 'page']);\n    }\n}\n"
  },
  {
    "path": "tests/Api/RolesApiTest.php",
    "content": "<?php\n\nnamespace Tests\\Api;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Models\\User;\nuse Tests\\TestCase;\n\nclass RolesApiTest extends TestCase\n{\n    use TestsApi;\n\n    protected string $baseEndpoint = '/api/roles';\n\n    protected array $endpointMap = [\n        ['get', '/api/roles'],\n        ['post', '/api/roles'],\n        ['get', '/api/roles/1'],\n        ['put', '/api/roles/1'],\n        ['delete', '/api/roles/1'],\n    ];\n\n    public function test_user_roles_manage_permission_needed_for_all_endpoints()\n    {\n        $this->actingAsApiEditor();\n        foreach ($this->endpointMap as [$method, $uri]) {\n            $resp = $this->json($method, $uri);\n            $resp->assertStatus(403);\n            $resp->assertJson($this->permissionErrorResponse());\n        }\n    }\n\n    public function test_index_endpoint_returns_expected_role_and_count()\n    {\n        $this->actingAsApiAdmin();\n        /** @var Role $firstRole */\n        $firstRole = Role::query()->orderBy('id', 'asc')->first();\n\n        $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');\n        $resp->assertJson(['data' => [\n            [\n                'id'                => $firstRole->id,\n                'display_name'      => $firstRole->display_name,\n                'description'       => $firstRole->description,\n                'mfa_enforced'      => $firstRole->mfa_enforced,\n                'external_auth_id'  => $firstRole->external_auth_id,\n                'permissions_count' => $firstRole->permissions()->count(),\n                'users_count'       => $firstRole->users()->count(),\n                'created_at'        => $firstRole->created_at->toJSON(),\n                'updated_at'        => $firstRole->updated_at->toJSON(),\n            ],\n        ]]);\n\n        $resp->assertJson(['total' => Role::query()->count()]);\n    }\n\n    public function test_create_endpoint()\n    {\n        $this->actingAsApiAdmin();\n        /** @var Role $role */\n        $role = Role::query()->first();\n\n        $resp = $this->postJson($this->baseEndpoint, [\n            'display_name' => 'My awesome role',\n            'description'  => 'My great role description',\n            'mfa_enforced' => true,\n            'external_auth_id' => 'auth_id',\n            'permissions'  => [\n                'content-export',\n                'page-view-all',\n                'page-view-own',\n                'users-manage',\n            ]\n        ]);\n\n        $resp->assertStatus(200);\n        $resp->assertJson([\n            'display_name' => 'My awesome role',\n            'description'  => 'My great role description',\n            'mfa_enforced' => true,\n            'external_auth_id' => 'auth_id',\n            'permissions'  => [\n                'content-export',\n                'page-view-all',\n                'page-view-own',\n                'users-manage',\n            ]\n        ]);\n\n        $this->assertDatabaseHas('roles', [\n            'display_name' => 'My awesome role',\n            'description'  => 'My great role description',\n            'mfa_enforced' => true,\n            'external_auth_id' => 'auth_id',\n        ]);\n\n        /** @var Role $role */\n        $role = Role::query()->where('display_name', '=', 'My awesome role')->first();\n        $this->assertActivityExists(ActivityType::ROLE_CREATE, null, $role->logDescriptor());\n        $this->assertEquals(4, $role->permissions()->count());\n    }\n\n    public function test_create_name_and_description_validation()\n    {\n        $this->actingAsApiAdmin();\n        /** @var User $existingUser */\n        $existingUser = User::query()->first();\n\n        $resp = $this->postJson($this->baseEndpoint, [\n            'description' => 'My new role',\n        ]);\n        $resp->assertStatus(422);\n        $resp->assertJson($this->validationResponse(['display_name' => ['The display name field is required.']]));\n\n        $resp = $this->postJson($this->baseEndpoint, [\n            'name' => 'My great role with a too long desc',\n            'description' => str_repeat('My great desc', 20),\n        ]);\n        $resp->assertStatus(422);\n        $resp->assertJson($this->validationResponse(['description' => ['The description may not be greater than 180 characters.']]));\n    }\n\n    public function test_read_endpoint()\n    {\n        $this->actingAsApiAdmin();\n        $role = $this->users->editor()->roles()->first();\n        $resp = $this->getJson($this->baseEndpoint . \"/{$role->id}\");\n\n        $resp->assertStatus(200);\n        $resp->assertJson([\n            'display_name' => $role->display_name,\n            'description'  => $role->description,\n            'mfa_enforced' => $role->mfa_enforced,\n            'external_auth_id' => $role->external_auth_id,\n            'permissions'  => $role->permissions()->orderBy('name', 'asc')->pluck('name')->toArray(),\n            'users' => $role->users()->get()->map(function (User $user) {\n                return [\n                    'id' => $user->id,\n                    'name' => $user->name,\n                    'slug' => $user->slug,\n                ];\n            })->toArray(),\n        ]);\n    }\n\n    public function test_update_endpoint()\n    {\n        $this->actingAsApiAdmin();\n        $role = $this->users->editor()->roles()->first();\n        $resp = $this->putJson($this->baseEndpoint . \"/{$role->id}\", [\n            'display_name' => 'My updated role',\n            'description'  => 'My great role description',\n            'mfa_enforced' => true,\n            'external_auth_id' => 'updated_auth_id',\n            'permissions'  => [\n                'content-export',\n                'page-view-all',\n                'page-view-own',\n                'users-manage',\n            ]\n        ]);\n\n        $resp->assertStatus(200);\n        $resp->assertJson([\n            'id'           => $role->id,\n            'display_name' => 'My updated role',\n            'description'  => 'My great role description',\n            'mfa_enforced' => true,\n            'external_auth_id' => 'updated_auth_id',\n            'permissions'  => [\n                'content-export',\n                'page-view-all',\n                'page-view-own',\n                'users-manage',\n            ]\n        ]);\n\n        $role->refresh();\n        $this->assertEquals(4, $role->permissions()->count());\n        $this->assertActivityExists(ActivityType::ROLE_UPDATE);\n    }\n\n    public function test_update_endpoint_does_not_remove_info_if_not_provided()\n    {\n        $this->actingAsApiAdmin();\n        $role = $this->users->editor()->roles()->first();\n        $resp = $this->putJson($this->baseEndpoint . \"/{$role->id}\", []);\n        $permissionCount = $role->permissions()->count();\n\n        $resp->assertStatus(200);\n        $this->assertDatabaseHas('roles', [\n            'id'           => $role->id,\n            'display_name' => $role->display_name,\n            'description'  => $role->description,\n            'external_auth_id' => $role->external_auth_id,\n        ]);\n\n        $role->refresh();\n        $this->assertEquals($permissionCount, $role->permissions()->count());\n    }\n\n    public function test_delete_endpoint()\n    {\n        $this->actingAsApiAdmin();\n        $role = $this->users->editor()->roles()->first();\n\n        $resp = $this->deleteJson($this->baseEndpoint . \"/{$role->id}\");\n\n        $resp->assertStatus(204);\n        $this->assertActivityExists(ActivityType::ROLE_DELETE, null, $role->logDescriptor());\n    }\n\n    public function test_delete_endpoint_fails_deleting_system_role()\n    {\n        $this->actingAsApiAdmin();\n        $adminRole = Role::getSystemRole('admin');\n\n        $resp = $this->deleteJson($this->baseEndpoint . \"/{$adminRole->id}\");\n\n        $resp->assertStatus(500);\n        $resp->assertJson($this->errorResponse('This role is a system role and cannot be deleted', 500));\n    }\n\n    public function test_delete_endpoint_fails_deleting_default_registration_role()\n    {\n        $this->actingAsApiAdmin();\n        $role = $this->users->attachNewRole($this->users->editor());\n        $this->setSettings(['registration-role' => $role->id]);\n\n        $resp = $this->deleteJson($this->baseEndpoint . \"/{$role->id}\");\n\n        $resp->assertStatus(500);\n        $resp->assertJson($this->errorResponse('This role cannot be deleted while set as the default registration role', 500));\n    }\n}\n"
  },
  {
    "path": "tests/Api/SearchApiTest.php",
    "content": "<?php\n\nnamespace Tests\\Api;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse Tests\\TestCase;\n\nclass SearchApiTest extends TestCase\n{\n    use TestsApi;\n\n    protected string $baseEndpoint = '/api/search';\n\n    public function test_all_endpoint_returns_search_filtered_results_with_query()\n    {\n        $this->actingAsApiEditor();\n        $uniqueTerm = 'MySuperUniqueTermForSearching';\n\n        /** @var Entity $entityClass */\n        foreach ([Page::class, Chapter::class, Book::class, Bookshelf::class] as $entityClass) {\n            /** @var Entity $first */\n            $first = $entityClass::query()->first();\n            $first->update(['name' => $uniqueTerm]);\n            $first->indexForSearch();\n        }\n\n        $resp = $this->getJson($this->baseEndpoint . '?query=' . $uniqueTerm . '&count=5&page=1');\n        $resp->assertJsonCount(4, 'data');\n        $resp->assertJsonFragment(['name' => $uniqueTerm, 'type' => 'book']);\n        $resp->assertJsonFragment(['name' => $uniqueTerm, 'type' => 'chapter']);\n        $resp->assertJsonFragment(['name' => $uniqueTerm, 'type' => 'page']);\n        $resp->assertJsonFragment(['name' => $uniqueTerm, 'type' => 'bookshelf']);\n    }\n\n    public function test_all_endpoint_returns_entity_url()\n    {\n        $page = $this->entities->page();\n        $page->update(['name' => 'name with superuniquevalue within']);\n        $page->indexForSearch();\n\n        $resp = $this->actingAsApiAdmin()->getJson($this->baseEndpoint . '?query=superuniquevalue');\n        $resp->assertJsonFragment([\n            'type' => 'page',\n            'url' => $page->getUrl(),\n        ]);\n    }\n\n    public function test_all_endpoint_returns_items_with_preview_html()\n    {\n        $book = $this->entities->book();\n        $book->forceFill(['name' => 'name with superuniquevalue within', 'description' => 'Description with superuniquevalue within'])->save();\n        $book->indexForSearch();\n\n        $resp = $this->actingAsApiAdmin()->getJson($this->baseEndpoint . '?query=superuniquevalue');\n        $resp->assertJsonFragment([\n            'type' => 'book',\n            'url' => $book->getUrl(),\n            'preview_html' => [\n                'name' => 'name with <strong>superuniquevalue</strong> within',\n                'content' => 'Description with <strong>superuniquevalue</strong> within',\n            ],\n        ]);\n    }\n\n    public function test_all_endpoint_requires_query_parameter()\n    {\n        $resp = $this->actingAsApiEditor()->get($this->baseEndpoint);\n        $resp->assertStatus(422);\n\n        $resp = $this->actingAsApiEditor()->get($this->baseEndpoint . '?query=myqueryvalue');\n        $resp->assertOk();\n    }\n\n    public function test_all_endpoint_includes_parent_details_where_visible()\n    {\n        $page = $this->entities->pageWithinChapter();\n        $chapter = $page->chapter;\n        $book = $page->book;\n\n        $page->update(['name' => 'name with superextrauniquevalue within']);\n        $page->indexForSearch();\n\n        $editor = $this->users->editor();\n        $this->actingAsApiEditor();\n        $resp = $this->getJson($this->baseEndpoint . '?query=superextrauniquevalue');\n        $resp->assertJsonFragment([\n            'id' => $page->id,\n            'type' => 'page',\n            'book' => [\n                'id' => $book->id,\n                'name' => $book->name,\n                'slug' => $book->slug,\n            ],\n            'chapter' => [\n                'id' => $chapter->id,\n                'name' => $chapter->name,\n                'slug' => $chapter->slug,\n            ],\n        ]);\n\n        $this->permissions->disableEntityInheritedPermissions($chapter);\n        $this->permissions->setEntityPermissions($page, ['view'], [$editor->roles()->first()]);\n\n        $resp = $this->getJson($this->baseEndpoint . '?query=superextrauniquevalue');\n        $resp->assertJsonPath('data.0.id', $page->id);\n        $resp->assertJsonPath('data.0.book.name', $book->name);\n        $resp->assertJsonMissingPath('data.0.chapter');\n\n        $this->permissions->disableEntityInheritedPermissions($book);\n\n        $resp = $this->getJson($this->baseEndpoint . '?query=superextrauniquevalue');\n        $resp->assertOk();\n        $resp->assertJsonPath('data.0.id', $page->id);\n        $resp->assertJsonMissingPath('data.0.book.name');\n    }\n}\n"
  },
  {
    "path": "tests/Api/ShelvesApiTest.php",
    "content": "<?php\n\nnamespace Tests\\Api;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Entities\\Repos\\BaseRepo;\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\DB;\nuse Tests\\TestCase;\n\nclass ShelvesApiTest extends TestCase\n{\n    use TestsApi;\n\n    protected string $baseEndpoint = '/api/shelves';\n\n    public function test_index_endpoint_returns_expected_shelf()\n    {\n        $this->actingAsApiEditor();\n        $firstBookshelf = Bookshelf::query()->orderBy('id', 'asc')->first();\n\n        $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');\n        $resp->assertJson(['data' => [\n            [\n                'id'   => $firstBookshelf->id,\n                'name' => $firstBookshelf->name,\n                'slug' => $firstBookshelf->slug,\n                'owned_by' => $firstBookshelf->owned_by,\n                'created_by' => $firstBookshelf->created_by,\n                'updated_by' => $firstBookshelf->updated_by,\n                'cover' => null,\n            ],\n        ]]);\n    }\n\n    public function test_index_endpoint_includes_cover_if_set()\n    {\n        $this->actingAsApiEditor();\n        $shelf = $this->entities->shelf();\n\n        $baseRepo = $this->app->make(BaseRepo::class);\n        $image = $this->files->uploadedImage('shelf_cover');\n        $baseRepo->updateCoverImage($shelf, $image);\n\n        $resp = $this->getJson($this->baseEndpoint . '?filter[id]=' . $shelf->id);\n        $resp->assertJson(['data' => [\n            [\n                'id'   => $shelf->id,\n                'cover' => [\n                    'id' => $shelf->coverInfo()->getImage()->id,\n                    'url' => $shelf->coverInfo()->getImage()->url,\n                ],\n            ],\n        ]]);\n    }\n\n    public function test_create_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $books = Book::query()->take(2)->get();\n\n        $details = [\n            'name'        => 'My API shelf',\n            'description' => 'A shelf created via the API',\n        ];\n\n        $resp = $this->postJson($this->baseEndpoint, array_merge($details, ['books' => [$books[0]->id, $books[1]->id]]));\n        $resp->assertStatus(200);\n        $newItem = Bookshelf::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();\n        $resp->assertJson(array_merge($details, [\n            'id' => $newItem->id,\n            'slug' => $newItem->slug,\n            'description_html' => '<p>A shelf created via the API</p>',\n        ]));\n        $this->assertActivityExists('bookshelf_create', $newItem);\n        foreach ($books as $index => $book) {\n            $this->assertDatabaseHas('bookshelves_books', [\n                'bookshelf_id' => $newItem->id,\n                'book_id'      => $book->id,\n                'order'        => $index,\n            ]);\n        }\n    }\n\n    public function test_create_endpoint_with_html()\n    {\n        $this->actingAsApiEditor();\n\n        $details = [\n            'name'             => 'My API shelf',\n            'description_html' => '<p>A <strong>shelf</strong> created via the API</p>',\n        ];\n\n        $resp = $this->postJson($this->baseEndpoint, $details);\n        $resp->assertStatus(200);\n        $newItem = Bookshelf::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();\n\n        $expectedDetails = array_merge($details, [\n            'id'          => $newItem->id,\n            'description' => 'A shelf created via the API',\n        ]);\n\n        $resp->assertJson($expectedDetails);\n        $this->assertDatabaseHasEntityData('bookshelf', $expectedDetails);\n    }\n\n    public function test_shelf_name_needed_to_create()\n    {\n        $this->actingAsApiEditor();\n        $details = [\n            'description' => 'A shelf created via the API',\n        ];\n\n        $resp = $this->postJson($this->baseEndpoint, $details);\n        $resp->assertStatus(422);\n        $resp->assertJson([\n            'error' => [\n                'message'    => 'The given data was invalid.',\n                'validation' => [\n                    'name' => ['The name field is required.'],\n                ],\n                'code' => 422,\n            ],\n        ]);\n    }\n\n    public function test_read_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $shelf = Bookshelf::visible()->first();\n\n        $resp = $this->getJson($this->baseEndpoint . \"/{$shelf->id}\");\n\n        $resp->assertStatus(200);\n        $resp->assertJson([\n            'id'         => $shelf->id,\n            'slug'       => $shelf->slug,\n            'created_by' => [\n                'name' => $shelf->createdBy->name,\n            ],\n            'updated_by' => [\n                'name' => $shelf->createdBy->name,\n            ],\n            'owned_by' => [\n                'name' => $shelf->ownedBy->name,\n            ],\n        ]);\n    }\n\n    public function test_update_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $shelf = Bookshelf::visible()->first();\n        $details = [\n            'name'        => 'My updated API shelf',\n            'description' => 'A shelf updated via the API',\n        ];\n\n        $resp = $this->putJson($this->baseEndpoint . \"/{$shelf->id}\", $details);\n        $shelf->refresh();\n\n        $resp->assertStatus(200);\n        $resp->assertJson(array_merge($details, [\n            'id' => $shelf->id,\n            'slug' => $shelf->slug,\n            'description_html' => '<p>A shelf updated via the API</p>',\n        ]));\n        $this->assertActivityExists('bookshelf_update', $shelf);\n    }\n\n    public function test_update_endpoint_with_html()\n    {\n        $this->actingAsApiEditor();\n        $shelf = Bookshelf::visible()->first();\n        $details = [\n            'name'             => 'My updated API shelf',\n            'description_html' => '<p>A shelf <em>updated</em> via the API</p>',\n        ];\n\n        $resp = $this->putJson($this->baseEndpoint . \"/{$shelf->id}\", $details);\n        $resp->assertStatus(200);\n\n        $this->assertDatabaseHasEntityData('bookshelf', array_merge($details, ['id' => $shelf->id, 'description' => 'A shelf updated via the API']));\n    }\n\n    public function test_update_increments_updated_date_if_only_tags_are_sent()\n    {\n        $this->actingAsApiEditor();\n        $shelf = Bookshelf::visible()->first();\n        $shelf->newQuery()->where('id', '=', $shelf->id)->update(['updated_at' => Carbon::now()->subWeek()]);\n\n        $details = [\n            'tags' => [['name' => 'Category', 'value' => 'Testing']],\n        ];\n\n        $this->putJson($this->baseEndpoint . \"/{$shelf->id}\", $details);\n        $shelf->refresh();\n        $this->assertGreaterThan(Carbon::now()->subDay()->unix(), $shelf->updated_at->unix());\n    }\n\n    public function test_update_only_assigns_books_if_param_provided()\n    {\n        $this->actingAsApiEditor();\n        $shelf = Bookshelf::visible()->first();\n        $this->assertTrue($shelf->books()->count() > 0);\n        $details = [\n            'name' => 'My updated API shelf',\n        ];\n\n        $resp = $this->putJson($this->baseEndpoint . \"/{$shelf->id}\", $details);\n        $resp->assertStatus(200);\n        $this->assertTrue($shelf->books()->count() > 0);\n\n        $resp = $this->putJson($this->baseEndpoint . \"/{$shelf->id}\", ['books' => []]);\n        $resp->assertStatus(200);\n        $this->assertTrue($shelf->books()->count() === 0);\n    }\n\n    public function test_update_cover_image_control()\n    {\n        $this->actingAsApiEditor();\n        /** @var Book $shelf */\n        $shelf = Bookshelf::visible()->first();\n        $this->assertNull($shelf->coverInfo()->getImage());\n        $file = $this->files->uploadedImage('image.png');\n\n        // Ensure cover image can be set via API\n        $resp = $this->call('PUT', $this->baseEndpoint . \"/{$shelf->id}\", [\n            'name'  => 'My updated API shelf with image',\n        ], [], ['image' => $file]);\n        $shelf->refresh();\n\n        $resp->assertStatus(200);\n        $this->assertNotNull($shelf->coverInfo()->getImage());\n\n        // Ensure further updates without image do not clear cover image\n        $resp = $this->put($this->baseEndpoint . \"/{$shelf->id}\", [\n            'name' => 'My updated shelf again',\n        ]);\n        $shelf->refresh();\n\n        $resp->assertStatus(200);\n        $this->assertNotNull($shelf->coverInfo()->getImage());\n\n        // Ensure update with null image property clears image\n        $resp = $this->put($this->baseEndpoint . \"/{$shelf->id}\", [\n            'image' => null,\n        ]);\n        $shelf->refresh();\n\n        $resp->assertStatus(200);\n        $this->assertNull($shelf->coverInfo()->getImage());\n    }\n\n    public function test_delete_endpoint()\n    {\n        $this->actingAsApiEditor();\n        $shelf = Bookshelf::visible()->first();\n        $resp = $this->deleteJson($this->baseEndpoint . \"/{$shelf->id}\");\n\n        $resp->assertStatus(204);\n        $this->assertActivityExists('bookshelf_delete');\n    }\n}\n"
  },
  {
    "path": "tests/Api/SystemApiTest.php",
    "content": "<?php\n\nnamespace Tests\\Api;\n\nuse Tests\\TestCase;\n\nclass SystemApiTest extends TestCase\n{\n    use TestsApi;\n\n    public function test_read_returns_app_info(): void\n    {\n        $resp = $this->actingAsApiEditor()->get('/api/system');\n        $data = $resp->json();\n\n        $this->assertStringStartsWith('v', $data['version']);\n        $this->assertEquals(setting('instance-id'), $data['instance_id']);\n        $this->assertEquals(setting('app-name'), $data['app_name']);\n        $this->assertEquals(url('/logo.png'), $data['app_logo']);\n        $this->assertEquals(url('/'), $data['base_url']);\n    }\n}\n"
  },
  {
    "path": "tests/Api/TestsApi.php",
    "content": "<?php\n\nnamespace Tests\\Api;\n\nuse BookStack\\Users\\Models\\User;\n\ntrait TestsApi\n{\n    protected string $apiTokenId = 'apitoken';\n    protected string $apiTokenSecret = 'password';\n\n    /**\n     * Set the given user as the current logged-in user via the API driver.\n     * This does not ensure API access. The user may still lack required role permissions.\n     */\n    protected function actingAsForApi(User $user): static\n    {\n        parent::actingAs($user, 'api');\n\n        return $this;\n    }\n\n    /**\n     * Set the API editor role as the current user via the API driver.\n     */\n    protected function actingAsApiEditor(): static\n    {\n        $this->actingAs($this->users->editor(), 'api');\n\n        return $this;\n    }\n\n    /**\n     * Set the API admin role as the current user via the API driver.\n     */\n    protected function actingAsApiAdmin(): static\n    {\n        $this->actingAs($this->users->admin(), 'api');\n\n        return $this;\n    }\n\n    /**\n     * Format the given items into a standardised error format.\n     */\n    protected function errorResponse(string $message, int $code): array\n    {\n        return ['error' => ['code' => $code, 'message' => $message]];\n    }\n\n    /**\n     * Get the structure that matches a permission error response.\n     */\n    protected function permissionErrorResponse(): array\n    {\n        return $this->errorResponse('You do not have permission to perform the requested action.', 403);\n    }\n\n    /**\n     * Format the given (field_name => [\"messages\"]) array\n     * into a standard validation response format.\n     */\n    protected function validationResponse(array $messages): array\n    {\n        $err = $this->errorResponse('The given data was invalid.', 422);\n        $err['error']['validation'] = $messages;\n\n        return $err;\n    }\n\n    /**\n     * Get an approved API auth header.\n     */\n    protected function apiAuthHeader(): array\n    {\n        return [\n            'Authorization' => \"Token {$this->apiTokenId}:{$this->apiTokenSecret}\",\n        ];\n    }\n}\n"
  },
  {
    "path": "tests/Api/UsersApiTest.php",
    "content": "<?php\n\nnamespace Tests\\Api;\n\nuse BookStack\\Access\\Notifications\\UserInviteNotification;\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Activity\\Models\\Activity as ActivityModel;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Facades\\Activity;\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Support\\Facades\\Hash;\nuse Illuminate\\Support\\Facades\\Notification;\nuse Tests\\TestCase;\n\nclass UsersApiTest extends TestCase\n{\n    use TestsApi;\n\n    protected string $baseEndpoint = '/api/users';\n\n    protected array $endpointMap = [\n        ['get', '/api/users'],\n        ['post', '/api/users'],\n        ['get', '/api/users/1'],\n        ['put', '/api/users/1'],\n        ['delete', '/api/users/1'],\n    ];\n\n    public function test_users_manage_permission_needed_for_all_endpoints()\n    {\n        $this->actingAsApiEditor();\n        foreach ($this->endpointMap as [$method, $uri]) {\n            $resp = $this->json($method, $uri);\n            $resp->assertStatus(403);\n            $resp->assertJson($this->permissionErrorResponse());\n        }\n    }\n\n    public function test_no_endpoints_accessible_in_demo_mode()\n    {\n        config()->set('app.env', 'demo');\n        $this->actingAsApiAdmin();\n\n        foreach ($this->endpointMap as [$method, $uri]) {\n            $resp = $this->json($method, $uri);\n            $resp->assertStatus(403);\n            $resp->assertJson($this->permissionErrorResponse());\n        }\n    }\n\n    public function test_index_endpoint_returns_expected_user()\n    {\n        $this->actingAsApiAdmin();\n        /** @var User $firstUser */\n        $firstUser = User::query()->orderBy('id', 'asc')->first();\n\n        $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');\n        $resp->assertJson(['data' => [\n            [\n                'id'          => $firstUser->id,\n                'name'        => $firstUser->name,\n                'slug'        => $firstUser->slug,\n                'email'       => $firstUser->email,\n                'profile_url' => $firstUser->getProfileUrl(),\n                'edit_url'    => $firstUser->getEditUrl(),\n                'avatar_url'  => $firstUser->getAvatar(),\n            ],\n        ]]);\n    }\n\n    public function test_index_endpoint_has_correct_created_and_last_activity_dates()\n    {\n        $user = $this->users->editor();\n        $user->created_at = now()->subYear();\n        $user->save();\n\n        $this->actingAs($user);\n        Activity::add(ActivityType::AUTH_LOGIN, 'test login activity');\n        /** @var ActivityModel $activity */\n        $activity = ActivityModel::query()->where('user_id', '=', $user->id)->latest()->first();\n\n        $resp = $this->actingAsApiAdmin()->getJson($this->baseEndpoint . '?filter[id]=3');\n        $resp->assertJson(['data' => [\n            [\n                'id'          => $user->id,\n                'created_at' => $user->created_at->toJSON(),\n                'last_activity_at' => $activity->created_at->toJson(),\n            ],\n        ]]);\n    }\n\n    public function test_create_endpoint()\n    {\n        $this->actingAsApiAdmin();\n        /** @var Role $role */\n        $role = Role::query()->first();\n\n        $resp = $this->postJson($this->baseEndpoint, [\n            'name'        => 'Benny Boris',\n            'email'       => 'bboris@example.com',\n            'password'    => 'mysuperpass',\n            'language'    => 'it',\n            'roles'       => [$role->id],\n            'send_invite' => false,\n        ]);\n\n        $resp->assertStatus(200);\n        $resp->assertJson([\n            'name'             => 'Benny Boris',\n            'email'            => 'bboris@example.com',\n            'external_auth_id' => '',\n            'roles'            => [\n                [\n                    'id'           => $role->id,\n                    'display_name' => $role->display_name,\n                ],\n            ],\n        ]);\n        $this->assertDatabaseHas('users', ['email' => 'bboris@example.com']);\n\n        /** @var User $user */\n        $user = User::query()->where('email', '=', 'bboris@example.com')->first();\n        $this->assertActivityExists(ActivityType::USER_CREATE, null, $user->logDescriptor());\n        $this->assertEquals(1, $user->roles()->count());\n        $this->assertEquals('it', setting()->getUser($user, 'language'));\n    }\n\n    public function test_create_with_send_invite()\n    {\n        $this->actingAsApiAdmin();\n        Notification::fake();\n\n        $resp = $this->postJson($this->baseEndpoint, [\n            'name'        => 'Benny Boris',\n            'email'       => 'bboris@example.com',\n            'send_invite' => true,\n        ]);\n\n        $resp->assertStatus(200);\n        /** @var User $user */\n        $user = User::query()->where('email', '=', 'bboris@example.com')->first();\n        Notification::assertSentTo($user, UserInviteNotification::class);\n    }\n\n    public function test_create_with_send_invite_works_with_value_of_1()\n    {\n        $this->actingAsApiAdmin();\n        Notification::fake();\n\n        $resp = $this->postJson($this->baseEndpoint, [\n            'name'        => 'Benny Boris',\n            'email'       => 'bboris@example.com',\n            'send_invite' => '1', // Submissions via x-www-form-urlencoded/form-data may use 1 instead of boolean\n        ]);\n\n        $resp->assertStatus(200);\n        /** @var User $user */\n        $user = User::query()->where('email', '=', 'bboris@example.com')->first();\n        Notification::assertSentTo($user, UserInviteNotification::class);\n    }\n\n    public function test_create_name_and_email_validation()\n    {\n        $this->actingAsApiAdmin();\n        /** @var User $existingUser */\n        $existingUser = User::query()->first();\n\n        $resp = $this->postJson($this->baseEndpoint, [\n            'email' => 'bboris@example.com',\n        ]);\n        $resp->assertStatus(422);\n        $resp->assertJson($this->validationResponse(['name' => ['The name field is required.']]));\n\n        $resp = $this->postJson($this->baseEndpoint, [\n            'name' => 'Benny Boris',\n        ]);\n        $resp->assertStatus(422);\n        $resp->assertJson($this->validationResponse(['email' => ['The email field is required.']]));\n\n        $resp = $this->postJson($this->baseEndpoint, [\n            'email' => $existingUser->email,\n            'name'  => 'Benny Boris',\n        ]);\n        $resp->assertStatus(422);\n        $resp->assertJson($this->validationResponse(['email' => ['The email has already been taken.']]));\n    }\n\n    public function test_read_endpoint()\n    {\n        $this->actingAsApiAdmin();\n        /** @var User $user */\n        $user = User::query()->first();\n        /** @var Role $userRole */\n        $userRole = $user->roles()->first();\n\n        $resp = $this->getJson($this->baseEndpoint . \"/{$user->id}\");\n\n        $resp->assertStatus(200);\n        $resp->assertJson([\n            'id'               => $user->id,\n            'slug'             => $user->slug,\n            'email'            => $user->email,\n            'external_auth_id' => $user->external_auth_id,\n            'roles'            => [\n                [\n                    'id'           => $userRole->id,\n                    'display_name' => $userRole->display_name,\n                ],\n            ],\n        ]);\n    }\n\n    public function test_update_endpoint()\n    {\n        $this->actingAsApiAdmin();\n        /** @var User $user */\n        $user = $this->users->admin();\n        $roles = Role::query()->pluck('id');\n        $resp = $this->putJson($this->baseEndpoint . \"/{$user->id}\", [\n            'name'             => 'My updated user',\n            'email'            => 'barrytest@example.com',\n            'roles'            => $roles,\n            'external_auth_id' => 'btest',\n            'password'         => 'barrytester',\n            'language'         => 'fr',\n        ]);\n\n        $resp->assertStatus(200);\n        $resp->assertJson([\n            'id'               => $user->id,\n            'name'             => 'My updated user',\n            'email'            => 'barrytest@example.com',\n            'external_auth_id' => 'btest',\n        ]);\n        $user->refresh();\n        $this->assertEquals('fr', setting()->getUser($user, 'language'));\n        $this->assertEquals(count($roles), $user->roles()->count());\n        $this->assertNotEquals('barrytester', $user->password);\n        $this->assertTrue(Hash::check('barrytester', $user->password));\n    }\n\n    public function test_update_endpoint_does_not_remove_info_if_not_provided()\n    {\n        $this->actingAsApiAdmin();\n        /** @var User $user */\n        $user = $this->users->admin();\n        $roleCount = $user->roles()->count();\n        $resp = $this->putJson($this->baseEndpoint . \"/{$user->id}\", []);\n\n        $resp->assertStatus(200);\n        $this->assertDatabaseHas('users', [\n            'id'       => $user->id,\n            'name'     => $user->name,\n            'email'    => $user->email,\n            'password' => $user->password,\n        ]);\n        $this->assertEquals($roleCount, $user->roles()->count());\n    }\n\n    public function test_delete_endpoint()\n    {\n        $this->actingAsApiAdmin();\n        /** @var User $user */\n        $user = User::query()->where('id', '!=', $this->users->admin()->id)\n            ->whereNull('system_name')\n            ->first();\n\n        $resp = $this->deleteJson($this->baseEndpoint . \"/{$user->id}\");\n\n        $resp->assertStatus(204);\n        $this->assertActivityExists('user_delete', null, $user->logDescriptor());\n    }\n\n    public function test_delete_endpoint_with_ownership_migration_user()\n    {\n        $this->actingAsApiAdmin();\n        /** @var User $user */\n        $user = User::query()->where('id', '!=', $this->users->admin()->id)\n            ->whereNull('system_name')\n            ->first();\n        $entityChain = $this->entities->createChainBelongingToUser($user);\n        /** @var User $newOwner */\n        $newOwner = User::query()->where('id', '!=', $user->id)->first();\n\n        /** @var Entity $entity */\n        foreach ($entityChain as $entity) {\n            $this->assertEquals($user->id, $entity->owned_by);\n        }\n\n        $resp = $this->deleteJson($this->baseEndpoint . \"/{$user->id}\", [\n            'migrate_ownership_id' => $newOwner->id,\n        ]);\n\n        $resp->assertStatus(204);\n        /** @var Entity $entity */\n        foreach ($entityChain as $entity) {\n            $this->assertEquals($newOwner->id, $entity->refresh()->owned_by);\n        }\n    }\n\n    public function test_delete_endpoint_fails_deleting_only_admin()\n    {\n        $this->actingAsApiAdmin();\n        $adminRole = Role::getSystemRole('admin');\n        $adminToDelete = $adminRole->users()->first();\n        $adminRole->users()->where('id', '!=', $adminToDelete->id)->delete();\n\n        $resp = $this->deleteJson($this->baseEndpoint . \"/{$adminToDelete->id}\");\n\n        $resp->assertStatus(500);\n        $resp->assertJson($this->errorResponse('You cannot delete the only admin', 500));\n    }\n\n    public function test_delete_endpoint_fails_deleting_public_user()\n    {\n        $this->actingAsApiAdmin();\n        /** @var User $publicUser */\n        $publicUser = User::query()->where('system_name', '=', 'public')->first();\n\n        $resp = $this->deleteJson($this->baseEndpoint . \"/{$publicUser->id}\");\n\n        $resp->assertStatus(500);\n        $resp->assertJson($this->errorResponse('You cannot delete the guest user', 500));\n    }\n}\n"
  },
  {
    "path": "tests/Auth/AuthTest.php",
    "content": "<?php\n\nnamespace Tests\\Auth;\n\nuse BookStack\\Access\\Mfa\\MfaSession;\nuse Illuminate\\Support\\Facades\\Hash;\nuse Illuminate\\Testing\\TestResponse;\nuse Tests\\TestCase;\n\nclass AuthTest extends TestCase\n{\n    public function test_auth_working()\n    {\n        $this->get('/')->assertRedirect('/login');\n    }\n\n    public function test_login()\n    {\n        $this->login('admin@admin.com', 'password')->assertRedirect('/');\n    }\n\n    public function test_public_viewing()\n    {\n        $this->setSettings(['app-public' => 'true']);\n        $this->get('/')\n            ->assertOk()\n            ->assertSee('Log in');\n    }\n\n    public function test_sign_up_link_on_login()\n    {\n        $this->get('/login')->assertDontSee('Sign up');\n\n        $this->setSettings(['registration-enabled' => 'true']);\n\n        $this->get('/login')->assertSee('Sign up');\n    }\n\n    public function test_logout()\n    {\n        $this->asAdmin()->get('/')->assertOk();\n        $this->post('/logout')->assertRedirect('/');\n        $this->get('/')->assertRedirect('/login');\n    }\n\n    public function test_mfa_session_cleared_on_logout()\n    {\n        $user = $this->users->editor();\n        $mfaSession = $this->app->make(MfaSession::class);\n\n        $mfaSession->markVerifiedForUser($user);\n        $this->assertTrue($mfaSession->isVerifiedForUser($user));\n\n        $this->asAdmin()->post('/logout');\n        $this->assertFalse($mfaSession->isVerifiedForUser($user));\n    }\n\n    public function test_login_redirects_to_initially_requested_url_correctly()\n    {\n        config()->set('app.url', 'http://localhost');\n        $page = $this->entities->page();\n\n        $this->get($page->getUrl())->assertRedirect(url('/login'));\n        $this->login('admin@admin.com', 'password')\n            ->assertRedirect($page->getUrl());\n    }\n\n    public function test_login_intended_redirect_does_not_redirect_to_external_pages()\n    {\n        config()->set('app.url', 'http://localhost');\n        $this->setSettings(['app-public' => true]);\n\n        $this->get('/login', ['referer' => 'https://example.com']);\n        $login = $this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']);\n\n        $login->assertRedirect('http://localhost');\n    }\n\n    public function test_login_intended_redirect_does_not_factor_mfa_routes()\n    {\n        $this->get('/books')->assertRedirect('/login');\n        $this->get('/mfa/setup')->assertRedirect('/login');\n        $login = $this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']);\n        $login->assertRedirect('/books');\n    }\n\n    public function test_login_authenticates_admins_on_all_guards()\n    {\n        $this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']);\n        $this->assertTrue(auth()->check());\n        $this->assertTrue(auth('ldap')->check());\n        $this->assertTrue(auth('saml2')->check());\n        $this->assertTrue(auth('oidc')->check());\n    }\n\n    public function test_login_authenticates_nonadmins_on_default_guard_only()\n    {\n        $editor = $this->users->editor();\n        $editor->password = bcrypt('password');\n        $editor->save();\n\n        $this->post('/login', ['email' => $editor->email, 'password' => 'password']);\n        $this->assertTrue(auth()->check());\n        $this->assertFalse(auth('ldap')->check());\n        $this->assertFalse(auth('saml2')->check());\n        $this->assertFalse(auth('oidc')->check());\n    }\n\n    public function test_failed_logins_are_logged_when_message_configured()\n    {\n        $log = $this->withTestLogger();\n        config()->set(['logging.failed_login.message' => 'Failed login for %u']);\n\n        $this->post('/login', ['email' => 'admin@example.com', 'password' => 'cattreedog']);\n        $this->assertTrue($log->hasWarningThatContains('Failed login for admin@example.com'));\n\n        $this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']);\n        $this->assertFalse($log->hasWarningThatContains('Failed login for admin@admin.com'));\n    }\n\n    public function test_logged_in_user_with_unconfirmed_email_is_logged_out()\n    {\n        $this->setSettings(['registration-confirmation' => 'true']);\n        $user = $this->users->editor();\n        $user->email_confirmed = false;\n        $user->save();\n\n        auth()->login($user);\n        $this->assertTrue(auth()->check());\n\n        $this->get('/books')->assertRedirect('/');\n        $this->assertFalse(auth()->check());\n    }\n\n    public function test_login_attempts_are_rate_limited()\n    {\n        for ($i = 0; $i < 5; $i++) {\n            $resp = $this->login('bennynotexisting@example.com', 'pw123');\n        }\n        $resp = $this->followRedirects($resp);\n        $resp->assertSee('These credentials do not match our records.');\n\n        // Check the fifth attempt provides a lockout response\n        $resp = $this->followRedirects($this->login('bennynotexisting@example.com', 'pw123'));\n        $resp->assertSee('Too many login attempts. Please try again in');\n    }\n\n    public function test_login_specifically_disabled_for_guest_account()\n    {\n        $guest = $this->users->guest();\n\n        $resp = $this->post('/login', ['email' => $guest->email, 'password' => 'password']);\n        $resp->assertRedirect('/login');\n        $resp = $this->followRedirects($resp);\n        $resp->assertSee('These credentials do not match our records.');\n\n        // Test login even with password somehow set\n        $guest->password = Hash::make('password');\n        $guest->save();\n\n        $resp = $this->post('/login', ['email' => $guest->email, 'password' => 'password']);\n        $resp->assertRedirect('/login');\n        $resp = $this->followRedirects($resp);\n        $resp->assertSee('These credentials do not match our records.');\n    }\n\n    /**\n     * Perform a login.\n     */\n    protected function login(string $email, string $password): TestResponse\n    {\n        return $this->post('/login', compact('email', 'password'));\n    }\n}\n"
  },
  {
    "path": "tests/Auth/GroupSyncServiceTest.php",
    "content": "<?php\n\nnamespace Tests\\Auth;\n\nuse BookStack\\Access\\GroupSyncService;\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Models\\User;\nuse Tests\\TestCase;\n\nclass GroupSyncServiceTest extends TestCase\n{\n    public function test_user_is_assigned_to_matching_roles()\n    {\n        $user = $this->users->viewer();\n\n        $roleA = Role::factory()->create(['display_name' => 'Wizards']);\n        $roleB = Role::factory()->create(['display_name' => 'Gremlins']);\n        $roleC = Role::factory()->create(['display_name' => 'ABC123', 'external_auth_id' => 'sales']);\n        $roleD = Role::factory()->create(['display_name' => 'DEF456', 'external_auth_id' => 'admin-team']);\n\n        foreach ([$roleA, $roleB, $roleC, $roleD] as $role) {\n            $this->assertFalse($user->hasRole($role->id));\n        }\n\n        (new GroupSyncService())->syncUserWithFoundGroups($user, ['Wizards', 'Gremlinz', 'Sales', 'Admin Team'], false);\n\n        $user = User::query()->find($user->id);\n        $this->assertTrue($user->hasRole($roleA->id));\n        $this->assertFalse($user->hasRole($roleB->id));\n        $this->assertTrue($user->hasRole($roleC->id));\n        $this->assertTrue($user->hasRole($roleD->id));\n    }\n\n    public function test_multiple_values_in_role_external_auth_id_handled()\n    {\n        $user = $this->users->viewer();\n        $role = Role::factory()->create(['display_name' => 'ABC123', 'external_auth_id' => 'sales, engineering, developers, marketers']);\n        $this->assertFalse($user->hasRole($role->id));\n\n        (new GroupSyncService())->syncUserWithFoundGroups($user, ['Developers'], false);\n\n        $user = User::query()->find($user->id);\n        $this->assertTrue($user->hasRole($role->id));\n    }\n\n    public function test_commas_can_be_used_in_external_auth_id_if_escaped()\n    {\n        $user = $this->users->viewer();\n        $role = Role::factory()->create(['display_name' => 'ABC123', 'external_auth_id' => 'sales\\,-developers, marketers']);\n        $this->assertFalse($user->hasRole($role->id));\n\n        (new GroupSyncService())->syncUserWithFoundGroups($user, ['Sales, Developers'], false);\n\n        $user = User::query()->find($user->id);\n        $this->assertTrue($user->hasRole($role->id));\n    }\n\n    public function test_external_auth_id_matches_ignoring_case()\n    {\n        $user = $this->users->viewer();\n        $role = Role::factory()->create(['display_name' => 'ABC123', 'external_auth_id' => 'WaRRioRs']);\n        $this->assertFalse($user->hasRole($role->id));\n\n        (new GroupSyncService())->syncUserWithFoundGroups($user, ['wArriors', 'penguiNs'], false);\n\n        $user = User::query()->find($user->id);\n        $this->assertTrue($user->hasRole($role->id));\n    }\n}\n"
  },
  {
    "path": "tests/Auth/LdapTest.php",
    "content": "<?php\n\nnamespace Tests\\Auth;\n\nuse BookStack\\Access\\Ldap;\nuse BookStack\\Access\\LdapService;\nuse BookStack\\Exceptions\\LdapException;\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Testing\\TestResponse;\nuse Mockery\\MockInterface;\nuse Tests\\TestCase;\n\nclass LdapTest extends TestCase\n{\n    protected MockInterface $mockLdap;\n\n    protected User $mockUser;\n    protected string $resourceId = 'resource-test';\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n        if (!defined('LDAP_OPT_REFERRALS')) {\n            define('LDAP_OPT_REFERRALS', 1);\n        }\n        config()->set([\n            'auth.method'                          => 'ldap',\n            'auth.defaults.guard'                  => 'ldap',\n            'services.ldap.base_dn'                => 'dc=ldap,dc=local',\n            'services.ldap.email_attribute'        => 'mail',\n            'services.ldap.display_name_attribute' => 'cn',\n            'services.ldap.id_attribute'           => 'uid',\n            'services.ldap.user_to_groups'         => false,\n            'services.ldap.version'                => '3',\n            'services.ldap.user_filter'            => '(&(uid={user}))',\n            'services.ldap.follow_referrals'       => false,\n            'services.ldap.tls_insecure'           => false,\n            'services.ldap.tls_ca_cert'            => false,\n            'services.ldap.thumbnail_attribute'    => null,\n        ]);\n        $this->mockLdap = $this->mock(Ldap::class);\n        $this->mockUser = User::factory()->make();\n    }\n\n    protected function runFailedAuthLogin()\n    {\n        $this->commonLdapMocks(1, 1, 1, 1, 1);\n        $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)\n            ->andReturn(['count' => 0]);\n        $this->post('/login', ['username' => 'timmyjenkins', 'password' => 'cattreedog']);\n    }\n\n    protected function mockEscapes($times = 1)\n    {\n        $this->mockLdap->shouldReceive('escape')->times($times)->andReturnUsing(function ($val) {\n            return ldap_escape($val);\n        });\n    }\n\n    protected function mockExplodes($times = 1)\n    {\n        $this->mockLdap->shouldReceive('explodeDn')->times($times)->andReturnUsing(function ($dn, $withAttrib) {\n            return ldap_explode_dn($dn, $withAttrib);\n        });\n    }\n\n    protected function mockUserLogin(?string $email = null): TestResponse\n    {\n        return $this->post('/login', [\n            'username' => $this->mockUser->name,\n            'password' => $this->mockUser->password,\n        ] + ($email ? ['email' => $email] : []));\n    }\n\n    /**\n     * Set LDAP method mocks for things we commonly call without altering.\n     */\n    protected function commonLdapMocks(int $connects = 1, int $versions = 1, int $options = 2, int $binds = 4, int $escapes = 2, int $explodes = 0, int $groups = 0)\n    {\n        $this->mockLdap->shouldReceive('connect')->times($connects)->andReturn($this->resourceId);\n        $this->mockLdap->shouldReceive('setVersion')->times($versions);\n        $this->mockLdap->shouldReceive('setOption')->times($options);\n        $this->mockLdap->shouldReceive('bind')->times($binds)->andReturn(true);\n        $this->mockEscapes($escapes);\n        $this->mockExplodes($explodes);\n        $this->mockGroupLookups($groups);\n    }\n\n    protected function mockGroupLookups(int $times = 1): void\n    {\n        $this->mockLdap->shouldReceive('read')->times($times)->andReturn(['count' => 0]);\n        $this->mockLdap->shouldReceive('getEntries')->times($times)->andReturn(['count' => 0]);\n    }\n\n    public function test_login()\n    {\n        $this->commonLdapMocks(1, 1, 2, 4, 2);\n        $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)\n            ->with($this->resourceId, config('services.ldap.base_dn'), \\Mockery::type('string'), \\Mockery::type('array'))\n            ->andReturn(['count' => 1, 0 => [\n                'uid' => [$this->mockUser->name],\n                'cn'  => [$this->mockUser->name],\n                'dn'  => 'dc=test' . config('services.ldap.base_dn'),\n            ]]);\n\n        $resp = $this->mockUserLogin();\n        $resp->assertRedirect('/login');\n        $resp = $this->followRedirects($resp);\n        $resp->assertSee('Please enter an email to use for this account.');\n        $resp->assertSee($this->mockUser->name);\n\n        $resp = $this->followingRedirects()->mockUserLogin($this->mockUser->email);\n        $this->withHtml($resp)->assertElementExists('#home-default');\n        $resp->assertSee($this->mockUser->name);\n        $this->assertDatabaseHas('users', [\n            'email'            => $this->mockUser->email,\n            'email_confirmed'  => false,\n            'external_auth_id' => $this->mockUser->name,\n        ]);\n    }\n\n    public function test_email_domain_restriction_active_on_new_ldap_login()\n    {\n        $this->setSettings([\n            'registration-restrict' => 'testing.com',\n        ]);\n\n        $this->commonLdapMocks(1, 1, 2, 4, 2);\n        $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)\n            ->with($this->resourceId, config('services.ldap.base_dn'), \\Mockery::type('string'), \\Mockery::type('array'))\n            ->andReturn(['count' => 1, 0 => [\n                'uid' => [$this->mockUser->name],\n                'cn'  => [$this->mockUser->name],\n                'dn'  => 'dc=test' . config('services.ldap.base_dn'),\n            ]]);\n\n        $resp = $this->mockUserLogin();\n        $resp->assertRedirect('/login');\n        $this->followRedirects($resp)->assertSee('Please enter an email to use for this account.');\n\n        $email = 'tester@invaliddomain.com';\n        $resp = $this->mockUserLogin($email);\n        $resp->assertRedirect('/login');\n        $this->followRedirects($resp)->assertSee('That email domain does not have access to this application');\n\n        $this->assertDatabaseMissing('users', ['email' => $email]);\n    }\n\n    public function test_login_works_when_no_uid_provided_by_ldap_server()\n    {\n        $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');\n\n        $this->commonLdapMocks(1, 1, 1, 2, 1);\n        $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)\n            ->with($this->resourceId, config('services.ldap.base_dn'), \\Mockery::type('string'), \\Mockery::type('array'))\n            ->andReturn(['count' => 1, 0 => [\n                'cn'   => [$this->mockUser->name],\n                'dn'   => $ldapDn,\n                'mail' => [$this->mockUser->email],\n            ]]);\n\n        $resp = $this->mockUserLogin();\n        $resp->assertRedirect('/');\n        $this->followRedirects($resp)->assertSee($this->mockUser->name);\n        $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $ldapDn]);\n    }\n\n    public function test_login_works_when_ldap_server_does_not_provide_a_cn_value()\n    {\n        $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');\n\n        $this->commonLdapMocks(1, 1, 1, 2, 1);\n        $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)\n            ->with($this->resourceId, config('services.ldap.base_dn'), \\Mockery::type('string'), \\Mockery::type('array'))\n            ->andReturn(['count' => 1, 0 => [\n                'dn'   => $ldapDn,\n                'mail' => [$this->mockUser->email],\n            ]]);\n\n        $resp = $this->mockUserLogin();\n        $resp->assertRedirect('/');\n        $this->assertDatabaseHas('users', [\n            'name' => 'test-user',\n            'email' => $this->mockUser->email,\n        ]);\n    }\n\n    public function test_a_custom_uid_attribute_can_be_specified_and_is_used_properly()\n    {\n        config()->set(['services.ldap.id_attribute' => 'my_custom_id']);\n\n        $this->commonLdapMocks(1, 1, 1, 2, 1);\n        $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');\n        $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)\n            ->with($this->resourceId, config('services.ldap.base_dn'), \\Mockery::type('string'), \\Mockery::type('array'))\n            ->andReturn(['count' => 1, 0 => [\n                'cn'           => [$this->mockUser->name],\n                'dn'           => $ldapDn,\n                'my_custom_id' => ['cooluser456'],\n                'mail'         => [$this->mockUser->email],\n            ]]);\n\n        $resp = $this->mockUserLogin();\n        $resp->assertRedirect('/');\n        $this->followRedirects($resp)->assertSee($this->mockUser->name);\n        $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => 'cooluser456']);\n    }\n\n    public function test_user_filter_default_placeholder_format()\n    {\n        config()->set('services.ldap.user_filter', '(&(uid={user}))');\n        $this->mockUser->name = 'barryldapuser';\n        $expectedFilter = '(&(uid=\\62\\61\\72\\72\\79\\6c\\64\\61\\70\\75\\73\\65\\72))';\n\n        $this->commonLdapMocks(1, 1, 1, 1, 1);\n        $this->mockLdap->shouldReceive('searchAndGetEntries')\n            ->once()\n            ->with($this->resourceId, config('services.ldap.base_dn'), $expectedFilter, \\Mockery::type('array'))\n            ->andReturn(['count' => 0, 0 => []]);\n\n        $resp = $this->mockUserLogin();\n        $resp->assertRedirect('/login');\n    }\n\n    public function test_user_filter_old_placeholder_format()\n    {\n        config()->set('services.ldap.user_filter', '(&(username=${user}))');\n        $this->mockUser->name = 'barryldapuser';\n        $expectedFilter = '(&(username=\\62\\61\\72\\72\\79\\6c\\64\\61\\70\\75\\73\\65\\72))';\n\n        $this->commonLdapMocks(1, 1, 1, 1, 1);\n        $this->mockLdap->shouldReceive('searchAndGetEntries')\n            ->once()\n            ->with($this->resourceId, config('services.ldap.base_dn'), $expectedFilter, \\Mockery::type('array'))\n            ->andReturn(['count' => 0, 0 => []]);\n\n        $resp = $this->mockUserLogin();\n        $resp->assertRedirect('/login');\n    }\n\n    public function test_initial_incorrect_credentials()\n    {\n        $this->commonLdapMocks(1, 1, 1, 0, 1);\n        $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)\n            ->with($this->resourceId, config('services.ldap.base_dn'), \\Mockery::type('string'), \\Mockery::type('array'))\n            ->andReturn(['count' => 1, 0 => [\n                'uid' => [$this->mockUser->name],\n                'cn'  => [$this->mockUser->name],\n                'dn'  => 'dc=test' . config('services.ldap.base_dn'),\n            ]]);\n        $this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true, false);\n\n        $resp = $this->mockUserLogin();\n        $resp->assertRedirect('/login');\n        $this->followRedirects($resp)->assertSee('These credentials do not match our records.');\n        $this->assertDatabaseMissing('users', ['external_auth_id' => $this->mockUser->name]);\n    }\n\n    public function test_login_not_found_username()\n    {\n        $this->commonLdapMocks(1, 1, 1, 1, 1);\n        $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)\n            ->with($this->resourceId, config('services.ldap.base_dn'), \\Mockery::type('string'), \\Mockery::type('array'))\n            ->andReturn(['count' => 0]);\n\n        $resp = $this->mockUserLogin();\n        $resp->assertRedirect('/login');\n        $this->followRedirects($resp)->assertSee('These credentials do not match our records.');\n        $this->assertDatabaseMissing('users', ['external_auth_id' => $this->mockUser->name]);\n    }\n\n    public function test_create_user_form()\n    {\n        $userForm = $this->asAdmin()->get('/settings/users/create');\n        $userForm->assertDontSee('Password');\n\n        $save = $this->post('/settings/users/create', [\n            'name'  => $this->mockUser->name,\n            'email' => $this->mockUser->email,\n        ]);\n        $save->assertSessionHasErrors(['external_auth_id' => 'The external auth id field is required.']);\n\n        $save = $this->post('/settings/users/create', [\n            'name'             => $this->mockUser->name,\n            'email'            => $this->mockUser->email,\n            'external_auth_id' => $this->mockUser->name,\n        ]);\n        $save->assertRedirect('/settings/users');\n        $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'external_auth_id' => $this->mockUser->name, 'email_confirmed' => true]);\n    }\n\n    public function test_user_edit_form()\n    {\n        $editUser = $this->users->viewer();\n        $editPage = $this->asAdmin()->get(\"/settings/users/{$editUser->id}\");\n        $editPage->assertSee('Edit User');\n        $editPage->assertDontSee('Password');\n\n        $update = $this->put(\"/settings/users/{$editUser->id}\", [\n            'name'             => $editUser->name,\n            'email'            => $editUser->email,\n            'external_auth_id' => 'test_auth_id',\n        ]);\n        $update->assertRedirect('/settings/users');\n        $this->assertDatabaseHas('users', ['email' => $editUser->email, 'external_auth_id' => 'test_auth_id']);\n    }\n\n    public function test_registration_disabled()\n    {\n        $resp = $this->followingRedirects()->get('/register');\n        $this->withHtml($resp)->assertElementContains('#content', 'Log In');\n    }\n\n    public function test_non_admins_cannot_change_auth_id()\n    {\n        $testUser = $this->users->viewer();\n        $this->actingAs($testUser)\n            ->get('/settings/users/' . $testUser->id)\n            ->assertDontSee('External Authentication');\n    }\n\n    public function test_login_maps_roles_and_retains_existing_roles()\n    {\n        $roleToReceive = Role::factory()->create(['display_name' => 'LdapTester']);\n        $roleToReceive2 = Role::factory()->create(['display_name' => 'LdapTester Second']);\n        $existingRole = Role::factory()->create(['display_name' => 'ldaptester-existing']);\n        $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();\n        $this->mockUser->attachRole($existingRole);\n\n        app('config')->set([\n            'services.ldap.user_to_groups'     => true,\n            'services.ldap.group_attribute'    => 'memberOf',\n            'services.ldap.remove_from_groups' => false,\n        ]);\n\n        $this->commonLdapMocks(1, 1, 4, 5, 2, 2, 2);\n        $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)\n            ->with($this->resourceId, config('services.ldap.base_dn'), \\Mockery::type('string'), \\Mockery::type('array'))\n            ->andReturn(['count' => 1, 0 => [\n                'uid'      => [$this->mockUser->name],\n                'cn'       => [$this->mockUser->name],\n                'dn'       => 'dc=test' . config('services.ldap.base_dn'),\n                'mail'     => [$this->mockUser->email],\n                'memberof' => [\n                    'count' => 2,\n                    0       => 'cn=ldaptester,ou=groups,dc=example,dc=com',\n                    1       => 'cn=ldaptester-second,ou=groups,dc=example,dc=com',\n                ],\n            ]]);\n\n        $this->mockUserLogin()->assertRedirect('/');\n\n        $user = User::where('email', $this->mockUser->email)->first();\n        $this->assertDatabaseHas('role_user', [\n            'user_id' => $user->id,\n            'role_id' => $roleToReceive->id,\n        ]);\n        $this->assertDatabaseHas('role_user', [\n            'user_id' => $user->id,\n            'role_id' => $roleToReceive2->id,\n        ]);\n        $this->assertDatabaseHas('role_user', [\n            'user_id' => $user->id,\n            'role_id' => $existingRole->id,\n        ]);\n    }\n\n    public function test_login_maps_roles_and_removes_old_roles_if_set()\n    {\n        $roleToReceive = Role::factory()->create(['display_name' => 'LdapTester']);\n        $existingRole = Role::factory()->create(['display_name' => 'ldaptester-existing']);\n        $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();\n        $this->mockUser->attachRole($existingRole);\n\n        app('config')->set([\n            'services.ldap.user_to_groups'     => true,\n            'services.ldap.group_attribute'    => 'memberOf',\n            'services.ldap.remove_from_groups' => true,\n        ]);\n\n        $this->commonLdapMocks(1, 1, 3, 4, 2, 1, 1);\n        $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)\n            ->with($this->resourceId, config('services.ldap.base_dn'), \\Mockery::type('string'), \\Mockery::type('array'))\n            ->andReturn(['count' => 1, 0 => [\n                'uid'      => [$this->mockUser->name],\n                'cn'       => [$this->mockUser->name],\n                'dn'       => 'dc=test' . config('services.ldap.base_dn'),\n                'mail'     => [$this->mockUser->email],\n                'memberof' => [\n                    'count' => 1,\n                    0       => 'cn=ldaptester,ou=groups,dc=example,dc=com',\n                ],\n            ]]);\n\n        $this->mockUserLogin()->assertRedirect('/');\n\n        $user = User::query()->where('email', $this->mockUser->email)->first();\n        $this->assertDatabaseHas('role_user', [\n            'user_id' => $user->id,\n            'role_id' => $roleToReceive->id,\n        ]);\n        $this->assertDatabaseMissing('role_user', [\n            'user_id' => $user->id,\n            'role_id' => $existingRole->id,\n        ]);\n    }\n\n    public function test_dump_user_groups_shows_group_related_details_as_json()\n    {\n        app('config')->set([\n            'services.ldap.user_to_groups'     => true,\n            'services.ldap.group_attribute'    => 'memberOf',\n            'services.ldap.remove_from_groups' => true,\n            'services.ldap.dump_user_groups'   => true,\n        ]);\n\n        $userResp = ['count' => 1, 0 => [\n            'uid'      => [$this->mockUser->name],\n            'cn'       => [$this->mockUser->name],\n            'dn'       => 'dc=test,' . config('services.ldap.base_dn'),\n            'mail'     => [$this->mockUser->email],\n        ]];\n        $this->commonLdapMocks(1, 1, 4, 5, 2, 2, 0);\n        $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)\n            ->with($this->resourceId, config('services.ldap.base_dn'), \\Mockery::type('string'), \\Mockery::type('array'))\n            ->andReturn($userResp, ['count' => 1,\n                0 => [\n                    'dn' => 'dc=test,' . config('services.ldap.base_dn'),\n                    'memberof' => [\n                        'count' => 1,\n                        0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',\n                    ],\n                ],\n            ]);\n\n        $this->mockLdap->shouldReceive('read')->times(2);\n        $this->mockLdap->shouldReceive('getEntries')->times(2)\n            ->andReturn([\n                'count' => 1,\n                0 => [\n                    'dn'        => 'cn=ldaptester,ou=groups,dc=example,dc=com',\n                    'memberof'  => [\n                        'count' => 1,\n                        0       => 'cn=monsters,ou=groups,dc=example,dc=com',\n                    ],\n                ],\n            ], ['count' => 0]);\n\n        $resp = $this->mockUserLogin();\n        $resp->assertJson([\n            'details_from_ldap' => [\n                'dn'       => 'dc=test,' . config('services.ldap.base_dn'),\n                'memberof' => [\n                    0       => 'cn=ldaptester,ou=groups,dc=example,dc=com',\n                    'count' => 1,\n                ],\n            ],\n            'parsed_direct_user_groups' => [\n                'cn=ldaptester,ou=groups,dc=example,dc=com',\n            ],\n            'parsed_recursive_user_groups' => [\n                'cn=ldaptester,ou=groups,dc=example,dc=com',\n                'cn=monsters,ou=groups,dc=example,dc=com',\n            ],\n            'parsed_resulting_group_names' => [\n                'ldaptester',\n                'monsters',\n            ],\n        ]);\n    }\n\n    public function test_recursive_group_search_queries_via_full_dn()\n    {\n        app('config')->set([\n            'services.ldap.user_to_groups'     => true,\n            'services.ldap.group_attribute'    => 'memberOf',\n        ]);\n\n        $userResp = ['count' => 1, 0 => [\n            'uid'      => [$this->mockUser->name],\n            'cn'       => [$this->mockUser->name],\n            'dn'       => 'dc=test,' . config('services.ldap.base_dn'),\n            'mail'     => [$this->mockUser->email],\n        ]];\n        $groupResp = ['count' => 1,\n                      0 => [\n                          'dn'       => 'dc=test,' . config('services.ldap.base_dn'),\n                          'memberof' => [\n                              'count' => 1,\n                              0       => 'cn=ldaptester,ou=groups,dc=example,dc=com',\n                          ],\n                      ],\n        ];\n\n        $this->commonLdapMocks(1, 1, 3, 4, 2, 1);\n\n        $escapedName = ldap_escape($this->mockUser->name);\n        $this->mockLdap->shouldReceive('searchAndGetEntries')->twice()\n            ->with($this->resourceId, config('services.ldap.base_dn'), \"(&(uid={$escapedName}))\", \\Mockery::type('array'))\n            ->andReturn($userResp, $groupResp);\n\n        $this->mockLdap->shouldReceive('read')->times(1)\n            ->with($this->resourceId, 'cn=ldaptester,ou=groups,dc=example,dc=com', '(objectClass=*)', ['memberof'])\n            ->andReturn(['count' => 0]);\n        $this->mockLdap->shouldReceive('getEntries')->times(1)\n            ->with($this->resourceId, ['count' => 0])\n            ->andReturn(['count' => 0]);\n\n        $resp = $this->mockUserLogin();\n        $resp->assertRedirect('/');\n    }\n\n    public function test_external_auth_id_visible_in_roles_page_when_ldap_active()\n    {\n        $role = Role::factory()->create(['display_name' => 'ldaptester', 'external_auth_id' => 'ex-auth-a, test-second-param']);\n        $this->asAdmin()->get('/settings/roles/' . $role->id)\n            ->assertSee('ex-auth-a');\n    }\n\n    public function test_login_maps_roles_using_external_auth_ids_if_set()\n    {\n        $roleToReceive = Role::factory()->create(['display_name' => 'ldaptester', 'external_auth_id' => 'test-second-param, ex-auth-a']);\n        $roleToNotReceive = Role::factory()->create(['display_name' => 'ex-auth-a', 'external_auth_id' => 'test-second-param']);\n\n        app('config')->set([\n            'services.ldap.user_to_groups'     => true,\n            'services.ldap.group_attribute'    => 'memberOf',\n            'services.ldap.remove_from_groups' => true,\n        ]);\n\n        $this->commonLdapMocks(1, 1, 3, 4, 2, 1, 1);\n        $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)\n            ->with($this->resourceId, config('services.ldap.base_dn'), \\Mockery::type('string'), \\Mockery::type('array'))\n            ->andReturn(['count' => 1, 0 => [\n                'uid'      => [$this->mockUser->name],\n                'cn'       => [$this->mockUser->name],\n                'dn'       => 'dc=test' . config('services.ldap.base_dn'),\n                'mail'     => [$this->mockUser->email],\n                'memberof' => [\n                    'count' => 1,\n                    0       => 'cn=ex-auth-a,ou=groups,dc=example,dc=com',\n                ],\n            ]]);\n\n        $this->mockUserLogin()->assertRedirect('/');\n\n        $user = User::query()->where('email', $this->mockUser->email)->first();\n        $this->assertDatabaseHas('role_user', [\n            'user_id' => $user->id,\n            'role_id' => $roleToReceive->id,\n        ]);\n        $this->assertDatabaseMissing('role_user', [\n            'user_id' => $user->id,\n            'role_id' => $roleToNotReceive->id,\n        ]);\n    }\n\n    public function test_login_group_mapping_does_not_conflict_with_default_role()\n    {\n        $roleToReceive = Role::factory()->create(['display_name' => 'LdapTester']);\n        $roleToReceive2 = Role::factory()->create(['display_name' => 'LdapTester Second']);\n        $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();\n\n        setting()->put('registration-role', $roleToReceive->id);\n\n        app('config')->set([\n            'services.ldap.user_to_groups'     => true,\n            'services.ldap.group_attribute'    => 'memberOf',\n            'services.ldap.remove_from_groups' => true,\n        ]);\n\n        $this->commonLdapMocks(1, 1, 4, 5, 2, 2, 2);\n        $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)\n            ->with($this->resourceId, config('services.ldap.base_dn'), \\Mockery::type('string'), \\Mockery::type('array'))\n            ->andReturn(['count' => 1, 0 => [\n                'uid'      => [$this->mockUser->name],\n                'cn'       => [$this->mockUser->name],\n                'dn'       => 'dc=test' . config('services.ldap.base_dn'),\n                'mail'     => [$this->mockUser->email],\n                'memberof' => [\n                    'count' => 2,\n                    0       => 'cn=ldaptester,ou=groups,dc=example,dc=com',\n                    1       => 'cn=ldaptester-second,ou=groups,dc=example,dc=com',\n                ],\n            ]]);\n\n        $this->mockUserLogin()->assertRedirect('/');\n\n        $user = User::query()->where('email', $this->mockUser->email)->first();\n        $this->assertDatabaseHas('role_user', [\n            'user_id' => $user->id,\n            'role_id' => $roleToReceive->id,\n        ]);\n        $this->assertDatabaseHas('role_user', [\n            'user_id' => $user->id,\n            'role_id' => $roleToReceive2->id,\n        ]);\n    }\n\n    public function test_login_uses_specified_display_name_attribute()\n    {\n        app('config')->set([\n            'services.ldap.display_name_attribute' => 'displayName',\n        ]);\n\n        $this->commonLdapMocks(1, 1, 2, 4, 2);\n        $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)\n            ->with($this->resourceId, config('services.ldap.base_dn'), \\Mockery::type('string'), \\Mockery::type('array'))\n            ->andReturn(['count' => 1, 0 => [\n                'uid'         => [$this->mockUser->name],\n                'cn'          => [$this->mockUser->name],\n                'dn'          => 'dc=test' . config('services.ldap.base_dn'),\n                'displayname' => 'displayNameAttribute',\n            ]]);\n\n        $this->mockUserLogin()->assertRedirect('/login');\n        $this->get('/login')->assertSee('Please enter an email to use for this account.');\n\n        $resp = $this->mockUserLogin($this->mockUser->email);\n        $resp->assertRedirect('/');\n        $this->get('/')->assertSee('displayNameAttribute');\n        $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, 'name' => 'displayNameAttribute']);\n    }\n\n    public function test_login_uses_multiple_display_properties_if_defined()\n    {\n        app('config')->set([\n            'services.ldap.display_name_attribute' => 'firstname|middlename|noname|lastname',\n        ]);\n\n        $this->commonLdapMocks(1, 1, 1, 2, 1);\n        $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)\n            ->with($this->resourceId, config('services.ldap.base_dn'), \\Mockery::type('string'), \\Mockery::type('array'))\n            ->andReturn(['count' => 1, 0 => [\n                'uid'         => [$this->mockUser->name],\n                'cn'          => [$this->mockUser->name],\n                'dn'          => 'dc=test' . config('services.ldap.base_dn'),\n                'firstname' => ['Barry'],\n                'middlename' => ['Elliott'],\n                'lastname' => ['Chuckle'],\n                'mail'     => [$this->mockUser->email],\n            ]]);\n\n        $this->mockUserLogin();\n\n        $this->assertDatabaseHas('users', [\n            'email' => $this->mockUser->email,\n            'name' => 'Barry Elliott Chuckle',\n        ]);\n    }\n\n    public function test_login_uses_default_display_name_attribute_if_specified_not_present()\n    {\n        app('config')->set([\n            'services.ldap.display_name_attribute' => 'displayName',\n        ]);\n\n        $this->commonLdapMocks(1, 1, 2, 4, 2);\n        $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)\n            ->with($this->resourceId, config('services.ldap.base_dn'), \\Mockery::type('string'), \\Mockery::type('array'))\n            ->andReturn(['count' => 1, 0 => [\n                'uid' => [$this->mockUser->name],\n                'cn'  => [$this->mockUser->name],\n                'dn'  => 'dc=test' . config('services.ldap.base_dn'),\n            ]]);\n\n        $this->mockUserLogin()->assertRedirect('/login');\n        $this->get('/login')->assertSee('Please enter an email to use for this account.');\n\n        $resp = $this->mockUserLogin($this->mockUser->email);\n        $resp->assertRedirect('/');\n        $this->get('/')->assertSee($this->mockUser->name);\n        $this->assertDatabaseHas('users', [\n            'email'            => $this->mockUser->email,\n            'email_confirmed'  => false,\n            'external_auth_id' => $this->mockUser->name,\n            'name'             => $this->mockUser->name,\n        ]);\n    }\n\n    protected function checkLdapReceivesCorrectDetails($serverString, $expectedHostString): void\n    {\n        app('config')->set(['services.ldap.server' => $serverString]);\n\n        $this->mockLdap->shouldReceive('connect')\n            ->once()\n            ->with($expectedHostString)\n            ->andReturn(false);\n\n        $this->mockUserLogin();\n    }\n\n    public function test_ldap_receives_correct_connect_host_from_config()\n    {\n        $expectedResultByInput = [\n            'ldaps://bookstack:8080' => 'ldaps://bookstack:8080',\n            'ldap.bookstack.com:8080' => 'ldap://ldap.bookstack.com:8080',\n            'ldap.bookstack.com' => 'ldap://ldap.bookstack.com',\n            'ldaps://ldap.bookstack.com' => 'ldaps://ldap.bookstack.com',\n            'ldaps://ldap.bookstack.com ldap://a.b.com' => 'ldaps://ldap.bookstack.com ldap://a.b.com',\n        ];\n\n        foreach ($expectedResultByInput as $input => $expectedResult) {\n            $this->checkLdapReceivesCorrectDetails($input, $expectedResult);\n            $this->refreshApplication();\n            $this->setUp();\n        }\n    }\n\n    public function test_forgot_password_routes_inaccessible()\n    {\n        $resp = $this->get('/password/email');\n        $this->assertPermissionError($resp);\n\n        $resp = $this->post('/password/email');\n        $this->assertPermissionError($resp);\n\n        $resp = $this->get('/password/reset/abc123');\n        $this->assertPermissionError($resp);\n\n        $resp = $this->post('/password/reset');\n        $this->assertPermissionError($resp);\n    }\n\n    public function test_user_invite_routes_inaccessible()\n    {\n        $resp = $this->get('/register/invite/abc123');\n        $this->assertPermissionError($resp);\n\n        $resp = $this->post('/register/invite/abc123');\n        $this->assertPermissionError($resp);\n    }\n\n    public function test_user_register_routes_inaccessible()\n    {\n        $resp = $this->get('/register');\n        $this->assertPermissionError($resp);\n\n        $resp = $this->post('/register');\n        $this->assertPermissionError($resp);\n    }\n\n    public function test_dump_user_details_option_works()\n    {\n        config()->set(['services.ldap.dump_user_details' => true, 'services.ldap.thumbnail_attribute' => 'jpegphoto']);\n\n        $this->commonLdapMocks(1, 1, 1, 1, 1);\n        $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)\n            ->with($this->resourceId, config('services.ldap.base_dn'), \\Mockery::type('string'), \\Mockery::type('array'))\n            ->andReturn(['count' => 1, 0 => [\n                'uid' => [$this->mockUser->name],\n                'cn'  => [$this->mockUser->name],\n                // Test dumping binary data for avatar responses\n                'jpegphoto' => base64_decode('/9j/4AAQSkZJRg=='),\n                'dn'        => 'dc=test' . config('services.ldap.base_dn'),\n            ]]);\n\n        $resp = $this->post('/login', [\n            'username' => $this->mockUser->name,\n            'password' => $this->mockUser->password,\n        ]);\n        $resp->assertJsonStructure([\n            'details_from_ldap'        => [],\n            'details_bookstack_parsed' => [],\n        ]);\n    }\n\n    public function test_start_tls_called_if_option_set()\n    {\n        config()->set(['services.ldap.start_tls' => true]);\n        $this->mockLdap->shouldReceive('startTls')->once()->andReturn(true);\n        $this->runFailedAuthLogin();\n    }\n\n    public function test_connection_fails_if_tls_fails()\n    {\n        config()->set(['services.ldap.start_tls' => true]);\n        $this->mockLdap->shouldReceive('startTls')->once()->andReturn(false);\n        $this->commonLdapMocks(1, 1, 0, 0, 0);\n        $resp = $this->post('/login', ['username' => 'timmyjenkins', 'password' => 'cattreedog']);\n        $resp->assertStatus(500);\n    }\n\n    public function test_ldap_attributes_can_be_binary_decoded_if_marked()\n    {\n        config()->set(['services.ldap.id_attribute' => 'BIN;uid']);\n        $ldapService = app()->make(LdapService::class);\n        $this->commonLdapMocks(1, 1, 1, 1, 1);\n        $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)\n            ->with($this->resourceId, config('services.ldap.base_dn'), \\Mockery::type('string'), ['cn', 'dn', 'uid', 'mail', 'cn'])\n            ->andReturn(['count' => 1, 0 => [\n                'uid' => [hex2bin('FFF8F7')],\n                'cn'  => [$this->mockUser->name],\n                'dn'  => 'dc=test' . config('services.ldap.base_dn'),\n            ]]);\n\n        $details = $ldapService->getUserDetails('test');\n        $this->assertEquals('fff8f7', $details['uid']);\n    }\n\n    public function test_new_ldap_user_login_with_already_used_email_address_shows_error_message_to_user()\n    {\n        $this->commonLdapMocks(1, 1, 2, 4, 2);\n        $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)\n            ->with($this->resourceId, config('services.ldap.base_dn'), \\Mockery::type('string'), \\Mockery::type('array'))\n            ->andReturn(['count' => 1, 0 => [\n                'uid'  => [$this->mockUser->name],\n                'cn'   => [$this->mockUser->name],\n                'dn'   => 'dc=test' . config('services.ldap.base_dn'),\n                'mail' => 'tester@example.com',\n            ]], ['count' => 1, 0 => [\n                'uid'  => ['Barry'],\n                'cn'   => ['Scott'],\n                'dn'   => 'dc=bscott' . config('services.ldap.base_dn'),\n                'mail' => 'tester@example.com',\n            ]]);\n\n        // First user login\n        $this->mockUserLogin()->assertRedirect('/');\n\n        // Second user login\n        auth()->logout();\n        $resp = $this->followingRedirects()->post('/login', ['username' => 'bscott', 'password' => 'pass']);\n        $resp->assertSee('A user with the email tester@example.com already exists but with different credentials');\n    }\n\n    public function test_login_with_email_confirmation_required_maps_groups_but_shows_confirmation_screen()\n    {\n        $roleToReceive = Role::factory()->create(['display_name' => 'LdapTester']);\n        $user = User::factory()->make();\n        setting()->put('registration-confirmation', 'true');\n\n        app('config')->set([\n            'services.ldap.user_to_groups'     => true,\n            'services.ldap.group_attribute'    => 'memberOf',\n            'services.ldap.remove_from_groups' => true,\n        ]);\n\n        $this->commonLdapMocks(1, 1, 6, 8, 4, 2, 2);\n        $this->mockLdap->shouldReceive('searchAndGetEntries')\n            ->times(4)\n            ->andReturn(['count' => 1, 0 => [\n                'uid'      => [$user->name],\n                'cn'       => [$user->name],\n                'dn'       => 'dc=test' . config('services.ldap.base_dn'),\n                'mail'     => [$user->email],\n                'memberof' => [\n                    'count' => 1,\n                    0       => 'cn=ldaptester,ou=groups,dc=example,dc=com',\n                ],\n            ]]);\n\n        $login = $this->followingRedirects()->mockUserLogin();\n        $login->assertSee('Thanks for registering!');\n        $this->assertDatabaseHas('users', [\n            'email'           => $user->email,\n            'email_confirmed' => false,\n        ]);\n\n        $user = User::query()->where('email', '=', $user->email)->first();\n        $this->assertDatabaseHas('role_user', [\n            'user_id' => $user->id,\n            'role_id' => $roleToReceive->id,\n        ]);\n\n        $this->assertNull(auth()->user());\n\n        $homePage = $this->get('/');\n        $homePage->assertRedirect('/login');\n\n        $login = $this->followingRedirects()->mockUserLogin();\n        $login->assertSee('Email Address Not Confirmed');\n    }\n\n    public function test_failed_logins_are_logged_when_message_configured()\n    {\n        $log = $this->withTestLogger();\n        config()->set(['logging.failed_login.message' => 'Failed login for %u']);\n        $this->runFailedAuthLogin();\n        $this->assertTrue($log->hasWarningThatContains('Failed login for timmyjenkins'));\n    }\n\n    public function test_thumbnail_attribute_used_as_user_avatar_if_configured()\n    {\n        config()->set(['services.ldap.thumbnail_attribute' => 'jpegPhoto']);\n\n        $this->commonLdapMocks(1, 1, 1, 2, 1);\n        $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');\n        $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)\n            ->with($this->resourceId, config('services.ldap.base_dn'), \\Mockery::type('string'), \\Mockery::type('array'))\n            ->andReturn(['count' => 1, 0 => [\n                'cn'        => [$this->mockUser->name],\n                'dn'        => $ldapDn,\n                'jpegphoto' => [base64_decode('/9j/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8Q\nEBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k=')],\n                'mail' => [$this->mockUser->email],\n            ]]);\n\n        $this->mockUserLogin()\n            ->assertRedirect('/');\n\n        $user = User::query()->where('email', '=', $this->mockUser->email)->first();\n        $this->assertNotNull($user->avatar);\n        $this->assertEquals('8c90748342f19b195b9c6b4eff742ded', md5_file(public_path($user->avatar->path)));\n    }\n\n    public function test_tls_ca_cert_option_throws_if_set_to_invalid_location()\n    {\n        $path = 'non_found_' . time();\n        config()->set(['services.ldap.tls_ca_cert' => $path]);\n\n        $this->commonLdapMocks(0, 0, 0, 0, 0);\n\n        $this->assertThrows(function () {\n            $this->withoutExceptionHandling()->mockUserLogin();\n        }, LdapException::class, \"Provided path [{$path}] for LDAP TLS CA certs could not be resolved to an existing location\");\n    }\n\n    public function test_tls_ca_cert_option_used_if_set_to_a_folder()\n    {\n        $path = $this->files->testFilePath('');\n        config()->set(['services.ldap.tls_ca_cert' => $path]);\n\n        $this->mockLdap->shouldReceive('setOption')->once()->with(null, LDAP_OPT_X_TLS_CACERTDIR, rtrim($path, '/'))->andReturn(true);\n        $this->runFailedAuthLogin();\n    }\n\n    public function test_tls_ca_cert_option_used_if_set_to_a_file()\n    {\n        $path = $this->files->testFilePath('test-file.txt');\n        config()->set(['services.ldap.tls_ca_cert' => $path]);\n\n        $this->mockLdap->shouldReceive('setOption')->once()->with(null, LDAP_OPT_X_TLS_CACERTFILE, $path)->andReturn(true);\n        $this->runFailedAuthLogin();\n    }\n}\n"
  },
  {
    "path": "tests/Auth/LoginAutoInitiateTest.php",
    "content": "<?php\n\nnamespace Tests\\Auth;\n\nuse Tests\\TestCase;\n\nclass LoginAutoInitiateTest extends TestCase\n{\n    protected function setUp(): void\n    {\n        parent::setUp();\n\n        config()->set([\n            'auth.auto_initiate'        => true,\n            'services.google.client_id' => false,\n            'services.github.client_id' => false,\n        ]);\n    }\n\n    public function test_with_oidc()\n    {\n        config()->set([\n            'auth.method' => 'oidc',\n        ]);\n\n        $req = $this->get('/login');\n        $req->assertSeeText('Attempting Login');\n        $this->withHtml($req)->assertElementExists('form[action$=\"/oidc/login\"][method=POST][id=\"login-form\"] button');\n        $this->withHtml($req)->assertElementExists('button[form=\"login-form\"]');\n    }\n\n    public function test_with_saml2()\n    {\n        config()->set([\n            'auth.method' => 'saml2',\n        ]);\n\n        $req = $this->get('/login');\n        $req->assertSeeText('Attempting Login');\n        $this->withHtml($req)->assertElementExists('form[action$=\"/saml2/login\"][method=POST][id=\"login-form\"] button');\n        $this->withHtml($req)->assertElementExists('button[form=\"login-form\"]');\n    }\n\n    public function test_it_does_not_run_if_social_provider_is_active()\n    {\n        config()->set([\n            'auth.method'                   => 'oidc',\n            'services.google.client_id'     => 'abc123a',\n            'services.google.client_secret' => 'def456',\n        ]);\n\n        $req = $this->get('/login');\n        $req->assertDontSeeText('Attempting Login');\n        $req->assertSee('Log In');\n    }\n\n    public function test_it_does_not_run_if_prevent_query_string_exists()\n    {\n        config()->set([\n            'auth.method' => 'oidc',\n        ]);\n\n        $req = $this->get('/login?prevent_auto_init=true');\n        $req->assertDontSeeText('Attempting Login');\n        $req->assertSee('Log In');\n    }\n\n    public function test_logout_with_auto_init_leads_to_login_page_with_prevention_query()\n    {\n        config()->set([\n            'auth.method' => 'oidc',\n        ]);\n        $this->actingAs($this->users->editor());\n\n        $req = $this->post('/logout');\n        $req->assertRedirect('/login?prevent_auto_init=true');\n    }\n}\n"
  },
  {
    "path": "tests/Auth/MfaConfigurationTest.php",
    "content": "<?php\n\nnamespace Tests\\Auth;\n\nuse BookStack\\Access\\Mfa\\MfaValue;\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Support\\Facades\\Hash;\nuse PragmaRX\\Google2FA\\Google2FA;\nuse Tests\\TestCase;\n\nclass MfaConfigurationTest extends TestCase\n{\n    public function test_totp_setup()\n    {\n        $editor = $this->users->editor();\n        $this->assertDatabaseMissing('mfa_values', ['user_id' => $editor->id]);\n\n        // Setup page state\n        $resp = $this->actingAs($editor)->get('/mfa/setup');\n        $this->withHtml($resp)->assertElementContains('a[href$=\"/mfa/totp/generate\"]', 'Setup');\n\n        // Generate page access\n        $resp = $this->get('/mfa/totp/generate');\n        $resp->assertSee('Mobile App Setup');\n        $resp->assertSee('Verify Setup');\n        $this->withHtml($resp)->assertElementExists('form[action$=\"/mfa/totp/confirm\"] button');\n        $this->assertSessionHas('mfa-setup-totp-secret');\n        $svg = $this->withHtml($resp)->getOuterHtml('#main-content .card svg');\n\n        // Validation error, code should remain the same\n        $resp = $this->post('/mfa/totp/confirm', [\n            'code' => 'abc123',\n        ]);\n        $resp->assertRedirect('/mfa/totp/generate');\n        $resp = $this->followRedirects($resp);\n        $resp->assertSee('The provided code is not valid or has expired.');\n        $revisitSvg = $this->withHtml($resp)->getOuterHtml('#main-content .card svg');\n        $this->assertTrue($svg === $revisitSvg);\n        $secret = decrypt(session()->get('mfa-setup-totp-secret'));\n\n        $resp->assertSee(\"?secret={$secret}&issuer=BookStack&algorithm=SHA1&digits=6&period=30\");\n\n        // Successful confirmation\n        $google2fa = new Google2FA();\n        $otp = $google2fa->getCurrentOtp($secret);\n        $resp = $this->post('/mfa/totp/confirm', [\n            'code' => $otp,\n        ]);\n        $resp->assertRedirect('/mfa/setup');\n\n        // Confirmation of setup\n        $resp = $this->followRedirects($resp);\n        $resp->assertSee('Multi-factor method successfully configured');\n        $this->withHtml($resp)->assertElementContains('a[href$=\"/mfa/totp/generate\"]', 'Reconfigure');\n\n        $this->assertDatabaseHas('mfa_values', [\n            'user_id' => $editor->id,\n            'method'  => 'totp',\n        ]);\n        $this->assertFalse(session()->has('mfa-setup-totp-secret'));\n        $value = MfaValue::query()->where('user_id', '=', $editor->id)\n            ->where('method', '=', 'totp')->first();\n        $this->assertEquals($secret, decrypt($value->value));\n    }\n\n    public function test_backup_codes_setup()\n    {\n        $editor = $this->users->editor();\n        $this->assertDatabaseMissing('mfa_values', ['user_id' => $editor->id]);\n\n        // Setup page state\n        $resp = $this->actingAs($editor)->get('/mfa/setup');\n        $this->withHtml($resp)->assertElementContains('a[href$=\"/mfa/backup_codes/generate\"]', 'Setup');\n\n        // Generate page access\n        $resp = $this->get('/mfa/backup_codes/generate');\n        $resp->assertSee('Backup Codes');\n        $this->withHtml($resp)->assertElementContains('form[action$=\"/mfa/backup_codes/confirm\"]', 'Confirm and Enable');\n        $this->assertSessionHas('mfa-setup-backup-codes');\n        $codes = decrypt(session()->get('mfa-setup-backup-codes'));\n        // Check code format\n        $this->assertCount(16, $codes);\n        $this->assertEquals(16 * 11, strlen(implode('', $codes)));\n        // Check download link\n        $resp->assertSee(base64_encode(implode(\"\\n\\n\", $codes)));\n\n        // Confirm submit\n        $resp = $this->post('/mfa/backup_codes/confirm');\n        $resp->assertRedirect('/mfa/setup');\n\n        // Confirmation of setup\n        $resp = $this->followRedirects($resp);\n        $resp->assertSee('Multi-factor method successfully configured');\n        $this->withHtml($resp)->assertElementContains('a[href$=\"/mfa/backup_codes/generate\"]', 'Reconfigure');\n\n        $this->assertDatabaseHas('mfa_values', [\n            'user_id' => $editor->id,\n            'method'  => 'backup_codes',\n        ]);\n        $this->assertFalse(session()->has('mfa-setup-backup-codes'));\n        $value = MfaValue::query()->where('user_id', '=', $editor->id)\n            ->where('method', '=', 'backup_codes')->first();\n        $this->assertEquals($codes, json_decode(decrypt($value->value)));\n    }\n\n    public function test_backup_codes_cannot_be_confirmed_if_not_previously_generated()\n    {\n        $resp = $this->asEditor()->post('/mfa/backup_codes/confirm');\n        $resp->assertStatus(500);\n    }\n\n    public function test_mfa_method_count_is_visible_on_user_edit_page()\n    {\n        $user = $this->users->editor();\n        $resp = $this->actingAs($this->users->admin())->get($user->getEditUrl());\n        $resp->assertSee('0 methods configured');\n\n        MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, 'test');\n        $resp = $this->get($user->getEditUrl());\n        $resp->assertSee('1 method configured');\n\n        MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, 'test');\n        $resp = $this->get($user->getEditUrl());\n        $resp->assertSee('2 methods configured');\n    }\n\n    public function test_mfa_setup_link_only_shown_when_viewing_own_user_edit_page()\n    {\n        $admin = $this->users->admin();\n        $resp = $this->actingAs($admin)->get($admin->getEditUrl());\n        $this->withHtml($resp)->assertElementExists('a[href$=\"/mfa/setup\"]');\n\n        $resp = $this->actingAs($admin)->get($this->users->editor()->getEditUrl());\n        $this->withHtml($resp)->assertElementNotExists('a[href$=\"/mfa/setup\"]');\n    }\n\n    public function test_mfa_indicator_shows_in_user_list()\n    {\n        $admin = $this->users->admin();\n        User::query()->where('id', '!=', $admin->id)->delete();\n\n        $resp = $this->actingAs($admin)->get('/settings/users');\n        $this->withHtml($resp)->assertElementNotExists('[title=\"MFA Configured\"] svg');\n\n        MfaValue::upsertWithValue($admin, MfaValue::METHOD_TOTP, 'test');\n        $resp = $this->actingAs($admin)->get('/settings/users');\n        $this->withHtml($resp)->assertElementExists('[title=\"MFA Configured\"] svg');\n    }\n\n    public function test_remove_mfa_method()\n    {\n        $admin = $this->users->admin();\n\n        MfaValue::upsertWithValue($admin, MfaValue::METHOD_TOTP, 'test');\n        $this->assertEquals(1, $admin->mfaValues()->count());\n        $resp = $this->actingAs($admin)->get('/mfa/setup');\n        $this->withHtml($resp)->assertElementExists('form[action$=\"/mfa/totp/remove\"]');\n\n        $resp = $this->delete('/mfa/totp/remove');\n        $resp->assertRedirect('/mfa/setup');\n        $resp = $this->followRedirects($resp);\n        $resp->assertSee('Multi-factor method successfully removed');\n\n        $this->assertActivityExists(ActivityType::MFA_REMOVE_METHOD);\n        $this->assertEquals(0, $admin->mfaValues()->count());\n    }\n\n    public function test_mfa_required_if_set_on_role()\n    {\n        $user = $this->users->viewer();\n        $user->password = Hash::make('password');\n        $user->save();\n        /** @var Role $role */\n        $role = $user->roles()->first();\n        $role->mfa_enforced = true;\n        $role->save();\n\n        $resp = $this->post('/login', ['email' => $user->email, 'password' => 'password']);\n        $this->assertFalse(auth()->check());\n        $resp->assertRedirect('/mfa/verify');\n    }\n\n    public function test_mfa_required_if_mfa_option_configured()\n    {\n        $user = $this->users->viewer();\n        $user->password = Hash::make('password');\n        $user->save();\n        $user->mfaValues()->create([\n            'method' => MfaValue::METHOD_TOTP,\n            'value'  => 'test',\n        ]);\n\n        $resp = $this->post('/login', ['email' => $user->email, 'password' => 'password']);\n        $this->assertFalse(auth()->check());\n        $resp->assertRedirect('/mfa/verify');\n    }\n\n    public function test_totp_setup_url_shows_correct_user_when_setup_forced_upon_login()\n    {\n        $admin = $this->users->admin();\n        /** @var Role $role */\n        $role = $admin->roles()->first();\n        $role->mfa_enforced = true;\n        $role->save();\n\n        $resp = $this->post('/login', ['email' => $admin->email, 'password' => 'password']);\n        $this->assertFalse(auth()->check());\n        $resp->assertRedirect('/mfa/verify');\n\n        $resp = $this->get('/mfa/totp/generate');\n        $resp->assertSeeText('Mobile App Setup');\n        $resp->assertDontSee('otpauth://totp/BookStack:guest%40example.com', false);\n        $resp->assertSee('otpauth://totp/BookStack:admin%40admin.com', false);\n    }\n}\n"
  },
  {
    "path": "tests/Auth/MfaVerificationTest.php",
    "content": "<?php\n\nnamespace Tests\\Auth;\n\nuse BookStack\\Access\\LoginService;\nuse BookStack\\Access\\Mfa\\MfaValue;\nuse BookStack\\Access\\Mfa\\TotpService;\nuse BookStack\\Exceptions\\StoppedAuthenticationException;\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Support\\Facades\\Hash;\nuse PragmaRX\\Google2FA\\Google2FA;\nuse Tests\\TestCase;\nuse Tests\\TestResponse;\n\nclass MfaVerificationTest extends TestCase\n{\n    public function test_totp_verification()\n    {\n        [$user, $secret, $loginResp] = $this->startTotpLogin();\n        $loginResp->assertRedirect('/mfa/verify');\n\n        $resp = $this->get('/mfa/verify');\n        $resp->assertSee('Verify Access');\n        $resp->assertSee('Enter the code, generated using your mobile app, below:');\n        $this->withHtml($resp)->assertElementExists('form[action$=\"/mfa/totp/verify\"] input[name=\"code\"][autofocus]');\n\n        $google2fa = new Google2FA();\n        $resp = $this->post('/mfa/totp/verify', [\n            'code' => $google2fa->getCurrentOtp($secret),\n        ]);\n        $resp->assertRedirect('/');\n        $this->assertEquals($user->id, auth()->user()->id);\n    }\n\n    public function test_totp_verification_fails_on_missing_invalid_code()\n    {\n        [$user, $secret, $loginResp] = $this->startTotpLogin();\n\n        $resp = $this->get('/mfa/verify');\n        $resp = $this->post('/mfa/totp/verify', [\n            'code' => '',\n        ]);\n        $resp->assertRedirect('/mfa/verify');\n\n        $resp = $this->get('/mfa/verify');\n        $resp->assertSeeText('The code field is required.');\n        $this->assertNull(auth()->user());\n\n        $resp = $this->post('/mfa/totp/verify', [\n            'code' => '123321',\n        ]);\n        $resp->assertRedirect('/mfa/verify');\n        $resp = $this->get('/mfa/verify');\n\n        $resp->assertSeeText('The provided code is not valid or has expired.');\n        $this->assertNull(auth()->user());\n    }\n\n    public function test_totp_form_has_autofill_configured()\n    {\n        [$user, $secret, $loginResp] = $this->startTotpLogin();\n        $html = $this->withHtml($this->get('/mfa/verify'));\n\n        $html->assertElementExists('form[autocomplete=\"off\"][action$=\"/verify\"]');\n        $html->assertElementExists('input[autocomplete=\"one-time-code\"][name=\"code\"]');\n    }\n\n    public function test_backup_code_verification()\n    {\n        [$user, $codes, $loginResp] = $this->startBackupCodeLogin();\n        $loginResp->assertRedirect('/mfa/verify');\n\n        $resp = $this->get('/mfa/verify');\n        $resp->assertSee('Verify Access');\n        $resp->assertSee('Backup Code');\n        $resp->assertSee('Enter one of your remaining backup codes below:');\n        $this->withHtml($resp)->assertElementExists('form[action$=\"/mfa/backup_codes/verify\"] input[name=\"code\"]');\n\n        $resp = $this->post('/mfa/backup_codes/verify', [\n            'code' => $codes[1],\n        ]);\n\n        $resp->assertRedirect('/');\n        $this->assertEquals($user->id, auth()->user()->id);\n        // Ensure code no longer exists in available set\n        $userCodes = MfaValue::getValueForUser($user, MfaValue::METHOD_BACKUP_CODES);\n        $this->assertStringNotContainsString($codes[1], $userCodes);\n        $this->assertStringContainsString($codes[0], $userCodes);\n    }\n\n    public function test_backup_code_verification_fails_on_missing_or_invalid_code()\n    {\n        [$user, $codes, $loginResp] = $this->startBackupCodeLogin();\n\n        $resp = $this->get('/mfa/verify');\n        $resp = $this->post('/mfa/backup_codes/verify', [\n            'code' => '',\n        ]);\n        $resp->assertRedirect('/mfa/verify');\n\n        $resp = $this->get('/mfa/verify');\n        $resp->assertSeeText('The code field is required.');\n        $this->assertNull(auth()->user());\n\n        $resp = $this->post('/mfa/backup_codes/verify', [\n            'code' => 'ab123-ab456',\n        ]);\n        $resp->assertRedirect('/mfa/verify');\n\n        $resp = $this->get('/mfa/verify');\n        $resp->assertSeeText('The provided code is not valid or has already been used.');\n        $this->assertNull(auth()->user());\n    }\n\n    public function test_backup_code_verification_fails_on_attempted_code_reuse()\n    {\n        [$user, $codes, $loginResp] = $this->startBackupCodeLogin();\n\n        $this->post('/mfa/backup_codes/verify', [\n            'code' => $codes[0],\n        ]);\n        $this->assertNotNull(auth()->user());\n        auth()->logout();\n        session()->flush();\n\n        $this->post('/login', ['email' => $user->email, 'password' => 'password']);\n        $this->get('/mfa/verify');\n        $resp = $this->post('/mfa/backup_codes/verify', [\n            'code' => $codes[0],\n        ]);\n        $resp->assertRedirect('/mfa/verify');\n        $this->assertNull(auth()->user());\n\n        $resp = $this->get('/mfa/verify');\n        $resp->assertSeeText('The provided code is not valid or has already been used.');\n    }\n\n    public function test_backup_code_verification_shows_warning_when_limited_codes_remain()\n    {\n        [$user, $codes, $loginResp] = $this->startBackupCodeLogin(['abc12-def45', 'abc12-def46']);\n\n        $resp = $this->post('/mfa/backup_codes/verify', [\n            'code' => $codes[0],\n        ]);\n        $resp = $this->followRedirects($resp);\n        $resp->assertSeeText('You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.');\n    }\n\n    public function test_backup_code_form_has_autofill_configured()\n    {\n        [$user, $codes, $loginResp] = $this->startBackupCodeLogin();\n        $html = $this->withHtml($this->get('/mfa/verify'));\n\n        $html->assertElementExists('form[autocomplete=\"off\"][action$=\"/verify\"]');\n        $html->assertElementExists('input[autocomplete=\"one-time-code\"][name=\"code\"]');\n    }\n\n    public function test_both_mfa_options_available_if_set_on_profile()\n    {\n        $user = $this->users->editor();\n        $user->password = Hash::make('password');\n        $user->save();\n\n        MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, 'abc123');\n        MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, '[\"abc12-def456\"]');\n\n        /** @var TestResponse $mfaView */\n        $mfaView = $this->followingRedirects()->post('/login', [\n            'email'    => $user->email,\n            'password' => 'password',\n        ]);\n\n        // Totp shown by default\n        $this->withHtml($mfaView)->assertElementExists('form[action$=\"/mfa/totp/verify\"] input[name=\"code\"]');\n        $this->withHtml($mfaView)->assertElementContains('a[href$=\"/mfa/verify?method=backup_codes\"]', 'Verify using a backup code');\n\n        // Ensure can view backup_codes view\n        $resp = $this->get('/mfa/verify?method=backup_codes');\n        $this->withHtml($resp)->assertElementExists('form[action$=\"/mfa/backup_codes/verify\"] input[name=\"code\"]');\n        $this->withHtml($resp)->assertElementContains('a[href$=\"/mfa/verify?method=totp\"]', 'Verify using a mobile app');\n    }\n\n    public function test_mfa_required_with_no_methods_leads_to_setup()\n    {\n        $user = $this->users->editor();\n        $user->password = Hash::make('password');\n        $user->save();\n        /** @var Role $role */\n        $role = $user->roles->first();\n        $role->mfa_enforced = true;\n        $role->save();\n\n        $this->assertDatabaseMissing('mfa_values', [\n            'user_id' => $user->id,\n        ]);\n\n        /** @var TestResponse $resp */\n        $resp = $this->followingRedirects()->post('/login', [\n            'email'    => $user->email,\n            'password' => 'password',\n        ]);\n\n        $resp->assertSeeText('No Methods Configured');\n        $this->withHtml($resp)->assertElementContains('a[href$=\"/mfa/setup\"]', 'Configure');\n\n        $this->get('/mfa/backup_codes/generate');\n        $resp = $this->post('/mfa/backup_codes/confirm');\n        $resp->assertRedirect('/login');\n        $this->assertDatabaseHas('mfa_values', [\n            'user_id' => $user->id,\n        ]);\n\n        $resp = $this->get('/login');\n        $resp->assertSeeText('Multi-factor method configured, Please now login again using the configured method.');\n\n        $resp = $this->followingRedirects()->post('/login', [\n            'email'    => $user->email,\n            'password' => 'password',\n        ]);\n        $resp->assertSeeText('Enter one of your remaining backup codes below:');\n    }\n\n    public function test_mfa_setup_route_access()\n    {\n        $routes = [\n            ['get', '/mfa/setup'],\n            ['get', '/mfa/totp/generate'],\n            ['post', '/mfa/totp/confirm'],\n            ['get', '/mfa/backup_codes/generate'],\n            ['post', '/mfa/backup_codes/confirm'],\n        ];\n\n        // Non-auth access\n        foreach ($routes as [$method, $path]) {\n            $resp = $this->call($method, $path);\n            $resp->assertRedirect('/login');\n        }\n\n        // Attempted login user, who has configured mfa, access\n        // Sets up user that has MFA required after attempted login.\n        $loginService = $this->app->make(LoginService::class);\n        $user = $this->users->editor();\n        /** @var Role $role */\n        $role = $user->roles->first();\n        $role->mfa_enforced = true;\n        $role->save();\n\n        try {\n            $loginService->login($user, 'testing');\n        } catch (StoppedAuthenticationException $e) {\n        }\n        $this->assertNotNull($loginService->getLastLoginAttemptUser());\n\n        MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, '[]');\n        foreach ($routes as [$method, $path]) {\n            $resp = $this->call($method, $path);\n            $resp->assertRedirect('/login');\n        }\n    }\n\n    public function test_login_mfa_interception_does_not_log_error()\n    {\n        $logHandler = $this->withTestLogger();\n\n        [$user, $secret, $loginResp] = $this->startTotpLogin();\n\n        $loginResp->assertRedirect('/mfa/verify');\n        $this->assertFalse($logHandler->hasErrorRecords());\n    }\n\n    /**\n     * @return array<User, string, TestResponse>\n     */\n    protected function startTotpLogin(): array\n    {\n        $secret = $this->app->make(TotpService::class)->generateSecret();\n        $user = $this->users->editor();\n        $user->password = Hash::make('password');\n        $user->save();\n        MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, $secret);\n        $loginResp = $this->post('/login', [\n            'email'    => $user->email,\n            'password' => 'password',\n        ]);\n\n        return [$user, $secret, $loginResp];\n    }\n\n    /**\n     * @return array<User, string, TestResponse>\n     */\n    protected function startBackupCodeLogin($codes = ['kzzu6-1pgll', 'bzxnf-plygd', 'bwdsp-ysl51', '1vo93-ioy7n', 'lf7nw-wdyka', 'xmtrd-oplac']): array\n    {\n        $user = $this->users->editor();\n        $user->password = Hash::make('password');\n        $user->save();\n        MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, json_encode($codes));\n        $loginResp = $this->post('/login', [\n            'email'    => $user->email,\n            'password' => 'password',\n        ]);\n\n        return [$user, $codes, $loginResp];\n    }\n}\n"
  },
  {
    "path": "tests/Auth/OidcTest.php",
    "content": "<?php\n\nnamespace Tests\\Auth;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Facades\\Theme;\nuse BookStack\\Theming\\ThemeEvents;\nuse BookStack\\Uploads\\UserAvatars;\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Models\\User;\nuse GuzzleHttp\\Psr7\\Response;\nuse Illuminate\\Testing\\TestResponse;\nuse Tests\\Helpers\\OidcJwtHelper;\nuse Tests\\TestCase;\n\nclass OidcTest extends TestCase\n{\n    protected string $keyFilePath;\n    protected $keyFile;\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n        // Set default config for OpenID Connect\n\n        $this->keyFile = tmpfile();\n        $this->keyFilePath = 'file://' . stream_get_meta_data($this->keyFile)['uri'];\n        file_put_contents($this->keyFilePath, OidcJwtHelper::publicPemKey());\n\n        config()->set([\n            'auth.method'                 => 'oidc',\n            'auth.defaults.guard'         => 'oidc',\n            'oidc.name'                   => 'SingleSignOn-Testing',\n            'oidc.display_name_claims'    => 'name',\n            'oidc.client_id'              => OidcJwtHelper::defaultClientId(),\n            'oidc.client_secret'          => 'testpass',\n            'oidc.jwt_public_key'         => $this->keyFilePath,\n            'oidc.issuer'                 => OidcJwtHelper::defaultIssuer(),\n            'oidc.authorization_endpoint' => 'https://oidc.local/auth',\n            'oidc.token_endpoint'         => 'https://oidc.local/token',\n            'oidc.userinfo_endpoint'      => 'https://oidc.local/userinfo',\n            'oidc.discover'               => false,\n            'oidc.dump_user_details'      => false,\n            'oidc.additional_scopes'      => '',\n            'odic.fetch_avatar'           => false,\n            'oidc.user_to_groups'         => false,\n            'oidc.groups_claim'           => 'group',\n            'oidc.remove_from_groups'     => false,\n            'oidc.external_id_claim'      => 'sub',\n            'oidc.end_session_endpoint'   => false,\n        ]);\n    }\n\n    protected function tearDown(): void\n    {\n        parent::tearDown();\n        if (file_exists($this->keyFilePath)) {\n            unlink($this->keyFilePath);\n        }\n    }\n\n    public function test_login_option_shows_on_login_page()\n    {\n        $req = $this->get('/login');\n        $req->assertSeeText('SingleSignOn-Testing');\n        $this->withHtml($req)->assertElementExists('form[action$=\"/oidc/login\"][method=POST] button');\n    }\n\n    public function test_oidc_routes_are_only_active_if_oidc_enabled()\n    {\n        config()->set(['auth.method' => 'standard']);\n        $routes = ['/login' => 'post', '/callback' => 'get'];\n        foreach ($routes as $uri => $method) {\n            $req = $this->call($method, '/oidc' . $uri);\n            $this->assertPermissionError($req);\n        }\n    }\n\n    public function test_forgot_password_routes_inaccessible()\n    {\n        $resp = $this->get('/password/email');\n        $this->assertPermissionError($resp);\n\n        $resp = $this->post('/password/email');\n        $this->assertPermissionError($resp);\n\n        $resp = $this->get('/password/reset/abc123');\n        $this->assertPermissionError($resp);\n\n        $resp = $this->post('/password/reset');\n        $this->assertPermissionError($resp);\n    }\n\n    public function test_standard_login_routes_inaccessible()\n    {\n        $resp = $this->post('/login');\n        $this->assertPermissionError($resp);\n    }\n\n    public function test_logout_route_functions()\n    {\n        $this->actingAs($this->users->editor());\n        $this->post('/logout');\n        $this->assertFalse(auth()->check());\n    }\n\n    public function test_user_invite_routes_inaccessible()\n    {\n        $resp = $this->get('/register/invite/abc123');\n        $this->assertPermissionError($resp);\n\n        $resp = $this->post('/register/invite/abc123');\n        $this->assertPermissionError($resp);\n    }\n\n    public function test_user_register_routes_inaccessible()\n    {\n        $resp = $this->get('/register');\n        $this->assertPermissionError($resp);\n\n        $resp = $this->post('/register');\n        $this->assertPermissionError($resp);\n    }\n\n    public function test_login()\n    {\n        $req = $this->post('/oidc/login');\n        $redirect = $req->headers->get('location');\n\n        $this->assertStringStartsWith('https://oidc.local/auth', $redirect, 'Login redirects to SSO location');\n        $this->assertFalse($this->isAuthenticated());\n        $this->assertStringContainsString('scope=openid%20profile%20email', $redirect);\n        $this->assertStringContainsString('client_id=' . OidcJwtHelper::defaultClientId(), $redirect);\n        $this->assertStringContainsString('redirect_uri=' . urlencode(url('/oidc/callback')), $redirect);\n    }\n\n    public function test_login_success_flow()\n    {\n        // Start auth\n        $this->post('/oidc/login');\n        $state = explode(':', session()->get('oidc_state'), 2)[1];\n\n        $transactions = $this->mockHttpClient([$this->getMockAuthorizationResponse([\n            'email' => 'benny@example.com',\n            'sub'   => 'benny1010101',\n        ])]);\n\n        // Callback from auth provider\n        // App calls token endpoint to get id token\n        $resp = $this->get('/oidc/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=' . $state);\n        $resp->assertRedirect('/');\n        $this->assertEquals(1, $transactions->requestCount());\n        $tokenRequest = $transactions->latestRequest();\n        $this->assertEquals('https://oidc.local/token', (string) $tokenRequest->getUri());\n        $this->assertEquals('POST', $tokenRequest->getMethod());\n        $this->assertEquals('Basic ' . base64_encode(OidcJwtHelper::defaultClientId() . ':testpass'), $tokenRequest->getHeader('Authorization')[0]);\n        $this->assertStringContainsString('grant_type=authorization_code', $tokenRequest->getBody());\n        $this->assertStringContainsString('code=SplxlOBeZQQYbYS6WxSbIA', $tokenRequest->getBody());\n        $this->assertStringContainsString('redirect_uri=' . urlencode(url('/oidc/callback')), $tokenRequest->getBody());\n\n        $this->assertTrue(auth()->check());\n        $this->assertDatabaseHas('users', [\n            'email'            => 'benny@example.com',\n            'external_auth_id' => 'benny1010101',\n            'email_confirmed'  => false,\n        ]);\n\n        $user = User::query()->where('email', '=', 'benny@example.com')->first();\n        $this->assertActivityExists(ActivityType::AUTH_LOGIN, null, \"oidc; ({$user->id}) Barry Scott\");\n    }\n\n    public function test_login_uses_custom_additional_scopes_if_defined()\n    {\n        config()->set([\n            'oidc.additional_scopes' => 'groups, badgers',\n        ]);\n\n        $redirect = $this->post('/oidc/login')->headers->get('location');\n\n        $this->assertStringContainsString('scope=openid%20profile%20email%20groups%20badgers', $redirect);\n    }\n\n    public function test_callback_fails_if_no_state_present_or_matching()\n    {\n        $this->get('/oidc/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=abc124');\n        $this->assertSessionError('Login using SingleSignOn-Testing failed, system did not provide successful authorization');\n\n        $this->post('/oidc/login');\n        $this->get('/oidc/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=abc124');\n        $this->assertSessionError('Login using SingleSignOn-Testing failed, system did not provide successful authorization');\n    }\n\n    public function test_callback_works_even_if_other_request_made_by_session()\n    {\n        $this->mockHttpClient([$this->getMockAuthorizationResponse([\n            'email' => 'benny@example.com',\n            'sub'   => 'benny1010101',\n        ])]);\n\n        $this->post('/oidc/login');\n        $state = explode(':', session()->get('oidc_state'), 2)[1];\n\n        $this->get('/');\n\n        $resp = $this->get(\"/oidc/callback?code=SplxlOBeZQQYbYS6WxSbIA&state={$state}\");\n        $resp->assertRedirect('/');\n    }\n\n    public function test_callback_fails_if_state_timestamp_is_too_old()\n    {\n        $this->post('/oidc/login');\n        $state = explode(':', session()->get('oidc_state'), 2)[1];\n        session()->put('oidc_state', (time() - 60 * 4) . ':' . $state);\n\n        $this->get('/');\n\n        $resp = $this->get(\"/oidc/callback?code=SplxlOBeZQQYbYS6WxSbIA&state={$state}\");\n        $resp->assertRedirect('/login');\n        $this->assertSessionError('Login using SingleSignOn-Testing failed, system did not provide successful authorization');\n    }\n\n    public function test_dump_user_details_option_outputs_as_expected()\n    {\n        config()->set('oidc.dump_user_details', true);\n\n        $resp = $this->runLogin([\n            'email' => 'benny@example.com',\n            'sub'   => 'benny505',\n        ]);\n\n        $resp->assertStatus(200);\n        $resp->assertJson([\n            'email' => 'benny@example.com',\n            'sub'   => 'benny505',\n            'iss'   => OidcJwtHelper::defaultIssuer(),\n            'aud'   => OidcJwtHelper::defaultClientId(),\n        ]);\n        $this->assertFalse(auth()->check());\n    }\n\n    public function test_auth_fails_if_no_email_exists_in_user_data()\n    {\n        config()->set('oidc.userinfo_endpoint', null);\n\n        $this->runLogin([\n            'email' => '',\n            'sub'   => 'benny505',\n        ]);\n\n        $this->assertSessionError('Could not find an email address, for this user, in the data provided by the external authentication system');\n    }\n\n    public function test_auth_fails_if_already_logged_in()\n    {\n        $this->asEditor();\n\n        $this->runLogin([\n            'email' => 'benny@example.com',\n            'sub'   => 'benny505',\n        ]);\n\n        $this->assertSessionError('Already logged in');\n    }\n\n    public function test_auth_login_as_existing_user()\n    {\n        $editor = $this->users->editor();\n        $editor->external_auth_id = 'benny505';\n        $editor->save();\n\n        $this->assertFalse(auth()->check());\n\n        $this->runLogin([\n            'email' => 'benny@example.com',\n            'sub'   => 'benny505',\n        ]);\n\n        $this->assertTrue(auth()->check());\n        $this->assertEquals($editor->id, auth()->user()->id);\n    }\n\n    public function test_auth_login_as_existing_user_email_with_different_auth_id_fails()\n    {\n        $editor = $this->users->editor();\n        $editor->external_auth_id = 'editor101';\n        $editor->save();\n\n        $this->assertFalse(auth()->check());\n\n        $resp = $this->runLogin([\n            'email' => $editor->email,\n            'sub'   => 'benny505',\n        ]);\n        $resp = $this->followRedirects($resp);\n\n        $resp->assertSeeText('A user with the email ' . $editor->email . ' already exists but with different credentials.');\n        $this->assertFalse(auth()->check());\n    }\n\n    public function test_auth_login_with_invalid_token_fails()\n    {\n        $resp = $this->runLogin([\n            'sub' => null,\n        ]);\n        $resp = $this->followRedirects($resp);\n\n        $resp->assertSeeText('ID token validation failed with error: Missing token subject value');\n        $this->assertFalse(auth()->check());\n    }\n\n    public function test_auth_fails_if_endpoints_start_with_https()\n    {\n        $endpointConfigKeys = [\n            'oidc.token_endpoint' => 'tokenEndpoint',\n            'oidc.authorization_endpoint' => 'authorizationEndpoint',\n            'oidc.userinfo_endpoint' => 'userinfoEndpoint',\n        ];\n\n        foreach ($endpointConfigKeys as $endpointConfigKey => $endpointName) {\n            $logger = $this->withTestLogger();\n            $original = config()->get($endpointConfigKey);\n            $new = str_replace('https://', 'http://', $original);\n            config()->set($endpointConfigKey, $new);\n\n            $this->withoutExceptionHandling();\n            $err = null;\n            try {\n                $resp = $this->runLogin();\n                $resp->assertRedirect('/login');\n            } catch (\\Exception $exception) {\n                $err = $exception;\n            }\n            $this->assertEquals(\"Endpoint value for \\\"{$endpointName}\\\" must start with https://\", $err->getMessage());\n\n            config()->set($endpointConfigKey, $original);\n        }\n    }\n\n    public function test_auth_login_with_autodiscovery()\n    {\n        $this->withAutodiscovery();\n\n        $transactions = $this->mockHttpClient([\n            $this->getAutoDiscoveryResponse(),\n            $this->getJwksResponse(),\n        ]);\n\n        $this->assertFalse(auth()->check());\n\n        $this->runLogin();\n\n        $this->assertTrue(auth()->check());\n\n        $discoverRequest = $transactions->requestAt(0);\n        $keysRequest = $transactions->requestAt(1);\n        $this->assertEquals('GET', $keysRequest->getMethod());\n        $this->assertEquals('GET', $discoverRequest->getMethod());\n        $this->assertEquals(OidcJwtHelper::defaultIssuer() . '/.well-known/openid-configuration', $discoverRequest->getUri());\n        $this->assertEquals(OidcJwtHelper::defaultIssuer() . '/oidc/keys', $keysRequest->getUri());\n    }\n\n    public function test_auth_fails_if_autodiscovery_fails()\n    {\n        $this->withAutodiscovery();\n        $this->mockHttpClient([\n            new Response(404, [], 'Not found'),\n        ]);\n\n        $resp = $this->followRedirects($this->runLogin());\n        $this->assertFalse(auth()->check());\n        $resp->assertSeeText('Login using SingleSignOn-Testing failed, system did not provide successful authorization');\n    }\n\n    public function test_autodiscovery_calls_are_cached()\n    {\n        $this->withAutodiscovery();\n\n        $transactions = $this->mockHttpClient([\n            $this->getAutoDiscoveryResponse(),\n            $this->getJwksResponse(),\n            $this->getAutoDiscoveryResponse([\n                'issuer' => 'https://auto.example.com',\n            ]),\n            $this->getJwksResponse(),\n        ]);\n\n        // Initial run\n        $this->post('/oidc/login');\n        $this->assertEquals(2, $transactions->requestCount());\n        // Second run, hits cache\n        $this->post('/oidc/login');\n        $this->assertEquals(2, $transactions->requestCount());\n\n        // Third run, different issuer, new cache key\n        config()->set(['oidc.issuer' => 'https://auto.example.com']);\n        $this->post('/oidc/login');\n        $this->assertEquals(4, $transactions->requestCount());\n    }\n\n    public function test_auth_login_with_autodiscovery_with_keys_that_do_not_have_alg_property()\n    {\n        $this->withAutodiscovery();\n\n        $keyArray = OidcJwtHelper::publicJwkKeyArray();\n        unset($keyArray['alg']);\n\n        $this->mockHttpClient([\n            $this->getAutoDiscoveryResponse(),\n            new Response(200, [\n                'Content-Type'  => 'application/json',\n                'Cache-Control' => 'no-cache, no-store',\n                'Pragma'        => 'no-cache',\n            ], json_encode([\n                'keys' => [\n                    $keyArray,\n                ],\n            ])),\n        ]);\n\n        $this->assertFalse(auth()->check());\n        $this->runLogin();\n        $this->assertTrue(auth()->check());\n    }\n\n    public function test_auth_login_with_autodiscovery_with_keys_that_do_not_have_use_property()\n    {\n        // Based on reading the OIDC discovery spec:\n        // > This contains the signing key(s) the RP uses to validate signatures from the OP. The JWK Set MAY also\n        // > contain the Server's encryption key(s), which are used by RPs to encrypt requests to the Server. When\n        // > both signing and encryption keys are made available, a use (Key Use) parameter value is REQUIRED for all\n        // > keys in the referenced JWK Set to indicate each key's intended usage.\n        // We can assume that keys without use are intended for signing.\n        $this->withAutodiscovery();\n\n        $keyArray = OidcJwtHelper::publicJwkKeyArray();\n        unset($keyArray['use']);\n\n        $this->mockHttpClient([\n            $this->getAutoDiscoveryResponse(),\n            new Response(200, [\n                'Content-Type'  => 'application/json',\n                'Cache-Control' => 'no-cache, no-store',\n                'Pragma'        => 'no-cache',\n            ], json_encode([\n                'keys' => [\n                    $keyArray,\n                ],\n            ])),\n        ]);\n\n        $this->assertFalse(auth()->check());\n        $this->runLogin();\n        $this->assertTrue(auth()->check());\n    }\n\n    public function test_auth_uses_configured_external_id_claim_option()\n    {\n        config()->set([\n            'oidc.external_id_claim' => 'super_awesome_id',\n        ]);\n\n        $resp = $this->runLogin([\n            'email'            => 'benny@example.com',\n            'sub'              => 'benny1010101',\n            'super_awesome_id' => 'xXBennyTheGeezXx',\n        ]);\n        $resp->assertRedirect('/');\n\n        /** @var User $user */\n        $user = User::query()->where('email', '=', 'benny@example.com')->first();\n        $this->assertEquals('xXBennyTheGeezXx', $user->external_auth_id);\n    }\n\n    public function test_auth_uses_mulitple_display_name_claims_if_configured()\n    {\n        config()->set(['oidc.display_name_claims' => 'first_name|last_name']);\n\n        $this->runLogin([\n            'email'      => 'benny@example.com',\n            'sub'        => 'benny1010101',\n            'first_name' => 'Benny',\n            'last_name'  => 'Jenkins'\n        ]);\n\n        $this->assertDatabaseHas('users', [\n            'name' => 'Benny Jenkins',\n            'email' => 'benny@example.com',\n        ]);\n    }\n\n    public function test_user_avatar_fetched_from_picture_on_first_login_if_enabled()\n    {\n        config()->set(['oidc.fetch_avatar' => true]);\n\n        $this->runLogin([\n            'email' => 'avatar@example.com',\n            'picture' => 'https://example.com/my-avatar.jpg',\n        ], [\n            new Response(200, ['Content-Type' => 'image/jpeg'], $this->files->jpegImageData())\n        ]);\n\n        $user = User::query()->where('email', '=', 'avatar@example.com')->first();\n        $this->assertNotNull($user);\n\n        $this->assertTrue($user->avatar()->exists());\n    }\n\n    public function test_user_avatar_fetched_for_existing_user_when_no_avatar_already_assigned()\n    {\n        config()->set(['oidc.fetch_avatar' => true]);\n        $editor = $this->users->editor();\n        $editor->external_auth_id = 'benny509';\n        $editor->save();\n\n        $this->assertFalse($editor->avatar()->exists());\n\n        $this->runLogin([\n            'picture' => 'https://example.com/my-avatar.jpg',\n            'sub' => 'benny509',\n        ], [\n            new Response(200, ['Content-Type' => 'image/jpeg'], $this->files->jpegImageData())\n        ]);\n\n        $editor->refresh();\n        $this->assertTrue($editor->avatar()->exists());\n    }\n\n    public function test_user_avatar_not_fetched_if_image_data_format_unknown()\n    {\n        config()->set(['oidc.fetch_avatar' => true]);\n\n        $this->runLogin([\n            'email' => 'avatar-format@example.com',\n            'picture' => 'https://example.com/my-avatar.jpg',\n        ], [\n            new Response(200, ['Content-Type' => 'image/jpeg'], str_repeat('abc123', 5))\n        ]);\n\n        $user = User::query()->where('email', '=', 'avatar-format@example.com')->first();\n        $this->assertNotNull($user);\n\n        $this->assertFalse($user->avatar()->exists());\n    }\n\n    public function test_user_avatar_not_fetched_when_avatar_already_assigned()\n    {\n        config()->set(['oidc.fetch_avatar' => true]);\n        $editor = $this->users->editor();\n        $editor->external_auth_id = 'benny509';\n        $editor->save();\n\n        $avatars = $this->app->make(UserAvatars::class);\n        $originalImageData = $this->files->pngImageData();\n        $avatars->assignToUserFromExistingData($editor, $originalImageData, 'png');\n\n        $this->runLogin([\n            'picture' => 'https://example.com/my-avatar.jpg',\n            'sub' => 'benny509',\n        ], [\n            new Response(200, ['Content-Type' => 'image/jpeg'], $this->files->jpegImageData())\n        ]);\n\n        $editor->refresh();\n        $newAvatarData = file_get_contents($this->files->relativeToFullPath($editor->avatar->path));\n        $this->assertEquals($originalImageData, $newAvatarData);\n    }\n\n    public function test_user_avatar_fetch_follows_up_to_three_redirects()\n    {\n        config()->set(['oidc.fetch_avatar' => true]);\n\n        $logger = $this->withTestLogger();\n\n        $this->runLogin([\n            'email' => 'avatar@example.com',\n            'picture' => 'https://example.com/my-avatar.jpg',\n        ], [\n            new Response(302, ['Location' => 'https://example.com/a']),\n            new Response(302, ['Location' => 'https://example.com/b']),\n            new Response(302, ['Location' => 'https://example.com/c']),\n            new Response(302, ['Location' => 'https://example.com/d']),\n        ]);\n\n        $user = User::query()->where('email', '=', 'avatar@example.com')->first();\n        $this->assertFalse($user->avatar()->exists());\n\n        $this->assertStringContainsString('\"Failed to fetch image, max redirect limit of 3 tries reached. Last fetched URL: https://example.com/c\"', $logger->getRecords()[0]->formatted);\n    }\n\n    public function test_login_group_sync()\n    {\n        config()->set([\n            'oidc.user_to_groups'     => true,\n            'oidc.groups_claim'       => 'groups',\n            'oidc.remove_from_groups' => false,\n        ]);\n        $roleA = Role::factory()->create(['display_name' => 'Wizards']);\n        $roleB = Role::factory()->create(['display_name' => 'ZooFolks', 'external_auth_id' => 'zookeepers']);\n        $roleC = Role::factory()->create(['display_name' => 'Another Role']);\n\n        $resp = $this->runLogin([\n            'email'  => 'benny@example.com',\n            'sub'    => 'benny1010101',\n            'groups' => ['Wizards', 'Zookeepers'],\n        ]);\n        $resp->assertRedirect('/');\n\n        /** @var User $user */\n        $user = User::query()->where('email', '=', 'benny@example.com')->first();\n\n        $this->assertTrue($user->hasRole($roleA->id));\n        $this->assertTrue($user->hasRole($roleB->id));\n        $this->assertFalse($user->hasRole($roleC->id));\n    }\n\n    public function test_login_group_sync_with_nested_groups_in_token()\n    {\n        config()->set([\n            'oidc.user_to_groups'     => true,\n            'oidc.groups_claim'       => 'my.custom.groups.attr',\n            'oidc.remove_from_groups' => false,\n        ]);\n        $roleA = Role::factory()->create(['display_name' => 'Wizards']);\n\n        $resp = $this->runLogin([\n            'email'  => 'benny@example.com',\n            'sub'    => 'benny1010101',\n            'my'     => [\n                'custom' => [\n                    'groups' => [\n                        'attr' => ['Wizards'],\n                    ],\n                ],\n            ],\n        ]);\n        $resp->assertRedirect('/');\n\n        /** @var User $user */\n        $user = User::query()->where('email', '=', 'benny@example.com')->first();\n        $this->assertTrue($user->hasRole($roleA->id));\n    }\n\n    public function test_oidc_logout_form_active_when_oidc_active()\n    {\n        $this->runLogin();\n\n        $resp = $this->get('/');\n        $this->withHtml($resp)->assertElementExists('header form[action$=\"/oidc/logout\"] button');\n    }\n    public function test_logout_with_autodiscovery_with_oidc_logout_enabled()\n    {\n        config()->set(['oidc.end_session_endpoint' => true]);\n        $this->withAutodiscovery();\n\n        $transactions = $this->mockHttpClient([\n            $this->getAutoDiscoveryResponse(),\n            $this->getJwksResponse(),\n        ]);\n\n        $resp = $this->asEditor()->post('/oidc/logout');\n        $resp->assertRedirect('https://auth.example.com/oidc/logout?post_logout_redirect_uri=' . urlencode(url('/')));\n\n        $this->assertEquals(2, $transactions->requestCount());\n        $this->assertFalse(auth()->check());\n    }\n\n    public function test_logout_with_autodiscovery_with_oidc_logout_disabled()\n    {\n        $this->withAutodiscovery();\n        config()->set(['oidc.end_session_endpoint' => false]);\n\n        $this->mockHttpClient([\n            $this->getAutoDiscoveryResponse(),\n            $this->getJwksResponse(),\n        ]);\n\n        $resp = $this->asEditor()->post('/oidc/logout');\n        $resp->assertRedirect('/');\n        $this->assertFalse(auth()->check());\n    }\n\n    public function test_logout_without_autodiscovery_but_with_endpoint_configured()\n    {\n        config()->set(['oidc.end_session_endpoint' => 'https://example.com/logout']);\n\n        $resp = $this->asEditor()->post('/oidc/logout');\n        $resp->assertRedirect('https://example.com/logout?post_logout_redirect_uri=' . urlencode(url('/')));\n        $this->assertFalse(auth()->check());\n    }\n\n    public function test_logout_without_autodiscovery_with_configured_endpoint_adds_to_query_if_existing()\n    {\n        config()->set(['oidc.end_session_endpoint' => 'https://example.com/logout?a=b']);\n\n        $resp = $this->asEditor()->post('/oidc/logout');\n        $resp->assertRedirect('https://example.com/logout?a=b&post_logout_redirect_uri=' . urlencode(url('/')));\n        $this->assertFalse(auth()->check());\n    }\n\n    public function test_logout_with_autodiscovery_and_auto_initiate_returns_to_auto_prevented_login()\n    {\n        $this->withAutodiscovery();\n        config()->set([\n            'auth.auto_initiate' => true,\n            'services.google.client_id' => false,\n            'services.github.client_id' => false,\n            'oidc.end_session_endpoint' => true,\n        ]);\n\n        $this->mockHttpClient([\n            $this->getAutoDiscoveryResponse(),\n            $this->getJwksResponse(),\n        ]);\n\n        $resp = $this->asEditor()->post('/oidc/logout');\n\n        $redirectUrl = url('/login?prevent_auto_init=true');\n        $resp->assertRedirect('https://auth.example.com/oidc/logout?post_logout_redirect_uri=' . urlencode($redirectUrl));\n        $this->assertFalse(auth()->check());\n    }\n\n    public function test_logout_endpoint_url_overrides_autodiscovery_endpoint()\n    {\n        config()->set(['oidc.end_session_endpoint' => 'https://a.example.com']);\n        $this->withAutodiscovery();\n\n        $transactions = $this->mockHttpClient([\n            $this->getAutoDiscoveryResponse(),\n            $this->getJwksResponse(),\n        ]);\n\n        $resp = $this->asEditor()->post('/oidc/logout');\n        $resp->assertRedirect('https://a.example.com?post_logout_redirect_uri=' . urlencode(url('/')));\n\n        $this->assertEquals(2, $transactions->requestCount());\n        $this->assertFalse(auth()->check());\n    }\n\n    public function test_logout_with_autodiscovery_does_not_use_rp_logout_if_no_url_via_autodiscovery()\n    {\n        config()->set(['oidc.end_session_endpoint' => true]);\n        $this->withAutodiscovery();\n\n        $this->mockHttpClient([\n            $this->getAutoDiscoveryResponse(['end_session_endpoint' => null]),\n            $this->getJwksResponse(),\n        ]);\n\n        $resp = $this->asEditor()->post('/oidc/logout');\n        $resp->assertRedirect('/');\n        $this->assertFalse(auth()->check());\n    }\n\n    public function test_logout_redirect_contains_id_token_hint_if_existing()\n    {\n        config()->set(['oidc.end_session_endpoint' => 'https://example.com/logout']);\n\n        // Fix times so our token is predictable\n        $claimOverrides = [\n            'iat' => time(),\n            'exp' => time() + 720,\n            'auth_time' => time()\n        ];\n        $this->runLogin($claimOverrides);\n\n        $resp = $this->asEditor()->post('/oidc/logout');\n        $query = 'id_token_hint=' . urlencode(OidcJwtHelper::idToken($claimOverrides)) .  '&post_logout_redirect_uri=' . urlencode(url('/'));\n        $resp->assertRedirect('https://example.com/logout?' . $query);\n    }\n\n    public function test_oidc_id_token_pre_validate_theme_event_without_return()\n    {\n        $args = [];\n        $callback = function (...$eventArgs) use (&$args) {\n            $args = $eventArgs;\n        };\n        Theme::listen(ThemeEvents::OIDC_ID_TOKEN_PRE_VALIDATE, $callback);\n\n        $resp = $this->runLogin([\n            'email' => 'benny@example.com',\n            'sub'   => 'benny1010101',\n            'name'  => 'Benny',\n        ]);\n        $resp->assertRedirect('/');\n\n        $this->assertDatabaseHas('users', [\n            'external_auth_id' => 'benny1010101',\n        ]);\n\n        $this->assertArrayHasKey('iss', $args[0]);\n        $this->assertArrayHasKey('sub', $args[0]);\n        $this->assertEquals('Benny', $args[0]['name']);\n        $this->assertEquals('benny1010101', $args[0]['sub']);\n\n        $this->assertArrayHasKey('access_token', $args[1]);\n        $this->assertArrayHasKey('expires_in', $args[1]);\n        $this->assertArrayHasKey('refresh_token', $args[1]);\n    }\n\n    public function test_oidc_id_token_pre_validate_theme_event_with_return()\n    {\n        $callback = function (...$eventArgs) {\n            return array_merge($eventArgs[0], [\n                'email' => 'lenny@example.com',\n                'sub' => 'lenny1010101',\n                'name' => 'Lenny',\n            ]);\n        };\n        Theme::listen(ThemeEvents::OIDC_ID_TOKEN_PRE_VALIDATE, $callback);\n\n        $resp = $this->runLogin([\n            'email' => 'benny@example.com',\n            'sub'   => 'benny1010101',\n            'name'  => 'Benny',\n        ]);\n        $resp->assertRedirect('/');\n\n        $this->assertDatabaseHas('users', [\n            'email' => 'lenny@example.com',\n            'external_auth_id' => 'lenny1010101',\n            'name' => 'Lenny',\n        ]);\n    }\n\n    public function test_oidc_auth_pre_redirect_theme_event_with_return()\n    {\n        $args = [];\n        $callback = function (...$eventArgs) use (&$args) {\n            $args = $eventArgs;\n            return 'https://cats.example.com?beans=true';\n        };\n        Theme::listen(ThemeEvents::OIDC_AUTH_PRE_REDIRECT, $callback);\n\n        $resp = $this->post('/oidc/login');\n        $resp->assertRedirect('https://cats.example.com?beans=true');\n\n        $this->assertCount(1, $args);\n        $this->assertStringStartsWith('https://oidc.local/auth', $args[0]);\n    }\n\n    public function test_oidc_auth_pre_redirect_theme_event_with_no_return()\n    {\n        $callback = function ($redirectUrl) {\n            $redirectUrl = 'cat';\n        };\n        Theme::listen(ThemeEvents::OIDC_AUTH_PRE_REDIRECT, $callback);\n\n        $resp = $this->post('/oidc/login');\n        $redirect = $resp->headers->get('Location');\n        $this->assertStringStartsWith('https://oidc.local/auth?', $redirect);\n    }\n\n    public function test_pkce_used_on_authorize_and_access()\n    {\n        // Start auth\n        $resp = $this->post('/oidc/login');\n        $state = explode(':', session()->get('oidc_state'), 2)[1];\n\n        $pkceCode = session()->get('oidc_pkce_code');\n        $this->assertGreaterThan(30, strlen($pkceCode));\n\n        $expectedCodeChallenge = trim(strtr(base64_encode(hash('sha256', $pkceCode, true)), '+/', '-_'), '=');\n        $redirect = $resp->headers->get('Location');\n        $redirectParams = [];\n        parse_str(parse_url($redirect, PHP_URL_QUERY), $redirectParams);\n        $this->assertEquals($expectedCodeChallenge, $redirectParams['code_challenge']);\n        $this->assertEquals('S256', $redirectParams['code_challenge_method']);\n\n        $transactions = $this->mockHttpClient([$this->getMockAuthorizationResponse([\n            'email' => 'benny@example.com',\n            'sub'   => 'benny1010101',\n        ])]);\n\n        $this->get('/oidc/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=' . $state);\n        $tokenRequest = $transactions->latestRequest();\n        $bodyParams = [];\n        parse_str($tokenRequest->getBody(), $bodyParams);\n        $this->assertEquals($pkceCode, $bodyParams['code_verifier']);\n    }\n\n    public function test_userinfo_endpoint_used_if_missing_claims_in_id_token()\n    {\n        config()->set('oidc.display_name_claims', 'first_name|last_name');\n        $this->post('/oidc/login');\n        $state = explode(':', session()->get('oidc_state'), 2)[1];\n\n        $client = $this->mockHttpClient([\n            $this->getMockAuthorizationResponse(['name' => null]),\n            new Response(200, [\n                'Content-Type'  => 'application/json',\n            ], json_encode([\n                'sub' => OidcJwtHelper::defaultPayload()['sub'],\n                'first_name' => 'Barry',\n                'last_name' => 'Userinfo',\n            ]))\n        ]);\n\n        $resp = $this->get('/oidc/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=' . $state);\n        $resp->assertRedirect('/');\n        $this->assertEquals(2, $client->requestCount());\n\n        $userinfoRequest = $client->requestAt(1);\n        $this->assertEquals('GET', $userinfoRequest->getMethod());\n        $this->assertEquals('https://oidc.local/userinfo', (string) $userinfoRequest->getUri());\n\n        $this->assertEquals('Barry Userinfo', user()->name);\n    }\n\n    public function test_userinfo_endpoint_fetch_with_different_sub_throws_error()\n    {\n        $userinfoResponseData = ['sub' => 'dcba4321'];\n        $userinfoResponse = new Response(200, ['Content-Type'  => 'application/json'], json_encode($userinfoResponseData));\n        $resp = $this->runLogin(['name' => null], [$userinfoResponse]);\n        $resp->assertRedirect('/login');\n        $this->assertSessionError('Userinfo endpoint response validation failed with error: Subject value provided in the userinfo endpoint does not match the provided ID token value');\n    }\n\n    public function test_userinfo_endpoint_fetch_returning_no_sub_throws_error()\n    {\n        $userinfoResponseData = ['name' => 'testing'];\n        $userinfoResponse = new Response(200, ['Content-Type'  => 'application/json'], json_encode($userinfoResponseData));\n        $resp = $this->runLogin(['name' => null], [$userinfoResponse]);\n        $resp->assertRedirect('/login');\n        $this->assertSessionError('Userinfo endpoint response validation failed with error: No valid subject value found in userinfo data');\n    }\n\n    public function test_userinfo_endpoint_fetch_can_parsed_nested_groups()\n    {\n        config()->set([\n            'oidc.user_to_groups'     => true,\n            'oidc.groups_claim'       => 'my.nested.groups.attr',\n            'oidc.remove_from_groups' => false,\n        ]);\n\n        $roleA = Role::factory()->create(['display_name' => 'Ducks']);\n        $userinfoResponseData = [\n            'sub' => OidcJwtHelper::defaultPayload()['sub'],\n            'my' => ['nested' => ['groups' => ['attr' => ['Ducks', 'Donkeys']]]]\n        ];\n        $userinfoResponse = new Response(200, ['Content-Type'  => 'application/json'], json_encode($userinfoResponseData));\n        $resp = $this->runLogin(['groups' => null], [$userinfoResponse]);\n        $resp->assertRedirect('/');\n\n        $user = User::where('email', OidcJwtHelper::defaultPayload()['email'])->first();\n        $this->assertTrue($user->hasRole($roleA->id));\n    }\n\n    public function test_userinfo_endpoint_response_with_complex_json_content_type_handled()\n    {\n        $userinfoResponseData = [\n            'sub' => OidcJwtHelper::defaultPayload()['sub'],\n            'name' => 'Barry',\n        ];\n        $userinfoResponse = new Response(200, ['Content-Type'  => 'Application/Json ; charset=utf-8'], json_encode($userinfoResponseData));\n        $resp = $this->runLogin(['name' => null], [$userinfoResponse]);\n        $resp->assertRedirect('/');\n\n        $user = User::where('email', OidcJwtHelper::defaultPayload()['email'])->first();\n        $this->assertEquals('Barry', $user->name);\n    }\n\n    public function test_userinfo_endpoint_jwks_response_handled()\n    {\n        $userinfoResponseData = OidcJwtHelper::idToken(['name' => 'Barry Jwks']);\n        $userinfoResponse = new Response(200, ['Content-Type'  => 'application/jwt'], $userinfoResponseData);\n\n        $resp = $this->runLogin(['name' => null], [$userinfoResponse]);\n        $resp->assertRedirect('/');\n\n        $user = User::where('email', OidcJwtHelper::defaultPayload()['email'])->first();\n        $this->assertEquals('Barry Jwks', $user->name);\n    }\n\n    public function test_userinfo_endpoint_jwks_response_returning_no_sub_throws()\n    {\n        $userinfoResponseData = OidcJwtHelper::idToken(['sub' => null]);\n        $userinfoResponse = new Response(200, ['Content-Type'  => 'application/jwt'], $userinfoResponseData);\n\n        $resp = $this->runLogin(['name' => null], [$userinfoResponse]);\n        $resp->assertRedirect('/login');\n        $this->assertSessionError('Userinfo endpoint response validation failed with error: No valid subject value found in userinfo data');\n    }\n\n    public function test_userinfo_endpoint_jwks_response_returning_non_matching_sub_throws()\n    {\n        $userinfoResponseData = OidcJwtHelper::idToken(['sub' => 'zzz123']);\n        $userinfoResponse = new Response(200, ['Content-Type'  => 'application/jwt'], $userinfoResponseData);\n\n        $resp = $this->runLogin(['name' => null], [$userinfoResponse]);\n        $resp->assertRedirect('/login');\n        $this->assertSessionError('Userinfo endpoint response validation failed with error: Subject value provided in the userinfo endpoint does not match the provided ID token value');\n    }\n\n    public function test_userinfo_endpoint_jwks_response_with_invalid_signature_throws()\n    {\n        $userinfoResponseData = OidcJwtHelper::idToken();\n        $exploded = explode('.', $userinfoResponseData);\n        $exploded[2] = base64_encode(base64_decode($exploded[2]) . 'ABC');\n        $userinfoResponse = new Response(200, ['Content-Type'  => 'application/jwt'], implode('.', $exploded));\n\n        $resp = $this->runLogin(['name' => null], [$userinfoResponse]);\n        $resp->assertRedirect('/login');\n        $this->assertSessionError('Userinfo endpoint response validation failed with error: Token signature could not be validated using the provided keys');\n    }\n\n    public function test_userinfo_endpoint_jwks_response_with_invalid_signature_alg_throws()\n    {\n        $userinfoResponseData = OidcJwtHelper::idToken([], ['alg' => 'ZZ512']);\n        $userinfoResponse = new Response(200, ['Content-Type'  => 'application/jwt'], $userinfoResponseData);\n\n        $resp = $this->runLogin(['name' => null], [$userinfoResponse]);\n        $resp->assertRedirect('/login');\n        $this->assertSessionError('Userinfo endpoint response validation failed with error: Only RS256 signature validation is supported. Token reports using ZZ512');\n    }\n\n    public function test_userinfo_endpoint_response_with_invalid_content_type_throws()\n    {\n        $userinfoResponse = new Response(200, ['Content-Type'  => 'application/beans'], json_encode(OidcJwtHelper::defaultPayload()));\n        $resp = $this->runLogin(['name' => null], [$userinfoResponse]);\n        $resp->assertRedirect('/login');\n        $this->assertSessionError('Userinfo endpoint response validation failed with error: No valid subject value found in userinfo data');\n    }\n\n    public function test_userinfo_endpoint_not_called_if_empty_groups_array_provided_in_id_token()\n    {\n        config()->set([\n            'oidc.user_to_groups'     => true,\n            'oidc.groups_claim'       => 'groups',\n            'oidc.remove_from_groups' => false,\n        ]);\n\n        $this->post('/oidc/login');\n        $state = explode(':', session()->get('oidc_state'), 2)[1];\n        $client = $this->mockHttpClient([$this->getMockAuthorizationResponse([\n            'groups' => [],\n        ])]);\n\n        $resp = $this->get('/oidc/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=' . $state);\n        $resp->assertRedirect('/');\n        $this->assertEquals(1, $client->requestCount());\n        $this->assertTrue(auth()->check());\n    }\n\n    protected function withAutodiscovery(): void\n    {\n        config()->set([\n            'oidc.issuer'                 => OidcJwtHelper::defaultIssuer(),\n            'oidc.discover'               => true,\n            'oidc.authorization_endpoint' => null,\n            'oidc.token_endpoint'         => null,\n            'oidc.userinfo_endpoint'      => null,\n            'oidc.jwt_public_key'         => null,\n        ]);\n    }\n\n    protected function runLogin($claimOverrides = [], $additionalHttpResponses = []): TestResponse\n    {\n        $this->post('/oidc/login');\n        $state = explode(':', session()->get('oidc_state'), 2)[1] ?? '';\n        $this->mockHttpClient([$this->getMockAuthorizationResponse($claimOverrides), ...$additionalHttpResponses]);\n\n        return $this->get('/oidc/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=' . $state);\n    }\n\n    protected function getAutoDiscoveryResponse($responseOverrides = []): Response\n    {\n        return new Response(200, [\n            'Content-Type'  => 'application/json',\n            'Cache-Control' => 'no-cache, no-store',\n            'Pragma'        => 'no-cache',\n        ], json_encode(array_merge([\n            'token_endpoint'         => OidcJwtHelper::defaultIssuer() . '/oidc/token',\n            'authorization_endpoint' => OidcJwtHelper::defaultIssuer() . '/oidc/authorize',\n            'userinfo_endpoint'      => OidcJwtHelper::defaultIssuer() . '/oidc/userinfo',\n            'jwks_uri'               => OidcJwtHelper::defaultIssuer() . '/oidc/keys',\n            'issuer'                 => OidcJwtHelper::defaultIssuer(),\n            'end_session_endpoint'   => OidcJwtHelper::defaultIssuer() . '/oidc/logout',\n        ], $responseOverrides)));\n    }\n\n    protected function getJwksResponse(): Response\n    {\n        return new Response(200, [\n            'Content-Type'  => 'application/json',\n            'Cache-Control' => 'no-cache, no-store',\n            'Pragma'        => 'no-cache',\n        ], json_encode([\n            'keys' => [\n                OidcJwtHelper::publicJwkKeyArray(),\n            ],\n        ]));\n    }\n\n    protected function getMockAuthorizationResponse($claimOverrides = []): Response\n    {\n        return new Response(200, [\n            'Content-Type'  => 'application/json',\n            'Cache-Control' => 'no-cache, no-store',\n            'Pragma'        => 'no-cache',\n        ], json_encode([\n            'access_token' => 'abc123',\n            'token_type'   => 'Bearer',\n            'expires_in'   => 3600,\n            'id_token'     => OidcJwtHelper::idToken($claimOverrides),\n        ]));\n    }\n}\n"
  },
  {
    "path": "tests/Auth/RegistrationTest.php",
    "content": "<?php\n\nnamespace Tests\\Auth;\n\nuse BookStack\\Access\\Notifications\\ConfirmEmailNotification;\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Notification;\nuse Tests\\TestCase;\n\nclass RegistrationTest extends TestCase\n{\n    public function test_confirmed_registration()\n    {\n        // Fake notifications\n        Notification::fake();\n\n        // Set settings and get user instance\n        $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']);\n        $user = User::factory()->make();\n\n        // Go through registration process\n        $resp = $this->post('/register', $user->only('name', 'email', 'password'));\n        $resp->assertRedirect('/register/confirm');\n        $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);\n\n        $resp = $this->get('/register/confirm');\n        $resp->assertSee('Thanks for registering!');\n\n        // Ensure notification sent\n        /** @var User $dbUser */\n        $dbUser = User::query()->where('email', '=', $user->email)->first();\n        Notification::assertSentTo($dbUser, ConfirmEmailNotification::class);\n\n        // Test access and resend confirmation email\n        $resp = $this->post('/login', ['email' => $user->email, 'password' => $user->password]);\n        $resp->assertRedirect('/register/confirm/awaiting');\n\n        $resp = $this->get('/register/confirm/awaiting');\n        $this->withHtml($resp)->assertElementContains('form[action=\"' . url('/register/confirm/resend') . '\"]', 'Resend');\n\n        $this->get('/books')->assertRedirect('/login');\n        $this->post('/register/confirm/resend', $user->only('email'));\n\n        // Get confirmation and confirm notification matches\n        $emailConfirmation = DB::table('email_confirmations')->where('user_id', '=', $dbUser->id)->first();\n        Notification::assertSentTo($dbUser, ConfirmEmailNotification::class, function ($notification, $channels) use ($emailConfirmation) {\n            return $notification->token === $emailConfirmation->token;\n        });\n\n        // Check confirmation email confirmation accept page.\n        $resp = $this->get('/register/confirm/' . $emailConfirmation->token);\n        $acceptPage = $this->withHtml($resp);\n        $resp->assertOk();\n        $resp->assertSee('Thanks for confirming!');\n        $acceptPage->assertElementExists('form[method=\"post\"][action$=\"/register/confirm/accept\"][component=\"auto-submit\"] button');\n        $acceptPage->assertFieldHasValue('token', $emailConfirmation->token);\n\n        // Check acceptance confirm\n        $this->post('/register/confirm/accept', ['token' => $emailConfirmation->token])->assertRedirect('/login');\n\n        // Check state on login redirect\n        $this->get('/login')->assertSee('Your email has been confirmed! You should now be able to login using this email address.');\n        $this->assertDatabaseMissing('email_confirmations', ['token' => $emailConfirmation->token]);\n        $this->assertDatabaseHas('users', ['name' => $dbUser->name, 'email' => $dbUser->email, 'email_confirmed' => true]);\n    }\n\n    public function test_restricted_registration()\n    {\n        $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true', 'registration-restrict' => 'example.com']);\n        $user = User::factory()->make();\n\n        // Go through registration process\n        $this->post('/register', $user->only('name', 'email', 'password'))\n            ->assertRedirect('/register');\n        $resp = $this->get('/register');\n        $resp->assertSee('That email domain does not have access to this application');\n        $this->assertDatabaseMissing('users', $user->only('email'));\n\n        $user->email = 'barry@example.com';\n\n        $this->post('/register', $user->only('name', 'email', 'password'))\n            ->assertRedirect('/register/confirm');\n        $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);\n\n        $this->assertNull(auth()->user());\n\n        $this->get('/')->assertRedirect('/login');\n        $resp = $this->followingRedirects()->post('/login', $user->only('email', 'password'));\n        $resp->assertSee('Email Address Not Confirmed');\n        $this->assertNull(auth()->user());\n    }\n\n    public function test_restricted_registration_with_confirmation_disabled()\n    {\n        $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'false', 'registration-restrict' => 'example.com']);\n        $user = User::factory()->make();\n\n        // Go through registration process\n        $this->post('/register', $user->only('name', 'email', 'password'))\n            ->assertRedirect('/register');\n        $this->assertDatabaseMissing('users', $user->only('email'));\n        $this->get('/register')->assertSee('That email domain does not have access to this application');\n\n        $user->email = 'barry@example.com';\n\n        $this->post('/register', $user->only('name', 'email', 'password'))\n            ->assertRedirect('/register/confirm');\n        $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);\n\n        $this->assertNull(auth()->user());\n\n        $this->get('/')->assertRedirect('/login');\n        $resp = $this->post('/login', $user->only('email', 'password'));\n        $resp->assertRedirect('/register/confirm/awaiting');\n        $this->get('/register/confirm/awaiting')->assertSee('Email Address Not Confirmed');\n        $this->assertNull(auth()->user());\n    }\n\n    public function test_registration_role_unset_by_default()\n    {\n        $this->assertFalse(setting('registration-role'));\n\n        $resp = $this->asAdmin()->get('/settings/registration');\n        $this->withHtml($resp)->assertElementContains('select[name=\"setting-registration-role\"] option[value=\"0\"][selected]', '-- None --');\n    }\n\n    public function test_registration_showing()\n    {\n        // Ensure registration form is showing\n        $this->setSettings(['registration-enabled' => 'true']);\n        $resp = $this->get('/login');\n        $this->withHtml($resp)->assertElementContains('a[href=\"' . url('/register') . '\"]', 'Sign up');\n    }\n\n    public function test_normal_registration()\n    {\n        // Set settings and get user instance\n        /** @var Role $registrationRole */\n        $registrationRole = Role::query()->first();\n        $this->setSettings(['registration-enabled' => 'true', 'registration-role' => $registrationRole->id]);\n        /** @var User $user */\n        $user = User::factory()->make();\n\n        // Test form and ensure user is created\n        $resp = $this->get('/register')\n            ->assertSee('Sign Up');\n        $this->withHtml($resp)->assertElementContains('form[action=\"' . url('/register') . '\"]', 'Create Account');\n\n        $resp = $this->post('/register', $user->only('password', 'name', 'email'));\n        $resp->assertRedirect('/');\n\n        $resp = $this->get('/');\n        $resp->assertOk();\n        $resp->assertSee($user->name);\n\n        $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email]);\n\n        $user = User::query()->where('email', '=', $user->email)->first();\n        $this->assertEquals(1, $user->roles()->count());\n        $this->assertEquals($registrationRole->id, $user->roles()->first()->id);\n    }\n\n    public function test_empty_registration_redirects_back_with_errors()\n    {\n        // Set settings and get user instance\n        $this->setSettings(['registration-enabled' => 'true']);\n\n        // Test form and ensure user is created\n        $this->get('/register');\n        $this->post('/register', [])->assertRedirect('/register');\n        $this->get('/register')->assertSee('The name field is required');\n    }\n\n    public function test_registration_validation()\n    {\n        $this->setSettings(['registration-enabled' => 'true']);\n\n        $this->get('/register');\n        $resp = $this->followingRedirects()->post('/register', [\n            'name'     => '1',\n            'email'    => '1',\n            'password' => '1',\n        ]);\n        $resp->assertSee('The name must be at least 2 characters.');\n        $resp->assertSee('The email must be a valid email address.');\n        $resp->assertSee('The password must be at least 8 characters.');\n    }\n\n    public function test_registration_simple_honeypot_active()\n    {\n        $this->setSettings(['registration-enabled' => 'true']);\n\n        $resp = $this->get('/register');\n        $this->withHtml($resp)->assertElementExists('form input[name=\"username\"]');\n\n        $resp = $this->post('/register', [\n            'name' => 'Barry',\n            'email' => 'barrybot@example.com',\n            'password' => 'barryIsTheBestBot',\n            'username' => 'MyUsername'\n        ]);\n        $resp->assertRedirect('/register');\n\n        $resp = $this->followRedirects($resp);\n        $this->withHtml($resp)->assertElementExists('form input[name=\"username\"].text-neg');\n    }\n\n    public function test_registration_endpoint_throttled()\n    {\n        $this->setSettings(['registration-enabled' => 'true']);\n\n        for ($i = 0; $i < 11; $i++) {\n            $response = $this->post('/register/', [\n                'name' => \"Barry{$i}\",\n                'email' => \"barry{$i}@example.com\",\n                'password' => \"barryIsTheBest{$i}\",\n            ]);\n            auth()->logout();\n        }\n\n        $response->assertStatus(429);\n    }\n\n    public function test_registration_confirmation_throttled()\n    {\n        $this->setSettings(['registration-enabled' => 'true']);\n\n        for ($i = 0; $i < 11; $i++) {\n            $response = $this->post('/register/confirm/accept', [\n                'token' => \"token{$i}\",\n            ]);\n        }\n\n        $response->assertStatus(429);\n    }\n\n    public function test_registration_confirmation_resend()\n    {\n        Notification::fake();\n        $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']);\n        $user = User::factory()->make();\n\n        $resp = $this->post('/register', $user->only('name', 'email', 'password'));\n        $resp->assertRedirect('/register/confirm');\n        $dbUser = User::query()->where('email', '=', $user->email)->first();\n\n        $resp = $this->post('/login', ['email' => $user->email, 'password' => $user->password]);\n        $resp->assertRedirect('/register/confirm/awaiting');\n\n        $resp = $this->post('/register/confirm/resend');\n        $resp->assertRedirect('/register/confirm');\n        Notification::assertSentToTimes($dbUser, ConfirmEmailNotification::class, 2);\n    }\n\n    public function test_registration_confirmation_expired_resend()\n    {\n        Notification::fake();\n        $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']);\n        $user = User::factory()->make();\n\n        $resp = $this->post('/register', $user->only('name', 'email', 'password'));\n        $resp->assertRedirect('/register/confirm');\n        $dbUser = User::query()->where('email', '=', $user->email)->first();\n\n        $resp = $this->post('/login', ['email' => $user->email, 'password' => $user->password]);\n        $resp->assertRedirect('/register/confirm/awaiting');\n\n        $emailConfirmation = DB::table('email_confirmations')->where('user_id', '=', $dbUser->id)->first();\n        $this->travel(2)->days();\n\n        $resp = $this->post(\"/register/confirm/accept\", [\n            'token' => $emailConfirmation->token,\n        ]);\n        $resp->assertRedirect('/register/confirm');\n        $this->assertSessionError('The confirmation token has expired, A new confirmation email has been sent.');\n\n        Notification::assertSentToTimes($dbUser, ConfirmEmailNotification::class, 2);\n    }\n\n    public function test_registration_confirmation_awaiting_and_resend_returns_to_log_if_no_login_attempt_user_found()\n    {\n        $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']);\n\n        $this->get('/register/confirm/awaiting')->assertRedirect('/login');\n        $this->assertSessionError('A user for this action could not be found.');\n        $this->flushSession();\n\n        $this->post('/register/confirm/resend')->assertRedirect('/login');\n        $this->assertSessionError('A user for this action could not be found.');\n    }\n}\n"
  },
  {
    "path": "tests/Auth/ResetPasswordTest.php",
    "content": "<?php\n\nnamespace Tests\\Auth;\n\nuse BookStack\\Access\\Notifications\\ResetPasswordNotification;\nuse BookStack\\Users\\Models\\User;\nuse Carbon\\CarbonInterval;\nuse Illuminate\\Support\\Facades\\Notification;\nuse Illuminate\\Support\\Sleep;\nuse Tests\\TestCase;\n\nclass ResetPasswordTest extends TestCase\n{\n    protected function setUp(): void\n    {\n        parent::setUp();\n        Sleep::fake();\n    }\n\n    public function test_reset_flow()\n    {\n        Notification::fake();\n\n        $resp = $this->get('/login');\n        $this->withHtml($resp)->assertElementContains('a[href=\"' . url('/password/email') . '\"]', 'Forgot Password?');\n\n        $resp = $this->get('/password/email');\n        $this->withHtml($resp)->assertElementContains('form[action=\"' . url('/password/email') . '\"]', 'Send Reset Link');\n\n        $resp = $this->post('/password/email', [\n            'email' => 'admin@admin.com',\n        ]);\n        $resp->assertRedirect('/password/email');\n\n        $resp = $this->get('/password/email');\n        $resp->assertSee('A password reset link will be sent to admin@admin.com if that email address is found in the system.');\n\n        $this->assertDatabaseHas('password_resets', [\n            'email' => 'admin@admin.com',\n        ]);\n\n        /** @var User $user */\n        $user = User::query()->where('email', '=', 'admin@admin.com')->first();\n\n        Notification::assertSentTo($user, ResetPasswordNotification::class);\n        $n = Notification::sent($user, ResetPasswordNotification::class);\n\n        $this->get('/password/reset/' . $n->first()->token)\n            ->assertOk()\n            ->assertSee('Reset Password');\n\n        $resp = $this->post('/password/reset', [\n            'email'                 => 'admin@admin.com',\n            'password'              => 'randompass',\n            'password_confirmation' => 'randompass',\n            'token'                 => $n->first()->token,\n        ]);\n        $resp->assertRedirect('/');\n\n        $this->get('/')->assertSee('Your password has been successfully reset');\n    }\n\n    public function test_reset_flow_shows_success_message_even_if_wrong_password_to_prevent_user_discovery()\n    {\n        $this->get('/password/email');\n        $resp = $this->followingRedirects()->post('/password/email', [\n            'email' => 'barry@admin.com',\n        ]);\n        $resp->assertSee('A password reset link will be sent to barry@admin.com if that email address is found in the system.');\n        $resp->assertDontSee('We can\\'t find a user');\n\n        $this->get('/password/reset/arandometokenvalue')->assertSee('Reset Password');\n        $resp = $this->post('/password/reset', [\n            'email'                 => 'barry@admin.com',\n            'password'              => 'randompass',\n            'password_confirmation' => 'randompass',\n            'token'                 => 'arandometokenvalue',\n        ]);\n        $resp->assertRedirect('/password/reset/arandometokenvalue');\n\n        $this->get('/password/reset/arandometokenvalue')\n            ->assertDontSee('We can\\'t find a user')\n            ->assertSee('The password reset token is invalid for this email address.');\n    }\n\n    public function test_reset_request_with_not_found_user_still_has_delay()\n    {\n        $this->followingRedirects()->post('/password/email', [\n            'email' => 'barrynotfoundrandomuser@example.com',\n        ]);\n\n        Sleep::assertSlept(function (CarbonInterval $duration): bool {\n            return $duration->totalMilliseconds > 999;\n        }, 1);\n    }\n\n    public function test_reset_page_shows_sign_links()\n    {\n        $this->setSettings(['registration-enabled' => 'true']);\n        $resp = $this->get('/password/email');\n        $this->withHtml($resp)->assertElementContains('a', 'Log in')\n            ->assertElementContains('a', 'Sign up');\n    }\n\n    public function test_reset_request_is_throttled()\n    {\n        $editor = $this->users->editor();\n        Notification::fake();\n        $this->get('/password/email');\n        $this->followingRedirects()->post('/password/email', [\n            'email' => $editor->email,\n        ]);\n\n        $resp = $this->followingRedirects()->post('/password/email', [\n            'email' => $editor->email,\n        ]);\n        Notification::assertSentTimes(ResetPasswordNotification::class, 1);\n        $resp->assertSee('A password reset link will be sent to ' . $editor->email . ' if that email address is found in the system.');\n    }\n\n    public function test_reset_request_with_not_found_user_is_throttled()\n    {\n        for ($i = 0; $i < 11; $i++) {\n            $response = $this->post('/password/email', [\n                'email' => 'barrynotfoundrandomuser@example.com',\n            ]);\n        }\n\n        $response->assertStatus(429);\n    }\n\n    public function test_reset_call_is_throttled()\n    {\n        for ($i = 0; $i < 11; $i++) {\n            $response = $this->post('/password/reset', [\n                'email' => \"arandomuser{$i}@example.com\",\n                'token' => \"randomtoken{$i}\",\n            ]);\n        }\n\n        $response->assertStatus(429);\n    }\n}\n"
  },
  {
    "path": "tests/Auth/Saml2Test.php",
    "content": "<?php\n\nnamespace Tests\\Auth;\n\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Models\\User;\nuse Tests\\TestCase;\n\nclass Saml2Test extends TestCase\n{\n    protected function setUp(): void\n    {\n        parent::setUp();\n        // Set default config for SAML2\n        config()->set([\n            'auth.method'                                   => 'saml2',\n            'auth.defaults.guard'                           => 'saml2',\n            'saml2.name'                                    => 'SingleSignOn-Testing',\n            'saml2.email_attribute'                         => 'email',\n            'saml2.display_name_attributes'                 => ['first_name', 'last_name'],\n            'saml2.external_id_attribute'                   => 'uid',\n            'saml2.user_to_groups'                          => false,\n            'saml2.group_attribute'                         => 'user_groups',\n            'saml2.remove_from_groups'                      => false,\n            'saml2.onelogin_overrides'                      => null,\n            'saml2.onelogin.idp.entityId'                   => 'http://saml.local/saml2/idp/metadata.php',\n            'saml2.onelogin.idp.singleSignOnService.url'    => 'http://saml.local/saml2/idp/SSOService.php',\n            'saml2.onelogin.idp.singleLogoutService.url'    => 'http://saml.local/saml2/idp/SingleLogoutService.php',\n            'saml2.autoload_from_metadata'                  => false,\n            'saml2.onelogin.idp.x509cert'                   => $this->testCert,\n            'saml2.onelogin.debug'                          => false,\n            'saml2.onelogin.security.requestedAuthnContext' => true,\n        ]);\n    }\n\n    public function test_metadata_endpoint_displays_xml_as_expected()\n    {\n        $req = $this->get('/saml2/metadata');\n        $req->assertHeader('Content-Type', 'text/xml; charset=utf-8');\n        $req->assertSee('md:EntityDescriptor');\n        $req->assertSee(url('/saml2/acs'));\n    }\n\n    public function test_metadata_endpoint_loads_when_autoloading_with_bad_url_set()\n    {\n        config()->set([\n            'saml2.autoload_from_metadata' => true,\n            'saml2.onelogin.idp.entityId' => 'http://192.168.1.1:9292',\n            'saml2.onelogin.idp.singleSignOnService.url' => null,\n        ]);\n\n        $req = $this->get('/saml2/metadata');\n        $req->assertOk();\n        $req->assertHeader('Content-Type', 'text/xml; charset=utf-8');\n        $req->assertSee('md:EntityDescriptor');\n    }\n\n    public function test_onelogin_overrides_functions_as_expected()\n    {\n        $json = '{\"sp\": {\"assertionConsumerService\": {\"url\": \"https://example.com/super-cats\"}}, \"contactPerson\": {\"technical\": {\"givenName\": \"Barry Scott\", \"emailAddress\": \"barry@example.com\"}}}';\n        config()->set(['saml2.onelogin_overrides' => $json]);\n\n        $req = $this->get('/saml2/metadata');\n        $req->assertSee('https://example.com/super-cats');\n        $req->assertSee('md:ContactPerson');\n        $req->assertSee('<md:GivenName>Barry Scott</md:GivenName>', false);\n    }\n\n    public function test_login_option_shows_on_login_page()\n    {\n        $req = $this->get('/login');\n        $req->assertSeeText('SingleSignOn-Testing');\n        $this->withHtml($req)->assertElementExists('form[action$=\"/saml2/login\"][method=POST] button');\n    }\n\n    public function test_login()\n    {\n        $req = $this->post('/saml2/login');\n        $redirect = $req->headers->get('location');\n        $this->assertStringStartsWith('http://saml.local/saml2/idp/SSOService.php', $redirect, 'Login redirects to SSO location');\n\n        config()->set(['saml2.onelogin.strict' => false]);\n        $this->assertFalse($this->isAuthenticated());\n\n        $acsPost = $this->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);\n        $redirect = $acsPost->headers->get('Location');\n        $acsId = explode('?id=', $redirect)[1];\n        $this->assertTrue(strlen($acsId) > 12);\n\n        $this->assertStringContainsString('/saml2/acs?id=', $redirect);\n        $this->assertTrue(cache()->has('saml2_acs:' . $acsId));\n\n        $acsGet = $this->get($redirect);\n        $acsGet->assertRedirect('/');\n        $this->assertFalse(cache()->has('saml2_acs:' . $acsId));\n\n        $this->assertTrue($this->isAuthenticated());\n        $this->assertDatabaseHas('users', [\n            'email'            => 'user@example.com',\n            'external_auth_id' => 'user',\n            'email_confirmed'  => false,\n            'name'             => 'Barry Scott',\n        ]);\n    }\n\n    public function test_acs_process_id_randomly_generated()\n    {\n        $acsPost = $this->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);\n        $redirectA = $acsPost->headers->get('Location');\n\n        $acsPost = $this->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);\n        $redirectB = $acsPost->headers->get('Location');\n\n        $this->assertFalse($redirectA === $redirectB);\n    }\n\n    public function test_process_acs_endpoint_cant_be_called_with_invalid_id()\n    {\n        $resp = $this->get('/saml2/acs');\n        $resp->assertRedirect('/login');\n        $this->followRedirects($resp)->assertSeeText('Login using SingleSignOn-Testing failed, system did not provide successful authorization');\n\n        $resp = $this->get('/saml2/acs?id=abc123');\n        $resp->assertRedirect('/login');\n        $this->followRedirects($resp)->assertSeeText('Login using SingleSignOn-Testing failed, system did not provide successful authorization');\n    }\n\n    public function test_group_role_sync_on_login()\n    {\n        config()->set([\n            'saml2.onelogin.strict'    => false,\n            'saml2.user_to_groups'     => true,\n            'saml2.remove_from_groups' => false,\n        ]);\n\n        $memberRole = Role::factory()->create(['external_auth_id' => 'member']);\n        $adminRole = Role::getSystemRole('admin');\n\n        $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);\n        $user = User::query()->where('external_auth_id', '=', 'user')->first();\n\n        $userRoleIds = $user->roles()->pluck('id');\n        $this->assertContains($memberRole->id, $userRoleIds, 'User was assigned to member role');\n        $this->assertContains($adminRole->id, $userRoleIds, 'User was assigned to admin role');\n    }\n\n    public function test_group_role_sync_removal_option_works_as_expected()\n    {\n        config()->set([\n            'saml2.onelogin.strict'    => false,\n            'saml2.user_to_groups'     => true,\n            'saml2.remove_from_groups' => true,\n        ]);\n\n        $acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);\n        $user = User::query()->where('external_auth_id', '=', 'user')->first();\n\n        $randomRole = Role::factory()->create(['external_auth_id' => 'random']);\n        $user->attachRole($randomRole);\n        $this->assertContains($randomRole->id, $user->roles()->pluck('id'));\n\n        auth()->logout();\n        $acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);\n        $this->assertNotContains($randomRole->id, $user->roles()->pluck('id'));\n    }\n\n    public function test_logout_link_directs_to_saml_path()\n    {\n        config()->set([\n            'saml2.onelogin.strict' => false,\n        ]);\n\n        $resp = $this->actingAs($this->users->editor())->get('/');\n        $this->withHtml($resp)->assertElementContains('form[action$=\"/saml2/logout\"] button', 'Logout');\n    }\n\n    public function test_logout_sls_flow()\n    {\n        config()->set([\n            'saml2.onelogin.strict' => false,\n        ]);\n\n        $handleLogoutResponse = function () {\n            $this->assertFalse($this->isAuthenticated());\n\n            $req = $this->get('/saml2/sls');\n            $req->assertRedirect('/');\n            $this->assertFalse($this->isAuthenticated());\n        };\n\n        $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);\n\n        $req = $this->post('/saml2/logout');\n        $redirect = $req->headers->get('location');\n        $this->assertStringStartsWith('http://saml.local/saml2/idp/SingleLogoutService.php', $redirect);\n        $sloData = $this->parseSamlDataFromUrl($redirect, 'SAMLRequest');\n        $this->assertStringContainsString('<samlp:SessionIndex>_4fe7c0d1572d64b27f930aa6f236a6f42e930901cc</samlp:SessionIndex>', $sloData);\n\n        $this->withGet(['SAMLResponse' => $this->sloResponseData], $handleLogoutResponse);\n    }\n\n    public function test_logout_sls_flow_when_sls_not_configured()\n    {\n        config()->set([\n            'saml2.onelogin.strict'                      => false,\n            'saml2.onelogin.idp.singleLogoutService.url' => null,\n        ]);\n\n        $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);\n        $this->assertTrue($this->isAuthenticated());\n\n        $req = $this->post('/saml2/logout');\n        $req->assertRedirect('/');\n        $this->assertFalse($this->isAuthenticated());\n    }\n\n    public function test_logout_sls_flow_logs_user_out_before_redirect()\n    {\n        config()->set([\n            'saml2.onelogin.strict' => false,\n        ]);\n\n        $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);\n        $this->assertTrue($this->isAuthenticated());\n\n        $req = $this->post('/saml2/logout');\n        $redirect = $req->headers->get('location');\n        $this->assertStringStartsWith('http://saml.local/saml2/idp/SingleLogoutService.php', $redirect);\n        $this->assertFalse($this->isAuthenticated());\n    }\n\n    public function test_logout_sls_request_redirect_prevents_auto_login_when_enabled()\n    {\n        config()->set([\n            'saml2.onelogin.strict' => false,\n            'auth.auto_initiate' => true,\n            'services.google.client_id' => false,\n            'services.github.client_id' => false,\n        ]);\n\n        $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);\n\n        $req = $this->post('/saml2/logout');\n        $redirect = $req->headers->get('location');\n        $this->assertStringContainsString(urlencode(url('/login?prevent_auto_init=true')), $redirect);\n    }\n\n    public function test_logout_sls_response_endpoint_redirect_prevents_auto_login_when_enabled()\n    {\n        config()->set([\n            'saml2.onelogin.strict' => false,\n            'auth.auto_initiate' => true,\n            'services.google.client_id' => false,\n            'services.github.client_id' => false,\n        ]);\n\n        $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);\n\n        $this->withGet(['SAMLResponse' => $this->sloResponseData], function () {\n            $req = $this->get('/saml2/sls');\n            $redirect = $req->headers->get('location');\n            $this->assertEquals(url('/login?prevent_auto_init=true'), $redirect);\n        });\n    }\n\n    public function test_dump_user_details_option_works()\n    {\n        config()->set([\n            'saml2.onelogin.strict'   => false,\n            'saml2.dump_user_details' => true,\n        ]);\n\n        $acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);\n        $acsPost->assertJsonStructure([\n            'id_from_idp',\n            'attrs_from_idp'      => [],\n            'attrs_after_parsing' => [],\n        ]);\n    }\n\n    public function test_dump_user_details_response_contains_parsed_group_data_if_groups_enabled()\n    {\n        config()->set([\n            'saml2.onelogin.strict'   => false,\n            'saml2.dump_user_details' => true,\n            'saml2.user_to_groups'    => true,\n        ]);\n\n        $acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);\n        $acsPost->assertJson([\n            'attrs_after_parsing' => [\n                'groups' => ['member', 'admin'],\n            ]\n        ]);\n    }\n\n    public function test_saml_routes_are_only_active_if_saml_enabled()\n    {\n        config()->set(['auth.method' => 'standard']);\n        $getRoutes = ['/metadata', '/sls'];\n        foreach ($getRoutes as $route) {\n            $req = $this->get('/saml2' . $route);\n            $this->assertPermissionError($req);\n        }\n\n        $postRoutes = ['/login', '/acs', '/logout'];\n        foreach ($postRoutes as $route) {\n            $req = $this->post('/saml2' . $route);\n            $this->assertPermissionError($req);\n        }\n    }\n\n    public function test_forgot_password_routes_inaccessible()\n    {\n        $resp = $this->get('/password/email');\n        $this->assertPermissionError($resp);\n\n        $resp = $this->post('/password/email');\n        $this->assertPermissionError($resp);\n\n        $resp = $this->get('/password/reset/abc123');\n        $this->assertPermissionError($resp);\n\n        $resp = $this->post('/password/reset');\n        $this->assertPermissionError($resp);\n    }\n\n    public function test_standard_login_routes_inaccessible()\n    {\n        $resp = $this->post('/login');\n        $this->assertPermissionError($resp);\n\n        $resp = $this->post('/logout');\n        $this->assertPermissionError($resp);\n    }\n\n    public function test_user_invite_routes_inaccessible()\n    {\n        $resp = $this->get('/register/invite/abc123');\n        $this->assertPermissionError($resp);\n\n        $resp = $this->post('/register/invite/abc123');\n        $this->assertPermissionError($resp);\n    }\n\n    public function test_user_register_routes_inaccessible()\n    {\n        $resp = $this->get('/register');\n        $this->assertPermissionError($resp);\n\n        $resp = $this->post('/register');\n        $this->assertPermissionError($resp);\n    }\n\n    public function test_email_domain_restriction_active_on_new_saml_login()\n    {\n        $this->setSettings([\n            'registration-restrict' => 'testing.com',\n        ]);\n        config()->set([\n            'saml2.onelogin.strict' => false,\n        ]);\n\n        $acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);\n        $acsPost->assertSeeText('That email domain does not have access to this application');\n        $this->assertFalse(auth()->check());\n        $this->assertDatabaseMissing('users', ['email' => 'user@example.com']);\n    }\n\n    public function test_group_sync_functions_when_email_confirmation_required()\n    {\n        setting()->put('registration-confirmation', 'true');\n        config()->set([\n            'saml2.onelogin.strict'    => false,\n            'saml2.user_to_groups'     => true,\n            'saml2.remove_from_groups' => false,\n        ]);\n\n        $memberRole = Role::factory()->create(['external_auth_id' => 'member']);\n        $adminRole = Role::getSystemRole('admin');\n\n        $acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);\n\n        $this->assertEquals('http://localhost/register/confirm', url()->current());\n        $acsPost->assertSee('Please check your email and click the confirmation button to access BookStack.');\n        /** @var User $user */\n        $user = User::query()->where('external_auth_id', '=', 'user')->first();\n\n        $userRoleIds = $user->roles()->pluck('id');\n        $this->assertContains($memberRole->id, $userRoleIds, 'User was assigned to member role');\n        $this->assertContains($adminRole->id, $userRoleIds, 'User was assigned to admin role');\n        $this->assertFalse(boolval($user->email_confirmed), 'User email remains unconfirmed');\n\n        $this->assertNull(auth()->user());\n        $homeGet = $this->get('/');\n        $homeGet->assertRedirect('/login');\n    }\n\n    public function test_login_where_existing_non_saml_user_shows_warning()\n    {\n        $this->post('/saml2/login');\n        config()->set(['saml2.onelogin.strict' => false]);\n\n        // Make the user pre-existing in DB with different auth_id\n        User::query()->forceCreate([\n            'email'            => 'user@example.com',\n            'external_auth_id' => 'old_system_user_id',\n            'email_confirmed'  => false,\n            'name'             => 'Barry Scott',\n        ]);\n\n        $acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);\n        $this->assertFalse($this->isAuthenticated());\n        $this->assertDatabaseHas('users', [\n            'email'            => 'user@example.com',\n            'external_auth_id' => 'old_system_user_id',\n        ]);\n\n        $acsPost->assertSee('A user with the email user@example.com already exists but with different credentials');\n    }\n\n    public function test_login_request_contains_expected_default_authncontext()\n    {\n        $authReq = $this->getAuthnRequest();\n        $this->assertStringContainsString('samlp:RequestedAuthnContext Comparison=\"exact\"', $authReq);\n        $this->assertStringContainsString('<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>', $authReq);\n    }\n\n    public function test_false_idp_authncontext_option_does_not_pass_authncontext_in_saml_request()\n    {\n        config()->set(['saml2.onelogin.security.requestedAuthnContext' => false]);\n        $authReq = $this->getAuthnRequest();\n        $this->assertStringNotContainsString('samlp:RequestedAuthnContext', $authReq);\n        $this->assertStringNotContainsString('<saml:AuthnContextClassRef>', $authReq);\n    }\n\n    public function test_array_idp_authncontext_option_passes_value_as_authncontextclassref_in_request()\n    {\n        config()->set(['saml2.onelogin.security.requestedAuthnContext' => ['urn:federation:authentication:windows', 'urn:federation:authentication:linux']]);\n        $authReq = $this->getAuthnRequest();\n        $this->assertStringContainsString('samlp:RequestedAuthnContext', $authReq);\n        $this->assertStringContainsString('<saml:AuthnContextClassRef>urn:federation:authentication:windows</saml:AuthnContextClassRef>', $authReq);\n        $this->assertStringContainsString('<saml:AuthnContextClassRef>urn:federation:authentication:linux</saml:AuthnContextClassRef>', $authReq);\n    }\n\n    protected function getAuthnRequest(): string\n    {\n        $req = $this->post('/saml2/login');\n        $location = $req->headers->get('Location');\n        return $this->parseSamlDataFromUrl($location, 'SAMLRequest');\n    }\n\n    protected function parseSamlDataFromUrl(string $url, string $paramName): string\n    {\n        $query = explode('?', $url)[1];\n        $params = [];\n        parse_str($query, $params);\n\n        return gzinflate(base64_decode($params[$paramName]));\n    }\n\n    protected function withGet(array $options, callable $callback)\n    {\n        return $this->withGlobal($_GET, $options, $callback);\n    }\n\n    protected function withGlobal(array &$global, array $options, callable $callback)\n    {\n        $original = [];\n        foreach ($options as $key => $val) {\n            $original[$key] = $global[$key] ?? null;\n            $global[$key] = $val;\n        }\n\n        $callback();\n\n        foreach ($options as $key => $val) {\n            $val = $original[$key];\n            if ($val) {\n                $global[$key] = $val;\n            } else {\n                unset($global[$key]);\n            }\n        }\n    }\n\n    /**\n     * The post data for a callback for single-sign-in.\n     * Provides the following attributes:\n     * array:5 [\n     * \"uid\" => array:1 [\n     * 0 => \"user\"\n     * ]\n     * \"first_name\" => array:1 [\n     * 0 => \"Barry\"\n     * ]\n     * \"last_name\" => array:1 [\n     * 0 => \"Scott\"\n     * ]\n     * \"email\" => array:1 [\n     * 0 => \"user@example.com\"\n     * ]\n     * \"user_groups\" => array:2 [\n     * 0 => \"member\"\n     * 1 => \"admin\"\n     * ]\n     * ].\n     */\n    protected string $acsPostData = 'PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJfNGRkNDU2NGRjNzk0MDYxZWYxYmFhMDQ2N2Q3OTAyOGNlZDNjZTU0YmVlIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxOS0xMS0xN1QxNzo1MzozOVoiIERlc3RpbmF0aW9uPSJodHRwOi8vYm9va3N0YWNrLmxvY2FsL3NhbWwyL2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl82YTBmNGYzOTkzMDQwZjE5ODdmZDM3MDY4YjUyOTYyMjlhZDUzNjFjIj48c2FtbDpJc3N1ZXI+aHR0cDovL3NhbWwubG9jYWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+CiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNfNGRkNDU2NGRjNzk0MDYxZWYxYmFhMDQ2N2Q3OTAyOGNlZDNjZTU0YmVlIj48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiLz48ZHM6RGlnZXN0VmFsdWU+dm1oL1M3NU5mK2crZWNESkN6QWJaV0tKVmx1ZzdCZnNDKzlhV05lSXJlUT08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+dnJhZ0tKWHNjVm5UNjJFaEk3bGk4MERUWHNOTGJOc3lwNWZ2QnU4WjFYSEtFUVA3QWpPNkcxcVBwaGpWQ2dRMzd6TldVVTZvUytQeFA3UDlHeG5xL3hKejRUT3lHcHJ5N1RoK2pIcHc0YWVzQTdrTmp6VU51UmU2c1ltWTlrRXh2VjMvTmJRZjROMlM2Y2RhRHIzWFRodllVVDcxYzQwNVVHOFJpQjJaY3liWHIxZU1yWCtXUDBnU2Qrc0F2RExqTjBJc3pVWlVUNThadFpEVE1ya1ZGL0pIbFBFQ04vVW1sYVBBeitTcUJ4c25xTndZK1oxYUt3MnlqeFRlNnUxM09Kb29OOVN1REowNE0rK2F3RlY3NkI4cXEyTzMxa3FBbDJibm1wTGxtTWdRNFEraUlnL3dCc09abTV1clphOWJObDNLVEhtTVBXbFpkbWhsLzgvMy9IT1RxN2thWGs3cnlWRHRLcFlsZ3FUajNhRUpuL0dwM2o4SFp5MUVialRiOTRRT1ZQMG5IQzB1V2hCaE13TjdzVjFrUSsxU2NjUlpUZXJKSGlSVUQvR0srTVg3M0YrbzJVTFRIL1Z6Tm9SM2o4N2hOLzZ1UC9JeG5aM1RudGR1MFZPZS9ucEdVWjBSMG9SWFhwa2JTL2poNWk1ZjU0RXN4eXZ1VEM5NHdKaEM8L2RzOlNpZ25hdHVyZVZhbHVlPgo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlFYXpDQ0F0T2dBd0lCQWdJVWU3YTA4OENucjRpem1ybkJFbng1cTNIVE12WXdEUVlKS29aSWh2Y05BUUVMQlFBd1JURUxNQWtHQTFVRUJoTUNSMEl4RXpBUkJnTlZCQWdNQ2xOdmJXVXRVM1JoZEdVeElUQWZCZ05WQkFvTUdFbHVkR1Z5Ym1WMElGZHBaR2RwZEhNZ1VIUjVJRXgwWkRBZUZ3MHhPVEV4TVRZeE1qRTNNVFZhRncweU9URXhNVFV4TWpFM01UVmFNRVV4Q3pBSkJnTlZCQVlUQWtkQ01STXdFUVlEVlFRSURBcFRiMjFsTFZOMFlYUmxNU0V3SHdZRFZRUUtEQmhKYm5SbGNtNWxkQ0JYYVdSbmFYUnpJRkIwZVNCTWRHUXdnZ0dpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCandBd2dnR0tBb0lCZ1FEekxlOUZmZHlwbFR4SHA0U3VROWdRdFpUM3QrU0RmdkVMNzJwcENmRlp3NytCNXM1Qi9UNzNhWHBvUTNTNTNwR0kxUklXQ2dlMmlDVVEydHptMjdhU05IMGl1OWFKWWNVUVovUklUcWQwYXl5RGtzMU5BMlBUM1RXNnQzbTdLVjVyZTRQME5iK1lEZXV5SGRreitqY010cG44Q21Cb1QwSCtza2hhMGhpcUlOa2prUlBpSHZMSFZHcCt0SFVFQS9JNm1ONGFCL1VFeFNUTHM3OU5zTFVmdGVxcXhlOSt0dmRVYVRveURQcmhQRmpPTnMrOU5LQ2t6SUM2dmN2N0o2QXR1S0c2bkVUK3pCOXlPV2d0R1lRaWZYcVFBMnk1ZEw4MUJCMHE1dU1hQkxTMnBxM2FQUGp6VTJGMytFeXNqeVNXVG5Da2ZrN0M1U3NDWFJ1OFErVTk1dHVucE5md2Y1b2xFNldhczQ4Tk1NK1B3VjdpQ05NUGtOemxscTZQQ2lNK1A4RHJNU2N6elVaWlFVU3Y2ZFN3UENvK1lTVmltRU0wT2czWEpUaU5oUTVBTmxhSW42Nkt3NWdmb0JmdWlYbXlJS2lTRHlBaURZbUZhZjQzOTV3V3dMa1RSK2N3OFdmamFIc3dLWlRvbW4xTVIzT0pzWTJVSjBlUkJZTStZU3NDQXdFQUFhTlRNRkV3SFFZRFZSME9CQllFRkltcDJDWUNHZmNiN3c5MUgvY1NoVENrWHdSL01COEdBMVVkSXdRWU1CYUFGSW1wMkNZQ0dmY2I3dzkxSC9jU2hUQ2tYd1IvTUE4R0ExVWRFd0VCL3dRRk1BTUJBZjh3RFFZSktvWklodmNOQVFFTEJRQURnZ0dCQUErZy9DN3VMOWxuK1crcUJrbkxXODFrb2pZZmxnUEsxSTFNSEl3bk12bC9aVEhYNGRSWEtEcms3S2NVcTFLanFhak5WNjZmMWNha3AwM0lpakJpTzBYaTFnWFVaWUxvQ2lOR1V5eXA5WGxvaUl5OVh3MlBpV25ydzAreVp5dlZzc2JlaFhYWUpsNFJpaEJqQld1bDlSNHdNWUxPVVNKRGUyV3hjVUJoSm54eU5ScytQMHhMU1FYNkIybjZueG9Ea280cDA3czhaS1hRa2VpWjJpd0ZkVHh6UmtHanRoTVV2NzA0bnpzVkdCVDBEQ1B0ZlNhTzVLSlpXMXJDczN5aU10aG5CeHE0cUVET1FKRklsKy9MRDcxS2JCOXZaY1c1SnVhdnpCRm1rS0dOcm8vNkcxSTdlbDQ2SVI0d2lqVHlORkNZVXVEOWR0aWduTm1wV3ROOE9XK3B0aUwvanRUeVNXdWtqeXMwcyt2TG44M0NWdmpCMGRKdFZBSVlPZ1hGZEl1aWk2Nmdjend3TS9MR2lPRXhKbjBkVE56c0ovSVlocHhMNEZCRXVQMHBza1kwbzBhVWxKMkxTMmord1NRVFJLc0JnTWp5clVyZWtsZTJPRFN0U3RuM2VhYmpJeDAvRkhscEZyMGpOSW0vb01QN2t3anRVWDR6YU5lNDdRSTRHZz09PC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9Il82ODQyZGY5YzY1OWYxM2ZlNTE5NmNkOWVmNmMyZjAyODM2NGFlOTQzYjEiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE5LTExLTE3VDE3OjUzOjM5WiI+PHNhbWw6SXNzdWVyPmh0dHA6Ly9zYW1sLmxvY2FsL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxkc2lnLW1vcmUjcnNhLXNoYTI1NiIvPgogIDxkczpSZWZlcmVuY2UgVVJJPSIjXzY4NDJkZjljNjU5ZjEzZmU1MTk2Y2Q5ZWY2YzJmMDI4MzY0YWU5NDNiMSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8+PGRzOkRpZ2VzdFZhbHVlPmtyYjV3NlM4dG9YYy9lU3daUFVPQnZRem4zb3M0SkFDdXh4ckpreHBnRnc9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPjJxcW1Ba3hucXhOa3N5eXh5dnFTVDUxTDg5VS9ZdHpja2t1ekF4ci9hQ1JTK1NPRzg1YkFNWm8vU3puc3d0TVlBYlFRQ0VGb0R1amdNdlpzSFl3NlR2dmFHanlXWUpRNVZyYWhlemZaSWlCVUU0NHBtWGFrOCswV0l0WTVndnBGSXhxWFZaRmdFUkt2VExmZVFCMzhkMVZQc0ZVZ0RYdXQ4VS9Qdm43dXZwdXZjVXorMUUyOUVKR2FZL0dndnhUN0tyWU9SQTh3SitNdVRzUVZtanNlUnhveVJTejA4TmJ3ZTJIOGpXQnpFWWNxWWwyK0ZnK2hwNWd0S216VmhLRnBkNXZBNjdBSXo1NXN0QmNHNSswNHJVaWpFSzRzci9xa0x5QmtKQjdLdkwzanZKcG8zQjhxYkxYeXhLb1dSSmRnazhKNHMvTVp1QWk3QWUxUXNTTjl2Z3ZTdVRlc0VCUjVpSHJuS1lrbEpRWXNrbUQzbSsremE4U1NRbnBlM0UzYUZBY3p6cElUdUQ4YkFCWmRqcUk2TkhrSmFRQXBmb0hWNVQrZ244ejdUTWsrSStUU2JlQURubUxCS3lnMHRabW10L0ZKbDV6eWowVmxwc1dzTVM2OVE2bUZJVStqcEhSanpOb2FLMVM1dlQ3ZW1HbUhKSUp0cWlOdXJRN0tkQlBJPC9kczpTaWduYXR1cmVWYWx1ZT4KPGRzOktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJRWF6Q0NBdE9nQXdJQkFnSVVlN2EwODhDbnI0aXptcm5CRW54NXEzSFRNdll3RFFZSktvWklodmNOQVFFTEJRQXdSVEVMTUFrR0ExVUVCaE1DUjBJeEV6QVJCZ05WQkFnTUNsTnZiV1V0VTNSaGRHVXhJVEFmQmdOVkJBb01HRWx1ZEdWeWJtVjBJRmRwWkdkcGRITWdVSFI1SUV4MFpEQWVGdzB4T1RFeE1UWXhNakUzTVRWYUZ3MHlPVEV4TVRVeE1qRTNNVFZhTUVVeEN6QUpCZ05WQkFZVEFrZENNUk13RVFZRFZRUUlEQXBUYjIxbExWTjBZWFJsTVNFd0h3WURWUVFLREJoSmJuUmxjbTVsZENCWGFXUm5hWFJ6SUZCMGVTQk1kR1F3Z2dHaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQmp3QXdnZ0dLQW9JQmdRRHpMZTlGZmR5cGxUeEhwNFN1UTlnUXRaVDN0K1NEZnZFTDcycHBDZkZadzcrQjVzNUIvVDczYVhwb1EzUzUzcEdJMVJJV0NnZTJpQ1VRMnR6bTI3YVNOSDBpdTlhSlljVVFaL1JJVHFkMGF5eURrczFOQTJQVDNUVzZ0M203S1Y1cmU0UDBOYitZRGV1eUhka3oramNNdHBuOENtQm9UMEgrc2toYTBoaXFJTmtqa1JQaUh2TEhWR3ArdEhVRUEvSTZtTjRhQi9VRXhTVExzNzlOc0xVZnRlcXF4ZTkrdHZkVWFUb3lEUHJoUEZqT05zKzlOS0NreklDNnZjdjdKNkF0dUtHNm5FVCt6Qjl5T1dndEdZUWlmWHFRQTJ5NWRMODFCQjBxNXVNYUJMUzJwcTNhUFBqelUyRjMrRXlzanlTV1RuQ2tmazdDNVNzQ1hSdThRK1U5NXR1bnBOZndmNW9sRTZXYXM0OE5NTStQd1Y3aUNOTVBrTnpsbHE2UENpTStQOERyTVNjenpVWlpRVVN2NmRTd1BDbytZU1ZpbUVNME9nM1hKVGlOaFE1QU5sYUluNjZLdzVnZm9CZnVpWG15SUtpU0R5QWlEWW1GYWY0Mzk1d1d3TGtUUitjdzhXZmphSHN3S1pUb21uMU1SM09Kc1kyVUowZVJCWU0rWVNzQ0F3RUFBYU5UTUZFd0hRWURWUjBPQkJZRUZJbXAyQ1lDR2ZjYjd3OTFIL2NTaFRDa1h3Ui9NQjhHQTFVZEl3UVlNQmFBRkltcDJDWUNHZmNiN3c5MUgvY1NoVENrWHdSL01BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dHQkFBK2cvQzd1TDlsbitXK3FCa25MVzgxa29qWWZsZ1BLMUkxTUhJd25NdmwvWlRIWDRkUlhLRHJrN0tjVXExS2pxYWpOVjY2ZjFjYWtwMDNJaWpCaU8wWGkxZ1hVWllMb0NpTkdVeXlwOVhsb2lJeTlYdzJQaVducncwK3laeXZWc3NiZWhYWFlKbDRSaWhCakJXdWw5UjR3TVlMT1VTSkRlMld4Y1VCaEpueHlOUnMrUDB4TFNRWDZCMm42bnhvRGtvNHAwN3M4WktYUWtlaVoyaXdGZFR4elJrR2p0aE1VdjcwNG56c1ZHQlQwRENQdGZTYU81S0paVzFyQ3MzeWlNdGhuQnhxNHFFRE9RSkZJbCsvTEQ3MUtiQjl2WmNXNUp1YXZ6QkZta0tHTnJvLzZHMUk3ZWw0NklSNHdpalR5TkZDWVV1RDlkdGlnbk5tcFd0TjhPVytwdGlML2p0VHlTV3VranlzMHMrdkxuODNDVnZqQjBkSnRWQUlZT2dYRmRJdWlpNjZnY3p3d00vTEdpT0V4Sm4wZFROenNKL0lZaHB4TDRGQkV1UDBwc2tZMG8wYVVsSjJMUzJqK3dTUVRSS3NCZ01qeXJVcmVrbGUyT0RTdFN0bjNlYWJqSXgwL0ZIbHBGcjBqTkltL29NUDdrd2p0VVg0emFOZTQ3UUk0R2c9PTwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cDovL2Jvb2tzdGFjay5sb2NhbC9zYW1sMi9tZXRhZGF0YSIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDp0cmFuc2llbnQiPl8yYzdhYjg2ZWI4ZjFkMTA2MzQ0M2YyMTljYzU4NjhmZjY2NzA4OTEyZTM8L3NhbWw6TmFtZUlEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMTktMTEtMTdUMTc6NTg6MzlaIiBSZWNpcGllbnQ9Imh0dHA6Ly9ib29rc3RhY2subG9jYWwvc2FtbDIvYWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzZhMGY0ZjM5OTMwNDBmMTk4N2ZkMzcwNjhiNTI5NjIyOWFkNTM2MWMiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxOS0xMS0xN1QxNzo1MzowOVoiIE5vdE9uT3JBZnRlcj0iMjAxOS0xMS0xN1QxNzo1ODozOVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cDovL2Jvb2tzdGFjay5sb2NhbC9zYW1sMi9tZXRhZGF0YTwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTktMTEtMTdUMTc6NTM6MzlaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE5LTExLTE4VDAxOjUzOjM5WiIgU2Vzc2lvbkluZGV4PSJfNGZlN2MwZDE1NzJkNjRiMjdmOTMwYWE2ZjIzNmE2ZjQyZTkzMDkwMWNjIj48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnVzZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iZmlyc3RfbmFtZSIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+QmFycnk8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibGFzdF9uYW1lIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5TY290dDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlbWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlckBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1c2VyX2dyb3VwcyIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+bWVtYmVyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+';\n\n    protected string $sloResponseData = 'fZHRa8IwEMb/lZJ3bdJa04a2MOYYglOY4sNe5JKms9gmpZfC/vxF3ZjC8OXgLvl938ddjtC1vVjZTzu6d429NaiDr641KC5PBRkHIyxgg8JAp1E4JbZPbysRTanoB+ussi25QR4TgKgH11hDguWiIIeawTxOaK1iPYt5XcczHUlJeVRlMklBJjOuM1qDVCTY6wE9WRAv5HHEUS8NOjDOjyjLJoxNGN+xVESpSNgHCRYaXWPAXaijc70IQ2ntyUPqNG2tgjY8Z45CbNFLmt8V7GxBNuuX1eZ1uT7EcZJKAE4TJhXPaMxlVlFffPKKJnXE5ryusoiU+VlMXJIN5Y/feXRn1VR92GkHFTiY9sc+D2+p/HqRrQM34n33bCsd7KEd9eMd4+W32I5KaUQSlleHP9Hwv6uX3w==';\n\n    protected string $testCert = 'MIIEazCCAtOgAwIBAgIUe7a088Cnr4izmrnBEnx5q3HTMvYwDQYJKoZIhvcNAQELBQAwRTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0xOTExMTYxMjE3MTVaFw0yOTExMTUxMjE3MTVaMEUxCzAJBgNVBAYTAkdCMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDzLe9FfdyplTxHp4SuQ9gQtZT3t+SDfvEL72ppCfFZw7+B5s5B/T73aXpoQ3S53pGI1RIWCge2iCUQ2tzm27aSNH0iu9aJYcUQZ/RITqd0ayyDks1NA2PT3TW6t3m7KV5re4P0Nb+YDeuyHdkz+jcMtpn8CmBoT0H+skha0hiqINkjkRPiHvLHVGp+tHUEA/I6mN4aB/UExSTLs79NsLUfteqqxe9+tvdUaToyDPrhPFjONs+9NKCkzIC6vcv7J6AtuKG6nET+zB9yOWgtGYQifXqQA2y5dL81BB0q5uMaBLS2pq3aPPjzU2F3+EysjySWTnCkfk7C5SsCXRu8Q+U95tunpNfwf5olE6Was48NMM+PwV7iCNMPkNzllq6PCiM+P8DrMSczzUZZQUSv6dSwPCo+YSVimEM0Og3XJTiNhQ5ANlaIn66Kw5gfoBfuiXmyIKiSDyAiDYmFaf4395wWwLkTR+cw8WfjaHswKZTomn1MR3OJsY2UJ0eRBYM+YSsCAwEAAaNTMFEwHQYDVR0OBBYEFImp2CYCGfcb7w91H/cShTCkXwR/MB8GA1UdIwQYMBaAFImp2CYCGfcb7w91H/cShTCkXwR/MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggGBAA+g/C7uL9ln+W+qBknLW81kojYflgPK1I1MHIwnMvl/ZTHX4dRXKDrk7KcUq1KjqajNV66f1cakp03IijBiO0Xi1gXUZYLoCiNGUyyp9XloiIy9Xw2PiWnrw0+yZyvVssbehXXYJl4RihBjBWul9R4wMYLOUSJDe2WxcUBhJnxyNRs+P0xLSQX6B2n6nxoDko4p07s8ZKXQkeiZ2iwFdTxzRkGjthMUv704nzsVGBT0DCPtfSaO5KJZW1rCs3yiMthnBxq4qEDOQJFIl+/LD71KbB9vZcW5JuavzBFmkKGNro/6G1I7el46IR4wijTyNFCYUuD9dtignNmpWtN8OW+ptiL/jtTySWukjys0s+vLn83CVvjB0dJtVAIYOgXFdIuii66gczwwM/LGiOExJn0dTNzsJ/IYhpxL4FBEuP0pskY0o0aUlJ2LS2j+wSQTRKsBgMjyrUrekle2ODStStn3eabjIx0/FHlpFr0jNIm/oMP7kwjtUX4zaNe47QI4Gg==';\n}\n"
  },
  {
    "path": "tests/Auth/SocialAuthTest.php",
    "content": "<?php\n\nnamespace Tests\\Auth;\n\nuse BookStack\\Access\\SocialAccount;\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Support\\Facades\\DB;\nuse Laravel\\Socialite\\Contracts\\Factory;\nuse Laravel\\Socialite\\Contracts\\Provider;\nuse Mockery;\nuse Tests\\TestCase;\n\nclass SocialAuthTest extends TestCase\n{\n    public function test_social_registration()\n    {\n        $user = User::factory()->make();\n\n        $this->setSettings(['registration-enabled' => 'true']);\n        config(['GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc']);\n\n        $mockSocialite = $this->mock(Factory::class);\n        $mockSocialDriver = Mockery::mock(Provider::class);\n        $mockSocialUser = Mockery::mock(\\Laravel\\Socialite\\Contracts\\User::class);\n\n        $mockSocialite->shouldReceive('driver')->twice()->with('google')->andReturn($mockSocialDriver);\n        $mockSocialDriver->shouldReceive('redirect')->once()->andReturn(redirect('/'));\n        $mockSocialDriver->shouldReceive('user')->once()->andReturn($mockSocialUser);\n\n        $mockSocialUser->shouldReceive('getId')->twice()->andReturn(1);\n        $mockSocialUser->shouldReceive('getEmail')->twice()->andReturn($user->email);\n        $mockSocialUser->shouldReceive('getName')->once()->andReturn($user->name);\n        $mockSocialUser->shouldReceive('getAvatar')->once()->andReturn('avatar_placeholder');\n\n        $this->get('/register/service/google');\n        $this->get('/login/service/google/callback');\n        $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email]);\n        $user = $user->whereEmail($user->email)->first();\n        $this->assertDatabaseHas('social_accounts', ['user_id' => $user->id]);\n    }\n\n    public function test_social_login()\n    {\n        config([\n            'GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc',\n            'GITHUB_APP_ID' => 'abc123', 'GITHUB_APP_SECRET' => '123abc',\n        ]);\n\n        $mockSocialite = $this->mock(Factory::class);\n        $mockSocialDriver = Mockery::mock(Provider::class);\n        $mockSocialUser = Mockery::mock(\\Laravel\\Socialite\\Contracts\\User::class);\n\n        $mockSocialUser->shouldReceive('getId')->twice()->andReturn('logintest123');\n\n        $mockSocialDriver->shouldReceive('user')->twice()->andReturn($mockSocialUser);\n        $mockSocialite->shouldReceive('driver')->twice()->with('google')->andReturn($mockSocialDriver);\n        $mockSocialite->shouldReceive('driver')->twice()->with('github')->andReturn($mockSocialDriver);\n        $mockSocialDriver->shouldReceive('redirect')->twice()->andReturn(redirect('/'));\n\n        // Test login routes\n        $resp = $this->get('/login');\n        $this->withHtml($resp)->assertElementExists('a#social-login-google[href$=\"/login/service/google\"]');\n        $resp = $this->followingRedirects()->get('/login/service/google');\n        $resp->assertSee('login-form');\n\n        // Test social callback\n        $resp = $this->followingRedirects()->get('/login/service/google/callback');\n        $resp->assertSee('login-form');\n        $resp->assertSee(trans('errors.social_account_not_used', ['socialAccount' => 'Google']));\n\n        $resp = $this->get('/login');\n        $this->withHtml($resp)->assertElementExists('a#social-login-github[href$=\"/login/service/github\"]');\n        $resp = $this->followingRedirects()->get('/login/service/github');\n        $resp->assertSee('login-form');\n\n        // Test social callback with matching social account\n        DB::table('social_accounts')->insert([\n            'user_id'   => $this->users->admin()->id,\n            'driver'    => 'github',\n            'driver_id' => 'logintest123',\n        ]);\n        $resp = $this->followingRedirects()->get('/login/service/github/callback');\n        $resp->assertDontSee('login-form');\n        $this->assertActivityExists(ActivityType::AUTH_LOGIN, null, 'github; (' . $this->users->admin()->id . ') ' . $this->users->admin()->name);\n    }\n\n    public function test_social_account_attach()\n    {\n        config([\n            'GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc',\n        ]);\n        $editor = $this->users->editor();\n\n        $mockSocialite = $this->mock(Factory::class);\n        $mockSocialDriver = Mockery::mock(Provider::class);\n        $mockSocialUser = Mockery::mock(\\Laravel\\Socialite\\Contracts\\User::class);\n\n        $mockSocialUser->shouldReceive('getId')->twice()->andReturn('logintest123');\n        $mockSocialUser->shouldReceive('getAvatar')->andReturn(null);\n\n        $mockSocialite->shouldReceive('driver')->twice()->with('google')->andReturn($mockSocialDriver);\n        $mockSocialDriver->shouldReceive('redirect')->once()->andReturn(redirect('/login/service/google/callback'));\n        $mockSocialDriver->shouldReceive('user')->once()->andReturn($mockSocialUser);\n\n        // Test login routes\n        $resp = $this->actingAs($editor)->followingRedirects()->get('/login/service/google');\n        $resp->assertSee('Access & Security');\n\n        // Test social callback with matching social account\n        $this->assertDatabaseHas('social_accounts', [\n            'user_id'   => $editor->id,\n            'driver'    => 'google',\n            'driver_id' => 'logintest123',\n        ]);\n    }\n\n    public function test_social_account_detach()\n    {\n        $editor = $this->users->editor();\n        config([\n            'GITHUB_APP_ID' => 'abc123', 'GITHUB_APP_SECRET' => '123abc',\n        ]);\n\n        $socialAccount = SocialAccount::query()->forceCreate([\n            'user_id'   => $editor->id,\n            'driver'    => 'github',\n            'driver_id' => 'logintest123',\n        ]);\n\n        $resp = $this->actingAs($editor)->get('/my-account/auth');\n        $this->withHtml($resp)->assertElementContains('form[action$=\"/login/service/github/detach\"]', 'Disconnect Account');\n\n        $resp = $this->post('/login/service/github/detach');\n        $resp->assertRedirect('/my-account/auth#social-accounts');\n        $resp = $this->followRedirects($resp);\n        $resp->assertSee('Github account was successfully disconnected from your profile.');\n\n        $this->assertDatabaseMissing('social_accounts', ['id' => $socialAccount->id]);\n    }\n\n    public function test_social_autoregister()\n    {\n        config([\n            'services.google.client_id' => 'abc123', 'services.google.client_secret' => '123abc',\n        ]);\n\n        $user = User::factory()->make();\n        $mockSocialite = $this->mock(Factory::class);\n        $mockSocialDriver = Mockery::mock(Provider::class);\n        $mockSocialUser = Mockery::mock(\\Laravel\\Socialite\\Contracts\\User::class);\n\n        $mockSocialUser->shouldReceive('getId')->times(4)->andReturn(1);\n        $mockSocialUser->shouldReceive('getEmail')->times(2)->andReturn($user->email);\n        $mockSocialUser->shouldReceive('getName')->once()->andReturn($user->name);\n        $mockSocialUser->shouldReceive('getAvatar')->once()->andReturn('avatar_placeholder');\n\n        $mockSocialDriver->shouldReceive('user')->times(2)->andReturn($mockSocialUser);\n        $mockSocialite->shouldReceive('driver')->times(4)->with('google')->andReturn($mockSocialDriver);\n        $mockSocialDriver->shouldReceive('redirect')->twice()->andReturn(redirect('/'));\n\n        $googleAccountNotUsedMessage = trans('errors.social_account_not_used', ['socialAccount' => 'Google']);\n\n        $this->get('/login/service/google');\n        $resp = $this->followingRedirects()->get('/login/service/google/callback');\n        $resp->assertSee($googleAccountNotUsedMessage);\n\n        config(['services.google.auto_register' => true]);\n\n        $this->get('/login/service/google');\n        $resp = $this->followingRedirects()->get('/login/service/google/callback');\n        $resp->assertDontSee($googleAccountNotUsedMessage);\n\n        $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);\n        $user = $user->whereEmail($user->email)->first();\n        $this->assertDatabaseHas('social_accounts', ['user_id' => $user->id]);\n    }\n\n    public function test_social_auto_email_confirm()\n    {\n        config([\n            'services.google.client_id' => 'abc123', 'services.google.client_secret' => '123abc',\n            'services.google.auto_register' => true, 'services.google.auto_confirm' => true,\n        ]);\n\n        $user = User::factory()->make();\n        $mockSocialite = $this->mock(Factory::class);\n        $mockSocialDriver = Mockery::mock(Provider::class);\n        $mockSocialUser = Mockery::mock(\\Laravel\\Socialite\\Contracts\\User::class);\n\n        $mockSocialUser->shouldReceive('getId')->times(3)->andReturn(1);\n        $mockSocialUser->shouldReceive('getEmail')->times(2)->andReturn($user->email);\n        $mockSocialUser->shouldReceive('getName')->once()->andReturn($user->name);\n        $mockSocialUser->shouldReceive('getAvatar')->once()->andReturn('avatar_placeholder');\n\n        $mockSocialDriver->shouldReceive('user')->times(1)->andReturn($mockSocialUser);\n        $mockSocialite->shouldReceive('driver')->times(2)->with('google')->andReturn($mockSocialDriver);\n        $mockSocialDriver->shouldReceive('redirect')->once()->andReturn(redirect('/'));\n\n        $this->get('/login/service/google');\n        $this->get('/login/service/google/callback');\n\n        $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => true]);\n        $user = $user->whereEmail($user->email)->first();\n        $this->assertDatabaseHas('social_accounts', ['user_id' => $user->id]);\n    }\n\n    public function test_google_select_account_option_changes_redirect_url()\n    {\n        config()->set('services.google.select_account', 'true');\n\n        $resp = $this->get('/login/service/google');\n        $this->assertStringContainsString('prompt=select_account', $resp->headers->get('Location'));\n    }\n\n    public function test_social_registration_with_no_name_uses_email_as_name()\n    {\n        $user = User::factory()->make(['email' => 'nonameuser@example.com']);\n\n        $this->setSettings(['registration-enabled' => 'true']);\n        config(['GITHUB_APP_ID' => 'abc123', 'GITHUB_APP_SECRET' => '123abc']);\n\n        $mockSocialite = $this->mock(Factory::class);\n        $mockSocialDriver = Mockery::mock(Provider::class);\n        $mockSocialUser = Mockery::mock(\\Laravel\\Socialite\\Contracts\\User::class);\n\n        $mockSocialite->shouldReceive('driver')->twice()->with('github')->andReturn($mockSocialDriver);\n        $mockSocialDriver->shouldReceive('redirect')->once()->andReturn(redirect('/'));\n        $mockSocialDriver->shouldReceive('user')->once()->andReturn($mockSocialUser);\n\n        $mockSocialUser->shouldReceive('getId')->twice()->andReturn(1);\n        $mockSocialUser->shouldReceive('getEmail')->twice()->andReturn($user->email);\n        $mockSocialUser->shouldReceive('getName')->once()->andReturn('');\n        $mockSocialUser->shouldReceive('getAvatar')->once()->andReturn('avatar_placeholder');\n\n        $this->get('/register/service/github');\n        $this->get('/login/service/github/callback');\n        $this->assertDatabaseHas('users', ['name' => 'nonameuser', 'email' => $user->email]);\n        $user = $user->whereEmail($user->email)->first();\n        $this->assertDatabaseHas('social_accounts', ['user_id' => $user->id]);\n    }\n}\n"
  },
  {
    "path": "tests/Auth/UserInviteTest.php",
    "content": "<?php\n\nnamespace Tests\\Auth;\n\nuse BookStack\\Access\\Notifications\\UserInviteNotification;\nuse BookStack\\Access\\UserInviteService;\nuse BookStack\\Users\\Models\\User;\nuse Carbon\\Carbon;\nuse Illuminate\\Notifications\\Messages\\MailMessage;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Notification;\nuse Illuminate\\Support\\Str;\nuse Tests\\TestCase;\n\nclass UserInviteTest extends TestCase\n{\n    public function test_user_creation_creates_invite()\n    {\n        Notification::fake();\n        $admin = $this->users->admin();\n\n        $email = Str::random(16) . '@example.com';\n        $resp = $this->actingAs($admin)->post('/settings/users/create', [\n            'name'        => 'Barry',\n            'email'       => $email,\n            'send_invite' => 'true',\n        ]);\n        $resp->assertRedirect('/settings/users');\n\n        $newUser = User::query()->where('email', '=', $email)->orderBy('id', 'desc')->first();\n\n        Notification::assertSentTo($newUser, UserInviteNotification::class);\n        $this->assertDatabaseHas('user_invites', [\n            'user_id' => $newUser->id,\n        ]);\n    }\n\n    public function test_user_invite_sent_in_selected_language()\n    {\n        Notification::fake();\n        $admin = $this->users->admin();\n\n        $email = Str::random(16) . '@example.com';\n        $resp = $this->actingAs($admin)->post('/settings/users/create', [\n            'name'        => 'Barry',\n            'email'       => $email,\n            'send_invite' => 'true',\n            'language'    => 'de',\n        ]);\n        $resp->assertRedirect('/settings/users');\n\n        $newUser = User::query()->where('email', '=', $email)->orderBy('id', 'desc')->first();\n        Notification::assertSentTo($newUser, UserInviteNotification::class, function ($notification, $channels, $notifiable) {\n            /** @var MailMessage $mail */\n            $mail = $notification->toMail($notifiable);\n\n            return 'Sie wurden eingeladen, BookStack beizutreten!' === $mail->subject &&\n                'Ein Konto wurde für Sie auf BookStack erstellt.' === $mail->greeting;\n        });\n    }\n\n    public function test_invite_set_password()\n    {\n        Notification::fake();\n        $user = $this->users->viewer();\n        $inviteService = app(UserInviteService::class);\n\n        $inviteService->sendInvitation($user);\n        $token = DB::table('user_invites')->where('user_id', '=', $user->id)->first()->token;\n\n        $setPasswordPageResp = $this->get('/register/invite/' . $token);\n        $setPasswordPageResp->assertSuccessful();\n        $setPasswordPageResp->assertSee('Welcome to BookStack!');\n        $setPasswordPageResp->assertSee('Password');\n        $setPasswordPageResp->assertSee('Confirm Password');\n\n        $setPasswordResp = $this->followingRedirects()->post('/register/invite/' . $token, [\n            'password' => 'my test password',\n        ]);\n        $setPasswordResp->assertSee('Password set, you should now be able to login using your set password to access BookStack!');\n        $newPasswordValid = auth()->validate([\n            'email'    => $user->email,\n            'password' => 'my test password',\n        ]);\n        $this->assertTrue($newPasswordValid);\n        $this->assertDatabaseMissing('user_invites', [\n            'user_id' => $user->id,\n        ]);\n    }\n\n    public function test_invite_set_has_password_validation()\n    {\n        Notification::fake();\n        $user = $this->users->viewer();\n        $inviteService = app(UserInviteService::class);\n\n        $inviteService->sendInvitation($user);\n        $token = DB::table('user_invites')->where('user_id', '=', $user->id)->first()->token;\n\n        $this->get('/register/invite/' . $token);\n        $shortPassword = $this->followingRedirects()->post('/register/invite/' . $token, [\n            'password' => 'mypassw',\n        ]);\n        $shortPassword->assertSee('The password must be at least 8 characters.');\n\n        $this->get('/register/invite/' . $token);\n        $noPassword = $this->followingRedirects()->post('/register/invite/' . $token, [\n            'password' => '',\n        ]);\n        $noPassword->assertSee('The password field is required.');\n\n        $this->assertDatabaseHas('user_invites', [\n            'user_id' => $user->id,\n        ]);\n    }\n\n    public function test_non_existent_invite_token_redirects_to_home()\n    {\n        $setPasswordPageResp = $this->get('/register/invite/' . Str::random(12));\n        $setPasswordPageResp->assertRedirect('/');\n\n        $setPasswordResp = $this->post('/register/invite/' . Str::random(12), ['password' => 'Password Test']);\n        $setPasswordResp->assertRedirect('/');\n    }\n\n    public function test_token_expires_after_two_weeks()\n    {\n        Notification::fake();\n        $user = $this->users->viewer();\n        $inviteService = app(UserInviteService::class);\n\n        $inviteService->sendInvitation($user);\n        $tokenEntry = DB::table('user_invites')->where('user_id', '=', $user->id)->first();\n        DB::table('user_invites')->update(['created_at' => Carbon::now()->subDays(14)->subHour(1)]);\n\n        $setPasswordPageResp = $this->get('/register/invite/' . $tokenEntry->token);\n        $setPasswordPageResp->assertRedirect('/password/email');\n        $setPasswordPageResp->assertSessionHas('error', 'This invitation link has expired. You can instead try to reset your account password.');\n    }\n\n    public function test_set_password_view_is_throttled()\n    {\n        for ($i = 0; $i < 11; $i++) {\n            $response = $this->get(\"/register/invite/tokenhere{$i}\");\n        }\n\n        $response->assertStatus(429);\n    }\n\n    public function test_set_password_post_is_throttled()\n    {\n        for ($i = 0; $i < 11; $i++) {\n            $response = $this->post(\"/register/invite/tokenhere{$i}\", [\n                'password' => 'my test password',\n            ]);\n        }\n\n        $response->assertStatus(429);\n    }\n}\n"
  },
  {
    "path": "tests/Commands/AssignSortRuleCommandTest.php",
    "content": "<?php\n\nnamespace Tests\\Commands;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Sorting\\SortRule;\nuse Tests\\TestCase;\n\nclass AssignSortRuleCommandTest extends TestCase\n{\n    public function test_no_given_sort_rule_lists_options()\n    {\n        $sortRules = SortRule::factory()->createMany(10);\n\n        $commandRun = $this->artisan('bookstack:assign-sort-rule')\n            ->expectsOutputToContain('Sort rule ID required!')\n            ->assertExitCode(1);\n\n        foreach ($sortRules as $sortRule) {\n            $commandRun->expectsOutputToContain(\"{$sortRule->id}: {$sortRule->name}\");\n        }\n    }\n\n    public function test_run_without_options_advises_help()\n    {\n        $this->artisan(\"bookstack:assign-sort-rule 100\")\n            ->expectsOutput(\"No option provided to specify target. Run with the -h option to see all available options.\")\n            ->assertExitCode(1);\n    }\n\n    public function test_run_without_valid_sort_advises_help()\n    {\n        $this->artisan(\"bookstack:assign-sort-rule 100342 --all-books\")\n            ->expectsOutput(\"Sort rule of provided id 100342 not found!\")\n            ->assertExitCode(1);\n    }\n\n    public function test_confirmation_required()\n    {\n        $sortRule = SortRule::factory()->create();\n\n        $this->artisan(\"bookstack:assign-sort-rule {$sortRule->id} --all-books\")\n            ->expectsConfirmation('Are you sure you want to continue?', 'no')\n            ->assertExitCode(1);\n\n        $booksWithSort = Book::query()->whereNotNull('sort_rule_id')->count();\n        $this->assertEquals(0, $booksWithSort);\n    }\n\n    public function test_assign_to_all_books()\n    {\n        $sortRule = SortRule::factory()->create();\n        $booksWithoutSort = Book::query()->whereNull('sort_rule_id')->count();\n        $this->assertGreaterThan(0, $booksWithoutSort);\n\n        $this->artisan(\"bookstack:assign-sort-rule {$sortRule->id} --all-books\")\n            ->expectsOutputToContain(\"This will apply sort rule [{$sortRule->id}: {$sortRule->name}] to {$booksWithoutSort} book(s)\")\n            ->expectsConfirmation('Are you sure you want to continue?', 'yes')\n            ->expectsOutputToContain(\"Sort applied to {$booksWithoutSort} book(s)\")\n            ->assertExitCode(0);\n\n        $booksWithoutSort = Book::query()->whereNull('sort_rule_id')->count();\n        $this->assertEquals(0, $booksWithoutSort);\n    }\n\n    public function test_assign_to_all_books_without_sort()\n    {\n        $totalBooks = Book::query()->count();\n        $book = $this->entities->book();\n        $sortRuleA = SortRule::factory()->create();\n        $sortRuleB = SortRule::factory()->create();\n        $book->sort_rule_id = $sortRuleA->id;\n        $book->save();\n\n        $booksWithoutSort = Book::query()->whereNull('sort_rule_id')->count();\n        $this->assertEquals($totalBooks, $booksWithoutSort + 1);\n\n        $this->artisan(\"bookstack:assign-sort-rule {$sortRuleB->id} --books-without-sort\")\n            ->expectsConfirmation('Are you sure you want to continue?', 'yes')\n            ->expectsOutputToContain(\"Sort applied to {$booksWithoutSort} book(s)\")\n            ->assertExitCode(0);\n\n        $booksWithoutSort = Book::query()->whereNull('sort_rule_id')->count();\n        $this->assertEquals(0, $booksWithoutSort);\n        $this->assertEquals($totalBooks, $sortRuleB->books()->count() + 1);\n    }\n\n    public function test_assign_to_all_books_with_sort()\n    {\n        $book = $this->entities->book();\n        $sortRuleA = SortRule::factory()->create();\n        $sortRuleB = SortRule::factory()->create();\n        $book->sort_rule_id = $sortRuleA->id;\n        $book->save();\n\n        $this->artisan(\"bookstack:assign-sort-rule {$sortRuleB->id} --books-with-sort={$sortRuleA->id}\")\n            ->expectsConfirmation('Are you sure you want to continue?', 'yes')\n            ->expectsOutputToContain(\"Sort applied to 1 book(s)\")\n            ->assertExitCode(0);\n\n        $book->refresh();\n        $this->assertEquals($sortRuleB->id, $book->sort_rule_id);\n        $this->assertEquals(1, $sortRuleB->books()->count());\n    }\n\n    public function test_assign_to_all_books_with_sort_id_is_validated()\n    {\n        $this->artisan(\"bookstack:assign-sort-rule 50 --books-with-sort=beans\")\n            ->expectsOutputToContain(\"Provided --books-with-sort option value is invalid\")\n            ->assertExitCode(1);\n    }\n}\n"
  },
  {
    "path": "tests/Commands/CleanupImagesCommandTest.php",
    "content": "<?php\n\nnamespace Tests\\Commands;\n\nuse BookStack\\Uploads\\Image;\nuse Tests\\TestCase;\n\nclass CleanupImagesCommandTest extends TestCase\n{\n    public function test_command_defaults_to_dry_run()\n    {\n        $page = $this->entities->page();\n        $image = Image::factory()->create(['uploaded_to' => $page->id]);\n\n        $this->artisan('bookstack:cleanup-images -v')\n            ->expectsOutput('Dry run, no images have been deleted')\n            ->expectsOutput('1 image(s) found that would have been deleted')\n            ->expectsOutputToContain($image->path)\n            ->assertExitCode(0);\n\n        $this->assertDatabaseHas('images', ['id' => $image->id]);\n    }\n\n    public function test_command_force_run()\n    {\n        $page = $this->entities->page();\n        $image = Image::factory()->create(['uploaded_to' => $page->id]);\n\n        $this->artisan('bookstack:cleanup-images --force')\n            ->expectsOutputToContain('This operation is destructive and is not guaranteed to be fully accurate')\n            ->expectsConfirmation('Are you sure you want to proceed?', 'yes')\n            ->expectsOutput('1 image(s) deleted')\n            ->assertExitCode(0);\n\n        $this->assertDatabaseMissing('images', ['id' => $image->id]);\n    }\n\n    public function test_command_force_run_negative_confirmation()\n    {\n        $page = $this->entities->page();\n        $image = Image::factory()->create(['uploaded_to' => $page->id]);\n\n        $this->artisan('bookstack:cleanup-images --force')\n            ->expectsConfirmation('Are you sure you want to proceed?', 'no')\n            ->assertExitCode(0);\n\n        $this->assertDatabaseHas('images', ['id' => $image->id]);\n    }\n\n    public function test_command_force_no_interaction_run()\n    {\n        $page = $this->entities->page();\n        $image = Image::factory()->create(['uploaded_to' => $page->id]);\n\n        $this->artisan('bookstack:cleanup-images --force --no-interaction')\n            ->expectsOutputToContain('This operation is destructive and is not guaranteed to be fully accurate')\n            ->expectsOutput('1 image(s) deleted')\n            ->assertExitCode(0);\n\n        $this->assertDatabaseMissing('images', ['id' => $image->id]);\n    }\n}\n"
  },
  {
    "path": "tests/Commands/ClearActivityCommandTest.php",
    "content": "<?php\n\nnamespace Tests\\Commands;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Facades\\Activity;\nuse Illuminate\\Support\\Facades\\Artisan;\nuse Illuminate\\Support\\Facades\\DB;\nuse Tests\\TestCase;\n\nclass ClearActivityCommandTest extends TestCase\n{\n    public function test_clear_activity_command()\n    {\n        $this->asEditor();\n        $page = $this->entities->page();\n        Activity::add(ActivityType::PAGE_UPDATE, $page);\n\n        $this->assertDatabaseHas('activities', [\n            'type'      => 'page_update',\n            'loggable_id' => $page->id,\n            'user_id'   => $this->users->editor()->id,\n        ]);\n\n        DB::rollBack();\n        $exitCode = Artisan::call('bookstack:clear-activity');\n        DB::beginTransaction();\n        $this->assertTrue($exitCode === 0, 'Command executed successfully');\n\n        $this->assertDatabaseMissing('activities', [\n            'type' => 'page_update',\n        ]);\n    }\n}\n"
  },
  {
    "path": "tests/Commands/ClearRevisionsCommandTest.php",
    "content": "<?php\n\nnamespace Tests\\Commands;\n\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Repos\\PageRepo;\nuse Illuminate\\Support\\Facades\\Artisan;\nuse Tests\\TestCase;\n\nclass ClearRevisionsCommandTest extends TestCase\n{\n    public function test_clear_revisions_command()\n    {\n        $this->asEditor();\n        $pageRepo = app(PageRepo::class);\n        $page = Page::first();\n        $pageRepo->update($page, ['name' => 'updated page', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);\n        $pageRepo->updatePageDraft($page, ['name' => 'updated page', 'html' => '<p>new content in draft</p>', 'summary' => 'page revision testing']);\n\n        $this->assertDatabaseHas('page_revisions', [\n            'page_id' => $page->id,\n            'type'    => 'version',\n        ]);\n        $this->assertDatabaseHas('page_revisions', [\n            'page_id' => $page->id,\n            'type'    => 'update_draft',\n        ]);\n\n        $exitCode = Artisan::call('bookstack:clear-revisions');\n        $this->assertTrue($exitCode === 0, 'Command executed successfully');\n\n        $this->assertDatabaseMissing('page_revisions', [\n            'page_id' => $page->id,\n            'type'    => 'version',\n        ]);\n        $this->assertDatabaseHas('page_revisions', [\n            'page_id' => $page->id,\n            'type'    => 'update_draft',\n        ]);\n\n        $exitCode = Artisan::call('bookstack:clear-revisions', ['--all' => true]);\n        $this->assertTrue($exitCode === 0, 'Command executed successfully');\n\n        $this->assertDatabaseMissing('page_revisions', [\n            'page_id' => $page->id,\n            'type'    => 'update_draft',\n        ]);\n    }\n}\n"
  },
  {
    "path": "tests/Commands/ClearViewsCommandTest.php",
    "content": "<?php\n\nnamespace Tests\\Commands;\n\nuse BookStack\\Entities\\Models\\Page;\nuse Illuminate\\Support\\Facades\\DB;\nuse Tests\\TestCase;\n\nclass ClearViewsCommandTest extends TestCase\n{\n    public function test_clear_views_command()\n    {\n        $this->asEditor();\n        $page = Page::first();\n\n        $this->get($page->getUrl());\n\n        $this->assertDatabaseHas('views', [\n            'user_id'     => $this->users->editor()->id,\n            'viewable_id' => $page->id,\n            'views'       => 1,\n        ]);\n\n        DB::rollBack();\n        $exitCode = \\Artisan::call('bookstack:clear-views');\n        DB::beginTransaction();\n        $this->assertTrue($exitCode === 0, 'Command executed successfully');\n\n        $this->assertDatabaseMissing('views', [\n            'user_id' => $this->users->editor()->id,\n        ]);\n    }\n}\n"
  },
  {
    "path": "tests/Commands/CopyShelfPermissionsCommandTest.php",
    "content": "<?php\n\nnamespace Tests\\Commands;\n\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse Tests\\TestCase;\n\nclass CopyShelfPermissionsCommandTest extends TestCase\n{\n    public function test_copy_shelf_permissions_command_shows_error_when_no_required_option_given()\n    {\n        $this->artisan('bookstack:copy-shelf-permissions')\n            ->expectsOutput('Either a --slug or --all option must be provided.')\n            ->assertExitCode(1);\n    }\n\n    public function test_copy_shelf_permissions_command_using_slug()\n    {\n        $shelf = $this->entities->shelf();\n        $child = $shelf->books()->first();\n        $editorRole = $this->users->editor()->roles()->first();\n        $this->assertFalse($child->hasPermissions(), 'Child book should not be restricted by default');\n        $this->assertTrue($child->permissions()->count() === 0, 'Child book should have no permissions by default');\n\n        $this->permissions->setEntityPermissions($shelf, ['view', 'update'], [$editorRole]);\n        $this->artisan('bookstack:copy-shelf-permissions', [\n            '--slug' => $shelf->slug,\n        ]);\n        $child = $shelf->books()->first();\n\n        $this->assertTrue($child->hasPermissions(), 'Child book should now be restricted');\n        $this->assertEquals(2, $child->permissions()->count(), 'Child book should have copied permissions');\n        $this->assertDatabaseHas('entity_permissions', [\n            'entity_type' => 'book',\n            'entity_id' => $child->id,\n            'role_id' => $editorRole->id,\n            'view' => true, 'update' => true, 'create' => false, 'delete' => false,\n        ]);\n    }\n\n    public function test_copy_shelf_permissions_command_using_all()\n    {\n        $shelf = $this->entities->shelf();\n        Bookshelf::query()->where('id', '!=', $shelf->id)->delete();\n        $child = $shelf->books()->first();\n        $editorRole = $this->users->editor()->roles()->first();\n        $this->assertFalse($child->hasPermissions(), 'Child book should not be restricted by default');\n        $this->assertTrue($child->permissions()->count() === 0, 'Child book should have no permissions by default');\n\n        $this->permissions->setEntityPermissions($shelf, ['view', 'update'], [$editorRole]);\n        $this->artisan('bookstack:copy-shelf-permissions --all')\n            ->expectsQuestion('Permission settings for all shelves will be cascaded. Books assigned to multiple shelves will receive only the permissions of it\\'s last processed shelf. Are you sure you want to proceed?', 'y');\n        $child = $shelf->books()->first();\n\n        $this->assertTrue($child->hasPermissions(), 'Child book should now be restricted');\n        $this->assertEquals(2, $child->permissions()->count(), 'Child book should have copied permissions');\n        $this->assertDatabaseHas('entity_permissions', [\n            'entity_type' => 'book',\n            'entity_id' => $child->id,\n            'role_id' => $editorRole->id,\n            'view' => true, 'update' => true, 'create' => false, 'delete' => false,\n        ]);\n    }\n}\n"
  },
  {
    "path": "tests/Commands/CreateAdminCommandTest.php",
    "content": "<?php\n\nnamespace Tests\\Commands;\n\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Support\\Facades\\Artisan;\nuse Illuminate\\Support\\Facades\\Auth;\nuse Illuminate\\Support\\Facades\\Hash;\nuse Tests\\TestCase;\n\nclass CreateAdminCommandTest extends TestCase\n{\n    public function test_standard_command_usage()\n    {\n        $this->artisan('bookstack:create-admin', [\n            '--email' => 'admintest@example.com',\n            '--name' => 'Admin Test',\n            '--password' => 'testing-4',\n        ])->assertExitCode(0);\n\n        $this->assertDatabaseHas('users', [\n            'email' => 'admintest@example.com',\n            'name' => 'Admin Test',\n        ]);\n\n        /** @var User $user */\n        $user = User::query()->where('email', '=', 'admintest@example.com')->first();\n        $this->assertTrue($user->hasSystemRole('admin'));\n        $this->assertTrue(Auth::attempt(['email' => 'admintest@example.com', 'password' => 'testing-4']));\n    }\n\n    public function test_providing_external_auth_id()\n    {\n        $this->artisan('bookstack:create-admin', [\n            '--email' => 'admintest@example.com',\n            '--name' => 'Admin Test',\n            '--external-auth-id' => 'xX_admin_Xx',\n        ])->assertExitCode(0);\n\n        $this->assertDatabaseHas('users', [\n            'email' => 'admintest@example.com',\n            'name' => 'Admin Test',\n            'external_auth_id' => 'xX_admin_Xx',\n        ]);\n\n        /** @var User $user */\n        $user = User::query()->where('email', '=', 'admintest@example.com')->first();\n        $this->assertNotEmpty($user->password);\n    }\n\n    public function test_password_required_if_external_auth_id_not_given()\n    {\n        $this->artisan('bookstack:create-admin', [\n            '--email' => 'admintest@example.com',\n            '--name' => 'Admin Test',\n        ])->expectsQuestion('Please specify a password for the new admin user (8 characters min)', 'hunter2000')\n            ->assertExitCode(0);\n\n        $this->assertDatabaseHas('users', [\n            'email' => 'admintest@example.com',\n            'name' => 'Admin Test',\n        ]);\n        $this->assertTrue(Auth::attempt(['email' => 'admintest@example.com', 'password' => 'hunter2000']));\n    }\n\n    public function test_generate_password_option()\n    {\n        $this->withoutMockingConsoleOutput()\n            ->artisan('bookstack:create-admin', [\n                '--email' => 'admintest@example.com',\n                '--name' => 'Admin Test',\n                '--generate-password' => true,\n            ]);\n\n        $output = trim(Artisan::output());\n        $this->assertMatchesRegularExpression('/^[a-zA-Z0-9]{32}$/', $output);\n\n        $user = User::query()->where('email', '=', 'admintest@example.com')->first();\n        $this->assertTrue(Hash::check($output, $user->password));\n    }\n\n    public function test_initial_option_updates_default_admin()\n    {\n        $defaultAdmin = User::query()->where('email', '=', 'admin@admin.com')->first();\n\n        $this->artisan('bookstack:create-admin', [\n            '--email' => 'firstadmin@example.com',\n            '--name' => 'Admin Test',\n            '--password' => 'testing-7',\n            '--initial' => true,\n        ])->expectsOutput('The default admin user has been updated with the provided details!')\n            ->assertExitCode(0);\n\n        $defaultAdmin->refresh();\n\n        $this->assertEquals('firstadmin@example.com', $defaultAdmin->email);\n    }\n\n    public function test_initial_option_does_not_update_if_only_non_default_admin_exists()\n    {\n        $defaultAdmin = User::query()->where('email', '=', 'admin@admin.com')->first();\n        $defaultAdmin->email = 'testadmin@example.com';\n        $defaultAdmin->save();\n\n        $this->artisan('bookstack:create-admin', [\n            '--email' => 'firstadmin@example.com',\n            '--name' => 'Admin Test',\n            '--password' => 'testing-7',\n            '--initial' => true,\n        ])->expectsOutput('Non-default admin user already exists. Skipping creation of new admin user.')\n            ->assertExitCode(2);\n\n        $defaultAdmin->refresh();\n\n        $this->assertEquals('testadmin@example.com', $defaultAdmin->email);\n    }\n\n    public function test_initial_option_updates_creates_new_admin_if_none_exists()\n    {\n        $adminRole = Role::getSystemRole('admin');\n        $adminRole->users()->delete();\n        $this->assertEquals(0, $adminRole->users()->count());\n\n        $this->artisan('bookstack:create-admin', [\n            '--email' => 'firstadmin@example.com',\n            '--name' => 'My initial admin',\n            '--password' => 'testing-7',\n            '--initial' => true,\n        ])->expectsOutput(\"Admin account with email \\\"firstadmin@example.com\\\" successfully created!\")\n            ->assertExitCode(0);\n\n        $this->assertEquals(1, $adminRole->users()->count());\n        $this->assertDatabaseHas('users', [\n            'email' => 'firstadmin@example.com',\n            'name' => 'My initial admin',\n        ]);\n    }\n\n    public function test_initial_rerun_does_not_error_but_skips()\n    {\n        $adminRole = Role::getSystemRole('admin');\n        $adminRole->users()->delete();\n\n        $this->artisan('bookstack:create-admin', [\n            '--email' => 'firstadmin@example.com',\n            '--name' => 'My initial admin',\n            '--password' => 'testing-7',\n            '--initial' => true,\n        ])->expectsOutput(\"Admin account with email \\\"firstadmin@example.com\\\" successfully created!\")\n            ->assertExitCode(0);\n\n        $this->artisan('bookstack:create-admin', [\n            '--email' => 'firstadmin@example.com',\n            '--name' => 'My initial admin',\n            '--password' => 'testing-7',\n            '--initial' => true,\n        ])->expectsOutput(\"Non-default admin user already exists. Skipping creation of new admin user.\")\n            ->assertExitCode(2);\n    }\n\n    public function test_initial_option_creation_errors_if_email_already_exists()\n    {\n        $adminRole = Role::getSystemRole('admin');\n        $adminRole->users()->delete();\n        $editor = $this->users->editor();\n\n        $this->artisan('bookstack:create-admin', [\n            '--email' => $editor->email,\n            '--name' => 'My initial admin',\n            '--password' => 'testing-7',\n            '--initial' => true,\n        ])->expectsOutput(\"Could not create admin account.\")\n            ->expectsOutput(\"An account with the email address \\\"{$editor->email}\\\" already exists.\")\n            ->assertExitCode(1);\n    }\n\n    public function test_initial_option_updating_errors_if_email_already_exists()\n    {\n        $editor = $this->users->editor();\n        $defaultAdmin = User::query()->where('email', '=', 'admin@admin.com')->first();\n        $this->assertNotNull($defaultAdmin);\n\n        $this->artisan('bookstack:create-admin', [\n            '--email' => $editor->email,\n            '--name' => 'My initial admin',\n            '--password' => 'testing-7',\n            '--initial' => true,\n        ])->expectsOutput(\"Could not create admin account.\")\n            ->expectsOutput(\"An account with the email address \\\"{$editor->email}\\\" already exists.\")\n            ->assertExitCode(1);\n    }\n\n    public function test_initial_option_does_not_require_name_or_email_to_be_passed()\n    {\n        $adminRole = Role::getSystemRole('admin');\n        $adminRole->users()->delete();\n        $this->assertEquals(0, $adminRole->users()->count());\n\n        $this->artisan('bookstack:create-admin', [\n            '--generate-password' => true,\n            '--initial' => true,\n        ])->assertExitCode(0);\n\n        $this->assertEquals(1, $adminRole->users()->count());\n        $this->assertDatabaseHas('users', [\n            'email' => 'admin@example.com',\n            'name' => 'Admin',\n        ]);\n    }\n\n    public function test_initial_option_updating_existing_user_with_generate_password_only_outputs_password()\n    {\n        $defaultAdmin = User::query()->where('email', '=', 'admin@admin.com')->first();\n\n        $this->withoutMockingConsoleOutput()\n            ->artisan('bookstack:create-admin', [\n            '--email' => 'firstadmin@example.com',\n            '--name' => 'Admin Test',\n            '--generate-password' => true,\n            '--initial' => true,\n        ]);\n\n        $output = Artisan::output();\n        $this->assertMatchesRegularExpression('/^[a-zA-Z0-9]{32}$/', $output);\n\n        $defaultAdmin->refresh();\n        $this->assertEquals('firstadmin@example.com', $defaultAdmin->email);\n    }\n}\n"
  },
  {
    "path": "tests/Commands/DeleteUsersCommandTest.php",
    "content": "<?php\n\nnamespace Tests\\Commands;\n\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Tests\\TestCase;\n\nclass DeleteUsersCommandTest extends TestCase\n{\n    public function test_command_deletes_users()\n    {\n        $userCount = User::query()->count();\n        $normalUsers = $this->getNormalUsers();\n\n        $normalUserCount = $userCount - count($normalUsers);\n        $this->artisan('bookstack:delete-users')\n            ->expectsConfirmation('Are you sure you want to continue?', 'yes')\n            ->expectsOutputToContain(\"Deleted $normalUserCount of $userCount total users.\")\n            ->assertExitCode(0);\n\n        $this->assertDatabaseMissing('users', ['id' => $normalUsers->first()->id]);\n    }\n\n    public function test_command_requires_confirmation()\n    {\n        $normalUsers = $this->getNormalUsers();\n\n        $this->artisan('bookstack:delete-users')\n            ->expectsConfirmation('Are you sure you want to continue?', 'no')\n            ->assertExitCode(0);\n\n        $this->assertDatabaseHas('users', ['id' => $normalUsers->first()->id]);\n    }\n\n    protected function getNormalUsers(): Collection\n    {\n        return User::query()->whereNull('system_name')\n            ->get()\n            ->filter(function (User $user) {\n                return !$user->hasSystemRole('admin');\n            });\n    }\n}\n"
  },
  {
    "path": "tests/Commands/InstallModuleCommandTest.php",
    "content": "<?php\n\nnamespace Tests\\Commands;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse Illuminate\\Support\\Facades\\File;\nuse Tests\\TestCase;\nuse ZipArchive;\n\nclass InstallModuleCommandTest extends TestCase\n{\n    public function test_local_module_install_with_active_theme()\n    {\n        $this->usingThemeFolder(function () {\n            $zip = $this->getModuleZipPath();\n            $expectedInstallPath = theme_path('modules/test-module');\n            $this->artisan('bookstack:install-module', ['location' => $zip])\n                ->expectsOutput(\"\\nThis will install a module from: {$zip}\\n\\nModules can contain code which would have the ability to do anything on the BookStack host server.\\nYou should only install modules from trusted sources.\")\n                ->expectsConfirmation('Are you sure you want to install this module?', 'yes')\n                ->expectsOutput('Module \"Test Module\" (v1.0.0) successfully installed!')\n                ->expectsOutput(\"Install location: {$expectedInstallPath}\")\n                ->assertExitCode(0);\n\n            $this->assertDirectoryExists($expectedInstallPath);\n            $this->assertFileExists($expectedInstallPath . '/bookstack-module.json');\n        });\n    }\n\n    public function test_remote_module_install_with_active_theme()\n    {\n        $this->usingThemeFolder(function () {\n            $zip = $this->getModuleZipPath();\n\n            $http = $this->mockHttpClient([\n                new Response(200, ['Content-Length' => filesize($zip)], file_get_contents($zip))\n            ]);\n            $expectedInstallPath = theme_path('modules/test-module');\n\n            $this->artisan('bookstack:install-module', ['location' => 'https://example.com/test-module.zip'])\n                ->expectsOutput(\"\\nThis will download a module from: example.com\\n\\nModules can contain code which would have the ability to do anything on the BookStack host server.\\nYou should only install modules from trusted sources.\")\n                ->expectsConfirmation('Are you sure you trust this source?', 'yes')\n                ->expectsOutput('Module \"Test Module\" (v1.0.0) successfully installed!')\n                ->expectsOutput(\"Install location: {$expectedInstallPath}\")\n                ->assertExitCode(0);\n\n            $this->assertEquals(1, $http->requestCount());\n            $request = $http->requestAt(0);\n            $this->assertEquals('/test-module.zip', $request->getUri()->getPath());\n\n            $this->assertDirectoryExists($expectedInstallPath);\n            $this->assertFileExists($expectedInstallPath . '/bookstack-module.json');\n        });\n    }\n\n    public function test_remote_http_module_warns_and_prompts_users()\n    {\n        $this->usingThemeFolder(function () {\n            $zip = $this->getModuleZipPath();\n\n            $http = $this->mockHttpClient([\n                new Response(200, ['Content-Length' => filesize($zip)], file_get_contents($zip))\n            ]);\n            $expectedInstallPath = theme_path('modules/test-module');\n\n            $this->artisan('bookstack:install-module', ['location' => 'http://example.com/test-module.zip'])\n                ->expectsOutput(\"\\nThis will download a module from: example.com\\n\\nModules can contain code which would have the ability to do anything on the BookStack host server.\\nYou should only install modules from trusted sources.\")\n                ->expectsConfirmation('Are you sure you trust this source?', 'yes')\n                ->expectsOutput(\"You are downloading a module from an insecure HTTP source.\\nWe recommend only using HTTPS sources to avoid various security risks.\")\n                ->expectsConfirmation('Are you sure you want to continue without HTTPS?', 'yes')\n                ->expectsOutput('Module \"Test Module\" (v1.0.0) successfully installed!')\n                ->expectsOutput(\"Install location: {$expectedInstallPath}\")\n                ->assertExitCode(0);\n\n            $request = $http->requestAt(0);\n            $this->assertEquals('/test-module.zip', $request->getUri()->getPath());\n        });\n    }\n\n    public function test_remote_module_install_follows_redirects()\n    {\n        $this->usingThemeFolder(function () {\n            $zip = $this->getModuleZipPath();\n\n            $http = $this->mockHttpClient([\n                new Response(302, ['Location' => 'https://example.com/a-test-module.zip']),\n                new Response(200, ['Content-Length' => filesize($zip)], file_get_contents($zip))\n            ]);\n\n            $this->artisan('bookstack:install-module', ['location' => 'https://example.com/test-module.zip'])\n                ->expectsConfirmation('Are you sure you trust this source?', 'yes')\n                ->assertExitCode(0);\n\n            $this->assertEquals(2, $http->requestCount());\n            $this->assertEquals('/test-module.zip', $http->requestAt(0)->getUri()->getPath());\n            $this->assertEquals('/a-test-module.zip', $http->requestAt(1)->getUri()->getPath());\n        });\n    }\n\n    public function test_remote_module_install_does_not_follow_redirects_to_different_origin()\n    {\n        $this->usingThemeFolder(function () {\n            $zip = $this->getModuleZipPath();\n\n            $http = $this->mockHttpClient([\n                new Response(302, ['Location' => 'http://example.com/a-test-module.zip']),\n                new Response(200, ['Content-Length' => filesize($zip)], file_get_contents($zip))\n            ]);\n\n            $this->artisan('bookstack:install-module', ['location' => 'https://example.com/test-module.zip'])\n                ->expectsConfirmation('Are you sure you trust this source?', 'yes')\n                ->assertExitCode(1);\n\n            $this->assertEquals(1, $http->requestCount());\n            $this->assertEquals('https', $http->requestAt(0)->getUri()->getScheme());\n        });\n    }\n\n    public function test_remote_module_install_download_failures_are_announced_to_user()\n    {\n        $this->usingThemeFolder(function () {\n            $http = $this->mockHttpClient([\n                new Response(404),\n            ]);\n\n            $this->artisan('bookstack:install-module', ['location' => 'https://example.com/test-module.zip'])\n                ->expectsConfirmation('Are you sure you trust this source?', 'yes')\n                ->expectsOutput('ERROR: Failed to download module from https://example.com/test-module.zip')\n                ->expectsOutput('Download failed with status code 404')\n                ->assertExitCode(1);\n            $this->assertEquals(1, $http->requestCount());\n        });\n    }\n\n    public function test_run_with_invalid_path_exits_early()\n    {\n        $this->artisan('bookstack:install-module', ['location' => '/not-found.zip'])\n            ->expectsOutput('ERROR: Module file not found at /not-found.zip')\n            ->assertExitCode(1);\n    }\n\n    public function test_run_with_invalid_zip_has_early_exit()\n    {\n        $zip = $this->getModuleZipPath();\n        file_put_contents($zip, 'invalid zip');\n\n        $this->artisan('bookstack:install-module', ['location' => $zip])\n            ->expectsConfirmation('Are you sure you want to install this module?', 'yes')\n            ->expectsOutput(\"ERROR: Cannot open ZIP file at {$zip}\")\n            ->assertExitCode(1);\n    }\n\n    public function test_run_with_large_zip_has_early_exit()\n    {\n        $zip = $this->getModuleZipPath(null, [\n            'large-file.txt' => str_repeat('a', 1024 * 1024 * 51)\n        ]);\n\n        $this->artisan('bookstack:install-module', ['location' => $zip])\n            ->expectsConfirmation('Are you sure you want to install this module?', 'yes')\n            ->expectsOutput(\"ERROR: Module ZIP file contents are too large. Maximum size is 50MB\")\n            ->assertExitCode(1);\n    }\n\n    public function test_run_with_invalid_module_data_has_early_exit()\n    {\n        $zip = $this->getModuleZipPath([\n            'name' => 'Invalid Module',\n            'description' => 'A module with invalid data',\n            'version' => 'dog',\n        ]);\n\n        $this->artisan('bookstack:install-module', ['location' => $zip])\n            ->expectsConfirmation('Are you sure you want to install this module?', 'yes')\n            ->expectsOutput(\"ERROR: Failed to read module metadata with error: Module in folder \\\"_temp\\\" has an invalid 'version' format. Expected semantic version format like '1.0.0' or 'v1.0.0'\")\n            ->assertExitCode(1);\n    }\n\n    public function test_local_module_install_without_active_theme_can_setup_theme_folder()\n    {\n        $zip = $this->getModuleZipPath();\n        $expectedThemePath = base_path('themes/custom');\n        File::deleteDirectory($expectedThemePath);\n\n        $this->artisan('bookstack:install-module', ['location' => $zip])\n            ->expectsConfirmation('Are you sure you want to install this module?', 'yes')\n            ->expectsConfirmation('No active theme folder found, would you like to create one?', 'yes')\n            ->expectsOutput(\"Created theme folder at {$expectedThemePath}\")\n            ->expectsOutput(\"You will need to set APP_THEME=custom in your BookStack env configuration to enable this theme!\")\n            ->expectsOutput('Module \"Test Module\" (v1.0.0) successfully installed!')\n            ->assertExitCode(0);\n\n        $this->assertDirectoryExists($expectedThemePath . '/modules/test-module');\n\n        File::deleteDirectory($expectedThemePath);\n    }\n\n    public function test_local_module_install_with_active_theme_and_conflicting_modules_file_causes_early_exit()\n    {\n        $this->usingThemeFolder(function () {\n            $zip = $this->getModuleZipPath();\n            File::put(theme_path('modules'), '{}');\n\n            $this->artisan('bookstack:install-module', ['location' => $zip])\n                ->expectsConfirmation('Are you sure you want to install this module?', 'yes')\n                ->expectsOutput(\"ERROR: Cannot create a modules folder, file already exists at \" . theme_path('modules'))\n                ->assertExitCode(1);\n        });\n    }\n\n    public function test_single_existing_module_with_same_name_replace()\n    {\n        $this->usingThemeFolder(function () {\n            $original = $this->createModuleFolderInCurrentTheme(['name' => 'Test Module', 'description' => 'cat', 'version' => '1.0.0']);\n            $new = $this->getModuleZipPath(['name' => 'Test Module', 'description' => '', 'version' => '2.0.0']);\n\n            $this->artisan('bookstack:install-module', ['location' => $new])\n                ->expectsConfirmation('Are you sure you want to install this module?', 'yes')\n                ->expectsOutput('The following modules already exist with the same name:')\n                ->expectsOutput('Test Module (test-module:v1.0.0) - cat')\n                ->expectsChoice('What would you like to do?', 'Replace existing module', ['Cancel module install', 'Add alongside existing module', 'Replace existing module'])\n                ->expectsOutput(\"Replacing existing module in test-module folder\")\n                ->assertExitCode(0);\n\n            $this->assertFileExists($original . '/bookstack-module.json');\n            $metadata = json_decode(file_get_contents($original . '/bookstack-module.json'), true);\n            $this->assertEquals('2.0.0', $metadata['version']);\n        });\n    }\n\n    public function test_single_existing_module_with_same_name_cancel()\n    {\n        $this->usingThemeFolder(function () {\n            $original = $this->createModuleFolderInCurrentTheme(['name' => 'Test Module', 'description' => 'cat', 'version' => '1.0.0']);\n            $new = $this->getModuleZipPath(['name' => 'Test Module', 'description' => '', 'version' => '2.0.0']);\n\n            $this->artisan('bookstack:install-module', ['location' => $new])\n                ->expectsConfirmation('Are you sure you want to install this module?', 'yes')\n                ->expectsOutput('The following modules already exist with the same name:')\n                ->expectsOutput('Test Module (test-module:v1.0.0) - cat')\n                ->expectsChoice('What would you like to do?', 'Cancel module install', ['Cancel module install', 'Add alongside existing module', 'Replace existing module'])\n                ->assertExitCode(1);\n\n            $this->assertFileExists($original . '/bookstack-module.json');\n            $metadata = json_decode(file_get_contents($original . '/bookstack-module.json'), true);\n            $this->assertEquals('1.0.0', $metadata['version']);\n        });\n    }\n\n    public function test_single_existing_module_with_same_name_add()\n    {\n        $this->usingThemeFolder(function () {\n            $original = $this->createModuleFolderInCurrentTheme(['name' => 'Test Module', 'description' => 'cat', 'version' => '1.0.0']);\n            $new = $this->getModuleZipPath(['name' => 'Test Module', 'description' => '', 'version' => '2.0.0']);\n\n            $this->artisan('bookstack:install-module', ['location' => $new])\n                ->expectsConfirmation('Are you sure you want to install this module?', 'yes')\n                ->expectsOutput('The following modules already exist with the same name:')\n                ->expectsOutput('Test Module (test-module:v1.0.0) - cat')\n                ->expectsChoice('What would you like to do?', 'Add alongside existing module', ['Cancel module install', 'Add alongside existing module', 'Replace existing module'])\n                ->assertExitCode(0);\n\n            $dirs = File::directories(theme_path('modules/'));\n            $this->assertCount(2, $dirs);\n        });\n    }\n\n    protected function createModuleFolderInCurrentTheme(array|null $metadata = null, array $extraFiles = []): string\n    {\n        $original = $this->getModuleZipPath($metadata, $extraFiles);\n        $targetPath = theme_path('modules/test-module');\n        mkdir($targetPath, 0777, true);\n        $originalZip = new ZipArchive();\n        $originalZip->open($original);\n        $originalZip->extractTo($targetPath);\n        $originalZip->close();\n\n        return $targetPath;\n    }\n\n    protected function getModuleZipPath(array|null $metadata = null, array $extraFiles = []): string\n    {\n        $zip = new ZipArchive();\n        $tmpFile = tempnam(sys_get_temp_dir(), 'bs-test-module');\n        $zip->open($tmpFile, ZipArchive::CREATE);\n\n        $zip->addFromString('bookstack-module.json', json_encode($metadata ?? [\n            'name' => 'Test Module',\n            'description' => 'A test module for BookStack',\n            'version' => '1.0.0',\n        ]));\n\n        foreach ($extraFiles as $path => $contents) {\n            $zip->addFromString($path, $contents);\n        }\n\n        $zip->close();\n        return $tmpFile;\n    }\n}\n"
  },
  {
    "path": "tests/Commands/RefreshAvatarCommandTest.php",
    "content": "<?php\n\nnamespace Tests\\Commands;\n\nuse BookStack\\Uploads\\Image;\nuse BookStack\\Users\\Models\\User;\nuse GuzzleHttp\\Psr7\\Response;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Tests\\TestCase;\n\nclass RefreshAvatarCommandTest extends TestCase\n{\n    public function setUp(): void\n    {\n        parent::setUp();\n\n        config()->set([\n            'services.disable_services' => false,\n            'services.avatar_url' => 'https://avatars.example.com?a=b',\n        ]);\n    }\n\n    public function test_command_errors_if_avatar_fetch_disabled()\n    {\n        config()->set(['services.avatar_url' => false]);\n\n        $this->artisan('bookstack:refresh-avatar')\n            ->expectsOutputToContain(\"Avatar fetching is disabled on this instance\")\n            ->assertExitCode(1);\n    }\n\n    public function test_command_requires_email_or_id_option()\n    {\n        $this->artisan('bookstack:refresh-avatar')\n            ->expectsOutputToContain(\"Either a --id=<number> or --email=<email> option must be provided\")\n            ->assertExitCode(1);\n    }\n\n    public function test_command_runs_with_provided_email()\n    {\n        $requests = $this->mockHttpClient([new Response(200, ['Content-Type' => 'image/png'], $this->files->pngImageData())]);\n\n        $user = $this->users->viewer();\n        $this->assertFalse($user->avatar()->exists());\n\n        $this->artisan(\"bookstack:refresh-avatar --email={$user->email} -f\")\n            ->expectsQuestion('Are you sure you want to proceed?', true)\n            ->expectsOutput(\"[ID: {$user->id}] {$user->email} - Updated\")\n            ->expectsOutputToContain('This will destroy any existing avatar images these users have, and attempt to fetch new avatar images from avatars.example.com')\n            ->assertExitCode(0);\n\n        $this->assertEquals('https://avatars.example.com?a=b', $requests->latestRequest()->getUri());\n\n        $user->refresh();\n        $this->assertTrue($user->avatar()->exists());\n    }\n\n    public function test_command_runs_with_provided_id()\n    {\n        $requests = $this->mockHttpClient([new Response(200, ['Content-Type' => 'image/png'], $this->files->pngImageData())]);\n\n        $user = $this->users->viewer();\n        $this->assertFalse($user->avatar()->exists());\n\n        $this->artisan(\"bookstack:refresh-avatar --id={$user->id} -f\")\n            ->expectsQuestion('Are you sure you want to proceed?', true)\n            ->expectsOutput(\"[ID: {$user->id}] {$user->email} - Updated\")\n            ->assertExitCode(0);\n\n        $this->assertEquals('https://avatars.example.com?a=b', $requests->latestRequest()->getUri());\n\n        $user->refresh();\n        $this->assertTrue($user->avatar()->exists());\n    }\n\n    public function test_command_runs_with_provided_id_error_upstream()\n    {\n        $requests = $this->mockHttpClient([new Response(404)]);\n\n        $user = $this->users->viewer();\n        $this->assertFalse($user->avatar()->exists());\n\n        $this->artisan(\"bookstack:refresh-avatar --id={$user->id} -f\")\n            ->expectsQuestion('Are you sure you want to proceed?', true)\n            ->expectsOutput(\"[ID: {$user->id}] {$user->email} - Not updated\")\n            ->assertExitCode(1);\n\n        $this->assertEquals(1, $requests->requestCount());\n        $this->assertFalse($user->avatar()->exists());\n    }\n\n    public function test_saying_no_to_confirmation_does_not_refresh_avatar()\n    {\n        $user = $this->users->viewer();\n\n        $this->assertFalse($user->avatar()->exists());\n        $this->artisan(\"bookstack:refresh-avatar --id={$user->id} -f\")\n            ->expectsQuestion('Are you sure you want to proceed?', false)\n            ->assertExitCode(0);\n        $this->assertFalse($user->avatar()->exists());\n    }\n\n    public function test_giving_non_existing_user_shows_error_message()\n    {\n        $this->artisan('bookstack:refresh-avatar --email=donkeys@example.com')\n            ->expectsOutput('A user where email=donkeys@example.com could not be found.')\n            ->assertExitCode(1);\n    }\n\n    public function test_command_runs_all_users_without_avatars_dry_run()\n    {\n        $users = User::query()->where('image_id', '=', 0)->get();\n\n        $this->artisan('bookstack:refresh-avatar --users-without-avatars')\n            ->expectsOutput(count($users) . ' user(s) found to update avatars for.')\n            ->expectsOutput(\"[ID: {$users[0]->id}] {$users[0]->email} - Not updated\")\n            ->expectsOutput('Dry run, no avatars were updated.')\n            ->assertExitCode(0);\n    }\n\n    public function test_command_runs_all_users_without_avatars_with_none_to_update()\n    {\n        $requests = $this->mockHttpClient();\n        $image = Image::factory()->create();\n        User::query()->update(['image_id' => $image->id]);\n\n        $this->artisan('bookstack:refresh-avatar --users-without-avatars -f')\n            ->expectsOutput('0 user(s) found to update avatars for.')\n            ->assertExitCode(0);\n\n        $this->assertEquals(0, $requests->requestCount());\n    }\n\n    public function test_command_runs_all_users_without_avatars()\n    {\n        /** @var Collection|User[] $users */\n        $users = User::query()->where('image_id', '=', 0)->get();\n\n        $pendingCommand = $this->artisan('bookstack:refresh-avatar --users-without-avatars -f');\n        $pendingCommand\n            ->expectsOutput($users->count() . ' user(s) found to update avatars for.')\n            ->expectsQuestion('Are you sure you want to proceed?', true);\n\n        $responses = [];\n        foreach ($users as $user) {\n            $pendingCommand->expectsOutput(\"[ID: {$user->id}] {$user->email} - Updated\");\n            $responses[] = new Response(200, ['Content-Type' => 'image/png'], $this->files->pngImageData());\n        }\n        $requests = $this->mockHttpClient($responses);\n\n        $pendingCommand->assertExitCode(0);\n        $pendingCommand->run();\n\n        $this->assertEquals(0, User::query()->where('image_id', '=', 0)->count());\n        $this->assertEquals($users->count(), $requests->requestCount());\n    }\n\n    public function test_saying_no_to_confirmation_all_users_without_avatars()\n    {\n        $requests = $this->mockHttpClient();\n\n        $this->artisan('bookstack:refresh-avatar --users-without-avatars -f')\n            ->expectsQuestion('Are you sure you want to proceed?', false)\n            ->assertExitCode(0);\n\n        $this->assertEquals(0, $requests->requestCount());\n    }\n\n    public function test_command_runs_all_users_dry_run()\n    {\n        $users = User::query()->where('image_id', '=', 0)->get();\n\n        $this->artisan('bookstack:refresh-avatar --all')\n            ->expectsOutput(count($users) . ' user(s) found to update avatars for.')\n            ->expectsOutput(\"[ID: {$users[0]->id}] {$users[0]->email} - Not updated\")\n            ->expectsOutput('Dry run, no avatars were updated.')\n            ->assertExitCode(0);\n    }\n\n    public function test_command_runs_update_all_users_avatar()\n    {\n        /** @var Collection|User[] $users */\n        $users = User::query()->get();\n\n        $pendingCommand = $this->artisan('bookstack:refresh-avatar --all -f');\n        $pendingCommand\n            ->expectsOutput($users->count() . ' user(s) found to update avatars for.')\n            ->expectsQuestion('Are you sure you want to proceed?', true);\n\n        $responses = [];\n        foreach ($users as $user) {\n            $pendingCommand->expectsOutput(\"[ID: {$user->id}] {$user->email} - Updated\");\n            $responses[] = new Response(200, ['Content-Type' => 'image/png'], $this->files->pngImageData());\n        }\n        $requests = $this->mockHttpClient($responses);\n\n        $pendingCommand->assertExitCode(0);\n        $pendingCommand->run();\n\n        $this->assertEquals(0, User::query()->where('image_id', '=', 0)->count());\n        $this->assertEquals($users->count(), $requests->requestCount());\n    }\n\n    public function test_command_runs_update_all_users_avatar_errors()\n    {\n        /** @var Collection|User[] $users */\n        $users = array_values(User::query()->get()->all());\n\n        $pendingCommand = $this->artisan('bookstack:refresh-avatar --all -f');\n        $pendingCommand\n            ->expectsOutput(count($users) . ' user(s) found to update avatars for.')\n            ->expectsQuestion('Are you sure you want to proceed?', true);\n\n        $responses = [];\n        foreach ($users as $index => $user) {\n            if ($index === 0) {\n                $pendingCommand->expectsOutput(\"[ID: {$user->id}] {$user->email} - Not updated\");\n                $responses[] = new Response(404);\n                continue;\n            }\n\n            $pendingCommand->expectsOutput(\"[ID: {$user->id}] {$user->email} - Updated\");\n            $responses[] = new Response(200, ['Content-Type' => 'image/png'], $this->files->pngImageData());\n        }\n\n        $requests = $this->mockHttpClient($responses);\n\n        $pendingCommand->assertExitCode(1);\n        $pendingCommand->run();\n\n        $userWithAvatars = User::query()->where('image_id', '!=', 0)->count();\n        $this->assertEquals(count($users) - 1, $userWithAvatars);\n        $this->assertEquals(count($users), $requests->requestCount());\n    }\n\n    public function test_saying_no_to_confirmation_update_all_users_avatar()\n    {\n        $requests = $this->mockHttpClient([new Response(200, ['Content-Type' => 'image/png'], $this->files->pngImageData())]);\n\n        $this->artisan('bookstack:refresh-avatar --all -f')\n            ->expectsQuestion('Are you sure you want to proceed?', false)\n            ->assertExitCode(0);\n\n        $this->assertEquals(0, $requests->requestCount());\n    }\n}\n"
  },
  {
    "path": "tests/Commands/RegeneratePermissionsCommandTest.php",
    "content": "<?php\n\nnamespace Tests\\Commands;\n\nuse BookStack\\Auth\\Permissions\\CollapsedPermission;\nuse BookStack\\Permissions\\Models\\JointPermission;\nuse Illuminate\\Support\\Facades\\Artisan;\nuse Illuminate\\Support\\Facades\\DB;\nuse Tests\\TestCase;\n\nclass RegeneratePermissionsCommandTest extends TestCase\n{\n    public function test_regen_permissions_command()\n    {\n        DB::rollBack();\n        $page = $this->entities->page();\n        $editor = $this->users->editor();\n        $role = $editor->roles()->first();\n        $this->permissions->addEntityPermission($page, ['view'], $role);\n        JointPermission::query()->truncate();\n\n        $this->assertDatabaseMissing('joint_permissions', ['entity_id' => $page->id]);\n\n        $exitCode = Artisan::call('bookstack:regenerate-permissions');\n        $this->assertTrue($exitCode === 0, 'Command executed successfully');\n\n        $this->assertDatabaseHas('joint_permissions', [\n            'entity_id' => $page->id,\n            'entity_type' => 'page',\n            'role_id' => $role->id,\n            'status' => 3, // Explicit allow\n        ]);\n\n        $page->permissions()->delete();\n        $page->rebuildPermissions();\n\n        DB::beginTransaction();\n    }\n}\n"
  },
  {
    "path": "tests/Commands/RegenerateReferencesCommandTest.php",
    "content": "<?php\n\nnamespace Tests\\Commands;\n\nuse Illuminate\\Support\\Facades\\DB;\nuse Tests\\TestCase;\n\nclass RegenerateReferencesCommandTest extends TestCase\n{\n    public function test_regenerate_references_command()\n    {\n        $page = $this->entities->page();\n        $book = $page->book;\n\n        $page->html = '<a href=\"' . $book->getUrl() . '\">Book Link</a>';\n        $page->save();\n\n        DB::table('references')->delete();\n\n        $this->artisan('bookstack:regenerate-references')\n            ->assertExitCode(0);\n\n        $this->assertDatabaseHas('references', [\n            'from_id'   => $page->id,\n            'from_type' => $page->getMorphClass(),\n            'to_id'     => $book->id,\n            'to_type'   => $book->getMorphClass(),\n        ]);\n    }\n}\n"
  },
  {
    "path": "tests/Commands/RegenerateSearchCommandTest.php",
    "content": "<?php\n\nnamespace Tests\\Commands;\n\nuse BookStack\\Search\\SearchTerm;\nuse Illuminate\\Support\\Facades\\DB;\nuse Tests\\TestCase;\n\nclass RegenerateSearchCommandTest extends TestCase\n{\n    public function test_command_regenerates_index()\n    {\n        DB::rollBack();\n        $page = $this->entities->page();\n        SearchTerm::truncate();\n\n        $this->assertDatabaseMissing('search_terms', ['entity_id' => $page->id]);\n\n        $this->artisan('bookstack:regenerate-search')\n            ->expectsOutput('Search index regenerated!')\n            ->assertExitCode(0);\n\n        $this->assertDatabaseHas('search_terms', [\n            'entity_type' => 'page',\n            'entity_id' => $page->id\n        ]);\n        DB::beginTransaction();\n    }\n}\n"
  },
  {
    "path": "tests/Commands/ResetMfaCommandTest.php",
    "content": "<?php\n\nnamespace Tests\\Commands;\n\nuse BookStack\\Access\\Mfa\\MfaValue;\nuse BookStack\\Users\\Models\\User;\nuse Tests\\TestCase;\n\nclass ResetMfaCommandTest extends TestCase\n{\n    public function test_command_requires_email_or_id_option()\n    {\n        $this->artisan('bookstack:reset-mfa')\n            ->expectsOutputToContain('Either a --id=<number> or --email=<email> option must be provided.')\n            ->assertExitCode(1);\n    }\n\n    public function test_command_runs_with_provided_email()\n    {\n        /** @var User $user */\n        $user = User::query()->first();\n        MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, 'test');\n\n        $this->assertEquals(1, $user->mfaValues()->count());\n        $this->artisan(\"bookstack:reset-mfa --email={$user->email}\")\n            ->expectsQuestion('Are you sure you want to proceed?', true)\n            ->expectsOutput('User MFA methods have been reset.')\n            ->assertExitCode(0);\n        $this->assertEquals(0, $user->mfaValues()->count());\n    }\n\n    public function test_command_runs_with_provided_id()\n    {\n        /** @var User $user */\n        $user = User::query()->first();\n        MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, 'test');\n\n        $this->assertEquals(1, $user->mfaValues()->count());\n        $this->artisan(\"bookstack:reset-mfa --id={$user->id}\")\n            ->expectsQuestion('Are you sure you want to proceed?', true)\n            ->expectsOutput('User MFA methods have been reset.')\n            ->assertExitCode(0);\n        $this->assertEquals(0, $user->mfaValues()->count());\n    }\n\n    public function test_saying_no_to_confirmation_does_not_reset_mfa()\n    {\n        /** @var User $user */\n        $user = User::query()->first();\n        MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, 'test');\n\n        $this->assertEquals(1, $user->mfaValues()->count());\n        $this->artisan(\"bookstack:reset-mfa --id={$user->id}\")\n            ->expectsQuestion('Are you sure you want to proceed?', false)\n            ->assertExitCode(1);\n        $this->assertEquals(1, $user->mfaValues()->count());\n    }\n\n    public function test_giving_non_existing_user_shows_error_message()\n    {\n        $this->artisan('bookstack:reset-mfa --email=donkeys@example.com')\n            ->expectsOutput('A user where email=donkeys@example.com could not be found.')\n            ->assertExitCode(1);\n    }\n}\n"
  },
  {
    "path": "tests/Commands/UpdateUrlCommandTest.php",
    "content": "<?php\n\nnamespace Tests\\Commands;\n\nuse BookStack\\Entities\\Models\\Entity;\nuse Illuminate\\Support\\Facades\\Artisan;\nuse Symfony\\Component\\Console\\Exception\\RuntimeException;\nuse Tests\\TestCase;\n\nclass UpdateUrlCommandTest extends TestCase\n{\n    public function test_command_updates_page_content()\n    {\n        $page = $this->entities->page();\n        $page->html = '<a href=\"https://example.com/donkeys\"></a>';\n        $page->save();\n\n        $this->artisan('bookstack:update-url https://example.com https://cats.example.com')\n            ->expectsQuestion(\"This will search for \\\"https://example.com\\\" in your database and replace it with  \\\"https://cats.example.com\\\".\\nAre you sure you want to proceed?\", 'y')\n            ->expectsQuestion('This operation could cause issues if used incorrectly. Have you made a backup of your existing database?', 'y');\n\n        $this->assertDatabaseHasEntityData('page', [\n            'id'   => $page->id,\n            'html' => '<a href=\"https://cats.example.com/donkeys\"></a>',\n        ]);\n    }\n\n    public function test_command_updates_description_html()\n    {\n        /** @var Entity[] $models */\n        $models = [$this->entities->book(), $this->entities->chapter(), $this->entities->shelf()];\n\n        foreach ($models as $model) {\n            $model->description_html = '<a href=\"https://example.com/donkeys\"></a>';\n            $model->save();\n        }\n\n        $this->artisan('bookstack:update-url https://example.com https://cats.example.com')\n            ->expectsQuestion(\"This will search for \\\"https://example.com\\\" in your database and replace it with  \\\"https://cats.example.com\\\".\\nAre you sure you want to proceed?\", 'y')\n            ->expectsQuestion('This operation could cause issues if used incorrectly. Have you made a backup of your existing database?', 'y');\n\n        foreach ($models as $model) {\n            $this->assertDatabaseHasEntityData($model->getMorphClass(), [\n                'id'               => $model->id,\n                'description_html' => '<a href=\"https://cats.example.com/donkeys\"></a>',\n            ]);\n        }\n    }\n\n    public function test_command_requires_valid_url()\n    {\n        $badUrlMessage = 'The given urls are expected to be full urls starting with http:// or https://';\n        $this->artisan('bookstack:update-url //example.com https://cats.example.com')->expectsOutput($badUrlMessage);\n        $this->artisan('bookstack:update-url https://example.com htts://cats.example.com')->expectsOutput($badUrlMessage);\n        $this->artisan('bookstack:update-url example.com https://cats.example.com')->expectsOutput($badUrlMessage);\n\n        $this->expectException(RuntimeException::class);\n        $this->artisan('bookstack:update-url https://cats.example.com');\n    }\n\n    public function test_command_force_option_skips_prompt()\n    {\n        $this->artisan('bookstack:update-url --force https://cats.example.com/donkey https://cats.example.com/monkey')\n            ->expectsOutputToContain('URL update procedure complete')\n            ->assertSuccessful();\n    }\n\n    public function test_command_updates_settings()\n    {\n        setting()->put('my-custom-item', 'https://example.com/donkey/cat');\n        $this->runUpdate('https://example.com', 'https://cats.example.com');\n\n        setting()->flushCache();\n\n        $settingVal = setting('my-custom-item');\n        $this->assertEquals('https://cats.example.com/donkey/cat', $settingVal);\n    }\n\n    public function test_command_updates_array_settings()\n    {\n        setting()->put('my-custom-array-item', [['name' => 'a https://example.com/donkey/cat url']]);\n        $this->runUpdate('https://example.com', 'https://cats.example.com');\n\n        setting()->flushCache();\n\n        $settingVal = setting('my-custom-array-item');\n        $this->assertEquals('a https://cats.example.com/donkey/cat url', $settingVal[0]['name']);\n    }\n\n    public function test_command_updates_page_revisions()\n    {\n        $page = $this->entities->page();\n\n        for ($i = 0; $i < 2; $i++) {\n            $this->entities->updatePage($page, [\n                'name' => $page->name,\n                'markdown' => \"[A link {$i}](https://example.com/donkey/cat)\"\n            ]);\n        }\n\n        $this->runUpdate('https://example.com', 'https://cats.example.com');\n        setting()->flushCache();\n\n        $this->assertDatabaseHas('page_revisions', [\n            'page_id' => $page->id,\n            'markdown' => '[A link 1](https://cats.example.com/donkey/cat)',\n            'html' => '<p id=\"bkmrk-a-link-1\"><a href=\"https://cats.example.com/donkey/cat\">A link 1</a></p>' . \"\\n\"\n        ]);\n    }\n\n    protected function runUpdate(string $oldUrl, string $newUrl)\n    {\n        $this->artisan(\"bookstack:update-url {$oldUrl} {$newUrl}\")\n            ->expectsQuestion(\"This will search for \\\"{$oldUrl}\\\" in your database and replace it with  \\\"{$newUrl}\\\".\\nAre you sure you want to proceed?\", 'y')\n            ->expectsQuestion('This operation could cause issues if used incorrectly. Have you made a backup of your existing database?', 'y');\n    }\n}\n"
  },
  {
    "path": "tests/Commands/UpgradeDatabaseEncodingCommandTest.php",
    "content": "<?php\n\nnamespace Tests\\Commands;\n\nuse Tests\\TestCase;\n\nclass UpgradeDatabaseEncodingCommandTest extends TestCase\n{\n    public function test_command_outputs_sql()\n    {\n        $this->artisan('bookstack:db-utf8mb4')\n            ->expectsOutputToContain('ALTER DATABASE')\n            ->expectsOutputToContain('ALTER TABLE `users` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');\n    }\n}\n"
  },
  {
    "path": "tests/CreatesApplication.php",
    "content": "<?php\n\nnamespace Tests;\n\nuse Illuminate\\Contracts\\Console\\Kernel;\nuse Illuminate\\Foundation\\Application;\n\ntrait CreatesApplication\n{\n    /**\n     * Creates the application.\n     */\n    public function createApplication(): Application\n    {\n        $app = require __DIR__ . '/../bootstrap/app.php';\n        $app->make(Kernel::class)->bootstrap();\n\n        return $app;\n    }\n}\n"
  },
  {
    "path": "tests/DebugViewTest.php",
    "content": "<?php\n\nnamespace Tests;\n\nuse BookStack\\Access\\SocialDriverManager;\nuse Illuminate\\Testing\\TestResponse;\n\nclass DebugViewTest extends TestCase\n{\n    public function test_debug_view_shows_expected_details()\n    {\n        config()->set('app.debug', true);\n        $resp = $this->getDebugViewForException(new \\InvalidArgumentException('An error occurred during testing'));\n\n        // Error message\n        $resp->assertSeeText('An error occurred during testing');\n        // Exception Class\n        $resp->assertSeeText('InvalidArgumentException');\n        // Stack trace\n        $resp->assertSeeText('#0');\n        $resp->assertSeeText('#1');\n        // Warning message\n        $resp->assertSeeText('WARNING: Application is in debug mode. This mode has the potential to leak');\n        // PHP version\n        $resp->assertSeeText('PHP Version: ' . phpversion());\n        // BookStack version\n        $resp->assertSeeText('BookStack Version: ' . trim(file_get_contents(base_path('version'))));\n        // Dynamic help links\n        $this->withHtml($resp)->assertElementExists('a[href*=\"q=' . urlencode('BookStack An error occurred during testing') . '\"]');\n        $this->withHtml($resp)->assertElementExists('a[href*=\"?q=is%3Aissue+' . urlencode('An error occurred during testing') . '\"]');\n    }\n\n    public function test_debug_view_only_shows_when_debug_mode_is_enabled()\n    {\n        config()->set('app.debug', true);\n        $resp = $this->getDebugViewForException(new \\InvalidArgumentException('An error occurred during testing'));\n        $resp->assertSeeText('Stack Trace');\n        $resp->assertDontSeeText('An unknown error occurred');\n\n        config()->set('app.debug', false);\n        $resp = $this->getDebugViewForException(new \\InvalidArgumentException('An error occurred during testing'));\n        $resp->assertDontSeeText('Stack Trace');\n        $resp->assertSeeText('An unknown error occurred');\n    }\n\n    protected function getDebugViewForException(\\Exception $exception): TestResponse\n    {\n        // Fake an error via social auth service used on login page\n        $mockService = $this->mock(SocialDriverManager::class);\n        $mockService->shouldReceive('getActive')->andThrow($exception);\n\n        return $this->get('/login');\n    }\n}\n"
  },
  {
    "path": "tests/Entity/BookShelfTest.php",
    "content": "<?php\n\nnamespace Tests\\Entity;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Uploads\\Image;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Support\\Str;\nuse Tests\\TestCase;\n\nclass BookShelfTest extends TestCase\n{\n    public function test_shelves_shows_in_header_if_have_view_permissions()\n    {\n        $viewer = $this->users->viewer();\n        $resp = $this->actingAs($viewer)->get('/');\n        $this->withHtml($resp)->assertElementContains('header', 'Shelves');\n\n        $viewer->roles()->delete();\n        $this->permissions->grantUserRolePermissions($viewer, []);\n        $resp = $this->actingAs($viewer)->get('/');\n        $this->withHtml($resp)->assertElementNotContains('header', 'Shelves');\n\n        $this->permissions->grantUserRolePermissions($viewer, ['bookshelf-view-all']);\n        $resp = $this->actingAs($viewer)->get('/');\n        $this->withHtml($resp)->assertElementContains('header', 'Shelves');\n\n        $viewer->roles()->delete();\n        $this->permissions->grantUserRolePermissions($viewer, ['bookshelf-view-own']);\n        $resp = $this->actingAs($viewer)->get('/');\n        $this->withHtml($resp)->assertElementContains('header', 'Shelves');\n    }\n\n    public function test_shelves_shows_in_header_if_have_any_shelve_view_permission()\n    {\n        $user = User::factory()->create();\n        $this->permissions->grantUserRolePermissions($user, ['image-create-all']);\n        $shelf = $this->entities->shelf();\n        $userRole = $user->roles()->first();\n\n        $resp = $this->actingAs($user)->get('/');\n        $this->withHtml($resp)->assertElementNotContains('header', 'Shelves');\n\n        $this->permissions->setEntityPermissions($shelf, ['view'], [$userRole]);\n\n        $resp = $this->get('/');\n        $this->withHtml($resp)->assertElementContains('header', 'Shelves');\n    }\n\n    public function test_shelves_page_contains_create_link()\n    {\n        $resp = $this->asEditor()->get('/shelves');\n        $this->withHtml($resp)->assertElementContains('a', 'New Shelf');\n    }\n\n    public function test_book_not_visible_in_shelf_list_view_if_user_cant_view_shelf()\n    {\n        config()->set([\n            'setting-defaults.user.bookshelves_view_type' => 'list',\n        ]);\n        $shelf = $this->entities->shelf();\n        $book = $shelf->books()->first();\n\n        $resp = $this->asEditor()->get('/shelves');\n        $resp->assertSee($book->name);\n        $resp->assertSee($book->getUrl());\n\n        $this->permissions->setEntityPermissions($book, []);\n\n        $resp = $this->asEditor()->get('/shelves');\n        $resp->assertDontSee($book->name);\n        $resp->assertDontSee($book->getUrl());\n    }\n\n    public function test_shelves_create()\n    {\n        $booksToInclude = Book::take(2)->get();\n        $shelfInfo = [\n            'name'             => 'My test shelf' . Str::random(4),\n            'description_html' => '<p>Test book description ' . Str::random(10) . '</p>',\n        ];\n        $resp = $this->asEditor()->post('/shelves', array_merge($shelfInfo, [\n            'books' => $booksToInclude->implode('id', ','),\n            'tags'  => [\n                [\n                    'name'  => 'Test Category',\n                    'value' => 'Test Tag Value',\n                ],\n            ],\n        ]));\n        $resp->assertRedirect();\n        $editorId = $this->users->editor()->id;\n        $this->assertDatabaseHasEntityData('bookshelf', array_merge($shelfInfo, ['created_by' => $editorId, 'updated_by' => $editorId]));\n\n        $shelf = Bookshelf::where('name', '=', $shelfInfo['name'])->first();\n        $shelfPage = $this->get($shelf->getUrl());\n        $shelfPage->assertSee($shelfInfo['name']);\n        $shelfPage->assertSee($shelfInfo['description_html'], false);\n        $this->withHtml($shelfPage)->assertElementContains('.tag-item', 'Test Category');\n        $this->withHtml($shelfPage)->assertElementContains('.tag-item', 'Test Tag Value');\n\n        $this->assertDatabaseHas('bookshelves_books', ['bookshelf_id' => $shelf->id, 'book_id' => $booksToInclude[0]->id]);\n        $this->assertDatabaseHas('bookshelves_books', ['bookshelf_id' => $shelf->id, 'book_id' => $booksToInclude[1]->id]);\n    }\n\n    public function test_shelves_create_sets_cover_image()\n    {\n        $shelfInfo = [\n            'name'             => 'My test shelf' . Str::random(4),\n            'description_html' => '<p>Test book description ' . Str::random(10) . '</p>',\n        ];\n\n        $imageFile = $this->files->uploadedImage('shelf-test.png');\n        $resp = $this->asEditor()->call('POST', '/shelves', $shelfInfo, [], ['image' => $imageFile]);\n        $resp->assertRedirect();\n\n        $lastImage = Image::query()->orderByDesc('id')->firstOrFail();\n        $shelf = Bookshelf::query()->where('name', '=', $shelfInfo['name'])->first();\n        $this->assertDatabaseHas('entity_container_data', [\n            'entity_id'       => $shelf->id,\n            'entity_type' => 'bookshelf',\n            'image_id' => $lastImage->id,\n        ]);\n        $this->assertEquals($lastImage->id, $shelf->coverInfo()->getImage()->id);\n        $this->assertEquals('cover_bookshelf', $lastImage->type);\n    }\n\n    public function test_shelf_view()\n    {\n        $shelf = $this->entities->shelf();\n        $resp = $this->asEditor()->get($shelf->getUrl());\n        $resp->assertStatus(200);\n        $resp->assertSeeText($shelf->name);\n        $resp->assertSeeText($shelf->description);\n\n        foreach ($shelf->books as $book) {\n            $resp->assertSee($book->name);\n        }\n    }\n\n    public function test_shelf_view_shows_action_buttons()\n    {\n        $shelf = $this->entities->shelf();\n        $resp = $this->asAdmin()->get($shelf->getUrl());\n        $resp->assertSee($shelf->getUrl('/create-book'));\n        $resp->assertSee($shelf->getUrl('/edit'));\n        $resp->assertSee($shelf->getUrl('/permissions'));\n        $resp->assertSee($shelf->getUrl('/delete'));\n        $this->withHtml($resp)->assertElementContains('a', 'New Book');\n        $this->withHtml($resp)->assertElementContains('a', 'Edit');\n        $this->withHtml($resp)->assertElementContains('a', 'Permissions');\n        $this->withHtml($resp)->assertElementContains('a', 'Delete');\n\n        $resp = $this->asEditor()->get($shelf->getUrl());\n        $resp->assertDontSee($shelf->getUrl('/permissions'));\n    }\n\n    public function test_shelf_view_has_sort_control_that_defaults_to_default()\n    {\n        $shelf = $this->entities->shelf();\n        $resp = $this->asAdmin()->get($shelf->getUrl());\n        $this->withHtml($resp)->assertElementExists('form[action$=\"change-sort/shelf_books\"]');\n        $this->withHtml($resp)->assertElementContains('form[action$=\"change-sort/shelf_books\"] [aria-haspopup=\"true\"]', 'Default');\n    }\n\n    public function test_shelf_view_sort_takes_action()\n    {\n        $shelf = Bookshelf::query()->whereHas('books')->with('books')->first();\n        $books = Book::query()->take(3)->get(['id', 'name']);\n        $books[0]->fill(['name' => 'bsfsdfsdfsd'])->save();\n        $books[1]->fill(['name' => 'adsfsdfsdfsd'])->save();\n        $books[2]->fill(['name' => 'hdgfgdfg'])->save();\n\n        // Set book ordering\n        $this->asAdmin()->put($shelf->getUrl(), [\n            'books' => $books->implode('id', ','),\n            'tags'  => [], 'description_html' => 'abc', 'name' => 'abc',\n        ]);\n        $this->assertEquals(3, $shelf->books()->count());\n        $shelf->refresh();\n\n        $resp = $this->asEditor()->get($shelf->getUrl());\n        $this->withHtml($resp)->assertElementContains('.book-content a.grid-card:nth-child(1)', $books[0]->name);\n        $this->withHtml($resp)->assertElementNotContains('.book-content a.grid-card:nth-child(3)', $books[0]->name);\n\n        setting()->putUser($this->users->editor(), 'shelf_books_sort_order', 'desc');\n        $resp = $this->asEditor()->get($shelf->getUrl());\n        $this->withHtml($resp)->assertElementNotContains('.book-content a.grid-card:nth-child(1)', $books[0]->name);\n        $this->withHtml($resp)->assertElementContains('.book-content a.grid-card:nth-child(3)', $books[0]->name);\n\n        setting()->putUser($this->users->editor(), 'shelf_books_sort_order', 'desc');\n        setting()->putUser($this->users->editor(), 'shelf_books_sort', 'name');\n        $resp = $this->asEditor()->get($shelf->getUrl());\n        $this->withHtml($resp)->assertElementContains('.book-content a.grid-card:nth-child(1)', 'hdgfgdfg');\n        $this->withHtml($resp)->assertElementContains('.book-content a.grid-card:nth-child(2)', 'bsfsdfsdfsd');\n        $this->withHtml($resp)->assertElementContains('.book-content a.grid-card:nth-child(3)', 'adsfsdfsdfsd');\n    }\n\n    public function test_shelf_view_sorts_by_name_case_insensitively()\n    {\n        $shelf = Bookshelf::query()->whereHas('books')->with('books')->first();\n        $books = Book::query()->take(3)->get(['id', 'name']);\n        $books[0]->fill(['name' => 'Book Ab'])->save();\n        $books[1]->fill(['name' => 'Book ac'])->save();\n        $books[2]->fill(['name' => 'Book AD'])->save();\n\n        // Set book ordering\n        $this->asAdmin()->put($shelf->getUrl(), [\n            'books' => $books->implode('id', ','),\n            'tags'  => [], 'description_html' => 'abc', 'name' => 'abc',\n        ]);\n        $this->assertEquals(3, $shelf->books()->count());\n        $shelf->refresh();\n\n        setting()->putUser($this->users->editor(), 'shelf_books_sort', 'name');\n        setting()->putUser($this->users->editor(), 'shelf_books_sort_order', 'asc');\n        $html = $this->withHtml($this->asEditor()->get($shelf->getUrl()));\n\n        $html->assertElementContains('.book-content a.grid-card:nth-child(1)', 'Book Ab');\n        $html->assertElementContains('.book-content a.grid-card:nth-child(2)', 'Book ac');\n        $html->assertElementContains('.book-content a.grid-card:nth-child(3)', 'Book AD');\n    }\n\n    public function test_shelf_edit()\n    {\n        $shelf = $this->entities->shelf();\n        $resp = $this->asEditor()->get($shelf->getUrl('/edit'));\n        $resp->assertSeeText('Edit Shelf');\n\n        $booksToInclude = Book::take(2)->get();\n        $shelfInfo = [\n            'name'             => 'My test shelf' . Str::random(4),\n            'description_html' => '<p>Test book description ' . Str::random(10) . '</p>',\n        ];\n\n        $resp = $this->asEditor()->put($shelf->getUrl(), array_merge($shelfInfo, [\n            'books' => $booksToInclude->implode('id', ','),\n            'tags'  => [\n                [\n                    'name'  => 'Test Category',\n                    'value' => 'Test Tag Value',\n                ],\n            ],\n        ]));\n        $shelf = Bookshelf::find($shelf->id);\n        $resp->assertRedirect($shelf->getUrl());\n        $this->assertSessionHas('success');\n\n        $editorId = $this->users->editor()->id;\n        $this->assertDatabaseHasEntityData('bookshelf', array_merge($shelfInfo, ['id' => $shelf->id, 'created_by' => $editorId, 'updated_by' => $editorId]));\n\n        $shelfPage = $this->get($shelf->getUrl());\n        $shelfPage->assertSee($shelfInfo['name']);\n        $shelfPage->assertSee($shelfInfo['description_html'], false);\n        $this->withHtml($shelfPage)->assertElementContains('.tag-item', 'Test Category');\n        $this->withHtml($shelfPage)->assertElementContains('.tag-item', 'Test Tag Value');\n\n        $this->assertDatabaseHas('bookshelves_books', ['bookshelf_id' => $shelf->id, 'book_id' => $booksToInclude[0]->id]);\n        $this->assertDatabaseHas('bookshelves_books', ['bookshelf_id' => $shelf->id, 'book_id' => $booksToInclude[1]->id]);\n    }\n\n    public function test_shelf_edit_does_not_alter_books_we_dont_have_access_to()\n    {\n        $shelf = $this->entities->shelf();\n        $shelf->books()->detach();\n        $this->entities->book();\n        $this->entities->book();\n\n        $newBooks = [$this->entities->book(), $this->entities->book()];\n        $originalBooks = [$this->entities->book(), $this->entities->book()];\n        foreach ($originalBooks as $book) {\n            $this->permissions->disableEntityInheritedPermissions($book);\n            $shelf->books()->attach($book->id);\n        }\n\n        $this->asEditor()->put($shelf->getUrl(), [\n            'name' => $shelf->name,\n            'books' => \"{$newBooks[0]->id},{$newBooks[1]->id}\",\n        ])->assertRedirect($shelf->getUrl());\n\n        $resultingBooksById = $shelf->books()->get()->keyBy('id')->toArray();\n        $this->assertCount(4, $resultingBooksById);\n        foreach ($newBooks as $book) {\n            $this->assertArrayHasKey($book->id, $resultingBooksById);\n        }\n        foreach ($originalBooks as $book) {\n            $this->assertArrayHasKey($book->id, $resultingBooksById);\n        }\n    }\n\n    public function test_shelf_create_new_book()\n    {\n        $shelf = $this->entities->shelf();\n        $resp = $this->asEditor()->get($shelf->getUrl('/create-book'));\n\n        $resp->assertSee('Create New Book');\n        $resp->assertSee($shelf->getShortName());\n\n        $testName = 'Test Book in Shelf Name';\n\n        $createBookResp = $this->asEditor()->post($shelf->getUrl('/create-book'), [\n            'name'             => $testName,\n            'description_html' => 'Book in shelf description',\n        ]);\n        $createBookResp->assertRedirect();\n\n        $newBook = Book::query()->orderBy('id', 'desc')->first();\n        $this->assertDatabaseHas('bookshelves_books', [\n            'bookshelf_id' => $shelf->id,\n            'book_id'      => $newBook->id,\n        ]);\n\n        $resp = $this->asEditor()->get($shelf->getUrl());\n        $resp->assertSee($testName);\n    }\n\n    public function test_shelf_delete()\n    {\n        $shelf = Bookshelf::query()->whereHas('books')->first();\n        $this->assertNull($shelf->deleted_at);\n        $bookCount = $shelf->books()->count();\n\n        $deleteViewReq = $this->asEditor()->get($shelf->getUrl('/delete'));\n        $deleteViewReq->assertSeeText('Are you sure you want to delete this shelf?');\n\n        $deleteReq = $this->delete($shelf->getUrl());\n        $deleteReq->assertRedirect(url('/shelves'));\n        $this->assertActivityExists('bookshelf_delete', $shelf);\n\n        $shelf->refresh();\n        $this->assertNotNull($shelf->deleted_at);\n\n        $this->assertTrue($shelf->books()->count() === $bookCount);\n        $this->assertTrue($shelf->deletions()->count() === 1);\n\n        $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));\n        $this->assertNotificationContains($redirectReq, 'Shelf Successfully Deleted');\n    }\n\n    public function test_shelf_copy_permissions()\n    {\n        $shelf = $this->entities->shelf();\n        $resp = $this->asAdmin()->get($shelf->getUrl('/permissions'));\n        $resp->assertSeeText('Copy Permissions');\n        $resp->assertSee(\"action=\\\"{$shelf->getUrl('/copy-permissions')}\\\"\", false);\n\n        $child = $shelf->books()->first();\n        $editorRole = $this->users->editor()->roles()->first();\n        $this->assertFalse($child->hasPermissions(), 'Child book should not be restricted by default');\n        $this->assertTrue($child->permissions()->count() === 0, 'Child book should have no permissions by default');\n\n        $this->permissions->setEntityPermissions($shelf, ['view', 'update'], [$editorRole]);\n        $resp = $this->post($shelf->getUrl('/copy-permissions'));\n        $child = $shelf->books()->first();\n\n        $resp->assertRedirect($shelf->getUrl());\n        $this->assertTrue($child->hasPermissions(), 'Child book should now be restricted');\n        $this->assertTrue($child->permissions()->count() === 2, 'Child book should have copied permissions');\n        $this->assertDatabaseHas('entity_permissions', [\n            'entity_type' => 'book',\n            'entity_id' => $child->id,\n            'role_id' => $editorRole->id,\n            'view' => true, 'update' => true, 'create' => false, 'delete' => false,\n        ]);\n    }\n\n    public function test_permission_page_has_a_warning_about_no_cascading()\n    {\n        $shelf = $this->entities->shelf();\n        $resp = $this->asAdmin()->get($shelf->getUrl('/permissions'));\n        $resp->assertSeeText('Permissions on shelves do not automatically cascade to contained books.');\n    }\n\n    public function test_bookshelves_show_in_breadcrumbs_if_in_context()\n    {\n        $shelf = $this->entities->shelf();\n        $shelfBook = $shelf->books()->first();\n        $shelfPage = $shelfBook->pages()->first();\n        $this->asAdmin();\n\n        $bookVisit = $this->get($shelfBook->getUrl());\n        $this->withHtml($bookVisit)->assertElementNotContains('.breadcrumbs', 'Shelves');\n        $this->withHtml($bookVisit)->assertElementNotContains('.breadcrumbs', $shelf->getShortName());\n\n        $this->get($shelf->getUrl());\n        $bookVisit = $this->get($shelfBook->getUrl());\n        $this->withHtml($bookVisit)->assertElementContains('.breadcrumbs', 'Shelves');\n        $this->withHtml($bookVisit)->assertElementContains('.breadcrumbs', $shelf->getShortName());\n\n        $pageVisit = $this->get($shelfPage->getUrl());\n        $this->withHtml($pageVisit)->assertElementContains('.breadcrumbs', 'Shelves');\n        $this->withHtml($pageVisit)->assertElementContains('.breadcrumbs', $shelf->getShortName());\n\n        $this->get('/books');\n        $pageVisit = $this->get($shelfPage->getUrl());\n        $this->withHtml($pageVisit)->assertElementNotContains('.breadcrumbs', 'Shelves');\n        $this->withHtml($pageVisit)->assertElementNotContains('.breadcrumbs', $shelf->getShortName());\n    }\n\n    public function test_bookshelves_show_on_book()\n    {\n        // Create shelf\n        $shelfInfo = [\n            'name'             => 'My test shelf' . Str::random(4),\n            'description_html' => '<p>Test shelf description ' . Str::random(10) . '</p>',\n        ];\n\n        $this->asEditor()->post('/shelves', $shelfInfo);\n        $shelf = Bookshelf::where('name', '=', $shelfInfo['name'])->first();\n\n        // Create book and add to shelf\n        $this->asEditor()->post($shelf->getUrl('/create-book'), [\n            'name'             => 'Test book name',\n            'description_html' => '<p>Book in shelf description</p>',\n        ]);\n\n        $newBook = Book::query()->orderBy('id', 'desc')->first();\n\n        $resp = $this->asEditor()->get($newBook->getUrl());\n        $this->withHtml($resp)->assertElementContains('.tri-layout-left-contents', $shelfInfo['name']);\n\n        // Remove shelf\n        $this->delete($shelf->getUrl());\n\n        $resp = $this->asEditor()->get($newBook->getUrl());\n        $resp->assertDontSee($shelfInfo['name']);\n    }\n\n    public function test_cancel_on_child_book_creation_returns_to_original_shelf()\n    {\n        $shelf = $this->entities->shelf();\n        $resp = $this->asEditor()->get($shelf->getUrl('/create-book'));\n        $this->withHtml($resp)->assertElementContains('form a[href=\"' . $shelf->getUrl() . '\"]', 'Cancel');\n    }\n\n    public function test_show_view_displays_description_if_no_description_html_set()\n    {\n        $shelf = $this->entities->shelf();\n        $shelf->description_html = '';\n        $shelf->description = \"My great\\ndescription\\n\\nwith newlines\";\n        $shelf->save();\n\n        $resp = $this->asEditor()->get($shelf->getUrl());\n        $resp->assertSee(\"<p>My great<br>\\ndescription<br>\\n<br>\\nwith newlines</p>\", false);\n    }\n}\n"
  },
  {
    "path": "tests/Entity/BookTest.php",
    "content": "<?php\n\nnamespace Tests\\Entity;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\BookChild;\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Entities\\Repos\\BookRepo;\nuse Tests\\TestCase;\n\nclass BookTest extends TestCase\n{\n    public function test_create()\n    {\n        $book = Book::factory()->make([\n            'name' => 'My First Book',\n        ]);\n\n        $resp = $this->asEditor()->get('/books');\n        $this->withHtml($resp)->assertElementContains('a[href=\"' . url('/create-book') . '\"]', 'Create New Book');\n\n        $resp = $this->get('/create-book');\n        $this->withHtml($resp)->assertElementContains('form[action=\"' . url('/books') . '\"][method=\"POST\"]', 'Save Book');\n\n        $resp = $this->post('/books', $book->only('name', 'description_html'));\n        $resp->assertRedirect('/books/my-first-book');\n\n        $resp = $this->get('/books/my-first-book');\n        $resp->assertSee($book->name);\n        $resp->assertSee($book->descriptionInfo()->getPlain());\n    }\n\n    public function test_create_uses_different_slugs_when_name_reused()\n    {\n        $book = Book::factory()->make([\n            'name' => 'My First Book',\n        ]);\n\n        $this->asEditor()->post('/books', $book->only('name', 'description_html'));\n        $this->asEditor()->post('/books', $book->only('name', 'description_html'));\n\n        $books = Book::query()->where('name', '=', $book->name)\n            ->orderBy('id', 'desc')\n            ->take(2)\n            ->get();\n\n        $this->assertMatchesRegularExpression('/my-first-book-[0-9a-zA-Z]{3}/', $books[0]->slug);\n        $this->assertEquals('my-first-book', $books[1]->slug);\n    }\n\n    public function test_create_sets_tags()\n    {\n        // Cheeky initial update to refresh slug\n        $this->asEditor()->post('books', [\n            'name'             => 'My book with tags',\n            'description_html' => '<p>A book with tags</p>',\n            'tags'             => [\n                [\n                    'name'  => 'Category',\n                    'value' => 'Donkey Content',\n                ],\n                [\n                    'name'  => 'Level',\n                    'value' => '5',\n                ],\n            ],\n        ]);\n\n        /** @var Book $book */\n        $book = Book::query()->where('name', '=', 'My book with tags')->firstOrFail();\n        $tags = $book->tags()->get();\n\n        $this->assertEquals(2, $tags->count());\n        $this->assertEquals('Donkey Content', $tags[0]->value);\n        $this->assertEquals('Level', $tags[1]->name);\n    }\n\n    public function test_update()\n    {\n        $book = $this->entities->book();\n        // Cheeky initial update to refresh slug\n        $this->asEditor()->put($book->getUrl(), ['name' => $book->name . '5', 'description_html' => $book->description_html]);\n        $book->refresh();\n\n        $newName = $book->name . ' Updated';\n        $newDesc = $book->description_html . '<p>with more content</p>';\n\n        $resp = $this->get($book->getUrl('/edit'));\n        $resp->assertSee($book->name);\n        $resp->assertSee($book->description_html);\n        $this->withHtml($resp)->assertElementContains('form[action=\"' . $book->getUrl() . '\"]', 'Save Book');\n\n        $resp = $this->put($book->getUrl(), ['name' => $newName, 'description_html' => $newDesc]);\n        $resp->assertRedirect($book->getUrl() . '-updated');\n\n        $resp = $this->get($book->getUrl() . '-updated');\n        $resp->assertSee($newName);\n        $resp->assertSee($newDesc, false);\n    }\n\n    public function test_update_sets_tags()\n    {\n        $book = $this->entities->book();\n\n        $this->assertEquals(0, $book->tags()->count());\n\n        // Cheeky initial update to refresh slug\n        $this->asEditor()->put($book->getUrl(), [\n            'name' => $book->name,\n            'tags' => [\n                [\n                    'name'  => 'Category',\n                    'value' => 'Dolphin Content',\n                ],\n                [\n                    'name'  => 'Level',\n                    'value' => '5',\n                ],\n            ],\n        ]);\n\n        $book->refresh();\n        $tags = $book->tags()->get();\n\n        $this->assertEquals(2, $tags->count());\n        $this->assertEquals('Dolphin Content', $tags[0]->value);\n        $this->assertEquals('Level', $tags[1]->name);\n    }\n\n    public function test_delete()\n    {\n        $book = Book::query()->whereHas('pages')->whereHas('chapters')->first();\n        $this->assertNull($book->deleted_at);\n        $pageCount = $book->pages()->count();\n        $chapterCount = $book->chapters()->count();\n\n        $deleteViewReq = $this->asEditor()->get($book->getUrl('/delete'));\n        $deleteViewReq->assertSeeText('Are you sure you want to delete this book?');\n\n        $deleteReq = $this->delete($book->getUrl());\n        $deleteReq->assertRedirect(url('/books'));\n        $this->assertActivityExists('book_delete', $book);\n\n        $book->refresh();\n        $this->assertNotNull($book->deleted_at);\n\n        $this->assertTrue($book->pages()->count() === 0);\n        $this->assertTrue($book->chapters()->count() === 0);\n        $this->assertTrue($book->pages()->withTrashed()->count() === $pageCount);\n        $this->assertTrue($book->chapters()->withTrashed()->count() === $chapterCount);\n        $this->assertTrue($book->deletions()->count() === 1);\n\n        $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));\n        $this->assertNotificationContains($redirectReq, 'Book Successfully Deleted');\n    }\n\n    public function test_delete_with_shelf_context_returns_to_shelf_view_after_delete()\n    {\n        $shelf = $this->entities->shelfHasBooks();\n        /** @var Book $book */\n        $book = $shelf->books()->first();\n\n        $this->asEditor()->get($shelf->getUrl());\n        $this->get($book->getUrl());\n        $this->get($book->getUrl('/delete'));\n        $resp = $this->delete($book->getUrl());\n\n        $resp->assertRedirect($shelf->getUrl());\n    }\n\n    public function test_cancel_on_create_page_leads_back_to_books_listing()\n    {\n        $resp = $this->asEditor()->get('/create-book');\n        $this->withHtml($resp)->assertElementContains('form a[href=\"' . url('/books') . '\"]', 'Cancel');\n    }\n\n    public function test_cancel_on_edit_book_page_leads_back_to_book()\n    {\n        $book = $this->entities->book();\n        $resp = $this->asEditor()->get($book->getUrl('/edit'));\n        $this->withHtml($resp)->assertElementContains('form a[href=\"' . $book->getUrl() . '\"]', 'Cancel');\n    }\n\n    public function test_next_previous_navigation_controls_show_within_book_content()\n    {\n        $book = $this->entities->book();\n        $chapter = $book->chapters->first();\n\n        $resp = $this->asEditor()->get($chapter->getUrl());\n        $this->withHtml($resp)->assertElementContains('#sibling-navigation', 'Next');\n        $this->withHtml($resp)->assertElementContains('#sibling-navigation', substr($chapter->pages[0]->name, 0, 20));\n\n        $resp = $this->get($chapter->pages[0]->getUrl());\n        $this->withHtml($resp)->assertElementContains('#sibling-navigation', substr($chapter->pages[1]->name, 0, 20));\n        $this->withHtml($resp)->assertElementContains('#sibling-navigation', 'Previous');\n        $this->withHtml($resp)->assertElementContains('#sibling-navigation', substr($chapter->name, 0, 20));\n    }\n\n    public function test_recently_viewed_books_updates_as_expected()\n    {\n        $books = Book::take(2)->get();\n\n        $resp = $this->asAdmin()->get('/books');\n        $this->withHtml($resp)->assertElementNotContains('#recents', $books[0]->name)\n            ->assertElementNotContains('#recents', $books[1]->name);\n\n        $this->get($books[0]->getUrl());\n        $this->get($books[1]->getUrl());\n\n        $resp = $this->get('/books');\n        $this->withHtml($resp)->assertElementContains('#recents', $books[0]->name)\n            ->assertElementContains('#recents', $books[1]->name);\n    }\n\n    public function test_popular_books_updates_upon_visits()\n    {\n        $books = Book::take(2)->get();\n\n        $resp = $this->asAdmin()->get('/books');\n        $this->withHtml($resp)->assertElementNotContains('#popular', $books[0]->name)\n            ->assertElementNotContains('#popular', $books[1]->name);\n\n        $this->get($books[0]->getUrl());\n        $this->get($books[1]->getUrl());\n        $this->get($books[0]->getUrl());\n\n        $resp = $this->get('/books');\n        $this->withHtml($resp)->assertElementContains('#popular .book:nth-child(1)', $books[0]->name)\n            ->assertElementContains('#popular .book:nth-child(2)', $books[1]->name);\n    }\n\n    public function test_books_view_shows_view_toggle_option()\n    {\n        /** @var Book $book */\n        $editor = $this->users->editor();\n        setting()->putUser($editor, 'books_view_type', 'list');\n\n        $resp = $this->actingAs($editor)->get('/books');\n        $this->withHtml($resp)->assertElementContains('form[action$=\"/preferences/change-view/books\"]', 'Grid View');\n        $this->withHtml($resp)->assertElementExists('button[name=\"view\"][value=\"grid\"]');\n\n        $resp = $this->patch(\"/preferences/change-view/books\", ['view' => 'grid']);\n        $resp->assertRedirect();\n        $this->assertEquals('grid', setting()->getUser($editor, 'books_view_type'));\n\n        $resp = $this->actingAs($editor)->get('/books');\n        $this->withHtml($resp)->assertElementContains('form[action$=\"/preferences/change-view/books\"]', 'List View');\n        $this->withHtml($resp)->assertElementExists('button[name=\"view\"][value=\"list\"]');\n\n        $resp = $this->patch(\"/preferences/change-view/books\", ['view_type' => 'list']);\n        $resp->assertRedirect();\n        $this->assertEquals('list', setting()->getUser($editor, 'books_view_type'));\n    }\n\n    public function test_description_limited_to_specific_html()\n    {\n        $book = $this->entities->book();\n\n        $input = '<h1>Test</h1><p id=\"abc\" href=\"beans\">Content<a href=\"#cat\" target=\"_blank\" data-a=\"b\">a</a><section>Hello</section></p>';\n        $expected = '<p>Content<a href=\"#cat\" target=\"_blank\">a</a></p>';\n\n        $this->asEditor()->put($book->getUrl(), [\n            'name' => $book->name,\n            'description_html' => $input\n        ]);\n\n        $book->refresh();\n        $this->assertEquals($expected, $book->description_html);\n    }\n\n    public function test_show_view_displays_description_if_no_description_html_set()\n    {\n        $book = $this->entities->book();\n        $book->description_html = '';\n        $book->description = \"My great\\ndescription\\n\\nwith newlines\";\n        $book->save();\n\n        $resp = $this->asEditor()->get($book->getUrl());\n        $resp->assertSee(\"<p>My great<br>\\ndescription<br>\\n<br>\\nwith newlines</p>\", false);\n    }\n\n    public function test_description_with_only_br_tags_results_in_empty_p_tag_used_on_show()\n    {\n        $descriptions = [\n            '<p><br></p>',\n            '<p><br><br><br><br></p>',\n            '<p><br><br><br></p><h1><br><br><br><br><br></h1>',\n        ];\n        $book = $this->entities->book();\n        $this->asEditor();\n\n        foreach ($descriptions as $descriptionTestCase) {\n            $book->description_html = $descriptionTestCase;\n            $book->save();\n\n            $resp = $this->get($book->getUrl());\n            $html = $this->withHtml($resp);\n            $descriptionHtml = $html->getInnerHtml('.book-content > div.text-muted:first-child');\n            $this->assertEquals('<p></p>', $descriptionHtml);\n        }\n    }\n}\n"
  },
  {
    "path": "tests/Entity/ChapterTest.php",
    "content": "<?php\n\nnamespace Tests\\Entity;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Page;\nuse Tests\\TestCase;\n\nclass ChapterTest extends TestCase\n{\n    public function test_create()\n    {\n        $book = $this->entities->book();\n\n        $chapter = Chapter::factory()->make([\n            'name' => 'My First Chapter',\n        ]);\n\n        $resp = $this->asEditor()->get($book->getUrl());\n        $this->withHtml($resp)->assertElementContains('a[href=\"' . $book->getUrl('/create-chapter') . '\"]', 'New Chapter');\n\n        $resp = $this->get($book->getUrl('/create-chapter'));\n        $this->withHtml($resp)->assertElementContains('form[action=\"' . $book->getUrl('/create-chapter') . '\"][method=\"POST\"]', 'Save Chapter');\n\n        $resp = $this->post($book->getUrl('/create-chapter'), $chapter->only('name', 'description_html'));\n        $resp->assertRedirect($book->getUrl('/chapter/my-first-chapter'));\n\n        $resp = $this->get($book->getUrl('/chapter/my-first-chapter'));\n        $resp->assertSee($chapter->name);\n        $resp->assertSee($chapter->description_html, false);\n    }\n\n    public function test_show_view_displays_description_if_no_description_html_set()\n    {\n        $chapter = $this->entities->chapter();\n        $chapter->description_html = '';\n        $chapter->description = \"My great\\ndescription\\n\\nwith newlines\";\n        $chapter->save();\n\n        $resp = $this->asEditor()->get($chapter->getUrl());\n        $resp->assertSee(\"<p>My great<br>\\ndescription<br>\\n<br>\\nwith newlines</p>\", false);\n    }\n\n    public function test_delete()\n    {\n        $chapter = Chapter::query()->whereHas('pages')->first();\n        $this->assertNull($chapter->deleted_at);\n        $pageCount = $chapter->pages()->count();\n\n        $deleteViewReq = $this->asEditor()->get($chapter->getUrl('/delete'));\n        $deleteViewReq->assertSeeText('Are you sure you want to delete this chapter?');\n\n        $deleteReq = $this->delete($chapter->getUrl());\n        $deleteReq->assertRedirect($chapter->getParent()->getUrl());\n        $this->assertActivityExists('chapter_delete', $chapter);\n\n        $chapter->refresh();\n        $this->assertNotNull($chapter->deleted_at);\n\n        $this->assertTrue($chapter->pages()->count() === 0);\n        $this->assertTrue($chapter->pages()->withTrashed()->count() === $pageCount);\n        $this->assertTrue($chapter->deletions()->count() === 1);\n\n        $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));\n        $this->assertNotificationContains($redirectReq, 'Chapter Successfully Deleted');\n    }\n\n\n\n    public function test_sort_book_action_visible_if_permissions_allow()\n    {\n        $chapter = $this->entities->chapter();\n\n        $resp = $this->actingAs($this->users->viewer())->get($chapter->getUrl());\n        $this->withHtml($resp)->assertLinkNotExists($chapter->book->getUrl('sort'));\n\n        $resp = $this->asEditor()->get($chapter->getUrl());\n        $this->withHtml($resp)->assertLinkExists($chapter->book->getUrl('sort'));\n    }\n}\n"
  },
  {
    "path": "tests/Entity/ConvertTest.php",
    "content": "<?php\n\nnamespace Tests\\Entity;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Activity\\Models\\Tag;\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Page;\nuse Tests\\TestCase;\n\nclass ConvertTest extends TestCase\n{\n    public function test_chapter_edit_view_shows_convert_option()\n    {\n        $chapter = $this->entities->chapter();\n\n        $resp = $this->asEditor()->get($chapter->getUrl('/edit'));\n        $resp->assertSee('Convert to Book');\n        $resp->assertSee('Convert Chapter');\n        $this->withHtml($resp)->assertElementExists('form[action$=\"/convert-to-book\"] button');\n    }\n\n    public function test_convert_chapter_to_book()\n    {\n        $chapter = $this->entities->chapterHasPages();\n        $chapter->tags()->save(new Tag(['name' => 'Category', 'value' => 'Penguins']));\n        /** @var Page $childPage */\n        $childPage = $chapter->pages()->first();\n\n        $resp = $this->asEditor()->post($chapter->getUrl('/convert-to-book'));\n        $resp->assertRedirectContains('/books/');\n\n        /** @var Book $newBook */\n        $newBook = Book::query()->orderBy('id', 'desc')->first();\n\n        $this->assertDatabaseMissing('entities', ['id' => $chapter->id, 'type' => 'chapter']);\n        $this->assertDatabaseHasEntityData('page', ['id' => $childPage->id, 'book_id' => $newBook->id, 'chapter_id' => 0]);\n        $this->assertCount(1, $newBook->tags);\n        $this->assertEquals('Category', $newBook->tags->first()->name);\n        $this->assertEquals('Penguins', $newBook->tags->first()->value);\n        $this->assertEquals($chapter->name, $newBook->name);\n        $this->assertEquals($chapter->description, $newBook->description);\n        $this->assertEquals($chapter->description_html, $newBook->description_html);\n\n        $this->assertActivityExists(ActivityType::BOOK_CREATE_FROM_CHAPTER, $newBook);\n    }\n\n    public function test_convert_chapter_to_book_requires_permissions()\n    {\n        $chapter = $this->entities->chapter();\n        $user = $this->users->viewer();\n\n        $permissions = ['chapter-delete-all', 'book-create-all', 'chapter-update-all'];\n        $this->permissions->grantUserRolePermissions($user, $permissions);\n\n        foreach ($permissions as $permission) {\n            $this->permissions->removeUserRolePermissions($user, [$permission]);\n            $resp = $this->actingAs($user)->post($chapter->getUrl('/convert-to-book'));\n            $this->assertPermissionError($resp);\n            $this->permissions->grantUserRolePermissions($user, [$permission]);\n        }\n\n        $resp = $this->actingAs($user)->post($chapter->getUrl('/convert-to-book'));\n        $this->assertNotPermissionError($resp);\n        $resp->assertRedirect();\n    }\n\n    public function test_book_edit_view_shows_convert_option()\n    {\n        $book = $this->entities->book();\n\n        $resp = $this->asEditor()->get($book->getUrl('/edit'));\n        $resp->assertSee('Convert to Shelf');\n        $resp->assertSee('Convert Book');\n        $resp->assertSee('Note that permissions on shelves do not auto-cascade to content');\n        $this->withHtml($resp)->assertElementExists('form[action$=\"/convert-to-shelf\"] button');\n    }\n\n    public function test_book_convert_to_shelf()\n    {\n        /** @var Book $book */\n        $book = Book::query()->whereHas('directPages')->whereHas('chapters')->firstOrFail();\n        $book->tags()->save(new Tag(['name' => 'Category', 'value' => 'Ducks']));\n        /** @var Page $childPage */\n        $childPage = $book->directPages()->first();\n        /** @var Chapter $childChapter */\n        $childChapter = $book->chapters()->whereHas('pages')->firstOrFail();\n        /** @var Page $chapterChildPage */\n        $chapterChildPage = $childChapter->pages()->firstOrFail();\n        $bookChapterCount = $book->chapters()->count();\n        $systemBookCount = Book::query()->count();\n\n        // Run conversion\n        $resp = $this->asEditor()->post($book->getUrl('/convert-to-shelf'));\n\n        /** @var Bookshelf $newShelf */\n        $newShelf = Bookshelf::query()->orderBy('id', 'desc')->first();\n\n        // Checks for new shelf\n        $resp->assertRedirectContains('/shelves/');\n        $this->assertDatabaseMissing('entities', ['id' => $childChapter->id, 'type' => 'chapter']);\n        $this->assertCount(1, $newShelf->tags);\n        $this->assertEquals('Category', $newShelf->tags->first()->name);\n        $this->assertEquals('Ducks', $newShelf->tags->first()->value);\n        $this->assertEquals($book->name, $newShelf->name);\n        $this->assertEquals($book->description, $newShelf->description);\n        $this->assertEquals($book->description_html, $newShelf->description_html);\n        $this->assertEquals($newShelf->books()->count(), $bookChapterCount + 1);\n        $this->assertEquals($systemBookCount + $bookChapterCount, Book::query()->count());\n        $this->assertActivityExists(ActivityType::BOOKSHELF_CREATE_FROM_BOOK, $newShelf);\n\n        // Checks for old book to contain child pages\n        $this->assertDatabaseHasEntityData('book', ['id' => $book->id, 'name' => $book->name . ' Pages']);\n        $this->assertDatabaseHasEntityData('page', ['id' => $childPage->id, 'book_id' => $book->id, 'chapter_id' => null]);\n\n        // Checks for nested page\n        $chapterChildPage->refresh();\n        $this->assertEquals(0, $chapterChildPage->chapter_id);\n        $this->assertEquals($childChapter->name, $chapterChildPage->book->name);\n    }\n\n    public function test_book_convert_to_shelf_requires_permissions()\n    {\n        $book = $this->entities->book();\n        $user = $this->users->viewer();\n\n        $permissions = ['book-delete-all', 'bookshelf-create-all', 'book-update-all', 'book-create-all'];\n        $this->permissions->grantUserRolePermissions($user, $permissions);\n\n        foreach ($permissions as $permission) {\n            $this->permissions->removeUserRolePermissions($user, [$permission]);\n            $resp = $this->actingAs($user)->post($book->getUrl('/convert-to-shelf'));\n            $this->assertPermissionError($resp);\n            $this->permissions->grantUserRolePermissions($user, [$permission]);\n        }\n\n        $resp = $this->actingAs($user)->post($book->getUrl('/convert-to-shelf'));\n        $this->assertNotPermissionError($resp);\n        $resp->assertRedirect();\n    }\n}\n"
  },
  {
    "path": "tests/Entity/CopyTest.php",
    "content": "<?php\n\nnamespace Tests\\Entity;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\BookChild;\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Repos\\BookRepo;\nuse Tests\\TestCase;\n\nclass CopyTest extends TestCase\n{\n    public function test_book_show_view_has_copy_button()\n    {\n        $book = $this->entities->book();\n        $resp = $this->asEditor()->get($book->getUrl());\n\n        $this->withHtml($resp)->assertElementContains(\"a[href=\\\"{$book->getUrl('/copy')}\\\"]\", 'Copy');\n    }\n\n    public function test_book_copy_view()\n    {\n        $book = $this->entities->book();\n        $resp = $this->asEditor()->get($book->getUrl('/copy'));\n\n        $resp->assertOk();\n        $resp->assertSee('Copy Book');\n        $this->withHtml($resp)->assertElementExists(\"input[name=\\\"name\\\"][value=\\\"{$book->name}\\\"]\");\n    }\n\n    public function test_book_copy()\n    {\n        /** @var Book $book */\n        $book = Book::query()->whereHas('chapters')->whereHas('pages')->first();\n        $resp = $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);\n\n        /** @var Book $copy */\n        $copy = Book::query()->where('name', '=', 'My copy book')->first();\n\n        $resp->assertRedirect($copy->getUrl());\n        $this->assertEquals($book->getDirectVisibleChildren()->count(), $copy->getDirectVisibleChildren()->count());\n\n        $this->get($copy->getUrl())->assertSee($book->description_html, false);\n    }\n\n    public function test_book_copy_does_not_copy_non_visible_content()\n    {\n        /** @var Book $book */\n        $book = Book::query()->whereHas('chapters')->whereHas('pages')->first();\n\n        // Hide child content\n        /** @var BookChild $page */\n        foreach ($book->getDirectVisibleChildren() as $child) {\n            $this->permissions->setEntityPermissions($child, [], []);\n        }\n\n        $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);\n        /** @var Book $copy */\n        $copy = Book::query()->where('name', '=', 'My copy book')->first();\n\n        $this->assertEquals(0, $copy->getDirectVisibleChildren()->count());\n    }\n\n    public function test_book_copy_does_not_copy_pages_or_chapters_if_user_cant_create()\n    {\n        /** @var Book $book */\n        $book = Book::query()->whereHas('chapters')->whereHas('directPages')->whereHas('chapters')->first();\n        $viewer = $this->users->viewer();\n        $this->permissions->grantUserRolePermissions($viewer, ['book-create-all']);\n\n        $this->actingAs($viewer)->post($book->getUrl('/copy'), ['name' => 'My copy book']);\n        /** @var Book $copy */\n        $copy = Book::query()->where('name', '=', 'My copy book')->first();\n\n        $this->assertEquals(0, $copy->pages()->count());\n        $this->assertEquals(0, $copy->chapters()->count());\n    }\n\n    public function test_book_copy_clones_cover_image_if_existing()\n    {\n        $book = $this->entities->book();\n        $bookRepo = $this->app->make(BookRepo::class);\n        $coverImageFile = $this->files->uploadedImage('cover.png');\n        $bookRepo->updateCoverImage($book, $coverImageFile);\n\n        $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book'])->assertRedirect();\n        /** @var Book $copy */\n        $copy = Book::query()->where('name', '=', 'My copy book')->first();\n\n        $this->assertNotNull($copy->coverInfo()->getImage());\n        $this->assertNotEquals($book->coverInfo()->getImage()->id, $copy->coverInfo()->getImage()->id);\n    }\n\n    public function test_book_copy_adds_book_to_shelves_if_edit_permissions_allows()\n    {\n        /** @var Bookshelf $shelfA */\n        /** @var Bookshelf $shelfB */\n        [$shelfA, $shelfB] = Bookshelf::query()->take(2)->get();\n        $book = $this->entities->book();\n\n        $shelfA->appendBook($book);\n        $shelfB->appendBook($book);\n\n        $viewer = $this->users->viewer();\n        $this->permissions->grantUserRolePermissions($viewer, ['book-update-all', 'book-create-all', 'bookshelf-update-all']);\n        $this->permissions->setEntityPermissions($shelfB);\n\n\n        $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);\n        /** @var Book $copy */\n        $copy = Book::query()->where('name', '=', 'My copy book')->first();\n\n        $this->assertTrue($copy->shelves()->where('id', '=', $shelfA->id)->exists());\n        $this->assertFalse($copy->shelves()->where('id', '=', $shelfB->id)->exists());\n    }\n\n    public function test_chapter_show_view_has_copy_button()\n    {\n        $chapter = $this->entities->chapter();\n\n        $resp = $this->asEditor()->get($chapter->getUrl());\n        $this->withHtml($resp)->assertElementContains(\"a[href$=\\\"{$chapter->getUrl('/copy')}\\\"]\", 'Copy');\n    }\n\n    public function test_chapter_copy_view()\n    {\n        $chapter = $this->entities->chapter();\n\n        $resp = $this->asEditor()->get($chapter->getUrl('/copy'));\n        $resp->assertOk();\n        $resp->assertSee('Copy Chapter');\n        $this->withHtml($resp)->assertElementExists(\"input[name=\\\"name\\\"][value=\\\"{$chapter->name}\\\"]\");\n        $this->withHtml($resp)->assertElementExists('input[name=\"entity_selection\"]');\n    }\n\n    public function test_chapter_copy()\n    {\n        /** @var Chapter $chapter */\n        $chapter = Chapter::query()->whereHas('pages')->first();\n        /** @var Book $otherBook */\n        $otherBook = Book::query()->where('id', '!=', $chapter->book_id)->first();\n\n        $resp = $this->asEditor()->post($chapter->getUrl('/copy'), [\n            'name'             => 'My copied chapter',\n            'entity_selection' => 'book:' . $otherBook->id,\n        ]);\n\n        /** @var Chapter $newChapter */\n        $newChapter = Chapter::query()->where('name', '=', 'My copied chapter')->first();\n\n        $resp->assertRedirect($newChapter->getUrl());\n        $this->assertEquals($otherBook->id, $newChapter->book_id);\n        $this->assertEquals($chapter->pages->count(), $newChapter->pages->count());\n    }\n\n    public function test_chapter_copy_does_not_copy_non_visible_pages()\n    {\n        $chapter = $this->entities->chapterHasPages();\n\n        // Hide pages to all non-admin roles\n        /** @var Page $page */\n        foreach ($chapter->pages as $page) {\n            $this->permissions->setEntityPermissions($page, [], []);\n        }\n\n        $this->asEditor()->post($chapter->getUrl('/copy'), [\n            'name' => 'My copied chapter',\n        ]);\n\n        /** @var Chapter $newChapter */\n        $newChapter = Chapter::query()->where('name', '=', 'My copied chapter')->first();\n        $this->assertEquals(0, $newChapter->pages()->count());\n    }\n\n    public function test_chapter_copy_does_not_copy_pages_if_user_cant_page_create()\n    {\n        $chapter = $this->entities->chapterHasPages();\n        $viewer = $this->users->viewer();\n        $this->permissions->grantUserRolePermissions($viewer, ['chapter-create-all']);\n\n        // Lacking permission results in no copied pages\n        $this->actingAs($viewer)->post($chapter->getUrl('/copy'), [\n            'name' => 'My copied chapter',\n        ]);\n\n        /** @var Chapter $newChapter */\n        $newChapter = Chapter::query()->where('name', '=', 'My copied chapter')->first();\n        $this->assertEquals(0, $newChapter->pages()->count());\n\n        $this->permissions->grantUserRolePermissions($viewer, ['page-create-all']);\n\n        // Having permission rules in copied pages\n        $this->actingAs($viewer)->post($chapter->getUrl('/copy'), [\n            'name' => 'My copied again chapter',\n        ]);\n\n        /** @var Chapter $newChapter2 */\n        $newChapter2 = Chapter::query()->where('name', '=', 'My copied again chapter')->first();\n        $this->assertEquals($chapter->pages()->count(), $newChapter2->pages()->count());\n    }\n\n    public function test_book_copy_updates_internal_references()\n    {\n        $book = $this->entities->bookHasChaptersAndPages();\n        /** @var Chapter $chapter */\n        $chapter = $book->chapters()->first();\n        /** @var Page $page */\n        $page = $chapter->pages()->first();\n        $this->asEditor();\n        $this->entities->updatePage($page, [\n            'name' => 'reference test page',\n            'html' => '<p>This is a test <a href=\"' . $book->getUrl() . '\">book link</a></p>',\n        ]);\n\n        // Quick pre-update to get stable slug\n        $this->put($book->getUrl(), ['name' => 'Internal ref test']);\n        $book->refresh();\n        $page->refresh();\n\n        $html = '<p>This is a test <a href=\"' . $page->getUrl() . '\">page link</a></p>';\n        $this->put($book->getUrl(), ['name' => 'Internal ref test', 'description_html' => $html]);\n\n        $this->post($book->getUrl('/copy'), ['name' => 'My copied book']);\n\n        $newBook = Book::query()->where('name', '=', 'My copied book')->first();\n        $newPage = $newBook->pages()->where('name', '=', 'reference test page')->first();\n\n        $this->assertStringContainsString($newBook->getUrl(), $newPage->html);\n        $this->assertStringContainsString($newPage->getUrl(), $newBook->description_html);\n\n        $this->assertStringNotContainsString($book->getUrl(), $newPage->html);\n        $this->assertStringNotContainsString($page->getUrl(), $newBook->description_html);\n    }\n\n    public function test_chapter_copy_updates_internal_references()\n    {\n        $chapter = $this->entities->chapterHasPages();\n        /** @var Page $page */\n        $page = $chapter->pages()->first();\n        $this->asEditor();\n        $this->entities->updatePage($page, [\n            'name' => 'reference test page',\n            'html' => '<p>This is a test <a href=\"' . $chapter->getUrl() . '\">chapter link</a></p>',\n        ]);\n\n        // Quick pre-update to get stable slug\n        $this->put($chapter->getUrl(), ['name' => 'Internal ref test']);\n        $chapter->refresh();\n        $page->refresh();\n\n        $html = '<p>This is a test <a href=\"' . $page->getUrl() . '\">page link</a></p>';\n        $this->put($chapter->getUrl(), ['name' => 'Internal ref test', 'description_html' => $html]);\n\n        $this->post($chapter->getUrl('/copy'), ['name' => 'My copied chapter']);\n\n        $newChapter = Chapter::query()->where('name', '=', 'My copied chapter')->first();\n        $newPage = $newChapter->pages()->where('name', '=', 'reference test page')->first();\n\n        $this->assertStringContainsString($newChapter->getUrl() . '\"', $newPage->html);\n        $this->assertStringContainsString($newPage->getUrl() . '\"', $newChapter->description_html);\n\n        $this->assertStringNotContainsString($chapter->getUrl() . '\"', $newPage->html);\n        $this->assertStringNotContainsString($page->getUrl() . '\"', $newChapter->description_html);\n    }\n\n    public function test_chapter_copy_updates_internal_permalink_references_in_its_description()\n    {\n        $chapter = $this->entities->chapterHasPages();\n        /** @var Page $page */\n        $page = $chapter->pages()->first();\n\n        $this->asEditor()->put($chapter->getUrl(), [\n            'name' => 'Internal ref test',\n            'description_html' => '<p>This is a test <a href=\"' . $page->getPermalink() . '\">page link</a></p>',\n        ]);\n        $chapter->refresh();\n\n        $this->post($chapter->getUrl('/copy'), ['name' => 'My copied chapter']);\n        $newChapter = Chapter::query()->where('name', '=', 'My copied chapter')->first();\n\n        $this->assertStringContainsString('/link/', $newChapter->description_html);\n        $this->assertStringNotContainsString($page->getPermalink() . '\"', $newChapter->description_html);\n    }\n\n    public function test_page_copy_updates_internal_self_references()\n    {\n        $page = $this->entities->page();\n        $this->asEditor();\n\n        // Initial update to get stable slug\n        $this->entities->updatePage($page, ['name' => 'reference test page']);\n\n        $page->refresh();\n        $this->entities->updatePage($page, [\n            'name' => 'reference test page',\n            'html' => '<p>This is a test <a href=\"' . $page->getUrl() . '\">page link</a></p>',\n        ]);\n\n        $this->post($page->getUrl('/copy'), ['name' => 'My copied page']);\n        $newPage = Page::query()->where('name', '=', 'My copied page')->first();\n        $this->assertNotNull($newPage);\n\n        $this->assertStringContainsString($newPage->getUrl(), $newPage->html);\n        $this->assertStringNotContainsString($page->getUrl(), $newPage->html);\n    }\n\n    public function test_page_copy()\n    {\n        $page = $this->entities->page();\n        $page->html = '<p>This is some test content</p>';\n        $page->save();\n\n        $currentBook = $page->book;\n        $newBook = Book::where('id', '!=', $currentBook->id)->first();\n\n        $resp = $this->asEditor()->get($page->getUrl('/copy'));\n        $resp->assertSee('Copy Page');\n\n        $movePageResp = $this->post($page->getUrl('/copy'), [\n            'entity_selection' => 'book:' . $newBook->id,\n            'name'             => 'My copied test page',\n        ]);\n        $pageCopy = Page::where('name', '=', 'My copied test page')->first();\n\n        $movePageResp->assertRedirect($pageCopy->getUrl());\n        $this->assertTrue($pageCopy->book->id == $newBook->id, 'Page was copied to correct book');\n        $this->assertStringContainsString('This is some test content', $pageCopy->html);\n    }\n\n    public function test_page_copy_with_markdown_has_both_html_and_markdown()\n    {\n        $page = $this->entities->page();\n        $page->html = '<h1>This is some test content</h1>';\n        $page->markdown = '# This is some test content';\n        $page->save();\n        $newBook = Book::where('id', '!=', $page->book->id)->first();\n\n        $this->asEditor()->post($page->getUrl('/copy'), [\n            'entity_selection' => 'book:' . $newBook->id,\n            'name'             => 'My copied test page',\n        ]);\n        $pageCopy = Page::where('name', '=', 'My copied test page')->first();\n\n        $this->assertStringContainsString('This is some test content', $pageCopy->html);\n        $this->assertEquals('# This is some test content', $pageCopy->markdown);\n    }\n\n    public function test_page_copy_with_no_destination()\n    {\n        $page = $this->entities->page();\n        $currentBook = $page->book;\n\n        $resp = $this->asEditor()->get($page->getUrl('/copy'));\n        $resp->assertSee('Copy Page');\n\n        $movePageResp = $this->post($page->getUrl('/copy'), [\n            'name' => 'My copied test page',\n        ]);\n\n        $pageCopy = Page::where('name', '=', 'My copied test page')->first();\n\n        $movePageResp->assertRedirect($pageCopy->getUrl());\n        $this->assertTrue($pageCopy->book->id == $currentBook->id, 'Page was copied to correct book');\n        $this->assertTrue($pageCopy->id !== $page->id, 'Page copy is not the same instance');\n    }\n\n    public function test_page_can_be_copied_without_edit_permission()\n    {\n        $page = $this->entities->page();\n        $currentBook = $page->book;\n        $newBook = Book::where('id', '!=', $currentBook->id)->first();\n        $viewer = $this->users->viewer();\n\n        $resp = $this->actingAs($viewer)->get($page->getUrl());\n        $resp->assertDontSee($page->getUrl('/copy'));\n\n        $newBook->owned_by = $viewer->id;\n        $newBook->save();\n        $this->permissions->grantUserRolePermissions($viewer, ['page-create-own']);\n        $this->permissions->regenerateForEntity($newBook);\n\n        $resp = $this->actingAs($viewer)->get($page->getUrl());\n        $resp->assertSee($page->getUrl('/copy'));\n\n        $movePageResp = $this->post($page->getUrl('/copy'), [\n            'entity_selection' => 'book:' . $newBook->id,\n            'name'             => 'My copied test page',\n        ]);\n        $movePageResp->assertRedirect();\n\n        $this->assertDatabaseHasEntityData('page', [\n            'name'       => 'My copied test page',\n            'created_by' => $viewer->id,\n            'book_id'    => $newBook->id,\n        ]);\n    }\n}\n"
  },
  {
    "path": "tests/Entity/DefaultTemplateTest.php",
    "content": "<?php\n\nnamespace Tests\\Entity;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Page;\nuse Tests\\TestCase;\n\nclass DefaultTemplateTest extends TestCase\n{\n    public function test_creating_book_with_default_template()\n    {\n        $templatePage = $this->entities->templatePage();\n        $details = [\n            'name' => 'My book with default template',\n            'default_template_id' => $templatePage->id,\n        ];\n\n        $this->asEditor()->post('/books', $details);\n        $this->assertDatabaseHasEntityData('book', $details);\n    }\n\n    public function test_creating_chapter_with_default_template()\n    {\n        $templatePage = $this->entities->templatePage();\n        $book = $this->entities->book();\n        $details = [\n            'name' => 'My chapter with default template',\n            'default_template_id' => $templatePage->id,\n        ];\n\n        $this->asEditor()->post($book->getUrl('/create-chapter'), $details);\n        $this->assertDatabaseHasEntityData('chapter', $details);\n    }\n\n    public function test_updating_book_with_default_template()\n    {\n        $book = $this->entities->book();\n        $templatePage = $this->entities->templatePage();\n\n        $this->asEditor()->put($book->getUrl(), ['name' => $book->name, 'default_template_id' => strval($templatePage->id)]);\n        $this->assertDatabaseHasEntityData('book', ['id' => $book->id, 'default_template_id' => $templatePage->id]);\n\n        $this->asEditor()->put($book->getUrl(), ['name' => $book->name, 'default_template_id' => '']);\n        $this->assertDatabaseHasEntityData('book', ['id' => $book->id, 'default_template_id' => null]);\n    }\n\n    public function test_updating_chapter_with_default_template()\n    {\n        $chapter = $this->entities->chapter();\n        $templatePage = $this->entities->templatePage();\n\n        $this->asEditor()->put($chapter->getUrl(), ['name' => $chapter->name, 'default_template_id' => strval($templatePage->id)]);\n        $this->assertDatabaseHasEntityData('chapter', ['id' => $chapter->id, 'default_template_id' => $templatePage->id]);\n\n        $this->asEditor()->put($chapter->getUrl(), ['name' => $chapter->name, 'default_template_id' => '']);\n        $this->assertDatabaseHasEntityData('chapter', ['id' => $chapter->id, 'default_template_id' => null]);\n    }\n\n    public function test_default_book_template_cannot_be_set_if_not_a_template()\n    {\n        $book = $this->entities->book();\n        $page = $this->entities->page();\n        $this->assertFalse($page->template);\n\n        $this->asEditor()->put(\"/books/{$book->slug}\", ['name' => $book->name, 'default_template_id' => $page->id]);\n        $this->assertDatabaseHasEntityData('book', ['id' => $book->id, 'default_template_id' => null]);\n    }\n\n    public function test_default_chapter_template_cannot_be_set_if_not_a_template()\n    {\n        $chapter = $this->entities->chapter();\n        $page = $this->entities->page();\n        $this->assertFalse($page->template);\n\n        $this->asEditor()->put(\"/chapters/{$chapter->slug}\", ['name' => $chapter->name, 'default_template_id' => $page->id]);\n        $this->assertDatabaseHasEntityData('chapter', ['id' => $chapter->id, 'default_template_id' => null]);\n    }\n\n\n    public function test_default_book_template_cannot_be_set_if_not_have_access()\n    {\n        $book = $this->entities->book();\n        $templatePage = $this->entities->templatePage();\n        $this->permissions->disableEntityInheritedPermissions($templatePage);\n\n        $this->asEditor()->put(\"/books/{$book->slug}\", ['name' => $book->name, 'default_template_id' => $templatePage->id]);\n        $this->assertDatabaseHasEntityData('book', ['id' => $book->id, 'default_template_id' => null]);\n    }\n\n    public function test_default_chapter_template_cannot_be_set_if_not_have_access()\n    {\n        $chapter = $this->entities->chapter();\n        $templatePage = $this->entities->templatePage();\n        $this->permissions->disableEntityInheritedPermissions($templatePage);\n\n        $this->asEditor()->put(\"/chapters/{$chapter->slug}\", ['name' => $chapter->name, 'default_template_id' => $templatePage->id]);\n        $this->assertDatabaseHasEntityData('chapter', ['id' => $chapter->id, 'default_template_id' => null]);\n    }\n\n    public function test_inaccessible_book_default_template_can_be_set_if_unchanged()\n    {\n        $templatePage = $this->entities->templatePage();\n        $book = $this->bookUsingDefaultTemplate($templatePage);\n        $this->permissions->disableEntityInheritedPermissions($templatePage);\n\n        $this->asEditor()->put(\"/books/{$book->slug}\", ['name' => $book->name, 'default_template_id' => $templatePage->id]);\n        $this->assertDatabaseHasEntityData('book', ['id' => $book->id, 'default_template_id' => $templatePage->id]);\n    }\n\n    public function test_inaccessible_chapter_default_template_can_be_set_if_unchanged()\n    {\n        $templatePage = $this->entities->templatePage();\n        $chapter = $this->chapterUsingDefaultTemplate($templatePage);\n        $this->permissions->disableEntityInheritedPermissions($templatePage);\n\n        $this->asEditor()->put(\"/chapters/{$chapter->slug}\", ['name' => $chapter->name, 'default_template_id' => $templatePage->id]);\n        $this->assertDatabaseHasEntityData('chapter', ['id' => $chapter->id, 'default_template_id' => $templatePage->id]);\n    }\n\n    public function test_default_page_template_option_shows_on_book_form()\n    {\n        $templatePage = $this->entities->templatePage();\n        $book = $this->bookUsingDefaultTemplate($templatePage);\n\n        $resp = $this->asEditor()->get($book->getUrl('/edit'));\n        $this->withHtml($resp)->assertElementExists('input[name=\"default_template_id\"][value=\"' . $templatePage->id . '\"]');\n    }\n\n    public function test_default_page_template_option_shows_on_chapter_form()\n    {\n        $templatePage = $this->entities->templatePage();\n        $chapter = $this->chapterUsingDefaultTemplate($templatePage);\n\n        $resp = $this->asEditor()->get($chapter->getUrl('/edit'));\n        $this->withHtml($resp)->assertElementExists('input[name=\"default_template_id\"][value=\"' . $templatePage->id . '\"]');\n    }\n\n    public function test_book_default_page_template_option_only_shows_template_name_if_visible()\n    {\n        $templatePage = $this->entities->templatePage();\n        $book = $this->bookUsingDefaultTemplate($templatePage);\n\n        $resp = $this->asEditor()->get($book->getUrl('/edit'));\n        $this->withHtml($resp)->assertElementContains('#template-control a.text-page', \"#{$templatePage->id}, {$templatePage->name}\");\n\n        $this->permissions->disableEntityInheritedPermissions($templatePage);\n\n        $resp = $this->asEditor()->get($book->getUrl('/edit'));\n        $this->withHtml($resp)->assertElementNotContains('#template-control a.text-page', \"#{$templatePage->id}, {$templatePage->name}\");\n        $this->withHtml($resp)->assertElementContains('#template-control a.text-page', \"#{$templatePage->id}\");\n    }\n\n    public function test_chapter_default_page_template_option_only_shows_template_name_if_visible()\n    {\n        $templatePage = $this->entities->templatePage();\n        $chapter = $this->chapterUsingDefaultTemplate($templatePage);\n\n        $resp = $this->asEditor()->get($chapter->getUrl('/edit'));\n        $this->withHtml($resp)->assertElementContains('#template-control a.text-page', \"#{$templatePage->id}, {$templatePage->name}\");\n\n        $this->permissions->disableEntityInheritedPermissions($templatePage);\n\n        $resp = $this->asEditor()->get($chapter->getUrl('/edit'));\n        $this->withHtml($resp)->assertElementNotContains('#template-control a.text-page', \"#{$templatePage->id}, {$templatePage->name}\");\n        $this->withHtml($resp)->assertElementContains('#template-control a.text-page', \"#{$templatePage->id}\");\n    }\n\n    public function test_creating_book_page_uses_book_default_template()\n    {\n        $templatePage = $this->entities->templatePage();\n        $templatePage->forceFill(['html' => '<p>My template page</p>', 'markdown' => '# My template page'])->save();\n        $book = $this->bookUsingDefaultTemplate($templatePage);\n\n        $this->asEditor()->get($book->getUrl('/create-page'))->assertRedirect();\n        $latestPage = $book->pages()\n            ->where('draft', '=', true)\n            ->where('template', '=', false)\n            ->latest()->first();\n\n        $this->assertEquals('<p>My template page</p>', $latestPage->html);\n        $this->assertEquals('# My template page', $latestPage->markdown);\n    }\n\n    public function test_creating_chapter_page_uses_chapter_default_template()\n    {\n        $templatePage = $this->entities->templatePage();\n        $templatePage->forceFill(['html' => '<p>My chapter template page</p>', 'markdown' => '# My chapter template page'])->save();\n        $chapter = $this->chapterUsingDefaultTemplate($templatePage);\n\n        $this->asEditor()->get($chapter->getUrl('/create-page'));\n        $latestPage = $chapter->pages()\n            ->where('draft', '=', true)\n            ->where('template', '=', false)\n            ->latest()->first();\n\n        $this->assertEquals('<p>My chapter template page</p>', $latestPage->html);\n        $this->assertEquals('# My chapter template page', $latestPage->markdown);\n    }\n\n    public function test_creating_chapter_page_uses_book_default_template_if_no_chapter_template_set()\n    {\n        $templatePage = $this->entities->templatePage();\n        $templatePage->forceFill(['html' => '<p>My template page in chapter</p>', 'markdown' => '# My template page in chapter'])->save();\n        $book = $this->bookUsingDefaultTemplate($templatePage);\n        $chapter = $book->chapters()->first();\n\n        $this->asEditor()->get($chapter->getUrl('/create-page'));\n        $latestPage = $chapter->pages()\n            ->where('draft', '=', true)\n            ->where('template', '=', false)\n            ->latest()->first();\n\n        $this->assertEquals('<p>My template page in chapter</p>', $latestPage->html);\n        $this->assertEquals('# My template page in chapter', $latestPage->markdown);\n    }\n\n    public function test_creating_chapter_page_uses_chapter_template_instead_of_book_template()\n    {\n        $bookTemplatePage = $this->entities->templatePage();\n        $bookTemplatePage->forceFill(['html' => '<p>My book template</p>', 'markdown' => '# My book template'])->save();\n        $book = $this->bookUsingDefaultTemplate($bookTemplatePage);\n\n        $chapterTemplatePage = $this->entities->templatePage();\n        $chapterTemplatePage->forceFill(['html' => '<p>My chapter template</p>', 'markdown' => '# My chapter template'])->save();\n        $chapter = $book->chapters()->first();\n        $chapter->default_template_id = $chapterTemplatePage->id;\n        $chapter->save();\n\n        $this->asEditor()->get($chapter->getUrl('/create-page'));\n        $latestPage = $chapter->pages()\n            ->where('draft', '=', true)\n            ->where('template', '=', false)\n            ->latest()->first();\n\n        $this->assertEquals('<p>My chapter template</p>', $latestPage->html);\n        $this->assertEquals('# My chapter template', $latestPage->markdown);\n    }\n\n    public function test_creating_page_as_guest_uses_default_template()\n    {\n        $templatePage = $this->entities->templatePage();\n        $templatePage->forceFill(['html' => '<p>My template page</p>', 'markdown' => '# My template page'])->save();\n        $book = $this->bookUsingDefaultTemplate($templatePage);\n        $chapter = $this->chapterUsingDefaultTemplate($templatePage);\n        $guest = $this->users->guest();\n\n        $this->permissions->makeAppPublic();\n        $this->permissions->grantUserRolePermissions($guest, ['page-create-all', 'page-update-all']);\n\n        $this->post($book->getUrl('/create-guest-page'), [\n            'name' => 'My guest page with template'\n        ])->assertRedirect();\n        $latestBookPage = $book->pages()\n            ->where('draft', '=', false)\n            ->where('template', '=', false)\n            ->where('created_by', '=', $guest->id)\n            ->latest()->first();\n\n        $this->assertEquals('<p>My template page</p>', $latestBookPage->html);\n        $this->assertEquals('# My template page', $latestBookPage->markdown);\n\n        $this->post($chapter->getUrl('/create-guest-page'), [\n            'name' => 'My guest page with template'\n        ]);\n        $latestChapterPage = $chapter->pages()\n            ->where('draft', '=', false)\n            ->where('template', '=', false)\n            ->where('created_by', '=', $guest->id)\n            ->latest()->first();\n\n        $this->assertEquals('<p>My template page</p>', $latestChapterPage->html);\n        $this->assertEquals('# My template page', $latestChapterPage->markdown);\n    }\n\n    public function test_templates_not_used_if_not_visible()\n    {\n        $templatePage = $this->entities->templatePage();\n        $templatePage->forceFill(['html' => '<p>My template page</p>', 'markdown' => '# My template page'])->save();\n        $book = $this->bookUsingDefaultTemplate($templatePage);\n        $chapter = $this->chapterUsingDefaultTemplate($templatePage);\n\n        $this->permissions->disableEntityInheritedPermissions($templatePage);\n\n        $this->asEditor()->get($book->getUrl('/create-page'));\n        $latestBookPage = $book->pages()\n            ->where('draft', '=', true)\n            ->where('template', '=', false)\n            ->latest()->first();\n\n        $this->assertEquals('', $latestBookPage->html);\n        $this->assertEquals('', $latestBookPage->markdown);\n\n        $this->asEditor()->get($chapter->getUrl('/create-page'));\n        $latestChapterPage = $chapter->pages()\n            ->where('draft', '=', true)\n            ->where('template', '=', false)\n            ->latest()->first();\n\n        $this->assertEquals('', $latestChapterPage->html);\n        $this->assertEquals('', $latestChapterPage->markdown);\n    }\n\n    public function test_template_page_delete_removes_template_usage()\n    {\n        $templatePage = $this->entities->templatePage();\n        $book = $this->bookUsingDefaultTemplate($templatePage);\n        $chapter = $this->chapterUsingDefaultTemplate($templatePage);\n\n        $book->refresh();\n        $this->assertEquals($templatePage->id, $book->default_template_id);\n        $this->assertEquals($templatePage->id, $chapter->default_template_id);\n\n        $this->asEditor()->delete($templatePage->getUrl());\n        $this->asAdmin()->post('/settings/recycle-bin/empty');\n\n        $book->refresh();\n        $chapter->refresh();\n        $this->assertEquals(null, $book->default_template_id);\n        $this->assertEquals(null, $chapter->default_template_id);\n    }\n\n    protected function bookUsingDefaultTemplate(Page $page): Book\n    {\n        $book = $this->entities->book();\n        $book->default_template_id = $page->id;\n        $book->save();\n\n        return $book;\n    }\n\n    protected function chapterUsingDefaultTemplate(Page $page): Chapter\n    {\n        $chapter = $this->entities->chapter();\n        $chapter->default_template_id = $page->id;\n        $chapter->save();\n\n        return $chapter;\n    }\n}\n"
  },
  {
    "path": "tests/Entity/EntityAccessTest.php",
    "content": "<?php\n\nnamespace Tests\\Entity;\n\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Users\\UserRepo;\nuse Tests\\TestCase;\n\nclass EntityAccessTest extends TestCase\n{\n    public function test_entities_viewable_after_creator_deletion()\n    {\n        // Create required assets and revisions\n        $creator = $this->users->editor();\n        $updater = $this->users->viewer();\n        $entities = $this->entities->createChainBelongingToUser($creator, $updater);\n        app()->make(UserRepo::class)->destroy($creator);\n        $this->entities->updatePage($entities['page'], ['html' => '<p>hello!</p>>']);\n\n        $this->checkEntitiesViewable($entities);\n    }\n\n    public function test_entities_viewable_after_updater_deletion()\n    {\n        // Create required assets and revisions\n        $creator = $this->users->viewer();\n        $updater = $this->users->editor();\n        $entities = $this->entities->createChainBelongingToUser($creator, $updater);\n        app()->make(UserRepo::class)->destroy($updater);\n        $this->entities->updatePage($entities['page'], ['html' => '<p>Hello there!</p>']);\n\n        $this->checkEntitiesViewable($entities);\n    }\n\n    /**\n     * @param array<string, Entity> $entities\n     */\n    private function checkEntitiesViewable(array $entities)\n    {\n        // Check pages and books are visible.\n        $this->asAdmin();\n        foreach ($entities as $entity) {\n            $this->get($entity->getUrl())\n                ->assertStatus(200)\n                ->assertSee($entity->name);\n        }\n\n        // Check revision listing shows no errors.\n        $this->get($entities['page']->getUrl('/revisions'))->assertStatus(200);\n    }\n}\n"
  },
  {
    "path": "tests/Entity/EntityQueryTest.php",
    "content": "<?php\n\nnamespace Tests\\Entity;\n\nuse BookStack\\Entities\\Models\\Book;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Tests\\TestCase;\n\nclass EntityQueryTest extends TestCase\n{\n    public function test_basic_entity_query_has_join_and_type_applied()\n    {\n        $query = Book::query();\n        $expected = 'select * from `entities` left join `entity_container_data` on `entity_container_data`.`entity_id` = `entities`.`id` and `entity_container_data`.`entity_type` = ? where `type` = ? and `entities`.`deleted_at` is null';\n        $this->assertEquals($expected, $query->toSql());\n        $this->assertEquals(['book', 'book'], $query->getBindings());\n    }\n\n    public function test_joins_in_sub_queries_use_alias_names()\n    {\n        $query = Book::query()->whereHas('chapters', function (Builder $query) {\n            $query->where('name', '=', 'a');\n        });\n\n        // Probably from type limits on relation where not needed?\n        $expected = 'select * from `entities` left join `entity_container_data` on `entity_container_data`.`entity_id` = `entities`.`id` and `entity_container_data`.`entity_type` = ? where exists (select * from `entities` as `laravel_reserved_%d` left join `entity_container_data` on `entity_container_data`.`entity_id` = `laravel_reserved_%d`.`id` and `entity_container_data`.`entity_type` = ? where `entities`.`id` = `laravel_reserved_%d`.`book_id` and `name` = ? and `type` = ? and `laravel_reserved_%d`.`deleted_at` is null) and `type` = ? and `entities`.`deleted_at` is null';\n        $this->assertStringMatchesFormat($expected, $query->toSql());\n        $this->assertEquals(['book', 'chapter', 'a', 'chapter', 'book'], $query->getBindings());\n    }\n\n    public function test_book_chapter_relation_applies_type_condition()\n    {\n        $book = $this->entities->book();\n        $query = $book->chapters();\n        $expected = 'select * from `entities` left join `entity_container_data` on `entity_container_data`.`entity_id` = `entities`.`id` and `entity_container_data`.`entity_type` = ? where `entities`.`book_id` = ? and `entities`.`book_id` is not null and `type` = ? and `entities`.`deleted_at` is null';\n        $this->assertEquals($expected, $query->toSql());\n        $this->assertEquals(['chapter', $book->id, 'chapter'], $query->getBindings());\n\n        $query = Book::query()->whereHas('chapters');\n        $expected = 'select * from `entities` left join `entity_container_data` on `entity_container_data`.`entity_id` = `entities`.`id` and `entity_container_data`.`entity_type` = ? where exists (select * from `entities` as `laravel_reserved_%d` left join `entity_container_data` on `entity_container_data`.`entity_id` = `laravel_reserved_%d`.`id` and `entity_container_data`.`entity_type` = ? where `entities`.`id` = `laravel_reserved_%d`.`book_id` and `type` = ? and `laravel_reserved_%d`.`deleted_at` is null) and `type` = ? and `entities`.`deleted_at` is null';\n        $this->assertStringMatchesFormat($expected, $query->toSql());\n        $this->assertEquals(['book', 'chapter', 'chapter', 'book'], $query->getBindings());\n    }\n}\n"
  },
  {
    "path": "tests/Entity/MarkdownToHtmlTest.php",
    "content": "<?php\n\nnamespace Tests\\Entity;\n\nuse BookStack\\Entities\\Tools\\Markdown\\HtmlToMarkdown;\nuse Tests\\TestCase;\n\nclass MarkdownToHtmlTest extends TestCase\n{\n    public function test_basic_formatting_conversion()\n    {\n        $this->assertConversion(\n            '<h1>Dogcat</h1><p>Some <strong>bold</strong> text</p>',\n            \"# Dogcat\\n\\nSome **bold** text\"\n        );\n    }\n\n    public function test_callouts_remain_html()\n    {\n        $this->assertConversion(\n            '<h1>Dogcat</h1><p class=\"callout info\">Some callout text</p><p>Another line</p>',\n            \"# Dogcat\\n\\n<p class=\\\"callout info\\\">Some callout text</p>\\n\\nAnother line\"\n        );\n    }\n\n    public function test_wysiwyg_code_format_handled_cleanly()\n    {\n        $this->assertConversion(\n            '<h1>Dogcat</h1>' . \"\\r\\n\" . '<pre id=\"bkmrk-var-a-%3D-%27cat%27%3B\"><code class=\"language-JavaScript\">var a = \\'cat\\';</code></pre><p>Another line</p>',\n            \"# Dogcat\\n\\n```JavaScript\\nvar a = 'cat';\\n```\\n\\nAnother line\"\n        );\n    }\n\n    public function test_tasklist_checkboxes_are_handled()\n    {\n        $this->assertConversion(\n            '<ul><li><input type=\"checkbox\" checked=\"checked\">Item A</li><li><input type=\"checkbox\">Item B</li></ul>',\n            \"- [x] Item A\\n- [ ] Item B\"\n        );\n    }\n\n    public function test_drawing_blocks_remain_html()\n    {\n        $this->assertConversion(\n            '<div drawio-diagram=\"190\" id=\"bkmrk--0\" contenteditable=\"false\"><img src=\"http://example.com/uploads/images/drawio/2022-04/drawing-1.png\" alt=\"\" /></div>Some text',\n            '<div drawio-diagram=\"190\"><img src=\"http://example.com/uploads/images/drawio/2022-04/drawing-1.png\" alt=\"\"/></div>' . \"\\n\\nSome text\"\n        );\n    }\n\n    public function test_summary_tags_have_newlines_after_to_separate_content()\n    {\n        $this->assertConversion(\n            '<details><summary>Toggle</summary><p>Test</p></details>',\n            \"<details><summary>Toggle</summary>\\n\\nTest\\n\\n</details>\"\n        );\n    }\n\n    public function test_iframes_tags_have_newlines_after_to_separate_content()\n    {\n        $this->assertConversion(\n            '<iframe src=\"https://example.com\"></iframe><p>Beans</p>',\n            \"<iframe src=\\\"https://example.com\\\"></iframe>\\n\\nBeans\"\n        );\n    }\n\n    protected function assertConversion(string $html, string $expectedMarkdown, bool $partialMdMatch = false)\n    {\n        $markdown = (new HtmlToMarkdown($html))->convert();\n\n        if ($partialMdMatch) {\n            static::assertStringContainsString($expectedMarkdown, $markdown);\n        } else {\n            static::assertEquals($expectedMarkdown, $markdown);\n        }\n    }\n}\n"
  },
  {
    "path": "tests/Entity/PageContentFilteringTest.php",
    "content": "<?php\n\nnamespace Tests\\Entity;\n\nuse Tests\\TestCase;\n\nclass PageContentFilteringTest extends TestCase\n{\n    public function test_page_content_scripts_removed_by_default()\n    {\n        $this->asEditor();\n        $page = $this->entities->page();\n        $script = 'abc123<script>console.log(\"hello-test\")</script>abc123';\n        $page->html = \"escape {$script}\";\n        $page->save();\n\n        $pageView = $this->get($page->getUrl());\n        $pageView->assertStatus(200);\n        $pageView->assertDontSee($script, false);\n        $pageView->assertSee('abc123abc123');\n    }\n\n    public function test_more_complex_content_script_escaping_scenarios()\n    {\n        config()->set('app.content_filtering', 'j');\n\n        $checks = [\n            \"<p>Some script</p><script>alert('cat')</script>\",\n            \"<div><div><div><div><p>Some script</p><script>alert('cat')</script></div></div></div></div>\",\n            \"<p>Some script<script>alert('cat')</script></p>\",\n            \"<p>Some script <div><script>alert('cat')</script></div></p>\",\n            \"<p>Some script <script><div>alert('cat')</script></div></p>\",\n            \"<p>Some script <script><div>alert('cat')</script><script><div>alert('cat')</script></p><script><div>alert('cat')</script>\",\n        ];\n\n        $this->asEditor();\n        $page = $this->entities->page();\n\n        foreach ($checks as $check) {\n            $page->html = $check;\n            $page->save();\n\n            $pageView = $this->get($page->getUrl());\n            $pageView->assertStatus(200);\n            $this->withHtml($pageView)->assertElementNotContains('.page-content', '<script>');\n            $this->withHtml($pageView)->assertElementNotContains('.page-content', '</script>');\n        }\n    }\n\n    public function test_js_and_base64_src_urls_are_removed()\n    {\n        config()->set('app.content_filtering', 'j');\n\n        $checks = [\n            '<iframe src=\"javascript:alert(document.cookie)\"></iframe>',\n            '<iframe src=\"JavAScRipT:alert(document.cookie)\"></iframe>',\n            '<iframe src=\"JavAScRipT:alert(document.cookie)\"></iframe>',\n            '<iframe SRC=\" javascript: alert(document.cookie)\"></iframe>',\n            '<iframe src=\"data:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg==\" frameborder=\"0\"></iframe>',\n            '<iframe src=\"DaTa:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg==\" frameborder=\"0\"></iframe>',\n            '<iframe src=\" data:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg==\" frameborder=\"0\"></iframe>',\n            '<img src=\"javascript:alert(document.cookie)\"/>',\n            '<img src=\"JavAScRipT:alert(document.cookie)\"/>',\n            '<img src=\"JavAScRipT:alert(document.cookie)\"/>',\n            '<img SRC=\" javascript: alert(document.cookie)\"/>',\n            '<img src=\"data:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg==\"/>',\n            '<img src=\"DaTa:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg==\"/>',\n            '<img src=\" data:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg==\"/>',\n            '<iframe srcdoc=\"<script>window.alert(document.cookie)</script>\"></iframe>',\n            '<iframe SRCdoc=\"<script>window.alert(document.cookie)</script>\"></iframe>',\n            '<IMG SRC=`javascript:alert(\"RSnake says, \\'XSS\\'\")`>',\n            '<object data=\"javascript:alert(document.cookie)\"></object>',\n            '<object data=\"JavAScRipT:alert(document.cookie)\"></object>',\n            '<object data=\"JavAScRipT:alert(document.cookie)\"></object>',\n            '<object SRC=\" javascript: alert(document.cookie)\"></object>',\n            '<object data=\"data:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg==\" frameborder=\"0\"></object>',\n            '<object data=\"DaTa:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg==\" frameborder=\"0\"></object>',\n            '<object data=\" data:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg==\" frameborder=\"0\"></object>',\n            '<embed src=\"javascript:alert(document.cookie)\"/>',\n            '<embed src=\"JavAScRipT:alert(document.cookie)\"/>',\n            '<embed src=\"JavAScRipT:alert(document.cookie)\"/>',\n            '<embed SRC=\" javascript: alert(document.cookie)\"/>',\n            '<embed src=\"data:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg==\"/>',\n            '<embed src=\"DaTa:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg==\"/>',\n            '<embed src=\" data:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg==\"/>',\n        ];\n\n        $this->asEditor();\n        $page = $this->entities->page();\n\n        foreach ($checks as $check) {\n            $page->html = $check;\n            $page->save();\n\n            $pageView = $this->get($page->getUrl());\n            $pageView->assertStatus(200);\n            $html = $this->withHtml($pageView);\n            $html->assertElementNotContains('.page-content', '<object');\n            $html->assertElementNotContains('.page-content', 'data=');\n            $html->assertElementNotContains('.page-content', '<iframe>');\n            $html->assertElementNotContains('.page-content', '<img');\n            $html->assertElementNotContains('.page-content', '</iframe>');\n            $html->assertElementNotContains('.page-content', 'src=');\n            $html->assertElementNotContains('.page-content', 'javascript:');\n            $html->assertElementNotContains('.page-content', 'data:');\n            $html->assertElementNotContains('.page-content', 'base64');\n        }\n    }\n\n    public function test_javascript_uri_links_are_removed()\n    {\n        config()->set('app.content_filtering', 'j');\n\n        $checks = [\n            '<a id=\"xss\" href=\"javascript:alert(document.cookie)>Click me</a>',\n            '<a id=\"xss\" href=\"javascript: alert(document.cookie)>Click me</a>',\n            '<a id=\"xss\" href=\"JaVaScRiPt: alert(document.cookie)>Click me</a>',\n            '<a id=\"xss\" href=\" JaVaScRiPt: alert(document.cookie)>Click me</a>',\n        ];\n\n        $this->asEditor();\n        $page = $this->entities->page();\n\n        foreach ($checks as $check) {\n            $page->html = $check;\n            $page->save();\n\n            $pageView = $this->get($page->getUrl());\n            $pageView->assertStatus(200);\n            $this->withHtml($pageView)->assertElementNotContains('.page-content', '<a id=\"xss\"');\n            $this->withHtml($pageView)->assertElementNotContains('.page-content', 'href=javascript:');\n        }\n    }\n\n    public function test_form_filtering_is_controlled_by_config()\n    {\n        config()->set('app.content_filtering', '');\n        $page = $this->entities->page();\n        $page->html = '<form><input type=\"text\" id=\"dont-see-this\" value=\"test\"></form>';\n        $page->save();\n\n        $this->asEditor()->get($page->getUrl())->assertSee('dont-see-this', false);\n\n        config()->set('app.content_filtering', 'f');\n        $this->get($page->getUrl())->assertDontSee('dont-see-this', false);\n    }\n\n    public function test_form_actions_with_javascript_are_removed()\n    {\n        config()->set('app.content_filtering', 'j');\n\n        $checks = [\n            '<customform><custominput id=\"xss\" type=submit formaction=javascript:alert(document.domain) value=Submit><custominput></customform>',\n            '<customform ><custombutton id=\"xss\" formaction=\"JaVaScRiPt:alert(document.domain)\">Click me</custombutton></customform>',\n            '<customform ><custombutton id=\"xss\" formaction=javascript:alert(document.domain)>Click me</custombutton></customform>',\n            '<customform id=\"xss\" action=javascript:alert(document.domain)><input type=submit value=Submit></customform>',\n            '<customform id=\"xss\" action=\"JaVaScRiPt:alert(document.domain)\"><input type=submit value=Submit></customform>',\n        ];\n\n        $this->asEditor();\n        $page = $this->entities->page();\n\n        foreach ($checks as $check) {\n            $page->html = $check;\n            $page->save();\n\n            $pageView = $this->get($page->getUrl());\n            $pageView->assertStatus(200);\n            $pageView->assertDontSee('id=\"xss\"', false);\n            $pageView->assertDontSee('action=javascript:', false);\n            $pageView->assertDontSee('action=JaVaScRiPt:', false);\n            $pageView->assertDontSee('formaction=javascript:', false);\n            $pageView->assertDontSee('formaction=JaVaScRiPt:', false);\n        }\n    }\n\n    public function test_form_elements_are_removed()\n    {\n        config()->set('app.content_filtering', 'f');\n\n        $checks = [\n            '<p>thisisacattofind</p><form>thisdogshouldnotbefound</form>',\n            '<p>thisisacattofind</p><input type=\"text\" value=\"thisdogshouldnotbefound\">',\n            '<p>thisisacattofind</p><select><option>thisdogshouldnotbefound</option></select>',\n            '<p>thisisacattofind</p><textarea>thisdogshouldnotbefound</textarea>',\n            '<p>thisisacattofind</p><fieldset>thisdogshouldnotbefound</fieldset>',\n            '<p>thisisacattofind</p><button>thisdogshouldnotbefound</button>',\n            '<p>thisisacattofind</p><BUTTON>thisdogshouldnotbefound</BUTTON>',\n            <<<'TESTCASE'\n<svg width=\"200\" height=\"100\" xmlns=\"http://www.w3.org/2000/svg\">\n  <foreignObject width=\"100%\" height=\"100%\">\n    \n    <body xmlns=\"http://www.w3.org/1999/xhtml\">\n    <p>thisisacattofind</p>\n      <form>\n        <p>thisdogshouldnotbefound</p>\n      </form>\n      <input type=\"text\" placeholder=\"thisdogshouldnotbefound\" />\n      <button type=\"submit\">thisdogshouldnotbefound</button>\n    </body>\n\n  </foreignObject>\n</svg>\nTESTCASE\n\n        ];\n\n        $this->asEditor();\n        $page = $this->entities->page();\n\n        foreach ($checks as $check) {\n            $page->html = $check;\n            $page->save();\n\n            $pageView = $this->get($page->getUrl());\n            $pageView->assertStatus(200);\n            $pageView->assertSee('thisisacattofind');\n            $pageView->assertDontSee('thisdogshouldnotbefound');\n        }\n    }\n\n    public function test_form_attributes_are_removed()\n    {\n        config()->set('app.content_filtering', 'f');\n\n        $withinSvgSample = <<<'TESTCASE'\n<svg width=\"200\" height=\"100\" xmlns=\"http://www.w3.org/2000/svg\">\n  <foreignObject width=\"100%\" height=\"100%\">\n    \n    <body xmlns=\"http://www.w3.org/1999/xhtml\">\n    <p formaction=\"a\">thisisacattofind</p>\n    <p formaction=\"a\">thisisacattofind</p>\n    </body>\n\n  </foreignObject>\n</svg>\nTESTCASE;\n\n        $checks = [\n            'formaction' => '<p formaction=\"a\">thisisacattofind</p>',\n            'form' => '<p form=\"a\">thisisacattofind</p>',\n            'formmethod' => '<p formmethod=\"a\">thisisacattofind</p>',\n            'formtarget' => '<p formtarget=\"a\">thisisacattofind</p>',\n            'FORMTARGET' => '<p FORMTARGET=\"a\">thisisacattofind</p>',\n        ];\n\n        $this->asEditor();\n        $page = $this->entities->page();\n\n        foreach ($checks as $attribute => $check) {\n            $page->html = $check;\n            $page->save();\n\n            $pageView = $this->get($page->getUrl());\n            $pageView->assertStatus(200);\n            $pageView->assertSee('thisisacattofind');\n            $this->withHtml($pageView)->assertElementNotExists(\".page-content [{$attribute}]\");\n        }\n\n        $page->html = $withinSvgSample;\n        $page->save();\n        $pageView = $this->get($page->getUrl());\n        $pageView->assertStatus(200);\n        $html = $this->withHtml($pageView);\n        foreach ($checks as $attribute => $check) {\n            $pageView->assertSee('thisisacattofind');\n            $html->assertElementNotExists(\".page-content [{$attribute}]\");\n        }\n    }\n\n    public function test_metadata_redirects_are_removed()\n    {\n        config()->set('app.content_filtering', 'h');\n\n        $checks = [\n            '<meta http-equiv=\"refresh\" content=\"0; url=//external_url\">',\n            '<meta http-equiv=\"refresh\" ConTeNt=\"0; url=//external_url\">',\n            '<meta http-equiv=\"refresh\" content=\"0; UrL=//external_url\">',\n        ];\n\n        $this->asEditor();\n        $page = $this->entities->page();\n\n        foreach ($checks as $check) {\n            $page->html = $check;\n            $page->save();\n\n            $pageView = $this->get($page->getUrl());\n            $pageView->assertStatus(200);\n            $this->withHtml($pageView)->assertElementNotContains('.page-content', '<meta>');\n            $this->withHtml($pageView)->assertElementNotContains('.page-content', '</meta>');\n            $this->withHtml($pageView)->assertElementNotContains('.page-content', 'content=');\n            $this->withHtml($pageView)->assertElementNotContains('.page-content', 'external_url');\n        }\n    }\n\n    public function test_page_inline_on_attributes_removed_by_default()\n    {\n        config()->set('app.content_filtering', 'j');\n\n        $this->asEditor();\n        $page = $this->entities->page();\n        $script = '<p onmouseenter=\"console.log(\\'test\\')\">Hello</p>';\n        $page->html = \"escape {$script}\";\n        $page->save();\n\n        $pageView = $this->get($page->getUrl());\n        $pageView->assertStatus(200);\n        $pageView->assertDontSee($script, false);\n        $pageView->assertSee('<p>Hello</p>', false);\n    }\n\n    public function test_more_complex_inline_on_attributes_escaping_scenarios()\n    {\n        config()->set('app.content_filtering', 'j');\n\n        $checks = [\n            '<p onclick=\"console.log(\\'test\\')\">Hello</p>',\n            '<p OnCliCk=\"console.log(\\'test\\')\">Hello</p>',\n            '<div>Lorem ipsum dolor sit amet.</div><p onclick=\"console.log(\\'test\\')\">Hello</p>',\n            '<div>Lorem ipsum dolor sit amet.<p onclick=\"console.log(\\'test\\')\">Hello</p></div>',\n            '<div><div><div><div>Lorem ipsum dolor sit amet.<p onclick=\"console.log(\\'test\\')\">Hello</p></div></div></div></div>',\n            '<div onclick=\"console.log(\\'test\\')\">Lorem ipsum dolor sit amet.</div><p onclick=\"console.log(\\'test\\')\">Hello</p><div></div>',\n            '<a a=\"<img src=1 onerror=\\'alert(1)\\'> ',\n            '\\<a onclick=\"alert(document.cookie)\"\\>xss link\\</a\\>',\n        ];\n\n        $this->asEditor();\n        $page = $this->entities->page();\n\n        foreach ($checks as $check) {\n            $page->html = $check;\n            $page->save();\n\n            $pageView = $this->get($page->getUrl());\n            $pageView->assertStatus(200);\n            $this->withHtml($pageView)->assertElementNotContains('.page-content', 'onclick');\n        }\n    }\n\n    public function test_page_content_scripts_show_with_filters_disabled()\n    {\n        $this->asEditor();\n        $page = $this->entities->page();\n        config()->set('app.content_filtering', '');\n\n        $script = 'abc123<script>console.log(\"hello-test\")</script>abc123';\n        $page->html = \"no escape {$script}\";\n        $page->save();\n\n        $pageView = $this->get($page->getUrl());\n        $pageView->assertSee($script, false);\n        $pageView->assertDontSee('abc123abc123');\n    }\n\n    public function test_svg_script_usage_is_removed()\n    {\n        config()->set('app.content_filtering', 'j');\n\n        $checks = [\n            '<svg id=\"test\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"100\" height=\"100\"><a xlink:href=\"javascript:alert(document.domain)\"><rect x=\"0\" y=\"0\" width=\"100\" height=\"100\" /></a></svg>',\n            '<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"><use xlink:href=\"data:application/xml;base64 ,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj4KPGRlZnM+CjxjaXJjbGUgaWQ9InRlc3QiIHI9IjAiIGN4PSIwIiBjeT0iMCIgc3R5bGU9ImZpbGw6ICNGMDAiPgo8c2V0IGF0dHJpYnV0ZU5hbWU9ImZpbGwiIGF0dHJpYnV0ZVR5cGU9IkNTUyIgb25iZWdpbj0nYWxlcnQoZG9jdW1lbnQuZG9tYWluKScKb25lbmQ9J2FsZXJ0KCJvbmVuZCIpJyB0bz0iIzAwRiIgYmVnaW49IjBzIiBkdXI9Ijk5OXMiIC8+CjwvY2lyY2xlPgo8L2RlZnM+Cjx1c2UgeGxpbms6aHJlZj0iI3Rlc3QiLz4KPC9zdmc+#test\"/></svg>',\n            '<svg><animate href=#xss attributeName=href values=javascript:alert(1) /></svg>',\n            '<svg><animate href=\"#xss\" attributeName=\"href\" values=\"a;javascript:alert(1)\" /></svg>',\n            '<svg><animate href=\"#xss\" attributeName=\"href\" values=\"a;data:alert(1)\" /></svg>',\n            '<svg><animate href=#xss attributeName=href from=javascript:alert(1) to=1 /><a id=xss><text x=20 y=20>XSS</text></a>',\n            '<svg><set href=#xss attributeName=href from=? to=javascript:alert(1) /><a id=xss><text x=20 y=20>XSS</text></a>',\n            '<svg><g><g><g><animate href=#xss attributeName=href values=javascript:alert(1) /></g></g></g></svg>',\n        ];\n\n        $this->asEditor();\n        $page = $this->entities->page();\n\n        foreach ($checks as $check) {\n            $page->html = $check;\n            $page->save();\n\n            $pageView = $this->get($page->getUrl());\n            $pageView->assertStatus(200);\n            $html = $this->withHtml($pageView);\n            $html->assertElementNotContains('.page-content', 'alert');\n            $html->assertElementNotContains('.page-content', 'xlink:href');\n            $html->assertElementNotContains('.page-content', 'application/xml');\n            $html->assertElementNotContains('.page-content', 'javascript');\n        }\n    }\n\n    public function test_page_inline_on_attributes_show_with_filters_disabled()\n    {\n        $this->asEditor();\n        $page = $this->entities->page();\n        config()->set('app.content_filtering', '');\n\n        $script = '<p onmouseenter=\"console.log(\\'test\\')\">Hello</p>';\n        $page->html = \"escape {$script}\";\n        $page->save();\n\n        $pageView = $this->get($page->getUrl());\n        $pageView->assertSee($script, false);\n        $pageView->assertDontSee('<p>Hello</p>', false);\n    }\n\n    public function test_non_content_filtering_is_controlled_by_config()\n    {\n        config()->set('app.content_filtering', '');\n        $page = $this->entities->page();\n        $html = <<<'HTML'\n<style>superbeans!</style>\n<template id=\"template\">superbeans!</template>\nHTML;\n        $page->html = $html;\n        $page->save();\n\n        $resp = $this->asEditor()->get($page->getUrl());\n        $resp->assertSee('superbeans', false);\n\n        config()->set('app.content_filtering', 'h');\n\n        $resp = $this->asEditor()->get($page->getUrl());\n        $resp->assertDontSee('superbeans', false);\n    }\n\n    public function test_non_content_filtering()\n    {\n        config()->set('app.content_filtering', 'h');\n        $page = $this->entities->page();\n        $html = <<<'HTML'\n<style>superbeans!</style>\n<p>inbetweenpsection</p>\n<link rel=\"stylesheet\" href=\"https://example.com/superbeans.css\">\n<meta name=\"description\" content=\"superbeans!\">\n<title>superbeans!</title>\n<template id=\"template\">superbeans!</template>\nHTML;\n\n        $page->html = $html;\n        $page->save();\n\n        $resp = $this->asEditor()->get($page->getUrl());\n        $resp->assertDontSee('superbeans', false);\n        $resp->assertSee('inbetweenpsection', false);\n    }\n\n    public function test_allow_list_filtering_is_controlled_by_config()\n    {\n        config()->set('app.content_filtering', '');\n        $page = $this->entities->page();\n        $page->html = '<div style=\"position: absolute; left: 0;color:#00FFEE;\">Hello!</div>';\n        $page->save();\n\n        $resp = $this->asEditor()->get($page->getUrl());\n        $resp->assertSee('style=\"position: absolute; left: 0;color:#00FFEE;\"', false);\n\n        config()->set('app.content_filtering', 'a');\n        $resp = $this->get($page->getUrl());\n        $resp->assertDontSee('style=\"position: absolute; left: 0;color:#00FFEE;\"', false);\n        $resp->assertSee('style=\"color:#00FFEE;\"', false);\n    }\n\n    public function test_allow_list_style_filtering()\n    {\n        $testCasesExpectedByInput = [\n            '<div style=\"position:absolute;left:0;color:#00FFEE;\">Hello!</div>' => '<div style=\"color:#00FFEE;\">Hello!</div>',\n            '<div style=\"background:#FF0000;left:0;color:#00FFEE;\">Hello!</div>' => '<div style=\"background:#FF0000;color:#00FFEE;\">Hello!</div>',\n            '<div style=\"color:#00FFEE;\">Hello!<style>testinghello!</style></div>' => '<div style=\"color:#00FFEE;\">Hello!</div>',\n            '<div drawio-diagram=\"5332\" another-attr=\"cat\">Hello!</div>' => '<div drawio-diagram=\"5332\">Hello!</div>',\n        ];\n\n        config()->set('app.content_filtering', 'a');\n        $page = $this->entities->page();\n        $this->asEditor();\n\n        foreach ($testCasesExpectedByInput as $input => $expected) {\n            $page->html = $input;\n            $page->save();\n            $resp = $this->get($page->getUrl());\n\n            $resp->assertSee($expected, false);\n        }\n    }\n\n    public function test_allow_list_does_not_filter_cases()\n    {\n        $testCasesExpectedByInput = [\n            '<p><a href=\"https://example.com\" target=\"_blank\">New tab linkydoodle</a></p>',\n            '<p><a href=\"https://example.com/user/1\" data-mention-user-id=\"5\">@mentionusertext</a></p>',\n            '<details><summary>Hello</summary><p>Mydetailshere</p></details>',\n        ];\n\n        config()->set('app.content_filtering', 'a');\n        $page = $this->entities->page();\n        $this->asEditor();\n\n        foreach ($testCasesExpectedByInput as $input) {\n            $page->html = $input;\n            $page->save();\n            $resp = $this->get($page->getUrl());\n\n            $resp->assertSee($input, false);\n        }\n    }\n}\n"
  },
  {
    "path": "tests/Entity/PageContentTest.php",
    "content": "<?php\n\nnamespace Tests\\Entity;\n\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Tools\\PageContent;\nuse Tests\\TestCase;\n\nclass PageContentTest extends TestCase\n{\n    protected string $base64Jpeg = '/9j/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k=';\n\n    public function test_page_includes()\n    {\n        $page = $this->entities->page();\n        $secondPage = $this->entities->page();\n\n        $secondPage->html = \"<p id='section1'>Hello, This is a test</p><p id='section2'>This is a second block of content</p>\";\n        $secondPage->save();\n\n        $this->asEditor();\n\n        $pageContent = $this->get($page->getUrl());\n        $pageContent->assertDontSee('Hello, This is a test');\n\n        $originalHtml = $page->html;\n        $page->html .= \"{{@{$secondPage->id}}}\";\n        $page->save();\n\n        $pageContent = $this->get($page->getUrl());\n        $pageContent->assertSee('Hello, This is a test');\n        $pageContent->assertSee('This is a second block of content');\n\n        $page->html = $originalHtml . \" Well {{@{$secondPage->id}#section2}}\";\n        $page->save();\n\n        $pageContent = $this->get($page->getUrl());\n        $pageContent->assertDontSee('Hello, This is a test');\n        $pageContent->assertSee('Well This is a second block of content');\n    }\n\n    public function test_saving_page_with_includes()\n    {\n        $page = $this->entities->page();\n        $secondPage = $this->entities->page();\n\n        $this->asEditor();\n        $includeTag = '{{@' . $secondPage->id . '}}';\n        $page->html = '<p>' . $includeTag . '</p>';\n\n        $resp = $this->put($page->getUrl(), ['name' => $page->name, 'html' => $page->html, 'summary' => '']);\n\n        $resp->assertStatus(302);\n\n        $page = Page::find($page->id);\n        $this->assertStringContainsString($includeTag, $page->html);\n        $this->assertEquals('', $page->text);\n    }\n\n    public function test_page_includes_rendered_on_book_export()\n    {\n        $page = $this->entities->page();\n        $secondPage = Page::query()\n            ->where('book_id', '!=', $page->book_id)\n            ->first();\n\n        $content = '<p id=\"bkmrk-meow\">my cat is awesome and scratchy</p>';\n        $secondPage->html = $content;\n        $secondPage->save();\n\n        $page->html = \"{{@{$secondPage->id}#bkmrk-meow}}\";\n        $page->save();\n\n        $this->asEditor();\n        $htmlContent = $this->get($page->book->getUrl('/export/html'));\n        $htmlContent->assertSee('my cat is awesome and scratchy');\n    }\n\n    public function test_page_includes_can_be_nested_up_to_three_times()\n    {\n        $page = $this->entities->page();\n        $tag = \"{{@{$page->id}#bkmrk-test}}\";\n        $page->html = '<p id=\"bkmrk-test\">Hello Barry ' . $tag . '</p>';\n        $page->save();\n\n        $pageResp = $this->asEditor()->get($page->getUrl());\n        $this->withHtml($pageResp)->assertElementContains('#bkmrk-test', 'Hello Barry Hello Barry Hello Barry Hello Barry ' . $tag);\n        $this->withHtml($pageResp)->assertElementNotContains('#bkmrk-test', 'Hello Barry Hello Barry Hello Barry Hello Barry Hello Barry ' . $tag);\n    }\n\n    public function test_page_includes_to_nonexisting_pages_does_not_error()\n    {\n        $page = $this->entities->page();\n        $missingId = Page::query()->max('id') + 1;\n        $tag = \"{{@{$missingId}}}\";\n        $page->html = '<p id=\"bkmrk-test\">Hello Barry ' . $tag . '</p>';\n        $page->save();\n\n        $pageResp = $this->asEditor()->get($page->getUrl());\n        $pageResp->assertOk();\n        $pageResp->assertSee('Hello Barry');\n    }\n\n    public function test_duplicate_ids_does_not_break_page_render()\n    {\n        $this->asEditor();\n        $pageA = Page::query()->first();\n        $pageB = Page::query()->where('id', '!=', $pageA->id)->first();\n\n        $content = '<ul id=\"bkmrk-xxx-%28\"></ul> <ul id=\"bkmrk-xxx-%28\"></ul>';\n        $pageA->html = $content;\n        $pageA->save();\n\n        $pageB->html = '<ul id=\"bkmrk-xxx-%28\"></ul> <p>{{@' . $pageA->id . '#test}}</p>';\n        $pageB->save();\n\n        $pageView = $this->get($pageB->getUrl());\n        $pageView->assertSuccessful();\n    }\n\n    public function test_duplicate_ids_fixed_on_page_save()\n    {\n        $this->asEditor();\n        $page = $this->entities->page();\n\n        $content = '<ul id=\"bkmrk-test\"><li>test a</li><li><ul id=\"bkmrk-test\"><li>test b</li></ul></li></ul>';\n        $pageSave = $this->put($page->getUrl(), [\n            'name'    => $page->name,\n            'html'    => $content,\n            'summary' => '',\n        ]);\n        $pageSave->assertRedirect();\n\n        $updatedPage = Page::query()->where('id', '=', $page->id)->first();\n        $this->assertEquals(substr_count($updatedPage->html, 'bkmrk-test\"'), 1);\n    }\n\n    public function test_anchors_referencing_non_bkmrk_ids_rewritten_after_save()\n    {\n        $this->asEditor();\n        $page = $this->entities->page();\n\n        $content = '<h1 id=\"non-standard-id\">test</h1><p><a href=\"#non-standard-id\">link</a></p>';\n        $this->put($page->getUrl(), [\n            'name'    => $page->name,\n            'html'    => $content,\n            'summary' => '',\n        ]);\n\n        $updatedPage = Page::query()->where('id', '=', $page->id)->first();\n        $this->assertStringContainsString('id=\"bkmrk-test\"', $updatedPage->html);\n        $this->assertStringContainsString('href=\"#bkmrk-test\"', $updatedPage->html);\n    }\n\n    public function test_get_page_nav_sets_correct_properties()\n    {\n        $content = '<h1 id=\"testa\">Hello</h1><h2 id=\"testb\">There</h2><h3 id=\"testc\">Donkey</h3>';\n        $pageContent = new PageContent(new Page(['html' => $content]));\n        $navMap = $pageContent->getNavigation($content);\n\n        $this->assertCount(3, $navMap);\n        $this->assertArrayMapIncludes([\n            'nodeName' => 'h1',\n            'link'     => '#testa',\n            'text'     => 'Hello',\n            'level'    => 1,\n        ], $navMap[0]);\n        $this->assertArrayMapIncludes([\n            'nodeName' => 'h2',\n            'link'     => '#testb',\n            'text'     => 'There',\n            'level'    => 2,\n        ], $navMap[1]);\n        $this->assertArrayMapIncludes([\n            'nodeName' => 'h3',\n            'link'     => '#testc',\n            'text'     => 'Donkey',\n            'level'    => 3,\n        ], $navMap[2]);\n    }\n\n    public function test_get_page_nav_does_not_show_empty_titles()\n    {\n        $content = '<h1 id=\"testa\">Hello</h1><h2 id=\"testb\">&nbsp;</h2><h3 id=\"testc\"></h3>';\n        $pageContent = new PageContent(new Page(['html' => $content]));\n        $navMap = $pageContent->getNavigation($content);\n\n        $this->assertCount(1, $navMap);\n        $this->assertArrayMapIncludes([\n            'nodeName' => 'h1',\n            'link'     => '#testa',\n            'text'     => 'Hello',\n        ], $navMap[0]);\n    }\n\n    public function test_get_page_nav_shifts_headers_if_only_smaller_ones_are_used()\n    {\n        $content = '<h4 id=\"testa\">Hello</h4><h5 id=\"testb\">There</h5><h6 id=\"testc\">Donkey</h6>';\n        $pageContent = new PageContent(new Page(['html' => $content]));\n        $navMap = $pageContent->getNavigation($content);\n\n        $this->assertCount(3, $navMap);\n        $this->assertArrayMapIncludes([\n            'nodeName' => 'h4',\n            'level'    => 1,\n        ], $navMap[0]);\n        $this->assertArrayMapIncludes([\n            'nodeName' => 'h5',\n            'level'    => 2,\n        ], $navMap[1]);\n        $this->assertArrayMapIncludes([\n            'nodeName' => 'h6',\n            'level'    => 3,\n        ], $navMap[2]);\n    }\n\n    public function test_get_page_nav_respects_non_breaking_spaces()\n    {\n        $content = '<h1 id=\"testa\">Hello&nbsp;There</h1>';\n        $pageContent = new PageContent(new Page(['html' => $content]));\n        $navMap = $pageContent->getNavigation($content);\n\n        $this->assertEquals([\n            'nodeName' => 'h1',\n            'link'     => '#testa',\n            'text'     => 'Hello There',\n            'level'    => 1,\n        ], $navMap[0]);\n    }\n\n    public function test_page_text_decodes_html_entities()\n    {\n        $page = $this->entities->page();\n\n        $this->actingAs($this->users->admin())\n            ->put($page->getUrl(''), [\n                'name' => 'Testing',\n                'html' => '<p>&quot;Hello &amp; welcome&quot;</p>',\n            ]);\n\n        $page->refresh();\n        $this->assertEquals('\"Hello & welcome\"', $page->text);\n    }\n\n    public function test_page_markdown_table_rendering()\n    {\n        $this->asEditor();\n        $page = $this->entities->page();\n\n        $content = '| Syntax      | Description |\n| ----------- | ----------- |\n| Header      | Title       |\n| Paragraph   | Text        |';\n        $this->put($page->getUrl(), [\n            'name' => $page->name,  'markdown' => $content,\n            'html' => '', 'summary' => '',\n        ]);\n\n        $page->refresh();\n        $this->assertStringContainsString('</tbody>', $page->html);\n\n        $pageView = $this->get($page->getUrl());\n        $this->withHtml($pageView)->assertElementExists('.page-content table tbody td');\n    }\n\n    public function test_page_markdown_task_list_rendering()\n    {\n        $this->asEditor();\n        $page = $this->entities->page();\n\n        $content = '- [ ] Item a\n- [x] Item b';\n        $this->put($page->getUrl(), [\n            'name' => $page->name,  'markdown' => $content,\n            'html' => '', 'summary' => '',\n        ]);\n\n        $page->refresh();\n        $this->assertStringContainsString('input', $page->html);\n        $this->assertStringContainsString('type=\"checkbox\"', $page->html);\n\n        $pageView = $this->get($page->getUrl());\n        $this->withHtml($pageView)->assertElementExists('.page-content li.task-list-item input[type=checkbox]');\n        $this->withHtml($pageView)->assertElementExists('.page-content li.task-list-item input[type=checkbox][checked]');\n    }\n\n    public function test_page_markdown_strikethrough_rendering()\n    {\n        $this->asEditor();\n        $page = $this->entities->page();\n\n        $content = '~~some crossed out text~~';\n        $this->put($page->getUrl(), [\n            'name' => $page->name,  'markdown' => $content,\n            'html' => '', 'summary' => '',\n        ]);\n\n        $page->refresh();\n        $this->assertStringMatchesFormat('%A<s%A>some crossed out text</s>%A', $page->html);\n\n        $pageView = $this->get($page->getUrl());\n        $this->withHtml($pageView)->assertElementExists('.page-content p > s');\n    }\n\n    public function test_page_markdown_single_html_comment_saving()\n    {\n        config()->set('app.content_filtering', 'jfh');\n        $this->asEditor();\n        $page = $this->entities->page();\n\n        $content = '<!-- Test Comment -->';\n        $this->put($page->getUrl(), [\n            'name' => $page->name,  'markdown' => $content,\n            'html' => '', 'summary' => '',\n        ])->assertRedirect();\n\n        $page->refresh();\n        $this->assertStringMatchesFormat($content, $page->html);\n\n        $pageView = $this->get($page->getUrl());\n        $pageView->assertStatus(200);\n        $pageView->assertSee($content, false);\n    }\n\n    public function test_base64_images_get_extracted_from_page_content()\n    {\n        $this->asEditor();\n        $page = $this->entities->page();\n\n        $this->put($page->getUrl(), [\n            'name' => $page->name, 'summary' => '',\n            'html' => '<p>test<img src=\"data:image/jpeg;base64,' . $this->base64Jpeg . '\"/></p>',\n        ]);\n\n        $page->refresh();\n        $this->assertStringMatchesFormat('%A<p%A>test<img src=\"http://localhost/uploads/images/gallery/%A.jpeg\">%A</p>%A', $page->html);\n\n        $matches = [];\n        preg_match('/src=\"http:\\/\\/localhost(.*?)\"/', $page->html, $matches);\n        $imagePath = $matches[1];\n        $imageFile = public_path($imagePath);\n        $this->assertEquals(base64_decode($this->base64Jpeg), file_get_contents($imageFile));\n\n        $this->files->deleteAtRelativePath($imagePath);\n    }\n\n    public function test_base64_images_get_extracted_when_containing_whitespace()\n    {\n        $this->asEditor();\n        $page = $this->entities->page();\n\n        $base64PngWithWhitespace = \"iVBORw0KGg\\noAAAANSUhE\\tUgAAAAEAAAA BCA   YAAAAfFcSJAAA\\n\\t ACklEQVR4nGMAAQAABQAB\";\n        $base64PngWithoutWhitespace = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQAB';\n        $this->put($page->getUrl(), [\n            'name' => $page->name, 'summary' => '',\n            'html' => '<p>test<img src=\"data:image/png;base64,' . $base64PngWithWhitespace . '\"/></p>',\n        ]);\n\n        $page->refresh();\n        $this->assertStringMatchesFormat('%A<p%A>test<img src=\"http://localhost/uploads/images/gallery/%A.png\">%A</p>%A', $page->html);\n\n        $matches = [];\n        preg_match('/src=\"http:\\/\\/localhost(.*?)\"/', $page->html, $matches);\n        $imagePath = $matches[1];\n        $imageFile = public_path($imagePath);\n        $this->assertEquals(base64_decode($base64PngWithoutWhitespace), file_get_contents($imageFile));\n\n        $this->files->deleteAtRelativePath($imagePath);\n    }\n\n    public function test_base64_images_within_html_blanked_if_not_supported_extension_for_extract()\n    {\n        // Relevant to https://github.com/BookStackApp/BookStack/issues/3010 and other cases\n        $extensions = [\n            'jiff', 'pngr', 'png ', ' png', '.png', 'png.', 'p.ng', ',png',\n            'data:image/png', ',data:image/png',\n        ];\n\n        foreach ($extensions as $extension) {\n            $this->asEditor();\n            $page = $this->entities->page();\n\n            $this->put($page->getUrl(), [\n                'name' => $page->name, 'summary' => '',\n                'html' => '<p>test<img src=\"data:image/' . $extension . ';base64,' . $this->base64Jpeg . '\"/></p>',\n            ]);\n\n            $page->refresh();\n            $this->assertStringContainsString('<img src=\"\"', $page->html);\n        }\n    }\n\n    public function test_base64_images_within_html_blanked_if_no_image_create_permission()\n    {\n        $editor = $this->users->editor();\n        $page = $this->entities->page();\n        $this->permissions->removeUserRolePermissions($editor, ['image-create-all']);\n\n        $this->actingAs($editor)->put($page->getUrl(), [\n            'name' => $page->name,\n            'html' => '<p>test<img src=\"data:image/jpeg;base64,' . $this->base64Jpeg . '\"/></p>',\n        ]);\n\n        $page->refresh();\n        $this->assertStringMatchesFormat('%A<p%A>test<img src=\"\">%A</p>%A', $page->html);\n    }\n\n    public function test_base64_images_within_html_blanked_if_content_does_not_appear_like_an_image()\n    {\n        $page = $this->entities->page();\n\n        $imgContent = base64_encode('file://test/a/b/c');\n        $this->asEditor()->put($page->getUrl(), [\n            'name' => $page->name,\n            'html' => '<p>test<img src=\"data:image/jpeg;base64,' . $imgContent . '\"/></p>',\n        ]);\n\n        $page->refresh();\n        $this->assertStringMatchesFormat('%A<p%A>test<img src=\"\">%A</p>%A', $page->html);\n    }\n\n    public function test_base64_images_get_extracted_from_markdown_page_content()\n    {\n        $this->asEditor();\n        $page = $this->entities->page();\n\n        $this->put($page->getUrl(), [\n            'name'     => $page->name, 'summary' => '',\n            'markdown' => 'test ![test](data:image/jpeg;base64,' . $this->base64Jpeg . ')',\n        ]);\n\n        $page->refresh();\n        $this->assertStringMatchesFormat('%A<p%A>test <img src=\"http://localhost/uploads/images/gallery/%A.jpeg\" alt=\"test\">%A</p>%A', $page->html);\n\n        $matches = [];\n        preg_match('/src=\"http:\\/\\/localhost(.*?)\"/', $page->html, $matches);\n        $imagePath = $matches[1];\n        $imageFile = public_path($imagePath);\n        $this->assertEquals(base64_decode($this->base64Jpeg), file_get_contents($imageFile));\n\n        $this->files->deleteAtRelativePath($imagePath);\n    }\n\n    public function test_markdown_base64_extract_not_limited_by_pcre_limits()\n    {\n        $pcreBacktrackLimit = ini_get('pcre.backtrack_limit');\n        $pcreRecursionLimit = ini_get('pcre.recursion_limit');\n\n        $this->asEditor();\n        $page = $this->entities->page();\n\n        ini_set('pcre.backtrack_limit', '500');\n        ini_set('pcre.recursion_limit', '500');\n\n        $content = str_repeat(base64_decode($this->base64Jpeg), 50);\n        $base64Content = base64_encode($content);\n\n        $this->put($page->getUrl(), [\n            'name'     => $page->name, 'summary' => '',\n            'markdown' => 'test ![test](data:image/jpeg;base64,' . $base64Content . ') ![test](data:image/jpeg;base64,' . $base64Content . ')',\n        ]);\n\n        $page->refresh();\n        $this->assertStringMatchesFormat('<p%A>test <img src=\"http://localhost/uploads/images/gallery/%A.jpeg\" alt=\"test\"> <img src=\"http://localhost/uploads/images/gallery/%A.jpeg\" alt=\"test\">%A</p>%A', $page->html);\n\n        $matches = [];\n        preg_match('/src=\"http:\\/\\/localhost(.*?)\"/', $page->html, $matches);\n        $imagePath = $matches[1];\n        $imageFile = public_path($imagePath);\n        $this->assertEquals($content, file_get_contents($imageFile));\n\n        $this->files->deleteAtRelativePath($imagePath);\n        ini_set('pcre.backtrack_limit', $pcreBacktrackLimit);\n        ini_set('pcre.recursion_limit', $pcreRecursionLimit);\n    }\n\n    public function test_base64_images_within_markdown_blanked_if_not_supported_extension_for_extract()\n    {\n        $page = $this->entities->page();\n\n        $this->asEditor()->put($page->getUrl(), [\n            'name'     => $page->name, 'summary' => '',\n            'markdown' => 'test ![test](data:image/jiff;base64,' . $this->base64Jpeg . ')',\n        ]);\n\n        $this->assertStringContainsString('<img src=\"\"', $page->refresh()->html);\n    }\n\n    public function test_base64_images_within_markdown_blanked_if_no_image_create_permission()\n    {\n        $editor = $this->users->editor();\n        $page = $this->entities->page();\n        $this->permissions->removeUserRolePermissions($editor, ['image-create-all']);\n\n        $this->actingAs($editor)->put($page->getUrl(), [\n            'name' => $page->name,\n            'markdown' => 'test ![test](data:image/jpeg;base64,' . $this->base64Jpeg . ')',\n        ]);\n\n        $this->assertStringContainsString('<img src=\"\"', $page->refresh()->html);\n    }\n\n    public function test_base64_images_within_markdown_blanked_if_content_does_not_appear_like_an_image()\n    {\n        $page = $this->entities->page();\n\n        $imgContent = base64_encode('file://test/a/b/c');\n        $this->asEditor()->put($page->getUrl(), [\n            'name' => $page->name,\n            'markdown' => 'test ![test](data:image/jpeg;base64,' . $imgContent . ')',\n        ]);\n\n        $page->refresh();\n        $this->assertStringContainsString('<img src=\"\"', $page->refresh()->html);\n    }\n\n    public function test_nested_headers_gets_assigned_an_id()\n    {\n        $page = $this->entities->page();\n\n        $content = '<table><tbody><tr><td><h5>Simple Test</h5></td></tr></tbody></table>';\n        $this->asEditor()->put($page->getUrl(), [\n            'name'    => $page->name,\n            'html'    => $content,\n        ]);\n\n        // The top level <table> node will get assign the bkmrk-simple-test id because the system will\n        // take the node value of h5\n        // So the h5 should get the bkmrk-simple-test-1 id\n        $this->assertStringContainsString('<h5 id=\"bkmrk-simple-test-1\">Simple Test</h5>', $page->refresh()->html);\n    }\n\n    public function test_non_breaking_spaces_are_preserved()\n    {\n        $page = $this->entities->page();\n\n        $content = '<p>&nbsp;</p>';\n        $this->asEditor()->put($page->getUrl(), [\n            'name'    => $page->name,\n            'html'    => $content,\n        ]);\n\n        $this->assertStringContainsString('<p id=\"bkmrk-%C2%A0\">&nbsp;</p>', $page->refresh()->html);\n    }\n\n    public function test_page_save_with_many_headers_and_links_is_reasonable()\n    {\n        $page = $this->entities->page();\n\n        $content = '';\n        for ($i = 0; $i < 500; $i++) {\n            $content .= \"<table><tbody><tr><td><h5 id='header-{$i}'>Simple Test</h5><a href='#header-{$i}'></a></td></tr></tbody></table>\";\n        }\n\n        $time = time();\n        $this->asEditor()->put($page->getUrl(), [\n            'name'    => $page->name,\n            'html'    => $content,\n        ])->assertRedirect();\n\n        $timeElapsed = time() - $time;\n        $this->assertLessThan(3, $timeElapsed);\n    }\n}\n"
  },
  {
    "path": "tests/Entity/PageDraftTest.php",
    "content": "<?php\n\nnamespace Tests\\Entity;\n\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Models\\PageRevision;\nuse BookStack\\Entities\\Repos\\PageRepo;\nuse Tests\\TestCase;\n\nclass PageDraftTest extends TestCase\n{\n    protected Page $page;\n    protected PageRepo $pageRepo;\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n        $this->page = $this->entities->page();\n        $this->pageRepo = app()->make(PageRepo::class);\n    }\n\n    public function test_draft_content_shows_if_available()\n    {\n        $addedContent = '<p>test message content</p>';\n\n        $resp = $this->asAdmin()->get($this->page->getUrl('/edit'));\n        $this->withHtml($resp)->assertElementNotContains('[name=\"html\"]', $addedContent);\n\n        $newContent = $this->page->html . $addedContent;\n        $this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]);\n        $resp = $this->asAdmin()->get($this->page->getUrl('/edit'));\n        $this->withHtml($resp)->assertElementContains('[name=\"html\"]', $newContent);\n    }\n\n    public function test_draft_not_visible_by_others()\n    {\n        $addedContent = '<p>test message content</p>';\n        $resp = $this->asAdmin()->get($this->page->getUrl('/edit'));\n        $this->withHtml($resp)->assertElementNotContains('[name=\"html\"]', $addedContent);\n\n        $newContent = $this->page->html . $addedContent;\n        $newUser = $this->users->editor();\n        $this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]);\n\n        $resp = $this->actingAs($newUser)->get($this->page->getUrl('/edit'));\n        $this->withHtml($resp)->assertElementNotContains('[name=\"html\"]', $newContent);\n    }\n\n    public function test_alert_message_shows_if_editing_draft()\n    {\n        $this->asAdmin();\n        $this->pageRepo->updatePageDraft($this->page, ['html' => 'test content']);\n        $this->asAdmin()->get($this->page->getUrl('/edit'))\n            ->assertSee('You are currently editing a draft');\n    }\n\n    public function test_alert_message_shows_if_someone_else_editing()\n    {\n        $nonEditedPage = Page::query()->take(10)->get()->last();\n        $addedContent = '<p>test message content</p>';\n        $resp = $this->asAdmin()->get($this->page->getUrl('/edit'));\n        $this->withHtml($resp)->assertElementNotContains('[name=\"html\"]', $addedContent);\n\n        $newContent = $this->page->html . $addedContent;\n        $newUser = $this->users->editor();\n        $this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]);\n\n        $this->actingAs($newUser)\n            ->get($this->page->getUrl('/edit'))\n            ->assertSee('Admin has started editing this page');\n        $this->flushSession();\n        $resp = $this->get($nonEditedPage->getUrl() . '/edit');\n        $this->withHtml($resp)->assertElementNotContains('.notification', 'Admin has started editing this page');\n    }\n\n    public function test_draft_save_shows_alert_if_draft_older_than_last_page_update()\n    {\n        $admin = $this->users->admin();\n        $editor = $this->users->editor();\n        $page = $this->entities->page();\n\n        $this->actingAs($editor)->put('/ajax/page/' . $page->id . '/save-draft', [\n            'name' => $page->name,\n            'html' => '<p>updated draft</p>',\n        ]);\n\n        /** @var PageRevision $draft */\n        $draft = $page->allRevisions()\n            ->where('type', '=', 'update_draft')\n            ->where('created_by', '=', $editor->id)\n            ->first();\n        $draft->created_at = now()->subMinute(1);\n        $draft->save();\n\n        $this->actingAs($admin)->put($page->refresh()->getUrl(), [\n            'name' => $page->name,\n            'html' => '<p>admin update</p>',\n        ]);\n\n        $resp = $this->actingAs($editor)->put('/ajax/page/' . $page->id . '/save-draft', [\n            'name' => $page->name,\n            'html' => '<p>updated draft again</p>',\n        ]);\n\n        $resp->assertJson([\n            'warning' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',\n        ]);\n    }\n\n    public function test_draft_save_shows_alert_if_draft_edit_started_by_someone_else()\n    {\n        $admin = $this->users->admin();\n        $editor = $this->users->editor();\n        $page = $this->entities->page();\n\n        $this->actingAs($admin)->put('/ajax/page/' . $page->id . '/save-draft', [\n            'name' => $page->name,\n            'html' => '<p>updated draft</p>',\n        ]);\n\n        $resp = $this->actingAs($editor)->put('/ajax/page/' . $page->id . '/save-draft', [\n            'name' => $page->name,\n            'html' => '<p>updated draft again</p>',\n        ]);\n\n        $resp->assertJson([\n            'warning' => 'Admin has started editing this page in the last 60 minutes. Take care not to overwrite each other\\'s updates!',\n        ]);\n    }\n\n    public function test_draft_pages_show_on_homepage()\n    {\n        $book = $this->entities->book();\n        $resp = $this->asAdmin()->get('/');\n        $this->withHtml($resp)->assertElementNotContains('#recent-drafts', 'New Page');\n\n        $this->get($book->getUrl() . '/create-page');\n\n        $this->withHtml($this->get('/'))->assertElementContains('#recent-drafts', 'New Page');\n    }\n\n    public function test_draft_pages_not_visible_by_others()\n    {\n        $book = $this->entities->book();\n        $chapter = $book->chapters->first();\n        $newUser = $this->users->editor();\n\n        $this->actingAs($newUser)->get($book->getUrl('/create-page'));\n        $this->get($chapter->getUrl('/create-page'));\n        $resp = $this->get($book->getUrl());\n        $this->withHtml($resp)->assertElementContains('.book-contents', 'New Page');\n\n        $resp = $this->asAdmin()->get($book->getUrl());\n        $this->withHtml($resp)->assertElementNotContains('.book-contents', 'New Page');\n        $resp = $this->get($chapter->getUrl());\n        $this->withHtml($resp)->assertElementNotContains('.book-contents', 'New Page');\n    }\n\n    public function test_page_html_in_ajax_fetch_response()\n    {\n        $this->asAdmin();\n        $page = $this->entities->page();\n        $page->html = '<p>test content<script>hellotherekitty</script></p>';\n        $page->save();\n\n        $this->getJson('/ajax/page/' . $page->id)->assertJson([\n            'html' => '<p>test content</p>',\n        ]);\n    }\n\n    public function test_user_draft_removed_on_user_drafts_delete_call()\n    {\n        $editor = $this->users->editor();\n        $page = $this->entities->page();\n\n        $this->actingAs($editor)->put('/ajax/page/' . $page->id . '/save-draft', [\n            'name' => $page->name,\n            'html' => '<p>updated draft again</p>',\n        ]);\n\n        $revisionData = [\n            'type' => 'update_draft',\n            'created_by' => $editor->id,\n            'page_id' => $page->id,\n        ];\n\n        $this->assertDatabaseHas('page_revisions', $revisionData);\n\n        $resp = $this->delete(\"/page-revisions/user-drafts/{$page->id}\");\n\n        $resp->assertOk();\n        $this->assertDatabaseMissing('page_revisions', $revisionData);\n    }\n\n    public function test_updating_page_draft_with_markdown_retains_markdown_content()\n    {\n        $book = $this->entities->book();\n        $this->asEditor()->get($book->getUrl('/create-page'));\n        /** @var Page $draft */\n        $draft = Page::query()->where('draft', '=', true)->where('book_id', '=', $book->id)->firstOrFail();\n\n        $resp = $this->put('/ajax/page/' . $draft->id . '/save-draft', [\n            'name'     => 'My updated draft',\n            'markdown' => \"# My markdown page\\n\\n[A link](https://example.com)\",\n            'html'     => '<p>checking markdown takes priority over this</p>',\n        ]);\n        $resp->assertOk();\n\n        $this->assertDatabaseHasEntityData('page', [\n            'id'       => $draft->id,\n            'draft'    => true,\n            'name'     => 'My updated draft',\n            'markdown' => \"# My markdown page\\n\\n[A link](https://example.com)\",\n        ]);\n\n        $draft->refresh();\n        $this->assertStringContainsString('href=\"https://example.com\"', $draft->html);\n    }\n\n    public function test_slug_generated_on_draft_publish_to_page_when_no_name_change()\n    {\n        $book = $this->entities->book();\n        $this->asEditor()->get($book->getUrl('/create-page'));\n        /** @var Page $draft */\n        $draft = Page::query()->where('draft', '=', true)->where('book_id', '=', $book->id)->firstOrFail();\n\n        $this->put('/ajax/page/' . $draft->id . '/save-draft', [\n            'name'     => 'My page',\n            'markdown' => 'Update test',\n        ])->assertOk();\n\n        $draft->refresh();\n        $this->assertEmpty($draft->slug);\n\n        $this->post($draft->getUrl(), [\n            'name'     => 'My page',\n            'markdown' => '# My markdown page',\n        ]);\n\n        $this->assertDatabaseHasEntityData('page', [\n            'id'    => $draft->id,\n            'draft' => false,\n            'slug'  => 'my-page',\n        ]);\n    }\n}\n"
  },
  {
    "path": "tests/Entity/PageEditorTest.php",
    "content": "<?php\n\nnamespace Tests\\Entity;\n\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Tools\\PageEditorType;\nuse Tests\\TestCase;\n\nclass PageEditorTest extends TestCase\n{\n    protected Page $page;\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n        $this->page = $this->entities->page();\n    }\n\n    public function test_default_editor_is_wysiwyg_for_new_pages()\n    {\n        $this->assertEquals('wysiwyg', setting('app-editor'));\n        $resp = $this->asAdmin()->get($this->page->book->getUrl('/create-page'));\n        $this->withHtml($this->followRedirects($resp))->assertElementExists('#html-editor');\n    }\n\n    public function test_editor_set_for_new_pages()\n    {\n        $book = $this->page->book;\n\n        $this->asEditor()->get($book->getUrl('/create-page'));\n        $newPage = $book->pages()->orderBy('id', 'desc')->first();\n        $this->assertEquals('wysiwyg', $newPage->editor);\n\n        $this->setSettings(['app-editor' => PageEditorType::Markdown->value]);\n\n        $this->asEditor()->get($book->getUrl('/create-page'));\n        $newPage = $book->pages()->orderBy('id', 'desc')->first();\n        $this->assertEquals('markdown', $newPage->editor);\n    }\n\n    public function test_markdown_setting_shows_markdown_editor_for_new_pages()\n    {\n        $this->setSettings(['app-editor' => PageEditorType::Markdown->value]);\n\n        $resp = $this->asAdmin()->get($this->page->book->getUrl('/create-page'));\n        $this->withHtml($this->followRedirects($resp))\n            ->assertElementNotExists('#html-editor')\n            ->assertElementExists('#markdown-editor');\n    }\n\n    public function test_markdown_content_given_to_editor()\n    {\n        $mdContent = '# hello. This is a test';\n        $this->page->markdown = $mdContent;\n        $this->page->editor = PageEditorType::Markdown;\n        $this->page->save();\n\n        $resp = $this->asAdmin()->get($this->page->getUrl('/edit'));\n        $this->withHtml($resp)->assertElementContains('[name=\"markdown\"]', $mdContent);\n    }\n\n    public function test_html_content_given_to_editor_if_no_markdown()\n    {\n        $this->page->editor = 'markdown';\n        $this->page->save();\n\n        $resp = $this->asAdmin()->get($this->page->getUrl() . '/edit');\n        $this->withHtml($resp)->assertElementContains('[name=\"markdown\"]', $this->page->html);\n    }\n\n    public function test_empty_markdown_still_saves_without_error()\n    {\n        $this->setSettings(['app-editor' => 'markdown']);\n        $book = $this->entities->book();\n\n        $this->asEditor()->get($book->getUrl('/create-page'));\n        $draft = Page::query()->where('book_id', '=', $book->id)\n            ->where('draft', '=', true)->first();\n\n        $details = [\n            'name'     => 'my page',\n            'markdown' => '',\n        ];\n        $resp = $this->post($book->getUrl(\"/draft/{$draft->id}\"), $details);\n        $resp->assertRedirect();\n\n        $this->assertDatabaseHasEntityData('page', [\n            'markdown' => $details['markdown'],\n            'id'       => $draft->id,\n            'draft'    => false,\n        ]);\n    }\n\n    public function test_back_link_in_editor_has_correct_url()\n    {\n        $book = $this->entities->bookHasChaptersAndPages();\n        $this->asEditor()->get($book->getUrl('/create-page'));\n        /** @var Chapter $chapter */\n        $chapter = $book->chapters()->firstOrFail();\n        /** @var Page $draft */\n        $draft = $book->pages()->where('draft', '=', true)->firstOrFail();\n\n        // Book draft goes back to book\n        $resp = $this->get($book->getUrl(\"/draft/{$draft->id}\"));\n        $this->withHtml($resp)->assertElementContains('a[href=\"' . $book->getUrl() . '\"]', 'Back');\n\n        // Chapter draft goes back to chapter\n        $draft->chapter_id = $chapter->id;\n        $draft->save();\n        $resp = $this->get($book->getUrl(\"/draft/{$draft->id}\"));\n        $this->withHtml($resp)->assertElementContains('a[href=\"' . $chapter->getUrl() . '\"]', 'Back');\n\n        // Saved page goes back to page\n        $this->post($book->getUrl(\"/draft/{$draft->id}\"), ['name' => 'Updated', 'html' => 'Updated']);\n        $draft->refresh();\n        $resp = $this->get($draft->getUrl('/edit'));\n        $this->withHtml($resp)->assertElementContains('a[href=\"' . $draft->getUrl() . '\"]', 'Back');\n    }\n\n    public function test_switching_from_html_to_clean_markdown_works()\n    {\n        $page = $this->entities->page();\n        $page->html = '<h2>A Header</h2><p>Some <strong>bold</strong> content.</p>';\n        $page->save();\n\n        $resp = $this->asAdmin()->get($page->getUrl('/edit?editor=markdown-clean'));\n        $resp->assertStatus(200);\n        $resp->assertSee(\"## A Header\\n\\nSome **bold** content.\");\n        $this->withHtml($resp)->assertElementExists('#markdown-editor');\n    }\n\n    public function test_switching_from_html_to_stable_markdown_works()\n    {\n        $page = $this->entities->page();\n        $page->html = '<h2>A Header</h2><p>Some <strong>bold</strong> content.</p>';\n        $page->save();\n\n        $resp = $this->asAdmin()->get($page->getUrl('/edit?editor=markdown-stable'));\n        $resp->assertStatus(200);\n        $resp->assertSee('<h2>A Header</h2><p>Some <strong>bold</strong> content.</p>', true);\n        $this->withHtml($resp)->assertElementExists('[component=\"markdown-editor\"]');\n    }\n\n    public function test_switching_from_markdown_to_wysiwyg_works()\n    {\n        $page = $this->entities->page();\n        $page->html = '';\n        $page->markdown = \"## A Header\\n\\nSome content with **bold** text!\";\n        $page->save();\n\n        $resp = $this->asAdmin()->get($page->getUrl('/edit?editor=wysiwyg'));\n        $resp->assertStatus(200);\n        $this->withHtml($resp)->assertElementExists('[component=\"wysiwyg-editor-tinymce\"]');\n        $resp->assertSee(\"<h2>A Header</h2>\\n<p>Some content with <strong>bold</strong> text!</p>\", true);\n    }\n\n    public function test_switching_from_markdown_to_wysiwyg2024_works()\n    {\n        $page = $this->entities->page();\n        $page->html = '';\n        $page->markdown = \"## A Header\\n\\nSome content with **bold** text!\";\n        $page->save();\n\n        $resp = $this->asAdmin()->get($page->getUrl('/edit?editor=wysiwyg2024'));\n        $resp->assertStatus(200);\n        $this->withHtml($resp)->assertElementExists('[component=\"wysiwyg-editor\"]');\n        $resp->assertSee(\"<h2>A Header</h2>\\n<p>Some content with <strong>bold</strong> text!</p>\", true);\n    }\n\n    public function test_page_editor_changes_with_editor_property()\n    {\n        $resp = $this->asAdmin()->get($this->page->getUrl('/edit'));\n        $this->withHtml($resp)->assertElementExists('[component=\"wysiwyg-editor-tinymce\"]');\n\n        $this->page->markdown = \"## A Header\\n\\nSome content with **bold** text!\";\n        $this->page->editor = 'markdown';\n        $this->page->save();\n\n        $resp = $this->asAdmin()->get($this->page->getUrl('/edit'));\n        $this->withHtml($resp)->assertElementExists('[component=\"markdown-editor\"]');\n\n        $this->page->editor = 'wysiwyg2024';\n        $this->page->save();\n\n        $resp = $this->asAdmin()->get($this->page->getUrl('/edit'));\n        $this->withHtml($resp)->assertElementExists('[component=\"wysiwyg-editor\"]');\n    }\n\n    public function test_editor_type_switch_options_show()\n    {\n        $resp = $this->asAdmin()->get($this->page->getUrl('/edit'));\n        $editLink = $this->page->getUrl('/edit') . '?editor=';\n        $this->withHtml($resp)->assertElementContains(\"a[href=\\\"{$editLink}markdown-clean\\\"]\", '(Clean Content)');\n        $this->withHtml($resp)->assertElementContains(\"a[href=\\\"{$editLink}markdown-stable\\\"]\", '(Stable Content)');\n        $this->withHtml($resp)->assertElementContains(\"a[href=\\\"{$editLink}wysiwyg2024\\\"]\", '(In Beta Testing)');\n\n        $resp = $this->asAdmin()->get($this->page->getUrl('/edit?editor=markdown-stable'));\n        $editLink = $this->page->getUrl('/edit') . '?editor=';\n        $this->withHtml($resp)->assertElementContains(\"a[href=\\\"{$editLink}wysiwyg\\\"]\", 'Switch to WYSIWYG Editor');\n    }\n\n    public function test_editor_type_switch_options_dont_show_if_without_change_editor_permissions()\n    {\n        $resp = $this->asEditor()->get($this->page->getUrl('/edit'));\n        $editLink = $this->page->getUrl('/edit') . '?editor=';\n        $this->withHtml($resp)->assertElementNotExists(\"a[href*=\\\"{$editLink}\\\"]\");\n    }\n\n    public function test_page_editor_type_switch_does_not_work_without_change_editor_permissions()\n    {\n        $page = $this->entities->page();\n        $page->html = '<h2>A Header</h2><p>Some <strong>bold</strong> content.</p>';\n        $page->save();\n\n        $resp = $this->asEditor()->get($page->getUrl('/edit?editor=markdown-stable'));\n        $resp->assertStatus(200);\n        $this->withHtml($resp)->assertElementExists('[component=\"wysiwyg-editor-tinymce\"]');\n        $this->withHtml($resp)->assertElementNotExists('[component=\"markdown-editor\"]');\n    }\n\n    public function test_page_save_does_not_change_active_editor_without_change_editor_permissions()\n    {\n        $page = $this->entities->page();\n        $page->html = '<h2>A Header</h2><p>Some <strong>bold</strong> content.</p>';\n        $page->editor = 'wysiwyg';\n        $page->save();\n\n        $this->asEditor()->put($page->getUrl(), ['name' => $page->name, 'markdown' => '## Updated content abc']);\n        $this->assertEquals('wysiwyg', $page->refresh()->editor);\n    }\n\n    public function test_editor_type_change_to_wysiwyg_infers_type_from_request_or_uses_system_default()\n    {\n        $tests = [\n            [\n                'setting' => 'wysiwyg',\n                'request' => 'wysiwyg2024',\n                'expected' => 'wysiwyg2024',\n            ],\n            [\n                'setting' => 'wysiwyg2024',\n                'request' => 'wysiwyg',\n                'expected' => 'wysiwyg',\n            ],\n            [\n                'setting' => 'wysiwyg',\n                'request' => null,\n                'expected' => 'wysiwyg',\n            ],\n            [\n                'setting' => 'wysiwyg2024',\n                'request' => null,\n                'expected' => 'wysiwyg2024',\n            ]\n        ];\n\n        $page = $this->entities->page();\n        foreach ($tests as $test) {\n            $page->editor = 'markdown';\n            $page->save();\n\n            $this->setSettings(['app-editor' => $test['setting']]);\n            $this->asAdmin()->put($page->getUrl(), ['name' => $page->name, 'html' => '<p>Hello</p>', 'editor' => $test['request']]);\n            $this->assertEquals($test['expected'], $page->refresh()->editor, \"Failed asserting global editor {$test['setting']} with request editor {$test['request']} results in {$test['expected']} set for the page\");\n        }\n    }\n\n    public function test_editor_html_content_is_filtered_if_loaded_by_a_different_user()\n    {\n        $editor = $this->users->editor();\n        $page = $this->entities->page();\n        $page->html = '<style>hellotherethisisaturtlemonster</style>';\n        $page->updated_by = $editor->id;\n        $page->save();\n\n        $resp = $this->asAdmin()->get($page->getUrl('edit'));\n        $resp->assertOk();\n        $resp->assertDontSee('hellotherethisisaturtlemonster', false);\n\n        $resp = $this->asAdmin()->get(\"/ajax/page/{$page->id}\");\n        $resp->assertOk();\n        $resp->assertDontSee('hellotherethisisaturtlemonster', false);\n    }\n\n    public function test_editor_html_filtered_does_not_cause_error_if_empty()\n    {\n        $emptyExamples = ['', '<p></p>', '<p>&nbsp;</p>', ' ', \"\\n\"];\n        $editor = $this->users->editor();\n        $page = $this->entities->page();\n        $page->updated_by = $editor->id;\n\n        foreach ($emptyExamples as $emptyExample) {\n            $page->html = $emptyExample;\n            $page->save();\n\n            $resp = $this->asAdmin()->get($page->getUrl('edit'));\n            $resp->assertOk();\n\n            $resp = $this->asAdmin()->get(\"/ajax/page/{$page->id}\");\n            $resp->assertOk();\n        }\n    }\n}\n"
  },
  {
    "path": "tests/Entity/PageRevisionTest.php",
    "content": "<?php\n\nnamespace Tests\\Entity;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Entities\\Models\\Page;\nuse Tests\\TestCase;\n\nclass PageRevisionTest extends TestCase\n{\n    public function test_revision_links_visible_to_viewer()\n    {\n        $page = $this->entities->page();\n\n        $html = $this->withHtml($this->asViewer()->get($page->getUrl()));\n        $html->assertLinkExists($page->getUrl('/revisions'));\n        $html->assertElementContains('a', 'Revisions');\n        $html->assertElementContains('a', 'Revision #1');\n    }\n\n    public function test_page_revision_views_viewable()\n    {\n        $this->asEditor();\n        $page = $this->entities->page();\n        $this->createRevisions($page, 1, ['name' => 'updated page', 'html' => '<p>new content</p>']);\n        $pageRevision = $page->revisions->last();\n\n        $resp = $this->get($page->getUrl() . '/revisions/' . $pageRevision->id);\n        $resp->assertStatus(200);\n        $resp->assertSee('new content');\n\n        $resp = $this->get($page->getUrl() . '/revisions/' . $pageRevision->id . '/changes');\n        $resp->assertStatus(200);\n        $resp->assertSee('new content');\n    }\n\n    public function test_page_revision_preview_shows_content_of_revision()\n    {\n        $this->asEditor();\n        $page = $this->entities->page();\n        $this->createRevisions($page, 1, ['name' => 'updated page', 'html' => '<p>new revision content</p>']);\n        $pageRevision = $page->revisions->last();\n        $this->createRevisions($page, 1, ['name' => 'updated page', 'html' => '<p>Updated content</p>']);\n\n        $revisionView = $this->get($page->getUrl() . '/revisions/' . $pageRevision->id);\n        $revisionView->assertStatus(200);\n        $revisionView->assertSee('new revision content');\n    }\n\n    public function test_page_revision_preview_filters_html_content()\n    {\n        $this->asEditor();\n        $page = $this->entities->page();\n        $this->createRevisions($page, 1, ['name' => 'updated page', 'html' => '<script>dontwantthishere</script><style>dontwantthishere</style><p>expectthisthough</p>']);\n        $pageRevision = $page->revisions->last();\n        $this->createRevisions($page, 1, ['name' => 'updated page', 'html' => '<p>Updated content</p>']);\n\n        $revisionView = $this->get($page->getUrl() . '/revisions/' . $pageRevision->id);\n        $revisionView->assertStatus(200);\n        $revisionView->assertSee('expectthisthough');\n        $revisionView->assertDontSee('dontwantthishere');\n    }\n\n    public function test_page_revision_restore_updates_content()\n    {\n        $this->asEditor();\n        $page = $this->entities->page();\n        $this->createRevisions($page, 1, ['name' => 'updated page abc123', 'html' => '<p>new contente def456</p>']);\n        $this->createRevisions($page, 1, ['name' => 'updated page again', 'html' => '<p>new content</p>']);\n        $page = Page::find($page->id);\n\n        $pageView = $this->get($page->getUrl());\n        $pageView->assertDontSee('abc123');\n        $pageView->assertDontSee('def456');\n\n        $revToRestore = $page->revisions()->where('name', 'like', '%abc123')->first();\n        $restoreReq = $this->put($page->getUrl() . '/revisions/' . $revToRestore->id . '/restore');\n        $page = Page::find($page->id);\n\n        $restoreReq->assertStatus(302);\n        $restoreReq->assertRedirect($page->getUrl());\n\n        $pageView = $this->get($page->getUrl());\n        $pageView->assertSee('abc123');\n        $pageView->assertSee('def456');\n    }\n\n    public function test_page_revision_restore_with_markdown_retains_markdown_content()\n    {\n        $this->asEditor();\n        $page = $this->entities->page();\n        $this->createRevisions($page, 1, ['name' => 'updated page abc123', 'markdown' => '## New Content def456']);\n        $this->createRevisions($page, 1, ['name' => 'updated page again', 'markdown' => '## New Content Updated']);\n        $page = Page::find($page->id);\n\n        $pageView = $this->get($page->getUrl());\n        $pageView->assertDontSee('abc123');\n        $pageView->assertDontSee('def456');\n\n        $revToRestore = $page->revisions()->where('name', 'like', '%abc123')->first();\n        $restoreReq = $this->put($page->getUrl() . '/revisions/' . $revToRestore->id . '/restore');\n        $page = Page::find($page->id);\n\n        $restoreReq->assertStatus(302);\n        $restoreReq->assertRedirect($page->getUrl());\n\n        $pageView = $this->get($page->getUrl());\n        $this->assertDatabaseHasEntityData('page', [\n            'id'       => $page->id,\n            'markdown' => '## New Content def456',\n        ]);\n        $pageView->assertSee('abc123');\n        $pageView->assertSee('def456');\n    }\n\n    public function test_page_revision_restore_sets_new_revision_with_summary()\n    {\n        $this->asEditor();\n        $page = $this->entities->page();\n        $this->createRevisions($page, 1, ['name' => 'updated page abc123', 'html' => '<p>new contente def456</p>', 'summary' => 'My first update']);\n        $this->createRevisions($page, 1, ['html' => '<p>new content</p>']);\n        $page->refresh();\n\n        $revToRestore = $page->revisions()->where('name', 'like', '%abc123')->first();\n        $this->put($page->getUrl() . '/revisions/' . $revToRestore->id . '/restore');\n        $page->refresh();\n\n        $this->assertDatabaseHas('page_revisions', [\n            'page_id' => $page->id,\n            'text'    => 'new contente def456',\n            'type'    => 'version',\n            'summary' => \"Restored from #{$revToRestore->id}; My first update\",\n        ]);\n\n        $detail = \"Revision #{$revToRestore->revision_number} (ID: {$revToRestore->id}) for page ID {$revToRestore->page_id}\";\n        $this->assertActivityExists(ActivityType::REVISION_RESTORE, null, $detail);\n    }\n\n    public function test_page_revision_count_increments_on_update()\n    {\n        $page = $this->entities->page();\n        $startCount = $page->revision_count;\n        $this->createRevisions($page, 1);\n\n        $this->assertTrue(Page::find($page->id)->revision_count === $startCount + 1);\n    }\n\n    public function test_revision_count_shown_in_page_meta()\n    {\n        $page = $this->entities->page();\n        $this->createRevisions($page, 2);\n\n        $pageView = $this->asViewer()->get($page->getUrl());\n        $pageView->assertSee('Revision #' . $page->revision_count);\n    }\n\n    public function test_revision_deletion()\n    {\n        $page = $this->entities->page();\n        $this->createRevisions($page, 2);\n        $beforeRevisionCount = $page->revisions->count();\n\n        // Delete the first revision\n        $revision = $page->revisions->get(1);\n        $resp = $this->asEditor()->delete($revision->getUrl('/delete/'));\n        $resp->assertRedirect($page->getUrl('/revisions'));\n\n        $page->refresh();\n        $afterRevisionCount = $page->revisions->count();\n\n        $this->assertTrue($beforeRevisionCount === ($afterRevisionCount + 1));\n\n        $detail = \"Revision #{$revision->revision_number} (ID: {$revision->id}) for page ID {$revision->page_id}\";\n        $this->assertActivityExists(ActivityType::REVISION_DELETE, null, $detail);\n\n        // Try to delete the latest revision\n        $beforeRevisionCount = $page->revisions->count();\n        $resp = $this->asEditor()->delete($page->currentRevision->getUrl('/delete/'));\n        $resp->assertRedirect($page->getUrl('/revisions'));\n\n        $page->refresh();\n        $afterRevisionCount = $page->revisions->count();\n        $this->assertTrue($beforeRevisionCount === $afterRevisionCount);\n    }\n\n    public function test_revision_limit_enforced()\n    {\n        config()->set('app.revision_limit', 2);\n        $page = $this->entities->page();\n        $this->createRevisions($page, 12);\n\n        $revisionCount = $page->revisions()->count();\n        $this->assertEquals(2, $revisionCount);\n    }\n\n    public function test_false_revision_limit_allows_many_revisions()\n    {\n        config()->set('app.revision_limit', false);\n        $page = $this->entities->page();\n        $this->createRevisions($page, 12);\n\n        $revisionCount = $page->revisions()->count();\n        $this->assertEquals(12, $revisionCount);\n    }\n\n    public function test_revision_list_shows_editor_type()\n    {\n        $page = $this->entities->page();\n        $this->createRevisions($page, 1, ['html' => 'new page html']);\n\n        $resp = $this->asAdmin()->get($page->refresh()->getUrl('/revisions'));\n        $this->withHtml($resp)->assertElementContains('.item-list-row > div:nth-child(2)', 'WYSIWYG)');\n        $this->withHtml($resp)->assertElementNotContains('.item-list-row > div:nth-child(2)', 'Markdown)');\n\n        $this->createRevisions($page, 1, ['markdown' => '# Some markdown content']);\n        $resp = $this->get($page->refresh()->getUrl('/revisions'));\n        $this->withHtml($resp)->assertElementContains('.item-list-row > div:nth-child(2)', 'Markdown)');\n    }\n\n    public function test_revision_changes_link_not_shown_for_oldest_revision()\n    {\n        $page = $this->entities->page();\n        $this->createRevisions($page, 3, ['html' => 'new page html']);\n\n        $resp = $this->asAdmin()->get($page->refresh()->getUrl('/revisions'));\n        $html = $this->withHtml($resp);\n\n        $html->assertElementNotExists('.item-list > .item-list-row:last-child a[href*=\"/changes\"]');\n        $html->assertElementContains('.item-list > .item-list-row:nth-child(2)', 'Changes');\n    }\n\n    public function test_revision_changes_view_shows_diff()\n    {\n        $this->asEditor();\n        $page = $this->entities->page();\n        $this->createRevisions($page, 1, ['name' => 'updated page', 'html' => '<p id=\"bkmrk-hello\">Hello there dog</p>']);\n        $this->createRevisions($page, 1, ['name' => 'updated page', 'html' => '<p id=\"bkmrk-hello\">Hello there cat</p>']);\n\n        $pageRevision = $page->revisions()->orderBy('id', 'desc')->first();\n        $revisionView = $this->get(\"{$page->getUrl()}/revisions/{$pageRevision->id}/changes\");\n        $revisionView->assertStatus(200);\n        $revisionView->assertSee('<p id=\"bkmrk-hello\">Hello there <del class=\"diffmod\">dog</del><ins class=\"diffmod\">cat</ins></p>', false);\n    }\n\n    public function test_revision_changes_view_filters_html_content()\n    {\n        $this->asEditor();\n        $page = $this->entities->page();\n        $html = '<script>dontwantthishere</script><style>dontwantthishere</style><p>expectthisthough</p>';\n        $this->createRevisions($page, 1, ['name' => 'updated page', 'html' => $html]);\n        $this->createRevisions($page, 1, ['name' => 'updated page', 'html' => $html]);\n\n        $pageRevision = $page->revisions()->orderBy('id', 'desc')->first();\n        $revisionView = $this->get(\"{$page->getUrl()}/revisions/{$pageRevision->id}/changes\");\n        $revisionView->assertStatus(200);\n        $revisionView->assertSee('expectthisthough');\n        $revisionView->assertDontSee('dontwantthishere');\n    }\n\n    public function test_revision_restore_action_only_visible_with_permission()\n    {\n        $page = $this->entities->page();\n        $this->createRevisions($page, 2);\n\n        $viewer = $this->users->viewer();\n        $this->actingAs($viewer);\n        $respHtml = $this->withHtml($this->get($page->getUrl('/revisions')));\n        $respHtml->assertElementNotContains('.actions a', 'Restore');\n        $respHtml->assertElementNotExists('form[action$=\"/restore\"]');\n\n        $this->permissions->grantUserRolePermissions($viewer, ['page-update-all']);\n\n        $respHtml = $this->withHtml($this->get($page->getUrl('/revisions')));\n        $respHtml->assertElementContains('.actions a', 'Restore');\n        $respHtml->assertElementExists('form[action$=\"/restore\"]');\n    }\n\n    public function test_revision_delete_action_only_visible_with_permission()\n    {\n        $page = $this->entities->page();\n        $this->createRevisions($page, 2);\n\n        $viewer = $this->users->viewer();\n        $this->actingAs($viewer);\n        $respHtml = $this->withHtml($this->get($page->getUrl('/revisions')));\n        $respHtml->assertElementNotContains('.actions a', 'Delete');\n        $respHtml->assertElementNotExists('form[action$=\"/delete\"]');\n\n        $this->permissions->grantUserRolePermissions($viewer, ['page-delete-all']);\n\n        $respHtml = $this->withHtml($this->get($page->getUrl('/revisions')));\n        $respHtml->assertElementContains('.actions a', 'Delete');\n        $respHtml->assertElementExists('form[action$=\"/delete\"]');\n    }\n\n    protected function createRevisions(Page $page, int $times, array $attrs = [])\n    {\n        $user = user();\n\n        for ($i = 0; $i < $times; $i++) {\n            $data = ['name' => 'Page update' . $i, 'summary' => 'Update entry' . $i];\n            if (!isset($attrs['markdown'])) {\n                $data['html'] = '<p>My update page</p>';\n            }\n            $this->asAdmin()->put($page->getUrl(), array_merge($data, $attrs));\n            $page->refresh();\n        }\n\n        $this->actingAs($user);\n    }\n}\n"
  },
  {
    "path": "tests/Entity/PageTemplateTest.php",
    "content": "<?php\n\nnamespace Tests\\Entity;\n\nuse BookStack\\Entities\\Models\\Page;\nuse Tests\\TestCase;\n\nclass PageTemplateTest extends TestCase\n{\n    public function test_active_templates_visible_on_page_view()\n    {\n        $page = $this->entities->page();\n\n        $this->asEditor();\n        $templateView = $this->get($page->getUrl());\n        $templateView->assertDontSee('Page Template');\n\n        $page->template = true;\n        $page->save();\n\n        $templateView = $this->get($page->getUrl());\n        $templateView->assertSee('Page Template');\n    }\n\n    public function test_manage_templates_permission_required_to_change_page_template_status()\n    {\n        $page = $this->entities->page();\n        $editor = $this->users->editor();\n        $this->actingAs($editor);\n\n        $pageUpdateData = [\n            'name'     => $page->name,\n            'html'     => $page->html,\n            'template' => 'true',\n        ];\n\n        $this->put($page->getUrl(), $pageUpdateData);\n        $this->assertDatabaseHasEntityData('page', [\n            'id'       => $page->id,\n            'template' => false,\n        ]);\n\n        $this->permissions->grantUserRolePermissions($editor, ['templates-manage']);\n\n        $this->put($page->getUrl(), $pageUpdateData);\n        $this->assertDatabaseHasEntityData('page', [\n            'id'       => $page->id,\n            'template' => true,\n        ]);\n    }\n\n    public function test_templates_content_should_be_fetchable_only_if_page_marked_as_template()\n    {\n        $content = '<div>my_custom_template_content</div>';\n        $page = $this->entities->page();\n        $editor = $this->users->editor();\n        $this->actingAs($editor);\n\n        $templateFetch = $this->get('/templates/' . $page->id);\n        $templateFetch->assertStatus(404);\n\n        $page->html = $content;\n        $page->template = true;\n        $page->save();\n\n        $templateFetch = $this->get('/templates/' . $page->id);\n        $templateFetch->assertStatus(200);\n        $templateFetch->assertJson([\n            'html'     => $content,\n            'markdown' => '',\n        ]);\n    }\n\n    public function test_template_endpoint_returns_paginated_list_of_templates()\n    {\n        $editor = $this->users->editor();\n        $this->actingAs($editor);\n\n        $toBeTemplates = Page::query()->orderBy('name', 'asc')->take(12)->get();\n        $page = $toBeTemplates->first();\n\n        $emptyTemplatesFetch = $this->get('/templates');\n        $emptyTemplatesFetch->assertDontSee($page->name);\n\n        Page::query()->whereIn('id', $toBeTemplates->pluck('id')->toArray())->update(['template' => true]);\n\n        $templatesFetch = $this->get('/templates');\n        $templatesFetch->assertSee($page->name);\n        $templatesFetch->assertSee('pagination');\n    }\n}\n"
  },
  {
    "path": "tests/Entity/PageTest.php",
    "content": "<?php\n\nnamespace Tests\\Entity;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Uploads\\Image;\nuse Carbon\\Carbon;\nuse Tests\\TestCase;\n\nclass PageTest extends TestCase\n{\n    public function test_create()\n    {\n        $chapter = $this->entities->chapter();\n        $page = Page::factory()->make([\n            'name' => 'My First Page',\n        ]);\n\n        $resp = $this->asEditor()->get($chapter->getUrl());\n        $this->withHtml($resp)->assertElementContains('a[href=\"' . $chapter->getUrl('/create-page') . '\"]', 'New Page');\n\n        $resp = $this->get($chapter->getUrl('/create-page'));\n        /** @var Page $draftPage */\n        $draftPage = Page::query()\n            ->where('draft', '=', true)\n            ->orderBy('created_at', 'desc')\n            ->first();\n        $resp->assertRedirect($draftPage->getUrl());\n\n        $resp = $this->get($draftPage->getUrl());\n        $this->withHtml($resp)->assertElementContains('form[action=\"' . $draftPage->getUrl() . '\"][method=\"POST\"]', 'Save Page');\n\n        $resp = $this->post($draftPage->getUrl(), $draftPage->only('name', 'html'));\n        $draftPage->refresh();\n        $resp->assertRedirect($draftPage->getUrl());\n    }\n\n    public function test_page_view_when_creator_is_deleted_but_owner_exists()\n    {\n        $page = $this->entities->page();\n        $user = $this->users->viewer();\n        $owner = $this->users->editor();\n        $page->created_by = $user->id;\n        $page->owned_by = $owner->id;\n        $page->save();\n        $user->delete();\n\n        $resp = $this->asAdmin()->get($page->getUrl());\n        $resp->assertStatus(200);\n        $resp->assertSeeText('Owned by ' . $owner->name);\n    }\n\n    public function test_page_show_includes_pointer_section_select_mode_button()\n    {\n        $page = $this->entities->page();\n        $resp = $this->asEditor()->get($page->getUrl());\n        $this->withHtml($resp)->assertElementContains('.content-wrap button.screen-reader-only', 'Enter section select mode');\n    }\n\n    public function test_page_creation_with_markdown_content()\n    {\n        $this->setSettings(['app-editor' => 'markdown']);\n        $book = $this->entities->book();\n\n        $this->asEditor()->get($book->getUrl('/create-page'));\n        $draft = Page::query()->where('book_id', '=', $book->id)\n            ->where('draft', '=', true)->first();\n\n        $details = [\n            'markdown' => '# a title',\n            'html'     => '<h1>a title</h1>',\n            'name'     => 'my page',\n        ];\n        $resp = $this->post($book->getUrl(\"/draft/{$draft->id}\"), $details);\n        $resp->assertRedirect();\n\n        $this->assertDatabaseHasEntityData('page', [\n            'markdown' => $details['markdown'],\n            'name'     => $details['name'],\n            'id'       => $draft->id,\n            'draft'    => false,\n        ]);\n\n        $draft->refresh();\n        $resp = $this->get($draft->getUrl('/edit'));\n        $resp->assertSee('# a title');\n    }\n\n    public function test_page_creation_allows_summary_to_be_set()\n    {\n        $book = $this->entities->book();\n\n        $this->asEditor()->get($book->getUrl('/create-page'));\n        $draft = Page::query()->where('book_id', '=', $book->id)\n            ->where('draft', '=', true)->first();\n\n        $details = [\n            'html'    => '<h1>a title</h1>',\n            'name'    => 'My page with summary',\n            'summary' => 'Here is my changelog message for a new page!',\n        ];\n        $resp = $this->post($book->getUrl(\"/draft/{$draft->id}\"), $details);\n        $resp->assertRedirect();\n\n        $this->assertDatabaseHas('page_revisions', [\n            'page_id' => $draft->id,\n            'summary' => 'Here is my changelog message for a new page!',\n        ]);\n\n        $draft->refresh();\n        $resp = $this->get($draft->getUrl('/revisions'));\n        $resp->assertSee('Here is my changelog message for a new page!');\n    }\n\n    public function test_page_delete()\n    {\n        $page = $this->entities->page();\n        $this->assertNull($page->deleted_at);\n\n        $deleteViewReq = $this->asEditor()->get($page->getUrl('/delete'));\n        $deleteViewReq->assertSeeText('Are you sure you want to delete this page?');\n\n        $deleteReq = $this->delete($page->getUrl());\n        $deleteReq->assertRedirect($page->getParent()->getUrl());\n        $this->assertActivityExists('page_delete', $page);\n\n        $page->refresh();\n        $this->assertNotNull($page->deleted_at);\n        $this->assertTrue($page->deletions()->count() === 1);\n\n        $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));\n        $this->assertNotificationContains($redirectReq, 'Page Successfully Deleted');\n    }\n\n    public function test_page_full_delete_removes_all_revisions()\n    {\n        $page = $this->entities->page();\n        $page->revisions()->create([\n            'html' => '<p>ducks</p>',\n            'name' => 'my page revision',\n            'type' => 'draft',\n        ]);\n        $page->revisions()->create([\n            'html' => '<p>ducks</p>',\n            'name' => 'my page revision',\n            'type' => 'revision',\n        ]);\n\n        $this->assertDatabaseHas('page_revisions', [\n            'page_id' => $page->id,\n        ]);\n\n        $this->asEditor()->delete($page->getUrl());\n        $this->asAdmin()->post('/settings/recycle-bin/empty');\n\n        $this->assertDatabaseMissing('page_revisions', [\n            'page_id' => $page->id,\n        ]);\n    }\n\n    public function test_page_full_delete_nulls_related_images()\n    {\n        $page = $this->entities->page();\n        $image = Image::factory()->create(['type' => 'gallery', 'uploaded_to' => $page->id]);\n\n        $this->asEditor()->delete($page->getUrl());\n        $this->asAdmin()->post('/settings/recycle-bin/empty');\n\n        $this->assertDatabaseMissing('images', [\n            'type' => 'gallery',\n            'uploaded_to' => $page->id,\n        ]);\n\n        $this->assertDatabaseHas('images', [\n            'id' => $image->id,\n            'uploaded_to' => null,\n        ]);\n    }\n\n    public function test_page_within_chapter_deletion_returns_to_chapter()\n    {\n        $chapter = $this->entities->chapter();\n        $page = $chapter->pages()->first();\n\n        $this->asEditor()->delete($page->getUrl())\n            ->assertRedirect($chapter->getUrl());\n    }\n\n    public function test_recently_updated_pages_view()\n    {\n        $user = $this->users->editor();\n        $content = $this->entities->createChainBelongingToUser($user);\n\n        $resp = $this->asAdmin()->get('/pages/recently-updated');\n        $this->withHtml($resp)->assertElementContains('.entity-list .page:nth-child(1)', $content['page']->name);\n    }\n\n    public function test_recently_updated_pages_view_shows_updated_by_details()\n    {\n        $user = $this->users->editor();\n        $page = $this->entities->page();\n\n        $this->actingAs($user)->put($page->getUrl(), [\n            'name' => 'Updated title',\n            'html' => '<p>Updated content</p>',\n        ]);\n\n        $resp = $this->asAdmin()->get('/pages/recently-updated');\n        $this->withHtml($resp)->assertElementContains('.entity-list .page:nth-child(1) small', 'by ' . $user->name);\n    }\n\n    public function test_recently_updated_pages_view_shows_parent_chain()\n    {\n        $user = $this->users->editor();\n        $page = $this->entities->pageWithinChapter();\n\n        $this->actingAs($user)->put($page->getUrl(), [\n            'name' => 'Updated title',\n            'html' => '<p>Updated content</p>',\n        ]);\n\n        $resp = $this->asAdmin()->get('/pages/recently-updated');\n        $this->withHtml($resp)->assertElementContains('.entity-list .page:nth-child(1)', $page->chapter->getShortName(42));\n        $this->withHtml($resp)->assertElementContains('.entity-list .page:nth-child(1)', $page->book->getShortName(42));\n    }\n\n    public function test_recently_updated_pages_view_does_not_show_parent_if_not_visible()\n    {\n        $user = $this->users->editor();\n        $page = $this->entities->pageWithinChapter();\n\n        $this->actingAs($user)->put($page->getUrl(), [\n            'name' => 'Updated title',\n            'html' => '<p>Updated content</p>',\n        ]);\n\n        $this->permissions->setEntityPermissions($page->book);\n        $this->permissions->setEntityPermissions($page, ['view'], [$user->roles->first()]);\n\n        $resp = $this->get('/pages/recently-updated');\n        $resp->assertDontSee($page->book->getShortName(42));\n        $resp->assertDontSee($page->chapter->getShortName(42));\n        $this->withHtml($resp)->assertElementContains('.entity-list .page:nth-child(1)', 'Updated title');\n    }\n\n    public function test_recently_updated_pages_on_home()\n    {\n        /** @var Page $page */\n        $page = Page::query()->orderBy('updated_at', 'asc')->first();\n        Page::query()->where('id', '!=', $page->id)->update([\n            'updated_at' => Carbon::now()->subSecond(1),\n        ]);\n\n        $resp = $this->asAdmin()->get('/');\n        $this->withHtml($resp)->assertElementNotContains('#recently-updated-pages', $page->name);\n\n        $this->put($page->getUrl(), [\n            'name' => $page->name,\n            'html' => $page->html,\n        ]);\n\n        $resp = $this->get('/');\n        $this->withHtml($resp)->assertElementContains('#recently-updated-pages', $page->name);\n    }\n\n    public function test_page_edit_without_update_permissions_but_with_view_redirects_to_page()\n    {\n        $page = $this->entities->page();\n\n        $resp = $this->asViewer()->get($page->getUrl('/edit'));\n        $resp->assertRedirect($page->getUrl());\n\n        $resp->assertSessionHas('error', 'You do not have permission to access the requested page.');\n    }\n}\n"
  },
  {
    "path": "tests/Entity/SlugTest.php",
    "content": "<?php\n\nnamespace Tests\\Entity;\n\nuse BookStack\\Entities\\Models\\SlugHistory;\nuse Tests\\TestCase;\n\nclass SlugTest extends TestCase\n{\n    public function test_slug_multi_byte_url_safe()\n    {\n        $book = $this->entities->newBook([\n            'name' => 'информация',\n        ]);\n\n        $this->assertEquals('informaciia', $book->slug);\n\n        $book = $this->entities->newBook([\n            'name' => '¿Qué?',\n        ]);\n\n        $this->assertEquals('que', $book->slug);\n    }\n\n    public function test_slug_format()\n    {\n        $book = $this->entities->newBook([\n            'name' => 'PartA / PartB / PartC',\n        ]);\n\n        $this->assertEquals('parta-partb-partc', $book->slug);\n    }\n\n    public function test_old_page_slugs_redirect_to_new_pages()\n    {\n        $page = $this->entities->page();\n        $pageUrl = $page->getUrl();\n\n        $this->asAdmin()->put($pageUrl, [\n            'name' => 'super test page',\n            'html' => '<p></p>',\n        ]);\n\n        $this->get($pageUrl)\n            ->assertRedirect(\"/books/{$page->book->slug}/page/super-test-page\");\n    }\n\n    public function test_old_shelf_slugs_redirect_to_new_shelf()\n    {\n        $shelf = $this->entities->shelf();\n        $shelfUrl = $shelf->getUrl();\n\n        $this->asAdmin()->put($shelf->getUrl(), [\n            'name' => 'super test shelf',\n        ]);\n\n        $this->get($shelfUrl)\n            ->assertRedirect(\"/shelves/super-test-shelf\");\n    }\n\n    public function test_old_book_slugs_redirect_to_new_book()\n    {\n        $book = $this->entities->book();\n        $bookUrl = $book->getUrl();\n\n        $this->asAdmin()->put($book->getUrl(), [\n            'name' => 'super test book',\n        ]);\n\n        $this->get($bookUrl)\n            ->assertRedirect(\"/books/super-test-book\");\n    }\n\n    public function test_old_chapter_slugs_redirect_to_new_chapter()\n    {\n        $chapter = $this->entities->chapter();\n        $chapterUrl = $chapter->getUrl();\n\n        $this->asAdmin()->put($chapter->getUrl(), [\n            'name' => 'super test chapter',\n        ]);\n\n        $this->get($chapterUrl)\n            ->assertRedirect(\"/books/{$chapter->book->slug}/chapter/super-test-chapter\");\n    }\n\n    public function test_old_book_slugs_in_page_urls_redirect_to_current_page_url()\n    {\n        $page = $this->entities->page();\n        $book = $page->book;\n        $pageUrl = $page->getUrl();\n\n        $this->asAdmin()->put($book->getUrl(), [\n            'name' => 'super test book',\n        ]);\n\n        $this->get($pageUrl)\n            ->assertRedirect(\"/books/super-test-book/page/{$page->slug}\");\n    }\n\n    public function test_old_book_slugs_in_chapter_urls_redirect_to_current_chapter_url()\n    {\n        $chapter = $this->entities->chapter();\n        $book = $chapter->book;\n        $chapterUrl = $chapter->getUrl();\n\n        $this->asAdmin()->put($book->getUrl(), [\n            'name' => 'super test book',\n        ]);\n\n        $this->get($chapterUrl)\n            ->assertRedirect(\"/books/super-test-book/chapter/{$chapter->slug}\");\n    }\n\n    public function test_slug_lookup_controlled_by_permissions()\n    {\n        $editor = $this->users->editor();\n        $pageA = $this->entities->page();\n        $pageB = $this->entities->page();\n\n        SlugHistory::factory()->create(['sluggable_id' => $pageA->id, 'sluggable_type' => 'page', 'slug' => 'monkey', 'parent_slug' => 'animals', 'created_at' => now()]);\n        SlugHistory::factory()->create(['sluggable_id' => $pageB->id, 'sluggable_type' => 'page', 'slug' => 'monkey', 'parent_slug' => 'animals', 'created_at' => now()->subDay()]);\n\n        // Defaults to latest where visible\n        $this->actingAs($editor)->get(\"/books/animals/page/monkey\")->assertRedirect($pageA->getUrl());\n\n        $this->permissions->disableEntityInheritedPermissions($pageA);\n\n        // Falls back to other entry where the latest is not visible\n        $this->actingAs($editor)->get(\"/books/animals/page/monkey\")->assertRedirect($pageB->getUrl());\n\n        // Original still accessible where permissions allow\n        $this->asAdmin()->get(\"/books/animals/page/monkey\")->assertRedirect($pageA->getUrl());\n    }\n\n    public function test_slugs_recorded_in_history_on_page_update()\n    {\n        $page = $this->entities->page();\n        $this->asAdmin()->put($page->getUrl(), [\n            'name' => 'new slug',\n            'html' => '<p></p>',\n        ]);\n\n        $oldSlug = $page->slug;\n        $page->refresh();\n        $this->assertNotEquals($oldSlug, $page->slug);\n\n        $this->assertDatabaseHas('slug_history', [\n            'sluggable_id' => $page->id,\n            'sluggable_type' => 'page',\n            'slug' => $oldSlug,\n            'parent_slug' => $page->book->slug,\n        ]);\n    }\n\n    public function test_slugs_recorded_in_history_on_chapter_update()\n    {\n        $chapter = $this->entities->chapter();\n        $this->asAdmin()->put($chapter->getUrl(), [\n            'name' => 'new slug',\n        ]);\n\n        $oldSlug = $chapter->slug;\n        $chapter->refresh();\n        $this->assertNotEquals($oldSlug, $chapter->slug);\n\n        $this->assertDatabaseHas('slug_history', [\n            'sluggable_id' => $chapter->id,\n            'sluggable_type' => 'chapter',\n            'slug' => $oldSlug,\n            'parent_slug' => $chapter->book->slug,\n        ]);\n    }\n\n    public function test_slugs_recorded_in_history_on_book_update()\n    {\n        $book = $this->entities->book();\n        $this->asAdmin()->put($book->getUrl(), [\n            'name' => 'new slug',\n        ]);\n\n        $oldSlug = $book->slug;\n        $book->refresh();\n        $this->assertNotEquals($oldSlug, $book->slug);\n\n        $this->assertDatabaseHas('slug_history', [\n            'sluggable_id' => $book->id,\n            'sluggable_type' => 'book',\n            'slug' => $oldSlug,\n            'parent_slug' => null,\n        ]);\n    }\n\n    public function test_slugs_recorded_in_history_on_shelf_update()\n    {\n        $shelf = $this->entities->shelf();\n        $this->asAdmin()->put($shelf->getUrl(), [\n            'name' => 'new slug',\n        ]);\n\n        $oldSlug = $shelf->slug;\n        $shelf->refresh();\n        $this->assertNotEquals($oldSlug, $shelf->slug);\n\n        $this->assertDatabaseHas('slug_history', [\n            'sluggable_id' => $shelf->id,\n            'sluggable_type' => 'bookshelf',\n            'slug' => $oldSlug,\n            'parent_slug' => null,\n        ]);\n    }\n}\n"
  },
  {
    "path": "tests/Entity/TagTest.php",
    "content": "<?php\n\nnamespace Tests\\Entity;\n\nuse BookStack\\Activity\\Models\\Tag;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse Tests\\TestCase;\n\nclass TagTest extends TestCase\n{\n    protected int $defaultTagCount = 20;\n\n    /**\n     * Get an instance of a page that has many tags.\n     */\n    protected function getEntityWithTags($class, ?array $tags = null): Entity\n    {\n        $entity = $class::first();\n\n        if (is_null($tags)) {\n            $tags = Tag::factory()->count($this->defaultTagCount)->make();\n        }\n\n        $entity->tags()->saveMany($tags);\n\n        return $entity;\n    }\n\n    public function test_tag_name_suggestions()\n    {\n        // Create some tags with similar names to test with\n        $attrs = collect();\n        $attrs = $attrs->merge(Tag::factory()->count(5)->make(['name' => 'country']));\n        $attrs = $attrs->merge(Tag::factory()->count(5)->make(['name' => 'color']));\n        $attrs = $attrs->merge(Tag::factory()->count(5)->make(['name' => 'city']));\n        $attrs = $attrs->merge(Tag::factory()->count(5)->make(['name' => 'county']));\n        $attrs = $attrs->merge(Tag::factory()->count(5)->make(['name' => 'planet']));\n        $attrs = $attrs->merge(Tag::factory()->count(5)->make(['name' => 'plans']));\n        $page = $this->getEntityWithTags(Page::class, $attrs->all());\n\n        $this->asAdmin()->get('/ajax/tags/suggest/names?search=dog')->assertSimilarJson([]);\n        $this->get('/ajax/tags/suggest/names?search=co')->assertSimilarJson(['color', 'country', 'county']);\n        $this->get('/ajax/tags/suggest/names?search=cou')->assertSimilarJson(['country', 'county']);\n        $this->get('/ajax/tags/suggest/names?search=pla')->assertSimilarJson(['planet', 'plans']);\n    }\n\n    public function test_tag_value_suggestions()\n    {\n        // Create some tags with similar values to test with\n        $attrs = collect();\n        $attrs = $attrs->merge(Tag::factory()->count(5)->make(['name' => 'country', 'value' => 'cats']));\n        $attrs = $attrs->merge(Tag::factory()->count(5)->make(['name' => 'color', 'value' => 'cattery']));\n        $attrs = $attrs->merge(Tag::factory()->count(5)->make(['name' => 'city', 'value' => 'castle']));\n        $attrs = $attrs->merge(Tag::factory()->count(5)->make(['name' => 'county', 'value' => 'dog']));\n        $attrs = $attrs->merge(Tag::factory()->count(5)->make(['name' => 'planet', 'value' => 'catapult']));\n        $attrs = $attrs->merge(Tag::factory()->count(5)->make(['name' => 'plans', 'value' => 'dodgy']));\n        $page = $this->getEntityWithTags(Page::class, $attrs->all());\n\n        $this->asAdmin()->get('/ajax/tags/suggest/values?search=ora')->assertSimilarJson([]);\n        $this->get('/ajax/tags/suggest/values?search=cat')->assertSimilarJson(['cats', 'cattery', 'catapult']);\n        $this->get('/ajax/tags/suggest/values?search=do')->assertSimilarJson(['dog', 'dodgy']);\n        $this->get('/ajax/tags/suggest/values?search=cas')->assertSimilarJson(['castle']);\n    }\n\n    public function test_entity_permissions_effect_tag_suggestions()\n    {\n        // Create some tags with similar names to test with and save to a page\n        $attrs = collect();\n        $attrs = $attrs->merge(Tag::factory()->count(5)->make(['name' => 'country']));\n        $attrs = $attrs->merge(Tag::factory()->count(5)->make(['name' => 'color']));\n        $page = $this->getEntityWithTags(Page::class, $attrs->all());\n\n        $this->asAdmin()->get('/ajax/tags/suggest/names?search=co')->assertSimilarJson(['color', 'country']);\n        $this->asEditor()->get('/ajax/tags/suggest/names?search=co')->assertSimilarJson(['color', 'country']);\n\n        // Set restricted permission the page\n        $this->permissions->setEntityPermissions($page, [], []);\n\n        $this->asAdmin()->get('/ajax/tags/suggest/names?search=co')->assertSimilarJson(['color', 'country']);\n        $this->asEditor()->get('/ajax/tags/suggest/names?search=co')->assertSimilarJson([]);\n    }\n\n    public function test_tags_shown_on_search_listing()\n    {\n        $tags = [\n            Tag::factory()->make(['name' => 'category', 'value' => 'buckets']),\n            Tag::factory()->make(['name' => 'color', 'value' => 'red']),\n        ];\n\n        $page = $this->getEntityWithTags(Page::class, $tags);\n        $resp = $this->asEditor()->get('/search?term=[category]');\n        $resp->assertSee($page->name);\n        $this->withHtml($resp)->assertElementContains('[href=\"' . $page->getUrl() . '\"]', 'category');\n        $this->withHtml($resp)->assertElementContains('[href=\"' . $page->getUrl() . '\"]', 'buckets');\n        $this->withHtml($resp)->assertElementContains('[href=\"' . $page->getUrl() . '\"]', 'color');\n        $this->withHtml($resp)->assertElementContains('[href=\"' . $page->getUrl() . '\"]', 'red');\n    }\n\n    public function test_tags_index_shows_tag_name_as_expected_with_right_counts()\n    {\n        $page = $this->entities->page();\n        $page->tags()->create(['name' => 'Category', 'value' => 'GreatTestContent']);\n        $page->tags()->create(['name' => 'Category', 'value' => 'OtherTestContent']);\n\n        $resp = $this->asEditor()->get('/tags');\n        $resp->assertSee('Category');\n        $html = $this->withHtml($resp);\n        $html->assertElementCount('.tag-item', 1);\n        $resp->assertDontSee('GreatTestContent');\n        $resp->assertDontSee('OtherTestContent');\n        $html->assertElementContains('a[title=\"Total tag usages\"]', '2');\n        $html->assertElementContains('a[title=\"Assigned to Pages\"]', '2');\n        $html->assertElementContains('a[title=\"Assigned to Books\"]', '0');\n        $html->assertElementContains('a[title=\"Assigned to Chapters\"]', '0');\n        $html->assertElementContains('a[title=\"Assigned to Shelves\"]', '0');\n        $html->assertElementContains('a[href$=\"/tags?name=Category\"]', '2 unique values');\n\n        $book = $this->entities->book();\n        $book->tags()->create(['name' => 'Category', 'value' => 'GreatTestContent']);\n        $resp = $this->asEditor()->get('/tags');\n        $this->withHtml($resp)->assertElementContains('a[title=\"Total tag usages\"]', '3');\n        $this->withHtml($resp)->assertElementContains('a[title=\"Assigned to Books\"]', '1');\n        $this->withHtml($resp)->assertElementContains('a[href$=\"/tags?name=Category\"]', '2 unique values');\n    }\n\n    public function test_tag_index_can_be_searched()\n    {\n        $page = $this->entities->page();\n        $page->tags()->create(['name' => 'Category', 'value' => 'GreatTestContent']);\n\n        $resp = $this->asEditor()->get('/tags?search=cat');\n        $this->withHtml($resp)->assertElementContains('.tag-item .tag-name', 'Category');\n\n        $resp = $this->asEditor()->get('/tags?search=content');\n        $this->withHtml($resp)->assertElementContains('.tag-item .tag-name', 'Category');\n        $this->withHtml($resp)->assertElementContains('.tag-item .tag-value', 'GreatTestContent');\n\n        $resp = $this->asEditor()->get('/tags?search=other');\n        $this->withHtml($resp)->assertElementNotExists('.tag-item .tag-name');\n    }\n\n    public function test_tag_index_search_will_show_mulitple_values_of_a_single_tag_name()\n    {\n        $page = $this->entities->page();\n        $page->tags()->create(['name' => 'Animal', 'value' => 'Catfish']);\n        $page->tags()->create(['name' => 'Animal', 'value' => 'Catdog']);\n\n        $resp = $this->asEditor()->get('/tags?search=cat');\n        $this->withHtml($resp)->assertElementContains('.tag-item .tag-value', 'Catfish');\n        $this->withHtml($resp)->assertElementContains('.tag-item .tag-value', 'Catdog');\n    }\n\n    public function test_tag_index_can_be_scoped_to_specific_tag_name()\n    {\n        $page = $this->entities->page();\n        $page->tags()->create(['name' => 'Category', 'value' => 'GreatTestContent']);\n        $page->tags()->create(['name' => 'Category', 'value' => 'OtherTestContent']);\n        $page->tags()->create(['name' => 'OtherTagName', 'value' => 'OtherValue']);\n\n        $resp = $this->asEditor()->get('/tags?name=Category');\n        $resp->assertSee('Category');\n        $resp->assertSee('GreatTestContent');\n        $resp->assertSee('OtherTestContent');\n        $resp->assertDontSee('OtherTagName');\n        $resp->assertSee('Active Filter:');\n        $this->withHtml($resp)->assertElementCount('.item-list .tag-item', 2);\n        $this->withHtml($resp)->assertElementContains('form[action$=\"/tags\"]', 'Clear Filter');\n    }\n\n    public function test_tags_index_adheres_to_page_permissions()\n    {\n        $page = $this->entities->page();\n        $page->tags()->create(['name' => 'SuperCategory', 'value' => 'GreatTestContent']);\n\n        $resp = $this->asEditor()->get('/tags');\n        $resp->assertSee('SuperCategory');\n        $resp = $this->get('/tags?name=SuperCategory');\n        $resp->assertSee('GreatTestContent');\n\n        $this->permissions->setEntityPermissions($page, [], []);\n\n        $resp = $this->asEditor()->get('/tags');\n        $resp->assertDontSee('SuperCategory');\n        $resp = $this->get('/tags?name=SuperCategory');\n        $resp->assertDontSee('GreatTestContent');\n    }\n\n    public function test_tag_index_shows_message_on_no_results()\n    {\n        $resp = $this->asEditor()->get('/tags?search=testingval');\n        $resp->assertSee('No items available');\n        $resp->assertSee('Tags can be assigned via the page editor sidebar');\n    }\n\n    public function test_tag_index_does_not_include_tags_on_recycle_bin_items()\n    {\n        $page = $this->entities->page();\n        $page->tags()->create(['name' => 'DeleteRecord', 'value' => 'itemToDeleteTest']);\n\n        $resp = $this->asEditor()->get('/tags');\n        $resp->assertSee('DeleteRecord');\n        $resp = $this->asEditor()->get('/tags?name=DeleteRecord');\n        $resp->assertSee('itemToDeleteTest');\n\n        $this->entities->sendToRecycleBin($page);\n\n        $resp = $this->asEditor()->get('/tags');\n        $resp->assertDontSee('DeleteRecord');\n        $resp = $this->asEditor()->get('/tags?name=DeleteRecord');\n        $resp->assertDontSee('itemToDeleteTest');\n    }\n\n    public function test_tag_classes_visible_on_entities()\n    {\n        $this->asEditor();\n\n        foreach ($this->entities->all() as $entity) {\n            $entity->tags()->create(['name' => 'My Super Tag Name', 'value' => 'An-awesome-value']);\n            $html = $this->withHtml($this->get($entity->getUrl()));\n            $html->assertElementExists('body.tag-name-mysupertagname.tag-value-anawesomevalue.tag-pair-mysupertagname-anawesomevalue');\n        }\n    }\n\n    public function test_tag_classes_are_escaped()\n    {\n        $page = $this->entities->page();\n        $page->tags()->create(['name' => '<>']);\n        $resp = $this->asEditor()->get($page->getUrl());\n        $resp->assertDontSee('tag-name-<>', false);\n        $resp->assertSee('tag-name-&lt;&gt;', false);\n    }\n\n    public function test_parent_tag_classes_visible()\n    {\n        $page = $this->entities->pageWithinChapter();\n        $page->chapter->tags()->create(['name' => 'My Chapter Tag', 'value' => 'abc123']);\n        $page->book->tags()->create(['name' => 'My Book Tag', 'value' => 'def456']);\n        $this->asEditor();\n\n        $html = $this->withHtml($this->get($page->getUrl()));\n        $html->assertElementExists('body.chapter-tag-pair-mychaptertag-abc123');\n        $html->assertElementExists('body.book-tag-pair-mybooktag-def456');\n\n        $html = $this->withHtml($this->get($page->chapter->getUrl()));\n        $html->assertElementExists('body.book-tag-pair-mybooktag-def456');\n    }\n\n    public function test_parent_tag_classes_not_visible_if_cannot_see_parent()\n    {\n        $page = $this->entities->pageWithinChapter();\n        $page->chapter->tags()->create(['name' => 'My Chapter Tag', 'value' => 'abc123']);\n        $page->book->tags()->create(['name' => 'My Book Tag', 'value' => 'def456']);\n        $editor = $this->users->editor();\n        $this->actingAs($editor);\n\n        $this->permissions->setEntityPermissions($page, ['view'], [$editor->roles()->first()]);\n        $this->permissions->disableEntityInheritedPermissions($page->chapter);\n\n        $html = $this->withHtml($this->get($page->getUrl()));\n        $html->assertElementNotExists('body.chapter-tag-pair-mychaptertag-abc123');\n        $html->assertElementExists('body.book-tag-pair-mybooktag-def456');\n\n        $this->permissions->disableEntityInheritedPermissions($page->book);\n        $html = $this->withHtml($this->get($page->getUrl()));\n        $html->assertElementNotExists('body.book-tag-pair-mybooktag-def456');\n    }\n}\n"
  },
  {
    "path": "tests/ErrorTest.php",
    "content": "<?php\n\nnamespace Tests;\n\nuse Illuminate\\Foundation\\Http\\Middleware\\ValidatePostSize;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass ErrorTest extends TestCase\n{\n    public function test_404_page_does_not_show_login()\n    {\n        // Due to middleware being handled differently this will not fail\n        // if our custom, middleware-loaded handler fails but this is here\n        // as a reminder and as a general check in the event of other issues.\n        $editor = $this->users->editor();\n        $editor->name = 'tester';\n        $editor->save();\n\n        $this->actingAs($editor);\n        $notFound = $this->get('/fgfdngldfnotfound');\n        $notFound->assertStatus(404);\n        $notFound->assertDontSeeText('Log in');\n        $notFound->assertSeeText('tester');\n    }\n\n    public function test_404_page_does_not_non_visible_content()\n    {\n        $editor = $this->users->editor();\n        $book = $this->entities->book();\n\n        $this->actingAs($editor)->get($book->getUrl())->assertOk();\n\n        $this->permissions->disableEntityInheritedPermissions($book);\n\n        $this->actingAs($editor)->get($book->getUrl())->assertNotFound();\n    }\n\n    public function test_404_page_shows_visible_content_within_non_visible_parent()\n    {\n        $editor = $this->users->editor();\n        $book = $this->entities->book();\n        $page = $book->pages()->first();\n\n        $this->actingAs($editor)->get($page->getUrl())->assertOk();\n\n        $this->permissions->disableEntityInheritedPermissions($book);\n        $this->permissions->addEntityPermission($page, ['view'], $editor->roles()->first());\n\n        $resp = $this->actingAs($editor)->get($book->getUrl());\n        $resp->assertNotFound();\n        $resp->assertSee($page->name);\n        $resp->assertDontSee($book->name);\n    }\n\n    public function test_item_not_found_does_not_get_logged_to_file()\n    {\n        $this->actingAs($this->users->viewer());\n        $handler = $this->withTestLogger();\n        $book = $this->entities->book();\n\n        // Ensure we're seeing errors\n        Log::error('cat');\n        $this->assertTrue($handler->hasErrorThatContains('cat'));\n\n        $this->get('/books/arandomnotfouindbook');\n        $this->get($book->getUrl('/chapter/arandomnotfouindchapter'));\n        $this->get($book->getUrl('/chapter/arandomnotfouindpages'));\n\n        $this->assertCount(1, $handler->getRecords());\n    }\n\n    public function test_access_to_non_existing_image_location_provides_404_response()\n    {\n        $resp = $this->actingAs($this->users->viewer())->get('/uploads/images/gallery/2021-05/anonexistingimage.png');\n        $resp->assertStatus(404);\n        $resp->assertSeeText('Image Not Found');\n    }\n\n    public function test_posts_above_php_limit_shows_friendly_error()\n    {\n        // Fake super large JSON request\n        $resp = $this->asEditor()->call('GET', '/books', [], [], [], [\n            'CONTENT_LENGTH' => '10000000000',\n            'HTTP_ACCEPT' => 'application/json',\n        ]);\n\n        $resp->assertStatus(413);\n        $resp->assertJson(['error' => 'The server cannot receive the provided amount of data. Try again with less data or a smaller file.']);\n    }\n}\n"
  },
  {
    "path": "tests/Exports/ExportUiTest.php",
    "content": "<?php\n\nnamespace Tests\\Exports;\n\nuse BookStack\\Entities\\Models\\Book;\nuse Tests\\TestCase;\n\nclass ExportUiTest extends TestCase\n{\n    public function test_export_option_only_visible_and_accessible_with_permission()\n    {\n        $book = Book::query()->whereHas('pages')->whereHas('chapters')->first();\n        $chapter = $book->chapters()->first();\n        $page = $chapter->pages()->first();\n        $entities = [$book, $chapter, $page];\n        $user = $this->users->viewer();\n        $this->actingAs($user);\n\n        foreach ($entities as $entity) {\n            $resp = $this->get($entity->getUrl());\n            $resp->assertSee('/export/pdf');\n        }\n\n        $this->permissions->removeUserRolePermissions($user, ['content-export']);\n\n        foreach ($entities as $entity) {\n            $resp = $this->get($entity->getUrl());\n            $resp->assertDontSee('/export/pdf');\n            $resp = $this->get($entity->getUrl('/export/pdf'));\n            $this->assertPermissionError($resp);\n        }\n    }\n}\n"
  },
  {
    "path": "tests/Exports/HtmlExportTest.php",
    "content": "<?php\n\nnamespace Tests\\Exports;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Page;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Tests\\TestCase;\n\nclass HtmlExportTest extends TestCase\n{\n    public function test_page_html_export()\n    {\n        $page = $this->entities->page();\n        $this->asEditor();\n\n        $resp = $this->get($page->getUrl('/export/html'));\n        $resp->assertStatus(200);\n        $resp->assertSee($page->name);\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $page->slug . '.html');\n    }\n\n    public function test_book_html_export()\n    {\n        $page = $this->entities->page();\n        $book = $page->book;\n        $this->asEditor();\n\n        $resp = $this->get($book->getUrl('/export/html'));\n        $resp->assertStatus(200);\n        $resp->assertSee($book->name);\n        $resp->assertSee($page->name);\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $book->slug . '.html');\n    }\n\n    public function test_book_html_export_shows_html_descriptions()\n    {\n        $book = $this->entities->bookHasChaptersAndPages();\n        $chapter = $book->chapters()->first();\n        $book->description_html = '<p>A description with <strong>HTML</strong> within!</p>';\n        $chapter->description_html = '<p>A chapter description with <strong>HTML</strong> within!</p>';\n        $book->save();\n        $chapter->save();\n\n        $resp = $this->asEditor()->get($book->getUrl('/export/html'));\n        $resp->assertSee($book->description_html, false);\n        $resp->assertSee($chapter->description_html, false);\n    }\n\n    public function test_chapter_html_export()\n    {\n        $chapter = $this->entities->chapter();\n        $page = $chapter->pages[0];\n        $this->asEditor();\n\n        $resp = $this->get($chapter->getUrl('/export/html'));\n        $resp->assertStatus(200);\n        $resp->assertSee($chapter->name);\n        $resp->assertSee($page->name);\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $chapter->slug . '.html');\n    }\n\n    public function test_chapter_html_export_shows_html_descriptions()\n    {\n        $chapter = $this->entities->chapter();\n        $chapter->description_html = '<p>A description with <strong>HTML</strong> within!</p>';\n        $chapter->save();\n\n        $resp = $this->asEditor()->get($chapter->getUrl('/export/html'));\n        $resp->assertSee($chapter->description_html, false);\n    }\n\n    public function test_page_html_export_contains_custom_head_if_set()\n    {\n        $page = $this->entities->page();\n\n        $customHeadContent = '<style>p{color: red;}</style>';\n        $this->setSettings(['app-custom-head' => $customHeadContent]);\n\n        $resp = $this->asEditor()->get($page->getUrl('/export/html'));\n        $resp->assertSee($customHeadContent, false);\n    }\n\n    public function test_page_html_export_does_not_break_with_only_comments_in_custom_head()\n    {\n        $page = $this->entities->page();\n\n        $customHeadContent = '<!-- A comment -->';\n        $this->setSettings(['app-custom-head' => $customHeadContent]);\n\n        $resp = $this->asEditor()->get($page->getUrl('/export/html'));\n        $resp->assertStatus(200);\n        $resp->assertSee($customHeadContent, false);\n    }\n\n    public function test_page_html_export_use_absolute_dates()\n    {\n        $page = $this->entities->page();\n\n        $resp = $this->asEditor()->get($page->getUrl('/export/html'));\n        $resp->assertSee($page->created_at->format('Y-m-d H:i:s T'));\n        $resp->assertDontSee($page->created_at->diffForHumans());\n        $resp->assertSee($page->updated_at->format('Y-m-d H:i:s T'));\n        $resp->assertDontSee($page->updated_at->diffForHumans());\n    }\n\n    public function test_page_export_does_not_include_user_or_revision_links()\n    {\n        $page = $this->entities->page();\n\n        $resp = $this->asEditor()->get($page->getUrl('/export/html'));\n        $resp->assertDontSee($page->getUrl('/revisions'));\n        $resp->assertDontSee($page->createdBy->getProfileUrl());\n        $resp->assertSee($page->createdBy->name);\n    }\n\n    public function test_page_export_sets_right_data_type_for_svg_embeds()\n    {\n        $page = $this->entities->page();\n        Storage::disk('local')->makeDirectory('uploads/images/gallery');\n        Storage::disk('local')->put('uploads/images/gallery/svg_test.svg', '<svg></svg>');\n        $page->html = '<img src=\"http://localhost/uploads/images/gallery/svg_test.svg\">';\n        $page->save();\n\n        $this->asEditor();\n        $resp = $this->get($page->getUrl('/export/html'));\n        Storage::disk('local')->delete('uploads/images/gallery/svg_test.svg');\n\n        $resp->assertStatus(200);\n        $resp->assertSee('<img src=\"data:image/svg+xml;base64', false);\n    }\n\n    public function test_page_image_containment_works_on_multiple_images_within_a_single_line()\n    {\n        $page = $this->entities->page();\n        Storage::disk('local')->makeDirectory('uploads/images/gallery');\n        Storage::disk('local')->put('uploads/images/gallery/svg_test.svg', '<svg></svg>');\n        Storage::disk('local')->put('uploads/images/gallery/svg_test2.svg', '<svg></svg>');\n        $page->html = '<img src=\"http://localhost/uploads/images/gallery/svg_test.svg\" class=\"a\"><img src=\"http://localhost/uploads/images/gallery/svg_test2.svg\" class=\"b\">';\n        $page->save();\n\n        $resp = $this->asEditor()->get($page->getUrl('/export/html'));\n        Storage::disk('local')->delete('uploads/images/gallery/svg_test.svg');\n        Storage::disk('local')->delete('uploads/images/gallery/svg_test2.svg');\n\n        $resp->assertDontSee('http://localhost/uploads/images/gallery/svg_test');\n    }\n\n    public function test_page_export_contained_html_image_fetches_only_run_when_url_points_to_image_upload_folder()\n    {\n        $page = $this->entities->page();\n        $page->html = '<img src=\"http://localhost/uploads/images/gallery/svg_test.svg\"/>'\n            . '<img src=\"http://localhost/uploads/svg_test.svg\"/>'\n            . '<img src=\"/uploads/svg_test.svg\"/>';\n        $storageDisk = Storage::disk('local');\n        $storageDisk->makeDirectory('uploads/images/gallery');\n        $storageDisk->put('uploads/images/gallery/svg_test.svg', '<svg>good</svg>');\n        $storageDisk->put('uploads/svg_test.svg', '<svg>bad</svg>');\n        $page->save();\n\n        $resp = $this->asEditor()->get($page->getUrl('/export/html'));\n\n        $storageDisk->delete('uploads/images/gallery/svg_test.svg');\n        $storageDisk->delete('uploads/svg_test.svg');\n\n        $resp->assertDontSee('http://localhost/uploads/images/gallery/svg_test.svg', false);\n        $resp->assertSee('http://localhost/uploads/svg_test.svg');\n        $resp->assertSee('src=\"/uploads/svg_test.svg\"', false);\n    }\n\n    public function test_page_export_contained_html_does_not_allow_upward_traversal_with_local()\n    {\n        $contents = file_get_contents(public_path('.htaccess'));\n        config()->set('filesystems.images', 'local');\n\n        $page = $this->entities->page();\n        $page->html = '<img src=\"http://localhost/uploads/images/../../.htaccess\"/>';\n        $page->save();\n\n        $resp = $this->asEditor()->get($page->getUrl('/export/html'));\n        $resp->assertDontSee(base64_encode($contents));\n    }\n\n    public function test_page_export_contained_html_does_not_allow_upward_traversal_with_local_secure()\n    {\n        $testFilePath = storage_path('logs/test.txt');\n        config()->set('filesystems.images', 'local_secure');\n        file_put_contents($testFilePath, 'I am a cat');\n\n        $page = $this->entities->page();\n        $page->html = '<img src=\"http://localhost/uploads/images/../../logs/test.txt\"/>';\n        $page->save();\n\n        $resp = $this->asEditor()->get($page->getUrl('/export/html'));\n        $resp->assertDontSee(base64_encode('I am a cat'));\n        unlink($testFilePath);\n    }\n\n    public function test_exports_removes_scripts_from_custom_head()\n    {\n        $entities = [\n            Page::query()->first(), Chapter::query()->first(), Book::query()->first(),\n        ];\n        setting()->put('app-custom-head', '<script>window.donkey = \"cat\";</script><style>.my-test-class { color: red; }</style>');\n\n        foreach ($entities as $entity) {\n            $resp = $this->asEditor()->get($entity->getUrl('/export/html'));\n            $resp->assertDontSee('window.donkey');\n            $resp->assertDontSee('<script', false);\n            $resp->assertSee('.my-test-class { color: red; }');\n        }\n    }\n\n    public function test_page_export_with_deleted_creator_and_updater()\n    {\n        $user = $this->users->viewer(['name' => 'ExportWizardTheFifth']);\n        $page = $this->entities->page();\n        $page->created_by = $user->id;\n        $page->updated_by = $user->id;\n        $page->save();\n\n        $resp = $this->asEditor()->get($page->getUrl('/export/html'));\n        $resp->assertSee('ExportWizardTheFifth');\n\n        $user->delete();\n        $resp = $this->get($page->getUrl('/export/html'));\n        $resp->assertStatus(200);\n        $resp->assertDontSee('ExportWizardTheFifth');\n    }\n\n    public function test_html_exports_contain_csp_meta_tag()\n    {\n        $entities = [\n            $this->entities->page(),\n            $this->entities->book(),\n            $this->entities->chapter(),\n        ];\n\n        foreach ($entities as $entity) {\n            $resp = $this->asEditor()->get($entity->getUrl('/export/html'));\n            $this->withHtml($resp)->assertElementExists('head meta[http-equiv=\"Content-Security-Policy\"][content*=\"script-src \"]');\n        }\n    }\n\n    public function test_html_exports_contain_body_classes_for_export_identification()\n    {\n        $page = $this->entities->page();\n\n        $resp = $this->asEditor()->get($page->getUrl('/export/html'));\n        $this->withHtml($resp)->assertElementExists('body.export.export-format-html.export-engine-none');\n    }\n}\n"
  },
  {
    "path": "tests/Exports/MarkdownExportTest.php",
    "content": "<?php\n\nnamespace Tests\\Exports;\n\nuse BookStack\\Entities\\Models\\Book;\nuse Tests\\TestCase;\n\nclass MarkdownExportTest extends TestCase\n{\n    public function test_page_markdown_export()\n    {\n        $page = $this->entities->page();\n\n        $resp = $this->asEditor()->get($page->getUrl('/export/markdown'));\n        $resp->assertStatus(200);\n        $resp->assertSee($page->name);\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $page->slug . '.md');\n    }\n\n    public function test_page_markdown_export_uses_existing_markdown_if_apparent()\n    {\n        $page = $this->entities->page()->forceFill([\n            'markdown' => '# A header',\n            'html'     => '<h1>Dogcat</h1>',\n        ]);\n        $page->save();\n\n        $resp = $this->asEditor()->get($page->getUrl('/export/markdown'));\n        $resp->assertSee('A header');\n        $resp->assertDontSee('Dogcat');\n    }\n\n    public function test_page_markdown_export_converts_html_where_no_markdown()\n    {\n        $page = $this->entities->page()->forceFill([\n            'markdown' => '',\n            'html'     => '<h1>Dogcat</h1><p>Some <strong>bold</strong> text</p>',\n        ]);\n        $page->save();\n\n        $resp = $this->asEditor()->get($page->getUrl('/export/markdown'));\n        $resp->assertSee(\"# Dogcat\\n\\nSome **bold** text\");\n    }\n\n    public function test_chapter_markdown_export()\n    {\n        $chapter = $this->entities->chapter();\n        $chapter->description_html = '<p>My <strong>chapter</strong> description</p>';\n        $chapter->save();\n        $page = $chapter->pages()->first();\n\n        $resp = $this->asEditor()->get($chapter->getUrl('/export/markdown'));\n\n        $resp->assertSee('# ' . $chapter->name);\n        $resp->assertSee('# ' . $page->name);\n        $resp->assertSee('My **chapter** description');\n    }\n\n    public function test_chapter_markdown_export_pages_are_permission_controlled()\n    {\n        $chapter = $this->entities->chapterHasPages();\n        $page = $chapter->pages()->first();\n        $page->name = 'MyPageWhichShouldNotBeFound';\n        $page->save();\n        $this->permissions->disableEntityInheritedPermissions($page);\n\n        $resp = $this->asEditor()->get($chapter->getUrl('/export/markdown'));\n\n        $resp->assertSee('# ' . $chapter->name);\n        $resp->assertDontSee('MyPageWhichShouldNotBeFound');\n    }\n\n    public function test_book_markdown_export()\n    {\n        $book = Book::query()->whereHas('pages')->whereHas('chapters')->first();\n        $book->description_html = '<p>My <strong>book</strong> description</p>';\n        $book->save();\n\n        $chapter = $book->chapters()->first();\n        $chapter->description_html = '<p>My <strong>chapter</strong> description</p>';\n        $chapter->save();\n\n        $page = $chapter->pages()->first();\n        $resp = $this->asEditor()->get($book->getUrl('/export/markdown'));\n\n        $resp->assertSee('# ' . $book->name);\n        $resp->assertSee('# ' . $chapter->name);\n        $resp->assertSee('# ' . $page->name);\n        $resp->assertSee('My **book** description');\n        $resp->assertSee('My **chapter** description');\n    }\n\n    public function test_book_markdown_export_chapters_are_permission_controlled()\n    {\n        $book = $this->entities->bookHasChaptersAndPages();\n        $chapter = $book->chapters()->first();\n        $page = $chapter->pages()->first();\n        $page->name = 'MyPageWhichShouldNotBeFound';\n        $page->save();\n        $chapter->name = 'MyChapterWhichShouldNotBeFound';\n        $chapter->save();\n        $this->permissions->disableEntityInheritedPermissions($chapter);\n\n        $resp = $this->asEditor()->get($book->getUrl('/export/markdown'));\n\n        $resp->assertSee('# ' . $book->name);\n        $resp->assertDontSee('MyChapterWhichShouldNotBeFound');\n        $resp->assertDontSee('MyPageWhichShouldNotBeFound');\n    }\n\n    public function test_book_markdown_export_direct_pages_are_permission_controlled()\n    {\n        $book = $this->entities->bookHasChaptersAndPages();\n        $page = $book->directPages()->first();\n        $page->name = 'MyPageWhichShouldNotBeFound';\n        $page->save();\n        $this->permissions->disableEntityInheritedPermissions($page);\n\n        $resp = $this->asEditor()->get($book->getUrl('/export/markdown'));\n\n        $resp->assertSee('# ' . $book->name);\n        $resp->assertDontSee('MyPageWhichShouldNotBeFound');\n    }\n\n    public function test_book_markdown_export_concats_immediate_pages_with_newlines()\n    {\n        /** @var Book $book */\n        $book = Book::query()->whereHas('pages')->first();\n\n        $this->asEditor()->get($book->getUrl('/create-page'));\n        $this->get($book->getUrl('/create-page'));\n\n        [$pageA, $pageB] = $book->pages()->whereNull('chapter_id')->get();\n        $pageA->html = '<p>hello tester</p>';\n        $pageA->save();\n        $pageB->name = 'The second page in this test';\n        $pageB->save();\n\n        $resp = $this->get($book->getUrl('/export/markdown'));\n        $resp->assertDontSee('hello tester# The second page in this test');\n        $resp->assertSee(\"hello tester\\n\\n# The second page in this test\");\n    }\n}\n"
  },
  {
    "path": "tests/Exports/PdfExportTest.php",
    "content": "<?php\n\nnamespace Tests\\Exports;\n\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Exceptions\\PdfExportException;\nuse BookStack\\Exports\\PdfGenerator;\nuse FilesystemIterator;\nuse Tests\\TestCase;\n\nclass PdfExportTest extends TestCase\n{\n    public function test_page_pdf_export()\n    {\n        $page = $this->entities->page();\n        $this->asEditor();\n\n        $resp = $this->get($page->getUrl('/export/pdf'));\n        $resp->assertStatus(200);\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $page->slug . '.pdf');\n    }\n\n    public function test_book_pdf_export()\n    {\n        $page = $this->entities->page();\n        $book = $page->book;\n        $this->asEditor();\n\n        $resp = $this->get($book->getUrl('/export/pdf'));\n        $resp->assertStatus(200);\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $book->slug . '.pdf');\n    }\n\n    public function test_chapter_pdf_export()\n    {\n        $chapter = $this->entities->chapter();\n        $this->asEditor();\n\n        $resp = $this->get($chapter->getUrl('/export/pdf'));\n        $resp->assertStatus(200);\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $chapter->slug . '.pdf');\n    }\n\n\n    public function test_page_pdf_export_converts_iframes_to_links()\n    {\n        $page = Page::query()->first()->forceFill([\n            'html'     => '<iframe width=\"560\" height=\"315\" src=\"//www.youtube.com/embed/ShqUjt33uOs\"></iframe>',\n        ]);\n        $page->save();\n\n        $pdfHtml = '';\n        $mockPdfGenerator = $this->mock(PdfGenerator::class);\n        $mockPdfGenerator->shouldReceive('fromHtml')\n            ->with(\\Mockery::capture($pdfHtml))\n            ->andReturn('');\n        $mockPdfGenerator->shouldReceive('getActiveEngine')->andReturn(PdfGenerator::ENGINE_DOMPDF);\n\n        $this->asEditor()->get($page->getUrl('/export/pdf'));\n        $this->assertStringNotContainsString('iframe>', $pdfHtml);\n        $this->assertStringContainsString('<p><a href=\"https://www.youtube.com/embed/ShqUjt33uOs\">https://www.youtube.com/embed/ShqUjt33uOs</a></p>', $pdfHtml);\n    }\n\n    public function test_page_pdf_export_opens_details_blocks()\n    {\n        $page = $this->entities->page()->forceFill([\n            'html'     => '<details><summary>Hello</summary><p>Content!</p></details>',\n        ]);\n        $page->save();\n\n        $pdfHtml = '';\n        $mockPdfGenerator = $this->mock(PdfGenerator::class);\n        $mockPdfGenerator->shouldReceive('fromHtml')\n            ->with(\\Mockery::capture($pdfHtml))\n            ->andReturn('');\n        $mockPdfGenerator->shouldReceive('getActiveEngine')->andReturn(PdfGenerator::ENGINE_DOMPDF);\n\n        $this->asEditor()->get($page->getUrl('/export/pdf'));\n        $this->assertStringContainsString('<details open=\"open\"', $pdfHtml);\n    }\n\n    public function test_wkhtmltopdf_only_used_when_allow_untrusted_is_true()\n    {\n        $page = $this->entities->page();\n\n        config()->set('exports.snappy.pdf_binary', '/abc123');\n        config()->set('app.allow_untrusted_server_fetching', false);\n\n        $resp = $this->asEditor()->get($page->getUrl('/export/pdf'));\n        $resp->assertStatus(200); // Sucessful response with invalid snappy binary indicates dompdf usage.\n\n        config()->set('app.allow_untrusted_server_fetching', true);\n        $resp = $this->get($page->getUrl('/export/pdf'));\n        $resp->assertStatus(500); // Bad response indicates wkhtml usage\n    }\n\n    public function test_pdf_command_option_used_if_set()\n    {\n        $page = $this->entities->page();\n        $command = 'cp {input_html_path} {output_pdf_path}';\n        config()->set('exports.pdf_command', $command);\n\n        $resp = $this->asEditor()->get($page->getUrl('/export/pdf'));\n        $download = $resp->getContent();\n\n        $this->assertStringContainsString(e($page->name), $download);\n        $this->assertStringContainsString('<html lang=', $download);\n    }\n\n    public function test_pdf_command_option_errors_if_output_path_not_written_to()\n    {\n        $page = $this->entities->page();\n        $command = 'echo \"hi\"';\n        config()->set('exports.pdf_command', $command);\n\n        $this->assertThrows(function () use ($page) {\n            $this->withoutExceptionHandling()->asEditor()->get($page->getUrl('/export/pdf'));\n        }, PdfExportException::class);\n    }\n\n    public function test_pdf_command_option_errors_if_command_returns_error_status()\n    {\n        $page = $this->entities->page();\n        $command = 'exit 1';\n        config()->set('exports.pdf_command', $command);\n\n        $this->assertThrows(function () use ($page) {\n            $this->withoutExceptionHandling()->asEditor()->get($page->getUrl('/export/pdf'));\n        }, PdfExportException::class);\n    }\n\n    public function test_pdf_command_timeout_option_limits_export_time()\n    {\n        $page = $this->entities->page();\n        $command = 'php -r \\'sleep(4);\\'';\n        config()->set('exports.pdf_command', $command);\n        config()->set('exports.pdf_command_timeout', 1);\n\n        $this->assertThrows(function () use ($page) {\n            $start = time();\n            $this->withoutExceptionHandling()->asEditor()->get($page->getUrl('/export/pdf'));\n\n            $this->assertTrue(time() < ($start + 3));\n        }, PdfExportException::class,\n            \"PDF Export via command failed due to timeout at 1 second(s)\");\n    }\n\n    public function test_pdf_command_option_does_not_leave_temp_files()\n    {\n        $tempDir = sys_get_temp_dir();\n        $startTempFileCount = iterator_count((new FileSystemIterator($tempDir, FilesystemIterator::SKIP_DOTS)));\n\n        $page = $this->entities->page();\n        $command = 'cp {input_html_path} {output_pdf_path}';\n        config()->set('exports.pdf_command', $command);\n\n        $this->asEditor()->get($page->getUrl('/export/pdf'));\n\n        $afterTempFileCount = iterator_count((new FileSystemIterator($tempDir, FilesystemIterator::SKIP_DOTS)));\n        $this->assertEquals($startTempFileCount, $afterTempFileCount);\n    }\n}\n"
  },
  {
    "path": "tests/Exports/TextExportTest.php",
    "content": "<?php\n\nnamespace Tests\\Exports;\n\nuse Tests\\TestCase;\n\nclass TextExportTest extends TestCase\n{\n    public function test_page_text_export()\n    {\n        $page = $this->entities->page();\n        $this->asEditor();\n\n        $resp = $this->get($page->getUrl('/export/plaintext'));\n        $resp->assertStatus(200);\n        $resp->assertSee($page->name);\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $page->slug . '.txt');\n    }\n\n    public function test_book_text_export()\n    {\n        $book = $this->entities->bookHasChaptersAndPages();\n        $directPage = $book->directPages()->first();\n        $chapter = $book->chapters()->first();\n        $chapterPage = $chapter->pages()->first();\n        $this->entities->updatePage($directPage, ['html' => '<p>My awesome page</p>']);\n        $this->entities->updatePage($chapterPage, ['html' => '<p>My little nested page</p>']);\n        $this->asEditor();\n\n        $resp = $this->get($book->getUrl('/export/plaintext'));\n        $resp->assertStatus(200);\n        $resp->assertSee($book->name);\n        $resp->assertSee($chapterPage->name);\n        $resp->assertSee($chapter->name);\n        $resp->assertSee($directPage->name);\n        $resp->assertSee('My awesome page');\n        $resp->assertSee('My little nested page');\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $book->slug . '.txt');\n    }\n\n    public function test_book_text_export_format()\n    {\n        $entities = $this->entities->createChainBelongingToUser($this->users->viewer());\n        $this->entities->updatePage($entities['page'], ['html' => '<p>My great page</p><p>Full of <strong>great</strong> stuff</p>', 'name' => 'My wonderful page!']);\n        $entities['chapter']->name = 'Export chapter';\n        $entities['chapter']->description = \"A test chapter to be exported\\nIt has loads of info within\";\n        $entities['book']->name = 'Export Book';\n        $entities['book']->description = \"This is a book with stuff to export\";\n        $entities['chapter']->save();\n        $entities['book']->save();\n\n        $resp = $this->asEditor()->get($entities['book']->getUrl('/export/plaintext'));\n\n        $expected = \"Export Book\\nThis is a book with stuff to export\\n\\nExport chapter\\nA test chapter to be exported\\nIt has loads of info within\\n\\n\";\n        $expected .= \"My wonderful page!\\nMy great page Full of great stuff\";\n        $resp->assertSee($expected);\n    }\n\n    public function test_chapter_text_export()\n    {\n        $chapter = $this->entities->chapter();\n        $page = $chapter->pages[0];\n        $this->entities->updatePage($page, ['html' => '<p>This is content within the page!</p>']);\n        $this->asEditor();\n\n        $resp = $this->get($chapter->getUrl('/export/plaintext'));\n        $resp->assertStatus(200);\n        $resp->assertSee($chapter->name);\n        $resp->assertSee($page->name);\n        $resp->assertSee('This is content within the page!');\n        $resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\\'\\'' . $chapter->slug . '.txt');\n    }\n\n    public function test_chapter_text_export_format()\n    {\n        $entities = $this->entities->createChainBelongingToUser($this->users->viewer());\n        $this->entities->updatePage($entities['page'], ['html' => '<p>My great page</p><p>Full of <strong>great</strong> stuff</p>', 'name' => 'My wonderful page!']);\n        $entities['chapter']->name = 'Export chapter';\n        $entities['chapter']->description = \"A test chapter to be exported\\nIt has loads of info within\";\n        $entities['chapter']->save();\n\n        $resp = $this->asEditor()->get($entities['book']->getUrl('/export/plaintext'));\n\n        $expected = \"Export chapter\\nA test chapter to be exported\\nIt has loads of info within\\n\\n\";\n        $expected .= \"My wonderful page!\\nMy great page Full of great stuff\";\n        $resp->assertSee($expected);\n    }\n}\n"
  },
  {
    "path": "tests/Exports/ZipExportTest.php",
    "content": "<?php\n\nnamespace Tests\\Exports;\n\nuse BookStack\\Activity\\Models\\Tag;\nuse BookStack\\Entities\\Repos\\BookRepo;\nuse BookStack\\Entities\\Tools\\PageContent;\nuse BookStack\\Uploads\\Attachment;\nuse BookStack\\Uploads\\Image;\nuse FilesystemIterator;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Testing\\TestResponse;\nuse Tests\\TestCase;\nuse ZipArchive;\n\nclass ZipExportTest extends TestCase\n{\n    public function test_export_results_in_zip_format()\n    {\n        $page = $this->entities->page();\n        $response = $this->asEditor()->get($page->getUrl(\"/export/zip\"));\n\n        $zipData = $response->streamedContent();\n        $zipFile = tempnam(sys_get_temp_dir(), 'bstesta-');\n        file_put_contents($zipFile, $zipData);\n        $zip = new ZipArchive();\n        $zip->open($zipFile, ZipArchive::RDONLY);\n\n        $this->assertNotFalse($zip->locateName('data.json'));\n        $this->assertNotFalse($zip->locateName('files/'));\n\n        $data = json_decode($zip->getFromName('data.json'), true);\n        $this->assertIsArray($data);\n        $this->assertGreaterThan(0, count($data));\n\n        $zip->close();\n        unlink($zipFile);\n    }\n\n    public function test_export_metadata()\n    {\n        $page = $this->entities->page();\n        $zipResp = $this->asEditor()->get($page->getUrl(\"/export/zip\"));\n        $zip = ZipTestHelper::extractFromZipResponse($zipResp);\n\n        $this->assertEquals($page->id, $zip->data['page']['id'] ?? null);\n        $this->assertArrayNotHasKey('book', $zip->data);\n        $this->assertArrayNotHasKey('chapter', $zip->data);\n\n        $now = time();\n        $date = Carbon::parse($zip->data['exported_at'])->unix();\n        $this->assertLessThan($now + 2, $date);\n        $this->assertGreaterThan($now - 2, $date);\n\n        $version = trim(file_get_contents(base_path('version')));\n        $this->assertEquals($version, $zip->data['instance']['version']);\n\n        $zipInstanceId = $zip->data['instance']['id'];\n        $instanceId = setting('instance-id');\n        $this->assertNotEmpty($instanceId);\n        $this->assertEquals($instanceId, $zipInstanceId);\n    }\n\n    public function test_export_leaves_no_temp_files()\n    {\n        $tempDir = sys_get_temp_dir();\n        $startTempFileCount = iterator_count((new FileSystemIterator($tempDir, FilesystemIterator::SKIP_DOTS)));\n\n        $page = $this->entities->pageWithinChapter();\n        $this->asEditor();\n        $pageResp = $this->get($page->getUrl(\"/export/zip\"));\n        $pageResp->streamedContent();\n        $pageResp->assertOk();\n        $this->get($page->chapter->getUrl(\"/export/zip\"))->assertOk();\n        $this->get($page->book->getUrl(\"/export/zip\"))->assertOk();\n\n        $afterTempFileCount = iterator_count((new FileSystemIterator($tempDir, FilesystemIterator::SKIP_DOTS)));\n\n        $this->assertEquals($startTempFileCount, $afterTempFileCount);\n    }\n\n    public function test_page_export()\n    {\n        $page = $this->entities->page();\n        $zipResp = $this->asEditor()->get($page->getUrl(\"/export/zip\"));\n        $zip = ZipTestHelper::extractFromZipResponse($zipResp);\n\n        $pageData = $zip->data['page'];\n        $this->assertEquals([\n            'id' => $page->id,\n            'name' => $page->name,\n            'html' => (new PageContent($page))->render(),\n            'priority' => $page->priority,\n            'attachments' => [],\n            'images' => [],\n            'tags' => [],\n        ], $pageData);\n    }\n\n    public function test_page_export_with_markdown()\n    {\n        $page = $this->entities->page();\n        $markdown = \"# My page\\n\\nwritten in markdown for export\\n\";\n        $page->markdown = $markdown;\n        $page->save();\n\n        $zipResp = $this->asEditor()->get($page->getUrl(\"/export/zip\"));\n        $zip = ZipTestHelper::extractFromZipResponse($zipResp);\n\n        $pageData = $zip->data['page'];\n        $this->assertEquals($markdown, $pageData['markdown']);\n        $this->assertNotEmpty($pageData['html']);\n    }\n\n    public function test_page_export_with_tags()\n    {\n        $page = $this->entities->page();\n        $page->tags()->saveMany([\n            new Tag(['name' => 'Exporty', 'value' => 'Content', 'order' => 1]),\n            new Tag(['name' => 'Another', 'value' => '', 'order' => 2]),\n        ]);\n\n        $zipResp = $this->asEditor()->get($page->getUrl(\"/export/zip\"));\n        $zip = ZipTestHelper::extractFromZipResponse($zipResp);\n\n        $pageData = $zip->data['page'];\n        $this->assertEquals([\n            [\n                'name' => 'Exporty',\n                'value' => 'Content',\n            ],\n            [\n                'name' => 'Another',\n                'value' => '',\n            ]\n        ], $pageData['tags']);\n    }\n\n    public function test_page_export_with_images()\n    {\n        $this->asEditor();\n        $page = $this->entities->page();\n        $result = $this->files->uploadGalleryImageToPage($this, $page);\n        $displayThumb = $result['response']->thumbs->gallery ?? '';\n        $page->html = '<p><img src=\"' . $displayThumb . '\" alt=\"My image\"></p>';\n        $page->save();\n        $image = Image::findOrFail($result['response']->id);\n\n        $zipResp = $this->asEditor()->get($page->getUrl(\"/export/zip\"));\n        $zip = ZipTestHelper::extractFromZipResponse($zipResp);\n        $pageData = $zip->data['page'];\n\n        $this->assertCount(1, $pageData['images']);\n        $imageData = $pageData['images'][0];\n        $this->assertEquals($image->id, $imageData['id']);\n        $this->assertEquals($image->name, $imageData['name']);\n        $this->assertEquals('gallery', $imageData['type']);\n        $this->assertNotEmpty($imageData['file']);\n\n        $filePath = $zip->extractPath(\"files/{$imageData['file']}\");\n        $this->assertFileExists($filePath);\n        $this->assertEquals(file_get_contents(public_path($image->path)), file_get_contents($filePath));\n\n        $this->assertEquals('<p><img src=\"[[bsexport:image:' . $imageData['id'] . ']]\" alt=\"My image\"></p>', $pageData['html']);\n    }\n\n    public function test_page_export_file_attachments()\n    {\n        $contents = 'My great attachment content!';\n\n        $page = $this->entities->page();\n        $this->asAdmin();\n        $attachment = $this->files->uploadAttachmentDataToPage($this, $page, 'PageAttachmentExport.txt', $contents, 'text/plain');\n\n        $zipResp = $this->get($page->getUrl(\"/export/zip\"));\n        $zip = ZipTestHelper::extractFromZipResponse($zipResp);\n\n        $pageData = $zip->data['page'];\n        $this->assertCount(1, $pageData['attachments']);\n\n        $attachmentData = $pageData['attachments'][0];\n        $this->assertEquals('PageAttachmentExport.txt', $attachmentData['name']);\n        $this->assertEquals($attachment->id, $attachmentData['id']);\n        $this->assertArrayNotHasKey('link', $attachmentData);\n        $this->assertNotEmpty($attachmentData['file']);\n\n        $fileRef = $attachmentData['file'];\n        $filePath = $zip->extractPath(\"/files/$fileRef\");\n        $this->assertFileExists($filePath);\n        $this->assertEquals($contents, file_get_contents($filePath));\n    }\n\n    public function test_page_export_link_attachments()\n    {\n        $page = $this->entities->page();\n        $this->asEditor();\n        $attachment = Attachment::factory()->create([\n            'name' => 'My link attachment for export',\n            'path' => 'https://example.com/cats',\n            'external' => true,\n            'uploaded_to' => $page->id,\n            'order' => 1,\n        ]);\n\n        $zipResp = $this->get($page->getUrl(\"/export/zip\"));\n        $zip = ZipTestHelper::extractFromZipResponse($zipResp);\n\n        $pageData = $zip->data['page'];\n        $this->assertCount(1, $pageData['attachments']);\n\n        $attachmentData = $pageData['attachments'][0];\n        $this->assertEquals('My link attachment for export', $attachmentData['name']);\n        $this->assertEquals($attachment->id, $attachmentData['id']);\n        $this->assertEquals('https://example.com/cats', $attachmentData['link']);\n        $this->assertArrayNotHasKey('file', $attachmentData);\n    }\n\n    public function test_book_export()\n    {\n        $book = $this->entities->bookHasChaptersAndPages();\n        $book->tags()->saveMany(Tag::factory()->count(2)->make());\n\n        $zipResp = $this->asEditor()->get($book->getUrl(\"/export/zip\"));\n        $zip = ZipTestHelper::extractFromZipResponse($zipResp);\n        $this->assertArrayHasKey('book', $zip->data);\n\n        $bookData = $zip->data['book'];\n        $this->assertEquals($book->id, $bookData['id']);\n        $this->assertEquals($book->name, $bookData['name']);\n        $this->assertEquals($book->descriptionInfo()->getHtml(), $bookData['description_html']);\n        $this->assertCount(2, $bookData['tags']);\n        $this->assertCount($book->directPages()->count(), $bookData['pages']);\n        $this->assertCount($book->chapters()->count(), $bookData['chapters']);\n        $this->assertArrayNotHasKey('cover', $bookData);\n    }\n\n    public function test_book_export_with_cover_image()\n    {\n        $book = $this->entities->book();\n        $bookRepo = $this->app->make(BookRepo::class);\n        $coverImageFile = $this->files->uploadedImage('cover.png');\n        $bookRepo->updateCoverImage($book, $coverImageFile);\n        $coverImage = $book->coverInfo()->getImage();\n\n        $zipResp = $this->asEditor()->get($book->getUrl(\"/export/zip\"));\n        $zip = ZipTestHelper::extractFromZipResponse($zipResp);\n\n        $this->assertArrayHasKey('cover', $zip->data['book']);\n        $coverRef = $zip->data['book']['cover'];\n        $coverPath = $zip->extractPath(\"/files/$coverRef\");\n        $this->assertFileExists($coverPath);\n        $this->assertEquals(file_get_contents(public_path($coverImage->path)), file_get_contents($coverPath));\n    }\n\n    public function test_chapter_export()\n    {\n        $chapter = $this->entities->chapter();\n        $chapter->tags()->saveMany(Tag::factory()->count(2)->make());\n\n        $zipResp = $this->asEditor()->get($chapter->getUrl(\"/export/zip\"));\n        $zip = ZipTestHelper::extractFromZipResponse($zipResp);\n        $this->assertArrayHasKey('chapter', $zip->data);\n\n        $chapterData = $zip->data['chapter'];\n        $this->assertEquals($chapter->id, $chapterData['id']);\n        $this->assertEquals($chapter->name, $chapterData['name']);\n        $this->assertEquals($chapter->descriptionInfo()->getHtml(), $chapterData['description_html']);\n        $this->assertCount(2, $chapterData['tags']);\n        $this->assertEquals($chapter->priority, $chapterData['priority']);\n        $this->assertCount($chapter->pages()->count(), $chapterData['pages']);\n    }\n\n    public function test_draft_pages_are_not_included()\n    {\n        $editor = $this->users->editor();\n        $entities = $this->entities->createChainBelongingToUser($editor);\n        $book = $entities['book'];\n        $page = $entities['page'];\n        $chapter = $entities['chapter'];\n        $book->tags()->saveMany(Tag::factory()->count(2)->make());\n\n        $page->created_by = $editor->id;\n        $page->draft = true;\n        $page->save();\n\n        $zipResp = $this->actingAs($editor)->get($book->getUrl(\"/export/zip\"));\n        $zip = ZipTestHelper::extractFromZipResponse($zipResp);\n        $this->assertCount(0, $zip->data['book']['chapters'][0]['pages'] ?? ['cat']);\n\n        $zipResp = $this->actingAs($editor)->get($chapter->getUrl(\"/export/zip\"));\n        $zip = ZipTestHelper::extractFromZipResponse($zipResp);\n        $this->assertCount(0, $zip->data['chapter']['pages'] ?? ['cat']);\n\n        $page->chapter_id = 0;\n        $page->save();\n\n        $zipResp = $this->actingAs($editor)->get($book->getUrl(\"/export/zip\"));\n        $zip = ZipTestHelper::extractFromZipResponse($zipResp);\n        $this->assertCount(0, $zip->data['book']['pages'] ?? ['cat']);\n    }\n\n\n    public function test_cross_reference_links_are_converted()\n    {\n        $book = $this->entities->bookHasChaptersAndPages();\n        $chapter = $book->chapters()->first();\n        $page = $chapter->pages()->first();\n\n        $book->description_html = '<p><a href=\"' . $chapter->getUrl() . '\">Link to chapter</a></p>';\n        $book->save();\n        $chapter->description_html = '<p><a href=\"' . $page->getUrl() . '#section2\">Link to page</a></p>';\n        $chapter->save();\n        $page->html = '<p><a href=\"' . $book->getUrl() . '?view=true\">Link to book</a></p>';\n        $page->save();\n\n        $zipResp = $this->asEditor()->get($book->getUrl(\"/export/zip\"));\n        $zip = ZipTestHelper::extractFromZipResponse($zipResp);\n        $bookData = $zip->data['book'];\n        $chapterData = $bookData['chapters'][0];\n        $pageData = $chapterData['pages'][0];\n\n        $this->assertStringContainsString('href=\"[[bsexport:chapter:' . $chapter->id . ']]\"', $bookData['description_html']);\n        $this->assertStringContainsString('href=\"[[bsexport:page:' . $page->id . ']]#section2\"', $chapterData['description_html']);\n        $this->assertStringContainsString('href=\"[[bsexport:book:' . $book->id . ']]?view=true\"', $pageData['html']);\n    }\n\n    public function test_book_and_chapter_description_links_to_images_in_pages_are_converted()\n    {\n        $book = $this->entities->bookHasChaptersAndPages();\n        $chapter = $book->chapters()->first();\n        $page = $chapter->pages()->first();\n\n        $this->asEditor();\n        $this->files->uploadGalleryImageToPage($this, $page);\n        /** @var Image $image */\n        $image = Image::query()->where('type', '=', 'gallery')\n            ->where('uploaded_to', '=', $page->id)->first();\n\n        $book->description_html = '<p><a href=\"' . $image->url . '\">Link to image</a></p>';\n        $book->save();\n        $chapter->description_html = '<p><a href=\"' . $image->url . '\">Link to image</a></p>';\n        $chapter->save();\n\n        $zipResp = $this->get($book->getUrl(\"/export/zip\"));\n        $zip = ZipTestHelper::extractFromZipResponse($zipResp);\n        $bookData = $zip->data['book'];\n        $chapterData = $bookData['chapters'][0];\n\n        $this->assertStringContainsString('href=\"[[bsexport:image:' . $image->id . ']]\"', $bookData['description_html']);\n        $this->assertStringContainsString('href=\"[[bsexport:image:' . $image->id . ']]\"', $chapterData['description_html']);\n    }\n\n    public function test_image_links_are_handled_when_using_external_storage_url()\n    {\n        $page = $this->entities->page();\n\n        $this->asEditor();\n        $this->files->uploadGalleryImageToPage($this, $page);\n        /** @var Image $image */\n        $image = Image::query()->where('type', '=', 'gallery')\n            ->where('uploaded_to', '=', $page->id)->first();\n\n        config()->set('filesystems.url', 'https://i.example.com/content');\n\n        $storageUrl = 'https://i.example.com/content/' . ltrim($image->path, '/');\n        $page->html = '<p><a href=\"' . $image->url . '\">Original URL</a><a href=\"' . $storageUrl . '\">Storage URL</a></p>';\n        $page->save();\n\n        $zipResp = $this->get($page->getUrl(\"/export/zip\"));\n        $zip = ZipTestHelper::extractFromZipResponse($zipResp);\n        $pageData = $zip->data['page'];\n\n        $ref = '[[bsexport:image:' . $image->id . ']]';\n        $this->assertStringContainsString(\"<a href=\\\"{$ref}\\\">Original URL</a><a href=\\\"{$ref}\\\">Storage URL</a>\", $pageData['html']);\n    }\n\n    public function test_orphaned_images_can_be_used_on_default_local_storage()\n    {\n        $this->asEditor();\n        $page = $this->entities->page();\n        $result = $this->files->uploadGalleryImageToPage($this, $page);\n        $displayThumb = $result['response']->thumbs->gallery ?? '';\n        $page->html = '<p><img src=\"' . $displayThumb . '\" alt=\"My image\"></p>';\n        $page->save();\n\n        $image = Image::findOrFail($result['response']->id);\n        $image->uploaded_to = null;\n        $image->save();\n\n        $zipResp = $this->asEditor()->get($page->getUrl(\"/export/zip\"));\n        $zipResp->assertOk();\n        $zip = ZipTestHelper::extractFromZipResponse($zipResp);\n        $pageData = $zip->data['page'];\n\n        $this->assertCount(1, $pageData['images']);\n        $imageData = $pageData['images'][0];\n        $this->assertEquals($image->id, $imageData['id']);\n\n        $this->assertEquals('<p><img src=\"[[bsexport:image:' . $imageData['id'] . ']]\" alt=\"My image\"></p>', $pageData['html']);\n    }\n\n    public function test_orphaned_images_cannot_be_used_on_local_secure_restricted()\n    {\n        config()->set('filesystems.images', 'local_secure_restricted');\n\n        $this->asEditor();\n        $page = $this->entities->page();\n        $result = $this->files->uploadGalleryImageToPage($this, $page);\n        $displayThumb = $result['response']->thumbs->gallery ?? '';\n        $page->html = '<p><img src=\"' . $displayThumb . '\" alt=\"My image\"></p>';\n        $page->save();\n\n        $image = Image::findOrFail($result['response']->id);\n        $image->uploaded_to = null;\n        $image->save();\n\n        $zipResp = $this->asEditor()->get($page->getUrl(\"/export/zip\"));\n        $zipResp->assertOk();\n        $zip = ZipTestHelper::extractFromZipResponse($zipResp);\n        $pageData = $zip->data['page'];\n\n        $this->assertCount(0, $pageData['images']);\n    }\n\n    public function test_cross_reference_links_external_to_export_are_not_converted()\n    {\n        $page = $this->entities->page();\n        $page->html = '<p><a href=\"' . $page->book->getUrl() . '\">Link to book</a></p>';\n        $page->save();\n\n        $zipResp = $this->asEditor()->get($page->getUrl(\"/export/zip\"));\n        $zip = ZipTestHelper::extractFromZipResponse($zipResp);\n        $pageData = $zip->data['page'];\n\n        $this->assertStringContainsString('href=\"' . $page->book->getUrl() . '\"', $pageData['html']);\n    }\n\n    public function test_attachments_links_are_converted()\n    {\n        $page = $this->entities->page();\n        $attachment = Attachment::factory()->create([\n            'name' => 'My link attachment for export reference',\n            'path' => 'https://example.com/cats/ref',\n            'external' => true,\n            'uploaded_to' => $page->id,\n            'order' => 1,\n        ]);\n\n        $page->html = '<p><a href=\"' . url(\"/attachments/{$attachment->id}\") . '?open=true\">Link to attachment</a></p>';\n        $page->save();\n\n        $zipResp = $this->asEditor()->get($page->getUrl(\"/export/zip\"));\n        $zip = ZipTestHelper::extractFromZipResponse($zipResp);\n        $pageData = $zip->data['page'];\n\n        $this->assertStringContainsString('href=\"[[bsexport:attachment:' . $attachment->id . ']]?open=true\"', $pageData['html']);\n    }\n\n    public function test_links_in_markdown_are_parsed()\n    {\n        $chapter = $this->entities->chapterHasPages();\n        $page = $chapter->pages()->first();\n\n        $page->markdown = \"[Link to chapter]({$chapter->getUrl()})\";\n        $page->save();\n\n        $zipResp = $this->asEditor()->get($chapter->getUrl(\"/export/zip\"));\n        $zip = ZipTestHelper::extractFromZipResponse($zipResp);\n        $pageData = $zip->data['chapter']['pages'][0];\n\n        $this->assertStringContainsString(\"[Link to chapter]([[bsexport:chapter:{$chapter->id}]])\", $pageData['markdown']);\n    }\n\n    public function test_exports_rate_limited_low_for_guest_viewers()\n    {\n        $this->setSettings(['app-public' => 'true']);\n\n        $page = $this->entities->page();\n        for ($i = 0; $i < 4; $i++) {\n            $this->get($page->getUrl(\"/export/zip\"))->assertOk();\n        }\n        $this->get($page->getUrl(\"/export/zip\"))->assertStatus(429);\n    }\n\n    public function test_exports_rate_limited_higher_for_logged_in_viewers()\n    {\n        $this->asAdmin();\n\n        $page = $this->entities->page();\n        for ($i = 0; $i < 10; $i++) {\n            $this->get($page->getUrl(\"/export/zip\"))->assertOk();\n        }\n        $this->get($page->getUrl(\"/export/zip\"))->assertStatus(429);\n    }\n}\n"
  },
  {
    "path": "tests/Exports/ZipExportValidatorTest.php",
    "content": "<?php\n\nnamespace Tests\\Exports;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Exports\\ZipExports\\ZipExportReader;\nuse BookStack\\Exports\\ZipExports\\ZipExportValidator;\nuse BookStack\\Exports\\ZipExports\\ZipImportRunner;\nuse BookStack\\Uploads\\Image;\nuse Tests\\TestCase;\n\nclass ZipExportValidatorTest extends TestCase\n{\n    protected array $filesToRemove = [];\n\n    protected function tearDown(): void\n    {\n        foreach ($this->filesToRemove as $file) {\n            unlink($file);\n        }\n\n        parent::tearDown();\n    }\n\n    protected function getValidatorForData(array $zipData, array $files = []): ZipExportValidator\n    {\n        $upload = ZipTestHelper::zipUploadFromData($zipData, $files);\n        $path = $upload->getRealPath();\n        $this->filesToRemove[] = $path;\n        $reader = new ZipExportReader($path);\n        return new ZipExportValidator($reader);\n    }\n\n    public function test_ids_have_to_be_unique()\n    {\n        $validator = $this->getValidatorForData([\n            'book' => [\n                'id' => 4,\n                'name' => 'My book',\n                'pages' => [\n                    [\n                        'id' => 4,\n                        'name' => 'My page',\n                        'markdown' => 'hello',\n                        'attachments' => [\n                            ['id' => 4, 'name' => 'Attachment A', 'link' => 'https://example.com'],\n                            ['id' => 4, 'name' => 'Attachment B', 'link' => 'https://example.com']\n                        ],\n                        'images' => [\n                            ['id' => 4, 'name' => 'Image A', 'type' => 'gallery', 'file' => 'cat'],\n                            ['id' => 4, 'name' => 'Image b', 'type' => 'gallery', 'file' => 'cat'],\n                        ],\n                    ],\n                    ['id' => 4, 'name' => 'My page', 'markdown' => 'hello'],\n                ],\n                'chapters' => [\n                    ['id' => 4, 'name' => 'Chapter 1'],\n                    ['id' => 4, 'name' => 'Chapter 2']\n                ]\n            ]\n        ], ['cat' => $this->files->testFilePath('test-image.png')]);\n\n        $results = $validator->validate();\n        $this->assertCount(4, $results);\n\n        $expectedMessage = 'The id must be unique for the object type within the ZIP.';\n        $this->assertEquals($expectedMessage, $results['book.pages.0.attachments.1.id']);\n        $this->assertEquals($expectedMessage, $results['book.pages.0.images.1.id']);\n        $this->assertEquals($expectedMessage, $results['book.pages.1.id']);\n        $this->assertEquals($expectedMessage, $results['book.chapters.1.id']);\n    }\n\n    public function test_image_files_need_to_be_a_valid_detected_image_file()\n    {\n        $validator = $this->getValidatorForData([\n            'page' => [\n                'id' => 4,\n                'name' => 'My page',\n                'markdown' => 'hello',\n                'images' => [\n                    ['id' => 4, 'name' => 'Image A', 'type' => 'gallery', 'file' => 'cat'],\n                ],\n            ]\n        ], ['cat' => $this->files->testFilePath('test-file.txt')]);\n\n        $results = $validator->validate();\n        $this->assertCount(1, $results);\n\n        $this->assertEquals('The file needs to reference a file of type image/png,image/jpeg,image/gif,image/webp, found text/plain.', $results['page.images.0.file']);\n    }\n}\n"
  },
  {
    "path": "tests/Exports/ZipImportRunnerTest.php",
    "content": "<?php\n\nnamespace Tests\\Exports;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Exceptions\\ZipImportException;\nuse BookStack\\Exports\\ZipExports\\ZipImportRunner;\nuse BookStack\\Uploads\\Image;\nuse Tests\\TestCase;\n\nclass ZipImportRunnerTest extends TestCase\n{\n    protected ZipImportRunner $runner;\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n        $this->runner = app()->make(ZipImportRunner::class);\n    }\n\n    public function test_book_import()\n    {\n        $testImagePath = $this->files->testFilePath('test-image.png');\n        $testFilePath = $this->files->testFilePath('test-file.txt');\n        $import = ZipTestHelper::importFromData([], [\n            'book' => [\n                'id' => 5,\n                'name' => 'Import test',\n                'cover' => 'book_cover_image',\n                'description_html' => '<p><a href=\"[[bsexport:page:3]]\">Link to chapter page</a></p>',\n                'tags' => [\n                    ['name' => 'Animal', 'value' => 'Cat'],\n                    ['name' => 'Category', 'value' => 'Test'],\n                ],\n                'chapters' => [\n                    [\n                        'id' => 6,\n                        'name' => 'Chapter A',\n                        'description_html' => '<p><a href=\"[[bsexport:book:5]]\">Link to book</a></p>',\n                        'priority' => 1,\n                        'tags' => [\n                            ['name' => 'Reviewed'],\n                            ['name' => 'Category', 'value' => 'Test Chapter'],\n                        ],\n                        'pages' => [\n                            [\n                                'id' => 3,\n                                'name' => 'Page A',\n                                'priority' => 6,\n                                'html' => '\n<p><a href=\"[[bsexport:page:3]]\">Link to self</a></p>\n<p><a href=\"[[bsexport:image:1]]\">Link to cat image</a></p>\n<p><a href=\"[[bsexport:attachment:4]]\">Link to text attachment</a></p>',\n                                'tags' => [\n                                    ['name' => 'Unreviewed'],\n                                ],\n                                'attachments' => [\n                                    [\n                                        'id' => 4,\n                                        'name' => 'Text attachment',\n                                        'file' => 'file_attachment'\n                                    ],\n                                    [\n                                        'name' => 'Cats',\n                                        'link' => 'https://example.com/cats',\n                                    ]\n                                ],\n                                'images' => [\n                                    [\n                                        'id' => 1,\n                                        'name' => 'Cat',\n                                        'type' => 'gallery',\n                                        'file' => 'cat_image'\n                                    ],\n                                    [\n                                        'id' => 2,\n                                        'name' => 'Dog Drawing',\n                                        'type' => 'drawio',\n                                        'file' => 'dog_image'\n                                    ]\n                                ],\n                            ],\n                        ],\n                    ],\n                    [\n                        'name' => 'Chapter child B',\n                        'priority' => 5,\n                    ]\n                ],\n                'pages' => [\n                    [\n                        'name' => 'Page C',\n                        'markdown' => '[Link to text]([[bsexport:attachment:4]]?scale=big)',\n                        'priority' => 3,\n                    ]\n                ],\n            ],\n        ], [\n            'book_cover_image' => $testImagePath,\n            'file_attachment'  => $testFilePath,\n            'cat_image' => $testImagePath,\n            'dog_image' => $testImagePath,\n        ]);\n\n        $this->asAdmin();\n        /** @var Book $book */\n        $book = $this->runner->run($import);\n\n        // Book checks\n        $this->assertEquals('Import test', $book->name);\n        $this->assertFileExists(public_path($book->coverInfo()->getImage()->path));\n        $this->assertCount(2, $book->tags);\n        $this->assertEquals('Cat', $book->tags()->first()->value);\n        $this->assertCount(2, $book->chapters);\n        $this->assertEquals(1, $book->directPages()->count());\n\n        // Chapter checks\n        $chapterA = $book->chapters()->where('name', 'Chapter A')->first();\n        $this->assertCount(2, $chapterA->tags);\n        $firstChapterTag = $chapterA->tags()->first();\n        $this->assertEquals('Reviewed', $firstChapterTag->name);\n        $this->assertEquals('', $firstChapterTag->value);\n        $this->assertCount(1, $chapterA->pages);\n\n        // Page checks\n        /** @var Page $pageA */\n        $pageA = $chapterA->pages->first();\n        $this->assertEquals('Page A', $pageA->name);\n        $this->assertCount(1, $pageA->tags);\n        $firstPageTag = $pageA->tags()->first();\n        $this->assertEquals('Unreviewed', $firstPageTag->name);\n        $this->assertCount(2, $pageA->attachments);\n        $firstAttachment = $pageA->attachments->first();\n        $this->assertEquals('Text attachment', $firstAttachment->name);\n        $this->assertFileEquals($testFilePath, storage_path($firstAttachment->path));\n        $this->assertFalse($firstAttachment->external);\n        $secondAttachment = $pageA->attachments->last();\n        $this->assertEquals('Cats', $secondAttachment->name);\n        $this->assertEquals('https://example.com/cats', $secondAttachment->path);\n        $this->assertTrue($secondAttachment->external);\n        $pageAImages = Image::where('uploaded_to', '=', $pageA->id)->whereIn('type', ['gallery', 'drawio'])->get();\n        $this->assertCount(2, $pageAImages);\n        $this->assertEquals('Cat', $pageAImages[0]->name);\n        $this->assertEquals('gallery', $pageAImages[0]->type);\n        $this->assertFileEquals($testImagePath, public_path($pageAImages[0]->path));\n        $this->assertEquals('Dog Drawing', $pageAImages[1]->name);\n        $this->assertEquals('drawio', $pageAImages[1]->type);\n\n        // Book order check\n        $children = $book->getDirectVisibleChildren()->values()->all();\n        $this->assertEquals($children[0]->name, 'Chapter A');\n        $this->assertEquals($children[1]->name, 'Page C');\n        $this->assertEquals($children[2]->name, 'Chapter child B');\n\n        // Reference checks\n        $textAttachmentUrl = $firstAttachment->getUrl();\n        $this->assertStringContainsString($pageA->getUrl(), $book->description_html);\n        $this->assertStringContainsString($book->getUrl(), $chapterA->description_html);\n        $this->assertStringContainsString($pageA->getUrl(), $pageA->html);\n        $this->assertStringContainsString($pageAImages[0]->getThumb(1680, null, true), $pageA->html);\n        $this->assertStringContainsString($firstAttachment->getUrl(), $pageA->html);\n\n        // Reference in converted markdown\n        $pageC = $children[1];\n        $this->assertStringContainsString(\"href=\\\"{$textAttachmentUrl}?scale=big\\\"\", $pageC->html);\n\n        ZipTestHelper::deleteZipForImport($import);\n    }\n\n    public function test_chapter_import()\n    {\n        $testImagePath = $this->files->testFilePath('test-image.png');\n        $testFilePath = $this->files->testFilePath('test-file.txt');\n        $parent = $this->entities->book();\n\n        $import = ZipTestHelper::importFromData([], [\n            'chapter' => [\n                'id' => 6,\n                'name' => 'Chapter A',\n                'description_html' => '<p><a href=\"[[bsexport:page:3]]\">Link to page</a></p>',\n                'priority' => 1,\n                'tags' => [\n                    ['name' => 'Reviewed', 'value' => '2024'],\n                ],\n                'pages' => [\n                    [\n                        'id' => 3,\n                        'name' => 'Page A',\n                        'priority' => 6,\n                        'html' => '<p><a href=\"[[bsexport:chapter:6]]\">Link to chapter</a></p>\n<p><a href=\"[[bsexport:image:2]]\">Link to dog drawing</a></p>\n<p><a href=\"[[bsexport:attachment:4]]\">Link to text attachment</a></p>',\n                        'tags' => [\n                            ['name' => 'Unreviewed'],\n                        ],\n                        'attachments' => [\n                            [\n                                'id' => 4,\n                                'name' => 'Text attachment',\n                                'file' => 'file_attachment'\n                            ]\n                        ],\n                        'images' => [\n                            [\n                                'id' => 2,\n                                'name' => 'Dog Drawing',\n                                'type' => 'drawio',\n                                'file' => 'dog_image'\n                            ]\n                        ],\n                    ],\n                    [\n                        'name' => 'Page B',\n                        'markdown' => '[Link to page A]([[bsexport:page:3]])',\n                        'priority' => 9,\n                    ],\n                ],\n            ],\n        ], [\n            'file_attachment'  => $testFilePath,\n            'dog_image' => $testImagePath,\n        ]);\n\n        $this->asAdmin();\n        /** @var Chapter $chapter */\n        $chapter = $this->runner->run($import, $parent);\n\n        // Chapter checks\n        $this->assertEquals('Chapter A', $chapter->name);\n        $this->assertEquals($parent->id, $chapter->book_id);\n        $this->assertCount(1, $chapter->tags);\n        $firstChapterTag = $chapter->tags()->first();\n        $this->assertEquals('Reviewed', $firstChapterTag->name);\n        $this->assertEquals('2024', $firstChapterTag->value);\n        $this->assertCount(2, $chapter->pages);\n\n        // Page checks\n        /** @var Page $pageA */\n        $pageA = $chapter->pages->first();\n        $this->assertEquals('Page A', $pageA->name);\n        $this->assertCount(1, $pageA->tags);\n        $this->assertCount(1, $pageA->attachments);\n        $pageAImages = Image::where('uploaded_to', '=', $pageA->id)->whereIn('type', ['gallery', 'drawio'])->get();\n        $this->assertCount(1, $pageAImages);\n\n        // Reference checks\n        $attachment = $pageA->attachments->first();\n        $this->assertStringContainsString($pageA->getUrl(), $chapter->description_html);\n        $this->assertStringContainsString($chapter->getUrl(), $pageA->html);\n        $this->assertStringContainsString($pageAImages[0]->url, $pageA->html);\n        $this->assertStringContainsString($attachment->getUrl(), $pageA->html);\n\n        ZipTestHelper::deleteZipForImport($import);\n    }\n\n    public function test_page_import()\n    {\n        $testImagePath = $this->files->testFilePath('test-image.png');\n        $testFilePath = $this->files->testFilePath('test-file.txt');\n        $parent = $this->entities->chapter();\n\n        $import = ZipTestHelper::importFromData([], [\n            'page' => [\n                'id' => 3,\n                'name' => 'Page A',\n                'priority' => 6,\n                'html' => '<p><a href=\"[[bsexport:page:3]]\">Link to self</a></p>\n<p><a href=\"[[bsexport:image:2]]\">Link to dog drawing</a></p>\n<p><a href=\"[[bsexport:attachment:4]]\">Link to text attachment</a></p>',\n                'tags' => [\n                    ['name' => 'Unreviewed'],\n                ],\n                'attachments' => [\n                    [\n                        'id' => 4,\n                        'name' => 'Text attachment',\n                        'file' => 'file_attachment'\n                    ]\n                ],\n                'images' => [\n                    [\n                        'id' => 2,\n                        'name' => 'Dog Drawing',\n                        'type' => 'drawio',\n                        'file' => 'dog_image'\n                    ]\n                ],\n            ],\n        ], [\n            'file_attachment'  => $testFilePath,\n            'dog_image' => $testImagePath,\n        ]);\n\n        $this->asAdmin();\n        /** @var Page $page */\n        $page = $this->runner->run($import, $parent);\n\n        // Page checks\n        $this->assertEquals('Page A', $page->name);\n        $this->assertCount(1, $page->tags);\n        $this->assertCount(1, $page->attachments);\n        $pageImages = Image::where('uploaded_to', '=', $page->id)->whereIn('type', ['gallery', 'drawio'])->get();\n        $this->assertCount(1, $pageImages);\n        $this->assertFileEquals($testImagePath, public_path($pageImages[0]->path));\n\n        // Reference checks\n        $this->assertStringContainsString($page->getUrl(), $page->html);\n        $this->assertStringContainsString($pageImages[0]->url, $page->html);\n        $this->assertStringContainsString($page->attachments->first()->getUrl(), $page->html);\n\n        ZipTestHelper::deleteZipForImport($import);\n    }\n\n    public function test_revert_cleans_up_uploaded_files()\n    {\n        $testImagePath = $this->files->testFilePath('test-image.png');\n        $testFilePath = $this->files->testFilePath('test-file.txt');\n        $parent = $this->entities->chapter();\n\n        $import = ZipTestHelper::importFromData([], [\n            'page' => [\n                'name' => 'Page A',\n                'html' => '<p>Hello</p>',\n                'attachments' => [\n                    [\n                        'name' => 'Text attachment',\n                        'file' => 'file_attachment'\n                    ]\n                ],\n                'images' => [\n                    [\n                        'name' => 'Dog Image',\n                        'type' => 'gallery',\n                        'file' => 'dog_image'\n                    ]\n                ],\n            ],\n        ], [\n            'file_attachment'  => $testFilePath,\n            'dog_image' => $testImagePath,\n        ]);\n\n        $this->asAdmin();\n        /** @var Page $page */\n        $page = $this->runner->run($import, $parent);\n\n        $attachment = $page->attachments->first();\n        $image = Image::query()->where('uploaded_to', '=', $page->id)->where('type', '=', 'gallery')->first();\n\n        $this->assertFileExists(public_path($image->path));\n        $this->assertFileExists(storage_path($attachment->path));\n\n        $this->runner->revertStoredFiles();\n\n        $this->assertFileDoesNotExist(public_path($image->path));\n        $this->assertFileDoesNotExist(storage_path($attachment->path));\n\n        ZipTestHelper::deleteZipForImport($import);\n    }\n\n    public function test_imported_images_have_their_detected_extension_added()\n    {\n        $testImagePath = $this->files->testFilePath('test-image.png');\n        $parent = $this->entities->chapter();\n\n        $import = ZipTestHelper::importFromData([], [\n            'page' => [\n                'name' => 'Page A',\n                'html' => '<p>hello</p>',\n                'images' => [\n                    [\n                        'id' => 2,\n                        'name' => 'Cat',\n                        'type' => 'gallery',\n                        'file' => 'cat_image'\n                    ]\n                ],\n            ],\n        ], [\n            'cat_image' => $testImagePath,\n        ]);\n\n        $this->asAdmin();\n        /** @var Page $page */\n        $page = $this->runner->run($import, $parent);\n\n        $pageImages = Image::where('uploaded_to', '=', $page->id)->whereIn('type', ['gallery', 'drawio'])->get();\n\n        $this->assertCount(1, $pageImages);\n        $this->assertStringEndsWith('.png', $pageImages[0]->url);\n        $this->assertStringEndsWith('.png', $pageImages[0]->path);\n\n        ZipTestHelper::deleteZipForImport($import);\n    }\n\n    public function test_drawing_references_are_updated_within_content()\n    {\n        $testImagePath = $this->files->testFilePath('test-image.png');\n        $parent = $this->entities->chapter();\n\n        $import = ZipTestHelper::importFromData([], [\n            'page' => [\n                'name' => 'Page A',\n                'html' => '<div drawio-diagram=\"1125\"><img src=\"[[bsexport:image:1125]]\"></div>',\n                'images' => [\n                    [\n                        'id' => 1125,\n                        'name' => 'Cat',\n                        'type' => 'drawio',\n                        'file' => 'my_drawing'\n                    ]\n                ],\n            ],\n        ], [\n            'my_drawing' => $testImagePath,\n        ]);\n\n        $this->asAdmin();\n        /** @var Page $page */\n        $page = $this->runner->run($import, $parent);\n\n        $pageImages = Image::where('uploaded_to', '=', $page->id)->whereIn('type', ['gallery', 'drawio'])->get();\n        $this->assertCount(1, $pageImages);\n        $this->assertEquals('drawio', $pageImages[0]->type);\n\n        $drawingId = $pageImages[0]->id;\n        $this->assertStringContainsString(\"drawio-diagram=\\\"{$drawingId}\\\"\", $page->html);\n        $this->assertStringNotContainsString('[[bsexport:image:1125]]', $page->html);\n        $this->assertStringNotContainsString('drawio-diagram=\"1125\"', $page->html);\n\n        ZipTestHelper::deleteZipForImport($import);\n    }\n\n    public function test_error_thrown_if_zip_item_exceeds_app_file_upload_limit()\n    {\n        $tempFile = tempnam(sys_get_temp_dir(), 'bs-zip-test');\n        file_put_contents($tempFile, str_repeat('a', 2500000));\n        $parent = $this->entities->chapter();\n        config()->set('app.upload_limit', 1);\n\n        $import = ZipTestHelper::importFromData([], [\n            'page' => [\n                'name' => 'Page A',\n                'html' => '<p>Hello</p>',\n                'attachments' => [\n                    [\n                        'name' => 'Text attachment',\n                        'file' => 'file_attachment'\n                    ]\n                ],\n            ],\n        ], [\n            'file_attachment' => $tempFile,\n        ]);\n\n        $this->asAdmin();\n\n        $this->expectException(ZipImportException::class);\n        $this->expectExceptionMessage('The file file_attachment must not exceed 1 MB.');\n\n        $this->runner->run($import, $parent);\n        ZipTestHelper::deleteZipForImport($import);\n    }\n\n    public function test_error_thrown_if_zip_data_exceeds_app_file_upload_limit()\n    {\n        $parent = $this->entities->chapter();\n        config()->set('app.upload_limit', 1);\n\n        $import = ZipTestHelper::importFromData([], [\n            'page' => [\n                'name' => 'Page A',\n                'html' => '<p>' . str_repeat('a', 2500000) . '</p>',\n            ],\n        ]);\n\n        $this->asAdmin();\n\n        $this->expectException(ZipImportException::class);\n        $this->expectExceptionMessage('ZIP data.json content exceeds the configured application maximum upload size.');\n\n        $this->runner->run($import, $parent);\n        ZipTestHelper::deleteZipForImport($import);\n    }\n}\n"
  },
  {
    "path": "tests/Exports/ZipImportTest.php",
    "content": "<?php\n\nnamespace Tests\\Exports;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Exports\\Import;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportBook;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportChapter;\nuse BookStack\\Exports\\ZipExports\\Models\\ZipExportPage;\nuse Illuminate\\Http\\UploadedFile;\nuse Illuminate\\Testing\\TestResponse;\nuse Tests\\TestCase;\nuse ZipArchive;\n\nclass ZipImportTest extends TestCase\n{\n    public function test_import_page_view()\n    {\n        $resp = $this->asAdmin()->get('/import');\n        $resp->assertSee('Import');\n        $this->withHtml($resp)->assertElementExists('form input[type=\"file\"][name=\"file\"]');\n    }\n\n    public function test_permissions_needed_for_import_page()\n    {\n        $user = $this->users->viewer();\n        $this->actingAs($user);\n\n        $resp = $this->get('/books');\n        $this->withHtml($resp)->assertLinkNotExists(url('/import'));\n        $resp = $this->get('/import');\n        $resp->assertRedirect('/');\n\n        $this->permissions->grantUserRolePermissions($user, ['content-import']);\n\n        $resp = $this->get('/books');\n        $this->withHtml($resp)->assertLinkExists(url('/import'));\n        $resp = $this->get('/import');\n        $resp->assertOk();\n        $resp->assertSeeText('Select ZIP file to upload');\n    }\n\n    public function test_import_page_pending_import_visibility_limited()\n    {\n        $user = $this->users->viewer();\n        $admin = $this->users->admin();\n        $userImport = Import::factory()->create(['name' => 'MySuperUserImport', 'created_by' => $user->id]);\n        $adminImport = Import::factory()->create(['name' => 'MySuperAdminImport', 'created_by' => $admin->id]);\n        $this->permissions->grantUserRolePermissions($user, ['content-import']);\n\n        $resp = $this->actingAs($user)->get('/import');\n        $resp->assertSeeText('MySuperUserImport');\n        $resp->assertDontSeeText('MySuperAdminImport');\n\n        $this->permissions->grantUserRolePermissions($user, ['settings-manage']);\n\n        $resp = $this->actingAs($user)->get('/import');\n        $resp->assertSeeText('MySuperUserImport');\n        $resp->assertSeeText('MySuperAdminImport');\n    }\n\n    public function test_zip_read_errors_are_shown_on_validation()\n    {\n        $invalidUpload = $this->files->uploadedImage('image.zip');\n\n        $this->asAdmin();\n        $resp = $this->runImportFromFile($invalidUpload);\n        $resp->assertRedirect('/import');\n\n        $resp = $this->followRedirects($resp);\n        $resp->assertSeeText('Could not read ZIP file');\n    }\n\n    public function test_error_shown_if_missing_data()\n    {\n        $zipFile = tempnam(sys_get_temp_dir(), 'bstest-');\n        $zip = new ZipArchive();\n        $zip->open($zipFile, ZipArchive::OVERWRITE);\n        $zip->addFromString('beans', 'cat');\n        $zip->close();\n\n        $this->asAdmin();\n        $upload = new UploadedFile($zipFile, 'upload.zip', 'application/zip', null, true);\n        $resp = $this->runImportFromFile($upload);\n        $resp->assertRedirect('/import');\n\n        $resp = $this->followRedirects($resp);\n        $resp->assertSeeText('Could not find and decode ZIP data.json content.');\n    }\n\n    public function test_error_shown_if_no_importable_key()\n    {\n        $this->asAdmin();\n        $resp = $this->runImportFromFile(ZipTestHelper::zipUploadFromData([\n            'instance' => []\n        ]));\n\n        $resp->assertRedirect('/import');\n        $resp = $this->followRedirects($resp);\n        $resp->assertSeeText('ZIP file data has no expected book, chapter or page content.');\n    }\n\n    public function test_zip_data_validation_messages_shown()\n    {\n        $this->asAdmin();\n        $resp = $this->runImportFromFile(ZipTestHelper::zipUploadFromData([\n            'book' => [\n                'id' => 4,\n                'pages' => [\n                    'cat',\n                    [\n                        'name' => 'My inner page',\n                        'tags' => [\n                            [\n                                'value' => 5\n                            ]\n                        ],\n                    ]\n                ],\n            ]\n        ]));\n\n        $resp->assertRedirect('/import');\n        $resp = $this->followRedirects($resp);\n\n        $resp->assertSeeText('[book.name]: The name field is required.');\n        $resp->assertSeeText('[book.pages.0.0]: Data object expected but \"string\" found.');\n        $resp->assertSeeText('[book.pages.1.tags.0.name]: The name field is required.');\n        $resp->assertSeeText('[book.pages.1.tags.0.value]: The value must be a string.');\n    }\n\n    public function test_import_upload_success()\n    {\n        $admin = $this->users->admin();\n        $this->actingAs($admin);\n        $data = [\n            'book' => [\n                'name' => 'My great book name',\n                'chapters' => [\n                    [\n                        'name' => 'my chapter',\n                        'pages' => [\n                            [\n                                'name' => 'my chapter page',\n                            ]\n                        ]\n                    ]\n                ],\n                'pages' => [\n                    [\n                        'name' => 'My page',\n                    ]\n                ],\n            ],\n        ];\n\n        $resp = $this->runImportFromFile(ZipTestHelper::zipUploadFromData($data));\n\n        $this->assertDatabaseHas('imports', [\n            'name' => 'My great book name',\n            'type' => 'book',\n            'created_by' => $admin->id,\n        ]);\n\n        /** @var Import $import */\n        $import = Import::query()->latest()->first();\n        $resp->assertRedirect(\"/import/{$import->id}\");\n        $this->assertFileExists(storage_path($import->path));\n        $this->assertActivityExists(ActivityType::IMPORT_CREATE);\n\n        ZipTestHelper::deleteZipForImport($import);\n    }\n\n    public function test_import_show_page()\n    {\n        $exportBook = new ZipExportBook();\n        $exportBook->name = 'My exported book';\n        $exportChapter = new ZipExportChapter();\n        $exportChapter->name = 'My exported chapter';\n        $exportPage = new ZipExportPage();\n        $exportPage->name = 'My exported page';\n        $exportBook->chapters = [$exportChapter];\n        $exportChapter->pages = [$exportPage];\n\n        $import = Import::factory()->create([\n            'name' => 'MySuperAdminImport',\n            'metadata' => json_encode($exportBook)\n        ]);\n\n        $resp = $this->asAdmin()->get(\"/import/{$import->id}\");\n        $resp->assertOk();\n        $resp->assertSeeText('My exported book');\n        $resp->assertSeeText('My exported chapter');\n        $resp->assertSeeText('My exported page');\n    }\n\n    public function test_import_show_page_access_limited()\n    {\n        $user = $this->users->viewer();\n        $admin = $this->users->admin();\n        $userImport = Import::factory()->create(['name' => 'MySuperUserImport', 'created_by' => $user->id]);\n        $adminImport = Import::factory()->create(['name' => 'MySuperAdminImport', 'created_by' => $admin->id]);\n        $this->actingAs($user);\n\n        $this->get(\"/import/{$userImport->id}\")->assertRedirect('/');\n        $this->get(\"/import/{$adminImport->id}\")->assertRedirect('/');\n\n        $this->permissions->grantUserRolePermissions($user, ['content-import']);\n\n        $this->get(\"/import/{$userImport->id}\")->assertOk();\n        $this->get(\"/import/{$adminImport->id}\")->assertStatus(404);\n\n        $this->permissions->grantUserRolePermissions($user, ['settings-manage']);\n\n        $this->get(\"/import/{$userImport->id}\")->assertOk();\n        $this->get(\"/import/{$adminImport->id}\")->assertOk();\n    }\n\n    public function test_import_delete()\n    {\n        $this->asAdmin();\n        $this->runImportFromFile(ZipTestHelper::zipUploadFromData([\n            'book' => [\n                'name' => 'My great book name'\n            ],\n        ]));\n\n        /** @var Import $import */\n        $import = Import::query()->latest()->first();\n        $this->assertDatabaseHas('imports', [\n            'id' => $import->id,\n            'name' => 'My great book name'\n        ]);\n        $this->assertFileExists(storage_path($import->path));\n\n        $resp = $this->delete(\"/import/{$import->id}\");\n\n        $resp->assertRedirect('/import');\n        $this->assertActivityExists(ActivityType::IMPORT_DELETE);\n        $this->assertDatabaseMissing('imports', [\n            'id' => $import->id,\n        ]);\n        $this->assertFileDoesNotExist(storage_path($import->path));\n    }\n\n    public function test_import_delete_access_limited()\n    {\n        $user = $this->users->viewer();\n        $admin = $this->users->admin();\n        $userImport = Import::factory()->create(['name' => 'MySuperUserImport', 'created_by' => $user->id]);\n        $adminImport = Import::factory()->create(['name' => 'MySuperAdminImport', 'created_by' => $admin->id]);\n        $this->actingAs($user);\n\n        $this->delete(\"/import/{$userImport->id}\")->assertRedirect('/');\n        $this->delete(\"/import/{$adminImport->id}\")->assertRedirect('/');\n\n        $this->permissions->grantUserRolePermissions($user, ['content-import']);\n\n        $this->delete(\"/import/{$userImport->id}\")->assertRedirect('/import');\n        $this->delete(\"/import/{$adminImport->id}\")->assertStatus(404);\n\n        $this->permissions->grantUserRolePermissions($user, ['settings-manage']);\n\n        $this->delete(\"/import/{$adminImport->id}\")->assertRedirect('/import');\n    }\n\n    public function test_run_simple_success_scenario()\n    {\n        $import = ZipTestHelper::importFromData([], [\n            'book' => [\n                'name' => 'My imported book',\n                'pages' => [\n                    [\n                        'name' => 'My imported book page',\n                        'html' => '<p>Hello there from child page!</p>'\n                    ]\n                ],\n            ]\n        ]);\n\n        $resp = $this->asAdmin()->post(\"/import/{$import->id}\");\n        $book = Book::query()->where('name', '=', 'My imported book')->latest()->first();\n        $resp->assertRedirect($book->getUrl());\n\n        $resp = $this->followRedirects($resp);\n        $resp->assertSee('My imported book page');\n        $resp->assertSee('Hello there from child page!');\n\n        $this->assertDatabaseMissing('imports', ['id' => $import->id]);\n        $this->assertFileDoesNotExist(storage_path($import->path));\n        $this->assertActivityExists(ActivityType::IMPORT_RUN, null, $import->logDescriptor());\n    }\n\n    public function test_import_run_access_limited()\n    {\n        $user = $this->users->editor();\n        $admin = $this->users->admin();\n        $userImport = Import::factory()->create(['name' => 'MySuperUserImport', 'created_by' => $user->id]);\n        $adminImport = Import::factory()->create(['name' => 'MySuperAdminImport', 'created_by' => $admin->id]);\n        $this->actingAs($user);\n\n        $this->post(\"/import/{$userImport->id}\")->assertRedirect('/');\n        $this->post(\"/import/{$adminImport->id}\")->assertRedirect('/');\n\n        $this->permissions->grantUserRolePermissions($user, ['content-import']);\n\n        $this->post(\"/import/{$userImport->id}\")->assertRedirect($userImport->getUrl()); // Getting validation response instead of access issue response\n        $this->post(\"/import/{$adminImport->id}\")->assertStatus(404);\n\n        $this->permissions->grantUserRolePermissions($user, ['settings-manage']);\n\n        $this->post(\"/import/{$adminImport->id}\")->assertRedirect($adminImport->getUrl()); // Getting validation response instead of access issue response\n    }\n\n    public function test_run_revalidates_content()\n    {\n        $import = ZipTestHelper::importFromData([], [\n            'book' => [\n                'id' => 'abc',\n            ]\n        ]);\n\n        $resp = $this->asAdmin()->post(\"/import/{$import->id}\");\n        $resp->assertRedirect($import->getUrl());\n\n        $resp = $this->followRedirects($resp);\n        $resp->assertSeeText('The name field is required.');\n        $resp->assertSeeText('The id must be an integer.');\n\n        ZipTestHelper::deleteZipForImport($import);\n    }\n\n    public function test_run_checks_permissions_on_import()\n    {\n        $viewer = $this->users->viewer();\n        $this->permissions->grantUserRolePermissions($viewer, ['content-import']);\n        $import = ZipTestHelper::importFromData(['created_by' => $viewer->id], [\n            'book' => ['name' => 'My import book'],\n        ]);\n\n        $resp = $this->asViewer()->post(\"/import/{$import->id}\");\n        $resp->assertRedirect($import->getUrl());\n\n        $resp = $this->followRedirects($resp);\n        $resp->assertSeeText('You are lacking the required permissions to create books.');\n\n        ZipTestHelper::deleteZipForImport($import);\n    }\n\n    public function test_run_requires_parent_for_chapter_and_page_imports()\n    {\n        $book = $this->entities->book();\n        $pageImport = ZipTestHelper::importFromData([], [\n            'page' => ['name' => 'My page', 'html' => '<p>page test!</p>'],\n        ]);\n        $chapterImport = ZipTestHelper::importFromData([], [\n            'chapter' => ['name' => 'My chapter'],\n        ]);\n\n        $resp = $this->asAdmin()->post(\"/import/{$pageImport->id}\");\n        $resp->assertRedirect($pageImport->getUrl());\n        $this->followRedirects($resp)->assertSee('The parent field is required.');\n\n        $resp = $this->asAdmin()->post(\"/import/{$pageImport->id}\", ['parent' => \"book:{$book->id}\"]);\n        $resp->assertRedirectContains($book->getUrl());\n\n        $resp = $this->asAdmin()->post(\"/import/{$chapterImport->id}\");\n        $resp->assertRedirect($chapterImport->getUrl());\n        $this->followRedirects($resp)->assertSee('The parent field is required.');\n\n        $resp = $this->asAdmin()->post(\"/import/{$chapterImport->id}\", ['parent' => \"book:{$book->id}\"]);\n        $resp->assertRedirectContains($book->getUrl());\n    }\n\n    public function test_run_validates_correct_parent_type()\n    {\n        $chapter = $this->entities->chapter();\n        $import = ZipTestHelper::importFromData([], [\n            'chapter' => ['name' => 'My chapter'],\n        ]);\n\n        $resp = $this->asAdmin()->post(\"/import/{$import->id}\", ['parent' => \"chapter:{$chapter->id}\"]);\n        $resp->assertRedirect($import->getUrl());\n\n        $resp = $this->followRedirects($resp);\n        $resp->assertSee('Parent book required for chapter import.');\n\n        ZipTestHelper::deleteZipForImport($import);\n    }\n\n    protected function runImportFromFile(UploadedFile $file): TestResponse\n    {\n        return $this->call('POST', '/import', [], [], ['file' => $file]);\n    }\n}\n"
  },
  {
    "path": "tests/Exports/ZipResultData.php",
    "content": "<?php\n\nnamespace Tests\\Exports;\n\nclass ZipResultData\n{\n    public function __construct(\n        public string $zipPath,\n        public string $extractedDirPath,\n        public array $data,\n    ) {\n    }\n\n    /**\n     * Build a path to a location the extracted content, using the given relative $path.\n     */\n    public function extractPath(string $path): string\n    {\n        $relPath = implode(DIRECTORY_SEPARATOR, explode('/', $path));\n        return $this->extractedDirPath . DIRECTORY_SEPARATOR . ltrim($relPath, DIRECTORY_SEPARATOR);\n    }\n}\n"
  },
  {
    "path": "tests/Exports/ZipTestHelper.php",
    "content": "<?php\n\nnamespace Tests\\Exports;\n\nuse BookStack\\Exports\\Import;\nuse Illuminate\\Http\\UploadedFile;\nuse Illuminate\\Testing\\TestResponse;\nuse ZipArchive;\n\nclass ZipTestHelper\n{\n    public static function importFromData(array $importData, array $zipData, array $files = []): Import\n    {\n        if (isset($zipData['book'])) {\n            $importData['type'] = 'book';\n        } else if (isset($zipData['chapter'])) {\n            $importData['type'] = 'chapter';\n        } else if (isset($zipData['page'])) {\n            $importData['type'] = 'page';\n        }\n\n        $import = Import::factory()->create($importData);\n        $zip = static::zipUploadFromData($zipData, $files);\n        $targetPath = storage_path($import->path);\n        $targetDir = dirname($targetPath);\n\n        if (!file_exists($targetDir)) {\n            mkdir($targetDir);\n        }\n\n        rename($zip->getRealPath(), $targetPath);\n\n        return $import;\n    }\n\n    public static function deleteZipForImport(Import $import): void\n    {\n        $path = storage_path($import->path);\n        if (file_exists($path)) {\n            unlink($path);\n        }\n    }\n\n    public static function zipUploadFromData(array $data, array $files = []): UploadedFile\n    {\n        $zipFile = tempnam(sys_get_temp_dir(), 'bstest-');\n\n        $zip = new ZipArchive();\n        $zip->open($zipFile, ZipArchive::OVERWRITE);\n        $zip->addFromString('data.json', json_encode($data));\n\n        foreach ($files as $name => $file) {\n            $zip->addFile($file, \"files/$name\");\n        }\n\n        $zip->close();\n\n        return new UploadedFile($zipFile, 'upload.zip', 'application/zip', null, true);\n    }\n\n    public static function extractFromZipResponse(TestResponse $response): ZipResultData\n    {\n        $zipData = $response->streamedContent();\n        $zipFile = tempnam(sys_get_temp_dir(), 'bstest-');\n\n        file_put_contents($zipFile, $zipData);\n        $extractDir = tempnam(sys_get_temp_dir(), 'bstestextracted-');\n        if (file_exists($extractDir)) {\n            unlink($extractDir);\n        }\n        mkdir($extractDir);\n\n        $zip = new ZipArchive();\n        $zip->open($zipFile, ZipArchive::RDONLY);\n        $zip->extractTo($extractDir);\n\n        $dataJson = file_get_contents($extractDir . DIRECTORY_SEPARATOR . \"data.json\");\n        $data = json_decode($dataJson, true);\n\n        return new ZipResultData(\n            $zipFile,\n            $extractDir,\n            $data,\n        );\n    }\n}\n"
  },
  {
    "path": "tests/FavouriteTest.php",
    "content": "<?php\n\nnamespace Tests;\n\nuse BookStack\\Activity\\Models\\Favourite;\nuse BookStack\\Users\\Models\\User;\n\nclass FavouriteTest extends TestCase\n{\n    public function test_page_add_favourite_flow()\n    {\n        $page = $this->entities->page();\n        $editor = $this->users->editor();\n\n        $resp = $this->actingAs($editor)->get($page->getUrl());\n        $this->withHtml($resp)->assertElementContains('button', 'Favourite');\n        $this->withHtml($resp)->assertElementExists('form[method=\"POST\"][action$=\"/favourites/add\"] input[name=\"type\"][value=\"page\"]');\n\n        $resp = $this->post('/favourites/add', [\n            'type' => $page->getMorphClass(),\n            'id'   => $page->id,\n        ]);\n        $resp->assertRedirect($page->getUrl());\n        $resp->assertSessionHas('success', \"\\\"{$page->name}\\\" has been added to your favourites\");\n\n        $this->assertDatabaseHas('favourites', [\n            'user_id'           => $editor->id,\n            'favouritable_type' => $page->getMorphClass(),\n            'favouritable_id'   => $page->id,\n        ]);\n    }\n\n    public function test_page_remove_favourite_flow()\n    {\n        $page = $this->entities->page();\n        $editor = $this->users->editor();\n        Favourite::query()->forceCreate([\n            'user_id'           => $editor->id,\n            'favouritable_id'   => $page->id,\n            'favouritable_type' => $page->getMorphClass(),\n        ]);\n\n        $resp = $this->actingAs($editor)->get($page->getUrl());\n        $this->withHtml($resp)->assertElementContains('button', 'Unfavourite');\n        $this->withHtml($resp)->assertElementExists('form[method=\"POST\"][action$=\"/favourites/remove\"]');\n\n        $resp = $this->post('/favourites/remove', [\n            'type' => $page->getMorphClass(),\n            'id'   => $page->id,\n        ]);\n        $resp->assertRedirect($page->getUrl());\n        $resp->assertSessionHas('success', \"\\\"{$page->name}\\\" has been removed from your favourites\");\n\n        $this->assertDatabaseMissing('favourites', [\n            'user_id' => $editor->id,\n        ]);\n    }\n\n    public function test_add_and_remove_redirect_to_entity_without_history()\n    {\n        $page = $this->entities->page();\n\n        $resp = $this->asEditor()->post('/favourites/add', [\n            'type' => $page->getMorphClass(),\n            'id'   => $page->id,\n        ]);\n        $resp->assertRedirect($page->getUrl());\n\n        $resp = $this->asEditor()->post('/favourites/remove', [\n            'type' => $page->getMorphClass(),\n            'id'   => $page->id,\n        ]);\n        $resp->assertRedirect($page->getUrl());\n    }\n\n    public function test_favourite_flow_with_own_permissions()\n    {\n        $book = $this->entities->book();\n        $user = User::factory()->create();\n        $book->owned_by = $user->id;\n        $book->save();\n\n        $this->permissions->grantUserRolePermissions($user, ['book-view-own']);\n\n        $this->actingAs($user)->get($book->getUrl());\n        $resp = $this->post('/favourites/add', [\n            'type' => $book->getMorphClass(),\n            'id'   => $book->id,\n        ]);\n        $resp->assertRedirect($book->getUrl());\n\n        $this->assertDatabaseHas('favourites', [\n            'user_id'           => $user->id,\n            'favouritable_type' => $book->getMorphClass(),\n            'favouritable_id'   => $book->id,\n        ]);\n    }\n\n    public function test_each_entity_type_shows_favourite_button()\n    {\n        $this->actingAs($this->users->editor());\n\n        foreach ($this->entities->all() as $entity) {\n            $resp = $this->get($entity->getUrl());\n            $this->withHtml($resp)->assertElementExists('form[method=\"POST\"][action$=\"/favourites/add\"]');\n        }\n    }\n\n    public function test_header_contains_link_to_favourites_page_when_logged_in()\n    {\n        $this->setSettings(['app-public' => 'true']);\n        $resp = $this->get('/');\n        $this->withHtml($resp)->assertElementNotContains('header', 'My Favourites');\n        $resp = $this->actingAs($this->users->viewer())->get('/');\n        $this->withHtml($resp)->assertElementContains('header a', 'My Favourites');\n    }\n\n    public function test_favourites_shown_on_homepage()\n    {\n        $editor = $this->users->editor();\n\n        $resp = $this->actingAs($editor)->get('/');\n        $this->withHtml($resp)->assertElementNotExists('#top-favourites');\n\n        $page = $this->entities->page();\n        $page->favourites()->save((new Favourite())->forceFill(['user_id' => $editor->id]));\n\n        $resp = $this->get('/');\n        $this->withHtml($resp)->assertElementExists('#top-favourites');\n        $this->withHtml($resp)->assertElementContains('#top-favourites', $page->name);\n    }\n\n    public function test_favourites_list_page_shows_favourites_and_has_working_pagination()\n    {\n        $page = $this->entities->page();\n        $editor = $this->users->editor();\n\n        $resp = $this->actingAs($editor)->get('/favourites');\n        $resp->assertDontSee($page->name);\n\n        $page->favourites()->save((new Favourite())->forceFill(['user_id' => $editor->id]));\n\n        $resp = $this->get('/favourites');\n        $resp->assertSee($page->name);\n\n        $resp = $this->get('/favourites?page=2');\n        $resp->assertDontSee($page->name);\n    }\n}\n"
  },
  {
    "path": "tests/Helpers/EntityProvider.php",
    "content": "<?php\n\nnamespace Tests\\Helpers;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Repos\\BookRepo;\nuse BookStack\\Entities\\Repos\\BookshelfRepo;\nuse BookStack\\Entities\\Repos\\ChapterRepo;\nuse BookStack\\Entities\\Repos\\PageRepo;\nuse BookStack\\Entities\\Tools\\TrashCan;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Database\\Eloquent\\Builder;\n\n/**\n * Class to provider and action entity models for common test case\n * operations. Tracks handled models and only returns fresh models.\n * Does not dedupe against nested/child/parent models.\n */\nclass EntityProvider\n{\n    /**\n     * @var array<string, int[]>\n     */\n    protected array $fetchCache = [\n        'book' => [],\n        'page' => [],\n        'bookshelf' => [],\n        'chapter' => [],\n    ];\n\n    /**\n     * Get an unfetched page from the system.\n     */\n    public function page(callable|null $queryFilter = null): Page\n    {\n        /** @var Page $page */\n        $page = Page::query()->when($queryFilter, $queryFilter)->whereNotIn('id', $this->fetchCache['page'])->first();\n        $this->addToCache($page);\n        return $page;\n    }\n\n    public function pageWithinChapter(): Page\n    {\n        return $this->page(fn(Builder $query) => $query->whereHas('chapter')->with('chapter'));\n    }\n\n    public function pageNotWithinChapter(): Page\n    {\n        return $this->page(fn(Builder $query) => $query->whereNull('chapter_id'));\n    }\n\n    public function templatePage(): Page\n    {\n        $page = $this->page();\n        $page->template = true;\n        $page->save();\n\n        return $page;\n    }\n\n    /**\n     * Get an unfetched chapter from the system.\n     */\n    public function chapter(callable|null $queryFilter = null): Chapter\n    {\n        /** @var Chapter $chapter */\n        $chapter = Chapter::query()->when($queryFilter, $queryFilter)->whereNotIn('id', $this->fetchCache['chapter'])->first();\n        $this->addToCache($chapter);\n        return $chapter;\n    }\n\n    public function chapterHasPages(): Chapter\n    {\n        return $this->chapter(fn(Builder $query) => $query->whereHas('pages'));\n    }\n\n    /**\n     * Get an unfetched book from the system.\n     */\n    public function book(callable|null $queryFilter = null): Book\n    {\n        /** @var Book $book */\n        $book = Book::query()->when($queryFilter, $queryFilter)->whereNotIn('id', $this->fetchCache['book'])->first();\n        $this->addToCache($book);\n        return $book;\n    }\n\n    /**\n     * Get a book that has chapters and pages assigned.\n     */\n    public function bookHasChaptersAndPages(): Book\n    {\n        return $this->book(function (Builder $query) {\n            $query->has('chapters')->has('pages')->with(['chapters', 'pages']);\n        });\n    }\n\n    /**\n     * Get an unfetched shelf from the system.\n     */\n    public function shelf(callable|null $queryFilter = null): Bookshelf\n    {\n        /** @var Bookshelf $shelf */\n        $shelf = Bookshelf::query()->when($queryFilter, $queryFilter)->whereNotIn('id', $this->fetchCache['bookshelf'])->first();\n        $this->addToCache($shelf);\n        return $shelf;\n    }\n\n    /**\n     * Get a shelf that has books assigned.\n     */\n    public function shelfHasBooks(): Bookshelf\n    {\n        return $this->shelf(fn(Builder $query) => $query->whereHas('books'));\n    }\n\n    /**\n     * Get all entity types from the system.\n     * @return array{page: Page, chapter: Chapter, book: Book, bookshelf: Bookshelf}\n     */\n    public function all(): array\n    {\n        return [\n            'page'      => $this->page(),\n            'chapter'   => $this->chapter(),\n            'book'      => $this->book(),\n            'bookshelf' => $this->shelf(),\n        ];\n    }\n\n    public function updatePage(Page $page, array $data): Page\n    {\n        $this->addToCache($page);\n        return app()->make(PageRepo::class)->update($page, $data);\n    }\n\n    /**\n     * Create a book to page chain of entities that belong to a specific user.\n     * @return array{book: Book, chapter: Chapter, page: Page}\n     */\n    public function createChainBelongingToUser(User $creatorUser, ?User $updaterUser = null): array\n    {\n        if (empty($updaterUser)) {\n            $updaterUser = $creatorUser;\n        }\n\n        $userAttrs = ['created_by' => $creatorUser->id, 'owned_by' => $creatorUser->id, 'updated_by' => $updaterUser->id];\n        /** @var Book $book */\n        $book = Book::factory()->create($userAttrs);\n        $chapter = Chapter::factory()->create(array_merge(['book_id' => $book->id], $userAttrs));\n        $page = Page::factory()->create(array_merge(['book_id' => $book->id, 'chapter_id' => $chapter->id], $userAttrs));\n\n        $book->rebuildPermissions();\n        $this->addToCache([$page, $chapter, $book]);\n\n        return compact('book', 'chapter', 'page');\n    }\n\n    /**\n     * Create and return a new bookshelf.\n     */\n    public function newShelf(array $input = ['name' => 'test shelf', 'description' => 'My new test shelf']): Bookshelf\n    {\n        $shelf = app(BookshelfRepo::class)->create($input, []);\n        $this->addToCache($shelf);\n        return $shelf;\n    }\n\n    /**\n     * Create and return a new book.\n     */\n    public function newBook(array $input = ['name' => 'test book', 'description' => 'My new test book']): Book\n    {\n        $book = app(BookRepo::class)->create($input);\n        $this->addToCache($book);\n        return $book;\n    }\n\n    /**\n     * Create and return a new test chapter.\n     */\n    public function newChapter(array $input, Book $book): Chapter\n    {\n        $chapter = app(ChapterRepo::class)->create($input, $book);\n        $this->addToCache($chapter);\n        return $chapter;\n    }\n\n    /**\n     * Create and return a new test page.\n     */\n    public function newPage(array $input = ['name' => 'test page', 'html' => 'My new test page']): Page\n    {\n        $book = $this->book();\n        $pageRepo = app(PageRepo::class);\n        $draftPage = $pageRepo->getNewDraftPage($book);\n        $this->addToCache($draftPage);\n        return $pageRepo->publishDraft($draftPage, $input);\n    }\n\n    /**\n     * Create and return a new test draft page.\n     */\n    public function newDraftPage(array $input = ['name' => 'test page', 'html' => 'My new test page']): Page\n    {\n        $book = $this->book();\n        $pageRepo = app(PageRepo::class);\n        $draftPage = $pageRepo->getNewDraftPage($book);\n        $pageRepo->updatePageDraft($draftPage, $input);\n        $this->addToCache($draftPage);\n        return $draftPage;\n    }\n\n    /**\n     * Send an entity to the recycle bin.\n     */\n    public function sendToRecycleBin(Entity $entity)\n    {\n        $trash = app()->make(TrashCan::class);\n\n        if ($entity instanceof Page) {\n            $trash->softDestroyPage($entity);\n        } elseif ($entity instanceof Chapter) {\n            $trash->softDestroyChapter($entity);\n        } elseif ($entity instanceof Book) {\n            $trash->softDestroyBook($entity);\n        } elseif ($entity instanceof Bookshelf) {\n            $trash->softDestroyBookshelf($entity);\n        }\n\n        $entity->refresh();\n        if (is_null($entity->deleted_at)) {\n            throw new \\Exception(\"Could not send entity type [{$entity->getMorphClass()}] to the recycle bin\");\n        }\n    }\n\n    /**\n     * Fully destroy the given entity from the system, bypassing the recycle bin\n     * stage. Still runs through main app deletion logic.\n     */\n    public function destroy(Entity $entity)\n    {\n        $trash = app()->make(TrashCan::class);\n        $trash->destroyEntity($entity);\n    }\n\n    /**\n     * @param Entity|Entity[] $entities\n     */\n    protected function addToCache($entities): void\n    {\n        if (!is_array($entities)) {\n            $entities = [$entities];\n        }\n\n        foreach ($entities as $entity) {\n            $this->fetchCache[$entity->getType()][] = $entity->id;\n        }\n    }\n}\n"
  },
  {
    "path": "tests/Helpers/FileProvider.php",
    "content": "<?php\n\nnamespace Tests\\Helpers;\n\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Uploads\\Attachment;\nuse BookStack\\Uploads\\AttachmentService;\nuse Illuminate\\Http\\UploadedFile;\nuse Illuminate\\Testing\\TestResponse;\nuse stdClass;\nuse Tests\\TestCase;\n\nclass FileProvider\n{\n    /**\n     * Get the path to a file in the test-data-directory.\n     */\n    public function testFilePath(string $fileName): string\n    {\n        return base_path('tests/test-data/' . $fileName);\n    }\n\n    /**\n     * Creates a new temporary image file using the given name,\n     * with the content decoded from the given bas64 file name.\n     * Is generally used for testing sketchy files that could trip AV.\n     */\n    public function imageFromBase64File(string $base64FileName, string $imageFileName): UploadedFile\n    {\n        $imagePath = implode(DIRECTORY_SEPARATOR, [sys_get_temp_dir(), $imageFileName]);\n        $base64FilePath = $this->testFilePath($base64FileName);\n        $data = file_get_contents($base64FilePath);\n        $decoded = base64_decode($data);\n        file_put_contents($imagePath, $decoded);\n\n        return new UploadedFile($imagePath, $imageFileName, 'image/png', null, true);\n    }\n\n    /**\n     * Get a test image UploadedFile instance, that can be uploaded via test requests.\n     */\n    public function uploadedImage(string $fileName, string $testDataFileName = ''): UploadedFile\n    {\n        return new UploadedFile($this->testFilePath($testDataFileName ?: 'test-image.png'), $fileName, 'image/png', null, true);\n    }\n\n    /**\n     * Get a test txt UploadedFile instance, that can be uploaded via test requests.\n     */\n    public function uploadedTextFile(string $fileName): UploadedFile\n    {\n        return new UploadedFile($this->testFilePath('test-file.txt'), $fileName, 'text/plain', null, true);\n    }\n\n    /**\n     * Get raw data for a PNG image test file.\n     */\n    public function pngImageData(): string\n    {\n        return file_get_contents($this->testFilePath('test-image.png'));\n    }\n\n    /**\n     * Get raw data for a Jpeg image test file.\n     */\n    public function jpegImageData(): string\n    {\n        return file_get_contents($this->testFilePath('test-image.jpg'));\n    }\n\n    /**\n     * Get the expected relative path for an uploaded image of the given type and filename.\n     */\n    public function expectedImagePath(string $imageType, string $fileName): string\n    {\n        return '/uploads/images/' . $imageType . '/' . date('Y-m') . '/' . $fileName;\n    }\n\n    /**\n     * Performs an image gallery upload request with the given name.\n     */\n    public function uploadGalleryImage(TestCase $case, string $name, int $uploadedTo = 0, string $contentType = 'image/png', string $testDataFileName = ''): TestResponse\n    {\n        $file = $this->uploadedImage($name, $testDataFileName);\n\n        return $case->call('POST', '/images/gallery', ['uploaded_to' => $uploadedTo], [], ['file' => $file], ['CONTENT_TYPE' => $contentType]);\n    }\n\n    /**\n     * Upload a new gallery image and return a set of details about the image,\n     * including the json decoded response of the upload.\n     * Ensures the upload succeeds.\n     *\n     * @return array{name: string, path: string, page: Page, response: stdClass}\n     */\n    public function uploadGalleryImageToPage(TestCase $case, Page $page, string $testDataFileName = ''): array\n    {\n        $imageName = $testDataFileName ?: 'first-image.png';\n        $relPath = $this->expectedImagePath('gallery', $imageName);\n        $this->deleteAtRelativePath($relPath);\n\n        $upload = $this->uploadGalleryImage($case, $imageName, $page->id, 'image/png', $testDataFileName);\n        $upload->assertStatus(200);\n\n        return [\n            'name' => $imageName,\n            'path' => $relPath,\n            'page' => $page,\n            'response' => json_decode($upload->getContent()),\n        ];\n    }\n\n    /**\n     * Uploads an attachment file with the given name.\n     */\n    public function uploadAttachmentFile(TestCase $case, string $name, int $uploadedTo = 0): TestResponse\n    {\n        $file = $this->uploadedTextFile($name);\n\n        return $case->call('POST', '/attachments/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []);\n    }\n\n    /**\n     * Upload a new attachment from the given raw data of the given type, to the given page.\n     * Returns the attachment\n     */\n    public function uploadAttachmentDataToPage(TestCase $case, Page $page, string $filename, string $content, string $mimeType): Attachment\n    {\n        $file = tmpfile();\n        $filePath = stream_get_meta_data($file)['uri'];\n        file_put_contents($filePath, $content);\n        $upload = new UploadedFile($filePath, $filename, $mimeType, null, true);\n\n        $case->call('POST', '/attachments/upload', ['uploaded_to' => $page->id], [], ['file' => $upload], []);\n\n        return $page->attachments()->where('uploaded_to', '=', $page->id)->latest()->firstOrFail();\n    }\n\n    /**\n     * Delete an uploaded image.\n     */\n    public function deleteAtRelativePath(string $path): void\n    {\n        $fullPath = $this->relativeToFullPath($path);\n        if (file_exists($fullPath)) {\n            unlink($fullPath);\n        }\n    }\n\n    /**\n     * Convert a relative path used by default in this provider to a full\n     * absolute local filesystem path.\n     */\n    public function relativeToFullPath(string $path): string\n    {\n        return public_path($path);\n    }\n\n    /**\n     * Delete all uploaded files.\n     * To assist with cleanup.\n     */\n    public function deleteAllAttachmentFiles(): void\n    {\n        $fileService = app()->make(AttachmentService::class);\n        foreach (Attachment::all() as $file) {\n            $fileService->deleteFile($file);\n        }\n    }\n\n    /**\n     * Reset the application favicon image in the public path.\n     */\n    public function resetAppFavicon(): void\n    {\n        file_put_contents(public_path('favicon.ico'), file_get_contents(public_path('icon.ico')));\n    }\n}\n"
  },
  {
    "path": "tests/Helpers/OidcJwtHelper.php",
    "content": "<?php\n\nnamespace Tests\\Helpers;\n\nuse phpseclib3\\Crypt\\RSA;\n\n/**\n * A collection of functions to help with OIDC JWT testing.\n * By default, unless overridden, content is provided in a correct working state.\n */\nclass OidcJwtHelper\n{\n    public static function defaultIssuer(): string\n    {\n        return 'https://auth.example.com';\n    }\n\n    public static function defaultClientId(): string\n    {\n        return 'xxyyzz.aaa.bbccdd.123';\n    }\n\n    public static function defaultPayload(): array\n    {\n        return [\n            'sub'                => 'abc1234def',\n            'name'               => 'Barry Scott',\n            'email'              => 'bscott@example.com',\n            'ver'                => 1,\n            'iss'                => static::defaultIssuer(),\n            'aud'                => static::defaultClientId(),\n            'iat'                => time(),\n            'exp'                => time() + 720,\n            'jti'                => 'ID.AaaBBBbbCCCcccDDddddddEEEeeeeee',\n            'amr'                => ['pwd'],\n            'idp'                => 'fghfghgfh546456dfgdfg',\n            'preferred_username' => 'xXBazzaXx',\n            'auth_time'          => time(),\n            'at_hash'            => 'sT4jbsdSGy9w12pq3iNYDA',\n        ];\n    }\n\n    public static function idToken($payloadOverrides = [], $headerOverrides = []): string\n    {\n        $payload = array_merge(static::defaultPayload(), $payloadOverrides);\n        $header = array_merge([\n            'kid' => 'xyz456',\n            'alg' => 'RS256',\n        ], $headerOverrides);\n\n        $top = implode('.', [\n            static::base64UrlEncode(json_encode($header)),\n            static::base64UrlEncode(json_encode($payload)),\n        ]);\n\n        $privateKey = static::privateKeyInstance();\n        $signature = $privateKey->sign($top);\n\n        return $top . '.' . static::base64UrlEncode($signature);\n    }\n\n    public static function privateKeyInstance()\n    {\n        static $key;\n        if (is_null($key)) {\n            $key = RSA::loadPrivateKey(static::privatePemKey())->withPadding(RSA::SIGNATURE_PKCS1);\n        }\n\n        return $key;\n    }\n\n    public static function base64UrlEncode(string $decoded): string\n    {\n        return strtr(base64_encode($decoded), '+/', '-_');\n    }\n\n    public static function publicPemKey(): string\n    {\n        return '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqo1OmfNKec5S2zQC4SP9\nDrHuUR0VgCi6oqcGERz7zqO36hqk3A3R3aCgJkEjfnbnMuszRRKs45NbXoOp9pvm\nzXL16c93Obn7G8x8A3ao6yN5qKO5S5+CETqOZfKN/g75Xlz7VsC3igOhgsXnPx6i\niM6sbYbk0U/XpFaT84LXKI8VTIPUo7gTeZN1pTET//i9FlzAOzX+xfWBKdOqlEzl\n+zihMHCZUUvQu99P+o0MDR0lMUT+vPJ6SJeRfnoHexwt6bZFiNnsZIEL03bX4QNk\nWvsLta1+jNUee+8IPVhzCO8bvM86NzLaKUJ4k6NZ5IVrmdCFpFsjCWByOrDG8wdw\n3wIDAQAB\n-----END PUBLIC KEY-----';\n    }\n\n    public static function privatePemKey(): string\n    {\n        return '-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCqjU6Z80p5zlLb\nNALhI/0Ose5RHRWAKLqipwYRHPvOo7fqGqTcDdHdoKAmQSN+ducy6zNFEqzjk1te\ng6n2m+bNcvXpz3c5ufsbzHwDdqjrI3moo7lLn4IROo5l8o3+DvleXPtWwLeKA6GC\nxec/HqKIzqxthuTRT9ekVpPzgtcojxVMg9SjuBN5k3WlMRP/+L0WXMA7Nf7F9YEp\n06qUTOX7OKEwcJlRS9C730/6jQwNHSUxRP688npIl5F+egd7HC3ptkWI2exkgQvT\ndtfhA2Ra+wu1rX6M1R577wg9WHMI7xu8zzo3MtopQniTo1nkhWuZ0IWkWyMJYHI6\nsMbzB3DfAgMBAAECggEADm7K2ghWoxwsstQh8j+DaLzx9/dIHIJV2PHdd5FGVeRQ\n6gS7MswQmHrBUrtsb4VMZ2iz/AJqkw+jScpGldH3pCc4XELsSfxNHbseO4TNIqjr\n4LOKOLYU4bRc3I+8KGXIAI5JzrucTJemEVUCDrte8cjbmqExt+zTyNpyxsapworF\nv+vnSdv40d62f+cS1xvwB+ymLK/B/wZ/DemDCi8jsi7ou/M7l5xNCzjH4iMSLtOW\nfgEhejIBG9miMJWPiVpTXE3tMdNuN3OsWc4XXm2t4VRovlZdu30Fax1xWB+Locsv\nHlHKLOFc8g+jZh0TL2KCNjPffMcC7kHhW3afshpIsQKBgQDhyWUnkqd6FzbwIX70\nSnaMgKoUv5W/K5T+Sv/PA2CyN8Gu8ih/OsoNZSnI0uqe3XQIvvgN/Fq3wO1ttLzf\nz5B6ZC7REfTgcR0190gihk6f5rtcj7d6Fy/oG2CE8sDSXgPnpEaBjvJVgN5v/U2s\nHpVaidmHTyGLCfEszoeoy8jyrQKBgQDBX8caGylmzQLc6XNntZChlt3e18Nj8MPA\nDxWLcoqgdDoofLDQAmLl+vPKyDmhQjos5eas1jgmVVEM4ge+MysaVezvuLBsSnOh\nihc0i63USU6i7YDE83DrCewCthpFHi/wW1S5FoCAzpVy8y99vwcqO4kOXcmf4O6Y\nuW6sMsjvOwKBgQDbFtqB+MtsLCSSBF61W6AHHD5tna4H75lG2623yXZF2NanFLF5\nK6muL9DI3ujtOMQETJJUt9+rWJjLEEsJ/dYa/SV0l7D/LKOEnyuu3JZkkLaTzZzi\n6qcA2bfhqdCzEKlHV99WjkfV8hNlpex9rLuOPB8JLh7FVONicBGxF/UojQKBgDXs\nIlYaSuI6utilVKQP0kPtEPOKERc2VS+iRSy8hQGXR3xwwNFQSQm+f+sFCGT6VcSd\nW0TI+6Fc2xwPj38vP465dTentbKM1E+wdSYW6SMwSfhO6ECDbfJsst5Sr2Kkt1N7\n9FUkfDLu6GfEfnK/KR1SurZB2u51R7NYyg7EnplvAoGAT0aTtOcck0oYN30g5mdf\nefqXPwg2wAPYeiec49EbfnteQQKAkqNfJ9K69yE2naf6bw3/5mCBsq/cXeuaBMII\nylysUIRBqt2J0kWm2yCpFWR7H+Ilhdx9A7ZLCqYVt8e+vjO/BOI3cQDe2VPOLPSl\nq/1PY4iJviGKddtmfClH3v4=\n-----END PRIVATE KEY-----';\n    }\n\n    public static function publicJwkKeyArray(): array\n    {\n        return [\n            'kty' => 'RSA',\n            'alg' => 'RS256',\n            'kid' => '066e52af-8884-4926-801d-032a276f9f2a',\n            'use' => 'sig',\n            'e'   => 'AQAB',\n            'n'   => 'qo1OmfNKec5S2zQC4SP9DrHuUR0VgCi6oqcGERz7zqO36hqk3A3R3aCgJkEjfnbnMuszRRKs45NbXoOp9pvmzXL16c93Obn7G8x8A3ao6yN5qKO5S5-CETqOZfKN_g75Xlz7VsC3igOhgsXnPx6iiM6sbYbk0U_XpFaT84LXKI8VTIPUo7gTeZN1pTET__i9FlzAOzX-xfWBKdOqlEzl-zihMHCZUUvQu99P-o0MDR0lMUT-vPJ6SJeRfnoHexwt6bZFiNnsZIEL03bX4QNkWvsLta1-jNUee-8IPVhzCO8bvM86NzLaKUJ4k6NZ5IVrmdCFpFsjCWByOrDG8wdw3w',\n        ];\n    }\n}\n"
  },
  {
    "path": "tests/Helpers/PermissionsProvider.php",
    "content": "<?php\n\nnamespace Tests\\Helpers;\n\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Permissions\\Models\\EntityPermission;\nuse BookStack\\Permissions\\Models\\RolePermission;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Settings\\SettingService;\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Models\\User;\n\nclass PermissionsProvider\n{\n    public function __construct(\n        protected UserRoleProvider $userRoleProvider\n    ) {\n    }\n\n    public function makeAppPublic(): void\n    {\n        $settings = app(SettingService::class);\n        $settings->put('app-public', 'true');\n    }\n\n    /**\n     * Grant role permissions to the provided user.\n     */\n    public function grantUserRolePermissions(User $user, array $permissions): void\n    {\n        $newRole = $this->userRoleProvider->createRole($permissions);\n        $user->attachRole($newRole);\n        $user->load('roles');\n        $user->clearPermissionCache();\n    }\n\n    /**\n     * Completely remove specific role permissions from the provided user.\n     */\n    public function removeUserRolePermissions(User $user, array $permissions): void\n    {\n        foreach ($permissions as $permissionName) {\n            /** @var RolePermission $permission */\n            $permission = RolePermission::query()\n                ->where('name', '=', $permissionName)\n                ->firstOrFail();\n\n            $roles = $user->roles()->whereHas('permissions', function ($query) use ($permission) {\n                $query->where('id', '=', $permission->id);\n            })->get();\n\n            /** @var Role $role */\n            foreach ($roles as $role) {\n                $role->detachPermission($permission);\n            }\n\n            $user->clearPermissionCache();\n        }\n    }\n\n    /**\n     * Change the owner of the given entity to the given user.\n     */\n    public function changeEntityOwner(Entity $entity, User $newOwner): void\n    {\n        $entity->owned_by = $newOwner->id;\n        $entity->save();\n        $entity->rebuildPermissions();\n    }\n\n    /**\n     * Regenerate the permission for an entity.\n     * Centralised to manage clearing of cached elements between requests.\n     */\n    public function regenerateForEntity(Entity $entity): void\n    {\n        $entity->rebuildPermissions();\n    }\n\n    /**\n     * Set the given entity as having restricted permissions, and apply the given\n     * permissions for the given roles.\n     * @param string[] $actions\n     * @param Role[] $roles\n     */\n    public function setEntityPermissions(Entity $entity, array $actions = [], array $roles = [], $inherit = false): void\n    {\n        $entity->permissions()->delete();\n\n        $permissions = [];\n\n        if (!$inherit) {\n            // Set default permissions to not allow actions so that only the provided role permissions are at play.\n            $permissions[] = ['role_id' => 0, 'view' => false, 'create' => false, 'update' => false, 'delete' => false];\n        }\n\n        foreach ($roles as $role) {\n            $permissions[] = $this->actionListToEntityPermissionData($actions, $role->id);\n        }\n\n        $this->addEntityPermissionEntries($entity, $permissions);\n    }\n\n    public function addEntityPermission(Entity $entity, array $actionList, Role $role)\n    {\n        $permissionData = $this->actionListToEntityPermissionData($actionList, $role->id);\n        $this->addEntityPermissionEntries($entity, [$permissionData]);\n    }\n\n    public function setFallbackPermissions(Entity $entity, array $actionList)\n    {\n        $entity->permissions()->where('role_id', '=', 0)->delete();\n        $permissionData = $this->actionListToEntityPermissionData($actionList, 0);\n        $this->addEntityPermissionEntries($entity, [$permissionData]);\n    }\n\n    /**\n     * Disable inherited permissions on the given entity.\n     * Effectively sets the \"Other Users\" UI permission option to not inherit, with no permissions.\n     */\n    public function disableEntityInheritedPermissions(Entity $entity): void\n    {\n        $entity->permissions()->where('role_id', '=', 0)->delete();\n        $fallback = $this->actionListToEntityPermissionData([]);\n        $this->addEntityPermissionEntries($entity, [$fallback]);\n    }\n\n    protected function addEntityPermissionEntries(Entity $entity, array $entityPermissionData): void\n    {\n        $entity->permissions()->createMany($entityPermissionData);\n        $entity->load('permissions');\n        $this->regenerateForEntity($entity);\n    }\n\n    /**\n     * For the given simple array of string actions (view, create, update, delete), convert\n     * the format to entity permission data, where permission is granted if the action is in the\n     * given actionList array.\n     */\n    protected function actionListToEntityPermissionData(array $actionList, int $roleId = 0): array\n    {\n        $permissionData = ['role_id' => $roleId];\n        foreach (Permission::genericForEntity() as $permission) {\n            $permissionData[$permission->value] = in_array($permission->value, $actionList);\n        }\n\n        return $permissionData;\n    }\n}\n"
  },
  {
    "path": "tests/Helpers/TestServiceProvider.php",
    "content": "<?php\n\nnamespace Tests\\Helpers;\n\nuse Illuminate\\Support\\Facades\\Artisan;\nuse Illuminate\\Support\\Facades\\ParallelTesting;\nuse Illuminate\\Support\\ServiceProvider;\n\nclass TestServiceProvider extends ServiceProvider\n{\n    /**\n     * Bootstrap services.\n     *\n     * @return void\n     */\n    public function boot()\n    {\n        // Tell Laravel's parallel testing functionality to seed the test\n        // databases with the DummyContentSeeder upon creation.\n        // This is only done for initial database creation. Seeding\n        // won't occur on every run.\n        ParallelTesting::setUpTestDatabase(function ($database, $token) {\n            Artisan::call('db:seed --class=DummyContentSeeder');\n        });\n    }\n}\n"
  },
  {
    "path": "tests/Helpers/UserRoleProvider.php",
    "content": "<?php\n\nnamespace Tests\\Helpers;\n\nuse BookStack\\Permissions\\PermissionsRepo;\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Models\\User;\n\nclass UserRoleProvider\n{\n    protected ?User $admin = null;\n    protected ?User $editor = null;\n\n    /**\n     * Get a typical \"Admin\" user.\n     */\n    public function admin(): User\n    {\n        if (is_null($this->admin)) {\n            $adminRole = Role::getSystemRole('admin');\n            $this->admin = $adminRole->users()->first();\n        }\n\n        return $this->admin;\n    }\n\n    /**\n     * Get a typical \"Editor\" user.\n     */\n    public function editor(): User\n    {\n        if ($this->editor === null) {\n            $editorRole = Role::getRole('editor');\n            $this->editor = $editorRole->users->first();\n        }\n\n        return $this->editor;\n    }\n\n    /**\n     * Get a typical \"Viewer\" user.\n     */\n    public function viewer(array $attributes = []): User\n    {\n        $user = Role::getRole('viewer')->users()->first();\n        if (!empty($attributes)) {\n            $user->forceFill($attributes)->save();\n        }\n\n        return $user;\n    }\n\n    /**\n     * Get the system \"guest\" user.\n     */\n    public function guest(): User\n    {\n        return User::getGuest();\n    }\n\n    /**\n     * Create a new fresh user without any relations.\n     */\n    public function newUser(array $attrs = []): User\n    {\n        return User::factory()->create($attrs);\n    }\n\n    /**\n     * Create a new fresh user, with the given attrs, that has assigned a fresh role\n     * that has the given role permissions.\n     * Intended as a helper to create a blank slate baseline user and role.\n     * @return array{0: User, 1: Role}\n     */\n    public function newUserWithRole(array $userAttrs = [], array $rolePermissions = []): array\n    {\n        $user = $this->newUser($userAttrs);\n        $role = $this->attachNewRole($user, $rolePermissions);\n\n        return [$user, $role];\n    }\n\n    /**\n     * Attach a new role, with the given role permissions, to the given user\n     * and return that role.\n     */\n    public function attachNewRole(User $user, array $rolePermissions = []): Role\n    {\n        $role = $this->createRole($rolePermissions);\n        $user->attachRole($role);\n        return $role;\n    }\n\n    /**\n     * Create a new basic role with the given role permissions.\n     */\n    public function createRole(array $rolePermissions = []): Role\n    {\n        $permissionRepo = app(PermissionsRepo::class);\n        $roleData = Role::factory()->make()->toArray();\n        $roleData['permissions'] = $rolePermissions;\n\n        return $permissionRepo->saveNewRole($roleData);\n    }\n}\n"
  },
  {
    "path": "tests/HomepageTest.php",
    "content": "<?php\n\nnamespace Tests;\n\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Models\\User;\n\nclass HomepageTest extends TestCase\n{\n    public function test_default_homepage_visible()\n    {\n        $this->asEditor();\n        $homeVisit = $this->get('/');\n        $homeVisit->assertSee('My Recently Viewed');\n        $homeVisit->assertSee('Recently Updated Pages');\n        $homeVisit->assertSee('Recent Activity');\n        $homeVisit->assertSee('home-default');\n    }\n\n    public function test_custom_homepage()\n    {\n        $this->asEditor();\n        $name = 'My custom homepage';\n        $content = str_repeat('This is the body content of my custom homepage.', 20);\n        $customPage = $this->entities->newPage(['name' => $name, 'html' => $content]);\n        $this->setSettings(['app-homepage' => $customPage->id]);\n        $this->setSettings(['app-homepage-type' => 'page']);\n\n        $homeVisit = $this->get('/');\n        $homeVisit->assertSee($name);\n        $homeVisit->assertSee($content);\n        $homeVisit->assertSee('My Recently Viewed');\n        $homeVisit->assertSee('Recently Updated Pages');\n        $homeVisit->assertSee('Recent Activity');\n    }\n\n    public function test_delete_custom_homepage()\n    {\n        $this->asEditor();\n        $name = 'My custom homepage';\n        $content = str_repeat('This is the body content of my custom homepage.', 20);\n        $customPage = $this->entities->newPage(['name' => $name, 'html' => $content]);\n        $this->setSettings([\n            'app-homepage'      => $customPage->id,\n            'app-homepage-type' => 'page',\n        ]);\n\n        $homeVisit = $this->get('/');\n        $homeVisit->assertSee($name);\n        $this->withHtml($homeVisit)->assertElementNotExists('#home-default');\n\n        $pageDeleteReq = $this->delete($customPage->getUrl());\n        $pageDeleteReq->assertStatus(302);\n        $pageDeleteReq->assertRedirect($customPage->getUrl());\n        $pageDeleteReq->assertSessionHas('error');\n        $pageDeleteReq->assertSessionMissing('success');\n\n        $homeVisit = $this->get('/');\n        $homeVisit->assertSee($name);\n        $homeVisit->assertStatus(200);\n    }\n\n    public function test_custom_homepage_can_be_deleted_once_custom_homepage_no_longer_used()\n    {\n        $this->asEditor();\n        $name = 'My custom homepage';\n        $content = str_repeat('This is the body content of my custom homepage.', 20);\n        $customPage = $this->entities->newPage(['name' => $name, 'html' => $content]);\n        $this->setSettings([\n            'app-homepage'      => $customPage->id,\n            'app-homepage-type' => 'default',\n        ]);\n\n        $pageDeleteReq = $this->delete($customPage->getUrl());\n        $pageDeleteReq->assertStatus(302);\n        $pageDeleteReq->assertSessionHas('success');\n        $pageDeleteReq->assertSessionMissing('error');\n    }\n\n    public function test_custom_homepage_cannot_be_deleted_from_parent_deletion()\n    {\n        $page = $this->entities->page();\n        $this->setSettings([\n            'app-homepage'      => $page->id,\n            'app-homepage-type' => 'page',\n        ]);\n\n        $this->asEditor()->delete($page->book->getUrl());\n        $this->assertSessionError('Cannot delete a page while it is set as a homepage');\n        $this->assertDatabaseMissing('deletions', ['deletable_id' => $page->book->id]);\n\n        $page->refresh();\n        $this->assertNull($page->deleted_at);\n        $this->assertNull($page->book->deleted_at);\n    }\n\n    public function test_custom_homepage_renders_includes()\n    {\n        $this->asEditor();\n        $included = $this->entities->page();\n        $content = str_repeat('This is the body content of my custom homepage.', 20);\n        $included->html = $content;\n        $included->save();\n\n        $name = 'My custom homepage';\n        $customPage = $this->entities->newPage(['name' => $name, 'html' => '{{@' . $included->id . '}}']);\n        $this->setSettings(['app-homepage' => $customPage->id]);\n        $this->setSettings(['app-homepage-type' => 'page']);\n\n        $homeVisit = $this->get('/');\n        $homeVisit->assertSee($name);\n        $homeVisit->assertSee($content);\n    }\n\n    public function test_set_book_homepage()\n    {\n        $editor = $this->users->editor();\n        setting()->putUser($editor, 'books_view_type', 'grid');\n\n        $this->setSettings(['app-homepage-type' => 'books']);\n\n        $this->asEditor();\n        $homeVisit = $this->get('/');\n        $homeVisit->assertSee('Books');\n        $homeVisit->assertSee('grid-card');\n        $homeVisit->assertSee('grid-card-content');\n        $homeVisit->assertSee('grid-card-footer');\n        $homeVisit->assertSee('featured-image-container');\n    }\n\n    public function test_set_bookshelves_homepage()\n    {\n        $editor = $this->users->editor();\n        setting()->putUser($editor, 'bookshelves_view_type', 'grid');\n        $shelf = $this->entities->shelf();\n\n        $this->setSettings(['app-homepage-type' => 'bookshelves']);\n\n        $this->asEditor();\n        $homeVisit = $this->get('/');\n        $homeVisit->assertSee('Shelves');\n        $homeVisit->assertSee('grid-card-content');\n        $homeVisit->assertSee('featured-image-container');\n        $this->withHtml($homeVisit)->assertElementContains('.grid-card', $shelf->name);\n    }\n\n    public function test_books_and_bookshelves_homepage_has_expected_actions()\n    {\n        $this->asEditor();\n\n        foreach (['bookshelves', 'books'] as $homepageType) {\n            $this->setSettings(['app-homepage-type' => $homepageType]);\n\n            $html = $this->withHtml($this->get('/'));\n            $html->assertElementContains('.actions button', 'Dark Mode');\n            $html->assertElementContains('.actions a[href$=\"/tags\"]', 'View Tags');\n        }\n    }\n\n    public function test_shelves_list_homepage_adheres_to_book_visibility_permissions()\n    {\n        $editor = $this->users->editor();\n        setting()->putUser($editor, 'bookshelves_view_type', 'list');\n        $this->setSettings(['app-homepage-type' => 'bookshelves']);\n        $this->asEditor();\n\n        $shelf = $this->entities->shelf();\n        $book = $shelf->books()->first();\n\n        // Ensure initially visible\n        $homeVisit = $this->get('/');\n        $this->withHtml($homeVisit)->assertElementContains('.content-wrap', $shelf->name);\n        $this->withHtml($homeVisit)->assertElementContains('.content-wrap', $book->name);\n\n        // Ensure book no longer visible without view permission\n        $editor->roles()->detach();\n        $this->permissions->grantUserRolePermissions($editor, ['bookshelf-view-all']);\n        $homeVisit = $this->get('/');\n        $this->withHtml($homeVisit)->assertElementContains('.content-wrap', $shelf->name);\n        $this->withHtml($homeVisit)->assertElementNotContains('.content-wrap', $book->name);\n\n        // Ensure is visible again with entity-level view permission\n        $this->permissions->setEntityPermissions($book, ['view'], [$editor->roles()->first()]);\n        $homeVisit = $this->get('/');\n        $this->withHtml($homeVisit)->assertElementContains('.content-wrap', $shelf->name);\n        $this->withHtml($homeVisit)->assertElementContains('.content-wrap', $book->name);\n    }\n\n    public function test_new_users_dont_have_any_recently_viewed()\n    {\n        $user = User::factory()->create();\n        $viewRole = Role::getRole('Viewer');\n        $user->attachRole($viewRole);\n\n        $homeVisit = $this->actingAs($user)->get('/');\n        $this->withHtml($homeVisit)->assertElementContains('#recently-viewed', 'You have not viewed any pages');\n    }\n}\n"
  },
  {
    "path": "tests/LanguageTest.php",
    "content": "<?php\n\nnamespace Tests;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Translation\\LocaleManager;\n\nclass LanguageTest extends TestCase\n{\n    protected array $langs;\n\n    /**\n     * LanguageTest constructor.\n     */\n    protected function setUp(): void\n    {\n        parent::setUp();\n        $this->langs = array_diff(scandir(lang_path('')), ['..', '.']);\n    }\n\n    public function test_locales_list_set_properly()\n    {\n        $appLocales = $this->app->make(LocaleManager::class)->getAllAppLocales();\n        sort($appLocales);\n        sort($this->langs);\n        $this->assertEquals(implode(':', $this->langs), implode(':', $appLocales), 'app.locales configuration variable does not match those found in lang files');\n    }\n\n    // Not part of standard phpunit test runs since we sometimes expect non-added langs.\n    public function test_locales_all_have_language_dropdown_entry()\n    {\n        $this->markTestSkipped('Only used when checking language inclusion');\n\n        $dropdownLocales = array_keys(trans('settings.language_select', [], 'en'));\n        sort($dropdownLocales);\n        sort($this->langs);\n        $diffs = array_diff($this->langs, $dropdownLocales);\n        if (count($diffs) > 0) {\n            $diffText = implode(',', $diffs);\n            $warning = \"Languages: {$diffText} found in files but not in language select dropdown.\";\n            $this->fail($warning);\n        }\n        $this->assertTrue(true);\n    }\n\n    public function test_correct_language_if_not_logged_in()\n    {\n        $loginReq = $this->get('/login');\n        $loginReq->assertSee('Log In');\n\n        $loginPageFrenchReq = $this->get('/login', ['Accept-Language' => 'fr']);\n        $loginPageFrenchReq->assertSee('Se Connecter');\n    }\n\n    public function test_public_lang_autodetect_can_be_disabled()\n    {\n        config()->set('app.auto_detect_locale', false);\n        $loginReq = $this->get('/login');\n        $loginReq->assertSee('Log In');\n\n        $loginPageFrenchReq = $this->get('/login', ['Accept-Language' => 'fr']);\n        $loginPageFrenchReq->assertDontSee('Se Connecter');\n    }\n\n    public function test_all_lang_files_loadable()\n    {\n        $files = array_diff(scandir(lang_path('en')), ['..', '.']);\n        foreach ($this->langs as $lang) {\n            foreach ($files as $file) {\n                $loadError = false;\n\n                try {\n                    $translations = trans(str_replace('.php', '', $file), [], $lang);\n                } catch (\\Exception $e) {\n                    $loadError = true;\n                }\n                $this->assertFalse($loadError, \"Translation file {$lang}/{$file} failed to load\");\n            }\n        }\n    }\n\n    public function test_views_use_rtl_if_rtl_language_is_set()\n    {\n        $this->asEditor()->withHtml($this->get('/'))->assertElementExists('html[dir=\"ltr\"]');\n\n        setting()->putUser($this->users->editor(), 'language', 'ar');\n\n        $this->withHtml($this->get('/'))->assertElementExists('html[dir=\"rtl\"]');\n    }\n\n    public function test_unknown_lang_does_not_break_app()\n    {\n        config()->set('app.locale', 'zz');\n\n        $loginReq = $this->get('/login', ['Accept-Language' => 'zz']);\n        $loginReq->assertOk();\n        $loginReq->assertSee('Log In');\n    }\n\n    public function test_all_activity_types_have_activity_text()\n    {\n        foreach (ActivityType::all() as $activityType) {\n            $langKey = 'activities.' . $activityType;\n            $this->assertNotEquals($langKey, trans($langKey, [], 'en'));\n        }\n    }\n}\n"
  },
  {
    "path": "tests/Meta/HelpTest.php",
    "content": "<?php\n\nnamespace Tests\\Meta;\n\nuse Tests\\TestCase;\n\nclass HelpTest extends TestCase\n{\n    public function test_tinymce_help_shows_tiny_and_tiny_license_link()\n    {\n        $resp = $this->get('/help/tinymce');\n        $resp->assertOk();\n        $this->withHtml($resp)->assertElementExists('a[href=\"https://www.tiny.cloud/\"]');\n        $this->withHtml($resp)->assertElementExists('a[href=\"' . url('/libs/tinymce/license.txt') . '\"]');\n    }\n\n    public function test_tiny_license_exists_where_expected()\n    {\n        $expectedPath = public_path('/libs/tinymce/license.txt');\n        $this->assertTrue(file_exists($expectedPath));\n\n        $contents = file_get_contents($expectedPath);\n        $this->assertStringContainsString('MIT License', $contents);\n    }\n\n    public function test_wysiwyg_help_shows_lexical_and_licenses_link()\n    {\n        $resp = $this->get('/help/wysiwyg');\n        $resp->assertOk();\n        $this->withHtml($resp)->assertElementExists('a[href=\"https://lexical.dev/\"]');\n        $this->withHtml($resp)->assertElementExists('a[href=\"' . url('/licenses') . '\"]');\n    }\n}\n"
  },
  {
    "path": "tests/Meta/LicensesTest.php",
    "content": "<?php\n\nnamespace Tests\\Meta;\n\nuse Tests\\TestCase;\n\nclass LicensesTest extends TestCase\n{\n    public function test_licenses_endpoint()\n    {\n        $resp = $this->get('/licenses');\n        $resp->assertOk();\n        $resp->assertSee('Licenses');\n        $resp->assertSee('PHP Library Licenses');\n        $resp->assertSee('Dan Brown and the BookStack project contributors');\n        $resp->assertSee('league/commonmark');\n        $resp->assertSee('@codemirror/lang-html');\n    }\n\n    public function test_licenses_linked_to_from_settings()\n    {\n        $resp = $this->asAdmin()->get('/settings/features');\n        $html = $this->withHtml($resp);\n        $html->assertLinkExists(url('/licenses'), 'License Details');\n    }\n}\n"
  },
  {
    "path": "tests/Meta/OpenGraphTest.php",
    "content": "<?php\n\nnamespace Tests\\Meta;\n\nuse BookStack\\Entities\\Repos\\BaseRepo;\nuse BookStack\\Entities\\Repos\\BookRepo;\nuse Illuminate\\Support\\Str;\nuse Illuminate\\Testing\\TestResponse;\nuse Tests\\TestCase;\n\nclass OpenGraphTest extends TestCase\n{\n    public function test_page_tags()\n    {\n        $page = $this->entities->page();\n        $resp = $this->asEditor()->get($page->getUrl());\n        $tags = $this->getOpenGraphTags($resp);\n\n        $this->assertEquals($page->getShortName() . ' | BookStack', $tags['title']);\n        $this->assertEquals($page->getUrl(), $tags['url']);\n        $this->assertEquals(Str::limit($page->text, 100, '...'), $tags['description']);\n    }\n\n    public function test_chapter_tags()\n    {\n        $chapter = $this->entities->chapter();\n        $resp = $this->asEditor()->get($chapter->getUrl());\n        $tags = $this->getOpenGraphTags($resp);\n\n        $this->assertEquals($chapter->getShortName() . ' | BookStack', $tags['title']);\n        $this->assertEquals($chapter->getUrl(), $tags['url']);\n        $this->assertEquals(Str::limit($chapter->description, 100, '...'), $tags['description']);\n    }\n\n    public function test_book_tags()\n    {\n        $book = $this->entities->book();\n        $resp = $this->asEditor()->get($book->getUrl());\n        $tags = $this->getOpenGraphTags($resp);\n\n        $this->assertEquals($book->getShortName() . ' | BookStack', $tags['title']);\n        $this->assertEquals($book->getUrl(), $tags['url']);\n        $this->assertEquals(Str::limit($book->description, 100, '...'), $tags['description']);\n        $this->assertArrayNotHasKey('image', $tags);\n\n        // Test image set if image has cover image\n        $bookRepo = app(BookRepo::class);\n        $bookRepo->updateCoverImage($book, $this->files->uploadedImage('image.png'));\n        $resp = $this->asEditor()->get($book->getUrl());\n        $tags = $this->getOpenGraphTags($resp);\n\n        $this->assertEquals($book->coverInfo()->getUrl(), $tags['image']);\n    }\n\n    public function test_shelf_tags()\n    {\n        $shelf = $this->entities->shelf();\n        $resp = $this->asEditor()->get($shelf->getUrl());\n        $tags = $this->getOpenGraphTags($resp);\n\n        $this->assertEquals($shelf->getShortName() . ' | BookStack', $tags['title']);\n        $this->assertEquals($shelf->getUrl(), $tags['url']);\n        $this->assertEquals(Str::limit($shelf->description, 100, '...'), $tags['description']);\n        $this->assertArrayNotHasKey('image', $tags);\n\n        // Test image set if image has cover image\n        $baseRepo = app(BaseRepo::class);\n        $baseRepo->updateCoverImage($shelf, $this->files->uploadedImage('image.png'));\n        $resp = $this->asEditor()->get($shelf->getUrl());\n        $tags = $this->getOpenGraphTags($resp);\n\n        $this->assertEquals($shelf->coverInfo()->getUrl(), $tags['image']);\n    }\n\n    /**\n     * Parse the open graph tags from a test response.\n     */\n    protected function getOpenGraphTags(TestResponse $resp): array\n    {\n        $tags = [];\n\n        libxml_use_internal_errors(true);\n        $doc = new \\DOMDocument();\n        $doc->loadHTML($resp->getContent());\n        $metaElems = $doc->getElementsByTagName('meta');\n        /** @var \\DOMElement $elem */\n        foreach ($metaElems as $elem) {\n            $prop = $elem->getAttribute('property');\n            $name = explode(':', $prop)[1] ?? null;\n            if ($name) {\n                $tags[$name] = $elem->getAttribute('content');\n            }\n        }\n\n        return $tags;\n    }\n}\n"
  },
  {
    "path": "tests/Meta/OpensearchTest.php",
    "content": "<?php\n\nnamespace Tests\\Meta;\n\nuse Tests\\TestCase;\n\nclass OpensearchTest extends TestCase\n{\n    public function test_opensearch_endpoint()\n    {\n        $appName = 'MyAppNameThatsReallyLongLikeThis';\n        setting()->put('app-name', $appName);\n        $resultUrl = url('/search') . '?term={searchTerms}';\n        $selfUrl = url('/opensearch.xml');\n\n        $resp = $this->get('/opensearch.xml');\n        $resp->assertOk();\n        $resp->assertSee('<?xml version=\"1.0\" encoding=\"UTF-8\"?>' . \"\\n\", false);\n\n        $html = $this->withHtml($resp);\n\n        $html->assertElementExists('OpenSearchDescription > ShortName');\n        $html->assertElementContains('OpenSearchDescription > ShortName', mb_strimwidth($appName, 0, 16));\n        $html->assertElementNotContains('OpenSearchDescription > ShortName', $appName);\n\n        $html->assertElementExists('OpenSearchDescription > Description');\n        $html->assertElementContains('OpenSearchDescription > Description', \"Search {$appName}\");\n        $html->assertElementExists('OpenSearchDescription > Image');\n        $html->assertElementExists('OpenSearchDescription > Url[rel=\"results\"][template=\"' . htmlspecialchars($resultUrl) . '\"]');\n        $html->assertElementExists('OpenSearchDescription > Url[rel=\"self\"][template=\"' . htmlspecialchars($selfUrl) . '\"]');\n    }\n\n    public function test_opensearch_linked_to_from_home()\n    {\n        $appName = setting('app-name');\n        $endpointUrl = url('/opensearch.xml');\n\n        $resp = $this->asViewer()->get('/');\n        $html = $this->withHtml($resp);\n\n        $html->assertElementExists('head > link[rel=\"search\"][type=\"application/opensearchdescription+xml\"][title=\"' . htmlspecialchars($appName) . '\"][href=\"' . htmlspecialchars($endpointUrl) . '\"]');\n    }\n}\n"
  },
  {
    "path": "tests/Meta/PwaManifestTest.php",
    "content": "<?php\n\nnamespace Tests\\Meta;\n\nuse Tests\\TestCase;\n\nclass PwaManifestTest extends TestCase\n{\n    public function test_manifest_access_and_format()\n    {\n        $this->setSettings(['app-color' => '#00ACED']);\n\n        $resp = $this->get('/manifest.json');\n        $resp->assertOk();\n\n        $resp->assertJson([\n            'name' => setting('app-name'),\n            'launch_handler' => [\n                'client_mode' => 'focus-existing'\n            ],\n            'theme_color' => '#00ACED',\n        ]);\n    }\n\n    public function test_pwa_meta_tags_in_head()\n    {\n        $html = $this->asViewer()->withHtml($this->get('/'));\n\n        $html->assertElementExists('head link[rel=\"manifest\"][href$=\"manifest.json\"]');\n        $html->assertElementExists('head meta[name=\"mobile-web-app-capable\"][content=\"yes\"]');\n    }\n\n    public function test_manifest_uses_configured_icons_if_existing()\n    {\n        $this->beforeApplicationDestroyed(fn() => $this->files->resetAppFavicon());\n\n        $resp = $this->get('/manifest.json');\n        $resp->assertJson([\n            'icons' => [[\n                \"src\" => 'http://localhost/icon-32.png',\n                \"sizes\" => \"32x32\",\n                \"type\" => \"image/png\"\n            ]]\n        ]);\n\n        $galleryFile = $this->files->uploadedImage('my-app-icon.png');\n        $this->asAdmin()->call('POST', '/settings/customization', [], [], ['app_icon' => $galleryFile], []);\n\n        $customIconUrl = setting()->get('app-icon-32');\n        $this->assertStringContainsString('my-app-icon', $customIconUrl);\n\n        $resp = $this->get('/manifest.json');\n        $resp->assertJson([\n            'icons' => [[\n                \"src\" => $customIconUrl,\n                \"sizes\" => \"32x32\",\n                \"type\" => \"image/png\"\n            ]]\n        ]);\n    }\n\n    public function test_manifest_changes_to_user_preferences()\n    {\n        $lightUser = $this->users->viewer();\n        $darkUser = $this->users->editor();\n        setting()->putUser($darkUser, 'dark-mode-enabled', 'true');\n\n        $resp = $this->actingAs($lightUser)->get('/manifest.json');\n        $resp->assertJson(['background_color' => '#F2F2F2']);\n\n        $resp = $this->actingAs($darkUser)->get('/manifest.json');\n        $resp->assertJson(['background_color' => '#111111']);\n    }\n}\n"
  },
  {
    "path": "tests/Meta/RobotsTest.php",
    "content": "<?php\n\nnamespace Tests\\Meta;\n\nuse Tests\\TestCase;\n\nclass RobotsTest extends TestCase\n{\n    public function test_robots_effected_by_public_status()\n    {\n        $this->get('/robots.txt')->assertSee(\"User-agent: *\\nDisallow: /\");\n\n        $this->setSettings(['app-public' => 'true']);\n\n        $resp = $this->get('/robots.txt');\n        $resp->assertSee(\"User-agent: *\\nDisallow:\");\n        $resp->assertDontSee('Disallow: /');\n    }\n\n    public function test_robots_effected_by_setting()\n    {\n        $this->get('/robots.txt')->assertSee(\"User-agent: *\\nDisallow: /\");\n\n        config()->set('app.allow_robots', true);\n\n        $resp = $this->get('/robots.txt');\n        $resp->assertSee(\"User-agent: *\\nDisallow:\");\n        $resp->assertDontSee('Disallow: /');\n\n        // Check config overrides app-public setting\n        config()->set('app.allow_robots', false);\n        $this->setSettings(['app-public' => 'true']);\n        $this->get('/robots.txt')->assertSee(\"User-agent: *\\nDisallow: /\");\n    }\n}\n"
  },
  {
    "path": "tests/Permissions/EntityOwnerChangeTest.php",
    "content": "<?php\n\nnamespace Tests\\Permissions;\n\nuse BookStack\\Users\\Models\\User;\nuse Tests\\TestCase;\n\nclass EntityOwnerChangeTest extends TestCase\n{\n    public function test_changing_page_owner()\n    {\n        $page = $this->entities->page();\n        $user = User::query()->where('id', '!=', $page->owned_by)->first();\n\n        $this->asAdmin()->put($page->getUrl('permissions'), ['owned_by' => $user->id]);\n        $this->assertDatabaseHasEntityData('page', ['owned_by' => $user->id, 'id' => $page->id]);\n    }\n\n    public function test_changing_chapter_owner()\n    {\n        $chapter = $this->entities->chapter();\n        $user = User::query()->where('id', '!=', $chapter->owned_by)->first();\n\n        $this->asAdmin()->put($chapter->getUrl('permissions'), ['owned_by' => $user->id]);\n        $this->assertDatabaseHasEntityData('chapter', ['owned_by' => $user->id, 'id' => $chapter->id]);\n    }\n\n    public function test_changing_book_owner()\n    {\n        $book = $this->entities->book();\n        $user = User::query()->where('id', '!=', $book->owned_by)->first();\n\n        $this->asAdmin()->put($book->getUrl('permissions'), ['owned_by' => $user->id]);\n        $this->assertDatabaseHasEntityData('book', ['owned_by' => $user->id, 'id' => $book->id]);\n    }\n\n    public function test_changing_shelf_owner()\n    {\n        $shelf = $this->entities->shelf();\n        $user = User::query()->where('id', '!=', $shelf->owned_by)->first();\n\n        $this->asAdmin()->put($shelf->getUrl('permissions'), ['owned_by' => $user->id]);\n        $this->assertDatabaseHasEntityData('bookshelf', ['owned_by' => $user->id, 'id' => $shelf->id]);\n    }\n}\n"
  },
  {
    "path": "tests/Permissions/EntityPermissionsTest.php",
    "content": "<?php\n\nnamespace Tests\\Permissions;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Models\\User;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Tests\\TestCase;\n\nclass EntityPermissionsTest extends TestCase\n{\n    protected User $user;\n    protected User $viewer;\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n        $this->user = $this->users->editor();\n        $this->viewer = $this->users->viewer();\n    }\n\n    protected function setRestrictionsForTestRoles(Entity $entity, array $actions = []): void\n    {\n        $roles = [\n            $this->user->roles->first(),\n            $this->viewer->roles->first(),\n        ];\n        $this->permissions->setEntityPermissions($entity, $actions, $roles);\n    }\n\n    public function test_bookshelf_view_restriction()\n    {\n        $shelf = $this->entities->shelf();\n\n        $this->actingAs($this->user)\n            ->get($shelf->getUrl())\n            ->assertStatus(200);\n\n        $this->setRestrictionsForTestRoles($shelf, []);\n\n        $this->followingRedirects()->get($shelf->getUrl())\n            ->assertSee('Shelf not found');\n\n        $this->setRestrictionsForTestRoles($shelf, ['view']);\n\n        $this->get($shelf->getUrl())\n            ->assertSee($shelf->name);\n    }\n\n    public function test_bookshelf_update_restriction()\n    {\n        $shelf = $this->entities->shelf();\n\n        $this->actingAs($this->user)\n            ->get($shelf->getUrl('/edit'))\n            ->assertSee('Edit Shelf');\n\n        $this->setRestrictionsForTestRoles($shelf, ['view', 'delete']);\n\n        $resp = $this->get($shelf->getUrl('/edit'))\n            ->assertRedirect('/');\n        $this->followRedirects($resp)->assertSee('You do not have permission');\n\n        $this->setRestrictionsForTestRoles($shelf, ['view', 'update']);\n\n        $this->get($shelf->getUrl('/edit'))\n            ->assertOk();\n    }\n\n    public function test_bookshelf_delete_restriction()\n    {\n        $shelf = $this->entities->shelf();\n\n        $this->actingAs($this->user)\n            ->get($shelf->getUrl('/delete'))\n            ->assertSee('Delete Shelf');\n\n        $this->setRestrictionsForTestRoles($shelf, ['view', 'update']);\n\n        $this->get($shelf->getUrl('/delete'))->assertRedirect('/');\n        $this->get('/')->assertSee('You do not have permission');\n\n        $this->setRestrictionsForTestRoles($shelf, ['view', 'delete']);\n\n        $this->get($shelf->getUrl('/delete'))\n            ->assertOk()\n            ->assertSee('Delete Shelf');\n    }\n\n    public function test_book_view_restriction()\n    {\n        $book = $this->entities->book();\n        $bookPage = $book->pages->first();\n        $bookChapter = $book->chapters->first();\n\n        $bookUrl = $book->getUrl();\n        $this->actingAs($this->user)\n            ->get($bookUrl)\n            ->assertOk();\n\n        $this->setRestrictionsForTestRoles($book, []);\n\n        $this->followingRedirects()->get($bookUrl)\n            ->assertSee('Book not found');\n        $this->followingRedirects()->get($bookPage->getUrl())\n            ->assertSee('Page not found');\n        $this->followingRedirects()->get($bookChapter->getUrl())\n            ->assertSee('Chapter not found');\n\n        $this->setRestrictionsForTestRoles($book, ['view']);\n\n        $this->get($bookUrl)\n            ->assertSee($book->name);\n        $this->get($bookPage->getUrl())\n            ->assertSee($bookPage->name);\n        $this->get($bookChapter->getUrl())\n            ->assertSee($bookChapter->name);\n    }\n\n    public function test_book_create_restriction()\n    {\n        $book = $this->entities->book();\n\n        $bookUrl = $book->getUrl();\n        $resp = $this->actingAs($this->viewer)->get($bookUrl);\n        $this->withHtml($resp)->assertElementNotContains('.actions', 'New Page')\n            ->assertElementNotContains('.actions', 'New Chapter');\n        $resp = $this->actingAs($this->user)->get($bookUrl);\n        $this->withHtml($resp)->assertElementContains('.actions', 'New Page')\n            ->assertElementContains('.actions', 'New Chapter');\n\n        $this->setRestrictionsForTestRoles($book, ['view', 'delete', 'update']);\n\n        $this->get($bookUrl . '/create-chapter')->assertRedirect('/');\n        $this->get('/')->assertSee('You do not have permission');\n\n        $this->get($bookUrl . '/create-page')->assertRedirect('/');\n        $this->get('/')->assertSee('You do not have permission');\n\n        $resp = $this->get($bookUrl);\n        $this->withHtml($resp)->assertElementNotContains('.actions', 'New Page')\n            ->assertElementNotContains('.actions', 'New Chapter');\n\n        $this->setRestrictionsForTestRoles($book, ['view', 'create']);\n\n        $resp = $this->post($book->getUrl('/create-chapter'), [\n            'name'        => 'test chapter',\n            'description' => 'desc',\n        ]);\n        $resp->assertRedirect($book->getUrl('/chapter/test-chapter'));\n\n        $this->get($book->getUrl('/create-page'));\n        /** @var Page $page */\n        $page = Page::query()->where('draft', '=', true)->orderBy('id', 'desc')->first();\n        $resp = $this->post($page->getUrl(), [\n            'name' => 'test page',\n            'html' => 'test content',\n        ]);\n        $resp->assertRedirect($book->getUrl('/page/test-page'));\n\n        $resp = $this->get($bookUrl);\n        $this->withHtml($resp)->assertElementContains('.actions', 'New Page')\n            ->assertElementContains('.actions', 'New Chapter');\n    }\n\n    public function test_book_update_restriction()\n    {\n        $book = $this->entities->book();\n        $bookPage = $book->pages->first();\n        $bookChapter = $book->chapters->first();\n\n        $bookUrl = $book->getUrl();\n        $this->actingAs($this->user)\n            ->get($bookUrl . '/edit')\n            ->assertSee('Edit Book');\n\n        $this->setRestrictionsForTestRoles($book, ['view', 'delete']);\n\n        $this->get($bookUrl . '/edit')->assertRedirect('/');\n        $this->get('/')->assertSee('You do not have permission');\n        $this->get($bookPage->getUrl() . '/edit')->assertRedirect($bookPage->getUrl());\n        $this->get('/')->assertSee('You do not have permission');\n        $this->get($bookChapter->getUrl() . '/edit')->assertRedirect('/');\n        $this->get('/')->assertSee('You do not have permission');\n\n        $this->setRestrictionsForTestRoles($book, ['view', 'update']);\n\n        $this->get($bookUrl . '/edit')->assertOk();\n        $this->get($bookPage->getUrl() . '/edit')->assertOk();\n        $this->get($bookChapter->getUrl() . '/edit')->assertSee('Edit Chapter');\n    }\n\n    public function test_book_delete_restriction()\n    {\n        $book = $this->entities->book();\n        $bookPage = $book->pages->first();\n        $bookChapter = $book->chapters->first();\n\n        $bookUrl = $book->getUrl();\n        $this->actingAs($this->user)->get($bookUrl . '/delete')\n            ->assertSee('Delete Book');\n\n        $this->setRestrictionsForTestRoles($book, ['view', 'update']);\n\n        $this->get($bookUrl . '/delete')->assertRedirect('/');\n        $this->get('/')->assertSee('You do not have permission');\n        $this->get($bookPage->getUrl() . '/delete')->assertRedirect('/');\n        $this->get('/')->assertSee('You do not have permission');\n        $this->get($bookChapter->getUrl() . '/delete')->assertRedirect('/');\n        $this->get('/')->assertSee('You do not have permission');\n\n        $this->setRestrictionsForTestRoles($book, ['view', 'delete']);\n\n        $this->get($bookUrl . '/delete')->assertOk()->assertSee('Delete Book');\n        $this->get($bookPage->getUrl('/delete'))->assertOk()->assertSee('Delete Page');\n        $this->get($bookChapter->getUrl('/delete'))->assertSee('Delete Chapter');\n    }\n\n    public function test_chapter_view_restriction()\n    {\n        $chapter = $this->entities->chapter();\n        $chapterPage = $chapter->pages->first();\n\n        $chapterUrl = $chapter->getUrl();\n        $this->actingAs($this->user)->get($chapterUrl)->assertOk();\n\n        $this->setRestrictionsForTestRoles($chapter, []);\n\n        $this->followingRedirects()->get($chapterUrl)->assertSee('Chapter not found');\n        $this->followingRedirects()->get($chapterPage->getUrl())->assertSee('Page not found');\n\n        $this->setRestrictionsForTestRoles($chapter, ['view']);\n\n        $this->get($chapterUrl)->assertSee($chapter->name);\n        $this->get($chapterPage->getUrl())->assertSee($chapterPage->name);\n    }\n\n    public function test_chapter_create_restriction()\n    {\n        $chapter = $this->entities->chapter();\n\n        $chapterUrl = $chapter->getUrl();\n        $resp = $this->actingAs($this->user)->get($chapterUrl);\n        $this->withHtml($resp)->assertElementContains('.actions', 'New Page');\n\n        $this->setRestrictionsForTestRoles($chapter, ['view', 'delete', 'update']);\n\n        $this->get($chapterUrl . '/create-page')->assertRedirect('/');\n        $this->get('/')->assertSee('You do not have permission');\n        $this->withHtml($this->get($chapterUrl))->assertElementNotContains('.actions', 'New Page');\n\n        $this->setRestrictionsForTestRoles($chapter, ['view', 'create']);\n\n        $this->get($chapter->getUrl('/create-page'));\n        /** @var Page $page */\n        $page = Page::query()->where('draft', '=', true)->orderBy('id', 'desc')->first();\n        $resp = $this->post($page->getUrl(), [\n            'name' => 'test page',\n            'html' => 'test content',\n        ]);\n        $resp->assertRedirect($chapter->book->getUrl('/page/test-page'));\n\n        $this->withHtml($this->get($chapterUrl))->assertElementContains('.actions', 'New Page');\n    }\n\n    public function test_chapter_update_restriction()\n    {\n        $chapter = $this->entities->chapter();\n        $chapterPage = $chapter->pages->first();\n\n        $chapterUrl = $chapter->getUrl();\n        $this->actingAs($this->user)->get($chapterUrl . '/edit')\n            ->assertSee('Edit Chapter');\n\n        $this->setRestrictionsForTestRoles($chapter, ['view', 'delete']);\n\n        $this->get($chapterUrl . '/edit')->assertRedirect('/');\n        $this->get('/')->assertSee('You do not have permission');\n        $this->get($chapterPage->getUrl() . '/edit')->assertRedirect($chapterPage->getUrl());\n        $this->get('/')->assertSee('You do not have permission');\n\n        $this->setRestrictionsForTestRoles($chapter, ['view', 'update']);\n\n        $this->get($chapterUrl . '/edit')->assertOk()->assertSee('Edit Chapter');\n        $this->get($chapterPage->getUrl() . '/edit')->assertOk();\n    }\n\n    public function test_chapter_delete_restriction()\n    {\n        $chapter = $this->entities->chapter();\n        $chapterPage = $chapter->pages->first();\n\n        $chapterUrl = $chapter->getUrl();\n        $this->actingAs($this->user)\n            ->get($chapterUrl . '/delete')\n            ->assertSee('Delete Chapter');\n\n        $this->setRestrictionsForTestRoles($chapter, ['view', 'update']);\n\n        $this->get($chapterUrl . '/delete')->assertRedirect('/');\n        $this->get('/')->assertSee('You do not have permission');\n        $this->get($chapterPage->getUrl() . '/delete')->assertRedirect('/');\n        $this->get('/')->assertSee('You do not have permission');\n\n        $this->setRestrictionsForTestRoles($chapter, ['view', 'delete']);\n\n        $this->get($chapterUrl . '/delete')->assertOk()->assertSee('Delete Chapter');\n        $this->get($chapterPage->getUrl() . '/delete')->assertOk()->assertSee('Delete Page');\n    }\n\n    public function test_page_view_restriction()\n    {\n        $page = $this->entities->page();\n\n        $pageUrl = $page->getUrl();\n        $this->actingAs($this->user)->get($pageUrl)->assertOk();\n\n        $this->setRestrictionsForTestRoles($page, ['update', 'delete']);\n\n        $this->get($pageUrl)->assertSee('Page not found');\n\n        $this->setRestrictionsForTestRoles($page, ['view']);\n\n        $this->get($pageUrl)->assertSee($page->name);\n    }\n\n    public function test_page_update_restriction()\n    {\n        $page = $this->entities->page();\n\n        $pageUrl = $page->getUrl();\n        $resp = $this->actingAs($this->user)\n            ->get($pageUrl . '/edit');\n        $this->withHtml($resp)->assertElementExists('input[name=\"name\"][value=\"' . $page->name . '\"]');\n\n        $this->setRestrictionsForTestRoles($page, ['view', 'delete']);\n\n        $this->get($pageUrl . '/edit')->assertRedirect($pageUrl);\n        $this->get('/')->assertSee('You do not have permission');\n\n        $this->setRestrictionsForTestRoles($page, ['view', 'update']);\n\n        $resp = $this->get($pageUrl . '/edit')\n            ->assertOk();\n        $this->withHtml($resp)->assertElementExists('input[name=\"name\"][value=\"' . $page->name . '\"]');\n    }\n\n    public function test_page_delete_restriction()\n    {\n        $page = $this->entities->page();\n\n        $pageUrl = $page->getUrl();\n        $this->actingAs($this->user)\n            ->get($pageUrl . '/delete')\n            ->assertSee('Delete Page');\n\n        $this->setRestrictionsForTestRoles($page, ['view', 'update']);\n\n        $this->get($pageUrl . '/delete')->assertRedirect('/');\n        $this->get('/')->assertSee('You do not have permission');\n\n        $this->setRestrictionsForTestRoles($page, ['view', 'delete']);\n\n        $this->get($pageUrl . '/delete')->assertOk()->assertSee('Delete Page');\n    }\n\n    protected function entityRestrictionFormTest(string $model, string $title, string $permission, string $roleId)\n    {\n        /** @var Entity $modelInstance */\n        $modelInstance = $model::query()->first();\n        $this->asAdmin()->get($modelInstance->getUrl('/permissions'))\n            ->assertSee($title);\n\n        $this->put($modelInstance->getUrl('/permissions'), [\n            'permissions' => [\n                $roleId => [\n                    $permission => 'true',\n                ],\n            ],\n        ]);\n\n        $this->assertDatabaseHas('entity_permissions', [\n            'entity_id'      => $modelInstance->id,\n            'entity_type'    => $modelInstance->getMorphClass(),\n            'role_id'        => $roleId,\n            $permission => true,\n        ]);\n    }\n\n    public function test_bookshelf_restriction_form()\n    {\n        $this->entityRestrictionFormTest(Bookshelf::class, 'Shelf Permissions', 'view', '2');\n    }\n\n    public function test_book_restriction_form()\n    {\n        $this->entityRestrictionFormTest(Book::class, 'Book Permissions', 'view', '2');\n    }\n\n    public function test_chapter_restriction_form()\n    {\n        $this->entityRestrictionFormTest(Chapter::class, 'Chapter Permissions', 'update', '2');\n    }\n\n    public function test_page_restriction_form()\n    {\n        $this->entityRestrictionFormTest(Page::class, 'Page Permissions', 'delete', '2');\n    }\n\n    public function test_shelf_create_permission_visible_with_notice()\n    {\n        $shelf = $this->entities->shelf();\n\n        $resp = $this->asAdmin()->get($shelf->getUrl('/permissions'));\n        $html = $this->withHtml($resp);\n        $html->assertElementExists('input[name$=\"[create]\"]');\n        $resp->assertSee('Shelf create permissions are only used for copying permissions to child books using the action below.');\n    }\n\n    public function test_restricted_pages_not_visible_in_book_navigation_on_pages()\n    {\n        $chapter = $this->entities->chapter();\n        $page = $chapter->pages->first();\n        $page2 = $chapter->pages[2];\n\n        $this->setRestrictionsForTestRoles($page, []);\n\n        $resp = $this->actingAs($this->user)->get($page2->getUrl());\n        $this->withHtml($resp)->assertElementNotContains('.sidebar-page-list', $page->name);\n    }\n\n    public function test_restricted_pages_not_visible_in_book_navigation_on_chapters()\n    {\n        $chapter = $this->entities->chapter();\n        $page = $chapter->pages->first();\n\n        $this->setRestrictionsForTestRoles($page, []);\n\n        $resp = $this->actingAs($this->user)->get($chapter->getUrl());\n        $this->withHtml($resp)->assertElementNotContains('.sidebar-page-list', $page->name);\n    }\n\n    public function test_restricted_pages_not_visible_on_chapter_pages()\n    {\n        $chapter = $this->entities->chapter();\n        $page = $chapter->pages->first();\n\n        $this->setRestrictionsForTestRoles($page, []);\n\n        $this->actingAs($this->user)\n            ->get($chapter->getUrl())\n            ->assertDontSee($page->name);\n    }\n\n    public function test_restricted_chapter_pages_not_visible_on_book_page()\n    {\n        $chapter = $this->entities->chapter();\n        $this->actingAs($this->user)\n            ->get($chapter->book->getUrl())\n            ->assertSee($chapter->pages->first()->name);\n\n        foreach ($chapter->pages as $page) {\n            $this->setRestrictionsForTestRoles($page, []);\n        }\n\n        $this->actingAs($this->user)\n            ->get($chapter->book->getUrl())\n            ->assertDontSee($chapter->pages->first()->name);\n    }\n\n    public function test_bookshelf_update_restriction_override()\n    {\n        $shelf = $this->entities->shelf();\n\n        $this->actingAs($this->viewer)\n            ->get($shelf->getUrl('/edit'))\n            ->assertDontSee('Edit Book');\n\n        $this->setRestrictionsForTestRoles($shelf, ['view', 'delete']);\n\n        $this->get($shelf->getUrl('/edit'))->assertRedirect('/');\n        $this->get('/')->assertSee('You do not have permission');\n\n        $this->setRestrictionsForTestRoles($shelf, ['view', 'update']);\n\n        $this->get($shelf->getUrl('/edit'))->assertOk();\n    }\n\n    public function test_bookshelf_delete_restriction_override()\n    {\n        $shelf = $this->entities->shelf();\n\n        $this->actingAs($this->viewer)\n            ->get($shelf->getUrl('/delete'))\n            ->assertDontSee('Delete Book');\n\n        $this->setRestrictionsForTestRoles($shelf, ['view', 'update']);\n\n        $this->get($shelf->getUrl('/delete'))->assertRedirect('/');\n        $this->get('/')->assertSee('You do not have permission');\n\n        $this->setRestrictionsForTestRoles($shelf, ['view', 'delete']);\n\n        $this->get($shelf->getUrl('/delete'))->assertOk()->assertSee('Delete Shelf');\n    }\n\n    public function test_book_create_restriction_override()\n    {\n        $book = $this->entities->book();\n\n        $bookUrl = $book->getUrl();\n        $resp = $this->actingAs($this->viewer)->get($bookUrl);\n        $this->withHtml($resp)->assertElementNotContains('.actions', 'New Page')\n            ->assertElementNotContains('.actions', 'New Chapter');\n\n        $this->setRestrictionsForTestRoles($book, ['view', 'delete', 'update']);\n\n        $this->get($bookUrl . '/create-chapter')->assertRedirect('/');\n        $this->get('/')->assertSee('You do not have permission');\n        $this->get($bookUrl . '/create-page')->assertRedirect('/');\n        $this->get('/')->assertSee('You do not have permission');\n        $resp = $this->get($bookUrl);\n        $this->withHtml($resp)->assertElementNotContains('.actions', 'New Page')\n            ->assertElementNotContains('.actions', 'New Chapter');\n\n        $this->setRestrictionsForTestRoles($book, ['view', 'create']);\n\n        $resp = $this->post($book->getUrl('/create-chapter'), [\n            'name'        => 'test chapter',\n            'description' => 'test desc',\n        ]);\n        $resp->assertRedirect($book->getUrl('/chapter/test-chapter'));\n\n        $this->get($book->getUrl('/create-page'));\n        /** @var Page $page */\n        $page = Page::query()->where('draft', '=', true)->orderByDesc('id')->first();\n        $resp = $this->post($page->getUrl(), [\n            'name' => 'test page',\n            'html' => 'test desc',\n        ]);\n        $resp->assertRedirect($book->getUrl('/page/test-page'));\n\n        $resp = $this->get($bookUrl);\n        $this->withHtml($resp)->assertElementContains('.actions', 'New Page')\n            ->assertElementContains('.actions', 'New Chapter');\n    }\n\n    public function test_book_update_restriction_override()\n    {\n        $book = $this->entities->book();\n        $bookPage = $book->pages->first();\n        $bookChapter = $book->chapters->first();\n\n        $bookUrl = $book->getUrl();\n        $this->actingAs($this->viewer)->get($bookUrl . '/edit')\n            ->assertDontSee('Edit Book');\n\n        $this->setRestrictionsForTestRoles($book, ['view', 'delete']);\n\n        $this->get($bookUrl . '/edit')->assertRedirect('/');\n        $this->get('/')->assertSee('You do not have permission');\n        $this->get($bookPage->getUrl() . '/edit')->assertRedirect($bookPage->getUrl());\n        $this->get('/')->assertSee('You do not have permission');\n        $this->get($bookChapter->getUrl() . '/edit')->assertRedirect('/');\n        $this->get('/')->assertSee('You do not have permission');\n\n        $this->setRestrictionsForTestRoles($book, ['view', 'update']);\n\n        $this->get($bookUrl . '/edit')->assertOk();\n        $this->get($bookPage->getUrl() . '/edit')->assertOk();\n        $this->get($bookChapter->getUrl() . '/edit')->assertSee('Edit Chapter');\n    }\n\n    public function test_book_delete_restriction_override()\n    {\n        $book = $this->entities->book();\n        $bookPage = $book->pages->first();\n        $bookChapter = $book->chapters->first();\n\n        $bookUrl = $book->getUrl();\n        $this->actingAs($this->viewer)\n            ->get($bookUrl . '/delete')\n            ->assertDontSee('Delete Book');\n\n        $this->setRestrictionsForTestRoles($book, ['view', 'update']);\n\n        $this->get($bookUrl . '/delete')->assertRedirect('/');\n        $this->get('/')->assertSee('You do not have permission');\n        $this->get($bookPage->getUrl() . '/delete')->assertRedirect('/');\n        $this->get('/')->assertSee('You do not have permission');\n        $this->get($bookChapter->getUrl() . '/delete')->assertRedirect('/');\n        $this->get('/')->assertSee('You do not have permission');\n\n        $this->setRestrictionsForTestRoles($book, ['view', 'delete']);\n\n        $this->get($bookUrl . '/delete')->assertOk()->assertSee('Delete Book');\n        $this->get($bookPage->getUrl() . '/delete')->assertOk()->assertSee('Delete Page');\n        $this->get($bookChapter->getUrl() . '/delete')->assertSee('Delete Chapter');\n    }\n\n    public function test_page_visible_if_has_permissions_when_book_not_visible()\n    {\n        $book = $this->entities->book();\n        $bookChapter = $book->chapters->first();\n        $bookPage = $bookChapter->pages->first();\n\n        foreach ([$book, $bookChapter, $bookPage] as $entity) {\n            $entity->name = Str::random(24);\n            $entity->save();\n        }\n\n        $this->setRestrictionsForTestRoles($book, []);\n        $this->setRestrictionsForTestRoles($bookPage, ['view']);\n\n        $this->actingAs($this->viewer);\n        $resp = $this->get($bookPage->getUrl());\n        $resp->assertOk();\n        $resp->assertSee($bookPage->name);\n        $resp->assertDontSee(substr($book->name, 0, 15));\n        $resp->assertDontSee(substr($bookChapter->name, 0, 15));\n    }\n\n    public function test_book_sort_view_permission()\n    {\n        $firstBook = $this->entities->book();\n        $secondBook = $this->entities->book();\n\n        $this->setRestrictionsForTestRoles($firstBook, ['view', 'update']);\n        $this->setRestrictionsForTestRoles($secondBook, ['view']);\n\n        // Test sort page visibility\n        $this->actingAs($this->user)->get($secondBook->getUrl('/sort'))->assertRedirect('/');\n        $this->get('/')->assertSee('You do not have permission');\n\n        // Check sort page on first book\n        $this->actingAs($this->user)->get($firstBook->getUrl('/sort'));\n    }\n\n    public function test_can_create_page_if_chapter_has_permissions_when_book_not_visible()\n    {\n        $book = $this->entities->book();\n        $this->setRestrictionsForTestRoles($book, []);\n        $bookChapter = $book->chapters->first();\n        $this->setRestrictionsForTestRoles($bookChapter, ['view']);\n\n        $this->actingAs($this->user)->get($bookChapter->getUrl())\n            ->assertDontSee('New Page');\n\n        $this->setRestrictionsForTestRoles($bookChapter, ['view', 'create']);\n\n        $this->get($bookChapter->getUrl('/create-page'));\n        /** @var Page $page */\n        $page = Page::query()->where('draft', '=', true)->orderByDesc('id')->first();\n        $resp = $this->post($page->getUrl(), [\n            'name' => 'test page',\n            'html' => 'test content',\n        ]);\n        $resp->assertRedirect($book->getUrl('/page/test-page'));\n    }\n\n    public function test_access_to_item_prevented_if_inheritance_active_but_permission_prevented_via_role()\n    {\n        $user = $this->users->viewer();\n        $viewerRole = $user->roles->first();\n        $chapter = $this->entities->chapter();\n        $book = $chapter->book;\n\n        $this->permissions->setEntityPermissions($book, ['update'], [$viewerRole], false);\n        $this->permissions->setEntityPermissions($chapter, [], [$viewerRole], true);\n\n        $this->assertFalse(userCan(Permission::ChapterUpdate, $chapter));\n    }\n\n    public function test_access_to_item_allowed_if_inheritance_active_and_permission_prevented_via_role_but_allowed_via_parent()\n    {\n        $user = $this->users->viewer();\n        $viewerRole = $user->roles->first();\n        $editorRole = Role::getRole('Editor');\n        $user->attachRole($editorRole);\n        $chapter = $this->entities->chapter();\n        $book = $chapter->book;\n\n        $this->permissions->setEntityPermissions($book, ['update'], [$editorRole], false);\n        $this->permissions->setEntityPermissions($chapter, [], [$viewerRole], true);\n\n        $this->actingAs($user);\n        $this->assertTrue(userCan(Permission::ChapterUpdate, $chapter));\n    }\n\n    public function test_book_permissions_can_be_generated_without_error_if_child_chapter_is_in_recycle_bin()\n    {\n        $book = $this->entities->bookHasChaptersAndPages();\n        /** @var Chapter $chapter */\n        $chapter = $book->chapters()->first();\n\n        $this->asAdmin()->delete($chapter->getUrl());\n\n        $error = null;\n        try {\n            $this->permissions->setEntityPermissions($book, ['view'], []);\n        } catch (Exception $e) {\n            $error = $e;\n        }\n\n        $this->assertNull($error);\n    }\n}\n"
  },
  {
    "path": "tests/Permissions/ExportPermissionsTest.php",
    "content": "<?php\n\nnamespace Tests\\Permissions;\n\nuse Illuminate\\Support\\Str;\nuse Tests\\TestCase;\n\nclass ExportPermissionsTest extends TestCase\n{\n    public function test_page_content_without_view_access_hidden_on_chapter_export()\n    {\n        $chapter = $this->entities->chapter();\n        $page = $chapter->pages()->firstOrFail();\n        $pageContent = Str::random(48);\n        $page->html = '<p>' . $pageContent . '</p>';\n        $page->save();\n        $viewer = $this->users->viewer();\n        $this->actingAs($viewer);\n        $formats = ['html', 'plaintext'];\n\n        foreach ($formats as $format) {\n            $resp = $this->get($chapter->getUrl(\"export/{$format}\"));\n            $resp->assertStatus(200);\n            $resp->assertSee($page->name);\n            $resp->assertSee($pageContent);\n        }\n\n        $this->permissions->setEntityPermissions($page, []);\n\n        foreach ($formats as $format) {\n            $resp = $this->get($chapter->getUrl(\"export/{$format}\"));\n            $resp->assertStatus(200);\n            $resp->assertDontSee($page->name);\n            $resp->assertDontSee($pageContent);\n        }\n    }\n\n    public function test_page_content_without_view_access_hidden_on_book_export()\n    {\n        $book = $this->entities->book();\n        $page = $book->pages()->firstOrFail();\n        $pageContent = Str::random(48);\n        $page->html = '<p>' . $pageContent . '</p>';\n        $page->save();\n        $viewer = $this->users->viewer();\n        $this->actingAs($viewer);\n        $formats = ['html', 'plaintext'];\n\n        foreach ($formats as $format) {\n            $resp = $this->get($book->getUrl(\"export/{$format}\"));\n            $resp->assertStatus(200);\n            $resp->assertSee($page->name);\n            $resp->assertSee($pageContent);\n        }\n\n        $this->permissions->setEntityPermissions($page, []);\n\n        foreach ($formats as $format) {\n            $resp = $this->get($book->getUrl(\"export/{$format}\"));\n            $resp->assertStatus(200);\n            $resp->assertDontSee($page->name);\n            $resp->assertDontSee($pageContent);\n        }\n    }\n}\n"
  },
  {
    "path": "tests/Permissions/RolePermissionsTest.php",
    "content": "<?php\n\nnamespace Tests\\Permissions;\n\nuse BookStack\\Activity\\Models\\Comment;\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Uploads\\Image;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Testing\\TestResponse;\nuse Tests\\TestCase;\n\nclass RolePermissionsTest extends TestCase\n{\n    protected User $user;\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n        $this->user = $this->users->viewer();\n    }\n\n    public function test_manage_user_permission()\n    {\n        $this->actingAs($this->user)->get('/settings/users')->assertRedirect('/');\n        $this->permissions->grantUserRolePermissions($this->user, ['users-manage']);\n        $this->actingAs($this->user)->get('/settings/users')->assertOk();\n    }\n\n    public function test_manage_users_permission_shows_link_in_header_if_does_not_have_settings_manage_permision()\n    {\n        $usersLink = 'href=\"' . url('/settings/users') . '\"';\n        $this->actingAs($this->user)->get('/')->assertDontSee($usersLink, false);\n        $this->permissions->grantUserRolePermissions($this->user, ['users-manage']);\n        $this->actingAs($this->user)->get('/')->assertSee($usersLink, false);\n        $this->permissions->grantUserRolePermissions($this->user, ['settings-manage', 'users-manage']);\n        $this->actingAs($this->user)->get('/')->assertDontSee($usersLink, false);\n    }\n\n    public function test_user_cannot_change_email_unless_they_have_manage_users_permission()\n    {\n        $originalEmail = $this->user->email;\n        $this->actingAs($this->user);\n\n        $resp = $this->get('/my-account/profile')->assertOk();\n        $this->withHtml($resp)->assertElementExists('input[name=email][disabled]');\n        $resp->assertSee('Unfortunately you don\\'t have permission to change your email address.');\n        $this->put('/my-account/profile', [\n            'name'  => 'my_new_name',\n            'email' => 'new_email@example.com',\n        ]);\n        $this->assertDatabaseHas('users', [\n            'id'    => $this->user->id,\n            'email' => $originalEmail,\n            'name'  => 'my_new_name',\n        ]);\n\n        $this->permissions->grantUserRolePermissions($this->user, ['users-manage']);\n\n        $resp = $this->get('/my-account/profile')->assertOk();\n        $this->withHtml($resp)\n            ->assertElementNotExists('input[name=email][disabled]')\n            ->assertElementExists('input[name=email]');\n\n        $this->put('/my-account/profile', [\n            'name'  => 'my_new_name_2',\n            'email' => 'new_email@example.com',\n        ]);\n\n        $this->assertDatabaseHas('users', [\n            'id'    => $this->user->id,\n            'email' => 'new_email@example.com',\n            'name'  => 'my_new_name_2',\n        ]);\n    }\n\n    public function test_user_roles_manage_permission()\n    {\n        $this->actingAs($this->user)->get('/settings/roles')->assertRedirect('/');\n        $this->get('/settings/roles/1')->assertRedirect('/');\n        $this->permissions->grantUserRolePermissions($this->user, ['user-roles-manage']);\n        $this->actingAs($this->user)->get('/settings/roles')->assertOk();\n        $this->get('/settings/roles/1')\n            ->assertOk()\n            ->assertSee('Admin');\n    }\n\n    public function test_settings_manage_permission()\n    {\n        $this->actingAs($this->user)->get('/settings/features')->assertRedirect('/');\n        $this->permissions->grantUserRolePermissions($this->user, ['settings-manage']);\n        $this->get('/settings/features')->assertOk();\n\n        $resp = $this->post('/settings/features', []);\n        $resp->assertRedirect('/settings/features');\n        $resp = $this->get('/settings/features');\n        $resp->assertSee('Settings successfully updated');\n    }\n\n    public function test_restrictions_manage_all_permission()\n    {\n        $page = $this->entities->page();\n\n        $this->actingAs($this->user)->get($page->getUrl())->assertDontSee('Permissions');\n        $this->get($page->getUrl('/permissions'))->assertRedirect('/');\n\n        $this->permissions->grantUserRolePermissions($this->user, ['restrictions-manage-all']);\n\n        $this->actingAs($this->user)->get($page->getUrl())->assertSee('Permissions');\n\n        $this->get($page->getUrl('/permissions'))\n            ->assertOk()\n            ->assertSee('Page Permissions');\n    }\n\n    public function test_restrictions_manage_own_permission()\n    {\n        $otherUsersPage = $this->entities->page();\n        $content = $this->entities->createChainBelongingToUser($this->user);\n\n        // Set a different creator on the page we're checking to ensure\n        // that the owner fields are checked\n        $page = $content['page']; /** @var Page $page */\n        $page->created_by = $otherUsersPage->id;\n        $page->owned_by = $this->user->id;\n        $page->save();\n\n        // Check can't restrict other's content\n        $this->actingAs($this->user)->get($otherUsersPage->getUrl())->assertDontSee('Permissions');\n        $this->get($otherUsersPage->getUrl('/permissions'))->assertRedirect('/');\n\n        // Check can't restrict own content\n        $this->actingAs($this->user)->get($page->getUrl())->assertDontSee('Permissions');\n        $this->get($page->getUrl('/permissions'))->assertRedirect('/');\n\n        $this->permissions->grantUserRolePermissions($this->user, ['restrictions-manage-own']);\n\n        // Check can't restrict other's content\n        $this->actingAs($this->user)->get($otherUsersPage->getUrl())->assertDontSee('Permissions');\n        $this->get($otherUsersPage->getUrl('/permissions'))->assertRedirect();\n\n        // Check can restrict own content\n        $this->actingAs($this->user)->get($page->getUrl())->assertSee('Permissions');\n        $this->get($page->getUrl('/permissions'))->assertOk();\n    }\n\n    /**\n     * Check a standard entity access permission.\n     */\n    private function checkAccessPermission(\n        string $permission,\n        array $accessUrls = [],\n        array $visibles = [],\n        string $expectedRedirectUri = '/',\n    ) {\n        foreach ($accessUrls as $url) {\n            $this->actingAs($this->user)->get($url)->assertRedirect($expectedRedirectUri);\n        }\n\n        foreach ($visibles as $url => $text) {\n            $resp = $this->actingAs($this->user)->get($url);\n            $this->withHtml($resp)->assertElementNotContains('.action-buttons', $text);\n        }\n\n        $this->permissions->grantUserRolePermissions($this->user, [$permission]);\n\n        foreach ($accessUrls as $url) {\n            $this->actingAs($this->user)->get($url)->assertOk();\n        }\n        foreach ($visibles as $url => $text) {\n            $this->actingAs($this->user)->get($url)->assertSee($text);\n        }\n    }\n\n    public function test_bookshelves_create_all_permissions()\n    {\n        $this->checkAccessPermission('bookshelf-create-all', [\n            '/create-shelf',\n        ], [\n            '/shelves' => 'New Shelf',\n        ]);\n\n        $this->post('/shelves', [\n            'name'        => 'test shelf',\n            'description' => 'shelf desc',\n        ])->assertRedirect('/shelves/test-shelf');\n    }\n\n    public function test_bookshelves_edit_own_permission()\n    {\n        /** @var Bookshelf $otherShelf */\n        $otherShelf = Bookshelf::query()->first();\n        $ownShelf = $this->entities->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']);\n        $ownShelf->forceFill(['owned_by' => $this->user->id, 'updated_by' => $this->user->id])->save();\n        $this->permissions->regenerateForEntity($ownShelf);\n\n        $this->checkAccessPermission('bookshelf-update-own', [\n            $ownShelf->getUrl('/edit'),\n        ], [\n            $ownShelf->getUrl() => 'Edit',\n        ]);\n\n        $resp = $this->get($otherShelf->getUrl());\n        $this->withHtml($resp)->assertElementNotContains('.action-buttons', 'Edit');\n        $this->get($otherShelf->getUrl('/edit'))->assertRedirect('/');\n    }\n\n    public function test_bookshelves_edit_all_permission()\n    {\n        /** @var Bookshelf $otherShelf */\n        $otherShelf = Bookshelf::query()->first();\n        $this->checkAccessPermission('bookshelf-update-all', [\n            $otherShelf->getUrl('/edit'),\n        ], [\n            $otherShelf->getUrl() => 'Edit',\n        ]);\n    }\n\n    public function test_bookshelves_delete_own_permission()\n    {\n        $this->permissions->grantUserRolePermissions($this->user, ['bookshelf-update-all']);\n        /** @var Bookshelf $otherShelf */\n        $otherShelf = Bookshelf::query()->first();\n        $ownShelf = $this->entities->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']);\n        $ownShelf->forceFill(['owned_by' => $this->user->id, 'updated_by' => $this->user->id])->save();\n        $this->permissions->regenerateForEntity($ownShelf);\n\n        $this->checkAccessPermission('bookshelf-delete-own', [\n            $ownShelf->getUrl('/delete'),\n        ], [\n            $ownShelf->getUrl() => 'Delete',\n        ]);\n\n        $resp = $this->get($otherShelf->getUrl());\n        $this->withHtml($resp)->assertElementNotContains('.action-buttons', 'Delete');\n        $this->get($otherShelf->getUrl('/delete'))->assertRedirect('/');\n\n        $this->get($ownShelf->getUrl());\n        $this->delete($ownShelf->getUrl())->assertRedirect('/shelves');\n        $this->get('/shelves')->assertDontSee($ownShelf->name);\n    }\n\n    public function test_bookshelves_delete_all_permission()\n    {\n        $this->permissions->grantUserRolePermissions($this->user, ['bookshelf-update-all']);\n        /** @var Bookshelf $otherShelf */\n        $otherShelf = Bookshelf::query()->first();\n        $this->checkAccessPermission('bookshelf-delete-all', [\n            $otherShelf->getUrl('/delete'),\n        ], [\n            $otherShelf->getUrl() => 'Delete',\n        ]);\n\n        $this->delete($otherShelf->getUrl())->assertRedirect('/shelves');\n        $this->get('/shelves')->assertDontSee($otherShelf->name);\n    }\n\n    public function test_books_create_all_permissions()\n    {\n        $this->checkAccessPermission('book-create-all', [\n            '/create-book',\n        ], [\n            '/books' => 'Create New Book',\n        ]);\n\n        $this->post('/books', [\n            'name'        => 'test book',\n            'description' => 'book desc',\n        ])->assertRedirect('/books/test-book');\n    }\n\n    public function test_books_edit_own_permission()\n    {\n        /** @var Book $otherBook */\n        $otherBook = Book::query()->take(1)->get()->first();\n        $ownBook = $this->entities->createChainBelongingToUser($this->user)['book'];\n        $this->checkAccessPermission('book-update-own', [\n            $ownBook->getUrl() . '/edit',\n        ], [\n            $ownBook->getUrl() => 'Edit',\n        ]);\n\n        $resp = $this->get($otherBook->getUrl());\n        $this->withHtml($resp)->assertElementNotContains('.action-buttons', 'Edit');\n        $this->get($otherBook->getUrl('/edit'))->assertRedirect('/');\n    }\n\n    public function test_books_edit_all_permission()\n    {\n        /** @var Book $otherBook */\n        $otherBook = Book::query()->take(1)->get()->first();\n        $this->checkAccessPermission('book-update-all', [\n            $otherBook->getUrl() . '/edit',\n        ], [\n            $otherBook->getUrl() => 'Edit',\n        ]);\n    }\n\n    public function test_books_delete_own_permission()\n    {\n        $this->permissions->grantUserRolePermissions($this->user, ['book-update-all']);\n        /** @var Book $otherBook */\n        $otherBook = Book::query()->take(1)->get()->first();\n        $ownBook = $this->entities->createChainBelongingToUser($this->user)['book'];\n        $this->checkAccessPermission('book-delete-own', [\n            $ownBook->getUrl() . '/delete',\n        ], [\n            $ownBook->getUrl() => 'Delete',\n        ]);\n\n        $resp = $this->get($otherBook->getUrl());\n        $this->withHtml($resp)->assertElementNotContains('.action-buttons', 'Delete');\n        $this->get($otherBook->getUrl('/delete'))->assertRedirect('/');\n        $this->get($ownBook->getUrl());\n        $this->delete($ownBook->getUrl())->assertRedirect('/books');\n        $this->get('/books')->assertDontSee($ownBook->name);\n    }\n\n    public function test_books_delete_all_permission()\n    {\n        $this->permissions->grantUserRolePermissions($this->user, ['book-update-all']);\n        /** @var Book $otherBook */\n        $otherBook = Book::query()->take(1)->get()->first();\n        $this->checkAccessPermission('book-delete-all', [\n            $otherBook->getUrl() . '/delete',\n        ], [\n            $otherBook->getUrl() => 'Delete',\n        ]);\n\n        $this->get($otherBook->getUrl());\n        $this->delete($otherBook->getUrl())->assertRedirect('/books');\n        $this->get('/books')->assertDontSee($otherBook->name);\n    }\n\n    public function test_chapter_create_own_permissions()\n    {\n        /** @var Book $book */\n        $book = Book::query()->take(1)->get()->first();\n        $ownBook = $this->entities->createChainBelongingToUser($this->user)['book'];\n        $this->checkAccessPermission('chapter-create-own', [\n            $ownBook->getUrl('/create-chapter'),\n        ], [\n            $ownBook->getUrl() => 'New Chapter',\n        ]);\n\n        $this->post($ownBook->getUrl('/create-chapter'), [\n            'name'        => 'test chapter',\n            'description' => 'chapter desc',\n        ])->assertRedirect($ownBook->getUrl('/chapter/test-chapter'));\n\n        $resp = $this->get($book->getUrl());\n        $this->withHtml($resp)->assertElementNotContains('.action-buttons', 'New Chapter');\n        $this->get($book->getUrl('/create-chapter'))->assertRedirect('/');\n    }\n\n    public function test_chapter_create_all_permissions()\n    {\n        $book = $this->entities->book();\n        $this->checkAccessPermission('chapter-create-all', [\n            $book->getUrl('/create-chapter'),\n        ], [\n            $book->getUrl() => 'New Chapter',\n        ]);\n\n        $this->post($book->getUrl('/create-chapter'), [\n            'name'        => 'test chapter',\n            'description' => 'chapter desc',\n        ])->assertRedirect($book->getUrl('/chapter/test-chapter'));\n    }\n\n    public function test_chapter_edit_own_permission()\n    {\n        /** @var Chapter $otherChapter */\n        $otherChapter = Chapter::query()->first();\n        $ownChapter = $this->entities->createChainBelongingToUser($this->user)['chapter'];\n        $this->checkAccessPermission('chapter-update-own', [\n            $ownChapter->getUrl() . '/edit',\n        ], [\n            $ownChapter->getUrl() => 'Edit',\n        ]);\n\n        $resp = $this->get($otherChapter->getUrl());\n        $this->withHtml($resp)->assertElementNotContains('.action-buttons', 'Edit');\n        $this->get($otherChapter->getUrl('/edit'))->assertRedirect('/');\n    }\n\n    public function test_chapter_edit_all_permission()\n    {\n        /** @var Chapter $otherChapter */\n        $otherChapter = Chapter::query()->take(1)->get()->first();\n        $this->checkAccessPermission('chapter-update-all', [\n            $otherChapter->getUrl() . '/edit',\n        ], [\n            $otherChapter->getUrl() => 'Edit',\n        ]);\n    }\n\n    public function test_chapter_delete_own_permission()\n    {\n        $this->permissions->grantUserRolePermissions($this->user, ['chapter-update-all']);\n        /** @var Chapter $otherChapter */\n        $otherChapter = Chapter::query()->first();\n        $ownChapter = $this->entities->createChainBelongingToUser($this->user)['chapter'];\n        $this->checkAccessPermission('chapter-delete-own', [\n            $ownChapter->getUrl() . '/delete',\n        ], [\n            $ownChapter->getUrl() => 'Delete',\n        ]);\n\n        $bookUrl = $ownChapter->book->getUrl();\n        $resp = $this->get($otherChapter->getUrl());\n        $this->withHtml($resp)->assertElementNotContains('.action-buttons', 'Delete');\n        $this->get($otherChapter->getUrl('/delete'))->assertRedirect('/');\n        $this->get($ownChapter->getUrl());\n        $this->delete($ownChapter->getUrl())->assertRedirect($bookUrl);\n        $resp = $this->get($bookUrl);\n        $this->withHtml($resp)->assertElementNotContains('.book-content', $ownChapter->name);\n    }\n\n    public function test_chapter_delete_all_permission()\n    {\n        $this->permissions->grantUserRolePermissions($this->user, ['chapter-update-all']);\n        /** @var Chapter $otherChapter */\n        $otherChapter = Chapter::query()->first();\n        $this->checkAccessPermission('chapter-delete-all', [\n            $otherChapter->getUrl() . '/delete',\n        ], [\n            $otherChapter->getUrl() => 'Delete',\n        ]);\n\n        $bookUrl = $otherChapter->book->getUrl();\n        $this->get($otherChapter->getUrl());\n        $this->delete($otherChapter->getUrl())->assertRedirect($bookUrl);\n        $resp = $this->get($bookUrl);\n        $this->withHtml($resp)->assertElementNotContains('.book-content', $otherChapter->name);\n    }\n\n    public function test_page_create_own_permissions()\n    {\n        $book = $this->entities->book();\n        $chapter = $this->entities->chapter();\n\n        $entities = $this->entities->createChainBelongingToUser($this->user);\n        $ownBook = $entities['book'];\n        $ownChapter = $entities['chapter'];\n\n        $createUrl = $ownBook->getUrl('/create-page');\n        $createUrlChapter = $ownChapter->getUrl('/create-page');\n        $accessUrls = [$createUrl, $createUrlChapter];\n\n        foreach ($accessUrls as $url) {\n            $this->actingAs($this->user)->get($url)->assertRedirect('/');\n        }\n\n        $this->checkAccessPermission('page-create-own', [], [\n            $ownBook->getUrl()    => 'New Page',\n            $ownChapter->getUrl() => 'New Page',\n        ]);\n\n        $this->permissions->grantUserRolePermissions($this->user, ['page-create-own']);\n\n        foreach ($accessUrls as $index => $url) {\n            $resp = $this->actingAs($this->user)->get($url);\n            $expectedUrl = Page::query()->where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl();\n            $resp->assertRedirect($expectedUrl);\n        }\n\n        $this->get($createUrl);\n        /** @var Page $draft */\n        $draft = Page::query()->where('draft', '=', true)->orderBy('id', 'desc')->first();\n        $this->post($draft->getUrl(), [\n            'name' => 'test page',\n            'html' => 'page desc',\n        ])->assertRedirect($ownBook->getUrl('/page/test-page'));\n\n        $resp = $this->get($book->getUrl());\n        $this->withHtml($resp)->assertElementNotContains('.action-buttons', 'New Page');\n        $this->get($book->getUrl('/create-page'))->assertRedirect('/');\n\n        $resp = $this->get($chapter->getUrl());\n        $this->withHtml($resp)->assertElementNotContains('.action-buttons', 'New Page');\n        $this->get($chapter->getUrl('/create-page'))->assertRedirect('/');\n    }\n\n    public function test_page_create_all_permissions()\n    {\n        $book = $this->entities->book();\n        $chapter = $this->entities->chapter();\n        $createUrl = $book->getUrl('/create-page');\n\n        $createUrlChapter = $chapter->getUrl('/create-page');\n        $accessUrls = [$createUrl, $createUrlChapter];\n\n        foreach ($accessUrls as $url) {\n            $this->actingAs($this->user)->get($url)->assertRedirect('/');\n        }\n\n        $this->checkAccessPermission('page-create-all', [], [\n            $book->getUrl()    => 'New Page',\n            $chapter->getUrl() => 'New Page',\n        ]);\n\n        $this->permissions->grantUserRolePermissions($this->user, ['page-create-all']);\n\n        foreach ($accessUrls as $index => $url) {\n            $resp = $this->actingAs($this->user)->get($url);\n            $expectedUrl = Page::query()->where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl();\n            $resp->assertRedirect($expectedUrl);\n        }\n\n        $this->get($createUrl);\n        /** @var Page $draft */\n        $draft = Page::query()->where('draft', '=', true)->orderByDesc('id')->first();\n        $this->post($draft->getUrl(), [\n            'name' => 'test page',\n            'html' => 'page desc',\n        ])->assertRedirect($book->getUrl('/page/test-page'));\n\n        $this->get($chapter->getUrl('/create-page'));\n        /** @var Page $draft */\n        $draft = Page::query()->where('draft', '=', true)->orderByDesc('id')->first();\n        $this->post($draft->getUrl(), [\n            'name' => 'new test page',\n            'html' => 'page desc',\n        ])->assertRedirect($book->getUrl('/page/new-test-page'));\n    }\n\n    public function test_page_edit_own_permission()\n    {\n        /** @var Page $otherPage */\n        $otherPage = Page::query()->first();\n        $ownPage = $this->entities->createChainBelongingToUser($this->user)['page'];\n        $this->checkAccessPermission('page-update-own', [\n            $ownPage->getUrl() . '/edit',\n        ], [\n            $ownPage->getUrl() => 'Edit',\n        ], $ownPage->getUrl());\n\n        $resp = $this->get($otherPage->getUrl());\n        $this->withHtml($resp)->assertElementNotContains('.action-buttons', 'Edit');\n        $this->get($otherPage->getUrl() . '/edit')->assertRedirect($otherPage->getUrl());\n    }\n\n    public function test_page_edit_all_permission()\n    {\n        /** @var Page $otherPage */\n        $otherPage = Page::query()->first();\n        $this->checkAccessPermission('page-update-all', [\n            $otherPage->getUrl('/edit'),\n        ], [\n            $otherPage->getUrl() => 'Edit',\n        ], $otherPage->getUrl());\n    }\n\n    public function test_page_delete_own_permission()\n    {\n        $this->permissions->grantUserRolePermissions($this->user, ['page-update-all']);\n        /** @var Page $otherPage */\n        $otherPage = Page::query()->first();\n        $ownPage = $this->entities->createChainBelongingToUser($this->user)['page'];\n        $this->checkAccessPermission('page-delete-own', [\n            $ownPage->getUrl() . '/delete',\n        ], [\n            $ownPage->getUrl() => 'Delete',\n        ]);\n\n        $parent = $ownPage->chapter ?? $ownPage->book;\n        $resp = $this->get($otherPage->getUrl());\n        $this->withHtml($resp)->assertElementNotContains('.action-buttons', 'Delete');\n        $this->get($otherPage->getUrl('/delete'))->assertRedirect('/');\n        $this->get($ownPage->getUrl());\n        $this->delete($ownPage->getUrl())->assertRedirect($parent->getUrl());\n        $resp = $this->get($parent->getUrl());\n        $this->withHtml($resp)->assertElementNotContains('.book-content', $ownPage->name);\n    }\n\n    public function test_page_delete_all_permission()\n    {\n        $this->permissions->grantUserRolePermissions($this->user, ['page-update-all']);\n        /** @var Page $otherPage */\n        $otherPage = Page::query()->first();\n\n        $this->checkAccessPermission('page-delete-all', [\n            $otherPage->getUrl() . '/delete',\n        ], [\n            $otherPage->getUrl() => 'Delete',\n        ]);\n\n        /** @var Entity $parent */\n        $parent = $otherPage->chapter ?? $otherPage->book;\n        $this->get($otherPage->getUrl());\n\n        $this->delete($otherPage->getUrl())->assertRedirect($parent->getUrl());\n        $this->get($parent->getUrl())->assertDontSee($otherPage->name);\n    }\n\n\n    public function test_image_delete_own_permission()\n    {\n        $this->permissions->grantUserRolePermissions($this->user, ['image-update-all']);\n        $page = $this->entities->page();\n        $image = Image::factory()->create([\n            'uploaded_to' => $page->id,\n            'created_by'  => $this->user->id,\n            'updated_by'  => $this->user->id,\n        ]);\n\n        $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertStatus(403);\n\n        $this->permissions->grantUserRolePermissions($this->user, ['image-delete-own']);\n\n        $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertOk();\n        $this->assertDatabaseMissing('images', ['id' => $image->id]);\n    }\n\n    public function test_image_delete_all_permission()\n    {\n        $this->permissions->grantUserRolePermissions($this->user, ['image-update-all']);\n        $admin = $this->users->admin();\n        $page = $this->entities->page();\n        $image = Image::factory()->create(['uploaded_to' => $page->id, 'created_by' => $admin->id, 'updated_by' => $admin->id]);\n\n        $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertStatus(403);\n\n        $this->permissions->grantUserRolePermissions($this->user, ['image-delete-own']);\n\n        $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertStatus(403);\n\n        $this->permissions->grantUserRolePermissions($this->user, ['image-delete-all']);\n\n        $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertOk();\n        $this->assertDatabaseMissing('images', ['id' => $image->id]);\n    }\n\n    public function test_empty_state_actions_not_visible_without_permission()\n    {\n        $admin = $this->users->admin();\n        // Book links\n        $book = Book::factory()->create(['created_by' => $admin->id, 'updated_by' => $admin->id]);\n        $this->permissions->regenerateForEntity($book);\n        $this->actingAs($this->users->viewer())->get($book->getUrl())\n            ->assertDontSee('Create a new page')\n            ->assertDontSee('Add a chapter');\n\n        // Chapter links\n        $chapter = Chapter::factory()->create(['created_by' => $admin->id, 'updated_by' => $admin->id, 'book_id' => $book->id]);\n        $this->permissions->regenerateForEntity($chapter);\n        $this->actingAs($this->users->viewer())->get($chapter->getUrl())\n            ->assertDontSee('Create a new page')\n            ->assertDontSee('Sort the current book');\n    }\n\n    public function test_comment_create_permission()\n    {\n        $ownPage = $this->entities->createChainBelongingToUser($this->user)['page'];\n\n        $this->actingAs($this->user)\n            ->addComment($ownPage)\n            ->assertStatus(403);\n\n        $this->permissions->grantUserRolePermissions($this->user, ['comment-create-all']);\n\n        $this->actingAs($this->user)\n            ->addComment($ownPage)\n            ->assertOk();\n    }\n\n    public function test_comment_update_own_permission()\n    {\n        $ownPage = $this->entities->createChainBelongingToUser($this->user)['page'];\n        $this->permissions->grantUserRolePermissions($this->user, ['comment-create-all']);\n        $this->actingAs($this->user)->addComment($ownPage);\n        /** @var Comment $comment */\n        $comment = $ownPage->comments()->latest()->first();\n\n        // no comment-update-own\n        $this->actingAs($this->user)->updateComment($comment)->assertStatus(403);\n\n        $this->permissions->grantUserRolePermissions($this->user, ['comment-update-own']);\n\n        // now has comment-update-own\n        $this->actingAs($this->user)->updateComment($comment)->assertOk();\n    }\n\n    public function test_comment_update_all_permission()\n    {\n        /** @var Page $ownPage */\n        $ownPage = $this->entities->createChainBelongingToUser($this->user)['page'];\n        $this->asAdmin()->addComment($ownPage);\n        /** @var Comment $comment */\n        $comment = $ownPage->comments()->latest()->first();\n\n        // no comment-update-all\n        $this->actingAs($this->user)->updateComment($comment)->assertStatus(403);\n\n        $this->permissions->grantUserRolePermissions($this->user, ['comment-update-all']);\n\n        // now has comment-update-all\n        $this->actingAs($this->user)->updateComment($comment)->assertOk();\n    }\n\n    public function test_comment_delete_own_permission()\n    {\n        /** @var Page $ownPage */\n        $ownPage = $this->entities->createChainBelongingToUser($this->user)['page'];\n        $this->permissions->grantUserRolePermissions($this->user, ['comment-create-all']);\n        $this->actingAs($this->user)->addComment($ownPage);\n\n        /** @var Comment $comment */\n        $comment = $ownPage->comments()->latest()->first();\n\n        // no comment-delete-own\n        $this->actingAs($this->user)->deleteComment($comment)->assertStatus(403);\n\n        $this->permissions->grantUserRolePermissions($this->user, ['comment-delete-own']);\n\n        // now has comment-update-own\n        $this->actingAs($this->user)->deleteComment($comment)->assertOk();\n    }\n\n    public function test_comment_delete_all_permission()\n    {\n        /** @var Page $ownPage */\n        $ownPage = $this->entities->createChainBelongingToUser($this->user)['page'];\n        $this->asAdmin()->addComment($ownPage);\n        /** @var Comment $comment */\n        $comment = $ownPage->comments()->latest()->first();\n\n        // no comment-delete-all\n        $this->actingAs($this->user)->deleteComment($comment)->assertStatus(403);\n\n        $this->permissions->grantUserRolePermissions($this->user, ['comment-delete-all']);\n\n        // now has comment-delete-all\n        $this->actingAs($this->user)->deleteComment($comment)->assertOk();\n    }\n\n    private function addComment(Page $page): TestResponse\n    {\n        return $this->postJson(\"/comment/$page->id\", ['html' => '<p>New comment content</p>']);\n    }\n\n    private function updateComment(Comment $comment): TestResponse\n    {\n        return $this->putJson(\"/comment/{$comment->id}\", ['html' => '<p>Updated comment content</p>']);\n    }\n\n    private function deleteComment(Comment $comment): TestResponse\n    {\n        return $this->json('DELETE', '/comment/' . $comment->id);\n    }\n}\n"
  },
  {
    "path": "tests/Permissions/Scenarios/EntityRolePermissionsTest.php",
    "content": "<?php\n\nnamespace Tests\\Permissions\\Scenarios;\n\nclass EntityRolePermissionsTest extends PermissionScenarioTestCase\n{\n    public function test_01_explicit_allow()\n    {\n        [$user, $role] = $this->users->newUserWithRole();\n        $page = $this->entities->page();\n        $this->permissions->setEntityPermissions($page, ['view'], [$role], false);\n\n        $this->assertVisibleToUser($page, $user);\n    }\n\n    public function test_02_explicit_deny()\n    {\n        [$user, $role] = $this->users->newUserWithRole();\n        $page = $this->entities->page();\n        $this->permissions->setEntityPermissions($page, [], [$role], false);\n\n        $this->assertNotVisibleToUser($page, $user);\n    }\n\n    public function test_03_same_level_conflicting()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole();\n        $roleB = $this->users->attachNewRole($user);\n        $page = $this->entities->page();\n\n        $this->permissions->disableEntityInheritedPermissions($page);\n        $this->permissions->addEntityPermission($page, [], $roleA);\n        $this->permissions->addEntityPermission($page, ['view'], $roleB);\n\n        $this->assertVisibleToUser($page, $user);\n    }\n\n    public function test_20_inherit_allow()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole();\n        $page = $this->entities->pageWithinChapter();\n        $chapter = $page->chapter;\n\n        $this->permissions->disableEntityInheritedPermissions($chapter);\n        $this->permissions->addEntityPermission($chapter, ['view'], $roleA);\n\n        $this->assertVisibleToUser($page, $user);\n    }\n\n    public function test_21_inherit_deny()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole();\n        $page = $this->entities->pageWithinChapter();\n        $chapter = $page->chapter;\n\n        $this->permissions->disableEntityInheritedPermissions($chapter);\n        $this->permissions->addEntityPermission($chapter, [], $roleA);\n\n        $this->assertNotVisibleToUser($page, $user);\n    }\n\n    public function test_22_same_level_conflict_inherit()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole();\n        $roleB = $this->users->attachNewRole($user);\n        $page = $this->entities->pageWithinChapter();\n        $chapter = $page->chapter;\n\n        $this->permissions->disableEntityInheritedPermissions($chapter);\n        $this->permissions->addEntityPermission($chapter, [], $roleA);\n        $this->permissions->addEntityPermission($chapter, ['view'], $roleB);\n\n        $this->assertVisibleToUser($page, $user);\n    }\n\n    public function test_30_child_inherit_override_allow()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole();\n        $page = $this->entities->pageWithinChapter();\n        $chapter = $page->chapter;\n\n        $this->permissions->disableEntityInheritedPermissions($chapter);\n        $this->permissions->addEntityPermission($chapter, [], $roleA);\n        $this->permissions->addEntityPermission($page, ['view'], $roleA);\n\n        $this->assertVisibleToUser($page, $user);\n    }\n\n    public function test_31_child_inherit_override_deny()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole();\n        $page = $this->entities->pageWithinChapter();\n        $chapter = $page->chapter;\n\n        $this->permissions->disableEntityInheritedPermissions($chapter);\n        $this->permissions->addEntityPermission($chapter, ['view'], $roleA);\n        $this->permissions->addEntityPermission($page, [], $roleA);\n\n        $this->assertNotVisibleToUser($page, $user);\n    }\n\n    public function test_40_multi_role_inherit_conflict_override_deny()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole();\n        $roleB = $this->users->attachNewRole($user);\n        $page = $this->entities->pageWithinChapter();\n        $chapter = $page->chapter;\n\n        $this->permissions->disableEntityInheritedPermissions($chapter);\n        $this->permissions->addEntityPermission($page, [], $roleA);\n        $this->permissions->addEntityPermission($chapter, ['view'], $roleB);\n\n        $this->assertVisibleToUser($page, $user);\n    }\n\n    public function test_41_multi_role_inherit_conflict_retain_allow()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole();\n        $roleB = $this->users->attachNewRole($user);\n        $page = $this->entities->pageWithinChapter();\n        $chapter = $page->chapter;\n\n        $this->permissions->disableEntityInheritedPermissions($chapter);\n        $this->permissions->addEntityPermission($page, ['view'], $roleA);\n        $this->permissions->addEntityPermission($chapter, [], $roleB);\n\n        $this->assertVisibleToUser($page, $user);\n    }\n\n    public function test_50_role_override_allow()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole();\n        $page = $this->entities->page();\n        $this->permissions->addEntityPermission($page, ['view'], $roleA);\n\n        $this->assertVisibleToUser($page, $user);\n    }\n\n    public function test_51_role_override_deny()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']);\n        $page = $this->entities->page();\n        $this->permissions->addEntityPermission($page, [], $roleA);\n\n        $this->assertNotVisibleToUser($page, $user);\n    }\n\n    public function test_60_inherited_role_override_allow()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole([], []);\n        $page = $this->entities->pageWithinChapter();\n        $chapter = $page->chapter;\n        $this->permissions->addEntityPermission($chapter, ['view'], $roleA);\n\n        $this->assertVisibleToUser($page, $user);\n    }\n\n    public function test_61_inherited_role_override_deny()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']);\n        $page = $this->entities->pageWithinChapter();\n        $chapter = $page->chapter;\n        $this->permissions->addEntityPermission($chapter, [], $roleA);\n\n        $this->assertNotVisibleToUser($page, $user);\n    }\n\n    public function test_62_inherited_role_override_deny_on_own()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-own']);\n        $page = $this->entities->pageWithinChapter();\n        $chapter = $page->chapter;\n        $this->permissions->addEntityPermission($chapter, [], $roleA);\n        $this->permissions->changeEntityOwner($page, $user);\n\n        $this->assertNotVisibleToUser($page, $user);\n    }\n\n    public function test_70_multi_role_inheriting_deny()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']);\n        $roleB = $this->users->attachNewRole($user);\n        $page = $this->entities->page();\n\n        $this->permissions->addEntityPermission($page, [], $roleB);\n\n        $this->assertNotVisibleToUser($page, $user);\n    }\n\n    public function test_71_multi_role_inheriting_deny_on_own()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-own']);\n        $roleB = $this->users->attachNewRole($user);\n        $page = $this->entities->page();\n        $this->permissions->changeEntityOwner($page, $user);\n\n        $this->permissions->addEntityPermission($page, [], $roleB);\n\n        $this->assertNotVisibleToUser($page, $user);\n    }\n\n\n    public function test_75_multi_role_inherited_deny_via_parent()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']);\n        $roleB = $this->users->attachNewRole($user);\n        $page = $this->entities->pageWithinChapter();\n        $chapter = $page->chapter;\n\n        $this->permissions->addEntityPermission($chapter, [], $roleB);\n\n        $this->assertNotVisibleToUser($page, $user);\n    }\n\n    public function test_76_multi_role_inherited_deny_via_parent_on_own()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-own']);\n        $roleB = $this->users->attachNewRole($user);\n        $page = $this->entities->pageWithinChapter();\n        $chapter = $page->chapter;\n        $this->permissions->changeEntityOwner($page, $user);\n\n        $this->permissions->addEntityPermission($chapter, [], $roleB);\n\n        $this->assertNotVisibleToUser($page, $user);\n    }\n\n    public function test_80_fallback_override_allow()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole();\n        $page = $this->entities->page();\n\n        $this->permissions->setFallbackPermissions($page, []);\n        $this->permissions->addEntityPermission($page, ['view'], $roleA);\n\n        $this->assertVisibleToUser($page, $user);\n    }\n    public function test_81_fallback_override_deny()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole();\n        $page = $this->entities->page();\n\n        $this->permissions->setFallbackPermissions($page, ['view']);\n        $this->permissions->addEntityPermission($page, [], $roleA);\n\n        $this->assertNotVisibleToUser($page, $user);\n    }\n\n    public function test_84_fallback_override_allow_multi_role()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole();\n        $roleB = $this->users->attachNewRole($user);\n        $page = $this->entities->page();\n\n        $this->permissions->setFallbackPermissions($page, []);\n        $this->permissions->addEntityPermission($page, ['view'], $roleA);\n\n        $this->assertVisibleToUser($page, $user);\n    }\n\n    public function test_85_fallback_override_deny_multi_role()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole();\n        $roleB = $this->users->attachNewRole($user);\n        $page = $this->entities->page();\n\n        $this->permissions->setFallbackPermissions($page, ['view']);\n        $this->permissions->addEntityPermission($page, [], $roleA);\n\n        $this->assertNotVisibleToUser($page, $user);\n    }\n\n    public function test_86_fallback_override_allow_inherit()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole();\n        $page = $this->entities->page();\n        $chapter = $page->chapter;\n\n        $this->permissions->setFallbackPermissions($chapter, []);\n        $this->permissions->addEntityPermission($chapter, ['view'], $roleA);\n\n        $this->assertVisibleToUser($page, $user);\n    }\n\n    public function test_87_fallback_override_deny_inherit()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole();\n        $page = $this->entities->page();\n        $chapter = $page->chapter;\n\n        $this->permissions->setFallbackPermissions($chapter, ['view']);\n        $this->permissions->addEntityPermission($chapter, [], $roleA);\n\n        $this->assertNotVisibleToUser($page, $user);\n    }\n\n    public function test_88_fallback_override_allow_multi_role_inherit()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole();\n        $roleB = $this->users->attachNewRole($user);\n        $page = $this->entities->page();\n        $chapter = $page->chapter;\n\n        $this->permissions->setFallbackPermissions($chapter, []);\n        $this->permissions->addEntityPermission($chapter, ['view'], $roleA);\n\n        $this->assertVisibleToUser($page, $user);\n    }\n\n    public function test_89_fallback_override_deny_multi_role_inherit()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole();\n        $roleB = $this->users->attachNewRole($user);\n        $page = $this->entities->page();\n        $chapter = $page->chapter;\n\n        $this->permissions->setFallbackPermissions($chapter, ['view']);\n        $this->permissions->addEntityPermission($chapter, [], $roleA);\n\n        $this->assertNotVisibleToUser($page, $user);\n    }\n\n    public function test_90_fallback_overrides_parent_entity_role_deny()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole();\n        $page = $this->entities->page();\n        $chapter = $page->chapter;\n\n        $this->permissions->setFallbackPermissions($chapter, []);\n        $this->permissions->setFallbackPermissions($page, []);\n        $this->permissions->addEntityPermission($chapter, ['view'], $roleA);\n\n        $this->assertNotVisibleToUser($page, $user);\n    }\n\n    public function test_91_fallback_overrides_parent_entity_role_inherit()\n    {\n        [$user, $roleA] = $this->users->newUserWithRole();\n        $page = $this->entities->page();\n        $chapter = $page->chapter;\n        $book = $page->book;\n\n        $this->permissions->setFallbackPermissions($book, []);\n        $this->permissions->setFallbackPermissions($chapter, []);\n        $this->permissions->addEntityPermission($book, ['view'], $roleA);\n\n        $this->assertNotVisibleToUser($page, $user);\n    }\n}\n"
  },
  {
    "path": "tests/Permissions/Scenarios/PermissionScenarioTestCase.php",
    "content": "<?php\n\nnamespace Tests\\Permissions\\Scenarios;\n\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Users\\Models\\User;\nuse Tests\\TestCase;\n\n// Cases defined in dev/docs/permission-scenario-testing.md\n\nclass PermissionScenarioTestCase extends TestCase\n{\n    protected function assertVisibleToUser(Entity $entity, User $user)\n    {\n        $this->actingAs($user);\n        $funcView = userCan($entity->getMorphClass() . '-view', $entity);\n        $queryView = $entity->newQuery()->scopes(['visible'])->find($entity->id) !== null;\n\n        $id = $entity->getMorphClass() . ':' . $entity->id;\n        $msg = \"Item [{$id}] should be visible but was not found via \";\n        $msg .= implode(' and ', array_filter([!$funcView ? 'userCan' : '', !$queryView ? 'query' : '']));\n\n        static::assertTrue($funcView && $queryView, $msg);\n    }\n\n    protected function assertNotVisibleToUser(Entity $entity, User $user)\n    {\n        $this->actingAs($user);\n        $funcView = userCan($entity->getMorphClass() . '-view', $entity);\n        $queryView = $entity->newQuery()->scopes(['visible'])->find($entity->id) !== null;\n\n        $id = $entity->getMorphClass() . ':' . $entity->id;\n        $msg = \"Item [{$id}] should not be visible but was found via \";\n        $msg .= implode(' and ', array_filter([$funcView ? 'userCan' : '', $queryView ? 'query' : '']));\n\n        static::assertTrue(!$funcView && !$queryView, $msg);\n    }\n}\n"
  },
  {
    "path": "tests/Permissions/Scenarios/RoleContentPermissionsTest.php",
    "content": "<?php\n\nnamespace Tests\\Permissions\\Scenarios;\n\nclass RoleContentPermissionsTest extends PermissionScenarioTestCase\n{\n    public function test_01_allow()\n    {\n        [$user] = $this->users->newUserWithRole([], ['page-view-all']);\n        $page = $this->entities->page();\n\n        $this->assertVisibleToUser($page, $user);\n    }\n\n    public function test_02_deny()\n    {\n        [$user] = $this->users->newUserWithRole([], []);\n        $page = $this->entities->page();\n\n        $this->assertNotVisibleToUser($page, $user);\n    }\n\n    public function test_10_allow_on_own_with_own()\n    {\n        [$user] = $this->users->newUserWithRole([], ['page-view-own']);\n        $page = $this->entities->page();\n        $this->permissions->changeEntityOwner($page, $user);\n\n        $this->assertVisibleToUser($page, $user);\n    }\n\n    public function test_11_deny_on_other_with_own()\n    {\n        [$user] = $this->users->newUserWithRole([], ['page-view-own']);\n        $page = $this->entities->page();\n        $this->permissions->changeEntityOwner($page, $this->users->editor());\n\n        $this->assertNotVisibleToUser($page, $user);\n    }\n\n    public function test_20_multiple_role_conflicting_all()\n    {\n        [$user] = $this->users->newUserWithRole([], ['page-view-all']);\n        $this->users->attachNewRole($user, []);\n        $page = $this->entities->page();\n\n        $this->assertVisibleToUser($page, $user);\n    }\n\n    public function test_21_multiple_role_conflicting_own()\n    {\n        [$user] = $this->users->newUserWithRole([], ['page-view-own']);\n        $this->users->attachNewRole($user, []);\n        $page = $this->entities->page();\n        $this->permissions->changeEntityOwner($page, $user);\n\n        $this->assertVisibleToUser($page, $user);\n    }\n}\n"
  },
  {
    "path": "tests/PublicActionTest.php",
    "content": "<?php\n\nnamespace Tests;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Permissions\\Models\\RolePermission;\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Support\\Facades\\Auth;\nuse Illuminate\\Support\\Facades\\View;\n\nclass PublicActionTest extends TestCase\n{\n    public function test_app_not_public()\n    {\n        $this->setSettings(['app-public' => 'false']);\n        $book = $this->entities->book();\n        $this->get('/books')->assertRedirect('/login');\n        $this->get($book->getUrl())->assertRedirect('/login');\n\n        $page = $this->entities->page();\n        $this->get($page->getUrl())->assertRedirect('/login');\n    }\n\n    public function test_login_link_visible()\n    {\n        $this->setSettings(['app-public' => 'true']);\n        $resp = $this->get('/');\n        $this->withHtml($resp)->assertElementExists('a[href=\"' . url('/login') . '\"]');\n    }\n\n    public function test_register_link_visible_when_enabled()\n    {\n        $this->setSettings(['app-public' => 'true']);\n        $home = $this->get('/');\n        $home->assertSee(url('/login'));\n        $home->assertDontSee(url('/register'));\n\n        $this->setSettings(['app-public' => 'true', 'registration-enabled' => 'true']);\n        $home = $this->get('/');\n        $home->assertSee(url('/login'));\n        $home->assertSee(url('/register'));\n    }\n\n    public function test_books_viewable()\n    {\n        $this->setSettings(['app-public' => 'true']);\n        $books = Book::query()->orderBy('name', 'asc')->take(10)->get();\n        $bookToVisit = $books[1];\n\n        // Check books index page is showing\n        $resp = $this->get('/books');\n        $resp->assertStatus(200);\n        $resp->assertSee($books[0]->name);\n\n        // Check individual book page is showing and it's child contents are visible.\n        $resp = $this->get($bookToVisit->getUrl());\n        $resp->assertSee($bookToVisit->name);\n        $resp->assertSee($bookToVisit->chapters()->first()->name);\n    }\n\n    public function test_chapters_viewable()\n    {\n        $this->setSettings(['app-public' => 'true']);\n        /** @var Chapter $chapterToVisit */\n        $chapterToVisit = Chapter::query()->first();\n        $pageToVisit = $chapterToVisit->pages()->first();\n\n        // Check chapters index page is showing\n        $resp = $this->get($chapterToVisit->getUrl());\n        $resp->assertStatus(200);\n        $resp->assertSee($chapterToVisit->name);\n        // Check individual chapter page is showing and it's child contents are visible.\n        $resp->assertSee($pageToVisit->name);\n        $resp = $this->get($pageToVisit->getUrl());\n        $resp->assertStatus(200);\n        $resp->assertSee($chapterToVisit->book->name);\n        $resp->assertSee($chapterToVisit->name);\n    }\n\n    public function test_public_page_creation()\n    {\n        $this->setSettings(['app-public' => 'true']);\n        $publicRole = Role::getSystemRole('public');\n        // Grant all permissions to public\n        $publicRole->permissions()->detach();\n        foreach (RolePermission::all() as $perm) {\n            $publicRole->attachPermission($perm);\n        }\n        user()->clearPermissionCache();\n\n        $chapter = $this->entities->chapter();\n        $resp = $this->get($chapter->getUrl());\n        $resp->assertSee('New Page');\n        $this->withHtml($resp)->assertElementExists('a[href=\"' . $chapter->getUrl('/create-page') . '\"]');\n\n        $resp = $this->get($chapter->getUrl('/create-page'));\n        $resp->assertSee('Continue');\n        $resp->assertSee('Page Name');\n        $this->withHtml($resp)->assertElementExists('form[action=\"' . $chapter->getUrl('/create-guest-page') . '\"]');\n\n        $resp = $this->post($chapter->getUrl('/create-guest-page'), ['name' => 'My guest page']);\n        $resp->assertRedirect($chapter->book->getUrl('/page/my-guest-page/edit'));\n\n        $user = $this->users->guest();\n        $this->assertDatabaseHasEntityData('page', [\n            'name'       => 'My guest page',\n            'chapter_id' => $chapter->id,\n            'created_by' => $user->id,\n            'updated_by' => $user->id,\n        ]);\n    }\n\n    public function test_content_not_listed_on_404_for_public_users()\n    {\n        $page = $this->entities->page();\n        $page->fill(['name' => 'my testing random unique page name'])->save();\n        $this->asAdmin()->get($page->getUrl()); // Fake visit to show on recents\n        $resp = $this->get('/cats/dogs/hippos');\n        $resp->assertStatus(404);\n        $resp->assertSee($page->name);\n        View::share('pageTitle', '');\n\n        Auth::logout();\n        $resp = $this->get('/cats/dogs/hippos');\n        $resp->assertStatus(404);\n        $resp->assertDontSee($page->name);\n    }\n\n    public function test_default_favicon_file_created_upon_access()\n    {\n        $faviconPath = public_path('favicon.ico');\n        if (file_exists($faviconPath)) {\n            unlink($faviconPath);\n        }\n\n        $this->assertFileDoesNotExist($faviconPath);\n        $this->get('/favicon.ico');\n        $this->assertFileExists($faviconPath);\n    }\n\n    public function test_public_view_then_login_redirects_to_previous_content()\n    {\n        $this->setSettings(['app-public' => 'true']);\n        $book = $this->entities->book();\n        $resp = $this->get($book->getUrl());\n        $resp->assertSee($book->name);\n\n        $this->get('/login');\n        $resp = $this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']);\n        $resp->assertRedirect($book->getUrl());\n    }\n\n    public function test_access_hidden_content_then_login_redirects_to_intended_content()\n    {\n        $this->setSettings(['app-public' => 'true']);\n        $book = $this->entities->book();\n        $this->permissions->setEntityPermissions($book);\n\n        $resp = $this->get($book->getUrl());\n        $resp->assertSee('Book not found');\n\n        $this->get('/login');\n        $resp = $this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']);\n        $resp->assertRedirect($book->getUrl());\n        $this->followRedirects($resp)->assertSee($book->name);\n    }\n\n    public function test_public_view_can_take_on_other_roles()\n    {\n        $this->setSettings(['app-public' => 'true']);\n        $newRole = $this->users->attachNewRole($this->users->guest(), []);\n        $page = $this->entities->page();\n        $this->permissions->disableEntityInheritedPermissions($page);\n        $this->permissions->addEntityPermission($page, ['view', 'update'], $newRole);\n\n        $resp = $this->get($page->getUrl());\n        $resp->assertOk();\n\n        $this->withHtml($resp)->assertLinkExists($page->getUrl('/edit'));\n    }\n\n    public function test_public_user_cannot_view_or_update_their_profile()\n    {\n        $this->setSettings(['app-public' => 'true']);\n        $guest = $this->users->guest();\n\n        $resp = $this->get($guest->getEditUrl());\n        $this->assertPermissionError($resp);\n\n        $resp = $this->put($guest->getEditUrl(), ['name' => 'My new guest name']);\n        $this->assertPermissionError($resp);\n    }\n}\n"
  },
  {
    "path": "tests/References/CrossLinkParserTest.php",
    "content": "<?php\n\nnamespace Tests\\References;\n\nuse BookStack\\References\\CrossLinkParser;\nuse Tests\\TestCase;\n\nclass CrossLinkParserTest extends TestCase\n{\n    public function test_instance_with_entity_resolvers_matches_entity_links()\n    {\n        $entities = $this->entities->all();\n        $otherPage = $this->entities->page();\n\n        $html = '\n<a href=\"' . url('/link/' . $otherPage->id) . '#cat\">Page Permalink</a>\n<a href=\"' . $entities['page']->getUrl() . '?a=b\">Page Link</a>\n<a href=\"' . $entities['chapter']->getUrl() . '?cat=mouse#donkey\">Chapter Link</a>\n<a href=\"' . $entities['book']->getUrl() . '/edit\">Book Link</a>\n<a href=\"' . $entities['bookshelf']->getUrl() . '/edit?cat=happy#hello\">Shelf Link</a>\n<a href=\"' . url('/settings') . '\">Settings Link</a>\n        ';\n\n        $parser = CrossLinkParser::createWithEntityResolvers();\n        $results = $parser->extractLinkedModels($html);\n\n        $this->assertCount(5, $results);\n        $this->assertEquals(get_class($otherPage), get_class($results[0]));\n        $this->assertEquals($otherPage->id, $results[0]->id);\n        $this->assertEquals(get_class($entities['page']), get_class($results[1]));\n        $this->assertEquals($entities['page']->id, $results[1]->id);\n        $this->assertEquals(get_class($entities['chapter']), get_class($results[2]));\n        $this->assertEquals($entities['chapter']->id, $results[2]->id);\n        $this->assertEquals(get_class($entities['book']), get_class($results[3]));\n        $this->assertEquals($entities['book']->id, $results[3]->id);\n        $this->assertEquals(get_class($entities['bookshelf']), get_class($results[4]));\n        $this->assertEquals($entities['bookshelf']->id, $results[4]->id);\n    }\n\n    public function test_similar_page_and_book_reference_links_dont_conflict()\n    {\n        $page = $this->entities->page();\n        $book = $page->book;\n\n        $html = '\n<a href=\"' . $page->getUrl() . '\">Page Link</a>\n<a href=\"' . $book->getUrl() . '\">Book Link</a>\n        ';\n\n        $parser = CrossLinkParser::createWithEntityResolvers();\n        $results = $parser->extractLinkedModels($html);\n\n        $this->assertCount(2, $results);\n        $this->assertEquals(get_class($page), get_class($results[0]));\n        $this->assertEquals($page->id, $results[0]->id);\n        $this->assertEquals(get_class($book), get_class($results[1]));\n        $this->assertEquals($book->id, $results[1]->id);\n    }\n}\n"
  },
  {
    "path": "tests/References/ReferencesTest.php",
    "content": "<?php\n\nnamespace Tests\\References;\n\nuse BookStack\\App\\Model;\nuse BookStack\\Entities\\Repos\\PageRepo;\nuse BookStack\\Entities\\Tools\\TrashCan;\nuse BookStack\\References\\Reference;\nuse Tests\\TestCase;\n\nclass ReferencesTest extends TestCase\n{\n    public function test_references_created_on_page_update()\n    {\n        $pageA = $this->entities->page();\n        $pageB = $this->entities->page();\n\n        $this->assertDatabaseMissing('references', ['from_id' => $pageA->id, 'from_type' => $pageA->getMorphClass()]);\n\n        $this->asEditor()->put($pageA->getUrl(), [\n            'name' => 'Reference test',\n            'html' => '<a href=\"' . $pageB->getUrl() . '\">Testing</a>',\n        ]);\n\n        $this->assertDatabaseHas('references', [\n            'from_id'   => $pageA->id,\n            'from_type' => $pageA->getMorphClass(),\n            'to_id'     => $pageB->id,\n            'to_type'   => $pageB->getMorphClass(),\n        ]);\n    }\n\n    public function test_references_created_on_book_chapter_bookshelf_update()\n    {\n        $entities = [$this->entities->book(), $this->entities->chapter(), $this->entities->shelf()];\n        $shelf = $this->entities->shelf();\n\n        foreach ($entities as $entity) {\n            $entity->refresh();\n            $this->assertDatabaseMissing('references', ['from_id' => $entity->id, 'from_type' => $entity->getMorphClass()]);\n\n            $this->asEditor()->put($entity->getUrl(), [\n                'name' => 'Reference test',\n                'description_html' => '<a href=\"' . $shelf->getUrl() . '\">Testing</a>',\n            ]);\n\n            $this->assertDatabaseHas('references', [\n                'from_id'   => $entity->id,\n                'from_type' => $entity->getMorphClass(),\n                'to_id'     => $shelf->id,\n                'to_type'   => $shelf->getMorphClass(),\n            ]);\n        }\n    }\n\n    public function test_references_deleted_on_page_delete()\n    {\n        $pageA = $this->entities->page();\n        $pageB = $this->entities->page();\n\n        $this->createReference($pageA, $pageB);\n        $this->createReference($pageB, $pageA);\n\n        $this->assertDatabaseHas('references', ['from_id' => $pageA->id, 'from_type' => $pageA->getMorphClass()]);\n        $this->assertDatabaseHas('references', ['to_id' => $pageA->id, 'to_type' => $pageA->getMorphClass()]);\n\n        app(PageRepo::class)->destroy($pageA);\n        app(TrashCan::class)->empty();\n\n        $this->assertDatabaseMissing('references', ['from_id' => $pageA->id, 'from_type' => $pageA->getMorphClass()]);\n        $this->assertDatabaseMissing('references', ['to_id' => $pageA->id, 'to_type' => $pageA->getMorphClass()]);\n    }\n\n    public function test_references_from_deleted_on_book_chapter_shelf_delete()\n    {\n        $entities = [$this->entities->chapter(), $this->entities->book(), $this->entities->shelf()];\n        $shelf = $this->entities->shelf();\n\n        foreach ($entities as $entity) {\n            $this->createReference($entity, $shelf);\n            $this->assertDatabaseHas('references', ['from_id' => $entity->id, 'from_type' => $entity->getMorphClass()]);\n\n            $this->asEditor()->delete($entity->getUrl());\n            app(TrashCan::class)->empty();\n\n            $this->assertDatabaseMissing('references', [\n                'from_id'   => $entity->id,\n                'from_type' => $entity->getMorphClass()\n            ]);\n        }\n    }\n\n    public function test_references_to_count_visible_on_entity_show_view()\n    {\n        $entities = $this->entities->all();\n        $otherPage = $this->entities->page();\n\n        $this->asEditor();\n        foreach ($entities as $entity) {\n            $this->createReference($entities['page'], $entity);\n        }\n\n        foreach ($entities as $entity) {\n            $resp = $this->get($entity->getUrl());\n            $resp->assertSee('Referenced by 1 item');\n            $resp->assertDontSee('Referenced by 1 items');\n        }\n\n        $this->createReference($otherPage, $entities['page']);\n        $resp = $this->get($entities['page']->getUrl());\n        $resp->assertSee('Referenced by 2 items');\n    }\n\n    public function test_references_to_visible_on_references_page()\n    {\n        $entities = $this->entities->all();\n        $this->asEditor();\n        foreach ($entities as $entity) {\n            $this->createReference($entities['page'], $entity);\n        }\n\n        foreach ($entities as $entity) {\n            $resp = $this->get($entity->getUrl('/references'));\n            $resp->assertSee('References');\n            $resp->assertSee($entities['page']->name);\n            $resp->assertDontSee('There are no tracked references');\n        }\n    }\n\n    public function test_reference_not_visible_if_view_permission_does_not_permit()\n    {\n        $page = $this->entities->page();\n        $pageB = $this->entities->page();\n        $this->createReference($pageB, $page);\n\n        $this->permissions->setEntityPermissions($pageB);\n\n        $this->asEditor()->get($page->getUrl('/references'))->assertDontSee($pageB->name);\n        $this->asAdmin()->get($page->getUrl('/references'))->assertSee($pageB->name);\n    }\n\n    public function test_reference_page_shows_empty_state_with_no_references()\n    {\n        $page = $this->entities->page();\n\n        $this->asEditor()\n            ->get($page->getUrl('/references'))\n            ->assertSee('There are no tracked references');\n    }\n\n    public function test_pages_leading_to_entity_updated_on_url_change()\n    {\n        $pageA = $this->entities->page();\n        $pageB = $this->entities->page();\n        $book = $this->entities->book();\n\n        foreach ([$pageA, $pageB] as $page) {\n            $page->html = '<a href=\"' . $book->getUrl() . '\">Link</a>';\n            $page->save();\n            $this->createReference($page, $book);\n        }\n\n        $this->asEditor()->put($book->getUrl(), [\n            'name' => 'my updated book slugaroo',\n        ]);\n\n        foreach ([$pageA, $pageB] as $page) {\n            $page->refresh();\n            $this->assertStringContainsString('href=\"http://localhost/books/my-updated-book-slugaroo\"', $page->html);\n            $this->assertDatabaseHas('page_revisions', [\n                'page_id' => $page->id,\n                'summary' => 'System auto-update of internal links',\n            ]);\n        }\n    }\n\n    public function test_pages_linking_to_other_page_updated_on_parent_book_url_change()\n    {\n        $bookPage = $this->entities->page();\n        $otherPage = $this->entities->page();\n        $book = $bookPage->book;\n\n        $otherPage->html = '<a href=\"' . $bookPage->getUrl() . '\">Link</a>';\n        $otherPage->save();\n        $this->createReference($otherPage, $bookPage);\n\n        $this->asEditor()->put($book->getUrl(), [\n            'name' => 'my updated book slugaroo',\n        ]);\n\n        $otherPage->refresh();\n        $this->assertStringContainsString('href=\"http://localhost/books/my-updated-book-slugaroo/page/' . $bookPage->slug . '\"', $otherPage->html);\n        $this->assertDatabaseHas('page_revisions', [\n            'page_id' => $otherPage->id,\n            'summary' => 'System auto-update of internal links',\n        ]);\n    }\n\n    public function test_pages_linking_to_chapter_updated_on_parent_book_url_change()\n    {\n        $bookChapter = $this->entities->chapter();\n        $otherPage = $this->entities->page();\n        $book = $bookChapter->book;\n\n        $otherPage->html = '<a href=\"' . $bookChapter->getUrl() . '\">Link</a>';\n        $otherPage->save();\n        $this->createReference($otherPage, $bookChapter);\n\n        $this->asEditor()->put($book->getUrl(), [\n            'name' => 'my updated book slugaroo',\n        ]);\n\n        $otherPage->refresh();\n        $this->assertStringContainsString('href=\"http://localhost/books/my-updated-book-slugaroo/chapter/' . $bookChapter->slug . '\"', $otherPage->html);\n        $this->assertDatabaseHas('page_revisions', [\n            'page_id' => $otherPage->id,\n            'summary' => 'System auto-update of internal links',\n        ]);\n    }\n\n    public function test_markdown_links_leading_to_entity_updated_on_url_change()\n    {\n        $page = $this->entities->page();\n        $book = $this->entities->book();\n\n        $bookUrl = $book->getUrl();\n        $markdown = '\n        [An awesome link](' . $bookUrl . ')\n        [An awesome link with query & hash](' . $bookUrl . '?test=yes#cats)\n        [An awesome link with path](' . $bookUrl . '/an/extra/trail)\n        [An awesome link with title](' . $bookUrl . ' \"title\")\n        [ref]: ' . $bookUrl . '?test=yes#dogs\n        [ref_without_space]:' . $bookUrl . '\n        [ref_with_title]: ' . $bookUrl . ' \"title\"';\n        $page->markdown = $markdown;\n        $page->save();\n        $this->createReference($page, $book);\n\n        $this->asEditor()->put($book->getUrl(), [\n            'name' => 'my updated book slugadoo',\n        ]);\n\n        $page->refresh();\n        $expected = str_replace($bookUrl, 'http://localhost/books/my-updated-book-slugadoo', $markdown);\n        $this->assertEquals($expected, $page->markdown);\n    }\n\n    public function test_description_links_from_book_chapter_shelf_updated_on_url_change()\n    {\n        $entities = [$this->entities->chapter(), $this->entities->book(), $this->entities->shelf()];\n        $shelf = $this->entities->shelf();\n        $this->asEditor();\n\n        foreach ($entities as $entity) {\n            $this->put($entity->getUrl(), [\n                'name' => 'Reference test',\n                'description_html' => '<a href=\"' . $shelf->getUrl() . '\">Testing</a>',\n            ]);\n        }\n\n        $oldUrl = $shelf->getUrl();\n        $this->put($shelf->getUrl(), ['name' => 'My updated shelf link'])->assertRedirect();\n        $shelf->refresh();\n        $this->assertNotEquals($oldUrl, $shelf->getUrl());\n\n        foreach ($entities as $entity) {\n            $oldHtml = $entity->description_html;\n            $entity->refresh();\n            $this->assertNotEquals($oldHtml, $entity->description_html);\n            $this->assertStringContainsString($shelf->getUrl(), $entity->description_html);\n        }\n    }\n\n    public function test_reference_from_deleted_item_does_not_count_or_show_in_references_page()\n    {\n        $page = $this->entities->page();\n        $referencingPageA = $this->entities->page();\n        $referencingPageB = $this->entities->page();\n\n        $this->asEditor();\n        $this->createReference($referencingPageA, $page);\n        $this->createReference($referencingPageB, $page);\n\n        $resp = $this->get($page->getUrl());\n        $resp->assertSee('Referenced by 2 items');\n\n        $this->delete($referencingPageA->getUrl());\n\n        $resp = $this->get($page->getUrl());\n        $resp->assertSee('Referenced by 1 item');\n\n        $resp = $this->get($page->getUrl('/references'));\n        $resp->assertOk();\n        $resp->assertSee($referencingPageB->getUrl());\n        $resp->assertDontSee($referencingPageA->getUrl());\n    }\n\n    protected function createReference(Model $from, Model $to): void\n    {\n        (new Reference())->forceFill([\n            'from_type' => $from->getMorphClass(),\n            'from_id'   => $from->id,\n            'to_type'   => $to->getMorphClass(),\n            'to_id'     => $to->id,\n        ])->save();\n    }\n}\n"
  },
  {
    "path": "tests/Search/EntitySearchTest.php",
    "content": "<?php\n\nnamespace Tests\\Search;\n\nuse BookStack\\Activity\\Models\\Tag;\nuse BookStack\\Entities\\Models\\Book;\nuse Tests\\TestCase;\n\nclass EntitySearchTest extends TestCase\n{\n    public function test_page_search()\n    {\n        $book = $this->entities->book();\n        $page = $book->pages->first();\n\n        $search = $this->asEditor()->get('/search?term=' . urlencode($page->name));\n        $search->assertSee('Search Results');\n        $search->assertSeeText($page->name, true);\n    }\n\n    public function test_bookshelf_search()\n    {\n        $shelf = $this->entities->shelf();\n\n        $search = $this->asEditor()->get('/search?term=' . urlencode($shelf->name) . '  {type:bookshelf}');\n        $search->assertSee('Search Results');\n        $search->assertSeeText($shelf->name, true);\n    }\n\n    public function test_search_shows_pagination()\n    {\n        $search = $this->asEditor()->get('/search?term=a');\n        $this->withHtml($search)->assertLinkExists(url('/search?term=a&page=2'), '2');\n    }\n\n    public function test_pagination_considers_sub_path_url_handling()\n    {\n        $this->runWithEnv(['APP_URL' => 'https://example.com/subpath'], function () {\n            $search = $this->asEditor()->get('https://example.com/search?term=a');\n            $this->withHtml($search)->assertLinkExists('https://example.com/subpath/search?term=a&page=2', '2');\n        });\n    }\n\n    public function test_invalid_page_search()\n    {\n        $resp = $this->asEditor()->get('/search?term=' . urlencode('<p>test</p>'));\n        $resp->assertSee('Search Results');\n        $resp->assertStatus(200);\n        $this->get('/search?term=cat+-')->assertStatus(200);\n    }\n\n    public function test_empty_search_shows_search_page()\n    {\n        $res = $this->asEditor()->get('/search');\n        $res->assertStatus(200);\n    }\n\n    public function test_searching_accents_and_small_terms()\n    {\n        $page = $this->entities->newPage(['name' => 'My new test quaffleachits', 'html' => 'some áéííúü¿¡ test content a2 orange dog']);\n        $this->asEditor();\n\n        $accentSearch = $this->get('/search?term=' . urlencode('áéíí'));\n        $accentSearch->assertStatus(200)->assertSee($page->name);\n\n        $smallSearch = $this->get('/search?term=' . urlencode('a2'));\n        $smallSearch->assertStatus(200)->assertSee($page->name);\n    }\n\n    public function test_book_search()\n    {\n        $book = Book::first();\n        $page = $book->pages->last();\n        $chapter = $book->chapters->last();\n\n        $pageTestResp = $this->asEditor()->get('/search/book/' . $book->id . '?term=' . urlencode($page->name));\n        $pageTestResp->assertSee($page->name);\n\n        $chapterTestResp = $this->asEditor()->get('/search/book/' . $book->id . '?term=' . urlencode($chapter->name));\n        $chapterTestResp->assertSee($chapter->name);\n    }\n\n    public function test_chapter_search()\n    {\n        $chapter = $this->entities->chapterHasPages();\n        $page = $chapter->pages[0];\n\n        $pageTestResp = $this->asEditor()->get('/search/chapter/' . $chapter->id . '?term=' . urlencode($page->name));\n        $pageTestResp->assertSee($page->name);\n    }\n\n    public function test_tag_search()\n    {\n        $newTags = [\n            new Tag([\n                'name'  => 'animal',\n                'value' => 'cat',\n            ]),\n            new Tag([\n                'name'  => 'color',\n                'value' => 'red',\n            ]),\n        ];\n\n        $pageA = $this->entities->page();\n        $pageA->tags()->saveMany($newTags);\n\n        $pageB = $this->entities->page();\n        $pageB->tags()->create(['name' => 'animal', 'value' => 'dog']);\n\n        $this->asEditor();\n        $tNameSearch = $this->get('/search?term=%5Banimal%5D');\n        $tNameSearch->assertSee($pageA->name)->assertSee($pageB->name);\n\n        $tNameSearch2 = $this->get('/search?term=%5Bcolor%5D');\n        $tNameSearch2->assertSee($pageA->name)->assertDontSee($pageB->name);\n\n        $tNameValSearch = $this->get('/search?term=%5Banimal%3Dcat%5D');\n        $tNameValSearch->assertSee($pageA->name)->assertDontSee($pageB->name);\n    }\n\n    public function test_exact_searches()\n    {\n        $page = $this->entities->newPage(['name' => 'My new test page', 'html' => 'this is a story about an orange donkey']);\n\n        $exactSearchA = $this->asEditor()->get('/search?term=' . urlencode('\"story about an orange\"'));\n        $exactSearchA->assertStatus(200)->assertSee($page->name);\n\n        $exactSearchB = $this->asEditor()->get('/search?term=' . urlencode('\"story not about an orange\"'));\n        $exactSearchB->assertStatus(200)->assertDontSee($page->name);\n    }\n\n    public function test_negated_searches()\n    {\n        $page = $this->entities->newPage(['name' => 'My new test negation page', 'html' => '<p>An angry tortoise wore trumpeted plimsoles</p>']);\n        $page->tags()->saveMany([new Tag(['name' => 'DonkCount', 'value' => '500'])]);\n        $page->created_by = $this->users->admin()->id;\n        $page->save();\n\n        $editor = $this->users->editor();\n        $this->actingAs($editor);\n\n        $exactSearch = $this->get('/search?term=' . urlencode('negation -\"tortoise\"'));\n        $exactSearch->assertStatus(200)->assertDontSeeText($page->name);\n\n        $tagSearchA = $this->get('/search?term=' . urlencode('negation [DonkCount=500]'));\n        $tagSearchA->assertStatus(200)->assertSeeText($page->name);\n        $tagSearchB = $this->get('/search?term=' . urlencode('negation -[DonkCount=500]'));\n        $tagSearchB->assertStatus(200)->assertDontSeeText($page->name);\n\n        $filterSearchA = $this->get('/search?term=' . urlencode('negation -{created_by:me}'));\n        $filterSearchA->assertStatus(200)->assertSeeText($page->name);\n        $page->created_by = $editor->id;\n        $page->save();\n        $filterSearchB = $this->get('/search?term=' . urlencode('negation -{created_by:me}'));\n        $filterSearchB->assertStatus(200)->assertDontSeeText($page->name);\n    }\n\n    public function test_search_terms_with_delimiters_are_converted_to_exact_matches()\n    {\n        $this->asEditor();\n        $page = $this->entities->newPage(['name' => 'Delimiter test', 'html' => '<p>1.1 2,2 3?3 4:4 5;5 (8) &lt;9&gt; \"10\" \\'11\\' `12`</p>']);\n        $terms = explode(' ', '1.1 2,2 3?3 4:4 5;5 (8) <9> \"10\" \\'11\\' `12`');\n\n        foreach ($terms as $term) {\n            $search = $this->get('/search?term=' . urlencode($term));\n            $search->assertSee($page->name);\n        }\n    }\n\n    public function test_search_filters()\n    {\n        $page = $this->entities->newPage(['name' => 'My new test quaffleachits', 'html' => 'this is about an orange donkey danzorbhsing']);\n        $editor = $this->users->editor();\n        $this->actingAs($editor);\n\n        // Viewed filter searches\n        $this->get('/search?term=' . urlencode('danzorbhsing {not_viewed_by_me}'))->assertSee($page->name);\n        $this->get('/search?term=' . urlencode('danzorbhsing {viewed_by_me}'))->assertDontSee($page->name);\n        $this->get($page->getUrl());\n        $this->get('/search?term=' . urlencode('danzorbhsing {not_viewed_by_me}'))->assertDontSee($page->name);\n        $this->get('/search?term=' . urlencode('danzorbhsing {viewed_by_me}'))->assertSee($page->name);\n\n        // User filters\n        $this->get('/search?term=' . urlencode('danzorbhsing {created_by:me}'))->assertDontSee($page->name);\n        $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertDontSee($page->name);\n        $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertDontSee($page->name);\n        $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:' . $editor->slug . '}'))->assertDontSee($page->name);\n        $page->created_by = $editor->id;\n        $page->save();\n        $this->get('/search?term=' . urlencode('danzorbhsing {created_by:me}'))->assertSee($page->name);\n        $this->get('/search?term=' . urlencode('danzorbhsing {created_by: ' . $editor->slug . '}'))->assertSee($page->name);\n        $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertDontSee($page->name);\n        $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertDontSee($page->name);\n        $page->updated_by = $editor->id;\n        $page->save();\n        $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertSee($page->name);\n        $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:' . $editor->slug . '}'))->assertSee($page->name);\n        $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertDontSee($page->name);\n        $page->owned_by = $editor->id;\n        $page->save();\n        $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertSee($page->name);\n        $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:' . $editor->slug . '}'))->assertSee($page->name);\n\n        // Content filters\n        $this->get('/search?term=' . urlencode('{in_name:danzorbhsing}'))->assertDontSee($page->name);\n        $this->get('/search?term=' . urlencode('{in_body:danzorbhsing}'))->assertSee($page->name);\n        $this->get('/search?term=' . urlencode('{in_name:test quaffleachits}'))->assertSee($page->name);\n        $this->get('/search?term=' . urlencode('{in_body:test quaffleachits}'))->assertDontSee($page->name);\n\n        // Restricted filter\n        $this->get('/search?term=' . urlencode('danzorbhsing {is_restricted}'))->assertDontSee($page->name);\n        $this->permissions->setEntityPermissions($page, ['view'], [$editor->roles->first()]);\n        $this->get('/search?term=' . urlencode('danzorbhsing {is_restricted}'))->assertSee($page->name);\n\n        // Date filters\n        $this->get('/search?term=' . urlencode('danzorbhsing {updated_after:2037-01-01}'))->assertDontSee($page->name);\n        $this->get('/search?term=' . urlencode('danzorbhsing {updated_before:2037-01-01}'))->assertSee($page->name);\n        $page->updated_at = '2037-02-01';\n        $page->save();\n        $this->get('/search?term=' . urlencode('danzorbhsing {updated_after:2037-01-01}'))->assertSee($page->name);\n        $this->get('/search?term=' . urlencode('danzorbhsing {updated_before:2037-01-01}'))->assertDontSee($page->name);\n\n        $this->get('/search?term=' . urlencode('danzorbhsing {created_after:2037-01-01}'))->assertDontSee($page->name);\n        $this->get('/search?term=' . urlencode('danzorbhsing {created_before:2037-01-01}'))->assertSee($page->name);\n        $page->created_at = '2037-02-01';\n        $page->save();\n        $this->get('/search?term=' . urlencode('danzorbhsing {created_after:2037-01-01}'))->assertSee($page->name);\n        $this->get('/search?term=' . urlencode('danzorbhsing {created_before:2037-01-01}'))->assertDontSee($page->name);\n    }\n\n    public function test_entity_selector_search()\n    {\n        $page = $this->entities->newPage(['name' => 'my ajax search test', 'html' => 'ajax test']);\n        $notVisitedPage = $this->entities->page();\n\n        // Visit the page to make popular\n        $this->asEditor()->get($page->getUrl());\n\n        $normalSearch = $this->get('/search/entity-selector?term=' . urlencode($page->name));\n        $normalSearch->assertSee($page->name);\n\n        $bookSearch = $this->get('/search/entity-selector?types=book&term=' . urlencode($page->name));\n        $bookSearch->assertDontSee($page->name);\n\n        $defaultListTest = $this->get('/search/entity-selector');\n        $defaultListTest->assertSee($page->name);\n        $defaultListTest->assertDontSee($notVisitedPage->name);\n    }\n\n    public function test_entity_selector_search_shows_breadcrumbs()\n    {\n        $chapter = $this->entities->chapter();\n        $page = $chapter->pages->first();\n        $this->asEditor();\n\n        $pageSearch = $this->get('/search/entity-selector?term=' . urlencode($page->name));\n        $pageSearch->assertSee($page->name);\n        $pageSearch->assertSee($chapter->getShortName(42));\n        $pageSearch->assertSee($page->book->getShortName(42));\n\n        $chapterSearch = $this->get('/search/entity-selector?term=' . urlencode($chapter->name));\n        $chapterSearch->assertSee($chapter->name);\n        $chapterSearch->assertSee($chapter->book->getShortName(42));\n    }\n\n    public function test_entity_selector_shows_breadcrumbs_on_default_view()\n    {\n        $page = $this->entities->pageWithinChapter();\n        $this->asEditor()->get($page->chapter->getUrl());\n\n        $resp = $this->asEditor()->get('/search/entity-selector?types=book,chapter&permission=page-create');\n        $html = $this->withHtml($resp);\n        $html->assertElementContains('.chapter.entity-list-item', $page->chapter->name);\n        $html->assertElementContains('.chapter.entity-list-item .entity-item-snippet', $page->book->getShortName(42));\n    }\n\n    public function test_entity_selector_search_reflects_items_without_permission()\n    {\n        $page = $this->entities->page();\n        $baseSelector = 'a[data-entity-type=\"page\"][data-entity-id=\"' . $page->id . '\"]';\n        $searchUrl = '/search/entity-selector?permission=update&term=' . urlencode($page->name);\n\n        $resp = $this->asEditor()->get($searchUrl);\n        $this->withHtml($resp)->assertElementContains($baseSelector, $page->name);\n        $this->withHtml($resp)->assertElementNotContains($baseSelector, \"You don't have the required permissions to select this item\");\n\n        $resp = $this->actingAs($this->users->viewer())->get($searchUrl);\n        $this->withHtml($resp)->assertElementContains($baseSelector, $page->name);\n        $this->withHtml($resp)->assertElementContains($baseSelector, \"You don't have the required permissions to select this item\");\n    }\n\n    public function test_entity_template_selector_search()\n    {\n        $templatePage = $this->entities->newPage(['name' => 'Template search test', 'html' => 'template test']);\n        $templatePage->template = true;\n        $templatePage->save();\n\n        $nonTemplatePage = $this->entities->newPage(['name' => 'Nontemplate page', 'html' => 'nontemplate', 'template' => false]);\n\n        // Visit both to make popular\n        $this->asEditor()->get($templatePage->getUrl());\n        $this->get($nonTemplatePage->getUrl());\n\n        $normalSearch = $this->get('/search/entity-selector-templates?term=test');\n        $normalSearch->assertSee($templatePage->name);\n        $normalSearch->assertDontSee($nonTemplatePage->name);\n\n        $normalSearch = $this->get('/search/entity-selector-templates?term=beans');\n        $normalSearch->assertDontSee($templatePage->name);\n        $normalSearch->assertDontSee($nonTemplatePage->name);\n\n        $defaultListTest = $this->get('/search/entity-selector-templates');\n        $defaultListTest->assertSee($templatePage->name);\n        $defaultListTest->assertDontSee($nonTemplatePage->name);\n\n        $this->permissions->disableEntityInheritedPermissions($templatePage);\n\n        $normalSearch = $this->get('/search/entity-selector-templates?term=test');\n        $normalSearch->assertDontSee($templatePage->name);\n\n        $defaultListTest = $this->get('/search/entity-selector-templates');\n        $defaultListTest->assertDontSee($templatePage->name);\n    }\n\n    public function test_search_works_on_updated_page_content()\n    {\n        $page = $this->entities->page();\n        $this->asEditor();\n\n        $update = $this->put($page->getUrl(), [\n            'name' => $page->name,\n            'html' => '<p>dog pandabearmonster spaghetti</p>',\n        ]);\n\n        $search = $this->asEditor()->get('/search?term=pandabearmonster');\n        $search->assertStatus(200);\n        $search->assertSeeText($page->name);\n        $search->assertSee($page->getUrl());\n    }\n\n    public function test_search_ranks_common_words_lower()\n    {\n        $this->entities->newPage(['name' => 'Test page A', 'html' => '<p>dog biscuit dog dog</p>']);\n        $this->entities->newPage(['name' => 'Test page B', 'html' => '<p>cat biscuit</p>']);\n\n        $search = $this->asEditor()->get('/search?term=cat+dog+biscuit');\n        $this->withHtml($search)->assertElementContains('.entity-list > .page:nth-child(1)', 'Test page A');\n        $this->withHtml($search)->assertElementContains('.entity-list > .page:nth-child(2)', 'Test page B');\n\n        for ($i = 0; $i < 2; $i++) {\n            $this->entities->newPage(['name' => 'Test page ' . $i, 'html' => '<p>dog</p>']);\n        }\n\n        $search = $this->asEditor()->get('/search?term=cat+dog+biscuit');\n        $this->withHtml($search)->assertElementContains('.entity-list > .page:nth-child(1)', 'Test page B');\n        $this->withHtml($search)->assertElementContains('.entity-list > .page:nth-child(2)', 'Test page A');\n    }\n\n    public function test_matching_terms_in_search_results_are_highlighted()\n    {\n        $this->entities->newPage(['name' => 'My Meowie Cat', 'html' => '<p>A superimportant page about meowieable animals</p>', 'tags' => [\n            ['name' => 'Animal', 'value' => 'MeowieCat'],\n            ['name' => 'SuperImportant'],\n        ]]);\n\n        $search = $this->asEditor()->get('/search?term=SuperImportant+Meowie');\n        // Title\n        $search->assertSee('My <strong>Meowie</strong> Cat', false);\n        // Content\n        $search->assertSee('A <strong>superimportant</strong> page about <strong>meowie</strong>able animals', false);\n        // Tag name\n        $this->withHtml($search)->assertElementContains('.tag-name.highlight', 'SuperImportant');\n        // Tag value\n        $this->withHtml($search)->assertElementContains('.tag-value.highlight', 'MeowieCat');\n    }\n\n    public function test_match_highlighting_works_with_multibyte_content()\n    {\n        $this->entities->newPage([\n            'name' => 'Test Page',\n            'html' => '<p>На мен ми трябва нещо добро test</p>',\n        ]);\n\n        $search = $this->asEditor()->get('/search?term=' . urlencode('На мен ми трябва нещо добро'));\n        $search->assertSee('<strong>На</strong> <strong>мен</strong> <strong>ми</strong> <strong>трябва</strong> <strong>нещо</strong> <strong>добро</strong> test', false);\n    }\n\n    public function test_match_highlighting_is_efficient_with_large_frequency_in_content()\n    {\n        $content = str_repeat('superbeans ', 10000);\n        $this->entities->newPage([\n            'name' => 'Test Page',\n            'html' => \"<p>{$content}</p>\",\n        ]);\n\n        $time = microtime(true);\n        $resp = $this->asEditor()->get('/search?term=' . urlencode('superbeans'));\n        $this->assertLessThan(0.5, microtime(true) - $time);\n\n        $resp->assertSee('<strong>superbeans</strong>', false);\n    }\n\n    public function test_html_entities_in_item_details_remains_escaped_in_search_results()\n    {\n        $this->entities->newPage(['name' => 'My <cool> TestPageContent', 'html' => '<p>My supercool &lt;great&gt; TestPageContent page</p>']);\n\n        $search = $this->asEditor()->get('/search?term=TestPageContent');\n        $search->assertSee('My &lt;cool&gt; <strong>TestPageContent</strong>', false);\n        $search->assertSee('My supercool &lt;great&gt; <strong>TestPageContent</strong> page', false);\n    }\n\n    public function test_words_adjacent_to_lines_breaks_can_be_matched_with_normal_terms()\n    {\n        $page = $this->entities->newPage(['name' => 'TermA', 'html' => '\n            <p>TermA<br>TermB<br>TermC</p>\n        ']);\n\n        $search = $this->asEditor()->get('/search?term=' . urlencode('TermB TermC'));\n\n        $search->assertSee($page->getUrl(), false);\n    }\n\n    public function test_backslashes_can_be_searched_upon()\n    {\n        $page = $this->entities->newPage(['name' => 'TermA', 'html' => '\n            <p>More info is at the path \\\\\\\\cat\\\\dog\\\\badger</p>\n        ']);\n        $page->tags()->save(new Tag(['name' => '\\\\Category', 'value' => '\\\\animals\\\\fluffy']));\n\n        $search = $this->asEditor()->get('/search?term=' . urlencode('\\\\\\\\cat\\\\dog'));\n        $search->assertSee($page->getUrl(), false);\n\n        $search = $this->asEditor()->get('/search?term=' . urlencode('\"\\\\dog\\\\\\\\\"'));\n        $search->assertSee($page->getUrl(), false);\n\n        $search = $this->asEditor()->get('/search?term=' . urlencode('\"\\\\badger\\\\\\\\\"'));\n        $search->assertDontSee($page->getUrl(), false);\n\n        $search = $this->asEditor()->get('/search?term=' . urlencode('[\\\\Categorylike%\\\\fluffy]'));\n        $search->assertSee($page->getUrl(), false);\n    }\n\n    public function test_searches_with_terms_without_controls_includes_them_in_extras()\n    {\n        $resp = $this->asEditor()->get('/search?term=' . urlencode('test {updated_by:dan} {created_by:dan} -{viewed_by_me} -[a=b] -\"dog\" {is_template} {sort_by:last_commented}'));\n        $this->withHtml($resp)->assertFieldHasValue('extras', '{updated_by:dan} {created_by:dan} {is_template} {sort_by:last_commented} -\"dog\" -[a=b] -{viewed_by_me}');\n    }\n\n    public function test_negated_searches_dont_show_in_inputs()\n    {\n        $resp = $this->asEditor()->get('/search?term=' . urlencode('-{created_by:me} -[a=b] -\"dog\"'));\n        $this->withHtml($resp)->assertElementNotExists('input[name=\"tags[]\"][value=\"a=b\"]');\n        $this->withHtml($resp)->assertElementNotExists('input[name=\"exact[]\"][value=\"dog\"]');\n        $this->withHtml($resp)->assertElementNotExists('input[name=\"filters[created_by]\"][value=\"me\"][checked=\"checked\"]');\n    }\n\n    public function test_searches_with_user_filters_using_me_adds_them_into_advanced_search_form()\n    {\n        $resp = $this->asEditor()->get('/search?term=' . urlencode('test {updated_by:me} {created_by:me}'));\n        $this->withHtml($resp)->assertElementExists('form input[name=\"filters[updated_by]\"][value=\"me\"][checked=\"checked\"]');\n        $this->withHtml($resp)->assertElementExists('form input[name=\"filters[created_by]\"][value=\"me\"][checked=\"checked\"]');\n    }\n\n    public function test_search_suggestion_endpoint()\n    {\n        $this->entities->newPage(['name' => 'My suggestion page', 'html' => '<p>My supercool suggestion page</p>']);\n\n        // Test specific search\n        $resp = $this->asEditor()->get('/search/suggest?term=\"supercool+suggestion\"');\n        $resp->assertSee('My suggestion page');\n        $resp->assertDontSee('My supercool suggestion page');\n        $resp->assertDontSee('No items available');\n        $this->withHtml($resp)->assertElementCount('a', 1);\n\n        // Test search limit\n        $resp = $this->asEditor()->get('/search/suggest?term=et');\n        $this->withHtml($resp)->assertElementCount('a', 5);\n\n        // Test empty state\n        $resp = $this->asEditor()->get('/search/suggest?term=spaghettisaurusrex');\n        $this->withHtml($resp)->assertElementCount('a', 0);\n        $resp->assertSee('No items available');\n    }\n}\n"
  },
  {
    "path": "tests/Search/SearchIndexingTest.php",
    "content": "<?php\n\nnamespace Tests\\Search;\n\nuse Tests\\TestCase;\n\nclass SearchIndexingTest extends TestCase\n{\n    public function test_terms_in_headers_have_an_adjusted_index_score()\n    {\n        $page = $this->entities->newPage(['name' => 'Test page A', 'html' => '\n            <p>TermA</p>\n            <h1>TermB <strong>TermNested</strong></h1>\n            <h2>TermC</h2>\n            <h3>TermD</h3>\n            <h4>TermE</h4>\n            <h5>TermF</h5>\n            <h6>TermG</h6>\n        ']);\n\n        $scoreByTerm = $page->searchTerms()->pluck('score', 'term');\n\n        $this->assertEquals(1, $scoreByTerm->get('TermA'));\n        $this->assertEquals(10, $scoreByTerm->get('TermB'));\n        $this->assertEquals(10, $scoreByTerm->get('TermNested'));\n        $this->assertEquals(5, $scoreByTerm->get('TermC'));\n        $this->assertEquals(4, $scoreByTerm->get('TermD'));\n        $this->assertEquals(3, $scoreByTerm->get('TermE'));\n        $this->assertEquals(2, $scoreByTerm->get('TermF'));\n        // Is 1.5 but stored as integer, rounding up\n        $this->assertEquals(2, $scoreByTerm->get('TermG'));\n    }\n\n    public function test_indexing_works_as_expected_for_page_with_lots_of_terms()\n    {\n        $this->markTestSkipped('Time consuming test');\n\n        $count = 100000;\n        $text = '';\n        $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_#';\n        for ($i = 0; $i < $count; $i++) {\n            $text .= substr(str_shuffle($chars), 0, 5) . ' ';\n        }\n\n        $page = $this->entities->newPage(['name' => 'Test page A', 'html' => '<p>' . $text . '</p>']);\n\n        $termCount = $page->searchTerms()->count();\n\n        // Expect at least 90% unique rate\n        $this->assertGreaterThan($count * 0.9, $termCount);\n    }\n\n    public function test_name_and_content_terms_are_merged_to_single_score()\n    {\n        $page = $this->entities->newPage(['name' => 'TermA', 'html' => '\n            <p>TermA</p>\n        ']);\n\n        $scoreByTerm = $page->searchTerms()->pluck('score', 'term');\n\n        // Scores 40 for being in the name then 1 for being in the content\n        $this->assertEquals(41, $scoreByTerm->get('TermA'));\n    }\n\n    public function test_tag_names_and_values_are_indexed_for_search()\n    {\n        $page = $this->entities->newPage(['name' => 'PageA', 'html' => '<p>content</p>', 'tags' => [\n            ['name' => 'Animal', 'value' => 'MeowieCat'],\n            ['name' => 'SuperImportant'],\n        ]]);\n\n        $scoreByTerm = $page->searchTerms()->pluck('score', 'term');\n        $this->assertEquals(5, $scoreByTerm->get('MeowieCat'));\n        $this->assertEquals(3, $scoreByTerm->get('Animal'));\n        $this->assertEquals(3, $scoreByTerm->get('SuperImportant'));\n    }\n\n    public function test_terms_containing_guillemets_handled()\n    {\n        $page = $this->entities->newPage(['html' => '<p>«Hello there» and « there »</p>']);\n\n        $scoreByTerm = $page->searchTerms()->pluck('score', 'term');\n        $expected = ['Hello', 'there', 'and'];\n        foreach ($expected as $term) {\n            $this->assertNotNull($scoreByTerm->get($term), \"Failed asserting that \\\"$term\\\" is indexed\");\n        }\n\n        $nonExpected = ['«', '»'];\n        foreach ($nonExpected as $term) {\n            $this->assertNull($scoreByTerm->get($term), \"Failed asserting that \\\"$term\\\" is not indexed\");\n        }\n    }\n\n    public function test_terms_containing_punctuation_within_retain_original_form_and_split_form_in_index()\n    {\n        $page = $this->entities->newPage(['html' => '<p>super.duper awesome-beans big- barry cheese.</p><p>biscuits</p><p>a-bs</p>']);\n\n        $scoreByTerm = $page->searchTerms()->pluck('score', 'term');\n        $expected = ['super', 'duper', 'super.duper', 'awesome-beans', 'awesome', 'beans', 'big', 'barry', 'cheese', 'biscuits', 'a-bs', 'a', 'bs'];\n        foreach ($expected as $term) {\n            $this->assertNotNull($scoreByTerm->get($term), \"Failed asserting that \\\"$term\\\" is indexed\");\n        }\n\n        $nonExpected = ['big-', 'big-barry', 'cheese.', 'cheese.biscuits'];\n        foreach ($nonExpected as $term) {\n            $this->assertNull($scoreByTerm->get($term), \"Failed asserting that \\\"$term\\\" is not indexed\");\n        }\n    }\n\n    public function test_non_breaking_spaces_handled_as_spaces()\n    {\n        $page = $this->entities->newPage(['html' => '<p>a&nbsp;tigerbadger is a dangerous&nbsp;animal</p>']);\n\n        $scoreByTerm = $page->searchTerms()->pluck('score', 'term');\n        $this->assertNotNull($scoreByTerm->get('tigerbadger'));\n        $this->assertNotNull($scoreByTerm->get('dangerous'));\n        $this->assertNotNull($scoreByTerm->get('animal'));\n    }\n}\n"
  },
  {
    "path": "tests/Search/SearchOptionsTest.php",
    "content": "<?php\n\nnamespace Tests\\Search;\n\nuse BookStack\\Search\\Options\\ExactSearchOption;\nuse BookStack\\Search\\Options\\FilterSearchOption;\nuse BookStack\\Search\\Options\\TagSearchOption;\nuse BookStack\\Search\\Options\\TermSearchOption;\nuse BookStack\\Search\\SearchOptions;\nuse BookStack\\Search\\SearchOptionSet;\nuse Illuminate\\Http\\Request;\nuse Tests\\TestCase;\n\nclass SearchOptionsTest extends TestCase\n{\n    public function test_from_string_parses_a_search_string_properly()\n    {\n        $options = SearchOptions::fromString('cat \"dog\" [tag=good] {is_tree}');\n\n        $this->assertEquals(['cat'], $options->searches->toValueArray());\n        $this->assertEquals(['dog'], $options->exacts->toValueArray());\n        $this->assertEquals(['tag=good'], $options->tags->toValueArray());\n        $this->assertEquals(['is_tree' => ''], $options->filters->toValueMap());\n    }\n\n    public function test_from_string_parses_negations()\n    {\n        $options = SearchOptions::fromString('cat -\"dog\" -[tag=good] -{is_tree}');\n\n        $this->assertEquals(['cat'], $options->searches->toValueArray());\n        $this->assertTrue($options->exacts->all()[0]->negated);\n        $this->assertTrue($options->tags->all()[0]->negated);\n        $this->assertTrue($options->filters->all()[0]->negated);\n    }\n\n    public function test_from_string_properly_parses_escaped_quotes()\n    {\n        $options = SearchOptions::fromString('\"\\\"cat\\\"\" surprise');\n        $this->assertEquals(['\"cat\"'], $options->exacts->toValueArray());\n\n        $options = SearchOptions::fromString('\"\\\"\\\"\" \"\\\"donkey\"');\n        $this->assertEquals(['\"\"', '\"donkey'], $options->exacts->toValueArray());\n\n        $options = SearchOptions::fromString('\"\\\"\" \"\\\\\\\\\"');\n        $this->assertEquals(['\"', '\\\\'], $options->exacts->toValueArray());\n    }\n\n    public function test_to_string_includes_all_items_in_the_correct_format()\n    {\n        $expected = 'cat \"dog\" [tag=good] {is_tree} {beans:valid}';\n        $options = new SearchOptions();\n        $options->searches = SearchOptionSet::fromValueArray(['cat'], TermSearchOption::class);\n        $options->exacts = SearchOptionSet::fromValueArray(['dog'], ExactSearchOption::class);\n        $options->tags = SearchOptionSet::fromValueArray(['tag=good'], TagSearchOption::class);\n        $options->filters = new SearchOptionSet([\n            new FilterSearchOption('', 'is_tree'),\n            new FilterSearchOption('valid', 'beans'),\n        ]);\n\n        $output = $options->toString();\n        foreach (explode(' ', $expected) as $term) {\n            $this->assertStringContainsString($term, $output);\n        }\n    }\n\n    public function test_to_string_handles_negations_as_expected()\n    {\n        $expected = 'cat -\"dog\" -[tag=good] -{is_tree}';\n        $options = new SearchOptions();\n        $options->searches = new SearchOptionSet([new TermSearchOption('cat')]);\n        $options->exacts = new SearchOptionSet([new ExactSearchOption('dog', true)]);\n        $options->tags = new SearchOptionSet([new TagSearchOption('tag=good', true)]);\n        $options->filters = new SearchOptionSet([\n            new FilterSearchOption('', 'is_tree', true),\n        ]);\n\n        $output = $options->toString();\n        foreach (explode(' ', $expected) as $term) {\n            $this->assertStringContainsString($term, $output);\n        }\n    }\n\n    public function test_to_string_escapes_as_expected()\n    {\n        $options = new SearchOptions();\n        $options->exacts = SearchOptionSet::fromValueArray(['\"cat\"', '\"\"', '\"donkey', '\"', '\\\\', '\\\\\"'], ExactSearchOption::class);\n\n        $output = $options->toString();\n        $this->assertEquals('\"\\\"cat\\\"\" \"\\\"\\\"\" \"\\\"donkey\" \"\\\"\" \"\\\\\\\\\" \"\\\\\\\\\\\"\"', $output);\n    }\n\n    public function test_correct_filter_values_are_set_from_string()\n    {\n        $opts = SearchOptions::fromString('{is_tree} {name:dan} {cat:happy}');\n\n        $this->assertEquals([\n            'is_tree' => '',\n            'name'    => 'dan',\n            'cat'     => 'happy',\n        ], $opts->filters->toValueMap());\n    }\n    public function test_it_cannot_parse_out_empty_exacts()\n    {\n        $options = SearchOptions::fromString('\"\" test \"\"');\n\n        $this->assertEmpty($options->exacts->toValueArray());\n        $this->assertCount(1, $options->searches->toValueArray());\n    }\n\n    public function test_from_request_properly_parses_exacts_from_search_terms()\n    {\n        $this->asEditor();\n        $request = new Request([\n            'search' => 'biscuits \"cheese\" \"\" \"baked beans\"'\n        ]);\n\n        $options = SearchOptions::fromRequest($request);\n        $this->assertEquals([\"biscuits\"], $options->searches->toValueArray());\n        $this->assertEquals(['\"cheese\"', '\"\"', '\"baked',  'beans\"'], $options->exacts->toValueArray());\n    }\n\n    public function test_from_request_properly_parses_provided_types()\n    {\n        $request = new Request([\n            'search' => '',\n            'types' => ['page', 'book'],\n        ]);\n\n        $options = SearchOptions::fromRequest($request);\n        $filters = $options->filters->toValueMap();\n        $this->assertCount(1, $filters);\n        $this->assertEquals('page|book', $filters['type'] ?? 'notfound');\n    }\n\n    public function test_from_request_properly_parses_out_extras_as_string()\n    {\n        $request = new Request([\n            'search' => '',\n            'tags' => ['a=b'],\n            'extras' => '-[b=c] -{viewed_by_me} -\"dino\"'\n        ]);\n\n        $options = SearchOptions::fromRequest($request);\n        $this->assertCount(2, $options->tags->all());\n        $this->assertEquals('b=c', $options->tags->negated()->all()[0]->value);\n        $this->assertEquals('viewed_by_me', $options->filters->all()[0]->getKey());\n        $this->assertTrue($options->filters->all()[0]->negated);\n        $this->assertEquals('dino', $options->exacts->all()[0]->value);\n        $this->assertTrue($options->exacts->all()[0]->negated);\n    }\n\n    public function test_from_string_results_are_count_limited_and_larger_for_logged_in_users()\n    {\n        $terms = [\n            ...array_fill(0, 40, 'cat'),\n            ...array_fill(0, 50, '\"bees\"'),\n            ...array_fill(0, 50, '{is_template}'),\n            ...array_fill(0, 50, '[a=b]'),\n        ];\n\n        $options = SearchOptions::fromString(implode(' ', $terms));\n\n        $this->assertCount(5, $options->searches->all());\n        $this->assertCount(2, $options->exacts->all());\n        $this->assertCount(4, $options->tags->all());\n        $this->assertCount(5, $options->filters->all());\n\n        $this->asEditor();\n        $options = SearchOptions::fromString(implode(' ', $terms));\n\n        $this->assertCount(10, $options->searches->all());\n        $this->assertCount(4, $options->exacts->all());\n        $this->assertCount(8, $options->tags->all());\n        $this->assertCount(10, $options->filters->all());\n    }\n\n    public function test_from_request_results_are_count_limited_and_larger_for_logged_in_users()\n    {\n        $request = new Request([\n            'search' => str_repeat('hello ', 20),\n            'tags' => array_fill(0, 20, 'a=b'),\n            'extras' => str_repeat('-[b=c] -{viewed_by_me} -\"dino\"', 20),\n        ]);\n\n        $options = SearchOptions::fromRequest($request);\n\n        $this->assertCount(5, $options->searches->all());\n        $this->assertCount(2, $options->exacts->all());\n        $this->assertCount(4, $options->tags->all());\n        $this->assertCount(5, $options->filters->all());\n\n        $this->asEditor();\n        $options = SearchOptions::fromRequest($request);\n\n        $this->assertCount(10, $options->searches->all());\n        $this->assertCount(4, $options->exacts->all());\n        $this->assertCount(8, $options->tags->all());\n        $this->assertCount(10, $options->filters->all());\n    }\n}\n"
  },
  {
    "path": "tests/Search/SiblingSearchTest.php",
    "content": "<?php\n\nnamespace Tests\\Search;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse Tests\\TestCase;\n\nclass SiblingSearchTest extends TestCase\n{\n    public function test_sibling_search_for_pages()\n    {\n        $chapter = $this->entities->chapterHasPages();\n        $this->assertGreaterThan(2, count($chapter->pages), 'Ensure we\\'re testing with at least 1 sibling');\n        $page = $chapter->pages->first();\n\n        $search = $this->actingAs($this->users->viewer())->get(\"/search/entity/siblings?entity_id={$page->id}&entity_type=page\");\n        $search->assertSuccessful();\n        foreach ($chapter->pages as $page) {\n            $search->assertSee($page->name);\n        }\n\n        $search->assertDontSee($chapter->name);\n    }\n\n    public function test_sibling_search_for_pages_without_chapter()\n    {\n        $page = $this->entities->pageNotWithinChapter();\n        $bookChildren = $page->book->getDirectVisibleChildren();\n        $this->assertGreaterThan(2, count($bookChildren), 'Ensure we\\'re testing with at least 1 sibling');\n\n        $search = $this->actingAs($this->users->viewer())->get(\"/search/entity/siblings?entity_id={$page->id}&entity_type=page\");\n        $search->assertSuccessful();\n        foreach ($bookChildren as $child) {\n            $search->assertSee($child->name);\n        }\n\n        $search->assertDontSee($page->book->name);\n    }\n\n    public function test_sibling_search_for_chapters()\n    {\n        $chapter = $this->entities->chapter();\n        $bookChildren = $chapter->book->getDirectVisibleChildren();\n        $this->assertGreaterThan(2, count($bookChildren), 'Ensure we\\'re testing with at least 1 sibling');\n\n        $search = $this->actingAs($this->users->viewer())->get(\"/search/entity/siblings?entity_id={$chapter->id}&entity_type=chapter\");\n        $search->assertSuccessful();\n        foreach ($bookChildren as $child) {\n            $search->assertSee($child->name);\n        }\n\n        $search->assertDontSee($chapter->book->name);\n    }\n\n    public function test_sibling_search_for_books()\n    {\n        $books = Book::query()->take(10)->get();\n        $book = $books->first();\n        $this->assertGreaterThan(2, count($books), 'Ensure we\\'re testing with at least 1 sibling');\n\n        $search = $this->actingAs($this->users->viewer())->get(\"/search/entity/siblings?entity_id={$book->id}&entity_type=book\");\n        $search->assertSuccessful();\n        foreach ($books as $expectedBook) {\n            $search->assertSee($expectedBook->name);\n        }\n    }\n\n    public function test_sibling_search_for_shelves()\n    {\n        $shelves = Bookshelf::query()->take(10)->get();\n        $shelf = $shelves->first();\n        $this->assertGreaterThan(2, count($shelves), 'Ensure we\\'re testing with at least 1 sibling');\n\n        $search = $this->actingAs($this->users->viewer())->get(\"/search/entity/siblings?entity_id={$shelf->id}&entity_type=bookshelf\");\n        $search->assertSuccessful();\n        foreach ($shelves as $expectedShelf) {\n            $search->assertSee($expectedShelf->name);\n        }\n    }\n\n    public function test_sibling_search_for_books_provides_results_in_alphabetical_order()\n    {\n        $contextBook = $this->entities->book();\n        $searchBook = $this->entities->book();\n\n        $searchBook->name = 'Zebras';\n        $searchBook->save();\n\n        $search = $this->actingAs($this->users->viewer())->get(\"/search/entity/siblings?entity_id={$contextBook->id}&entity_type=book\");\n        $this->withHtml($search)->assertElementNotContains('a:first-child', 'Zebras');\n\n        $searchBook->name = '1AAAAAAArdvarks';\n        $searchBook->save();\n\n        $search = $this->actingAs($this->users->viewer())->get(\"/search/entity/siblings?entity_id={$contextBook->id}&entity_type=book\");\n        $this->withHtml($search)->assertElementContains('a:first-child', '1AAAAAAArdvarks');\n    }\n\n    public function test_sibling_search_for_shelves_provides_results_in_alphabetical_order()\n    {\n        $contextShelf = $this->entities->shelf();\n        $searchShelf = $this->entities->shelf();\n\n        $searchShelf->name = 'Zebras';\n        $searchShelf->save();\n\n        $search = $this->actingAs($this->users->viewer())->get(\"/search/entity/siblings?entity_id={$contextShelf->id}&entity_type=bookshelf\");\n        $this->withHtml($search)->assertElementNotContains('a:first-child', 'Zebras');\n\n        $searchShelf->name = '1AAAAAAArdvarks';\n        $searchShelf->save();\n\n        $search = $this->actingAs($this->users->viewer())->get(\"/search/entity/siblings?entity_id={$contextShelf->id}&entity_type=bookshelf\");\n        $this->withHtml($search)->assertElementContains('a:first-child', '1AAAAAAArdvarks');\n    }\n}\n"
  },
  {
    "path": "tests/SecurityHeaderTest.php",
    "content": "<?php\n\nnamespace Tests;\n\nuse BookStack\\Util\\CspService;\nuse Illuminate\\Testing\\TestResponse;\n\nclass SecurityHeaderTest extends TestCase\n{\n    public function test_cookies_samesite_lax_by_default()\n    {\n        $resp = $this->get('/');\n        foreach ($resp->headers->getCookies() as $cookie) {\n            $this->assertEquals('lax', $cookie->getSameSite());\n        }\n    }\n\n    public function test_cookies_samesite_none_when_iframe_hosts_set()\n    {\n        $this->runWithEnv(['ALLOWED_IFRAME_HOSTS' => 'http://example.com'], function () {\n            $resp = $this->get('/');\n            foreach ($resp->headers->getCookies() as $cookie) {\n                $this->assertEquals('none', $cookie->getSameSite());\n            }\n        });\n    }\n\n    public function test_secure_cookies_controlled_by_app_url()\n    {\n        $this->runWithEnv(['APP_URL' => 'http://example.com'], function () {\n            $resp = $this->get('/');\n            foreach ($resp->headers->getCookies() as $cookie) {\n                $this->assertFalse($cookie->isSecure());\n            }\n        });\n\n        $this->runWithEnv(['APP_URL' => 'https://example.com'], function () {\n            $resp = $this->get('/');\n            foreach ($resp->headers->getCookies() as $cookie) {\n                $this->assertTrue($cookie->isSecure());\n            }\n        });\n    }\n\n    public function test_iframe_csp_self_only_by_default()\n    {\n        $resp = $this->get('/');\n        $frameHeader = $this->getCspHeader($resp, 'frame-ancestors');\n\n        $this->assertEquals('frame-ancestors \\'self\\'', $frameHeader);\n    }\n\n    public function test_iframe_csp_includes_extra_hosts_if_configured()\n    {\n        $this->runWithEnv(['ALLOWED_IFRAME_HOSTS' => 'https://a.example.com https://b.example.com'], function () {\n            $resp = $this->get('/');\n            $frameHeader = $this->getCspHeader($resp, 'frame-ancestors');\n\n            $this->assertNotEmpty($frameHeader);\n            $this->assertEquals('frame-ancestors \\'self\\' https://a.example.com https://b.example.com', $frameHeader);\n        });\n    }\n\n    public function test_script_csp_set_on_responses()\n    {\n        $resp = $this->get('/');\n        $scriptHeader = $this->getCspHeader($resp, 'script-src');\n        $this->assertStringContainsString('\\'strict-dynamic\\'', $scriptHeader);\n        $this->assertStringContainsString('\\'nonce-', $scriptHeader);\n    }\n\n    public function test_script_csp_nonce_matches_nonce_used_in_custom_head()\n    {\n        $this->setSettings(['app-custom-head' => '<script>console.log(\"cat\");</script>']);\n        $resp = $this->get('/login');\n        $scriptHeader = $this->getCspHeader($resp, 'script-src');\n\n        $nonce = app()->make(CspService::class)->getNonce();\n        $this->assertStringContainsString('nonce-' . $nonce, $scriptHeader);\n        $resp->assertSee('<script nonce=\"' . $nonce . '\">console.log(\"cat\");</script>', false);\n    }\n\n    public function test_script_csp_nonce_changes_per_request()\n    {\n        $resp = $this->get('/');\n        $firstHeader = $this->getCspHeader($resp, 'script-src');\n\n        $this->refreshApplication();\n\n        $resp = $this->get('/');\n        $secondHeader = $this->getCspHeader($resp, 'script-src');\n\n        $this->assertNotEquals($firstHeader, $secondHeader);\n    }\n\n    public function test_content_filtering_config_controls_csp_script_headers()\n    {\n        config()->set('app.content_filtering', '');\n        $resp = $this->get('/');\n        $scriptHeader = $this->getCspHeader($resp, 'script-src');\n        $this->assertEmpty($scriptHeader);\n\n        config()->set('app.content_filtering', 'j');\n        $resp = $this->get('/');\n        $scriptHeader = $this->getCspHeader($resp, 'script-src');\n        $this->assertNotEmpty($scriptHeader);\n    }\n\n    public function test_object_src_csp_header_set()\n    {\n        $resp = $this->get('/');\n        $scriptHeader = $this->getCspHeader($resp, 'object-src');\n        $this->assertEquals('object-src \\'self\\'', $scriptHeader);\n    }\n\n    public function test_base_uri_csp_header_set()\n    {\n        $resp = $this->get('/');\n        $scriptHeader = $this->getCspHeader($resp, 'base-uri');\n        $this->assertEquals('base-uri \\'self\\'', $scriptHeader);\n    }\n\n    public function test_frame_src_csp_header_set()\n    {\n        $resp = $this->get('/');\n        $scriptHeader = $this->getCspHeader($resp, 'frame-src');\n        $this->assertEquals('frame-src \\'self\\' https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com', $scriptHeader);\n    }\n\n    public function test_frame_src_csp_header_has_drawio_host_added()\n    {\n        config()->set([\n            'app.iframe_sources' => 'https://example.com',\n            'services.drawio'    => 'https://diagrams.example.com/testing?cat=dog',\n        ]);\n\n        $resp = $this->get('/');\n        $scriptHeader = $this->getCspHeader($resp, 'frame-src');\n        $this->assertEquals('frame-src \\'self\\' https://example.com https://diagrams.example.com', $scriptHeader);\n    }\n\n    public function test_frame_src_csp_header_drawio_host_includes_port_if_existing()\n    {\n        config()->set([\n            'app.iframe_sources' => 'https://example.com',\n            'services.drawio'    => 'https://diagrams.example.com:8080/testing?cat=dog',\n        ]);\n\n        $resp = $this->get('/');\n        $scriptHeader = $this->getCspHeader($resp, 'frame-src');\n        $this->assertEquals('frame-src \\'self\\' https://example.com https://diagrams.example.com:8080', $scriptHeader);\n    }\n\n    public function test_cache_control_headers_are_set_on_responses()\n    {\n        // Public access\n        $resp = $this->get('/');\n        $resp->assertHeader('Cache-Control', 'no-cache, no-store, private');\n        $resp->assertHeader('Expires', 'Sun, 12 Jul 2015 19:01:00 GMT');\n\n        // Authed access\n        $this->asEditor();\n        $resp = $this->get('/');\n        $resp->assertHeader('Cache-Control', 'no-cache, no-store, private');\n        $resp->assertHeader('Expires', 'Sun, 12 Jul 2015 19:01:00 GMT');\n    }\n\n    /**\n     * Get the value of the first CSP header of the given type.\n     */\n    protected function getCspHeader(TestResponse $resp, string $type): string\n    {\n        $cspHeaders = explode('; ', $resp->headers->get('Content-Security-Policy'));\n\n        foreach ($cspHeaders as $cspHeader) {\n            if (strpos($cspHeader, $type) === 0) {\n                return $cspHeader;\n            }\n        }\n\n        return '';\n    }\n}\n"
  },
  {
    "path": "tests/SessionTest.php",
    "content": "<?php\n\nnamespace Tests;\n\nclass SessionTest extends TestCase\n{\n    public function test_secure_images_not_tracked_in_session_history()\n    {\n        config()->set('filesystems.images', 'local_secure');\n        $this->asEditor();\n        $page = $this->entities->page();\n        $result = $this->files->uploadGalleryImageToPage($this, $page);\n        $expectedPath = storage_path($result['path']);\n        $this->assertFileExists($expectedPath);\n\n        $this->get('/books');\n        $this->assertEquals(url('/books'), session()->previousUrl());\n\n        $resp = $this->get($result['path']);\n        $resp->assertOk();\n        $resp->assertHeader('Content-Type', 'image/png');\n\n        $this->assertEquals(url('/books'), session()->previousUrl());\n\n        if (file_exists($expectedPath)) {\n            unlink($expectedPath);\n        }\n    }\n\n    public function test_pwa_manifest_is_not_tracked_in_session_history()\n    {\n        $this->asEditor()->get('/books');\n        $this->get('/manifest.json');\n\n        $this->assertEquals(url('/books'), session()->previousUrl());\n    }\n\n    public function test_dist_dir_access_is_not_tracked_in_session_history()\n    {\n        $this->asEditor()->get('/books');\n        $this->get('/dist/sub/hello.txt');\n\n        $this->assertEquals(url('/books'), session()->previousUrl());\n    }\n\n    public function test_opensearch_is_not_tracked_in_session_history()\n    {\n        $this->asEditor()->get('/books');\n        $this->get('/opensearch.xml');\n\n        $this->assertEquals(url('/books'), session()->previousUrl());\n    }\n}\n"
  },
  {
    "path": "tests/Settings/CustomHeadContentTest.php",
    "content": "<?php\n\nnamespace Tests\\Settings;\n\nuse BookStack\\Util\\CspService;\nuse Tests\\TestCase;\n\nclass CustomHeadContentTest extends TestCase\n{\n    public function test_configured_content_shows_on_pages()\n    {\n        $this->setSettings(['app-custom-head' => '<script>console.log(\"cat\");</script>']);\n        $resp = $this->get('/login');\n        $resp->assertSee('console.log(\"cat\")', false);\n    }\n\n    public function test_content_wrapped_in_specific_html_comments()\n    {\n        // These comments are used to identify head content for editor injection\n        $this->setSettings(['app-custom-head' => '<script>console.log(\"cat\");</script>']);\n        $resp = $this->get('/login');\n        $resp->assertSee('<!-- Start: custom user content -->', false);\n        $resp->assertSee('<!-- End: custom user content -->', false);\n    }\n\n    public function test_configured_content_does_not_show_on_settings_page()\n    {\n        $this->setSettings(['app-custom-head' => '<script>console.log(\"cat\");</script>']);\n        $resp = $this->asAdmin()->get('/settings/features');\n        $resp->assertDontSee('console.log(\"cat\")', false);\n    }\n\n    public function test_divs_in_js_preserved_in_configured_content()\n    {\n        $this->setSettings(['app-custom-head' => '<script><div id=\"hello\">cat</div></script>']);\n        $resp = $this->get('/login');\n        $resp->assertSee('<div id=\"hello\">cat</div>', false);\n    }\n\n    public function test_nonce_application_handles_edge_cases()\n    {\n        $mockCSP = $this->mock(CspService::class);\n        $mockCSP->shouldReceive('getNonce')->andReturn('abc123');\n\n        $content = trim('\n<script>console.log(\"cat\");</script>\n<script type=\"text/html\"><\\script>const a = `<div></div>`<\\/\\script></script>\n<script >const a = `<div></div>`;</script>\n<script type=\"<script text>test\">const c = `<div></div>`;</script>\n<script\n    type=\"text/html\"\n>\nconst a = `<\\script><\\/script>`;\nconst b = `<script`;\n</script>\n<SCRIPT>const b = `↗️£`;</SCRIPT>\n        ');\n\n        $expectedOutput = trim('\n<script nonce=\"abc123\">console.log(\"cat\");</script>\n<script type=\"text/html\" nonce=\"abc123\"><\\script>const a = `<div></div>`<\\/\\script></script>\n<script nonce=\"abc123\">const a = `<div></div>`;</script>\n<script type=\"&lt;script text&gt;test\" nonce=\"abc123\">const c = `<div></div>`;</script>\n<script type=\"text/html\" nonce=\"abc123\">\nconst a = `<\\script><\\/script>`;\nconst b = `<script`;\n</script>\n<script nonce=\"abc123\">const b = `↗️£`;</script>\n        ');\n\n        $this->setSettings(['app-custom-head' => $content]);\n        $resp = $this->get('/login');\n        $resp->assertSee($expectedOutput, false);\n    }\n}\n"
  },
  {
    "path": "tests/Settings/FooterLinksTest.php",
    "content": "<?php\n\nnamespace Tests\\Settings;\n\nuse Tests\\TestCase;\n\nclass FooterLinksTest extends TestCase\n{\n    public function test_saving_setting()\n    {\n        $resp = $this->asAdmin()->post('/settings/customization', [\n            'setting-app-footer-links' => [\n                ['label' => 'My custom link 1', 'url' => 'https://example.com/1'],\n                ['label' => 'My custom link 2', 'url' => 'https://example.com/2'],\n            ],\n        ]);\n        $resp->assertRedirect('/settings/customization');\n\n        $result = setting('app-footer-links');\n        $this->assertIsArray($result);\n        $this->assertCount(2, $result);\n        $this->assertEquals('My custom link 2', $result[1]['label']);\n        $this->assertEquals('https://example.com/1', $result[0]['url']);\n    }\n\n    public function test_set_options_visible_on_settings_page()\n    {\n        $this->setSettings(['app-footer-links' => [\n            ['label' => 'My custom link', 'url' => 'https://example.com/link-a'],\n            ['label' => 'Another Link', 'url' => 'https://example.com/link-b'],\n        ]]);\n\n        $resp = $this->asAdmin()->get('/settings/customization');\n        $resp->assertSee('value=\"My custom link\"', false);\n        $resp->assertSee('value=\"Another Link\"', false);\n        $resp->assertSee('value=\"https://example.com/link-a\"', false);\n        $resp->assertSee('value=\"https://example.com/link-b\"', false);\n    }\n\n    public function test_footer_links_show_on_pages()\n    {\n        $this->setSettings(['app-footer-links' => [\n            ['label' => 'My custom link', 'url' => 'https://example.com/link-a'],\n            ['label' => 'Another Link', 'url' => 'https://example.com/link-b'],\n        ]]);\n\n        $resp = $this->get('/login');\n        $this->withHtml($resp)->assertElementContains('footer a[href=\"https://example.com/link-a\"]', 'My custom link');\n\n        $resp = $this->asEditor()->get('/');\n        $this->withHtml($resp)->assertElementContains('footer a[href=\"https://example.com/link-b\"]', 'Another link');\n    }\n\n    public function test_using_translation_system_for_labels()\n    {\n        $this->setSettings(['app-footer-links' => [\n            ['label' => 'trans::common.privacy_policy', 'url' => 'https://example.com/privacy'],\n            ['label' => 'trans::common.terms_of_service', 'url' => 'https://example.com/terms'],\n        ]]);\n\n        $resp = $this->get('/login');\n        $this->withHtml($resp)->assertElementContains('footer a[href=\"https://example.com/privacy\"]', 'Privacy Policy');\n        $this->withHtml($resp)->assertElementContains('footer a[href=\"https://example.com/terms\"]', 'Terms of Service');\n    }\n}\n"
  },
  {
    "path": "tests/Settings/PageListLimitsTest.php",
    "content": "<?php\n\nnamespace Tests\\Settings;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Bookshelf;\nuse Tests\\TestCase;\n\nclass PageListLimitsTest extends TestCase\n{\n    public function test_saving_setting_and_loading()\n    {\n        $resp = $this->asAdmin()->post('/settings/sorting', [\n            'setting-lists-page-count-shelves' => '3',\n            'setting-lists-page-count-books' => '6',\n            'setting-lists-page-count-search' => '9',\n        ]);\n        $resp->assertRedirect('/settings/sorting');\n\n        $this->assertEquals(3, setting()->getInteger('lists-page-count-shelves', 18));\n        $this->assertEquals(6, setting()->getInteger('lists-page-count-books', 18));\n        $this->assertEquals(9, setting()->getInteger('lists-page-count-search', 18));\n\n        $resp = $this->get('/settings/sorting');\n        $html = $this->withHtml($resp);\n\n        $html->assertFieldHasValue('setting-lists-page-count-shelves', '3');\n        $html->assertFieldHasValue('setting-lists-page-count-books', '6');\n        $html->assertFieldHasValue('setting-lists-page-count-search', '9');\n    }\n\n    public function test_invalid_counts_will_use_default_when_fetched_as_an_integer()\n    {\n        $this->asAdmin()->post('/settings/sorting', [\n            'setting-lists-page-count-shelves' => 'cat',\n        ]);\n\n        $this->assertEquals(18, setting()->getInteger('lists-page-count-shelves', 18));\n    }\n\n    public function test_shelf_count_is_used_on_shelves_view()\n    {\n        $resp = $this->asAdmin()->get('/shelves');\n        $defaultCount = min(Bookshelf::query()->count(), 18);\n        $this->withHtml($resp)->assertElementCount('main [data-entity-type=\"bookshelf\"]', $defaultCount);\n\n        $this->post('/settings/sorting', [\n            'setting-lists-page-count-shelves' => '1',\n        ]);\n\n        $resp = $this->get('/shelves');\n        $this->withHtml($resp)->assertElementCount('main [data-entity-type=\"bookshelf\"]', 1);\n    }\n\n    public function test_book_count_is_used_on_books_view()\n    {\n        $resp = $this->asAdmin()->get('/books');\n        $defaultCount = min(Book::query()->count(), 18);\n        $this->withHtml($resp)->assertElementCount('main [data-entity-type=\"book\"]', $defaultCount);\n\n        $this->post('/settings/sorting', [\n            'setting-lists-page-count-books' => '1',\n        ]);\n\n        $resp = $this->get('/books');\n        $this->withHtml($resp)->assertElementCount('main [data-entity-type=\"book\"]', 1);\n    }\n\n    public function test_search_count_is_used_on_search_view()\n    {\n        $resp = $this->asAdmin()->get('/search');\n        $this->withHtml($resp)->assertElementCount('.entity-list [data-entity-id]', 18);\n\n        $this->post('/settings/sorting', [\n            'setting-lists-page-count-search' => '1',\n        ]);\n\n        $resp = $this->get('/search');\n        $this->withHtml($resp)->assertElementCount('.entity-list [data-entity-id]', 1);\n    }\n}\n"
  },
  {
    "path": "tests/Settings/RecycleBinTest.php",
    "content": "<?php\n\nnamespace Tests\\Settings;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Deletion;\nuse BookStack\\Entities\\Models\\Page;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Facades\\DB;\nuse Tests\\TestCase;\n\nclass RecycleBinTest extends TestCase\n{\n    public function test_recycle_bin_routes_permissions()\n    {\n        $page = $this->entities->page();\n        $editor = $this->users->editor();\n        $this->actingAs($editor)->delete($page->getUrl());\n        $deletion = Deletion::query()->firstOrFail();\n\n        $routes = [\n            'GET:/settings/recycle-bin',\n            'POST:/settings/recycle-bin/empty',\n            \"GET:/settings/recycle-bin/{$deletion->id}/destroy\",\n            \"GET:/settings/recycle-bin/{$deletion->id}/restore\",\n            \"POST:/settings/recycle-bin/{$deletion->id}/restore\",\n            \"DELETE:/settings/recycle-bin/{$deletion->id}\",\n        ];\n\n        foreach ($routes as $route) {\n            [$method, $url] = explode(':', $route);\n            $resp = $this->call($method, $url);\n            $this->assertPermissionError($resp);\n        }\n\n        $this->permissions->grantUserRolePermissions($editor, ['restrictions-manage-all']);\n\n        foreach ($routes as $route) {\n            [$method, $url] = explode(':', $route);\n            $resp = $this->call($method, $url);\n            $this->assertPermissionError($resp);\n        }\n\n        $this->permissions->grantUserRolePermissions($editor, ['settings-manage']);\n\n        foreach ($routes as $route) {\n            DB::beginTransaction();\n            [$method, $url] = explode(':', $route);\n            $resp = $this->call($method, $url);\n            $this->assertNotPermissionError($resp);\n            DB::rollBack();\n        }\n    }\n\n    public function test_recycle_bin_view()\n    {\n        $page = $this->entities->page();\n        $book = Book::query()->whereHas('pages')->whereHas('chapters')->withCount(['pages', 'chapters'])->first();\n        $editor = $this->users->editor();\n        $this->actingAs($editor)->delete($page->getUrl());\n        $this->actingAs($editor)->delete($book->getUrl());\n\n        $viewReq = $this->asAdmin()->get('/settings/recycle-bin');\n        $html = $this->withHtml($viewReq);\n        $html->assertElementContains('.item-list-row', $page->name);\n        $html->assertElementContains('.item-list-row', $editor->name);\n        $html->assertElementContains('.item-list-row', $book->name);\n        $html->assertElementContains('.item-list-row', $book->pages_count . ' Pages');\n        $html->assertElementContains('.item-list-row', $book->chapters_count . ' Chapters');\n    }\n\n    public function test_recycle_bin_empty()\n    {\n        $page = $this->entities->page();\n        $book = Book::query()->where('id', '!=', $page->book_id)->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();\n        $editor = $this->users->editor();\n        $this->actingAs($editor)->delete($page->getUrl());\n        $this->actingAs($editor)->delete($book->getUrl());\n\n        $this->assertTrue(Deletion::query()->count() === 2);\n        $emptyReq = $this->asAdmin()->post('/settings/recycle-bin/empty');\n        $emptyReq->assertRedirect('/settings/recycle-bin');\n\n        $this->assertTrue(Deletion::query()->count() === 0);\n        $this->assertDatabaseMissing('entities', ['id' => $book->id, 'type' => 'book']);\n        $this->assertDatabaseMissing('entity_container_data', ['entity_id' => $book->id, 'entity_type' => 'book']);\n        $this->assertDatabaseMissing('entities', ['id' => $book->pages->first()->id, 'type' => 'page']);\n        $this->assertDatabaseMissing('entity_page_data', ['page_id' => $book->pages->first()->id]);\n        $this->assertDatabaseMissing('entities', ['id' => $book->chapters->first()->id, 'type' => 'chapter']);\n        $this->assertDatabaseMissing('entity_container_data', ['entity_id' => $book->chapters->first()->id, 'entity_type' => 'chapter']);\n\n        $itemCount = 2 + $book->pages->count() + $book->chapters->count();\n        $redirectReq = $this->get('/settings/recycle-bin');\n        $this->assertNotificationContains($redirectReq, 'Deleted ' . $itemCount . ' total items from the recycle bin');\n    }\n\n    public function test_entity_restore()\n    {\n        $book = $this->entities->bookHasChaptersAndPages();\n        $this->asEditor()->delete($book->getUrl())->assertRedirect();\n        $deletion = Deletion::query()->firstOrFail();\n\n        $this->assertEquals($book->pages->count(), Page::query()->withTrashed()->where('book_id', '=', $book->id)->whereNotNull('deleted_at')->count());\n        $this->assertEquals($book->chapters->count(), Chapter::query()->withTrashed()->where('book_id', '=', $book->id)->whereNotNull('deleted_at')->count());\n\n        $restoreReq = $this->asAdmin()->post(\"/settings/recycle-bin/{$deletion->id}/restore\");\n        $restoreReq->assertRedirect('/settings/recycle-bin');\n        $this->assertTrue(Deletion::query()->count() === 0);\n\n        $this->assertEquals($book->pages->count(), Page::query()->where('book_id', '=', $book->id)->whereNull('deleted_at')->count());\n        $this->assertEquals($book->chapters->count(), Chapter::query()->where('book_id', '=', $book->id)->whereNull('deleted_at')->count());\n\n        $itemCount = 1 + $book->pages->count() + $book->chapters->count();\n        $redirectReq = $this->get('/settings/recycle-bin');\n        $this->assertNotificationContains($redirectReq, 'Restored ' . $itemCount . ' total items from the recycle bin');\n    }\n\n    public function test_permanent_delete()\n    {\n        $book = $this->entities->bookHasChaptersAndPages();\n        $this->asEditor()->delete($book->getUrl());\n        $deletion = Deletion::query()->firstOrFail();\n\n        $deleteReq = $this->asAdmin()->delete(\"/settings/recycle-bin/{$deletion->id}\");\n        $deleteReq->assertRedirect('/settings/recycle-bin');\n        $this->assertTrue(Deletion::query()->count() === 0);\n\n        $this->assertDatabaseMissing('entities', ['id' => $book->id, 'type' => 'book']);\n        $this->assertDatabaseMissing('entity_container_data', ['entity_id' => $book->id, 'entity_type' => 'book']);\n        $this->assertDatabaseMissing('entities', ['id' => $book->pages->first()->id, 'type' => 'page']);\n        $this->assertDatabaseMissing('entity_page_data', ['page_id' => $book->pages->first()->id]);\n        $this->assertDatabaseMissing('entities', ['id' => $book->chapters->first()->id, 'type' => 'chapter']);\n        $this->assertDatabaseMissing('entity_container_data', ['entity_id' => $book->chapters->first()->id, 'entity_type' => 'chapter']);\n\n        $itemCount = 1 + $book->pages->count() + $book->chapters->count();\n        $redirectReq = $this->get('/settings/recycle-bin');\n        $this->assertNotificationContains($redirectReq, 'Deleted ' . $itemCount . ' total items from the recycle bin');\n    }\n\n    public function test_permanent_delete_for_each_type()\n    {\n        foreach ($this->entities->all() as $type => $entity) {\n            $this->asEditor()->delete($entity->getUrl());\n            $deletion = Deletion::query()->orderBy('id', 'desc')->firstOrFail();\n\n            $deleteReq = $this->asAdmin()->delete(\"/settings/recycle-bin/{$deletion->id}\");\n            $deleteReq->assertRedirect('/settings/recycle-bin');\n            $this->assertDatabaseMissing('deletions', ['id' => $deletion->id]);\n            $this->assertDatabaseMissing($entity->getTable(), ['id' => $entity->id]);\n        }\n    }\n\n    public function test_permanent_entity_delete_updates_existing_activity_with_entity_name()\n    {\n        $page = $this->entities->page();\n        $this->asEditor()->delete($page->getUrl());\n        $deletion = $page->deletions()->firstOrFail();\n\n        $this->assertDatabaseHas('activities', [\n            'type'        => 'page_delete',\n            'loggable_id'   => $page->id,\n            'loggable_type' => $page->getMorphClass(),\n        ]);\n\n        $this->asAdmin()->delete(\"/settings/recycle-bin/{$deletion->id}\");\n\n        $this->assertDatabaseMissing('activities', [\n            'type'        => 'page_delete',\n            'loggable_id'   => $page->id,\n            'loggable_type' => $page->getMorphClass(),\n        ]);\n\n        $this->assertDatabaseHas('activities', [\n            'type'        => 'page_delete',\n            'loggable_id'   => null,\n            'loggable_type' => null,\n            'detail'      => $page->name,\n        ]);\n    }\n\n    public function test_permanent_book_delete_removes_shelf_relation_data()\n    {\n        $book = $this->entities->book();\n        $shelf = $this->entities->shelf();\n        $shelf->books()->attach($book);\n        $this->assertDatabaseHas('bookshelves_books', ['book_id' => $book->id]);\n\n        $this->asEditor()->delete($book->getUrl());\n        $deletion = $book->deletions()->firstOrFail();\n        $this->asAdmin()->delete(\"/settings/recycle-bin/{$deletion->id}\")->assertRedirect();\n\n        $this->assertDatabaseMissing('bookshelves_books', ['book_id' => $book->id]);\n    }\n\n    public function test_permanent_shelf_delete_removes_book_relation_data()\n    {\n        $book = $this->entities->book();\n        $shelf = $this->entities->shelf();\n        $shelf->books()->attach($book);\n        $this->assertDatabaseHas('bookshelves_books', ['bookshelf_id' => $shelf->id]);\n\n        $this->asEditor()->delete($shelf->getUrl());\n        $deletion = $shelf->deletions()->firstOrFail();\n        $this->asAdmin()->delete(\"/settings/recycle-bin/{$deletion->id}\")->assertRedirect();\n\n        $this->assertDatabaseMissing('bookshelves_books', ['bookshelf_id' => $shelf->id]);\n    }\n\n    public function test_auto_clear_functionality_works()\n    {\n        config()->set('app.recycle_bin_lifetime', 5);\n        $page = $this->entities->page();\n        $otherPage = $this->entities->page();\n\n        $this->asEditor()->delete($page->getUrl());\n        $this->assertDatabaseHasEntityData('page', ['id' => $page->id]);\n        $this->assertEquals(1, Deletion::query()->count());\n\n        Carbon::setTestNow(Carbon::now()->addDays(6));\n        $this->asEditor()->delete($otherPage->getUrl());\n        $this->assertEquals(1, Deletion::query()->count());\n\n        $this->assertDatabaseMissing('entities', ['id' => $page->id, 'type' => 'page']);\n    }\n\n    public function test_auto_clear_functionality_with_negative_time_keeps_forever()\n    {\n        config()->set('app.recycle_bin_lifetime', -1);\n        $page = $this->entities->page();\n        $otherPage = $this->entities->page();\n\n        $this->asEditor()->delete($page->getUrl());\n        $this->assertEquals(1, Deletion::query()->count());\n\n        Carbon::setTestNow(Carbon::now()->addDays(6000));\n        $this->asEditor()->delete($otherPage->getUrl());\n        $this->assertEquals(2, Deletion::query()->count());\n\n        $this->assertDatabaseHasEntityData('page', ['id' => $page->id]);\n    }\n\n    public function test_auto_clear_functionality_with_zero_time_deletes_instantly()\n    {\n        config()->set('app.recycle_bin_lifetime', 0);\n        $page = $this->entities->page();\n\n        $this->asEditor()->delete($page->getUrl());\n        $this->assertDatabaseMissing('entities', ['id' => $page->id, 'type' => 'page']);\n        $this->assertEquals(0, Deletion::query()->count());\n    }\n\n    public function test_restore_flow_when_restoring_nested_delete_first()\n    {\n        $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();\n        $chapter = $book->chapters->first();\n        $this->asEditor()->delete($chapter->getUrl());\n        $this->asEditor()->delete($book->getUrl());\n\n        $bookDeletion = $book->deletions()->first();\n        $chapterDeletion = $chapter->deletions()->first();\n\n        $chapterRestoreView = $this->asAdmin()->get(\"/settings/recycle-bin/{$chapterDeletion->id}/restore\");\n        $chapterRestoreView->assertStatus(200);\n        $chapterRestoreView->assertSeeText($chapter->name);\n\n        $chapterRestore = $this->post(\"/settings/recycle-bin/{$chapterDeletion->id}/restore\");\n        $chapterRestore->assertRedirect('/settings/recycle-bin');\n        $this->assertDatabaseMissing('deletions', ['id' => $chapterDeletion->id]);\n\n        $chapter->refresh();\n        $this->assertNotNull($chapter->deleted_at);\n\n        $bookRestoreView = $this->asAdmin()->get(\"/settings/recycle-bin/{$bookDeletion->id}/restore\");\n        $bookRestoreView->assertStatus(200);\n        $bookRestoreView->assertSeeText($chapter->name);\n\n        $this->post(\"/settings/recycle-bin/{$bookDeletion->id}/restore\");\n        $chapter->refresh();\n        $this->assertNull($chapter->deleted_at);\n    }\n\n    public function test_restore_page_shows_link_to_parent_restore_if_parent_also_deleted()\n    {\n        $book = $this->entities->bookHasChaptersAndPages();\n        $chapter = $book->chapters->first();\n        /** @var Page $page */\n        $page = $chapter->pages->first();\n        $this->asEditor()->delete($page->getUrl());\n        $this->asEditor()->delete($book->getUrl());\n\n        $bookDeletion = $book->deletions()->first();\n        $pageDeletion = $page->deletions()->first();\n\n        $pageRestoreView = $this->asAdmin()->get(\"/settings/recycle-bin/{$pageDeletion->id}/restore\");\n        $pageRestoreView->assertSee('The parent of this item has also been deleted.');\n        $this->withHtml($pageRestoreView)->assertElementContains('a[href$=\"/settings/recycle-bin/' . $bookDeletion->id . '/restore\"]', 'Restore Parent');\n    }\n}\n"
  },
  {
    "path": "tests/Settings/RegenerateReferencesTest.php",
    "content": "<?php\n\nnamespace Tests\\Settings;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\References\\ReferenceStore;\nuse Tests\\TestCase;\n\nclass RegenerateReferencesTest extends TestCase\n{\n    public function test_option_visible_on_maintenance_page()\n    {\n        $pageView = $this->asAdmin()->get('/settings/maintenance');\n        $formCssSelector = 'form[action$=\"/settings/maintenance/regenerate-references\"]';\n        $html = $this->withHtml($pageView);\n        $html->assertElementExists('#regenerate-references');\n        $html->assertElementExists($formCssSelector);\n        $html->assertElementContains($formCssSelector . ' button', 'Regenerate References');\n    }\n\n    public function test_action_runs_reference_regen()\n    {\n        $this->mock(ReferenceStore::class)\n            ->shouldReceive('updateForAll')\n            ->once();\n\n        $resp = $this->asAdmin()->post('/settings/maintenance/regenerate-references');\n        $resp->assertRedirect('/settings/maintenance#regenerate-references');\n        $this->assertSessionHas('success', 'Reference index has been regenerated!');\n        $this->assertActivityExists(ActivityType::MAINTENANCE_ACTION_RUN, null, 'regenerate-references');\n    }\n\n    public function test_settings_manage_permission_required()\n    {\n        $editor = $this->users->editor();\n        $resp = $this->actingAs($editor)->post('/settings/maintenance/regenerate-references');\n        $this->assertPermissionError($resp);\n\n        $this->permissions->grantUserRolePermissions($editor, ['settings-manage']);\n\n        $resp = $this->actingAs($editor)->post('/settings/maintenance/regenerate-references');\n        $this->assertNotPermissionError($resp);\n    }\n\n    public function test_action_failed_shown_as_error_notification()\n    {\n        $this->mock(ReferenceStore::class)\n            ->shouldReceive('updateForAll')\n            ->andThrow(\\Exception::class, 'A badger stopped the task');\n\n        $resp = $this->asAdmin()->post('/settings/maintenance/regenerate-references');\n        $resp->assertRedirect('/settings/maintenance#regenerate-references');\n        $this->assertSessionError('A badger stopped the task');\n    }\n}\n"
  },
  {
    "path": "tests/Settings/SettingsTest.php",
    "content": "<?php\n\nnamespace Tests\\Settings;\n\nuse Tests\\TestCase;\n\nclass SettingsTest extends TestCase\n{\n    public function test_admin_can_see_settings()\n    {\n        $this->asAdmin()->get('/settings/features')->assertSee('Settings');\n    }\n\n    public function test_settings_endpoint_redirects_to_settings_view()\n    {\n        $resp = $this->asAdmin()->get('/settings');\n\n        $resp->assertStatus(302);\n\n        // Manually check path to ensure it's generated as the full path\n        $location = $resp->headers->get('location');\n        $this->assertEquals(url('/settings/features'), $location);\n    }\n\n    public function test_settings_category_links_work_as_expected()\n    {\n        $this->asAdmin();\n        $categories = [\n            'features'      => 'Features & Security',\n            'customization' => 'Customization',\n            'registration'  => 'Registration',\n        ];\n\n        foreach ($categories as $category => $title) {\n            $resp = $this->get(\"/settings/{$category}\");\n            $this->withHtml($resp)->assertElementContains('h1', $title);\n            $this->withHtml($resp)->assertElementExists(\"form[action$=\\\"/settings/{$category}\\\"]\");\n        }\n    }\n\n    public function test_not_found_setting_category_throws_404()\n    {\n        $resp = $this->asAdmin()->get('/settings/biscuits');\n\n        $resp->assertStatus(404);\n        $resp->assertSee('Page Not Found');\n    }\n\n    public function test_updating_and_removing_app_icon()\n    {\n        $this->asAdmin();\n        $galleryFile = $this->files->uploadedImage('my-app-icon.png');\n        $expectedPath = public_path('uploads/images/system/' . date('Y-m') . '/my-app-icon.png');\n\n        $this->assertFalse(setting()->get('app-icon'));\n        $this->assertFalse(setting()->get('app-icon-180'));\n        $this->assertFalse(setting()->get('app-icon-128'));\n        $this->assertFalse(setting()->get('app-icon-64'));\n        $this->assertFalse(setting()->get('app-icon-32'));\n        $this->assertEquals(\n            file_get_contents(public_path('icon.ico')),\n            file_get_contents(public_path('favicon.ico')),\n        );\n\n        $prevFileCount = count(glob(dirname($expectedPath) . DIRECTORY_SEPARATOR . '*.png'));\n\n        $upload = $this->call('POST', '/settings/customization', [], [], ['app_icon' => $galleryFile], []);\n        $upload->assertRedirect('/settings/customization');\n\n        $this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: ' . $expectedPath);\n        $this->assertStringContainsString('my-app-icon', setting()->get('app-icon'));\n        $this->assertStringContainsString('my-app-icon', setting()->get('app-icon-180'));\n        $this->assertStringContainsString('my-app-icon', setting()->get('app-icon-128'));\n        $this->assertStringContainsString('my-app-icon', setting()->get('app-icon-64'));\n        $this->assertStringContainsString('my-app-icon', setting()->get('app-icon-32'));\n\n        $newFileCount = count(glob(dirname($expectedPath) . DIRECTORY_SEPARATOR . '*.png'));\n        $this->assertEquals(5, $newFileCount - $prevFileCount);\n\n        $resp = $this->get('/');\n        $this->withHtml($resp)->assertElementCount('link[sizes][href*=\"my-app-icon\"]', 6);\n\n        $this->assertNotEquals(\n            file_get_contents(public_path('icon.ico')),\n            file_get_contents(public_path('favicon.ico')),\n        );\n\n        $reset = $this->post('/settings/customization', ['app_icon_reset' => 'true']);\n        $reset->assertRedirect('/settings/customization');\n\n        $resetFileCount = count(glob(dirname($expectedPath) . DIRECTORY_SEPARATOR . '*.png'));\n        $this->assertEquals($prevFileCount, $resetFileCount);\n        $this->assertFalse(setting()->get('app-icon'));\n        $this->assertFalse(setting()->get('app-icon-180'));\n        $this->assertFalse(setting()->get('app-icon-128'));\n        $this->assertFalse(setting()->get('app-icon-64'));\n        $this->assertFalse(setting()->get('app-icon-32'));\n\n        $this->assertEquals(\n            file_get_contents(public_path('icon.ico')),\n            file_get_contents(public_path('favicon.ico')),\n        );\n    }\n\n    public function test_both_light_and_dark_colors_are_used_in_the_base_view()\n    {\n        // To allow for dynamic color changes on the front-end where desired.\n        $this->setSettings(['page-color' => 'superlightblue', 'page-color-dark' => 'superdarkblue']);\n\n        $resp = $this->get('/login');\n\n        $resp->assertSee(':root {');\n        $resp->assertSee('superlightblue');\n        $resp->assertSee(':root.dark-mode {');\n        $resp->assertSee('superdarkblue');\n    }\n}\n"
  },
  {
    "path": "tests/Settings/TestEmailTest.php",
    "content": "<?php\n\nnamespace Tests\\Settings;\n\nuse BookStack\\Settings\\TestEmailNotification;\nuse Illuminate\\Contracts\\Notifications\\Dispatcher;\nuse Illuminate\\Support\\Facades\\Notification;\nuse Tests\\TestCase;\n\nclass TestEmailTest extends TestCase\n{\n    public function test_a_send_test_button_shows()\n    {\n        $pageView = $this->asAdmin()->get('/settings/maintenance');\n        $formCssSelector = 'form[action$=\"/settings/maintenance/send-test-email\"]';\n        $this->withHtml($pageView)->assertElementExists($formCssSelector);\n        $this->withHtml($pageView)->assertElementContains($formCssSelector . ' button', 'Send Test Email');\n    }\n\n    public function test_send_test_email_endpoint_sends_email_and_redirects_user_and_shows_notification()\n    {\n        Notification::fake();\n        $admin = $this->users->admin();\n\n        $sendReq = $this->actingAs($admin)->post('/settings/maintenance/send-test-email');\n        $sendReq->assertRedirect('/settings/maintenance#image-cleanup');\n        $this->assertSessionHas('success', 'Email sent to ' . $admin->email);\n\n        Notification::assertSentTo($admin, TestEmailNotification::class);\n    }\n\n    public function test_send_test_email_failure_displays_error_notification()\n    {\n        $mockDispatcher = $this->mock(Dispatcher::class);\n        $this->app[Dispatcher::class] = $mockDispatcher;\n\n        $exception = new \\Exception('A random error occurred when testing an email');\n        $mockDispatcher->shouldReceive('sendNow')->andThrow($exception);\n\n        $admin = $this->users->admin();\n        $sendReq = $this->actingAs($admin)->post('/settings/maintenance/send-test-email');\n        $sendReq->assertRedirect('/settings/maintenance#image-cleanup');\n        $this->assertSessionHas('error');\n\n        $message = session()->get('error');\n        $this->assertStringContainsString('Error thrown when sending a test email:', $message);\n        $this->assertStringContainsString('A random error occurred when testing an email', $message);\n    }\n\n    public function test_send_test_email_requires_settings_manage_permission()\n    {\n        Notification::fake();\n        $user = $this->users->viewer();\n\n        $sendReq = $this->actingAs($user)->post('/settings/maintenance/send-test-email');\n        Notification::assertNothingSent();\n\n        $this->permissions->grantUserRolePermissions($user, ['settings-manage']);\n        $sendReq = $this->actingAs($user)->post('/settings/maintenance/send-test-email');\n        Notification::assertSentTo($user, TestEmailNotification::class);\n    }\n}\n"
  },
  {
    "path": "tests/Sorting/BookSortTest.php",
    "content": "<?php\n\nnamespace Tests\\Sorting;\n\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Repos\\PageRepo;\nuse BookStack\\Sorting\\SortRule;\nuse Tests\\TestCase;\n\nclass BookSortTest extends TestCase\n{\n    public function test_book_sort_page_shows()\n    {\n        $bookToSort = $this->entities->book();\n\n        $resp = $this->asAdmin()->get($bookToSort->getUrl());\n        $this->withHtml($resp)->assertElementExists('a[href=\"' . $bookToSort->getUrl('/sort') . '\"]');\n\n        $resp = $this->get($bookToSort->getUrl('/sort'));\n        $resp->assertStatus(200);\n        $resp->assertSee($bookToSort->name);\n    }\n\n    public function test_drafts_do_not_show_up()\n    {\n        $this->asAdmin();\n        $pageRepo = app(PageRepo::class);\n        $book = $this->entities->book();\n        $draft = $pageRepo->getNewDraftPage($book);\n\n        $resp = $this->get($book->getUrl());\n        $resp->assertSee($draft->name);\n\n        $resp = $this->get($book->getUrl('/sort'));\n        $resp->assertDontSee($draft->name);\n    }\n\n    public function test_book_sort()\n    {\n        $oldBook = $this->entities->book();\n        $chapterToMove = $this->entities->newChapter(['name' => 'chapter to move'], $oldBook);\n        $newBook = $this->entities->newBook(['name' => 'New sort book']);\n        $pagesToMove = Page::query()->take(5)->get();\n\n        // Create request data\n        $reqData = [\n            [\n                'id'            => $chapterToMove->id,\n                'sort'          => 0,\n                'parentChapter' => false,\n                'type'          => 'chapter',\n                'book'          => $newBook->id,\n            ],\n        ];\n        foreach ($pagesToMove as $index => $page) {\n            $reqData[] = [\n                'id'            => $page->id,\n                'sort'          => $index,\n                'parentChapter' => $index === count($pagesToMove) - 1 ? $chapterToMove->id : false,\n                'type'          => 'page',\n                'book'          => $newBook->id,\n            ];\n        }\n\n        $sortResp = $this->asEditor()->put($newBook->getUrl() . '/sort', ['sort-tree' => json_encode($reqData)]);\n        $sortResp->assertRedirect($newBook->getUrl());\n        $sortResp->assertStatus(302);\n        $this->assertDatabaseHasEntityData('chapter', [\n            'id'       => $chapterToMove->id,\n            'book_id'  => $newBook->id,\n            'priority' => 0,\n        ]);\n        $this->assertTrue($newBook->chapters()->count() === 1);\n        $this->assertTrue($newBook->chapters()->first()->pages()->count() === 1);\n\n        $checkPage = $pagesToMove[1];\n        $checkResp = $this->get($checkPage->refresh()->getUrl());\n        $checkResp->assertSee($newBook->name);\n    }\n\n    public function test_book_sort_makes_no_changes_if_new_chapter_does_not_align_with_new_book()\n    {\n        $page = $this->entities->pageWithinChapter();\n        $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first();\n\n        $sortData = [\n            'id'            => $page->id,\n            'sort'          => 0,\n            'parentChapter' => $otherChapter->id,\n            'type'          => 'page',\n            'book'          => $page->book_id,\n        ];\n        $this->asEditor()->put($page->book->getUrl('/sort'), ['sort-tree' => json_encode([$sortData])])->assertRedirect();\n\n        $this->assertDatabaseHasEntityData('page', [\n            'id' => $page->id, 'chapter_id' => $page->chapter_id, 'book_id' => $page->book_id,\n        ]);\n    }\n\n    public function test_book_sort_makes_no_changes_if_no_view_permissions_on_new_chapter()\n    {\n        $page = $this->entities->pageWithinChapter();\n        /** @var Chapter $otherChapter */\n        $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first();\n        $this->permissions->setEntityPermissions($otherChapter);\n\n        $sortData = [\n            'id'            => $page->id,\n            'sort'          => 0,\n            'parentChapter' => $otherChapter->id,\n            'type'          => 'page',\n            'book'          => $otherChapter->book_id,\n        ];\n        $this->asEditor()->put($page->book->getUrl('/sort'), ['sort-tree' => json_encode([$sortData])])->assertRedirect();\n\n        $this->assertDatabaseHasEntityData('page', [\n            'id' => $page->id, 'chapter_id' => $page->chapter_id, 'book_id' => $page->book_id,\n        ]);\n    }\n\n    public function test_book_sort_makes_no_changes_if_no_view_permissions_on_new_book()\n    {\n        $page = $this->entities->pageWithinChapter();\n        /** @var Chapter $otherChapter */\n        $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first();\n        $editor = $this->users->editor();\n        $this->permissions->setEntityPermissions($otherChapter->book, ['update', 'delete'], [$editor->roles()->first()]);\n\n        $sortData = [\n            'id'            => $page->id,\n            'sort'          => 0,\n            'parentChapter' => $otherChapter->id,\n            'type'          => 'page',\n            'book'          => $otherChapter->book_id,\n        ];\n        $this->actingAs($editor)->put($page->book->getUrl('/sort'), ['sort-tree' => json_encode([$sortData])])->assertRedirect();\n\n        $this->assertDatabaseHasEntityData('page', [\n            'id' => $page->id, 'chapter_id' => $page->chapter_id, 'book_id' => $page->book_id,\n        ]);\n    }\n\n    public function test_book_sort_makes_no_changes_if_no_update_or_create_permissions_on_new_chapter()\n    {\n        $page = $this->entities->pageWithinChapter();\n        /** @var Chapter $otherChapter */\n        $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first();\n        $editor = $this->users->editor();\n        $this->permissions->setEntityPermissions($otherChapter, ['view', 'delete'], [$editor->roles()->first()]);\n\n        $sortData = [\n            'id'            => $page->id,\n            'sort'          => 0,\n            'parentChapter' => $otherChapter->id,\n            'type'          => 'page',\n            'book'          => $otherChapter->book_id,\n        ];\n        $this->actingAs($editor)->put($page->book->getUrl('/sort'), ['sort-tree' => json_encode([$sortData])])->assertRedirect();\n\n        $this->assertDatabaseHasEntityData('page', [\n            'id' => $page->id, 'chapter_id' => $page->chapter_id, 'book_id' => $page->book_id,\n        ]);\n    }\n\n    public function test_book_sort_makes_no_changes_if_no_update_permissions_on_moved_item()\n    {\n        $page = $this->entities->pageWithinChapter();\n        /** @var Chapter $otherChapter */\n        $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first();\n        $editor = $this->users->editor();\n        $this->permissions->setEntityPermissions($page, ['view', 'delete'], [$editor->roles()->first()]);\n\n        $sortData = [\n            'id'            => $page->id,\n            'sort'          => 0,\n            'parentChapter' => $otherChapter->id,\n            'type'          => 'page',\n            'book'          => $otherChapter->book_id,\n        ];\n        $this->actingAs($editor)->put($page->book->getUrl('/sort'), ['sort-tree' => json_encode([$sortData])])->assertRedirect();\n\n        $this->assertDatabaseHasEntityData('page', [\n            'id' => $page->id, 'chapter_id' => $page->chapter_id, 'book_id' => $page->book_id,\n        ]);\n    }\n\n    public function test_book_sort_makes_no_changes_if_no_delete_permissions_on_moved_item()\n    {\n        $page = $this->entities->pageWithinChapter();\n        /** @var Chapter $otherChapter */\n        $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first();\n        $editor = $this->users->editor();\n        $this->permissions->setEntityPermissions($page, ['view', 'update'], [$editor->roles()->first()]);\n\n        $sortData = [\n            'id'            => $page->id,\n            'sort'          => 0,\n            'parentChapter' => $otherChapter->id,\n            'type'          => 'page',\n            'book'          => $otherChapter->book_id,\n        ];\n        $this->actingAs($editor)->put($page->book->getUrl('/sort'), ['sort-tree' => json_encode([$sortData])])->assertRedirect();\n\n        $this->assertDatabaseHasEntityData('page', [\n            'id' => $page->id, 'chapter_id' => $page->chapter_id, 'book_id' => $page->book_id,\n        ]);\n    }\n\n    public function test_book_sort_does_not_change_timestamps_on_just_order_changes()\n    {\n        $book = $this->entities->bookHasChaptersAndPages();\n        $chapter = $book->chapters()->first();\n        Chapter::query()->where('id', '=', $chapter->id)->update([\n            'priority' => 10001,\n            'updated_at' => \\Carbon\\Carbon::now()->subYear(5),\n        ]);\n\n        $chapter->refresh();\n        $oldUpdatedAt = $chapter->updated_at->unix();\n\n        $sortData = [\n            'id'            => $chapter->id,\n            'sort'          => 0,\n            'parentChapter' => false,\n            'type'          => 'chapter',\n            'book'          => $book->id,\n        ];\n        $this->asEditor()->put($book->getUrl('/sort'), ['sort-tree' => json_encode([$sortData])])->assertRedirect();\n\n        $chapter->refresh();\n        $this->assertNotEquals(10001, $chapter->priority);\n        $this->assertEquals($oldUpdatedAt, $chapter->updated_at->unix());\n    }\n\n    public function test_book_sort_item_returns_book_content()\n    {\n        $bookToSort = $this->entities->book();\n        $firstPage = $bookToSort->pages[0];\n        $firstChapter = $bookToSort->chapters[0];\n\n        $resp = $this->asAdmin()->get($bookToSort->getUrl('/sort-item'));\n\n        // Ensure book details are returned\n        $resp->assertSee($bookToSort->name);\n        $resp->assertSee($firstPage->name);\n        $resp->assertSee($firstChapter->name);\n    }\n\n    public function test_book_sort_item_shows_auto_sort_status()\n    {\n        $sort = SortRule::factory()->create(['name' => 'My sort']);\n        $book = $this->entities->book();\n\n        $resp = $this->asAdmin()->get($book->getUrl('/sort-item'));\n        $this->withHtml($resp)->assertElementNotExists(\"span[title='Auto Sort Active: My sort']\");\n\n        $book->sort_rule_id = $sort->id;\n        $book->save();\n\n        $resp = $this->asAdmin()->get($book->getUrl('/sort-item'));\n        $this->withHtml($resp)->assertElementExists(\"span[title='Auto Sort Active: My sort']\");\n    }\n\n    public function test_auto_sort_options_shown_on_sort_page()\n    {\n        $sort = SortRule::factory()->create();\n        $book = $this->entities->book();\n        $resp = $this->asAdmin()->get($book->getUrl('/sort'));\n\n        $this->withHtml($resp)->assertElementExists('select[name=\"auto-sort\"] option[value=\"' . $sort->id . '\"]');\n    }\n\n    public function test_auto_sort_option_submit_saves_to_book()\n    {\n        $sort = SortRule::factory()->create();\n        $book = $this->entities->book();\n        $bookPage = $book->pages()->first();\n        $bookPage->priority = 10000;\n        $bookPage->save();\n\n        $resp = $this->asAdmin()->put($book->getUrl('/sort'), [\n            'auto-sort' => $sort->id,\n        ]);\n\n        $resp->assertRedirect($book->getUrl());\n        $book->refresh();\n        $bookPage->refresh();\n\n        $this->assertEquals($sort->id, $book->sort_rule_id);\n        $this->assertNotEquals(10000, $bookPage->priority);\n\n        $resp = $this->get($book->getUrl('/sort'));\n        $this->withHtml($resp)->assertElementExists('select[name=\"auto-sort\"] option[value=\"' . $sort->id . '\"][selected]');\n    }\n\n    public function test_pages_in_book_show_sorted_by_priority()\n    {\n        $book = $this->entities->bookHasChaptersAndPages();\n        $book->chapters()->forceDelete();\n        /** @var Page[] $pages */\n        $pages = $book->pages()->whereNull('chapter_id')->take(2)->get();\n        $book->pages()->whereNotIn('id', $pages->pluck('id'))->delete();\n\n        $resp = $this->asEditor()->get($book->getUrl());\n        $this->withHtml($resp)->assertElementContains('.content-wrap a.page:nth-child(1)', $pages[0]->name);\n        $this->withHtml($resp)->assertElementContains('.content-wrap a.page:nth-child(2)', $pages[1]->name);\n\n        $pages[0]->forceFill(['priority' => 10])->save();\n        $pages[1]->forceFill(['priority' => 5])->save();\n\n        $resp = $this->asEditor()->get($book->getUrl());\n        $this->withHtml($resp)->assertElementContains('.content-wrap a.page:nth-child(1)', $pages[1]->name);\n        $this->withHtml($resp)->assertElementContains('.content-wrap a.page:nth-child(2)', $pages[0]->name);\n    }\n}\n"
  },
  {
    "path": "tests/Sorting/MoveTest.php",
    "content": "<?php\n\nnamespace Tests\\Sorting;\n\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Chapter;\nuse BookStack\\Entities\\Models\\Page;\nuse Tests\\TestCase;\n\nclass MoveTest extends TestCase\n{\n    public function test_page_move_into_book()\n    {\n        $page = $this->entities->page();\n        $currentBook = $page->book;\n        $newBook = Book::query()->where('id', '!=', $currentBook->id)->first();\n\n        $resp = $this->asEditor()->get($page->getUrl('/move'));\n        $resp->assertSee('Move Page');\n\n        $movePageResp = $this->put($page->getUrl('/move'), [\n            'entity_selection' => 'book:' . $newBook->id,\n        ])->assertRedirect();\n        $page->refresh();\n\n        $movePageResp->assertRedirect($page->getUrl());\n        $this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book');\n\n        $newBookResp = $this->get($newBook->getUrl());\n        $newBookResp->assertSee('moved page');\n        $newBookResp->assertSee($page->name);\n    }\n\n    public function test_page_move_into_chapter()\n    {\n        $page = $this->entities->page();\n        $currentBook = $page->book;\n        $newBook = Book::query()->where('id', '!=', $currentBook->id)->first();\n        $newChapter = $newBook->chapters()->first();\n\n        $movePageResp = $this->actingAs($this->users->editor())->put($page->getUrl('/move'), [\n            'entity_selection' => 'chapter:' . $newChapter->id,\n        ]);\n        $page->refresh();\n\n        $movePageResp->assertRedirect($page->getUrl());\n        $this->assertTrue($page->book->id == $newBook->id, 'Page parent is now the new chapter');\n\n        $newChapterResp = $this->get($newChapter->getUrl());\n        $newChapterResp->assertSee($page->name);\n    }\n\n    public function test_page_move_from_chapter_to_book()\n    {\n        $oldChapter = Chapter::query()->first();\n        $page = $oldChapter->pages()->first();\n        $newBook = Book::query()->where('id', '!=', $oldChapter->book_id)->first();\n\n        $movePageResp = $this->actingAs($this->users->editor())->put($page->getUrl('/move'), [\n            'entity_selection' => 'book:' . $newBook->id,\n        ]);\n        $page->refresh();\n\n        $movePageResp->assertRedirect($page->getUrl());\n        $this->assertTrue($page->book->id == $newBook->id, 'Page parent is now the new book');\n        $this->assertTrue($page->chapter === null, 'Page has no parent chapter');\n\n        $newBookResp = $this->get($newBook->getUrl());\n        $newBookResp->assertSee($page->name);\n    }\n\n    public function test_page_move_requires_create_permissions_on_parent()\n    {\n        $page = $this->entities->page();\n        $currentBook = $page->book;\n        $newBook = Book::query()->where('id', '!=', $currentBook->id)->first();\n        $editor = $this->users->editor();\n\n        $this->permissions->setEntityPermissions($newBook, ['view', 'update', 'delete'], $editor->roles->all());\n\n        $movePageResp = $this->actingAs($editor)->put($page->getUrl('/move'), [\n            'entity_selection' => 'book:' . $newBook->id,\n        ]);\n        $this->assertPermissionError($movePageResp);\n\n        $this->permissions->setEntityPermissions($newBook, ['view', 'update', 'delete', 'create'], $editor->roles->all());\n        $movePageResp = $this->put($page->getUrl('/move'), [\n            'entity_selection' => 'book:' . $newBook->id,\n        ]);\n\n        $page->refresh();\n        $movePageResp->assertRedirect($page->getUrl());\n\n        $this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book');\n    }\n\n    public function test_page_move_requires_delete_permissions()\n    {\n        $page = $this->entities->page();\n        $currentBook = $page->book;\n        $newBook = Book::query()->where('id', '!=', $currentBook->id)->first();\n        $editor = $this->users->editor();\n\n        $this->permissions->setEntityPermissions($newBook, ['view', 'update', 'create', 'delete'], $editor->roles->all());\n        $this->permissions->setEntityPermissions($page, ['view', 'update', 'create'], $editor->roles->all());\n\n        $movePageResp = $this->actingAs($editor)->put($page->getUrl('/move'), [\n            'entity_selection' => 'book:' . $newBook->id,\n        ]);\n        $this->assertPermissionError($movePageResp);\n        $pageView = $this->get($page->getUrl());\n        $pageView->assertDontSee($page->getUrl('/move'));\n\n        $this->permissions->setEntityPermissions($page, ['view', 'update', 'create', 'delete'], $editor->roles->all());\n        $movePageResp = $this->put($page->getUrl('/move'), [\n            'entity_selection' => 'book:' . $newBook->id,\n        ]);\n\n        $page->refresh();\n        $movePageResp->assertRedirect($page->getUrl());\n        $this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book');\n    }\n\n    public function test_chapter_move()\n    {\n        $chapter = $this->entities->chapter();\n        $currentBook = $chapter->book;\n        $pageToCheck = $chapter->pages->first();\n        $newBook = Book::query()->where('id', '!=', $currentBook->id)->first();\n\n        $chapterMoveResp = $this->asEditor()->get($chapter->getUrl('/move'));\n        $chapterMoveResp->assertSee('Move Chapter');\n\n        $moveChapterResp = $this->put($chapter->getUrl('/move'), [\n            'entity_selection' => 'book:' . $newBook->id,\n        ]);\n\n        $chapter = Chapter::query()->find($chapter->id);\n        $moveChapterResp->assertRedirect($chapter->getUrl());\n        $this->assertTrue($chapter->book->id === $newBook->id, 'Chapter Book is now the new book');\n\n        $newBookResp = $this->get($newBook->getUrl());\n        $newBookResp->assertSee('moved chapter');\n        $newBookResp->assertSee($chapter->name);\n\n        $pageToCheck = Page::query()->find($pageToCheck->id);\n        $this->assertTrue($pageToCheck->book_id === $newBook->id, 'Chapter child page\\'s book id has changed to the new book');\n        $pageCheckResp = $this->get($pageToCheck->getUrl());\n        $pageCheckResp->assertSee($newBook->name);\n    }\n\n    public function test_chapter_move_requires_delete_permissions()\n    {\n        $chapter = $this->entities->chapter();\n        $currentBook = $chapter->book;\n        $newBook = Book::query()->where('id', '!=', $currentBook->id)->first();\n        $editor = $this->users->editor();\n\n        $this->permissions->setEntityPermissions($newBook, ['view', 'update', 'create', 'delete'], $editor->roles->all());\n        $this->permissions->setEntityPermissions($chapter, ['view', 'update', 'create'], $editor->roles->all());\n\n        $moveChapterResp = $this->actingAs($editor)->put($chapter->getUrl('/move'), [\n            'entity_selection' => 'book:' . $newBook->id,\n        ]);\n        $this->assertPermissionError($moveChapterResp);\n        $pageView = $this->get($chapter->getUrl());\n        $pageView->assertDontSee($chapter->getUrl('/move'));\n\n        $this->permissions->setEntityPermissions($chapter, ['view', 'update', 'create', 'delete'], $editor->roles->all());\n        $moveChapterResp = $this->put($chapter->getUrl('/move'), [\n            'entity_selection' => 'book:' . $newBook->id,\n        ]);\n\n        $chapter = Chapter::query()->find($chapter->id);\n        $moveChapterResp->assertRedirect($chapter->getUrl());\n        $this->assertTrue($chapter->book->id == $newBook->id, 'Page book is now the new book');\n    }\n\n    public function test_chapter_move_requires_create_permissions_in_new_book()\n    {\n        $chapter = $this->entities->chapter();\n        $currentBook = $chapter->book;\n        $newBook = Book::query()->where('id', '!=', $currentBook->id)->first();\n        $editor = $this->users->editor();\n\n        $this->permissions->setEntityPermissions($newBook, ['view', 'update', 'delete'], [$editor->roles->first()]);\n        $this->permissions->setEntityPermissions($chapter, ['view', 'update', 'create', 'delete'], [$editor->roles->first()]);\n\n        $moveChapterResp = $this->actingAs($editor)->put($chapter->getUrl('/move'), [\n            'entity_selection' => 'book:' . $newBook->id,\n        ]);\n        $this->assertPermissionError($moveChapterResp);\n\n        $this->permissions->setEntityPermissions($newBook, ['view', 'update', 'create', 'delete'], [$editor->roles->first()]);\n        $moveChapterResp = $this->put($chapter->getUrl('/move'), [\n            'entity_selection' => 'book:' . $newBook->id,\n        ]);\n\n        $chapter = Chapter::query()->find($chapter->id);\n        $moveChapterResp->assertRedirect($chapter->getUrl());\n        $this->assertTrue($chapter->book->id == $newBook->id, 'Page book is now the new book');\n    }\n\n    public function test_chapter_move_changes_book_for_deleted_pages_within()\n    {\n        /** @var Chapter $chapter */\n        $chapter = Chapter::query()->whereHas('pages')->first();\n        $currentBook = $chapter->book;\n        $pageToCheck = $chapter->pages->first();\n        $newBook = Book::query()->where('id', '!=', $currentBook->id)->first();\n\n        $pageToCheck->delete();\n\n        $this->asEditor()->put($chapter->getUrl('/move'), [\n            'entity_selection' => 'book:' . $newBook->id,\n        ]);\n\n        $pageToCheck->refresh();\n        $this->assertEquals($newBook->id, $pageToCheck->book_id);\n    }\n}\n"
  },
  {
    "path": "tests/Sorting/SortRuleTest.php",
    "content": "<?php\n\nnamespace Tests\\Sorting;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Sorting\\SortRule;\nuse BookStack\\Sorting\\SortRuleOperation;\nuse Tests\\Api\\TestsApi;\nuse Tests\\TestCase;\n\nclass SortRuleTest extends TestCase\n{\n    use TestsApi;\n\n    public function test_manage_settings_permission_required()\n    {\n        $rule = SortRule::factory()->create();\n        $user = $this->users->viewer();\n        $this->actingAs($user);\n\n        $actions = [\n            ['GET', '/settings/sorting'],\n            ['POST', '/settings/sorting/rules'],\n            ['GET', \"/settings/sorting/rules/{$rule->id}\"],\n            ['PUT', \"/settings/sorting/rules/{$rule->id}\"],\n            ['DELETE', \"/settings/sorting/rules/{$rule->id}\"],\n        ];\n\n        foreach ($actions as [$method, $path]) {\n            $resp = $this->call($method, $path);\n            $this->assertPermissionError($resp);\n        }\n\n        $this->permissions->grantUserRolePermissions($user, ['settings-manage']);\n\n        foreach ($actions as [$method, $path]) {\n            $resp = $this->call($method, $path);\n            $this->assertNotPermissionError($resp);\n        }\n    }\n\n    public function test_create_flow()\n    {\n        $resp = $this->asAdmin()->get('/settings/sorting');\n        $this->withHtml($resp)->assertLinkExists(url('/settings/sorting/rules/new'));\n\n        $resp = $this->get('/settings/sorting/rules/new');\n        $this->withHtml($resp)->assertElementExists('form[action$=\"/settings/sorting/rules\"] input[name=\"name\"]');\n        $resp->assertSeeText('Name - Alphabetical (Asc)');\n\n        $details = ['name' => 'My new sort', 'sequence' => 'name_asc'];\n        $resp = $this->post('/settings/sorting/rules', $details);\n        $resp->assertRedirect('/settings/sorting');\n\n        $this->assertActivityExists(ActivityType::SORT_RULE_CREATE);\n        $this->assertDatabaseHas('sort_rules', $details);\n    }\n\n    public function test_listing_in_settings()\n    {\n        $rule = SortRule::factory()->create(['name' => 'My super sort rule', 'sequence' => 'name_asc']);\n        $books = Book::query()->limit(5)->get();\n        foreach ($books as $book) {\n            $book->sort_rule_id = $rule->id;\n            $book->save();\n        }\n\n        $resp = $this->asAdmin()->get('/settings/sorting');\n        $resp->assertSeeText('My super sort rule');\n        $resp->assertSeeText('Name - Alphabetical (Asc)');\n        $this->withHtml($resp)->assertElementContains('.item-list-row [title=\"Assigned to 5 Books\"]', '5');\n    }\n\n    public function test_update_flow()\n    {\n        $rule = SortRule::factory()->create(['name' => 'My sort rule to update', 'sequence' => 'name_asc']);\n\n        $resp = $this->asAdmin()->get(\"/settings/sorting/rules/{$rule->id}\");\n        $respHtml = $this->withHtml($resp);\n        $respHtml->assertElementContains('.configured-option-list', 'Name - Alphabetical (Asc)');\n        $respHtml->assertElementNotContains('.available-option-list', 'Name - Alphabetical (Asc)');\n\n        $updateData = ['name' => 'My updated sort', 'sequence' => 'name_desc,chapters_last'];\n        $resp = $this->put(\"/settings/sorting/rules/{$rule->id}\", $updateData);\n\n        $resp->assertRedirect('/settings/sorting');\n        $this->assertActivityExists(ActivityType::SORT_RULE_UPDATE);\n        $this->assertDatabaseHas('sort_rules', $updateData);\n    }\n\n    public function test_update_triggers_resort_on_assigned_books()\n    {\n        $book = $this->entities->bookHasChaptersAndPages();\n        $chapter = $book->chapters()->first();\n        $rule = SortRule::factory()->create(['name' => 'My sort rule to update', 'sequence' => 'name_asc']);\n        $book->sort_rule_id = $rule->id;\n        $book->save();\n        $chapter->priority = 10000;\n        $chapter->save();\n\n        $resp = $this->asAdmin()->put(\"/settings/sorting/rules/{$rule->id}\", ['name' => $rule->name, 'sequence' => 'chapters_last']);\n        $resp->assertRedirect('/settings/sorting');\n\n        $chapter->refresh();\n        $this->assertNotEquals(10000, $chapter->priority);\n    }\n\n    public function test_delete_flow()\n    {\n        $rule = SortRule::factory()->create();\n\n        $resp = $this->asAdmin()->get(\"/settings/sorting/rules/{$rule->id}\");\n        $resp->assertSeeText('Delete Sort Rule');\n\n        $resp = $this->delete(\"settings/sorting/rules/{$rule->id}\");\n        $resp->assertRedirect('/settings/sorting');\n\n        $this->assertActivityExists(ActivityType::SORT_RULE_DELETE);\n        $this->assertDatabaseMissing('sort_rules', ['id' => $rule->id]);\n    }\n\n    public function test_delete_requires_confirmation_if_books_assigned()\n    {\n        $rule = SortRule::factory()->create();\n        $books = Book::query()->limit(5)->get();\n        foreach ($books as $book) {\n            $book->sort_rule_id = $rule->id;\n            $book->save();\n        }\n\n        $resp = $this->asAdmin()->get(\"/settings/sorting/rules/{$rule->id}\");\n        $resp->assertSeeText('Delete Sort Rule');\n\n        $resp = $this->delete(\"settings/sorting/rules/{$rule->id}\");\n        $resp->assertRedirect(\"/settings/sorting/rules/{$rule->id}#delete\");\n        $resp = $this->followRedirects($resp);\n\n        $resp->assertSeeText('This sort rule is currently used on 5 book(s). Are you sure you want to delete this?');\n        $this->assertDatabaseHas('sort_rules', ['id' => $rule->id]);\n\n        $resp = $this->delete(\"settings/sorting/rules/{$rule->id}\", ['confirm' => 'true']);\n        $resp->assertRedirect('/settings/sorting');\n        $this->assertDatabaseMissing('sort_rules', ['id' => $rule->id]);\n        $this->assertDatabaseMissing('entity_container_data', ['sort_rule_id' => $rule->id]);\n    }\n\n    public function test_page_create_triggers_book_sort()\n    {\n        $book = $this->entities->bookHasChaptersAndPages();\n        $rule = SortRule::factory()->create(['sequence' => 'name_asc,chapters_first']);\n        $book->sort_rule_id = $rule->id;\n        $book->save();\n\n        $resp = $this->actingAsApiEditor()->post(\"/api/pages\", [\n            'book_id' => $book->id,\n            'name' => '1111 page',\n            'markdown' => 'Hi'\n        ]);\n        $resp->assertOk();\n\n        $this->assertDatabaseHasEntityData('page', [\n            'book_id' => $book->id,\n            'name' => '1111 page',\n            'priority' => $book->chapters()->count() + 1,\n        ]);\n    }\n\n    public function test_auto_book_sort_does_not_touch_timestamps()\n    {\n        $book = $this->entities->bookHasChaptersAndPages();\n        $rule = SortRule::factory()->create(['sequence' => 'name_asc,chapters_first']);\n        $book->sort_rule_id = $rule->id;\n        $book->save();\n        $page = $book->pages()->first();\n        $chapter = $book->chapters()->first();\n\n        $resp = $this->actingAsApiEditor()->put(\"/api/pages/{$page->id}\", [\n            'name' => '1111 page',\n        ]);\n        $resp->assertOk();\n\n        $oldTime = $chapter->updated_at->unix();\n        $oldPriority = $chapter->priority;\n        $chapter->refresh();\n        $this->assertEquals($oldTime, $chapter->updated_at->unix());\n        $this->assertNotEquals($oldPriority, $chapter->priority);\n    }\n\n    public function test_name_alphabetical_ordering()\n    {\n        $book = Book::factory()->create();\n        $rule = SortRule::factory()->create(['sequence' => 'name_asc']);\n        $book->sort_rule_id = $rule->id;\n        $book->save();\n        $this->permissions->regenerateForEntity($book);\n\n        $namesToAdd = [\n            \"Beans\",\n            \"bread\",\n            \"Éclaire\",\n            \"egg\",\n            \"É😀ire\",\n            \"É🫠ire\",\n            \"Milk\",\n            \"pizza\",\n            \"Tomato\",\n        ];\n\n        $reverseNamesToAdd = array_reverse($namesToAdd);\n        foreach ($reverseNamesToAdd as $name) {\n            $this->actingAsApiEditor()->post(\"/api/pages\", [\n                'book_id' => $book->id,\n                'name' => $name,\n                'markdown' => 'Hello'\n            ]);\n        }\n\n        foreach ($namesToAdd as $index => $name) {\n            $this->assertDatabaseHasEntityData('page', [\n                'book_id' => $book->id,\n                'name' => $name,\n                'priority' => $index + 1,\n            ]);\n        }\n    }\n\n    public function test_name_numeric_ordering()\n    {\n        $book = Book::factory()->create();\n        $rule = SortRule::factory()->create(['sequence' => 'name_numeric_asc']);\n        $book->sort_rule_id = $rule->id;\n        $book->save();\n        $this->permissions->regenerateForEntity($book);\n\n        $namesToAdd = [\n            \"1 - Pizza\",\n            \"2.0 - Tomato\",\n            \"2.5 - Beans\",\n            \"10 - Bread\",\n            \"20 - Milk\",\n        ];\n\n        $reverseNamesToAdd = array_reverse($namesToAdd);\n        foreach ($reverseNamesToAdd as $name) {\n            $this->actingAsApiEditor()->post(\"/api/pages\", [\n                'book_id' => $book->id,\n                'name' => $name,\n                'markdown' => 'Hello'\n            ]);\n        }\n\n        foreach ($namesToAdd as $index => $name) {\n            $this->assertDatabaseHasEntityData('page', [\n                'book_id' => $book->id,\n                'name' => $name,\n                'priority' => $index + 1,\n            ]);\n        }\n    }\n\n    public function test_each_sort_rule_operation_has_a_comparison_function()\n    {\n        $operations = SortRuleOperation::cases();\n\n        foreach ($operations as $operation) {\n            $comparisonFunc = $operation->getSortFunction();\n            $this->assertIsCallable($comparisonFunc);\n        }\n    }\n}\n"
  },
  {
    "path": "tests/StatusTest.php",
    "content": "<?php\n\nnamespace Tests;\n\nuse Exception;\nuse Illuminate\\Cache\\ArrayStore;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Session;\nuse Mockery;\n\nclass StatusTest extends TestCase\n{\n    public function test_returns_json_with_expected_results()\n    {\n        $resp = $this->get('/status');\n        $resp->assertStatus(200);\n        $resp->assertJson([\n            'database' => true,\n            'cache'    => true,\n            'session'  => true,\n        ]);\n    }\n\n    public function test_returns_500_status_and_false_on_db_error()\n    {\n        DB::shouldReceive('table')->andThrow(new Exception());\n\n        $resp = $this->get('/status');\n        $resp->assertStatus(500);\n        $resp->assertJson([\n            'database' => false,\n        ]);\n    }\n\n    public function test_returns_500_status_and_false_on_wrong_cache_return()\n    {\n        $mockStore = Mockery::mock(new ArrayStore())->makePartial();\n        Cache::swap($mockStore);\n        $mockStore->shouldReceive('pull')->andReturn('cat');\n\n        $resp = $this->get('/status');\n        $resp->assertStatus(500);\n        $resp->assertJson([\n            'cache' => false,\n        ]);\n    }\n\n    public function test_returns_500_status_and_false_on_wrong_session_return()\n    {\n        $session = Session::getFacadeRoot();\n        $mockSession = Mockery::mock($session)->makePartial();\n        Session::swap($mockSession);\n        $mockSession->shouldReceive('get')->andReturn('cat');\n\n        $resp = $this->get('/status');\n        $resp->assertStatus(500);\n        $resp->assertJson([\n            'session' => false,\n        ]);\n    }\n}\n"
  },
  {
    "path": "tests/TestCase.php",
    "content": "<?php\n\nnamespace Tests;\n\nuse BookStack\\Entities\\Models\\Entity;\nuse BookStack\\Http\\HttpClientHistory;\nuse BookStack\\Http\\HttpRequestService;\nuse BookStack\\Settings\\SettingService;\nuse Exception;\nuse Illuminate\\Contracts\\Console\\Kernel;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Foundation\\Testing\\TestCase as BaseTestCase;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Support\\Env;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\File;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Testing\\Assert as PHPUnit;\nuse Illuminate\\Testing\\Constraints\\HasInDatabase;\nuse Monolog\\Handler\\TestHandler;\nuse Monolog\\Logger;\nuse Ssddanbrown\\AssertHtml\\TestsHtml;\nuse Tests\\Helpers\\EntityProvider;\nuse Tests\\Helpers\\FileProvider;\nuse Tests\\Helpers\\PermissionsProvider;\nuse Tests\\Helpers\\TestServiceProvider;\nuse Tests\\Helpers\\UserRoleProvider;\n\nabstract class TestCase extends BaseTestCase\n{\n    use CreatesApplication;\n    use DatabaseTransactions;\n    use TestsHtml;\n\n    protected EntityProvider $entities;\n    protected UserRoleProvider $users;\n    protected PermissionsProvider $permissions;\n    protected FileProvider $files;\n\n    protected function setUp(): void\n    {\n        $this->entities = new EntityProvider();\n        $this->users = new UserRoleProvider();\n        $this->permissions = new PermissionsProvider($this->users);\n        $this->files = new FileProvider();\n\n        parent::setUp();\n\n        // We can uncomment the below to run tests with failings upon deprecations.\n        // Can't leave on since some deprecations can only be fixed upstream.\n         // $this->withoutDeprecationHandling();\n    }\n\n    /**\n     * The base URL to use while testing the application.\n     */\n    protected string $baseUrl = 'http://localhost';\n\n    /**\n     * Creates the application.\n     *\n     * @return \\Illuminate\\Foundation\\Application\n     */\n    public function createApplication()\n    {\n        /** @var \\Illuminate\\Foundation\\Application  $app */\n        $app = require __DIR__ . '/../bootstrap/app.php';\n        $app->register(TestServiceProvider::class);\n        $app->make(Kernel::class)->bootstrap();\n\n        return $app;\n    }\n\n    /**\n     * Set the current user context to be an admin.\n     */\n    public function asAdmin()\n    {\n        return $this->actingAs($this->users->admin());\n    }\n\n    /**\n     * Set the current user context to be an editor.\n     */\n    public function asEditor()\n    {\n        return $this->actingAs($this->users->editor());\n    }\n\n    /**\n     * Set the current user context to be a viewer.\n     */\n    public function asViewer()\n    {\n        return $this->actingAs($this->users->viewer());\n    }\n\n    /**\n     * Quickly sets an array of settings.\n     */\n    protected function setSettings(array $settingsArray): void\n    {\n        $settings = app(SettingService::class);\n        foreach ($settingsArray as $key => $value) {\n            $settings->put($key, $value);\n        }\n    }\n\n    /**\n     * Mock the http client used in BookStack http calls.\n     */\n    protected function mockHttpClient(array $responses = []): HttpClientHistory\n    {\n        return $this->app->make(HttpRequestService::class)->mockClient($responses);\n    }\n\n    /**\n     * Run a set test with the given env variable.\n     * Remembers the original and resets the value after test.\n     * Database config is juggled so the value can be restored when\n     * parallel testing are used, where multiple databases exist.\n     */\n    protected function runWithEnv(array $valuesByKey, callable $callback, bool $handleDatabase = true): void\n    {\n        Env::disablePutenv();\n        $originals = [];\n        foreach ($valuesByKey as $key => $value) {\n            $originals[$key] = $_SERVER[$key] ?? null;\n\n            if (is_null($value)) {\n                unset($_SERVER[$key]);\n            } else {\n                $_SERVER[$key] = $value;\n            }\n        }\n\n        $database = config('database.connections.mysql_testing.database');\n        $this->refreshApplication();\n\n        if ($handleDatabase) {\n            DB::purge();\n            config()->set('database.connections.mysql_testing.database', $database);\n            DB::beginTransaction();\n        }\n\n        $callback();\n\n        if ($handleDatabase) {\n            DB::rollBack();\n        }\n\n        foreach ($originals as $key => $value) {\n            if (is_null($value)) {\n                unset($_SERVER[$key]);\n            } else {\n                $_SERVER[$key] = $value;\n            }\n        }\n    }\n\n    protected function usingThemeFolder(callable $callback): void\n    {\n        // Create a folder and configure a theme\n        $themeFolderName = 'testing_theme_' . str_shuffle(rtrim(base64_encode(time()), '='));\n        config()->set('view.theme', $themeFolderName);\n        $themeFolderPath = theme_path('');\n\n        // Create a theme folder and clean it up on application tear-down\n        File::makeDirectory($themeFolderPath);\n        $this->beforeApplicationDestroyed(fn() => File::deleteDirectory($themeFolderPath));\n\n        // Run provided callback with the theme env option set\n        $this->runWithEnv(['APP_THEME' => $themeFolderName], function () use ($callback, $themeFolderName) {\n            call_user_func($callback, $themeFolderName);\n        });\n    }\n\n    /**\n     * Check the keys and properties in the given map to include\n     * exist, albeit not exclusively, within the map to check.\n     */\n    protected function assertArrayMapIncludes(array $mapToInclude, array $mapToCheck, string $message = ''): void\n    {\n        $passed = true;\n\n        foreach ($mapToInclude as $key => $value) {\n            if (!isset($mapToCheck[$key]) || $mapToCheck[$key] !== $mapToInclude[$key]) {\n                $passed = false;\n            }\n        }\n\n        $toIncludeStr = print_r($mapToInclude, true);\n        $toCheckStr = print_r($mapToCheck, true);\n        self::assertThat($passed, self::isTrue(), \"Failed asserting that given map:\\n\\n{$toCheckStr}\\n\\nincludes:\\n\\n{$toIncludeStr}\");\n    }\n\n    /**\n     * Assert a permission error has occurred.\n     */\n    protected function assertPermissionError($response)\n    {\n        PHPUnit::assertTrue($this->isPermissionError($response->baseResponse ?? $response->response), 'Failed asserting the response contains a permission error.');\n    }\n\n    /**\n     * Assert a permission error has occurred.\n     */\n    protected function assertNotPermissionError($response)\n    {\n        PHPUnit::assertFalse($this->isPermissionError($response->baseResponse ?? $response->response), 'Failed asserting the response does not contain a permission error.');\n    }\n\n    /**\n     * Check if the given response is a permission error.\n     */\n    private function isPermissionError($response): bool\n    {\n        if ($response->status() === 403 && $response instanceof JsonResponse) {\n            $errMessage = $response->getData(true)['error']['message'] ?? '';\n            return str_contains($errMessage, 'do not have permission');\n        }\n\n        return $response->status() === 302\n            && $response->headers->get('Location') === url('/')\n            && str_starts_with(session()->pull('error', ''), 'You do not have permission to access');\n    }\n\n    /**\n     * Assert that the session has a particular error notification message set.\n     */\n    protected function assertSessionError(string $message)\n    {\n        $error = session()->get('error');\n        PHPUnit::assertTrue($error === $message, \"Failed asserting the session contains an error. \\nFound: {$error}\\nExpecting: {$message}\");\n    }\n\n    /**\n     * Assert the session contains a specific entry.\n     */\n    protected function assertSessionHas(string $key): self\n    {\n        $this->assertTrue(session()->has($key), \"Session does not contain a [{$key}] entry\");\n\n        return $this;\n    }\n\n    protected function assertNotificationContains(\\Illuminate\\Testing\\TestResponse $resp, string $text)\n    {\n        return $this->withHtml($resp)->assertElementContains('.notification[role=\"alert\"]', $text);\n    }\n\n    /**\n     * Set a test handler as the logging interface for the application.\n     * Allows capture of logs for checking against during tests.\n     */\n    protected function withTestLogger(): TestHandler\n    {\n        $monolog = new Logger('testing');\n        $testHandler = new TestHandler();\n        $monolog->pushHandler($testHandler);\n\n        Log::extend('testing', function () use ($monolog) {\n            return $monolog;\n        });\n        Log::setDefaultDriver('testing');\n\n        return $testHandler;\n    }\n\n    /**\n     * Assert that an activity entry exists of the given key.\n     * Checks the activity belongs to the given entity if provided.\n     */\n    protected function assertActivityExists(string $type, ?Entity $entity = null, string $detail = '')\n    {\n        $detailsToCheck = ['type' => $type];\n\n        if ($entity) {\n            $detailsToCheck['loggable_type'] = $entity->getMorphClass();\n            $detailsToCheck['loggable_id'] = $entity->id;\n        }\n\n        if ($detail) {\n            $detailsToCheck['detail'] = $detail;\n        }\n\n        $this->assertDatabaseHas('activities', $detailsToCheck);\n    }\n\n    /**\n     * Assert the database has the given data for an entity type.\n     */\n    protected function assertDatabaseHasEntityData(string $type, array $data = []): self\n    {\n        $entityFields = array_intersect_key($data, array_flip(Entity::$commonFields));\n        $extraFields = array_diff_key($data, $entityFields);\n        $extraTable = $type === 'page' ? 'entity_page_data' : 'entity_container_data';\n        $entityFields['type'] = $type;\n\n        $this->assertThat(\n            $this->getTable('entities'),\n            new HasInDatabase($this->getConnection(null, 'entities'), $entityFields)\n        );\n\n        if (!empty($extraFields)) {\n            $id = $entityFields['id'] ?? DB::table($this->getTable('entities'))\n                ->where($entityFields)->orderByDesc('id')->first()->id ?? null;\n            if (is_null($id)) {\n                throw new Exception('Failed to find entity id for asserting database data');\n            }\n\n            if ($type !== 'page') {\n                $extraFields['entity_id'] = $id;\n                $extraFields['entity_type'] = $type;\n            } else {\n                $extraFields['page_id'] = $id;\n            }\n\n            $this->assertThat(\n                $this->getTable($extraTable),\n                new HasInDatabase($this->getConnection(null, $extraTable), $extraFields)\n            );\n        }\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "tests/Theme/LogicalThemeEventsTest.php",
    "content": "<?php\n\nnamespace Tests\\Theme;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Activity\\DispatchWebhookJob;\nuse BookStack\\Activity\\Models\\Webhook;\nuse BookStack\\Entities\\Models\\Book;\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Tools\\PageContent;\nuse BookStack\\Facades\\Theme;\nuse BookStack\\Theming\\ThemeEvents;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse League\\CommonMark\\Environment\\Environment;\nuse Tests\\TestCase;\n\nclass LogicalThemeEventsTest extends TestCase\n{\n    public function test_commonmark_environment_configure()\n    {\n        $callbackCalled = false;\n        $callback = function ($environment) use (&$callbackCalled) {\n            $this->assertInstanceOf(Environment::class, $environment);\n            $callbackCalled = true;\n\n            return $environment;\n        };\n        Theme::listen(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $callback);\n\n        $page = $this->entities->page();\n        $content = new PageContent($page);\n        $content->setNewMarkdown('# test', $this->users->editor());\n\n        $this->assertTrue($callbackCalled);\n    }\n\n    public function test_web_middleware_before()\n    {\n        $callbackCalled = false;\n        $requestParam = null;\n        $callback = function ($request) use (&$callbackCalled, &$requestParam) {\n            $requestParam = $request;\n            $callbackCalled = true;\n        };\n\n        Theme::listen(ThemeEvents::WEB_MIDDLEWARE_BEFORE, $callback);\n        $this->get('/login', ['Donkey' => 'cat']);\n\n        $this->assertTrue($callbackCalled);\n        $this->assertInstanceOf(Request::class, $requestParam);\n        $this->assertEquals('cat', $requestParam->header('donkey'));\n    }\n\n    public function test_web_middleware_before_return_val_used_as_response()\n    {\n        $callback = function (Request $request) {\n            return response('cat', 412);\n        };\n\n        Theme::listen(ThemeEvents::WEB_MIDDLEWARE_BEFORE, $callback);\n        $resp = $this->get('/login', ['Donkey' => 'cat']);\n        $resp->assertSee('cat');\n        $resp->assertStatus(412);\n    }\n\n    public function test_web_middleware_after()\n    {\n        $callbackCalled = false;\n        $requestParam = null;\n        $responseParam = null;\n        $callback = function ($request, Response $response) use (&$callbackCalled, &$requestParam, &$responseParam) {\n            $requestParam = $request;\n            $responseParam = $response;\n            $callbackCalled = true;\n            $response->header('donkey', 'cat123');\n        };\n\n        Theme::listen(ThemeEvents::WEB_MIDDLEWARE_AFTER, $callback);\n\n        $resp = $this->get('/login', ['Donkey' => 'cat']);\n        $this->assertTrue($callbackCalled);\n        $this->assertInstanceOf(Request::class, $requestParam);\n        $this->assertInstanceOf(Response::class, $responseParam);\n        $resp->assertHeader('donkey', 'cat123');\n    }\n\n    public function test_web_middleware_after_return_val_used_as_response()\n    {\n        $callback = function () {\n            return response('cat456', 443);\n        };\n\n        Theme::listen(ThemeEvents::WEB_MIDDLEWARE_AFTER, $callback);\n\n        $resp = $this->get('/login', ['Donkey' => 'cat']);\n        $resp->assertSee('cat456');\n        $resp->assertStatus(443);\n    }\n\n    public function test_auth_login_standard()\n    {\n        $args = [];\n        $callback = function (...$eventArgs) use (&$args) {\n            $args = $eventArgs;\n        };\n\n        Theme::listen(ThemeEvents::AUTH_LOGIN, $callback);\n        $this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']);\n\n        $this->assertCount(2, $args);\n        $this->assertEquals('standard', $args[0]);\n        $this->assertInstanceOf(User::class, $args[1]);\n    }\n\n    public function test_auth_register_standard()\n    {\n        $args = [];\n        $callback = function (...$eventArgs) use (&$args) {\n            $args = $eventArgs;\n        };\n        Theme::listen(ThemeEvents::AUTH_REGISTER, $callback);\n        $this->setSettings(['registration-enabled' => 'true']);\n\n        $user = User::factory()->make();\n        $this->post('/register', ['email' => $user->email, 'name' => $user->name, 'password' => 'password']);\n\n        $this->assertCount(2, $args);\n        $this->assertEquals('standard', $args[0]);\n        $this->assertInstanceOf(User::class, $args[1]);\n    }\n\n    public function test_auth_pre_register()\n    {\n        $args = [];\n        $callback = function (...$eventArgs) use (&$args) {\n            $args = $eventArgs;\n        };\n        Theme::listen(ThemeEvents::AUTH_PRE_REGISTER, $callback);\n        $this->setSettings(['registration-enabled' => 'true']);\n\n        $user = User::factory()->make();\n        $this->post('/register', ['email' => $user->email, 'name' => $user->name, 'password' => 'password']);\n\n        $this->assertCount(2, $args);\n        $this->assertEquals('standard', $args[0]);\n        $this->assertEquals([\n            'email' => $user->email,\n            'name' => $user->name,\n            'password' => 'password',\n        ], $args[1]);\n        $this->assertDatabaseHas('users', ['email' => $user->email]);\n    }\n\n    public function test_auth_pre_register_with_false_return_blocks_registration()\n    {\n        $callback = function () {\n            return false;\n        };\n        Theme::listen(ThemeEvents::AUTH_PRE_REGISTER, $callback);\n        $this->setSettings(['registration-enabled' => 'true']);\n\n        $user = User::factory()->make();\n        $resp = $this->post('/register', ['email' => $user->email, 'name' => $user->name, 'password' => 'password']);\n        $resp->assertRedirect('/login');\n        $this->assertSessionError('User account could not be registered for the provided details');\n        $this->assertDatabaseMissing('users', ['email' => $user->email]);\n    }\n\n    public function test_webhook_call_before()\n    {\n        $args = [];\n        $callback = function (...$eventArgs) use (&$args) {\n            $args = $eventArgs;\n\n            return ['test' => 'hello!'];\n        };\n        Theme::listen(ThemeEvents::WEBHOOK_CALL_BEFORE, $callback);\n\n        $responses = $this->mockHttpClient([new \\GuzzleHttp\\Psr7\\Response(200, [], '')]);\n\n        $webhook = new Webhook(['name' => 'Test webhook', 'endpoint' => 'https://example.com']);\n        $webhook->save();\n        $event = ActivityType::PAGE_UPDATE;\n        $detail = Page::query()->first();\n\n        dispatch((new DispatchWebhookJob($webhook, $event, $detail)));\n\n        $this->assertCount(5, $args);\n        $this->assertEquals($event, $args[0]);\n        $this->assertEquals($webhook->id, $args[1]->id);\n        $this->assertEquals($detail->id, $args[2]->id);\n\n        $this->assertEquals(1, $responses->requestCount());\n        $request = $responses->latestRequest();\n        $reqData = json_decode($request->getBody(), true);\n        $this->assertEquals('hello!', $reqData['test']);\n    }\n\n    public function test_activity_logged()\n    {\n        $book = $this->entities->book();\n        $args = [];\n        $callback = function (...$eventArgs) use (&$args) {\n            $args = $eventArgs;\n        };\n\n        Theme::listen(ThemeEvents::ACTIVITY_LOGGED, $callback);\n        $this->asEditor()->put($book->getUrl(), ['name' => 'My cool update book!']);\n\n        $this->assertCount(2, $args);\n        $this->assertEquals(ActivityType::BOOK_UPDATE, $args[0]);\n        $this->assertTrue($args[1] instanceof Book);\n        $this->assertEquals($book->id, $args[1]->id);\n    }\n\n    public function test_page_content_pre_store_fires_on_page_save()\n    {\n        $page = $this->entities->page();\n\n        $args = [];\n        $callback = function (...$eventArgs) use (&$args) {\n            $args = $eventArgs;\n            return '<p>New Content!</p>';\n        };\n\n        Theme::listen(ThemeEvents::PAGE_CONTENT_PRE_STORE, $callback);\n\n        $this->asEditor();\n        $this->entities->updatePage($page, ['name' => 'My cool update page!', 'html' => '<p>Old content!</p>']);\n\n        $this->assertCount(2, $args);\n        $this->assertEquals($page->id, $args[1]->id);\n        $this->assertEquals('<p id=\"bkmrk-old-content%21\">Old content!</p>', $args[0]);\n\n        $newPageHtml = $page->refresh()->html;\n        $this->assertEquals('<p>New Content!</p>', $newPageHtml);\n    }\n\n    public function test_page_content_pre_store_does_not_change_content_if_nothing_returned()\n    {\n        $page = $this->entities->page();\n        Theme::listen(ThemeEvents::PAGE_CONTENT_PRE_STORE, fn() => null);\n\n        $this->asEditor();\n        $this->entities->updatePage($page, ['name' => 'My cool update page!', 'html' => '<p>Old content!</p>']);\n\n        $newPageHtml = $page->refresh()->html;\n        $this->assertEquals('<p id=\"bkmrk-old-content%21\">Old content!</p>', $newPageHtml);\n    }\n\n    public function test_page_content_post_render_fires_on_page_view()\n    {\n        $page = $this->entities->page();\n        $page->html = '<p>Old content!</p>';\n        $page->save();\n\n        $args = [];\n        $callback = function (...$eventArgs) use (&$args) {\n            $args = $eventArgs;\n            return '<p>New postrendercontentforyou!</p>';\n        };\n\n        Theme::listen(ThemeEvents::PAGE_CONTENT_POST_RENDER, $callback);\n\n        $resp = $this->asEditor()->get($page->getUrl());\n        $resp->assertSee('<p>New postrendercontentforyou!</p>', false);\n\n        $this->assertCount(2, $args);\n        $this->assertEquals($page->id, $args[1]->id);\n        $this->assertEquals('<p>Old content!</p>', $args[0]);\n    }\n\n    public function test_page_content_post_render_returns_original_content_if_no_return()\n    {\n        $page = $this->entities->page();\n        $page->html = '<p>Old content!</p>';\n        $page->save();\n\n        $args = [];\n        $callback = function (...$eventArgs) use (&$args) {\n            $args = $eventArgs;\n        };\n\n        Theme::listen(ThemeEvents::PAGE_CONTENT_POST_RENDER, $callback);\n\n        $resp = $this->asEditor()->get($page->getUrl());\n        $resp->assertSee('<p>Old content!</p>', false);\n\n        $this->assertCount(2, $args);\n    }\n\n    public function test_page_include_parse()\n    {\n        /** @var Page $page */\n        /** @var Page $otherPage */\n        $page = $this->entities->page();\n        $otherPage = Page::query()->where('id', '!=', $page->id)->first();\n        $otherPage->html = '<p id=\"bkmrk-cool\">This is a really cool section</p>';\n        $page->html = \"<p>{{@{$otherPage->id}#bkmrk-cool}}</p>\";\n        $page->save();\n        $otherPage->save();\n\n        $args = [];\n        $callback = function (...$eventArgs) use (&$args) {\n            $args = $eventArgs;\n\n            return '<strong>Big &amp; content replace surprise!</strong>';\n        };\n\n        Theme::listen(ThemeEvents::PAGE_INCLUDE_PARSE, $callback);\n        $resp = $this->asEditor()->get($page->getUrl());\n        $this->withHtml($resp)->assertElementContains('.page-content strong', 'Big & content replace surprise!');\n\n        $this->assertCount(4, $args);\n        $this->assertEquals($otherPage->id . '#bkmrk-cool', $args[0]);\n        $this->assertEquals('This is a really cool section', $args[1]);\n        $this->assertTrue($args[2] instanceof Page);\n        $this->assertTrue($args[3] instanceof Page);\n        $this->assertEquals($page->id, $args[2]->id);\n        $this->assertEquals($otherPage->id, $args[3]->id);\n    }\n\n    public function test_routes_register_web_and_web_auth()\n    {\n        $functionsContent = <<<'END'\n<?php\nuse BookStack\\Theming\\ThemeEvents;\nuse BookStack\\Facades\\Theme;\nuse Illuminate\\Routing\\Router;\nTheme::listen(ThemeEvents::ROUTES_REGISTER_WEB, function (Router $router) {\n    $router->get('/cat', fn () => 'cat')->name('say.cat');\n});\nTheme::listen(ThemeEvents::ROUTES_REGISTER_WEB_AUTH, function (Router $router) {\n    $router->get('/dog', fn () => 'dog')->name('say.dog');\n});\nEND;\n\n        $this->usingThemeFolder(function () use ($functionsContent) {\n\n            $functionsFile = theme_path('functions.php');\n            file_put_contents($functionsFile, $functionsContent);\n\n            $app = $this->createApplication();\n            /** @var \\Illuminate\\Routing\\Router $router */\n            $router = $app->get('router');\n\n            /** @var \\Illuminate\\Routing\\Route $catRoute */\n            $catRoute = $router->getRoutes()->getRoutesByName()['say.cat'];\n            $this->assertEquals(['web'], $catRoute->middleware());\n\n            /** @var \\Illuminate\\Routing\\Route $dogRoute */\n            $dogRoute = $router->getRoutes()->getRoutesByName()['say.dog'];\n            $this->assertEquals(['web', 'auth'], $dogRoute->middleware());\n        });\n    }\n\n    public function test_register_views_to_insert_views_before_and_after()\n    {\n        $this->usingThemeFolder(function (string $folder) {\n            $before = 'this-is-my-before-header-string';\n            $afterA = 'this-is-my-after-header-string-a';\n            $afterB = 'this-is-my-after-header-string-b';\n            $afterC = 'this-is-my-after-header-string-{{ 1+51 }}';\n\n            $functionsContent = <<<'CONTENT'\n<?php use BookStack\\Facades\\Theme;\nuse BookStack\\Theming\\ThemeEvents;\nuse BookStack\\Theming\\ThemeViews;\nTheme::listen(ThemeEvents::THEME_REGISTER_VIEWS, function (ThemeViews $themeViews) {\n    $themeViews->renderBefore('layouts.parts.header', 'before', 4);\n    $themeViews->renderAfter('layouts.parts.header', 'after-a', 4);\n    $themeViews->renderAfter('layouts.parts.header', 'after-b', 1);\n    $themeViews->renderAfter('layouts.parts.header', 'after-c', 12);\n});\nCONTENT;\n\n            $viewDir = theme_path();\n            file_put_contents($viewDir . '/functions.php', $functionsContent);\n            file_put_contents($viewDir . '/before.blade.php', $before);\n            file_put_contents($viewDir . '/after-a.blade.php', $afterA);\n            file_put_contents($viewDir . '/after-b.blade.php', $afterB);\n            file_put_contents($viewDir . '/after-c.blade.php', $afterC);\n\n            $this->refreshApplication();\n            $this->artisan('view:clear');\n\n            $resp = $this->get('/login');\n            $resp->assertSee($before);\n            // Ensure ordering of the multiple after views\n            $resp->assertSee($afterB . \"\\n\" . $afterA . \"\\nthis-is-my-after-header-string-52\");\n        });\n\n        $this->artisan('view:clear');\n    }\n}\n"
  },
  {
    "path": "tests/Theme/LogicalThemeTest.php",
    "content": "<?php\n\nnamespace Tests\\Theme;\n\nuse BookStack\\Exceptions\\ThemeException;\nuse BookStack\\Facades\\Theme;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Support\\Facades\\Artisan;\nuse Tests\\TestCase;\n\nclass LogicalThemeTest extends TestCase\n{\n    public function test_theme_functions_file_used_and_app_boot_event_runs()\n    {\n        $this->usingThemeFolder(function ($themeFolder) {\n            $functionsFile = theme_path('functions.php');\n            app()->alias('cat', 'dog');\n            file_put_contents($functionsFile, \"<?php\\nTheme::listen(\\BookStack\\Theming\\ThemeEvents::APP_BOOT, function(\\$app) { \\$app->alias('cat', 'dog');});\");\n            $this->runWithEnv(['APP_THEME' => $themeFolder], function () {\n                $this->assertEquals('cat', $this->app->getAlias('dog'));\n            });\n        });\n    }\n\n    public function test_theme_functions_loads_errors_are_caught_and_logged()\n    {\n        $this->usingThemeFolder(function ($themeFolder) {\n            $functionsFile = theme_path('functions.php');\n            file_put_contents($functionsFile, \"<?php\\n\\\\BookStack\\\\Biscuits::eat();\");\n\n            $this->expectException(ThemeException::class);\n            $this->expectExceptionMessageMatches('/Failed loading theme functions file at \".*?\" with error: Class \"BookStack\\\\\\\\Biscuits\" not found/');\n\n            $this->runWithEnv(['APP_THEME' => $themeFolder], fn() => null);\n        });\n    }\n\n    public function test_add_social_driver()\n    {\n        Theme::addSocialDriver('catnet', [\n            'client_id'     => 'abc123',\n            'client_secret' => 'def456',\n        ], 'SocialiteProviders\\Discord\\DiscordExtendSocialite@handleTesting');\n\n        $this->assertEquals('catnet', config('services.catnet.name'));\n        $this->assertEquals('abc123', config('services.catnet.client_id'));\n        $this->assertEquals(url('/login/service/catnet/callback'), config('services.catnet.redirect'));\n\n        $loginResp = $this->get('/login');\n        $loginResp->assertSee('login/service/catnet');\n    }\n\n    public function test_add_social_driver_uses_name_in_config_if_given()\n    {\n        Theme::addSocialDriver('catnet', [\n            'client_id'     => 'abc123',\n            'client_secret' => 'def456',\n            'name'          => 'Super Cat Name',\n        ], 'SocialiteProviders\\Discord\\DiscordExtendSocialite@handleTesting');\n\n        $this->assertEquals('Super Cat Name', config('services.catnet.name'));\n        $loginResp = $this->get('/login');\n        $loginResp->assertSee('Super Cat Name');\n    }\n\n    public function test_add_social_driver_allows_a_configure_for_redirect_callback_to_be_passed()\n    {\n        Theme::addSocialDriver(\n            'discord',\n            [\n                'client_id'     => 'abc123',\n                'client_secret' => 'def456',\n                'name'          => 'Super Cat Name',\n            ],\n            'SocialiteProviders\\Discord\\DiscordExtendSocialite@handle',\n            function ($driver) {\n                $driver->with(['donkey' => 'donut']);\n            }\n        );\n\n        $loginResp = $this->get('/login/service/discord');\n        $redirect = $loginResp->headers->get('location');\n        $this->assertStringContainsString('donkey=donut', $redirect);\n    }\n\n    public function test_register_command_allows_provided_command_to_be_usable_via_artisan()\n    {\n        Theme::registerCommand(new MyCustomCommand());\n\n        Artisan::call('bookstack:test-custom-command', []);\n        $output = Artisan::output();\n\n        $this->assertStringContainsString('Command ran!', $output);\n    }\n}\n\nclass MyCustomCommand extends Command\n{\n    protected $signature = 'bookstack:test-custom-command';\n\n    public function handle()\n    {\n        $this->line('Command ran!');\n    }\n}\n"
  },
  {
    "path": "tests/Theme/ThemeModuleTest.php",
    "content": "<?php\n\nnamespace Tests\\Theme;\n\nuse BookStack\\Facades\\Theme;\nuse BookStack\\Util\\CspService;\nuse Tests\\TestCase;\n\nclass ThemeModuleTest extends TestCase\n{\n    public function test_modules_loaded_on_theme_load()\n    {\n        $this->usingThemeFolder(function ($themeFolder) {\n            $a = theme_path('modules/a');\n            $b = theme_path('modules/b');\n            mkdir($a, 0777, true);\n            mkdir($b, 0777, true);\n\n            file_put_contents($a . '/bookstack-module.json', json_encode([\n                'name' => 'Module A',\n                'description' => 'This is module A',\n                'version' => '1.0.0',\n            ]));\n            file_put_contents($b . '/bookstack-module.json', json_encode([\n                'name' => 'Module B',\n                'description' => 'This is module B',\n                'version' => 'v0.5.0',\n            ]));\n\n            $this->refreshApplication();\n\n            $modules = Theme::getModules();\n            $this->assertCount(2, $modules);\n\n            $moduleA = $modules['a'];\n            $this->assertEquals('Module A', $moduleA->name);\n            $this->assertEquals('This is module A', $moduleA->description);\n            $this->assertEquals('1.0.0', $moduleA->version);\n        });\n    }\n\n    public function test_module_not_loaded_if_no_bookstack_module_json()\n    {\n        $this->usingThemeFolder(function ($themeFolder) {\n            $moduleDir = theme_path('/modules/a');\n            mkdir($moduleDir, 0777, true);\n            file_put_contents($moduleDir . '/module.json', '{}');\n            $this->refreshApplication();\n            $modules = Theme::getModules();\n            $this->assertCount(0, $modules);\n        });\n    }\n\n    public function test_language_text_overridable_via_module()\n    {\n        $this->usingModuleFolder(function (string $moduleFolderPath) {\n            $translationPath = $moduleFolderPath . '/lang/en';\n            mkdir($translationPath, 0777, true);\n            file_put_contents($translationPath . '/entities.php', '<?php return [\"books\" => \"SuperBeans\"];');\n            $this->refreshApplication();\n\n            $this->asAdmin()->get('/books')->assertSee('SuperBeans');\n        });\n    }\n\n    public function test_language_files_merge_with_theme_files_with_theme_taking_precedence()\n    {\n        $this->usingModuleFolder(function (string $moduleFolderPath) {\n            $moduleTranslationPath = $moduleFolderPath . '/lang/en';\n            mkdir($moduleTranslationPath, 0777, true);\n            file_put_contents($moduleTranslationPath . '/entities.php', '<?php return [\"books\" => \"SuperBeans\", \"recently_viewed\" => \"ViewedBiscuits\"];');\n\n            $themeTranslationPath = theme_path('lang/en');\n            mkdir($themeTranslationPath, 0777, true);\n            file_put_contents($themeTranslationPath . '/entities.php', '<?php return [\"books\" => \"WonderBeans\"];');\n            $this->refreshApplication();\n\n            $this->asAdmin()->get('/books')\n                ->assertSee('WonderBeans')\n                ->assertDontSee('SuperBeans')\n                ->assertSee('ViewedBiscuits');\n        });\n    }\n\n    public function test_view_files_overridable_from_module()\n    {\n        $this->usingModuleFolder(function (string $moduleFolderPath) {\n            $viewsFolder = $moduleFolderPath . '/views/layouts/parts';\n            mkdir($viewsFolder, 0777, true);\n            file_put_contents($viewsFolder . '/header.blade.php', 'My custom header that says badgerriffic');\n            $this->refreshApplication();\n            $this->asAdmin()->get('/')->assertSee('badgerriffic');\n        });\n    }\n\n    public function test_theme_view_files_take_precedence_over_module_view_files()\n    {\n        $this->usingModuleFolder(function (string $moduleFolderPath) {\n            $viewsFolder = $moduleFolderPath . '/views/layouts/parts';\n            mkdir($viewsFolder, 0777, true);\n            file_put_contents($viewsFolder . '/header.blade.php', 'My custom header that says badgerriffic');\n\n            $themeViewsFolder = theme_path('layouts/parts');\n            mkdir($themeViewsFolder, 0777, true);\n            file_put_contents($themeViewsFolder . '/header.blade.php', 'My theme header that says awesomeferrets');\n\n            $this->refreshApplication();\n            $this->asAdmin()->get('/')\n                ->assertDontSee('badgerriffic')\n                ->assertSee('awesomeferrets');\n        });\n    }\n\n    public function test_theme_and_modules_views_can_be_used_at_the_same_time()\n    {\n        $this->usingModuleFolder(function (string $moduleFolderPath) {\n            $viewsFolder = $moduleFolderPath . '/views/layouts/parts';\n            mkdir($viewsFolder, 0777, true);\n            file_put_contents($viewsFolder . '/base-body-start.blade.php', 'My custom header that says badgerriffic');\n\n            $themeViewsFolder = theme_path('layouts/parts');\n            mkdir($themeViewsFolder, 0777, true);\n            file_put_contents($themeViewsFolder . '/base-body-end.blade.php', 'My theme header that says awesomeferrets');\n\n            $this->refreshApplication();\n            $this->asAdmin()->get('/')\n                ->assertSee('badgerriffic')\n                ->assertSee('awesomeferrets');\n        });\n    }\n\n    public function test_icons_can_be_overridden_from_module()\n    {\n        $this->usingModuleFolder(function (string $moduleFolderPath) {\n            $iconsFolder = $moduleFolderPath . '/icons';\n            mkdir($iconsFolder, 0777, true);\n            file_put_contents($iconsFolder . '/books.svg', '<svg><path d=\"supericonpath\"/></svg>');\n            $this->refreshApplication();\n\n            $this->asAdmin()->get('/')->assertSee('supericonpath', false);\n        });\n    }\n\n    public function test_theme_icons_take_precedence_over_module_icons()\n    {\n        $this->usingModuleFolder(function (string $moduleFolderPath) {\n            $iconsFolder = $moduleFolderPath . '/icons';\n            mkdir($iconsFolder, 0777, true);\n            file_put_contents($iconsFolder . '/books.svg', '<svg><path d=\"supericonpath\"/></svg>');\n            $this->refreshApplication();\n\n            $themeViewsFolder = theme_path('icons');\n            mkdir($themeViewsFolder, 0777, true);\n            file_put_contents($themeViewsFolder . '/books.svg', '<svg><path d=\"wackyiconpath\"/></svg>');\n\n\n            $this->asAdmin()->get('/')\n                ->assertSee('wackyiconpath', false)\n                ->assertDontSee('supericonpath', false);\n        });\n    }\n\n    public function test_public_folder_can_be_provided_from_module()\n    {\n        $this->usingModuleFolder(function (string $moduleFolderPath) {\n            $publicFolder = $moduleFolderPath . '/public';\n            mkdir($publicFolder, 0777, true);\n            $themeName = basename(dirname(dirname($moduleFolderPath)));\n            file_put_contents($publicFolder . '/test.txt', 'hellofrominsidethisfileimaghostwoooo!');\n            $this->refreshApplication();\n\n            $resp = $this->asAdmin()->get(\"/theme/{$themeName}/test.txt\")->streamedContent();\n            $this->assertEquals('hellofrominsidethisfileimaghostwoooo!', $resp);\n        });\n    }\n\n    public function test_theme_public_files_take_precedence_over_modules()\n    {\n        $this->usingModuleFolder(function (string $moduleFolderPath) {\n            $publicFolder = $moduleFolderPath . '/public';\n            mkdir($publicFolder, 0777, true);\n            $themeName = basename(theme_path());\n            file_put_contents($publicFolder . '/test.txt', 'hellofrominsidethisfileimaghostwoooo!');\n\n            $themePublicFolder = theme_path('public');\n            mkdir($themePublicFolder, 0777, true);\n            file_put_contents($themePublicFolder . '/test.txt', 'imadifferentghostinsidethetheme,woooooo!');\n\n            $this->refreshApplication();\n\n            $resp = $this->asAdmin()->get(\"/theme/{$themeName}/test.txt\")->streamedContent();\n            $this->assertEquals('imadifferentghostinsidethetheme,woooooo!', $resp);\n        });\n    }\n\n    public function test_logical_functions_file_loaded_from_module_and_it_runs_alongside_theme_functions()\n    {\n        $this->usingModuleFolder(function (string $moduleFolderPath) {\n            file_put_contents($moduleFolderPath . '/functions.php', \"<?php\\nTheme::listen(\\BookStack\\Theming\\ThemeEvents::APP_BOOT, function(\\$app) { \\$app->alias('cat', 'dog');});\");\n\n            $themeFunctionsFile = theme_path('functions.php');\n            file_put_contents($themeFunctionsFile, \"<?php\\nTheme::listen(\\BookStack\\Theming\\ThemeEvents::APP_BOOT, function(\\$app) { \\$app->alias('beans', 'cheese');});\");\n\n            $this->refreshApplication();\n\n            $this->assertEquals('cat', $this->app->getAlias('dog'));\n            $this->assertEquals('beans', $this->app->getAlias('cheese'));\n        });\n    }\n\n    public function test_module_can_use_theme_view_render_functions()\n    {\n        $this->usingModuleFolder(function (string $moduleFolderPath) {\n            file_put_contents($moduleFolderPath . '/functions.php', \"<?php\\n\\BookStack\\Facades\\Theme::listen(\\BookStack\\Theming\\ThemeEvents::THEME_REGISTER_VIEWS, fn(\\$views) => \\$views->renderBefore('layouts.parts.header', 'cat', 100));\");\n            mkdir($moduleFolderPath . '/views', 0777, true);\n            file_put_contents($moduleFolderPath . '/views/cat.blade.php', 'mysupercatispouncy');\n\n            $this->refreshApplication();\n\n            $this->asAdmin()->get('/')->assertSee('mysupercatispouncy');\n        });\n    }\n\n    public function test_module_can_provide_head_content()\n    {\n        $this->usingModuleFolder(function (string $moduleFolderPath) {\n            mkdir($moduleFolderPath . '/head', 0777, true);\n            file_put_contents($moduleFolderPath . '/head/hello.html', '<meta name=\"beans\" content=\"hello\"><script>hellofromcustomscript</script>');\n\n            $this->refreshApplication();\n\n            $cspService = $this->app->make(CspService::class);\n            $nonce = $cspService->getNonce();\n\n            $resp = $this->asAdmin()->get('/');\n            $resp->assertSee('<meta name=\"beans\" content=\"hello\">', false);\n            $resp->assertSee('<script nonce=\"' . $nonce . '\">hellofromcustomscript</script>', false);\n        });\n    }\n\n    protected function usingModuleFolder(callable $callback): void\n    {\n        $this->usingThemeFolder(function (string $themeFolder) use ($callback) {\n            $moduleFolderPath = theme_path('modules/test-module');\n            mkdir($moduleFolderPath, 0777, true);\n            file_put_contents($moduleFolderPath . '/bookstack-module.json', json_encode([\n                'name' => 'Test Module',\n                'description' => 'This is a test module',\n                'version' => 'v1.0.0',\n            ]));\n            $callback($moduleFolderPath);\n        });\n    }\n}\n"
  },
  {
    "path": "tests/Theme/VisualThemeTest.php",
    "content": "<?php\n\nnamespace Tests\\Theme;\n\nuse Illuminate\\Support\\Facades\\File;\nuse Tests\\TestCase;\n\nclass VisualThemeTest extends TestCase\n{\n    public function test_translation_text_can_be_overridden_via_theme()\n    {\n        $this->usingThemeFolder(function () {\n            $translationPath = theme_path('/lang/en');\n            File::makeDirectory($translationPath, 0777, true);\n\n            $customTranslations = '<?php\n            return [\\'books\\' => \\'Sandwiches\\'];\n        ';\n            file_put_contents($translationPath . '/entities.php', $customTranslations);\n\n            $homeRequest = $this->actingAs($this->users->viewer())->get('/');\n            $this->withHtml($homeRequest)->assertElementContains('header nav', 'Sandwiches');\n        });\n    }\n\n    public function test_custom_settings_category_page_can_be_added_via_view_file()\n    {\n        $content = 'My SuperCustomSettings';\n\n        $this->usingThemeFolder(function (string $folder) use ($content) {\n            $viewDir = theme_path('settings/categories');\n            mkdir($viewDir, 0777, true);\n            file_put_contents($viewDir . '/beans.blade.php', $content);\n\n            $this->asAdmin()->get('/settings/beans')->assertSee($content);\n        });\n    }\n\n    public function test_base_body_start_and_end_template_files_can_be_used()\n    {\n        $bodyStartStr = 'barry-fought-against-the-panther';\n        $bodyEndStr = 'barry-lost-his-fight-with-grace';\n\n        $this->usingThemeFolder(function (string $folder) use ($bodyStartStr, $bodyEndStr) {\n            $viewDir = theme_path('layouts/parts');\n            mkdir($viewDir, 0777, true);\n            file_put_contents($viewDir . '/base-body-start.blade.php', $bodyStartStr);\n            file_put_contents($viewDir . '/base-body-end.blade.php', $bodyEndStr);\n\n            $resp = $this->asEditor()->get('/');\n            $resp->assertSee($bodyStartStr);\n            $resp->assertSee($bodyEndStr);\n        });\n    }\n\n    public function test_export_body_start_and_end_template_files_can_be_used()\n    {\n        $bodyStartStr = 'garry-fought-against-the-panther';\n        $bodyEndStr = 'garry-lost-his-fight-with-grace';\n        $page = $this->entities->page();\n\n        $this->usingThemeFolder(function (string $folder) use ($bodyStartStr, $bodyEndStr, $page) {\n            $viewDir = theme_path('layouts/parts');\n            mkdir($viewDir, 0777, true);\n            file_put_contents($viewDir . '/export-body-start.blade.php', $bodyStartStr);\n            file_put_contents($viewDir . '/export-body-end.blade.php', $bodyEndStr);\n\n            $resp = $this->asEditor()->get($page->getUrl('/export/html'));\n            $resp->assertSee($bodyStartStr);\n            $resp->assertSee($bodyEndStr);\n        });\n    }\n\n    public function test_login_and_register_message_template_files_can_be_used()\n    {\n        $loginMessage = 'Welcome to this instance, login below you scallywag';\n        $registerMessage = 'You want to register? Enter the deets below you numpty';\n\n        $this->usingThemeFolder(function (string $folder) use ($loginMessage, $registerMessage) {\n            $viewDir = theme_path('auth/parts');\n            mkdir($viewDir, 0777, true);\n            file_put_contents($viewDir . '/login-message.blade.php', $loginMessage);\n            file_put_contents($viewDir . '/register-message.blade.php', $registerMessage);\n            $this->setSettings(['registration-enabled' => 'true']);\n\n            $this->get('/login')->assertSee($loginMessage);\n            $this->get('/register')->assertSee($registerMessage);\n        });\n    }\n\n    public function test_header_links_start_template_file_can_be_used()\n    {\n        $content = 'This is added text in the header bar';\n\n        $this->usingThemeFolder(function (string $folder) use ($content) {\n            $viewDir = theme_path('layouts/parts');\n            mkdir($viewDir, 0777, true);\n            file_put_contents($viewDir . '/header-links-start.blade.php', $content);\n            $this->setSettings(['registration-enabled' => 'true']);\n\n            $this->get('/login')->assertSee($content);\n        });\n    }\n\n    public function test_public_folder_contents_accessible_via_route()\n    {\n        $this->usingThemeFolder(function (string $themeFolderName) {\n            $publicDir = theme_path('public');\n            mkdir($publicDir, 0777, true);\n\n            $text = 'some-text ' . md5(random_bytes(5));\n            $css = \"body { background-color: tomato !important; }\";\n            file_put_contents(\"{$publicDir}/file.txt\", $text);\n            file_put_contents(\"{$publicDir}/file.css\", $css);\n            copy($this->files->testFilePath('test-image.png'), \"{$publicDir}/image.png\");\n\n            $resp = $this->asAdmin()->get(\"/theme/{$themeFolderName}/file.txt\");\n            $resp->assertStreamedContent($text);\n            $resp->assertHeader('Content-Type', 'text/plain; charset=utf-8');\n            $resp->assertHeader('Cache-Control', 'max-age=86400, private');\n\n            $resp = $this->asAdmin()->get(\"/theme/{$themeFolderName}/image.png\");\n            $resp->assertHeader('Content-Type', 'image/png');\n            $resp->assertHeader('Cache-Control', 'max-age=86400, private');\n\n            $resp = $this->asAdmin()->get(\"/theme/{$themeFolderName}/file.css\");\n            $resp->assertStreamedContent($css);\n            $resp->assertHeader('Content-Type', 'text/css; charset=utf-8');\n            $resp->assertHeader('Cache-Control', 'max-age=86400, private');\n        });\n    }\n}\n"
  },
  {
    "path": "tests/Unit/ConfigTest.php",
    "content": "<?php\n\nnamespace Tests\\Unit;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Mail;\nuse Symfony\\Component\\Mailer\\Transport\\Smtp\\EsmtpTransport;\nuse Tests\\TestCase;\n\n/**\n * Class ConfigTest\n * Many of the tests here are to check on tweaks made\n * to maintain backwards compatibility.\n */\nclass ConfigTest extends TestCase\n{\n    public function test_filesystem_images_falls_back_to_storage_type_var()\n    {\n        $this->runWithEnv(['STORAGE_TYPE' => 'local_secure'], function () {\n            $this->checkEnvConfigResult('STORAGE_IMAGE_TYPE', 's3', 'filesystems.images', 's3');\n            $this->checkEnvConfigResult('STORAGE_IMAGE_TYPE', null, 'filesystems.images', 'local_secure');\n        });\n    }\n\n    public function test_filesystem_attachments_falls_back_to_storage_type_var()\n    {\n        $this->runWithEnv(['STORAGE_TYPE' => 'local_secure'], function () {\n            $this->checkEnvConfigResult('STORAGE_ATTACHMENT_TYPE', 's3', 'filesystems.attachments', 's3');\n            $this->checkEnvConfigResult('STORAGE_ATTACHMENT_TYPE', null, 'filesystems.attachments', 'local_secure');\n        });\n    }\n\n    public function test_app_url_blank_if_old_default_value()\n    {\n        $initUrl = 'https://example.com/docs';\n        $oldDefault = 'http://bookstack.dev';\n        $this->checkEnvConfigResult('APP_URL', $initUrl, 'app.url', $initUrl);\n        $this->checkEnvConfigResult('APP_URL', $oldDefault, 'app.url', '');\n    }\n\n    public function test_errorlog_plain_webserver_channel()\n    {\n        // We can't full test this due to it being targeted for the SAPI logging handler\n        // so we just overwrite that component so we can capture the error log output.\n        config()->set([\n            'logging.channels.errorlog_plain_webserver.handler_with' => [0],\n        ]);\n\n        $temp = tempnam(sys_get_temp_dir(), 'bs-test');\n        $original = ini_set('error_log', $temp);\n\n        Log::channel('errorlog_plain_webserver')->info('Aww, look, a cute puppy');\n\n        ini_set('error_log', $original);\n\n        $output = file_get_contents($temp);\n        $this->assertStringContainsString('Aww, look, a cute puppy', $output);\n        $this->assertStringNotContainsString('INFO', $output);\n        $this->assertStringNotContainsString('info', $output);\n        $this->assertStringNotContainsString('testing', $output);\n    }\n\n    public function test_session_cookie_uses_sub_path_from_app_url()\n    {\n        $this->checkEnvConfigResult('APP_URL', 'https://example.com', 'session.path', '/');\n        $this->checkEnvConfigResult('APP_URL', 'https://a.com/b', 'session.path', '/b');\n        $this->checkEnvConfigResult('APP_URL', 'https://a.com/b/d/e', 'session.path', '/b/d/e');\n        $this->checkEnvConfigResult('APP_URL', '', 'session.path', '/');\n    }\n\n    public function test_saml2_idp_authn_context_string_parsed_as_space_separated_array()\n    {\n        $this->checkEnvConfigResult(\n            'SAML2_IDP_AUTHNCONTEXT',\n            'urn:federation:authentication:windows urn:federation:authentication:linux',\n            'saml2.onelogin.security.requestedAuthnContext',\n            ['urn:federation:authentication:windows', 'urn:federation:authentication:linux']\n        );\n    }\n\n    public function test_dompdf_remote_fetching_controlled_by_allow_untrusted_server_fetching_false()\n    {\n        $this->checkEnvConfigResult('ALLOW_UNTRUSTED_SERVER_FETCHING', 'false', 'exports.dompdf.enable_remote', false);\n        $this->checkEnvConfigResult('ALLOW_UNTRUSTED_SERVER_FETCHING', 'true', 'exports.dompdf.enable_remote', true);\n    }\n\n    public function test_dompdf_paper_size_options_are_limited()\n    {\n        $this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'cat', 'exports.dompdf.default_paper_size', 'a4');\n        $this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'letter', 'exports.dompdf.default_paper_size', 'letter');\n        $this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'a4', 'exports.dompdf.default_paper_size', 'a4');\n    }\n\n    public function test_snappy_paper_size_options_are_limited()\n    {\n        $this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'cat', 'exports.snappy.options.page-size', 'A4');\n        $this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'letter', 'exports.snappy.options.page-size', 'Letter');\n        $this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'a4', 'exports.snappy.options.page-size', 'A4');\n    }\n\n    public function test_sendmail_command_is_configurable()\n    {\n        $this->checkEnvConfigResult('MAIL_SENDMAIL_COMMAND', '/var/sendmail -o', 'mail.mailers.sendmail.path', '/var/sendmail -o');\n    }\n\n    public function test_mail_disable_ssl_verification_alters_mailer()\n    {\n        $getStreamOptions = function (): array {\n            /** @var EsmtpTransport $transport */\n            $transport = Mail::mailer('smtp')->getSymfonyTransport();\n            return $transport->getStream()->getStreamOptions();\n        };\n\n        $this->assertEmpty($getStreamOptions());\n\n\n        $this->runWithEnv(['MAIL_VERIFY_SSL' => 'false'], function () use ($getStreamOptions) {\n            $options = $getStreamOptions();\n            $this->assertArrayHasKey('ssl', $options);\n            $this->assertFalse($options['ssl']['verify_peer']);\n            $this->assertFalse($options['ssl']['verify_peer_name']);\n        });\n    }\n\n    public function test_app_url_changes_smtp_ehlo_host_on_mailer()\n    {\n        $getLocalDomain = function (): string {\n            /** @var EsmtpTransport $transport */\n            $transport = Mail::mailer('smtp')->getSymfonyTransport();\n            return $transport->getLocalDomain();\n        };\n\n        $this->runWithEnv(['APP_URL' => ''], function () use ($getLocalDomain) {\n            $this->assertEquals('[127.0.0.1]', $getLocalDomain());\n        });\n\n        $this->runWithEnv(['APP_URL' => 'https://example.com/cats/dogs'], function () use ($getLocalDomain) {\n            $this->assertEquals('example.com', $getLocalDomain());\n        });\n\n        $this->runWithEnv(['APP_URL' => 'http://beans.cat.example.com'], function () use ($getLocalDomain) {\n            $this->assertEquals('beans.cat.example.com', $getLocalDomain());\n        });\n    }\n\n    public function test_non_null_mail_encryption_options_enforce_smtp_scheme()\n    {\n        $this->checkEnvConfigResult('MAIL_ENCRYPTION', 'tls', 'mail.mailers.smtp.require_tls', true);\n        $this->checkEnvConfigResult('MAIL_ENCRYPTION', 'ssl', 'mail.mailers.smtp.require_tls', true);\n        $this->checkEnvConfigResult('MAIL_ENCRYPTION', 'null', 'mail.mailers.smtp.require_tls', false);\n    }\n\n    public function test_smtp_scheme_and_certain_port_forces_tls_usage()\n    {\n        $isMailTlsRequired = function () {\n            /** @var EsmtpTransport $transport */\n            $transport = Mail::mailer('smtp')->getSymfonyTransport();\n            Mail::purge('smtp');\n            return $transport->isTlsRequired();\n        };\n\n        $runTest = function (string $tlsOption, int $port, bool $expectedResult) use ($isMailTlsRequired) {\n            $this->runWithEnv(['MAIL_ENCRYPTION' => $tlsOption, 'MAIL_PORT' => $port], function () use ($isMailTlsRequired, $port, $expectedResult) {\n                $this->assertEquals($expectedResult, $isMailTlsRequired());\n            });\n        };\n\n        $runTest('null', 587, false);\n        $runTest('tls', 587, true);\n        $runTest('null', 465, true);\n    }\n\n    public function test_mysql_host_parsed_as_expected()\n    {\n        $cases = [\n            '127.0.0.1' => ['127.0.0.1', 3306],\n            '127.0.0.1:3307' => ['127.0.0.1', 3307],\n            'a.example.com' => ['a.example.com', 3306],\n            'a.example.com:3307' => ['a.example.com', 3307],\n            '[::1]' => ['[::1]', 3306],\n            '[::1]:123' => ['[::1]', 123],\n            '[2001:db8:3c4d:0015:0000:0000:1a2f]' => ['[2001:db8:3c4d:0015:0000:0000:1a2f]', 3306],\n            '[2001:db8:3c4d:0015:0000:0000:1a2f]:4567' => ['[2001:db8:3c4d:0015:0000:0000:1a2f]', 4567],\n        ];\n\n        foreach ($cases as $host => [$expectedHost, $expectedPort]) {\n            $this->runWithEnv([\"DB_HOST\" => $host], function () use ($expectedHost, $expectedPort) {\n                $this->assertEquals($expectedHost, config(\"database.connections.mysql.host\"));\n                $this->assertEquals($expectedPort, config(\"database.connections.mysql.port\"));\n            }, false);\n        }\n    }\n\n    public function test_content_filtering_defaults_to_enabled()\n    {\n        $this->runWithEnv(['APP_CONTENT_FILTERING' => null, 'ALLOW_CONTENT_SCRIPTS' => null], function () {\n            $this->assertEquals('jhfa', config('app.content_filtering'));\n        });\n    }\n\n    public function test_content_filtering_can_be_disabled()\n    {\n        $this->runWithEnv(['APP_CONTENT_FILTERING' => \"\", 'ALLOW_CONTENT_SCRIPTS' => null], function () {\n            $this->assertEquals('', config('app.content_filtering'));\n        });\n    }\n\n    public function test_allow_content_scripts_disables_content_filtering()\n    {\n        $this->runWithEnv(['APP_CONTENT_FILTERING' => null, 'ALLOW_CONTENT_SCRIPTS' => 'true'], function () {\n            $this->assertEquals('', config('app.content_filtering'));\n        });\n    }\n\n    /**\n     * Set an environment variable of the given name and value\n     * then check the given config key to see if it matches the given result.\n     * Providing a null $envVal clears the variable.\n     */\n    protected function checkEnvConfigResult(string $envName, ?string $envVal, string $configKey, mixed $expectedResult): void\n    {\n        $this->runWithEnv([$envName => $envVal], function () use ($configKey, $expectedResult) {\n            $this->assertEquals($expectedResult, config($configKey));\n        });\n    }\n}\n"
  },
  {
    "path": "tests/Unit/FrameworkAssumptionTest.php",
    "content": "<?php\n\nnamespace Tests\\Unit;\n\nuse BadMethodCallException;\nuse BookStack\\Entities\\Models\\Page;\nuse Tests\\TestCase;\n\n/**\n * This class tests assumptions we're relying upon in the framework.\n * This is primarily to keep track of certain bits of functionality that\n * may be used in important areas such as to enforce permissions.\n */\nclass FrameworkAssumptionTest extends TestCase\n{\n    public function test_scopes_error_if_not_existing()\n    {\n        $this->expectException(BadMethodCallException::class);\n        $this->expectExceptionMessage('Call to undefined method BookStack\\Entities\\Models\\Page::scopeNotfoundscope()');\n        Page::query()->scopes('notfoundscope');\n    }\n\n    public function test_scopes_applies_upon_existing()\n    {\n        // Page has SoftDeletes trait by default, so we apply our custom scope and ensure\n        // it stacks on the global scope to filter out deleted items.\n        $query = Page::query()->scopes('visible')->toSql();\n        $this->assertStringContainsString('joint_permissions', $query);\n        $this->assertStringContainsString('`deleted_at` is null', $query);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/IpFormatterTest.php",
    "content": "<?php\n\nnamespace Tests\\Unit;\n\nuse BookStack\\Activity\\Tools\\IpFormatter;\nuse Tests\\TestCase;\n\nclass IpFormatterTest extends TestCase\n{\n    public function test_ips_formatted_as_expected()\n    {\n        $this->assertEquals('192.123.45.5', (new IpFormatter('192.123.45.5', 4))->format());\n        $this->assertEquals('192.123.45.x', (new IpFormatter('192.123.45.5', 3))->format());\n        $this->assertEquals('192.123.x.x', (new IpFormatter('192.123.45.5', 2))->format());\n        $this->assertEquals('192.x.x.x', (new IpFormatter('192.123.45.5', 1))->format());\n        $this->assertEquals('x.x.x.x', (new IpFormatter('192.123.45.5', 0))->format());\n\n        $ipv6 = '2001:db8:85a3:8d3:1319:8a2e:370:7348';\n        $this->assertEquals($ipv6, (new IpFormatter($ipv6, 4))->format());\n        $this->assertEquals('2001:db8:85a3:8d3:1319:8a2e:x:x', (new IpFormatter($ipv6, 3))->format());\n        $this->assertEquals('2001:db8:85a3:8d3:x:x:x:x', (new IpFormatter($ipv6, 2))->format());\n        $this->assertEquals('2001:db8:x:x:x:x:x:x', (new IpFormatter($ipv6, 1))->format());\n        $this->assertEquals('x:x:x:x:x:x:x:x', (new IpFormatter($ipv6, 0))->format());\n    }\n\n    public function test_shortened_ipv6_addresses_expands_as_expected()\n    {\n        $this->assertEquals('2001:0:0:0:0:0:x:x', (new IpFormatter('2001::370:7348', 3))->format());\n        $this->assertEquals('2001:0:0:0:0:85a3:x:x', (new IpFormatter('2001::85a3:370:7348', 3))->format());\n        $this->assertEquals('2001:0:x:x:x:x:x:x', (new IpFormatter('2001::', 1))->format());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/OidcIdTokenTest.php",
    "content": "<?php\n\nnamespace Tests\\Unit;\n\nuse BookStack\\Access\\Oidc\\OidcIdToken;\nuse BookStack\\Access\\Oidc\\OidcInvalidTokenException;\nuse Tests\\Helpers\\OidcJwtHelper;\nuse Tests\\TestCase;\n\nclass OidcIdTokenTest extends TestCase\n{\n    public function test_valid_token_passes_validation()\n    {\n        $token = new OidcIdToken(OidcJwtHelper::idToken(), OidcJwtHelper::defaultIssuer(), [\n            OidcJwtHelper::publicJwkKeyArray(),\n        ]);\n\n        $this->assertTrue($token->validate('xxyyzz.aaa.bbccdd.123'));\n    }\n\n    public function test_get_claim_returns_value_if_existing()\n    {\n        $token = new OidcIdToken(OidcJwtHelper::idToken(), OidcJwtHelper::defaultIssuer(), []);\n        $this->assertEquals('bscott@example.com', $token->getClaim('email'));\n    }\n\n    public function test_get_claim_returns_null_if_not_existing()\n    {\n        $token = new OidcIdToken(OidcJwtHelper::idToken(), OidcJwtHelper::defaultIssuer(), []);\n        $this->assertEquals(null, $token->getClaim('emails'));\n    }\n\n    public function test_get_all_claims_returns_all_payload_claims()\n    {\n        $defaultPayload = OidcJwtHelper::defaultPayload();\n        $token = new OidcIdToken(OidcJwtHelper::idToken($defaultPayload), OidcJwtHelper::defaultIssuer(), []);\n        $this->assertEquals($defaultPayload, $token->getAllClaims());\n    }\n\n    public function test_token_structure_error_cases()\n    {\n        $idToken = OidcJwtHelper::idToken();\n        $idTokenExploded = explode('.', $idToken);\n\n        $messagesAndTokenValues = [\n            ['Could not parse out a valid header within the provided token', ''],\n            ['Could not parse out a valid header within the provided token', 'cat'],\n            ['Could not parse out a valid payload within the provided token', $idTokenExploded[0]],\n            ['Could not parse out a valid payload within the provided token', $idTokenExploded[0] . '.' . 'dog'],\n            ['Could not parse out a valid signature within the provided token', $idTokenExploded[0] . '.' . $idTokenExploded[1]],\n            ['Could not parse out a valid signature within the provided token', $idTokenExploded[0] . '.' . $idTokenExploded[1] . '.' . '@$%'],\n        ];\n\n        foreach ($messagesAndTokenValues as [$message, $tokenValue]) {\n            $token = new OidcIdToken($tokenValue, OidcJwtHelper::defaultIssuer(), []);\n            $err = null;\n\n            try {\n                $token->validate('abc');\n            } catch (\\Exception $exception) {\n                $err = $exception;\n            }\n\n            $this->assertInstanceOf(OidcInvalidTokenException::class, $err, $message);\n            $this->assertEquals($message, $err->getMessage());\n        }\n    }\n\n    public function test_error_thrown_if_token_signature_not_validated_from_no_keys()\n    {\n        $token = new OidcIdToken(OidcJwtHelper::idToken(), OidcJwtHelper::defaultIssuer(), []);\n        $this->expectException(OidcInvalidTokenException::class);\n        $this->expectExceptionMessage('Token signature could not be validated using the provided keys');\n        $token->validate('abc');\n    }\n\n    public function test_error_thrown_if_token_signature_not_validated_from_non_matching_key()\n    {\n        $token = new OidcIdToken(OidcJwtHelper::idToken(), OidcJwtHelper::defaultIssuer(), [\n            array_merge(OidcJwtHelper::publicJwkKeyArray(), [\n                'n' => 'iqK-1QkICMf_cusNLpeNnN-bhT0-9WLBvzgwKLALRbrevhdi5ttrLHIQshaSL0DklzfyG2HWRmAnJ9Q7sweEjuRiiqRcSUZbYu8cIv2hLWYu7K_NH67D2WUjl0EnoHEuiVLsZhQe1CmdyLdx087j5nWkd64K49kXRSdxFQUlj8W3NeK3CjMEUdRQ3H4RZzJ4b7uuMiFA29S2ZhMNG20NPbkUVsFL-jiwTd10KSsPT8yBYipI9O7mWsUWt_8KZs1y_vpM_k3SyYihnWpssdzDm1uOZ8U3mzFr1xsLAO718GNUSXk6npSDzLl59HEqa6zs4O9awO2qnSHvcmyELNk31w',\n            ]),\n        ]);\n        $this->expectException(OidcInvalidTokenException::class);\n        $this->expectExceptionMessage('Token signature could not be validated using the provided keys');\n        $token->validate('abc');\n    }\n\n    public function test_error_thrown_if_invalid_key_provided()\n    {\n        $token = new OidcIdToken(OidcJwtHelper::idToken(), OidcJwtHelper::defaultIssuer(), ['url://example.com']);\n        $this->expectException(OidcInvalidTokenException::class);\n        $this->expectExceptionMessage('Unexpected type of key value provided');\n        $token->validate('abc');\n    }\n\n    public function test_error_thrown_if_token_algorithm_is_not_rs256()\n    {\n        $token = new OidcIdToken(OidcJwtHelper::idToken([], ['alg' => 'HS256']), OidcJwtHelper::defaultIssuer(), []);\n        $this->expectException(OidcInvalidTokenException::class);\n        $this->expectExceptionMessage('Only RS256 signature validation is supported. Token reports using HS256');\n        $token->validate('abc');\n    }\n\n    public function test_token_claim_error_cases()\n    {\n        /** @var array<array{0: string: 1: array}> $claimOverridesByErrorMessage */\n        $claimOverridesByErrorMessage = [\n            // 1. iss claim present\n            ['Missing or non-matching token issuer value', ['iss' => null]],\n            // 1. iss claim matches provided issuer\n            ['Missing or non-matching token issuer value', ['iss' => 'https://auth.example.co.uk']],\n            // 2. aud claim present\n            ['Missing token audience value', ['aud' => null]],\n            // 2. aud claim validates all values against those expected (Only expect single)\n            ['Token audience value has 2 values, Expected 1', ['aud' => ['xxyyzz.aaa.bbccdd.123', 'def']]],\n            // 2. aud claim matches client id\n            ['Token audience value did not match the expected client_id', ['aud' => 'xxyyzz.aaa.bbccdd.456']],\n            // 4. azp claim matches client id if present\n            ['Token authorized party exists but does not match the expected client_id', ['azp' => 'xxyyzz.aaa.bbccdd.456']],\n            // 5. exp claim present\n            ['Missing token expiration time value', ['exp' => null]],\n            // 5. exp claim not expired\n            ['Token has expired', ['exp' => time() - 360]],\n            // 6. iat claim present\n            ['Missing token issued at time value', ['iat' => null]],\n            // 6. iat claim too far in the future\n            ['Token issue at time is not recent or is invalid', ['iat' => time() + 600]],\n            // 6. iat claim too far in the past\n            ['Token issue at time is not recent or is invalid', ['iat' => time() - 172800]],\n\n            // Custom: sub is present\n            ['Missing token subject value', ['sub' => null]],\n        ];\n\n        foreach ($claimOverridesByErrorMessage as [$message, $overrides]) {\n            $token = new OidcIdToken(OidcJwtHelper::idToken($overrides), OidcJwtHelper::defaultIssuer(), [\n                OidcJwtHelper::publicJwkKeyArray(),\n            ]);\n\n            $err = null;\n\n            try {\n                $token->validate('xxyyzz.aaa.bbccdd.123');\n            } catch (\\Exception $exception) {\n                $err = $exception;\n            }\n\n            $this->assertInstanceOf(OidcInvalidTokenException::class, $err, $message);\n            $this->assertEquals($message, $err->getMessage());\n        }\n    }\n\n    public function test_keys_can_be_a_local_file_reference_to_pem_key()\n    {\n        $file = tmpfile();\n        $testFilePath = 'file://' . stream_get_meta_data($file)['uri'];\n        file_put_contents($testFilePath, OidcJwtHelper::publicPemKey());\n        $token = new OidcIdToken(OidcJwtHelper::idToken(), OidcJwtHelper::defaultIssuer(), [\n            $testFilePath,\n        ]);\n\n        $this->assertTrue($token->validate('xxyyzz.aaa.bbccdd.123'));\n        unlink($testFilePath);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/PageIncludeParserTest.php",
    "content": "<?php\n\nnamespace Tests\\Unit;\n\nuse BookStack\\Entities\\Tools\\PageIncludeContent;\nuse BookStack\\Entities\\Tools\\PageIncludeParser;\nuse BookStack\\Entities\\Tools\\PageIncludeTag;\nuse BookStack\\Util\\HtmlDocument;\nuse Tests\\TestCase;\n\nclass PageIncludeParserTest extends TestCase\n{\n    public function test_simple_inline_text()\n    {\n        $this->runParserTest(\n            '<p>{{@45#content}}</p>',\n            ['45' => '<p id=\"content\">Testing</p>'],\n            '<p>Testing</p>',\n        );\n    }\n\n    public function test_simple_inline_text_with_existing_siblings()\n    {\n        $this->runParserTest(\n            '<p>{{@45#content}} <strong>Hi</strong>there!</p>',\n            ['45' => '<p id=\"content\">Testing</p>'],\n            '<p>Testing <strong>Hi</strong>there!</p>',\n        );\n    }\n\n    public function test_simple_inline_text_within_other_text()\n    {\n        $this->runParserTest(\n            '<p>Hello {{@45#content}}there!</p>',\n            ['45' => '<p id=\"content\">Testing</p>'],\n            '<p>Hello Testingthere!</p>',\n        );\n    }\n\n    public function test_complex_inline_text_within_other_text()\n    {\n        $this->runParserTest(\n            '<p>Hello {{@45#content}}there!</p>',\n            ['45' => '<p id=\"content\"><strong>Testing</strong> with<em>some</em><i>extra</i>tags</p>'],\n            '<p>Hello <strong>Testing</strong> with<em>some</em><i>extra</i>tagsthere!</p>',\n        );\n    }\n\n    public function test_block_content_types()\n    {\n        $inputs = [\n            '<table id=\"content\"><td>Text</td></table>',\n            '<ul id=\"content\"><li>Item A</li></ul>',\n            '<ol id=\"content\"><li>Item A</li></ol>',\n            '<pre id=\"content\">Code</pre>',\n        ];\n\n        foreach ($inputs as $input) {\n            $this->runParserTest(\n                '<p>A{{@45#content}}B</p>',\n                ['45' => $input],\n                '<p>A</p>' . $input . '<p>B</p>',\n            );\n        }\n    }\n\n    public function test_block_content_nested_origin_gets_placed_before()\n    {\n        $this->runParserTest(\n            '<p><strong>A {{@45#content}} there!</strong></p>',\n            ['45' => '<pre id=\"content\">Testing</pre>'],\n            '<pre id=\"content\">Testing</pre><p><strong>A  there!</strong></p>',\n        );\n    }\n\n    public function test_block_content_nested_origin_gets_placed_after()\n    {\n        $this->runParserTest(\n            '<p><strong>Some really good {{@45#content}} there!</strong></p>',\n            ['45' => '<pre id=\"content\">Testing</pre>'],\n            '<p><strong>Some really good  there!</strong></p><pre id=\"content\">Testing</pre>',\n        );\n    }\n\n    public function test_block_content_in_shallow_origin_gets_split()\n    {\n        $this->runParserTest(\n            '<p>Some really good {{@45#content}} there!</p>',\n            ['45' => '<pre id=\"content\">doggos</pre>'],\n            '<p>Some really good </p><pre id=\"content\">doggos</pre><p> there!</p>',\n        );\n    }\n\n    public function test_block_content_in_shallow_origin_split_does_not_duplicate_id()\n    {\n        $this->runParserTest(\n            '<p id=\"test\" title=\"Hi\">Some really good {{@45#content}} there!</p>',\n            ['45' => '<pre id=\"content\">doggos</pre>'],\n            '<p title=\"Hi\">Some really good </p><pre id=\"content\">doggos</pre><p id=\"test\" title=\"Hi\"> there!</p>',\n        );\n    }\n\n    public function test_block_content_in_shallow_origin_does_not_leave_empty_nodes()\n    {\n        $this->runParserTest(\n            '<p>{{@45#content}}</p>',\n            ['45' => '<pre id=\"content\">doggos</pre>'],\n            '<pre id=\"content\">doggos</pre>',\n        );\n    }\n\n    public function test_block_content_in_allowable_parent_element()\n    {\n        $this->runParserTest(\n            '<div>{{@45#content}}</div>',\n            ['45' => '<pre id=\"content\">doggos</pre>'],\n            '<div><pre id=\"content\">doggos</pre></div>',\n        );\n    }\n\n    public function test_block_content_in_paragraph_origin_with_allowable_grandparent()\n    {\n        $this->runParserTest(\n            '<div><p>{{@45#content}}</p></div>',\n            ['45' => '<pre id=\"content\">doggos</pre>'],\n            '<div><pre id=\"content\">doggos</pre></div>',\n        );\n    }\n\n    public function test_block_content_in_paragraph_origin_with_allowable_grandparent_with_adjacent_content()\n    {\n        $this->runParserTest(\n            '<div><p>Cute {{@45#content}} over there!</p></div>',\n            ['45' => '<pre id=\"content\">doggos</pre>'],\n            '<div><p>Cute </p><pre id=\"content\">doggos</pre><p> over there!</p></div>',\n        );\n    }\n\n    public function test_block_content_in_child_within_paragraph_origin_with_allowable_grandparent_with_adjacent_content()\n    {\n        $this->runParserTest(\n            '<div><p><strong>Cute {{@45#content}} over there!</strong></p></div>',\n            ['45' => '<pre id=\"content\">doggos</pre>'],\n            '<div><pre id=\"content\">doggos</pre><p><strong>Cute  over there!</strong></p></div>',\n        );\n    }\n\n    public function test_block_content_in_paragraph_origin_within_details()\n    {\n        $this->runParserTest(\n            '<details><p>{{@45#content}}</p></details>',\n            ['45' => '<pre id=\"content\">doggos</pre>'],\n            '<details><pre id=\"content\">doggos</pre></details>',\n        );\n    }\n\n    public function test_simple_whole_document()\n    {\n        $this->runParserTest(\n            '<p>{{@45}}</p>',\n            ['45' => '<p id=\"content\">Testing</p>'],\n            '<p id=\"content\">Testing</p>',\n        );\n    }\n\n    public function test_multi_source_elem_whole_document()\n    {\n        $this->runParserTest(\n            '<p>{{@45}}</p>',\n            ['45' => '<p>Testing</p><blockquote>This</blockquote>'],\n            '<p>Testing</p><blockquote>This</blockquote>',\n        );\n    }\n\n    public function test_multi_source_elem_whole_document_with_shared_content_origin()\n    {\n        $this->runParserTest(\n            '<p>This is {{@45}} some text</p>',\n            ['45' => '<p>Testing</p><blockquote>This</blockquote>'],\n            '<p>This is </p><p>Testing</p><blockquote>This</blockquote><p> some text</p>',\n        );\n    }\n\n    public function test_multi_source_elem_whole_document_with_nested_content_origin()\n    {\n        $this->runParserTest(\n            '<p><strong>{{@45}}</strong></p>',\n            ['45' => '<p>Testing</p><blockquote>This</blockquote>'],\n            '<p>Testing</p><blockquote>This</blockquote>',\n        );\n    }\n\n    public function test_multiple_tags_in_same_origin_with_inline_content()\n    {\n        $this->runParserTest(\n            '<p>This {{@45#content}}{{@45#content}} content is {{@45#content}}</p>',\n            ['45' => '<p id=\"content\">inline</p>'],\n            '<p>This inlineinline content is inline</p>',\n        );\n    }\n\n    public function test_multiple_tags_in_same_origin_with_block_content()\n    {\n        $this->runParserTest(\n            '<p>This {{@45#content}}{{@45#content}} content is {{@45#content}}</p>',\n            ['45' => '<pre id=\"content\">block</pre>'],\n            '<p>This </p><pre id=\"content\">block</pre><pre id=\"content\">block</pre><p> content is </p><pre id=\"content\">block</pre>',\n        );\n    }\n\n    public function test_multiple_tags_in_differing_origin_levels_with_block_content()\n    {\n        $this->runParserTest(\n            '<div><p>This <strong>{{@45#content}}</strong> content is {{@45#content}}</p>{{@45#content}}</div>',\n            ['45' => '<pre id=\"content\">block</pre>'],\n            '<div><pre id=\"content\">block</pre><p>This  content is </p><pre id=\"content\">block</pre><pre id=\"content\">block</pre></div>',\n        );\n    }\n\n    public function test_multiple_tags_in_shallow_origin_with_multi_block_content()\n    {\n        $this->runParserTest(\n            '<p>{{@45}}C{{@45}}</p><div>{{@45}}{{@45}}</div>',\n            ['45' => '<p>A</p><p>B</p>'],\n            '<p>A</p><p>B</p><p>C</p><p>A</p><p>B</p><div><p>A</p><p>B</p><p>A</p><p>B</p></div>',\n        );\n    }\n\n    protected function runParserTest(string $html, array $contentById, string $expected): void\n    {\n        $doc = new HtmlDocument($html);\n        $parser = new PageIncludeParser($doc, function (PageIncludeTag $tag) use ($contentById): PageIncludeContent {\n            $html = $contentById[strval($tag->getPageId())] ?? '';\n            return PageIncludeContent::fromHtmlAndTag($html, $tag);\n        });\n\n        $parser->parse();\n        $this->assertEquals($expected, $doc->getBodyInnerHtml());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/SsrUrlValidatorTest.php",
    "content": "<?php\n\nnamespace Tests\\Unit;\n\nuse BookStack\\Exceptions\\HttpFetchException;\nuse BookStack\\Util\\SsrUrlValidator;\nuse Tests\\TestCase;\n\nclass SsrUrlValidatorTest extends TestCase\n{\n    public function test_allowed()\n    {\n        $testMap = [\n            // Single values\n            ['config' => '', 'url' => '', 'result' => false],\n            ['config' => '', 'url' => 'https://example.com', 'result' => false],\n            ['config' => '    ', 'url' => 'https://example.com', 'result' => false],\n            ['config' => '*', 'url' => '', 'result' => false],\n            ['config' => '*', 'url' => 'https://example.com', 'result' => true],\n            ['config' => 'https://*', 'url' => 'https://example.com', 'result' => true],\n            ['config' => 'http://*', 'url' => 'https://example.com', 'result' => false],\n            ['config' => 'https://*example.com', 'url' => 'https://example.com', 'result' => true],\n            ['config' => 'https://*ample.com', 'url' => 'https://example.com', 'result' => true],\n            ['config' => 'https://*.example.com', 'url' => 'https://example.com', 'result' => false],\n            ['config' => 'https://*.example.com', 'url' => 'https://test.example.com', 'result' => true],\n            ['config' => '*//example.com', 'url' => 'https://example.com', 'result' => true],\n            ['config' => '*//example.com', 'url' => 'http://example.com', 'result' => true],\n            ['config' => '*//example.co', 'url' => 'http://example.co.uk', 'result' => false],\n            ['config' => '*//example.co/bookstack', 'url' => 'https://example.co/bookstack/a/path', 'result' => true],\n            ['config' => '*//example.co*', 'url' => 'https://example.co.uk/bookstack/a/path', 'result' => true],\n            ['config' => 'https://example.com', 'url' => 'https://example.com/a/b/c?test=cat', 'result' => true],\n            ['config' => 'https://example.com', 'url' => 'https://example.co.uk', 'result' => false],\n\n            // Escapes\n            ['config' => 'https://(.*?).com', 'url' => 'https://example.com', 'result' => false],\n            ['config' => 'https://example.com', 'url' => 'https://example.co.uk#https://example.com', 'result' => false],\n\n            // Multi values\n            ['config' => '*//example.org *//example.com', 'url' => 'https://example.com', 'result' => true],\n            ['config' => '*//example.org *//example.com', 'url' => 'https://example.com/a/b/c?test=cat#hello', 'result' => true],\n            ['config' => '*.example.org *.example.com', 'url' => 'https://example.co.uk', 'result' => false],\n            ['config' => '  *.example.org  *.example.com  ', 'url' => 'https://example.co.uk', 'result' => false],\n            ['config' => '* *.example.com', 'url' => 'https://example.co.uk', 'result' => true],\n            ['config' => '*//example.org *//example.com *//example.co.uk', 'url' => 'https://example.co.uk', 'result' => true],\n            ['config' => '*//example.org *//example.com *//example.co.uk', 'url' => 'https://example.net', 'result' => false],\n        ];\n\n        foreach ($testMap as $test) {\n            $result = (new SsrUrlValidator($test['config']))->allowed($test['url']);\n            $this->assertEquals($test['result'], $result, \"Failed asserting url '{$test['url']}' with config '{$test['config']}' results \" . ($test['result'] ? 'true' : 'false'));\n        }\n    }\n\n    public function test_enssure_allowed()\n    {\n        $result = (new SsrUrlValidator('https://example.com'))->ensureAllowed('https://example.com');\n        $this->assertNull($result);\n\n        $this->expectException(HttpFetchException::class);\n        (new SsrUrlValidator('https://example.com'))->ensureAllowed('https://test.example.com');\n    }\n}\n"
  },
  {
    "path": "tests/Uploads/AttachmentTest.php",
    "content": "<?php\n\nnamespace Tests\\Uploads;\n\nuse BookStack\\Entities\\Models\\Page;\nuse BookStack\\Entities\\Repos\\PageRepo;\nuse BookStack\\Entities\\Tools\\TrashCan;\nuse BookStack\\Uploads\\Attachment;\nuse Tests\\TestCase;\n\nclass AttachmentTest extends TestCase\n{\n    public function test_file_upload()\n    {\n        $page = $this->entities->page();\n        $this->asAdmin();\n        $admin = $this->users->admin();\n        $fileName = 'upload_test_file.txt';\n\n        $expectedResp = [\n            'name'       => $fileName,\n            'uploaded_to' => $page->id,\n            'extension'  => 'txt',\n            'order'      => 1,\n            'created_by' => $admin->id,\n            'updated_by' => $admin->id,\n        ];\n\n        $upload = $this->files->uploadAttachmentFile($this, $fileName, $page->id);\n        $upload->assertStatus(200);\n\n        $attachment = Attachment::query()->orderBy('id', 'desc')->first();\n        $upload->assertJson($expectedResp);\n\n        $expectedResp['path'] = $attachment->path;\n        $this->assertDatabaseHas('attachments', $expectedResp);\n\n        $this->files->deleteAllAttachmentFiles();\n    }\n\n    public function test_file_upload_does_not_use_filename()\n    {\n        $page = $this->entities->page();\n        $fileName = 'upload_test_file.txt';\n\n        $this->asAdmin();\n        $upload = $this->files->uploadAttachmentFile($this, $fileName, $page->id);\n        $upload->assertStatus(200);\n\n        $attachment = Attachment::query()->orderBy('id', 'desc')->first();\n        $this->assertStringNotContainsString($fileName, $attachment->path);\n        $this->assertStringEndsWith('-txt', $attachment->path);\n        $this->files->deleteAllAttachmentFiles();\n    }\n\n    public function test_file_display_and_access()\n    {\n        $page = $this->entities->page();\n        $this->asAdmin();\n        $fileName = 'upload_test_file.txt';\n\n        $upload = $this->files->uploadAttachmentFile($this, $fileName, $page->id);\n        $upload->assertStatus(200);\n        $attachment = Attachment::orderBy('id', 'desc')->take(1)->first();\n\n        $pageGet = $this->get($page->getUrl());\n        $pageGet->assertSeeText($fileName);\n        $pageGet->assertSee($attachment->getUrl());\n\n        $attachmentGet = $this->get($attachment->getUrl());\n        $content = $attachmentGet->streamedContent();\n        $this->assertStringContainsString('Hi, This is a test file for testing the upload process.', $content);\n\n        $this->files->deleteAllAttachmentFiles();\n    }\n\n    public function test_attaching_link_to_page()\n    {\n        $page = $this->entities->page();\n        $admin = $this->users->admin();\n        $this->asAdmin();\n\n        $linkReq = $this->call('POST', 'attachments/link', [\n            'attachment_link_url'         => 'https://example.com',\n            'attachment_link_name'        => 'Example Attachment Link',\n            'attachment_link_uploaded_to' => $page->id,\n        ]);\n\n        $expectedData = [\n            'path'        => 'https://example.com',\n            'name'        => 'Example Attachment Link',\n            'uploaded_to' => $page->id,\n            'created_by'  => $admin->id,\n            'updated_by'  => $admin->id,\n            'external'    => true,\n            'order'       => 1,\n            'extension'   => '',\n        ];\n\n        $linkReq->assertStatus(200);\n        $this->assertDatabaseHas('attachments', $expectedData);\n        $attachment = Attachment::orderBy('id', 'desc')->take(1)->first();\n\n        $pageGet = $this->get($page->getUrl());\n        $pageGet->assertSeeText('Example Attachment Link');\n        $pageGet->assertSee($attachment->getUrl());\n\n        $attachmentGet = $this->get($attachment->getUrl());\n        $attachmentGet->assertRedirect('https://example.com');\n\n        $this->files->deleteAllAttachmentFiles();\n    }\n\n    public function test_attaching_long_links_to_a_page()\n    {\n        $page = $this->entities->page();\n\n        $link = 'https://example.com?query=' . str_repeat('catsIScool', 195);\n        $linkReq = $this->asAdmin()->post('attachments/link', [\n            'attachment_link_url'         => $link,\n            'attachment_link_name'        => 'Example Attachment Link',\n            'attachment_link_uploaded_to' => $page->id,\n        ]);\n\n        $linkReq->assertStatus(200);\n        $this->assertDatabaseHas('attachments', [\n            'uploaded_to' => $page->id,\n            'path' => $link,\n            'external' => true,\n        ]);\n\n        $attachment = $page->attachments()->where('external', '=', true)->first();\n        $resp = $this->get($attachment->getUrl());\n        $resp->assertRedirect($link);\n    }\n\n    public function test_attachment_updating()\n    {\n        $page = $this->entities->page();\n        $this->asAdmin();\n\n        $attachment = Attachment::factory()->create(['uploaded_to' => $page->id]);\n        $update = $this->call('PUT', 'attachments/' . $attachment->id, [\n            'attachment_edit_name' => 'My new attachment name',\n            'attachment_edit_url'  => 'https://test.example.com',\n        ]);\n\n        $expectedData = [\n            'id'          => $attachment->id,\n            'path'        => 'https://test.example.com',\n            'name'        => 'My new attachment name',\n            'uploaded_to' => $page->id,\n        ];\n\n        $update->assertStatus(200);\n        $this->assertDatabaseHas('attachments', $expectedData);\n\n        $this->files->deleteAllAttachmentFiles();\n    }\n\n    public function test_file_deletion()\n    {\n        $page = $this->entities->page();\n        $this->asAdmin();\n        $fileName = 'deletion_test.txt';\n        $this->files->uploadAttachmentFile($this, $fileName, $page->id);\n\n        $attachment = Attachment::query()->orderBy('id', 'desc')->first();\n        $filePath = storage_path($attachment->path);\n        $this->assertTrue(file_exists($filePath), 'File at path ' . $filePath . ' does not exist');\n\n        $attachment = Attachment::first();\n        $this->delete($attachment->getUrl());\n\n        $this->assertDatabaseMissing('attachments', [\n            'name' => $fileName,\n        ]);\n        $this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected');\n\n        $this->files->deleteAllAttachmentFiles();\n    }\n\n    public function test_attachment_deletion_on_page_deletion()\n    {\n        $page = $this->entities->page();\n        $this->asAdmin();\n        $fileName = 'deletion_test.txt';\n        $this->files->uploadAttachmentFile($this, $fileName, $page->id);\n\n        $attachment = Attachment::query()->orderBy('id', 'desc')->first();\n        $filePath = storage_path($attachment->path);\n\n        $this->assertTrue(file_exists($filePath), 'File at path ' . $filePath . ' does not exist');\n        $this->assertDatabaseHas('attachments', [\n            'name' => $fileName,\n        ]);\n\n        app(PageRepo::class)->destroy($page);\n        app(TrashCan::class)->empty();\n\n        $this->assertDatabaseMissing('attachments', [\n            'name' => $fileName,\n        ]);\n        $this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected');\n\n        $this->files->deleteAllAttachmentFiles();\n    }\n\n    public function test_attachment_access_without_permission_shows_404()\n    {\n        $admin = $this->users->admin();\n        $viewer = $this->users->viewer();\n        $page = $this->entities->page(); /** @var Page $page */\n        $this->actingAs($admin);\n        $fileName = 'permission_test.txt';\n        $this->files->uploadAttachmentFile($this, $fileName, $page->id);\n        $attachment = Attachment::orderBy('id', 'desc')->take(1)->first();\n\n        $this->permissions->setEntityPermissions($page, [], []);\n\n        $this->actingAs($viewer);\n        $attachmentGet = $this->get($attachment->getUrl());\n        $attachmentGet->assertStatus(404);\n        $attachmentGet->assertSee('Attachment not found');\n\n        $this->files->deleteAllAttachmentFiles();\n    }\n\n    public function test_data_and_js_links_cannot_be_attached_to_a_page()\n    {\n        $page = $this->entities->page();\n        $this->asAdmin();\n\n        $badLinks = [\n            'javascript:alert(\"bunny\")',\n            ' javascript:alert(\"bunny\")',\n            'JavaScript:alert(\"bunny\")',\n            \"\\t\\n\\t\\nJavaScript:alert(\\\"bunny\\\")\",\n            'data:text/html;<a></a>',\n            'Data:text/html;<a></a>',\n            'Data:text/html;<a></a>',\n        ];\n\n        foreach ($badLinks as $badLink) {\n            $linkReq = $this->post('attachments/link', [\n                'attachment_link_url'         => $badLink,\n                'attachment_link_name'        => 'Example Attachment Link',\n                'attachment_link_uploaded_to' => $page->id,\n            ]);\n            $linkReq->assertStatus(422);\n            $this->assertDatabaseMissing('attachments', [\n                'path' => $badLink,\n            ]);\n        }\n\n        $attachment = Attachment::factory()->create(['uploaded_to' => $page->id]);\n\n        foreach ($badLinks as $badLink) {\n            $linkReq = $this->put('attachments/' . $attachment->id, [\n                'attachment_edit_url'  => $badLink,\n                'attachment_edit_name' => 'Example Attachment Link',\n            ]);\n            $linkReq->assertStatus(422);\n            $this->assertDatabaseMissing('attachments', [\n                'path' => $badLink,\n            ]);\n        }\n    }\n\n    public function test_attachment_delete_only_shows_with_permission()\n    {\n        $this->asAdmin();\n        $page = $this->entities->page();\n        $this->files->uploadAttachmentFile($this, 'upload_test.txt', $page->id);\n        $attachment = $page->attachments()->first();\n        $viewer = $this->users->viewer();\n\n        $this->permissions->grantUserRolePermissions($viewer, ['page-update-all', 'attachment-create-all']);\n\n        $resp = $this->actingAs($viewer)->get($page->getUrl('/edit'));\n        $html = $this->withHtml($resp);\n        $html->assertElementExists(\".card[data-id=\\\"{$attachment->id}\\\"]\");\n        $html->assertElementNotExists(\".card[data-id=\\\"{$attachment->id}\\\"] button[title=\\\"Delete\\\"]\");\n\n        $this->permissions->grantUserRolePermissions($viewer, ['attachment-delete-all']);\n\n        $resp = $this->actingAs($viewer)->get($page->getUrl('/edit'));\n        $html = $this->withHtml($resp);\n        $html->assertElementExists(\".card[data-id=\\\"{$attachment->id}\\\"] button[title=\\\"Delete\\\"]\");\n    }\n\n    public function test_attachment_edit_only_shows_with_permission()\n    {\n        $this->asAdmin();\n        $page = $this->entities->page();\n        $this->files->uploadAttachmentFile($this, 'upload_test.txt', $page->id);\n        $attachment = $page->attachments()->first();\n        $viewer = $this->users->viewer();\n\n        $this->permissions->grantUserRolePermissions($viewer, ['page-update-all', 'attachment-create-all']);\n\n        $resp = $this->actingAs($viewer)->get($page->getUrl('/edit'));\n        $html = $this->withHtml($resp);\n        $html->assertElementExists(\".card[data-id=\\\"{$attachment->id}\\\"]\");\n        $html->assertElementNotExists(\".card[data-id=\\\"{$attachment->id}\\\"] button[title=\\\"Edit\\\"]\");\n\n        $this->permissions->grantUserRolePermissions($viewer, ['attachment-update-all']);\n\n        $resp = $this->actingAs($viewer)->get($page->getUrl('/edit'));\n        $html = $this->withHtml($resp);\n        $html->assertElementExists(\".card[data-id=\\\"{$attachment->id}\\\"] button[title=\\\"Edit\\\"]\");\n    }\n\n    public function test_file_access_with_open_query_param_provides_inline_response_with_correct_content_type()\n    {\n        $page = $this->entities->page();\n        $this->asAdmin();\n        $fileName = 'upload_test_file.txt';\n\n        $upload = $this->files->uploadAttachmentFile($this, $fileName, $page->id);\n        $upload->assertStatus(200);\n        $attachment = Attachment::query()->orderBy('id', 'desc')->take(1)->first();\n\n        $attachmentGet = $this->get($attachment->getUrl(true));\n        // http-foundation/Response does some 'fixing' of responses to add charsets to text responses.\n        $attachmentGet->assertHeader('Content-Type', 'text/plain; charset=utf-8');\n        $attachmentGet->assertHeader('Content-Disposition', 'inline; filename*=UTF-8\\'\\'upload_test_file.txt');\n        $attachmentGet->assertHeader('X-Content-Type-Options', 'nosniff');\n\n        $this->files->deleteAllAttachmentFiles();\n    }\n\n    public function test_html_file_access_with_open_forces_plain_content_type()\n    {\n        $page = $this->entities->page();\n        $this->asAdmin();\n\n        $attachment = $this->files->uploadAttachmentDataToPage($this, $page, 'test_file.html', '<html></html><p>testing</p>', 'text/html');\n\n        $attachmentGet = $this->get($attachment->getUrl(true));\n        // http-foundation/Response does some 'fixing' of responses to add charsets to text responses.\n        $attachmentGet->assertHeader('Content-Type', 'text/plain; charset=utf-8');\n        $attachmentGet->assertHeader('Content-Disposition', 'inline; filename*=UTF-8\\'\\'test_file.html');\n\n        $this->files->deleteAllAttachmentFiles();\n    }\n\n    public function test_file_access_name_in_content_disposition_header_is_sanitized()\n    {\n        $page = $this->entities->page();\n        $this->asAdmin();\n\n        $attachment = $this->files->uploadAttachmentDataToPage($this, $page, 'test_file.html', '<html></html><p>testing</p>', 'text/html');\n        $attachment->name = \"my\\\\_/super\\n_fu\\$n_\\tfile\";\n        $attachment->save();\n\n        $attachmentGet = $this->get($attachment->getUrl(true));\n        $attachmentGet->assertHeader('Content-Disposition', 'inline; filename*=UTF-8\\'\\'my_super_fun_file.html');\n\n        $this->files->deleteAllAttachmentFiles();\n    }\n\n    public function test_file_upload_works_when_local_secure_restricted_is_in_use()\n    {\n        config()->set('filesystems.attachments', 'local_secure_restricted');\n\n        $page = $this->entities->page();\n        $fileName = 'upload_test_file.txt';\n\n        $this->asAdmin();\n        $upload = $this->files->uploadAttachmentFile($this, $fileName, $page->id);\n        $upload->assertStatus(200);\n\n        $attachment = Attachment::query()->orderBy('id', 'desc')->where('uploaded_to', '=', $page->id)->first();\n        $this->assertFileExists(storage_path($attachment->path));\n        $this->files->deleteAllAttachmentFiles();\n    }\n\n    public function test_file_get_range_access()\n    {\n        $page = $this->entities->page();\n        $this->asAdmin();\n        $attachment = $this->files->uploadAttachmentDataToPage($this, $page, 'my_text.txt', 'abc123456', 'text/plain');\n\n        // Download access\n        $resp = $this->get($attachment->getUrl(), ['Range' => 'bytes=3-5']);\n        $resp->assertStatus(206);\n        $resp->assertStreamedContent('123');\n        $resp->assertHeader('Content-Length', '3');\n        $resp->assertHeader('Content-Range', 'bytes 3-5/9');\n\n        // Inline access\n        $resp = $this->get($attachment->getUrl(true), ['Range' => 'bytes=5-7']);\n        $resp->assertStatus(206);\n        $resp->assertStreamedContent('345');\n        $resp->assertHeader('Content-Length', '3');\n        $resp->assertHeader('Content-Range', 'bytes 5-7/9');\n\n        $this->files->deleteAllAttachmentFiles();\n    }\n\n    public function test_file_head_range_returns_no_content()\n    {\n        $page = $this->entities->page();\n        $this->asAdmin();\n        $attachment = $this->files->uploadAttachmentDataToPage($this, $page, 'my_text.txt', 'abc123456', 'text/plain');\n\n        $resp = $this->head($attachment->getUrl(), ['Range' => 'bytes=0-9']);\n        $resp->assertStreamedContent('');\n        $resp->assertHeader('Content-Length', '9');\n        $resp->assertStatus(200);\n\n        $this->files->deleteAllAttachmentFiles();\n    }\n\n    public function test_file_head_range_edge_cases()\n    {\n        $page = $this->entities->page();\n        $this->asAdmin();\n\n        // Mime-type \"sniffing\" happens on first 2k bytes, hence this content (2005 bytes)\n        $content = '01234' . str_repeat('a', 1990) . '0123456789';\n        $attachment = $this->files->uploadAttachmentDataToPage($this, $page, 'my_text.txt', $content, 'text/plain');\n\n        // Test for both inline and download attachment serving\n        foreach ([true, false] as $isInline) {\n            // No end range\n            $resp = $this->get($attachment->getUrl($isInline), ['Range' => 'bytes=5-']);\n            $resp->assertStreamedContent(substr($content, 5));\n            $resp->assertHeader('Content-Length', '2000');\n            $resp->assertHeader('Content-Range', 'bytes 5-2004/2005');\n            $resp->assertStatus(206);\n\n            // End only range\n            $resp = $this->get($attachment->getUrl($isInline), ['Range' => 'bytes=-10']);\n            $resp->assertStreamedContent('0123456789');\n            $resp->assertHeader('Content-Length', '10');\n            $resp->assertHeader('Content-Range', 'bytes 1995-2004/2005');\n            $resp->assertStatus(206);\n\n            // Range across sniff point\n            $resp = $this->get($attachment->getUrl($isInline), ['Range' => 'bytes=1997-2002']);\n            $resp->assertStreamedContent('234567');\n            $resp->assertHeader('Content-Length', '6');\n            $resp->assertHeader('Content-Range', 'bytes 1997-2002/2005');\n            $resp->assertStatus(206);\n\n            // Range up to sniff point\n            $resp = $this->get($attachment->getUrl($isInline), ['Range' => 'bytes=0-1997']);\n            $resp->assertHeader('Content-Length', '1998');\n            $resp->assertHeader('Content-Range', 'bytes 0-1997/2005');\n            $resp->assertStreamedContent(substr($content, 0, 1998));\n            $resp->assertStatus(206);\n\n            // Range beyond sniff point\n            $resp = $this->get($attachment->getUrl($isInline), ['Range' => 'bytes=2001-2003']);\n            $resp->assertStreamedContent('678');\n            $resp->assertHeader('Content-Length', '3');\n            $resp->assertHeader('Content-Range', 'bytes 2001-2003/2005');\n            $resp->assertStatus(206);\n\n            // Range beyond content\n            $resp = $this->get($attachment->getUrl($isInline), ['Range' => 'bytes=0-2010']);\n            $resp->assertStreamedContent($content);\n            $resp->assertHeader('Content-Length', '2005');\n            $resp->assertHeader('Content-Range', 'bytes 0-2004/2005');\n            $resp->assertStatus(206);\n\n            // Range start before end\n            $resp = $this->get($attachment->getUrl($isInline), ['Range' => 'bytes=50-10']);\n            $resp->assertStreamedContent($content);\n            $resp->assertHeader('Content-Length', '2005');\n            $resp->assertHeader('Content-Range', 'bytes */2005');\n            $resp->assertStatus(416);\n\n            // Full range request\n            $resp = $this->get($attachment->getUrl($isInline), ['Range' => 'bytes=0-']);\n            $resp->assertStreamedContent($content);\n            $resp->assertHeader('Content-Length', '2005');\n            $resp->assertHeader('Content-Range', 'bytes 0-2004/2005');\n            $resp->assertStatus(206);\n        }\n\n        $this->files->deleteAllAttachmentFiles();\n    }\n}\n"
  },
  {
    "path": "tests/Uploads/AvatarTest.php",
    "content": "<?php\n\nnamespace Tests\\Uploads;\n\nuse BookStack\\Exceptions\\HttpFetchException;\nuse BookStack\\Uploads\\UserAvatars;\nuse BookStack\\Users\\Models\\User;\nuse GuzzleHttp\\Exception\\ConnectException;\nuse GuzzleHttp\\Psr7\\Request;\nuse GuzzleHttp\\Psr7\\Response;\nuse Tests\\TestCase;\n\nclass AvatarTest extends TestCase\n{\n    protected function createUserRequest($user): User\n    {\n        $this->asAdmin()->post('/settings/users/create', [\n            'name'             => $user->name,\n            'email'            => $user->email,\n            'password'         => 'testing101',\n            'password-confirm' => 'testing101',\n        ]);\n\n        return User::query()->where('email', '=', $user->email)->first();\n    }\n\n    protected function deleteUserImage(User $user): void\n    {\n        $this->files->deleteAtRelativePath($user->avatar->path);\n    }\n\n    public function test_gravatar_fetched_on_user_create()\n    {\n        $requests = $this->mockHttpClient([new Response(200, ['Content-Type' => 'image/png'], $this->files->pngImageData())]);\n        config()->set(['services.disable_services' => false]);\n        $user = User::factory()->make();\n\n        $user = $this->createUserRequest($user);\n        $this->assertDatabaseHas('images', [\n            'type'       => 'user',\n            'created_by' => $user->id,\n        ]);\n        $this->deleteUserImage($user);\n\n        $expectedUri = 'https://www.gravatar.com/avatar/' . md5(strtolower($user->email)) . '?s=500&d=identicon';\n        $this->assertEquals($expectedUri, $requests->latestRequest()->getUri());\n    }\n\n    public function test_custom_url_used_if_set()\n    {\n        config()->set([\n            'services.disable_services' => false,\n            'services.avatar_url'       => 'https://example.com/${email}/${hash}/${size}',\n        ]);\n\n        $user = User::factory()->make();\n        $url = 'https://example.com/' . urlencode(strtolower($user->email)) . '/' . md5(strtolower($user->email)) . '/500';\n        $requests = $this->mockHttpClient([new Response(200, ['Content-Type' => 'image/png'], $this->files->pngImageData())]);\n\n        $user = $this->createUserRequest($user);\n        $this->assertEquals($url, $requests->latestRequest()->getUri());\n        $this->deleteUserImage($user);\n    }\n\n    public function test_avatar_not_fetched_if_no_custom_url_and_services_disabled()\n    {\n        config()->set(['services.disable_services' => true]);\n        $user = User::factory()->make();\n        $requests = $this->mockHttpClient([new Response()]);\n\n        $this->createUserRequest($user);\n\n        $this->assertEquals(0, $requests->requestCount());\n    }\n\n    public function test_avatar_not_fetched_if_avatar_url_option_set_to_false()\n    {\n        config()->set([\n            'services.disable_services' => false,\n            'services.avatar_url'       => false,\n        ]);\n\n        $user = User::factory()->make();\n        $requests = $this->mockHttpClient([new Response()]);\n\n        $this->createUserRequest($user);\n\n        $this->assertEquals(0, $requests->requestCount());\n    }\n\n    public function test_no_failure_but_error_logged_on_failed_avatar_fetch()\n    {\n        config()->set(['services.disable_services' => false]);\n\n        $this->mockHttpClient([new ConnectException('Failed to connect', new Request('GET', ''))]);\n\n        $logger = $this->withTestLogger();\n\n        $user = User::factory()->make();\n        $this->createUserRequest($user);\n        $this->assertTrue($logger->hasError('Failed to save user avatar image'));\n    }\n\n    public function test_exception_message_on_failed_fetch()\n    {\n        // set wrong url\n        config()->set([\n            'services.disable_services' => false,\n            'services.avatar_url'       => 'http_malformed_url/${email}/${hash}/${size}',\n        ]);\n\n        $user = User::factory()->make();\n        $avatar = app()->make(UserAvatars::class);\n        $logger = $this->withTestLogger();\n        $this->mockHttpClient([new ConnectException('Could not resolve host http_malformed_url', new Request('GET', ''))]);\n\n        $avatar->fetchAndAssignToUser($user);\n\n        $url = 'http_malformed_url/' . urlencode(strtolower($user->email)) . '/' . md5(strtolower($user->email)) . '/500';\n        $this->assertTrue($logger->hasError('Failed to save user avatar image'));\n        $exception = $logger->getRecords()[0]['context']['exception'];\n        $this->assertInstanceOf(HttpFetchException::class, $exception);\n        $this->assertEquals('Cannot get image from ' . $url, $exception->getMessage());\n        $this->assertEquals('Could not resolve host http_malformed_url', $exception->getPrevious()->getMessage());\n    }\n}\n"
  },
  {
    "path": "tests/Uploads/DrawioTest.php",
    "content": "<?php\n\nnamespace Tests\\Uploads;\n\nuse BookStack\\Uploads\\Image;\nuse Tests\\TestCase;\n\nclass DrawioTest extends TestCase\n{\n    public function test_get_image_as_base64()\n    {\n        $page = $this->entities->page();\n        $this->asAdmin();\n        $imageName = 'first-image.png';\n\n        $this->files->uploadGalleryImage($this, $imageName, $page->id);\n        /** @var Image $image */\n        $image = Image::query()->first();\n        $image->type = 'drawio';\n        $image->save();\n\n        $imageGet = $this->getJson(\"/images/drawio/base64/{$image->id}\");\n        $imageGet->assertJson([\n            'content' => 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=',\n        ]);\n    }\n\n    public function test_non_accessible_image_returns_404_error_and_message()\n    {\n        $page = $this->entities->page();\n        $this->asEditor();\n        $imageName = 'non-accessible-image.png';\n\n        $this->files->uploadGalleryImage($this, $imageName, $page->id);\n        /** @var Image $image */\n        $image = Image::query()->first();\n        $image->type = 'drawio';\n        $image->save();\n        $this->permissions->disableEntityInheritedPermissions($page);\n\n        $imageGet = $this->getJson(\"/images/drawio/base64/{$image->id}\");\n        $imageGet->assertNotFound();\n        $imageGet->assertJson([\n            'message' => 'Drawing data could not be loaded. The drawing file might no longer exist or you may not have permission to access it.',\n        ]);\n    }\n\n    public function test_drawing_base64_upload()\n    {\n        $page = $this->entities->page();\n        $editor = $this->users->editor();\n        $this->actingAs($editor);\n\n        $upload = $this->postJson('images/drawio', [\n            'uploaded_to' => $page->id,\n            'image'       => 'image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=',\n        ]);\n\n        $upload->assertStatus(200);\n        $upload->assertJson([\n            'type'        => 'drawio',\n            'uploaded_to' => $page->id,\n            'created_by'  => $editor->id,\n            'updated_by'  => $editor->id,\n        ]);\n\n        $image = Image::where('type', '=', 'drawio')->first();\n        $this->assertTrue(file_exists(public_path($image->path)), 'Uploaded image not found at path: ' . public_path($image->path));\n\n        $testImageData = $this->files->pngImageData();\n        $uploadedImageData = file_get_contents(public_path($image->path));\n        $this->assertTrue($testImageData === $uploadedImageData, 'Uploaded image file data does not match our test image as expected');\n    }\n\n    public function test_drawio_url_can_be_configured()\n    {\n        config()->set('services.drawio', 'http://cats.com?dog=tree');\n        $page = $this->entities->page();\n        $editor = $this->users->editor();\n\n        $resp = $this->actingAs($editor)->get($page->getUrl('/edit'));\n        $resp->assertSee('drawio-url=\"http://cats.com?dog=tree\"', false);\n    }\n\n    public function test_drawio_url_can_be_disabled()\n    {\n        config()->set('services.drawio', true);\n        $page = $this->entities->page();\n        $editor = $this->users->editor();\n\n        $resp = $this->actingAs($editor)->get($page->getUrl('/edit'));\n        $resp->assertSee('drawio-url=\"https://embed.diagrams.net/?embed=1&amp;proto=json&amp;spin=1&amp;configure=1\"', false);\n\n        config()->set('services.drawio', false);\n        $resp = $this->actingAs($editor)->get($page->getUrl('/edit'));\n        $resp->assertDontSee('drawio-url', false);\n    }\n}\n"
  },
  {
    "path": "tests/Uploads/ImageStorageTest.php",
    "content": "<?php\n\nnamespace Tests\\Uploads;\n\nuse BookStack\\Uploads\\ImageStorage;\nuse Tests\\TestCase;\n\nclass ImageStorageTest extends TestCase\n{\n    public function test_local_image_storage_sets_755_directory_permissions()\n    {\n        if (PHP_OS_FAMILY !== 'Linux') {\n            $this->markTestSkipped('Test only works on Linux');\n        }\n\n        config()->set('filesystems.default', 'local');\n        $storage = $this->app->make(ImageStorage::class);\n        $dirToCheck = 'test-dir-perms-' . substr(md5(random_bytes(16)), 0, 6);\n\n        $disk = $storage->getDisk('gallery');\n        $disk->put(\"{$dirToCheck}/image.png\", 'abc', true);\n\n        $expectedPath = public_path(\"uploads/images/{$dirToCheck}\");\n        $permissionsApplied = substr(sprintf('%o', fileperms($expectedPath)), -4);\n        $this->assertEquals('0755', $permissionsApplied);\n\n        @unlink(\"{$expectedPath}/image.png\");\n        @rmdir($expectedPath);\n    }\n}\n"
  },
  {
    "path": "tests/Uploads/ImageTest.php",
    "content": "<?php\n\nnamespace Tests\\Uploads;\n\nuse BookStack\\Entities\\Repos\\PageRepo;\nuse BookStack\\Uploads\\Image;\nuse BookStack\\Uploads\\ImageService;\nuse BookStack\\Uploads\\UserAvatars;\nuse BookStack\\Users\\Models\\Role;\nuse Illuminate\\Support\\Str;\nuse Tests\\TestCase;\n\nclass ImageTest extends TestCase\n{\n    public function test_image_upload()\n    {\n        $page = $this->entities->page();\n        $admin = $this->users->admin();\n        $this->actingAs($admin);\n\n        $imgDetails = $this->files->uploadGalleryImageToPage($this, $page);\n        $relPath = $imgDetails['path'];\n\n        $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image found at path: ' . public_path($relPath));\n\n        $this->files->deleteAtRelativePath($relPath);\n\n        $this->assertDatabaseHas('images', [\n            'url'         => $this->baseUrl . $relPath,\n            'type'        => 'gallery',\n            'uploaded_to' => $page->id,\n            'path'        => $relPath,\n            'created_by'  => $admin->id,\n            'updated_by'  => $admin->id,\n            'name'        => $imgDetails['name'],\n        ]);\n    }\n\n    public function test_image_display_thumbnail_generation_does_not_increase_image_size()\n    {\n        $page = $this->entities->page();\n        $admin = $this->users->admin();\n        $this->actingAs($admin);\n\n        $originalFile = $this->files->testFilePath('compressed.png');\n        $originalFileSize = filesize($originalFile);\n        $imgDetails = $this->files->uploadGalleryImageToPage($this, $page, 'compressed.png');\n        $relPath = $imgDetails['path'];\n\n        $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image found at path: ' . public_path($relPath));\n        $displayImage = $imgDetails['response']->thumbs->display;\n\n        $displayImageRelPath = implode('/', array_slice(explode('/', $displayImage), 3));\n        $displayImagePath = public_path($displayImageRelPath);\n        $displayFileSize = filesize($displayImagePath);\n\n        $this->files->deleteAtRelativePath($relPath);\n        $this->files->deleteAtRelativePath($displayImageRelPath);\n\n        $this->assertEquals($originalFileSize, $displayFileSize, 'Display thumbnail generation should not increase image size');\n    }\n\n    public function test_image_display_thumbnail_generation_for_apng_images_uses_original_file()\n    {\n        $page = $this->entities->page();\n        $admin = $this->users->admin();\n        $this->actingAs($admin);\n\n        $imgDetails = $this->files->uploadGalleryImageToPage($this, $page, 'animated.png');\n        $this->files->deleteAtRelativePath($imgDetails['path']);\n\n        $this->assertStringContainsString('thumbs-', $imgDetails['response']->thumbs->gallery);\n        $this->assertStringNotContainsString('scaled-', $imgDetails['response']->thumbs->display);\n    }\n\n    public function test_image_display_thumbnail_generation_for_animated_avif_images_uses_original_file()\n    {\n        if (! function_exists('imageavif')) {\n            $this->markTestSkipped('imageavif() is not available');\n        }\n\n        $page = $this->entities->page();\n        $admin = $this->users->admin();\n        $this->actingAs($admin);\n\n        $imgDetails = $this->files->uploadGalleryImageToPage($this, $page, 'animated.avif');\n        $this->files->deleteAtRelativePath($imgDetails['path']);\n\n        $this->assertStringContainsString('thumbs-', $imgDetails['response']->thumbs->gallery);\n        $this->assertStringNotContainsString('scaled-', $imgDetails['response']->thumbs->display);\n    }\n\n    public function test_image_edit()\n    {\n        $editor = $this->users->editor();\n        $this->actingAs($editor);\n\n        $imgDetails = $this->files->uploadGalleryImageToPage($this, $this->entities->page());\n        $image = Image::query()->first();\n\n        $newName = Str::random();\n        $update = $this->put('/images/' . $image->id, ['name' => $newName]);\n        $update->assertSuccessful();\n        $update->assertSee($newName);\n\n        $this->files->deleteAtRelativePath($imgDetails['path']);\n\n        $this->assertDatabaseHas('images', [\n            'type' => 'gallery',\n            'name' => $newName,\n        ]);\n    }\n\n    public function test_image_file_update()\n    {\n        $page = $this->entities->page();\n        $this->asEditor();\n\n        $imgDetails = $this->files->uploadGalleryImageToPage($this, $page);\n        $relPath = $imgDetails['path'];\n\n        $newUpload = $this->files->uploadedImage('updated-image.png', 'compressed.png');\n        $this->assertFileEquals($this->files->testFilePath('test-image.png'), public_path($relPath));\n\n        $imageId = $imgDetails['response']->id;\n        $image = Image::findOrFail($imageId);\n        $image->updated_at = now()->subMonth();\n        $image->save();\n\n        $this->call('PUT', \"/images/{$imageId}/file\", [], [], ['file' => $newUpload])\n            ->assertOk();\n\n        $this->assertFileEquals($this->files->testFilePath('compressed.png'), public_path($relPath));\n\n        $image->refresh();\n        $this->assertTrue($image->updated_at->gt(now()->subMinute()));\n\n        $this->files->deleteAtRelativePath($relPath);\n    }\n\n    public function test_image_file_update_allows_case_differences()\n    {\n        $page = $this->entities->page();\n        $this->asEditor();\n\n        $imgDetails = $this->files->uploadGalleryImageToPage($this, $page);\n        $relPath = $imgDetails['path'];\n\n        $newUpload = $this->files->uploadedImage('updated-image.PNG', 'compressed.png');\n        $this->assertFileEquals($this->files->testFilePath('test-image.png'), public_path($relPath));\n\n        $imageId = $imgDetails['response']->id;\n        $image = Image::findOrFail($imageId);\n        $image->updated_at = now()->subMonth();\n        $image->save();\n\n        $this->call('PUT', \"/images/{$imageId}/file\", [], [], ['file' => $newUpload])\n            ->assertOk();\n\n        $this->assertFileEquals($this->files->testFilePath('compressed.png'), public_path($relPath));\n\n        $image->refresh();\n        $this->assertTrue($image->updated_at->gt(now()->subMinute()));\n\n        $this->files->deleteAtRelativePath($relPath);\n    }\n\n    public function test_image_file_update_does_not_allow_change_in_image_extension()\n    {\n        $page = $this->entities->page();\n        $this->asEditor();\n\n        $imgDetails = $this->files->uploadGalleryImageToPage($this, $page);\n        $relPath = $imgDetails['path'];\n        $newUpload = $this->files->uploadedImage('updated-image.jpg', 'compressed.png');\n\n        $imageId = $imgDetails['response']->id;\n        $this->call('PUT', \"/images/{$imageId}/file\", [], [], ['file' => $newUpload])\n            ->assertJson([\n                \"message\" => \"Image file replacements must be of the same type\",\n                \"status\" => \"error\",\n            ]);\n\n        $this->files->deleteAtRelativePath($relPath);\n    }\n\n    public function test_gallery_get_list_format()\n    {\n        $this->asEditor();\n\n        $imgDetails = $this->files->uploadGalleryImageToPage($this, $this->entities->page());\n        $image = Image::query()->first();\n\n        $pageId = $imgDetails['page']->id;\n        $firstPageRequest = $this->get(\"/images/gallery?page=1&uploaded_to={$pageId}\");\n        $firstPageRequest->assertSuccessful();\n        $this->withHtml($firstPageRequest)->assertElementExists('div');\n        $firstPageRequest->assertSuccessful()->assertSeeText($image->name);\n\n        $secondPageRequest = $this->get(\"/images/gallery?page=2&uploaded_to={$pageId}\");\n        $secondPageRequest->assertSuccessful();\n        $this->withHtml($secondPageRequest)->assertElementNotExists('div');\n\n        $namePartial = substr($imgDetails['name'], 0, 3);\n        $searchHitRequest = $this->get(\"/images/gallery?page=1&uploaded_to={$pageId}&search={$namePartial}\");\n        $searchHitRequest->assertSuccessful()->assertSee($imgDetails['name']);\n\n        $namePartial = Str::random(16);\n        $searchFailRequest = $this->get(\"/images/gallery?page=1&uploaded_to={$pageId}&search={$namePartial}\");\n        $searchFailRequest->assertSuccessful()->assertDontSee($imgDetails['name']);\n        $searchFailRequest->assertSuccessful();\n        $this->withHtml($searchFailRequest)->assertElementNotExists('div');\n    }\n\n    public function test_image_gallery_lists_for_draft_page()\n    {\n        $this->actingAs($this->users->editor());\n        $draft = $this->entities->newDraftPage();\n        $this->files->uploadGalleryImageToPage($this, $draft);\n        $image = Image::query()->where('uploaded_to', '=', $draft->id)->firstOrFail();\n\n        $resp = $this->get(\"/images/gallery?page=1&uploaded_to={$draft->id}\");\n        $resp->assertSee($image->getThumb(150, 150));\n    }\n\n    public function test_image_usage()\n    {\n        $page = $this->entities->page();\n        $editor = $this->users->editor();\n        $this->actingAs($editor);\n\n        $imgDetails = $this->files->uploadGalleryImageToPage($this, $page);\n\n        $image = Image::query()->first();\n        $page->html = '<img src=\"' . $image->url . '\">';\n        $page->save();\n\n        $usage = $this->get('/images/edit/' . $image->id . '?delete=true');\n        $usage->assertSuccessful();\n        $usage->assertSeeText($page->name);\n        $usage->assertSee($page->getUrl());\n\n        $this->files->deleteAtRelativePath($imgDetails['path']);\n    }\n\n    public function test_php_files_cannot_be_uploaded()\n    {\n        $page = $this->entities->page();\n        $admin = $this->users->admin();\n        $this->actingAs($admin);\n\n        $fileName = 'bad.php';\n        $relPath = $this->files->expectedImagePath('gallery', $fileName);\n        $this->files->deleteAtRelativePath($relPath);\n\n        $file = $this->files->imageFromBase64File('bad-php.base64', $fileName);\n        $upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []);\n        $upload->assertStatus(500);\n        $this->assertStringContainsString('The file must have a valid & supported image extension', $upload->json('message'));\n\n        $this->assertFalse(file_exists(public_path($relPath)), 'Uploaded php file was uploaded but should have been stopped');\n\n        $this->assertDatabaseMissing('images', [\n            'type' => 'gallery',\n            'name' => $fileName,\n        ]);\n    }\n\n    public function test_php_like_files_cannot_be_uploaded()\n    {\n        $page = $this->entities->page();\n        $admin = $this->users->admin();\n        $this->actingAs($admin);\n\n        $fileName = 'bad.phtml';\n        $relPath = $this->files->expectedImagePath('gallery', $fileName);\n        $this->files->deleteAtRelativePath($relPath);\n\n        $file = $this->files->imageFromBase64File('bad-phtml.base64', $fileName);\n        $upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []);\n        $upload->assertStatus(500);\n        $this->assertStringContainsString('The file must have a valid & supported image extension', $upload->json('message'));\n\n        $this->assertFalse(file_exists(public_path($relPath)), 'Uploaded php file was uploaded but should have been stopped');\n    }\n\n    public function test_files_with_double_extensions_will_get_sanitized()\n    {\n        $page = $this->entities->page();\n        $admin = $this->users->admin();\n        $this->actingAs($admin);\n\n        $fileName = 'bad.phtml.png';\n        $relPath = $this->files->expectedImagePath('gallery', $fileName);\n        $expectedRelPath = dirname($relPath) . '/bad-phtml.png';\n        $this->files->deleteAtRelativePath($expectedRelPath);\n\n        $file = $this->files->imageFromBase64File('bad-phtml-png.base64', $fileName);\n        $upload = $this->withHeader('Content-Type', 'image/png')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []);\n        $upload->assertStatus(200);\n\n        $lastImage = Image::query()->latest('id')->first();\n\n        $this->assertEquals('bad.phtml.png', $lastImage->name);\n        $this->assertEquals('bad-phtml.png', basename($lastImage->path));\n        $this->assertFileDoesNotExist(public_path($relPath), 'Uploaded image file name was not stripped of dots');\n        $this->assertFileExists(public_path($expectedRelPath));\n\n        $this->files->deleteAtRelativePath($lastImage->path);\n    }\n\n    public function test_url_entities_removed_from_filenames()\n    {\n        $this->asEditor();\n        $badNames = [\n            'bad-char-#-image.png',\n            'bad-char-?-image.png',\n            '?#.png',\n            '?.png',\n            '#.png',\n        ];\n        foreach ($badNames as $name) {\n            $galleryFile = $this->files->uploadedImage($name);\n            $page = $this->entities->page();\n            $badPath = $this->files->expectedImagePath('gallery', $name);\n            $this->files->deleteAtRelativePath($badPath);\n\n            $upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);\n            $upload->assertStatus(200);\n\n            $lastImage = Image::query()->latest('id')->first();\n            $newFileName = explode('.', basename($lastImage->path))[0];\n\n            $this->assertEquals($lastImage->name, $name);\n            $this->assertFalse(strpos($lastImage->path, $name), 'Path contains original image name');\n            $this->assertFalse(file_exists(public_path($badPath)), 'Uploaded image file name was not stripped of url entities');\n\n            $this->assertTrue(strlen($newFileName) > 0, 'File name was reduced to nothing');\n\n            $this->files->deleteAtRelativePath($lastImage->path);\n        }\n    }\n\n    public function test_secure_images_uploads_to_correct_place()\n    {\n        config()->set('filesystems.images', 'local_secure');\n        $this->asEditor();\n        $galleryFile = $this->files->uploadedImage('my-secure-test-upload.png');\n        $page = $this->entities->page();\n        $expectedPath = storage_path('uploads/images/gallery/' . date('Y-m') . '/my-secure-test-upload.png');\n\n        $upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);\n        $upload->assertStatus(200);\n\n        $this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: ' . $expectedPath);\n\n        if (file_exists($expectedPath)) {\n            unlink($expectedPath);\n        }\n    }\n\n    public function test_secure_image_paths_traversal_causes_500()\n    {\n        config()->set('filesystems.images', 'local_secure');\n        $this->asEditor();\n\n        $resp = $this->get('/uploads/images/../../logs/laravel.log');\n        $resp->assertStatus(500);\n    }\n\n    public function test_secure_image_paths_traversal_on_non_secure_images_causes_404()\n    {\n        config()->set('filesystems.images', 'local');\n        $this->asEditor();\n\n        $resp = $this->get('/uploads/images/../../logs/laravel.log');\n        $resp->assertStatus(404);\n    }\n\n    public function test_secure_image_paths_dont_serve_non_images()\n    {\n        config()->set('filesystems.images', 'local_secure');\n        $this->asEditor();\n\n        $testFilePath = storage_path('/uploads/images/testing.txt');\n        file_put_contents($testFilePath, 'hello from test_secure_image_paths_dont_serve_non_images');\n\n        $resp = $this->get('/uploads/images/testing.txt');\n        $resp->assertStatus(404);\n    }\n\n    public function test_secure_images_included_in_exports()\n    {\n        config()->set('filesystems.images', 'local_secure');\n        $this->asEditor();\n        $galleryFile = $this->files->uploadedImage('my-secure-test-upload.png');\n        $page = $this->entities->page();\n        $expectedPath = storage_path('uploads/images/gallery/' . date('Y-m') . '/my-secure-test-upload.png');\n\n        $upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);\n        $imageUrl = json_decode($upload->getContent(), true)['url'];\n        $page->html .= \"<img src=\\\"{$imageUrl}\\\">\";\n        $page->save();\n        $upload->assertStatus(200);\n\n        $encodedImageContent = base64_encode(file_get_contents($expectedPath));\n        $export = $this->get($page->getUrl('/export/html'));\n        $this->assertTrue(strpos($export->getContent(), $encodedImageContent) !== false, 'Uploaded image in export content');\n\n        if (file_exists($expectedPath)) {\n            unlink($expectedPath);\n        }\n    }\n\n    public function test_system_images_remain_public_with_local_secure()\n    {\n        config()->set('filesystems.images', 'local_secure');\n        $this->asAdmin();\n        $galleryFile = $this->files->uploadedImage('my-system-test-upload.png');\n        $expectedPath = public_path('uploads/images/system/' . date('Y-m') . '/my-system-test-upload.png');\n\n        $upload = $this->call('POST', '/settings/customization', [], [], ['app_logo' => $galleryFile], []);\n        $upload->assertRedirect('/settings/customization');\n\n        $this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: ' . $expectedPath);\n\n        if (file_exists($expectedPath)) {\n            unlink($expectedPath);\n        }\n    }\n\n    public function test_system_images_remain_public_with_local_secure_restricted()\n    {\n        config()->set('filesystems.images', 'local_secure_restricted');\n        $this->asAdmin();\n        $galleryFile = $this->files->uploadedImage('my-system-test-restricted-upload.png');\n        $expectedPath = public_path('uploads/images/system/' . date('Y-m') . '/my-system-test-restricted-upload.png');\n\n        $upload = $this->call('POST', '/settings/customization', [], [], ['app_logo' => $galleryFile], []);\n        $upload->assertRedirect('/settings/customization');\n\n        $this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: ' . $expectedPath);\n\n        if (file_exists($expectedPath)) {\n            unlink($expectedPath);\n        }\n    }\n\n    public function test_avatar_images_visible_only_when_public_access_enabled_with_local_secure_restricted()\n    {\n        config()->set('filesystems.images', 'local_secure_restricted');\n        $user = $this->users->admin();\n        $avatars = $this->app->make(UserAvatars::class);\n        $avatars->assignToUserFromExistingData($user, $this->files->pngImageData(), 'png');\n\n        $avatarUrl = $user->getAvatar();\n\n        $resp = $this->get($avatarUrl);\n        $resp->assertRedirect('/login');\n\n        $this->permissions->makeAppPublic();\n\n        $resp = $this->get($avatarUrl);\n        $resp->assertOk();\n\n        $this->files->deleteAtRelativePath($user->avatar->path);\n    }\n\n    public function test_secure_restricted_images_inaccessible_without_relation_permission()\n    {\n        config()->set('filesystems.images', 'local_secure_restricted');\n        $this->asEditor();\n        $galleryFile = $this->files->uploadedImage('my-secure-restricted-test-upload.png');\n        $page = $this->entities->page();\n\n        $upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);\n        $upload->assertStatus(200);\n        $expectedUrl = url('uploads/images/gallery/' . date('Y-m') . '/my-secure-restricted-test-upload.png');\n        $expectedPath = storage_path('uploads/images/gallery/' . date('Y-m') . '/my-secure-restricted-test-upload.png');\n\n        $this->get($expectedUrl)->assertOk();\n\n        $this->permissions->setEntityPermissions($page, [], []);\n\n        $resp = $this->get($expectedUrl);\n        $resp->assertNotFound();\n\n        if (file_exists($expectedPath)) {\n            unlink($expectedPath);\n        }\n    }\n\n    public function test_secure_restricted_images_accessible_with_public_guest_access()\n    {\n        config()->set('filesystems.images', 'local_secure_restricted');\n        $this->permissions->makeAppPublic();\n\n        $this->asEditor();\n        $page = $this->entities->page();\n        $this->files->uploadGalleryImageToPage($this, $page);\n        $image = Image::query()->where('type', '=', 'gallery')\n            ->where('uploaded_to', '=', $page->id)\n            ->first();\n\n        $expectedUrl = url($image->path);\n        $expectedPath = storage_path($image->path);\n        auth()->logout();\n\n        $this->get($expectedUrl)->assertOk();\n\n        $this->permissions->setEntityPermissions($page, [], []);\n\n        $resp = $this->get($expectedUrl);\n        $resp->assertNotFound();\n\n        $this->permissions->setEntityPermissions($page, ['view'], [Role::getSystemRole('public')]);\n\n        $this->get($expectedUrl)->assertOk();\n\n        if (file_exists($expectedPath)) {\n            unlink($expectedPath);\n        }\n    }\n\n    public function test_thumbnail_path_handled_by_secure_restricted_images()\n    {\n        config()->set('filesystems.images', 'local_secure_restricted');\n        $this->asEditor();\n        $galleryFile = $this->files->uploadedImage('my-secure-restricted-thumb-test-test.png');\n        $page = $this->entities->page();\n\n        $upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);\n        $upload->assertStatus(200);\n        $expectedUrl = url('uploads/images/gallery/' . date('Y-m') . '/thumbs-150-150/my-secure-restricted-thumb-test-test.png');\n        $expectedPath = storage_path('uploads/images/gallery/' . date('Y-m') . '/my-secure-restricted-thumb-test-test.png');\n\n        $this->get($expectedUrl)->assertOk();\n\n        $this->permissions->setEntityPermissions($page, [], []);\n\n        $resp = $this->get($expectedUrl);\n        $resp->assertNotFound();\n\n        if (file_exists($expectedPath)) {\n            unlink($expectedPath);\n        }\n    }\n\n    public function test_secure_restricted_image_access_controlled_in_exports()\n    {\n        config()->set('filesystems.images', 'local_secure_restricted');\n        $this->asEditor();\n        $galleryFile = $this->files->uploadedImage('my-secure-restricted-export-test.png');\n\n        $pageA = $this->entities->page();\n        $pageB = $this->entities->page();\n        $expectedPath = storage_path('uploads/images/gallery/' . date('Y-m') . '/my-secure-restricted-export-test.png');\n\n        $upload = $this->asEditor()->call('POST', '/images/gallery', ['uploaded_to' => $pageA->id], [], ['file' => $galleryFile], []);\n        $upload->assertOk();\n\n        $imageUrl = json_decode($upload->getContent(), true)['url'];\n        $pageB->html .= \"<img src=\\\"{$imageUrl}\\\">\";\n        $pageB->save();\n\n        $encodedImageContent = base64_encode(file_get_contents($expectedPath));\n        $export = $this->get($pageB->getUrl('/export/html'));\n        $this->assertStringContainsString($encodedImageContent, $export->getContent());\n\n        $this->permissions->setEntityPermissions($pageA, [], []);\n\n        $export = $this->get($pageB->getUrl('/export/html'));\n        $this->assertStringNotContainsString($encodedImageContent, $export->getContent());\n\n        if (file_exists($expectedPath)) {\n            unlink($expectedPath);\n        }\n    }\n\n    public function test_image_delete()\n    {\n        $page = $this->entities->page();\n        $this->asAdmin();\n        $imageName = 'first-image.png';\n        $relPath = $this->files->expectedImagePath('gallery', $imageName);\n        $this->files->deleteAtRelativePath($relPath);\n\n        $this->files->uploadGalleryImage($this, $imageName, $page->id);\n        $image = Image::first();\n\n        $delete = $this->delete('/images/' . $image->id);\n        $delete->assertStatus(200);\n\n        $this->assertDatabaseMissing('images', [\n            'url'  => $this->baseUrl . $relPath,\n            'type' => 'gallery',\n        ]);\n\n        $this->assertFalse(file_exists(public_path($relPath)), 'Uploaded image has not been deleted as expected');\n    }\n\n    public function test_image_delete_does_not_delete_similar_images()\n    {\n        $page = $this->entities->page();\n        $this->asAdmin();\n        $imageName = 'first-image.png';\n\n        $relPath = $this->files->expectedImagePath('gallery', $imageName);\n        $this->files->deleteAtRelativePath($relPath);\n\n        $this->files->uploadGalleryImage($this, $imageName, $page->id);\n        $this->files->uploadGalleryImage($this, $imageName, $page->id);\n        $this->files->uploadGalleryImage($this, $imageName, $page->id);\n\n        $image = Image::first();\n        $folder = public_path(dirname($relPath));\n        $imageCount = count(glob($folder . '/*'));\n\n        $delete = $this->delete('/images/' . $image->id);\n        $delete->assertStatus(200);\n\n        $newCount = count(glob($folder . '/*'));\n        $this->assertEquals($imageCount - 1, $newCount, 'More files than expected have been deleted');\n        $this->assertFalse(file_exists(public_path($relPath)), 'Uploaded image has not been deleted as expected');\n    }\n\n    public function test_image_manager_delete_button_only_shows_with_permission()\n    {\n        $page = $this->entities->page();\n        $this->asAdmin();\n        $imageName = 'first-image.png';\n        $relPath = $this->files->expectedImagePath('gallery', $imageName);\n        $this->files->deleteAtRelativePath($relPath);\n        $viewer = $this->users->viewer();\n\n        $this->files->uploadGalleryImage($this, $imageName, $page->id);\n        $image = Image::first();\n\n        $resp = $this->get(\"/images/edit/{$image->id}\");\n        $this->withHtml($resp)->assertElementExists('button#image-manager-delete');\n\n        $resp = $this->actingAs($viewer)->get(\"/images/edit/{$image->id}\");\n        $this->withHtml($resp)->assertElementNotExists('button#image-manager-delete');\n\n        $this->permissions->grantUserRolePermissions($viewer, ['image-delete-all']);\n\n        $resp = $this->actingAs($viewer)->get(\"/images/edit/{$image->id}\");\n        $this->withHtml($resp)->assertElementExists('button#image-manager-delete');\n\n        $this->files->deleteAtRelativePath($relPath);\n    }\n\n    public function test_image_manager_regen_thumbnails()\n    {\n        $this->asEditor();\n        $imageName = 'first-image.png';\n        $relPath = $this->files->expectedImagePath('gallery', $imageName);\n        $this->files->deleteAtRelativePath($relPath);\n\n        $this->files->uploadGalleryImage($this, $imageName, $this->entities->page()->id);\n        $image = Image::first();\n\n        $resp = $this->get(\"/images/edit/{$image->id}\");\n        $this->withHtml($resp)->assertElementExists('button#image-manager-rebuild-thumbs');\n\n        $expectedThumbPath = dirname($relPath) . '/scaled-1680-/' . basename($relPath);\n        $this->files->deleteAtRelativePath($expectedThumbPath);\n        $this->assertFileDoesNotExist($this->files->relativeToFullPath($expectedThumbPath));\n\n        $resp = $this->put(\"/images/{$image->id}/rebuild-thumbnails\");\n        $resp->assertOk();\n\n        $this->assertFileExists($this->files->relativeToFullPath($expectedThumbPath));\n        $this->files->deleteAtRelativePath($relPath);\n    }\n\n    public function test_gif_thumbnail_generation()\n    {\n        $this->asAdmin();\n        $originalFile = $this->files->testFilePath('animated.gif');\n        $originalFileSize = filesize($originalFile);\n\n        $imgDetails = $this->files->uploadGalleryImageToPage($this, $this->entities->page(), 'animated.gif');\n        $relPath = $imgDetails['path'];\n\n        $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image found at path: ' . public_path($relPath));\n        $galleryThumb = $imgDetails['response']->thumbs->gallery;\n        $displayThumb = $imgDetails['response']->thumbs->display;\n\n        // Ensure display thumbnail is original image\n        $this->assertStringEndsWith($imgDetails['path'], $displayThumb);\n        $this->assertStringNotContainsString('thumbs', $displayThumb);\n\n        // Ensure gallery thumbnail is reduced image (single frame)\n        $galleryThumbRelPath = implode('/', array_slice(explode('/', $galleryThumb), 3));\n        $galleryThumbPath = public_path($galleryThumbRelPath);\n        $galleryFileSize = filesize($galleryThumbPath);\n\n        // Basic scan of GIF content to check frame count\n        $originalFrameCount = count(explode(\"\\x00\\x21\\xF9\", file_get_contents($originalFile))) - 1;\n        $galleryFrameCount = count(explode(\"\\x00\\x21\\xF9\", file_get_contents($galleryThumbPath))) - 1;\n\n        $this->files->deleteAtRelativePath($relPath);\n        $this->files->deleteAtRelativePath($galleryThumbRelPath);\n\n        $this->assertNotEquals($originalFileSize, $galleryFileSize);\n        $this->assertEquals(2, $originalFrameCount);\n        $this->assertLessThan(2, $galleryFrameCount);\n    }\n\n    protected function getTestProfileImage()\n    {\n        $imageName = 'profile.png';\n        $relPath = $this->files->expectedImagePath('user', $imageName);\n        $this->files->deleteAtRelativePath($relPath);\n\n        return $this->files->uploadedImage($imageName);\n    }\n\n    public function test_user_image_upload()\n    {\n        $editor = $this->users->editor();\n        $admin = $this->users->admin();\n        $this->actingAs($admin);\n\n        $file = $this->getTestProfileImage();\n        $this->call('PUT', '/settings/users/' . $editor->id, [], [], ['profile_image' => $file], []);\n\n        $this->assertDatabaseHas('images', [\n            'type'        => 'user',\n            'uploaded_to' => $editor->id,\n            'created_by'  => $admin->id,\n        ]);\n    }\n\n    public function test_user_images_deleted_on_user_deletion()\n    {\n        $editor = $this->users->editor();\n        $this->actingAs($editor);\n\n        $file = $this->getTestProfileImage();\n        $this->call('PUT', '/my-account/profile', [], [], ['profile_image' => $file], []);\n\n        $profileImages = Image::where('type', '=', 'user')->where('created_by', '=', $editor->id)->get();\n        $this->assertTrue($profileImages->count() === 1, 'Found profile images does not match upload count');\n\n        $imagePath = public_path($profileImages->first()->path);\n        $this->assertTrue(file_exists($imagePath));\n\n        $userDelete = $this->asAdmin()->delete($editor->getEditUrl());\n        $userDelete->assertStatus(302);\n\n        $this->assertDatabaseMissing('images', [\n            'type'       => 'user',\n            'created_by' => $editor->id,\n        ]);\n        $this->assertDatabaseMissing('images', [\n            'type'        => 'user',\n            'uploaded_to' => $editor->id,\n        ]);\n\n        $this->assertFalse(file_exists($imagePath));\n    }\n\n    public function test_deleted_unused_images()\n    {\n        $page = $this->entities->page();\n        $admin = $this->users->admin();\n        $this->actingAs($admin);\n\n        $imageName = 'unused-image.png';\n        $relPath = $this->files->expectedImagePath('gallery', $imageName);\n        $this->files->deleteAtRelativePath($relPath);\n\n        $upload = $this->files->uploadGalleryImage($this, $imageName, $page->id);\n        $upload->assertStatus(200);\n        $image = Image::where('type', '=', 'gallery')->first();\n\n        $pageRepo = app(PageRepo::class);\n        $pageRepo->update($page, [\n            'name'    => $page->name,\n            'html'    => $page->html . \"<img src=\\\"{$image->url}\\\">\",\n            'summary' => '',\n        ]);\n\n        // Ensure no images are reported as deletable\n        $imageService = app(ImageService::class);\n        $toDelete = $imageService->deleteUnusedImages(true, true);\n        $this->assertCount(0, $toDelete);\n\n        // Save a revision of our page without the image;\n        $pageRepo->update($page, [\n            'name'    => $page->name,\n            'html'    => '<p>Hello</p>',\n            'summary' => '',\n        ]);\n\n        // Ensure revision images are picked up okay\n        $imageService = app(ImageService::class);\n        $toDelete = $imageService->deleteUnusedImages(true, true);\n        $this->assertCount(0, $toDelete);\n        $toDelete = $imageService->deleteUnusedImages(false, true);\n        $this->assertCount(1, $toDelete);\n\n        // Check image is found when revisions are destroyed\n        $page->revisions()->delete();\n        $toDelete = $imageService->deleteUnusedImages(true, true);\n        $this->assertCount(1, $toDelete);\n\n        // Check the image is deleted\n        $absPath = public_path($relPath);\n        $this->assertTrue(file_exists($absPath), \"Existing uploaded file at path {$absPath} exists\");\n        $toDelete = $imageService->deleteUnusedImages(true, false);\n        $this->assertCount(1, $toDelete);\n        $this->assertFalse(file_exists($absPath));\n\n        $this->files->deleteAtRelativePath($relPath);\n    }\n}\n"
  },
  {
    "path": "tests/UrlTest.php",
    "content": "<?php\n\nnamespace Tests;\n\nuse BookStack\\Http\\Request;\n\nclass UrlTest extends TestCase\n{\n    public function test_url_helper_takes_custom_url_into_account()\n    {\n        $this->runWithEnv(['APP_URL' => 'http://example.com/bookstack'], function () {\n            $this->assertEquals('http://example.com/bookstack/books', url('/books'));\n        });\n    }\n\n    public function test_url_helper_sets_correct_scheme_even_when_request_scheme_is_different()\n    {\n        $this->runWithEnv(['APP_URL' => 'https://example.com/'], function () {\n            $this->get('http://example.com/login')->assertSee('https://example.com/dist/styles.css');\n        });\n    }\n\n    public function test_app_url_forces_overrides_on_base_request()\n    {\n        config()->set('app.url', 'https://donkey.example.com:8091/cool/docs');\n\n        // Have to manually get and wrap request in our custom type due to testing mechanics\n        $this->get('/login');\n        $bsRequest = Request::createFrom(request());\n\n        $this->assertEquals('https://donkey.example.com:8091', $bsRequest->getSchemeAndHttpHost());\n        $this->assertEquals('/cool/docs', $bsRequest->getBaseUrl());\n        $this->assertEquals('https://donkey.example.com:8091/cool/docs/login', $bsRequest->getUri());\n    }\n\n    public function test_app_url_without_path_does_not_duplicate_path_slash()\n    {\n        config()->set('app.url', 'https://donkey.example.com');\n\n        // Have to manually get and wrap request in our custom type due to testing mechanics\n        $this->get('/settings');\n        $bsRequest = Request::createFrom(request());\n\n        $this->assertEquals('https://donkey.example.com', $bsRequest->getSchemeAndHttpHost());\n        $this->assertEquals('', $bsRequest->getBaseUrl());\n        $this->assertEquals('/settings', $bsRequest->getPathInfo());\n        $this->assertEquals('https://donkey.example.com/settings', $bsRequest->getUri());\n    }\n}\n"
  },
  {
    "path": "tests/User/RoleManagementTest.php",
    "content": "<?php\n\nnamespace Tests\\User;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Models\\User;\nuse Tests\\TestCase;\n\nclass RoleManagementTest extends TestCase\n{\n    public function test_cannot_delete_admin_role()\n    {\n        $adminRole = Role::getRole('admin');\n        $deletePageUrl = '/settings/roles/delete/' . $adminRole->id;\n\n        $this->asAdmin()->get($deletePageUrl);\n        $this->delete($deletePageUrl)->assertRedirect($deletePageUrl);\n        $this->get($deletePageUrl)->assertSee('cannot be deleted');\n    }\n\n    public function test_role_cannot_be_deleted_if_default()\n    {\n        $newRole = $this->users->createRole();\n        $this->setSettings(['registration-role' => $newRole->id]);\n\n        $deletePageUrl = '/settings/roles/delete/' . $newRole->id;\n        $this->asAdmin()->get($deletePageUrl);\n        $this->delete($deletePageUrl)->assertRedirect($deletePageUrl);\n        $this->get($deletePageUrl)->assertSee('cannot be deleted');\n    }\n\n    public function test_role_create_update_delete_flow()\n    {\n        $testRoleName = 'Test Role';\n        $testRoleDesc = 'a little test description';\n        $testRoleUpdateName = 'An Super Updated role';\n\n        // Creation\n        $resp = $this->asAdmin()->get('/settings/features');\n        $this->withHtml($resp)->assertElementContains('a[href=\"' . url('/settings/roles') . '\"]', 'Roles');\n\n        $resp = $this->get('/settings/roles');\n        $this->withHtml($resp)->assertElementContains('a[href=\"' . url('/settings/roles/new') . '\"]', 'Create New Role');\n\n        $resp = $this->get('/settings/roles/new');\n        $this->withHtml($resp)->assertElementContains('form[action=\"' . url('/settings/roles/new') . '\"]', 'Save Role');\n\n        $resp = $this->post('/settings/roles/new', [\n            'display_name' => $testRoleName,\n            'description'  => $testRoleDesc,\n        ]);\n        $resp->assertRedirect('/settings/roles');\n\n        $resp = $this->get('/settings/roles');\n        $resp->assertSee($testRoleName);\n        $resp->assertSee($testRoleDesc);\n        $this->assertDatabaseHas('roles', [\n            'display_name' => $testRoleName,\n            'description'  => $testRoleDesc,\n            'mfa_enforced' => false,\n        ]);\n\n        /** @var Role $role */\n        $role = Role::query()->where('display_name', '=', $testRoleName)->first();\n\n        // Updating\n        $resp = $this->get('/settings/roles/' . $role->id);\n        $resp->assertSee($testRoleName);\n        $resp->assertSee($testRoleDesc);\n        $this->withHtml($resp)->assertElementContains('form[action=\"' . url('/settings/roles/' . $role->id) . '\"]', 'Save Role');\n\n        $resp = $this->put('/settings/roles/' . $role->id, [\n            'display_name' => $testRoleUpdateName,\n            'description'  => $testRoleDesc,\n            'mfa_enforced' => 'true',\n        ]);\n        $resp->assertRedirect('/settings/roles');\n        $this->assertDatabaseHas('roles', [\n            'display_name' => $testRoleUpdateName,\n            'description'  => $testRoleDesc,\n            'mfa_enforced' => true,\n        ]);\n\n        // Deleting\n        $resp = $this->get('/settings/roles/' . $role->id);\n        $this->withHtml($resp)->assertElementContains('a[href=\"' . url(\"/settings/roles/delete/$role->id\") . '\"]', 'Delete Role');\n\n        $resp = $this->get(\"/settings/roles/delete/$role->id\");\n        $resp->assertSee($testRoleUpdateName);\n        $this->withHtml($resp)->assertElementContains('form[action=\"' . url(\"/settings/roles/delete/$role->id\") . '\"]', 'Confirm');\n\n        $resp = $this->delete(\"/settings/roles/delete/$role->id\");\n        $resp->assertRedirect('/settings/roles');\n        $this->get('/settings/roles')->assertSee('Role successfully deleted');\n        $this->assertActivityExists(ActivityType::ROLE_DELETE);\n    }\n\n    public function test_role_external_auth_id_validation()\n    {\n        config()->set('auth.method', 'oidc');\n        $role = Role::query()->first();\n        $routeByMethod = [\n            'post' => '/settings/roles/new',\n            'put' => \"/settings/roles/{$role->id}\",\n        ];\n\n        foreach ($routeByMethod as $method => $route) {\n            $resp = $this->asAdmin()->get($route);\n            $resp->assertDontSee('The external auth id');\n\n            $resp = $this->asAdmin()->call($method, $route, [\n                'display_name' => 'Test role for auth id validation',\n                'description'  => '',\n                'external_auth_id' => str_repeat('a', 181),\n            ]);\n\n            $resp->assertRedirect($route);\n            $resp = $this->followRedirects($resp);\n            $resp->assertSee('The external auth id may not be greater than 180 characters.');\n        }\n    }\n\n    public function test_admin_role_cannot_be_removed_if_user_last_admin()\n    {\n        /** @var Role $adminRole */\n        $adminRole = Role::query()->where('system_name', '=', 'admin')->first();\n        $adminUser = $this->users->admin();\n        $adminRole->users()->where('id', '!=', $adminUser->id)->delete();\n        $this->assertEquals(1, $adminRole->users()->count());\n\n        $viewerRole = $this->users->viewer()->roles()->first();\n\n        $editUrl = '/settings/users/' . $adminUser->id;\n        $resp = $this->actingAs($adminUser)->put($editUrl, [\n            'name'  => $adminUser->name,\n            'email' => $adminUser->email,\n            'roles' => [\n                'viewer' => strval($viewerRole->id),\n            ],\n        ]);\n\n        $resp->assertRedirect($editUrl);\n\n        $resp = $this->get($editUrl);\n        $resp->assertSee('This user is the only user assigned to the administrator role');\n    }\n\n    public function test_migrate_users_on_delete_works()\n    {\n        $roleA = $this->users->createRole();\n        $roleB = $this->users->createRole();\n        $user = $this->users->viewer();\n        $user->attachRole($roleB);\n\n        $this->assertCount(0, $roleA->users()->get());\n        $this->assertCount(1, $roleB->users()->get());\n\n        $deletePage = $this->asAdmin()->get(\"/settings/roles/delete/$roleB->id\");\n        $this->withHtml($deletePage)->assertElementExists('select[name=migrate_role_id]');\n        $this->asAdmin()->delete(\"/settings/roles/delete/$roleB->id\", [\n            'migrate_role_id' => $roleA->id,\n        ]);\n\n        $this->assertCount(1, $roleA->users()->get());\n        $this->assertEquals($user->id, $roleA->users()->first()->id);\n    }\n\n    public function test_delete_with_empty_migrate_option_works()\n    {\n        $role = $this->users->attachNewRole($this->users->viewer());\n\n        $this->assertCount(1, $role->users()->get());\n\n        $deletePage = $this->asAdmin()->get(\"/settings/roles/delete/$role->id\");\n        $this->withHtml($deletePage)->assertElementExists('select[name=migrate_role_id]');\n        $resp = $this->asAdmin()->delete(\"/settings/roles/delete/$role->id\", [\n            'migrate_role_id' => '',\n        ]);\n\n        $resp->assertRedirect('/settings/roles');\n        $this->assertDatabaseMissing('roles', ['id' => $role->id]);\n    }\n\n    public function test_entity_permissions_are_removed_on_delete()\n    {\n        /** @var Role $roleA */\n        $roleA = Role::query()->create(['display_name' => 'Entity Permissions Delete Test']);\n        $page = $this->entities->page();\n\n        $this->permissions->setEntityPermissions($page, ['view'], [$roleA]);\n\n        $this->assertDatabaseHas('entity_permissions', [\n            'role_id' => $roleA->id,\n            'entity_id' => $page->id,\n            'entity_type' => $page->getMorphClass(),\n        ]);\n\n        $this->asAdmin()->delete(\"/settings/roles/delete/$roleA->id\");\n\n        $this->assertDatabaseMissing('entity_permissions', [\n            'role_id' => $roleA->id,\n            'entity_id' => $page->id,\n            'entity_type' => $page->getMorphClass(),\n        ]);\n    }\n\n    public function test_image_view_notice_shown_on_role_form()\n    {\n        /** @var Role $role */\n        $role = Role::query()->first();\n        $this->asAdmin()->get(\"/settings/roles/{$role->id}\")\n            ->assertSee('Actual access of uploaded image files will be dependant upon system image storage option');\n    }\n\n    public function test_copy_role_button_shown()\n    {\n        /** @var Role $role */\n        $role = Role::query()->first();\n        $resp = $this->asAdmin()->get(\"/settings/roles/{$role->id}\");\n        $this->withHtml($resp)->assertElementContains('a[href$=\"/roles/new?copy_from=' . $role->id . '\"]', 'Copy');\n    }\n\n    public function test_copy_from_param_on_create_prefills_with_other_role_data()\n    {\n        /** @var Role $role */\n        $role = Role::query()->first();\n        $resp = $this->asAdmin()->get(\"/settings/roles/new?copy_from={$role->id}\");\n        $resp->assertOk();\n        $this->withHtml($resp)->assertElementExists('input[name=\"display_name\"][value=\"' . ($role->display_name . ' (Copy)') . '\"]');\n    }\n\n    public function test_public_role_visible_in_user_edit_screen()\n    {\n        /** @var User $user */\n        $user = User::query()->first();\n        $adminRole = Role::getSystemRole('admin');\n        $publicRole = Role::getSystemRole('public');\n        $resp = $this->asAdmin()->get('/settings/users/' . $user->id);\n        $this->withHtml($resp)->assertElementExists('[name=\"roles[' . $adminRole->id . ']\"]')\n            ->assertElementExists('[name=\"roles[' . $publicRole->id . ']\"]');\n    }\n\n    public function test_public_role_visible_in_role_listing()\n    {\n        $this->asAdmin()->get('/settings/roles')\n            ->assertSee('Admin')\n            ->assertSee('Public');\n    }\n\n    public function test_public_role_visible_in_default_role_setting()\n    {\n        $resp = $this->asAdmin()->get('/settings/registration');\n        $this->withHtml($resp)->assertElementExists('[data-system-role-name=\"admin\"]')\n            ->assertElementExists('[data-system-role-name=\"public\"]');\n    }\n\n    public function test_public_role_not_deletable()\n    {\n        /** @var Role $publicRole */\n        $publicRole = Role::getSystemRole('public');\n        $resp = $this->asAdmin()->delete('/settings/roles/delete/' . $publicRole->id);\n        $resp->assertRedirect('/settings/roles/delete/' . $publicRole->id);\n\n        $this->get('/settings/roles/delete/' . $publicRole->id);\n        $resp = $this->delete('/settings/roles/delete/' . $publicRole->id);\n        $resp->assertRedirect('/settings/roles/delete/' . $publicRole->id);\n        $resp = $this->get('/settings/roles/delete/' . $publicRole->id);\n        $resp->assertSee('This role is a system role and cannot be deleted');\n    }\n\n    public function test_role_permission_removal()\n    {\n        // To cover issue fixed in f99c8ff99aee9beb8c692f36d4b84dc6e651e50a.\n        $page = $this->entities->page();\n        $viewerRole = Role::getRole('viewer');\n        $viewer = $this->users->viewer();\n        $this->actingAs($viewer)->get($page->getUrl())->assertOk();\n\n        $this->asAdmin()->put('/settings/roles/' . $viewerRole->id, [\n            'display_name' => $viewerRole->display_name,\n            'description'  => $viewerRole->description,\n            'permissions'  => [],\n        ])->assertStatus(302);\n\n        $this->actingAs($viewer)->get($page->getUrl())->assertStatus(404);\n    }\n\n    public function test_index_listing_sorting()\n    {\n        $this->asAdmin();\n        $role = $this->users->createRole();\n        $role->display_name = 'zz test role';\n        $role->created_at = now()->addDays(1);\n        $role->save();\n\n        $runTest = function (string $order, string $direction, bool $expectFirstResult) use ($role) {\n            setting()->putForCurrentUser('roles_sort', $order);\n            setting()->putForCurrentUser('roles_sort_order', $direction);\n            $html = $this->withHtml($this->get('/settings/roles'));\n            $selector = \".item-list-row:first-child a[href$=\\\"/roles/{$role->id}\\\"]\";\n            if ($expectFirstResult) {\n                $html->assertElementExists($selector);\n            } else {\n                $html->assertElementNotExists($selector);\n            }\n        };\n\n        $runTest('name', 'asc', false);\n        $runTest('name', 'desc', true);\n        $runTest('created_at', 'desc', true);\n        $runTest('created_at', 'asc', false);\n    }\n}\n"
  },
  {
    "path": "tests/User/UserApiTokenTest.php",
    "content": "<?php\n\nnamespace Tests\\User;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Api\\ApiToken;\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Hash;\nuse Tests\\TestCase;\n\nclass UserApiTokenTest extends TestCase\n{\n    protected array $testTokenData = [\n        'name'       => 'My test API token',\n        'expires_at' => '2050-04-01',\n    ];\n\n    public function test_tokens_section_not_visible_in_my_account_without_access_api_permission()\n    {\n        $user = $this->users->viewer();\n\n        $resp = $this->actingAs($user)->get('/my-account/auth');\n        $resp->assertDontSeeText('API Tokens');\n\n        $this->permissions->grantUserRolePermissions($user, ['access-api']);\n\n        $resp = $this->actingAs($user)->get('/my-account/auth');\n        $resp->assertSeeText('API Tokens');\n        $resp->assertSeeText('Create Token');\n    }\n\n    public function test_those_with_manage_users_can_view_other_user_tokens_but_not_create()\n    {\n        $viewer = $this->users->viewer();\n        $editor = $this->users->editor();\n        $this->permissions->grantUserRolePermissions($viewer, ['users-manage']);\n\n        $resp = $this->actingAs($viewer)->get($editor->getEditUrl());\n        $resp->assertSeeText('API Tokens');\n        $resp->assertDontSeeText('Create Token');\n    }\n\n    public function test_create_api_token()\n    {\n        $editor = $this->users->editor();\n\n        $resp = $this->asAdmin()->get(\"/api-tokens/{$editor->id}/create\");\n        $resp->assertStatus(200);\n        $resp->assertSee('Create API Token');\n        $resp->assertSee('Token Secret');\n\n        $resp = $this->post(\"/api-tokens/{$editor->id}/create\", $this->testTokenData);\n        $token = ApiToken::query()->latest()->first();\n        $resp->assertRedirect(\"/api-tokens/{$editor->id}/{$token->id}\");\n        $this->assertDatabaseHas('api_tokens', [\n            'user_id'    => $editor->id,\n            'name'       => $this->testTokenData['name'],\n            'expires_at' => $this->testTokenData['expires_at'],\n        ]);\n\n        // Check secret token\n        $this->assertSessionHas('api-token-secret:' . $token->id);\n        $secret = session('api-token-secret:' . $token->id);\n        $this->assertDatabaseMissing('api_tokens', [\n            'secret' => $secret,\n        ]);\n        $this->assertTrue(Hash::check($secret, $token->secret));\n\n        $this->assertTrue(strlen($token->token_id) === 32);\n        $this->assertTrue(strlen($secret) === 32);\n\n        $this->assertSessionHas('success');\n        $this->assertActivityExists(ActivityType::API_TOKEN_CREATE);\n    }\n\n    public function test_create_with_no_expiry_sets_expiry_hundred_years_away()\n    {\n        $editor = $this->users->editor();\n\n        $resp = $this->asAdmin()->post(\"/api-tokens/{$editor->id}/create\", ['name' => 'No expiry token', 'expires_at' => '']);\n        $resp->assertRedirect();\n\n        $token = ApiToken::query()->latest()->first();\n\n        $over = Carbon::now()->addYears(101);\n        $under = Carbon::now()->addYears(99);\n        $this->assertTrue(\n            ($token->expires_at < $over && $token->expires_at > $under),\n            'Token expiry set at 100 years in future'\n        );\n    }\n\n    public function test_created_token_displays_on_profile_page()\n    {\n        $editor = $this->users->editor();\n        $resp = $this->asAdmin()->post(\"/api-tokens/{$editor->id}/create\", $this->testTokenData);\n        $resp->assertRedirect();\n\n        $token = ApiToken::query()->latest()->first();\n\n        $resp = $this->get($editor->getEditUrl());\n        $this->withHtml($resp)->assertElementExists('#api_tokens');\n        $this->withHtml($resp)->assertElementContains('#api_tokens', $token->name);\n        $this->withHtml($resp)->assertElementContains('#api_tokens', $token->token_id);\n        $this->withHtml($resp)->assertElementContains('#api_tokens', $token->expires_at->format('Y-m-d'));\n    }\n\n    public function test_secret_shown_once_after_creation()\n    {\n        $editor = $this->users->editor();\n        $resp = $this->asAdmin()->followingRedirects()->post(\"/api-tokens/{$editor->id}/create\", $this->testTokenData);\n        $resp->assertSeeText('Token Secret');\n\n        $token = ApiToken::query()->latest()->first();\n        $this->assertNull(session('api-token-secret:' . $token->id));\n\n        $resp = $this->get(\"/api-tokens/{$editor->id}/{$token->id}\");\n        $resp->assertOk();\n        $resp->assertDontSeeText('Client Secret');\n    }\n\n    public function test_token_update()\n    {\n        $editor = $this->users->editor();\n        $this->asAdmin()->post(\"/api-tokens/{$editor->id}/create\", $this->testTokenData);\n        $token = ApiToken::query()->latest()->first();\n        $updateData = [\n            'name'       => 'My updated token',\n            'expires_at' => '2011-01-01',\n        ];\n\n        $resp = $this->put(\"/api-tokens/{$editor->id}/{$token->id}\", $updateData);\n        $resp->assertRedirect(\"/api-tokens/{$editor->id}/{$token->id}\");\n\n        $this->assertDatabaseHas('api_tokens', array_merge($updateData, ['id' => $token->id]));\n        $this->assertSessionHas('success');\n        $this->assertActivityExists(ActivityType::API_TOKEN_UPDATE);\n    }\n\n    public function test_token_update_with_blank_expiry_sets_to_hundred_years_away()\n    {\n        $editor = $this->users->editor();\n        $this->asAdmin()->post(\"/api-tokens/{$editor->id}/create\", $this->testTokenData);\n        $token = ApiToken::query()->latest()->first();\n\n        $this->put(\"/api-tokens/{$editor->id}/{$token->id}\", [\n            'name'       => 'My updated token',\n            'expires_at' => '',\n        ])->assertRedirect();\n        $token->refresh();\n\n        $over = Carbon::now()->addYears(101);\n        $under = Carbon::now()->addYears(99);\n        $this->assertTrue(\n            ($token->expires_at < $over && $token->expires_at > $under),\n            'Token expiry set at 100 years in future'\n        );\n    }\n\n    public function test_token_delete()\n    {\n        $editor = $this->users->editor();\n        $this->asAdmin()->post(\"/api-tokens/{$editor->id}/create\", $this->testTokenData);\n        $token = ApiToken::query()->latest()->first();\n\n        $tokenUrl = \"/api-tokens/{$editor->id}/{$token->id}\";\n\n        $resp = $this->get($tokenUrl . '/delete');\n        $resp->assertSeeText('Delete Token');\n        $resp->assertSeeText($token->name);\n        $this->withHtml($resp)->assertElementExists('form[action$=\"' . $tokenUrl . '\"]');\n\n        $resp = $this->delete($tokenUrl);\n        $resp->assertRedirect($editor->getEditUrl('#api_tokens'));\n        $this->assertDatabaseMissing('api_tokens', ['id' => $token->id]);\n        $this->assertActivityExists(ActivityType::API_TOKEN_DELETE);\n    }\n\n    public function test_user_manage_can_delete_token_without_api_permission_themselves()\n    {\n        $viewer = $this->users->viewer();\n        $editor = $this->users->editor();\n        $this->permissions->grantUserRolePermissions($editor, ['users-manage']);\n\n        $this->asAdmin()->post(\"/api-tokens/{$viewer->id}/create\", $this->testTokenData);\n        $token = ApiToken::query()->latest()->first();\n\n        $resp = $this->actingAs($editor)->get(\"/api-tokens/{$viewer->id}/{$token->id}\");\n        $resp->assertStatus(200);\n        $resp->assertSeeText('Delete Token');\n\n        $resp = $this->actingAs($editor)->delete(\"/api-tokens/{$viewer->id}/{$token->id}\");\n        $resp->assertRedirect($viewer->getEditUrl('#api_tokens'));\n        $this->assertDatabaseMissing('api_tokens', ['id' => $token->id]);\n    }\n\n    public function test_return_routes_change_depending_on_entry_context()\n    {\n        $user = $this->users->admin();\n        $returnByContext = [\n            'settings' => url(\"/settings/users/{$user->id}/#api_tokens\"),\n            'my-account' => url('/my-account/auth#api_tokens'),\n        ];\n\n        foreach ($returnByContext as $context => $returnUrl) {\n            $resp = $this->actingAs($user)->get(\"/api-tokens/{$user->id}/create?context={$context}\");\n            $this->withHtml($resp)->assertLinkExists($returnUrl, 'Cancel');\n\n            $this->post(\"/api-tokens/{$user->id}/create\", $this->testTokenData);\n            $token = $user->apiTokens()->latest()->first();\n\n            $resp = $this->get($token->getUrl());\n            $this->withHtml($resp)->assertLinkExists($returnUrl, 'Back');\n\n            $resp = $this->delete($token->getUrl());\n            $resp->assertRedirect($returnUrl);\n        }\n    }\n\n    public function test_context_assumed_for_editing_tokens_of_another_user()\n    {\n        $user = $this->users->viewer();\n\n        $resp = $this->asAdmin()->get(\"/api-tokens/{$user->id}/create?context=my-account\");\n        $this->withHtml($resp)->assertLinkExists($user->getEditUrl('#api_tokens'), 'Cancel');\n    }\n}\n"
  },
  {
    "path": "tests/User/UserManagementTest.php",
    "content": "<?php\n\nnamespace Tests\\User;\n\nuse BookStack\\Access\\Mfa\\MfaValue;\nuse BookStack\\Access\\SocialAccount;\nuse BookStack\\Access\\UserInviteException;\nuse BookStack\\Access\\UserInviteService;\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Activity\\Models\\Activity;\nuse BookStack\\Activity\\Models\\Comment;\nuse BookStack\\Activity\\Models\\Favourite;\nuse BookStack\\Activity\\Models\\View;\nuse BookStack\\Activity\\Models\\Watch;\nuse BookStack\\Api\\ApiToken;\nuse BookStack\\Entities\\Models\\Deletion;\nuse BookStack\\Entities\\Models\\PageRevision;\nuse BookStack\\Exports\\Import;\nuse BookStack\\Uploads\\Attachment;\nuse BookStack\\Uploads\\Image;\nuse BookStack\\Users\\Models\\Role;\nuse BookStack\\Users\\Models\\User;\nuse Illuminate\\Support\\Facades\\Hash;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Tests\\TestCase;\n\nclass UserManagementTest extends TestCase\n{\n    public function test_user_creation()\n    {\n        /** @var User $user */\n        $user = User::factory()->make();\n        $adminRole = Role::getRole('admin');\n\n        $resp = $this->asAdmin()->get('/settings/users');\n        $this->withHtml($resp)->assertElementContains('a[href=\"' . url('/settings/users/create') . '\"]', 'Add New User');\n\n        $resp = $this->get('/settings/users/create');\n        $this->withHtml($resp)->assertElementContains('form[action=\"' . url('/settings/users/create') . '\"]', 'Save');\n\n        $resp = $this->post('/settings/users/create', [\n            'name' => $user->name,\n            'email' => $user->email,\n            'password' => $user->password,\n            'password-confirm' => $user->password,\n            'roles[' . $adminRole->id . ']' => 'true',\n        ]);\n        $resp->assertRedirect('/settings/users');\n\n        $resp = $this->get('/settings/users');\n        $resp->assertSee($user->name);\n\n        $this->assertDatabaseHas('users', $user->only('name', 'email'));\n\n        $user->refresh();\n        $this->assertStringStartsWith(Str::slug($user->name), $user->slug);\n    }\n\n    public function test_user_updating()\n    {\n        $user = $this->users->viewer();\n        $password = $user->password;\n\n        $resp = $this->asAdmin()->get('/settings/users/' . $user->id);\n        $resp->assertSee($user->email);\n\n        $this->put($user->getEditUrl(), [\n            'name' => 'Barry Scott',\n        ])->assertRedirect('/settings/users');\n\n        $this->assertDatabaseHas('users', ['id' => $user->id, 'name' => 'Barry Scott', 'password' => $password]);\n        $this->assertDatabaseMissing('users', ['name' => $user->name]);\n\n        $user->refresh();\n        $this->assertStringStartsWith(Str::slug($user->name), $user->slug);\n    }\n\n    public function test_user_password_update()\n    {\n        $user = $this->users->viewer();\n        $userProfilePage = '/settings/users/' . $user->id;\n\n        $this->asAdmin()->get($userProfilePage);\n        $this->put($userProfilePage, [\n            'password' => 'newpassword',\n        ])->assertRedirect($userProfilePage);\n\n        $this->get($userProfilePage)->assertSee('Password confirmation required');\n\n        $this->put($userProfilePage, [\n            'password' => 'newpassword',\n            'password-confirm' => 'newpassword',\n        ])->assertRedirect('/settings/users');\n\n        $userPassword = User::query()->find($user->id)->password;\n        $this->assertTrue(Hash::check('newpassword', $userPassword));\n    }\n\n    public function test_user_can_be_updated_with_single_char_name()\n    {\n        $user = $this->users->viewer();\n        $this->asAdmin()->put(\"/settings/users/{$user->id}\", [\n            'name' => 'b'\n        ])->assertRedirect('/settings/users');\n\n        $this->assertEquals('b', $user->refresh()->name);\n    }\n\n    public function test_user_cannot_be_deleted_if_last_admin()\n    {\n        $adminRole = Role::getRole('admin');\n\n        // Delete all but one admin user if there are more than one\n        $adminUsers = $adminRole->users;\n        if (count($adminUsers) > 1) {\n            /** @var User $user */\n            foreach ($adminUsers->splice(1) as $user) {\n                $user->delete();\n            }\n        }\n\n        // Ensure we currently only have 1 admin user\n        $this->assertEquals(1, $adminRole->users()->count());\n        /** @var User $user */\n        $user = $adminRole->users->first();\n\n        $resp = $this->asAdmin()->delete('/settings/users/' . $user->id);\n        $resp->assertRedirect('/settings/users/' . $user->id);\n\n        $resp = $this->get('/settings/users/' . $user->id);\n        $resp->assertSee('You cannot delete the only admin');\n\n        $this->assertDatabaseHas('users', ['id' => $user->id]);\n    }\n\n    public function test_delete()\n    {\n        $editor = $this->users->editor();\n        $resp = $this->asAdmin()->delete(\"settings/users/{$editor->id}\");\n        $resp->assertRedirect('/settings/users');\n        $resp = $this->followRedirects($resp);\n\n        $resp->assertSee('User successfully removed');\n        $this->assertActivityExists(ActivityType::USER_DELETE);\n\n        $this->assertDatabaseMissing('users', ['id' => $editor->id]);\n    }\n\n    public function test_delete_offers_migrate_option()\n    {\n        $editor = $this->users->editor();\n        $resp = $this->asAdmin()->get(\"settings/users/{$editor->id}/delete\");\n        $resp->assertSee('Migrate Ownership');\n        $resp->assertSee('new_owner_id');\n    }\n\n    public function test_migrate_option_hidden_if_user_cannot_manage_users()\n    {\n        $editor = $this->users->editor();\n\n        $resp = $this->asEditor()->get(\"settings/users/{$editor->id}/delete\");\n        $resp->assertDontSee('Migrate Ownership');\n        $resp->assertDontSee('new_owner_id');\n\n        $this->permissions->grantUserRolePermissions($editor, ['users-manage']);\n\n        $resp = $this->asEditor()->get(\"settings/users/{$editor->id}/delete\");\n        $resp->assertSee('Migrate Ownership');\n        $this->withHtml($resp)->assertElementExists('form input[name=\"new_owner_id\"]');\n        $resp->assertSee('new_owner_id');\n    }\n\n    public function test_delete_with_new_owner_id_changes_ownership()\n    {\n        $page = $this->entities->page();\n        $owner = $page->ownedBy;\n        $newOwner = User::query()->where('id', '!=', $owner->id)->first();\n\n        $this->asAdmin()->delete(\"settings/users/{$owner->id}\", ['new_owner_id' => $newOwner->id])->assertRedirect();\n        $this->assertDatabaseHasEntityData('page', [\n            'id' => $page->id,\n            'owned_by' => $newOwner->id,\n        ]);\n    }\n\n    public function test_delete_with_empty_owner_migration_id_works()\n    {\n        $user = $this->users->editor();\n\n        $resp = $this->asAdmin()->delete(\"settings/users/{$user->id}\", ['new_owner_id' => '']);\n        $resp->assertRedirect('/settings/users');\n        $this->assertActivityExists(ActivityType::USER_DELETE);\n        $this->assertSessionHas('success');\n    }\n\n    public function test_delete_with_empty_owner_migration_id_clears_relevant_id_uses()\n    {\n        $user = $this->users->editor();\n        $page = $this->entities->page();\n        $this->actingAs($user);\n\n        // Create relations\n        $activity = Activity::factory()->create(['user_id' => $user->id]);\n        $attachment = Attachment::factory()->create(['created_by' => $user->id, 'updated_by' => $user->id]);\n        $comment = Comment::factory()->create(['created_by' => $user->id, 'updated_by' => $user->id]);\n        $deletion = Deletion::factory()->create(['deleted_by' => $user->id]);\n        $page->forceFill(['owned_by' => $user->id, 'created_by' => $user->id, 'updated_by' => $user->id])->save();\n        $page->rebuildPermissions();\n        $image = Image::factory()->create(['created_by' => $user->id, 'updated_by' => $user->id]);\n        $import = Import::factory()->create(['created_by' => $user->id]);\n        $revision = PageRevision::factory()->create(['created_by' => $user->id]);\n\n        $apiToken = ApiToken::factory()->create(['user_id' => $user->id]);\n        \\DB::table('email_confirmations')->insert(['user_id' => $user->id, 'token' => 'abc123']);\n        $favourite = Favourite::factory()->create(['user_id' => $user->id]);\n        $mfaValue = MfaValue::factory()->create(['user_id' => $user->id]);\n        $socialAccount = SocialAccount::factory()->create(['user_id' => $user->id]);\n        \\DB::table('user_invites')->insert(['user_id' => $user->id, 'token' => 'abc123']);\n        View::incrementFor($page);\n        $watch = Watch::factory()->create(['user_id' => $user->id]);\n\n        $userColumnsByTable = [\n            'api_tokens' => ['user_id'],\n            'attachments' => ['created_by', 'updated_by'],\n            'comments' => ['created_by', 'updated_by'],\n            'deletions' => ['deleted_by'],\n            'email_confirmations' => ['user_id'],\n            'entities' => ['created_by', 'updated_by', 'owned_by'],\n            'favourites' => ['user_id'],\n            'images' => ['created_by', 'updated_by'],\n            'imports' => ['created_by'],\n            'joint_permissions' => ['owner_id'],\n            'mfa_values' => ['user_id'],\n            'page_revisions' => ['created_by'],\n            'role_user' => ['user_id'],\n            'social_accounts' => ['user_id'],\n            'user_invites' => ['user_id'],\n            'views' => ['user_id'],\n            'watches' => ['user_id'],\n        ];\n\n        // Ensure columns have user id before deletion\n        foreach ($userColumnsByTable as $table => $columns) {\n            foreach ($columns as $column) {\n                $this->assertDatabaseHas($table, [$column => $user->id]);\n            }\n        }\n\n        $resp = $this->asAdmin()->delete(\"settings/users/{$user->id}\", ['new_owner_id' => '']);\n        $resp->assertRedirect('/settings/users');\n\n        // Ensure columns missing user id after deletion\n        foreach ($userColumnsByTable as $table => $columns) {\n            foreach ($columns as $column) {\n                $this->assertDatabaseMissing($table, [$column => $user->id]);\n            }\n        }\n\n        // Check models exist where should be retained\n        $this->assertDatabaseHas('attachments', ['id' => $attachment->id, 'created_by' => null, 'updated_by' => null]);\n        $this->assertDatabaseHas('comments', ['id' => $comment->id, 'created_by' => null, 'updated_by' => null]);\n        $this->assertDatabaseHas('deletions', ['id' => $deletion->id, 'deleted_by' => null]);\n        $this->assertDatabaseHas('entities', ['id' => $page->id, 'created_by' => null, 'updated_by' => null, 'owned_by' => null]);\n        $this->assertDatabaseHas('images', ['id' => $image->id, 'created_by' => null, 'updated_by' => null]);\n        $this->assertDatabaseHas('imports', ['id' => $import->id, 'created_by' => null]);\n        $this->assertDatabaseHas('page_revisions', ['id' => $revision->id, 'created_by' => null]);\n\n        // Check models no longer exist where should have been deleted with the user\n        $this->assertDatabaseMissing('api_tokens', ['id' => $apiToken->id]);\n        $this->assertDatabaseMissing('email_confirmations', ['token' => 'abc123']);\n        $this->assertDatabaseMissing('favourites', ['id' => $favourite->id]);\n        $this->assertDatabaseMissing('mfa_values', ['id' => $mfaValue->id]);\n        $this->assertDatabaseMissing('social_accounts', ['id' => $socialAccount->id]);\n        $this->assertDatabaseMissing('user_invites', ['token' => 'abc123']);\n        $this->assertDatabaseMissing('watches', ['id' => $watch->id]);\n\n        // Ensure activity remains using the old ID (Special case for auditing changes)\n        $this->assertDatabaseHas('activities', ['id' => $activity->id, 'user_id' => $user->id]);\n    }\n\n    public function test_delete_removes_user_preferences()\n    {\n        $editor = $this->users->editor();\n        setting()->putUser($editor, 'dark-mode-enabled', 'true');\n\n        $this->assertDatabaseHas('settings', [\n            'setting_key' => 'user:' . $editor->id . ':dark-mode-enabled',\n            'value' => 'true',\n        ]);\n\n        $this->asAdmin()->delete(\"settings/users/{$editor->id}\");\n\n        $this->assertDatabaseMissing('settings', [\n            'setting_key' => 'user:' . $editor->id . ':dark-mode-enabled',\n        ]);\n    }\n\n    public function test_guest_profile_shows_limited_form()\n    {\n        $guest = $this->users->guest();\n\n        $resp = $this->asAdmin()->get('/settings/users/' . $guest->id);\n        $resp->assertSee('Guest');\n        $html = $this->withHtml($resp);\n\n        $html->assertElementNotExists('#password');\n        $html->assertElementNotExists('[name=\"language\"]');\n    }\n\n    public function test_guest_profile_cannot_be_deleted()\n    {\n        $guestUser = $this->users->guest();\n        $resp = $this->asAdmin()->get('/settings/users/' . $guestUser->id . '/delete');\n        $resp->assertSee('Delete User');\n        $resp->assertSee('Guest');\n        $this->withHtml($resp)->assertElementContains('form[action$=\"/settings/users/' . $guestUser->id . '\"] button', 'Confirm');\n\n        $resp = $this->delete('/settings/users/' . $guestUser->id);\n        $resp->assertRedirect('/settings/users/' . $guestUser->id);\n        $resp = $this->followRedirects($resp);\n        $resp->assertSee('cannot delete the guest user');\n    }\n\n    public function test_user_create_language_reflects_default_system_locale()\n    {\n        $langs = ['en', 'fr', 'hr'];\n        foreach ($langs as $lang) {\n            config()->set('app.default_locale', $lang);\n            $resp = $this->asAdmin()->get('/settings/users/create');\n            $this->withHtml($resp)->assertElementExists('select[name=\"language\"] option[value=\"' . $lang . '\"][selected]');\n        }\n    }\n\n    public function test_user_creation_is_not_performed_if_the_invitation_sending_fails()\n    {\n        /** @var User $user */\n        $user = User::factory()->make();\n        $adminRole = Role::getRole('admin');\n\n        // Simulate an invitation sending failure\n        $this->mock(UserInviteService::class, function (MockInterface $mock) {\n            $mock->shouldReceive('sendInvitation')->once()->andThrow(UserInviteException::class);\n        });\n\n        $this->asAdmin()->post('/settings/users/create', [\n            'name' => $user->name,\n            'email' => $user->email,\n            'send_invite' => 'true',\n            'roles[' . $adminRole->id . ']' => 'true',\n        ]);\n\n        // Since the invitation failed, the user should not exist in the database\n        $this->assertDatabaseMissing('users', $user->only('name', 'email'));\n    }\n\n    public function test_user_create_activity_is_not_persisted_if_the_invitation_sending_fails()\n    {\n        /** @var User $user */\n        $user = User::factory()->make();\n\n        $this->mock(UserInviteService::class, function (MockInterface $mock) {\n            $mock->shouldReceive('sendInvitation')->once()->andThrow(UserInviteException::class);\n        });\n\n        $this->asAdmin()->post('/settings/users/create', [\n            'name' => $user->name,\n            'email' => $user->email,\n            'send_invite' => 'true',\n        ]);\n\n        $this->assertDatabaseMissing('activities', ['type' => 'USER_CREATE']);\n    }\n\n    public function test_return_to_form_with_warning_if_the_invitation_sending_fails()\n    {\n        $logger = $this->withTestLogger();\n        /** @var User $user */\n        $user = User::factory()->make();\n\n        $this->mock(UserInviteService::class, function (MockInterface $mock) {\n            $mock->shouldReceive('sendInvitation')->once()->andThrow(UserInviteException::class);\n        });\n\n        $resp = $this->asAdmin()->post('/settings/users/create', [\n            'name' => $user->name,\n            'email' => $user->email,\n            'send_invite' => 'true',\n        ]);\n\n        $resp->assertRedirect('/settings/users/create');\n        $this->assertSessionError('Could not create user since invite email failed to send');\n        $this->assertEquals($user->email, session()->getOldInput('email'));\n        $this->assertTrue($logger->hasErrorThatContains('Failed to send user invite with error:'));\n    }\n\n    public function test_user_create_update_fails_if_locale_is_invalid()\n    {\n        $user = $this->users->editor();\n\n        // Too long\n        $resp = $this->asAdmin()->put($user->getEditUrl(), ['language' => 'this_is_too_long']);\n        $resp->assertSessionHasErrors(['language' => 'The language may not be greater than 15 characters.']);\n        session()->flush();\n\n        // Invalid characters\n        $resp = $this->put($user->getEditUrl(), ['language' => 'en<GB']);\n        $resp->assertSessionHasErrors(['language' => 'The language may only contain letters, numbers, dashes and underscores.']);\n        session()->flush();\n\n        // Both on create\n        $resp = $this->post('/settings/users/create', [\n            'language' => 'en<GB_and_this_is_longer',\n            'name' => 'My name',\n            'email' => 'jimmy@example.com',\n        ]);\n        $resp->assertSessionHasErrors(['language' => 'The language may not be greater than 15 characters.']);\n        $resp->assertSessionHasErrors(['language' => 'The language may only contain letters, numbers, dashes and underscores.']);\n    }\n\n    public function test_user_avatar_update_and_reset()\n    {\n        $user = $this->users->viewer();\n        $avatarFile = $this->files->uploadedImage('avatar-icon.png');\n\n        $this->assertEquals(0, $user->image_id);\n\n        $upload = $this->asAdmin()->call('PUT', \"/settings/users/{$user->id}\", [\n            'name' => 'Barry Scott',\n        ], [], ['profile_image' => $avatarFile], []);\n        $upload->assertRedirect('/settings/users');\n\n        $user->refresh();\n        $this->assertNotEquals(0, $user->image_id);\n        /** @var Image $image */\n        $image = Image::query()->findOrFail($user->image_id);\n        $this->assertFileExists(public_path($image->path));\n\n        $reset = $this->put(\"/settings/users/{$user->id}\", [\n            'name' => 'Barry Scott',\n            'profile_image_reset' => 'true',\n        ]);\n        $upload->assertRedirect('/settings/users');\n\n        $user->refresh();\n        $this->assertFileDoesNotExist(public_path($image->path));\n        $this->assertEquals(0, $user->image_id);\n    }\n}\n"
  },
  {
    "path": "tests/User/UserMyAccountTest.php",
    "content": "<?php\n\nnamespace Tests\\User;\n\nuse BookStack\\Access\\Mfa\\MfaValue;\nuse BookStack\\Activity\\Tools\\UserEntityWatchOptions;\nuse BookStack\\Activity\\WatchLevels;\nuse BookStack\\Api\\ApiToken;\nuse BookStack\\Uploads\\Image;\nuse Illuminate\\Support\\Facades\\Hash;\nuse Illuminate\\Support\\Str;\nuse Tests\\TestCase;\n\nclass UserMyAccountTest extends TestCase\n{\n    public function test_index_view()\n    {\n        $resp = $this->asEditor()->get('/my-account');\n        $resp->assertRedirect('/my-account/profile');\n    }\n\n    public function test_views_not_accessible_to_guest_user()\n    {\n        $categories = ['profile', 'auth', 'shortcuts', 'notifications', ''];\n        $this->setSettings(['app-public' => 'true']);\n\n        $this->permissions->grantUserRolePermissions($this->users->guest(), ['receive-notifications']);\n\n        foreach ($categories as $category) {\n            $resp = $this->get('/my-account/' . $category);\n            $resp->assertRedirect('/');\n        }\n    }\n\n    public function test_profile_updating()\n    {\n        $editor = $this->users->editor();\n\n        $resp = $this->actingAs($editor)->get('/my-account/profile');\n        $resp->assertSee('Profile Details');\n\n        $html = $this->withHtml($resp);\n        $html->assertFieldHasValue('name', $editor->name);\n        $html->assertFieldHasValue('email', $editor->email);\n\n        $resp = $this->put('/my-account/profile', [\n            'name' => 'Barryius',\n            'email' => 'barryius@example.com',\n            'language' => 'fr',\n        ]);\n\n        $resp->assertRedirect('/my-account/profile');\n        $this->assertDatabaseHas('users', [\n            'name' => 'Barryius',\n            'email' => $editor->email, // No email change due to not having permissions\n        ]);\n        $this->assertEquals(setting()->getUser($editor, 'language'), 'fr');\n    }\n\n    public function test_profile_user_avatar_update_and_reset()\n    {\n        $user = $this->users->viewer();\n        $avatarFile = $this->files->uploadedImage('avatar-icon.png');\n\n        $this->assertEquals(0, $user->image_id);\n\n        $upload = $this->actingAs($user)->call('PUT', \"/my-account/profile\", [\n            'name' => 'Barry Scott',\n        ], [], ['profile_image' => $avatarFile], []);\n        $upload->assertRedirect('/my-account/profile');\n\n\n        $user->refresh();\n        $this->assertNotEquals(0, $user->image_id);\n        /** @var Image $image */\n        $image = Image::query()->findOrFail($user->image_id);\n        $this->assertFileExists(public_path($image->path));\n\n        $reset = $this->put(\"/my-account/profile\", [\n            'name' => 'Barry Scott',\n            'profile_image_reset' => 'true',\n        ]);\n        $upload->assertRedirect('/my-account/profile');\n\n        $user->refresh();\n        $this->assertFileDoesNotExist(public_path($image->path));\n        $this->assertEquals(0, $user->image_id);\n    }\n\n    public function test_profile_admin_options_link_shows_if_permissions_allow()\n    {\n        $editor = $this->users->editor();\n\n        $resp = $this->actingAs($editor)->get('/my-account/profile');\n        $resp->assertDontSee('Administrator Options');\n        $this->withHtml($resp)->assertLinkNotExists(url(\"/settings/users/{$editor->id}\"));\n\n        $this->permissions->grantUserRolePermissions($editor, ['users-manage']);\n\n        $resp = $this->actingAs($editor)->get('/my-account/profile');\n        $resp->assertSee('Administrator Options');\n        $this->withHtml($resp)->assertLinkExists(url(\"/settings/users/{$editor->id}\"));\n    }\n\n    public function test_profile_self_delete()\n    {\n        $editor = $this->users->editor();\n\n        $resp = $this->actingAs($editor)->get('/my-account/profile');\n        $this->withHtml($resp)->assertLinkExists(url('/my-account/delete'), 'Delete Account');\n\n        $resp = $this->get('/my-account/delete');\n        $resp->assertSee('Delete My Account');\n        $this->withHtml($resp)->assertElementContains('form[action$=\"/my-account\"] button', 'Confirm');\n\n        $resp = $this->delete('/my-account');\n        $resp->assertRedirect('/');\n\n        $this->assertDatabaseMissing('users', ['id' => $editor->id]);\n    }\n\n    public function test_profile_self_delete_shows_ownership_migration_if_can_manage_users()\n    {\n        $editor = $this->users->editor();\n\n        $resp = $this->actingAs($editor)->get('/my-account/delete');\n        $resp->assertDontSee('Migrate Ownership');\n\n        $this->permissions->grantUserRolePermissions($editor, ['users-manage']);\n\n        $resp = $this->actingAs($editor)->get('/my-account/delete');\n        $resp->assertSee('Migrate Ownership');\n    }\n\n    public function test_auth_password_change()\n    {\n        $editor = $this->users->editor();\n\n        $resp = $this->actingAs($editor)->get('/my-account/auth');\n        $resp->assertSee('Change Password');\n        $this->withHtml($resp)->assertElementExists('form[action$=\"/my-account/auth/password\"]');\n\n        $password = Str::random();\n        $resp = $this->put('/my-account/auth/password', [\n            'password' => $password,\n            'password-confirm' => $password,\n        ]);\n        $resp->assertRedirect('/my-account/auth');\n\n        $editor->refresh();\n        $this->assertTrue(Hash::check($password, $editor->password));\n    }\n\n    public function test_auth_password_change_hides_if_not_using_email_auth()\n    {\n        $editor = $this->users->editor();\n\n        $resp = $this->actingAs($editor)->get('/my-account/auth');\n        $resp->assertSee('Change Password');\n\n        config()->set('auth.method', 'oidc');\n\n        $resp = $this->actingAs($editor)->get('/my-account/auth');\n        $resp->assertDontSee('Change Password');\n    }\n\n    public function test_auth_page_has_mfa_links()\n    {\n        $editor = $this->users->editor();\n        $resp = $this->actingAs($editor)->get('/my-account/auth');\n        $resp->assertSee('0 methods configured');\n        $this->withHtml($resp)->assertLinkExists(url('/mfa/setup'));\n\n        MfaValue::upsertWithValue($editor, 'totp', 'testval');\n\n        $resp = $this->get('/my-account/auth');\n        $resp->assertSee('1 method configured');\n    }\n\n    public function test_auth_page_api_tokens()\n    {\n        $editor = $this->users->editor();\n        $resp = $this->actingAs($editor)->get('/my-account/auth');\n        $resp->assertSee('API Tokens');\n        $this->withHtml($resp)->assertLinkExists(url(\"/api-tokens/{$editor->id}/create?context=my-account\"));\n\n        ApiToken::factory()->create(['user_id' => $editor->id, 'name' => 'My great token']);\n        $editor->unsetRelations();\n\n        $resp = $this->get('/my-account/auth');\n        $resp->assertSee('My great token');\n    }\n\n    public function test_interface_shortcuts_updating()\n    {\n        $this->asEditor();\n\n        // View preferences with defaults\n        $resp = $this->get('/my-account/shortcuts');\n        $resp->assertSee('UI Shortcut Preferences');\n\n        $html = $this->withHtml($resp);\n        $html->assertFieldHasValue('enabled', 'false');\n        $html->assertFieldHasValue('shortcut[home_view]', '1');\n\n        // Update preferences\n        $resp = $this->put('/my-account/shortcuts', [\n            'enabled' => 'true',\n            'shortcut' => ['home_view' => 'Ctrl + 1'],\n        ]);\n\n        $resp->assertRedirect('/my-account/shortcuts');\n        $resp->assertSessionHas('success', 'Shortcut preferences have been updated!');\n\n        // View updates to preferences page\n        $resp = $this->get('/my-account/shortcuts');\n        $html = $this->withHtml($resp);\n        $html->assertFieldHasValue('enabled', 'true');\n        $html->assertFieldHasValue('shortcut[home_view]', 'Ctrl + 1');\n    }\n\n    public function test_body_has_shortcuts_component_when_active()\n    {\n        $editor = $this->users->editor();\n        $this->actingAs($editor);\n\n        $this->withHtml($this->get('/'))->assertElementNotExists('body[component=\"shortcuts\"]');\n\n        setting()->putUser($editor, 'ui-shortcuts-enabled', 'true');\n        $this->withHtml($this->get('/'))->assertElementExists('body[component=\"shortcuts\"]');\n    }\n\n    public function test_notification_routes_requires_notification_permission()\n    {\n        $viewer = $this->users->viewer();\n        $resp = $this->actingAs($viewer)->get('/my-account/notifications');\n        $this->assertPermissionError($resp);\n\n        $resp = $this->actingAs($viewer)->get('/my-account/profile');\n        $resp->assertDontSeeText('Notification Preferences');\n\n        $resp = $this->put('/my-account/notifications');\n        $this->assertPermissionError($resp);\n\n        $this->permissions->grantUserRolePermissions($viewer, ['receive-notifications']);\n        $resp = $this->get('/my-account/notifications');\n        $resp->assertOk();\n        $resp->assertSee('Notification Preferences');\n    }\n\n    public function test_notification_preferences_updating()\n    {\n        $editor = $this->users->editor();\n\n        // View preferences with defaults\n        $resp = $this->actingAs($editor)->get('/my-account/notifications');\n        $resp->assertSee('Notification Preferences');\n\n        $html = $this->withHtml($resp);\n        $html->assertFieldHasValue('preferences[comment-replies]', 'false');\n\n        // Update preferences\n        $resp = $this->put('/my-account/notifications', [\n            'preferences' => ['comment-replies' => 'true'],\n        ]);\n\n        $resp->assertRedirect('/my-account/notifications');\n        $resp->assertSessionHas('success', 'Notification preferences have been updated!');\n\n        // View updates to preferences page\n        $resp = $this->get('/my-account/notifications');\n        $html = $this->withHtml($resp);\n        $html->assertFieldHasValue('preferences[comment-replies]', 'true');\n    }\n\n    public function test_notification_preferences_show_watches()\n    {\n        $editor = $this->users->editor();\n        $book = $this->entities->book();\n\n        $options = new UserEntityWatchOptions($editor, $book);\n        $options->updateLevelByValue(WatchLevels::COMMENTS);\n\n        $resp = $this->actingAs($editor)->get('/my-account/notifications');\n        $resp->assertSee($book->name);\n        $resp->assertSee('All Page Updates & Comments');\n\n        $options->updateLevelByValue(WatchLevels::DEFAULT);\n\n        $resp = $this->actingAs($editor)->get('/my-account/notifications');\n        $resp->assertDontSee($book->name);\n        $resp->assertDontSee('All Page Updates & Comments');\n    }\n\n    public function test_notification_preferences_dont_error_on_deleted_items()\n    {\n        $editor = $this->users->editor();\n        $book = $this->entities->book();\n\n        $options = new UserEntityWatchOptions($editor, $book);\n        $options->updateLevelByValue(WatchLevels::COMMENTS);\n\n        $this->actingAs($editor)->delete($book->getUrl());\n        $book->refresh();\n        $this->assertNotNull($book->deleted_at);\n\n        $resp = $this->actingAs($editor)->get('/my-account/notifications');\n        $resp->assertOk();\n        $resp->assertDontSee($book->name);\n    }\n\n    public function test_notification_preferences_not_accessible_to_guest()\n    {\n        $this->setSettings(['app-public' => 'true']);\n        $guest = $this->users->guest();\n        $this->permissions->grantUserRolePermissions($guest, ['receive-notifications']);\n\n        $resp = $this->get('/my-account/notifications');\n        $this->assertPermissionError($resp);\n\n        $resp = $this->put('/my-account/notifications', [\n            'preferences' => ['comment-replies' => 'true'],\n        ]);\n        $this->assertPermissionError($resp);\n    }\n\n    public function test_notification_comment_options_only_exist_if_comments_active()\n    {\n        $resp = $this->asEditor()->get('/my-account/notifications');\n        $resp->assertSee('Notify upon comments');\n        $resp->assertSee('Notify upon replies');\n        $resp->assertSee('Notify when I\\'m mentioned in a comment');\n\n        setting()->put('app-disable-comments', true);\n\n        $resp = $this->get('/my-account/notifications');\n        $resp->assertDontSee('Notify upon comments');\n        $resp->assertDontSee('Notify upon replies');\n        $resp->assertDontSee('Notify when I\\'m mentioned in a comment');\n    }\n\n    public function test_notification_comment_mention_option_enabled_by_default()\n    {\n        $resp = $this->asEditor()->get('/my-account/notifications');\n        $this->withHtml($resp)->assertElementExists('input[name=\"preferences[comment-mentions]\"][value=\"true\"]');\n    }\n}\n"
  },
  {
    "path": "tests/User/UserPreferencesTest.php",
    "content": "<?php\n\nnamespace Tests\\User;\n\nuse Tests\\TestCase;\n\nclass UserPreferencesTest extends TestCase\n{\n    public function test_update_sort_preference()\n    {\n        $editor = $this->users->editor();\n        $this->actingAs($editor);\n\n        $updateRequest = $this->patch('/preferences/change-sort/books', [\n            'sort'  => 'created_at',\n            'order' => 'desc',\n        ]);\n        $updateRequest->assertStatus(302);\n\n        $this->assertDatabaseHas('settings', [\n            'setting_key' => 'user:' . $editor->id . ':books_sort',\n            'value'       => 'created_at',\n        ]);\n        $this->assertDatabaseHas('settings', [\n            'setting_key' => 'user:' . $editor->id . ':books_sort_order',\n            'value'       => 'desc',\n        ]);\n        $this->assertEquals('created_at', setting()->getForCurrentUser('books_sort'));\n        $this->assertEquals('desc', setting()->getForCurrentUser('books_sort_order'));\n    }\n\n    public function test_update_sort_bad_entity_type_handled()\n    {\n        $editor = $this->users->editor();\n        $this->actingAs($editor);\n\n        $updateRequest = $this->patch('/preferences/change-sort/dogs', [\n            'sort'  => 'name',\n            'order' => 'asc',\n        ]);\n        $updateRequest->assertRedirect();\n\n        $this->assertNotEmpty('name', setting()->getForCurrentUser('bookshelves_sort'));\n        $this->assertNotEmpty('asc', setting()->getForCurrentUser('bookshelves_sort_order'));\n    }\n\n    public function test_update_expansion_preference()\n    {\n        $editor = $this->users->editor();\n        $this->actingAs($editor);\n\n        $updateRequest = $this->patch('/preferences/change-expansion/home-details', ['expand' => 'true']);\n        $updateRequest->assertStatus(204);\n\n        $this->assertDatabaseHas('settings', [\n            'setting_key' => 'user:' . $editor->id . ':section_expansion#home-details',\n            'value'       => 'true',\n        ]);\n        $this->assertEquals(true, setting()->getForCurrentUser('section_expansion#home-details'));\n\n        $invalidKeyRequest = $this->patch('/preferences/change-expansion/my-home-details', ['expand' => 'true']);\n        $invalidKeyRequest->assertStatus(500);\n    }\n\n    public function test_toggle_dark_mode()\n    {\n        $home = $this->actingAs($this->users->editor())->get('/');\n        $home->assertSee('Dark Mode');\n        $this->withHtml($home)->assertElementNotExists('.dark-mode');\n\n        $this->assertEquals(false, setting()->getForCurrentUser('dark-mode-enabled', false));\n        $prefChange = $this->patch('/preferences/toggle-dark-mode');\n        $prefChange->assertRedirect();\n        $this->assertEquals(true, setting()->getForCurrentUser('dark-mode-enabled'));\n\n        $home = $this->actingAs($this->users->editor())->get('/');\n        $this->withHtml($home)->assertElementExists('.dark-mode');\n        $home->assertDontSee('Dark Mode');\n        $home->assertSee('Light Mode');\n    }\n\n    public function test_dark_mode_defaults_to_config_option()\n    {\n        config()->set('setting-defaults.user.dark-mode-enabled', false);\n        $this->assertEquals(false, setting()->getForCurrentUser('dark-mode-enabled'));\n        $home = $this->get('/login');\n        $this->withHtml($home)->assertElementNotExists('.dark-mode');\n\n        config()->set('setting-defaults.user.dark-mode-enabled', true);\n        $this->assertEquals(true, setting()->getForCurrentUser('dark-mode-enabled'));\n        $home = $this->get('/login');\n        $this->withHtml($home)->assertElementExists('.dark-mode');\n    }\n\n    public function test_dark_mode_toggle_endpoint_changes_to_light_when_dark_by_default()\n    {\n        config()->set('setting-defaults.user.dark-mode-enabled', true);\n        $editor = $this->users->editor();\n\n        $this->assertEquals(true, setting()->getUser($editor, 'dark-mode-enabled'));\n        $prefChange = $this->actingAs($editor)->patch('/preferences/toggle-dark-mode');\n        $prefChange->assertRedirect();\n        $this->assertEquals(false, setting()->getUser($editor, 'dark-mode-enabled'));\n\n        $home = $this->get('/');\n        $this->withHtml($home)->assertElementNotExists('.dark-mode');\n        $home->assertDontSee('Light Mode');\n        $home->assertSee('Dark Mode');\n    }\n\n    public function test_books_view_type_preferences_when_list()\n    {\n        $editor = $this->users->editor();\n        setting()->putUser($editor, 'books_view_type', 'list');\n\n        $resp = $this->actingAs($editor)->get('/books');\n        $this->withHtml($resp)\n            ->assertElementNotExists('.featured-image-container')\n            ->assertElementExists('.content-wrap .entity-list-item');\n    }\n\n    public function test_books_view_type_preferences_when_grid()\n    {\n        $editor = $this->users->editor();\n        setting()->putUser($editor, 'books_view_type', 'grid');\n\n        $resp = $this->actingAs($editor)->get('/books');\n        $this->withHtml($resp)->assertElementExists('.featured-image-container');\n    }\n\n    public function test_shelf_view_type_change()\n    {\n        $editor = $this->users->editor();\n        $shelf = $this->entities->shelf();\n        setting()->putUser($editor, 'bookshelf_view_type', 'list');\n\n        $resp = $this->actingAs($editor)->get($shelf->getUrl())->assertSee('Grid View');\n        $this->withHtml($resp)\n            ->assertElementNotExists('.featured-image-container')\n            ->assertElementExists('.content-wrap .entity-list-item');\n\n        $req = $this->patch(\"/preferences/change-view/bookshelf\", [\n            'view' => 'grid',\n            '_return' => $shelf->getUrl(),\n        ]);\n        $req->assertRedirect($shelf->getUrl());\n\n        $resp = $this->actingAs($editor)->get($shelf->getUrl())\n            ->assertSee('List View');\n\n        $this->withHtml($resp)\n            ->assertElementExists('.featured-image-container')\n            ->assertElementNotExists('.content-wrap .entity-list-item');\n    }\n\n    public function test_redirect_on_preference_change_checks_host()\n    {\n        $expectedByRedirect = [\n            'http://localhost/beans' => 'http://localhost/beans',\n            'https://localhost/beans' => 'http://localhost',\n            'http://localhost:9090/beans' => 'http://localhost',\n            'http://localhost.example.com/beans' => 'http://localhost',\n            'http://localhost@example.com/beans' => 'http://localhost',\n        ];\n\n        $this->asEditor();\n        foreach ($expectedByRedirect as $url => $expected) {\n            $req = $this->patch(\"/preferences/change-view/bookshelf\", [\n                'view' => 'grid',\n                '_return' => $url,\n            ]);\n            $req->assertRedirect($expected);\n        }\n    }\n\n    public function test_update_code_language_favourite()\n    {\n        $editor = $this->users->editor();\n        $page = $this->entities->page();\n        $this->actingAs($editor);\n\n        $this->patch('/preferences/update-code-language-favourite', ['language' => 'php', 'active' => true]);\n        $this->patch('/preferences/update-code-language-favourite', ['language' => 'javascript', 'active' => true]);\n\n        $resp = $this->get($page->getUrl('/edit'));\n        $resp->assertSee('option:code-editor:favourites=\"php,javascript\"', false);\n\n        $this->patch('/preferences/update-code-language-favourite', ['language' => 'ruby', 'active' => true]);\n        $this->patch('/preferences/update-code-language-favourite', ['language' => 'php', 'active' => false]);\n\n        $resp = $this->get($page->getUrl('/edit'));\n        $resp->assertSee('option:code-editor:favourites=\"javascript,ruby\"', false);\n    }\n}\n"
  },
  {
    "path": "tests/User/UserProfileTest.php",
    "content": "<?php\n\nnamespace Tests\\User;\n\nuse BookStack\\Activity\\ActivityType;\nuse BookStack\\Facades\\Activity;\nuse BookStack\\Users\\Models\\User;\nuse Tests\\TestCase;\n\nclass UserProfileTest extends TestCase\n{\n    /**\n     * @var User\n     */\n    protected $user;\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n        $this->user = User::all()->last();\n    }\n\n    public function test_profile_page_shows_name()\n    {\n        $this->asAdmin()\n            ->get('/user/' . $this->user->slug)\n            ->assertSee($this->user->name);\n    }\n\n    public function test_profile_page_shows_recent_entities()\n    {\n        $content = $this->entities->createChainBelongingToUser($this->user, $this->user);\n\n        $resp = $this->asAdmin()->get('/user/' . $this->user->slug);\n        // Check the recently created page is shown\n        $resp->assertSee($content['page']->name);\n        // Check the recently created chapter is shown\n        $resp->assertSee($content['chapter']->name);\n        // Check the recently created book is shown\n        $resp->assertSee($content['book']->name);\n    }\n\n    public function test_profile_page_shows_created_content_counts()\n    {\n        $newUser = User::factory()->create();\n\n        $resp = $this->asAdmin()->get('/user/' . $newUser->slug)\n            ->assertSee($newUser->name);\n        $this->withHtml($resp)->assertElementContains('#content-counts', '0 Books')\n            ->assertElementContains('#content-counts', '0 Chapters')\n            ->assertElementContains('#content-counts', '0 Pages');\n\n        $this->entities->createChainBelongingToUser($newUser, $newUser);\n\n        $resp = $this->asAdmin()->get('/user/' . $newUser->slug)\n            ->assertSee($newUser->name);\n        $this->withHtml($resp)->assertElementContains('#content-counts', '1 Book')\n            ->assertElementContains('#content-counts', '1 Chapter')\n            ->assertElementContains('#content-counts', '1 Page');\n    }\n\n    public function test_profile_page_shows_recent_activity()\n    {\n        $newUser = User::factory()->create();\n        $this->actingAs($newUser);\n        $entities = $this->entities->createChainBelongingToUser($newUser, $newUser);\n        Activity::add(ActivityType::BOOK_UPDATE, $entities['book']);\n        Activity::add(ActivityType::PAGE_CREATE, $entities['page']);\n\n        $resp = $this->asAdmin()->get('/user/' . $newUser->slug);\n        $this->withHtml($resp)->assertElementContains('#recent-user-activity', 'updated book')\n            ->assertElementContains('#recent-user-activity', 'created page')\n            ->assertElementContains('#recent-user-activity', $entities['page']->name);\n    }\n\n    public function test_user_activity_has_link_leading_to_profile()\n    {\n        $newUser = User::factory()->create();\n        $this->actingAs($newUser);\n        $entities = $this->entities->createChainBelongingToUser($newUser, $newUser);\n        Activity::add(ActivityType::BOOK_UPDATE, $entities['book']);\n        Activity::add(ActivityType::PAGE_CREATE, $entities['page']);\n\n        $linkSelector = '#recent-activity a[href$=\"/user/' . $newUser->slug . '\"]';\n        $resp = $this->asAdmin()->get('/');\n        $this->withHtml($resp)->assertElementContains($linkSelector, $newUser->name);\n    }\n\n    public function test_profile_has_search_links_in_created_entity_lists()\n    {\n        $user = $this->users->editor();\n        $resp = $this->actingAs($this->users->admin())->get('/user/' . $user->slug);\n\n        $expectedLinks = [\n            '/search?term=%7Bcreated_by%3A' . $user->slug . '%7D+%7Btype%3Apage%7D',\n            '/search?term=%7Bcreated_by%3A' . $user->slug . '%7D+%7Btype%3Achapter%7D',\n            '/search?term=%7Bcreated_by%3A' . $user->slug . '%7D+%7Btype%3Abook%7D',\n            '/search?term=%7Bcreated_by%3A' . $user->slug . '%7D+%7Btype%3Abookshelf%7D',\n        ];\n\n        foreach ($expectedLinks as $link) {\n            $this->withHtml($resp)->assertElementContains('[href$=\"' . $link . '\"]', 'View All');\n        }\n    }\n}\n"
  },
  {
    "path": "tests/User/UserSearchTest.php",
    "content": "<?php\n\nnamespace Tests\\User;\n\nuse BookStack\\Permissions\\Permission;\nuse BookStack\\Users\\Models\\User;\nuse Tests\\TestCase;\n\nclass UserSearchTest extends TestCase\n{\n    public function test_select_search_matches_by_name()\n    {\n        $viewer = $this->users->viewer();\n        $admin = $this->users->admin();\n        $resp = $this->actingAs($admin)->get('/search/users/select?search=' . urlencode($viewer->name));\n\n        $resp->assertOk();\n        $resp->assertSee($viewer->name);\n        $resp->assertDontSee($admin->name);\n    }\n\n    public function test_select_search_shows_first_by_name_without_search()\n    {\n        /** @var User $firstUser */\n        $firstUser = User::query()->orderBy('name', 'desc')->first();\n        $resp = $this->asAdmin()->get('/search/users/select');\n\n        $resp->assertOk();\n        $resp->assertSee($firstUser->name);\n    }\n\n    public function test_select_search_does_not_match_by_email()\n    {\n        $viewer = $this->users->viewer();\n        $editor = $this->users->editor();\n        $resp = $this->actingAs($editor)->get('/search/users/select?search=' . urlencode($viewer->email));\n\n        $resp->assertDontSee($viewer->name);\n    }\n\n    public function test_select_requires_right_permission()\n    {\n        $permissions = ['users-manage', 'restrictions-manage-own', 'restrictions-manage-all'];\n        $user = $this->users->viewer();\n\n        foreach ($permissions as $permission) {\n            $resp = $this->actingAs($user)->get('/search/users/select?search=a');\n            $this->assertPermissionError($resp);\n\n            $this->permissions->grantUserRolePermissions($user, [$permission]);\n            $resp = $this->actingAs($user)->get('/search/users/select?search=a');\n            $resp->assertOk();\n            $user->roles()->delete();\n            $user->clearPermissionCache();\n        }\n    }\n\n    public function test_select_requires_logged_in_user()\n    {\n        $this->setSettings(['app-public' => true]);\n        $this->permissions->grantUserRolePermissions($this->users->guest(), ['users-manage']);\n\n        $resp = $this->get('/search/users/select?search=a');\n        $this->assertPermissionError($resp);\n    }\n\n    public function test_mentions_search_matches_by_name()\n    {\n        $viewer = $this->users->viewer();\n        $editor = $this->users->editor();\n\n        $resp = $this->actingAs($editor)->get('/search/users/mention?search=' . urlencode($viewer->name));\n\n        $resp->assertOk();\n        $resp->assertSee($viewer->name);\n        $resp->assertDontSee($editor->name);\n    }\n\n    public function test_mentions_search_does_not_match_by_email()\n    {\n        $viewer = $this->users->viewer();\n\n        $resp = $this->asEditor()->get('/search/users/mention?search=' . urlencode($viewer->email));\n\n        $resp->assertDontSee($viewer->name);\n    }\n\n    public function test_mentions_search_requires_logged_in_user()\n    {\n        $this->setSettings(['app-public' => true]);\n        $guest = $this->users->guest();\n        $this->permissions->grantUserRolePermissions($guest, [Permission::CommentCreateAll, Permission::CommentUpdateAll]);\n\n        $resp = $this->get('/search/users/mention?search=a');\n        $this->assertPermissionError($resp);\n    }\n\n    public function test_mentions_search_requires_comment_create_or_update_permission()\n    {\n        $viewer = $this->users->viewer();\n        $editor = $this->users->editor();\n\n        $resp = $this->actingAs($viewer)->get('/search/users/mention?search=' . urlencode($editor->name));\n        $this->assertPermissionError($resp);\n\n        $this->permissions->grantUserRolePermissions($viewer, [Permission::CommentCreateAll]);\n\n        $resp = $this->actingAs($editor)->get('/search/users/mention?search=' . urlencode($viewer->name));\n        $resp->assertOk();\n        $resp->assertSee($viewer->name);\n\n        $this->permissions->removeUserRolePermissions($viewer, [Permission::CommentCreateAll]);\n        $this->permissions->grantUserRolePermissions($viewer, [Permission::CommentUpdateAll]);\n\n        $resp = $this->actingAs($editor)->get('/search/users/mention?search=' . urlencode($viewer->name));\n        $resp->assertOk();\n        $resp->assertSee($viewer->name);\n    }\n\n    public function test_mentions_search_shows_first_by_name_without_search()\n    {\n        /** @var User $firstUser */\n        $firstUser = User::query()\n            ->orderBy('name', 'asc')\n            ->first();\n\n        $resp = $this->asEditor()->get('/search/users/mention');\n\n        $resp->assertOk();\n        $this->withHtml($resp)->assertElementContains('a[data-id]:first-child', $firstUser->name);\n    }\n}\n"
  },
  {
    "path": "tests/Util/DateFormatterTest.php",
    "content": "<?php\n\nnamespace Tests\\Util;\n\nuse BookStack\\Util\\DateFormatter;\nuse Carbon\\Carbon;\nuse Tests\\TestCase;\n\nclass DateFormatterTest extends TestCase\n{\n    public function test_iso_with_timezone_alters_from_stored_to_display_timezone()\n    {\n        $formatter = new DateFormatter('Europe/London');\n        $dateTime = new Carbon('2020-06-01 12:00:00', 'UTC');\n\n        $result = $formatter->absolute($dateTime);\n        $this->assertEquals('2020-06-01 13:00:00 BST', $result);\n    }\n\n    public function test_iso_with_timezone_works_from_non_utc_dates()\n    {\n        $formatter = new DateFormatter('Asia/Shanghai');\n        $dateTime = new Carbon('2025-06-10 15:25:00', 'America/New_York');\n\n        $result = $formatter->absolute($dateTime);\n        $this->assertEquals('2025-06-11 03:25:00 CST', $result);\n    }\n\n    public function test_relative()\n    {\n        $formatter = new DateFormatter('Europe/London');\n        $dateTime = (new Carbon('now', 'UTC'))->subMinutes(50);\n\n        $result = $formatter->relative($dateTime);\n        $this->assertEquals('50 minutes ago', $result);\n    }\n}\n"
  },
  {
    "path": "tests/test-data/bad-php.base64",
    "content": "/9j/4AAQSkZJRgABAQEBLAEsAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAAEBAQEBAQEBAQEB\nAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBD\nAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB\nAQEBAQEBAQH/wgARCAABAAEDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACv/EABQBAQAA\nAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhADEAAAAT/n/8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgB\nAQABBQJ//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAwEBPwF//8QAFBEBAAAAAAAAAAAAAAAA\nAAAAAP/aAAgBAgEBPwF//8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQAGPwJ//8QAFBABAAAA\nAAAAAAAAAAAAAAAAAP/aAAgBAQABPyF//9oADAMBAAIAAwAAABAf/8QAFBEBAAAAAAAAAAAAAAAA\nAAAAAP/aAAgBAwEBPxB//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAgEBPxB//8QAFBABAAAA\nAAAAAAAAAAAAAAAAAP/aAAgBAQABPxB//9k8P3BocCBlY2hvICdiYWRwaHAnOwo=\n"
  },
  {
    "path": "tests/test-data/bad-phtml-png.base64",
    "content": "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA\nB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUH\nAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=\n"
  },
  {
    "path": "tests/test-data/bad-phtml.base64",
    "content": "/9j/4AAQSkZJRgABAQEBLAEsAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAAEBAQEBAQEBAQEB\nAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBD\nAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB\nAQEBAQEBAQH/wgARCAABAAEDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACv/EABQBAQAA\nAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhADEAAAAT/n/8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgB\nAQABBQJ//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAwEBPwF//8QAFBEBAAAAAAAAAAAAAAAA\nAAAAAP/aAAgBAgEBPwF//8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQAGPwJ//8QAFBABAAAA\nAAAAAAAAAAAAAAAAAP/aAAgBAQABPyF//9oADAMBAAIAAwAAABAf/8QAFBEBAAAAAAAAAAAAAAAA\nAAAAAP/aAAgBAwEBPxB//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAgEBPxB//8QAFBABAAAA\nAAAAAAAAAAAAAAAAAP/aAAgBAQABPxB//9k8P3BocCBlY2hvICdiYWRwaHAnOwo=\n"
  },
  {
    "path": "tests/test-data/test-file.txt",
    "content": "Hi, This is a test file for testing the upload process."
  },
  {
    "path": "themes/.gitignore",
    "content": "*\n!.gitignore\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"include\": [\"resources/js/**/*\"],\n  \"exclude\": [\"resources/js/wysiwyg/lexical/yjs/*\"],\n  \"compilerOptions\": {\n    \"target\": \"es2022\",\n    \"module\": \"commonjs\",\n    \"rootDir\": \"./resources/js/\",\n    \"baseUrl\": \"./\",\n    \"paths\": {\n      \"@icons/*\": [\"resources/icons/*\"],\n      \"lexical\": [\"resources/js/wysiwyg/lexical/core/index.ts\"],\n      \"lexical/*\": [\"resources/js/wysiwyg/lexical/core/*\"],\n      \"@lexical/*\": [\"resources/js/wysiwyg/lexical/*\"]\n    },\n    \"resolveJsonModule\": true,\n    \"allowJs\": true,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "version",
    "content": "v26.01-dev\n"
  }
]